From c6b528d5eb81f2cfade72b11890db1b1434fc2fe Mon Sep 17 00:00:00 2001 From: thanawat saiyota Date: Fri, 17 Jul 2026 11:37:01 +0700 Subject: [PATCH] add: New Test mode on the video main page and brewing page --- .../tools/video-mainpage/+page.svelte | 174 +++++++++++++++++- .../api/video-mainpage/testmode/+server.ts | 32 ++++ 2 files changed, 202 insertions(+), 4 deletions(-) create mode 100644 src/routes/api/video-mainpage/testmode/+server.ts diff --git a/src/routes/(authed)/tools/video-mainpage/+page.svelte b/src/routes/(authed)/tools/video-mainpage/+page.svelte index 846a9aa..8d13abf 100644 --- a/src/routes/(authed)/tools/video-mainpage/+page.svelte +++ b/src/routes/(authed)/tools/video-mainpage/+page.svelte @@ -10,6 +10,7 @@ import Badge from '$lib/components/ui/badge/badge.svelte'; import Spinner from '$lib/components/ui/spinner/spinner.svelte'; import Progress from '$lib/components/ui/progress/progress.svelte'; + import { Checkbox } from '$lib/components/ui/checkbox/index.js'; import { Upload, X, @@ -22,7 +23,8 @@ CalendarDays, Clock, ChevronDown, - ImageIcon + ImageIcon, + FlaskConical } from '@lucide/svelte/icons'; import * as adb from '$lib/core/adb/adb'; import { env } from '$env/dynamic/public'; @@ -31,6 +33,7 @@ const CREATE_ENDPOINT = '/api/video-mainpage'; const LIST_ENDPOINT = '/api/video-mainpage/list'; const UPDATE_ENDPOINT = '/api/video-mainpage/update'; + const TESTMODE_ENDPOINT = '/api/video-mainpage/testmode'; const MACHINE_PROJECT_DIR = '/sdcard/coffeevending/taobin_project'; const GET_IMAGE = env.PUBLIC_GET_IMAGE; const DURATION_TRIM = 4; // brewing play seconds = video length − 4 @@ -102,6 +105,26 @@ let pushProgress = $state({ percent: 0, name: '', active: false }); let isAdbConnected = $derived(Boolean(AdbInstance.instance)); + // ── test mode (machine-local date/time override) ───────────────────────── + // '' = unset (leave that clock field on the real value). + let testMonth = $state(''); + let testDay = $state(''); + let testHour = $state(''); + let testMinute = $state(''); + let testEnabled = $state(true); // emits `Var TestMode = 1` so the gate fires + let applyingTest = $state(false); + const hasTestValues = $derived( + [testMonth, testDay, testHour, testMinute].some((v) => v !== '') + ); + + // Dropdown options. Hour/minute are zero-padded for display; the value is the + // plain number string (the backend parses it with int()). + const MONTH_OPTS = Array.from({ length: 12 }, (_, i) => String(i + 1)); + const DAY_OPTS = Array.from({ length: 31 }, (_, i) => String(i + 1)); + const HOUR_OPTS = Array.from({ length: 24 }, (_, i) => String(i)); + const MINUTE_OPTS = Array.from({ length: 60 }, (_, i) => String(i)); + const pad2 = (s: string) => s.padStart(2, '0'); + // ── list ──────────────────────────────────────────────────────────────── let managed = $state([]); let readonlyList = $state([]); @@ -282,6 +305,49 @@ } } + // Inject (or, when `clear`, remove) the TestMode date/time override in the + // header of the web scripts and push them to the connected machine over ADB. + // Never touches git/FTP — this override is machine-local by design. + async function applyTestMode(clear = false) { + if (!AdbInstance.instance) return addNotification('ERR:Connect a machine first'); + const body = clear + ? {} + : { + enabled: testEnabled, + month: testMonth, + day: testDay, + hour: testHour, + minute: testMinute + }; + applyingTest = true; + pushProgress = { percent: 0, name: '', active: true }; + try { + const res = await fetch(`${TESTMODE_ENDPOINT}?country=${encodeURIComponent(country)}`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(body) + }); + const result = await res.json().catch(() => ({})); + if (!res.ok) throw new Error(result?.message || result?.detail || 'Test-mode apply failed'); + await pushScripts(result?.content?.scripts ?? []); + if (result?.cleared) { + testMonth = testDay = testHour = testMinute = ''; + testEnabled = false; + addNotification(`INFO:Test mode cleared on machine (${country})`); + } else { + const o = result?.override ?? {}; + addNotification( + `INFO:Test mode pushed to machine — ${o.month ?? '–'}/${o.day ?? '–'} ${o.hour ?? '–'}:${o.minute ?? '–'} (${country})` + ); + } + } catch (error) { + addNotification(`ERR:${error instanceof Error ? error.message : 'Test-mode failed'}`); + } finally { + applyingTest = false; + pushProgress = { percent: 0, name: '', active: false }; + } + } + async function handleSubmit() { const user = $auth; if (!user) return addNotification('ERR:Not logged in'); @@ -325,9 +391,15 @@ ); await pushScripts(result?.content?.scripts ?? []); - if (result?.sftp?.error) - addNotification(`WARN:Uploaded but FTP sync failed: ${result.sftp.error}`); - addNotification(`INFO:Added "${name.trim()}" as brewing_adv${result.n} (${country})`); + if (result?.duplicate) { + addNotification( + `WARN:"${name.trim()}" already exists as brewing_adv${result.n} — pushed to machine, no duplicate created` + ); + } else { + if (result?.sftp?.error) + addNotification(`WARN:Uploaded but FTP sync failed: ${result.sftp.error}`); + addNotification(`INFO:Added "${name.trim()}" as brewing_adv${result.n} (${country})`); + } clearMain(); clearBrewing(); clearBrewingTxt(); @@ -629,6 +701,100 @@ + + + + + Test mode + + + Override the machine clock (AdvSystem*) to preview + date-gated videos. Gated by TestMode = 1 and pushed to + the connected machine only — never committed or synced. Fill only what you need. + + + +
+
+ + + {testMonth || '—'} + + + {#each MONTH_OPTS as m} + {m} + {/each} + + +
+
+ + + {testDay || '—'} + + + {#each DAY_OPTS as d} + {d} + {/each} + + +
+
+ + + {testHour === '' ? '—' : pad2(testHour)} + + + {#each HOUR_OPTS as h} + {pad2(h)} + {/each} + + +
+
+ + + {testMinute === '' ? '—' : pad2(testMinute)} + + + {#each MINUTE_OPTS as mi} + {pad2(mi)} + {/each} + + +
+
+ +
+ + + {#if !isAdbConnected} + Connect a machine to apply. + {/if} +
+
+
+
diff --git a/src/routes/api/video-mainpage/testmode/+server.ts b/src/routes/api/video-mainpage/testmode/+server.ts new file mode 100644 index 0000000..385fc97 --- /dev/null +++ b/src/routes/api/video-mainpage/testmode/+server.ts @@ -0,0 +1,32 @@ +import { json, error } from '@sveltejs/kit'; +import type { RequestHandler } from './$types'; +import { env } from '$env/dynamic/public'; + +const API_BASE = env.PUBLIC_POST_IMAGE; + +// Return the country's web video scripts with a TestMode date/time override +// injected (or removed). The caller pushes the result to a machine over ADB — +// nothing is committed or synced. POST because the Kong route is POST-only. +export const POST: RequestHandler = async ({ url, request }) => { + try { + const country = (url.searchParams.get('country') || 'tha').toLowerCase(); + const body = await request.json().catch(() => ({})); + const response = await fetch( + `${API_BASE}/video/mainpage/testmode/${encodeURIComponent(country)}`, + { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(body) + } + ); + if (!response.ok) { + const e = await response.json().catch(() => ({ detail: response.statusText })); + throw error(response.status, e.detail || 'Test-mode apply failed'); + } + return json(await response.json()); + } catch (err) { + console.error('[Video MainPage TestMode Proxy] Error:', err); + if (err && typeof err === 'object' && 'status' in err) throw err; + throw error(500, err instanceof Error ? err.message : 'Internal server error'); + } +};