Add developer API, Prometheus metrics, and community links

This commit is contained in:
2026-10-12 19:59:37 +05:00
parent 3cc3136762
commit 018be5049e
50 changed files with 4087 additions and 31 deletions

View File

@@ -0,0 +1,271 @@
"use client";
import { useEffect, useState } from "react";
import { Activity, Copy, Download } from "lucide-react";
import { api } from "@/lib/client-api";
import { prometheusConfig, type PrometheusStatus } from "@/shared/prometheus";
import { grafanaDashboard } from "@/shared/grafana-dashboard";
import { useI18n } from "./I18nProvider";
export function PrometheusIntegration() {
const { t, locale } = useI18n();
const [status, setStatus] = useState<PrometheusStatus | null>(null);
const [token, setToken] = useState<string | null>(null);
const [busy, setBusy] = useState(false);
const [error, setError] = useState("");
const [message, setMessage] = useState("");
const [confirm, setConfirm] = useState<"rotate" | "disable" | null>(null);
async function load() {
try {
setStatus(await api<PrometheusStatus>("moderation/prometheus"));
setError("");
} catch (e) {
setError((e as Error).message);
}
}
useEffect(() => {
void load();
}, []);
async function configure(action: "rotate" | "disable") {
setBusy(true);
setError("");
setMessage("");
try {
const result = await api<PrometheusStatus & { token: string | null }>(
"moderation/prometheus",
{ action },
);
const { token: nextToken, ...nextStatus } = result;
setStatus(nextStatus);
setToken(nextToken);
setConfirm(null);
setMessage(
action === "rotate"
? "New metrics token generated. Copy the config now; it cannot be recovered later."
: "Metrics access disabled. Existing scrape credentials no longer work.",
);
} catch (e) {
setError((e as Error).message);
} finally {
setBusy(false);
}
}
async function copy(value: string) {
try {
await navigator.clipboard.writeText(value);
setMessage("Copied to clipboard.");
setError("");
} catch {
setError("Could not copy. Select the text and copy it manually.");
}
}
function dashboard() {
const url = URL.createObjectURL(
new Blob([JSON.stringify(grafanaDashboard(), null, 2)], {
type: "application/json",
}),
);
const link = document.createElement("a");
link.href = url;
link.download = "minegeo-grafana-dashboard.json";
link.click();
setTimeout(() => URL.revokeObjectURL(url), 1000);
}
const config =
status && token && status.endpoint.startsWith("https:")
? prometheusConfig(status.endpoint, token)
: "";
return (
<section className="tool-card" aria-labelledby="prometheus-title">
<div className="settings-heading">
<Activity size={24} aria-hidden="true" />
<h2 id="prometheus-title">{t("Prometheus & Grafana")}</h2>
</div>
<p>
{t(
"Track games, maps, multiplayer, uploads, and reports in your own Grafana dashboard. Admin permission is required to manage access.",
)}
</p>
<p className="muted">
{t(
"Only aggregate counts are exported. Usernames, messages, emails, and map identifiers are excluded.",
)}
</p>
{!status && !error && <p role="status">{t("Loading metrics setup…")}</p>}
{status && (
<>
<p>
<span className="badge">
{status.enabled ? t("Metrics enabled") : t("Metrics disabled")}
</span>
</p>
<label className="field">
{t("Metrics endpoint")}
<input readOnly value={status.endpoint} />
</label>
<button className="button" onClick={() => void copy(status.endpoint)}>
<Copy size={16} aria-hidden="true" />
{t("Copy endpoint")}
</button>
<p className="muted">
{t("Last successful scrape: {time}", {
time: status.lastScrapedAt
? new Date(status.lastScrapedAt).toLocaleString(
locale === "ru" ? "ru-RU" : "en-GB",
)
: t("Not scraped yet"),
})}
</p>
{!status.endpoint.startsWith("https:") && (
<p className="error-message">
{t(
"Use the hosted HTTPS site to generate a production scrape config.",
)}
</p>
)}
<ol className="metrics-steps">
<li>
{t(
"Generate a token below, then copy the config into prometheus.yml. If scrape_configs already exists, add only the MineGeo job beneath it.",
)}
</li>
<li>
{t(
"Reload or restart Prometheus. Open its Targets page and check that the minegeo job is UP.",
)}
</li>
<li>
{t(
"In Grafana, add your Prometheus server as a data source. Import the downloaded dashboard JSON and select that data source.",
)}
</li>
</ol>
<p className="muted">
{t(
"History starts with the first scrape. Counts are cached for 30 seconds; room states are not online presence. This integration does not export page views or Vercel CPU statistics.",
)}
</p>
<div className="inline-actions">
<button
className="button primary"
disabled={busy}
onClick={() =>
status.enabled ? setConfirm("rotate") : void configure("rotate")
}
>
{status.enabled
? t("Rotate metrics token")
: t("Generate metrics token")}
</button>
{status.enabled && (
<button
className="button"
disabled={busy}
onClick={() => setConfirm("disable")}
>
{t("Disable metrics access")}
</button>
)}
<button className="button" onClick={dashboard}>
<Download size={16} aria-hidden="true" />
{t("Download Grafana dashboard")}
</button>
<button
className="text-link"
disabled={busy}
onClick={() => void load()}
>
{t("Refresh status")}
</button>
</div>
{confirm && (
<div
className="token-box"
role="group"
aria-label={t("Confirm metrics access change")}
>
<p>
{confirm === "rotate"
? t(
"Rotating immediately invalidates the old token. Update Prometheus with the new config afterward.",
)
: t(
"Disabling immediately stops all authenticated scrapes. Prometheus will report this target as DOWN.",
)}
</p>
<div className="inline-actions">
<button
className="button"
disabled={busy}
onClick={() => void configure(confirm)}
>
{t("Confirm change")}
</button>
<button
className="button"
disabled={busy}
onClick={() => setConfirm(null)}
>
{t("Cancel")}
</button>
</div>
</div>
)}
{token && (
<div className="token-box">
<p>
{t(
"Keep this config private. The token is shown once and grants read-only access to aggregate metrics. Rotating or disabling it revokes access.",
)}
</p>
{config ? (
<>
<label className="field">
{t("Prometheus scrape config")}
<textarea
className="metrics-config"
readOnly
spellCheck={false}
value={config}
rows={15}
/>
</label>
<button className="button" onClick={() => void copy(config)}>
<Copy size={16} aria-hidden="true" />
{t("Copy Prometheus config")}
</button>
</>
) : (
<label className="field">
{t("Metrics token")}
<textarea readOnly value={token} />
</label>
)}
<button
className="text-link"
onClick={() => {
setToken(null);
setMessage("");
}}
>
{t("Hide generated credentials")}
</button>
</div>
)}
{status.enabled && !token && (
<p className="muted">
{t(
"The existing token cannot be retrieved. Use your saved config, or rotate the token to generate a new one.",
)}
</p>
)}
</>
)}
{error && (
<p className="error-message" role="alert">
{t(error)}
</p>
)}
{message && <p role="status">{t(message)}</p>}
</section>
);
}