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:
@@ -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)}`;
|
||||
}
|
||||
Reference in New Issue
Block a user