Archilyzer · Source

archilyzer

Archilyzer
git clone https://archilyzer.pages.dev/source/archilyzer.git
Log | Files | Refs | README | LICENSE

commit 73a5797e82b86bb248c6a12bc737da3387a74f14
parent f2123449ace96b7c09b53fb6ab223caad734d472
Author: I Mean I'm Just Saying <imeanimjustsaying@kiwifarms.st>
Date:   Sun, 30 Aug 2026 13:11:09 -0400

plans: slice 5 shipped, and the docs say so

CHANGELOG bullet, "Slice 5, as shipped" in the IA doc (the three operator
decisions, the sixteen findings, what stayed), STATE.md #13 with the new
"recommended next" — the UI slices are done, so what is left is the transcode
band, Phase 6 and slice 9 — and a FACTS section for the seams a later reader
would otherwise re-derive: the has-rule anchoring and the query merge, the
metadata template rule, revalidatePath literal vs "layout", seedsSiteParam's
reason beside the new path rule, and why none of the three co-resident site
titles on /sites collides in Playwright.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

Diffstat:
Meditor/CHANGELOG.md | 1+
Mplans/FACTS.md | 117+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mplans/STATE.md | 32++++++++++++++++++++++++++------
Mplans/editor-operations-ia.md | 112+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++----
4 files changed, 251 insertions(+), 11 deletions(-)

