Workers P2: API-Client mit deutschen Fehlertexten

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
ClaudeandClaude Sonnet 5.5 committed 2026-10-18 01:26:07 +00:00
1 parent 66fe0f38ef
commit 97fdfbfcf0
3 files changed
+269

No files matched your search

+109
View File
@@ -0,0 +1,109 @@
// API-Client der Hustler-Seite (Dienst unter /api/workers, Übersicht §6.2).
// Rein: kein Appwrite-Import, getJwt und fetch werden injiziert (workersClient.js verdrahtet sie), damit die
// deutschen Fehlertexte unter Node testbar sind. Fehler sind Error mit deutscher `message` und `status`.
export const DOWN_TEXT = 'Der Hustler-Dienst ist gerade nicht erreichbar.'
export const BAD_REPLY_TEXT = 'Der Hustler-Dienst antwortet nicht richtig.'
export const SESSION_TEXT = 'Anmeldung abgelaufen. Bitte neu anmelden.'
export const FORBIDDEN_TEXT = 'Dafür fehlt dir die Berechtigung.'
// Pult-Status des Dienstes (Übersicht §4.11)
export const PULT_STATUS = Object.freeze(['offen', 'in_arbeit', 'gesendet', 'verworfen', 'veraltet', 'erledigt'])
// Erlaubte Aktionen am Pult-Eintrag; alles andere wird abgewiesen, bevor ein Pfad entsteht
const PULT_ACTIONS = Object.freeze(['claim', 'sent', 'discard', 'done', 'reply', 'approve-preview', 'paste'])
const BASE = '/api/workers'
const EVENTS_LIMIT = 200
const TRACE_LIMIT = 50
const statusError = (message, status) => Object.assign(new Error(message), { status })
// Query aus Paaren; leere Werte (undefined, null, '') fallen weg, der Rest wird kodiert
function query(pairs) {
const parts = []
for (const [key, value] of pairs) {
if (value === undefined || value === null || value === '') continue
parts.push(`${key}=${encodeURIComponent(value)}`)
}
return parts.length ? `?${parts.join('&')}` : ''
}
// Standard nur bei fehlendem Wert (nicht bei 0); danach auf 1..max geklemmt
function clampLimit(limit, max) {
const n = limit === undefined || limit === null ? NaN : Math.trunc(Number(limit))
if (!Number.isFinite(n)) return max
return Math.min(max, Math.max(1, n))
}
// status: ein Pult-Status, eine Komma-Liste oder ein Array; leer = 'offen'; jeder Eintrag (getrimmt) muss bekannt sein
function pultStatusList(status) {
const raw = Array.isArray(status) ? status : String(status ?? '').split(',')
const list = raw.map((s) => String(s).trim())
if (list.length === 0 || (list.length === 1 && list[0] === '')) return ['offen']
for (const s of list) {
if (!PULT_STATUS.includes(s)) throw statusError(`Unbekannter Pult-Status: ${s}`, 400)
}
return list
}
export function createWorkersApi({ getJwt, fetchImpl = (...args) => globalThis.fetch(...args) }) {
// fetch wirft (Netz, Dienst aus) ⇒ DOWN_TEXT mit status 0
async function send(path, method, body, jwt) {
const headers = { 'X-Appwrite-JWT': jwt }
if (body !== undefined) headers['Content-Type'] = 'application/json'
try {
return await fetchImpl(`${BASE}${path}`, { method, headers, body: body === undefined ? undefined : JSON.stringify(body) })
} catch {
throw statusError(DOWN_TEXT, 0)
}
}
async function jwtFor(options) {
try {
return await getJwt(options)
} catch {
throw statusError(SESSION_TEXT, 401)
}
}
async function request(path, { method = 'GET', body } = {}) {
let res = await send(path, method, body, await jwtFor())
if (res.status === 401) {
// einmal mit frischem JWT wiederholen, danach gilt die Sitzung als abgelaufen
res = await send(path, method, body, await jwtFor({ fresh: true }))
if (res.status === 401) throw statusError(SESSION_TEXT, 401)
}
let data
try {
data = await res.json()
} catch {
// keine JSON-Antwort: Fehlerseite des Proxys (502/503/504) bzw. index.html von nginx statt der API
const gatewayDown = res.status === 502 || res.status === 503 || res.status === 504
throw statusError(gatewayDown ? DOWN_TEXT : BAD_REPLY_TEXT, res.status)
}
if (!res.ok) {
const info = data && typeof data === 'object' ? data : {}
const message = typeof info.error === 'string' && info.error ? info.error : null
if (message) throw statusError(message, res.status)
throw statusError(res.status === 403 ? FORBIDDEN_TEXT : `Hustler-Fehler ${res.status}`, res.status)
}
return data ?? {}
}
return {
state: () => request('/state'),
events: (after, limit = EVENTS_LIMIT) => request(`/events${query([['after', after], ['limit', clampLimit(limit, EVENTS_LIMIT)]])}`),
trace: (workerId, limit = TRACE_LIMIT) => request(`/worker/${encodeURIComponent(workerId)}/trace${query([['limit', clampLimit(limit, TRACE_LIMIT)]])}`),
pause: (paused) => request('/pause', { method: 'POST', body: { paused: Boolean(paused) } }),
settings: () => request('/settings'),
saveSettings: (patch) => request('/settings', { method: 'PUT', body: patch }),
// Das Komma bleibt unkodiert: die Einträge bestehen nur aus Kleinbuchstaben und _ (PULT_STATUS)
pult: async (status = 'offen') => request(`/pult?status=${pultStatusList(status).join(',')}`),
pultAction: async (id, action, body) => {
if (!PULT_ACTIONS.includes(action)) throw statusError(`Unbekannte Aktion: ${action}`, 400)
return request(`/pult/${encodeURIComponent(id)}/${action}`, { method: 'POST', body })
},
botleads: ({ room, q, page, id } = {}) => request(`/botleads${query([['room', room], ['q', q], ['page', page], ['id', id]])}`),
prospect: (id) => request(`/prospect/${encodeURIComponent(id)}`),
}
}
+14
View File
@@ -0,0 +1,14 @@
import { account } from './appwrite'
import { createJwtCache } from './serverpulsAuth'
import { createWorkersApi } from './workersApi'
// Verdrahtung des Hustler-API-Clients mit der Appwrite-Sitzung (wird nicht unter Node geladen, wie serverpulsApi.js).
// JWT 10 Min. gecacht (Appwrite begrenzt das Erzeugen pro Nutzer); `fresh` verwirft den Cache für die 401-Wiederholung.
const jwtCache = createJwtCache({ createJWT: () => account.createJWT() })
const getJwt = (options) => {
if (options?.fresh) jwtCache.invalidate()
return jwtCache.get()
}
export const workersApi = createWorkersApi({ getJwt })