#302: configurable pond start page, created with every new pond #310

Merged
opus-5 merged 3 commits from issue-302-pond-start-page into main 2026-08-01 12:14:27 +02:00
Contributor

Closes #302.

/p/:pondSlug redirected to pages.data[0] — the first page in the sidebar's sort order. Deterministic, but a rule no user could see, and one whose target moves as soon as someone adds a page that sorts ahead of it. New ponds had no page at all and landed on the empty-pond hint.

No migration needed

Pond.settings is already Json, so startPageId is a schema addition in packages/shared/src/ponds.ts and nothing else. The issue assumed a migration; there isn't one.

It stores an id, not a slug — renaming or moving the page keeps the setting working.

Resolution

PondHomePage prefers the configured page, but only when it appears in this user's page list. That list already contains just what they may see, so:

  • a start page hidden from a particular user by a page-scoped grant falls back silently instead of landing them on a 404;
  • a trashed start page is a stale id, not an error — it simply is not in the list either.

Checking against the loaded list rather than fetching the page costs no extra request.

Creation

Both paths (createShared, ensurePersonalPond) create a start page, titled from the creator's stored user.locale — the mechanism already used for mails. A new api-side ponds i18n namespace holds the two strings.

It runs after the creating transaction commits. The owner's POND_ADMIN grant is written inside that transaction and the permission layer caches per pond, so creating the page any earlier would ask about rights the grant has not published yet. It goes through PagesService.create() so the page carries every invariant — unique slug, sort key, content cache, search indexing, a Yjs state the collab server can bind to.

Failure is logged, not fatal: a pond without a start page falls back and still works. Losing the pond over its first page would be the worse trade.

Module direction, not forwardRef

PagesModule imported PondsModule without using anything from it — PagesService never injected PondsService, and both controllers only take PagesService. Removing that vestigial edge let PondsModule depend on PagesModule in the honest direction. The alternative, forwardRef in both, would have written a cycle into the module graph to work around an import that was already dead.

Fallout the full suite caught

Every pond created through the api now owns a page. Page.pond deliberately has no onDelete: Cascade — a real purge removes a pond's contents explicitly and audits it, and a silent database cascade would hide that. So eight suites whose teardown went straight for pond.deleteMany hit the foreign key.

A shared deletePondsWhere(prisma, where) helper in testing/test-db.ts deletes pages first; the affected teardowns use it.

Two tests counted pages and now account for the start page instead of pretending the pond began empty:

  • plugin-api: "a full reader sees all pages" reads the id from the pond's settings and includes it, keeping the assertion exact.
  • pond-purge: the audited page count is 3, with a comment saying why.

Fixing those surfaced a real bug: createShared returned the pond row captured inside the transaction, so callers got startPageId: null for a pond that had one. It now re-reads before building the view.

Verification

  • api full suite against a fresh database: 101 files, 565 tests green.
  • One run had attachment-integrity fail; it passes in isolation and passed on the repeat run — the load flakiness documented for parallel local runs, not this change.
  • New tests: start page for shared and personal ponds, the title following the creator's locale (both de and en halves, so the test cannot pass on a hardcoded string), changing the start page without losing neighbouring settings, and clearing it back to the default.
  • pnpm typecheck, full pnpm lint, pnpm i18n:check green; shared rebuilt.

Not verified here: the picker in a running browser. The a11y pack covers /settings, not pond settings.

Closes #302. `/p/:pondSlug` redirected to `pages.data[0]` — the first page in the sidebar's sort order. Deterministic, but a rule no user could see, and one whose target moves as soon as someone adds a page that sorts ahead of it. New ponds had no page at all and landed on the empty-pond hint. ## No migration needed `Pond.settings` is already `Json`, so `startPageId` is a schema addition in `packages/shared/src/ponds.ts` and nothing else. The issue assumed a migration; there isn't one. It stores an **id**, not a slug — renaming or moving the page keeps the setting working. ## Resolution `PondHomePage` prefers the configured page, but only when it appears in this user's page list. That list already contains just what they may see, so: - a start page hidden from a particular user by a page-scoped grant falls back silently instead of landing them on a 404; - a trashed start page is a stale id, not an error — it simply is not in the list either. Checking against the loaded list rather than fetching the page costs no extra request. ## Creation Both paths (`createShared`, `ensurePersonalPond`) create a start page, titled from the creator's stored `user.locale` — the mechanism already used for mails. A new api-side `ponds` i18n namespace holds the two strings. It runs **after** the creating transaction commits. The owner's `POND_ADMIN` grant is written inside that transaction and the permission layer caches per pond, so creating the page any earlier would ask about rights the grant has not published yet. It goes through `PagesService.create()` so the page carries every invariant — unique slug, sort key, content cache, search indexing, a Yjs state the collab server can bind to. Failure is logged, not fatal: a pond without a start page falls back and still works. Losing the pond over its first page would be the worse trade. ## Module direction, not forwardRef `PagesModule` imported `PondsModule` without using anything from it — `PagesService` never injected `PondsService`, and both controllers only take `PagesService`. Removing that vestigial edge let `PondsModule` depend on `PagesModule` in the honest direction. The alternative, `forwardRef` in both, would have written a cycle into the module graph to work around an import that was already dead. ## Fallout the full suite caught Every pond created through the api now owns a page. `Page.pond` deliberately has **no** `onDelete: Cascade` — a real purge removes a pond's contents explicitly and audits it, and a silent database cascade would hide that. So eight suites whose teardown went straight for `pond.deleteMany` hit the foreign key. A shared `deletePondsWhere(prisma, where)` helper in `testing/test-db.ts` deletes pages first; the affected teardowns use it. Two tests counted pages and now account for the start page instead of pretending the pond began empty: - `plugin-api`: "a full reader sees all pages" reads the id from the pond's settings and includes it, keeping the assertion exact. - `pond-purge`: the audited page count is 3, with a comment saying why. Fixing those surfaced a real bug: `createShared` returned the pond row captured **inside** the transaction, so callers got `startPageId: null` for a pond that had one. It now re-reads before building the view. ## Verification - api full suite against a fresh database: **101 files, 565 tests green.** - One run had `attachment-integrity` fail; it passes in isolation and passed on the repeat run — the load flakiness documented for parallel local runs, not this change. - New tests: start page for shared and personal ponds, the title following the creator's locale (both `de` and `en` halves, so the test cannot pass on a hardcoded string), changing the start page without losing neighbouring settings, and clearing it back to the default. - `pnpm typecheck`, full `pnpm lint`, `pnpm i18n:check` green; shared rebuilt. **Not verified here**: the picker in a running browser. The a11y pack covers `/settings`, not pond settings.
opus-5 added this to the M33 — Tweaks & Feinschliff milestone 2026-08-01 08:07:11 +02:00
opus-5 added 1 commit 2026-08-01 08:07:11 +02:00
#302: configurable pond start page, created with every new pond
Some checks failed
CI / Lint, typecheck, test (pull_request) Successful in 6m28s
CI / Import/export fidelity gate (pull_request) Has been skipped
CI / Auth e2e pack (pull_request) Failing after 4m1s
CI / Build container images (pull_request) Successful in 4m3s
45f1925917
Opening a pond landed on whatever sorted first in the sidebar — stable,
but a rule nobody could see, and one whose target moved as soon as
someone added a page ahead of it. New ponds landed on the empty-pond hint
instead of anything useful.

