Files
recap/ROADMAP.md
T
Keysat 890d671bf2 Dedupe relay response-parsing + operator calls; add relay unit tests
Collapse three byte-identical response-parsing blocks (getRelay, postRelay,
and the tts error path) into one handleRelayResponse helper. pingBalance is
deliberately left out: it records a relay error even on a parsed envelope,
a different contract from the other three (updateRelayState clears lastError
while recordRelayError sets it), so folding it in would change observable
state.

Collapse the six operator-to-relay calls into operatorPost / operatorGet,
preserving their intentional split: writes (tier grant, invoice, order)
throw on misconfig or non-OK so the operator action surfaces the failure;
reads (tier, plans, expiring subs) return null so the caller falls back to
a default. Per-function signatures, body shapes, error messages, and the
throw-vs-null behavior are unchanged.

Add server/test/relay.test.js (first fetch-mock harness for relay.js):
14 tests covering the tts error-path control flow, handleRelayResponse's
envelope parsing and error-recording rule, and the operator throw-vs-null
contract including the missing-config branch. 158 tests pass.

ROADMAP gains the deferred refactor-survey items (subscription engine,
/api/process pipeline, sweep middleware, transcript coalescers) and notes
the relay-test coverage against the existing known-debt entries.
2026-06-20 06:07:21 -05:00

138 lines
16 KiB
Markdown

