diff --git a/client/package.json b/client/package.json
index 58375dc8f..7ca85b4ff 100644
--- a/client/package.json
+++ b/client/package.json
@@ -15,5 +15,8 @@
"devDependencies": {
"@vitejs/plugin-vue": "^5.0.4",
"vite": "^5.2.0"
+ },
+ "allowScripts": {
+ "esbuild@0.21.5": true
}
}
diff --git a/client/src/App.vue b/client/src/App.vue
index c2da05a5c..dd6849b1c 100644
--- a/client/src/App.vue
+++ b/client/src/App.vue
@@ -22,6 +22,9 @@
| {{ t('orders.table.orderNumber') }} | +{{ t('orders.table.items') }} | +{{ t('orders.table.status') }} | +{{ t('orders.table.orderDate') }} | +{{ t('restocking.leadTime') }} | +{{ t('restocking.eta') }} | +{{ t('orders.table.totalValue') }} | +
|---|---|---|---|---|---|---|
| {{ order.order_number }} | +
+
+
+ + {{ t('orders.itemsCount', { count: order.items.length }) }} ++
+
+
+ {{ item.item_name }}
+
+
+ |
+ + {{ t('restocking.statusSubmitted') }} + | +{{ formatDate(order.order_date) }} | +{{ t('restocking.daysLead', { days: order.max_lead_time_days }) }} | +{{ formatDate(order.expected_delivery) }} | +{{ currencySymbol }}{{ order.total_cost.toLocaleString() }} | +
+ A single-page Vue 3 dashboard backed by a read-only FastAPI service. All business data is + loaded from JSON files into Python memory at import time — there is no database, no ORM, + and no persistence layer. Filtering happens as list comprehensions over in-memory dictionaries. +
+ +all| Layer | Technology | Version | Role |
|---|---|---|---|
| Frontend | Vue | ^3.4.21 | Composition API throughout; no Options API components |
| Frontend | vue-router | ^4.3.0 | createWebHistory, 6 flat routes, no guards or lazy loading |
| Frontend | axios | ^1.6.7 | HTTP client; base URL hardcoded to localhost:8001 |
| Build | Vite | ^5.2.0 | Dev server on port 3000; @vitejs/plugin-vue |
| Backend | FastAPI | ≥0.110 | Routing, query-param parsing, OpenAPI docs at /docs |
| Backend | Pydantic | ≥2.5 | Response models — outbound validation only |
| Backend | uvicorn | ≥0.24 | ASGI server, bound to 0.0.0.0:8001 |
| Runtime | Python | ≥3.11 | Managed by uv; venv at server/.venv |
| Testing | pytest + TestClient | ≥8.0 | 40 tests across 3 files in tests/backend |
| Tooling | MCP servers | — | Playwright (browser testing), GitHub (repo operations) |
+ There is no state-management library, no TypeScript, no CSS framework, and no charting dependency.
+ Shared state is plain module-scope ref()s; charts are hand-written SVG inside the views.
+
v-model writes straight into a shared ref — the component owns no local copy.
+ client/src/components/FilterBar.vue
The four filter refs live at module scope, outside useFilters(), so all callers
+ receive the same instances. This singleton pattern is what keeps views in sync without a store.
+ client/src/composables/useFilters.js
Each view watches only the filters it honors — Dashboard watches all four, + Inventory watches just location and category, since inventory has no time dimension. + views/Dashboard.vue:676 · views/Inventory.vue:169
+getCurrentFilters() maps UI names to API names:
+ selectedLocation becomes warehouse, selectedPeriod becomes month.
+ composables/useFilters.js
Every method drops params equal to all before appending, so a default filter
+ is simply an absent param rather than a sentinel the server must interpret.
+ client/src/api.js
Handlers chain apply_filters() then filter_by_month(). Both rebind a local
+ name to a new list comprehension, so the module-level source data is never mutated.
+ server/main.py:33 · server/main.py:17
The response_model shapes and type-checks each record on the way out. This is the
+ one place a JSON-file edit that breaks the schema will surface — as a 500, not a silent pass-through.
+ server/main.py
Raw responses land in refs (allOrders, inventoryItems); every derived
+ figure — totals, status counts, chart geometry — is a cached computed that re-evaluates only when its
+ dependencies change.
+ client/src/views/*.vue
| Endpoint | Filters accepted | Returns | |
|---|---|---|---|
| GET | / | — | Service name and version |
| GET | /api/inventory | warehouse, category | InventoryItem[] |
| GET | /api/inventory/{item_id} | — | InventoryItem, else 404 |
| GET | /api/orders | warehouse, category, status, month | Order[] |
| GET | /api/orders/{order_id} | — | Order, else 404 |
| GET | /api/demand | — | DemandForecast[] |
| GET | /api/backlog | — | BacklogItem[] with has_purchase_order |
| GET | /api/dashboard/summary | warehouse, category, status, month | 5 aggregate figures |
| GET | /api/spending/summary | — | Spending totals object |
| GET | /api/spending/monthly | — | Monthly spending series |
| GET | /api/spending/categories | — | Spending by category |
| GET | /api/spending/transactions | — | Recent transactions |
| GET | /api/reports/quarterly | — | Per-quarter revenue and fulfillment rate |
| GET | /api/reports/monthly-trends | — | Month-over-month order count and revenue |
+ Every route is a GET. The service exposes no way to create, update, or delete anything —
+ which is consistent with data that is reloaded from disk on every restart.
+
| File | Records | Bound to | Notes |
|---|---|---|---|
| orders.json | 250 | orders | The only dataset with a usable time dimension |
| transactions.json | 56 | recent_transactions | Feeds the Spending view |
| inventory.json | 32 | inventory_items | No date field, so month filters cannot apply |
| demand_forecasts.json | 9 | demand_forecasts | Served unfiltered |
| backlog_items.json | 4 | backlog_items | Served unfiltered |
| spending.json | 3 keys | spending_summary, monthly_spending, category_spending | One file split across three globals |
| purchase_orders.json | 0 | purchase_orders | Empty; see findings below |
order_dateQUARTER_MAP before matchingall is treated as no filter at both endssrc/localeslocalStorage under app-locale+ Three gaps surfaced while tracing the client against the server. Each was confirmed against the + running application rather than inferred from the source alone. +
+ +
+ api.js exposes getTasks, createTask, deleteTask,
+ toggleTask, createPurchaseOrder, and getPurchaseOrderByBacklogItem,
+ which call /api/tasks and /api/purchase-orders. Neither path exists in
+ main.py, whose 14 routes are all GET.
+
+ The four task methods are live: App.vue calls getTasks() on mount, so every page
+ load issues a request that 404s. The failure is caught and written to the console, and the UI falls back to
+ the hardcoded task list in useAuth.js — so the feature looks like it works while the write path
+ silently does nothing. The two purchase-order methods have no callers at all.
+
+ The view exists at client/src/views/Backlog.vue (152 lines) but is registered in no route in
+ main.js, linked from no router-link in App.vue, and imported nowhere.
+ Vite will not include it in a build, and no navigation path reaches it.
+
+ Backlog data does still surface in the UI, but by a different route entirely — the Dashboard renders it
+ through BacklogDetailModal.vue.
+
+ /api/backlog computes the flag by checking each backlog item against
+ purchase_orders. That list is loaded from purchase_orders.json, which
+ contains an empty array — and since no endpoint writes to it, nothing can populate it at runtime.
+
+ The flag returns false for all four backlog items on every request, so any UI branch
+ depending on it is effectively dead.
+
+ Separately, and by design for a demo: CORS is set to allow_origins=["*"], there is no
+ authentication, and the API base URL is hardcoded to localhost:8001 with no environment
+ override. server/CLAUDE.md already documents all three as local-development-only choices.
+