@shaifulshabuj-waymarks/server 0.1.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/dist/api/server.js +218 -0
- package/dist/approvals/handler.js +77 -0
- package/dist/db/database.js +209 -0
- package/dist/mcp/server.js +331 -0
- package/dist/notifications/slack.js +77 -0
- package/dist/policies/engine.js +161 -0
- package/package.json +58 -0
- package/src/ui/index.html +418 -0
|
@@ -0,0 +1,418 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8" />
|
|
5
|
+
<title>waymark — agent action viewer</title>
|
|
6
|
+
<style>
|
|
7
|
+
* { box-sizing: border-box; margin: 0; padding: 0; }
|
|
8
|
+
body { font-family: monospace; font-size: 13px; background: #0f0f0f; color: #d0d0d0; padding: 16px; }
|
|
9
|
+
h1 { font-size: 18px; margin-bottom: 4px; color: #fff; }
|
|
10
|
+
.subtitle { color: #555; margin-bottom: 16px; font-size: 12px; }
|
|
11
|
+
.meta { display: flex; gap: 16px; margin-bottom: 12px; color: #666; font-size: 11px; }
|
|
12
|
+
table { width: 100%; border-collapse: collapse; }
|
|
13
|
+
th { text-align: left; padding: 6px 8px; background: #1a1a1a; color: #888; font-weight: normal; border-bottom: 1px solid #333; }
|
|
14
|
+
td { padding: 6px 8px; border-bottom: 1px solid #1e1e1e; vertical-align: top; }
|
|
15
|
+
tr:hover td { background: #161616; }
|
|
16
|
+
.badge { display: inline-block; padding: 2px 6px; border-radius: 3px; font-size: 11px; font-weight: bold; }
|
|
17
|
+
.badge-write_file { background: #7c3a00; color: #ffb347; }
|
|
18
|
+
.badge-read_file { background: #002f5c; color: #5ab4ff; }
|
|
19
|
+
.badge-bash { background: #222; color: #aaa; border: 1px solid #444; }
|
|
20
|
+
.decision-block { background: #5c0000; color: #ff6b6b; }
|
|
21
|
+
.decision-pending { background: #4a3800; color: #ffd166; }
|
|
22
|
+
.decision-rejected { background: #3a0000; color: #ff9999; }
|
|
23
|
+
.status-success { color: #4caf50; }
|
|
24
|
+
.status-error { color: #f44336; }
|
|
25
|
+
.status-pending { color: #ffb347; }
|
|
26
|
+
.status-blocked { color: #ff6b6b; }
|
|
27
|
+
.path { color: #888; max-width: 280px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; display: block; }
|
|
28
|
+
button.rollback {
|
|
29
|
+
background: #3a1f00; border: 1px solid #7c3a00; color: #ffb347;
|
|
30
|
+
padding: 2px 8px; cursor: pointer; font-size: 11px; font-family: monospace;
|
|
31
|
+
border-radius: 3px;
|
|
32
|
+
}
|
|
33
|
+
button.rollback:hover { background: #5c3000; }
|
|
34
|
+
button.rollback:disabled { opacity: 0.4; cursor: default; }
|
|
35
|
+
.msg-ok { color: #4caf50; margin-left: 6px; }
|
|
36
|
+
.msg-err { color: #f44336; margin-left: 6px; }
|
|
37
|
+
.rolled-back { color: #555; font-size: 11px; }
|
|
38
|
+
#status-bar { position: fixed; bottom: 8px; right: 16px; color: #333; font-size: 11px; }
|
|
39
|
+
.empty { padding: 32px; text-align: center; color: #444; }
|
|
40
|
+
|
|
41
|
+
/* Config viewer */
|
|
42
|
+
#config-section { margin-top: 24px; }
|
|
43
|
+
#config-toggle {
|
|
44
|
+
background: none; border: 1px solid #333; color: #888; padding: 4px 10px;
|
|
45
|
+
cursor: pointer; font-family: monospace; font-size: 12px; border-radius: 3px;
|
|
46
|
+
}
|
|
47
|
+
#config-toggle:hover { border-color: #555; color: #aaa; }
|
|
48
|
+
#config-content { display: none; margin-top: 12px; padding: 12px; background: #141414; border: 1px solid #222; border-radius: 4px; }
|
|
49
|
+
#config-content h3 { color: #666; font-size: 11px; font-weight: normal; margin-bottom: 10px; }
|
|
50
|
+
.policy-group { margin-bottom: 12px; }
|
|
51
|
+
.policy-group h4 { font-size: 11px; color: #555; margin-bottom: 4px; font-weight: normal; text-transform: uppercase; letter-spacing: 0.05em; }
|
|
52
|
+
.policy-group ul { list-style: none; padding: 0; }
|
|
53
|
+
.policy-group li { padding: 2px 0; font-size: 12px; }
|
|
54
|
+
.policy-allow { color: #4caf50; }
|
|
55
|
+
.policy-block { color: #ff6b6b; }
|
|
56
|
+
.policy-pending { color: #ffd166; }
|
|
57
|
+
|
|
58
|
+
/* v3: approval flow */
|
|
59
|
+
.status-rejected { color: #e57373; }
|
|
60
|
+
.status-approved { color: #4caf50; }
|
|
61
|
+
.btn-approve {
|
|
62
|
+
background: #003a1a; border: 1px solid #2e7d32; color: #81c784;
|
|
63
|
+
padding: 2px 8px; cursor: pointer; font-size: 11px; font-family: monospace;
|
|
64
|
+
border-radius: 3px; margin-right: 4px;
|
|
65
|
+
}
|
|
66
|
+
.btn-approve:hover { background: #1b5e20; }
|
|
67
|
+
.btn-approve:disabled { opacity: 0.4; cursor: default; }
|
|
68
|
+
.btn-reject {
|
|
69
|
+
background: #3a0000; border: 1px solid #7f0000; color: #ef9a9a;
|
|
70
|
+
padding: 2px 8px; cursor: pointer; font-size: 11px; font-family: monospace;
|
|
71
|
+
border-radius: 3px;
|
|
72
|
+
}
|
|
73
|
+
.btn-reject:hover { background: #5c0000; }
|
|
74
|
+
.btn-reject:disabled { opacity: 0.4; cursor: default; }
|
|
75
|
+
.reject-input {
|
|
76
|
+
background: #1a1a1a; border: 1px solid #444; color: #ccc;
|
|
77
|
+
padding: 2px 6px; font-family: monospace; font-size: 11px;
|
|
78
|
+
border-radius: 3px; width: 120px; margin-right: 4px;
|
|
79
|
+
}
|
|
80
|
+
#pending-badge {
|
|
81
|
+
background: #7f0000; color: #ef9a9a; border-radius: 3px;
|
|
82
|
+
padding: 1px 6px; font-size: 13px; margin-left: 8px;
|
|
83
|
+
}
|
|
84
|
+
</style>
|
|
85
|
+
</head>
|
|
86
|
+
<body>
|
|
87
|
+
<h1>waymark <span id="pending-badge" style="display:none"></span></h1>
|
|
88
|
+
<p class="subtitle">uglybugly agent action viewer — intercepts and logs MCP tool calls</p>
|
|
89
|
+
<div class="meta">
|
|
90
|
+
<span id="count-display">loading...</span>
|
|
91
|
+
<span id="refresh-display"></span>
|
|
92
|
+
</div>
|
|
93
|
+
<table>
|
|
94
|
+
<thead>
|
|
95
|
+
<tr>
|
|
96
|
+
<th>Time</th>
|
|
97
|
+
<th>Tool</th>
|
|
98
|
+
<th>Decision</th>
|
|
99
|
+
<th>Target / Command</th>
|
|
100
|
+
<th>Status</th>
|
|
101
|
+
<th>Stdout</th>
|
|
102
|
+
<th>Action</th>
|
|
103
|
+
</tr>
|
|
104
|
+
</thead>
|
|
105
|
+
<tbody id="action-tbody">
|
|
106
|
+
<tr><td colspan="7" class="empty">loading...</td></tr>
|
|
107
|
+
</tbody>
|
|
108
|
+
</table>
|
|
109
|
+
|
|
110
|
+
<div id="config-section">
|
|
111
|
+
<button id="config-toggle">▶ Current Policy</button>
|
|
112
|
+
<div id="config-content">
|
|
113
|
+
<h3>waymark.config.json</h3>
|
|
114
|
+
<div id="config-body">loading...</div>
|
|
115
|
+
</div>
|
|
116
|
+
</div>
|
|
117
|
+
|
|
118
|
+
<div id="status-bar">auto-refresh every 3s</div>
|
|
119
|
+
|
|
120
|
+
<script>
|
|
121
|
+
function timeAgo(dateStr) {
|
|
122
|
+
const now = new Date();
|
|
123
|
+
const then = new Date(dateStr + (dateStr.includes('Z') ? '' : 'Z'));
|
|
124
|
+
const diff = Math.floor((now - then) / 1000);
|
|
125
|
+
if (diff < 5) return 'just now';
|
|
126
|
+
if (diff < 60) return diff + 's ago';
|
|
127
|
+
if (diff < 3600) return Math.floor(diff / 60) + 'm ago';
|
|
128
|
+
if (diff < 86400) return Math.floor(diff / 3600) + 'h ago';
|
|
129
|
+
return Math.floor(diff / 86400) + 'd ago';
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function toolBadge(name) {
|
|
133
|
+
return `<span class="badge badge-${name}">${name}</span>`;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function statusLabel(status) {
|
|
137
|
+
return `<span class="status-${status}">${status}</span>`;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function decisionBadge(row) {
|
|
141
|
+
const d = row.decision || 'allow';
|
|
142
|
+
if (d === 'block') {
|
|
143
|
+
const tip = row.policy_reason ? row.policy_reason.replace(/"/g, '"') : '';
|
|
144
|
+
return `<span class="badge decision-block" title="${tip}">blocked</span>`;
|
|
145
|
+
}
|
|
146
|
+
if (d === 'pending') {
|
|
147
|
+
const tip = row.policy_reason ? row.policy_reason.replace(/"/g, '"') : '';
|
|
148
|
+
return `<span class="badge decision-pending" title="${tip}">pending</span>`;
|
|
149
|
+
}
|
|
150
|
+
if (d === 'rejected') {
|
|
151
|
+
const tip = row.rejected_reason ? row.rejected_reason.replace(/"/g, '"') : '';
|
|
152
|
+
return `<span class="badge decision-rejected" title="${tip}">rejected</span>`;
|
|
153
|
+
}
|
|
154
|
+
return `<span style="color:#333">—</span>`;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function truncatePath(p) {
|
|
158
|
+
if (!p) return '<span style="color:#444">—</span>';
|
|
159
|
+
const parts = p.replace(/\\/g, '/').split('/');
|
|
160
|
+
const display = parts.length > 3 ? '…/' + parts.slice(-3).join('/') : p;
|
|
161
|
+
return `<span class="path" title="${p}">${display}</span>`;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function getTargetDisplay(row) {
|
|
165
|
+
if (row.tool_name === 'bash') {
|
|
166
|
+
const cmd = JSON.parse(row.input_payload).command || '';
|
|
167
|
+
const short = cmd.length > 60 ? cmd.slice(0, 57) + '...' : cmd;
|
|
168
|
+
return `<span class="path" title="${cmd.replace(/"/g, '"')}">${short}</span>`;
|
|
169
|
+
}
|
|
170
|
+
return truncatePath(row.target_path);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
async function doRollback(actionId, btn, msgSpan, isNewFile) {
|
|
174
|
+
btn.disabled = true;
|
|
175
|
+
btn.textContent = isNewFile ? 'deleting...' : 'rolling back...';
|
|
176
|
+
try {
|
|
177
|
+
const res = await fetch(`/api/actions/${actionId}/rollback`, { method: 'POST' });
|
|
178
|
+
const data = await res.json();
|
|
179
|
+
if (res.ok) {
|
|
180
|
+
msgSpan.className = 'msg-ok';
|
|
181
|
+
msgSpan.textContent = data.action === 'deleted' ? '✓ deleted' : '✓ restored';
|
|
182
|
+
btn.textContent = data.action === 'deleted' ? 'deleted' : 'rolled back';
|
|
183
|
+
} else {
|
|
184
|
+
msgSpan.className = 'msg-err';
|
|
185
|
+
msgSpan.textContent = '✗ ' + (data.error || 'failed');
|
|
186
|
+
btn.disabled = false;
|
|
187
|
+
btn.textContent = isNewFile ? 'delete (new file)' : 'rollback';
|
|
188
|
+
}
|
|
189
|
+
} catch (e) {
|
|
190
|
+
msgSpan.className = 'msg-err';
|
|
191
|
+
msgSpan.textContent = '✗ network error';
|
|
192
|
+
btn.disabled = false;
|
|
193
|
+
btn.textContent = isNewFile ? 'delete (new file)' : 'rollback';
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
async function doApprove(actionId, btn, msgSpan) {
|
|
198
|
+
btn.disabled = true;
|
|
199
|
+
btn.textContent = 'approving...';
|
|
200
|
+
try {
|
|
201
|
+
const res = await fetch(`/api/actions/${actionId}/approve`, { method: 'POST' });
|
|
202
|
+
const data = await res.json();
|
|
203
|
+
if (res.ok) {
|
|
204
|
+
msgSpan.className = 'msg-ok';
|
|
205
|
+
msgSpan.textContent = '✓ approved';
|
|
206
|
+
// Full refresh to get updated row
|
|
207
|
+
setTimeout(refresh, 300);
|
|
208
|
+
} else {
|
|
209
|
+
msgSpan.className = 'msg-err';
|
|
210
|
+
msgSpan.textContent = '✗ ' + (data.error || 'failed');
|
|
211
|
+
btn.disabled = false;
|
|
212
|
+
btn.textContent = '✅ Approve';
|
|
213
|
+
}
|
|
214
|
+
} catch (e) {
|
|
215
|
+
msgSpan.className = 'msg-err';
|
|
216
|
+
msgSpan.textContent = '✗ network error';
|
|
217
|
+
btn.disabled = false;
|
|
218
|
+
btn.textContent = '✅ Approve';
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
async function doReject(actionId, btn, rejectBtn, msgSpan) {
|
|
223
|
+
// Show inline input for reason
|
|
224
|
+
rejectBtn.disabled = true;
|
|
225
|
+
const inputEl = document.createElement('input');
|
|
226
|
+
inputEl.className = 'reject-input';
|
|
227
|
+
inputEl.type = 'text';
|
|
228
|
+
inputEl.value = 'Not approved';
|
|
229
|
+
inputEl.placeholder = 'reason...';
|
|
230
|
+
const confirmBtn = document.createElement('button');
|
|
231
|
+
confirmBtn.className = 'btn-reject';
|
|
232
|
+
confirmBtn.textContent = 'Confirm';
|
|
233
|
+
rejectBtn.replaceWith(inputEl);
|
|
234
|
+
msgSpan.parentNode.insertBefore(confirmBtn, msgSpan);
|
|
235
|
+
|
|
236
|
+
confirmBtn.addEventListener('click', async () => {
|
|
237
|
+
confirmBtn.disabled = true;
|
|
238
|
+
inputEl.disabled = true;
|
|
239
|
+
confirmBtn.textContent = 'rejecting...';
|
|
240
|
+
try {
|
|
241
|
+
const res = await fetch(`/api/actions/${actionId}/reject`, {
|
|
242
|
+
method: 'POST',
|
|
243
|
+
headers: { 'Content-Type': 'application/json' },
|
|
244
|
+
body: JSON.stringify({ reason: inputEl.value || 'Not approved' }),
|
|
245
|
+
});
|
|
246
|
+
const data = await res.json();
|
|
247
|
+
if (res.ok) {
|
|
248
|
+
msgSpan.className = 'msg-ok';
|
|
249
|
+
msgSpan.textContent = '✓ rejected';
|
|
250
|
+
setTimeout(refresh, 300);
|
|
251
|
+
} else {
|
|
252
|
+
msgSpan.className = 'msg-err';
|
|
253
|
+
msgSpan.textContent = '✗ ' + (data.error || 'failed');
|
|
254
|
+
confirmBtn.disabled = false;
|
|
255
|
+
confirmBtn.textContent = 'Confirm';
|
|
256
|
+
}
|
|
257
|
+
} catch (e) {
|
|
258
|
+
msgSpan.className = 'msg-err';
|
|
259
|
+
msgSpan.textContent = '✗ network error';
|
|
260
|
+
confirmBtn.disabled = false;
|
|
261
|
+
confirmBtn.textContent = 'Confirm';
|
|
262
|
+
}
|
|
263
|
+
});
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
function renderActions(actions) {
|
|
267
|
+
const tbody = document.getElementById('action-tbody');
|
|
268
|
+
if (!actions.length) {
|
|
269
|
+
tbody.innerHTML = '<tr><td colspan="7" class="empty">No actions yet. Connect Claude Code to the waymark MCP server and run some tools.</td></tr>';
|
|
270
|
+
return;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
const rows = actions.map(row => {
|
|
274
|
+
const isWriteFile = row.tool_name === 'write_file';
|
|
275
|
+
const isNewFile = isWriteFile && !row.before_snapshot;
|
|
276
|
+
const isPending = row.decision === 'pending' && row.status === 'pending';
|
|
277
|
+
const isApproved = row.status === 'success' && row.approved_by;
|
|
278
|
+
const isRejected = row.status === 'rejected';
|
|
279
|
+
const isBlocked = row.decision === 'block';
|
|
280
|
+
const canRollback = isWriteFile && !row.rolled_back && !isBlocked && !isPending && !isApproved && row.status === 'success' && !row.approved_by;
|
|
281
|
+
|
|
282
|
+
let actionCell;
|
|
283
|
+
if (isPending) {
|
|
284
|
+
actionCell = `<button class="btn-approve" data-id="${row.action_id}">✅ Approve</button><button class="btn-reject" data-id="${row.action_id}">❌ Reject</button><span class="action-msg"></span>`;
|
|
285
|
+
} else if (isApproved) {
|
|
286
|
+
const tip = `by ${row.approved_by}${row.approved_at ? ' at ' + row.approved_at : ''}`.replace(/"/g, '"');
|
|
287
|
+
actionCell = `<span class="status-approved" title="${tip}">✅ Approved</span>`;
|
|
288
|
+
} else if (isRejected) {
|
|
289
|
+
const tip = (row.rejected_reason || '').replace(/"/g, '"');
|
|
290
|
+
actionCell = `<span class="status-rejected" title="${tip}">❌ Rejected</span>`;
|
|
291
|
+
} else if (row.rolled_back) {
|
|
292
|
+
actionCell = `<span class="rolled-back">↩ ${isNewFile ? 'deleted' : 'rolled back'}</span>`;
|
|
293
|
+
} else if (canRollback) {
|
|
294
|
+
const btnLabel = isNewFile ? 'delete (new file)' : 'rollback';
|
|
295
|
+
actionCell = `<button class="rollback" data-id="${row.action_id}" data-newfile="${isNewFile}">${btnLabel}</button><span class="rollback-msg"></span>`;
|
|
296
|
+
} else {
|
|
297
|
+
actionCell = `<span style="color:#333">—</span>`;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
const stdoutPreview = row.tool_name === 'bash' && row.stdout
|
|
301
|
+
? `<span style="color:#666;font-size:11px">${row.stdout.slice(0, 100).replace(/\n/g, '↵')}</span>`
|
|
302
|
+
: `<span style="color:#333">—</span>`;
|
|
303
|
+
|
|
304
|
+
return `<tr>
|
|
305
|
+
<td style="white-space:nowrap;color:#555">${timeAgo(row.created_at)}</td>
|
|
306
|
+
<td>${toolBadge(row.tool_name)}</td>
|
|
307
|
+
<td>${decisionBadge(row)}</td>
|
|
308
|
+
<td>${getTargetDisplay(row)}</td>
|
|
309
|
+
<td>${statusLabel(row.status)}${row.error_message ? `<br><span style="color:#666;font-size:11px">${row.error_message.slice(0,80)}</span>` : ''}</td>
|
|
310
|
+
<td>${stdoutPreview}</td>
|
|
311
|
+
<td style="white-space:nowrap">${actionCell}</td>
|
|
312
|
+
</tr>`;
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
tbody.innerHTML = rows.join('');
|
|
316
|
+
|
|
317
|
+
tbody.querySelectorAll('button.rollback').forEach(btn => {
|
|
318
|
+
btn.addEventListener('click', () => {
|
|
319
|
+
const msgSpan = btn.nextElementSibling;
|
|
320
|
+
doRollback(btn.dataset.id, btn, msgSpan, btn.dataset.newfile === 'true');
|
|
321
|
+
});
|
|
322
|
+
});
|
|
323
|
+
|
|
324
|
+
tbody.querySelectorAll('button.btn-approve').forEach(btn => {
|
|
325
|
+
btn.addEventListener('click', () => {
|
|
326
|
+
const msgSpan = btn.parentElement.querySelector('.action-msg');
|
|
327
|
+
doApprove(btn.dataset.id, btn, msgSpan);
|
|
328
|
+
});
|
|
329
|
+
});
|
|
330
|
+
|
|
331
|
+
tbody.querySelectorAll('button.btn-reject').forEach(btn => {
|
|
332
|
+
btn.addEventListener('click', () => {
|
|
333
|
+
const approveBtn = btn.previousElementSibling;
|
|
334
|
+
const msgSpan = btn.nextElementSibling;
|
|
335
|
+
approveBtn.disabled = true;
|
|
336
|
+
doReject(btn.dataset.id, approveBtn, btn, msgSpan);
|
|
337
|
+
});
|
|
338
|
+
});
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
async function refresh() {
|
|
342
|
+
try {
|
|
343
|
+
const [actionsRes, countRes] = await Promise.all([
|
|
344
|
+
fetch('/api/actions'),
|
|
345
|
+
fetch('/api/actions?count=true'),
|
|
346
|
+
]);
|
|
347
|
+
const actions = await actionsRes.json();
|
|
348
|
+
const { count } = await countRes.json();
|
|
349
|
+
renderActions(Array.isArray(actions) ? actions : []);
|
|
350
|
+
document.getElementById('count-display').textContent = `${actions.length} action(s)`;
|
|
351
|
+
document.getElementById('refresh-display').textContent = 'last refresh: ' + new Date().toLocaleTimeString();
|
|
352
|
+
const badge = document.getElementById('pending-badge');
|
|
353
|
+
badge.style.display = count > 0 ? 'inline' : 'none';
|
|
354
|
+
badge.textContent = `[${count} pending]`;
|
|
355
|
+
} catch (e) {
|
|
356
|
+
document.getElementById('count-display').textContent = 'error fetching actions';
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
// Config viewer toggle
|
|
361
|
+
const configToggle = document.getElementById('config-toggle');
|
|
362
|
+
const configContent = document.getElementById('config-content');
|
|
363
|
+
let configOpen = false;
|
|
364
|
+
|
|
365
|
+
configToggle.addEventListener('click', async () => {
|
|
366
|
+
configOpen = !configOpen;
|
|
367
|
+
configContent.style.display = configOpen ? 'block' : 'none';
|
|
368
|
+
configToggle.textContent = (configOpen ? '▼' : '▶') + ' Current Policy';
|
|
369
|
+
if (configOpen) await loadConfigView();
|
|
370
|
+
});
|
|
371
|
+
|
|
372
|
+
async function loadConfigView() {
|
|
373
|
+
const body = document.getElementById('config-body');
|
|
374
|
+
try {
|
|
375
|
+
const res = await fetch('/api/config');
|
|
376
|
+
const cfg = await res.json();
|
|
377
|
+
const p = cfg.policies || {};
|
|
378
|
+
|
|
379
|
+
function renderList(items, cls) {
|
|
380
|
+
if (!items || !items.length) return '<li style="color:#444">none</li>';
|
|
381
|
+
return items.map(i => {
|
|
382
|
+
if (i.startsWith('regex:')) {
|
|
383
|
+
const pat = i.slice(6);
|
|
384
|
+
return `<li class="${cls}">${pat} <em style="color:#555;font-style:italic">[pattern]</em></li>`;
|
|
385
|
+
}
|
|
386
|
+
return `<li class="${cls}">${i}</li>`;
|
|
387
|
+
}).join('');
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
body.innerHTML = `
|
|
391
|
+
<div class="policy-group">
|
|
392
|
+
<h4>Allowed Paths</h4>
|
|
393
|
+
<ul>${renderList(p.allowedPaths, 'policy-allow')}</ul>
|
|
394
|
+
</div>
|
|
395
|
+
<div class="policy-group">
|
|
396
|
+
<h4>Blocked Paths</h4>
|
|
397
|
+
<ul>${renderList(p.blockedPaths, 'policy-block')}</ul>
|
|
398
|
+
</div>
|
|
399
|
+
<div class="policy-group">
|
|
400
|
+
<h4>Blocked Commands</h4>
|
|
401
|
+
<ul>${renderList(p.blockedCommands, 'policy-block')}</ul>
|
|
402
|
+
</div>
|
|
403
|
+
<div class="policy-group">
|
|
404
|
+
<h4>Require Approval</h4>
|
|
405
|
+
<ul>${renderList(p.requireApproval, 'policy-pending')}</ul>
|
|
406
|
+
</div>
|
|
407
|
+
`;
|
|
408
|
+
} catch (e) {
|
|
409
|
+
body.textContent = 'Error loading config';
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
// Initial load + interval
|
|
414
|
+
refresh();
|
|
415
|
+
setInterval(refresh, 3000);
|
|
416
|
+
</script>
|
|
417
|
+
</body>
|
|
418
|
+
</html>
|