diff --git a/editor/CHANGELOG.md b/editor/CHANGELOG.md @@ -1,6 +1,7 @@ # Changelog ## [Unreleased] +- **A site has tabs, and the family has one page.** Charts, Search aliases, Deploy, Build and Homepage were five sidebar entries beside *Sites*, three of them reading the site from a `?site=` parameter the sidebar picker had to seed, one of them (Build) about no site at all, and one (Homepage) about the family's own hub. A site is one thing now: **`/sites/<id>` is Settings · Charts · Search aliases · Publish**, the site named in the path, the picker following it (and Dashboard and Channels following the picker). **`/sites` is the family page**: the list, then *Release notes*, *Build all sites* with the Basic/Docker mode, the *Hub*, and the *Pool* — the corpus-wide index, stats, sidecar and archive jobs — folded under a disclosure. Search aliases keep both sections on the site's tab: the global dictionary and the site's overrides. Every button, label and log is unchanged; "Select a specific site from the sidebar" is gone because a site's page always has one. The five routes redirect — a `?site=<id>` bookmark lands on that site's tab (the query rides along), `?site=__all__` and the bare routes on `/sites`; a bookmark to a deleted site 404s there exactly as `/sites/<id>` does. The Sites group is one entry; the nav is **eleven**, the IA doc's end state. **Nothing on disk changes.** - **Jobs is one list.** `/jobs`, `/jobs/active` and `/jobs/queue` were three pages over three shapes — the directory listing, the registry's running work with its progress bars, and the scheduler's slots with their stuck reasons — and the same job was drawn three ways or hidden by one page's filter. `/jobs` is one table now: **one row per job**, the live head first (running, then queued in queue order with *next in line* / *2nd in line*, then anything that finished in the last half-minute) and the paged history below it, refreshed as before. A running row carries its per-task progress bars, its ETA and its *Transcripts: n / m* inside the Status cell; a queued row its Promote / ↑ / ↓; a stuck slot its `stuck · reason` badge, the last line of its log and **Force-release**, with **Reap stuck** on the health line above (active queues · running · queued · stuck · workers) and the lane strip above that. The kind/status/search filters apply to every row, live ones included. **The scheduler's drift check rides the live payload now**: every surface that draws it — this page, the dashboard, the monitor widget, `/api/jobs/active` — frees a running slot whose record is finished or gone the moment it sees one, after drawing it once. The channel, video, build and operation pages' *Active jobs* cards come off the same builder, so they show the progress bars they used to drop. `/jobs/active` and `/jobs/queue` redirect; the sidebar's *Active* entry and its running-count badge are gone (*Jobs* still counts running + queued); `/api/jobs/active` never moved. **Nothing on disk changes.** - **Sync is an operation on the board, and the schedule is its page.** The pipelines rail's first row is **Sync** — channels on a cadence, due now, overdue, in the same four state words every other row uses — because a corpus that has stopped noticing new videos is not idle, it is broken, and until now that fact sat in a panel of its own below the rail. Its page is **`/operations/sync`**, which is the schedule that used to be at `/scheduler`: the per-channel cadence table, the bulk retune bar, *Run scheduler now* and the recent ticks, every control unchanged — with **every Sync scheduler setting below it**, the fieldset that used to be on **Settings**, saved by one button. `/scheduler` redirects, so bookmarks land; the API paths and the `pnpm sync:tick` cron client never moved; the sidebar's separate *Schedule* entry is gone, and searching the command palette for "schedule" or "cadence" finds Operations. The two **Storage chores** that ride the same heartbeat — the keep-latest deletion check and the saved-video backup — are named as such on the console, in a tick's own summary line and on **Saved videos**: they are not sync, they just share the one timer the editor has. **Transcription workers are configured on Workers now**, directly under the live list, with their own *Save workers* — so seeing a worker and changing it are no longer two pages. **Settings** keeps the machine and the site, and points at both. The reason a lane is not working is now one table rather than two. **Nothing on disk changes; no setting is renamed.** - **A video's page shows one panel per operation, not just the digest's.** Speaker diarization and both *Speaker names* operations wrote records that no per-video screen could show — and for diarization that record is often the only surviving evidence of audio the cleanup sweep has since deleted. Each operation now has its own panel: what produced its record (engine, models, threshold, which transcript the names were read from, how many chunks survived), and the **same state word the channel's own counts use** — *current*, *stale*, *partly stale*, *not generated*, *held — …* with the reason spelled out, or *waiting on …* naming what has to happen first. Each speaker panel carries a **Run** button that runs **that one video** on the speaker lane, in front of the sweep's work; the channel's Speakers stage still runs the whole channel. A record whose feature has since been switched off is still shown, marked *switched off*, with no Run button — hiding it is how a record that cost audio nobody has any more becomes invisible. Both *Speaker names* operations read the one `attribution.json` they share, so their panels differ in the state word and not in the record; the `method` line says which lane wrote it. The digest panel is unchanged apart from its heading, which is now **Digest** rather than *AI digest* — the name the rest of the editor already uses for it — and the page no longer works out the digest's freshness a second way of its own, so a video's panel and its channel's count cannot disagree. A re-run of a per-video run from **Jobs** stays per-video. **Nothing on disk changes.** diff --git a/plans/FACTS.md b/plans/FACTS.md @@ -2985,3 +2985,120 @@ is the one list; `components/ReapStuckButton.tsx` is the queue strip's action. D while reading only `status` and `kind`. The channel page no longer holds `JobRecord`s, so the input takes the SHAPE (`ReadonlyArray<{ status: string; kind: string }>`) instead. Its other caller (`videos/page.tsx` → `videoRowsServer.ts`) still passes records and is untouched. + +## Verified 2026-08-30 — editor IA slice 5 seams (a site has tabs; the family has one page) + +Read-only at `2a8081e`, implemented at `140212a` → `df9b839`. Trust these over re-deriving. + +**What the five pages actually were** (the IA bullet's premise, "all per-site facts already; +only their routes say otherwise", is wrong for two of them): + +| page | scope | reads | writes | `?site=` | +|---|---|---|---|---| +| `/charts` | **per-site** templates; preview DATA = last-composed site | `readTemplates(paths, siteId)` → `sites/<id>/chart-templates.json`; `useStats`/`useSummaries` fetch `/stats/*` = `export/public/stats` = whichever site `compose-site` ran last | `saveDefaultDashboardAction`, no revalidate | yes, REQUIRED one site | +| `/aliases` | **global + per-site overlay** | `readGlobalAliases` → `transcripts/search-aliases.json`; `readSiteAliases` → `sites/<id>/search-aliases.json`; merged at compose, per-site id SHADOWS global (`aliasesStore.ts:68-73`) | `saveGlobalAliasesAction`, `saveSiteAliasesAction`, no revalidate | optional | +| `/deploy` | **per-site target + global chrome** | `listSites`; `export/CHANGELOG.md`; `export/out/index.html` mtime (`resolveOutDir` ignores siteId); `settings.buildPipeline.mode` | `cutReleaseAction`, `setBuildModeAction`, `deployExportAction(siteId)` + four from `buildAction` | optional | +| `/build` | **corpus-wide, no site notion at all** | `activeQueueNames()`, `liveJobRows(BUILD_KINDS)` — no `searchParams`, no `listSites` | eleven actions in `buildAction.ts` | **never** | +| `/homepage` | **global — the hub's identity** | `getHomepageConfig` → `sites/_homepage/homepage.json` | `saveHomepageConfigAction` | **never**; page was sync | + +**The seven import edges, and where each went.** `build/` and `deploy/` were already one module +in two directories: `build/buildAction.ts:34` imported `deploy/buildDeployCore`, and four +`deploy/components/*` imported `build/buildAction`. Now: server modules in +`editor/app/sites/lib/` — `buildAction.ts`, `deployAction.ts`, `cutReleaseAction.ts`, +`buildModeAction.ts`, `homepageActions.ts`, `chartsActions.ts`, `aliasesActions.ts` (all +`"use server"`) plus `buildDeployCore.ts` (deliberately NOT, its header says why). Components +FLAT in `editor/app/sites/components/`, so every moved file has the same import shape: +`../lib/x`, `../../components/QueueControl`, `../../jobs/actions` — the last two byte-identical +to what they were from `deploy/components/`. `changelog/page.tsx:12` is the one outside +repoint (`CutReleaseForm`, `workspace="editor"`, driven by `cut-release.spec.ts`). +`SiteTabs.tsx` lives under `sites/[siteId]/components/` (the `ChannelTabs` precedent). Neither +`sites/lib` nor `sites/components` has a `page.tsx`, so neither is routable and neither can +shadow a site id — and **no static sibling of `[siteId]` was added**, because `/sites/publish` +would shadow a site called `publish` (`SITE_ID_RE = /^[a-z0-9][a-z0-9-]*$/`). + +**`has`-rule anchoring and the query merge (Next 16.2.3).** `matchHas` compiles the value as +`new RegExp(`^${hasItem.value}$`)` +(`next/dist/shared/lib/router/utils/prepare-destination.js:101`) — ANCHORED, so +`?site=__all__` fails `[a-z0-9][a-z0-9-]*` and falls through to the bare rule. The named group +becomes a destination param (validator: `next/dist/lib/load-custom-routes.js:210-263`). The +initial query is MERGED into the destination (`prepare-destination.js:278-284`) and stringified +into `Location` (`next/dist/server/lib/router-utils/resolve-routes.js:509-517`), so +**`/charts?site=testsite` lands on `/sites/testsite/charts?site=testsite`** — the query is not +stripped. Any spec regex on a captured redirect must therefore end `(\?|$)`. Redirects are +checked BEFORE the filesystem, so a redirect for a route that still has a page hides the page: +each commit added only the redirects for the pages it deleted. + +**The metadata template rule.** A layout's `title.template` applies to CHILD segments and NOT +to a `title` in the same segment's `page.js` +(`node_modules/next/dist/docs/.../generate-metadata.md:284-287`). So +`sites/[siteId]/layout.tsx` exports +`{ default: `${siteId} — Sites`, template: `%s — ${siteId} — Sites` }`: the index page exports +no title at all and gets `default` (today's string, unchanged), and each tab exports a bare +static `title` ("Charts", "Search aliases", "Publish") and gets templated. + +**`revalidatePath` literal vs `"layout"`.** A literal path invalidates only that page +(`revalidatePath.md:38`, `:151` "will not invalidate pages beneath"); `type: "layout"` +invalidates the layout, nested layouts and every page beneath (`:39`, `:155-162`). +`saveSiteAction`'s `revalidatePath(`/sites/${siteId}`)` therefore did NOT cover the tabs by the +letter — it worked only through the "temporary" server-function behaviour that refreshes every +visited page. It is `revalidatePath("/sites/[siteId]", "layout")` now (the h1 above every tab +reads `siteTitle`, which that form edits). `buildStatsAction`'s `/charts` became +`revalidatePath("/sites/[siteId]/charts", "page")` (precedent: `channels/actions.ts:312`). +`revalidatePath("/sites")` now has **eight** callers (was five). + +**`seedsSiteParam` and the path rule beside it.** `SiteScopeSelect.tsx`'s +`seedsSiteParam = !pathname.startsWith("/sites")` is UNCHANGED and its reason still holds: the +mount effect's `replace()` clobbered an in-flight push to `/sites/<id>` (a link click on the +list), bouncing the user back to `/sites?site=<id>`. Beside it now: `siteIdFromPathname()` in +`app/lib/activeSite.ts` — a pure `SITE_PATH_RE = /^\/sites\/([a-z0-9][a-z0-9-]*)(?:\/([a-z-]+))?\/?$/` +that rejects `new` (a static route that beats `[siteId]`) and anything deeper than a tab. The +id pattern is `SITE_ID_RE` **re-spelled**, because `common/lib/site.ts` imports `node:fs` and +`activeSite.ts` must stay importable from the client picker. On a site path the picker's value +IS the path segment, the reconcile effect writes it to localStorage (so Dashboard and Channels +follow), `onChange` does `router.push("/sites/<other>/<segment>")` and "All sites" pushes +`/sites`. + +**`BUILD_KINDS` is wrong in both directions** and was moved verbatim, with a comment saying so: +six kinds (`build-index`, `build-stats`, `build-export`, `normalize-transcripts`, +`archive-transcripts`, `archive-combined-transcripts`) — MISSING the three live-chat kinds the +Pool's own buttons enqueue, and INCLUDING `build-export`, which is enqueued from a site's +Publish tab and never from here. Not fixed by slice 5. + +**`_homepage` can never be a `[siteId]`.** `HomepageConfig` lives at +`sites/_homepage/homepage.json` — inside `sitesDir`, with a leading underscore that +`isValidSiteId` rejects. That is why the Hub is a section of `/sites` and not a tab. + +**The hub's form is not what deploys the hub.** The `homepage/` package's own `deploy` script +hardcodes `--project-name archilyzer`, so the form's `cloudflareProject` field is NOT what +`pnpm deploy:homepage` uses, and no editor action, job or button builds or deploys the hub. +Pre-existing; slice 5 does not touch the script. Adjacent globals that are NOT the same field: +`HomepageConfig.siteUrl` (the Hub section) vs `SiteSettings.homepageUrl` ("Family hub URL" on +`/settings`) vs per-site `Site.hubUrl` (`SiteForm.tsx`). + +**`idBaseUrl` is absolute**, so moving the charts client changed nothing about its data: +`useStats`/`useSummaries` fetch `${idBaseUrl(origin)}/stats/…` +(`common/components/statsCache.ts:28,44`), served through `next.config.ts`'s +`/stats|/summaries|/transcripts → /exported/…` rewrites. The preview therefore still draws +whichever site `compose-site` ran last, which the Charts tab's intro now says out loud. + +**`getSite` throws on a malformed id** (`common/lib/site.ts:356`, sync), which is why the tab +pages do not call it directly: `sites/[siteId]/lib/siteCache.ts` wraps +`isValidSiteId(...) && listSiteIds(...).includes(...)` and returns `Site | null`, React +`cache()`d so the layout and the tab page share ONE read per request. The layout's `notFound()` +is the one 404 for `/sites/<missing>` and every tab under it; each tab page guards through the +same read (the channel page does likewise). There is no `loading.tsx` anywhere under `sites/` +— in this app one turns `notFound()` 404s into 200s. + +**e2e labels newly co-resident on `/sites`, and why none collides.** A site's title now appears +THREE times on that page: the picker `<option>`, the list `<Link>`, and `BuildSitesPanel`'s +visible `<span>{s.siteTitle}</span>` beside its `aria-label="Select <title>"` checkbox. +`sites-crud.spec.ts` survives because it already scopes by +`getByRole("link", { name: /Alpha Site/ })`, and its `getByText("Doomed")).toHaveCount(0)` runs +AFTER the delete, when all three are gone. Its `getByRole("button", { name: /delete/i })` stays +unique: none of the ~15 buttons added to `/sites` contains "delete". +`getByRole("button", { name: "Build index" })` is a SUBSTRING match and still unique there +("Build all sites", "Build selected", "Build stats dataset" do not contain it), and +`getByLabel("Build index output")` is route-independent. **`getByRole` ignores what a closed +`<details>` hides**, so anything inside the Pool disclosure or "Individual steps" needs the +click first — which is why `e2e/helpers.ts` grew `buildIndex(page)` (goto `/sites`, click +"Pool jobs", press, assert "Done") and now imports `expect` as a value. diff --git a/plans/STATE.md b/plans/STATE.md @@ -3,7 +3,21 @@ The working memory for the local-AI derived-corpus work. Rewritten at the end of every session, before context is cleared. See [`README.md`](README.md) for the protocol. -**Last updated:** 2026-08-30 — **editor IA slice 8c shipped** (`2226680` → `21feed3`): +**Last updated:** 2026-08-30 — **editor IA slice 5 shipped** (`140212a` → `df9b839`): +**a site has tabs, and the family has one page.** Charts, Search aliases, Deploy, Build and +Homepage were five sidebar entries beside *Sites*, three reading the site from a `?site=` +param the picker had to seed, one about no site at all, one about the family's own hub. +`/sites/<id>` is now **Settings · Charts · Search aliases · Publish** — a layout, the site +named in the PATH, and the picker reading that path through a new pure `siteIdFromPathname()` +so the two can never disagree (it writes it to storage, so Dashboard and Channels follow). +`/sites` is the family page: the list, *Release notes*, *Build all sites* with the +Basic/Docker mode, the *Hub*, and the *Pool* — the corpus-wide jobs — under a disclosure. +The five routes 307, and a `?site=<id>` bookmark rides a `has` rule to that site's tab. +`build/` and `deploy/` — already one module in two directories — are `sites/lib` + +`sites/components`. The nav is **eleven**, the IA doc's end state, held by `nav.test.ts`; the +IA doc's UI slices are complete. **Nothing on disk changes.** See "Slice 5, as shipped" in +`editor-operations-ia.md`. +Previously: 2026-08-30 — **editor IA slice 8c shipped** (`2226680` → `21feed3`): **`/jobs` is one list, and slice 8 is complete.** The design question #12 asked first — what is a ROW — is answered *a row is one job*, whichever of three places knows about it: a registry record, a `.log` + sidecar the registry has forgotten, or a scheduler slot whose record was @@ -304,11 +318,17 @@ nothing renders. IA slice 8c seams". The design question was answered **"a row is one job"** — and because it was, there is no mode: one list, live head and paged tail, one `<tr>` per job. Slice 8 is complete. - -**Recommended next (editor IA).** The remaining candidates are **slice 5** (Sites — Charts, -Search aliases, Deploy, Build and Homepage fold into `/sites/[siteId]`), the **transcode band** -(`EXTERNAL_BAND_IDS` to three, which needs the snapshot writer to record a transcode -population), and **Phase 6**. +13. ~~**Editor IA slice 5** (Sites absorb the five)~~ — **DONE 2026-08-30**, `140212a` → + `df9b839`. Plan: [`editor-ia-slice-5.md`](editor-ia-slice-5.md); outcome: "Slice 5, as + shipped" in `editor-operations-ia.md` and the FACTS section "Verified 2026-08-30 — editor + IA slice 5 seams". A site has tabs and the family page has the globals; the five satellite + routes 307. **The nav is eleven** — the IA doc's end state — and the IA doc's UI slices are + complete. + +**Recommended next (editor IA).** The UI slices are done; what is left is not UI. The +candidates are the **transcode band** (`EXTERNAL_BAND_IDS` to three, which needs the snapshot +writer to record a transcode population per channel — a snapshot-shape change, so it is its +own plan), **Phase 6**, and **slice 9**, which is gated on the sweep running in production. --- diff --git a/plans/editor-operations-ia.md b/plans/editor-operations-ia.md @@ -69,7 +69,7 @@ Four groups, eleven top-level entries, down from three groups and nineteen: - **Corpus** — Dashboard, Channels, Review - **Operations** — Operations *(Schedule folded in, 2026-08-29: it is `/operations/sync`)* -- **Sites** — Sites *(+ Charts, Search aliases, Deploy, Build, Homepage until slice 5)* +- **Sites** — Sites *(Charts, Search aliases, Deploy, Build and Homepage folded in, 2026-08-30)* - **Machine** — Jobs *(Active and Queue folded in, 2026-08-30)*, Workers, Cleanup, Saved videos, Settings, Changelog **The rule, and it is umtool's rule, not a new one.** `umtool/components/AppNav.tsx` states @@ -144,10 +144,15 @@ dependencies allow. Sizes are S/M/L. it. e2e: `actionable.spec.ts`, `cleanup-actionable.spec.ts`, `site-scope.spec.ts:101`, `backfill.spec.ts:769`, `attribution.spec.ts:358`, `navigation.spec.ts:38`. **M–L, medium risk** — dashboard numbers move, and the changelog must say so. -5. **Sites absorb charts / aliases / deploy / build / homepage.** `/sites/[siteId]/{charts, - aliases,publish}`; `?site=` stays for Dashboard and Channels. Deletes five pages. e2e: - `build.spec.ts`, `deploy-page.spec.ts`, `site-scope.spec.ts:63-87`, `aliases.spec.ts`, - `channel-build-toggle.spec.ts:17`. **M.** +5. **Sites absorb charts / aliases / deploy / build / homepage — SHIPPED** (`140212a` → + `df9b839`; see "Slice 5, as shipped" below). The bullet's premise — "all per-site facts + already; only their routes say otherwise" — was wrong for two of five: `/build` has no site + notion at all, and `/homepage` edits `sites/_homepage/homepage.json`, which `isValidSiteId` + rejects, so the hub can never be a `[siteId]`. `/deploy` was half and half. What shipped: + the per-site halves are TABS (`/sites/[siteId]/{charts,aliases,publish}`), the family's + halves — Release notes, the batch build with its mode, the Hub and the Pool — are sections + of `/sites`. `?site=` stays for Dashboard and Channels; on a site's own pages the PATH is + the selection and the picker follows it. Deletes five pages. **M.** 6. **The video page: one panel per operation — SHIPPED** (`6986efc` → `d5ca90a`; see "Slice 6, as shipped" below). `DigestPanel.tsx` is the digest's BODY inside a generic `OperationPanel`, built per registry entry from `inspectVideoOperations`; attribution and @@ -680,3 +685,100 @@ The widget's own `JobRow`. `PulsePayload.runningJobs`. `/api/jobs/active`. `STUC constant, not a setting. The reorder spec's effect assertion. The `gap-0.5` climb from a task bar to its elapsed timer — the bars are a shared component, and the table's Status stack is `gap-1` so the nearest `gap-0.5` ancestor is still the task's own wrapper. + +## Slice 5, as shipped + +Four commits after the plan ([`editor-ia-slice-5.md`](editor-ia-slice-5.md), `5660fae`): +`140212a` (a site has tabs; Charts and Search aliases are two of them) → `ffd5ac6` (Publish is +the site's tab; the family page cuts the release and batches the builds) → `a756049` (the pool +runs from the family page) → `df9b839` (the hub is the family's, and the Sites group is one +entry) → this docs commit. This is the LAST UI-only slice and the one that reached the nav's +end state: **eleven top-level entries, four groups**, with `nav.test.ts` holding it there. + +**Three operator decisions, taken before the plan was written:** + +- **(A) The globals live on `/sites`, the family page** — not on a site, and not on a new + static sibling of `[siteId]` (which would shadow a site with that id). `/sites` is the list + PLUS Release notes, Build all sites with the Basic/Docker mode, the Hub and the Pool. It was + needed because two of the five pages have no site at all and a third is half the family's: + a literal "fold them into `[siteId]`" had nowhere to put them. +- **(B) The Search aliases tab renders BOTH sections** — h2 *Global* and h2 *This site — <id>*, + exactly as `EditorAliasesClient` already drew them. Needed because aliases are a global + dictionary with a per-site overlay (a per-site id SHADOWS the global one at compose time), so + a tab showing only the overlay would hide the thing it overlays. +- **The nav end state is one Sites entry, eleven total**, and the five routes 307 + (`permanent: false`, the "TEMPORARY" rule in `next.config.ts`). Needed because the redirects + are the second half of umtool's fold rule, and a permanent redirect on a self-hosted admin + surface is a support call with no remedy. + +**Sixteen places the slice-5 bullet could not be implemented literally, and what happened +instead.** The census is in FACTS.md ("Verified 2026-08-30 — editor IA slice 5 seams"). + +1. **"They are all per-site facts already" is wrong for two of five.** `/build` had no + `searchParams`, no `listSites`, eleven corpus-wide actions; `/homepage` edits + `sites/_homepage/`. `/deploy` was half and half. → decision (A). +2. **Aliases are a dictionary with an overlay.** → decision (B). +3. **The picker and the path disagreed on `/sites/<id>/…`.** `SiteScopeSelect`'s value was + `resolveActiveSite(searchParams.site)`, so on a site's page it showed whatever was stored + last. → a pure `siteIdFromPathname()` in `lib/activeSite.ts` (six node:test cases): on a + site path the picker's value IS the path segment, the reconcile effect writes it to + localStorage so Dashboard and Channels follow, `onChange` pushes `/sites/<other>/<segment>` + and "All sites" pushes `/sites`. `seedsSiteParam` and its reason stay untouched. +4. **`?site=` is routed at config level and the query SURVIVES the redirect.** A `has` rule + whose value is `(?<site>[a-z0-9][a-z0-9-]*)` — Next anchors it, so `?site=__all__` misses + and falls to the bare rule — with `:site` in the destination. The matched query is merged + into the destination, so `/charts?site=a` lands on `/sites/a/charts?site=a`: harmless (the + tab reads `params.siteId`), but every spec regex on a captured redirect is `(\?|$)`. +5. **Layout mechanics.** Layout `params` is a Promise; layouts do not rerender and cannot read + the pathname, so the active tab is a client component (`SiteTabs`, the `ChannelTabs` + precedent) and `useSelectedLayoutSegment()` is `null` on the index page = the Settings tab. + A layout's `title.template` applies to CHILD segments only, so the layout exports + `{ default: "<id> — Sites", template: "%s — <id> — Sites" }`, the index page exports no + title, and each tab exports a bare one. +6. **`revalidatePath` literal vs `"layout"`.** `saveSiteAction`'s `revalidatePath("/sites/<id>")` + does not cover the tabs by the letter — it worked only through the "temporary" behaviour + that refreshes every visited page. It is `revalidatePath("/sites/[siteId]", "layout")` now; + the stats build's `/charts` became `revalidatePath("/sites/[siteId]/charts", "page")`, and + the cut-release, build-mode and homepage actions revalidate `/sites`. +7. **The all-sites disabled state cannot exist on a site tab.** `BuildDeployButton`, + `BuildExportButton` and `DeployButton` take `siteId: string` / `siteTitle: string`; the + three "Select a specific site from the sidebar…" notices and both `disabled={!siteId}` are + gone. The missing-Cloudflare-project branch is untouched — that is what the button says when + it cannot deploy, and "the site's page" is now literally the Settings tab beside it. +8. **The Pool is eight job consoles.** `RunningJobsList` renders ABOVE the disclosure (a + running pool job must be visible without a click); the consoles sit under + `<details><summary>Pool jobs`. `getByRole` ignores what a closed disclosure hides, so one + `buildIndex(page)` helper in `e2e/helpers.ts` opens it, presses and asserts "Done" — + replacing three inline six-line copies across nine call sites. +9. **`build/` and `deploy/` were already one module in two directories.** Server modules are + `editor/app/sites/lib/` (seven `"use server"` files plus `buildDeployCore.ts`, which is + deliberately NOT one); components are FLAT in `editor/app/sites/components/`, because the + repo's component dirs are flat and every moved file then has the same import shape. + `SiteTabs.tsx` is under `sites/[siteId]/components/`. Neither `sites/lib` nor + `sites/components` has a `page.tsx`, so neither is routable and neither can shadow a site id. +10. **The hub's form is not what deploys the hub** — the `homepage` package's own deploy script + hardcodes `--project-name archilyzer`. Pre-existing; the section keeps today's copy. +11. **Charts preview data is whatever site `compose-site` ran last.** One sentence says so in + the Charts tab's intro. Out of scope to fix. +12. **`navigation.spec.ts`'s redirect test had no site**, so a captured redirect would 404. It + writes `testsite` before the capture case. +13. **`/sites` became async and reads six things** — all file/registry reads the two retired + pages already did per request; it was `force-dynamic` already. +14. **`BUILD_KINDS` is wrong in both directions** — six kinds, missing the three live-chat + kinds the buttons enqueue and including `build-export`, which none does. Moved verbatim + with a comment saying exactly that; **not fixed**. +15. **`SettingsForm`'s "The mode can also be toggled on the Deploy page"** points at Sites. +16. **A stale `?site=<deleted>` bookmark 404s at `/sites/<id>/charts`** exactly as `/sites/<id>` + does, where `/charts?site=bogus` used to fall back to the lone/all site. The picker never + seeds an invalid id, so only a hand-typed URL reaches this. Accepted; in the CHANGELOG. + +**What stayed.** `seedsSiteParam` and its reason (seeding on `/sites*` clobbered an in-flight +push to `/sites/<id>`). `BUILD_KINDS`' mismatch (14). `EditorAliasesClient`'s +`siteId: string | null` and its now-unreachable global-only branch — a `git mv` plus one import +line is the whole diff, and narrowing it was not this slice. The charts preview's +last-composed data (11). The hub deploy script's hardcoded project name (10). +`RunningJobsList` as the Pool's renderer. `/api/*` — nothing there belonged to any of the five, +and the charts preview's `/stats` rewrite is absolute and unaffected. `deleteSiteAction` still +`rm -rf`s the whole site dir including `chart-templates.json` and `search-aliases.json`; the +tabs only make that visible. No `loading.tsx` anywhere under `sites/` — the layout's +`notFound()` is the guard, and each tab page guards through the same `cache()`d read.