468804494d
plan ds-processing-ui-6an (시안 choice 채택: 안2 1차 + 안5/6 지원): - GET /api/queue/overview — 머신(GPU/맥미니/맥북) 귀속 라이브 집계 5쿼리, 마이그레이션 0. summarize 풀 완료 실적은 documents.ai_model_version 조인으로 맥북/맥미니 분리, 보류(deferred_until)=맥북 카드 귀속, state=active/deferred/idle. raw 모델명 비노출 - 홈: 처리 머신 보드(3열 카드 + 지금 처리 중 제목) + ETA 라인(유입 우세 시 null 명시), 기존 stage 테이블은 details 접힘으로 강등 (구조 개편) - 전 페이지: 상태 스트립(처리중·대기·실패·맥북 칩) + 우측 드로어(QueueDrawer, dialog a11y) — 공유 60s 폴링 store, 경량 fetch(401 강제 logout 부수효과 회피) - tests: 판정부 30건 (귀속/풀 분리/state 9케이스/ETA 경계/trend 버킷/계약 shape) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
64 lines
2.2 KiB
TypeScript
64 lines
2.2 KiB
TypeScript
// 처리 큐 overview store — GET /api/queue/overview 를 60초 주기로 폴링.
|
|
// system.ts 의 dashboardSummary 와 같은 구독 기반 패턴 (첫 subscribe 시 시작).
|
|
//
|
|
// 의도적으로 api() 헬퍼를 쓰지 않는다 — 폴링 경로의 401 이 refresh 실패 →
|
|
// window.location='/login' 강제 logout 부수효과를 일으키면 안 됨 (eid 리뷰
|
|
// finding 재발 방지). 백엔드 미배포(404)/401/네트워크 실패 전부 silent 하게
|
|
// null 로 수렴하고, 소비자(스트립/보드/드로어)는 null 이면 스스로 숨는다.
|
|
|
|
import { writable } from 'svelte/store';
|
|
import { browser } from '$app/environment';
|
|
import { getAccessToken } from '$lib/api';
|
|
import type { QueueOverview } from '$lib/types/queue';
|
|
|
|
const POLL_INTERVAL_MS = 60_000;
|
|
|
|
let pollHandle: ReturnType<typeof setInterval> | null = null;
|
|
let subscriberCount = 0;
|
|
let inFlight: Promise<void> | null = null;
|
|
|
|
const internal = writable<QueueOverview | null>(null, (_set) => {
|
|
subscriberCount += 1;
|
|
if (subscriberCount === 1 && browser) {
|
|
void refreshQueueOverview();
|
|
pollHandle = setInterval(() => void refreshQueueOverview(), POLL_INTERVAL_MS);
|
|
}
|
|
return () => {
|
|
subscriberCount -= 1;
|
|
if (subscriberCount === 0 && pollHandle) {
|
|
clearInterval(pollHandle);
|
|
pollHandle = null;
|
|
}
|
|
};
|
|
});
|
|
|
|
export const queueOverview = { subscribe: internal.subscribe };
|
|
|
|
/** 경량 fetch — 실패는 전부 null (silent 비차단, 강제 logout 경로 없음) */
|
|
async function fetchOverview(): Promise<QueueOverview | null> {
|
|
try {
|
|
const headers: Record<string, string> = {};
|
|
const token = getAccessToken();
|
|
if (token) headers['Authorization'] = `Bearer ${token}`;
|
|
const res = await fetch('/api/queue/overview', { headers, credentials: 'include' });
|
|
if (!res.ok) return null;
|
|
return (await res.json()) as QueueOverview;
|
|
} catch {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
/** 수동/추가 폴링용 — 홈은 자체 30s interval 로 이 함수를 호출 (동시 fetch 합치기) */
|
|
export async function refreshQueueOverview(): Promise<void> {
|
|
if (!browser) return;
|
|
if (inFlight) return inFlight;
|
|
inFlight = (async () => {
|
|
try {
|
|
internal.set(await fetchOverview());
|
|
} finally {
|
|
inFlight = null;
|
|
}
|
|
})();
|
|
return inFlight;
|
|
}
|