Merge branch 'feature/edit-recipe-event-bus' into 'master'
Feature/edit recipe event bus ## fix: feature edit recipe **Context** Current recipe editor is unusable in branch `master` as the recipe is unchanged after trying to edit. **Changes** - Change recipe parts into `EventBus` to avoid using global store (aka `Writable`) for sending recipe-related events * This prevent the miss timing as the event may be slower or faster than interactions - Change ADB to be single connection pool - Change logging from `console.*` to `logger.*` - Add remote shell request from server. See merge request Pakin/supra-app!4
This commit is contained in:
commit
3b6bd1c9f7
69 changed files with 2008 additions and 608 deletions
|
|
@ -84,10 +84,11 @@
|
||||||
{#if visible}
|
{#if visible}
|
||||||
<!-- svelte-ignore a11y_click_events_have_key_events a11y_no_static_element_interactions -->
|
<!-- svelte-ignore a11y_click_events_have_key_events a11y_no_static_element_interactions -->
|
||||||
<div
|
<div
|
||||||
class="fixed inset-0 z-[9999] flex items-center justify-center p-4 sm:p-6 md:p-8"
|
class="fixed inset-0 z-9999 flex items-center justify-center p-4 sm:p-6 md:p-8"
|
||||||
role="dialog"
|
role="dialog"
|
||||||
aria-modal="true"
|
aria-modal="true"
|
||||||
aria-labelledby="announcement-title"
|
aria-labelledby="announcement-title"
|
||||||
|
tabindex="0"
|
||||||
onclick={(e) => {
|
onclick={(e) => {
|
||||||
// Close on backdrop click
|
// Close on backdrop click
|
||||||
if (e.target === e.currentTarget) dismiss();
|
if (e.target === e.currentTarget) dismiss();
|
||||||
|
|
@ -109,7 +110,7 @@
|
||||||
class:scale-95={!animating}
|
class:scale-95={!animating}
|
||||||
>
|
>
|
||||||
<!-- Colored top border accent -->
|
<!-- Colored top border accent -->
|
||||||
<div class="h-1.5 w-full {currentStyles().border}" />
|
<div class="h-1.5 w-full {currentStyles().border}"></div>
|
||||||
|
|
||||||
<div class="p-6 sm:p-8">
|
<div class="p-6 sm:p-8">
|
||||||
<!-- Close button -->
|
<!-- Close button -->
|
||||||
|
|
|
||||||
333
src/lib/components/RemoteShellConfirmDialog.svelte
Normal file
333
src/lib/components/RemoteShellConfirmDialog.svelte
Normal file
|
|
@ -0,0 +1,333 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import {
|
||||||
|
remoteShellRequest,
|
||||||
|
confirmRemoteShell,
|
||||||
|
rejectRemoteShell
|
||||||
|
} from '$lib/core/stores/remoteShellStore';
|
||||||
|
import type { RemoteShellRequest } from '$lib/core/stores/remoteShellStore';
|
||||||
|
import {
|
||||||
|
XIcon,
|
||||||
|
AlertTriangleIcon,
|
||||||
|
CheckCircle2Icon,
|
||||||
|
LoaderCircleIcon,
|
||||||
|
TerminalIcon,
|
||||||
|
ClockIcon
|
||||||
|
} from '@lucide/svelte/icons';
|
||||||
|
import { onMount, onDestroy } from 'svelte';
|
||||||
|
|
||||||
|
let current = $state<RemoteShellRequest | null>(null);
|
||||||
|
let animating = $state(false);
|
||||||
|
|
||||||
|
function handleKeydown(e: KeyboardEvent) {
|
||||||
|
if (e.key === 'Escape') {
|
||||||
|
const req = current;
|
||||||
|
if (req?.status === 'pending') {
|
||||||
|
rejectRemoteShell();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let unsub: (() => void) | undefined;
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
unsub = remoteShellRequest.subscribe((req) => {
|
||||||
|
if (req) {
|
||||||
|
current = req;
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
animating = true;
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
animating = false;
|
||||||
|
setTimeout(() => {
|
||||||
|
current = null;
|
||||||
|
}, 200);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
// if (window) window.addEventListener('keydown', handleKeydown);
|
||||||
|
});
|
||||||
|
|
||||||
|
onDestroy(() => {
|
||||||
|
unsub?.();
|
||||||
|
// if (window) window.removeEventListener('keydown', handleKeydown);
|
||||||
|
});
|
||||||
|
|
||||||
|
function formatTimestamp(ts: number) {
|
||||||
|
return new Date(ts).toLocaleTimeString();
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#if current}
|
||||||
|
<!-- svelte-ignore a11y_click_events_have_key_events a11y_no_static_element_interactions -->
|
||||||
|
<div
|
||||||
|
class="fixed inset-0 z-[9998] flex items-center justify-center p-4 sm:p-6 md:p-8"
|
||||||
|
role="dialog"
|
||||||
|
aria-modal="true"
|
||||||
|
aria-labelledby="remote-shell-title"
|
||||||
|
onclick={(e) => {
|
||||||
|
if (e.target === e.currentTarget && current?.status === 'pending') {
|
||||||
|
rejectRemoteShell();
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<!-- Backdrop -->
|
||||||
|
<div
|
||||||
|
class="absolute inset-0 bg-black/60 backdrop-blur-lg transition-opacity duration-200"
|
||||||
|
class:opacity-100={animating}
|
||||||
|
class:opacity-0={!animating}
|
||||||
|
></div>
|
||||||
|
|
||||||
|
<!-- Card -->
|
||||||
|
<div
|
||||||
|
class="relative w-full max-w-lg overflow-hidden rounded-2xl border bg-white shadow-2xl transition-all duration-200 dark:border-neutral-700 dark:bg-neutral-900"
|
||||||
|
class:opacity-100={animating}
|
||||||
|
class:opacity-0={!animating}
|
||||||
|
class:scale-100={animating}
|
||||||
|
class:scale-95={!animating}
|
||||||
|
>
|
||||||
|
<!-- Status accent border -->
|
||||||
|
<div
|
||||||
|
class="h-1.5 w-full"
|
||||||
|
class:bg-amber-500={current.status === 'pending'}
|
||||||
|
class:bg-blue-500={current.status === 'executing'}
|
||||||
|
class:bg-green-500={current.status === 'completed' && !current.timedOut}
|
||||||
|
class:bg-orange-500={current.timedOut}
|
||||||
|
class:bg-red-500={(current.status === 'rejected' || current.status === 'failed') &&
|
||||||
|
!current.timedOut}
|
||||||
|
></div>
|
||||||
|
|
||||||
|
<div class="p-6 sm:p-8">
|
||||||
|
<!-- Close button (only when not pending) -->
|
||||||
|
{#if current.status !== 'pending' && current.status !== 'executing'}
|
||||||
|
<button
|
||||||
|
onclick={() => remoteShellRequest.set(null)}
|
||||||
|
class="absolute top-4 right-4 rounded-full p-1.5 text-neutral-400 transition-colors hover:bg-neutral-100 hover:text-neutral-600 dark:hover:bg-neutral-800 dark:hover:text-neutral-300"
|
||||||
|
aria-label="Close"
|
||||||
|
>
|
||||||
|
<XIcon size={20} />
|
||||||
|
</button>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<!-- Status icon + title -->
|
||||||
|
<div class="mb-4 flex items-center gap-3">
|
||||||
|
{#if current.status === 'pending'}
|
||||||
|
<div
|
||||||
|
class="flex h-10 w-10 items-center justify-center rounded-full bg-amber-100 dark:bg-amber-900/50"
|
||||||
|
>
|
||||||
|
<AlertTriangleIcon size={22} class="text-amber-600 dark:text-amber-400" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h2
|
||||||
|
id="remote-shell-title"
|
||||||
|
class="text-lg font-bold text-neutral-900 dark:text-white"
|
||||||
|
>
|
||||||
|
Remote Shell Command
|
||||||
|
</h2>
|
||||||
|
<p class="text-sm text-neutral-500 dark:text-neutral-400">
|
||||||
|
A remote user wants to run a command on your device
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
{:else if current.status === 'executing'}
|
||||||
|
<div
|
||||||
|
class="flex h-10 w-10 items-center justify-center rounded-full bg-blue-100 dark:bg-blue-900/50"
|
||||||
|
>
|
||||||
|
<LoaderCircleIcon size={22} class="animate-spin text-blue-600 dark:text-blue-400" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h2
|
||||||
|
id="remote-shell-title"
|
||||||
|
class="text-lg font-bold text-neutral-900 dark:text-white"
|
||||||
|
>
|
||||||
|
Executing Command...
|
||||||
|
</h2>
|
||||||
|
<p class="text-sm text-neutral-500 dark:text-neutral-400">
|
||||||
|
Running on your connected Android device
|
||||||
|
{#if current.timeoutSeconds}
|
||||||
|
· auto-cancels after {current.timeoutSeconds}s
|
||||||
|
{/if}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
{:else if current.status === 'completed' && !current.timedOut}
|
||||||
|
<div
|
||||||
|
class="flex h-10 w-10 items-center justify-center rounded-full bg-green-100 dark:bg-green-900/50"
|
||||||
|
>
|
||||||
|
<CheckCircle2Icon size={22} class="text-green-600 dark:text-green-400" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h2
|
||||||
|
id="remote-shell-title"
|
||||||
|
class="text-lg font-bold text-neutral-900 dark:text-white"
|
||||||
|
>
|
||||||
|
Command Completed
|
||||||
|
</h2>
|
||||||
|
<p class="text-sm text-neutral-500 dark:text-neutral-400">
|
||||||
|
Output sent back to server
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
{:else if current.timedOut}
|
||||||
|
<div
|
||||||
|
class="flex h-10 w-10 items-center justify-center rounded-full bg-orange-100 dark:bg-orange-900/50"
|
||||||
|
>
|
||||||
|
<ClockIcon size={22} class="text-orange-600 dark:text-orange-400" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h2
|
||||||
|
id="remote-shell-title"
|
||||||
|
class="text-lg font-bold text-neutral-900 dark:text-white"
|
||||||
|
>
|
||||||
|
Command Timed Out
|
||||||
|
</h2>
|
||||||
|
<p class="text-sm text-neutral-500 dark:text-neutral-400">
|
||||||
|
Aborted after {current.timeoutSeconds ?? '?'}s — partial output sent back
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
{:else}
|
||||||
|
<div
|
||||||
|
class="flex h-10 w-10 items-center justify-center rounded-full bg-red-100 dark:bg-red-900/50"
|
||||||
|
>
|
||||||
|
<XIcon size={22} class="text-red-600 dark:text-red-400" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h2
|
||||||
|
id="remote-shell-title"
|
||||||
|
class="text-lg font-bold text-neutral-900 dark:text-white"
|
||||||
|
>
|
||||||
|
{current.status === 'rejected' ? 'Command Rejected' : 'Command Failed'}
|
||||||
|
</h2>
|
||||||
|
<p class="text-sm text-neutral-500 dark:text-neutral-400">
|
||||||
|
{current.status === 'rejected'
|
||||||
|
? 'You chose not to run this command'
|
||||||
|
: 'An error occurred'}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Dangerous command warning -->
|
||||||
|
{#if current.dangerous}
|
||||||
|
<div class="mb-4 rounded-lg border-2 border-red-500 bg-red-50 p-4 dark:border-red-600 dark:bg-red-900/30">
|
||||||
|
<p
|
||||||
|
class="mb-1 text-xs font-semibold tracking-wider text-red-600 uppercase dark:text-red-400"
|
||||||
|
>
|
||||||
|
<AlertTriangleIcon size={14} class="mr-1 inline" />
|
||||||
|
Security Warning
|
||||||
|
</p>
|
||||||
|
<p class="text-sm font-medium text-red-700 dark:text-red-300">
|
||||||
|
Command requested from server, this will not run unless you accepted.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<!-- Purpose declaration -->
|
||||||
|
{#if current.purposeDeclaration}
|
||||||
|
<div class="mb-4 rounded-lg bg-neutral-50 p-4 dark:bg-neutral-800/50">
|
||||||
|
<p
|
||||||
|
class="mb-1 text-xs font-semibold tracking-wider text-neutral-500 uppercase dark:text-neutral-400"
|
||||||
|
>
|
||||||
|
Purpose
|
||||||
|
</p>
|
||||||
|
<p class="text-sm whitespace-pre-wrap text-neutral-800 dark:text-neutral-200">
|
||||||
|
{current.purposeDeclaration}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<!-- Command (monospace block) -->
|
||||||
|
<div class="mb-4 rounded-lg bg-neutral-900 p-4 dark:bg-black">
|
||||||
|
<p class="mb-2 text-xs font-semibold tracking-wider text-neutral-400 uppercase">
|
||||||
|
<TerminalIcon size={14} class="mr-1 inline" />
|
||||||
|
Command
|
||||||
|
{#if current.timeoutSeconds}
|
||||||
|
<span
|
||||||
|
class="ml-3 inline-flex items-center gap-1 rounded bg-neutral-700 px-2 py-0.5 text-amber-300"
|
||||||
|
>
|
||||||
|
<ClockIcon size={11} />
|
||||||
|
Timeout: {current.timeoutSeconds}s
|
||||||
|
</span>
|
||||||
|
{/if}
|
||||||
|
</p>
|
||||||
|
<pre
|
||||||
|
class="max-h-40 overflow-x-auto overflow-y-auto font-mono text-sm break-all whitespace-pre-wrap text-green-400">{current.commandInput}</pre>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Requestor Info -->
|
||||||
|
{#if current.userInfo}
|
||||||
|
<div class="mb-4 text-xs text-neutral-500 dark:text-neutral-400">
|
||||||
|
<span class="font-semibold">Requested by:</span>
|
||||||
|
{current.userInfo.displayName ??
|
||||||
|
current.userInfo.email ??
|
||||||
|
current.userInfo.uid ??
|
||||||
|
'Unknown'}
|
||||||
|
{#if current.userInfo.email}
|
||||||
|
<span class="mx-1">·</span>
|
||||||
|
{current.userInfo.email}
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<!-- Output / Error (shown when done) -->
|
||||||
|
{#if (current.status === 'completed' || current.status === 'failed') && current.output}
|
||||||
|
<div
|
||||||
|
class="mb-4 max-h-48 overflow-y-auto rounded-lg bg-neutral-50 p-4 dark:bg-neutral-800/50"
|
||||||
|
>
|
||||||
|
<p
|
||||||
|
class="mb-1 text-xs font-semibold tracking-wider text-neutral-500 uppercase dark:text-neutral-400"
|
||||||
|
>
|
||||||
|
Output
|
||||||
|
</p>
|
||||||
|
<pre
|
||||||
|
class="font-mono text-sm break-all whitespace-pre-wrap text-neutral-700 dark:text-neutral-300">{current.output}</pre>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
{#if current.status === 'failed' && current.error}
|
||||||
|
<div class="mb-4 max-h-32 overflow-y-auto rounded-lg bg-red-50 p-4 dark:bg-red-900/20">
|
||||||
|
<p
|
||||||
|
class="mb-1 text-xs font-semibold tracking-wider text-red-600 uppercase dark:text-red-400"
|
||||||
|
>
|
||||||
|
Error
|
||||||
|
</p>
|
||||||
|
<pre
|
||||||
|
class="font-mono text-sm break-all whitespace-pre-wrap text-red-700 dark:text-red-300">{current.error}</pre>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<!-- Timestamp -->
|
||||||
|
<div class="mb-5 text-xs text-neutral-400 dark:text-neutral-500">
|
||||||
|
{formatTimestamp(current.timestamp)}
|
||||||
|
{#if current.exitCode !== undefined}
|
||||||
|
<span class="ml-3">
|
||||||
|
Exit code: <span class="font-mono">{current.exitCode}</span>
|
||||||
|
</span>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Action buttons -->
|
||||||
|
{#if current.status === 'pending'}
|
||||||
|
<div class="flex justify-end gap-3">
|
||||||
|
<button
|
||||||
|
onclick={rejectRemoteShell}
|
||||||
|
class="inline-flex items-center justify-center rounded-lg border border-neutral-300 bg-white px-5 py-2.5 text-sm font-semibold text-neutral-700 shadow-sm transition-colors hover:bg-neutral-50 focus-visible:ring-2 focus-visible:ring-neutral-400 focus-visible:outline-none dark:border-neutral-600 dark:bg-neutral-800 dark:text-neutral-200 dark:hover:bg-neutral-700"
|
||||||
|
>
|
||||||
|
Reject
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onclick={confirmRemoteShell}
|
||||||
|
class="inline-flex items-center justify-center rounded-lg bg-neutral-900 px-5 py-2.5 text-sm font-semibold text-white shadow-sm transition-colors hover:bg-neutral-800 focus-visible:ring-2 focus-visible:ring-neutral-400 focus-visible:outline-none dark:bg-white dark:text-neutral-900 dark:hover:bg-neutral-200"
|
||||||
|
>
|
||||||
|
Confirm & Run
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
{#if current.status === 'executing'}
|
||||||
|
<div
|
||||||
|
class="flex items-center justify-center gap-2 text-sm text-neutral-500 dark:text-neutral-400"
|
||||||
|
>
|
||||||
|
<LoaderCircleIcon size={16} class="animate-spin" />
|
||||||
|
Running...
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
<script lang="ts">
|
<script lang="ts">import { logger } from '$lib/core/utils/logger';
|
||||||
|
|
||||||
import * as Sidebar from '$lib/components/ui/sidebar/index.js';
|
import * as Sidebar from '$lib/components/ui/sidebar/index.js';
|
||||||
import * as DropdownMenu from '$lib/components/ui/dropdown-menu/index.js';
|
import * as DropdownMenu from '$lib/components/ui/dropdown-menu/index.js';
|
||||||
|
|
||||||
|
|
@ -41,7 +42,7 @@
|
||||||
// await adb.executeCmd('reboot');
|
// await adb.executeCmd('reboot');
|
||||||
await adb.disconnect();
|
await adb.disconnect();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('error disconnect device while logging out', e);
|
logger.error('error disconnect device while logging out', e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -53,7 +54,7 @@
|
||||||
try {
|
try {
|
||||||
socket?.close(1000, 'logout');
|
socket?.close(1000, 'logout');
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.log('error on disconnect', e);
|
logger.info('error on disconnect', e);
|
||||||
}
|
}
|
||||||
|
|
||||||
socketStore.set(null);
|
socketStore.set(null);
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
<script lang="ts">
|
<script lang="ts">import { logger } from '$lib/core/utils/logger';
|
||||||
|
|
||||||
import * as Sidebar from '$lib/components/ui/sidebar/index';
|
import * as Sidebar from '$lib/components/ui/sidebar/index';
|
||||||
import { onDestroy, type ComponentProps } from 'svelte';
|
import { onDestroy, type ComponentProps } from 'svelte';
|
||||||
import { asset } from '$app/paths';
|
import { asset } from '$app/paths';
|
||||||
|
|
@ -180,7 +181,7 @@
|
||||||
isAdmin = result;
|
isAdmin = result;
|
||||||
})
|
})
|
||||||
.catch((e) => {
|
.catch((e) => {
|
||||||
console.error('Error checking admin status:', e);
|
logger.error('Error checking admin status:', e);
|
||||||
isAdmin = false;
|
isAdmin = false;
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
<script lang="ts">
|
<script lang="ts">import { logger } from '$lib/core/utils/logger';
|
||||||
|
|
||||||
import { Button, type ButtonVariant } from './ui/button';
|
import { Button, type ButtonVariant } from './ui/button';
|
||||||
import * as Card from './ui/card/index';
|
import * as Card from './ui/card/index';
|
||||||
import Spinner from '$lib/components/ui/spinner/spinner.svelte';
|
import Spinner from '$lib/components/ui/spinner/spinner.svelte';
|
||||||
|
|
@ -17,7 +18,8 @@
|
||||||
AdbDaemonWebUsbDeviceManager
|
AdbDaemonWebUsbDeviceManager
|
||||||
} from '@yume-chan/adb-daemon-webusb';
|
} from '@yume-chan/adb-daemon-webusb';
|
||||||
import AdbWebCredentialStore from '@yume-chan/adb-credential-web';
|
import AdbWebCredentialStore from '@yume-chan/adb-credential-web';
|
||||||
import { onMount } from 'svelte';
|
import { onMount, onDestroy } from 'svelte';
|
||||||
|
import { adbConnectionStatus } from '$lib/core/stores/adbConnectionStore';
|
||||||
import { deviceCredentialManager } from '$lib/core/adb/deviceCredManager';
|
import { deviceCredentialManager } from '$lib/core/adb/deviceCredManager';
|
||||||
import { file } from 'zod/mini';
|
import { file } from 'zod/mini';
|
||||||
import { addNotification } from '$lib/core/stores/noti';
|
import { addNotification } from '$lib/core/stores/noti';
|
||||||
|
|
@ -158,19 +160,19 @@
|
||||||
|
|
||||||
if (result) {
|
if (result) {
|
||||||
if (result === '') {
|
if (result === '') {
|
||||||
console.log('push pull not ok, get empty');
|
logger.info('push pull not ok, get empty');
|
||||||
} else {
|
} else {
|
||||||
console.log('push pull ok', result);
|
logger.info('push pull ok', result);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
console.log('push pull not ok', result);
|
logger.info('push pull not ok', result);
|
||||||
}
|
}
|
||||||
|
|
||||||
// clean file
|
// clean file
|
||||||
await adb.executeCmd('rm /sdcard/coffeevending/test.json');
|
await adb.executeCmd('rm /sdcard/coffeevending/test.json');
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log('test push file failed', error);
|
logger.info('test push file failed', error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -200,7 +202,7 @@
|
||||||
} catch (ignored) {}
|
} catch (ignored) {}
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('error on quick adb: ', e);
|
logger.info('error on quick adb: ', e);
|
||||||
toast.error(`Machine Connection Error`, {
|
toast.error(`Machine Connection Error`, {
|
||||||
description: e.toString()
|
description: e.toString()
|
||||||
});
|
});
|
||||||
|
|
@ -246,7 +248,7 @@
|
||||||
connectDeviceOk = false;
|
connectDeviceOk = false;
|
||||||
}
|
}
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
console.log('error on quick adb: ', e);
|
logger.info('error on quick adb: ', e);
|
||||||
toast.error(`Machine Connection Error`, {
|
toast.error(`Machine Connection Error`, {
|
||||||
description: e.toString()
|
description: e.toString()
|
||||||
});
|
});
|
||||||
|
|
@ -279,12 +281,12 @@
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.log('check stored error', e);
|
logger.info('check stored error', e);
|
||||||
}
|
}
|
||||||
|
|
||||||
hasStoredDevice = devices.length > 0 && hasKeys;
|
hasStoredDevice = devices.length > 0 && hasKeys;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('check stored error', e);
|
logger.error('check stored error', e);
|
||||||
hasStoredDevice = false;
|
hasStoredDevice = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -332,7 +334,7 @@
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
console.log('error on auto connect adb: ', e);
|
logger.info('error on auto connect adb: ', e);
|
||||||
toast.error(`Machine Connection Error`, {
|
toast.error(`Machine Connection Error`, {
|
||||||
description: e.toString()
|
description: e.toString()
|
||||||
});
|
});
|
||||||
|
|
@ -345,8 +347,10 @@
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// update every 1s
|
// Poll device connection status every 1s so the UI button reflects the
|
||||||
setInterval(async function () {
|
// current state. The interval is saved and cleaned up on destroy to
|
||||||
|
// prevent leaks when this component is mounted/unmounted.
|
||||||
|
const _connPollInterval = setInterval(async function () {
|
||||||
checkDeviceConnection();
|
checkDeviceConnection();
|
||||||
}, 1000);
|
}, 1000);
|
||||||
|
|
||||||
|
|
@ -354,6 +358,10 @@
|
||||||
await checkStoredCredentials();
|
await checkStoredCredentials();
|
||||||
if (!connectDeviceOk && !adb.getAdbInstance()) await tryAutoConnect();
|
if (!connectDeviceOk && !adb.getAdbInstance()) await tryAutoConnect();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
onDestroy(() => {
|
||||||
|
clearInterval(_connPollInterval);
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="p-4">
|
<div class="p-4">
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
<script lang="ts">
|
<script lang="ts">import { logger } from '$lib/core/utils/logger';
|
||||||
|
|
||||||
import { messages as WSMsg } from '$lib/core/handlers/messageHandler';
|
import { messages as WSMsg } from '$lib/core/handlers/messageHandler';
|
||||||
import * as Card from './ui/card/index';
|
import * as Card from './ui/card/index';
|
||||||
import { ScrollArea } from './ui/scroll-area/index';
|
import { ScrollArea } from './ui/scroll-area/index';
|
||||||
|
|
@ -42,7 +43,7 @@
|
||||||
unsubWebSocketMsg();
|
unsubWebSocketMsg();
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log('current department: ', get(departmentStore));
|
logger.info('current department: ', get(departmentStore));
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="grid grid-flow-row-dense grid-cols-3 grid-rows-3">
|
<div class="grid grid-flow-row-dense grid-cols-3 grid-rows-3">
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
<script lang="ts">
|
<script lang="ts">import { logger } from '$lib/core/utils/logger';
|
||||||
|
|
||||||
import { Button, type ButtonVariant } from './ui/button';
|
import { Button, type ButtonVariant } from './ui/button';
|
||||||
import * as Card from './ui/card/index';
|
import * as Card from './ui/card/index';
|
||||||
import Spinner from '$lib/components/ui/spinner/spinner.svelte';
|
import Spinner from '$lib/components/ui/spinner/spinner.svelte';
|
||||||
|
|
@ -15,7 +16,7 @@
|
||||||
let infoMap: any | undefined = $state();
|
let infoMap: any | undefined = $state();
|
||||||
|
|
||||||
let unsubMachineInfo = machineInfoStore.subscribe((mInfo) => {
|
let unsubMachineInfo = machineInfoStore.subscribe((mInfo) => {
|
||||||
console.log('get info', JSON.stringify(mInfo));
|
logger.info('get info', JSON.stringify(mInfo));
|
||||||
// check status web socket
|
// check status web socket
|
||||||
|
|
||||||
currentMachineInfo = mInfo;
|
currentMachineInfo = mInfo;
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
import { logger } from '$lib/core/utils/logger';
|
||||||
// {
|
// {
|
||||||
// "MixOrder": 0,
|
// "MixOrder": 0,
|
||||||
// "StringParam": "",
|
// "StringParam": "",
|
||||||
|
|
@ -21,7 +22,8 @@ import RecipelistValue from './recipelist-value.svelte';
|
||||||
import { DragHandle } from './recipelist-table.svelte';
|
import { DragHandle } from './recipelist-table.svelte';
|
||||||
import { createRawSnippet } from 'svelte';
|
import { createRawSnippet } from 'svelte';
|
||||||
import RecipelistMatSelect from './recipelist-mat-select.svelte';
|
import RecipelistMatSelect from './recipelist-mat-select.svelte';
|
||||||
import { recipeDataEvent } from '$lib/core/stores/recipeStore';
|
import { GlobalEventBus } from '$lib/core/utils/eventBus';
|
||||||
|
import { RECIPE_EVENTS } from './recipe-editor-events';
|
||||||
import RecipelistValueEditor from './recipelist-value-editor.svelte';
|
import RecipelistValueEditor from './recipelist-value-editor.svelte';
|
||||||
|
|
||||||
export type RecipelistMaterial = {
|
export type RecipelistMaterial = {
|
||||||
|
|
@ -92,10 +94,9 @@ export const columns: ColumnDef<RecipelistMaterial>[] = [
|
||||||
currentMat: row.original.material_id,
|
currentMat: row.original.material_id,
|
||||||
onMatChange: (value: any) => {
|
onMatChange: (value: any) => {
|
||||||
row.original.material_id = value;
|
row.original.material_id = value;
|
||||||
console.log('change mat', value);
|
logger.info('change mat', value);
|
||||||
recipeDataEvent.set({
|
GlobalEventBus.emit(RECIPE_EVENTS.MAT_CHANGE, {
|
||||||
event_type: 'mat_change',
|
mat_id: value,
|
||||||
payload: value,
|
|
||||||
index: row.original.id
|
index: row.original.id
|
||||||
});
|
});
|
||||||
row.toggleSelected(row.original.is_use);
|
row.toggleSelected(row.original.is_use);
|
||||||
|
|
@ -112,10 +113,9 @@ export const columns: ColumnDef<RecipelistMaterial>[] = [
|
||||||
row_uid: row.original.id,
|
row_uid: row.original.id,
|
||||||
mat_id: row.original.material_id,
|
mat_id: row.original.material_id,
|
||||||
onEditValue: (changes: any) => {
|
onEditValue: (changes: any) => {
|
||||||
// console.log('triggered on edit value', changes);
|
// logger.info('triggered on edit value', changes);
|
||||||
|
|
||||||
recipeDataEvent.set({
|
GlobalEventBus.emit(RECIPE_EVENTS.EDIT_CHANGE_VALUE_RPL, {
|
||||||
event_type: 'edit_change_value_rpl',
|
|
||||||
payload: JSON.parse(changes),
|
payload: JSON.parse(changes),
|
||||||
index: row.original.id
|
index: row.original.id
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
<script lang="ts">
|
<script lang="ts">import { logger } from '$lib/core/utils/logger';
|
||||||
|
|
||||||
import * as Tabs from '$lib/components/ui/tabs/index';
|
import * as Tabs from '$lib/components/ui/tabs/index';
|
||||||
import * as Card from '$lib/components/ui/card/index';
|
import * as Card from '$lib/components/ui/card/index';
|
||||||
import Label from '$lib/components/ui/label/label.svelte';
|
import Label from '$lib/components/ui/label/label.svelte';
|
||||||
|
|
@ -54,7 +55,8 @@
|
||||||
let materialSnapshot: any = $state();
|
let materialSnapshot: any = $state();
|
||||||
let machineInfoSnapshot: any = $state();
|
let machineInfoSnapshot: any = $state();
|
||||||
|
|
||||||
let recipeListMatState: RecipelistMaterial[] = $state([]);
|
let activeTab = $state('info');
|
||||||
|
|
||||||
let recipeListOriginal: RecipelistMaterial[] = $state([]);
|
let recipeListOriginal: RecipelistMaterial[] = $state([]);
|
||||||
|
|
||||||
let toppingSlotState: any = $state([]);
|
let toppingSlotState: any = $state([]);
|
||||||
|
|
@ -63,61 +65,52 @@
|
||||||
|
|
||||||
const recipeDetailDispatch = createEventDispatcher();
|
const recipeDetailDispatch = createEventDispatcher();
|
||||||
|
|
||||||
function remappingToColumn(data: any[]): RecipelistMaterial[] {
|
/** Map-based material lookup — O(1) instead of O(n) Array.filter per row. */
|
||||||
let ret: RecipelistMaterial[] = [];
|
function remappingToColumn(data: any[] | undefined, materials: any[] | undefined): RecipelistMaterial[] {
|
||||||
// expect recipelist
|
if (!data || data.length === 0) return [];
|
||||||
if (materialSnapshot) {
|
|
||||||
let d_cnt = 0;
|
|
||||||
for (let rpl of data) {
|
|
||||||
let mat = materialSnapshot.filter(
|
|
||||||
(x: any) => x['id'].toString() === rpl['materialPathId'].toString()
|
|
||||||
)[0];
|
|
||||||
|
|
||||||
// console.log('mat filter get', Object(mat), Object.keys(mat));
|
// Build Map<materialId_str, material> once for O(1) lookups
|
||||||
|
const matMap = new Map<string, any>();
|
||||||
let name = mat ? mat['materialOtherName'] : rpl['materialPathId'];
|
if (materials) {
|
||||||
|
for (const mat of materials) {
|
||||||
if (rpl['materialPathId'] === 0) {
|
matMap.set(mat.id.toString(), mat);
|
||||||
name = '-';
|
|
||||||
}
|
|
||||||
|
|
||||||
// let gen_id = generateRowId();
|
|
||||||
|
|
||||||
// console.log(`generated for ${rpl['materialPathId']} = ${gen_id}`);
|
|
||||||
|
|
||||||
ret.push({
|
|
||||||
id: d_cnt,
|
|
||||||
material_id: `${name} (${rpl['materialPathId']})`,
|
|
||||||
is_use: rpl['isUse'],
|
|
||||||
values: {
|
|
||||||
string_param: rpl['StringParam'],
|
|
||||||
mix_order: rpl['MixOrder'],
|
|
||||||
stir_time: rpl['stirTime'],
|
|
||||||
feed: {
|
|
||||||
pattern: rpl['FeedPattern'],
|
|
||||||
parameter: rpl['FeedParameter']
|
|
||||||
},
|
|
||||||
powder: {
|
|
||||||
gram: rpl['powderGram'],
|
|
||||||
time: rpl['powderTime']
|
|
||||||
},
|
|
||||||
syrup: {
|
|
||||||
gram: rpl['syrupGram'],
|
|
||||||
time: rpl['syrupTime']
|
|
||||||
},
|
|
||||||
water: {
|
|
||||||
cold: rpl['waterCold'],
|
|
||||||
yield: rpl['waterYield']
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
d_cnt++;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const ret: RecipelistMaterial[] = [];
|
||||||
|
for (let d_cnt = 0; d_cnt < data.length; d_cnt++) {
|
||||||
|
const rpl = data[d_cnt];
|
||||||
|
const mat = matMap.get(rpl['materialPathId'].toString());
|
||||||
|
|
||||||
|
let name = mat ? mat['materialOtherName'] : rpl['materialPathId'];
|
||||||
|
if (rpl['materialPathId'] === 0) {
|
||||||
|
name = '-';
|
||||||
|
}
|
||||||
|
|
||||||
|
ret.push({
|
||||||
|
id: d_cnt,
|
||||||
|
material_id: `${name} (${rpl['materialPathId']})`,
|
||||||
|
is_use: rpl['isUse'],
|
||||||
|
values: {
|
||||||
|
string_param: rpl['StringParam'],
|
||||||
|
mix_order: rpl['MixOrder'],
|
||||||
|
stir_time: rpl['stirTime'],
|
||||||
|
feed: { pattern: rpl['FeedPattern'], parameter: rpl['FeedParameter'] },
|
||||||
|
powder: { gram: rpl['powderGram'], time: rpl['powderTime'] },
|
||||||
|
syrup: { gram: rpl['syrupGram'], time: rpl['syrupTime'] },
|
||||||
|
water: { cold: rpl['waterCold'], yield: rpl['waterYield'] }
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
return ret;
|
return ret;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Reactively build recipe-list table data when inputs change. */
|
||||||
|
const recipeListMatState: RecipelistMaterial[] = $derived(
|
||||||
|
remappingToColumn(recipeData?.recipes, materialSnapshot)
|
||||||
|
);
|
||||||
|
|
||||||
async function getCurrentQueue() {
|
async function getCurrentQueue() {
|
||||||
let inst = adb.getAdbInstance();
|
let inst = adb.getAdbInstance();
|
||||||
if (inst) {
|
if (inst) {
|
||||||
|
|
@ -129,7 +122,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
let current_brewing = await adb.pull(currentRecipePath);
|
let current_brewing = await adb.pull(currentRecipePath);
|
||||||
// console.log(`current brewing queue: ${current_brewing}`);
|
// logger.info(`current brewing queue: ${current_brewing}`);
|
||||||
if (current_brewing === '') {
|
if (current_brewing === '') {
|
||||||
current_brewing = '{}';
|
current_brewing = '{}';
|
||||||
}
|
}
|
||||||
|
|
@ -159,7 +152,7 @@
|
||||||
async function sendTriggerBrewNow() {
|
async function sendTriggerBrewNow() {
|
||||||
// check queue ready
|
// check queue ready
|
||||||
// let currentBrewingQueue = await getCurrentQueue();
|
// let currentBrewingQueue = await getCurrentQueue();
|
||||||
// console.log('checking queue ... ', Object.keys(currentBrewingQueue).length);
|
// logger.info('checking queue ... ', Object.keys(currentBrewingQueue).length);
|
||||||
|
|
||||||
await sendToAndroid({
|
await sendToAndroid({
|
||||||
type: 'brew_prep',
|
type: 'brew_prep',
|
||||||
|
|
@ -176,10 +169,10 @@
|
||||||
//
|
//
|
||||||
let inst = adb.getAdbInstance();
|
let inst = adb.getAdbInstance();
|
||||||
if (inst) {
|
if (inst) {
|
||||||
console.log('check adb writer', isAdbWriterAvailable());
|
logger.info('check adb writer', isAdbWriterAvailable());
|
||||||
recipeDetailDispatch('brewNow');
|
recipeDetailDispatch('brewNow');
|
||||||
} else {
|
} else {
|
||||||
console.log('result check fail');
|
logger.info('result check fail');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -200,7 +193,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
async function checkChanges(productCode: string, original: any) {
|
async function checkChanges(productCode: string, original: any) {
|
||||||
// console.log('old', original, 'updated', recipeListMatState);
|
// logger.info('old', original, 'updated', recipeListMatState);
|
||||||
if (recipeListOriginal.length == 0) {
|
if (recipeListOriginal.length == 0) {
|
||||||
recipeListOriginal = original;
|
recipeListOriginal = original;
|
||||||
}
|
}
|
||||||
|
|
@ -246,8 +239,6 @@
|
||||||
} else if (refPage == 'brew') {
|
} else if (refPage == 'brew') {
|
||||||
materialSnapshot = get(materialFromMachineQuery);
|
materialSnapshot = get(materialFromMachineQuery);
|
||||||
}
|
}
|
||||||
|
|
||||||
recipeListMatState = remappingToColumn(recipeData['recipes']);
|
|
||||||
toppingSlotState = recipeData['ToppingSet'];
|
toppingSlotState = recipeData['ToppingSet'];
|
||||||
|
|
||||||
latestRecipeToppingData.set(toppingSlotState);
|
latestRecipeToppingData.set(toppingSlotState);
|
||||||
|
|
@ -257,12 +248,12 @@
|
||||||
let currentPricesFromServer = get(priceRecipeData);
|
let currentPricesFromServer = get(priceRecipeData);
|
||||||
let currentPrice = currentPricesFromServer[productCode] ?? '';
|
let currentPrice = currentPricesFromServer[productCode] ?? '';
|
||||||
|
|
||||||
console.log(currentPricesFromServer);
|
logger.info(currentPricesFromServer);
|
||||||
|
|
||||||
if (currentPrice != '') {
|
if (currentPrice != '') {
|
||||||
// has price
|
// has price
|
||||||
let priceParts = currentPrice.split(',');
|
let priceParts = currentPrice.split(',');
|
||||||
console.log('price part', priceParts);
|
logger.info('price part', priceParts);
|
||||||
try {
|
try {
|
||||||
let price = parseInt(priceParts[0]);
|
let price = parseInt(priceParts[0]);
|
||||||
let extraParam: string = priceParts[1] ?? '';
|
let extraParam: string = priceParts[1] ?? '';
|
||||||
|
|
@ -271,7 +262,7 @@
|
||||||
isMenuHideByPrice = extraParam.includes('hide=true');
|
isMenuHideByPrice = extraParam.includes('hide=true');
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('hide = ', extraParam);
|
logger.info('hide = ', extraParam);
|
||||||
|
|
||||||
menuCurrentPrice = price;
|
menuCurrentPrice = price;
|
||||||
|
|
||||||
|
|
@ -304,7 +295,7 @@
|
||||||
<!-- Menu Status -->
|
<!-- Menu Status -->
|
||||||
|
|
||||||
<div class="-mb-4 flex w-full flex-col gap-6">
|
<div class="-mb-4 flex w-full flex-col gap-6">
|
||||||
<Tabs.Root value="info">
|
<Tabs.Root bind:value={activeTab}>
|
||||||
<div class="flex flex-row justify-between">
|
<div class="flex flex-row justify-between">
|
||||||
<Tabs.List>
|
<Tabs.List>
|
||||||
<Tabs.Trigger value="info">Info</Tabs.Trigger>
|
<Tabs.Trigger value="info">Info</Tabs.Trigger>
|
||||||
|
|
@ -434,25 +425,28 @@
|
||||||
</Tabs.Content>
|
</Tabs.Content>
|
||||||
|
|
||||||
<Tabs.Content value="details">
|
<Tabs.Content value="details">
|
||||||
<!-- hide by machine state -->
|
<!-- Lazy-mount: RecipelistTable + sub-components only render when user clicks this tab -->
|
||||||
|
{#if activeTab === 'details'}
|
||||||
|
<!-- hide by machine state -->
|
||||||
|
|
||||||
{#if $machineInfoStore?.status == 'IDLE' || $machineInfoStore?.status == '' || refPage == 'overview'}
|
{#if $machineInfoStore?.status == 'IDLE' || $machineInfoStore?.status == '' || refPage == 'overview'}
|
||||||
<RecipelistTable
|
<RecipelistTable
|
||||||
data={recipeListMatState}
|
data={recipeListMatState}
|
||||||
{columns}
|
{columns}
|
||||||
onStateChange={checkChanges}
|
onStateChange={checkChanges}
|
||||||
{productCode}
|
{productCode}
|
||||||
/>
|
/>
|
||||||
{:else}
|
{:else}
|
||||||
<Card.Root>
|
<Card.Root>
|
||||||
<Card.Content>
|
<Card.Content>
|
||||||
<div class="grid h-[60vh] w-full place-items-center rounded-md border">
|
<div class="grid h-[60vh] w-full place-items-center rounded-md border">
|
||||||
<div class="m-4 flex flex-row gap-2">
|
<div class="m-4 flex flex-row gap-2">
|
||||||
<Spinner />Machine is working. Please wait for a moment.
|
<Spinner />Machine is working. Please wait for a moment.
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</Card.Content>
|
||||||
</Card.Content>
|
</Card.Root>
|
||||||
</Card.Root>
|
{/if}
|
||||||
{/if}
|
{/if}
|
||||||
</Tabs.Content>
|
</Tabs.Content>
|
||||||
</Tabs.Root>
|
</Tabs.Root>
|
||||||
|
|
|
||||||
94
src/lib/components/recipe-details/recipe-editor-events.ts
Normal file
94
src/lib/components/recipe-details/recipe-editor-events.ts
Normal file
|
|
@ -0,0 +1,94 @@
|
||||||
|
/**
|
||||||
|
* Recipe Editor Event Types
|
||||||
|
*
|
||||||
|
* Typed event definitions for the recipe-editor domain.
|
||||||
|
* Replaces the `recipeDataEvent` writable store pattern with GlobalEventBus.
|
||||||
|
*
|
||||||
|
* Event name convention: `recipe:${action}`
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { GlobalEventBus } from '$lib/core/utils/eventBus';
|
||||||
|
|
||||||
|
// ─── Event name constants ───────────────────────────────────────────
|
||||||
|
export const RECIPE_EVENTS = {
|
||||||
|
MAT_CHANGE: 'recipe:mat_change',
|
||||||
|
EDIT_MAT_FIELD: 'recipe:edit_mat_field',
|
||||||
|
EDIT_MAT_FIELD_PREP: 'recipe:edit_mat_field_prep',
|
||||||
|
SAVE_MAT_FIELD: 'recipe:save_mat_field',
|
||||||
|
EDIT_CHANGE_VALUE_RPL: 'recipe:edit_change_value_rpl',
|
||||||
|
REVERT_CHANGE: 'recipe:revert_change'
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
// ─── Payload types ──────────────────────────────────────────────────
|
||||||
|
|
||||||
|
export interface EditMatFieldPrepPayload {
|
||||||
|
mat_id: number;
|
||||||
|
mat_type: string;
|
||||||
|
mat_name: string;
|
||||||
|
params: Record<string, string>;
|
||||||
|
toppings: any[];
|
||||||
|
current_topping_group: any;
|
||||||
|
current_topping_list: any[];
|
||||||
|
has_mix_ord: boolean;
|
||||||
|
feed: { pattern: number; parameter: number };
|
||||||
|
water: { cold: number; yield: number };
|
||||||
|
powder: { gram: number; time: number };
|
||||||
|
syrup: { gram: number; time: number };
|
||||||
|
stir_time: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SaveMatFieldPayload {
|
||||||
|
source: EditMatFieldPrepPayload;
|
||||||
|
change: Record<string, any>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface EditChangeValueRplPayload extends SaveMatFieldPayload {
|
||||||
|
current_toppings: any[];
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Module-level pending save data ────────────────────────────────
|
||||||
|
// Replaces direct `get(recipeDataEvent)` reads in recipe-editor-dialog.svelte.
|
||||||
|
// The recipe-dialog reads the latest pending save data synchronously;
|
||||||
|
// GlobalEventBus has no "get latest value" so we store it here.
|
||||||
|
|
||||||
|
interface PendingSaveData {
|
||||||
|
change: Record<string, any>;
|
||||||
|
payload: SaveMatFieldPayload & { current_toppings?: any[] };
|
||||||
|
index: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
let _latestPendingSave: PendingSaveData | null = null;
|
||||||
|
|
||||||
|
export function setPendingSaveData(data: PendingSaveData): void {
|
||||||
|
_latestPendingSave = data;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getPendingSaveData(): PendingSaveData | null {
|
||||||
|
return _latestPendingSave;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function clearPendingSaveData(): void {
|
||||||
|
_latestPendingSave = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Typed emit helpers ────────────────────────────────────────────
|
||||||
|
|
||||||
|
export function emitMatChange(matId: string, index: number): void {
|
||||||
|
GlobalEventBus.emit(RECIPE_EVENTS.MAT_CHANGE, { mat_id: matId, index });
|
||||||
|
}
|
||||||
|
|
||||||
|
export function emitEditMatField(index: number): void {
|
||||||
|
GlobalEventBus.emit(RECIPE_EVENTS.EDIT_MAT_FIELD, { index });
|
||||||
|
}
|
||||||
|
|
||||||
|
export function emitEditMatFieldPrep(payload: EditMatFieldPrepPayload, index: number): void {
|
||||||
|
GlobalEventBus.emit(RECIPE_EVENTS.EDIT_MAT_FIELD_PREP, { payload, index });
|
||||||
|
}
|
||||||
|
|
||||||
|
export function emitSaveMatField(payload: SaveMatFieldPayload, index: number): void {
|
||||||
|
GlobalEventBus.emit(RECIPE_EVENTS.SAVE_MAT_FIELD, { payload, index });
|
||||||
|
}
|
||||||
|
|
||||||
|
export function emitRevertChange(index: number): void {
|
||||||
|
GlobalEventBus.emit(RECIPE_EVENTS.REVERT_CHANGE, { index });
|
||||||
|
}
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
<script lang="ts">
|
<script lang="ts">import { logger } from '$lib/core/utils/logger';
|
||||||
|
|
||||||
import { onMount } from 'svelte';
|
import { onMount } from 'svelte';
|
||||||
import { Button } from '../ui/button';
|
import { Button } from '../ui/button';
|
||||||
import * as DropdownMenu from '$lib/components/ui/dropdown-menu/index';
|
import * as DropdownMenu from '$lib/components/ui/dropdown-menu/index';
|
||||||
|
|
@ -28,7 +29,7 @@
|
||||||
if (refPage === 'brew') allMatData = get(materialFromMachineQuery);
|
if (refPage === 'brew') allMatData = get(materialFromMachineQuery);
|
||||||
else if (refPage === 'overview') allMatData = get(materialFromServerQuery);
|
else if (refPage === 'overview') allMatData = get(materialFromServerQuery);
|
||||||
|
|
||||||
// console.log('all mat data', JSON.stringify(allMatData));
|
// logger.info('all mat data', JSON.stringify(allMatData));
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
<script module>
|
<script module>import { logger } from '$lib/core/utils/logger';
|
||||||
|
|
||||||
export { DragHandle };
|
export { DragHandle };
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
@ -76,14 +77,14 @@
|
||||||
getSortedRowModel: getSortedRowModel(),
|
getSortedRowModel: getSortedRowModel(),
|
||||||
getFacetedRowModel: getFacetedRowModel(),
|
getFacetedRowModel: getFacetedRowModel(),
|
||||||
onStateChange: async (updater) => {
|
onStateChange: async (updater) => {
|
||||||
console.log('table state change', data);
|
logger.info('table state change', data);
|
||||||
await onStateChange(
|
await onStateChange(
|
||||||
productCode,
|
productCode,
|
||||||
table.getRowModel().rows.map((x) => x.original)
|
table.getRowModel().rows.map((x) => x.original)
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
onSortingChange: async (updater) => {
|
onSortingChange: async (updater) => {
|
||||||
console.log('triggering sorting');
|
logger.info('triggering sorting');
|
||||||
if (typeof updater === 'function') {
|
if (typeof updater === 'function') {
|
||||||
sorting = updater(sorting);
|
sorting = updater(sorting);
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -100,7 +101,7 @@
|
||||||
rowSelection = updater(rowSelection);
|
rowSelection = updater(rowSelection);
|
||||||
let rows = table.getRowModel().rows;
|
let rows = table.getRowModel().rows;
|
||||||
|
|
||||||
// console.log('state size', data, rows);
|
// logger.info('state size', data, rows);
|
||||||
} else {
|
} else {
|
||||||
rowSelection = updater;
|
rowSelection = updater;
|
||||||
}
|
}
|
||||||
|
|
@ -126,10 +127,13 @@
|
||||||
// @ts-expect-error @dnd-kit/adbstract types are botched atm
|
// @ts-expect-error @dnd-kit/adbstract types are botched atm
|
||||||
RestrictToVerticalAxis
|
RestrictToVerticalAxis
|
||||||
]}
|
]}
|
||||||
onDragEnd={(e) => {
|
onDragEnd={async (e) => {
|
||||||
// snap
|
// snap
|
||||||
data = move(data as any, e as any);
|
data = move(data as any, e as any);
|
||||||
//
|
// Notify parent of reorder so changes are saved
|
||||||
|
if (onStateChange) {
|
||||||
|
await onStateChange(productCode, data);
|
||||||
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<ScrollArea class="h-[60vh] w-full rounded-md border" type="always">
|
<ScrollArea class="h-[60vh] w-full rounded-md border" type="always">
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
<script lang="ts">
|
<script lang="ts">import { logger } from '$lib/core/utils/logger';
|
||||||
|
|
||||||
import { CupSodaIcon, PencilIcon, ChevronRightIcon, StarsIcon } from '@lucide/svelte/icons';
|
import { CupSodaIcon, PencilIcon, ChevronRightIcon, StarsIcon } from '@lucide/svelte/icons';
|
||||||
import Button from '$lib/components/ui/button/button.svelte';
|
import Button from '$lib/components/ui/button/button.svelte';
|
||||||
import * as Select from '$lib/components/ui/select/index';
|
import * as Select from '$lib/components/ui/select/index';
|
||||||
|
|
@ -10,17 +11,18 @@
|
||||||
|
|
||||||
import {
|
import {
|
||||||
latestRecipeToppingData,
|
latestRecipeToppingData,
|
||||||
recipeDataEvent,
|
|
||||||
toppingGroupFromServerQuery,
|
toppingGroupFromServerQuery,
|
||||||
toppingListFromServerQuery,
|
toppingListFromServerQuery,
|
||||||
toppingGroupFromMachineQuery,
|
toppingGroupFromMachineQuery,
|
||||||
toppingListFromMachineQuery,
|
toppingListFromMachineQuery,
|
||||||
referenceFromPage
|
referenceFromPage
|
||||||
} from '$lib/core/stores/recipeStore';
|
} from '$lib/core/stores/recipeStore';
|
||||||
import { onMount } from 'svelte';
|
import { onDestroy, onMount } from 'svelte';
|
||||||
import { addNotification } from '$lib/core/stores/noti';
|
import { addNotification } from '$lib/core/stores/noti';
|
||||||
import { get } from 'svelte/store';
|
import { get } from 'svelte/store';
|
||||||
import { actionReport, ValueEvent } from './value_event';
|
import { actionReport, ValueEvent } from './value_event';
|
||||||
|
import { GlobalEventBus } from '$lib/core/utils/eventBus';
|
||||||
|
import { RECIPE_EVENTS, emitEditMatField, emitSaveMatField } from './recipe-editor-events';
|
||||||
import ScrollArea from '../ui/scroll-area/scroll-area.svelte';
|
import ScrollArea from '../ui/scroll-area/scroll-area.svelte';
|
||||||
import { sendMessage } from '$lib/core/handlers/ws_messageSender';
|
import { sendMessage } from '$lib/core/handlers/ws_messageSender';
|
||||||
import { auth } from '$lib/core/stores/auth';
|
import { auth } from '$lib/core/stores/auth';
|
||||||
|
|
@ -38,6 +40,8 @@
|
||||||
|
|
||||||
let warnUserNotSaveChange = $state(false);
|
let warnUserNotSaveChange = $state(false);
|
||||||
|
|
||||||
|
let unsubEvents: (() => void) | null = null;
|
||||||
|
|
||||||
// --------------------------------------------------
|
// --------------------------------------------------
|
||||||
|
|
||||||
let toggledOpenPowder = $state(false);
|
let toggledOpenPowder = $state(false);
|
||||||
|
|
@ -81,12 +85,12 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleEvents(event: { event_type: string; payload: any; index: number | undefined }) {
|
function handleEvents(event: { event_type: string; payload: any; index: number | undefined }) {
|
||||||
// console.log('triggered event', event.event_type, JSON.stringify(event.payload));
|
// logger.info('triggered event', event.event_type, JSON.stringify(event.payload));
|
||||||
if (event.event_type === 'edit_mat_field_prep') {
|
if (event.event_type === 'edit_mat_field_prep') {
|
||||||
// update value, do re-render
|
// update value, do re-render
|
||||||
if (event.payload) {
|
if (event.payload) {
|
||||||
current_editing_data = event.payload;
|
current_editing_data = event.payload;
|
||||||
// console.log(`GET requested data: ${JSON.stringify(current_editing_data)}`);
|
// logger.info(`GET requested data: ${JSON.stringify(current_editing_data)}`);
|
||||||
|
|
||||||
// default topping
|
// default topping
|
||||||
if (
|
if (
|
||||||
|
|
@ -124,12 +128,8 @@
|
||||||
function requestDataFromDisplay() {
|
function requestDataFromDisplay() {
|
||||||
// pause show until have data
|
// pause show until have data
|
||||||
sheetOpenState = false;
|
sheetOpenState = false;
|
||||||
console.log('sending request edit', row_id);
|
logger.info('sending request edit', row_id);
|
||||||
recipeDataEvent.set({
|
emitEditMatField(row_id);
|
||||||
event_type: 'edit_mat_field',
|
|
||||||
payload: row_id,
|
|
||||||
index: row_id
|
|
||||||
});
|
|
||||||
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
if (current_editing_data === undefined) {
|
if (current_editing_data === undefined) {
|
||||||
|
|
@ -151,7 +151,7 @@
|
||||||
try {
|
try {
|
||||||
curr_val = curr_val[field];
|
curr_val = curr_val[field];
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('try get field error: ', e);
|
logger.error('try get field error: ', e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -174,18 +174,14 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function saveEditingValue() {
|
function saveEditingValue() {
|
||||||
console.log('saving value ...', value_event_state);
|
logger.info('saving value ...', value_event_state);
|
||||||
if (value_event_state === ValueEvent.EDITED || value_event_state === ValueEvent.SAVED) {
|
if (value_event_state === ValueEvent.EDITED || value_event_state === ValueEvent.SAVED) {
|
||||||
let payload = {
|
let payload = {
|
||||||
source: current_editing_data,
|
source: current_editing_data,
|
||||||
change: changed_data
|
change: changed_data
|
||||||
};
|
};
|
||||||
recipeDataEvent.set({
|
emitSaveMatField(payload, row_id);
|
||||||
event_type: 'save_mat_field',
|
|
||||||
payload,
|
|
||||||
index: row_id
|
|
||||||
});
|
|
||||||
|
|
||||||
value_event_state = ValueEvent.SAVED;
|
value_event_state = ValueEvent.SAVED;
|
||||||
|
|
||||||
|
|
@ -208,7 +204,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleToppingGroupChange(v: any) {
|
function handleToppingGroupChange(v: any) {
|
||||||
console.log('change topping group');
|
logger.info('change topping group');
|
||||||
|
|
||||||
// TODO: clear topping list, otherwise, it will continue to append filter to list by each navigate back and forth
|
// TODO: clear topping list, otherwise, it will continue to append filter to list by each navigate back and forth
|
||||||
|
|
||||||
|
|
@ -226,11 +222,11 @@
|
||||||
if (changed_data['toppings'] == undefined || changed_data['toppings'] == null) {
|
if (changed_data['toppings'] == undefined || changed_data['toppings'] == null) {
|
||||||
changed_data['toppings'] = new Array<any>(toppings_length);
|
changed_data['toppings'] = new Array<any>(toppings_length);
|
||||||
|
|
||||||
console.log('filling change topping', JSON.stringify(changed_data));
|
logger.info('filling change topping', JSON.stringify(changed_data));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('current editing data', current_editing_data['toppings']);
|
logger.info('current editing data', current_editing_data['toppings']);
|
||||||
|
|
||||||
let idx = getToppingSlotIndex(current_editing_data['mat_id']);
|
let idx = getToppingSlotIndex(current_editing_data['mat_id']);
|
||||||
|
|
||||||
|
|
@ -238,7 +234,7 @@
|
||||||
let current_selection = current_editing_data['toppings'][idx];
|
let current_selection = current_editing_data['toppings'][idx];
|
||||||
|
|
||||||
// let default_from_recipe = current_editing_data['toppings'][idx]['ListGroupID'][0];
|
// let default_from_recipe = current_editing_data['toppings'][idx]['ListGroupID'][0];
|
||||||
console.log(`Current TG: `, JSON.stringify(current_selection));
|
logger.info(`Current TG: `, JSON.stringify(current_selection));
|
||||||
if (current_selection['groupID'] !== undefined || current_selection['groupID'] !== null) {
|
if (current_selection['groupID'] !== undefined || current_selection['groupID'] !== null) {
|
||||||
try {
|
try {
|
||||||
changed_data['toppings'][idx] = current_selection;
|
changed_data['toppings'][idx] = current_selection;
|
||||||
|
|
@ -246,13 +242,13 @@
|
||||||
changed_data['toppings'][idx]['defaultIDSelect'] = selected_topping_list_id;
|
changed_data['toppings'][idx]['defaultIDSelect'] = selected_topping_list_id;
|
||||||
changed_data['toppings'][idx]['ListGroupID'][0] = selected_topping_list_id;
|
changed_data['toppings'][idx]['ListGroupID'][0] = selected_topping_list_id;
|
||||||
} catch (topping_group_exception) {
|
} catch (topping_group_exception) {
|
||||||
console.error('Error on topping group select', topping_group_exception);
|
logger.error('Error on topping group select', topping_group_exception);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleToppingListChange(v: any) {
|
function handleToppingListChange(v: any) {
|
||||||
console.log('Topping list chose ');
|
logger.info('Topping list chose ');
|
||||||
selected_topping_list_id = v.id;
|
selected_topping_list_id = v.id;
|
||||||
|
|
||||||
// set to edit state even if selected same group again
|
// set to edit state even if selected same group again
|
||||||
|
|
@ -278,7 +274,7 @@
|
||||||
changed_data['toppings'][idx]['defaultIDSelect'] = selected_topping_list_id;
|
changed_data['toppings'][idx]['defaultIDSelect'] = selected_topping_list_id;
|
||||||
changed_data['toppings'][idx]['ListGroupID'][0] = selected_topping_list_id;
|
changed_data['toppings'][idx]['ListGroupID'][0] = selected_topping_list_id;
|
||||||
} catch (topping_list_exception) {
|
} catch (topping_list_exception) {
|
||||||
console.error('Error on topping group select', topping_list_exception);
|
logger.error('Error on topping group select', topping_list_exception);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -290,7 +286,7 @@
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
sheetOpenState = false;
|
sheetOpenState = false;
|
||||||
// console.log('sheet open? ', sheetOpenState);
|
// logger.info('sheet open? ', sheetOpenState);
|
||||||
|
|
||||||
let refFrom = get(referenceFromPage);
|
let refFrom = get(referenceFromPage);
|
||||||
categories = $state.snapshot(
|
categories = $state.snapshot(
|
||||||
|
|
@ -303,11 +299,24 @@
|
||||||
// save ref
|
// save ref
|
||||||
currentRef = refFrom;
|
currentRef = refFrom;
|
||||||
|
|
||||||
return recipeDataEvent.subscribe((event) => {
|
unsubEvents = (() => {
|
||||||
if (event !== null && event.index !== undefined && event.index === row_id) {
|
const unsub = GlobalEventBus.on(RECIPE_EVENTS.EDIT_MAT_FIELD_PREP, (data: any) => {
|
||||||
handleEvents(event);
|
if (data && data.index === row_id) {
|
||||||
}
|
handleEvents({
|
||||||
});
|
event_type: 'edit_mat_field_prep',
|
||||||
|
payload: data.payload,
|
||||||
|
index: data.index
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return () => {
|
||||||
|
unsub();
|
||||||
|
};
|
||||||
|
})();
|
||||||
|
});
|
||||||
|
|
||||||
|
onDestroy(() => {
|
||||||
|
if (unsubEvents) unsubEvents();
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
<script lang="ts">
|
<script lang="ts">import { logger } from '$lib/core/utils/logger';
|
||||||
|
|
||||||
import { onDestroy, onMount } from 'svelte';
|
import { onDestroy, onMount } from 'svelte';
|
||||||
import type { RecipelistMaterial } from './columns';
|
import type { RecipelistMaterial } from './columns';
|
||||||
import {
|
import {
|
||||||
|
|
@ -14,7 +15,6 @@
|
||||||
import * as Tooltip from '../ui/tooltip/index';
|
import * as Tooltip from '../ui/tooltip/index';
|
||||||
import {
|
import {
|
||||||
latestRecipeToppingData,
|
latestRecipeToppingData,
|
||||||
recipeDataEvent,
|
|
||||||
toppingGroupFromServerQuery,
|
toppingGroupFromServerQuery,
|
||||||
toppingListFromServerQuery,
|
toppingListFromServerQuery,
|
||||||
toppingGroupFromMachineQuery,
|
toppingGroupFromMachineQuery,
|
||||||
|
|
@ -22,6 +22,13 @@
|
||||||
referenceFromPage
|
referenceFromPage
|
||||||
} from '$lib/core/stores/recipeStore';
|
} from '$lib/core/stores/recipeStore';
|
||||||
import { get } from 'svelte/store';
|
import { get } from 'svelte/store';
|
||||||
|
import { GlobalEventBus } from '$lib/core/utils/eventBus';
|
||||||
|
import {
|
||||||
|
RECIPE_EVENTS,
|
||||||
|
emitEditMatFieldPrep,
|
||||||
|
setPendingSaveData,
|
||||||
|
clearPendingSaveData
|
||||||
|
} from './recipe-editor-events';
|
||||||
import { sendMessage } from '$lib/core/handlers/ws_messageSender';
|
import { sendMessage } from '$lib/core/handlers/ws_messageSender';
|
||||||
import { auth } from '$lib/core/stores/auth';
|
import { auth } from '$lib/core/stores/auth';
|
||||||
import { departmentStore } from '$lib/core/stores/departments';
|
import { departmentStore } from '$lib/core/stores/departments';
|
||||||
|
|
@ -52,6 +59,15 @@
|
||||||
let currentStringParams: any = $state({});
|
let currentStringParams: any = $state({});
|
||||||
let currentMaterialInt: number = $state(0);
|
let currentMaterialInt: number = $state(0);
|
||||||
|
|
||||||
|
// Local reactive copies of display values so applyChanges() triggers
|
||||||
|
// immediate template re-render (mutating $props() objects directly
|
||||||
|
// does not reliably trigger Svelte 5 deep proxy reactivity).
|
||||||
|
let displayWater = $state<{ cold: number; yield: number }>({ ...water });
|
||||||
|
let displayStirTime = $state<number>(stir_time);
|
||||||
|
let displayFeed = $state<{ pattern: number; parameter: number }>({ ...feed });
|
||||||
|
let displayPowder = $state<{ gram: number; time: number }>({ ...powder });
|
||||||
|
let displaySyrup = $state<{ gram: number; time: number }>({ ...syrup });
|
||||||
|
|
||||||
// toppings
|
// toppings
|
||||||
let currentToppings: any = $state([]);
|
let currentToppings: any = $state([]);
|
||||||
let oldToppings: any = $state(null);
|
let oldToppings: any = $state(null);
|
||||||
|
|
@ -62,6 +78,8 @@
|
||||||
let currentToppingNamesOnly = $state('');
|
let currentToppingNamesOnly = $state('');
|
||||||
let oldToppingNamesOnly = $state(null);
|
let oldToppingNamesOnly = $state(null);
|
||||||
|
|
||||||
|
let toppingDataLoading = $state(true);
|
||||||
|
|
||||||
let unsubRecipeDataEvent: any;
|
let unsubRecipeDataEvent: any;
|
||||||
let unsubRecipeTopping: any;
|
let unsubRecipeTopping: any;
|
||||||
|
|
||||||
|
|
@ -85,7 +103,7 @@
|
||||||
let key = kv[0];
|
let key = kv[0];
|
||||||
let value = kv[1] ?? '';
|
let value = kv[1] ?? '';
|
||||||
|
|
||||||
// console.log('key', key, 'value', value);
|
// logger.info('key', key, 'value', value);
|
||||||
currentStringParams[key] = value;
|
currentStringParams[key] = value;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -94,9 +112,9 @@
|
||||||
|
|
||||||
function getPowderSyrupValue() {
|
function getPowderSyrupValue() {
|
||||||
if (currentMaterialType === 'powder' || currentMaterialType === 'bean') {
|
if (currentMaterialType === 'powder' || currentMaterialType === 'bean') {
|
||||||
return powder.gram;
|
return displayPowder.gram;
|
||||||
} else if (currentMaterialType === 'syrup') {
|
} else if (currentMaterialType === 'syrup') {
|
||||||
return syrup.gram;
|
return displaySyrup.gram;
|
||||||
}
|
}
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
@ -126,26 +144,36 @@
|
||||||
refFrom === 'overview' ? toppingListFromServerQuery : toppingListFromMachineQuery
|
refFrom === 'overview' ? toppingListFromServerQuery : toppingListFromMachineQuery
|
||||||
);
|
);
|
||||||
|
|
||||||
console.log('old topping', oldToppings[getToppingSlot()]);
|
// Guard: bail out if topping slot data is null/undefined (unused slot)
|
||||||
|
if (!current_row_topping) {
|
||||||
|
currentToppingInRow = { name: 'Empty', otherName: 'Empty', id: 0 };
|
||||||
|
selectableToppingInGroup = [{ name: 'Empty', otherName: 'Empty', id: 0 }];
|
||||||
|
currentToppingNamesOnly = '-';
|
||||||
|
oldToppingNamesOnly = null;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
console.log('current topping', current_row_topping);
|
logger.info('old topping', oldToppings?.[getToppingSlot()]);
|
||||||
|
|
||||||
|
logger.info('current topping', current_row_topping);
|
||||||
oldToppingNamesOnly = null; // reset first
|
oldToppingNamesOnly = null; // reset first
|
||||||
if (oldToppings[getToppingSlot()] != current_row_topping) {
|
if (oldToppings?.[getToppingSlot()] != null && oldToppings[getToppingSlot()] != current_row_topping) {
|
||||||
console.log('detect change on topping row', row_uid);
|
logger.info('detect change on topping row', row_uid);
|
||||||
|
|
||||||
|
let oldSlot = oldToppings[getToppingSlot()];
|
||||||
let groupIDchange =
|
let groupIDchange =
|
||||||
oldToppings[getToppingSlot()]['groupID'] !== current_row_topping['groupID'];
|
oldSlot['groupID'] !== current_row_topping['groupID'];
|
||||||
let defaultIDchange =
|
let defaultIDchange =
|
||||||
oldToppings[getToppingSlot()]['defaultIDSelect'] !== current_row_topping['defaultIDSelect'];
|
oldSlot['defaultIDSelect'] !== current_row_topping['defaultIDSelect'];
|
||||||
|
|
||||||
console.log('group change', groupIDchange, 'default id change', defaultIDchange);
|
logger.info('group change', groupIDchange, 'default id change', defaultIDchange);
|
||||||
|
|
||||||
if (!groupIDchange && !defaultIDchange) {
|
if (!groupIDchange && !defaultIDchange) {
|
||||||
oldToppingNamesOnly = null;
|
oldToppingNamesOnly = null;
|
||||||
} else {
|
} else {
|
||||||
// has change, display old name
|
// has change, display old name
|
||||||
let oldGroupData = groupQuery.find(
|
let oldGroupData = groupQuery.find(
|
||||||
(x: any) => x.groupID.toString() === oldToppings[getToppingSlot()]['groupID']
|
(x: any) => x.groupID.toString() === oldSlot['groupID']
|
||||||
);
|
);
|
||||||
let oldListData = listQuery.filter(
|
let oldListData = listQuery.filter(
|
||||||
(x: any) =>
|
(x: any) =>
|
||||||
|
|
@ -153,7 +181,7 @@
|
||||||
Object.keys(oldGroupData).includes('idInGroup') &&
|
Object.keys(oldGroupData).includes('idInGroup') &&
|
||||||
oldGroupData['idInGroup'].split(',').includes(x.id.toString())
|
oldGroupData['idInGroup'].split(',').includes(x.id.toString())
|
||||||
);
|
);
|
||||||
oldToppingNamesOnly = oldGroupData['otherName'];
|
oldToppingNamesOnly = oldGroupData?.['otherName'] ?? null;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
oldToppingNamesOnly = null;
|
oldToppingNamesOnly = null;
|
||||||
|
|
@ -169,7 +197,7 @@
|
||||||
groupData['idInGroup'].split(',').includes(x.id.toString())
|
groupData['idInGroup'].split(',').includes(x.id.toString())
|
||||||
);
|
);
|
||||||
|
|
||||||
console.log('topping data', JSON.stringify(groupData), 'list', listData.length);
|
logger.info('topping data', JSON.stringify(groupData), 'list', listData.length);
|
||||||
|
|
||||||
currentToppingNamesOnly = groupData ? (groupData['otherName'] ?? '-') : '-';
|
currentToppingNamesOnly = groupData ? (groupData['otherName'] ?? '-') : '-';
|
||||||
|
|
||||||
|
|
@ -203,15 +231,19 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
function getCurrentSelectedToppingList() {
|
function getCurrentSelectedToppingList() {
|
||||||
// FIXME: show unknown on preview
|
let slot = getToppingSlot();
|
||||||
|
if (!currentToppings || !currentToppings[slot] || !currentToppings[slot]['ListGroupID']) {
|
||||||
|
return 'Loading...';
|
||||||
|
}
|
||||||
|
|
||||||
|
let listGroupId = currentToppings[slot]['ListGroupID'];
|
||||||
let current_selected = selectableToppingInGroup.find(
|
let current_selected = selectableToppingInGroup.find(
|
||||||
(x) => x.id === currentToppings[getToppingSlot()]['ListGroupID'][0]
|
(x) => x.id === (Array.isArray(listGroupId) ? listGroupId[0] : listGroupId)
|
||||||
);
|
);
|
||||||
|
|
||||||
console.log('current selected topping list', row_uid, current_selected);
|
logger.info('current selected topping list', row_uid, current_selected);
|
||||||
|
|
||||||
if (currentToppings[getToppingSlot()]['ListGroupID'][0] === 0) {
|
if (!currentToppings[slot]['ListGroupID'] || currentToppings[slot]['ListGroupID'][0] === 0) {
|
||||||
return 'Empty';
|
return 'Empty';
|
||||||
}
|
}
|
||||||
if (current_selected === undefined || current_selected === null) {
|
if (current_selected === undefined || current_selected === null) {
|
||||||
|
|
@ -226,13 +258,20 @@
|
||||||
|
|
||||||
extractStringParam();
|
extractStringParam();
|
||||||
|
|
||||||
|
// Sync local $state copies from prop values on mount
|
||||||
|
displayWater = { ...water };
|
||||||
|
displayStirTime = stir_time;
|
||||||
|
displayFeed = { ...feed };
|
||||||
|
displayPowder = { ...powder };
|
||||||
|
displaySyrup = { ...syrup };
|
||||||
|
|
||||||
if (isToppingId(mat_id) && onDetectToppingSlot) {
|
if (isToppingId(mat_id) && onDetectToppingSlot) {
|
||||||
// FIXME: this should not be updated yet, must be updated after user pressed save only
|
// FIXME: this should not be updated yet, must be updated after user pressed save only
|
||||||
let latest_topping_data_snapshot = $state.snapshot(get(latestRecipeToppingData));
|
let latest_topping_data_snapshot = $state.snapshot(get(latestRecipeToppingData));
|
||||||
currentToppings = latest_topping_data_snapshot;
|
currentToppings = latest_topping_data_snapshot;
|
||||||
// console.log('current topping in initialize', currentToppings);
|
// logger.info('current topping in initialize', currentToppings);
|
||||||
if (oldToppings == null) {
|
if (oldToppings == null) {
|
||||||
// console.log('saving original topping', oldToppings);
|
// logger.info('saving original topping', oldToppings);
|
||||||
oldToppings = $state.snapshot(currentToppings);
|
oldToppings = $state.snapshot(currentToppings);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -248,15 +287,15 @@
|
||||||
let mat_type_t1 = getMaterialType(mat_num);
|
let mat_type_t1 = getMaterialType(mat_num);
|
||||||
let mat_type_t2 = getMaterialType(convertFromInterProductCode(mat_num));
|
let mat_type_t2 = getMaterialType(convertFromInterProductCode(mat_num));
|
||||||
|
|
||||||
// console.log('type get', mat_type_t1, mat_type_t2);
|
// logger.info('type get', mat_type_t1, mat_type_t2);
|
||||||
currentMaterialType = mat_type_t1 === mat_type_t2 ? mat_type_t1 : mat_type_t2;
|
currentMaterialType = mat_type_t1 === mat_type_t2 ? mat_type_t1 : mat_type_t2;
|
||||||
|
|
||||||
// if (hasMixOrder) {
|
// if (hasMixOrder) {
|
||||||
// console.log('detect mix order', mat_num);
|
// logger.info('detect mix order', mat_num);
|
||||||
// }
|
// }
|
||||||
|
|
||||||
// if (feed.parameter > 0 || feed.pattern > 0) {
|
// if (feed.parameter > 0 || feed.pattern > 0) {
|
||||||
// console.log('has feed fields', JSON.stringify(feed));
|
// logger.info('has feed fields', JSON.stringify(feed));
|
||||||
// }
|
// }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -267,127 +306,177 @@
|
||||||
for (const key of keys) {
|
for (const key of keys) {
|
||||||
if (key == 'toppings' && currentMaterialType == 'topping') {
|
if (key == 'toppings' && currentMaterialType == 'topping') {
|
||||||
let topping_change = value[key][getToppingSlot()];
|
let topping_change = value[key][getToppingSlot()];
|
||||||
console.log('topping applying', topping_change);
|
logger.info('topping applying', topping_change);
|
||||||
currentToppings[getToppingSlot()] = topping_change;
|
currentToppings[getToppingSlot()] = topping_change;
|
||||||
} else {
|
} else {
|
||||||
console.log('apply change on recipelist value', key);
|
logger.info('apply change on recipelist value', key);
|
||||||
|
|
||||||
if (key.startsWith('powder')) {
|
if (key.startsWith('powder')) {
|
||||||
if (key.endsWith('gram')) {
|
if (key.endsWith('gram')) {
|
||||||
powder.gram = parseInt(value[key]);
|
displayPowder.gram = parseInt(value[key]);
|
||||||
} else if (key.endsWith('time')) {
|
} else if (key.endsWith('time')) {
|
||||||
powder.time = parseInt(value[key]);
|
displayPowder.time = parseInt(value[key]);
|
||||||
}
|
}
|
||||||
} else if (key.startsWith('syrup')) {
|
} else if (key.startsWith('syrup')) {
|
||||||
if (key.endsWith('gram')) {
|
if (key.endsWith('gram')) {
|
||||||
syrup.gram = parseInt(value[key]);
|
displaySyrup.gram = parseInt(value[key]);
|
||||||
} else if (key.endsWith('time')) {
|
} else if (key.endsWith('time')) {
|
||||||
syrup.time = parseInt(value[key]);
|
displaySyrup.time = parseInt(value[key]);
|
||||||
}
|
}
|
||||||
} else if (key.startsWith('water')) {
|
} else if (key.startsWith('water')) {
|
||||||
if (key.endsWith('yield')) {
|
if (key.endsWith('yield')) {
|
||||||
water.yield = parseInt(value[key]);
|
displayWater.yield = parseInt(value[key]);
|
||||||
} else if (key.endsWith('cold')) {
|
} else if (key.endsWith('cold')) {
|
||||||
water.cold = parseInt(value[key]);
|
displayWater.cold = parseInt(value[key]);
|
||||||
}
|
}
|
||||||
} else if (key.startsWith('feed')) {
|
} else if (key.startsWith('feed')) {
|
||||||
if (key.endsWith('pattern')) {
|
if (key.endsWith('pattern')) {
|
||||||
feed.pattern = parseInt(value[key]);
|
displayFeed.pattern = parseInt(value[key]);
|
||||||
} else if (key.endsWith('parameter')) {
|
} else if (key.endsWith('parameter')) {
|
||||||
feed.parameter = parseInt(value[key]);
|
displayFeed.parameter = parseInt(value[key]);
|
||||||
}
|
}
|
||||||
} else if (key == 'stir_time') {
|
} else if (key == 'stir_time') {
|
||||||
stir_time = parseInt(value[key]);
|
displayStirTime = parseInt(value[key]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleEvents(event: { event_type: string; payload: any; index: number | undefined }) {
|
function handleEvents(event: { event_type: string; payload: any; index: number | undefined }) {
|
||||||
// console.log('triggered event', event.event_type, JSON.stringify(event.payload));
|
// logger.info('triggered event', event.event_type, JSON.stringify(event.payload));
|
||||||
if (event.event_type === 'mat_change') {
|
if (event.event_type === 'mat_change') {
|
||||||
// update value, do re-render
|
// update value, do re-render
|
||||||
initialize();
|
initialize();
|
||||||
} else if (event.event_type === 'edit_mat_field') {
|
} else if (event.event_type === 'edit_mat_field') {
|
||||||
console.log('request edit mat');
|
logger.info('request edit mat');
|
||||||
|
|
||||||
// fix bug: topping instant change by unlink topping
|
// fix bug: topping instant change by unlink topping
|
||||||
let _current_toppings = $state.snapshot(currentToppings);
|
let _current_toppings = $state.snapshot(currentToppings);
|
||||||
|
|
||||||
// pack all shown data
|
// pack all shown data
|
||||||
recipeDataEvent.set({
|
emitEditMatFieldPrep({
|
||||||
event_type: 'edit_mat_field_prep',
|
mat_id: currentMaterialInt,
|
||||||
payload: {
|
mat_type: currentMaterialType,
|
||||||
mat_id: currentMaterialInt,
|
mat_name: mat_id,
|
||||||
mat_type: currentMaterialType,
|
params: currentStringParams,
|
||||||
mat_name: mat_id,
|
toppings: _current_toppings,
|
||||||
params: currentStringParams,
|
current_topping_group: currentToppingInRow,
|
||||||
toppings: _current_toppings,
|
current_topping_list: selectableToppingInGroup,
|
||||||
current_topping_group: currentToppingInRow,
|
has_mix_ord: hasMixOrder,
|
||||||
current_topping_list: selectableToppingInGroup,
|
feed,
|
||||||
has_mix_ord: hasMixOrder,
|
water,
|
||||||
feed,
|
powder,
|
||||||
water,
|
syrup,
|
||||||
powder,
|
stir_time
|
||||||
syrup,
|
}, row_uid);
|
||||||
stir_time
|
|
||||||
},
|
|
||||||
index: row_uid
|
|
||||||
});
|
|
||||||
} else if (event.event_type === 'save_mat_field') {
|
} else if (event.event_type === 'save_mat_field') {
|
||||||
console.log('receive saving process mat, do refresh...');
|
logger.info('receive saving process mat, do refresh...');
|
||||||
let change_values = event.payload.change;
|
let change_values = event.payload.change;
|
||||||
|
|
||||||
// apply now
|
// apply now
|
||||||
let keys = Object.keys(change_values);
|
let keys = Object.keys(change_values);
|
||||||
console.log('change keys', JSON.stringify(keys));
|
logger.info('change keys', JSON.stringify(keys));
|
||||||
let _current_toppings = $state.snapshot(currentToppings);
|
let _current_toppings = $state.snapshot(currentToppings);
|
||||||
|
|
||||||
// initialize();
|
// initialize();
|
||||||
applyChanges(change_values);
|
applyChanges(change_values);
|
||||||
|
|
||||||
event.payload = {
|
let enhanced_payload = {
|
||||||
current_toppings: _current_toppings,
|
current_toppings: _current_toppings,
|
||||||
...event.payload
|
...event.payload
|
||||||
};
|
};
|
||||||
|
|
||||||
// console.log('check on change before trigger', change_values);
|
// Store for synchronous read by recipe-editor-dialog
|
||||||
|
setPendingSaveData({
|
||||||
|
change: change_values,
|
||||||
|
payload: enhanced_payload,
|
||||||
|
index: row_uid
|
||||||
|
});
|
||||||
|
|
||||||
if (onEditValue) onEditValue(JSON.stringify(event.payload));
|
if (onEditValue) onEditValue(JSON.stringify(enhanced_payload));
|
||||||
} else if (event.event_type === 'revert_change') {
|
} else if (event.event_type === 'revert_change') {
|
||||||
// console.log('revert back ...', row_uid);
|
// logger.info('revert back ...', row_uid);
|
||||||
|
|
||||||
currentToppings = oldToppings;
|
currentToppings = oldToppings;
|
||||||
|
clearPendingSaveData();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
initialize();
|
initialize();
|
||||||
unsubRecipeDataEvent = recipeDataEvent.subscribe((event) => {
|
toppingDataLoading = false;
|
||||||
if (event !== null && event.index !== undefined && event.index === row_uid) {
|
|
||||||
// has some event
|
|
||||||
handleEvents(event);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
|
// Subscribe to GlobalEventBus for recipe editor events
|
||||||
|
unsubRecipeDataEvent = (() => {
|
||||||
|
const unsubMatChange = GlobalEventBus.on(RECIPE_EVENTS.MAT_CHANGE, (data: any) => {
|
||||||
|
if (data && data.index === row_uid) {
|
||||||
|
handleEvents({
|
||||||
|
event_type: 'mat_change',
|
||||||
|
payload: data.mat_id,
|
||||||
|
index: data.index
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
const unsubEditMatField = GlobalEventBus.on(RECIPE_EVENTS.EDIT_MAT_FIELD, (data: any) => {
|
||||||
|
if (data && data.index === row_uid) {
|
||||||
|
handleEvents({
|
||||||
|
event_type: 'edit_mat_field',
|
||||||
|
payload: data.payload,
|
||||||
|
index: data.index
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
const unsubSaveMatField = GlobalEventBus.on(RECIPE_EVENTS.SAVE_MAT_FIELD, (data: any) => {
|
||||||
|
if (data && data.index === row_uid) {
|
||||||
|
handleEvents({
|
||||||
|
event_type: 'save_mat_field',
|
||||||
|
payload: data.payload,
|
||||||
|
index: data.index
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
const unsubRevertChange = GlobalEventBus.on(RECIPE_EVENTS.REVERT_CHANGE, (data: any) => {
|
||||||
|
if (data && (data.index === -1 || data.index === row_uid)) {
|
||||||
|
handleEvents({
|
||||||
|
event_type: 'revert_change',
|
||||||
|
payload: data.payload,
|
||||||
|
index: data.index
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return () => {
|
||||||
|
unsubMatChange();
|
||||||
|
unsubEditMatField();
|
||||||
|
unsubSaveMatField();
|
||||||
|
unsubRevertChange();
|
||||||
|
};
|
||||||
|
})();
|
||||||
|
|
||||||
|
// Reactively update topping display when external store changes.
|
||||||
|
// This replaces the old $effect that caused an infinite loop
|
||||||
|
// (the effect wrote $state variables that re-triggered the effect).
|
||||||
unsubRecipeTopping = latestRecipeToppingData.subscribe((payload) => {
|
unsubRecipeTopping = latestRecipeToppingData.subscribe((payload) => {
|
||||||
// console.log('topping data subscribe', payload);
|
if (payload && currentMaterialType === 'topping') {
|
||||||
|
currentToppings = payload;
|
||||||
|
getToppingDisplay();
|
||||||
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
onDestroy(() => {
|
onDestroy(() => {
|
||||||
if (unsubRecipeDataEvent) {
|
if (unsubRecipeDataEvent) {
|
||||||
// do last event before destroy
|
|
||||||
// console.log('trigger last event before destroy', get(latestRecipeToppingData));
|
|
||||||
handleEvents(get(recipeDataEvent) ?? { event_type: '', payload: {}, index: -1 });
|
|
||||||
|
|
||||||
unsubRecipeDataEvent();
|
unsubRecipeDataEvent();
|
||||||
// console.log('destroy recipe data event listener');
|
|
||||||
}
|
}
|
||||||
if (unsubRecipeTopping) {
|
if (unsubRecipeTopping) {
|
||||||
unsubRecipeTopping();
|
unsubRecipeTopping();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// NOTE: Topping display reactivity is now handled by the store subscription
|
||||||
|
// in onMount (latestRecipeToppingData.subscribe). The old $effect approach
|
||||||
|
// caused an infinite loop because getToppingDisplay() writes $state variables
|
||||||
|
// that re-triggered the effect. Using the store subscription breaks the cycle
|
||||||
|
// because store callbacks don't participate in Svelte 5 signal tracking.
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
|
|
@ -395,21 +484,27 @@
|
||||||
<!-- do topping layout -->
|
<!-- do topping layout -->
|
||||||
<div>
|
<div>
|
||||||
<!-- get name of topping -->
|
<!-- get name of topping -->
|
||||||
<div class="mx-auto my-4 flex flex-row gap-8">
|
{#if toppingDataLoading}
|
||||||
<p class="text-muted-foreground">
|
<div class="mx-auto my-4">
|
||||||
<b>
|
<p class="text-muted-foreground italic">Loading...</p>
|
||||||
{getCurrentSelectedToppingGroup()}
|
</div>
|
||||||
</b>, {getCurrentSelectedToppingList()}
|
{:else}
|
||||||
</p>
|
<div class="mx-auto my-4 flex flex-row gap-8">
|
||||||
{#if oldToppingNamesOnly != null}
|
<p class="text-muted-foreground">
|
||||||
<br />
|
<b>
|
||||||
<Button variant="outline" class="font-bold text-red-500">
|
{getCurrentSelectedToppingGroup()}
|
||||||
<!-- old topping -->
|
</b>, {getCurrentSelectedToppingList()}
|
||||||
<Undo />
|
</p>
|
||||||
<!-- {oldToppingNamesOnly} -->
|
{#if oldToppingNamesOnly != null}
|
||||||
</Button>
|
<br />
|
||||||
{/if}
|
<Button variant="outline" class="font-bold text-red-500">
|
||||||
</div>
|
<!-- old topping -->
|
||||||
|
<Undo />
|
||||||
|
<!-- {oldToppingNamesOnly} -->
|
||||||
|
</Button>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
{:else}
|
{:else}
|
||||||
<div>
|
<div>
|
||||||
|
|
@ -440,7 +535,7 @@
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
{#if water.yield > 0}
|
{#if displayWater.yield > 0}
|
||||||
<div class="my-4">
|
<div class="my-4">
|
||||||
<!-- <Label class="font-bold" for={`water_yield_volume_${row_uid}`}>Hot</Label>
|
<!-- <Label class="font-bold" for={`water_yield_volume_${row_uid}`}>Hot</Label>
|
||||||
<div class="flex flex-row items-center space-x-2 text-center">
|
<div class="flex flex-row items-center space-x-2 text-center">
|
||||||
|
|
@ -448,7 +543,7 @@
|
||||||
class="w-16"
|
class="w-16"
|
||||||
type="number"
|
type="number"
|
||||||
id={`water_yield_volume_${row_uid}`}
|
id={`water_yield_volume_${row_uid}`}
|
||||||
value={water.yield}
|
value={displayWater.yield}
|
||||||
onchangecapture={(v) =>
|
onchangecapture={(v) =>
|
||||||
triggerEditChange(`${v.currentTarget.id}=${v.currentTarget.value}`)}
|
triggerEditChange(`${v.currentTarget.id}=${v.currentTarget.value}`)}
|
||||||
/>
|
/>
|
||||||
|
|
@ -457,12 +552,12 @@
|
||||||
|
|
||||||
<p class="text-muted-foreground">
|
<p class="text-muted-foreground">
|
||||||
<b> Hot </b>
|
<b> Hot </b>
|
||||||
{water.yield} ml
|
{displayWater.yield} ml
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
{#if water.cold > 0}
|
{#if displayWater.cold > 0}
|
||||||
<div class="my-4">
|
<div class="my-4">
|
||||||
<!-- <Label class="font-bold" for={`water_cold_volume_${row_uid}`}>Cold</Label>
|
<!-- <Label class="font-bold" for={`water_cold_volume_${row_uid}`}>Cold</Label>
|
||||||
<div class="flex flex-row items-center space-x-2 text-center">
|
<div class="flex flex-row items-center space-x-2 text-center">
|
||||||
|
|
@ -470,7 +565,7 @@
|
||||||
class="w-16"
|
class="w-16"
|
||||||
type="number"
|
type="number"
|
||||||
id={`water_cold_volume_${row_uid}`}
|
id={`water_cold_volume_${row_uid}`}
|
||||||
value={water.cold}
|
value={displayWater.cold}
|
||||||
onchangecapture={(v) =>
|
onchangecapture={(v) =>
|
||||||
triggerEditChange(`${v.currentTarget.id}=${v.currentTarget.value}`)}
|
triggerEditChange(`${v.currentTarget.id}=${v.currentTarget.value}`)}
|
||||||
/>
|
/>
|
||||||
|
|
@ -478,12 +573,12 @@
|
||||||
</div> -->
|
</div> -->
|
||||||
<p class="text-muted-foreground">
|
<p class="text-muted-foreground">
|
||||||
<b> Hot </b>
|
<b> Hot </b>
|
||||||
{water.cold} ml
|
{displayWater.cold} ml
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
{#if currentMaterialType !== 'cup' && currentMaterialType !== 'topping' && stir_time > 0}
|
{#if currentMaterialType !== 'cup' && currentMaterialType !== 'topping' && displayStirTime > 0}
|
||||||
<div class="my-4">
|
<div class="my-4">
|
||||||
<!-- <Label class="font-bold" for={`stir_time_${row_uid}`}>{getStirTimeName()}</Label>
|
<!-- <Label class="font-bold" for={`stir_time_${row_uid}`}>{getStirTimeName()}</Label>
|
||||||
<div class="flex flex-row items-center space-x-2 text-center">
|
<div class="flex flex-row items-center space-x-2 text-center">
|
||||||
|
|
@ -491,7 +586,7 @@
|
||||||
class="w-16"
|
class="w-16"
|
||||||
type="number"
|
type="number"
|
||||||
id={`stir_time_${row_uid}`}
|
id={`stir_time_${row_uid}`}
|
||||||
value={stir_time / 10}
|
value={displayStirTime / 10}
|
||||||
onchangecapture={(v) =>
|
onchangecapture={(v) =>
|
||||||
triggerEditChange(`${v.currentTarget.id}=${v.currentTarget.value}`)}
|
triggerEditChange(`${v.currentTarget.id}=${v.currentTarget.value}`)}
|
||||||
/>
|
/>
|
||||||
|
|
@ -499,7 +594,7 @@
|
||||||
</div> -->
|
</div> -->
|
||||||
<p class="text-muted-foreground">
|
<p class="text-muted-foreground">
|
||||||
<b>{getStirTimeName()}</b>
|
<b>{getStirTimeName()}</b>
|
||||||
{stir_time / 10} sec(s)
|
{displayStirTime / 10} sec(s)
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
@ -527,10 +622,10 @@
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
<!-- feed pattern -->
|
<!-- feed pattern -->
|
||||||
{#if feed.parameter > 0 || feed.pattern > 0}
|
{#if displayFeed.parameter > 0 || displayFeed.pattern > 0}
|
||||||
<div class="mx-auto my-4 flex items-center gap-8 text-center">
|
<div class="mx-auto my-4 flex items-center gap-8 text-center">
|
||||||
<p class="text-muted-foreground">Style {feed.pattern}</p>
|
<p class="text-muted-foreground">Style {displayFeed.pattern}</p>
|
||||||
<p class="text-muted-foreground">Level {feed.parameter}</p>
|
<p class="text-muted-foreground">Level {displayFeed.parameter}</p>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,10 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
import { logger } from '$lib/core/utils/logger';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
currentEditingRecipeProductCode,
|
currentEditingRecipeProductCode,
|
||||||
latestRecipeToppingData,
|
latestRecipeToppingData,
|
||||||
|
recipeFromMachine,
|
||||||
recipeFromMachineQuery,
|
recipeFromMachineQuery,
|
||||||
recipeFromServerQuery,
|
recipeFromServerQuery,
|
||||||
referenceFromPage
|
referenceFromPage
|
||||||
|
|
@ -21,7 +24,8 @@
|
||||||
import { sendToAndroid } from '$lib/core/stores/adbWriter';
|
import { sendToAndroid } from '$lib/core/stores/adbWriter';
|
||||||
import { env } from '$env/dynamic/public';
|
import { env } from '$env/dynamic/public';
|
||||||
|
|
||||||
import { recipeDataEvent } from '$lib/core/stores/recipeStore';
|
import { GlobalEventBus } from '$lib/core/utils/eventBus';
|
||||||
|
import { RECIPE_EVENTS, getPendingSaveData } from './recipe-details/recipe-editor-events';
|
||||||
import { sendMessage } from '$lib/core/handlers/ws_messageSender';
|
import { sendMessage } from '$lib/core/handlers/ws_messageSender';
|
||||||
import { auth } from '$lib/core/stores/auth';
|
import { auth } from '$lib/core/stores/auth';
|
||||||
import { departmentStore } from '$lib/core/stores/departments';
|
import { departmentStore } from '$lib/core/stores/departments';
|
||||||
|
|
@ -57,7 +61,7 @@
|
||||||
let interval_get_machine_status: any;
|
let interval_get_machine_status: any;
|
||||||
|
|
||||||
async function onPendingChange(newChange: { target: string; value: any; ref_pd: string }) {
|
async function onPendingChange(newChange: { target: string; value: any; ref_pd: string }) {
|
||||||
console.log('detect pending change', matchMenuStatus(currentData.MenuStatus));
|
logger.info('detect pending change', matchMenuStatus(currentData.MenuStatus));
|
||||||
hasPendingChange = true;
|
hasPendingChange = true;
|
||||||
|
|
||||||
let originalMenuStatus = matchMenuStatus(currentData.MenuStatus);
|
let originalMenuStatus = matchMenuStatus(currentData.MenuStatus);
|
||||||
|
|
@ -74,22 +78,18 @@
|
||||||
currentData.MenuStatus = MenuStatus.pendingOnline;
|
currentData.MenuStatus = MenuStatus.pendingOnline;
|
||||||
|
|
||||||
if (newChange.target === 'recipeList') {
|
if (newChange.target === 'recipeList') {
|
||||||
// currentData.recipes = newChange.value;
|
logger.info('pending change recipe list', newChange);
|
||||||
//
|
|
||||||
// TODO: build into structure, flatten fields into 1 layer, strip off `id` (row id)
|
|
||||||
console.log('pending change recipe list', newChange);
|
|
||||||
|
|
||||||
let isMatchedProduct = newChange.ref_pd == productCode;
|
let isMatchedProduct = newChange.ref_pd == productCode;
|
||||||
|
|
||||||
// get latest edit
|
// get latest edit from pending save data
|
||||||
let latest_event = get(recipeDataEvent);
|
let latest_save = getPendingSaveData();
|
||||||
// expect edit_change_value_rpl
|
logger.info('latest pending save data', latest_save);
|
||||||
console.log('latest data event', latest_event);
|
|
||||||
|
|
||||||
let topping_value_for_revert = latest_event?.payload.current_toppings;
|
let topping_value_for_revert = latest_save?.payload.current_toppings;
|
||||||
let new_topping_value_for_save = latest_event?.payload.source.toppings;
|
let new_topping_value_for_save = latest_save?.payload.source.toppings;
|
||||||
|
|
||||||
// console.log(
|
// logger.info(
|
||||||
// 'topping_data_latest',
|
// 'topping_data_latest',
|
||||||
// get(latestRecipeToppingData),
|
// get(latestRecipeToppingData),
|
||||||
// 'topping_old',
|
// 'topping_old',
|
||||||
|
|
@ -98,7 +98,7 @@
|
||||||
// get(currentEditingRecipeProductCode)
|
// get(currentEditingRecipeProductCode)
|
||||||
// );
|
// );
|
||||||
|
|
||||||
console.log('accepting changes');
|
logger.info('accepting changes');
|
||||||
let current_data_to_brew =
|
let current_data_to_brew =
|
||||||
ready_to_send_brew.length == 0 ? $state.snapshot(currentData) : ready_to_send_brew.shift();
|
ready_to_send_brew.length == 0 ? $state.snapshot(currentData) : ready_to_send_brew.shift();
|
||||||
|
|
||||||
|
|
@ -121,27 +121,67 @@
|
||||||
// current_data_to_brew['ToppingSet'] = topping_value_for_revert;
|
// current_data_to_brew['ToppingSet'] = topping_value_for_revert;
|
||||||
// }
|
// }
|
||||||
|
|
||||||
|
// Reorder current_data_to_brew.recipes to match the drag-and-drop order.
|
||||||
|
// newChange.value[].id = original index in the recipes array.
|
||||||
|
// NOTE: We ONLY reorder the queue snapshot (current_data_to_brew), NOT the
|
||||||
|
// $state (currentData.recipes). Updating currentData.recipes would cause
|
||||||
|
// recipeListMatState to re-derive → table gets new data prop → TanStack
|
||||||
|
// fires onStateChange → checkChanges fires onPendingChange → infinite loop.
|
||||||
|
// The table already manages its own local data for correct visual display.
|
||||||
|
if (
|
||||||
|
newChange.value &&
|
||||||
|
Array.isArray(newChange.value) &&
|
||||||
|
current_data_to_brew.recipes &&
|
||||||
|
newChange.value.length === current_data_to_brew.recipes.length
|
||||||
|
) {
|
||||||
|
const oldRecipes = [...current_data_to_brew.recipes];
|
||||||
|
// Use currentData.recipes for the ID→recipe lookup — it is NEVER
|
||||||
|
// reordered (updating currentData.recipes would cause an infinite loop
|
||||||
|
// per the rule at lines 126-130, so it stays in original index order).
|
||||||
|
// oldRecipes reflects the queue snapshot which MAY have been reordered
|
||||||
|
// by a previous drag, making oldRecipes[item.id] incorrect because
|
||||||
|
// item.id is the ORIGINAL array index, not the current position.
|
||||||
|
const originalRecipes: any[] = currentData?.recipes ?? oldRecipes;
|
||||||
|
const reordered = newChange.value.map(
|
||||||
|
(item: any) => originalRecipes[item.id] ?? oldRecipes[item.id]
|
||||||
|
);
|
||||||
|
current_data_to_brew.recipes = reordered;
|
||||||
|
|
||||||
|
logger.info('reordered recipes by drag-and-drop', reordered, oldRecipes, {
|
||||||
|
originalRecipesLength: originalRecipes.length,
|
||||||
|
usingOriginalRecipes: originalRecipes !== oldRecipes
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
logger.info(
|
||||||
|
'reorder skipped — length mismatch or missing recipes',
|
||||||
|
newChange.value?.length,
|
||||||
|
current_data_to_brew.recipes?.length
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure ToppingSet has no null entries before applying further changes.
|
||||||
|
sanitizeToppingSet(current_data_to_brew);
|
||||||
|
|
||||||
// apply changes now
|
// apply changes now
|
||||||
let new_change = applyChangeToRecipeForBrewing(latest_event, current_data_to_brew);
|
let new_change = applyChangeToRecipeForBrewing(latest_save, current_data_to_brew);
|
||||||
|
|
||||||
ready_to_send_brew.push(new_change);
|
ready_to_send_brew.push(new_change);
|
||||||
|
|
||||||
callback_revert_value_if_not_save = async (save: any) => {
|
callback_revert_value_if_not_save = async (save: any) => {
|
||||||
if (!save) {
|
if (!save) {
|
||||||
latestRecipeToppingData.set(topping_value_for_revert);
|
latestRecipeToppingData.set(topping_value_for_revert);
|
||||||
console.log('revert change', get(latestRecipeToppingData));
|
logger.info('revert change', get(latestRecipeToppingData));
|
||||||
recipeDataEvent.set({
|
GlobalEventBus.emit(RECIPE_EVENTS.REVERT_CHANGE, {
|
||||||
event_type: 'revert_change',
|
|
||||||
payload: {},
|
payload: {},
|
||||||
index: -1
|
index: -1
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
// topping part
|
// topping part
|
||||||
// latestRecipeToppingData.set(new_topping_value_for_save);
|
// latestRecipeToppingData.set(new_topping_value_for_save);
|
||||||
// console.log('save change', get(latestRecipeToppingData));
|
// logger.info('save change', get(latestRecipeToppingData));
|
||||||
// currentData['ToppingSet'] = latestRecipeToppingData;
|
// currentData['ToppingSet'] = latestRecipeToppingData;
|
||||||
|
|
||||||
// console.log('current data', currentData);
|
// logger.info('current data', currentData);
|
||||||
let curr_user = get(auth);
|
let curr_user = get(auth);
|
||||||
|
|
||||||
let user_info: any;
|
let user_info: any;
|
||||||
|
|
@ -168,6 +208,21 @@
|
||||||
recipeFromMachineQuery.set(recipeDevSnapshot);
|
recipeFromMachineQuery.set(recipeDevSnapshot);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Also update recipeFromMachine (source-of-truth for brew page)
|
||||||
|
// so that if buildOverviewForBrewing() is called later (e.g. via
|
||||||
|
// handleMenuSaved), it rebuilds from the latest data.
|
||||||
|
let machineRecipeSnapshot = get(recipeFromMachine);
|
||||||
|
if (machineRecipeSnapshot?.Recipe01) {
|
||||||
|
const targetCode = ready_to_send_brew[0]['productCode'];
|
||||||
|
const idx = machineRecipeSnapshot.Recipe01.findIndex(
|
||||||
|
(r: any) => r['productCode'] === targetCode
|
||||||
|
);
|
||||||
|
if (idx >= 0) {
|
||||||
|
machineRecipeSnapshot.Recipe01[idx] = ready_to_send_brew[0];
|
||||||
|
}
|
||||||
|
recipeFromMachine.set(machineRecipeSnapshot);
|
||||||
|
}
|
||||||
|
|
||||||
sendToAndroid({
|
sendToAndroid({
|
||||||
type: 'save_recipe_machine',
|
type: 'save_recipe_machine',
|
||||||
payload: {
|
payload: {
|
||||||
|
|
@ -199,7 +254,7 @@
|
||||||
payload: {
|
payload: {
|
||||||
user_info,
|
user_info,
|
||||||
country: get(departmentStore) ?? 'unknown',
|
country: get(departmentStore) ?? 'unknown',
|
||||||
values: currentData
|
values: ready_to_send_brew[0]
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
@ -240,32 +295,66 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Create a default empty topping-set entry (no topping configured). */
|
||||||
|
function getDefaultToppingSet() {
|
||||||
|
return { ListGroupID: [0, 0, 0, 0], defaultIDSelect: 0, groupID: '0', isUse: false };
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Guard: fill any null/undefined ToppingSet slots with defaults. */
|
||||||
|
function sanitizeToppingSet(data: any) {
|
||||||
|
if (data && data['ToppingSet']) {
|
||||||
|
for (let i = 0; i < data['ToppingSet'].length; i++) {
|
||||||
|
if (data['ToppingSet'][i] == null || data['ToppingSet'][i] == undefined) {
|
||||||
|
data['ToppingSet'][i] = getDefaultToppingSet();
|
||||||
|
logger.info('filled null ToppingSet slot', i);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function applyChangeToRecipeForBrewing(latest_event: any, current_data_to_brew: any) {
|
function applyChangeToRecipeForBrewing(latest_event: any, current_data_to_brew: any) {
|
||||||
console.log('applyChangeToRecipeForBrewing', Object.keys(latest_event?.payload));
|
logger.info('checkBeforeApplyChange', latest_event);
|
||||||
|
|
||||||
|
if (latest_event == null || latest_event == undefined) {
|
||||||
|
sanitizeToppingSet(current_data_to_brew);
|
||||||
|
return current_data_to_brew;
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.info('applyChangeToRecipeForBrewing', Object.keys(latest_event?.payload));
|
||||||
|
|
||||||
let changes = latest_event?.payload.change;
|
let changes = latest_event?.payload.change;
|
||||||
|
|
||||||
let apply_to_index = latest_event?.index;
|
let apply_to_index = latest_event?.index;
|
||||||
let apply_to_keys = Object.keys(changes);
|
let apply_to_keys = Object.keys(changes);
|
||||||
|
|
||||||
console.log('applying', apply_to_index, apply_to_keys, current_data_to_brew['recipes'].length);
|
logger.info('applying', apply_to_index, apply_to_keys, current_data_to_brew['recipes'].length);
|
||||||
|
|
||||||
console.log('topping before apply', current_data_to_brew['ToppingSet']);
|
logger.info('topping before apply', current_data_to_brew['ToppingSet']);
|
||||||
for (const key of apply_to_keys) {
|
for (const key of apply_to_keys) {
|
||||||
if (key == 'toppings') {
|
if (key == 'toppings') {
|
||||||
current_data_to_brew['ToppingSet'][apply_to_index - 1] = changes[key][apply_to_index - 1];
|
// Find the actual topping-slot index from the sparse changes array.
|
||||||
console.log('applying topping', apply_to_index - 1);
|
// The editor builds changes['toppings'] = new Array(N) with only the
|
||||||
|
// slot being edited filled; all other entries are undefined. We must
|
||||||
|
// NOT use apply_to_index (recipe row index) because ToppingSet is
|
||||||
|
// indexed by material-path slot (0..19), NOT by recipe position.
|
||||||
|
const toppingSlot = changes[key].findIndex((v: any) => v != null && v != undefined);
|
||||||
|
if (toppingSlot >= 0) {
|
||||||
|
current_data_to_brew['ToppingSet'][toppingSlot] = changes[key][toppingSlot];
|
||||||
|
logger.info('applying topping at slot', toppingSlot);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
// get actual key
|
// get actual key
|
||||||
|
|
||||||
current_data_to_brew['recipes'][apply_to_index][getRecipeListKeyName(key)] = parseInt(
|
current_data_to_brew['recipes'][apply_to_index][getRecipeListKeyName(key)] = parseInt(
|
||||||
changes[key]
|
changes[key]
|
||||||
);
|
);
|
||||||
console.log('applying ', key, current_data_to_brew['recipes'][apply_to_index]);
|
logger.info('applying ', key, current_data_to_brew['recipes'][apply_to_index]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('topping after apply', current_data_to_brew['ToppingSet']);
|
sanitizeToppingSet(current_data_to_brew);
|
||||||
|
|
||||||
|
logger.info('topping after apply', current_data_to_brew['ToppingSet']);
|
||||||
|
|
||||||
return current_data_to_brew;
|
return current_data_to_brew;
|
||||||
}
|
}
|
||||||
|
|
@ -276,7 +365,7 @@
|
||||||
ready_to_send_brew.shift();
|
ready_to_send_brew.shift();
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('sending brewing payload', ready_to_send_brew);
|
logger.info('sending brewing payload', ready_to_send_brew);
|
||||||
|
|
||||||
// save topping
|
// save topping
|
||||||
|
|
||||||
|
|
@ -340,7 +429,7 @@
|
||||||
let recipe01Snap = recipeServerSnapshot['recipe'];
|
let recipe01Snap = recipeServerSnapshot['recipe'];
|
||||||
if (recipe01Snap) {
|
if (recipe01Snap) {
|
||||||
currentData = recipe01Snap[productCode] ?? {};
|
currentData = recipe01Snap[productCode] ?? {};
|
||||||
// console.log(`current data : ${JSON.stringify(Object.keys(recipe01Snap))}`);
|
// logger.info(`current data : ${JSON.stringify(Object.keys(recipe01Snap))}`);
|
||||||
if (currentData.MenuStatus) {
|
if (currentData.MenuStatus) {
|
||||||
currentMenuStatus = matchMenuStatus(currentData.MenuStatus);
|
currentMenuStatus = matchMenuStatus(currentData.MenuStatus);
|
||||||
}
|
}
|
||||||
|
|
@ -352,6 +441,12 @@
|
||||||
// interval check 1s
|
// interval check 1s
|
||||||
// machine
|
// machine
|
||||||
if (refPage === 'brew') {
|
if (refPage === 'brew') {
|
||||||
|
// Clear any previous interval before creating a new one
|
||||||
|
// ($effect can re-run when its dependencies change).
|
||||||
|
if (interval_get_machine_status) {
|
||||||
|
clearInterval(interval_get_machine_status);
|
||||||
|
}
|
||||||
|
|
||||||
interval_get_machine_status = setInterval(() => {
|
interval_get_machine_status = setInterval(() => {
|
||||||
if (
|
if (
|
||||||
getMachineStatus() == undefined ||
|
getMachineStatus() == undefined ||
|
||||||
|
|
@ -362,7 +457,7 @@
|
||||||
updateMachineStatus('');
|
updateMachineStatus('');
|
||||||
}
|
}
|
||||||
|
|
||||||
// console.log(
|
// logger.info(
|
||||||
// 'machine status pinging recipe editor dialog',
|
// 'machine status pinging recipe editor dialog',
|
||||||
// getMachineStatus(),
|
// getMachineStatus(),
|
||||||
// $machineInfoStore?.status
|
// $machineInfoStore?.status
|
||||||
|
|
@ -377,11 +472,17 @@
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}, 1000);
|
}, 1000);
|
||||||
|
} else if (interval_get_machine_status) {
|
||||||
|
// No longer on 'brew' page — clean up the interval
|
||||||
|
clearInterval(interval_get_machine_status);
|
||||||
|
interval_get_machine_status = null;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
onDestroy(() => {
|
onDestroy(() => {
|
||||||
clearInterval(interval_get_machine_status);
|
if (interval_get_machine_status) {
|
||||||
|
clearInterval(interval_get_machine_status);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
@ -419,7 +520,7 @@
|
||||||
save_change = true;
|
save_change = true;
|
||||||
|
|
||||||
callback_revert_value_if_not_save(save_change);
|
callback_revert_value_if_not_save(save_change);
|
||||||
await saveRecipeMenuFileToAndroid();
|
// await saveRecipeMenuFileToAndroid();
|
||||||
|
|
||||||
addNotification('INFO:Save recipe');
|
addNotification('INFO:Save recipe');
|
||||||
}}
|
}}
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,7 @@
|
||||||
closeTerminalDrawer
|
closeTerminalDrawer
|
||||||
} from '$lib/core/stores/terminalDrawer';
|
} from '$lib/core/stores/terminalDrawer';
|
||||||
import { getAdbInstance } from '$lib/core/adb/adb';
|
import { getAdbInstance } from '$lib/core/adb/adb';
|
||||||
|
import { adbConnectionStatus } from '$lib/core/stores/adbConnectionStore';
|
||||||
import {
|
import {
|
||||||
initTerminalSession,
|
initTerminalSession,
|
||||||
reinitTerminalSession,
|
reinitTerminalSession,
|
||||||
|
|
@ -270,17 +271,17 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check connection status periodically
|
// Subscribe to the shared connection store instead of polling every 2s.
|
||||||
let _connInterval: ReturnType<typeof setInterval> | undefined;
|
// The store is updated by adb.ts whenever a device connects or disconnects.
|
||||||
|
let _unsubConnStatus: (() => void) | undefined;
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
_connInterval = setInterval(() => {
|
_unsubConnStatus = adbConnectionStatus.subscribe((status) => {
|
||||||
const instance = getAdbInstance();
|
connStatus = status === 'connected' ? 'connected' : 'disconnected';
|
||||||
connStatus = instance ? 'connected' : 'disconnected';
|
});
|
||||||
}, 2000);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
onDestroy(() => {
|
onDestroy(() => {
|
||||||
if (_connInterval) clearInterval(_connInterval);
|
_unsubConnStatus?.();
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
<script lang="ts">
|
<script lang="ts">import { logger } from '$lib/core/utils/logger';
|
||||||
|
|
||||||
import * as adb from '$lib/core/adb/adb';
|
import * as adb from '$lib/core/adb/adb';
|
||||||
|
|
||||||
import { Xterm, XtermAddon } from '@battlefieldduck/xterm-svelte';
|
import { Xterm, XtermAddon } from '@battlefieldduck/xterm-svelte';
|
||||||
|
|
@ -17,7 +18,7 @@
|
||||||
};
|
};
|
||||||
|
|
||||||
async function onLoad() {
|
async function onLoad() {
|
||||||
console.log('[Term] loaded');
|
logger.info('[Term] loaded');
|
||||||
|
|
||||||
const xmod = await XtermAddon.FitAddon();
|
const xmod = await XtermAddon.FitAddon();
|
||||||
|
|
||||||
|
|
@ -45,7 +46,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
async function onKey(data: { key: string; domEvent: KeyboardEvent }) {
|
async function onKey(data: { key: string; domEvent: KeyboardEvent }) {
|
||||||
console.log('on key', data, data.domEvent.key == 'Backspace');
|
logger.info('on key', data, data.domEvent.key == 'Backspace');
|
||||||
|
|
||||||
// TODO: implement arror keys
|
// TODO: implement arror keys
|
||||||
let isArrowKeys = data.domEvent.key.startsWith('Arrow');
|
let isArrowKeys = data.domEvent.key.startsWith('Arrow');
|
||||||
|
|
@ -67,13 +68,13 @@
|
||||||
// let txt = document.getElementById("cmd-input") as HTMLInputElement;
|
// let txt = document.getElementById("cmd-input") as HTMLInputElement;
|
||||||
|
|
||||||
let input_cmd = get(chars);
|
let input_cmd = get(chars);
|
||||||
console.log('instance existed, ', input_cmd);
|
logger.info('instance existed, ', input_cmd);
|
||||||
|
|
||||||
terminal?.writeln('');
|
terminal?.writeln('');
|
||||||
|
|
||||||
if ((await handleNonAdbCmd(input_cmd ?? '')) < 0) {
|
if ((await handleNonAdbCmd(input_cmd ?? '')) < 0) {
|
||||||
let result = await adb.executeCmd(input_cmd ?? '');
|
let result = await adb.executeCmd(input_cmd ?? '');
|
||||||
console.log(result);
|
logger.info(result);
|
||||||
if (result.output) {
|
if (result.output) {
|
||||||
terminal?.writeln(result.output);
|
terminal?.writeln(result.output);
|
||||||
} else if (result.error) {
|
} else if (result.error) {
|
||||||
|
|
@ -100,7 +101,7 @@
|
||||||
current_data != null &&
|
current_data != null &&
|
||||||
current_data.length > 0
|
current_data.length > 0
|
||||||
) {
|
) {
|
||||||
console.log('back ', current_data);
|
logger.info('back ', current_data);
|
||||||
current_data = current_data!.slice(0, -1);
|
current_data = current_data!.slice(0, -1);
|
||||||
chars.set(current_data);
|
chars.set(current_data);
|
||||||
terminal?.write('\b \b');
|
terminal?.write('\b \b');
|
||||||
|
|
@ -113,24 +114,19 @@
|
||||||
// -1: unknown command or failed
|
// -1: unknown command or failed
|
||||||
// 0: ok
|
// 0: ok
|
||||||
async function handleNonAdbCmd(cmd: string) {
|
async function handleNonAdbCmd(cmd: string) {
|
||||||
|
// SECURITY: eval() removed — arbitrary code execution risk.
|
||||||
|
// Use explicit commands only (clear, help, etc.).
|
||||||
if (cmd.trim().startsWith('eval')) {
|
if (cmd.trim().startsWith('eval')) {
|
||||||
let eval_statement = cmd.replace('eval', '').trim();
|
terminal?.writeln('eval is disabled for security. Use explicit commands only.');
|
||||||
console.log('get program: ', eval_statement);
|
|
||||||
try {
|
|
||||||
let result = eval(eval_statement);
|
|
||||||
console.log('result: ', result);
|
|
||||||
terminal?.writeln(`${result}`);
|
|
||||||
} catch (e) {
|
|
||||||
terminal?.writeln(`${e}`);
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
return 0;
|
return 0;
|
||||||
} else {
|
} else {
|
||||||
switch (cmd) {
|
switch (cmd.trim()) {
|
||||||
case 'clear':
|
case 'clear':
|
||||||
terminal?.clear();
|
terminal?.clear();
|
||||||
return 0;
|
return 0;
|
||||||
|
case 'help':
|
||||||
|
terminal?.writeln('Available commands: clear, help');
|
||||||
|
return 0;
|
||||||
default:
|
default:
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
import { logger } from '$lib/core/utils/logger';
|
||||||
import { Adb, AdbDaemonTransport, encodeUtf8 } from '@yume-chan/adb';
|
import { Adb, AdbDaemonTransport, encodeUtf8 } from '@yume-chan/adb';
|
||||||
import AdbWebCredentialStore from '@yume-chan/adb-credential-web';
|
import AdbWebCredentialStore from '@yume-chan/adb-credential-web';
|
||||||
import {
|
import {
|
||||||
|
|
@ -16,8 +17,60 @@ import { adbWriter } from '../stores/adbWriter';
|
||||||
import { WritableStream } from '@yume-chan/stream-extra';
|
import { WritableStream } from '@yume-chan/stream-extra';
|
||||||
import { env } from '$env/dynamic/public';
|
import { env } from '$env/dynamic/public';
|
||||||
import { get } from 'svelte/store';
|
import { get } from 'svelte/store';
|
||||||
|
import { adbConnectionStatus } from '../stores/adbConnectionStore';
|
||||||
|
|
||||||
|
let _connectionPromise: Promise<boolean> | null = null;
|
||||||
let syncConnection: any = null;
|
let syncConnection: any = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Centralized connection function that all pages and components should use.
|
||||||
|
*
|
||||||
|
* - If a device is already connected, returns `true` immediately.
|
||||||
|
* - If a connection is already in progress, waits for it and returns its result.
|
||||||
|
* - Otherwise attempts to connect using stored credentials first
|
||||||
|
* (AdbDaemonWebUsbDeviceManager.BROWSER.getDevices), falling back to
|
||||||
|
* prompting the user with the browser's WebUSB device picker.
|
||||||
|
*
|
||||||
|
* This deduplicates connection attempts across the app: no matter how many
|
||||||
|
* pages/components call it simultaneously, only one WebUSB handshake happens.
|
||||||
|
*
|
||||||
|
* @returns `true` when a device is (or became) connected, `false` otherwise.
|
||||||
|
*/
|
||||||
|
export async function ensureAdbConnection(): Promise<boolean> {
|
||||||
|
// Already connected.
|
||||||
|
if (getAdbInstance()) return true;
|
||||||
|
|
||||||
|
// Already in progress — wait for it.
|
||||||
|
if (_connectionPromise) return await _connectionPromise;
|
||||||
|
|
||||||
|
adbConnectionStatus.setConnecting();
|
||||||
|
|
||||||
|
_connectionPromise = (async () => {
|
||||||
|
try {
|
||||||
|
const deviceManager = AdbDaemonWebUsbDeviceManager.BROWSER;
|
||||||
|
if (deviceManager && 'usb' in navigator) {
|
||||||
|
const devices = await deviceManager.getDevices();
|
||||||
|
if (devices && devices.length === 1) {
|
||||||
|
const credStore = new AdbWebCredentialStore();
|
||||||
|
await connectDeviceByCred(devices[0], credStore);
|
||||||
|
return Boolean(getAdbInstance());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback: prompt user to pick a device.
|
||||||
|
await connnectViaWebUSB();
|
||||||
|
return Boolean(getAdbInstance());
|
||||||
|
} catch (e) {
|
||||||
|
logger.error('ensureAdbConnection failed', e);
|
||||||
|
adbConnectionStatus.setDisconnected();
|
||||||
|
return false;
|
||||||
|
} finally {
|
||||||
|
_connectionPromise = null;
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
|
||||||
|
return await _connectionPromise;
|
||||||
|
}
|
||||||
let syncOperation: Promise<unknown> = Promise.resolve();
|
let syncOperation: Promise<unknown> = Promise.resolve();
|
||||||
let recipeMenuAdbConnectPromise: Promise<Adb | undefined> | null = null;
|
let recipeMenuAdbConnectPromise: Promise<Adb | undefined> | null = null;
|
||||||
let recipeMenuAndroidServerConnectPromise: Promise<void> | null = null;
|
let recipeMenuAndroidServerConnectPromise: Promise<void> | null = null;
|
||||||
|
|
@ -137,10 +190,11 @@ async function connectWithRetry<T>(
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function connnectViaWebUSB(connectAndroidServer = true) {
|
export async function connnectViaWebUSB(connectAndroidServer = true) {
|
||||||
|
adbConnectionStatus.setConnecting();
|
||||||
const device = await AdbDaemonWebUsbDeviceManager.BROWSER?.requestDevice();
|
const device = await AdbDaemonWebUsbDeviceManager.BROWSER?.requestDevice();
|
||||||
console.log('usb ok', (globalThis.navigator as Navigator & { usb?: unknown }).usb);
|
logger.info('usb ok', (globalThis.navigator as Navigator & { usb?: unknown }).usb);
|
||||||
if (device) {
|
if (device) {
|
||||||
console.log('connect ', device.name);
|
logger.info('connect ', device.name);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const credentialStore = new AdbWebCredentialStore();
|
const credentialStore = new AdbWebCredentialStore();
|
||||||
|
|
@ -161,7 +215,7 @@ export async function connnectViaWebUSB(connectAndroidServer = true) {
|
||||||
// save device info
|
// save device info
|
||||||
await deviceCredentialManager.saveDeviceInfo(device);
|
await deviceCredentialManager.saveDeviceInfo(device);
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
console.error('error on connect', e);
|
logger.error('error on connect', e);
|
||||||
|
|
||||||
if (e instanceof AdbDaemonWebUsbDevice.DeviceBusyError) {
|
if (e instanceof AdbDaemonWebUsbDevice.DeviceBusyError) {
|
||||||
addNotification(
|
addNotification(
|
||||||
|
|
@ -179,6 +233,7 @@ export async function connectDeviceByCred(
|
||||||
credStore: AdbWebCredentialStore,
|
credStore: AdbWebCredentialStore,
|
||||||
connectAndroidServer = true
|
connectAndroidServer = true
|
||||||
) {
|
) {
|
||||||
|
adbConnectionStatus.setConnecting();
|
||||||
try {
|
try {
|
||||||
const connection = await device.connect();
|
const connection = await device.connect();
|
||||||
const transport = await AdbDaemonTransport.authenticate({
|
const transport = await AdbDaemonTransport.authenticate({
|
||||||
|
|
@ -196,6 +251,7 @@ export async function connectDeviceByCred(
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
adbConnectionStatus.setDisconnected();
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -203,6 +259,11 @@ export async function connectDeviceByCred(
|
||||||
export async function saveAdbInstance(adb: Adb | undefined) {
|
export async function saveAdbInstance(adb: Adb | undefined) {
|
||||||
await cleanupSync();
|
await cleanupSync();
|
||||||
AdbInstance.instance = adb;
|
AdbInstance.instance = adb;
|
||||||
|
if (adb) {
|
||||||
|
adbConnectionStatus.setConnected();
|
||||||
|
} else {
|
||||||
|
adbConnectionStatus.setDisconnected();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getAdbInstance() {
|
export function getAdbInstance() {
|
||||||
|
|
@ -210,6 +271,7 @@ export function getAdbInstance() {
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function connectRecipeMenuViaWebUSB() {
|
export async function connectRecipeMenuViaWebUSB() {
|
||||||
|
adbConnectionStatus.setConnecting();
|
||||||
const currentInstance = getAdbInstance();
|
const currentInstance = getAdbInstance();
|
||||||
if (currentInstance) {
|
if (currentInstance) {
|
||||||
await connectToAndroidRecipeMenuServer();
|
await connectToAndroidRecipeMenuServer();
|
||||||
|
|
@ -218,9 +280,9 @@ export async function connectRecipeMenuViaWebUSB() {
|
||||||
if (recipeMenuAdbConnectPromise) return await recipeMenuAdbConnectPromise;
|
if (recipeMenuAdbConnectPromise) return await recipeMenuAdbConnectPromise;
|
||||||
|
|
||||||
const device = await AdbDaemonWebUsbDeviceManager.BROWSER?.requestDevice();
|
const device = await AdbDaemonWebUsbDeviceManager.BROWSER?.requestDevice();
|
||||||
console.log('recipe menu usb ok', 'usb' in globalThis.navigator);
|
logger.info('recipe menu usb ok', 'usb' in globalThis.navigator);
|
||||||
if (device) {
|
if (device) {
|
||||||
console.log('recipe menu connect ', device.name);
|
logger.info('recipe menu connect ', device.name);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const credentialStore = new AdbWebCredentialStore();
|
const credentialStore = new AdbWebCredentialStore();
|
||||||
|
|
@ -230,7 +292,7 @@ export async function connectRecipeMenuViaWebUSB() {
|
||||||
await deviceCredentialManager.saveDeviceInfo(device);
|
await deviceCredentialManager.saveDeviceInfo(device);
|
||||||
return adb;
|
return adb;
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
console.error('recipe menu connect error', e);
|
logger.error('recipe menu connect error', e);
|
||||||
|
|
||||||
if (e instanceof AdbDaemonWebUsbDevice.DeviceBusyError) {
|
if (e instanceof AdbDaemonWebUsbDevice.DeviceBusyError) {
|
||||||
addNotification(
|
addNotification(
|
||||||
|
|
@ -249,6 +311,7 @@ export async function connectRecipeMenuDeviceByCred(
|
||||||
device: AdbDaemonWebUsbDevice,
|
device: AdbDaemonWebUsbDevice,
|
||||||
credStore: AdbWebCredentialStore
|
credStore: AdbWebCredentialStore
|
||||||
) {
|
) {
|
||||||
|
adbConnectionStatus.setConnecting();
|
||||||
const currentInstance = getAdbInstance();
|
const currentInstance = getAdbInstance();
|
||||||
if (currentInstance) {
|
if (currentInstance) {
|
||||||
await connectToAndroidRecipeMenuServer();
|
await connectToAndroidRecipeMenuServer();
|
||||||
|
|
@ -306,20 +369,35 @@ export async function sendRecipeMenuMessageToAndroid(message: any) {
|
||||||
try {
|
try {
|
||||||
const encoder = new TextEncoder();
|
const encoder = new TextEncoder();
|
||||||
await writer.write(encoder.encode(JSON.stringify(message) + '\n'));
|
await writer.write(encoder.encode(JSON.stringify(message) + '\n'));
|
||||||
console.log('recipe menu sent! ', JSON.stringify(message).length);
|
logger.info('recipe menu sent! ', JSON.stringify(message).length);
|
||||||
return true;
|
return true;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('recipe menu write failed', error);
|
logger.error('recipe menu write failed', error);
|
||||||
addNotification(`ERR:Failed to send recipe menu\n${error}`);
|
addNotification(`ERR:Failed to send recipe menu\n${error}`);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reject shell metacharacters that enable command injection.
|
||||||
|
* Allows alphanumerics, spaces, common paths (/ - _ .), and ADB shell primitives.
|
||||||
|
*/
|
||||||
|
const UNSAFE_SHELL_PATTERN = /[;&|`$(){}[\]<>!#~*?\\\n\r]/;
|
||||||
|
|
||||||
|
function sanitizeAdbCommand(command: string): string {
|
||||||
|
if (UNSAFE_SHELL_PATTERN.test(command)) {
|
||||||
|
logger.warn(`Command contains unsafe characters, rejecting: ${command.slice(0, 80)}`);
|
||||||
|
throw new Error(`Command rejected: contains shell metacharacters`);
|
||||||
|
}
|
||||||
|
return command;
|
||||||
|
}
|
||||||
|
|
||||||
export async function executeCmd(command: string) {
|
export async function executeCmd(command: string) {
|
||||||
|
command = sanitizeAdbCommand(command);
|
||||||
let instance = getAdbInstance();
|
let instance = getAdbInstance();
|
||||||
|
|
||||||
if (!instance) {
|
if (!instance) {
|
||||||
console.error('instance not found');
|
logger.error('instance not found');
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -348,7 +426,7 @@ export async function executeCmd(command: string) {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
// console.log(e.message);
|
// logger.info(e.message);
|
||||||
//ExactReadable ended
|
//ExactReadable ended
|
||||||
if (e.message.includes('ExactReadable ended')) {
|
if (e.message.includes('ExactReadable ended')) {
|
||||||
return {
|
return {
|
||||||
|
|
@ -358,7 +436,7 @@ export async function executeCmd(command: string) {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
console.error('error while execute command', e);
|
logger.error('error while execute command', e);
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -386,6 +464,7 @@ export async function executeStreamingCmd(
|
||||||
onExit?: (exitCode: number | undefined) => void;
|
onExit?: (exitCode: number | undefined) => void;
|
||||||
}
|
}
|
||||||
): Promise<() => void> {
|
): Promise<() => void> {
|
||||||
|
command = sanitizeAdbCommand(command);
|
||||||
const instance = getAdbInstance();
|
const instance = getAdbInstance();
|
||||||
let aborted = false;
|
let aborted = false;
|
||||||
|
|
||||||
|
|
@ -454,7 +533,7 @@ export async function disconnect() {
|
||||||
if (instance) {
|
if (instance) {
|
||||||
try {
|
try {
|
||||||
await instance.close();
|
await instance.close();
|
||||||
console.log('close instance');
|
logger.info('close instance');
|
||||||
} finally {
|
} finally {
|
||||||
await saveAdbInstance(undefined);
|
await saveAdbInstance(undefined);
|
||||||
}
|
}
|
||||||
|
|
@ -466,7 +545,7 @@ export async function cleanupSync() {
|
||||||
try {
|
try {
|
||||||
await syncConnection.dispose();
|
await syncConnection.dispose();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('error on dispose sync', e);
|
logger.error('error on dispose sync', e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -498,7 +577,7 @@ export async function pull(filename: string, timeoutMs: number = 5000) {
|
||||||
return result_string;
|
return result_string;
|
||||||
}
|
}
|
||||||
} catch (pull_error: any) {
|
} catch (pull_error: any) {
|
||||||
console.log('pulling error', pull_error);
|
logger.info('pulling error', pull_error);
|
||||||
} finally {
|
} finally {
|
||||||
await cleanupSync();
|
await cleanupSync();
|
||||||
}
|
}
|
||||||
|
|
@ -520,14 +599,14 @@ export async function push(path: string, obj: string) {
|
||||||
});
|
});
|
||||||
|
|
||||||
try {
|
try {
|
||||||
console.log('support push v2', sync.supportsSendReceiveV2);
|
logger.info('support push v2', sync.supportsSendReceiveV2);
|
||||||
|
|
||||||
await sync.write({
|
await sync.write({
|
||||||
filename: path,
|
filename: path,
|
||||||
file
|
file
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log('error while trying to write to machine', error);
|
logger.info('error while trying to write to machine', error);
|
||||||
} finally {
|
} finally {
|
||||||
await sync.dispose();
|
await sync.dispose();
|
||||||
}
|
}
|
||||||
|
|
@ -572,7 +651,7 @@ export async function pushBinary(
|
||||||
onProgress?.(total, total);
|
onProgress?.(total, total);
|
||||||
return true;
|
return true;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log('error while pushing binary to machine', error);
|
logger.info('error while pushing binary to machine', error);
|
||||||
return false;
|
return false;
|
||||||
} finally {
|
} finally {
|
||||||
await sync.dispose();
|
await sync.dispose();
|
||||||
|
|
@ -591,7 +670,7 @@ async function connectToAndroidServer(maxRetries = 5) {
|
||||||
try {
|
try {
|
||||||
let inst = getAdbInstance();
|
let inst = getAdbInstance();
|
||||||
if (!inst) {
|
if (!inst) {
|
||||||
console.warn('adb instance not found');
|
logger.warn('adb instance not found');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -608,7 +687,7 @@ async function connectToAndroidServer(maxRetries = 5) {
|
||||||
const writer = stream.writable.getWriter();
|
const writer = stream.writable.getWriter();
|
||||||
const reader = stream.readable.getReader();
|
const reader = stream.readable.getReader();
|
||||||
|
|
||||||
console.log('checking on writer ', writer);
|
// logger.info('checking on writer ', writer);
|
||||||
adbWriter.set(writer);
|
adbWriter.set(writer);
|
||||||
if (writer) {
|
if (writer) {
|
||||||
addNotification('INFO:Enable Brewing Mode T on machine');
|
addNotification('INFO:Enable Brewing Mode T on machine');
|
||||||
|
|
@ -641,18 +720,19 @@ async function connectToAndroidServer(maxRetries = 5) {
|
||||||
// handleAdbPayload(new TextDecoder().decode(value));
|
// handleAdbPayload(new TextDecoder().decode(value));
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('read error', e);
|
logger.error('read error', e);
|
||||||
if (isRecoverableError(e)) {
|
if (isRecoverableError(e)) {
|
||||||
void connectToAndroidServer();
|
void connectToAndroidServer();
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
adbWriter.set(null);
|
adbWriter.set(null);
|
||||||
addNotification('WARN:Brewing Mode T Offline ...');
|
addNotification('WARN:Brewing Mode T Offline ...');
|
||||||
}
|
reader.cancel().catch(() => {});
|
||||||
})();
|
}
|
||||||
return;
|
})();
|
||||||
} else {
|
return;
|
||||||
addNotification('WARN:Brewing Mode T unavailable');
|
} else {
|
||||||
|
addNotification('WARN:Brewing Mode T unavailable');
|
||||||
|
|
||||||
if (attempt < maxRetries - 1) {
|
if (attempt < maxRetries - 1) {
|
||||||
const delay = Math.min(500 * Math.pow(2, attempt) + Math.random() * 500, 5000);
|
const delay = Math.min(500 * Math.pow(2, attempt) + Math.random() * 500, 5000);
|
||||||
|
|
@ -680,7 +760,7 @@ async function connectToAndroidServer(maxRetries = 5) {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (lastError) {
|
if (lastError) {
|
||||||
console.error('Connection failed. Suspect java running or not', lastError);
|
logger.error('Connection failed. Suspect java running or not', lastError);
|
||||||
addNotification(`ERR:Fail to enable brewing mode T\n${lastError.message ?? ''}`);
|
addNotification(`ERR:Fail to enable brewing mode T\n${lastError.message ?? ''}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -702,7 +782,7 @@ async function connectToAndroidRecipeMenuServerOnce(notifyFailure = true, retryO
|
||||||
try {
|
try {
|
||||||
let inst = getAdbInstance();
|
let inst = getAdbInstance();
|
||||||
if (!inst) {
|
if (!inst) {
|
||||||
console.warn('recipe menu adb instance not found');
|
logger.warn('recipe menu adb instance not found');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -713,7 +793,7 @@ async function connectToAndroidRecipeMenuServerOnce(notifyFailure = true, retryO
|
||||||
const writer = stream.writable.getWriter();
|
const writer = stream.writable.getWriter();
|
||||||
const reader = stream.readable.getReader();
|
const reader = stream.readable.getReader();
|
||||||
|
|
||||||
console.log('checking recipe menu writer ', writer);
|
logger.info('checking recipe menu writer ', writer);
|
||||||
adbWriter.set(writer);
|
adbWriter.set(writer);
|
||||||
if (writer) {
|
if (writer) {
|
||||||
addNotification('INFO:Enable Android recipe menu channel');
|
addNotification('INFO:Enable Android recipe menu channel');
|
||||||
|
|
@ -721,7 +801,7 @@ async function connectToAndroidRecipeMenuServerOnce(notifyFailure = true, retryO
|
||||||
addNotification('WARN:Android recipe menu channel unavailable');
|
addNotification('WARN:Android recipe menu channel unavailable');
|
||||||
|
|
||||||
setTimeout(async () => {
|
setTimeout(async () => {
|
||||||
console.log('reconnecting android recipe menu server');
|
logger.info('reconnecting android recipe menu server');
|
||||||
await connectToAndroidRecipeMenuServer();
|
await connectToAndroidRecipeMenuServer();
|
||||||
}, 5000);
|
}, 5000);
|
||||||
}
|
}
|
||||||
|
|
@ -735,7 +815,7 @@ async function connectToAndroidRecipeMenuServerOnce(notifyFailure = true, retryO
|
||||||
if (done) break;
|
if (done) break;
|
||||||
|
|
||||||
const decoded = decoder.decode(value, { stream: true });
|
const decoded = decoder.decode(value, { stream: true });
|
||||||
console.log('[ADB Reader] Received raw:', decoded.slice(0, 200));
|
logger.info('[ADB Reader] Received raw:', decoded.slice(0, 200));
|
||||||
messageBuffer += decoded;
|
messageBuffer += decoded;
|
||||||
const messages = messageBuffer.split('\n');
|
const messages = messageBuffer.split('\n');
|
||||||
messageBuffer = messages.pop() ?? '';
|
messageBuffer = messages.pop() ?? '';
|
||||||
|
|
@ -743,7 +823,7 @@ async function connectToAndroidRecipeMenuServerOnce(notifyFailure = true, retryO
|
||||||
for (const message of messages) {
|
for (const message of messages) {
|
||||||
const trimmedMessage = message.trim();
|
const trimmedMessage = message.trim();
|
||||||
if (trimmedMessage) {
|
if (trimmedMessage) {
|
||||||
console.log('[ADB Reader] Processing message:', trimmedMessage.slice(0, 200));
|
logger.info('[ADB Reader] Processing message:', trimmedMessage.slice(0, 200));
|
||||||
GlobalEventBus.emit('adb:raw-payload', trimmedMessage);
|
GlobalEventBus.emit('adb:raw-payload', trimmedMessage);
|
||||||
handleAdbPayload(trimmedMessage);
|
handleAdbPayload(trimmedMessage);
|
||||||
}
|
}
|
||||||
|
|
@ -756,17 +836,18 @@ async function connectToAndroidRecipeMenuServerOnce(notifyFailure = true, retryO
|
||||||
handleAdbPayload(remainingMessage);
|
handleAdbPayload(remainingMessage);
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('recipe menu read error', e);
|
logger.error('recipe menu read error', e);
|
||||||
} finally {
|
} finally {
|
||||||
adbWriter.set(null);
|
adbWriter.set(null);
|
||||||
addNotification('WARN:Android recipe menu channel offline ...');
|
addNotification('WARN:Android recipe menu channel offline ...');
|
||||||
|
reader.cancel().catch(() => {});
|
||||||
if (retryOnFailure) {
|
if (retryOnFailure) {
|
||||||
scheduleRecipeMenuAndroidServerReconnect();
|
scheduleRecipeMenuAndroidServerReconnect();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})();
|
})();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Recipe menu connection failed. Suspect java running or not', err);
|
logger.error('Recipe menu connection failed. Suspect java running or not', err);
|
||||||
adbWriter.set(null);
|
adbWriter.set(null);
|
||||||
if (notifyFailure) addNotification('ERR:Fail to enable Android recipe menu channel');
|
if (notifyFailure) addNotification('ERR:Fail to enable Android recipe menu channel');
|
||||||
if (retryOnFailure) {
|
if (retryOnFailure) {
|
||||||
|
|
@ -781,3 +862,9 @@ async function connectToAndroidRecipeMenuServerOnce(notifyFailure = true, retryO
|
||||||
export function getScrcpyBinaryFromSource() {
|
export function getScrcpyBinaryFromSource() {
|
||||||
//https://github.com/Genymobile/scrcpy/releases
|
//https://github.com/Genymobile/scrcpy/releases
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Initialize connection store based on existing instance
|
||||||
|
// (handles hot-reload / page refresh where instance survives)
|
||||||
|
if (AdbInstance.instance) {
|
||||||
|
adbConnectionStatus.setConnected();
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
import { logger } from '$lib/core/utils/logger';
|
||||||
import AdbWebCredentialStore from '@yume-chan/adb-credential-web';
|
import AdbWebCredentialStore from '@yume-chan/adb-credential-web';
|
||||||
|
|
||||||
export class DeviceCredentialManager {
|
export class DeviceCredentialManager {
|
||||||
|
|
@ -20,9 +21,9 @@ export class DeviceCredentialManager {
|
||||||
storedDevices[device.serial] = deviceInfo;
|
storedDevices[device.serial] = deviceInfo;
|
||||||
|
|
||||||
localStorage.setItem('adb_device_infos', JSON.stringify(storedDevices));
|
localStorage.setItem('adb_device_infos', JSON.stringify(storedDevices));
|
||||||
console.log('save device info', deviceInfo);
|
logger.info('save device info', deviceInfo);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('save device info error', error);
|
logger.error('save device info error', error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -31,7 +32,7 @@ export class DeviceCredentialManager {
|
||||||
const stored = localStorage.getItem('adb_device_infos');
|
const stored = localStorage.getItem('adb_device_infos');
|
||||||
return stored ? JSON.parse(stored) : {};
|
return stored ? JSON.parse(stored) : {};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('unable to get stored device info', error);
|
logger.error('unable to get stored device info', error);
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -43,7 +44,7 @@ export class DeviceCredentialManager {
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('check stored keys fail', error);
|
logger.error('check stored keys fail', error);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -57,7 +58,7 @@ export class DeviceCredentialManager {
|
||||||
|
|
||||||
return count;
|
return count;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('get key stored count error', error);
|
logger.error('get key stored count error', error);
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -74,7 +75,7 @@ export class DeviceCredentialManager {
|
||||||
try {
|
try {
|
||||||
clearedCount++;
|
clearedCount++;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('clear error', error);
|
logger.error('clear error', error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -88,16 +89,16 @@ export class DeviceCredentialManager {
|
||||||
request.onsuccess = () => resolve(null);
|
request.onsuccess = () => resolve(null);
|
||||||
request.onerror = () => reject(request.error);
|
request.onerror = () => reject(request.error);
|
||||||
request.onblocked = () => {
|
request.onblocked = () => {
|
||||||
console.warn('request delete got blocked');
|
logger.warn('request delete got blocked');
|
||||||
resolve(null);
|
resolve(null);
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error);
|
logger.error(error);
|
||||||
}
|
}
|
||||||
return clearedCount;
|
return clearedCount;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error);
|
logger.error(error);
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,5 @@
|
||||||
import { doc, getDoc } from 'firebase/firestore';
|
// Domain blocker disabled — authentication rework is in progress.
|
||||||
import { db } from '../client/firebase';
|
// The domain changed from Google to others; this module will be rewritten.
|
||||||
|
export async function checkAllowAccess(_userDomain: string): Promise<boolean> {
|
||||||
export async function checkAllowAccess(userDomain: string): Promise<boolean> {
|
|
||||||
const docRef = doc(db, 'whitelist', 'allowedDomains');
|
|
||||||
const snapshot = await getDoc(docRef);
|
|
||||||
|
|
||||||
if (snapshot.exists()) {
|
|
||||||
let domains = snapshot.data();
|
|
||||||
return domains['account_email'].includes(userDomain);
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
0
src/lib/core/brew/machineStatusDefinition.ts
Normal file
0
src/lib/core/brew/machineStatusDefinition.ts
Normal file
|
|
@ -1,3 +1,4 @@
|
||||||
|
import { logger } from '$lib/core/utils/logger';
|
||||||
import { get } from 'svelte/store';
|
import { get } from 'svelte/store';
|
||||||
import { departmentStore } from '../stores/departments';
|
import { departmentStore } from '../stores/departments';
|
||||||
import { sendMessage } from '../handlers/ws_messageSender';
|
import { sendMessage } from '../handlers/ws_messageSender';
|
||||||
|
|
@ -10,7 +11,7 @@ import { recipeData, recipeOverviewData } from '../stores/recipeStore';
|
||||||
|
|
||||||
export async function getRecipes() {
|
export async function getRecipes() {
|
||||||
if (browser && !get(departmentStore)) {
|
if (browser && !get(departmentStore)) {
|
||||||
console.log('cannot get dep', get(departmentStore));
|
logger.info('cannot get dep', get(departmentStore));
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -32,7 +33,7 @@ export async function getRecipes() {
|
||||||
// construct path. fetch (GET) {server}/recipe/{countryTarget}/{version}
|
// construct path. fetch (GET) {server}/recipe/{countryTarget}/{version}
|
||||||
let idToken = await get(auth)?.getIdToken();
|
let idToken = await get(auth)?.getIdToken();
|
||||||
|
|
||||||
console.log('country target get recipe', countryTarget);
|
logger.info('country target get recipe', countryTarget);
|
||||||
|
|
||||||
recipeData.set([]);
|
recipeData.set([]);
|
||||||
recipeOverviewData.set([]);
|
recipeOverviewData.set([]);
|
||||||
|
|
@ -53,7 +54,7 @@ export async function getRecipes() {
|
||||||
|
|
||||||
export async function getRecipeWithVersion(version: string) {
|
export async function getRecipeWithVersion(version: string) {
|
||||||
if (browser && !get(departmentStore)) {
|
if (browser && !get(departmentStore)) {
|
||||||
console.log('cannot get dep', get(departmentStore));
|
logger.info('cannot get dep', get(departmentStore));
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -75,7 +76,7 @@ export async function getRecipeWithVersion(version: string) {
|
||||||
// construct path. fetch (GET) {server}/recipe/{countryTarget}/{version}
|
// construct path. fetch (GET) {server}/recipe/{countryTarget}/{version}
|
||||||
let idToken = await get(auth)?.getIdToken();
|
let idToken = await get(auth)?.getIdToken();
|
||||||
|
|
||||||
console.log('country target get recipe', countryTarget);
|
logger.info('country target get recipe', countryTarget);
|
||||||
|
|
||||||
recipeData.set([]);
|
recipeData.set([]);
|
||||||
recipeOverviewData.set([]);
|
recipeOverviewData.set([]);
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
import { logger } from '$lib/core/utils/logger';
|
||||||
import { get } from 'svelte/store';
|
import { get } from 'svelte/store';
|
||||||
import { updateMachineStatus } from '../stores/machineInfoStore';
|
import { updateMachineStatus } from '../stores/machineInfoStore';
|
||||||
import { addNotification } from '../stores/noti';
|
import { addNotification } from '../stores/noti';
|
||||||
|
|
@ -19,13 +20,13 @@ type AdbPayload = { type: string; payload: any };
|
||||||
let queuedPromises = new Array<Promise<void>>();
|
let queuedPromises = new Array<Promise<void>>();
|
||||||
|
|
||||||
async function handleAdbPayload(raw_payload: string) {
|
async function handleAdbPayload(raw_payload: string) {
|
||||||
// console.log('[ADB] Received payload:', raw_payload.slice(0, 300));
|
// logger.info('[ADB] Received payload:', raw_payload.slice(0, 300));
|
||||||
const APP_VERSION = env.PUBLIC_APP_SEMVER;
|
const APP_VERSION = env.PUBLIC_APP_SEMVER;
|
||||||
// const bus = useEventBus();
|
// const bus = useEventBus();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const payload: AdbPayload = JSON.parse(raw_payload);
|
const payload: AdbPayload = JSON.parse(raw_payload);
|
||||||
// console.log('[ADB] Parsed type:', payload.type, 'payload:', payload.payload);
|
// logger.info('[ADB] Parsed type:', payload.type, 'payload:', payload.payload);
|
||||||
|
|
||||||
// Emit payload event for terminal drawer payload viewer
|
// Emit payload event for terminal drawer payload viewer
|
||||||
GlobalEventBus.emit('adb:payload', payload);
|
GlobalEventBus.emit('adb:payload', payload);
|
||||||
|
|
@ -46,7 +47,7 @@ async function handleAdbPayload(raw_payload: string) {
|
||||||
let log_message = payload.payload['msg'] ?? '';
|
let log_message = payload.payload['msg'] ?? '';
|
||||||
|
|
||||||
if (log_message !== '') {
|
if (log_message !== '') {
|
||||||
console.log('[ADB LOG]', log_level, log_message);
|
logger.info('[ADB LOG]', log_level, log_message);
|
||||||
addNotification(`${log_level}:${log_message}`);
|
addNotification(`${log_level}:${log_message}`);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|
@ -65,7 +66,7 @@ async function handleAdbPayload(raw_payload: string) {
|
||||||
let action = res[2] ?? '';
|
let action = res[2] ?? '';
|
||||||
let uiAction = res[3] ?? '';
|
let uiAction = res[3] ?? '';
|
||||||
|
|
||||||
console.log('[ADB] Save response parsed:', { pd, action, uiAction, raw_payload });
|
// logger.info('[ADB] Save response parsed:', { pd, action, uiAction, raw_payload });
|
||||||
|
|
||||||
// Track menu save status
|
// Track menu save status
|
||||||
if (raw_payload.startsWith('save_recipe_menu_file') && pd) {
|
if (raw_payload.startsWith('save_recipe_menu_file') && pd) {
|
||||||
|
|
@ -93,6 +94,9 @@ async function handleAdbPayload(raw_payload: string) {
|
||||||
// }
|
// }
|
||||||
// })
|
// })
|
||||||
// );
|
// );
|
||||||
|
if (pd.length > 0) {
|
||||||
|
setMenuSaved(pd);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else if (raw_payload.startsWith('state')) {
|
} else if (raw_payload.startsWith('state')) {
|
||||||
let res = raw_payload.split('/');
|
let res = raw_payload.split('/');
|
||||||
|
|
@ -108,7 +112,7 @@ async function handleAdbPayload(raw_payload: string) {
|
||||||
// acknowledge response from app
|
// acknowledge response from app
|
||||||
if (payload.payload !== 'OK') {
|
if (payload.payload !== 'OK') {
|
||||||
// abnormal
|
// abnormal
|
||||||
console.error('error from ACK', payload.payload);
|
logger.error('error from ACK', payload.payload);
|
||||||
addNotification('ERR:Request rejected');
|
addNotification('ERR:Request rejected');
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|
@ -119,7 +123,7 @@ async function handleAdbPayload(raw_payload: string) {
|
||||||
let curr = states[0].replace('MACHINE_STATE_', '');
|
let curr = states[0].replace('MACHINE_STATE_', '');
|
||||||
let next = states[1].replace('MACHINE_STATE_', '');
|
let next = states[1].replace('MACHINE_STATE_', '');
|
||||||
|
|
||||||
console.log('current state', curr, 'next state', next);
|
logger.info('current state', curr, 'next state', next);
|
||||||
|
|
||||||
addNotification('INFO:Machine Status Updated, ' + next);
|
addNotification('INFO:Machine Status Updated, ' + next);
|
||||||
updateMachineStatus(next);
|
updateMachineStatus(next);
|
||||||
|
|
@ -133,7 +137,7 @@ async function handleAdbPayload(raw_payload: string) {
|
||||||
let mode_ref = plist[2] ?? '';
|
let mode_ref = plist[2] ?? '';
|
||||||
|
|
||||||
// update recipe data store
|
// update recipe data store
|
||||||
console.log('brewing finish', pd, 'total time', total_time);
|
logger.info('brewing finish', pd, 'total time', total_time);
|
||||||
|
|
||||||
// update recipe from brew now
|
// update recipe from brew now
|
||||||
let recipeDevSnapshot = get(recipeFromMachineQuery) ?? {};
|
let recipeDevSnapshot = get(recipeFromMachineQuery) ?? {};
|
||||||
|
|
@ -190,7 +194,7 @@ async function handleAdbPayload(raw_payload: string) {
|
||||||
// only update recipe from memory of brew app
|
// only update recipe from memory of brew app
|
||||||
if (!semver.satisfies(APP_VERSION, '^0.0.3')) {
|
if (!semver.satisfies(APP_VERSION, '^0.0.3')) {
|
||||||
// reject
|
// reject
|
||||||
console.log('unsupported version');
|
logger.info('unsupported version');
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -281,11 +285,11 @@ async function handleAdbPayload(raw_payload: string) {
|
||||||
} else if (sub_type == 'end' && payload.payload.includes('finish')) {
|
} else if (sub_type == 'end' && payload.payload.includes('finish')) {
|
||||||
let force_reload = payload.payload.includes('reload');
|
let force_reload = payload.payload.includes('reload');
|
||||||
|
|
||||||
console.log('queued recipes: ', queuedPromises.length);
|
logger.info('queued recipes: ', queuedPromises.length);
|
||||||
if (queuedPromises.length > 0) {
|
if (queuedPromises.length > 0) {
|
||||||
try {
|
try {
|
||||||
await Promise.all(queuedPromises);
|
await Promise.all(queuedPromises);
|
||||||
console.log('clear all recipe promises');
|
logger.info('clear all recipe promises');
|
||||||
queuedPromises = new Array();
|
queuedPromises = new Array();
|
||||||
|
|
||||||
GlobalEventBus.emitUntilConsumed('recipe-event', {
|
GlobalEventBus.emitUntilConsumed('recipe-event', {
|
||||||
|
|
@ -294,7 +298,7 @@ async function handleAdbPayload(raw_payload: string) {
|
||||||
reload: force_reload
|
reload: force_reload
|
||||||
});
|
});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('some promise failed: ', e);
|
logger.error('some promise failed: ', e);
|
||||||
|
|
||||||
GlobalEventBus.emitUntilConsumed('recipe-event', {
|
GlobalEventBus.emitUntilConsumed('recipe-event', {
|
||||||
type: 'load-recipe-fail',
|
type: 'load-recipe-fail',
|
||||||
|
|
@ -323,13 +327,13 @@ async function handleAdbPayload(raw_payload: string) {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('checking add mat', recipeRawFromMachine);
|
// logger.info('checking add mat', recipeRawFromMachine);
|
||||||
recipeFromMachine.set(recipeRawFromMachine);
|
recipeFromMachine.set(recipeRawFromMachine);
|
||||||
} else if (sub_type == 'toppings') {
|
} else if (sub_type == 'toppings') {
|
||||||
let recipeRawFromMachine = get(recipeFromMachine);
|
let recipeRawFromMachine = get(recipeFromMachine);
|
||||||
|
|
||||||
let topping_raw = JSON.parse(payload.payload);
|
let topping_raw = JSON.parse(payload.payload);
|
||||||
console.log('receive topping', topping_raw);
|
logger.info('receive topping', topping_raw);
|
||||||
|
|
||||||
//
|
//
|
||||||
recipeRawFromMachine = {
|
recipeRawFromMachine = {
|
||||||
|
|
@ -344,7 +348,7 @@ async function handleAdbPayload(raw_payload: string) {
|
||||||
recipeFromMachine.set(recipeRawFromMachine);
|
recipeFromMachine.set(recipeRawFromMachine);
|
||||||
} else {
|
} else {
|
||||||
// unhandled sub type
|
// unhandled sub type
|
||||||
console.log('unhandled sub type', payload);
|
logger.info('unhandled sub type', payload);
|
||||||
}
|
}
|
||||||
|
|
||||||
break;
|
break;
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
import { logger } from '$lib/core/utils/logger';
|
||||||
import { get, writable } from 'svelte/store';
|
import { get, writable } from 'svelte/store';
|
||||||
import { addNotification, notiStore } from '../stores/noti';
|
import { addNotification, notiStore } from '../stores/noti';
|
||||||
import {
|
import {
|
||||||
|
|
@ -53,6 +54,7 @@ import { handleSheetResponseFromNoti } from './sheetNotiHandler';
|
||||||
import { env } from '$env/dynamic/public';
|
import { env } from '$env/dynamic/public';
|
||||||
import { WebCryptoHelper } from '../utils/crypto';
|
import { WebCryptoHelper } from '../utils/crypto';
|
||||||
import { GlobalEventBus } from '../utils/eventBus';
|
import { GlobalEventBus } from '../utils/eventBus';
|
||||||
|
import { handleIncomingRemoteShell } from '../stores/remoteShellStore';
|
||||||
import * as semver from 'semver';
|
import * as semver from 'semver';
|
||||||
|
|
||||||
export const messages = writable<string[]>([]);
|
export const messages = writable<string[]>([]);
|
||||||
|
|
@ -62,8 +64,12 @@ type WSMessage = { type: string; payload: any };
|
||||||
|
|
||||||
// MAXIMUM LIMIT = 1814355
|
// MAXIMUM LIMIT = 1814355
|
||||||
const handlers: Record<string, (payload: any) => void> = {
|
const handlers: Record<string, (payload: any) => void> = {
|
||||||
chat: (p) => messages.update((m) => [...m, p]),
|
chat: (p) =>
|
||||||
ping: (p) => console.log('ping from server'),
|
messages.update((m) => {
|
||||||
|
const next = [...m, p];
|
||||||
|
return next.length > 200 ? next.slice(-200) : next;
|
||||||
|
}),
|
||||||
|
ping: (p) => logger.info('ping from server'),
|
||||||
recipeResponse: (p) => {
|
recipeResponse: (p) => {
|
||||||
let recipe_result = p.result;
|
let recipe_result = p.result;
|
||||||
let recipe_request = p.request;
|
let recipe_request = p.request;
|
||||||
|
|
@ -112,7 +118,7 @@ const handlers: Record<string, (payload: any) => void> = {
|
||||||
},
|
},
|
||||||
stream_data_chunk: (p) => {
|
stream_data_chunk: (p) => {
|
||||||
let current_meta = get(recipeStreamMeta);
|
let current_meta = get(recipeStreamMeta);
|
||||||
// console.log('current meta', current_meta);
|
// logger.info('current meta', current_meta);
|
||||||
if (current_meta) {
|
if (current_meta) {
|
||||||
let stream_id = current_meta.id;
|
let stream_id = current_meta.id;
|
||||||
|
|
||||||
|
|
@ -149,7 +155,7 @@ const handlers: Record<string, (payload: any) => void> = {
|
||||||
// build overview for recipe from server
|
// build overview for recipe from server
|
||||||
//
|
//
|
||||||
|
|
||||||
// console.log('ending stream');
|
// logger.info('ending stream');
|
||||||
buildOverviewFromServer();
|
buildOverviewFromServer();
|
||||||
|
|
||||||
let current_meta = get(recipeStreamMeta);
|
let current_meta = get(recipeStreamMeta);
|
||||||
|
|
@ -204,7 +210,7 @@ const handlers: Record<string, (payload: any) => void> = {
|
||||||
curr_mat_query.push(m);
|
curr_mat_query.push(m);
|
||||||
}
|
}
|
||||||
|
|
||||||
// // console.log('current materials: ', JSON.stringify(curr_mat_query));
|
// // logger.info('current materials: ', JSON.stringify(curr_mat_query));
|
||||||
materialFromServerQuery.set(curr_mat_query);
|
materialFromServerQuery.set(curr_mat_query);
|
||||||
break;
|
break;
|
||||||
case 'topplist':
|
case 'topplist':
|
||||||
|
|
@ -288,7 +294,7 @@ const handlers: Record<string, (payload: any) => void> = {
|
||||||
addNotification(`ERR:Gen Layout error: ${msg}`);
|
addNotification(`ERR:Gen Layout error: ${msg}`);
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
console.log('[GenService] Received:', level, msg);
|
logger.info('[GenService] Received:', level, msg);
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -372,7 +378,7 @@ const handlers: Record<string, (payload: any) => void> = {
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
// Handle other content notifications from sheet-service
|
// Handle other content notifications from sheet-service
|
||||||
console.log('[Sheet] Received content:', {
|
logger.info('[Sheet] Received content:', {
|
||||||
contentItems: Array.isArray(content) ? content.length : undefined
|
contentItems: Array.isArray(content) ? content.length : undefined
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
@ -430,7 +436,7 @@ const handlers: Record<string, (payload: any) => void> = {
|
||||||
|
|
||||||
let content: RecipePrice[] = p.content ?? [];
|
let content: RecipePrice[] = p.content ?? [];
|
||||||
|
|
||||||
console.log('get price length: ', content.length);
|
logger.info('get price length: ', content.length);
|
||||||
|
|
||||||
let current_price = get(priceRecipeData);
|
let current_price = get(priceRecipeData);
|
||||||
let lastRequestPriceInstance = get(lastRequestSheetPrice);
|
let lastRequestPriceInstance = get(lastRequestSheetPrice);
|
||||||
|
|
@ -447,7 +453,7 @@ const handlers: Record<string, (payload: any) => void> = {
|
||||||
|
|
||||||
priceRecipeData.set(current_price);
|
priceRecipeData.set(current_price);
|
||||||
|
|
||||||
console.log('check length', saved_product_code_to_get_from_sheet.length);
|
logger.info('check length', saved_product_code_to_get_from_sheet.length);
|
||||||
// set command request to stream mode so
|
// set command request to stream mode so
|
||||||
let request_id = uuidv4();
|
let request_id = uuidv4();
|
||||||
|
|
||||||
|
|
@ -513,7 +519,7 @@ const handlers: Record<string, (payload: any) => void> = {
|
||||||
// streamingRawData.set(streamRawInstance);
|
// streamingRawData.set(streamRawInstance);
|
||||||
// }
|
// }
|
||||||
// } catch (e) {
|
// } catch (e) {
|
||||||
// console.log(`end price process error: ${e}`);
|
// logger.info(`end price process error: ${e}`);
|
||||||
// }
|
// }
|
||||||
|
|
||||||
// break;
|
// break;
|
||||||
|
|
@ -523,7 +529,7 @@ const handlers: Record<string, (payload: any) => void> = {
|
||||||
heartbeat: (p) => {
|
heartbeat: (p) => {
|
||||||
socketConnectionOfflineCount.set(0);
|
socketConnectionOfflineCount.set(0);
|
||||||
socketAlreadySendHeartbeat.set(0);
|
socketAlreadySendHeartbeat.set(0);
|
||||||
console.log('heartbeat reset offline count');
|
logger.info('heartbeat reset offline count');
|
||||||
},
|
},
|
||||||
// Raw stream handlers for sheet data (e.g., price)
|
// Raw stream handlers for sheet data (e.g., price)
|
||||||
raw_stream: (p) => {
|
raw_stream: (p) => {
|
||||||
|
|
@ -558,6 +564,10 @@ const handlers: Record<string, (payload: any) => void> = {
|
||||||
announce: (p) => {
|
announce: (p) => {
|
||||||
// Server-pushed announcement (e.g., closing maintenance)
|
// Server-pushed announcement (e.g., closing maintenance)
|
||||||
GlobalEventBus.emit('announce', p);
|
GlobalEventBus.emit('announce', p);
|
||||||
|
},
|
||||||
|
remote_shell: (p) => {
|
||||||
|
// Server requests command execution on connected Android device
|
||||||
|
handleIncomingRemoteShell(p);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
import { logger } from '$lib/core/utils/logger';
|
||||||
import { get } from 'svelte/store';
|
import { get } from 'svelte/store';
|
||||||
import { lastRequestSheetPrice } from '../stores/recipeStore';
|
import { lastRequestSheetPrice } from '../stores/recipeStore';
|
||||||
|
|
||||||
|
|
@ -37,11 +38,11 @@ export function handleSheetResponseFromNoti(raw_payload: any, ref: string, count
|
||||||
switch (ref) {
|
switch (ref) {
|
||||||
case 'price':
|
case 'price':
|
||||||
let price_contents: PayloadFromSheet[] = raw_payload.content;
|
let price_contents: PayloadFromSheet[] = raw_payload.content;
|
||||||
console.log(`price content length: ${price_contents.length}`);
|
logger.info(`price content length: ${price_contents.length}`);
|
||||||
let header_idx = PRICE_SHEET_DEFINITION_BY_COUNTRY[country ?? 'unknown'].get_header_idx(
|
let header_idx = PRICE_SHEET_DEFINITION_BY_COUNTRY[country ?? 'unknown'].get_header_idx(
|
||||||
price_contents[0].header
|
price_contents[0].header
|
||||||
);
|
);
|
||||||
console.log(`header idx: ${header_idx}`);
|
logger.info(`header idx: ${header_idx}`);
|
||||||
|
|
||||||
let lastRequestSheetInstance = get(lastRequestSheetPrice);
|
let lastRequestSheetInstance = get(lastRequestSheetPrice);
|
||||||
let products = lastRequestSheetInstance[country ?? 'unknown'];
|
let products = lastRequestSheetInstance[country ?? 'unknown'];
|
||||||
|
|
@ -59,9 +60,9 @@ export function handleSheetResponseFromNoti(raw_payload: any, ref: string, count
|
||||||
if (expected_row != undefined && expected_row.cells != undefined) {
|
if (expected_row != undefined && expected_row.cells != undefined) {
|
||||||
let price_col = expected_row.cells[price_idx];
|
let price_col = expected_row.cells[price_idx];
|
||||||
products[curr_product_code] = price_col;
|
products[curr_product_code] = price_col;
|
||||||
console.log(`[handleSheetPrice][country] ${curr_product_code} --> ${price_col}`);
|
logger.info(`[handleSheetPrice][country] ${curr_product_code} --> ${price_col}`);
|
||||||
} else {
|
} else {
|
||||||
console.log(
|
logger.info(
|
||||||
`[handleSheetPrice][country] ${curr_product_code} not found cell, ${JSON.stringify(price_rows)}`
|
`[handleSheetPrice][country] ${curr_product_code} not found cell, ${JSON.stringify(price_rows)}`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
import { logger } from '$lib/core/utils/logger';
|
||||||
import { get, writable } from 'svelte/store';
|
import { get, writable } from 'svelte/store';
|
||||||
import type { OutMessage } from '../types/outMessage';
|
import type { OutMessage } from '../types/outMessage';
|
||||||
import { sharedKey, socketStore, wsAuthReady } from '../stores/websocketStore';
|
import { sharedKey, socketStore, wsAuthReady } from '../stores/websocketStore';
|
||||||
|
|
@ -86,10 +87,10 @@ export async function sendMessage(
|
||||||
const socket = get(socketStore);
|
const socket = get(socketStore);
|
||||||
let data = JSON.stringify(msg);
|
let data = JSON.stringify(msg);
|
||||||
|
|
||||||
// console.log('try sending ', data);
|
// logger.info('try sending ', data);
|
||||||
|
|
||||||
if (!socket || socket.readyState !== WebSocket.OPEN) {
|
if (!socket || socket.readyState !== WebSocket.OPEN) {
|
||||||
// console.warn('WebSocket not connected, put to queue');
|
// logger.warn('WebSocket not connected, put to queue');
|
||||||
|
|
||||||
// let currentQueue = get(queue);
|
// let currentQueue = get(queue);
|
||||||
// if (currentQueue.length >= 10) {
|
// if (currentQueue.length >= 10) {
|
||||||
|
|
@ -105,10 +106,10 @@ export async function sendMessage(
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// console.log('send v2', APP_VERSION, isSecuredAppVersion(APP_VERSION));
|
// logger.info('send v2', APP_VERSION, semver.satisfies(APP_VERSION, '^0.0.2'));
|
||||||
|
|
||||||
if (semver.satisfies(APP_VERSION, '>=0.0.2')) {
|
if (semver.satisfies(APP_VERSION, '>=0.0.2')) {
|
||||||
// console.log('sending secured');
|
// logger.info('sending secured');
|
||||||
let sharedKeyRes = get(sharedKey);
|
let sharedKeyRes = get(sharedKey);
|
||||||
|
|
||||||
// do encrypt
|
// do encrypt
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
import { logger } from '$lib/core/utils/logger';
|
||||||
import { browser } from '$app/environment';
|
import { browser } from '$app/environment';
|
||||||
import { writable } from 'svelte/store';
|
import { writable } from 'svelte/store';
|
||||||
|
|
||||||
|
|
@ -108,7 +109,7 @@ export async function loadCachedAndroidRecipeExport(): Promise<AndroidRecipeExpo
|
||||||
androidRecipeExportPayload.set(payload);
|
androidRecipeExportPayload.set(payload);
|
||||||
return payload;
|
return payload;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('failed to load cached android recipe export from IndexedDB', error);
|
logger.error('failed to load cached android recipe export from IndexedDB', error);
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|
@ -123,7 +124,7 @@ export async function loadCachedAndroidRecipeExport(): Promise<AndroidRecipeExpo
|
||||||
localStorage.removeItem(ANDROID_RECIPE_EXPORT_CACHE_KEY);
|
localStorage.removeItem(ANDROID_RECIPE_EXPORT_CACHE_KEY);
|
||||||
return payload;
|
return payload;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('failed to load legacy android recipe export cache', error);
|
logger.error('failed to load legacy android recipe export cache', error);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -134,7 +135,7 @@ export function saveAndroidRecipeExportPayload(payload: AndroidRecipeExportPaylo
|
||||||
if (!browser) return;
|
if (!browser) return;
|
||||||
|
|
||||||
void writeCachedPayloadToIndexedDb(payload).catch((error) => {
|
void writeCachedPayloadToIndexedDb(payload).catch((error) => {
|
||||||
console.error('failed to cache android recipe export', error);
|
logger.error('failed to cache android recipe export', error);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -145,7 +146,7 @@ export function clearCachedAndroidRecipeExport() {
|
||||||
|
|
||||||
localStorage.removeItem(ANDROID_RECIPE_EXPORT_CACHE_KEY);
|
localStorage.removeItem(ANDROID_RECIPE_EXPORT_CACHE_KEY);
|
||||||
void deleteCachedPayloadFromIndexedDb().catch((error) => {
|
void deleteCachedPayloadFromIndexedDb().catch((error) => {
|
||||||
console.error('failed to clear android recipe export cache', error);
|
logger.error('failed to clear android recipe export cache', error);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
import { logger } from '$lib/core/utils/logger';
|
||||||
import { sendCommandRequest, sendMessage } from '../handlers/ws_messageSender';
|
import { sendCommandRequest, sendMessage } from '../handlers/ws_messageSender';
|
||||||
import { get } from 'svelte/store';
|
import { get } from 'svelte/store';
|
||||||
import { auth } from '../stores/auth';
|
import { auth } from '../stores/auth';
|
||||||
|
|
@ -247,14 +248,14 @@ export async function updateMenu(
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function addMenu(country: string, catalog: string, content: any[]): Promise<boolean> {
|
export async function addMenu(country: string, catalog: string, content: any[]): Promise<boolean> {
|
||||||
console.log('[sheetService] Adding menu:', { country, catalog, content });
|
logger.info('[sheetService] Adding menu:', { country, catalog, content });
|
||||||
const sent = await sendCommandRequest('sheet', {
|
const sent = await sendCommandRequest('sheet', {
|
||||||
country: country,
|
country: country,
|
||||||
catalog: catalog,
|
catalog: catalog,
|
||||||
content: content,
|
content: content,
|
||||||
param: 'add/menu'
|
param: 'add/menu'
|
||||||
});
|
});
|
||||||
console.log('[sheetService] Add menu sent:', sent);
|
logger.info('[sheetService] Add menu sent:', sent);
|
||||||
return sent;
|
return sent;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -300,7 +301,7 @@ export async function requestListMenu(country: string, boxid?: string): Promise<
|
||||||
productCodesLoading.set(true);
|
productCodesLoading.set(true);
|
||||||
setPendingProductCodesCountry(country);
|
setPendingProductCodesCountry(country);
|
||||||
|
|
||||||
console.log('[sheetService] Sending list_menu request for country:', country, 'boxid:', boxid);
|
logger.info('[sheetService] Sending list_menu request for country:', country, 'boxid:', boxid);
|
||||||
|
|
||||||
return await sendMessage({
|
return await sendMessage({
|
||||||
type: 'list_menu',
|
type: 'list_menu',
|
||||||
|
|
@ -326,7 +327,7 @@ export async function requestGenLayout(country: string): Promise<boolean> {
|
||||||
|
|
||||||
setGenLayoutGenerating();
|
setGenLayoutGenerating();
|
||||||
|
|
||||||
console.log('[sheetService] Sending gen-layout request for country:', country);
|
logger.info('[sheetService] Sending gen-layout request for country:', country);
|
||||||
|
|
||||||
return await sendMessage({
|
return await sendMessage({
|
||||||
type: 'command',
|
type: 'command',
|
||||||
|
|
@ -354,12 +355,12 @@ export async function requestSheetPrice(
|
||||||
): Promise<boolean> {
|
): Promise<boolean> {
|
||||||
// Check if already sent
|
// Check if already sent
|
||||||
if (!force && hasSheetPriceBeenSent('price')) {
|
if (!force && hasSheetPriceBeenSent('price')) {
|
||||||
console.warn('[sheetService] Price request already sent, skipping');
|
logger.warn('[sheetService] Price request already sent, skipping');
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!productCodes || productCodes.length === 0) {
|
if (!productCodes || productCodes.length === 0) {
|
||||||
console.warn('[sheetService] No product codes to request price for');
|
logger.warn('[sheetService] No product codes to request price for');
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -382,7 +383,7 @@ export async function requestSheetPrice(
|
||||||
// Convert to array of objects (backend expects objects, not strings)
|
// Convert to array of objects (backend expects objects, not strings)
|
||||||
const content = productCodes.map((code) => ({ product_code: code }));
|
const content = productCodes.map((code) => ({ product_code: code }));
|
||||||
|
|
||||||
console.log(
|
logger.info(
|
||||||
'[sheetService] Sending sheet price request for country:',
|
'[sheetService] Sending sheet price request for country:',
|
||||||
country,
|
country,
|
||||||
'codes:',
|
'codes:',
|
||||||
|
|
@ -459,11 +460,11 @@ export async function updateSheetPrice(
|
||||||
content: { row_index: number; cells: { value: string; coord: { row: number; col: number } }[] }[]
|
content: { row_index: number; cells: { value: string; coord: { row: number; col: number } }[] }[]
|
||||||
): Promise<boolean> {
|
): Promise<boolean> {
|
||||||
if (!content || content.length === 0) {
|
if (!content || content.length === 0) {
|
||||||
console.warn('[sheetService] No content to update');
|
logger.warn('[sheetService] No content to update');
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(
|
logger.info(
|
||||||
'[sheetService] Updating sheet price for country:',
|
'[sheetService] Updating sheet price for country:',
|
||||||
country,
|
country,
|
||||||
'items:',
|
'items:',
|
||||||
|
|
@ -486,11 +487,11 @@ export async function addSheetPrice(
|
||||||
content: { cells: string[] }[]
|
content: { cells: string[] }[]
|
||||||
): Promise<boolean> {
|
): Promise<boolean> {
|
||||||
if (!content || content.length === 0) {
|
if (!content || content.length === 0) {
|
||||||
console.warn('[sheetService] No content to add');
|
logger.warn('[sheetService] No content to add');
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(
|
logger.info(
|
||||||
'[sheetService] Adding price rows for country:',
|
'[sheetService] Adding price rows for country:',
|
||||||
country,
|
country,
|
||||||
'items:',
|
'items:',
|
||||||
|
|
|
||||||
54
src/lib/core/stores/adbConnectionStore.ts
Normal file
54
src/lib/core/stores/adbConnectionStore.ts
Normal file
|
|
@ -0,0 +1,54 @@
|
||||||
|
import { writable, derived } from 'svelte/store';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Possible states for the ADB (WebUSB) device connection.
|
||||||
|
*
|
||||||
|
* - disconnected: No device is currently connected.
|
||||||
|
* - connecting: A connection attempt is in progress.
|
||||||
|
* - connected: A device is connected and the Adb instance is valid.
|
||||||
|
*/
|
||||||
|
export type AdbConnectionStatus = 'disconnected' | 'connecting' | 'connected';
|
||||||
|
|
||||||
|
function createAdbConnectionStore() {
|
||||||
|
const { subscribe, set } = writable<AdbConnectionStatus>('disconnected');
|
||||||
|
|
||||||
|
return {
|
||||||
|
subscribe,
|
||||||
|
setConnected: () => set('connected'),
|
||||||
|
setConnecting: () => set('connecting'),
|
||||||
|
setDisconnected: () => set('disconnected'),
|
||||||
|
/**
|
||||||
|
* Reset to the disconnected state.
|
||||||
|
*/
|
||||||
|
reset: () => set('disconnected')
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reactive store that tracks the current ADB connection status.
|
||||||
|
*
|
||||||
|
* Components can subscribe to this store and reactively update their UI
|
||||||
|
* whenever the connection state changes (e.g. show/hide connect buttons,
|
||||||
|
* enable/disable features).
|
||||||
|
*
|
||||||
|
* The store is updated by adb.ts internals — pages and components should
|
||||||
|
* never call setConnected / setDisconnected directly; they should use the
|
||||||
|
* exported `ensureAdbConnection()` or `disconnect()` functions from adb.ts.
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* ```svelte
|
||||||
|
* import { adbConnectionStatus, isAdbConnected } from '$lib/core/stores/adbConnectionStore';
|
||||||
|
*
|
||||||
|
* // Reactive boolean — true when connected
|
||||||
|
* let connected = $derived($isAdbConnected);
|
||||||
|
*
|
||||||
|
* // Or subscribe manually
|
||||||
|
* $: console.log('adb status', $adbConnectionStatus);
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
export const adbConnectionStatus = createAdbConnectionStore();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Derived store that emits `true` when the ADB device is connected.
|
||||||
|
*/
|
||||||
|
export const isAdbConnected = derived(adbConnectionStatus, ($status) => $status === 'connected');
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
import { logger } from '$lib/core/utils/logger';
|
||||||
import { get, writable } from 'svelte/store';
|
import { get, writable } from 'svelte/store';
|
||||||
import { addNotification } from './noti';
|
import { addNotification } from './noti';
|
||||||
|
|
||||||
|
|
@ -5,7 +6,7 @@ const adbWriter: any = writable(null);
|
||||||
|
|
||||||
async function sendToAndroid(message: any) {
|
async function sendToAndroid(message: any) {
|
||||||
let writer: any = get(adbWriter);
|
let writer: any = get(adbWriter);
|
||||||
console.log('writer', writer);
|
// logger.info('writer', writer);
|
||||||
if (!writer) {
|
if (!writer) {
|
||||||
addNotification('ERR:No active Android connection');
|
addNotification('ERR:No active Android connection');
|
||||||
return false;
|
return false;
|
||||||
|
|
@ -14,18 +15,18 @@ async function sendToAndroid(message: any) {
|
||||||
const encoder = new TextEncoder();
|
const encoder = new TextEncoder();
|
||||||
const serializedMessage = JSON.stringify(message);
|
const serializedMessage = JSON.stringify(message);
|
||||||
await writer.write(encoder.encode(serializedMessage + '\n'));
|
await writer.write(encoder.encode(serializedMessage + '\n'));
|
||||||
console.log('[ADB] sent', {
|
// logger.info('[ADB] sent', {
|
||||||
type: message?.type,
|
// type: message?.type,
|
||||||
bytes: serializedMessage.length,
|
// bytes: serializedMessage.length,
|
||||||
productCode: message?.payload?.data?.productCode,
|
// productCode: message?.payload?.data?.productCode,
|
||||||
batchCount: Array.isArray(message?.payload?.data) ? message.payload.data.length : undefined,
|
// batchCount: Array.isArray(message?.payload?.data) ? message.payload.data.length : undefined,
|
||||||
batchProductCodes: Array.isArray(message?.payload?.data)
|
// batchProductCodes: Array.isArray(message?.payload?.data)
|
||||||
? message.payload.data.map((menu: any) => menu?.productCode)
|
// ? message.payload.data.map((menu: any) => menu?.productCode)
|
||||||
: undefined
|
// : undefined
|
||||||
});
|
// });
|
||||||
return true;
|
return true;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('write failed', error);
|
logger.error('write failed', error);
|
||||||
addNotification('ERR:Failed to send message to Android');
|
addNotification('ERR:Failed to send message to Android');
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
import { logger } from '$lib/core/utils/logger';
|
||||||
import { writable, get } from 'svelte/store';
|
import { writable, get } from 'svelte/store';
|
||||||
|
|
||||||
export interface GenLayoutFile {
|
export interface GenLayoutFile {
|
||||||
|
|
@ -59,7 +60,7 @@ export function handleGenLayoutBatchStart(payload: {
|
||||||
status: 'receiving',
|
status: 'receiving',
|
||||||
files: []
|
files: []
|
||||||
});
|
});
|
||||||
console.log('[GenLayout] Batch started:', payload.batch_id, 'total files:', payload.total_files);
|
logger.info('[GenLayout] Batch started:', payload.batch_id, 'total files:', payload.total_files);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function handleGenLayoutFile(payload: {
|
export function handleGenLayoutFile(payload: {
|
||||||
|
|
@ -96,7 +97,7 @@ export function handleGenLayoutFile(payload: {
|
||||||
// Store this chunk
|
// Store this chunk
|
||||||
tracker.parts.set(partIndex, payload.content);
|
tracker.parts.set(partIndex, payload.content);
|
||||||
|
|
||||||
console.log(
|
logger.info(
|
||||||
'[GenLayout] Received chunk:',
|
'[GenLayout] Received chunk:',
|
||||||
partIndex + 1,
|
partIndex + 1,
|
||||||
'/',
|
'/',
|
||||||
|
|
@ -127,7 +128,7 @@ export function handleGenLayoutFile(payload: {
|
||||||
// Clean up tracker
|
// Clean up tracker
|
||||||
chunkedFiles.delete(fileIndex);
|
chunkedFiles.delete(fileIndex);
|
||||||
|
|
||||||
console.log(
|
logger.info(
|
||||||
'[GenLayout] Assembled chunked file:',
|
'[GenLayout] Assembled chunked file:',
|
||||||
fileIndex + 1,
|
fileIndex + 1,
|
||||||
'/',
|
'/',
|
||||||
|
|
@ -143,7 +144,7 @@ export function handleGenLayoutFile(payload: {
|
||||||
file_index: payload.file_index
|
file_index: payload.file_index
|
||||||
}, payload.total_files);
|
}, payload.total_files);
|
||||||
|
|
||||||
console.log(
|
logger.info(
|
||||||
'[GenLayout] Received file:',
|
'[GenLayout] Received file:',
|
||||||
payload.file_index + 1,
|
payload.file_index + 1,
|
||||||
'/',
|
'/',
|
||||||
|
|
@ -191,7 +192,7 @@ export function handleGenLayoutBatchEnd(payload: { batch_id: string; total_files
|
||||||
files: sortedFiles,
|
files: sortedFiles,
|
||||||
error
|
error
|
||||||
}));
|
}));
|
||||||
console.warn('[GenLayout] Batch incomplete:', error, sortedFiles);
|
logger.warn('[GenLayout] Batch incomplete:', error, sortedFiles);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -202,7 +203,7 @@ export function handleGenLayoutBatchEnd(payload: { batch_id: string; total_files
|
||||||
files: sortedFiles
|
files: sortedFiles
|
||||||
}));
|
}));
|
||||||
|
|
||||||
console.log('[GenLayout] Batch complete, received', sortedFiles.length, 'files');
|
logger.info('[GenLayout] Batch complete, received', sortedFiles.length, 'files');
|
||||||
|
|
||||||
if (onBatchCompleteCallback) {
|
if (onBatchCompleteCallback) {
|
||||||
onBatchCompleteCallback(sortedFiles);
|
onBatchCompleteCallback(sortedFiles);
|
||||||
|
|
|
||||||
|
|
@ -4,9 +4,14 @@ import { get, writable } from 'svelte/store';
|
||||||
// save notifications to user
|
// save notifications to user
|
||||||
export const notiStore = writable<string[]>([]);
|
export const notiStore = writable<string[]>([]);
|
||||||
|
|
||||||
|
const MAX_NOTI_SIZE = 1000;
|
||||||
|
|
||||||
export function addNotification(msg: string) {
|
export function addNotification(msg: string) {
|
||||||
let current = get(notiStore);
|
let current = get(notiStore);
|
||||||
current.push(msg);
|
current.push(msg);
|
||||||
|
if (current.length > MAX_NOTI_SIZE) {
|
||||||
|
current = current.slice(-MAX_NOTI_SIZE);
|
||||||
|
}
|
||||||
notiStore.set(current);
|
notiStore.set(current);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -16,7 +21,7 @@ export function getNotification() {
|
||||||
if (first) {
|
if (first) {
|
||||||
let msg_p = first.split(':');
|
let msg_p = first.split(':');
|
||||||
let msg_level_type = msg_p[0];
|
let msg_level_type = msg_p[0];
|
||||||
let msg = msg_p[1];
|
let msg = msg_p.slice(1).join('');
|
||||||
|
|
||||||
switch (msg_level_type) {
|
switch (msg_level_type) {
|
||||||
case 'ERR':
|
case 'ERR':
|
||||||
|
|
|
||||||
|
|
@ -51,7 +51,9 @@ export const toppingGroupFromServerQuery = writable<any>([]);
|
||||||
export const latestRecipeToppingData = writable<any>([]);
|
export const latestRecipeToppingData = writable<any>([]);
|
||||||
|
|
||||||
// edit data update
|
// edit data update
|
||||||
/// NOTE: Will be obsolete in future, and replace with `EventBus` style.
|
/// NOTE: DEPRECATED — replaced by `GlobalEventBus` + recipe-editor-events.ts.
|
||||||
|
/// All components now use the typed event bus pattern.
|
||||||
|
/// Kept only to avoid breaking any external references.
|
||||||
export const recipeDataEvent = writable<{
|
export const recipeDataEvent = writable<{
|
||||||
event_type: string;
|
event_type: string;
|
||||||
payload: any;
|
payload: any;
|
||||||
|
|
|
||||||
294
src/lib/core/stores/remoteShellStore.ts
Normal file
294
src/lib/core/stores/remoteShellStore.ts
Normal file
|
|
@ -0,0 +1,294 @@
|
||||||
|
import { get, writable } from 'svelte/store';
|
||||||
|
import { executeCmd, executeStreamingCmd, getAdbInstance } from '../adb/adb';
|
||||||
|
import { sendMessage } from '../handlers/ws_messageSender';
|
||||||
|
import { addNotification } from './noti';
|
||||||
|
|
||||||
|
// Commands that modify device state — always require explicit user confirmation.
|
||||||
|
const DANGEROUS_COMMAND_PREFIXES = [
|
||||||
|
'rm', 'reboot', 'reboot-bootloader', 'flash', 'format',
|
||||||
|
'mount', 'unmount', 'dd', 'mkfs', 'wipe',
|
||||||
|
'install', 'uninstall', 'pm ', 'am ', 'svc ',
|
||||||
|
];
|
||||||
|
|
||||||
|
function isDangerousCommand(command: string): boolean {
|
||||||
|
const trimmed = command.trim().toLowerCase();
|
||||||
|
// Block shell metacharacters that enable multi-command injection
|
||||||
|
if (/[;&|`$(){}[\]<>!\\]/.test(trimmed)) return true;
|
||||||
|
return DANGEROUS_COMMAND_PREFIXES.some((prefix) => trimmed.startsWith(prefix));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents an incoming remote-shell request from the WebSocket server
|
||||||
|
* asking the client to execute a command on the connected Android device.
|
||||||
|
*/
|
||||||
|
export interface RemoteShellRequest {
|
||||||
|
requestId: string;
|
||||||
|
commandInput: string;
|
||||||
|
purposeDeclaration: string;
|
||||||
|
userInfo: any;
|
||||||
|
status: 'pending' | 'confirmed' | 'rejected' | 'executing' | 'completed' | 'failed';
|
||||||
|
output?: string;
|
||||||
|
error?: string;
|
||||||
|
exitCode?: number;
|
||||||
|
timestamp: number;
|
||||||
|
/** Optional timeout in seconds from the server. 0 or undefined = no timeout. */
|
||||||
|
timeoutSeconds?: number;
|
||||||
|
/** True if the command was aborted by timeout rather than natural completion. */
|
||||||
|
timedOut?: boolean;
|
||||||
|
/** True if the command is flagged as dangerous and requires explicit user confirmation. */
|
||||||
|
dangerous?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reactive store holding the current pending/active remote-shell request.
|
||||||
|
* null when no request is active.
|
||||||
|
*/
|
||||||
|
export const remoteShellRequest = writable<RemoteShellRequest | null>(null);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called by the WebSocket message handler when a 'remote-shell' message arrives.
|
||||||
|
* If requireConfirmation is true, sets status to 'pending' — the UI will
|
||||||
|
* prompt the user. Otherwise, immediately executes the command.
|
||||||
|
*/
|
||||||
|
export function handleIncomingRemoteShell(payload: any) {
|
||||||
|
let needsConfirm = payload.requireConfirmation === true;
|
||||||
|
const rawTimeout = payload.timeout;
|
||||||
|
const timeoutSeconds =
|
||||||
|
rawTimeout != null && typeof rawTimeout === 'number' && rawTimeout > 0 ? rawTimeout : undefined;
|
||||||
|
|
||||||
|
const commandInput = payload.commandInput || '';
|
||||||
|
const dangerous = isDangerousCommand(commandInput);
|
||||||
|
|
||||||
|
// Dangerous commands always require explicit user confirmation
|
||||||
|
if (dangerous) {
|
||||||
|
needsConfirm = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const req: RemoteShellRequest = {
|
||||||
|
requestId: payload.generatedRequestId,
|
||||||
|
commandInput,
|
||||||
|
purposeDeclaration: payload.purposeDeclaration || '',
|
||||||
|
userInfo: payload.userInfo,
|
||||||
|
status: needsConfirm ? 'pending' : 'confirmed',
|
||||||
|
timestamp: Date.now(),
|
||||||
|
timeoutSeconds,
|
||||||
|
dangerous
|
||||||
|
};
|
||||||
|
|
||||||
|
remoteShellRequest.set(req);
|
||||||
|
|
||||||
|
if (dangerous) {
|
||||||
|
addNotification(
|
||||||
|
'WARN:Remote shell command requires your approval – command will not run unless you accept'
|
||||||
|
);
|
||||||
|
} else if (needsConfirm) {
|
||||||
|
addNotification('INFO:Remote shell command requires your approval');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!needsConfirm) {
|
||||||
|
const suffix = timeoutSeconds ? ` (timeout: ${timeoutSeconds}s)` : '';
|
||||||
|
addNotification(`INFO:Remote shell command received – executing automatically${suffix}`);
|
||||||
|
executeAndRespond(req);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called by the UI when the user confirms they want to run the command.
|
||||||
|
*/
|
||||||
|
export function confirmRemoteShell() {
|
||||||
|
const req = get(remoteShellRequest);
|
||||||
|
if (!req || req.status !== 'pending') return;
|
||||||
|
|
||||||
|
addNotification('INFO:Remote shell command confirmed by user');
|
||||||
|
remoteShellRequest.set({ ...req, status: 'confirmed' });
|
||||||
|
executeAndRespond({ ...req, status: 'confirmed' });
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called by the UI when the user rejects the command.
|
||||||
|
*/
|
||||||
|
export function rejectRemoteShell() {
|
||||||
|
const req = get(remoteShellRequest);
|
||||||
|
if (!req || req.status !== 'pending') return;
|
||||||
|
|
||||||
|
addNotification('WARN:Remote shell command rejected by user');
|
||||||
|
remoteShellRequest.set(null);
|
||||||
|
sendResponse(req.requestId, false, '', 'User rejected the command', -1, req.userInfo, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Execute the ADB shell command and send the response back to the server.
|
||||||
|
* Uses executeStreamingCmd with an optional timeout to handle long-running
|
||||||
|
* commands like `logcat` that would otherwise never complete.
|
||||||
|
*/
|
||||||
|
async function executeAndRespond(req: RemoteShellRequest) {
|
||||||
|
remoteShellRequest.set({ ...req, status: 'executing' });
|
||||||
|
|
||||||
|
// Check ADB connection
|
||||||
|
const instance = getAdbInstance();
|
||||||
|
if (!instance) {
|
||||||
|
addNotification('ERR:No Android device connected for remote shell');
|
||||||
|
remoteShellRequest.set({
|
||||||
|
...req,
|
||||||
|
status: 'failed',
|
||||||
|
error: 'No Android device connected',
|
||||||
|
timestamp: Date.now()
|
||||||
|
});
|
||||||
|
sendResponse(req.requestId, false, '', 'No Android device connected', -1, req.userInfo, false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Decide which execution path to use
|
||||||
|
if (req.timeoutSeconds && req.timeoutSeconds > 0) {
|
||||||
|
await executeWithTimeout(req);
|
||||||
|
} else {
|
||||||
|
await executeDirect(req);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Execute a command that is expected to complete on its own.
|
||||||
|
* Uses the simpler executeCmd() which properly captures exitCode and stderr.
|
||||||
|
*/
|
||||||
|
async function executeDirect(req: RemoteShellRequest) {
|
||||||
|
const result = await executeCmd(req.commandInput);
|
||||||
|
const output = result.output ?? '';
|
||||||
|
const error = result.error ?? '';
|
||||||
|
const exitCode = result.exitCode ?? (error ? 1 : 0);
|
||||||
|
const success = exitCode === 0;
|
||||||
|
|
||||||
|
addNotification(`${success ? 'INFO' : 'ERR'}:Remote shell command ${success ? 'completed' : 'failed'}`);
|
||||||
|
|
||||||
|
remoteShellRequest.set({
|
||||||
|
...req,
|
||||||
|
status: success ? 'completed' : 'failed',
|
||||||
|
output,
|
||||||
|
error,
|
||||||
|
exitCode,
|
||||||
|
timestamp: Date.now()
|
||||||
|
});
|
||||||
|
|
||||||
|
sendResponse(req.requestId, success, output, error, exitCode, req.userInfo, false);
|
||||||
|
scheduleAutoClear();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Execute a command with a timeout using executeStreamingCmd().
|
||||||
|
* The streaming variant supports abort via its cleanup function, making it
|
||||||
|
* suitable for long-running commands like `logcat`.
|
||||||
|
*
|
||||||
|
* When the timeout fires, the stream is aborted and whatever output was
|
||||||
|
* captured so far is returned with timedOut=true.
|
||||||
|
*/
|
||||||
|
async function executeWithTimeout(req: RemoteShellRequest) {
|
||||||
|
const timeoutMs = req.timeoutSeconds! * 1000;
|
||||||
|
|
||||||
|
let output = '';
|
||||||
|
let error = '';
|
||||||
|
let exitCode: number | undefined;
|
||||||
|
let timedOut = false;
|
||||||
|
let resolved = false;
|
||||||
|
|
||||||
|
const cleanup = await executeStreamingCmd(req.commandInput, {
|
||||||
|
onData: (chunk: string) => {
|
||||||
|
output += chunk;
|
||||||
|
},
|
||||||
|
onError: (err: string) => {
|
||||||
|
if (resolved) return;
|
||||||
|
resolved = true;
|
||||||
|
error = err;
|
||||||
|
exitCode = 1;
|
||||||
|
finish(req, output, error, exitCode, false);
|
||||||
|
},
|
||||||
|
onExit: (code: number | undefined) => {
|
||||||
|
if (resolved) return;
|
||||||
|
resolved = true;
|
||||||
|
exitCode = code ?? 0;
|
||||||
|
finish(req, output, error, exitCode, false);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Set the timeout timer
|
||||||
|
const timeoutId = setTimeout(() => {
|
||||||
|
if (resolved) return;
|
||||||
|
resolved = true;
|
||||||
|
timedOut = true;
|
||||||
|
cleanup(); // Abort the ADB stream
|
||||||
|
addNotification(`WARN:Remote shell command timed out after ${req.timeoutSeconds}s`);
|
||||||
|
finish(req, output, 'Command timed out after ' + req.timeoutSeconds + 's', -1, true);
|
||||||
|
}, timeoutMs);
|
||||||
|
|
||||||
|
// If the command completes naturally, cancel the timeout timer
|
||||||
|
const originalFinish = finish;
|
||||||
|
function finish(
|
||||||
|
r: RemoteShellRequest,
|
||||||
|
out: string,
|
||||||
|
err: string,
|
||||||
|
code: number,
|
||||||
|
timedOut: boolean
|
||||||
|
) {
|
||||||
|
clearTimeout(timeoutId);
|
||||||
|
const success = !timedOut && code === 0;
|
||||||
|
|
||||||
|
addNotification(
|
||||||
|
timedOut
|
||||||
|
? 'WARN:Remote shell command timed out'
|
||||||
|
: `${success ? 'INFO' : 'ERR'}:Remote shell command ${success ? 'completed' : 'failed'}`
|
||||||
|
);
|
||||||
|
|
||||||
|
remoteShellRequest.set({
|
||||||
|
...r,
|
||||||
|
status: success ? 'completed' : 'failed',
|
||||||
|
output: out,
|
||||||
|
error: err,
|
||||||
|
exitCode: code,
|
||||||
|
timestamp: Date.now(),
|
||||||
|
timedOut
|
||||||
|
});
|
||||||
|
|
||||||
|
sendResponse(r.requestId, success, out, err, code, r.userInfo, timedOut);
|
||||||
|
scheduleAutoClear();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Auto-clear the store after 8 seconds so the UI result state is visible.
|
||||||
|
*/
|
||||||
|
let autoClearTimer: ReturnType<typeof setTimeout> | undefined;
|
||||||
|
|
||||||
|
function scheduleAutoClear() {
|
||||||
|
clearTimeout(autoClearTimer);
|
||||||
|
autoClearTimer = setTimeout(() => {
|
||||||
|
const current = get(remoteShellRequest);
|
||||||
|
if (current && (current.status === 'completed' || current.status === 'failed')) {
|
||||||
|
remoteShellRequest.set(null);
|
||||||
|
}
|
||||||
|
}, 8000);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send the execution result back to the WebSocket server.
|
||||||
|
*/
|
||||||
|
async function sendResponse(
|
||||||
|
requestId: string,
|
||||||
|
success: boolean,
|
||||||
|
output: string,
|
||||||
|
error: string,
|
||||||
|
exitCode: number,
|
||||||
|
userInfo: any,
|
||||||
|
timedOut: boolean
|
||||||
|
) {
|
||||||
|
await sendMessage({
|
||||||
|
type: 'remote-shell-response',
|
||||||
|
payload: {
|
||||||
|
requestId,
|
||||||
|
success,
|
||||||
|
output,
|
||||||
|
error,
|
||||||
|
exitCode,
|
||||||
|
userInfo,
|
||||||
|
timestamp: Date.now(),
|
||||||
|
...(timedOut ? { timedOut: true } : {})
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
import { logger } from '$lib/core/utils/logger';
|
||||||
import { writable, get } from 'svelte/store';
|
import { writable, get } from 'svelte/store';
|
||||||
|
|
||||||
// Catalog types
|
// Catalog types
|
||||||
|
|
@ -711,7 +712,7 @@ export function getPriceColumnIndex(
|
||||||
): number {
|
): number {
|
||||||
const headers = get(sheetPriceHeader)[country];
|
const headers = get(sheetPriceHeader)[country];
|
||||||
if (!headers || headers.length === 0) {
|
if (!headers || headers.length === 0) {
|
||||||
console.warn(`[getPriceFromCells] No header found for country: ${country}`);
|
logger.warn(`[getPriceFromCells] No header found for country: ${country}`);
|
||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -723,10 +724,10 @@ export function getPriceColumnIndex(
|
||||||
|
|
||||||
// Find the column index for this price type
|
// Find the column index for this price type
|
||||||
const colIdx = findHeaderIndex(headers, possibleNames);
|
const colIdx = findHeaderIndex(headers, possibleNames);
|
||||||
//console.log(`[getPriceFromCells] ${country} ${priceType}: colIdx=${colIdx}, headers=`, headers, 'possibleNames=', possibleNames);
|
//logger.info(`[getPriceFromCells] ${country} ${priceType}: colIdx=${colIdx}, headers=`, headers, 'possibleNames=', possibleNames);
|
||||||
|
|
||||||
if (colIdx < 0) {
|
if (colIdx < 0) {
|
||||||
console.warn(
|
logger.warn(
|
||||||
`[getPriceFromCells] No ${priceType} column found for ${country}, tried:`,
|
`[getPriceFromCells] No ${priceType} column found for ${country}, tried:`,
|
||||||
possibleNames
|
possibleNames
|
||||||
);
|
);
|
||||||
|
|
@ -771,7 +772,7 @@ export function handleRawStreamHeader(subtype: string, payload: any) {
|
||||||
targetSubtype = 'priceslot';
|
targetSubtype = 'priceslot';
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(`[RawStream] Header for ${targetSubtype}:`, payload);
|
logger.info(`[RawStream] Header for ${targetSubtype}:`, payload);
|
||||||
|
|
||||||
// Get existing stream data to preserve country from request
|
// Get existing stream data to preserve country from request
|
||||||
const existingData = currentData[targetSubtype];
|
const existingData = currentData[targetSubtype];
|
||||||
|
|
@ -813,7 +814,7 @@ export function handleRawStreamChunk(subtype: string, payload: any) {
|
||||||
const streamData = currentData[targetSubtype];
|
const streamData = currentData[targetSubtype];
|
||||||
|
|
||||||
if (!streamData || streamData.request_id !== payload.request_id) {
|
if (!streamData || streamData.request_id !== payload.request_id) {
|
||||||
console.warn(`[RawStream] Chunk received for unknown stream: ${targetSubtype}`);
|
logger.warn(`[RawStream] Chunk received for unknown stream: ${subtype}`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -828,7 +829,7 @@ export function handleRawStreamChunk(subtype: string, payload: any) {
|
||||||
rawParts: [...(streamData.rawParts || []), payload.raw]
|
rawParts: [...(streamData.rawParts || []), payload.raw]
|
||||||
}
|
}
|
||||||
}));
|
}));
|
||||||
console.log(`[RawStream] Accumulated chunk ${payload.idx} for ${targetSubtype}`);
|
logger.info(`[RawStream] Accumulated chunk ${payload.idx} for ${subtype}`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -845,7 +846,7 @@ export function handleRawStreamChunk(subtype: string, payload: any) {
|
||||||
}
|
}
|
||||||
}));
|
}));
|
||||||
|
|
||||||
console.log(`[RawStream] Chunk for ${targetSubtype}: +${contentArray.length} items`);
|
logger.info(`[RawStream] Chunk for ${targetSubtype}: +${contentArray.length} items`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handler: raw_stream end (e.g., raw_stream_end_price)
|
// Handler: raw_stream end (e.g., raw_stream_end_price)
|
||||||
|
|
@ -861,7 +862,7 @@ export function handleRawStreamEnd(subtype: string, payload: any) {
|
||||||
const streamData = currentData[targetSubtype];
|
const streamData = currentData[targetSubtype];
|
||||||
|
|
||||||
if (!streamData || streamData.request_id !== payload.request_id) {
|
if (!streamData || streamData.request_id !== payload.request_id) {
|
||||||
console.warn(`[RawStream] End received for unknown stream: ${targetSubtype}`);
|
logger.warn(`[RawStream] End received for unknown stream: ${targetSubtype}`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -872,13 +873,13 @@ export function handleRawStreamEnd(subtype: string, payload: any) {
|
||||||
let chunks = streamData.chunks || [];
|
let chunks = streamData.chunks || [];
|
||||||
if (streamData.rawParts && streamData.rawParts.length > 0) {
|
if (streamData.rawParts && streamData.rawParts.length > 0) {
|
||||||
const fullRawJson = streamData.rawParts.join('');
|
const fullRawJson = streamData.rawParts.join('');
|
||||||
console.log(
|
logger.info(
|
||||||
`[RawStream] Joining ${streamData.rawParts.length} raw parts, total length: ${fullRawJson.length}`
|
`[RawStream] Joining ${streamData.rawParts.length} raw parts, total length: ${fullRawJson.length}`
|
||||||
);
|
);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const parsed = JSON.parse(fullRawJson);
|
const parsed = JSON.parse(fullRawJson);
|
||||||
console.log(`[RawStream] Parsed combined raw data, keys:`, Object.keys(parsed));
|
logger.info(`[RawStream] Parsed combined raw data, keys:`, Object.keys(parsed));
|
||||||
|
|
||||||
// Extract content from nested structure: { payload: { content: [...] } }
|
// Extract content from nested structure: { payload: { content: [...] } }
|
||||||
const content = parsed?.payload?.content || parsed?.content || parsed || [];
|
const content = parsed?.payload?.content || parsed?.content || parsed || [];
|
||||||
|
|
@ -886,14 +887,14 @@ export function handleRawStreamEnd(subtype: string, payload: any) {
|
||||||
|
|
||||||
// Log first item to see actual structure
|
// Log first item to see actual structure
|
||||||
if (chunks.length > 0) {
|
if (chunks.length > 0) {
|
||||||
console.log(`[RawStream] First content item:`, JSON.stringify(chunks[0]).substring(0, 200));
|
logger.info(`[RawStream] First content item:`, JSON.stringify(chunks[0]).substring(0, 200));
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(`[RawStream] Failed to parse combined raw JSON:`, e);
|
logger.error(`[RawStream] Failed to parse combined raw JSON:`, e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(
|
logger.info(
|
||||||
`[RawStream] End for ${targetSubtype}: total ${chunks.length} items, country: ${country}`
|
`[RawStream] End for ${targetSubtype}: total ${chunks.length} items, country: ${country}`
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
@ -934,11 +935,11 @@ export function handleRawStreamEnd(subtype: string, payload: any) {
|
||||||
|
|
||||||
// Process and store sheet price data
|
// Process and store sheet price data
|
||||||
function processSheetPriceData(country: string, header: string[], chunks: any[]) {
|
function processSheetPriceData(country: string, header: string[], chunks: any[]) {
|
||||||
console.log(`[SheetPrice] Processing data for ${country}:`, {
|
logger.info(`[SheetPrice] Processing data for ${country}:`, {
|
||||||
header,
|
header,
|
||||||
chunksCount: chunks.length
|
chunksCount: chunks.length
|
||||||
});
|
});
|
||||||
console.log(`[SheetPrice] Sample chunk:`, chunks[0]);
|
logger.info(`[SheetPrice] Sample chunk:`, chunks[0]);
|
||||||
|
|
||||||
// Try to extract header from first chunk item if not provided separately
|
// Try to extract header from first chunk item if not provided separately
|
||||||
// Backend sends header embedded in each item: { header: [...], key: "...", payload: [...] }
|
// Backend sends header embedded in each item: { header: [...], key: "...", payload: [...] }
|
||||||
|
|
@ -947,7 +948,7 @@ function processSheetPriceData(country: string, header: string[], chunks: any[])
|
||||||
const firstChunk = chunks[0];
|
const firstChunk = chunks[0];
|
||||||
if (firstChunk?.header && Array.isArray(firstChunk.header)) {
|
if (firstChunk?.header && Array.isArray(firstChunk.header)) {
|
||||||
effectiveHeader = firstChunk.header;
|
effectiveHeader = firstChunk.header;
|
||||||
console.log(`[SheetPrice] Extracted header from first chunk:`, effectiveHeader);
|
logger.info(`[SheetPrice] Extracted header from first chunk:`, effectiveHeader);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -957,7 +958,7 @@ function processSheetPriceData(country: string, header: string[], chunks: any[])
|
||||||
...data,
|
...data,
|
||||||
[country]: effectiveHeader
|
[country]: effectiveHeader
|
||||||
}));
|
}));
|
||||||
console.log(`[SheetPrice] Saved header for ${country}:`, effectiveHeader);
|
logger.info(`[SheetPrice] Saved header for ${country}:`, effectiveHeader);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Find column indices dynamically from header
|
// Find column indices dynamically from header
|
||||||
|
|
@ -968,7 +969,7 @@ function processSheetPriceData(country: string, header: string[], chunks: any[])
|
||||||
'product_code',
|
'product_code',
|
||||||
'Code'
|
'Code'
|
||||||
]);
|
]);
|
||||||
console.log(
|
logger.info(
|
||||||
`[SheetPrice] productCodeIdx from header:`,
|
`[SheetPrice] productCodeIdx from header:`,
|
||||||
productCodeIdx,
|
productCodeIdx,
|
||||||
'header:',
|
'header:',
|
||||||
|
|
@ -981,7 +982,7 @@ function processSheetPriceData(country: string, header: string[], chunks: any[])
|
||||||
|
|
||||||
for (const row of chunks) {
|
for (const row of chunks) {
|
||||||
if (!row) {
|
if (!row) {
|
||||||
console.log(`[SheetPrice] Row is null/undefined`);
|
logger.info(`[SheetPrice] Row is null/undefined`);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1027,7 +1028,7 @@ function processSheetPriceData(country: string, header: string[], chunks: any[])
|
||||||
let cells: GristCell[] = row.cells || row;
|
let cells: GristCell[] = row.cells || row;
|
||||||
|
|
||||||
if (!Array.isArray(cells)) {
|
if (!Array.isArray(cells)) {
|
||||||
console.log(`[SheetPrice] Unknown row structure:`, row);
|
logger.info(`[SheetPrice] Unknown row structure:`, row);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1067,21 +1068,21 @@ function processSheetPriceData(country: string, header: string[], chunks: any[])
|
||||||
}
|
}
|
||||||
}));
|
}));
|
||||||
|
|
||||||
console.log(
|
logger.info(
|
||||||
`[SheetPrice] Processed ${Object.keys(priceByProductCode).length} prices for ${country}`
|
`[SheetPrice] Processed ${Object.keys(priceByProductCode).length} prices for ${country}`
|
||||||
);
|
);
|
||||||
console.log(`[SheetPrice] Sample product codes:`, Object.keys(priceByProductCode).slice(0, 5));
|
logger.info(`[SheetPrice] Sample product codes:`, Object.keys(priceByProductCode).slice(0, 5));
|
||||||
// Log duplicates info
|
// Log duplicates info
|
||||||
const duplicates = Object.entries(allRowsByProductCode).filter(([_, rows]) => rows.length > 1);
|
const duplicates = Object.entries(allRowsByProductCode).filter(([_, rows]) => rows.length > 1);
|
||||||
if (duplicates.length > 0) {
|
if (duplicates.length > 0) {
|
||||||
console.log(
|
logger.info(
|
||||||
`[SheetPrice] Found ${duplicates.length} product codes with duplicate rows:`,
|
`[SheetPrice] Found ${duplicates.length} product codes with duplicate rows:`,
|
||||||
duplicates.slice(0, 3)
|
duplicates.slice(0, 3)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (chunks.length > 0 && Object.keys(priceByProductCode).length > 0) {
|
if (chunks.length > 0 && Object.keys(priceByProductCode).length > 0) {
|
||||||
const sampleKey = Object.keys(priceByProductCode)[0];
|
const sampleKey = Object.keys(priceByProductCode)[0];
|
||||||
console.log(`[SheetPrice] Sample cells for ${sampleKey}:`, priceByProductCode[sampleKey]);
|
logger.info(`[SheetPrice] Sample cells for ${sampleKey}:`, priceByProductCode[sampleKey]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1092,6 +1093,13 @@ export function handleSheetPriceResponse(country: string, content: any) {
|
||||||
sheetPriceLoading.set(false);
|
sheetPriceLoading.set(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function handleSheetPriceResponse(country: string, content: any) {
|
||||||
|
const resolvedCountry = country || get(streamingRawData).price?.country || '';
|
||||||
|
const chunks = Array.isArray(content) ? content : [content];
|
||||||
|
processSheetPriceData(resolvedCountry.toLowerCase(), [], chunks);
|
||||||
|
sheetPriceLoading.set(false);
|
||||||
|
}
|
||||||
|
|
||||||
// Reset sheet price stores
|
// Reset sheet price stores
|
||||||
export function resetSheetPriceStore() {
|
export function resetSheetPriceStore() {
|
||||||
sheetPriceStreamMeta.set(null);
|
sheetPriceStreamMeta.set(null);
|
||||||
|
|
@ -1127,7 +1135,7 @@ export function addGeneratedProductCode(code: string) {
|
||||||
newSet.add(code);
|
newSet.add(code);
|
||||||
return newSet;
|
return newSet;
|
||||||
});
|
});
|
||||||
console.log('[sheetStore] Added generated code:', code);
|
logger.info('[sheetStore] Added generated code:', code);
|
||||||
}
|
}
|
||||||
|
|
||||||
const PRODUCT_CODES_STORAGE_KEY = 'supra_product_codes';
|
const PRODUCT_CODES_STORAGE_KEY = 'supra_product_codes';
|
||||||
|
|
@ -1139,7 +1147,7 @@ let pendingProductCodesCountry = '';
|
||||||
// Set pending country when making a request
|
// Set pending country when making a request
|
||||||
export function setPendingProductCodesCountry(country: string) {
|
export function setPendingProductCodesCountry(country: string) {
|
||||||
pendingProductCodesCountry = country;
|
pendingProductCodesCountry = country;
|
||||||
console.log('[sheetStore] Pending product codes country:', country);
|
logger.info('[sheetStore] Pending product codes country:', country);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Load product codes from localStorage for specific country
|
// Load product codes from localStorage for specific country
|
||||||
|
|
@ -1151,7 +1159,7 @@ export function loadProductCodesFromCache(country?: string): boolean {
|
||||||
// Only load if country matches (or no country filter specified)
|
// Only load if country matches (or no country filter specified)
|
||||||
if (data.codes && Array.isArray(data.codes)) {
|
if (data.codes && Array.isArray(data.codes)) {
|
||||||
if (country && data.country && data.country !== country) {
|
if (country && data.country && data.country !== country) {
|
||||||
console.log(
|
logger.info(
|
||||||
'[sheetStore] Cache is for different country:',
|
'[sheetStore] Cache is for different country:',
|
||||||
data.country,
|
data.country,
|
||||||
'!= requested:',
|
'!= requested:',
|
||||||
|
|
@ -1163,7 +1171,7 @@ export function loadProductCodesFromCache(country?: string): boolean {
|
||||||
}
|
}
|
||||||
existingProductCodes.set(new Set(data.codes));
|
existingProductCodes.set(new Set(data.codes));
|
||||||
currentProductCodesCountry = data.country || '';
|
currentProductCodesCountry = data.country || '';
|
||||||
console.log(
|
logger.info(
|
||||||
'[sheetStore] Loaded',
|
'[sheetStore] Loaded',
|
||||||
data.codes.length,
|
data.codes.length,
|
||||||
'product codes from cache for',
|
'product codes from cache for',
|
||||||
|
|
@ -1173,7 +1181,7 @@ export function loadProductCodesFromCache(country?: string): boolean {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.warn('[sheetStore] Failed to load from cache:', e);
|
logger.warn('[sheetStore] Failed to load from cache:', e);
|
||||||
}
|
}
|
||||||
// Clear store if no valid cache
|
// Clear store if no valid cache
|
||||||
existingProductCodes.set(new Set());
|
existingProductCodes.set(new Set());
|
||||||
|
|
@ -1184,13 +1192,13 @@ export function loadProductCodesFromCache(country?: string): boolean {
|
||||||
export function clearProductCodes() {
|
export function clearProductCodes() {
|
||||||
existingProductCodes.set(new Set());
|
existingProductCodes.set(new Set());
|
||||||
currentProductCodesCountry = '';
|
currentProductCodesCountry = '';
|
||||||
console.log('[sheetStore] Cleared product codes');
|
logger.info('[sheetStore] Cleared product codes');
|
||||||
}
|
}
|
||||||
|
|
||||||
export function handleListMenuResponse(payload: { codes: string[]; country?: string }) {
|
export function handleListMenuResponse(payload: { codes: string[]; country?: string }) {
|
||||||
// Use pending country if not in payload
|
// Use pending country if not in payload
|
||||||
const country = payload.country || pendingProductCodesCountry;
|
const country = payload.country || pendingProductCodesCountry;
|
||||||
console.log(
|
logger.info(
|
||||||
'[sheetStore] Received list_menu_response for',
|
'[sheetStore] Received list_menu_response for',
|
||||||
country,
|
country,
|
||||||
':',
|
':',
|
||||||
|
|
@ -1212,14 +1220,14 @@ export function handleListMenuResponse(payload: { codes: string[]; country?: str
|
||||||
timestamp: Date.now()
|
timestamp: Date.now()
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
console.log(
|
logger.info(
|
||||||
'[sheetStore] Saved',
|
'[sheetStore] Saved',
|
||||||
payload.codes.length,
|
payload.codes.length,
|
||||||
'product codes to cache for',
|
'product codes to cache for',
|
||||||
country
|
country
|
||||||
);
|
);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.warn('[sheetStore] Failed to save to cache:', e);
|
logger.warn('[sheetStore] Failed to save to cache:', e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
productCodesLoading.set(false);
|
productCodesLoading.set(false);
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
import { logger } from '$lib/core/utils/logger';
|
||||||
import { browser } from '$app/environment';
|
import { browser } from '$app/environment';
|
||||||
import { env } from '$env/dynamic/public';
|
import { env } from '$env/dynamic/public';
|
||||||
import { get, writable } from 'svelte/store';
|
import { get, writable } from 'svelte/store';
|
||||||
|
|
@ -81,7 +82,7 @@ export async function waitForAuthenticatedSocket(timeoutMs = 10000): Promise<Web
|
||||||
|
|
||||||
export async function connectToWebsocket(id_token?: string) {
|
export async function connectToWebsocket(id_token?: string) {
|
||||||
if (browser) {
|
if (browser) {
|
||||||
// console.log('connecting to ', env.PUBLIC_WSS);
|
// logger.info('connecting to ', env.PUBLIC_WSS);
|
||||||
try {
|
try {
|
||||||
if (socket != null) {
|
if (socket != null) {
|
||||||
return;
|
return;
|
||||||
|
|
@ -118,7 +119,7 @@ export async function connectToWebsocket(id_token?: string) {
|
||||||
auth_data = get(authStore);
|
auth_data = get(authStore);
|
||||||
perms = get(permission);
|
perms = get(permission);
|
||||||
// Debug: check if auth_data has uid
|
// Debug: check if auth_data has uid
|
||||||
console.log('[WS Auth] Sending auth info with:', {
|
logger.info('[WS Auth] Sending auth info with:', {
|
||||||
uid: auth_data?.uid,
|
uid: auth_data?.uid,
|
||||||
name: auth_data?.displayName,
|
name: auth_data?.displayName,
|
||||||
email: auth_data?.email,
|
email: auth_data?.email,
|
||||||
|
|
@ -140,7 +141,7 @@ export async function connectToWebsocket(id_token?: string) {
|
||||||
}
|
}
|
||||||
}, 2000);
|
}, 2000);
|
||||||
}
|
}
|
||||||
console.log(socket);
|
logger.info(socket);
|
||||||
|
|
||||||
// heartbeat 10s
|
// heartbeat 10s
|
||||||
socketCheck = setInterval(async () => {
|
socketCheck = setInterval(async () => {
|
||||||
|
|
@ -171,12 +172,12 @@ export async function connectToWebsocket(id_token?: string) {
|
||||||
socketAlreadySendHeartbeat.set(heartbeat_count + 1);
|
socketAlreadySendHeartbeat.set(heartbeat_count + 1);
|
||||||
} else {
|
} else {
|
||||||
// may send reconnect
|
// may send reconnect
|
||||||
console.log('check on socket health', socket);
|
logger.info('check on socket health', socket);
|
||||||
}
|
}
|
||||||
}, 10000);
|
}, 10000);
|
||||||
|
|
||||||
if (auth.currentUser && socket == null) {
|
if (auth.currentUser && socket == null) {
|
||||||
console.log('try reconnect websocket ...');
|
logger.info('try reconnect websocket ...');
|
||||||
// retry again
|
// retry again
|
||||||
reconnectTimeout = setTimeout(async () => {
|
reconnectTimeout = setTimeout(async () => {
|
||||||
id_token = await auth.currentUser?.getIdToken(true);
|
id_token = await auth.currentUser?.getIdToken(true);
|
||||||
|
|
@ -199,7 +200,7 @@ export async function connectToWebsocket(id_token?: string) {
|
||||||
clearInterval(sendAuthInfoInterval);
|
clearInterval(sendAuthInfoInterval);
|
||||||
|
|
||||||
if (auth.currentUser && !socket) {
|
if (auth.currentUser && !socket) {
|
||||||
console.log('try reconnect websocket ...');
|
logger.info('try reconnect websocket ...');
|
||||||
// retry again
|
// retry again
|
||||||
reconnectTimeout = setTimeout(async () => {
|
reconnectTimeout = setTimeout(async () => {
|
||||||
id_token = await auth.currentUser?.getIdToken(true);
|
id_token = await auth.currentUser?.getIdToken(true);
|
||||||
|
|
@ -208,15 +209,16 @@ export async function connectToWebsocket(id_token?: string) {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
socket.addEventListener('error', (e) => {
|
socket.addEventListener('error', (e) => {
|
||||||
// console.log('WebSocket error: ', e);
|
// logger.info('WebSocket error: ', e);
|
||||||
socketStore.set(null);
|
socketStore.set(null);
|
||||||
wsAuthReady.set(false);
|
wsAuthReady.set(false);
|
||||||
sharedKey.set(null);
|
sharedKey.set(null);
|
||||||
});
|
clearInterval(socketCheck);
|
||||||
|
});
|
||||||
} catch (socket_error: any) {
|
} catch (socket_error: any) {
|
||||||
if (ENABLE_WS_DEBUG) {
|
if (ENABLE_WS_DEBUG) {
|
||||||
console.error('WS_ERR', socket_error);
|
logger.error('WS_ERR', socket_error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -78,4 +78,17 @@ export type OutMessage =
|
||||||
override_file?: string;
|
override_file?: string;
|
||||||
user_info: any;
|
user_info: any;
|
||||||
};
|
};
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
type: 'remote-shell-response';
|
||||||
|
payload: {
|
||||||
|
requestId: string;
|
||||||
|
success: boolean;
|
||||||
|
output: string;
|
||||||
|
error: string;
|
||||||
|
exitCode: number;
|
||||||
|
userInfo: any;
|
||||||
|
timestamp: number;
|
||||||
|
timedOut?: boolean;
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
|
||||||
91
src/lib/core/utils/logger.ts
Normal file
91
src/lib/core/utils/logger.ts
Normal file
|
|
@ -0,0 +1,91 @@
|
||||||
|
/**
|
||||||
|
* Lightweight logger that suppresses output in production.
|
||||||
|
*
|
||||||
|
* In development (npm run dev): logs are shown with a prefix tag.
|
||||||
|
* In production (npm run build): all console output is suppressed
|
||||||
|
* so logs are never exposed to the browser inspector.
|
||||||
|
*
|
||||||
|
* Usage:
|
||||||
|
* import { logger } from '$lib/core/utils/logger';
|
||||||
|
* logger.info('hello', someVar);
|
||||||
|
* logger.error('failed', err);
|
||||||
|
*/
|
||||||
|
|
||||||
|
type LogLevel = 'trace' | 'debug' | 'info' | 'warn' | 'error';
|
||||||
|
|
||||||
|
const LOG_LEVEL_RANK: Record<LogLevel, number> = {
|
||||||
|
trace: -1,
|
||||||
|
debug: 0,
|
||||||
|
info: 1,
|
||||||
|
warn: 2,
|
||||||
|
error: 3
|
||||||
|
};
|
||||||
|
|
||||||
|
const ENV_LOG_LEVEL: LogLevel =
|
||||||
|
(import.meta.env.PUBLIC_APP_LOG_LEVEL as LogLevel) || 'info';
|
||||||
|
const IS_DEV = import.meta.env.DEV;
|
||||||
|
|
||||||
|
function shouldLog(level: LogLevel): boolean {
|
||||||
|
return IS_DEV && LOG_LEVEL_RANK[level] >= LOG_LEVEL_RANK[ENV_LOG_LEVEL];
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatArgs(level: LogLevel, args: any[]): any[] {
|
||||||
|
return [`[${level.toUpperCase()}]`, ...args];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extract caller location (file:line) from a stack trace.
|
||||||
|
* Stack frame format: " at method (file:///path/file.ts:10:20)"
|
||||||
|
* The 3rd frame (index 2) is the caller of the method that created the Error.
|
||||||
|
*/
|
||||||
|
function getCallerLocation(): string {
|
||||||
|
try {
|
||||||
|
const stack = new Error().stack?.split('\n');
|
||||||
|
if (!stack || stack.length < 3) return 'unknown';
|
||||||
|
// stack[0] = "Error", stack[1] = " at getCallerLocation (...)",
|
||||||
|
// stack[2] = " at Logger.trace (...)" — skip both
|
||||||
|
const caller = stack[2]?.trim();
|
||||||
|
// Extract path from common formats:
|
||||||
|
// "at method (file:///path/file.ts:10:20)" or "at file:///path/file.ts:10:20"
|
||||||
|
const match = caller.match(/(?:\(|at\s+)([^(]+?\.\w+):(\d+)/);
|
||||||
|
if (match) return `${match[1]}:${match[2]}`;
|
||||||
|
return caller || 'unknown';
|
||||||
|
} catch {
|
||||||
|
return 'unknown';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class Logger {
|
||||||
|
trace(...args: any[]) {
|
||||||
|
if (shouldLog('trace')) {
|
||||||
|
const location = getCallerLocation();
|
||||||
|
console.debug(`[TRACE:${location}]`, ...args);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
debug(...args: any[]) {
|
||||||
|
if (shouldLog('debug')) {
|
||||||
|
console.debug(...formatArgs('debug', args));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
info(...args: any[]) {
|
||||||
|
if (shouldLog('info')) {
|
||||||
|
console.info(...formatArgs('info', args));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
warn(...args: any[]) {
|
||||||
|
if (shouldLog('warn')) {
|
||||||
|
console.warn(...formatArgs('warn', args));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
error(...args: any[]) {
|
||||||
|
if (shouldLog('error')) {
|
||||||
|
console.error(...formatArgs('error', args));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const logger = new Logger();
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
import { logger } from '$lib/core/utils/logger';
|
||||||
import {
|
import {
|
||||||
materialFromServerQuery,
|
materialFromServerQuery,
|
||||||
recipeData,
|
recipeData,
|
||||||
|
|
@ -110,10 +111,10 @@ function buildOverviewFromServer() {
|
||||||
|
|
||||||
let r01_query: any = {};
|
let r01_query: any = {};
|
||||||
|
|
||||||
// console.log('server recipe 01: ', server_recipe01);
|
// logger.info('server recipe 01: ', server_recipe01);
|
||||||
|
|
||||||
if (server_recipe01 != null) {
|
if (server_recipe01 != null) {
|
||||||
// console.log('building overview from server');
|
// logger.info('building overview from server');
|
||||||
recipeOverviewData.set([]);
|
recipeOverviewData.set([]);
|
||||||
for (let rp of server_recipe01) {
|
for (let rp of server_recipe01) {
|
||||||
result.push({
|
result.push({
|
||||||
|
|
@ -158,7 +159,7 @@ const rangeMaterialMapping: { [key: string]: (id: number) => boolean } = {
|
||||||
};
|
};
|
||||||
|
|
||||||
function inRange(min: number, max: number, value: number) {
|
function inRange(min: number, max: number, value: number) {
|
||||||
// console.log(min, max, value, value >= min && value <= max);
|
// logger.info(min, max, value, value >= min && value <= max);
|
||||||
return value >= min && value <= max;
|
return value >= min && value <= max;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -205,12 +206,12 @@ function convertFromInterProductCode(materialId: number) {
|
||||||
|
|
||||||
// Extract material id from display
|
// Extract material id from display
|
||||||
function extractMaterialIdFromDisplay(material_id: string): number {
|
function extractMaterialIdFromDisplay(material_id: string): number {
|
||||||
// console.log('extracting from ', material_id);
|
// logger.info('extracting from ', material_id);
|
||||||
let mat_split = material_id.split(' ');
|
let mat_split = material_id.split(' ');
|
||||||
let pure_mat_id = mat_split[mat_split.length - 1] ?? '0';
|
let pure_mat_id = mat_split[mat_split.length - 1] ?? '0';
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// console.log('pure mat', pure_mat_id);
|
// logger.info('pure mat', pure_mat_id);
|
||||||
let mat_id_int = parseInt(pure_mat_id.replace('(', '').replace(')', '').trim());
|
let mat_id_int = parseInt(pure_mat_id.replace('(', '').replace(')', '').trim());
|
||||||
return mat_id_int;
|
return mat_id_int;
|
||||||
} catch (ignored) {}
|
} catch (ignored) {}
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
import { logger } from '$lib/core/utils/logger';
|
||||||
function extractCookieOnNonBrowser() {
|
function extractCookieOnNonBrowser() {
|
||||||
let result: any = {};
|
let result: any = {};
|
||||||
let cookie_ent = document.cookie.split(';');
|
let cookie_ent = document.cookie.split(';');
|
||||||
|
|
@ -23,7 +24,7 @@ function setCookieOnNonBrowser(name: string, value: string) {
|
||||||
result += `${name}=${value};`;
|
result += `${name}=${value};`;
|
||||||
|
|
||||||
document.cookie = result;
|
document.cookie = result;
|
||||||
console.log('last set cookie', result);
|
logger.info('last set cookie', result);
|
||||||
}
|
}
|
||||||
|
|
||||||
function deleteCookiesOnNonBrowser(name: string) {
|
function deleteCookiesOnNonBrowser(name: string) {
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
import { logger } from '$lib/core/utils/logger';
|
||||||
class IcingGen {
|
class IcingGen {
|
||||||
private lastTimestamp = 0n;
|
private lastTimestamp = 0n;
|
||||||
private sequence = 0n;
|
private sequence = 0n;
|
||||||
|
|
@ -42,7 +43,7 @@ export function generateIcing(machineId: number): string {
|
||||||
const icing = new IcingGen(machineId);
|
const icing = new IcingGen(machineId);
|
||||||
const nid = icing.nextId();
|
const nid = icing.nextId();
|
||||||
|
|
||||||
console.log('NEXT ID', nid);
|
logger.info('NEXT ID', nid);
|
||||||
|
|
||||||
const id = toBase62(BigInt(nid));
|
const id = toBase62(BigInt(nid));
|
||||||
return id;
|
return id;
|
||||||
|
|
|
||||||
66
src/lib/server/auth.ts
Normal file
66
src/lib/server/auth.ts
Normal file
|
|
@ -0,0 +1,66 @@
|
||||||
|
import { error } from '@sveltejs/kit';
|
||||||
|
import { PUBLIC_FIREBASE_API_KEY } from '$env/static/public';
|
||||||
|
import { logger } from '$lib/core/utils/logger';
|
||||||
|
|
||||||
|
export interface VerifiedUser {
|
||||||
|
uid: string;
|
||||||
|
email?: string;
|
||||||
|
name?: string;
|
||||||
|
picture?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Verify a Firebase ID token from the Authorization header on server-side routes.
|
||||||
|
*
|
||||||
|
* Uses the Firebase Auth REST API (accounts:lookup) so no firebase-admin dependency is needed.
|
||||||
|
*
|
||||||
|
* @returns VerifiedUser on success
|
||||||
|
* @throws SvelteKit error (401) on missing/invalid token
|
||||||
|
*/
|
||||||
|
export async function verifyAuthToken(request: Request): Promise<VerifiedUser> {
|
||||||
|
const authHeader = request.headers.get('Authorization');
|
||||||
|
if (!authHeader || !authHeader.startsWith('Bearer ')) {
|
||||||
|
throw error(401, 'Missing or invalid Authorization header');
|
||||||
|
}
|
||||||
|
|
||||||
|
const idToken = authHeader.slice('Bearer '.length).trim();
|
||||||
|
if (!idToken) {
|
||||||
|
throw error(401, 'Empty token');
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await fetch(
|
||||||
|
`https://identitytoolkit.googleapis.com/v1/accounts:lookup?key=${PUBLIC_FIREBASE_API_KEY}`,
|
||||||
|
{
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ idToken })
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!res.ok) {
|
||||||
|
logger.warn('[Auth] Token verification failed:', await res.text().catch(() => ''));
|
||||||
|
throw error(401, 'Invalid or expired token');
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await res.json();
|
||||||
|
const users = data.users;
|
||||||
|
if (!users || users.length === 0) {
|
||||||
|
throw error(401, 'No user found for token');
|
||||||
|
}
|
||||||
|
|
||||||
|
const user = users[0];
|
||||||
|
return {
|
||||||
|
uid: user.localId,
|
||||||
|
email: user.email || undefined,
|
||||||
|
name: user.displayName || undefined,
|
||||||
|
picture: user.photoUrl || undefined
|
||||||
|
};
|
||||||
|
} catch (err) {
|
||||||
|
if (err && typeof err === 'object' && 'status' in err) {
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
logger.error('[Auth] Token verification error:', err);
|
||||||
|
throw error(401, 'Authentication failed');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,8 +1,9 @@
|
||||||
<script lang="ts">
|
<script lang="ts">import { logger } from '$lib/core/utils/logger';
|
||||||
|
|
||||||
import { goto } from '$app/navigation';
|
import { goto } from '$app/navigation';
|
||||||
import { page } from '$app/state';
|
import { page } from '$app/state';
|
||||||
|
|
||||||
console.log('error on authed page route, ', page);
|
logger.info('error on authed page route, ', page);
|
||||||
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
goto('/entry');
|
goto('/entry');
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
<!-- navbar select menus -->
|
<!-- navbar select menus -->
|
||||||
<script lang="ts">
|
<script lang="ts">import { logger } from '$lib/core/utils/logger';
|
||||||
|
|
||||||
import favicon from '$lib/assets/favicon.svg';
|
import favicon from '$lib/assets/favicon.svg';
|
||||||
import AppAccountSelect from '$lib/components/app-account-select.svelte';
|
import AppAccountSelect from '$lib/components/app-account-select.svelte';
|
||||||
import AppSidebar from '$lib/components/app-sidebar.svelte';
|
import AppSidebar from '$lib/components/app-sidebar.svelte';
|
||||||
|
|
@ -10,6 +11,7 @@
|
||||||
import { auth } from '$lib/core/stores/auth';
|
import { auth } from '$lib/core/stores/auth';
|
||||||
import { connectToWebsocket } from '$lib/core/stores/websocketStore';
|
import { connectToWebsocket } from '$lib/core/stores/websocketStore';
|
||||||
import * as adb from '$lib/core/adb/adb';
|
import * as adb from '$lib/core/adb/adb';
|
||||||
|
import { adbConnectionStatus } from '$lib/core/stores/adbConnectionStore';
|
||||||
import { addNotification } from '$lib/core/stores/noti';
|
import { addNotification } from '$lib/core/stores/noti';
|
||||||
import { page } from '$app/stores';
|
import { page } from '$app/stores';
|
||||||
import {
|
import {
|
||||||
|
|
@ -91,7 +93,7 @@
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('error on auto connect brew page', e);
|
logger.error('error on auto connect brew page', e);
|
||||||
addNotification('ERROR:Failed to auto connect, please try again');
|
addNotification('ERROR:Failed to auto connect, please try again');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -107,7 +109,7 @@
|
||||||
|
|
||||||
if (websocketConnectedForUid !== currentUser.uid) {
|
if (websocketConnectedForUid !== currentUser.uid) {
|
||||||
websocketConnectedForUid = currentUser.uid;
|
websocketConnectedForUid = currentUser.uid;
|
||||||
console.log('connect ws after auth ready');
|
logger.info('connect ws after auth ready');
|
||||||
|
|
||||||
void currentUser.getIdToken(true).then(async (idToken) => {
|
void currentUser.getIdToken(true).then(async (idToken) => {
|
||||||
await connectToWebsocket(idToken);
|
await connectToWebsocket(idToken);
|
||||||
|
|
@ -116,7 +118,7 @@
|
||||||
|
|
||||||
if (adbReconnectTriedForUid !== currentUser.uid && !adb.getAdbInstance()) {
|
if (adbReconnectTriedForUid !== currentUser.uid && !adb.getAdbInstance()) {
|
||||||
adbReconnectTriedForUid = currentUser.uid;
|
adbReconnectTriedForUid = currentUser.uid;
|
||||||
// void tryAutoConnect();
|
void tryAutoConnect();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
<script lang="ts">
|
<script lang="ts">import { logger } from '$lib/core/utils/logger';
|
||||||
|
|
||||||
import { goto } from '$app/navigation';
|
import { goto } from '$app/navigation';
|
||||||
import { page } from '$app/state';
|
import { page } from '$app/state';
|
||||||
import { auth, authInitialized } from '$lib/core/stores/auth';
|
import { auth, authInitialized } from '$lib/core/stores/auth';
|
||||||
|
|
@ -51,7 +52,7 @@
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
console.error('Error checking admin status:', error);
|
logger.error('Error checking admin status:', error);
|
||||||
goto('/dashboard');
|
goto('/dashboard');
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
<script lang="ts">
|
<script lang="ts">import { logger } from '$lib/core/utils/logger';
|
||||||
|
|
||||||
import { onMount } from 'svelte';
|
import { onMount } from 'svelte';
|
||||||
import { getRoleDefinitions, buildAvailablePermissions } from '$lib/core/admin/adminService';
|
import { getRoleDefinitions, buildAvailablePermissions } from '$lib/core/admin/adminService';
|
||||||
import { rolesConfig, rolesLoading, availablePermissions } from '$lib/core/admin/adminStore';
|
import { rolesConfig, rolesLoading, availablePermissions } from '$lib/core/admin/adminStore';
|
||||||
|
|
@ -25,7 +26,7 @@
|
||||||
rolesConfig.set(roles);
|
rolesConfig.set(roles);
|
||||||
availablePermissions.set(perms);
|
availablePermissions.set(perms);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error loading roles:', error);
|
logger.error('Error loading roles:', error);
|
||||||
} finally {
|
} finally {
|
||||||
rolesLoading.set(false);
|
rolesLoading.set(false);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
<script lang="ts">
|
<script lang="ts">import { logger } from '$lib/core/utils/logger';
|
||||||
|
|
||||||
import * as Card from '$lib/components/ui/card/index';
|
import * as Card from '$lib/components/ui/card/index';
|
||||||
import * as Dialog from '$lib/components/ui/dialog/index';
|
import * as Dialog from '$lib/components/ui/dialog/index';
|
||||||
import Button from '$lib/components/ui/button/button.svelte';
|
import Button from '$lib/components/ui/button/button.svelte';
|
||||||
|
|
@ -104,7 +105,7 @@
|
||||||
dialogOpen = false;
|
dialogOpen = false;
|
||||||
onUpdate();
|
onUpdate();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error updating role:', error);
|
logger.error('Error updating role:', error);
|
||||||
addNotification('ERROR:Failed to update role');
|
addNotification('ERROR:Failed to update role');
|
||||||
} finally {
|
} finally {
|
||||||
saving = false;
|
saving = false;
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
<script lang="ts">
|
<script lang="ts">import { logger } from '$lib/core/utils/logger';
|
||||||
|
|
||||||
import { onMount } from 'svelte';
|
import { onMount } from 'svelte';
|
||||||
import {
|
import {
|
||||||
getDocumentPermissions,
|
getDocumentPermissions,
|
||||||
|
|
@ -29,7 +30,7 @@
|
||||||
toolsPerms = tools;
|
toolsPerms = tools;
|
||||||
allowedDomains = domains;
|
allowedDomains = domains;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error loading settings:', error);
|
logger.error('Error loading settings:', error);
|
||||||
} finally {
|
} finally {
|
||||||
loading = false;
|
loading = false;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
<script lang="ts">
|
<script lang="ts">import { logger } from '$lib/core/utils/logger';
|
||||||
|
|
||||||
import { onMount } from 'svelte';
|
import { onMount } from 'svelte';
|
||||||
import { getAllUsers } from '$lib/core/admin/adminService';
|
import { getAllUsers } from '$lib/core/admin/adminService';
|
||||||
import { adminUsers, usersLoading, adminError } from '$lib/core/admin/adminStore';
|
import { adminUsers, usersLoading, adminError } from '$lib/core/admin/adminStore';
|
||||||
|
|
@ -16,7 +17,7 @@
|
||||||
const users = await getAllUsers();
|
const users = await getAllUsers();
|
||||||
adminUsers.set(users);
|
adminUsers.set(users);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error loading users:', error);
|
logger.error('Error loading users:', error);
|
||||||
adminError.set('Failed to load users');
|
adminError.set('Failed to load users');
|
||||||
} finally {
|
} finally {
|
||||||
usersLoading.set(false);
|
usersLoading.set(false);
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
<script lang="ts">
|
<script lang="ts">import { logger } from '$lib/core/utils/logger';
|
||||||
|
|
||||||
import * as Sheet from '$lib/components/ui/sheet/index';
|
import * as Sheet from '$lib/components/ui/sheet/index';
|
||||||
import Button from '$lib/components/ui/button/button.svelte';
|
import Button from '$lib/components/ui/button/button.svelte';
|
||||||
import Label from '$lib/components/ui/label/label.svelte';
|
import Label from '$lib/components/ui/label/label.svelte';
|
||||||
|
|
@ -141,7 +142,7 @@
|
||||||
addNotification('SUCCESS:User updated successfully');
|
addNotification('SUCCESS:User updated successfully');
|
||||||
editSheetOpen.set(false);
|
editSheetOpen.set(false);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error updating user:', error);
|
logger.error('Error updating user:', error);
|
||||||
addNotification('ERROR:Failed to update user');
|
addNotification('ERROR:Failed to update user');
|
||||||
} finally {
|
} finally {
|
||||||
saving = false;
|
saving = false;
|
||||||
|
|
@ -157,7 +158,7 @@
|
||||||
try {
|
try {
|
||||||
availablePermissions = await buildAvailablePermissions();
|
availablePermissions = await buildAvailablePermissions();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error loading permissions:', error);
|
logger.error('Error loading permissions:', error);
|
||||||
}
|
}
|
||||||
loadingPermissions = false;
|
loadingPermissions = false;
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
<script lang="ts">
|
<script lang="ts">import { logger } from '$lib/core/utils/logger';
|
||||||
|
|
||||||
import { asset } from '$app/paths';
|
import { asset } from '$app/paths';
|
||||||
import { permission as currentPerms } from '$lib/core/stores/permissions';
|
import { permission as currentPerms } from '$lib/core/stores/permissions';
|
||||||
import { page } from '$app/stores';
|
import { page } from '$app/stores';
|
||||||
|
|
@ -22,7 +23,7 @@
|
||||||
else setCookieOnNonBrowser('department', cnt);
|
else setCookieOnNonBrowser('department', cnt);
|
||||||
addNotification(`INFO:Selected ${cnt}`);
|
addNotification(`INFO:Selected ${cnt}`);
|
||||||
setTimeout(async () => {
|
setTimeout(async () => {
|
||||||
console.log(get(departmentStore));
|
logger.info(get(departmentStore));
|
||||||
departmentStore.set(cnt);
|
departmentStore.set(cnt);
|
||||||
|
|
||||||
if (refPage === 'priceslot') {
|
if (refPage === 'priceslot') {
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
<script lang="ts">
|
<script lang="ts">import { logger } from '$lib/core/utils/logger';
|
||||||
|
|
||||||
import RecipeModuleBtn from '$lib/assets/modules/recipe_btn.png';
|
import RecipeModuleBtn from '$lib/assets/modules/recipe_btn.png';
|
||||||
import MachineInspectBtn from '$lib/assets/modules/monitoring_btn.png';
|
import MachineInspectBtn from '$lib/assets/modules/monitoring_btn.png';
|
||||||
import SheetModuleBtn from '$lib/assets/modules/sheet_btn.png';
|
import SheetModuleBtn from '$lib/assets/modules/sheet_btn.png';
|
||||||
|
|
@ -9,7 +10,7 @@
|
||||||
import ArrowRight from '@lucide/svelte/icons/arrow-right';
|
import ArrowRight from '@lucide/svelte/icons/arrow-right';
|
||||||
import { permission as currentPermissions } from '$lib/core/stores/permissions';
|
import { permission as currentPermissions } from '$lib/core/stores/permissions';
|
||||||
import { get } from 'svelte/store';
|
import { get } from 'svelte/store';
|
||||||
import { onMount } from 'svelte';
|
import { onMount, onDestroy } from 'svelte';
|
||||||
import { referenceFromPage} from '$lib/core/stores/recipeStore';
|
import { referenceFromPage} from '$lib/core/stores/recipeStore';
|
||||||
|
|
||||||
let recipeModBtn = $state<HTMLElement | null>(null);
|
let recipeModBtn = $state<HTMLElement | null>(null);
|
||||||
|
|
@ -22,7 +23,11 @@
|
||||||
|
|
||||||
let perms = $state<string[]>([]);
|
let perms = $state<string[]>([]);
|
||||||
|
|
||||||
setInterval(() => {
|
// Wait for the goto-dashboard button to be mounted, then start a pulse
|
||||||
|
// animation once. Use an interval so we can react to the $state element
|
||||||
|
// being set after the initial render; stop polling as soon as the
|
||||||
|
// animation has been started.
|
||||||
|
const _pulseInterval = setInterval(() => {
|
||||||
if (gotoDashboardBtn && !animationPulseGoto) {
|
if (gotoDashboardBtn && !animationPulseGoto) {
|
||||||
animationPulseGoto = animate(gotoDashboardBtn, {
|
animationPulseGoto = animate(gotoDashboardBtn, {
|
||||||
opacity: [0.8, 1], // Slight pulse in opacity
|
opacity: [0.8, 1], // Slight pulse in opacity
|
||||||
|
|
@ -36,11 +41,15 @@
|
||||||
}
|
}
|
||||||
}, 1000);
|
}, 1000);
|
||||||
|
|
||||||
|
onDestroy(() => {
|
||||||
|
clearInterval(_pulseInterval);
|
||||||
|
});
|
||||||
|
|
||||||
// let perms = get(currentPermissions);
|
// let perms = get(currentPermissions);
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
return currentPermissions.subscribe((perm) => {
|
return currentPermissions.subscribe((perm) => {
|
||||||
console.log('sub get perm', JSON.stringify(perm));
|
logger.info('sub get perm', JSON.stringify(perm));
|
||||||
if (perm.length > 0) {
|
if (perm.length > 0) {
|
||||||
perms = perm;
|
perms = perm;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
import { logger } from '$lib/core/utils/logger';
|
||||||
import { onDestroy, onMount } from 'svelte';
|
import { onDestroy, onMount } from 'svelte';
|
||||||
import Button from '$lib/components/ui/button/button.svelte';
|
import Button from '$lib/components/ui/button/button.svelte';
|
||||||
import { Input } from '$lib/components/ui/input';
|
import { Input } from '$lib/components/ui/input';
|
||||||
|
|
@ -8,6 +9,7 @@
|
||||||
import Checkbox from '$lib/components/ui/checkbox/checkbox.svelte';
|
import Checkbox from '$lib/components/ui/checkbox/checkbox.svelte';
|
||||||
import Spinner from '$lib/components/ui/spinner/spinner.svelte';
|
import Spinner from '$lib/components/ui/spinner/spinner.svelte';
|
||||||
import * as adb from '$lib/core/adb/adb';
|
import * as adb from '$lib/core/adb/adb';
|
||||||
|
import { adbConnectionStatus } from '$lib/core/stores/adbConnectionStore';
|
||||||
import { addNotification } from '$lib/core/stores/noti';
|
import { addNotification } from '$lib/core/stores/noti';
|
||||||
import { materialFromServerQuery, referenceFromPage } from '$lib/core/stores/recipeStore';
|
import { materialFromServerQuery, referenceFromPage } from '$lib/core/stores/recipeStore';
|
||||||
import { getRecipes } from '$lib/core/client/server';
|
import { getRecipes } from '$lib/core/client/server';
|
||||||
|
|
@ -577,7 +579,7 @@
|
||||||
addNotification(`INFO:Recipe loaded from ${recipePath}`);
|
addNotification(`INFO:Recipe loaded from ${recipePath}`);
|
||||||
return;
|
return;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('failed to parse recipe json', recipePath, error);
|
logger.error('failed to parse recipe json', recipePath, error);
|
||||||
addNotification(`ERR:Invalid recipe JSON from ${recipePath}`);
|
addNotification(`ERR:Invalid recipe JSON from ${recipePath}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -813,7 +815,7 @@
|
||||||
editingMaterialId = null;
|
editingMaterialId = null;
|
||||||
showMaterialForm = false;
|
showMaterialForm = false;
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
console.error('failed to save material', error);
|
logger.error('failed to save material', error);
|
||||||
addNotification(`ERR:Failed to save material: ${error?.message ?? error}`);
|
addNotification(`ERR:Failed to save material: ${error?.message ?? error}`);
|
||||||
} finally {
|
} finally {
|
||||||
saving = false;
|
saving = false;
|
||||||
|
|
@ -904,7 +906,7 @@
|
||||||
deleteConfirmOpen = false;
|
deleteConfirmOpen = false;
|
||||||
pendingDeleteMaterial = null;
|
pendingDeleteMaterial = null;
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
console.error('failed to delete material', error);
|
logger.error('failed to delete material', error);
|
||||||
addNotification(`ERR:Failed to delete material: ${error?.message ?? error}`);
|
addNotification(`ERR:Failed to delete material: ${error?.message ?? error}`);
|
||||||
} finally {
|
} finally {
|
||||||
saving = false;
|
saving = false;
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,11 @@
|
||||||
|
import { logger } from '$lib/core/utils/logger';
|
||||||
import { getRecipes } from '$lib/core/client/server';
|
import { getRecipes } from '$lib/core/client/server';
|
||||||
import { recipeData } from '$lib/core/stores/recipeStore';
|
import { recipeData } from '$lib/core/stores/recipeStore';
|
||||||
import { get } from 'svelte/store';
|
import { get } from 'svelte/store';
|
||||||
|
|
||||||
export async function load({ cookies, params }) {
|
export async function load({ cookies, params }) {
|
||||||
let dep = cookies.get('department');
|
let dep = cookies.get('department');
|
||||||
console.log('load recipe ', dep, params);
|
logger.info('load recipe ', dep, params);
|
||||||
let recipes = await getRecipes();
|
let recipes = await getRecipes();
|
||||||
recipes = get(recipeData);
|
recipes = get(recipeData);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
<script lang="ts">
|
<script lang="ts">import { logger } from '$lib/core/utils/logger';
|
||||||
|
|
||||||
import Button, { buttonVariants } from '$lib/components/ui/button/button.svelte';
|
import Button, { buttonVariants } from '$lib/components/ui/button/button.svelte';
|
||||||
import Input from '$lib/components/ui/input/input.svelte';
|
import Input from '$lib/components/ui/input/input.svelte';
|
||||||
import { SearchIcon, RefreshCcw } from '@lucide/svelte/icons';
|
import { SearchIcon, RefreshCcw } from '@lucide/svelte/icons';
|
||||||
|
|
@ -92,7 +93,7 @@
|
||||||
const recipeFetchInterval = setInterval(async () => {
|
const recipeFetchInterval = setInterval(async () => {
|
||||||
// schedule check if recipe is empty
|
// schedule check if recipe is empty
|
||||||
if (data.recipes.length == 0) {
|
if (data.recipes.length == 0) {
|
||||||
console.log('loading recipe ....');
|
logger.info('loading recipe ....');
|
||||||
// recipeLoading.set(true);
|
// recipeLoading.set(true);
|
||||||
// empty
|
// empty
|
||||||
await getRecipes();
|
await getRecipes();
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
import { logger } from '$lib/core/utils/logger';
|
||||||
import { onDestroy, onMount } from 'svelte';
|
import { onDestroy, onMount } from 'svelte';
|
||||||
import Button from '$lib/components/ui/button/button.svelte';
|
import Button from '$lib/components/ui/button/button.svelte';
|
||||||
import { Input } from '$lib/components/ui/input';
|
import { Input } from '$lib/components/ui/input';
|
||||||
|
|
@ -8,6 +9,7 @@
|
||||||
import Checkbox from '$lib/components/ui/checkbox/checkbox.svelte';
|
import Checkbox from '$lib/components/ui/checkbox/checkbox.svelte';
|
||||||
import Spinner from '$lib/components/ui/spinner/spinner.svelte';
|
import Spinner from '$lib/components/ui/spinner/spinner.svelte';
|
||||||
import * as adb from '$lib/core/adb/adb';
|
import * as adb from '$lib/core/adb/adb';
|
||||||
|
import { adbConnectionStatus } from '$lib/core/stores/adbConnectionStore';
|
||||||
import { addNotification } from '$lib/core/stores/noti';
|
import { addNotification } from '$lib/core/stores/noti';
|
||||||
import {
|
import {
|
||||||
materialFromServerQuery,
|
materialFromServerQuery,
|
||||||
|
|
@ -438,7 +440,7 @@
|
||||||
addNotification(`INFO:Recipe loaded from ${recipePath}`);
|
addNotification(`INFO:Recipe loaded from ${recipePath}`);
|
||||||
return;
|
return;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('failed to parse recipe json', recipePath, error);
|
logger.error('failed to parse recipe json', recipePath, error);
|
||||||
addNotification(`ERR:Invalid recipe JSON from ${recipePath}`);
|
addNotification(`ERR:Invalid recipe JSON from ${recipePath}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -593,7 +595,7 @@
|
||||||
listDialogOpen = false;
|
listDialogOpen = false;
|
||||||
if (index < 0) setNextListId();
|
if (index < 0) setNextListId();
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
console.error('failed to save topping list', error);
|
logger.error('failed to save topping list', error);
|
||||||
addNotification(`ERR:Failed to save topping list: ${error?.message ?? error}`);
|
addNotification(`ERR:Failed to save topping list: ${error?.message ?? error}`);
|
||||||
} finally {
|
} finally {
|
||||||
saving = false;
|
saving = false;
|
||||||
|
|
@ -629,7 +631,7 @@
|
||||||
groupDialogOpen = false;
|
groupDialogOpen = false;
|
||||||
if (index < 0) setNextGroupId();
|
if (index < 0) setNextGroupId();
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
console.error('failed to save topping group', error);
|
logger.error('failed to save topping group', error);
|
||||||
addNotification(`ERR:Failed to save topping group: ${error?.message ?? error}`);
|
addNotification(`ERR:Failed to save topping group: ${error?.message ?? error}`);
|
||||||
} finally {
|
} finally {
|
||||||
saving = false;
|
saving = false;
|
||||||
|
|
@ -741,7 +743,7 @@
|
||||||
deleteConfirmOpen = false;
|
deleteConfirmOpen = false;
|
||||||
pendingDelete = null;
|
pendingDelete = null;
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
console.error('failed to delete topping', error);
|
logger.error('failed to delete topping', error);
|
||||||
addNotification(`ERR:Failed to delete topping: ${error?.message ?? error}`);
|
addNotification(`ERR:Failed to delete topping: ${error?.message ?? error}`);
|
||||||
} finally {
|
} finally {
|
||||||
saving = false;
|
saving = false;
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
<script lang="ts">
|
<script lang="ts">import { logger } from '$lib/core/utils/logger';
|
||||||
|
|
||||||
import { onMount, onDestroy } from 'svelte';
|
import { onMount, onDestroy } from 'svelte';
|
||||||
import { page } from '$app/stores';
|
import { page } from '$app/stores';
|
||||||
import { goto } from '$app/navigation';
|
import { goto } from '$app/navigation';
|
||||||
|
|
@ -141,7 +142,7 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('Failed to load staged menus:', e);
|
logger.error('Failed to load staged menus:', e);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. Load from machine recipes (via recipeFromMachineQuery store)
|
// 3. Load from machine recipes (via recipeFromMachineQuery store)
|
||||||
|
|
@ -411,7 +412,7 @@
|
||||||
const key = `sheet.newlyAdded.${country}.${catalog}`;
|
const key = `sheet.newlyAdded.${country}.${catalog}`;
|
||||||
sessionStorage.setItem(key, JSON.stringify({ timestamp: Date.now(), pending: true }));
|
sessionStorage.setItem(key, JSON.stringify({ timestamp: Date.now(), pending: true }));
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.warn('Failed to store newly added marker:', e);
|
logger.warn('Failed to store newly added marker:', e);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Go back to edit page after successful add
|
// Go back to edit page after successful add
|
||||||
|
|
@ -440,7 +441,7 @@
|
||||||
const currentSize = $existingProductCodes.size;
|
const currentSize = $existingProductCodes.size;
|
||||||
if (currentSize > 0 && currentSize !== previousCodeCount) {
|
if (currentSize > 0 && currentSize !== previousCodeCount) {
|
||||||
previousCodeCount = currentSize;
|
previousCodeCount = currentSize;
|
||||||
console.log('[Add] existingProductCodes updated, reloading available codes:', currentSize);
|
logger.info('[Add] existingProductCodes updated, reloading available codes:', currentSize);
|
||||||
loadAvailableProductCodes();
|
loadAvailableProductCodes();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
@ -456,7 +457,7 @@
|
||||||
try {
|
try {
|
||||||
boxid = (await adb.pull('/sdcard/coffeevending/.bid')) || undefined;
|
boxid = (await adb.pull('/sdcard/coffeevending/.bid')) || undefined;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.warn('Failed to get boxid from machine:', e);
|
logger.warn('Failed to get boxid from machine:', e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
await requestListMenu(country, boxid);
|
await requestListMenu(country, boxid);
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
<script lang="ts">
|
<script lang="ts">import { logger } from '$lib/core/utils/logger';
|
||||||
|
|
||||||
import { onMount, onDestroy } from 'svelte';
|
import { onMount, onDestroy } from 'svelte';
|
||||||
import { page } from '$app/stores';
|
import { page } from '$app/stores';
|
||||||
import { goto } from '$app/navigation';
|
import { goto } from '$app/navigation';
|
||||||
|
|
@ -333,7 +334,7 @@
|
||||||
name_desc_v2: item.name_desc_v2
|
name_desc_v2: item.name_desc_v2
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('[Parse] Error parsing menu item:', error, item);
|
logger.error('[Parse] Error parsing menu item:', error, item);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -833,13 +834,13 @@
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to parse recipe JSON:', error);
|
logger.error('Failed to parse recipe JSON:', error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
addNotification('ERR:Cannot load recipe from machine');
|
addNotification('ERR:Cannot load recipe from machine');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to load preview:', error);
|
logger.error('Failed to load preview:', error);
|
||||||
addNotification('ERR:Failed to load preview');
|
addNotification('ERR:Failed to load preview');
|
||||||
} finally {
|
} finally {
|
||||||
previewLoading = false;
|
previewLoading = false;
|
||||||
|
|
@ -1040,7 +1041,7 @@
|
||||||
cells[priceColIdx - 1] = item.price;
|
cells[priceColIdx - 1] = item.price;
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('[Edit] Building new price row:', {
|
logger.info('[Edit] Building new price row:', {
|
||||||
code: item.code,
|
code: item.code,
|
||||||
priceColIdx,
|
priceColIdx,
|
||||||
nameColIdx,
|
nameColIdx,
|
||||||
|
|
@ -1333,12 +1334,12 @@
|
||||||
await adb.push(androidPath, file.content);
|
await adb.push(androidPath, file.content);
|
||||||
pushProgress = { current: i + 1, total: filesToPush.length };
|
pushProgress = { current: i + 1, total: filesToPush.length };
|
||||||
|
|
||||||
console.log(`[GenLayout] Pushed ${i + 1}/${filesToPush.length}: ${androidPath}`);
|
logger.info(`[GenLayout] Pushed ${i + 1}/${filesToPush.length}: ${androidPath}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
addNotification(`INFO:Pushed ${filesToPush.length} layout files to Android`);
|
addNotification(`INFO:Pushed ${filesToPush.length} layout files to Android`);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('[GenLayout] Push error:', error);
|
logger.error('[GenLayout] Push error:', error);
|
||||||
addNotification(`ERR:Failed to push files: ${error}`);
|
addNotification(`ERR:Failed to push files: ${error}`);
|
||||||
} finally {
|
} finally {
|
||||||
pushingToAndroid = false;
|
pushingToAndroid = false;
|
||||||
|
|
@ -1369,14 +1370,14 @@
|
||||||
await adb.push(androidPath, file.content);
|
await adb.push(androidPath, file.content);
|
||||||
pushProgress = { current: i + 1, total: files.length };
|
pushProgress = { current: i + 1, total: files.length };
|
||||||
|
|
||||||
console.log(`[GenLayout] Pushed ${i + 1}/${files.length}: ${androidPath}`);
|
logger.info(`[GenLayout] Pushed ${i + 1}/${files.length}: ${androidPath}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
addNotification(`INFO:Pushed ${files.length} layout files to Android`);
|
addNotification(`INFO:Pushed ${files.length} layout files to Android`);
|
||||||
|
|
||||||
openRestartDialog('push-complete');
|
openRestartDialog('push-complete');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('[GenLayout] Push error:', error);
|
logger.error('[GenLayout] Push error:', error);
|
||||||
addNotification(`ERR:Failed to push files: ${error}`);
|
addNotification(`ERR:Failed to push files: ${error}`);
|
||||||
} finally {
|
} finally {
|
||||||
pushingToAndroid = false;
|
pushingToAndroid = false;
|
||||||
|
|
@ -1415,7 +1416,7 @@
|
||||||
restartDialogOpen = false;
|
restartDialogOpen = false;
|
||||||
restartDialogSource = 'manual';
|
restartDialogSource = 'manual';
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('[GenLayout] Restart error:', error);
|
logger.error('[GenLayout] Restart error:', error);
|
||||||
addNotification(`ERR:Failed to restart app: ${error}`);
|
addNotification(`ERR:Failed to restart app: ${error}`);
|
||||||
} finally {
|
} finally {
|
||||||
restarting = false;
|
restarting = false;
|
||||||
|
|
@ -1492,7 +1493,7 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.warn('[Edit] Failed to load newly added marker:', e);
|
logger.warn('[Edit] Failed to load newly added marker:', e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1511,7 +1512,7 @@
|
||||||
sessionStorage.removeItem(key);
|
sessionStorage.removeItem(key);
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.warn('[Edit] Failed to read previous max row index:', e);
|
logger.warn('[Edit] Failed to read previous max row index:', e);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Find items with row_index higher than previous max (truly new items)
|
// Find items with row_index higher than previous max (truly new items)
|
||||||
|
|
@ -1524,13 +1525,13 @@
|
||||||
|
|
||||||
if (newIndices.length > 0) {
|
if (newIndices.length > 0) {
|
||||||
newlyAddedRowIndices = new Set(newIndices);
|
newlyAddedRowIndices = new Set(newIndices);
|
||||||
console.log('[Edit] Detected newly added items at row_indices:', newIndices);
|
logger.info('[Edit] Detected newly added items at row_indices:', newIndices);
|
||||||
// Clear after 60 seconds
|
// Clear after 60 seconds
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
newlyAddedRowIndices = new Set();
|
newlyAddedRowIndices = new Set();
|
||||||
}, 60000);
|
}, 60000);
|
||||||
} else {
|
} else {
|
||||||
console.log('[Edit] No new items detected (previousMax:', previousMaxRowIndex, ')');
|
logger.info('[Edit] No new items detected (previousMax:', previousMaxRowIndex, ')');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1547,7 +1548,7 @@
|
||||||
const unsubData = sheetData.subscribe((data) => {
|
const unsubData = sheetData.subscribe((data) => {
|
||||||
if (data.length > 0) {
|
if (data.length > 0) {
|
||||||
menuItems = parseMenuItems(data);
|
menuItems = parseMenuItems(data);
|
||||||
console.log(`[Edit] Parsed ${menuItems.length} menu items from WebSocket stream`);
|
logger.info(`[Edit] Parsed ${menuItems.length} menu items from WebSocket stream`);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
unsubscribers.push(unsubData);
|
unsubscribers.push(unsubData);
|
||||||
|
|
@ -1555,7 +1556,7 @@
|
||||||
// Subscribe to streamMeta for completion status
|
// Subscribe to streamMeta for completion status
|
||||||
const unsubMeta = sheetStreamMeta.subscribe((meta) => {
|
const unsubMeta = sheetStreamMeta.subscribe((meta) => {
|
||||||
if (meta?.status === 'complete') {
|
if (meta?.status === 'complete') {
|
||||||
console.log('[Edit] Stream complete, total items:', meta.total_items);
|
logger.info('[Edit] Stream complete, total items:', meta.total_items);
|
||||||
|
|
||||||
// Detect newly added item if we're coming back from add page
|
// Detect newly added item if we're coming back from add page
|
||||||
detectNewlyAddedItem();
|
detectNewlyAddedItem();
|
||||||
|
|
@ -1570,7 +1571,7 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (allCodes.length > 0) {
|
if (allCodes.length > 0) {
|
||||||
console.log('[Edit] Requesting prices for', allCodes.length, 'product codes');
|
logger.info('[Edit] Requesting prices for', allCodes.length, 'product codes');
|
||||||
requestSheetPrice(country, allCodes);
|
requestSheetPrice(country, allCodes);
|
||||||
}
|
}
|
||||||
} else if (meta?.status === 'error') {
|
} else if (meta?.status === 'error') {
|
||||||
|
|
@ -1612,7 +1613,7 @@
|
||||||
setTimeout(async () => {
|
setTimeout(async () => {
|
||||||
const requested = await requestCatalogMenu(country, catalog);
|
const requested = await requestCatalogMenu(country, catalog);
|
||||||
if (requested) {
|
if (requested) {
|
||||||
console.log('[Edit] Requested menu data via WebSocket');
|
logger.info('[Edit] Requested menu data via WebSocket');
|
||||||
} else {
|
} else {
|
||||||
addNotification('ERR:Failed to request menu data');
|
addNotification('ERR:Failed to request menu data');
|
||||||
sheetLoading.set(false);
|
sheetLoading.set(false);
|
||||||
|
|
@ -1627,7 +1628,7 @@
|
||||||
})();
|
})();
|
||||||
|
|
||||||
// Data will arrive via WebSocket streaming through sheetStore
|
// Data will arrive via WebSocket streaming through sheetStore
|
||||||
console.log('[Edit] Waiting for data via WebSocket streaming...');
|
logger.info('[Edit] Waiting for data via WebSocket streaming...');
|
||||||
});
|
});
|
||||||
|
|
||||||
// Exit room on unmount
|
// Exit room on unmount
|
||||||
|
|
@ -1760,7 +1761,7 @@
|
||||||
const key = `sheet.maxRowIndex.${country}.${catalog}`;
|
const key = `sheet.maxRowIndex.${country}.${catalog}`;
|
||||||
sessionStorage.setItem(key, String(maxRowIndex));
|
sessionStorage.setItem(key, String(maxRowIndex));
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.warn('[Edit] Failed to store max row index:', e);
|
logger.warn('[Edit] Failed to store max row index:', e);
|
||||||
}
|
}
|
||||||
|
|
||||||
addMenuLoading = true;
|
addMenuLoading = true;
|
||||||
|
|
@ -1805,7 +1806,7 @@
|
||||||
recipe: recipe01_query
|
recipe: recipe01_query
|
||||||
}));
|
}));
|
||||||
|
|
||||||
console.log(
|
logger.info(
|
||||||
'[Edit] Loaded',
|
'[Edit] Loaded',
|
||||||
Object.keys(recipe01_query).length,
|
Object.keys(recipe01_query).length,
|
||||||
'recipes from machine'
|
'recipes from machine'
|
||||||
|
|
@ -1813,11 +1814,11 @@
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
} catch (parseError) {
|
} catch (parseError) {
|
||||||
console.warn('[Edit] Failed to parse recipe JSON from', recipePath, parseError);
|
logger.warn('[Edit] Failed to parse recipe JSON from', recipePath, parseError);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.warn('[Edit] Failed to load machine recipe:', e);
|
logger.warn('[Edit] Failed to load machine recipe:', e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
<script lang="ts">
|
<script lang="ts">import { logger } from '$lib/core/utils/logger';
|
||||||
|
|
||||||
import { auth } from '$lib/core/stores/auth';
|
import { auth } from '$lib/core/stores/auth';
|
||||||
import { addNotification } from '$lib/core/stores/noti';
|
import { addNotification } from '$lib/core/stores/noti';
|
||||||
import Button from '$lib/components/ui/button/button.svelte';
|
import Button from '$lib/components/ui/button/button.svelte';
|
||||||
|
|
@ -108,7 +109,7 @@
|
||||||
AdbInstance.instance ? 'INFO:Machine connected' : 'WARN:No machine selected'
|
AdbInstance.instance ? 'INFO:Machine connected' : 'WARN:No machine selected'
|
||||||
);
|
);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('[Adv] connect error:', error);
|
logger.error('[Adv] connect error:', error);
|
||||||
addNotification(`ERR:Connect failed: ${error instanceof Error ? error.message : 'unknown'}`);
|
addNotification(`ERR:Connect failed: ${error instanceof Error ? error.message : 'unknown'}`);
|
||||||
} finally {
|
} finally {
|
||||||
connecting = false;
|
connecting = false;
|
||||||
|
|
@ -318,7 +319,7 @@
|
||||||
addNotification(`INFO:Pushed ${success} video(s) to machine (${targetDir})`);
|
addNotification(`INFO:Pushed ${success} video(s) to machine (${targetDir})`);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('[Adv] push to machine error:', error);
|
logger.error('[Adv] push to machine error:', error);
|
||||||
addNotification(`ERR:Push error: ${error instanceof Error ? error.message : 'unknown'}`);
|
addNotification(`ERR:Push error: ${error instanceof Error ? error.message : 'unknown'}`);
|
||||||
} finally {
|
} finally {
|
||||||
pushingToMachine = false;
|
pushingToMachine = false;
|
||||||
|
|
@ -389,7 +390,7 @@
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
files[index].status = 'error';
|
files[index].status = 'error';
|
||||||
files[index].error = error instanceof Error ? error.message : 'Unknown error';
|
files[index].error = error instanceof Error ? error.message : 'Unknown error';
|
||||||
console.error(`Upload error for ${item.file.name}:`, error);
|
logger.error(`Upload error for ${item.file.name}:`, error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -429,7 +430,7 @@
|
||||||
await uploadManifestText(text, uid, displayName, email);
|
await uploadManifestText(text, uid, displayName, email);
|
||||||
addNotification(`INFO:Manifest uploaded (${active.length} active file(s))`);
|
addNotification(`INFO:Manifest uploaded (${active.length} active file(s))`);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('[Adv] selected manifest error:', error);
|
logger.error('[Adv] selected manifest error:', error);
|
||||||
addNotification(`ERR:Manifest failed: ${error instanceof Error ? error.message : 'unknown'}`);
|
addNotification(`ERR:Manifest failed: ${error instanceof Error ? error.message : 'unknown'}`);
|
||||||
} finally {
|
} finally {
|
||||||
generatingManifest = false;
|
generatingManifest = false;
|
||||||
|
|
@ -522,7 +523,7 @@
|
||||||
addNotification(`INFO:Machine adv folder synced (${targets.length} file(s))`);
|
addNotification(`INFO:Machine adv folder synced (${targets.length} file(s))`);
|
||||||
return true;
|
return true;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('[Adv] machine sync error:', error);
|
logger.error('[Adv] machine sync error:', error);
|
||||||
addNotification(`ERR:Machine sync failed: ${error instanceof Error ? error.message : 'unknown'}`);
|
addNotification(`ERR:Machine sync failed: ${error instanceof Error ? error.message : 'unknown'}`);
|
||||||
return false;
|
return false;
|
||||||
} finally {
|
} finally {
|
||||||
|
|
@ -567,7 +568,7 @@
|
||||||
}
|
}
|
||||||
addNotification('INFO:Manifest (from machine) uploaded to server');
|
addNotification('INFO:Manifest (from machine) uploaded to server');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('[Adv] machine manifest error:', error);
|
logger.error('[Adv] machine manifest error:', error);
|
||||||
addNotification(`ERR:Machine manifest failed: ${error instanceof Error ? error.message : 'unknown'}`);
|
addNotification(`ERR:Machine manifest failed: ${error instanceof Error ? error.message : 'unknown'}`);
|
||||||
} finally {
|
} finally {
|
||||||
generatingManifest = false;
|
generatingManifest = false;
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,13 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
import { logger } from '$lib/core/utils/logger';
|
||||||
|
|
||||||
import Button from '$lib/components/ui/button/button.svelte';
|
import Button from '$lib/components/ui/button/button.svelte';
|
||||||
import Spinner from '$lib/components/ui/spinner/spinner.svelte';
|
import Spinner from '$lib/components/ui/spinner/spinner.svelte';
|
||||||
import * as Dialog from '$lib/components/ui/dialog/index.js';
|
import * as Dialog from '$lib/components/ui/dialog/index.js';
|
||||||
import { onMount, onDestroy } from 'svelte';
|
import { onMount, onDestroy } from 'svelte';
|
||||||
|
|
||||||
import * as adb from '$lib/core/adb/adb';
|
import * as adb from '$lib/core/adb/adb';
|
||||||
|
import { adbConnectionStatus } from '$lib/core/stores/adbConnectionStore';
|
||||||
import { addNotification } from '$lib/core/stores/noti';
|
import { addNotification } from '$lib/core/stores/noti';
|
||||||
import { columns, type RecipeOverview } from '../../recipe/overview/columns';
|
import { columns, type RecipeOverview } from '../../recipe/overview/columns';
|
||||||
import {
|
import {
|
||||||
|
|
@ -70,8 +73,8 @@
|
||||||
|
|
||||||
// clear out event
|
// clear out event
|
||||||
|
|
||||||
GlobalEventBus.on('recipe-event', (d: any) => {
|
const unsubRecipeEvent = GlobalEventBus.on('recipe-event', (d: any) => {
|
||||||
console.log('[recipe-ev] get event: ', d);
|
logger.info('[recipe-ev] get event: ', d);
|
||||||
if (d?.type == 'load-recipe' && d?.status == 'end') {
|
if (d?.type == 'load-recipe' && d?.status == 'end') {
|
||||||
addNotification('INFO:Get data, waiting for reloading ...');
|
addNotification('INFO:Get data, waiting for reloading ...');
|
||||||
// load finish
|
// load finish
|
||||||
|
|
@ -81,13 +84,13 @@
|
||||||
if (recipeRaw) {
|
if (recipeRaw) {
|
||||||
devRecipe = recipeRaw;
|
devRecipe = recipeRaw;
|
||||||
// update material & topping
|
// update material & topping
|
||||||
console.log('check dev recipe', devRecipe);
|
logger.info('check dev recipe', devRecipe);
|
||||||
}
|
}
|
||||||
// data.recipes = r01Q.recipe;
|
// data.recipes = r01Q.recipe;
|
||||||
|
|
||||||
buildOverviewForBrewing();
|
buildOverviewForBrewing();
|
||||||
|
|
||||||
console.log('refresh by m2 mem recipe data done');
|
logger.info('refresh by m2 mem recipe data done');
|
||||||
recipeLoading = false;
|
recipeLoading = false;
|
||||||
|
|
||||||
addNotification('INFO:Load recipe from memories success!');
|
addNotification('INFO:Load recipe from memories success!');
|
||||||
|
|
@ -110,7 +113,7 @@
|
||||||
let instance = adb.getAdbInstance();
|
let instance = adb.getAdbInstance();
|
||||||
// recipeFromMachineLoading.set(true);
|
// recipeFromMachineLoading.set(true);
|
||||||
referenceFromPage.set('brew');
|
referenceFromPage.set('brew');
|
||||||
console.log('check instance', instance);
|
logger.info('check instance', instance);
|
||||||
if (instance) {
|
if (instance) {
|
||||||
recipeLoading = true;
|
recipeLoading = true;
|
||||||
|
|
||||||
|
|
@ -129,7 +132,7 @@
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
try {
|
try {
|
||||||
console.log('instance passed!');
|
logger.info('instance passed!');
|
||||||
const recipePaths = [
|
const recipePaths = [
|
||||||
`${sourceDir}/cfg/recipe_branch_dev.json`,
|
`${sourceDir}/cfg/recipe_branch_dev.json`,
|
||||||
`${sourceDir}/coffeethai02.json`
|
`${sourceDir}/coffeethai02.json`
|
||||||
|
|
@ -137,7 +140,7 @@
|
||||||
|
|
||||||
for (const recipePath of recipePaths) {
|
for (const recipePath of recipePaths) {
|
||||||
const dev_recipe = await pullTextWithRetry(recipePath);
|
const dev_recipe = await pullTextWithRetry(recipePath);
|
||||||
console.log('dev recipe pull result', {
|
logger.info('dev recipe pull result', {
|
||||||
recipePath,
|
recipePath,
|
||||||
loaded: dev_recipe != undefined,
|
loaded: dev_recipe != undefined,
|
||||||
size: dev_recipe?.length ?? 0
|
size: dev_recipe?.length ?? 0
|
||||||
|
|
@ -149,7 +152,7 @@
|
||||||
buildOverviewForBrewing();
|
buildOverviewForBrewing();
|
||||||
return;
|
return;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('failed to parse recipe json', recipePath, error);
|
logger.error('failed to parse recipe json', recipePath, error);
|
||||||
addNotification(`ERROR:Invalid recipe JSON from ${recipePath}`);
|
addNotification(`ERROR:Invalid recipe JSON from ${recipePath}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -276,7 +279,7 @@
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('error on auto connect brew page', e);
|
logger.error('error on auto connect brew page', e);
|
||||||
addNotification('ERROR:Failed to auto connect, please try again');
|
addNotification('ERROR:Failed to auto connect, please try again');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -349,7 +352,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
afterNavigate(async () => {
|
afterNavigate(async () => {
|
||||||
console.log('after navigate brew');
|
logger.info('after navigate brew');
|
||||||
await startFetchRecipeFromMachine();
|
await startFetchRecipeFromMachine();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -577,7 +580,7 @@
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('failed to persist staged menus to Android', error);
|
logger.error('failed to persist staged menus to Android', error);
|
||||||
addNotification('WARN:Failed to save draft menus to Android');
|
addNotification('WARN:Failed to save draft menus to Android');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -612,7 +615,7 @@
|
||||||
buildOverviewForBrewing();
|
buildOverviewForBrewing();
|
||||||
return true;
|
return true;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('failed to load staged menus from Android', error);
|
logger.error('failed to load staged menus from Android', error);
|
||||||
addNotification('WARN:Failed to load draft menus from Android');
|
addNotification('WARN:Failed to load draft menus from Android');
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
@ -796,7 +799,18 @@
|
||||||
persistStagedMenus();
|
persistStagedMenus();
|
||||||
clearMenuSaveState(productCode);
|
clearMenuSaveState(productCode);
|
||||||
addNotification(`INFO:Menu saved: ${productCode}`);
|
addNotification(`INFO:Menu saved: ${productCode}`);
|
||||||
|
|
||||||
|
// Sync devRecipe from recipeFromMachine before rebuild —
|
||||||
|
// the dialog's save handler may have updated recipeFromMachine
|
||||||
|
// with user edits, and we want buildOverviewForBrewing() to
|
||||||
|
// use the latest data rather than stale devRecipe.
|
||||||
|
const latest = get(recipeFromMachine);
|
||||||
|
if (latest) {
|
||||||
|
devRecipe = latest;
|
||||||
|
}
|
||||||
|
|
||||||
buildOverviewForBrewing();
|
buildOverviewForBrewing();
|
||||||
|
refresh_counter += 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
|
|
@ -816,6 +830,7 @@
|
||||||
});
|
});
|
||||||
|
|
||||||
onDestroy(() => {
|
onDestroy(() => {
|
||||||
|
unsubRecipeEvent();
|
||||||
clearOnMenuSavedCallback();
|
clearOnMenuSavedCallback();
|
||||||
void adb.goToMachineHome();
|
void adb.goToMachineHome();
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
<script lang="ts">
|
<script lang="ts">import { logger } from '$lib/core/utils/logger';
|
||||||
|
|
||||||
import Button from '$lib/components/ui/button/button.svelte';
|
import Button from '$lib/components/ui/button/button.svelte';
|
||||||
import Input from '$lib/components/ui/input/input.svelte';
|
import Input from '$lib/components/ui/input/input.svelte';
|
||||||
import Label from '$lib/components/ui/label/label.svelte';
|
import Label from '$lib/components/ui/label/label.svelte';
|
||||||
|
|
@ -9,6 +10,7 @@
|
||||||
import { goto } from '$app/navigation';
|
import { goto } from '$app/navigation';
|
||||||
|
|
||||||
import * as adb from '$lib/core/adb/adb';
|
import * as adb from '$lib/core/adb/adb';
|
||||||
|
import { adbConnectionStatus } from '$lib/core/stores/adbConnectionStore';
|
||||||
import { addNotification } from '$lib/core/stores/noti';
|
import { addNotification } from '$lib/core/stores/noti';
|
||||||
import { referenceFromPage } from '$lib/core/stores/recipeStore';
|
import { referenceFromPage } from '$lib/core/stores/recipeStore';
|
||||||
import { env } from '$env/dynamic/public';
|
import { env } from '$env/dynamic/public';
|
||||||
|
|
@ -519,7 +521,8 @@
|
||||||
|
|
||||||
async function connectAdb() {
|
async function connectAdb() {
|
||||||
try {
|
try {
|
||||||
if (adb.getAdbInstance()) {
|
const connected = await adb.ensureAdbConnection();
|
||||||
|
if (connected) {
|
||||||
if (!isAdbWriterAvailable()) {
|
if (!isAdbWriterAvailable()) {
|
||||||
await adb.reconnectAndroidRecipeMenuServer();
|
await adb.reconnectAndroidRecipeMenuServer();
|
||||||
}
|
}
|
||||||
|
|
@ -577,7 +580,7 @@
|
||||||
addNotification('INFO:Recipe loaded');
|
addNotification('INFO:Recipe loaded');
|
||||||
return;
|
return;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('failed to parse recipe json', recipePath, error);
|
logger.error('failed to parse recipe json', recipePath, error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -603,7 +606,7 @@
|
||||||
// No country file means Thailand
|
// No country file means Thailand
|
||||||
detectedCountry = 'THAI';
|
detectedCountry = 'THAI';
|
||||||
}
|
}
|
||||||
console.log(
|
logger.info(
|
||||||
'[CreateMenu] Detected country:',
|
'[CreateMenu] Detected country:',
|
||||||
detectedCountry,
|
detectedCountry,
|
||||||
'-> prefix:',
|
'-> prefix:',
|
||||||
|
|
@ -612,7 +615,7 @@
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// Error reading file means Thailand (default)
|
// Error reading file means Thailand (default)
|
||||||
detectedCountry = 'THAI';
|
detectedCountry = 'THAI';
|
||||||
console.log('[CreateMenu] Country file not found, defaulting to THAI');
|
logger.info('[CreateMenu] Country file not found, defaulting to THAI');
|
||||||
} finally {
|
} finally {
|
||||||
countryLoading = false;
|
countryLoading = false;
|
||||||
}
|
}
|
||||||
|
|
@ -1018,7 +1021,7 @@
|
||||||
const result = await adb.executeCmd(`mv ${tempPath} ${stagedMenuAndroidPath}`);
|
const result = await adb.executeCmd(`mv ${tempPath} ${stagedMenuAndroidPath}`);
|
||||||
if (result?.error) throw new Error(String(result.error));
|
if (result?.error) throw new Error(String(result.error));
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('failed to persist staged menus to Android', error);
|
logger.error('failed to persist staged menus to Android', error);
|
||||||
addNotification('WARN:Failed to save draft menus to Android');
|
addNotification('WARN:Failed to save draft menus to Android');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1052,7 +1055,7 @@
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('failed to load staged menus from Android', error);
|
logger.error('failed to load staged menus from Android', error);
|
||||||
addNotification('WARN:Failed to load draft menus from Android');
|
addNotification('WARN:Failed to load draft menus from Android');
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
@ -1219,7 +1222,7 @@
|
||||||
addNotification('WARN:No draft menus ready to save');
|
addNotification('WARN:No draft menus ready to save');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
console.log(
|
logger.info(
|
||||||
'[Create Menu] save all draft menus',
|
'[Create Menu] save all draft menus',
|
||||||
menus.map((menu) => menu.productCode)
|
menus.map((menu) => menu.productCode)
|
||||||
);
|
);
|
||||||
|
|
@ -1416,7 +1419,7 @@
|
||||||
try {
|
try {
|
||||||
boxid = (await adb.pull('/sdcard/coffeevending/.bid')) || undefined;
|
boxid = (await adb.pull('/sdcard/coffeevending/.bid')) || undefined;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.warn('Failed to get boxid from machine:', e);
|
logger.warn('Failed to get boxid from machine:', e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
requestListMenu('tha', boxid);
|
requestListMenu('tha', boxid);
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
<script lang="ts">
|
<script lang="ts">import { logger } from '$lib/core/utils/logger';
|
||||||
|
|
||||||
import { auth } from '$lib/core/stores/auth';
|
import { auth } from '$lib/core/stores/auth';
|
||||||
import { addNotification } from '$lib/core/stores/noti';
|
import { addNotification } from '$lib/core/stores/noti';
|
||||||
import Button from '$lib/components/ui/button/button.svelte';
|
import Button from '$lib/components/ui/button/button.svelte';
|
||||||
|
|
@ -171,7 +172,7 @@
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
files[index].status = 'error';
|
files[index].status = 'error';
|
||||||
files[index].error = error instanceof Error ? error.message : 'Unknown error';
|
files[index].error = error instanceof Error ? error.message : 'Unknown error';
|
||||||
console.error(`Upload error for ${item.file.name}:`, error);
|
logger.error(`Upload error for ${item.file.name}:`, error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
<!-- This is common file and will render through all pages -->
|
<!-- This is common file and will render through all pages -->
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">import { logger } from '$lib/core/utils/logger';
|
||||||
|
|
||||||
import '../app.css';
|
import '../app.css';
|
||||||
import favicon from '$lib/assets/favicon.svg';
|
import favicon from '$lib/assets/favicon.svg';
|
||||||
|
|
||||||
|
|
@ -28,6 +29,7 @@
|
||||||
import { connectToWebsocket } from '$lib/core/stores/websocketStore';
|
import { connectToWebsocket } from '$lib/core/stores/websocketStore';
|
||||||
import { GlobalEventBus } from '$lib/core/utils/eventBus';
|
import { GlobalEventBus } from '$lib/core/utils/eventBus';
|
||||||
import AnnouncementDialog from '$lib/components/AnnouncementDialog.svelte';
|
import AnnouncementDialog from '$lib/components/AnnouncementDialog.svelte';
|
||||||
|
import RemoteShellConfirmDialog from '$lib/components/RemoteShellConfirmDialog.svelte';
|
||||||
import * as semver from 'semver';
|
import * as semver from 'semver';
|
||||||
import { env } from '$env/dynamic/public';
|
import { env } from '$env/dynamic/public';
|
||||||
|
|
||||||
|
|
@ -41,7 +43,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
console.log('base url', window.location.origin, document.cookie);
|
logger.info('base url', window.location.origin, document.cookie);
|
||||||
|
|
||||||
onAuthStateChanged(auth, async function (s) {
|
onAuthStateChanged(auth, async function (s) {
|
||||||
authStore.set(s);
|
authStore.set(s);
|
||||||
|
|
@ -60,14 +62,14 @@
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
return authStore.subscribe(async function (user) {
|
return authStore.subscribe(async function (user) {
|
||||||
// console.log(`store get ${JSON.stringify(user)}`);
|
// logger.info(`store get ${JSON.stringify(user)}`);
|
||||||
|
|
||||||
// reloading permissions
|
// reloading permissions
|
||||||
if (get(currentPermissions).length == 0 && user != null) {
|
if (get(currentPermissions).length == 0 && user != null) {
|
||||||
// need update
|
// need update
|
||||||
let currentUser = user;
|
let currentUser = user;
|
||||||
currentPermissions.set(await getUserPermission(currentUser));
|
currentPermissions.set(await getUserPermission(currentUser));
|
||||||
console.log('reloading permissions ... ');
|
logger.info('reloading permissions ... ');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!get(departmentStore)) {
|
if (!get(departmentStore)) {
|
||||||
|
|
@ -77,7 +79,7 @@
|
||||||
: extractCookieOnNonBrowser()['department'];
|
: extractCookieOnNonBrowser()['department'];
|
||||||
if (saved) {
|
if (saved) {
|
||||||
departmentStore.set(saved.value);
|
departmentStore.set(saved.value);
|
||||||
console.log('get dep', get(departmentStore));
|
logger.info('get dep', get(departmentStore));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
@ -93,4 +95,5 @@
|
||||||
<ModeWatcher />
|
<ModeWatcher />
|
||||||
<Toaster />
|
<Toaster />
|
||||||
<AnnouncementDialog />
|
<AnnouncementDialog />
|
||||||
|
<RemoteShellConfirmDialog />
|
||||||
{@render children()}
|
{@render children()}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
<script lang="ts">
|
<script lang="ts">import { logger } from '$lib/core/utils/logger';
|
||||||
|
|
||||||
import { Button } from '$lib/components/ui/button/index.js';
|
import { Button } from '$lib/components/ui/button/index.js';
|
||||||
import { Label } from '$lib/components/ui/label/index.js';
|
import { Label } from '$lib/components/ui/label/index.js';
|
||||||
import { Input } from '$lib/components/ui/input/index.js';
|
import { Input } from '$lib/components/ui/input/index.js';
|
||||||
|
|
@ -18,7 +19,7 @@
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
let existed_account = get(authStore);
|
let existed_account = get(authStore);
|
||||||
let current_user_logged = auth.currentUser;
|
let current_user_logged = auth.currentUser;
|
||||||
console.log(
|
logger.info(
|
||||||
`has acc ${JSON.stringify(existed_account)}, from session: ${JSON.stringify(current_user_logged)}`
|
`has acc ${JSON.stringify(existed_account)}, from session: ${JSON.stringify(current_user_logged)}`
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,17 @@
|
||||||
|
import { logger } from '$lib/core/utils/logger';
|
||||||
import { json, error } from '@sveltejs/kit';
|
import { json, error } from '@sveltejs/kit';
|
||||||
import type { RequestHandler } from './$types';
|
import type { RequestHandler } from './$types';
|
||||||
import { env } from '$env/dynamic/public';
|
import { env } from '$env/dynamic/public';
|
||||||
|
import { verifyAuthToken } from '$lib/server/auth';
|
||||||
|
|
||||||
// Method 2: forward a machine-generated sync_1.file to the adv FTP server.
|
// Method 2: forward a machine-generated sync_1.file to the adv FTP server.
|
||||||
const ADV_API_BASE = env.PUBLIC_POST_IMAGE;
|
const ADV_API_BASE = env.PUBLIC_POST_IMAGE;
|
||||||
|
|
||||||
export const POST: RequestHandler = async ({ request }) => {
|
export const POST: RequestHandler = async ({ request }) => {
|
||||||
try {
|
try {
|
||||||
|
// Verify authentication
|
||||||
|
await verifyAuthToken(request);
|
||||||
|
|
||||||
const formData = await request.formData();
|
const formData = await request.formData();
|
||||||
|
|
||||||
const country = formData.get('country') as string;
|
const country = formData.get('country') as string;
|
||||||
|
|
@ -33,7 +38,7 @@ export const POST: RequestHandler = async ({ request }) => {
|
||||||
|
|
||||||
return json(await response.json());
|
return json(await response.json());
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('[Adv Manifest Proxy] Error:', err);
|
logger.error('[Adv Manifest Proxy] Error:', err);
|
||||||
if (err && typeof err === 'object' && 'status' in err) {
|
if (err && typeof err === 'object' && 'status' in err) {
|
||||||
throw err;
|
throw err;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,22 @@
|
||||||
|
import { logger } from '$lib/core/utils/logger';
|
||||||
import { json, error } from '@sveltejs/kit';
|
import { json, error } from '@sveltejs/kit';
|
||||||
import type { RequestHandler } from './$types';
|
import type { RequestHandler } from './$types';
|
||||||
import { env } from '$env/dynamic/public';
|
import { env } from '$env/dynamic/public';
|
||||||
|
import { verifyAuthToken } from '$lib/server/auth';
|
||||||
|
|
||||||
// Adv videos are served by the same taobin_image service as menu images.
|
// Adv videos are served by the same taobin_image service as menu images.
|
||||||
const ADV_API_BASE = env.PUBLIC_POST_IMAGE;
|
const ADV_API_BASE = env.PUBLIC_POST_IMAGE;
|
||||||
|
|
||||||
|
const ALLOWED_MIME_TYPES = [
|
||||||
|
'video/mp4', 'video/webm', 'video/ogg',
|
||||||
|
'image/jpeg', 'image/png', 'image/gif', 'image/webp'
|
||||||
|
];
|
||||||
|
|
||||||
export const POST: RequestHandler = async ({ request }) => {
|
export const POST: RequestHandler = async ({ request }) => {
|
||||||
try {
|
try {
|
||||||
|
// Verify authentication
|
||||||
|
await verifyAuthToken(request);
|
||||||
|
|
||||||
const formData = await request.formData();
|
const formData = await request.formData();
|
||||||
|
|
||||||
const country = formData.get('country') as string;
|
const country = formData.get('country') as string;
|
||||||
|
|
@ -21,11 +31,16 @@ export const POST: RequestHandler = async ({ request }) => {
|
||||||
throw error(400, 'Missing required fields');
|
throw error(400, 'Missing required fields');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// File type validation (size limit removed — upstream handles it)
|
||||||
|
if (!ALLOWED_MIME_TYPES.includes(file.type)) {
|
||||||
|
throw error(400, `File type "${file.type}" not allowed. Allowed: ${ALLOWED_MIME_TYPES.join(', ')}`);
|
||||||
|
}
|
||||||
|
|
||||||
const endpoint =
|
const endpoint =
|
||||||
`${ADV_API_BASE}/adv/upload/${encodeURIComponent(country)}/${encodeURIComponent(uid)}/${encodeURIComponent(displayName)}/${encodeURIComponent(email)}` +
|
`${ADV_API_BASE}/adv/upload/${encodeURIComponent(country)}/${encodeURIComponent(uid)}/${encodeURIComponent(displayName)}/${encodeURIComponent(email)}` +
|
||||||
`?regenerate=${encodeURIComponent(regenerate)}`;
|
`?regenerate=${encodeURIComponent(regenerate)}`;
|
||||||
|
|
||||||
console.log('[Adv Upload Proxy] Endpoint:', endpoint, 'file:', file.name);
|
logger.info('[Adv Upload Proxy] Endpoint:', endpoint, 'file:', file.name);
|
||||||
|
|
||||||
// Upstream expects the multipart field name `files`.
|
// Upstream expects the multipart field name `files`.
|
||||||
const uploadFormData = new FormData();
|
const uploadFormData = new FormData();
|
||||||
|
|
@ -44,7 +59,7 @@ export const POST: RequestHandler = async ({ request }) => {
|
||||||
const result = await response.json();
|
const result = await response.json();
|
||||||
return json(result);
|
return json(result);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('[Adv Upload Proxy] Error:', err);
|
logger.error('[Adv Upload Proxy] Error:', err);
|
||||||
|
|
||||||
if (err && typeof err === 'object' && 'status' in err) {
|
if (err && typeof err === 'object' && 'status' in err) {
|
||||||
throw err;
|
throw err;
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,18 @@
|
||||||
|
import { logger } from '$lib/core/utils/logger';
|
||||||
import { json, error } from '@sveltejs/kit';
|
import { json, error } from '@sveltejs/kit';
|
||||||
import type { RequestHandler } from './$types';
|
import type { RequestHandler } from './$types';
|
||||||
import { env } from '$env/dynamic/public';
|
import { env } from '$env/dynamic/public';
|
||||||
|
import { verifyAuthToken } from '$lib/server/auth';
|
||||||
|
|
||||||
const IMAGE_API_BASE = env.PUBLIC_POST_IMAGE;
|
const IMAGE_API_BASE = env.PUBLIC_POST_IMAGE;
|
||||||
|
|
||||||
|
const ALLOWED_MIME_TYPES = ['image/jpeg', 'image/png', 'image/gif', 'image/webp', 'image/avif'];
|
||||||
|
|
||||||
export const POST: RequestHandler = async ({ request }) => {
|
export const POST: RequestHandler = async ({ request }) => {
|
||||||
try {
|
try {
|
||||||
|
// Verify authentication
|
||||||
|
await verifyAuthToken(request);
|
||||||
|
|
||||||
const formData = await request.formData();
|
const formData = await request.formData();
|
||||||
|
|
||||||
const country = formData.get('country') as string;
|
const country = formData.get('country') as string;
|
||||||
|
|
@ -18,6 +25,11 @@ export const POST: RequestHandler = async ({ request }) => {
|
||||||
if (!folder || !uid || !displayName || !email || !file) {
|
if (!folder || !uid || !displayName || !email || !file) {
|
||||||
throw error(400, 'Missing required fields');
|
throw error(400, 'Missing required fields');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// File type validation (size limit removed — upstream handles it)
|
||||||
|
if (!ALLOWED_MIME_TYPES.includes(file.type)) {
|
||||||
|
throw error(400, `File type "${file.type}" not allowed. Allowed: ${ALLOWED_MIME_TYPES.join(', ')}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
// Build the upload endpoint
|
// Build the upload endpoint
|
||||||
|
|
@ -28,7 +40,7 @@ export const POST: RequestHandler = async ({ request }) => {
|
||||||
endpoint = `${IMAGE_API_BASE}/image/${encodeURIComponent(folder)}/upload/${encodeURIComponent(uid)}/${encodeURIComponent(displayName)}/${encodeURIComponent(email)}`;
|
endpoint = `${IMAGE_API_BASE}/image/${encodeURIComponent(folder)}/upload/${encodeURIComponent(uid)}/${encodeURIComponent(displayName)}/${encodeURIComponent(email)}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('[Image Upload Proxy] Endpoint:', endpoint);
|
logger.info('[Image Upload Proxy] Endpoint:', endpoint);
|
||||||
|
|
||||||
// Create new FormData for the upstream request
|
// Create new FormData for the upstream request
|
||||||
const uploadFormData = new FormData();
|
const uploadFormData = new FormData();
|
||||||
|
|
@ -47,7 +59,7 @@ export const POST: RequestHandler = async ({ request }) => {
|
||||||
const result = await response.json();
|
const result = await response.json();
|
||||||
return json(result);
|
return json(result);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('[Image Upload Proxy] Error:', err);
|
logger.error('[Image Upload Proxy] Error:', err);
|
||||||
|
|
||||||
if (err && typeof err === 'object' && 'status' in err) {
|
if (err && typeof err === 'object' && 'status' in err) {
|
||||||
throw err;
|
throw err;
|
||||||
|
|
|
||||||
|
|
@ -1,20 +1,50 @@
|
||||||
import { json } from '@sveltejs/kit';
|
import { logger } from '$lib/core/utils/logger';
|
||||||
|
import { error, json } from '@sveltejs/kit';
|
||||||
|
import type { RequestEvent } from '@sveltejs/kit';
|
||||||
|
import { verifyAuthToken } from '$lib/server/auth';
|
||||||
|
|
||||||
// In-memory store for streamed catalog data
|
// In-memory store for streamed catalog data
|
||||||
// Format: { batchId: { chunks: [...], status: 'collecting'|'complete'|'error' } }
|
// Format: { batchId: { chunks: [...], status: 'collecting'|'complete'|'error' } }
|
||||||
const streamCache = new Map<string, any>();
|
const streamCache = new Map<string, any>();
|
||||||
|
const MAX_CACHE_SIZE = 200; // max number of batches in cache
|
||||||
|
const MAX_CHUNKS_PER_BATCH = 500;
|
||||||
|
|
||||||
export async function POST({ request }) {
|
export async function POST({ request }: RequestEvent) {
|
||||||
try {
|
try {
|
||||||
|
await verifyAuthToken(request);
|
||||||
|
|
||||||
const data = await request.json();
|
const data = await request.json();
|
||||||
const { batch_id, msg, content, current_chunk, total_chunks } = data.payload;
|
const { batch_id, msg, content, current_chunk, total_chunks } = data.payload ?? {};
|
||||||
|
|
||||||
|
// Input validation
|
||||||
|
if (!batch_id || typeof batch_id !== 'string') {
|
||||||
|
throw error(400, 'Missing or invalid batch_id');
|
||||||
|
}
|
||||||
|
if (!msg || typeof msg !== 'string') {
|
||||||
|
throw error(400, 'Missing or invalid msg');
|
||||||
|
}
|
||||||
|
if (content !== undefined && content !== null && typeof content !== 'string') {
|
||||||
|
throw error(400, 'content must be a string');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Enforce cache size limit — evict oldest batches when over capacity
|
||||||
|
if (!streamCache.has(batch_id) && streamCache.size >= MAX_CACHE_SIZE) {
|
||||||
|
const oldestKey = streamCache.keys().next().value;
|
||||||
|
if (oldestKey !== undefined) {
|
||||||
|
streamCache.delete(oldestKey);
|
||||||
|
logger.warn(`[API] Evicted oldest batch "${oldestKey}" — cache full`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Initialize or update batch
|
// Initialize or update batch
|
||||||
if (!streamCache.has(batch_id)) {
|
if (!streamCache.has(batch_id)) {
|
||||||
|
const cappedTotal = typeof total_chunks === 'number' && total_chunks > 0
|
||||||
|
? Math.min(total_chunks, MAX_CHUNKS_PER_BATCH)
|
||||||
|
: MAX_CHUNKS_PER_BATCH;
|
||||||
streamCache.set(batch_id, {
|
streamCache.set(batch_id, {
|
||||||
chunks: [],
|
chunks: [],
|
||||||
status: 'collecting',
|
status: 'collecting',
|
||||||
total_chunks,
|
total_chunks: cappedTotal,
|
||||||
createdAt: Date.now()
|
createdAt: Date.now()
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
@ -23,27 +53,34 @@ export async function POST({ request }) {
|
||||||
|
|
||||||
// Handle different message types
|
// Handle different message types
|
||||||
if (msg === 'start') {
|
if (msg === 'start') {
|
||||||
console.log(`[API] Stream started for batch ${batch_id}`);
|
logger.info(`[API] Stream started for batch ${batch_id}`);
|
||||||
} else if (msg === 'chunk') {
|
} else if (msg === 'chunk') {
|
||||||
|
if (batch.chunks.length >= MAX_CHUNKS_PER_BATCH) {
|
||||||
|
logger.warn(`[API] Max chunks reached for batch ${batch_id}, ignoring chunk`);
|
||||||
|
return json({ status: 'received', batch_id, warning: 'max_chunks_reached' });
|
||||||
|
}
|
||||||
batch.chunks.push(content);
|
batch.chunks.push(content);
|
||||||
console.log(`[API] Received chunk ${current_chunk}/${total_chunks} for batch ${batch_id}`);
|
logger.info(`[API] Received chunk ${current_chunk}/${total_chunks} for batch ${batch_id}`);
|
||||||
} else if (msg === 'end') {
|
} else if (msg === 'end') {
|
||||||
batch.status = 'complete';
|
batch.status = 'complete';
|
||||||
console.log(`[API] Stream complete for batch ${batch_id}, total chunks: ${batch.chunks.length}`);
|
logger.info(`[API] Stream complete for batch ${batch_id}, total chunks: ${batch.chunks.length}`);
|
||||||
} else if (msg === 'error') {
|
} else if (msg === 'error') {
|
||||||
batch.status = 'error';
|
batch.status = 'error';
|
||||||
batch.error = content;
|
batch.error = content;
|
||||||
console.log(`[API] Stream error for batch ${batch_id}:`, content);
|
logger.info(`[API] Stream error for batch ${batch_id}:`, content);
|
||||||
}
|
}
|
||||||
|
|
||||||
return json({ status: 'received', batch_id });
|
return json({ status: 'received', batch_id });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('[API] Error processing stream:', error);
|
logger.error('[API] Error processing stream:', error);
|
||||||
return json({ status: 'error', message: String(error) }, { status: 500 });
|
return json({ status: 'error', message: String(error) }, { status: 500 });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function GET({ url }) {
|
export async function GET({ request, url }: RequestEvent) {
|
||||||
|
// Verify authentication
|
||||||
|
await verifyAuthToken(request);
|
||||||
|
|
||||||
const batchId = url.searchParams.get('batch_id');
|
const batchId = url.searchParams.get('batch_id');
|
||||||
|
|
||||||
// Clean up old cache entries (older than 5 minutes)
|
// Clean up old cache entries (older than 5 minutes)
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
<script lang="ts">
|
<script lang="ts">import { logger } from '$lib/core/utils/logger';
|
||||||
|
|
||||||
import { goto } from '$app/navigation';
|
import { goto } from '$app/navigation';
|
||||||
import { page } from '$app/state';
|
import { page } from '$app/state';
|
||||||
import { env } from '$env/dynamic/public';
|
import { env } from '$env/dynamic/public';
|
||||||
|
|
@ -60,7 +61,7 @@
|
||||||
// credentials: 'include'
|
// credentials: 'include'
|
||||||
// });
|
// });
|
||||||
//
|
//
|
||||||
console.log('login success!');
|
logger.info('login success!');
|
||||||
|
|
||||||
if (browser && 'cookieStore' in window) await cookieStore.set('logged_in', 'true');
|
if (browser && 'cookieStore' in window) await cookieStore.set('logged_in', 'true');
|
||||||
else {
|
else {
|
||||||
|
|
@ -70,7 +71,7 @@
|
||||||
goto('/entry');
|
goto('/entry');
|
||||||
}
|
}
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
console.error(error);
|
logger.error(error);
|
||||||
|
|
||||||
// TODO:
|
// TODO:
|
||||||
authStore.set(null);
|
authStore.set(null);
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue