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
|
argon2Memory: 65536
|
||||||
argon2TimeCost: 3
|
argon2TimeCost: 3
|
||||||
argon2Parallelism: 2
|
argon2Parallelism: 2
|
||||||
|
noderedUrl: http://127.0.0.1:1880
|
||||||
|
|
||||||
# ----- Live kiosk WebSocket channel -----
|
# ----- Live kiosk WebSocket channel -----
|
||||||
service-coordinator-ws:
|
service-coordinator-ws:
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,7 @@ import { initSecrets } from "../../shared/secrets.js";
|
||||||
import { createAuth } from "../../shared/auth.js";
|
import { createAuth } from "../../shared/auth.js";
|
||||||
import { initiatePairing, claimPairing } from "../../shared/pairing.js";
|
import { initiatePairing, claimPairing } from "../../shared/pairing.js";
|
||||||
import { generateBundle } from "../../shared/bundle.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 { Repository } from "../service-store/repository.js";
|
||||||
import type { AuthApi } from "../../shared/auth.js";
|
import type { AuthApi } from "../../shared/auth.js";
|
||||||
import type { SecretsApi } from "../../shared/secrets.js";
|
import type { SecretsApi } from "../../shared/secrets.js";
|
||||||
|
|
@ -42,6 +43,7 @@ const ConfigSchema = av.object(
|
||||||
loginLockoutThreshold: av.int().min(1).default(8),
|
loginLockoutThreshold: av.int().min(1).default(8),
|
||||||
loginLockoutSeconds: av.int().min(1).default(900),
|
loginLockoutSeconds: av.int().min(1).default(900),
|
||||||
totpIssuer: av.string().minLength(1).default("BetterFrame"),
|
totpIssuer: av.string().minLength(1).default("BetterFrame"),
|
||||||
|
noderedUrl: av.string().minLength(1).default("http://127.0.0.1:1880"),
|
||||||
},
|
},
|
||||||
{ unknownKeys: "strip" },
|
{ unknownKeys: "strip" },
|
||||||
);
|
);
|
||||||
|
|
@ -99,11 +101,15 @@ export class Plugin extends BSBService<InstanceType<typeof Config>, typeof Event
|
||||||
cookieName: this.config.cookieName,
|
cookieName: this.config.cookieName,
|
||||||
});
|
});
|
||||||
const codeTtl = this.config.codeTtlSeconds;
|
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();
|
const app = new H3();
|
||||||
|
|
||||||
registerPairingRoutes(app, repo, auth, secrets, codeTtl);
|
registerPairingRoutes(app, repo, auth, secrets, codeTtl);
|
||||||
registerKioskRoutes(app, repo, auth, secrets);
|
registerKioskRoutes(app, repo, auth, secrets, nodered);
|
||||||
|
|
||||||
this.server = serve(app, {
|
this.server = serve(app, {
|
||||||
port: this.config.port,
|
port: this.config.port,
|
||||||
|
|
@ -198,6 +204,7 @@ function registerKioskRoutes(
|
||||||
repo: Repository,
|
repo: Repository,
|
||||||
auth: AuthApi,
|
auth: AuthApi,
|
||||||
secrets: SecretsApi,
|
secrets: SecretsApi,
|
||||||
|
nodered: NoderedBridge,
|
||||||
): void {
|
): void {
|
||||||
// Bundle delivery
|
// Bundle delivery
|
||||||
app.get("/api/kiosk/bundle", async (event) => {
|
app.get("/api/kiosk/bundle", async (event) => {
|
||||||
|
|
@ -289,6 +296,17 @@ function registerKioskRoutes(
|
||||||
forwarded_to_nodered: false,
|
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 };
|
return { ok: true, event_id: eventId };
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,48 @@
|
||||||
/**
|
/**
|
||||||
* Node-RED HTTP bridge — shared module stub.
|
* Node-RED bridge — best-effort outbound event forwarder.
|
||||||
* TODO: implement outbound forwarder + inbound callbacks.
|
*
|
||||||
|
* 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