@pingagent/sdk 0.1.13 → 0.1.15
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/bin/pingagent.js +312 -10
- package/dist/chunk-BCYHGKQE.js +4825 -0
- package/dist/chunk-MFKDD5X3.js +4235 -0
- package/dist/chunk-SAI2R63F.js +3923 -0
- package/dist/chunk-TWKCLIYT.js +4007 -0
- package/dist/index.d.ts +290 -18
- package/dist/index.js +39 -3
- package/dist/web-server.js +695 -6
- package/package.json +2 -2
package/dist/web-server.js
CHANGED
|
@@ -1,29 +1,39 @@
|
|
|
1
1
|
import {
|
|
2
|
+
CollaborationEventManager,
|
|
3
|
+
CollaborationProjectionOutboxManager,
|
|
2
4
|
ContactManager,
|
|
3
5
|
LocalStore,
|
|
6
|
+
OperatorSeenStateManager,
|
|
4
7
|
PingAgentClient,
|
|
5
8
|
TrustPolicyAuditManager,
|
|
6
9
|
TrustRecommendationManager,
|
|
10
|
+
buildDeliveryTimeline,
|
|
11
|
+
buildProjectionPreview,
|
|
7
12
|
decideContactPolicy,
|
|
8
13
|
decideTaskPolicy,
|
|
9
14
|
defaultTrustPolicyDoc,
|
|
15
|
+
deriveTransportHealth,
|
|
10
16
|
ensureTokenValid,
|
|
11
17
|
getActiveSessionFilePath,
|
|
12
18
|
getSessionBindingAlertsFilePath,
|
|
13
19
|
getSessionMapFilePath,
|
|
14
20
|
getTrustRecommendationActionLabel,
|
|
21
|
+
listPendingDecisionViews,
|
|
15
22
|
loadIdentity,
|
|
16
23
|
normalizeTrustPolicyDoc,
|
|
17
24
|
readCurrentActiveSessionKey,
|
|
18
25
|
readIngressRuntimeStatus,
|
|
19
26
|
readSessionBindingAlerts,
|
|
20
27
|
readSessionBindings,
|
|
28
|
+
readTransportPreference,
|
|
21
29
|
removeSessionBinding,
|
|
22
30
|
setSessionBinding,
|
|
31
|
+
summarizeSinceLastSeen,
|
|
23
32
|
summarizeTrustPolicyAudit,
|
|
33
|
+
switchTransportPreference,
|
|
24
34
|
updateStoredToken,
|
|
25
35
|
upsertTrustPolicyRecommendation
|
|
26
|
-
} from "./chunk-
|
|
36
|
+
} from "./chunk-BCYHGKQE.js";
|
|
27
37
|
|
|
28
38
|
// src/web-server.ts
|
|
29
39
|
import * as fs from "fs";
|
|
@@ -182,6 +192,7 @@ function getHostPanelHtml() {
|
|
|
182
192
|
<div class="profile-list" id="profileList"></div>
|
|
183
193
|
<div class="nav">
|
|
184
194
|
<button id="navRuntime" class="active">Runtime</button>
|
|
195
|
+
<button id="navDecisions">Decisions</button>
|
|
185
196
|
<button id="navPolicy">Policy</button>
|
|
186
197
|
</div>
|
|
187
198
|
<div class="link-row">
|
|
@@ -207,6 +218,10 @@ function getHostPanelHtml() {
|
|
|
207
218
|
|
|
208
219
|
<section id="runtimePanel" class="panel active">
|
|
209
220
|
<div class="card" id="activationCard" style="margin-bottom:16px"></div>
|
|
221
|
+
<div class="grid two-col" style="margin-bottom:16px">
|
|
222
|
+
<div class="card" id="transportHealthCard"></div>
|
|
223
|
+
<div class="card" id="sinceLastSeenCard"></div>
|
|
224
|
+
</div>
|
|
210
225
|
<div class="grid stats" id="statsGrid"></div>
|
|
211
226
|
<div class="grid runtime-layout" style="margin-top:16px">
|
|
212
227
|
<div class="card">
|
|
@@ -236,8 +251,40 @@ function getHostPanelHtml() {
|
|
|
236
251
|
</div>
|
|
237
252
|
</section>
|
|
238
253
|
|
|
254
|
+
<section id="decisionsPanel" class="panel">
|
|
255
|
+
<div class="grid two-col">
|
|
256
|
+
<div class="card">
|
|
257
|
+
<h2>Decision Inbox</h2>
|
|
258
|
+
<div id="decisionInboxSummary" class="muted small" style="margin-bottom:12px">Loading pending decisions\u2026</div>
|
|
259
|
+
<div class="audit-list" id="decisionInboxList"></div>
|
|
260
|
+
</div>
|
|
261
|
+
<div class="card">
|
|
262
|
+
<h2>Projection Delivery</h2>
|
|
263
|
+
<div id="projectionOutboxSummary" class="muted small" style="margin-bottom:12px">Loading projection delivery state\u2026</div>
|
|
264
|
+
<div class="audit-list" id="projectionOutboxList"></div>
|
|
265
|
+
</div>
|
|
266
|
+
</div>
|
|
267
|
+
</section>
|
|
268
|
+
|
|
239
269
|
<section id="policyPanel" class="panel">
|
|
240
270
|
<div class="grid policy-grid">
|
|
271
|
+
<div class="card">
|
|
272
|
+
<h2>Projection Policy</h2>
|
|
273
|
+
<div class="form-grid">
|
|
274
|
+
<label class="label">Preset</label>
|
|
275
|
+
<select id="projectionPreset">
|
|
276
|
+
<option value="quiet">quiet</option>
|
|
277
|
+
<option value="balanced">balanced</option>
|
|
278
|
+
<option value="strict">strict</option>
|
|
279
|
+
</select>
|
|
280
|
+
<div class="muted small">Balanced is the default: key conclusions, handoffs, repairs, and required decisions are pushed to the human thread; ordinary progress stays summary-first.</div>
|
|
281
|
+
<div class="row-actions">
|
|
282
|
+
<button class="action-btn" id="saveProjectionPresetBtn">Save projection preset</button>
|
|
283
|
+
</div>
|
|
284
|
+
<div id="projectionPolicyPreview" class="muted small" style="margin-top:12px"></div>
|
|
285
|
+
</div>
|
|
286
|
+
</div>
|
|
287
|
+
|
|
241
288
|
<div class="card">
|
|
242
289
|
<h2>Policy Defaults</h2>
|
|
243
290
|
<div class="form-grid">
|
|
@@ -357,7 +404,7 @@ function getHostPanelHtml() {
|
|
|
357
404
|
return {
|
|
358
405
|
profile: profile && profile.trim() ? profile.trim() : null,
|
|
359
406
|
sessionKey: sessionKey && sessionKey.trim() ? sessionKey.trim() : null,
|
|
360
|
-
view: view === 'policy' ? 'policy' : 'runtime',
|
|
407
|
+
view: view === 'policy' ? 'policy' : (view === 'decisions' ? 'decisions' : 'runtime'),
|
|
361
408
|
};
|
|
362
409
|
})();
|
|
363
410
|
|
|
@@ -367,11 +414,16 @@ function getHostPanelHtml() {
|
|
|
367
414
|
profiles: [],
|
|
368
415
|
overview: null,
|
|
369
416
|
session: null,
|
|
417
|
+
decisions: null,
|
|
370
418
|
policy: null,
|
|
371
419
|
selectedSessionKey: initialQuery.sessionKey || null,
|
|
372
420
|
detailMode: sessionStorage.getItem('pingagent_host_panel_detail_mode') || 'basic',
|
|
373
421
|
showUnreadOnly: false,
|
|
374
422
|
};
|
|
423
|
+
const seenMarkers = {
|
|
424
|
+
globalTab: null,
|
|
425
|
+
sessionKey: null,
|
|
426
|
+
};
|
|
375
427
|
|
|
376
428
|
function esc(value) {
|
|
377
429
|
return String(value == null ? '' : value)
|
|
@@ -394,13 +446,137 @@ function getHostPanelHtml() {
|
|
|
394
446
|
try { return new Date(value).toLocaleString(); } catch { return String(value); }
|
|
395
447
|
}
|
|
396
448
|
|
|
449
|
+
function countSinceLastSeen(summary) {
|
|
450
|
+
if (!summary) return 0;
|
|
451
|
+
return Number(summary.new_external_messages || 0)
|
|
452
|
+
+ Number(summary.new_conclusions || 0)
|
|
453
|
+
+ Number(summary.new_handoffs || 0)
|
|
454
|
+
+ Number(summary.new_decisions || 0)
|
|
455
|
+
+ Number(summary.new_failures || 0)
|
|
456
|
+
+ Number(summary.new_repairs || 0)
|
|
457
|
+
+ Number(summary.new_projection_failures || 0);
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
function renderSinceLastSeenSummary(summary) {
|
|
461
|
+
if (!summary) return '<div class="empty">No seen-state yet.</div>';
|
|
462
|
+
const count = countSinceLastSeen(summary);
|
|
463
|
+
return '' +
|
|
464
|
+
'<div class="status-main">' +
|
|
465
|
+
'<h2>Since Last Seen</h2>' +
|
|
466
|
+
'<div class="muted small">Per operator view. Opening runtime, detail, or decisions resets the matching anchor instead of forcing every refresh to zero out the feed.</div>' +
|
|
467
|
+
'<div class="summary-pills">' +
|
|
468
|
+
'<span class="pill">new=' + esc(count) + '</span>' +
|
|
469
|
+
'<span class="pill">external=' + esc(summary.new_external_messages || 0) + '</span>' +
|
|
470
|
+
'<span class="pill">conclusions=' + esc(summary.new_conclusions || 0) + '</span>' +
|
|
471
|
+
'<span class="pill">handoffs=' + esc(summary.new_handoffs || 0) + '</span>' +
|
|
472
|
+
'<span class="pill">decisions=' + esc(summary.new_decisions || 0) + '</span>' +
|
|
473
|
+
'<span class="pill">failures=' + esc(summary.new_failures || 0) + '</span>' +
|
|
474
|
+
'<span class="pill">repairs=' + esc(summary.new_repairs || 0) + '</span>' +
|
|
475
|
+
'<span class="pill">projection=' + esc(summary.new_projection_failures || 0) + '</span>' +
|
|
476
|
+
'</div>' +
|
|
477
|
+
'<div class="muted small" style="margin-top:8px">latest=' + esc(fmtTs(summary.latest_ts)) + '</div>' +
|
|
478
|
+
'</div>';
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
function renderTransportHealthCard(transportHealth) {
|
|
482
|
+
const health = transportHealth || {};
|
|
483
|
+
const stateLabel = health.state || 'Ready';
|
|
484
|
+
const stateClass = stateLabel === 'Degraded' || stateLabel === 'Switching Recommended' ? 'degraded' : 'ready';
|
|
485
|
+
const current = health.transport_mode || 'bridge';
|
|
486
|
+
const preferred = health.preferred_transport_mode || current;
|
|
487
|
+
return '' +
|
|
488
|
+
'<div class="status-strip">' +
|
|
489
|
+
'<div class="status-main">' +
|
|
490
|
+
'<h2>Transport Health</h2>' +
|
|
491
|
+
'<div class="status-state ' + esc(stateClass) + '">' + esc(stateLabel) + '</div>' +
|
|
492
|
+
'<div class="summary-pills">' +
|
|
493
|
+
'<span class="pill">current=' + esc(current) + '</span>' +
|
|
494
|
+
'<span class="pill">preferred=' + esc(preferred) + '</span>' +
|
|
495
|
+
'<span class="pill">retry_queue=' + esc(health.retry_queue_length || 0) + '</span>' +
|
|
496
|
+
'<span class="pill">failures=' + esc(health.consecutive_failures || 0) + '</span>' +
|
|
497
|
+
'</div>' +
|
|
498
|
+
'<div class="muted small" style="margin-top:8px">last inbound=' + esc(fmtTs(health.last_inbound_at)) + ' \xB7 last outbound=' + esc(fmtTs(health.last_outbound_at)) + '</div>' +
|
|
499
|
+
'<div class="muted small">last degraded=' + esc(fmtTs(health.last_degraded_at)) + ' \xB7 last repaired=' + esc(fmtTs(health.last_repaired_at)) + '</div>' +
|
|
500
|
+
(health.last_error ? '<div style="margin-top:8px;color:#fecaca">' + esc(health.last_error) + '</div>' : '<div class="muted small" style="margin-top:8px">No recent transport errors.</div>') +
|
|
501
|
+
'</div>' +
|
|
502
|
+
'<div style="min-width:240px">' +
|
|
503
|
+
'<div class="label">Actions</div>' +
|
|
504
|
+
'<div class="row-actions" style="margin-top:8px">' +
|
|
505
|
+
'<button class="secondary-btn transport-switch-btn" data-mode="bridge" style="width:auto">Switch to bridge</button>' +
|
|
506
|
+
'<button class="secondary-btn transport-switch-btn" data-mode="channel" style="width:auto">Switch to channel</button>' +
|
|
507
|
+
'</div>' +
|
|
508
|
+
(health.state === 'Switching Recommended'
|
|
509
|
+
? '<div class="muted small" style="margin-top:10px">Assist Then Switch is recommending a move back to bridge. The preference file already captures the desired fallback path.</div>'
|
|
510
|
+
: '<div class="muted small" style="margin-top:10px">Managed restart keeps the transport layer swappable without changing the human-facing model.</div>') +
|
|
511
|
+
'</div>' +
|
|
512
|
+
'</div>';
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
function renderProjectionPreviewBlock(preview) {
|
|
516
|
+
if (!preview) return '<div class="empty">No projection preview available for the selected session yet.</div>';
|
|
517
|
+
const detailOnly = Array.isArray(preview.description && preview.description.detail_only) ? preview.description.detail_only : [];
|
|
518
|
+
const immediate = Array.isArray(preview.description && preview.description.immediate) ? preview.description.immediate : [];
|
|
519
|
+
return '' +
|
|
520
|
+
'<div class="muted small">Preset <strong>' + esc(preview.preset) + '</strong> projects <code>' + esc(immediate.join(', ') || 'none') + '</code> immediately and keeps <code>' + esc(detailOnly.join(', ') || 'none') + '</code> in detail-only mode.</div>' +
|
|
521
|
+
'<div class="audit-list" style="margin-top:10px">' +
|
|
522
|
+
(Array.isArray(preview.recent) && preview.recent.length
|
|
523
|
+
? preview.recent.map(function (entry) {
|
|
524
|
+
return '<div class="audit-row"><div class="top"><strong>' + esc(entry.event_type) + '</strong><span class="badge">' + esc(entry.disposition) + '</span></div>' +
|
|
525
|
+
'<div class="muted small">' + esc(entry.reason || '(no reason)') + '</div>' +
|
|
526
|
+
'</div>';
|
|
527
|
+
}).join('')
|
|
528
|
+
: '<div class="empty">No recent collaboration events to preview.</div>') +
|
|
529
|
+
'</div>';
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
function renderDeliveryTimeline(timeline) {
|
|
533
|
+
if (!Array.isArray(timeline) || !timeline.length) {
|
|
534
|
+
return '<div class="empty">No delivery timeline yet.</div>';
|
|
535
|
+
}
|
|
536
|
+
return timeline.map(function (entry) {
|
|
537
|
+
return '<div class="audit-row"><div class="top"><strong>' + esc(entry.kind || 'event') + '</strong><span class="badge">' + esc(fmtTs(entry.ts_ms)) + '</span></div>' +
|
|
538
|
+
'<div style="margin-top:8px">' + esc(entry.summary || '(no summary)') + '</div>' +
|
|
539
|
+
'</div>';
|
|
540
|
+
}).join('');
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
async function markSeen(scopeType, scopeKey) {
|
|
544
|
+
return api('/api/runtime/seen', {
|
|
545
|
+
method: 'POST',
|
|
546
|
+
headers: { 'Content-Type': 'application/json' },
|
|
547
|
+
body: JSON.stringify({
|
|
548
|
+
operator_id: 'host_panel',
|
|
549
|
+
scope_type: scopeType,
|
|
550
|
+
scope_key: scopeType === 'session' ? scopeKey : null,
|
|
551
|
+
}),
|
|
552
|
+
});
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
async function maybeMarkGlobalSeen(tab, force) {
|
|
556
|
+
if (tab !== 'runtime' && tab !== 'decisions') return;
|
|
557
|
+
if (!force && seenMarkers.globalTab === tab) return;
|
|
558
|
+
try {
|
|
559
|
+
await markSeen('global', null);
|
|
560
|
+
seenMarkers.globalTab = tab;
|
|
561
|
+
} catch {}
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
async function maybeMarkSessionSeen(sessionKey, force) {
|
|
565
|
+
if (!sessionKey) return;
|
|
566
|
+
if (!force && seenMarkers.sessionKey === sessionKey) return;
|
|
567
|
+
try {
|
|
568
|
+
await markSeen('session', sessionKey);
|
|
569
|
+
seenMarkers.sessionKey = sessionKey;
|
|
570
|
+
} catch {}
|
|
571
|
+
}
|
|
572
|
+
|
|
397
573
|
function syncUrlState() {
|
|
398
574
|
const url = new URL(window.location.href);
|
|
399
575
|
if (state.selectedProfile) url.searchParams.set('profile', state.selectedProfile);
|
|
400
576
|
else url.searchParams.delete('profile');
|
|
401
577
|
if (state.selectedSessionKey) url.searchParams.set('session_key', state.selectedSessionKey);
|
|
402
578
|
else url.searchParams.delete('session_key');
|
|
403
|
-
url.searchParams.set('view', state.currentTab === 'policy' ? 'policy' : 'runtime');
|
|
579
|
+
url.searchParams.set('view', state.currentTab === 'policy' ? 'policy' : (state.currentTab === 'decisions' ? 'decisions' : 'runtime'));
|
|
404
580
|
history.replaceState(null, '', url.pathname + (url.search ? url.search : ''));
|
|
405
581
|
}
|
|
406
582
|
|
|
@@ -477,12 +653,24 @@ function getHostPanelHtml() {
|
|
|
477
653
|
}
|
|
478
654
|
|
|
479
655
|
function setTab(tab) {
|
|
656
|
+
const previous = state.currentTab;
|
|
480
657
|
state.currentTab = tab;
|
|
481
658
|
document.getElementById('navRuntime').classList.toggle('active', tab === 'runtime');
|
|
659
|
+
document.getElementById('navDecisions').classList.toggle('active', tab === 'decisions');
|
|
482
660
|
document.getElementById('navPolicy').classList.toggle('active', tab === 'policy');
|
|
483
661
|
document.getElementById('runtimePanel').classList.toggle('active', tab === 'runtime');
|
|
662
|
+
document.getElementById('decisionsPanel').classList.toggle('active', tab === 'decisions');
|
|
484
663
|
document.getElementById('policyPanel').classList.toggle('active', tab === 'policy');
|
|
664
|
+
if (previous !== tab && tab !== 'runtime') {
|
|
665
|
+
seenMarkers.sessionKey = null;
|
|
666
|
+
}
|
|
485
667
|
syncUrlState();
|
|
668
|
+
if (previous !== tab) {
|
|
669
|
+
void maybeMarkGlobalSeen(tab, true);
|
|
670
|
+
if (tab === 'runtime' && state.selectedSessionKey) {
|
|
671
|
+
void maybeMarkSessionSeen(state.selectedSessionKey, true);
|
|
672
|
+
}
|
|
673
|
+
}
|
|
486
674
|
}
|
|
487
675
|
|
|
488
676
|
function renderHeader() {
|
|
@@ -572,6 +760,112 @@ function getHostPanelHtml() {
|
|
|
572
760
|
'</div>';
|
|
573
761
|
}
|
|
574
762
|
|
|
763
|
+
function renderCollaborationEventsBlock(events, isAdvanced) {
|
|
764
|
+
if (!events || !events.length) {
|
|
765
|
+
return '<div class="empty">No collaboration events recorded yet. New external updates, runtime shifts, and decision points will appear here.</div>';
|
|
766
|
+
}
|
|
767
|
+
return events.map(function (event) {
|
|
768
|
+
const detail = event && event.detail ? event.detail : null;
|
|
769
|
+
const resolution = detail && detail.approval_resolution ? detail.approval_resolution : null;
|
|
770
|
+
const badges = [
|
|
771
|
+
'<span class="badge">' + esc(event.severity || 'info') + '</span>',
|
|
772
|
+
event.approval_required
|
|
773
|
+
? '<span class="badge">' + esc(event.approval_status === 'pending' ? 'review' : event.approval_status) + '</span>'
|
|
774
|
+
: '',
|
|
775
|
+
event.target_human_session ? '<span class="badge">human thread</span>' : '',
|
|
776
|
+
].filter(Boolean).join('');
|
|
777
|
+
const actions = event.approval_required && event.approval_status === 'pending'
|
|
778
|
+
? '<div class="row-actions" style="margin-top:10px">' +
|
|
779
|
+
'<button class="action-btn collaboration-decision-btn" data-event-id="' + esc(event.id) + '" data-decision="approved">Approve</button>' +
|
|
780
|
+
'<button class="danger-btn collaboration-decision-btn" data-event-id="' + esc(event.id) + '" data-decision="rejected">Reject</button>' +
|
|
781
|
+
'</div>'
|
|
782
|
+
: '';
|
|
783
|
+
const resolutionLine = resolution
|
|
784
|
+
? '<div class="muted small" style="margin-top:8px">resolution=' + esc(resolution.approval_status || event.approval_status || '(unknown)') +
|
|
785
|
+
' \xB7 resolved_at=' + esc(fmtTs(resolution.resolved_at)) +
|
|
786
|
+
(resolution.resolved_by ? ' \xB7 resolved_by=' + esc(resolution.resolved_by) : '') +
|
|
787
|
+
(resolution.note ? ' \xB7 note=' + esc(resolution.note) : '') +
|
|
788
|
+
'</div>'
|
|
789
|
+
: '';
|
|
790
|
+
return '<div class="audit-row"><div class="top"><strong>' + esc(event.event_type) + '</strong>' + badges + '</div>' +
|
|
791
|
+
'<div class="muted small">' + esc(fmtTs(event.ts_ms)) + '</div>' +
|
|
792
|
+
'<div style="margin-top:8px">' + esc(event.summary || '(no summary)') + '</div>' +
|
|
793
|
+
'<div class="muted small" style="margin-top:8px">detail_ref=' + esc(detail && detail.detail_ref && detail.detail_ref.session_key ? detail.detail_ref.session_key : (event.session_key || '(none)')) + ' \xB7 conversation=' + esc(event.conversation_id || '(none)') + '</div>' +
|
|
794
|
+
resolutionLine +
|
|
795
|
+
actions +
|
|
796
|
+
(isAdvanced && detail ? '<pre style="margin-top:8px">' + esc(JSON.stringify(detail, null, 2)) + '</pre>' : '') +
|
|
797
|
+
'</div>';
|
|
798
|
+
}).join('');
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
function renderDecisionInbox() {
|
|
802
|
+
const data = state.decisions;
|
|
803
|
+
const pendingDecisions = data && Array.isArray(data.pendingDecisions) ? data.pendingDecisions : [];
|
|
804
|
+
const overdueDecisions = pendingDecisions.filter(function (event) { return !!event.overdue; });
|
|
805
|
+
const pendingOutbox = data && Array.isArray(data.projectionOutboxPending) ? data.projectionOutboxPending : [];
|
|
806
|
+
const failedOutbox = data && Array.isArray(data.projectionOutboxFailed) ? data.projectionOutboxFailed : [];
|
|
807
|
+
document.getElementById('decisionInboxSummary').textContent =
|
|
808
|
+
'pending=' + pendingDecisions.length + ' \xB7 overdue=' + overdueDecisions.length + ' \xB7 projection_pending=' + pendingOutbox.length + ' \xB7 projection_failed=' + failedOutbox.length;
|
|
809
|
+
document.getElementById('decisionInboxList').innerHTML = pendingDecisions.length
|
|
810
|
+
? pendingDecisions.map(function (event) {
|
|
811
|
+
return '<div class="audit-row"><div class="top"><strong>' + esc(event.summary || '(no summary)') + '</strong>' +
|
|
812
|
+
'<div style="display:flex;gap:6px;flex-wrap:wrap;justify-content:flex-end"><span class="badge">' + esc(event.severity || 'warning') + '</span>' +
|
|
813
|
+
(event.overdue ? '<span class="badge alert">overdue</span>' : '') + '</div></div>' +
|
|
814
|
+
'<div class="muted small">' + esc(fmtTs(event.ts_ms)) + ' \xB7 session=' + esc(event.session_key || '(none)') + ' \xB7 target=' + esc(event.target_human_session || '(none)') +
|
|
815
|
+
(event.overdue ? ' \xB7 overdue_by=' + esc(Math.ceil((event.overdue_by_ms || 0) / 60000)) + 'm' : '') + '</div>' +
|
|
816
|
+
'<div class="muted small" style="margin-top:8px">detail_ref=' + esc(event.conversation_id || '(none)') + '</div>' +
|
|
817
|
+
'<div class="row-actions">' +
|
|
818
|
+
'<button class="action-btn inbox-decision-btn" data-event-id="' + esc(event.id) + '" data-decision="approved">Approve</button>' +
|
|
819
|
+
'<button class="danger-btn inbox-decision-btn" data-event-id="' + esc(event.id) + '" data-decision="rejected">Reject</button>' +
|
|
820
|
+
'<button class="secondary-btn inbox-open-detail-btn" data-session-key="' + esc(event.session_key || '') + '">Open Detail</button>' +
|
|
821
|
+
'</div>' +
|
|
822
|
+
'</div>';
|
|
823
|
+
}).join('')
|
|
824
|
+
: '<div class="empty">No pending collaboration decisions.</div>';
|
|
825
|
+
|
|
826
|
+
const combinedOutbox = pendingOutbox.concat(failedOutbox);
|
|
827
|
+
document.getElementById('projectionOutboxSummary').textContent =
|
|
828
|
+
combinedOutbox.length
|
|
829
|
+
? 'Human-thread projection uses a stable outbox. Failed rows stay visible until delivery recovers.'
|
|
830
|
+
: 'No undelivered human-thread projections.';
|
|
831
|
+
document.getElementById('projectionOutboxList').innerHTML = combinedOutbox.length
|
|
832
|
+
? combinedOutbox.map(function (entry) {
|
|
833
|
+
return '<div class="audit-row"><div class="top"><strong>' + esc(entry.summary || '(no summary)') + '</strong>' +
|
|
834
|
+
'<span class="badge">' + esc(entry.status || 'pending') + '</span></div>' +
|
|
835
|
+
'<div class="muted small">' + esc(fmtTs(entry.created_at)) + ' \xB7 kind=' + esc(entry.projection_kind || 'collaboration_update') + ' \xB7 attempts=' + esc(entry.attempt_count || 0) + '</div>' +
|
|
836
|
+
'<div class="muted small" style="margin-top:8px">target=' + esc(entry.target_human_session || '(missing)') + ' \xB7 session=' + esc(entry.session_key || '(none)') + '</div>' +
|
|
837
|
+
(entry.last_error ? '<div style="margin-top:8px">' + esc(entry.last_error) + '</div>' : '') +
|
|
838
|
+
'</div>';
|
|
839
|
+
}).join('')
|
|
840
|
+
: '<div class="empty">No pending or failed projection deliveries.</div>';
|
|
841
|
+
|
|
842
|
+
document.querySelectorAll('.inbox-decision-btn').forEach(function (btn) {
|
|
843
|
+
btn.addEventListener('click', async function () {
|
|
844
|
+
const eventId = Number(btn.getAttribute('data-event-id'));
|
|
845
|
+
const decision = btn.getAttribute('data-decision');
|
|
846
|
+
if (!Number.isInteger(eventId) || !decision) return;
|
|
847
|
+
await api('/api/runtime/collaboration-decisions/resolve', {
|
|
848
|
+
method: 'POST',
|
|
849
|
+
headers: { 'Content-Type': 'application/json' },
|
|
850
|
+
body: JSON.stringify({ event_id: eventId, decision: decision }),
|
|
851
|
+
});
|
|
852
|
+
await refreshAll();
|
|
853
|
+
setTab('decisions');
|
|
854
|
+
});
|
|
855
|
+
});
|
|
856
|
+
|
|
857
|
+
document.querySelectorAll('.inbox-open-detail-btn').forEach(function (btn) {
|
|
858
|
+
btn.addEventListener('click', async function () {
|
|
859
|
+
const sessionKey = btn.getAttribute('data-session-key');
|
|
860
|
+
if (!sessionKey) return;
|
|
861
|
+
state.selectedSessionKey = sessionKey;
|
|
862
|
+
await loadSession(sessionKey);
|
|
863
|
+
renderOverview();
|
|
864
|
+
setTab('runtime');
|
|
865
|
+
});
|
|
866
|
+
});
|
|
867
|
+
}
|
|
868
|
+
|
|
575
869
|
function getOverviewSessions() {
|
|
576
870
|
return state.overview && Array.isArray(state.overview.sessions) ? state.overview.sessions : [];
|
|
577
871
|
}
|
|
@@ -607,6 +901,11 @@ function getHostPanelHtml() {
|
|
|
607
901
|
if (!overview) return;
|
|
608
902
|
syncSelectedSessionFromOverview();
|
|
609
903
|
const ingressState = ingressStatusModel(overview);
|
|
904
|
+
const transportHealth = overview.transportHealth || null;
|
|
905
|
+
const sinceLastSeen = overview.sinceLastSeen || null;
|
|
906
|
+
const overdueDecisions = Array.isArray(overview.pendingDecisions)
|
|
907
|
+
? overview.pendingDecisions.filter(function (event) { return !!event.overdue; })
|
|
908
|
+
: [];
|
|
610
909
|
document.getElementById('activationCard').innerHTML =
|
|
611
910
|
'<div class="status-strip">' +
|
|
612
911
|
'<div class="status-main">' +
|
|
@@ -614,6 +913,11 @@ function getHostPanelHtml() {
|
|
|
614
913
|
'<div class="status-state ' + ingressState.className + '">' + esc(ingressState.label) + '</div>' +
|
|
615
914
|
'<div class="muted small">' + esc(ingressState.detail) + '</div>' +
|
|
616
915
|
'<div class="muted small">Public link: ' + esc(overview.publicSelf && overview.publicSelf.public_url ? overview.publicSelf.public_url : '(not ready yet)') + '</div>' +
|
|
916
|
+
'<div class="summary-pills">' +
|
|
917
|
+
'<span class="pill">pending_decisions=' + esc((overview.pendingDecisions || []).length) + '</span>' +
|
|
918
|
+
'<span class="pill">overdue=' + esc(overdueDecisions.length) + '</span>' +
|
|
919
|
+
'<span class="pill">projection=' + esc(overview.collaborationProjection && overview.collaborationProjection.preset ? overview.collaborationProjection.preset : 'balanced') + '</span>' +
|
|
920
|
+
'</div>' +
|
|
617
921
|
'</div>' +
|
|
618
922
|
'<div style="min-width:320px">' +
|
|
619
923
|
'<div class="label">Quick Start</div>' +
|
|
@@ -628,6 +932,8 @@ function getHostPanelHtml() {
|
|
|
628
932
|
'</div>' +
|
|
629
933
|
'</div>' +
|
|
630
934
|
'</div>';
|
|
935
|
+
document.getElementById('transportHealthCard').innerHTML = renderTransportHealthCard(transportHealth);
|
|
936
|
+
document.getElementById('sinceLastSeenCard').innerHTML = renderSinceLastSeenSummary(sinceLastSeen);
|
|
631
937
|
const subscription = overview.subscription || null;
|
|
632
938
|
const stats = [
|
|
633
939
|
{ label: 'Plan', value: subscription ? subscription.tier : 'ghost', sub: subscription ? subscription.summary : 'subscription unavailable' },
|
|
@@ -638,6 +944,7 @@ function getHostPanelHtml() {
|
|
|
638
944
|
{ label: 'Unread', value: overview.unreadTotal, sub: 'session-first inbox state' },
|
|
639
945
|
{ label: 'Tasks', value: overview.tasksTotal, sub: 'recent local task threads' },
|
|
640
946
|
{ label: 'Audit', value: overview.auditSummary.total_events, sub: 'policy / runtime audit events' },
|
|
947
|
+
{ label: 'Collaboration', value: overview.collaborationSummary ? overview.collaborationSummary.total_events : 0, sub: overview.collaborationSummary ? ('pending_review=' + overview.collaborationSummary.pending_approvals) : 'projected external collaboration events' },
|
|
641
948
|
{ label: 'Recommendations', value: overview.recommendationSummary ? overview.recommendationSummary.total : overview.recommendations.length, sub: overview.recommendationSummary ? JSON.stringify(overview.recommendationSummary.by_status || {}) : 'learned policy suggestions' },
|
|
642
949
|
{ label: 'Public Link', value: overview.publicSelf && overview.publicSelf.public_url ? 'ready' : 'disabled', sub: overview.publicSelf && overview.publicSelf.public_url ? overview.publicSelf.public_url : 'create a hosted shareable profile link' },
|
|
643
950
|
];
|
|
@@ -655,6 +962,9 @@ function getHostPanelHtml() {
|
|
|
655
962
|
const active = session.session_key === state.selectedSessionKey ? ' active' : '';
|
|
656
963
|
const badges = [
|
|
657
964
|
'<span class="badge ' + esc(session.trust_state) + '">' + esc(session.trust_state) + '</span>',
|
|
965
|
+
(countSinceLastSeen(session.since_last_seen) > 0
|
|
966
|
+
? '<span class="badge alert">new ' + esc(countSinceLastSeen(session.since_last_seen)) + '</span>'
|
|
967
|
+
: ''),
|
|
658
968
|
session.binding_alert
|
|
659
969
|
? '<button type="button" class="badge alert rebind-badge-btn" data-session="' + esc(session.session_key) + '" data-conversation="' + esc(session.conversation_id || '') + '" data-bound-session="' + esc(session.mapped_work_session || '') + '" data-remote-did="' + esc(session.remote_did || '') + '" title="OpenClaw chat link needs attention">Needs reconnect</button>'
|
|
660
970
|
: '',
|
|
@@ -692,6 +1002,25 @@ function getHostPanelHtml() {
|
|
|
692
1002
|
});
|
|
693
1003
|
}
|
|
694
1004
|
|
|
1005
|
+
document.querySelectorAll('.transport-switch-btn').forEach(function (btn) {
|
|
1006
|
+
btn.addEventListener('click', async function () {
|
|
1007
|
+
const mode = btn.getAttribute('data-mode');
|
|
1008
|
+
if (mode !== 'bridge' && mode !== 'channel') return;
|
|
1009
|
+
const confirmed = window.confirm('Switch preferred transport to ' + mode + ' and request a managed restart?');
|
|
1010
|
+
if (!confirmed) return;
|
|
1011
|
+
const result = await api('/api/runtime/transport-switch', {
|
|
1012
|
+
method: 'POST',
|
|
1013
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1014
|
+
body: JSON.stringify({ mode: mode }),
|
|
1015
|
+
});
|
|
1016
|
+
window.alert(result && result.result && result.result.restarted
|
|
1017
|
+
? ('Preferred transport switched to ' + mode + '. Managed restart was attempted.')
|
|
1018
|
+
: ('Preferred transport switched to ' + mode + '. Restart is still required.'));
|
|
1019
|
+
await refreshAll();
|
|
1020
|
+
setTab('runtime');
|
|
1021
|
+
});
|
|
1022
|
+
});
|
|
1023
|
+
|
|
695
1024
|
const tasks = Array.isArray(overview.tasks) ? overview.tasks : [];
|
|
696
1025
|
document.getElementById('taskList').innerHTML = tasks.length
|
|
697
1026
|
? tasks.map(function (task) {
|
|
@@ -783,6 +1112,8 @@ conversation=' + result.conversation_id));
|
|
|
783
1112
|
const tasks = Array.isArray(detail.tasks) ? detail.tasks : [];
|
|
784
1113
|
const messages = Array.isArray(detail.messages) ? detail.messages : [];
|
|
785
1114
|
const auditEvents = Array.isArray(detail.auditEvents) ? detail.auditEvents : [];
|
|
1115
|
+
const collaborationEvents = Array.isArray(detail.collaborationEvents) ? detail.collaborationEvents : [];
|
|
1116
|
+
const pendingCollaborationEvents = Array.isArray(detail.pendingCollaborationEvents) ? detail.pendingCollaborationEvents : [];
|
|
786
1117
|
const recommendations = Array.isArray(detail.recommendations) ? detail.recommendations : [];
|
|
787
1118
|
const openRecommendation = recommendations.find(function (item) { return item.status === 'open'; }) || null;
|
|
788
1119
|
const reopenRecommendation = recommendations.find(function (item) { return item.status === 'dismissed' || item.status === 'superseded'; }) || null;
|
|
@@ -790,6 +1121,14 @@ conversation=' + result.conversation_id));
|
|
|
790
1121
|
const bindingAlert = detail.bindingAlert || null;
|
|
791
1122
|
const activeWorkSession = detail.activeWorkSession || null;
|
|
792
1123
|
const summary = detail.sessionSummary || null;
|
|
1124
|
+
const projectionPreset = detail.collaborationProjection && detail.collaborationProjection.preset
|
|
1125
|
+
? detail.collaborationProjection.preset
|
|
1126
|
+
: 'balanced';
|
|
1127
|
+
const projectionOutbox = Array.isArray(detail.projectionOutbox) ? detail.projectionOutbox : [];
|
|
1128
|
+
const sinceLastSeen = detail.sinceLastSeen || null;
|
|
1129
|
+
const deliveryTimeline = Array.isArray(detail.deliveryTimeline) ? detail.deliveryTimeline : [];
|
|
1130
|
+
const projectionPreview = detail.projectionPreview || null;
|
|
1131
|
+
const transportHealth = detail.transportHealth || null;
|
|
793
1132
|
const isAdvanced = state.detailMode === 'advanced';
|
|
794
1133
|
const sessionLink = buildSessionLink(session.session_key);
|
|
795
1134
|
const summaryPills = [];
|
|
@@ -824,6 +1163,10 @@ conversation=' + result.conversation_id));
|
|
|
824
1163
|
? '<div class="muted small" style="margin-top:8px">trust_action=' + esc(recommendationActionLabel(openRecommendation)) + '</div>'
|
|
825
1164
|
: (reopenRecommendation ? '<div class="muted small" style="margin-top:8px">trust_action=' + esc(recommendationActionLabel(reopenRecommendation)) + '</div>' : '')) +
|
|
826
1165
|
(summaryPills.length ? '<div class="summary-pills">' + summaryPills.join('') + '</div>' : '') +
|
|
1166
|
+
(pendingCollaborationEvents.length
|
|
1167
|
+
? '<div style="margin-top:10px;padding:10px 12px;border:1px solid #f59e0b;border-radius:10px;background:rgba(120,53,15,0.25);color:#fde68a"><strong>Decision pending</strong><div class="small" style="margin-top:6px">A collaboration update needs review before the human thread can be treated as fully current. Resolve it in the Collaboration Feed below.' +
|
|
1168
|
+
(pendingCollaborationEvents[0].overdue ? ' This item is overdue.' : '') + '</div></div>'
|
|
1169
|
+
: '') +
|
|
827
1170
|
(bindingAlert
|
|
828
1171
|
? '<div style="margin-top:10px;padding:10px 12px;border:1px solid #ef4444;border-radius:10px;background:rgba(127,29,29,0.25);color:#fecaca"><strong>Needs reconnect</strong><div class="small" style="margin-top:6px">' + esc(isAdvanced ? (bindingAlert.message || 'Bound work session is missing. Rebind this PingAgent conversation to the current chat session.') : 'OpenClaw chat link is stale. Attach this PingAgent session to the current OpenClaw chat.') + '</div></div>'
|
|
829
1172
|
: '') +
|
|
@@ -919,6 +1262,11 @@ conversation=' + result.conversation_id));
|
|
|
919
1262
|
return '<div class="message-row"><div class="muted small">' + esc(fmtTs(msg.ts_ms)) + ' \xB7 ' + esc(msg.direction) + ' \xB7 ' + esc(msg.schema) + '</div><div style="margin-top:8px">' + esc(messageSummary) + '</div></div>';
|
|
920
1263
|
}).join('') : '<div class="empty">No local message history yet.</div>') +
|
|
921
1264
|
'</div></div>' +
|
|
1265
|
+
'<div><div class="label">Collaboration Feed</div><div class="audit-list" style="margin-top:8px">' +
|
|
1266
|
+
renderCollaborationEventsBlock(collaborationEvents, isAdvanced) +
|
|
1267
|
+
'</div></div>' +
|
|
1268
|
+
'</div>' +
|
|
1269
|
+
'<div class="grid two-col" style="margin-top:16px">' +
|
|
922
1270
|
'<div><div class="label">Policy Audit</div><div class="audit-list" style="margin-top:8px">' +
|
|
923
1271
|
(auditEvents.length ? auditEvents.map(function (event) {
|
|
924
1272
|
return '<div class="audit-row"><div class="top"><strong>' + esc(event.event_type) + '</strong><span class="badge">' + esc(event.action || event.outcome || '-') + '</span></div>' +
|
|
@@ -927,6 +1275,36 @@ conversation=' + result.conversation_id));
|
|
|
927
1275
|
'</div>';
|
|
928
1276
|
}).join('') : '<div class="empty">No audit events for this session.</div>') +
|
|
929
1277
|
'</div></div>' +
|
|
1278
|
+
'<div><div class="label">Human Thread Posture</div><div class="audit-list" style="margin-top:8px">' +
|
|
1279
|
+
'<div class="audit-row"><div class="top"><strong>Projection policy</strong><span class="badge">' + esc(projectionPreset) + '</span></div>' +
|
|
1280
|
+
'<div class="muted small">The collaboration session keeps the raw transcript. The human thread receives concise updates, risk signals, decisions, and approval results through the projection outbox.</div>' +
|
|
1281
|
+
'<div style="margin-top:8px">Use this detail view to inspect every raw message, task, handoff, audit event, and runtime change before taking action.</div>' +
|
|
1282
|
+
(sinceLastSeen
|
|
1283
|
+
? '<div class="summary-pills" style="margin-top:8px">' +
|
|
1284
|
+
'<span class="pill">new=' + esc(countSinceLastSeen(sinceLastSeen)) + '</span>' +
|
|
1285
|
+
'<span class="pill">decisions=' + esc(sinceLastSeen.new_decisions || 0) + '</span>' +
|
|
1286
|
+
'<span class="pill">failures=' + esc(sinceLastSeen.new_failures || 0) + '</span>' +
|
|
1287
|
+
'</div>'
|
|
1288
|
+
: '') +
|
|
1289
|
+
(transportHealth
|
|
1290
|
+
? '<div class="muted small" style="margin-top:8px">transport=' + esc(transportHealth.transport_mode || 'bridge') + ' \xB7 state=' + esc(transportHealth.state || 'Ready') + ' \xB7 retry_queue=' + esc(transportHealth.retry_queue_length || 0) + '</div>'
|
|
1291
|
+
: '') +
|
|
1292
|
+
(projectionOutbox.length
|
|
1293
|
+
? '<div class="muted small" style="margin-top:8px">outbox=' + esc(projectionOutbox[0].status || 'pending') + ' \xB7 target=' + esc(projectionOutbox[0].target_human_session || '(missing)') + '</div>'
|
|
1294
|
+
: '<div class="muted small" style="margin-top:8px">outbox=clear</div>') +
|
|
1295
|
+
'<div style="margin-top:12px"><div class="label">Projection Preview</div><div style="margin-top:8px">' + renderProjectionPreviewBlock(projectionPreview) + '</div></div>' +
|
|
1296
|
+
'</div>' +
|
|
1297
|
+
'</div></div>' +
|
|
1298
|
+
'</div>';
|
|
1299
|
+
|
|
1300
|
+
el.innerHTML +=
|
|
1301
|
+
'<div class="grid two-col" style="margin-top:16px">' +
|
|
1302
|
+
'<div><div class="label">Since Last Seen</div><div class="audit-list" style="margin-top:8px">' +
|
|
1303
|
+
'<div class="audit-row">' + renderSinceLastSeenSummary(sinceLastSeen) + '</div>' +
|
|
1304
|
+
'</div></div>' +
|
|
1305
|
+
'<div><div class="label">Delivery Timeline</div><div class="audit-list" style="margin-top:8px">' +
|
|
1306
|
+
renderDeliveryTimeline(deliveryTimeline) +
|
|
1307
|
+
'</div></div>' +
|
|
930
1308
|
'</div>';
|
|
931
1309
|
|
|
932
1310
|
el.querySelectorAll('.approve-session-btn').forEach(function (btn) {
|
|
@@ -1014,6 +1392,21 @@ conversation=' + result.conversation_id));
|
|
|
1014
1392
|
setTab('runtime');
|
|
1015
1393
|
});
|
|
1016
1394
|
});
|
|
1395
|
+
el.querySelectorAll('.collaboration-decision-btn').forEach(function (btn) {
|
|
1396
|
+
btn.addEventListener('click', async function () {
|
|
1397
|
+
const eventId = Number(btn.getAttribute('data-event-id'));
|
|
1398
|
+
const decision = btn.getAttribute('data-decision');
|
|
1399
|
+
if (!Number.isInteger(eventId) || !decision) return;
|
|
1400
|
+
await api('/api/runtime/session/collaboration-decision', {
|
|
1401
|
+
method: 'POST',
|
|
1402
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1403
|
+
body: JSON.stringify({ event_id: eventId, decision: decision }),
|
|
1404
|
+
});
|
|
1405
|
+
await refreshAll();
|
|
1406
|
+
if (state.selectedSessionKey) await loadSession(state.selectedSessionKey);
|
|
1407
|
+
setTab('runtime');
|
|
1408
|
+
});
|
|
1409
|
+
});
|
|
1017
1410
|
const sendSessionReplyBtn = document.getElementById('sendSessionReplyBtn');
|
|
1018
1411
|
if (sendSessionReplyBtn) {
|
|
1019
1412
|
sendSessionReplyBtn.addEventListener('click', async function () {
|
|
@@ -1089,6 +1482,11 @@ Previous chat link: ' + previous
|
|
|
1089
1482
|
const policy = state.policy;
|
|
1090
1483
|
if (!policy) return;
|
|
1091
1484
|
const profile = state.overview && state.overview.profile ? state.overview.profile : null;
|
|
1485
|
+
document.getElementById('projectionPreset').value =
|
|
1486
|
+
policy.doc && policy.doc.collaboration_projection && policy.doc.collaboration_projection.preset
|
|
1487
|
+
? policy.doc.collaboration_projection.preset
|
|
1488
|
+
: 'balanced';
|
|
1489
|
+
document.getElementById('projectionPolicyPreview').innerHTML = renderProjectionPreviewBlock(state.session && state.session.projectionPreview ? state.session.projectionPreview : null);
|
|
1092
1490
|
document.getElementById('contactDefault').value = policy.doc.contact_policy.default_action;
|
|
1093
1491
|
document.getElementById('taskDefault').value = policy.doc.task_policy.default_action;
|
|
1094
1492
|
document.getElementById('profileDisplayName').value = profile && profile.display_name ? profile.display_name : '';
|
|
@@ -1260,6 +1658,7 @@ Previous chat link: ' + previous
|
|
|
1260
1658
|
syncSelectedSessionFromOverview();
|
|
1261
1659
|
renderHeader();
|
|
1262
1660
|
renderOverview();
|
|
1661
|
+
if (state.currentTab === 'runtime') await maybeMarkGlobalSeen('runtime');
|
|
1263
1662
|
if (state.selectedSessionKey) {
|
|
1264
1663
|
await loadSession(state.selectedSessionKey);
|
|
1265
1664
|
} else {
|
|
@@ -1271,10 +1670,14 @@ Previous chat link: ' + previous
|
|
|
1271
1670
|
|
|
1272
1671
|
async function loadSession(sessionKey) {
|
|
1273
1672
|
if (!sessionKey) return;
|
|
1673
|
+
const previousSessionKey = state.selectedSessionKey;
|
|
1274
1674
|
state.selectedSessionKey = sessionKey;
|
|
1275
1675
|
state.session = await api('/api/runtime/session?session_key=' + encodeURIComponent(sessionKey));
|
|
1276
1676
|
syncUrlState();
|
|
1277
1677
|
renderSession();
|
|
1678
|
+
if (state.currentTab === 'runtime') {
|
|
1679
|
+
await maybeMarkSessionSeen(sessionKey, previousSessionKey !== sessionKey);
|
|
1680
|
+
}
|
|
1278
1681
|
}
|
|
1279
1682
|
|
|
1280
1683
|
async function loadPolicy() {
|
|
@@ -1282,17 +1685,25 @@ Previous chat link: ' + previous
|
|
|
1282
1685
|
renderPolicy();
|
|
1283
1686
|
}
|
|
1284
1687
|
|
|
1688
|
+
async function loadDecisions() {
|
|
1689
|
+
state.decisions = await api('/api/runtime/collaboration-decisions');
|
|
1690
|
+
renderDecisionInbox();
|
|
1691
|
+
if (state.currentTab === 'decisions') await maybeMarkGlobalSeen('decisions');
|
|
1692
|
+
}
|
|
1693
|
+
|
|
1285
1694
|
async function refreshAll() {
|
|
1286
1695
|
if (!state.selectedProfile && state.profiles.length > 1) {
|
|
1287
1696
|
renderHeader();
|
|
1288
1697
|
return;
|
|
1289
1698
|
}
|
|
1290
1699
|
await loadOverview();
|
|
1700
|
+
await loadDecisions();
|
|
1291
1701
|
await loadPolicy();
|
|
1292
1702
|
renderHeader();
|
|
1293
1703
|
}
|
|
1294
1704
|
|
|
1295
1705
|
document.getElementById('navRuntime').addEventListener('click', function () { setTab('runtime'); });
|
|
1706
|
+
document.getElementById('navDecisions').addEventListener('click', function () { setTab('decisions'); });
|
|
1296
1707
|
document.getElementById('navPolicy').addEventListener('click', function () { setTab('policy'); });
|
|
1297
1708
|
document.getElementById('toggleUnreadBtn').addEventListener('click', async function () {
|
|
1298
1709
|
state.showUnreadOnly = !state.showUnreadOnly;
|
|
@@ -1331,6 +1742,15 @@ Previous chat link: ' + previous
|
|
|
1331
1742
|
await refreshAll();
|
|
1332
1743
|
setTab('policy');
|
|
1333
1744
|
});
|
|
1745
|
+
document.getElementById('saveProjectionPresetBtn').addEventListener('click', async function () {
|
|
1746
|
+
await api('/api/runtime/policy/projection', {
|
|
1747
|
+
method: 'POST',
|
|
1748
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1749
|
+
body: JSON.stringify({ preset: document.getElementById('projectionPreset').value }),
|
|
1750
|
+
});
|
|
1751
|
+
await refreshAll();
|
|
1752
|
+
setTab('policy');
|
|
1753
|
+
});
|
|
1334
1754
|
document.getElementById('addRuleBtn').addEventListener('click', async function () {
|
|
1335
1755
|
await api('/api/runtime/policy/rules', {
|
|
1336
1756
|
method: 'POST',
|
|
@@ -1678,6 +2098,62 @@ function writeTrustPolicyDoc(identityPath, doc) {
|
|
|
1678
2098
|
fs.writeFileSync(policyPath, JSON.stringify(normalizeTrustPolicyDoc(doc), null, 2), "utf-8");
|
|
1679
2099
|
return policyPath;
|
|
1680
2100
|
}
|
|
2101
|
+
function normalizeProjectionPreset(value) {
|
|
2102
|
+
if (value === "quiet" || value === "strict") return value;
|
|
2103
|
+
return "balanced";
|
|
2104
|
+
}
|
|
2105
|
+
function readProjectionOutboxState(storePath, limit = 20) {
|
|
2106
|
+
const store = new LocalStore(storePath);
|
|
2107
|
+
try {
|
|
2108
|
+
const manager = new CollaborationProjectionOutboxManager(store);
|
|
2109
|
+
const pending = manager.listByStatus("pending", limit);
|
|
2110
|
+
const failed = manager.listByStatus("failed", limit);
|
|
2111
|
+
return {
|
|
2112
|
+
pending,
|
|
2113
|
+
failed,
|
|
2114
|
+
total_pending: pending.length,
|
|
2115
|
+
total_failed: failed.length
|
|
2116
|
+
};
|
|
2117
|
+
} finally {
|
|
2118
|
+
store.close();
|
|
2119
|
+
}
|
|
2120
|
+
}
|
|
2121
|
+
function readTransportHealthState(storePath) {
|
|
2122
|
+
const store = new LocalStore(storePath);
|
|
2123
|
+
try {
|
|
2124
|
+
const eventManager = new CollaborationEventManager(store);
|
|
2125
|
+
const outboxManager = new CollaborationProjectionOutboxManager(store);
|
|
2126
|
+
const runtimeStatus = readIngressRuntimeStatus();
|
|
2127
|
+
const preference = readTransportPreference();
|
|
2128
|
+
const normalizedStatus = runtimeStatus ? {
|
|
2129
|
+
...runtimeStatus,
|
|
2130
|
+
preferred_transport_mode: preference?.preferred_mode ?? runtimeStatus.preferred_transport_mode ?? "bridge"
|
|
2131
|
+
} : {
|
|
2132
|
+
receive_mode: "webhook",
|
|
2133
|
+
transport_mode: preference?.preferred_mode ?? "bridge",
|
|
2134
|
+
preferred_transport_mode: preference?.preferred_mode ?? "bridge"
|
|
2135
|
+
};
|
|
2136
|
+
return deriveTransportHealth({
|
|
2137
|
+
runtime_status: normalizedStatus,
|
|
2138
|
+
recent_events: eventManager.listRecent(30),
|
|
2139
|
+
projection_outbox_failed: outboxManager.listByStatus("failed", 20)
|
|
2140
|
+
});
|
|
2141
|
+
} finally {
|
|
2142
|
+
store.close();
|
|
2143
|
+
}
|
|
2144
|
+
}
|
|
2145
|
+
function readSinceLastSeenState(storePath, operatorId, scopeType, scopeKey) {
|
|
2146
|
+
const store = new LocalStore(storePath);
|
|
2147
|
+
try {
|
|
2148
|
+
return summarizeSinceLastSeen(store, {
|
|
2149
|
+
operator_id: operatorId,
|
|
2150
|
+
scope_type: scopeType,
|
|
2151
|
+
scope_key: scopeKey
|
|
2152
|
+
});
|
|
2153
|
+
} finally {
|
|
2154
|
+
store.close();
|
|
2155
|
+
}
|
|
2156
|
+
}
|
|
1681
2157
|
function buildPolicyDecisionShape(identityPath, remoteDid, opts) {
|
|
1682
2158
|
const policy = readTrustPolicyDoc(identityPath);
|
|
1683
2159
|
const runtimeMode = opts?.runtimeMode ?? getRuntimeMode();
|
|
@@ -1741,7 +2217,8 @@ async function buildRuntimeOverviewPayload(ctx) {
|
|
|
1741
2217
|
const taskManager = client.getTaskThreadManager();
|
|
1742
2218
|
const taskHandoffManager = client.getTaskHandoffManager();
|
|
1743
2219
|
const historyManager = client.getHistoryManager();
|
|
1744
|
-
|
|
2220
|
+
const collaborationEventManager = client.getCollaborationEventManager();
|
|
2221
|
+
if (!sessionManager || !sessionSummaryManager || !taskManager || !taskHandoffManager || !historyManager || !collaborationEventManager) {
|
|
1745
2222
|
throw new Error("Runtime overview requires a writable local store");
|
|
1746
2223
|
}
|
|
1747
2224
|
const sessions = sessionManager.listRecentSessions(24);
|
|
@@ -1787,6 +2264,17 @@ async function buildRuntimeOverviewPayload(ctx) {
|
|
|
1787
2264
|
acc[session.trust_state] = (acc[session.trust_state] ?? 0) + 1;
|
|
1788
2265
|
return acc;
|
|
1789
2266
|
}, {});
|
|
2267
|
+
const collaborationSummary = collaborationEventManager.summarize(200);
|
|
2268
|
+
const projectionOutbox = readProjectionOutboxState(ctx.storePath, 20);
|
|
2269
|
+
const transportHealth = readTransportHealthState(ctx.storePath);
|
|
2270
|
+
const sinceLastSeen = readSinceLastSeenState(ctx.storePath, "host_panel", "global");
|
|
2271
|
+
const decisionStore = new LocalStore(ctx.storePath);
|
|
2272
|
+
let decisionViews = [];
|
|
2273
|
+
try {
|
|
2274
|
+
decisionViews = listPendingDecisionViews(decisionStore, 100);
|
|
2275
|
+
} finally {
|
|
2276
|
+
decisionStore.close();
|
|
2277
|
+
}
|
|
1790
2278
|
return {
|
|
1791
2279
|
did: ctx.myDid,
|
|
1792
2280
|
serverUrl: ctx.serverUrl,
|
|
@@ -1816,18 +2304,27 @@ async function buildRuntimeOverviewPayload(ctx) {
|
|
|
1816
2304
|
contact: policy.contact_policy.enabled ? policy.contact_policy.default_action : "disabled",
|
|
1817
2305
|
task: policy.task_policy.enabled ? policy.task_policy.default_action : "disabled"
|
|
1818
2306
|
},
|
|
2307
|
+
collaborationProjection: policy.collaboration_projection,
|
|
1819
2308
|
sessionsTotal: sessions.length,
|
|
1820
2309
|
tasksTotal: refreshedTasks.length,
|
|
1821
2310
|
unreadTotal,
|
|
1822
2311
|
trustCounts,
|
|
1823
2312
|
recommendationSummary: recommendationState.summary,
|
|
2313
|
+
collaborationSummary,
|
|
2314
|
+
projectionOutbox,
|
|
2315
|
+
transportHealth,
|
|
2316
|
+
sinceLastSeen,
|
|
2317
|
+
pendingDecisions: decisionViews,
|
|
2318
|
+
recentCollaborationEvents: collaborationEventManager.listRecent(20),
|
|
1824
2319
|
sessions: sessions.map((session) => ({
|
|
1825
2320
|
...session,
|
|
1826
2321
|
session_summary: sessionSummaryManager.get(session.session_key),
|
|
1827
2322
|
mapped_work_session: session.conversation_id ? bindingByConversation.get(session.conversation_id) ?? null : null,
|
|
1828
2323
|
binding_alert: session.conversation_id ? bindingAlertByConversation.get(session.conversation_id) ?? null : null,
|
|
1829
2324
|
is_active_work_session: session.session_key === activeWorkSession,
|
|
1830
|
-
latest_messages: session.conversation_id ? historyManager.listRecent(session.conversation_id, 3) : []
|
|
2325
|
+
latest_messages: session.conversation_id ? historyManager.listRecent(session.conversation_id, 3) : [],
|
|
2326
|
+
collaboration_events: collaborationEventManager.listBySession(session.session_key, 3),
|
|
2327
|
+
since_last_seen: readSinceLastSeenState(ctx.storePath, "host_panel", "session", session.session_key)
|
|
1831
2328
|
})),
|
|
1832
2329
|
tasks: refreshedTasks.map((task) => ({
|
|
1833
2330
|
...task,
|
|
@@ -1847,7 +2344,8 @@ async function buildSessionOverviewPayload(ctx, sessionKey) {
|
|
|
1847
2344
|
const taskManager = client.getTaskThreadManager();
|
|
1848
2345
|
const taskHandoffManager = client.getTaskHandoffManager();
|
|
1849
2346
|
const historyManager = client.getHistoryManager();
|
|
1850
|
-
|
|
2347
|
+
const collaborationEventManager = client.getCollaborationEventManager();
|
|
2348
|
+
if (!sessionManager || !sessionSummaryManager || !taskManager || !taskHandoffManager || !historyManager || !collaborationEventManager) {
|
|
1851
2349
|
throw new Error("Session overview requires a writable local store");
|
|
1852
2350
|
}
|
|
1853
2351
|
const session = sessionKey ? sessionManager.get(sessionKey) : sessionManager.getActiveSession() ?? sessionManager.listRecentSessions(1)[0] ?? null;
|
|
@@ -1881,16 +2379,49 @@ async function buildSessionOverviewPayload(ctx, sessionKey) {
|
|
|
1881
2379
|
runtimeMode: getRuntimeMode(),
|
|
1882
2380
|
limit: 20
|
|
1883
2381
|
});
|
|
2382
|
+
const outboxStore = new LocalStore(ctx.storePath);
|
|
2383
|
+
let projectionOutbox = [];
|
|
2384
|
+
let sinceLastSeen = null;
|
|
2385
|
+
let deliveryTimeline = [];
|
|
2386
|
+
let projectionPreview = null;
|
|
2387
|
+
let pendingDecisionViews = [];
|
|
2388
|
+
try {
|
|
2389
|
+
projectionOutbox = new CollaborationProjectionOutboxManager(outboxStore).listBySession(session.session_key, 20);
|
|
2390
|
+
sinceLastSeen = summarizeSinceLastSeen(outboxStore, {
|
|
2391
|
+
operator_id: "host_panel",
|
|
2392
|
+
scope_type: "session",
|
|
2393
|
+
scope_key: session.session_key
|
|
2394
|
+
});
|
|
2395
|
+
deliveryTimeline = buildDeliveryTimeline(outboxStore, session.session_key, 40);
|
|
2396
|
+
projectionPreview = buildProjectionPreview(
|
|
2397
|
+
outboxStore,
|
|
2398
|
+
session.session_key,
|
|
2399
|
+
policy.collaboration_projection.preset,
|
|
2400
|
+
5
|
|
2401
|
+
);
|
|
2402
|
+
pendingDecisionViews = listPendingDecisionViews(outboxStore, 100).filter((event) => event.session_key === session.session_key).slice(0, 20);
|
|
2403
|
+
} finally {
|
|
2404
|
+
outboxStore.close();
|
|
2405
|
+
}
|
|
1884
2406
|
return {
|
|
1885
2407
|
session,
|
|
1886
2408
|
sessionSummary: sessionSummaryManager.get(session.session_key),
|
|
2409
|
+
collaborationEvents: collaborationEventManager.listBySession(session.session_key, 40),
|
|
2410
|
+
pendingCollaborationEvents: pendingDecisionViews,
|
|
2411
|
+
collaborationSummary: collaborationEventManager.summarize(200),
|
|
1887
2412
|
ingressRuntime: readIngressRuntimeStatus(),
|
|
2413
|
+
transportHealth: readTransportHealthState(ctx.storePath),
|
|
1888
2414
|
binding,
|
|
1889
2415
|
bindingAlert,
|
|
1890
2416
|
activeWorkSession,
|
|
1891
2417
|
activeWorkSessionFile: getActiveSessionFilePath(),
|
|
1892
2418
|
sessionMapPath: getSessionMapFilePath(),
|
|
1893
2419
|
sessionBindingAlertsPath: getSessionBindingAlertsFilePath(),
|
|
2420
|
+
collaborationProjection: policy.collaboration_projection,
|
|
2421
|
+
projectionOutbox,
|
|
2422
|
+
sinceLastSeen,
|
|
2423
|
+
deliveryTimeline,
|
|
2424
|
+
projectionPreview,
|
|
1894
2425
|
policyExplain: buildPolicyDecisionShape(ctx.identityPath, session.remote_did, { runtimeMode: getRuntimeMode() }),
|
|
1895
2426
|
tasks: tasks.map((task) => ({
|
|
1896
2427
|
...task,
|
|
@@ -1967,6 +2498,70 @@ async function handleApi(pathname, req, ctx) {
|
|
|
1967
2498
|
status: readIngressRuntimeStatus()
|
|
1968
2499
|
};
|
|
1969
2500
|
}
|
|
2501
|
+
if (parts[1] === "transport-health" && req.method === "GET") {
|
|
2502
|
+
return {
|
|
2503
|
+
transportHealth: readTransportHealthState(ctx.storePath),
|
|
2504
|
+
transportPreference: readTransportPreference()
|
|
2505
|
+
};
|
|
2506
|
+
}
|
|
2507
|
+
if (parts[1] === "transport-switch" && req.method === "POST") {
|
|
2508
|
+
const body = await readBody(req);
|
|
2509
|
+
const mode = String(body?.mode ?? "").trim().toLowerCase();
|
|
2510
|
+
if (mode !== "bridge" && mode !== "channel") throw new Error("mode must be bridge or channel");
|
|
2511
|
+
const result = switchTransportPreference(mode, {
|
|
2512
|
+
updated_by: "host_panel"
|
|
2513
|
+
});
|
|
2514
|
+
const store = new LocalStore(ctx.storePath);
|
|
2515
|
+
try {
|
|
2516
|
+
new CollaborationEventManager(store).record({
|
|
2517
|
+
event_type: "transport_switched",
|
|
2518
|
+
severity: result.restarted ? "notice" : "warning",
|
|
2519
|
+
summary: result.restarted ? `Preferred transport switched to ${mode}. A managed restart was attempted.` : `Preferred transport switched to ${mode}. Restart is still required.`,
|
|
2520
|
+
detail: {
|
|
2521
|
+
preferred_mode: result.preferred_mode,
|
|
2522
|
+
restart_required: result.restart_required,
|
|
2523
|
+
restart_method: result.restart_method ?? null,
|
|
2524
|
+
restart_error: result.restart_error ?? null,
|
|
2525
|
+
preference_path: result.preference_path
|
|
2526
|
+
}
|
|
2527
|
+
});
|
|
2528
|
+
} finally {
|
|
2529
|
+
store.close();
|
|
2530
|
+
}
|
|
2531
|
+
return {
|
|
2532
|
+
ok: true,
|
|
2533
|
+
result,
|
|
2534
|
+
transportHealth: readTransportHealthState(ctx.storePath)
|
|
2535
|
+
};
|
|
2536
|
+
}
|
|
2537
|
+
if (parts[1] === "seen" && req.method === "POST") {
|
|
2538
|
+
const body = await readBody(req);
|
|
2539
|
+
const operatorId = String(body?.operator_id ?? "host_panel").trim();
|
|
2540
|
+
const scopeType = String(body?.scope_type ?? "global").trim();
|
|
2541
|
+
const scopeKey = typeof body?.scope_key === "string" ? body.scope_key.trim() : void 0;
|
|
2542
|
+
if (!["host_panel", "tui", "mcp"].includes(operatorId)) throw new Error("operator_id must be host_panel, tui, or mcp");
|
|
2543
|
+
if (scopeType !== "global" && scopeType !== "session") throw new Error("scope_type must be global or session");
|
|
2544
|
+
const store = new LocalStore(ctx.storePath);
|
|
2545
|
+
try {
|
|
2546
|
+
const seen = new OperatorSeenStateManager(store).markSeen({
|
|
2547
|
+
operator_id: operatorId,
|
|
2548
|
+
scope_type: scopeType,
|
|
2549
|
+
scope_key: scopeType === "session" ? scopeKey : null,
|
|
2550
|
+
last_seen_ts: Date.now()
|
|
2551
|
+
});
|
|
2552
|
+
return {
|
|
2553
|
+
ok: true,
|
|
2554
|
+
seen,
|
|
2555
|
+
summary: summarizeSinceLastSeen(store, {
|
|
2556
|
+
operator_id: operatorId,
|
|
2557
|
+
scope_type: scopeType,
|
|
2558
|
+
scope_key: scopeType === "session" ? scopeKey : null
|
|
2559
|
+
})
|
|
2560
|
+
};
|
|
2561
|
+
} finally {
|
|
2562
|
+
store.close();
|
|
2563
|
+
}
|
|
2564
|
+
}
|
|
1970
2565
|
if (parts[1] === "demo" && req.method === "POST") {
|
|
1971
2566
|
const body = await readBody(req);
|
|
1972
2567
|
const preset = typeof body?.preset === "string" ? body.preset.trim().toLowerCase() : "";
|
|
@@ -2016,9 +2611,48 @@ async function handleApi(pathname, req, ctx) {
|
|
|
2016
2611
|
receiveMode: readIngressRuntimeStatus()
|
|
2017
2612
|
};
|
|
2018
2613
|
}
|
|
2614
|
+
if (parts[1] === "collaboration-decisions") {
|
|
2615
|
+
const store = new LocalStore(ctx.storePath);
|
|
2616
|
+
try {
|
|
2617
|
+
const manager = new CollaborationEventManager(store);
|
|
2618
|
+
const outboxManager = new CollaborationProjectionOutboxManager(store);
|
|
2619
|
+
if (req.method === "GET") {
|
|
2620
|
+
const pendingDecisions = listPendingDecisionViews(store, 100);
|
|
2621
|
+
return {
|
|
2622
|
+
pendingDecisions,
|
|
2623
|
+
projectionOutboxPending: outboxManager.listByStatus("pending", 50),
|
|
2624
|
+
projectionOutboxFailed: outboxManager.listByStatus("failed", 50)
|
|
2625
|
+
};
|
|
2626
|
+
}
|
|
2627
|
+
if (parts[2] === "resolve" && req.method === "POST") {
|
|
2628
|
+
const body = await readBody(req);
|
|
2629
|
+
const eventId = Number(body?.event_id);
|
|
2630
|
+
const decision = String(body?.decision ?? "").trim().toLowerCase();
|
|
2631
|
+
if (!Number.isInteger(eventId) || eventId <= 0) throw new Error("event_id is required");
|
|
2632
|
+
if (decision !== "approved" && decision !== "rejected") {
|
|
2633
|
+
throw new Error("decision must be approved or rejected");
|
|
2634
|
+
}
|
|
2635
|
+
const result = manager.resolveApproval(eventId, decision, {
|
|
2636
|
+
resolved_by: "host_panel",
|
|
2637
|
+
note: typeof body?.note === "string" ? body.note.trim() || void 0 : void 0
|
|
2638
|
+
});
|
|
2639
|
+
if (!result) throw new Error(`Collaboration event ${eventId} not found`);
|
|
2640
|
+
return {
|
|
2641
|
+
ok: true,
|
|
2642
|
+
event: result.updated,
|
|
2643
|
+
resolutionEvent: result.resolution_event,
|
|
2644
|
+
projectionOutbox: result.projection_outbox
|
|
2645
|
+
};
|
|
2646
|
+
}
|
|
2647
|
+
} finally {
|
|
2648
|
+
store.close();
|
|
2649
|
+
}
|
|
2650
|
+
}
|
|
2019
2651
|
if (parts[1] === "session") {
|
|
2020
2652
|
const sessionManager = ctx.client.getSessionManager();
|
|
2653
|
+
const collaborationEventManager = ctx.client.getCollaborationEventManager();
|
|
2021
2654
|
if (!sessionManager) throw new Error("Session actions require a writable local store");
|
|
2655
|
+
if (!collaborationEventManager) throw new Error("Collaboration actions require a writable local store");
|
|
2022
2656
|
if (parts[2] === "reply" && req.method === "POST") {
|
|
2023
2657
|
const body = await readBody(req);
|
|
2024
2658
|
const session = resolveSessionForInput(sessionManager, body);
|
|
@@ -2059,6 +2693,28 @@ async function handleApi(pathname, req, ctx) {
|
|
|
2059
2693
|
session: updated
|
|
2060
2694
|
};
|
|
2061
2695
|
}
|
|
2696
|
+
if (parts[2] === "collaboration-decision" && req.method === "POST") {
|
|
2697
|
+
const body = await readBody(req);
|
|
2698
|
+
const eventId = Number(body?.event_id);
|
|
2699
|
+
const decision = String(body?.decision ?? "").trim().toLowerCase();
|
|
2700
|
+
if (!Number.isInteger(eventId) || eventId <= 0) throw new Error("event_id is required");
|
|
2701
|
+
if (decision !== "approved" && decision !== "rejected") {
|
|
2702
|
+
throw new Error("decision must be approved or rejected");
|
|
2703
|
+
}
|
|
2704
|
+
const result = collaborationEventManager.resolveApproval(eventId, decision, {
|
|
2705
|
+
resolved_by: "host_panel",
|
|
2706
|
+
note: typeof body?.note === "string" ? body.note.trim() || void 0 : void 0
|
|
2707
|
+
});
|
|
2708
|
+
if (!result) throw new Error(`Collaboration event ${eventId} not found`);
|
|
2709
|
+
const session = result.updated.session_key ? sessionManager.get(result.updated.session_key) : null;
|
|
2710
|
+
return {
|
|
2711
|
+
ok: true,
|
|
2712
|
+
event: result.updated,
|
|
2713
|
+
resolutionEvent: result.resolution_event,
|
|
2714
|
+
projectionOutbox: result.projection_outbox,
|
|
2715
|
+
session
|
|
2716
|
+
};
|
|
2717
|
+
}
|
|
2062
2718
|
const url = new URL(req.url || "", "http://x");
|
|
2063
2719
|
const sessionKey = url.searchParams.get("session_key");
|
|
2064
2720
|
return buildSessionOverviewPayload(ctx, sessionKey);
|
|
@@ -2186,6 +2842,39 @@ async function handleApi(pathname, req, ctx) {
|
|
|
2186
2842
|
}
|
|
2187
2843
|
return { ok: true, path: savedPath, doc };
|
|
2188
2844
|
}
|
|
2845
|
+
if (parts[2] === "projection") {
|
|
2846
|
+
if (req.method === "GET") {
|
|
2847
|
+
const doc = readTrustPolicyDoc(ctx.identityPath);
|
|
2848
|
+
return {
|
|
2849
|
+
path: policyPath,
|
|
2850
|
+
projection: doc.collaboration_projection
|
|
2851
|
+
};
|
|
2852
|
+
}
|
|
2853
|
+
if (req.method === "POST") {
|
|
2854
|
+
const body = await readBody(req);
|
|
2855
|
+
const doc = readTrustPolicyDoc(ctx.identityPath);
|
|
2856
|
+
doc.collaboration_projection = {
|
|
2857
|
+
preset: normalizeProjectionPreset(body?.preset)
|
|
2858
|
+
};
|
|
2859
|
+
const savedPath = writeTrustPolicyDoc(ctx.identityPath, doc);
|
|
2860
|
+
const auditStore = new LocalStore(ctx.storePath);
|
|
2861
|
+
try {
|
|
2862
|
+
new TrustPolicyAuditManager(auditStore).record({
|
|
2863
|
+
event_type: "policy_default_updated",
|
|
2864
|
+
policy_scope: "session",
|
|
2865
|
+
action: "collaboration_projection_updated",
|
|
2866
|
+
outcome: "saved",
|
|
2867
|
+
explanation: `Updated projection preset=${doc.collaboration_projection.preset}`,
|
|
2868
|
+
detail: {
|
|
2869
|
+
preset: doc.collaboration_projection.preset
|
|
2870
|
+
}
|
|
2871
|
+
});
|
|
2872
|
+
} finally {
|
|
2873
|
+
auditStore.close();
|
|
2874
|
+
}
|
|
2875
|
+
return { ok: true, path: savedPath, projection: doc.collaboration_projection, doc };
|
|
2876
|
+
}
|
|
2877
|
+
}
|
|
2189
2878
|
if (parts[2] === "rules" && req.method === "POST") {
|
|
2190
2879
|
const body = await readBody(req);
|
|
2191
2880
|
const doc = readTrustPolicyDoc(ctx.identityPath);
|