cvpfus Codex commited on
Commit
eee651b
·
1 Parent(s): f32d42c

Add submission readiness rollup

Browse files

Co-authored-by: Codex <codex@openai.com>

Files changed (8) hide show
  1. FIELD_NOTES.md +4 -0
  2. README.md +5 -1
  3. SUBMISSION.md +3 -0
  4. app.py +87 -0
  5. scripts/verify.py +33 -1
  6. static/app.css +10 -1
  7. static/app.js +34 -0
  8. static/index.html +5 -0
FIELD_NOTES.md CHANGED
@@ -30,6 +30,8 @@ Runtime setup is also data-backed. `/api/runtime-setup` lists the app command, l
30
 
31
  The judge runbook lives at `/api/demo-script`. It keeps the live presentation repeatable by pairing visible actions with API checks for health, model budget, runtime setup, runtime status, image descriptions, reader narration, and speech.
32
 
 
 
33
  Image provenance lives in `/api/image-descriptions`. Each article illustration carries the planned FLUX.2 klein model id, prompt, seed, asset URL, and fallback status, and the session panel renders those receipts so the generated-image claim is inspectable.
34
 
35
  Accessibility evidence lives at `/api/accessibility-audit`. It records the reader-mode choices that matter most for this prototype: semantic reading order, keyboard navigation, reader cursor state, shortcut safety, live narration, image alt text, transcript review, user-controlled playback, and fallback resilience.
@@ -77,4 +79,6 @@ Every model-facing backend path reports `elapsed_ms`. The frontend surfaces the
77
 
78
  The app exposes `/api/award-evidence` and renders that data in the session panel, so the live demo can point directly to the hackathon bonus targets it is designed to satisfy.
79
 
 
 
80
  `/api/runtime-status` keeps the demo honest: if llama.cpp or Kokoro is unavailable, the UI labels the fallback state instead of silently pretending that every model path is online.
 
30
 
31
  The judge runbook lives at `/api/demo-script`. It keeps the live presentation repeatable by pairing visible actions with API checks for health, model budget, runtime setup, runtime status, image descriptions, reader narration, and speech.
32
 
33
+ Submission readiness lives at `/api/submission-readiness`. It aggregates the demo-critical checks into one payload: model budget, award evidence, custom frontend assets, runtime setup, accessibility audit, image receipts, and executable demo API checks.
34
+
35
  Image provenance lives in `/api/image-descriptions`. Each article illustration carries the planned FLUX.2 klein model id, prompt, seed, asset URL, and fallback status, and the session panel renders those receipts so the generated-image claim is inspectable.
36
 
37
  Accessibility evidence lives at `/api/accessibility-audit`. It records the reader-mode choices that matter most for this prototype: semantic reading order, keyboard navigation, reader cursor state, shortcut safety, live narration, image alt text, transcript review, user-controlled playback, and fallback resilience.
 
79
 
80
  The app exposes `/api/award-evidence` and renders that data in the session panel, so the live demo can point directly to the hackathon bonus targets it is designed to satisfy.
81
 
82
+ The app also renders `/api/submission-readiness`, so the Field Notes story has a single rollup of which submission claims are currently backed by app evidence.
83
+
84
  `/api/runtime-status` keeps the demo honest: if llama.cpp or Kokoro is unavailable, the UI labels the fallback state instead of silently pretending that every model path is online.
README.md CHANGED
@@ -52,7 +52,7 @@ Start the app:
52
  python app.py
