Revision 7 of 16

Makes the reference Blazor app the source of truth: exact copy per screen, Bulma tokens, one-segment vanity routes, and permanent delete replacing soft delete.

20.9 KB

+125 −79 lines of text vs revision 6 (this site's diff)

The text of revision 7 against revision 6: + marks an added line, − a removed one, with three unchanged lines of context around each change; old and new line numbers on the left where there is room.
Old New Change Line
@@ -2,8 +2,10 @@
22
33Build the complete application in this repository. Work autonomously from start to finish and stop only when the app is complete.
44
5If a minor detail is not specified, choose a reasonable solution and document it. Do not copy an existing implementation of The Urlist.
5+If a minor detail is not specified, choose a reasonable solution and document it. Replicate the observable user experience of the reference app exactly: layouts, visible copy (button labels, errors, headings, tooltips, states), interactions, and the overall visual language. Do not copy the reference app's implementation or framework.
66
7+Reference app (source of truth for UX): `the-urlist/blazor-static-web-apps` at https://github.com/the-urlist/blazor-static-web-apps — see `Client/Pages/*.razor`, `Client/Shared/*.razor`, and `Client/wwwroot/css/app.css` for exact copy, layout, and design tokens. A live demo (if available) is linked from that repository's README.
8+
79## Technical specification and checklist
810
911Before coding, create and commit `TECHNICAL_SPEC.md`. Keep it concise; do not restate this document.
@@ -27,153 +29,193 @@
2729
2830Use the Node.js runtime, not Edge, for SQLite, sessions, file access, and metadata fetching.
2931
30Before UI work, invoke `/postrboard` and follow it across the app. Make list creation and editing the primary work surface. Use Postrboard classes and tokens before custom CSS. Avoid generic SaaS sections, decorative cards, fake data, emoji, icon grids, and gradient blobs. Run the Postrboard self-review before completion.
31
32## Product
32+Design system: replicate the reference app's visual language, which is **Bulma CSS** (0.9.x) with **Font Awesome 5.6.3** icons and the **Roboto** font at an 18px base size. Use these exact design tokens:
3333
34The Urlist lets people create ordered lists of web links and publish each list at a public alias.
34+- Primary: `#20ae96` (teal). Danger/error: `#d9255e`.
35+- Light theme: text `#222c38`, main background `#fff`, section background `#f9fafc`, card shadow `0 50px 40px -40px #e8e8e8`.
36+- Dark theme: text `#fff`, main background `#14161a`, section background `#353638`, card shadow `0 50px 40px -40px #292727`.
37+- Cards: 4px border radius; inputs/textareas: 3.5rem height, 1px `#979797` border, 4px radius, `outline: none`.
38+- Invalid input state: 4px `--danger` border with a ~0.6s horizontal shake animation, plus the field's error text below it.
3539
36Use this home-page copy:
40+Use these image assets (recreate equivalent SVG/PNG files under a `public/images/` directory using the same filenames):
3741
38> Group links, save and share them with the world.
42+- `logo.svg` (navbar brand, 100×60, light theme) and `logo-dark.svg` (navbar brand, dark theme)
43+- `banner-logo-large.svg` (home hero, 500×500)
44+- `burger.svg` (mobile menu toggle, ~60×60)
45+- `no-image.png` (link image placeholder)
46+- `bg.png` (small dot texture for My Lists tiles)
47+- `urly.png` (not-found mascot, 500×500, a sad green fuzzy character)
3948
40> Add links to a list and share it with one simple URL.
49+## Product
4150
42Ownership is set at publication:
51+The Urlist lets people create ordered lists of web links and publish each list at a public alias.
4352
44- Signed out: the list is anonymous, permanent, immutable, non-deletable, and never claimable.
45- Signed in: the list belongs to the current user, who can edit, soft-delete, and restore it.
53+Publishing requires a signed-in user; there is no anonymous publishing. The Publish button reads **"Login to Publish"** and is disabled while signed out. List ownership belongs to the signed-in user who published it. The owner can edit and delete their list at any time while signed in. Deletion is **permanent**: the list's alias becomes immediately available to anyone. There is no soft delete, restore, tombstone, or anonymous list in this product.
4654
4755Anyone can view an active list.
4856
49The server is authoritative: revalidate input, ownership, alias availability, and immutability on every write; ignore client-supplied owner IDs.
57+The server is authoritative: revalidate input, ownership, and alias availability on every write; ignore client-supplied owner IDs.
5058
5159## Routes
5260
5361| Route | Behavior |
5462|---|---|
55| `/` | Home page and first-link entry |
56| `/s/new` | Create or resume the one local draft |
57| `/s/edit/{...alias}` | Owner-only, reloadable edit page |
58| `/s/mylists` | Current user's active and deleted lists |
59| `/{...alias}` | Active public list, deleted tombstone, or unused-alias state |
63+| `/` | Home page |
64+| `/s/new` | Editor for the one local draft (new mode) |
65+| `/s/edit` | Editor for an owned, published list (edit mode) — **no alias in the URL**; the list being edited is the one currently loaded in the editor state |
66+| `/s/mylists` | Current user's lists (login required) |
67+| `/s/terms` | Static Terms of Service page (reasonable content is fine) |
68+| `/{vanity}` | Public list — **a single path segment only** |
6069
61Aliases may contain multiple `/`-separated segments. Static routes take priority; reserve first segments `s`, `api`, `auth`, `__test`, and `.well-known`.
70+404 page (everything else, including multi-segment paths): show the text **"Sorry, there's nothing at this address."**
6271
63Global navigation must include Home, New, My Lists when signed in, Login or the current user with Log out, and Light/Dark/System theme controls. Confirm before New clears a non-empty draft.
72+Static routes take priority over `/{vanity}`. Reserved first segments: `s`, `api`, `__test`.
6473
65## Draft and editor
74+### Navigation bar
6675
67Keep one draft per browser profile in browser-local storage. It must survive reload, navigation, login, and logout. Clear it only after publication or confirmed discard.
76+Bulma navbar. Left: logo (alt "urlist logo"); on mobile, a hamburger button (`burger.svg`) that toggles the menu. Menu items in this order, each an icon plus label:
6877
69The home page accepts the first URL, adds it to the current draft, starts metadata enrichment, and opens `/s/new`.
78+1. `New` — icon `fas fa-plus-circle`
79+2. `My Lists` — icon `fas fa-user-circle` — **only when signed in**
80+3. `About` — external link `https://aka.ms/theurlist`, icon `fas fa-question-circle`
81+4. `Terms` — link to `/s/terms`, icon `fas fa-info`
7082
71A list:
83+Right: theme dropdown, then either the Login item (icon `fas fa-sign-in-alt`, label "Login", opens the login modal) or the signed-in user's avatar and name with a dropdown showing "Signed in with {provider}" and a "Log Out" item (icon `fas fa-sign-out-alt`).
7284
73- Has no product-level link limit
74- Allows duplicate destination URLs as separate entries
75- Has an optional description and alias
85+Clicking `New` while on `/s/new` with a non-empty draft must pop a confirm modal: title **"Clear this list?"**, prompt **"This will reset the current list and you will lose all changes. Are you sure you want to do that?"** with OK/Cancel. On OK, reset the draft and go to `/s/new`. Otherwise, clicking `New` resets the draft and goes to `/s/new`.
7686
77For each link, store a stable ID, immutable destination URL, optional title, optional description, optional enrichment-controlled image, and position.
87+Document title: "The Urlist - Share the internet".
7888
79Authors can add and delete links and edit titles and descriptions. They cannot edit an existing destination or image URL.
89+## Home page
8090
81Accept only absolute HTTP or HTTPS destinations with valid hosts and no embedded credentials. You may add `https://` to domain-like input, but show the normalized URL. Show field-associated validation errors.
91+Top section, two columns on desktop (banner hidden on mobile):
8292
83### Reordering
93+- Left column, in order:
94+ 1. H1: **Group links, Save & Share them with the world** — the single words "Group", "Save", and "Share" rendered in the primary teal color.
95+ 2. Paragraph: **Add links to a list and share it with one simple URL.**
96+ 3. Paragraph: **Create a list anonymously or login to save, manage, and edit your lists.**
97+- Right column: `banner-logo-large.svg` (lazy loaded, 500×500).
8498
85Drag-and-drop is the primary reorder interaction: provide a visible handle per editable link, pointer and touch dragging to any position, and clear picked-up, dragging, drop-target, and dropped states. Also provide accessible Move up and Move down controls. Persist order in the draft and SQLite.
99+Bottom section on a distinct background: a centered, bold, primary-colored H2 **Get Started**, followed by the first-link input widget.
86100
87## Live metadata
101+First-link input widget:
88102
89Fetch and parse the live destination page on the server after a link is added. Show Pending, Succeeded, or Failed per link.
103+- Helper text above the input: **"Enter a link and press enter"**
104+- Extra-large text input with placeholder `http://example.com`
105+- Submit with Enter key. Validation: non-empty after trim; must parse as an absolute `http` or `https` URL (a bare domain-like value may be prefixed with `http://`); the host (after stripping a leading `www.`) must be a DNS-like host containing at least one dot. Invalid: apply the invalid input state and show **"That doesn't look like a valid URL"**
106+- On valid submit: add the link to the local draft, clear the input, refocus it, start metadata enrichment for that link, navigate to `/s/new`.
90107
91Abort the fetch after 10 seconds and settle the link into a terminal state. Failure keeps the link and permits manual title/description editing and publication.
108+## Draft and editor
92109
93Metadata precedence:
110+Keep one draft per browser profile in browser-local storage. It holds the list's alias, description, and links with their order. It must survive reload, navigation, login, and logout. Clear it only after successful publication or a confirmed reset via the `New` guard above.
94111
951. Open Graph
962. Twitter Card (`name` or `property`)
973. HTML title/meta description/page icon, with `<h1>` and `/favicon.ico` as final fallbacks
112+Both editor pages (`/s/new` and `/s/edit`) show, top to bottom: the **publish bar**, the link list (when non-empty), and (on `/s/new`) the first-link input widget for adding more links.
98113
99Resolve relative images against the final URL after redirects. Empty metadata is a successful result. Preserve manual title and description edits. Show a neutral image placeholder when needed.
114+### Publish bar
100115
101The fetcher must be SSRF-safe: HTTP/HTTPS only, no credentials, reject non-public/internal addresses, revalidate DNS on every redirect to prevent rebinding, follow at most five redirects, limit content to 2 MiB, accept only HTML/XHTML, and never forward app credentials.
116+A card with three columns: **Vanity Url** input, **Description** textarea, and the **Publish** button.
102117
103## Aliases and publication
118+- **Vanity Url** (label "Vanity Url"): tooltip *"Optional: Enter a vanity url for this list (i.e. my-list becomes theurlist.com/my-list). If you leave this box blank, we'll generate a random vanity for you."* While the user types: if it contains anything other than letters, numbers, or dashes, show **"Vanity URLs can only contain letters, numbers, and dashes."** Otherwise, after ≥300ms of the value being stable, check availability against the server; if it is in use, show **"This vanity URL is already in use. Please choose another."** On `/s/edit` the input is **disabled** (the alias never changes after publication).
119+- **Description** (label "Description"): textarea, 2 rows. Tooltip *"Optional: The description will show up as the title on your public list page."*
120+- **Publish** button (large, primary, bold): while signed out it is disabled with the label **"Login to Publish"** (clicking it opens the login modal). While signed in it is enabled only when the alias is valid (or blank) and the list has at least one link. On success the server response becomes the editor state and the browser navigates to `/{vanity}`. On failure keep all user input and show an error.
104121
105Normalize aliases to lowercase. An alias has one or more `/`-separated segments. Each segment:
122+### Link list editor
106123
107- Uses only `a-z`, `0-9`, and hyphens
108- Is 1–50 characters
109- Does not start or end with a hyphen
124+When the list has links, show a "Links" heading with a right-aligned hint **"Drag links to re-order"**, then the linked rows.
110125
111The full alias is at most 200 characters.
126+Each row: a drag grip (`fas fa-grip-vertical`) at the far left; a 64px image (24px on mobile) or the `no-image.png` placeholder; a **Title** text input (placeholder "Enter a title", bold text); a **Description** textarea (placeholder "Enter a description"); a **URL** text input showing the destination (editable); and a delete button (`fas fa-times`) that removes the row immediately with **no confirmation**.
112127
113Match the exact full path, so `burke` and `burke/links` can both exist. Once published, aliases are permanent and globally unique in SQLite across active, deleted, and anonymous lists; they never change or become available again.
128+While a link's metadata fetch is in flight, show a small progress bar overlay on that row; when it settles the progress bar disappears. There are no per-link status badges or error messages: failed metadata simply leaves the fields as they are (or empty) and clears the progress bar.
114129
115If blank, generate an available seven-character lowercase alphanumeric alias.
130+Reordering is **drag-and-drop only**, via the grip handle, with smooth animation. No other reorder affordances.
116131
117Block publish/save when the list is empty, metadata is Pending, or the alias is invalid/unavailable. On failure, keep all user input.
132+Allow duplicate destination URLs as separate entries. No product-level link limit. Each link keeps a stable server ID, destination URL, optional title, optional description, optional image, and position. Persist order.
118133
119For anonymous publication, require a blocking confirmation that says the list cannot be edited or deleted after publication. Use **Publish permanently** and **Cancel**. Anonymous lists never appear in My Lists.
134+## Live metadata
120135
121## Mock login and ownership
136+Fetch and parse the live destination page **server-side** after a link is added. Cap the fetch at 20 seconds. Failure keeps the link with whatever metadata was obtained (or none) and clears the row's progress bar; publication is never blocked by it. Preserve any manual title/description edits the author has made.
122137
123Provide a local mock GitHub provider with two stable fictional users. Show user A first and user B second with controls named `Continue as {display name}`. Do not call GitHub.
138+Extract metadata with this precedence:
124139
125Use a server-verifiable HTTP-only session cookie with an appropriate SameSite policy. Use Secure in production HTTPS while allowing local HTTP development. Login survives reload; logout ends the session. Use stable user IDs, not display names, for ownership.
140+- Title: `<title>` tag → `og:title` → `twitter:title` → first `<h1>` → `og:site_name`
141+- Description: `og:description` → `twitter:description` → `meta[name=description]`
142+- Image: `og:image` → `twitter:image` → `apple-touch-icon` → `mask-icon` → shortcut icon → `itemprop=image` → fetch the destination's `/favicon.ico`; resolve relative image URLs against the final post-redirect URL
126143
127Only an owner can load, save, delete, or restore an account list.
144+The fetcher must be SSRF-safe: HTTP/HTTPS only, no credentials embedded, reject non-public and internal addresses, revalidate DNS on every redirect to prevent rebinding, follow at most five redirects, limit content to 2 MiB, accept only HTML/XHTML, and never forward app credentials. Empty metadata is a successful result.
128145
129The edit page loads persistent data directly and permits list-description, link-membership, order, and per-link title/description changes. Show save progress and errors without losing input.
146+## Aliases and publication
130147
131My Lists requires login and shows only the current user's lists in separate Active and Deleted sections. Active cards show alias, optional description, and link count. Provide Create new list and Restore.
148+An alias is **one segment** of letters, numbers, and hyphens (normalized to lowercase; 1–50 characters). It is globally unique among **active** lists (deleted aliases are immediately reusable, because deletion is permanent). Enforce the same live validation messages as the publish bar on the server.
132149
133## Delete and restore
150+If the alias is blank at publication, generate an available 7-character random alias from lowercase letters and digits.
134151
135Delete requires confirmation and performs a soft delete that retains content and ownership. There is no permanent-delete action.
152+Block publish/save when the list is empty or the alias is invalid or already in use. On failure, keep all user input. There is no additional "permanent" confirmation for publication.
136153
137The public tombstone shows only:
154+## Login and ownership
138155
139> This list was deleted.
156+Show a login modal styled like the reference app: a heading "Sign in to" with the logo, then three full-width colored provider buttons, in this order, each with its provider icon:
140157
141Only the signed-in owner also sees Restore. Restore reactivates the same alias, content, order, and ownership.
158+1. **"with Twitter/X"** (`fab fa-twitter`)
159+2. **"with GitHub"** (`fab fa-github`)
160+3. **"with Google"** (`fab fa-google`)
142161
143## Public list
162+This app is local and must not call any real identity provider. Each button signs in as a stable fictional user stored in SQLite; map each provider button to a distinct mock identity (at least two distinct users total) and render that user's name and avatar in the navbar. Ownership is the (stable user ID, provider) pair, not the display name.
144163
145Use the description as the heading or the alias when blank. Show links in saved order.
164+Use a server-verifiable HTTP-only session cookie with an appropriate SameSite policy; use Secure in production HTTPS while allowing local HTTP development. Login survives reload; logout ends the session. The navbar's signed-in state and all owner-only pages derive from the server session, never from client storage.
146165
147Each link card shows its image/placeholder, title or hostname/URL fallback, optional description, and destination. Open destinations safely in a new context with `noopener` and `noreferrer`.
166+An owner can load, edit, and delete their list. Non-owner requests for an owner's list fail (401 on update/delete). My Lists requires login.
148167
149Provide:
168+## My Lists
150169
151- Links and QR code views with accessible selected state
152- A scannable QR code for the canonical absolute list URL
153- Copy link with announced success/failure
154- Correctly encoded X/Twitter, Facebook, and LinkedIn share links
170+Login required. H2 (large, medium weight, primary color): **"My Lists"**. Then a responsive grid of tiles: 1 column on mobile, 2 on tablet, 3 on desktop, 4 on widescreen.
155171
156For a valid unused alias, show Not found and a Create with this alias action. Prefill the draft without silently replacing existing content. Invalid/reserved paths get a generic not-found state.
172+- First tile: a dashed placeholder tile containing a large **+** and the text **"Create new list"**; clicking it starts a fresh draft and goes to `/s/new`.
173+- Per-list tile: the `bg.png` dot texture in the card; a primary-colored tag reading **"{N} Links"** (e.g. "4 Links"); the vanity URL as the title; the description below it (omitted when empty). The whole tile is clickable and loads that list into the editor state, then goes to `/s/edit`.
174+- While loading: 3 skeleton tiles. If the request fails, show an empty grid without error text.
157175
176+There is **no Deleted section and no Restore action**.
177+
178+## Delete
179+
180+The edit page (`/s/edit`) shows a full-width danger (red) button labeled **"Delete This List"** below the link list. Clicking it opens a danger confirm modal with title **"Delete this list?"** and body **"The url {vanity} will be released for others to use."** (the alias rendered in the danger color). On confirm: permanently delete the list (content, links, and alias), reset the editor state, and navigate to `/s/new`. There is no restore.
181+
182+## Public list
183+
184+`/{vanity}` renders one active list from the server. While loading show a large primary-colored H2 reading **"Loading {vanity}"** followed by 5 skeleton link rows.
185+
186+On success:
187+
188+- Heading: the list's **description** (large, primary color). No fallback needed.
189+- Share row (left, a connected group of icon buttons, all opening in a new tab, URL-encoded):
190+ - X/Twitter: `https://twitter.com/intent/tweet?text={description} https://theurlist.com/{vanity}` — icon `fab fa-twitter`
191+ - Facebook: `https://www.facebook.com/sharer/sharer.php?u=https://theurlist.com/{vanity}` — icon `fab fa-facebook-f`
192+ - LinkedIn: `https://www.linkedin.com/shareArticle?mini=true&summary={description}&url=https://theurlist.com/{vanity}` — icon `fab fa-linkedin-in`
193+- View toggle (right, a connected group of two icon buttons with an accessible selected state): **"View as List"** (`fas fa-list-ul`) and **"View as QR Code"** (`fas fa-qrcode`).
194+- QR view: a centered, scannable QR code SVG of the URL `https://theurlist.com/{vanity}` (4x module scale, colors `#121212` on `#F9FAFC`, error correction medium).
195+- List view: link cards in stored order. Each card is itself a link opening the destination in a new tab (`rel="noopener noreferrer"`), showing the 64px image or placeholder, the bold title (fall back to the destination URL), the description (omitted when empty), and the plain destination URL.
196+- Below the cards, a link **"Report this list"** that opens a mailto to `support@theurlist.com` with subject "LinkBundle Flagged" and the list URL in the body.
197+
198+When the alias does not resolve to an active list, show the not-found state instead: the `urly.png` mascot (500×500, alt "Sad green fuzzy"), H2 **"We couldn't find that Urlist"**, and H3 **"But don't be sad! That means {vanity} is still available."** where the alias is a link. Clicking that alias starts a fresh draft pre-filled with it and goes to `/s/new`. There is no tombstone or "deleted" state.
199+
158200## Theme, responsive UI, and accessibility
159201
160Provide persisted Light, Dark, and System themes. System follows OS preference. Keep accessible contrast and avoid an initial wrong-theme flash when practical.
202+Provide a theme control in the navbar: a hoverable dropdown whose button shows the currently active theme's icon, with items (icon + label, checkmark on the active one): **Light** (`fas fa-sun`), **Dark** (`fas fa-moon`), **System** (`fas fa-desktop`). Persist the choice (e.g. `localStorage["preferredTheme"]`, default `"system"`), apply it via a `data-theme` attribute on `<html>`, and set it with an inline script before first paint to avoid a wrong-theme flash. System follows the OS preference. The dark theme swaps the navbar logo to `logo-dark.svg`.
161203
162Support current Chromium from desktop down to 320 CSS pixels with no page-level horizontal scrolling; modals, drag controls, and the editor must remain usable.
204+Support current Chromium from desktop down to 320 CSS pixels with no page-level horizontal scrolling; the editor, modals, and public page must remain usable.
163205
164Meet WCAG 2.2 AA, including full keyboard operation, reduced-motion support, sensible focus after deletion or reordering, and announced status/error changes.
206+Meet WCAG 2.2 AA, including full keyboard operation, reduced-motion support, sensible focus management, and announced status/error changes.
165207
166208Show truthful loading, empty, success, blocked, and error states. Errors must explain recovery. Never present failure as success.
167209
168210## Storage and security
169211
170Use versioned SQL migrations or an idempotent versioned initializer. Enable SQLite foreign keys. Use transactions for publish, save, delete, restore, and reset.
212+Use versioned SQL migrations or an idempotent versioned initializer. Enable SQLite foreign keys. Use transactions for publish, save, delete, and reset.
171213
172Persist users, lists, ownership/state/timestamps, links, and positions. Published and deleted data must survive a complete restart. Configure the database path by environment variable with a safe local default; do not commit database files.
214+Persist users (mock identities), lists, ownership/timestamps, links, and positions. Published data must survive a complete restart. Configure the database path by environment variable with a safe local default; do not commit database files.
173215
174216Treat user text and fetched metadata as untrusted when rendering, prevent stored/reflected script execution, encode share parameters, apply CSRF protection, and keep internals out of errors.
175217
176Provide a development/test-only deterministic reset, preferably `POST /__test/reset` returning `204`. It clears lists and sessions and restores the two users. Disable or protect it in production.
218+Provide a development/test-only deterministic reset, preferably `POST /__test/reset` returning `204`. It clears lists and sessions and restores the mock identities. Disable or protect it in production.
177219
178220## Scripts, tests, and documentation
179221
@@ -191,8 +233,12 @@
191233db:reset
192234```
193235
194Use Vitest for URL/alias validation, metadata parsing and network policy, ownership, SQLite transactions, and share URLs. Use Playwright for anonymous/account publication, owner/non-owner access, drag-and-drop and keyboard reordering with persisted order, delete/restore/tombstone, and public/QR/share views. Tests use a separate temporary database and must not depend on order.
236+Use Vitest for URL and alias validation, random alias generation, metadata parse precedence (title/description/image including the favicon fallback), ownership, SQLite transactions, and share URL construction.
195237
238+Use Playwright for: the home page first-link flow (valid and invalid → "That doesn't look like a valid URL"); draft persistence across reload and logout; publishing while signed out (disabled "Login to Publish" → login modal with the three provider buttons → publish → landing on `/{vanity}`); live "already in use" and "letters, numbers, and dashes" validation; edit mode (disabled alias, field editing, drag-and-drop reorder); "Delete This List" confirmation ("The url … will be released for others to use.") and the alias being immediately claimable by another user; My Lists (create-new tile, "N Links" tags, skeleton loading, click-through to `/s/edit`); the public page (skeleton "Loading …", description heading, X/Facebook/LinkedIn share URLs, List/QR toggle with a scannable QR of `https://theurlist.com/{vanity}`, "Report this list" mailto); the not-found state ("We couldn't find that Urlist" + click-the-alias pre-filled draft flow); the terms page and the 404 page; the New-draft "Clear this list?" guard; theme switching, persistence, and no flash; and desktop plus 320px mobile layouts of the home, editor, and public pages.
239+
240+Tests use a separate temporary database and must not depend on order.
241+
196242Ship a README covering setup, environment variables, database, commands, mock login, reset, tests, and assumptions.
197243
198244## Completion
@@ -201,8 +247,8 @@
201247
2022481. Run `npm run lint`, `npm run typecheck`, `npm test`, `npm run test:e2e`, and `npm run build`.
2032492. Start the production build and confirm the app responds.
2043. In a real Chromium browser, complete every user journey: anonymous publication; login/logout for both users; owned-list create/edit/drag-reorder/delete/restore; non-owner denial; public links/QR/sharing; themes; and desktop/mobile layouts.
2054. Confirm active, anonymous, and deleted data survives restart.
250+3. In a real Chromium browser, complete every user journey: login/logout for at least two mock identities via the three provider buttons; signed-out → disabled "Login to Publish" → login → publish; owned-list create, drag-reorder, and field editing; non-owner denial (401); hard delete and alias re-claim by a second user; My Lists create/edit flows; public links/QR/share; the not-found pre-filled-alias flow; the terms and 404 pages; the New-draft guard; themes; and desktop/mobile layouts.
251+4. Confirm active data survives a complete restart.
2062525. Reconcile `TECHNICAL_SPEC.md` against this instruction, fix every missing or incorrect requirement, and confirm each checked item has evidence.
2072536. Repeat affected validation and leave no unchecked item unless it has a reported external blocker.
208254