v0.6.54.0 feat(#320): per-Lifebook briefing sections fold + collapsible web rendering#335
Conversation
…le web rendering New repo method briefingRepository.getLatestPerLifebook(userId, cadence?) uses DISTINCT ON (domain_name) for one round-trip regardless of Lifebook count. Hard-filters domain_name IS NOT NULL so global briefings can never accidentally fold into the sections list. GET /api/twin-briefings/latest grew an additive sections[] field: one entry per visible Lifebook that has a per-domain briefing, ordered by Lifebook importance (core -> secondary -> emerging). Lifebooks without a matching briefing are omitted (no empty slots). Web rendering in twin-briefing.js: per-Lifebook sections render as collapsible <details> elements between the global prose section and the history sidebar. First card open by default; rest collapsed to avoid a wall of text. Native browser collapsing, zero JS state. Backend partitioning shipped earlier in #258; this PR is the API fold + web rendering — the dashboard now has a single round-trip to render the full partitioned view. Backward-additive: existing briefing field unchanged; sections always an array (never undefined). /lifebook/:domain/latest untouched. Updated briefing-routes.test.ts to mock the new lifebookRepository + getLatestPerLifebook defaults so pre-#320 tests still pass. 5 new repo tests + 6 new route tests. 290 db / 675 api tests pass. Closes #320 (no deferred scope — mobile rendering tracked under #179). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Adds per-Lifebook briefing sections to the latest twin briefing API response and renders them as collapsible cards in the web briefing page.
Changes:
- Adds
briefingRepository.getLatestPerLifebook(userId, cadence?). - Extends
GET /api/twin-briefings/latestwith additivesections[]. - Renders per-Lifebook sections in
twin-briefing.jsand adds API/repository tests.
Reviewed changes
Copilot reviewed 9 out of 9 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| VERSION | Bumps release version to 0.6.54.0. |
| package.json | Bumps package version to 0.6.54.0. |
| CHANGELOG.md | Documents the new per-Lifebook briefing fold and rendering. |
| packages/db/src/repositories/briefing-repository.ts | Adds latest per-domain briefing lookup. |
| packages/db/src/tests/briefing-repository-per-lifebook.test.ts | Adds repository tests for the new lookup. |
| apps/api/src/routes/twin-briefings.ts | Adds sections[] folding to /latest. |
| apps/api/src/tests/twin-briefings-sections-fold.test.ts | Adds route tests for folded sections. |
| apps/api/src/tests/briefing-routes.test.ts | Extends existing route mocks for the new dependencies. |
| apps/web/public/js/pages/twin-briefing.js | Renders per-Lifebook briefing sections as collapsible cards. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| * #320: return the latest per-Lifebook briefing for EACH of a user's | ||
| * visible Lifebooks, in importance order (core → secondary → | ||
| * emerging). One row per Lifebook, NULL when no briefing for that | ||
| * domain exists yet (the worker hasn't emitted one). Used by | ||
| * `GET /api/twin-briefings/latest`'s `sections[]` fold to render the | ||
| * partitioned dashboard view alongside the global briefing. | ||
| * | ||
| * Single SQL query with `DISTINCT ON (domain_name)` so cost is one | ||
| * query no matter how many Lifebooks the user has. Equivalent to N+1 | ||
| * `getLatestForUserDomain` calls but bounded. |
There was a problem hiding this comment.
Fixed in f40d57d — docstring now describes what the method actually does (DISTINCT ON over twin_briefings non-null-domain rows) and explicitly defers visibility / ordering / omission to the route's join against listVisible. No more overclaiming.
| // Two parallel queries + a join in JS — cheaper than per-Lifebook | ||
| // round-trips, and the per-call cost is bounded by the user's | ||
| // visible-Lifebook count (typically < 10). |
There was a problem hiding this comment.
Fixed in f40d57d — comment now reads 'Three parallel queries' matching the actual Promise.all of getLatestForUser + getLatestPerLifebook + listVisible.
| ### Tests | ||
|
|
||
| - 5 new repository tests in `briefing-repository-per-lifebook.test.ts` pinning: DISTINCT ON + cadence threading + global-briefing exclusion + empty-result case. | ||
| - 7 new route tests in `twin-briefings-sections-fold.test.ts` pinning: 400 on missing userId; empty-state shape; global-only path; importance-ordered sections with omit-when-no-briefing; hidden Lifebooks excluded (via listVisible contract); cadence query param threads to both queries. |
There was a problem hiding this comment.
Fixed in f40d57d — CHANGELOG now reads 6, matching the actual it() count in twin-briefings-sections-fold.test.ts. Off-by-one on my part.
… count Three Copilot doc findings, all valid: 1. briefingRepository.getLatestPerLifebook docstring overstated scope — it claimed visibility filtering / importance ordering / null placeholders, but those happen later in the API route's join against listVisible. Rewrote docstring to describe what the method actually does (DISTINCT ON over twin_briefings rows with non-null domain_name) and explicitly note the route handles visibility / ordering / omission. 2. Route comment said 'Two parallel queries' but it's actually three (getLatestForUser, getLatestPerLifebook, listVisible). Corrected. 3. CHANGELOG claimed 7 new route tests; the file has 6. Off-by-one I miscounted. Fixed. No functional changes.
…ive-toolchain caches (#336) Three changes to .github/workflows/{build,evals,release}.yml to cut CI wall-clock: 1. Turbo remote cache via dtinth/setup-github-actions-caching-for-turbo@v1 on every job that runs `pnpm build`. First job populates the GH Actions cache; downstream jobs (desktop-mac/linux, mobile-android/ios, evals, release matrix) get `pnpm build` as cache restores across the monorepo. Skipped on Windows desktop runners (action writes to /tmp). 2. Path-filtered desktop + mobile jobs on PRs via dorny/paths-filter@v3. PRs that don't touch apps/desktop/**, apps/mobile/**, packages/**, or lockfiles skip the 5 desktop+mobile packaging jobs. Push events to main and tag pushes always run everything; release artifact coverage unchanged. 3. Native-toolchain caches: electron-builder downloads (per-OS), Gradle (gradle/actions/setup-gradle@v4), CocoaPods (Pods/ + global cache). Bumped 0.6.54.0 → 0.6.55.0 after rebase (#335 took the 0.6.54.0 slot). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Summary
briefingRepository.getLatestPerLifebook(userId, cadence?)— single DISTINCT ON queryGET /api/twin-briefings/latestgrows additivesections: Array<{lifebookId, domainName, importance, briefing}>fieldtwin-briefing.jsrenders sections as collapsible<details>cards between global prose and historyWhy
Backend per-Lifebook partitioning shipped in #258. Until now the dashboard only fetched the global briefing; per-Lifebook rows were only reachable via the per-Lifebook detail page. This adds the API fold + UI so the dashboard renders the full partitioned view in one round-trip.
Backward compatibility
briefingfield unchangedsectionsalways an array (never undefined)/lifebook/:domain/latestuntouchedTest plan
pnpm build --concurrency=1cleanpnpm --filter @skytwin/db test— 290 passed (5 new inbriefing-repository-per-lifebook.test.ts)pnpm --filter @skytwin/api test— 675 passed (6 new intwin-briefings-sections-fold.test.ts; pre-existingbriefing-routes.test.tsmocks extended for the new repo calls)🤖 Generated with Claude Code