53
  ```
54
 
55
- Open the local URL printed by Gradio. The custom frontend calls `/api/reader-brain`, `/api/image-descriptions`, `/api/describe-image`, `/api/speak`, `/api/generate-image`, `/api/model-budget`, `/api/runtime-setup`, `/api/demo-script`, and `/api/accessibility-audit`.
56
 
57
  Useful environment variables:
58
 
@@ -81,6 +81,8 @@ The verifier checks syntax, static assets, Space metadata consistency, determini
81
 
82
  `/api/image-descriptions` includes image-generation provenance for every article illustration: the planned FLUX.2 klein model, prompt, seed, bundled asset URL, and fallback-ready status.
83
 
 
 
84
  ## Screen Reader Mode
85
 
86
  The frontend builds a reading queue from semantic article nodes. When screen-reader mode is on:
@@ -120,6 +122,8 @@ Reader-brain, image-description, speech, and image-generation responses include
120
 
121
  The session panel also renders a manifest-backed demo checklist for Tiny Titan, Llama Champion, Off-Brand, and Field Notes evidence. A model-budget panel reads `/api/model-budget` so judges can see each role's parameter count in the live app.
122
 
 
 
123
  A runtime-plan panel reads `/api/runtime-setup` and summarizes each model path's runtime plus fallback, keeping the live demo honest about what is online and what is deterministic.
124
 
125
  `/api/runtime-status` performs a short readiness check for llama.cpp and local speech dependencies, then reports which fallback paths are ready for a live demo.
 
52
  python app.py
53
  ```
54
 
55
+ Open the local URL printed by Gradio. The custom frontend calls `/api/reader-brain`, `/api/image-descriptions`, `/api/describe-image`, `/api/speak`, `/api/generate-image`, `/api/model-budget`, `/api/runtime-setup`, `/api/demo-script`, `/api/accessibility-audit`, and `/api/submission-readiness`.
56
 
57
  Useful environment variables:
58
 
 
81
 
82
  `/api/image-descriptions` includes image-generation provenance for every article illustration: the planned FLUX.2 klein model, prompt, seed, bundled asset URL, and fallback-ready status.
83
 
84
+ `/api/submission-readiness` aggregates the judging receipts into one pass/fail payload covering model budget, award evidence, custom frontend assets, runtime setup, accessibility, image receipts, and demo API checks.
85
+
86
  ## Screen Reader Mode
87
 
88
  The frontend builds a reading queue from semantic article nodes. When screen-reader mode is on:
 
122
 
123
  The session panel also renders a manifest-backed demo checklist for Tiny Titan, Llama Champion, Off-Brand, and Field Notes evidence. A model-budget panel reads `/api/model-budget` so judges can see each role's parameter count in the live app.
124
 
125
+ A submission-readiness panel reads `/api/submission-readiness`, giving judges one compact rollup of the claims the live app can prove.
126
+
127
  A runtime-plan panel reads `/api/runtime-setup` and summarizes each model path's runtime plus fallback, keeping the live demo honest about what is online and what is deterministic.
128
 
129
  `/api/runtime-status` performs a short readiness check for llama.cpp and local speech dependencies, then reports which fallback paths are ready for a live demo.
SUBMISSION.md CHANGED
@@ -43,6 +43,7 @@ The prototype is designed for a live hackathon demo: every model-facing path has
43
  - `/api/accessibility-audit`: semantic queue, keyboard navigation, reader cursor state, shortcut safety, live narration, alt text, transcript, user control, and fallback evidence.
44
  - `/api/demo-script`: repeatable judge runbook and API checks.
45
  - `/api/image-descriptions`: generated article image descriptions plus prompt, seed, model, asset URL, and fallback status receipts.
 
46
 
47
  The POST checks in `/api/demo-script` include sample JSON bodies for `/api/reader-brain` and `/api/speak`.
48
 
@@ -50,6 +51,8 @@ The accessibility audit also documents two reader-mode details that matter durin
50
 
51
  The image receipts keep the generated-asset claim inspectable: each article illustration names the planned `black-forest-labs/FLUX.2-klein-4B` path, prompt, seed, and bundled fallback asset.
52
 
 
 
53
  ## Reliability notes
54
 
55
  The demo remains navigable when local models are unavailable. The reader brain falls back to deterministic narration, image descriptions fall back to cached alt text, speech falls back to browser speech plus transcript, and generated images fall back to bundled article assets. Fallback states are labeled instead of hidden.
 
