feat: central services heartbeat senden
All checks were successful
Build and Push Docker Images / build-backend (push) Successful in 26s
Build and Push Docker Images / build-frontend (push) Successful in 16s
Build and Push Docker Images / build-website (push) Successful in 16s
Build and Push Docker Images / build-central-services-api (push) Successful in 21s
Build and Push Docker Images / build-central-services-admin (push) Successful in 41s
Build and Push Docker Images / build-docs (push) Successful in 16s

This commit is contained in:
2026-08-05 13:49:22 +02:00
parent de36589822
commit d18ba16f8a
6 changed files with 82 additions and 1 deletions

View File

@@ -61,6 +61,7 @@ import {initMailer} from "./utils/mailer"
import {initS3} from "./utils/s3"; import {initS3} from "./utils/s3";
import { runBootstrap } from "./modules/bootstrap.service"; import { runBootstrap } from "./modules/bootstrap.service";
import { startMatrixPushWorker } from "./modules/matrix-push-worker.service"; import { startMatrixPushWorker } from "./modules/matrix-push-worker.service";
import { startCentralServicesHeartbeat } from "./modules/central-services-heartbeat.service";
//Services //Services
@@ -87,6 +88,7 @@ async function main() {
await app.register(servicesPlugin); await app.register(servicesPlugin);
await runBootstrap(app); await runBootstrap(app);
startMatrixPushWorker(app); startMatrixPushWorker(app);
startCentralServicesHeartbeat(app);
app.addHook('preHandler', (req, reply, done) => { app.addHook('preHandler', (req, reply, done) => {
console.log(req.method) console.log(req.method)

View File

@@ -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()
}

View File

@@ -30,6 +30,12 @@ export type SendPushServerMessageInput = {
data?: Record<string, unknown> data?: Record<string, unknown>
} }
export type CentralServicesHeartbeatInput = {
fedeoVersion?: string
baseUrl?: string
capabilities?: string[]
}
function configured() { function configured() {
return Boolean(secrets.PUSH_SERVER_URL && secrets.PUSH_SERVER_INSTANCE_ID && secrets.PUSH_SERVER_SECRET) 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 = { export const centralServicesClient = {
configured, configured,
heartbeat(input: CentralServicesHeartbeatInput) {
return requestPushServer<{
status: string
instanceId: string
payloadMode: string
capabilities: string[]
}>("POST", "/v1/instances/heartbeat", input)
},
aiChatCompletions(payload: Record<string, unknown>) { aiChatCompletions(payload: Record<string, unknown>) {
return requestPushServer<any>("POST", "/v1/services/ai/chat-completions", payload) return requestPushServer<any>("POST", "/v1/services/ai/chat-completions", payload)
}, },

View File

@@ -54,6 +54,8 @@ export let secrets = {
PUSH_SERVER_INSTANCE_ID?: string PUSH_SERVER_INSTANCE_ID?: string
PUSH_SERVER_SECRET?: string PUSH_SERVER_SECRET?: string
FEDEO_CENTRAL_SERVICES_ENABLED?: boolean FEDEO_CENTRAL_SERVICES_ENABLED?: boolean
FEDEO_CENTRAL_HEARTBEAT_INTERVAL_MS?: number
FEDEO_VERSION?: string
} }
const secretKeys = [ const secretKeys = [
@@ -102,9 +104,11 @@ const secretKeys = [
"PUSH_SERVER_INSTANCE_ID", "PUSH_SERVER_INSTANCE_ID",
"PUSH_SERVER_SECRET", "PUSH_SERVER_SECRET",
"FEDEO_CENTRAL_SERVICES_ENABLED", "FEDEO_CENTRAL_SERVICES_ENABLED",
"FEDEO_CENTRAL_HEARTBEAT_INTERVAL_MS",
"FEDEO_VERSION",
] as const ] 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"]) const booleanKeys = new Set(["DOKUBOX_IMAP_SECURE", "FEDEO_CENTRAL_SERVICES_ENABLED"])
function normalizeEnvValue(key: string, value: string) { function normalizeEnvValue(key: string, value: string) {

View File

@@ -133,6 +133,8 @@ services:
PUSH_SERVER_URL: ${PUSH_SERVER_URL:-} PUSH_SERVER_URL: ${PUSH_SERVER_URL:-}
PUSH_SERVER_INSTANCE_ID: ${PUSH_SERVER_INSTANCE_ID:-} PUSH_SERVER_INSTANCE_ID: ${PUSH_SERVER_INSTANCE_ID:-}
PUSH_SERVER_SECRET: ${PUSH_SERVER_SECRET:-} 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} STIRLING_API_KEY: ${STIRLING_API_KEY}
FEDEO_BOOTSTRAP_ADMIN_EMAIL: ${FEDEO_BOOTSTRAP_ADMIN_EMAIL:-} FEDEO_BOOTSTRAP_ADMIN_EMAIL: ${FEDEO_BOOTSTRAP_ADMIN_EMAIL:-}
FEDEO_BOOTSTRAP_ADMIN_PASSWORD: ${FEDEO_BOOTSTRAP_ADMIN_PASSWORD:-} FEDEO_BOOTSTRAP_ADMIN_PASSWORD: ${FEDEO_BOOTSTRAP_ADMIN_PASSWORD:-}

View File

@@ -13,8 +13,11 @@ FEDEO_CENTRAL_SERVICES_ENABLED=true
PUSH_SERVER_URL=https://services.fedeo.de PUSH_SERVER_URL=https://services.fedeo.de
PUSH_SERVER_INSTANCE_ID=inst_... PUSH_SERVER_INSTANCE_ID=inst_...
PUSH_SERVER_SECRET=fps_... 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. 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 ## Minimaler Push-Auftrag