# Session diary

## 2026-08-24 (later) — workspace move

Moved the app project to /srv/apps/dutyfreeprofessor and gave /srv/apps/
professormediagroup to the WordPress site (gateway rename), with a symlink at
professormediagroup/main/dutyfreeprofessor standing in for the nesting the gateway
cannot do. Migration via dump/restore rather than copying the postgres volume.
Caught in the act: rsync overwrote the new project's .project.json with the old
project's identity — recreated the project and re-copied with exclusions; the
compose image tag was the one non-parameterised name. Verified end to end: health,
stats identical, both domains gated and serving, audit clean.

## 2026-08-24 — domain swap

Moved the app to dutyfreeprofessor.demoing.info (add-dns, verify cert + gate, then
remove-dns on the old host) and created the professormedia WordPress project on the
freed professormediagroup.demoing.info (template clone). Gateway rename only supports
WordPress projects, so the app workspace keeps its name. Verified: cert issued, gate
fires on both hosts with deep-link redirects intact, WP renders with the template
stack, audit clean. Gotcha hit: removing then recreating a DNS name leaves a few
minutes of negative-cache misses on machines that queried during the gap.

## 2026-08-23 (evening) — the quality pass

Rian asked for four things: unfound problems, workarounds for known limits, a data-quality
assessment with manual spot checks, and a discussion-list review. Ran seventeen
independent auditors (data/backend/frontend/copy finders with adversarial verifiers,
live price re-checks against real retailer pages, workaround probes). 51 findings
confirmed, 0 refuted. Fixed everything critical the same session; v0.13.0 → v0.13.2.

Data quality verdict: the foundations held (FX consistent, extremes real, live samples
matched to the penny), but four systemic bugs were live: variant prices recorded against
full-size bottles, one medal in three on the wrong bottle, 327 duplicate products from
a one-line resolution bug, and out-of-stock prices winning "cheapest". All fixed with
migrations dry-run on a snapshot copy first.

The compliance find outranks everything: the whole Heinemann platform family disallows
the search endpoint under modern robots matching — stdlib's parser predates wildcards
and read it as permission. Stopped collection across the family, removed one mistaken
run, built a stricter-than-RFC matcher into the collectors, told the discussion list.

Process notes: the audit's own live checks used the same broken stdlib semantics and
initially cleared BorderShop — re-checking every host with the new matcher caught it.
Deploys kill in-flight collections (hit twice); batched commits saved the partial work.
My savings-page layout swapped under a shopper's second click — caught by clicking
through the flow in a real browser, not by the typecheck.

Four new rules in agents.md: modern robots matching with any-Disallow-is-no; parse the
price the shopper pays and quarantine the implausible; one tokenizer does not fit every
matcher; a fallback identity lookup considers every key-holder.

## 2026-08-23 — the trip becomes a persistent airport set

Reworked the headline feature on rian's instruction: the shopper picks *which airports
they will be at* rather than entering a flight path, and that choice then follows them
across the whole site. Added Adam's wireframe navigation, announcement ticker and header
search, and rebuilt the home page around his editorial layout.

- New `MyAirportsProvider` (localStorage) + `AirportPicker`; header control; `at=` query
  scoping on the catalogue, similar-products and savings endpoints.
- `/savings` replaces the flight-path trip page, with search / category / minimum-saving
  / sort / awards / exclusives filters. `/trip` redirects.
- Product page gained an "On your trip" verdict and marks the shopper's shops in the
  price table; the coverage page's airport cards double as the picker.
- Exclusives and Awards became real filtered pages (the flags already existed); Reviews,
  Price tracker, Buying guides, News and Newsletter are explanatory.
- Stats strip removed from the home page, reduced to one line, per rian.

Verified in the browser end to end: selecting ATH + DXB + LHR switched the home page from
the generic featured route to "Where to buy on your trip", and the product page to a
per-trip verdict.

Four data bugs found by looking at the result rather than the tests:
- Mexico City's store quotes USD beside pesos; we stored the USD figure as MXN, so the
  whole shop published at ~1/17 of its real prices and an $18 XO cognac led "cheapest
  anywhere". Collector now reads the page's declared `priceCurrency`. Audited the other
  six Avolta stores by comparing local-price medians against their currency — all sane,
  so the fault was isolated.
- A related-products rail kept an unscoped query behind the label "in 3 of your shops".
- Seven Open Food Facts image URLs contained `/products/invalid/` and 404'd; rejected on
  ingest now, migration `b8c9d0e1f2a3` clears the stored ones.
- Madrid (configured, zero products) was offered in the picker and counted as a shop.

A fifth, found while checking the explainers: `spec.html` had no route and fell through to
the SPA catch-all, returning 200 with the app inside it. Explainers now serve by rule.

Recorded three rules in `agents.md`: never infer currency from a symbol; a personalised
label needs a personalised query behind it; a catch-all route makes a missing route look
like a working one.

## 2026-08-21 — build session: PoC from empty workspace to live app

Started from a scaffolded workspace with no code. Ended with the proof of concept
deployed and populated with real data.

Order of work was deliberately riskiest-first: proved the retailer APIs returned usable
data before writing any application code, and proved the barcode join produced real
cross-retailer matches before building any UI.

- Verified Dubai's store API (33,871 products, 100% GTIN coverage) and Heinemann's
  global catalogue search (10,336 products, GTIN present) by direct probe.
- Built the fetch port, normalisers, both collectors, ingest with barcode matching,
  FX conversion, the API, and the React SPA.
