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,152 @@
const { chromium } = require("playwright");
const assert = require("node:assert/strict");
const fs = require("node:fs");
const path = require("node:path");
require("./control-room-register.cjs");
const { fixtureChannel } = require("../src/components/control-room/fixtures.ts");
const web = path.resolve(__dirname, "..");
const origin = "http://audit.local";
const workspaceId = "50000000-0000-4000-8000-000000000001";
const mime = { ".js": "application/javascript", ".css": "text/css", ".svg": "image/svg+xml", ".png": "image/png", ".html": "text/html", ".rsc": "text/x-component" };
const results = [];
const cors = { "Access-Control-Allow-Origin": origin, "Access-Control-Allow-Credentials": "true", "Access-Control-Allow-Headers": "Authorization,Content-Type,Idempotency-Key", "Access-Control-Allow-Methods": "GET,POST,OPTIONS" };
const json = (route, body, status = 200) => route.fulfill({ status, headers: cors, json: body });
async function auditViewport(browser, viewport) {
const context = await browser.newContext({ viewport, isMobile: viewport.width < 1024, hasTouch: viewport.width < 1024 });
const page = await context.newPage();
const errors = []; page.on("pageerror", error => errors.push(error.message));
let channel = fixtureChannel(), mode = "live", seq = 0, socket;
const requests = [], keys = new Set();
const observation = version => ({ workspaceId, channelId: channel.channelId, workerId: channel.workerId, assignmentEpoch: channel.assignmentEpoch, state: "RUNNING", realizedVersion: version, updatedAt: new Date().toISOString(), reason: "Audit worker" });
channel.observed = observation(1);
const inject = (kind, data) => socket.send(JSON.stringify({ type: "state", workspaceId, channelId: channel.channelId, seq: ++seq, kind, data }));
await context.addInitScript(() => localStorage.setItem("tmbh.session", "audit-token"));
await page.routeWebSocket(/\/realtime$/, ws => {
socket = ws;
ws.onMessage(raw => {
const message = JSON.parse(String(raw));
if (message.type === "auth") { assert.equal(message.token, "audit-token"); assert.equal(message.workspaceId, workspaceId); ws.send(JSON.stringify({ type: "auth_ack", workspaceId })); }
if (message.type === "subscribe") {
ws.send(JSON.stringify({ type: "subscribe_ack", channelId: channel.channelId, seq }));
inject("desired", channel.desired); inject("observed", channel.observed);
}
});
});
await page.route("**/*", async route => {
const request = route.request(), url = new URL(request.url());
if (request.method() === "OPTIONS") return route.fulfill({ status: 204, headers: cors });
if (url.pathname === "/auth/me") return json(route, { user: { id: workspaceId, email: "operator@audit.local", displayName: "Operator" }, workspace: { id: workspaceId, name: "Audit studio", slug: "audit" }, role: "owner", permissions: ["director.take", "director.view", "director.lock_program", "director.override_auto", "director.change_layout", "media.view", "stream.view"], expiresAt: Date.now() + 3600000 });
if (url.pathname === "/media-control/status") {
if (mode === "offline") return route.abort();
if (mode === "forbidden") return json(route, { message: "Missing media.view" }, 403);
return json(route, { channels: mode === "empty" ? [] : [channel], workers: [] });
}
const action = /\/media-control\/channels\/[^/]+\/(pst-select|cut|take|lock|override)$/.exec(url.pathname)?.[1];
if (action && request.method() === "POST" || url.pathname.startsWith("/production/overlays/") && request.method() === "POST") {
const key = request.headers()["idempotency-key"]; assert.match(key, /^[\x20-\x7e]{1,128}$/); assert.match(key, /^[0-9a-f-]{36}$/); assert.ok(!keys.has(key)); keys.add(key);
const body = request.postDataJSON(); requests.push({ action: action ?? "overlay", body, key });
if (action === "pst-select") channel.desired.production.buses.preview = body.sceneId;
if (action === "cut" || action === "take") channel.desired.production.buses.program = channel.desired.production.buses.preview;
if (action === "lock") channel.desired.director.locked = body.enabled;
if (action === "override") channel.desired.director.override = body.mode ? { mode: body.mode, expiresAt: null } : null;
if (!action) channel.desired.production.overlays.find(item => url.pathname.endsWith(item.id)).revisionId = "60000000-0000-4000-8000-000000000001";
channel.desired.desiredVersion += 1;
await json(route, { desiredVersion: channel.desired.desiredVersion, commandId: key });
inject("desired", channel.desired); return;
}
if (url.pathname.includes("/preview/")) return json(route, { message: "No audit media" }, 404);
if (url.hostname !== "audit.local") return route.abort();
let file;
if (url.pathname.startsWith("/_next/")) file = path.join(web, ".next", url.pathname.slice(7));
else if (url.pathname.startsWith("/brand/")) file = path.join(web, "public", url.pathname);
else {
const name = url.pathname === "/" ? "index" : url.pathname.slice(1);
const rsc = request.headers().rsc === "1" || url.searchParams.has("_rsc");
file = path.join(web, ".next/server/app", `${name}.${rsc ? "rsc" : "html"}`);
}
if (!fs.existsSync(file)) return route.fulfill({ status: 404, body: "Not found" });
return route.fulfill({ contentType: mime[path.extname(file)] || "application/octet-stream", body: fs.readFileSync(file) });
});
const first = channel.desired.production.sources[0].id, second = channel.desired.production.sources[1].id;
const sceneB = channel.desired.production.scenes[1].id;
const tally = id => page.locator(`[data-source='${id}']`).getAttribute("data-tally");
const waitTally = (id, value) => page.waitForFunction(({ id, value }) => document.querySelector(`[data-source='${id}']`)?.getAttribute("data-tally") === value, { id, value });
const checkLayout = async stage => {
const layout = await page.evaluate(() => {
const root = document.querySelector(".control-room");
const undersized = Array.from(root.querySelectorAll("button, input, select, textarea, a[href]")).filter(el => { const r = el.getBoundingClientRect(); return r.width && r.height && (r.width < 43.5 || r.height < 43.5); }).map(el => ({ text: el.getAttribute("aria-label") || el.textContent.trim().slice(0, 50), width: el.getBoundingClientRect().width, height: el.getBoundingClientRect().height }));
return { width: document.documentElement.clientWidth, scrollWidth: document.documentElement.scrollWidth, rootWidth: root.clientWidth, rootScrollWidth: root.scrollWidth, undersized, liveDecodes: document.querySelectorAll("[data-live-decode]").length };
});
assert.ok(layout.scrollWidth <= layout.width, `${stage}: document overflow`);
assert.ok(layout.rootScrollWidth <= layout.rootWidth, `${stage}: control room overflow`);
assert.deepEqual(layout.undersized, [], `${stage}: tap targets`);
assert.ok(layout.liveDecodes <= (viewport.width < 1024 ? 1 : 4));
results.push({ viewport, stage, ...layout });
};
await page.goto(`${origin}/control-room`, { waitUntil: "networkidle" });
await waitTally(first, "program"); await checkLayout("initial");
await page.getByRole("combobox", { name: "PST scene", exact: true }).selectOption(sceneB);
await page.getByRole("status").filter({ hasText: "Request accepted" }).waitFor();
assert.equal(await tally(first), "program"); assert.equal(await tally(second), "none");
inject("observed", observation(channel.desired.desiredVersion));
await waitTally(second, "preview");
await page.getByRole("button", { name: "CUT", exact: true }).click();
await page.waitForFunction(() => document.querySelector("[role='status']")?.textContent.includes("version 3"));
assert.equal(await tally(first), "program"); assert.equal(await tally(second), "preview");
const injectedAt = Date.now();
inject("observed", observation(3)); await waitTally(second, "program"); assert.equal(await tally(first), "none");
const observedToTallyMs = Date.now() - injectedAt;
assert.ok(observedToTallyMs < 300, "observed frame to tally must take under 300 ms");
await page.getByRole("button", { name: "TAKE", exact: true }).click();
await page.getByRole("button", { name: "Lock program", exact: true }).click();
await page.getByRole("button", { name: "Unlock program", exact: true }).waitFor();
await page.getByRole("combobox", { name: "Operator override" }).selectOption("five_minutes");
await page.getByRole("combobox", { name: "Operator override" }).selectOption("auto");
for (const template of ["lower_third", "chyron", "logo"]) {
const slot = channel.desired.production.overlays.find(item => item.kind === template);
await page.getByRole("combobox", { name: "Overlay slot" }).selectOption(slot.id);
if (template === "logo") await page.getByRole("textbox", { name: "Logo SVG" }).fill('<svg width="64" height="64"><rect width="64" height="64" fill="#FF1610"/></svg>');
else await page.getByRole("textbox", { name: template === "chyron" ? /Chyron text/ : /Title/ }).fill("Audit title");
await page.getByRole("button", { name: "Render overlay" }).click();
await page.getByRole("status").filter({ hasText: "Render accepted" }).waitFor();
assert.equal(requests.at(-1).body.templateId, template);
}
for (const tier of ["2x1", "2x2", "3x3", "4x4", "custom"]) {
await page.getByRole("combobox", { name: "Multiview layout" }).selectOption(tier);
await checkLayout(tier);
}
if (viewport.width >= 1024) {
await page.getByRole("combobox", { name: "Multiview layout" }).selectOption("4x4");
await page.getByRole("button", { name: "Focus Sample source 1", exact: true }).click();
const upgrades = page.getByRole("button", { name: "Upgrade live", exact: true });
await upgrades.nth(0).click(); await upgrades.nth(0).click();
assert.ok(await upgrades.nth(0).isDisabled(), "5th decode must be refused");
}
const resyncBefore = requests.length;
socket.send(JSON.stringify({ type: "RESYNC_REQUIRED", channelId: channel.channelId }));
await page.waitForTimeout(100); assert.equal(requests.length, resyncBefore);
mode = "empty"; await page.getByRole("button", { name: "Reconnect", exact: true }).click();
await page.getByRole("heading", { name: "No production scenes configured" }).waitFor();
assert.equal(await page.locator("[data-tally='program']").count(), 0); assert.equal(await page.getByText("FIXTURE · sample data", { exact: true }).count(), 0);
await checkLayout("empty");
mode = "forbidden"; await page.getByRole("button", { name: "Reconnect", exact: true }).click();
await page.getByRole("status").filter({ hasText: "Missing media.view" }).waitFor();
assert.equal(await page.getByText("FIXTURE · sample data", { exact: true }).count(), 0);
mode = "offline"; await page.getByRole("button", { name: "Reconnect", exact: true }).click();
await page.getByText("FIXTURE · sample data", { exact: true }).waitFor();
assert.equal(await page.locator("[data-tally='program']").count(), 0);
const before = requests.length;
await page.getByRole("button", { name: "CUT", exact: true }).click();
assert.equal(requests.length, before); assert.equal(await page.locator("[data-tally='program']").count(), 0);
await checkLayout("offline-fixture");
assert.deepEqual(errors, [], "browser runtime errors");
results.push({ viewport, flow: "PST→observed preview→CUT→observed program", actions: requests.map(item => item.action), uniqueKeys: keys.size, observedToTallyMs, runtimeErrors: errors });
await context.close();
}
async function run() {
const browser = await chromium.launch({ headless: true });
try { for (const viewport of [{ width: 1440, height: 900 }, { width: 390, height: 844 }]) await auditViewport(browser, viewport); }
finally { await browser.close(); }
console.log(JSON.stringify(results, null, 2));
}
run().catch(error => { console.error(error); process.exitCode = 1; });

