@dollhousemcp/mcp-server 2.0.13 → 2.0.14

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.
Files changed (35) hide show
  1. package/CHANGELOG.md +8 -0
  2. package/dist/di/Container.d.ts.map +1 -1
  3. package/dist/di/Container.js +19 -9
  4. package/dist/elements/BaseElement.js +2 -2
  5. package/dist/elements/memories/Memory.d.ts.map +1 -1
  6. package/dist/elements/memories/Memory.js +3 -3
  7. package/dist/elements/skills/Skill.d.ts.map +1 -1
  8. package/dist/elements/skills/Skill.js +4 -4
  9. package/dist/elements/templates/Template.d.ts.map +1 -1
  10. package/dist/elements/templates/Template.js +4 -4
  11. package/dist/generated/version.d.ts +2 -2
  12. package/dist/generated/version.js +3 -3
  13. package/dist/handlers/ElementCRUDHandler.d.ts +10 -0
  14. package/dist/handlers/ElementCRUDHandler.d.ts.map +1 -1
  15. package/dist/handlers/ElementCRUDHandler.js +123 -1
  16. package/dist/handlers/mcp-aql/MCPAQLHandler.d.ts +1 -0
  17. package/dist/handlers/mcp-aql/MCPAQLHandler.d.ts.map +1 -1
  18. package/dist/handlers/mcp-aql/MCPAQLHandler.js +31 -2
  19. package/dist/services/ActivationStore.d.ts +20 -0
  20. package/dist/services/ActivationStore.d.ts.map +1 -1
  21. package/dist/services/ActivationStore.js +104 -1
  22. package/dist/web/console/IngestRoutes.d.ts +1 -0
  23. package/dist/web/console/IngestRoutes.d.ts.map +1 -1
  24. package/dist/web/console/IngestRoutes.js +4 -1
  25. package/dist/web/console/UnifiedConsole.js +2 -1
  26. package/dist/web/public/permissions.css +224 -16
  27. package/dist/web/public/permissions.js +326 -63
  28. package/dist/web/public/sessions.js +218 -98
  29. package/dist/web/public/styles.css +15 -10
  30. package/dist/web/routes/permissionRoutes.d.ts.map +1 -1
  31. package/dist/web/routes/permissionRoutes.js +57 -19
  32. package/dist/web/server.d.ts.map +1 -1
  33. package/dist/web/server.js +2 -1
  34. package/package.json +1 -1
  35. package/server.json +2 -2
@@ -15,6 +15,16 @@
15
15
  const POLL_INTERVAL_MS = 3000;
16
16
  let initialized = false;
17
17
  let lastDecisionId = null;
18
+ let latestAggregateData = null;
19
+ let latestSelectedData = null;
20
+ let latestPollRequestId = 0;
21
+
22
+ async function fetchPermissionStatus(sessionId) {
23
+ const query = sessionId ? `?sessionId=${encodeURIComponent(sessionId)}` : '';
24
+ const res = await DollhouseAuth.apiFetch(`/api/permissions/status${query}`);
25
+ if (!res.ok) throw new Error(`HTTP ${res.status}`);
26
+ return res.json();
27
+ }
18
28
 
19
29
  // ── Public API ─────────────────────────────────────────────
20
30
 
@@ -23,6 +33,7 @@
23
33
  init: initPermissions,
24
34
  destroy: destroyPermissions,
25
35
  refresh: function () { poll(); },
36
+ onSessionChange: function () { renderFromCache(); },
26
37
  };
27
38
 
28
39
  // Hook into tab switching — Todd's app.js lazyInitTab only knows logs/metrics,
@@ -69,22 +80,44 @@
69
80
  // ── Polling ────────────────────────────────────────────────
70
81
 
