backblaze-proofframe / apps /web /index.html
ADJCJH's picture
Sync closeout public sync hardening d429215
931c763 verified
Raw
History Blame Contribute Delete
69.4 kB
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>ProofFrame</title>
<link
rel="icon"
href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'%3E%3Crect width='32' height='32' fill='%23171714'/%3E%3Cpath d='M8 8h16v5H13v4h9v5h-9v6H8z' fill='%23fbfaf6'/%3E%3C/svg%3E"
/>
<style>
:root {
--paper: #fbfaf6;
--ink: #171714;
--muted: #6b6a61;
--rule: #d9d4c8;
--panel: #f1eee5;
--line: #c9c1b1;
--blue: #346c9c;
--coral: #c85c42;
--green: #2f815f;
--gold: #b88d37;
--mono: "IBM Plex Mono", "SFMono-Regular", Consolas, monospace;
--body: "Aptos", "Helvetica Neue", Helvetica, Arial, sans-serif;
--display: "Fraunces", Georgia, serif;
}
* {
box-sizing: border-box;
}
body {
margin: 0;
min-height: 100vh;
color: var(--ink);
background:
linear-gradient(90deg, rgba(23, 23, 20, 0.035) 1px, transparent 1px) 0 0 / 48px 48px,
linear-gradient(0deg, rgba(23, 23, 20, 0.03) 1px, transparent 1px) 0 0 / 48px 48px,
var(--paper);
font-family: var(--body);
letter-spacing: 0;
}
button,
input,
textarea {
font: inherit;
}
button {
border: 1px solid var(--ink);
background: var(--ink);
color: var(--paper);
min-height: 38px;
padding: 0 14px;
cursor: pointer;
}
button.secondary {
background: transparent;
color: var(--ink);
}
button.approve {
border-color: var(--green);
background: var(--green);
}
button.reject {
border-color: var(--coral);
background: var(--coral);
}
button:disabled {
cursor: not-allowed;
opacity: 0.48;
}
.shell {
display: grid;
grid-template-columns: 260px 1fr 360px;
min-height: 100vh;
}
.sidebar {
border-right: 1px solid var(--rule);
padding: 26px 22px;
background: rgba(241, 238, 229, 0.68);
}
.brand {
font-family: var(--display);
font-size: 31px;
font-weight: 700;
margin-bottom: 6px;
}
.tagline {
color: var(--muted);
font-size: 13px;
line-height: 1.4;
margin-bottom: 26px;
}
.status-stack {
border-top: 1px solid var(--line);
border-bottom: 1px solid var(--line);
padding: 14px 0;
display: grid;
gap: 12px;
}
.gate-panel {
border-bottom: 1px solid var(--line);
display: grid;
gap: 10px;
padding: 14px 0;
}
.gate-meter {
display: grid;
gap: 6px;
}
.gate-track {
background: rgba(23, 23, 20, 0.12);
height: 8px;
overflow: hidden;
}
.gate-fill {
background: var(--green);
height: 100%;
width: 0%;
}
.gate-list {
display: grid;
gap: 6px;
margin: 0;
padding: 0;
}
.gate-item {
align-items: start;
display: grid;
gap: 7px;
grid-template-columns: 56px 1fr;
}
.gate-pill {
border: 1px solid var(--line);
font-family: var(--mono);
font-size: 10px;
font-weight: 800;
min-height: 22px;
padding: 4px 6px;
text-align: center;
text-transform: uppercase;
}
.gate-pill.done {
border-color: var(--green);
color: var(--green);
}
.gate-pill.blocked {
border-color: var(--coral);
color: var(--coral);
}
.gate-pill.doing {
border-color: var(--gold);
color: var(--gold);
}
.gate-action {
color: var(--muted);
font-size: 12px;
line-height: 1.35;
}
.stat label,
.field label {
display: block;
color: var(--muted);
font-size: 11px;
font-weight: 700;
letter-spacing: 0.08em;
text-transform: uppercase;
margin-bottom: 5px;
}
.stat strong {
display: block;
font-size: 14px;
line-height: 1.25;
}
.campaign-list {
margin-top: 24px;
}
.campaign-item {
border-top: 1px solid var(--rule);
padding: 11px 0;
display: grid;
gap: 3px;
}
.campaign-item span {
color: var(--muted);
font-size: 12px;
}
main {
padding: 26px 30px 86px;
}
.topbar {
display: flex;
align-items: start;
justify-content: space-between;
gap: 24px;
border-bottom: 2px solid var(--ink);
padding-bottom: 20px;
}
.judge-slate {
background: rgba(255, 255, 255, 0.42);
border-bottom: 1px solid var(--rule);
border-top: 1px solid var(--ink);
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
margin: 18px 0 24px;
}
.slate-item {
min-height: 86px;
padding: 14px 16px;
border-right: 1px solid var(--rule);
}
.slate-item:last-child {
border-right: 0;
}
.slate-item:nth-child(3) {
border-right: 0;
}
.sponsor-map {
border-top: 1px solid var(--ink);
grid-column: 1 / -1;
padding: 14px 16px 16px;
}
.sponsor-map header {
align-items: baseline;
display: flex;
gap: 12px;
justify-content: space-between;
margin-bottom: 12px;
}
.sponsor-map h2 {
font-family: var(--display);
font-size: 21px;
line-height: 1;
margin: 0;
}
.sponsor-map header span {
color: var(--muted);
font-family: var(--mono);
font-size: 11px;
}
.sponsor-rail {
display: grid;
grid-template-columns: repeat(4, minmax(0, 1fr));
gap: 0;
border-top: 1px solid var(--rule);
}
.sponsor-node {
min-height: 82px;
border-right: 1px solid var(--rule);
padding: 12px 12px 0 0;
}
.sponsor-node:last-child {
border-right: 0;
}
.sponsor-node label {
color: var(--muted);
display: block;
font-size: 10px;
font-weight: 800;
letter-spacing: 0.08em;
margin-bottom: 8px;
text-transform: uppercase;
}
.sponsor-node strong {
display: block;
font-size: 14px;
line-height: 1.25;
margin-bottom: 6px;
}
.sponsor-node span {
color: var(--muted);
display: block;
font-family: var(--mono);
font-size: 11px;
line-height: 1.42;
overflow-wrap: anywhere;
}
.judge-brief-strip {
border-top: 1px solid var(--ink);
display: grid;
gap: 13px;
grid-column: 1 / -1;
padding: 14px 16px 16px;
}
.judge-brief-strip header {
align-items: baseline;
display: flex;
gap: 12px;
justify-content: space-between;
}
.judge-brief-strip h2 {
font-family: var(--display);
font-size: 21px;
line-height: 1;
margin: 0;
}
.judge-brief-strip header span {
color: var(--muted);
font-family: var(--mono);
font-size: 11px;
}
.brief-grid {
border-top: 1px solid var(--rule);
display: grid;
grid-template-columns: 1.25fr 0.7fr 1fr;
}
.brief-node {
border-right: 1px solid var(--rule);
min-height: 84px;
padding: 12px 14px 2px 0;
}
.brief-node:last-child {
border-right: 0;
}
.brief-node label {
color: var(--muted);
display: block;
font-size: 10px;
font-weight: 800;
letter-spacing: 0.08em;
margin-bottom: 8px;
text-transform: uppercase;
}
.brief-node strong {
display: block;
font-size: 16px;
line-height: 1.22;
margin-bottom: 7px;
}
.brief-node span {
color: var(--muted);
display: block;
font-family: var(--mono);
font-size: 11px;
line-height: 1.42;
overflow-wrap: anywhere;
}
.brief-links {
display: flex;
flex-wrap: wrap;
gap: 8px;
}
.brief-links a {
border: 1px solid var(--line);
color: var(--ink);
font-family: var(--mono);
font-size: 11px;
font-weight: 800;
padding: 7px 9px;
text-decoration: none;
}
.brief-links a:hover {
border-color: var(--ink);
background: rgba(23, 23, 20, 0.06);
}
.criteria-crosswalk-strip {
border-top: 1px solid var(--ink);
display: grid;
gap: 12px;
grid-column: 1 / -1;
padding: 14px 16px 16px;
}
.criteria-crosswalk-strip header {
align-items: baseline;
display: flex;
gap: 12px;
justify-content: space-between;
}
.criteria-crosswalk-strip h2 {
font-family: var(--display);
font-size: 21px;
line-height: 1;
margin: 0;
}
.criteria-crosswalk-strip header span {
color: var(--muted);
font-family: var(--mono);
font-size: 11px;
}
.crosswalk-grid {
border-top: 1px solid var(--rule);
display: grid;
grid-template-columns: repeat(4, minmax(0, 1fr));
}
.crosswalk-row {
border-right: 1px solid var(--rule);
min-height: 118px;
padding: 12px 12px 4px 0;
}
.crosswalk-row:last-child {
border-right: 0;
}
.crosswalk-row label {
color: var(--muted);
display: block;
font-size: 10px;
font-weight: 800;
letter-spacing: 0.08em;
margin-bottom: 8px;
text-transform: uppercase;
}
.crosswalk-row strong {
display: block;
font-size: 15px;
line-height: 1.22;
margin-bottom: 7px;
}
.crosswalk-row span {
color: var(--muted);
display: block;
font-family: var(--mono);
font-size: 11px;
line-height: 1.42;
overflow-wrap: anywhere;
}
.recording-runbook-strip {
border-top: 1px solid var(--ink);
display: grid;
gap: 12px;
grid-column: 1 / -1;
padding: 14px 16px 16px;
}
.recording-runbook-strip header {
align-items: baseline;
display: flex;
gap: 12px;
justify-content: space-between;
}
.recording-runbook-strip h2 {
font-family: var(--display);
font-size: 21px;
line-height: 1;
margin: 0;
}
.recording-runbook-strip header span {
color: var(--muted);
font-family: var(--mono);
font-size: 11px;
}
.recording-grid {
border-top: 1px solid var(--rule);
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
}
.recording-row {
border-right: 1px solid var(--rule);
min-height: 116px;
padding: 12px 12px 4px 0;
}
.recording-row:last-child {
border-right: 0;
}
.recording-row label {
color: var(--muted);
display: block;
font-size: 10px;
font-weight: 800;
letter-spacing: 0.08em;
margin-bottom: 8px;
text-transform: uppercase;
}
.recording-row strong {
display: block;
font-size: 15px;
line-height: 1.22;
margin-bottom: 7px;
}
.recording-row span {
color: var(--muted);
display: block;
font-family: var(--mono);
font-size: 11px;
line-height: 1.42;
overflow-wrap: anywhere;
}
.devpost-kit-strip {
border-top: 1px solid var(--ink);
display: grid;
gap: 12px;
grid-column: 1 / -1;
padding: 14px 16px 16px;
}
.devpost-kit-strip header {
align-items: baseline;
display: flex;
gap: 12px;
justify-content: space-between;
}
.devpost-kit-strip h2 {
font-family: var(--display);
font-size: 21px;
line-height: 1;
margin: 0;
}
.devpost-kit-strip header span {
color: var(--muted);
font-family: var(--mono);
font-size: 11px;
}
.devpost-grid {
border-top: 1px solid var(--rule);
display: grid;
grid-template-columns: 0.8fr 1fr 1fr;
}
.devpost-row {
border-right: 1px solid var(--rule);
min-height: 116px;
padding: 12px 12px 4px 0;
}
.devpost-row:last-child {
border-right: 0;
}
.devpost-row label {
color: var(--muted);
display: block;
font-size: 10px;
font-weight: 800;
letter-spacing: 0.08em;
margin-bottom: 8px;
text-transform: uppercase;
}
.devpost-row strong {
display: block;
font-size: 15px;
line-height: 1.22;
margin-bottom: 7px;
}
.devpost-row span {
color: var(--muted);
display: block;
font-family: var(--mono);
font-size: 11px;
line-height: 1.42;
overflow-wrap: anywhere;
}
.submit-checklist-strip,
.closeout-strip {
border-top: 1px solid var(--ink);
display: grid;
gap: 12px;
grid-column: 1 / -1;
padding: 14px 16px 16px;
}
.submit-checklist-strip header,
.closeout-strip header {
align-items: baseline;
display: flex;
gap: 12px;
justify-content: space-between;
}
.submit-checklist-strip h2,
.closeout-strip h2 {
font-family: var(--display);
font-size: 21px;
line-height: 1;
margin: 0;
}
.submit-checklist-strip header span,
.closeout-strip header span {
color: var(--muted);
font-family: var(--mono);
font-size: 11px;
}
.submit-grid,
.closeout-grid {
border-top: 1px solid var(--rule);
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
}
.submit-row,
.closeout-row {
border-right: 1px solid var(--rule);
min-height: 116px;
padding: 12px 12px 4px 0;
}
.submit-row:last-child,
.closeout-row:last-child {
border-right: 0;
}
.submit-row label,
.closeout-row label {
color: var(--muted);
display: block;
font-size: 10px;
font-weight: 800;
letter-spacing: 0;
margin-bottom: 8px;
text-transform: uppercase;
}
.submit-row strong,
.closeout-row strong {
display: block;
font-size: 15px;
line-height: 1.22;
margin-bottom: 7px;
}
.submit-row span,
.closeout-row span {
color: var(--muted);
display: block;
font-family: var(--mono);
font-size: 11px;
line-height: 1.42;
overflow-wrap: anywhere;
}
.slate-item label {
color: var(--muted);
display: block;
font-size: 10px;
font-weight: 800;
letter-spacing: 0.08em;
margin-bottom: 9px;
text-transform: uppercase;
}
.slate-item strong {
display: block;
font-size: 18px;
line-height: 1.15;
margin-bottom: 8px;
}
.slate-item span {
color: var(--muted);
display: block;
font-family: var(--mono);
font-size: 11px;
line-height: 1.45;
overflow-wrap: anywhere;
}
h1 {
font-family: var(--display);
font-size: 44px;
line-height: 1;
margin: 0 0 8px;
}
.subtle {
color: var(--muted);
line-height: 1.45;
margin: 0;
max-width: 680px;
}
.brief-panel {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 16px;
margin: 24px 0;
padding-bottom: 22px;
border-bottom: 1px solid var(--rule);
}
.field.wide {
grid-column: 1 / -1;
}
input,
textarea {
width: 100%;
border: 1px solid var(--line);
background: rgba(255, 255, 255, 0.58);
color: var(--ink);
padding: 11px 12px;
outline: none;
}
textarea {
min-height: 92px;
resize: vertical;
}
input:focus,
textarea:focus {
border-color: var(--blue);
box-shadow: inset 0 0 0 1px var(--blue);
}
.action-row {
display: flex;
align-items: center;
gap: 10px;
flex-wrap: wrap;
}
.review-console {
display: grid;
grid-template-columns: minmax(220px, 1fr) 180px 180px;
gap: 12px;
align-items: end;
margin: 0 0 18px;
}
.review-console select {
width: 100%;
min-height: 42px;
border: 1px solid var(--line);
background: rgba(255, 255, 255, 0.58);
color: var(--ink);
padding: 0 10px;
}
.scorecard {
display: grid;
grid-template-columns: repeat(5, minmax(0, 1fr));
border-top: 1px solid var(--ink);
border-bottom: 1px solid var(--rule);
margin: 0 0 18px;
}
.scorecard-item {
min-height: 74px;
padding: 11px 13px;
border-right: 1px solid var(--rule);
}
.scorecard-item:last-child {
border-right: 0;
}
.scorecard-item label {
color: var(--muted);
display: block;
font-size: 10px;
font-weight: 800;
letter-spacing: 0.08em;
margin-bottom: 8px;
text-transform: uppercase;
}
.scorecard-item strong {
display: block;
font-family: var(--display);
font-size: 26px;
line-height: 1;
}
.scorecard-item span {
color: var(--muted);
display: block;
font-size: 12px;
margin-top: 6px;
}
.ledger-head,
.asset-row {
display: grid;
grid-template-columns: 92px 1.25fr 0.8fr 0.9fr 112px;
gap: 14px;
align-items: center;
}
.ledger-head {
color: var(--muted);
font-size: 11px;
font-weight: 800;
letter-spacing: 0.08em;
text-transform: uppercase;
border-bottom: 1px solid var(--ink);
padding: 0 0 9px;
}
.asset-row {
min-height: 116px;
border-bottom: 1px solid var(--rule);
padding: 13px 0;
cursor: pointer;
}
.asset-row.selected {
background: linear-gradient(90deg, rgba(52, 108, 156, 0.09), transparent 75%);
}
.thumb {
width: 92px;
height: 62px;
object-fit: cover;
border: 1px solid var(--line);
background: var(--panel);
}
.asset-title {
display: block;
font-weight: 800;
margin-bottom: 5px;
}
.mono {
font-family: var(--mono);
font-size: 12px;
color: #34342f;
overflow-wrap: anywhere;
}
.status {
display: inline-flex;
width: fit-content;
align-items: center;
min-height: 24px;
padding: 0 9px;
border: 1px solid var(--line);
font-size: 12px;
font-weight: 800;
}
.status.approved {
border-color: var(--green);
color: var(--green);
}
.status.rejected {
border-color: var(--coral);
color: var(--coral);
}
.inspector {
border-left: 1px solid var(--rule);
padding: 26px 22px 82px;
background: rgba(255, 255, 255, 0.44);
}
.inspector h2 {
font-family: var(--display);
font-size: 28px;
margin: 0 0 14px;
}
.chain {
display: grid;
gap: 14px;
margin: 20px 0;
}
.chain-step {
border-top: 1px solid var(--rule);
padding-top: 12px;
}
.chain-step label {
color: var(--muted);
display: block;
font-size: 11px;
font-weight: 800;
letter-spacing: 0.08em;
margin-bottom: 5px;
text-transform: uppercase;
}
.manifest-box {
background: #181815;
color: #f6f2e8;
font-family: var(--mono);
font-size: 11px;
line-height: 1.55;
padding: 14px;
max-height: 240px;
overflow: auto;
white-space: pre-wrap;
}
.packet-bar {
display: flex;
align-items: center;
justify-content: space-between;
gap: 18px;
border-top: 1px solid var(--ink);
border-bottom: 1px solid var(--rule);
margin: 0 0 16px;
padding: 13px 0 14px;
}
.footer-note {
color: var(--muted);
font-size: 12px;
text-decoration: none;
}
.deerflow-signature {
border-top: 1px solid var(--rule);
color: var(--muted);
display: inline-block;
font-size: 11px;
margin-top: 20px;
padding-top: 12px;
text-decoration: none;
}
.deerflow-signature:hover {
color: var(--ink);
}
@media (max-width: 1100px) {
.shell {
grid-template-columns: 1fr;
}
.shell > * {
min-width: 0;
}
main {
padding: 22px 16px 72px;
}
h1 {
font-size: 36px;
}
.topbar {
flex-direction: column;
}
.sidebar,
.inspector {
border: 0;
}
.brief-panel {
grid-template-columns: 1fr;
}
.review-console,
.scorecard,
.judge-slate {
grid-template-columns: 1fr;
}
.sponsor-rail {
grid-template-columns: 1fr;
}
.brief-grid {
grid-template-columns: 1fr;
}
.crosswalk-grid {
grid-template-columns: 1fr;
}
.recording-grid {
grid-template-columns: 1fr;
}
.devpost-grid {
grid-template-columns: 1fr;
}
.submit-grid,
.closeout-grid {
grid-template-columns: 1fr;
}
.slate-item {
border-right: 0;
border-bottom: 1px solid var(--rule);
}
.sponsor-node {
border-right: 0;
border-bottom: 1px solid var(--rule);
padding-right: 0;
}
.sponsor-node:last-child {
border-bottom: 0;
}
.brief-node {
border-right: 0;
border-bottom: 1px solid var(--rule);
padding-right: 0;
}
.brief-node:last-child {
border-bottom: 0;
}
.crosswalk-row {
border-right: 0;
border-bottom: 1px solid var(--rule);
padding-right: 0;
}
.crosswalk-row:last-child {
border-bottom: 0;
}
.recording-row {
border-right: 0;
border-bottom: 1px solid var(--rule);
padding-right: 0;
}
.recording-row:last-child {
border-bottom: 0;
}
.devpost-row {
border-right: 0;
border-bottom: 1px solid var(--rule);
padding-right: 0;
}
.devpost-row:last-child {
border-bottom: 0;
}
.submit-row,
.closeout-row {
border-right: 0;
border-bottom: 1px solid var(--rule);
padding-right: 0;
}
.submit-row:last-child,
.closeout-row:last-child {
border-bottom: 0;
}
.packet-bar {
align-items: stretch;
flex-direction: column;
}
section[aria-label="Asset ledger"] {
overflow-x: auto;
}
.ledger-head,
.asset-row {
min-width: 640px;
}
.scorecard-item {
border-right: 0;
border-bottom: 1px solid var(--rule);
}
}
</style>
</head>
<body>
<div class="shell">
<aside class="sidebar">
<div class="brand">ProofFrame</div>
<p class="tagline">A storage-backed ledger for generated media approvals.</p>
<div class="status-stack">
<div class="stat">
<label>Storage</label>
<strong id="storageStatus">Checking...</strong>
</div>
<div class="stat">
<label>Generation</label>
<strong id="generationStatus">Checking...</strong>
</div>
<div class="stat">
<label>Submission Gate</label>
<strong id="submissionGateStatus">Checking...</strong>
</div>
</div>
<section class="gate-panel" aria-label="Submission readiness gate">
<div class="gate-meter">
<div class="stat">
<label>Final Gate Progress</label>
<strong id="gateProgress">0 / 6 complete</strong>
</div>
<div class="gate-track" aria-hidden="true">
<div class="gate-fill" id="gateFill"></div>
</div>
</div>
<div id="gateList" class="gate-list"></div>
<div class="gate-action" id="gateNextAction">Loading final submission blockers...</div>
</section>
<section class="campaign-list">
<div class="stat">
<label>Campaign Queue</label>
</div>
<div id="campaignList"></div>
</section>
<a class="deerflow-signature" href="https://deerflow.tech" target="_blank" rel="noreferrer">
Created By Deerflow
</a>
</aside>
<main>
<div class="topbar">
<div>
<h1>Generated Media Ledger</h1>
<p class="subtle">
Create a campaign packet, generate variants, approve usable assets, then export a
downloadable evidence packet with hashes, storage keys, provider metadata, and review
notes.
</p>
</div>
<div class="action-row">
<button id="judgeDemo">Judge Demo</button>
<button class="secondary" id="loadCampaigns">Refresh</button>
<button class="secondary" id="sampleBrief">Load Sample</button>
</div>
</div>
<section class="judge-slate" aria-label="Judge recording slate">
<div class="slate-item">
<label>Demo Mode</label>
<strong id="slateMode">Checking...</strong>
<span id="slateRuntime">Waiting for runtime.</span>
</div>
<div class="slate-item">
<label>Evidence Packet</label>
<strong id="slatePacket">No packet yet</strong>
<span id="slatePacketDetail">Awaiting manifest evidence.</span>
</div>
<div class="slate-item">
<label>Claim Boundary</label>
<strong id="slateClaim">Pre-live safe</strong>
<span id="slateClaimDetail">Live B2 and Genblaze proof remain gated.</span>
</div>
<div class="sponsor-map" aria-label="Sponsor evidence model">
<header>
<h2>Sponsor Evidence Model</h2>
<span id="sponsorModelStatus">B2/Genblaze final proof gated</span>
</header>
<div class="sponsor-rail">
<div class="sponsor-node">
<label>Genblaze Step</label>
<strong id="sponsorGenblaze">Provider path ready</strong>
<span id="sponsorGenblazeDetail">mock now / Genblaze final gate</span>
</div>
<div class="sponsor-node">
<label>B2 Object Route</label>
<strong id="sponsorB2">B2 adapter ready</strong>
<span id="sponsorB2Detail">private bucket prepared / live upload gated</span>
</div>
<div class="sponsor-node">
<label>Manifest Proof</label>
<strong id="sponsorManifest">Prompt + model + hash</strong>
<span id="sponsorManifestDetail">storage key and checksum travel together</span>
</div>
<div class="sponsor-node">
<label>Claim Mode</label>
<strong id="sponsorClaim">Pre-live safe</strong>
<span id="sponsorClaimDetail">no completed B2/Genblaze claim yet</span>
</div>
</div>
</div>
<div class="judge-brief-strip" aria-label="30-second judge brief">
<header>
<h2>30-Second Judge Brief</h2>
<span id="briefStatus">Loading safe boundary</span>
</header>
<div class="brief-grid">
<div class="brief-node">
<label>Outcome</label>
<strong id="briefOutcome">B2-ready evidence packets</strong>
<span id="briefOutcomeDetail">Generated media becomes manifest-backed approval evidence.</span>
</div>
<div class="brief-node">
<label>Award Posture</label>
<strong id="briefScore">Checking</strong>
<span id="briefScoreDetail">Final closure remains gated.</span>
</div>
<div class="brief-node">
<label>Still Gated</label>
<strong id="briefBlockers">Live proof pending</strong>
<span id="briefBlockersDetail">B2, Genblaze, video, audit, and Devpost receipt.</span>
</div>
</div>
<nav class="brief-links" aria-label="Judge evidence links">
<a
href="https://huggingface.co/spaces/ADJCJH/backblaze-proofframe/raw/main/docs/assets/judge-brief.md"
target="_blank"
rel="noreferrer"
>Judge brief</a>
<a
href="https://huggingface.co/spaces/ADJCJH/backblaze-proofframe/raw/main/docs/assets/judge-crosswalk.md"
target="_blank"
rel="noreferrer"
>Criteria crosswalk</a>
<a
href="https://huggingface.co/spaces/ADJCJH/backblaze-proofframe/raw/main/docs/assets/judge-decision-brief.md"
target="_blank"
rel="noreferrer"
>Decision brief</a>
<a
href="https://huggingface.co/spaces/ADJCJH/backblaze-proofframe/raw/main/docs/assets/judge-evidence-index.md"
target="_blank"
rel="noreferrer"
>Evidence index</a>
<a
href="https://huggingface.co/spaces/ADJCJH/backblaze-proofframe/raw/main/docs/assets/final-video-publish-kit.md"
target="_blank"
rel="noreferrer"
>Video publish kit</a>
<a
href="https://huggingface.co/spaces/ADJCJH/backblaze-proofframe/raw/main/docs/assets/recording-assets.md"
target="_blank"
rel="noreferrer"
>Recording runbook</a>
<a
href="https://huggingface.co/spaces/ADJCJH/backblaze-proofframe/raw/main/docs/assets/devpost-form-kit.md"
target="_blank"
rel="noreferrer"
>Devpost kit</a>
<a
href="https://huggingface.co/spaces/ADJCJH/backblaze-proofframe/raw/main/docs/assets/devpost-submission-checklist.md"
target="_blank"
rel="noreferrer"
>Submit checklist</a>
<a
href="https://huggingface.co/spaces/ADJCJH/backblaze-proofframe/raw/main/docs/assets/final-launch-plan.md"
target="_blank"
rel="noreferrer"
>Launch plan</a>
<a
href="https://huggingface.co/spaces/ADJCJH/backblaze-proofframe/raw/main/docs/assets/final-submission-control.md"
target="_blank"
rel="noreferrer"
>Final gate</a>
<a
href="https://huggingface.co/spaces/ADJCJH/backblaze-proofframe/raw/main/docs/assets/final-closeout-status.md"
target="_blank"
rel="noreferrer"
>Final closeout</a>
</nav>
</div>
<div class="criteria-crosswalk-strip" aria-label="Criteria crosswalk">
<header>
<h2>Criteria Crosswalk</h2>
<span id="crosswalkStatus">Loading official criteria</span>
</header>
<div class="crosswalk-grid" id="crosswalkRows">
<div class="crosswalk-row">
<label>Official Angle</label>
<strong>Real-world Utility</strong>
<span>Waiting for public-safe evidence map.</span>
</div>
</div>
</div>
<div class="recording-runbook-strip" aria-label="Recording runbook">
<header>
<h2>Recording Runbook</h2>
<span id="recordingStatus">Loading recording gate</span>
</header>
<div class="recording-grid" id="recordingRows">
<div class="recording-row">
<label>First Shot</label>
<strong>Judge slate</strong>
<span>Waiting for public-safe recording assets.</span>
</div>
</div>
</div>
<div class="devpost-kit-strip" aria-label="Devpost kit">
<header>
<h2>Devpost Kit</h2>
<span id="devpostStatus">Loading form gate</span>
</header>
<div class="devpost-grid" id="devpostRows">
<div class="devpost-row">
<label>Project Name</label>
<strong>ProofFrame</strong>
<span>Waiting for public-safe form kit.</span>
</div>
</div>
</div>
<div class="submit-checklist-strip" aria-label="Submit checklist">
<header>
<h2>Submit Checklist</h2>
<span id="submitStatus">Loading preflight</span>
</header>
<div class="submit-grid" id="submitRows">
<div class="submit-row">
<label>Preflight</label>
<strong>Final gate pending</strong>
<span>Waiting for public-safe submission checklist.</span>
</div>
</div>
</div>
<div class="closeout-strip" aria-label="Final closeout">
<header>
<h2>Final Closeout</h2>
<span id="closeoutStatus">Loading closeout gate</span>
</header>
<div class="closeout-grid" id="closeoutRows">
<div class="closeout-row">
<label>Next Command</label>
<strong>Credential entry</strong>
<span>Waiting for no-secret final closeout status.</span>
</div>
</div>
</div>
</section>
<section class="brief-panel" aria-label="Campaign brief">
<div class="field">
<label for="title">Campaign</label>
<input id="title" value="Neighborhood Museum Launch" />
</div>
<div class="field">
<label for="audience">Audience</label>
<input id="audience" value="local families and arts donors" />
</div>
<div class="field">
<label for="tone">Tone</label>
<input id="tone" value="warm editorial, precise, public-service" />
</div>
<div class="field">
<label for="variantCount">Variants</label>
<input id="variantCount" type="number" min="1" max="6" value="3" />
</div>
<div class="field wide">
<label for="brief">Brief</label>
<textarea id="brief">Announce a new neighborhood museum opening with generated visuals that feel archival, welcoming, and trustworthy.</textarea>
</div>
<div class="field wide action-row">
<button id="createCampaign">Create Campaign</button>
<button id="generateAssets" class="secondary" disabled>Generate Variants</button>
</div>
</section>
<section class="scorecard" aria-label="Evidence scorecard">
<div class="scorecard-item">
<label>Total Assets</label>
<strong id="scoreTotal">0</strong>
<span id="scoreBackend">No backend yet</span>
</div>
<div class="scorecard-item">
<label>Approved</label>
<strong id="scoreApproved">0</strong>
<span>ready for packet</span>
</div>
<div class="scorecard-item">
<label>Draft</label>
<strong id="scoreDraft">0</strong>
<span>needs decision</span>
</div>
<div class="scorecard-item">
<label>Rejected</label>
<strong id="scoreRejected">0</strong>
<span>kept for audit trail</span>
</div>
<div class="scorecard-item">
<label>Coverage</label>
<strong id="scoreCoverage">0%</strong>
<span>reviewed assets</span>
</div>
</section>
<section class="review-console" aria-label="Review console">
<div class="field">
<label for="assetSearch">Search Evidence</label>
<input id="assetSearch" placeholder="prompt, model, storage key, checksum" />
</div>
<div class="field">
<label for="statusFilter">Status</label>
<select id="statusFilter">
<option value="all">All assets</option>
<option value="draft">Draft</option>
<option value="approved">Approved</option>
<option value="rejected">Rejected</option>
</select>
</div>
<button class="secondary" id="copySummary" disabled>Copy Summary</button>
</section>
<section class="packet-bar" aria-label="Packet actions">
<div>
<strong id="packetStatus">No packet exported</strong>
<div class="mono" id="packetKey">Awaiting campaign manifest.</div>
</div>
<div class="action-row">
<button id="exportPacket" disabled>Export Manifest</button>
<button id="downloadPacket" class="secondary" disabled>Download ZIP</button>
<span class="footer-note">ProofFrame public mock demo</span>
</div>
</section>
<section aria-label="Asset ledger">
<div class="ledger-head">
<div>Preview</div>
<div>Asset</div>
<div>Storage Key</div>
<div>Checksum</div>
<div>Status</div>
</div>
<div id="assetRows"></div>
</section>
</main>
<aside class="inspector">
<h2>Provenance Chain</h2>
<p class="subtle" id="inspectorIntro">
Select an asset to inspect its brief, provider, B2-ready storage route, hash, and review
state.
</p>
<div class="chain">
<div class="chain-step">
<label>Brief</label>
<div id="chainBrief">No campaign yet.</div>
</div>
<div class="chain-step">
<label>Genblaze Step</label>
<div id="chainProvider" class="mono">mock provider until Genblaze credentials are set</div>
</div>
<div class="chain-step">
<label>B2 Object Route</label>
<div id="chainStorage" class="mono">Waiting for generated asset</div>
</div>
<div class="chain-step">
<label>Decision</label>
<div class="action-row">
<button id="approveAsset" class="approve" disabled>Approve</button>
<button id="rejectAsset" class="reject" disabled>Reject</button>
</div>
</div>
</div>
<div class="chain-step">
<label>Manifest Preview</label>
<pre class="manifest-box" id="manifestPreview">{}</pre>
</div>
</aside>
</div>
<script>
const state = {
campaign: null,
campaigns: [],
assets: [],
selectedAssetId: null,
manifest: {},
submissionGate: null,
judgeBrief: null,
judgeCrosswalk: null,
judgeRecording: null,
judgeDevpost: null,
judgeSubmitChecklist: null,
judgeCloseout: null,
health: null,
packetStatus: "No packet exported",
packetDetail: "Awaiting campaign manifest.",
filters: {
query: "",
status: "all",
},
};
const el = (id) => document.getElementById(id);
function escapeHtml(value) {
return String(value || "")
.replaceAll("&", "&amp;")
.replaceAll("<", "&lt;")
.replaceAll(">", "&gt;")
.replaceAll('"', "&quot;")
.replaceAll("'", "&#039;");
}
function short(value, length = 16) {
if (!value) return "";
return value.length > length ? `${value.slice(0, length)}...` : value;
}
function humanizeStatus(value) {
return String(value || "")
.replaceAll("_", " ")
.replace(/\b\w/g, (match) => match.toUpperCase())
.replaceAll("Pre Live", "Pre-Live")
.replaceAll("Post Live", "Post-Live");
}
function shouldAutoLoadJudgeDemo() {
const params = new URLSearchParams(window.location.search);
const value = String(params.get("judge") || params.get("demo") || "").toLowerCase();
return ["1", "true", "yes"].includes(value);
}
function filteredAssets() {
const query = state.filters.query.trim().toLowerCase();
return state.assets.filter((asset) => {
const statusMatch = state.filters.status === "all" || asset.status === state.filters.status;
const searchable = [
asset.prompt,
asset.provider,
asset.model,
asset.storage_backend,
asset.storage_key,
asset.sha256,
asset.risk_note,
]
.join(" ")
.toLowerCase();
return statusMatch && (!query || searchable.includes(query));
});
}
async function api(path, options = {}) {
const response = await fetch(path, {
headers: { "Content-Type": "application/json" },
...options,
});
if (!response.ok) {
const body = await response.text();
throw new Error(`${response.status} ${body}`);
}
return response.json();
}
async function loadHealth() {
const health = await api("/api/health");
state.health = health;
el("storageStatus").textContent = `${health.storage_backend}${
health.b2_configured ? " / B2 configured" : " / B2 pending"
}`;
el("generationStatus").textContent = `${health.generation_backend}${
health.genblaze_configured ? " / Genblaze configured" : " / Genblaze pending"
}`;
renderJudgeSlate();
}
async function loadSubmissionGate() {
state.submissionGate = await api("/api/submission/gate");
renderSubmissionGate();
}
async function loadJudgeBrief() {
try {
state.judgeBrief = await api("/api/judge/brief");
} catch (error) {
state.judgeBrief = { error: error.message };
}
renderJudgeBrief();
}
async function loadJudgeCrosswalk() {
try {
state.judgeCrosswalk = await api("/api/judge/crosswalk");
} catch (error) {
state.judgeCrosswalk = { error: error.message };
}
renderJudgeCrosswalk();
}
async function loadJudgeRecording() {
try {
state.judgeRecording = await api("/api/judge/recording");
} catch (error) {
state.judgeRecording = { error: error.message };
}
renderJudgeRecording();
}
async function loadJudgeDevpost() {
try {
state.judgeDevpost = await api("/api/judge/devpost");
} catch (error) {
state.judgeDevpost = { error: error.message };
}
renderJudgeDevpost();
}
async function loadJudgeSubmitChecklist() {
try {
state.judgeSubmitChecklist = await api("/api/judge/submission-checklist");
} catch (error) {
state.judgeSubmitChecklist = { error: error.message };
}
renderJudgeSubmitChecklist();
}
async function loadJudgeCloseout() {
try {
state.judgeCloseout = await api("/api/judge/final-closeout");
} catch (error) {
state.judgeCloseout = { error: error.message };
}
renderJudgeCloseout();
}
async function loadCampaigns() {
state.campaigns = await api("/api/campaigns");
renderCampaigns();
}
function renderSubmissionGate() {
const gate = state.submissionGate;
if (!gate) return;
const summary = gate.summary || { required: 6, done: 0 };
const percent = summary.required ? Math.round((summary.done / summary.required) * 100) : 0;
el("submissionGateStatus").textContent = gate.ok
? "Ready for Devpost submission"
: `${summary.done} / ${summary.required} final gates ready`;
el("gateProgress").textContent = `${summary.done} / ${summary.required} complete`;
el("gateFill").style.width = `${percent}%`;
el("gateList").innerHTML = (gate.task_gates || [])
.map(
(item) => `
<div class="gate-item">
<span class="gate-pill ${escapeHtml(item.status)}">${escapeHtml(item.status)}</span>
<div>
<strong>${escapeHtml(item.id)}</strong>
<div class="gate-action">${escapeHtml(short(item.title, 72))}</div>
</div>
</div>
`,
)
.join("");
const evidence = gate.evidence_gate?.ok ? "Live B2 + Genblaze proof verified." : "Live proof missing.";
const reports = gate.report_gate?.ok ? "Final reports verified." : "Final reports pending.";
const firstAction = gate.next_actions?.[0] || evidence;
el("gateNextAction").textContent = `${evidence} ${reports} Next: ${firstAction}`;
renderJudgeSlate();
}
function setPacketCopy(status, detail) {
state.packetStatus = status;
state.packetDetail = detail;
el("packetStatus").textContent = status;
el("packetKey").textContent = detail;
renderJudgeSlate();
}
function renderJudgeSlate() {
const health = state.health || {};
const gate = state.submissionGate || {};
const total = state.assets.length;
const approved = state.assets.filter((asset) => asset.status === "approved").length;
const firstAsset = state.assets[0];
const runtime = `${health.storage_backend || "checking"} storage / ${
health.generation_backend || "checking"
} generation`;
const finalReady = Boolean(gate.ok);
const claimMode = gate.mode === "final_ready" ? "Post-live verified" : "Pre-live safe";
const nextAction =
gate.next_actions?.[0] ||
(finalReady ? "Ready for final Devpost submission." : "Live B2 and Genblaze proof remain gated.");
el("slateMode").textContent = finalReady ? "Final-ready proof" : "Public mock demo";
el("slateRuntime").textContent = runtime;
el("slatePacket").textContent = total
? `${total} assets / ${approved} approved`
: state.packetStatus;
el("slatePacketDetail").textContent = state.packetDetail;
el("slateClaim").textContent = claimMode;
el("slateClaimDetail").textContent = nextAction;
el("sponsorModelStatus").textContent = finalReady
? "B2/Genblaze proof verified"
: "B2/Genblaze final proof gated";
el("sponsorGenblaze").textContent = health.genblaze_configured
? "Genblaze configured"
: "Provider path ready";
el("sponsorGenblazeDetail").textContent = firstAsset
? `${firstAsset.provider} / ${firstAsset.model}`
: "mock now / Genblaze final gate";
el("sponsorB2").textContent = health.b2_configured ? "B2 configured" : "B2 adapter ready";
el("sponsorB2Detail").textContent = firstAsset
? `${firstAsset.storage_backend} / ${short(firstAsset.storage_key, 42)}`
: "private bucket prepared / live upload gated";
el("sponsorManifest").textContent = total
? `${total} assets tracked`
: "Prompt + model + hash";
el("sponsorManifestDetail").textContent = firstAsset
? `sha256 ${short(firstAsset.sha256, 24)}`
: "storage key and checksum travel together";
el("sponsorClaim").textContent = claimMode;
el("sponsorClaimDetail").textContent = finalReady
? "safe to claim live sponsor proof"
: "no completed B2/Genblaze claim yet";
renderJudgeBrief();
}
function renderJudgeBrief() {
const brief = state.judgeBrief || {};
const status = brief.status || {};
const blockers = Array.isArray(brief.not_yet_claimed) ? brief.not_yet_claimed : [];
const safeToSubmit = Boolean(status.safe_to_submit);
const awardScore =
status.award_score == null || status.award_max_score == null
? "Checking"
: `${status.award_score}/${status.award_max_score}`;
const closureScore =
status.final_closure_score == null || status.final_closure_max_score == null
? "Final closure gated"
: `Final closure ${status.final_closure_score}/${status.final_closure_max_score}`;
el("briefStatus").textContent = brief.error
? "Brief unavailable"
: safeToSubmit
? "Final-ready"
: "Pre-live safe";
el("briefOutcome").textContent = brief.tagline || "B2-ready evidence packets";
el("briefOutcomeDetail").textContent =
brief.one_liner || "Generated media becomes manifest-backed approval evidence.";
el("briefScore").textContent = awardScore;
el("briefScoreDetail").textContent = `${humanizeStatus(
status.award_mode || "pre_live_competitive",
)} / ${closureScore}`;
el("briefBlockers").textContent = safeToSubmit
? "Ready for Devpost"
: `${blockers.length || 3} final claims gated`;
el("briefBlockersDetail").textContent =
blockers.slice(0, 2).join(" / ") || "B2, Genblaze, video, audit, and Devpost receipt.";
}
function renderJudgeCrosswalk() {
const crosswalk = state.judgeCrosswalk || {};
const rows = Array.isArray(crosswalk.rows) ? crosswalk.rows : [];
const safeToSubmit = Boolean(crosswalk.safe_to_submit);
el("crosswalkStatus").textContent = crosswalk.error
? "Crosswalk unavailable"
: safeToSubmit
? "Final-ready criteria map"
: humanizeStatus(crosswalk.mode || "pre_live_crosswalk_ready");
el("crosswalkRows").innerHTML = rows.length
? rows
.slice(0, 4)
.map(
(row) => `
<div class="crosswalk-row">
<label>${escapeHtml(humanizeStatus(row.readiness || "tracked"))}</label>
<strong>${escapeHtml(row.criterion || "Official criterion")}</strong>
<span>${escapeHtml(row.final_gate || row.safe_claim || "Evidence gate tracked.")}</span>
</div>
`,
)
.join("")
: `
<div class="crosswalk-row">
<label>Unavailable</label>
<strong>Criteria map pending</strong>
<span>${escapeHtml(crosswalk.error || "Waiting for generated judge crosswalk artifact.")}</span>
</div>
`;
}
function renderJudgeRecording() {
const recording = state.judgeRecording || {};
const shots = Array.isArray(recording.shot_plan) ? recording.shot_plan : [];
const nextActions = Array.isArray(recording.next_actions) ? recording.next_actions : [];
const finalVideoReady = Boolean(recording.final_video_ready);
el("recordingStatus").textContent = recording.error
? "Runbook unavailable"
: finalVideoReady
? "Final video ready"
: humanizeStatus(recording.mode || "mock_recording_ready");
el("recordingRows").innerHTML = shots.length
? shots
.slice(0, 3)
.map(
(shot) => `
<div class="recording-row">
<label>${escapeHtml(humanizeStatus(shot.id || "shot"))}</label>
<strong>${escapeHtml(shot.source || "Recording source")}</strong>
<span>${escapeHtml(shot.screen || "Screen detail tracked.")}</span>
</div>
`,
)
.join("")
: `
<div class="recording-row">
<label>Next Action</label>
<strong>Recording gate pending</strong>
<span>${escapeHtml(recording.error || nextActions[0] || "Waiting for generated recording assets.")}</span>
</div>
`;
}
function renderJudgeDevpost() {
const devpost = state.judgeDevpost || {};
const fields = Array.isArray(devpost.fields) ? devpost.fields : [];
const fieldById = Object.fromEntries(fields.map((field) => [field.id, field]));
const displayFields = ["project_name", "tagline", "video_url"]
.map((fieldId) => fieldById[fieldId])
.filter(Boolean);
const finalFormReady = Boolean(devpost.final_form_ready);
el("devpostStatus").textContent = devpost.error
? "Form kit unavailable"
: finalFormReady
? "Final form ready"
: humanizeStatus(devpost.mode || "pre_live_form_ready");
el("devpostRows").innerHTML = displayFields.length
? displayFields
.map(
(field) => `
<div class="devpost-row">
<label>${escapeHtml(field.ok_for_final ? "Final Ready" : "Final Pending")}</label>
<strong>${escapeHtml(field.label || "Devpost field")}</strong>
<span>${escapeHtml(field.value || "Awaiting final value.")}</span>
</div>
`,
)
.join("")
: `
<div class="devpost-row">
<label>Unavailable</label>
<strong>Form kit pending</strong>
<span>${escapeHtml(devpost.error || "Waiting for generated Devpost form kit.")}</span>
</div>
`;
}
function renderJudgeSubmitChecklist() {
const checklist = state.judgeSubmitChecklist || {};
const preflight = Array.isArray(checklist.preflight) ? checklist.preflight : [];
const stopRules = Array.isArray(checklist.stop_rules) ? checklist.stop_rules : [];
const safeToSubmit = Boolean(checklist.safe_to_submit);
el("submitStatus").textContent = checklist.error
? "Checklist unavailable"
: safeToSubmit
? "Ready to submit"
: humanizeStatus(checklist.mode || "pre_submit_blocked");
el("submitRows").innerHTML = preflight.length
? preflight
.slice(0, 3)
.map(
(item) => `
<div class="submit-row">
<label>${escapeHtml(item.ok ? "OK" : "Blocked")}</label>
<strong>${escapeHtml(humanizeStatus(item.id || "preflight"))}</strong>
<span>${escapeHtml(item.detail || item.evidence || "Preflight tracked.")}</span>
</div>
`,
)
.join("")
: `
<div class="submit-row">
<label>Stop Rule</label>
<strong>Submission gate pending</strong>
<span>${escapeHtml(checklist.error || stopRules[0] || "Waiting for generated Devpost submission checklist.")}</span>
</div>
`;
}
function renderJudgeCloseout() {
const closeout = state.judgeCloseout || {};
const gates = Array.isArray(closeout.gates) ? closeout.gates : [];
const blocked = gates.filter((gate) => gate && gate.ok === false);
const displayGates = blocked.length ? blocked.slice(0, 3) : gates.slice(0, 3);
const safeToSubmit = Boolean(closeout.safe_to_submit);
const nextCommand = closeout.error || closeout.next_command || closeout.next_detail;
const nextRow = `
<div class="closeout-row">
<label>${escapeHtml(closeout.error ? "Status" : "Next Command")}</label>
<strong>${escapeHtml(humanizeStatus(closeout.phase || "credential_entry"))}</strong>
<span>${escapeHtml(nextCommand || "Waiting for generated final closeout status.")}</span>
</div>
`;
el("closeoutStatus").textContent = closeout.error
? "Closeout unavailable"
: safeToSubmit
? "Final closeout ready"
: humanizeStatus(closeout.mode || "waiting_for_credentials");
el("closeoutRows").innerHTML = displayGates.length
? displayGates
.map(
(gate) => `
<div class="closeout-row">
<label>${escapeHtml(gate.ok ? "OK" : "Blocked")}</label>
<strong>${escapeHtml(humanizeStatus(gate.id || "closeout"))}</strong>
<span>${escapeHtml(gate.detail || gate.evidence || "Closeout gate tracked.")}</span>
</div>
`,
)
.join("") + nextRow
: nextRow;
}
function renderCampaigns() {
el("campaignList").innerHTML =
state.campaigns
.map(
(campaign) => `
<div class="campaign-item">
<strong>${escapeHtml(campaign.title)}</strong>
<span>${escapeHtml(new Date(campaign.created_at).toLocaleString())}</span>
</div>
`,
)
.join("") || '<p class="subtle">No campaigns yet.</p>';
}
function selectedAsset() {
return state.assets.find((asset) => asset.id === state.selectedAssetId) || state.assets[0];
}
function renderAssets() {
const visibleAssets = filteredAssets();
const rows = visibleAssets
.map(
(asset, index) => `
<div class="asset-row ${asset.id === state.selectedAssetId ? "selected" : ""}" data-id="${asset.id}">
<img class="thumb" src="${escapeHtml(asset.public_url || "")}" alt="Generated variant ${index + 1}" />
<div>
<span class="asset-title">Variant ${index + 1}</span>
<div class="mono">${escapeHtml(short(asset.prompt, 84))}</div>
</div>
<div class="mono">${escapeHtml(asset.storage_key)}</div>
<div class="mono">${escapeHtml(short(asset.sha256, 24))}</div>
<div><span class="status ${escapeHtml(asset.status)}">${escapeHtml(asset.status)}</span></div>
</div>
`,
)
.join("");
const emptyCopy = state.assets.length
? "No assets match the current review filter."
: "No generated assets yet.";
el("assetRows").innerHTML = rows || `<p class="subtle">${emptyCopy}</p>`;
document.querySelectorAll(".asset-row").forEach((row) => {
row.addEventListener("click", () => {
state.selectedAssetId = row.dataset.id;
renderAssets();
renderInspector();
});
});
renderScorecard();
}
function renderScorecard() {
const total = state.assets.length;
const approved = state.assets.filter((asset) => asset.status === "approved").length;
const rejected = state.assets.filter((asset) => asset.status === "rejected").length;
const draft = state.assets.filter((asset) => asset.status === "draft").length;
const reviewed = approved + rejected;
const coverage = total ? Math.round((reviewed / total) * 100) : 0;
const firstAsset = state.assets[0];
el("scoreTotal").textContent = total;
el("scoreApproved").textContent = approved;
el("scoreDraft").textContent = draft;
el("scoreRejected").textContent = rejected;
el("scoreCoverage").textContent = `${coverage}%`;
el("scoreBackend").textContent = firstAsset
? `${firstAsset.storage_backend} / ${firstAsset.provider}`
: "No backend yet";
el("copySummary").disabled = !state.campaign || total === 0;
renderJudgeSlate();
}
function renderInspector() {
const asset = selectedAsset();
el("chainBrief").textContent = state.campaign
? `${state.campaign.title}: ${state.campaign.brief}`
: "No campaign yet.";
el("chainProvider").textContent = asset
? `${asset.provider} / ${asset.model}`
: "mock provider until Genblaze credentials are set";
el("chainStorage").textContent = asset ? asset.storage_key : "Waiting for generated asset";
el("approveAsset").disabled = !asset;
el("rejectAsset").disabled = !asset;
el("exportPacket").disabled = !state.campaign || state.assets.length === 0;
el("downloadPacket").disabled = !state.campaign || state.assets.length === 0;
el("manifestPreview").textContent = JSON.stringify(state.manifest, null, 2);
}
async function refreshManifest() {
if (!state.campaign) return;
state.manifest = await api(`/api/campaigns/${state.campaign.id}/manifest`);
renderInspector();
}
async function createCampaign() {
state.campaign = await api("/api/campaigns", {
method: "POST",
body: JSON.stringify({
title: el("title").value,
brief: el("brief").value,
audience: el("audience").value,
tone: el("tone").value,
}),
});
state.assets = [];
state.selectedAssetId = null;
setPacketCopy("No packet exported", "Campaign manifest pending export.");
el("generateAssets").disabled = false;
await loadCampaigns();
await refreshManifest();
renderAssets();
}
async function generateAssets() {
const count = Number(el("variantCount").value || 3);
state.assets = await api(`/api/campaigns/${state.campaign.id}/generate?count=${count}`, {
method: "POST",
});
state.selectedAssetId = state.assets[0]?.id || null;
await refreshManifest();
renderAssets();
}
async function updateStatus(status) {
const asset = selectedAsset();
if (!asset) return;
const updated = await api(`/api/assets/${asset.id}/status`, {
method: "POST",
body: JSON.stringify({
status,
risk_note:
status === "approved"
? "Approved for demo use after provenance review."
: "Rejected pending provenance or brand review.",
}),
});
state.assets = state.assets.map((item) => (item.id === updated.id ? updated : item));
await refreshManifest();
renderAssets();
}
async function exportPacket() {
const packet = await api(`/api/campaigns/${state.campaign.id}/export`, { method: "POST" });
state.manifest = packet.manifest;
setPacketCopy("Manifest exported", packet.stored_manifest.storage_key);
renderInspector();
}
async function loadJudgeDemo(source = "manual") {
const demo = await api("/api/demo/judge-packet", { method: "POST" });
state.campaign = demo.campaign;
state.assets = demo.assets;
state.manifest = demo.manifest;
state.selectedAssetId = state.assets[0]?.id || null;
el("generateAssets").disabled = false;
await loadCampaigns();
renderAssets();
renderInspector();
setPacketCopy(
source === "auto" ? "Judge demo auto-loaded" : "Judge demo packet ready",
"Public-safe evidence ZIP staged.",
);
}
async function copySummary() {
if (!state.campaign || state.assets.length === 0) return;
const total = state.assets.length;
const approved = state.assets.filter((asset) => asset.status === "approved").length;
const rejected = state.assets.filter((asset) => asset.status === "rejected").length;
const draft = state.assets.filter((asset) => asset.status === "draft").length;
const firstAsset = state.assets[0];
const lines = [
`ProofFrame packet: ${state.campaign.title}`,
`Campaign ID: ${state.campaign.id}`,
`Assets: ${total} total, ${approved} approved, ${rejected} rejected, ${draft} draft`,
`Runtime: ${firstAsset.storage_backend} storage / ${firstAsset.provider} generation`,
`First asset checksum: ${firstAsset.sha256}`,
`First storage key: ${firstAsset.storage_key}`,
"No secrets, credentials, cookies, or signed URLs are included in this summary.",
];
await navigator.clipboard.writeText(lines.join("\n"));
setPacketCopy("Evidence summary copied", `${state.campaign.id} summary copied without secrets.`);
}
function downloadPacket() {
if (!state.campaign) return;
const link = document.createElement("a");
link.href = `/api/campaigns/${state.campaign.id}/packet.zip`;
link.download = `${state.campaign.id}-proofframe-packet.zip`;
document.body.appendChild(link);
link.click();
link.remove();
setPacketCopy("Evidence packet download started", `${state.campaign.id}-proofframe-packet.zip`);
}
el("createCampaign").addEventListener("click", createCampaign);
el("generateAssets").addEventListener("click", generateAssets);
el("approveAsset").addEventListener("click", () => updateStatus("approved"));
el("rejectAsset").addEventListener("click", () => updateStatus("rejected"));
el("exportPacket").addEventListener("click", exportPacket);
el("downloadPacket").addEventListener("click", downloadPacket);
el("copySummary").addEventListener("click", copySummary);
el("judgeDemo").addEventListener("click", () => loadJudgeDemo());
el("loadCampaigns").addEventListener("click", () => {
loadCampaigns();
loadSubmissionGate();
});
el("assetSearch").addEventListener("input", (event) => {
state.filters.query = event.target.value;
renderAssets();
});
el("statusFilter").addEventListener("change", (event) => {
state.filters.status = event.target.value;
renderAssets();
});
el("sampleBrief").addEventListener("click", () => {
el("title").value = "Indie Film Festival Packet";
el("audience").value = "festival programmers and local press";
el("tone").value = "cinematic, credible, storage-first";
el("brief").value =
"Generate a public-safe visual packet for a neighborhood film festival, with manifest evidence ready for client approval.";
});
async function boot() {
await Promise.all([
loadHealth(),
loadCampaigns(),
loadSubmissionGate(),
loadJudgeBrief(),
loadJudgeCrosswalk(),
loadJudgeRecording(),
loadJudgeDevpost(),
loadJudgeSubmitChecklist(),
loadJudgeCloseout(),
]);
renderInspector();
if (shouldAutoLoadJudgeDemo()) {
await loadJudgeDemo("auto");
}
}
boot().catch((error) => {
setPacketCopy("Runtime error", error.message);
});
</script>
</body>
</html>