@x12i/memorix-service 3.7.0 → 3.7.6
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/dist/activity-ring.d.ts +169 -0
- package/dist/activity-ring.d.ts.map +1 -0
- package/dist/activity-ring.js +234 -0
- package/dist/activity-ring.js.map +1 -0
- package/dist/activity-routes.d.ts +3 -0
- package/dist/activity-routes.d.ts.map +1 -0
- package/dist/activity-routes.js +41 -0
- package/dist/activity-routes.js.map +1 -0
- package/dist/activity-ui.d.ts +3 -0
- package/dist/activity-ui.d.ts.map +1 -0
- package/dist/activity-ui.js +899 -0
- package/dist/activity-ui.js.map +1 -0
- package/dist/app.d.ts.map +1 -1
- package/dist/app.js +14 -2
- package/dist/app.js.map +1 -1
- package/dist/caller-identity.d.ts +3 -2
- package/dist/caller-identity.d.ts.map +1 -1
- package/dist/caller-identity.js.map +1 -1
- package/dist/catalog-bridge.js +1 -1
- package/dist/catalog-bridge.js.map +1 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +4 -0
- package/dist/cli.js.map +1 -1
- package/dist/connector-catalog.d.ts +1 -1
- package/dist/connector-ops/attempt-store.d.ts.map +1 -1
- package/dist/connector-ops/attempt-store.js +1 -3
- package/dist/connector-ops/attempt-store.js.map +1 -1
- package/dist/connector-ops/checkpoint-store.d.ts.map +1 -1
- package/dist/connector-ops/checkpoint-store.js +1 -3
- package/dist/connector-ops/checkpoint-store.js.map +1 -1
- package/dist/connector-ops/override-store.d.ts.map +1 -1
- package/dist/connector-ops/override-store.js +1 -3
- package/dist/connector-ops/override-store.js.map +1 -1
- package/dist/connector-ops/run-store.d.ts.map +1 -1
- package/dist/connector-ops/run-store.js +1 -3
- package/dist/connector-ops/run-store.js.map +1 -1
- package/dist/connector-ops/schedule-lease-store.d.ts.map +1 -1
- package/dist/connector-ops/schedule-lease-store.js +1 -3
- package/dist/connector-ops/schedule-lease-store.js.map +1 -1
- package/dist/data-changes-routes.d.ts +7 -0
- package/dist/data-changes-routes.d.ts.map +1 -0
- package/dist/data-changes-routes.js +27 -0
- package/dist/data-changes-routes.js.map +1 -0
- package/dist/data-changes-ui.d.ts +3 -0
- package/dist/data-changes-ui.d.ts.map +1 -0
- package/dist/data-changes-ui.js +239 -0
- package/dist/data-changes-ui.js.map +1 -0
- package/dist/data-changes.d.ts +51 -0
- package/dist/data-changes.d.ts.map +1 -0
- package/dist/data-changes.js +218 -0
- package/dist/data-changes.js.map +1 -0
- package/dist/errors.d.ts +3 -0
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +36 -3
- package/dist/errors.js.map +1 -1
- package/dist/identity-alias.d.ts +12 -0
- package/dist/identity-alias.d.ts.map +1 -0
- package/dist/identity-alias.js +22 -0
- package/dist/identity-alias.js.map +1 -0
- package/dist/incremental-state/state-store.d.ts.map +1 -1
- package/dist/incremental-state/state-store.js +1 -3
- package/dist/incremental-state/state-store.js.map +1 -1
- package/dist/integrations/instance-store.d.ts.map +1 -1
- package/dist/integrations/instance-store.js +1 -3
- package/dist/integrations/instance-store.js.map +1 -1
- package/dist/isolation.d.ts.map +1 -1
- package/dist/isolation.js +3 -2
- package/dist/isolation.js.map +1 -1
- package/dist/land-v2-guards.d.ts +22 -0
- package/dist/land-v2-guards.d.ts.map +1 -0
- package/dist/land-v2-guards.js +57 -0
- package/dist/land-v2-guards.js.map +1 -0
- package/dist/live.d.ts.map +1 -1
- package/dist/nexum-analysis/backfill.d.ts +59 -0
- package/dist/nexum-analysis/backfill.d.ts.map +1 -0
- package/dist/nexum-analysis/backfill.js +199 -0
- package/dist/nexum-analysis/backfill.js.map +1 -0
- package/dist/nexum-analysis/enqueue.d.ts +29 -0
- package/dist/nexum-analysis/enqueue.d.ts.map +1 -0
- package/dist/nexum-analysis/enqueue.js +190 -0
- package/dist/nexum-analysis/enqueue.js.map +1 -0
- package/dist/nexum-analysis/outbox.d.ts +44 -0
- package/dist/nexum-analysis/outbox.d.ts.map +1 -0
- package/dist/nexum-analysis/outbox.js +176 -0
- package/dist/nexum-analysis/outbox.js.map +1 -0
- package/dist/nexum-analysis/policy.d.ts +48 -0
- package/dist/nexum-analysis/policy.d.ts.map +1 -0
- package/dist/nexum-analysis/policy.js +165 -0
- package/dist/nexum-analysis/policy.js.map +1 -0
- package/dist/nexum-analysis/reconcile.d.ts +8 -0
- package/dist/nexum-analysis/reconcile.d.ts.map +1 -0
- package/dist/nexum-analysis/reconcile.js +37 -0
- package/dist/nexum-analysis/reconcile.js.map +1 -0
- package/dist/openapi/openapi.json +591 -4
- package/dist/operational-db.d.ts +6 -0
- package/dist/operational-db.d.ts.map +1 -0
- package/dist/operational-db.js +22 -0
- package/dist/operational-db.js.map +1 -0
- package/dist/platform.d.ts +4 -1
- package/dist/platform.d.ts.map +1 -1
- package/dist/platform.js +67 -14
- package/dist/platform.js.map +1 -1
- package/dist/request-countex.d.ts +25 -0
- package/dist/request-countex.d.ts.map +1 -1
- package/dist/request-countex.js +103 -18
- package/dist/request-countex.js.map +1 -1
- package/dist/restricted-store/object-store.d.ts.map +1 -1
- package/dist/restricted-store/object-store.js +1 -3
- package/dist/restricted-store/object-store.js.map +1 -1
- package/dist/routes/connector-invocations.d.ts.map +1 -1
- package/dist/routes/connector-invocations.js +7 -1
- package/dist/routes/connector-invocations.js.map +1 -1
- package/dist/routes/data.d.ts.map +1 -1
- package/dist/routes/data.js +105 -12
- package/dist/routes/data.js.map +1 -1
- package/dist/routes/knowledge.d.ts +8 -0
- package/dist/routes/knowledge.d.ts.map +1 -0
- package/dist/routes/knowledge.js +114 -0
- package/dist/routes/knowledge.js.map +1 -0
- package/dist/routes/memory.d.ts.map +1 -1
- package/dist/routes/memory.js +33 -9
- package/dist/routes/memory.js.map +1 -1
- package/dist/routes/metadata.d.ts.map +1 -1
- package/dist/routes/metadata.js +102 -1
- package/dist/routes/metadata.js.map +1 -1
- package/dist/routes/mgc-landing.d.ts.map +1 -1
- package/dist/routes/mgc-landing.js +90 -1
- package/dist/routes/mgc-landing.js.map +1 -1
- package/dist/routes/operations.d.ts.map +1 -1
- package/dist/routes/operations.js +39 -2
- package/dist/routes/operations.js.map +1 -1
- package/dist/routes/publications.d.ts +4 -0
- package/dist/routes/publications.d.ts.map +1 -0
- package/dist/routes/publications.js +111 -0
- package/dist/routes/publications.js.map +1 -0
- package/dist/routes/workflow.d.ts.map +1 -1
- package/dist/routes/workflow.js +18 -9
- package/dist/routes/workflow.js.map +1 -1
- package/openapi/openapi.json +591 -4
- package/package.json +5 -5
|
@@ -0,0 +1,899 @@
|
|
|
1
|
+
/** HTML for GET /_live/activity — land / connect / fetch operator feed. */
|
|
2
|
+
export function activityHtml() {
|
|
3
|
+
return `<!DOCTYPE html>
|
|
4
|
+
<html lang="en">
|
|
5
|
+
<head>
|
|
6
|
+
<meta charset="utf-8" />
|
|
7
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
8
|
+
<title>Memorix · Activity</title>
|
|
9
|
+
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
|
10
|
+
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
|
11
|
+
<link href="https://fonts.googleapis.com/css2?family=IBM+Plex+Sans:wght@400;500;600;700&family=IBM+Plex+Mono:wght@400;500&display=swap" rel="stylesheet" />
|
|
12
|
+
<style>
|
|
13
|
+
:root {
|
|
14
|
+
--bg: #0a0c10; --bg-elev: #10141b; --bg-row: #141922; --line: #242b38;
|
|
15
|
+
--text: #e8ebe6; --muted: #8a9288; --dim: #5c645c;
|
|
16
|
+
--ok: #3dba7a; --bad: #e25a5a; --accent: #c8f082; --accent-ink: #0a0c10;
|
|
17
|
+
--api: #7eb8d4; --ai: #d4a574;
|
|
18
|
+
--sans: "IBM Plex Sans", system-ui, sans-serif;
|
|
19
|
+
--mono: "IBM Plex Mono", ui-monospace, monospace;
|
|
20
|
+
}
|
|
21
|
+
* { box-sizing: border-box; }
|
|
22
|
+
html, body { height: 100%; margin: 0; }
|
|
23
|
+
body {
|
|
24
|
+
font-family: var(--sans);
|
|
25
|
+
background:
|
|
26
|
+
radial-gradient(1200px 600px at 10% -10%, #1a2214 0%, transparent 55%),
|
|
27
|
+
radial-gradient(900px 500px at 100% 0%, #121820 0%, transparent 50%),
|
|
28
|
+
var(--bg);
|
|
29
|
+
color: var(--text); font-size: 14px; line-height: 1.45;
|
|
30
|
+
}
|
|
31
|
+
a { color: var(--accent); }
|
|
32
|
+
.shell { max-width: 1280px; margin: 0 auto; padding: 36px 28px 64px; }
|
|
33
|
+
header {
|
|
34
|
+
display: flex; align-items: flex-end; justify-content: space-between; gap: 24px;
|
|
35
|
+
margin-bottom: 22px; padding-bottom: 18px; border-bottom: 1px solid var(--line);
|
|
36
|
+
}
|
|
37
|
+
.brand { font-size: 26px; font-weight: 700; letter-spacing: -0.03em; }
|
|
38
|
+
.brand span { color: var(--accent); }
|
|
39
|
+
.sub { margin-top: 8px; color: var(--muted); font-size: 13px; max-width: 68ch; }
|
|
40
|
+
.meta { text-align: right; font-family: var(--mono); font-size: 11px; color: var(--dim); display: grid; gap: 4px; }
|
|
41
|
+
.section-label {
|
|
42
|
+
margin: 0 0 10px; font-size: 12px; font-family: var(--mono);
|
|
43
|
+
letter-spacing: 0.08em; text-transform: uppercase; color: var(--dim);
|
|
44
|
+
}
|
|
45
|
+
.health-grid {
|
|
46
|
+
display: grid; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
|
|
47
|
+
gap: 14px; margin-bottom: 20px;
|
|
48
|
+
}
|
|
49
|
+
.health-card {
|
|
50
|
+
background: var(--bg-elev); border: 1px solid var(--line); padding: 14px 14px 10px;
|
|
51
|
+
}
|
|
52
|
+
.health-card h3 {
|
|
53
|
+
margin: 0 0 4px; font-size: 12px; font-family: var(--mono);
|
|
54
|
+
letter-spacing: 0.06em; text-transform: uppercase; color: var(--dim); font-weight: 500;
|
|
55
|
+
}
|
|
56
|
+
.health-card .val {
|
|
57
|
+
font-family: var(--mono); font-size: 20px; font-weight: 600; color: var(--text);
|
|
58
|
+
margin-bottom: 8px;
|
|
59
|
+
}
|
|
60
|
+
.health-card .val span { color: var(--muted); font-size: 12px; font-weight: 500; margin-left: 6px; }
|
|
61
|
+
.health-card svg { width: 100%; height: 56px; display: block; }
|
|
62
|
+
.health-card .hint { margin-top: 6px; font-size: 11px; color: var(--dim); font-family: var(--mono); }
|
|
63
|
+
.legend {
|
|
64
|
+
display: grid; gap: 10px; margin: 0 0 18px;
|
|
65
|
+
padding: 14px 16px; background: var(--bg-elev); border: 1px solid var(--line);
|
|
66
|
+
}
|
|
67
|
+
.legend-row {
|
|
68
|
+
display: grid; grid-template-columns: 100px 1fr; gap: 12px; align-items: baseline;
|
|
69
|
+
font-size: 13px;
|
|
70
|
+
}
|
|
71
|
+
.legend-row strong {
|
|
72
|
+
font-family: var(--mono); font-size: 11px; letter-spacing: 0.06em;
|
|
73
|
+
text-transform: uppercase; color: var(--accent); font-weight: 600;
|
|
74
|
+
}
|
|
75
|
+
.legend-row span { color: var(--muted); }
|
|
76
|
+
.tabs { display: flex; gap: 6px; flex-wrap: wrap; margin-bottom: 10px; }
|
|
77
|
+
.tab, .btn {
|
|
78
|
+
appearance: none; border: 1px solid var(--line); background: transparent; color: var(--muted);
|
|
79
|
+
font: 500 12px/1 var(--sans); letter-spacing: 0.04em; text-transform: uppercase;
|
|
80
|
+
padding: 10px 16px; cursor: pointer; border-radius: 2px;
|
|
81
|
+
}
|
|
82
|
+
.tab[aria-selected="true"] {
|
|
83
|
+
background: var(--accent); color: var(--accent-ink); border-color: var(--accent);
|
|
84
|
+
}
|
|
85
|
+
.btn { background: var(--bg-row); color: var(--text); text-transform: none; letter-spacing: 0; }
|
|
86
|
+
.btn:hover { border-color: var(--accent); }
|
|
87
|
+
.lane-hint { margin: 0 0 14px; color: var(--muted); font-size: 13px; max-width: 72ch; }
|
|
88
|
+
.lane-hint strong { color: var(--text); font-weight: 500; }
|
|
89
|
+
.filters {
|
|
90
|
+
display: flex; flex-wrap: wrap; gap: 10px; align-items: center;
|
|
91
|
+
margin-bottom: 14px; padding: 12px 14px;
|
|
92
|
+
background: var(--bg-elev); border: 1px solid var(--line);
|
|
93
|
+
}
|
|
94
|
+
.filters label {
|
|
95
|
+
display: flex; flex-direction: column; gap: 4px;
|
|
96
|
+
font-family: var(--mono); font-size: 10px; letter-spacing: 0.06em;
|
|
97
|
+
text-transform: uppercase; color: var(--dim);
|
|
98
|
+
}
|
|
99
|
+
.filters label[hidden] { display: none; }
|
|
100
|
+
.filters input, .filters select {
|
|
101
|
+
appearance: none; border: 1px solid var(--line); background: var(--bg-row); color: var(--text);
|
|
102
|
+
font: 500 12px/1.2 var(--sans); padding: 8px 10px; border-radius: 2px; min-width: 120px;
|
|
103
|
+
}
|
|
104
|
+
.filters input { min-width: 180px; }
|
|
105
|
+
.filters .btn { align-self: flex-end; }
|
|
106
|
+
.status-bar { display: flex; align-items: center; justify-content: space-between; gap: 16px; margin-bottom: 14px; }
|
|
107
|
+
.summary { display: flex; gap: 14px; flex-wrap: wrap; font-family: var(--mono); font-size: 12px; color: var(--muted); }
|
|
108
|
+
.summary strong { color: var(--text); font-weight: 500; }
|
|
109
|
+
.pause-bar {
|
|
110
|
+
display: flex; align-items: center; justify-content: space-between; gap: 12px;
|
|
111
|
+
margin-bottom: 10px; padding: 8px 12px;
|
|
112
|
+
background: #1a2214; border: 1px solid var(--accent); color: var(--text);
|
|
113
|
+
font-family: var(--mono); font-size: 12px;
|
|
114
|
+
}
|
|
115
|
+
.pause-bar[hidden] { display: none; }
|
|
116
|
+
.table-scroll {
|
|
117
|
+
max-height: min(62vh, 720px);
|
|
118
|
+
overflow: auto;
|
|
119
|
+
border: 1px solid var(--line);
|
|
120
|
+
}
|
|
121
|
+
table.list {
|
|
122
|
+
width: 100%; table-layout: fixed; border-collapse: collapse;
|
|
123
|
+
background: var(--bg-elev); border: 0;
|
|
124
|
+
}
|
|
125
|
+
table.list th, table.list td {
|
|
126
|
+
text-align: left; padding: 8px 10px; border-bottom: 1px solid var(--line);
|
|
127
|
+
vertical-align: middle; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
|
|
128
|
+
}
|
|
129
|
+
table.list td.detail-cell {
|
|
130
|
+
white-space: normal; overflow: visible; text-overflow: clip; vertical-align: top;
|
|
131
|
+
}
|
|
132
|
+
th {
|
|
133
|
+
font-family: var(--mono); font-size: 10px; letter-spacing: 0.08em; text-transform: uppercase;
|
|
134
|
+
color: var(--dim); font-weight: 500; background: #0d1016;
|
|
135
|
+
}
|
|
136
|
+
tr:last-child td { border-bottom: 0; }
|
|
137
|
+
tr:hover td { background: var(--bg-row); }
|
|
138
|
+
tr.clickable { cursor: pointer; }
|
|
139
|
+
tr.open td { background: #1a2214; }
|
|
140
|
+
.mono { font-family: var(--mono); font-size: 12px; color: var(--muted); }
|
|
141
|
+
.badge {
|
|
142
|
+
display: inline-flex; align-items: center; gap: 6px; font-family: var(--mono);
|
|
143
|
+
font-size: 11px; padding: 3px 8px; border: 1px solid var(--line);
|
|
144
|
+
}
|
|
145
|
+
.badge::before { content: ""; width: 7px; height: 7px; border-radius: 50%; background: var(--dim); }
|
|
146
|
+
.badge.ok { color: var(--ok); }
|
|
147
|
+
.badge.ok::before { background: var(--ok); }
|
|
148
|
+
.badge.bad { color: var(--bad); }
|
|
149
|
+
.badge.bad::before { background: var(--bad); }
|
|
150
|
+
.badge.queued { color: var(--api); }
|
|
151
|
+
.badge.queued::before { background: var(--api); }
|
|
152
|
+
.badge.ai { color: var(--ai); }
|
|
153
|
+
.badge.ai::before { background: var(--ai); }
|
|
154
|
+
.detail {
|
|
155
|
+
margin-top: 8px; padding: 12px; background: #0d1016; border: 1px solid var(--line);
|
|
156
|
+
font-family: var(--mono); font-size: 11px; color: var(--muted);
|
|
157
|
+
}
|
|
158
|
+
.detail h3 { margin: 0 0 8px; font-size: 12px; color: var(--text); font-weight: 600; letter-spacing: 0.04em; text-transform: uppercase; }
|
|
159
|
+
.edge-list { margin: 0; padding-left: 18px; }
|
|
160
|
+
.edge-list li { margin: 2px 0; }
|
|
161
|
+
.graph-svg {
|
|
162
|
+
width: 100%; max-width: 640px; height: 220px; margin-top: 10px;
|
|
163
|
+
background: #0a0c10; border: 1px solid var(--line);
|
|
164
|
+
}
|
|
165
|
+
.graph-svg text { fill: var(--muted); font-family: var(--mono); font-size: 10px; }
|
|
166
|
+
.graph-svg circle { fill: var(--accent); stroke: #2a3218; stroke-width: 1; }
|
|
167
|
+
.graph-svg line { stroke: var(--api); stroke-width: 1.2; opacity: 0.85; }
|
|
168
|
+
.empty { color: var(--muted); padding: 24px 0; }
|
|
169
|
+
.dialog-back {
|
|
170
|
+
position: fixed; inset: 0; z-index: 30;
|
|
171
|
+
background: rgba(6, 8, 10, 0.72);
|
|
172
|
+
display: flex; align-items: flex-start; justify-content: center;
|
|
173
|
+
padding: 40px 20px; overflow: auto;
|
|
174
|
+
}
|
|
175
|
+
.dialog-back[hidden] { display: none; }
|
|
176
|
+
.dialog {
|
|
177
|
+
width: min(920px, 100%);
|
|
178
|
+
background: var(--bg-elev); border: 1px solid var(--line);
|
|
179
|
+
padding: 14px 18px 20px;
|
|
180
|
+
}
|
|
181
|
+
.dialog-bar {
|
|
182
|
+
display: flex; align-items: center; justify-content: space-between; gap: 12px;
|
|
183
|
+
margin-bottom: 8px;
|
|
184
|
+
}
|
|
185
|
+
.dialog-bar strong {
|
|
186
|
+
font-size: 13px; letter-spacing: 0.04em; text-transform: uppercase;
|
|
187
|
+
}
|
|
188
|
+
.dialog .detail { margin-top: 0; border: 0; background: transparent; padding: 4px 0 0; }
|
|
189
|
+
.dialog .graph-svg { max-width: none; height: 280px; }
|
|
190
|
+
footer { margin-top: 28px; color: var(--dim); font-size: 12px; }
|
|
191
|
+
footer a { margin-right: 12px; }
|
|
192
|
+
</style>
|
|
193
|
+
</head>
|
|
194
|
+
<body>
|
|
195
|
+
<div class="shell">
|
|
196
|
+
<header>
|
|
197
|
+
<div>
|
|
198
|
+
<div class="brand">Memorix <span>activity</span></div>
|
|
199
|
+
<p class="sub">Live feed of writes in, Nexum knowledge connections (rules vs AI), and reads out. In-memory — last ~10 000 per lane; clears on process restart.</p>
|
|
200
|
+
</div>
|
|
201
|
+
<div class="meta">
|
|
202
|
+
<div><strong>UI</strong> /_live/activity</div>
|
|
203
|
+
<div><strong>API</strong> /_live/api/activity</div>
|
|
204
|
+
<div><a href="/_live">← live view</a> · <a href="/_live/changes">data changes</a></div>
|
|
205
|
+
</div>
|
|
206
|
+
</header>
|
|
207
|
+
|
|
208
|
+
<p class="section-label">Activity · last 60 minutes</p>
|
|
209
|
+
<div class="health-grid" id="charts"></div>
|
|
210
|
+
|
|
211
|
+
<div class="legend" aria-label="What each tab means">
|
|
212
|
+
<div class="legend-row">
|
|
213
|
+
<strong>Land</strong>
|
|
214
|
+
<span>Data <em>written into</em> Memorix — <span class="mono">POST /api/data/content</span>. Column <em>Nexum</em> = did we enqueue analyze after this write?</span>
|
|
215
|
+
</div>
|
|
216
|
+
<div class="legend-row">
|
|
217
|
+
<strong>Connect</strong>
|
|
218
|
+
<span>Nexum built a knowledge graph. <em>rules · no AI</em> = deterministic; <em>AI</em> = semantic. Relations = Memorix relationship plans created.</span>
|
|
219
|
+
</div>
|
|
220
|
+
<div class="legend-row">
|
|
221
|
+
<strong>Fetch</strong>
|
|
222
|
+
<span>Data <em>read out of</em> Memorix — <span class="mono">GET /api/data/content</span>.</span>
|
|
223
|
+
</div>
|
|
224
|
+
<div class="legend-row">
|
|
225
|
+
<strong>Nexum gate</strong>
|
|
226
|
+
<span>Auto-analyze is <em>on by default</em> for types named by an installed <span class="mono">nexum.indexing-policy</span> (built-in default: <span class="mono">content</span> / <span class="mono">snapshots|knowx</span>; security-map estate products when that policy is installed). A type can be stored and still show <em>skipped · no policy</em> — that is the analyze allowlist, not the switch.</span>
|
|
227
|
+
</div>
|
|
228
|
+
</div>
|
|
229
|
+
|
|
230
|
+
<nav class="tabs" role="tablist">
|
|
231
|
+
<button class="tab" role="tab" aria-selected="true" data-lane="land">Land · writes</button>
|
|
232
|
+
<button class="tab" role="tab" aria-selected="false" data-lane="index">Connect · Nexum</button>
|
|
233
|
+
<button class="tab" role="tab" aria-selected="false" data-lane="fetch">Fetch · reads</button>
|
|
234
|
+
</nav>
|
|
235
|
+
<p class="lane-hint" id="lane-hint"></p>
|
|
236
|
+
|
|
237
|
+
<div class="filters" id="filters">
|
|
238
|
+
<label>Search
|
|
239
|
+
<input id="f-q" type="search" placeholder="record, org, caller…" />
|
|
240
|
+
</label>
|
|
241
|
+
<label>Org
|
|
242
|
+
<select id="f-org"><option value="">All</option></select>
|
|
243
|
+
</label>
|
|
244
|
+
<label>Caller
|
|
245
|
+
<select id="f-caller"><option value="">All</option></select>
|
|
246
|
+
</label>
|
|
247
|
+
<label>OT
|
|
248
|
+
<select id="f-ot"><option value="">All</option></select>
|
|
249
|
+
</label>
|
|
250
|
+
<label>CT
|
|
251
|
+
<select id="f-ct"><option value="">All</option></select>
|
|
252
|
+
</label>
|
|
253
|
+
<label id="f-nexum-wrap">Nexum
|
|
254
|
+
<select id="f-nexum">
|
|
255
|
+
<option value="">All</option>
|
|
256
|
+
<option value="queued">queued · analyze</option>
|
|
257
|
+
<option value="skipped">skipped</option>
|
|
258
|
+
</select>
|
|
259
|
+
</label>
|
|
260
|
+
<label id="f-how-wrap" hidden>How
|
|
261
|
+
<select id="f-how">
|
|
262
|
+
<option value="">All</option>
|
|
263
|
+
<option value="rules">rules · no AI</option>
|
|
264
|
+
<option value="ai">AI</option>
|
|
265
|
+
<option value="unknown">unknown</option>
|
|
266
|
+
</select>
|
|
267
|
+
</label>
|
|
268
|
+
<label id="f-hit-wrap" hidden>Hit
|
|
269
|
+
<select id="f-hit">
|
|
270
|
+
<option value="">All</option>
|
|
271
|
+
<option value="hit">hit</option>
|
|
272
|
+
<option value="miss">miss</option>
|
|
273
|
+
</select>
|
|
274
|
+
</label>
|
|
275
|
+
<button class="btn" id="f-clear" type="button">Clear filters</button>
|
|
276
|
+
</div>
|
|
277
|
+
|
|
278
|
+
<div class="status-bar">
|
|
279
|
+
<div class="summary" id="summary"></div>
|
|
280
|
+
<button class="btn" id="refresh" type="button">Refresh</button>
|
|
281
|
+
</div>
|
|
282
|
+
<div class="pause-bar" id="pause-bar" hidden>
|
|
283
|
+
<span id="pause-msg">List held</span>
|
|
284
|
+
<button class="btn" id="resume" type="button">Show new</button>
|
|
285
|
+
</div>
|
|
286
|
+
<div class="table-scroll" id="table-scroll"></div>
|
|
287
|
+
<footer>
|
|
288
|
+
<a href="/_live">/_live</a>
|
|
289
|
+
<a href="/_live/changes">/_live/changes</a>
|
|
290
|
+
Click a row to open it. The list pauses only when you scroll away from the top.
|
|
291
|
+
</footer>
|
|
292
|
+
</div>
|
|
293
|
+
<div class="dialog-back" id="dialog-back" hidden>
|
|
294
|
+
<div class="dialog" role="dialog" aria-modal="true" aria-labelledby="dialog-title">
|
|
295
|
+
<div class="dialog-bar">
|
|
296
|
+
<strong id="dialog-title">Item</strong>
|
|
297
|
+
<button class="btn" id="dialog-close" type="button">Close</button>
|
|
298
|
+
</div>
|
|
299
|
+
<div id="dialog-body"></div>
|
|
300
|
+
</div>
|
|
301
|
+
</div>
|
|
302
|
+
<script>
|
|
303
|
+
const $ = (id) => document.getElementById(id);
|
|
304
|
+
let lane = "land";
|
|
305
|
+
let entries = [];
|
|
306
|
+
let liveEntries = [];
|
|
307
|
+
let followLive = true;
|
|
308
|
+
let pendingNew = 0;
|
|
309
|
+
let ignoreScroll = false;
|
|
310
|
+
let sizes = { land: 0, index: 0, fetch: 0 };
|
|
311
|
+
let capacity = 10000;
|
|
312
|
+
let seriesBuckets = [];
|
|
313
|
+
let filterState = { q: "", org: "", caller: "", ot: "", ct: "", nexum: "", how: "", hit: "" };
|
|
314
|
+
|
|
315
|
+
function listHeld() {
|
|
316
|
+
return !followLive;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
function dialogTitle(e) {
|
|
320
|
+
const t = typeOf(e);
|
|
321
|
+
const id = e.recordId || (e.source && e.source.recordId) || (e.snapshot && e.snapshot.recordId) || "";
|
|
322
|
+
return [t.ot, t.ct, id].filter(Boolean).join(" / ") || "Item";
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
function openDialog(e) {
|
|
326
|
+
$("dialog-title").textContent = dialogTitle(e);
|
|
327
|
+
$("dialog-body").innerHTML = renderDetail(e);
|
|
328
|
+
$("dialog-back").hidden = false;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
function closeDialog() {
|
|
332
|
+
$("dialog-back").hidden = true;
|
|
333
|
+
$("dialog-body").innerHTML = "";
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
document.querySelectorAll(".tab").forEach((tab) => {
|
|
337
|
+
tab.addEventListener("click", () => {
|
|
338
|
+
document.querySelectorAll(".tab").forEach((t) => t.setAttribute("aria-selected", "false"));
|
|
339
|
+
tab.setAttribute("aria-selected", "true");
|
|
340
|
+
lane = tab.dataset.lane;
|
|
341
|
+
closeDialog();
|
|
342
|
+
followLive = true;
|
|
343
|
+
pendingNew = 0;
|
|
344
|
+
syncFilterVisibility();
|
|
345
|
+
load(true);
|
|
346
|
+
});
|
|
347
|
+
});
|
|
348
|
+
$("refresh").onclick = () => {
|
|
349
|
+
followLive = true;
|
|
350
|
+
load(true);
|
|
351
|
+
};
|
|
352
|
+
$("resume").onclick = () => {
|
|
353
|
+
followLive = true;
|
|
354
|
+
applyLive();
|
|
355
|
+
};
|
|
356
|
+
$("dialog-close").onclick = () => closeDialog();
|
|
357
|
+
$("dialog-back").addEventListener("click", (ev) => {
|
|
358
|
+
if (ev.target === $("dialog-back")) closeDialog();
|
|
359
|
+
});
|
|
360
|
+
document.addEventListener("keydown", (ev) => {
|
|
361
|
+
if (ev.key === "Escape" && !$("dialog-back").hidden) closeDialog();
|
|
362
|
+
});
|
|
363
|
+
$("table-scroll").addEventListener("scroll", () => {
|
|
364
|
+
if (ignoreScroll) return;
|
|
365
|
+
const atTop = $("table-scroll").scrollTop < 8;
|
|
366
|
+
if (atTop && !followLive) {
|
|
367
|
+
followLive = true;
|
|
368
|
+
if (pendingNew) applyLive();
|
|
369
|
+
else updatePauseBar();
|
|
370
|
+
} else if (!atTop && followLive) {
|
|
371
|
+
followLive = false;
|
|
372
|
+
updatePauseBar();
|
|
373
|
+
}
|
|
374
|
+
});
|
|
375
|
+
$("f-clear").onclick = () => {
|
|
376
|
+
filterState = { q: "", org: "", caller: "", ot: "", ct: "", nexum: "", how: "", hit: "" };
|
|
377
|
+
$("f-q").value = "";
|
|
378
|
+
$("f-org").value = "";
|
|
379
|
+
$("f-caller").value = "";
|
|
380
|
+
$("f-ot").value = "";
|
|
381
|
+
$("f-ct").value = "";
|
|
382
|
+
$("f-nexum").value = "";
|
|
383
|
+
$("f-how").value = "";
|
|
384
|
+
$("f-hit").value = "";
|
|
385
|
+
render();
|
|
386
|
+
};
|
|
387
|
+
["f-q", "f-org", "f-caller", "f-ot", "f-ct", "f-nexum", "f-how", "f-hit"].forEach((id) => {
|
|
388
|
+
$(id).addEventListener("input", () => {
|
|
389
|
+
filterState = {
|
|
390
|
+
q: $("f-q").value.trim().toLowerCase(),
|
|
391
|
+
org: $("f-org").value,
|
|
392
|
+
caller: $("f-caller").value,
|
|
393
|
+
ot: $("f-ot").value,
|
|
394
|
+
ct: $("f-ct").value,
|
|
395
|
+
nexum: $("f-nexum").value,
|
|
396
|
+
how: $("f-how").value,
|
|
397
|
+
hit: $("f-hit").value,
|
|
398
|
+
};
|
|
399
|
+
if (!listHeld() && liveEntries.length) entries = liveEntries;
|
|
400
|
+
render();
|
|
401
|
+
});
|
|
402
|
+
$(id).addEventListener("change", () => $(id).dispatchEvent(new Event("input")));
|
|
403
|
+
});
|
|
404
|
+
|
|
405
|
+
function syncFilterVisibility() {
|
|
406
|
+
$("f-nexum-wrap").hidden = lane !== "land";
|
|
407
|
+
$("f-how-wrap").hidden = lane !== "index";
|
|
408
|
+
$("f-hit-wrap").hidden = lane !== "fetch";
|
|
409
|
+
}
|
|
410
|
+
syncFilterVisibility();
|
|
411
|
+
|
|
412
|
+
function escapeHtml(s) {
|
|
413
|
+
return String(s ?? "").replace(/[&<>"']/g, (c) => ({
|
|
414
|
+
"&": "&", "<": "<", ">": ">", '"': """, "'": "'"
|
|
415
|
+
})[c]);
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
function fmtTime(ts) {
|
|
419
|
+
try { return new Date(ts).toISOString().slice(11, 23); }
|
|
420
|
+
catch { return ts; }
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
function statusBadge(status) {
|
|
424
|
+
const s = String(status ?? "");
|
|
425
|
+
let cls = "";
|
|
426
|
+
if (/succeed|connected|hit|created|updated|queued|rules/.test(s) && !/failed|dead/.test(s)) {
|
|
427
|
+
cls = /queued|analyzing/.test(s) ? "queued" : "ok";
|
|
428
|
+
} else if (/fail|dead|miss|error/.test(s)) {
|
|
429
|
+
cls = "bad";
|
|
430
|
+
} else if (/ai/i.test(s)) {
|
|
431
|
+
cls = "ai";
|
|
432
|
+
}
|
|
433
|
+
return '<span class="badge ' + cls + '">' + escapeHtml(s) + "</span>";
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
function shortSkipReason(reason) {
|
|
437
|
+
const r = String(reason || "").toLowerCase();
|
|
438
|
+
if (!r) return "skipped";
|
|
439
|
+
if (r.includes("no matching")) return "skipped · no policy";
|
|
440
|
+
if (r.includes("disabled")) return "skipped · off";
|
|
441
|
+
if (r.includes("worker unset")) return "skipped · no worker";
|
|
442
|
+
if (r.includes("no analyzeable") || r.includes("no payload")) return "skipped · no payload";
|
|
443
|
+
if (r.includes("operational")) return "skipped · no ops db";
|
|
444
|
+
return "skipped · " + String(reason).slice(0, 40);
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
/** Land-row Nexum cell: skipped | queued (rules vs AI appear on Connect). */
|
|
448
|
+
function nexumLandCell(indexing) {
|
|
449
|
+
if (!indexing) return "—";
|
|
450
|
+
if (indexing.status === "skipped") {
|
|
451
|
+
return '<span class="badge">' + escapeHtml(shortSkipReason(indexing.reason)) + "</span>";
|
|
452
|
+
}
|
|
453
|
+
if (indexing.status === "queued") {
|
|
454
|
+
return '<span class="badge queued">queued · analyze</span>';
|
|
455
|
+
}
|
|
456
|
+
return statusBadge(indexing.status);
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
function modeBadge(mode) {
|
|
460
|
+
if (mode === "rules") return '<span class="badge ok">rules · no AI</span>';
|
|
461
|
+
if (mode === "ai") return '<span class="badge ai">AI</span>';
|
|
462
|
+
if (!mode) return "—";
|
|
463
|
+
return '<span class="badge">unknown</span>';
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
function addr(a) {
|
|
467
|
+
if (!a) return "—";
|
|
468
|
+
return escapeHtml(a.objectType + "/" + a.contentType + "/" + a.recordId);
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
function stableKey(e) {
|
|
472
|
+
if (e.lane === "index") {
|
|
473
|
+
const snap = e.snapshot && e.snapshot.recordId || "";
|
|
474
|
+
const src = e.source && e.source.recordId || "";
|
|
475
|
+
return ["index", e.operationId || "", e.status || "", e.ts || "", snap, src].join("|");
|
|
476
|
+
}
|
|
477
|
+
if (e.lane === "fetch") {
|
|
478
|
+
return ["fetch", e.ts || "", e.mode || "", e.hit ? "1" : "0", e.recordId || "", e.objectType || "", e.contentType || ""].join("|");
|
|
479
|
+
}
|
|
480
|
+
return ["land", e.ts || "", e.objectType || "", e.contentType || "", e.recordId || "", e.revision ?? "", e.outcome || ""].join("|");
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
function countNew() {
|
|
484
|
+
const have = new Set(entries.map(stableKey));
|
|
485
|
+
pendingNew = liveEntries.filter((e) => !have.has(stableKey(e))).length;
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
function updatePauseBar() {
|
|
489
|
+
const bar = $("pause-bar");
|
|
490
|
+
if (!listHeld()) {
|
|
491
|
+
bar.hidden = true;
|
|
492
|
+
return;
|
|
493
|
+
}
|
|
494
|
+
bar.hidden = false;
|
|
495
|
+
const why = "Scrolled — list held in place";
|
|
496
|
+
$("pause-msg").textContent = pendingNew > 0 ? why + " · " + pendingNew + " new" : why;
|
|
497
|
+
$("resume").textContent = pendingNew > 0 ? "Show " + pendingNew + " new" : "Resume live";
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
function applyLive() {
|
|
501
|
+
entries = liveEntries;
|
|
502
|
+
pendingNew = 0;
|
|
503
|
+
followLive = true;
|
|
504
|
+
const sc = $("table-scroll");
|
|
505
|
+
ignoreScroll = true;
|
|
506
|
+
render();
|
|
507
|
+
if (sc) sc.scrollTop = 0;
|
|
508
|
+
updatePauseBar();
|
|
509
|
+
requestAnimationFrame(() => { ignoreScroll = false; });
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
function sparkline(values, color) {
|
|
513
|
+
const w = 280, h = 56, pad = 4;
|
|
514
|
+
const nums = values.filter((v) => typeof v === "number" && Number.isFinite(v));
|
|
515
|
+
if (nums.length < 2) {
|
|
516
|
+
return '<svg viewBox="0 0 ' + w + " " + h + '" preserveAspectRatio="none" aria-hidden="true">' +
|
|
517
|
+
'<text x="8" y="32" fill="#5c645c" font-size="11" font-family="IBM Plex Mono, monospace">collecting…</text></svg>';
|
|
518
|
+
}
|
|
519
|
+
const min = Math.min(...nums);
|
|
520
|
+
const max = Math.max(...nums);
|
|
521
|
+
const span = max - min || 1;
|
|
522
|
+
const pts = nums.map((v, i) => {
|
|
523
|
+
const x = pad + (i / (nums.length - 1)) * (w - pad * 2);
|
|
524
|
+
const y = h - pad - ((v - min) / span) * (h - pad * 2);
|
|
525
|
+
return x.toFixed(1) + "," + y.toFixed(1);
|
|
526
|
+
}).join(" ");
|
|
527
|
+
const last = nums[nums.length - 1];
|
|
528
|
+
const lastX = pad + (w - pad * 2);
|
|
529
|
+
const lastY = h - pad - ((last - min) / span) * (h - pad * 2);
|
|
530
|
+
return '<svg viewBox="0 0 ' + w + " " + h + '" preserveAspectRatio="none" aria-hidden="true">' +
|
|
531
|
+
'<polyline fill="none" stroke="' + color + '" stroke-width="1.75" points="' + pts + '" />' +
|
|
532
|
+
'<circle cx="' + lastX.toFixed(1) + '" cy="' + lastY.toFixed(1) + '" r="2.5" fill="' + color + '" /></svg>';
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
function renderCharts() {
|
|
536
|
+
const b = seriesBuckets;
|
|
537
|
+
const sum = (key) => b.reduce((n, x) => n + (Number(x[key]) || 0), 0);
|
|
538
|
+
const last = (key) => (b.length ? Number(b[b.length - 1][key]) || 0 : 0);
|
|
539
|
+
const cards = [
|
|
540
|
+
{
|
|
541
|
+
title: "Writes · land",
|
|
542
|
+
value: sizes.land,
|
|
543
|
+
unit: "in ring",
|
|
544
|
+
series: b.map((x) => x.land),
|
|
545
|
+
color: "#c8f082",
|
|
546
|
+
hint: "last min " + last("land") + " · Σ60m " + sum("land"),
|
|
547
|
+
},
|
|
548
|
+
{
|
|
549
|
+
title: "Connect · Nexum",
|
|
550
|
+
value: sizes.index,
|
|
551
|
+
unit: "in ring",
|
|
552
|
+
series: b.map((x) => x.connect),
|
|
553
|
+
color: "#7eb8d4",
|
|
554
|
+
hint: "rules Σ " + sum("connectRules") + " · AI Σ " + sum("connectAi"),
|
|
555
|
+
},
|
|
556
|
+
{
|
|
557
|
+
title: "Relations created",
|
|
558
|
+
value: sum("relations"),
|
|
559
|
+
unit: "Σ60m",
|
|
560
|
+
series: b.map((x) => x.relations),
|
|
561
|
+
color: "#d4a574",
|
|
562
|
+
hint: "Memorix relationship plans from connected graphs",
|
|
563
|
+
},
|
|
564
|
+
{
|
|
565
|
+
title: "Reads · fetch",
|
|
566
|
+
value: sizes.fetch,
|
|
567
|
+
unit: "in ring",
|
|
568
|
+
series: b.map((x) => x.fetch),
|
|
569
|
+
color: "#3dba7a",
|
|
570
|
+
hint: "last min " + last("fetch") + " · Σ60m " + sum("fetch"),
|
|
571
|
+
},
|
|
572
|
+
];
|
|
573
|
+
$("charts").innerHTML = cards.map((c) =>
|
|
574
|
+
'<div class="health-card"><h3>' + escapeHtml(c.title) + "</h3>" +
|
|
575
|
+
'<div class="val">' + escapeHtml(c.value) + "<span>" + escapeHtml(c.unit) + "</span></div>" +
|
|
576
|
+
sparkline(c.series, c.color) +
|
|
577
|
+
'<div class="hint">' + escapeHtml(c.hint) + "</div></div>"
|
|
578
|
+
).join("");
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
function layoutNodes(labels, edges) {
|
|
582
|
+
const ids = [];
|
|
583
|
+
const seen = new Set();
|
|
584
|
+
for (const lab of labels || []) {
|
|
585
|
+
if (!seen.has(lab)) { seen.add(lab); ids.push(lab); }
|
|
586
|
+
}
|
|
587
|
+
for (const e of edges || []) {
|
|
588
|
+
if (e.from && !seen.has(e.from)) { seen.add(e.from); ids.push(e.from); }
|
|
589
|
+
if (e.to && !seen.has(e.to)) { seen.add(e.to); ids.push(e.to); }
|
|
590
|
+
}
|
|
591
|
+
const n = Math.max(ids.length, 1);
|
|
592
|
+
const cx = 320, cy = 110, r = Math.min(90, 40 + n * 6);
|
|
593
|
+
const pos = new Map();
|
|
594
|
+
ids.forEach((id, i) => {
|
|
595
|
+
const ang = (Math.PI * 2 * i) / n - Math.PI / 2;
|
|
596
|
+
pos.set(id, { x: cx + r * Math.cos(ang), y: cy + r * Math.sin(ang), id });
|
|
597
|
+
});
|
|
598
|
+
return { ids, pos };
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
function renderGraphSvg(nodeLabels, edgeSummaries) {
|
|
602
|
+
const { ids, pos } = layoutNodes(nodeLabels, edgeSummaries);
|
|
603
|
+
if (!ids.length) return "";
|
|
604
|
+
let lines = "";
|
|
605
|
+
for (const e of edgeSummaries || []) {
|
|
606
|
+
const a = pos.get(e.from), b = pos.get(e.to);
|
|
607
|
+
if (!a || !b) continue;
|
|
608
|
+
lines += '<line x1="' + a.x + '" y1="' + a.y + '" x2="' + b.x + '" y2="' + b.y + '" />';
|
|
609
|
+
}
|
|
610
|
+
let nodes = "";
|
|
611
|
+
for (const id of ids) {
|
|
612
|
+
const p = pos.get(id);
|
|
613
|
+
const short = id.length > 18 ? id.slice(0, 16) + "…" : id;
|
|
614
|
+
nodes += '<circle cx="' + p.x + '" cy="' + p.y + '" r="6" />';
|
|
615
|
+
nodes += '<text x="' + p.x + '" y="' + (p.y + 18) + '" text-anchor="middle">' + escapeHtml(short) + "</text>";
|
|
616
|
+
}
|
|
617
|
+
return '<svg class="graph-svg" viewBox="0 0 640 220" xmlns="http://www.w3.org/2000/svg">' +
|
|
618
|
+
lines + nodes + "</svg>";
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
function renderDetail(e) {
|
|
622
|
+
if (e.lane === "land") {
|
|
623
|
+
return '<div class="detail"><h3>Land · write</h3>' +
|
|
624
|
+
'<div>address <span class="mono">' + escapeHtml(e.objectType + "/" + e.contentType + "/" + e.recordId) + "</span></div>" +
|
|
625
|
+
"<div>revision <strong>" + escapeHtml(e.revision) + "</strong> · outcome " + escapeHtml(e.outcome) + "</div>" +
|
|
626
|
+
(e.indexing
|
|
627
|
+
? "<div>Nexum " + nexumLandCell(e.indexing) +
|
|
628
|
+
(e.indexing.reason
|
|
629
|
+
? ' <span class="mono">(' + escapeHtml(e.indexing.reason) + ")</span>"
|
|
630
|
+
: "") +
|
|
631
|
+
"</div>"
|
|
632
|
+
: "") +
|
|
633
|
+
"</div>";
|
|
634
|
+
}
|
|
635
|
+
if (e.lane === "fetch") {
|
|
636
|
+
return '<div class="detail"><h3>Fetch · read</h3>' +
|
|
637
|
+
"<div>mode " + escapeHtml(e.mode) + " · hit " + (e.hit ? "yes" : "no") +
|
|
638
|
+
(e.count != null ? " · count " + e.count : "") +
|
|
639
|
+
(e.knowledgeGraph ? " · knowledge-graph subject" : "") +
|
|
640
|
+
"</div></div>";
|
|
641
|
+
}
|
|
642
|
+
let html = '<div class="detail"><h3>Connect · Nexum</h3>';
|
|
643
|
+
html += "<div>status " + statusBadge(e.status) + "</div>";
|
|
644
|
+
if (e.analyzeMode || e.analyzeMethod) {
|
|
645
|
+
html += "<div>analyze " + modeBadge(e.analyzeMode || "unknown") +
|
|
646
|
+
(e.analyzeMethod ? ' <span class="mono">(' + escapeHtml(e.analyzeMethod) + ")</span>" : "") +
|
|
647
|
+
"</div>";
|
|
648
|
+
}
|
|
649
|
+
if (e.operationId) html += '<div>operation <span class="mono">' + escapeHtml(e.operationId) + "</span></div>";
|
|
650
|
+
if (e.source) html += "<div>source " + addr(e.source) + "</div>";
|
|
651
|
+
if (e.snapshot) html += "<div>snapshot " + addr(e.snapshot) + "</div>";
|
|
652
|
+
if (e.sourceRefs && e.sourceRefs.length) {
|
|
653
|
+
html += '<div>sourceRefs <span class="mono">' + escapeHtml(e.sourceRefs.join(", ")) + "</span></div>";
|
|
654
|
+
}
|
|
655
|
+
html += "<div>nodes <strong>" + escapeHtml(e.nodeCount ?? 0) +
|
|
656
|
+
"</strong> · graph edges <strong>" + escapeHtml(e.edgeCount ?? 0) +
|
|
657
|
+
"</strong> · relations created <strong>" + escapeHtml(e.relationCount ?? 0) + "</strong></div>";
|
|
658
|
+
if (e.error) html += '<div style="color:var(--bad)">' + escapeHtml(e.error) + "</div>";
|
|
659
|
+
const edges = e.edgeSummaries || [];
|
|
660
|
+
if (edges.length) {
|
|
661
|
+
html += "<h3 style='margin-top:12px'>Edges</h3><ul class='edge-list'>";
|
|
662
|
+
for (const ed of edges) {
|
|
663
|
+
html += "<li>" + escapeHtml(ed.from) + " → " + escapeHtml(ed.to) +
|
|
664
|
+
(ed.label ? " <span class='mono'>(" + escapeHtml(ed.label) + ")</span>" : "") +
|
|
665
|
+
"</li>";
|
|
666
|
+
}
|
|
667
|
+
html += "</ul>";
|
|
668
|
+
}
|
|
669
|
+
if ((e.nodeLabels && e.nodeLabels.length) || edges.length) {
|
|
670
|
+
html += renderGraphSvg(e.nodeLabels || [], edges);
|
|
671
|
+
}
|
|
672
|
+
html += "</div>";
|
|
673
|
+
return html;
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
/** Land/fetch store OT/CT on the event. Connect stores them on source. */
|
|
677
|
+
function typeOf(e) {
|
|
678
|
+
const src = e.source || {};
|
|
679
|
+
return {
|
|
680
|
+
ot: src.objectType || e.objectType || "",
|
|
681
|
+
ct: src.contentType || e.contentType || "",
|
|
682
|
+
};
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
function typeCells(e) {
|
|
686
|
+
const t = typeOf(e);
|
|
687
|
+
return [
|
|
688
|
+
'<span class="mono">' + escapeHtml(t.ot || "—") + "</span>",
|
|
689
|
+
'<span class="mono">' + escapeHtml(t.ct || "—") + "</span>",
|
|
690
|
+
];
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
function rowCells(e) {
|
|
694
|
+
if (e.lane === "land") {
|
|
695
|
+
return [
|
|
696
|
+
fmtTime(e.ts),
|
|
697
|
+
escapeHtml(e.orgId),
|
|
698
|
+
escapeHtml(e.caller || "—"),
|
|
699
|
+
...typeCells(e),
|
|
700
|
+
'<span class="mono">' + escapeHtml(e.recordId) + "</span>",
|
|
701
|
+
escapeHtml(e.outcome),
|
|
702
|
+
nexumLandCell(e.indexing),
|
|
703
|
+
];
|
|
704
|
+
}
|
|
705
|
+
if (e.lane === "fetch") {
|
|
706
|
+
return [
|
|
707
|
+
fmtTime(e.ts),
|
|
708
|
+
escapeHtml(e.orgId),
|
|
709
|
+
escapeHtml(e.caller || "—"),
|
|
710
|
+
...typeCells(e),
|
|
711
|
+
escapeHtml(e.mode),
|
|
712
|
+
'<span class="mono">' + escapeHtml(e.recordId || "—") + "</span>",
|
|
713
|
+
statusBadge(e.hit ? "hit" : "miss"),
|
|
714
|
+
];
|
|
715
|
+
}
|
|
716
|
+
return [
|
|
717
|
+
fmtTime(e.ts),
|
|
718
|
+
escapeHtml(e.orgId),
|
|
719
|
+
statusBadge(e.status),
|
|
720
|
+
modeBadge(e.analyzeMode || (e.status === "connected" ? "unknown" : "")),
|
|
721
|
+
...typeCells(e),
|
|
722
|
+
e.snapshot
|
|
723
|
+
? '<span class="mono">' + escapeHtml(e.snapshot.recordId) + "</span>"
|
|
724
|
+
: "—",
|
|
725
|
+
(e.relationCount != null || e.edgeCount != null)
|
|
726
|
+
? escapeHtml((e.relationCount ?? 0) + " rel / " + (e.edgeCount ?? 0) + " edges")
|
|
727
|
+
: "—",
|
|
728
|
+
];
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
function headersForLane() {
|
|
732
|
+
if (lane === "land") return ["Time", "Org", "Caller", "OT", "CT", "Record", "Outcome", "Nexum"];
|
|
733
|
+
if (lane === "fetch") return ["Time", "Org", "Caller", "OT", "CT", "Mode", "Record", "Hit"];
|
|
734
|
+
return ["Time", "Org", "Status", "How", "OT", "CT", "Snapshot", "Relations"];
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
const LANE_HINT = {
|
|
738
|
+
land: "<strong>Land</strong> — writes. <em>Nexum</em> column: skipped (with why) or queued · analyze. Rules vs AI only appear on Connect after analyze finishes.",
|
|
739
|
+
index: "<strong>Connect</strong> — Nexum graph builds. How = <em>rules · no AI</em> or <em>AI</em>. Relations = relationship plans created.",
|
|
740
|
+
fetch: "<strong>Fetch</strong> — recent reads (up to " + capacity + ").",
|
|
741
|
+
};
|
|
742
|
+
|
|
743
|
+
function emptyMessage() {
|
|
744
|
+
if (lane === "land") return "No writes match these filters (or the ring is empty).";
|
|
745
|
+
if (lane === "index") return "No Nexum connects match these filters (or none yet).";
|
|
746
|
+
return "No reads match these filters (or the ring is empty).";
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
function fillSelect(sel, values, current) {
|
|
750
|
+
const keep = current || "";
|
|
751
|
+
const opts = ['<option value="">All</option>'].concat(
|
|
752
|
+
values.map((v) => '<option value="' + escapeHtml(v) + '"' +
|
|
753
|
+
(v === keep ? " selected" : "") + ">" + escapeHtml(v) + "</option>")
|
|
754
|
+
);
|
|
755
|
+
sel.innerHTML = opts.join("");
|
|
756
|
+
if ([...sel.options].some((o) => o.value === keep)) sel.value = keep;
|
|
757
|
+
else sel.value = "";
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
function refreshFilterOptions(all) {
|
|
761
|
+
const orgs = [...new Set(all.map((e) => e.orgId).filter(Boolean))].sort();
|
|
762
|
+
const callers = [...new Set(all.map((e) => e.caller).filter(Boolean))].sort();
|
|
763
|
+
const ots = [...new Set(all.map((e) => typeOf(e).ot).filter(Boolean))].sort();
|
|
764
|
+
const cts = [...new Set(all.filter((e) => !filterState.ot || typeOf(e).ot === filterState.ot).map((e) => typeOf(e).ct).filter(Boolean))].sort();
|
|
765
|
+
fillSelect($("f-org"), orgs, filterState.org);
|
|
766
|
+
fillSelect($("f-caller"), callers, filterState.caller);
|
|
767
|
+
fillSelect($("f-ot"), ots, filterState.ot);
|
|
768
|
+
fillSelect($("f-ct"), cts, filterState.ct);
|
|
769
|
+
if (filterState.ot && !$("f-ot").value) filterState.ot = "";
|
|
770
|
+
if (filterState.ct && !$("f-ct").value) filterState.ct = "";
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
function filteredEntries() {
|
|
774
|
+
return entries.filter((e) => {
|
|
775
|
+
if (filterState.org && e.orgId !== filterState.org) return false;
|
|
776
|
+
if (filterState.caller && e.caller !== filterState.caller) return false;
|
|
777
|
+
const types = typeOf(e);
|
|
778
|
+
if (filterState.ot && types.ot !== filterState.ot) return false;
|
|
779
|
+
if (filterState.ct && types.ct !== filterState.ct) return false;
|
|
780
|
+
if (filterState.q) {
|
|
781
|
+
const hay = [
|
|
782
|
+
e.orgId, e.caller, e.recordId, e.objectType, e.contentType,
|
|
783
|
+
e.source && e.source.recordId, e.snapshot && e.snapshot.recordId,
|
|
784
|
+
e.status, e.analyzeMode,
|
|
785
|
+
e.indexing && e.indexing.status, e.indexing && e.indexing.reason,
|
|
786
|
+
].filter(Boolean).join(" ").toLowerCase();
|
|
787
|
+
if (!hay.includes(filterState.q)) return false;
|
|
788
|
+
}
|
|
789
|
+
if (lane === "land" && filterState.nexum) {
|
|
790
|
+
const st = (e.indexing && e.indexing.status) || "";
|
|
791
|
+
if (st !== filterState.nexum) return false;
|
|
792
|
+
}
|
|
793
|
+
if (lane === "index" && filterState.how) {
|
|
794
|
+
const how = e.analyzeMode || "unknown";
|
|
795
|
+
if (how !== filterState.how) return false;
|
|
796
|
+
}
|
|
797
|
+
if (lane === "fetch" && filterState.hit) {
|
|
798
|
+
if (filterState.hit === "hit" && !e.hit) return false;
|
|
799
|
+
if (filterState.hit === "miss" && e.hit) return false;
|
|
800
|
+
}
|
|
801
|
+
return true;
|
|
802
|
+
});
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
function updateSummary(shownCount, totalCount) {
|
|
806
|
+
$("summary").innerHTML =
|
|
807
|
+
"<span>writes <strong>" + sizes.land + "/" + capacity + "</strong></span>" +
|
|
808
|
+
"<span>connects <strong>" + sizes.index + "/" + capacity + "</strong></span>" +
|
|
809
|
+
"<span>reads <strong>" + sizes.fetch + "/" + capacity + "</strong></span>" +
|
|
810
|
+
"<span>showing <strong>" + shownCount + "</strong> of <strong>" + totalCount + "</strong></span>";
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
function render() {
|
|
814
|
+
$("lane-hint").innerHTML = LANE_HINT[lane] || "";
|
|
815
|
+
refreshFilterOptions(entries);
|
|
816
|
+
const shown = filteredEntries();
|
|
817
|
+
updateSummary(shown.length, entries.length);
|
|
818
|
+
renderCharts();
|
|
819
|
+
updatePauseBar();
|
|
820
|
+
|
|
821
|
+
const sc = $("table-scroll");
|
|
822
|
+
const prevScroll = sc.scrollTop;
|
|
823
|
+
ignoreScroll = true;
|
|
824
|
+
|
|
825
|
+
if (!shown.length) {
|
|
826
|
+
sc.innerHTML = "<p class='empty'>" + escapeHtml(emptyMessage()) + "</p>";
|
|
827
|
+
requestAnimationFrame(() => { ignoreScroll = false; });
|
|
828
|
+
return;
|
|
829
|
+
}
|
|
830
|
+
const heads = headersForLane();
|
|
831
|
+
const colW = lane === "fetch"
|
|
832
|
+
? ["8%","7%","10%","12%","14%","7%","30%","12%"]
|
|
833
|
+
: lane === "index"
|
|
834
|
+
? ["8%","7%","9%","11%","12%","14%","23%","16%"]
|
|
835
|
+
: ["8%","7%","11%","12%","14%","26%","8%","14%"];
|
|
836
|
+
let html = '<table class="list"><colgroup>' +
|
|
837
|
+
colW.map((w) => '<col style="width:' + w + '">').join("") +
|
|
838
|
+
"</colgroup><thead><tr>" + heads.map((h) => "<th>" + h + "</th>").join("") + "</tr></thead><tbody>";
|
|
839
|
+
shown.forEach((e) => {
|
|
840
|
+
const key = stableKey(e);
|
|
841
|
+
const cells = rowCells(e);
|
|
842
|
+
html += '<tr class="clickable" data-key="' + escapeHtml(key) + '">' +
|
|
843
|
+
cells.map((c) => {
|
|
844
|
+
const tip = String(c).replace(/<[^>]*>/g, "").replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
845
|
+
return '<td title="' + escapeHtml(tip) + '">' + c + "</td>";
|
|
846
|
+
}).join("") + "</tr>";
|
|
847
|
+
});
|
|
848
|
+
html += "</tbody></table>";
|
|
849
|
+
sc.innerHTML = html;
|
|
850
|
+
if (listHeld()) sc.scrollTop = prevScroll;
|
|
851
|
+
requestAnimationFrame(() => { ignoreScroll = false; });
|
|
852
|
+
sc.querySelectorAll("tr.clickable").forEach((tr) => {
|
|
853
|
+
tr.addEventListener("click", () => {
|
|
854
|
+
const k = tr.getAttribute("data-key");
|
|
855
|
+
const e = shown.find((row) => stableKey(row) === k);
|
|
856
|
+
if (e) openDialog(e);
|
|
857
|
+
});
|
|
858
|
+
});
|
|
859
|
+
}
|
|
860
|
+
|
|
861
|
+
async function load(applyNow) {
|
|
862
|
+
try {
|
|
863
|
+
const [actRes, serRes] = await Promise.all([
|
|
864
|
+
fetch("/_live/api/activity?lane=" + encodeURIComponent(lane) + "&limit=10000"),
|
|
865
|
+
fetch("/_live/api/activity-series?minutes=60"),
|
|
866
|
+
]);
|
|
867
|
+
const body = await actRes.json();
|
|
868
|
+
if (!actRes.ok || body.ok === false) throw new Error(body?.error?.message || ("HTTP " + actRes.status));
|
|
869
|
+
liveEntries = body.entries || [];
|
|
870
|
+
sizes = body.sizes || sizes;
|
|
871
|
+
capacity = body.capacity || 10000;
|
|
872
|
+
if (serRes.ok) {
|
|
873
|
+
const ser = await serRes.json();
|
|
874
|
+
seriesBuckets = ser.buckets || [];
|
|
875
|
+
if (ser.sizes) sizes = ser.sizes;
|
|
876
|
+
}
|
|
877
|
+
renderCharts();
|
|
878
|
+
if (applyNow || !listHeld()) {
|
|
879
|
+
entries = liveEntries;
|
|
880
|
+
pendingNew = 0;
|
|
881
|
+
render();
|
|
882
|
+
} else {
|
|
883
|
+
countNew();
|
|
884
|
+
const shown = filteredEntries();
|
|
885
|
+
updateSummary(shown.length, entries.length);
|
|
886
|
+
updatePauseBar();
|
|
887
|
+
}
|
|
888
|
+
} catch (err) {
|
|
889
|
+
$("table-scroll").innerHTML = "<p class='empty'>" + escapeHtml(err.message || String(err)) + "</p>";
|
|
890
|
+
}
|
|
891
|
+
}
|
|
892
|
+
|
|
893
|
+
load();
|
|
894
|
+
setInterval(load, 1500);
|
|
895
|
+
</script>
|
|
896
|
+
</body>
|
|
897
|
+
</html>`;
|
|
898
|
+
}
|
|
899
|
+
//# sourceMappingURL=activity-ui.js.map
|