feat: dashboard UI + key API + realtime decode

Full HTML dashboard (dark theme, vanilla JS, no external deps) served at / and /dashboard with five tabs: system status (admin), file upload-to-transcribe with progress/result/downloads, job history with cancel/result modal, realtime mic demo over the existing WebSocket, and API key create/list.

Backend: POST/GET /v1/keys (admin; raw key returned once, digest-only storage); KeyStore.list_keys(); EngineOwner.emit_hypothesis is now a real implementation (PCM16 chunk -> WAV -> realtime-lane decode with the single GPU lock) instead of a stub.

Notebook: tunnel cell links /dashboard and smoke-checks the HTML.

+ 6 tests (dashboard public HTML, key create/list/scope, WAV header, emit_hypothesis cleanup). 153 tests pass, ruff clean, JS syntax verified with node --check.
This commit is contained in:
2026-08-12 21:41:42 +09:00
parent 440e947d47
commit 3dfa660503
11 changed files with 1111 additions and 13 deletions
+38
View File
@@ -44,6 +44,44 @@ class TestHealth:
assert body["model_ready"] is False # 모델 캐시 없음 → 프로비저닝 보류
class TestDashboard:
def test_dashboard_html_public(self, client: TestClient):
"""대시보드 HTML은 공개 — 인증은 클라이언트에서 API 키 입력."""
for path in ("/", "/dashboard"):
r = client.get(path)
assert r.status_code == 200
assert "text/html" in r.headers["content-type"]
assert "luke_scribe" in r.text
assert "실시간" in r.text # 전체 기능 포함
class TestKeys:
def test_create_and_list_key(self, client: TestClient):
headers = {"X-API-Key": "key-admin"}
r = client.post("/v1/keys", json={"scopes": ["transcribe"]}, headers=headers)
assert r.status_code == 201, r.text
body = r.json()
assert body["key"].startswith("luke-")
assert body["key_id"].startswith("k-")
# raw 키는 1회만 노출 — 목록에는 다이제스트 ID만
r2 = client.get("/v1/keys", headers=headers)
keys = r2.json()["keys"]
assert body["key_id"] in [k["id"] for k in keys]
assert all("key" not in k for k in keys)
def test_admin_scope_required(self, client: TestClient):
headers = {"X-API-Key": "key-transcribe"}
assert (
client.post("/v1/keys", json={"scopes": ["transcribe"]}, headers=headers).status_code
== 403
)
assert client.get("/v1/keys", headers=headers).status_code == 403
def test_no_auth_401(self, client: TestClient):
assert client.get("/v1/keys").status_code == 401
assert client.post("/v1/keys", json={"scopes": []}).status_code == 401
class TestAutoWorker:
def test_auto_worker_off_by_default(self, client: TestClient):
"""기본(False)에서는 워커 스레드를 띄우지 않는다 (테스트/프로덕션 안전)."""