@x12i/memorix-service 3.7.6 → 3.7.7

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 (57) hide show
  1. package/dist/activity-ring.d.ts +16 -0
  2. package/dist/activity-ring.d.ts.map +1 -1
  3. package/dist/activity-ring.js +15 -0
  4. package/dist/activity-ring.js.map +1 -1
  5. package/dist/activity-routes.d.ts.map +1 -1
  6. package/dist/activity-routes.js +6 -0
  7. package/dist/activity-routes.js.map +1 -1
  8. package/dist/activity-ui.d.ts +1 -1
  9. package/dist/activity-ui.d.ts.map +1 -1
  10. package/dist/activity-ui.js +383 -177
  11. package/dist/activity-ui.js.map +1 -1
  12. package/dist/app.d.ts.map +1 -1
  13. package/dist/app.js +8 -1
  14. package/dist/app.js.map +1 -1
  15. package/dist/catalog-bridge.d.ts.map +1 -1
  16. package/dist/catalog-bridge.js +6 -0
  17. package/dist/catalog-bridge.js.map +1 -1
  18. package/dist/cli.d.ts.map +1 -1
  19. package/dist/cli.js +9 -1
  20. package/dist/cli.js.map +1 -1
  21. package/dist/data-changes-ui.js +1 -1
  22. package/dist/graph-ui.d.ts +3 -0
  23. package/dist/graph-ui.d.ts.map +1 -0
  24. package/dist/graph-ui.js +676 -0
  25. package/dist/graph-ui.js.map +1 -0
  26. package/dist/knowledge-upstream.d.ts +13 -0
  27. package/dist/knowledge-upstream.d.ts.map +1 -0
  28. package/dist/knowledge-upstream.js +38 -0
  29. package/dist/knowledge-upstream.js.map +1 -0
  30. package/dist/live-ops-nav.d.ts +2 -0
  31. package/dist/live-ops-nav.d.ts.map +1 -0
  32. package/dist/live-ops-nav.js +53 -0
  33. package/dist/live-ops-nav.js.map +1 -0
  34. package/dist/live.d.ts.map +1 -1
  35. package/dist/mcp/nexum-tools.d.ts +28 -0
  36. package/dist/mcp/nexum-tools.d.ts.map +1 -0
  37. package/dist/mcp/nexum-tools.js +257 -0
  38. package/dist/mcp/nexum-tools.js.map +1 -0
  39. package/dist/mcp/service-public-url.d.ts +7 -0
  40. package/dist/mcp/service-public-url.d.ts.map +1 -0
  41. package/dist/mcp/service-public-url.js +19 -0
  42. package/dist/mcp/service-public-url.js.map +1 -0
  43. package/dist/routes/data.d.ts.map +1 -1
  44. package/dist/routes/data.js +8 -2
  45. package/dist/routes/data.js.map +1 -1
  46. package/dist/routes/knowledge.d.ts.map +1 -1
  47. package/dist/routes/knowledge.js +9 -37
  48. package/dist/routes/knowledge.js.map +1 -1
  49. package/dist/routes/nexum-mcp.d.ts +9 -0
  50. package/dist/routes/nexum-mcp.d.ts.map +1 -0
  51. package/dist/routes/nexum-mcp.js +95 -0
  52. package/dist/routes/nexum-mcp.js.map +1 -0
  53. package/dist/safe-query.d.ts +28 -0
  54. package/dist/safe-query.d.ts.map +1 -0
  55. package/dist/safe-query.js +104 -0
  56. package/dist/safe-query.js.map +1 -0
  57. package/package.json +1 -1
