Add Z-AMPP UI: screens-ingest + screens-admin: screens-admin.jsx

This commit is contained in:
Zac Gaetano 2026-05-22 08:22:38 -04:00
parent 20dfa504e5
commit 1eaf9dff5c

View file

@ -0,0 +1,661 @@
// screens-admin.jsx Users, Tokens, Containers, Cluster (graph), Settings
const { USERS, TOKENS_LIST, CONTAINERS, NODES } = window.ZAMPP_DATA;
/* ========== Users & Groups ========== */
function Users() {
const [tab, setTab] = React.useState("users");
return (
<div className="page">
<div className="page-header">
<h1>Users &amp; Groups</h1>
<div className="spacer" />
<button className="btn ghost sm"><Icon name="download" />Export</button>
<button className="btn primary"><Icon name="plus" />Invite user</button>
</div>
<div className="page-body">
<div className="tab-group" style={{ width: "fit-content", marginBottom: 12 }}>
<button className={tab === "users" ? "active" : ""} onClick={() => setTab("users")}>Users · {USERS.length}</button>
<button className={tab === "groups" ? "active" : ""} onClick={() => setTab("groups")}>Groups · 4</button>
<button className={tab === "policies" ? "active" : ""} onClick={() => setTab("policies")}>Policies · 7</button>
</div>
<div className="panel">
<div className="user-row head">
<div>User</div>
<div>Role</div>
<div>Groups</div>
<div>Last active</div>
<div></div>
</div>
{USERS.map(u => (
<div key={u.id} className="user-row">
<div style={{ display: "flex", alignItems: "center", gap: 10 }}>
<div className="avatar" style={{ width: 32, height: 32, fontSize: 11, background: avatarColor(u.avatar) }}>{u.avatar}</div>
<div>
<div style={{ fontWeight: 500, fontSize: 13 }}>{u.display}</div>
<div className="mono" style={{ fontSize: 11, color: "var(--text-3)" }}>@{u.username}</div>
</div>
</div>
<div><span className={`badge ${u.role === "admin" ? "purple" : u.role === "service" ? "neutral" : "accent"}`}>{u.role}</span></div>
<div style={{ display: "flex", gap: 4, flexWrap: "wrap" }}>
{u.groups.map(g => <span key={g} className="badge outline" style={{ textTransform: "lowercase" }}>{g}</span>)}
</div>
<div className="mono" style={{ fontSize: 11.5, color: "var(--text-3)" }}>{u.last}</div>
<div><button className="icon-btn"><Icon name="more" /></button></div>
</div>
))}
</div>
</div>
</div>
);
}
/* ========== Tokens (parody) ========== */
function Tokens() {
const [burned, setBurned] = React.useState(14340);
const [rate, setRate] = React.useState(2.4);
const [showCalc, setShowCalc] = React.useState(false);
React.useEffect(() => {
const i = setInterval(() => {
setBurned(b => b + Math.floor(Math.random() * 8) + 1);
setRate(r => Math.max(0.8, Math.min(8, r + (Math.random() - 0.5) * 0.4)));
}, 800);
return () => clearInterval(i);
}, []);
const burnSpark = React.useMemo(() => Array.from({ length: 40 }, (_, i) => 100 + i * 8 + Math.sin(i * 0.7) * 12), []);
const competitorSpark = React.useMemo(() => Array.from({ length: 40 }, (_, i) => 200 + i * 22 + Math.cos(i * 0.5) * 30), []);
const yourCostSpark = React.useMemo(() => Array.from({ length: 40 }, () => 1), []);
const [events, setEvents] = React.useState([
{ t: "21:14:02", action: "preview thumbnail generated", cost: 4 },
{ t: "21:14:01", action: "user clicked play", cost: 12 },
{ t: "21:13:58", action: "API health check", cost: 8 },
{ t: "21:13:54", action: "asset metadata read", cost: 2 },
{ t: "21:13:51", action: "session token refreshed", cost: 18 },
{ t: "21:13:47", action: "scrubbed timeline 1 frame", cost: 6 },
{ t: "21:13:42", action: "took a deep breath near the API", cost: 24 },
]);
React.useEffect(() => {
const actions = [
"preview thumbnail generated", "user clicked play", "API health check",
"scrubbed timeline 1 frame", "asset metadata read", "session token refreshed",
"checked job queue", "rendered a tooltip", "loaded sidebar icon",
"blinked", "made eye contact with the cluster", "opened a modal (twice)",
"asset list pagination request", "thought about a comment", "moved cursor near 'Save'",
];
const i = setInterval(() => {
const now = new Date();
const t = `${String(now.getHours()).padStart(2, "0")}:${String(now.getMinutes()).padStart(2, "0")}:${String(now.getSeconds()).padStart(2, "0")}`;
const a = actions[Math.floor(Math.random() * actions.length)];
const c = Math.floor(Math.random() * 28) + 1;
setEvents(ev => [{ t, action: a, cost: c }, ...ev].slice(0, 12));
}, 1600);
return () => clearInterval(i);
}, []);
const tiers = [
{ name: "Starter", desc: "For \"evaluation only\" — definitely not production", price: "$2,400", per: "/ month", tokens: "100k tokens", popular: false, color: "#6B7280" },
{ name: "Broadcast", desc: "Most teams. Most pain.", price: "$28,000", per: "/ month", tokens: "1.5M tokens", popular: true, color: "#5B7CFA" },
{ name: "Enterprise", desc: "If you have to ask, you can't afford it (but you'll ask anyway)", price: "$call us", per: "and bring lawyers", tokens: "∞ tokens", popular: false, color: "#B57CFA" },
];
return (
<div className="page">
<div className="page-header">
<h1>Tokens</h1>
<span className="subtitle">Token-metered pricing parody · You actually pay <strong style={{ color: "var(--success)" }}>$0.00</strong></span>
<div className="spacer" />
<span className="badge warning"><Icon name="alert" size={10} /> SATIRE</span>
<button className="btn ghost sm" onClick={() => setShowCalc(!showCalc)}><Icon name="sliders" />Cost calculator</button>
</div>
<div className="page-body">
<div className="token-hero">
<div className="token-burn-card">
<div className="token-card-label">TOKENS BURNED THIS SESSION</div>
<div className="token-counter">
<span className="token-flame">🔥</span>
<span className="token-big mono">{burned.toLocaleString()}</span>
</div>
<div className="token-rate">
<span className="mono" style={{ color: "var(--danger)" }}> {rate.toFixed(1)}k/sec</span>
<span style={{ color: "var(--text-3)", marginLeft: 10 }}>burning since you logged in</span>
</div>
<div style={{ marginTop: 12 }}>
<Sparkline data={burnSpark} color="#FF5B5B" />
</div>
</div>
<div className="token-actual-card">
<div className="token-card-label">WHAT YOU ACTUALLY PAY</div>
<div className="token-actual-amount">
<span style={{ fontSize: 48, fontWeight: 700, letterSpacing: "-0.04em" }}>$0</span>
<span style={{ fontSize: 18, color: "var(--text-3)" }}>.00</span>
</div>
<div style={{ fontSize: 12, color: "var(--text-3)", lineHeight: 1.5 }}>
Dragonflight is self-hosted. The tokens above are imaginary.<br />
Imagine them as a stress test for your sanity.
</div>
<div style={{ marginTop: 12 }}>
<Sparkline data={yourCostSpark} color="#2DD4A8" fill={false} />
</div>
</div>
</div>
<div className="token-comparison">
<div className="token-card-label" style={{ padding: "16px 16px 0" }}>HOURLY BURN DRAGONFLIGHT vs. THE OTHER GUYS</div>
<div className="token-compare-chart">
<ChartLine
series={[
{ label: "AMPP-style competitor", data: competitorSpark, color: "#FF5B5B" },
{ label: "Dragonflight (yours)", data: yourCostSpark.map((_, i) => i < 20 ? 1 : 1), color: "#2DD4A8" },
]}
/>
<div className="token-compare-legend">
<div><span className="dot" style={{ background: "#FF5B5B" }} />Competitor: $1,247/hr and rising</div>
<div><span className="dot" style={{ background: "#2DD4A8" }} />Dragonflight: $0.00/hr forever</div>
</div>
</div>
</div>
<div className="token-grid">
<div>
<div className="token-card-label" style={{ marginBottom: 8 }}>LIVE BILLING EVENTS</div>
<div className="panel">
{events.map((e, i) => (
<div key={i} className={`token-event ${i === 0 ? "fresh" : ""}`}>
<span className="mono" style={{ color: "var(--text-3)", fontSize: 11 }}>{e.t}</span>
<span style={{ flex: 1, fontSize: 12.5 }}>{e.action}</span>
<span className="mono" style={{ color: "var(--danger)", fontWeight: 600 }}>+{e.cost} tk</span>
</div>
))}
</div>
</div>
<div>
<div className="token-card-label" style={{ marginBottom: 8 }}>PRICING TIERS WE DIDN'T COPY</div>
<div className="token-tiers">
{tiers.map(t => (
<div key={t.name} className={`token-tier ${t.popular ? "popular" : ""}`}>
{t.popular && <span className="token-tier-badge">MOST PAIN</span>}
<div className="token-tier-name" style={{ color: t.color }}>{t.name}</div>
<div className="token-tier-desc">{t.desc}</div>
<div className="token-tier-price">
<span style={{ fontSize: 26, fontWeight: 700, letterSpacing: "-0.02em" }}>{t.price}</span>
<span style={{ fontSize: 12, color: "var(--text-3)", marginLeft: 4 }}>{t.per}</span>
</div>
<div className="token-tier-tokens mono">{t.tokens}</div>
<button className="btn subtle sm" disabled style={{ width: "100%", marginTop: 8 }}>Not for sale</button>
</div>
))}
</div>
</div>
</div>
{showCalc && <CostCalculator onClose={() => setShowCalc(false)} />}
<div className="token-footnote">
<Icon name="alert" size={14} />
<div>
<strong>Disclaimer:</strong> No actual tokens were billed in the making of this page. Wild Dragon's broadcast platform
is self-hosted infrastructure. Any resemblance to per-API-call broadcast token economies, living or dead, is satirical
and protected as commentary. If you came here looking for actual API tokens, that page no longer exists service
credentials are managed through the cluster's own JWT issuer (see <span style={{ color: "var(--accent-text)" }}>Settings AMPP Integration</span>).
</div>
</div>
</div>
</div>
);
}
function ChartLine({ series }) {
const w = 600, h = 140;
return (
<svg viewBox={`0 0 ${w} ${h}`} preserveAspectRatio="none" style={{ width: "100%", height: 140 }}>
<defs>
<pattern id="cgrid" width="60" height="28" patternUnits="userSpaceOnUse">
<path d="M 60 0 L 0 0 0 28" fill="none" stroke="rgba(255,255,255,0.04)" strokeWidth="1" />
</pattern>
</defs>
<rect width={w} height={h} fill="url(#cgrid)" />
{series.map((s, si) => {
const max = Math.max(...series.flatMap(x => x.data), 1);
const pts = s.data.map((d, i) => {
const x = (i / (s.data.length - 1)) * w;
const y = h - (d / max) * (h - 10) - 4;
return `${x},${y}`;
}).join(" ");
const area = `0,${h} ${pts} ${w},${h}`;
return (
<g key={si}>
<polygon points={area} fill={s.color} opacity="0.1" />
<polyline points={pts} fill="none" stroke={s.color} strokeWidth="2" />
<circle cx={w} cy={h - (s.data[s.data.length - 1] / max) * (h - 10) - 4} r="4" fill={s.color} />
<circle cx={w} cy={h - (s.data[s.data.length - 1] / max) * (h - 10) - 4} r="8" fill={s.color} opacity="0.3">
<animate attributeName="r" values="4;14;4" dur="2s" repeatCount="indefinite" />
<animate attributeName="opacity" values="0.6;0;0.6" dur="2s" repeatCount="indefinite" />
</circle>
</g>
);
})}
</svg>
);
}
function CostCalculator({ onClose }) {
const [users, setUsers] = React.useState(12);
const [assets, setAssets] = React.useState(500);
const [clicks, setClicks] = React.useState(2000);
const cost = users * 240 + assets * 8 + clicks * 0.12;
return (
<div className="modal-backdrop" onClick={onClose}>
<div className="modal" style={{ width: 520 }} onClick={e => e.stopPropagation()}>
<div className="modal-head">
<div>
<div style={{ fontSize: 15, fontWeight: 600 }}>Token Cost Calculator</div>
<div style={{ fontSize: 12, color: "var(--text-3)", marginTop: 2 }}>What it would cost on AMPP-style pricing</div>
</div>
<button className="icon-btn" onClick={onClose}><Icon name="x" /></button>
</div>
<div className="modal-body">
<CalcSlider label="Users" value={users} onChange={setUsers} min={1} max={100} unit=" people" />
<CalcSlider label="Assets in library" value={assets} onChange={setAssets} min={50} max={10000} step={50} unit="" />
<CalcSlider label="UI clicks per day" value={clicks} onChange={setClicks} min={100} max={20000} step={100} unit="" />
<div style={{ background: "var(--bg-2)", border: "1px solid var(--border)", borderRadius: 8, padding: 16, marginTop: 8 }}>
<div style={{ fontSize: 11, color: "var(--text-3)", textTransform: "uppercase", letterSpacing: "0.06em", fontWeight: 600 }}>You would be paying</div>
<div style={{ fontSize: 36, fontWeight: 700, color: "var(--danger)", letterSpacing: "-0.02em", marginTop: 4 }}>
${cost.toLocaleString("en-US", { maximumFractionDigits: 0 })}<span style={{ fontSize: 14, color: "var(--text-3)", fontWeight: 400, marginLeft: 4 }}>/ month</span>
</div>
<div style={{ marginTop: 8, padding: 10, background: "var(--success-soft)", borderRadius: 6, fontSize: 12.5, color: "var(--success)" }}>
<strong>Your actual Dragonflight cost:</strong> $0.00. You're welcome.
</div>
</div>
</div>
</div>
</div>
);
}
function CalcSlider({ label, value, onChange, min, max, step = 1, unit }) {
return (
<div>
<div style={{ display: "flex", justifyContent: "space-between", marginBottom: 6, fontSize: 12 }}>
<span style={{ color: "var(--text-2)" }}>{label}</span>
<span className="mono" style={{ color: "var(--text-1)", fontWeight: 600 }}>{value.toLocaleString()}{unit}</span>
</div>
<input
type="range"
min={min} max={max} step={step}
value={value}
onChange={e => onChange(Number(e.target.value))}
style={{ width: "100%", accentColor: "var(--accent)" }}
/>
</div>
);
}
/* ========== Containers ========== */
function Containers() {
return (
<div className="page">
<div className="page-header">
<h1>Containers</h1>
<span className="subtitle">Docker Compose services across the cluster</span>
<div className="spacer" />
<div className="status-pip">
<span className="dot" />
<span>{CONTAINERS.filter(c => c.state === "running").length} / {CONTAINERS.length} running</span>
</div>
<button className="btn ghost sm"><Icon name="refresh" />Refresh</button>
</div>
<div className="page-body">
<div className="panel">
<div className="container-row head">
<div>Container</div>
<div>Image</div>
<div>State</div>
<div>CPU</div>
<div>Memory</div>
<div>Ports</div>
<div></div>
</div>
{CONTAINERS.map(c => (
<div key={c.id} className="container-row">
<div>
<div style={{ fontWeight: 500, fontSize: 13 }}>{c.name}</div>
<div className="mono" style={{ fontSize: 11, color: "var(--text-3)" }}>up {c.uptime}</div>
</div>
<div className="mono" style={{ fontSize: 11.5, color: "var(--text-2)" }}>{c.image}</div>
<div>
<span className="badge success"><StatusDot status="online" /> RUNNING</span>
{c.healthy && <span style={{ fontSize: 10.5, color: "var(--success)", marginLeft: 6 }}>healthy</span>}
</div>
<div className="mono" style={{ fontSize: 11.5 }}>
<div style={{ display: "flex", alignItems: "center", gap: 6 }}>
<div style={{ width: 40, height: 4, background: "var(--bg-3)", borderRadius: 99, overflow: "hidden" }}>
<div style={{ width: `${Math.min(c.cpu * 4, 100)}%`, height: "100%", background: "var(--accent)" }} />
</div>
<span>{c.cpu.toFixed(1)}%</span>
</div>
</div>
<div className="mono" style={{ fontSize: 11.5 }}>{c.mem} MB</div>
<div className="mono" style={{ fontSize: 10.5, color: "var(--text-3)" }}>{c.ports}</div>
<div style={{ display: "flex", gap: 4 }}>
<button className="btn ghost sm">Logs</button>
<button className="btn ghost sm">Restart</button>
</div>
</div>
))}
</div>
</div>
</div>
);
}
/* ========== Cluster (topology graph) ========== */
function Cluster() {
const [hovered, setHovered] = React.useState(null);
const [selected, setSelected] = React.useState(NODES[0]);
const W = 720, H = 460;
const primary = NODES.find(n => n.role === "primary");
const edges = NODES.filter(n => n.id !== primary.id).map(n => ({
from: primary,
to: n,
alive: n.status === "online",
}));
return (
<div className="page">
<div className="page-header">
<h1>Cluster</h1>
<span className="subtitle">{NODES.filter(n => n.status === "online").length} of {NODES.length} nodes online</span>
<div className="spacer" />
<div className="status-pip"><span className="dot" /><span>Live</span></div>
<button className="btn ghost sm"><Icon name="refresh" />Refresh</button>
<button className="btn primary"><Icon name="plus" />Add node</button>
</div>
<div className="page-body">
<div className="stat-row" style={{ padding: 0, marginBottom: 16 }}>
<div className="stat-card">
<div className="label"><Icon name="cluster" size={12} />Nodes</div>
<div className="value">{NODES.length}</div>
<div className="delta">across 2 regions</div>
</div>
<div className="stat-card">
<div className="label"><Icon name="cpu" size={12} />Total CPU</div>
<div className="value">320 <span style={{ fontSize: 14, color: "var(--text-3)" }}>cores</span></div>
<div className="delta">14% utilized</div>
</div>
<div className="stat-card">
<div className="label"><Icon name="gpu" size={12} />GPUs</div>
<div className="value">5</div>
<div className="delta">3 idle · 2 transcoding</div>
</div>
<div className="stat-card">
<div className="label"><Icon name="hdd" size={12} />Memory</div>
<div className="value">243 <span style={{ fontSize: 14, color: "var(--text-3)" }}>GB</span></div>
<div className="delta">15% utilized</div>
</div>
</div>
<div style={{ display: "grid", gridTemplateColumns: "1fr 340px", gap: 16, alignItems: "start" }}>
<div className="cluster-canvas">
<div style={{ display: "flex", alignItems: "center", justifyContent: "space-between", padding: "12px 16px", borderBottom: "1px solid var(--border)" }}>
<span style={{ fontWeight: 600, fontSize: 13 }}>Topology</span>
<div className="tab-group">
<button className="active">Graph</button>
<button>Map</button>
<button>List</button>
</div>
</div>
<svg viewBox={`0 0 ${W} ${H}`} style={{ display: "block", width: "100%", height: "auto" }}>
<defs>
<radialGradient id="nodeGlow">
<stop offset="0%" stopColor="rgba(91,124,250,0.3)" />
<stop offset="100%" stopColor="rgba(91,124,250,0)" />
</radialGradient>
<pattern id="grid" width="40" height="40" patternUnits="userSpaceOnUse">
<path d="M 40 0 L 0 0 0 40" fill="none" stroke="rgba(255,255,255,0.025)" strokeWidth="1" />
</pattern>
</defs>
<rect width={W} height={H} fill="url(#grid)" />
{edges.map((e, i) => {
const x1 = e.from.x * W, y1 = e.from.y * H;
const x2 = e.to.x * W, y2 = e.to.y * H;
return (
<g key={i}>
<line x1={x1} y1={y1} x2={x2} y2={y2}
stroke={e.alive ? "var(--accent)" : "var(--text-4)"}
strokeWidth="1"
strokeDasharray={e.alive ? "0" : "4 3"}
opacity={e.alive ? 0.5 : 0.25}
/>
{e.alive && (
<circle r="3" fill="var(--accent)">
<animateMotion dur={`${2 + i * 0.4}s`} repeatCount="indefinite"
path={`M ${x1} ${y1} L ${x2} ${y2}`} />
</circle>
)}
</g>
);
})}
{NODES.map(n => {
const cx = n.x * W, cy = n.y * H;
const isHovered = hovered === n.id;
const isSelected = selected.id === n.id;
const color = n.status === "online" ? "var(--success)" : "var(--text-4)";
return (
<g key={n.id} transform={`translate(${cx}, ${cy})`}
style={{ cursor: "pointer" }}
onMouseEnter={() => setHovered(n.id)}
onMouseLeave={() => setHovered(null)}
onClick={() => setSelected(n)}>
{n.status === "online" && (
<circle r="44" fill="url(#nodeGlow)">
<animate attributeName="r" values="34;48;34" dur="3s" repeatCount="indefinite" />
</circle>
)}
<circle r={isSelected ? 26 : 22} fill="var(--bg-2)" stroke={isSelected ? "var(--accent)" : "var(--border-stronger)"} strokeWidth={isSelected ? 2 : 1} />
<circle r="6" cx="-13" cy="-13" fill={color} />
{n.role === "primary" && <path d="M -4 -2 L 0 2 L 4 -2 L 0 -6 Z" fill="var(--accent)" stroke="none" />}
{n.role !== "primary" && <text textAnchor="middle" y="3" fill="var(--text-2)" fontSize="10" fontFamily="var(--font-mono)">{n.role[0].toUpperCase()}</text>}
<text textAnchor="middle" y="40" fill={isSelected ? "var(--text-1)" : "var(--text-2)"} fontSize="11" fontWeight={isSelected ? 600 : 500}>{n.id}</text>
<text textAnchor="middle" y="54" fill="var(--text-3)" fontSize="10" fontFamily="var(--font-mono)">{n.ip}</text>
</g>
);
})}
</svg>
</div>
<div className="panel">
<div style={{ padding: "12px 16px", borderBottom: "1px solid var(--border)", display: "flex", alignItems: "center", gap: 8 }}>
<StatusDot status={selected.status} />
<span style={{ fontWeight: 600, fontSize: 13 }}>{selected.id}</span>
<span className={`badge ${selected.role === "primary" ? "accent" : "neutral"}`}>{selected.role}</span>
</div>
<div style={{ padding: 14, display: "flex", flexDirection: "column", gap: 10 }}>
<DetailRow k="Status" v={<span style={{ color: selected.status === "online" ? "var(--success)" : "var(--text-3)" }}>{selected.status}</span>} />
<DetailRow k="IP" v={selected.ip} mono />
<DetailRow k="Version" v={selected.version} mono />
<DetailRow k="Uptime" v={selected.uptime} mono />
<DetailRow k="CPU" v={
<div style={{ display: "flex", alignItems: "center", gap: 6 }}>
<div style={{ width: 80, height: 4, background: "var(--bg-3)", borderRadius: 99, overflow: "hidden" }}>
<div style={{ width: `${selected.cpu}%`, height: "100%", background: "var(--accent)" }} />
</div>
<span className="mono">{selected.cpu}%</span>
</div>
} />
<DetailRow k="Memory" v={
<div style={{ display: "flex", alignItems: "center", gap: 6 }}>
<div style={{ width: 80, height: 4, background: "var(--bg-3)", borderRadius: 99, overflow: "hidden" }}>
<div style={{ width: `${(selected.mem / selected.memTotal) * 100}%`, height: "100%", background: "var(--purple)" }} />
</div>
<span className="mono">{selected.mem} / {selected.memTotal} GB</span>
</div>
} />
{selected.gpus.length > 0 && (
<div>
<div style={{ fontSize: 11, color: "var(--text-3)", marginBottom: 6 }}>GPUs ({selected.gpus.length})</div>
{selected.gpus.map((g, i) => (
<div key={i} className="mono" style={{ fontSize: 11.5, padding: "5px 8px", background: "var(--bg-2)", borderRadius: 4, marginBottom: 4, display: "flex", alignItems: "center", gap: 6 }}>
<Icon name="gpu" size={11} style={{ color: "var(--text-3)" }} />
<span>{g}</span>
</div>
))}
</div>
)}
{selected.devices && (
<div>
<div style={{ fontSize: 11, color: "var(--text-3)", marginBottom: 6 }}>Capture devices</div>
{selected.devices.map((d, i) => (
<div key={i} className="mono" style={{ fontSize: 11.5, padding: "5px 8px", background: "var(--bg-2)", borderRadius: 4 }}>
<Icon name="video" size={11} style={{ color: "var(--text-3)", marginRight: 6 }} />{d}
</div>
))}
</div>
)}
<div style={{ display: "flex", gap: 6, marginTop: 6 }}>
<button className="btn ghost sm">Logs</button>
<button className="btn ghost sm">Drain</button>
{selected.role !== "primary" && <button className="btn danger sm">Remove</button>}
</div>
</div>
</div>
</div>
</div>
</div>
);
}
function DetailRow({ k, v, mono }) {
return (
<div style={{ display: "grid", gridTemplateColumns: "90px 1fr", alignItems: "center", fontSize: 12 }}>
<span style={{ color: "var(--text-3)" }}>{k}</span>
<span className={mono ? "mono" : ""} style={{ fontSize: mono ? 11.5 : 12 }}>{v}</span>
</div>
);
}
/* ========== Settings ========== */
function Settings() {
return (
<div className="page">
<div className="page-header">
<h1>Settings</h1>
<span className="subtitle">System configuration · changes apply without restart</span>
</div>
<div className="page-body">
<div style={{ display: "grid", gridTemplateColumns: "200px 1fr", gap: 24, alignItems: "start" }}>
<nav className="settings-nav">
{[
{ id: "storage", label: "S3 / Object storage", icon: "hdd" },
{ id: "gpu", label: "GPU / Transcoding", icon: "gpu" },
{ id: "sdi", label: "SDI capture", icon: "video" },
{ id: "ampp", label: "AMPP integration", icon: "link" },
{ id: "branding", label: "Branding", icon: "image" },
{ id: "logs", label: "Logs &amp; telemetry", icon: "list" },
].map((s, i) => (
<a key={s.id} className={`settings-nav-item ${i === 0 ? "active" : ""}`}>
<Icon name={s.icon} size={14} />{s.label}
</a>
))}
</nav>
<div style={{ display: "flex", flexDirection: "column", gap: 16 }}>
<SettingsCard
icon="hdd"
title="S3 / Object Storage"
sub="S3-compatible bucket for media asset storage"
tag={<span className="badge success">connected</span>}
>
<Field label="Endpoint URL" value="https://broadcastmgmt.wilddragon.net" mono />
<div style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: 12 }}>
<Field label="Region" value="us-east-1" mono />
<Field label="Bucket" value="dragonmam" mono />
</div>
<Field label="Access key ID" value="boLH2fUE7D6tzmgHvLb…" mono />
<Field label="Secret access key" value="••••••••••••••••" mono right={<button className="btn ghost sm">Show</button>} />
<div style={{ display: "flex", gap: 6, marginTop: 8 }}>
<button className="btn primary sm">Save &amp; apply</button>
<button className="btn ghost sm">Test connection</button>
</div>
</SettingsCard>
<SettingsCard
icon="gpu"
title="GPU / Transcoding"
sub="NVIDIA NVENC acceleration for proxy generation and transcoding jobs"
tag={<span className="badge accent">3 GPUs available</span>}
>
<label className="checkbox-row">
<input type="checkbox" defaultChecked /> Enable GPU-accelerated transcoding
</label>
<div style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: 12 }}>
<Field label="Encoder" value="H.264 (h264_nvenc)" select />
<Field label="Quality preset" value="p4 — medium (default)" select />
</div>
<div style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: 12 }}>
<Field label="Proxy bitrate" value="2 Mbps" mono />
<Field label="Transcoding node" value="Auto (first online with GPU)" select />
</div>
<div style={{ display: "flex", gap: 6, marginTop: 8 }}>
<button className="btn primary sm">Save GPU settings</button>
</div>
</SettingsCard>
<SettingsCard
icon="link"
title="AMPP Integration"
sub="Grass Valley AMPP platform connectivity for asset sync"
tag={<span className="badge warning">setup needed</span>}
>
<Field label="AMPP base URL" value="https://ampp.example.com" mono />
<Field label="Tenant ID" value="" placeholder="e.g. wild-dragon-prod" />
<div style={{ display: "flex", gap: 6, marginTop: 8 }}>
<button className="btn primary sm">Connect</button>
</div>
</SettingsCard>
</div>
</div>
</div>
</div>
);
}
function SettingsCard({ icon, title, sub, tag, children }) {
return (
<div className="settings-card">
<div className="settings-card-head">
<div className="settings-card-icon"><Icon name={icon} size={16} /></div>
<div style={{ flex: 1, minWidth: 0 }}>
<div style={{ fontWeight: 600, fontSize: 14 }}>{title}</div>
<div style={{ fontSize: 12, color: "var(--text-3)", marginTop: 2 }}>{sub}</div>
</div>
{tag}
</div>
<div className="settings-card-body">{children}</div>
</div>
);
}
function Field({ label, value, mono, select, placeholder, right }) {
return (
<div className="field">
<label className="field-label">{label}</label>
<div className="field-input-wrap">
{select ? (
<div className="field-input select">
<span className={mono ? "mono" : ""}>{value || placeholder}</span>
<Icon name="chevronDown" size={12} style={{ color: "var(--text-3)" }} />
</div>
) : (
<input className={`field-input ${mono ? "mono" : ""}`} defaultValue={value} placeholder={placeholder} />
)}
{right}
</div>
</div>
);
}
Object.assign(window, { Users, Tokens, Containers, Cluster, Settings });