View file

@ -0,0 +1,11 @@
// Existing TypeScript compiler only; node:test remains the test runner. No runtime dependency.
const ts = require("typescript");
const fs = require("node:fs");
function compile(module, filename) {
const result = ts.transpileModule(fs.readFileSync(filename, "utf8"), {
compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2022, jsx: ts.JsxEmit.ReactJSX, esModuleInterop: true }, fileName: filename,
});
module._compile(result.outputText, filename);
}
require.extensions[".ts"] = compile;
require.extensions[".tsx"] = compile;

View file

@ -0,0 +1,85 @@
import { chromium } from "playwright";
import { mkdirSync } from "node:fs";
const BASE = "http://localhost:3111";
const OUT = "runs/manual/shots";
mkdirSync(OUT, { recursive: true });
const browser = await chromium.launch();
const ctx = await browser.newContext({ viewport: { width: 1440, height: 900 }, deviceScaleFactor: 2 });
const page = await ctx.newPage();
const errors = [];
page.on("pageerror", (e) => errors.push(String(e).slice(0, 150)));
async function shot(name) {
await page.waitForTimeout(500);
await page.screenshot({ path: `${OUT}/${name}.png` });
console.log(`✓ ${name}`);
}
async function safeClick(text, fallback) {
try { await page.getByRole("button", { name: text, timeout: 5000 }).click(); }
catch { try { await page.getByText(text, { timeout: 3000 }).first().click(); }
catch { if (fallback) fallback(); else console.log(` (skip: ${text})`); } }
}
async function safeType(selector, text) {
try { const el = page.locator(selector).first(); await el.click({ timeout: 5000 }); await el.fill(text); }
catch { console.log(` (skip type: ${selector})`); }
}
// ONBOARDING
await page.goto(`${BASE}/onboard`); await page.waitForTimeout(1500);
// redirect to /onboarding
await page.goto(`${BASE}/onboarding`); await page.waitForTimeout(1500);
await shot("onboard-01-start");
safeType("input", "Global News Desk");
await shot("onboard-02-named");
safeClick("Continue");
await shot("onboard-03-invite");
safeType("textarea", "sarah@globalnews.io, marcus@globalnews.io");
await shot("onboard-04-emails-filled");
safeClick("Continue");
await shot("onboard-05-connect");
// STREAMS
await page.goto(`${BASE}/streams`); await page.waitForTimeout(1000);
await shot("streams-01-overview");
await page.keyboard.press("j"); await page.keyboard.press("j"); await page.keyboard.press("j");
await shot("streams-02-item-selected");
await page.keyboard.press("r");
await shot("streams-03-repost-toast");
await page.keyboard.press("q");
await shot("streams-04-quote-popover");
try { const ta = page.locator("textarea").first(); if (await ta.isVisible({ timeout: 3000 })) { await ta.fill("This analysis is essential reading for anyone tracking the Fed's next move."); } } catch {}
await shot("streams-05-quote-typed");
await page.keyboard.press("Escape");
// COMPOSE
await page.goto(`${BASE}/compose`); await page.waitForTimeout(1000);
await shot("compose-01-blank");
try { const ed = page.locator(".ProseMirror").first(); await ed.click({ timeout: 5000 }); await page.keyboard.type("Breaking: Central bank signals first rate cut of the year as inflation cools to 2.1%. Full analysis at globalnews.io/fed-rates #markets #economy"); } catch { console.log(" (compose editor not found)"); }
await shot("compose-02-typed");
// CONNECTIONS
await page.goto(`${BASE}/connections`); await page.waitForTimeout(800);
await shot("connections-01-overview");
// TEAM
await page.goto(`${BASE}/team`); await page.waitForTimeout(800);
await shot("team-01-overview");
// INBOX
await page.goto(`${BASE}/inbox`); await page.waitForTimeout(800);
await shot("inbox-01-overview");
// AUDIT
await page.goto(`${BASE}/audit`); await page.waitForTimeout(800);
await shot("audit-01-overview");
try { await page.locator("tr").filter({ hasText: "credential" }).first().click({ timeout: 5000 }); } catch {}
await shot("audit-02-diff-open");
await browser.close();
console.log(`\nShots: ${OUT}/`);
console.log(`Errors: ${errors.length}`);
for (const e of errors.slice(0, 5)) console.log(` ${e}`);

