Tactical Media Broadcast Hub — initial release
Self-hosted multi-platform broadcast newsroom for activist collectives: publishing pipeline with group approval and DST-proof scheduling, TweetDeck-style column wall with hostile-content-safe rendering, live control room with Auto Director, bridged chat governance with polls and quorum, distributed sandboxed transcode workers, encrypted content-addressed archive, monthly analytics. MIT licensed. See README.md and docs/.
This commit is contained in:
commit
aa5e262f3a
880 changed files with 111042 additions and 0 deletions
26
apps/api/src/health/health.controller.ts
Normal file
26
apps/api/src/health/health.controller.ts
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
import { Controller, Get, Res, Inject } from "@nestjs/common";
|
||||
import type { Response } from "express";
|
||||
import { HealthService, type ComponentHealth } from "./health.service";
|
||||
|
||||
interface ReadinessBody {
|
||||
status: "ready" | "degraded";
|
||||
components: Record<string, ComponentHealth>;
|
||||
}
|
||||
|
||||
@Controller()
|
||||
export class HealthController {
|
||||
constructor(@Inject(HealthService) private readonly health: HealthService) {}
|
||||
|
||||
@Get("healthz")
|
||||
liveness(@Res() res: Response): void {
|
||||
res.status(200).json({ status: "alive" });
|
||||
}
|
||||
|
||||
@Get("readyz")
|
||||
async readiness(@Res() res: Response): Promise<void> {
|
||||
const components = await this.health.components();
|
||||
const ready = Object.values(components).every((c) => c.status === "up");
|
||||
const body: ReadinessBody = { status: ready ? "ready" : "degraded", components };
|
||||
res.status(ready ? 200 : 503).json(body);
|
||||
}
|
||||
}
|
||||
2
apps/api/src/health/health.deps.ts
Normal file
2
apps/api/src/health/health.deps.ts
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
export { Injectable } from "@nestjs/common";
|
||||
export { logger } from "../telemetry/logger";
|
||||
10
apps/api/src/health/health.module.ts
Normal file
10
apps/api/src/health/health.module.ts
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
import { Module } from "@nestjs/common";
|
||||
import { HealthController } from "./health.controller";
|
||||
import { HealthService } from "./health.service";
|
||||
|
||||
@Module({
|
||||
providers: [HealthService],
|
||||
controllers: [HealthController],
|
||||
exports: [HealthService],
|
||||
})
|
||||
export class HealthModule {}
|
||||
50
apps/api/src/health/health.service.ts
Normal file
50
apps/api/src/health/health.service.ts
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
import { assertPublishingSchema } from "@tactical-media-broadcast-hub/publishing-execution";
|
||||
import { pool } from "../db/client.js";
|
||||
import * as net from "net";
|
||||
import { Injectable } from "@nestjs/common";
|
||||
|
||||
export type ComponentStatus = "up" | "down";
|
||||
|
||||
export interface ComponentHealth {
|
||||
status: ComponentStatus;
|
||||
}
|
||||
|
||||
interface ServiceEndpoint {
|
||||
host: string;
|
||||
port: number;
|
||||
}
|
||||
|
||||
const serviceEnv: Record<string, ServiceEndpoint> = {
|
||||
postgres: { host: process.env.POSTGRES_HOST ?? "localhost", port: Number(process.env.POSTGRES_PORT ?? 5433) },
|
||||
redis: { host: process.env.REDIS_HOST ?? "localhost", port: Number(process.env.REDIS_PORT ?? 6379) },
|
||||
temporal: { host: process.env.TEMPORAL_HOST ?? "localhost", port: Number(process.env.TEMPORAL_PORT ?? 7233) },
|
||||
};
|
||||
|
||||
function probeTcp(host: string, port: number, timeoutMs: number): Promise<boolean> {
|
||||
return new Promise((resolve) => {
|
||||
const socket = new net.Socket();
|
||||
const done = (ok: boolean): void => {
|
||||
socket.destroy();
|
||||
resolve(ok);
|
||||
};
|
||||
socket.setTimeout(timeoutMs);
|
||||
socket.once("connect", () => done(true));
|
||||
socket.once("timeout", () => done(false));
|
||||
socket.once("error", () => done(false));
|
||||
socket.connect(port, host);
|
||||
});
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class HealthService {
|
||||
async components(): Promise<Record<string, ComponentHealth>> {
|
||||
const entries = await Promise.all(
|
||||
Object.entries(serviceEnv).map(async ([name, { host, port }]) => {
|
||||
const up = await probeTcp(host, port, 1500);
|
||||
return [name, { status: up ? "up" : "down" } as ComponentHealth] as const;
|
||||
}),
|
||||
);
|
||||
const schema = await assertPublishingSchema(pool).then(() => "up" as const).catch(() => "down" as const);
|
||||
return { ...Object.fromEntries(entries), publishing_schema: { status: schema } };
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue