@pingagent/sdk 0.1.14 → 0.1.16
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 +250 -11
- package/dist/chunk-BCYHGKQE.js +4825 -0
- package/dist/chunk-GU5W4KRD.js +5847 -0
- package/dist/chunk-HSVC3KDT.js +5611 -0
- package/dist/index.d.ts +415 -17
- package/dist/index.js +65 -3
- package/dist/web-server.js +597 -27
- package/package.json +3 -3
package/dist/web-server.js
CHANGED
|
@@ -2,30 +2,42 @@ import {
|
|
|
2
2
|
CollaborationEventManager,
|
|
3
3
|
CollaborationProjectionOutboxManager,
|
|
4
4
|
ContactManager,
|
|
5
|
+
HumanDeliveryBindingManager,
|
|
5
6
|
LocalStore,
|
|
7
|
+
NotificationIntentManager,
|
|
8
|
+
OperatorSeenStateManager,
|
|
6
9
|
PingAgentClient,
|
|
7
10
|
TrustPolicyAuditManager,
|
|
8
11
|
TrustRecommendationManager,
|
|
12
|
+
buildDeliveryTimeline,
|
|
13
|
+
buildProjectionPreview,
|
|
9
14
|
decideContactPolicy,
|
|
10
15
|
decideTaskPolicy,
|
|
11
16
|
defaultTrustPolicyDoc,
|
|
17
|
+
deriveTransportHealth,
|
|
12
18
|
ensureTokenValid,
|
|
13
19
|
getActiveSessionFilePath,
|
|
14
20
|
getSessionBindingAlertsFilePath,
|
|
15
21
|
getSessionMapFilePath,
|
|
16
22
|
getTrustRecommendationActionLabel,
|
|
23
|
+
listPendingDecisionViews,
|
|
24
|
+
listRecentBindingsForSession,
|
|
17
25
|
loadIdentity,
|
|
18
26
|
normalizeTrustPolicyDoc,
|
|
19
27
|
readCurrentActiveSessionKey,
|
|
20
28
|
readIngressRuntimeStatus,
|
|
21
29
|
readSessionBindingAlerts,
|
|
22
30
|
readSessionBindings,
|
|
31
|
+
readTransportPreference,
|
|
23
32
|
removeSessionBinding,
|
|
24
33
|
setSessionBinding,
|
|
34
|
+
summarizeHumanDelivery,
|
|
35
|
+
summarizeSinceLastSeen,
|
|
25
36
|
summarizeTrustPolicyAudit,
|
|
37
|
+
switchTransportPreference,
|
|
26
38
|
updateStoredToken,
|
|
27
39
|
upsertTrustPolicyRecommendation
|
|
28
|
-
} from "./chunk-
|
|
40
|
+
} from "./chunk-GU5W4KRD.js";
|
|
29
41
|
|
|
30
42
|
// src/web-server.ts
|
|
31
43
|
import * as fs from "fs";
|
|
@@ -210,6 +222,10 @@ function getHostPanelHtml() {
|
|
|
210
222
|
|
|
211
223
|
<section id="runtimePanel" class="panel active">
|
|
212
224
|
<div class="card" id="activationCard" style="margin-bottom:16px"></div>
|
|
225
|
+
<div class="grid two-col" style="margin-bottom:16px">
|
|
226
|
+
<div class="card" id="transportHealthCard"></div>
|
|
227
|
+
<div class="card" id="sinceLastSeenCard"></div>
|
|
228
|
+
</div>
|
|
213
229
|
<div class="grid stats" id="statsGrid"></div>
|
|
214
230
|
<div class="grid runtime-layout" style="margin-top:16px">
|
|
215
231
|
<div class="card">
|
|
@@ -269,6 +285,7 @@ function getHostPanelHtml() {
|
|
|
269
285
|
<div class="row-actions">
|
|
270
286
|
<button class="action-btn" id="saveProjectionPresetBtn">Save projection preset</button>
|
|
271
287
|
</div>
|
|
288
|
+
<div id="projectionPolicyPreview" class="muted small" style="margin-top:12px"></div>
|
|
272
289
|
</div>
|
|
273
290
|
</div>
|
|
274
291
|
|
|
@@ -407,6 +424,10 @@ function getHostPanelHtml() {
|
|
|
407
424
|
detailMode: sessionStorage.getItem('pingagent_host_panel_detail_mode') || 'basic',
|
|
408
425
|
showUnreadOnly: false,
|
|
409
426
|
};
|
|
427
|
+
const seenMarkers = {
|
|
428
|
+
globalTab: null,
|
|
429
|
+
sessionKey: null,
|
|
430
|
+
};
|
|
410
431
|
|
|
411
432
|
function esc(value) {
|
|
412
433
|
return String(value == null ? '' : value)
|
|
@@ -429,6 +450,220 @@ function getHostPanelHtml() {
|
|
|
429
450
|
try { return new Date(value).toLocaleString(); } catch { return String(value); }
|
|
430
451
|
}
|
|
431
452
|
|
|
453
|
+
function countSinceLastSeen(summary) {
|
|
454
|
+
if (!summary) return 0;
|
|
455
|
+
return Number(summary.new_external_messages || 0)
|
|
456
|
+
+ Number(summary.new_conclusions || 0)
|
|
457
|
+
+ Number(summary.new_handoffs || 0)
|
|
458
|
+
+ Number(summary.new_decisions || 0)
|
|
459
|
+
+ Number(summary.new_failures || 0)
|
|
460
|
+
+ Number(summary.new_repairs || 0)
|
|
461
|
+
+ Number(summary.new_projection_failures || 0);
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
function renderSinceLastSeenSummary(summary) {
|
|
465
|
+
if (!summary) return '<div class="empty">No seen-state yet.</div>';
|
|
466
|
+
const count = countSinceLastSeen(summary);
|
|
467
|
+
return '' +
|
|
468
|
+
'<div class="status-main">' +
|
|
469
|
+
'<h2>Since Last Seen</h2>' +
|
|
470
|
+
'<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>' +
|
|
471
|
+
'<div class="summary-pills">' +
|
|
472
|
+
'<span class="pill">new=' + esc(count) + '</span>' +
|
|
473
|
+
'<span class="pill">external=' + esc(summary.new_external_messages || 0) + '</span>' +
|
|
474
|
+
'<span class="pill">conclusions=' + esc(summary.new_conclusions || 0) + '</span>' +
|
|
475
|
+
'<span class="pill">handoffs=' + esc(summary.new_handoffs || 0) + '</span>' +
|
|
476
|
+
'<span class="pill">decisions=' + esc(summary.new_decisions || 0) + '</span>' +
|
|
477
|
+
'<span class="pill">failures=' + esc(summary.new_failures || 0) + '</span>' +
|
|
478
|
+
'<span class="pill">repairs=' + esc(summary.new_repairs || 0) + '</span>' +
|
|
479
|
+
'<span class="pill">projection=' + esc(summary.new_projection_failures || 0) + '</span>' +
|
|
480
|
+
'</div>' +
|
|
481
|
+
'<div class="muted small" style="margin-top:8px">latest=' + esc(fmtTs(summary.latest_ts)) + '</div>' +
|
|
482
|
+
'</div>';
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
function renderTransportHealthCard(transportHealth) {
|
|
486
|
+
const health = transportHealth || {};
|
|
487
|
+
const stateLabel = health.state || 'Ready';
|
|
488
|
+
const stateClass = stateLabel === 'Degraded' || stateLabel === 'Switching Recommended' ? 'degraded' : 'ready';
|
|
489
|
+
const current = health.transport_mode || 'bridge';
|
|
490
|
+
const preferred = health.preferred_transport_mode || current;
|
|
491
|
+
return '' +
|
|
492
|
+
'<div class="status-strip">' +
|
|
493
|
+
'<div class="status-main">' +
|
|
494
|
+
'<h2>Transport Health</h2>' +
|
|
495
|
+
'<div class="status-state ' + esc(stateClass) + '">' + esc(stateLabel) + '</div>' +
|
|
496
|
+
'<div class="summary-pills">' +
|
|
497
|
+
'<span class="pill">current=' + esc(current) + '</span>' +
|
|
498
|
+
'<span class="pill">preferred=' + esc(preferred) + '</span>' +
|
|
499
|
+
'<span class="pill">retry_queue=' + esc(health.retry_queue_length || 0) + '</span>' +
|
|
500
|
+
'<span class="pill">failures=' + esc(health.consecutive_failures || 0) + '</span>' +
|
|
501
|
+
'</div>' +
|
|
502
|
+
'<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>' +
|
|
503
|
+
'<div class="muted small">last degraded=' + esc(fmtTs(health.last_degraded_at)) + ' \xB7 last repaired=' + esc(fmtTs(health.last_repaired_at)) + '</div>' +
|
|
504
|
+
(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>') +
|
|
505
|
+
'</div>' +
|
|
506
|
+
'<div style="min-width:240px">' +
|
|
507
|
+
'<div class="label">Actions</div>' +
|
|
508
|
+
'<div class="row-actions" style="margin-top:8px">' +
|
|
509
|
+
'<button class="secondary-btn transport-switch-btn" data-mode="bridge" style="width:auto">Switch to bridge</button>' +
|
|
510
|
+
'<button class="secondary-btn transport-switch-btn" data-mode="channel" style="width:auto">Switch to channel</button>' +
|
|
511
|
+
'</div>' +
|
|
512
|
+
(health.state === 'Switching Recommended'
|
|
513
|
+
? '<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>'
|
|
514
|
+
: '<div class="muted small" style="margin-top:10px">Managed restart keeps the transport layer swappable without changing the human-facing model.</div>') +
|
|
515
|
+
'</div>' +
|
|
516
|
+
'</div>';
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
function renderProjectionPreviewBlock(preview) {
|
|
520
|
+
if (!preview) return '<div class="empty">No projection preview available for the selected session yet.</div>';
|
|
521
|
+
const detailOnly = Array.isArray(preview.description && preview.description.detail_only) ? preview.description.detail_only : [];
|
|
522
|
+
const immediate = Array.isArray(preview.description && preview.description.immediate) ? preview.description.immediate : [];
|
|
523
|
+
return '' +
|
|
524
|
+
'<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>' +
|
|
525
|
+
'<div class="audit-list" style="margin-top:10px">' +
|
|
526
|
+
(Array.isArray(preview.recent) && preview.recent.length
|
|
527
|
+
? preview.recent.map(function (entry) {
|
|
528
|
+
return '<div class="audit-row"><div class="top"><strong>' + esc(entry.event_type) + '</strong><span class="badge">' + esc(entry.disposition) + '</span></div>' +
|
|
529
|
+
'<div class="muted small">' + esc(entry.reason || '(no reason)') + '</div>' +
|
|
530
|
+
'</div>';
|
|
531
|
+
}).join('')
|
|
532
|
+
: '<div class="empty">No recent collaboration events to preview.</div>') +
|
|
533
|
+
'</div>';
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
function renderDeliveryTimeline(timeline) {
|
|
537
|
+
if (!Array.isArray(timeline) || !timeline.length) {
|
|
538
|
+
return '<div class="empty">No delivery timeline yet.</div>';
|
|
539
|
+
}
|
|
540
|
+
return timeline.map(function (entry) {
|
|
541
|
+
return '<div class="audit-row"><div class="top"><strong>' + esc(entry.kind || 'event') + '</strong><span class="badge">' + esc(fmtTs(entry.ts_ms)) + '</span></div>' +
|
|
542
|
+
'<div style="margin-top:8px">' + esc(entry.summary || '(no summary)') + '</div>' +
|
|
543
|
+
'</div>';
|
|
544
|
+
}).join('');
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
function renderHumanDeliveryIntents(intents, emptyLabel) {
|
|
548
|
+
if (!Array.isArray(intents) || !intents.length) {
|
|
549
|
+
return '<div class="empty">' + esc(emptyLabel || 'No notification intents yet.') + '</div>';
|
|
550
|
+
}
|
|
551
|
+
return intents.map(function (intent) {
|
|
552
|
+
const bindingLabel = intent.resolved_binding_id ? ('binding=' + intent.resolved_binding_id) : 'binding=(unresolved)';
|
|
553
|
+
const ackLabel = intent.acknowledged_at
|
|
554
|
+
? (' \xB7 ack=' + fmtTs(intent.acknowledged_at))
|
|
555
|
+
: '';
|
|
556
|
+
const actions = [];
|
|
557
|
+
if (intent.status === 'failed' || intent.status === 'unresolved') {
|
|
558
|
+
actions.push('<button class="secondary-btn human-delivery-action-btn" data-action="retry_intent" data-intent-id="' + esc(intent.id) + '">Retry</button>');
|
|
559
|
+
actions.push('<button class="secondary-btn human-delivery-action-btn" data-action="re_resolve_intent_binding" data-intent-id="' + esc(intent.id) + '">Re-resolve</button>');
|
|
560
|
+
} else if (intent.status === 'pending' || intent.status === 'bound' || intent.status === 'sent') {
|
|
561
|
+
actions.push('<button class="secondary-btn human-delivery-action-btn" data-action="re_resolve_intent_binding" data-intent-id="' + esc(intent.id) + '">Re-resolve</button>');
|
|
562
|
+
}
|
|
563
|
+
if (intent.status !== 'canceled' && intent.status !== 'acknowledged') {
|
|
564
|
+
actions.push('<button class="danger-btn human-delivery-action-btn" data-action="cancel_intent" data-intent-id="' + esc(intent.id) + '">Cancel</button>');
|
|
565
|
+
}
|
|
566
|
+
return '<div class="audit-row"><div class="top"><strong>' + esc(intent.title || intent.summary || '(no title)') + '</strong>' +
|
|
567
|
+
'<span class="badge">' + esc(intent.status || 'pending') + '</span></div>' +
|
|
568
|
+
'<div class="muted small">' + esc(fmtTs(intent.created_at || intent.updated_at)) + ' \xB7 type=' + esc(intent.intent_type || 'collaboration_update') + ' \xB7 ' + esc(bindingLabel) + esc(ackLabel) + '</div>' +
|
|
569
|
+
'<div style="margin-top:8px">' + esc(intent.summary || '(no summary)') + '</div>' +
|
|
570
|
+
(intent.last_error ? '<div class="muted small" style="margin-top:8px;color:#fecaca">' + esc(intent.last_error) + '</div>' : '') +
|
|
571
|
+
(actions.length ? '<div class="row-actions" style="margin-top:10px">' + actions.join('') + '</div>' : '') +
|
|
572
|
+
'</div>';
|
|
573
|
+
}).join('');
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
function renderRecentBindings(bindings) {
|
|
577
|
+
if (!Array.isArray(bindings) || !bindings.length) {
|
|
578
|
+
return '<div class="empty">No human reply targets recorded yet. Once a person talks to OpenClaw from any channel, the explicit reply target will appear here.</div>';
|
|
579
|
+
}
|
|
580
|
+
return bindings.map(function (binding) {
|
|
581
|
+
const actions = binding.status === 'active'
|
|
582
|
+
? '<div class="row-actions" style="margin-top:10px"><button class="secondary-btn human-delivery-action-btn" data-action="mark_binding_stale" data-binding-id="' + esc(binding.id) + '">Mark stale</button></div>'
|
|
583
|
+
: '';
|
|
584
|
+
return '<div class="audit-row"><div class="top"><strong>' + esc(binding.channel + ' -> ' + binding.to) + '</strong>' +
|
|
585
|
+
'<span class="badge">' + esc(binding.status || 'active') + '</span></div>' +
|
|
586
|
+
'<div class="muted small">' + esc(fmtTs(binding.last_inbound_at || binding.updated_at)) + ' \xB7 owner=' + esc(binding.owner_ref || '(none)') + '</div>' +
|
|
587
|
+
'<div class="muted small" style="margin-top:8px">session=' + esc(binding.source_session_key || '(none)') + ' \xB7 conversation=' + esc(binding.source_conversation_id || '(none)') + '</div>' +
|
|
588
|
+
actions +
|
|
589
|
+
'</div>';
|
|
590
|
+
}).join('');
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
function renderHumanDeliveryCapabilities(humanDelivery) {
|
|
594
|
+
const capabilities = Array.isArray(humanDelivery && humanDelivery.channel_capabilities) ? humanDelivery.channel_capabilities : [];
|
|
595
|
+
if (!capabilities.length) {
|
|
596
|
+
return '<div class="empty">No channel capability data yet.</div>';
|
|
597
|
+
}
|
|
598
|
+
return capabilities.map(function (cap) {
|
|
599
|
+
const supportLabel = cap.supports_explicit_send ? 'explicit-send' : 'unsupported';
|
|
600
|
+
const canary = cap.last_canary_at
|
|
601
|
+
? (' \xB7 canary=' + (cap.last_canary_ok ? 'ok' : 'failed') + ' @ ' + fmtTs(cap.last_canary_at))
|
|
602
|
+
: ' \xB7 canary=pending';
|
|
603
|
+
return '<div class="audit-row"><div class="top"><strong>' + esc(cap.channel) + '</strong><span class="badge">' + esc(supportLabel) + '</span></div>' +
|
|
604
|
+
'<div class="muted small">configured=' + esc(cap.configured ? 'true' : 'false') + ' \xB7 thread=' + esc(cap.supports_thread_id ? 'yes' : 'no') + ' \xB7 account=' + esc(cap.supports_account_id ? 'yes' : 'no') + ' \xB7 dry_run=' + esc(cap.supports_dry_run ? 'yes' : 'no') + esc(canary) + '</div>' +
|
|
605
|
+
(cap.last_canary_error ? '<div class="muted small" style="margin-top:8px;color:#fecaca">' + esc(cap.last_canary_error) + '</div>' : '') +
|
|
606
|
+
'</div>';
|
|
607
|
+
}).join('');
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
async function runHumanDeliveryAction(action, payload) {
|
|
611
|
+
return api('/api/runtime/human-delivery/action', {
|
|
612
|
+
method: 'POST',
|
|
613
|
+
headers: { 'Content-Type': 'application/json' },
|
|
614
|
+
body: JSON.stringify(Object.assign({ action: action }, payload || {})),
|
|
615
|
+
});
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
function wireHumanDeliveryActionButtons(root) {
|
|
619
|
+
if (!root || !root.querySelectorAll) return;
|
|
620
|
+
root.querySelectorAll('.human-delivery-action-btn').forEach(function (btn) {
|
|
621
|
+
btn.addEventListener('click', async function () {
|
|
622
|
+
const action = btn.getAttribute('data-action');
|
|
623
|
+
if (!action) return;
|
|
624
|
+
const intentId = btn.getAttribute('data-intent-id');
|
|
625
|
+
const bindingId = btn.getAttribute('data-binding-id');
|
|
626
|
+
await runHumanDeliveryAction(action, {
|
|
627
|
+
intent_id: intentId ? Number(intentId) : undefined,
|
|
628
|
+
binding_id: bindingId ? Number(bindingId) : undefined,
|
|
629
|
+
});
|
|
630
|
+
await refreshAll();
|
|
631
|
+
if (state.selectedSessionKey) await loadSession(state.selectedSessionKey);
|
|
632
|
+
setTab(state.tab || 'runtime');
|
|
633
|
+
});
|
|
634
|
+
});
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
async function markSeen(scopeType, scopeKey) {
|
|
638
|
+
return api('/api/runtime/seen', {
|
|
639
|
+
method: 'POST',
|
|
640
|
+
headers: { 'Content-Type': 'application/json' },
|
|
641
|
+
body: JSON.stringify({
|
|
642
|
+
operator_id: 'host_panel',
|
|
643
|
+
scope_type: scopeType,
|
|
644
|
+
scope_key: scopeType === 'session' ? scopeKey : null,
|
|
645
|
+
}),
|
|
646
|
+
});
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
async function maybeMarkGlobalSeen(tab, force) {
|
|
650
|
+
if (tab !== 'runtime' && tab !== 'decisions') return;
|
|
651
|
+
if (!force && seenMarkers.globalTab === tab) return;
|
|
652
|
+
try {
|
|
653
|
+
await markSeen('global', null);
|
|
654
|
+
seenMarkers.globalTab = tab;
|
|
655
|
+
} catch {}
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
async function maybeMarkSessionSeen(sessionKey, force) {
|
|
659
|
+
if (!sessionKey) return;
|
|
660
|
+
if (!force && seenMarkers.sessionKey === sessionKey) return;
|
|
661
|
+
try {
|
|
662
|
+
await markSeen('session', sessionKey);
|
|
663
|
+
seenMarkers.sessionKey = sessionKey;
|
|
664
|
+
} catch {}
|
|
665
|
+
}
|
|
666
|
+
|
|
432
667
|
function syncUrlState() {
|
|
433
668
|
const url = new URL(window.location.href);
|
|
434
669
|
if (state.selectedProfile) url.searchParams.set('profile', state.selectedProfile);
|
|
@@ -512,6 +747,7 @@ function getHostPanelHtml() {
|
|
|
512
747
|
}
|
|
513
748
|
|
|
514
749
|
function setTab(tab) {
|
|
750
|
+
const previous = state.currentTab;
|
|
515
751
|
state.currentTab = tab;
|
|
516
752
|
document.getElementById('navRuntime').classList.toggle('active', tab === 'runtime');
|
|
517
753
|
document.getElementById('navDecisions').classList.toggle('active', tab === 'decisions');
|
|
@@ -519,7 +755,16 @@ function getHostPanelHtml() {
|
|
|
519
755
|
document.getElementById('runtimePanel').classList.toggle('active', tab === 'runtime');
|
|
520
756
|
document.getElementById('decisionsPanel').classList.toggle('active', tab === 'decisions');
|
|
521
757
|
document.getElementById('policyPanel').classList.toggle('active', tab === 'policy');
|
|
758
|
+
if (previous !== tab && tab !== 'runtime') {
|
|
759
|
+
seenMarkers.sessionKey = null;
|
|
760
|
+
}
|
|
522
761
|
syncUrlState();
|
|
762
|
+
if (previous !== tab) {
|
|
763
|
+
void maybeMarkGlobalSeen(tab, true);
|
|
764
|
+
if (tab === 'runtime' && state.selectedSessionKey) {
|
|
765
|
+
void maybeMarkSessionSeen(state.selectedSessionKey, true);
|
|
766
|
+
}
|
|
767
|
+
}
|
|
523
768
|
}
|
|
524
769
|
|
|
525
770
|
function renderHeader() {
|
|
@@ -650,15 +895,24 @@ function getHostPanelHtml() {
|
|
|
650
895
|
function renderDecisionInbox() {
|
|
651
896
|
const data = state.decisions;
|
|
652
897
|
const pendingDecisions = data && Array.isArray(data.pendingDecisions) ? data.pendingDecisions : [];
|
|
898
|
+
const overdueDecisions = pendingDecisions.filter(function (event) { return !!event.overdue; });
|
|
653
899
|
const pendingOutbox = data && Array.isArray(data.projectionOutboxPending) ? data.projectionOutboxPending : [];
|
|
654
900
|
const failedOutbox = data && Array.isArray(data.projectionOutboxFailed) ? data.projectionOutboxFailed : [];
|
|
655
|
-
|
|
656
|
-
|
|
901
|
+
const pendingIntents = data && Array.isArray(data.notificationIntentsPending) ? data.notificationIntentsPending : [];
|
|
902
|
+
const unresolvedIntents = data && Array.isArray(data.notificationIntentsUnresolved) ? data.notificationIntentsUnresolved : [];
|
|
903
|
+
const failedIntents = data && Array.isArray(data.notificationIntentsFailed) ? data.notificationIntentsFailed : [];
|
|
904
|
+
const humanDelivery = data && data.humanDelivery ? data.humanDelivery : null;
|
|
905
|
+
const boundChannelReply = !!(humanDelivery && humanDelivery.mode === 'bound_channel_reply');
|
|
906
|
+
document.getElementById('decisionInboxSummary').textContent = boundChannelReply
|
|
907
|
+
? ('pending=' + pendingDecisions.length + ' \xB7 overdue=' + overdueDecisions.length + ' \xB7 notify_pending=' + pendingIntents.length + ' \xB7 unresolved=' + unresolvedIntents.length + ' \xB7 notify_failed=' + failedIntents.length)
|
|
908
|
+
: ('pending=' + pendingDecisions.length + ' \xB7 overdue=' + overdueDecisions.length + ' \xB7 projection_pending=' + pendingOutbox.length + ' \xB7 projection_failed=' + failedOutbox.length);
|
|
657
909
|
document.getElementById('decisionInboxList').innerHTML = pendingDecisions.length
|
|
658
910
|
? pendingDecisions.map(function (event) {
|
|
659
911
|
return '<div class="audit-row"><div class="top"><strong>' + esc(event.summary || '(no summary)') + '</strong>' +
|
|
660
|
-
'<span class="badge">' + esc(event.severity || 'warning') + '</span
|
|
661
|
-
'<
|
|
912
|
+
'<div style="display:flex;gap:6px;flex-wrap:wrap;justify-content:flex-end"><span class="badge">' + esc(event.severity || 'warning') + '</span>' +
|
|
913
|
+
(event.overdue ? '<span class="badge alert">overdue</span>' : '') + '</div></div>' +
|
|
914
|
+
'<div class="muted small">' + esc(fmtTs(event.ts_ms)) + ' \xB7 session=' + esc(event.session_key || '(none)') + ' \xB7 target=' + esc(event.target_human_session || '(none)') +
|
|
915
|
+
(event.overdue ? ' \xB7 overdue_by=' + esc(Math.ceil((event.overdue_by_ms || 0) / 60000)) + 'm' : '') + '</div>' +
|
|
662
916
|
'<div class="muted small" style="margin-top:8px">detail_ref=' + esc(event.conversation_id || '(none)') + '</div>' +
|
|
663
917
|
'<div class="row-actions">' +
|
|
664
918
|
'<button class="action-btn inbox-decision-btn" data-event-id="' + esc(event.id) + '" data-decision="approved">Approve</button>' +
|
|
@@ -670,20 +924,26 @@ function getHostPanelHtml() {
|
|
|
670
924
|
: '<div class="empty">No pending collaboration decisions.</div>';
|
|
671
925
|
|
|
672
926
|
const combinedOutbox = pendingOutbox.concat(failedOutbox);
|
|
673
|
-
document.getElementById('projectionOutboxSummary').textContent =
|
|
674
|
-
|
|
927
|
+
document.getElementById('projectionOutboxSummary').textContent = boundChannelReply
|
|
928
|
+
? ('bound reply mode \xB7 pending=' + pendingIntents.length + ' \xB7 unresolved=' + unresolvedIntents.length + ' \xB7 failed=' + failedIntents.length)
|
|
929
|
+
: (combinedOutbox.length
|
|
675
930
|
? 'Human-thread projection uses a stable outbox. Failed rows stay visible until delivery recovers.'
|
|
676
|
-
: 'No undelivered human-thread projections.';
|
|
677
|
-
document.getElementById('projectionOutboxList').innerHTML =
|
|
678
|
-
?
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
931
|
+
: 'No undelivered human-thread projections.');
|
|
932
|
+
document.getElementById('projectionOutboxList').innerHTML = boundChannelReply
|
|
933
|
+
? renderHumanDeliveryIntents(
|
|
934
|
+
pendingIntents.concat(unresolvedIntents).concat(failedIntents),
|
|
935
|
+
'No pending, unresolved, or failed human-delivery intents.',
|
|
936
|
+
)
|
|
937
|
+
: (combinedOutbox.length
|
|
938
|
+
? combinedOutbox.map(function (entry) {
|
|
939
|
+
return '<div class="audit-row"><div class="top"><strong>' + esc(entry.summary || '(no summary)') + '</strong>' +
|
|
940
|
+
'<span class="badge">' + esc(entry.status || 'pending') + '</span></div>' +
|
|
941
|
+
'<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>' +
|
|
942
|
+
'<div class="muted small" style="margin-top:8px">target=' + esc(entry.target_human_session || '(missing)') + ' \xB7 session=' + esc(entry.session_key || '(none)') + '</div>' +
|
|
943
|
+
(entry.last_error ? '<div style="margin-top:8px">' + esc(entry.last_error) + '</div>' : '') +
|
|
944
|
+
'</div>';
|
|
945
|
+
}).join('')
|
|
946
|
+
: '<div class="empty">No pending or failed projection deliveries.</div>');
|
|
687
947
|
|
|
688
948
|
document.querySelectorAll('.inbox-decision-btn').forEach(function (btn) {
|
|
689
949
|
btn.addEventListener('click', async function () {
|
|
@@ -710,6 +970,7 @@ function getHostPanelHtml() {
|
|
|
710
970
|
setTab('runtime');
|
|
711
971
|
});
|
|
712
972
|
});
|
|
973
|
+
wireHumanDeliveryActionButtons(document.getElementById('projectionOutboxList'));
|
|
713
974
|
}
|
|
714
975
|
|
|
715
976
|
function getOverviewSessions() {
|
|
@@ -747,6 +1008,12 @@ function getHostPanelHtml() {
|
|
|
747
1008
|
if (!overview) return;
|
|
748
1009
|
syncSelectedSessionFromOverview();
|
|
749
1010
|
const ingressState = ingressStatusModel(overview);
|
|
1011
|
+
const transportHealth = overview.transportHealth || null;
|
|
1012
|
+
const sinceLastSeen = overview.sinceLastSeen || null;
|
|
1013
|
+
const humanDelivery = overview.humanDelivery || null;
|
|
1014
|
+
const overdueDecisions = Array.isArray(overview.pendingDecisions)
|
|
1015
|
+
? overview.pendingDecisions.filter(function (event) { return !!event.overdue; })
|
|
1016
|
+
: [];
|
|
750
1017
|
document.getElementById('activationCard').innerHTML =
|
|
751
1018
|
'<div class="status-strip">' +
|
|
752
1019
|
'<div class="status-main">' +
|
|
@@ -754,6 +1021,12 @@ function getHostPanelHtml() {
|
|
|
754
1021
|
'<div class="status-state ' + ingressState.className + '">' + esc(ingressState.label) + '</div>' +
|
|
755
1022
|
'<div class="muted small">' + esc(ingressState.detail) + '</div>' +
|
|
756
1023
|
'<div class="muted small">Public link: ' + esc(overview.publicSelf && overview.publicSelf.public_url ? overview.publicSelf.public_url : '(not ready yet)') + '</div>' +
|
|
1024
|
+
'<div class="summary-pills">' +
|
|
1025
|
+
'<span class="pill">pending_decisions=' + esc((overview.pendingDecisions || []).length) + '</span>' +
|
|
1026
|
+
'<span class="pill">overdue=' + esc(overdueDecisions.length) + '</span>' +
|
|
1027
|
+
'<span class="pill">projection=' + esc(overview.collaborationProjection && overview.collaborationProjection.preset ? overview.collaborationProjection.preset : 'balanced') + '</span>' +
|
|
1028
|
+
(humanDelivery ? '<span class="pill">human_delivery=' + esc(humanDelivery.mode || 'projection_outbox') + '</span>' : '') +
|
|
1029
|
+
'</div>' +
|
|
757
1030
|
'</div>' +
|
|
758
1031
|
'<div style="min-width:320px">' +
|
|
759
1032
|
'<div class="label">Quick Start</div>' +
|
|
@@ -768,6 +1041,8 @@ function getHostPanelHtml() {
|
|
|
768
1041
|
'</div>' +
|
|
769
1042
|
'</div>' +
|
|
770
1043
|
'</div>';
|
|
1044
|
+
document.getElementById('transportHealthCard').innerHTML = renderTransportHealthCard(transportHealth);
|
|
1045
|
+
document.getElementById('sinceLastSeenCard').innerHTML = renderSinceLastSeenSummary(sinceLastSeen);
|
|
771
1046
|
const subscription = overview.subscription || null;
|
|
772
1047
|
const stats = [
|
|
773
1048
|
{ label: 'Plan', value: subscription ? subscription.tier : 'ghost', sub: subscription ? subscription.summary : 'subscription unavailable' },
|
|
@@ -779,12 +1054,16 @@ function getHostPanelHtml() {
|
|
|
779
1054
|
{ label: 'Tasks', value: overview.tasksTotal, sub: 'recent local task threads' },
|
|
780
1055
|
{ label: 'Audit', value: overview.auditSummary.total_events, sub: 'policy / runtime audit events' },
|
|
781
1056
|
{ label: 'Collaboration', value: overview.collaborationSummary ? overview.collaborationSummary.total_events : 0, sub: overview.collaborationSummary ? ('pending_review=' + overview.collaborationSummary.pending_approvals) : 'projected external collaboration events' },
|
|
1057
|
+
{ label: 'Human Delivery', value: humanDelivery ? ((humanDelivery.pending_intents || 0) + '/' + (humanDelivery.active_bindings || 0)) : '-', sub: humanDelivery ? ('mode=' + (humanDelivery.mode || 'projection_outbox') + ' unresolved=' + (humanDelivery.unresolved_intents || 0) + ' failed=' + (humanDelivery.failed_intents || 0)) : 'binding-based human notification state' },
|
|
782
1058
|
{ label: 'Recommendations', value: overview.recommendationSummary ? overview.recommendationSummary.total : overview.recommendations.length, sub: overview.recommendationSummary ? JSON.stringify(overview.recommendationSummary.by_status || {}) : 'learned policy suggestions' },
|
|
783
1059
|
{ 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' },
|
|
784
1060
|
];
|
|
785
1061
|
document.getElementById('statsGrid').innerHTML = stats.map(function (item) {
|
|
786
1062
|
return '<div class="card"><div class="label">' + esc(item.label) + '</div><div class="value">' + esc(item.value) + '</div><div class="muted small">' + esc(item.sub) + '</div></div>';
|
|
787
1063
|
}).join('');
|
|
1064
|
+
document.getElementById('taskList').innerHTML = '<div class="task-row"><div class="top"><strong>Human Delivery Channels</strong><span class="badge">' + esc((humanDelivery && humanDelivery.supported_channels ? humanDelivery.supported_channels.length : 0) + '/' + (humanDelivery && humanDelivery.channel_capabilities ? humanDelivery.channel_capabilities.length : 0)) + '</span></div>' +
|
|
1065
|
+
'<div class="muted small">Capabilities and canary state for explicit human-delivery replies.</div>' +
|
|
1066
|
+
'<div class="audit-list" style="margin-top:8px">' + renderHumanDeliveryCapabilities(humanDelivery) + '</div></div>';
|
|
788
1067
|
|
|
789
1068
|
const toggleUnreadBtn = document.getElementById('toggleUnreadBtn');
|
|
790
1069
|
if (toggleUnreadBtn) toggleUnreadBtn.textContent = 'Unread only: ' + (state.showUnreadOnly ? 'on' : 'off');
|
|
@@ -796,6 +1075,9 @@ function getHostPanelHtml() {
|
|
|
796
1075
|
const active = session.session_key === state.selectedSessionKey ? ' active' : '';
|
|
797
1076
|
const badges = [
|
|
798
1077
|
'<span class="badge ' + esc(session.trust_state) + '">' + esc(session.trust_state) + '</span>',
|
|
1078
|
+
(countSinceLastSeen(session.since_last_seen) > 0
|
|
1079
|
+
? '<span class="badge alert">new ' + esc(countSinceLastSeen(session.since_last_seen)) + '</span>'
|
|
1080
|
+
: ''),
|
|
799
1081
|
session.binding_alert
|
|
800
1082
|
? '<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>'
|
|
801
1083
|
: '',
|
|
@@ -833,8 +1115,27 @@ function getHostPanelHtml() {
|
|
|
833
1115
|
});
|
|
834
1116
|
}
|
|
835
1117
|
|
|
1118
|
+
document.querySelectorAll('.transport-switch-btn').forEach(function (btn) {
|
|
1119
|
+
btn.addEventListener('click', async function () {
|
|
1120
|
+
const mode = btn.getAttribute('data-mode');
|
|
1121
|
+
if (mode !== 'bridge' && mode !== 'channel') return;
|
|
1122
|
+
const confirmed = window.confirm('Switch preferred transport to ' + mode + ' and request a managed restart?');
|
|
1123
|
+
if (!confirmed) return;
|
|
1124
|
+
const result = await api('/api/runtime/transport-switch', {
|
|
1125
|
+
method: 'POST',
|
|
1126
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1127
|
+
body: JSON.stringify({ mode: mode }),
|
|
1128
|
+
});
|
|
1129
|
+
window.alert(result && result.result && result.result.restarted
|
|
1130
|
+
? ('Preferred transport switched to ' + mode + '. Managed restart was attempted.')
|
|
1131
|
+
: ('Preferred transport switched to ' + mode + '. Restart is still required.'));
|
|
1132
|
+
await refreshAll();
|
|
1133
|
+
setTab('runtime');
|
|
1134
|
+
});
|
|
1135
|
+
});
|
|
1136
|
+
|
|
836
1137
|
const tasks = Array.isArray(overview.tasks) ? overview.tasks : [];
|
|
837
|
-
document.getElementById('taskList').innerHTML
|
|
1138
|
+
document.getElementById('taskList').innerHTML += tasks.length
|
|
838
1139
|
? tasks.map(function (task) {
|
|
839
1140
|
return '<div class="task-row"><div class="top"><strong>' + esc(task.title || task.task_id) + '</strong><span class="badge">' + esc(task.status) + '</span></div>' +
|
|
840
1141
|
'<div class="muted small">task_id=' + esc(task.task_id) + ' \xB7 session=' + esc(task.session_key) + '</div>' +
|
|
@@ -843,7 +1144,7 @@ function getHostPanelHtml() {
|
|
|
843
1144
|
(task.error_message ? '<div style="margin-top:8px;color:#fca5a5">' + esc(task.error_message) + '</div>' : '') +
|
|
844
1145
|
renderHandoffBlock(task.handoff) +
|
|
845
1146
|
'</div>';
|
|
846
|
-
|
|
1147
|
+
}).join('')
|
|
847
1148
|
: '<div class="empty">No recent task threads.</div>';
|
|
848
1149
|
|
|
849
1150
|
if (subscription) {
|
|
@@ -937,6 +1238,13 @@ conversation=' + result.conversation_id));
|
|
|
937
1238
|
? detail.collaborationProjection.preset
|
|
938
1239
|
: 'balanced';
|
|
939
1240
|
const projectionOutbox = Array.isArray(detail.projectionOutbox) ? detail.projectionOutbox : [];
|
|
1241
|
+
const recentBindings = Array.isArray(detail.recentBindings) ? detail.recentBindings : [];
|
|
1242
|
+
const recentNotificationIntents = Array.isArray(detail.recentNotificationIntents) ? detail.recentNotificationIntents : [];
|
|
1243
|
+
const sinceLastSeen = detail.sinceLastSeen || null;
|
|
1244
|
+
const deliveryTimeline = Array.isArray(detail.deliveryTimeline) ? detail.deliveryTimeline : [];
|
|
1245
|
+
const projectionPreview = detail.projectionPreview || null;
|
|
1246
|
+
const transportHealth = detail.transportHealth || null;
|
|
1247
|
+
const humanDelivery = detail.humanDelivery || null;
|
|
940
1248
|
const isAdvanced = state.detailMode === 'advanced';
|
|
941
1249
|
const sessionLink = buildSessionLink(session.session_key);
|
|
942
1250
|
const summaryPills = [];
|
|
@@ -972,7 +1280,8 @@ conversation=' + result.conversation_id));
|
|
|
972
1280
|
: (reopenRecommendation ? '<div class="muted small" style="margin-top:8px">trust_action=' + esc(recommendationActionLabel(reopenRecommendation)) + '</div>' : '')) +
|
|
973
1281
|
(summaryPills.length ? '<div class="summary-pills">' + summaryPills.join('') + '</div>' : '') +
|
|
974
1282
|
(pendingCollaborationEvents.length
|
|
975
|
-
? '<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
|
|
1283
|
+
? '<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.' +
|
|
1284
|
+
(pendingCollaborationEvents[0].overdue ? ' This item is overdue.' : '') + '</div></div>'
|
|
976
1285
|
: '') +
|
|
977
1286
|
(bindingAlert
|
|
978
1287
|
? '<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>'
|
|
@@ -1084,15 +1393,49 @@ conversation=' + result.conversation_id));
|
|
|
1084
1393
|
'</div></div>' +
|
|
1085
1394
|
'<div><div class="label">Human Thread Posture</div><div class="audit-list" style="margin-top:8px">' +
|
|
1086
1395
|
'<div class="audit-row"><div class="top"><strong>Projection policy</strong><span class="badge">' + esc(projectionPreset) + '</span></div>' +
|
|
1087
|
-
|
|
1088
|
-
|
|
1396
|
+
'<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>' +
|
|
1397
|
+
'<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>' +
|
|
1398
|
+
(sinceLastSeen
|
|
1399
|
+
? '<div class="summary-pills" style="margin-top:8px">' +
|
|
1400
|
+
'<span class="pill">new=' + esc(countSinceLastSeen(sinceLastSeen)) + '</span>' +
|
|
1401
|
+
'<span class="pill">decisions=' + esc(sinceLastSeen.new_decisions || 0) + '</span>' +
|
|
1402
|
+
'<span class="pill">failures=' + esc(sinceLastSeen.new_failures || 0) + '</span>' +
|
|
1403
|
+
'</div>'
|
|
1404
|
+
: '') +
|
|
1405
|
+
(transportHealth
|
|
1406
|
+
? '<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>'
|
|
1407
|
+
: '') +
|
|
1408
|
+
(humanDelivery
|
|
1409
|
+
? '<div class="muted small" style="margin-top:8px">human_delivery=' + esc(humanDelivery.mode || 'projection_outbox') + ' \xB7 unresolved=' + esc(humanDelivery.unresolved_intents || 0) + ' \xB7 failed=' + esc(humanDelivery.failed_intents || 0) + '</div>'
|
|
1410
|
+
: '') +
|
|
1089
1411
|
(projectionOutbox.length
|
|
1090
1412
|
? '<div class="muted small" style="margin-top:8px">outbox=' + esc(projectionOutbox[0].status || 'pending') + ' \xB7 target=' + esc(projectionOutbox[0].target_human_session || '(missing)') + '</div>'
|
|
1091
1413
|
: '<div class="muted small" style="margin-top:8px">outbox=clear</div>') +
|
|
1414
|
+
'<div style="margin-top:12px"><div class="label">Projection Preview</div><div style="margin-top:8px">' + renderProjectionPreviewBlock(projectionPreview) + '</div></div>' +
|
|
1092
1415
|
'</div>' +
|
|
1093
1416
|
'</div></div>' +
|
|
1094
1417
|
'</div>';
|
|
1095
1418
|
|
|
1419
|
+
el.innerHTML +=
|
|
1420
|
+
'<div class="grid two-col" style="margin-top:16px">' +
|
|
1421
|
+
'<div><div class="label">Since Last Seen</div><div class="audit-list" style="margin-top:8px">' +
|
|
1422
|
+
'<div class="audit-row">' + renderSinceLastSeenSummary(sinceLastSeen) + '</div>' +
|
|
1423
|
+
'</div></div>' +
|
|
1424
|
+
'<div><div class="label">Delivery Timeline</div><div class="audit-list" style="margin-top:8px">' +
|
|
1425
|
+
renderDeliveryTimeline(deliveryTimeline) +
|
|
1426
|
+
'</div></div>' +
|
|
1427
|
+
'</div>';
|
|
1428
|
+
|
|
1429
|
+
el.innerHTML +=
|
|
1430
|
+
'<div class="grid two-col" style="margin-top:16px">' +
|
|
1431
|
+
'<div><div class="label">Human Reply Targets</div><div class="audit-list" style="margin-top:8px">' +
|
|
1432
|
+
renderRecentBindings(recentBindings) +
|
|
1433
|
+
'</div></div>' +
|
|
1434
|
+
'<div><div class="label">Notification Intents</div><div class="audit-list" style="margin-top:8px">' +
|
|
1435
|
+
renderHumanDeliveryIntents(recentNotificationIntents, 'No notification intents for this session yet.') +
|
|
1436
|
+
'</div></div>' +
|
|
1437
|
+
'</div>';
|
|
1438
|
+
|
|
1096
1439
|
el.querySelectorAll('.approve-session-btn').forEach(function (btn) {
|
|
1097
1440
|
btn.addEventListener('click', async function () {
|
|
1098
1441
|
const sessionKey = btn.getAttribute('data-session');
|
|
@@ -1233,6 +1576,7 @@ conversation=' + result.conversation_id));
|
|
|
1233
1576
|
setTab('runtime');
|
|
1234
1577
|
});
|
|
1235
1578
|
}
|
|
1579
|
+
wireHumanDeliveryActionButtons(el);
|
|
1236
1580
|
}
|
|
1237
1581
|
|
|
1238
1582
|
async function promptBindCurrentChat(conversationId, previousBinding, remoteDid) {
|
|
@@ -1272,6 +1616,7 @@ Previous chat link: ' + previous
|
|
|
1272
1616
|
policy.doc && policy.doc.collaboration_projection && policy.doc.collaboration_projection.preset
|
|
1273
1617
|
? policy.doc.collaboration_projection.preset
|
|
1274
1618
|
: 'balanced';
|
|
1619
|
+
document.getElementById('projectionPolicyPreview').innerHTML = renderProjectionPreviewBlock(state.session && state.session.projectionPreview ? state.session.projectionPreview : null);
|
|
1275
1620
|
document.getElementById('contactDefault').value = policy.doc.contact_policy.default_action;
|
|
1276
1621
|
document.getElementById('taskDefault').value = policy.doc.task_policy.default_action;
|
|
1277
1622
|
document.getElementById('profileDisplayName').value = profile && profile.display_name ? profile.display_name : '';
|
|
@@ -1443,6 +1788,7 @@ Previous chat link: ' + previous
|
|
|
1443
1788
|
syncSelectedSessionFromOverview();
|
|
1444
1789
|
renderHeader();
|
|
1445
1790
|
renderOverview();
|
|
1791
|
+
if (state.currentTab === 'runtime') await maybeMarkGlobalSeen('runtime');
|
|
1446
1792
|
if (state.selectedSessionKey) {
|
|
1447
1793
|
await loadSession(state.selectedSessionKey);
|
|
1448
1794
|
} else {
|
|
@@ -1454,10 +1800,14 @@ Previous chat link: ' + previous
|
|
|
1454
1800
|
|
|
1455
1801
|
async function loadSession(sessionKey) {
|
|
1456
1802
|
if (!sessionKey) return;
|
|
1803
|
+
const previousSessionKey = state.selectedSessionKey;
|
|
1457
1804
|
state.selectedSessionKey = sessionKey;
|
|
1458
1805
|
state.session = await api('/api/runtime/session?session_key=' + encodeURIComponent(sessionKey));
|
|
1459
1806
|
syncUrlState();
|
|
1460
1807
|
renderSession();
|
|
1808
|
+
if (state.currentTab === 'runtime') {
|
|
1809
|
+
await maybeMarkSessionSeen(sessionKey, previousSessionKey !== sessionKey);
|
|
1810
|
+
}
|
|
1461
1811
|
}
|
|
1462
1812
|
|
|
1463
1813
|
async function loadPolicy() {
|
|
@@ -1468,6 +1818,7 @@ Previous chat link: ' + previous
|
|
|
1468
1818
|
async function loadDecisions() {
|
|
1469
1819
|
state.decisions = await api('/api/runtime/collaboration-decisions');
|
|
1470
1820
|
renderDecisionInbox();
|
|
1821
|
+
if (state.currentTab === 'decisions') await maybeMarkGlobalSeen('decisions');
|
|
1471
1822
|
}
|
|
1472
1823
|
|
|
1473
1824
|
async function refreshAll() {
|
|
@@ -1897,6 +2248,50 @@ function readProjectionOutboxState(storePath, limit = 20) {
|
|
|
1897
2248
|
store.close();
|
|
1898
2249
|
}
|
|
1899
2250
|
}
|
|
2251
|
+
function readTransportHealthState(storePath) {
|
|
2252
|
+
const store = new LocalStore(storePath);
|
|
2253
|
+
try {
|
|
2254
|
+
const eventManager = new CollaborationEventManager(store);
|
|
2255
|
+
const outboxManager = new CollaborationProjectionOutboxManager(store);
|
|
2256
|
+
const runtimeStatus = readIngressRuntimeStatus();
|
|
2257
|
+
const preference = readTransportPreference();
|
|
2258
|
+
const normalizedStatus = runtimeStatus ? {
|
|
2259
|
+
...runtimeStatus,
|
|
2260
|
+
preferred_transport_mode: preference?.preferred_mode ?? runtimeStatus.preferred_transport_mode ?? "bridge"
|
|
2261
|
+
} : {
|
|
2262
|
+
receive_mode: "webhook",
|
|
2263
|
+
transport_mode: preference?.preferred_mode ?? "bridge",
|
|
2264
|
+
preferred_transport_mode: preference?.preferred_mode ?? "bridge"
|
|
2265
|
+
};
|
|
2266
|
+
return deriveTransportHealth({
|
|
2267
|
+
runtime_status: normalizedStatus,
|
|
2268
|
+
recent_events: eventManager.listRecent(30),
|
|
2269
|
+
projection_outbox_failed: outboxManager.listByStatus("failed", 20)
|
|
2270
|
+
});
|
|
2271
|
+
} finally {
|
|
2272
|
+
store.close();
|
|
2273
|
+
}
|
|
2274
|
+
}
|
|
2275
|
+
function readSinceLastSeenState(storePath, operatorId, scopeType, scopeKey) {
|
|
2276
|
+
const store = new LocalStore(storePath);
|
|
2277
|
+
try {
|
|
2278
|
+
return summarizeSinceLastSeen(store, {
|
|
2279
|
+
operator_id: operatorId,
|
|
2280
|
+
scope_type: scopeType,
|
|
2281
|
+
scope_key: scopeKey
|
|
2282
|
+
});
|
|
2283
|
+
} finally {
|
|
2284
|
+
store.close();
|
|
2285
|
+
}
|
|
2286
|
+
}
|
|
2287
|
+
function readHumanDeliveryState(storePath, limit = 12) {
|
|
2288
|
+
const store = new LocalStore(storePath);
|
|
2289
|
+
try {
|
|
2290
|
+
return summarizeHumanDelivery(store, limit);
|
|
2291
|
+
} finally {
|
|
2292
|
+
store.close();
|
|
2293
|
+
}
|
|
2294
|
+
}
|
|
1900
2295
|
function buildPolicyDecisionShape(identityPath, remoteDid, opts) {
|
|
1901
2296
|
const policy = readTrustPolicyDoc(identityPath);
|
|
1902
2297
|
const runtimeMode = opts?.runtimeMode ?? getRuntimeMode();
|
|
@@ -2009,6 +2404,25 @@ async function buildRuntimeOverviewPayload(ctx) {
|
|
|
2009
2404
|
}, {});
|
|
2010
2405
|
const collaborationSummary = collaborationEventManager.summarize(200);
|
|
2011
2406
|
const projectionOutbox = readProjectionOutboxState(ctx.storePath, 20);
|
|
2407
|
+
const transportHealth = readTransportHealthState(ctx.storePath);
|
|
2408
|
+
const sinceLastSeen = readSinceLastSeenState(ctx.storePath, "host_panel", "global");
|
|
2409
|
+
const humanDelivery = readHumanDeliveryState(ctx.storePath, 20);
|
|
2410
|
+
const decisionStore = new LocalStore(ctx.storePath);
|
|
2411
|
+
let decisionViews = [];
|
|
2412
|
+
let humanDeliveryDetails = null;
|
|
2413
|
+
try {
|
|
2414
|
+
decisionViews = listPendingDecisionViews(decisionStore, 100);
|
|
2415
|
+
const bindingManager = new HumanDeliveryBindingManager(decisionStore);
|
|
2416
|
+
const intentManager = new NotificationIntentManager(decisionStore);
|
|
2417
|
+
humanDeliveryDetails = {
|
|
2418
|
+
unresolved_intents: intentManager.listByStatus("unresolved", 20),
|
|
2419
|
+
failed_intents: intentManager.listByStatus("failed", 20),
|
|
2420
|
+
acknowledged_intents: intentManager.listByStatus("acknowledged", 20),
|
|
2421
|
+
recent_bindings: bindingManager.listRecent(20)
|
|
2422
|
+
};
|
|
2423
|
+
} finally {
|
|
2424
|
+
decisionStore.close();
|
|
2425
|
+
}
|
|
2012
2426
|
return {
|
|
2013
2427
|
did: ctx.myDid,
|
|
2014
2428
|
serverUrl: ctx.serverUrl,
|
|
@@ -2046,6 +2460,11 @@ async function buildRuntimeOverviewPayload(ctx) {
|
|
|
2046
2460
|
recommendationSummary: recommendationState.summary,
|
|
2047
2461
|
collaborationSummary,
|
|
2048
2462
|
projectionOutbox,
|
|
2463
|
+
transportHealth,
|
|
2464
|
+
sinceLastSeen,
|
|
2465
|
+
humanDelivery,
|
|
2466
|
+
humanDeliveryDetails,
|
|
2467
|
+
pendingDecisions: decisionViews,
|
|
2049
2468
|
recentCollaborationEvents: collaborationEventManager.listRecent(20),
|
|
2050
2469
|
sessions: sessions.map((session) => ({
|
|
2051
2470
|
...session,
|
|
@@ -2054,7 +2473,8 @@ async function buildRuntimeOverviewPayload(ctx) {
|
|
|
2054
2473
|
binding_alert: session.conversation_id ? bindingAlertByConversation.get(session.conversation_id) ?? null : null,
|
|
2055
2474
|
is_active_work_session: session.session_key === activeWorkSession,
|
|
2056
2475
|
latest_messages: session.conversation_id ? historyManager.listRecent(session.conversation_id, 3) : [],
|
|
2057
|
-
collaboration_events: collaborationEventManager.listBySession(session.session_key, 3)
|
|
2476
|
+
collaboration_events: collaborationEventManager.listBySession(session.session_key, 3),
|
|
2477
|
+
since_last_seen: readSinceLastSeenState(ctx.storePath, "host_panel", "session", session.session_key)
|
|
2058
2478
|
})),
|
|
2059
2479
|
tasks: refreshedTasks.map((task) => ({
|
|
2060
2480
|
...task,
|
|
@@ -2111,8 +2531,31 @@ async function buildSessionOverviewPayload(ctx, sessionKey) {
|
|
|
2111
2531
|
});
|
|
2112
2532
|
const outboxStore = new LocalStore(ctx.storePath);
|
|
2113
2533
|
let projectionOutbox = [];
|
|
2534
|
+
let notificationIntents = [];
|
|
2535
|
+
let recentBindings = [];
|
|
2536
|
+
let sinceLastSeen = null;
|
|
2537
|
+
let deliveryTimeline = [];
|
|
2538
|
+
let projectionPreview = null;
|
|
2539
|
+
let pendingDecisionViews = [];
|
|
2540
|
+
let humanDelivery = null;
|
|
2114
2541
|
try {
|
|
2115
2542
|
projectionOutbox = new CollaborationProjectionOutboxManager(outboxStore).listBySession(session.session_key, 20);
|
|
2543
|
+
notificationIntents = new NotificationIntentManager(outboxStore).listBySession(session.session_key, 20);
|
|
2544
|
+
recentBindings = listRecentBindingsForSession(outboxStore, session.session_key, session.conversation_id, 20);
|
|
2545
|
+
sinceLastSeen = summarizeSinceLastSeen(outboxStore, {
|
|
2546
|
+
operator_id: "host_panel",
|
|
2547
|
+
scope_type: "session",
|
|
2548
|
+
scope_key: session.session_key
|
|
2549
|
+
});
|
|
2550
|
+
deliveryTimeline = buildDeliveryTimeline(outboxStore, session.session_key, 40);
|
|
2551
|
+
projectionPreview = buildProjectionPreview(
|
|
2552
|
+
outboxStore,
|
|
2553
|
+
session.session_key,
|
|
2554
|
+
policy.collaboration_projection.preset,
|
|
2555
|
+
5
|
|
2556
|
+
);
|
|
2557
|
+
pendingDecisionViews = listPendingDecisionViews(outboxStore, 100).filter((event) => event.session_key === session.session_key).slice(0, 20);
|
|
2558
|
+
humanDelivery = summarizeHumanDelivery(outboxStore, 20);
|
|
2116
2559
|
} finally {
|
|
2117
2560
|
outboxStore.close();
|
|
2118
2561
|
}
|
|
@@ -2120,9 +2563,10 @@ async function buildSessionOverviewPayload(ctx, sessionKey) {
|
|
|
2120
2563
|
session,
|
|
2121
2564
|
sessionSummary: sessionSummaryManager.get(session.session_key),
|
|
2122
2565
|
collaborationEvents: collaborationEventManager.listBySession(session.session_key, 40),
|
|
2123
|
-
pendingCollaborationEvents:
|
|
2566
|
+
pendingCollaborationEvents: pendingDecisionViews,
|
|
2124
2567
|
collaborationSummary: collaborationEventManager.summarize(200),
|
|
2125
2568
|
ingressRuntime: readIngressRuntimeStatus(),
|
|
2569
|
+
transportHealth: readTransportHealthState(ctx.storePath),
|
|
2126
2570
|
binding,
|
|
2127
2571
|
bindingAlert,
|
|
2128
2572
|
activeWorkSession,
|
|
@@ -2131,6 +2575,12 @@ async function buildSessionOverviewPayload(ctx, sessionKey) {
|
|
|
2131
2575
|
sessionBindingAlertsPath: getSessionBindingAlertsFilePath(),
|
|
2132
2576
|
collaborationProjection: policy.collaboration_projection,
|
|
2133
2577
|
projectionOutbox,
|
|
2578
|
+
recentBindings,
|
|
2579
|
+
recentNotificationIntents: notificationIntents,
|
|
2580
|
+
sinceLastSeen,
|
|
2581
|
+
deliveryTimeline,
|
|
2582
|
+
projectionPreview,
|
|
2583
|
+
humanDelivery,
|
|
2134
2584
|
policyExplain: buildPolicyDecisionShape(ctx.identityPath, session.remote_did, { runtimeMode: getRuntimeMode() }),
|
|
2135
2585
|
tasks: tasks.map((task) => ({
|
|
2136
2586
|
...task,
|
|
@@ -2207,6 +2657,121 @@ async function handleApi(pathname, req, ctx) {
|
|
|
2207
2657
|
status: readIngressRuntimeStatus()
|
|
2208
2658
|
};
|
|
2209
2659
|
}
|
|
2660
|
+
if (parts[1] === "transport-health" && req.method === "GET") {
|
|
2661
|
+
return {
|
|
2662
|
+
transportHealth: readTransportHealthState(ctx.storePath),
|
|
2663
|
+
transportPreference: readTransportPreference()
|
|
2664
|
+
};
|
|
2665
|
+
}
|
|
2666
|
+
if (parts[1] === "human-delivery") {
|
|
2667
|
+
const store = new LocalStore(ctx.storePath);
|
|
2668
|
+
try {
|
|
2669
|
+
const bindingManager = new HumanDeliveryBindingManager(store);
|
|
2670
|
+
const intentManager = new NotificationIntentManager(store);
|
|
2671
|
+
if (req.method === "GET") {
|
|
2672
|
+
return {
|
|
2673
|
+
humanDelivery: summarizeHumanDelivery(store, 20),
|
|
2674
|
+
recentBindings: bindingManager.listRecent(50),
|
|
2675
|
+
unresolvedIntents: intentManager.listByStatus("unresolved", 50),
|
|
2676
|
+
failedIntents: intentManager.listByStatus("failed", 50),
|
|
2677
|
+
acknowledgedIntents: intentManager.listByStatus("acknowledged", 50)
|
|
2678
|
+
};
|
|
2679
|
+
}
|
|
2680
|
+
if (req.method === "POST" && parts[2] === "action") {
|
|
2681
|
+
const body = await readBody(req);
|
|
2682
|
+
const action = String(body?.action ?? "").trim();
|
|
2683
|
+
const intentId = Number(body?.intent_id);
|
|
2684
|
+
const bindingId = Number(body?.binding_id);
|
|
2685
|
+
let intent = null;
|
|
2686
|
+
let binding = null;
|
|
2687
|
+
if (action === "retry_intent") {
|
|
2688
|
+
if (!Number.isInteger(intentId) || intentId <= 0) throw new Error("intent_id is required");
|
|
2689
|
+
intent = intentManager.markPending(intentId, { clear_error: true, clear_ack: false, clear_binding: false });
|
|
2690
|
+
} else if (action === "re_resolve_intent_binding") {
|
|
2691
|
+
if (!Number.isInteger(intentId) || intentId <= 0) throw new Error("intent_id is required");
|
|
2692
|
+
intent = intentManager.markPending(intentId, { clear_error: true, clear_ack: false, clear_binding: true });
|
|
2693
|
+
} else if (action === "cancel_intent") {
|
|
2694
|
+
if (!Number.isInteger(intentId) || intentId <= 0) throw new Error("intent_id is required");
|
|
2695
|
+
intent = intentManager.markCanceled(intentId, "manual_cancel");
|
|
2696
|
+
} else if (action === "mark_binding_stale") {
|
|
2697
|
+
if (!Number.isInteger(bindingId) || bindingId <= 0) throw new Error("binding_id is required");
|
|
2698
|
+
binding = bindingManager.markStale(bindingId);
|
|
2699
|
+
} else {
|
|
2700
|
+
throw new Error("action must be retry_intent, re_resolve_intent_binding, cancel_intent, or mark_binding_stale");
|
|
2701
|
+
}
|
|
2702
|
+
return {
|
|
2703
|
+
ok: true,
|
|
2704
|
+
action,
|
|
2705
|
+
intent,
|
|
2706
|
+
binding,
|
|
2707
|
+
humanDelivery: summarizeHumanDelivery(store, 20),
|
|
2708
|
+
unresolvedIntents: intentManager.listByStatus("unresolved", 50),
|
|
2709
|
+
failedIntents: intentManager.listByStatus("failed", 50),
|
|
2710
|
+
acknowledgedIntents: intentManager.listByStatus("acknowledged", 50)
|
|
2711
|
+
};
|
|
2712
|
+
}
|
|
2713
|
+
} finally {
|
|
2714
|
+
store.close();
|
|
2715
|
+
}
|
|
2716
|
+
}
|
|
2717
|
+
if (parts[1] === "transport-switch" && req.method === "POST") {
|
|
2718
|
+
const body = await readBody(req);
|
|
2719
|
+
const mode = String(body?.mode ?? "").trim().toLowerCase();
|
|
2720
|
+
if (mode !== "bridge" && mode !== "channel") throw new Error("mode must be bridge or channel");
|
|
2721
|
+
const result = switchTransportPreference(mode, {
|
|
2722
|
+
updated_by: "host_panel"
|
|
2723
|
+
});
|
|
2724
|
+
const store = new LocalStore(ctx.storePath);
|
|
2725
|
+
try {
|
|
2726
|
+
new CollaborationEventManager(store).record({
|
|
2727
|
+
event_type: "transport_switched",
|
|
2728
|
+
severity: result.restarted ? "notice" : "warning",
|
|
2729
|
+
summary: result.restarted ? `Preferred transport switched to ${mode}. A managed restart was attempted.` : `Preferred transport switched to ${mode}. Restart is still required.`,
|
|
2730
|
+
detail: {
|
|
2731
|
+
preferred_mode: result.preferred_mode,
|
|
2732
|
+
restart_required: result.restart_required,
|
|
2733
|
+
restart_method: result.restart_method ?? null,
|
|
2734
|
+
restart_error: result.restart_error ?? null,
|
|
2735
|
+
preference_path: result.preference_path
|
|
2736
|
+
}
|
|
2737
|
+
});
|
|
2738
|
+
} finally {
|
|
2739
|
+
store.close();
|
|
2740
|
+
}
|
|
2741
|
+
return {
|
|
2742
|
+
ok: true,
|
|
2743
|
+
result,
|
|
2744
|
+
transportHealth: readTransportHealthState(ctx.storePath)
|
|
2745
|
+
};
|
|
2746
|
+
}
|
|
2747
|
+
if (parts[1] === "seen" && req.method === "POST") {
|
|
2748
|
+
const body = await readBody(req);
|
|
2749
|
+
const operatorId = String(body?.operator_id ?? "host_panel").trim();
|
|
2750
|
+
const scopeType = String(body?.scope_type ?? "global").trim();
|
|
2751
|
+
const scopeKey = typeof body?.scope_key === "string" ? body.scope_key.trim() : void 0;
|
|
2752
|
+
if (!["host_panel", "tui", "mcp"].includes(operatorId)) throw new Error("operator_id must be host_panel, tui, or mcp");
|
|
2753
|
+
if (scopeType !== "global" && scopeType !== "session") throw new Error("scope_type must be global or session");
|
|
2754
|
+
const store = new LocalStore(ctx.storePath);
|
|
2755
|
+
try {
|
|
2756
|
+
const seen = new OperatorSeenStateManager(store).markSeen({
|
|
2757
|
+
operator_id: operatorId,
|
|
2758
|
+
scope_type: scopeType,
|
|
2759
|
+
scope_key: scopeType === "session" ? scopeKey : null,
|
|
2760
|
+
last_seen_ts: Date.now()
|
|
2761
|
+
});
|
|
2762
|
+
return {
|
|
2763
|
+
ok: true,
|
|
2764
|
+
seen,
|
|
2765
|
+
summary: summarizeSinceLastSeen(store, {
|
|
2766
|
+
operator_id: operatorId,
|
|
2767
|
+
scope_type: scopeType,
|
|
2768
|
+
scope_key: scopeType === "session" ? scopeKey : null
|
|
2769
|
+
})
|
|
2770
|
+
};
|
|
2771
|
+
} finally {
|
|
2772
|
+
store.close();
|
|
2773
|
+
}
|
|
2774
|
+
}
|
|
2210
2775
|
if (parts[1] === "demo" && req.method === "POST") {
|
|
2211
2776
|
const body = await readBody(req);
|
|
2212
2777
|
const preset = typeof body?.preset === "string" ? body.preset.trim().toLowerCase() : "";
|
|
@@ -2262,11 +2827,16 @@ async function handleApi(pathname, req, ctx) {
|
|
|
2262
2827
|
const manager = new CollaborationEventManager(store);
|
|
2263
2828
|
const outboxManager = new CollaborationProjectionOutboxManager(store);
|
|
2264
2829
|
if (req.method === "GET") {
|
|
2265
|
-
const pendingDecisions =
|
|
2830
|
+
const pendingDecisions = listPendingDecisionViews(store, 100);
|
|
2831
|
+
const intentManager = new NotificationIntentManager(store);
|
|
2266
2832
|
return {
|
|
2267
2833
|
pendingDecisions,
|
|
2268
2834
|
projectionOutboxPending: outboxManager.listByStatus("pending", 50),
|
|
2269
|
-
projectionOutboxFailed: outboxManager.listByStatus("failed", 50)
|
|
2835
|
+
projectionOutboxFailed: outboxManager.listByStatus("failed", 50),
|
|
2836
|
+
notificationIntentsPending: intentManager.listByStatus("pending", 50).concat(intentManager.listByStatus("bound", 50)),
|
|
2837
|
+
notificationIntentsUnresolved: intentManager.listByStatus("unresolved", 50),
|
|
2838
|
+
notificationIntentsFailed: intentManager.listByStatus("failed", 50),
|
|
2839
|
+
humanDelivery: summarizeHumanDelivery(store, 20)
|
|
2270
2840
|
};
|
|
2271
2841
|
}
|
|
2272
2842
|
if (parts[2] === "resolve" && req.method === "POST") {
|