# ruff: noqa: E501
from html import escape
from typing import Annotated, Any
from fastapi import APIRouter, Depends
from fastapi.responses import HTMLResponse
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
from core_hub.db import session_scope
from core_hub.models import (
ApiConsumer,
Hub,
HubCapabilityManifest,
InteractionEvent,
MigrationRun,
Widget,
)
from core_hub.security import require_api_token
router = APIRouter(tags=["operator-console"])
TokenDependency = Annotated[str, Depends(require_api_token)]
SessionDependency = Annotated[AsyncSession, Depends(session_scope)]
CONSOLE_CSS = """
:root {
--ink: #0a0a0a;
--ink-2: #1f1f1f;
--ink-3: #5c5c5c;
--ink-4: #8a8a8a;
--ink-5: #b5b5b3;
--line: #e5e5e2;
--line-strong: #c9c9c5;
--line-soft: #f0f0ec;
--paper: #ffffff;
--paper-2: #fafaf7;
--paper-3: #f4f4ef;
--hi: #ffe14a;
--ff-sans: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
--ff-mono: ui-monospace, "SF Mono", Menlo, Consolas, monospace;
--sp-2: 8px;
--sp-3: 12px;
--sp-4: 16px;
--sp-5: 24px;
--sp-6: 32px;
--r-2: 4px;
}
* { box-sizing: border-box; }
html { font-family: var(--ff-sans); color: var(--ink); background: var(--paper); }
body { margin: 0; }
a { color: inherit; text-decoration: underline; text-decoration-color: var(--line-strong); text-underline-offset: 3px; }
code { font-family: var(--ff-mono); font-size: 0.92em; background: var(--paper-3); padding: 1px 5px; border-radius: 2px; }
.wn-topnav { height: 56px; border-bottom: 1px solid var(--line); display: flex; align-items: center; justify-content: space-between; padding: 0 var(--sp-5); background: var(--paper); position: sticky; top: 0; z-index: 2; }
.wn-topnav__brand { display: flex; align-items: baseline; gap: var(--sp-2); font-weight: 600; }
.wn-topnav__brand-slug { font: 500 12px/1 var(--ff-mono); color: var(--ink-3); letter-spacing: 0.04em; }
.wn-topnav__right { font: 500 11px/1 var(--ff-mono); color: var(--ink-3); text-transform: uppercase; letter-spacing: 0.08em; }
.console-shell { min-height: calc(100vh - 56px); display: grid; grid-template-columns: 248px minmax(0, 1fr); }
.wn-sidebar { border-right: 1px solid var(--line); background: var(--paper-2); padding: var(--sp-5); position: sticky; top: 56px; height: calc(100vh - 56px); }
.wn-sidebar__group { display: grid; gap: var(--sp-2); margin-bottom: var(--sp-5); }
.wn-sidebar__group-label, .wn-eyebrow { font: 500 11px/1.2 var(--ff-mono); letter-spacing: 0.08em; text-transform: uppercase; color: var(--ink-3); }
.wn-sidebar__item { display: flex; justify-content: space-between; gap: var(--sp-3); padding: 8px 0; font: 500 13px/1.35 var(--ff-sans); text-decoration: none; border-bottom: 1px solid transparent; }
.wn-sidebar__item:hover { border-bottom-color: var(--line-strong); }
.wn-sidebar__count { font: 500 11px/1 var(--ff-mono); color: var(--ink-4); }
.console-main { padding: var(--sp-6); min-width: 0; }
.wn-page-header { display: grid; gap: var(--sp-3); margin-bottom: var(--sp-6); }
.wn-page-header__row { display: flex; justify-content: space-between; gap: var(--sp-5); align-items: end; }
.wn-page-header__title { font: 600 36px/1.05 var(--ff-sans); margin: 0; letter-spacing: 0; }
.wn-page-header__lede { margin: 0; max-width: 760px; color: var(--ink-3); line-height: 1.55; }
.wn-btn { display: inline-flex; align-items: center; border: 1px solid var(--ink); border-radius: var(--r-2); padding: 9px 14px; font: 500 13px/1.2 var(--ff-sans); text-decoration: none; background: var(--ink); color: var(--paper); }
.kpi-grid { display: grid; grid-template-columns: repeat(6, minmax(120px, 1fr)); gap: var(--sp-3); margin-bottom: var(--sp-6); }
.wn-card { background: var(--paper); border: 1px solid var(--line); border-radius: var(--r-2); padding: var(--sp-4); }
.stat-card { min-height: 96px; display: grid; align-content: space-between; gap: var(--sp-3); }
.stat-card strong { font: 600 30px/1 var(--ff-sans); }
.stat-card span { font: 500 11px/1.2 var(--ff-mono); letter-spacing: 0.08em; text-transform: uppercase; color: var(--ink-3); }
.console-section { margin-bottom: var(--sp-6); }
.section-head { display: flex; justify-content: space-between; gap: var(--sp-5); align-items: baseline; margin-bottom: var(--sp-3); }
.section-head h2 { font: 600 20px/1.2 var(--ff-sans); margin: 0; letter-spacing: 0; }
.section-head p { margin: 0; color: var(--ink-3); font-size: 13px; }
.table-wrap { border: 1px solid var(--line); border-radius: var(--r-2); overflow-x: auto; background: var(--paper); }
table { width: 100%; border-collapse: collapse; font-size: 13px; min-width: 680px; }
th, td { text-align: left; padding: 11px 14px; border-bottom: 1px solid var(--line); vertical-align: top; }
th { font: 500 11px/1.2 var(--ff-mono); letter-spacing: 0.06em; text-transform: uppercase; color: var(--ink-3); background: var(--paper-2); }
tr:last-child td { border-bottom: 0; }
.wn-tag { display: inline-block; border: 1px solid var(--line); border-radius: 999px; padding: 5px 9px; font: 500 10px/1 var(--ff-mono); letter-spacing: 0.08em; text-transform: uppercase; white-space: nowrap; color: var(--ink-3); }
.wn-tag--active, .wn-tag--done, .wn-tag--imported { background: var(--ink); border-color: var(--ink); color: var(--paper); }
.wn-tag--wait, .wn-tag--blocked, .wn-tag--degraded { background: var(--paper-3); color: var(--ink-2); }
.wn-tag--warn, .wn-tag--draft, .wn-tag--dry-run { background: var(--hi); border-color: transparent; color: #1a1500; }
.wn-empty-state { border: 1px dashed var(--line-strong); border-radius: var(--r-2); padding: var(--sp-5); color: var(--ink-3); font-size: 14px; }
.gate-list { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: var(--sp-3); }
.gate { border: 1px solid var(--line); border-radius: var(--r-2); padding: var(--sp-4); display: grid; gap: var(--sp-2); }
.gate b { font-size: 14px; }
.gate span { color: var(--ink-3); font-size: 13px; line-height: 1.45; }
@media (max-width: 980px) {
.console-shell { grid-template-columns: 1fr; }
.wn-sidebar { position: static; height: auto; border-right: 0; border-bottom: 1px solid var(--line); }
.kpi-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
.gate-list { grid-template-columns: 1fr; }
}
@media (max-width: 640px) {
.wn-topnav { padding: 0 var(--sp-4); }
.console-main { padding: var(--sp-4); }
.wn-page-header__row { display: grid; }
.wn-page-header__title { font-size: 28px; }
.kpi-grid { grid-template-columns: 1fr; }
}
"""
def text(value: Any) -> str:
if value is None or value == "":
return "-"
return escape(str(value))
def short_id(value: str | None) -> str:
if not value:
return "-"
return escape(value[:8])
def tag(value: str | None) -> str:
status = str(value or "unknown")
css_status = escape(status.lower().replace("_", "-").replace(" ", "-"))
return f'{escape(status)}'
def table_or_empty(headers: list[str], rows: list[list[str]], empty: str) -> str:
if not rows:
return f'
{escape(empty)}
'
head = "".join(f"{escape(header)} | " for header in headers)
body = "".join("" + "".join(f"| {cell} | " for cell in row) + "
" for row in rows)
return f''
async def fetch_rows(
session: AsyncSession, model: Any, order_by: Any | None = None, limit: int = 12
) -> list[Any]:
statement = select(model)
if order_by is not None:
statement = statement.order_by(order_by)
statement = statement.limit(limit)
result = await session.execute(statement)
return list(result.scalars())
@router.get("/console", response_class=HTMLResponse)
async def operator_console(_: TokenDependency, session: SessionDependency) -> HTMLResponse:
hubs = await fetch_rows(session, Hub, Hub.slug)
manifests = await fetch_rows(
session, HubCapabilityManifest, HubCapabilityManifest.created_at.desc()
)
widgets = await fetch_rows(session, Widget, Widget.name)
events = await fetch_rows(
session, InteractionEvent, InteractionEvent.created_at.desc(), limit=8
)
consumers = await fetch_rows(session, ApiConsumer, ApiConsumer.name)
migrations = await fetch_rows(session, MigrationRun, MigrationRun.created_at.desc(), limit=8)
active_manifests = sum(1 for item in manifests if item.status == "active")
stats = [
("Hubs", len(hubs)),
("Active manifests", active_manifests),
("Widgets", len(widgets)),
("Events", len(events)),
("Consumers", len(consumers)),
("Migration runs", len(migrations)),
]
registry_rows = [
[
f"{text(hub.slug)}",
text(hub.name),
text(hub.domain),
text(hub.hub_kind),
tag(hub.status),
]
for hub in hubs
]
migration_rows = [
[
f"{short_id(run.id)}",
text(run.source),
text(run.schema_version),
f"{short_id(run.bundle_sha256)}",
tag("dry-run" if run.dry_run else run.status),
]
for run in migrations
]
consumer_rows = [
[
f"{text(consumer.slug)}",
text(consumer.name),
text(consumer.key_prefix),
text(consumer.rate_limit_per_minute),
tag(consumer.status),
]
for consumer in consumers
]
event_rows = [
[
f"{short_id(event.id)}",
text(event.event_type),
f"{short_id(event.widget_id)}",
text(event.view_context),
]
for event in events
]
stat_cards = "".join(
f'{escape(label)}{value}'
for label, value in stats
)
html = f"""
Core Hub operator console
Core Hub/ operator
protected console
Readiness gates
Current blocker map
{tag("blocked")} ops-hub smokeLocal smoke passed. Closure waits on deployed runtime and approved credential routing.
{tag("blocked")} staging importImporter is ready. Closure waits on approved Inter-Hub export bundle and staging DB.
{tag("blocked")} activity-core smokeWaits on deployed Core Hub runtime or explicit transition fallback decision.
{tag("active")} console prototypeProtected read-only console surface is active.
Hub registry
Hubs visible to the protected runtime
{table_or_empty(["Slug", "Name", "Domain", "Kind", "Status"], registry_rows, "No hubs registered yet.")}
Migration runs
Import evidence and bundle identity
{table_or_empty(["Run", "Source", "Schema", "Bundle", "Status"], migration_rows, "No migration runs recorded yet.")}
Access
Non-secret API consumer metadata
{table_or_empty(["Slug", "Name", "Key prefix", "Rate/min", "Status"], consumer_rows, "No API consumers registered yet.")}
Recent events
Latest interaction evidence
{table_or_empty(["Event", "Type", "Widget", "Context"], event_rows, "No interaction events recorded yet.")}
"""
return HTMLResponse(html)