monitor/html/js/office-api.js
fds2610-bot 79ccef099d Hooks-KPI im KPI-Monitor ergaenzen (Issue #38)
Die im Issue beschriebene Event-Anzeige (neuestes Event oben, aeltere
schieben nach, Ueberschuss wird abgeschnitten) ist bereits seit Phase 4
umgesetzt (buildEventLines()/MAX_EVENT_LINES, F5) und braucht keine
Aenderung.

Neu ist die vierte, immer sichtbare KPI-Kachel ganz rechts im
KPI-Monitor: "Hooks: n" zaehlt die seit dem letzten Start des
api-Containers empfangenen Webhooks. Der Zaehler ist dateibasiert
(Metrics\HookCounter), weil der eingebaute PHP-Server jede Anfrage als
eigene Skriptausfuehrung behandelt; docker/entrypoint.sh setzt die Datei
beim Start des api-Containers (nicht des sync-Containers) zurueck. Neuer
oeffentlicher Endpunkt GET /api/v1/webhooks/stats liefert den Stand,
das Frontend haengt ihn als eigene Kachel an (buildHooksKpi()) und
reserviert ihr dafuer eine feste Kachel statt sie der MAX_KPI_TILES-
Kappung der uebrigen KPIs zu unterwerfen. Der Wert fliesst zusaetzlich
in den SSE-Fingerabdruck (ChangeFeed) ein.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-04 13:18:05 +02:00

144 lines
4.9 KiB
JavaScript

// Duenne fetch()-Wrapper fuer die Phase-1-REST-API (siehe API.md). Reine Daten-Ebene, kein
// DOM-Zugriff - office-render.js ruft diese Funktionen auf und reicht die Ergebnisse an die
// Layoutlogik (office-scene.js) weiter. Basis-URL ist per window.MONITOR_API_BASE
// ueberschreibbar - gesetzt wird sie zur Laufzeit von /config.js, das der Webserver aus der
// Umgebungsvariable MONITOR_API_BASE erzeugt (siehe .env.example und
// docker/nginx/default.conf.template). Noetig, weil Frontend und API auf unterschiedlichen
// Origins laufen koennen (Dev: web :8090, api :8091); die API liefert dafuer CORS-Header,
// siehe api/src/Http/Response.php.
const DEFAULT_API_BASE = "/api/v1";
function apiBase() {
const configured = typeof window !== "undefined" ? window.MONITOR_API_BASE : null;
// Leer = gleicher Origin. Der startsWith-Check faengt einen nicht ersetzten Platzhalter
// ab (z.B. wenn config.js von einem anderen Webserver ohne Substitution ausgeliefert
// wird) - der waere sonst eine truthy Muell-URL.
if (!configured || configured.startsWith("${")) {
return DEFAULT_API_BASE;
}
return configured;
}
async function getJson(path) {
const response = await fetch(`${apiBase()}${path}`, { headers: { Accept: "application/json" } });
if (!response.ok) {
throw new Error(`GET ${path} fehlgeschlagen: HTTP ${response.status}`);
}
return response.json();
}
export async function fetchTickets() {
const { tickets } = await getJson("/tickets");
return tickets;
}
export async function fetchEvents(limit = 200) {
const { events } = await getJson(`/events?limit=${encodeURIComponent(limit)}`);
return events;
}
export async function fetchSystems() {
const { systems } = await getJson("/systems");
return systems;
}
export async function fetchKpis() {
const { kpis } = await getJson("/kpis");
return kpis;
}
export async function fetchAgents() {
const { agents } = await getJson("/agents");
return agents;
}
/**
* Hooks-KPI (Issue #38): Anzahl der seit dem letzten Start des api-Containers empfangenen
* Webhooks, siehe API.md "GET /api/v1/webhooks/stats". Anders als die anderen fetch*()-
* Funktionen kein eigener Datenbereich der Buero-Szene, sondern nur die Datenquelle einer
* einzelnen KPI-Monitor-Kachel (buildHooksKpi() in office-scene.js).
*/
export async function fetchWebhookStats() {
return getJson("/webhooks/stats");
}
/**
* Baut die Stream-URL fuer den SSE-Push-Endpoint (F13, Phase 4). Wird von office-realtime.js
* an EventSource() uebergeben.
*/
export function streamUrl() {
return `${apiBase()}/stream`;
}
// ---------------------------------------------------------------------------------------
// Projekte des Codeberg-Sync (Issue #27: Klick auf den PO oeffnet ein Modal, um Projekte
// hinzuzufuegen/zu bearbeiten/zu loeschen). Schreibende Endpunkte verlangen X-Admin-Key
// (siehe API.md "Authentifizierung") - anders als der Agenten-/Ticket-API-Key gibt es dafuer
// keine Registrierung im Browser, der Admin-Key wird daher einmalig manuell eingegeben und
// im lokalen Speicher gemerkt (siehe office-render.js openRepoConfigModal()).
const ADMIN_KEY_STORAGE_KEY = "monitor.adminKey";
export function getStoredAdminKey() {
try {
return window.localStorage.getItem(ADMIN_KEY_STORAGE_KEY) || "";
} catch {
// localStorage kann im Privat-/Inkognito-Modus blockiert sein - dann muss der Admin-Key
// bei jeder Aenderung erneut eingegeben werden, kein Hard-Fail.
return "";
}
}
export function setStoredAdminKey(key) {
try {
if (key) {
window.localStorage.setItem(ADMIN_KEY_STORAGE_KEY, key);
} else {
window.localStorage.removeItem(ADMIN_KEY_STORAGE_KEY);
}
} catch {
// s.o.
}
}
async function adminJson(method, path, body, adminKey) {
const response = await fetch(`${apiBase()}${path}`, {
method,
headers: {
Accept: "application/json",
"Content-Type": "application/json",
"X-Admin-Key": adminKey,
},
body: body === undefined ? undefined : JSON.stringify(body),
});
if (response.status === 401) {
const error = new Error("Admin-Key ungueltig oder fehlend.");
error.isAuthError = true;
throw error;
}
if (!response.ok) {
const payload = await response.json().catch(() => null);
throw new Error(payload?.error || `${method} ${path} fehlgeschlagen: HTTP ${response.status}`);
}
return response.status === 204 ? null : response.json();
}
export async function fetchRepoConfigs() {
const { repos } = await getJson("/repo-configs");
return repos;
}
export async function createRepoConfig(data, adminKey) {
const { repo } = await adminJson("POST", "/repo-configs", data, adminKey);
return repo;
}
export async function updateRepoConfig(id, data, adminKey) {
const { repo } = await adminJson("PATCH", `/repo-configs/${encodeURIComponent(id)}`, data, adminKey);
return repo;
}
export async function deleteRepoConfig(id, adminKey) {
await adminJson("DELETE", `/repo-configs/${encodeURIComponent(id)}`, undefined, adminKey);
}