Overhaul dashboard UX (two-pane rooms, live turn persistence, react-markdown) (#42)
* Overhaul dashboard UX with two-pane rooms and live turn persistence - Two-pane room board (Discord-style list + detail) with sender role colors, inbox attention badges, and per-role left-border sectioning - Settings page: nickname + language extracted to /#/settings; sidebar freed up for mini stats (rooms, queue, inbox) and nav badges - react-markdown + remark-gfm for message body (tables, code, links, proper line breaks); unified timeline merges outputs and human messages - Live turn elapsed counter ticks every second; paused state preserved across service restarts via new progress_text column on paired_turns (migration 015) so in-flight context survives reboots - Batched /api/rooms-timeline endpoint with server-side stale-while- revalidate cache + gzip; cold ~3s -> warm ~10ms - Optimistic UI for outgoing messages with rollback on failure; nickname propagated to sender_name on send - Density sweep: tighter paddings, radii, font sizes; tokens align across panel/card/sidebar/timeline Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * Strip redundant LIVE/intent/state labels from turn header The left dot already signals LIVE and the role chip already shows the turn role; the standalone LIVE label, intentKind em, and state pill were repeating that information. Keep only role chip, time, elapsed, and a "중단됨" label when paused. Also picks up Prettier-formatted long lines in i18n and backend files left over from the previous commit. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * Convert sidebar to lucide-react icon rail and fix optimistic UI - Replace 168px text-label sidebar with 56px flush-left icon rail (lucide-react icons: Gauge/Inbox/Activity/MessageSquare/Clock/Settings) Discord/Slack pattern: rail against left edge, no centered shell margin - Optimistic UI now survives polling: pending messages tracked in a separate state and merged into the timeline render; an effect prunes pending entries once the server snapshot confirms them by content match - LIVE elapsed counter shows seconds at all granularities (formatLiveElapsed uses m+s/h+m+s instead of dropping seconds after 60s) - Refresh button shows spin animation while busy Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * Make Rooms the default landing view Rooms is the primary surface (chat-style live agent flow), so reorder nav (Rooms first) and switch DEFAULT_VIEW from inbox to rooms. The topbar fallback label also changes from 'usage' to 'rooms'. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * Submit room message on Enter, allow Shift+Enter for newline IME composition (event.nativeEvent.isComposing) is excluded so that mid-Hangul Enter does not trigger send. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * Viewport-lock rooms view to remove double scroll The rooms view now fills the viewport (height: 100vh - topbar) with list and detail each scrolling in their own column. The page itself no longer scrolls when the detail thread grows long, so the wheel scrolls whichever column the cursor is over. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * Flatten rooms view, single-line compose with icon send button Drop nested card chrome on rooms view: outer panel now has no padding, border, or background so the rail / list / detail sit flush against the page. Internal panes (rooms-list, rooms-detail) lose their own borders and rely on a single divider line for separation. Compose textarea is single-row by default (rows=1) and grows up to ~5 lines; the send button is icon-only (Send from lucide-react), fixed 36x36, pinned to the bottom of the detail pane. The redundant '메시지' h4 and the 세부 fold are removed — the room title in the header already establishes context. Detail pane is now a flex column where only the timeline scrolls; the head sticks at top and the compose pins at bottom (messenger pattern), so the input is always reachable. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * Fix rooms layout: viewport lock, restore panel tone, ensure thread scrolls - Lock body to viewport height (overflow:hidden) only when rooms view is active, via body:has(.view-stack.view-rooms). Section nav becomes static so the layout fits exactly in the viewport instead of being clipped under the sticky header. - Restore subtle panel background and border on .rooms-list and .rooms-detail so chat content has tonal contrast against the page bg (pure black was unreadable). Outer panel chrome stays flat. - Make the thread section the only flex:1 child of the pinned card so it actually scrolls when content overflows; everything else (head, meta, current turn, watcher fold, compose) stays flex:none. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * Make rooms-detail itself scroll with sticky head and compose Drop the brittle flex-column-with-1fr-child setup; just let .rooms-detail be overflow-y:auto and pin .room-card-head to top and .room-compose-section to bottom via position:sticky. The head and compose stay visible while the thread between them scrolls naturally. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * Match compose-section background to panel The sticky compose bar was using var(--bg) (near-black) which jarred against the surrounding panel tone. Use var(--panel) so it sits continuously with the room detail. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * Fix compose full-width and hide internal IPC payloads - The compose section's full-width grid was being clobbered by the later .room-section { display: grid } rule (same specificity, source order won). Bump specificity to .room-section.room-compose-section with display: block so the inner form/textarea fills the panel. - Filter out internal protocol payloads from the timeline: isInternalProtocolPayload() detects {"author":..."recipient":...} shaped agent IPC messages and excludes them from humanMessages and outputEntries. These are routing metadata, not human-readable chat. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * Tighten rooms width: explicit 100% on detail/card, responsive list - Make rooms-list responsive (clamp(180px, 16vw, 240px)) so on smaller screens the chat area gets more room - Add explicit width: 100% to rooms-twopane, rooms-detail, and the pinned room-card-v2 so nothing collapses to content-size Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * Remove timeline left indent and broaden IPC payload filter - Drop the 3px colored left border + 12px left padding on each timeline item; messages were starting 29px from the panel edge (3 border + 12 item padding + 14 thread padding). Role colors are already conveyed by the role chip, so the border is redundant. - Tighten thread-section padding from 14px to 12px to match compose. - Broaden isInternalProtocolPayload to also catch <sub-agent-prompt>, <tool-call>, <internal> wrapped payloads — these were leaking into the visible thread when the agent emitted them as message content. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * Restore role color borders and tighten left padding - Bring back the role-colored left border on timeline items (2px now, was 3px) so owner/reviewer/arbiter/human are visually distinct - Tighten left-side spacing across the chat panel: thread section padding-left 12 -> 4, item padding-left 12 -> 8, card head padding-left 14 -> 8, compose-section padding-left 12 -> 4. Content now starts ~10px from the detail panel's inner edge instead of ~30px Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * Unclamp output text in unified timeline + add measure debug toggle - The legacy .room-output-item p / .room-message-item p / .room-latest-output p rules used -webkit-line-clamp:2/4 to truncate previews. The unified timeline doesn't use those classes so it shouldn't be clamped, but add a defensive override on .room-timeline-body .parsed-body that unclamps and clears max-height in case any sibling rule leaks in. - Add ?debug=1 (outline panels) and ?measure=1 (overlay container x/w in viewport coords) toggles via App.tsx so we can diagnose layout issues without npm-installing devtools. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * Tighten rooms layout left/right margins Drop view-panel and section-nav left margin from 12px to 8px. Removes the visible empty band between icon-rail and rooms-list (was 12px, now 8px). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * Remove internal left padding in rooms-detail panel The detail panel had 4-8px of left padding inside the thread and compose sections, leaving an empty band on the left. Drop padding-left to 0 on .room-thread-section and .room-compose-section. Timeline items keep their own 8px padding-left (for the role border + text gap), and the textarea inherits the same flush-left starting position as messages — they align vertically now. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * Fix 26px left empty band: zero out .room-expanded padding in pinned mode Root cause located via pixel scan + DOM dump: the legacy .room-expanded wrapper (used by the click-to-expand layout) has padding: 14px 0 4px 26px to align expanded content with the head's pulse-dot column. In the pinned twopane layout it was leaving a 26px empty band between the panel border and the chat content. Override .room-expanded inside .room-card-v2.is-pinned to zero padding/margin/border-top/gap, so thread-section and compose-section align flush at the panel's inner left edge. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * Add Playwright + dash-inspect helper script Playwright (chromium) is installed so future layout debugging can use real DOM/computed-style queries instead of pixel scans on screenshots. Helper commands: bun scripts/dash-inspect.ts shot [route] bun scripts/dash-inspect.ts measure '<selector>' bun scripts/dash-inspect.ts eval '<expr>' bun scripts/dash-inspect.ts dom '<selector>' bun scripts/dash-inspect.ts click '<selector>' Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * Auto-scroll chat to bottom on send and on new messages - When the user sends a message, scroll the rooms-detail panel to the very bottom over 4 frames (covers the post-render layout settle). - Add a near-bottom tracker: a ref flips false when the user scrolls more than 80px from the bottom, true otherwise. The auto-follow effect (triggered when messages/outputs/pending counts change) only scrolls if the user was already near the bottom — so reading scrollback doesn't get yanked. - Initial room selection and any new content added while at-bottom always shows the latest message. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * Add Settings page: model role config + multi-account management Backend: - src/settings-store.ts: filesystem reader for Claude (~/.claude, ~/.claude-accounts/N) and Codex (~/.codex, ~/.codex-accounts/N) account directories. Returns safe metadata only (expiresAt, scopes, subscriptionType, planType, accountId from JWT) — never raw tokens. Atomic .env writer for OWNER/REVIEWER/ARBITER MODEL/EFFORT pairs. Account directory deletion (refuses index 0). Claude account add via pasted OAuth JWT (decodes exp/sub from payload, writes new ~/.claude-accounts/N/.credentials.json). - src/web-dashboard-server.ts: new endpoints GET /api/settings/accounts GET /api/settings/models PUT /api/settings/models (PATCH alias) POST /api/settings/accounts/claude (body: { token }) DELETE /api/settings/accounts/{provider}/{index} Frontend: - apps/dashboard/src/api.ts: typed clients for the new endpoints. - apps/dashboard/src/App.tsx: SettingsPanel split into 일반 / 모델 / 계정. Model section: editable owner/reviewer/arbiter model+effort fields with dirty tracking and a save button. Account section: per-provider list with delete buttons (default index 0 protected) and a token-paste field for adding a Claude account. Both sections expose a "스택 재시작" button that calls the existing /api/services/stack/actions endpoint with a confirmation prompt — changes only take effect after restart. UX scope: phase B (model edit + account delete) + phase D (account add via paste) per the recommendation; full OAuth flow not included. Restart is gated behind a confirm() since it kills running agents. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * Format: Prettier follow-up on settings PR Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * Show cron / external-trigger messages inline (separate from CI watcher) trusted_external_bot messages (Sentry Cron, GitHub webhook triggers, etc.) were lumped into the CI-watcher fold and hidden by default. Split them: WATCHER_RE now only matches actual CI status messages (Build/Deploy/Lint/Release/[CI]/[Watcher]/GitHub Actions on bot kind), so the watcher fold stays focused on CI noise. trusted_external_bot messages now get rendered inline in the unified timeline as cronMessages (sourceKind preserved). senderRoleClass returns 'role-cron' for senders matching cron/sentry/webhook so the chip and left border show in muted gray to visually distinguish from agent and human messages. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * Live progress fallback: scan TASK_STATUS messages for current turn only When paired_turns.progress_text is empty (codex runner doesn't always populate it in time), fall back to scanning the room's messages table for ones starting with the TASK_STATUS_MESSAGE_PREFIX (the invisible 3-char prefix used on Discord live-edit messages). Strip the prefix and the trailing elapsed indicator (e.g. "\n\n12초"), then render in the LIVE timeline entry. Filters: - timestamp must be >= turn.createdAt (don't show pre-turn history) - sender role must match the current turn's role (don't show a stale reviewer status when owner is now speaking) Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * Server fallback: derive live progressText from messages when column empty Keep paired_turns.progress_text as the primary source (populated by recordTurnProgress in MessageTurnController). When the column is empty on an active turn — happens for codex turns where the controller path isn't fully wired, or any path where ingestion lags — scan the messages table for the most recent TASK_STATUS_MESSAGE_PREFIX-prefixed message that belongs to the current turn (sender role match + timestamp >= turn.createdAt) and use that as progressText. This makes the dashboard converge with Discord without requiring the column to be reliably written: if Discord shows a live edit, the ingest pipeline writes it to messages, and we surface it. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * Poll room timelines every 1.5s on rooms view (was every 15s) The rooms-detail thread was reusing the 15s overview refresh tick, which made the chat lag noticeably behind Discord. Add a dedicated setInterval(1500ms) fetch on the rooms view (cleared when leaving), with an in-flight guard so overlapping ticks don't pile up. Backend already has a 2s stale-while-revalidate cache so this stays cheap. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * Real-time rooms updates via Server-Sent Events Backend: GET /api/stream emits text/event-stream with 'rooms-timeline' events whenever the server's roomsTimelineCache rebuilds (key change). A 1.5s tick checks the cache; if builtAt advanced, push the new JSON, otherwise send a SSE comment heartbeat ': ping ts\n\n' to keep proxies from idle-closing the connection. AbortSignal handler closes the ReadableStream when the client disconnects. Frontend: replace setInterval(1500ms) on rooms view with EventSource. Initial fetch primes the data so first paint isn't blank, then SSE takes over for instant updates. EventSource auto-reconnects on disconnect; while it's flapping, a 2s polling fallback runs so data keeps flowing. When SSE recovers (next 'rooms-timeline' event) the polling fallback is cleared. Latency: previously 0–1.5s polling lag; now ~tick latency (50–150ms) once cache rebuilds. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * Add breathing room between timeline header and body Increase room-timeline-item gap from 2px to 6px so the role chip / timestamp line has visible breathing room above the parsed body — header → body relationship reads more clearly. Bump vertical padding from 6px to 8px and item separator from 1px to 2px to match. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * Hide the 2px processing bar in pinned twopane to free role-color borders .room-card-v2.status-processing::before draws a 2px green bar at the card's left edge. In pinned twopane mode it sits right on top of each timeline item's role-colored left border (owner blue / reviewer orange / arbiter green / human yellow), masking them. The header's pulse dot already conveys "processing", so suppress the bar in pinned mode and let the role colors show through. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * Polish room compose: tighter pill input + override global button min-height The 48px button min-height global rule was forcing the compose grid track to 48px, pushing the textarea content visibly down inside the pill. Adding min-height: 0 on the compose button lets the track collapse to ~29px so the placeholder lines up with the send button vertically. Also slimmed paddings (form 3px vert, textarea 4px vert) and added bottom breathing room on the compose section (8px → 14px) at user request. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * Format styles.css to multi-line transition values Prettier 3.8.1 prefers multi-line transition shorthand. CI was failing format:check on these single-line transitions in the room-compose rules. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -131,6 +131,8 @@ export interface DashboardRoomActivity {
|
||||
updatedAt: string;
|
||||
completedAt: string | null;
|
||||
lastError: string | null;
|
||||
progressText: string | null;
|
||||
progressUpdatedAt: string | null;
|
||||
} | null;
|
||||
outputs: Array<{
|
||||
id: number;
|
||||
@@ -239,6 +241,14 @@ export async function fetchRoomTimeline(
|
||||
);
|
||||
}
|
||||
|
||||
export async function fetchRoomsTimelineBatch(): Promise<
|
||||
Record<string, DashboardRoomActivity>
|
||||
> {
|
||||
return fetchJson<Record<string, DashboardRoomActivity>>(
|
||||
'/api/rooms-timeline',
|
||||
);
|
||||
}
|
||||
|
||||
export async function runScheduledTaskAction(
|
||||
taskId: string,
|
||||
action: DashboardTaskAction,
|
||||
@@ -324,9 +334,108 @@ export async function sendRoomMessage(
|
||||
roomJid: string,
|
||||
text: string,
|
||||
requestId: string,
|
||||
nickname?: string | null,
|
||||
): Promise<{ ok: true; id: string; queued: boolean }> {
|
||||
return postJson(`/api/rooms/${encodeURIComponent(roomJid)}/messages`, {
|
||||
requestId,
|
||||
text,
|
||||
nickname: nickname ?? undefined,
|
||||
});
|
||||
}
|
||||
|
||||
export interface ClaudeAccountSummary {
|
||||
index: number;
|
||||
expiresAt: number | null;
|
||||
scopes: string[];
|
||||
subscriptionType?: string;
|
||||
rateLimitTier?: string;
|
||||
exists: boolean;
|
||||
}
|
||||
|
||||
export interface CodexAccountSummary {
|
||||
index: number;
|
||||
accountId: string | null;
|
||||
planType: string | null;
|
||||
subscriptionUntil: string | null;
|
||||
exists: boolean;
|
||||
}
|
||||
|
||||
export interface ModelRoleConfig {
|
||||
model: string;
|
||||
effort: string;
|
||||
}
|
||||
|
||||
export interface ModelConfigSnapshot {
|
||||
owner: ModelRoleConfig;
|
||||
reviewer: ModelRoleConfig;
|
||||
arbiter: ModelRoleConfig;
|
||||
}
|
||||
|
||||
export async function fetchAccounts(): Promise<{
|
||||
claude: ClaudeAccountSummary[];
|
||||
codex: CodexAccountSummary[];
|
||||
}> {
|
||||
return fetchJson('/api/settings/accounts');
|
||||
}
|
||||
|
||||
export async function fetchModelConfig(): Promise<ModelConfigSnapshot> {
|
||||
return fetchJson('/api/settings/models');
|
||||
}
|
||||
|
||||
export async function updateModels(
|
||||
input: Partial<{
|
||||
owner: Partial<ModelRoleConfig>;
|
||||
reviewer: Partial<ModelRoleConfig>;
|
||||
arbiter: Partial<ModelRoleConfig>;
|
||||
}>,
|
||||
): Promise<ModelConfigSnapshot> {
|
||||
const response = await fetch('/api/settings/models', {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
accept: 'application/json',
|
||||
'content-type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(input),
|
||||
});
|
||||
if (!response.ok) {
|
||||
let msg = `update models failed: ${response.status}`;
|
||||
try {
|
||||
const payload = (await response.json()) as { error?: string };
|
||||
if (payload.error) msg = payload.error;
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
throw new Error(msg);
|
||||
}
|
||||
return (await response.json()) as ModelConfigSnapshot;
|
||||
}
|
||||
|
||||
export async function deleteAccount(
|
||||
provider: 'claude' | 'codex',
|
||||
index: number,
|
||||
): Promise<{ ok: true; provider: string; index: number }> {
|
||||
const response = await fetch(`/api/settings/accounts/${provider}/${index}`, {
|
||||
method: 'DELETE',
|
||||
});
|
||||
if (!response.ok) {
|
||||
let msg = `delete account failed: ${response.status}`;
|
||||
try {
|
||||
const payload = (await response.json()) as { error?: string };
|
||||
if (payload.error) msg = payload.error;
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
throw new Error(msg);
|
||||
}
|
||||
return (await response.json()) as {
|
||||
ok: true;
|
||||
provider: string;
|
||||
index: number;
|
||||
};
|
||||
}
|
||||
|
||||
export async function addClaudeAccount(
|
||||
token: string,
|
||||
): Promise<{ ok: true; index: number; accountId: string | null }> {
|
||||
return postJson('/api/settings/accounts/claude', { token });
|
||||
}
|
||||
|
||||
@@ -25,12 +25,26 @@ export interface Messages {
|
||||
usage: string;
|
||||
rooms: string;
|
||||
scheduled: string;
|
||||
settings: string;
|
||||
};
|
||||
language: {
|
||||
label: string;
|
||||
};
|
||||
settings: {
|
||||
title: string;
|
||||
nicknameLabel: string;
|
||||
nicknamePlaceholder: string;
|
||||
nicknameHelp: string;
|
||||
languageLabel: string;
|
||||
};
|
||||
error: {
|
||||
api: string;
|
||||
network: string;
|
||||
timeout: string;
|
||||
server: string;
|
||||
notFound: string;
|
||||
auth: string;
|
||||
unknown: string;
|
||||
};
|
||||
control: {
|
||||
aria: string;
|
||||
@@ -146,6 +160,8 @@ export interface Messages {
|
||||
agent: string;
|
||||
status: string;
|
||||
queue: string;
|
||||
queueWaitingMessages: string;
|
||||
tasks: string;
|
||||
elapsed: string;
|
||||
activity: string;
|
||||
loadingActivity: string;
|
||||
@@ -169,6 +185,12 @@ export interface Messages {
|
||||
messagePlaceholder: string;
|
||||
send: string;
|
||||
sending: string;
|
||||
error: string;
|
||||
filterAll: string;
|
||||
sortRecent: string;
|
||||
sortName: string;
|
||||
sortQueue: string;
|
||||
sortLabel: string;
|
||||
};
|
||||
usage: {
|
||||
empty: string;
|
||||
@@ -316,12 +338,26 @@ export const messages = {
|
||||
usage: '사용량',
|
||||
rooms: '룸',
|
||||
scheduled: '예약',
|
||||
settings: '설정',
|
||||
},
|
||||
language: {
|
||||
label: '언어',
|
||||
},
|
||||
settings: {
|
||||
title: '설정',
|
||||
nicknameLabel: '닉네임',
|
||||
nicknamePlaceholder: 'Web Dashboard',
|
||||
nicknameHelp: '룸에서 메시지 보낼 때 표시될 이름',
|
||||
languageLabel: '언어',
|
||||
},
|
||||
error: {
|
||||
api: 'API 오류',
|
||||
api: '문제 발생',
|
||||
network: '네트워크 연결 확인 후 다시 시도해주세요.',
|
||||
timeout: '응답 시간이 초과됐어요. 잠시 후 다시 시도해주세요.',
|
||||
server: '서버에 일시적인 문제가 있어요. 잠시 후 다시 시도해주세요.',
|
||||
notFound: '요청한 정보를 찾을 수 없어요.',
|
||||
auth: '권한이 없어요. 다시 로그인해주세요.',
|
||||
unknown: '알 수 없는 오류가 발생했어요.',
|
||||
},
|
||||
control: {
|
||||
aria: '컨트롤 플레인 요약',
|
||||
@@ -376,9 +412,9 @@ export const messages = {
|
||||
queue: '큐',
|
||||
ciFailures: 'CI 실패',
|
||||
affectedServices: '이상 서비스',
|
||||
restart: 'Restart',
|
||||
restartStack: 'Restart stack',
|
||||
restarting: 'Restarting...',
|
||||
restart: '재시작',
|
||||
restartStack: '스택 재시작',
|
||||
restarting: '재시작 중...',
|
||||
restartHint: '서비스 전체 재시작',
|
||||
confirmRestart: 'Restart stack now?',
|
||||
restartLog: 'Restart log',
|
||||
@@ -437,6 +473,8 @@ export const messages = {
|
||||
agent: '에이전트',
|
||||
status: '상태',
|
||||
queue: '큐',
|
||||
queueWaitingMessages: '대기 메시지',
|
||||
tasks: '태스크',
|
||||
elapsed: '경과',
|
||||
activity: '진행',
|
||||
loadingActivity: '진행 로딩 중',
|
||||
@@ -456,10 +494,16 @@ export const messages = {
|
||||
messageHistory: '메시지 기록',
|
||||
noMessages: '메시지 없음',
|
||||
details: '세부',
|
||||
message: 'Message',
|
||||
messagePlaceholder: 'Type request...',
|
||||
send: 'Send',
|
||||
sending: 'Sending',
|
||||
message: '메시지',
|
||||
messagePlaceholder: '요청 입력...',
|
||||
send: '전송',
|
||||
sending: '전송 중...',
|
||||
error: '오류',
|
||||
filterAll: '전체',
|
||||
sortRecent: '최근 활동',
|
||||
sortName: '이름순',
|
||||
sortQueue: '큐 많은 순',
|
||||
sortLabel: '정렬',
|
||||
},
|
||||
usage: {
|
||||
empty: '사용량 스냅샷 없음. 수집기 확인.',
|
||||
@@ -591,12 +635,26 @@ export const messages = {
|
||||
usage: 'Usage',
|
||||
rooms: 'Rooms',
|
||||
scheduled: 'Scheduled',
|
||||
settings: 'Settings',
|
||||
},
|
||||
language: {
|
||||
label: 'Language',
|
||||
},
|
||||
settings: {
|
||||
title: 'Settings',
|
||||
nicknameLabel: 'Nickname',
|
||||
nicknamePlaceholder: 'Web Dashboard',
|
||||
nicknameHelp: 'Name shown when sending messages from this dashboard',
|
||||
languageLabel: 'Language',
|
||||
},
|
||||
error: {
|
||||
api: 'API error',
|
||||
api: 'Something went wrong',
|
||||
network: 'Check your network connection and try again.',
|
||||
timeout: 'The request timed out. Try again in a moment.',
|
||||
server: 'Server hiccup. Try again in a bit.',
|
||||
notFound: "We couldn't find what you asked for.",
|
||||
auth: 'Sign in again to continue.',
|
||||
unknown: 'An unexpected error occurred.',
|
||||
},
|
||||
control: {
|
||||
aria: 'Control plane summary',
|
||||
@@ -712,6 +770,8 @@ export const messages = {
|
||||
agent: 'agent',
|
||||
status: 'status',
|
||||
queue: 'queue',
|
||||
queueWaitingMessages: 'pending messages',
|
||||
tasks: 'tasks',
|
||||
elapsed: 'elapsed',
|
||||
activity: 'activity',
|
||||
loadingActivity: 'Loading activity',
|
||||
@@ -735,6 +795,12 @@ export const messages = {
|
||||
messagePlaceholder: 'Type request...',
|
||||
send: 'Send',
|
||||
sending: 'Sending',
|
||||
error: 'Error',
|
||||
filterAll: 'All',
|
||||
sortRecent: 'Recent activity',
|
||||
sortName: 'Name',
|
||||
sortQueue: 'Queue size',
|
||||
sortLabel: 'Sort',
|
||||
},
|
||||
usage: {
|
||||
empty: 'No usage snapshot. Check collector.',
|
||||
@@ -866,12 +932,26 @@ export const messages = {
|
||||
usage: '用量',
|
||||
rooms: '房间',
|
||||
scheduled: '计划',
|
||||
settings: '设置',
|
||||
},
|
||||
language: {
|
||||
label: '语言',
|
||||
},
|
||||
settings: {
|
||||
title: '设置',
|
||||
nicknameLabel: '昵称',
|
||||
nicknamePlaceholder: 'Web Dashboard',
|
||||
nicknameHelp: '从此面板发送消息时显示的名称',
|
||||
languageLabel: '语言',
|
||||
},
|
||||
error: {
|
||||
api: 'API 错误',
|
||||
api: '发生问题',
|
||||
network: '请检查网络连接后重试。',
|
||||
timeout: '请求超时,请稍后重试。',
|
||||
server: '服务器暂时出现问题,请稍后重试。',
|
||||
notFound: '找不到所请求的信息。',
|
||||
auth: '没有权限,请重新登录。',
|
||||
unknown: '发生未知错误。',
|
||||
},
|
||||
control: {
|
||||
aria: '控制平面摘要',
|
||||
@@ -987,6 +1067,8 @@ export const messages = {
|
||||
agent: '代理',
|
||||
status: '状态',
|
||||
queue: '队列',
|
||||
queueWaitingMessages: '待处理消息',
|
||||
tasks: '任务',
|
||||
elapsed: '耗时',
|
||||
activity: '进展',
|
||||
loadingActivity: '正在加载进展',
|
||||
@@ -1007,9 +1089,15 @@ export const messages = {
|
||||
noMessages: '暂无消息',
|
||||
details: '详情',
|
||||
message: 'Message',
|
||||
messagePlaceholder: 'Type request...',
|
||||
send: 'Send',
|
||||
sending: 'Sending',
|
||||
messagePlaceholder: '输入请求...',
|
||||
send: '发送',
|
||||
sending: '发送中...',
|
||||
error: '错误',
|
||||
filterAll: '全部',
|
||||
sortRecent: '最近活动',
|
||||
sortName: '名称',
|
||||
sortQueue: '队列大小',
|
||||
sortLabel: '排序',
|
||||
},
|
||||
usage: {
|
||||
empty: '暂无用量快照。检查采集器。',
|
||||
@@ -1141,12 +1229,28 @@ export const messages = {
|
||||
usage: '使用量',
|
||||
rooms: 'ルーム',
|
||||
scheduled: '予定',
|
||||
settings: '設定',
|
||||
},
|
||||
language: {
|
||||
label: '言語',
|
||||
},
|
||||
settings: {
|
||||
title: '設定',
|
||||
nicknameLabel: 'ニックネーム',
|
||||
nicknamePlaceholder: 'Web Dashboard',
|
||||
nicknameHelp: 'このダッシュボードからメッセージを送る時に表示される名前',
|
||||
languageLabel: '言語',
|
||||
},
|
||||
error: {
|
||||
api: 'API エラー',
|
||||
api: '問題が発生しました',
|
||||
network: 'ネットワーク接続を確認してから再試行してください。',
|
||||
timeout:
|
||||
'リクエストがタイムアウトしました。少し経ってから再試行してください。',
|
||||
server:
|
||||
'サーバーに一時的な問題があります。少し経ってから再試行してください。',
|
||||
notFound: 'お探しの情報が見つかりませんでした。',
|
||||
auth: '権限がありません。再ログインしてください。',
|
||||
unknown: '不明なエラーが発生しました。',
|
||||
},
|
||||
control: {
|
||||
aria: 'コントロールプレーン概要',
|
||||
@@ -1262,6 +1366,8 @@ export const messages = {
|
||||
agent: 'エージェント',
|
||||
status: '状態',
|
||||
queue: 'キュー',
|
||||
queueWaitingMessages: '待機メッセージ',
|
||||
tasks: 'タスク',
|
||||
elapsed: '経過',
|
||||
activity: '進行',
|
||||
loadingActivity: '進行を読み込み中',
|
||||
@@ -1281,10 +1387,16 @@ export const messages = {
|
||||
messageHistory: 'メッセージ履歴',
|
||||
noMessages: 'メッセージなし',
|
||||
details: '詳細',
|
||||
message: 'Message',
|
||||
messagePlaceholder: 'Type request...',
|
||||
send: 'Send',
|
||||
sending: 'Sending',
|
||||
message: 'メッセージ',
|
||||
messagePlaceholder: '依頼を入力...',
|
||||
send: '送信',
|
||||
sending: '送信中...',
|
||||
error: 'エラー',
|
||||
filterAll: '全て',
|
||||
sortRecent: '最近の活動',
|
||||
sortName: '名前順',
|
||||
sortQueue: 'キュー順',
|
||||
sortLabel: '並び替え',
|
||||
},
|
||||
usage: {
|
||||
empty: '使用量スナップショットなし。収集器を確認。',
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -9,10 +9,10 @@ export default defineConfig({
|
||||
emptyOutDir: true,
|
||||
},
|
||||
server: {
|
||||
host: '127.0.0.1',
|
||||
host: process.env.VITE_DEV_HOST ?? '127.0.0.1',
|
||||
port: 5174,
|
||||
proxy: {
|
||||
'/api': 'http://127.0.0.1:8734',
|
||||
'/api': process.env.VITE_API_TARGET ?? 'http://127.0.0.1:8734',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
214
bun.lock
214
bun.lock
@@ -9,10 +9,13 @@
|
||||
"cron-parser": "^5.5.0",
|
||||
"discord.js": "^14.18.0",
|
||||
"ejclaw-runners-shared": "file:./runners/shared",
|
||||
"lucide-react": "^1.11.0",
|
||||
"pino": "^9.6.0",
|
||||
"pino-pretty": "^13.0.0",
|
||||
"react": "^19.2.5",
|
||||
"react-dom": "^19.2.5",
|
||||
"react-markdown": "^10.1.0",
|
||||
"remark-gfm": "^4.0.1",
|
||||
"yaml": "^2.8.2",
|
||||
"zod": "^4.3.6",
|
||||
},
|
||||
@@ -26,6 +29,7 @@
|
||||
"@vitest/coverage-v8": "^4.0.18",
|
||||
"better-sqlite3": "^12.8.0",
|
||||
"husky": "^9.1.7",
|
||||
"playwright": "^1.59.1",
|
||||
"prettier": "^3.8.1",
|
||||
"typescript": "^5.7.0",
|
||||
"vite": "^8.0.10",
|
||||
@@ -228,18 +232,32 @@
|
||||
|
||||
"@types/chai": ["@types/chai@5.2.3", "", { "dependencies": { "@types/deep-eql": "*", "assertion-error": "^2.0.1" } }, "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA=="],
|
||||
|
||||
"@types/debug": ["@types/debug@4.1.13", "", { "dependencies": { "@types/ms": "*" } }, "sha512-KSVgmQmzMwPlmtljOomayoR89W4FynCAi3E8PPs7vmDVPe84hT+vGPKkJfThkmXs0x0jAaa9U8uW8bbfyS2fWw=="],
|
||||
|
||||
"@types/deep-eql": ["@types/deep-eql@4.0.2", "", {}, "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw=="],
|
||||
|
||||
"@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="],
|
||||
|
||||
"@types/estree-jsx": ["@types/estree-jsx@1.0.5", "", { "dependencies": { "@types/estree": "*" } }, "sha512-52CcUVNFyfb1A2ALocQw/Dd1BQFNmSdkuC3BkZ6iqhdMfQz7JWOFRuJFloOzjk+6WijU56m9oKXFAXc7o3Towg=="],
|
||||
|
||||
"@types/hast": ["@types/hast@3.0.4", "", { "dependencies": { "@types/unist": "*" } }, "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ=="],
|
||||
|
||||
"@types/mdast": ["@types/mdast@4.0.4", "", { "dependencies": { "@types/unist": "*" } }, "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA=="],
|
||||
|
||||
"@types/ms": ["@types/ms@2.1.0", "", {}, "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA=="],
|
||||
|
||||
"@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/unist": ["@types/unist@3.0.3", "", {}, "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q=="],
|
||||
|
||||
"@types/ws": ["@types/ws@8.18.1", "", { "dependencies": { "@types/node": "*" } }, "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg=="],
|
||||
|
||||
"@ungap/structured-clone": ["@ungap/structured-clone@1.3.0", "", {}, "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g=="],
|
||||
|
||||
"@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=="],
|
||||
@@ -272,6 +290,8 @@
|
||||
|
||||
"atomic-sleep": ["atomic-sleep@1.0.0", "", {}, "sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ=="],
|
||||
|
||||
"bail": ["bail@2.0.2", "", {}, "sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw=="],
|
||||
|
||||
"base64-js": ["base64-js@1.5.1", "", {}, "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="],
|
||||
|
||||
"better-sqlite3": ["better-sqlite3@12.8.0", "", { "dependencies": { "bindings": "^1.5.0", "prebuild-install": "^7.1.1" } }, "sha512-RxD2Vd96sQDjQr20kdP+F+dK/1OUNiVOl200vKBZY8u0vTwysfolF6Hq+3ZK2+h8My9YvZhHsF+RSGZW2VYrPQ=="],
|
||||
@@ -292,12 +312,24 @@
|
||||
|
||||
"call-bound": ["call-bound@1.0.4", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "get-intrinsic": "^1.3.0" } }, "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg=="],
|
||||
|
||||
"ccount": ["ccount@2.0.1", "", {}, "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg=="],
|
||||
|
||||
"chai": ["chai@6.2.2", "", {}, "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg=="],
|
||||
|
||||
"character-entities": ["character-entities@2.0.2", "", {}, "sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ=="],
|
||||
|
||||
"character-entities-html4": ["character-entities-html4@2.1.0", "", {}, "sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA=="],
|
||||
|
||||
"character-entities-legacy": ["character-entities-legacy@3.0.0", "", {}, "sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ=="],
|
||||
|
||||
"character-reference-invalid": ["character-reference-invalid@2.0.1", "", {}, "sha512-iBZ4F4wRbyORVsu0jPV7gXkOsGYjGHPmAyv+HiHG8gi5PtC9KI2j1+v8/tlibRvjoWX027ypmG/n0HtO5t7unw=="],
|
||||
|
||||
"chownr": ["chownr@1.1.4", "", {}, "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg=="],
|
||||
|
||||
"colorette": ["colorette@2.0.20", "", {}, "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w=="],
|
||||
|
||||
"comma-separated-tokens": ["comma-separated-tokens@2.0.3", "", {}, "sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg=="],
|
||||
|
||||
"content-disposition": ["content-disposition@1.0.1", "", {}, "sha512-oIXISMynqSqm241k6kcQ5UwttDILMK4BiurCfGEREw6+X9jkkpEe5T9FZaApyLGGOnFuyMWZpdolTXMtvEJ08Q=="],
|
||||
|
||||
"content-type": ["content-type@1.0.5", "", {}, "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA=="],
|
||||
@@ -318,14 +350,20 @@
|
||||
|
||||
"debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="],
|
||||
|
||||
"decode-named-character-reference": ["decode-named-character-reference@1.3.0", "", { "dependencies": { "character-entities": "^2.0.0" } }, "sha512-GtpQYB283KrPp6nRw50q3U9/VfOutZOe103qlN7BPP6Ad27xYnOIWv4lPzo8HCAL+mMZofJ9KEy30fq6MfaK6Q=="],
|
||||
|
||||
"decompress-response": ["decompress-response@6.0.0", "", { "dependencies": { "mimic-response": "^3.1.0" } }, "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ=="],
|
||||
|
||||
"deep-extend": ["deep-extend@0.6.0", "", {}, "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA=="],
|
||||
|
||||
"depd": ["depd@2.0.0", "", {}, "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw=="],
|
||||
|
||||
"dequal": ["dequal@2.0.3", "", {}, "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA=="],
|
||||
|
||||
"detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="],
|
||||
|
||||
"devlop": ["devlop@1.1.0", "", { "dependencies": { "dequal": "^2.0.0" } }, "sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA=="],
|
||||
|
||||
"discord-api-types": ["discord-api-types@0.38.41", "", {}, "sha512-yMECyR8j9c2fVTvCQ+Qc24pweYFIZk/XoxDOmt1UvPeSw5tK6gXBd/2hhP+FEAe9Y6ny8pRMaf618XDK4U53OQ=="],
|
||||
|
||||
"discord.js": ["discord.js@14.25.1", "", { "dependencies": { "@discordjs/builders": "^1.13.0", "@discordjs/collection": "1.5.3", "@discordjs/formatters": "^0.6.2", "@discordjs/rest": "^2.6.0", "@discordjs/util": "^1.2.0", "@discordjs/ws": "^1.2.3", "@sapphire/snowflake": "3.5.3", "discord-api-types": "^0.38.33", "fast-deep-equal": "3.1.3", "lodash.snakecase": "4.1.1", "magic-bytes.js": "^1.10.0", "tslib": "^2.6.3", "undici": "6.21.3" } }, "sha512-2l0gsPOLPs5t6GFZfQZKnL1OJNYFcuC/ETWsW4VtKVD/tg4ICa9x+jb9bkPffkMdRpRpuUaO/fKkHCBeiCKh8g=="],
|
||||
@@ -352,6 +390,10 @@
|
||||
|
||||
"escape-html": ["escape-html@1.0.3", "", {}, "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow=="],
|
||||
|
||||
"escape-string-regexp": ["escape-string-regexp@5.0.0", "", {}, "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw=="],
|
||||
|
||||
"estree-util-is-identifier-name": ["estree-util-is-identifier-name@3.0.0", "", {}, "sha512-hFtqIDZTIUZ9BXLb8y4pYGyk6+wekIivNVTcmvk8NoOh+VeRn5y6cEHzbURrWbfp1fIqdVipilzj+lfaadNZmg=="],
|
||||
|
||||
"estree-walker": ["estree-walker@3.0.3", "", { "dependencies": { "@types/estree": "^1.0.0" } }, "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g=="],
|
||||
|
||||
"etag": ["etag@1.8.1", "", {}, "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg=="],
|
||||
@@ -368,6 +410,8 @@
|
||||
|
||||
"express-rate-limit": ["express-rate-limit@8.3.1", "", { "dependencies": { "ip-address": "10.1.0" }, "peerDependencies": { "express": ">= 4.11" } }, "sha512-D1dKN+cmyPWuvB+G2SREQDzPY1agpBIcTa9sJxOPMCNeH3gwzhqJRDWCXW3gg0y//+LQ/8j52JbMROWyrKdMdw=="],
|
||||
|
||||
"extend": ["extend@3.0.2", "", {}, "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g=="],
|
||||
|
||||
"fast-copy": ["fast-copy@4.0.2", "", {}, "sha512-ybA6PDXIXOXivLJK/z9e+Otk7ve13I4ckBvGO5I2RRmBU1gMHLVDJYEuJYhGwez7YNlYji2M2DvVU+a9mSFDlw=="],
|
||||
|
||||
"fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="],
|
||||
@@ -388,7 +432,7 @@
|
||||
|
||||
"fs-constants": ["fs-constants@1.0.0", "", {}, "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow=="],
|
||||
|
||||
"fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="],
|
||||
"fsevents": ["fsevents@2.3.2", "", { "os": "darwin" }, "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA=="],
|
||||
|
||||
"function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="],
|
||||
|
||||
@@ -408,12 +452,18 @@
|
||||
|
||||
"hasown": ["hasown@2.0.2", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ=="],
|
||||
|
||||
"hast-util-to-jsx-runtime": ["hast-util-to-jsx-runtime@2.3.6", "", { "dependencies": { "@types/estree": "^1.0.0", "@types/hast": "^3.0.0", "@types/unist": "^3.0.0", "comma-separated-tokens": "^2.0.0", "devlop": "^1.0.0", "estree-util-is-identifier-name": "^3.0.0", "hast-util-whitespace": "^3.0.0", "mdast-util-mdx-expression": "^2.0.0", "mdast-util-mdx-jsx": "^3.0.0", "mdast-util-mdxjs-esm": "^2.0.0", "property-information": "^7.0.0", "space-separated-tokens": "^2.0.0", "style-to-js": "^1.0.0", "unist-util-position": "^5.0.0", "vfile-message": "^4.0.0" } }, "sha512-zl6s8LwNyo1P9uw+XJGvZtdFF1GdAkOg8ujOw+4Pyb76874fLps4ueHXDhXWdk6YHQ6OgUtinliG7RsYvCbbBg=="],
|
||||
|
||||
"hast-util-whitespace": ["hast-util-whitespace@3.0.0", "", { "dependencies": { "@types/hast": "^3.0.0" } }, "sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw=="],
|
||||
|
||||
"help-me": ["help-me@5.0.0", "", {}, "sha512-7xgomUX6ADmcYzFik0HzAxh/73YlKR9bmFzf51CZwR+b6YtzU2m0u49hQCqV6SvlqIqsaxovfwdvbnsw3b/zpg=="],
|
||||
|
||||
"hono": ["hono@4.12.9", "", {}, "sha512-wy3T8Zm2bsEvxKZM5w21VdHDDcwVS1yUFFY6i8UobSsKfFceT7TOwhbhfKsDyx7tYQlmRM5FLpIuYvNFyjctiA=="],
|
||||
|
||||
"html-escaper": ["html-escaper@2.0.2", "", {}, "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg=="],
|
||||
|
||||
"html-url-attributes": ["html-url-attributes@3.0.1", "", {}, "sha512-ol6UPyBWqsrO6EJySPz2O7ZSr856WDrEzM5zMqp+FJJLGMW35cLYmmZnl0vztAZxRUoNZJFTCohfjuIJ8I4QBQ=="],
|
||||
|
||||
"http-errors": ["http-errors@2.0.1", "", { "dependencies": { "depd": "~2.0.0", "inherits": "~2.0.4", "setprototypeof": "~1.2.0", "statuses": "~2.0.2", "toidentifier": "~1.0.1" } }, "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ=="],
|
||||
|
||||
"husky": ["husky@9.1.7", "", { "bin": "bin.js" }, "sha512-5gs5ytaNjBrh5Ow3zrvdUUY+0VxIuWVL4i9irt6friV+BqdCfmV11CQTWMiBYWHbXhco+J1kHfTOUkePhCDvMA=="],
|
||||
@@ -426,10 +476,22 @@
|
||||
|
||||
"ini": ["ini@1.3.8", "", {}, "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew=="],
|
||||
|
||||
"inline-style-parser": ["inline-style-parser@0.2.7", "", {}, "sha512-Nb2ctOyNR8DqQoR0OwRG95uNWIC0C1lCgf5Naz5H6Ji72KZ8OcFZLz2P5sNgwlyoJ8Yif11oMuYs5pBQa86csA=="],
|
||||
|
||||
"ip-address": ["ip-address@10.1.0", "", {}, "sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q=="],
|
||||
|
||||
"ipaddr.js": ["ipaddr.js@1.9.1", "", {}, "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g=="],
|
||||
|
||||
"is-alphabetical": ["is-alphabetical@2.0.1", "", {}, "sha512-FWyyY60MeTNyeSRpkM2Iry0G9hpr7/9kD40mD/cGQEuilcZYS4okz8SN2Q6rLCJ8gbCt6fN+rC+6tMGS99LaxQ=="],
|
||||
|
||||
"is-alphanumerical": ["is-alphanumerical@2.0.1", "", { "dependencies": { "is-alphabetical": "^2.0.0", "is-decimal": "^2.0.0" } }, "sha512-hmbYhX/9MUMF5uh7tOXyK/n0ZvWpad5caBA17GsC6vyuCqaWliRG5K1qS9inmUhEMaOBIW7/whAnSwveW/LtZw=="],
|
||||
|
||||
"is-decimal": ["is-decimal@2.0.1", "", {}, "sha512-AAB9hiomQs5DXWcRB1rqsxGUstbRroFOPPVAomNk/3XHR5JyEZChOyTWe2oayKnsSsr/kcGqF+z6yuH6HHpN0A=="],
|
||||
|
||||
"is-hexadecimal": ["is-hexadecimal@2.0.1", "", {}, "sha512-DgZQp241c8oO6cA1SbTEWiXeoxV42vlcJxgH+B3hi1AiqqKruZR3ZGF8In3fj4+/y/7rHvlOZLZtgJ/4ttYGZg=="],
|
||||
|
||||
"is-plain-obj": ["is-plain-obj@4.1.0", "", {}, "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg=="],
|
||||
|
||||
"is-promise": ["is-promise@4.0.0", "", {}, "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ=="],
|
||||
|
||||
"isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="],
|
||||
@@ -478,6 +540,10 @@
|
||||
|
||||
"lodash.snakecase": ["lodash.snakecase@4.1.1", "", {}, "sha512-QZ1d4xoBHYUeuouhEq3lk3Uq7ldgyFXGBhg04+oRLnIz8o9T65Eh+8YdroUwn846zchkA9yDsDl5CVVaV2nqYw=="],
|
||||
|
||||
"longest-streak": ["longest-streak@3.1.0", "", {}, "sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g=="],
|
||||
|
||||
"lucide-react": ["lucide-react@1.11.0", "", { "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-UOhjdztXCgdBReRcIhsvz2siIBogfv/lhJEIViCpLt924dO+GDms9T7DNoucI23s6kEPpe988m5N0D2ajnzb2g=="],
|
||||
|
||||
"luxon": ["luxon@3.7.2", "", {}, "sha512-vtEhXh/gNjI9Yg1u4jX/0YVPMvxzHuGgCm6tC5kZyb08yjGWGnqAjGJvcXbqQR2P3MyMEFnRbpcdFS6PBcLqew=="],
|
||||
|
||||
"magic-bytes.js": ["magic-bytes.js@1.13.0", "", {}, "sha512-afO2mnxW7GDTXMm5/AoN1WuOcdoKhtgXjIvHmobqTD1grNplhGdv3PFOyjCVmrnOZBIT/gD/koDKpYG+0mvHcg=="],
|
||||
@@ -488,12 +554,100 @@
|
||||
|
||||
"make-dir": ["make-dir@4.0.0", "", { "dependencies": { "semver": "^7.5.3" } }, "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw=="],
|
||||
|
||||
"markdown-table": ["markdown-table@3.0.4", "", {}, "sha512-wiYz4+JrLyb/DqW2hkFJxP7Vd7JuTDm77fvbM8VfEQdmSMqcImWeeRbHwZjBjIFki/VaMK2BhFi7oUUZeM5bqw=="],
|
||||
|
||||
"math-intrinsics": ["math-intrinsics@1.1.0", "", {}, "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="],
|
||||
|
||||
"mdast-util-find-and-replace": ["mdast-util-find-and-replace@3.0.2", "", { "dependencies": { "@types/mdast": "^4.0.0", "escape-string-regexp": "^5.0.0", "unist-util-is": "^6.0.0", "unist-util-visit-parents": "^6.0.0" } }, "sha512-Tmd1Vg/m3Xz43afeNxDIhWRtFZgM2VLyaf4vSTYwudTyeuTneoL3qtWMA5jeLyz/O1vDJmmV4QuScFCA2tBPwg=="],
|
||||
|
||||
"mdast-util-from-markdown": ["mdast-util-from-markdown@2.0.3", "", { "dependencies": { "@types/mdast": "^4.0.0", "@types/unist": "^3.0.0", "decode-named-character-reference": "^1.0.0", "devlop": "^1.0.0", "mdast-util-to-string": "^4.0.0", "micromark": "^4.0.0", "micromark-util-decode-numeric-character-reference": "^2.0.0", "micromark-util-decode-string": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0", "unist-util-stringify-position": "^4.0.0" } }, "sha512-W4mAWTvSlKvf8L6J+VN9yLSqQ9AOAAvHuoDAmPkz4dHf553m5gVj2ejadHJhoJmcmxEnOv6Pa8XJhpxE93kb8Q=="],
|
||||
|
||||
"mdast-util-gfm": ["mdast-util-gfm@3.1.0", "", { "dependencies": { "mdast-util-from-markdown": "^2.0.0", "mdast-util-gfm-autolink-literal": "^2.0.0", "mdast-util-gfm-footnote": "^2.0.0", "mdast-util-gfm-strikethrough": "^2.0.0", "mdast-util-gfm-table": "^2.0.0", "mdast-util-gfm-task-list-item": "^2.0.0", "mdast-util-to-markdown": "^2.0.0" } }, "sha512-0ulfdQOM3ysHhCJ1p06l0b0VKlhU0wuQs3thxZQagjcjPrlFRqY215uZGHHJan9GEAXd9MbfPjFJz+qMkVR6zQ=="],
|
||||
|
||||
"mdast-util-gfm-autolink-literal": ["mdast-util-gfm-autolink-literal@2.0.1", "", { "dependencies": { "@types/mdast": "^4.0.0", "ccount": "^2.0.0", "devlop": "^1.0.0", "mdast-util-find-and-replace": "^3.0.0", "micromark-util-character": "^2.0.0" } }, "sha512-5HVP2MKaP6L+G6YaxPNjuL0BPrq9orG3TsrZ9YXbA3vDw/ACI4MEsnoDpn6ZNm7GnZgtAcONJyPhOP8tNJQavQ=="],
|
||||
|
||||
"mdast-util-gfm-footnote": ["mdast-util-gfm-footnote@2.1.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "devlop": "^1.1.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0" } }, "sha512-sqpDWlsHn7Ac9GNZQMeUzPQSMzR6Wv0WKRNvQRg0KqHh02fpTz69Qc1QSseNX29bhz1ROIyNyxExfawVKTm1GQ=="],
|
||||
|
||||
"mdast-util-gfm-strikethrough": ["mdast-util-gfm-strikethrough@2.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0" } }, "sha512-mKKb915TF+OC5ptj5bJ7WFRPdYtuHv0yTRxK2tJvi+BDqbkiG7h7u/9SI89nRAYcmap2xHQL9D+QG/6wSrTtXg=="],
|
||||
|
||||
"mdast-util-gfm-table": ["mdast-util-gfm-table@2.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "devlop": "^1.0.0", "markdown-table": "^3.0.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0" } }, "sha512-78UEvebzz/rJIxLvE7ZtDd/vIQ0RHv+3Mh5DR96p7cS7HsBhYIICDBCu8csTNWNO6tBWfqXPWekRuj2FNOGOZg=="],
|
||||
|
||||
"mdast-util-gfm-task-list-item": ["mdast-util-gfm-task-list-item@2.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "devlop": "^1.0.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0" } }, "sha512-IrtvNvjxC1o06taBAVJznEnkiHxLFTzgonUdy8hzFVeDun0uTjxxrRGVaNFqkU1wJR3RBPEfsxmU6jDWPofrTQ=="],
|
||||
|
||||
"mdast-util-mdx-expression": ["mdast-util-mdx-expression@2.0.1", "", { "dependencies": { "@types/estree-jsx": "^1.0.0", "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "devlop": "^1.0.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0" } }, "sha512-J6f+9hUp+ldTZqKRSg7Vw5V6MqjATc+3E4gf3CFNcuZNWD8XdyI6zQ8GqH7f8169MM6P7hMBRDVGnn7oHB9kXQ=="],
|
||||
|
||||
"mdast-util-mdx-jsx": ["mdast-util-mdx-jsx@3.2.0", "", { "dependencies": { "@types/estree-jsx": "^1.0.0", "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "@types/unist": "^3.0.0", "ccount": "^2.0.0", "devlop": "^1.1.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0", "parse-entities": "^4.0.0", "stringify-entities": "^4.0.0", "unist-util-stringify-position": "^4.0.0", "vfile-message": "^4.0.0" } }, "sha512-lj/z8v0r6ZtsN/cGNNtemmmfoLAFZnjMbNyLzBafjzikOM+glrjNHPlf6lQDOTccj9n5b0PPihEBbhneMyGs1Q=="],
|
||||
|
||||
"mdast-util-mdxjs-esm": ["mdast-util-mdxjs-esm@2.0.1", "", { "dependencies": { "@types/estree-jsx": "^1.0.0", "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "devlop": "^1.0.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0" } }, "sha512-EcmOpxsZ96CvlP03NghtH1EsLtr0n9Tm4lPUJUBccV9RwUOneqSycg19n5HGzCf+10LozMRSObtVr3ee1WoHtg=="],
|
||||
|
||||
"mdast-util-phrasing": ["mdast-util-phrasing@4.1.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "unist-util-is": "^6.0.0" } }, "sha512-TqICwyvJJpBwvGAMZjj4J2n0X8QWp21b9l0o7eXyVJ25YNWYbJDVIyD1bZXE6WtV6RmKJVYmQAKWa0zWOABz2w=="],
|
||||
|
||||
"mdast-util-to-hast": ["mdast-util-to-hast@13.2.1", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "@ungap/structured-clone": "^1.0.0", "devlop": "^1.0.0", "micromark-util-sanitize-uri": "^2.0.0", "trim-lines": "^3.0.0", "unist-util-position": "^5.0.0", "unist-util-visit": "^5.0.0", "vfile": "^6.0.0" } }, "sha512-cctsq2wp5vTsLIcaymblUriiTcZd0CwWtCbLvrOzYCDZoWyMNV8sZ7krj09FSnsiJi3WVsHLM4k6Dq/yaPyCXA=="],
|
||||
|
||||
"mdast-util-to-markdown": ["mdast-util-to-markdown@2.1.2", "", { "dependencies": { "@types/mdast": "^4.0.0", "@types/unist": "^3.0.0", "longest-streak": "^3.0.0", "mdast-util-phrasing": "^4.0.0", "mdast-util-to-string": "^4.0.0", "micromark-util-classify-character": "^2.0.0", "micromark-util-decode-string": "^2.0.0", "unist-util-visit": "^5.0.0", "zwitch": "^2.0.0" } }, "sha512-xj68wMTvGXVOKonmog6LwyJKrYXZPvlwabaryTjLh9LuvovB/KAH+kvi8Gjj+7rJjsFi23nkUxRQv1KqSroMqA=="],
|
||||
|
||||
"mdast-util-to-string": ["mdast-util-to-string@4.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0" } }, "sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg=="],
|
||||
|
||||
"media-typer": ["media-typer@1.1.0", "", {}, "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw=="],
|
||||
|
||||
"merge-descriptors": ["merge-descriptors@2.0.0", "", {}, "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g=="],
|
||||
|
||||
"micromark": ["micromark@4.0.2", "", { "dependencies": { "@types/debug": "^4.0.0", "debug": "^4.0.0", "decode-named-character-reference": "^1.0.0", "devlop": "^1.0.0", "micromark-core-commonmark": "^2.0.0", "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-chunked": "^2.0.0", "micromark-util-combine-extensions": "^2.0.0", "micromark-util-decode-numeric-character-reference": "^2.0.0", "micromark-util-encode": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0", "micromark-util-resolve-all": "^2.0.0", "micromark-util-sanitize-uri": "^2.0.0", "micromark-util-subtokenize": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-zpe98Q6kvavpCr1NPVSCMebCKfD7CA2NqZ+rykeNhONIJBpc1tFKt9hucLGwha3jNTNI8lHpctWJWoimVF4PfA=="],
|
||||
|
||||
"micromark-core-commonmark": ["micromark-core-commonmark@2.0.3", "", { "dependencies": { "decode-named-character-reference": "^1.0.0", "devlop": "^1.0.0", "micromark-factory-destination": "^2.0.0", "micromark-factory-label": "^2.0.0", "micromark-factory-space": "^2.0.0", "micromark-factory-title": "^2.0.0", "micromark-factory-whitespace": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-chunked": "^2.0.0", "micromark-util-classify-character": "^2.0.0", "micromark-util-html-tag-name": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0", "micromark-util-resolve-all": "^2.0.0", "micromark-util-subtokenize": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-RDBrHEMSxVFLg6xvnXmb1Ayr2WzLAWjeSATAoxwKYJV94TeNavgoIdA0a9ytzDSVzBy2YKFK+emCPOEibLeCrg=="],
|
||||
|
||||
"micromark-extension-gfm": ["micromark-extension-gfm@3.0.0", "", { "dependencies": { "micromark-extension-gfm-autolink-literal": "^2.0.0", "micromark-extension-gfm-footnote": "^2.0.0", "micromark-extension-gfm-strikethrough": "^2.0.0", "micromark-extension-gfm-table": "^2.0.0", "micromark-extension-gfm-tagfilter": "^2.0.0", "micromark-extension-gfm-task-list-item": "^2.0.0", "micromark-util-combine-extensions": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-vsKArQsicm7t0z2GugkCKtZehqUm31oeGBV/KVSorWSy8ZlNAv7ytjFhvaryUiCUJYqs+NoE6AFhpQvBTM6Q4w=="],
|
||||
|
||||
"micromark-extension-gfm-autolink-literal": ["micromark-extension-gfm-autolink-literal@2.1.0", "", { "dependencies": { "micromark-util-character": "^2.0.0", "micromark-util-sanitize-uri": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-oOg7knzhicgQ3t4QCjCWgTmfNhvQbDDnJeVu9v81r7NltNCVmhPy1fJRX27pISafdjL+SVc4d3l48Gb6pbRypw=="],
|
||||
|
||||
"micromark-extension-gfm-footnote": ["micromark-extension-gfm-footnote@2.1.0", "", { "dependencies": { "devlop": "^1.0.0", "micromark-core-commonmark": "^2.0.0", "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0", "micromark-util-sanitize-uri": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-/yPhxI1ntnDNsiHtzLKYnE3vf9JZ6cAisqVDauhp4CEHxlb4uoOTxOCJ+9s51bIB8U1N1FJ1RXOKTIlD5B/gqw=="],
|
||||
|
||||
"micromark-extension-gfm-strikethrough": ["micromark-extension-gfm-strikethrough@2.1.0", "", { "dependencies": { "devlop": "^1.0.0", "micromark-util-chunked": "^2.0.0", "micromark-util-classify-character": "^2.0.0", "micromark-util-resolve-all": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-ADVjpOOkjz1hhkZLlBiYA9cR2Anf8F4HqZUO6e5eDcPQd0Txw5fxLzzxnEkSkfnD0wziSGiv7sYhk/ktvbf1uw=="],
|
||||
|
||||
"micromark-extension-gfm-table": ["micromark-extension-gfm-table@2.1.1", "", { "dependencies": { "devlop": "^1.0.0", "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-t2OU/dXXioARrC6yWfJ4hqB7rct14e8f7m0cbI5hUmDyyIlwv5vEtooptH8INkbLzOatzKuVbQmAYcbWoyz6Dg=="],
|
||||
|
||||
"micromark-extension-gfm-tagfilter": ["micromark-extension-gfm-tagfilter@2.0.0", "", { "dependencies": { "micromark-util-types": "^2.0.0" } }, "sha512-xHlTOmuCSotIA8TW1mDIM6X2O1SiX5P9IuDtqGonFhEK0qgRI4yeC6vMxEV2dgyr2TiD+2PQ10o+cOhdVAcwfg=="],
|
||||
|
||||
"micromark-extension-gfm-task-list-item": ["micromark-extension-gfm-task-list-item@2.1.0", "", { "dependencies": { "devlop": "^1.0.0", "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-qIBZhqxqI6fjLDYFTBIa4eivDMnP+OZqsNwmQ3xNLE4Cxwc+zfQEfbs6tzAo2Hjq+bh6q5F+Z8/cksrLFYWQQw=="],
|
||||
|
||||
"micromark-factory-destination": ["micromark-factory-destination@2.0.1", "", { "dependencies": { "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-Xe6rDdJlkmbFRExpTOmRj9N3MaWmbAgdpSrBQvCFqhezUn4AHqJHbaEnfbVYYiexVSs//tqOdY/DxhjdCiJnIA=="],
|
||||
|
||||
"micromark-factory-label": ["micromark-factory-label@2.0.1", "", { "dependencies": { "devlop": "^1.0.0", "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-VFMekyQExqIW7xIChcXn4ok29YE3rnuyveW3wZQWWqF4Nv9Wk5rgJ99KzPvHjkmPXF93FXIbBp6YdW3t71/7Vg=="],
|
||||
|
||||
"micromark-factory-space": ["micromark-factory-space@2.0.1", "", { "dependencies": { "micromark-util-character": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg=="],
|
||||
|
||||
"micromark-factory-title": ["micromark-factory-title@2.0.1", "", { "dependencies": { "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-5bZ+3CjhAd9eChYTHsjy6TGxpOFSKgKKJPJxr293jTbfry2KDoWkhBb6TcPVB4NmzaPhMs1Frm9AZH7OD4Cjzw=="],
|
||||
|
||||
"micromark-factory-whitespace": ["micromark-factory-whitespace@2.0.1", "", { "dependencies": { "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-Ob0nuZ3PKt/n0hORHyvoD9uZhr+Za8sFoP+OnMcnWK5lngSzALgQYKMr9RJVOWLqQYuyn6ulqGWSXdwf6F80lQ=="],
|
||||
|
||||
"micromark-util-character": ["micromark-util-character@2.1.1", "", { "dependencies": { "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q=="],
|
||||
|
||||
"micromark-util-chunked": ["micromark-util-chunked@2.0.1", "", { "dependencies": { "micromark-util-symbol": "^2.0.0" } }, "sha512-QUNFEOPELfmvv+4xiNg2sRYeS/P84pTW0TCgP5zc9FpXetHY0ab7SxKyAQCNCc1eK0459uoLI1y5oO5Vc1dbhA=="],
|
||||
|
||||
"micromark-util-classify-character": ["micromark-util-classify-character@2.0.1", "", { "dependencies": { "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-K0kHzM6afW/MbeWYWLjoHQv1sgg2Q9EccHEDzSkxiP/EaagNzCm7T/WMKZ3rjMbvIpvBiZgwR3dKMygtA4mG1Q=="],
|
||||
|
||||
"micromark-util-combine-extensions": ["micromark-util-combine-extensions@2.0.1", "", { "dependencies": { "micromark-util-chunked": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-OnAnH8Ujmy59JcyZw8JSbK9cGpdVY44NKgSM7E9Eh7DiLS2E9RNQf0dONaGDzEG9yjEl5hcqeIsj4hfRkLH/Bg=="],
|
||||
|
||||
"micromark-util-decode-numeric-character-reference": ["micromark-util-decode-numeric-character-reference@2.0.2", "", { "dependencies": { "micromark-util-symbol": "^2.0.0" } }, "sha512-ccUbYk6CwVdkmCQMyr64dXz42EfHGkPQlBj5p7YVGzq8I7CtjXZJrubAYezf7Rp+bjPseiROqe7G6foFd+lEuw=="],
|
||||
|
||||
"micromark-util-decode-string": ["micromark-util-decode-string@2.0.1", "", { "dependencies": { "decode-named-character-reference": "^1.0.0", "micromark-util-character": "^2.0.0", "micromark-util-decode-numeric-character-reference": "^2.0.0", "micromark-util-symbol": "^2.0.0" } }, "sha512-nDV/77Fj6eH1ynwscYTOsbK7rR//Uj0bZXBwJZRfaLEJ1iGBR6kIfNmlNqaqJf649EP0F3NWNdeJi03elllNUQ=="],
|
||||
|
||||
"micromark-util-encode": ["micromark-util-encode@2.0.1", "", {}, "sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw=="],
|
||||
|
||||
"micromark-util-html-tag-name": ["micromark-util-html-tag-name@2.0.1", "", {}, "sha512-2cNEiYDhCWKI+Gs9T0Tiysk136SnR13hhO8yW6BGNyhOC4qYFnwF1nKfD3HFAIXA5c45RrIG1ub11GiXeYd1xA=="],
|
||||
|
||||
"micromark-util-normalize-identifier": ["micromark-util-normalize-identifier@2.0.1", "", { "dependencies": { "micromark-util-symbol": "^2.0.0" } }, "sha512-sxPqmo70LyARJs0w2UclACPUUEqltCkJ6PhKdMIDuJ3gSf/Q+/GIe3WKl0Ijb/GyH9lOpUkRAO2wp0GVkLvS9Q=="],
|
||||
|
||||
"micromark-util-resolve-all": ["micromark-util-resolve-all@2.0.1", "", { "dependencies": { "micromark-util-types": "^2.0.0" } }, "sha512-VdQyxFWFT2/FGJgwQnJYbe1jjQoNTS4RjglmSjTUlpUMa95Htx9NHeYW4rGDJzbjvCsl9eLjMQwGeElsqmzcHg=="],
|
||||
|
||||
"micromark-util-sanitize-uri": ["micromark-util-sanitize-uri@2.0.1", "", { "dependencies": { "micromark-util-character": "^2.0.0", "micromark-util-encode": "^2.0.0", "micromark-util-symbol": "^2.0.0" } }, "sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ=="],
|
||||
|
||||
"micromark-util-subtokenize": ["micromark-util-subtokenize@2.1.0", "", { "dependencies": { "devlop": "^1.0.0", "micromark-util-chunked": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-XQLu552iSctvnEcgXw6+Sx75GflAPNED1qx7eBJ+wydBb2KCbRZe+NwvIEEMM83uml1+2WSXpBAcp9IUCgCYWA=="],
|
||||
|
||||
"micromark-util-symbol": ["micromark-util-symbol@2.0.1", "", {}, "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q=="],
|
||||
|
||||
"micromark-util-types": ["micromark-util-types@2.0.2", "", {}, "sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA=="],
|
||||
|
||||
"mime-db": ["mime-db@1.54.0", "", {}, "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ=="],
|
||||
|
||||
"mime-types": ["mime-types@3.0.2", "", { "dependencies": { "mime-db": "^1.54.0" } }, "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A=="],
|
||||
@@ -526,6 +680,8 @@
|
||||
|
||||
"once": ["once@1.4.0", "", { "dependencies": { "wrappy": "1" } }, "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w=="],
|
||||
|
||||
"parse-entities": ["parse-entities@4.0.2", "", { "dependencies": { "@types/unist": "^2.0.0", "character-entities-legacy": "^3.0.0", "character-reference-invalid": "^2.0.0", "decode-named-character-reference": "^1.0.0", "is-alphanumerical": "^2.0.0", "is-decimal": "^2.0.0", "is-hexadecimal": "^2.0.0" } }, "sha512-GG2AQYWoLgL877gQIKeRPGO1xF9+eG1ujIb5soS5gPvLQ1y2o8FL90w2QWNdf9I361Mpp7726c+lj3U0qK1uGw=="],
|
||||
|
||||
"parseurl": ["parseurl@1.3.3", "", {}, "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ=="],
|
||||
|
||||
"path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="],
|
||||
@@ -548,6 +704,10 @@
|
||||
|
||||
"pkce-challenge": ["pkce-challenge@5.0.1", "", {}, "sha512-wQ0b/W4Fr01qtpHlqSqspcj3EhBvimsdh0KlHhH8HRZnMsEa0ea2fTULOXOS9ccQr3om+GcGRk4e+isrZWV8qQ=="],
|
||||
|
||||
"playwright": ["playwright@1.59.1", "", { "dependencies": { "playwright-core": "1.59.1" }, "optionalDependencies": { "fsevents": "2.3.2" }, "bin": { "playwright": "cli.js" } }, "sha512-C8oWjPR3F81yljW9o5OxcWzfh6avkVwDD2VYdwIGqTkl+OGFISgypqzfu7dOe4QNLL2aqcWBmI3PMtLIK233lw=="],
|
||||
|
||||
"playwright-core": ["playwright-core@1.59.1", "", { "bin": { "playwright-core": "cli.js" } }, "sha512-HBV/RJg81z5BiiZ9yPzIiClYV/QMsDCKUyogwH9p3MCP6IYjUFu/MActgYAvK0oWyV9NlwM3GLBjADyWgydVyg=="],
|
||||
|
||||
"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=="],
|
||||
@@ -556,6 +716,8 @@
|
||||
|
||||
"process-warning": ["process-warning@5.0.0", "", {}, "sha512-a39t9ApHNx2L4+HBnQKqxxHNs1r7KF+Intd8Q/g1bUh6q0WIp9voPXJ/x0j+ZL45KF1pJd9+q2jLIRMfvEshkA=="],
|
||||
|
||||
"property-information": ["property-information@7.1.0", "", {}, "sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ=="],
|
||||
|
||||
"proxy-addr": ["proxy-addr@2.0.7", "", { "dependencies": { "forwarded": "0.2.0", "ipaddr.js": "1.9.1" } }, "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg=="],
|
||||
|
||||
"pump": ["pump@3.0.3", "", { "dependencies": { "end-of-stream": "^1.1.0", "once": "^1.3.1" } }, "sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA=="],
|
||||
@@ -574,10 +736,20 @@
|
||||
|
||||
"react-dom": ["react-dom@19.2.5", "", { "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { "react": "^19.2.5" } }, "sha512-J5bAZz+DXMMwW/wV3xzKke59Af6CHY7G4uYLN1OvBcKEsWOs4pQExj86BBKamxl/Ik5bx9whOrvBlSDfWzgSag=="],
|
||||
|
||||
"react-markdown": ["react-markdown@10.1.0", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "devlop": "^1.0.0", "hast-util-to-jsx-runtime": "^2.0.0", "html-url-attributes": "^3.0.0", "mdast-util-to-hast": "^13.0.0", "remark-parse": "^11.0.0", "remark-rehype": "^11.0.0", "unified": "^11.0.0", "unist-util-visit": "^5.0.0", "vfile": "^6.0.0" }, "peerDependencies": { "@types/react": ">=18", "react": ">=18" } }, "sha512-qKxVopLT/TyA6BX3Ue5NwabOsAzm0Q7kAPwq6L+wWDwisYs7R8vZ0nRXqq6rkueboxpkjvLGU9fWifiX/ZZFxQ=="],
|
||||
|
||||
"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=="],
|
||||
|
||||
"remark-gfm": ["remark-gfm@4.0.1", "", { "dependencies": { "@types/mdast": "^4.0.0", "mdast-util-gfm": "^3.0.0", "micromark-extension-gfm": "^3.0.0", "remark-parse": "^11.0.0", "remark-stringify": "^11.0.0", "unified": "^11.0.0" } }, "sha512-1quofZ2RQ9EWdeN34S79+KExV1764+wCUGop5CPL1WGdD0ocPpu91lzPGbwWMECpEpd42kJGQwzRfyov9j4yNg=="],
|
||||
|
||||
"remark-parse": ["remark-parse@11.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "mdast-util-from-markdown": "^2.0.0", "micromark-util-types": "^2.0.0", "unified": "^11.0.0" } }, "sha512-FCxlKLNGknS5ba/1lmpYijMUzX2esxW5xQqjWxw2eHFfS2MSdaHVINFmhjo+qN1WhZhNimq0dZATN9pH0IDrpA=="],
|
||||
|
||||
"remark-rehype": ["remark-rehype@11.1.2", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "mdast-util-to-hast": "^13.0.0", "unified": "^11.0.0", "vfile": "^6.0.0" } }, "sha512-Dh7l57ianaEoIpzbp0PC9UKAdCSVklD8E5Rpw7ETfbTl3FqcOOgq5q2LVDhgGCkaBv7p24JXikPdvhhmHvKMsw=="],
|
||||
|
||||
"remark-stringify": ["remark-stringify@11.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "mdast-util-to-markdown": "^2.0.0", "unified": "^11.0.0" } }, "sha512-1OSmLd3awB/t8qdoEOMazZkNsfVTeY4fTsgzcQFdXNq8ToTN4ZGwrMnlda4K6smTFKD+GRV6O48i6Z4iKgPPpw=="],
|
||||
|
||||
"require-from-string": ["require-from-string@2.0.2", "", {}, "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw=="],
|
||||
|
||||
"resolve-pkg-maps": ["resolve-pkg-maps@1.0.0", "", {}, "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw=="],
|
||||
@@ -628,6 +800,8 @@
|
||||
|
||||
"source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="],
|
||||
|
||||
"space-separated-tokens": ["space-separated-tokens@2.0.2", "", {}, "sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q=="],
|
||||
|
||||
"split2": ["split2@4.2.0", "", {}, "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg=="],
|
||||
|
||||
"stackback": ["stackback@0.0.2", "", {}, "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw=="],
|
||||
@@ -638,8 +812,14 @@
|
||||
|
||||
"string_decoder": ["string_decoder@1.3.0", "", { "dependencies": { "safe-buffer": "~5.2.0" } }, "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA=="],
|
||||
|
||||
"stringify-entities": ["stringify-entities@4.0.4", "", { "dependencies": { "character-entities-html4": "^2.0.0", "character-entities-legacy": "^3.0.0" } }, "sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg=="],
|
||||
|
||||
"strip-json-comments": ["strip-json-comments@5.0.3", "", {}, "sha512-1tB5mhVo7U+ETBKNf92xT4hrQa3pm0MZ0PQvuDnWgAAGHDsfp4lPSpiS6psrSiet87wyGPh9ft6wmhOMQ0hDiw=="],
|
||||
|
||||
"style-to-js": ["style-to-js@1.1.21", "", { "dependencies": { "style-to-object": "1.0.14" } }, "sha512-RjQetxJrrUJLQPHbLku6U/ocGtzyjbJMP9lCNK7Ag0CNh690nSH8woqWH9u16nMjYBAok+i7JO1NP2pOy8IsPQ=="],
|
||||
|
||||
"style-to-object": ["style-to-object@1.0.14", "", { "dependencies": { "inline-style-parser": "0.2.7" } }, "sha512-LIN7rULI0jBscWQYaSswptyderlarFkjQ+t79nzty8tcIAceVomEVlLzH5VP4Cmsv6MtKhs7qaAiwlcp+Mgaxw=="],
|
||||
|
||||
"supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="],
|
||||
|
||||
"tar-fs": ["tar-fs@2.1.4", "", { "dependencies": { "chownr": "^1.1.1", "mkdirp-classic": "^0.5.2", "pump": "^3.0.0", "tar-stream": "^2.1.4" } }, "sha512-mDAjwmZdh7LTT6pNleZ05Yt65HC3E+NiQzl672vQG38jIrehtJk/J3mNwIg+vShQPcLF/LV7CMnDW6vjj6sfYQ=="],
|
||||
@@ -658,6 +838,10 @@
|
||||
|
||||
"toidentifier": ["toidentifier@1.0.1", "", {}, "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA=="],
|
||||
|
||||
"trim-lines": ["trim-lines@3.0.1", "", {}, "sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg=="],
|
||||
|
||||
"trough": ["trough@2.2.0", "", {}, "sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw=="],
|
||||
|
||||
"ts-mixer": ["ts-mixer@6.0.4", "", {}, "sha512-ufKpbmrugz5Aou4wcr5Wc1UUFWOLhq+Fm6qa6P0w0K5Qw2yhaUoiWszhCVuNQyNwrlGiscHOmqYoAox1PtvgjA=="],
|
||||
|
||||
"tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
|
||||
@@ -674,12 +858,28 @@
|
||||
|
||||
"undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="],
|
||||
|
||||
"unified": ["unified@11.0.5", "", { "dependencies": { "@types/unist": "^3.0.0", "bail": "^2.0.0", "devlop": "^1.0.0", "extend": "^3.0.0", "is-plain-obj": "^4.0.0", "trough": "^2.0.0", "vfile": "^6.0.0" } }, "sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA=="],
|
||||
|
||||
"unist-util-is": ["unist-util-is@6.0.1", "", { "dependencies": { "@types/unist": "^3.0.0" } }, "sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g=="],
|
||||
|
||||
"unist-util-position": ["unist-util-position@5.0.0", "", { "dependencies": { "@types/unist": "^3.0.0" } }, "sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA=="],
|
||||
|
||||
"unist-util-stringify-position": ["unist-util-stringify-position@4.0.0", "", { "dependencies": { "@types/unist": "^3.0.0" } }, "sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ=="],
|
||||
|
||||
"unist-util-visit": ["unist-util-visit@5.1.0", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-is": "^6.0.0", "unist-util-visit-parents": "^6.0.0" } }, "sha512-m+vIdyeCOpdr/QeQCu2EzxX/ohgS8KbnPDgFni4dQsfSCtpz8UqDyY5GjRru8PDKuYn7Fq19j1CQ+nJSsGKOzg=="],
|
||||
|
||||
"unist-util-visit-parents": ["unist-util-visit-parents@6.0.2", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-is": "^6.0.0" } }, "sha512-goh1s1TBrqSqukSc8wrjwWhL0hiJxgA8m4kFxGlQ+8FYQ3C/m11FcTs4YYem7V664AhHVvgoQLk890Ssdsr2IQ=="],
|
||||
|
||||
"unpipe": ["unpipe@1.0.0", "", {}, "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ=="],
|
||||
|
||||
"util-deprecate": ["util-deprecate@1.0.2", "", {}, "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="],
|
||||
|
||||
"vary": ["vary@1.1.2", "", {}, "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg=="],
|
||||
|
||||
"vfile": ["vfile@6.0.3", "", { "dependencies": { "@types/unist": "^3.0.0", "vfile-message": "^4.0.0" } }, "sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q=="],
|
||||
|
||||
"vfile-message": ["vfile-message@4.0.3", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-stringify-position": "^4.0.0" } }, "sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw=="],
|
||||
|
||||
"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=="],
|
||||
@@ -698,22 +898,34 @@
|
||||
|
||||
"zod-to-json-schema": ["zod-to-json-schema@3.25.1", "", { "peerDependencies": { "zod": "^3.25 || ^4" } }, "sha512-pM/SU9d3YAggzi6MtR4h7ruuQlqKtad8e9S0fmxcMi+ueAK5Korys/aWcV9LIIHTVbj01NdzxcnXSN+O74ZIVA=="],
|
||||
|
||||
"zwitch": ["zwitch@2.0.4", "", {}, "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A=="],
|
||||
|
||||
"@discordjs/rest/@discordjs/collection": ["@discordjs/collection@2.1.1", "", {}, "sha512-LiSusze9Tc7qF03sLCujF5iZp7K+vRNEDBZ86FT9aQAv3vxMLihUvKvpsCWiQ2DJq1tVckopKm1rxomgNUc9hg=="],
|
||||
|
||||
"@discordjs/ws/@discordjs/collection": ["@discordjs/collection@2.1.1", "", {}, "sha512-LiSusze9Tc7qF03sLCujF5iZp7K+vRNEDBZ86FT9aQAv3vxMLihUvKvpsCWiQ2DJq1tVckopKm1rxomgNUc9hg=="],
|
||||
|
||||
"parse-entities/@types/unist": ["@types/unist@2.0.11", "", {}, "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA=="],
|
||||
|
||||
"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=="],
|
||||
|
||||
"rolldown/@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-rc.17", "", {}, "sha512-n8iosDOt6Ig1UhJ2AYqoIhHWh/isz0xpicHTzpKBeotdVsTEcxsSA/i3EVM7gQAj0rU27OLAxCjzlj15IWY7bg=="],
|
||||
|
||||
"rollup/fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="],
|
||||
|
||||
"tsx/fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="],
|
||||
|
||||
"vite/fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="],
|
||||
|
||||
"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/fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="],
|
||||
|
||||
"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=="],
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,10 +32,13 @@
|
||||
"cron-parser": "^5.5.0",
|
||||
"discord.js": "^14.18.0",
|
||||
"ejclaw-runners-shared": "file:./runners/shared",
|
||||
"lucide-react": "^1.11.0",
|
||||
"pino": "^9.6.0",
|
||||
"pino-pretty": "^13.0.0",
|
||||
"react": "^19.2.5",
|
||||
"react-dom": "^19.2.5",
|
||||
"react-markdown": "^10.1.0",
|
||||
"remark-gfm": "^4.0.1",
|
||||
"yaml": "^2.8.2",
|
||||
"zod": "^4.3.6"
|
||||
},
|
||||
@@ -49,6 +52,7 @@
|
||||
"@vitest/coverage-v8": "^4.0.18",
|
||||
"better-sqlite3": "^12.8.0",
|
||||
"husky": "^9.1.7",
|
||||
"playwright": "^1.59.1",
|
||||
"prettier": "^3.8.1",
|
||||
"typescript": "^5.7.0",
|
||||
"vite": "^8.0.10",
|
||||
|
||||
141
scripts/dash-inspect.ts
Normal file
141
scripts/dash-inspect.ts
Normal file
@@ -0,0 +1,141 @@
|
||||
#!/usr/bin/env bun
|
||||
/**
|
||||
* Dashboard inspector via Playwright.
|
||||
*
|
||||
* Usage:
|
||||
* bun scripts/dash-inspect.ts shot # full-page screenshot
|
||||
* bun scripts/dash-inspect.ts shot rooms # screenshot of #/rooms
|
||||
* bun scripts/dash-inspect.ts measure '<sel>' # bounding box + computed style
|
||||
* bun scripts/dash-inspect.ts click '<sel>' # click and screenshot after
|
||||
* bun scripts/dash-inspect.ts eval '<expr>' # evaluate JS in page
|
||||
* bun scripts/dash-inspect.ts dom '<sel>' # outerHTML of selector
|
||||
* bun scripts/dash-inspect.ts route <hash> # navigate to #/<hash>
|
||||
*
|
||||
* Output: writes to /tmp/dash-shot.png + prints data to stdout.
|
||||
*/
|
||||
|
||||
import { chromium, type BrowserContext, type Page } from 'playwright';
|
||||
|
||||
const BASE = process.env.DASH_URL ?? 'http://100.101.210.95:5174';
|
||||
const SHOT = '/tmp/dash-shot.png';
|
||||
|
||||
async function withPage<T>(
|
||||
fn: (page: Page, ctx: BrowserContext) => Promise<T>,
|
||||
): Promise<T> {
|
||||
const browser = await chromium.launch({ headless: true });
|
||||
const ctx = await browser.newContext({
|
||||
viewport: { width: 1600, height: 1000 },
|
||||
deviceScaleFactor: 1,
|
||||
});
|
||||
const page = await ctx.newPage();
|
||||
try {
|
||||
return await fn(page, ctx);
|
||||
} finally {
|
||||
await browser.close();
|
||||
}
|
||||
}
|
||||
|
||||
async function gotoRoute(page: Page, route?: string) {
|
||||
const url = route ? `${BASE}/#/${route.replace(/^#?\/?/, '')}` : BASE;
|
||||
await page.goto(url, { waitUntil: 'networkidle', timeout: 20000 });
|
||||
await page.waitForTimeout(1500);
|
||||
}
|
||||
|
||||
async function shot(page: Page) {
|
||||
await page.screenshot({ path: SHOT, fullPage: false });
|
||||
console.log(`screenshot saved: ${SHOT}`);
|
||||
}
|
||||
|
||||
async function measure(page: Page, sel: string) {
|
||||
const all = await page.$$(sel);
|
||||
if (all.length === 0) {
|
||||
console.log(`NOT FOUND: ${sel}`);
|
||||
return;
|
||||
}
|
||||
for (let i = 0; i < Math.min(all.length, 6); i++) {
|
||||
const el = all[i];
|
||||
const box = await el.boundingBox();
|
||||
const style = await el.evaluate((node: object) => {
|
||||
const cs = (
|
||||
globalThis as unknown as {
|
||||
getComputedStyle: (e: object) => Record<string, string>;
|
||||
}
|
||||
).getComputedStyle(node);
|
||||
return {
|
||||
display: cs.display,
|
||||
position: cs.position,
|
||||
margin: cs.margin,
|
||||
padding: cs.padding,
|
||||
width: cs.width,
|
||||
height: cs.height,
|
||||
boxSizing: cs.boxSizing,
|
||||
gridTemplateColumns: cs.gridTemplateColumns,
|
||||
gridTemplateRows: cs.gridTemplateRows,
|
||||
};
|
||||
});
|
||||
console.log(
|
||||
`[${i}] ${sel}\n rect: ${box ? `x=${Math.round(box.x)} y=${Math.round(box.y)} w=${Math.round(box.width)} h=${Math.round(box.height)}` : 'none'}\n ${JSON.stringify(style)}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async function evalExpr(page: Page, expr: string) {
|
||||
const result = await page.evaluate((e) => {
|
||||
// eslint-disable-next-line no-eval
|
||||
return eval(e);
|
||||
}, expr);
|
||||
console.log(JSON.stringify(result, null, 2));
|
||||
}
|
||||
|
||||
async function dom(page: Page, sel: string) {
|
||||
const el = await page.$(sel);
|
||||
if (!el) {
|
||||
console.log(`NOT FOUND: ${sel}`);
|
||||
return;
|
||||
}
|
||||
const html = await el.evaluate(
|
||||
(node: object) => (node as unknown as { outerHTML: string }).outerHTML,
|
||||
);
|
||||
console.log(
|
||||
html.length > 4000 ? `${html.slice(0, 4000)}\n…(truncated)` : html,
|
||||
);
|
||||
}
|
||||
|
||||
async function clickAndShot(page: Page, sel: string) {
|
||||
await page.click(sel);
|
||||
await page.waitForTimeout(500);
|
||||
await shot(page);
|
||||
}
|
||||
|
||||
const [, , cmd = 'shot', ...rest] = process.argv;
|
||||
|
||||
await withPage(async (page) => {
|
||||
// Default route from rest[0] when shot/measure/etc; otherwise look for explicit `route` arg.
|
||||
const routeArg =
|
||||
cmd === 'shot' && rest[0] && !rest[0].startsWith('.') ? rest[0] : 'rooms';
|
||||
await gotoRoute(page, routeArg);
|
||||
|
||||
switch (cmd) {
|
||||
case 'shot':
|
||||
await shot(page);
|
||||
break;
|
||||
case 'measure':
|
||||
await measure(page, rest[0]);
|
||||
break;
|
||||
case 'click':
|
||||
await clickAndShot(page, rest[0]);
|
||||
break;
|
||||
case 'eval':
|
||||
await evalExpr(page, rest[0]);
|
||||
break;
|
||||
case 'dom':
|
||||
await dom(page, rest[0]);
|
||||
break;
|
||||
case 'route':
|
||||
await shot(page);
|
||||
break;
|
||||
default:
|
||||
console.error(`unknown command: ${cmd}`);
|
||||
process.exit(1);
|
||||
}
|
||||
});
|
||||
@@ -22,6 +22,7 @@ export {
|
||||
getOpenWorkItem,
|
||||
getOpenWorkItemForChat,
|
||||
getRecentChatMessages,
|
||||
getRecentChatMessagesBatch,
|
||||
hasMessage,
|
||||
hasRecentRestartAnnouncement,
|
||||
markWorkItemDelivered,
|
||||
@@ -101,7 +102,10 @@ export {
|
||||
getPairedTurnAttempts,
|
||||
getPairedTurnById,
|
||||
getPairedTurnOutputs,
|
||||
getRecentPairedTurnOutputsForChat,
|
||||
getPairedTurnsForTask,
|
||||
getLatestPairedTurnForTask,
|
||||
updatePairedTurnProgressText,
|
||||
getPairedWorkspace,
|
||||
getPendingServiceHandoffs,
|
||||
insertPairedTurnOutput,
|
||||
|
||||
@@ -24,6 +24,7 @@ export function applyBaseSchema(database: Database): void {
|
||||
FOREIGN KEY (chat_jid) REFERENCES chats(jid)
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_timestamp ON messages(timestamp);
|
||||
CREATE INDEX IF NOT EXISTS idx_messages_chat_timestamp ON messages(chat_jid, timestamp DESC);
|
||||
CREATE TABLE IF NOT EXISTS message_sequence (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT
|
||||
);
|
||||
|
||||
@@ -39,6 +39,7 @@ function getExpectedSchemaMigrations(): Array<{
|
||||
{ version: 12, name: 'paired_verdict_and_step_telemetry' },
|
||||
{ version: 13, name: 'message_source_kind' },
|
||||
{ version: 14, name: 'work_item_attachments' },
|
||||
{ version: 15, name: 'turn_progress_text' },
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
@@ -319,12 +319,19 @@ export function getMessagesSinceSeqFromDatabase(
|
||||
return rows.map(normalizeMessageRow);
|
||||
}
|
||||
|
||||
const recentChatMessagesStmtCache = new WeakMap<
|
||||
Database,
|
||||
ReturnType<Database['prepare']>
|
||||
>();
|
||||
|
||||
export function getRecentChatMessagesFromDatabase(
|
||||
database: Database,
|
||||
chatJid: string,
|
||||
limit: number = 20,
|
||||
): NewMessage[] {
|
||||
const sql = `
|
||||
let stmt = recentChatMessagesStmtCache.get(database);
|
||||
if (!stmt) {
|
||||
stmt = database.prepare(`
|
||||
SELECT * FROM (
|
||||
SELECT id, chat_jid, sender, sender_name, content, timestamp, is_from_me, is_bot_message, message_source_kind
|
||||
FROM messages
|
||||
@@ -333,8 +340,10 @@ export function getRecentChatMessagesFromDatabase(
|
||||
ORDER BY timestamp DESC
|
||||
LIMIT ?
|
||||
) ORDER BY timestamp
|
||||
`;
|
||||
const rows = database.prepare(sql).all(chatJid, limit) as Array<
|
||||
`);
|
||||
recentChatMessagesStmtCache.set(database, stmt);
|
||||
}
|
||||
const rows = stmt.all(chatJid, limit) as Array<
|
||||
NewMessage & {
|
||||
is_from_me?: boolean | number;
|
||||
is_bot_message?: boolean | number;
|
||||
@@ -343,6 +352,44 @@ export function getRecentChatMessagesFromDatabase(
|
||||
return rows.map(normalizeMessageRow);
|
||||
}
|
||||
|
||||
export function getRecentChatMessagesBatchFromDatabase(
|
||||
database: Database,
|
||||
chatJids: string[],
|
||||
limit: number = 8,
|
||||
): Map<string, NewMessage[]> {
|
||||
const out = new Map<string, NewMessage[]>();
|
||||
if (chatJids.length === 0) return out;
|
||||
const placeholders = chatJids.map(() => '?').join(',');
|
||||
const sql = `
|
||||
WITH ranked AS (
|
||||
SELECT id, chat_jid, sender, sender_name, content, timestamp,
|
||||
is_from_me, is_bot_message, message_source_kind,
|
||||
ROW_NUMBER() OVER (PARTITION BY chat_jid ORDER BY timestamp DESC) AS rn
|
||||
FROM messages
|
||||
WHERE chat_jid IN (${placeholders})
|
||||
AND content != '' AND content IS NOT NULL
|
||||
)
|
||||
SELECT id, chat_jid, sender, sender_name, content, timestamp,
|
||||
is_from_me, is_bot_message, message_source_kind
|
||||
FROM ranked
|
||||
WHERE rn <= ?
|
||||
ORDER BY chat_jid, timestamp ASC
|
||||
`;
|
||||
const rows = database.prepare(sql).all(...chatJids, limit) as Array<
|
||||
NewMessage & {
|
||||
is_from_me?: boolean | number;
|
||||
is_bot_message?: boolean | number;
|
||||
}
|
||||
>;
|
||||
for (const row of rows) {
|
||||
const normalized = normalizeMessageRow(row);
|
||||
const existing = out.get(normalized.chat_jid);
|
||||
if (existing) existing.push(normalized);
|
||||
else out.set(normalized.chat_jid, [normalized]);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
export function getLastHumanMessageTimestampFromDatabase(
|
||||
database: Database,
|
||||
chatJid: string,
|
||||
|
||||
19
src/db/migrations/015_turn-progress-text.ts
Normal file
19
src/db/migrations/015_turn-progress-text.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import type { Database } from 'bun:sqlite';
|
||||
|
||||
import { tableHasColumn } from './helpers.js';
|
||||
import type { SchemaMigrationDefinition } from './types.js';
|
||||
|
||||
export const TURN_PROGRESS_TEXT_MIGRATION: SchemaMigrationDefinition = {
|
||||
version: 15,
|
||||
name: 'turn_progress_text',
|
||||
apply(database: Database) {
|
||||
if (!tableHasColumn(database, 'paired_turns', 'progress_text')) {
|
||||
database.exec(`ALTER TABLE paired_turns ADD COLUMN progress_text TEXT`);
|
||||
}
|
||||
if (!tableHasColumn(database, 'paired_turns', 'progress_updated_at')) {
|
||||
database.exec(
|
||||
`ALTER TABLE paired_turns ADD COLUMN progress_updated_at TEXT`,
|
||||
);
|
||||
}
|
||||
},
|
||||
};
|
||||
@@ -14,6 +14,7 @@ import { OWNER_FAILURE_COUNT_MIGRATION } from './011_owner-failure-count.js';
|
||||
import { PAIRED_VERDICT_AND_STEP_TELEMETRY_MIGRATION } from './012_paired-verdict-and-step-telemetry.js';
|
||||
import { MESSAGE_SOURCE_KIND_MIGRATION } from './013_message-source-kind.js';
|
||||
import { WORK_ITEM_ATTACHMENTS_MIGRATION } from './014_work-item-attachments.js';
|
||||
import { TURN_PROGRESS_TEXT_MIGRATION } from './015_turn-progress-text.js';
|
||||
import type {
|
||||
SchemaMigrationArgs,
|
||||
SchemaMigrationDefinition,
|
||||
@@ -36,6 +37,7 @@ const ORDERED_SCHEMA_MIGRATIONS: readonly SchemaMigrationDefinition[] = [
|
||||
PAIRED_VERDICT_AND_STEP_TELEMETRY_MIGRATION,
|
||||
MESSAGE_SOURCE_KIND_MIGRATION,
|
||||
WORK_ITEM_ATTACHMENTS_MIGRATION,
|
||||
TURN_PROGRESS_TEXT_MIGRATION,
|
||||
];
|
||||
|
||||
function ensureSchemaMigrationsTable(database: Database): void {
|
||||
|
||||
@@ -229,21 +229,27 @@ export function getPairedTaskByIdFromDatabase(
|
||||
return row ? hydratePairedTaskRow(database, row) : undefined;
|
||||
}
|
||||
|
||||
const latestPairedTaskStmtCache = new WeakMap<
|
||||
Database,
|
||||
ReturnType<Database['prepare']>
|
||||
>();
|
||||
|
||||
export function getLatestPairedTaskForChatFromDatabase(
|
||||
database: Database,
|
||||
chatJid: string,
|
||||
): PairedTask | undefined {
|
||||
const row = database
|
||||
.prepare(
|
||||
`
|
||||
let stmt = latestPairedTaskStmtCache.get(database);
|
||||
if (!stmt) {
|
||||
stmt = database.prepare(`
|
||||
SELECT *
|
||||
FROM paired_tasks
|
||||
WHERE chat_jid = ?
|
||||
ORDER BY updated_at DESC
|
||||
LIMIT 1
|
||||
`,
|
||||
)
|
||||
.get(chatJid) as StoredPairedTaskRow | undefined;
|
||||
`);
|
||||
latestPairedTaskStmtCache.set(database, stmt);
|
||||
}
|
||||
const row = stmt.get(chatJid) as StoredPairedTaskRow | undefined;
|
||||
return row ? hydratePairedTaskRow(database, row) : undefined;
|
||||
}
|
||||
|
||||
|
||||
@@ -56,6 +56,32 @@ export function getPairedTurnOutputsFromDatabase(
|
||||
.all(taskId) as PairedTurnOutput[];
|
||||
}
|
||||
|
||||
const recentOutputsForChatStmtCache = new WeakMap<
|
||||
Database,
|
||||
ReturnType<Database['prepare']>
|
||||
>();
|
||||
|
||||
export function getRecentPairedTurnOutputsForChatFromDatabase(
|
||||
database: Database,
|
||||
chatJid: string,
|
||||
limit: number = 8,
|
||||
): PairedTurnOutput[] {
|
||||
let stmt = recentOutputsForChatStmtCache.get(database);
|
||||
if (!stmt) {
|
||||
stmt = database.prepare(`
|
||||
SELECT o.*
|
||||
FROM paired_turn_outputs o
|
||||
INNER JOIN paired_tasks t ON o.task_id = t.id
|
||||
WHERE t.chat_jid = ?
|
||||
ORDER BY o.created_at DESC
|
||||
LIMIT ?
|
||||
`);
|
||||
recentOutputsForChatStmtCache.set(database, stmt);
|
||||
}
|
||||
const rows = stmt.all(chatJid, limit) as PairedTurnOutput[];
|
||||
return rows.reverse();
|
||||
}
|
||||
|
||||
export function getLatestTurnNumberFromDatabase(
|
||||
database: Database,
|
||||
taskId: string,
|
||||
|
||||
@@ -33,6 +33,8 @@ export interface PairedTurnRecord {
|
||||
updated_at: string;
|
||||
completed_at: string | null;
|
||||
last_error: string | null;
|
||||
progress_text?: string | null;
|
||||
progress_updated_at?: string | null;
|
||||
}
|
||||
|
||||
interface StoredPairedTurnRow {
|
||||
@@ -43,6 +45,8 @@ interface StoredPairedTurnRow {
|
||||
intent_kind: PairedTurnIdentity['intentKind'];
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
progress_text?: string | null;
|
||||
progress_updated_at?: string | null;
|
||||
}
|
||||
|
||||
function hydratePairedTurnRecord(
|
||||
@@ -493,6 +497,56 @@ export function getPairedTurnByIdFromDatabase(
|
||||
);
|
||||
}
|
||||
|
||||
const updateProgressTextStmtCache = new WeakMap<
|
||||
Database,
|
||||
ReturnType<Database['prepare']>
|
||||
>();
|
||||
|
||||
export function updatePairedTurnProgressTextFromDatabase(
|
||||
database: Database,
|
||||
turnId: string,
|
||||
progressText: string | null,
|
||||
): void {
|
||||
let stmt = updateProgressTextStmtCache.get(database);
|
||||
if (!stmt) {
|
||||
stmt = database.prepare(`
|
||||
UPDATE paired_turns
|
||||
SET progress_text = ?, progress_updated_at = ?
|
||||
WHERE turn_id = ?
|
||||
`);
|
||||
updateProgressTextStmtCache.set(database, stmt);
|
||||
}
|
||||
stmt.run(progressText, new Date().toISOString(), turnId);
|
||||
}
|
||||
|
||||
const latestPairedTurnStmtCache = new WeakMap<
|
||||
Database,
|
||||
ReturnType<Database['prepare']>
|
||||
>();
|
||||
|
||||
export function getLatestPairedTurnForTaskFromDatabase(
|
||||
database: Database,
|
||||
taskId: string,
|
||||
): PairedTurnRecord | null {
|
||||
let stmt = latestPairedTurnStmtCache.get(database);
|
||||
if (!stmt) {
|
||||
stmt = database.prepare(`
|
||||
SELECT *
|
||||
FROM paired_turns
|
||||
WHERE task_id = ?
|
||||
ORDER BY updated_at DESC, turn_id DESC
|
||||
LIMIT 1
|
||||
`);
|
||||
latestPairedTurnStmtCache.set(database, stmt);
|
||||
}
|
||||
const row = stmt.get(taskId) as StoredPairedTurnRow | undefined;
|
||||
if (!row) return null;
|
||||
return hydratePairedTurnRecord(
|
||||
row,
|
||||
getCurrentPairedTurnAttemptForTurnFromDatabase(database, row.turn_id),
|
||||
);
|
||||
}
|
||||
|
||||
export function getPairedTurnsForTaskFromDatabase(
|
||||
database: Database,
|
||||
taskId: string,
|
||||
|
||||
@@ -26,6 +26,7 @@ import {
|
||||
getNewMessagesBySeqFromDatabase,
|
||||
getNewMessagesFromDatabase,
|
||||
getRecentChatMessagesFromDatabase,
|
||||
getRecentChatMessagesBatchFromDatabase,
|
||||
hasMessageInDatabase,
|
||||
hasRecentRestartAnnouncementInDatabase,
|
||||
storeChatMetadataInDatabase,
|
||||
@@ -224,6 +225,17 @@ export function getRecentChatMessages(
|
||||
return getRecentChatMessagesFromDatabase(requireDatabase(), chatJid, limit);
|
||||
}
|
||||
|
||||
export function getRecentChatMessagesBatch(
|
||||
chatJids: string[],
|
||||
limit: number = 8,
|
||||
): Map<string, NewMessage[]> {
|
||||
return getRecentChatMessagesBatchFromDatabase(
|
||||
requireDatabase(),
|
||||
chatJids,
|
||||
limit,
|
||||
);
|
||||
}
|
||||
|
||||
export function getLastHumanMessageTimestamp(chatJid: string): string | null {
|
||||
return getLastHumanMessageTimestampFromDatabase(requireDatabase(), chatJid);
|
||||
}
|
||||
|
||||
@@ -49,6 +49,7 @@ import {
|
||||
import {
|
||||
getLatestTurnNumberFromDatabase,
|
||||
getPairedTurnOutputsFromDatabase,
|
||||
getRecentPairedTurnOutputsForChatFromDatabase,
|
||||
insertPairedTurnOutputInDatabase,
|
||||
} from './paired-turn-outputs.js';
|
||||
import {
|
||||
@@ -58,6 +59,8 @@ import {
|
||||
failPairedTurnInDatabase,
|
||||
getPairedTurnByIdFromDatabase,
|
||||
getPairedTurnsForTaskFromDatabase,
|
||||
getLatestPairedTurnForTaskFromDatabase,
|
||||
updatePairedTurnProgressTextFromDatabase,
|
||||
markPairedTurnRunningInDatabase,
|
||||
type PairedTurnRecord,
|
||||
} from './paired-turns.js';
|
||||
@@ -211,6 +214,23 @@ export function getPairedTurnsForTask(taskId: string): PairedTurnRecord[] {
|
||||
return getPairedTurnsForTaskFromDatabase(requireDatabase(), taskId);
|
||||
}
|
||||
|
||||
export function getLatestPairedTurnForTask(
|
||||
taskId: string,
|
||||
): PairedTurnRecord | null {
|
||||
return getLatestPairedTurnForTaskFromDatabase(requireDatabase(), taskId);
|
||||
}
|
||||
|
||||
export function updatePairedTurnProgressText(
|
||||
turnId: string,
|
||||
progressText: string | null,
|
||||
): void {
|
||||
updatePairedTurnProgressTextFromDatabase(
|
||||
requireDatabase(),
|
||||
turnId,
|
||||
progressText,
|
||||
);
|
||||
}
|
||||
|
||||
export function getPairedTurnAttempts(
|
||||
turnId: string,
|
||||
): PairedTurnAttemptRecord[] {
|
||||
@@ -333,6 +353,17 @@ export function getPairedTurnOutputs(taskId: string): PairedTurnOutput[] {
|
||||
return getPairedTurnOutputsFromDatabase(requireDatabase(), taskId);
|
||||
}
|
||||
|
||||
export function getRecentPairedTurnOutputsForChat(
|
||||
chatJid: string,
|
||||
limit: number = 8,
|
||||
): PairedTurnOutput[] {
|
||||
return getRecentPairedTurnOutputsForChatFromDatabase(
|
||||
requireDatabase(),
|
||||
chatJid,
|
||||
limit,
|
||||
);
|
||||
}
|
||||
|
||||
export function getLatestTurnNumber(taskId: string): number {
|
||||
return getLatestTurnNumberFromDatabase(requireDatabase(), taskId);
|
||||
}
|
||||
|
||||
@@ -119,6 +119,7 @@ interface CreateExecuteTurnDeps {
|
||||
deliveryRole: PairedRoomRole | null;
|
||||
pairedRoom: boolean;
|
||||
}) => Promise<void>;
|
||||
recordTurnProgress: (turnId: string, progressText: string) => void;
|
||||
}
|
||||
|
||||
export function createRunAgent(deps: {
|
||||
@@ -190,6 +191,8 @@ export function createExecuteTurn(deps: CreateExecuteTurnDeps): ExecuteTurnFn {
|
||||
deliveryRole: resolvedDeliveryRole,
|
||||
deliveryServiceId: resolvedDeliveryServiceId,
|
||||
pairedTurnIdentity: args.pairedTurnIdentity ?? null,
|
||||
recordTurnProgress: (turnId, progressText) =>
|
||||
deps.recordTurnProgress(turnId, progressText),
|
||||
canDeliverFinalText: () => {
|
||||
if (!args.pairedTurnIdentity) {
|
||||
return true;
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
getLatestOpenPairedTaskForChat,
|
||||
markWorkItemDelivered,
|
||||
getPairedTaskById,
|
||||
updatePairedTurnProgressText,
|
||||
} from './db.js';
|
||||
import {
|
||||
isSessionCommandSenderAllowed,
|
||||
@@ -262,6 +263,9 @@ export function createMessageRuntime(deps: MessageRuntimeDeps): {
|
||||
continuationTracker.open(targetChatJid),
|
||||
});
|
||||
},
|
||||
recordTurnProgress: (turnId, progressText) => {
|
||||
updatePairedTurnProgressText(turnId, progressText);
|
||||
},
|
||||
afterDeliverySuccess: async ({
|
||||
chatJid,
|
||||
runId,
|
||||
|
||||
@@ -49,6 +49,7 @@ interface MessageTurnControllerOptions {
|
||||
deliveryRole?: PairedRoomRole | null;
|
||||
deliveryServiceId?: string | null;
|
||||
pairedTurnIdentity?: PairedTurnIdentity | null;
|
||||
recordTurnProgress?: (turnId: string, progressText: string) => void;
|
||||
}
|
||||
|
||||
export class MessageTurnController {
|
||||
@@ -398,34 +399,24 @@ export class MessageTurnController {
|
||||
return this.visiblePhase === 'final';
|
||||
}
|
||||
|
||||
private renderProgressMessage(text: string): string {
|
||||
const elapsedMs =
|
||||
this.progressStartedAt === null
|
||||
? 0
|
||||
: Math.floor((Date.now() - this.progressStartedAt) / 5_000) * 5000;
|
||||
|
||||
const suffix = `\n\n${formatElapsedKorean(elapsedMs)}`;
|
||||
let body: string;
|
||||
|
||||
private composeProgressBody(text: string): string {
|
||||
if (this.subagents.size > 1) {
|
||||
// Compact: one line per subagent with latest activity
|
||||
const lines: string[] = [];
|
||||
for (const [, track] of this.subagents) {
|
||||
const latest = track.activities[track.activities.length - 1];
|
||||
lines.push(latest ? `${track.label} · ${latest}` : track.label);
|
||||
}
|
||||
body = lines.join('\n');
|
||||
} else if (this.subagents.size === 1) {
|
||||
// Single subagent: detailed view with activity sub-lines
|
||||
return lines.join('\n');
|
||||
}
|
||||
if (this.subagents.size === 1) {
|
||||
const [, track] = this.subagents.entries().next().value!;
|
||||
const lines: string[] = [track.label];
|
||||
for (let i = 0; i < track.activities.length; i++) {
|
||||
const isLast = i === track.activities.length - 1;
|
||||
lines.push(`${isLast ? '└' : '├'} ${track.activities[i]}`);
|
||||
}
|
||||
body = lines.join('\n');
|
||||
} else {
|
||||
// Single agent rendering
|
||||
return lines.join('\n');
|
||||
}
|
||||
const activityLines =
|
||||
this.toolActivities.length > 0
|
||||
? '\n' +
|
||||
@@ -438,9 +429,33 @@ export class MessageTurnController {
|
||||
})
|
||||
.join('\n')
|
||||
: '';
|
||||
body = text + activityLines;
|
||||
return text + activityLines;
|
||||
}
|
||||
|
||||
private persistProgressBody(body: string): void {
|
||||
const turnId = this.options.pairedTurnIdentity?.turnId;
|
||||
if (!turnId || !this.options.recordTurnProgress) return;
|
||||
try {
|
||||
this.options.recordTurnProgress(turnId, body);
|
||||
} catch (err) {
|
||||
this.log.warn(
|
||||
{ err, turnId, bodyLength: body.length },
|
||||
'Failed to persist progress body',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private renderProgressMessage(text: string): string {
|
||||
const elapsedMs =
|
||||
this.progressStartedAt === null
|
||||
? 0
|
||||
: Math.floor((Date.now() - this.progressStartedAt) / 5_000) * 5000;
|
||||
|
||||
const suffix = `\n\n${formatElapsedKorean(elapsedMs)}`;
|
||||
const body = this.composeProgressBody(text);
|
||||
|
||||
this.persistProgressBody(body);
|
||||
|
||||
const maxBody = 2000 - TASK_STATUS_MESSAGE_PREFIX.length - suffix.length;
|
||||
const truncated =
|
||||
body.length > maxBody ? body.slice(0, maxBody - 1) + '…' : body;
|
||||
@@ -465,6 +480,14 @@ export class MessageTurnController {
|
||||
this.progressMessageId = null;
|
||||
this.progressStartedAt = null;
|
||||
this.progressEditFailCount = 0;
|
||||
const turnId = this.options.pairedTurnIdentity?.turnId;
|
||||
if (turnId && this.options.recordTurnProgress) {
|
||||
try {
|
||||
this.options.recordTurnProgress(turnId, '');
|
||||
} catch {
|
||||
/* clearing progress is best-effort */
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
330
src/settings-store.ts
Normal file
330
src/settings-store.ts
Normal file
@@ -0,0 +1,330 @@
|
||||
/**
|
||||
* Settings store for the web dashboard:
|
||||
* - lists Claude / Codex accounts (safe metadata only, no tokens)
|
||||
* - reads/writes model role configuration via .env
|
||||
* - removes account directories on the filesystem
|
||||
*
|
||||
* Account directory layout (existing convention):
|
||||
* Claude default: ~/.claude/.credentials.json
|
||||
* Claude index N≥1: ~/.claude-accounts/{N}/.credentials.json
|
||||
* Codex default: ~/.codex/auth.json
|
||||
* Codex index N≥1: ~/.codex-accounts/{N}/auth.json
|
||||
*/
|
||||
|
||||
import fs from 'node:fs';
|
||||
import os from 'node:os';
|
||||
import path from 'node:path';
|
||||
|
||||
export interface ClaudeAccountSummary {
|
||||
index: number;
|
||||
expiresAt: number | null;
|
||||
scopes: string[];
|
||||
subscriptionType?: string;
|
||||
rateLimitTier?: string;
|
||||
exists: boolean;
|
||||
}
|
||||
|
||||
export interface CodexAccountSummary {
|
||||
index: number;
|
||||
accountId: string | null;
|
||||
planType: string | null;
|
||||
subscriptionUntil: string | null;
|
||||
exists: boolean;
|
||||
}
|
||||
|
||||
export interface ModelRoleConfig {
|
||||
model: string;
|
||||
effort: string;
|
||||
}
|
||||
|
||||
export interface ModelConfigSnapshot {
|
||||
owner: ModelRoleConfig;
|
||||
reviewer: ModelRoleConfig;
|
||||
arbiter: ModelRoleConfig;
|
||||
}
|
||||
|
||||
const ROLE_KEYS = ['OWNER', 'REVIEWER', 'ARBITER'] as const;
|
||||
type RoleKey = (typeof ROLE_KEYS)[number];
|
||||
|
||||
function envFilePath(): string {
|
||||
return path.join(process.cwd(), '.env');
|
||||
}
|
||||
|
||||
function readJson<T>(file: string): T | null {
|
||||
try {
|
||||
if (!fs.existsSync(file)) return null;
|
||||
return JSON.parse(fs.readFileSync(file, 'utf-8')) as T;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function claudeCredsPath(index: number): string {
|
||||
if (index === 0) {
|
||||
return path.join(os.homedir(), '.claude', '.credentials.json');
|
||||
}
|
||||
return path.join(
|
||||
os.homedir(),
|
||||
'.claude-accounts',
|
||||
String(index),
|
||||
'.credentials.json',
|
||||
);
|
||||
}
|
||||
|
||||
function codexAuthPath(index: number): string {
|
||||
if (index === 0) {
|
||||
return path.join(os.homedir(), '.codex', 'auth.json');
|
||||
}
|
||||
return path.join(os.homedir(), '.codex-accounts', String(index), 'auth.json');
|
||||
}
|
||||
|
||||
function readClaudeAccount(index: number): ClaudeAccountSummary | null {
|
||||
const file = claudeCredsPath(index);
|
||||
if (!fs.existsSync(file)) return null;
|
||||
const data = readJson<{ claudeAiOauth?: Record<string, unknown> }>(file);
|
||||
const oauth = data?.claudeAiOauth ?? {};
|
||||
return {
|
||||
index,
|
||||
expiresAt: typeof oauth.expiresAt === 'number' ? oauth.expiresAt : null,
|
||||
scopes: Array.isArray(oauth.scopes) ? (oauth.scopes as string[]) : [],
|
||||
subscriptionType:
|
||||
typeof oauth.subscriptionType === 'string'
|
||||
? oauth.subscriptionType
|
||||
: undefined,
|
||||
rateLimitTier:
|
||||
typeof oauth.rateLimitTier === 'string' ? oauth.rateLimitTier : undefined,
|
||||
exists: true,
|
||||
};
|
||||
}
|
||||
|
||||
function readCodexAccount(index: number): CodexAccountSummary | null {
|
||||
const file = codexAuthPath(index);
|
||||
if (!fs.existsSync(file)) return null;
|
||||
const data = readJson<{
|
||||
OPENAI_API_KEY?: string;
|
||||
tokens?: { id_token?: string; access_token?: string };
|
||||
}>(file);
|
||||
let accountId: string | null = null;
|
||||
let planType: string | null = null;
|
||||
let subscriptionUntil: string | null = null;
|
||||
const idToken = data?.tokens?.id_token;
|
||||
if (idToken && typeof idToken === 'string') {
|
||||
const parts = idToken.split('.');
|
||||
if (parts.length >= 2) {
|
||||
try {
|
||||
const payload = JSON.parse(
|
||||
Buffer.from(parts[1], 'base64').toString('utf-8'),
|
||||
) as Record<string, unknown>;
|
||||
if (typeof payload.sub === 'string') accountId = payload.sub;
|
||||
const auth = payload['https://api.openai.com/auth'] as
|
||||
| Record<string, unknown>
|
||||
| undefined;
|
||||
if (auth) {
|
||||
if (typeof auth.chatgpt_plan_type === 'string') {
|
||||
planType = auth.chatgpt_plan_type;
|
||||
}
|
||||
if (typeof auth.chatgpt_subscription_active_until === 'string') {
|
||||
subscriptionUntil = auth.chatgpt_subscription_active_until;
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
/* ignore parse errors */
|
||||
}
|
||||
}
|
||||
}
|
||||
return {
|
||||
index,
|
||||
accountId,
|
||||
planType,
|
||||
subscriptionUntil,
|
||||
exists: true,
|
||||
};
|
||||
}
|
||||
|
||||
export function listClaudeAccounts(): ClaudeAccountSummary[] {
|
||||
const out: ClaudeAccountSummary[] = [];
|
||||
const def = readClaudeAccount(0);
|
||||
if (def) out.push(def);
|
||||
const dir = path.join(os.homedir(), '.claude-accounts');
|
||||
if (fs.existsSync(dir)) {
|
||||
const entries = fs.readdirSync(dir);
|
||||
const indices = entries
|
||||
.map((e) => Number.parseInt(e, 10))
|
||||
.filter((n) => Number.isFinite(n) && n >= 1)
|
||||
.sort((a, b) => a - b);
|
||||
for (const i of indices) {
|
||||
const acc = readClaudeAccount(i);
|
||||
if (acc) out.push(acc);
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
export function listCodexAccounts(): CodexAccountSummary[] {
|
||||
const out: CodexAccountSummary[] = [];
|
||||
const def = readCodexAccount(0);
|
||||
if (def) out.push(def);
|
||||
const dir = path.join(os.homedir(), '.codex-accounts');
|
||||
if (fs.existsSync(dir)) {
|
||||
const entries = fs.readdirSync(dir);
|
||||
const indices = entries
|
||||
.map((e) => Number.parseInt(e, 10))
|
||||
.filter((n) => Number.isFinite(n) && n >= 1)
|
||||
.sort((a, b) => a - b);
|
||||
for (const i of indices) {
|
||||
const acc = readCodexAccount(i);
|
||||
if (acc) out.push(acc);
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
function pickEnvValue(content: string, key: string): string | undefined {
|
||||
const re = new RegExp(`^${key}=(.*)$`, 'm');
|
||||
const match = content.match(re);
|
||||
if (!match) return undefined;
|
||||
return match[1].trim().replace(/^['"]|['"]$/g, '');
|
||||
}
|
||||
|
||||
function readEnvFile(): string {
|
||||
const file = envFilePath();
|
||||
if (!fs.existsSync(file)) return '';
|
||||
return fs.readFileSync(file, 'utf-8');
|
||||
}
|
||||
|
||||
function readEnvOrProcess(key: string): string | undefined {
|
||||
const fromFile = pickEnvValue(readEnvFile(), key);
|
||||
if (fromFile !== undefined) return fromFile;
|
||||
const fromProc = process.env[key];
|
||||
return fromProc;
|
||||
}
|
||||
|
||||
export function getModelConfig(): ModelConfigSnapshot {
|
||||
const out: Partial<ModelConfigSnapshot> = {};
|
||||
for (const role of ROLE_KEYS) {
|
||||
const model = readEnvOrProcess(`${role}_MODEL`) ?? '';
|
||||
const effort = readEnvOrProcess(`${role}_EFFORT`) ?? '';
|
||||
(out as Record<string, ModelRoleConfig>)[role.toLowerCase()] = {
|
||||
model,
|
||||
effort,
|
||||
};
|
||||
}
|
||||
return out as ModelConfigSnapshot;
|
||||
}
|
||||
|
||||
export interface ModelUpdateInput {
|
||||
owner?: Partial<ModelRoleConfig>;
|
||||
reviewer?: Partial<ModelRoleConfig>;
|
||||
arbiter?: Partial<ModelRoleConfig>;
|
||||
}
|
||||
|
||||
function setOrInsertEnvLine(
|
||||
content: string,
|
||||
key: string,
|
||||
value: string,
|
||||
): string {
|
||||
const re = new RegExp(`^${key}=.*$`, 'm');
|
||||
if (re.test(content)) {
|
||||
return content.replace(re, `${key}=${value}`);
|
||||
}
|
||||
const trimmed = content.replace(/\s*$/, '');
|
||||
return `${trimmed}\n${key}=${value}\n`;
|
||||
}
|
||||
|
||||
export function updateModelConfig(
|
||||
input: ModelUpdateInput,
|
||||
): ModelConfigSnapshot {
|
||||
const file = envFilePath();
|
||||
let content = '';
|
||||
if (fs.existsSync(file)) {
|
||||
content = fs.readFileSync(file, 'utf-8');
|
||||
}
|
||||
|
||||
for (const role of ROLE_KEYS) {
|
||||
const update = (
|
||||
input as Record<string, Partial<ModelRoleConfig> | undefined>
|
||||
)[role.toLowerCase()];
|
||||
if (!update) continue;
|
||||
if (update.model !== undefined) {
|
||||
content = setOrInsertEnvLine(content, `${role}_MODEL`, update.model);
|
||||
}
|
||||
if (update.effort !== undefined) {
|
||||
content = setOrInsertEnvLine(content, `${role}_EFFORT`, update.effort);
|
||||
}
|
||||
}
|
||||
|
||||
const tempPath = `${file}.tmp`;
|
||||
fs.writeFileSync(tempPath, content, { mode: 0o600 });
|
||||
fs.renameSync(tempPath, file);
|
||||
|
||||
return getModelConfig();
|
||||
}
|
||||
|
||||
export function removeAccountDirectory(
|
||||
provider: 'claude' | 'codex',
|
||||
index: number,
|
||||
): void {
|
||||
if (!Number.isFinite(index) || index < 1) {
|
||||
throw new Error('cannot remove default account (index 0)');
|
||||
}
|
||||
const baseDir =
|
||||
provider === 'claude'
|
||||
? path.join(os.homedir(), '.claude-accounts', String(index))
|
||||
: path.join(os.homedir(), '.codex-accounts', String(index));
|
||||
if (!fs.existsSync(baseDir)) {
|
||||
throw new Error(`account directory not found: ${baseDir}`);
|
||||
}
|
||||
fs.rmSync(baseDir, { recursive: true, force: true });
|
||||
}
|
||||
|
||||
export function addClaudeAccountFromToken(token: string): {
|
||||
index: number;
|
||||
accountId: string | null;
|
||||
} {
|
||||
const trimmed = token.trim();
|
||||
if (!trimmed) throw new Error('empty token');
|
||||
|
||||
const parts = trimmed.split('.');
|
||||
if (parts.length < 2) {
|
||||
throw new Error('invalid OAuth token: expected JWT format');
|
||||
}
|
||||
let payload: Record<string, unknown> = {};
|
||||
try {
|
||||
payload = JSON.parse(
|
||||
Buffer.from(parts[1], 'base64').toString('utf-8'),
|
||||
) as Record<string, unknown>;
|
||||
} catch {
|
||||
throw new Error('invalid OAuth token: payload not parseable');
|
||||
}
|
||||
const exp = typeof payload.exp === 'number' ? payload.exp * 1000 : 0;
|
||||
const accountId = typeof payload.sub === 'string' ? payload.sub : null;
|
||||
|
||||
const baseDir = path.join(os.homedir(), '.claude-accounts');
|
||||
if (!fs.existsSync(baseDir)) {
|
||||
fs.mkdirSync(baseDir, { recursive: true, mode: 0o700 });
|
||||
}
|
||||
|
||||
let nextIndex = 1;
|
||||
while (
|
||||
fs.existsSync(path.join(baseDir, String(nextIndex), '.credentials.json'))
|
||||
) {
|
||||
nextIndex += 1;
|
||||
}
|
||||
const dir = path.join(baseDir, String(nextIndex));
|
||||
fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
|
||||
|
||||
const creds = {
|
||||
claudeAiOauth: {
|
||||
accessToken: trimmed,
|
||||
refreshToken: '',
|
||||
expiresAt: exp,
|
||||
scopes: ['user:profile', 'user:inference'],
|
||||
},
|
||||
};
|
||||
const credsPath = path.join(dir, '.credentials.json');
|
||||
const tempPath = `${credsPath}.tmp`;
|
||||
fs.writeFileSync(tempPath, JSON.stringify(creds, null, 2), { mode: 0o600 });
|
||||
fs.renameSync(tempPath, credsPath);
|
||||
|
||||
return { index: nextIndex, accountId };
|
||||
}
|
||||
@@ -87,6 +87,8 @@ export interface WebDashboardRoomTurn {
|
||||
updatedAt: string;
|
||||
completedAt: string | null;
|
||||
lastError: string | null;
|
||||
progressText: string | null;
|
||||
progressUpdatedAt: string | null;
|
||||
}
|
||||
|
||||
export interface WebDashboardRoomTurnOutput {
|
||||
@@ -419,22 +421,82 @@ function sanitizeRoomMessage(message: NewMessage): WebDashboardRoomMessage {
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* SSOT for live progress: scan the `messages` table for the most recent
|
||||
* TASK_STATUS_MESSAGE_PREFIX-prefixed message that belongs to the current
|
||||
* turn (sender role match + timestamp >= turn.created_at). The Discord
|
||||
* bridge ingests every edit of the bot's live message back into messages,
|
||||
* so this single source replaces the legacy paired_turns.progress_text
|
||||
* column for derivation.
|
||||
*/
|
||||
const TASK_STATUS_PREFIX = '';
|
||||
|
||||
function turnRoleFromSenderName(name: string | null | undefined): string {
|
||||
const v = (name ?? '').toLowerCase();
|
||||
if (v.includes('오너') || v.includes('owner')) return 'owner';
|
||||
if (v.includes('리뷰어') || v.includes('reviewer')) return 'reviewer';
|
||||
if (v.includes('중재자') || v.includes('arbiter')) return 'arbiter';
|
||||
return 'human';
|
||||
}
|
||||
|
||||
function deriveLiveProgress(
|
||||
turnRole: string,
|
||||
turnCreatedAt: string,
|
||||
messages: NewMessage[],
|
||||
): { progressText: string | null; progressUpdatedAt: string | null } {
|
||||
const startMs = new Date(turnCreatedAt).getTime();
|
||||
const targetRole = turnRoleFromSenderName(turnRole);
|
||||
for (let i = messages.length - 1; i >= 0; i--) {
|
||||
const m = messages[i];
|
||||
const content = m.content ?? '';
|
||||
if (!content.startsWith(TASK_STATUS_PREFIX)) continue;
|
||||
const ts = m.timestamp ? new Date(m.timestamp).getTime() : 0;
|
||||
if (Number.isFinite(startMs) && ts < startMs) continue;
|
||||
const role = turnRoleFromSenderName(m.sender_name);
|
||||
if (role !== targetRole) continue;
|
||||
const body = content.slice(TASK_STATUS_PREFIX.length);
|
||||
const stripped = body.replace(/\n\n\d+[초smhMSH]?$/, '').trim();
|
||||
if (!stripped) continue;
|
||||
return { progressText: stripped, progressUpdatedAt: m.timestamp };
|
||||
}
|
||||
return { progressText: null, progressUpdatedAt: null };
|
||||
}
|
||||
|
||||
function sanitizeRoomTurn(
|
||||
turn: PairedTurnRecord,
|
||||
attempt: PairedTurnAttemptRecord | null,
|
||||
messages: NewMessage[] = [],
|
||||
): WebDashboardRoomTurn {
|
||||
const role = attempt?.role ?? turn.role;
|
||||
const createdAt = attempt?.created_at ?? turn.created_at;
|
||||
const completedAt = attempt?.completed_at ?? turn.completed_at;
|
||||
|
||||
// Read paired_turns.progress_text first (written by recordTurnProgress
|
||||
// when the controller is in the loop). If that's empty AND the turn is
|
||||
// active, fall back to scanning the messages table for the most recent
|
||||
// TASK_STATUS-prefixed message — same content the Discord bridge ingests.
|
||||
let progressText = turn.progress_text ?? null;
|
||||
let progressUpdatedAt = turn.progress_updated_at ?? null;
|
||||
if ((!progressText || !progressText.trim()) && completedAt === null) {
|
||||
const live = deriveLiveProgress(role, createdAt, messages);
|
||||
if (live.progressText) {
|
||||
progressText = live.progressText;
|
||||
progressUpdatedAt = live.progressUpdatedAt;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
turnId: turn.turn_id,
|
||||
role: attempt?.role ?? turn.role,
|
||||
role,
|
||||
intentKind: attempt?.intent_kind ?? turn.intent_kind,
|
||||
state: attempt?.state ?? turn.state,
|
||||
attemptNo: attempt?.attempt_no ?? turn.attempt_no,
|
||||
executorServiceId: attempt?.executor_service_id ?? turn.executor_service_id,
|
||||
executorAgentType: attempt?.executor_agent_type ?? turn.executor_agent_type,
|
||||
activeRunId: attempt?.active_run_id ?? null,
|
||||
createdAt: attempt?.created_at ?? turn.created_at,
|
||||
createdAt,
|
||||
updatedAt: attempt?.updated_at ?? turn.updated_at,
|
||||
completedAt: attempt?.completed_at ?? turn.completed_at,
|
||||
completedAt,
|
||||
lastError:
|
||||
(attempt?.last_error ?? turn.last_error)
|
||||
? buildRoomPreview(
|
||||
@@ -442,6 +504,8 @@ function sanitizeRoomTurn(
|
||||
ROOM_MESSAGE_PREVIEW_MAX_LENGTH,
|
||||
)
|
||||
: null,
|
||||
progressText,
|
||||
progressUpdatedAt,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -506,7 +570,7 @@ export function buildWebDashboardRoomActivity(args: {
|
||||
roundTripCount: args.pairedTask.round_trip_count,
|
||||
updatedAt: args.pairedTask.updated_at,
|
||||
currentTurn: currentTurn
|
||||
? sanitizeRoomTurn(currentTurn, currentAttempt)
|
||||
? sanitizeRoomTurn(currentTurn, currentAttempt, args.messages)
|
||||
: null,
|
||||
outputs: args.outputs.slice(-outputLimit).map(sanitizeRoomTurnOutput),
|
||||
}
|
||||
|
||||
@@ -14,8 +14,11 @@ import {
|
||||
getPairedTaskById,
|
||||
getPairedTurnAttempts,
|
||||
getPairedTurnOutputs,
|
||||
getRecentPairedTurnOutputsForChat,
|
||||
getPairedTurnsForTask,
|
||||
getLatestPairedTurnForTask,
|
||||
getRecentChatMessages,
|
||||
getRecentChatMessagesBatch,
|
||||
getTaskById,
|
||||
hasMessage,
|
||||
storeChatMetadata,
|
||||
@@ -43,6 +46,14 @@ import {
|
||||
buildWebDashboardOverview,
|
||||
sanitizeScheduledTask,
|
||||
} from './web-dashboard-data.js';
|
||||
import {
|
||||
addClaudeAccountFromToken,
|
||||
getModelConfig,
|
||||
listClaudeAccounts,
|
||||
listCodexAccounts,
|
||||
removeAccountDirectory,
|
||||
updateModelConfig,
|
||||
} from './settings-store.js';
|
||||
|
||||
const DEFAULT_STATUS_MAX_AGE_MS = 10 * 60 * 1000;
|
||||
const ROOM_MESSAGE_ID_CACHE_LIMIT = 500;
|
||||
@@ -80,6 +91,7 @@ export interface ServiceRestartRecord {
|
||||
export interface WebDashboardHandlerOptions {
|
||||
staticDir?: string;
|
||||
statusMaxAgeMs?: number;
|
||||
startBackgroundCacheRefresh?: boolean;
|
||||
readStatusSnapshots?: (maxAgeMs: number) => StatusSnapshot[];
|
||||
getTasks?: () => ScheduledTask[];
|
||||
getTaskById?: (id: string) => ScheduledTask | undefined;
|
||||
@@ -114,8 +126,10 @@ export interface WebDashboardHandlerOptions {
|
||||
getPairedTasks?: () => PairedTask[];
|
||||
getLatestPairedTaskForChat?: (chatJid: string) => PairedTask | undefined;
|
||||
getPairedTurnsForTask?: typeof getPairedTurnsForTask;
|
||||
getLatestPairedTurnForTask?: typeof getLatestPairedTurnForTask;
|
||||
getPairedTurnAttempts?: typeof getPairedTurnAttempts;
|
||||
getPairedTurnOutputs?: typeof getPairedTurnOutputs;
|
||||
getRecentPairedTurnOutputsForChat?: typeof getRecentPairedTurnOutputsForChat;
|
||||
getRecentChatMessages?: typeof getRecentChatMessages;
|
||||
getPairedTaskById?: (id: string) => PairedTask | undefined;
|
||||
updatePairedTaskIfUnchanged?: (
|
||||
@@ -150,14 +164,41 @@ export interface StartedWebDashboardServer {
|
||||
stop: () => void;
|
||||
}
|
||||
|
||||
function jsonResponse(value: unknown, init?: ResponseInit): Response {
|
||||
return new Response(JSON.stringify(value, null, 2), {
|
||||
const ROOMS_TIMELINE_BG_INTERVAL_MS = 2000;
|
||||
let roomsTimelineCache: {
|
||||
key: string;
|
||||
builtAt: number;
|
||||
rawJson: string;
|
||||
gzipBuffer: Uint8Array;
|
||||
} | null = null;
|
||||
|
||||
function jsonResponse(
|
||||
value: unknown,
|
||||
init?: ResponseInit,
|
||||
request?: Request,
|
||||
): Response {
|
||||
const body = JSON.stringify(value);
|
||||
const acceptsGzip =
|
||||
request?.headers.get('accept-encoding')?.includes('gzip') ?? false;
|
||||
const baseHeaders: Record<string, string> = {
|
||||
'content-type': 'application/json; charset=utf-8',
|
||||
...(init?.headers as Record<string, string> | undefined),
|
||||
};
|
||||
if (acceptsGzip && body.length > 1024) {
|
||||
const compressed = Bun.gzipSync(new TextEncoder().encode(body));
|
||||
return new Response(compressed, {
|
||||
...init,
|
||||
headers: {
|
||||
'content-type': 'application/json; charset=utf-8',
|
||||
...(init?.headers ?? {}),
|
||||
...baseHeaders,
|
||||
'content-encoding': 'gzip',
|
||||
vary: 'accept-encoding',
|
||||
},
|
||||
});
|
||||
}
|
||||
return new Response(body, {
|
||||
...init,
|
||||
headers: baseHeaders,
|
||||
});
|
||||
}
|
||||
|
||||
function getContentType(filePath: string): string {
|
||||
@@ -509,20 +550,29 @@ function sanitizeRoomMessageRequestId(value: unknown): string | null {
|
||||
return safe || null;
|
||||
}
|
||||
|
||||
async function readRoomMessageBody(
|
||||
request: Request,
|
||||
): Promise<{ text: string; requestId: string | null } | null> {
|
||||
async function readRoomMessageBody(request: Request): Promise<{
|
||||
text: string;
|
||||
requestId: string | null;
|
||||
nickname: string | null;
|
||||
} | null> {
|
||||
try {
|
||||
const body = (await request.json()) as {
|
||||
text?: unknown;
|
||||
requestId?: unknown;
|
||||
nickname?: unknown;
|
||||
};
|
||||
if (typeof body.text !== 'string') return null;
|
||||
const text = body.text.trim();
|
||||
if (!text) return null;
|
||||
let nickname: string | null = null;
|
||||
if (typeof body.nickname === 'string') {
|
||||
const trimmed = body.nickname.trim().slice(0, 32);
|
||||
if (trimmed) nickname = trimmed;
|
||||
}
|
||||
return {
|
||||
text: text.length > 8000 ? text.slice(0, 8000) : text,
|
||||
requestId: sanitizeRoomMessageRequestId(body.requestId),
|
||||
nickname,
|
||||
};
|
||||
} catch {
|
||||
return null;
|
||||
@@ -673,8 +723,12 @@ export function createWebDashboardHandler(
|
||||
opts.getLatestPairedTaskForChat ?? getLatestPairedTaskForChat;
|
||||
const loadPairedTurnsForTask =
|
||||
opts.getPairedTurnsForTask ?? getPairedTurnsForTask;
|
||||
const loadLatestPairedTurnForTask =
|
||||
opts.getLatestPairedTurnForTask ?? getLatestPairedTurnForTask;
|
||||
const loadPairedTurnOutputs =
|
||||
opts.getPairedTurnOutputs ?? getPairedTurnOutputs;
|
||||
const loadRecentPairedTurnOutputsForChat =
|
||||
opts.getRecentPairedTurnOutputsForChat ?? getRecentPairedTurnOutputsForChat;
|
||||
const loadPairedTurnAttempts =
|
||||
opts.getPairedTurnAttempts ?? getPairedTurnAttempts;
|
||||
const loadRecentChatMessages =
|
||||
@@ -690,6 +744,99 @@ export function createWebDashboardHandler(
|
||||
const restartServiceStack =
|
||||
opts.restartServiceStack ?? restartEjclawStackServices;
|
||||
const statusMaxAgeMs = opts.statusMaxAgeMs ?? DEFAULT_STATUS_MAX_AGE_MS;
|
||||
|
||||
function buildRoomsTimelineResult(): Record<
|
||||
string,
|
||||
ReturnType<typeof buildWebDashboardRoomActivity>
|
||||
> {
|
||||
const snapshots = readSnapshots(statusMaxAgeMs);
|
||||
const uniqueByJid = new Map<
|
||||
string,
|
||||
{
|
||||
snapshot: (typeof snapshots)[number];
|
||||
entry: (typeof snapshots)[number]['entries'][number];
|
||||
}
|
||||
>();
|
||||
for (const snapshot of snapshots) {
|
||||
for (const entry of snapshot.entries) {
|
||||
const existing = uniqueByJid.get(entry.jid);
|
||||
if (
|
||||
!existing ||
|
||||
existing.snapshot.updatedAt.localeCompare(snapshot.updatedAt) < 0
|
||||
) {
|
||||
uniqueByJid.set(entry.jid, { snapshot, entry });
|
||||
}
|
||||
}
|
||||
}
|
||||
const result: Record<
|
||||
string,
|
||||
ReturnType<typeof buildWebDashboardRoomActivity>
|
||||
> = {};
|
||||
for (const [jid, { snapshot, entry }] of uniqueByJid) {
|
||||
const pairedTask = loadLatestPairedTaskForChat(jid) ?? null;
|
||||
const messages = loadRecentChatMessages(jid, 8);
|
||||
const outputs = loadRecentPairedTurnOutputsForChat(jid, 8);
|
||||
if (!pairedTask && messages.length === 0 && outputs.length === 0)
|
||||
continue;
|
||||
const latestTurn = pairedTask
|
||||
? loadLatestPairedTurnForTask(pairedTask.id)
|
||||
: null;
|
||||
const turns = latestTurn ? [latestTurn] : [];
|
||||
result[jid] = buildWebDashboardRoomActivity({
|
||||
serviceId: snapshot.serviceId,
|
||||
entry,
|
||||
pairedTask,
|
||||
turns,
|
||||
attempts: [],
|
||||
outputs,
|
||||
messages,
|
||||
outputLimit: 8,
|
||||
});
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
function computeRoomsCacheKey(): string {
|
||||
return readSnapshots(statusMaxAgeMs)
|
||||
.map((s) => s.updatedAt)
|
||||
.sort()
|
||||
.join('|');
|
||||
}
|
||||
|
||||
function ensureRoomsTimelineCache(): NonNullable<typeof roomsTimelineCache> {
|
||||
const key = computeRoomsCacheKey();
|
||||
if (roomsTimelineCache && roomsTimelineCache.key === key) {
|
||||
return roomsTimelineCache;
|
||||
}
|
||||
const result = buildRoomsTimelineResult();
|
||||
const rawJson = JSON.stringify(result);
|
||||
const gzipBuffer = Bun.gzipSync(new TextEncoder().encode(rawJson));
|
||||
roomsTimelineCache = {
|
||||
key,
|
||||
builtAt: Date.now(),
|
||||
rawJson,
|
||||
gzipBuffer,
|
||||
};
|
||||
return roomsTimelineCache;
|
||||
}
|
||||
|
||||
if (opts.startBackgroundCacheRefresh !== false) {
|
||||
setTimeout(() => {
|
||||
try {
|
||||
ensureRoomsTimelineCache();
|
||||
} catch {
|
||||
/* warm-up failure is non-fatal */
|
||||
}
|
||||
}, 0);
|
||||
setInterval(() => {
|
||||
try {
|
||||
ensureRoomsTimelineCache();
|
||||
} catch {
|
||||
/* refresh failure is non-fatal */
|
||||
}
|
||||
}, ROOMS_TIMELINE_BG_INTERVAL_MS).unref();
|
||||
}
|
||||
|
||||
const seenRoomMessageIds: string[] = [];
|
||||
const seenRoomMessageIdSet = new Set<string>();
|
||||
const dismissedInboxKeys = new Set<string>();
|
||||
@@ -969,7 +1116,7 @@ export function createWebDashboardHandler(
|
||||
id,
|
||||
chat_jid: messageRoomJid,
|
||||
sender: 'web-dashboard',
|
||||
sender_name: 'Web Dashboard',
|
||||
sender_name: body.nickname ?? 'Web Dashboard',
|
||||
content: body.text,
|
||||
timestamp,
|
||||
is_from_me: false,
|
||||
@@ -1099,6 +1246,73 @@ export function createWebDashboardHandler(
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
url.pathname === '/api/settings/models' &&
|
||||
(request.method === 'PUT' || request.method === 'PATCH')
|
||||
) {
|
||||
let body: unknown = null;
|
||||
try {
|
||||
body = await request.json();
|
||||
} catch {
|
||||
return jsonResponse({ error: 'Invalid JSON body' }, { status: 400 });
|
||||
}
|
||||
if (!body || typeof body !== 'object') {
|
||||
return jsonResponse(
|
||||
{ error: 'Body must be a JSON object' },
|
||||
{ status: 400 },
|
||||
);
|
||||
}
|
||||
try {
|
||||
const next = updateModelConfig(body as Record<string, unknown>);
|
||||
return jsonResponse(next);
|
||||
} catch (err) {
|
||||
const message = err instanceof Error ? err.message : String(err);
|
||||
return jsonResponse({ error: message }, { status: 500 });
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
const accountAddMatch = url.pathname.match(
|
||||
/^\/api\/settings\/accounts\/(claude)$/,
|
||||
);
|
||||
if (accountAddMatch && request.method === 'POST') {
|
||||
let body: { token?: unknown } | null = null;
|
||||
try {
|
||||
body = (await request.json()) as { token?: unknown };
|
||||
} catch {
|
||||
return jsonResponse({ error: 'Invalid JSON body' }, { status: 400 });
|
||||
}
|
||||
const token = typeof body?.token === 'string' ? body.token.trim() : '';
|
||||
if (!token) {
|
||||
return jsonResponse({ error: 'token is required' }, { status: 400 });
|
||||
}
|
||||
try {
|
||||
const result = addClaudeAccountFromToken(token);
|
||||
return jsonResponse({ ok: true, ...result });
|
||||
} catch (err) {
|
||||
const message = err instanceof Error ? err.message : String(err);
|
||||
return jsonResponse({ error: message }, { status: 400 });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
const accountDelMatch = url.pathname.match(
|
||||
/^\/api\/settings\/accounts\/(claude|codex)\/(\d+)$/,
|
||||
);
|
||||
if (accountDelMatch && request.method === 'DELETE') {
|
||||
const provider = accountDelMatch[1] as 'claude' | 'codex';
|
||||
const index = Number.parseInt(accountDelMatch[2], 10);
|
||||
try {
|
||||
removeAccountDirectory(provider, index);
|
||||
return jsonResponse({ ok: true, provider, index });
|
||||
} catch (err) {
|
||||
const message = err instanceof Error ? err.message : String(err);
|
||||
return jsonResponse({ error: message }, { status: 400 });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (url.pathname === '/api/tasks' && request.method === 'POST') {
|
||||
if (!loadRoomBindings) {
|
||||
return jsonResponse(
|
||||
@@ -1293,6 +1507,106 @@ export function createWebDashboardHandler(
|
||||
return jsonResponse(loadTasks().map(sanitizeScheduledTask));
|
||||
}
|
||||
|
||||
if (url.pathname === '/api/settings/accounts') {
|
||||
return jsonResponse({
|
||||
claude: listClaudeAccounts(),
|
||||
codex: listCodexAccounts(),
|
||||
});
|
||||
}
|
||||
|
||||
if (url.pathname === '/api/settings/models') {
|
||||
return jsonResponse(getModelConfig());
|
||||
}
|
||||
|
||||
if (url.pathname === '/api/stream') {
|
||||
const encoder = new TextEncoder();
|
||||
const stream = new ReadableStream({
|
||||
start(controller) {
|
||||
let lastBuiltAt = 0;
|
||||
let closed = false;
|
||||
|
||||
const enqueue = (chunk: string) => {
|
||||
if (closed) return;
|
||||
try {
|
||||
controller.enqueue(encoder.encode(chunk));
|
||||
} catch {
|
||||
closed = true;
|
||||
}
|
||||
};
|
||||
|
||||
// Initial: send retry hint and seed event
|
||||
enqueue(`retry: 3000\n\n`);
|
||||
try {
|
||||
const cache = ensureRoomsTimelineCache();
|
||||
lastBuiltAt = cache.builtAt;
|
||||
enqueue(`event: rooms-timeline\ndata: ${cache.rawJson}\n\n`);
|
||||
} catch {
|
||||
/* warm-up failure is non-fatal */
|
||||
}
|
||||
|
||||
const tick = () => {
|
||||
if (closed) return;
|
||||
try {
|
||||
const cache = ensureRoomsTimelineCache();
|
||||
if (cache.builtAt !== lastBuiltAt) {
|
||||
lastBuiltAt = cache.builtAt;
|
||||
enqueue(`event: rooms-timeline\ndata: ${cache.rawJson}\n\n`);
|
||||
} else {
|
||||
// heartbeat to keep connection alive through proxies
|
||||
enqueue(`: ping ${Date.now()}\n\n`);
|
||||
}
|
||||
} catch {
|
||||
/* skip this tick */
|
||||
}
|
||||
};
|
||||
|
||||
const interval = setInterval(tick, 1500);
|
||||
const close = () => {
|
||||
if (closed) return;
|
||||
closed = true;
|
||||
clearInterval(interval);
|
||||
try {
|
||||
controller.close();
|
||||
} catch {
|
||||
/* already closed */
|
||||
}
|
||||
};
|
||||
request.signal.addEventListener('abort', close);
|
||||
},
|
||||
});
|
||||
|
||||
return new Response(stream, {
|
||||
headers: {
|
||||
'content-type': 'text/event-stream; charset=utf-8',
|
||||
'cache-control': 'no-cache, no-transform',
|
||||
connection: 'keep-alive',
|
||||
'x-accel-buffering': 'no',
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
if (url.pathname === '/api/rooms-timeline') {
|
||||
const cache = ensureRoomsTimelineCache();
|
||||
const acceptsGzip =
|
||||
request.headers.get('accept-encoding')?.includes('gzip') ?? false;
|
||||
if (acceptsGzip) {
|
||||
return new Response(cache.gzipBuffer, {
|
||||
headers: {
|
||||
'content-type': 'application/json; charset=utf-8',
|
||||
'content-encoding': 'gzip',
|
||||
vary: 'accept-encoding',
|
||||
'x-cache-age': String(Date.now() - cache.builtAt),
|
||||
},
|
||||
});
|
||||
}
|
||||
return new Response(cache.rawJson, {
|
||||
headers: {
|
||||
'content-type': 'application/json; charset=utf-8',
|
||||
'x-cache-age': String(Date.now() - cache.builtAt),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
if (url.pathname.startsWith('/api/')) {
|
||||
return jsonResponse({ error: 'Not found' }, { status: 404 });
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user