initial relay scaffold
This commit is contained in:
@@ -0,0 +1,102 @@
|
||||
// /admin/* — operator dashboard endpoints. All require the admin
|
||||
// session cookie (enforced by admin-auth middleware).
|
||||
//
|
||||
// v0.1 endpoints (JSON only; v0.2 will add an HTML dashboard):
|
||||
// GET /admin/usage — all install rows + last-month aggregates
|
||||
// GET /admin/config — current operator config (sans password hash)
|
||||
// POST /admin/quotas — adjust tier quotas live (mirror of StartOS
|
||||
// action but reachable from the dashboard)
|
||||
|
||||
import express from "express";
|
||||
import { getConfigSnapshot } from "../config.js";
|
||||
import { snapshotAll } from "../credits.js";
|
||||
import { snapshotCache } from "../keysat-client.js";
|
||||
import { snapshotJobs } from "../job-credits.js";
|
||||
import fs from "fs/promises";
|
||||
import path from "path";
|
||||
|
||||
export function adminRouter({ dataDir }) {
|
||||
const router = express.Router();
|
||||
|
||||
router.get("/usage", async (_req, res) => {
|
||||
const rows = snapshotAll();
|
||||
res.json({
|
||||
installs: rows.length,
|
||||
rows,
|
||||
});
|
||||
});
|
||||
|
||||
router.get("/config", async (_req, res) => {
|
||||
const cfg = await getConfigSnapshot();
|
||||
// Strip secrets before exposing to the dashboard.
|
||||
const safe = {
|
||||
keysat_base_url: cfg.relay_keysat_base_url,
|
||||
parakeet_base_url: cfg.relay_parakeet_base_url,
|
||||
gemma_base_url: cfg.relay_gemma_base_url,
|
||||
gemini_configured: !!cfg.relay_gemini_api_key,
|
||||
admin_username: cfg.relay_admin_username,
|
||||
tier_quotas: tryParse(cfg.relay_tier_quotas_json),
|
||||
};
|
||||
res.json(safe);
|
||||
});
|
||||
|
||||
router.get("/license-cache", async (_req, res) => {
|
||||
res.json({ entries: snapshotCache() });
|
||||
});
|
||||
|
||||
router.get("/jobs", async (_req, res) => {
|
||||
res.json({ entries: snapshotJobs() });
|
||||
});
|
||||
|
||||
// Adjust the live quotas blob. Same shape the StartOS action writes
|
||||
// to relay_tier_quotas_json — kept here so the dashboard can tune
|
||||
// quotas without round-tripping the StartOS UI.
|
||||
router.post("/quotas", express.json(), async (req, res) => {
|
||||
const incoming = req.body || {};
|
||||
const normalized = {
|
||||
core: {
|
||||
lifetime: numOrNull(incoming?.core?.lifetime, 5),
|
||||
monthly: numOrNull(incoming?.core?.monthly, null),
|
||||
geminiCapMonthly: numOrNull(incoming?.core?.geminiCapMonthly, null),
|
||||
},
|
||||
pro: {
|
||||
lifetime: numOrNull(incoming?.pro?.lifetime, null),
|
||||
monthly: numOrNull(incoming?.pro?.monthly, 50),
|
||||
geminiCapMonthly: numOrNull(incoming?.pro?.geminiCapMonthly, 25),
|
||||
},
|
||||
max: {
|
||||
lifetime: numOrNull(incoming?.max?.lifetime, null),
|
||||
monthly: numOrNull(incoming?.max?.monthly, null),
|
||||
geminiCapMonthly: numOrNull(incoming?.max?.geminiCapMonthly, 50),
|
||||
},
|
||||
};
|
||||
// Write directly into relay-config.json — the live-reloader picks
|
||||
// it up on the next read.
|
||||
const configPath = path.join(dataDir, "config", "relay-config.json");
|
||||
let existing = {};
|
||||
try {
|
||||
existing = JSON.parse(await fs.readFile(configPath, "utf8"));
|
||||
} catch {}
|
||||
existing.relay_tier_quotas_json = JSON.stringify(normalized);
|
||||
await fs.mkdir(path.dirname(configPath), { recursive: true });
|
||||
await fs.writeFile(configPath, JSON.stringify(existing), { mode: 0o600 });
|
||||
res.json({ ok: true, quotas: normalized });
|
||||
});
|
||||
|
||||
return router;
|
||||
}
|
||||
|
||||
function numOrNull(v, fallback) {
|
||||
if (v === null) return null;
|
||||
const n = Number(v);
|
||||
if (Number.isFinite(n)) return n;
|
||||
return fallback;
|
||||
}
|
||||
|
||||
function tryParse(s) {
|
||||
try {
|
||||
return JSON.parse(s);
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user