- {overview.services.map((service) => (
-
-
- {service.agentType}
-
{service.assistantName}
-
-
-
-
- {t.service.heartbeat} {formatDate(service.updatedAt, locale)}
-
-
-
-
-
- {t.service.service}
- - {service.serviceId}
-
-
-
- {t.service.rooms}
- -
- {service.activeRooms}/{service.totalRooms} {t.status.active}
-
-
-
-
- {t.service.updated}
- - {formatDate(service.updatedAt, locale)}
-
-
-
- ))}
+
+
+
+ {t.inbox.total}
+ {items.length}
+
+
+ {t.inbox.severity.error}
+ {severityCounts.error}
+
+
+ {t.inbox.severity.warn}
+ {severityCounts.warn}
+
+
+ {t.inbox.severity.info}
+ {severityCounts.info}
+
+
+
+
+ {INBOX_FILTERS.map((item) => {
+ if (item !== 'all' && counts[item] === 0) return null;
+ const label = item === 'all' ? t.inbox.all : t.inbox.kinds[item];
+ return (
+
+ );
+ })}
+
+
+
+ {filteredItems.map((item) => {
+ const href = inboxTargetHref(item);
+ return (
+
+
+
+ {t.inbox.kinds[item.kind]}
+ {item.title}
+
+
+ {t.inbox.severity[item.severity]}
+
+
+ {item.summary || t.inbox.noSummary}
+
+
+ {t.inbox.occurred}
+ {formatDate(item.occurredAt, locale)}
+
+
+ {t.inbox.source}
+ {item.source}
+
+
+ {t.inbox.target}
+
+ {item.taskId ??
+ item.roomName ??
+ item.groupFolder ??
+ item.roomJid ??
+ '-'}
+
+
+
+ {href ? (
+
+ {item.taskId ? t.inbox.openTask : t.inbox.openRoom}
+
+ ) : null}
+
+ );
+ })}
+
+
+ );
+}
+
+function HealthPanel({
+ data,
+ locale,
+ t,
+}: {
+ data: DashboardState;
+ locale: Locale;
+ t: Messages;
+}) {
+ const services = data.overview.services;
+ const serviceLevels = services.map((service) => ({
+ service,
+ level: serviceHealthLevel(service, data.overview.generatedAt),
+ age: serviceAgeMs(service, data.overview.generatedAt),
+ }));
+ const down = serviceLevels.filter((item) => item.level === 'down').length;
+ const stale = serviceLevels.filter((item) => item.level === 'stale').length;
+ const queue = data.snapshots.reduce(
+ (acc, snapshot) => {
+ for (const entry of snapshot.entries) {
+ acc.pendingTasks += entry.pendingTasks;
+ if (entry.pendingMessages) acc.pendingMessageRooms += 1;
+ }
+ return acc;
+ },
+ { pendingTasks: 0, pendingMessageRooms: 0 },
+ );
+ const ciFailures = data.overview.inbox.filter(
+ (item) => item.kind === 'ci-failure',
+ ).length;
+ const healthLevel: HealthLevel =
+ down > 0 ? 'down' : stale > 0 || ciFailures > 0 ? 'stale' : 'ok';
+
+ return (
+
+
+
+ {t.health.system}
+ {t.health.levels[healthLevel]}
+
+ {t.health.summary}
+
+
+
+
+ {t.health.services}
+
+ {services.length - stale - down}/{services.length}
+
+ {t.health.fresh}
+
+
+ {t.health.stale}
+ {stale + down}
+
+ {down} {t.health.levels.down}
+
+
+
+ {t.health.queue}
+ {queue.pendingTasks}
+
+ {queue.pendingMessageRooms} {t.control.pendingRooms}
+
+
+
+ {t.health.ciFailures}
+ {ciFailures}
+
+ {data.overview.tasks.watchers.paused} {t.status.paused}
+
+
+
+
+ {services.length === 0 ? (
+
{t.service.empty}
+ ) : (
+
+ {serviceLevels.map(({ service, level, age }) => (
+
+
+ {service.agentType}
+ {service.assistantName}
+ {service.serviceId}
+
+
+ {t.health.levels[level]}
+
+
+ {t.service.updated}
+ {formatDate(service.updatedAt, locale)}
+ {formatDuration(age, t)}
+
+
+ {t.service.rooms}
+
+ {service.activeRooms}/{service.totalRooms}
+
+
+
+ ))}
+
+ )}
);
}
@@ -1082,13 +1313,23 @@ function App() {
>
) : null}
+ {activeView === 'inbox' ? (
+
+
+
{t.panels.inbox}
+ {t.panels.inboxQueue}
+
+
+
+ ) : null}
+
{activeView === 'health' ? (
{t.panels.health}
- {t.panels.heartbeat}
+ {t.panels.healthSignals}
-
+
) : null}
diff --git a/apps/dashboard/src/api.ts b/apps/dashboard/src/api.ts
index 031eec8..67c8646 100644
--- a/apps/dashboard/src/api.ts
+++ b/apps/dashboard/src/api.ts
@@ -35,6 +35,28 @@ export interface DashboardOverview {
}>;
fetchedAt: string | null;
};
+ inbox: Array<{
+ id: string;
+ kind:
+ | 'pending-room'
+ | 'reviewer-request'
+ | 'approval'
+ | 'arbiter-request'
+ | 'ci-failure'
+ | 'mention';
+ severity: 'info' | 'warn' | 'error';
+ title: string;
+ summary: string;
+ occurredAt: string;
+ createdAt: string;
+ source: 'status-snapshot' | 'paired-task' | 'scheduled-task';
+ roomJid?: string;
+ roomName?: string;
+ groupFolder?: string;
+ serviceId?: string;
+ taskId?: string;
+ taskStatus?: string;
+ }>;
}
export interface StatusSnapshot {
diff --git a/apps/dashboard/src/i18n.ts b/apps/dashboard/src/i18n.ts
index ba0820c..6ef0e50 100644
--- a/apps/dashboard/src/i18n.ts
+++ b/apps/dashboard/src/i18n.ts
@@ -21,6 +21,7 @@ export interface Messages {
operations: string;
updated: string;
health: string;
+ inbox: string;
usage: string;
rooms: string;
scheduled: string;
@@ -45,6 +46,9 @@ export interface Messages {
panels: {
health: string;
heartbeat: string;
+ healthSignals: string;
+ inbox: string;
+ inboxQueue: string;
usage: string;
usageWindow: string;
rooms: string;
@@ -59,6 +63,48 @@ export interface Messages {
rooms: string;
updated: string;
};
+ health: {
+ system: string;
+ summary: string;
+ signals: string;
+ services: string;
+ fresh: string;
+ stale: string;
+ queue: string;
+ ciFailures: string;
+ levels: {
+ ok: string;
+ stale: string;
+ down: string;
+ };
+ };
+ inbox: {
+ empty: string;
+ cardsAria: string;
+ summary: string;
+ total: string;
+ filters: string;
+ all: string;
+ noSummary: string;
+ occurred: string;
+ source: string;
+ target: string;
+ openTask: string;
+ openRoom: string;
+ kinds: {
+ 'pending-room': string;
+ 'reviewer-request': string;
+ approval: string;
+ 'arbiter-request': string;
+ 'ci-failure': string;
+ mention: string;
+ };
+ severity: {
+ info: string;
+ warn: string;
+ error: string;
+ };
+ };
rooms: {
empty: string;
cardsAria: string;
@@ -169,6 +215,7 @@ export const messages = {
operations: '운영',
updated: '갱신',
health: '상태',
+ inbox: '인입',
usage: '사용량',
rooms: '룸',
scheduled: '예약',
@@ -193,6 +240,9 @@ export const messages = {
panels: {
health: '상태',
heartbeat: '하트비트',
+ healthSignals: '운영 신호',
+ inbox: '인입',
+ inboxQueue: '대기 항목',
usage: '사용량',
usageWindow: '5시간 / 7일',
rooms: '룸',
@@ -207,6 +257,48 @@ export const messages = {
rooms: '룸',
updated: '갱신',
},
+ health: {
+ system: '시스템',
+ summary: '헬스는 heartbeat, 큐, CI 실패를 합산합니다.',
+ signals: '헬스 신호',
+ services: '서비스',
+ fresh: '정상 heartbeat',
+ stale: '지연',
+ queue: '큐',
+ ciFailures: 'CI 실패',
+ levels: {
+ ok: '정상',
+ stale: '주의',
+ down: '중단',
+ },
+ },
+ inbox: {
+ empty: '인입 없음.',
+ cardsAria: '인입 항목',
+ summary: '인입 요약',
+ total: '전체',
+ filters: '인입 필터',
+ all: '전체',
+ noSummary: '요약 없음',
+ occurred: '발생',
+ source: '소스',
+ target: '대상',
+ openTask: '예약 보기',
+ openRoom: '룸 보기',
+ kinds: {
+ 'pending-room': '대기 룸',
+ 'reviewer-request': '리뷰 요청',
+ approval: '승인',
+ 'arbiter-request': '중재 요청',
+ 'ci-failure': 'CI 실패',
+ mention: '멘션',
+ },
+ severity: {
+ info: '정보',
+ warn: '주의',
+ error: '위험',
+ },
+ },
rooms: {
empty: '룸 없음.',
cardsAria: '룸 상태 카드',
@@ -301,6 +393,7 @@ export const messages = {
operations: 'Ops',
updated: 'Updated',
health: 'Health',
+ inbox: 'Inbox',
usage: 'Usage',
rooms: 'Rooms',
scheduled: 'Scheduled',
@@ -325,6 +418,9 @@ export const messages = {
panels: {
health: 'Health',
heartbeat: 'Heartbeat',
+ healthSignals: 'Signals',
+ inbox: 'Inbox',
+ inboxQueue: 'Work intake',
usage: 'Usage',
usageWindow: '5h / 7d',
rooms: 'Rooms',
@@ -339,6 +435,48 @@ export const messages = {
rooms: 'rooms',
updated: 'updated',
},
+ health: {
+ system: 'System',
+ summary: 'Health combines heartbeat, queue, and CI failures.',
+ signals: 'Health signals',
+ services: 'Services',
+ fresh: 'fresh heartbeat',
+ stale: 'stale',
+ queue: 'Queue',
+ ciFailures: 'CI failures',
+ levels: {
+ ok: 'OK',
+ stale: 'Watch',
+ down: 'Down',
+ },
+ },
+ inbox: {
+ empty: 'No inbound work.',
+ cardsAria: 'Inbox items',
+ summary: 'Inbox summary',
+ total: 'Total',
+ filters: 'Inbox filters',
+ all: 'All',
+ noSummary: 'No summary',
+ occurred: 'Occurred',
+ source: 'Source',
+ target: 'Target',
+ openTask: 'Open scheduled',
+ openRoom: 'Open room',
+ kinds: {
+ 'pending-room': 'Pending room',
+ 'reviewer-request': 'Review request',
+ approval: 'Approval',
+ 'arbiter-request': 'Arbiter request',
+ 'ci-failure': 'CI failure',
+ mention: 'Mention',
+ },
+ severity: {
+ info: 'Info',
+ warn: 'Warn',
+ error: 'Risk',
+ },
+ },
rooms: {
empty: 'No rooms yet.',
cardsAria: 'Room status cards',
@@ -433,6 +571,7 @@ export const messages = {
operations: '运营',
updated: '更新',
health: '健康',
+ inbox: '收件',
usage: '用量',
rooms: '房间',
scheduled: '计划',
@@ -457,6 +596,9 @@ export const messages = {
panels: {
health: '健康',
heartbeat: '心跳',
+ healthSignals: '运行信号',
+ inbox: '收件',
+ inboxQueue: '待处理',
usage: '用量',
usageWindow: '5小时 / 7天',
rooms: '房间',
@@ -471,6 +613,48 @@ export const messages = {
rooms: '房间',
updated: '更新',
},
+ health: {
+ system: '系统',
+ summary: '健康状态汇总心跳、队列和 CI 失败。',
+ signals: '健康信号',
+ services: '服务',
+ fresh: '心跳正常',
+ stale: '延迟',
+ queue: '队列',
+ ciFailures: 'CI 失败',
+ levels: {
+ ok: '正常',
+ stale: '关注',
+ down: '中断',
+ },
+ },
+ inbox: {
+ empty: '暂无收件。',
+ cardsAria: '收件项',
+ summary: '收件摘要',
+ total: '全部',
+ filters: '收件筛选',
+ all: '全部',
+ noSummary: '无摘要',
+ occurred: '发生',
+ source: '来源',
+ target: '目标',
+ openTask: '查看计划',
+ openRoom: '查看房间',
+ kinds: {
+ 'pending-room': '待处理房间',
+ 'reviewer-request': '评审请求',
+ approval: '审批',
+ 'arbiter-request': '仲裁请求',
+ 'ci-failure': 'CI 失败',
+ mention: '提及',
+ },
+ severity: {
+ info: '信息',
+ warn: '关注',
+ error: '风险',
+ },
+ },
rooms: {
empty: '暂无房间。',
cardsAria: '房间状态卡片',
@@ -565,6 +749,7 @@ export const messages = {
operations: '運用',
updated: '更新',
health: '状態',
+ inbox: '受信',
usage: '使用量',
rooms: 'ルーム',
scheduled: '予定',
@@ -589,6 +774,9 @@ export const messages = {
panels: {
health: '状態',
heartbeat: 'ハートビート',
+ healthSignals: '運用シグナル',
+ inbox: '受信',
+ inboxQueue: '対応待ち',
usage: '使用量',
usageWindow: '5時間 / 7日',
rooms: 'ルーム',
@@ -603,6 +791,48 @@ export const messages = {
rooms: 'ルーム',
updated: '更新',
},
+ health: {
+ system: 'システム',
+ summary: '状態はハートビート、キュー、CI失敗を集計します。',
+ signals: '状態シグナル',
+ services: 'サービス',
+ fresh: '正常ハートビート',
+ stale: '遅延',
+ queue: 'キュー',
+ ciFailures: 'CI失敗',
+ levels: {
+ ok: '正常',
+ stale: '注意',
+ down: '停止',
+ },
+ },
+ inbox: {
+ empty: '受信なし。',
+ cardsAria: '受信項目',
+ summary: '受信サマリー',
+ total: '全体',
+ filters: '受信フィルター',
+ all: '全体',
+ noSummary: '概要なし',
+ occurred: '発生',
+ source: 'ソース',
+ target: '対象',
+ openTask: '予定を見る',
+ openRoom: 'ルームを見る',
+ kinds: {
+ 'pending-room': '待機ルーム',
+ 'reviewer-request': 'レビュー依頼',
+ approval: '承認',
+ 'arbiter-request': '仲裁依頼',
+ 'ci-failure': 'CI失敗',
+ mention: 'メンション',
+ },
+ severity: {
+ info: '情報',
+ warn: '注意',
+ error: '危険',
+ },
+ },
rooms: {
empty: 'ルームなし。',
cardsAria: 'ルーム状態カード',
diff --git a/apps/dashboard/src/styles.css b/apps/dashboard/src/styles.css
index 7c157b4..b18e4db 100644
--- a/apps/dashboard/src/styles.css
+++ b/apps/dashboard/src/styles.css
@@ -523,63 +523,6 @@ dd,
letter-spacing: -0.05em;
}
-.service-grid {
- display: grid;
- grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
- gap: 14px;
-}
-
-.service-card {
- display: grid;
- gap: 18px;
- padding: 20px;
- border-radius: 24px;
- background: rgba(255, 250, 240, 0.72);
-}
-
-.service-card h3 {
- margin: 6px 0 0;
- font-size: 26px;
- letter-spacing: -0.04em;
-}
-
-.heartbeat-line {
- display: flex;
- gap: 10px;
- align-items: center;
- color: var(--muted);
-}
-
-.heartbeat-line span {
- width: 10px;
- height: 10px;
- border-radius: 999px;
- background: var(--green);
- box-shadow: 0 0 0 7px rgba(63, 127, 81, 0.13);
-}
-
-dl {
- display: grid;
- gap: 10px;
- margin: 0;
-}
-
-dl div {
- display: flex;
- gap: 12px;
- justify-content: space-between;
-}
-
-dt {
- color: var(--muted);
- font-weight: 750;
-}
-
-dd {
- margin: 0;
- text-align: right;
-}
-
.table-wrap {
overflow: auto;
border: 1px solid rgba(43, 55, 38, 0.1);
@@ -837,6 +780,235 @@ progress::-moz-progress-bar {
background: rgba(255, 250, 240, 0.45);
}
+.pill-info {
+ color: #625d52;
+ background: rgba(109, 115, 95, 0.16);
+}
+
+.pill-error,
+.pill-down {
+ color: #7c2518;
+ background: rgba(183, 71, 52, 0.18);
+}
+
+.pill-stale {
+ color: #6e4a05;
+ background: rgba(181, 138, 34, 0.2);
+}
+
+.inbox-board,
+.health-board {
+ display: grid;
+ gap: 12px;
+}
+
+.inbox-summary,
+.health-signals {
+ display: grid;
+ grid-template-columns: repeat(4, minmax(0, 1fr));
+ gap: 8px;
+}
+
+.inbox-summary div,
+.health-signals div,
+.health-overview,
+.inbox-card,
+.health-service {
+ border: 1px solid rgba(43, 55, 38, 0.1);
+ border-radius: 18px;
+ background: rgba(255, 250, 240, 0.64);
+}
+
+.inbox-summary div,
+.health-signals div {
+ display: grid;
+ gap: 4px;
+ min-width: 0;
+ padding: 11px 12px;
+}
+
+.inbox-summary span,
+.health-signals span,
+.inbox-meta small,
+.health-service small,
+.health-service em {
+ color: var(--muted);
+}
+
+.inbox-summary strong,
+.health-signals strong {
+ font-size: clamp(18px, 2vw, 28px);
+ line-height: 1;
+}
+
+.inbox-filters {
+ display: flex;
+ gap: 7px;
+ overflow-x: auto;
+ padding-bottom: 2px;
+}
+
+.inbox-filters button {
+ display: inline-flex;
+ min-height: 38px;
+ flex: none;
+ gap: 7px;
+ align-items: center;
+ padding: 0 12px;
+ color: var(--bg-ink);
+ background: rgba(43, 55, 38, 0.08);
+ box-shadow: none;
+ font-size: 13px;
+ font-weight: 900;
+}
+
+.inbox-filters button.is-active,
+.inbox-filters button[aria-pressed='true'] {
+ color: #fffaf0;
+ background: linear-gradient(135deg, #2b3726, #1c211c);
+}
+
+.inbox-filters span {
+ color: inherit;
+ opacity: 0.72;
+}
+
+.inbox-list {
+ display: grid;
+ grid-template-columns: repeat(auto-fill, minmax(290px, 1fr));
+ gap: 10px;
+}
+
+.inbox-card {
+ display: grid;
+ gap: 10px;
+ min-width: 0;
+ padding: 13px;
+}
+
+.inbox-error {
+ border-color: rgba(183, 71, 52, 0.26);
+ background: rgba(255, 244, 238, 0.72);
+}
+
+.inbox-warn {
+ border-color: rgba(181, 138, 34, 0.24);
+}
+
+.inbox-card-head,
+.health-service {
+ display: grid;
+ grid-template-columns: minmax(0, 1fr) auto;
+ gap: 10px;
+ align-items: start;
+}
+
+.inbox-card-head strong,
+.inbox-card p,
+.inbox-meta strong,
+.health-service strong,
+.health-service small {
+ min-width: 0;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+
+.inbox-card p {
+ margin: 0;
+ color: var(--bg-ink);
+ overflow-wrap: anywhere;
+ white-space: normal;
+}
+
+.inbox-meta {
+ display: grid;
+ grid-template-columns: repeat(3, minmax(0, 1fr));
+ gap: 7px;
+}
+
+.inbox-meta span {
+ min-width: 0;
+ padding: 8px;
+ border-radius: 12px;
+ background: rgba(43, 55, 38, 0.045);
+}
+
+.inbox-meta small,
+.inbox-meta strong {
+ display: block;
+}
+
+.inbox-target {
+ display: inline-flex;
+ width: fit-content;
+ min-height: 34px;
+ align-items: center;
+ padding: 0 12px;
+ border-radius: 999px;
+ color: var(--accent-strong);
+ background: rgba(191, 95, 44, 0.11);
+ font-size: 13px;
+ font-weight: 900;
+ text-decoration: none;
+}
+
+.health-overview {
+ display: grid;
+ grid-template-columns: minmax(0, 0.42fr) minmax(0, 1fr);
+ gap: 14px;
+ align-items: center;
+ padding: 16px;
+}
+
+.health-overview strong {
+ display: block;
+ margin-top: 4px;
+ font-size: clamp(24px, 3vw, 40px);
+ letter-spacing: -0.05em;
+}
+
+.health-overview p {
+ margin: 0;
+ color: var(--muted);
+}
+
+.health-down {
+ border-color: rgba(183, 71, 52, 0.32);
+}
+
+.health-stale {
+ border-color: rgba(181, 138, 34, 0.28);
+}
+
+.health-service-list {
+ display: grid;
+ gap: 8px;
+}
+
+.health-service {
+ grid-template-columns: minmax(0, 1.2fr) auto minmax(0, 0.9fr) minmax(
+ 64px,
+ 0.35fr
+ );
+ align-items: center;
+ padding: 12px;
+}
+
+.health-service > div {
+ min-width: 0;
+}
+
+.health-service strong,
+.health-service small,
+.health-service em {
+ display: block;
+}
+
+.health-service em {
+ font-style: normal;
+}
+
.task-board {
display: grid;
gap: 12px;
@@ -1370,6 +1542,37 @@ progress::-moz-progress-bar {
grid-template-columns: 1fr 1fr;
}
+ .inbox-summary,
+ .health-signals {
+ grid-template-columns: repeat(2, minmax(0, 1fr));
+ }
+
+ .inbox-list {
+ grid-template-columns: 1fr;
+ }
+
+ .inbox-card {
+ padding: 10px;
+ }
+
+ .inbox-meta {
+ grid-template-columns: repeat(2, minmax(0, 1fr));
+ }
+
+ .health-overview {
+ grid-template-columns: 1fr;
+ gap: 8px;
+ }
+
+ .health-service {
+ grid-template-columns: minmax(0, 1fr) auto;
+ }
+
+ .health-service > div:nth-of-type(2),
+ .health-service > div:nth-of-type(3) {
+ grid-column: 1 / -1;
+ }
+
.task-group {
padding: 10px;
}
@@ -1440,6 +1643,11 @@ progress::-moz-progress-bar {
grid-template-columns: 1fr;
}
+ .inbox-meta,
+ .health-signals {
+ grid-template-columns: 1fr;
+ }
+
.task-time-grid {
grid-template-columns: repeat(3, minmax(0, 1fr));
}