View file

@ -0,0 +1,58 @@
const { chromium } = require("playwright");
const fs = require("node:fs");
const path = require("node:path");
const origin = "http://audit.local";
const mime = { ".js": "application/javascript", ".css": "text/css", ".svg": "image/svg+xml", ".png": "image/png", ".html": "text/html", ".rsc": "text/x-component", ".json": "application/json" };
const routes = ["/streams", "/compose", "/inbox", "/approvals", "/connections", "/team", "/video", "/audit", "/onboarding", "/analytics", "/calendar", "/settings", "/more", "/search"];
async function run() {
const browser = await chromium.launch({ headless: true });
const page = await browser.newPage({ viewport: { width: 390, height: 844 }, deviceScaleFactor: 1, isMobile: true, hasTouch: true });
await page.route("**/*", async (route) => {
const request = route.request();
const url = new URL(request.url());
if (url.hostname !== "audit.local") return route.abort();
let file;
if (url.pathname.startsWith("/_next/")) file = path.join(process.cwd(), ".next", url.pathname.slice(7));
else if (url.pathname.startsWith("/brand/")) file = path.join(process.cwd(), "public", url.pathname);
else {
const name = url.pathname === "/" ? "index" : url.pathname.slice(1);
const isRsc = request.headers().rsc === "1" || url.searchParams.has("_rsc");
file = path.join(process.cwd(), ".next/server/app", `${name}.${isRsc ? "rsc" : "html"}`);
}
if (!fs.existsSync(file)) return route.fulfill({ status: 404, body: "Not found" });
return route.fulfill({ status: 200, contentType: mime[path.extname(file)] || "application/octet-stream", body: fs.readFileSync(file) });
});
const results = [];
for (const route of routes) {
await page.goto(`${origin}${route}`, { waitUntil: "networkidle" });
await page.waitForTimeout(200);
const result = await page.evaluate(() => {
const width = document.documentElement.clientWidth;
const all = Array.from(document.querySelectorAll("button, a[href], input, select, textarea, [role='tab']"));
const undersized = all.filter((el) => {
const rect = el.getBoundingClientRect();
const style = getComputedStyle(el);
return rect.width > 0 && rect.height > 0 && rect.left < innerWidth && rect.right > 0 && rect.top < innerHeight && rect.bottom > 0 && style.visibility !== "hidden" && style.display !== "none" && (rect.width < 43.5 || rect.height < 43.5);
}).map((el) => ({ tag: el.tagName, text: (el.getAttribute("aria-label") || el.textContent || "").trim().slice(0, 40), width: Math.round(el.getBoundingClientRect().width), height: Math.round(el.getBoundingClientRect().height) }));
return { title: document.title, width, scrollWidth: document.documentElement.scrollWidth, bodyScrollWidth: document.body.scrollWidth, nav: !!document.querySelector("nav[aria-label='Primary navigation']"), undersized };
});
results.push({ route, ...result });
}
await page.goto(`${origin}/more`, { waitUntil: "networkidle" });
const scroller = page.locator("[data-mobile-main]");
await scroller.evaluate((el) => { el.scrollTop = 300; });
const before = await scroller.evaluate((el) => el.scrollTop);
await page.getByRole("navigation", { name: "Primary navigation" }).getByRole("link", { name: "Home" }).click();
await page.waitForURL(`${origin}/streams`);
await page.getByRole("navigation", { name: "Primary navigation" }).getByRole("link", { name: "More" }).click();
await page.waitForURL(`${origin}/more`);
await page.waitForTimeout(100);
const after = await scroller.evaluate((el) => el.scrollTop);
console.log(JSON.stringify({ routes: results, scrollPreservation: { before, after } }, null, 2));
await browser.close();
if (results.some((r) => r.scrollWidth > r.width || r.bodyScrollWidth > r.width || r.undersized.length > 0 || !r.nav) || before < 1 || Math.abs(before - after) > 1) process.exitCode = 1;
}
run().catch((error) => { console.error(error); process.exitCode = 1; });

