Add admin web UI (Phase 2): spend dashboard, media gallery, provider management

Fastify + single-page frontend on shared @nexusai/core. Reads the same SQLite
journal and media dir. Features: usage stats (by provider/capability/day),
content gallery (image/audio/video with local file serving + path-traversal
guard), provider CRUD with encrypted keys (masked in UI), enable/disable,
set-default, and real key validation via zero-cost /media/models call.
Adds Journal.getById and NordRouterMediaProvider.checkKey to core.
This commit is contained in:
OpenCode
2026-08-17 17:22:07 +07:00
parent 62c692c5a1
commit 679e56424b
13 changed files with 1495 additions and 5 deletions
+12
View File
@@ -79,6 +79,18 @@ export class Journal {
}
}
getById(id: string): GenerationRecord | undefined {
const row = this.db
.prepare(
`SELECT id, provider, capability, model, input_summary as inputSummary, status,
cost_usd as costUsd, file_path as filePath, result_url as resultUrl,
error, duration_ms as durationMs, created_at as createdAt
FROM generations WHERE id = ?`
)
.get(id);
return row as GenerationRecord | undefined;
}
list(limit = 50, offset = 0, capability?: string): GenerationRecord[] {
const where = capability ? 'WHERE capability = ?' : '';
const params = capability ? [capability, limit, offset] : [limit, offset];
@@ -147,6 +147,16 @@ export class NordRouterMediaProvider implements MediaProvider {
return { ref: res, url: res.url };
}
/** Validate the API key with a zero-cost authenticated call. */
async checkKey(): Promise<{ ok: boolean; modelsCount?: number; error?: string }> {
try {
const models = await this.listModels();
return { ok: true, modelsCount: models.length };
} catch (err) {
return { ok: false, error: err instanceof Error ? err.message : String(err) };
}
}
async listModels(): Promise<ModelInfo[]> {
const now = Date.now();
if (this.modelsCache && now - this.modelsCache.at < this.modelsTtlMs) {