Pluggable AI providers, relay credit system, picker UX overhaul

Captures roughly forty version bumps (v0.2.6 → v0.2.47) of work that
accumulated without commits.

- Pluggable provider system under server/providers/: gemini, anthropic,
  openai, openai-compatible, ollama, whisper-compatible, relay. Mix and
  match transcription + analysis per request via the picker UI.
- Relay backend integration. Hardcoded relay URL in server/relay-default.js
  (operator-controlled at build time, not user-configurable). New
  /api/relay/{status,policy} endpoints proxy to the relay; balance pings
  populate a cached credit display.
- Per-install identity in server/install-id.js for relay credit accounting.
  Sent to the relay as X-Recap-Install-Id; persists across upgrades, lost
  on a full uninstall + reinstall. Not surfaced in the UI.
- Admin login gate (server/admin-auth.js + setAdminPassword action). Scrypt
  password hash + HMAC-signed session cookie.
- Entitlement scheme rename: pro / max (each paired with subscriptions and
  relay_pro / relay_max), replacing the misleading "core" entitlement
  that conflicted with the user-facing "Core" tier name.
- Activation screen: dynamic credit count pulled from /api/relay/policy,
  "Skip — use free mode" button, accurate paid-feature list.
- Top toolbar: inline credit-balance pill (or "BYO configured" fallback),
  Upgrade + "I have a key" buttons.
- Picker UI: per-provider sections with Save/Test/Delete buttons, sections
  collapsible by chevron, default-collapsed unless currently selected,
  "Use comped credits (reset to relay)" link when the user has strayed,
  green hint under inputs whose values are server-configured.
- Activity log: chevron-collapsible groups per video, refresh-survival via
  localStorage + a 500-entry server-side buffer, explicit Clear button.
- YouTube captions fast-path with user toggle (skips audio download + AI
  transcription when captions are available — uncheck for speaker labels).
- Cancel button: AbortController plumbed through every provider SDK call;
  retryAPI short-circuits on AbortError; cancellation events surface in
  the activity log instead of silent retries.
- Long-video analysis: auto-coalesce transcript entries before building the
  analysis prompt so local-model context windows (32k-ish) don't overflow.
  Original entries preserved for transcript display via an index map; the
  analyzer sees a coarser view but click-to-seek timestamps stay precise.
- StartOS action grouping (Setup / AI Providers) so the actions list is
  navigable.
- Manifest description rewritten to reflect multi-provider support and
  free-tier relay credits.
- Smaller fixes: summarize-button enablement no longer requires a Gemini
  key when other providers are configured; analysis fallback chain handles
  context-length and 503 capacity errors; single-segment expansion for
  providers that don't return per-segment timestamps (Parakeet et al.);
  many other UX polish items.
This commit is contained in:
Keysat
2026-05-11 23:46:20 -05:00
parent 2544cf7dde
commit 373d10595b
79 changed files with 6322 additions and 397 deletions
+75
View File
@@ -0,0 +1,75 @@
// Persistent per-install identifier. Generated once on first boot and
// stashed at `<DATA_DIR>/install-id` (typically /data/install-id on
// StartOS). Survives container restarts and Recap upgrades; lost on a
// full uninstall + reinstall.
//
// What it's for: the upcoming relay backend will use this ID as the
// owner of comped/paid relay credits. Without a stable client identity
// the relay can't tell whether a request belongs to a credited install
// or a fresh one. Direct install-ID auth is the v1 choice (see the
// project roadmap discussion) — simple, sufficient for low-count free
// credits, can be hardened later with license-server-minted JWTs.
//
// What it is NOT: a license key. The license system (./license.js) is
// completely separate — license keys are user-facing strings that
// authorize Pro features, while install-IDs are opaque per-install
// UUIDs the relay backend uses for credit accounting.
import fs from "fs/promises";
import path from "path";
import { randomUUID } from "crypto";
let cachedId = null;
// Initialize on boot. Reads the existing ID off disk; if there's no
// file, generates a fresh UUIDv4 and writes it. Subsequent calls to
// getInstallId() return the cached value without touching disk.
//
// `dataDir` must be writable — on StartOS that's /data (the persistent
// volume), on local dev it's the project root.
export async function initInstallId({ dataDir }) {
if (!dataDir) throw new Error("initInstallId: dataDir is required");
const filePath = path.join(dataDir, "install-id");
try {
const raw = await fs.readFile(filePath, "utf8");
const trimmed = raw.trim();
if (isValidInstallId(trimmed)) {
cachedId = trimmed;
console.log(`[install-id] loaded ${redact(cachedId)} from ${filePath}`);
return cachedId;
}
console.warn(
`[install-id] file at ${filePath} contained an invalid value — regenerating`
);
} catch (err) {
if (err.code !== "ENOENT") {
console.warn(`[install-id] read failed (${err.code}): ${err.message}`);
}
}
// No valid file — mint a new one. UUIDv4 is plenty: 122 bits of
// randomness, no collision risk across realistic install counts, and
// it's opaque enough to share over the wire without leaking system
// info (unlike e.g. a machine-id).
const fresh = randomUUID();
await fs.writeFile(filePath, fresh + "\n", { mode: 0o600 });
cachedId = fresh;
console.log(`[install-id] generated ${redact(cachedId)}${filePath}`);
return cachedId;
}
export function getInstallId() {
return cachedId;
}
// Loose UUID shape check — accepts any reasonable UUID-ish string.
// Avoids requiring v4 specifically in case operators want to seed
// non-standard IDs.
function isValidInstallId(s) {
return typeof s === "string" && /^[0-9a-f-]{32,40}$/i.test(s);
}
// Log-safe display: first 8 + last 4 chars only.
function redact(id) {
if (!id || id.length < 12) return "(short)";
return `${id.slice(0, 8)}${id.slice(-4)}`;
}