From b630c31077c769840bedb717c92189b77a6aacf9 Mon Sep 17 00:00:00 2001 From: hyungi Date: Sat, 13 Jun 2026 13:54:39 +0900 Subject: [PATCH] feat(board): expose summarize_by_machine for offload visibility (A-1) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 요약 풀의 머신별 완료 실적(맥미니 vs 맥북)을 /api/queue/overview 응답에 summarize_by_machine 로 노출. rows_to_summarize_split 이 이미 계산하던 값의 additive 투영 — 신규 수집 SQL/마이그 0. 통합 보드 레인의 오프로드 가시화 (맥북이 요약 86% 처리) 재료. + FE 타입 동기 + store 신선도 timestamp(B-4). Co-Authored-By: Claude Fable 5 --- app/api/queue_overview.py | 15 ++++++++++++++ app/services/queue_overview.py | 11 ++++++++++ frontend/src/lib/stores/queueOverview.ts | 9 +++++++- frontend/src/lib/types/queue.ts | 9 +++++++- tests/test_queue_overview.py | 26 ++++++++++++++++++++++++ 5 files changed, 68 insertions(+), 2 deletions(-) diff --git a/app/api/queue_overview.py b/app/api/queue_overview.py index c98e130..383cc71 100644 --- a/app/api/queue_overview.py +++ b/app/api/queue_overview.py @@ -59,6 +59,20 @@ class SummarizeEta(BaseModel): eta_minutes: int | None +class MachineDone(BaseModel): + """머신 1대의 summarize 완료 실적 (분담 표시용).""" + done_1h: int + done_today: int + + +class SummarizeByMachine(BaseModel): + """summarize 풀의 머신별 완료 실적 분담 — 보드 레인의 '맥미니 vs 맥북' + 오프로드 가시화용. rows_to_summarize_split 이 이미 계산하던 값의 노출 + (ds-board-merged A-1, 신규 수집 SQL 0).""" + macmini: MachineDone + macbook: MachineDone + + class TrendBucket(BaseModel): """summarize 24h 추이 버킷 — hour 는 KST "HH:00" 라벨.""" hour: str @@ -93,6 +107,7 @@ class QueueOverviewResponse(BaseModel): machines: list[MachineCard] stages: list[StageRow] summarize_eta: SummarizeEta + summarize_by_machine: SummarizeByMachine trend_24h: list[TrendBucket] totals: Totals diff --git a/app/services/queue_overview.py b/app/services/queue_overview.py index c796600..682660f 100644 --- a/app/services/queue_overview.py +++ b/app/services/queue_overview.py @@ -213,6 +213,16 @@ def build_summarize_eta(stage_stats: dict[str, dict]) -> dict: } +def build_summarize_by_machine(summarize_split: dict[str, dict]) -> dict: + """summarize 머신별 완료 실적 분담 (macmini vs macbook) — 보드 레인의 + 오프로드 가시화용. rows_to_summarize_split 이 이미 만든 값을 응답 형태로 + 투영(done_1h/done_today 만, done_15m 은 내부 state 판정 전용이라 제외).""" + def m(key: str) -> dict: + s = summarize_split.get(key, {}) + return {"done_1h": int(s.get("done_1h", 0)), "done_today": int(s.get("done_today", 0))} + return {"macmini": m("macmini"), "macbook": m("macbook")} + + def build_trend( inflow_buckets: dict[str, int], done_buckets: dict[str, int], @@ -292,6 +302,7 @@ def compose_overview( ), "stages": build_stages(stage_stats), "summarize_eta": build_summarize_eta(stage_stats), + "summarize_by_machine": build_summarize_by_machine(summarize_split), "trend_24h": build_trend(inflow_buckets, done_buckets, now_kst), "totals": build_totals(stage_stats), } diff --git a/frontend/src/lib/stores/queueOverview.ts b/frontend/src/lib/stores/queueOverview.ts index d46d61f..6d2faa5 100644 --- a/frontend/src/lib/stores/queueOverview.ts +++ b/frontend/src/lib/stores/queueOverview.ts @@ -17,6 +17,11 @@ let pollHandle: ReturnType | null = null; let subscriberCount = 0; let inFlight: Promise | null = null; +// 마지막 성공 갱신 시각(epoch ms) — 보드 신선도 '갱신 N초 전' + stale 경고용 +// (ds-board-merged B-4). 실패(null 수렴) 시엔 갱신 안 함 → age 가 늘어 stale 로 드러남. +const updatedAt = writable(null); +export const queueUpdatedAt = { subscribe: updatedAt.subscribe }; + const internal = writable(null, (_set) => { subscriberCount += 1; if (subscriberCount === 1 && browser) { @@ -54,7 +59,9 @@ export async function refreshQueueOverview(): Promise { if (inFlight) return inFlight; inFlight = (async () => { try { - internal.set(await fetchOverview()); + const ov = await fetchOverview(); + internal.set(ov); + if (ov) updatedAt.set(Date.now()); // 성공 시에만 신선도 갱신 (실패=stale 유지) } finally { inFlight = null; } diff --git a/frontend/src/lib/types/queue.ts b/frontend/src/lib/types/queue.ts index 740251d..0915bbf 100644 --- a/frontend/src/lib/types/queue.ts +++ b/frontend/src/lib/types/queue.ts @@ -43,13 +43,19 @@ export interface SummarizeEta { eta_minutes: number | null; } -/** 시간당 유입 vs 소화 (이번 트랙 미렌더 — 후속 추세 위젯 슬롯) */ +/** 시간당 유입 vs 소화 (요약 24h 추이) */ export interface TrendPoint { hour: string; inflow: number; done: number; } +/** summarize 머신별 완료 실적 분담 (오프로드 가시화 — ds-board-merged A-1) */ +export interface SummarizeByMachine { + macmini: { done_1h: number; done_today: number }; + macbook: { done_1h: number; done_today: number }; +} + export interface QueueTotals { pending: number; processing: number; @@ -72,6 +78,7 @@ export interface QueueStageRow { export interface QueueOverview { machines: MachineOverview[]; summarize_eta: SummarizeEta; + summarize_by_machine: SummarizeByMachine; trend_24h: TrendPoint[]; stages: QueueStageRow[]; totals: QueueTotals; diff --git a/tests/test_queue_overview.py b/tests/test_queue_overview.py index c24664e..a1f230a 100644 --- a/tests/test_queue_overview.py +++ b/tests/test_queue_overview.py @@ -103,6 +103,32 @@ def test_summarize_pool_split_attribution(): assert macbook["pending"] == 0 # 풀 pending 은 macmini 만 +def test_summarize_by_machine_projection(): + """build_summarize_by_machine = split 의 done_1h/done_today 를 머신별로 투영 + (done_15m 은 제외 — 내부 state 판정 전용).""" + from services.queue_overview import build_summarize_by_machine + split = _split( + macbook={"done_1h": 226, "done_today": 312, "done_15m": 60}, + macmini={"done_1h": 37, "done_today": 94, "done_15m": 9}, + ) + sbm = build_summarize_by_machine(split) + assert sbm == { + "macmini": {"done_1h": 37, "done_today": 94}, + "macbook": {"done_1h": 226, "done_today": 312}, + } + assert "done_15m" not in sbm["macbook"] + + +def test_compose_overview_includes_summarize_by_machine(): + """compose_overview 응답 계약에 summarize_by_machine 포함 (FE 레인 분담 재료).""" + now_kst = datetime(2026, 6, 13, 13, 0, tzinfo=KST) + stats = {"summarize": _stage(pending=1317, done_1h=264)} + split = _split(macbook={"done_1h": 226, "done_today": 312}, macmini={"done_1h": 37, "done_today": 94}) + ov = compose_overview(stats, split, {}, {}, [], deep_enabled=True, now_kst=now_kst) + assert ov["summarize_by_machine"]["macbook"]["done_1h"] == 226 + assert ov["summarize_by_machine"]["macmini"]["done_today"] == 94 + + def test_deep_disabled_deep_summary_counts_to_macmini(): stats = {"deep_summary": _stage(pending=2, processing=1, done_1h=3, done_today=4)} machines = build_machines(stats, _split(), [], deep_enabled=False)