(
private loop: ControlLoop,
private upstream: Upstream,
private recorder: Recorder,
private video: VideoStream,
private videoRec: VideoRecorder,
)
| 26 | private stateTimer: ReturnType<typeof setInterval> | null = null; |
| 27 | |
| 28 | constructor( |
| 29 | private loop: ControlLoop, |
| 30 | private upstream: Upstream, |
| 31 | private recorder: Recorder, |
| 32 | private video: VideoStream, |
| 33 | private videoRec: VideoRecorder, |
| 34 | ) { |
| 35 | const app = express(); |
| 36 | // The debug UI is served from :3000 but talks to this tracker on :3001 of |
| 37 | // the SAME host, so its fetch()es are cross-origin. Allow cross-port reads |
| 38 | // from the same hostname (covers prod :3000, dev :5173, and LAN-IP access) |
| 39 | // while still rejecting foreign origins; answer the CORS preflight. |
| 40 | app.use((req, res, next) => { |
| 41 | const origin = req.headers.origin; |
| 42 | if (origin) { |
| 43 | let sameHost = false; |
| 44 | try { |
| 45 | sameHost = new URL(origin).hostname === req.hostname; |
| 46 | } catch { |
| 47 | /* malformed Origin — treat as not allowed */ |
| 48 | } |
| 49 | if (sameHost) { |
| 50 | res.setHeader("Access-Control-Allow-Origin", origin); |
| 51 | res.setHeader("Vary", "Origin"); |
| 52 | res.setHeader("Access-Control-Allow-Methods", "GET, POST, DELETE, OPTIONS"); |
| 53 | res.setHeader("Access-Control-Allow-Headers", "content-type"); |
| 54 | } |
| 55 | } |
| 56 | if (req.method === "OPTIONS") return res.sendStatus(204); |
| 57 | next(); |
| 58 | }); |
| 59 | app.use(express.json()); |
| 60 | |
| 61 | app.get("/api/tracker/health", (_req, res) => res.json({ ok: true })); |
| 62 | app.get("/api/tracker/state", (_req, res) => res.json(this.loop.getState())); |
| 63 | app.get("/api/tracker/config", (_req, res) => |
| 64 | res.json(this.upstream.getConfig().tracker), |
| 65 | ); |
| 66 | app.get("/video", (_req, res) => this.video.addClient(res)); |
| 67 | app.get("/frame.jpg", (_req, res) => { |
| 68 | const frame = this.video.latestFrame(); |
| 69 | if (!frame) return res.status(503).json({ error: "no frame yet" }); |
| 70 | res.type("image/jpeg").send(frame); |
| 71 | }); |
| 72 | // The frame as the detector sees it: red = masked clutter, green = blob. |
| 73 | app.get("/vision-debug.jpg", (_req, res) => { |
| 74 | const frame = this.video.latestFrame(); |
| 75 | if (!frame) return res.status(503).json({ error: "no frame yet" }); |
| 76 | renderDebug(frame) |
| 77 | .then((img) => res.type("image/jpeg").send(img)) |
| 78 | .catch((err) => res.status(500).json({ error: String(err) })); |
| 79 | }); |
| 80 | |
| 81 | // --- full-quality clip recording --- |
| 82 | app.post("/api/record/video", (req, res) => { |
| 83 | if (req.body?.on) res.json(this.videoRec.start()); |
| 84 | else { |
| 85 | this.videoRec.stop(); |
nothing calls this directly
no test coverage detected