diff --git a/server/routes/admin.js b/server/routes/admin.js index 82ecb20..d4fef7a 100644 --- a/server/routes/admin.js +++ b/server/routes/admin.js @@ -5,7 +5,7 @@ import { Router } from "express"; import { one, query } from "../db.js"; import { requireAdmin, signAdminToken } from "../auth.js"; import { getPlan } from "../plans.js"; -import { env } from "../env.js"; +import { env, provisioningMock } from "../env.js"; export const adminRouter = Router(); @@ -29,6 +29,8 @@ adminRouter.get("/stats", async (_req, res) => { const instances = await one("select count(*)::int n from instances"); const activeInst = await one("select count(*)::int n from instances where status = 'active'"); const suspendedInst = await one("select count(*)::int n from instances where status = 'suspended'"); + // Zahlungsprobleme: Abos in past_due (fehlgeschlagene Zahlung) — braucht Aufmerksamkeit. + const pastDue = await one("select count(*)::int n from subscriptions where status = 'past_due'"); // MRR + Verteilung nach Plan (nur aktive Abos). const { rows: subs } = await query("select plan from subscriptions where status = 'active'"); @@ -147,6 +149,47 @@ adminRouter.get("/accounts/:id/instances", async (req, res) => { res.json({ instances: rows }); }); +// — Health-Check aller aktiven Instanzen — +// Pingt jede Instanz-URL (HEAD, kurzes Timeout) und meldet up/down. Im +// MOCK-Modus (provisioningMock) sind die URLs synthetisch → Status "unknown" +// statt fake "up", damit das Cockpit ehrlich bleibt. +adminRouter.get("/health", async (_req, res) => { + const { rows } = await query( + "select id, studio_slug, label, instance_url, status from instances where status = 'active'" + ); + + if (provisioningMock) { + return res.json({ + mock: true, + checked: 0, + instances: rows.map((i) => ({ ...i, health: "unknown" })), + }); + } + + async function ping(url) { + const ctrl = new AbortController(); + const t = setTimeout(() => ctrl.abort(), 4000); + try { + const r = await fetch(url, { method: "HEAD", signal: ctrl.signal, redirect: "manual" }); + return r.status < 500 ? "up" : "down"; + } catch { + return "down"; + } finally { + clearTimeout(t); + } + } + + const checked = await Promise.all( + rows.map(async (i) => ({ ...i, health: await ping(i.instance_url) })) + ); + res.json({ + mock: false, + checked: checked.length, + down: checked.filter((i) => i.health === "down").length, + instances: checked, + }); +}); + // — Instanz sperren / reaktivieren — adminRouter.post("/instances/:id/:action", async (req, res) => { const { id, action } = req.params;