@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/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');