@shardworks/dashboard-apparatus 0.1.113

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/dist/html.js ADDED
@@ -0,0 +1,1017 @@
1
+ /**
2
+ * Dashboard web UI — embedded HTML/CSS/JS as a single-file SPA.
3
+ *
4
+ * Returned by the server's root handler. All API calls go to /api/*.
5
+ */
6
+ export function getDashboardHtml() {
7
+ return `<!DOCTYPE html>
8
+ <html lang="en">
9
+ <head>
10
+ <meta charset="UTF-8">
11
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
12
+ <title>Guild Dashboard</title>
13
+ <style>
14
+ *,*::before,*::after{box-sizing:border-box;margin:0;padding:0}
15
+ :root{
16
+ --bg:#0f1117;--surface:#1a1d27;--surface2:#242736;--surface3:#2e3248;
17
+ --border:#3a3f5c;--text:#e2e8f0;--muted:#8892a4;--accent:#6366f1;
18
+ --accent2:#818cf8;--green:#22c55e;--yellow:#eab308;--red:#ef4444;
19
+ --blue:#3b82f6;--orange:#f97316;--radius:6px;--font:'Inter',system-ui,sans-serif;
20
+ }
21
+ body{background:var(--bg);color:var(--text);font-family:var(--font);font-size:14px;min-height:100vh;display:flex;flex-direction:column}
22
+ a{color:var(--accent2);text-decoration:none}
23
+ button{cursor:pointer;font-family:inherit;font-size:13px;border:none;border-radius:var(--radius);padding:5px 12px;transition:opacity .15s}
24
+ button:hover{opacity:.85}
25
+ button:disabled{opacity:.4;cursor:default}
26
+ input,select,textarea{background:var(--surface3);color:var(--text);border:1px solid var(--border);border-radius:var(--radius);padding:6px 10px;font-family:inherit;font-size:13px;outline:none;transition:border-color .15s}
27
+ input:focus,select:focus,textarea:focus{border-color:var(--accent)}
28
+ select option{background:var(--surface2)}
29
+ label{display:block;font-size:12px;color:var(--muted);margin-bottom:4px;font-weight:500;text-transform:uppercase;letter-spacing:.05em}
30
+ .btn-primary{background:var(--accent);color:#fff}
31
+ .btn-ghost{background:var(--surface3);color:var(--text);border:1px solid var(--border)}
32
+ .btn-danger{background:var(--red);color:#fff}
33
+ .btn-success{background:var(--green);color:#000}
34
+ .btn-warning{background:var(--yellow);color:#000}
35
+ .btn-sm{padding:3px 8px;font-size:12px}
36
+
37
+ /* Layout */
38
+ header{background:var(--surface);border-bottom:1px solid var(--border);padding:0 24px;display:flex;align-items:center;gap:16px;height:52px;flex-shrink:0}
39
+ header h1{font-size:16px;font-weight:600;color:var(--text);display:flex;align-items:center;gap:8px}
40
+ header h1 .guild-name{color:var(--accent2)}
41
+ .header-meta{margin-left:auto;display:flex;align-items:center;gap:12px;color:var(--muted);font-size:12px}
42
+ .status-dot{width:8px;height:8px;border-radius:50%;background:var(--green);display:inline-block}
43
+
44
+ nav{background:var(--surface);border-bottom:1px solid var(--border);padding:0 24px;display:flex;gap:2px;flex-shrink:0}
45
+ .tab{padding:10px 16px;font-size:13px;font-weight:500;color:var(--muted);border-bottom:2px solid transparent;cursor:pointer;transition:color .15s,border-color .15s;user-select:none;display:flex;align-items:center;gap:6px}
46
+ .tab:hover{color:var(--text)}
47
+ .tab.active{color:var(--accent2);border-bottom-color:var(--accent2)}
48
+ .tab-badge{background:var(--surface3);color:var(--muted);font-size:10px;padding:1px 6px;border-radius:10px;font-weight:600}
49
+ .tab.active .tab-badge{background:var(--accent);color:#fff}
50
+
51
+ main{flex:1;overflow:auto;padding:24px}
52
+ .tab-panel{display:none}
53
+ .tab-panel.active{display:block}
54
+
55
+ /* Cards */
56
+ .card{background:var(--surface);border:1px solid var(--border);border-radius:var(--radius);padding:20px;margin-bottom:16px}
57
+ .card-title{font-size:13px;font-weight:600;color:var(--muted);text-transform:uppercase;letter-spacing:.07em;margin-bottom:16px;display:flex;align-items:center;gap:8px}
58
+ .card-title svg{flex-shrink:0}
59
+ .grid-2{display:grid;grid-template-columns:1fr 1fr;gap:16px}
60
+ .grid-3{display:grid;grid-template-columns:1fr 1fr 1fr;gap:16px}
61
+ .grid-4{display:grid;grid-template-columns:repeat(4,1fr);gap:16px}
62
+
63
+ /* Stats */
64
+ .stat-card{background:var(--surface);border:1px solid var(--border);border-radius:var(--radius);padding:16px}
65
+ .stat-label{font-size:11px;color:var(--muted);font-weight:500;text-transform:uppercase;letter-spacing:.06em;margin-bottom:6px}
66
+ .stat-value{font-size:28px;font-weight:700;color:var(--text);line-height:1}
67
+ .stat-sub{font-size:11px;color:var(--muted);margin-top:4px}
68
+
69
+ /* Badges / status */
70
+ .badge{display:inline-flex;align-items:center;gap:4px;padding:2px 8px;border-radius:10px;font-size:11px;font-weight:600;text-transform:lowercase;letter-spacing:.03em}
71
+ .badge-ready{background:#1e3a5f;color:#60a5fa}
72
+ .badge-active{background:#1a3a2a;color:#4ade80}
73
+ .badge-completed{background:#1a2a1a;color:#86efac}
74
+ .badge-failed{background:#3a1a1a;color:#f87171}
75
+ .badge-cancelled{background:#2a2a2a;color:#9ca3af}
76
+ .badge-running{background:#1a2a3a;color:#38bdf8;animation:pulse 2s infinite}
77
+ .badge-pending{background:#2a2a1a;color:#fbbf24}
78
+ .badge-ready-codex{background:#1e3a5f;color:#60a5fa}
79
+ .badge-cloning{background:#2a2a1a;color:#fbbf24}
80
+ .badge-error{background:#3a1a1a;color:#f87171}
81
+ @keyframes pulse{0%,100%{opacity:1}50%{opacity:.6}}
82
+
83
+ /* Tables */
84
+ .toolbar{display:flex;align-items:center;gap:10px;margin-bottom:14px;flex-wrap:wrap}
85
+ .toolbar-right{margin-left:auto;display:flex;gap:8px;align-items:center}
86
+ .search-input{width:200px}
87
+ table{width:100%;border-collapse:collapse}
88
+ thead tr{border-bottom:1px solid var(--border)}
89
+ th{text-align:left;font-size:11px;font-weight:600;color:var(--muted);text-transform:uppercase;letter-spacing:.07em;padding:8px 12px;white-space:nowrap;cursor:pointer;user-select:none}
90
+ th:hover{color:var(--text)}
91
+ th .sort-icon{display:inline-block;margin-left:4px;opacity:.4}
92
+ th.sorted .sort-icon{opacity:1;color:var(--accent2)}
93
+ td{padding:10px 12px;border-bottom:1px solid var(--border);vertical-align:middle;max-width:340px}
94
+ tr:last-child td{border-bottom:none}
95
+ tr:hover td{background:rgba(255,255,255,.02)}
96
+ .td-id{font-family:monospace;font-size:11px;color:var(--muted);white-space:nowrap}
97
+ .td-title{font-weight:500;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
98
+ .td-time{font-size:12px;color:var(--muted);white-space:nowrap}
99
+ .td-actions{white-space:nowrap;display:flex;gap:6px;align-items:center}
100
+ .empty-state{text-align:center;padding:48px 16px;color:var(--muted)}
101
+ .empty-state h3{font-size:15px;margin-bottom:6px;color:var(--text)}
102
+ .empty-icon{font-size:32px;margin-bottom:12px}
103
+ .pagination{display:flex;align-items:center;gap:8px;margin-top:12px;justify-content:flex-end;font-size:12px;color:var(--muted)}
104
+ .page-btn{background:var(--surface3);border:1px solid var(--border);color:var(--text);border-radius:4px;padding:4px 10px;font-size:12px}
105
+ .page-btn:disabled{opacity:.35;cursor:default}
106
+
107
+ /* Expandable rows */
108
+ .row-detail{background:var(--surface2);padding:12px 16px;border-bottom:1px solid var(--border)}
109
+ .row-detail pre{font-size:11px;color:var(--muted);white-space:pre-wrap;word-break:break-all;max-height:200px;overflow:auto;background:var(--surface);padding:10px;border-radius:4px;border:1px solid var(--border);margin-top:6px}
110
+ .detail-label{font-size:11px;font-weight:600;color:var(--muted);text-transform:uppercase;letter-spacing:.06em;margin-bottom:4px}
111
+ .detail-grid{display:grid;grid-template-columns:repeat(3,1fr);gap:12px;margin-bottom:10px}
112
+ .detail-item{background:var(--surface);border:1px solid var(--border);border-radius:4px;padding:8px 10px}
113
+ .detail-item .k{font-size:10px;color:var(--muted);font-weight:600;text-transform:uppercase;letter-spacing:.06em;margin-bottom:2px}
114
+ .detail-item .v{font-size:12px;font-family:monospace;word-break:break-all}
115
+
116
+ /* Modal */
117
+ .modal-overlay{display:none;position:fixed;inset:0;background:rgba(0,0,0,.7);z-index:100;align-items:center;justify-content:center}
118
+ .modal-overlay.open{display:flex}
119
+ .modal{background:var(--surface);border:1px solid var(--border);border-radius:8px;padding:24px;width:520px;max-width:95vw;max-height:90vh;overflow:auto}
120
+ .modal h2{font-size:16px;font-weight:600;margin-bottom:20px}
121
+ .modal-footer{display:flex;gap:10px;justify-content:flex-end;margin-top:20px}
122
+ .form-group{margin-bottom:14px}
123
+ .form-group input,.form-group select,.form-group textarea{width:100%}
124
+ .form-group textarea{resize:vertical;min-height:80px}
125
+ .form-row{display:grid;grid-template-columns:1fr 1fr;gap:12px}
126
+ .error-msg{color:var(--red);font-size:12px;margin-top:6px;display:none}
127
+ .error-msg.show{display:block}
128
+ .success-msg{color:var(--green);font-size:12px;margin-top:6px}
129
+
130
+ /* Plugin list */
131
+ .plugin-list{display:flex;flex-direction:column;gap:6px}
132
+ .plugin-item{display:flex;align-items:center;gap:10px;padding:8px 12px;background:var(--surface2);border:1px solid var(--border);border-radius:var(--radius)}
133
+ .plugin-item .pi-name{font-weight:500;flex:1}
134
+ .plugin-item .pi-type{font-size:10px;padding:2px 6px;border-radius:4px;font-weight:600;text-transform:uppercase}
135
+ .pi-type-apparatus{background:#1e2a4a;color:#818cf8}
136
+ .pi-type-kit{background:#1a2a2a;color:#34d399}
137
+ .plugin-item .pi-ver{font-size:11px;color:var(--muted);font-family:monospace}
138
+
139
+ /* Config view */
140
+ .config-view{background:var(--surface2);border:1px solid var(--border);border-radius:var(--radius);padding:14px;font-family:monospace;font-size:12px;color:var(--muted);white-space:pre-wrap;max-height:400px;overflow:auto;line-height:1.6}
141
+ .config-key{color:var(--accent2)}
142
+ .config-str{color:var(--green)}
143
+ .config-num{color:var(--orange)}
144
+ .config-bool{color:var(--yellow)}
145
+ .config-null{color:var(--muted)}
146
+
147
+ /* Engine pipeline */
148
+ .pipeline{display:flex;align-items:center;gap:0;overflow-x:auto;padding:4px 0}
149
+ .engine-chip{display:flex;align-items:center;gap:5px;background:var(--surface2);border:1px solid var(--border);border-radius:4px;padding:3px 8px;font-size:11px;white-space:nowrap}
150
+ .engine-arrow{color:var(--border);font-size:14px;margin:0 2px;flex-shrink:0}
151
+
152
+ /* Loading */
153
+ .loading{display:flex;align-items:center;justify-content:center;padding:40px;color:var(--muted);gap:10px}
154
+ .spinner{width:18px;height:18px;border:2px solid var(--border);border-top-color:var(--accent2);border-radius:50%;animation:spin .7s linear infinite}
155
+ @keyframes spin{to{transform:rotate(360deg)}}
156
+ .refresh-btn{background:none;border:none;color:var(--muted);padding:4px;line-height:1;font-size:16px}
157
+ .refresh-btn:hover{color:var(--text)}
158
+ .toast-area{position:fixed;bottom:20px;right:20px;display:flex;flex-direction:column;gap:8px;z-index:200}
159
+ .toast{background:var(--surface2);border:1px solid var(--border);border-radius:6px;padding:10px 16px;font-size:13px;animation:slide-in .2s ease;max-width:340px}
160
+ .toast.success{border-color:var(--green);color:var(--green)}
161
+ .toast.error{border-color:var(--red);color:var(--red)}
162
+ @keyframes slide-in{from{opacity:0;transform:translateY(10px)}to{opacity:1;transform:translateY(0)}}
163
+ </style>
164
+ </head>
165
+ <body>
166
+ <header>
167
+ <h1>
168
+ <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polygon points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2"/></svg>
169
+ <span id="guild-title">Guild Dashboard</span>
170
+ </h1>
171
+ <div class="header-meta">
172
+ <span class="status-dot"></span>
173
+ <span id="header-status">Loading…</span>
174
+ <button class="refresh-btn" onclick="refreshCurrent()" title="Refresh">↻</button>
175
+ </div>
176
+ </header>
177
+
178
+ <nav id="tab-nav">
179
+ <div class="tab active" data-tab="overview">Overview</div>
180
+ <div class="tab" data-tab="clerk">Clerk <span class="tab-badge" id="badge-clerk">—</span></div>
181
+ <div class="tab" data-tab="walker">Walker <span class="tab-badge" id="badge-walker">—</span></div>
182
+ <div class="tab" data-tab="animator">Animator <span class="tab-badge" id="badge-animator">—</span></div>
183
+ <div class="tab" data-tab="codexes">Codexes <span class="tab-badge" id="badge-codexes">—</span></div>
184
+ </nav>
185
+
186
+ <main>
187
+ <!-- OVERVIEW -->
188
+ <div class="tab-panel active" id="panel-overview">
189
+ <div id="overview-loading" class="loading"><div class="spinner"></div>Loading…</div>
190
+ <div id="overview-content" style="display:none">
191
+ <div class="grid-4" id="overview-stats" style="margin-bottom:16px"></div>
192
+ <div class="grid-2">
193
+ <div>
194
+ <div class="card">
195
+ <div class="card-title">
196
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><line x1="12" y1="8" x2="12" y2="12"/><line x1="12" y1="16" x2="12.01" y2="16"/></svg>
197
+ Guild Info
198
+ </div>
199
+ <div id="overview-info"></div>
200
+ </div>
201
+ <div class="card">
202
+ <div class="card-title">
203
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="3"/><path d="M19.07 4.93A10 10 0 0 0 4.93 19.07M4.93 4.93a10 10 0 0 0 14.14 14.14"/></svg>
204
+ Settings
205
+ </div>
206
+ <div id="overview-settings"></div>
207
+ </div>
208
+ </div>
209
+ <div>
210
+ <div class="card">
211
+ <div class="card-title">
212
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="2" y="3" width="20" height="14" rx="2"/><line x1="8" y1="21" x2="16" y2="21"/><line x1="12" y1="17" x2="12" y2="21"/></svg>
213
+ Loaded Plugins
214
+ </div>
215
+ <div id="overview-plugins" class="plugin-list"></div>
216
+ </div>
217
+ </div>
218
+ </div>
219
+ </div>
220
+ </div>
221
+
222
+ <!-- CLERK -->
223
+ <div class="tab-panel" id="panel-clerk">
224
+ <div class="card" style="margin-bottom:0">
225
+ <div class="toolbar">
226
+ <select id="clerk-filter-status" onchange="loadWrits()">
227
+ <option value="">All statuses</option>
228
+ <option value="ready">Ready</option>
229
+ <option value="active">Active</option>
230
+ <option value="completed">Completed</option>
231
+ <option value="failed">Failed</option>
232
+ <option value="cancelled">Cancelled</option>
233
+ </select>
234
+ <select id="clerk-filter-type" onchange="loadWrits()">
235
+ <option value="">All types</option>
236
+ </select>
237
+ <input class="search-input" type="text" id="clerk-search" placeholder="Search title…" oninput="filterWritsLocal()">
238
+ <div class="toolbar-right">
239
+ <span id="clerk-count-label" style="color:var(--muted);font-size:12px"></span>
240
+ <button class="btn-primary" onclick="openPostModal()">+ Post Commission</button>
241
+ </div>
242
+ </div>
243
+ <div id="clerk-loading" class="loading" style="display:none"><div class="spinner"></div>Loading…</div>
244
+ <div id="clerk-table-wrap">
245
+ <table>
246
+ <thead>
247
+ <tr>
248
+ <th onclick="sortWrits('id')" data-col="id">ID <span class="sort-icon">↕</span></th>
249
+ <th onclick="sortWrits('type')" data-col="type">Type <span class="sort-icon">↕</span></th>
250
+ <th onclick="sortWrits('title')" data-col="title">Title <span class="sort-icon">↕</span></th>
251
+ <th onclick="sortWrits('status')" data-col="status">Status <span class="sort-icon">↕</span></th>
252
+ <th onclick="sortWrits('createdAt')" data-col="createdAt">Created <span class="sort-icon">↕</span></th>
253
+ <th onclick="sortWrits('updatedAt')" data-col="updatedAt">Updated <span class="sort-icon">↕</span></th>
254
+ <th>Actions</th>
255
+ </tr>
256
+ </thead>
257
+ <tbody id="clerk-tbody"></tbody>
258
+ </table>
259
+ <div id="clerk-empty" class="empty-state" style="display:none">
260
+ <div class="empty-icon">📋</div>
261
+ <h3>No writs found</h3>
262
+ <p>Post a commission to create your first writ.</p>
263
+ </div>
264
+ </div>
265
+ <div class="pagination">
266
+ <button class="page-btn" id="clerk-prev" onclick="writPage(-1)" disabled>‹ Prev</button>
267
+ <span id="clerk-page-info" style="font-size:12px;color:var(--muted)"></span>
268
+ <button class="page-btn" id="clerk-next" onclick="writPage(1)">Next ›</button>
269
+ </div>
270
+ </div>
271
+ </div>
272
+
273
+ <!-- WALKER -->
274
+ <div class="tab-panel" id="panel-walker">
275
+ <div class="card" style="margin-bottom:0">
276
+ <div class="toolbar">
277
+ <select id="walker-filter-status" onchange="loadRigs()">
278
+ <option value="">All statuses</option>
279
+ <option value="running">Running</option>
280
+ <option value="completed">Completed</option>
281
+ <option value="failed">Failed</option>
282
+ </select>
283
+ <div class="toolbar-right">
284
+ <span id="walker-count-label" style="color:var(--muted);font-size:12px"></span>
285
+ </div>
286
+ </div>
287
+ <div id="walker-loading" class="loading" style="display:none"><div class="spinner"></div>Loading…</div>
288
+ <table>
289
+ <thead>
290
+ <tr>
291
+ <th onclick="sortRigs('id')" data-col="id">Rig ID <span class="sort-icon">↕</span></th>
292
+ <th onclick="sortRigs('writId')" data-col="writId">Writ <span class="sort-icon">↕</span></th>
293
+ <th onclick="sortRigs('status')" data-col="status">Status <span class="sort-icon">↕</span></th>
294
+ <th>Pipeline</th>
295
+ <th>Progress</th>
296
+ </tr>
297
+ </thead>
298
+ <tbody id="walker-tbody"></tbody>
299
+ </table>
300
+ <div id="walker-empty" class="empty-state" style="display:none">
301
+ <div class="empty-icon">⚙️</div>
302
+ <h3>No rigs found</h3>
303
+ <p>Rigs are created when the Walker processes writs.</p>
304
+ </div>
305
+ </div>
306
+ </div>
307
+
308
+ <!-- ANIMATOR -->
309
+ <div class="tab-panel" id="panel-animator">
310
+ <div class="card" style="margin-bottom:0">
311
+ <div class="toolbar">
312
+ <select id="animator-filter-status" onchange="loadSessions()">
313
+ <option value="">All statuses</option>
314
+ <option value="running">Running</option>
315
+ <option value="completed">Completed</option>
316
+ <option value="failed">Failed</option>
317
+ <option value="timeout">Timeout</option>
318
+ </select>
319
+ <div class="toolbar-right">
320
+ <span id="animator-count-label" style="color:var(--muted);font-size:12px"></span>
321
+ </div>
322
+ </div>
323
+ <div id="animator-loading" class="loading" style="display:none"><div class="spinner"></div>Loading…</div>
324
+ <table>
325
+ <thead>
326
+ <tr>
327
+ <th onclick="sortSessions('id')" data-col="id">Session ID <span class="sort-icon">↕</span></th>
328
+ <th onclick="sortSessions('status')" data-col="status">Status <span class="sort-icon">↕</span></th>
329
+ <th onclick="sortSessions('provider')" data-col="provider">Provider <span class="sort-icon">↕</span></th>
330
+ <th onclick="sortSessions('startedAt')" data-col="startedAt">Started <span class="sort-icon">↕</span></th>
331
+ <th onclick="sortSessions('durationMs')" data-col="durationMs">Duration <span class="sort-icon">↕</span></th>
332
+ <th>Tokens / Cost</th>
333
+ </tr>
334
+ </thead>
335
+ <tbody id="animator-tbody"></tbody>
336
+ </table>
337
+ <div id="animator-empty" class="empty-state" style="display:none">
338
+ <div class="empty-icon">✨</div>
339
+ <h3>No sessions recorded</h3>
340
+ <p>Sessions appear here when animas are animated.</p>
341
+ </div>
342
+ <div class="pagination">
343
+ <button class="page-btn" id="animator-prev" onclick="sessionPage(-1)" disabled>‹ Prev</button>
344
+ <span id="animator-page-info"></span>
345
+ <button class="page-btn" id="animator-next" onclick="sessionPage(1)">Next ›</button>
346
+ </div>
347
+ </div>
348
+ </div>
349
+
350
+ <!-- CODEXES -->
351
+ <div class="tab-panel" id="panel-codexes">
352
+ <div id="codexes-loading" class="loading"><div class="spinner"></div>Loading…</div>
353
+ <div id="codexes-content" style="display:none">
354
+ <div class="toolbar" style="margin-bottom:16px">
355
+ <div class="toolbar-right">
356
+ <span id="codexes-count-label" style="color:var(--muted);font-size:12px"></span>
357
+ </div>
358
+ </div>
359
+ <table>
360
+ <thead>
361
+ <tr>
362
+ <th>Name</th>
363
+ <th>Remote URL</th>
364
+ <th>Status</th>
365
+ <th>Active Drafts</th>
366
+ </tr>
367
+ </thead>
368
+ <tbody id="codexes-tbody"></tbody>
369
+ </table>
370
+ <div id="codexes-empty" class="empty-state" style="display:none">
371
+ <div class="empty-icon">📚</div>
372
+ <h3>No codexes registered</h3>
373
+ <p>Add a codex with <code>nsg codex add &lt;name&gt; &lt;url&gt;</code>.</p>
374
+ </div>
375
+ <div id="drafts-section" style="margin-top:24px;display:none">
376
+ <div class="card">
377
+ <div class="card-title">Active Drafts</div>
378
+ <table>
379
+ <thead>
380
+ <tr>
381
+ <th>ID</th>
382
+ <th>Codex</th>
383
+ <th>Branch</th>
384
+ <th>Associated With</th>
385
+ <th>Created</th>
386
+ </tr>
387
+ </thead>
388
+ <tbody id="drafts-tbody"></tbody>
389
+ </table>
390
+ </div>
391
+ </div>
392
+ </div>
393
+ </div>
394
+ </main>
395
+
396
+ <!-- POST COMMISSION MODAL -->
397
+ <div class="modal-overlay" id="post-modal">
398
+ <div class="modal">
399
+ <h2>Post Commission</h2>
400
+ <div class="form-row">
401
+ <div class="form-group">
402
+ <label for="pm-type">Type</label>
403
+ <select id="pm-type"></select>
404
+ </div>
405
+ <div class="form-group">
406
+ <label for="pm-codex">Codex (optional)</label>
407
+ <select id="pm-codex">
408
+ <option value="">None</option>
409
+ </select>
410
+ </div>
411
+ </div>
412
+ <div class="form-group">
413
+ <label for="pm-title">Title</label>
414
+ <input type="text" id="pm-title" placeholder="Short description of the work">
415
+ </div>
416
+ <div class="form-group">
417
+ <label for="pm-body">Body</label>
418
+ <textarea id="pm-body" placeholder="Detailed description, requirements, context…" rows="5"></textarea>
419
+ </div>
420
+ <div id="pm-error" class="error-msg"></div>
421
+ <div class="modal-footer">
422
+ <button class="btn-ghost" onclick="closePostModal()">Cancel</button>
423
+ <button class="btn-primary" id="pm-submit" onclick="submitPost()">Post Commission</button>
424
+ </div>
425
+ </div>
426
+ </div>
427
+
428
+ <!-- TRANSITION MODAL -->
429
+ <div class="modal-overlay" id="trans-modal">
430
+ <div class="modal" style="width:420px">
431
+ <h2 id="trans-title">Transition Writ</h2>
432
+ <p style="color:var(--muted);font-size:13px;margin-bottom:16px" id="trans-desc"></p>
433
+ <div class="form-group" id="trans-resolution-wrap" style="display:none">
434
+ <label for="trans-resolution">Resolution (optional)</label>
435
+ <textarea id="trans-resolution" rows="3" placeholder="Brief summary of how this writ resolved…"></textarea>
436
+ </div>
437
+ <div id="trans-error" class="error-msg"></div>
438
+ <div class="modal-footer">
439
+ <button class="btn-ghost" onclick="closeTransModal()">Cancel</button>
440
+ <button class="btn-primary" id="trans-submit" onclick="submitTransition()">Confirm</button>
441
+ </div>
442
+ </div>
443
+ </div>
444
+
445
+ <div class="toast-area" id="toast-area"></div>
446
+
447
+ <script>
448
+ // ── State ────────────────────────────────────────────────────────
449
+ let activeTab = 'overview';
450
+ let overview = null;
451
+ let writs = [];
452
+ let writsTotal = 0;
453
+ let writsPage = 0;
454
+ const WRIT_PAGE_SIZE = 20;
455
+ let writSort = { col: 'createdAt', dir: 'desc' };
456
+ let rigs = [];
457
+ let rigSort = { col: 'id', dir: 'desc' };
458
+ let sessions = [];
459
+ let sessionsTotal = 0;
460
+ let sessionsPage = 0;
461
+ const SESSION_PAGE_SIZE = 20;
462
+ let sessionSort = { col: 'startedAt', dir: 'desc' };
463
+ let transData = null;
464
+
465
+ // ── Tabs ─────────────────────────────────────────────────────────
466
+ document.querySelectorAll('.tab').forEach(t => {
467
+ t.addEventListener('click', () => switchTab(t.dataset.tab));
468
+ });
469
+
470
+ function switchTab(id) {
471
+ activeTab = id;
472
+ document.querySelectorAll('.tab').forEach(t => t.classList.toggle('active', t.dataset.tab === id));
473
+ document.querySelectorAll('.tab-panel').forEach(p => p.classList.toggle('active', p.id === 'panel-' + id));
474
+ loadTab(id);
475
+ }
476
+
477
+ function loadTab(id) {
478
+ if (id === 'overview') loadOverview();
479
+ else if (id === 'clerk') loadWrits();
480
+ else if (id === 'walker') loadRigs();
481
+ else if (id === 'animator') loadSessions();
482
+ else if (id === 'codexes') loadCodexes();
483
+ }
484
+
485
+ function refreshCurrent() { loadTab(activeTab); }
486
+
487
+ // ── API helpers ──────────────────────────────────────────────────
488
+ async function api(path, opts) {
489
+ const r = await fetch('/api' + path, opts);
490
+ if (!r.ok) {
491
+ const t = await r.text().catch(() => 'Unknown error');
492
+ throw new Error(t || r.statusText);
493
+ }
494
+ return r.json();
495
+ }
496
+
497
+ async function apiPost(path, body) {
498
+ return api(path, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body) });
499
+ }
500
+
501
+ // ── Toast ────────────────────────────────────────────────────────
502
+ function toast(msg, type='success') {
503
+ const area = document.getElementById('toast-area');
504
+ const el = document.createElement('div');
505
+ el.className = 'toast ' + type;
506
+ el.textContent = msg;
507
+ area.appendChild(el);
508
+ setTimeout(() => el.remove(), 3500);
509
+ }
510
+
511
+ // ── OVERVIEW ─────────────────────────────────────────────────────
512
+ async function loadOverview() {
513
+ document.getElementById('overview-loading').style.display = 'flex';
514
+ document.getElementById('overview-content').style.display = 'none';
515
+ try {
516
+ overview = await api('/overview');
517
+ renderOverview(overview);
518
+ document.getElementById('header-status').textContent = overview.guild.name + ' · nexus ' + overview.guild.nexus;
519
+ document.getElementById('guild-title').innerHTML =
520
+ 'Guild Dashboard · <span class="guild-name">' + esc(overview.guild.name) + '</span>';
521
+ } catch(e) {
522
+ document.getElementById('overview-loading').innerHTML = '<span style="color:var(--red)">Error: ' + esc(e.message) + '</span>';
523
+ return;
524
+ }
525
+ document.getElementById('overview-loading').style.display = 'none';
526
+ document.getElementById('overview-content').style.display = 'block';
527
+ }
528
+
529
+ function renderOverview(data) {
530
+ // Stats
531
+ const stats = [
532
+ { label: 'Plugins', value: data.plugins.length, sub: data.plugins.filter(p=>p.type==='apparatus').length + ' apparatus' },
533
+ { label: 'Writs', value: data.counts.writs ?? '—', sub: (data.counts.ready ?? 0) + ' ready · ' + (data.counts.active ?? 0) + ' active' },
534
+ { label: 'Sessions', value: data.counts.sessions ?? '—', sub: (data.counts.runningSessions ?? 0) + ' running' },
535
+ { label: 'Rigs', value: data.counts.rigs ?? '—', sub: (data.counts.runningRigs ?? 0) + ' running' },
536
+ ];
537
+ document.getElementById('overview-stats').innerHTML = stats.map(s =>
538
+ '<div class="stat-card"><div class="stat-label">' + esc(s.label) + '</div><div class="stat-value">' + esc(String(s.value)) + '</div><div class="stat-sub">' + esc(s.sub) + '</div></div>'
539
+ ).join('');
540
+
541
+ // Info
542
+ const g = data.guild;
543
+ document.getElementById('overview-info').innerHTML = kv([
544
+ ['Name', g.name],
545
+ ['Nexus Version', g.nexus],
546
+ ['Model', g.settings?.model ?? '(default)'],
547
+ ['Auto Migrate', g.settings?.autoMigrate !== false ? 'Yes' : 'No'],
548
+ ]);
549
+
550
+ // Settings — show full clockworks if present
551
+ let settingsHtml = '';
552
+ if (g.clockworks?.standingOrders?.length) {
553
+ settingsHtml += '<div class="detail-label" style="margin-bottom:8px">Standing Orders</div>';
554
+ settingsHtml += g.clockworks.standingOrders.map(o => {
555
+ const trigger = 'on: ' + o.on;
556
+ const action = o.run ? 'run: ' + o.run : o.summon ? 'summon: ' + o.summon : 'brief: ' + o.brief;
557
+ return '<div style="font-size:12px;padding:4px 0;border-bottom:1px solid var(--border);display:flex;gap:8px"><span style="color:var(--muted);font-family:monospace">' + esc(trigger) + '</span><span style="color:var(--accent2);font-family:monospace">' + esc(action) + '</span></div>';
558
+ }).join('');
559
+ }
560
+ if (g.clerk?.writTypes?.length) {
561
+ settingsHtml += '<div class="detail-label" style="margin:12px 0 6px">Writ Types</div>';
562
+ settingsHtml += g.clerk.writTypes.map(t =>
563
+ '<span class="badge badge-ready" style="margin:2px">' + esc(t.name) + '</span>'
564
+ ).join(' ');
565
+ }
566
+ if (!settingsHtml) settingsHtml = '<span style="color:var(--muted);font-size:12px">No additional configuration</span>';
567
+ document.getElementById('overview-settings').innerHTML = settingsHtml;
568
+
569
+ // Plugins
570
+ document.getElementById('overview-plugins').innerHTML = data.plugins.map(p =>
571
+ '<div class="plugin-item">' +
572
+ '<span class="pi-type ' + (p.type==='apparatus'?'pi-type-apparatus':'pi-type-kit') + '">' + esc(p.type) + '</span>' +
573
+ '<span class="pi-name">' + esc(p.id) + '</span>' +
574
+ '<span class="pi-ver">' + esc(p.version) + '</span>' +
575
+ '</div>'
576
+ ).join('');
577
+
578
+ // Update badges
579
+ if (data.counts.writs !== undefined) setBadge('clerk', data.counts.writs);
580
+ if (data.counts.rigs !== undefined) setBadge('walker', data.counts.rigs);
581
+ if (data.counts.sessions !== undefined) setBadge('animator', data.counts.sessions);
582
+ if (data.counts.codexes !== undefined) setBadge('codexes', data.counts.codexes);
583
+ }
584
+
585
+ function kv(pairs) {
586
+ return pairs.map(([k,v]) =>
587
+ '<div style="display:flex;align-items:baseline;gap:8px;padding:5px 0;border-bottom:1px solid var(--border)">' +
588
+ '<span style="font-size:11px;font-weight:600;color:var(--muted);min-width:110px;text-transform:uppercase;letter-spacing:.05em">' + esc(k) + '</span>' +
589
+ '<span style="font-size:13px">' + esc(String(v ?? '—')) + '</span>' +
590
+ '</div>'
591
+ ).join('');
592
+ }
593
+
594
+ function setBadge(tab, val) {
595
+ const el = document.getElementById('badge-' + tab);
596
+ if (el) el.textContent = String(val);
597
+ }
598
+
599
+ // ── CLERK ─────────────────────────────────────────────────────────
600
+ async function loadWrits() {
601
+ const status = document.getElementById('clerk-filter-status').value;
602
+ const type = document.getElementById('clerk-filter-type').value;
603
+ document.getElementById('clerk-loading').style.display = 'flex';
604
+ document.getElementById('clerk-table-wrap').style.display = 'none';
605
+ try {
606
+ const params = new URLSearchParams({ limit: WRIT_PAGE_SIZE, offset: writsPage * WRIT_PAGE_SIZE });
607
+ if (status) params.set('status', status);
608
+ if (type) params.set('type', type);
609
+ const data = await api('/writs?' + params);
610
+ writs = data.writs;
611
+ writsTotal = data.total;
612
+ // Populate type filter (once)
613
+ if (data.types?.length && document.getElementById('clerk-filter-type').options.length <= 1) {
614
+ data.types.forEach(t => {
615
+ const o = document.createElement('option');
616
+ o.value = t; o.textContent = t;
617
+ document.getElementById('clerk-filter-type').appendChild(o);
618
+ });
619
+ }
620
+ // Populate type select in modal
621
+ if (data.types?.length) {
622
+ const sel = document.getElementById('pm-type');
623
+ sel.innerHTML = '';
624
+ data.types.forEach(t => {
625
+ const o = document.createElement('option');
626
+ o.value = t; o.textContent = t;
627
+ sel.appendChild(o);
628
+ });
629
+ }
630
+ renderWrits();
631
+ setBadge('clerk', writsTotal);
632
+ document.getElementById('clerk-count-label').textContent = writsTotal + ' writ' + (writsTotal!==1?'s':'');
633
+ } catch(e) {
634
+ document.getElementById('clerk-loading').innerHTML = '<span style="color:var(--red)">Error: ' + esc(e.message) + '</span>';
635
+ return;
636
+ }
637
+ document.getElementById('clerk-loading').style.display = 'none';
638
+ document.getElementById('clerk-table-wrap').style.display = 'block';
639
+ }
640
+
641
+ function renderWrits() {
642
+ const search = (document.getElementById('clerk-search').value || '').toLowerCase();
643
+ let rows = writs.filter(w => !search || w.title.toLowerCase().includes(search));
644
+ rows = stableSort(rows, writSort.col, writSort.dir);
645
+ updateSortHeaders('clerk-tbody', writSort);
646
+
647
+ const tbody = document.getElementById('clerk-tbody');
648
+ tbody.innerHTML = rows.map(w =>
649
+ '<tr>' +
650
+ '<td class="td-id">' + esc(w.id) + '</td>' +
651
+ '<td><code style="font-size:11px;color:var(--muted)">' + esc(w.type) + '</code></td>' +
652
+ '<td class="td-title" title="' + esc(w.title) + '">' + esc(w.title) + '</td>' +
653
+ '<td>' + statusBadge(w.status) + '</td>' +
654
+ '<td class="td-time">' + fmtDate(w.createdAt) + '</td>' +
655
+ '<td class="td-time">' + fmtDate(w.updatedAt) + '</td>' +
656
+ '<td class="td-actions">' + writActions(w) + '</td>' +
657
+ '</tr>'
658
+ ).join('');
659
+
660
+ document.getElementById('clerk-empty').style.display = rows.length ? 'none' : 'block';
661
+
662
+ // Pagination
663
+ const totalPages = Math.ceil(writsTotal / WRIT_PAGE_SIZE);
664
+ document.getElementById('clerk-prev').disabled = writsPage <= 0;
665
+ document.getElementById('clerk-next').disabled = writsPage >= totalPages - 1;
666
+ document.getElementById('clerk-page-info').textContent = totalPages > 1
667
+ ? 'Page ' + (writsPage+1) + ' of ' + totalPages : '';
668
+ }
669
+
670
+ function filterWritsLocal() { renderWrits(); }
671
+
672
+ function writActions(w) {
673
+ const btns = [];
674
+ if (w.status === 'ready') {
675
+ btns.push('<button class="btn-success btn-sm" onclick="openTrans(\'' + w.id + '\',\'active\')">Accept</button>');
676
+ btns.push('<button class="btn-danger btn-sm" onclick="openTrans(\'' + w.id + '\',\'cancelled\')">Cancel</button>');
677
+ } else if (w.status === 'active') {
678
+ btns.push('<button class="btn-success btn-sm" onclick="openTrans(\'' + w.id + '\',\'completed\')">Complete</button>');
679
+ btns.push('<button class="btn-danger btn-sm" onclick="openTrans(\'' + w.id + '\',\'failed\')">Fail</button>');
680
+ btns.push('<button class="btn-ghost btn-sm" onclick="openTrans(\'' + w.id + '\',\'cancelled\')">Cancel</button>');
681
+ }
682
+ return btns.join('') || '<span style="color:var(--muted);font-size:11px">Terminal</span>';
683
+ }
684
+
685
+ function sortWrits(col) {
686
+ if (writSort.col === col) writSort.dir = writSort.dir === 'asc' ? 'desc' : 'asc';
687
+ else { writSort.col = col; writSort.dir = 'desc'; }
688
+ renderWrits();
689
+ }
690
+
691
+ function writPage(delta) {
692
+ writsPage = Math.max(0, writsPage + delta);
693
+ loadWrits();
694
+ }
695
+
696
+ // ── POST COMMISSION MODAL ─────────────────────────────────────────
697
+ function openPostModal() {
698
+ document.getElementById('pm-title').value = '';
699
+ document.getElementById('pm-body').value = '';
700
+ document.getElementById('pm-error').className = 'error-msg';
701
+ // Populate codexes
702
+ const sel = document.getElementById('pm-codex');
703
+ sel.innerHTML = '<option value="">None</option>';
704
+ if (overview?.counts?.codexNames) {
705
+ overview.counts.codexNames.forEach(n => {
706
+ const o = document.createElement('option');
707
+ o.value = n; o.textContent = n;
708
+ sel.appendChild(o);
709
+ });
710
+ }
711
+ document.getElementById('post-modal').classList.add('open');
712
+ document.getElementById('pm-title').focus();
713
+ }
714
+
715
+ function closePostModal() {
716
+ document.getElementById('post-modal').classList.remove('open');
717
+ }
718
+
719
+ async function submitPost() {
720
+ const title = document.getElementById('pm-title').value.trim();
721
+ const body = document.getElementById('pm-body').value.trim();
722
+ const type = document.getElementById('pm-type').value;
723
+ const codex = document.getElementById('pm-codex').value || undefined;
724
+ const errEl = document.getElementById('pm-error');
725
+ errEl.className = 'error-msg';
726
+
727
+ if (!title) { errEl.textContent = 'Title is required.'; errEl.className = 'error-msg show'; return; }
728
+ if (!body) { errEl.textContent = 'Body is required.'; errEl.className = 'error-msg show'; return; }
729
+
730
+ document.getElementById('pm-submit').disabled = true;
731
+ try {
732
+ await apiPost('/writs', { title, body, type, codex });
733
+ closePostModal();
734
+ toast('Commission posted!');
735
+ writsPage = 0;
736
+ loadWrits();
737
+ loadOverview();
738
+ } catch(e) {
739
+ errEl.textContent = e.message;
740
+ errEl.className = 'error-msg show';
741
+ } finally {
742
+ document.getElementById('pm-submit').disabled = false;
743
+ }
744
+ }
745
+
746
+ // ── TRANSITION MODAL ──────────────────────────────────────────────
747
+ function openTrans(id, to) {
748
+ transData = { id, to };
749
+ const labels = { active:'Accept', completed:'Complete', failed:'Fail', cancelled:'Cancel' };
750
+ const descs = {
751
+ active: 'Accept this writ and begin working on it.',
752
+ completed: 'Mark this writ as completed.',
753
+ failed: 'Mark this writ as failed.',
754
+ cancelled: 'Cancel this writ.',
755
+ };
756
+ document.getElementById('trans-title').textContent = labels[to] + ' Writ';
757
+ document.getElementById('trans-desc').textContent = descs[to] || '';
758
+ const showRes = to === 'completed' || to === 'failed' || to === 'cancelled';
759
+ document.getElementById('trans-resolution-wrap').style.display = showRes ? 'block' : 'none';
760
+ document.getElementById('trans-resolution').value = '';
761
+ document.getElementById('trans-error').className = 'error-msg';
762
+ const btn = document.getElementById('trans-submit');
763
+ btn.className = 'btn-primary';
764
+ if (to === 'failed' || to === 'cancelled') btn.className = 'btn-danger';
765
+ if (to === 'completed') btn.className = 'btn-success';
766
+ btn.textContent = labels[to];
767
+ document.getElementById('trans-modal').classList.add('open');
768
+ }
769
+
770
+ function closeTransModal() {
771
+ document.getElementById('trans-modal').classList.remove('open');
772
+ transData = null;
773
+ }
774
+
775
+ async function submitTransition() {
776
+ if (!transData) return;
777
+ const { id, to } = transData;
778
+ const resolution = document.getElementById('trans-resolution').value.trim() || undefined;
779
+ const errEl = document.getElementById('trans-error');
780
+ errEl.className = 'error-msg';
781
+ document.getElementById('trans-submit').disabled = true;
782
+ try {
783
+ await apiPost('/writs/' + id + '/transition', { to, ...(resolution ? { resolution } : {}) });
784
+ closeTransModal();
785
+ toast('Writ transitioned to ' + to);
786
+ loadWrits();
787
+ loadOverview();
788
+ } catch(e) {
789
+ errEl.textContent = e.message;
790
+ errEl.className = 'error-msg show';
791
+ } finally {
792
+ document.getElementById('trans-submit').disabled = false;
793
+ }
794
+ }
795
+
796
+ // ── WALKER ────────────────────────────────────────────────────────
797
+ async function loadRigs() {
798
+ const status = document.getElementById('walker-filter-status').value;
799
+ document.getElementById('walker-loading').style.display = 'flex';
800
+ try {
801
+ const params = new URLSearchParams();
802
+ if (status) params.set('status', status);
803
+ const data = await api('/rigs?' + params);
804
+ rigs = data.rigs;
805
+ renderRigs();
806
+ setBadge('walker', rigs.length);
807
+ document.getElementById('walker-count-label').textContent = rigs.length + ' rig' + (rigs.length!==1?'s':'');
808
+ } catch(e) {
809
+ document.getElementById('walker-loading').innerHTML = '<span style="color:var(--red)">Error: ' + esc(e.message) + '</span>';
810
+ return;
811
+ }
812
+ document.getElementById('walker-loading').style.display = 'none';
813
+ }
814
+
815
+ function renderRigs() {
816
+ const rows = stableSort(rigs, rigSort.col, rigSort.dir);
817
+ const tbody = document.getElementById('walker-tbody');
818
+ tbody.innerHTML = rows.map(r => {
819
+ const engines = r.engines || [];
820
+ const done = engines.filter(e => e.status==='completed' || e.status==='failed').length;
821
+ const total = engines.length;
822
+ const pct = total ? Math.round(done/total*100) : 0;
823
+ return '<tr>' +
824
+ '<td class="td-id">' + esc(r.id) + '</td>' +
825
+ '<td class="td-id">' + esc(r.writId) + '</td>' +
826
+ '<td>' + statusBadge(r.status) + '</td>' +
827
+ '<td><div class="pipeline">' + engines.map((e,i) =>
828
+ (i>0?'<span class="engine-arrow">›</span>':'')+
829
+ '<div class="engine-chip">' + statusDot(e.status) + ' ' + esc(e.id) + '</div>'
830
+ ).join('') + '</div></td>' +
831
+ '<td><div style="font-size:11px;color:var(--muted)">' + done + '/' + total + ' engines</div>' +
832
+ '<div style="height:4px;background:var(--surface3);border-radius:2px;margin-top:4px;width:80px">' +
833
+ '<div style="height:4px;background:' + (r.status==='failed'?'var(--red)':r.status==='completed'?'var(--green)':'var(--accent)') + ';border-radius:2px;width:' + pct + '%"></div>' +
834
+ '</div>' +
835
+ '</td>' +
836
+ '</tr>';
837
+ }).join('');
838
+ document.getElementById('walker-empty').style.display = rows.length ? 'none' : 'block';
839
+ }
840
+
841
+ function sortRigs(col) {
842
+ if (rigSort.col === col) rigSort.dir = rigSort.dir === 'asc' ? 'desc' : 'asc';
843
+ else { rigSort.col = col; rigSort.dir = 'desc'; }
844
+ renderRigs();
845
+ }
846
+
847
+ // ── ANIMATOR ─────────────────────────────────────────────────────
848
+ async function loadSessions() {
849
+ const status = document.getElementById('animator-filter-status').value;
850
+ document.getElementById('animator-loading').style.display = 'flex';
851
+ try {
852
+ const params = new URLSearchParams({ limit: SESSION_PAGE_SIZE, offset: sessionsPage * SESSION_PAGE_SIZE });
853
+ if (status) params.set('status', status);
854
+ const data = await api('/sessions?' + params);
855
+ sessions = data.sessions;
856
+ sessionsTotal = data.total;
857
+ renderSessions();
858
+ setBadge('animator', sessionsTotal);
859
+ document.getElementById('animator-count-label').textContent = sessionsTotal + ' session' + (sessionsTotal!==1?'s':'');
860
+ } catch(e) {
861
+ document.getElementById('animator-loading').innerHTML = '<span style="color:var(--red)">Error: ' + esc(e.message) + '</span>';
862
+ return;
863
+ }
864
+ document.getElementById('animator-loading').style.display = 'none';
865
+ }
866
+
867
+ function renderSessions() {
868
+ const rows = stableSort(sessions, sessionSort.col, sessionSort.dir);
869
+ const tbody = document.getElementById('animator-tbody');
870
+ tbody.innerHTML = rows.map(s => {
871
+ const tokens = s.tokenUsage
872
+ ? (s.tokenUsage.inputTokens||0) + '↑ ' + (s.tokenUsage.outputTokens||0) + '↓'
873
+ : '—';
874
+ const cost = s.costUsd != null ? '$' + s.costUsd.toFixed(4) : '—';
875
+ return '<tr>' +
876
+ '<td class="td-id">' + esc(s.id) + '</td>' +
877
+ '<td>' + statusBadge(s.status) + '</td>' +
878
+ '<td style="font-size:12px;color:var(--muted)">' + esc(s.provider||'—') + '</td>' +
879
+ '<td class="td-time">' + fmtDate(s.startedAt) + '</td>' +
880
+ '<td class="td-time">' + fmtDuration(s.durationMs) + '</td>' +
881
+ '<td style="font-size:11px;color:var(--muted);font-family:monospace">' + esc(tokens) + ' · ' + esc(cost) + '</td>' +
882
+ '</tr>';
883
+ }).join('');
884
+ document.getElementById('animator-empty').style.display = rows.length ? 'none' : 'block';
885
+
886
+ const totalPages = Math.ceil(sessionsTotal / SESSION_PAGE_SIZE);
887
+ document.getElementById('animator-prev').disabled = sessionsPage <= 0;
888
+ document.getElementById('animator-next').disabled = sessionsPage >= totalPages - 1;
889
+ document.getElementById('animator-page-info').textContent = totalPages > 1
890
+ ? 'Page ' + (sessionsPage+1) + ' of ' + totalPages : '';
891
+ }
892
+
893
+ function sortSessions(col) {
894
+ if (sessionSort.col === col) sessionSort.dir = sessionSort.dir === 'asc' ? 'desc' : 'asc';
895
+ else { sessionSort.col = col; sessionSort.dir = 'desc'; }
896
+ renderSessions();
897
+ }
898
+
899
+ function sessionPage(delta) {
900
+ sessionsPage = Math.max(0, sessionsPage + delta);
901
+ loadSessions();
902
+ }
903
+
904
+ // ── CODEXES ───────────────────────────────────────────────────────
905
+ async function loadCodexes() {
906
+ document.getElementById('codexes-loading').style.display = 'flex';
907
+ document.getElementById('codexes-content').style.display = 'none';
908
+ try {
909
+ const data = await api('/codexes');
910
+ renderCodexes(data);
911
+ setBadge('codexes', data.codexes.length);
912
+ document.getElementById('codexes-count-label').textContent = data.codexes.length + ' codex' + (data.codexes.length!==1?'es':'');
913
+ } catch(e) {
914
+ document.getElementById('codexes-loading').innerHTML = '<span style="color:var(--red)">Error: ' + esc(e.message) + '</span>';
915
+ return;
916
+ }
917
+ document.getElementById('codexes-loading').style.display = 'none';
918
+ document.getElementById('codexes-content').style.display = 'block';
919
+ }
920
+
921
+ function renderCodexes(data) {
922
+ const tbody = document.getElementById('codexes-tbody');
923
+ tbody.innerHTML = data.codexes.map(c =>
924
+ '<tr>' +
925
+ '<td style="font-weight:500">' + esc(c.name) + '</td>' +
926
+ '<td style="font-size:11px;font-family:monospace;color:var(--muted);max-width:240px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap" title="' + esc(c.remoteUrl) + '">' + esc(c.remoteUrl) + '</td>' +
927
+ '<td>' + codexStatusBadge(c.cloneStatus) + '</td>' +
928
+ '<td style="text-align:center">' + (c.activeDrafts || 0) + '</td>' +
929
+ '</tr>'
930
+ ).join('');
931
+ document.getElementById('codexes-empty').style.display = data.codexes.length ? 'none' : 'block';
932
+
933
+ // Drafts
934
+ const allDrafts = data.drafts || [];
935
+ document.getElementById('drafts-section').style.display = allDrafts.length ? 'block' : 'none';
936
+ if (allDrafts.length) {
937
+ document.getElementById('drafts-tbody').innerHTML = allDrafts.map(d =>
938
+ '<tr>' +
939
+ '<td class="td-id">' + esc(d.id) + '</td>' +
940
+ '<td>' + esc(d.codexName) + '</td>' +
941
+ '<td style="font-family:monospace;font-size:11px">' + esc(d.branch) + '</td>' +
942
+ '<td class="td-id">' + esc(d.associatedWith || '—') + '</td>' +
943
+ '<td class="td-time">' + fmtDate(d.createdAt) + '</td>' +
944
+ '</tr>'
945
+ ).join('');
946
+ }
947
+ }
948
+
949
+ function codexStatusBadge(s) {
950
+ const map = { ready:'badge-ready', cloning:'badge-cloning', error:'badge-error' };
951
+ return '<span class="badge ' + (map[s]||'badge-cancelled') + '">' + esc(s) + '</span>';
952
+ }
953
+
954
+ // ── Utilities ────────────────────────────────────────────────────
955
+ function statusBadge(s) {
956
+ return '<span class="badge badge-' + s + '">' + esc(s) + '</span>';
957
+ }
958
+
959
+ function statusDot(s) {
960
+ const colors = { pending:'var(--yellow)', running:'var(--blue)', completed:'var(--green)', failed:'var(--red)' };
961
+ return '<span style="width:7px;height:7px;border-radius:50%;background:' + (colors[s]||'var(--muted)') + ';display:inline-block;flex-shrink:0"></span>';
962
+ }
963
+
964
+ function esc(s) {
965
+ return String(s||'').replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;');
966
+ }
967
+
968
+ function fmtDate(iso) {
969
+ if (!iso) return '—';
970
+ const d = new Date(iso);
971
+ const now = new Date();
972
+ const diff = now - d;
973
+ if (diff < 60000) return 'just now';
974
+ if (diff < 3600000) return Math.floor(diff/60000) + 'm ago';
975
+ if (diff < 86400000) return Math.floor(diff/3600000) + 'h ago';
976
+ return d.toLocaleDateString() + ' ' + d.toLocaleTimeString([], {hour:'2-digit',minute:'2-digit'});
977
+ }
978
+
979
+ function fmtDuration(ms) {
980
+ if (!ms && ms !== 0) return '—';
981
+ if (ms < 1000) return ms + 'ms';
982
+ if (ms < 60000) return (ms/1000).toFixed(1) + 's';
983
+ return Math.floor(ms/60000) + 'm ' + Math.round((ms%60000)/1000) + 's';
984
+ }
985
+
986
+ function stableSort(arr, col, dir) {
987
+ return [...arr].sort((a, b) => {
988
+ const av = a[col] ?? '', bv = b[col] ?? '';
989
+ const cmp = av < bv ? -1 : av > bv ? 1 : 0;
990
+ return dir === 'asc' ? cmp : -cmp;
991
+ });
992
+ }
993
+
994
+ function updateSortHeaders(tbodyId, sort) {
995
+ const table = document.getElementById(tbodyId)?.closest('table');
996
+ if (!table) return;
997
+ table.querySelectorAll('th').forEach(th => {
998
+ const col = th.dataset?.col;
999
+ th.classList.toggle('sorted', col === sort.col);
1000
+ const icon = th.querySelector('.sort-icon');
1001
+ if (icon && col === sort.col) icon.textContent = sort.dir === 'asc' ? '↑' : '↓';
1002
+ else if (icon) icon.textContent = '↕';
1003
+ });
1004
+ }
1005
+
1006
+ // Close modals on overlay click
1007
+ document.querySelectorAll('.modal-overlay').forEach(o => {
1008
+ o.addEventListener('click', e => { if (e.target === o) o.classList.remove('open'); });
1009
+ });
1010
+
1011
+ // ── Init ─────────────────────────────────────────────────────────
1012
+ loadOverview();
1013
+ </script>
1014
+ </body>
1015
+ </html>`;
1016
+ }
1017
+ //# sourceMappingURL=html.js.map