43
  - `/api/accessibility-audit`: semantic queue, keyboard navigation, reader cursor state, shortcut safety, live narration, alt text, transcript, user control, and fallback evidence.
44
  - `/api/demo-script`: repeatable judge runbook and API checks.
45
  - `/api/image-descriptions`: generated article image descriptions plus prompt, seed, model, asset URL, and fallback status receipts.
46
+ - `/api/submission-readiness`: one pass/fail rollup for model budget, award evidence, custom frontend, runtime setup, accessibility, image receipts, and demo API checks.
47
 
48
  The POST checks in `/api/demo-script` include sample JSON bodies for `/api/reader-brain` and `/api/speak`.
49
 
 
51
 
52
  The image receipts keep the generated-asset claim inspectable: each article illustration names the planned `black-forest-labs/FLUX.2-klein-4B` path, prompt, seed, and bundled fallback asset.
53
 
54
+ The submission-readiness panel and API give judges a compact checklist for the whole build, so the live demo can move from individual receipts to an overall readiness view.
55
+
56
  ## Reliability notes
57
 
58
  The demo remains navigable when local models are unavailable. The reader brain falls back to deterministic narration, image descriptions fall back to cached alt text, speech falls back to browser speech plus transcript, and generated images fall back to bundled article assets. Fallback states are labeled instead of hidden.
app.py CHANGED
@@ -252,6 +252,7 @@ def demo_script_core() -> dict[str, Any]:
252
  {"method": "GET", "path": "/api/runtime-status", "expect": "online or fallback-ready runtime labels"},
253
  {"method": "GET", "path": "/api/accessibility-audit", "expect": "all reader-mode checks pass"},
254
  {"method": "GET", "path": "/api/image-descriptions", "expect": "three article image descriptions"},
 
255
  {
256
  "method": "POST",
257
  "path": "/api/reader-brain",
@@ -277,6 +278,82 @@ def demo_script_core() -> dict[str, Any]:
277
  }
278
 
279
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
280
  def accessibility_audit_core() -> dict[str, Any]:
