WPPosTill — Kitchen POS & Edge Server
A LAN-based point-of-sale stack: Thin Clients (Electron/Capacitor) talk only to the Edge Server; the Edge Server syncs with WooCommerce and WordPress and drives label/receipt printing.
Language
Edge Server: The headless Node.js hub on the store LAN that owns SQLite data, WooCommerce polling, and printing. Avoid: Local server, backend, API server (when meaning this specific role)
Thin Client: A POS terminal app with no business-data database; all catalog and orders come from the Edge Server. Electron may use singleton auxiliary BrowserWindows for catalog and kitchen; Capacitor uses the same Vue routes (#/catalog, #/kitchen) in one WebView. Both platforms share Till session, boot PIN, and permission gates (#49, ADR 0014). Avoid: Kiosk, terminal, client app (generic)
Till user: A person who operates the POS, identified on the Edge Server by display name and a four-digit PIN (hashed at rest). Permissions for what they may do on the till are stored as JSON on their row; enforcement is a separate slice. Not a WordPress/wp-admin account and not a Customer. Avoid: User (WordPress sense), staff account, wp user, operator account
Settings (permission gate): Configuration areas gated by settings.* keys on the signed-in Till user’s permissions_json. When #49 ships, checks use the active Till session (no second PIN). If the user has no settings.* grants, the Settings cog is hidden on the main till and #/settings is blocked (grill #49 2026-06-08, Q8 A). If the user has some settings.* grants, Settings opens and only permitted sub-sections unlock; denied sections stay visible but locked with an explanation. PINs must be unique store-wide. Avoid: Split gate (partial open shell), admin mode (generic)
Settings session: The authenticated Till user context while using Settings after PIN verification (v1 today). Operator enters a four-digit PIN only (no name picker); the Edge Server resolves the matching Till user row and returns an opaque settings token (Authorization: Bearer) for admin PATCH/CRUD until Logout or process exit. Session state (token, display name, allowed keys) persists in sessionStorage. PINs must be unique across all Till users. After 5 failed PIN attempts on this till, sign-in is blocked for 15 minutes on that device. Superseded for normal operation by Till session when #49 ships—PR 1 removes Settings-only sign-in; Settings uses the boot Till session token with no second PIN (grill Q24 A). Avoid: Login session (app-wide, ambiguous)
Till session: The app-wide authenticated Till user context after boot PIN sign-in (#49, ADR 0014). One four-digit PIN at launch (after Edge Server connection succeeds) unlocks Settings and all gated till areas until Logout or Switch user. The main till header exposes both Switch user (handoff) and Logout (end shift); both revoke the Till session token on the Edge Server and return to the boot PIN modal in place. Switch user opens the PIN modal immediately; Logout requires confirmation (End shift and sign out?) before revoke (grill #49 2026-06-08, Q17 A). Current basket is preserved across Switch user and Logout—the next signed-in operator inherits the same cart lines (grill #49 2026-06-08, Q10 C, Q11 C). When sign-in completes with a non-empty basket, show a one-time toast that the basket was carried over from the previous operator (grill #49 2026-06-08, Q12 B). Checkout attribution uses the Till user active at Authorize & submit, not who built the cart. The Edge Server returns an opaque till token (Authorization: Bearer) plus full permissions_json; the same token authorizes settings admin routes, mutating till APIs, and authenticated catalog read routes (GET /api/products, composite definition reads, LAN product images)—401 without a valid token when not in bootstrap mode; no per-capability check on those reads (grill #49 2026-06-08, Q9 A). Session state persists in sessionStorage until Logout, Switch user, token expiry (default 12 h TTL), or process exit. TTL expiry behaves like handoff: boot PIN modal in place, Current basket preserved, message Session expired — sign in again; non-empty cart triggers the same one-time inherited-basket toast (grill #49 2026-06-08, Q15 A). Replaces the separate Settings-only sign-in for normal operation; bootstrap mode (zero Till users) skips the gate so the first-run wizard can create the first account. Avoid: Settings session (when meaning app-wide), operator login (generic)
Boot PIN gate: The blocking sign-in shell shown when the Thin Client launches and at least one Till user exists. Operator must enter a valid four-digit PIN before any till routes, cart, or Edge Server till APIs run—including GET /api/products, composite definition reads, and LAN GET /images/{id}.webp. The Edge Server enforces the same Till session bearer on those read routes (401 without token); only connection health, bootstrap/wizard endpoints, and other explicitly unauthenticated bootstrap paths may run without auth. Bootstrap mode skips the gate on client and server. Auxiliary Electron windows (catalog management, Kitchen board) require the same active Till session as the main till—they do not load anonymously. When the session ends (Logout, Switch user, or TTL expiry), open auxiliary windows show a lock overlay with the same boot PIN flow until re-auth succeeds; windows are not closed and stale content is not left visible (grill #49 2026-06-08, Q16 A). Implementation: lock overlay ships post-#49 follow-up; until then PR 2 blocks opening catalog/kitchen windows without a valid session (Q25 B). One Thin Client app instance shares a single Till session—successful PIN on any window restores the session for every window of that instance via IPC (grill #49 2026-06-08, Q18 A). Avoid: Overlay login, background catalog preload
Till user permissions: JSON on a Till user row describing what they may do. v1 enforces settings.* keys only (four checkboxes in the user editor). New keys are added via an extensible permission catalogue (server + client share the same key list) without SQLite migrations; #49 enforces catalog, checkout, kitchen, EOD, and label print keys. A Till user may grant only permissions they already hold (subset delegation). Checkout discount cap uses checkout.max_discount_percent (0–100; only preset till discount buttons ≤ cap are available). Catalog label print uses catalog.print_label (separate from catalog.edit and catalog.upload_image). pos.print_label is not used (grid print removed). Empty permissions ({} or no grantable capability keys resolved at sign-in) block boot PIN sign-in with a clear message—managers must assign a permission profile or explicit capabilities before the account can use the till (grill #49 2026-06-08, Q4 C). Training or minimal roles use an explicit profile (e.g. grid + cart + checkout.submit: false), not an empty object. Avoid: Roles, role matrix (generic RBAC)
Permission profile: A named reusable preset stored on the Edge Server (till_permission_profiles table) holding a full permissions_json snapshot (e.g. Manager, Cashier, Kitchen, Training). Apply profile on a Till user copies the preset onto that user’s row—it is not a live link; editing a profile does not change existing users until a manager re-applies or edits the user. Optional last_applied_profile_id on the user row is audit/display only. Delete profile is allowed with confirmation even when users still reference it; affected users keep their copied permissions_json and last_applied_profile_id is cleared (grill #49 2026-06-08, Q7 B). Table + four starter permission profiles seed in implementation PR 1; admin CRUD UI in PR 4 (grill Q23 A). Manager (all registry capabilities + all settings.* + checkout.max_discount_percent: 100), Cashier (checkout.submit only; grid/cart automatic), Kitchen (kitchen.view only), Training (grid/cart automatic, checkout.submit: false only). The first-run wizard first Till user receives Manager applied to permissions_json (grill #49 2026-06-08, Q6 A). Avoid: Role template (live join), WordPress role
Website service account: WordPress credentials (username + application password) used by the Edge Server for website-facing sync features. Stored on the Edge Server (shared across all Thin Clients on the LAN), not on individual tills. Distinct from WooCommerce REST credentials and Till users. Avoid: WordPress user, wp-admin account
Local Order Id: The permanent primary identifier for an order row on the Edge Server and in Thin Client state, assigned at checkout or first ingest. Avoid: Order id (ambiguous with WooCommerce), internal id
Woo Order Id: The numeric/string identifier assigned by WooCommerce for a remote order. Avoid: Order number (ambiguous on receipts), remote id
Order: A basket of line items with totals, payment method, status, and timestamps, stored on the Edge Server. Avoid: Transaction, sale, checkout (verb sense)
Till Order: An order created at the POS; outbound sync may create a matching Woo order, but the Edge Server does not link the Woo id back onto the Till Order row. Avoid: Local order, POS order
Web Order: An order row created by inbound Woo polling; Local Order Id uses the WOO-{wooOrderId} prefix (e.g. WOO-4521). Avoid: Online order, Woo order (row)
Relationships
- Every Order has exactly one Local Order Id on its row (never re-keyed after creation)
- A Till Order and a Web Order may both exist for the same real-world sale when till checkout was pushed to WooCommerce (two kitchen cards; manual merge out of scope v1)
- Thin Clients reference Till Orders by their Local Order Id; Web Orders appear separately when the poller ingests them
- Repeat orders increases at most once per distinct Woo Order Id ingested; Till Orders do not change it
- Each Web Order belongs to at most one Customer (via Customer match key)
- Receipt rendering for a Web Order includes Customer data; for a Till Order it does not
- Catalog bootstrap runs once on first initialization (products—including composite/bundle—and customers downloaded from the website)
- After bootstrap, the Edge Server is source of truth for products and customers; staff edits push to the website; wp-admin edits are overwritten on push or reconcile
- Labels and Receipts use separate Print routes when configured
- Real-time broadcasts use
order_upsertedwith Order source on every payload - Web Order ingest statuses control which remote orders appear on the Kitchen board; Repeat orders may increment later when status advances (e.g.
pending→processing) - Kitchen dismissal rules differ by Order source (see ADR 0009): Web Orders dismiss on Woo terminal
status; Till Orders dismiss onkitchen_dismissedwhile paymentstatusmay staycompleted - Print endpoints queue Print jobs; HTTP does not block on PDF or hardware
- Till users are stored only on the Edge Server; Thin Clients do not hold a local user database
- A Till user is distinct from WooCommerce REST credentials, Website service account credentials, and Customer rows
Kitchen board: The live order list on Thin Clients; Till Orders and Web Orders both appear when ingested or checked out. Shown on a dedicated #/kitchen screen (responsive card grid). On Windows Electron, a singleton kitchen BrowserWindow (see ADR 0004); not on the checkout till layout. Avoid: Order monitor, queue (generic)
Customer: A person or organization known to the Edge Server, stored separately from Orders, enriched from Woo/WordPress billing data. Avoid: User (WordPress account sense), account, shopper
Customer match key: The rule used to find or create a Customer row on Web Order ingest: WordPress user_id if present, else Woo customer_id if positive, else normalized billing email hash for guests. Avoid: Customer id (ambiguous with Woo), dedup key
Repeat orders (repeat_orders): A local-only counter on a Customer row: incremented once per Woo Order Id when that order first reaches an actionable status (processing, completed)—not on initial pending ingest. Avoid: Loyalty points, order count (ambiguous with row counts)
Web Order ingest statuses: Woo orders in pending, processing, or completed are stored and shown on the kitchen board; other statuses (e.g. cancelled, refunded) update existing rows but do not create new kitchen tickets. Avoid: Order status (generic Woo term)
Kitchen dismissal: Removing an order from the active Kitchen board after an order_upserted broadcast. Web Orders: Woo terminal status (cancelled, refunded, completed). Till Orders: staff sets kitchen_dismissed via PATCH /api/orders/:id; payment status stays completed for EOD (ADR 0009). Avoid: Archive, complete (ambiguous with Woo completed)
kitchen_dismissed: Till-only flag on an Order row (INTEGER, default 0). When 1, the Till Order is cleared from the Kitchen board but remains in EOD and receipts. Not used on Web Orders. Avoid: Kitchen complete, archived, done flag (generic)
eod_settled: Flag on an Order row (INTEGER, default 0). Set to 1 for every Order in the business-date window at EOD close (including cancelled / refunded); cleared then re-set on re-submit for that window. While eod_settled = 1, Woo inbound poll ignores all status updates on that row (grill #54 2026-06-09, B); non-status field updates may still apply. New Orders after close start at 0 until the next re-submit. Avoid: Archived, locked order (generic)
End of day (EOD) window: Thin Client screen (#/eod) listing Orders for a selected calendar business date with POS / Online / Grand totals. v1 is read-only via GET /api/orders/eod; v2 adds reconciliation fields and EOD close (PRD #54). eod.view opens the window and loads the report; eod.submit (Manager profile only) authorizes EOD close. Day boundaries use the Edge Server local timezone (ADR 0006). Avoid: Z-report, daily sales report (generic)
EOD close: Manager action that finalizes a business date on the Edge Server: atomically upserts one eod_records row (replace on re-submit), snapshots included Orders as JSON, and settles open work. On submit: pending / processing → status = completed (including processing Web Orders); cancelled / refunded unchanged; any Till Order still on the Kitchen board → kitchen_dismissed = 1; every Order in the window → eod_settled = 1. Local SQLite only—no reverse sync to WooCommerce (grill #54 2026-06-09, C). Does not block new Orders for that date afterward—late checkout and Woo ingest continue; the EOD record is stale until the manager re-submits (grill #54 2026-06-09, A). Permitted only for today and past business dates (Edge local timezone)—future dates are view-only (grill #54 2026-06-09, A). Zero orders for the date is allowed—declared external payments may still be saved; system totals are zero (grill #54 2026-06-09, A). Saved eod_records and orders_json are never mutated by Woo poll—only by manager re-submit. Woo poll ignores all status updates when eod_settled = 1 (grill #54 2026-06-09, B). Web Orders that arrive after close for the same business date follow normal Woo poll rules until the next re-submit settles them (grill #54 2026-06-09, A). Every submit shows a confirm dialog first, including business date, replace warning, and preview record_grand_total (grill #54 2026-06-09, B). Today may be closed at any time while trading continues—no extra time-of-day guard; late Orders trigger drift warning until re-submit (grill #54 2026-06-09, A). Re-submit re-runs settlement on all Orders in the business-date window (idempotent full pass), replaces eod_records, and refreshes orders_json (grill #54 2026-06-09, A). Concurrent closes for the same date: last write wins (grill #54 2026-06-09, A). Avoid: Close register, archive day (generic)
EOD record: One persisted row in eod_records per business date (unique). Stores system totals (pos_total, online_total, grand_total), five declared external payment amounts (see Declared external payments), record_grand_total, submitter audit fields, and orders_json snapshot at close time. Re-submit replaces the row for that date. When a Till user reopens that business date in the EOD window, declared fields pre-fill from the saved record; UI shows close metadata and highlights differences between live order totals/count and the saved snapshot (grill #54 2026-06-09, C). Drift warning when live order_count, grand_total, pos_total, or online_total differs from the saved EOD record (grill #54 2026-06-09, B). Each order in orders_json mirrors the v1 EOD grid fields plus parsed line items (name, quantity, line total)—not the full raw order row (grill #54 2026-06-09, B). Composite Till Order lines include parent row plus nested child lines (name, qty, line total)—not the full raw line_items blob (grill #54 2026-06-09, B). eod.view without eod.submit may read close metadata (timestamp, submitter, order-count/total diff hints) via GET /api/eod/records but not declared payment amounts; full record including declared fields requires eod.submit (grill #54 2026-06-09, C). Avoid: Daily backup, Z-report file
Declared external payments (EOD close): Five optional amounts entered at EOD close — Additional Cash, Just Eat, Deliveroo, Other Payments, Direct Payments — representing sales outside this POS’s Till Orders and ingested Web Orders. Blank or zero allowed. Summed as declared_extras_total and added to system grand_total for record_grand_total (grill #54 2026-06-09). Not auto-derived from order rows. Stored and summed at 2 decimal places (round half-up), matching v1 EOD money rounding (grill #54 2026-06-09, A). Avoid: Payment method breakdown, channel auto-total
EOD close receipt: Thermal Receipt printed automatically on successful EOD close via the configured Receipt Print route (same hardware path as order Receipts). v2 uses ESC/POS text layout only (same pipeline as auto kitchen/order receipts on TCP thermal routes); HTML/CUPS receipt routes do not print EOD slips—receiptPrintQueued: false with client toast (grill #54 2026-06-09, A). POST /api/eod/records saves the EOD record, enqueues the Print job on the Edge Server, and returns receiptPrintQueued (grill #54 2026-06-09, A). Layout: storeName from store.companyName, else store.siteTitle, else omit blank header line (grill #54 2026-06-09, A); title End of day report, business date plus closed at local time (Edge timezone), Orders: {order_count}, rule line, one row per total (type left, £amount right, 2 dp, £ prefix — grill #54 2026-06-09, A): system rows POS, Online, Grand, Total (record grand total); declared rows match UI labels (Additional Cash, Just Eat, Deliveroo, Other Payments, Direct Payments) when > 0 (grill #54 2026-06-09, C); rule line, EOD performed by {submitter display name}. EOD close succeeds even if receipt print fails—show error toast, still return to till; EOD record is not rolled back (grill #54 2026-06-09, A). Success toast shows record_grand_total and receipt outcome (printing vs failed — use Reprint) from receiptPrintQueued before return to till (grill #54 2026-06-09, B). After print is queued (or fails), the EOD window returns to the till. eod.submit users may Reprint EOD receipt when a saved EOD record exists—same layout, no re-settlement, no confirm dialog; toast print outcome only (grill #54 2026-06-09, B); Closed at uses original submitted_at from the record, not reprint time (grill #54 2026-06-09, A). Avoid: Z-report slip (generic), EOD export
Receipt: A printed ticket composed on the Edge Server from HTML templates; Till Orders use order-row data only, Web Orders also join Customer fields. Avoid: Invoice, slip (unless distinguished from kitchen slip in UI copy)
Catalog bootstrap: The one-time (or operator re-run) download of products, variations, composite/bundle types, and customers from WooCommerce/WordPress into local SQLite. Completing catalog row ingest sets bootstrap_completed; a follow-on catalog image phase (async) downloads and optimizes primary catalog product image files without blocking checkout (grill 2026-06-07, PRD #43). Avoid: Initial sync, first pull (vague)
Catalog image phase: Async Edge Server work after Catalog bootstrap (or during re-bootstrap) via BootstrapImageSync (dedicated batch runner—not per-product sync_jobs). If Edge restarts while images_ready is false, resume—skip products whose fresh file + sidecar match SQLite. 4 concurrent WAN downloads; progress in sync_state: images_ready, images_processed, images_total (images_total = products with valid http(s):// primary URL only). Sockets: catalog_image_progress during batch; catalog_images_ready at end. Re-bootstrap: rebuild in place; delete orphans after batch. Batch failures enqueue REFRESH_PRODUCT_IMAGE per failed id. Staff POST/PATCH image changes enqueue REFRESH_PRODUCT_IMAGE on sync_jobs, processed by a dedicated image worker (paused while batch incomplete). Dedupe pending jobs; in-flight stale no-op; clear Images deletes files; success re-emits product_upserted. Each REFRESH_PRODUCT_IMAGE job runs once (internal 3-attempt download like batch), then marks completed or failed—no multi-cycle sync_jobs retries (grill 2026-06-07, PRD #43). Avoid: Image bootstrap (ambiguous with full re-bootstrap), thumbnail sync
Product image cache (Thin Client): On-device copy of catalog product image file bytes for fast <img> rendering, stored under a platform-specific directory (Electron userData/product-images; Capacitor Filesystem). Cache hits expose a platform file URI to <img> (file:// on Electron; Capacitor-converted local URI on Android)—not blob: URLs. If GET /api/images/status is unavailable (legacy Edge Server), the image subsystem stays off for that session—resolver uses images[0].src only (grill 2026-06-07, PRD #43). v1 uses hybrid warming when enabled: eager fetch for products in the active POS category tab after catalog load; lazy fetch when other products enter the viewport or catalog detail; 4 concurrent LAN downloads max per till (grill 2026-06-07, PRD #43). LRU cap: 500 cached product images max (evict least-recently-used when over—delete evicted file from device storage). On socket catalog_images_ready, wipe entire cache and re-warm active tab. During batch, catalog_image_progress triggers debounced catalog refetch (replace entire in-memory product list from GET /api/products) (grill 2026-06-07, PRD #43). product_upserted: invalidate cache for that catalog product id; PosView merges payload into in-memory catalog list so tiles pick up local_src / resolver changes (grill 2026-06-07, PRD #43). Resolver order: platform file URI → local_src while persisting → images[0].src (Woo) → placeholder. Avoid: Full-catalog prefetch, CDN cache
Variation image fallback (Edge read path): When a variation row has no primary image URL (and no fresh catalog product image file), GET /api/products may set local_src from the variable parent’s LAN file at read time if the parent file exists and is not stale—no duplicate WebP on disk in v1 (grill 2026-06-07, PRD #43). Avoid: Parent image copy at bootstrap, inherited SKU file
Catalog product image HTTP (Edge LAN serve): GET /images/{catalogProductId}.webp returns WebP with ETag (file hash or mtime) and Cache-Control: private, max-age=86400, must-revalidate (grill 2026-06-07, PRD #43). Requires valid Till session bearer when not in bootstrap mode (grill #49 2026-06-08, Q9 A). Avoid: Public CDN caching, sidecar over HTTP
Catalog push: Uploading local product state to the website after staff changes (any field, including images), or via full product reconcile push. Avoid: Product sync (ambiguous direction), upload
Customer push: Uploading local customer state (name, contact, addresses) to WooCommerce/WordPress after staff edits, or via full customer reconcile push. Does not include repeat_orders. Avoid: Customer sync (ambiguous direction)
Source of truth (catalog and customers): After bootstrap, the Edge Server master is authoritative for LAN and for the public store; routine pulls from wp-admin do not refresh products or customers. Avoid: Master, golden copy
Re-bootstrap: A guarded, operator-confirmed full re-download of products and customers from the website; destructive to local catalog/customer tables; orders and repeat_orders preserved by default. Avoid: Factory reset, re-sync (vague)
Local_Only (product): A product that exists on the Edge Server and at the till but is never published or updated on the website. Avoid: Internal product, non-web SKU
Catalog draft (catalog management UI): In-memory edits to a catalog product in the management detail pane before Update persists to the Edge Server. The draft may include any field accepted by PATCH /api/products/:id, not a fixed subset. Revert resets the draft to the last loaded snapshot without PATCH. If the draft is dirty, changing selection, closing the window, refreshing the catalog, or starting New product prompts save, discard, or cancel. Save runs Update then continues the pending action (action-bar visibility/local_only applies after a successful save). Discard reloads the last loaded snapshot (equivalent to Revert), then continues the pending action. Avoid: Unsaved changes, dirty form
Staff product edit (catalog management): A till-initiated change to any allowed column on a catalog product row, saved via PATCH /api/products/:id and enqueued for catalog push when the product is not Local_Only. After save, staff see a non-blocking toast: saved on till when Local_Only; saved on till; syncing to website… when push applies—not confirmation Woo has finished. Failures use alert until #50 inline errors ship (grill 2026-06-08). Avoid: Product update, sync
Catalog management window (Electron): The singleton second BrowserWindow for staff catalog browse/edit, defaulting larger than the main till window (e.g. 1600×900). Opening it again focuses the existing instance. Manage catalog opens with no selection; Manage this product asks Are you sure? on the till grid before open/focus with a catalog product id pre-selected. Selecting a product does not add to the checkout basket. Closing the main till window closes the catalog window (with draft guard on catalog if dirty). PRD split (grill 2026-06-08): catalog management shell owns window, accordion, draft guard, action bar, print, socket, composite conversion entry; #50 supersedes detail pane + New product UX when that slice ships. When #49 ships: catalog.open grants entry; without catalog.edit, staff get read-only catalog access (full detail view, all mutating actions blocked; Print Label still gated by catalog.print_label) (grill #49 2026-06-08, Q5 A). Avoid: Product admin, back office window
Read-only catalog access: Till session mode when catalog.open is granted but catalog.edit is not. Staff may open catalog management, search, and view pinned + All fields read-only; Update, New product, hide/show, local-only flips, Configure composite, and Attach photo are blocked. Print Label remains available only with catalog.print_label. Mutating Edge Server catalog routes return 403 regardless of UI (grill #49 2026-06-08, Q5 A). Avoid: Browse-only SKU list, summary-only pane
Manage catalog (POS action): Opens the catalog management window with no product pre-selected. Requires catalog.open on the active Till session when #49 ships (header hidden/disabled without it). Avoid: Products, admin, catalog admin
New product (catalog management action): Creates a catalog product on the Edge Server via POST /api/products with required name, SKU (trimmed, non-empty in current shell), regular price, and at least one category; type simple or composite (composite → Composite conversion screen after save); row is Local_Only by default until staff run Allow website sync. Then selects it in the UI. Requires server create endpoint. #50: optional SKU auto-fill Till+DDMM+MMSS when blank; default visibility catalog on create (grill 2026-06-08). Avoid: Add SKU, create product
Manage this product (POS action): Shows Are you sure? (Yes / No; Escape or backdrop click = No). Yes opens the catalog management window with the chosen grid card’s catalog product id selected, matching category panels expanded, and the row scrolled into view. Not offered on variable parent cards. Manage catalog has no confirmation. Requires catalog.open—same gate as Manage catalog; read-only when catalog.edit is not granted (#49; grill 2026-06-08, Q13 A). When #50 composite filter is active and the product sits only under hidden composite panels, auto-enable Show composite categories once with toast Showing composite categories to locate this product, then expand and scroll (grill 2026-06-08, Q17 A). Avoid: Edit product, product admin
Checkout till layout (POS route #/): Product catalog in responsive-card-grid (~82.5% width); Current basket column ~17.5%. Kitchen board and Manage catalog are header actions opening separate routes/windows—not embedded on the till. See ADR 0004 for kitchen. Avoid: POS screen, register layout
WOOCO composite (catalog product): A catalog product with Woo type composite and WPClever WOOCO definition meta (Meta: wooco_components, pricing/limit meta) on the Edge Server master after catalog bootstrap. Staff sell it via the Composite fulfillment picker, not one-tap add. Out of scope v1: official Woo Composite Products (_bto_*), WPC bundles, grouped products. Avoid: Bundle (generic), kit product (ambiguous)
Non-composite kit types (bundle, grouped): Woo bundle and grouped parents on the POS grid keep v1 one-tap add (parent line only, no picker, no composite_children), same as simple products until a dedicated picker PRD exists. Avoid: Bundle picker, grouped picker (v1)
Optional composite component (none): An optional WOOCO component section includes an explicit None choice (selectable, same control pattern as product options). None means no selection for that component: no row in composite_children, valid for submit, no component price. Required sections never offer None. Avoid: Skip section, empty optional (implicit)
Composite operator copy (wooco_before_text / wooco_after_text): Optional strings from WOOCO meta shown in the Composite fulfillment picker when non-empty: before text above component sections, after text below. Render as plain text only (strip HTML tags from website meta). Not shown on Kitchen board or Receipt in v1 unless added in a follow-on ticket. Avoid: HTML description, website-only copy
Multi-select composite component: When WOOCO allows more than one product in a component section, the picker uses checkboxes (touch-friendly) with per-component min/max enforced. Single-select sections use radio or a static row when only one option exists. Each selected product is a separate entry in composite_children (with its own quantity when customizable). Avoid: Multi-pick v2, one-product-only simplification
Same products rule (wooco_same_products): When WOOCO forbids reusing the same catalog product id across component sections, the picker prevents it: already-selected ids are disabled or blocked in other sections with inline validation; Add to basket / Update basket disabled until resolved. When WOOCO allows same products, no cross-section restriction. Avoid: Duplicate warning only, auto-deselect
Composite total limits (wooco_total_limits): WOOCO min/max on total quantity across all selected components in one kit. Violations show an inline error in the picker; Add to basket / Update basket disabled until fixed (same pattern as required/OOS validation). Avoid: Toast-only, ignore limits v1
Composite fulfillment picker (checkout UI): Full-screen modal on checkout route #/ (v1 only—no separate Electron window; optional singleton window deferred to v1.1 per PRD) opened when staff tap a WOOCO composite on the POS grid. While open, other checkout till layout actions are blocked (header Kitchen board, Manage catalog, grid add, etc.) until Cancel / confirm—modal is modal on #/. Separate Electron windows may remain open but do not auto-close the picker. Staff choose component catalog product options per WOOCO rules, then Add to basket or Cancel. A grid tap always starts a new configuration session (WOOCO defaults)—not editing an existing composite basket line. Edit components on a basket row reopens the picker with that line’s saved composite configuration; Cancel / backdrop / Escape discards in-picker edits and leaves the basket line unchanged. Confirming edit updates the basket: replaces that line in place when no other line shares the same composite configuration; if another line matches, merge into it (increment quantity, remove the edited line). Grid-origin Add to basket uses the same merge rule. Primary action label: Add to basket for grid sessions, Update basket for Edit components sessions (same validation rules). Distinct from catalog management and from Manage this product. Avoid: Component builder, product configurator (generic)
Composite basket line (Current basket): One checkout row for a configured WOOCO composite: parent catalog product id, display name, SKU, line price, and quantity (kits). Attached composite children list each chosen component (catalog product id, name, SKU, per-kit quantity, unit price). Not separate top-level basket rows. Adding the same parent with the same component picks (same composite configuration) merges into one line by incrementing quantity; different picks stay separate lines. v1 UI: collapsed row (parent + line total + component count subtitle); expand to show component names and per-kit qtys; Edit components on the row. Avoid: Bundle line, kit item (ambiguous)
Composite configuration: The staff’s chosen component catalog product ids and per-kit quantities for one WOOCO composite. Used to decide merge vs a new composite basket line (identical configuration → increment parent quantity; any difference → new line). Fingerprint = parent catalog product id + sorted list of (child catalog product id, per-kit qty); selection order and sub-section layout do not affect merge. Avoid: Line key, picker state (implementation)
Composite catalog resolver (Edge Server / degraded client fallback): Expands each WOOCO component to selectable catalog product options from the master catalog: explicit ids in wooco_components, plus category-sourced composite component paths. Applies stock, Internal category recipe rules, and catalog-gap flags. Returns required vs optional per component from WOOCO meta. Parser and resolver live in a shared WOOCO module (shared/wooco/ or equivalent) consumed by Edge Server (Composite definition API) and Thin Client (degraded fallback)—one test suite, no duplicated rules. Avoid: Client-only resolver (primary path), duplicated parser
WOOCO exclude list (component exclude meta): SKUs or product identifiers on a category-sourced WOOCO component section that must not appear as selectable options when the section is expanded (WPClever Exclude field, often _sku_… tokens). v1 Composite catalog resolver and Composite fulfillment picker honor the list at sell time after staff save from Composite conversion screen. Distinct from exclude base Composite pricing mode. Avoid: Excluded product (ambiguous), blacklist
Category-sourced composite component: A WOOCO component whose source is a Woo category (Premium)—the WOOCO-selected category stored in wooco_components as a name or hierarchical path string (e.g. Extras or Internal > Extras), not a numeric term id in v1. One component may list multiple selected categories; v1 renders separate sub-sections within that WOOCO component—one labeled group per selected category. A catalog product is included when its Woo Categories field contains a segment that equals the selected category string (trimmed, case-sensitive)—match what WOOCO configured, not parent-prefix or unrelated categories with the same leaf name. Dedupe by catalog product id if the same product appears under multiple selected categories (show once in the first matching sub-section). variable parent rows are not expanded as options; variation rows are. Required/optional and min/max apply to the whole WOOCO component. If a required category expands to zero options, sale is blocked with “no options on till”. Picker options: name and price only; sub-section order = WOOCO category path order; options within each sub-section sorted per WOOCO order by / order when set, otherwise A–Z by name. Avoid: Tag pool, dynamic AJAX category search, inferred parent/leaf matching
Composite definition API (GET /api/products/:id/composite): Edge Server returns the normalized WOOCO composite definition and resolved component options for a parent catalog product id (explicit ids, category-sourced composite component expansion, stock, Internal category recipe inclusion, missing/unavailable ids, required/optional flags). Thin Client uses this as the primary source for opening the Composite fulfillment picker; selection validation and pricing may still run on the client against the latest catalog snapshot. When the API fails but a prior GET /api/products snapshot includes the parent row, the till may degraded-fallback parse WOOCO meta client-side from cached csv to open the picker; show connection warning and offer Retry (no one-tap parent add, no empty picker). Avoid: WOOCO parser endpoint (vague), client-only primary path
Composite items table (composite_items): Materialized SQLite table on the Edge Server that expands every local WOOCO composite parent’s product_cat components into flat rows: one row per (parent, component, category slug, matching catalog product). WOOCO Premium stores category slugs in each component’s other array (see WPCleaverPro.md); expansion uses the same slug-to-category matching as the Composite catalog resolver (shared/wooco/). Each row has local_composite_products_id (autoincrement primary key), canonical product_id (duplicates allowed), parent/component context, and product_snapshot_json (denormalized product payload). Rebuild is manual only: POST /api/admin/composite-items/rebuild or scripts/rebuild-composite-items.mjs — truncate then repopulate from current products master. Does not replace runtime Composite definition API; no till UI in v1. See ADR 0007. Avoid: Composite cache (ambiguous), picker database (client has none)
WOOCO composites table (wooco_composites): Materialized SQLite table on the Edge Server for WOOCO component definitions parsed from meta_wooco_components on each local composite parent—one row per component section × category slug in other[] (e.g. Extras with extras-set-1 and extras-set-4 → two rows). Stores component key, name, required/optional, multiple, pricing mode snapshot, and component_snapshot_json; does not list expanded catalog products (see Composite items table). Rebuild manual only: POST /api/admin/wooco-composites/rebuild. PRD: edge-server-wooco-composites-table.md. Avoid: wooco_components table (ambiguous with meta key name)
Composite kit browser: Read-only till overlay showing WOOCO component_key sections and attached catalog products from materialized wooco_composites + composite_items via GET /api/products/:id/composite-kit. Opened from View kit structure in the Composite fulfillment picker header—not from POS grid tap (grid tap opens the picker for selection). Back to configure closes the browser and returns to the picker. Does not accept component selection or Add to basket; selling uses the picker only. PRD: thin-client-composite-kit-browser-modal.md. Avoid: Kit picker, composite inspector modal (generic)
Composite kitchen display: On the Kitchen board, a Till Order line that has composite_children shows the parent name and quantity, with each component listed indented underneath (component name and quantity only in v1; SKU not shown on kitchen card). Quantities reflect persisted composite children, including parent quantity scaling. v1 ships with the Composite fulfillment picker slice; Receipt indented composite lines are a follow-on ticket. Web Orders do not require WOOCO component breakdown in v1; till parity first, Order ingestion mapping later. Avoid: Recipe view, sub-line display (generic)
Composite children (composite_children on Till Order line_items): The structured list on a composite basket line persisted with the parent in the order JSON. Kitchen and Receipt render components under the parent. Woo outbound sync for Till Orders sends the parent line only in v1 (component detail is LAN/kitchen-local). When the parent line quantity is > 1, each child’s stored quantity is per-kit quantity × parent quantity. Avoid: Sub-items, modifiers, line item meta (generic)
Composite pricing (checkout): The Thin Client computes parent line price and component unit prices in the Composite fulfillment picker (WOOCO pricing mode and discount percent) and sends them on checkout; the Edge Server persists those amounts without re-pricing on POST /api/orders in v1 (same trust model as simple lines). The picker shows a running estimated total in a sticky footer, updating as selections change. Parent and component unit prices use the same effective rule as the POS grid (posPriceFromRow: sale price when set, else regular price); v1 does not evaluate sale schedule dates in picker pricing. When Meta: wooco_custom_price is set on the parent, it overrides the parent base amount for only base and include base WOOCO modes (not component option prices). exclude base ignores parent base and sums components only. Monetary amounts round half-up to two decimal places per component contribution and line total (v1). Meta: wooco_shipping_fee is ignored at the till in v1 (in-store POS; no shipping fee on Composite pricing). Avoid: Server-side quote, dynamic repricing (v1)
Composite stock rules (picker): Out-of-stock component options are shown but not selectable (disabled with reason). If a required WOOCO component has no in-stock selectable option, Add to basket is disabled and staff see that the WOOCO composite cannot be sold right now. Optional components with all options out of stock follow WOOCO optional/none rules; staff cannot override stock in v1. Avoid: OOS override, backorder picker
Composite catalog gap (picker): A catalog product id listed in wooco_components but missing from the Edge Server master (or unresolved by the Composite definition API) is shown as unavailable — not on till (sync incomplete), not as out-of-stock. It is not selectable. A required component with no resolved selectable options blocks sale, same as all-OOS required components. Avoid: Unknown SKU fallback, silent omit
Local_Only (composite sales): Local_Only parents and components follow the same till rules as simple lines: sellable at the till when on the POS grid; composite_children persist on Till Orders and appear on Composite kitchen display; Woo outbound sync still omits Local_Only parents (parent-only payload when the parent is syncable). Avoid: Non-web kit block, composite sync exception
Invalid composite definition: Meta: wooco_components empty or unparsable for a WOOCO composite. Grid tap opens the picker shell with a clear error and Close / Cancel only (no basket change). Composite definition API returns a stable error (e.g. 422); broken composites remain visible in catalog management for staff to fix. Avoid: Fallback add, silent hide on grid
Composite picker catalog freshness (v1): The Composite fulfillment picker uses the composite definition and catalog snapshot from when it opened. It does not live-refresh on product_upserted while open; staff close and re-open to pick up price/stock changes. Basket lines keep prices from when they were added or last Edit components save. Avoid: Live repricing, socket-driven picker
Internal category (Woo): Category name Internal (case-sensitive). Any catalog product with categories[] containing that name is excluded from the checkout POS grid and its category tabs; staff can still browse and edit those rows in catalog management. Those rows may appear as selectable options in the Composite fulfillment picker when listed explicitly in wooco_components or when expanded from a category-sourced composite component (e.g. Internal > Extras). Internal products not in that definition or category expansion are never offered in the picker. Avoid: Internal product, staff-only SKU
Catalog product image (catalog management UI): The hero preview uses resolveProductImageUrl(product) (same as POS and composite pickers)—not raw images[0].src alone. Composite kit / fulfillment UI resolves from the parent catalog product in memory via the same helper—not from composite-kit API parentImage alone. Current shell: edit Images URLs in All fields + Update. #50 Attach photo becomes primary; URL field advanced/legacy (grill 2026-06-08). Avoid: Photo upload, gallery
Catalog product image file (Edge / till display): The optimized WebP binary the Edge Server stores for till display—not the Woo original. Applies to Local_Only products too (Local_Only affects Catalog push only, not LAN image download) (grill 2026-06-07, PRD #43). sharp pipeline: max edge 512 px, fit: inside, quality 80, no upscaling; max 5 MB Woo source download; 30 s HTTP timeout; 2 retries (3 attempts total, backoff ~1s then ~3s) before skip-with-warning (grill 2026-06-07, PRD #43). On-disk and LAN filenames use the catalog product id (e.g. {id}.webp); LAN local_src is http://{edge}:3005/images/{catalogProductId}.webp. Each file has a sidecar {id}.webp.meta.json recording source_url (primary Woo URL at download time); sidecars are not served over HTTP—/images/ exposes *.webp only (grill 2026-06-07, PRD #43). Primary image URL must be http:// or https://; otherwise skip download with warning—no file, no local_src (variation parent fallback still applies at read time) (grill 2026-06-07, PRD #43). GET /api/products keeps images[0].src as the Woo HTTPS URL; when LAN file is fresh (sidecar source_url matches SQLite Images), payload includes images[0].local_src; otherwise local_src omitted. No JPEG twin on disk in v1; clients that cannot decode WebP fall through to remote src. SQLite images column unchanged for Catalog push (grill 2026-06-07, PRD #43). Avoid: SKU image file, thumbnail blob, gallery file set, JPEG fallback file
Pinned fields (catalog management UI): The always-visible block in the product detail pane (current shell): name, regular and sale price; stock quantity when tracked, otherwise —. Catalog product id, SKU (change in All fields only), Local_Only, and website visibility are read-only in pinned fields; the label shows the current Woo value (e.g. Catalog, Search). Hide from website shown when visibility ≠ hidden; Show on website shown only when visibility = hidden (sets visible). When #50 Product manage form ships: Visibility moves to an editable dropdown (draft + Update); hide/show action bar unchanged for quick hidden/visible when draft clean (grill Q26 A). Avoid: Quick edit, main form
Make local only (staff action): Sets local_only on the catalog product via PATCH after staff confirmation. The product is not pushed to the website while Local_Only is on. Paired with Allow website sync on the action bar. Uses the same draft-clean / draft-guard rules as hide/show. On #50 Product manage form, Local only is a read-only badge—not a toggle (grill Q25 A). Avoid: Internal SKU toggle, delist
Allow website sync (staff action): Clears local_only via PATCH after staff confirmation (copy warns the entire product will be published). Enqueues catalog push with the full row immediately—no extra Update required. Paired with Make local only. Avoid: Publish to web, go live
Category panel (catalog management UI): A collapsible group in the catalog management left column keyed by Woo category name. Multiple panels may be expanded at the same time; expanding one does not collapse others. Sellable catalog rows inside a panel are sorted A–Z by product name. A product with multiple categories appears in every panel whose name matches one of its categories[] entries; selection is always by catalog product id, not by panel row identity. Rows with type === 'variable' (parent products) are omitted; variation rows and types such as composite, bundle, and grouped parents are listed like simple products. Products with no categories appear only in the Uncategorized panel (always last; named panels sorted A–Z above it). The checkout POS grid is not required to use the same filter. Composite category filter (omit Composite-named panels and zero-price composite-tagged rows) — rules in #50; accordion implements when #50 ships (grill 2026-06-08). Avoid: Tab, shelf
Uncategorized (catalog management UI): The bottom category panel listing sellable catalog rows whose categories[] is empty. Avoid: No category, misc
Sellable catalog row: A catalog product row staff may select in catalog management: simple products, variations, and non-variable types — not a variable-type parent row that only groups variations. Avoid: SKU line, menu item
Catalog search (catalog management UI): A filter on the left column that limits visible sellable catalog rows by case-insensitive substring match on product name or SKU. Clearing the search restores the full list. When nothing matches globally, show no products match in the left column; do not clear the selected product in the detail pane. Default (shell): while search is active, hide category panels with zero matching rows. When #50 composite filter ships (grill Q21 C): active search replaces the accordion with a flat Search results list; each row includes a subtitle of assigned category path(s); matches include rows hidden by composite panel/row rules until search is cleared. Avoid: Global search, product finder
Configure composite (catalog management action): Opens the Composite conversion screen from product detail only for persisted composite catalog product rows (hidden on simple, bundle, grouped, and variation detail). Selected catalog product is pre-loaded; staff pick materialized WOOCO groups and save Meta: wooco_components. Avoid: Composite Builder, left-panel configure
Composite conversion screen (catalog management UI): Full-width catalog view (list hidden) for one composite parent: searchable dropdown of distinct wooco_composites groups (GET /api/catalog/wooco-composite-groups, one row per category_path), pills with remove (×), live wooco_components JSON preview, Save composite. When a group snapshot other[] has multiple slugs, the edge server expands related wooco_composites rows and merges slugs into one WPClever section (WPClever Pro shape). Back / kit then catalog draft guards unchanged. Distinct from the Composite fulfillment picker (selling) and Composite kit browser (read-only structure). Avoid: Accordion section panels (legacy), product configurator (generic)
WOOCO snapshot merge (staff product edit): When staff save a WOOCO composite from the Composite conversion screen, each WOOCO component section is written by merging the loaded WPClever object (parsed raw snapshot) with till-edited fields from the panel (title, categories, WOOCO exclude list, WOOCO component selector, etc.). Keys the staff did not model in the UI are preserved unless that section was removed or a modeled field was changed. New sections start from a fresh object. Avoid: Full meta rewrite, passthrough-only (no overlay)
WOOCO component section (composite definition): One keyed block in Meta: wooco_components (property name = WOOCO component key, e.g. 56ou). Staff-facing panel in the Composite conversion screen with till editors for the same fields WPClever exposes per component (not read-only passthrough): section title, description, source (products vs product_cat), category terms / explicit products, order by & order, exclude products, default option, required, multiple selection, per-item and component qty limits, custom quantity, component new price, WOOCO component selector, etc. Saves via WOOCO snapshot merge only where a modeled field overlays the same key. Same concept as a row in the WOOCO composites table for category-sourced keys—not a composite basket line child. Avoid: Component panel (ambiguous with picker UI), composite line
WOOCO component selector (selector meta): WPClever Selector interface for a WOOCO component section on the website. Stored values include default, list, grid_2, grid_3, and grid_4 (not a single grid string). v1 Composite conversion screen exposes staff-facing choices that map to these values; save persists via WOOCO snapshot merge. v1 Composite fulfillment picker does not change layout from selector—picker keeps radio/checkbox rows per ADR 0005. Avoid: POS grid layout, picker grid mode (v1), generic “grid” (ambiguous)
Hide from website (staff action): Sets the product’s catalog visibility to hidden on the Edge Server master and pushes catalog_visibility: hidden to Woo while Published stays on. Shown on the action bar when visibility ≠ hidden. The product remains in the LAN catalog and on the till checkout grid; it is not the same as Local_Only (never on the website) or unpublishing to draft. If a dirty catalog draft exists, save / discard / cancel runs before the visibility PATCH; when clean, applies immediately without pressing Update. Success toast matches Update (context-aware when push applies). Avoid: Delist, unpublish, remove from till
Show on website (staff action): Sets Visibility in catalog to visible and pushes catalog_visibility: visible to Woo while Published stays on. Shown on the action bar only when visibility = hidden (paired with Hide from website; never both at once). Staff must confirm before apply (same pattern as hide). If a dirty catalog draft exists, save / discard / cancel runs before the visibility PATCH; when clean, applies immediately without pressing Update. catalog / search visibility is set via the manage-form Visibility dropdown + Update (#50; grill Q26 A)—not via Show on website. Avoid: Publish, unhide, relist
Label: A die-cut or continuous stock print for a product, rendered HTML → PDF and sent to the configured label printer (typically Brother QL-810W). Avoid: Sticker, tag
Print Label (catalog management action): Queues a Label Print job for the selected catalog product id. Staff enter quantity in a dialog each time (default 1, integer 1–999). If the catalog draft is dirty, the till persists the draft (PATCH) before printing so the label matches saved master data. Not blocked by zero stock. Cancelling the quantity dialog skips the print job only (a prior auto-save from dirty draft is not rolled back). v1 checkout grid quick-print removed; this is the only staff label entry point when #49 ships. Requires catalog.print_label on the Till session (client hide/disable + Edge Server enforcement on label print routes); distinct from catalog.edit and catalog.upload_image (grill #49 2026-06-08, Q2 B, Q3 A). Avoid: Print sticker, tag print, grid print, pos.print_label
Print label (checkout grid action): Removed for #49 (grill 2026-06-08, Q2 B). Was a quick Label Print job (qty 1, no dialog) from the POS product grid. Superseded by catalog management Print Label only; pos.print_label is not in the permission catalogue. Avoid: Grid print, quick print, pos.print_label
Catalog product id: The WooCommerce numeric product or variation id used as the primary key in the product master and in print/cart requests. Avoid: Product id (ambiguous), SKU (different identifier)
Print route: Server configuration mapping document types to CUPS/IPP queues (labelPrinter, receiptPrinter), each independently set. Avoid: Printer driver, spooler (generic)
Print job: An asynchronous server-side render-and-spool task; clients receive 202 with a job id and poll status (or subscribe via socket later). Avoid: Print request, spool (verb)
Order source (source on socket/API payloads): Whether an Order originated at the till (pos) or from Woo inbound ingest (woocommerce). Avoid: Channel, origin (generic)
Product upserted (socket event): Edge Server broadcast when a catalog product row changes (payload includes catalog product id). Catalog management client handler ships with shell; server emitter is a non-blocking follow-up if not yet present. Reloads the selected product when ids match and catalog draft is clean; if dirty, toast only (grill 2026-06-08). Avoid: catalog_changed, product_updated
Example dialogue
Dev: “Till
ORD-100synced to Woo #500 and the poller ingested #500 — one ticket or two?” Domain expert: “Two. Staff see the Till Order they rang and the Web Order from the website pipeline; they complete each independently.”
Dev: “What Local Order Id does a pure website order get?” Domain expert: “
WOO-4521— staff can tell it from tillORD-…ids at a glance.”
Dev: “Someone changed a price in wp-admin — what happens?” Domain expert: “Nothing locally. Next till edit or a full catalog push overwrites the website from our copy.”
Dev: “We edit a customer’s phone at the till — does Woo update?” Domain expert: “Yes. Same rules as products.
repeat_ordersstays local only.”
Flagged ambiguities
- Resolved: Order id on till checkout APIs means the Till Order’s Local Order Id.
- Resolved: Manual merge of Till Order + Web Order is out of scope for v1 (staff use two cards; see ADR 0001).
- Resolved: Customer match key uses WP user → Woo customer id → email hash (priority order).
- Resolved: Catalog authority is local-after-bootstrap (ADR 0002); orders still ingest from Woo (ADR 0001).
- Resolved: Hide from website = visibility hidden, published unchanged; till catalog unchanged (grill 2026-06-02, catalog management PRD).
- Resolved: Multi-category products appear in every matching category panel; one catalog product id (grill 2026-06-02).
- Resolved: Dirty catalog draft → save / discard / cancel on navigation, window close, or catalog refresh (grill 2026-06-02).
- Resolved: Catalog management accordion lists sellable catalog rows only (
variableparents excluded); POS grid unchanged (grill 2026-06-02). - Resolved: Catalog search in v1 — filter by name and SKU substring (grill 2026-06-02).
- Resolved: v1 staff product edit = any PATCH-eligible field, not a fixed four-field form (grill 2026-06-02).
- Resolved: Detail layout = pinned fields + collapsible All fields (grill 2026-06-02).
- Resolved: Show on website action in v1 (paired with hide; visibility
visible/hidden) (grill 2026-06-02). - Resolved: Hide/show PATCH immediate when draft clean; else draft guard first (grill 2026-06-02).
- Resolved: Electron catalog management window is a singleton (focus, do not duplicate) (grill 2026-06-02).
- Resolved: Print Label auto-saves dirty catalog draft before print job (grill 2026-06-02).
- Resolved: v1 = Electron catalog window only; Android full-screen route v1.1 + feature flag (grill 2026-06-02).
- Resolved: Apply discount code disabled + tooltip in v1 (grill 2026-06-02).
- Resolved: Show on website requires confirmation dialog (grill 2026-06-02).
- Resolved: Empty categories → Uncategorized panel at bottom (grill 2026-06-02).
- Resolved: Local_Only via action bar (Make local only / Allow website sync) + confirm; badge read-only in pinned fields (grill 2026-06-02).
- Resolved: Label quantity via dialog on each print (default 1) (grill 2026-06-02).
- Resolved: Accordion allows multiple expanded category panels (grill 2026-06-02).
- Resolved: composite / bundle / grouped parents stay in accordion; only variable parents excluded (grill 2026-06-02).
- Resolved: Post-Update toast = context-aware — saved on till when Local_Only; saved on till; syncing to website… when push applies (grill 2026-06-08).
- Resolved: Revert button on dirty catalog draft (no PATCH) (grill 2026-06-02).
- Resolved: Images = hero via
resolveProductImageUrl; edit Images URLs in All fields only in v1 (grill 2026-06-02; display path grill 2026-06-07 PRD #43). - Resolved: Catalog product image file —
{catalogProductId}.webpon Edge, async catalog image phase,REFRESH_PRODUCT_IMAGEsync_jobs, hybrid product image cache,src/local_srcsplit (grill 2026-06-07, PRD #43). - Resolved: Catalog Electron window default 1600×900 (larger than main till) (grill 2026-06-02).
- Resolved: Missing product after refresh → clear selection, empty detail (grill 2026-06-02).
- Resolved: Manage this product from POS grid pre-selects catalog product id (grill 2026-06-02).
- Resolved: Empty catalog search → left message only; keep detail selection (grill 2026-06-02).
- Resolved: Manage this product disabled on
variableparent grid cards (grill 2026-06-02). - Resolved: Keep checkout grid Print label alongside catalog management print (grill 2026-06-02) — superseded: grid print removed; labels catalog-only (#49; grill 2026-06-08, Q2 B).
- Resolved: Catalog Print Label gated by
catalog.print_labelonly (notcatalog.editorcatalog.open) (#49; grill 2026-06-08, Q3 A). - Resolved: Manage this product requires
catalog.open(same as Manage catalog); read-only withoutcatalog.edit(#49; grill 2026-06-08, Q13 A). - Resolved: Non-empty inherited basket → one-time toast after sign-in (no persistent banner) (#49; grill 2026-06-08, Q12 B).
- Resolved: Current basket preserved on Switch user and Logout; checkout attributes to operator at submit (#49; grill 2026-06-08, Q11 C).
- Resolved: Main till header exposes both Switch user and Logout; both revoke Till session and show boot PIN modal (#49; grill 2026-06-08, Q10 C).
- Resolved: Edge Server catalog read routes (
GET /api/products, composite definition reads,GET /images/{id}.webp) require Till session bearer when not in bootstrap; no capability check beyond signed-in (#49; grill 2026-06-08, Q9 A). - Resolved: No
settings.*grants → Settings cog hidden;#/settingsblocked (#49; grill 2026-06-08, Q8 A). - Resolved: Delete permission profile — allow with confirmation; clear
last_applied_profile_idon affected users;permissions_jsonunchanged (#49; grill 2026-06-08, Q7 B). - Resolved: PRD #49 targeted amend — Grill decisions section + contradiction fixes; full story renumber deferred (#49; grill 2026-06-08, Q20 C).
- Resolved: #49 grill ADRs — 0014 umbrella + 0015–0019.
- Resolved: Capacitor Android — full route parity with Electron; route-level session lock overlay in #49; Done includes Android smoke (#49; grill 2026-06-08, Q28 A).
- Resolved: Action Capability Registry =
shared/permissions/permissionCatalogue.ts(types/helpers) +actionCapabilityRegistry.ts(tree) (#49; grill 2026-06-08, Q27 B). - Resolved: Settings separate PIN removed in PR 1 — Settings uses boot Till session token (#49; grill 2026-06-08, Q24 A).
- Resolved:
till_permission_profilestable + seed in PR 1; profile admin UI PR 4 (#49; grill 2026-06-08, Q23 A). - Resolved: PR 2 = registry + client gates + server
checkout.submitenforce; remaining mutators PR 3 (#49; grill 2026-06-08, Q22 B). - Resolved: Implementation PR 1 = security spine (Edge session, sign-in/out, authenticated catalog reads, boot PIN,
{}→ Manager upgrade) (#49; grill 2026-06-08, Q21 A). - Resolved: Architecture captured in ADR 0014 (#49; grill 2026-06-08, Q19 A).
- Resolved: One Till session per app instance — PIN on any window unlocks all windows; per-surface capability gates unchanged (#49; grill 2026-06-08, Q18 A).
- Resolved: Logout requires confirm; Switch user goes straight to PIN modal (#49; grill 2026-06-08, Q17 A).
- Resolved: Session end on auxiliary Electron windows → lock overlay + PIN re-auth (target behaviour Q16 A); ships post-#49; interim PR 2 blocks aux window open without session (Q25 B).
- Resolved: Till session TTL expiry → PIN modal in place; cart preserved; Session expired — sign in again + inherited-basket toast if non-empty (#49; grill 2026-06-08, Q15 A).
- Resolved: Fourth seeded profile Training — grid/cart only,
checkout.submit: false(#49; grill 2026-06-08, Q14 A). - Resolved: Starter permission profiles seeded on first install (Manager, Cashier, Kitchen, Training); first wizard Till user gets Manager applied (#49; grill 2026-06-08, Q6 A).
- Resolved:
catalog.openwithoutcatalog.edit= read-only catalog access (full detail read-only; mutators blocked; Print Label viacatalog.print_label) (#49; grill 2026-06-08, Q5 A). - Resolved: Empty
{}permissions block boot PIN sign-in until a manager fixes the account (#49; grill 2026-06-08, Q4 C; supersedes PRD story 81 “sign in with grid only”). - Resolved: Boot PIN gate blocks all till API calls (including
GET /api/products) until sign-in; no background catalog preload (#49; grill 2026-06-08, Q1 A). - Resolved: Grid print = qty 1, no dialog; catalog print = dialog + save-then-print if dirty (grill 2026-06-02).
- Resolved: Make local only and Allow website sync both require confirmation (grill 2026-06-02).
- Resolved: Staff menu label Manage catalog (grill 2026-06-02).
- Resolved: Category panel order A–Z, Uncategorized last (grill 2026-06-02).
- Resolved: Pre-select expands matching panels + scrolls row into view (grill 2026-06-02).
- Resolved: Catalog window = same Vue app,
#/catalogroute (grill 2026-06-02). - Resolved: Rows within a category panel sorted A–Z by name (grill 2026-06-02).
- Resolved: Action-bar PATCH success toasts match Update rules (grill 2026-06-02).
- Resolved: Failed PATCH → alert + preserve catalog draft (grill 2026-06-02).
- Resolved: All fields collapsed by default on selection (grill 2026-06-02).
- Resolved: Catalog product id read-only in header; not editable in All fields (grill 2026-06-02).
- Resolved: Category list rows show name, SKU, price (grill 2026-06-02).
- Resolved:
#/catalogroute = catalog UI only; Kitchen board on#/kitchen+ Electron kitchen window (ADR 0004; supersedes main-till strip). - Resolved: Manage this product requires Are you sure? confirmation before opening catalog (2026-06-02).
- Resolved: POS product grid and kitchen orders use shared responsive-card-grid (
auto-fill,minmax(220px, 1fr)) (2026-06-02). - Resolved: Checkout till basket column ~17.5% width; product area ~82.5% (2026-06-02).
- Resolved: Catalog selection does not add to basket (grill 2026-06-02).
- Resolved: Website visibility read-only in pinned fields; hide/show on action bar (grill 2026-06-02).
- Resolved: Catalog search hides empty category panels (grill 2026-06-02).
- Resolved: Catalog fetch failure → error banner + Retry in open window (grill 2026-06-02).
- Resolved: Print Label allowed at zero stock (grill 2026-06-02).
- Resolved: Pinned visibility shows raw Woo value; Hide from website when visibility ≠
hidden; Show on website only whenhidden→ setsvisible(grill 2026-06-08; supersedes 2026-06-02 hide/show pairing). - Resolved: Cancel print quantity dialog → no print; no draft rollback (grill 2026-06-02).
- Resolved: v1 catalog management has no staff auth until #49; LAN trust only; then
catalog.open/catalog.edit/catalog.create_product(grill 2026-06-08; supersedes 2026-06-02 deferral). - Resolved: Busy state disables mutating actions except Refresh catalog (allowed in parallel) (grill 2026-06-02).
- Resolved: Null stock → — in pinned; manage stock in All fields (grill 2026-06-02).
- Resolved: Print quantity dialog integer 1–999 (grill 2026-06-02).
- Resolved: Manage catalog opens with all category panels collapsed (grill 2026-06-02).
- Resolved: Pinned prices — regular ≥ 0; sale ≥ 0 or empty (grill 2026-06-02).
- Resolved: Success = toast; failure = alert (grill 2026-06-02).
- Resolved: Draft-guard Discard = Revert then continue pending action (grill 2026-06-02).
- Resolved: All fields excludes editable Type and Parent (read-only); id and local_only excluded (grill 2026-06-02).
- Resolved: Draft-guard Save = Update then continue (action-bar after save) (grill 2026-06-02).
- Resolved: Close main till → close catalog window (draft guard first) (grill 2026-06-02).
- Resolved: Sale schedule dates editable in All fields only (grill 2026-06-02).
- Resolved: Socket reload selected product only when draft clean; toast if dirty (grill 2026-06-02).
- Resolved: New product requires name, SKU, regular price, ≥1 category (grill 2026-06-02).
- Resolved: New product creates
simpleorcompositeat till; variable / bundle / grouped create out of scope (grill 2026-06-08; supersedes “simple only” 2026-06-02). - Resolved: Create categories = multi-select existing + optional new name (grill 2026-06-02).
- Resolved: New product with dirty draft → draft guard first (grill 2026-06-02).
- Resolved: Delete product out of scope v1 (grill 2026-06-02).
- Resolved: Duplicate SKU on create/update →
alert: SKU exists (grill 2026-06-02). - Resolved: Tax status / Tax class not editable at till v1 (wp-admin) (grill 2026-06-02).
- Resolved: New product defaults to Local_Only (no web push until Allow website sync) (grill 2026-06-02).
- Resolved: After create, use full POST body if present else refetch list (grill 2026-06-02).
- Resolved: New product control left column above catalog search (grill 2026-06-02).
- Resolved: Allow website sync → confirm full publish + immediate catalog push (grill 2026-06-02).
- Resolved: Internal Woo category products hidden on checkout POS grid only; catalog management unchanged (2026-06-02).
- Resolved: Composite conversion screen is the single full-page kit editor in catalog management (replaces Composite Builder overlay); product detail deep-links with pre-selected parent (grill 2026-06-03, composite conversion PRD #18).
- Resolved: WOOCO snapshot merge on save — overlay staff-edited fields onto each section’s loaded snapshot; do not strip unmodeled WPClever keys (grill 2026-06-03, composite conversion PRD #18).
- Resolved: Composite conversion screen replaces accordion + product detail while open; dedicated search (name, SKU, id); Back restores list/detail (grill 2026-06-03, composite conversion PRD #18).
- Resolved: WOOCO component selector edited and saved at till; Composite fulfillment picker layout unchanged in v1 (persist-only) (grill 2026-06-03, composite conversion PRD #18).
- Resolved: + Add component enabled only when draft type is Composite; auto-select Composite when loaded product has parsable non-empty
wooco_componentseven if WooTypeis stillsimple(grill 2026-06-03, composite conversion PRD #18). - Resolved: + Add component appends an empty WOOCO component section panel with Source default Categories (title-first, then category typeahead) (grill 2026-06-03, composite conversion PRD #18).
- Resolved: WOOCO component selector till dropdown matches WPClever: Default, List, Grid 2/3/4 columns (
default,list,grid_2,grid_3,grid_4) (grill 2026-06-03, composite conversion PRD #18). - Resolved: WOOCO component section panels include full till editors for WPClever component fields (exclude, orderby, default option, qty limits, etc.) in v1—not read-only “website extras” (grill 2026-06-03, composite conversion PRD #18).
- Resolved: Exclude and Default option use LAN catalog product search pickers (not comma text); Exclude hidden when section Source is explicit products (grill 2026-06-03, composite conversion PRD #18).
- Resolved: Single / Composite on both product detail and Composite conversion screen, synced via one catalog draft; Configure composite deep-links with pre-selected parent; existing draft guard before open when detail is dirty (grill 2026-06-03, composite conversion PRD #18).
- Resolved: WOOCO exclude list on a category-sourced section filters selectable options at checkout (Composite catalog resolver / Composite definition API) in v1, not website-only (grill 2026-06-03, composite conversion PRD #18).
- Resolved: WOOCO order by / order on a section drive option sort order in Composite catalog resolver when not
default;defaultkeeps documented till fallback (grill 2026-06-03, composite conversion PRD #18). - Resolved: Composite conversion screen v1 includes duplicate section and reorder sections (up/down or drag); section order matches checkout picker section order (grill 2026-06-03, composite conversion PRD #18).
- Resolved: Save composite persists the full catalog draft for the selected catalog product (same PATCH as Update), not WOOCO meta alone (grill 2026-06-03, composite conversion PRD #18).
- Resolved: Composite conversion screen shows editable parent pinned fields (name, price, categories, etc.) for the selected catalog product, not kit-only (grill 2026-06-03, composite conversion PRD #18).
- Resolved: Duplicate section always assigns a new component key; never reuses the source key (grill 2026-06-03, composite conversion PRD #18).
- Resolved: Configure composite button in catalog left chrome opens Composite conversion screen with no parent pre-selected; detail Configure composite deep-links with selection (grill 2026-06-03, composite conversion PRD #18).
- Resolved: WOOCO component section panels use accordion UX (one expanded at a time; new/duplicated section opens expanded) (grill 2026-06-03, composite conversion PRD #18).
- Resolved: Sell-time WOOCO exclude list and order by / order documented in ADR 0008; selector layout still website-only at picker (grill 2026-06-03, composite conversion PRD #18).
- Resolved: Per-section qty limit fields (min/max, m_min/m_max) visible only when Custom quantity is Yes, matching WPClever
wooco_show_if_custom_qty(grill 2026-06-03, composite conversion PRD #18). - Resolved: New product created as Composite opens Composite conversion screen with that catalog product pre-selected (grill 2026-06-03, composite conversion PRD #18).
- Resolved: New WOOCO component section panels default WOOCO component selector to
default(WPClever empty component) until staff change it (grill 2026-06-03, composite conversion PRD #18). - Resolved: When Multiple selection is Yes, Default selector is disabled; if selector was
default, auto-set togrid_3(WPCleverwooco_component_multiple) (grill 2026-06-03, composite conversion PRD #18). - Resolved: After parent selection on Composite conversion screen, search collapses to summary + Change product (reopens search; dirty guard on switch) (grill 2026-06-03, composite conversion PRD #18).
- Resolved: Parent WOOCO pricing/limit fields live in a collapsible Kit pricing & limits section (collapsed by default) on Composite conversion screen (grill 2026-06-03, composite conversion PRD #18).
- Resolved: Composite conversion screen uses full catalog management window width; CatalogLeftPanel hidden until Back (grill 2026-06-03, composite conversion PRD #18).
- Resolved: GitHub #16 closed as superseded by #18 (composite builder PRD absorbed) (grill 2026-06-03).
- Resolved: WOOCO component new price (
priceon a section) editable and saved from Composite conversion screen; Composite pricing at checkout ignores it in v1 (website-only until follow-on) (grill 2026-06-03, composite conversion PRD #18). - Resolved: Leaving Composite conversion screen uses two guards: kit draft guard (unsaved WOOCO panel edits — discard kit / cancel) then catalog draft guard (pinned fields / type — save / discard / cancel) when both dirty; single-kind dirty shows only the relevant guard (grill 2026-06-03, composite conversion PRD #18).
- Resolved: Save composite blocked until at least one WOOCO component section exists (validation before PATCH) (grill 2026-06-03, composite conversion PRD #18).
- Resolved: Refresh catalog on Composite conversion screen uses kit draft guard then catalog draft guard (same order as leave); reload parent + panel baseline when clean (grill 2026-06-03, composite conversion PRD #18).
- Resolved: Save composite requires ≥1 section and per-section validation (title; categories → path; products → selection) via extended
validateBuilderState(grill 2026-06-03, composite conversion PRD #18). - Resolved: Escape on Composite conversion screen behaves like Back (same kit then catalog guards) (grill 2026-06-03, composite conversion PRD #18).
- Resolved:
product_upsertedfor the open parent on conversion: toast if kit or catalog dirty; reload row + panel baseline when clean (grill 2026-06-03, composite conversion PRD #18). - Resolved: Staff may remove the last WOOCO component section; Save composite stays blocked until ≥1 valid section remains (grill 2026-06-03, composite conversion PRD #18).
- Resolved: After successful Save composite, staff remain on Composite conversion screen with kit and catalog baselines reset to saved state (success toast) (grill 2026-06-03, composite conversion PRD #18).
- Resolved: View kit structure on conversion opens Composite kit browser when parent is persisted with a valid definition (enabled after first successful Save composite or when loading an existing composite) (grill 2026-06-03, composite conversion PRD #18).
- Resolved: Configure composite from chrome with no parent shows search-first empty state (no panels until a catalog product is selected) (grill 2026-06-03, composite conversion PRD #18).
- Resolved: Configure composite is disabled until the parent catalog product has a persisted server id (unsaved New product draft must be saved first); post-save Composite creation still auto-opens conversion with that id (grill 2026-06-03, composite conversion PRD #18).
- Resolved: Composite conversion parent search omits bundle, grouped, and variation rows (only simple / composite and repairable simple+
wooco_componentsappear) (grill 2026-06-03, composite conversion PRD #18). - Resolved: Product detail Configure composite is shown for persisted simple / composite parents (not only when type is already Composite); opening leaves Single until staff set Composite on the conversion screen (+ gated) (grill 2026-06-03, composite conversion PRD #18).
- Resolved: Product detail Configure composite is hidden for bundle, grouped, and variation rows (same eligibility as conversion parent search) (grill 2026-06-03, composite conversion PRD #18).
- Resolved: Change product with only kit draft dirty runs kit draft guard (Discard kit changes / Cancel only — no Save in that dialog); when only catalog draft dirty, catalog draft guard; when both, kit then catalog (grill 2026-06-03, composite conversion PRD #18).
- Resolved: Tapping Configure composite while already on Composite conversion screen resets to search-first empty state (kit then catalog guards when dirty); same affordance on conversion header when left chrome is hidden (grill 2026-06-03, composite conversion PRD #18).
- Resolved: Back from Composite conversion screen returns to catalog list with the same parent catalog product still selected in accordion + detail (grill 2026-06-03, composite conversion PRD #18).
- Resolved: Till Order composite = nested parent line +
composite_childrenonline_items(grill 2026-06-02, composite fulfillment picker PRD). - Resolved: Persisted child qty = per-kit qty × parent line qty (grill 2026-06-02, composite fulfillment picker PRD).
- Resolved: Identical composite configuration → one composite basket line, increment
quantity(grill 2026-06-02). - Resolved: WOOCO parse + resolved options via Composite definition API on Edge Server v1 (grill 2026-06-02).
- Resolved: v1 slice = picker + basket + Till Order persist + Composite kitchen display; Receipt composite lines later (grill 2026-06-02).
- Resolved: Internal category in Composite fulfillment picker = recipe exception (explicit ids or category-sourced expansion) (grill 2026-06-02).
- Resolved: Grid tap composite = new picker session; Edit components = edit existing basket line (grill 2026-06-02).
- Resolved: Composite pricing = Thin Client authoritative at checkout v1 (grill 2026-06-02).
- Resolved: Composite stock rules = hard block; OOS options disabled (grill 2026-06-02).
- Resolved: Web Orders WOOCO kitchen breakdown out of v1 slice (grill 2026-06-02).
- Resolved: Composite catalog gap = show unavailable, non-selectable; block if required (grill 2026-06-02).
- Resolved: Local_Only composites/components = same till/Woo rules as simple lines (grill 2026-06-02).
- Resolved: Invalid composite definition = error modal + Cancel only (grill 2026-06-02).
- Resolved: Composite picker catalog freshness = snapshot at open; no live refresh v1 (grill 2026-06-02).
- Resolved: Edit components Cancel restores basket line; confirm updates line in place (grill 2026-06-02).
- Resolved: Composite fulfillment picker v1 = checkout modal only; Electron window v1.1 (grill 2026-06-02).
- Resolved: Edit mode confirm label Update basket (grill 2026-06-02).
- Resolved: bundle / grouped grid tap = one-tap parent add v1 (grill 2026-06-02).
- Resolved: Optional WOOCO component = explicit None choice (grill 2026-06-02).
- Resolved: Composite pricing uses grid-effective price (sale else regular) for parent and components v1 (grill 2026-06-02).
- Resolved: Composite operator copy (before/after text) shown in picker when non-empty (grill 2026-06-02).
- Resolved: Multi-select composite component = checkboxes + min/max when WOOCO allows (grill 2026-06-02).
- Resolved: Edit components merge into existing line when config matches another basket line (grill 2026-06-02).
- Resolved: Same products rule = prevent forbidden cross-component duplicates (grill 2026-06-02).
- Resolved: Sale schedule dates ignored in Composite pricing v1 (matches
posPriceFromRow/ grid) (grill 2026-06-02). - Resolved: Composite kitchen display = component name + qty only v1 (grill 2026-06-02).
- Resolved:
wooco_custom_priceoverrides parent base for only base / include base when set (grill 2026-06-02). - Resolved: Composite basket line UI = collapsed + expand breakdown v1 (grill 2026-06-02).
- Resolved: Composite API failure → degraded client parse from cached product list + Retry (grill 2026-06-02).
- Resolved: Composite pricing rounding = half-up to 2 decimals v1 (grill 2026-06-02).
- Resolved:
wooco_shipping_feeignored in Composite pricing v1 (grill 2026-06-02). - Resolved: v1 includes category-sourced composite component expansion (grill 2026-06-02).
- Resolved: Category-sourced picker options = name + price only (grill 2026-06-02).
- Resolved: Required category source with zero expanded options = hard block (grill 2026-06-02).
- Resolved: Multiple category paths per component = separate sub-sections per path (dedupe across groups) (grill 2026-06-02; corrected from union).
- Resolved: Category sub-section order = WOOCO path order; options A–Z by name within each (grill 2026-06-02).
- Resolved: Category match = WOOCO-selected category string equals a Woo
Categoriessegment (grill 2026-06-02). - Resolved: WOOCO-selected category in meta = name/path string (match Woo
Categoriessegments) (grill 2026-06-02). - Resolved: WOOCO parser/resolver in shared WOOCO module (server + client fallback) (grill 2026-06-02).
- Resolved: Composite configuration fingerprint = sorted child ids + per-kit qtys (grill 2026-06-02).
- Resolved: Picker running estimated total = sticky footer v1 (grill 2026-06-02).
- Resolved: Picker open blocks other checkout till layout actions v1 (grill 2026-06-02).
- Resolved: Before/after text = plain text (strip HTML) v1 (grill 2026-06-02).
- Resolved: Composite total limits = inline error + disabled confirm v1 (grill 2026-06-02).
- Resolved: Catalog management PRD = implemented shell; #50 supersedes detail + create UX + composite accordion filter + Attach photo (grill 2026-06-08).
- Resolved:
product_upserted— client handler ships first; server emitter non-blocking follow-up (grill 2026-06-08). - Resolved: Create SKU — required trim + non-empty in current shell; #50 auto
Till+DDMM+MMSSwhen blank at save (grill 2026-06-08; supersedes 2026-06-02 trim rule alone). - Resolved: Default visibility on create =
catalogwhen #50 ships (grill 2026-06-08). - Resolved: Validation errors =
alertuntil #50 inline form errors (grill 2026-06-08). - Resolved: Attach photo gated by
catalog.upload_image(separate fromcatalog.edit) when #49 ships (grill 2026-06-08, Q15 B). - Resolved: Hidden Brands on create =
store.companyNameif set, elsestore.siteTitle(Woo site title from bootstrap) (grill 2026-06-08, Q16 B). - Resolved: Manage this product + composite filter — auto Show composite categories + toast when product only in hidden panels (#50; grill 2026-06-08, Q17 A).
- Resolved: Create wizard — Local only, Published, Visibility
catalog, Allow reviews applied silently; Review shows read-only callouts; publish fields editable on Product manage form only (#50; grill 2026-06-08, Q18 A). - Resolved: Attach photo on Product manage form edit only—not on create wizard (#50; grill 2026-06-08, Q19 A).
- Resolved: Legacy Images URL edit in collapsed Advanced (website) on manage form; Attach photo primary when
catalog.upload_imagegranted (#50; grill 2026-06-08, Q20 A). - Resolved: Active Catalog search with composite filter = flat Search results list; category path subtitle per row; includes composite-hidden matches (#50; grill 2026-06-08, Q21 C).
- Resolved: Meal Preps block includes Serving size →
meta__nutrition.serving_sizefor label “per {serving}” (#50; grill 2026-06-08, Q22 A). - Resolved: CategoryDualListPicker Add new category — staff-entered path string on
Categoriescolumn; no Woo term API in v1 (#50; grill 2026-06-08, Q23 A). - Resolved: Attach photo on Local_Only — LAN file only until Allow website sync; full-row push then uploads image to Woo (#50; grill 2026-06-08, Q24 A).
- Resolved: Local only on Product manage form = read-only badge; flip only via action bar confirmations (#50; grill 2026-06-08, Q25 A).
- Resolved: Visibility dropdown on manage form = draft + Update; hide/show action bar = immediate
hidden/visiblewhen draft clean (#50; grill 2026-06-08, Q26 A). - Resolved: Composite on manage form — Update blocked until Configure composite completes with ≥1 valid section (#50; grill 2026-06-08, Q27 C).
- Resolved: Composite → Single on manage form — confirm dialog; Update clears
wooco_components(#50; grill 2026-06-08, Q28 A). - Resolved: #50 PRD status Ready for agent — grilled with implementation order: (1) composite filter + search, (2) manage form + wizard, (3) image upload + Edge APIs (grill 2026-06-08, Q29 C).
- Resolved: SKU read-only pinned; editable All fields only (grill 2026-06-02).
- Resolved: POS staff accounts are Till users (Edge Server PIN accounts); not WordPress users (grill 2026-06-04, settings menu PRD #35).
- Resolved: Settings menu v1 uses permission gate — all sub-sections locked unless the signed-in Till user’s
permissions_jsongrants that section (grill 2026-06-04, settings menu PRD #35). - Resolved: First Till user is created in the first-run setup wizard with full
settings.*permissions; no empty-table LAN bypass (grill 2026-06-04, settings menu PRD #35). - Resolved: Settings session — PIN once; permitted sections stay unlocked until Logout; close/reopen Settings does not re-prompt (grill 2026-06-04, settings menu PRD #35).
- Resolved: Settings session persisted in
sessionStorageuntil Logout or app/tab process exit (grill 2026-06-04, settings menu PRD #35). - Resolved: Settings sign-in is PIN-only; server resolves Till user; PINs must be unique store-wide (grill 2026-06-04, settings menu PRD #35).
- Resolved: Website service account (application password + username) stored on Edge Server shared across tills; not per-till
setup.json(grill 2026-06-04, settings menu PRD #35). - Resolved: Till user permissions edited via checkboxes in v1 (four
settings.*keys); schema and UI driven by an extensible permission catalogue for future gates (grill 2026-06-04, settings menu PRD #35). - Resolved: Subset delegation — a Till user may grant only permissions they already hold when editing others (grill 2026-06-04, settings menu PRD #35).
- Resolved: Settings sign-in till-local lockout — 5 failed PINs → 15-minute block on that device (
sessionStorage) (grill 2026-06-04, settings menu PRD #35). - Resolved: Printer IP/port stored on Edge Server only (
.printerroutes); remove tillsetup.jsonprinterblock (grill 2026-06-04, settings menu PRD #35). - Resolved: Settings header shows signed-in Till user display name while settings session is active (grill 2026-06-04, settings menu PRD #35).
- Resolved: Till user delete guardrails — cannot delete yourself; cannot delete the last Till user in the store (grill 2026-06-04, settings menu PRD #35).
- Resolved: Settings opens as overlay from cog and via
#/settingsroute (same component) (grill 2026-06-04, settings menu PRD #35). - Resolved: Settings sub-menu labels — Printers, Store API keys, Website login, Users (grill 2026-06-04, settings menu PRD #35).
- Resolved: First-run till wizard orchestrates Edge bootstrap — Edge connection → Store API keys → Website login → Printers → first Till user → review; till
setup.jsonisserveronly (grill 2026-06-04, settings menu PRD #35 / wizard #34). - Resolved: Settings admin calls use server-issued opaque settings token (
Authorization: Bearer) after PIN sign-in; invalidated on Logout (grill 2026-06-04, settings menu PRD #35). - Resolved: Permission catalogue registers settings (v1 enforced) plus future keys: catalog (open, edit, create product, configure composite), checkout (submit, max discount %), POS print label, kitchen, EOD (grill 2026-06-04, settings menu PRD #35).
- Resolved: EOD close order settlement —
pending/processing→completed;cancelled/refundedunchanged; undismissed Till Orders →kitchen_dismissed = 1; no Woo write-back (grill #54 2026-06-09, C). - Resolved:
record_grand_total= systemgrand_total(POS + Online orders on Edge) + sum of all five declared external payments (Additional Cash, Just Eat, Deliveroo, Other Payments, Direct Payments)—external-channel sales not captured as Orders in this POS (grill #54 2026-06-09). - Resolved: EOD close does not lock the business date—new Orders may arrive after close; saved EOD record is stale until manager re-submits; UI should warn when orders exist after last close (grill #54 2026-06-09, A).
- Resolved: Reopening a closed business date in the EOD window pre-fills declared payments from the saved EOD record and highlights diffs vs live order totals/count (grill #54 2026-06-09, C).
- Resolved:
eod.submit( EOD close ) seeded on Manager profile only;eod.viewwithouteod.submitis read-only—no close action (grill #54 2026-06-09, A). - Resolved:
orders_jsonsnapshot — v1 EOD grid fields per Order plus parsed line items (name, qty, line total); not full raw order row (grill #54 2026-06-09, B). - Resolved: EOD close rejected for future business dates (Edge local timezone); today and past only (grill #54 2026-06-09, A).
- Resolved:
GET /api/eod/records—eod.view: close metadata + order total/count diff hints only (no declared payment amounts);eod.submit: full record including declared fields for pre-fill (grill #54 2026-06-09, C). - Resolved: ADR 0020 — EOD close settlement, external payments, open day, permissions, snapshot shape (#54).
- Resolved: EOD close allowed with zero orders for the business date—declared external payments still persist; system totals zero (grill #54 2026-06-09, A).
- Resolved: After EOD close, saved
eod_records/orders_jsonare Woo-immune (re-submit only);processingWeb Orders →completedat close; poller must not downgrade those rows for that business date (grill #54 2026-06-09). - Resolved: EOD close always requires an explicit confirm dialog before submit—including re-close and zero-order days (grill #54 2026-06-09, A).
- Resolved: Declared external payments and
record_grand_totaluse 2 dp round half-up, consistent with v1 EOD order totals (grill #54 2026-06-09, A). - Resolved: Today may be EOD closed at any time while trading continues—standard confirm only; drift warning when new Orders arrive afterward (grill #54 2026-06-09, A).
- Resolved: Stale close drift warning when live
order_count,grand_total,pos_total, oronline_totaldiffers from saved EOD record (grill #54 2026-06-09, B). - Resolved:
orders_jsoncomposite lines include parent + nested child line summaries (grill #54 2026-06-09, B). - Resolved: Web Orders ingested after EOD close for that business date use normal Woo poll lifecycle until re-submit settles them (grill #54 2026-06-09, A).
- Resolved: Re-submit runs a full idempotent settlement pass on all Orders in the business-date window and replaces
eod_records+orders_json(grill #54 2026-06-09, A). - Resolved: Successful EOD close enqueues an EOD close receipt on the Receipt Print route, then returns to till (grill #54 2026-06-09).
- Resolved: EOD close receipt always lists system totals + Record grand total; declared external payment lines only when > 0 (grill #54 2026-06-09, B).
- Resolved: EOD close receipt prints business date and closed at local timestamp under the title (grill #54 2026-06-09, B).
- Resolved: EOD close is not rolled back on receipt print failure—error toast, still return to till (grill #54 2026-06-09, A).
- Resolved:
eod.submitmay Reprint EOD receipt when a saved EOD record exists—no re-settlement (grill #54 2026-06-09, A). - Resolved:
POST /api/eod/recordsenqueues EOD close receipt on the Edge Server; response includesreceiptPrintQueued(grill #54 2026-06-09, A). - Resolved: EOD close receipt includes
Orders: {order_count}before the totals block (grill #54 2026-06-09, A). - Resolved: EOD close receipt v2 = ESC/POS text on thermal receipt routes only; HTML/CUPS routes skip print (grill #54 2026-06-09, A).
- Resolved:
eod_settledflag on Order rows at close; Woo poll must not downgradestatuswhen set; re-submit clears and re-sets for full window (grill #54 2026-06-09, A). - Resolved: EOD close receipt header
storeName:companyName→siteTitle→ omit (grill #54 2026-06-09, A). - Resolved:
eod_settled = 1on every Order in the business-date window, includingcancelled/refunded(grill #54 2026-06-09, A). - Resolved: EOD close receipt amounts use
£prefix and 2 dp, matching order receipt ESC/POS (grill #54 2026-06-09, A). - Resolved: Woo poll ignores all
statusupdates oneod_settled = 1rows (grill #54 2026-06-09, B). - Resolved: EOD close confirm dialog shows business date, replace warning, and preview
record_grand_total(grill #54 2026-06-09, B). - Resolved: Reprint EOD receipt uses original
submitted_atfor Closed at (grill #54 2026-06-09, A). - Resolved: Concurrent EOD close for the same business date: last write wins (grill #54 2026-06-09, A).
- Resolved: Post-close toast:
record_grand_totalplus receipt printing / failed fromreceiptPrintQueued, then return to till (grill #54 2026-06-09, B). - Resolved: EOD close receipt has no declared extras subtotal line—only individual declared lines (if > 0) then Record grand total (grill #54 2026-06-09, A).
- Resolved: Reprint EOD receipt — no confirm dialog; toast print outcome only (grill #54 2026-06-09, B).
- Resolved: Receipt labels — system POS / Online / Grand / Total; declared lines match UI field names (grill #54 2026-06-09, C).
- Resolved: Checkout discount permission =
checkout.max_discount_percent(0–100); preset buttons ≤ cap only (grill 2026-06-04, settings menu PRD #35). - Architecture decisions (index): docs/adr/README.md.
- Order identity / kitchen lifecycle: ADR 0001.
- Catalog authority: ADR 0002.
- Catalog management window: ADR 0003.
- Kitchen orders window: ADR 0004.
- WOOCO composite Till lines + definition API: ADR 0005.
- Unified Till session / boot PIN: ADR 0014 · 0015 profiles · 0016 catalog caps · 0017 handoff · 0018 registry · 0019 platform lock (#49).
- EOD close / eod_records: ADR 0020 (#54).
- Tension: PRD calls for idempotent ingest keyed by Woo Order Id — with no write-back, dedup means “skip if a Web Order row already exists,” not “attach to Till Order.”