Add read-only web dashboard MVP
Adds a disabled-by-default loopback web dashboard MVP with read-only control-plane views, prompt preview redaction, Vite React UI, and validation coverage.
This commit is contained in:
12
apps/dashboard/index.html
Normal file
12
apps/dashboard/index.html
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="ko">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>EJClaw Dashboard</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="root"></div>
|
||||||
|
<script type="module" src="/src/main.tsx"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
444
apps/dashboard/src/App.tsx
Normal file
444
apps/dashboard/src/App.tsx
Normal file
@@ -0,0 +1,444 @@
|
|||||||
|
import { useEffect, useMemo, useState, type ReactNode } from 'react';
|
||||||
|
|
||||||
|
import {
|
||||||
|
type DashboardOverview,
|
||||||
|
type DashboardTask,
|
||||||
|
type StatusSnapshot,
|
||||||
|
fetchDashboardData,
|
||||||
|
} from './api';
|
||||||
|
import './styles.css';
|
||||||
|
|
||||||
|
interface DashboardState {
|
||||||
|
overview: DashboardOverview;
|
||||||
|
snapshots: StatusSnapshot[];
|
||||||
|
tasks: DashboardTask[];
|
||||||
|
}
|
||||||
|
|
||||||
|
const REFRESH_INTERVAL_MS = 15_000;
|
||||||
|
|
||||||
|
function formatDate(value: string | null | undefined): string {
|
||||||
|
if (!value) return '-';
|
||||||
|
const date = new Date(value);
|
||||||
|
if (Number.isNaN(date.getTime())) return value;
|
||||||
|
return new Intl.DateTimeFormat('ko-KR', {
|
||||||
|
month: '2-digit',
|
||||||
|
day: '2-digit',
|
||||||
|
hour: '2-digit',
|
||||||
|
minute: '2-digit',
|
||||||
|
second: '2-digit',
|
||||||
|
}).format(date);
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatPct(value: number): string {
|
||||||
|
return `${Math.round(value)}%`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function statusLabel(status: string): string {
|
||||||
|
switch (status) {
|
||||||
|
case 'processing':
|
||||||
|
return '처리중';
|
||||||
|
case 'waiting':
|
||||||
|
return '대기';
|
||||||
|
case 'inactive':
|
||||||
|
return '휴면';
|
||||||
|
case 'active':
|
||||||
|
return '활성';
|
||||||
|
case 'paused':
|
||||||
|
return '일시정지';
|
||||||
|
case 'completed':
|
||||||
|
return '완료';
|
||||||
|
default:
|
||||||
|
return status;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatDuration(value: number | null): string {
|
||||||
|
if (value === null) return '-';
|
||||||
|
const seconds = Math.floor(value / 1000);
|
||||||
|
if (seconds < 60) return `${seconds}s`;
|
||||||
|
const minutes = Math.floor(seconds / 60);
|
||||||
|
if (minutes < 60) return `${minutes}m`;
|
||||||
|
const hours = Math.floor(minutes / 60);
|
||||||
|
return `${hours}h ${minutes % 60}m`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function Card({
|
||||||
|
label,
|
||||||
|
value,
|
||||||
|
hint,
|
||||||
|
}: {
|
||||||
|
label: string;
|
||||||
|
value: string | number;
|
||||||
|
hint?: string;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<section className="card metric-card">
|
||||||
|
<span>{label}</span>
|
||||||
|
<strong>{value}</strong>
|
||||||
|
{hint ? <small>{hint}</small> : null}
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function EmptyState({ children }: { children: ReactNode }) {
|
||||||
|
return <div className="empty-state">{children}</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
function ControlRail({ data }: { data: DashboardState }) {
|
||||||
|
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 },
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section className="control-rail" aria-label="control plane summary">
|
||||||
|
<div>
|
||||||
|
<span className="eyebrow">agent heartbeat</span>
|
||||||
|
<strong>
|
||||||
|
{data.overview.rooms.active + data.overview.rooms.waiting}/
|
||||||
|
{data.overview.rooms.total}
|
||||||
|
</strong>
|
||||||
|
<small>processing + waiting rooms</small>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span className="eyebrow">work queue</span>
|
||||||
|
<strong>{queue.pendingTasks}</strong>
|
||||||
|
<small>{queue.pendingMessageRooms} rooms with pending messages</small>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span className="eyebrow">governance</span>
|
||||||
|
<strong>read-only</strong>
|
||||||
|
<small>no approvals, merges, or worker kills in MVP</small>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span className="eyebrow">audit safety</span>
|
||||||
|
<strong>redacted</strong>
|
||||||
|
<small>task prompts are preview-only</small>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function ServicePanel({ overview }: { overview: DashboardOverview }) {
|
||||||
|
if (overview.services.length === 0) {
|
||||||
|
return (
|
||||||
|
<EmptyState>
|
||||||
|
최근 status snapshot이 없어. 서비스가 아직 heartbeat를 안 쓴 상태일 수
|
||||||
|
있어.
|
||||||
|
</EmptyState>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="service-grid">
|
||||||
|
{overview.services.map((service) => (
|
||||||
|
<section className="card service-card" key={service.serviceId}>
|
||||||
|
<div>
|
||||||
|
<span className="eyebrow">{service.agentType}</span>
|
||||||
|
<h3>{service.assistantName}</h3>
|
||||||
|
</div>
|
||||||
|
<div className="heartbeat-line">
|
||||||
|
<span />
|
||||||
|
<small>heartbeat {formatDate(service.updatedAt)}</small>
|
||||||
|
</div>
|
||||||
|
<dl>
|
||||||
|
<div>
|
||||||
|
<dt>service</dt>
|
||||||
|
<dd>{service.serviceId}</dd>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<dt>rooms</dt>
|
||||||
|
<dd>
|
||||||
|
{service.activeRooms}/{service.totalRooms} active
|
||||||
|
</dd>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<dt>updated</dt>
|
||||||
|
<dd>{formatDate(service.updatedAt)}</dd>
|
||||||
|
</div>
|
||||||
|
</dl>
|
||||||
|
</section>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function RoomPanel({ snapshots }: { snapshots: StatusSnapshot[] }) {
|
||||||
|
const entries = snapshots.flatMap((snapshot) =>
|
||||||
|
snapshot.entries.map((entry) => ({
|
||||||
|
...entry,
|
||||||
|
serviceId: snapshot.serviceId,
|
||||||
|
})),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (entries.length === 0) {
|
||||||
|
return <EmptyState>표시할 룸 상태가 없어.</EmptyState>;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="table-wrap">
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>room</th>
|
||||||
|
<th>service</th>
|
||||||
|
<th>agent</th>
|
||||||
|
<th>status</th>
|
||||||
|
<th>queue</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{entries.map((entry) => (
|
||||||
|
<tr key={`${entry.serviceId}:${entry.jid}`}>
|
||||||
|
<td>
|
||||||
|
<strong>{entry.name}</strong>
|
||||||
|
<span>
|
||||||
|
{entry.folder} · {entry.jid}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td>{entry.serviceId}</td>
|
||||||
|
<td>{entry.agentType}</td>
|
||||||
|
<td>
|
||||||
|
<span className={`pill pill-${entry.status}`}>
|
||||||
|
{statusLabel(entry.status)}
|
||||||
|
</span>
|
||||||
|
<small>{formatDuration(entry.elapsedMs)}</small>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
{entry.pendingTasks} task{entry.pendingMessages ? ' · msg' : ''}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function UsagePanel({ overview }: { overview: DashboardOverview }) {
|
||||||
|
if (overview.usage.rows.length === 0) {
|
||||||
|
return (
|
||||||
|
<EmptyState>
|
||||||
|
사용량 snapshot이 없어. usage dashboard가 꺼져 있거나 아직 수집 전일 수
|
||||||
|
있어.
|
||||||
|
</EmptyState>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="usage-list">
|
||||||
|
{overview.usage.rows.map((row) => (
|
||||||
|
<section className="usage-row" key={row.name}>
|
||||||
|
<div className="usage-title">
|
||||||
|
<strong>{row.name}</strong>
|
||||||
|
<span>fetched {formatDate(overview.usage.fetchedAt)}</span>
|
||||||
|
</div>
|
||||||
|
<div className="bar-line">
|
||||||
|
<span>5h</span>
|
||||||
|
<progress max={100} value={row.h5pct} />
|
||||||
|
<strong>{formatPct(row.h5pct)}</strong>
|
||||||
|
<small>{row.h5reset}</small>
|
||||||
|
</div>
|
||||||
|
<div className="bar-line">
|
||||||
|
<span>7d</span>
|
||||||
|
<progress max={100} value={row.d7pct} />
|
||||||
|
<strong>{formatPct(row.d7pct)}</strong>
|
||||||
|
<small>{row.d7reset}</small>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function TaskPanel({ tasks }: { tasks: DashboardTask[] }) {
|
||||||
|
const sortedTasks = useMemo(
|
||||||
|
() =>
|
||||||
|
[...tasks].sort((a, b) => {
|
||||||
|
const statusRank = { active: 0, paused: 1, completed: 2 } as const;
|
||||||
|
const rankDelta = statusRank[a.status] - statusRank[b.status];
|
||||||
|
if (rankDelta !== 0) return rankDelta;
|
||||||
|
return (a.nextRun ?? a.createdAt).localeCompare(
|
||||||
|
b.nextRun ?? b.createdAt,
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
[tasks],
|
||||||
|
);
|
||||||
|
|
||||||
|
if (sortedTasks.length === 0) {
|
||||||
|
return <EmptyState>등록된 scheduled task가 없어.</EmptyState>;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="table-wrap">
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>task</th>
|
||||||
|
<th>status</th>
|
||||||
|
<th>schedule</th>
|
||||||
|
<th>next</th>
|
||||||
|
<th>last</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{sortedTasks.map((task) => (
|
||||||
|
<tr key={task.id}>
|
||||||
|
<td>
|
||||||
|
<strong>{task.isWatcher ? 'CI Watch' : task.id}</strong>
|
||||||
|
<span>{task.promptPreview || '(empty prompt preview)'}</span>
|
||||||
|
<small>
|
||||||
|
{task.groupFolder} · {task.promptLength} chars
|
||||||
|
</small>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<span className={`pill pill-${task.status}`}>
|
||||||
|
{statusLabel(task.status)}
|
||||||
|
</span>
|
||||||
|
{task.suspendedUntil ? (
|
||||||
|
<small>until {formatDate(task.suspendedUntil)}</small>
|
||||||
|
) : null}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
{task.scheduleType} · {task.scheduleValue}
|
||||||
|
<small>{task.contextMode}</small>
|
||||||
|
</td>
|
||||||
|
<td>{formatDate(task.nextRun)}</td>
|
||||||
|
<td>
|
||||||
|
{formatDate(task.lastRun)}
|
||||||
|
{task.lastResult ? <small>{task.lastResult}</small> : null}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function App() {
|
||||||
|
const [data, setData] = useState<DashboardState | null>(null);
|
||||||
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [refreshing, setRefreshing] = useState(false);
|
||||||
|
|
||||||
|
async function refresh(showSpinner = false) {
|
||||||
|
if (showSpinner) setRefreshing(true);
|
||||||
|
try {
|
||||||
|
const nextData = await fetchDashboardData();
|
||||||
|
setData(nextData);
|
||||||
|
setError(null);
|
||||||
|
} catch (err) {
|
||||||
|
setError(err instanceof Error ? err.message : String(err));
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
setRefreshing(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
void refresh();
|
||||||
|
const id = window.setInterval(() => {
|
||||||
|
void refresh();
|
||||||
|
}, REFRESH_INTERVAL_MS);
|
||||||
|
return () => window.clearInterval(id);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
if (loading && !data) {
|
||||||
|
return (
|
||||||
|
<main className="shell loading">EJClaw Dashboard 불러오는 중...</main>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<main className="shell">
|
||||||
|
<header className="hero">
|
||||||
|
<div>
|
||||||
|
<span className="eyebrow">EJClaw Control Plane · read-only MVP</span>
|
||||||
|
<h1>Agent factory control room</h1>
|
||||||
|
<p>
|
||||||
|
Discord는 pager와 승인 호출로 남기고, 여기서는 agent heartbeat, room
|
||||||
|
queue, scheduled work, quota, audit preview를 한 화면에서 본다.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<button disabled={refreshing} onClick={() => void refresh(true)}>
|
||||||
|
{refreshing ? '새로고침 중' : '새로고침'}
|
||||||
|
</button>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
{error ? (
|
||||||
|
<section className="error-card">API 오류: {error}</section>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
{data ? (
|
||||||
|
<>
|
||||||
|
<ControlRail data={data} />
|
||||||
|
|
||||||
|
<section className="metrics-grid">
|
||||||
|
<Card
|
||||||
|
label="agents"
|
||||||
|
value={data.overview.services.length}
|
||||||
|
hint={formatDate(data.overview.generatedAt)}
|
||||||
|
/>
|
||||||
|
<Card
|
||||||
|
label="rooms"
|
||||||
|
value={data.overview.rooms.total}
|
||||||
|
hint={`${data.overview.rooms.active} processing · ${data.overview.rooms.waiting} waiting`}
|
||||||
|
/>
|
||||||
|
<Card
|
||||||
|
label="tasks"
|
||||||
|
value={data.overview.tasks.total}
|
||||||
|
hint={`${data.overview.tasks.active} active · ${data.overview.tasks.paused} paused`}
|
||||||
|
/>
|
||||||
|
<Card
|
||||||
|
label="CI watchers"
|
||||||
|
value={data.overview.tasks.watchers.active}
|
||||||
|
hint={`${data.overview.tasks.watchers.paused} paused · ${data.overview.tasks.watchers.completed} done`}
|
||||||
|
/>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section className="panel">
|
||||||
|
<div className="panel-title">
|
||||||
|
<h2>Agent Heartbeats</h2>
|
||||||
|
<span>최근 heartbeat 기준</span>
|
||||||
|
</div>
|
||||||
|
<ServicePanel overview={data.overview} />
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section className="panel split-panel">
|
||||||
|
<div>
|
||||||
|
<div className="panel-title">
|
||||||
|
<h2>Cost & Quota</h2>
|
||||||
|
<span>5h / 7d usage snapshot</span>
|
||||||
|
</div>
|
||||||
|
<UsagePanel overview={data.overview} />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div className="panel-title">
|
||||||
|
<h2>Rooms & Queues</h2>
|
||||||
|
<span>processing / waiting / inactive</span>
|
||||||
|
</div>
|
||||||
|
<RoomPanel snapshots={data.snapshots} />
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section className="panel">
|
||||||
|
<div className="panel-title">
|
||||||
|
<h2>Scheduled Work</h2>
|
||||||
|
<span>watchers, heartbeats, redacted prompt previews</span>
|
||||||
|
</div>
|
||||||
|
<TaskPanel tasks={data.tasks} />
|
||||||
|
</section>
|
||||||
|
</>
|
||||||
|
) : null}
|
||||||
|
</main>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default App;
|
||||||
100
apps/dashboard/src/api.ts
Normal file
100
apps/dashboard/src/api.ts
Normal file
@@ -0,0 +1,100 @@
|
|||||||
|
export interface DashboardOverview {
|
||||||
|
generatedAt: string;
|
||||||
|
services: Array<{
|
||||||
|
serviceId: string;
|
||||||
|
assistantName: string;
|
||||||
|
agentType: string;
|
||||||
|
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: Array<{
|
||||||
|
name: string;
|
||||||
|
h5pct: number;
|
||||||
|
h5reset: string;
|
||||||
|
d7pct: number;
|
||||||
|
d7reset: string;
|
||||||
|
}>;
|
||||||
|
fetchedAt: string | null;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface StatusSnapshot {
|
||||||
|
serviceId: string;
|
||||||
|
agentType: string;
|
||||||
|
assistantName: string;
|
||||||
|
updatedAt: string;
|
||||||
|
entries: Array<{
|
||||||
|
jid: string;
|
||||||
|
name: string;
|
||||||
|
folder: string;
|
||||||
|
agentType: string;
|
||||||
|
status: 'processing' | 'waiting' | 'inactive';
|
||||||
|
elapsedMs: number | null;
|
||||||
|
pendingMessages: boolean;
|
||||||
|
pendingTasks: number;
|
||||||
|
}>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DashboardTask {
|
||||||
|
id: string;
|
||||||
|
groupFolder: string;
|
||||||
|
chatJid: string;
|
||||||
|
agentType: string | null;
|
||||||
|
ciProvider: string | null;
|
||||||
|
ciMetadata: string | null;
|
||||||
|
scheduleType: string;
|
||||||
|
scheduleValue: string;
|
||||||
|
contextMode: string;
|
||||||
|
nextRun: string | null;
|
||||||
|
lastRun: string | null;
|
||||||
|
lastResult: string | null;
|
||||||
|
status: 'active' | 'paused' | 'completed';
|
||||||
|
suspendedUntil: string | null;
|
||||||
|
createdAt: string;
|
||||||
|
promptPreview: string;
|
||||||
|
promptLength: number;
|
||||||
|
isWatcher: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function fetchJson<T>(path: string): Promise<T> {
|
||||||
|
const response = await fetch(path, {
|
||||||
|
headers: { accept: 'application/json' },
|
||||||
|
});
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`${path} failed: ${response.status}`);
|
||||||
|
}
|
||||||
|
return (await response.json()) as T;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function fetchDashboardData(): Promise<{
|
||||||
|
overview: DashboardOverview;
|
||||||
|
snapshots: StatusSnapshot[];
|
||||||
|
tasks: DashboardTask[];
|
||||||
|
}> {
|
||||||
|
const [overview, snapshots, tasks] = await Promise.all([
|
||||||
|
fetchJson<DashboardOverview>('/api/overview'),
|
||||||
|
fetchJson<StatusSnapshot[]>('/api/status-snapshots'),
|
||||||
|
fetchJson<DashboardTask[]>('/api/tasks'),
|
||||||
|
]);
|
||||||
|
|
||||||
|
return { overview, snapshots, tasks };
|
||||||
|
}
|
||||||
16
apps/dashboard/src/main.tsx
Normal file
16
apps/dashboard/src/main.tsx
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
import { StrictMode } from 'react';
|
||||||
|
import { createRoot } from 'react-dom/client';
|
||||||
|
|
||||||
|
import App from './App';
|
||||||
|
|
||||||
|
const root = document.getElementById('root');
|
||||||
|
|
||||||
|
if (!root) {
|
||||||
|
throw new Error('Dashboard root element was not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
createRoot(root).render(
|
||||||
|
<StrictMode>
|
||||||
|
<App />
|
||||||
|
</StrictMode>,
|
||||||
|
);
|
||||||
506
apps/dashboard/src/styles.css
Normal file
506
apps/dashboard/src/styles.css
Normal file
@@ -0,0 +1,506 @@
|
|||||||
|
:root {
|
||||||
|
color-scheme: light;
|
||||||
|
--bg: #f3efe4;
|
||||||
|
--bg-ink: #1c211c;
|
||||||
|
--panel: rgba(255, 252, 242, 0.9);
|
||||||
|
--panel-solid: #fffaf0;
|
||||||
|
--panel-border: rgba(43, 55, 38, 0.16);
|
||||||
|
--muted: #6d735f;
|
||||||
|
--accent: #bf5f2c;
|
||||||
|
--accent-strong: #8f351b;
|
||||||
|
--green: #3f7f51;
|
||||||
|
--yellow: #b58a22;
|
||||||
|
--red: #b74734;
|
||||||
|
--shadow: 0 24px 70px rgba(44, 39, 25, 0.18);
|
||||||
|
font-family:
|
||||||
|
'Avenir Next', 'Segoe UI Variable', 'Noto Sans KR', ui-sans-serif,
|
||||||
|
sans-serif;
|
||||||
|
background: var(--bg);
|
||||||
|
color: var(--bg-ink);
|
||||||
|
}
|
||||||
|
|
||||||
|
* {
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
min-width: 320px;
|
||||||
|
min-height: 100vh;
|
||||||
|
margin: 0;
|
||||||
|
background:
|
||||||
|
radial-gradient(
|
||||||
|
circle at 18% 12%,
|
||||||
|
rgba(214, 127, 58, 0.22),
|
||||||
|
transparent 28rem
|
||||||
|
),
|
||||||
|
radial-gradient(
|
||||||
|
circle at 88% 8%,
|
||||||
|
rgba(74, 123, 87, 0.18),
|
||||||
|
transparent 24rem
|
||||||
|
),
|
||||||
|
linear-gradient(135deg, #f4ead5 0%, #edf1df 46%, #f5f0e5 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
button,
|
||||||
|
input,
|
||||||
|
table {
|
||||||
|
font: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
.shell {
|
||||||
|
width: min(1480px, calc(100% - 40px));
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 34px 0 56px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.shell::before {
|
||||||
|
position: fixed;
|
||||||
|
inset: 0;
|
||||||
|
z-index: -1;
|
||||||
|
pointer-events: none;
|
||||||
|
content: '';
|
||||||
|
background-image:
|
||||||
|
linear-gradient(rgba(28, 33, 28, 0.055) 1px, transparent 1px),
|
||||||
|
linear-gradient(90deg, rgba(28, 33, 28, 0.045) 1px, transparent 1px);
|
||||||
|
background-size: 44px 44px;
|
||||||
|
mask-image: linear-gradient(to bottom, rgba(0, 0, 0, 0.75), transparent 82%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading {
|
||||||
|
display: grid;
|
||||||
|
min-height: 100vh;
|
||||||
|
place-items: center;
|
||||||
|
font-size: clamp(24px, 5vw, 54px);
|
||||||
|
font-weight: 900;
|
||||||
|
letter-spacing: -0.05em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr auto;
|
||||||
|
gap: 28px;
|
||||||
|
align-items: end;
|
||||||
|
padding: clamp(28px, 4vw, 54px);
|
||||||
|
overflow: hidden;
|
||||||
|
border: 1px solid var(--panel-border);
|
||||||
|
border-radius: 34px;
|
||||||
|
background:
|
||||||
|
linear-gradient(
|
||||||
|
128deg,
|
||||||
|
rgba(255, 250, 240, 0.96),
|
||||||
|
rgba(255, 250, 240, 0.76)
|
||||||
|
),
|
||||||
|
linear-gradient(45deg, rgba(191, 95, 44, 0.12), rgba(63, 127, 81, 0.1));
|
||||||
|
box-shadow: var(--shadow);
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero h1 {
|
||||||
|
max-width: 880px;
|
||||||
|
margin: 10px 0 12px;
|
||||||
|
font-family: 'Georgia', 'Noto Serif KR', serif;
|
||||||
|
font-size: clamp(42px, 8vw, 108px);
|
||||||
|
line-height: 0.92;
|
||||||
|
letter-spacing: -0.08em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero p {
|
||||||
|
max-width: 720px;
|
||||||
|
margin: 0;
|
||||||
|
color: var(--muted);
|
||||||
|
font-size: clamp(16px, 2vw, 22px);
|
||||||
|
line-height: 1.55;
|
||||||
|
}
|
||||||
|
|
||||||
|
.eyebrow {
|
||||||
|
color: var(--accent-strong);
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 850;
|
||||||
|
letter-spacing: 0.16em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
min-height: 48px;
|
||||||
|
padding: 0 22px;
|
||||||
|
border: 0;
|
||||||
|
border-radius: 999px;
|
||||||
|
color: #fffaf0;
|
||||||
|
background: linear-gradient(135deg, var(--accent), var(--accent-strong));
|
||||||
|
box-shadow: 0 14px 30px rgba(143, 53, 27, 0.24);
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
button:disabled {
|
||||||
|
cursor: wait;
|
||||||
|
opacity: 0.62;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-card,
|
||||||
|
.card,
|
||||||
|
.panel {
|
||||||
|
border: 1px solid var(--panel-border);
|
||||||
|
background: var(--panel);
|
||||||
|
box-shadow: 0 12px 44px rgba(44, 39, 25, 0.09);
|
||||||
|
backdrop-filter: blur(14px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-card {
|
||||||
|
margin-top: 18px;
|
||||||
|
padding: 18px 20px;
|
||||||
|
border-color: rgba(183, 71, 52, 0.35);
|
||||||
|
border-radius: 22px;
|
||||||
|
color: #7c2518;
|
||||||
|
background: rgba(255, 235, 225, 0.82);
|
||||||
|
font-weight: 800;
|
||||||
|
}
|
||||||
|
|
||||||
|
.metrics-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||||
|
gap: 16px;
|
||||||
|
margin: 22px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.control-rail {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||||
|
gap: 1px;
|
||||||
|
margin: 18px 0 22px;
|
||||||
|
overflow: hidden;
|
||||||
|
border: 1px solid var(--panel-border);
|
||||||
|
border-radius: 28px;
|
||||||
|
background: rgba(43, 55, 38, 0.12);
|
||||||
|
box-shadow: 0 18px 50px rgba(44, 39, 25, 0.08);
|
||||||
|
}
|
||||||
|
|
||||||
|
.control-rail div {
|
||||||
|
display: grid;
|
||||||
|
gap: 8px;
|
||||||
|
min-height: 116px;
|
||||||
|
padding: 20px;
|
||||||
|
background:
|
||||||
|
linear-gradient(
|
||||||
|
145deg,
|
||||||
|
rgba(255, 250, 240, 0.9),
|
||||||
|
rgba(255, 250, 240, 0.68)
|
||||||
|
),
|
||||||
|
radial-gradient(
|
||||||
|
circle at 18% 18%,
|
||||||
|
rgba(191, 95, 44, 0.1),
|
||||||
|
transparent 16rem
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
.control-rail strong {
|
||||||
|
font-size: clamp(22px, 3vw, 38px);
|
||||||
|
line-height: 1;
|
||||||
|
letter-spacing: -0.05em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.control-rail small {
|
||||||
|
color: var(--muted);
|
||||||
|
line-height: 1.35;
|
||||||
|
}
|
||||||
|
|
||||||
|
.metric-card {
|
||||||
|
display: grid;
|
||||||
|
gap: 8px;
|
||||||
|
min-height: 142px;
|
||||||
|
padding: 22px;
|
||||||
|
border-radius: 26px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.metric-card span,
|
||||||
|
.metric-card small,
|
||||||
|
td span,
|
||||||
|
td small,
|
||||||
|
dd,
|
||||||
|
.panel-title span,
|
||||||
|
.usage-title span,
|
||||||
|
.bar-line small {
|
||||||
|
color: var(--muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.metric-card span {
|
||||||
|
font-weight: 850;
|
||||||
|
letter-spacing: 0.08em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
|
||||||
|
.metric-card strong {
|
||||||
|
font-size: clamp(36px, 5vw, 66px);
|
||||||
|
line-height: 0.9;
|
||||||
|
letter-spacing: -0.07em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.panel {
|
||||||
|
margin-top: 18px;
|
||||||
|
padding: 24px;
|
||||||
|
border-radius: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.split-panel {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: minmax(340px, 0.82fr) minmax(0, 1.18fr);
|
||||||
|
gap: 22px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.panel-title {
|
||||||
|
display: flex;
|
||||||
|
gap: 16px;
|
||||||
|
align-items: baseline;
|
||||||
|
justify-content: space-between;
|
||||||
|
margin-bottom: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.panel-title h2 {
|
||||||
|
margin: 0;
|
||||||
|
font-family: 'Georgia', 'Noto Serif KR', serif;
|
||||||
|
font-size: clamp(26px, 3vw, 40px);
|
||||||
|
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);
|
||||||
|
border-radius: 22px;
|
||||||
|
background: rgba(255, 250, 240, 0.56);
|
||||||
|
}
|
||||||
|
|
||||||
|
table {
|
||||||
|
width: 100%;
|
||||||
|
border-collapse: collapse;
|
||||||
|
min-width: 720px;
|
||||||
|
}
|
||||||
|
|
||||||
|
th,
|
||||||
|
td {
|
||||||
|
padding: 16px;
|
||||||
|
border-bottom: 1px solid rgba(43, 55, 38, 0.1);
|
||||||
|
text-align: left;
|
||||||
|
vertical-align: top;
|
||||||
|
}
|
||||||
|
|
||||||
|
th {
|
||||||
|
color: var(--muted);
|
||||||
|
font-size: 12px;
|
||||||
|
letter-spacing: 0.1em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
background: rgba(255, 250, 240, 0.7);
|
||||||
|
}
|
||||||
|
|
||||||
|
tr:last-child td {
|
||||||
|
border-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
td strong,
|
||||||
|
td span,
|
||||||
|
td small {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
td strong {
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
td small {
|
||||||
|
margin-top: 4px;
|
||||||
|
max-width: 480px;
|
||||||
|
overflow-wrap: anywhere;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pill {
|
||||||
|
display: inline-flex;
|
||||||
|
width: fit-content;
|
||||||
|
align-items: center;
|
||||||
|
padding: 5px 10px;
|
||||||
|
border-radius: 999px;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 850;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pill-processing,
|
||||||
|
.pill-active {
|
||||||
|
color: #173e23;
|
||||||
|
background: rgba(63, 127, 81, 0.18);
|
||||||
|
}
|
||||||
|
|
||||||
|
.pill-waiting,
|
||||||
|
.pill-paused {
|
||||||
|
color: #6e4a05;
|
||||||
|
background: rgba(181, 138, 34, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.pill-inactive,
|
||||||
|
.pill-completed {
|
||||||
|
color: #625d52;
|
||||||
|
background: rgba(109, 115, 95, 0.16);
|
||||||
|
}
|
||||||
|
|
||||||
|
.usage-list {
|
||||||
|
display: grid;
|
||||||
|
gap: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.usage-row {
|
||||||
|
padding: 18px;
|
||||||
|
border: 1px solid rgba(43, 55, 38, 0.1);
|
||||||
|
border-radius: 22px;
|
||||||
|
background: rgba(255, 250, 240, 0.62);
|
||||||
|
}
|
||||||
|
|
||||||
|
.usage-title {
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
justify-content: space-between;
|
||||||
|
margin-bottom: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bar-line {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 34px 1fr 54px minmax(60px, auto);
|
||||||
|
gap: 10px;
|
||||||
|
align-items: center;
|
||||||
|
margin-top: 9px;
|
||||||
|
}
|
||||||
|
|
||||||
|
progress {
|
||||||
|
width: 100%;
|
||||||
|
height: 12px;
|
||||||
|
overflow: hidden;
|
||||||
|
border: 0;
|
||||||
|
border-radius: 999px;
|
||||||
|
background: rgba(43, 55, 38, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
progress::-webkit-progress-bar {
|
||||||
|
background: rgba(43, 55, 38, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
progress::-webkit-progress-value {
|
||||||
|
border-radius: 999px;
|
||||||
|
background: linear-gradient(90deg, var(--green), var(--accent));
|
||||||
|
}
|
||||||
|
|
||||||
|
progress::-moz-progress-bar {
|
||||||
|
border-radius: 999px;
|
||||||
|
background: linear-gradient(90deg, var(--green), var(--accent));
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-state {
|
||||||
|
padding: 24px;
|
||||||
|
border: 1px dashed rgba(43, 55, 38, 0.2);
|
||||||
|
border-radius: 22px;
|
||||||
|
color: var(--muted);
|
||||||
|
background: rgba(255, 250, 240, 0.45);
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 980px) {
|
||||||
|
.hero,
|
||||||
|
.split-panel {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero {
|
||||||
|
align-items: start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.metrics-grid {
|
||||||
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||||
|
}
|
||||||
|
|
||||||
|
.control-rail {
|
||||||
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 640px) {
|
||||||
|
.shell {
|
||||||
|
width: min(100% - 24px, 1480px);
|
||||||
|
padding: 18px 0 34px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero,
|
||||||
|
.panel {
|
||||||
|
border-radius: 22px;
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.metrics-grid {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.control-rail {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.panel-title,
|
||||||
|
.usage-title {
|
||||||
|
display: grid;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bar-line {
|
||||||
|
grid-template-columns: 28px 1fr 44px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bar-line small {
|
||||||
|
grid-column: 2 / -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
1
apps/dashboard/src/vite-env.d.ts
vendored
Normal file
1
apps/dashboard/src/vite-env.d.ts
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
/// <reference types="vite/client" />
|
||||||
21
apps/dashboard/tsconfig.json
Normal file
21
apps/dashboard/tsconfig.json
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ES2022",
|
||||||
|
"useDefineForClassFields": true,
|
||||||
|
"lib": ["DOM", "DOM.Iterable", "ES2022"],
|
||||||
|
"allowJs": false,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"allowSyntheticDefaultImports": true,
|
||||||
|
"strict": true,
|
||||||
|
"forceConsistentCasingInFileNames": true,
|
||||||
|
"module": "ESNext",
|
||||||
|
"moduleResolution": "Bundler",
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"isolatedModules": true,
|
||||||
|
"noEmit": true,
|
||||||
|
"jsx": "react-jsx"
|
||||||
|
},
|
||||||
|
"include": ["src"],
|
||||||
|
"references": []
|
||||||
|
}
|
||||||
18
apps/dashboard/vite.config.ts
Normal file
18
apps/dashboard/vite.config.ts
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
import react from '@vitejs/plugin-react';
|
||||||
|
import { defineConfig } from 'vite';
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
root: __dirname,
|
||||||
|
plugins: [react()],
|
||||||
|
build: {
|
||||||
|
outDir: 'dist',
|
||||||
|
emptyOutDir: true,
|
||||||
|
},
|
||||||
|
server: {
|
||||||
|
host: '127.0.0.1',
|
||||||
|
port: 5174,
|
||||||
|
proxy: {
|
||||||
|
'/api': 'http://127.0.0.1:8734',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
108
bun.lock
108
bun.lock
@@ -11,6 +11,8 @@
|
|||||||
"ejclaw-runners-shared": "file:./runners/shared",
|
"ejclaw-runners-shared": "file:./runners/shared",
|
||||||
"pino": "^9.6.0",
|
"pino": "^9.6.0",
|
||||||
"pino-pretty": "^13.0.0",
|
"pino-pretty": "^13.0.0",
|
||||||
|
"react": "^19.2.5",
|
||||||
|
"react-dom": "^19.2.5",
|
||||||
"yaml": "^2.8.2",
|
"yaml": "^2.8.2",
|
||||||
"zod": "^4.3.6",
|
"zod": "^4.3.6",
|
||||||
},
|
},
|
||||||
@@ -18,11 +20,15 @@
|
|||||||
"@types/better-sqlite3": "^7.6.13",
|
"@types/better-sqlite3": "^7.6.13",
|
||||||
"@types/bun": "^1.3.11",
|
"@types/bun": "^1.3.11",
|
||||||
"@types/node": "^22.10.0",
|
"@types/node": "^22.10.0",
|
||||||
|
"@types/react": "^19.2.14",
|
||||||
|
"@types/react-dom": "^19.2.3",
|
||||||
|
"@vitejs/plugin-react": "^6.0.1",
|
||||||
"@vitest/coverage-v8": "^4.0.18",
|
"@vitest/coverage-v8": "^4.0.18",
|
||||||
"better-sqlite3": "^12.8.0",
|
"better-sqlite3": "^12.8.0",
|
||||||
"husky": "^9.1.7",
|
"husky": "^9.1.7",
|
||||||
"prettier": "^3.8.1",
|
"prettier": "^3.8.1",
|
||||||
"typescript": "^5.7.0",
|
"typescript": "^5.7.0",
|
||||||
|
"vite": "^8.0.10",
|
||||||
"vitest": "^4.0.18",
|
"vitest": "^4.0.18",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -50,6 +56,12 @@
|
|||||||
|
|
||||||
"@discordjs/ws": ["@discordjs/ws@1.2.3", "", { "dependencies": { "@discordjs/collection": "^2.1.0", "@discordjs/rest": "^2.5.1", "@discordjs/util": "^1.1.0", "@sapphire/async-queue": "^1.5.2", "@types/ws": "^8.5.10", "@vladfrangu/async_event_emitter": "^2.2.4", "discord-api-types": "^0.38.1", "tslib": "^2.6.2", "ws": "^8.17.0" } }, "sha512-wPlQDxEmlDg5IxhJPuxXr3Vy9AjYq5xCvFWGJyD7w7Np8ZGu+Mc+97LCoEc/+AYCo2IDpKioiH0/c/mj5ZR9Uw=="],
|
"@discordjs/ws": ["@discordjs/ws@1.2.3", "", { "dependencies": { "@discordjs/collection": "^2.1.0", "@discordjs/rest": "^2.5.1", "@discordjs/util": "^1.1.0", "@sapphire/async-queue": "^1.5.2", "@types/ws": "^8.5.10", "@vladfrangu/async_event_emitter": "^2.2.4", "discord-api-types": "^0.38.1", "tslib": "^2.6.2", "ws": "^8.17.0" } }, "sha512-wPlQDxEmlDg5IxhJPuxXr3Vy9AjYq5xCvFWGJyD7w7Np8ZGu+Mc+97LCoEc/+AYCo2IDpKioiH0/c/mj5ZR9Uw=="],
|
||||||
|
|
||||||
|
"@emnapi/core": ["@emnapi/core@1.10.0", "", { "dependencies": { "@emnapi/wasi-threads": "1.2.1", "tslib": "^2.4.0" } }, "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw=="],
|
||||||
|
|
||||||
|
"@emnapi/runtime": ["@emnapi/runtime@1.10.0", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA=="],
|
||||||
|
|
||||||
|
"@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.2.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w=="],
|
||||||
|
|
||||||
"@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.27.3", "", { "os": "aix", "cpu": "ppc64" }, "sha512-9fJMTNFTWZMh5qwrBItuziu834eOCUcEqymSH7pY+zoMVEZg3gcPuBNxH1EvfVYe9h0x/Ptw8KBzv7qxb7l8dg=="],
|
"@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.27.3", "", { "os": "aix", "cpu": "ppc64" }, "sha512-9fJMTNFTWZMh5qwrBItuziu834eOCUcEqymSH7pY+zoMVEZg3gcPuBNxH1EvfVYe9h0x/Ptw8KBzv7qxb7l8dg=="],
|
||||||
|
|
||||||
"@esbuild/android-arm": ["@esbuild/android-arm@0.27.3", "", { "os": "android", "cpu": "arm" }, "sha512-i5D1hPY7GIQmXlXhs2w8AWHhenb00+GxjxRncS2ZM7YNVGNfaMxgzSGuO8o8SJzRc/oZwU2bcScvVERk03QhzA=="],
|
"@esbuild/android-arm": ["@esbuild/android-arm@0.27.3", "", { "os": "android", "cpu": "arm" }, "sha512-i5D1hPY7GIQmXlXhs2w8AWHhenb00+GxjxRncS2ZM7YNVGNfaMxgzSGuO8o8SJzRc/oZwU2bcScvVERk03QhzA=="],
|
||||||
@@ -112,8 +124,44 @@
|
|||||||
|
|
||||||
"@modelcontextprotocol/sdk": ["@modelcontextprotocol/sdk@1.27.1", "", { "dependencies": { "@hono/node-server": "^1.19.9", "ajv": "^8.17.1", "ajv-formats": "^3.0.1", "content-type": "^1.0.5", "cors": "^2.8.5", "cross-spawn": "^7.0.5", "eventsource": "^3.0.2", "eventsource-parser": "^3.0.0", "express": "^5.2.1", "express-rate-limit": "^8.2.1", "hono": "^4.11.4", "jose": "^6.1.3", "json-schema-typed": "^8.0.2", "pkce-challenge": "^5.0.0", "raw-body": "^3.0.0", "zod": "^3.25 || ^4.0", "zod-to-json-schema": "^3.25.1" }, "peerDependencies": { "@cfworker/json-schema": "^4.1.1", "zod": "^3.25 || ^4.0" }, "optionalPeers": ["@cfworker/json-schema"] }, "sha512-sr6GbP+4edBwFndLbM60gf07z0FQ79gaExpnsjMGePXqFcSSb7t6iscpjk9DhFhwd+mTEQrzNafGP8/iGGFYaA=="],
|
"@modelcontextprotocol/sdk": ["@modelcontextprotocol/sdk@1.27.1", "", { "dependencies": { "@hono/node-server": "^1.19.9", "ajv": "^8.17.1", "ajv-formats": "^3.0.1", "content-type": "^1.0.5", "cors": "^2.8.5", "cross-spawn": "^7.0.5", "eventsource": "^3.0.2", "eventsource-parser": "^3.0.0", "express": "^5.2.1", "express-rate-limit": "^8.2.1", "hono": "^4.11.4", "jose": "^6.1.3", "json-schema-typed": "^8.0.2", "pkce-challenge": "^5.0.0", "raw-body": "^3.0.0", "zod": "^3.25 || ^4.0", "zod-to-json-schema": "^3.25.1" }, "peerDependencies": { "@cfworker/json-schema": "^4.1.1", "zod": "^3.25 || ^4.0" }, "optionalPeers": ["@cfworker/json-schema"] }, "sha512-sr6GbP+4edBwFndLbM60gf07z0FQ79gaExpnsjMGePXqFcSSb7t6iscpjk9DhFhwd+mTEQrzNafGP8/iGGFYaA=="],
|
||||||
|
|
||||||
|
"@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@1.1.4", "", { "dependencies": { "@tybys/wasm-util": "^0.10.1" }, "peerDependencies": { "@emnapi/core": "^1.7.1", "@emnapi/runtime": "^1.7.1" } }, "sha512-3NQNNgA1YSlJb/kMH1ildASP9HW7/7kYnRI2szWJaofaS1hWmbGI4H+d3+22aGzXXN9IJ+n+GiFVcGipJP18ow=="],
|
||||||
|
|
||||||
|
"@oxc-project/types": ["@oxc-project/types@0.127.0", "", {}, "sha512-aIYXQBo4lCbO4z0R3FHeucQHpF46l2LbMdxRvqvuRuW2OxdnSkcng5B8+K12spgLDj93rtN3+J2Vac/TIO+ciQ=="],
|
||||||
|
|
||||||
"@pinojs/redact": ["@pinojs/redact@0.4.0", "", {}, "sha512-k2ENnmBugE/rzQfEcdWHcCY+/FM3VLzH9cYEsbdsoqrvzAKRhUZeRNhAZvB8OitQJ1TBed3yqWtdjzS6wJKBwg=="],
|
"@pinojs/redact": ["@pinojs/redact@0.4.0", "", {}, "sha512-k2ENnmBugE/rzQfEcdWHcCY+/FM3VLzH9cYEsbdsoqrvzAKRhUZeRNhAZvB8OitQJ1TBed3yqWtdjzS6wJKBwg=="],
|
||||||
|
|
||||||
|
"@rolldown/binding-android-arm64": ["@rolldown/binding-android-arm64@1.0.0-rc.17", "", { "os": "android", "cpu": "arm64" }, "sha512-s70pVGhw4zqGeFnXWvAzJDlvxhlRollagdCCKRgOsgUOH3N1l0LIxf83AtGzmb5SiVM4Hjl5HyarMRfdfj3DaQ=="],
|
||||||
|
|
||||||
|
"@rolldown/binding-darwin-arm64": ["@rolldown/binding-darwin-arm64@1.0.0-rc.17", "", { "os": "darwin", "cpu": "arm64" }, "sha512-4ksWc9n0mhlZpZ9PMZgTGjeOPRu8MB1Z3Tz0Mo02eWfWCHMW1zN82Qz/pL/rC+yQa+8ZnutMF0JjJe7PjwasYw=="],
|
||||||
|
|
||||||
|
"@rolldown/binding-darwin-x64": ["@rolldown/binding-darwin-x64@1.0.0-rc.17", "", { "os": "darwin", "cpu": "x64" }, "sha512-SUSDOI6WwUVNcWxd02QEBjLdY1VPHvlEkw6T/8nYG322iYWCTxRb1vzk4E+mWWYehTp7ERibq54LSJGjmouOsw=="],
|
||||||
|
|
||||||
|
"@rolldown/binding-freebsd-x64": ["@rolldown/binding-freebsd-x64@1.0.0-rc.17", "", { "os": "freebsd", "cpu": "x64" }, "sha512-hwnz3nw9dbJ05EDO/PvcjaaewqqDy7Y1rn1UO81l8iIK1GjenME75dl16ajbvSSMfv66WXSRCYKIqfgq2KCfxw=="],
|
||||||
|
|
||||||
|
"@rolldown/binding-linux-arm-gnueabihf": ["@rolldown/binding-linux-arm-gnueabihf@1.0.0-rc.17", "", { "os": "linux", "cpu": "arm" }, "sha512-IS+W7epTcwANmFSQFrS1SivEXHtl1JtuQA9wlxrZTcNi6mx+FDOYrakGevvvTwgj2JvWiK8B29/qD9BELZPyXQ=="],
|
||||||
|
|
||||||
|
"@rolldown/binding-linux-arm64-gnu": ["@rolldown/binding-linux-arm64-gnu@1.0.0-rc.17", "", { "os": "linux", "cpu": "arm64" }, "sha512-e6usGaHKW5BMNZOymS1UcEYGowQMWcgZ71Z17Sl/h2+ZziNJ1a9n3Zvcz6LdRyIW5572wBCTH/Z+bKuZouGk9Q=="],
|
||||||
|
|
||||||
|
"@rolldown/binding-linux-arm64-musl": ["@rolldown/binding-linux-arm64-musl@1.0.0-rc.17", "", { "os": "linux", "cpu": "arm64" }, "sha512-b/CgbwAJpmrRLp02RPfhbudf5tZnN9nsPWK82znefso832etkem8H7FSZwxrOI9djcdTP7U6YfNhbRnh7djErg=="],
|
||||||
|
|
||||||
|
"@rolldown/binding-linux-ppc64-gnu": ["@rolldown/binding-linux-ppc64-gnu@1.0.0-rc.17", "", { "os": "linux", "cpu": "ppc64" }, "sha512-4EII1iNGRUN5WwGbF/kOh/EIkoDN9HsupgLQoXfY+D1oyJm7/F4t5PYU5n8SWZgG0FEwakyM8pGgwcBYruGTlA=="],
|
||||||
|
|
||||||
|
"@rolldown/binding-linux-s390x-gnu": ["@rolldown/binding-linux-s390x-gnu@1.0.0-rc.17", "", { "os": "linux", "cpu": "s390x" }, "sha512-AH8oq3XqQo4IibpVXvPeLDI5pzkpYn0WiZAfT05kFzoJ6tQNzwRdDYQ45M8I/gslbodRZwW8uxLhbSBbkv96rA=="],
|
||||||
|
|
||||||
|
"@rolldown/binding-linux-x64-gnu": ["@rolldown/binding-linux-x64-gnu@1.0.0-rc.17", "", { "os": "linux", "cpu": "x64" }, "sha512-cLnjV3xfo7KslbU41Z7z8BH/E1y5mzUYzAqih1d1MDaIGZRCMqTijqLv76/P7fyHuvUcfGsIpqCdddbxLLK9rA=="],
|
||||||
|
|
||||||
|
"@rolldown/binding-linux-x64-musl": ["@rolldown/binding-linux-x64-musl@1.0.0-rc.17", "", { "os": "linux", "cpu": "x64" }, "sha512-0phclDw1spsL7dUB37sIARuis2tAgomCJXAHZlpt8PXZ4Ba0dRP1e+66lsRqrfhISeN9bEGNjQs+T/Fbd7oYGw=="],
|
||||||
|
|
||||||
|
"@rolldown/binding-openharmony-arm64": ["@rolldown/binding-openharmony-arm64@1.0.0-rc.17", "", { "os": "none", "cpu": "arm64" }, "sha512-0ag/hEgXOwgw4t8QyQvUCxvEg+V0KBcA6YuOx9g0r02MprutRF5dyljgm3EmR02O292UX7UeS6HzWHAl6KgyhA=="],
|
||||||
|
|
||||||
|
"@rolldown/binding-wasm32-wasi": ["@rolldown/binding-wasm32-wasi@1.0.0-rc.17", "", { "dependencies": { "@emnapi/core": "1.10.0", "@emnapi/runtime": "1.10.0", "@napi-rs/wasm-runtime": "^1.1.4" }, "cpu": "none" }, "sha512-LEXei6vo0E5wTGwpkJ4KoT3OZJRnglwldt5ziLzOlc6qqb55z4tWNq2A+PFqCJuvWWdP53CVhG1Z9NtToDPJrA=="],
|
||||||
|
|
||||||
|
"@rolldown/binding-win32-arm64-msvc": ["@rolldown/binding-win32-arm64-msvc@1.0.0-rc.17", "", { "os": "win32", "cpu": "arm64" }, "sha512-gUmyzBl3SPMa6hrqFUth9sVfcLBlYsbMzBx5PlexMroZStgzGqlZ26pYG89rBb45Mnia+oil6YAIFeEWGWhoZA=="],
|
||||||
|
|
||||||
|
"@rolldown/binding-win32-x64-msvc": ["@rolldown/binding-win32-x64-msvc@1.0.0-rc.17", "", { "os": "win32", "cpu": "x64" }, "sha512-3hkiolcUAvPB9FLb3UZdfjVVNWherN1f/skkGWJP/fgSQhYUZpSIRr0/I8ZK9TkF3F7kxvJAk0+IcKvPHk9qQg=="],
|
||||||
|
|
||||||
|
"@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-rc.7", "", {}, "sha512-qujRfC8sFVInYSPPMLQByRh7zhwkGFS4+tyMQ83srV1qrxL4g8E2tyxVVyxd0+8QeBM1mIk9KbWxkegRr76XzA=="],
|
||||||
|
|
||||||
"@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.57.1", "", { "os": "android", "cpu": "arm" }, "sha512-A6ehUVSiSaaliTxai040ZpZ2zTevHYbvu/lDoeAteHI8QnaosIzm4qwtezfRg1jOYaUmnzLX1AOD6Z+UJjtifg=="],
|
"@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.57.1", "", { "os": "android", "cpu": "arm" }, "sha512-A6ehUVSiSaaliTxai040ZpZ2zTevHYbvu/lDoeAteHI8QnaosIzm4qwtezfRg1jOYaUmnzLX1AOD6Z+UJjtifg=="],
|
||||||
|
|
||||||
"@rollup/rollup-android-arm64": ["@rollup/rollup-android-arm64@4.57.1", "", { "os": "android", "cpu": "arm64" }, "sha512-dQaAddCY9YgkFHZcFNS/606Exo8vcLHwArFZ7vxXq4rigo2bb494/xKMMwRRQW6ug7Js6yXmBZhSBRuBvCCQ3w=="],
|
"@rollup/rollup-android-arm64": ["@rollup/rollup-android-arm64@4.57.1", "", { "os": "android", "cpu": "arm64" }, "sha512-dQaAddCY9YgkFHZcFNS/606Exo8vcLHwArFZ7vxXq4rigo2bb494/xKMMwRRQW6ug7Js6yXmBZhSBRuBvCCQ3w=="],
|
||||||
@@ -172,6 +220,8 @@
|
|||||||
|
|
||||||
"@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="],
|
"@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="],
|
||||||
|
|
||||||
|
"@tybys/wasm-util": ["@tybys/wasm-util@0.10.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg=="],
|
||||||
|
|
||||||
"@types/better-sqlite3": ["@types/better-sqlite3@7.6.13", "", { "dependencies": { "@types/node": "*" } }, "sha512-NMv9ASNARoKksWtsq/SHakpYAYnhBrQgGD8zkLYk/jaK8jUGn08CfEdTRgYhMypUQAfzSP8W6gNLe0q19/t4VA=="],
|
"@types/better-sqlite3": ["@types/better-sqlite3@7.6.13", "", { "dependencies": { "@types/node": "*" } }, "sha512-NMv9ASNARoKksWtsq/SHakpYAYnhBrQgGD8zkLYk/jaK8jUGn08CfEdTRgYhMypUQAfzSP8W6gNLe0q19/t4VA=="],
|
||||||
|
|
||||||
"@types/bun": ["@types/bun@1.3.11", "", { "dependencies": { "bun-types": "1.3.11" } }, "sha512-5vPne5QvtpjGpsGYXiFyycfpDF2ECyPcTSsFBMa0fraoxiQyMJ3SmuQIGhzPg2WJuWxVBoxWJ2kClYTcw/4fAg=="],
|
"@types/bun": ["@types/bun@1.3.11", "", { "dependencies": { "bun-types": "1.3.11" } }, "sha512-5vPne5QvtpjGpsGYXiFyycfpDF2ECyPcTSsFBMa0fraoxiQyMJ3SmuQIGhzPg2WJuWxVBoxWJ2kClYTcw/4fAg=="],
|
||||||
@@ -184,8 +234,14 @@
|
|||||||
|
|
||||||
"@types/node": ["@types/node@22.19.11", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-BH7YwL6rA93ReqeQS1c4bsPpcfOmJasG+Fkr6Y59q83f9M1WcBRHR2vM+P9eOisYRcN3ujQoiZY8uk5W+1WL8w=="],
|
"@types/node": ["@types/node@22.19.11", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-BH7YwL6rA93ReqeQS1c4bsPpcfOmJasG+Fkr6Y59q83f9M1WcBRHR2vM+P9eOisYRcN3ujQoiZY8uk5W+1WL8w=="],
|
||||||
|
|
||||||
|
"@types/react": ["@types/react@19.2.14", "", { "dependencies": { "csstype": "^3.2.2" } }, "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w=="],
|
||||||
|
|
||||||
|
"@types/react-dom": ["@types/react-dom@19.2.3", "", { "peerDependencies": { "@types/react": "^19.2.0" } }, "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ=="],
|
||||||
|
|
||||||
"@types/ws": ["@types/ws@8.18.1", "", { "dependencies": { "@types/node": "*" } }, "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg=="],
|
"@types/ws": ["@types/ws@8.18.1", "", { "dependencies": { "@types/node": "*" } }, "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg=="],
|
||||||
|
|
||||||
|
"@vitejs/plugin-react": ["@vitejs/plugin-react@6.0.1", "", { "dependencies": { "@rolldown/pluginutils": "1.0.0-rc.7" }, "peerDependencies": { "@rolldown/plugin-babel": "^0.1.7 || ^0.2.0", "babel-plugin-react-compiler": "^1.0.0", "vite": "^8.0.0" }, "optionalPeers": ["@rolldown/plugin-babel", "babel-plugin-react-compiler"] }, "sha512-l9X/E3cDb+xY3SWzlG1MOGt2usfEHGMNIaegaUGFsLkb3RCn/k8/TOXBcab+OndDI4TBtktT8/9BwwW8Vi9KUQ=="],
|
||||||
|
|
||||||
"@vitest/coverage-v8": ["@vitest/coverage-v8@4.0.18", "", { "dependencies": { "@bcoe/v8-coverage": "^1.0.2", "@vitest/utils": "4.0.18", "ast-v8-to-istanbul": "^0.3.10", "istanbul-lib-coverage": "^3.2.2", "istanbul-lib-report": "^3.0.1", "istanbul-reports": "^3.2.0", "magicast": "^0.5.1", "obug": "^2.1.1", "std-env": "^3.10.0", "tinyrainbow": "^3.0.3" }, "peerDependencies": { "@vitest/browser": "4.0.18", "vitest": "4.0.18" }, "optionalPeers": ["@vitest/browser"] }, "sha512-7i+N2i0+ME+2JFZhfuz7Tg/FqKtilHjGyGvoHYQ6iLV0zahbsJ9sljC9OcFcPDbhYKCet+sG8SsVqlyGvPflZg=="],
|
"@vitest/coverage-v8": ["@vitest/coverage-v8@4.0.18", "", { "dependencies": { "@bcoe/v8-coverage": "^1.0.2", "@vitest/utils": "4.0.18", "ast-v8-to-istanbul": "^0.3.10", "istanbul-lib-coverage": "^3.2.2", "istanbul-lib-report": "^3.0.1", "istanbul-reports": "^3.2.0", "magicast": "^0.5.1", "obug": "^2.1.1", "std-env": "^3.10.0", "tinyrainbow": "^3.0.3" }, "peerDependencies": { "@vitest/browser": "4.0.18", "vitest": "4.0.18" }, "optionalPeers": ["@vitest/browser"] }, "sha512-7i+N2i0+ME+2JFZhfuz7Tg/FqKtilHjGyGvoHYQ6iLV0zahbsJ9sljC9OcFcPDbhYKCet+sG8SsVqlyGvPflZg=="],
|
||||||
|
|
||||||
"@vitest/expect": ["@vitest/expect@4.0.18", "", { "dependencies": { "@standard-schema/spec": "^1.0.0", "@types/chai": "^5.2.2", "@vitest/spy": "4.0.18", "@vitest/utils": "4.0.18", "chai": "^6.2.1", "tinyrainbow": "^3.0.3" } }, "sha512-8sCWUyckXXYvx4opfzVY03EOiYVxyNrHS5QxX3DAIi5dpJAAkyJezHCP77VMX4HKA2LDT/Jpfo8i2r5BE3GnQQ=="],
|
"@vitest/expect": ["@vitest/expect@4.0.18", "", { "dependencies": { "@standard-schema/spec": "^1.0.0", "@types/chai": "^5.2.2", "@vitest/spy": "4.0.18", "@vitest/utils": "4.0.18", "chai": "^6.2.1", "tinyrainbow": "^3.0.3" } }, "sha512-8sCWUyckXXYvx4opfzVY03EOiYVxyNrHS5QxX3DAIi5dpJAAkyJezHCP77VMX4HKA2LDT/Jpfo8i2r5BE3GnQQ=="],
|
||||||
@@ -256,6 +312,8 @@
|
|||||||
|
|
||||||
"cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="],
|
"cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="],
|
||||||
|
|
||||||
|
"csstype": ["csstype@3.2.3", "", {}, "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ=="],
|
||||||
|
|
||||||
"dateformat": ["dateformat@4.6.3", "", {}, "sha512-2P0p0pFGzHS5EMnhdxQi7aJN+iMheud0UhG4dlE1DLAlvL8JHjJJTX/CSm4JXwV0Ka5nGk3zC5mcb5bUQUxxMA=="],
|
"dateformat": ["dateformat@4.6.3", "", {}, "sha512-2P0p0pFGzHS5EMnhdxQi7aJN+iMheud0UhG4dlE1DLAlvL8JHjJJTX/CSm4JXwV0Ka5nGk3zC5mcb5bUQUxxMA=="],
|
||||||
|
|
||||||
"debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="],
|
"debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="],
|
||||||
@@ -392,6 +450,30 @@
|
|||||||
|
|
||||||
"json-schema-typed": ["json-schema-typed@8.0.2", "", {}, "sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA=="],
|
"json-schema-typed": ["json-schema-typed@8.0.2", "", {}, "sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA=="],
|
||||||
|
|
||||||
|
"lightningcss": ["lightningcss@1.32.0", "", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-android-arm64": "1.32.0", "lightningcss-darwin-arm64": "1.32.0", "lightningcss-darwin-x64": "1.32.0", "lightningcss-freebsd-x64": "1.32.0", "lightningcss-linux-arm-gnueabihf": "1.32.0", "lightningcss-linux-arm64-gnu": "1.32.0", "lightningcss-linux-arm64-musl": "1.32.0", "lightningcss-linux-x64-gnu": "1.32.0", "lightningcss-linux-x64-musl": "1.32.0", "lightningcss-win32-arm64-msvc": "1.32.0", "lightningcss-win32-x64-msvc": "1.32.0" } }, "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ=="],
|
||||||
|
|
||||||
|
"lightningcss-android-arm64": ["lightningcss-android-arm64@1.32.0", "", { "os": "android", "cpu": "arm64" }, "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg=="],
|
||||||
|
|
||||||
|
"lightningcss-darwin-arm64": ["lightningcss-darwin-arm64@1.32.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ=="],
|
||||||
|
|
||||||
|
"lightningcss-darwin-x64": ["lightningcss-darwin-x64@1.32.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w=="],
|
||||||
|
|
||||||
|
"lightningcss-freebsd-x64": ["lightningcss-freebsd-x64@1.32.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig=="],
|
||||||
|
|
||||||
|
"lightningcss-linux-arm-gnueabihf": ["lightningcss-linux-arm-gnueabihf@1.32.0", "", { "os": "linux", "cpu": "arm" }, "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw=="],
|
||||||
|
|
||||||
|
"lightningcss-linux-arm64-gnu": ["lightningcss-linux-arm64-gnu@1.32.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ=="],
|
||||||
|
|
||||||
|
"lightningcss-linux-arm64-musl": ["lightningcss-linux-arm64-musl@1.32.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg=="],
|
||||||
|
|
||||||
|
"lightningcss-linux-x64-gnu": ["lightningcss-linux-x64-gnu@1.32.0", "", { "os": "linux", "cpu": "x64" }, "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA=="],
|
||||||
|
|
||||||
|
"lightningcss-linux-x64-musl": ["lightningcss-linux-x64-musl@1.32.0", "", { "os": "linux", "cpu": "x64" }, "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg=="],
|
||||||
|
|
||||||
|
"lightningcss-win32-arm64-msvc": ["lightningcss-win32-arm64-msvc@1.32.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw=="],
|
||||||
|
|
||||||
|
"lightningcss-win32-x64-msvc": ["lightningcss-win32-x64-msvc@1.32.0", "", { "os": "win32", "cpu": "x64" }, "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q=="],
|
||||||
|
|
||||||
"lodash": ["lodash@4.17.23", "", {}, "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w=="],
|
"lodash": ["lodash@4.17.23", "", {}, "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w=="],
|
||||||
|
|
||||||
"lodash.snakecase": ["lodash.snakecase@4.1.1", "", {}, "sha512-QZ1d4xoBHYUeuouhEq3lk3Uq7ldgyFXGBhg04+oRLnIz8o9T65Eh+8YdroUwn846zchkA9yDsDl5CVVaV2nqYw=="],
|
"lodash.snakecase": ["lodash.snakecase@4.1.1", "", {}, "sha512-QZ1d4xoBHYUeuouhEq3lk3Uq7ldgyFXGBhg04+oRLnIz8o9T65Eh+8YdroUwn846zchkA9yDsDl5CVVaV2nqYw=="],
|
||||||
@@ -454,7 +536,7 @@
|
|||||||
|
|
||||||
"picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="],
|
"picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="],
|
||||||
|
|
||||||
"picomatch": ["picomatch@4.0.3", "", {}, "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q=="],
|
"picomatch": ["picomatch@4.0.4", "", {}, "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A=="],
|
||||||
|
|
||||||
"pino": ["pino@9.14.0", "", { "dependencies": { "@pinojs/redact": "^0.4.0", "atomic-sleep": "^1.0.0", "on-exit-leak-free": "^2.1.0", "pino-abstract-transport": "^2.0.0", "pino-std-serializers": "^7.0.0", "process-warning": "^5.0.0", "quick-format-unescaped": "^4.0.3", "real-require": "^0.2.0", "safe-stable-stringify": "^2.3.1", "sonic-boom": "^4.0.1", "thread-stream": "^3.0.0" }, "bin": "bin.js" }, "sha512-8OEwKp5juEvb/MjpIc4hjqfgCNysrS94RIOMXYvpYCdm/jglrKEiAYmiumbmGhCvs+IcInsphYDFwqrjr7398w=="],
|
"pino": ["pino@9.14.0", "", { "dependencies": { "@pinojs/redact": "^0.4.0", "atomic-sleep": "^1.0.0", "on-exit-leak-free": "^2.1.0", "pino-abstract-transport": "^2.0.0", "pino-std-serializers": "^7.0.0", "process-warning": "^5.0.0", "quick-format-unescaped": "^4.0.3", "real-require": "^0.2.0", "safe-stable-stringify": "^2.3.1", "sonic-boom": "^4.0.1", "thread-stream": "^3.0.0" }, "bin": "bin.js" }, "sha512-8OEwKp5juEvb/MjpIc4hjqfgCNysrS94RIOMXYvpYCdm/jglrKEiAYmiumbmGhCvs+IcInsphYDFwqrjr7398w=="],
|
||||||
|
|
||||||
@@ -466,7 +548,7 @@
|
|||||||
|
|
||||||
"pkce-challenge": ["pkce-challenge@5.0.1", "", {}, "sha512-wQ0b/W4Fr01qtpHlqSqspcj3EhBvimsdh0KlHhH8HRZnMsEa0ea2fTULOXOS9ccQr3om+GcGRk4e+isrZWV8qQ=="],
|
"pkce-challenge": ["pkce-challenge@5.0.1", "", {}, "sha512-wQ0b/W4Fr01qtpHlqSqspcj3EhBvimsdh0KlHhH8HRZnMsEa0ea2fTULOXOS9ccQr3om+GcGRk4e+isrZWV8qQ=="],
|
||||||
|
|
||||||
"postcss": ["postcss@8.5.6", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg=="],
|
"postcss": ["postcss@8.5.10", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-pMMHxBOZKFU6HgAZ4eyGnwXF/EvPGGqUr0MnZ5+99485wwW41kW91A4LOGxSHhgugZmSChL5AlElNdwlNgcnLQ=="],
|
||||||
|
|
||||||
"prebuild-install": ["prebuild-install@7.1.3", "", { "dependencies": { "detect-libc": "^2.0.0", "expand-template": "^2.0.3", "github-from-package": "0.0.0", "minimist": "^1.2.3", "mkdirp-classic": "^0.5.3", "napi-build-utils": "^2.0.0", "node-abi": "^3.3.0", "pump": "^3.0.0", "rc": "^1.2.7", "simple-get": "^4.0.0", "tar-fs": "^2.0.0", "tunnel-agent": "^0.6.0" }, "bin": { "prebuild-install": "bin.js" } }, "sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug=="],
|
"prebuild-install": ["prebuild-install@7.1.3", "", { "dependencies": { "detect-libc": "^2.0.0", "expand-template": "^2.0.3", "github-from-package": "0.0.0", "minimist": "^1.2.3", "mkdirp-classic": "^0.5.3", "napi-build-utils": "^2.0.0", "node-abi": "^3.3.0", "pump": "^3.0.0", "rc": "^1.2.7", "simple-get": "^4.0.0", "tar-fs": "^2.0.0", "tunnel-agent": "^0.6.0" }, "bin": { "prebuild-install": "bin.js" } }, "sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug=="],
|
||||||
|
|
||||||
@@ -488,6 +570,10 @@
|
|||||||
|
|
||||||
"rc": ["rc@1.2.8", "", { "dependencies": { "deep-extend": "^0.6.0", "ini": "~1.3.0", "minimist": "^1.2.0", "strip-json-comments": "~2.0.1" }, "bin": { "rc": "./cli.js" } }, "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw=="],
|
"rc": ["rc@1.2.8", "", { "dependencies": { "deep-extend": "^0.6.0", "ini": "~1.3.0", "minimist": "^1.2.0", "strip-json-comments": "~2.0.1" }, "bin": { "rc": "./cli.js" } }, "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw=="],
|
||||||
|
|
||||||
|
"react": ["react@19.2.5", "", {}, "sha512-llUJLzz1zTUBrskt2pwZgLq59AemifIftw4aB7JxOqf1HY2FDaGDxgwpAPVzHU1kdWabH7FauP4i1oEeer2WCA=="],
|
||||||
|
|
||||||
|
"react-dom": ["react-dom@19.2.5", "", { "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { "react": "^19.2.5" } }, "sha512-J5bAZz+DXMMwW/wV3xzKke59Af6CHY7G4uYLN1OvBcKEsWOs4pQExj86BBKamxl/Ik5bx9whOrvBlSDfWzgSag=="],
|
||||||
|
|
||||||
"readable-stream": ["readable-stream@3.6.2", "", { "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", "util-deprecate": "^1.0.1" } }, "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA=="],
|
"readable-stream": ["readable-stream@3.6.2", "", { "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", "util-deprecate": "^1.0.1" } }, "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA=="],
|
||||||
|
|
||||||
"real-require": ["real-require@0.2.0", "", {}, "sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg=="],
|
"real-require": ["real-require@0.2.0", "", {}, "sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg=="],
|
||||||
@@ -496,6 +582,8 @@
|
|||||||
|
|
||||||
"resolve-pkg-maps": ["resolve-pkg-maps@1.0.0", "", {}, "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw=="],
|
"resolve-pkg-maps": ["resolve-pkg-maps@1.0.0", "", {}, "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw=="],
|
||||||
|
|
||||||
|
"rolldown": ["rolldown@1.0.0-rc.17", "", { "dependencies": { "@oxc-project/types": "=0.127.0", "@rolldown/pluginutils": "1.0.0-rc.17" }, "optionalDependencies": { "@rolldown/binding-android-arm64": "1.0.0-rc.17", "@rolldown/binding-darwin-arm64": "1.0.0-rc.17", "@rolldown/binding-darwin-x64": "1.0.0-rc.17", "@rolldown/binding-freebsd-x64": "1.0.0-rc.17", "@rolldown/binding-linux-arm-gnueabihf": "1.0.0-rc.17", "@rolldown/binding-linux-arm64-gnu": "1.0.0-rc.17", "@rolldown/binding-linux-arm64-musl": "1.0.0-rc.17", "@rolldown/binding-linux-ppc64-gnu": "1.0.0-rc.17", "@rolldown/binding-linux-s390x-gnu": "1.0.0-rc.17", "@rolldown/binding-linux-x64-gnu": "1.0.0-rc.17", "@rolldown/binding-linux-x64-musl": "1.0.0-rc.17", "@rolldown/binding-openharmony-arm64": "1.0.0-rc.17", "@rolldown/binding-wasm32-wasi": "1.0.0-rc.17", "@rolldown/binding-win32-arm64-msvc": "1.0.0-rc.17", "@rolldown/binding-win32-x64-msvc": "1.0.0-rc.17" }, "bin": { "rolldown": "bin/cli.mjs" } }, "sha512-ZrT53oAKrtA4+YtBWPQbtPOxIbVDbxT0orcYERKd63VJTF13zPcgXTvD4843L8pcsI7M6MErt8QtON6lrB9tyA=="],
|
||||||
|
|
||||||
"rollup": ["rollup@4.57.1", "", { "dependencies": { "@types/estree": "1.0.8" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.57.1", "@rollup/rollup-android-arm64": "4.57.1", "@rollup/rollup-darwin-arm64": "4.57.1", "@rollup/rollup-darwin-x64": "4.57.1", "@rollup/rollup-freebsd-arm64": "4.57.1", "@rollup/rollup-freebsd-x64": "4.57.1", "@rollup/rollup-linux-arm-gnueabihf": "4.57.1", "@rollup/rollup-linux-arm-musleabihf": "4.57.1", "@rollup/rollup-linux-arm64-gnu": "4.57.1", "@rollup/rollup-linux-arm64-musl": "4.57.1", "@rollup/rollup-linux-loong64-gnu": "4.57.1", "@rollup/rollup-linux-loong64-musl": "4.57.1", "@rollup/rollup-linux-ppc64-gnu": "4.57.1", "@rollup/rollup-linux-ppc64-musl": "4.57.1", "@rollup/rollup-linux-riscv64-gnu": "4.57.1", "@rollup/rollup-linux-riscv64-musl": "4.57.1", "@rollup/rollup-linux-s390x-gnu": "4.57.1", "@rollup/rollup-linux-x64-gnu": "4.57.1", "@rollup/rollup-linux-x64-musl": "4.57.1", "@rollup/rollup-openbsd-x64": "4.57.1", "@rollup/rollup-openharmony-arm64": "4.57.1", "@rollup/rollup-win32-arm64-msvc": "4.57.1", "@rollup/rollup-win32-ia32-msvc": "4.57.1", "@rollup/rollup-win32-x64-gnu": "4.57.1", "@rollup/rollup-win32-x64-msvc": "4.57.1", "fsevents": "~2.3.2" }, "bin": "dist/bin/rollup" }, "sha512-oQL6lgK3e2QZeQ7gcgIkS2YZPg5slw37hYufJ3edKlfQSGGm8ICoxswK15ntSzF/a8+h7ekRy7k7oWc3BQ7y8A=="],
|
"rollup": ["rollup@4.57.1", "", { "dependencies": { "@types/estree": "1.0.8" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.57.1", "@rollup/rollup-android-arm64": "4.57.1", "@rollup/rollup-darwin-arm64": "4.57.1", "@rollup/rollup-darwin-x64": "4.57.1", "@rollup/rollup-freebsd-arm64": "4.57.1", "@rollup/rollup-freebsd-x64": "4.57.1", "@rollup/rollup-linux-arm-gnueabihf": "4.57.1", "@rollup/rollup-linux-arm-musleabihf": "4.57.1", "@rollup/rollup-linux-arm64-gnu": "4.57.1", "@rollup/rollup-linux-arm64-musl": "4.57.1", "@rollup/rollup-linux-loong64-gnu": "4.57.1", "@rollup/rollup-linux-loong64-musl": "4.57.1", "@rollup/rollup-linux-ppc64-gnu": "4.57.1", "@rollup/rollup-linux-ppc64-musl": "4.57.1", "@rollup/rollup-linux-riscv64-gnu": "4.57.1", "@rollup/rollup-linux-riscv64-musl": "4.57.1", "@rollup/rollup-linux-s390x-gnu": "4.57.1", "@rollup/rollup-linux-x64-gnu": "4.57.1", "@rollup/rollup-linux-x64-musl": "4.57.1", "@rollup/rollup-openbsd-x64": "4.57.1", "@rollup/rollup-openharmony-arm64": "4.57.1", "@rollup/rollup-win32-arm64-msvc": "4.57.1", "@rollup/rollup-win32-ia32-msvc": "4.57.1", "@rollup/rollup-win32-x64-gnu": "4.57.1", "@rollup/rollup-win32-x64-msvc": "4.57.1", "fsevents": "~2.3.2" }, "bin": "dist/bin/rollup" }, "sha512-oQL6lgK3e2QZeQ7gcgIkS2YZPg5slw37hYufJ3edKlfQSGGm8ICoxswK15ntSzF/a8+h7ekRy7k7oWc3BQ7y8A=="],
|
||||||
|
|
||||||
"router": ["router@2.2.0", "", { "dependencies": { "debug": "^4.4.0", "depd": "^2.0.0", "is-promise": "^4.0.0", "parseurl": "^1.3.3", "path-to-regexp": "^8.0.0" } }, "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ=="],
|
"router": ["router@2.2.0", "", { "dependencies": { "debug": "^4.4.0", "depd": "^2.0.0", "is-promise": "^4.0.0", "parseurl": "^1.3.3", "path-to-regexp": "^8.0.0" } }, "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ=="],
|
||||||
@@ -506,6 +594,8 @@
|
|||||||
|
|
||||||
"safer-buffer": ["safer-buffer@2.1.2", "", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="],
|
"safer-buffer": ["safer-buffer@2.1.2", "", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="],
|
||||||
|
|
||||||
|
"scheduler": ["scheduler@0.27.0", "", {}, "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q=="],
|
||||||
|
|
||||||
"secure-json-parse": ["secure-json-parse@4.1.0", "", {}, "sha512-l4KnYfEyqYJxDwlNVyRfO2E4NTHfMKAWdUuA8J0yve2Dz/E/PdBepY03RvyJpssIpRFwJoCD55wA+mEDs6ByWA=="],
|
"secure-json-parse": ["secure-json-parse@4.1.0", "", {}, "sha512-l4KnYfEyqYJxDwlNVyRfO2E4NTHfMKAWdUuA8J0yve2Dz/E/PdBepY03RvyJpssIpRFwJoCD55wA+mEDs6ByWA=="],
|
||||||
|
|
||||||
"semver": ["semver@7.7.4", "", { "bin": "bin/semver.js" }, "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA=="],
|
"semver": ["semver@7.7.4", "", { "bin": "bin/semver.js" }, "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA=="],
|
||||||
@@ -562,7 +652,7 @@
|
|||||||
|
|
||||||
"tinyexec": ["tinyexec@1.0.2", "", {}, "sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg=="],
|
"tinyexec": ["tinyexec@1.0.2", "", {}, "sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg=="],
|
||||||
|
|
||||||
"tinyglobby": ["tinyglobby@0.2.15", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.3" } }, "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ=="],
|
"tinyglobby": ["tinyglobby@0.2.16", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.4" } }, "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg=="],
|
||||||
|
|
||||||
"tinyrainbow": ["tinyrainbow@3.0.3", "", {}, "sha512-PSkbLUoxOFRzJYjjxHJt9xro7D+iilgMX/C9lawzVuYiIdcihh9DXmVibBe8lmcFrRi/VzlPjBxbN7rH24q8/Q=="],
|
"tinyrainbow": ["tinyrainbow@3.0.3", "", {}, "sha512-PSkbLUoxOFRzJYjjxHJt9xro7D+iilgMX/C9lawzVuYiIdcihh9DXmVibBe8lmcFrRi/VzlPjBxbN7rH24q8/Q=="],
|
||||||
|
|
||||||
@@ -590,7 +680,7 @@
|
|||||||
|
|
||||||
"vary": ["vary@1.1.2", "", {}, "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg=="],
|
"vary": ["vary@1.1.2", "", {}, "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg=="],
|
||||||
|
|
||||||
"vite": ["vite@7.3.1", "", { "dependencies": { "esbuild": "^0.27.0", "fdir": "^6.5.0", "picomatch": "^4.0.3", "postcss": "^8.5.6", "rollup": "^4.43.0", "tinyglobby": "^0.2.15" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "jiti": ">=1.21.0", "less": "^4.0.0", "lightningcss": "^1.21.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser"], "bin": "bin/vite.js" }, "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA=="],
|
"vite": ["vite@8.0.10", "", { "dependencies": { "lightningcss": "^1.32.0", "picomatch": "^4.0.4", "postcss": "^8.5.10", "rolldown": "1.0.0-rc.17", "tinyglobby": "^0.2.16" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "@vitejs/devtools": "^0.1.0", "esbuild": "^0.27.0 || ^0.28.0", "jiti": ">=1.21.0", "less": "^4.0.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "@vitejs/devtools", "esbuild", "jiti", "less", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-rZuUu9j6J5uotLDs+cAA4O5H4K1SfPliUlQwqa6YEwSrWDZzP4rhm00oJR5snMewjxF5V/K3D4kctsUTsIU9Mw=="],
|
||||||
|
|
||||||
"vitest": ["vitest@4.0.18", "", { "dependencies": { "@vitest/expect": "4.0.18", "@vitest/mocker": "4.0.18", "@vitest/pretty-format": "4.0.18", "@vitest/runner": "4.0.18", "@vitest/snapshot": "4.0.18", "@vitest/spy": "4.0.18", "@vitest/utils": "4.0.18", "es-module-lexer": "^1.7.0", "expect-type": "^1.2.2", "magic-string": "^0.30.21", "obug": "^2.1.1", "pathe": "^2.0.3", "picomatch": "^4.0.3", "std-env": "^3.10.0", "tinybench": "^2.9.0", "tinyexec": "^1.0.2", "tinyglobby": "^0.2.15", "tinyrainbow": "^3.0.3", "vite": "^6.0.0 || ^7.0.0", "why-is-node-running": "^2.3.0" }, "peerDependencies": { "@edge-runtime/vm": "*", "@opentelemetry/api": "^1.9.0", "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", "@vitest/browser-playwright": "4.0.18", "@vitest/browser-preview": "4.0.18", "@vitest/browser-webdriverio": "4.0.18", "@vitest/ui": "4.0.18", "happy-dom": "*", "jsdom": "*" }, "optionalPeers": ["@edge-runtime/vm", "@opentelemetry/api", "@vitest/browser-playwright", "@vitest/browser-preview", "@vitest/browser-webdriverio", "@vitest/ui", "happy-dom", "jsdom"], "bin": "vitest.mjs" }, "sha512-hOQuK7h0FGKgBAas7v0mSAsnvrIgAvWmRFjmzpJ7SwFHH3g1k2u37JtYwOwmEKhK6ZO3v9ggDBBm0La1LCK4uQ=="],
|
"vitest": ["vitest@4.0.18", "", { "dependencies": { "@vitest/expect": "4.0.18", "@vitest/mocker": "4.0.18", "@vitest/pretty-format": "4.0.18", "@vitest/runner": "4.0.18", "@vitest/snapshot": "4.0.18", "@vitest/spy": "4.0.18", "@vitest/utils": "4.0.18", "es-module-lexer": "^1.7.0", "expect-type": "^1.2.2", "magic-string": "^0.30.21", "obug": "^2.1.1", "pathe": "^2.0.3", "picomatch": "^4.0.3", "std-env": "^3.10.0", "tinybench": "^2.9.0", "tinyexec": "^1.0.2", "tinyglobby": "^0.2.15", "tinyrainbow": "^3.0.3", "vite": "^6.0.0 || ^7.0.0", "why-is-node-running": "^2.3.0" }, "peerDependencies": { "@edge-runtime/vm": "*", "@opentelemetry/api": "^1.9.0", "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", "@vitest/browser-playwright": "4.0.18", "@vitest/browser-preview": "4.0.18", "@vitest/browser-webdriverio": "4.0.18", "@vitest/ui": "4.0.18", "happy-dom": "*", "jsdom": "*" }, "optionalPeers": ["@edge-runtime/vm", "@opentelemetry/api", "@vitest/browser-playwright", "@vitest/browser-preview", "@vitest/browser-webdriverio", "@vitest/ui", "happy-dom", "jsdom"], "bin": "vitest.mjs" }, "sha512-hOQuK7h0FGKgBAas7v0mSAsnvrIgAvWmRFjmzpJ7SwFHH3g1k2u37JtYwOwmEKhK6ZO3v9ggDBBm0La1LCK4uQ=="],
|
||||||
|
|
||||||
@@ -615,5 +705,15 @@
|
|||||||
"pino-pretty/pino-abstract-transport": ["pino-abstract-transport@3.0.0", "", { "dependencies": { "split2": "^4.0.0" } }, "sha512-wlfUczU+n7Hy/Ha5j9a/gZNy7We5+cXp8YL+X+PG8S0KXxw7n/JXA3c46Y0zQznIJ83URJiwy7Lh56WLokNuxg=="],
|
"pino-pretty/pino-abstract-transport": ["pino-abstract-transport@3.0.0", "", { "dependencies": { "split2": "^4.0.0" } }, "sha512-wlfUczU+n7Hy/Ha5j9a/gZNy7We5+cXp8YL+X+PG8S0KXxw7n/JXA3c46Y0zQznIJ83URJiwy7Lh56WLokNuxg=="],
|
||||||
|
|
||||||
"rc/strip-json-comments": ["strip-json-comments@2.0.1", "", {}, "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ=="],
|
"rc/strip-json-comments": ["strip-json-comments@2.0.1", "", {}, "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ=="],
|
||||||
|
|
||||||
|
"rolldown/@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-rc.17", "", {}, "sha512-n8iosDOt6Ig1UhJ2AYqoIhHWh/isz0xpicHTzpKBeotdVsTEcxsSA/i3EVM7gQAj0rU27OLAxCjzlj15IWY7bg=="],
|
||||||
|
|
||||||
|
"vitest/picomatch": ["picomatch@4.0.3", "", {}, "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q=="],
|
||||||
|
|
||||||
|
"vitest/tinyglobby": ["tinyglobby@0.2.15", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.3" } }, "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ=="],
|
||||||
|
|
||||||
|
"vitest/vite": ["vite@7.3.1", "", { "dependencies": { "esbuild": "^0.27.0", "fdir": "^6.5.0", "picomatch": "^4.0.3", "postcss": "^8.5.6", "rollup": "^4.43.0", "tinyglobby": "^0.2.15" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "jiti": ">=1.21.0", "less": "^4.0.0", "lightningcss": "^1.21.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser"], "bin": "bin/vite.js" }, "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA=="],
|
||||||
|
|
||||||
|
"vitest/vite/postcss": ["postcss@8.5.6", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg=="],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
17
package.json
17
package.json
@@ -7,9 +7,12 @@
|
|||||||
"main": "dist/index.js",
|
"main": "dist/index.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "tsc",
|
"build": "tsc",
|
||||||
|
"dashboard:dev": "vite --config apps/dashboard/vite.config.ts",
|
||||||
|
"dashboard:build": "vite build --config apps/dashboard/vite.config.ts",
|
||||||
|
"dashboard:preview": "vite preview --config apps/dashboard/vite.config.ts",
|
||||||
"install:runners": "bun install --frozen-lockfile --cwd runners/shared && bun install --frozen-lockfile --cwd runners/agent-runner && bun install --frozen-lockfile --cwd runners/codex-runner",
|
"install:runners": "bun install --frozen-lockfile --cwd runners/shared && bun install --frozen-lockfile --cwd runners/agent-runner && bun install --frozen-lockfile --cwd runners/codex-runner",
|
||||||
"build:runners": "bun run install:runners && bun run --cwd runners/shared build && bun run --cwd runners/agent-runner build && bun run --cwd runners/codex-runner build",
|
"build:runners": "bun run install:runners && bun run --cwd runners/shared build && bun run --cwd runners/agent-runner build && bun run --cwd runners/codex-runner build",
|
||||||
"build:all": "bun run build && bun run build:runners",
|
"build:all": "bun run build && bun run dashboard:build && bun run build:runners",
|
||||||
"build:runtime": "bun run build:all",
|
"build:runtime": "bun run build:all",
|
||||||
"verify:dist": "bash scripts/check-dist-fresh.sh",
|
"verify:dist": "bash scripts/check-dist-fresh.sh",
|
||||||
"deploy": "git pull --ff-only && bun run build:all && bun run verify:dist && bun setup/index.ts --step migrate-room-registrations && systemctl --user restart ejclaw",
|
"deploy": "git pull --ff-only && bun run build:all && bun run verify:dist && bun setup/index.ts --step migrate-room-registrations && systemctl --user restart ejclaw",
|
||||||
@@ -17,9 +20,9 @@
|
|||||||
"dev": "bun --watch src/index.ts",
|
"dev": "bun --watch src/index.ts",
|
||||||
"test": "vitest run",
|
"test": "vitest run",
|
||||||
"typecheck": "tsc --noEmit",
|
"typecheck": "tsc --noEmit",
|
||||||
"typecheck:all": "bun run install:runners && bunx tsc --noEmit -p tsconfig.check.json && bunx tsc --noEmit -p runners/shared/tsconfig.json && bunx tsc --noEmit -p runners/agent-runner/tsconfig.json && bunx tsc --noEmit -p runners/codex-runner/tsconfig.json",
|
"typecheck:all": "bun run install:runners && bunx tsc --noEmit -p tsconfig.check.json && bunx tsc --noEmit -p apps/dashboard/tsconfig.json && bunx tsc --noEmit -p runners/shared/tsconfig.json && bunx tsc --noEmit -p runners/agent-runner/tsconfig.json && bunx tsc --noEmit -p runners/codex-runner/tsconfig.json",
|
||||||
"format:fix": "prettier --write \"{src,setup,shared,test,scripts}/**/*.{ts,js}\" \"runners/**/*.{ts,js}\" \"vitest*.ts\"",
|
"format:fix": "prettier --write \"{src,setup,shared,test,scripts,apps}/**/*.{ts,tsx,js,jsx,css,html}\" \"runners/**/*.{ts,js}\" \"vitest*.ts\"",
|
||||||
"format:check": "prettier --check \"{src,setup,shared,test,scripts}/**/*.{ts,js}\" \"runners/**/*.{ts,js}\" \"vitest*.ts\"",
|
"format:check": "prettier --check \"{src,setup,shared,test,scripts,apps}/**/*.{ts,tsx,js,jsx,css,html}\" \"runners/**/*.{ts,js}\" \"vitest*.ts\"",
|
||||||
"check": "bun run format:check && bun run typecheck:all && bun run test && bun run build:all",
|
"check": "bun run format:check && bun run typecheck:all && bun run test && bun run build:all",
|
||||||
"prepare": "husky",
|
"prepare": "husky",
|
||||||
"setup": "bun setup/index.ts"
|
"setup": "bun setup/index.ts"
|
||||||
@@ -31,6 +34,8 @@
|
|||||||
"ejclaw-runners-shared": "file:./runners/shared",
|
"ejclaw-runners-shared": "file:./runners/shared",
|
||||||
"pino": "^9.6.0",
|
"pino": "^9.6.0",
|
||||||
"pino-pretty": "^13.0.0",
|
"pino-pretty": "^13.0.0",
|
||||||
|
"react": "^19.2.5",
|
||||||
|
"react-dom": "^19.2.5",
|
||||||
"yaml": "^2.8.2",
|
"yaml": "^2.8.2",
|
||||||
"zod": "^4.3.6"
|
"zod": "^4.3.6"
|
||||||
},
|
},
|
||||||
@@ -38,11 +43,15 @@
|
|||||||
"@types/better-sqlite3": "^7.6.13",
|
"@types/better-sqlite3": "^7.6.13",
|
||||||
"@types/bun": "^1.3.11",
|
"@types/bun": "^1.3.11",
|
||||||
"@types/node": "^22.10.0",
|
"@types/node": "^22.10.0",
|
||||||
|
"@types/react": "^19.2.14",
|
||||||
|
"@types/react-dom": "^19.2.3",
|
||||||
|
"@vitejs/plugin-react": "^6.0.1",
|
||||||
"@vitest/coverage-v8": "^4.0.18",
|
"@vitest/coverage-v8": "^4.0.18",
|
||||||
"better-sqlite3": "^12.8.0",
|
"better-sqlite3": "^12.8.0",
|
||||||
"husky": "^9.1.7",
|
"husky": "^9.1.7",
|
||||||
"prettier": "^3.8.1",
|
"prettier": "^3.8.1",
|
||||||
"typescript": "^5.7.0",
|
"typescript": "^5.7.0",
|
||||||
|
"vite": "^8.0.10",
|
||||||
"vitest": "^4.0.18"
|
"vitest": "^4.0.18"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
|
|||||||
@@ -243,7 +243,12 @@ describe('room registration state', () => {
|
|||||||
JSON.stringify({ last_timestamp: '1234' }),
|
JSON.stringify({ last_timestamp: '1234' }),
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(detectRoomRegistrationState({ projectRoot: tempRoot })).toEqual({
|
expect(
|
||||||
|
detectRoomRegistrationState({
|
||||||
|
projectRoot: tempRoot,
|
||||||
|
dbPath: path.join(tempRoot, 'messages.db'),
|
||||||
|
}),
|
||||||
|
).toEqual({
|
||||||
assignedRooms: 0,
|
assignedRooms: 0,
|
||||||
roomsByOwnerAgent: {},
|
roomsByOwnerAgent: {},
|
||||||
legacyRegisteredGroupRows: 0,
|
legacyRegisteredGroupRows: 0,
|
||||||
|
|||||||
@@ -42,6 +42,10 @@ describe('config/env loading', () => {
|
|||||||
delete process.env.CODEX_WARMUP_COMMAND_TIMEOUT_MS;
|
delete process.env.CODEX_WARMUP_COMMAND_TIMEOUT_MS;
|
||||||
delete process.env.CODEX_WARMUP_FAILURE_COOLDOWN_MS;
|
delete process.env.CODEX_WARMUP_FAILURE_COOLDOWN_MS;
|
||||||
delete process.env.CODEX_WARMUP_MAX_CONSECUTIVE_FAILURES;
|
delete process.env.CODEX_WARMUP_MAX_CONSECUTIVE_FAILURES;
|
||||||
|
delete process.env.WEB_DASHBOARD_ENABLED;
|
||||||
|
delete process.env.WEB_DASHBOARD_HOST;
|
||||||
|
delete process.env.WEB_DASHBOARD_PORT;
|
||||||
|
delete process.env.WEB_DASHBOARD_STATIC_DIR;
|
||||||
delete process.env.SESSION_COMMAND_USER_IDS;
|
delete process.env.SESSION_COMMAND_USER_IDS;
|
||||||
vi.resetModules();
|
vi.resetModules();
|
||||||
});
|
});
|
||||||
@@ -163,8 +167,32 @@ describe('config/env loading', () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('keeps the web dashboard disabled by default and loads explicit bind/static settings', async () => {
|
||||||
|
let config = await import('./config.js');
|
||||||
|
expect(config.WEB_DASHBOARD.enabled).toBe(false);
|
||||||
|
expect(config.WEB_DASHBOARD.host).toBe('127.0.0.1');
|
||||||
|
expect(config.WEB_DASHBOARD.port).toBe(8734);
|
||||||
|
expect(config.WEB_DASHBOARD.staticDir).toBe(
|
||||||
|
path.resolve(tempRoot, 'apps', 'dashboard', 'dist'),
|
||||||
|
);
|
||||||
|
|
||||||
|
vi.resetModules();
|
||||||
|
process.env.WEB_DASHBOARD_ENABLED = 'true';
|
||||||
|
process.env.WEB_DASHBOARD_HOST = '0.0.0.0';
|
||||||
|
process.env.WEB_DASHBOARD_PORT = '9001';
|
||||||
|
process.env.WEB_DASHBOARD_STATIC_DIR = './custom-dashboard-dist';
|
||||||
|
config = await import('./config.js');
|
||||||
|
|
||||||
|
expect(config.WEB_DASHBOARD).toEqual({
|
||||||
|
enabled: true,
|
||||||
|
host: '0.0.0.0',
|
||||||
|
port: 9001,
|
||||||
|
staticDir: path.resolve(tempRoot, 'custom-dashboard-dist'),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
it('fails fast when a legacy Discord token alias is configured', async () => {
|
it('fails fast when a legacy Discord token alias is configured', async () => {
|
||||||
process.env.DISCORD_BOT_TOKEN = 'legacy-owner-token';
|
process.env.DISCORD_BOT_TOKEN = 'legacy...oken';
|
||||||
|
|
||||||
const { loadConfig } = await import('./config/load-config.js');
|
const { loadConfig } = await import('./config/load-config.js');
|
||||||
|
|
||||||
|
|||||||
@@ -141,6 +141,7 @@ export const STATUS_SHOW_ROOMS = CONFIG.status.showRooms;
|
|||||||
export const STATUS_SHOW_ROOM_DETAILS = CONFIG.status.showRoomDetails;
|
export const STATUS_SHOW_ROOM_DETAILS = CONFIG.status.showRoomDetails;
|
||||||
export const USAGE_DASHBOARD_ENABLED = CONFIG.status.usageDashboardEnabled;
|
export const USAGE_DASHBOARD_ENABLED = CONFIG.status.usageDashboardEnabled;
|
||||||
export const CODEX_WARMUP_CONFIG = CONFIG.codexWarmup;
|
export const CODEX_WARMUP_CONFIG = CONFIG.codexWarmup;
|
||||||
|
export const WEB_DASHBOARD = CONFIG.webDashboard;
|
||||||
|
|
||||||
// Timezone for scheduled tasks (cron expressions, etc.)
|
// Timezone for scheduled tasks (cron expressions, etc.)
|
||||||
// Uses system timezone by default
|
// Uses system timezone by default
|
||||||
|
|||||||
@@ -260,6 +260,15 @@ export function loadConfig(): AppConfig {
|
|||||||
timezone:
|
timezone:
|
||||||
readText('TZ') ?? Intl.DateTimeFormat().resolvedOptions().timeZone,
|
readText('TZ') ?? Intl.DateTimeFormat().resolvedOptions().timeZone,
|
||||||
},
|
},
|
||||||
|
webDashboard: {
|
||||||
|
enabled: readBoolean('WEB_DASHBOARD_ENABLED', false),
|
||||||
|
host: readNonEmptyText('WEB_DASHBOARD_HOST') ?? '127.0.0.1',
|
||||||
|
port: readIntegerAtLeast('WEB_DASHBOARD_PORT', 8734, 1),
|
||||||
|
staticDir: path.resolve(
|
||||||
|
readNonEmptyText('WEB_DASHBOARD_STATIC_DIR') ??
|
||||||
|
path.join(projectRoot, 'apps', 'dashboard', 'dist'),
|
||||||
|
),
|
||||||
|
},
|
||||||
codexWarmup: {
|
codexWarmup: {
|
||||||
enabled: readBoolean('CODEX_WARMUP_ENABLED', false),
|
enabled: readBoolean('CODEX_WARMUP_ENABLED', false),
|
||||||
prompt:
|
prompt:
|
||||||
|
|||||||
@@ -78,6 +78,12 @@ export interface AppConfig {
|
|||||||
usageDashboardEnabled: boolean;
|
usageDashboardEnabled: boolean;
|
||||||
timezone: string;
|
timezone: string;
|
||||||
};
|
};
|
||||||
|
webDashboard: {
|
||||||
|
enabled: boolean;
|
||||||
|
host: string;
|
||||||
|
port: number;
|
||||||
|
staticDir: string;
|
||||||
|
};
|
||||||
codexWarmup: {
|
codexWarmup: {
|
||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
prompt: string;
|
prompt: string;
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import {
|
|||||||
TIMEZONE,
|
TIMEZONE,
|
||||||
TRIGGER_PATTERN,
|
TRIGGER_PATTERN,
|
||||||
USAGE_UPDATE_INTERVAL,
|
USAGE_UPDATE_INTERVAL,
|
||||||
|
WEB_DASHBOARD,
|
||||||
} from './config.js';
|
} from './config.js';
|
||||||
import './channels/index.js';
|
import './channels/index.js';
|
||||||
import {
|
import {
|
||||||
@@ -54,6 +55,7 @@ import {
|
|||||||
import { createMessageRuntime } from './message-runtime.js';
|
import { createMessageRuntime } from './message-runtime.js';
|
||||||
import { nudgeSchedulerLoop, startSchedulerLoop } from './task-scheduler.js';
|
import { nudgeSchedulerLoop, startSchedulerLoop } from './task-scheduler.js';
|
||||||
import { startUnifiedDashboard } from './unified-dashboard.js';
|
import { startUnifiedDashboard } from './unified-dashboard.js';
|
||||||
|
import { startWebDashboardServer } from './web-dashboard-server.js';
|
||||||
import { Channel, NewMessage, RegisteredGroup } from './types.js';
|
import { Channel, NewMessage, RegisteredGroup } from './types.js';
|
||||||
import { logger } from './logger.js';
|
import { logger } from './logger.js';
|
||||||
import { initCodexTokenRotation } from './codex-token-rotation.js';
|
import { initCodexTokenRotation } from './codex-token-rotation.js';
|
||||||
@@ -248,6 +250,7 @@ async function main(): Promise<void> {
|
|||||||
|
|
||||||
// Graceful shutdown handlers
|
// Graceful shutdown handlers
|
||||||
let leaseRecoveryTimer: ReturnType<typeof setInterval> | null = null;
|
let leaseRecoveryTimer: ReturnType<typeof setInterval> | null = null;
|
||||||
|
let webDashboardServer: ReturnType<typeof startWebDashboardServer> = null;
|
||||||
const shutdown = async (signal: string) => {
|
const shutdown = async (signal: string) => {
|
||||||
logger.info({ signal }, 'Shutdown signal received');
|
logger.info({ signal }, 'Shutdown signal received');
|
||||||
stopTokenRefreshLoop();
|
stopTokenRefreshLoop();
|
||||||
@@ -255,6 +258,8 @@ async function main(): Promise<void> {
|
|||||||
clearInterval(leaseRecoveryTimer);
|
clearInterval(leaseRecoveryTimer);
|
||||||
leaseRecoveryTimer = null;
|
leaseRecoveryTimer = null;
|
||||||
}
|
}
|
||||||
|
webDashboardServer?.stop();
|
||||||
|
webDashboardServer = null;
|
||||||
const roomBindings = runtimeState.getRoomBindings();
|
const roomBindings = runtimeState.getRoomBindings();
|
||||||
const interruptedGroups = queue
|
const interruptedGroups = queue
|
||||||
.getStatuses(Object.keys(roomBindings))
|
.getStatuses(Object.keys(roomBindings))
|
||||||
@@ -516,6 +521,7 @@ async function main(): Promise<void> {
|
|||||||
},
|
},
|
||||||
purgeOnStart: true,
|
purgeOnStart: true,
|
||||||
});
|
});
|
||||||
|
webDashboardServer = startWebDashboardServer(WEB_DASHBOARD);
|
||||||
|
|
||||||
leaseRecoveryTimer = setInterval(() => {
|
leaseRecoveryTimer = setInterval(() => {
|
||||||
const failover = getGlobalFailoverInfo();
|
const failover = getGlobalFailoverInfo();
|
||||||
|
|||||||
125
src/web-dashboard-data.test.ts
Normal file
125
src/web-dashboard-data.test.ts
Normal file
@@ -0,0 +1,125 @@
|
|||||||
|
import { describe, expect, it } from 'vitest';
|
||||||
|
|
||||||
|
import type { StatusSnapshot } from './status-dashboard.js';
|
||||||
|
import type { ScheduledTask } from './types.js';
|
||||||
|
import {
|
||||||
|
buildWebDashboardOverview,
|
||||||
|
sanitizeScheduledTask,
|
||||||
|
} from './web-dashboard-data.js';
|
||||||
|
|
||||||
|
function makeTask(overrides: Partial<ScheduledTask>): ScheduledTask {
|
||||||
|
return {
|
||||||
|
id: 'task-1',
|
||||||
|
group_folder: 'general',
|
||||||
|
chat_jid: 'dc:general',
|
||||||
|
agent_type: null,
|
||||||
|
status_message_id: null,
|
||||||
|
status_started_at: null,
|
||||||
|
prompt: 'secret long prompt that should not be exposed in full',
|
||||||
|
schedule_type: 'cron',
|
||||||
|
schedule_value: '* * * * *',
|
||||||
|
context_mode: 'group',
|
||||||
|
next_run: '2026-04-26T05:00:00.000Z',
|
||||||
|
last_run: null,
|
||||||
|
last_result: null,
|
||||||
|
status: 'active',
|
||||||
|
created_at: '2026-04-26T04:00:00.000Z',
|
||||||
|
...overrides,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('web dashboard data', () => {
|
||||||
|
it('builds overview counts from status snapshots and scheduled tasks', () => {
|
||||||
|
const snapshots: StatusSnapshot[] = [
|
||||||
|
{
|
||||||
|
serviceId: 'codex-main',
|
||||||
|
agentType: 'codex',
|
||||||
|
assistantName: 'Codex',
|
||||||
|
updatedAt: '2026-04-26T04:59:00.000Z',
|
||||||
|
entries: [
|
||||||
|
{
|
||||||
|
jid: 'dc:1',
|
||||||
|
name: '#general',
|
||||||
|
folder: 'general',
|
||||||
|
agentType: 'codex',
|
||||||
|
status: 'processing',
|
||||||
|
elapsedMs: 1200,
|
||||||
|
pendingMessages: true,
|
||||||
|
pendingTasks: 2,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
jid: 'dc:2',
|
||||||
|
name: '#brain',
|
||||||
|
folder: 'brain',
|
||||||
|
agentType: 'claude-code',
|
||||||
|
status: 'inactive',
|
||||||
|
elapsedMs: null,
|
||||||
|
pendingMessages: false,
|
||||||
|
pendingTasks: 0,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
usageRows: [
|
||||||
|
{
|
||||||
|
name: 'codex-a',
|
||||||
|
h5pct: 10,
|
||||||
|
h5reset: '1h',
|
||||||
|
d7pct: 20,
|
||||||
|
d7reset: '2d',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const overview = buildWebDashboardOverview({
|
||||||
|
now: '2026-04-26T05:00:00.000Z',
|
||||||
|
snapshots,
|
||||||
|
tasks: [
|
||||||
|
makeTask({
|
||||||
|
id: 'watch-1',
|
||||||
|
prompt: '[BACKGROUND CI WATCH] owner/repo#1',
|
||||||
|
status: 'active',
|
||||||
|
}),
|
||||||
|
makeTask({ id: 'cron-1', prompt: 'regular cleanup', status: 'paused' }),
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(overview.rooms.total).toBe(2);
|
||||||
|
expect(overview.rooms.active).toBe(1);
|
||||||
|
expect(overview.rooms.waiting).toBe(0);
|
||||||
|
expect(overview.rooms.inactive).toBe(1);
|
||||||
|
expect(overview.tasks.total).toBe(2);
|
||||||
|
expect(overview.tasks.active).toBe(1);
|
||||||
|
expect(overview.tasks.paused).toBe(1);
|
||||||
|
expect(overview.tasks.watchers.active).toBe(1);
|
||||||
|
expect(overview.usage.rows).toHaveLength(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does not expose full scheduled task prompts through API payloads', () => {
|
||||||
|
const sanitized = sanitizeScheduledTask(
|
||||||
|
makeTask({
|
||||||
|
prompt: 'x'.repeat(220),
|
||||||
|
last_result: 'ok',
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(sanitized).not.toHaveProperty('prompt');
|
||||||
|
expect(sanitized.promptPreview.length).toBeLessThanOrEqual(123);
|
||||||
|
expect(sanitized.promptLength).toBe(220);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('redacts common secret values from scheduled task prompt previews', () => {
|
||||||
|
const sanitized = sanitizeScheduledTask(
|
||||||
|
makeTask({
|
||||||
|
prompt:
|
||||||
|
'deploy with OPENAI_API_KEY=sk-abcdefghijklmnopqrstuvwxyz123456 and BOT_TOKEN=plain-secret-value',
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(sanitized.promptPreview).toContain('OPENAI_API_KEY=<redacted>');
|
||||||
|
expect(sanitized.promptPreview).toContain('BOT_TOKEN=<redacted>');
|
||||||
|
expect(sanitized.promptPreview).not.toContain(
|
||||||
|
'sk-abcdefghijklmnopqrstuvwxyz123456',
|
||||||
|
);
|
||||||
|
expect(sanitized.promptPreview).not.toContain('plain-secret-value');
|
||||||
|
});
|
||||||
|
});
|
||||||
185
src/web-dashboard-data.ts
Normal file
185
src/web-dashboard-data.ts
Normal file
@@ -0,0 +1,185 @@
|
|||||||
|
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());
|
||||||
|
}
|
||||||
|
|
||||||
|
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 = args.snapshots.flatMap(
|
||||||
|
(snapshot) => snapshot.usageRows ?? [],
|
||||||
|
);
|
||||||
|
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,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
73
src/web-dashboard-server.test.ts
Normal file
73
src/web-dashboard-server.test.ts
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
import fs from 'fs';
|
||||||
|
import os from 'os';
|
||||||
|
import path from 'path';
|
||||||
|
|
||||||
|
import { afterEach, describe, expect, it } from 'vitest';
|
||||||
|
|
||||||
|
import { createWebDashboardHandler } from './web-dashboard-server.js';
|
||||||
|
|
||||||
|
const tempDirs: string[] = [];
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
for (const dir of tempDirs.splice(0)) {
|
||||||
|
fs.rmSync(dir, { recursive: true, force: true });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('web dashboard server handler', () => {
|
||||||
|
it('serves health and overview JSON without requiring Discord', async () => {
|
||||||
|
const handler = createWebDashboardHandler({
|
||||||
|
readStatusSnapshots: () => [],
|
||||||
|
getTasks: () => [],
|
||||||
|
});
|
||||||
|
|
||||||
|
const health = await handler(new Request('http://localhost/api/health'));
|
||||||
|
expect(health.status).toBe(200);
|
||||||
|
await expect(health.json()).resolves.toEqual({ ok: true });
|
||||||
|
|
||||||
|
const overview = await handler(
|
||||||
|
new Request('http://localhost/api/overview'),
|
||||||
|
);
|
||||||
|
expect(overview.status).toBe(200);
|
||||||
|
const body = (await overview.json()) as {
|
||||||
|
rooms: { total: number };
|
||||||
|
tasks: { total: number };
|
||||||
|
};
|
||||||
|
expect(body.rooms.total).toBe(0);
|
||||||
|
expect(body.tasks.total).toBe(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('serves Vite static assets and falls back to index for SPA routes', async () => {
|
||||||
|
const staticDir = fs.mkdtempSync(
|
||||||
|
path.join(os.tmpdir(), 'ejclaw-dashboard-'),
|
||||||
|
);
|
||||||
|
tempDirs.push(staticDir);
|
||||||
|
fs.writeFileSync(
|
||||||
|
path.join(staticDir, 'index.html'),
|
||||||
|
'<div id="root"></div>',
|
||||||
|
);
|
||||||
|
fs.mkdirSync(path.join(staticDir, 'assets'));
|
||||||
|
fs.writeFileSync(
|
||||||
|
path.join(staticDir, 'assets', 'app.js'),
|
||||||
|
'console.log("ok")',
|
||||||
|
);
|
||||||
|
|
||||||
|
const handler = createWebDashboardHandler({
|
||||||
|
staticDir,
|
||||||
|
readStatusSnapshots: () => [],
|
||||||
|
getTasks: () => [],
|
||||||
|
});
|
||||||
|
|
||||||
|
const asset = await handler(new Request('http://localhost/assets/app.js'));
|
||||||
|
expect(asset.status).toBe(200);
|
||||||
|
expect(asset.headers.get('content-type')).toContain('text/javascript');
|
||||||
|
await expect(asset.text()).resolves.toContain('console.log');
|
||||||
|
|
||||||
|
const fallback = await handler(
|
||||||
|
new Request('http://localhost/tasks/swarm_123'),
|
||||||
|
);
|
||||||
|
expect(fallback.status).toBe(200);
|
||||||
|
expect(fallback.headers.get('content-type')).toContain('text/html');
|
||||||
|
await expect(fallback.text()).resolves.toContain('root');
|
||||||
|
});
|
||||||
|
});
|
||||||
182
src/web-dashboard-server.ts
Normal file
182
src/web-dashboard-server.ts
Normal file
@@ -0,0 +1,182 @@
|
|||||||
|
import fs from 'fs';
|
||||||
|
import path from 'path';
|
||||||
|
|
||||||
|
import { WEB_DASHBOARD } from './config.js';
|
||||||
|
import { getAllTasks } from './db.js';
|
||||||
|
import { logger } from './logger.js';
|
||||||
|
import {
|
||||||
|
readStatusSnapshots,
|
||||||
|
type StatusSnapshot,
|
||||||
|
} from './status-dashboard.js';
|
||||||
|
import type { ScheduledTask } from './types.js';
|
||||||
|
import {
|
||||||
|
buildWebDashboardOverview,
|
||||||
|
sanitizeScheduledTask,
|
||||||
|
} from './web-dashboard-data.js';
|
||||||
|
|
||||||
|
const DEFAULT_STATUS_MAX_AGE_MS = 10 * 60 * 1000;
|
||||||
|
|
||||||
|
export interface WebDashboardHandlerOptions {
|
||||||
|
staticDir?: string;
|
||||||
|
statusMaxAgeMs?: number;
|
||||||
|
readStatusSnapshots?: (maxAgeMs: number) => StatusSnapshot[];
|
||||||
|
getTasks?: () => ScheduledTask[];
|
||||||
|
now?: () => string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface StartedWebDashboardServer {
|
||||||
|
url: string;
|
||||||
|
stop: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
function jsonResponse(value: unknown, init?: ResponseInit): Response {
|
||||||
|
return new Response(JSON.stringify(value, null, 2), {
|
||||||
|
...init,
|
||||||
|
headers: {
|
||||||
|
'content-type': 'application/json; charset=utf-8',
|
||||||
|
...(init?.headers ?? {}),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function getContentType(filePath: string): string {
|
||||||
|
const ext = path.extname(filePath).toLowerCase();
|
||||||
|
switch (ext) {
|
||||||
|
case '.html':
|
||||||
|
return 'text/html; charset=utf-8';
|
||||||
|
case '.js':
|
||||||
|
case '.mjs':
|
||||||
|
return 'text/javascript; charset=utf-8';
|
||||||
|
case '.css':
|
||||||
|
return 'text/css; charset=utf-8';
|
||||||
|
case '.json':
|
||||||
|
return 'application/json; charset=utf-8';
|
||||||
|
case '.svg':
|
||||||
|
return 'image/svg+xml';
|
||||||
|
case '.png':
|
||||||
|
return 'image/png';
|
||||||
|
case '.jpg':
|
||||||
|
case '.jpeg':
|
||||||
|
return 'image/jpeg';
|
||||||
|
case '.webp':
|
||||||
|
return 'image/webp';
|
||||||
|
case '.ico':
|
||||||
|
return 'image/x-icon';
|
||||||
|
default:
|
||||||
|
return 'application/octet-stream';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolveStaticFile(staticDir: string, pathname: string): string | null {
|
||||||
|
const normalizedPath = decodeURIComponent(pathname).replace(/^\/+/, '');
|
||||||
|
const candidate = path.resolve(staticDir, normalizedPath || 'index.html');
|
||||||
|
const root = path.resolve(staticDir);
|
||||||
|
if (candidate !== root && !candidate.startsWith(`${root}${path.sep}`)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (fs.existsSync(candidate) && fs.statSync(candidate).isFile()) {
|
||||||
|
return candidate;
|
||||||
|
}
|
||||||
|
|
||||||
|
const indexPath = path.join(root, 'index.html');
|
||||||
|
if (fs.existsSync(indexPath) && fs.statSync(indexPath).isFile()) {
|
||||||
|
return indexPath;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function serveStaticFile(staticDir: string, pathname: string): Response {
|
||||||
|
const filePath = resolveStaticFile(staticDir, pathname);
|
||||||
|
if (!filePath) {
|
||||||
|
return new Response('Not found', { status: 404 });
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Response(fs.readFileSync(filePath), {
|
||||||
|
headers: {
|
||||||
|
'content-type': getContentType(filePath),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createWebDashboardHandler(
|
||||||
|
opts: WebDashboardHandlerOptions = {},
|
||||||
|
): (request: Request) => Response | Promise<Response> {
|
||||||
|
const readSnapshots = opts.readStatusSnapshots ?? readStatusSnapshots;
|
||||||
|
const loadTasks = opts.getTasks ?? getAllTasks;
|
||||||
|
const statusMaxAgeMs = opts.statusMaxAgeMs ?? DEFAULT_STATUS_MAX_AGE_MS;
|
||||||
|
|
||||||
|
return (request: Request): Response => {
|
||||||
|
const url = new URL(request.url);
|
||||||
|
|
||||||
|
if (request.method !== 'GET' && request.method !== 'HEAD') {
|
||||||
|
return jsonResponse({ error: 'Method not allowed' }, { status: 405 });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (url.pathname === '/api/health') {
|
||||||
|
return jsonResponse({ ok: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (url.pathname === '/api/overview') {
|
||||||
|
const snapshots = readSnapshots(statusMaxAgeMs);
|
||||||
|
const tasks = loadTasks();
|
||||||
|
return jsonResponse(
|
||||||
|
buildWebDashboardOverview({
|
||||||
|
now: opts.now?.(),
|
||||||
|
snapshots,
|
||||||
|
tasks,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (url.pathname === '/api/status-snapshots') {
|
||||||
|
return jsonResponse(readSnapshots(statusMaxAgeMs));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (url.pathname === '/api/tasks') {
|
||||||
|
return jsonResponse(loadTasks().map(sanitizeScheduledTask));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (url.pathname.startsWith('/api/')) {
|
||||||
|
return jsonResponse({ error: 'Not found' }, { status: 404 });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!opts.staticDir) {
|
||||||
|
return new Response('Dashboard static directory is not configured', {
|
||||||
|
status: 404,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return serveStaticFile(opts.staticDir, url.pathname);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function startWebDashboardServer(
|
||||||
|
opts: {
|
||||||
|
enabled?: boolean;
|
||||||
|
host?: string;
|
||||||
|
port?: number;
|
||||||
|
staticDir?: string;
|
||||||
|
} = {},
|
||||||
|
): StartedWebDashboardServer | null {
|
||||||
|
const enabled = opts.enabled ?? WEB_DASHBOARD.enabled;
|
||||||
|
if (!enabled) return null;
|
||||||
|
|
||||||
|
const host = opts.host ?? WEB_DASHBOARD.host;
|
||||||
|
const port = opts.port ?? WEB_DASHBOARD.port;
|
||||||
|
const staticDir = opts.staticDir ?? WEB_DASHBOARD.staticDir;
|
||||||
|
const server = Bun.serve({
|
||||||
|
hostname: host,
|
||||||
|
port,
|
||||||
|
fetch: createWebDashboardHandler({ staticDir }),
|
||||||
|
});
|
||||||
|
const url = `http://${host}:${server.port}`;
|
||||||
|
|
||||||
|
logger.info({ url, staticDir }, 'Web dashboard started');
|
||||||
|
|
||||||
|
return {
|
||||||
|
url,
|
||||||
|
stop: () => server.stop(true),
|
||||||
|
};
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user