- `startPageId` joins the pond settings. No migration: `Pond.settings` is
  already jsonb. It stores an id, not a slug, so renaming or moving the
  page keeps it working.
- `PondHomePage` prefers it, but only when the page is in this user's
  page list. That list already holds just what they may see, so a start
  page hidden by a page-scoped grant — or trashed — falls back silently
  instead of landing them on a 404, and it costs no extra request.
- Both creation paths give the pond a start page, titled from the
  creator's stored locale. It happens after the creating transaction
  commits: the owner's grant is written inside it and permissions cache
  per pond, so creating the page any earlier would ask about rights the
  grant has not published yet. A failure is logged, not fatal — a pond
  without a start page still works.

`PagesModule` imported `PondsModule` without using it. Removing that
vestigial edge let PondsModule depend on PagesModule in the honest
direction instead of tying the two together with forwardRef.

Every pond created through the api now owns a page, which broke eight
suites whose teardown deleted ponds directly — `Page.pond` deliberately
has no cascade, because a real purge removes contents explicitly and
audits it. A shared `deletePondsWhere` helper deletes pages first. Two
tests that counted pages now account for the start page rather than
pretending the pond began empty.
fable-5 added 1 commit 2026-08-01 08:25:54 +02:00
#302: the permission matrix counts the start page
Some checks failed
CI / Lint, typecheck, test (pull_request) Successful in 6m26s
CI / Auth e2e pack (pull_request) Failing after 6m12s
CI / Import/export fidelity gate (pull_request) Has been skipped
CI / Build container images (pull_request) Successful in 1m20s
30fd1ff53b
Every pond created through the api now carries one, and the matrix pond
is created that way. The start page is an ordinary page with no grant of
its own, so it follows the pond-wide permissions: the three member
subjects each see one more, the label-restricted editor too, and the
outsider — who reaches only the explicitly public page — still sees one.

The 429 in the same run was the login rate limit, reached through the
retries of this failure rather than on its own.
fable-5 added 1 commit 2026-08-01 11:29:57 +02:00
#302: the vault import test reaches its page through the sidebar
All checks were successful
CI / Lint, typecheck, test (pull_request) Successful in 6m30s
CI / Build container images (pull_request) Successful in 1m21s
CI / Auth e2e pack (pull_request) Successful in 8m49s
CI / Import/export fidelity gate (pull_request) Successful in 56s
CD / Build and push images (push) Successful in 35s
CD / Smoke tests against Test (push) Successful in 1m25s
CD / Deploy to Test (push) Successful in 14s
CD / Promote to Int (push) Successful in 12s
CI / Lint, typecheck, test (push) Successful in 6m49s
CI / Import/export fidelity gate (push) Successful in 1m0s
CI / Build container images (push) Has been skipped
CI / Auth e2e pack (push) Successful in 8m30s
f9149eba13
The Obsidian fixture vault contains a note called "Startseite", and the
pond now creates one too — the seeded fixtures use locale `de`. Two
consequences, and the second is the one that mattered:

- the unscoped title locator matched two sidebar entries;
- `/p/<pond>/startseite` no longer belongs to the imported note. The
  pond's own start page took that slug, so the import landed on a
  suffixed one and the test was about to assert against the wrong page.

Both are fixed by scoping to the mount page and navigating through the
sidebar instead of guessing a slug. The test stays meaningful: it then
clicks a wikilink inside the page content, which the empty auto-created
start page would not have.

CI caught this; the local run passed it. Worth remembering that a
title-based locator can go green by luck.
opus-5 merged commit f9149eba13 into main 2026-08-01 12:14:27 +02:00
Sign in to join this conversation.
No description provided.