feat: produce worker deployment + web producer/collective/branding UI + mobile CSS (wave-2c)

Deployment (t4g2): media-worker gains font-dejavu (drawtext lower-thirds)
and a dedicated produce-worker compose service running src/produce.ts —
claims video_processing_jobs compose|mix by lane, renders assembled/
overlaid/voiced cuts, registers derived library assets. Deliberately its
own process so a remote-lane box needs only Postgres + S3 + ffmpeg.

Web: compose media attachments UI (t2), Producer panel in the video
drawer (t5), collective creation + branding settings + shell branding
(brandweb), mobile CSS (mcss: touch targets, icon sizing, safe areas).
This commit is contained in:
glass beadian 2026-10-14 20:40:49 +00:00
parent 1183d69849
commit d9e8cdf789
18 changed files with 2384 additions and 20 deletions

View file

@ -3,6 +3,7 @@
import Link from "next/link";
import { usePathname } from "next/navigation";
import { useAuth } from "../../lib/auth-store";
import { useBranding } from "../../lib/branding";
import { useApiResource } from "../../lib/use-api";
import type { DraftRecord } from "../../lib/api-types";
import { LogoMark } from "../brand/logo";
@ -22,7 +23,7 @@ const NAV: { href: string; label: string; icon: string; external?: boolean }[] =
{ href: "/analytics", label: "Analytics", icon: "analytics" },
{ href: "/connections", label: "Channels", icon: "connections" },
{ href: "/team", label: "Team", icon: "team" },
{ href: "/audit", label: "Audit", icon: "audit" },
{ href: "/audit", label: "Audit log", icon: "audit" },
{ href: "/onboarding", label: "Onboarding", icon: "home" },
{ href: "/settings", label: "Settings", icon: "settings" },
];
@ -33,19 +34,41 @@ const emptyDrafts: DraftRecord[] = [];
export function Sidebar(): React.ReactElement {
const pathname = usePathname();
const { session, can } = useAuth();
const branding = useBranding();
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";
// Branding is per collective; the session's workspace is the source of truth
// for the name, branding only decorates it (and may be loading/unavailable).
const workspaceName = session?.workspace.name ?? branding.workspaceName ?? "Studio";
const showTagline = branding.source === "live" && branding.tagline !== null;
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="text-xs font-semibold leading-tight tracking-tight">Tactical Media Broadcast Hub</div>
<div className="truncate text-2xs text-text-faint" title={workspaceName}>
{workspaceName} ▾
<div className="flex items-start gap-2.5 border-b border-border-subtle px-4 py-4">
{branding.logoUrl !== null ? (
<img
src={branding.logoUrl}
alt={`${workspaceName} logo`}
width={30}
height={30}
className="mt-0.5 h-[30px] w-[30px] shrink-0 rounded object-contain"
/>
) : (
<LogoMark size={30} title="Tactical Media Broadcast Hub" />
)}
<div className="min-w-0 flex-1">
<div className="text-xs font-semibold leading-tight tracking-tight" title={workspaceName}>
{workspaceName}
</div>
{showTagline ? (
<div className="mt-0.5 truncate text-2xs leading-snug text-text-faint" title={branding.tagline ?? undefined}>
{branding.tagline}
</div>
) : (
<div className="truncate text-2xs text-text-faint" title={workspaceName}>
Tactical Media Broadcast Hub
</div>
)}
</div>
</div>
<nav className="flex-1 overflow-y-auto scrollbar-thin p-2">