Recent Activity
';
- if (d.recentAudit.length === 0) {
+ if (recentAudit.length === 0) {
html += '
';
} else {
- d.recentAudit.forEach(function(a) {
+ recentAudit.forEach(function(a) {
var badgeClass = OP_BADGES[a.operation] || 'badge-muted';
html += '
';
html += '' + esc(a.operation) + ' ';
@@ -1346,9 +1365,9 @@
agentmemory
html += '';
}
- var semFacts = d.semantic || [];
- var procItems = d.procedural || [];
- var relItems = d.relations || [];
+ var semFacts = asArray(d.semantic).filter(function(f) { return f && typeof f === 'object'; });
+ var procItems = asArray(d.procedural).filter(function(p) { return p && typeof p === 'object'; });
+ var relItems = asArray(d.relations).filter(function(r) { return r && typeof r === 'object'; });
html += '
';
html += '
';
@@ -1380,10 +1399,11 @@
agentmemory
html += '
' + esc(p.name || p.title || 'Procedure') + '
';
if (p.trigger || p.triggerCondition) html += '
Trigger: ' + esc(p.trigger || p.triggerCondition) + '
';
if (p.frequency) html += '
Freq: ' + p.frequency + '
';
- if (p.steps && p.steps.length > 0) {
+ var steps = asArray(p.steps);
+ if (steps.length > 0) {
html += '
';
- p.steps.slice(0, 4).forEach(function(s) { html += '- ' + esc(typeof s === 'string' ? s : s.description || s.action || JSON.stringify(s)) + '
'; });
- if (p.steps.length > 4) html += '- + ' + (p.steps.length - 4) + ' more...
';
+ steps.slice(0, 4).forEach(function(s) { html += '- ' + esc(typeof s === 'string' ? s : s.description || s.action || JSON.stringify(s)) + '
'; });
+ if (steps.length > 4) html += '- + ' + (steps.length - 4) + ' more...
';
html += '
';
}
html += '
';
diff --git a/test/viewer-dashboard.test.ts b/test/viewer-dashboard.test.ts
new file mode 100644
index 0000000..deabcf8
--- /dev/null
+++ b/test/viewer-dashboard.test.ts
@@ -0,0 +1,168 @@
+import { readFileSync } from "node:fs";
+import { runInNewContext } from "node:vm";
+import { describe, expect, it, vi } from "vitest";
+
+type ViewerElement = {
+ innerHTML: string;
+ textContent: string;
+ value: string;
+ dataset: Record
;
+ classList: {
+ toggle: ReturnType;
+ add: ReturnType;
+ remove: ReturnType;
+ };
+ addEventListener: ReturnType;
+ removeEventListener: ReturnType;
+ querySelector: ReturnType;
+ querySelectorAll: ReturnType;
+};
+
+function escapeHtml(value: string): string {
+ return value
+ .replaceAll("&", "&")
+ .replaceAll("<", "<")
+ .replaceAll(">", ">")
+ .replaceAll('"', """);
+}
+
+function loadViewerScript(): string {
+ const html = readFileSync(
+ new URL("../src/viewer/index.html", import.meta.url),
+ "utf8",
+ );
+ const match = html.match(
+ /