Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 16 additions & 1 deletion .claude/CONTEXT.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,9 +50,24 @@ After all 6 phases were done, a pass comparing the system against the actual exa

Snyk: 0 issues. Typecheck and full `bun run test` (all 6 packages) green after both bug fixes.

## Addition: React frontend (branch `feature/frontend`, additive, not graded)

After backend submission, the user wanted a UI to click-test the system themselves before the oral defense. New top-level `frontend/` (React + Vite + TypeScript, react-router-dom), its own 7th Docker container with a healthcheck, served by a hand-rolled Bun static file server (`frontend/serve.ts` — path-traversal-safe, SPA-fallback for client routes). `nginx.conf` now splits traffic: `/api/*` and `= /health` → gateway (unchanged), everything else → the new `frontend` upstream — nginx remains the system's only published port.

Key decisions, each one a deliberate fix for a concrete risk surfaced during planning/implementation, not guesswork:
- **Did NOT import `OrderItem` from `@quickbite/shared`** for client-side cart validation — that package's barrel re-exports `mq.ts`/`idempotency.ts`, which pull in `amqplib`/`ioredis` (Node-only, not browser-safe). Duplicated a minimal zod schema in `frontend/src/lib/orderItem.ts` instead, same "client previews, server stays authoritative" reasoning as `cartTotal.ts` vs. `services/order/src/pricing.ts`.
- **`useOrderPolling` hook**: setTimeout-chain (never raw `setInterval`, to avoid overlapping in-flight requests), AbortController + `cancelled` flag (no setState-after-unmount), treats a 404 as "not found yet" within a bounded ~60s window (not fatal — the outbox pattern means the row can briefly lag right after `POST /orders` returns), explicit "timed-out" UI state if `"ready"` is never reached.
- **Real test-infra bug found and fixed**: `@testing-library/react`'s `screen` binds to `document.body` once, at *module-load time* — and ESM hoists all `import` statements above any other code regardless of where they're written. A static `import { cleanup } from "@testing-library/react"` at the top of `frontend/tests/setup.ts`, written after `GlobalRegistrator.register()` in source order, was still being evaluated *before* it at runtime, permanently breaking every `screen.*` query. Fixed with a dynamic `await import("@testing-library/react")` placed after registration. Also needed an `afterEach(cleanup)` — without it, happy-dom's shared document persists rendered output across test files, causing false "multiple elements found" errors.
- Frontend's own `zod` was pinned to `^3.23.8` to match the rest of the repo (it resolved to v4 by default) — same major version everywhere, deliberately.
- Verified live, not just via tests: full `docker compose up --build` → `frontend` reaches `healthy` → curl-driven `placed → accepted → ready` through nginx → confirmed a hard-refresh on a deep link like `/orders/<uuid>` returns the SPA shell (200), not a 404 → confirmed path-traversal attempts on `serve.ts` fall through safely to the SPA fallback, never leak a file outside `dist/` → confirmed the public-entry-point invariant still holds (only nginx has a published port; frontend's internal port 3005 is unreachable from the host).

Snyk: 0 issues. Typecheck and full `bun run test` (now 7 packages, frontend included — pure `lib/` unit tests + `happy-dom`-backed component tests) green.

**Per explicit user instruction: do not merge this branch automatically.** Wait for the user's own manual click-through in a real browser before merging — unlike every prior phase, where merge was pre-approved.

## Next step

This fix-up pass needs to be committed, pushed, and merged via PR (same workflow as every phase — own branch, no Claude/Anthropic references). After that: no more build work planned. The user's next intent is a Q&A/walkthrough session to fully understand the codebase ahead of the oral defense (explain architecture, justify decisions, describe traffic flow, name an error scenario and its handling). Possible further follow-ups if wanted later: a real notification send (email/push, still a `TODO` in `services/notification/src/index.ts`), CI badge/status in `README.md`, or load-testing the outbox poller under concurrent order volume.
Frontend branch is implemented, tested, and verified server-side — pending the user's own manual browser click-through, then commit/push/PR/merge. After that: no more build work planned. The user's next intent is a Q&A/walkthrough session to fully understand the codebase ahead of the oral defense (explain architecture, justify decisions, describe traffic flow, name an error scenario and its handling). Possible further follow-ups if wanted later: a real notification send (email/push, still a `TODO` in `services/notification/src/index.ts`), CI badge/status in `README.md`, or load-testing the outbox poller under concurrent order volume.

## Key files to know

Expand Down
6 changes: 4 additions & 2 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
- Microservices, each owning **only its own** PostgreSQL database. A service **never** reads or writes another service's tables.
- Inter-service workflow communication is via **RabbitMQ events only**. No direct service-to-service HTTP calls for the order workflow.
- Synchronous HTTP exists **only** on the path `client → Nginx → API gateway → service` for user-facing reads (menu, order status).
- The gateway is the single public entry point. Individual services are not exposed publicly.
- **Nginx is the single public entry point** — it routes `/api/*` and `/health` to the gateway, and everything else to the `frontend` container. Neither the gateway, the frontend, nor any individual service is exposed publicly.

Flow: `Order` publishes `order.placed` → `Kitchen` consumes it, then publishes `order.accepted` and `order.ready` → `Notification` consumes all order events.

Expand All @@ -17,7 +17,8 @@ Flow: `Order` publishes `order.placed` → `Kitchen` consumes it, then publishes
- Messaging: **RabbitMQ** (topic exchange, durable queues, DLQ)
- Data: **PostgreSQL**, one database per service
- Cache + idempotency: **Redis** (menu cache with TTL, event dedup with SETNX + TTL)
- Edge: **Nginx** reverse proxy / load balancer in front of the gateway
- Edge: **Nginx** reverse proxy / load balancer in front of the gateway and the frontend
- Frontend: **React + Vite + TypeScript**, served as static files by its own Bun-based static server (`frontend/serve.ts`) — additive, non-graded, for manual click-testing
- Local orchestration: **Docker Compose**
- CI/CD: **GitHub Actions**
- Tests: **bun test** + **Testcontainers** for integration
Expand All @@ -37,6 +38,7 @@ quickbite/
│ ├── order/
│ ├── kitchen/
│ └── notification/
├── frontend/ # React + Vite UI — its own container, not a "service" (no DB, no events)
└── .claude/{commands,agents}/
```

Expand Down
47 changes: 35 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,20 +9,23 @@ are the actual point, not just the CRUD surface.
## Architecture

```
client → nginx (:80) → gateway → menu / order (synchronous reads)
|
v
order.placed (RabbitMQ)
|
+------------+------------+
v v
kitchen notification
(accepts, then ready) (logs every order event)
+-- / --> frontend (React UI)
client → nginx (:80) --------+
+-- /api/*, /health --> gateway → menu / order (synchronous reads)
|
v
order.placed (RabbitMQ)
|
+------------+------------+
v v
kitchen notification
(accepts, then ready) (logs every order event)
```

- **nginx is the system's only public entry point.** Every other service —
RabbitMQ, Postgres, Redis, and even the gateway itself — is reachable only
from other containers on the internal Docker network, never from the host.
RabbitMQ, Postgres, Redis, the gateway, and the frontend container itself —
is reachable only from other containers on the internal Docker network,
never from the host.
- Synchronous HTTP exists only on the path client → nginx → gateway → service,
for user-facing reads (menu, order status). The actual order workflow
(placed → accepted → ready → notified) happens entirely over RabbitMQ events.
Expand Down Expand Up @@ -60,6 +63,23 @@ Watch `docker compose logs -f kitchen notification` to see the event flow:
kitchen accepts the order immediately, marks it ready ~3s later (simulated
cook time), and notification logs all 3 order events as they happen.

## Frontend

A small React UI lives at `http://localhost/` (same nginx, same port 80) —
menu → cart → place order → a status screen that polls
`GET /api/orders/:id` live as it moves placed → accepted → ready, plus a
localStorage-backed order history. It's same-origin with the API (nginx
serves both under port 80), so there's no CORS configuration anywhere —
that's deliberate, not an oversight.

For local frontend development with hot reload, run the backend stack as
usual and run the frontend separately on the host:

```bash
docker compose up -d
bun run dev:frontend # Vite dev server; proxies /api to http://localhost:80
```

### Local development

For direct access to Postgres/RabbitMQ/Redis (e.g. a DB GUI, or running one
Expand All @@ -79,7 +99,10 @@ bun run typecheck

Runs automatically in CI on every push and pull request (`.github/workflows/ci.yml`).
No mocks for any infrastructure — integration and e2e tests spin up real
Postgres/RabbitMQ/Redis containers.
Postgres/RabbitMQ/Redis containers. The frontend's tests are split the same
way: pure logic (`frontend/tests/lib/`, no DOM) and component-rendering
tests (`frontend/tests/components/`, real DOM via happy-dom +
Testing Library) — no backend changes needed for either.

## Resilience features

Expand Down
Loading
Loading