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:

flowchart LR Scan["Scan product QR/barcode"] --> Decode["Decode GTIN and encoded qualifiers"] Decode --> Resolve["Resolve product passport"] Resolve --> Result["Read Verdict and product facts"] Result --> Fridge["Add product to Fridge"] Result --> List["Add product to Shopping List"] Result --> Compare["Compare recent scans"]

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:

flowchart LR UI["Routes and components"] --> Port["ScanGateway port"] Root["infrastructure/config/scan.ts<br/>composition root"] --> Port Port --> HTTP["HTTP adapter"] Port --> Memory["In-memory adapter"] HTTP --> Client["API client"] Client --> Gateway["api-gateway"] HTTP --> Mapper["ResolvedPassport mapper"] Memory --> Fixtures["mockProducts fixtures"]

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.

sequenceDiagram participant User participant Scanner as BarcodeScanner participant Page as Scan page participant Product as Product page participant Gateway as api-gateway User->>Scanner: allow camera and point at code Scanner->>Scanner: decode QR/EAN/UPC Scanner->>Scanner: decode GTIN and encoded qualifiers Scanner-->>Page: decoded scan Page->>Product: navigate with GTIN and supported qualifiers Product->>Gateway: POST /api/v1/scans Gateway-->>Product: resolved passport

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:

  1. apiFetch attaches the bearer token only when the access token is still valid.
  2. If an access token is expired but a refresh token exists, the client calls POST /api/v1/sessions/refresh before sending the protected request.
  3. If the gateway returns 401 for a request that had a stored session, the client tries one refresh and replays the request.
  4. 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

  1. Keep routes and components dependent on ports, not concrete infrastructure.
  2. Add external response translation in an adapter or mapper, not in a component.
  3. Keep browser stores explicitly temporary; do not present them as authoritative backend state.
  4. Preserve anonymous scan access and never treat Visitor ID as authentication.
  5. Render missing data as unavailable; do not invent sustainability, health, or origin facts.
  6. Use the shared UI primitives and semantic tokens before introducing new styles.
  7. Update this page whenever the shipped frontend behavior or structure changes.