Files
EJClaw/src/web-dashboard-data.ts
Eyejoker 646bc34372 [codex] Improve mobile dashboard UX (#19)
* feat(dashboard): improve mobile control plane UX

* fix(dashboard): improve mobile nav accessibility

* feat(dashboard): add mobile drawer and usage glance cards

* feat(dashboard): add compact usage matrix and i18n

* fix(dashboard): make usage-first console layout

* fix(dashboard): remove chrome and group usage rows
2026-04-26 21:29:13 +09:00

204 lines
5.3 KiB
TypeScript

import { isWatchCiTask } from './task-watch-status.js';
import type { StatusSnapshot, UsageRowSnapshot } from './status-dashboard.js';
import type { ScheduledTask } from './types.js';
export interface SanitizedScheduledTask {
id: string;
groupFolder: string;
chatJid: string;
agentType: ScheduledTask['agent_type'];
ciProvider: ScheduledTask['ci_provider'];
ciMetadata: ScheduledTask['ci_metadata'];
scheduleType: ScheduledTask['schedule_type'];
scheduleValue: string;
contextMode: ScheduledTask['context_mode'];
nextRun: string | null;
lastRun: string | null;
lastResult: string | null;
status: ScheduledTask['status'];
suspendedUntil: string | null;
createdAt: string;
promptPreview: string;
promptLength: number;
isWatcher: boolean;
}
export interface WebDashboardOverview {
generatedAt: string;
services: Array<{
serviceId: string;
assistantName: string;
agentType: StatusSnapshot['agentType'];
updatedAt: string;
totalRooms: number;
activeRooms: number;
}>;
rooms: {
total: number;
active: number;
waiting: number;
inactive: number;
};
tasks: {
total: number;
active: number;
paused: number;
completed: number;
watchers: {
active: number;
paused: number;
completed: number;
};
};
usage: {
rows: UsageRowSnapshot[];
fetchedAt: string | null;
};
}
const SECRET_ASSIGNMENT_RE =
/\b([A-Z0-9_]*(?:TOKEN|SECRET|PASSWORD|API_KEY|AUTH|PRIVATE_KEY)[A-Z0-9_]*)\s*=\s*([^\s"'`]+)/gi;
const SECRET_VALUE_RE =
/\b(?:sk-[A-Za-z0-9_-]{12,}|gh[pousr]_[A-Za-z0-9_]{12,}|xox[baprs]-[A-Za-z0-9-]{12,}|eyJ[A-Za-z0-9_-]{20,}\.[A-Za-z0-9_-]{20,}\.[A-Za-z0-9_-]{10,})\b/g;
function redactSensitiveText(value: string): string {
return value
.replace(SECRET_ASSIGNMENT_RE, '$1=<redacted>')
.replace(SECRET_VALUE_RE, '<redacted-token>');
}
function truncateText(value: string, maxLength = 120): string {
if (value.length <= maxLength) return value;
return `${value.slice(0, maxLength)}...`;
}
function buildPromptPreview(prompt: string): string {
return truncateText(redactSensitiveText(prompt).replace(/\s+/g, ' ').trim());
}
function collectUsageRows(snapshots: StatusSnapshot[]): UsageRowSnapshot[] {
const rows: UsageRowSnapshot[] = [];
const seen = new Set<string>();
const sortedSnapshots = [...snapshots].sort((a, b) =>
(b.usageRowsFetchedAt ?? b.updatedAt).localeCompare(
a.usageRowsFetchedAt ?? a.updatedAt,
),
);
for (const snapshot of sortedSnapshots) {
for (const row of snapshot.usageRows ?? []) {
if (seen.has(row.name)) continue;
seen.add(row.name);
rows.push(row);
}
}
return rows;
}
export function sanitizeScheduledTask(
task: ScheduledTask,
): SanitizedScheduledTask {
return {
id: task.id,
groupFolder: task.group_folder,
chatJid: task.chat_jid,
agentType: task.agent_type,
ciProvider: task.ci_provider ?? null,
ciMetadata: task.ci_metadata ?? null,
scheduleType: task.schedule_type,
scheduleValue: task.schedule_value,
contextMode: task.context_mode,
nextRun: task.next_run,
lastRun: task.last_run,
lastResult: task.last_result,
status: task.status,
suspendedUntil: task.suspended_until ?? null,
createdAt: task.created_at,
promptPreview: buildPromptPreview(task.prompt),
promptLength: task.prompt.length,
isWatcher: isWatchCiTask(task),
};
}
export function buildWebDashboardOverview(args: {
now?: string;
snapshots: StatusSnapshot[];
tasks: ScheduledTask[];
}): WebDashboardOverview {
const rooms = {
total: 0,
active: 0,
waiting: 0,
inactive: 0,
};
const services = args.snapshots.map((snapshot) => {
let activeRooms = 0;
for (const entry of snapshot.entries) {
rooms.total += 1;
if (entry.status === 'processing') {
rooms.active += 1;
activeRooms += 1;
} else if (entry.status === 'waiting') {
rooms.waiting += 1;
activeRooms += 1;
} else {
rooms.inactive += 1;
}
}
return {
serviceId: snapshot.serviceId,
assistantName: snapshot.assistantName,
agentType: snapshot.agentType,
updatedAt: snapshot.updatedAt,
totalRooms: snapshot.entries.length,
activeRooms,
};
});
const tasks = {
total: args.tasks.length,
active: 0,
paused: 0,
completed: 0,
watchers: {
active: 0,
paused: 0,
completed: 0,
},
};
for (const task of args.tasks) {
if (task.status === 'active') tasks.active += 1;
if (task.status === 'paused') tasks.paused += 1;
if (task.status === 'completed') tasks.completed += 1;
if (isWatchCiTask(task)) {
if (task.status === 'active') tasks.watchers.active += 1;
if (task.status === 'paused') tasks.watchers.paused += 1;
if (task.status === 'completed') tasks.watchers.completed += 1;
}
}
const usageRows = collectUsageRows(args.snapshots);
const usageFetchedAt =
args.snapshots
.map((snapshot) => snapshot.usageRowsFetchedAt)
.filter((value): value is string => !!value)
.sort()
.at(-1) ?? null;
return {
generatedAt: args.now ?? new Date().toISOString(),
services,
rooms,
tasks,
usage: {
rows: usageRows,
fetchedAt: usageFetchedAt,
},
};
}