# ROADMAP
Longer-term backlog for Recaps. Near-term in-flight work and known issues live in `AGENTS.md` under **Current state**.
## Near-term backlog
- **Persist provider preference server-side.** `processItemInternally` currently runtime-detects (relay-if-configured / gemini-fallback) because the user's choice lives only in the client's `localStorage`. Persist it so a fresh-container rebuild or any non-browser caller (cron, background processor) picks the right path. Probably a single key in the StartOS config blob + a small migration to seed it from the first authenticated client.
- **Apply Export ▾ menu to the clip-collection panel.** The main view and history rows already have it; the clip collection still has the single legacy "Export PDF" button. Reuse the existing menu component.
- **CI lint + type-check.** No `lint` script in `server/package.json`; top-level `tsconfig.json` exists but the server is pure `.js`. Decide: add ESLint, adopt JSDoc-driven TS checking, or remove the empty `tsconfig.json`.
- **Surface failed auto-queue items in the dashboard.** Currently hidden by default behind a "Show all" toggle. Worth a small banner / count chip when failures exist so operators notice without hunting.
- **Zaprite recurring card billing (BLOCKED on Zaprite).** Grant wants card payments to DEFAULT to recurring (buyer can opt out at checkout). Zaprite's public API (`api.zaprite.com/openapi.json`) only creates one-time `/v1/orders` — recurring is a hosted/dashboard feature with no per-buyer metadata, no renewal webhook, and no billing-portal URL via API. The shipped card rail is one-time prepaid. UNBLOCK by confirming with Zaprite support whether the account can: (a) attach a per-buyer reference/metadata to a recurring checkout (so a payment maps to a Recaps user), (b) fire a webhook on each renewal charge (so we extend the tier each period), (c) expose a customer/billing-portal URL (for the chosen "link to Zaprite portal" cancel path). Decisions already made: no reminder emails for auto-renewing cards; a failed charge = lapse at period end (the relay's expiry-enforcement already does this — a missed renewal just doesn't extend `expires_at`).
- **Close the architecture-simplification gaps** (`docs/architecture-simplification-plan.md`). After core-decoupling + self-serve, these steps remain OPEN: **(8) "Take Recaps home"** — mint a fresh Keysat token on demand at click time; likely BROKEN today because relay-tier cloud users have no `keysat_license` for `/api/account/license-key` to return. **(10) cloud paid-only** — the free signed-in tier + signup-grant credits are still live; the plan wanted cloud to be paid-only with self-hosted as the free path (product call — confirm intent before building). **(5, partial) anon signup→Pro** still routes through `/api/license/purchase` + `pending_signups` (Keysat license) instead of the relay tier like the signed-in flow does. **(6, partial) tokenized renew** — the reminder email's renew link is `?renew=1` (requires sign-in); the plan wanted a one-time-token `/renew?token=…` for friction-free renewal. NOTE: the doc's Zaprite-*recurring* / cancel-button / Recaps-DB-owns-expiry parts were intentionally SUPERSEDED by the prepaid + relay-owns-tier model — don't build those.
- **Decide the Max tier-quota default.** The relay code default is `max.monthly: null` (unlimited) → cards render "Unlimited" on a fresh install. The operator set `max.monthly: 120` on their box via the Adjust-Tier-Quotas action (so cards show 120 there). Decide whether a metered number (e.g. 120) should be the shipped default in `recap-relay/server/config.js` — note it also enforces the ceiling, not just the card label.
- **Add Gemini 3.5 to model selection.** First have a research agent confirm which stable Gemini model versions are actually available and the correct model id/name before wiring anything. The model list is duplicated server + client (provider config under `server/providers/` + the model picker in `public/index.html`) — add the option in lockstep, like the URL-parser convention. Coordinate with the matching relay-side capture (the relay routes Gemini, so its model list must agree). — captured 2026-06-16
## Design-contract conformance cleanup (from the 2026-06-16 `/design` extract)
The `design/` contract (`design/DESIGN.md` + `design/tokens.tokens.json`) was extracted
from the as-built UI and reconciled with Grant on 2026-06-16. The code was structurally
aligned but a set of legacy values had survived as off-contract drift.
**Phase 1 — DONE 2026-06-16 (not yet deployed to the box).** Introduced a canonical `:root`
token block (the single source of truth, mirroring `tokens.tokens.json`) at the top of the
`public/index.html` `<style>` block and migrated the whole stylesheet to `var(--token)`;
`public/auth.html` got its own subset `:root` and was migrated too. Fixed **all** color +
weight drift across every surface (stylesheet, ~447 inline styles, JS handlers, the
`SHARE_PAGE_CSS` export): legacy indigos `#6366f1`/`#4f46e5`/`#4338ca` + `rgba(99,102,241,…)`
`#818cf8`/`#a5b4fc`/`rgba(129,140,248,…)`; blue `#3b82f6` interactive buttons (incl. the
whole auth screen) → indigo; legacy darks `#0a0e17`/`#0b1120`/`#020617`/`#121828`/`#1f2942`
→ the ladder; `#f5f9ff``#f1f5f9`; `#312e81``#1e293b`; weights `650→600`, `680→700`.
Verified: 144 tests pass, both pages serve 200, all 426+27 `var()` references resolve, no
undefined vars. (`SHARE_PAGE_CSS` and `auth.html` are standalone documents that each carry
their own copy — kept in sync; the meta `theme-color` stays a literal `#0a0e1a`.)
**Phase 2 — DONE 2026-06-17 (shipped in app 0.2.161).**
- **Var-ified the long-tail inline `style=` attributes** — 346 inline-style hexes (+7
`#475569`, mapped by property to `--text-faint`/`--border-strong`) → `var(--token)`. Scoped
to CSS-value position (hex preceded by `:`/space/`,`, never a quote), which cleanly dodged
the non-`var()` spots: the `<meta theme-color>`, SVG `fill`/`stroke` attrs, and hex held in
JS *logic* (quoted ternary branches like `${cond ? "#1e293b" : ...}`, `const colour = …`).
Left as literals on purpose: `#fff` (its uses split between on-accent button text and
functional white — `--on-accent` doesn't cleanly cover both, zero visual gain), no-token
hexes (`#e0e7ff`/`#c7d2fe`/`#a78bfa`/`#04210f`/etc.), and the entire `SHARE_PAGE_*` export
region (a standalone doc with no `:root``var()` wouldn't resolve there).
- **Snapped off-scale font sizes** (21 occ): `9/10.5→10`, `11.5/12.5→12`, `15→16`, `24→22`.
Left `40px`/`56px` display glyphs (success numeral, buy spinner) — off-scale by design.
- **Snapped off-scale radii** (18 occ): `3→4`, `5→6`, `7→6`, `11→12`; `9→10` for the two 18px
capsules (`.menu-badge`, `.rc-spk` — radius clamps at 9 on an 18px box, so on-scale and
visually identical) and `9→8` for `.icon-btn`/`.buy-select-btn`/`.buy-discount-input`. Left
the `1px` hamburger-bar radius. Verified: 144 tests pass, both pages serve 200, every
introduced `var()` resolves against `:root`, no off-scale residue.
- **(stretch, NOT done) Generate `design/brand/palette.css` from the tokens** (Style
Dictionary) and `@import`/inline it, so the `:root` block isn't hand-maintained in three
places. Still open.
## Known debt (P2, from the 2026-06-14 full-eval — `EVALUATION.md`)
Real but not release-blocking for self-host. The P0/P1 findings from the same eval were fixed 2026-06-15 (see git log + `EVALUATION.md`).
- **Operator-internal strings leak to cloud users at the SSE error boundary** (Parakeet/Gemma/CUDA/LAN IPs) — no scrub exists, violating the scrub convention in `AGENTS.md`. `server/index.js:3432,3003,4246` + `providers/relay.js:134-143` (now centralized in `handleRelayResponse`, with a copy in `pingBalance` at `:834` — the 2026-06-19 dedup makes the relay-side scrub a single-point fix). (Sharp edge: `index.js:3419` *detects* these strings, then forwards them anyway.)
- **Credit over-spend TOCTOU on licensed installs** — N parallel requests pass the `total>0` check before any blind `debitOne` lands. Make check+debit atomic (reserve up front, refund on failure). `index.js:2497-2550` vs `:3158,:4197`.
- **Multi-mode tenant can spend the operator's server Gemini key** via `transcriptionProvider:"gemini"` + empty key (bypasses relay metering) — `providers/index.js:104-114`. Refuse the operator-key fallback for non-admin tenants.
- **`GET /api/history` parses every full session file** (transcript+summary, MB each) just to list ~8 metadata fields — cache them into `_meta.json` on save. `history.js:418-437`.
- **Dependency CVEs** — nodemailer 6.10.1 (high; low practical reach here), ws/qs/express/protobufjs (moderate). `npm audit fix` (nodemailer is a major bump).
- **No tests on the riskiest files** (`/api/process` gating, `tenant-auth.js`, billing; `relay.js`'s response handling + operator helpers now have unit coverage via `test/relay.test.js`, but its transcribe/summarize/analyze paths still don't) — the real summarize→save→debit path can't run end-to-end without a key/credits. Add an integration test as the regression net.
- **Smaller hardening:** unsanitized IDs persisted to `_meta.json` (array-form library import + `PUT /api/history/move`) — no file-path escape (read-time `safeFilename` guards the load), but sanitize at write too; `PUT /api/history/meta` accepts arbitrary JSON shapes with no schema; `index.js` is 4351 lines mixing routing/pipeline/yt-dlp/SSE.
- **Doc drift (high-value):** AGENTS.md credit-gate order omits the "paid cloud user" bypass state (`:77` vs `index.js:2464-2472`); operator-facing `startos-registry/.../INSTRUCTIONS.md` + `assets/ABOUT.md` are stale Gemini-first (relay is the default provider).
## Refactoring backlog (from the 2026-06-19 refactor-scout survey)
The two low-risk wins from the same survey are already DONE (relay.js response-parsing
+ operator-call dedup — extracted `handleRelayResponse` / `operatorPost` / `operatorGet`,
with a new `server/test/relay.test.js` covering the tts error-path control flow, envelope
parsing, and the operator throw-vs-null split; **158 tests pass**, +14). The rest are
deferred behind a **missing test net**`server/index.js`
(4,376 lines) is the god file (router + pipeline + subscription engine + podcast RSS), and
its riskiest logic has no integration coverage, so these extractions need a characterization
test FIRST. Listed by descending value; the highest-value one is the subscription engine.
- **Extract the subscription engine.** Move the subscription-discovery helpers
(`isPodcastFeedUrl`, `isAppleShowUrl`, `resolveAppleShowToFeed`, `parsePodcastRSS`,
`fetchDatesFromRSS`, `getChannelId`, `fetchChannelName`, `listChannelVideosFast`,
`fetchUploadDates`; `index.js:~891-1008`) plus `checkScopeSubscriptions` /
`_checkSubscriptionsInner` (`index.js:~1421`, ~800 lines total) into
`server/subscription-engine.js`. None depend on the Express `app` — they're trivially
testable in isolation but currently unreachable by any test. Write characterization tests
against fixture XML + mock yt-dlp output first.
- **Extract the `/api/process` pipeline.** The handler is a single ~1,800-line function
(`index.js:~2484-4281`): credit gate, provider resolution, URL resolution, relay
unified-pipeline branch, captions fast-path, relay-URL fast-path, audio-download-with-retry,
chunked-transcription, analysis-with-fallback, dispatch. Split into `server/pipeline.js`,
one step at a time. RISK: the many early-return paths with `releaseFreeSlot()` + `res.end()`
make blind extraction dangerous — write a characterization test (mock transcriber +
analyzer, ≥3 paths: relay mode, captions fast-path, chunked fallback) BEFORE touching it.
- **Extract `sweepAndRefreshTrial(req, res)` middleware.** A ~35-line block (sweep unapplied
purchases for `req.user`/`req.trial`, re-read the trial row, update `req.trial`) is
copy-pasted between `/api/account/whoami` (`index.js:~280-420`) and `/api/relay/status`
(`index.js:~561-675`), differing only by log prefix. MED risk (auth/billing path; moving it
changes when `req.trial` mutates relative to other middleware) — write an integration test
for both endpoints first.
- **Move the pure transcript coalescers to `util.js`.** `coalesceTranscriptEntries` +
`coalesceForAnalysis` (`index.js:~2167-2270`) are pure functions (no I/O, no state) buried
in the god file and currently untested, with non-trivial logic (median-duration guard,
bucket sizing, `indexMap`). Low-risk move; add unit tests alongside per the repo's
test-with-the-change convention.
## Deferred hardening & cleanup (P3, from the 2026-06-14 full-eval — `EVALUATION.md`)
Low-severity; batch when convenient. None block release. (P0/P1 work queue and P2 known debt live in `AGENTS.md` → Current state.)
- **Request-size / fetch caps.** `express.json({limit:"100mb"})` on every route (`server/index.js:203`) is a cheap memory-exhaustion lever; tighten it. `downloadPodcastAudio` also needs a size/time cap — folds into the P0 SSRF fix.
- **`/api/credits/claim` invoice-ID hijack.** A leaked anon BTCPay invoice ID is claimable by any signed-in account (`server/credits-purchase.js:342`); bind claims to the buyer's email. Random IDs keep this low-risk.
- **Container runs as root** (no `USER` in `Dockerfile`) — acceptable under StartOS isolation; add a non-root user for the cloud image.
- **In-memory auth rate-limit buckets reset on restart** (`server/auth-routes.js:106`) — fine for self-host single-operator; note for cloud HA.
- **Repo hygiene.** Delete the stale `youtube-summarizer_x86_64.s9pk` (~223MB, old package ID) and rename the root `package.json` (still `youtube-summarizer-startos`). `cookies.txt` is sensitive plaintext in the repo root and is expiring (`/api/health` already reports `fileExpiring:true`) — gitignored, but rotate/move it.
- **StartOS community-registry submission — deferred (decision 2026-06-15: self-host + cloud only for now).** Hard blockers if/when we submit: add a root `instructions.md`; point `packageRepo`/`upstreamRepo` at a public source repo (currently `https://ten31.xyz`, a homepage); choose a source-available license for the wrapper (currently `Proprietary`). Softer polish: empty `manifest.docsUrls`; verify the multi-tenant cloud actions (`enableMultiTenantMode` et al., `startos/actions/index.ts`) run cleanly — not stack-trace — in single mode; 172 empty version-file migration stubs are a growing maintenance surface. None of this affects `make install`.
- **Doc reconciliation (bulk).** AGENTS.md directory layout omits ~25 server modules; `docs/guides/relay-client.md:17` Authorization header is missing the `Bearer` scheme; `index.html` is stated as `~10k` lines but is 12.5k (`AGENTS.md:51`); the `safeFilename()` convention (`AGENTS.md:79`) becomes accurate once the function is exported (the P0 fix).
## Larger plans (already drafted in `docs/`)
- `docs/architecture-simplification-plan.md` — broader simplification arc
- `docs/core-decoupling-plan.md` — separating the core summarize pipeline from billing / multi-tenant concerns
- `docs/per-tenant-subscriptions-plan.md` — moving subscription state into the per-user scope
- `docs/self-serve-purchase-plan.md` — buyer flow for Pro/Max and a la carte credits
- `docs/path-2b-and-path-1-interweave.md` — sequencing for the multi-tenant cloud meetings work (depends on the relay's Path 2A)
Treat the `docs/` plans as the source of truth for those items; cross-reference rather than restating here.
## Adjacent (lives in `../recap-relay`)
The relay now has its own `AGENTS.md` + `ROADMAP.md` — track relay work there; this is just what the client surfaces or waits on.
- **Speaker MERGE + re-run detection + re-polish — SHIPPED relay-side** (operator dashboard, live on the box at relay 0.2.124, 2026-06-13). Merge folds two clusters into one; re-run re-clusters at a new strictness to split over-merged speakers; re-polish rewrites topic summaries to corrected names. App-side UI for these is now unblocked if wanted. *(The relay tree is at 0.2.124 but uncommitted to git — see `../recap-relay/ROADMAP.md`.)*
- Cross-call speaker fingerprint memory (recognize the same voice across meetings) — not yet shipped.
- Phase 3 of Path 2A: multiple operator-editable meeting prompt sets (1on1 / all-hands / customer-interview / standup) selectable per upload — not yet shipped.
Avoid building app-side UI for the unshipped items until the relay-side pieces land.