Reorganize into src/ + public/ layout; dignified empty states + Inbox integrity - #5
Merged
Merged
Conversation
…LICENSE Pre-existing working-tree changes captured as their own commit before the directory reorganization: package.json metadata + typecheck script, @types/better-sqlite3, tsconfig allowJs/checkJs + exclude list, LICENSE, and assorted test/source tweaks. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The repo grew from an HTML/JS prototype, leaving ~15 loose backend .ts files
at the root next to frontend assets, config JSON, and scattered docs. Move to a
conventional layout so server code, browser code, and data are distinguishable
at a glance. No runtime behavior change.
- src/ all server + browser-shipped code (loose root .ts + api/ capture/
capture-clients/ db/ descriptor/ enrichment/ llm/ skills/, intact)
- public/ static shell served as-is (index.html, sw.js, manifest, icons)
- docs/ stories/ -> docs/stories, _planning_documents/ -> docs/planning
The whole backend tree moved uniformly, so every relative import stayed valid.
Only the non-relative couplings were touched:
- data reads anchored at repo root (collections.json / taxonomy.json /
bookmarks.json / library.json) via path.join(__dirname, "..", ...) so existing
local data is preserved in place
- server.ts static root -> ../public; two explicit routes serve the browser JS
that stays in src/ (/collections-ui.js, /descriptor/render-map.js)
- spawn cwd for add.ts kept at repo root (DATA_DIR is cwd-relative)
- package.json test script switched to a glob (src/**, extension/**); run-mode
paths (Dockerfile CMD, systemd units, README) -> src/server.ts
- tsconfig exclude sw.js -> public/sw.js
Verified: tsc --noEmit clean; 518 tests across 62 files pass (== baseline);
server boots and every served URL returns 200 with the right content-type.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Three related touches so the board never dead-ends and composed boards arrive
with taste (PRODUCT.md: 'degrade with dignity', 'empty states are first-class
designed moments').
A. Inbox is now a protected system board — DELETE /api/boards/inbox returns 409
instead of cascading, so the app can never reach zero boards.
B. Frontend resilience: load() no longer white-screens when /api/collections
fails or returns empty. It always renders the switcher chrome (incl. the +
affordance) and a board-level fallback (renderBoardsFallback): a calm
'Can't reach the server' + Retry, or a 'No boards yet' invite whose
'Describe a board' CTA opens the existing composer (progressive disclosure).
The filter/sort toolbar hides in these states; '⚙' hides with no active board.
C. The composer now writes per-board empty-state copy. Added an optional
empty_state {head, body} to the board descriptor (backward-compatible, like
archive_on_promote); the compose-board prompt asks for it in the board's
voice; guardrails trim/cap and drop it if blank; emptyVoice() renders it for
composed boards. A 'Mood board' or 'Videos' board now gets a bespoke empty
state instead of the generic 'This board is ready.'
Verified: tsc clean; 524 tests pass (+6); in-browser — Inbox delete 409,
degraded state + Retry recovery, and no-boards → composer all confirmed.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This was referenced Jun 27, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Two things, in three commits:
src/+public/layout. The project grew from an HTML/JS prototype, leaving ~15 loose backend.tsfiles at the root next to frontend assets, config JSON, and scattered docs. This moves all server + browser-shipped code undersrc/, the static shell underpublic/, and consolidates narrative docs underdocs/— with no runtime behavior change.Commits
chore:capture pre-existing working-tree edits (package metadata,typecheckscript, tsconfigallowJs/exclude,LICENSE) as their own commit.refactor(repo):thesrc/+public/move. The whole backend tree moved uniformly, so every relative import stayed valid; only the non-relative couplings were touched (static root →../public,__dirnamedata reads re-anchored at the repo root, child-processcwd, thetestscript → globs, Dockerfile/systemd/README run paths, tsconfig exclude).feat:dignified empty/degraded states + Inbox integrity (details below).Feature detail
DELETE /api/boards/inboxnow returns 409 instead of cascading, so the app can never reach zero boards.load()no longer throws pastrenderSwitcherwhen/api/collectionsfails or is empty. It always renders the switcher (incl. the + affordance) plus a board-level fallback — a calm "Can't reach the server" + Retry, or a "No boards yet" invite whose "Describe a board" CTA opens the existing composer (progressive disclosure). The filter/sort toolbar hides in these states; ⚙ hides with no active board.empty_state {head, body}on the board descriptor (backward-compatible, likearchive_on_promote). The compose-board prompt asks for it in the board's voice; guardrails trim/cap and drop it if blank;emptyVoice()renders it for composed boards. A "Mood board" or "Videos" board now gets bespoke copy instead of the generic "This board is ready."Verification
npm run typecheckclean.testscript now globssrc/**+extension/**; baseline was 518 + 6 new tests).src/-served browser modules.Notes for review
chore:commit folds in pre-existing local edits that were uncommitted before this work.db/,capture/,enrichment/, …) were intentionally not restructured — only relocated undersrc/.🤖 Generated with Claude Code