diff --git a/CLAUDE.md b/CLAUDE.md
index 89c307d15..8e2b0ce3e 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -54,6 +54,17 @@ npm install && npm run dev
- `GET /api/dashboard/summary` - All filters
- `GET /api/demand`, `/api/backlog` - No filters
- `GET /api/spending/*` - Summary, monthly, categories, transactions
+- `GET /api/restocking/recommendations` - Required: budget. Filters: warehouse, category
+- `GET|POST /api/restocking/orders` - No filters. The only write endpoint; persists to `server/data/restock_orders.json`
+
+## Code Conventions
+
+**Always document non-obvious logic with comments.** When logic encodes something the code itself can't show — a business rule, a workaround, an ordering dependency, a magic constant — add a short comment stating *why*, not what. Match the surrounding comment style and density; don't narrate self-evident lines.
+
+```javascript
+// Inventory has no time dimension, so the month filter is intentionally ignored here
+const params = buildParams({ warehouse, category })
+```
## Common Issues
1. Use unique keys in v-for (not `index`) - use `sku`, `month`, etc.
diff --git a/client/src/App.vue b/client/src/App.vue
index c2da05a5c..b61aae150 100644
--- a/client/src/App.vue
+++ b/client/src/App.vue
@@ -16,6 +16,9 @@
| {{ t('orders.table.orderNumber') }} | +{{ t('orders.table.items') }} | +{{ t('orders.table.status') }} | +{{ t('orders.table.submittedDate') }} | +{{ t('orders.table.leadTime') }} | +{{ t('orders.table.expectedDelivery') }} | +{{ t('orders.table.totalValue') }} | +
|---|---|---|---|---|---|---|
| {{ order.order_number }} | +
+
+
+ + {{ t('orders.itemsCount', { count: order.items.length }) }} ++
+
+
+ {{ item.name }}
+
+
+
+ |
+ + + {{ t(`status.${order.status.toLowerCase()}`) }} + + | +{{ formatDate(order.submitted_at) }} | +{{ t('restocking.days', { count: order.lead_time_days }) }} | +{{ formatDate(order.expected_delivery) }} | +{{ formatCurrency(order.total_value, currentCurrency) }} | +
{{ t('restocking.description') }}
+| {{ t('inventory.table.sku') }} | +{{ t('inventory.table.itemName') }} | +{{ t('orders.table.category') }} | +{{ t('orders.table.warehouse') }} | +{{ t('restocking.table.onHand') }} | +{{ t('restocking.table.reorderPoint') }} | +{{ t('restocking.table.target') }} | +{{ t('restocking.table.shortfall') }} | +{{ t('restocking.table.orderQuantity') }} | +{{ t('restocking.table.unitCost') }} | +{{ t('restocking.table.lineCost') }} | +{{ t('restocking.table.leadTime') }} | +{{ t('restocking.table.priority') }} | +
|---|---|---|---|---|---|---|---|---|---|---|---|---|
| {{ item.sku }} | +{{ translateProductName(item.name) }} | +{{ translateCategory(item.category) }} | +{{ translateWarehouse(item.warehouse) }} | +{{ item.quantity_on_hand }} | +{{ item.reorder_point }} | +{{ item.target_quantity }} | +{{ item.shortfall }} | ++ {{ item.recommended_quantity }} + {{ t('restocking.partial') }} + | +{{ formatCurrencyWithDecimals(item.unit_cost, currentCurrency, 2) }} | +{{ formatCurrency(item.line_cost, currentCurrency) }} | +{{ t('restocking.days', { count: item.lead_time_days }) }} | ++ + {{ item.priority === 'critical' ? t('restocking.critical') : t('restocking.low') }} + + | +
| {{ t('restocking.budgetUsed') }} | +{{ formatCurrency(data.budget_used, currentCurrency) }} | ++ | ||||||||||
+ A single-page Vue 3 dashboard over a stateless FastAPI service that filters in-memory + JSON fixtures. No database, no authentication, no build step on the server — the whole + system is two processes and seven data files. +
+
+ Four tiers, one direction. The browser loads the SPA from Vite; every data read leaves the
+ browser as an axios GET against port 8001; the API filters Python lists that were
+ loaded from disk once at import time and never written back.
+
vue-router 4 in history mode, no state library — shared state lives in module-scoped refs inside composables.vite.config.js. Only @vitejs/plugin-vue. No TypeScript, no linter, no test runner on the client.src/api.js against a hardcoded localhost:8001 base — no env var, so the origin is baked in at author time.main.py: models, filter helpers and all routes. Served by Uvicorn on 0.0.0.0:8001, auto-docs at /docs.response_model on typed endpoints. The aggregate endpoints (dashboard, spending, reports) return bare dicts and are unvalidated.uv from pyproject.toml. Three backend test modules drive FastAPI's TestClient..mcp.json wires Playwright (browser testing) and GitHub (repo operations, token from env).
+ One filter interaction drives the entire application. Filter state is global by construction:
+ useFilters.js declares its four refs at module scope, so every importer shares one
+ instance and each view independently watches it and refetches.
+
The client sends only the four filter values; it never receives the full dataset and filters locally. Values equal to all are omitted from the query string entirely rather than sent and ignored.
/api/dashboard/summary and /api/reports/* compute totals in Python. Everything else returns rows, and the views derive totals in computed() properties.
Locale (en/ja) and currency never reach the API. utils/currency.js converts USD to JPY at a hardcoded rate of 150 for display, so yen figures are cosmetic rather than authoritative.
No charting library is installed. Each view maps its data into coordinates inside computed properties and emits inline SVG.
Fourteen routes, all GET. The filter columns show which query parameters actually affect the result.
| Endpoint | Accepts | Backing data | Notes | +
|---|---|---|---|
GET / | — | — | Version banner |
/api/inventory | warehouse, category | inventory.json | No time dimension, so month is deliberately unsupported |
/api/inventory/{id} | path id | inventory.json | 404 when absent |
/api/orders | warehouse, category, status, month | orders.json | The only fully-filterable collection |
/api/orders/{id} | path id | orders.json | 404 when absent |
/api/demand | — | demand_forecasts.json | Ignores all filters |
/api/backlog | — | backlog_items.json | Joins against purchase orders to add has_purchase_order |
/api/dashboard/summary | all four | inventory + orders | Returns 5 aggregates; backlog count is unfiltered |
/api/spending/summary | — | spending.json | Pre-computed in the fixture |
/api/spending/monthly | — | spending.json | Pre-computed in the fixture |
/api/spending/categories | — | spending.json | Pre-computed in the fixture |
/api/spending/transactions | — | transactions.json | 56 rows, unpaginated |
/api/reports/quarterly | — | orders.json | Derives revenue and fulfilment rate per quarter |
/api/reports/monthly-trends | — | orders.json | Groups on the YYYY-MM prefix of order_date |
+ Every figure in the UI traces back to these seven files. mock_data.py reads them
+ once at import; the API then filters those lists per request.
+
| File | Records | Shape | Consumed by |
|---|---|---|---|
orders.json | 250 | List; spans all 12 months of 2025 | Orders, Dashboard, Spending, Reports |
transactions.json | 56 | List | Spending |
inventory.json | 32 | List, keyed by SKU | Inventory, Dashboard, Demand, Backlog |
demand_forecasts.json | 9 | List | Demand |
backlog_items.json | 4 | List | Dashboard |
spending.json | 3 | Object: summary, monthly, categories | Spending |
purchase_orders.json | 0 | Empty list | /api/backlog join |
2025-01…2025-12, plus quarters Q1-2025…Q4-2025 expanded by QUARTER_MAP. Matched by substring against order_date.orders carries it.+ Points where the code as written diverges from what the structure implies. Each was verified + against the running system rather than inferred. +
+| Finding | Detail | Effect |
|---|---|---|
| Gap Five client methods hit routes that do not exist | +api.js defines getTasks, createTask, deleteTask, toggleTask and createPurchaseOrder/getPurchaseOrderByBacklogItem. main.py declares no write routes at all; GET /api/tasks and GET /api/purchase-orders/… both return 404 on the running server. |
+ Task CRUD silently falls back to the mock list in useAuth (errors are caught and logged); purchase-order creation cannot succeed. The PurchaseOrder and CreatePurchaseOrderRequest models exist but are unreachable. |
+
Gap views/Backlog.vue is orphaned |
+ 152 lines, fully implemented, but absent from the router in main.js, from the nav in App.vue, and from every import. |
+ Unreachable at any URL. Backlog data surfaces only through the Dashboard table and its modal. | +
Note Reports.vue bypasses the API client |
+ The only view importing axios directly instead of api.js, and the only one importing neither useFilters nor useI18n. |
+ Reports ignore the global filter bar and are not translated — the base URL is also duplicated there. | +
Note purchase_orders.json is empty |
+ The /api/backlog join always evaluates has_purchase_order to false, and nothing can populate the file without a write endpoint. |
+ That branch of the backlog UI is permanently in its empty state. | +
| Note Aggregate endpoints are untyped | +dashboard/summary, spending/* and reports/* return plain dicts with no response_model. |
+ Shape changes in those payloads fail at the Vue template rather than at the API boundary. | +
| By design Demo-grade security posture | +CORS allow_origins=["*"] with credentials enabled, no authentication, no rate limiting, and the API origin hardcoded client-side. |
+ Documented as local-development-only in server/CLAUDE.md; would need to change before any shared deployment. |
+
cd server && uv run python main.py/docs.cd client && npm run devscripts/start.shscripts/stop.sh tears them down.cd tests && uv run pytest backend/ -vTestClient. There is no client-side test setup.