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:
glass beadian 2026-10-10 11:56:42 +00:00
commit aa5e262f3a
880 changed files with 111042 additions and 0 deletions

View file

@ -0,0 +1,77 @@
"use client";
import Link from "next/link";
import { usePathname } from "next/navigation";
import { useAuth } from "../../lib/auth-store";
import { useApiResource } from "../../lib/use-api";
import type { DraftRecord } from "../../lib/api-types";
import { LogoMark } from "../brand/logo";
import { NavIcon } from "../brand";
const NAV = [
{ href: "/streams", label: "Streams" },
{ href: "/compose", label: "Compose" },
{ href: "/calendar", label: "Calendar" },
{ href: "/video", label: "Video" },
{ href: "/control-room", label: "Control Room" },
{ href: "/inbox", label: "Inbox" },
{ href: "/approvals", label: "Approvals" },
{ href: "/archive", label: "Archive" },
{ href: "/analytics", label: "Analytics" },
{ href: "/connections", label: "Channels" },
{ href: "/team", label: "Team" },
{ href: "/audit", label: "Audit" },
{ href: "/onboarding", label: "Onboarding" },
{ href: "/settings", label: "Settings" },
];
const identity = (drafts: DraftRecord[]): DraftRecord[] => drafts;
const emptyDrafts: DraftRecord[] = [];
export function Sidebar(): React.ReactElement {
const pathname = usePathname();
const { session, can } = useAuth();
const drafts = useApiResource<DraftRecord[], DraftRecord[]>("/publishing/drafts", identity, emptyDrafts, { state: "in_review", limit: 100 });
// Real in-review count from the API; hidden while loading/offline and for non-approvers.
const approvals = can("approval.grant") && drafts.source === "live" ? drafts.data.length : 0;
const workspaceName = session?.workspace.name ?? "Studio";
return (
<aside className="hidden h-screen w-52 shrink-0 flex-col border-r border-border-subtle bg-surface lg:flex">
<div className="flex items-center gap-2.5 border-b border-border-subtle px-4 py-4">
<LogoMark size={30} title="Tactical Media Broadcast Hub" />
<div className="min-w-0">
<div className="truncate text-sm font-semibold tracking-tight">Tactical Media Broadcast Hub</div>
<div className="truncate text-2xs text-text-faint" title={workspaceName}>
{workspaceName} ▾
</div>
</div>
</div>
<nav className="flex-1 overflow-y-auto scrollbar-thin p-2">
{NAV.map((item) => {
const active = pathname.startsWith(item.href);
return (
<Link
key={item.href}
href={item.href}
className={`group flex items-center justify-between rounded-md px-3 py-1.5 text-sm transition-colors duration-150 ${item.href === "/control-room" ? "min-h-11" : ""} ${
active ? "bg-accent-dim text-accent" : "text-text-dim hover:bg-surface-2 hover:text-text"
}`}
>
<span className="flex items-center gap-2.5">
{item.href === "/video" || item.href === "/control-room" ? (
<NavIcon name="video" size={14} />
) : (
<span className={`h-1.5 w-1.5 rounded-full ${active ? "bg-accent" : "bg-border group-hover:bg-text-faint"}`} />
)}
{item.label}
</span>
{item.href === "/approvals" && approvals > 0 && (
<span className="text-2xs text-text-faint">{approvals}</span>
)}
</Link>
);
})}
</nav>
</aside>
);
}