@@ -1,4 +1,4 @@
1
- /** HTML for GET /_live/activity — land / connect / fetch operator feed. */
1
+ /** HTML for GET /_live/activity — pushed / processed / fetched operator feeds. */
2
2
  export function activityHtml() {
3
3
  return `<!DOCTYPE html>
4
4
  <html lang="en">
@@ -42,6 +42,13 @@ export function activityHtml() {
42
42
  margin: 0 0 10px; font-size: 12px; font-family: var(--mono);
43
43
  letter-spacing: 0.08em; text-transform: uppercase; color: var(--dim);
44
44
  }
45
+ .quiet-banner {
46
+ margin: 0 0 18px; padding: 14px 16px;
47
+ border: 1px solid #e2b25a; background: #1c1810;
48
+ color: var(--text); font-size: 15px; line-height: 1.5;
49
+ }
50
+ .quiet-banner[hidden] { display: none; }
51
+ .quiet-banner strong { color: #e2b25a; font-weight: 600; }
45
52
  .health-grid {
46
53
  display: grid; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
47
54
  gap: 14px; margin-bottom: 20px;
@@ -60,28 +67,30 @@ export function activityHtml() {
60
67
  .health-card .val span { color: var(--muted); font-size: 12px; font-weight: 500; margin-left: 6px; }
61
68
  .health-card svg { width: 100%; height: 56px; display: block; }
62
69
  .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);
70
+ .tabs {
71
+ display: grid; grid-template-columns: repeat(3, minmax(0, 1fr));
72
+ gap: 8px; margin: 0 0 16px;
66
73
  }
67
- .legend-row {
68
- display: grid; grid-template-columns: 100px 1fr; gap: 12px; align-items: baseline;
69
- font-size: 13px;
74
+ .tab, .btn {
75
+ appearance: none; border: 1px solid var(--line); background: var(--bg-elev); color: var(--muted);
76
+ font: 500 12px/1.3 var(--sans); padding: 12px 14px; cursor: pointer; border-radius: 2px;
70
77
  }
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;
78
+ .tab {
79
+ display: flex; flex-direction: column; align-items: flex-start; gap: 4px; text-align: left;
74
80
  }
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
+ .tab-name {
82
+ font-family: var(--mono); font-size: 12px; letter-spacing: 0.06em;
83
+ text-transform: uppercase; font-weight: 600; color: var(--text);
81
84
  }
85
+ .tab-desc { font-size: 12px; font-weight: 400; color: var(--muted); }
82
86
  .tab[aria-selected="true"] {
83
87
  background: var(--accent); color: var(--accent-ink); border-color: var(--accent);
84
88
  }
89
+ .tab[aria-selected="true"] .tab-name,
90
+ .tab[aria-selected="true"] .tab-desc { color: var(--accent-ink); }
91
+ @media (max-width: 720px) {
92
+ .tabs { grid-template-columns: 1fr; }
93
+ }
85
94
  .btn { background: var(--bg-row); color: var(--text); text-transform: none; letter-spacing: 0; }
86
95
  .btn:hover { border-color: var(--accent); }
87
96
  .lane-hint { margin: 0 0 14px; color: var(--muted); font-size: 13px; max-width: 72ch; }
@@ -113,10 +122,9 @@ export function activityHtml() {
113
122
  font-family: var(--mono); font-size: 12px;
114
123
  }
115
124
  .pause-bar[hidden] { display: none; }
116
- .table-scroll {
117
- max-height: min(62vh, 720px);
118
- overflow: auto;
125
+ .table-wrap {
119
126
  border: 1px solid var(--line);
127
+ background: var(--bg-elev);
120
128
  }
121
129
  table.list {
122
130
  width: 100%; table-layout: fixed; border-collapse: collapse;
@@ -126,23 +134,21 @@ export function activityHtml() {
126
134
  text-align: left; padding: 8px 10px; border-bottom: 1px solid var(--line);
127
135
  vertical-align: middle; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
128
136
  }
129
- table.list td.detail-cell {
130
- white-space: normal; overflow: visible; text-overflow: clip; vertical-align: top;
131
- }
132
137
  th {
133
138
  font-family: var(--mono); font-size: 10px; letter-spacing: 0.08em; text-transform: uppercase;
134
139
  color: var(--dim); font-weight: 500; background: #0d1016;
140
+ position: sticky; top: 0; z-index: 2;
135
141
  }
136
142
  tr:last-child td { border-bottom: 0; }
137
143
  tr:hover td { background: var(--bg-row); }
138
144
  tr.clickable { cursor: pointer; }
139
- tr.open td { background: #1a2214; }
140
145
  .mono { font-family: var(--mono); font-size: 12px; color: var(--muted); }
141
146
  .badge {
142
147
  display: inline-flex; align-items: center; gap: 6px; font-family: var(--mono);
143
148
  font-size: 11px; padding: 3px 8px; border: 1px solid var(--line);
149
+ max-width: 100%;
144
150
  }
145
- .badge::before { content: ""; width: 7px; height: 7px; border-radius: 50%; background: var(--dim); }
151
+ .badge::before { content: ""; width: 7px; height: 7px; border-radius: 50%; background: var(--dim); flex: 0 0 auto; }
146
152
  .badge.ok { color: var(--ok); }
147
153
  .badge.ok::before { background: var(--ok); }
148
154
  .badge.bad { color: var(--bad); }
@@ -151,11 +157,13 @@ export function activityHtml() {
151
157
  .badge.queued::before { background: var(--api); }
152
158
  .badge.ai { color: var(--ai); }
153
159
  .badge.ai::before { background: var(--ai); }
160
+ .badge.skip { color: var(--dim); }
154
161
  .detail {
155
162
  margin-top: 8px; padding: 12px; background: #0d1016; border: 1px solid var(--line);
156
163
  font-family: var(--mono); font-size: 11px; color: var(--muted);
157
164
  }
158
165
  .detail h3 { margin: 0 0 8px; font-size: 12px; color: var(--text); font-weight: 600; letter-spacing: 0.04em; text-transform: uppercase; }
166
+ .detail .row { margin: 4px 0; }
159
167
  .edge-list { margin: 0; padding-left: 18px; }
160
168
  .edge-list li { margin: 2px 0; }
161
169
  .graph-svg {
@@ -167,26 +175,49 @@ export function activityHtml() {
167
175
  .graph-svg line { stroke: var(--api); stroke-width: 1.2; opacity: 0.85; }
168
176
  .empty { color: var(--muted); padding: 24px 0; }
169
177
  .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;
178
+ position: fixed; inset: 0; z-index: 40;
179
+ background: rgba(6, 8, 10, 0.45);
174
180
  }
175
- .dialog-back[hidden] { display: none; }
181
+ .dialog-back[hidden] { display: none !important; }
176
182
  .dialog {
177
- width: min(920px, 100%);
178
- background: var(--bg-elev); border: 1px solid var(--line);
179
- padding: 14px 18px 20px;
183
+ position: fixed; top: 0; right: 0; bottom: 0;
184
+ width: min(440px, 100vw);
185
+ margin: 0;
186
+ display: flex; flex-direction: column;
187
+ background: var(--bg-elev);
188
+ border-left: 1px solid var(--line);
189
+ box-shadow: -20px 0 48px rgba(0, 0, 0, 0.5);
180
190
  }
181
191
  .dialog-bar {
182
- display: flex; align-items: center; justify-content: space-between; gap: 12px;
183
- margin-bottom: 8px;
192
+ flex: 0 0 auto;
193
+ display: flex; align-items: flex-start; justify-content: space-between; gap: 12px;
194
+ padding: 16px 16px 14px;
195
+ background: #0d1016;
196
+ border-bottom: 1px solid var(--line);
197
+ }
198
+ .dialog-kicker {
199
+ font-family: var(--mono); font-size: 10px; letter-spacing: 0.08em;
200
+ text-transform: uppercase; color: var(--accent); margin-bottom: 4px;
184
201
  }
185
202
  .dialog-bar strong {
186
- font-size: 13px; letter-spacing: 0.04em; text-transform: uppercase;
203
+ display: block; font-size: 14px; font-weight: 600; letter-spacing: 0;
204
+ text-transform: none; line-height: 1.35; word-break: break-word;
187
205
  }
188
- .dialog .detail { margin-top: 0; border: 0; background: transparent; padding: 4px 0 0; }
189
- .dialog .graph-svg { max-width: none; height: 280px; }
206
+ #dialog-body {
207
+ flex: 1 1 auto; overflow: auto;
208
+ padding: 16px 16px 32px;
209
+ }
210
+ .facts {
211
+ margin: 0; display: grid; grid-template-columns: 96px 1fr; gap: 10px 12px;
212
+ font-size: 13px;
213
+ }
214
+ .facts dt {
215
+ margin: 0; font-family: var(--mono); font-size: 10px; letter-spacing: 0.06em;
216
+ text-transform: uppercase; color: var(--dim); padding-top: 3px;
217
+ }
218
+ .facts dd { margin: 0; color: var(--text); word-break: break-word; }
219
+ .dialog .detail { margin-top: 16px; }
220
+ .dialog .graph-svg { max-width: none; height: 220px; }
190
221
  footer { margin-top: 28px; color: var(--dim); font-size: 12px; }
191
222
  footer a { margin-right: 12px; }
192
223
  </style>
@@ -196,41 +227,33 @@ export function activityHtml() {
196
227
  <header>
197
228
  <div>
198
229
  <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>
230
+ <p class="sub">Three feeds: data pushed in, data Nexum is processing, and data fetched out. In-memory — last ~10 000 per feed; clears on process restart.</p>
200
231
  </div>
201
232
  <div class="meta">
202
233
  <div><strong>UI</strong> /_live/activity</div>
203
234
  <div><strong>API</strong> /_live/api/activity</div>
204
- <div><a href="/_live">← live view</a> · <a href="/_live/changes">data changes</a></div>
235
+ <div><a href="/_live">← live view</a> · <a href="/_live/graph">stored graph</a> · <a href="/_live/changes">data changes</a></div>
205
236
  </div>
206
237
  </header>
207
238
 
239
+ <div class="quiet-banner" id="quiet-banner" hidden></div>
240
+
208
241
  <p class="section-label">Activity · last 60 minutes</p>
209
242
  <div class="health-grid" id="charts"></div>
210
243
 
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>
244
+ <nav class="tabs" role="tablist" aria-label="Activity type">
245
+ <button class="tab" role="tab" aria-selected="true" data-lane="land">
246
+ <span class="tab-name">Pushed</span>
247
+ <span class="tab-desc">Data written into Memorix</span>
248
+ </button>
249
+ <button class="tab" role="tab" aria-selected="false" data-lane="index">
250
+ <span class="tab-name">Processed</span>
251
+ <span class="tab-desc">Nexum working on that data</span>
252
+ </button>
253
+ <button class="tab" role="tab" aria-selected="false" data-lane="fetch">
254
+ <span class="tab-name">Fetched</span>
255
+ <span class="tab-desc">Data read out of Memorix</span>
256
+ </button>
234
257
  </nav>
235
258
  <p class="lane-hint" id="lane-hint"></p>
236
259
 
@@ -244,10 +267,10 @@ export function activityHtml() {
244
267
  <label>Caller
245
268
  <select id="f-caller"><option value="">All</option></select>
246
269
  </label>
247
- <label>OT
270
+ <label>Object type
248
271
  <select id="f-ot"><option value="">All</option></select>
249
272
  </label>
250
- <label>CT
273
+ <label>Content type
251
274
  <select id="f-ct"><option value="">All</option></select>
252
275
  </label>
253
276
  <label id="f-nexum-wrap">Nexum
@@ -283,17 +306,21 @@ export function activityHtml() {
283
306
  <span id="pause-msg">List held</span>
284
307
  <button class="btn" id="resume" type="button">Show new</button>
285
308
  </div>
286
- <div class="table-scroll" id="table-scroll"></div>
309
+ <div class="table-wrap" id="table-wrap"></div>
287
310
  <footer>
288
311
  <a href="/_live">/_live</a>
312
+ <a href="/_live/graph">/_live/graph</a>
289
313
  <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.
314
+ Click a row to open a dialog. Live updates pause while a dialog is open or the page is scrolled down.
291
315
  </footer>
292
316
  </div>
293
317
  <div class="dialog-back" id="dialog-back" hidden>
294
318
  <div class="dialog" role="dialog" aria-modal="true" aria-labelledby="dialog-title">
295
319
  <div class="dialog-bar">
296
- <strong id="dialog-title">Item</strong>
320
+ <div>
321
+ <div class="dialog-kicker" id="dialog-kicker">Detail</div>
322
+ <strong id="dialog-title">Item</strong>
323
+ </div>
297
324
  <button class="btn" id="dialog-close" type="button">Close</button>
298
325
  </div>
299
326
  <div id="dialog-body"></div>
@@ -301,7 +328,17 @@ export function activityHtml() {
301
328
  </div>
302
329
  <script>
303
330
  const $ = (id) => document.getElementById(id);
304
- let lane = "land";
331
+ const LANE_FROM_HASH = {
332
+ pushed: "land", land: "land",
333
+ processed: "index", index: "index", connect: "index",
334
+ fetched: "fetch", fetch: "fetch",
335
+ };
336
+ const HASH_FROM_LANE = { land: "pushed", index: "processed", fetch: "fetched" };
337
+ function hashLane() {
338
+ const key = (location.hash || "").replace(/^#/, "").toLowerCase();
339
+ return LANE_FROM_HASH[key] || "land";
340
+ }
341
+ let lane = hashLane();
305
342
  let entries = [];
306
343
  let liveEntries = [];
307
344
  let followLive = true;
@@ -310,6 +347,8 @@ export function activityHtml() {
310
347
  let sizes = { land: 0, index: 0, fetch: 0 };
311
348
  let capacity = 10000;
312
349
  let seriesBuckets = [];
350
+ let startedAt = "";
351
+ let latest = { land: null, index: null, fetch: null };
313
352
  let filterState = { q: "", org: "", caller: "", ot: "", ct: "", nexum: "", how: "", hit: "" };
314
353
 
315
354
  function listHeld() {
@@ -317,39 +356,78 @@ export function activityHtml() {
317
356
  }
318
357
 
319
358
  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";
359
+ return e.recordId || (e.source && e.source.recordId) || (e.snapshot && e.snapshot.recordId) || "Item";
360
+ }
361
+
362
+ function laneLabel(e) {
363
+ if (e.lane === "index") return "Processed";
364
+ if (e.lane === "fetch") return "Fetched";
365
+ return "Pushed";
366
+ }
367
+
368
+ function fact(label, valueHtml) {
369
+ return "<dt>" + escapeHtml(label) + "</dt><dd>" + valueHtml + "</dd>";
370
+ }
371
+
372
+ function dialogOpen() {
373
+ return !$("dialog-back").hidden;
323
374
  }
324
375
 
325
376
  function openDialog(e) {
377
+ followLive = false;
378
+ document.body.style.overflow = "hidden";
379
+ $("dialog-kicker").textContent = laneLabel(e);
326
380
  $("dialog-title").textContent = dialogTitle(e);
327
381
  $("dialog-body").innerHTML = renderDetail(e);
328
382
  $("dialog-back").hidden = false;
383
+ updatePauseBar();
329
384
  }
330
385
 
331
386
  function closeDialog() {
332
387
  $("dialog-back").hidden = true;
333
388
  $("dialog-body").innerHTML = "";
389
+ document.body.style.overflow = "";
390
+ if (window.scrollY < 8) {
391
+ followLive = true;
392
+ applyLive();
393
+ } else {
394
+ updatePauseBar();
395
+ }
334
396
  }
335
397
 
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);
398
+ function applyLaneChrome() {
399
+ document.querySelectorAll(".tab").forEach((t) => {
400
+ t.setAttribute("aria-selected", t.dataset.lane === lane ? "true" : "false");
346
401
  });
402
+ const hash = "#" + (HASH_FROM_LANE[lane] || "pushed");
403
+ if (location.hash !== hash) history.replaceState(null, "", hash);
404
+ syncFilterVisibility();
405
+ }
406
+
407
+ function switchLane(next) {
408
+ if (!next || next === lane) return;
409
+ lane = next;
410
+ $("dialog-back").hidden = true;
411
+ $("dialog-body").innerHTML = "";
412
+ document.body.style.overflow = "";
413
+ followLive = true;
414
+ pendingNew = 0;
415
+ applyLaneChrome();
416
+ load(true);
417
+ }
418
+
419
+ document.querySelectorAll(".tab").forEach((tab) => {
420
+ tab.addEventListener("click", () => switchLane(tab.dataset.lane));
347
421
  });
422
+ window.addEventListener("hashchange", () => switchLane(hashLane()));
348
423
  $("refresh").onclick = () => {
349
424
  followLive = true;
350
425
  load(true);
351
426
  };
352
427
  $("resume").onclick = () => {
428
+ $("dialog-back").hidden = true;
429
+ $("dialog-body").innerHTML = "";
430
+ document.body.style.overflow = "";
353
431
  followLive = true;
354
432
  applyLive();
355
433
  };
@@ -360,9 +438,9 @@ export function activityHtml() {
360
438
  document.addEventListener("keydown", (ev) => {
361
439
  if (ev.key === "Escape" && !$("dialog-back").hidden) closeDialog();
362
440
  });
363
- $("table-scroll").addEventListener("scroll", () => {
364
- if (ignoreScroll) return;
365
- const atTop = $("table-scroll").scrollTop < 8;
441
+ window.addEventListener("scroll", () => {
442
+ if (ignoreScroll || dialogOpen()) return;
443
+ const atTop = window.scrollY < 8;
366
444
  if (atTop && !followLive) {
367
445
  followLive = true;
368
446
  if (pendingNew) applyLive();
@@ -371,7 +449,7 @@ export function activityHtml() {
371
449
  followLive = false;
372
450
  updatePauseBar();
373
451
  }
374
- });
452
+ }, { passive: true });
375
453
  $("f-clear").onclick = () => {
376
454
  filterState = { q: "", org: "", caller: "", ot: "", ct: "", nexum: "", how: "", hit: "" };
377
455
  $("f-q").value = "";
@@ -407,7 +485,7 @@ export function activityHtml() {
407
485
  $("f-how-wrap").hidden = lane !== "index";
408
486
  $("f-hit-wrap").hidden = lane !== "fetch";
409
487
  }
410
- syncFilterVisibility();
488
+ applyLaneChrome();
411
489
 
412
490
  function escapeHtml(s) {
413
491
  return String(s ?? "").replace(/[&<>"']/g, (c) => ({
@@ -433,31 +511,55 @@ export function activityHtml() {
433
511
  return '<span class="badge ' + cls + '">' + escapeHtml(s) + "</span>";
434
512
  }
435
513
 
436
- function shortSkipReason(reason) {
514
+ function skipKind(reason) {
437
515
  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);
516
+ if (!r) return { short: "skip", label: "skipped" };
517
+ if (r.includes("no matching")) return { short: "no-pol", label: "skipped · no policy" };
518
+ if (r.includes("disabled")) return { short: "off", label: "skipped · off" };
519
+ if (r.includes("worker unset")) return { short: "no-wk", label: "skipped · no worker" };
520
+ if (r.includes("no analyzeable") || r.includes("no payload")) return { short: "empty", label: "skipped · no payload" };
521
+ if (r.includes("operational")) return { short: "no-db", label: "skipped · no ops db" };
522
+ return { short: "skip", label: "skipped" };
445
523
  }
446
524
 
447
- /** Land-row Nexum cell: skipped | queued (rules vs AI appear on Connect). */
525
+ /** Compact land-row Nexum badge (full text lives in the dialog). */
448
526
  function nexumLandCell(indexing) {
449
527
  if (!indexing) return "—";
450
528
  if (indexing.status === "skipped") {
451
- return '<span class="badge">' + escapeHtml(shortSkipReason(indexing.reason)) + "</span>";
529
+ const kind = skipKind(indexing.reason);
530
+ const tip = indexing.reason ? kind.label + " — " + indexing.reason : kind.label;
531
+ return '<span class="badge skip" title="' + escapeHtml(tip) + '">' + escapeHtml(kind.short) + "</span>";
452
532
  }
453
533
  if (indexing.status === "queued") {
454
- return '<span class="badge queued">queued · analyze</span>';
534
+ return '<span class="badge queued" title="queued · analyze">queued</span>';
535
+ }
536
+ const s = String(indexing.status || "");
537
+ return '<span class="badge" title="' + escapeHtml(s) + '">' + escapeHtml(s.slice(0, 10)) + "</span>";
538
+ }
539
+
540
+ function nexumLandFull(indexing) {
541
+ if (!indexing) return "—";
542
+ if (indexing.status === "skipped") {
543
+ const kind = skipKind(indexing.reason);
544
+ return '<span class="badge skip">' + escapeHtml(kind.label) + "</span>" +
545
+ (indexing.reason
546
+ ? ' <span class="mono">' + escapeHtml(indexing.reason) + "</span>"
547
+ : "");
548
+ }
549
+ if (indexing.status === "queued") {
550
+ return '<span class="badge queued">queued · analyze</span>' +
551
+ (indexing.policy ? ' <span class="mono">policy ' + escapeHtml(indexing.policy) + "</span>" : "") +
552
+ (indexing.operationId ? ' <span class="mono">' + escapeHtml(indexing.operationId) + "</span>" : "");
455
553
  }
456
554
  return statusBadge(indexing.status);
457
555
  }
458
556
 
459
- function modeBadge(mode) {
460
- if (mode === "rules") return '<span class="badge ok">rules · no AI</span>';
557
+ function modeBadge(mode, compact) {
558
+ if (mode === "rules") {
559
+ return compact
560
+ ? '<span class="badge ok" title="rules · no AI">rules</span>'
561
+ : '<span class="badge ok">rules · no AI</span>';
562
+ }
461
563
  if (mode === "ai") return '<span class="badge ai">AI</span>';
462
564
  if (!mode) return "—";
463
565
  return '<span class="badge">unknown</span>';
@@ -492,7 +594,9 @@ export function activityHtml() {
492
594
  return;
493
595
  }
494
596
  bar.hidden = false;
495
- const why = "Scrolled — list held in place";
597
+ const why = dialogOpen()
598
+ ? "Dialog open — list held"
599
+ : "Scrolled down — list held";
496
600
  $("pause-msg").textContent = pendingNew > 0 ? why + " · " + pendingNew + " new" : why;
497
601
  $("resume").textContent = pendingNew > 0 ? "Show " + pendingNew + " new" : "Resume live";
498
602
  }
@@ -501,10 +605,9 @@ export function activityHtml() {
501
605
  entries = liveEntries;
502
606
  pendingNew = 0;
503
607
  followLive = true;
504
- const sc = $("table-scroll");
505
608
  ignoreScroll = true;
506
609
  render();
507
- if (sc) sc.scrollTop = 0;
610
+ window.scrollTo(0, 0);
508
611
  updatePauseBar();
509
612
  requestAnimationFrame(() => { ignoreScroll = false; });
510
613
  }
@@ -538,17 +641,17 @@ export function activityHtml() {
538
641
  const last = (key) => (b.length ? Number(b[b.length - 1][key]) || 0 : 0);
539
642
  const cards = [
540
643
  {
541
- title: "Writes · land",
644
+ title: "Pushed",
542
645
  value: sizes.land,
543
- unit: "in ring",
646
+ unit: "in feed",
544
647
  series: b.map((x) => x.land),
545
648
  color: "#c8f082",
546
649
  hint: "last min " + last("land") + " · Σ60m " + sum("land"),
547
650
  },
548
651
  {
549
- title: "Connect · Nexum",
652
+ title: "Processed",
550
653
  value: sizes.index,
551
- unit: "in ring",
654
+ unit: "in feed",
552
655
  series: b.map((x) => x.connect),
553
656
  color: "#7eb8d4",
554
657
  hint: "rules Σ " + sum("connectRules") + " · AI Σ " + sum("connectAi"),
@@ -559,19 +662,20 @@ export function activityHtml() {
559
662
  unit: "Σ60m",
560
663
  series: b.map((x) => x.relations),
561
664
  color: "#d4a574",
562
- hint: "Memorix relationship plans from connected graphs",
665
+ hint: "Relationship plans from Nexum graphs",
563
666
  },
564
667
  {
565
- title: "Reads · fetch",
668
+ title: "Fetched",
566
669
  value: sizes.fetch,
567
- unit: "in ring",
670
+ unit: "in feed",
568
671
  series: b.map((x) => x.fetch),
569
672
  color: "#3dba7a",
570
673
  hint: "last min " + last("fetch") + " · Σ60m " + sum("fetch"),
571
674
  },
572
675
  ];
573
676
  $("charts").innerHTML = cards.map((c) =>
574
- '<div class="health-card"><h3>' + escapeHtml(c.title) + "</h3>" +
677
+ '<div class="health-card">' +
678
+ "<h3>" + escapeHtml(c.title) + "</h3>" +
575
679
  '<div class="val">' + escapeHtml(c.value) + "<span>" + escapeHtml(c.unit) + "</span></div>" +
576
680
  sparkline(c.series, c.color) +
577
681
  '<div class="hint">' + escapeHtml(c.hint) + "</div></div>"
@@ -619,57 +723,80 @@ export function activityHtml() {
619
723
  }
620
724
 
621
725
  function renderDetail(e) {
726
+ const t = typeOf(e);
622
727
  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>";
728
+ return '<dl class="facts">' +
729
+ fact("Time", escapeHtml(fmtTime(e.ts))) +
730
+ fact("Org", escapeHtml(e.orgId || "—")) +
731
+ fact("Caller", escapeHtml(e.caller || "—")) +
732
+ fact("Object type", '<span class="mono">' + escapeHtml(t.ot || "—") + "</span>") +
733
+ fact("Content type", '<span class="mono">' + escapeHtml(t.ct || "—") + "</span>") +
734
+ fact("Record", '<span class="mono">' + escapeHtml(e.recordId || "—") + "</span>") +
735
+ fact("Revision", escapeHtml(e.revision)) +
736
+ fact("Outcome", escapeHtml(e.outcome || "—")) +
737
+ (e.indexing ? fact("Nexum", nexumLandFull(e.indexing)) : "") +
738
+ "</dl>";
634
739
  }
635
740
  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>";
741
+ return '<dl class="facts">' +
742
+ fact("Time", escapeHtml(fmtTime(e.ts))) +
743
+ fact("Org", escapeHtml(e.orgId || "—")) +
744
+ fact("Caller", escapeHtml(e.caller || "—")) +
745
+ fact("Object type", '<span class="mono">' + escapeHtml(t.ot || "—") + "</span>") +
746
+ fact("Content type", '<span class="mono">' + escapeHtml(t.ct || "—") + "</span>") +
747
+ fact("Mode", escapeHtml(e.mode || "—")) +
748
+ fact("Record", '<span class="mono">' + escapeHtml(e.recordId || "—") + "</span>") +
749
+ fact("Hit", e.hit ? "yes" : "no") +
750
+ (e.count != null ? fact("Count", escapeHtml(e.count)) : "") +
751
+ (e.knowledgeGraph ? fact("Kind", "knowledge-graph subject") : "") +
752
+ "</dl>";
641
753
  }
642
- let html = '<div class="detail"><h3>Connect · Nexum</h3>';
643
- html += "<div>status " + statusBadge(e.status) + "</div>";
754
+ let html = '<dl class="facts">';
755
+ html += fact("Time", escapeHtml(fmtTime(e.ts)));
756
+ html += fact("Org", escapeHtml(e.orgId || "—"));
757
+ html += fact("Status", statusBadge(e.status));
644
758
  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>";
759
+ html += fact("How", modeBadge(e.analyzeMode || "unknown", false) +
760
+ (e.analyzeMethod ? ' <span class="mono">' + escapeHtml(e.analyzeMethod) + "</span>" : ""));
648
761
  }
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>";
762
+ html += fact("Object type", '<span class="mono">' + escapeHtml(t.ot || "—") + "</span>");
763
+ html += fact("Content type", '<span class="mono">' + escapeHtml(t.ct || "—") + "</span>");
764
+ if (e.operationId) html += fact("Operation", '<span class="mono">' + escapeHtml(e.operationId) + "</span>");
765
+ if (e.source) html += fact("Source", addr(e.source));
766
+ if (e.snapshot) html += fact("Snapshot", addr(e.snapshot));
652
767
  if (e.sourceRefs && e.sourceRefs.length) {
653
- html += '<div>sourceRefs <span class="mono">' + escapeHtml(e.sourceRefs.join(", ")) + "</span></div>";
768
+ html += fact("Source refs", '<span class="mono">' + escapeHtml(e.sourceRefs.join(", ")) + "</span>");
654
769
  }
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>";
770
+ html += fact("Nodes", escapeHtml(e.nodeCount ?? 0));
771
+ html += fact("Graph edges", escapeHtml(e.edgeCount ?? 0));
772
+ html += fact("Relations", escapeHtml(e.relationCount ?? 0));
773
+ {
774
+ const snapId = e.snapshot && e.snapshot.recordId;
775
+ const src0 = e.sourceRefs && e.sourceRefs[0];
776
+ const graphHref = snapId
777
+ ? "/_live/graph?recordId=" + encodeURIComponent(snapId)
778
+ : src0
779
+ ? "/_live/graph?source=" + encodeURIComponent(src0)
780
+ : "/_live/graph";
781
+ html += fact("Graph", '<a href="' + escapeHtml(graphHref) + '">Open stored graph</a>');
782
+ }
783
+ if (e.error) html += fact("Error", '<span style="color:var(--bad)">' + escapeHtml(e.error) + "</span>");
784
+ html += "</dl>";
659
785
  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>";
786
+ if (edges.length || (e.nodeLabels && e.nodeLabels.length)) {
787
+ html += '<div class="detail">';
788
+ if (edges.length) {
789
+ html += "<h3>Edges</h3><ul class='edge-list'>";
790
+ for (const ed of edges) {
791
+ html += "<li>" + escapeHtml(ed.from) + " → " + escapeHtml(ed.to) +
792
+ (ed.label ? " <span class='mono'>(" + escapeHtml(ed.label) + ")</span>" : "") +
793
+ "</li>";
794
+ }
795
+ html += "</ul>";
666
796
  }
667
- html += "</ul>";
668
- }
669
- if ((e.nodeLabels && e.nodeLabels.length) || edges.length) {
670
797
  html += renderGraphSvg(e.nodeLabels || [], edges);
798
+ html += "</div>";
671
799
  }
672
- html += "</div>";
673
800
  return html;
674
801
  }
675
802
 
@@ -717,33 +844,95 @@ export function activityHtml() {
717
844
  fmtTime(e.ts),
718
845
  escapeHtml(e.orgId),
719
846
  statusBadge(e.status),
720
- modeBadge(e.analyzeMode || (e.status === "connected" ? "unknown" : "")),
847
+ modeBadge(e.analyzeMode || (e.status === "connected" ? "unknown" : ""), true),
721
848
  ...typeCells(e),
722
849
  e.snapshot
723
850
  ? '<span class="mono">' + escapeHtml(e.snapshot.recordId) + "</span>"
724
851
  : "—",
725
852
  (e.relationCount != null || e.edgeCount != null)
726
- ? escapeHtml((e.relationCount ?? 0) + " rel / " + (e.edgeCount ?? 0) + " edges")
853
+ ? '<span class="mono" title="' + escapeHtml((e.relationCount ?? 0) + " rel / " + (e.edgeCount ?? 0) + " edges") + '">' +
854
+ escapeHtml((e.relationCount ?? 0) + "/" + (e.edgeCount ?? 0)) + "</span>"
727
855
  : "—",
728
856
  ];
729
857
  }
730
858
 
731
859
  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"];
860
+ if (lane === "land") return ["Time", "Org", "Caller", "Object type", "Content type", "Record", "Outcome", "Nexum"];
861
+ if (lane === "fetch") return ["Time", "Org", "Caller", "Object type", "Content type", "Mode", "Record", "Hit"];
862
+ return ["Time", "Org", "Status", "How", "Object type", "Content type", "Snapshot", "Rel/Edges"];
735
863
  }
736
864
 
737
865
  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 + ").",
866
+ land: "<strong>Pushed</strong> — data written into Memorix. The <em>Nexum</em> column is short (<em>no-pol</em>, <em>queued</em>, …): whether this write was sent on to processing. Open a row for the full reason. Rules vs AI show on Processed after Nexum finishes.",
867
+ index: "<strong>Processed</strong> — Nexum working on pushed data. How = <em>rules</em> (no AI) or <em>AI</em>. Rel/Edges = relationship plans / graph edges (full counts in the dialog).",
868
+ fetch: "<strong>Fetched</strong> — data read out of Memorix (up to " + capacity + ").",
741
869
  };
742
870
 
871
+ function fmtWhen(iso) {
872
+ if (!iso) return "";
873
+ const d = new Date(iso);
874
+ return Number.isNaN(d.getTime()) ? String(iso) : d.toLocaleString();
875
+ }
876
+
877
+ function joinOr(names) {
878
+ if (names.length <= 1) return names[0] || "";
879
+ if (names.length === 2) return names[0] + " or " + names[1];
880
+ return names.slice(0, -1).join(", ") + ", or " + names[names.length - 1];
881
+ }
882
+
883
+ function applyFreshness(src) {
884
+ if (!src) return;
885
+ if (src.startedAt) startedAt = src.startedAt;
886
+ if (src.latest) {
887
+ latest = {
888
+ land: src.latest.land || null,
889
+ index: src.latest.index || null,
890
+ fetch: src.latest.fetch || null,
891
+ };
892
+ }
893
+ }
894
+
895
+ function renderBanner() {
896
+ const el = $("quiet-banner");
897
+ if (!el) return;
898
+ const rows = [
899
+ { quiet: "pushed", last: "push", n: sizes.land || 0, ts: latest.land },
900
+ { quiet: "processed", last: "processed event", n: sizes.index || 0, ts: latest.index },
901
+ { quiet: "fetched", last: "fetch", n: sizes.fetch || 0, ts: latest.fetch },
902
+ ];
903
+ const silent = rows.filter((r) => !r.n);
904
+ const active = rows.filter((r) => r.n && r.ts);
905
+ if (!silent.length) {
906
+ el.hidden = true;
907
+ el.innerHTML = "";
908
+ return;
909
+ }
910
+ const since = fmtWhen(startedAt);
911
+ let html = "<strong>Nothing " + escapeHtml(joinOr(silent.map((r) => r.quiet))) + "</strong>";
912
+ html += since
913
+ ? " since " + escapeHtml(since) + ", when this process started."
914
+ : " in this process.";
915
+ if (active.length) {
916
+ html += " " + active.map((r) =>
917
+ "Last " + escapeHtml(r.last) + " " + escapeHtml(fmtWhen(r.ts)) + "."
918
+ ).join(" ");
919
+ }
920
+ el.hidden = false;
921
+ el.innerHTML = html;
922
+ }
923
+
743
924
  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).";
925
+ const n = laneSize();
926
+ const since = fmtWhen(startedAt);
927
+ const sinceBit = since ? " since " + since + "." : " in this process.";
928
+ if (!n) {
929
+ if (lane === "land") return "Nothing pushed" + sinceBit;
930
+ if (lane === "index") return "Nothing processed" + sinceBit;
931
+ return "Nothing fetched" + sinceBit;
932
+ }
933
+ if (lane === "land") return "No pushed data matches these filters.";
934
+ if (lane === "index") return "No processed data matches these filters.";
935
+ return "No fetched data matches these filters.";
747
936
  }
748
937
 
749
938
  function fillSelect(sel, values, current) {
@@ -802,37 +991,44 @@ export function activityHtml() {
802
991
  });
803
992
  }
804
993
 
994
+ function laneSize() {
995
+ if (lane === "land") return sizes.land || 0;
996
+ if (lane === "index") return sizes.index || 0;
997
+ return sizes.fetch || 0;
998
+ }
999
+
805
1000
  function updateSummary(shownCount, totalCount) {
806
1001
  $("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>";
1002
+ "<span>pushed <strong>" + sizes.land + "/" + capacity + "</strong></span>" +
1003
+ "<span>processed <strong>" + sizes.index + "/" + capacity + "</strong></span>" +
1004
+ "<span>fetched <strong>" + sizes.fetch + "/" + capacity + "</strong></span>" +
1005
+ "<span>showing <strong>" + shownCount + "</strong> latest of <strong>" + totalCount + "</strong></span>";
811
1006
  }
812
1007
 
813
1008
  function render() {
1009
+ renderBanner();
814
1010
  $("lane-hint").innerHTML = LANE_HINT[lane] || "";
815
1011
  refreshFilterOptions(entries);
816
1012
  const shown = filteredEntries();
817
- updateSummary(shown.length, entries.length);
1013
+ updateSummary(shown.length, laneSize());
818
1014
  renderCharts();
819
1015
  updatePauseBar();
820
1016
 
821
- const sc = $("table-scroll");
822
- const prevScroll = sc.scrollTop;
1017
+ const wrap = $("table-wrap");
1018
+ const prevY = window.scrollY;
823
1019
  ignoreScroll = true;
824
1020
 
825
1021
  if (!shown.length) {
826
- sc.innerHTML = "<p class='empty'>" + escapeHtml(emptyMessage()) + "</p>";
1022
+ wrap.innerHTML = "<p class='empty'>" + escapeHtml(emptyMessage()) + "</p>";
827
1023
  requestAnimationFrame(() => { ignoreScroll = false; });
828
1024
  return;
829
1025
  }
830
1026
  const heads = headersForLane();
831
1027
  const colW = lane === "fetch"
832
- ? ["8%","7%","10%","12%","14%","7%","30%","12%"]
1028
+ ? ["8%","7%","9%","14%","16%","7%","27%","12%"]
833
1029
  : lane === "index"
834
- ? ["8%","7%","9%","11%","12%","14%","23%","16%"]
835
- : ["8%","7%","11%","12%","14%","26%","8%","14%"];
1030
+ ? ["8%","7%","8%","10%","14%","16%","25%","12%"]
1031
+ : ["8%","7%","10%","14%","16%","26%","9%","10%"];
836
1032
  let html = '<table class="list"><colgroup>' +
837
1033
  colW.map((w) => '<col style="width:' + w + '">').join("") +
838
1034
  "</colgroup><thead><tr>" + heads.map((h) => "<th>" + h + "</th>").join("") + "</tr></thead><tbody>";
@@ -846,10 +1042,10 @@ export function activityHtml() {
846
1042
  }).join("") + "</tr>";
847
1043
  });
848
1044
  html += "</tbody></table>";
849
- sc.innerHTML = html;
850
- if (listHeld()) sc.scrollTop = prevScroll;
1045
+ wrap.innerHTML = html;
1046
+ if (listHeld()) window.scrollTo(0, prevY);
851
1047
  requestAnimationFrame(() => { ignoreScroll = false; });
852
- sc.querySelectorAll("tr.clickable").forEach((tr) => {
1048
+ wrap.querySelectorAll("tr.clickable").forEach((tr) => {
853
1049
  tr.addEventListener("click", () => {
854
1050
  const k = tr.getAttribute("data-key");
855
1051
  const e = shown.find((row) => stableKey(row) === k);
@@ -858,22 +1054,30 @@ export function activityHtml() {
858
1054
  });
859
1055
  }
860
1056
 
1057
+ let loadSeq = 0;
861
1058
  async function load(applyNow) {
1059
+ const seq = ++loadSeq;
1060
+ const requested = lane;
862
1061
  try {
863
1062
  const [actRes, serRes] = await Promise.all([
864
- fetch("/_live/api/activity?lane=" + encodeURIComponent(lane) + "&limit=10000"),
1063
+ fetch("/_live/api/activity?lane=" + encodeURIComponent(requested) + "&limit=400"),
865
1064
  fetch("/_live/api/activity-series?minutes=60"),
866
1065
  ]);
1066
+ if (seq !== loadSeq || requested !== lane) return;
867
1067
  const body = await actRes.json();
1068
+ if (seq !== loadSeq || requested !== lane) return;
868
1069
  if (!actRes.ok || body.ok === false) throw new Error(body?.error?.message || ("HTTP " + actRes.status));
869
1070
  liveEntries = body.entries || [];
870
1071
  sizes = body.sizes || sizes;
871
1072
  capacity = body.capacity || 10000;
1073
+ applyFreshness(body);
872
1074
  if (serRes.ok) {
873
1075
  const ser = await serRes.json();
874
1076
  seriesBuckets = ser.buckets || [];
875
1077
  if (ser.sizes) sizes = ser.sizes;
1078
+ applyFreshness(ser);
876
1079
  }
1080
+ if (seq !== loadSeq || requested !== lane) return;
877
1081
  renderCharts();
878
1082
  if (applyNow || !listHeld()) {
879
1083
  entries = liveEntries;
@@ -882,11 +1086,13 @@ export function activityHtml() {
882
1086
  } else {
883
1087
  countNew();
884
1088
  const shown = filteredEntries();
885
- updateSummary(shown.length, entries.length);
1089
+ updateSummary(shown.length, laneSize());
886
1090
  updatePauseBar();
1091
+ renderBanner();
887
1092
  }
888
1093
  } catch (err) {
889
- $("table-scroll").innerHTML = "<p class='empty'>" + escapeHtml(err.message || String(err)) + "</p>";
1094
+ if (seq !== loadSeq || requested !== lane) return;
1095
+ $("table-wrap").innerHTML = "<p class='empty'>" + escapeHtml(err.message || String(err)) + "</p>";
890
1096
  }
891
1097
  }
892
1098