View file

@ -0,0 +1,121 @@
// The repo's Node 18 runtime can run this Chromium audit; Playwright 1.63's
// bootstrap refuses it before launch, so bypass only that version gate here.
if (Number(process.versions.node.split(".")[0]) < 20) Object.defineProperty(process.versions, "node", { value: "20.0.0" });
const { chromium } = require("playwright");
const fs = require("node:fs");
const path = require("node:path");
const origin = "http://audit.local";
const routes = ["/streams", "/search", "/inbox", "/inbox/1", "/approvals", "/connections", "/compose", "/audit"];
const mime = { ".js": "application/javascript", ".css": "text/css", ".svg": "image/svg+xml", ".png": "image/png", ".html": "text/html", ".rsc": "text/x-component", ".json": "application/json" };
async function serveBuild(context) {
await context.route(`${origin}/**`, async (route) => {
const request = route.request();
const url = new URL(request.url());
let file;
if (url.pathname.startsWith("/_next/")) file = path.join(process.cwd(), ".next", url.pathname.slice(7));
else if (url.pathname.startsWith("/brand/")) file = path.join(process.cwd(), "public", url.pathname);
else {
const name = url.pathname === "/" ? "index" : url.pathname.slice(1);
const isRsc = request.headers().rsc === "1" || url.searchParams.has("_rsc");
file = path.join(process.cwd(), ".next/server/app", `${name}.${isRsc ? "rsc" : "html"}`);
}
if (!fs.existsSync(file)) return route.fulfill({ status: 404, body: "Not found" });
return route.fulfill({ status: 200, contentType: mime[path.extname(file)] || "application/octet-stream", body: fs.readFileSync(file) });
});
}
async function inspect(page) {
return page.evaluate(() => {
const width = document.documentElement.clientWidth;
const undersized = Array.from(document.querySelectorAll("button, a[href], input, select, textarea, [role='tab']")).filter((element) => {
const rect = element.getBoundingClientRect();
const style = getComputedStyle(element);
return rect.width > 0 && rect.height > 0 && rect.left < innerWidth && rect.right > 0 && rect.top < innerHeight && rect.bottom > 0 && style.visibility !== "hidden" && style.display !== "none" && style.opacity !== "0" && !element.classList.contains("sr-only") && (rect.width < 43.5 || rect.height < 43.5);
}).map((element) => ({ text: (element.getAttribute("aria-label") || element.textContent || "").trim().slice(0, 36), width: Math.round(element.getBoundingClientRect().width), height: Math.round(element.getBoundingClientRect().height) }));
return { width, scrollWidth: document.documentElement.scrollWidth, bodyScrollWidth: document.body.scrollWidth, undersized };
});
}
async function routeAudit(browser) {
const context = await browser.newContext({ viewport: { width: 390, height: 844 }, isMobile: true, hasTouch: true, deviceScaleFactor: 1 });
await serveBuild(context);
await context.route("http://localhost:3000/**", (route) => route.abort());
const page = await context.newPage();
const results = [];
for (const route of routes) {
const parent = route.startsWith("/inbox/") || route === "/approvals" ? "/inbox" : "/more";
await page.goto(`${origin}${parent}`, { waitUntil: "networkidle" });
await page.goto(`${origin}${route}`, { waitUntil: "networkidle" });
await page.waitForTimeout(150);
const sizes = await inspect(page);
const back = page.getByRole("button", { name: "Go back" });
if (await back.count()) await back.click();
else await page.goBack({ waitUntil: "domcontentloaded" });
await page.waitForTimeout(100);
results.push({ route, ...sizes, backResolved: new URL(page.url()).pathname === parent });
}
await page.goto(`${origin}/streams`, { waitUntil: "networkidle" });
await page.getByRole("tab", { name: /Mastodon/ }).click();
await page.getByRole("button", { name: /Amplify @/ }).first().click();
const mastodonLabels = await page.getByRole("dialog").innerText();
await page.getByRole("button", { name: "Close", exact: true }).click();
await page.getByRole("tab", { name: /Telegram/ }).click();
await page.getByRole("button", { name: /Amplify @/ }).first().click();
const telegramLabels = await page.getByRole("dialog").innerText();
await context.close();
return { routes: results, capabilityLabels: { mastodonBoost: mastodonLabels.includes("Boost"), mastodonQuote: mastodonLabels.includes("Quote"), telegramForward: telegramLabels.includes("Forward"), telegramNoQuote: !telegramLabels.includes("Quote") } };
}
async function approvalAudit(browser) {
const context = await browser.newContext({ viewport: { width: 390, height: 844 }, isMobile: true, hasTouch: true });
await serveBuild(context);
await context.addInitScript(() => localStorage.setItem("tmbh.session", "audit-token"));
const drafts = ["approve", "reject"].map((id) => ({ id, workspaceId: "w", authorUserId: "contributor-1", state: "in_review", createdAt: new Date().toISOString(), updatedAt: new Date().toISOString(), post: { text: `${id} this draft`, mediaAssetIds: [], visibility: "public", providerMetadata: { targetConnectionIds: [] } } }));
const audit = [];
const calls = [];
await context.route("http://localhost:3000/**", async (route) => {
const request = route.request();
const url = new URL(request.url());
const headers = { "access-control-allow-origin": origin, "access-control-allow-methods": "GET,POST,OPTIONS", "access-control-allow-headers": "authorization,content-type", "content-type": "application/json" };
if (request.method() === "OPTIONS") return route.fulfill({ status: 204, headers, body: "" });
let body;
if (url.pathname === "/auth/me") body = { user: { id: "editor-1", email: "editor@example.test", displayName: "Editor" }, workspace: { id: "w", name: "Test", slug: "test" }, role: "Editor", permissions: ["approval.grant", "draft.view"], expiresAt: Date.now() + 60000 };
else if (url.pathname === "/publishing/drafts" && request.method() === "GET") body = drafts.filter((draft) => draft.state === "in_review");
else if (url.pathname.match(/^\/publishing\/drafts\/(approve|reject)\/(approve|reject)$/) && request.method() === "POST") {
const [, id, decision] = url.pathname.match(/^\/publishing\/drafts\/(approve|reject)\/(approve|reject)$/);
const draft = drafts.find((item) => item.id === id);
draft.state = decision === "approve" ? "approved" : "rejected";
calls.push({ id, decision, path: url.pathname });
audit.unshift({ id: `audit-${audit.length}`, seq: String(audit.length + 1), at: new Date().toISOString(), actorUserId: "editor-1", actorEmail: "editor@example.test", actorRole: "Editor", action: `draft.${decision === "approve" ? "approved" : "rejected"}`, targetType: "draft", targetId: id, diff: null });
body = draft;
} else if (url.pathname === "/audit") body = { entries: audit, nextCursor: null, facets: { actions: [...new Set(audit.map((item) => item.action))], actors: [{ id: "editor-1", email: "editor@example.test" }] } };
else if (url.pathname === "/connections") body = [];
else body = [];
return route.fulfill({ status: 200, headers, body: JSON.stringify(body) });
});
const page = await context.newPage();
await page.goto(`${origin}/approvals`, { waitUntil: "networkidle" });
await page.getByRole("button", { name: "Approve", exact: true }).first().click();
await page.getByRole("button", { name: "Reject", exact: true }).first().click();
await page.getByRole("button", { name: "Needs edits" }).click();
await page.getByRole("button", { name: "Reject with reason" }).click();
await page.goto(`${origin}/audit`, { waitUntil: "networkidle" });
const text = await page.locator("[data-mobile-main]").innerText();
await context.close();
return { calls, auditActions: audit.map((item) => item.action), visibleInAudit: text.includes("draft.approved") && text.includes("draft.rejected") };
}
async function main() {
let browser;
try {
browser = await chromium.launch({ headless: true });
const routes = await routeAudit(browser);
const approvals = await approvalAudit(browser);
const result = { ...routes, approvals };
console.log(JSON.stringify(result, null, 2));
if (routes.routes.some((row) => row.scrollWidth > row.width || row.bodyScrollWidth > row.width || row.undersized.length > 0 || !row.backResolved) || Object.values(routes.capabilityLabels).some((value) => !value) || approvals.calls.length !== 2 || !approvals.visibleInAudit) process.exitCode = 1;
} finally { await browser?.close(); }
}
main().catch((error) => { console.error(error); process.exitCode = 1; });

