diff --git a/src/lib/components/AnnouncementDialog.svelte b/src/lib/components/AnnouncementDialog.svelte index d668515..a1ac50b 100644 --- a/src/lib/components/AnnouncementDialog.svelte +++ b/src/lib/components/AnnouncementDialog.svelte @@ -1,24 +1,47 @@ + + + + + + + +
+ +
+
+

Screen Mirror

+ {#if sessionCodec} + + {sessionCodec} + + {/if} + {#if sessionSize} + + {sessionSize} + + {/if} + {#if frameCount > 0} + + {frameCount} frames + + {/if} +
+ + + Close + +
+ + +
+ {#if loading && frameCount === 0 && !error} +
+ {#each stepStates as step (step.key)} +
+ {#if step.isCompleted} +
+ +
+ {:else if step.isActive} +
+ {:else} +
+ {/if} + {step.label} + {#if step.isActive && stepDetail} + — {stepDetail} + {/if} +
+ {/each} +
+ {/if} + {#if error} +
+
+

{error}

+
+ +
+ {:else if !loading && frameCount === 0 && sessionStarted && !stopping} +
+ +
+ {/if} + + {#if touchLocked && frameCount > 0} +
+ + + + + Touch locked +
+ {/if} +
+ + +
+
+ + + +
+ +
+ + +
+
+ {#each logEntries as entry (entry.id)} + {@const iconClass = entry.type === 'ok' + ? 'text-emerald-600 dark:text-emerald-400' + : entry.type === 'error' + ? 'text-red-600 dark:text-red-400' + : entry.type === 'warn' + ? 'text-yellow-600 dark:text-yellow-400' + : 'text-muted-foreground'} + {@const msgClass = entry.type === 'ok' + ? 'text-emerald-700 dark:text-emerald-300' + : entry.type === 'error' + ? 'text-red-700 dark:text-red-300' + : entry.type === 'warn' + ? 'text-yellow-700 dark:text-yellow-300' + : 'text-foreground'} +
+ {entry.time} + + {entry.type === 'ok' ? '✓' : entry.type === 'error' ? '✗' : entry.type === 'warn' ? '!' : '·'} + + + {entry.message} + +
+ {/each} + {#if logEntries.length === 0} + Waiting… + {/if} +
+
+
+
+
+
diff --git a/src/lib/components/ui/button/button.svelte b/src/lib/components/ui/button/button.svelte index b6f80ef..88f03b6 100644 --- a/src/lib/components/ui/button/button.svelte +++ b/src/lib/components/ui/button/button.svelte @@ -13,6 +13,8 @@ ghost: "hover:bg-muted hover:text-foreground dark:hover:bg-muted/50 aria-expanded:bg-muted aria-expanded:text-foreground", destructive: "bg-destructive/10 hover:bg-destructive/20 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/20 text-destructive focus-visible:border-destructive/40 dark:hover:bg-destructive/30", link: "text-primary underline-offset-4 hover:underline", + accept: "bg-emerald-600 text-white shadow-sm hover:bg-emerald-500 dark:bg-emerald-600 dark:hover:bg-emerald-500", + reject: "bg-red-600 text-white shadow-sm hover:bg-red-500 dark:bg-red-600 dark:hover:bg-red-500", }, size: { default: "h-8 gap-1.5 px-2.5 has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2", diff --git a/src/lib/core/adb/adb.ts b/src/lib/core/adb/adb.ts index 0b9192e..cdb93f9 100644 --- a/src/lib/core/adb/adb.ts +++ b/src/lib/core/adb/adb.ts @@ -13,7 +13,7 @@ import { AdbScrcpyClient } from '@yume-chan/adb-scrcpy'; import { addNotification } from '../stores/noti'; import { handleAdbPayload } from '../handlers/adbPayloadHandler'; import { GlobalEventBus } from '../utils/eventBus'; -import { adbWriter } from '../stores/adbWriter'; +import { adbWriter, sendToAndroid } from '../stores/adbWriter'; import { adbReconnect } from '../stores/adbReconnectStore'; import { WritableStream } from '@yume-chan/stream-extra'; import { env } from '$env/dynamic/public'; @@ -27,6 +27,8 @@ let syncConnection: any = null; let worker: Worker | undefined; +let healthCheckRoutine: any; + /** * Centralized connection function that all pages and components should use. * @@ -220,11 +222,9 @@ export async function connnectViaWebUSB(connectAndroidServer = true) { // save device info await deviceCredentialManager.saveDeviceInfo(device); } catch (e: any) { - logger.error('error on connect', e); - if (e instanceof AdbDaemonWebUsbDevice.DeviceBusyError) { addNotification( - 'ERR:Device is already in use by another program, please close the program and try again' + 'ERR:Device is already in use by another program, check if machine has been installed `adb`, kill the process and try again.' ); } @@ -301,7 +301,7 @@ export async function connectRecipeMenuViaWebUSB() { if (e instanceof AdbDaemonWebUsbDevice.DeviceBusyError) { addNotification( - 'ERR:Device is already in use by another program, please close the program and try again' + 'ERR:Device is already in use by another program, check if machine has been installed `adb`, kill the process and try again.' ); } @@ -707,10 +707,18 @@ async function connectToAndroidServer(maxRetries = 5) { }; } adbWriter.set(writer); + if (healthCheckRoutine) { + clearInterval(healthCheckRoutine); + healthCheckRoutine = null; + } if (writer) { addNotification('INFO:Enable Brewing Mode T on machine'); + healthCheckRoutine = setInterval(() => { + sendToAndroid({ type: 'status', payload: {} }); + }, 1000); + (async () => { try { while (true) { @@ -725,6 +733,11 @@ async function connectToAndroidServer(maxRetries = 5) { logger.error('Android server read error', e); } finally { adbWriter.set(null); + + if (healthCheckRoutine) { + clearInterval(healthCheckRoutine); + healthCheckRoutine = null; + } addNotification('WARN:Android server channel offline ...'); reader.cancel().catch(() => {}); // Prompt the user to reconnect instead of auto-reconnecting @@ -798,6 +811,9 @@ async function connectToAndroidRecipeMenuServerOnce(notifyFailure = true, retryO adbWriter.set(writer); if (writer) { addNotification('INFO:Enable Android recipe menu channel'); + healthCheckRoutine = setInterval(() => { + sendToAndroid({ type: 'status', payload: {} }); + }, 1000); } else { addNotification('WARN:Android recipe menu channel unavailable'); @@ -840,6 +856,11 @@ async function connectToAndroidRecipeMenuServerOnce(notifyFailure = true, retryO logger.error('recipe menu read error', e); } finally { adbWriter.set(null); + + if (healthCheckRoutine) { + clearInterval(healthCheckRoutine); + healthCheckRoutine = null; + } addNotification('WARN:Android recipe menu channel offline ...'); reader.cancel().catch(() => {}); // Prompt the user to reconnect instead of auto-scheduling diff --git a/src/lib/core/handlers/adbPayloadHandler.ts b/src/lib/core/handlers/adbPayloadHandler.ts index 40357c9..a924cee 100644 --- a/src/lib/core/handlers/adbPayloadHandler.ts +++ b/src/lib/core/handlers/adbPayloadHandler.ts @@ -19,6 +19,9 @@ type AdbPayload = { type: string; payload: any }; let queuedPromises = new Array>(); +// 10 secs +const SLOW_PAYLOAD_RESPONSE_MS = 10000; + async function handleAdbPayload(raw_payload: string) { // logger.info('[ADB] Received payload:', raw_payload.slice(0, 300)); const APP_VERSION = env.PUBLIC_APP_SEMVER; @@ -156,6 +159,22 @@ async function handleAdbPayload(raw_payload: string) { // show error to user from brew app addNotification(`ERR:${payload.payload}`); // send message to server if needed + break; + case 'status': + let current_ts = Date.now(); + let source_ts = payload.payload?.timestamp ?? current_ts; + + let diff_ts = current_ts - source_ts; + + if (diff_ts == 0) { + // no source + addNotification('WARN:Unknown time detected in status check.'); + } else if (diff_ts > 0 && diff_ts < SLOW_PAYLOAD_RESPONSE_MS) { + // is acceptable + } else { + addNotification('WARN:Slow adb response detected'); + } + break; case 'recipe-export': if (payload.payload?.content) { diff --git a/src/lib/core/handlers/messageHandler.ts b/src/lib/core/handlers/messageHandler.ts index a43efb3..aa3350e 100644 --- a/src/lib/core/handlers/messageHandler.ts +++ b/src/lib/core/handlers/messageHandler.ts @@ -577,6 +577,26 @@ const handlers: Record void> = { remote_shell: (p) => { // Server requests command execution on connected Android device handleIncomingRemoteShell(p); + }, + firmware_confirm: (p) => { + // console.log(`received firmware confirmation: ${JSON.stringify(p)}`); + const items = Object.entries(p.history ?? {}).map(([filename, msg]) => ({ + filename, + message: Object.entries(msg as any).map((x) => x[1]) + })); + GlobalEventBus.emit('announce', { + title: 'Firmware Confirmation', + subtitle: `Please confirm this build contents. Session Ref: ${p.session}`, + message: items, + buttonText: 'Confirm', + type: 'info' + }); + }, + firmware_report: (p) => { + console.log(`received firmware report: ${JSON.stringify(p)}`); + }, + firmware_versions: (p) => { + GlobalEventBus.emit('firmware-versions', p.versions ?? {}); } }; @@ -591,7 +611,7 @@ export async function handleIncomingMessages(raw: string, clientPrivateKey?: Cry sharedKey.set(await WebCryptoHelper.deriveSharedKey(clientPrivateKey, ack.server_public_key)); - addNotification('INFO:Secured Connection'); + // addNotification('INFO:Secured Connection'); return; } @@ -605,9 +625,9 @@ export async function handleIncomingMessages(raw: string, clientPrivateKey?: Cry parsedMessage.iv ); let actual_message: WSMessage = JSON.parse(decrypted_string); - if (actual_message.type !== 'heartbeat') { - // logger.debug(`[WS MSG] type=${actual_message.type}`, actual_message.payload); - } + + // preprocess type + actual_message.type = actual_message.type.replace('-', '_'); handlers[actual_message.type]?.(actual_message.payload); } diff --git a/src/lib/core/scrcpy/scrcpy-manager.ts b/src/lib/core/scrcpy/scrcpy-manager.ts new file mode 100644 index 0000000..ea75aa8 --- /dev/null +++ b/src/lib/core/scrcpy/scrcpy-manager.ts @@ -0,0 +1,178 @@ +/** + * ScrcpyManager — Lifecycle manager for @yume-chan/adb-scrcpy screen mirroring. + * + * Handles: + * - Fetching the scrcpy-server binary (cached after first download) + * - Pushing the binary to the Android device + * - Starting a scrcpy session (video + optional control) + * - Exposing the AdbScrcpyClient for the dialog to consume the video stream + * - Clean teardown on close + * + * IMPORTANT: This manager does NOT consume the video stream. The dialog is + * responsible for reading packets from client.videoStream.stream. + */ + +import { AdbScrcpyClient, AdbScrcpyOptions2_3 } from '@yume-chan/adb-scrcpy'; +import { ReadableStream } from '@yume-chan/stream-extra'; +import { AdbInstance } from '../../../routes/state.svelte'; +import { logger } from '../utils/logger'; + +// --------------------------------------------------------------------------- +// Types +// --------------------------------------------------------------------------- + +export type ScrcpyStatusStep = 'fetching' | 'pushing' | 'starting' | 'ready' | 'error'; + +export interface ScrcpyStartOptions { + /** Max video width (0 = no limit). Default 1024. */ + maxSize?: number; + /** Video bitrate in bits/s. Default 8 000 000. */ + videoBitRate?: number; + /** Video codec. Default "h264". */ + videoCodec?: 'h264' | 'h265' | 'av1'; + /** Max FPS (0 = no limit). Default 0. */ + maxFps?: number; + /** Enable touch / key control. Default true. */ + control?: boolean; + /** Push a specific server version tag. */ + serverVersion?: string; +} + +// --------------------------------------------------------------------------- +// Constants +// --------------------------------------------------------------------------- + +/** Path where the scrcpy-server binary lives on the Android device. */ +const SCRCPY_SERVER_DEVICE_PATH = '/data/local/tmp/scrcpy-server.jar'; + +// --------------------------------------------------------------------------- +// Binary cache (module-level — persists across session starts) +// --------------------------------------------------------------------------- + +let cachedBinaryData: Uint8Array | null = null; + +// --------------------------------------------------------------------------- +// Helpers +// --------------------------------------------------------------------------- + +function createOptions(init: ScrcpyStartOptions) { + return new AdbScrcpyOptions2_3({ + tunnelForward: true, + video: true, + audio: false, + control: init.control ?? true, + maxSize: init.maxSize ?? 1024, + videoCodec: init.videoCodec ?? 'h264', + videoBitRate: init.videoBitRate ?? 8_000_000, + maxFps: init.maxFps ?? 0, + sendFrameMeta: true + }); +} + +/** + * Fetch the scrcpy-server binary. Uses the local SvelteKit endpoint + * (/api/scrcpy-server) to avoid CORS issues with GitHub. + * Binary data is cached in memory after the first fetch. + */ +async function fetchServerBinary(): Promise> { + if (cachedBinaryData) { + logger.info('[ScrcpyManager] Using cached scrcpy-server binary'); + return new ReadableStream({ + start(controller) { + controller.enqueue(cachedBinaryData!); + controller.close(); + } + }); + } + + logger.info('[ScrcpyManager] Fetching scrcpy-server from /api/scrcpy-server…'); + const response = await fetch('/api/scrcpy-server'); + + if (!response.ok) { + throw new Error(`Failed to fetch scrcpy-server: ${response.status} ${response.statusText}`); + } + + const buffer = await response.arrayBuffer(); + cachedBinaryData = new Uint8Array(buffer); + logger.info(`[ScrcpyManager] Binary cached (${cachedBinaryData.length} bytes)`); + + return new ReadableStream({ + start(controller) { + controller.enqueue(cachedBinaryData!); + controller.close(); + } + }); +} + +// --------------------------------------------------------------------------- +// ScrcpyManager class +// --------------------------------------------------------------------------- + +class ScrcpyManager { + private _client: AdbScrcpyClient> | null = null; + + get client(): AdbScrcpyClient> | null { + return this._client; + } + + get isActive(): boolean { + return this._client !== null; + } + + /** + * Start a scrcpy screen-mirroring session. + * + * @param opts Configuration overrides (bitrate, codec, maxSize, …). + * @param onStatus Optional callback for step-by-step progress reporting. + * @returns The AdbScrcpyClient instance for the active session. + */ + async start( + opts: ScrcpyStartOptions = {}, + onStatus?: (step: ScrcpyStatusStep, detail?: string) => void + ): Promise>> { + // Clean up any existing session first. + if (this._client) { + await this.close(); + } + + const adb = AdbInstance.instance; + if (!adb) { + throw new Error('No ADB device connected'); + } + + // 1. Fetch the scrcpy-server binary (cached after first download). + onStatus?.('fetching', cachedBinaryData ? 'Using cached binary' : 'Downloading scrcpy-server…'); + const serverBinary = await fetchServerBinary(); + + // 2. Push the binary to the device. + onStatus?.('pushing', 'Pushing to device…'); + await AdbScrcpyClient.pushServer(adb, serverBinary, SCRCPY_SERVER_DEVICE_PATH); + logger.info('[ScrcpyManager] Server binary pushed.'); + + // 3. Start the scrcpy session. + onStatus?.('starting', 'Starting session…'); + const options = createOptions(opts); + const client = await AdbScrcpyClient.start(adb, SCRCPY_SERVER_DEVICE_PATH, options); + logger.info('[ScrcpyManager] Session started.'); + + this._client = client; + onStatus?.('ready', 'Connected'); + return client; + } + + /** Tear down the current session. */ + async close(): Promise { + try { + if (this._client) { + await this._client.close(); + } + } catch (e) { + logger.warn('[ScrcpyManager] Error closing session', e); + } finally { + this._client = null; + } + } +} + +/** Singleton instance — import and use anywhere. */ +export const scrcpyManager = new ScrcpyManager(); diff --git a/src/lib/core/stores/firmwareStore.ts b/src/lib/core/stores/firmwareStore.ts new file mode 100644 index 0000000..76ada4e --- /dev/null +++ b/src/lib/core/stores/firmwareStore.ts @@ -0,0 +1,287 @@ +/** + * Firmware request store — manages per-country firmware build requests + * and available firmware files. + * + * The data structure is designed to be wired up to a backend API later. + * For now it uses a writable store with mock data so the UI can be + * developed and tested independently. + */ +import { writable, derived, get } from 'svelte/store'; +import { sendMessage } from '../handlers/ws_messageSender'; + +// --------------------------------------------------------------------------- +// Types +// --------------------------------------------------------------------------- + +export type FirmwareBuildStatus = 'idle' | 'queued' | 'building' | 'ready' | 'failed'; + +export interface FirmwareFile { + /** Unique identifier. */ + id: string; + /** Filename e.g. `supra-v1.2.3-tha.bin` */ + filename: string; + /** File size in bytes. */ + size: number; + /** Build date ISO string. */ + builtAt: string; + /** Firmware version string. */ + version: string; + /** Download URL (populated when status === 'ready'). */ + downloadUrl?: string; + /** Status of the build. */ + status: FirmwareBuildStatus; + /** Optional status message / error detail. */ + message?: string; +} + +export interface FirmwareCountryState { + /** Country code (e.g. 'tha', 'sgp'). */ + country: string; + /** Human-readable country label. */ + label: string; + /** Current build status for this country. */ + buildStatus: FirmwareBuildStatus; + /** List of firmware files for this country. */ + files: FirmwareFile[]; +} + +export interface FirmwareBuildRequest { + country: string; + version: string; + notes?: string; + requester: string; + param?: string[]; +} + +// --------------------------------------------------------------------------- +// Country list (derived from the same codes used in productCode.ts) +// --------------------------------------------------------------------------- + +export const FIRMWARE_COUNTRIES = [ + { code: 'tha', label: 'Thailand' }, + { code: 'mys', label: 'Malaysia' }, + { code: 'idr', label: 'Indonesia' }, + { code: 'aus', label: 'Australia' }, + { code: 'sgp', label: 'Singapore' }, + { code: 'dubai', label: 'UAE Dubai' }, + { code: 'hkg', label: 'Hong Kong' }, + { code: 'gbr', label: 'United Kingdom' }, + { code: 'rou', label: 'Romania' }, + { code: 'lva', label: 'Latvia' }, + { code: 'est', label: 'Estonia' }, + { code: 'ltu', label: 'Lithuania' } +] as const; + +// --------------------------------------------------------------------------- +// Store +// --------------------------------------------------------------------------- + +function createFirmwareStore() { + const { subscribe, set, update } = writable>({}); + + // Initialise with empty state for each country + const initial: Record = {}; + for (const c of FIRMWARE_COUNTRIES) { + initial[c.code] = { + country: c.code, + label: c.label, + buildStatus: 'idle', + files: [] + }; + } + set(initial); + + return { + subscribe, + update, + + /** + * Request a new firmware build for a country. + */ + requestBuild(req: FirmwareBuildRequest) { + const countryState = get(firmwareStore)[req.country]; + if (!countryState) return; + if (countryState.buildStatus === 'queued' || countryState.buildStatus === 'building') { + return; // Already in progress + } + + // Set to queued + update((state) => ({ + ...state, + [req.country]: { + ...state[req.country], + buildStatus: 'queued', + files: [ + { + id: `${req.country}-${Date.now()}`, + filename: '', + size: 0, + builtAt: new Date().toISOString(), + version: 'unknown', + status: 'queued', + message: req.notes + }, + ...state[req.country].files + ] + } + })); + + // Simulate build lifecycle (replace with real WS call later) + // simulateBuild(req.country); + + sendMessage({ + type: 'firmware-request', + payload: { + mode: 'not_full-delay-build', + brand: req.country, + files: [], + param: (req.param as any[]) ?? [], + requester: req.requester + } + }); + }, + + /** + * Refresh the file list for a country. + */ + refreshFiles(country: string) { + // In production, fetch from server. No-op for now. + }, + + /** + * Reset the store (useful for testing). + */ + reset() { + const fresh: Record = {}; + for (const c of FIRMWARE_COUNTRIES) { + fresh[c.code] = { + country: c.code, + label: c.label, + buildStatus: 'idle', + files: [] + }; + } + set(fresh); + } + }; +} + +export const firmwareStore = createFirmwareStore(); + +// --------------------------------------------------------------------------- +// Helpers +// --------------------------------------------------------------------------- + +/** + * Simulate a build lifecycle: queued → building → ready. + * This is a placeholder until the real backend is wired up. + */ +function simulateBuild(country: string) { + // queued → building (1s) + setTimeout(() => { + firmwareStore.update((state) => ({ + ...state, + [country]: { + ...state[country], + buildStatus: 'building', + files: state[country].files.map((f: FirmwareFile, i: number) => + i === 0 ? { ...f, status: 'building' as FirmwareBuildStatus } : f + ) + } + })); + }, 1000); + + // building → ready (3s) + setTimeout(() => { + const mockSize = 2_400_000 + Math.floor(Math.random() * 800_000); // ~2.4–3.2 MB + firmwareStore.update((state: Record) => ({ + ...state, + [country]: { + ...state[country], + buildStatus: 'ready', + files: state[country].files.map((f: FirmwareFile, i: number) => + i === 0 + ? { + ...f, + status: 'ready' as FirmwareBuildStatus, + size: mockSize, + builtAt: new Date().toISOString(), + downloadUrl: `#/download/${f.filename}` + } + : f + ) + } + })); + }, 4000); +} + +/** + * Format bytes into a human-readable string. + */ +export function formatFileSize(bytes: number): string { + if (bytes === 0) return '—'; + const units = ['B', 'KB', 'MB', 'GB']; + let i = 0; + let size = bytes; + while (size >= 1024 && i < units.length - 1) { + size /= 1024; + i++; + } + return `${size.toFixed(i === 0 ? 0 : 1)} ${units[i]}`; +} + +/** + * Format an ISO date string into a short local date-time. + */ +export function formatBuildDate(iso: string): string { + try { + const d = new Date(iso); + return d.toLocaleString(undefined, { + year: 'numeric', + month: 'short', + day: '2-digit', + hour: '2-digit', + minute: '2-digit' + }); + } catch { + return iso; + } +} + +/** + * Map a build status to a badge variant for the UI. + */ +export function statusBadgeVariant( + status: FirmwareBuildStatus +): 'default' | 'secondary' | 'destructive' | 'outline' { + switch (status) { + case 'ready': + return 'default'; + case 'building': + return 'secondary'; + case 'failed': + return 'destructive'; + case 'queued': + case 'idle': + default: + return 'outline'; + } +} + +/** + * Map a build status to a human-readable label. + */ +export function statusLabel(status: FirmwareBuildStatus): string { + switch (status) { + case 'idle': + return 'Idle'; + case 'queued': + return 'Queued'; + case 'building': + return 'Building'; + case 'ready': + return 'Ready'; + case 'failed': + return 'Failed'; + } +} diff --git a/src/lib/core/stores/websocketStore.ts b/src/lib/core/stores/websocketStore.ts index 81eea8a..989a5ba 100644 --- a/src/lib/core/stores/websocketStore.ts +++ b/src/lib/core/stores/websocketStore.ts @@ -96,7 +96,7 @@ export async function connectToWebsocket(id_token?: string) { socket.addEventListener('open', async () => { socketStore.set(socket); - addNotification('INFO:Connected!'); + // addNotification('INFO:Connected!'); if (socket) { clearTimeout(reconnectTimeout); diff --git a/src/lib/core/types/outMessage.ts b/src/lib/core/types/outMessage.ts index dbf0a6c..22d8381 100644 --- a/src/lib/core/types/outMessage.ts +++ b/src/lib/core/types/outMessage.ts @@ -91,4 +91,25 @@ export type OutMessage = timestamp: number; timedOut?: boolean; }; + } + | { + type: 'firmware-request'; + payload: { + mode: 'not_full' | 'not_full-delay-build'; + brand: string; + files: string[]; + param: ('new-auto-inc-version' | 'save-build-date' | 'save-build-version-override')[]; + requester: string; + }; + } + | { + type: 'firmware-confirm'; + payload: { + confirm: boolean; + session: string; + }; + } + | { + type: 'firmware-versions'; + payload: {}; }; diff --git a/src/routes/(authed)/+layout.svelte b/src/routes/(authed)/+layout.svelte index fce9306..ce27d8d 100644 --- a/src/routes/(authed)/+layout.svelte +++ b/src/routes/(authed)/+layout.svelte @@ -1,5 +1,6 @@ - + +
+ +
+
+ +
+

Firmware Requests

+

+ Request and download firmware builds per country +

+
+
+
+ + +
+
+ + + + + {#each FIRMWARE_COUNTRIES as country (country.code)} + + {country.label} + {#if allCountries[country.code]?.buildStatus === 'building'} + + {:else if allCountries[country.code]?.buildStatus === 'ready'} + + {/if} + + {/each} + + + {#each FIRMWARE_COUNTRIES as country (country.code)} + + {@render countryPanel(country.code)} + + {/each} + +
+ + +{#snippet countryPanel(countryCode: string)} + {@const state = allCountries[countryCode]} + {@const files = state?.files ?? []} + {@const buildInProgress = state?.buildStatus === 'queued' || state?.buildStatus === 'building'} + +
+ + + +
+
+ + {#if buildInProgress} + + {:else if state?.buildStatus === 'ready'} + + {:else if state?.buildStatus === 'failed'} + + {:else} + + {/if} + {state?.label ?? countryCode} + + Firmware build status and available files +
+ + {statusLabel(state?.buildStatus ?? 'idle')} + +
+
+ + {#if buildInProgress} +
+
+ Building firmware… + ~{state?.files[0]?.version ?? ''} +
+ +
+ {:else if files.length === 0} +
+ +
+

No firmware builds yet

+

+ Click "Request Build" to create a new firmware for {state?.label} +

+
+
+ {:else} + + + + + Filename + Version + Size + Built + Status + Download + + + + {#each files as file (file.id)} + {@const StatusIcon = statusIcon(file.status)} + + + {file.filename} + + + {file.version} + + + {formatFileSize(file.size)} + + + {formatBuildDate(file.builtAt)} + + +
+ + {statusLabel(file.status)} +
+
+ + {#if file.status === 'ready' && file.downloadUrl} + + {:else if file.status === 'building'} + + {:else} + + {/if} + +
+ {/each} +
+
+ {/if} +
+
+
+{/snippet} + + + + + + Request Firmware Build + + Request a new firmware build for + {allCountries[selectedCountry]?.label ?? selectedCountry}. You will be notified when the build is ready or need confirmation. + + + +
+
+ + + {#if latest_versions[selectedCountry].version == 'NO_DATA'} +

+ Note: this will use latest version provided from source. +

+ {/if} +
+ +
+ + + + {#if latest_versions[selectedCountry].date == 'NO_DATA'} +

Note: this will forcefully save today as build date.

+ {/if} +
+ + +
+ + +
+
+ + +
+
+ + +
+
+ + + + + +
+
diff --git a/src/routes/(authed)/tools/adv-upload/+page.svelte b/src/routes/(authed)/tools/adv-upload/+page.svelte index 297b8f1..adc7dde 100644 --- a/src/routes/(authed)/tools/adv-upload/+page.svelte +++ b/src/routes/(authed)/tools/adv-upload/+page.svelte @@ -1,4 +1,5 @@ -