281
  checks = [
282
  {
@@ -710,6 +787,11 @@ async def accessibility_audit() -> JSONResponse:
710
  return _json(accessibility_audit_core())
711
 
712
 
 
 
 
 
 
713
  @app.get("/api/runtime-status")
714
  async def runtime_status() -> JSONResponse:
715
  return _json(_runtime_status_core())
@@ -775,6 +857,11 @@ def accessibility_audit_api() -> str:
775
  return json.dumps(accessibility_audit_core())
776
 
777
 
 
 
 
 
 
778
  @app.api(name="speak")
779
  def speak_api(text: str, voice: str = "af_heart", speed: float = 1.0) -> str:
780
  return json.dumps(speak_core(text, voice, speed))
 
252
  {"method": "GET", "path": "/api/runtime-status", "expect": "online or fallback-ready runtime labels"},
253
  {"method": "GET", "path": "/api/accessibility-audit", "expect": "all reader-mode checks pass"},
254
  {"method": "GET", "path": "/api/image-descriptions", "expect": "three article image descriptions"},
255
+ {"method": "GET", "path": "/api/submission-readiness", "expect": "all submission readiness checks pass"},
256
  {
257
  "method": "POST",
258
  "path": "/api/reader-brain",
 
278
  }
279
 
280
 
281
+ def submission_readiness_core() -> dict[str, Any]:
282
+ model_budget = model_budget_core()
283
+ runtime_setup = runtime_setup_core()
284
+ accessibility = accessibility_audit_core()
285
+ demo_script = demo_script_core()
286
+ image_descriptions = describe_article_images_core()
287
+ runtime_roles = {step["role"] for step in runtime_setup["steps"]}
288
+ expected_roles = set(MODEL_MANIFEST)
289
+ api_paths = {item["path"] for item in demo_script["api_checks"]}
290
+ checks = [
291
+ {
292
+ "id": "tiny_titan_budget",
293
+ "label": "Tiny Titan model budget",
294
+ "status": "pass" if model_budget["all_models_within_limit"] else "fail",
295
+ "evidence": f"{len(model_budget['models'])} model roles are at or below {TINY_TITAN_LIMIT_B}B parameters.",
296
+ },
297
+ {
298
+ "id": "award_targets",
299
+ "label": "Targeted award evidence",
300
+ "status": "pass" if len(AWARD_EVIDENCE) == 4 else "fail",
301
+ "evidence": "Tiny Titan, Llama Champion, Off-Brand, and Field Notes evidence is exposed.",
302
+ },
303
+ {
304
+ "id": "custom_frontend",
305
+ "label": "Custom frontend",
306
+ "status": "pass" if all((STATIC_DIR / name).exists() for name in ["index.html", "app.css", "app.js"]) else "fail",
307
+ "evidence": "The app serves custom HTML, CSS, and JavaScript through Gradio Server.",
308
+ },
309
+ {
310
+ "id": "runtime_setup",
311
+ "label": "Runtime setup",
312
+ "status": "pass" if runtime_roles == expected_roles else "fail",
313
+ "evidence": "Runtime setup covers reader brain, speech, vision, and image generation paths.",
314
+ },
315
+ {
316
+ "id": "reader_accessibility",
317
+ "label": "Reader accessibility",
318
+ "status": "pass" if accessibility["all_passed"] else "fail",
319
+ "evidence": f"{accessibility['passed_checks']} of {accessibility['total_checks']} accessibility checks pass.",
320
+ },
321
+ {
322
+ "id": "image_receipts",
323
+ "label": "Image receipts",
324
+ "status": "pass"
325
+ if all(
326
+ item.get("generation_model") == MODEL_MANIFEST["image_generation"]["id"] and isinstance(item.get("seed"), int)
327
+ for item in image_descriptions["descriptions"]
328
+ )
329
+ else "fail",
330
+ "evidence": "Every article illustration exposes prompt, seed, model, asset URL, and fallback status.",
331
+ },
332
+ {
333
+ "id": "demo_api_checks",
334
+ "label": "Demo API checks",
335
+ "status": "pass"
336
+ if {
337
+ "/api/model-budget",
338
+ "/api/runtime-setup",
339
+ "/api/accessibility-audit",
340
+ "/api/image-descriptions",
341
+ "/api/reader-brain",
342
+ "/api/speak",
343
+ }.issubset(api_paths)
344
+ else "fail",
345
+ "evidence": "The judge runbook includes GET evidence checks and executable POST sample bodies.",
346
+ },
347
+ ]
348
+ return {
349
+ "ok": True,
350
+ "all_passed": all(check["status"] == "pass" for check in checks),
351
+ "passed_checks": sum(1 for check in checks if check["status"] == "pass"),
352
+ "total_checks": len(checks),
353
+ "checks": checks,
354
+ }
355
+
356
+
357
  def accessibility_audit_core() -> dict[str, Any]:
358
  checks = [
359
  {
 
787
  return _json(accessibility_audit_core())
788
 
789
 
790
+ @app.get("/api/submission-readiness")
791
+ async def submission_readiness() -> JSONResponse:
792
+ return _json(submission_readiness_core())
793
+
794
+
795
  @app.get("/api/runtime-status")
796
  async def runtime_status() -> JSONResponse:
797
  return _json(_runtime_status_core())
 
857
  return json.dumps(accessibility_audit_core())
858
 
859
 
860
+ @app.api(name="submission_readiness")
861
+ def submission_readiness_api() -> str:
862
+ return json.dumps(submission_readiness_core())
863
+
864
+
865
  @app.api(name="speak")
866
  def speak_api(text: str, voice: str = "af_heart", speed: float = 1.0) -> str:
867
  return json.dumps(speak_core(text, voice, speed))
scripts/verify.py CHANGED
@@ -39,6 +39,7 @@ def verify_static_assets() -> None:
39
  assert_true('aria-live="polite"' in index_html, "Article should expose an aria-live narration region")
40
  assert_true("transcriptLog" in index_html, "Article should expose a visible transcript log")
41
  assert_true("imageReceiptList" in index_html, "Article should expose generated image receipts")
 
42
  assert_true("function haltPlayback" in app_js, "Reader controls should expose a shared playback halt helper")
43
  assert_true(
44
  "haltPlayback({ clearAutoAdvance: false });" in app_js,
@@ -52,7 +53,13 @@ def verify_static_assets() -> None:
52
  submission = (ROOT / "SUBMISSION.md").read_text(encoding="utf-8")
53
  for target in ["Tiny Titan", "Llama Champion", "Off-Brand", "Field Notes"]:
54
  assert_true(target in submission, f"Submission packet should mention {target}")
55
- for endpoint in ["/api/model-budget", "/api/runtime-setup", "/api/accessibility-audit", "/api/demo-script"]:
 
 
 
 
 
 
56
  assert_true(endpoint in submission, f"Submission packet should mention {endpoint}")
57
  assert_true(
58
  "nvidia/NVIDIA-Nemotron-3-Nano-4B-GGUF" in submission,
@@ -194,6 +201,8 @@ def verify_routes() -> None:
194
  assert_true("autoAdvanceControl" in home.text, "Home route should include auto-advance control")
195
  assert_true("transcriptLog" in home.text, "Home route should include transcript log")
196
  assert_true("awardEvidenceList" in home.text, "Home route should include award evidence list")
 
 
197
  assert_true("budgetStatus" in home.text, "Home route should include model budget status")
198
  assert_true("modelBudgetList" in home.text, "Home route should include model budget list")
199
  assert_true("runtimeSetupStatus" in home.text, "Home route should include runtime setup status")
@@ -309,6 +318,10 @@ def verify_routes() -> None:
309
  any(item["path"] == "/api/accessibility-audit" for item in demo_payload["api_checks"]),
310
  "Demo script should include the accessibility audit check",
311
  )
 
 
 
 
312
  reader_check = next(item for item in demo_payload["api_checks"] if item["path"] == "/api/reader-brain")
313
  speech_check = next(item for item in demo_payload["api_checks"] if item["path"] == "/api/speak")
314
  assert_true(reader_check["sample_body"]["mode"] == "narrate", "Reader-brain demo check should include a sample body")
@@ -351,6 +364,25 @@ def verify_routes() -> None:
351
  "Accessibility audit should cover reader semantics, keyboard use, cursor state, shortcut safety, live narration, alt text, and transcript",
352
  )
353
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
354
  runtime = client.get("/api/runtime-status")
355
  assert_true(runtime.status_code == 200, "Runtime status route should return 200")
356
  runtime_payload = runtime.json()
 
39
  assert_true('aria-live="polite"' in index_html, "Article should expose an aria-live narration region")
40
  assert_true("transcriptLog" in index_html, "Article should expose a visible transcript log")
41
  assert_true("imageReceiptList" in index_html, "Article should expose generated image receipts")
42
+ assert_true("submissionReadinessList" in index_html, "Article should expose submission readiness checks")
43
  assert_true("function haltPlayback" in app_js, "Reader controls should expose a shared playback halt helper")
44
  assert_true(
45
  "haltPlayback({ clearAutoAdvance: false });" in app_js,
 
53
  submission = (ROOT / "SUBMISSION.md").read_text(encoding="utf-8")
54
  for target in ["Tiny Titan", "Llama Champion", "Off-Brand", "Field Notes"]:
55
  assert_true(target in submission, f"Submission packet should mention {target}")
56
+ for endpoint in [
57
+ "/api/model-budget",
58
+ "/api/runtime-setup",
59
+ "/api/accessibility-audit",
60
+ "/api/demo-script",
61
+ "/api/submission-readiness",
62
+ ]:
63
  assert_true(endpoint in submission, f"Submission packet should mention {endpoint}")
64
  assert_true(
65
  "nvidia/NVIDIA-Nemotron-3-Nano-4B-GGUF" in submission,
 
201
  assert_true("autoAdvanceControl" in home.text, "Home route should include auto-advance control")
202
  assert_true("transcriptLog" in home.text, "Home route should include transcript log")
203
  assert_true("awardEvidenceList" in home.text, "Home route should include award evidence list")
204
+ assert_true("submissionReadinessStatus" in home.text, "Home route should include submission readiness status")
205
+ assert_true("submissionReadinessList" in home.text, "Home route should include submission readiness list")
206
  assert_true("budgetStatus" in home.text, "Home route should include model budget status")
207
  assert_true("modelBudgetList" in home.text, "Home route should include model budget list")
208
  assert_true("runtimeSetupStatus" in home.text, "Home route should include runtime setup status")
 
318
  any(item["path"] == "/api/accessibility-audit" for item in demo_payload["api_checks"]),
319
  "Demo script should include the accessibility audit check",
320
  )
321
+ assert_true(
322
+ any(item["path"] == "/api/submission-readiness" for item in demo_payload["api_checks"]),
323
+ "Demo script should include the submission readiness check",
324
+ )
325
  reader_check = next(item for item in demo_payload["api_checks"] if item["path"] == "/api/reader-brain")
326
  speech_check = next(item for item in demo_payload["api_checks"] if item["path"] == "/api/speak")
327
  assert_true(reader_check["sample_body"]["mode"] == "narrate", "Reader-brain demo check should include a sample body")
 
364
  "Accessibility audit should cover reader semantics, keyboard use, cursor state, shortcut safety, live narration, alt text, and transcript",
365
  )
366
 
367
+ readiness = client.get("/api/submission-readiness")
368
+ assert_true(readiness.status_code == 200, "Submission readiness route should return 200")
369
+ readiness_payload = readiness.json()
370
+ assert_true(readiness_payload["ok"], "Submission readiness payload should be ok")
371
+ assert_true(readiness_payload["all_passed"], "Submission readiness checks should pass")
372
+ assert_true(
373
+ {item["id"] for item in readiness_payload["checks"]}
374
+ >= {
375
+ "tiny_titan_budget",
376
+ "award_targets",
377
+ "custom_frontend",
378
+ "runtime_setup",
379
+ "reader_accessibility",
380
+ "image_receipts",
381
+ "demo_api_checks",
382
+ },
383
+ "Submission readiness should aggregate model, award, frontend, runtime, accessibility, image, and demo evidence",
384
+ )
385
+
386
  runtime = client.get("/api/runtime-status")
387
  assert_true(runtime.status_code == 200, "Runtime status route should return 200")
388
  runtime_payload = runtime.json()
static/app.css CHANGED
@@ -290,6 +290,14 @@ dd {
290
  margin-top: 22px;
291
  }
292
 
 
 
 
 
 
 
 
 
293
  .budget-header,
294
  .image-receipt-header {
295
  display: flex;
@@ -309,7 +317,8 @@ dd {
309
 
310
  .budget-header span,
311
  .runtime-header span,
312
- .image-receipt-header span {
 
313
  color: var(--accent-strong);
314
  font-size: 0.76rem;
315
  font-weight: 850;
 
290
  margin-top: 22px;
291
  }
292
 
293
+ .submission-header {
294
+ display: flex;
295
+ align-items: center;
296
+ justify-content: space-between;
297
+ gap: 10px;
298
+ margin-top: 22px;
299
+ }
300
+
301
  .budget-header,
302
  .image-receipt-header {
303
  display: flex;
 
317
 
318
  .budget-header span,
319
  .runtime-header span,
320
+ .image-receipt-header span,
321
+ .submission-header span {
322
  color: var(--accent-strong);
323
  font-size: 0.76rem;
324
  font-weight: 850;
static/app.js CHANGED
@@ -18,6 +18,8 @@ const transcriptLog = document.querySelector("#transcriptLog");
18
  const copyTranscriptButton = document.querySelector("#copyTranscriptButton");
19
  const clearTranscriptButton = document.querySelector("#clearTranscriptButton");
20
  const awardEvidenceList = document.querySelector("#awardEvidenceList");
 
 
21
  const budgetStatus = document.querySelector("#budgetStatus");
22
  const modelBudgetList = document.querySelector("#modelBudgetList");
23
  const runtimeSetupStatus = document.querySelector("#runtimeSetupStatus");
@@ -224,6 +226,37 @@ async function loadAwardEvidence() {
224
  }
225
  }
226
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
227
  function roleLabel(role) {
228
  return role
229
  .split("_")
@@ -651,6 +684,7 @@ controls.play.addEventListener("click", () => {
651
 
652
  loadManifest();
653
  loadAwardEvidence();
 
654
  loadModelBudget();
655
  loadRuntimeSetup();
656
  loadRuntimeStatus();
 
18
  const copyTranscriptButton = document.querySelector("#copyTranscriptButton");
19
  const clearTranscriptButton = document.querySelector("#clearTranscriptButton");
20
  const awardEvidenceList = document.querySelector("#awardEvidenceList");
21
+ const submissionReadinessStatus = document.querySelector("#submissionReadinessStatus");
22
+ const submissionReadinessList = document.querySelector("#submissionReadinessList");
23
  const budgetStatus = document.querySelector("#budgetStatus");
24
  const modelBudgetList = document.querySelector("#modelBudgetList");
25
  const runtimeSetupStatus = document.querySelector("#runtimeSetupStatus");
 
226
  }
227
  }
228
 
229
+ async function loadSubmissionReadiness() {
230
+ try {
231
+ const payload = await postJson("/api/submission-readiness");
232
+ submissionReadinessStatus.textContent = payload.all_passed
233
+ ? `${payload.passed_checks}/${payload.total_checks} pass`
234
+ : "Review needed";
235
+ submissionReadinessList.innerHTML = payload.checks
236
+ .map((item) => `
237
+ <li>
238
+ <div class="award-title">
239
+ <span>${escapeHtml(item.label)}</span>
240
+ <span class="award-status">${escapeHtml(item.status)}</span>
241
+ </div>
242
+ <p class="award-evidence">${escapeHtml(item.evidence)}</p>
243
+ </li>
244
+ `)
245
+ .join("");
246
+ } catch {
247
+ submissionReadinessStatus.textContent = "Unavailable";
248
+ submissionReadinessList.innerHTML = `
249
+ <li>
250
+ <div class="award-title">
251
+ <span>Submission readiness</span>
252
+ <span class="award-status">offline</span>
253
+ </div>
254
+ <p class="award-evidence">Submission readiness evidence is unavailable.</p>
255
+ </li>
256
+ `;
257
+ }
258
+ }
259
+
260
  function roleLabel(role) {
261
  return role
262
  .split("_")
 
684
 
685
  loadManifest();
686
  loadAwardEvidence();
687
+ loadSubmissionReadiness();
688
  loadModelBudget();
689
  loadRuntimeSetup();
690
  loadRuntimeStatus();
static/index.html CHANGED
@@ -126,6 +126,11 @@
126
  <h3>Demo Checklist</h3>
127
  </div>
128
  <ul id="awardEvidenceList" class="award-list" aria-label="Hackathon bonus evidence"></ul>
 
 
 
 
 
129
  <div class="budget-header">
130
  <h3>Model Budget</h3>
131
  <span id="budgetStatus">Checking</span>
 
126
  <h3>Demo Checklist</h3>
127
  </div>
128
  <ul id="awardEvidenceList" class="award-list" aria-label="Hackathon bonus evidence"></ul>
129
+ <div class="submission-header">
130
+ <h3>Submission Readiness</h3>
131
+ <span id="submissionReadinessStatus">Checking</span>
132
+ </div>
133
+ <ul id="submissionReadinessList" class="award-list" aria-label="Submission readiness checks"></ul>
134
  <div class="budget-header">
135
  <h3>Model Budget</h3>
136
  <span id="budgetStatus">Checking</span>