@aliceshimada/mica 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +14 -0
- package/CONTRIBUTING.md +22 -0
- package/LICENSE +21 -0
- package/README.md +308 -0
- package/README.zh-CN.md +308 -0
- package/SECURITY.md +22 -0
- package/dist/src/backend/agentRegistry.js +115 -0
- package/dist/src/backend/backendQueue.js +212 -0
- package/dist/src/backend/backendState.js +99 -0
- package/dist/src/backend/notebookRegistry.js +136 -0
- package/dist/src/backend/protocol.js +32 -0
- package/dist/src/bridge/httpBridge.js +366 -0
- package/dist/src/bridge/requestQueue.js +200 -0
- package/dist/src/bun/dashboard.js +387 -0
- package/dist/src/bun/httpServer.js +356 -0
- package/dist/src/bun/index.js +91 -0
- package/dist/src/cli/doctor.js +235 -0
- package/dist/src/cli/index.js +125 -0
- package/dist/src/index.js +54 -0
- package/dist/src/mcp/backendTools.js +216 -0
- package/dist/src/mcp/descriptions.js +6 -0
- package/dist/src/mcp/prompts.js +52 -0
- package/dist/src/mcp/toolResults.js +183 -0
- package/dist/src/mcp/toolSchemas.js +60 -0
- package/dist/src/mcp/tools.js +161 -0
- package/dist/src/runtime/config.js +76 -0
- package/dist/src/runtime/session.js +14 -0
- package/dist/src/runtimeOptions.js +3 -0
- package/dist/src/types.js +2 -0
- package/package.json +63 -0
- package/paclet/FrontEnd/Palettes/MMAAgentBridge.nb +22 -0
- package/paclet/Kernel/MMAAgentBridge.wl +1831 -0
- package/paclet/Kernel/init.wl +1 -0
- package/paclet/PacletInfo.wl +14 -0
- package/scripts/install.js +526 -0
- package/src/bun/index.ts +120 -0
|
@@ -0,0 +1,387 @@
|
|
|
1
|
+
export function renderDashboard() {
|
|
2
|
+
return `<!doctype html>
|
|
3
|
+
<html lang="en">
|
|
4
|
+
<head>
|
|
5
|
+
<meta charset="utf-8" />
|
|
6
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
7
|
+
<title>MICA Dashboard</title>
|
|
8
|
+
<style>
|
|
9
|
+
:root {
|
|
10
|
+
color-scheme: dark;
|
|
11
|
+
--bg: #0b0d12;
|
|
12
|
+
--surface: #11151c;
|
|
13
|
+
--surface-raised: #0f131a;
|
|
14
|
+
--surface-selected: #121926;
|
|
15
|
+
--border: #252c3a;
|
|
16
|
+
--border-strong: #3b82f6;
|
|
17
|
+
--text: #e6edf3;
|
|
18
|
+
--muted: #8b95a7;
|
|
19
|
+
--subtle: #8b95a7;
|
|
20
|
+
--live: #34d399;
|
|
21
|
+
--degraded: #fbbf24;
|
|
22
|
+
--offline: #64748b;
|
|
23
|
+
}
|
|
24
|
+
* { box-sizing: border-box; }
|
|
25
|
+
body { margin: 0; font-family: ui-sans-serif, system-ui, -apple-system, Segoe UI, sans-serif; background: var(--bg); color: var(--text); -webkit-font-smoothing: antialiased; }
|
|
26
|
+
main { max-width: 1240px; margin: 0 auto; padding: 32px 24px; }
|
|
27
|
+
.hero { display: flex; justify-content: space-between; align-items: flex-start; gap: 16px; margin-bottom: 22px; }
|
|
28
|
+
.eyebrow { margin: 0 0 6px; color: var(--subtle); font-size: 0.72rem; letter-spacing: 0.08em; text-transform: uppercase; }
|
|
29
|
+
h1 { margin: 0; font-size: clamp(2rem, 4vw, 3rem); line-height: 1; letter-spacing: -0.04em; }
|
|
30
|
+
.subtitle { margin: 10px 0 0; color: var(--muted); font-size: 0.9rem; }
|
|
31
|
+
.auth-pill { display: inline-flex; align-items: center; gap: 8px; border: 1px solid #23483a; background: #0f1714; color: #8ee6bd; border-radius: 999px; padding: 7px 11px; font-size: 0.78rem; white-space: nowrap; }
|
|
32
|
+
.status-dot { width: 7px; height: 7px; border-radius: 999px; display: inline-block; background: currentColor; }
|
|
33
|
+
.status-dot.live { color: var(--live); }
|
|
34
|
+
.status-dot.degraded { color: var(--degraded); }
|
|
35
|
+
.status-dot.offline { color: var(--offline); }
|
|
36
|
+
#auth-message { margin: 0 0 16px; color: var(--muted); min-height: 1.2em; }
|
|
37
|
+
.card-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); gap: 12px; }
|
|
38
|
+
.card { min-height: 116px; border: 1px solid var(--border); border-radius: 14px; background: var(--surface); color: var(--text); padding: 14px; text-align: left; transition: border-color 150ms ease, background 150ms ease, transform 150ms ease; }
|
|
39
|
+
.card--interactive { cursor: pointer; }
|
|
40
|
+
.card--interactive:hover { border-color: #4b5871; transform: translateY(-1px); }
|
|
41
|
+
.card--interactive:focus-visible, .detail-close:focus-visible { outline: 0; box-shadow: 0 0 0 2px var(--bg), 0 0 0 4px var(--border-strong); }
|
|
42
|
+
.card--selected { background: var(--surface-selected); border-color: var(--border-strong); }
|
|
43
|
+
.card-header { display: flex; justify-content: space-between; align-items: center; gap: 8px; margin-bottom: 13px; }
|
|
44
|
+
.card-title { color: var(--subtle); font-size: 0.72rem; letter-spacing: 0.08em; text-transform: uppercase; }
|
|
45
|
+
.card--selected .card-title { color: #9fc3ff; }
|
|
46
|
+
.card-value { font-size: 1.45rem; line-height: 1.1; font-weight: 680; letter-spacing: -0.02em; font-variant-numeric: tabular-nums; }
|
|
47
|
+
.card-footer { margin-top: 13px; padding-top: 10px; border-top: 1px solid #222938; color: var(--muted); font-size: 0.78rem; font-variant-numeric: tabular-nums; }
|
|
48
|
+
.chevron { color: var(--subtle); font-size: 0.8rem; }
|
|
49
|
+
.card--selected .chevron { color: #9fc3ff; }
|
|
50
|
+
.detail-panel { margin-top: 16px; border: 1px solid #252f40; border-radius: 16px; background: var(--surface-raised); overflow: hidden; }
|
|
51
|
+
.detail-panel[hidden] { display: none; }
|
|
52
|
+
.detail-header { display: flex; justify-content: space-between; align-items: center; gap: 12px; padding: 14px 16px; border-bottom: 1px solid #222938; background: #111722; }
|
|
53
|
+
.detail-title { margin: 0; font-size: 1rem; letter-spacing: -0.01em; }
|
|
54
|
+
.detail-subtitle { margin: 4px 0 0; color: var(--muted); font-size: 0.78rem; }
|
|
55
|
+
.detail-close { border: 1px solid #2d3546; border-radius: 10px; background: #151b26; color: #cbd5e1; padding: 7px 11px; cursor: pointer; }
|
|
56
|
+
.detail-body { padding: 12px 16px 16px; max-height: 40vh; overflow: auto; }
|
|
57
|
+
.detail-table { display: grid; gap: 8px; }
|
|
58
|
+
.detail-row { display: grid; grid-template-columns: 1.4fr 0.8fr 1fr 1fr 1fr; gap: 10px; align-items: center; border: 1px solid #222938; border-radius: 11px; background: var(--surface); padding: 10px 8px; font-size: 0.82rem; font-variant-numeric: tabular-nums; }
|
|
59
|
+
.detail-row--header { border: 0; background: transparent; padding-bottom: 0; color: var(--subtle); font-size: 0.68rem; letter-spacing: 0.08em; text-transform: uppercase; }
|
|
60
|
+
.mono { font-family: ui-monospace, Cascadia Code, Consolas, monospace; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
61
|
+
.chip { display: inline-flex; align-items: center; gap: 6px; width: fit-content; border-radius: 999px; padding: 2px 9px; font-size: 0.76rem; }
|
|
62
|
+
.chip::before { content: ""; width: 6px; height: 6px; border-radius: 999px; background: currentColor; }
|
|
63
|
+
.chip--live { background: rgba(52, 211, 153, 0.12); color: #7ee7b8; }
|
|
64
|
+
.chip--degraded { background: rgba(251, 191, 36, 0.12); color: #f7d36d; }
|
|
65
|
+
.chip--offline, .chip--retired { background: rgba(100, 116, 139, 0.16); color: #aeb8c7; }
|
|
66
|
+
.empty-state { border: 1px dashed #30384a; border-radius: 12px; padding: 24px 16px; text-align: center; color: var(--muted); }
|
|
67
|
+
@media (max-width: 760px) { .hero { flex-direction: column; } .detail-row { grid-template-columns: 1fr; } .detail-row--header { display: none; } .detail-cell::before { content: attr(data-label) ": "; color: var(--subtle); font-size: 0.72rem; letter-spacing: 0.08em; text-transform: uppercase; } }
|
|
68
|
+
@media (prefers-reduced-motion: reduce) { .card { transition: none; } .card--interactive:hover { transform: none; } }
|
|
69
|
+
</style>
|
|
70
|
+
</head>
|
|
71
|
+
<body>
|
|
72
|
+
<main>
|
|
73
|
+
<header class="hero">
|
|
74
|
+
<div>
|
|
75
|
+
<p class="eyebrow">Local bridge diagnostics</p>
|
|
76
|
+
<h1>MICA Dashboard</h1>
|
|
77
|
+
<p class="subtitle">Taste-polished status overview for the local Wolfram notebook bridge. <span id="updated-at">Waiting for data.</span></p>
|
|
78
|
+
</div>
|
|
79
|
+
<div class="auth-pill"><span class="status-dot live"></span><span id="auth-state">Auth pending</span></div>
|
|
80
|
+
</header>
|
|
81
|
+
<p id="auth-message"></p>
|
|
82
|
+
|
|
83
|
+
<div class="card-grid" aria-label="Diagnostic modules">
|
|
84
|
+
<section class="card" aria-label="Server">
|
|
85
|
+
<div class="card-header"><span class="card-title">Server</span><span id="server-dot" class="status-dot offline"></span></div>
|
|
86
|
+
<div id="server-value" class="card-value">Waiting</div>
|
|
87
|
+
<div id="server-meta" class="card-footer">No authenticated data yet</div>
|
|
88
|
+
</section>
|
|
89
|
+
|
|
90
|
+
<button id="agents-card" class="card card--interactive" type="button" data-detail="agents" aria-controls="detail-panel" aria-expanded="false">
|
|
91
|
+
<div class="card-header"><span class="card-title">Agents</span><span class="chevron">▸</span></div>
|
|
92
|
+
<div id="agents-value" class="card-value">Locked</div>
|
|
93
|
+
<div id="agents-meta" class="card-footer">Open dashboard token URL</div>
|
|
94
|
+
</button>
|
|
95
|
+
|
|
96
|
+
<button id="notebooks-card" class="card card--interactive" type="button" data-detail="notebooks" aria-controls="detail-panel" aria-expanded="false">
|
|
97
|
+
<div class="card-header"><span class="card-title">Notebooks</span><span class="chevron">▸</span></div>
|
|
98
|
+
<div id="notebooks-value" class="card-value">Locked</div>
|
|
99
|
+
<div id="notebooks-meta" class="card-footer">Open dashboard token URL</div>
|
|
100
|
+
</button>
|
|
101
|
+
|
|
102
|
+
<section class="card" aria-label="Requests">
|
|
103
|
+
<div class="card-header"><span class="card-title">Requests</span><span class="status-dot offline"></span></div>
|
|
104
|
+
<div id="requests-value" class="card-value">Waiting</div>
|
|
105
|
+
<div id="requests-meta" class="card-footer">No queue data yet</div>
|
|
106
|
+
</section>
|
|
107
|
+
|
|
108
|
+
<section class="card" aria-label="Security">
|
|
109
|
+
<div class="card-header"><span class="card-title">Security</span><span id="security-dot" class="status-dot offline"></span></div>
|
|
110
|
+
<div id="security-value" class="card-value">Token hidden</div>
|
|
111
|
+
<div id="security-meta" class="card-footer token-hidden">Bearer auth status pending</div>
|
|
112
|
+
</section>
|
|
113
|
+
</div>
|
|
114
|
+
|
|
115
|
+
<section id="detail-panel" class="detail-panel" role="region" aria-labelledby="detail-title" aria-live="polite" hidden>
|
|
116
|
+
<div class="detail-header">
|
|
117
|
+
<div>
|
|
118
|
+
<h2 id="detail-title" class="detail-title">Details</h2>
|
|
119
|
+
<p id="detail-subtitle" class="detail-subtitle">Select Agents or Notebooks to inspect live bridge records.</p>
|
|
120
|
+
</div>
|
|
121
|
+
<button id="detail-close" class="detail-close" type="button" aria-label="Close details panel">Collapse</button>
|
|
122
|
+
</div>
|
|
123
|
+
<div id="detail-body" class="detail-body"></div>
|
|
124
|
+
</section>
|
|
125
|
+
</main>
|
|
126
|
+
<script>
|
|
127
|
+
let refreshInFlight = false;
|
|
128
|
+
let latestStatus = null;
|
|
129
|
+
let latestNotebooks = null;
|
|
130
|
+
let activeDetail = null;
|
|
131
|
+
const token = new URLSearchParams(location.hash.slice(1)).get('token');
|
|
132
|
+
|
|
133
|
+
function dashboardHeaders() {
|
|
134
|
+
return token ? { authorization: \`Bearer \${token}\` } : {};
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function requireDashboardToken() {
|
|
138
|
+
if (token) return true;
|
|
139
|
+
const message = 'Missing dashboard token. Open the dashboard URL printed by the MICA server.';
|
|
140
|
+
document.getElementById('auth-message').textContent = message;
|
|
141
|
+
document.getElementById('auth-state').textContent = 'Auth required';
|
|
142
|
+
renderLockedDashboard(message);
|
|
143
|
+
return false;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function renderLockedDashboard(message) {
|
|
147
|
+
setText('server-value', 'Locked');
|
|
148
|
+
setText('server-meta', message);
|
|
149
|
+
setText('agents-value', 'Locked');
|
|
150
|
+
setText('agents-meta', 'Authenticate to view agents');
|
|
151
|
+
setText('notebooks-value', 'Locked');
|
|
152
|
+
setText('notebooks-meta', 'Authenticate to view notebooks');
|
|
153
|
+
setText('requests-value', 'Locked');
|
|
154
|
+
setText('requests-meta', 'Authenticate to view queue');
|
|
155
|
+
setText('security-value', 'Protected');
|
|
156
|
+
setText('security-meta', 'Token required; token hidden');
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
async function refreshDashboard() {
|
|
160
|
+
if (!requireDashboardToken()) return;
|
|
161
|
+
if (refreshInFlight) return;
|
|
162
|
+
refreshInFlight = true;
|
|
163
|
+
setText('auth-state', 'Refreshing…');
|
|
164
|
+
|
|
165
|
+
try {
|
|
166
|
+
const [statusResponse, notebooksResponse] = await Promise.all([
|
|
167
|
+
fetch('/status', { headers: dashboardHeaders() }),
|
|
168
|
+
fetch('/notebooks', { headers: dashboardHeaders() }),
|
|
169
|
+
]);
|
|
170
|
+
|
|
171
|
+
latestStatus = await statusResponse.json();
|
|
172
|
+
latestNotebooks = await notebooksResponse.json();
|
|
173
|
+
renderDashboardData();
|
|
174
|
+
} finally {
|
|
175
|
+
refreshInFlight = false;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function renderDashboardData() {
|
|
180
|
+
const agents = Array.isArray(latestStatus?.agents) ? latestStatus.agents : [];
|
|
181
|
+
const notebooks = Array.isArray(latestNotebooks?.notebooks) ? latestNotebooks.notebooks : [];
|
|
182
|
+
const requests = latestStatus?.requests ?? {};
|
|
183
|
+
const server = latestStatus?.server ?? {};
|
|
184
|
+
const security = latestStatus?.security ?? {};
|
|
185
|
+
|
|
186
|
+
document.getElementById('auth-message').textContent = '';
|
|
187
|
+
setText('auth-state', security.authEnabled ? 'Auth enabled' : 'Auth disabled');
|
|
188
|
+
setText('updated-at', 'Updated just now.');
|
|
189
|
+
setText('server-value', titleCase(server.state ?? 'running'));
|
|
190
|
+
setText('server-meta', 'pid ' + safeText(server.pid) + ' · ' + formatDuration(server.uptimeMs) + ' uptime');
|
|
191
|
+
document.getElementById('server-dot').className = 'status-dot live';
|
|
192
|
+
|
|
193
|
+
const liveAgents = countStatus(agents, 'live');
|
|
194
|
+
const degradedAgents = countStatus(agents, 'degraded');
|
|
195
|
+
const offlineAgents = agents.length - liveAgents - degradedAgents;
|
|
196
|
+
setText('agents-value', liveAgents + ' live');
|
|
197
|
+
setText('agents-meta', degradedAgents + ' degraded · ' + offlineAgents + ' offline');
|
|
198
|
+
|
|
199
|
+
const liveNotebooks = countStatus(notebooks, 'live');
|
|
200
|
+
const degradedNotebooks = countStatus(notebooks, 'degraded');
|
|
201
|
+
setText('notebooks-value', liveNotebooks + ' live');
|
|
202
|
+
setText('notebooks-meta', degradedNotebooks + ' degraded · active ' + safeText(latestNotebooks?.activeNotebookId ?? 'none'));
|
|
203
|
+
|
|
204
|
+
setText('requests-value', safeText(requests.running ?? 0) + ' running');
|
|
205
|
+
setText('requests-meta', 'queued ' + safeText(requests.queued ?? 0) + ' · timed out ' + safeText(requests.timed_out ?? 0));
|
|
206
|
+
|
|
207
|
+
setText('security-value', security.authEnabled ? 'Protected' : 'Local only');
|
|
208
|
+
setText('security-meta', security.dashboardTokenPresent ? 'Bearer auth · token hidden' : 'No bearer token configured');
|
|
209
|
+
document.getElementById('security-dot').className = security.authEnabled ? 'status-dot live' : 'status-dot offline';
|
|
210
|
+
|
|
211
|
+
if (activeDetail) renderDetail(activeDetail);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function openDetail(kind) {
|
|
215
|
+
if (!latestStatus && !latestNotebooks) return;
|
|
216
|
+
activeDetail = activeDetail === kind ? null : kind;
|
|
217
|
+
updateDetailState();
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function updateDetailState() {
|
|
221
|
+
for (const card of document.querySelectorAll('[data-detail]')) {
|
|
222
|
+
const selected = card.dataset.detail === activeDetail;
|
|
223
|
+
card.classList.toggle('card--selected', selected);
|
|
224
|
+
card.setAttribute('aria-expanded', selected ? 'true' : 'false');
|
|
225
|
+
card.querySelector('.chevron').textContent = selected ? '▾' : '▸';
|
|
226
|
+
}
|
|
227
|
+
const panel = document.getElementById('detail-panel');
|
|
228
|
+
panel.hidden = !activeDetail;
|
|
229
|
+
if (activeDetail) renderDetail(activeDetail);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function renderDetail(kind) {
|
|
233
|
+
const body = document.getElementById('detail-body');
|
|
234
|
+
body.textContent = '';
|
|
235
|
+
if (kind === 'agents') {
|
|
236
|
+
setText('detail-title', 'Agents');
|
|
237
|
+
setText('detail-subtitle', 'Connected Wolfram control agents, status, and heartbeat age.');
|
|
238
|
+
renderRows(body, ['Agent', 'Status', 'Last seen', 'Wolfram', 'Platform'], (latestStatus?.agents ?? []).map((agent) => [
|
|
239
|
+
mono(agent.agentSessionId), chip(agent.status), formatAge(agent.lastSeenAt), safeText(agent.wolframVersion), safeText(agent.platform)
|
|
240
|
+
]), 'No agents connected. Start MICA and open Wolfram Desktop to register one.');
|
|
241
|
+
return;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
setText('detail-title', 'Notebooks');
|
|
245
|
+
setText('detail-subtitle', 'Registered live notebooks and mutation permissions.');
|
|
246
|
+
renderRows(body, ['Notebook', 'Status', 'Last seen', 'ID', 'Permissions'], (latestNotebooks?.notebooks ?? []).map((notebook) => [
|
|
247
|
+
safeText(notebook.displayName), chip(notebook.status), formatAge(notebook.lastSeenAt), mono(notebook.notebookId), safeText(enabledPermissions(notebook.permissions))
|
|
248
|
+
]), 'No notebooks registered. Open a notebook in Wolfram Desktop to see it here.');
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
function renderRows(container, headers, rows, emptyMessage) {
|
|
252
|
+
const table = document.createElement('div');
|
|
253
|
+
table.className = 'detail-table';
|
|
254
|
+
table.setAttribute('role', 'table');
|
|
255
|
+
const header = document.createElement('div');
|
|
256
|
+
header.className = 'detail-row detail-row--header';
|
|
257
|
+
header.setAttribute('role', 'row');
|
|
258
|
+
for (const label of headers) header.appendChild(cell(label, label, 'columnheader'));
|
|
259
|
+
table.appendChild(header);
|
|
260
|
+
if (rows.length === 0) {
|
|
261
|
+
const empty = document.createElement('div');
|
|
262
|
+
empty.className = 'empty-state';
|
|
263
|
+
empty.textContent = emptyMessage;
|
|
264
|
+
table.appendChild(empty);
|
|
265
|
+
} else {
|
|
266
|
+
for (const row of rows) {
|
|
267
|
+
const item = document.createElement('div');
|
|
268
|
+
item.className = 'detail-row';
|
|
269
|
+
item.setAttribute('role', 'row');
|
|
270
|
+
row.forEach((value, index) => item.appendChild(value instanceof Node ? labelNode(value, headers[index]) : cell(value, headers[index])));
|
|
271
|
+
table.appendChild(item);
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
container.appendChild(table);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
function cell(value, label, role = 'cell') {
|
|
278
|
+
const div = document.createElement('div');
|
|
279
|
+
div.className = 'detail-cell';
|
|
280
|
+
div.setAttribute('role', role);
|
|
281
|
+
if (label) div.dataset.label = label;
|
|
282
|
+
div.textContent = safeText(value);
|
|
283
|
+
return div;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
function labelNode(node, label) {
|
|
287
|
+
const wrapper = document.createElement('div');
|
|
288
|
+
wrapper.className = 'detail-cell';
|
|
289
|
+
wrapper.setAttribute('role', 'cell');
|
|
290
|
+
if (label) wrapper.dataset.label = label;
|
|
291
|
+
wrapper.appendChild(node);
|
|
292
|
+
return wrapper;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
function chip(status) {
|
|
296
|
+
const span = document.createElement('span');
|
|
297
|
+
const value = safeText(status || 'unknown');
|
|
298
|
+
span.className = 'chip chip--' + value;
|
|
299
|
+
span.setAttribute('role', 'status');
|
|
300
|
+
span.setAttribute('aria-label', 'Status: ' + value);
|
|
301
|
+
span.textContent = value;
|
|
302
|
+
return span;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
function mono(value) {
|
|
306
|
+
const span = document.createElement('span');
|
|
307
|
+
span.className = 'mono';
|
|
308
|
+
span.title = safeText(value);
|
|
309
|
+
span.textContent = safeText(value);
|
|
310
|
+
return span;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
function enabledPermissions(permissions) {
|
|
314
|
+
if (!permissions || typeof permissions !== 'object') return 'unknown';
|
|
315
|
+
return Object.entries(permissions).filter(([, allowed]) => allowed).map(([name]) => name.replace(/Notebook|Cell/g, '')).join(', ') || 'none';
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
function countStatus(items, status) {
|
|
319
|
+
return items.filter((item) => item.status === status).length;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
function formatAge(timestamp) {
|
|
323
|
+
if (typeof timestamp !== 'number') return 'unknown';
|
|
324
|
+
return formatDuration(Date.now() - timestamp) + ' ago';
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
function formatDuration(ms) {
|
|
328
|
+
if (typeof ms !== 'number' || !Number.isFinite(ms)) return 'unknown';
|
|
329
|
+
const seconds = Math.max(0, Math.floor(ms / 1000));
|
|
330
|
+
if (seconds < 60) return seconds + 's';
|
|
331
|
+
const minutes = Math.floor(seconds / 60);
|
|
332
|
+
if (minutes < 60) return minutes + 'm';
|
|
333
|
+
return Math.floor(minutes / 60) + 'h';
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
function titleCase(value) {
|
|
337
|
+
const text = safeText(value);
|
|
338
|
+
return text.charAt(0).toUpperCase() + text.slice(1);
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
function safeText(value) {
|
|
342
|
+
if (value === undefined || value === null || value === '') return 'none';
|
|
343
|
+
return String(value);
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
function setText(id, value) {
|
|
347
|
+
document.getElementById(id).textContent = safeText(value);
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
function closeDetail() {
|
|
351
|
+
activeDetail = null;
|
|
352
|
+
updateDetailState();
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
for (const card of document.querySelectorAll('[data-detail]')) {
|
|
356
|
+
card.addEventListener('click', () => openDetail(card.dataset.detail));
|
|
357
|
+
}
|
|
358
|
+
document.getElementById('detail-close').addEventListener('click', closeDetail);
|
|
359
|
+
document.addEventListener('keydown', (event) => {
|
|
360
|
+
if (event.key === 'Escape' && activeDetail) closeDetail();
|
|
361
|
+
});
|
|
362
|
+
|
|
363
|
+
function scheduleRefresh() {
|
|
364
|
+
setTimeout(() => {
|
|
365
|
+
refreshDashboard()
|
|
366
|
+
.catch((error) => {
|
|
367
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
368
|
+
document.getElementById('auth-message').textContent = 'Failed to refresh dashboard: ' + message;
|
|
369
|
+
})
|
|
370
|
+
.finally(() => {
|
|
371
|
+
scheduleRefresh();
|
|
372
|
+
});
|
|
373
|
+
}, 2000);
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
refreshDashboard()
|
|
377
|
+
.catch((error) => {
|
|
378
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
379
|
+
document.getElementById('auth-message').textContent = 'Failed to refresh dashboard: ' + message;
|
|
380
|
+
})
|
|
381
|
+
.finally(() => {
|
|
382
|
+
scheduleRefresh();
|
|
383
|
+
});
|
|
384
|
+
</script>
|
|
385
|
+
</body>
|
|
386
|
+
</html>`;
|
|
387
|
+
}
|