feat(admin): Zahlungsausfaelle + Instanz-Health-Check
- /admin/stats: pastDueSubscriptions (Abos mit fehlgeschlagener Zahlung) - /admin/health: pingt aktive Instanz-URLs (HEAD, 4s Timeout) -> up/down; im MOCK-Modus ehrlich 'unknown' statt fake 'up' E2E: past_due fliesst in stats, health gibt im Mock 'unknown'. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
+44
-1
@@ -5,7 +5,7 @@ import { Router } from "express";
|
|||||||
import { one, query } from "../db.js";
|
import { one, query } from "../db.js";
|
||||||
import { requireAdmin, signAdminToken } from "../auth.js";
|
import { requireAdmin, signAdminToken } from "../auth.js";
|
||||||
import { getPlan } from "../plans.js";
|
import { getPlan } from "../plans.js";
|
||||||
import { env } from "../env.js";
|
import { env, provisioningMock } from "../env.js";
|
||||||
|
|
||||||
export const adminRouter = Router();
|
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 instances = await one("select count(*)::int n from instances");
|
||||||
const activeInst = await one("select count(*)::int n from instances where status = 'active'");
|
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'");
|
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).
|
// MRR + Verteilung nach Plan (nur aktive Abos).
|
||||||
const { rows: subs } = await query("select plan from subscriptions where status = 'active'");
|
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 });
|
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 —
|
// — Instanz sperren / reaktivieren —
|
||||||
adminRouter.post("/instances/:id/:action", async (req, res) => {
|
adminRouter.post("/instances/:id/:action", async (req, res) => {
|
||||||
const { id, action } = req.params;
|
const { id, action } = req.params;
|
||||||
|
|||||||
Reference in New Issue
Block a user