Setzt den in #84 skizzierten Workflow um: record.html nimmt im Handy-Browser eine Sprachnotiz per MediaRecorder auf und laedt sie unauthentifiziert zu POST /api/v1/agents/{id}/recordings hoch. Ein Klick auf die Personen-Figur jedes registrierten Agenten in der Buero-Visualisierung verlinkt dorthin, ein Zaehler am Schreibtisch zeigt offene Aufnahmen. Der Agent selbst holt seine Warteschlange ueber die bestehenden Api-Key-authentifizierten Endpunkte ab (GET .../recordings, GET /recordings/{id}/audio) und markiert eine Aufnahme nach der Transkription per PATCH als erledigt - die eigentliche Transkription (whisper.cpp/faster-whisper) bleibt agent-seitig, siehe fds2610/Agent#17. Schliesst #85
247 lines
9.2 KiB
JavaScript
247 lines
9.2 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;
|
|
}
|
|
|
|
/**
|
|
* Issue #85: record.html zeigt den Namen des Agenten, fuer den gerade aufgenommen wird - holt
|
|
* dafuer einfach die volle (kleine) Agentenliste und filtert lokal, ein eigener
|
|
* GET-/agents/{id}-Endpunkt existiert nicht.
|
|
*/
|
|
export async function fetchAgent(agentId) {
|
|
const agents = await fetchAgents();
|
|
return agents.find((agent) => String(agent.id) === String(agentId)) ?? null;
|
|
}
|
|
|
|
/**
|
|
* Verlinkt von der Buero-Szene aus (Klick auf die Personen-Figur eines registrierten Agenten,
|
|
* Issue #85) auf die Aufnahme-Webseite. record.html liegt neben office.html im selben
|
|
* Verzeichnis, daher ein rein relativer Pfad statt ueber apiBase() (record.html spricht die
|
|
* API zwar auch an, ist selbst aber keine API-Route).
|
|
*/
|
|
export function recordingUrl(agentId) {
|
|
return `record.html?agent=${encodeURIComponent(agentId)}`;
|
|
}
|
|
|
|
/**
|
|
* Laedt eine im Browser aufgenommene Sprachnotiz hoch (Issue #85). Unauthentifiziert wie der
|
|
* Endpunkt selbst - record.html laeuft im Handy-Browser eines Menschen, nicht eines
|
|
* registrierten Agenten, ohne Api-/Admin-Key. FormData statt JSON, weil die Audiodatei ein
|
|
* Blob ist; der Browser setzt den multipart-Content-Type inkl. Boundary selbst, ein manuell
|
|
* gesetzter Header wuerde die Boundary kaputt machen.
|
|
*/
|
|
export async function uploadRecording(agentId, blob, filename) {
|
|
const formData = new FormData();
|
|
formData.append("audio", blob, filename);
|
|
|
|
const response = await fetch(`${apiBase()}/agents/${encodeURIComponent(agentId)}/recordings`, {
|
|
method: "POST",
|
|
body: formData,
|
|
});
|
|
|
|
if (!response.ok) {
|
|
const payload = await response.json().catch(() => null);
|
|
throw new Error(payload?.error || `Upload fehlgeschlagen: HTTP ${response.status}`);
|
|
}
|
|
return response.json();
|
|
}
|
|
|
|
/**
|
|
* 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);
|
|
}
|
|
|
|
/**
|
|
* Issue #53: Prueft nach dem Anlegen/Bearbeiten eines Projekts, ob fds2610-bot dort
|
|
* Schreibrechte hat und ob der Webhook korrekt eingerichtet ist.
|
|
*
|
|
* @returns {Promise<{write_access: {ok: boolean, message: string}, webhook: {ok: boolean, message: string}}>}
|
|
*/
|
|
export async function checkRepoConfig(id, adminKey) {
|
|
const { checks } = await adminJson("POST", `/repo-configs/${encodeURIComponent(id)}/check`, undefined, adminKey);
|
|
return checks;
|
|
}
|
|
|
|
/**
|
|
* Issue #51: "Board neu einlesen"-Knopf am Kanban-Board - loescht serverseitig alle Tickets
|
|
* und liest alle konfigurierten Codeberg-Projekte synchron neu ein. Braucht wie die
|
|
* Projekte-Verwaltung den Admin-Key, weil ein Voll-Reset des Boards potenziell teuer
|
|
* (mehrere Codeberg-Requests) und kein normaler Agenten-Vorgang ist.
|
|
*
|
|
* @returns {Promise<Array<object>>} der neue Board-Zustand.
|
|
*/
|
|
export async function resyncTickets(adminKey) {
|
|
const { tickets } = await adminJson("POST", "/tickets/resync", undefined, adminKey);
|
|
return tickets;
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------------------
|
|
// Hosts der Ping-Ueberwachung (Issue #70: Zahnrad am Wandmonitor "Systeme" oeffnet ein Modal,
|
|
// um die hosts.conf des Ping-Rechners ueber die GUI zu pflegen). Schreibend hinter demselben
|
|
// Admin-Key wie die Projekte-Verwaltung; der Ping-Check selbst holt seine Liste dagegen mit
|
|
// seinem Agenten-Key ab (GET /hosts/conf, siehe check_fds.sh) - das macht der Browser nicht.
|
|
// ---------------------------------------------------------------------------------------
|
|
|
|
export async function fetchHostConfigs() {
|
|
const { hosts } = await getJson("/hosts");
|
|
return hosts;
|
|
}
|
|
|
|
export async function createHostConfig(data, adminKey) {
|
|
const { host } = await adminJson("POST", "/hosts", data, adminKey);
|
|
return host;
|
|
}
|
|
|
|
export async function updateHostConfig(id, data, adminKey) {
|
|
const { host } = await adminJson("PATCH", `/hosts/${encodeURIComponent(id)}`, data, adminKey);
|
|
return host;
|
|
}
|
|
|
|
export async function deleteHostConfig(id, adminKey) {
|
|
await adminJson("DELETE", `/hosts/${encodeURIComponent(id)}`, undefined, adminKey);
|
|
}
|
|
|
|
/**
|
|
* Entfernt eine Kachel aus dem System-Monitor. Fuer Systeme, die zu keinem hier gepflegten
|
|
* Host gehoeren - etwa weil sie sich frueher selbst per REST gemeldet haben und das nicht
|
|
* mehr tun. Ein Host aus der Ping-Ueberwachung nimmt seine Kachel dagegen beim Loeschen
|
|
* selbst mit (serverseitig, siehe HostConfigController::delete).
|
|
*/
|
|
export async function deleteSystem(id, adminKey) {
|
|
await adminJson("DELETE", `/systems/${encodeURIComponent(id)}`, undefined, adminKey);
|
|
}
|