71
82
  async function poll() {
83
+ const requestId = ++latestPollRequestId;
72
84
  try {
73
- const res = await DollhouseAuth.apiFetch('/api/permissions/status');
74
- if (!res.ok) throw new Error(`HTTP ${res.status}`);
75
- const data = await res.json();
76
- render(data);
85
+ const aggregateData = await fetchPermissionStatus('');
86
+ if (requestId !== latestPollRequestId) {
87
+ return;
88
+ }
89
+
90
+ const currentSessionId = window.DollhouseSessions?.getFilterSessionId?.() || '';
91
+ const selectedData = deriveSelectedSessionData(aggregateData, currentSessionId);
92
+
93
+ latestAggregateData = aggregateData;
94
+ latestSelectedData = selectedData;
95
+ window.DollhouseSessions?.setPolicySessions?.(aggregateData.knownSessions || []);
96
+ render(aggregateData, selectedData);
77
97
  } catch (err) {
78
98
  renderError(err.message);
79
99
  }
80
100
  }
81
101
 
102
+ function renderFromCache() {
103
+ if (!latestAggregateData) {
104
+ poll();
105
+ return;
106
+ }
107
+
108
+ const sessionId = window.DollhouseSessions?.getFilterSessionId?.() || '';
109
+ latestSelectedData = deriveSelectedSessionData(latestAggregateData, sessionId);
110
+ renderPolicySources(latestAggregateData, latestSelectedData);
111
+ renderSelectedSessionDetail(latestSelectedData);
112
+ }
113
+
82
114
  // ── Rendering ──────────────────────────────────────────────
83
115
 
