diff --git a/backend/src/index.ts b/backend/src/index.ts index e6830b3..0c43144 100644 --- a/backend/src/index.ts +++ b/backend/src/index.ts @@ -61,6 +61,7 @@ import {initMailer} from "./utils/mailer" import {initS3} from "./utils/s3"; import { runBootstrap } from "./modules/bootstrap.service"; import { startMatrixPushWorker } from "./modules/matrix-push-worker.service"; +import { startCentralServicesHeartbeat } from "./modules/central-services-heartbeat.service"; //Services @@ -87,6 +88,7 @@ async function main() { await app.register(servicesPlugin); await runBootstrap(app); startMatrixPushWorker(app); + startCentralServicesHeartbeat(app); app.addHook('preHandler', (req, reply, done) => { console.log(req.method) diff --git a/backend/src/modules/central-services-heartbeat.service.ts b/backend/src/modules/central-services-heartbeat.service.ts new file mode 100644 index 0000000..096278b --- /dev/null +++ b/backend/src/modules/central-services-heartbeat.service.ts @@ -0,0 +1,55 @@ +import type { FastifyInstance } from "fastify" + +import { centralServicesClient } from "./push-server.client" +import { secrets } from "../utils/secrets" + +const DEFAULT_HEARTBEAT_INTERVAL_MS = 5 * 60 * 1000 + +function instanceBaseUrl() { + if (!secrets.API_BASE_URL) return undefined + try { + return new URL(secrets.API_BASE_URL).origin + } catch { + console.warn("⚠️ Central Services Heartbeat: API_BASE_URL ist keine gültige URL") + return undefined + } +} + +export function startCentralServicesHeartbeat(server: FastifyInstance) { + if (!centralServicesClient.configured()) { + console.log("ℹ️ Central Services Heartbeat deaktiviert: Instanzzugangsdaten fehlen") + return + } + + const configuredInterval = Number(secrets.FEDEO_CENTRAL_HEARTBEAT_INTERVAL_MS) + const intervalMs = Number.isFinite(configuredInterval) && configuredInterval >= 30_000 + ? configuredInterval + : DEFAULT_HEARTBEAT_INTERVAL_MS + let timer: NodeJS.Timeout | undefined + let stopped = false + + const sendHeartbeat = async () => { + try { + const response = await centralServicesClient.heartbeat({ + fedeoVersion: secrets.FEDEO_VERSION || "selfhost", + baseUrl: instanceBaseUrl(), + }) + console.log(`✅ Central Services Heartbeat: ${response.instanceId} (${response.status})`) + } catch (error: any) { + console.error(`❌ Central Services Heartbeat fehlgeschlagen: ${error?.message || error}`) + } finally { + if (!stopped) { + timer = setTimeout(() => void sendHeartbeat(), intervalMs) + timer.unref() + } + } + } + + server.addHook("onClose", async () => { + stopped = true + if (timer) clearTimeout(timer) + }) + + console.log(`ℹ️ Central Services Heartbeat gestartet (${Math.round(intervalMs / 1000)} Sekunden)`) + void sendHeartbeat() +} diff --git a/backend/src/modules/push-server.client.ts b/backend/src/modules/push-server.client.ts index a552150..1a0fc61 100644 --- a/backend/src/modules/push-server.client.ts +++ b/backend/src/modules/push-server.client.ts @@ -30,6 +30,12 @@ export type SendPushServerMessageInput = { data?: Record } +export type CentralServicesHeartbeatInput = { + fedeoVersion?: string + baseUrl?: string + capabilities?: string[] +} + function configured() { return Boolean(secrets.PUSH_SERVER_URL && secrets.PUSH_SERVER_INSTANCE_ID && secrets.PUSH_SERVER_SECRET) } @@ -105,6 +111,15 @@ export const pushServerClient = { export const centralServicesClient = { configured, + heartbeat(input: CentralServicesHeartbeatInput) { + return requestPushServer<{ + status: string + instanceId: string + payloadMode: string + capabilities: string[] + }>("POST", "/v1/instances/heartbeat", input) + }, + aiChatCompletions(payload: Record) { return requestPushServer("POST", "/v1/services/ai/chat-completions", payload) }, diff --git a/backend/src/utils/secrets.ts b/backend/src/utils/secrets.ts index aa4b4f4..aea020c 100644 --- a/backend/src/utils/secrets.ts +++ b/backend/src/utils/secrets.ts @@ -54,6 +54,8 @@ export let secrets = { PUSH_SERVER_INSTANCE_ID?: string PUSH_SERVER_SECRET?: string FEDEO_CENTRAL_SERVICES_ENABLED?: boolean + FEDEO_CENTRAL_HEARTBEAT_INTERVAL_MS?: number + FEDEO_VERSION?: string } const secretKeys = [ @@ -102,9 +104,11 @@ const secretKeys = [ "PUSH_SERVER_INSTANCE_ID", "PUSH_SERVER_SECRET", "FEDEO_CENTRAL_SERVICES_ENABLED", + "FEDEO_CENTRAL_HEARTBEAT_INTERVAL_MS", + "FEDEO_VERSION", ] as const -const numberKeys = new Set(["PORT", "MAILER_SMTP_PORT", "DOKUBOX_IMAP_PORT"]) +const numberKeys = new Set(["PORT", "MAILER_SMTP_PORT", "DOKUBOX_IMAP_PORT", "FEDEO_CENTRAL_HEARTBEAT_INTERVAL_MS"]) const booleanKeys = new Set(["DOKUBOX_IMAP_SECURE", "FEDEO_CENTRAL_SERVICES_ENABLED"]) function normalizeEnvValue(key: string, value: string) { diff --git a/docker-compose.selfhost.yml b/docker-compose.selfhost.yml index 19fdb36..5987436 100644 --- a/docker-compose.selfhost.yml +++ b/docker-compose.selfhost.yml @@ -133,6 +133,8 @@ services: PUSH_SERVER_URL: ${PUSH_SERVER_URL:-} PUSH_SERVER_INSTANCE_ID: ${PUSH_SERVER_INSTANCE_ID:-} PUSH_SERVER_SECRET: ${PUSH_SERVER_SECRET:-} + FEDEO_CENTRAL_HEARTBEAT_INTERVAL_MS: ${FEDEO_CENTRAL_HEARTBEAT_INTERVAL_MS:-300000} + FEDEO_VERSION: ${FEDEO_VERSION:-selfhost-dev} STIRLING_API_KEY: ${STIRLING_API_KEY} FEDEO_BOOTSTRAP_ADMIN_EMAIL: ${FEDEO_BOOTSTRAP_ADMIN_EMAIL:-} FEDEO_BOOTSTRAP_ADMIN_PASSWORD: ${FEDEO_BOOTSTRAP_ADMIN_PASSWORD:-} diff --git a/push-server/docs/instance-client.md b/push-server/docs/instance-client.md index d40c78f..5eb90f2 100644 --- a/push-server/docs/instance-client.md +++ b/push-server/docs/instance-client.md @@ -13,8 +13,11 @@ FEDEO_CENTRAL_SERVICES_ENABLED=true PUSH_SERVER_URL=https://services.fedeo.de PUSH_SERVER_INSTANCE_ID=inst_... PUSH_SERVER_SECRET=fps_... +FEDEO_CENTRAL_HEARTBEAT_INTERVAL_MS=300000 ``` +Das Selfhost-Backend sendet direkt beim Start und anschließend standardmäßig alle fünf Minuten einen signierten Heartbeat. Im Backend-Log erscheinen erfolgreiche Meldungen als `Central Services Heartbeat: inst_... (active)`; Verbindungs-, Signatur- oder Freischaltungsfehler werden mit ihrer zentralen Fehlermeldung protokolliert. Werte unter 30 Sekunden werden aus Sicherheitsgründen ignoriert. + Wenn `FEDEO_CENTRAL_SERVICES_ENABLED=false` bleibt, verwendet FEDEO wie bisher direkt konfigurierte OpenAI- und GoCardless-Zugangsdaten. Damit bleibt ein vollständig unabhängiger Betrieb möglich. ## Minimaler Push-Auftrag