stage-22: cockpit reflects the live adapter + all six feeds

The Worker Mesh Classifier badge was hardcoded "psyc-v4" — it kept
claiming the old model even after the v5 swap, making it look like
nothing had changed. Now derived dynamically from the inference
server's reported adapter via inference.adapter_name(); shows the real
version (psyc-v5) or "rules" when the server is down, so it can't go
stale on the next swap.

Also refreshed the cases page intro/help text to name all six feeds
(URLhaus, CISA KEV, Feodo, ThreatFox, MalwareBazaar, OTX) instead of
the original three.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
m17hr1l
2026-05-22 23:33:19 +02:00
parent ee387abcd4
commit f88db2fdf7
4 changed files with 18 additions and 4 deletions

View File

@@ -77,7 +77,12 @@ def case_journey(request: Request, case_id: str) -> HTMLResponse:
if isinstance(result, Err):
raise HTTPException(status_code=404, detail=result.reason)
beats = journey_view.build_journey(result.value)
return TEMPLATES.TemplateResponse(request, "journey.html", {"case": result.value, "beats": beats})
model_label = inference.adapter_name() or "rules"
return TEMPLATES.TemplateResponse(
request,
"journey.html",
{"case": result.value, "beats": beats, "model_label": model_label},
)
@app.get("/ledger", response_class=HTMLResponse)

View File

@@ -34,6 +34,15 @@ def server_adapter(timeout: float = 2.0) -> Optional[str]:
return None
def adapter_name(timeout: float = 2.0) -> Optional[str]:
"""Short name of the live adapter, e.g. 'psyc-v5' from '/data/adapters/psyc-v5/final'."""
path = server_adapter(timeout=timeout)
if not path:
return None
parts = [p for p in path.split("/") if p and p != "final"]
return parts[-1] if parts else None
def model_severity(case: Case, timeout: float = 15.0) -> Optional[str]:
"""Ask the live model to classify case severity. None if the server is down."""
payload = {

View File

@@ -6,12 +6,12 @@
<h1>Case Queue</h1>
<span class="count">{{ total }} case{{ '' if total == 1 else 's' }}</span>
</div>
<p class="page-intro">Every threat case psyc is tracking — ingested from URLhaus, CISA KEV, and Feodo Tracker, then classified by severity and TLP. The live queue of what the platform currently knows about; click any case to follow it through the pipeline.</p>
<p class="page-intro">Every threat case psyc is tracking — ingested from URLhaus, CISA KEV, Feodo Tracker, ThreatFox, MalwareBazaar, and AlienVault OTX, then classified by severity and TLP. The live queue of what the platform currently knows about; click any case to follow it through the pipeline.</p>
<details class="page-help">
<summary>how to use this view</summary>
<div class="help-body">
<p><b>How to use.</b> Scan the severity and TLP badges to triage; click any case ID to open its full record and Worker Mesh journey. Run <code>psyc fetch-all</code> to pull fresh cases from the feeds.</p>
<p><b>What you're seeing.</b> Each row is one normalized Case object — ingested by Scoutline from URLhaus, CISA KEV, or Feodo Tracker, then rated by Classifyline.</p>
<p><b>What you're seeing.</b> Each row is one normalized Case object — ingested by Scoutline from any of six feeds (URLhaus, CISA KEV, Feodo Tracker, ThreatFox, MalwareBazaar, OTX), then rated by Classifyline.</p>
<p><b>Why it matters.</b> A defender needs one place that answers "what is happening right now" before deciding what to act on — this queue is that place.</p>
</div>
</details>

View File

@@ -74,7 +74,7 @@
{% set rule_sev = case.classification.severity.value if case.classification.severity else '' %}
{% set agrees = b.model_answer == rule_sev %}
<p class="bot-model {{ 'model-agree' if agrees else 'model-differ' }}">
<span class="model-chip">psyc-v4 · live model</span>
<span class="model-chip">{{ model_label }} · live model</span>
severity: <strong>{{ b.model_answer | upper }}</strong>
{% if agrees %}<span class="model-verdict">✓ agrees with the rule</span>
{% else %}<span class="model-verdict">✗ differs — rule said {{ rule_sev | upper }}</span>{% endif %}