84
- function render(data) {
116
+ function render(data, selectedData) {
85
117
  renderStatusBar(data);
86
118
  renderSummaryStats(data);
87
- renderPolicySources(data);
119
+ renderPolicySources(data, selectedData);
120
+ renderSelectedSessionDetail(selectedData);
88
121
  renderDenyPatterns(data);
89
122
  renderAllowPatterns(data);
90
123
  renderConfirmPatterns(data);
@@ -128,9 +161,9 @@
128
161
  }
129
162
 
130
163
  function renderSummaryStats(data) {
131
- setText('perm-stat-deny-count', data.denyPatterns?.length || 0);
132
- setText('perm-stat-allow-count', data.allowPatterns?.length || 0);
133
- setText('perm-stat-confirm-count', data.confirmPatterns?.length || 0);
164
+ setText('perm-stat-deny-count', getAggregatePatterns(data, 'denyPatterns').length);
165
+ setText('perm-stat-allow-count', getAggregatePatterns(data, 'allowPatterns').length);
166
+ setText('perm-stat-confirm-count', getAggregatePatterns(data, 'confirmPatterns').length);
134
167
  setText('perm-stat-decisions', data.recentDecisions?.length || 0);
135
168
 
136
169
  // Decision breakdown
@@ -143,18 +176,19 @@
143
176
  setText('perm-stat-asked', asked);
144
177
  }
145
178
 
146
- function renderPolicySources(data) {
179
+ function renderPolicySources(data, selectedData) {
147
180
  const list = document.getElementById('perm-source-list');
148
181
  if (!list) return;
149
182
 
150
183
  const elements = data.elements || [];
184
+ const selectedSessionId = selectedData?.sessionId;
151
185
  if (elements.length === 0) {
152
186
  list.innerHTML = '<li class="perm-pattern-empty">No active elements with policies</li>';
153
187
  return;
154
188
  }
155
189
 
156
190
  list.innerHTML = elements.map(el => `
157
- <li class="perm-source-item">
191
+ <li class="perm-source-item${elementMatchesSelected(el, selectedSessionId) ? ' perm-source-item--selected' : ''}">
158
192
  <span class="perm-source-type">${esc(el.type)}</span>
159
193
  <span class="perm-source-name">${esc(el.element_name)}</span>
160
194
  ${el.description ? `<span style="color:var(--ink-400);font-size:0.75rem;margin-left:auto">${esc(el.description)}</span>` : ''}
@@ -162,16 +196,69 @@
162
196
  `).join('');
163
197
  }
164
198
 
199
+ function renderSelectedSessionDetail(selectedData) {
200
+ const card = document.getElementById('perm-selected-card');
201
+ const title = document.getElementById('perm-selected-title');
202
+ const subtitle = document.getElementById('perm-selected-subtitle');
203
+ const badge = document.getElementById('perm-selected-badge');
204
+ const sourceList = document.getElementById('perm-selected-source-list');
205
+ const denyList = document.getElementById('perm-selected-deny-list');
206
+ const allowList = document.getElementById('perm-selected-allow-list');
207
+ const confirmList = document.getElementById('perm-selected-confirm-list');
208
+
209
+ if (!card || !title || !subtitle || !badge || !sourceList || !denyList || !allowList || !confirmList) {
210
+ return;
211
+ }
212
+
213
+ if (!selectedData?.sessionId) {
214
+ card.hidden = true;
215
+ return;
216
+ }
217
+
218
+ card.hidden = false;
219
+
220
+ const sessionInfo = window.DollhouseSessions?.getSelectableSessions?.()
221
+ ?.find(session => session.sessionId === selectedData.sessionId);
222
+ const sessionLabel = window.DollhouseSessions?.displayName?.(sessionInfo || selectedData.sessionId)
223
+ || selectedData.sessionId;
224
+ const policyOnly = !!sessionInfo?.isPolicyOnly;
225
+
226
+ title.textContent = `Selected Session: ${sessionLabel}`;
227
+ subtitle.textContent = policyOnly
228
+ ? `${selectedData.sessionId} is showing saved policy state from disk. This is not a live attached client.`
229
+ : `${selectedData.sessionId} is the current live policy view for this session. Decision activity is still shown in the All Sessions section below.`;
230
+
231
+ badge.hidden = !policyOnly;
232
+ if (policyOnly) {
233
+ badge.textContent = 'Persisted Policy State (Debug Info)';
234
+ }
235
+
236
+ const elements = selectedData.elements || [];
237
+ sourceList.innerHTML = elements.length === 0
238
+ ? '<li class="perm-pattern-empty">No policy-bearing elements found for this session</li>'
239
+ : elements.map(el => `
240
+ <li class="perm-source-item perm-source-item--detail">
241
+ <span class="perm-source-type">${esc(el.type)}</span>
242
+ <span class="perm-source-name">${esc(el.element_name)}</span>
243
+ ${el.description ? `<span style="color:var(--ink-400);font-size:0.75rem;margin-left:auto">${esc(el.description)}</span>` : ''}
244
+ </li>
245
+ `).join('');
246
+
247
+ renderPatternList('perm-selected-deny-list', selectedData.denyPatterns || [], 'deny');
248
+ renderPatternList('perm-selected-allow-list', selectedData.allowPatterns || [], 'allow');
249
+ renderPatternList('perm-selected-confirm-list', selectedData.confirmPatterns || [], 'confirm');
250
+ }
251
+
165
252
  function renderDenyPatterns(data) {
166
- renderPatternList('perm-deny-list', data.denyPatterns || [], 'deny');
253
+ renderPatternList('perm-deny-list', getAggregatePatterns(data, 'denyPatterns'), 'deny');
167
254
  }
168
255
 
169
256
  function renderAllowPatterns(data) {
170
- renderPatternList('perm-allow-list', data.allowPatterns || [], 'allow');
257
+ renderPatternList('perm-allow-list', getAggregatePatterns(data, 'allowPatterns'), 'allow');
171
258
  }
172
259
 
173
260
  function renderConfirmPatterns(data) {
174
- renderPatternList('perm-confirm-list', data.confirmPatterns || [], 'confirm');
261
+ renderPatternList('perm-confirm-list', getAggregatePatterns(data, 'confirmPatterns'), 'confirm');
175
262
  }
176
263
 
177
264
  function renderPatternList(elementId, patterns, type) {
@@ -193,11 +280,16 @@
193
280
 
194
281
  function renderLiveFeed(data) {
195
282
  const feed = document.getElementById('perm-feed');
283
+ const modalFeed = document.getElementById('perm-audit-modal-feed');
284
+ const modalCount = document.getElementById('perm-audit-modal-count');
196
285
  if (!feed) return;
197
286
 
198
287
  const decisions = data.recentDecisions || [];
199
288
  if (decisions.length === 0) {
200
- feed.innerHTML = '<div class="perm-feed-empty">No permission decisions yet. Waiting for tool calls...</div>';
289
+ const empty = '<div class="perm-feed-empty">No permission decisions yet. Waiting for tool calls...</div>';
290
+ feed.innerHTML = empty;
291
+ if (modalFeed) modalFeed.innerHTML = empty;
292
+ if (modalCount) modalCount.textContent = '0 captured entries';
201
293
  return;
202
294
  }
203
295
 
@@ -206,7 +298,7 @@
206
298
  if (latestId === lastDecisionId) return; // no change
207
299
  lastDecisionId = latestId;
208
300
 
209
- feed.innerHTML = decisions.map(d => {
301
+ const html = decisions.map(d => {
210
302
  const time = new Date(d.timestamp).toLocaleTimeString();
211
303
  const toolDisplay = d.tool_name === 'Bash'
212
304
  ? `Bash: ${esc(truncate(d.command || '', 60))}`
@@ -221,6 +313,52 @@
221
313
  </div>
222
314
  `;
223
315
  }).join('');
316
+
317
+ feed.innerHTML = html;
318
+ if (modalFeed) modalFeed.innerHTML = html;
319
+ if (modalCount) {
320
+ modalCount.textContent = `${decisions.length} captured ${decisions.length === 1 ? 'entry' : 'entries'}`;
321
+ }
322
+ }
323
+
324
+ function deriveSelectedSessionData(aggregateData, sessionId) {
325
+ if (!sessionId) return null;
326
+
327
+ const elements = (aggregateData?.elements || []).filter(function (element) {
328
+ return Array.isArray(element.sessionIds) && element.sessionIds.indexOf(sessionId) !== -1;
329
+ });
330
+
331
+ return {
332
+ sessionId: sessionId,
333
+ activeElementCount: elements.length,
334
+ hasAllowlist: elements.some(function (element) {
335
+ return Array.isArray(element.allowPatterns) && element.allowPatterns.length > 0;
336
+ }),
337
+ denyPatterns: flattenElementPatterns(elements, 'denyPatterns'),
338
+ allowPatterns: flattenElementPatterns(elements, 'allowPatterns'),
339
+ confirmPatterns: flattenElementPatterns(elements, 'confirmPatterns'),
340
+ elements: elements.map(function (element) {
341
+ return {
342
+ type: element.type,
343
+ element_name: element.element_name,
344
+ description: element.description,
345
+ };
346
+ }),
347
+ permissionPromptActive: !!aggregateData?.permissionPromptActive,
348
+ recentDecisions: aggregateData?.recentDecisions || [],
349
+ };
350
+ }
351
+
352
+ function flattenElementPatterns(elements, key) {
353
+ return elements.flatMap(function (element) {
354
+ return Array.isArray(element[key]) ? element[key] : [];
355
+ });
356
+ }
357
+
358
+ function getAggregatePatterns(data, key) {
359
+ const combined = Array.isArray(data && data[key]) ? data[key] : [];
360
+ const perElement = flattenElementPatterns((data && data.elements) || [], key);
361
+ return Array.from(new Set(combined.concat(perElement)));
224
362
  }
225
363
 
226
364
  // ── Dashboard HTML ─────────────────────────────────────────
@@ -246,8 +384,35 @@
246
384
 
247
385
  <div class="perm-dashboard">
248
386
 
387
+ <!-- All Sessions Live Decision Feed -->
388
+ <div class="perm-card perm-card--full" data-collapsed="false" id="perm-all-feed-card">
389
+ <div class="perm-card-header" role="button" tabindex="0" aria-expanded="true">
390
+ <h3 class="perm-card-title">All Sessions Live Decision Feed</h3>
391
+ <span class="perm-card-toggle" aria-hidden="true">&#9662;</span>
392
+ </div>
393
+ <div class="perm-card-body">
394
+ <div class="perm-selected-header perm-selected-header--compact">
395
+ <div>
396
+ <div class="perm-selected-subtitle">Aggregate audit stream across all sessions. Newest decisions appear first.</div>
397
+ </div>
398
+ <button
399
+ type="button"
400
+ class="perm-panel-action"
401
+ id="perm-feed-expand-btn"
402
+ aria-haspopup="dialog"
403
+ aria-controls="perm-audit-modal"
404
+ >
405
+ Open Audit View
406
+ </button>
407
+ </div>
408
+ <div class="perm-feed" id="perm-feed" role="log" aria-live="polite" aria-label="Permission decisions across all sessions">
409
+ <div class="perm-feed-empty">No permission decisions yet. Waiting for tool calls...</div>
410
+ </div>
411
+ </div>
412
+ </div>
413
+
249
414
  <!-- Summary Stats -->
250
- <div class="perm-card perm-card--full" data-collapsed="false">
415
+ <div class="perm-card perm-card--full" data-collapsed="false" id="perm-autonomy-card">
251
416
  <div class="perm-card-header" role="button" tabindex="0" aria-expanded="true">
252
417
  <h3 class="perm-card-title">Autonomy Overview</h3>
253
418
  <span class="perm-card-toggle" aria-hidden="true">&#9662;</span>
@@ -286,72 +451,115 @@
286
451
  </div>
287
452
  </div>
288
453
 
289
- <!-- Policy Sources -->
290
- <div class="perm-card" data-collapsed="false">
454
+ <!-- Selected Session Detail -->
455
+ <div class="perm-card perm-card--full" data-collapsed="false" id="perm-selected-card" hidden>
291
456
  <div class="perm-card-header" role="button" tabindex="0" aria-expanded="true">
292
- <h3 class="perm-card-title">Policy Sources</h3>
457
+ <h3 class="perm-card-title">Selected Session Detail</h3>
293
458
  <span class="perm-card-toggle" aria-hidden="true">&#9662;</span>
294
459
  </div>
295
460
  <div class="perm-card-body">
296
- <ul class="perm-source-list" id="perm-source-list">
297
- <li class="perm-pattern-empty">Loading...</li>
298
- </ul>
299
- </div>
300
- </div>
461
+ <div class="perm-selected-header">
462
+ <div>
463
+ <div class="perm-selected-title" id="perm-selected-title">Selected Session</div>
464
+ <div class="perm-selected-subtitle" id="perm-selected-subtitle"></div>
465
+ </div>
466
+ <span class="perm-selected-badge" id="perm-selected-badge" hidden>Persisted Policy State (Debug Info)</span>
467
+ </div>
301
468
 
302
- <!-- Deny Patterns -->
303
- <div class="perm-card" data-collapsed="false">
304
- <div class="perm-card-header" role="button" tabindex="0" aria-expanded="true">
305
- <h3 class="perm-card-title">Deny Patterns</h3>
306
- <span class="perm-card-toggle" aria-hidden="true">&#9662;</span>
307
- </div>
308
- <div class="perm-card-body">
309
- <ul class="perm-pattern-list" id="perm-deny-list">
310
- <li class="perm-pattern-empty">Loading...</li>
311
- </ul>
469
+ <div class="perm-selected-grid">
470
+ <div class="perm-selected-panel">
471
+ <h4 class="perm-selected-panel-title">Policy Sources</h4>
472
+ <ul class="perm-source-list" id="perm-selected-source-list">
473
+ <li class="perm-pattern-empty">Loading...</li>
474
+ </ul>
475
+ </div>
476
+ <div class="perm-selected-panel">
477
+ <h4 class="perm-selected-panel-title">Deny Patterns</h4>
478
+ <ul class="perm-pattern-list" id="perm-selected-deny-list">
479
+ <li class="perm-pattern-empty">Loading...</li>
480
+ </ul>
481
+ </div>
482
+ <div class="perm-selected-panel">
483
+ <h4 class="perm-selected-panel-title">Allow Patterns</h4>
484
+ <ul class="perm-pattern-list" id="perm-selected-allow-list">
485
+ <li class="perm-pattern-empty">Loading...</li>
486
+ </ul>
487
+ </div>
488
+ <div class="perm-selected-panel">
489
+ <h4 class="perm-selected-panel-title">Confirm Patterns</h4>
490
+ <ul class="perm-pattern-list" id="perm-selected-confirm-list">
491
+ <li class="perm-pattern-empty">Loading...</li>
492
+ </ul>
493
+ </div>
494
+ </div>
312
495
  </div>
313
496
  </div>
314
497
 
315
- <!-- Allow Patterns -->
316
- <div class="perm-card" data-collapsed="true">
498
+ <div class="perm-card perm-card--full" data-collapsed="false" id="perm-all-detail-card">
317
499
  <div class="perm-card-header" role="button" tabindex="0" aria-expanded="true">
318
- <h3 class="perm-card-title">Allow Patterns</h3>
500
+ <h3 class="perm-card-title">All Sessions Detail</h3>
319
501
  <span class="perm-card-toggle" aria-hidden="true">&#9662;</span>
320
502
  </div>
321
503
  <div class="perm-card-body">
322
- <ul class="perm-pattern-list" id="perm-allow-list">
323
- <li class="perm-pattern-empty">Loading...</li>
324
- </ul>
325
- </div>
326
- </div>
504
+ <div class="perm-selected-header perm-selected-header--compact">
505
+ <div>
506
+ <div class="perm-selected-title">All Sessions</div>
507
+ <div class="perm-selected-subtitle">Aggregate policy state across all live and persisted sessions. The decision feed below is currently aggregate, not selection-scoped.</div>
508
+ </div>
509
+ </div>
327
510
 
328
- <!-- Confirm Patterns -->
329
- <div class="perm-card" data-collapsed="false">
330
- <div class="perm-card-header" role="button" tabindex="0" aria-expanded="true">
331
- <h3 class="perm-card-title">Confirm Patterns (Requires Approval)</h3>
332
- <span class="perm-card-toggle" aria-hidden="true">&#9662;</span>
333
- </div>
334
- <div class="perm-card-body">
335
- <ul class="perm-pattern-list" id="perm-confirm-list">
336
- <li class="perm-pattern-empty">Loading...</li>
337
- </ul>
511
+ <div class="perm-selected-grid">
512
+ <div class="perm-selected-panel">
513
+ <h4 class="perm-selected-panel-title">Policy Sources</h4>
514
+ <ul class="perm-source-list" id="perm-source-list">
515
+ <li class="perm-pattern-empty">Loading...</li>
516
+ </ul>
517
+ </div>
518
+ <div class="perm-selected-panel">
519
+ <h4 class="perm-selected-panel-title">Deny Patterns</h4>
520
+ <ul class="perm-pattern-list" id="perm-deny-list">
521
+ <li class="perm-pattern-empty">Loading...</li>
522
+ </ul>
523
+ </div>
524
+ <div class="perm-selected-panel">
525
+ <h4 class="perm-selected-panel-title">Allow Patterns</h4>
526
+ <ul class="perm-pattern-list" id="perm-allow-list">
527
+ <li class="perm-pattern-empty">Loading...</li>
528
+ </ul>
529
+ </div>
530
+ <div class="perm-selected-panel">
531
+ <h4 class="perm-selected-panel-title">Confirm Patterns</h4>
532
+ <ul class="perm-pattern-list" id="perm-confirm-list">
533
+ <li class="perm-pattern-empty">Loading...</li>
534
+ </ul>
535
+ </div>
536
+ </div>
338
537
  </div>
339
538
  </div>
340
539
 
341
- <!-- Live Decision Feed -->
342
- <div class="perm-card perm-card--full" data-collapsed="false">
343
- <div class="perm-card-header" role="button" tabindex="0" aria-expanded="true">
344
- <h3 class="perm-card-title">Live Decision Feed</h3>
345
- <span class="perm-card-toggle" aria-hidden="true">&#9662;</span>
346
- </div>
347
- <div class="perm-card-body">
348
- <div class="perm-feed" id="perm-feed" role="log" aria-live="polite" aria-label="Permission decisions">
540
+ </div>
541
+
542
+ <dialog class="modal perm-audit-modal" id="perm-audit-modal" aria-labelledby="perm-audit-modal-title">
543
+ <div class="modal-overlay" data-close-audit-modal></div>
544
+ <div class="modal-dialog perm-audit-modal-dialog" role="document">
545
+ <header class="modal-header">
546
+ <div class="modal-heading">
547
+ <h2 class="modal-title" id="perm-audit-modal-title">All Sessions Audit View</h2>
548
+ <span class="modal-type">Permissions</span>
549
+ </div>
550
+ <div class="modal-meta">
551
+ <span>Aggregate decision log across all sessions</span>
552
+ <span id="perm-audit-modal-count">0 captured entries</span>
553
+ </div>
554
+ <button type="button" class="modal-close" id="perm-audit-modal-close" aria-label="Close audit view">✕</button>
555
+ </header>
556
+ <div class="modal-body">
557
+ <div class="perm-feed perm-feed--modal" id="perm-audit-modal-feed" role="log" aria-live="polite" aria-label="Full permission decision audit feed">
349
558
  <div class="perm-feed-empty">No permission decisions yet. Waiting for tool calls...</div>
350
559
  </div>
351
560
  </div>
352
561
  </div>
353
-
354
- </div>
562
+ </dialog>
355
563
  `;
356
564
  }
357
565
 
@@ -370,6 +578,56 @@
370
578
  if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); toggle(); }
371
579
  });
372
580
  });
581
+
582
+ const expandBtn = document.getElementById('perm-feed-expand-btn');
583
+ const auditModal = document.getElementById('perm-audit-modal');
584
+ const closeBtn = document.getElementById('perm-audit-modal-close');
585
+ if (expandBtn && auditModal) {
586
+ expandBtn.addEventListener('click', function (e) {
587
+ e.stopPropagation();
588
+ openAuditModal();
589
+ });
590
+ }
591
+
592
+ if (closeBtn) {
593
+ closeBtn.addEventListener('click', closeAuditModal);
594
+ }
595
+
596
+ if (auditModal) {
597
+ auditModal.addEventListener('click', function (e) {
598
+ if (e.target === auditModal || e.target.hasAttribute('data-close-audit-modal')) {
599
+ closeAuditModal();
600
+ }
601
+ });
602
+ auditModal.addEventListener('close', function () {
603
+ document.body.classList.remove('modal-open');
604
+ });
605
+ auditModal.addEventListener('cancel', function () {
606
+ document.body.classList.remove('modal-open');
607
+ });
608
+ }
609
+ }
610
+
611
+ function openAuditModal() {
612
+ const auditModal = document.getElementById('perm-audit-modal');
613
+ if (!auditModal) return;
614
+ if (typeof auditModal.showModal === 'function') {
615
+ auditModal.showModal();
616
+ } else {
617
+ auditModal.setAttribute('open', '');
618
+ }
619
+ document.body.classList.add('modal-open');
620
+ }
621
+
622
+ function closeAuditModal() {
623
+ const auditModal = document.getElementById('perm-audit-modal');
624
+ if (!auditModal) return;
625
+ if (typeof auditModal.close === 'function') {
626
+ auditModal.close();
627
+ } else {
628
+ auditModal.removeAttribute('open');
629
+ }
630
+ document.body.classList.remove('modal-open');
373
631
  }
374
632
 
375
633
  function setText(id, value) {
@@ -377,6 +635,11 @@
377
635
  if (el) el.textContent = String(value);
378
636
  }
379
637
 
638
+ function elementMatchesSelected(element, sessionId) {
639
+ if (!sessionId || !Array.isArray(element?.sessionIds)) return false;
640
+ return element.sessionIds.includes(sessionId);
641
+ }
642
+
380
643
  // dmcp-sec[DMCP-SEC-004] — Client-side JS: UnicodeValidator unavailable in browser.
381
644
  // Using native String.normalize('NFC') which performs the same NFC normalization.
382
645
  // All data comes from our own server API, not direct user input.