Implemented phase 5 for the POC prototype in the Website service.

This commit is contained in:
javier committed 2026-09-21 13:32:05 +02:00
1 parent 89a7a7b8e5
commit 31d2d98d49
46 files changed
+4039 -162

No files matched your search

+18 -4
View File
@@ -7,6 +7,7 @@ The service:
- Serves the public catalogue, rendered per request: search results at `GET /search` (filtered through its query parameters), a listing at `GET /listing/{slug}`, and a seller's shop at `GET /shop/{slug}`; an unknown slug gets the not-found page (see [Catalogue](#catalogue)).
- Serves the path to a sale, for signed-in accounts: the offer sheet, the messages, the sale records, and the seller dashboard (see [Path to a sale](#path-to-a-sale)).
- Serves the gate: the sign-in screen at `GET /sign-in`, its passkey ceremonies under `POST /sign-in/passkey/…`, the demo sign-in at `POST /sign-in`, and the sign-out at `POST /sign-out` (see [Sign-in](#sign-in)).
- Serves what an account does for itself — its first sign-in, its profile and passkeys, opening a shop — and what a collector uses: the want list, the series screens, and the comparison of copies (see [Accounts and collecting](#accounts-and-collecting)).
- Serves the public grading guide at `GET /grading-guide`, and the new-listing form at `GET`/`POST /sell/new` for the owner of a shop (see [Selling](#selling)).
- Negotiates each request's language from the `lang` query parameter, the leading path segment, then its `Accept-Language` header, against the languages in the `WebsiteLibrary` String Catalog, falling back to the default (`en`) — asked for only where the answer is used, the bare landing route and the not-found page; pages are served from the per-language cache with `Content-Language` and `Vary: Accept-Language` headers.
- Builds every page on the shared `Page` scaffolding from `Infrastructure`, which assembles the document head around the page's own markup: the viewport declaration, the optional `description` summary and `rel="canonical"` link, the Open Graph / Twitter link-preview tags, the JSON-LD structured-data script, and the optional analytics tracker (see [Page metadata](#page-metadata)).
@@ -52,6 +53,8 @@ LogRequestsMiddleware
RootController (GET / → landing page, language negotiated; GET /<lang> → its other editions)
CatalogueController (GET /search, GET /listing/{slug}, GET /shop/{slug}, GET /grading-guide → rendered per request)
SessionController (GET /sign-in, POST /sign-in, POST /sign-out)
AccountController (GET/POST /welcome, GET /account, POST /account/…, GET/POST /sell/open-a-shop)
CollectingController (GET/POST /wants…, GET /series, GET /series/{slug}, GET /compare)
PasskeyController (POST /sign-in/passkey/options, POST /sign-in/passkey/finish → JSON)
ListingFormController (GET /sell/new, POST /sell/new → multipart, for a shop's owner)
TradeController (offer sheet, messages, sale records, seller dashboard → signed-in accounts only)
@@ -69,9 +72,9 @@ The screens follow the **CCN marketplace** design canvas:
### Catalogue
- **Data**: `Persistence`'s `CatalogueRepository` reads the shops and listings; the in-memory backend is migrated and seeded with demo data at launch.
- **Landing page**: rendered once and cached, so its listing rows are a snapshot taken at launch (`HomeListings`) — a copy sold since still shows there until the next launch.
- **Landing page**: rendered once and cached (`HomePages`), and rendered again whenever a sale or a new listing changes the catalogue — so its entity tag changes with it.
- **Search and shop**: `SearchParameters` turns the query string into a `ListingFilter` and builds every link that changes it — filter pills, tabs, pagination — so the screens work as plain `GET` forms and links. Searches default to listings shipping from the EU.
- **Gated actions**: signed out, buy, offer, message, save, and follow link to the sign-in screen with a `return` parameter; signed in, buy, offer, and message lead on (see [Path to a sale](#path-to-a-sale)), while save and follow render disabled. A sold copy, and a seller's own listing, show no actions.
- **Gated actions**: signed out, buy, offer, message, and follow link to the sign-in screen with a `return` parameter; signed in, buy, offer, and message lead on (see [Path to a sale](#path-to-a-sale)), while follow renders disabled. Saving a copy adds a want for it. A sold copy, and a seller's own listing, show no actions.
- **Covers**: a listing shows its first photo, or — like every seeded listing — a placeholder `Cover` drawn from its lettering and tone.
- **Scripts**: `js/shared.js` only submits the sort select on change and opens the filter rail on older desktop browsers; everything works without it.
@@ -86,8 +89,9 @@ How it holds together:
- **Sessions**: `SessionStore` keeps them in memory, keyed by a random token in the `ccn_session` cookie (`HttpOnly`, `SameSite=Lax`, and `Secure` over HTTPS). They end with the process.
- **Return to the intent**: the `return` parameter is followed only when it is a rooted path on this site; anything else returns home.
- **Per-viewer pages**: the controllers read the viewer from the request and send those pages `Cache-Control: private, no-cache`. The landing page stays cached for signed-out visitors and is rendered per request for signed-in ones.
- **Sign-out** is a `POST` form in the header, so a link on another site cannot trigger it. The not-found page always renders signed out.
- **Left out**: the first-sign-in screen (display name, backup passkey), recovery, and managing passkeys from an account screen.
- **Adding a passkey** happens from the account screen alone: the account's own session is the proof of who is asking.
- **Sign-out** is a `POST` form on the account screen, so a link on another site cannot trigger it. The not-found page always renders signed out.
- **Left out**: passkey recovery.
The signatures need a WebAuthn client, so the ceremonies are checked in a browser rather than in `swift test`: `Tests/Browser/passkeys.py` drives headless Chrome with a virtual authenticator over the DevTools protocol (Python standard library only; the script's header has the commands).
@@ -105,6 +109,16 @@ The signatures need a WebAuthn client, so the ceremonies are checked in a browse
- **Demo accounts**: every seeded shop has an owner; sign in as `joost@kestrelrow.example` to sell for Kestrel Row Comics.
- **Left out**: counter-offers, problem reports and disputes, the PDF record, price guidance, and vacation mode.
### Accounts and collecting
- **First sign-in**: a new account — joined by passkey or through the demo sign-in — meets `/welcome` once, picks its display name, and goes on to what it came for.
- **Account**: the display name, the passkeys with a way to add and revoke them, and the sign-out. Notifications and language settings are left out: nothing stands behind them yet.
- **Open a shop**: one form — the shop's public identity, private or business seller (a business states its KvK number, as Dutch consumer law asks), and policies — then straight on to the first listing. A new shop starts unverified and unrated.
- **Want list**: a want is words a listing's title must carry, within a grade and a price, among copies shipping from the EU. Matches listed since the list was last looked at are new, and ring the header's bell. Saving a copy, and a gap in a series, both fill the want form.
- **Series**: `Persistence` seeds a small catalogue of runs, which is what lets a gap show: every issue of the run, the cheapest copy of each, and *alert me* where none is listed. A series sellers list but the catalogue lacks is made up from its listings, without gaps.
- **Compare**: up to three copies of an issue, ordered by what each costs delivered to the Netherlands, flagging a customs border.
Every link on the site leads somewhere: `Tests/Browser/links.py` crawls them all — signed out, as a buyer, and as a seller — against a running service.
### Selling
- **One vocabulary**: `Grading` holds the scale, the conditions, and the eight-box defect checklist. The grading guide prints it, the form offers it, and `ListingForm` applies it — a colour-breaking crease caps a raw book at 7.0, and case scuffs are a slab's alone.
- **The form is the data model**: format, catalogue fields, slabbed or raw, grade, declared defects, photos, price. It posts as `multipart/form-data`, parsed with [MultipartKit](https://github.com/vapor/multipart-kit). Without scripts every field shows, labelled by what it applies to; with them, `js/shared.js` hides the rest.