Extract dashboard ServicePanel component (#66)

* 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
This commit is contained in:
Eyejoker
2026-04-28 16:18:54 +09:00
committed by GitHub
parent 5902960144
commit 74cb43b1d8
3 changed files with 376 additions and 210 deletions

View File

@@ -0,0 +1,150 @@
import { createElement } from 'react';
import { renderToStaticMarkup } from 'react-dom/server';
import { describe, expect, it } from 'vitest';
import type { DashboardOverview, StatusSnapshot } from './api';
import { messages, type Messages } from './i18n';
import { ServicePanel, type ServicePanelProps } from './ServicePanel';
const t = messages.en;
function formatDuration(value: number | null, t: Messages): string {
if (value === null) return '-';
const seconds = Math.floor(value / 1000);
if (seconds < 60) return `${seconds}${t.units.second}`;
const minutes = Math.floor(seconds / 60);
if (minutes < 60) return `${minutes}${t.units.minute}`;
const hours = Math.floor(minutes / 60);
return `${hours}${t.units.hour} ${minutes % 60}${t.units.minute}`;
}
function overview(
services: DashboardOverview['services'],
overrides: Partial<DashboardOverview> = {},
): DashboardOverview {
return {
generatedAt: '2026-04-28T04:15:00.000Z',
inbox: [],
operations: { serviceRestarts: [] },
rooms: { active: 0, inactive: 0, total: 0, waiting: 0 },
services,
tasks: {
active: 0,
completed: 0,
paused: 0,
total: 0,
watchers: { active: 0, completed: 0, paused: 0 },
},
usage: { fetchedAt: '2026-04-28T04:15:00.000Z', rows: [] },
...overrides,
};
}
const snapshots: StatusSnapshot[] = [
{
agentType: 'codex',
assistantName: 'owner',
entries: [
{
agentType: 'codex',
elapsedMs: null,
folder: 'eyejokerdb',
jid: 'room-1',
name: 'eyejokerdb-main',
pendingMessages: true,
pendingTasks: 3,
status: 'waiting',
},
],
serviceId: 'svc-owner',
updatedAt: '2026-04-28T04:14:00.000Z',
},
];
const baseProps: ServicePanelProps = {
formatDuration,
locale: 'en',
onRestartStack: () => {},
overview: overview(
[
{
activeRooms: 2,
agentType: 'codex',
assistantName: 'owner',
serviceId: 'svc-owner',
totalRooms: 3,
updatedAt: '2026-04-28T04:14:00.000Z',
},
{
activeRooms: 0,
agentType: 'claude',
assistantName: 'reviewer',
serviceId: 'svc-reviewer',
totalRooms: 1,
updatedAt: '2026-04-28T04:08:00.000Z',
},
],
{
inbox: [
{
createdAt: '2026-04-28T04:10:00.000Z',
groupFolder: 'eyejokerdb',
groupKey: 'ci',
id: 'ci-1',
kind: 'ci-failure',
lastOccurredAt: '2026-04-28T04:12:00.000Z',
occurrences: 2,
occurredAt: '2026-04-28T04:10:00.000Z',
severity: 'error',
source: 'status-snapshot',
summary: 'CI failed',
title: 'CI failed',
},
],
operations: {
serviceRestarts: [
{
completedAt: null,
id: 'restart-1',
requestedAt: '2026-04-28T04:13:00.000Z',
services: ['owner', 'reviewer'],
status: 'running',
target: 'stack',
},
],
},
},
),
serviceActionKey: null,
snapshots,
t,
};
describe('ServicePanel', () => {
it('renders service health, queue signals, and restart log', () => {
const html = renderToStaticMarkup(createElement(ServicePanel, baseProps));
expect(html).toContain(t.health.levels.stale);
expect(html).toContain(t.health.ciFailures);
expect(html).toContain('2');
expect(html).toContain('3');
expect(html).toContain('reviewer');
expect(html).toContain(t.health.restartLog);
expect(html).toContain('owner, reviewer');
});
it('renders restart pending state and empty services state', () => {
const html = renderToStaticMarkup(
createElement(ServicePanel, {
...baseProps,
overview: overview([]),
serviceActionKey: 'stack:restart',
snapshots: [],
}),
);
expect(html).toContain(t.health.restarting);
expect(html).toContain('disabled=""');
expect(html).toContain(t.service.empty);
});
});