mirror of
https://github.com/BetterCorp/BetterFrame.git
synced 2026-05-26 19:06:34 +00:00
feat: Node-RED outbound bridge — forward kiosk events to Node-RED
- shared/nodered-bridge.ts: fire-and-forget POST to Node-RED HTTP-in - api-http: kiosk event endpoint now forwards to Node-RED at /in/<topic> - Best-effort, never blocks. 3s timeout, warn on failure. - sec-config: noderedUrl on api-http (defaults to http://127.0.0.1:1880) Node-RED flows can attach http-in nodes at /in/<topic> to receive camera motion, GPIO events, etc. Inbound commands (Node-RED → server) go through the admin API with admin Bearer token (no new endpoints needed for v0.1).
This commit is contained in:
parent
c0704be343
commit
f61c3db0e8
3 changed files with 66 additions and 3 deletions
|
|
@ -57,6 +57,7 @@ default:
|
|||
argon2Memory: 65536
|
||||
argon2TimeCost: 3
|
||||
argon2Parallelism: 2
|
||||
noderedUrl: http://127.0.0.1:1880
|
||||
|
||||
# ----- Live kiosk WebSocket channel -----
|
||||
service-coordinator-ws:
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ import { initSecrets } from "../../shared/secrets.js";
|
|||
import { createAuth } from "../../shared/auth.js";
|
||||
import { initiatePairing, claimPairing } from "../../shared/pairing.js";
|
||||
import { generateBundle } from "../../shared/bundle.js";
|
||||
import { initNoderedBridge, type NoderedBridge } from "../../shared/nodered-bridge.js";
|
||||
import type { Repository } from "../service-store/repository.js";
|
||||
import type { AuthApi } from "../../shared/auth.js";
|
||||
import type { SecretsApi } from "../../shared/secrets.js";
|
||||
|
|
@ -42,6 +43,7 @@ const ConfigSchema = av.object(
|
|||
loginLockoutThreshold: av.int().min(1).default(8),
|
||||
loginLockoutSeconds: av.int().min(1).default(900),
|
||||
totpIssuer: av.string().minLength(1).default("BetterFrame"),
|
||||
noderedUrl: av.string().minLength(1).default("http://127.0.0.1:1880"),
|
||||
},
|
||||
{ unknownKeys: "strip" },
|
||||
);
|
||||
|
|
@ -99,11 +101,15 @@ export class Plugin extends BSBService<InstanceType<typeof Config>, typeof Event
|
|||
cookieName: this.config.cookieName,
|
||||
});
|
||||
const codeTtl = this.config.codeTtlSeconds;
|
||||
const nodered = initNoderedBridge(
|
||||
{ baseUrl: this.config.noderedUrl },
|
||||
{ info: (m) => obs.log.info(m as any, {}), warn: (m) => obs.log.warn(m as any, {}) },
|
||||
);
|
||||
|
||||
const app = new H3();
|
||||
|
||||
registerPairingRoutes(app, repo, auth, secrets, codeTtl);
|
||||
registerKioskRoutes(app, repo, auth, secrets);
|
||||
registerKioskRoutes(app, repo, auth, secrets, nodered);
|
||||
|
||||
this.server = serve(app, {
|
||||
port: this.config.port,
|
||||
|
|
@ -198,6 +204,7 @@ function registerKioskRoutes(
|
|||
repo: Repository,
|
||||
auth: AuthApi,
|
||||
secrets: SecretsApi,
|
||||
nodered: NoderedBridge,
|
||||
): void {
|
||||
// Bundle delivery
|
||||
app.get("/api/kiosk/bundle", async (event) => {
|
||||
|
|
@ -289,6 +296,17 @@ function registerKioskRoutes(
|
|||
forwarded_to_nodered: false,
|
||||
});
|
||||
|
||||
// Best-effort forward to Node-RED
|
||||
nodered.forward(body.topic, {
|
||||
event_id: eventId,
|
||||
kiosk_id: kiosk.id,
|
||||
camera_id: body.camera_id ?? null,
|
||||
source_type: body.source_type ?? "system",
|
||||
property_op: body.property_op ?? null,
|
||||
payload: body.payload ?? {},
|
||||
timestamp: new Date().toISOString(),
|
||||
});
|
||||
|
||||
return { ok: true, event_id: eventId };
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,48 @@
|
|||
/**
|
||||
* Node-RED HTTP bridge — shared module stub.
|
||||
* TODO: implement outbound forwarder + inbound callbacks.
|
||||
* Node-RED bridge — best-effort outbound event forwarder.
|
||||
*
|
||||
* Server fires events (camera motion, kiosk status, layout switch, GPIO
|
||||
* pulse). This module POSTs them to Node-RED HTTP-in nodes. Failures
|
||||
* are logged but never block the event flow.
|
||||
*/
|
||||
|
||||
export interface NoderedConfig {
|
||||
baseUrl: string;
|
||||
timeoutMs?: number;
|
||||
}
|
||||
|
||||
export interface NoderedLog {
|
||||
info(msg: string): void;
|
||||
warn(msg: string): void;
|
||||
}
|
||||
|
||||
export interface NoderedBridge {
|
||||
forward(topic: string, payload: Record<string, unknown>): void;
|
||||
}
|
||||
|
||||
export function initNoderedBridge(config: NoderedConfig, log: NoderedLog): NoderedBridge {
|
||||
const base = config.baseUrl.replace(/\/+$/, "");
|
||||
const timeoutMs = config.timeoutMs ?? 3000;
|
||||
|
||||
return {
|
||||
forward(topic: string, payload: Record<string, unknown>): void {
|
||||
const ctrl = new AbortController();
|
||||
const t = setTimeout(() => ctrl.abort(), timeoutMs);
|
||||
|
||||
// POST to /in/<topic> on Node-RED. Node-RED flows can attach
|
||||
// http-in nodes at /in/<topic> to consume.
|
||||
const url = `${base}/in/${encodeURIComponent(topic)}`;
|
||||
fetch(url, {
|
||||
method: "POST",
|
||||
headers: { "content-type": "application/json" },
|
||||
body: JSON.stringify(payload),
|
||||
signal: ctrl.signal,
|
||||
})
|
||||
.then((r) => {
|
||||
if (!r.ok) log.warn(`nodered ${topic} → ${r.status}`);
|
||||
})
|
||||
.catch((err) => log.warn(`nodered ${topic} failed: ${(err as Error).message}`))
|
||||
.finally(() => clearTimeout(t));
|
||||
},
|
||||
};
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue