-{/if}
diff --git a/src/lib/core/adb/adb.ts b/src/lib/core/adb/adb.ts
index b5a0d5e..2cab7d5 100644
--- a/src/lib/core/adb/adb.ts
+++ b/src/lib/core/adb/adb.ts
@@ -11,7 +11,6 @@ import { Consumable, MaybeConsumable, ReadableStream } from '@yume-chan/stream-e
import { AdbScrcpyClient } from '@yume-chan/adb-scrcpy';
import { addNotification } from '../stores/noti';
import { handleAdbPayload } from '../handlers/adbPayloadHandler';
-import { GlobalEventBus } from '../utils/eventBus';
import { adbWriter } from '../stores/adbWriter';
import { WritableStream } from '@yume-chan/stream-extra';
import { env } from '$env/dynamic/public';
@@ -138,7 +137,7 @@ async function connectWithRetry(
export async function connnectViaWebUSB(connectAndroidServer = true) {
const device = await AdbDaemonWebUsbDeviceManager.BROWSER?.requestDevice();
- console.log('usb ok', (globalThis.navigator as Navigator & { usb?: unknown }).usb);
+ console.log('usb ok', globalThis.navigator.usb);
if (device) {
console.log('connect ', device.name);
@@ -363,92 +362,6 @@ export async function executeCmd(command: string) {
}
}
-export async function goToMachineHome() {
- if (!getAdbInstance()) return;
- try {
- await executeCmd('input keyevent KEYCODE_HOME');
- } catch (e) {
- console.error('[goToMachineHome] error', e);
- }
-}
-
-/**
- * Execute an ADB command and stream its output via callbacks.
- * Used for commands like `logcat` that run indefinitely.
- *
- * Returns a cleanup function that can be called to abort the stream.
- */
-export async function executeStreamingCmd(
- command: string,
- callbacks: {
- onData?: (chunk: string) => void;
- onError?: (error: string) => void;
- onExit?: (exitCode: number | undefined) => void;
- }
-): Promise<() => void> {
- const instance = getAdbInstance();
- let aborted = false;
-
- if (!instance) {
- callbacks.onError?.('No ADB device connected');
- return () => {};
- }
-
- try {
- // NOTE: Always use noneProtocol.spawn() for streaming.
- // shellProtocol.spawnWaitText() waits for the process to exit — fine for
- // batch commands like 'echo foo', but logcat runs indefinitely, so the
- // Promise would never resolve and onData would never fire.
- const process = await instance.subprocess.noneProtocol.spawn(command);
-
- const reader = process.output.getReader();
- const decoder = new TextDecoder();
-
- // Start reading in the background
- (async () => {
- try {
- while (!aborted) {
- const { done, value } = await reader.read();
- if (done) break;
- const text = decoder.decode(value, { stream: true });
- if (text && !aborted) {
- callbacks.onData?.(text);
- }
- }
- } catch (e: any) {
- if (!aborted) {
- callbacks.onError?.(e.message ?? 'Stream read error');
- }
- } finally {
- if (!aborted) {
- reader.releaseLock();
- callbacks.onExit?.(0);
- }
- }
- })();
-
- // Return cleanup function to abort the stream
- return () => {
- aborted = true;
- try {
- reader.cancel();
- } catch {
- // reader may already be done
- }
- };
- } catch (e: any) {
- if (!aborted) {
- if (e.message?.includes('ExactReadable ended')) {
- callbacks.onError?.('Connection closed');
- } else {
- callbacks.onError?.(e.message ?? 'Unknown error');
- }
- }
- }
-
- return () => {};
-}
-
export async function disconnect() {
let instance = getAdbInstance();
if (instance) {
@@ -613,32 +526,12 @@ async function connectToAndroidServer(maxRetries = 5) {
if (writer) {
addNotification('INFO:Enable Brewing Mode T on machine');
- const textDecoder = new TextDecoder();
- let buffer = '';
-
(async () => {
try {
while (true) {
const { value, done } = await reader.read();
if (done) break;
-
- // decode chunk
- buffer += textDecoder.decode(value, { stream: true });
-
- let lines = buffer.split('\n');
-
- // save potential incomplete
- buffer = lines.pop() ?? '';
-
- for (const line of lines) {
- if (line.trim() === '') continue;
-
- GlobalEventBus.emit('adb:raw-payload', line);
- handleAdbPayload(line);
- }
-
- // GlobalEventBus.emit('adb:raw-payload', new TextDecoder().decode(value));
- // handleAdbPayload(new TextDecoder().decode(value));
+ handleAdbPayload(new TextDecoder().decode(value));
}
} catch (e) {
console.error('read error', e);
@@ -744,7 +637,6 @@ async function connectToAndroidRecipeMenuServerOnce(notifyFailure = true, retryO
const trimmedMessage = message.trim();
if (trimmedMessage) {
console.log('[ADB Reader] Processing message:', trimmedMessage.slice(0, 200));
- GlobalEventBus.emit('adb:raw-payload', trimmedMessage);
handleAdbPayload(trimmedMessage);
}
}
@@ -752,7 +644,6 @@ async function connectToAndroidRecipeMenuServerOnce(notifyFailure = true, retryO
const remainingMessage = messageBuffer.trim();
if (remainingMessage) {
- GlobalEventBus.emit('adb:raw-payload', remainingMessage);
handleAdbPayload(remainingMessage);
}
} catch (e) {
diff --git a/src/lib/core/adb/adbTerminal.ts b/src/lib/core/adb/adbTerminal.ts
deleted file mode 100644
index d665f99..0000000
--- a/src/lib/core/adb/adbTerminal.ts
+++ /dev/null
@@ -1,1151 +0,0 @@
-import { get } from 'svelte/store';
-import { getAdbInstance, executeCmd, executeStreamingCmd } from './adb';
-import type { Terminal } from '@xterm/xterm';
-import { GlobalEventBus } from '../utils/eventBus';
-import { addNotification } from '../stores/noti';
-import { sendToAndroid, adbWriter } from '../stores/adbWriter';
-
-/**
- * ADB Terminal Service
- *
- * Bridges an xterm.js Terminal with the ADB connection.
- * Supports:
- * - Running raw adb shell commands
- * - Built-in commands (!help, !payload {filter}, !cls, !exit, send, !status)
- * - Real-time payload log viewing via !payload
- * - Interactive ADB TCP socket payload sending via send command
- */
-
-export type LogEntry = {
- timestamp: Date;
- level: string;
- message: string;
- raw: string;
-};
-
-export type PayloadSubscriptionCallback = (payload: any) => void;
-
-/**
- * A recorded command output for the output-blocks display.
- */
-export type CommandOutput = {
- id: number;
- command: string;
- fullOutput: string;
- lines: number;
- level: 'info' | 'error' | 'success';
- timestamp: number;
-};
-
-/**
- * Callback invoked when a new command output is available.
- * Set by terminal-drawer.svelte to reactively show output blocks.
- */
-let _onCommandOutput: ((output: CommandOutput) => void) | null = null;
-let _onExpandOutputRequest: ((id: number) => void) | null = null;
-
-let _outputIdCounter = 0;
-let _commandOutputs: CommandOutput[] = [];
-
-/**
- * Register a listener for new command outputs.
- * Returns an unsubscribe function.
- */
-export function onCommandOutput(cb: (output: CommandOutput) => void): () => void {
- _onCommandOutput = cb;
- return () => {
- _onCommandOutput = null;
- };
-}
-
-/**
- * Register a listener for expand-output click requests from the terminal.
- * Returns an unsubscribe function.
- */
-export function onExpandOutputRequest(cb: (id: number) => void): () => void {
- _onExpandOutputRequest = cb;
- return () => {
- _onExpandOutputRequest = null;
- };
-}
-
-/**
- * Get the current list of command outputs.
- */
-export function getCommandOutputs(): CommandOutput[] {
- return _commandOutputs;
-}
-
-/**
- * Clear all stored command outputs.
- */
-export function clearCommandOutputs() {
- _commandOutputs = [];
-}
-
-/**
- * Record a command output and notify listeners.
- * Returns the recorded output.
- */
-export function recordCommandOutput(cmd: string, fullOutput: string, level: CommandOutput['level'] = 'info') {
- const id = ++_outputIdCounter;
- const lines = fullOutput ? fullOutput.split('\n').filter(l => l.trim()).length : 0;
- const output: CommandOutput = {
- id,
- command: cmd,
- fullOutput,
- lines,
- level,
- timestamp: Date.now()
- };
- _lastOutputId = id;
- _commandOutputs.push(output);
- if (_commandOutputs.length > MAX_OUTPUT_BLOCKS) {
- _commandOutputs.shift();
- }
- _onCommandOutput?.(output);
- return output;
-}
-
-let _lastOutputId = 0;
-
-/**
- * Get the most recent command output ID (for terminal link click expansion).
- */
-export function getLastOutputId(): number {
- return _lastOutputId;
-}
-
-/**
- * Trigger expand of a specific output (called from terminal-drawer link handler).
- */
-export function requestExpandOutput(id: number) {
- _onExpandOutputRequest?.(id);
-}
-
-const COMMAND_HISTORY_KEY = 'supra:adb_terminal_history';
-const MAX_HISTORY = 200;
-
-let _terminal: Terminal | null = null;
-let _commandHistory: string[] = [];
-let _historyIndex = -1;
-let _isPayloadMode = false;
-let _payloadFilter = '';
-let _payloadUnsubscribe: (() => void) | null = null;
-let _payloadLogBuffer: string[] = [];
-
-let _isLogcatMode = false;
-let _logcatCleanup: (() => void) | null = null;
-let _logcatBuffer: string[] = [];
-const LOGCAT_MAX_LINES = 5000; // keep last 5000 lines in buffer
-const LOGCAT_VISIBLE_LINES = 3; // show only 3 lines to terminal
-const MAX_OUTPUT_BLOCKS = 50; // keep last 50 command outputs
-const VISIBLE_OUTPUT_LINES = 3; // lines shown per command in terminal
-
-/**
- * Load command history from localStorage
- */
-function loadHistory(): string[] {
- try {
- const saved = localStorage.getItem(COMMAND_HISTORY_KEY);
- return saved ? JSON.parse(saved) : [];
- } catch {
- return [];
- }
-}
-
-/**
- * Save a command to history
- */
-function saveToHistory(cmd: string) {
- if (!cmd.trim()) return;
- _commandHistory = _commandHistory.filter((c) => c !== cmd);
- _commandHistory.push(cmd);
- if (_commandHistory.length > MAX_HISTORY) {
- _commandHistory = _commandHistory.slice(-MAX_HISTORY);
- }
- try {
- localStorage.setItem(COMMAND_HISTORY_KEY, JSON.stringify(_commandHistory));
- } catch {
- // ignore storage errors
- }
- _historyIndex = _commandHistory.length;
-}
-
-/**
- * Initialize the terminal session
- */
-export function initTerminalSession(terminal: Terminal) {
- _terminal = terminal;
- _commandHistory = loadHistory();
- _historyIndex = _commandHistory.length;
- _isPayloadMode = false;
-
- // Write welcome banner
- const instance = getAdbInstance();
- const deviceStatus = instance ? 'Connected' : 'Disconnected';
-
- terminal.writeln('\x1b[36m╔══════════════════════════════════════════════════════╗\x1b[0m');
- terminal.writeln(
- '\x1b[36m║ \x1b[1mSupra ADB Terminal v1.0\x1b[22m ║\x1b[0m'
- );
- terminal.writeln('\x1b[36m╠══════════════════════════════════════════════════════╣\x1b[0m');
- terminal.writeln(`\x1b[36m║ Device: \x1b[33m${deviceStatus.padEnd(43)}\x1b[36m║\x1b[0m`);
- terminal.writeln(
- `\x1b[36m║ Type \x1b[32m!help\x1b[0m\x1b[36m for available commands ║\x1b[0m`
- );
- terminal.writeln('\x1b[36m╚══════════════════════════════════════════════════════╝\x1b[0m');
- terminal.write('\r\n');
-
- prompt(terminal);
-}
-
-/**
- * Re-initialize terminal after drawer reopen (terminal element stays mounted,
- * but we need to re-fit and re-attach event handlers).
- */
-export function reinitTerminalSession(terminal: Terminal) {
- if (_terminal !== terminal) {
- // If it's a completely new terminal instance, do full init
- initTerminalSession(terminal);
- return;
- }
- // Terminal is same instance — just show appropriate prompt
- if (_isLogcatMode) {
- terminal.writeln('\x1b[90m[Logcat streaming active]\x1b[0m');
- prompt(terminal);
- } else if (_isPayloadMode) {
- // Re-show payload header
- const filterInfo = _payloadFilter ? ` filter="${_payloadFilter}"` : '';
- terminal.writeln(`\x1b[90m[Payload monitoring active${filterInfo}]\x1b[0m`);
- prompt(terminal);
- } else {
- prompt(terminal);
- }
-}
-
-/**
- * Write a prompt to the terminal
- */
-function prompt(terminal: Terminal) {
- if (_isLogcatMode) {
- terminal.write(`\r\n\x1b[33mlogcat\x1b[0m $ `);
- } else if (_isPayloadMode) {
- const filterInfo = _payloadFilter ? ` filter="${_payloadFilter}"` : '';
- terminal.write(`\r\n\x1b[32mpayload${filterInfo}\x1b[0m $ `);
- } else {
- terminal.write(`\r\n\x1b[31m$\x1b[0m `);
- }
-}
-
-/**
- * Handle terminal data input (keypresses/input from xterm onData)
- */
-export function handleTerminalData(data: string) {
- if (!_terminal) return;
-
- const terminal = _terminal;
-
- // Handle special keys
- switch (data) {
- case '\r': // Enter
- handleEnter(terminal);
- return;
- case '\u007f': // Backspace
- handleBackspace(terminal);
- return;
- case '\u001b[A': // Up arrow
- handleHistoryUp(terminal);
- return;
- case '\u001b[B': // Down arrow
- handleHistoryDown(terminal);
- return;
- case '\u0003': // Ctrl+C
- handleCtrlC(terminal);
- return;
- case '\u0001': // Ctrl+A — beginning of line
- terminal.write(`\x1b[${_cursorPos}D`);
- _cursorPos = 0;
- return;
- case '\u0005': // Ctrl+E — end of line
- {
- const charsToEnd = _currentInput.length - _cursorPos;
- if (charsToEnd > 0) {
- terminal.write(`\x1b[${charsToEnd}C`);
- _cursorPos = _currentInput.length;
- }
- }
- return;
- case '\u001b[D': // Left arrow
- handleCursorLeft(terminal);
- return;
- case '\u001b[C': // Right arrow
- handleCursorRight(terminal);
- return;
- case '\t': // Tab
- // Ignore tab
- return;
- default:
- // Only write printable characters
- if (data.length === 1 && data >= ' ') {
- // If in payload mode or logcat mode, stop it when user types a new command
- if (_isPayloadMode && data.trim()) {
- stopPayloadMode(terminal);
- }
- if (_isLogcatMode && data.trim()) {
- stopLogcatMode(terminal);
- }
- // Insert character at cursor position and redraw
- _currentInput = _currentInput.slice(0, _cursorPos) + data + _currentInput.slice(_cursorPos);
- _cursorPos++;
- redrawInputLine(terminal);
- }
- return;
- }
-}
-
-let _currentInput = '';
-let _cursorPos = 0;
-
-/**
- * Redraw the full input line (prompt + current input) and position the cursor.
- */
-function redrawInputLine(terminal: Terminal) {
- terminal.write('\r\x1b[K');
- if (_isLogcatMode) {
- terminal.write('\x1b[33mlogcat\x1b[0m $ ');
- } else if (_isPayloadMode) {
- const filterInfo = _payloadFilter ? ` filter="${_payloadFilter}"` : '';
- terminal.write(`\x1b[32mpayload${filterInfo}\x1b[0m $ `);
- } else {
- terminal.write('\x1b[31m$\x1b[0m ');
- }
- terminal.write(_currentInput);
- const back = _currentInput.length - _cursorPos;
- if (back > 0) {
- terminal.write(`\x1b[${back}D`);
- }
-}
-
-function handleCursorLeft(terminal: Terminal) {
- if (_cursorPos > 0) {
- _cursorPos--;
- terminal.write('\x1b[D');
- }
-}
-
-function handleCursorRight(terminal: Terminal) {
- if (_cursorPos < _currentInput.length) {
- _cursorPos++;
- terminal.write('\x1b[C');
- }
-}
-
-function handleEnter(terminal: Terminal) {
- // Get the current line content from xterm
- const line = _currentInput.trim();
- _currentInput = '';
- _cursorPos = 0;
-
- terminal.write('\r\n');
-
- if (_isLogcatMode) {
- // In logcat mode, empty Enter = stop logcat, text = execute command
- if (!line) {
- stopLogcatMode(terminal);
- } else {
- // Stop logcat, then execute the command
- stopLogcatMode(terminal);
- executeLine(terminal, line);
- }
- return;
- }
-
- if (_isPayloadMode) {
- // In payload mode, treat Enter as continuing to watch
- // If user typed something, exit payload mode and execute
- if (line) {
- stopPayloadMode(terminal);
- executeLine(terminal, line);
- } else {
- prompt(terminal);
- }
- return;
- }
-
- executeLine(terminal, line);
-}
-
-function handleBackspace(terminal: Terminal) {
- // Delete character before cursor
- if (_cursorPos > 0 && _currentInput.length > 0) {
- _currentInput = _currentInput.slice(0, _cursorPos - 1) + _currentInput.slice(_cursorPos);
- _cursorPos--;
- redrawInputLine(terminal);
- }
-}
-
-function handleHistoryUp(terminal: Terminal) {
- if (_commandHistory.length === 0) return;
- if (_historyIndex <= 0) return;
-
- _historyIndex--;
- _currentInput = _commandHistory[_historyIndex];
- _cursorPos = _currentInput.length;
- redrawInputLine(terminal);
-}
-
-function handleHistoryDown(terminal: Terminal) {
- if (_historyIndex >= _commandHistory.length - 1) {
- _historyIndex = _commandHistory.length;
- _currentInput = '';
- _cursorPos = 0;
- redrawInputLine(terminal);
- return;
- }
-
- _historyIndex++;
- _currentInput = _commandHistory[_historyIndex];
- _cursorPos = _currentInput.length;
- redrawInputLine(terminal);
-}
-
-function handleCtrlC(terminal: Terminal) {
- terminal.write('^C\r\n');
- if (_isLogcatMode) {
- stopLogcatMode(terminal);
- return;
- }
- if (_isPayloadMode) {
- stopPayloadMode(terminal);
- }
- _currentInput = '';
- _cursorPos = 0;
- prompt(terminal);
-}
-
-function clearCurrentLine(terminal: Terminal) {
- // Move to beginning of line and clear
- terminal.write('\r\x1b[K');
-}
-
-/**
- * Update the current input buffer (called from onKey)
- *
- * @deprecated This function does not support cursor position. Prefer handleTerminalData.
- */
-export function updateCurrentInput(key: string) {
- if (key.length === 1 && key >= ' ') {
- _currentInput += key;
- _cursorPos = _currentInput.length;
- }
-}
-
-/**
- * Execute a command line
- */
-async function executeLine(terminal: Terminal, line: string) {
- if (!line.trim()) {
- prompt(terminal);
- return;
- }
-
- saveToHistory(line);
-
- // Check for built-in commands (prefixed with !)
- if (line.startsWith('!')) {
- await handleBuiltInCommand(terminal, line);
- return;
- }
-
- // Check for non-prefixed built-in commands
- const firstWord = line.split(' ')[0].toLowerCase();
- if (firstWord === 'send') {
- await handleSendCommand(terminal, line);
- return;
- }
-
- // Regular adb shell command
- await executeAdbCommand(terminal, line);
-}
-
-/**
- * Handle built-in commands (prefixed with !)
- */
-async function handleBuiltInCommand(terminal: Terminal, cmd: string) {
- const parts = cmd.split(' ');
- const command = parts[0].toLowerCase();
- const args = parts.slice(1);
-
- switch (command) {
- case '!help':
- showHelp(terminal);
- prompt(terminal);
- break;
-
- case '!cls':
- case '!clear':
- terminal.clear();
- prompt(terminal);
- break;
-
- case '!exit':
- terminal.writeln('\x1b[33mClosing terminal...\x1b[0m');
- if (_isPayloadMode) stopPayloadMode(terminal);
- // Close the drawer
- const { closeTerminalDrawer } = await import('../stores/terminalDrawer');
- closeTerminalDrawer();
- break;
-
- case '!payload':
- case '!logs':
- startPayloadMode(terminal, args.join(' '));
- break;
-
- case '!status':
- case '!info':
- showDeviceStatus(terminal);
- prompt(terminal);
- break;
-
- case '!history':
- showHistory(terminal);
- prompt(terminal);
- break;
-
- case '!logcat':
- await handleLogcatCommand(terminal, cmd);
- // Don't call prompt here — handleLogcatCommand manages its own prompt
- break;
-
- default:
- terminal.writeln(`\x1b[31mUnknown command: ${command}\x1b[0m`);
- terminal.writeln(`Type \x1b[32m!help\x1b[0m for available commands`);
- prompt(terminal);
- }
-}
-
-/**
- * Format ADB shell command output for readable display.
- *
- * 1. Trims ALL leading and trailing whitespace from each line — raw ADB
- * output often contains varying leading tabs that accumulate per line
- * and make the display unreadable.
- * 2. Replaces internal tabs with a single space (cleaner than 2 spaces).
- * 3. Prepends a consistent 2-space indent for visual separation from
- * prompts/commands.
- * 4. Strips trailing blank lines.
- */
-function formatAdbOutput(output: string): string {
- const lines = output.split('\n');
- // Trim trailing empty lines
- let end = lines.length;
- while (end > 0 && lines[end - 1].trim() === '') end--;
- const trimmed = lines.slice(0, end);
- if (trimmed.length === 0) return '';
-
- // Trim both leading and trailing whitespace, replace internal tabs with
- // single space, then add consistent 2-space indent.
- const cleaned = trimmed.map((line) => {
- const stripped = line.trim().replace(/\t+/g, ' ');
- return stripped ? ` ${stripped}` : '';
- });
-
- return cleaned.join('\r\n');
-}
-
-/**
- * Show help text
- */
-function showHelp(terminal: Terminal) {
- terminal.writeln('');
- terminal.writeln('\x1b[1;36m── Supra ADB Terminal Commands ──\x1b[0m');
- terminal.writeln('');
- terminal.writeln('\x1b[33m Built-in:\x1b[0m');
- terminal.writeln(' \x1b[32m!help\x1b[0m Show this help message');
- terminal.writeln(' \x1b[32m!cls\x1b[0m Clear the terminal screen');
- terminal.writeln(' \x1b[32m!clear\x1b[0m Clear the terminal screen');
- terminal.writeln(' \x1b[32m!status\x1b[0m Show device connection status');
- terminal.writeln(' \x1b[32m!history\x1b[0m Show command history');
- terminal.writeln(' \x1b[32m!exit\x1b[0m Close the terminal drawer');
- terminal.writeln('');
- terminal.writeln('\x1b[33m Payload & Send:\x1b[0m');
- terminal.writeln(' \x1b[32m!payload\x1b[0m Start watching ADB payload logs');
- terminal.writeln(' \x1b[32m!payload \x1b[0m Watch logs filtered by type');
- terminal.writeln(' \x1b[32m!logs\x1b[0m Alias for !payload');
- terminal.writeln(
- " \x1b[32msend -t -p '' [-w ]\x1b[0m Send a TCP payload to the Android device"
- );
- terminal.writeln(' \x1b[90m Example: send -t test -p \'{"p1": "test12"}\'\x1b[0m');
- terminal.writeln(
- ' \x1b[90m Flags: -t type -p payload -w timeout in seconds (0=∞, default=30)\x1b[0m'
- );
- terminal.writeln('');
- terminal.writeln('\x1b[33m Logcat:\x1b[0m');
- terminal.writeln(' \x1b[32m!logcat\x1b[0m Stream ADB logcat from device');
- terminal.writeln(' \x1b[32m!logcat \x1b[0m Filter logcat output');
- terminal.writeln(' \x1b[90m e.g. !logcat -s MyTag, !logcat *:E\x1b[0m');
- terminal.writeln('');
- terminal.writeln('\x1b[33m ADB Shell:\x1b[0m');
- terminal.writeln(
- ' Any command not starting with \x1b[32m!\x1b[0m is sent as adb shell command'
- );
- terminal.writeln(' \x1b[90m e.g. "echo hello", "ls -la", "dumpsys battery"\x1b[0m');
- terminal.writeln('');
- terminal.writeln('\x1b[33m Tips:\x1b[0m');
- terminal.writeln(' \x1b[90m ↑/↓ - Navigate command history\x1b[0m');
- terminal.writeln(' \x1b[90m Ctrl+C - Cancel current command / exit payload mode\x1b[0m');
- terminal.writeln('');
-}
-
-/**
- * Show device connection status
- */
-function showDeviceStatus(terminal: Terminal) {
- const instance = getAdbInstance();
- terminal.writeln('');
- terminal.writeln('\x1b[1;36m── Device Status ──\x1b[0m');
- if (instance) {
- terminal.writeln(` \x1b[32m●\x1b[0m Connected`);
- terminal.writeln(` Transport: ${instance.transport.constructor.name}`);
- try {
- terminal.writeln(` Serial: ${(instance.transport as any).serial ?? 'unknown'}`);
- } catch {
- // serial may not be accessible
- }
- // Show ADB writer status
- const writer = get(adbWriter);
- terminal.writeln(
- ` TCP Socket: ${writer ? '\x1b[32mActive\x1b[0m' : '\x1b[90mNot connected\x1b[0m'}`
- );
- } else {
- terminal.writeln(` \x1b[31m●\x1b[0m Disconnected`);
- terminal.writeln(
- ` \x1b[33m → Use the "Connect" button in the dashboard to connect a device\x1b[0m`
- );
- }
- terminal.writeln('');
-}
-
-/**
- * Show command history
- */
-function showHistory(terminal: Terminal) {
- terminal.writeln('');
- terminal.writeln('\x1b[1;36m── Command History ──\x1b[0m');
- if (_commandHistory.length === 0) {
- terminal.writeln(' \x1b[90m(no commands yet)\x1b[0m');
- } else {
- const start = Math.max(0, _commandHistory.length - 50);
- for (let i = start; i < _commandHistory.length; i++) {
- const num = (i + 1).toString().padStart(4, ' ');
- terminal.writeln(` \x1b[90m${num}\x1b[0m ${_commandHistory[i]}`);
- }
- }
- terminal.writeln('');
-}
-
-/**
- * Parsed send command arguments.
- */
-type SendArgs = {
- type: string;
- payload: string;
- rawPayload: string;
- timeoutMs: number; // 0 = no timeout
-};
-
-const DEFAULT_SEND_TIMEOUT_MS = 30_000;
-
-/**
- * Parse argument string with -t -p [-w ] support.
- * Handles quoted strings for JSON payloads.
- */
-function parseSendArgs(argStr: string): SendArgs {
- const result: SendArgs = {
- type: '',
- payload: '',
- rawPayload: '',
- timeoutMs: DEFAULT_SEND_TIMEOUT_MS
- };
-
- // Tokenize respecting single and double quotes
- const tokens: string[] = [];
- let current = '';
- let inSingle = false;
- let inDouble = false;
-
- for (let i = 0; i < argStr.length; i++) {
- const ch = argStr[i];
- if (ch === "'" && !inDouble) {
- inSingle = !inSingle;
- if (!inSingle) {
- tokens.push(current);
- current = '';
- continue;
- }
- continue;
- }
- if (ch === '"' && !inSingle) {
- inDouble = !inDouble;
- if (!inDouble) {
- tokens.push(current);
- current = '';
- continue;
- }
- continue;
- }
- if (ch === ' ' && !inSingle && !inDouble) {
- if (current) {
- tokens.push(current);
- current = '';
- }
- continue;
- }
- current += ch;
- }
- if (current) tokens.push(current);
-
- // Parse tokens for -t, -p, -w flags
- for (let i = 0; i < tokens.length; i++) {
- if (tokens[i] === '-t' && i + 1 < tokens.length) {
- result.type = tokens[++i];
- } else if (tokens[i] === '-p' && i + 1 < tokens.length) {
- result.rawPayload = tokens[++i];
- } else if (tokens[i] === '-w' && i + 1 < tokens.length) {
- const val = parseInt(tokens[++i], 10);
- if (!isNaN(val) && val >= 0) {
- result.timeoutMs = val === 0 ? 0 : val * 1000;
- }
- }
- }
-
- return result;
-}
-
-/**
- * Execute the interactive send command.
- *
- * Syntax: send -t -p ''
- *
- * Sends a JSON message via the ADB TCP socket and waits for a response event.
- */
-async function handleSendCommand(terminal: Terminal, line: string) {
- // Extract args after "send"
- const args = line.slice('send'.length).trim();
- const parsed = parseSendArgs(args);
-
- if (!parsed.type) {
- terminal.writeln("\x1b[33mUsage: send -t -p '' [-w ]\x1b[0m");
- terminal.writeln('\x1b[90m Example: send -t test -p \'{"p1": "test12"}\' -w 60\x1b[0m');
- prompt(terminal);
- return;
- }
-
- if (!parsed.rawPayload) {
- terminal.writeln('\x1b[31mError: Missing payload (-p).\x1b[0m');
- terminal.writeln("\x1b[33mUsage: send -t -p '' [-w ]\x1b[0m");
- prompt(terminal);
- return;
- }
-
- // Parse the JSON payload
- let payloadObj: any;
- try {
- payloadObj = JSON.parse(parsed.rawPayload);
- } catch {
- terminal.writeln(`\x1b[31mError: Invalid JSON payload\x1b[0m`);
- terminal.writeln(`\x1b[90m Received: ${parsed.rawPayload}\x1b[0m`);
- prompt(terminal);
- return;
- }
-
- // Build the message
- const message = { type: parsed.type, payload: payloadObj };
-
- // Show sent confirmation
- terminal.writeln(`\x1b[90m→ Sending: ${JSON.stringify(message)}\x1b[0m`);
-
- // Send via the writer
- try {
- const sent = await sendToAndroid(message);
- if (!sent) {
- terminal.writeln(`\x1b[31mError: Failed to send message — no ADB socket connection.\x1b[0m`);
- terminal.writeln(
- `\x1b[33m Make sure the device is connected and the TCP socket is active.\x1b[0m`
- );
- prompt(terminal);
- return;
- }
- } catch (e: any) {
- terminal.writeln(`\x1b[31mError sending message: ${e.message ?? 'Unknown error'}\x1b[0m`);
- prompt(terminal);
- return;
- }
-
- // Wait for a response via the event bus (configurable timeout)
- const timeoutSeconds = parsed.timeoutMs === 0 ? '∞' : `${parsed.timeoutMs / 1000}`;
- terminal.writeln(`\x1b[90m← Waiting for response (timeout: ${timeoutSeconds}s)...\x1b[0m`);
-
- const response = await waitForPayloadResponse(parsed.timeoutMs);
-
- if (response === null) {
- terminal.writeln(`\x1b[33m⚠ No response received within timeout.\x1b[0m`);
- } else {
- terminal.writeln(`\x1b[32m✓ Response received:\x1b[0m`);
- terminal.writeln(`\x1b[2m${formatAdbOutput(JSON.stringify(response, null, 2))}\x1b[0m`);
- }
-
- prompt(terminal);
-}
-
-/**
- * Wait for a payload response via GlobalEventBus.
- * Resolves with the first adb:payload event emitted, or null on timeout.
- * Pass 0 for timeoutMs to wait indefinitely.
- */
-function waitForPayloadResponse(timeoutMs: number): Promise {
- return new Promise((resolve) => {
- let timer: ReturnType | undefined;
-
- if (timeoutMs > 0) {
- timer = setTimeout(() => {
- unsub();
- resolve(null);
- }, timeoutMs);
- }
-
- const unsub = GlobalEventBus.on('adb:payload', (payload: any) => {
- if (timer) clearTimeout(timer);
- unsub();
- resolve(payload);
- });
- });
-}
-
-/**
- * Execute an ADB shell command and display formatted output.
- *
- * Display layout — user already sees their typed command via xterm local echo,
- * so we only show indented output for visual separation:
- *
- * ← blank line separator
- * ← 2-space indent, dim gray
- * ← red, if present
- * ← yellow, if non-zero
- * ← blank line before prompt
- * $ ← next prompt
- */
-async function executeAdbCommand(terminal: Terminal, cmd: string) {
- const instance = getAdbInstance();
- if (!instance) {
- const err = 'No ADB device connected.';
- terminal.writeln(`\x1b[31mError: ${err}\x1b[0m`);
- terminal.writeln('\x1b[33mPlease connect a device first via the dashboard.\x1b[0m');
- recordCommandOutput(cmd, `Error: ${err}\nPlease connect a device first via the dashboard.`, 'error');
- prompt(terminal);
- return;
- }
-
- try {
- const result: Record = (await executeCmd(cmd)) ?? {};
-
- terminal.writeln('');
-
- let fullOutput = '';
- const level: CommandOutput['level'] = result.exitCode && result.exitCode !== 0 ? 'error' : 'info';
-
- if (result.output) {
- const formatted = formatAdbOutput(String(result.output));
- if (formatted) {
- fullOutput = formatted;
- const outputLines = formatted.split('\r\n');
- // Show a brief summary in the terminal
- terminal.writeln(`\x1b[90m ✓ Result stored (${outputLines.length} line${outputLines.length > 1 ? 's' : ''})\x1b[0m`);
- }
- }
-
- if (result.error) {
- const errText = String(result.error);
- terminal.writeln(`\x1b[31m${errText}\x1b[0m`);
- if (fullOutput) fullOutput += '\n' + errText;
- else fullOutput = errText;
- }
-
- if (result.exitCode !== undefined && result.exitCode !== 0) {
- terminal.writeln(`\x1b[33mExit code: ${result.exitCode}\x1b[0m`);
- }
-
- // Record for output blocks (even if empty)
- recordCommandOutput(cmd, fullOutput || '(no output)', level);
-
- // Auto-popup the output dialog immediately
- requestExpandOutput(getLastOutputId());
- } catch (e: any) {
- const errMsg = e.message ?? 'Unknown error';
- terminal.writeln(`\x1b[31mError: ${errMsg}\x1b[0m`);
- recordCommandOutput(cmd, `Error: ${errMsg}`, 'error');
- requestExpandOutput(getLastOutputId());
- }
-
- // Blank line before prompt for visual breathing room
- terminal.writeln('');
-
- prompt(terminal);
-}
-
-/**
- * Start payload monitoring mode - shows incoming ADB payloads in real-time
- */
-function startPayloadMode(terminal: Terminal, filter: string) {
- _isPayloadMode = true;
- _payloadFilter = filter;
- _payloadLogBuffer = [];
-
- terminal.writeln('');
- terminal.writeln(`\x1b[1;36m── ADB Payload Monitor ──\x1b[0m`);
- if (filter) {
- terminal.writeln(`\x1b[33m Filter: ${filter}\x1b[0m`);
- }
- terminal.writeln('\x1b[90m Watching for incoming ADB payloads...\x1b[0m');
- terminal.writeln('\x1b[90m Press Ctrl+C or type a command to exit\x1b[0m');
- terminal.writeln('');
-
- // Subscribe to payload events
- _payloadUnsubscribe = GlobalEventBus.on('adb:payload', (payload: any) => {
- if (!_terminal) return;
-
- // Apply filter if set
- if (filter && payload.type) {
- const filterLower = filter.toLowerCase();
- const typeLower = payload.type.toLowerCase();
- if (
- !typeLower.includes(filterLower) &&
- !JSON.stringify(payload).toLowerCase().includes(filterLower)
- ) {
- return;
- }
- }
-
- const timestamp = new Date().toLocaleTimeString();
- const type = payload.type ?? 'unknown';
- const payloadStr = payload.payload ? JSON.stringify(payload.payload).slice(0, 500) : '';
-
- // Color code by type
- let colorPrefix = '';
- switch (type) {
- case 'log':
- colorPrefix = '\x1b[36m'; // cyan
- break;
- case 'error':
- colorPrefix = '\x1b[31m'; // red
- break;
- case 'machine':
- colorPrefix = '\x1b[33m'; // yellow
- break;
- case 'response':
- colorPrefix = '\x1b[32m'; // green
- break;
- case 'brew-finish':
- colorPrefix = '\x1b[35m'; // magenta
- break;
- default:
- colorPrefix = '\x1b[90m'; // gray
- }
-
- terminal.writeln(`${colorPrefix}[${timestamp}] ${type}:\x1b[0m ${payloadStr}`);
-
- // Keep buffer bounded
- _payloadLogBuffer.push(`[${timestamp}] ${type}: ${payloadStr}`);
- if (_payloadLogBuffer.length > 1000) {
- _payloadLogBuffer.shift();
- }
- });
-
- prompt(terminal);
-}
-
-/**
- * Stop payload monitoring mode
- */
-function stopPayloadMode(terminal: Terminal) {
- _isPayloadMode = false;
- _payloadFilter = '';
-
- if (_payloadUnsubscribe) {
- _payloadUnsubscribe();
- _payloadUnsubscribe = null;
- }
-
- terminal.writeln('');
- terminal.writeln('\x1b[33mPayload monitoring stopped.\x1b[0m');
-}
-
-/**
- * Start logcat mode — streams adb logcat output in real-time.
- * Syntax: !logcat [filters]
- * Filters follow Android logcat syntax:
- * !logcat → all logs
- * !logcat -s MyTag → only MyTag (silent others)
- * !logcat *:E → only errors
- * !logcat ActivityManager:I *:S → ActivityManager info+ only
- * !logcat -v color → colored output
- */
-async function handleLogcatCommand(terminal: Terminal, cmd: string) {
- const instance = getAdbInstance();
- if (!instance) {
- terminal.writeln('\x1b[31mError: No ADB device connected.\x1b[0m');
- terminal.writeln('\x1b[33mPlease connect a device first via the dashboard.\x1b[0m');
- prompt(terminal);
- return;
- }
-
- // Parse filter args from "!logcat" command
- // "!logcat -s MyTag" → args = "-s MyTag"
- const args = cmd.slice('!logcat'.length).trim();
- const logcatCommand = `logcat -v brief ${args}`;
-
- _isLogcatMode = true;
- _logcatBuffer = [];
-
- terminal.writeln('');
- terminal.writeln(`\x1b[1;36m── ADB Logcat ──\x1b[0m`);
- if (args) {
- terminal.writeln(`\x1b[33m Filter: ${args}\x1b[0m`);
- }
- terminal.writeln('\x1b[90m Streaming from device...\x1b[0m');
- terminal.writeln('\x1b[90m Press Ctrl+C or type Enter (empty) to stop\x1b[0m');
- terminal.writeln('');
-
- let lineCount = 0;
-
- _logcatCleanup = await executeStreamingCmd(logcatCommand, {
- onData: (chunk: string) => {
- if (!_terminal || !_isLogcatMode) return;
-
- // Split chunk into lines
- const lines = chunk.split('\n');
- for (const line of lines) {
- if (!line.trim()) continue;
- lineCount++;
-
- // Prepend timestamp to both buffer and terminal output
- // so the history dialog shows timestamps too
- const ts = new Date().toLocaleTimeString();
- const tsLine = `[${ts}] ${line}`;
-
- // Store in buffer (with timestamp)
- _logcatBuffer.push(tsLine);
- if (_logcatBuffer.length > LOGCAT_MAX_LINES) {
- _logcatBuffer.shift();
- }
-
- // Write to terminal: color the line content (not timestamp)
- // Lines look like: "V/MyTag: message" or "E/MyTag: message"
- let coloredLine = tsLine;
- if (line.includes('/')) {
- const level = line.charAt(0).toUpperCase();
- switch (level) {
- case 'E': coloredLine = `\x1b[31m${tsLine}\x1b[0m`; break; // red
- case 'W': coloredLine = `\x1b[33m${tsLine}\x1b[0m`; break; // yellow
- case 'I': coloredLine = `\x1b[36m${tsLine}\x1b[0m`; break; // cyan
- case 'D': coloredLine = `\x1b[90m${tsLine}\x1b[0m`; break; // gray
- case 'V': coloredLine = `\x1b[2m${tsLine}\x1b[0m`; break; // dim
- case 'F': coloredLine = `\x1b[35m${tsLine}\x1b[0m`; break; // magenta (fatal)
- }
- }
-
- terminal.writeln(coloredLine);
- }
- },
- onError: (error: string) => {
- if (!_terminal || !_isLogcatMode) return;
- terminal.writeln(`\x1b[31mLogcat error: ${error}\x1b[0m`);
- },
- onExit: (exitCode: number | undefined) => {
- if (!_terminal || !_isLogcatMode) return;
- terminal.writeln('');
- terminal.writeln(`\x1b[33mLogcat exited (code: ${exitCode ?? 'unknown'}).\x1b[0m`);
- _isLogcatMode = false;
- _logcatCleanup = null;
- // Record logcat history as a command output and auto-show dialog
- if (_logcatBuffer.length > 0) {
- const fullOutput = _logcatBuffer.join('\n');
- recordCommandOutput('!logcat', fullOutput, 'info');
- requestExpandOutput(getLastOutputId());
- }
- _logcatBuffer = [];
- prompt(terminal);
- }
- });
-
- // We're in streaming mode — prompt will show when logcat ends or user stops it
-}
-
-/**
- * Stop logcat streaming
- */
-function stopLogcatMode(terminal: Terminal) {
- _isLogcatMode = false;
-
- if (_logcatCleanup) {
- _logcatCleanup();
- _logcatCleanup = null;
- }
-
- terminal.writeln('');
- terminal.writeln(`\x1b[33mLogcat stopped. Captured \x1b[1m${_logcatBuffer.length}\x1b[22m lines.\x1b[0m`);
-
- // Record logcat history as a command output and auto-show dialog
- if (_logcatBuffer.length > 0) {
- const fullOutput = _logcatBuffer.join('\n');
- recordCommandOutput('!logcat', fullOutput, 'info');
- requestExpandOutput(getLastOutputId());
- }
-
- _logcatBuffer = [];
- prompt(terminal);
-}
-
-/**
- * Clean up terminal session resources.
- * Called when the terminal component is destroyed (page navigation, not drawer close).
- */
-export function cleanupTerminalSession() {
- if (_logcatCleanup) {
- _logcatCleanup();
- _logcatCleanup = null;
- }
- if (_payloadUnsubscribe) {
- _payloadUnsubscribe();
- _payloadUnsubscribe = null;
- }
- _isLogcatMode = false;
- _isPayloadMode = false;
- _payloadFilter = '';
- _terminal = null;
- _currentInput = '';
- _cursorPos = 0;
- _logcatBuffer = [];
-}
-
-/**
- * Save the terminal buffer content so it can be restored on reopen.
- * Called when the drawer is minimized to preserve session state.
- * With the current "keep-mounted" approach, this is a lightweight save
- * of the cursor / input state.
- */
-export function onTerminalMinimized() {
- _currentInput = '';
- _cursorPos = 0;
-}
-
-/**
- * Check if the user has admin permission based on their role
- */
-export function isAdminUser(user: { role?: string } | null): boolean {
- return user?.role === 'admin';
-}
-
-// Re-export for use in components
-export { _isPayloadMode as isPayloadMode };
diff --git a/src/lib/core/client/server.ts b/src/lib/core/client/server.ts
index 979c021..98e2ea9 100644
--- a/src/lib/core/client/server.ts
+++ b/src/lib/core/client/server.ts
@@ -37,7 +37,7 @@ export async function getRecipes() {
recipeData.set([]);
recipeOverviewData.set([]);
- await sendMessage({
+ sendMessage({
type: 'recipe',
payload: {
auth: idToken ?? '',
@@ -82,7 +82,7 @@ export async function getRecipeWithVersion(version: string) {
// NOTE: although version is provided, actual version field is still need to be latest
// Just in case version is not found
- await sendMessage({
+ sendMessage({
type: 'recipe',
payload: {
auth: idToken ?? '',
diff --git a/src/lib/core/handlers/adbPayloadHandler.ts b/src/lib/core/handlers/adbPayloadHandler.ts
index 450b202..fbc3f2b 100644
--- a/src/lib/core/handlers/adbPayloadHandler.ts
+++ b/src/lib/core/handlers/adbPayloadHandler.ts
@@ -7,40 +7,16 @@ import {
} from '../services/androidRecipeExportService';
import { handleIncomingMessages } from './messageHandler';
import { setMenuSaved, setMenuSaveError } from '../stores/menuSaveStore';
-import { recipeFromMachine, recipeFromMachineQuery } from '../stores/recipeStore';
-import { buildTags, getMenuStatus } from '$lib/data/recipeService';
-import * as semver from 'semver';
-import { env } from '$env/dynamic/public';
-import { getContext } from 'svelte';
-import { GlobalEventBus, useEventBus } from '../utils/eventBus';
+import { recipeFromMachineQuery } from '../stores/recipeStore';
type AdbPayload = { type: string; payload: any };
-let queuedPromises = new Array>();
-
async function handleAdbPayload(raw_payload: string) {
- // console.log('[ADB] Received payload:', raw_payload.slice(0, 300));
- const APP_VERSION = env.PUBLIC_APP_SEMVER;
- // const bus = useEventBus();
-
+ console.log('[ADB] Received payload:', raw_payload.slice(0, 300));
try {
const payload: AdbPayload = JSON.parse(raw_payload);
- // console.log('[ADB] Parsed type:', payload.type, 'payload:', payload.payload);
-
- // Emit payload event for terminal drawer payload viewer
- GlobalEventBus.emit('adb:payload', payload);
-
- let payload_type = payload.type;
- let sub_type = null;
- // sub type handler
- if (payload_type.includes('/')) {
- //
- let tspl = payload_type.split('/');
- payload_type = tspl[0];
- sub_type = tspl[1];
- }
-
- switch (payload_type) {
+ console.log('[ADB] Parsed type:', payload.type, 'payload:', payload.payload);
+ switch (payload.type) {
case 'log':
let log_level = payload.payload['level'] ?? 'INFO';
let log_message = payload.payload['msg'] ?? '';
@@ -83,16 +59,16 @@ async function handleAdbPayload(raw_payload: string) {
}
if (raw_payload.startsWith('save_recipe_machine')) {
- // handleIncomingMessages(
- // JSON.stringify({
- // type: 'ui_action',
- // payload: {
- // action: uiAction,
- // from: 'brew',
- // ref: `${pd}.${action}`
- // }
- // })
- // );
+ handleIncomingMessages(
+ JSON.stringify({
+ type: 'ui_action',
+ payload: {
+ action: uiAction,
+ from: 'brew',
+ ref: `${pd}.${action}`
+ }
+ })
+ );
}
} else if (raw_payload.startsWith('state')) {
let res = raw_payload.split('/');
@@ -186,177 +162,10 @@ async function handleAdbPayload(raw_payload: string) {
addNotification(`ERR:${error?.message ?? 'Unable to load recipe export from Android'}`)
);
break;
- case 'get-recipe-response':
- // only update recipe from memory of brew app
- if (!semver.satisfies(APP_VERSION, '^0.0.3')) {
- // reject
- console.log('unsupported version');
- break;
- }
-
- let stream_recipe_idx = -1;
-
- try {
- stream_recipe_idx = parseInt(sub_type ?? '-1');
- } catch (_) {}
-
- if (stream_recipe_idx > -1) {
- // TODO: update both recipeFromMachine and recipeFromMachineQuery
- //
-
- queuedPromises.push(
- new Promise((resolve, reject) => {
- let recipeRawFromMachine = get(recipeFromMachine);
- let recipeMachineQ = get(recipeFromMachineQuery);
-
- if (!Object.keys(recipeMachineQ).includes('recipe')) {
- recipeMachineQ = {
- ...recipeMachineQ,
- recipe: {}
- };
- }
-
- let current_r01_query = recipeMachineQ.recipe;
-
- let rp_from_payload = JSON.parse(payload.payload);
-
- try {
- if (
- recipeRawFromMachine != null &&
- Object.keys(recipeRawFromMachine).includes('Recipe01')
- ) {
- let is_update = false;
- // update, assume that we already fetch
- for (let rp of recipeRawFromMachine['Recipe01']) {
- if (rp['productCode'] == rp_from_payload['productCode']) {
- rp = rp_from_payload;
- is_update = true;
- break;
- }
- }
-
- // new menu
- if (!is_update) recipeRawFromMachine['Recipe01'].push(rp_from_payload);
- } else {
- // not initialize
- recipeRawFromMachine = {
- Recipe01: [rp_from_payload]
- };
- }
-
- // build as overview style compatible
-
- let overview_menu = {
- productCode: rp_from_payload['productCode'] ?? '',
- name: rp_from_payload['name']
- ? rp_from_payload['name']
- : (rp_from_payload['otherName'] ?? ''),
- description: rp_from_payload['desciption']
- ? rp_from_payload['desciption']
- : (rp_from_payload['otherDescription'] ?? ''),
- tags: buildTags(rp_from_payload),
- status: getMenuStatus(rp_from_payload['MenuStatus'])
- };
-
- current_r01_query[overview_menu.productCode] = overview_menu;
-
- recipeMachineQ = {
- ...recipeMachineQ,
- recipe: current_r01_query
- };
-
- recipeFromMachineQuery.set(recipeMachineQ);
- recipeFromMachine.set(recipeRawFromMachine);
-
- if (stream_recipe_idx == 0) {
- addNotification(`INFO:Loading recipes ...`);
- }
-
- resolve();
- } catch (e) {
- reject(e);
- }
- })
- );
- } else if (sub_type == 'end' && payload.payload.includes('finish')) {
- let force_reload = payload.payload.includes('reload');
-
- console.log('queued recipes: ', queuedPromises.length);
- if (queuedPromises.length > 0) {
- try {
- await Promise.all(queuedPromises);
- console.log('clear all recipe promises');
- queuedPromises = new Array();
-
- GlobalEventBus.emitUntilConsumed('recipe-event', {
- type: 'load-recipe',
- status: 'end',
- reload: force_reload
- });
- } catch (e) {
- console.error('some promise failed: ', e);
-
- GlobalEventBus.emitUntilConsumed('recipe-event', {
- type: 'load-recipe-fail',
- status: 'end',
- reload: force_reload
- });
- }
- }
- } else if (sub_type?.startsWith('mat')) {
- let recipeRawFromMachine = get(recipeFromMachine);
-
- if (sub_type == 'mat-0') {
- addNotification('INFO:Loading materials ...');
- }
-
- if (
- recipeRawFromMachine != null &&
- Object.keys(recipeRawFromMachine).includes('MaterialSetting')
- ) {
- recipeRawFromMachine.MaterialSetting.push(JSON.parse(payload.payload));
- } else {
- // not has field material yet
- recipeRawFromMachine = {
- ...recipeRawFromMachine,
- MaterialSetting: [JSON.parse(payload.payload)]
- };
- }
-
- console.log('checking add mat', recipeRawFromMachine);
- recipeFromMachine.set(recipeRawFromMachine);
- } else if (sub_type == 'toppings') {
- let recipeRawFromMachine = get(recipeFromMachine);
-
- let topping_raw = JSON.parse(payload.payload);
- console.log('receive topping', topping_raw);
-
- //
- recipeRawFromMachine = {
- ...recipeRawFromMachine,
- Topping: topping_raw
- };
-
- // materialFromMachineQuery
-
- addNotification('INFO:Loading toppings ...');
-
- recipeFromMachine.set(recipeRawFromMachine);
- } else {
- // unhandled sub type
- console.log('unhandled sub type', payload);
- }
-
- break;
default:
}
} catch (error: any) {
- // invalid format — emit error event so listeners (e.g. terminal) can react
- GlobalEventBus.emit('adb:payload-error', {
- raw_payload,
- error: error?.message ?? 'Unknown parse error',
- timestamp: new Date().toISOString()
- });
+ // invalid format
}
}
diff --git a/src/lib/core/handlers/messageHandler.ts b/src/lib/core/handlers/messageHandler.ts
index e00612e..61cd6a3 100644
--- a/src/lib/core/handlers/messageHandler.ts
+++ b/src/lib/core/handlers/messageHandler.ts
@@ -21,14 +21,11 @@ import {
handleSheetStreamEnd,
handleSheetStreamError,
handleCatalogsResponse,
- handlePriceSlotsResponse,
- isPriceSlotsPayload,
handleListMenuResponse,
sheetCatalogsLoading,
handleRawStreamHeader,
handleRawStreamChunk,
- handleRawStreamEnd,
- handleSheetPriceResponse
+ handleRawStreamEnd
} from '../stores/sheetStore';
import {
handleGenLayoutBatchStart,
@@ -40,24 +37,15 @@ import { buildOverviewFromServer } from '$lib/data/recipeService';
import { auth } from '../client/firebase';
import { type RecipeVersion } from '$lib/models/recipe_version.model';
import { goto } from '$app/navigation';
-import {
- sharedKey as sharedKey,
- socketAlreadySendHeartbeat,
- socketConnectionOfflineCount
-} from '../stores/websocketStore';
+import { socketAlreadySendHeartbeat, socketConnectionOfflineCount } from '../stores/websocketStore';
import type { RecipePrice } from '$lib/models/price.model';
import { sendCommandRequest, sendMessage } from './ws_messageSender';
import { auth as authStore } from '../stores/auth';
import { v4 as uuidv4 } from 'uuid';
import { handleSheetResponseFromNoti } from './sheetNotiHandler';
-import { env } from '$env/dynamic/public';
-import { WebCryptoHelper } from '../utils/crypto';
-import { GlobalEventBus } from '../utils/eventBus';
-import * as semver from 'semver';
export const messages = writable([]);
-type HandshakeAck = { server_public_key: string; status: string };
type WSMessage = { type: string; payload: any };
// MAXIMUM LIMIT = 1814355
@@ -143,7 +131,7 @@ const handlers: Record void> = {
}
}
},
- stream_data_end: async (p) => {
+ stream_data_end: (p) => {
recipeLoading.set(false);
// build overview for recipe from server
@@ -166,7 +154,7 @@ const handlers: Record void> = {
}
// send next chain message
- await sendMessage({
+ sendMessage({
type: 'price',
payload: {
action: {
@@ -295,76 +283,26 @@ const handlers: Record void> = {
if (from === 'sheet-service' && level === 'content') {
const currentUid = auth.currentUser?.uid;
- const content = p.content ?? p.value ?? p.payload;
- const ref = p.ref ?? '';
- console.log('[Sheet] Notify content received:', {
- msg,
- target,
- currentUid,
- contentKeys: content && typeof content === 'object' ? Object.keys(content) : [],
- contentItems: Array.isArray(content) ? content.length : undefined
- });
-
- if (!target || (currentUid && target === currentUid)) {
- if (!msg && content?.catalogs) {
- handleCatalogsResponse(content);
- addNotification(`INFO:Loaded ${content.catalogs?.length || 0} catalogs`);
- return;
- }
-
- if (
- !msg &&
- (content?.priceSlots ||
- content?.priceslots ||
- content?.price_slots ||
- content?.slots ||
- content?.param === 'priceslot' ||
- content?.option === 'PriceSlot' ||
- isPriceSlotsPayload(content))
- ) {
- handlePriceSlotsResponse(content);
- addNotification('INFO:Loaded PriceSlot data');
- return;
- }
-
- if (!msg && ref === 'price') {
- handleSheetPriceResponse(p.country ?? p.payload?.country ?? '', content);
- addNotification('INFO:Loaded sheet price data');
+ if (target && currentUid && target === currentUid) {
+ if (!msg && p.content?.catalogs) {
+ handleCatalogsResponse(p.content);
+ addNotification(`INFO:Loaded ${p.content.catalogs?.length || 0} catalogs`);
return;
}
// Handle streaming messages (with msg field)
switch (msg) {
- case 'priceslot':
- case 'price_slot':
- handlePriceSlotsResponse(content);
- addNotification('INFO:Loaded PriceSlot data');
- break;
case 'start':
- if (ref === 'price') {
- addNotification('INFO:Sheet price streaming started');
- } else {
- handleSheetStreamStart(p);
- addNotification('INFO:Sheet data streaming started');
- }
+ handleSheetStreamStart(p);
+ addNotification('INFO:Sheet data streaming started');
break;
case 'chunk':
- if (isPriceSlotsPayload(content)) {
- handlePriceSlotsResponse(content);
- } else if (ref === 'price') {
- handleSheetPriceResponse(p.country ?? p.payload?.country ?? '', content);
- } else {
- handleSheetStreamChunk(p);
- }
+ handleSheetStreamChunk(p);
break;
case 'end':
- if (ref === 'price') {
- addNotification('INFO:Sheet price streaming complete');
- } else {
- handleSheetStreamEnd(p);
- addNotification('INFO:Sheet data streaming complete');
- }
+ handleSheetStreamEnd(p);
+ addNotification('INFO:Sheet data streaming complete');
break;
case 'error':
handleSheetStreamError(p);
@@ -372,17 +310,8 @@ const handlers: Record void> = {
break;
default:
// Handle other content notifications from sheet-service
- console.log('[Sheet] Received content:', {
- contentItems: Array.isArray(content) ? content.length : undefined
- });
+ console.log('[Sheet] Received content:', p.content);
}
- } else {
- console.warn('[Sheet] Ignored content because target does not match current user:', {
- target,
- currentUid,
- msg,
- contentItems: Array.isArray(content) ? content.length : undefined
- });
}
return;
}
@@ -423,7 +352,7 @@ const handlers: Record void> = {
currentRecipeVersionsSelector.set(result);
}
},
- price: async (p) => {
+ price: (p) => {
let req_action = p.req_action;
let status = p.status;
let to = p.to;
@@ -456,11 +385,10 @@ const handlers: Record void> = {
current_streaming_instance[request_id] = '';
streamingRawData.set(current_streaming_instance);
- await sendCommandRequest('sheet', {
+ sendCommandRequest('sheet', {
country: current_meta?.country ?? '',
content: saved_product_code_to_get_from_sheet,
param: 'price',
- option: 'price',
stream: true,
request_id
});
@@ -538,79 +466,32 @@ const handlers: Record void> = {
// Header for price stream
handleRawStreamHeader('price', p);
},
- raw_stream_priceslot: (p) => {
- handleRawStreamHeader('priceslot', p);
- },
raw_stream_chunk_price: (p) => {
// Chunk for price stream
handleRawStreamChunk('price', p);
},
- raw_stream_chunk_priceslot: (p) => {
- handleRawStreamChunk('priceslot', p);
- },
raw_stream_end_price: (p) => {
// End for price stream
handleRawStreamEnd('price', p);
- },
- raw_stream_end_priceslot: (p) => {
- handleRawStreamEnd('priceslot', p);
- },
- announce: (p) => {
- // Server-pushed announcement (e.g., closing maintenance)
- GlobalEventBus.emit('announce', p);
}
};
-export async function handleIncomingMessages(raw: string, clientPrivateKey?: CryptoKey) {
- const APP_VERSION = env.PUBLIC_APP_SEMVER;
- const parsedMessage = JSON.parse(raw);
-
- const ack: HandshakeAck = parsedMessage;
- if (ack != null && ack.status === 'authenticated') {
- // has server response
- if (!clientPrivateKey) return;
-
- sharedKey.set(await WebCryptoHelper.deriveSharedKey(clientPrivateKey, ack.server_public_key));
-
- addNotification('INFO:Secured Connection');
-
+export function handleIncomingMessages(raw: string) {
+ const msg: WSMessage = JSON.parse(raw);
+ // console.log(`[WS MSG] type=${msg.type}`, msg.payload);
+ if (msg == null) {
+ // error response
+ addNotification('ERR:No response from server');
return;
}
- if (semver.satisfies(APP_VERSION, '>=0.0.2') && parsedMessage.ciphertext && parsedMessage.iv) {
- // secured message decryption
- let sharedKeyStore = get(sharedKey);
- if (sharedKeyStore) {
- let decrypted_string = await WebCryptoHelper.decryptMessage(
- sharedKeyStore,
- parsedMessage.ciphertext,
- parsedMessage.iv
- );
- let actual_message: WSMessage = JSON.parse(decrypted_string);
- if (actual_message.type !== 'heartbeat') {
- // console.log(`[WS MSG] type=${actual_message.type}`, actual_message.payload);
- }
- handlers[actual_message.type]?.(actual_message.payload);
- }
- } else {
- const msg: WSMessage = parsedMessage;
- if (msg.type !== 'heartbeat') {
- // console.log(`[WS MSG] type=${msg.type}`, msg.payload);
- }
- if (msg == null) {
- // error response
- addNotification('ERR:No response from server');
- return;
- }
+ // raw streaming type
+ // if (msg.type.startsWith('raw_stream')) {
+ // // convert
+ // let sub_type = msg.type.replace('raw_stream_', '');
+ // msg.payload.sub_type = sub_type;
+ // msg.type = 'raw_stream';
+ // }
- // raw streaming type
- // if (msg.type.startsWith('raw_stream')) {
- // // convert
- // let sub_type = msg.type.replace('raw_stream_', '');
- // msg.payload.sub_type = sub_type;
- // msg.type = 'raw_stream';
- // }
-
- handlers[msg.type]?.(msg.payload);
- }
+ handlers[msg.type]?.(msg.payload);
}
diff --git a/src/lib/core/handlers/ws_messageSender.ts b/src/lib/core/handlers/ws_messageSender.ts
index 1590409..5d3dd01 100644
--- a/src/lib/core/handlers/ws_messageSender.ts
+++ b/src/lib/core/handlers/ws_messageSender.ts
@@ -1,15 +1,12 @@
import { get, writable } from 'svelte/store';
import type { OutMessage } from '../types/outMessage';
-import { sharedKey, socketStore, wsAuthReady } from '../stores/websocketStore';
+import { socketStore } from '../stores/websocketStore';
import { addNotification } from '../stores/noti';
import { auth } from '../stores/auth';
-import { WebCryptoHelper } from '../utils/crypto';
-import { env } from '$env/dynamic/public';
-import * as semver from 'semver';
export const queue = writable([]);
-type CommandRequest = 'sheet' | 'command' | 'upload-log';
+type CommandRequest = 'sheet' | 'command';
function getServiceName(cmdReq: CommandRequest) {
switch (cmdReq) {
@@ -17,45 +14,11 @@ function getServiceName(cmdReq: CommandRequest) {
return 'sheet-service';
case 'command':
return 'command';
- case 'upload-log':
- return 'upload-log';
}
}
-function waitForWsAuthReady(timeoutMs = 10000): Promise {
- if (get(wsAuthReady)) return Promise.resolve(true);
-
- return new Promise((resolve) => {
- let settled = false;
- let unsubscribe = () => {};
- const timeout = setTimeout(() => {
- if (settled) return;
- settled = true;
- unsubscribe();
- resolve(false);
- }, timeoutMs);
-
- unsubscribe = wsAuthReady.subscribe((ready) => {
- if (!ready || settled) return;
- settled = true;
- clearTimeout(timeout);
- unsubscribe();
- resolve(true);
- });
- });
-}
-
// Websocket message wrapper for commands like `sheet`, `command`
-export async function sendCommandRequest(target: CommandRequest, values: any): Promise {
- const authReady = await waitForWsAuthReady();
- if (!authReady) {
- console.warn('[WS Send] Skip command request because websocket auth is not ready', {
- target,
- param: values?.param
- });
- return false;
- }
-
+export function sendCommandRequest(target: CommandRequest, values: any): boolean {
let srv_name = getServiceName(target);
let curr_user = get(auth);
@@ -68,7 +31,7 @@ export async function sendCommandRequest(target: CommandRequest, values: any): P
};
}
- return await sendMessage({
+ return sendMessage({
type: target,
payload: {
user_info: user_info ?? {},
@@ -78,13 +41,9 @@ export async function sendCommandRequest(target: CommandRequest, values: any): P
});
}
-export async function sendMessage(
- msg: OutMessage,
- ignore_queue_request: boolean = true
-): Promise {
- const APP_VERSION = env.PUBLIC_APP_SEMVER;
+export function sendMessage(msg: OutMessage, ignore_queue_request: boolean = true): boolean {
const socket = get(socketStore);
- let data = JSON.stringify(msg);
+ const data = JSON.stringify(msg);
// console.log('try sending ', data);
@@ -105,24 +64,6 @@ export async function sendMessage(
return false;
}
- // console.log('send v2', APP_VERSION, isSecuredAppVersion(APP_VERSION));
-
- if (semver.satisfies(APP_VERSION, '>=0.0.2')) {
- // console.log('sending secured');
- let sharedKeyRes = get(sharedKey);
-
- // do encrypt
- if (sharedKeyRes != null)
- data = JSON.stringify(await WebCryptoHelper.encryptMessage(sharedKeyRes, data));
- }
-
- // console.log('[WS Send]', {
- // type: logMessage.type,
- // service: logMessage.payload?.srv_name,
- // param: logMessage.payload?.values?.param,
- // bytes: data.length,
- // secured: isSecuredAppVersion(APP_VERSION)
- // });
socket.send(data);
return true;
}
diff --git a/src/lib/core/services/sheetService.ts b/src/lib/core/services/sheetService.ts
index 9b89acd..8fb6c39 100644
--- a/src/lib/core/services/sheetService.ts
+++ b/src/lib/core/services/sheetService.ts
@@ -7,238 +7,74 @@ import {
markSheetPriceAsSent,
sheetPriceLoading,
streamingRawData,
- setPendingProductCodesCountry,
- setPendingPriceSlotsCountry,
- priceSlotsLoading,
- resetPriceSlotsCountry
+ setPendingProductCodesCountry
} from '../stores/sheetStore';
-import type { PriceSlot } from '../stores/sheetStore';
import { setGenLayoutGenerating } from '../stores/genLayoutStore';
-type SheetCellUpdate = { value: string; coord: { row: number; col: number } };
-type SheetRowUpdate = { row_index: number; cells: SheetCellUpdate[] };
-type SheetRowCreate = { header?: string[]; cells: string[] };
-
-export async function requestCatalogs(country: string): Promise {
- return await sendCommandRequest('sheet', {
+export function requestCatalogs(country: string): boolean {
+ return sendCommandRequest('sheet', {
country: country,
param: 'catalogs'
});
}
-/**
- * Register a newly created catalog as a Grist table so it shows in the overview
- * and menus can be added to it. `catalog` is the .skt filename produced by
- * /api/catalog-create (e.g. "page_catalog_group_pro_summer_splash.skt").
- */
-export async function addCatalog(
- country: string,
- catalogName: string,
- catalog: string
-): Promise {
- return await sendCommandRequest('sheet', {
+export function requestPriceSlots(country: string): boolean {
+ return sendCommandRequest('sheet', {
country: country,
- catalog: catalog,
- catalog_name: catalogName,
- param: 'add/catalog'
+ param: 'priceslot'
});
}
-export async function requestPriceSlots(country: string): Promise {
- setPendingPriceSlotsCountry(country);
- resetPriceSlotsCountry(country);
- return requestPriceSlotOption(country, 'PriceSlot');
-}
-
-export async function requestPriceSlot(country: string, slotNumber: number): Promise {
- setPendingPriceSlotsCountry(country);
- return requestPriceSlotOption(country, `PriceSlot${slotNumber}`);
-}
-
-async function requestPriceSlotOption(country: string, option: string): Promise {
- const request_id = crypto.randomUUID();
-
- streamingRawData.update((data) => ({
- ...data,
- priceslot: {
- request_id,
- country,
- chunks: [],
- rawParts: []
- }
- }));
- priceSlotsLoading.set(true);
-
- const values = {
- country: country,
- param: 'price',
- option,
- stream: true,
- request_id
- };
- console.log('[sheetService] Sending PriceSlot request:', values);
- const sent = await sendCommandRequest('sheet', values);
- console.log('[sheetService] PriceSlot request sent:', sent);
- if (!sent) {
- priceSlotsLoading.set(false);
+export function updatePriceSlot(
+ country: string,
+ content: {
+ slot: number;
+ name: string;
+ description: string;
+ products: { product_code: string; price: number | null; row_index?: number }[];
}
- return sent;
-}
-
-export async function refreshPriceSlotList(country: string): Promise {
- return requestPriceSlotOption(country, 'PriceSlot');
-}
-
-export async function updatePriceSlot(
- country: string,
- slot: PriceSlot,
- content: SheetRowUpdate[]
-): Promise {
- // console.log('[sheetService] Sending PriceSlot update:', {
- // country,
- // slot: slot.slot,
- // name: slot.name,
- // description: slot.description,
- // kind: slot.kind,
- // rows: content.length,
- // param: 'update/price',
- // option: `PriceSlot${slot.slot}`
- // });
-
- const sent = await sendCommandRequest('sheet', {
+): boolean {
+ return sendCommandRequest('sheet', {
country: country,
content: content,
- param: 'update/price',
- option: `PriceSlot${slot.slot}`
+ param: 'update/priceslot'
});
-
- console.log('[sheetService] PriceSlot update sent:', {
- country,
- slot: slot.slot,
- sent
- });
-
- return sent;
}
-export async function addPriceSlot(
- country: string,
- slot: PriceSlot,
- content: SheetRowCreate[]
-): Promise {
- console.log('[sheetService] Sending PriceSlot create:', {
- country,
- slot: slot.slot,
- name: slot.name,
- description: slot.description,
- kind: slot.kind,
- rows: content.length,
- param: 'add/price',
- option: `PriceSlot${slot.slot}`
- });
-
- const sent = await sendCommandRequest('sheet', {
- country: country,
- content: content,
- param: 'add/price',
- option: `PriceSlot${slot.slot}`
- });
-
- console.log('[sheetService] PriceSlot create sent:', {
- country,
- slot: slot.slot,
- sent
- });
-
- return sent;
-}
-
-export async function addPriceSlotRows(
- country: string,
- slot: PriceSlot,
- content: SheetRowCreate[]
-): Promise {
- if (!content || content.length === 0) return true;
-
- const sent = await sendCommandRequest('sheet', {
- country: country,
- content: content,
- param: 'add/price',
- option: `PriceSlot${slot.slot}`
- });
-
- console.log('[sheetService] PriceSlot rows add sent:', {
- country,
- slot: slot.slot,
- rows: content.length,
- sent
- });
-
- return sent;
-}
-
-export async function deletePriceSlotRows(
- country: string,
- slot: PriceSlot,
- rowIds: number[]
-): Promise {
- if (!rowIds || rowIds.length === 0) return true;
-
- const sent = await sendCommandRequest('sheet', {
- country: country,
- content: rowIds.map((target_id) => ({ target_id })),
- param: 'delete/price',
- option: `PriceSlot${slot.slot}`
- });
-
- console.log('[sheetService] PriceSlot rows delete sent:', {
- country,
- slot: slot.slot,
- rows: rowIds.length,
- sent
- });
-
- return sent;
-}
-
-export async function enterRoom(country: string, catalog: string): Promise {
- return await sendCommandRequest('sheet', {
+export function enterRoom(country: string, catalog: string): boolean {
+ return sendCommandRequest('sheet', {
country: country,
catalog: catalog,
param: 'enter'
});
}
-export async function sendHeartbeat(country: string, catalog: string): Promise {
- return await sendCommandRequest('sheet', {
+export function sendHeartbeat(country: string, catalog: string): boolean {
+ return sendCommandRequest('sheet', {
country: country,
catalog: catalog,
param: 'heartbeat'
});
}
-export async function exitRoom(country: string, catalog: string): Promise {
- return await sendCommandRequest('sheet', {
+export function exitRoom(country: string, catalog: string): boolean {
+ return sendCommandRequest('sheet', {
country: country,
catalog: catalog,
param: 'exit'
});
}
-export async function requestCatalogMenu(country: string, catalog: string): Promise {
- return await sendCommandRequest('sheet', {
+export function requestCatalogMenu(country: string, catalog: string): boolean {
+ return sendCommandRequest('sheet', {
country: country,
catalog: catalog,
param: 'catalog/menu'
});
}
-export async function updateMenu(
- country: string,
- catalog: string,
- content: any[]
-): Promise {
- return await sendCommandRequest('sheet', {
+export function updateMenu(country: string, catalog: string, content: any[]): boolean {
+ return sendCommandRequest('sheet', {
country: country,
catalog: catalog,
content: content,
@@ -246,9 +82,9 @@ export async function updateMenu(
});
}
-export async function addMenu(country: string, catalog: string, content: any[]): Promise {
+export function addMenu(country: string, catalog: string, content: any[]): boolean {
console.log('[sheetService] Adding menu:', { country, catalog, content });
- const sent = await sendCommandRequest('sheet', {
+ const sent = sendCommandRequest('sheet', {
country: country,
catalog: catalog,
content: content,
@@ -258,13 +94,9 @@ export async function addMenu(country: string, catalog: string, content: any[]):
return sent;
}
-export async function deleteMenu(
- country: string,
- catalog: string,
- targetIds: number[]
-): Promise {
+export function deleteMenu(country: string, catalog: string, targetIds: number[]): boolean {
const content = targetIds.map((id) => ({ target_id: id }));
- return await sendCommandRequest('sheet', {
+ return sendCommandRequest('sheet', {
country: country,
catalog: catalog,
content: content,
@@ -272,12 +104,12 @@ export async function deleteMenu(
});
}
-export async function swapMenu(
+export function swapMenu(
country: string,
catalog: string,
swaps: { source_id: number; target_id: number }[]
-): Promise {
- return await sendCommandRequest('sheet', {
+): boolean {
+ return sendCommandRequest('sheet', {
country: country,
catalog: catalog,
content: swaps,
@@ -285,7 +117,7 @@ export async function swapMenu(
});
}
-export async function requestListMenu(country: string, boxid?: string): Promise {
+export function requestListMenu(country: string, boxid?: string): boolean {
const curr_user = get(auth);
let user_info: any = {};
@@ -302,7 +134,7 @@ export async function requestListMenu(country: string, boxid?: string): Promise<
console.log('[sheetService] Sending list_menu request for country:', country, 'boxid:', boxid);
- return await sendMessage({
+ return sendMessage({
type: 'list_menu',
payload: {
user_info,
@@ -312,7 +144,7 @@ export async function requestListMenu(country: string, boxid?: string): Promise<
});
}
-export async function requestGenLayout(country: string): Promise {
+export function requestGenLayout(country: string): boolean {
const curr_user = get(auth);
let user_info: any = {};
@@ -328,7 +160,7 @@ export async function requestGenLayout(country: string): Promise {
console.log('[sheetService] Sending gen-layout request for country:', country);
- return await sendMessage({
+ return sendMessage({
type: 'command',
payload: {
user_info,
@@ -347,13 +179,9 @@ export async function requestGenLayout(country: string): Promise {
* Request price data from sheet for specific product codes
* NOTE: Can only send once per type (price). Use hasSheetPriceBeenSent to check.
*/
-export async function requestSheetPrice(
- country: string,
- productCodes: string[],
- force = false
-): Promise {
+export function requestSheetPrice(country: string, productCodes: string[]): boolean {
// Check if already sent
- if (!force && hasSheetPriceBeenSent('price')) {
+ if (hasSheetPriceBeenSent('price')) {
console.warn('[sheetService] Price request already sent, skipping');
return false;
}
@@ -382,61 +210,12 @@ export async function requestSheetPrice(
// Convert to array of objects (backend expects objects, not strings)
const content = productCodes.map((code) => ({ product_code: code }));
- console.log(
- '[sheetService] Sending sheet price request for country:',
- country,
- 'codes:',
- productCodes.length,
- 'request_id:',
- request_id
- );
+ console.log('[sheetService] Sending sheet price request for country:', country, 'codes:', productCodes.length, 'request_id:', request_id);
- const sent = await sendCommandRequest('sheet', {
+ const sent = sendCommandRequest('sheet', {
country: country,
content: content,
param: 'price',
- option: 'price',
- stream: true,
- request_id
- });
- console.log('[sheetService] Sheet price request sent:', { country, request_id, sent });
-
- if (sent) {
- markSheetPriceAsSent('price');
- } else {
- sheetPriceLoading.set(false);
- }
-
- return sent;
-}
-
-export async function requestAllSheetPrice(country: string, force = false): Promise {
- if (!force && hasSheetPriceBeenSent('price')) {
- console.warn('[sheetService] Price request already sent, skipping');
- return false;
- }
-
- const request_id = crypto.randomUUID();
-
- streamingRawData.update((data) => ({
- ...data,
- price: {
- request_id,
- country,
- chunks: [],
- rawParts: []
- }
- }));
-
- sheetPriceLoading.set(true);
-
- console.log('[sheetService] Sending all sheet price request:', { country, request_id });
-
- const sent = await sendCommandRequest('sheet', {
- country,
- content: [],
- param: 'price',
- option: 'price',
stream: true,
request_id
});
@@ -454,23 +233,18 @@ export async function requestAllSheetPrice(country: string, force = false): Prom
* Update price data in sheet
* content: [{ row_index: number, cells: [{ value: string, coord: { row: number, col: number } }] }]
*/
-export async function updateSheetPrice(
+export function updateSheetPrice(
country: string,
content: { row_index: number; cells: { value: string; coord: { row: number; col: number } }[] }[]
-): Promise {
+): boolean {
if (!content || content.length === 0) {
console.warn('[sheetService] No content to update');
return false;
}
- console.log(
- '[sheetService] Updating sheet price for country:',
- country,
- 'items:',
- content.length
- );
+ console.log('[sheetService] Updating sheet price for country:', country, 'items:', content.length);
- return await sendCommandRequest('sheet', {
+ return sendCommandRequest('sheet', {
country: country,
content: content,
param: 'update/price'
@@ -481,24 +255,18 @@ export async function updateSheetPrice(
* Add new price rows to sheet (for product codes that don't exist in price sheet)
* content: [{ cells: [product_code, name_en, name_th, ..., price, ...] }]
*/
-export async function addSheetPrice(
+export function addSheetPrice(
country: string,
content: { cells: string[] }[]
-): Promise {
+): boolean {
if (!content || content.length === 0) {
console.warn('[sheetService] No content to add');
return false;
}
- console.log(
- '[sheetService] Adding price rows for country:',
- country,
- 'items:',
- content.length,
- content
- );
+ console.log('[sheetService] Adding price rows for country:', country, 'items:', content.length, content);
- return await sendCommandRequest('sheet', {
+ return sendCommandRequest('sheet', {
country: country,
content: content,
param: 'add/price'
diff --git a/src/lib/core/stores/recipeStore.ts b/src/lib/core/stores/recipeStore.ts
index 7af3a35..ac21a3a 100644
--- a/src/lib/core/stores/recipeStore.ts
+++ b/src/lib/core/stores/recipeStore.ts
@@ -51,7 +51,6 @@ export const toppingGroupFromServerQuery = writable([]);
export const latestRecipeToppingData = writable([]);
// edit data update
-/// NOTE: Will be obsolete in future, and replace with `EventBus` style.
export const recipeDataEvent = writable<{
event_type: string;
payload: any;
diff --git a/src/lib/core/stores/sheetStore.ts b/src/lib/core/stores/sheetStore.ts
index cecd26f..eb2d626 100644
--- a/src/lib/core/stores/sheetStore.ts
+++ b/src/lib/core/stores/sheetStore.ts
@@ -24,304 +24,16 @@ export interface PriceSlotProduct {
row_index?: number;
}
-export interface PriceSlotServiceRow {
- row_index?: number;
- cells: Record;
-}
-
export interface PriceSlot {
slot: number;
name: string;
description: string;
- kind?: 'price' | 'service';
- header?: string[];
products: PriceSlotProduct[];
- serviceRows?: PriceSlotServiceRow[];
}
export const priceSlots = writable>({});
-export const priceSlotNamespaces = writable>({});
export const priceSlotsLoading = writable(false);
export const priceSlotsError = writable(null);
-let pendingPriceSlotsCountry = '';
-
-export function setPendingPriceSlotsCountry(country: string) {
- pendingPriceSlotsCountry = country.toLowerCase();
-}
-
-export function resetPriceSlotsCountry(country: string) {
- const key = country.toLowerCase();
- priceSlots.update((data) => ({
- ...data,
- [key]: []
- }));
- priceSlotNamespaces.update((data) => ({
- ...data,
- [key]: []
- }));
- priceSlotsError.set(null);
-}
-
-function normalizePriceSlotProduct(product: any): PriceSlotProduct | null {
- const cells = Array.isArray(product?.cells) ? product.cells : [];
- const cellValue = (col: number) => cells.find((cell: any) => cell?.coord?.col === col)?.value;
- const productCode =
- product?.product_code ?? product?.ProductCode ?? product?.code ?? cellValue(1);
-
- if (!productCode) return null;
-
- const priceValue =
- product?.price ??
- product?.Price ??
- product?.value ??
- product?.cash_price ??
- product?.CashPrice ??
- cellValue(5);
- const price =
- priceValue === '' || priceValue === undefined || priceValue === null
- ? null
- : Number(priceValue);
-
- return {
- product_code: String(productCode),
- name: String(
- product?.name ?? product?.ProductName ?? product?.product_name ?? cellValue(2) ?? ''
- ),
- price: Number.isNaN(price) ? null : price,
- row_index: product?.row_index ?? product?.row
- };
-}
-
-function getPriceSlotHeader(slot: any): string[] {
- const header = Array.isArray(slot?.header) ? slot.header : [];
- return header.map((value: any) => String(value ?? '').trim());
-}
-
-function isServicePriceSlotHeader(header: string[]): boolean {
- return header.some((value) => value.toLowerCase() === 'servicetype');
-}
-
-function normalizePriceSlotServiceRow(row: any, header: string[]): PriceSlotServiceRow | null {
- const cells = Array.isArray(row?.cells) ? row.cells : [];
- const mappedCells = header.reduce>((result, columnName, index) => {
- if (!columnName) return result;
- const value =
- row?.[columnName] ??
- row?.[columnName.replace(/\s+/g, '')] ??
- cells.find((cell: any) => cell?.coord?.col === index + 1)?.value ??
- '';
- result[columnName] = String(value ?? '');
- return result;
- }, {});
-
- if (Object.values(mappedCells).every((value) => value === '')) return null;
-
- return {
- row_index: row?.row_index ?? row?.row,
- cells: mappedCells
- };
-}
-
-function normalizePriceSlot(slot: any, index: number): PriceSlot {
- const sheetName = slot?.sheet ?? slot?.Sheet;
- const displayName = slot?.name ?? slot?.title ?? sheetName;
- const slotNumber = Number(
- slot?.slot ?? slot?.price_slot ?? slot?.id ?? displayName?.match?.(/\d+/)?.[0] ?? index + 1
- );
- const productsSource = slot?.products ?? slot?.items ?? slot?.rows ?? slot?.payload ?? [];
- const header = getPriceSlotHeader(slot);
- const isServiceSlot = isServicePriceSlotHeader(header);
- const headerName = isServiceSlot ? header[12] : header[10];
- const headerDescription = isServiceSlot ? header[13] : header[11];
- const products = (Array.isArray(productsSource) ? productsSource : [])
- .map(normalizePriceSlotProduct)
- .filter((product): product is PriceSlotProduct => product !== null);
- const serviceRows = isServiceSlot
- ? (Array.isArray(productsSource) ? productsSource : [])
- .map((row) => normalizePriceSlotServiceRow(row, header))
- .filter((row): row is PriceSlotServiceRow => row !== null)
- : [];
-
- return {
- slot: Number.isNaN(slotNumber) ? index + 1 : slotNumber,
- name: String(
- headerName ?? displayName ?? `PriceSlot${Number.isNaN(slotNumber) ? index + 1 : slotNumber}`
- ),
- description: String(headerDescription ?? ''),
- kind: isServiceSlot ? 'service' : 'price',
- header,
- products: isServiceSlot ? [] : products,
- serviceRows
- };
-}
-
-function normalizePriceSlotNamespace(sheetName: string, index: number): PriceSlot {
- const slotNumber = Number(sheetName.match(/\d+/)?.[0] ?? index + 1);
- const slot = Number.isNaN(slotNumber) ? index + 1 : slotNumber;
-
- return {
- slot,
- name: sheetName || `PriceSlot${slot}`,
- description: '',
- kind: 'price',
- header: [],
- products: []
- };
-}
-
-function getPriceSlotSource(content: any) {
- return (
- content?.priceSlots ??
- content?.priceslots ??
- content?.price_slots ??
- content?.slots ??
- content?.data ??
- content?.value ??
- content?.content ??
- content
- );
-}
-
-function getPriceSlotItems(content: any): any[] {
- const source = getPriceSlotSource(content);
-
- if (Array.isArray(source)) {
- return source.flatMap((item) => {
- if (Array.isArray(item?.sheet)) {
- return item.sheet.map((sheetName: any, index: number) =>
- normalizePriceSlotNamespace(String(sheetName ?? ''), index)
- );
- }
- return [item];
- });
- }
- if (Array.isArray(source?.sheet)) {
- return source.sheet.map((sheetName: any, index: number) =>
- normalizePriceSlotNamespace(String(sheetName ?? ''), index)
- );
- }
- if (typeof source?.sheet === 'string' && source.sheet.startsWith('PriceSlot')) return [source];
- if (typeof source === 'object' && source) {
- return Object.entries(source).map(([key, value]) => ({
- ...(typeof value === 'object' && value ? value : {}),
- name: (value as any)?.name ?? key
- }));
- }
-
- return [];
-}
-
-export function handlePriceSlotsResponse(content: any) {
- console.log('[PriceSlot] Raw backend response:', {
- items: Array.isArray(content) ? content.length : undefined,
- keys:
- content && typeof content === 'object' && !Array.isArray(content) ? Object.keys(content) : []
- });
- const country = String(
- content?.country ?? content?.Country ?? pendingPriceSlotsCountry
- ).toLowerCase();
- const source = getPriceSlotSource(content);
- const slotList = getPriceSlotItems(content);
-
- if (!country || slotList.length === 0) {
- console.warn('[PriceSlot] No slot list found:', {
- country,
- sourceItems: Array.isArray(source) ? source.length : undefined
- });
- priceSlotsError.set('No PriceSlot data found in backend response');
- priceSlotsLoading.set(false);
- return;
- }
-
- const normalizedSlots = slotList.map((slot, index) =>
- isPriceSlotNamespace(slot) ? slot : normalizePriceSlot(slot, index)
- );
-
- if (normalizedSlots.length === 0) {
- console.warn('[PriceSlot] Response did not include usable rows:', {
- country,
- slotListItems: slotList.length
- });
- return;
- }
-
- console.log('[PriceSlot] Normalized slots:', {
- country,
- slots: normalizedSlots.length,
- firstSlot: normalizedSlots[0]
- ? {
- slot: normalizedSlots[0].slot,
- name: normalizedSlots[0].name,
- kind: normalizedSlots[0].kind,
- products: normalizedSlots[0].products.length,
- serviceRows: normalizedSlots[0].serviceRows?.length ?? 0
- }
- : undefined
- });
-
- const loadedSlots = normalizedSlots.filter((slot) => !isPriceSlotNamespace(slot as any));
-
- if (loadedSlots.length > 0) {
- priceSlots.update((data) => {
- const merged = new Map();
- for (const slot of data[country] ?? []) {
- merged.set(slot.slot, slot);
- }
- for (const slot of loadedSlots) {
- merged.set(slot.slot, slot);
- }
-
- return {
- ...data,
- [country]: Array.from(merged.values()).sort((a, b) => a.slot - b.slot)
- };
- });
- }
- priceSlotNamespaces.update((data) => {
- const merged = new Map();
- for (const slot of data[country] ?? []) {
- merged.set(slot.slot, slot);
- }
- for (const slot of normalizedSlots) {
- merged.set(slot.slot, slot);
- }
-
- return {
- ...data,
- [country]: Array.from(merged.values()).sort((a, b) => a.slot - b.slot)
- };
- });
- priceSlotsError.set(null);
- priceSlotsLoading.set(false);
-}
-
-export function isPriceSlotsPayload(content: any): boolean {
- const source = getPriceSlotSource(content);
-
- if (content?.param === 'priceslot' || content?.option === 'PriceSlot') return true;
- if (Array.isArray(source?.sheet)) {
- return source.sheet.some((sheetName: any) => String(sheetName ?? '').startsWith('PriceSlot'));
- }
- if (typeof source?.sheet === 'string') return source.sheet.startsWith('PriceSlot');
- if (!Array.isArray(source)) return false;
- return source.some(
- (item) =>
- String(item?.sheet ?? item?.Sheet ?? '').startsWith('PriceSlot') ||
- (Array.isArray(item?.sheet) &&
- item.sheet.some((sheetName: any) => String(sheetName ?? '').startsWith('PriceSlot')))
- );
-}
-
-function isPriceSlotNamespace(slot: any): slot is PriceSlot {
- return (
- typeof slot?.slot === 'number' &&
- Array.isArray(slot?.products) &&
- slot.products.length === 0 &&
- Array.isArray(slot?.header) &&
- slot.header.length === 0 &&
- slot.name?.startsWith?.('PriceSlot')
- );
-}
export const countryPrimaryLanguageMap: Record = {
THAI: 'Thai',
@@ -366,19 +78,11 @@ export function getCountryPrimaryLanguage(countryCode: string): string {
// Sheet column configuration by country for new_layout_v2
// Maps language keys to column indices and product code columns
-export const SHEET_COLUMN_CONFIG_BY_COUNTRY: Record<
- string,
- {
- // Column→language map for the new-layout-v2 sheet (menu name/desc rows).
- language: Record;
- // Column→language map for the name-desc-v2 sheet (Translations). Different
- // namespace/sheet so the columns can differ from new-layout-v2; falls back
- // to `language` when not set (countries where the two are identical).
- nameDescLanguage?: Record;
- productCode: { hot: number; cold: number; blend: number };
- primaryLanguage: string;
- }
-> = {
+export const SHEET_COLUMN_CONFIG_BY_COUNTRY: Record;
+ productCode: { hot: number; cold: number; blend: number };
+ primaryLanguage: string;
+}> = {
tha: {
language: { en: 3, th: 4, zh: 5, my: 8 },
productCode: { hot: 9, cold: 10, blend: 11 },
@@ -386,7 +90,6 @@ export const SHEET_COLUMN_CONFIG_BY_COUNTRY: Record<
},
aus: {
language: { en: 3, th: 4 },
- nameDescLanguage: { en: 3, th: 4, ms: 7 },
productCode: { hot: 9, cold: 10, blend: 11 },
primaryLanguage: 'en'
},
@@ -397,13 +100,11 @@ export const SHEET_COLUMN_CONFIG_BY_COUNTRY: Record<
},
hkg: {
language: { en: 3, zh_hans: 4, zh_hant: 5, th: 6 },
- nameDescLanguage: { en: 3, zh_hans: 4, zh_hant: 5 },
productCode: { hot: 9, cold: 10, blend: 11 },
primaryLanguage: 'zh_hant'
},
ltu: {
language: { en: 3, th: 4, lt: 5, ro: 6 },
- nameDescLanguage: { en: 3, lt: 5, ro: 6 },
productCode: { hot: 9, cold: 10, blend: 11 },
primaryLanguage: 'lt'
},
@@ -429,7 +130,6 @@ export const SHEET_COLUMN_CONFIG_BY_COUNTRY: Record<
},
sgp: {
language: { en: 3, th: 4 },
- nameDescLanguage: { en: 3 },
productCode: { hot: 9, cold: 10, blend: 11 },
primaryLanguage: 'en'
},
@@ -451,10 +151,8 @@ export const SHEET_COLUMN_CONFIG_BY_COUNTRY: Record<
};
export function getSheetColumnConfig(countryCode: string) {
- return (
- SHEET_COLUMN_CONFIG_BY_COUNTRY[countryCode.toLowerCase()] ||
- SHEET_COLUMN_CONFIG_BY_COUNTRY.default
- );
+ return SHEET_COLUMN_CONFIG_BY_COUNTRY[countryCode.toLowerCase()]
+ || SHEET_COLUMN_CONFIG_BY_COUNTRY.default;
}
export function handleCatalogsResponse(content: CatalogsResponse) {
@@ -606,13 +304,10 @@ export interface SheetPriceItem {
// Price sheet header name mappings by country
// Maps our field names to the actual header names in the sheet
-export const PRICE_HEADER_NAMES_BY_COUNTRY: Record<
- string,
- {
- cash_price: string[]; // Possible header names for cash price
- non_cash_price: string[]; // Possible header names for non-cash price
- }
-> = {
+export const PRICE_HEADER_NAMES_BY_COUNTRY: Record = {
tha: {
cash_price: ['Price'],
non_cash_price: ['MainPrice']
@@ -671,7 +366,7 @@ export const PRICE_HEADER_NAMES_BY_COUNTRY: Record<
// Find column index from header array by matching header names
export function findHeaderIndex(headerArray: string[], possibleNames: string[]): number {
for (const name of possibleNames) {
- const idx = headerArray.findIndex((h) => h.toLowerCase() === name.toLowerCase());
+ const idx = headerArray.findIndex(h => h.toLowerCase() === name.toLowerCase());
if (idx !== -1) {
// Return col index (header index + 1 because cells start from col 1)
return idx + 1;
@@ -687,9 +382,7 @@ export const lastRequestSheetPrice = writable>({});
// Store: sheetPriceAllRows[country][product_code] = array of {row, cells} (ALL rows for duplicates)
-export const sheetPriceAllRows = writable<
- Record>
->({});
+export const sheetPriceAllRows = writable>>({});
// Helper function to get price value from cells using dynamic header lookup
export function getPriceFromCells(
@@ -697,43 +390,29 @@ export function getPriceFromCells(
cells: GristCell[],
priceType: 'cash_price' | 'non_cash_price' = 'cash_price'
): string | null {
- const colIdx = getPriceColumnIndex(country, priceType);
- if (colIdx < 0) return null;
-
- // Find the cell with matching column index
- const priceCell = cells.find((c) => c.coord?.col === colIdx);
- return priceCell?.value ?? null;
-}
-
-export function getPriceColumnIndex(
- country: string,
- priceType: 'cash_price' | 'non_cash_price' = 'cash_price'
-): number {
const headers = get(sheetPriceHeader)[country];
if (!headers || headers.length === 0) {
console.warn(`[getPriceFromCells] No header found for country: ${country}`);
- return -1;
+ return null;
}
// Get possible header names for this country
- const headerNames =
- PRICE_HEADER_NAMES_BY_COUNTRY[country] || PRICE_HEADER_NAMES_BY_COUNTRY.default;
- const possibleNames =
- priceType === 'cash_price' ? headerNames.cash_price : headerNames.non_cash_price;
+ const headerNames = PRICE_HEADER_NAMES_BY_COUNTRY[country] || PRICE_HEADER_NAMES_BY_COUNTRY.default;
+ const possibleNames = priceType === 'cash_price' ? headerNames.cash_price : headerNames.non_cash_price;
// Find the column index for this price type
const colIdx = findHeaderIndex(headers, possibleNames);
//console.log(`[getPriceFromCells] ${country} ${priceType}: colIdx=${colIdx}, headers=`, headers, 'possibleNames=', possibleNames);
if (colIdx < 0) {
- console.warn(
- `[getPriceFromCells] No ${priceType} column found for ${country}, tried:`,
- possibleNames
- );
- return -1;
+ console.warn(`[getPriceFromCells] No ${priceType} column found for ${country}, tried:`, possibleNames);
+ return null;
}
- return colIdx;
+ // Find the cell with matching column index
+ const priceCell = cells.find((c) => c.coord?.col === colIdx);
+ //console.log(`[getPriceFromCells] Found cell for col ${colIdx}:`, priceCell);
+ return priceCell?.value ?? null;
}
// Store for tracking streaming state
@@ -765,20 +444,15 @@ export const streamingRawData = writable<
// Handler: raw_stream header (e.g., raw_stream_price)
export function handleRawStreamHeader(subtype: string, payload: any) {
- let targetSubtype = subtype;
- const currentData = get(streamingRawData);
- if (subtype === 'price' && currentData.priceslot?.request_id === payload.request_id) {
- targetSubtype = 'priceslot';
- }
-
- console.log(`[RawStream] Header for ${targetSubtype}:`, payload);
+ console.log(`[RawStream] Header for ${subtype}:`, payload);
// Get existing stream data to preserve country from request
- const existingData = currentData[targetSubtype];
+ const currentData = get(streamingRawData);
+ const existingData = currentData[subtype];
streamingRawData.update((data) => ({
...data,
- [targetSubtype]: {
+ [subtype]: {
request_id: payload.request_id,
header: payload.header || payload.headers,
country: payload.country || existingData?.country || '',
@@ -787,7 +461,7 @@ export function handleRawStreamHeader(subtype: string, payload: any) {
}
}));
- if (targetSubtype === 'price') {
+ if (subtype === 'price') {
sheetPriceStreamMeta.set({
request_id: payload.request_id,
country: payload.country || existingData?.country || '',
@@ -799,21 +473,13 @@ export function handleRawStreamHeader(subtype: string, payload: any) {
// Handler: raw_stream chunk (e.g., raw_stream_chunk_price)
export function handleRawStreamChunk(subtype: string, payload: any) {
+ console.log(`[RawStream] Chunk ${payload.idx} for ${subtype}, raw length:`, payload.raw?.length);
+
const currentData = get(streamingRawData);
- let targetSubtype = subtype;
- if (subtype === 'price' && currentData.priceslot?.request_id === payload.request_id) {
- targetSubtype = 'priceslot';
- }
-
- console.log(
- `[RawStream] Chunk ${payload.idx} for ${targetSubtype}, raw length:`,
- payload.raw?.length
- );
-
- const streamData = currentData[targetSubtype];
+ const streamData = currentData[subtype];
if (!streamData || streamData.request_id !== payload.request_id) {
- console.warn(`[RawStream] Chunk received for unknown stream: ${targetSubtype}`);
+ console.warn(`[RawStream] Chunk received for unknown stream: ${subtype}`);
return;
}
@@ -822,13 +488,13 @@ export function handleRawStreamChunk(subtype: string, payload: any) {
// Accumulate raw parts - will be joined and parsed in handleRawStreamEnd
streamingRawData.update((data) => ({
...data,
- [targetSubtype]: {
+ [subtype]: {
...streamData,
country: payload.country || streamData.country,
rawParts: [...(streamData.rawParts || []), payload.raw]
}
}));
- console.log(`[RawStream] Accumulated chunk ${payload.idx} for ${targetSubtype}`);
+ console.log(`[RawStream] Accumulated chunk ${payload.idx} for ${subtype}`);
return;
}
@@ -838,30 +504,25 @@ export function handleRawStreamChunk(subtype: string, payload: any) {
streamingRawData.update((data) => ({
...data,
- [targetSubtype]: {
+ [subtype]: {
...streamData,
country: payload.country || streamData.country,
chunks: [...streamData.chunks, ...contentArray]
}
}));
- console.log(`[RawStream] Chunk for ${targetSubtype}: +${contentArray.length} items`);
+ console.log(`[RawStream] Chunk for ${subtype}: +${contentArray.length} items`);
}
// Handler: raw_stream end (e.g., raw_stream_end_price)
export function handleRawStreamEnd(subtype: string, payload: any) {
+ console.log(`[RawStream] End payload for ${subtype}:`, payload);
+
const currentData = get(streamingRawData);
- let targetSubtype = subtype;
- if (subtype === 'price' && currentData.priceslot?.request_id === payload.request_id) {
- targetSubtype = 'priceslot';
- }
-
- console.log(`[RawStream] End payload for ${targetSubtype}:`, payload);
-
- const streamData = currentData[targetSubtype];
+ const streamData = currentData[subtype];
if (!streamData || streamData.request_id !== payload.request_id) {
- console.warn(`[RawStream] End received for unknown stream: ${targetSubtype}`);
+ console.warn(`[RawStream] End received for unknown stream: ${subtype}`);
return;
}
@@ -893,41 +554,18 @@ export function handleRawStreamEnd(subtype: string, payload: any) {
}
}
- console.log(
- `[RawStream] End for ${targetSubtype}: total ${chunks.length} items, country: ${country}`
- );
+ console.log(`[RawStream] End for ${subtype}: total ${chunks.length} items, country: ${country}`);
- if (targetSubtype === 'priceslot' && isPriceSlotsPayload({ slots: chunks })) {
- handlePriceSlotsResponse({ country, slots: chunks });
- }
- if (targetSubtype === 'priceslot') {
- priceSlotsLoading.set(false);
- }
-
- if (targetSubtype === 'price') {
- const looksLikePriceSlot = chunks.some((item) => {
- return (
- String(item?.sheet ?? item?.Sheet ?? '').startsWith('PriceSlot') ||
- (Array.isArray(item?.sheet) &&
- item.sheet.some((sheetName: any) => String(sheetName ?? '').startsWith('PriceSlot'))) ||
- item?.option === 'PriceSlot' ||
- item?.param === 'priceslot'
- );
- });
-
- if (looksLikePriceSlot) {
- handlePriceSlotsResponse({ country, slots: chunks });
- } else {
- processSheetPriceData(country, streamData.header || [], chunks);
- sheetPriceStreamMeta.update((meta) => (meta ? { ...meta, status: 'complete' } : null));
- sheetPriceLoading.set(false);
- }
+ if (subtype === 'price') {
+ processSheetPriceData(country, streamData.header || [], chunks);
+ sheetPriceStreamMeta.update((meta) => (meta ? { ...meta, status: 'complete' } : null));
+ sheetPriceLoading.set(false);
}
// Clear the streaming data
streamingRawData.update((data) => {
const newData = { ...data };
- delete newData[targetSubtype];
+ delete newData[subtype];
return newData;
});
}
@@ -962,18 +600,8 @@ function processSheetPriceData(country: string, header: string[], chunks: any[])
// Find column indices dynamically from header
// product_code header is typically "ProductCode" or similar
- const productCodeIdx = findHeaderIndex(effectiveHeader, [
- 'ProductCode',
- 'Product_Code',
- 'product_code',
- 'Code'
- ]);
- console.log(
- `[SheetPrice] productCodeIdx from header:`,
- productCodeIdx,
- 'header:',
- effectiveHeader
- );
+ const productCodeIdx = findHeaderIndex(effectiveHeader, ['ProductCode', 'Product_Code', 'product_code', 'Code']);
+ console.log(`[SheetPrice] productCodeIdx from header:`, productCodeIdx, 'header:', effectiveHeader);
const priceByProductCode: Record = {};
// Track ALL rows per product code (for duplicates)
@@ -1074,10 +702,7 @@ function processSheetPriceData(country: string, header: string[], chunks: any[])
// Log duplicates info
const duplicates = Object.entries(allRowsByProductCode).filter(([_, rows]) => rows.length > 1);
if (duplicates.length > 0) {
- console.log(
- `[SheetPrice] Found ${duplicates.length} product codes with duplicate rows:`,
- duplicates.slice(0, 3)
- );
+ console.log(`[SheetPrice] Found ${duplicates.length} product codes with duplicate rows:`, duplicates.slice(0, 3));
}
if (chunks.length > 0 && Object.keys(priceByProductCode).length > 0) {
const sampleKey = Object.keys(priceByProductCode)[0];
@@ -1085,13 +710,6 @@ function processSheetPriceData(country: string, header: string[], chunks: any[])
}
}
-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
export function resetSheetPriceStore() {
sheetPriceStreamMeta.set(null);
@@ -1151,24 +769,14 @@ export function loadProductCodesFromCache(country?: string): boolean {
// Only load if country matches (or no country filter specified)
if (data.codes && Array.isArray(data.codes)) {
if (country && data.country && data.country !== country) {
- console.log(
- '[sheetStore] Cache is for different country:',
- data.country,
- '!= requested:',
- country
- );
+ console.log('[sheetStore] Cache is for different country:', data.country, '!= requested:', country);
// Clear the store for different country
existingProductCodes.set(new Set());
return false;
}
existingProductCodes.set(new Set(data.codes));
currentProductCodesCountry = data.country || '';
- console.log(
- '[sheetStore] Loaded',
- data.codes.length,
- 'product codes from cache for',
- data.country || 'unknown'
- );
+ console.log('[sheetStore] Loaded', data.codes.length, 'product codes from cache for', data.country || 'unknown');
return true;
}
}
@@ -1190,13 +798,7 @@ export function clearProductCodes() {
export function handleListMenuResponse(payload: { codes: string[]; country?: string }) {
// Use pending country if not in payload
const country = payload.country || pendingProductCodesCountry;
- console.log(
- '[sheetStore] Received list_menu_response for',
- country,
- ':',
- payload.codes?.length,
- 'codes'
- );
+ console.log('[sheetStore] Received list_menu_response for', country, ':', payload.codes?.length, 'codes');
if (payload && payload.codes) {
existingProductCodes.set(new Set(payload.codes));
@@ -1212,12 +814,7 @@ export function handleListMenuResponse(payload: { codes: string[]; country?: str
timestamp: Date.now()
})
);
- console.log(
- '[sheetStore] Saved',
- payload.codes.length,
- 'product codes to cache for',
- country
- );
+ console.log('[sheetStore] Saved', payload.codes.length, 'product codes to cache for', country);
} catch (e) {
console.warn('[sheetStore] Failed to save to cache:', e);
}
diff --git a/src/lib/core/stores/terminalDrawer.ts b/src/lib/core/stores/terminalDrawer.ts
deleted file mode 100644
index a41044b..0000000
--- a/src/lib/core/stores/terminalDrawer.ts
+++ /dev/null
@@ -1,28 +0,0 @@
-import { writable } from 'svelte/store';
-
-/**
- * Store for managing the terminal drawer open/closed state.
- * Also holds command history and connection status.
- */
-export const terminalDrawerOpen = writable(false);
-
-/**
- * Toggle the terminal drawer open/closed
- */
-export function toggleTerminalDrawer() {
- terminalDrawerOpen.update((v) => !v);
-}
-
-/**
- * Open the terminal drawer
- */
-export function openTerminalDrawer() {
- terminalDrawerOpen.set(true);
-}
-
-/**
- * Close the terminal drawer
- */
-export function closeTerminalDrawer() {
- terminalDrawerOpen.set(false);
-}
diff --git a/src/lib/core/stores/websocketStore.ts b/src/lib/core/stores/websocketStore.ts
index a824f8b..720fb7e 100644
--- a/src/lib/core/stores/websocketStore.ts
+++ b/src/lib/core/stores/websocketStore.ts
@@ -7,20 +7,15 @@ import { auth } from '../client/firebase';
import { auth as authStore } from '$lib/core/stores/auth';
import { addNotification } from './noti';
import { permission } from './permissions';
-import { WebCryptoHelper } from '../utils/crypto';
let socket: WebSocket | null = null;
let reconnectTimeout: any;
let socketCheck: any;
-let sendAuthInfoInterval: any;
const ENABLE_WS_DEBUG: boolean = false;
export const socketConnectionOfflineCount = writable(0);
export const socketAlreadySendHeartbeat = writable(0);
export const socketStore = writable(null);
-export const wsAuthReady = writable(false);
-
-export const sharedKey = writable(null);
export function waitForOpenSocket(timeoutMs = 8000): Promise {
const currentSocket = get(socketStore);
@@ -54,32 +49,7 @@ export function waitForOpenSocket(timeoutMs = 8000): Promise {
});
}
-export async function waitForAuthenticatedSocket(timeoutMs = 10000): Promise {
- const openSocket = await waitForOpenSocket(timeoutMs);
- if (!openSocket) return null;
- if (get(wsAuthReady)) return openSocket;
-
- return new Promise((resolve) => {
- let settled = false;
- let unsubscribe = () => {};
- const timeout = setTimeout(() => {
- if (settled) return;
- settled = true;
- unsubscribe();
- resolve(null);
- }, timeoutMs);
-
- unsubscribe = wsAuthReady.subscribe((ready) => {
- if (!ready || settled) return;
- settled = true;
- clearTimeout(timeout);
- unsubscribe();
- resolve(openSocket);
- });
- });
-}
-
-export async function connectToWebsocket(id_token?: string) {
+export function connectToWebsocket(id_token?: string) {
if (browser) {
// console.log('connecting to ', env.PUBLIC_WSS);
try {
@@ -87,13 +57,12 @@ export async function connectToWebsocket(id_token?: string) {
return;
}
- let ws_url = env.PUBLIC_WSS;
- socket = new WebSocket(ws_url);
- wsAuthReady.set(false);
- sharedKey.set(null);
- const { privateKey, publicKeyBase64 } = await WebCryptoHelper.generateKeyPair();
+ let productionMode = env.PUBLIC_WSS.startsWith('wss');
- socket.addEventListener('open', async () => {
+ let ws_url = productionMode ? `${env.PUBLIC_WSS}?token=${id_token}` : `${env.PUBLIC_WSS}`;
+ socket = new WebSocket(ws_url);
+
+ socket.addEventListener('open', () => {
socketStore.set(socket);
addNotification('INFO:Connected!');
@@ -105,45 +74,29 @@ export async function connectToWebsocket(id_token?: string) {
let auth_data = get(authStore);
let perms = get(permission);
- socket.send(
- JSON.stringify({
- token: id_token ?? '',
- client_public_key: publicKeyBase64,
- client_version: env.PUBLIC_APP_SEMVER
- })
- );
+ // Debug: check if auth_data has uid
+ console.log('[WS Auth] Sending auth with:', {
+ uid: auth_data?.uid,
+ name: auth_data?.displayName,
+ email: auth_data?.email
+ });
- sendAuthInfoInterval = setInterval(async () => {
- if (get(sharedKey)) {
- auth_data = get(authStore);
- perms = get(permission);
- // Debug: check if auth_data has uid
- console.log('[WS Auth] Sending auth info with:', {
- uid: auth_data?.uid,
- name: auth_data?.displayName,
- email: auth_data?.email,
- date: new Date()
- });
- const sent = await sendMessage({
- type: 'auth',
- payload: {
- user: {
- uid: auth_data?.uid ?? '',
- name: auth_data?.displayName ?? '',
- email: auth_data?.email ?? '',
- permissions: perms.join(',')
- }
- }
- });
- wsAuthReady.set(sent);
- clearInterval(sendAuthInfoInterval);
+ sendMessage({
+ type: 'auth',
+ payload: {
+ user: {
+ uid: auth_data?.uid ?? '',
+ name: auth_data?.displayName ?? '',
+ email: auth_data?.email ?? '',
+ permissions: perms.join(',')
+ }
}
- }, 2000);
+ });
}
console.log(socket);
// heartbeat 10s
- socketCheck = setInterval(async () => {
+ socketCheck = setInterval(() => {
if (get(socketAlreadySendHeartbeat) > 0) {
let heartbeat_may_offline_count = get(socketConnectionOfflineCount);
@@ -155,14 +108,13 @@ export async function connectToWebsocket(id_token?: string) {
socketConnectionOfflineCount.set(0);
socketAlreadySendHeartbeat.set(0);
- id_token = await auth.currentUser?.getIdToken(true);
- await connectToWebsocket(id_token);
+ connectToWebsocket(id_token);
return;
}
if (socket != null) {
- await sendMessage({
+ sendMessage({
type: 'heartbeat',
payload: {}
});
@@ -178,41 +130,32 @@ export async function connectToWebsocket(id_token?: string) {
if (auth.currentUser && socket == null) {
console.log('try reconnect websocket ...');
// retry again
- reconnectTimeout = setTimeout(async () => {
- id_token = await auth.currentUser?.getIdToken(true);
- await connectToWebsocket(id_token);
+ reconnectTimeout = setTimeout(() => {
+ connectToWebsocket(id_token);
}, 5000);
}
});
- socket.addEventListener('message', async (event) => {
- await handleIncomingMessages(event.data, privateKey);
+ socket.addEventListener('message', (event) => {
+ handleIncomingMessages(event.data);
});
socket.addEventListener('close', () => {
socketStore.set(null);
- wsAuthReady.set(false);
- sharedKey.set(null);
socket = null;
clearInterval(socketCheck);
- clearInterval(sendAuthInfoInterval);
if (auth.currentUser && !socket) {
console.log('try reconnect websocket ...');
// retry again
- reconnectTimeout = setTimeout(async () => {
- id_token = await auth.currentUser?.getIdToken(true);
- await connectToWebsocket(id_token);
- }, 5000);
+ reconnectTimeout = setTimeout(() => connectToWebsocket(id_token), 5000);
}
});
socket.addEventListener('error', (e) => {
// console.log('WebSocket error: ', e);
socketStore.set(null);
- wsAuthReady.set(false);
- sharedKey.set(null);
});
} catch (socket_error: any) {
if (ENABLE_WS_DEBUG) {
diff --git a/src/lib/core/types/outMessage.ts b/src/lib/core/types/outMessage.ts
index 13dff0e..f51bd46 100644
--- a/src/lib/core/types/outMessage.ts
+++ b/src/lib/core/types/outMessage.ts
@@ -1,5 +1,4 @@
export type OutMessage =
- | { token: any; client_public_key: any }
| { type: 'chat'; payload: string }
| { type: 'ping' }
| { type: 'lock'; payload: { field: string } }
@@ -48,14 +47,14 @@ export type OutMessage =
payload: {};
}
| {
- type: 'sheet' | 'command' | 'upload-log';
+ type: 'sheet' | 'command';
payload: {
user_info: any;
srv_name: string;
values: any;
};
}
- | {
+ | {
type: 'list_menu';
payload: {
user_info: any;
@@ -63,6 +62,7 @@ export type OutMessage =
boxid?: string;
};
}
+
| {
type: 'price';
payload: {
diff --git a/src/lib/core/utils/crypto.ts b/src/lib/core/utils/crypto.ts
deleted file mode 100644
index 7cbe33f..0000000
--- a/src/lib/core/utils/crypto.ts
+++ /dev/null
@@ -1,78 +0,0 @@
-export class WebCryptoHelper {
- private static bytesToBase64(bytes: Uint8Array) {
- const chunkSize = 0x8000;
- let binary = '';
- for (let i = 0; i < bytes.length; i += chunkSize) {
- binary += String.fromCharCode(...bytes.subarray(i, i + chunkSize));
- }
-
- return btoa(binary);
- }
-
- static async generateKeyPair() {
- const keyPair = await window.crypto.subtle.generateKey(
- {
- name: 'ECDH',
- namedCurve: 'P-256'
- },
- true,
- ['deriveKey', 'deriveBits']
- );
-
- const exportedPublic = await window.crypto.subtle.exportKey('raw', keyPair.publicKey);
- const publicKeyBase64 = WebCryptoHelper.bytesToBase64(new Uint8Array(exportedPublic));
-
- return { privateKey: keyPair.privateKey, publicKeyBase64 };
- }
-
- static async deriveSharedKey(clientPrivateKey: any, serverPublicKeyBase64: any) {
- const binarySign = atob(serverPublicKeyBase64);
- const bytes = new Uint8Array(binarySign.length);
- for (let i = 0; i < binarySign.length; i++) {
- bytes[i] = binarySign.charCodeAt(i);
- }
-
- const importedServerPublic = await window.crypto.subtle.importKey(
- 'raw',
- bytes,
- { name: 'ECDH', namedCurve: 'P-256' },
- true,
- []
- );
- return await window.crypto.subtle.deriveKey(
- { name: 'ECDH', public: importedServerPublic },
- clientPrivateKey,
- { name: 'AES-GCM', length: 256 },
- true,
- ['encrypt', 'decrypt']
- );
- }
-
- static async decryptMessage(aesKey: any, ciphertextBase64: any, ivBase64: any) {
- const rawCipher = Uint8Array.from(atob(ciphertextBase64), (c) => c.charCodeAt(0));
- const rawIv = Uint8Array.from(atob(ivBase64), (c) => c.charCodeAt(0));
- const decryptedBuffer = await window.crypto.subtle.decrypt(
- { name: 'AES-GCM', iv: rawIv },
- aesKey,
- rawCipher
- );
- return new TextDecoder().decode(decryptedBuffer);
- }
-
- // Encrypt outgoing messages before sending them to your Axum backend
- static async encryptMessage(aesKey: any, plainText: any) {
- const iv = window.crypto.getRandomValues(new Uint8Array(12)); // 12-byte nonce
- const encodedText = new TextEncoder().encode(plainText);
-
- const ciphertextBuffer = await window.crypto.subtle.encrypt(
- { name: 'AES-GCM', iv: iv },
- aesKey,
- encodedText
- );
-
- const ciphertextBase64 = WebCryptoHelper.bytesToBase64(new Uint8Array(ciphertextBuffer));
- const ivBase64 = WebCryptoHelper.bytesToBase64(iv);
-
- return { ciphertext: ciphertextBase64, iv: ivBase64 };
- }
-}
diff --git a/src/lib/core/utils/eventBus.ts b/src/lib/core/utils/eventBus.ts
deleted file mode 100644
index ce7e6c5..0000000
--- a/src/lib/core/utils/eventBus.ts
+++ /dev/null
@@ -1,117 +0,0 @@
-import { getContext, setContext } from 'svelte';
-
-const COMMON_BUS = Symbol('g-event');
-
-class EventBus {
- #listeners = new Map();
-
- /**
- * Register event with callback on this channel
- * @param event
- * @param callback
- * @returns unsubscribe function of this event, remove callback out of this event
- */
- on(event: string, callback: any) {
- if (!this.#listeners.has(event)) {
- this.#listeners.set(event, new Set());
- }
-
- this.#listeners.get(event).add(callback);
-
- // return unsubscribe
- return () => this.#listeners.get(event).delete(callback);
- }
-
- /**
- * Emit data to this event, call every registered callbacks
- * @param event
- * @param data
- */
- emit(event: string, data: any) {
- if (this.#listeners.has(event)) {
- this.#listeners.get(event).forEach((cb: any) => cb(data));
- }
- }
-
- emitUntilConsumed(event: string, data: any, timeout?: number) {
- if (this.#listeners.has(event)) {
- let listener_count = this.#listeners.get(event).length;
-
- if (listener_count == 0) {
- setTimeout(
- () => {
- this.emitUntilConsumed(event, data);
- },
- (timeout ?? 1) * 1000
- );
- } else {
- this.emit(event, data);
- }
- }
- }
-
- /**
- * Clear all listeners
- */
- clear() {
- this.#listeners.clear();
- }
-
- /**
- * Clear all callbacks on this event
- * @param event
- */
- resetCallbackOnEvent(event: string) {
- if (this.#listeners.has(event)) {
- this.#listeners.set(event, new Set());
- }
- }
-}
-
-/**
- * Initialize the common channel event bus
- * @returns EventBus | undefined
- */
-function setEventBus(): EventBus | undefined {
- return setContext(COMMON_BUS, new EventBus());
-}
-
-/**
- * Get common channel event bus, cannot be used in non-component
- * @returns EventBus | undefined
- */
-function useEventBus(): EventBus | undefined {
- return getContext(COMMON_BUS);
-}
-
-/**
- * Initialize the channel with name event bus
- * @param name channel name
- * @returns EventBus | undefined
- */
-function setEventBusWithName(name: string): EventBus | undefined {
- return setContext(name, new EventBus());
-}
-
-/**
- * Get a specific channel event bus, cannot be used in non-component
- * @param name channel name
- * @returns EventBus | undefined
- */
-function useEventBusWithName(name: string): EventBus | undefined {
- return getContext(name);
-}
-
-/**
- * Global type event bus, allow use without Svelte context
- */
-const GlobalEventBus = new EventBus();
-
-export {
- setEventBus,
- useEventBus,
- setEventBusWithName,
- useEventBusWithName,
- COMMON_BUS,
- GlobalEventBus
-};
diff --git a/src/lib/data/recipeService.ts b/src/lib/data/recipeService.ts
index 7791fdf..778698d 100644
--- a/src/lib/data/recipeService.ts
+++ b/src/lib/data/recipeService.ts
@@ -272,7 +272,5 @@ export {
getMaterialType,
getCategories,
isNonMaterial,
- extractMaterialIdFromDisplay,
- getMenuStatus,
- buildTags
+ extractMaterialIdFromDisplay
};
diff --git a/src/routes/(authed)/+layout.svelte b/src/routes/(authed)/+layout.svelte
index 0d38883..926c2d3 100644
--- a/src/routes/(authed)/+layout.svelte
+++ b/src/routes/(authed)/+layout.svelte
@@ -18,22 +18,10 @@
} from '@yume-chan/adb-daemon-webusb';
import AdbWebCredentialStore from '@yume-chan/adb-credential-web';
import { deviceCredentialManager } from '$lib/core/adb/deviceCredManager';
- import { browser } from '$app/environment';
- import { onMount } from 'svelte';
let { children } = $props();
let websocketConnectedForUid = $state('');
let adbReconnectTriedForUid = $state('');
- let TerminalDrawerComponent: any = $state(null);
-
- // Dynamic import: TerminalDrawer depends on @xterm/xterm which references
- // browser globals (self, window). Static import would crash SSR evaluation.
- onMount(async () => {
- if (browser) {
- const mod = await import('$lib/components/terminal-drawer.svelte');
- TerminalDrawerComponent = mod.default;
- }
- });
function getAutoConnectChannel(pathname: string) {
if (pathname.startsWith('/tools/create-menu')) {
@@ -109,8 +97,8 @@
websocketConnectedForUid = currentUser.uid;
console.log('connect ws after auth ready');
- void currentUser.getIdToken(true).then(async (idToken) => {
- await connectToWebsocket(idToken);
+ void currentUser.getIdToken().then((idToken) => {
+ connectToWebsocket(idToken);
});
}
@@ -137,8 +125,4 @@
{@render children()}
-
- {#if TerminalDrawerComponent}
-
- {/if}
diff --git a/src/routes/(authed)/departments/+page.svelte b/src/routes/(authed)/departments/+page.svelte
index 31a295c..77ce3d9 100644
--- a/src/routes/(authed)/departments/+page.svelte
+++ b/src/routes/(authed)/departments/+page.svelte
@@ -27,8 +27,6 @@
if (refPage === 'priceslot') {
await goto(`/sheet/priceslot/${cnt}`);
- } else if (refPage === 'price') {
- await goto(`/sheet/price/${cnt}`);
} else if (refPage === 'sheet') {
await goto(`/sheet/overview/${cnt}`);
} else {
@@ -39,7 +37,7 @@
// read or write permission
let userCurrentPerms = get(currentPerms).filter((x) => {
- if (refPage === 'sheet' || refPage === 'priceslot' || refPage === 'price') {
+ if (refPage === 'sheet') {
return x.startsWith('document.write');
}
return x.startsWith('document.read');
@@ -52,7 +50,7 @@
setTimeout(() => {
// read or write permission
let userCurrentPerms = get(currentPerms).filter((x) => {
- if (refPage === 'sheet' || refPage === 'priceslot' || refPage === 'price') {
+ if (refPage === 'sheet') {
return x.startsWith('document.write');
}
return x.startsWith('document.read');
diff --git a/src/routes/(authed)/recipe/material/+page.svelte b/src/routes/(authed)/recipe/material/+page.svelte
index a91f747..e7beba8 100644
--- a/src/routes/(authed)/recipe/material/+page.svelte
+++ b/src/routes/(authed)/recipe/material/+page.svelte
@@ -1,5 +1,5 @@
@@ -939,93 +459,21 @@
{/if}
-
-
- Recipe Source
-
-
-
-
-
-
+
+
-
-
-
- Load Recipe From Server
- Select a country to load material data from server.
-
-
-
- {#each serverCountries as country}
-
- {/each}
-
-
-
-
-
-
-
-
-
-
-
- Machine Not Connected
-
- Connect to the machine with ADB/WebUSB before loading recipe data from Machine.
-
-
-
-
-
-
-
-
-
@@ -1051,60 +499,31 @@
- {isEditingMaterial ? 'Edit Material' : 'Add Material'}
+ {existingMaterial ? 'Edit Material' : 'Add Material'}
- Create or update one MaterialSetting entry. Server-loaded data is read-only until
- ADB is connected.
+ Create or update one MaterialSetting entry. The JSON preview shows the payload
+ before saving to Android.
@@ -1269,46 +713,14 @@
-
-
-
- Select Material Type
-
- Choose the type for the new material. Material ID will be generated from existing IDs in
- that type + 1.
-
-
-
-
- {#each selectableMaterialTypeOptions as option}
- selectMaterialType(option.value)}
- >
-
@@ -1334,10 +955,7 @@
{existingGroup ? 'Edit Topping Group' : 'Add Topping Group'}
-
- Manage one group inside ToppingGroup. Server-loaded data is read-only until
- ADB is connected.
-
+ Manage one group inside ToppingGroup.