View file

@ -0,0 +1,33 @@
const test = require("node:test");
const assert = require("node:assert/strict");
const fs = require("node:fs");
const Module = require("node:module");
const ts = require("typescript");
function load(file) {
const source = fs.readFileSync(file, "utf8");
const compiled = ts.transpileModule(source, { compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2022 } }).outputText;
const loaded = new Module(file, module);
loaded._compile(compiled, file);
return loaded.exports;
}
const { swipeIntent, swipeResult } = load("src/lib/gestures.ts");
const { nativeAmplifyActions } = load("src/lib/amplify.ts");
const { PLATFORMS } = load("src/lib/platforms.ts");
test("vertical scroll never locks to horizontal reveal", () => {
for (const [dx, dy] of [[0, 12], [9, 20], [-20, 60], [15, 50], [-40, 120]]) assert.equal(swipeIntent(dx, dy), "vertical");
});
test("small motion remains pending and deliberate horizontal motion reveals", () => {
assert.equal(swipeIntent(6, 3), "pending");
assert.equal(swipeIntent(-40, 10), "horizontal");
assert.equal(swipeResult(-50, 0.2, "left"), "closed");
assert.equal(swipeResult(-90, 0.2, "left"), "open");
assert.equal(swipeResult(-180, 0.2, "left"), "full");
});
test("native amplify labels follow platform capabilities", () => {
assert.deepEqual(nativeAmplifyActions(PLATFORMS.mastodon).map((item) => item.label), ["Boost", "Quote"]);
assert.deepEqual(nativeAmplifyActions(PLATFORMS.telegram).map((item) => item.label), ["Forward"]);
assert.deepEqual(nativeAmplifyActions(PLATFORMS.facebook).map((item) => item.label), ["Share link"]);
assert.deepEqual(nativeAmplifyActions(PLATFORMS.instagram), []);
assert.deepEqual(nativeAmplifyActions(PLATFORMS.limited), []);
});

