Rebuild the Restaurant Management System on NimbusCMS - #18
Merged
Merged
Conversation
Starts rebuilding the Restaurant Management System as an application on NimbusCMS, and uses it to validate the platform. The legacy PHP stays on master and in history; this branch builds the modern version on Nimbus. Menu is the first vertical and needs no changes to Nimbus core: categories and priced menu items are ordinary Nimbus collections (a number price, a relation to categories), managed in the admin and served over the read API. - app/collections.php declares the app's content model as data. - bin/provision-menu.sh installs that model onto a running Nimbus using only its public HTTP admin API — the app installs itself, no internal classes, no core changes. Verified end to end and idempotent (re-running skips). - docs/PLATFORM-VALIDATION.md is the running ledger: Menu proven, plus three findings the vertical surfaced (F1 API returns relations as bare ids; F2 no supported way to consume Nimbus from a separate app repo; F3 number decimal formatting). None blocked Menu, so none has been built — each becomes a Nimbus capability + ADR only when a later vertical makes it a blocker. Success for the whole initiative is finishing the restaurant system without any restaurant-specific logic landing in Nimbus core.
Design-first record for the rebuild, before any code: - docs/ARCHITECTURE.md — thesis, the architecture decision (Restaurant as a co-located Nimbus plugin composed with the CRM + menu collections + a theme), full legacy→Nimbus domain map, plugin shape, capability/role model, the terminals, findings resolution, an 8-slice build sequence (Tables first), legacy-issue disposition, and the definition of done. - docs/adr/0001 — records the pivotal decision and the alternatives (compose generic plugins; collections-only external app) that were weighed and why they were not chosen. - PLATFORM-VALIDATION.md — F2 (how an app consumes Nimbus) marked DECIDED, pointing at ADR-0001; original analysis kept for the record. No code yet: each build slice still runs both Nimbus review skills first. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Ran the platform (nimbus-review-loop) and security (nimbus-security-review)
disciplines on the architecture before any code. Corrections:
- Capability model: stock Nimbus gates admin pages only on {pluginId}:read|write,
so the floor/kitchen/manage split is not expressible today. v1 uses a single
coarse danmat.restaurant:read|write; fine-grained roles logged as finding F4
(a core PR at the Staff & roles slice). Finer logic enforced in-app meanwhile.
- Terminals are capability-gated ADMIN PAGES, not plugin routes — routes are
public (no auth/CSRF), so a kitchen/floor route would be unauthenticated.
- Added §6a Security controls (merge bar per slice): capability-gate every
surface incl. MCP, server-computed money + manager-checked voids, cross-plugin
CRM PII resolved only for crm:read holders, escape-on-render, bound SQL + enum
allow-lists, graceful dangling contact refs.
- Ledger: F4 recorded; Staff & roles row marked as a confirmed core gap.
Verdict: security-green to build Slice 1 (Tables) — no money/PII/CRM link.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The first operational vertical, and the first code of the co-located restaurant plugin (plugin/, type: nimbuscms-plugin, id danmat.restaurant) — mirroring the official Inventory/Commerce/CRM plugin pattern, zero Nimbus core change. - Schema: rest_table (label unique, seats, status ENUM open/occupied/dirty/ reserved) + migration 001_tables. - Tables service: field allow-list; status is a write-time allow-list; label required + unique; bounded seats; bound SQL; total delete; setStatus quick move. - MCP (namespace restaurant): tables / table_get / table_set / table_status / table_delete, gated on the wildcard-immune danmat.restaurant capability (read/write), non-enumerating — a content *:write token can't reach the floor. - Admin: a mobile-first floor board (tables by status, per-card quick actions seat/clear/open/reserve, status filter, create/edit), a capability-gated ADMIN PAGE (not a public route, per the security review) + CSRF. - Guide: tables section for agents. - Tests: TablesTest, RestaurantToolsetTest (authz matrix), TablesAdminTest (escape-on-render + board + quick actions). - CI: .github/workflows/plugin-ci.yml runs cs-fixer + PHPStan L6 + phpunit in plugin/ against MySQL, on PHP 8.2 + 8.3. cs-fixer + PHPStan green locally (borrowed-vendor); phpunit runs in CI. Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
The heart of service, and the first consumer of the new core content-read capability (ADR 0029 in NimbusCMS). - Schema: rest_order + rest_order_item (002_orders). Lines snapshot name + unit_price at order time; menu_item_id is a soft breadcrumb, never a join. - Orders service: open (validates the table, occupies it, atomic); add/set-qty/ remove line items; server-COMPUTED totals (never stored/trusted); workflow status allow-list (open→sent→preparing→ready→served→closed); transactional delete takes the lines with it. Depends on Tables + a snapshot resolver closure, so it's unit-tested with no core schema. - Menu: a thin adapter over PluginContext::content() (ADR 0029) mapping menu_items collection entries to the picker list + a price snapshot; behind a MenuSource seam so the admin/MCP are testable without a collection. - MCP: menu + order_open/orders/order_get/order_status/order_add_item/ order_set_item_qty/order_remove_item/order_delete, all gated on the wildcard-immune danmat.restaurant capability, non-enumerating. - Admin: an orders terminal (list + status filter, open-on-table, and a single-order screen with the menu picker, quantities, line removal, workflow actions and the computed total). Capability-gated admin page + CSRF, mobile-reflow. - Guide: menu + orders section. - Tests: OrdersTest (open/occupy, snapshot, totals, qty, workflow, delete cascade, filters), OrdersAdminTest (escape + total + picker via a fake MenuSource), RestaurantToolsetTest extended (order tools listed, content token can't reach orders, an order run end-to-end over MCP). cs-fixer + PHPStan green locally (borrowed-vendor); phpunit runs in CI. Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Ledger + README: Tables and Orders verticals marked done; findings F1 (relation/reference expansion) and A2 (plugins reading a collection in-process) resolved by the NimbusCMS plugin content-read capability (ADR 0029) that the Orders vertical forced. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The cook's screen — no new schema, built on the Orders workflow. - Orders::ticketsByStatus(statuses) — the kitchen queue: orders in the given allow-listed statuses with their line items, oldest-first (FIFO), batched into two bound queries (never N+1). - KitchenAdmin: a three-column board (New=sent → Preparing → Ready), each ticket showing table, items and age; Start (sent→preparing) and Ready (preparing→ready) advance actions; the floor still owns "served". A capability-gated admin page (not a public route, per the security review), CSRF on advance, mobile-reflow, and a nonce'd auto-refresh (15s). - MCP: a `kitchen` read tool (the queue); advancing reuses order_status. - Guide: kitchen section. - Tests: OrdersTest (ticketsByStatus FIFO + item batching + unknown-status ignored), KitchenAdminTest (columns, advance action, escape-on-render, nonce'd refresh, ready has no kitchen action), RestaurantToolsetTest (kitchen tool listed + queue lists sent/preparing tickets). cs-fixer + PHPStan green locally; phpunit runs in CI. Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Closes the service loop — settle a bill, close the order, turn the table. - Schema: payment columns on rest_order (amount_paid, payment_method, paid_at) folded into its migration (unreleased; no deployed install to ALTER). - Orders::pay(orderId, method, now): the AMOUNT is the server-computed order total — never a caller value; only the method (allow-list cash/card/other) is chosen. Marks paid + closed and sets the table `dirty`, atomically. Rejects a bad method, a missing order, and a double payment. get()/all() now surface amount_paid / payment_method / paid_at. - Admin: a payment section on the order screen (shows total due + method select, no amount input; a receipt once settled). "served" no longer offers a Close button — payment closes the order. The floor's existing dirty→open quick action finishes the turn. - MCP: order_pay (id + method only). - Guide: payment note. - Tests: OrdersTest (computed-total charge, close, table turn, method allow-list, missing/double-pay rejected), OrdersAdminTest (pay form has total + no amount input; receipt after), RestaurantToolsetTest (order_pay listed; pays the computed total over MCP; bad method as data). Security: amount is never client-set (structurally — pay() takes no amount); the payment record carries no PII. cs-fixer + PHPStan green locally; phpunit runs in CI. Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Real role separation, now that fine-grained plugin capabilities exist in core. - Declares fine-grained actions on the capability: read, write, floor, kitchen, manage (was read/write only). - Re-gates the terminals: Floor (tables) and Orders (incl. taking payment) → danmat.restaurant:floor; Kitchen display → danmat.restaurant:kitchen. Each admin action inherits its page's gate, so a cook (:kitchen) cannot take payment and a waiter (:floor) cannot advance kitchen tickets; a manager holds all three. read/write remain for the MCP/agent surface. `manage` is declared for reports (a later slice). - Guide: staff-roles section (which grant does what). Legacy roles map: waiter/host/busboy→floor, cook→kitchen, manager/admin→manage(+floor+kitchen). Staff users are granted these in Nimbus's own roles/tokens admin. - Test: RestaurantPluginTest (no DB) — register() declares the fine actions as grants and gates each terminal on the right one. cs-fixer + PHPStan green locally; phpunit runs in CI. Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Lights up the CRM integration without ever reading CRM data. - Schema: rest_reservation (003_reservations) — party_name/size/reserved_at/ notes are the restaurant's own first-party data; table_id (validated, same-plugin) + contact_id (a bare link to a CRM contact). - Reservations service: allow-list, party name required, allow-listed status (booked/seated/cancelled/no_show), strict datetime (accepts datetime-local), validated table ref. contact_id is stored as given and NOT resolved or existence-checked — reading the CRM would breach its capability gate. - Admin: the book (list + status filter + create/edit). Shows only the booking's own data; a CRM-linked booking renders a "Guest in CRM" LINK to /admin/crm?edit=<id> (core gates that page) — never CRM data itself. Gated on danmat.restaurant:floor + CSRF, mobile-reflow. - MCP: reservations / _get / _set / _status / _delete; contact_id passes through as a bare link. - Guide: reservations section (+ the CRM boundary). - Tests: ReservationsTest, ReservationsAdminTest (escape + gated CRM link, no CRM data), RestaurantToolsetTest (reservation tools + round-trip carrying only the link), RestaurantPluginTest (reservations page gated :floor). Security: the restaurant stores only contact_id and links out to the CRM's own gated page; no CRM PII is read, resolved or copied into rest_* tables. A proper in-process guest picker would need a CRM read service port (ADR 0019) — a documented follow-up. cs-fixer + PHPStan green locally; phpunit runs in CI. Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* Slice 7: Reports (manager dashboard) Read-only revenue reporting over paid orders — the last operational vertical. - Reports service: revenueBetween / revenueByDay / topItems / activeOrders, over paid orders (revenue = recorded amount_paid, never recomputed). Callers pass explicit [from,to) bounds, so it's deterministic and testable; bound SQL. - Admin: a manager dashboard (today + last-7-days revenue cards, active orders, a 7-day breakdown, week's top items). Read-only, gated on the manage action (danmat.restaurant:manage) — the first use of that grant. Mobile cards. - MCP: a reports read tool (today/last_7_days/active_orders/top_items). - Guide: reports section. - Tests: ReportsTest (windowed revenue, unpaid≠revenue but active, by-day grouping, top-items ranking), ReportsAdminTest (today's revenue + escapes item names + empty period), RestaurantToolsetTest (reports tool listed), RestaurantPluginTest (reports page gated :manage; manage label). No new tables, no core change. cs-fixer + PHPStan green locally; phpunit in CI. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * Reports: rename card to 'Revenue today' (avoid apostrophe-escape mismatch) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Retain the feel of the original Restaurant Automation System, uplifted. - Branding::head — a shared "RAS · Restaurant Automation System" eyebrow + title + subtitle on every terminal (Floor, Orders, Kitchen, Reservations, Reports), so they read as one system. Nonce'd styles (admin CSP drops inline style=). - Floor: the signature circular table tokens are back — a grid of status- coloured circles (open green / occupied amber / dirty red / reserved blue, modernized from the harsh originals), each linking to the table with seats + contextual quick actions beneath, plus a colour legend. Replaces the rectangular cards; keeps the same status classes, actions and behaviour. - Shared status palette via a --rs custom property (legend dots + circles from one source). No behaviour change; markup/CSS only. Tests updated to assert the circular tokens; all terminal tests still green. cs-fixer + PHPStan green locally. Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The guest-facing side of the rebuild — the public menu, in the RAS identity. - theme/ (source co-located in the app repo; deploy drops it into the site's themes/): a dark, committed restaurant look uplifted from the original RAS #222, with a warm gold accent and an elegant printed-menu feel. - Templates: layout / header (RAS wordmark) / footer / entry / 404, and collection-menu_items — the menu grouped by its category relation (expanded by EntryView) with prices and dotted leaders. theme.json declares it + nav. - assets/app.css — one stylesheet, responsive, no build step. - Theme CI: php -l the templates on 8.2 + 8.3 (view files, no test DB). Reads the same menu_items collection the Orders picker uses; no core change. Templates lint clean locally; rendered a standalone preview to verify the look. Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The artifacts to stand up the RAS public demo (explorable multi-role logins, data reset hourly), mirroring the Foodmart deploy. Repo-side only — no live changes. - deploy/DEPLOY.md — runbook: site image (Nimbus core + this plugin via a Composer path repo + CRM + theme), compose + Caddy/Cloudflare for a subdomain, first deploy, the hourly-reset cron, and the public demo-login table. - deploy/seed-demo.php — seeds capability roles (Waiter/Host/Busboy→floor, Cook→kitchen, Manager→floor+kitchen+manage+crm:read/write), one login per role (public password "demopass"), the menu (categories + items), and live sample data (tables in mixed states, open + paid orders, reservations incl. one linked to a CRM guest). Manager holds crm:read so the reservation→CRM PII gate is visible: a floor login can't open the guest, a manager can. - deploy/reset-demo.sh — the hourly reset: drop DB, migrate (core + plugin), make admin, re-seed. Fresh DB each run, so the seed needs no idempotency. seed lints clean (php -l); reset lints clean (sh -n). Live execution (box build, DNS, first reset, cron) is the go-live step. Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…hars) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
… the Nimbus placeholder The guest-facing root (/) was rendering the bare 'No home page configured' placeholder because site.home lives in nb_settings (which shadows config/site.php at runtime) and was never seeded. Set it — plus the site title/description brand — via SettingsRepository so a from-scratch rebuild matches the golden restore now serving the branded menu at /. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The restaurant manager owns the menu, so grant menu_items:write + categories:write (write implies read) on the Manager role — scoped to those collections only (no schema:write). Now the manager can edit the menu from the CMS, and the admin dashboard's Collections/Entries tiles are meaningful for them rather than linking to an empty list. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* Slice B: public homepage + denser guest site The 2014 original had no public site; the guest site was a single menu page. Add a real restaurant homepage in the RAS identity — hero, about, live featured dishes, hours/location, and a call-to-reserve CTA — with the menu as its own page. - Homepage modeled as a `single`-kind collection `home` (editable CMS content: hero/about/hours/address/phone); site.home -> home. Theme entry-home.php renders the five sections and degrades gracefully when a field is blank. - Live featured dishes via the ADR-0027 view-data hinge: the plugin's new HomeViewData contributor returns a handful of menu items (description-first, deterministic, visitor-independent) only on the home page; the theme reads contrib['danmat.restaurant']['featured'] and escapes on render. Menu::featured() added behind the MenuSource seam. - Header nav -> Home · Menu. New homepage CSS in the existing token system, responsive at 375px. Seed creates the home collection + entry. No core change. Design + 3-hat review in docs/design/slice-b-homepage.md. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * theme: avoid duplicated title on the homepage (Name · Name) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Ships the account list for the NimbusCMS demo-mode 'Explore as …' picker (config/demo.php). Manager first (backs the pre-fill). Documented in DEPLOY.md; mounted read-only into the ras service. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add a public takeaway flow: a themed order page (menu + qty + name/phone), a public POST that places a table-less order straight into the kitchen queue, and a token-gated confirmation. The checkout is a clearly-labelled DEMO — no real payment, no processor, no card data. - Order model (folded into the unreleased 002_orders): table_id nullable, a channel enum (dine_in/online), customer_name/phone, and a confirm_token; plus a tiny per-IP rate table. Dine-in unchanged. - Orders::placeOnline() snapshots name+price from the menu (client prices never trusted), computes the total server-side, caps qty + line count, requires a name + phone, and creates a paid 'sent' online order in one transaction. onlineForConfirmation()/confirmToken() make the confirmation non-enumerable. - Public surface (plugin-owned auth per ADR 0017): themed page /order (ADR 0023, JS-free) + POST /ext/restaurant/order, guarded by a honeypot + per-IP throttle (RateLimiter) + order-size caps. Kitchen shows online tickets as 'Online · name'; Reports counts online sales automatically. - Nav gains Order; seed adds one sample online order. Design + 3-hat + full Attacker/Defender/QA security review in docs/design/slice-c2-online-ordering.md. Tests: placeOnline (snapshot/caps/ table-less/kitchen/token) + RateLimiter; 89 plugin tests green, PHPStan L6 + cs-fixer clean. Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
The reservations 'Add a reservation' form (and the orders/tables forms) had a <select> with no width constraint inside a flex column, so a long option (e.g. a table label, a menu item) pushed it past its column and overlapped the next field on narrow screens. Add width:100%/max-width:100% to the fields and min-width:0 to the flex label so it can shrink. No behaviour change. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Clean up the folder tree now that the rebuild is the project: - Move the original 2014 app (procedural PHP, oose.sql, css/js/font-awesome, the design doc, the old README) into archive/, unchanged. The old README is marked as the origin 2014 version. - Promote the Nimbus rebuild README to the root README.md (was README-NIMBUS.md), rewritten as the primary readme with the live demo, status, layout, and run-locally instructions; it points to archive/ for the origin. - Make the archived app runnable again: it wouldn't start on a modern PHP (it uses the removed mysql_* extension), so add archive/Dockerfile (PHP 5.6 + mysql ext, output_buffering on to restore the 2014 shared-hosting default its session/header flow assumes) + archive/docker-compose.yml (MySQL 5.7, auto-imports oose.sql). The only source concession: settings.inc.php reads the DB host from env (original values as fallback). Login (waiter/123) works; screenshots committed as proof. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Replaces the 2014 hand-rolled PHP app with a full rebuild as an application on NimbusCMS — and makes it the default branch.
What's here
plugin/) + the official CRM + a theme (theme/). Zero Nimbus core change for app logic./admin).docs/PLATFORM-VALIDATION.md.archive/, and made runnable again via Docker (PHP 5.6) — seearchive/README.md.Verification
All work shipped as reviewed slices (design + 3-hat + security review where relevant), each CI-green. Every operational vertical rebuilt and verified live; the online-ordering security review is in
docs/design/slice-c2-online-ordering.md.Legacy issues #1–#4 will be closed as superseded after this merges (the "won't start" one is now answered by
archive/).🤖 Generated with Claude Code