diff --git a/src/unified-dashboard.test.ts b/src/unified-dashboard.test.ts new file mode 100644 index 0000000..bb1cd66 --- /dev/null +++ b/src/unified-dashboard.test.ts @@ -0,0 +1,59 @@ +import { describe, expect, it } from 'vitest'; + +import { + formatStatusHeader, + summarizeWatcherTasks, +} from './unified-dashboard.js'; + +describe('summarizeWatcherTasks', () => { + it('counts active and paused watcher tasks only', () => { + const summary = summarizeWatcherTasks([ + { + prompt: + '[BACKGROUND CI WATCH]\n\nWatch target:\nGitHub Actions run 1\n\nTask ID:\na', + status: 'active', + }, + { + prompt: + '[BACKGROUND CI WATCH]\n\nWatch target:\nGitHub Actions run 2\n\nTask ID:\nb', + status: 'paused', + }, + { + prompt: + '[BACKGROUND CI WATCH]\n\nWatch target:\nGitHub Actions run 3\n\nTask ID:\nc', + status: 'completed', + }, + { + prompt: 'normal scheduled task', + status: 'active', + }, + ]); + + expect(summary).toEqual({ + active: 1, + paused: 1, + }); + }); +}); + +describe('formatStatusHeader', () => { + it('shows active watcher count in the dashboard header', () => { + expect( + formatStatusHeader({ + totalActive: 3, + totalRooms: 8, + watchers: { active: 2, paused: 0 }, + }), + ).toBe('**πŸ“Š μ—μ΄μ „νŠΈ μƒνƒœ** β€” ν™œμ„± 3 / 8 | κ°μ‹œ 2'); + }); + + it('adds paused watcher count only when present', () => { + expect( + formatStatusHeader({ + totalActive: 3, + totalRooms: 8, + watchers: { active: 2, paused: 1 }, + }), + ).toBe('**πŸ“Š μ—μ΄μ „νŠΈ μƒνƒœ** β€” ν™œμ„± 3 / 8 | κ°μ‹œ 2 | μΌμ‹œμ •μ§€ 1'); + }); +}); diff --git a/src/unified-dashboard.ts b/src/unified-dashboard.ts index 384ca34..3a60a47 100644 --- a/src/unified-dashboard.ts +++ b/src/unified-dashboard.ts @@ -25,9 +25,10 @@ import { mergeClaudeDashboardAccounts, type UsageRow, } from './dashboard-usage-rows.js'; -import { getAllChats, updateRegisteredGroupName } from './db.js'; +import { getAllChats, getAllTasks, updateRegisteredGroupName } from './db.js'; import type { GroupQueue } from './group-queue.js'; import { logger } from './logger.js'; +import { isWatchCiTask } from './task-watch-status.js'; import { readStatusSnapshots, writeStatusSnapshot, @@ -37,6 +38,7 @@ import type { Channel, ChannelMeta, RegisteredGroup, + ScheduledTask, } from './types.js'; export interface UnifiedDashboardOptions { @@ -81,6 +83,43 @@ let cachedCodexUsageRows: UsageRow[] = []; /** Codex service only: ISO timestamp of last successful usage fetch. */ let codexUsageFetchedAt: string | null = null; +export interface WatcherTaskSummary { + active: number; + paused: number; +} + +export function summarizeWatcherTasks( + tasks: Array>, +): WatcherTaskSummary { + let active = 0; + let paused = 0; + + for (const task of tasks) { + if (!isWatchCiTask(task)) continue; + if (task.status === 'active') active += 1; + if (task.status === 'paused') paused += 1; + } + + return { active, paused }; +} + +export function formatStatusHeader(args: { + totalActive: number; + totalRooms: number; + watchers: WatcherTaskSummary; +}): string { + const parts = [ + `**πŸ“Š μ—μ΄μ „νŠΈ μƒνƒœ** β€” ν™œμ„± ${args.totalActive} / ${args.totalRooms}`, + `κ°μ‹œ ${args.watchers.active}`, + ]; + + if (args.watchers.paused > 0) { + parts.push(`μΌμ‹œμ •μ§€ ${args.watchers.paused}`); + } + + return parts.join(' | '); +} + function findDiscordChannel(channels: Channel[]): Channel | undefined { return channels.find( (channel) => channel.name.startsWith('discord') && channel.isConnected(), @@ -212,6 +251,7 @@ function buildStatusContent(): string { if (!STATUS_SHOW_ROOMS) return ''; const snapshots = readStatusSnapshots(STATUS_SNAPSHOT_MAX_AGE_MS); + const watcherSummary = summarizeWatcherTasks(getAllTasks()); const chatNameByJid = new Map( getAllChats().map((chat) => [chat.jid, chat.name]), ); @@ -313,7 +353,11 @@ function buildStatusContent(): string { } } - const header = `**πŸ“Š μ—μ΄μ „νŠΈ μƒνƒœ** β€” ν™œμ„± ${totalActive} / ${totalRooms}`; + const header = formatStatusHeader({ + totalActive, + totalRooms, + watchers: watcherSummary, + }); const sections = renderCategorizedRoomSections({ lines: roomLines, showCategoryHeaders: channelMetaCache.size > 0,