From 96d99f63e0674019e326213d333a7f784f9013cf Mon Sep 17 00:00:00 2001 From: Eyejoker Date: Tue, 28 Apr 2026 16:56:37 +0900 Subject: [PATCH] Extract dashboard SettingsPanel component (#68) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * review: harden readonly git checks and lint verification * test: satisfy readonly reviewer sandbox typing * test: fix readonly reviewer sandbox assertions * test: remove impossible readonly sandbox guards * test: relax readonly sandbox assertions * test: cast readonly sandbox expectation shape * test: cast readonly sandbox expectation through unknown * Phase 0 — STEP_DONE/TASK_DONE split + owner-follow-up integration smoke * Add STEP_DONE guards, verdict storage, and stale delivery suppression * style: format paired stepdone telemetry files * Route STEP_DONE through reviewer * Add structured Discord attachments * style: format structured attachment test * Fix room thread bot output parity * Remove duplicate work item attachment migration from owner branch * Remove legacy dashboard rooms renderer * Extract dashboard parsed body renderer * Extract dashboard redaction helpers * Extract dashboard RoomCardV2 component * Include RoomCardV2 test in vitest suite * Extract dashboard RoomBoardV2 component * Extract dashboard EmptyState component * Extract dashboard InboxPanel component * Split dashboard InboxPanel card renderer * Extract dashboard TaskPanel component * Extract dashboard UsagePanel component * Fix dashboard room thread chunk rendering * Extract dashboard ServicePanel component * Render dashboard live progress markdown * Extract dashboard SettingsPanel component --- apps/dashboard/src/App.tsx | 608 +------------------- apps/dashboard/src/SettingsPanel.test.ts | 42 ++ apps/dashboard/src/SettingsPanel.tsx | 696 +++++++++++++++++++++++ 3 files changed, 739 insertions(+), 607 deletions(-) create mode 100644 apps/dashboard/src/SettingsPanel.test.ts create mode 100644 apps/dashboard/src/SettingsPanel.tsx diff --git a/apps/dashboard/src/App.tsx b/apps/dashboard/src/App.tsx index 84a2903..5e250dd 100644 --- a/apps/dashboard/src/App.tsx +++ b/apps/dashboard/src/App.tsx @@ -1,35 +1,20 @@ import { useEffect, useState } from 'react'; import { - type ClaudeAccountSummary, - type CodexAccountSummary, type CreateScheduledTaskInput, type DashboardInboxAction, type DashboardRoomActivity, type DashboardTaskAction, type DashboardOverview, type DashboardTask, - type FastModeSnapshot, - type ModelConfigSnapshot, - type ModelRoleConfig, type UpdateScheduledTaskInput, type StatusSnapshot, - addClaudeAccount, createScheduledTask, - deleteAccount, - fetchAccounts, fetchDashboardData, - fetchFastMode, - fetchModelConfig, - refreshAllCodexAccounts as refreshAllCodexAccountsApi, - refreshCodexAccount as refreshCodexAccountApi, runInboxAction, runServiceAction, runScheduledTaskAction, sendRoomMessage, - setCurrentCodexAccount as setCurrentCodexAccountApi, - updateFastMode, - updateModels, updateScheduledTask, } from './api'; import { @@ -53,9 +38,9 @@ import { formatDate, statusLabel } from './dashboardHelpers'; import { InboxPanel, type InboxActionKey, type InboxItem } from './InboxPanel'; import { RoomBoardV2 } from './RoomBoardV2'; import { ServicePanel, type ServiceActionKey } from './ServicePanel'; +import { SettingsPanel } from './SettingsPanel'; import { TaskPanel, type RoomOption, type TaskActionKey } from './TaskPanel'; import { UsagePanel } from './UsagePanel'; -import { ParsedBody } from './ParsedBody'; import './styles.css'; interface DashboardState { @@ -276,597 +261,6 @@ function LanguageSelector({ ); } -function SettingsPanel({ - locale, - nickname, - onLocaleChange, - onNicknameChange, - onRestartStack, - t, -}: { - locale: Locale; - nickname: string; - onLocaleChange: (locale: Locale) => void; - onNicknameChange: (next: string) => void; - onRestartStack: () => void; - t: Messages; -}) { - return ( -
-
-

일반

- - -
- - - - - - -
- ); -} - -function ModelSettings({ onRestartStack }: { onRestartStack: () => void }) { - const [config, setConfig] = useState(null); - const [draft, setDraft] = useState(null); - const [busy, setBusy] = useState(false); - const [error, setError] = useState(null); - const [savedAt, setSavedAt] = useState(null); - - useEffect(() => { - let cancelled = false; - setBusy(true); - fetchModelConfig() - .then((c) => { - if (cancelled) return; - setConfig(c); - setDraft(c); - setError(null); - }) - .catch((err) => { - if (cancelled) return; - setError(err instanceof Error ? err.message : String(err)); - }) - .finally(() => { - if (!cancelled) setBusy(false); - }); - return () => { - cancelled = true; - }; - }, []); - - async function save() { - if (!draft || !config) return; - setBusy(true); - setError(null); - try { - const next = await updateModels(draft); - setConfig(next); - setDraft(next); - setSavedAt(Date.now()); - } catch (err) { - setError(err instanceof Error ? err.message : String(err)); - } finally { - setBusy(false); - } - } - - function setRole( - role: keyof ModelConfigSnapshot, - patch: Partial, - ) { - setDraft((prev) => - prev - ? { - ...prev, - [role]: { ...prev[role], ...patch }, - } - : prev, - ); - } - - const dirty = - draft !== null && - config !== null && - JSON.stringify(draft) !== JSON.stringify(config); - - return ( -
-

모델

- {error ?

{error}

: null} - {!draft ? ( -

- {busy ? '불러오는 중…' : '모델 정보 없음'} -

- ) : ( - <> - {(['owner', 'reviewer', 'arbiter'] as const).map((role) => ( -
- {role} - setRole(role, { model: e.target.value })} - placeholder="claude / codex / claude-opus-4-7 …" - type="text" - value={draft[role].model} - /> - setRole(role, { effort: e.target.value })} - placeholder="effort" - type="text" - value={draft[role].effort} - /> -
- ))} -
- - {savedAt && !dirty ? ( - - 저장됨. 적용하려면 스택 재시작 필요. - - ) : null} - -
- - )} -
- ); -} - -function FastModeSettings() { - const [state, setState] = useState(null); - const [busy, setBusy] = useState(false); - const [error, setError] = useState(null); - - useEffect(() => { - let cancelled = false; - fetchFastMode() - .then((s) => { - if (cancelled) return; - setState(s); - setError(null); - }) - .catch((err) => { - if (cancelled) return; - setError(err instanceof Error ? err.message : String(err)); - }); - return () => { - cancelled = true; - }; - }, []); - - async function toggle(provider: keyof FastModeSnapshot) { - if (!state) return; - const next = !state[provider]; - const optimistic = { ...state, [provider]: next }; - setState(optimistic); - setBusy(true); - setError(null); - try { - const fresh = await updateFastMode({ [provider]: next }); - setState(fresh); - } catch (err) { - setState(state); - setError(err instanceof Error ? err.message : String(err)); - } finally { - setBusy(false); - } - } - - return ( -
-

패스트 모드

- {error ?

{error}

: null} - {!state ? ( -

불러오는 중…

- ) : ( - <> - - - - )} -
- ); -} - -function formatExpiry( - iso: string | null, -): { label: string; cls: string } | null { - if (!iso) return null; - const dt = new Date(iso); - if (Number.isNaN(dt.getTime())) return null; - const days = (dt.getTime() - Date.now()) / 86400000; - const dateStr = dt.toLocaleDateString('ko-KR', { - year: '2-digit', - month: '2-digit', - day: '2-digit', - }); - if (days < 0) { - const ago = Math.ceil(-days); - return { - label: `결제 만료 ${dateStr} (${ago}일 전)`, - cls: 'is-expired', - }; - } - if (days < 7) { - return { - label: `결제 ${dateStr}까지 (${Math.floor(days)}일)`, - cls: 'is-soon', - }; - } - return { - label: `결제 ${dateStr}까지 (${Math.floor(days)}일)`, - cls: 'is-active', - }; -} - -function AccountSettings({ onRestartStack }: { onRestartStack: () => void }) { - const [data, setData] = useState<{ - claude: ClaudeAccountSummary[]; - codex: CodexAccountSummary[]; - codexCurrentIndex?: number; - } | null>(null); - const [busy, setBusy] = useState(false); - const [perRowBusy, setPerRowBusy] = useState(null); - const [error, setError] = useState(null); - const [tokenInput, setTokenInput] = useState(''); - - async function refresh() { - setBusy(true); - setError(null); - try { - const fresh = await fetchAccounts(); - setData(fresh); - } catch (err) { - setError(err instanceof Error ? err.message : String(err)); - } finally { - setBusy(false); - } - } - - useEffect(() => { - void refresh(); - }, []); - - async function handleDelete(provider: 'claude' | 'codex', index: number) { - if ( - !window.confirm( - `${provider} 계정 #${index} 디렉터리를 삭제합니다. 되돌릴 수 없습니다. 계속할까요?`, - ) - ) { - return; - } - setBusy(true); - setError(null); - try { - await deleteAccount(provider, index); - await refresh(); - } catch (err) { - setError(err instanceof Error ? err.message : String(err)); - } finally { - setBusy(false); - } - } - - async function handleAdd() { - const token = tokenInput.trim(); - if (!token) return; - setBusy(true); - setError(null); - try { - await addClaudeAccount(token); - setTokenInput(''); - await refresh(); - } catch (err) { - setError(err instanceof Error ? err.message : String(err)); - } finally { - setBusy(false); - } - } - - async function handleCodexRefresh(index: number) { - setPerRowBusy(`refresh:${index}`); - setError(null); - try { - await refreshCodexAccountApi(index); - await refresh(); - } catch (err) { - setError(err instanceof Error ? err.message : String(err)); - } finally { - setPerRowBusy(null); - } - } - - async function handleRefreshAllCodex() { - setBusy(true); - setError(null); - try { - const result = await refreshAllCodexAccountsApi(); - await refresh(); - if (result.failed.length > 0) { - setError( - `일부 갱신 실패: ${result.failed.map((f) => `#${f.index}`).join(', ')}`, - ); - } - } catch (err) { - setError(err instanceof Error ? err.message : String(err)); - } finally { - setBusy(false); - } - } - - async function handleSwitchCodex(index: number) { - setPerRowBusy(`switch:${index}`); - setError(null); - try { - await setCurrentCodexAccountApi(index); - await refresh(); - } catch (err) { - setError(err instanceof Error ? err.message : String(err)); - } finally { - setPerRowBusy(null); - } - } - - return ( -
-

계정

- {error ?

{error}

: null} - -
-

Claude

- {!data ? ( -

{busy ? '불러오는 중…' : '없음'}

- ) : data.claude.length === 0 ? ( -

계정 없음

- ) : ( -
    - {data.claude.map((acc) => ( -
  • -
    - #{acc.index} - - {acc.subscriptionType ?? 'unknown'} - {acc.rateLimitTier ? ` · ${acc.rateLimitTier}` : ''} - - claude - - 토큰 자동갱신 - -
    - {acc.index > 0 ? ( - - ) : ( - 기본 - )} -
  • - ))} -
- )} -
-