Current Web App

This page documents the consumer web app as it exists now. It is an implementation guide, not a second domain model. The canonical vocabulary and rules remain in the Domain Definition, and architecture-significant decisions remain in the ADRs.

Purpose

The SvelteKit app is the consumer-facing entry point for PackyTrace. Its current experience is:

flowchart LR GTIN["Enter a GTIN"] --> Resolve["Resolve product passport"] Resolve --> Result["Read Verdict and product facts"] Result --> Fridge["Add product to Fridge"] Result --> Compare["Compare recent scans"]

The app is real-first: by default it calls the API gateway. An isolated in-memory adapter remains available for offline development and demo fallback.

Run It

Start the full platform and frontend:

make up
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 scan adapter

Current Routes

Route Current responsibility Data source
/ Manual GTIN lookup, recent scans, Compare entry point Browser state
/product/[id] Resolve and render a product passport; report unknown GTINs; add to Fridge ScanGateway plus browser state
/compare Select two or three recent fixture products and compare facts In-memory fixture catalog
/fridge View, recycle, or remove saved products Browser localStorage
/profile Show guest/mock-account state and open Health Profile Browser state
/preferences Edit Health Profile; currently mock-auth gated Browser localStorage
/setup Health Profile onboarding and explicit opt-in Browser localStorage
/auth Mock sign-in/register interaction In-memory boolean store

Navigation is intentionally scan-first: Scan, Fridge, and Profile. Mobile uses a bottom navigation bar; desktop uses a sidebar.

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["scan/index.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/scan/port.ts Defines ScanGateway.resolve and ScanGateway.report
src/lib/scan/index.ts Composition root; selects the adapter once
src/lib/scan/http.adapter.ts Calls the real gateway; maps HTTP 404 to unknown product
src/lib/scan/memory.adapter.ts Isolated fixture implementation and temporary mock Verdict
src/lib/api/client.ts Adds Visitor ID, credentials, base URL, and normalized API errors
src/lib/api/mappers.ts Anti-corruption layer from wire ResolvedPassport to UI types

Manual GTIN flow

The landing page uses manual GTIN entry because camera capture is not implemented yet. This is a truthful temporary affordance and exercises the real scan path:

  1. The user enters an 8–14 digit GTIN.
  2. The app navigates to /product/{gtin}.
  3. The product route calls scanGateway.resolve({ gtin }).
  4. The HTTP adapter sends POST /api/v1/scans.
  5. The mapper converts the gateway response into the current UI model.
  6. The resolved product is recorded in the lightweight recent-scans cache.

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 is ADR-004 visitor identity, 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. Until the real personalization journey lands, anonymous or absent Verdicts display as Unknown, never silently Good.
  • Overview contains product identity, Digital Label, nutrition, and allergens.
  • Journey and Eco are separate sections and can degrade independently.
  • Unknown products can be reported.
  • A resolved product can be added to the local Fridge.
  • Product images use ProductImage.svelte, which shows a neutral fallback when the real 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

Some journeys are intentionally browser-backed until their real backend phases land:

Store Persistence Current limitation
visitor.ts localStorage Pseudonymous ID cache only; not authentication
recentScans.ts localStorage Lightweight UI history, not server ScanRecord
healthProfile.ts localStorage Temporary profile and consent prototype
fridge.ts localStorage Deduplicates by product ID; not physical-item identity
auth.ts Memory only Boolean mock session; Keycloak integration is not present

Do not treat these stores as authoritative persistence. The backend-owned versions replace them as J2–J4 are completed.

Compare

Compare is currently a polished mock-only decision flow:

  • It starts from recent scans when at least two match fixture products.
  • 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 must remain fixture-backed until a real read model and owning backend boundary are decided. A future AI-generated comparison brief must 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/
│   ├── api/                low-level HTTP client and response mapper
│   ├── scan/               ScanGateway port, composition root, adapters
│   ├── components/
│   │   ├── layout/         responsive app shell and navigation
│   │   └── product/        product-result presentation components
│   ├── data/               fixture catalog used only by the memory adapter/Compare
│   ├── stores/             temporary browser and mock state
│   ├── types/              compatibility-oriented UI types
│   └── utils/              class merging, expiry helpers, legacy GS1 hint
└── 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

  • Camera scanning is not implemented; manual GTIN entry is the current scan action.
  • Authentication is mocked; Keycloak-backed identity arrives in J2.
  • The real personal Verdict and backend Health Profile arrive in J3.
  • Fridge persistence and physical-item identity arrive in J4.
  • Compare is fixture-backed and has no backend read model.
  • Journey and Eco data can be absent or degraded in real mode.
  • 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 backend truth.
  4. Preserve anonymous scan access and never treat Visitor ID as authentication.
  5. Render missing data honestly; 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.