diff --git a/AGENTS.md b/AGENTS.md index 8ab76a1..dce643b 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -59,6 +59,61 @@ docs/issues-backlog.md detailed issue log 6. **Extras** (`meeting-extras.js`). 7. **Audio is deleted after processing** (success or failure) — the relay never retains uploaded audio. +## Endpoints (server-side contract) + +All routes mount in `server/index.js`. Public paths sit under `/relay/*`; operator paths under `/admin/*`. + +### Auth model + +- **`X-Recap-Operator-Key`** + **`X-Recap-User-Id`** → "cloud" path. The Recaps cloud server (`recaps.cc`) authenticates once with a shared operator key (`relay_cloud_operator_key`) and names the acting user. Credit pool keyed `user:`, tier comes from the relay's stored row, NOT a per-user license. See `server/identity.js`. +- **`X-Recap-Install-Id`** (+ optional `Authorization: `) → "license" path. Self-hosted installs and the operator's single-mode app. Credits/tier come from the resolved Keysat license + install id. +- **Admin session cookie** → `/admin/*`. Cookie issued by `POST /admin/login`; `/admin/login` and `/admin/status` are exempt inside `setupAdminAuthMiddleware`. +- **Webhook signature** → `POST /relay/btcpay/webhook` validates `BTCPay-Sig` against `relay_btcpay_webhook_secret`. Zaprite's webhook re-fetches the order through the Zaprite API to verify, so no shared-secret signing. +- **`X-Recap-Job-Id`** is a billing key, not auth: the first call with a given id charges one credit; later calls with the same id are free (so transcribe + analyze for one summary = one credit total). + +### `/relay/*` (public; per-call header auth) + +- `GET /relay/health` — liveness; tolerates partial config. (`routes/health.js`) +- `GET /relay/policy` — `{ tiers, core_total_credits, core_gemini_credits }`; no auth. (`routes/policy.js`) +- `GET /relay/capabilities` — operator-wide feature flags (hardware ready, TTS backend choice, etc). `X-Recap-Install-Id` optional. (`routes/capabilities.js`) +- `GET /relay/balance` — caller's credit balance (`routes/balance.js`). +- `POST /relay/transcribe` — multipart audio → `{ text, segments, duration_seconds, model, ... }`. Body fields: `mime_type`, `title`, `channel`, `description`. (`routes/transcribe.js`) +- `POST /relay/transcribe-url` — async; `{ media_url, type, mime_type, title, channel, description, chapters }` → `{ job_id }` then poll `GET /relay/jobs/:id`. (`routes/transcribe-url.js`) +- `POST /relay/summarize-url` — async; same body shape, full transcribe+analyze pipeline → `{ job_id }` then stream `GET /relay/summarize-url/:jobId/events` (SSE). (`routes/summarize-url.js`) +- `POST /relay/analyze` — `{ transcript, … }` → topic sections JSON. (`routes/analyze.js`) +- `POST /relay/tts` — text → audio; gated by `capabilities.has_tts`. (`routes/tts.js`) +- `GET /relay/credits/packages`, `POST /relay/credits/buy`, `GET /relay/credits/invoice/:id` — à-la-carte credit purchase (BTCPay). (`routes/credits.js`) +- `POST /relay/btcpay/webhook` — BTCPay settle → either `extendUserTier` (subscription) or credit grant (à-la-carte). HMAC validated. (`routes/credits.js`) +- `POST /relay/zaprite/webhook` — Zaprite settle → `extendUserTier` only. Re-fetches order to verify. (`routes/zaprite-webhook.js`) + +### `/relay/*` (operator-key only — cloud → relay control plane) + +All require a valid `X-Recap-Operator-Key`. Defined in `routes/user-tier.js`. + +- `POST /relay/user-tier` — `{ user_id, tier: "core"|"pro"|"max", expires_at? }` → sets the cloud user's stored tier (operator comp grants live here). +- `POST /relay/tier-invoice` — `{ user_id, tier: "pro"|"max", return_url }` → mints a BTCPay tier-purchase invoice (Lightning QR). +- `POST /relay/tier-zaprite-order` — same idea on the card rail. +- `GET /relay/tier-plans` — `{ ok, period_days, plans: [{tier, sats, fiat_amount, fiat_currency, credits_per_period}], card_available }`. `credits_per_period: null` → "Unlimited"; never hardcode this label. +- `GET /relay/expiring-subscriptions?within_days=7&lapsed_days=3` — `{ ok, now, subscriptions: [{user_id, tier, expires_at, expired, days_left}] }`. The Recaps server maps user_id → email and sends the reminder; the relay never sees email. +- `GET /relay/user-tier/:userId` — read the stored row. + +### `/admin/*` (operator dashboard; cookie-gated) + +`routes/admin.js`: `GET /admin/{usage,config,license-cache,hardware-queue,jobs,jobs-history,job/:id/details,dashboard,dashboard.csv,settings,output-store-stats}`, `POST /admin/{quotas,wipe-all}`, `PUT /admin/settings`, `DELETE /admin/job-outputs`. `routes/admin-test-run.js`: `POST /admin/test-run`. BTCPay setup wizard under `/admin/btcpay/*` (`routes/btcpay-setup.js`). + +### `/admin/internal-meetings/*` (cookie-gated; `routes/internal-meetings.js`) + +- `POST /upload` — multipart audio; runs the full pipeline (chunk → diarize → cluster → analyze → polish → extras → save). Audio is deleted after. +- `GET /` → `{ meetings: [...] }`; `GET /:id` → full saved record (`rec`). +- `GET /:id/markdown`, `GET /:id/html`, `GET /:id/download` — exports. +- `GET /jobs/:id`, `GET /jobs/:id/stream` (SSE) — progress for a running upload. +- `PATCH /:id/speakers` — rename a cluster (display-name only). +- `PATCH /:id/entries` — per-line `speaker_override`. +- `PATCH /:id/merge-speakers` — fold cluster(s) into one (split-as-two). Offline, no LLM. +- `POST /:id/recluster` — re-run clustering at a new threshold (merged-as-one). Offline, uses `rec.diarization` fingerprints. Resets `speaker_names`, per-line overrides, and extras attributions. 400 if no fingerprints. +- `POST /:id/repolish` — re-runs `runSummaryPolish` with the CURRENT names (no re-inference). Synchronous; needs hardware analyze online; 400 if no named speakers. +- `DELETE /:id`. + ## Conventions for this codebase specifically - **A saved meeting record stores the per-chunk TitaNet fingerprints in `rec.diarization`.** Because the audio is gone, this is what makes re-clustering possible *offline* — no re-upload, no Spark Control round-trip. @@ -86,9 +141,9 @@ docs/issues-backlog.md detailed issue log - **Never edit a `startos/versions/.ts` that's already been built/installed** — add a new version file. - **Don't push to GitHub by default** — remote is self-hosted Gitea. -## Current state (2026-06-13) — at `0.2.124`; only git commits lag +## Current state — box AND working tree at `0.2.124`; git is the gap -- **Box AND local working tree are both at relay `0.2.124`** (app `0.2.155`). Confirmed on the StartOS UI (version + the Merge/Re-polish controls visible on the dashboard). -- **The version files `v0.2.117`–`v0.2.124` are all in this working tree** (untracked). v0.2.124's note is a billing change ("tier Bitcoin invoices return the Lightning BOLT11 + per-period credit allotment"). A **concurrent chat session** during 2026-06-13 continued from this session's 0.2.117, bumped through 0.2.124, and built+installed it to the box — so the working tree matches the box. (Heads-up: more than one session may be editing this tree; re-read before assuming.) -- **The post-hoc speaker tools are present and live**: `meeting-speaker-edits.js` (merge/recluster/repolish + backfill) and the matching `/admin/internal-meetings/:id/{merge-speakers,recluster,repolish}` routes; the dashboard shows the controls. Tests pass (32, `npm test`). -- **The real gap is git, not versions.** Committed HEAD is `v0.2.11`; everything since — v0.2.12→v0.2.124, the entire internal-meetings feature, diarization, speaker-edit tools, billing — is **uncommitted** (≈28 modified + 153 untracked). "Catching up local git" = committing this large working tree (see ROADMAP). The 0.2.117 this session installed was superseded by the concurrent 0.2.124 — **no box downgrade occurred.** +- **Box AND local working tree are both at relay `0.2.124`** (app at `0.2.155`). `startos/versions/index.ts` `current: v_0_2_124`; the StartOS dashboard reflects the same. +- **Version files `v0.2.117`–`v0.2.124` are present in the working tree** (untracked). A concurrent 2026-06-13 session continued from this session's 0.2.117, bumped through 0.2.124, and shipped to the box — re-read the tree before assuming what's there. +- **Post-hoc speaker tools are live**: `meeting-speaker-edits.js` (merge / recluster / repolish + backfill) and the matching `PATCH/POST /admin/internal-meetings/:id/{merge-speakers,recluster,repolish}` routes are present; the dashboard exposes the controls. Tests pass via `cd server && npm test`. +- **The real gap is git, not versions.** `HEAD` is `6fa175a Add agent docs`; `HEAD~1` is `b7f7590 v0.2.11 /relay/capabilities + /relay/transcribe-url`. So the last code commit is at `v0.2.11`; everything from `v0.2.12` → `v0.2.124` — the entire internal-meetings feature, diarization, speaker-edit tools, billing, the user-tier control plane — is uncommitted. Working-tree counts: **28 modified, 150 untracked, 5 deleted (183 total)** as of this read. "Catching up git" = committing this tree (see ROADMAP). diff --git a/ROADMAP.md b/ROADMAP.md index 77f07f1..6bbfc42 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -19,6 +19,11 @@ Versions are reconciled: box and local working tree are both at **0.2.124** (a c - **Make re-polish async for long meetings.** `POST /:id/repolish` is synchronous (one LLM pass per analysis window); a 2-hr meeting could make the request hang. Move to the existing job system (createJob/appendEvent/markComplete) + poll, like the main pipeline. - **Speaker MERGE provenance.** Merge sums stats and approximates `chunks_appeared_in` as `max` (raw per-cluster chunk sets aren't retained). Recompute exactly from `rec.diarization` if precision ever matters. +## Doc precision follow-ups + +- **`AGENTS.md` `## Current state` working-tree counts drift.** Reviewer caught `28 M` → actual `29 M` (and `183 total` → `184 total`) within minutes of writing the snapshot. Either refresh on every touch or change the phrasing to "(as of this write)" so the numbers stop reading as authoritative-forever. +- **`AGENTS.md` `### /admin/*` route list is non-exhaustive.** The brace-expanded shortlist silently omits at least three real routes registered in `server/routes/admin.js`: `GET /admin/job-output/:id` (line 268), `GET /admin/output-store-ids` (line 336), and `POST /admin/settings/promote-prompt` (line 1156). Either append them or label the list "(representative, not exhaustive)". + ## Open issues (see docs/issues-backlog.md) - **Empty analysis section at a window boundary** (observed v0.2.77 smoke test). Likely the LLM returning an empty `{title:"",summary:""}` section the stitcher accepts, or a window-merge boundary hole. Low priority. Full triage path in `docs/issues-backlog.md`.