+
+
+
+
+ Reboot Machine?
+
+ New {mode} prices only take effect after a full device reboot. The machine
+ will restart — this takes a few minutes.
+
+
+
+
+
+
+
+
diff --git a/src/lib/core/handlers/messageHandler.ts b/src/lib/core/handlers/messageHandler.ts
index 276a71f..a43efb3 100644
--- a/src/lib/core/handlers/messageHandler.ts
+++ b/src/lib/core/handlers/messageHandler.ts
@@ -261,14 +261,23 @@ const handlers: Record void> = {
// Handle gen-service responses
if (from === 'gen-service') {
switch (level) {
- case 'batch_start':
+ case 'batch_start': {
+ const genRef = p.ref ?? 'gen-layout';
handleGenLayoutBatchStart({
batch_id: p.batch_id,
total_files: p.total_files,
- total_size_bytes: p.total_size_bytes
+ total_size_bytes: p.total_size_bytes,
+ ref: genRef
});
- addNotification(`INFO:Gen Layout started (${p.total_files} files)`);
+ const genLabel =
+ genRef === 'gen-price'
+ ? 'Gen Price'
+ : genRef === 'gen-priceslot'
+ ? 'Gen PriceSlot'
+ : 'Gen Layout';
+ addNotification(`INFO:${genLabel} started (${p.total_files} files)`);
break;
+ }
case 'file':
handleGenLayoutFile({
batch_id: p.batch_id,
@@ -287,11 +296,11 @@ const handlers: Record void> = {
batch_id: p.batch_id,
total_files: p.total_files
});
- addNotification('INFO:Gen Layout complete');
+ addNotification('INFO:Generation complete');
break;
case 'ERROR':
handleGenLayoutError(msg);
- addNotification(`ERR:Gen Layout error: ${msg}`);
+ addNotification(`ERR:Generation error: ${msg}`);
break;
default:
logger.info('[GenService] Received:', level, msg);
diff --git a/src/lib/core/services/sheetService.ts b/src/lib/core/services/sheetService.ts
index 2034fb7..7cb0d05 100644
--- a/src/lib/core/services/sheetService.ts
+++ b/src/lib/core/services/sheetService.ts
@@ -325,7 +325,7 @@ export async function requestGenLayout(country: string): Promise {
};
}
- setGenLayoutGenerating();
+ setGenLayoutGenerating('gen-layout');
logger.info('[sheetService] Sending gen-layout request for country:', country);
@@ -344,6 +344,79 @@ export async function requestGenLayout(country: string): Promise {
});
}
+/**
+ * Trigger gen-service to (re)generate the machine price profile from Grist.
+ * Backend: taobin_admin app.py `gen-price` branch -> gen_price_supra.gen_price.
+ * Produces profile__master.json, commits to recipe-repo, streams files back
+ * (ref='gen-price') for the frontend to push to the machine.
+ */
+export async function requestGenPrice(country: string): Promise {
+ const curr_user = get(auth);
+
+ let user_info: any = {};
+ if (curr_user) {
+ user_info = {
+ displayName: curr_user.displayName,
+ email: curr_user.email,
+ uid: curr_user.uid
+ };
+ }
+
+ setGenLayoutGenerating('gen-price');
+
+ logger.info('[sheetService] Sending gen-price request for country:', country);
+
+ return await sendMessage({
+ type: 'command',
+ payload: {
+ user_info,
+ srv_name: 'gen-service',
+ values: {
+ file_layout: 'sheet',
+ file_desc: 'sheet',
+ country: country,
+ param: 'gen-price-supra_app'
+ }
+ }
+ });
+}
+
+/**
+ * Trigger gen-service to (re)generate the per-slot price profiles from Grist.
+ * Backend: taobin_admin app.py `gen-priceslot` branch -> gen_price_supra.gen_priceslot.
+ * Produces profile__slot_.json (ref='gen-priceslot').
+ */
+export async function requestGenPriceSlot(country: string): Promise {
+ const curr_user = get(auth);
+
+ let user_info: any = {};
+ if (curr_user) {
+ user_info = {
+ displayName: curr_user.displayName,
+ email: curr_user.email,
+ uid: curr_user.uid
+ };
+ }
+
+ setGenLayoutGenerating('gen-priceslot');
+
+ logger.info('[sheetService] Sending gen-priceslot request for country:', country);
+
+ return await sendMessage({
+ type: 'command',
+ payload: {
+ user_info,
+ srv_name: 'gen-service',
+ values: {
+ file_layout: 'sheet',
+ file_desc: 'sheet',
+ country: country,
+ param: 'gen-priceslot-supra_app'
+ }
+ }
+ });
+}
+
/**
* Request price data from sheet for specific product codes
* NOTE: Can only send once per type (price). Use hasSheetPriceBeenSent to check.
diff --git a/src/lib/core/stores/genLayoutStore.ts b/src/lib/core/stores/genLayoutStore.ts
index d0a5e7c..ccf811c 100644
--- a/src/lib/core/stores/genLayoutStore.ts
+++ b/src/lib/core/stores/genLayoutStore.ts
@@ -7,12 +7,20 @@ export interface GenLayoutFile {
file_index: number;
}
+// Kind of gen-service batch. Distinguishes which files were generated so the
+// push logic can map them to the correct machine path.
+// gen-layout -> layout files under taobin_project/
+// gen-price -> profile__master.json -> /sdcard/coffeevending/profile/
+// gen-priceslot -> profile__slot_.json -> .../price/
+export type GenKind = 'gen-layout' | 'gen-price' | 'gen-priceslot';
+
export interface GenLayoutBatch {
batch_id: string;
total_files: number;
total_size_bytes: number;
status: 'idle' | 'generating' | 'receiving' | 'complete' | 'error';
files: GenLayoutFile[];
+ ref: GenKind;
error?: string;
}
@@ -29,7 +37,8 @@ const initialState: GenLayoutBatch = {
total_files: 0,
total_size_bytes: 0,
status: 'idle',
- files: []
+ files: [],
+ ref: 'gen-layout'
};
export const genLayoutBatch = writable(initialState);
@@ -52,15 +61,17 @@ export function handleGenLayoutBatchStart(payload: {
batch_id: string;
total_files: number;
total_size_bytes: number;
+ ref?: GenKind;
}) {
genLayoutBatch.set({
batch_id: payload.batch_id,
total_files: payload.total_files,
total_size_bytes: payload.total_size_bytes,
status: 'receiving',
- files: []
+ files: [],
+ ref: payload.ref ?? get(genLayoutBatch).ref ?? 'gen-layout'
});
- logger.info('[GenLayout] Batch started:', payload.batch_id, 'total files:', payload.total_files);
+ logger.info('[GenLayout] Batch started:', payload.batch_id, 'ref:', payload.ref, 'total files:', payload.total_files);
}
export function handleGenLayoutFile(payload: {
@@ -79,7 +90,10 @@ export function handleGenLayoutFile(payload: {
if (payload.is_chunked) {
const fileIndex = payload.file_index;
- const partIndex = payload.part_index ?? 0;
+ // Backend sends part_index 1-based (1..N); normalize to 0-based so it
+ // matches the 0..totalParts-1 assembly loop below. Without this, part 1
+ // was stored empty and the last chunk was dropped (files > 1MB corrupted).
+ const partIndex = (payload.part_index ?? 1) - 1;
const totalParts = payload.total_parts ?? 1;
// Get or create tracker for this file
@@ -242,9 +256,10 @@ export function resetGenLayoutBatch() {
chunkedFiles.clear();
}
-export function setGenLayoutGenerating() {
+export function setGenLayoutGenerating(ref: GenKind = 'gen-layout') {
genLayoutBatch.update((batch) => ({
...batch,
+ ref,
status: 'generating'
}));
}
diff --git a/src/routes/(authed)/sheet/price/[country]/+page.svelte b/src/routes/(authed)/sheet/price/[country]/+page.svelte
index 3667b69..96a1a8a 100644
--- a/src/routes/(authed)/sheet/price/[country]/+page.svelte
+++ b/src/routes/(authed)/sheet/price/[country]/+page.svelte
@@ -24,6 +24,7 @@
import * as Table from '$lib/components/ui/table/index.js';
import Spinner from '$lib/components/ui/spinner/spinner.svelte';
import { Plus, RefreshCw, Save } from '@lucide/svelte/icons';
+ import GenPricePanel from '$lib/components/gen-price-panel.svelte';
let selectedCountry = $state($page.params.country || get(departmentStore) || '');
let search = $state('');
@@ -211,6 +212,12 @@
+ {#if selectedCountry}
+