feat(dashboard): focus ops usage signals (#25)

* feat(dashboard): focus ops usage signals

* style(dashboard): apply usage formatting

* fix(dashboard): hide refresh timestamp noise
This commit is contained in:
Eyejoker
2026-04-27 00:25:21 +09:00
committed by GitHub
parent 8624c8c122
commit 19afc76b89
6 changed files with 357 additions and 175 deletions

View File

@@ -291,6 +291,14 @@ describe('web dashboard data', () => {
last_result: 'Error: BOT_TOKEN=plain-secret-value failed',
status: 'paused',
}),
makeTask({
id: 'ci-2',
prompt:
'[BACKGROUND CI WATCH]\nWatch target:\nPR #22\n\nCheck instructions:\nwatch',
last_run: '2026-04-26T05:07:00.000Z',
last_result: 'Error: BOT_TOKEN=plain-secret-value failed',
status: 'active',
}),
makeTask({
id: 'cron-1',
prompt: 'regular cron',
@@ -329,10 +337,12 @@ describe('web dashboard data', () => {
);
const ciFailure = overview.inbox.find((item) => item.kind === 'ci-failure');
expect(ciFailure).toMatchObject({
id: 'ci:ci-1',
id: 'ci:ci-2',
severity: 'error',
occurrences: 2,
source: 'scheduled-task',
taskId: 'ci-1',
taskId: 'ci-2',
lastOccurredAt: '2026-04-26T05:07:00.000Z',
});
expect(ciFailure?.summary).toContain('BOT_TOKEN=<redacted>');
expect(ciFailure?.summary).not.toContain('plain-secret-value');

View File

@@ -69,12 +69,15 @@ export type InboxItemSeverity = 'info' | 'warn' | 'error';
export interface InboxItem {
id: string;
groupKey: string;
kind: InboxItemKind;
severity: InboxItemSeverity;
title: string;
summary: string;
occurredAt: string;
lastOccurredAt: string;
createdAt: string;
occurrences: number;
source: 'status-snapshot' | 'paired-task' | 'scheduled-task';
roomJid?: string;
roomName?: string;
@@ -108,6 +111,14 @@ function buildInboxPreview(value: string): string {
return truncateText(redactSensitiveText(value).replace(/\s+/g, ' ').trim());
}
function stableHash(value: string): string {
let hash = 0;
for (let index = 0; index < value.length; index += 1) {
hash = (hash * 31 + value.charCodeAt(index)) | 0;
}
return Math.abs(hash).toString(36);
}
function collectUsageRows(snapshots: StatusSnapshot[]): UsageRowSnapshot[] {
const rows: UsageRowSnapshot[] = [];
const seen = new Set<string>();
@@ -174,12 +185,15 @@ function collectInboxItems(args: {
items.push({
id: `room:${snapshot.serviceId}:${entry.jid}`,
groupKey: `room:${snapshot.serviceId}:${entry.jid}`,
kind: 'pending-room',
severity: entry.pendingTasks > 0 ? 'warn' : 'info',
title: entry.name || entry.folder || entry.jid,
summary: parts.join(' · '),
occurredAt: snapshot.updatedAt,
lastOccurredAt: snapshot.updatedAt,
createdAt: args.createdAt,
occurrences: 1,
source: 'status-snapshot',
roomJid: entry.jid,
roomName: entry.name,
@@ -195,9 +209,9 @@ function collectInboxItems(args: {
items.push({
id: `paired:${task.id}:${task.status}`,
groupKey: `paired:${task.id}:${task.status}`,
kind,
severity:
kind === 'approval' || kind === 'arbiter-request' ? 'error' : 'warn',
severity: kind === 'arbiter-request' ? 'error' : 'warn',
title: task.title || task.group_folder,
summary: task.status,
occurredAt:
@@ -206,7 +220,14 @@ function collectInboxItems(args: {
: kind === 'reviewer-request'
? (task.review_requested_at ?? task.updated_at)
: task.updated_at,
lastOccurredAt:
kind === 'arbiter-request'
? (task.arbiter_requested_at ?? task.updated_at)
: kind === 'reviewer-request'
? (task.review_requested_at ?? task.updated_at)
: task.updated_at,
createdAt: args.createdAt,
occurrences: 1,
source: 'paired-task',
roomJid: task.chat_jid,
groupFolder: task.group_folder,
@@ -226,12 +247,15 @@ function collectInboxItems(args: {
items.push({
id: `ci:${task.id}`,
groupKey: `ci:${stableHash(result.toLowerCase())}`,
kind: 'ci-failure',
severity: task.status === 'paused' ? 'error' : 'warn',
title: 'CI watcher failed',
summary: result,
occurredAt: task.last_run ?? task.created_at,
lastOccurredAt: task.last_run ?? task.created_at,
createdAt: args.createdAt,
occurrences: 1,
source: 'scheduled-task',
roomJid: task.chat_jid,
groupFolder: task.group_folder,
@@ -247,11 +271,40 @@ function collectInboxItems(args: {
info: 2,
};
return items
const groupedItems = new Map<string, InboxItem>();
for (const item of items) {
const existing = groupedItems.get(item.groupKey);
if (!existing) {
groupedItems.set(item.groupKey, item);
continue;
}
const occurrences = existing.occurrences + item.occurrences;
const severity =
severityRank[item.severity] < severityRank[existing.severity]
? item.severity
: existing.severity;
const latest =
item.lastOccurredAt.localeCompare(existing.lastOccurredAt) > 0
? item
: existing;
groupedItems.set(item.groupKey, {
...latest,
severity,
occurrences,
lastOccurredAt:
item.lastOccurredAt.localeCompare(existing.lastOccurredAt) > 0
? item.lastOccurredAt
: existing.lastOccurredAt,
});
}
return [...groupedItems.values()]
.sort((a, b) => {
const severityDelta = severityRank[a.severity] - severityRank[b.severity];
if (severityDelta !== 0) return severityDelta;
return b.occurredAt.localeCompare(a.occurredAt);
return b.lastOccurredAt.localeCompare(a.lastOccurredAt);
})
.slice(0, 50);
}