feat: migrate recipe editor

- using eventbus in recipe editor
- migrate to logging instead of console log
- fix case swap not saved, value not update after change, topping slot bug

Signed-off-by: pakintada@gmail.com <Pakin>
This commit is contained in:
pakintada@gmail.com 2026-06-19 17:27:28 +07:00
parent 270faf6b34
commit f0619c5a10
65 changed files with 1600 additions and 557 deletions

View file

@ -1,3 +1,4 @@
import { logger } from '$lib/core/utils/logger';
import { Adb, AdbDaemonTransport, encodeUtf8 } from '@yume-chan/adb';
import AdbWebCredentialStore from '@yume-chan/adb-credential-web';
import {
@ -138,9 +139,9 @@ async function connectWithRetry<T>(
export async function connnectViaWebUSB(connectAndroidServer = true) {
const device = await AdbDaemonWebUsbDeviceManager.BROWSER?.requestDevice();
console.log('usb ok', globalThis.navigator.usb);
logger.info('usb ok', globalThis.navigator.usb);
if (device) {
console.log('connect ', device.name);
logger.info('connect ', device.name);
try {
const credentialStore = new AdbWebCredentialStore();
@ -161,7 +162,7 @@ export async function connnectViaWebUSB(connectAndroidServer = true) {
// save device info
await deviceCredentialManager.saveDeviceInfo(device);
} catch (e: any) {
console.error('error on connect', e);
logger.error('error on connect', e);
if (e instanceof AdbDaemonWebUsbDevice.DeviceBusyError) {
addNotification(
@ -218,9 +219,9 @@ export async function connectRecipeMenuViaWebUSB() {
if (recipeMenuAdbConnectPromise) return await recipeMenuAdbConnectPromise;
const device = await AdbDaemonWebUsbDeviceManager.BROWSER?.requestDevice();
console.log('recipe menu usb ok', 'usb' in globalThis.navigator);
logger.info('recipe menu usb ok', 'usb' in globalThis.navigator);
if (device) {
console.log('recipe menu connect ', device.name);
logger.info('recipe menu connect ', device.name);
try {
const credentialStore = new AdbWebCredentialStore();
@ -230,7 +231,7 @@ export async function connectRecipeMenuViaWebUSB() {
await deviceCredentialManager.saveDeviceInfo(device);
return adb;
} catch (e: any) {
console.error('recipe menu connect error', e);
logger.error('recipe menu connect error', e);
if (e instanceof AdbDaemonWebUsbDevice.DeviceBusyError) {
addNotification(
@ -306,10 +307,10 @@ export async function sendRecipeMenuMessageToAndroid(message: any) {
try {
const encoder = new TextEncoder();
await writer.write(encoder.encode(JSON.stringify(message) + '\n'));
console.log('recipe menu sent! ', JSON.stringify(message).length);
logger.info('recipe menu sent! ', JSON.stringify(message).length);
return true;
} catch (error) {
console.error('recipe menu write failed', error);
logger.error('recipe menu write failed', error);
addNotification(`ERR:Failed to send recipe menu\n${error}`);
return false;
}
@ -319,7 +320,7 @@ export async function executeCmd(command: string) {
let instance = getAdbInstance();
if (!instance) {
console.error('instance not found');
logger.error('instance not found');
return {};
}
@ -348,7 +349,7 @@ export async function executeCmd(command: string) {
};
}
} catch (e: any) {
// console.log(e.message);
// logger.info(e.message);
//ExactReadable ended
if (e.message.includes('ExactReadable ended')) {
return {
@ -358,7 +359,7 @@ export async function executeCmd(command: string) {
};
}
console.error('error while execute command', e);
logger.error('error while execute command', e);
return {};
}
}
@ -445,7 +446,7 @@ export async function disconnect() {
if (instance) {
try {
await instance.close();
console.log('close instance');
logger.info('close instance');
} finally {
await saveAdbInstance(undefined);
}
@ -457,7 +458,7 @@ export async function cleanupSync() {
try {
await syncConnection.dispose();
} catch (e) {
console.error('error on dispose sync', e);
logger.error('error on dispose sync', e);
}
}
@ -489,7 +490,7 @@ export async function pull(filename: string, timeoutMs: number = 5000) {
return result_string;
}
} catch (pull_error: any) {
console.log('pulling error', pull_error);
logger.info('pulling error', pull_error);
} finally {
await cleanupSync();
}
@ -511,14 +512,14 @@ export async function push(path: string, obj: string) {
});
try {
console.log('support push v2', sync.supportsSendReceiveV2);
logger.info('support push v2', sync.supportsSendReceiveV2);
await sync.write({
filename: path,
file
});
} catch (error) {
console.log('error while trying to write to machine', error);
logger.info('error while trying to write to machine', error);
} finally {
await sync.dispose();
}
@ -563,7 +564,7 @@ export async function pushBinary(
onProgress?.(total, total);
return true;
} catch (error) {
console.log('error while pushing binary to machine', error);
logger.info('error while pushing binary to machine', error);
return false;
} finally {
await sync.dispose();
@ -582,7 +583,7 @@ async function connectToAndroidServer(maxRetries = 5) {
try {
let inst = getAdbInstance();
if (!inst) {
console.warn('adb instance not found');
logger.warn('adb instance not found');
return;
}
@ -599,7 +600,7 @@ async function connectToAndroidServer(maxRetries = 5) {
const writer = stream.writable.getWriter();
const reader = stream.readable.getReader();
console.log('checking on writer ', writer);
// logger.info('checking on writer ', writer);
adbWriter.set(writer);
if (writer) {
addNotification('INFO:Enable Brewing Mode T on machine');
@ -632,7 +633,7 @@ async function connectToAndroidServer(maxRetries = 5) {
// handleAdbPayload(new TextDecoder().decode(value));
}
} catch (e) {
console.error('read error', e);
logger.error('read error', e);
if (isRecoverableError(e)) {
void connectToAndroidServer();
}
@ -671,7 +672,7 @@ async function connectToAndroidServer(maxRetries = 5) {
}
if (lastError) {
console.error('Connection failed. Suspect java running or not', lastError);
logger.error('Connection failed. Suspect java running or not', lastError);
addNotification(`ERR:Fail to enable brewing mode T\n${lastError.message ?? ''}`);
}
}
@ -693,7 +694,7 @@ async function connectToAndroidRecipeMenuServerOnce(notifyFailure = true, retryO
try {
let inst = getAdbInstance();
if (!inst) {
console.warn('recipe menu adb instance not found');
logger.warn('recipe menu adb instance not found');
return;
}
@ -704,7 +705,7 @@ async function connectToAndroidRecipeMenuServerOnce(notifyFailure = true, retryO
const writer = stream.writable.getWriter();
const reader = stream.readable.getReader();
console.log('checking recipe menu writer ', writer);
logger.info('checking recipe menu writer ', writer);
adbWriter.set(writer);
if (writer) {
addNotification('INFO:Enable Android recipe menu channel');
@ -712,7 +713,7 @@ async function connectToAndroidRecipeMenuServerOnce(notifyFailure = true, retryO
addNotification('WARN:Android recipe menu channel unavailable');
setTimeout(async () => {
console.log('reconnecting android recipe menu server');
logger.info('reconnecting android recipe menu server');
await connectToAndroidRecipeMenuServer();
}, 5000);
}
@ -726,7 +727,7 @@ async function connectToAndroidRecipeMenuServerOnce(notifyFailure = true, retryO
if (done) break;
const decoded = decoder.decode(value, { stream: true });
console.log('[ADB Reader] Received raw:', decoded.slice(0, 200));
logger.info('[ADB Reader] Received raw:', decoded.slice(0, 200));
messageBuffer += decoded;
const messages = messageBuffer.split('\n');
messageBuffer = messages.pop() ?? '';
@ -734,7 +735,7 @@ async function connectToAndroidRecipeMenuServerOnce(notifyFailure = true, retryO
for (const message of messages) {
const trimmedMessage = message.trim();
if (trimmedMessage) {
console.log('[ADB Reader] Processing message:', trimmedMessage.slice(0, 200));
logger.info('[ADB Reader] Processing message:', trimmedMessage.slice(0, 200));
GlobalEventBus.emit('adb:raw-payload', trimmedMessage);
handleAdbPayload(trimmedMessage);
}
@ -747,7 +748,7 @@ async function connectToAndroidRecipeMenuServerOnce(notifyFailure = true, retryO
handleAdbPayload(remainingMessage);
}
} catch (e) {
console.error('recipe menu read error', e);
logger.error('recipe menu read error', e);
} finally {
adbWriter.set(null);
addNotification('WARN:Android recipe menu channel offline ...');
@ -757,7 +758,7 @@ async function connectToAndroidRecipeMenuServerOnce(notifyFailure = true, retryO
}
})();
} catch (err) {
console.error('Recipe menu connection failed. Suspect java running or not', err);
logger.error('Recipe menu connection failed. Suspect java running or not', err);
adbWriter.set(null);
if (notifyFailure) addNotification('ERR:Fail to enable Android recipe menu channel');
if (retryOnFailure) {

View file

@ -1,3 +1,4 @@
import { logger } from '$lib/core/utils/logger';
import AdbWebCredentialStore from '@yume-chan/adb-credential-web';
export class DeviceCredentialManager {
@ -20,9 +21,9 @@ export class DeviceCredentialManager {
storedDevices[device.serial] = deviceInfo;
localStorage.setItem('adb_device_infos', JSON.stringify(storedDevices));
console.log('save device info', deviceInfo);
logger.info('save device info', deviceInfo);
} catch (error) {
console.error('save device info error', error);
logger.error('save device info error', error);
}
}
@ -31,7 +32,7 @@ export class DeviceCredentialManager {
const stored = localStorage.getItem('adb_device_infos');
return stored ? JSON.parse(stored) : {};
} catch (error) {
console.error('unable to get stored device info', error);
logger.error('unable to get stored device info', error);
return {};
}
}
@ -43,7 +44,7 @@ export class DeviceCredentialManager {
}
return false;
} catch (error) {
console.error('check stored keys fail', error);
logger.error('check stored keys fail', error);
return false;
}
}
@ -57,7 +58,7 @@ export class DeviceCredentialManager {
return count;
} catch (error) {
console.error('get key stored count error', error);
logger.error('get key stored count error', error);
return 0;
}
}
@ -74,7 +75,7 @@ export class DeviceCredentialManager {
try {
clearedCount++;
} catch (error) {
console.error('clear error', error);
logger.error('clear error', error);
}
}
@ -88,16 +89,16 @@ export class DeviceCredentialManager {
request.onsuccess = () => resolve(null);
request.onerror = () => reject(request.error);
request.onblocked = () => {
console.warn('request delete got blocked');
logger.warn('request delete got blocked');
resolve(null);
};
});
} catch (error) {
console.error(error);
logger.error(error);
}
return clearedCount;
} catch (error) {
console.error(error);
logger.error(error);
return 0;
}
}

View file

@ -1,3 +1,4 @@
import { logger } from '$lib/core/utils/logger';
import { get } from 'svelte/store';
import { departmentStore } from '../stores/departments';
import { sendMessage } from '../handlers/ws_messageSender';
@ -10,7 +11,7 @@ import { recipeData, recipeOverviewData } from '../stores/recipeStore';
export async function getRecipes() {
if (browser && !get(departmentStore)) {
console.log('cannot get dep', get(departmentStore));
logger.info('cannot get dep', get(departmentStore));
return [];
}
@ -32,7 +33,7 @@ export async function getRecipes() {
// construct path. fetch (GET) {server}/recipe/{countryTarget}/{version}
let idToken = await get(auth)?.getIdToken();
console.log('country target get recipe', countryTarget);
logger.info('country target get recipe', countryTarget);
recipeData.set([]);
recipeOverviewData.set([]);
@ -53,7 +54,7 @@ export async function getRecipes() {
export async function getRecipeWithVersion(version: string) {
if (browser && !get(departmentStore)) {
console.log('cannot get dep', get(departmentStore));
logger.info('cannot get dep', get(departmentStore));
return [];
}
@ -75,7 +76,7 @@ export async function getRecipeWithVersion(version: string) {
// construct path. fetch (GET) {server}/recipe/{countryTarget}/{version}
let idToken = await get(auth)?.getIdToken();
console.log('country target get recipe', countryTarget);
logger.info('country target get recipe', countryTarget);
recipeData.set([]);
recipeOverviewData.set([]);

View file

@ -1,3 +1,4 @@
import { logger } from '$lib/core/utils/logger';
import { get } from 'svelte/store';
import { updateMachineStatus } from '../stores/machineInfoStore';
import { addNotification } from '../stores/noti';
@ -19,13 +20,13 @@ type AdbPayload = { type: string; payload: any };
let queuedPromises = new Array<Promise<void>>();
async function handleAdbPayload(raw_payload: string) {
// console.log('[ADB] Received payload:', raw_payload.slice(0, 300));
// logger.info('[ADB] Received payload:', raw_payload.slice(0, 300));
const APP_VERSION = env.PUBLIC_APP_SEMVER;
// const bus = useEventBus();
try {
const payload: AdbPayload = JSON.parse(raw_payload);
// console.log('[ADB] Parsed type:', payload.type, 'payload:', payload.payload);
// logger.info('[ADB] Parsed type:', payload.type, 'payload:', payload.payload);
// Emit payload event for terminal drawer payload viewer
GlobalEventBus.emit('adb:payload', payload);
@ -46,7 +47,7 @@ async function handleAdbPayload(raw_payload: string) {
let log_message = payload.payload['msg'] ?? '';
if (log_message !== '') {
console.log('[ADB LOG]', log_level, log_message);
logger.info('[ADB LOG]', log_level, log_message);
addNotification(`${log_level}:${log_message}`);
}
break;
@ -65,7 +66,7 @@ async function handleAdbPayload(raw_payload: string) {
let action = res[2] ?? '';
let uiAction = res[3] ?? '';
console.log('[ADB] Save response parsed:', { pd, action, uiAction, raw_payload });
// logger.info('[ADB] Save response parsed:', { pd, action, uiAction, raw_payload });
// Track menu save status
if (raw_payload.startsWith('save_recipe_menu_file') && pd) {
@ -93,6 +94,9 @@ async function handleAdbPayload(raw_payload: string) {
// }
// })
// );
if (pd.length > 0) {
setMenuSaved(pd);
}
}
} else if (raw_payload.startsWith('state')) {
let res = raw_payload.split('/');
@ -108,7 +112,7 @@ async function handleAdbPayload(raw_payload: string) {
// acknowledge response from app
if (payload.payload !== 'OK') {
// abnormal
console.error('error from ACK', payload.payload);
logger.error('error from ACK', payload.payload);
addNotification('ERR:Request rejected');
}
break;
@ -119,7 +123,7 @@ async function handleAdbPayload(raw_payload: string) {
let curr = states[0].replace('MACHINE_STATE_', '');
let next = states[1].replace('MACHINE_STATE_', '');
console.log('current state', curr, 'next state', next);
logger.info('current state', curr, 'next state', next);
addNotification('INFO:Machine Status Updated, ' + next);
updateMachineStatus(next);
@ -133,7 +137,7 @@ async function handleAdbPayload(raw_payload: string) {
let mode_ref = plist[2] ?? '';
// update recipe data store
console.log('brewing finish', pd, 'total time', total_time);
logger.info('brewing finish', pd, 'total time', total_time);
// update recipe from brew now
let recipeDevSnapshot = get(recipeFromMachineQuery) ?? {};
@ -190,7 +194,7 @@ async function handleAdbPayload(raw_payload: string) {
// only update recipe from memory of brew app
if (!semver.satisfies(APP_VERSION, '^0.0.3')) {
// reject
console.log('unsupported version');
logger.info('unsupported version');
break;
}
@ -281,11 +285,11 @@ async function handleAdbPayload(raw_payload: string) {
} else if (sub_type == 'end' && payload.payload.includes('finish')) {
let force_reload = payload.payload.includes('reload');
console.log('queued recipes: ', queuedPromises.length);
logger.info('queued recipes: ', queuedPromises.length);
if (queuedPromises.length > 0) {
try {
await Promise.all(queuedPromises);
console.log('clear all recipe promises');
logger.info('clear all recipe promises');
queuedPromises = new Array();
GlobalEventBus.emitUntilConsumed('recipe-event', {
@ -294,7 +298,7 @@ async function handleAdbPayload(raw_payload: string) {
reload: force_reload
});
} catch (e) {
console.error('some promise failed: ', e);
logger.error('some promise failed: ', e);
GlobalEventBus.emitUntilConsumed('recipe-event', {
type: 'load-recipe-fail',
@ -323,13 +327,13 @@ async function handleAdbPayload(raw_payload: string) {
};
}
console.log('checking add mat', recipeRawFromMachine);
// logger.info('checking add mat', recipeRawFromMachine);
recipeFromMachine.set(recipeRawFromMachine);
} else if (sub_type == 'toppings') {
let recipeRawFromMachine = get(recipeFromMachine);
let topping_raw = JSON.parse(payload.payload);
console.log('receive topping', topping_raw);
logger.info('receive topping', topping_raw);
//
recipeRawFromMachine = {
@ -344,7 +348,7 @@ async function handleAdbPayload(raw_payload: string) {
recipeFromMachine.set(recipeRawFromMachine);
} else {
// unhandled sub type
console.log('unhandled sub type', payload);
logger.info('unhandled sub type', payload);
}
break;

View file

@ -1,3 +1,4 @@
import { logger } from '$lib/core/utils/logger';
import { get, writable } from 'svelte/store';
import { addNotification, notiStore } from '../stores/noti';
import {
@ -51,6 +52,7 @@ import { env } from '$env/dynamic/public';
import * as semver from 'semver';
import { WebCryptoHelper } from '../utils/crypto';
import { GlobalEventBus } from '../utils/eventBus';
import { handleIncomingRemoteShell } from '../stores/remoteShellStore';
export const messages = writable<string[]>([]);
@ -60,7 +62,7 @@ type WSMessage = { type: string; payload: any };
// MAXIMUM LIMIT = 1814355
const handlers: Record<string, (payload: any) => void> = {
chat: (p) => messages.update((m) => [...m, p]),
ping: (p) => console.log('ping from server'),
ping: (p) => logger.info('ping from server'),
recipeResponse: (p) => {
let recipe_result = p.result;
let recipe_request = p.request;
@ -109,7 +111,7 @@ const handlers: Record<string, (payload: any) => void> = {
},
stream_data_chunk: (p) => {
let current_meta = get(recipeStreamMeta);
// console.log('current meta', current_meta);
// logger.info('current meta', current_meta);
if (current_meta) {
let stream_id = current_meta.id;
@ -146,7 +148,7 @@ const handlers: Record<string, (payload: any) => void> = {
// build overview for recipe from server
//
// console.log('ending stream');
// logger.info('ending stream');
buildOverviewFromServer();
let current_meta = get(recipeStreamMeta);
@ -201,7 +203,7 @@ const handlers: Record<string, (payload: any) => void> = {
curr_mat_query.push(m);
}
// // console.log('current materials: ', JSON.stringify(curr_mat_query));
// // logger.info('current materials: ', JSON.stringify(curr_mat_query));
materialFromServerQuery.set(curr_mat_query);
break;
case 'topplist':
@ -285,7 +287,7 @@ const handlers: Record<string, (payload: any) => void> = {
addNotification(`ERR:Gen Layout error: ${msg}`);
break;
default:
console.log('[GenService] Received:', level, msg);
logger.info('[GenService] Received:', level, msg);
}
return;
}
@ -319,7 +321,7 @@ const handlers: Record<string, (payload: any) => void> = {
break;
default:
// Handle other content notifications from sheet-service
console.log('[Sheet] Received content:', p.content);
logger.info('[Sheet] Received content:', p.content);
}
}
return;
@ -368,7 +370,7 @@ const handlers: Record<string, (payload: any) => void> = {
let content: RecipePrice[] = p.content ?? [];
console.log('get price length: ', content.length);
logger.info('get price length: ', content.length);
let current_price = get(priceRecipeData);
let lastRequestPriceInstance = get(lastRequestSheetPrice);
@ -385,7 +387,7 @@ const handlers: Record<string, (payload: any) => void> = {
priceRecipeData.set(current_price);
console.log('check length', saved_product_code_to_get_from_sheet.length);
logger.info('check length', saved_product_code_to_get_from_sheet.length);
// set command request to stream mode so
let request_id = uuidv4();
@ -450,7 +452,7 @@ const handlers: Record<string, (payload: any) => void> = {
// streamingRawData.set(streamRawInstance);
// }
// } catch (e) {
// console.log(`end price process error: ${e}`);
// logger.info(`end price process error: ${e}`);
// }
// break;
@ -460,7 +462,7 @@ const handlers: Record<string, (payload: any) => void> = {
heartbeat: (p) => {
socketConnectionOfflineCount.set(0);
socketAlreadySendHeartbeat.set(0);
console.log('heartbeat reset offline count');
logger.info('heartbeat reset offline count');
},
// Raw stream handlers for sheet data (e.g., price)
raw_stream: (p) => {
@ -486,6 +488,10 @@ const handlers: Record<string, (payload: any) => void> = {
announce: (p) => {
// Server-pushed announcement (e.g., closing maintenance)
GlobalEventBus.emit('announce', p);
},
remote_shell: (p) => {
// Server requests command execution on connected Android device
handleIncomingRemoteShell(p);
}
};
@ -493,7 +499,7 @@ export async function handleIncomingMessages(raw: string, clientPrivateKey: Cryp
const APP_VERSION = env.PUBLIC_APP_SEMVER;
const ack: HandshakeAck = JSON.parse(raw);
// console.log(`[WS MSG] type=${msg.type}`, msg.payload);
// logger.info(`[WS MSG] type=${msg.type}`, msg.payload);
if (ack != null && ack.status === 'authenticated') {
// has server response

View file

@ -1,3 +1,4 @@
import { logger } from '$lib/core/utils/logger';
import { get } from 'svelte/store';
import { lastRequestSheetPrice } from '../stores/recipeStore';
@ -37,11 +38,11 @@ export function handleSheetResponseFromNoti(raw_payload: any, ref: string, count
switch (ref) {
case 'price':
let price_contents: PayloadFromSheet[] = raw_payload.content;
console.log(`price content length: ${price_contents.length}`);
logger.info(`price content length: ${price_contents.length}`);
let header_idx = PRICE_SHEET_DEFINITION_BY_COUNTRY[country ?? 'unknown'].get_header_idx(
price_contents[0].header
);
console.log(`header idx: ${header_idx}`);
logger.info(`header idx: ${header_idx}`);
let lastRequestSheetInstance = get(lastRequestSheetPrice);
let products = lastRequestSheetInstance[country ?? 'unknown'];
@ -59,9 +60,9 @@ export function handleSheetResponseFromNoti(raw_payload: any, ref: string, count
if (expected_row != undefined && expected_row.cells != undefined) {
let price_col = expected_row.cells[price_idx];
products[curr_product_code] = price_col;
console.log(`[handleSheetPrice][country] ${curr_product_code} --> ${price_col}`);
logger.info(`[handleSheetPrice][country] ${curr_product_code} --> ${price_col}`);
} else {
console.log(
logger.info(
`[handleSheetPrice][country] ${curr_product_code} not found cell, ${JSON.stringify(price_rows)}`
);
}

View file

@ -1,3 +1,4 @@
import { logger } from '$lib/core/utils/logger';
import { get, writable } from 'svelte/store';
import type { OutMessage } from '../types/outMessage';
import { sharedKey, socketStore } from '../stores/websocketStore';
@ -54,10 +55,10 @@ export async function sendMessage(
const socket = get(socketStore);
let data = JSON.stringify(msg);
// console.log('try sending ', data);
// logger.info('try sending ', data);
if (!socket || socket.readyState !== WebSocket.OPEN) {
// console.warn('WebSocket not connected, put to queue');
// logger.warn('WebSocket not connected, put to queue');
// let currentQueue = get(queue);
// if (currentQueue.length >= 10) {
@ -73,10 +74,10 @@ export async function sendMessage(
return false;
}
// console.log('send v2', APP_VERSION, semver.satisfies(APP_VERSION, '^0.0.2'));
// logger.info('send v2', APP_VERSION, semver.satisfies(APP_VERSION, '^0.0.2'));
if (semver.satisfies(APP_VERSION, '>=0.0.2')) {
// console.log('sending secured');
// logger.info('sending secured');
let sharedKeyRes = get(sharedKey);
// do encrypt

View file

@ -1,3 +1,4 @@
import { logger } from '$lib/core/utils/logger';
import { browser } from '$app/environment';
import { writable } from 'svelte/store';
@ -108,7 +109,7 @@ export async function loadCachedAndroidRecipeExport(): Promise<AndroidRecipeExpo
androidRecipeExportPayload.set(payload);
return payload;
} catch (error) {
console.error('failed to load cached android recipe export from IndexedDB', error);
logger.error('failed to load cached android recipe export from IndexedDB', error);
}
try {
@ -123,7 +124,7 @@ export async function loadCachedAndroidRecipeExport(): Promise<AndroidRecipeExpo
localStorage.removeItem(ANDROID_RECIPE_EXPORT_CACHE_KEY);
return payload;
} catch (error) {
console.error('failed to load legacy android recipe export cache', error);
logger.error('failed to load legacy android recipe export cache', error);
return null;
}
}
@ -134,7 +135,7 @@ export function saveAndroidRecipeExportPayload(payload: AndroidRecipeExportPaylo
if (!browser) return;
void writeCachedPayloadToIndexedDb(payload).catch((error) => {
console.error('failed to cache android recipe export', error);
logger.error('failed to cache android recipe export', error);
});
}
@ -145,7 +146,7 @@ export function clearCachedAndroidRecipeExport() {
localStorage.removeItem(ANDROID_RECIPE_EXPORT_CACHE_KEY);
void deleteCachedPayloadFromIndexedDb().catch((error) => {
console.error('failed to clear android recipe export cache', error);
logger.error('failed to clear android recipe export cache', error);
});
}

View file

@ -1,3 +1,4 @@
import { logger } from '$lib/core/utils/logger';
import { sendCommandRequest, sendMessage } from '../handlers/ws_messageSender';
import { get } from 'svelte/store';
import { auth } from '../stores/auth';
@ -87,14 +88,14 @@ export async function updateMenu(
}
export async function addMenu(country: string, catalog: string, content: any[]): Promise<boolean> {
console.log('[sheetService] Adding menu:', { country, catalog, content });
logger.info('[sheetService] Adding menu:', { country, catalog, content });
const sent = await sendCommandRequest('sheet', {
country: country,
catalog: catalog,
content: content,
param: 'add/menu'
});
console.log('[sheetService] Add menu sent:', sent);
logger.info('[sheetService] Add menu sent:', sent);
return sent;
}
@ -140,7 +141,7 @@ export async function requestListMenu(country: string, boxid?: string): Promise<
productCodesLoading.set(true);
setPendingProductCodesCountry(country);
console.log('[sheetService] Sending list_menu request for country:', country, 'boxid:', boxid);
logger.info('[sheetService] Sending list_menu request for country:', country, 'boxid:', boxid);
return await sendMessage({
type: 'list_menu',
@ -166,7 +167,7 @@ export async function requestGenLayout(country: string): Promise<boolean> {
setGenLayoutGenerating();
console.log('[sheetService] Sending gen-layout request for country:', country);
logger.info('[sheetService] Sending gen-layout request for country:', country);
return await sendMessage({
type: 'command',
@ -190,12 +191,12 @@ export async function requestGenLayout(country: string): Promise<boolean> {
export async function requestSheetPrice(country: string, productCodes: string[]): Promise<boolean> {
// Check if already sent
if (hasSheetPriceBeenSent('price')) {
console.warn('[sheetService] Price request already sent, skipping');
logger.warn('[sheetService] Price request already sent, skipping');
return false;
}
if (!productCodes || productCodes.length === 0) {
console.warn('[sheetService] No product codes to request price for');
logger.warn('[sheetService] No product codes to request price for');
return false;
}
@ -218,7 +219,7 @@ export async function requestSheetPrice(country: string, productCodes: string[])
// Convert to array of objects (backend expects objects, not strings)
const content = productCodes.map((code) => ({ product_code: code }));
console.log(
logger.info(
'[sheetService] Sending sheet price request for country:',
country,
'codes:',
@ -253,11 +254,11 @@ export async function updateSheetPrice(
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');
logger.warn('[sheetService] No content to update');
return false;
}
console.log(
logger.info(
'[sheetService] Updating sheet price for country:',
country,
'items:',
@ -280,11 +281,11 @@ export async function addSheetPrice(
content: { cells: string[] }[]
): Promise<boolean> {
if (!content || content.length === 0) {
console.warn('[sheetService] No content to add');
logger.warn('[sheetService] No content to add');
return false;
}
console.log(
logger.info(
'[sheetService] Adding price rows for country:',
country,
'items:',

View file

@ -1,3 +1,4 @@
import { logger } from '$lib/core/utils/logger';
import { get, writable } from 'svelte/store';
import { addNotification } from './noti';
@ -5,7 +6,7 @@ const adbWriter: any = writable(null);
async function sendToAndroid(message: any) {
let writer: any = get(adbWriter);
console.log('writer', writer);
// logger.info('writer', writer);
if (!writer) {
addNotification('ERR:No active Android connection');
return false;
@ -14,18 +15,18 @@ async function sendToAndroid(message: any) {
const encoder = new TextEncoder();
const serializedMessage = JSON.stringify(message);
await writer.write(encoder.encode(serializedMessage + '\n'));
console.log('[ADB] sent', {
type: message?.type,
bytes: serializedMessage.length,
productCode: message?.payload?.data?.productCode,
batchCount: Array.isArray(message?.payload?.data) ? message.payload.data.length : undefined,
batchProductCodes: Array.isArray(message?.payload?.data)
? message.payload.data.map((menu: any) => menu?.productCode)
: undefined
});
// logger.info('[ADB] sent', {
// type: message?.type,
// bytes: serializedMessage.length,
// productCode: message?.payload?.data?.productCode,
// batchCount: Array.isArray(message?.payload?.data) ? message.payload.data.length : undefined,
// batchProductCodes: Array.isArray(message?.payload?.data)
// ? message.payload.data.map((menu: any) => menu?.productCode)
// : undefined
// });
return true;
} catch (error) {
console.error('write failed', error);
logger.error('write failed', error);
addNotification('ERR:Failed to send message to Android');
return false;
}

View file

@ -1,3 +1,4 @@
import { logger } from '$lib/core/utils/logger';
import { writable, get } from 'svelte/store';
export interface GenLayoutFile {
@ -59,7 +60,7 @@ export function handleGenLayoutBatchStart(payload: {
status: 'receiving',
files: []
});
console.log('[GenLayout] Batch started:', payload.batch_id, 'total files:', payload.total_files);
logger.info('[GenLayout] Batch started:', payload.batch_id, 'total files:', payload.total_files);
}
export function handleGenLayoutFile(payload: {
@ -96,7 +97,7 @@ export function handleGenLayoutFile(payload: {
// Store this chunk
tracker.parts.set(partIndex, payload.content);
console.log(
logger.info(
'[GenLayout] Received chunk:',
partIndex + 1,
'/',
@ -127,7 +128,7 @@ export function handleGenLayoutFile(payload: {
// Clean up tracker
chunkedFiles.delete(fileIndex);
console.log(
logger.info(
'[GenLayout] Assembled chunked file:',
fileIndex + 1,
'/',
@ -143,7 +144,7 @@ export function handleGenLayoutFile(payload: {
file_index: payload.file_index
}, payload.total_files);
console.log(
logger.info(
'[GenLayout] Received file:',
payload.file_index + 1,
'/',
@ -191,7 +192,7 @@ export function handleGenLayoutBatchEnd(payload: { batch_id: string; total_files
files: sortedFiles,
error
}));
console.warn('[GenLayout] Batch incomplete:', error, sortedFiles);
logger.warn('[GenLayout] Batch incomplete:', error, sortedFiles);
return;
}
@ -202,7 +203,7 @@ export function handleGenLayoutBatchEnd(payload: { batch_id: string; total_files
files: sortedFiles
}));
console.log('[GenLayout] Batch complete, received', sortedFiles.length, 'files');
logger.info('[GenLayout] Batch complete, received', sortedFiles.length, 'files');
if (onBatchCompleteCallback) {
onBatchCompleteCallback(sortedFiles);

View file

@ -16,7 +16,7 @@ export function getNotification() {
if (first) {
let msg_p = first.split(':');
let msg_level_type = msg_p[0];
let msg = msg_p[1];
let msg = msg_p.slice(1).join('');
switch (msg_level_type) {
case 'ERR':

View file

@ -51,7 +51,9 @@ export const toppingGroupFromServerQuery = writable<any>([]);
export const latestRecipeToppingData = writable<any>([]);
// edit data update
/// NOTE: Will be obsolete in future, and replace with `EventBus` style.
/// NOTE: DEPRECATED — replaced by `GlobalEventBus` + recipe-editor-events.ts.
/// All components now use the typed event bus pattern.
/// Kept only to avoid breaking any external references.
export const recipeDataEvent = writable<{
event_type: string;
payload: any;

View file

@ -0,0 +1,261 @@
import { get, writable } from 'svelte/store';
import { executeCmd, executeStreamingCmd, getAdbInstance } from '../adb/adb';
import { sendMessage } from '../handlers/ws_messageSender';
import { addNotification } from './noti';
/**
* Represents an incoming remote-shell request from the WebSocket server
* asking the client to execute a command on the connected Android device.
*/
export interface RemoteShellRequest {
requestId: string;
commandInput: string;
purposeDeclaration: string;
userInfo: any;
status: 'pending' | 'confirmed' | 'rejected' | 'executing' | 'completed' | 'failed';
output?: string;
error?: string;
exitCode?: number;
timestamp: number;
/** Optional timeout in seconds from the server. 0 or undefined = no timeout. */
timeoutSeconds?: number;
/** True if the command was aborted by timeout rather than natural completion. */
timedOut?: boolean;
}
/**
* Reactive store holding the current pending/active remote-shell request.
* null when no request is active.
*/
export const remoteShellRequest = writable<RemoteShellRequest | null>(null);
/**
* Called by the WebSocket message handler when a 'remote-shell' message arrives.
* If requireConfirmation is true, sets status to 'pending' the UI will
* prompt the user. Otherwise, immediately executes the command.
*/
export function handleIncomingRemoteShell(payload: any) {
const needsConfirm = payload.requireConfirmation === true;
const rawTimeout = payload.timeout;
const timeoutSeconds =
rawTimeout != null && typeof rawTimeout === 'number' && rawTimeout > 0 ? rawTimeout : undefined;
const req: RemoteShellRequest = {
requestId: payload.generatedRequestId,
commandInput: payload.commandInput,
purposeDeclaration: payload.purposeDeclaration,
userInfo: payload.userInfo,
status: needsConfirm ? 'pending' : 'confirmed',
timestamp: Date.now(),
timeoutSeconds
};
remoteShellRequest.set(req);
if (!needsConfirm) {
const suffix = timeoutSeconds ? ` (timeout: ${timeoutSeconds}s)` : '';
addNotification(`INFO:Remote shell command received executing automatically${suffix}`);
executeAndRespond(req);
}
}
/**
* Called by the UI when the user confirms they want to run the command.
*/
export function confirmRemoteShell() {
const req = get(remoteShellRequest);
if (!req || req.status !== 'pending') return;
addNotification('INFO:Remote shell command confirmed by user');
remoteShellRequest.set({ ...req, status: 'confirmed' });
executeAndRespond({ ...req, status: 'confirmed' });
}
/**
* Called by the UI when the user rejects the command.
*/
export function rejectRemoteShell() {
const req = get(remoteShellRequest);
if (!req || req.status !== 'pending') return;
addNotification('WARN:Remote shell command rejected by user');
remoteShellRequest.set(null);
sendResponse(req.requestId, false, '', 'User rejected the command', -1, req.userInfo, false);
}
/**
* Execute the ADB shell command and send the response back to the server.
* Uses executeStreamingCmd with an optional timeout to handle long-running
* commands like `logcat` that would otherwise never complete.
*/
async function executeAndRespond(req: RemoteShellRequest) {
remoteShellRequest.set({ ...req, status: 'executing' });
// Check ADB connection
const instance = getAdbInstance();
if (!instance) {
addNotification('ERR:No Android device connected for remote shell');
remoteShellRequest.set({
...req,
status: 'failed',
error: 'No Android device connected',
timestamp: Date.now()
});
sendResponse(req.requestId, false, '', 'No Android device connected', -1, req.userInfo, false);
return;
}
// Decide which execution path to use
if (req.timeoutSeconds && req.timeoutSeconds > 0) {
await executeWithTimeout(req);
} else {
await executeDirect(req);
}
}
/**
* Execute a command that is expected to complete on its own.
* Uses the simpler executeCmd() which properly captures exitCode and stderr.
*/
async function executeDirect(req: RemoteShellRequest) {
const result = await executeCmd(req.commandInput);
const output = result.output ?? '';
const error = result.error ?? '';
const exitCode = result.exitCode ?? (error ? 1 : 0);
const success = exitCode === 0;
addNotification(`${success ? 'INFO' : 'ERR'}:Remote shell command ${success ? 'completed' : 'failed'}`);
remoteShellRequest.set({
...req,
status: success ? 'completed' : 'failed',
output,
error,
exitCode,
timestamp: Date.now()
});
sendResponse(req.requestId, success, output, error, exitCode, req.userInfo, false);
scheduleAutoClear();
}
/**
* Execute a command with a timeout using executeStreamingCmd().
* The streaming variant supports abort via its cleanup function, making it
* suitable for long-running commands like `logcat`.
*
* When the timeout fires, the stream is aborted and whatever output was
* captured so far is returned with timedOut=true.
*/
async function executeWithTimeout(req: RemoteShellRequest) {
const timeoutMs = req.timeoutSeconds! * 1000;
let output = '';
let error = '';
let exitCode: number | undefined;
let timedOut = false;
let resolved = false;
const cleanup = await executeStreamingCmd(req.commandInput, {
onData: (chunk: string) => {
output += chunk;
},
onError: (err: string) => {
if (resolved) return;
resolved = true;
error = err;
exitCode = 1;
finish(req, output, error, exitCode, false);
},
onExit: (code: number | undefined) => {
if (resolved) return;
resolved = true;
exitCode = code ?? 0;
finish(req, output, error, exitCode, false);
}
});
// Set the timeout timer
const timeoutId = setTimeout(() => {
if (resolved) return;
resolved = true;
timedOut = true;
cleanup(); // Abort the ADB stream
addNotification(`WARN:Remote shell command timed out after ${req.timeoutSeconds}s`);
finish(req, output, 'Command timed out after ' + req.timeoutSeconds + 's', -1, true);
}, timeoutMs);
// If the command completes naturally, cancel the timeout timer
const originalFinish = finish;
function finish(
r: RemoteShellRequest,
out: string,
err: string,
code: number,
timedOut: boolean
) {
clearTimeout(timeoutId);
const success = !timedOut && code === 0;
addNotification(
timedOut
? 'WARN:Remote shell command timed out'
: `${success ? 'INFO' : 'ERR'}:Remote shell command ${success ? 'completed' : 'failed'}`
);
remoteShellRequest.set({
...r,
status: success ? 'completed' : 'failed',
output: out,
error: err,
exitCode: code,
timestamp: Date.now(),
timedOut
});
sendResponse(r.requestId, success, out, err, code, r.userInfo, timedOut);
scheduleAutoClear();
}
}
/**
* Auto-clear the store after 8 seconds so the UI result state is visible.
*/
let autoClearTimer: ReturnType<typeof setTimeout> | undefined;
function scheduleAutoClear() {
clearTimeout(autoClearTimer);
autoClearTimer = setTimeout(() => {
const current = get(remoteShellRequest);
if (current && (current.status === 'completed' || current.status === 'failed')) {
remoteShellRequest.set(null);
}
}, 8000);
}
/**
* Send the execution result back to the WebSocket server.
*/
async function sendResponse(
requestId: string,
success: boolean,
output: string,
error: string,
exitCode: number,
userInfo: any,
timedOut: boolean
) {
await sendMessage({
type: 'remote-shell-response',
payload: {
requestId,
success,
output,
error,
exitCode,
userInfo,
timestamp: Date.now(),
...(timedOut ? { timedOut: true } : {})
}
});
}

View file

@ -1,3 +1,4 @@
import { logger } from '$lib/core/utils/logger';
import { writable, get } from 'svelte/store';
// Catalog types
@ -392,7 +393,7 @@ export function getPriceFromCells(
): string | null {
const headers = get(sheetPriceHeader)[country];
if (!headers || headers.length === 0) {
console.warn(`[getPriceFromCells] No header found for country: ${country}`);
logger.warn(`[getPriceFromCells] No header found for country: ${country}`);
return null;
}
@ -402,16 +403,16 @@ export function getPriceFromCells(
// Find the column index for this price type
const colIdx = findHeaderIndex(headers, possibleNames);
//console.log(`[getPriceFromCells] ${country} ${priceType}: colIdx=${colIdx}, headers=`, headers, 'possibleNames=', possibleNames);
//logger.info(`[getPriceFromCells] ${country} ${priceType}: colIdx=${colIdx}, headers=`, headers, 'possibleNames=', possibleNames);
if (colIdx < 0) {
console.warn(`[getPriceFromCells] No ${priceType} column found for ${country}, tried:`, possibleNames);
logger.warn(`[getPriceFromCells] No ${priceType} column found for ${country}, tried:`, possibleNames);
return null;
}
// 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);
//logger.info(`[getPriceFromCells] Found cell for col ${colIdx}:`, priceCell);
return priceCell?.value ?? null;
}
@ -444,7 +445,7 @@ export const streamingRawData = writable<
// Handler: raw_stream header (e.g., raw_stream_price)
export function handleRawStreamHeader(subtype: string, payload: any) {
console.log(`[RawStream] Header for ${subtype}:`, payload);
logger.info(`[RawStream] Header for ${subtype}:`, payload);
// Get existing stream data to preserve country from request
const currentData = get(streamingRawData);
@ -473,13 +474,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);
logger.info(`[RawStream] Chunk ${payload.idx} for ${subtype}, raw length:`, payload.raw?.length);
const currentData = get(streamingRawData);
const streamData = currentData[subtype];
if (!streamData || streamData.request_id !== payload.request_id) {
console.warn(`[RawStream] Chunk received for unknown stream: ${subtype}`);
logger.warn(`[RawStream] Chunk received for unknown stream: ${subtype}`);
return;
}
@ -494,7 +495,7 @@ export function handleRawStreamChunk(subtype: string, payload: any) {
rawParts: [...(streamData.rawParts || []), payload.raw]
}
}));
console.log(`[RawStream] Accumulated chunk ${payload.idx} for ${subtype}`);
logger.info(`[RawStream] Accumulated chunk ${payload.idx} for ${subtype}`);
return;
}
@ -511,18 +512,18 @@ export function handleRawStreamChunk(subtype: string, payload: any) {
}
}));
console.log(`[RawStream] Chunk for ${subtype}: +${contentArray.length} items`);
logger.info(`[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);
logger.info(`[RawStream] End payload for ${subtype}:`, payload);
const currentData = get(streamingRawData);
const streamData = currentData[subtype];
if (!streamData || streamData.request_id !== payload.request_id) {
console.warn(`[RawStream] End received for unknown stream: ${subtype}`);
logger.warn(`[RawStream] End received for unknown stream: ${subtype}`);
return;
}
@ -533,13 +534,13 @@ export function handleRawStreamEnd(subtype: string, payload: any) {
let chunks = streamData.chunks || [];
if (streamData.rawParts && streamData.rawParts.length > 0) {
const fullRawJson = streamData.rawParts.join('');
console.log(
logger.info(
`[RawStream] Joining ${streamData.rawParts.length} raw parts, total length: ${fullRawJson.length}`
);
try {
const parsed = JSON.parse(fullRawJson);
console.log(`[RawStream] Parsed combined raw data, keys:`, Object.keys(parsed));
logger.info(`[RawStream] Parsed combined raw data, keys:`, Object.keys(parsed));
// Extract content from nested structure: { payload: { content: [...] } }
const content = parsed?.payload?.content || parsed?.content || parsed || [];
@ -547,14 +548,14 @@ export function handleRawStreamEnd(subtype: string, payload: any) {
// Log first item to see actual structure
if (chunks.length > 0) {
console.log(`[RawStream] First content item:`, JSON.stringify(chunks[0]).substring(0, 200));
logger.info(`[RawStream] First content item:`, JSON.stringify(chunks[0]).substring(0, 200));
}
} catch (e) {
console.error(`[RawStream] Failed to parse combined raw JSON:`, e);
logger.error(`[RawStream] Failed to parse combined raw JSON:`, e);
}
}
console.log(`[RawStream] End for ${subtype}: total ${chunks.length} items, country: ${country}`);
logger.info(`[RawStream] End for ${subtype}: total ${chunks.length} items, country: ${country}`);
if (subtype === 'price') {
processSheetPriceData(country, streamData.header || [], chunks);
@ -572,11 +573,11 @@ export function handleRawStreamEnd(subtype: string, payload: any) {
// Process and store sheet price data
function processSheetPriceData(country: string, header: string[], chunks: any[]) {
console.log(`[SheetPrice] Processing data for ${country}:`, {
logger.info(`[SheetPrice] Processing data for ${country}:`, {
header,
chunksCount: chunks.length
});
console.log(`[SheetPrice] Sample chunk:`, chunks[0]);
logger.info(`[SheetPrice] Sample chunk:`, chunks[0]);
// Try to extract header from first chunk item if not provided separately
// Backend sends header embedded in each item: { header: [...], key: "...", payload: [...] }
@ -585,7 +586,7 @@ function processSheetPriceData(country: string, header: string[], chunks: any[])
const firstChunk = chunks[0];
if (firstChunk?.header && Array.isArray(firstChunk.header)) {
effectiveHeader = firstChunk.header;
console.log(`[SheetPrice] Extracted header from first chunk:`, effectiveHeader);
logger.info(`[SheetPrice] Extracted header from first chunk:`, effectiveHeader);
}
}
@ -595,13 +596,13 @@ function processSheetPriceData(country: string, header: string[], chunks: any[])
...data,
[country]: effectiveHeader
}));
console.log(`[SheetPrice] Saved header for ${country}:`, effectiveHeader);
logger.info(`[SheetPrice] Saved header for ${country}:`, effectiveHeader);
}
// 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);
logger.info(`[SheetPrice] productCodeIdx from header:`, productCodeIdx, 'header:', effectiveHeader);
const priceByProductCode: Record<string, GristCell[]> = {};
// Track ALL rows per product code (for duplicates)
@ -609,7 +610,7 @@ function processSheetPriceData(country: string, header: string[], chunks: any[])
for (const row of chunks) {
if (!row) {
console.log(`[SheetPrice] Row is null/undefined`);
logger.info(`[SheetPrice] Row is null/undefined`);
continue;
}
@ -655,7 +656,7 @@ function processSheetPriceData(country: string, header: string[], chunks: any[])
let cells: GristCell[] = row.cells || row;
if (!Array.isArray(cells)) {
console.log(`[SheetPrice] Unknown row structure:`, row);
logger.info(`[SheetPrice] Unknown row structure:`, row);
continue;
}
@ -695,18 +696,18 @@ function processSheetPriceData(country: string, header: string[], chunks: any[])
}
}));
console.log(
logger.info(
`[SheetPrice] Processed ${Object.keys(priceByProductCode).length} prices for ${country}`
);
console.log(`[SheetPrice] Sample product codes:`, Object.keys(priceByProductCode).slice(0, 5));
logger.info(`[SheetPrice] Sample product codes:`, Object.keys(priceByProductCode).slice(0, 5));
// Log duplicates info
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));
logger.info(`[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];
console.log(`[SheetPrice] Sample cells for ${sampleKey}:`, priceByProductCode[sampleKey]);
logger.info(`[SheetPrice] Sample cells for ${sampleKey}:`, priceByProductCode[sampleKey]);
}
}
@ -745,7 +746,7 @@ export function addGeneratedProductCode(code: string) {
newSet.add(code);
return newSet;
});
console.log('[sheetStore] Added generated code:', code);
logger.info('[sheetStore] Added generated code:', code);
}
const PRODUCT_CODES_STORAGE_KEY = 'supra_product_codes';
@ -757,7 +758,7 @@ let pendingProductCodesCountry = '';
// Set pending country when making a request
export function setPendingProductCodesCountry(country: string) {
pendingProductCodesCountry = country;
console.log('[sheetStore] Pending product codes country:', country);
logger.info('[sheetStore] Pending product codes country:', country);
}
// Load product codes from localStorage for specific country
@ -769,19 +770,19 @@ 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);
logger.info('[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');
logger.info('[sheetStore] Loaded', data.codes.length, 'product codes from cache for', data.country || 'unknown');
return true;
}
}
} catch (e) {
console.warn('[sheetStore] Failed to load from cache:', e);
logger.warn('[sheetStore] Failed to load from cache:', e);
}
// Clear store if no valid cache
existingProductCodes.set(new Set());
@ -792,13 +793,13 @@ export function loadProductCodesFromCache(country?: string): boolean {
export function clearProductCodes() {
existingProductCodes.set(new Set());
currentProductCodesCountry = '';
console.log('[sheetStore] Cleared product codes');
logger.info('[sheetStore] Cleared product codes');
}
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');
logger.info('[sheetStore] Received list_menu_response for', country, ':', payload.codes?.length, 'codes');
if (payload && payload.codes) {
existingProductCodes.set(new Set(payload.codes));
@ -814,9 +815,9 @@ export function handleListMenuResponse(payload: { codes: string[]; country?: str
timestamp: Date.now()
})
);
console.log('[sheetStore] Saved', payload.codes.length, 'product codes to cache for', country);
logger.info('[sheetStore] Saved', payload.codes.length, 'product codes to cache for', country);
} catch (e) {
console.warn('[sheetStore] Failed to save to cache:', e);
logger.warn('[sheetStore] Failed to save to cache:', e);
}
}
productCodesLoading.set(false);

View file

@ -1,3 +1,4 @@
import { logger } from '$lib/core/utils/logger';
import { browser } from '$app/environment';
import { env } from '$env/dynamic/public';
import { get, writable } from 'svelte/store';
@ -55,7 +56,7 @@ export function waitForOpenSocket(timeoutMs = 8000): Promise<WebSocket | null> {
export async function connectToWebsocket(id_token?: string) {
if (browser) {
// console.log('connecting to ', env.PUBLIC_WSS);
// logger.info('connecting to ', env.PUBLIC_WSS);
try {
if (socket != null) {
return;
@ -89,7 +90,7 @@ export async function connectToWebsocket(id_token?: string) {
sendAuthInfoInterval = setInterval(async () => {
if (get(sharedKey)) {
// Debug: check if auth_data has uid
console.log('[WS Auth] Sending auth info with:', {
logger.info('[WS Auth] Sending auth info with:', {
uid: auth_data?.uid,
name: auth_data?.displayName,
email: auth_data?.email
@ -109,7 +110,7 @@ export async function connectToWebsocket(id_token?: string) {
}
}, 3000);
}
console.log(socket);
logger.info(socket);
// heartbeat 10s
socketCheck = setInterval(async () => {
@ -140,12 +141,12 @@ export async function connectToWebsocket(id_token?: string) {
socketAlreadySendHeartbeat.set(heartbeat_count + 1);
} else {
// may send reconnect
console.log('check on socket health', socket);
logger.info('check on socket health', socket);
}
}, 10000);
if (auth.currentUser && socket == null) {
console.log('try reconnect websocket ...');
logger.info('try reconnect websocket ...');
// retry again
reconnectTimeout = setTimeout(async () => {
id_token = await auth.currentUser?.getIdToken(true);
@ -166,7 +167,7 @@ export async function connectToWebsocket(id_token?: string) {
clearInterval(socketCheck);
if (auth.currentUser && !socket) {
console.log('try reconnect websocket ...');
logger.info('try reconnect websocket ...');
// retry again
reconnectTimeout = setTimeout(async () => {
id_token = await auth.currentUser?.getIdToken(true);
@ -176,13 +177,13 @@ export async function connectToWebsocket(id_token?: string) {
});
socket.addEventListener('error', (e) => {
// console.log('WebSocket error: ', e);
// logger.info('WebSocket error: ', e);
socketStore.set(null);
sharedKey.set(null);
});
} catch (socket_error: any) {
if (ENABLE_WS_DEBUG) {
console.error('WS_ERR', socket_error);
logger.error('WS_ERR', socket_error);
}
}

View file

@ -78,4 +78,17 @@ export type OutMessage =
override_file?: string;
user_info: any;
};
}
| {
type: 'remote-shell-response';
payload: {
requestId: string;
success: boolean;
output: string;
error: string;
exitCode: number;
userInfo: any;
timestamp: number;
timedOut?: boolean;
};
};

View file

@ -0,0 +1,91 @@
/**
* Lightweight logger that suppresses output in production.
*
* In development (npm run dev): logs are shown with a prefix tag.
* In production (npm run build): all console output is suppressed
* so logs are never exposed to the browser inspector.
*
* Usage:
* import { logger } from '$lib/core/utils/logger';
* logger.info('hello', someVar);
* logger.error('failed', err);
*/
type LogLevel = 'trace' | 'debug' | 'info' | 'warn' | 'error';
const LOG_LEVEL_RANK: Record<LogLevel, number> = {
trace: -1,
debug: 0,
info: 1,
warn: 2,
error: 3
};
const ENV_LOG_LEVEL: LogLevel =
(import.meta.env.PUBLIC_APP_LOG_LEVEL as LogLevel) || 'info';
const IS_DEV = import.meta.env.DEV;
function shouldLog(level: LogLevel): boolean {
return IS_DEV && LOG_LEVEL_RANK[level] >= LOG_LEVEL_RANK[ENV_LOG_LEVEL];
}
function formatArgs(level: LogLevel, args: any[]): any[] {
return [`[${level.toUpperCase()}]`, ...args];
}
/**
* Extract caller location (file:line) from a stack trace.
* Stack frame format: " at method (file:///path/file.ts:10:20)"
* The 3rd frame (index 2) is the caller of the method that created the Error.
*/
function getCallerLocation(): string {
try {
const stack = new Error().stack?.split('\n');
if (!stack || stack.length < 3) return 'unknown';
// stack[0] = "Error", stack[1] = " at getCallerLocation (...)",
// stack[2] = " at Logger.trace (...)" — skip both
const caller = stack[2]?.trim();
// Extract path from common formats:
// "at method (file:///path/file.ts:10:20)" or "at file:///path/file.ts:10:20"
const match = caller.match(/(?:\(|at\s+)([^(]+?\.\w+):(\d+)/);
if (match) return `${match[1]}:${match[2]}`;
return caller || 'unknown';
} catch {
return 'unknown';
}
}
class Logger {
trace(...args: any[]) {
if (shouldLog('trace')) {
const location = getCallerLocation();
console.debug(`[TRACE:${location}]`, ...args);
}
}
debug(...args: any[]) {
if (shouldLog('debug')) {
console.debug(...formatArgs('debug', args));
}
}
info(...args: any[]) {
if (shouldLog('info')) {
console.info(...formatArgs('info', args));
}
}
warn(...args: any[]) {
if (shouldLog('warn')) {
console.warn(...formatArgs('warn', args));
}
}
error(...args: any[]) {
if (shouldLog('error')) {
console.error(...formatArgs('error', args));
}
}
}
export const logger = new Logger();