fix(web): review round 1: optimistic edits, friendly errors, preferences, Gelasio
- Tags and custom fields update the cached entry optimistically and build from the latest cache; one entry's PATCHes run in order (mutation scope). - The review textarea follows server changes (Restore) unless edited. - friendlyError() maps validation reasons and 413 to plain copy everywhere. - Rating scale from me/preferences; /login and /register use auth/status; Attachments lists from the API. - Gelasio vendored (official OFL variable TTFs); minors from the review. - Docs: DESIGN (AA on orange, faint labels), DECISIONS, TODO, ARCHITECTURE. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
d204775235
commit
5fa59310f3
@@ -15,8 +15,8 @@ user layer or files a Changes-inbox event.
|
||||
| Path | Responsibility | State |
|
||||
|---|---|---|
|
||||
| `apps/server` | `main.ts` boots (config → `createDb` → `migrateToLatest` → `loadPlugins` → worker + scheduler + daily backup → `Bun.serve`). `app.ts` `createApp(deps)` = `/api/v1` route table + `/ingest/*` mount (404 in Phase 1), testable via `app.fetch`. `http.ts` = `route()`, auth (session cookie / Bearer app token), CSRF header, body caps, rate limit, error mapping. `routes/<area>.ts` one file per area; `openapi.ts` builds `/api/v1/openapi.json` from the table | exists |
|
||||
| `apps/web` | React app (HTML imports); `index.html` is a placeholder until Task 17 | placeholder |
|
||||
| `packages/core` | domain services, one module per area (import by path, no barrel): `auth`, `crypto`, `works`, `matching`, `entries` (entries, library listing, quick add, per-user moves, match suggestions), `engagements`, `history` (`user_history`, restore), `lists`, `copies`, `custom-fields`, `attachments`, `export` (per-user bundle, CSV, restore), `backup` (scheduled bundles + SQLite `VACUUM INTO`), `sync/` (the change engine: `snapshot` hashing and versions, `apply` per-record diff and apply, `run` = `runSync`, `changes` = Changes inbox `listChanges`/`resolveChange`, `listens` = `ingestListens`), `jobs` (queue, worker, scheduler), `registry` (`loadPlugins`), `connections` (CRUD, plugin context, `syncHandler`) | exists (partial) |
|
||||
| `apps/web` | React 19 app served by the server as an HTML import (`index.html` → `src/main.tsx`, `src/theme.css`). wouter routes in `main.tsx` (`/login`, `/register`, `/`, `/entries/:id`; other sections are placeholders until Task 18); `api.ts` = fetch client (`request`, `ApiError`, cookie auth, CSRF header, mirrored wire types); `format.ts` = pure display helpers and `friendlyError`; `edits.ts` = next-value builders for entry edits; `hooks.ts` = TanStack Query hooks incl. optimistic `useEntryPatch`; `settings.ts` = preferences; `components/` (shell, detail boxes, history drawer, rating control) and `routes/`. Never imports `packages/*` at runtime | exists |
|
||||
| `packages/core` | domain services, one module per area (import by path, no barrel): `auth`, `crypto`, `works`, `matching`, `entries` (entries, library listing, quick add, per-user moves, match suggestions), `engagements`, `history` (`user_history`, restore), `lists`, `copies`, `custom-fields`, `attachments`, `preferences` (per-user rating scale and default visibility), `export` (per-user bundle, CSV, restore), `backup` (scheduled bundles + SQLite `VACUUM INTO`), `sync/` (the change engine: `snapshot` hashing and versions, `apply` per-record diff and apply, `run` = `runSync`, `changes` = Changes inbox `listChanges`/`resolveChange`, `listens` = `ingestListens`), `jobs` (queue, worker, scheduler), `registry` (`loadPlugins`), `connections` (CRUD, plugin context, `syncHandler`) | exists (partial) |
|
||||
| `packages/db` | Kysely over one Bun.SQL dialect (SQLite + Postgres), ids, JSON helpers, migrations, `forEachDialect` test harness | exists (schema pending) |
|
||||
| `packages/plugin-sdk` | plugin contract, `SourceRecord`, http/rate-limit helpers, fixture test harness | planned |
|
||||
| `plugins/<id>` | one source integration each (hardcover, trakt, mangabaka, listenbrainz, lastfm, csv) | planned |
|
||||
|
||||
@@ -4,6 +4,32 @@ Append-only. Never edit or delete an entry. Supersede it with a new one that lin
|
||||
One entry per decision a future maintainer would otherwise have to reverse-engineer.
|
||||
No index: `grep -n '^## ' docs/DECISIONS.md` lists every entry (date, title, line number).
|
||||
|
||||
## 2026-10-30: Web app (Task 17): preferences, first run, optimistic edits, AA on orange, self-hosted fonts
|
||||
**Status:** accepted
|
||||
**Context:** Task 17 and its fix round 1 (controller rulings).
|
||||
**Decision:**
|
||||
- **Three small API routes for the web app.** `GET auth/status` (no auth) → `{ hasUsers }`: `/login` redirects a fresh
|
||||
instance to `/register`, which shows "Create the admin account", or invite mode when users exist (also on
|
||||
`invite_required`, and with `?invite=` prefilled). `GET attachments?entity=&entityId=` lists an owned record's
|
||||
attachments with their links (404 when the record is not the caller's). `GET/PATCH me/preferences` →
|
||||
`{ ratingScale: "5half"|"5"|"10"|"100", defaultVisibility }`, stored in `users.preferences` under the same keys
|
||||
`defaultVisibility()` already reads (`packages/core/src/preferences.ts`); unknown stored keys are kept.
|
||||
- **Rating scale** comes from preferences (default five stars with halves). Stored ratings stay integers 0–100; the web
|
||||
formats them with `formatRating(value, scale)`.
|
||||
- **Entry edits are optimistic and serial.** `useEntryPatch` updates the cached entry in `onMutate` (rollback on error),
|
||||
runs one entry's PATCHes in order through a TanStack mutation `scope`, and `edit(cur => patch)` builds from the latest
|
||||
cache, so two quick tag adds keep both. Only the last queued edit writes the server's answer back.
|
||||
- **Keyboard rating saves once:** arrow steps update locally and save 500 ms after the last key, on blur, or on unmount,
|
||||
so stepping does not write a history row per keypress.
|
||||
- **Errors in the product's voice:** every error display goes through `friendlyError()` (`apps/web/src/format.ts`), which maps
|
||||
`validation` field/reason pairs and 413 to plain copy; other API errors keep the server's message.
|
||||
- **AA contrast on the approved palette:** dark text on `--warn`, `--text-faint` group labels, `--text-muted` placeholders on
|
||||
`--surface-3` (recorded in DESIGN.md).
|
||||
- **Fonts are vendored** (Inter woff2 from fontsource; Gelasio as the official variable TTFs from github.com/google/fonts
|
||||
`ofl/gelasio`, unmodified, because no woff2 tooling was available offline) with their OFL texts.
|
||||
- **The web app mirrors wire types** in `apps/web/src/api.ts` instead of importing `@sttracker/core` (apps/web has no
|
||||
workspace dependency on it, and the browser bundle must not pull server code).
|
||||
|
||||
## 2026-10-30: Token actor in history, connection deleted_at, CSRF on every browser write
|
||||
**Status:** accepted. Supersedes the **Actor**, **CSRF** and **Deleted connections** bullets of
|
||||
"HTTP API: auth modes, CSRF header, error mapping, actor" below.
|
||||
|
||||
@@ -18,6 +18,9 @@ Small, specific, independently actionable. Remove only when actually done.
|
||||
- [ ] API: `PATCH list-items/:id` only moves items; editing an item's note needs a core function.
|
||||
- [ ] Phase 2 and Phase 3 plans (spec §12).
|
||||
- [ ] Generate image thumbnails for attachments (Phase 1 sizes images with CSS).
|
||||
- [ ] Web: convert the vendored Gelasio variable TTFs (~170 KB each) to latin-subset woff2 once fonttools/woff2 is available; Bun inlines fonts into the CSS bundle.
|
||||
- [ ] Web/API: "Linked sources" is inferred from `plugin:` actors in `fieldSources`, so a source that matched an entry but never wrote a field is not shown. Add a per-entry source-links route.
|
||||
- [ ] Web: the detail pane only lists attachments of the entry itself, not those linked to its reads or copies.
|
||||
|
||||
## Someday
|
||||
|
||||
@@ -32,6 +35,7 @@ Small, specific, independently actionable. Remove only when actually done.
|
||||
file, and compare steps and token use.
|
||||
|
||||
## Known issues
|
||||
- [ ] Web: in production the bundled HTML references `/../../chunk-*.css|js` (Bun bundles `../../web/index.html` from `apps/server`). Browsers normalise it to `/chunk-*` and it loads; check it behind a reverse proxy.
|
||||
- [ ] Plugins: reload re-imports entry files only; edits to a plugin's other modules need a server restart.
|
||||
- [ ] Plugins: rate limiting is per run (each run builds its own client), not per plugin (spec §11); share a limiter per plugin in Phase 2.
|
||||
- [ ] Plugins: `ctx.log` reaches `source_runs.log` only after the run ends, so a crash mid-run loses it.
|
||||
|
||||
Reference in New Issue
Block a user