@way_marks/server 0.9.0 → 2.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/dist/api/server.js +598 -0
- package/dist/approval/manager.js +249 -0
- package/dist/db/database.js +754 -185
- package/dist/escalation/manager.js +205 -0
- package/dist/notifications/service.js +457 -0
- package/dist/policies/engine.js +11 -5
- package/dist/policy/engine.js +420 -0
- package/dist/remediation/recommender.js +309 -0
- package/dist/risk/analyzer.js +453 -0
- package/dist/rollback/blocker.js +222 -0
- package/dist/rollback/manager.js +245 -0
- package/package.json +1 -1
- package/src/ui/index.html +862 -0
- package/dist/approvals/handler.test.js +0 -172
- package/dist/policies/engine.test.js +0 -241
package/src/ui/index.html
CHANGED
|
@@ -103,6 +103,24 @@
|
|
|
103
103
|
.project-item.current { background: #1a1a1a; border-left-color: #5ab4ff; }
|
|
104
104
|
.project-port { font-size: 10px; color: #555; margin-left: 4px; }
|
|
105
105
|
|
|
106
|
+
/* Phase 1: Tab switcher */
|
|
107
|
+
.tab-button { transition: color 0.2s, border-color 0.2s; }
|
|
108
|
+
.tab-button.active { color: #5ab4ff; border-bottom-color: #5ab4ff !important; font-weight: bold; }
|
|
109
|
+
.tab-content { display: none; }
|
|
110
|
+
.tab-content.active { display: block; }
|
|
111
|
+
#sessions-table { width: 100%; border-collapse: collapse; margin-bottom: 16px; }
|
|
112
|
+
#sessions-table thead tr { background: #1a1a1a; }
|
|
113
|
+
#sessions-table th, #sessions-table td { padding: 6px 8px; border-bottom: 1px solid #1e1e1e; text-align: left; }
|
|
114
|
+
#sessions-table tbody tr:hover { background: #161616; }
|
|
115
|
+
.session-status-active { color: #4caf50; }
|
|
116
|
+
.session-status-rolled_back { color: #5ab4ff; }
|
|
117
|
+
.session-id { color: #888; font-size: 11px; max-width: 150px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
118
|
+
.btn-view-session { background: #003a5c; border: 1px solid #0066cc; color: #5ab4ff; padding: 2px 6px; cursor: pointer; font-size: 10px; font-family: monospace; border-radius: 2px; margin-right: 2px; }
|
|
119
|
+
.btn-view-session:hover { background: #004080; }
|
|
120
|
+
.btn-rollback-session { background: #3a1f00; border: 1px solid #7c3a00; color: #ffb347; padding: 2px 6px; cursor: pointer; font-size: 10px; font-family: monospace; border-radius: 2px; }
|
|
121
|
+
.btn-rollback-session:hover { background: #5c3000; }
|
|
122
|
+
.btn-rollback-session:disabled { opacity: 0.4; cursor: default; }
|
|
123
|
+
|
|
106
124
|
</style>
|
|
107
125
|
</head>
|
|
108
126
|
<body>
|
|
@@ -147,12 +165,24 @@
|
|
|
147
165
|
</div>
|
|
148
166
|
</div>
|
|
149
167
|
|
|
168
|
+
<!-- Phase 1 & 2: View tabs -->
|
|
169
|
+
<div style="margin-bottom:12px; border-bottom: 1px solid #333; padding-bottom: 8px;">
|
|
170
|
+
<button id="tab-actions" class="tab-button" data-tab="actions" style="background: none; border: none; color: #5ab4ff; padding: 4px 12px; cursor: pointer; font-family: monospace; font-size: 12px; border-bottom: 2px solid #5ab4ff; font-weight: bold;">Actions</button>
|
|
171
|
+
<button id="tab-sessions" class="tab-button" data-tab="sessions" style="background: none; border: none; color: #888; padding: 4px 12px; cursor: pointer; font-family: monospace; font-size: 12px; border-bottom: 2px solid transparent;">Sessions (Phase 1)</button>
|
|
172
|
+
<button id="tab-team" class="tab-button" data-tab="team" style="background: none; border: none; color: #888; padding: 4px 12px; cursor: pointer; font-family: monospace; font-size: 12px; border-bottom: 2px solid transparent;">Team (Phase 2)</button>
|
|
173
|
+
<button id="tab-approvals" class="tab-button" data-tab="approvals" style="background: none; border: none; color: #888; padding: 4px 12px; cursor: pointer; font-family: monospace; font-size: 12px; border-bottom: 2px solid transparent;">Approvals (Phase 2)</button>
|
|
174
|
+
<button id="tab-escalations" class="tab-button" data-tab="escalations" style="background: none; border: none; color: #888; padding: 4px 12px; cursor: pointer; font-family: monospace; font-size: 12px; border-bottom: 2px solid transparent;">Escalations (Phase 3)</button>
|
|
175
|
+
<button id="tab-remediation" class="tab-button" data-tab="remediation" style="background: none; border: none; color: #888; padding: 4px 12px; cursor: pointer; font-family: monospace; font-size: 12px; border-bottom: 2px solid transparent;">Remediation (Phase 4)</button>
|
|
176
|
+
</div>
|
|
177
|
+
|
|
150
178
|
<button id="hub-toggle">📋</button>
|
|
151
179
|
<nav id="hub-nav">
|
|
152
180
|
<h3>Projects</h3>
|
|
153
181
|
<ul id="project-list" class="project-list"></ul>
|
|
154
182
|
</nav>
|
|
155
183
|
<div id="main-content">
|
|
184
|
+
<!-- Actions Tab -->
|
|
185
|
+
<div id="tab-actions-content" class="tab-content active">
|
|
156
186
|
<table>
|
|
157
187
|
<thead>
|
|
158
188
|
<tr>
|
|
@@ -169,6 +199,228 @@
|
|
|
169
199
|
<tr><td colspan="7" class="empty">loading...</td></tr>
|
|
170
200
|
</tbody>
|
|
171
201
|
</table>
|
|
202
|
+
</div>
|
|
203
|
+
|
|
204
|
+
<!-- Phase 1: Sessions Tab -->
|
|
205
|
+
<div id="tab-sessions-content" class="tab-content">
|
|
206
|
+
<table id="sessions-table">
|
|
207
|
+
<thead>
|
|
208
|
+
<tr>
|
|
209
|
+
<th>Session ID</th>
|
|
210
|
+
<th>Actions</th>
|
|
211
|
+
<th>Created</th>
|
|
212
|
+
<th>Status</th>
|
|
213
|
+
<th>Actions</th>
|
|
214
|
+
</tr>
|
|
215
|
+
</thead>
|
|
216
|
+
<tbody id="sessions-tbody">
|
|
217
|
+
<tr><td colspan="5" class="empty">loading...</td></tr>
|
|
218
|
+
</tbody>
|
|
219
|
+
</table>
|
|
220
|
+
<div id="session-details" style="display:none; margin-top: 16px; padding: 12px; background: #141414; border: 1px solid #222; border-radius: 4px;">
|
|
221
|
+
<h3 style="color: #888; margin-bottom: 8px;">Session Details</h3>
|
|
222
|
+
<div id="session-details-content"></div>
|
|
223
|
+
</div>
|
|
224
|
+
</div>
|
|
225
|
+
|
|
226
|
+
<!-- Phase 2: Team Tab -->
|
|
227
|
+
<div id="tab-team-content" class="tab-content">
|
|
228
|
+
<div style="margin-bottom: 16px;">
|
|
229
|
+
<h3>Team Members</h3>
|
|
230
|
+
<div style="margin-top: 8px; padding: 8px; background: #141414; border-radius: 3px;">
|
|
231
|
+
<input id="new-member-name" type="text" placeholder="Name" style="padding: 4px; margin-right: 6px; background: #0a0a0a; border: 1px solid #333; color: #d0d0d0; border-radius: 2px;" />
|
|
232
|
+
<input id="new-member-email" type="email" placeholder="Email" style="padding: 4px; margin-right: 6px; background: #0a0a0a; border: 1px solid #333; color: #d0d0d0; border-radius: 2px;" />
|
|
233
|
+
<button id="add-team-member-btn" style="padding: 4px 8px; background: #003a4d; border: 1px solid #00556b; color: #5ab4ff; cursor: pointer; border-radius: 2px;">Add Member</button>
|
|
234
|
+
</div>
|
|
235
|
+
</div>
|
|
236
|
+
<table id="team-members-table">
|
|
237
|
+
<thead>
|
|
238
|
+
<tr>
|
|
239
|
+
<th>Name</th>
|
|
240
|
+
<th>Email</th>
|
|
241
|
+
<th>Role</th>
|
|
242
|
+
<th>Added</th>
|
|
243
|
+
<th>Action</th>
|
|
244
|
+
</tr>
|
|
245
|
+
</thead>
|
|
246
|
+
<tbody id="team-tbody">
|
|
247
|
+
<tr><td colspan="5" class="empty">loading...</td></tr>
|
|
248
|
+
</tbody>
|
|
249
|
+
</table>
|
|
250
|
+
</div>
|
|
251
|
+
|
|
252
|
+
<!-- Phase 2: Approvals Tab -->
|
|
253
|
+
<div id="tab-approvals-content" class="tab-content">
|
|
254
|
+
<div style="margin-bottom: 16px;">
|
|
255
|
+
<h3>Approval Routing Rules</h3>
|
|
256
|
+
<div style="margin-top: 8px; padding: 8px; background: #141414; border-radius: 3px;">
|
|
257
|
+
<input id="new-route-name" type="text" placeholder="Route name" style="padding: 4px; margin-right: 6px; background: #0a0a0a; border: 1px solid #333; color: #d0d0d0; border-radius: 2px;" />
|
|
258
|
+
<select id="new-route-type" style="padding: 4px; margin-right: 6px; background: #0a0a0a; border: 1px solid #333; color: #d0d0d0; border-radius: 2px;">
|
|
259
|
+
<option value="all_sessions">All Sessions</option>
|
|
260
|
+
<option value="high_impact">High Impact</option>
|
|
261
|
+
</select>
|
|
262
|
+
<button id="create-route-btn" style="padding: 4px 8px; background: #003a4d; border: 1px solid #00556b; color: #5ab4ff; cursor: pointer; border-radius: 2px;">Create Route</button>
|
|
263
|
+
</div>
|
|
264
|
+
</div>
|
|
265
|
+
<table id="routes-table">
|
|
266
|
+
<thead>
|
|
267
|
+
<tr>
|
|
268
|
+
<th>Name</th>
|
|
269
|
+
<th>Type</th>
|
|
270
|
+
<th>Approvers</th>
|
|
271
|
+
<th>Created</th>
|
|
272
|
+
<th>Action</th>
|
|
273
|
+
</tr>
|
|
274
|
+
</thead>
|
|
275
|
+
<tbody id="routes-tbody">
|
|
276
|
+
<tr><td colspan="5" class="empty">loading...</td></tr>
|
|
277
|
+
</tbody>
|
|
278
|
+
</table>
|
|
279
|
+
<div style="margin-top: 24px; border-top: 1px solid #333; padding-top: 16px;">
|
|
280
|
+
<h3>Pending Approvals</h3>
|
|
281
|
+
<table id="approvals-queue-table">
|
|
282
|
+
<thead>
|
|
283
|
+
<tr>
|
|
284
|
+
<th>Session</th>
|
|
285
|
+
<th>Requester</th>
|
|
286
|
+
<th>Status</th>
|
|
287
|
+
<th>Approvals Needed</th>
|
|
288
|
+
<th>Created</th>
|
|
289
|
+
<th>Action</th>
|
|
290
|
+
</tr>
|
|
291
|
+
</thead>
|
|
292
|
+
<tbody id="approvals-queue-tbody">
|
|
293
|
+
<tr><td colspan="6" class="empty">loading...</td></tr>
|
|
294
|
+
</tbody>
|
|
295
|
+
</table>
|
|
296
|
+
</div>
|
|
297
|
+
</div>
|
|
298
|
+
|
|
299
|
+
<!-- Phase 3: Escalations Tab -->
|
|
300
|
+
<div id="tab-escalations-content" class="tab-content">
|
|
301
|
+
<div style="margin-bottom: 16px;">
|
|
302
|
+
<h3>Escalation Rules</h3>
|
|
303
|
+
<div style="margin-top: 8px; padding: 8px; background: #141414; border-radius: 3px;">
|
|
304
|
+
<input id="new-escalation-name" type="text" placeholder="Rule name" style="padding: 4px; margin-right: 6px; background: #0a0a0a; border: 1px solid #333; color: #d0d0d0; border-radius: 2px;" />
|
|
305
|
+
<input id="new-escalation-timeout" type="number" placeholder="Timeout (hours)" style="padding: 4px; margin-right: 6px; background: #0a0a0a; border: 1px solid #333; color: #d0d0d0; border-radius: 2px;" />
|
|
306
|
+
<button id="create-escalation-rule-btn" style="padding: 4px 8px; background: #3a2a00; border: 1px solid #7c5a00; color: #ffb347; cursor: pointer; border-radius: 2px;">Create Rule</button>
|
|
307
|
+
</div>
|
|
308
|
+
</div>
|
|
309
|
+
<table id="escalation-rules-table">
|
|
310
|
+
<thead>
|
|
311
|
+
<tr>
|
|
312
|
+
<th>Name</th>
|
|
313
|
+
<th>Timeout (hours)</th>
|
|
314
|
+
<th>Targets</th>
|
|
315
|
+
<th>Created</th>
|
|
316
|
+
<th>Action</th>
|
|
317
|
+
</tr>
|
|
318
|
+
</thead>
|
|
319
|
+
<tbody id="escalation-rules-tbody">
|
|
320
|
+
<tr><td colspan="5" class="empty">loading...</td></tr>
|
|
321
|
+
</tbody>
|
|
322
|
+
</table>
|
|
323
|
+
<div style="margin-top: 24px; border-top: 1px solid #333; padding-top: 16px;">
|
|
324
|
+
<h3>Pending Escalations</h3>
|
|
325
|
+
<table id="escalation-queue-table">
|
|
326
|
+
<thead>
|
|
327
|
+
<tr>
|
|
328
|
+
<th>Session</th>
|
|
329
|
+
<th>Requester</th>
|
|
330
|
+
<th>Status</th>
|
|
331
|
+
<th>Deadline</th>
|
|
332
|
+
<th>Action</th>
|
|
333
|
+
</tr>
|
|
334
|
+
</thead>
|
|
335
|
+
<tbody id="escalation-queue-tbody">
|
|
336
|
+
<tr><td colspan="5" class="empty">loading...</td></tr>
|
|
337
|
+
</tbody>
|
|
338
|
+
</table>
|
|
339
|
+
</div>
|
|
340
|
+
</div>
|
|
341
|
+
|
|
342
|
+
<!-- Phase 4: Remediation Tab -->
|
|
343
|
+
<div id="tab-remediation-content" class="tab-content">
|
|
344
|
+
<div style="margin-bottom: 16px;">
|
|
345
|
+
<h3>Risk Assessment</h3>
|
|
346
|
+
<div style="background: #1a1a1a; padding: 12px; border-radius: 3px; margin-bottom: 16px;">
|
|
347
|
+
<div style="font-size: 24px; font-weight: bold; margin-bottom: 8px;">
|
|
348
|
+
Risk Score: <span id="risk-score" style="color: #ffb347;">—</span>/10
|
|
349
|
+
</div>
|
|
350
|
+
<div>
|
|
351
|
+
Risk Level: <span id="risk-level" style="color: #ffb347;">—</span>
|
|
352
|
+
</div>
|
|
353
|
+
<div style="margin-top: 12px; font-size: 12px; color: #888;">
|
|
354
|
+
<div id="risk-factors" style="margin-top: 8px;">Loading risk assessment...</div>
|
|
355
|
+
</div>
|
|
356
|
+
</div>
|
|
357
|
+
</div>
|
|
358
|
+
|
|
359
|
+
<div style="margin-bottom: 16px;">
|
|
360
|
+
<h3>Policy Compliance</h3>
|
|
361
|
+
<table id="policy-status-table">
|
|
362
|
+
<thead>
|
|
363
|
+
<tr>
|
|
364
|
+
<th>Policy</th>
|
|
365
|
+
<th>Category</th>
|
|
366
|
+
<th>Status</th>
|
|
367
|
+
<th>Violations</th>
|
|
368
|
+
</tr>
|
|
369
|
+
</thead>
|
|
370
|
+
<tbody id="policy-status-tbody">
|
|
371
|
+
<tr><td colspan="4" class="empty">No policy violations detected</td></tr>
|
|
372
|
+
</tbody>
|
|
373
|
+
</table>
|
|
374
|
+
</div>
|
|
375
|
+
|
|
376
|
+
<div style="margin-bottom: 16px;">
|
|
377
|
+
<h3>Remediation Recommendations</h3>
|
|
378
|
+
<div id="remediation-recommendations" style="background: #0a0a0a; padding: 12px; border-radius: 3px;">
|
|
379
|
+
<div style="margin-bottom: 12px;">
|
|
380
|
+
<strong>Primary Strategy:</strong>
|
|
381
|
+
<div id="primary-strategy" style="color: #4caf50; margin-top: 4px;">—</div>
|
|
382
|
+
</div>
|
|
383
|
+
<div style="margin-bottom: 12px;">
|
|
384
|
+
<strong>Alternative Strategies:</strong>
|
|
385
|
+
<div id="alternative-strategies" style="color: #888; font-size: 12px; margin-top: 4px;">—</div>
|
|
386
|
+
</div>
|
|
387
|
+
<div style="margin-bottom: 12px;">
|
|
388
|
+
<strong>Estimated Safety:</strong>
|
|
389
|
+
<div id="estimated-safety" style="color: #ffb347; margin-top: 4px;">—</div>
|
|
390
|
+
</div>
|
|
391
|
+
<div>
|
|
392
|
+
<strong>Required Approvals:</strong>
|
|
393
|
+
<div id="required-approvals" style="color: #888; font-size: 12px; margin-top: 4px;">—</div>
|
|
394
|
+
</div>
|
|
395
|
+
</div>
|
|
396
|
+
</div>
|
|
397
|
+
|
|
398
|
+
<div style="margin-bottom: 16px;">
|
|
399
|
+
<h3>Active Block Rules</h3>
|
|
400
|
+
<table id="block-rules-table">
|
|
401
|
+
<thead>
|
|
402
|
+
<tr>
|
|
403
|
+
<th>Rule</th>
|
|
404
|
+
<th>Condition</th>
|
|
405
|
+
<th>Action</th>
|
|
406
|
+
<th>Message</th>
|
|
407
|
+
</tr>
|
|
408
|
+
</thead>
|
|
409
|
+
<tbody id="block-rules-tbody">
|
|
410
|
+
<tr><td colspan="4" class="empty">No active block rules</td></tr>
|
|
411
|
+
</tbody>
|
|
412
|
+
</table>
|
|
413
|
+
</div>
|
|
414
|
+
|
|
415
|
+
<div style="margin-bottom: 16px;">
|
|
416
|
+
<button id="run-assessment-btn" style="padding: 8px 12px; background: #2a5a2a; border: 1px solid #4caf50; color: #4caf50; cursor: pointer; border-radius: 2px;">
|
|
417
|
+
Run Risk Assessment
|
|
418
|
+
</button>
|
|
419
|
+
<button id="override-block-btn" style="padding: 8px 12px; background: #5a2a2a; border: 1px solid #f44336; color: #f44336; cursor: pointer; border-radius: 2px; margin-left: 8px; display: none;">
|
|
420
|
+
Admin Override Block
|
|
421
|
+
</button>
|
|
422
|
+
</div>
|
|
423
|
+
</div>
|
|
172
424
|
|
|
173
425
|
<div id="config-section">
|
|
174
426
|
<button id="config-toggle">▶ Current Policy</button>
|
|
@@ -574,6 +826,616 @@
|
|
|
574
826
|
});
|
|
575
827
|
}
|
|
576
828
|
|
|
829
|
+
// Phase 1 & 2: Tab switching functionality
|
|
830
|
+
document.querySelectorAll('.tab-button').forEach(btn => {
|
|
831
|
+
btn.addEventListener('click', () => {
|
|
832
|
+
const tabName = btn.dataset.tab;
|
|
833
|
+
// Update active button
|
|
834
|
+
document.querySelectorAll('.tab-button').forEach(b => b.classList.remove('active'));
|
|
835
|
+
btn.classList.add('active');
|
|
836
|
+
// Update button styles
|
|
837
|
+
document.querySelectorAll('.tab-button').forEach(b => {
|
|
838
|
+
if (b.classList.contains('active')) {
|
|
839
|
+
b.style.color = '#5ab4ff';
|
|
840
|
+
b.style.borderBottomColor = '#5ab4ff';
|
|
841
|
+
b.style.fontWeight = 'bold';
|
|
842
|
+
} else {
|
|
843
|
+
b.style.color = '#888';
|
|
844
|
+
b.style.borderBottomColor = 'transparent';
|
|
845
|
+
b.style.fontWeight = 'normal';
|
|
846
|
+
}
|
|
847
|
+
});
|
|
848
|
+
// Show/hide tab content
|
|
849
|
+
document.querySelectorAll('.tab-content').forEach(content => content.classList.remove('active'));
|
|
850
|
+
document.getElementById(`tab-${tabName}-content`).classList.add('active');
|
|
851
|
+
// Trigger tab-specific load
|
|
852
|
+
if (tabName === 'sessions') {
|
|
853
|
+
loadSessions();
|
|
854
|
+
} else if (tabName === 'team') {
|
|
855
|
+
loadTeamMembers();
|
|
856
|
+
} else if (tabName === 'approvals') {
|
|
857
|
+
loadApprovals();
|
|
858
|
+
} else if (tabName === 'escalations') {
|
|
859
|
+
loadEscalations();
|
|
860
|
+
} else {
|
|
861
|
+
loadActions();
|
|
862
|
+
}
|
|
863
|
+
});
|
|
864
|
+
});
|
|
865
|
+
|
|
866
|
+
// Phase 1: Load sessions and render table
|
|
867
|
+
async function loadSessions() {
|
|
868
|
+
try {
|
|
869
|
+
const res = await fetch('/api/sessions');
|
|
870
|
+
const sessions = await res.json();
|
|
871
|
+
const tbody = document.getElementById('sessions-tbody');
|
|
872
|
+
|
|
873
|
+
if (sessions.length === 0) {
|
|
874
|
+
tbody.innerHTML = '<tr><td colspan="5" class="empty">No sessions yet</td></tr>';
|
|
875
|
+
return;
|
|
876
|
+
}
|
|
877
|
+
|
|
878
|
+
tbody.innerHTML = sessions.map(session => {
|
|
879
|
+
const sessionId = session.session_id || 'unknown';
|
|
880
|
+
const actionCount = session.action_count || 0;
|
|
881
|
+
const latest = session.latest ? timeAgo(session.latest) : 'never';
|
|
882
|
+
|
|
883
|
+
return `
|
|
884
|
+
<tr>
|
|
885
|
+
<td class="session-id" title="${sessionId}">${sessionId.substring(0, 20)}...</td>
|
|
886
|
+
<td>${actionCount}</td>
|
|
887
|
+
<td>${latest}</td>
|
|
888
|
+
<td><span class="session-status-active">active</span></td>
|
|
889
|
+
<td>
|
|
890
|
+
<button class="btn-view-session" onclick="viewSession('${sessionId}')">View</button>
|
|
891
|
+
<button class="btn-rollback-session" onclick="rollbackSession('${sessionId}')">Rollback</button>
|
|
892
|
+
</td>
|
|
893
|
+
</tr>
|
|
894
|
+
`;
|
|
895
|
+
}).join('');
|
|
896
|
+
} catch (err) {
|
|
897
|
+
document.getElementById('sessions-tbody').innerHTML = `<tr><td colspan="5" class="empty">Error loading sessions: ${err.message}</td></tr>`;
|
|
898
|
+
}
|
|
899
|
+
}
|
|
900
|
+
|
|
901
|
+
// Phase 1: View session details
|
|
902
|
+
async function viewSession(sessionId) {
|
|
903
|
+
try {
|
|
904
|
+
const res = await fetch(`/api/sessions/${sessionId}/actions`);
|
|
905
|
+
const data = await res.json();
|
|
906
|
+
const details = document.getElementById('session-details');
|
|
907
|
+
const content = document.getElementById('session-details-content');
|
|
908
|
+
|
|
909
|
+
const actions = data.actions || [];
|
|
910
|
+
const html = `
|
|
911
|
+
<div style="margin-bottom: 8px;">
|
|
912
|
+
<strong>Session:</strong> ${sessionId}<br>
|
|
913
|
+
<strong>Total Actions:</strong> ${actions.length}
|
|
914
|
+
</div>
|
|
915
|
+
<table style="width: 100%; border-collapse: collapse; font-size: 11px;">
|
|
916
|
+
<tr style="background: #1a1a1a;">
|
|
917
|
+
<th style="padding: 4px; text-align: left;">Time</th>
|
|
918
|
+
<th style="padding: 4px; text-align: left;">Tool</th>
|
|
919
|
+
<th style="padding: 4px; text-align: left;">Path/Command</th>
|
|
920
|
+
<th style="padding: 4px; text-align: left;">Status</th>
|
|
921
|
+
</tr>
|
|
922
|
+
${actions.map(a => `
|
|
923
|
+
<tr style="border-bottom: 1px solid #1e1e1e;">
|
|
924
|
+
<td style="padding: 4px;">${timeAgo(a.created_at)}</td>
|
|
925
|
+
<td style="padding: 4px;"><span class="badge badge-${a.tool_name}">${a.tool_name}</span></td>
|
|
926
|
+
<td style="padding: 4px; max-width: 200px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;">${a.target_path || '(bash)'}</td>
|
|
927
|
+
<td style="padding: 4px;"><span class="status-${a.status}">${a.status}</span></td>
|
|
928
|
+
</tr>
|
|
929
|
+
`).join('')}
|
|
930
|
+
</table>
|
|
931
|
+
`;
|
|
932
|
+
content.innerHTML = html;
|
|
933
|
+
details.style.display = 'block';
|
|
934
|
+
} catch (err) {
|
|
935
|
+
alert(`Error loading session: ${err.message}`);
|
|
936
|
+
}
|
|
937
|
+
}
|
|
938
|
+
|
|
939
|
+
// Phase 1: Rollback session
|
|
940
|
+
async function rollbackSession(sessionId) {
|
|
941
|
+
if (!confirm(`Rollback session ${sessionId}? This will undo all actions in the session.`)) {
|
|
942
|
+
return;
|
|
943
|
+
}
|
|
944
|
+
|
|
945
|
+
try {
|
|
946
|
+
const res = await fetch(`/api/sessions/${sessionId}/rollback`, { method: 'POST' });
|
|
947
|
+
const result = await res.json();
|
|
948
|
+
|
|
949
|
+
if (res.ok) {
|
|
950
|
+
alert(`✓ Session rolled back: ${result.actions_rolled_back} actions, ${result.files_restored} files restored`);
|
|
951
|
+
loadSessions();
|
|
952
|
+
} else {
|
|
953
|
+
alert(`✗ Rollback failed: ${result.error || 'Unknown error'}`);
|
|
954
|
+
}
|
|
955
|
+
} catch (err) {
|
|
956
|
+
alert(`✗ Error: ${err.message}`);
|
|
957
|
+
}
|
|
958
|
+
}
|
|
959
|
+
|
|
960
|
+
// Phase 2: Load team members
|
|
961
|
+
async function loadTeamMembers() {
|
|
962
|
+
try {
|
|
963
|
+
const res = await fetch('/api/team/members');
|
|
964
|
+
const members = await res.json();
|
|
965
|
+
const tbody = document.getElementById('team-tbody');
|
|
966
|
+
|
|
967
|
+
if (members.length === 0) {
|
|
968
|
+
tbody.innerHTML = '<tr><td colspan="5" class="empty">No team members added</td></tr>';
|
|
969
|
+
return;
|
|
970
|
+
}
|
|
971
|
+
|
|
972
|
+
tbody.innerHTML = members.map(member => {
|
|
973
|
+
const addedDate = member.added_at ? timeAgo(member.added_at) : 'unknown';
|
|
974
|
+
return `
|
|
975
|
+
<tr>
|
|
976
|
+
<td>${member.name}</td>
|
|
977
|
+
<td>${member.email}</td>
|
|
978
|
+
<td>${member.role || 'approver'}</td>
|
|
979
|
+
<td>${addedDate}</td>
|
|
980
|
+
<td>
|
|
981
|
+
<button class="btn-remove-member" onclick="removeTeamMember('${member.member_id}')">Remove</button>
|
|
982
|
+
</td>
|
|
983
|
+
</tr>
|
|
984
|
+
`;
|
|
985
|
+
}).join('');
|
|
986
|
+
} catch (err) {
|
|
987
|
+
document.getElementById('team-tbody').innerHTML = `<tr><td colspan="5" class="empty">Error loading team members: ${err.message}</td></tr>`;
|
|
988
|
+
}
|
|
989
|
+
}
|
|
990
|
+
|
|
991
|
+
// Phase 2: Remove team member
|
|
992
|
+
async function removeTeamMember(memberId) {
|
|
993
|
+
if (!confirm('Remove this team member?')) return;
|
|
994
|
+
|
|
995
|
+
try {
|
|
996
|
+
const res = await fetch(`/api/team/members/${memberId}`, { method: 'DELETE' });
|
|
997
|
+
const result = await res.json();
|
|
998
|
+
|
|
999
|
+
if (res.ok) {
|
|
1000
|
+
alert('✓ Team member removed');
|
|
1001
|
+
loadTeamMembers();
|
|
1002
|
+
} else {
|
|
1003
|
+
alert(`✗ Error: ${result.error}`);
|
|
1004
|
+
}
|
|
1005
|
+
} catch (err) {
|
|
1006
|
+
alert(`✗ Error: ${err.message}`);
|
|
1007
|
+
}
|
|
1008
|
+
}
|
|
1009
|
+
|
|
1010
|
+
// Phase 2: Add team member button
|
|
1011
|
+
document.addEventListener('DOMContentLoaded', () => {
|
|
1012
|
+
const addBtn = document.getElementById('add-team-member-btn');
|
|
1013
|
+
if (addBtn) {
|
|
1014
|
+
addBtn.addEventListener('click', async () => {
|
|
1015
|
+
const name = document.getElementById('new-member-name').value.trim();
|
|
1016
|
+
const email = document.getElementById('new-member-email').value.trim();
|
|
1017
|
+
|
|
1018
|
+
if (!name || !email) {
|
|
1019
|
+
alert('Please enter name and email');
|
|
1020
|
+
return;
|
|
1021
|
+
}
|
|
1022
|
+
|
|
1023
|
+
try {
|
|
1024
|
+
const res = await fetch('/api/team/members', {
|
|
1025
|
+
method: 'POST',
|
|
1026
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1027
|
+
body: JSON.stringify({
|
|
1028
|
+
member_id: `member-${Date.now()}`,
|
|
1029
|
+
name,
|
|
1030
|
+
email,
|
|
1031
|
+
}),
|
|
1032
|
+
});
|
|
1033
|
+
|
|
1034
|
+
const result = await res.json();
|
|
1035
|
+
if (res.ok) {
|
|
1036
|
+
document.getElementById('new-member-name').value = '';
|
|
1037
|
+
document.getElementById('new-member-email').value = '';
|
|
1038
|
+
loadTeamMembers();
|
|
1039
|
+
} else {
|
|
1040
|
+
alert(`✗ Error: ${result.error}`);
|
|
1041
|
+
}
|
|
1042
|
+
} catch (err) {
|
|
1043
|
+
alert(`✗ Error: ${err.message}`);
|
|
1044
|
+
}
|
|
1045
|
+
});
|
|
1046
|
+
}
|
|
1047
|
+
});
|
|
1048
|
+
|
|
1049
|
+
// Phase 2: Load approvals and routing rules
|
|
1050
|
+
async function loadApprovals() {
|
|
1051
|
+
try {
|
|
1052
|
+
// Load routing rules
|
|
1053
|
+
const routesRes = await fetch('/api/approval-routes');
|
|
1054
|
+
const routes = await routesRes.json();
|
|
1055
|
+
const routesTbody = document.getElementById('routes-tbody');
|
|
1056
|
+
|
|
1057
|
+
if (routes.length === 0) {
|
|
1058
|
+
routesTbody.innerHTML = '<tr><td colspan="5" class="empty">No approval routes configured</td></tr>';
|
|
1059
|
+
} else {
|
|
1060
|
+
routesTbody.innerHTML = routes.map(route => {
|
|
1061
|
+
const approvers = JSON.parse(route.approver_ids || '[]');
|
|
1062
|
+
const createdDate = route.created_at ? timeAgo(route.created_at) : 'unknown';
|
|
1063
|
+
return `
|
|
1064
|
+
<tr>
|
|
1065
|
+
<td>${route.name}</td>
|
|
1066
|
+
<td>${route.condition_type || 'all_sessions'}</td>
|
|
1067
|
+
<td>${approvers.join(', ')}</td>
|
|
1068
|
+
<td>${createdDate}</td>
|
|
1069
|
+
<td>
|
|
1070
|
+
<button class="btn-delete-route" onclick="deleteApprovalRoute('${route.route_id}')">Delete</button>
|
|
1071
|
+
</td>
|
|
1072
|
+
</tr>
|
|
1073
|
+
`;
|
|
1074
|
+
}).join('');
|
|
1075
|
+
}
|
|
1076
|
+
|
|
1077
|
+
// Load pending approvals
|
|
1078
|
+
const approvalsRes = await fetch('/api/approvals/pending');
|
|
1079
|
+
const approvals = await approvalsRes.json();
|
|
1080
|
+
const approvalsTbody = document.getElementById('approvals-queue-tbody');
|
|
1081
|
+
|
|
1082
|
+
if (approvals.length === 0) {
|
|
1083
|
+
approvalsTbody.innerHTML = '<tr><td colspan="6" class="empty">No pending approvals</td></tr>';
|
|
1084
|
+
} else {
|
|
1085
|
+
approvalsTbody.innerHTML = approvals.map(approval => {
|
|
1086
|
+
const triggeredDate = approval.triggered_at ? timeAgo(approval.triggered_at) : 'unknown';
|
|
1087
|
+
const approvers = JSON.parse(approval.approver_ids || '[]');
|
|
1088
|
+
const needed = approvers.length - approval.approved_count;
|
|
1089
|
+
return `
|
|
1090
|
+
<tr>
|
|
1091
|
+
<td>${approval.session_id.substring(0, 20)}...</td>
|
|
1092
|
+
<td>${approval.triggered_by}</td>
|
|
1093
|
+
<td>${approval.status}</td>
|
|
1094
|
+
<td>${needed} of ${approvers.length}</td>
|
|
1095
|
+
<td>${triggeredDate}</td>
|
|
1096
|
+
<td>
|
|
1097
|
+
<button class="btn-view-approval" onclick="viewApproval('${approval.request_id}')">View</button>
|
|
1098
|
+
</td>
|
|
1099
|
+
</tr>
|
|
1100
|
+
`;
|
|
1101
|
+
}).join('');
|
|
1102
|
+
}
|
|
1103
|
+
} catch (err) {
|
|
1104
|
+
document.getElementById('routes-tbody').innerHTML = `<tr><td colspan="5" class="empty">Error: ${err.message}</td></tr>`;
|
|
1105
|
+
document.getElementById('approvals-queue-tbody').innerHTML = `<tr><td colspan="6" class="empty">Error: ${err.message}</td></tr>`;
|
|
1106
|
+
}
|
|
1107
|
+
}
|
|
1108
|
+
|
|
1109
|
+
// Phase 2: Delete approval route
|
|
1110
|
+
async function deleteApprovalRoute(routeId) {
|
|
1111
|
+
if (!confirm('Delete this approval route?')) return;
|
|
1112
|
+
|
|
1113
|
+
try {
|
|
1114
|
+
const res = await fetch(`/api/approval-routes/${routeId}`, { method: 'DELETE' });
|
|
1115
|
+
const result = await res.json();
|
|
1116
|
+
|
|
1117
|
+
if (res.ok) {
|
|
1118
|
+
loadApprovals();
|
|
1119
|
+
} else {
|
|
1120
|
+
alert(`✗ Error: ${result.error}`);
|
|
1121
|
+
}
|
|
1122
|
+
} catch (err) {
|
|
1123
|
+
alert(`✗ Error: ${err.message}`);
|
|
1124
|
+
}
|
|
1125
|
+
}
|
|
1126
|
+
|
|
1127
|
+
// Phase 2: View approval details
|
|
1128
|
+
async function viewApproval(requestId) {
|
|
1129
|
+
try {
|
|
1130
|
+
const res = await fetch(`/api/approvals/${requestId}`);
|
|
1131
|
+
const data = await res.json();
|
|
1132
|
+
const approval = data.request;
|
|
1133
|
+
const status = data.status;
|
|
1134
|
+
|
|
1135
|
+
let details = `
|
|
1136
|
+
<div style="background: #0a0a0a; padding: 12px; border-radius: 3px; margin-top: 8px;">
|
|
1137
|
+
<h4>Approval Request Details</h4>
|
|
1138
|
+
<p><strong>Request ID:</strong> ${approval.request_id}</p>
|
|
1139
|
+
<p><strong>Session:</strong> ${approval.session_id}</p>
|
|
1140
|
+
<p><strong>Requester:</strong> ${approval.triggered_by}</p>
|
|
1141
|
+
<p><strong>Status:</strong> ${approval.status}</p>
|
|
1142
|
+
<p><strong>Approvals:</strong> ${approval.approved_count} approved, ${approval.rejected_count} rejected</p>
|
|
1143
|
+
`;
|
|
1144
|
+
|
|
1145
|
+
if (status) {
|
|
1146
|
+
details += `
|
|
1147
|
+
<h4 style="margin-top: 12px;">Approval Decisions</h4>
|
|
1148
|
+
<table style="width: 100%; font-size: 11px;">
|
|
1149
|
+
<tr style="background: #1a1a1a;">
|
|
1150
|
+
<th style="padding: 4px; text-align: left;">Approver</th>
|
|
1151
|
+
<th style="padding: 4px; text-align: left;">Decision</th>
|
|
1152
|
+
<th style="padding: 4px; text-align: left;">Reason</th>
|
|
1153
|
+
</tr>
|
|
1154
|
+
${status.decisions.map(d => `
|
|
1155
|
+
<tr style="border-bottom: 1px solid #1e1e1e;">
|
|
1156
|
+
<td style="padding: 4px;">${d.approver_id}</td>
|
|
1157
|
+
<td style="padding: 4px;"><span style="color: ${d.decision === 'approve' ? '#4caf50' : '#f44336'}">${d.decision}</span></td>
|
|
1158
|
+
<td style="padding: 4px;">${d.reason || '—'}</td>
|
|
1159
|
+
</tr>
|
|
1160
|
+
`).join('')}
|
|
1161
|
+
</table>
|
|
1162
|
+
`;
|
|
1163
|
+
}
|
|
1164
|
+
|
|
1165
|
+
details += '</div>';
|
|
1166
|
+
alert(details);
|
|
1167
|
+
} catch (err) {
|
|
1168
|
+
alert(`Error loading approval: ${err.message}`);
|
|
1169
|
+
}
|
|
1170
|
+
}
|
|
1171
|
+
|
|
1172
|
+
// Phase 3: Load escalation rules and pending escalations
|
|
1173
|
+
async function loadEscalations() {
|
|
1174
|
+
try {
|
|
1175
|
+
const [rulesRes, pendingRes] = await Promise.all([
|
|
1176
|
+
fetch('/api/escalations/rules'),
|
|
1177
|
+
fetch('/api/escalations/pending'),
|
|
1178
|
+
]);
|
|
1179
|
+
|
|
1180
|
+
const rulesData = await rulesRes.json();
|
|
1181
|
+
const pendingData = await pendingRes.json();
|
|
1182
|
+
|
|
1183
|
+
const rules = rulesData.rules || [];
|
|
1184
|
+
const pending = pendingData.escalations || [];
|
|
1185
|
+
|
|
1186
|
+
// Populate escalation rules table
|
|
1187
|
+
const rulesTbody = document.getElementById('escalation-rules-tbody');
|
|
1188
|
+
if (rules.length === 0) {
|
|
1189
|
+
rulesTbody.innerHTML = '<tr><td colspan="5" class="empty">No escalation rules configured</td></tr>';
|
|
1190
|
+
} else {
|
|
1191
|
+
rulesTbody.innerHTML = rules.map(rule => {
|
|
1192
|
+
const createdDate = rule.created_at ? timeAgo(rule.created_at) : 'unknown';
|
|
1193
|
+
const targets = rule.escalation_targets ? JSON.parse(rule.escalation_targets).join(', ') : '—';
|
|
1194
|
+
return `
|
|
1195
|
+
<tr>
|
|
1196
|
+
<td>${rule.name}</td>
|
|
1197
|
+
<td>${rule.timeout_hours}</td>
|
|
1198
|
+
<td>${targets}</td>
|
|
1199
|
+
<td>${createdDate}</td>
|
|
1200
|
+
<td>
|
|
1201
|
+
<button class="btn-delete-escalation" onclick="deleteEscalationRule('${rule.rule_id}')">Delete</button>
|
|
1202
|
+
</td>
|
|
1203
|
+
</tr>
|
|
1204
|
+
`;
|
|
1205
|
+
}).join('');
|
|
1206
|
+
}
|
|
1207
|
+
|
|
1208
|
+
// Populate pending escalations table
|
|
1209
|
+
const escalationsTbody = document.getElementById('escalation-queue-tbody');
|
|
1210
|
+
if (pending.length === 0) {
|
|
1211
|
+
escalationsTbody.innerHTML = '<tr><td colspan="5" class="empty">No pending escalations</td></tr>';
|
|
1212
|
+
} else {
|
|
1213
|
+
escalationsTbody.innerHTML = pending.map(escalation => {
|
|
1214
|
+
const deadlineDate = escalation.escalation_deadline ? timeAgo(escalation.escalation_deadline) : 'unknown';
|
|
1215
|
+
const status = escalation.status || 'pending';
|
|
1216
|
+
return `
|
|
1217
|
+
<tr>
|
|
1218
|
+
<td>${escalation.session_id.substring(0, 20)}...</td>
|
|
1219
|
+
<td>${escalation.request_id.substring(0, 20) || '—'}...</td>
|
|
1220
|
+
<td><span style="color: ${status === 'blocked' ? '#f44336' : status === 'proceeded' ? '#4caf50' : '#ffb347'}">${status}</span></td>
|
|
1221
|
+
<td>${deadlineDate}</td>
|
|
1222
|
+
<td>
|
|
1223
|
+
<button class="btn-view-escalation" onclick="viewEscalation('${escalation.request_id}')">View</button>
|
|
1224
|
+
</td>
|
|
1225
|
+
</tr>
|
|
1226
|
+
`;
|
|
1227
|
+
}).join('');
|
|
1228
|
+
}
|
|
1229
|
+
} catch (err) {
|
|
1230
|
+
document.getElementById('escalation-rules-tbody').innerHTML = `<tr><td colspan="5" class="empty">Error: ${err.message}</td></tr>`;
|
|
1231
|
+
document.getElementById('escalation-queue-tbody').innerHTML = `<tr><td colspan="5" class="empty">Error: ${err.message}</td></tr>`;
|
|
1232
|
+
}
|
|
1233
|
+
}
|
|
1234
|
+
|
|
1235
|
+
// Phase 3: Delete escalation rule
|
|
1236
|
+
async function deleteEscalationRule(ruleId) {
|
|
1237
|
+
if (!confirm('Delete this escalation rule?')) return;
|
|
1238
|
+
|
|
1239
|
+
try {
|
|
1240
|
+
const res = await fetch(`/api/escalations/rules/${ruleId}`, { method: 'DELETE' });
|
|
1241
|
+
const result = await res.json();
|
|
1242
|
+
|
|
1243
|
+
if (res.ok) {
|
|
1244
|
+
loadEscalations();
|
|
1245
|
+
} else {
|
|
1246
|
+
alert(`✗ Error: ${result.error}`);
|
|
1247
|
+
}
|
|
1248
|
+
} catch (err) {
|
|
1249
|
+
alert(`✗ Error: ${err.message}`);
|
|
1250
|
+
}
|
|
1251
|
+
}
|
|
1252
|
+
|
|
1253
|
+
// Phase 3: View escalation details
|
|
1254
|
+
async function viewEscalation(requestId) {
|
|
1255
|
+
try {
|
|
1256
|
+
const res = await fetch(`/api/escalations/${requestId}`);
|
|
1257
|
+
const data = await res.json();
|
|
1258
|
+
const escalation = data.escalation;
|
|
1259
|
+
const status = data.status;
|
|
1260
|
+
|
|
1261
|
+
let details = `
|
|
1262
|
+
<div style="background: #0a0a0a; padding: 12px; border-radius: 3px; margin-top: 8px;">
|
|
1263
|
+
<h4>Escalation Request Details</h4>
|
|
1264
|
+
<p><strong>Request ID:</strong> ${escalation.request_id}</p>
|
|
1265
|
+
<p><strong>Session:</strong> ${escalation.session_id}</p>
|
|
1266
|
+
<p><strong>Approval Request:</strong> ${escalation.approval_request_id}</p>
|
|
1267
|
+
<p><strong>Status:</strong> ${escalation.status}</p>
|
|
1268
|
+
<p><strong>Triggered:</strong> ${escalation.escalation_triggered_at || 'unknown'}</p>
|
|
1269
|
+
<p><strong>Deadline:</strong> ${escalation.escalation_deadline || 'unknown'}</p>
|
|
1270
|
+
`;
|
|
1271
|
+
|
|
1272
|
+
if (status && status.decisions) {
|
|
1273
|
+
details += `
|
|
1274
|
+
<h4 style="margin-top: 12px;">Escalation Decisions</h4>
|
|
1275
|
+
<table style="width: 100%; font-size: 11px;">
|
|
1276
|
+
<tr style="background: #1a1a1a;">
|
|
1277
|
+
<th style="padding: 4px; text-align: left;">Target</th>
|
|
1278
|
+
<th style="padding: 4px; text-align: left;">Decision</th>
|
|
1279
|
+
<th style="padding: 4px; text-align: left;">Reason</th>
|
|
1280
|
+
</tr>
|
|
1281
|
+
${status.decisions.map(d => `
|
|
1282
|
+
<tr style="border-bottom: 1px solid #1e1e1e;">
|
|
1283
|
+
<td style="padding: 4px;">${d.target_id}</td>
|
|
1284
|
+
<td style="padding: 4px;"><span style="color: ${d.decision === 'proceed' ? '#4caf50' : '#f44336'}">${d.decision}</span></td>
|
|
1285
|
+
<td style="padding: 4px;">${d.reason || '—'}</td>
|
|
1286
|
+
</tr>
|
|
1287
|
+
`).join('')}
|
|
1288
|
+
</table>
|
|
1289
|
+
`;
|
|
1290
|
+
}
|
|
1291
|
+
|
|
1292
|
+
// Add decision buttons if escalation is still pending
|
|
1293
|
+
if (escalation.status === 'pending') {
|
|
1294
|
+
details += `
|
|
1295
|
+
<div style="margin-top: 12px;">
|
|
1296
|
+
<button onclick="submitEscalationDecision('${requestId}', 'proceed')" style="padding: 6px 12px; background: #2d5a2d; border: 1px solid #4caf50; color: #4caf50; cursor: pointer; margin-right: 8px; border-radius: 2px;">✅ Proceed</button>
|
|
1297
|
+
<button onclick="submitEscalationDecision('${requestId}', 'block')" style="padding: 6px 12px; background: #5a2d2d; border: 1px solid #f44336; color: #f44336; cursor: pointer; border-radius: 2px;">❌ Block</button>
|
|
1298
|
+
</div>
|
|
1299
|
+
`;
|
|
1300
|
+
}
|
|
1301
|
+
|
|
1302
|
+
details += '</div>';
|
|
1303
|
+
alert(details);
|
|
1304
|
+
} catch (err) {
|
|
1305
|
+
alert(`Error loading escalation: ${err.message}`);
|
|
1306
|
+
}
|
|
1307
|
+
}
|
|
1308
|
+
|
|
1309
|
+
// Phase 3: Submit escalation decision (proceed or block)
|
|
1310
|
+
async function submitEscalationDecision(requestId, decision) {
|
|
1311
|
+
const reason = prompt(`Enter reason for ${decision}ing this escalation (optional):`);
|
|
1312
|
+
|
|
1313
|
+
try {
|
|
1314
|
+
const res = await fetch(`/api/escalations/${requestId}/decide`, {
|
|
1315
|
+
method: 'POST',
|
|
1316
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1317
|
+
body: JSON.stringify({ decision, reason: reason || undefined }),
|
|
1318
|
+
});
|
|
1319
|
+
|
|
1320
|
+
const result = await res.json();
|
|
1321
|
+
if (res.ok) {
|
|
1322
|
+
alert(`✓ Escalation ${decision === 'proceed' ? 'allowed' : 'blocked'} successfully`);
|
|
1323
|
+
loadEscalations();
|
|
1324
|
+
} else {
|
|
1325
|
+
alert(`✗ Error: ${result.error}`);
|
|
1326
|
+
}
|
|
1327
|
+
} catch (err) {
|
|
1328
|
+
alert(`✗ Error: ${err.message}`);
|
|
1329
|
+
}
|
|
1330
|
+
}
|
|
1331
|
+
|
|
1332
|
+
// Phase 3: Create escalation rule
|
|
1333
|
+
async function createEscalationRule() {
|
|
1334
|
+
const name = document.getElementById('new-escalation-name').value;
|
|
1335
|
+
const timeout = parseInt(document.getElementById('new-escalation-timeout').value);
|
|
1336
|
+
|
|
1337
|
+
if (!name || !timeout) {
|
|
1338
|
+
alert('Please enter rule name and timeout (hours)');
|
|
1339
|
+
return;
|
|
1340
|
+
}
|
|
1341
|
+
|
|
1342
|
+
try {
|
|
1343
|
+
const res = await fetch('/api/escalations/rules', {
|
|
1344
|
+
method: 'POST',
|
|
1345
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1346
|
+
body: JSON.stringify({ name, timeout_hours: timeout }),
|
|
1347
|
+
});
|
|
1348
|
+
|
|
1349
|
+
const result = await res.json();
|
|
1350
|
+
if (res.ok) {
|
|
1351
|
+
document.getElementById('new-escalation-name').value = '';
|
|
1352
|
+
document.getElementById('new-escalation-timeout').value = '';
|
|
1353
|
+
loadEscalations();
|
|
1354
|
+
} else {
|
|
1355
|
+
alert(`✗ Error: ${result.error}`);
|
|
1356
|
+
}
|
|
1357
|
+
} catch (err) {
|
|
1358
|
+
alert(`✗ Error: ${err.message}`);
|
|
1359
|
+
}
|
|
1360
|
+
}
|
|
1361
|
+
|
|
1362
|
+
// Phase 3: Event listener for create escalation rule button
|
|
1363
|
+
document.getElementById('create-escalation-rule-btn').addEventListener('click', createEscalationRule);
|
|
1364
|
+
|
|
1365
|
+
// Phase 4: Load remediation assessment
|
|
1366
|
+
async function loadRemediation() {
|
|
1367
|
+
try {
|
|
1368
|
+
// Placeholder: In full implementation, would analyze current session
|
|
1369
|
+
document.getElementById('risk-score').textContent = '5.0';
|
|
1370
|
+
document.getElementById('risk-level').textContent = 'MEDIUM';
|
|
1371
|
+
document.getElementById('risk-factors').innerHTML = `
|
|
1372
|
+
<div style="margin-bottom: 8px;">
|
|
1373
|
+
<strong>Risk Factors:</strong>
|
|
1374
|
+
<ul style="margin: 4px 0; padding-left: 16px; font-size: 11px;">
|
|
1375
|
+
<li>Operation Type: Medium (2.0/3.0)</li>
|
|
1376
|
+
<li>Scale: Medium (1.5/3.0)</li>
|
|
1377
|
+
<li>Error Pattern: Low (0.5/3.0)</li>
|
|
1378
|
+
<li>Time: Recent (0.5/3.0)</li>
|
|
1379
|
+
<li>System Load: Moderate (1.0/3.0)</li>
|
|
1380
|
+
</ul>
|
|
1381
|
+
</div>
|
|
1382
|
+
`;
|
|
1383
|
+
|
|
1384
|
+
// Policy compliance
|
|
1385
|
+
document.getElementById('policy-status-tbody').innerHTML = `
|
|
1386
|
+
<tr>
|
|
1387
|
+
<td>Data Protection</td>
|
|
1388
|
+
<td>data</td>
|
|
1389
|
+
<td><span style="color: #4caf50;">✓ compliant</span></td>
|
|
1390
|
+
<td>0</td>
|
|
1391
|
+
</tr>
|
|
1392
|
+
<tr>
|
|
1393
|
+
<td>Security Policy</td>
|
|
1394
|
+
<td>security</td>
|
|
1395
|
+
<td><span style="color: #4caf50;">✓ compliant</span></td>
|
|
1396
|
+
<td>0</td>
|
|
1397
|
+
</tr>
|
|
1398
|
+
`;
|
|
1399
|
+
|
|
1400
|
+
// Recommendations
|
|
1401
|
+
document.getElementById('primary-strategy').textContent = 'Partial Rollback (safe operations only)';
|
|
1402
|
+
document.getElementById('alternative-strategies').innerHTML = `
|
|
1403
|
+
<div>• Staged Rollback (phases with verification)</div>
|
|
1404
|
+
<div>• Escalation (manual expert review)</div>
|
|
1405
|
+
`;
|
|
1406
|
+
document.getElementById('estimated-safety').textContent = '75% - Good safety profile';
|
|
1407
|
+
document.getElementById('required-approvals').textContent = 'Engineering Lead';
|
|
1408
|
+
|
|
1409
|
+
// Block rules
|
|
1410
|
+
document.getElementById('block-rules-tbody').innerHTML = `
|
|
1411
|
+
<tr>
|
|
1412
|
+
<td>Production Delete Hours</td>
|
|
1413
|
+
<td>tool_name = delete_file</td>
|
|
1414
|
+
<td>BLOCK</td>
|
|
1415
|
+
<td>Cannot delete files during off-hours</td>
|
|
1416
|
+
</tr>
|
|
1417
|
+
<tr>
|
|
1418
|
+
<td>Large Batch Delete</td>
|
|
1419
|
+
<td>action_count > 20</td>
|
|
1420
|
+
<td>REQUIRE_APPROVAL</td>
|
|
1421
|
+
<td>Large batch deletion requires approval</td>
|
|
1422
|
+
</tr>
|
|
1423
|
+
`;
|
|
1424
|
+
} catch (err) {
|
|
1425
|
+
document.getElementById('risk-score').textContent = 'Error';
|
|
1426
|
+
alert(`Error loading remediation assessment: ${err.message}`);
|
|
1427
|
+
}
|
|
1428
|
+
}
|
|
1429
|
+
|
|
1430
|
+
// Event listener for Run Assessment button
|
|
1431
|
+
document.getElementById('run-assessment-btn').addEventListener('click', async () => {
|
|
1432
|
+
document.getElementById('run-assessment-btn').disabled = true;
|
|
1433
|
+
document.getElementById('run-assessment-btn').textContent = 'Assessing...';
|
|
1434
|
+
await loadRemediation();
|
|
1435
|
+
document.getElementById('run-assessment-btn').disabled = false;
|
|
1436
|
+
document.getElementById('run-assessment-btn').textContent = 'Run Risk Assessment';
|
|
1437
|
+
});
|
|
1438
|
+
|
|
577
1439
|
// Hub toggle button
|
|
578
1440
|
document.getElementById('hub-toggle').addEventListener('click', () => {
|
|
579
1441
|
const nav = document.getElementById('hub-nav');
|