- Deployed, ran collections in the container, imported competition medals.
- Fixed in-session: GTIN zero-padding rejected long barcodes; brand repeated in a name
  broke the fallback match key; Heinemann pagination used the wrong field name;
  Dubai paged the entire 33k catalogue looking for liquor; packaging words were being
  stored as categories (with a backfill migration for rows already written).

Substrate notes worth keeping: `srv-gw db-query --format json` returns a bare list, not
an object with a `rows` key, and returns numeric columns as strings.

## 2026-08-22 — autonomous session: from working PoC to near-launchable demo

Ran roughly four hours unattended with three helper agents: two building and verifying
collectors, one drafting the decisions content. Orchestration held; all three delivered
usable work.

Added Dublin & Cork and Keflavik as collectors, taking the site to 8 shops and ~730
comparable products. Keflavik was the valuable one — 100% barcode coverage, which is
what let Dubai and Reykjavik match across continents.

Rebuilt the front end around the trip search: a real home page, a working airports
coverage page, decluttered consumer filters with build-time toggles moved to settings,
in-app pages for every future feature, and a themed decisions-and-realities page.

Fixed in-session: a GET endpoint that wrote to the database and deadlocked against a
running collection; a stylesheet that nothing imported (silent, unstyled render); an
image search that sent duplicated brands and sizes and therefore matched nothing; a
single delisted product aborting a whole collection; and "was" prices equal to the
current price being shown as savings.

Added `main/check.sh` — regenerates the typed API client and typechecks in seconds,
after a three-minute Docker build failed on a one-character JSX error.

## 2026-08-25 14:24 — rian

**Changed** (40 files, detected via mtime):
- `.archive/public-index-placeholder-pre-app.html`
- `.env`

## 2026-09-04 22:49 — rian

**Changed** (2 files, detected via git):
- `main/docs/COLLECTORS.md`
- `main/static`

## 2026-09-04 22:56 — rian

**Changed** (4 files, detected via git):
- `main/docs/COLLECTORS.md`
- `main/docs/DATA-MODEL.md`
- `main/docs/RUNBOOK.md`
- `main/static`

## 2026-09-04 23:05 — rian

**Changed** (1 files, detected via git):
- `main/docs/DATA-MODEL.md`

## 2026-09-05 09:53 — rian

**Changed** (3 files, detected via git):
- `main/app/services/collectors/fetch.py`
- `main/browser/server.py`
- `main/tests/test_render.py`

## 2026-09-05 10:05 — rian

**Changed** (1 files, detected via git):
- `import/issues.md`

## 2026-09-05 15:15 — rian

**Changed** (3 files, detected via git):
- `main/CHANGELOG.md`
- `main/docs/SEO.md`
- `main/web/tsconfig.tsbuildinfo`

## 2026-09-05 15:47 — rian

**Changed** (3 files, detected via git):
- `import/items.json`
- `main/CHANGELOG.md`
- `main/web/tsconfig.tsbuildinfo`

## 2026-09-09 20:04 — rian

**Changed** (2 files, detected via git):
- `main/web/src/api/schema.ts`
- `main/web/tsconfig.tsbuildinfo`

## 2026-09-09 20:06 — rian

**Changed** (2 files, detected via git):
- `main/web/src/api/schema.ts`
- `main/web/tsconfig.tsbuildinfo`

## 2026-09-09 21:20 — rian

**Changed** (5 files, detected via git):
- `import/items.json`
- `import/progress.json`
- `main/docs/RUNBOOK.md`
- `.claude/commands/stream-s.md`
- `notes/review-drafts-2026-09-10.md`

## 2026-09-09 22:36 — rian

**Changed** (3 files, detected via git):
- `brief.md`
- `import/progress.json`
- `main/web/tsconfig.tsbuildinfo`

## 2026-09-10 10:30 — rian

**Changed** (1 files, detected via git):
- `main/web/tsconfig.tsbuildinfo`

## 2026-09-10 13:25 — rian

**Changed** (11 files, detected via git):
- `brief.md`
- `import/items.json`
- `import/progress.json`
- `main/CHANGELOG.md`
- `main/app/main.py`
- `main/web/tsconfig.tsbuildinfo`
- `.claude/commands/stream-r.md`
- `.claude/commands/stream-r2.md`
- `.claude/commands/stream-r3.md`
- `deploy/`
- `main/tests/test_catch_all_confinement.py`

## 2026-09-10 14:59 — rian

**Changed** (4 files, detected via git):
- `main/web/tsconfig.tsbuildinfo`
- `deploy/`
- `(1).md"`
- `notes/Duty_Free_Professor_Airport_Guide_Heathrow.md`

## 2026-09-10 17:40 — rian

**Changed** (3 files, detected via git):
- `main/web/tsconfig.tsbuildinfo`
- `(1).md"`
- `notes/Duty_Free_Professor_Airport_Guide_Heathrow.md`

## 2026-09-10 17:43 — rian

**Changed** (3 files, detected via git):
- `main/web/tsconfig.tsbuildinfo`
- `(1).md"`
- `notes/Duty_Free_Professor_Airport_Guide_Heathrow.md`

## 2026-09-10 19:08 — rian

**Changed** (4 files, detected via git):
- `brief.md`
- `main/web/tsconfig.tsbuildinfo`
- `(1).md"`
- `notes/Duty_Free_Professor_Airport_Guide_Heathrow.md`

## 2026-09-10 20:55 — rian

**Changed** (5 files, detected via git):
- `brief.md`
- `main/web/tsconfig.tsbuildinfo`
- `(1).md"`
- `notes/Duty_Free_Professor_Airport_Guide_Heathrow.md`
- `notes/home-hero.jpg`