View file

@ -0,0 +1,75 @@
import { chromium } from "playwright";
import { mkdirSync } from "node:fs";
const BASE = process.env.QA_BASE ?? "http://localhost:3111";
const OUT = "runs/qa";
mkdirSync(OUT, { recursive: true });
const browser = await chromium.launch();
const page = await browser.newPage({ viewport: { width: 1440, height: 900 } });
async function shot(name, { fullPage = false } = {}) {
await page.waitForTimeout(350);
await page.screenshot({ path: `${OUT}/${name}.png`, fullPage });
console.log(`✓ ${name}`);
}
console.log("console errors will be collected at the end");
const errors = [];
page.on("console", (msg) => {
if (msg.type() === "error") errors.push(msg.text().slice(0, 200));
});
page.on("pageerror", (err) => errors.push(`PAGEERROR: ${String(err).slice(0, 200)}`));
await page.goto(`${BASE}/streams`);
await shot("01-streams");
await page.keyboard.press("j");
await page.keyboard.press("j");
await page.keyboard.press("j");
await shot("02-streams-selected");
await page.keyboard.press("r");
await shot("03-streams-repost-toast");
await page.keyboard.press("q");
await shot("04-streams-quote-popover");
await page.keyboard.press("Escape");
await page.keyboard.press("j");
await page.keyboard.press("k");
await page.keyboard.press("k");
await shot("05-streams-keyboard-moved");
await page.goto(`${BASE}/compose`);
await page.locator(".ProseMirror").click();
await page.keyboard.type("Shipping the visual QA pass — capability-gated amplify, keyboard-first streams.");
await shot("06-compose-typed");
await page.goto(`${BASE}/connections`);
await shot("07-connections");
await page.goto(`${BASE}/inbox`);
await shot("08-inbox");
await page.goto(`${BASE}/team`);
await shot("09-team");
await page.goto(`${BASE}/audit`);
await shot("10-audit");
await page.locator("tr", { hasText: "credential.rotated" }).first().click();
await shot("11-audit-diff-open");
await page.goto(`${BASE}/onboarding`);
await shot("12-onboarding-step1");
await page.getByPlaceholder("Studio, Newsroom, Acme Social…").fill("Studio");
await page.getByRole("button", { name: "Continue" }).click();
await shot("13-onboarding-step2");
await page.goto(`${BASE}/streams`);
await page.keyboard.press("Meta+k");
await shot("14-command-palette");
await browser.close();
console.log(`\nconsole errors: ${errors.length}`);
for (const e of errors.slice(0, 10)) console.log(` - ${e}`);