commit 94e6624131b351dd655bb839791ae193ecc2336e
parent da0e96e4f88f837eaa5404b16402985d368f1702
Author: I Mean I'm Just Saying <imeanimjustsaying@kiwifarms.st>
Date: Wed, 30 Sep 2026 01:06:29 -0400
plans: slice SS after the merge of main — L5 to its commits, the new-channel pre-check, the re-gates
release-15.md (SS section): the new-channel bullet, the ?site= table's two
rows, the commit table (0321d240 to da0e96e4), the new e2e case with its
pre-L5 run, the gates after the merge (tsc, common 2,301, editor unit 95, the
L5 spec list 56/0, the capped build), two decision rows, L5 in the Review
table, and the two "Found and left" entries L5 closed. FACTS: the new-channel
pre-check. Changelog: New channel starts with the picked site ticked.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Diffstat:
3 files changed, 37 insertions(+), 21 deletions(-)
diff --git a/editor/CHANGELOG.md b/editor/CHANGELOG.md
@@ -14,7 +14,7 @@
- **Two grounds, Light and Dark, and no accent picker in the header.** The editor's header keeps its theme toggle, which cycles System, Light and Dark; the theme menu (Base and Accent) is gone, and the editor wears its own accent, Signal. A stored choice of the retired third ground loads as Light and is rewritten once; a stored accent is not read and is left in storage. A site's accent is still set in its form; the form's hint no longer says a reader can pick another.
- **The hub URL hints say what the setting does now.** Settings' **Family hub URL** and a site's **Hub URL** no longer promise a Hub link in the header (it was removed): the value is published as `hubUrl` in each site's `/site.json` and `/corpus.json`, so the hub can tell its member sites. `SETTINGS.md` and `SITE.md` say the same.
- **A site can be left off the homepage and the hub.** A site's settings have a new checkbox, **List on the Archilyzer homepage and hub**, on by default (`listed` in `site.json`; only `false` is written). Turned off, the site still builds and deploys at its own URL as before, but the homepage has no card, chart series, `/stats` entry or recent item for it; the hub does not list it as a member, search it, or name it in its `corpus.json` and `llms.txt`; no other site's footer links it; and `channel-sites.json` and the homepage's `stats/` leave it out. A channel only unlisted sites carry is in none of the published totals, the homepage's headline numbers included; a channel a listed site also carries is counted under the listed site. The editor's own pages still show every site. It takes effect at the next homepage, hub and site builds.
-- **The sidebar's site picker shows your site from the first paint.** It used to show "All sites" on every page and then jump to the site you had picked, and Dashboard and Channels came up in your site only after a `?site=` had been added to the address. The picked site is now kept in a cookie that the editor reads before it draws a page, so the picker, Dashboard and Channels open in it at once, and the address is left alone. A link that carries `?site=<id>` still opens that page in that site, without changing the one you picked; picking a site on such a page drops the `?site=` from the address. On a site's own pages (Charts, Publish, …) the picker still follows the page, and opening one still makes that site the picked one. The first time you open the editor after updating, a site picked before is moved into the cookie; the picker may show "All sites" for a moment that once. A site picked in one tab reaches the editor's other open tabs without a reload. Each editor keeps its own pick, as before, when several run on one machine on different ports.
+- **The sidebar's site picker shows your site from the first paint.** It used to show "All sites" on every page and then jump to the site you had picked, and Dashboard and Channels came up in your site only after a `?site=` had been added to the address. The picked site is now kept in a cookie that the editor reads before it draws a page, so the picker, Dashboard and Channels open in it at once, and the address is left alone. A link that carries `?site=<id>` still opens that page in that site, without changing the one you picked; picking a site on such a page drops the `?site=` from the address. On a site's own pages (Charts, Publish, …) the picker still follows the page, and opening one still makes that site the picked one. The first time you open the editor after updating, a site picked before is moved into the cookie; the picker may show "All sites" for a moment that once. A site picked in one tab reaches the editor's other open tabs without a reload. **New channel** starts with the picked site ticked under its sites (or the site of a `?site=` link), including when it is opened from the editor's own links. Each editor keeps its own pick, as before, when several run on one machine on different ports.
## [0.10.0] - 2026-09-28
- **The homepage can be built and deployed from `/sites`.** Under a new **Homepage** section, after Hub, there is **Build homepage** (tick **Deploy after build** to ship it in the same job, only if the build succeeds) and **Deploy homepage**, which ships the build already in `homepage/out`. A **Preview branch** box beside them sends either deploy to a Cloudflare Pages preview of the `archilyzer` project instead of production, and shows the preview's address as you type; a name Cloudflare would refuse or rewrite, or `main`, greys the deploy buttons out and says why. A line under the buttons says what a deploy would ship: when `homepage/out` was built (or that it holds no build yet), and where it goes, with the live URL. Deploy homepage with nothing built is refused before any job starts. The homepage reads the search index as it stands, so run **Build index** first when its numbers should move. The jobs run the same code as `archilyzer build homepage` / `deploy homepage`, and show on `/jobs` as `build-homepage`, `deploy-homepage` and `build-deploy-homepage`. The Hub section no longer describes the homepage.
diff --git a/plans/FACTS.md b/plans/FACTS.md
@@ -3253,8 +3253,11 @@ follow), `onChange` does `router.push("/sites/<other>/<segment>")` and "All site
moved reads the new value. In both, the choice is held for the URL it was made on, so the
controlled select does not snap back while the navigation is in flight, and dropped on the first
render at another URL (else Back to that URL showed the old choice over the path).
-- **Left:** `ChannelFormClient` (`/channels/new`) still pre-checks the site from `?site=` only
- (it reads `window.location.search` on mount); no editor link to `/channels/new` carries one.
+- **A new channel starts checked on the active site** (review L5): `ChannelFormClient` resolves
+ `resolveActiveSiteFrom([?site=, useSiteScope().stored], siteIds)` on its first render, and
+ `SiteMembershipsSection` takes it into its INITIAL state (there was an effect), so the box is
+ checked in the server's HTML and a later refresh cannot re-check a box the user cleared. With
+ one site configured and nothing stored, that site starts checked.
**`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`,
diff --git a/plans/release-15.md b/plans/release-15.md
@@ -901,6 +901,17 @@ cannot read localStorage) rendered unscoped until the param arrived.
pages until the push landed. The hold is dropped on the first render at another URL
(`352ea8f7`); before that, Back to the page it was made on showed the old choice over the
path.
+- **A new channel starts checked on the active site** (`5006c28f`, review L5, after DS merged).
+ - `ChannelFormClient` resolves it as the picker does off a site's pages:
+ `resolveActiveSiteFrom([?site=, useSiteScope().stored], siteIds)`, on its first render. It
+ used to read only `window.location.search`, in a mount effect, so `/channels/new` reached from
+ the editor's own links never pre-checked a site.
+ - `SiteMembershipsSection` takes that site into its initial state instead of an effect. The
+ server renders the box checked, so there is no unchecked first paint. A later refresh (from
+ the pulse, or a pick in another tab) cannot re-check a box the user has cleared; the effect
+ re-ran whenever `sites` came back as a new array.
+ - With one site configured and nothing stored, the active site is that site, so it starts
+ checked (see the decisions table).
**`?site=` after this slice.** The editor's own navigation no longer appends it; nothing in
`editor/app` builds a `?site=` link. What still reads or carries one:
@@ -910,8 +921,8 @@ cannot read localStorage) rendered unscoped until the param arrived.
| Dashboard, Channels (`readActiveSite(site)`) | A valid one governs that request; it is not stored |
| The picker | Shows a valid one on that page; a choice there drops it |
| `next.config.ts` redirects | A retired `/charts`, `/aliases` or `/deploy` bookmark carrying `?site=<id>` lands on that site's tab (the comment now says the picker ignores the query there, `e684668c`) |
-| `ChannelFormClient` (`/channels/new`) | Pre-checks that site's membership box; read from `window.location.search` on mount. It is the only source of that pre-check, as before (see "Found and left") |
-| `ChannelVolumeBar`'s chips | Keep whatever `?site=` the URL has when they add `?location=` |
+| `ChannelFormClient` (`/channels/new`) | A valid one is the site a new channel starts checked on, instead of the stored one (`5006c28f`) |
+| `ChannelVolumeBar`'s chips | Keep whatever `?site=` the URL has when they add `?location=`; the comment now says the stored scope is a cookie (`5006c28f`) |
| e2e (`channel-groups`, `channel-priority`, `channels-rack-*`, `channel-site-membership`, `site-scope`, `navigation`) | Deep links; all still work |
**Commits**
@@ -927,7 +938,12 @@ cannot read localStorage) rendered unscoped until the param arrived.
| `f645572c` | `editor:` review M1: a successful write is broadcast, and the other tabs refresh. |
| `09559337` | `editor(e2e):` review M1 and L2: the two-tab case; `expectNoSiteParam()` after the settle waits. |
| `8104732b` | `editor:` review L1: the layout's comment says "every page". |
-| this commit | `plans:` the review, its findings to their commits, L3, L4 and L6 under "Found and left", the rulings; FACTS (L1, other tabs); the changelog's other-tabs sentence. |
+| `001c23d0` | `plans:` the review, its findings to their commits, L3, L4 and L6 under "Found and left", the rulings; FACTS (L1, other tabs); the changelog's other-tabs sentence. |
+| `4b61be87` | `plans:` two lines of this section reflowed. |
+| `07d9126e` | Merge `main` `ef4f1d7c` (DS and the rest). The one conflict, this file, kept `main`'s sections whole (IG, UT, DS) with SS's after them and its row after UT's; FACTS and the changelog merged cleanly. |
+| `5006c28f` | `editor:` review L5: a new channel starts checked on the active site, from the provider, as initial state; the stale `?site=` comments in `ChannelFormClient`, `ChannelForm`, `SiteMembershipsSection` and `ChannelVolumeBar`. |
+| `677d1668` | `editor(e2e):` the new-channel case. |
+| this commit | `plans:` L5 and the merge in this section; FACTS; the report. |
**Tests**
@@ -953,6 +969,7 @@ cannot read localStorage) rendered unscoped until the param arrived.
| a `?site=` link scopes its own page and is not stored; a choice there drops it | `/channels?site=beta` paints beta and lists beta's channel; the cookie stays alpha, and the next `/channels` is alpha; choosing "All sites" on `/channels?site=beta` leaves `/channels` with both channels and the cookie `__all__` | not run (the cookie assertions cannot hold) |
| a choice the old picker kept in localStorage moves to the cookie once | the key seeded from a route that mounts no app; `/channels` then shows beta scoped, the cookie is beta and the key is gone; the next `/` paints beta; a leftover key with a cookie is removed and never read | not run |
| Back to a site's page shows that site, not the choice made there | on `/sites/alpha/charts`, choosing beta goes to `/sites/beta/charts`; Back shows alpha | not run on the pre-change code; with `0465d37b`'s picker (the fix line removed) it received `"beta"` |
+| a new channel starts checked on the stored site, or on a `?site=` link's (`677d1668`) | alpha stored: `/channels/new` has Alpha checked and Beta not, read with no retry after `domcontentloaded`; `/channels/new?site=beta` has Beta checked and not Alpha as well; with Alpha cleared by hand, a pick of beta in another tab refreshes the page and checks neither | on the pre-L5 form (`07d9126e`'s `ChannelFormClient` and `SiteMembershipsSection`): fails at the first read, `stored site, first paint` expected `true`, received `false` |
| a site picked in another tab reaches this one (`09559337`) | two pages in one context, passive refresh off (`autoRefreshIntervalSeconds: 0`), so only the broadcast can move tab A. Tab A stores alpha and sits on `/settings`; tab B picks beta; tab A's picker becomes beta with no reload, and its next Channels page (a sidebar click) is beta's. A client-side visit from tab A to `/sites/alpha` records alpha, and tab B's picker follows | with the broadcast's `router.refresh()` removed, tab A stays `"alpha"`. With passive refresh left on, tab A had moved anyway, through a pulse-driven tree refresh, and only tab B's check caught the missing broadcast; hence the setting |
The spec's comment says why: Playwright's auto-retrying `toHaveValue` cannot see a one-paint flash;
@@ -976,7 +993,10 @@ it polls until the value is right and passes.
- **Docs:** `docs env --check`, `docs files --check` and `settings example --check` all exit **0**.
- **Build:** the editor's `next build`, with the primary's `transcripts/` linked in and capped at
5 GB with no swap: **33 s, max RSS 1,628 MB**, exit 0. Every page is listed `ƒ`. The link was
- removed after the build, and nothing ran through it.
+ removed after the build, and nothing ran through it. After the merge of `main` and L5, at
+ `677d1668`: **36 s, max RSS 1,617 MB**, exit 0; only `/icon.svg` is `○`.
+- **After the merge of `main` and L5** (at `677d1668`): tsc (all workspaces) clean, 35 s; common
+ **2,301/2,301** (`main`'s count), 51 s; editor unit **95/95**.
- **e2e** (editor, detached and queued):
- `site-scope.spec.ts` alone:
- at `9dc33d59`: 8 passed, 2 failed (the two `/sites/<id>/…` cases, fixed by `d394d0b6`);
@@ -986,6 +1006,10 @@ it polls until the value is right and passes.
- at `352ea8f7` (with the Back case): **11 passed, 0 failed, 53 s**;
- after the review fixes (the code of `8104732b`, whose layout change is a comment): **12
passed, 0 failed, 1.6 min**, after about 2 min in the queue.
+ - **After the merge of `main` and L5** (at `677d1668`; `$T/ss-specs-l5.txt`: `site-scope` plus
+ every spec that visits `/channels/new`: `channel-site-membership`, `channels`,
+ `new-channel-onboarding`, `social-channel`, `pipeline`, `queues`, `dashboard`): **56 passed,
+ 0 failed, 6.0 min**. The full suite was not rerun, as the parent directed.
- The pre-change checks above: the old code swapped in once, then restored. The Back case was
run once with the fix line removed, then restored. So was the two-tab case, with the
broadcast's `router.refresh()` replaced by a no-op, twice: with passive refresh on, then off.
@@ -1007,15 +1031,6 @@ it polls until the value is right and passes.
- **`export/app/(workspace)/WorkspaceView.tsx:60-73` (`splitOn`)** has the same class of one-paint
flash: a localStorage value restored in an effect after the first paint. `export/**` is not this
slice's.
-- **`/channels/new` pre-checks a site only from a `?site=` link.** `ChannelFormClient` reads
- `window.location.search` once, on mount, and no editor link to `/channels/new` carries one.
- The old picker added `?site=` by a `router.replace` from an effect, which by my reading of the
- effect order landed after the form's read, so that route did not pre-check before this slice
- either (not measured). With the provider, the fix is one line in `ChannelFormClient`:
- `useSiteScope().stored` as the fallback. The comments that still say the picker mirrors the
- scope into `?site=` (`ChannelFormClient.tsx:35`, `ChannelForm.tsx:99`,
- `SiteMembershipsSection.tsx:31`) are wrong now. All three files are under
- `editor/app/channels/**`, which is slice DS's.
- **A change on a site's pages waits one server round trip before it navigates** (the cookie write
comes first), and a change there right after landing also waits for the visit's own write,
because Next runs server actions one at a time. The select shows the choice at once.
@@ -1037,10 +1052,6 @@ it polls until the value is right and passes.
seeded `?site=__all__`.
- **A tab restored from the back-forward cache** is not refreshed; the review offered a
`pageshow` handler as optional, and it was not added.
-- **Review L5 waits for slice DS:** the comments in `ChannelFormClient.tsx:35`,
- `ChannelForm.tsx:99`, `SiteMembershipsSection.tsx:31` and `ChannelVolumeBar.tsx:25`, and the
- one-line `/channels/new` pre-check from the provider. Those files are DS's; the parent hands
- them to this slice once DS is on `main`.
#### Decisions the operator could overturn
@@ -1053,6 +1064,8 @@ it polls until the value is right and passes.
| Choosing on a `?site=` link's page drops the param and stores the choice. **Ruled at review: it stays.** | Keep the param and store nothing, as a link's page is "just that page"; the picker and the page would then disagree |
| On a site's pages, the cookie is written before the push. **Ruled at review: the round trip is accepted.** | Push first and write after: faster, but a page opened right after the URL moves could read the old value, and a navigation started while an action is pending discards the action's re-render |
| The migration writes without an optimistic update, so its one flash lasts until the write's re-render | Update at once: a shorter flash, but a state change during hydration (see above) |
+| A new channel starts checked on the active site as the picker resolves it, so an editor with one site and nothing stored starts it checked on that site | Only a stored choice or a `?site=` link; the lone site would then need a click |
+| The new-channel pre-check is the form's initial state, so a pick made in another tab while the form is open does not change its boxes | Follow the active site while the form is untouched |
#### Review
@@ -1067,7 +1080,7 @@ It reproduced M1 with a two-tab probe under the queue lock.
| L2: the inverted URL assertions pass on their first poll | `09559337`: `expectNoSiteParam()` |
| L3: two server renders per change on a site's page or a `?site=` page | "Found and left" |
| L4: a narrow migration race | "Found and left" |
-| L5: stale `?site=` comments, and the `/channels/new` pre-check | Waits for DS on `main`, as the parent ruled |
+| L5: stale `?site=` comments, and the `/channels/new` pre-check | After DS reached `main` (merged at `07d9126e`): `5006c28f`, `677d1668` |
| L6: a stored `__all__` with one site left | "Found and left" (pre-existing) |
| Questions: the port in the name, `?site=` naming no site, a choice on a `?site=` page, the round trip | Ruled: all four stay (see the decisions table) |