Current Web App¶
This page documents the consumer web app as verified against the source tree on 2026-08-01. It is an implementation guide, not a second domain model. The canonical vocabulary and rules remain in the Domain Definition, while implementation structure is explained in the Architecture reference.
Purpose¶
The SvelteKit app is the consumer-facing entry point for PackyTrace. Its current experience is:
The app uses the API gateway by default. An isolated in-memory adapter remains available for offline development and demo fallback.
Run It¶
Start the platform without attaching in one terminal:
make up-detached
Start the frontend in a second terminal:
make web
The frontend runs at http://localhost:5173 and calls the gateway at http://localhost:8080 by default.
Run only the frontend against the in-memory fixture catalog:
PUBLIC_SCAN_ADAPTER=memory make web
Useful frontend checks:
cd client/web-app
npm run check
npm run build
The public build-time settings are documented in client/web-app/.env.example:
| Setting | Default | Purpose |
|---|---|---|
| PUBLIC_API_BASE_URL | http://localhost:8080 | API gateway origin |
| PUBLIC_SCAN_ADAPTER | http | Selects http or memory mode for scan, auth, and profile adapters |
Current Routes¶
| Route | Current responsibility | Data source |
|---|---|---|
| / | Camera barcode scanning, "Jump back in" (last few scans), Compare entry point | Camera + browser state |
| /product/[id] | Resolve and render a product passport; report unknown GTINs; add to Fridge or Shopping List | ScanGateway plus browser state |
| /compare | Select two or three scanned products and compare facts | Recent-scan cache |
| /fridge | View owned items, mark used/thrown, prompt to re-buy on the list | fridge-service |
| /shopping-list | Product-linked list to buy: search-to-add, quantity, mark bought (-> add to fridge) | shopping-list-service |
| /history | Searchable device-local recent products, capped at 100 and deduplicated by product | Browser scan cache |
| /profile | Show guest/mock-account state and open Health Profile | Browser state |
| /preferences | Edit consent-gated Health Profile; shows session-expired state when needed | Profile gateway |
| /setup | Health Profile onboarding and explicit opt-in | Profile gateway plus browser UI cache |
| /auth | Register, sign in, and store Keycloak-issued session tokens | Auth gateway |
Navigation is scan-first, with Scan as the raised center action. The mobile bottom bar carries five destinations, Fridge · List · Scan · History · Profile, and the desktop sidebar mirrors them. The Fridge ↔ Shopping List loop is reached through in-page prompts and cross-links rather than a backend coupling.
Scan Architecture¶
The UI depends on one frontend port instead of directly depending on HTTP or fixture data:
Relevant files:
| Path | Role |
|---|---|
| src/lib/business_logic/outbound_ports/scan.ts | Defines the ScanGateway required by the application service |
| src/lib/business_logic/application/scan.ts | Implements scan resolution and unknown-product reporting use cases |
| src/lib/infrastructure/config/scan.ts | Composition root; selects the HTTP or memory adapter once |
| src/lib/infrastructure/outbound_adapters/http/scan.ts | Calls the API gateway and maps HTTP responses |
| src/lib/infrastructure/outbound_adapters/memory/scan.ts | Isolated fixture implementation |
| src/lib/infrastructure/outbound_adapters/http/client.ts | Adds Visitor ID, credentials, bearer token, refresh, and normalized API errors |
| src/lib/infrastructure/outbound_adapters/http/mappers.ts | Anti-corruption mapping from wire responses to UI types |
| src/lib/components/scan/BarcodeScanner.svelte | Camera scanner with native BarcodeDetector and ZXing fallback |
| src/lib/business_logic/domain/gs1.ts | Decodes supported Digital Link qualifiers and bare EAN/UPC/GTIN digits |
Camera scan flow¶
The landing page opens a full-screen camera scanner. It uses the browser's native BarcodeDetector when available and falls back to @zxing/browser elsewhere.
The scanner accepts:
- GS1 Digital Link QR codes such as /01/{gtin}/10/{lot}?17={expiry};
- bare EAN-8, UPC-A, EAN-13, or GTIN digit payloads.
Digital Link URLs preserve supported lot, serial and expiry application identifiers. Bare linear barcode payloads provide only the GTIN; the client does not invent missing qualifiers.
If camera access is blocked or unsupported, the scanner shows a recovery message. Demo QR codes can be generated with:
scripts/gen-demo-qr.sh
The product route records the resolved product in the lightweight recent-scans cache. That cached product powers Compare without creating extra scan records.
An unknown GTIN renders the report flow. Reporting calls POST /api/v1/products/{gtin}/requests through ScanGateway.report.
Visitor identity¶
src/lib/stores/visitor.ts creates a pseudonymous UUID on first API use and caches it in localStorage. The API client sends it as X-Visitor-Id. This identifies an anonymous visitor; it is not authentication. The client also sends credentials so a gateway-set visitor cookie can participate in same-origin or credentialed-CORS flows.
Product Result¶
The product page is the primary result surface:
- A personal Verdict hero appears first for authenticated profiles. Anonymous, missing-profile or unavailable Verdict states remain explicit and are never silently presented as Good.
- Overview contains product identity, Digital Label, nutrition, and allergens.
- Journey and Eco are separate sections and can degrade independently.
- Recipe suggestions are optional and degrade independently.
- Unknown products can be reported.
- A resolved product can be added to the Fridge through the gateway.
- Product images use ProductImage.svelte, which shows a neutral fallback when the catalog image key has no bundled asset.
The current ScanResponse and Product interfaces are compatibility-oriented UI models. They are not backend aggregates and must not override the canonical domain.
Browser State¶
The following stores are client caches, not service-owned state:
| Store | Persistence | Current limitation |
|---|---|---|
| visitor.ts | localStorage | Pseudonymous ID cache only; not authentication |
| recentScans.ts | localStorage | Lightweight UI history plus product snapshot, not server ScanRecord |
| healthProfile.ts | localStorage | UI echo/cache; HTTP mode saves through ProfileGateway |
| auth.ts | localStorage token vault + Svelte store | Client cache of Keycloak-issued tokens; gateway remains authoritative |
| vocabulary.ts | memory/Svelte store | Display vocabulary loaded through the vocabulary gateway |
Do not treat these stores as authoritative persistence. They are local UX caches around backend-owned state, or temporary local-only state where the backend slice is not yet connected.
Authentication And Sessions¶
Identity also uses the API-backed path by default. In HTTP mode, authGateway uses the gateway's account/session endpoints. The token vault stores the Keycloak access token, refresh token, and display account in localStorage.
Request handling is defensive:
- apiFetch attaches the bearer token only when the access token is still valid.
- If an access token is expired but a refresh token exists, the client calls POST /api/v1/sessions/refresh before sending the protected request.
- If the gateway returns 401 for a request that had a stored session, the client tries one refresh and replays the request.
- If refresh fails, the token vault is cleared and locked pages show a "session expired" message instead of a generic guest state.
The current auth flow still uses Keycloak-backed Resource Owner Password Credentials and stores tokens in localStorage. This demonstration flow is vulnerable to browser script compromise and is not approved for production users. Replace it with Authorization Code + PKCE and an appropriate token-storage/session design before production use.
Compare¶
Compare is a recent-scan decision aid:
- It starts from products the user actually scanned.
- The user selects two or three products.
- It highlights field-level differences such as eco score and listed sugar.
- It groups detailed facts into overview, nutrition, and sustainability.
- It does not claim an overall health winner.
Compare intentionally uses the cached resolved product snapshot from recentScans instead of re-fetching. That avoids creating duplicate scan records just to compare two products. A future server-backed comparison read model must still be grounded in displayed structured facts and must never replace the deterministic personal Verdict.
UI Structure¶
client/web-app/src/
├── app.css semantic tokens and shared UI primitive classes
├── lib/
│ ├── business_logic/
│ │ ├── domain/ UI-domain types and pure rules
│ │ ├── application/ use-case implementations
│ │ ├── inbound_ports/ interfaces called by routes/components
│ │ └── outbound_ports/ dependencies required by application services
│ ├── infrastructure/
│ │ ├── config/ composition roots, one per capability
│ │ └── outbound_adapters/ HTTP and memory implementations
│ ├── components/
│ │ ├── layout/ responsive app shell and navigation
│ │ ├── product/ product-result presentation components
│ │ └── scan/ camera scanner component
│ ├── i18n/ locale setup and translation files
│ ├── stores/ browser caches and shared reactive state
│ └── utils/ navigation, styling and image helpers
└── routes/ SvelteKit route components
Component-local state uses Svelte 5 runes. Shared reactive state uses Svelte stores.
Visual Language¶
The app uses Tailwind CSS with semantic HSL tokens defined in src/app.css.
The small shared primitive layer keeps high-traffic surfaces coherent:
| Class | Use |
|---|---|
| .ui-button-primary | Main action on a screen |
| .ui-button-secondary | Secondary or alternative action |
| .ui-button-icon | Back and compact icon actions |
| .ui-card | Static grouped content |
| .ui-card-interactive | Clickable product or option cards |
| .ui-input | Text and GTIN entry |
Use semantic colors such as primary, accent, success, warning, and destructive; do not add one-off hard-coded colors. Prefer rounded-xl for controls, rounded-2xl for content cards, and rounded-full only for pills, avatars, and circular status indicators. Focus-visible and reduced-motion behavior are defined globally.
Known Limitations¶
- Browser camera support varies; the scanner uses ZXing fallback where native BarcodeDetector is unavailable.
- ROPC and local-storage tokens are demonstration-only; PKCE is not implemented yet.
- A failed vocabulary request clears the profile pickers and offers no visible retry yet.
- Compare is browser-cache-backed and has no backend read model.
- Recent scan history is device-local, capped at 100 products and deduplicated by product.
- Journey and Eco data can be absent or degraded in HTTP mode.
- Italian, Spanish, and French strings require native review before production-quality localization can be claimed.
- Reviews remain in compatibility types/fixtures but are not part of the current product-result navigation.
Rules for New Frontend Work¶
- Keep routes and components dependent on ports, not concrete infrastructure.
- Add external response translation in an adapter or mapper, not in a component.
- Keep browser stores explicitly temporary; do not present them as authoritative backend state.
- Preserve anonymous scan access and never treat Visitor ID as authentication.
- Render missing data as unavailable; do not invent sustainability, health, or origin facts.
- Use the shared UI primitives and semantic tokens before introducing new styles.
- Update this page whenever the shipped frontend behavior or structure changes.