@gamaze/hicortex 0.20.10 → 0.22.0
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/assets/dashboard.html +476 -41
- package/dist/calibration.d.ts +30 -0
- package/dist/calibration.js +31 -1
- package/dist/capture-health.d.ts +42 -5
- package/dist/capture-health.js +57 -7
- package/dist/capture-pause.d.ts +7 -4
- package/dist/capture-pause.js +7 -4
- package/dist/consolidate.d.ts +19 -0
- package/dist/consolidate.js +116 -51
- package/dist/dashboard.d.ts +41 -16
- package/dist/dashboard.js +110 -41
- package/dist/db.js +16 -0
- package/dist/dedup.js +4 -1
- package/dist/eval/decay-eval.d.ts +2 -3
- package/dist/eval/decay-eval.js +6 -44
- package/dist/eval/ranking-eval.js +1 -1
- package/dist/eval/ranking-fixtures.d.ts +5 -5
- package/dist/eval/ranking-fixtures.js +7 -7
- package/dist/eval/run-eval.js +1 -1
- package/dist/graph.d.ts +1 -2
- package/dist/graph.js +3 -4
- package/dist/mcp-server.js +1 -1
- package/dist/nofit.d.ts +2 -1
- package/dist/nofit.js +2 -1
- package/dist/recall-index.d.ts +3 -2
- package/dist/recall-index.js +3 -2
- package/dist/retrieval.d.ts +9 -3
- package/dist/retrieval.js +21 -32
- package/dist/status.js +18 -0
- package/dist/storage.d.ts +2 -1
- package/dist/storage.js +6 -1
- package/dist/types.d.ts +25 -4
- package/package.json +1 -1
- package/server.json +4 -4
package/assets/dashboard.html
CHANGED
|
@@ -6,20 +6,31 @@
|
|
|
6
6
|
visual on real endpoints, zero new write paths (Update/Remove ride the
|
|
7
7
|
existing /update + /delete; identity Save rides PUT /identity).
|
|
8
8
|
|
|
9
|
+
NAMING (owner ruling 2026-09-14): this surface is "the Hicortex server
|
|
10
|
+
dashboard" — the word "console" never appears in USER-VISIBLE strings
|
|
11
|
+
(titles, tooltips, aria-labels, placeholders, toasts, empty states).
|
|
12
|
+
Historical "console" wording in these comments is internal shorthand and
|
|
13
|
+
stays.
|
|
14
|
+
|
|
9
15
|
PORTED FROM mockups/409-r6-flight.html (the design of record — its banner
|
|
10
16
|
documents every behavior; the engine below is that page with the seeded
|
|
11
17
|
data layer replaced by fetches). Live-data substitutions:
|
|
12
|
-
/dashboard/data?range=
|
|
18
|
+
/dashboard/data?range=Rd — band numbers, snapshot series, digest
|
|
13
19
|
/dashboard/field — the whole field (memories + real links)
|
|
14
|
-
/dashboard/events?days=
|
|
20
|
+
/dashboard/events?days=R — the replay ledger (real nights)
|
|
15
21
|
/health + /health/detail — live dot (30 s poll) + model label
|
|
16
22
|
/account + /account/token — the account menu (harvested from 0.20.4)
|
|
17
23
|
/search /memory /update /delete /identity — the browser + agent rail
|
|
24
|
+
(R = the range selector's value: 30 by default, 7/90/180 opt-in — #452)
|
|
18
25
|
|
|
19
26
|
PHASE 2 (#422 — operator surfaces) LANDED:
|
|
20
27
|
- Capture health: real /distill accounting rows (posts · sessions · MB ·
|
|
21
28
|
held per machine × harness) via /dashboard/data capture_health; the
|
|
22
29
|
phase-1 counts-only rows remain as the degrade.
|
|
30
|
+
Fix round 7 (#409, owner ruling 2026-09-14 — "the normal 30 day as the
|
|
31
|
+
other cards"): the card's face is the ROLLING window (window_rows,
|
|
32
|
+
window_days), the newest night rides as a per-row secondary suffix;
|
|
33
|
+
retention == window (CAPTURE_HEALTH_WINDOW_DAYS).
|
|
23
34
|
- Activity bars: five segments (added + merged + the three graduation
|
|
24
35
|
deltas from snapshot stage_counts; two-segment rendering when buckets
|
|
25
36
|
are absent).
|
|
@@ -87,6 +98,28 @@
|
|
|
87
98
|
within ~2–2.5× (owner: no huge green balls, nothing barely visible);
|
|
88
99
|
stage colors untouched.
|
|
89
100
|
|
|
101
|
+
FIX ROUND 6 (owner-approved proto of record mockups/progress-bar.html,
|
|
102
|
+
2026-09-14 — variant 3 + copy A): ENTRY LOADING SCREEN. Pure inline
|
|
103
|
+
HTML+CSS at the very top of the body, BEFORE the app markup and the
|
|
104
|
+
engine script — it paints on URL entry with ZERO script (the real
|
|
105
|
+
page is black for seconds while the engine parses/boots; the splash
|
|
106
|
+
must beat that). All motion is opacity/transform only, so the
|
|
107
|
+
compositor keeps it animating straight through the engine's main-
|
|
108
|
+
thread freeze. Fully opaque black in BOTH themes (the proto's light
|
|
109
|
+
"paper twin" was review chrome, not shipped behavior): a breathing
|
|
110
|
+
green dot above an indeterminate flowing line (two brand-orange
|
|
111
|
+
segments, uneven pace, looping — never a percentage, never an ETA)
|
|
112
|
+
under the copy line "Entering the Hicortex fleet brain". The engine
|
|
113
|
+
only ever fades it away: entrySplashDismiss hooks the boot REVEAL —
|
|
114
|
+
the existing single handover (data landed → world built → app
|
|
115
|
+
unhidden), NOT DOMContentLoaded — fades .4s, then removes the node.
|
|
116
|
+
Failure seams swap the copy line honestly and never loop silently:
|
|
117
|
+
both core sources rejected → "could not reach the Hicortex server…"
|
|
118
|
+
(held readable ≥2.8 s); boot() throws → same treatment + the static
|
|
119
|
+
shell; 30 s without any settlement → "still waiting…" (no dismiss —
|
|
120
|
+
data may still land). #still removes it synchronously (screenshot
|
|
121
|
+
contract); reduced motion rests on the bar's static base transform.
|
|
122
|
+
|
|
90
123
|
FIX ROUND 3 (owner findings on the live console, 2026-09-14):
|
|
91
124
|
- FIELD LINKS (owner: "the white lines … get very thick — they kind of
|
|
92
125
|
overtake most of the image"): settled strokes are hairline-floored and
|
|
@@ -104,6 +137,30 @@
|
|
|
104
137
|
stays marked; the visible word failed to communicate). Vocabulary
|
|
105
138
|
amendment: Low / HEALTHY / Overfetching. Marker + trend stay.
|
|
106
139
|
|
|
140
|
+
REPLAY + RANGE (#452, owner directives 2026-09-16):
|
|
141
|
+
- 30-DAY DEFAULT FOR ALL VIEWS: the replay ledger, the band and the
|
|
142
|
+
snapshot series all default to 30 days (server EVENTS_DEFAULT_DAYS
|
|
143
|
+
90 → 30; the client's every fetch is selector-driven). Longer
|
|
144
|
+
ranges are OPT-IN via a compact 7 / 30 / 90 / 180 segmented control
|
|
145
|
+
in the replay foot — explicit click only, never auto-loaded
|
|
146
|
+
(SUPERSEDES the 2026-09-12 no-dropdowns ruling). No persistence:
|
|
147
|
+
every load starts at 30 (loads fast).
|
|
148
|
+
- PLAY ONCE, THEN REST: the replay auto-starts as before (~3.6 s after
|
|
149
|
+
boot), walks the ledger ONCE to today, and STOPS there — no loop
|
|
150
|
+
(SUPERSEDES the 2026-09-13 auto-loop/no-buttons ruling). Controls in
|
|
151
|
+
the replay foot, styled like "return to now": pause/resume (holds
|
|
152
|
+
position mid-cruise; hides once the playback completed — nothing is
|
|
153
|
+
playing) and "replay again" (appears once completed; restarts from
|
|
154
|
+
the range start). Drag/scrub behavior is UNCHANGED: a manual take
|
|
155
|
+
pauses the cruise and re-engages ~12 s after idle, continuing
|
|
156
|
+
toward today — where it now stops instead of looping.
|
|
157
|
+
- DATE LABEL ON THE SCRUBBER: the cursor dot carries a small mono
|
|
158
|
+
label above it (the night being viewed, "now" at the end).
|
|
159
|
+
- TRUTH-MANAGEMENT CAPTIONS: Superseded / Retracted / Rewritten ride
|
|
160
|
+
the events ledger alongside learned/enriched/merged (server-side
|
|
161
|
+
sources in dashboard.ts; superseded/retracted bloom in the fading
|
|
162
|
+
color — demoted — rewritten in the truth color).
|
|
163
|
+
|
|
107
164
|
REMAINING DEGRADATIONS (planned, per the #409 phase table — not gaps):
|
|
108
165
|
- Recall card: no raw shown/used counts (the per-memory adoption sums
|
|
109
166
|
are not in any payload yet).
|
|
@@ -442,6 +499,22 @@
|
|
|
442
499
|
padding:2px 11px; white-space:nowrap; width:fit-content;
|
|
443
500
|
}
|
|
444
501
|
#lim-ag .limit-cta:hover{ border-color:var(--amber); }
|
|
502
|
+
/* #409 fix round 4 — the Model card's configure pill: the mock of record
|
|
503
|
+
puts it at the card's BOTTOM-RIGHT, grey/muted (the neutral twin of the
|
|
504
|
+
Agents Connect pill above). margin-top:auto pins it to the card's foot
|
|
505
|
+
however tall the row stretches — row-2 siblings flex-fill, so
|
|
506
|
+
align-self:flex-end alone would leave it floating mid-card. The
|
|
507
|
+
:not([hidden]) guard keeps the degrade (old server / GET failure)
|
|
508
|
+
working: a plain display rule would override the hidden attribute. All
|
|
509
|
+
colors ride theme vars — both themes come free. */
|
|
510
|
+
#model-card .limit-cta:not([hidden]){
|
|
511
|
+
display:inline-block; align-self:flex-end; margin-top:auto;
|
|
512
|
+
color:var(--dim); border:1px solid var(--line2);
|
|
513
|
+
background:color-mix(in srgb, var(--text) 4%, transparent);
|
|
514
|
+
font-size:10.5px; font-weight:600; border-radius:999px;
|
|
515
|
+
padding:2px 11px; white-space:nowrap; width:fit-content;
|
|
516
|
+
}
|
|
517
|
+
#model-card .limit-cta:not([hidden]):hover{ border-color:var(--dim); color:var(--text); }
|
|
445
518
|
|
|
446
519
|
/* ---- row-2 operator cards share the .bseg base; .gh is THE card-header
|
|
447
520
|
component (titles + non-numeric descriptors only) ---- */
|
|
@@ -517,11 +590,25 @@
|
|
|
517
590
|
pointer-events:none; transition:opacity 1.4s; z-index:5;
|
|
518
591
|
}
|
|
519
592
|
.hint em{ font-style:normal; color:var(--dim); }
|
|
520
|
-
/* replay — inside the brain panel; drag
|
|
593
|
+
/* replay — inside the brain panel; drag to scrub, or let the cruise walk
|
|
594
|
+
the ledger ONCE to today (#452: range pills + pause / replay again) */
|
|
521
595
|
.replay-foot{ border-top:1px solid var(--line); padding:7px 12px 8px; flex:none; background:color-mix(in srgb, var(--panel) 88%, transparent); }
|
|
522
596
|
#replay{ display:flex; align-items:center; gap:12px; }
|
|
523
597
|
#replay.off{ opacity:.45; pointer-events:none; }
|
|
598
|
+
/* the range selector stays LIVE even when the instrument is off — a wider
|
|
599
|
+
window can reveal ≥2 nights and revive the replay (#452) */
|
|
600
|
+
#replay.off #rp-range{ opacity:1; pointer-events:auto; }
|
|
524
601
|
#rp-label{ font-size:10px; color:var(--faint); text-transform:uppercase; letter-spacing:.14em; white-space:nowrap; flex:none; }
|
|
602
|
+
/* #452 range selector — the .idpill.seg idiom (mono pills, active = green) */
|
|
603
|
+
#rp-range{ display:inline-flex; gap:3px; flex:none; }
|
|
604
|
+
.rp-seg{
|
|
605
|
+
font-size:9px; font-family:var(--mono); letter-spacing:.05em; color:var(--faint);
|
|
606
|
+
border:1px solid var(--line2); border-radius:999px; padding:3px 8px; white-space:nowrap;
|
|
607
|
+
transition:color .15s, border-color .15s;
|
|
608
|
+
}
|
|
609
|
+
.rp-seg:hover{ color:var(--dim); }
|
|
610
|
+
.rp-seg[aria-pressed="true"]{ color:var(--green-mid); border-color:color-mix(in srgb, var(--green) 40%, transparent); }
|
|
611
|
+
html:not([data-theme="light"]) .rp-seg[aria-pressed="true"]{ box-shadow:0 0 10px rgba(61,255,122,.12); }
|
|
525
612
|
#rp-track{ flex:1; position:relative; height:26px; cursor:pointer; touch-action:none; min-width:60px; }
|
|
526
613
|
#rp-rail{ position:absolute; left:0; right:0; top:11px; height:3px; border-radius:2px; background:var(--tbar-bg); }
|
|
527
614
|
#rp-fill{ position:absolute; left:0; top:11px; height:3px; border-radius:2px; width:100%;
|
|
@@ -533,14 +620,23 @@
|
|
|
533
620
|
}
|
|
534
621
|
html:not([data-theme="light"]) #rp-cursor{ background:#0d1f14; box-shadow:0 0 12px rgba(61,255,122,.7); }
|
|
535
622
|
html[data-theme="light"] #rp-cursor{ box-shadow:0 1px 4px rgba(56,54,42,.3); }
|
|
623
|
+
/* #452: the night being viewed, floating above the scrub dot (the
|
|
624
|
+
#rp-dates span styling; overflow is visible — it may overlap the canvas
|
|
625
|
+
bottom edge like .hint does) */
|
|
626
|
+
#rp-date{
|
|
627
|
+
position:absolute; top:-13px; transform:translateX(-50%); white-space:nowrap;
|
|
628
|
+
font-size:9px; color:var(--faint); font-family:var(--mono); pointer-events:none; z-index:3;
|
|
629
|
+
display:none;
|
|
630
|
+
}
|
|
631
|
+
#rp-date.show{ display:block; }
|
|
536
632
|
.rp-dot{ position:absolute; top:9px; width:5px; height:5px; border-radius:50%; margin-left:-2.5px; background:var(--line2); }
|
|
537
633
|
.rp-dot.g{ background:var(--green-mid); }
|
|
538
634
|
.rp-dot.w{ background:var(--dim); }
|
|
539
635
|
#rp-dates{ position:absolute; left:0; right:0; top:21px; height:12px; }
|
|
540
636
|
#rp-dates span{ position:absolute; transform:translateX(-50%); font-size:9px; color:var(--faint); font-family:var(--mono); }
|
|
541
|
-
#rp-now{ display:none; font-size:10.5px; color:var(--green-mid); border:1px solid color-mix(in srgb, var(--green) 40%, transparent); border-radius:999px; padding:5px 11px; white-space:nowrap; }
|
|
542
|
-
#rp-now.show{ display:inline-block; }
|
|
543
|
-
html:not([data-theme="light"]) #rp-now{ box-shadow:0 0 12px rgba(61,255,122,.15); }
|
|
637
|
+
#rp-now,#rp-pause,#rp-again{ display:none; font-size:10.5px; color:var(--green-mid); border:1px solid color-mix(in srgb, var(--green) 40%, transparent); border-radius:999px; padding:5px 11px; white-space:nowrap; }
|
|
638
|
+
#rp-now.show,#rp-pause.show,#rp-again.show{ display:inline-block; }
|
|
639
|
+
html:not([data-theme="light"]) #rp-now,html:not([data-theme="light"]) #rp-pause,html:not([data-theme="light"]) #rp-again{ box-shadow:0 0 12px rgba(61,255,122,.15); }
|
|
544
640
|
|
|
545
641
|
/* ---- agents rail — the ONLY thing beside the brain, rightmost ---- */
|
|
546
642
|
#agentscol{
|
|
@@ -724,7 +820,7 @@
|
|
|
724
820
|
/* capture health — phase 2: per machine × harness rows from /distill
|
|
725
821
|
accounting (posts/sessions/MB/held); the phase-1 counts-only rows remain
|
|
726
822
|
as the degrade for old servers / nothing recorded yet */
|
|
727
|
-
.cap-row2{ display:flex; align-items:center; gap:9px; padding:2.5px 0; border-bottom:1px solid color-mix(in srgb, var(--line) 60%, transparent); }
|
|
823
|
+
.cap-row2{ display:flex; flex-wrap:wrap; align-items:center; gap:1px 9px; padding:2.5px 0; border-bottom:1px solid color-mix(in srgb, var(--line) 60%, transparent); }
|
|
728
824
|
.cap-row2:last-of-type{ border-bottom:none; }
|
|
729
825
|
.cap-dot{ width:6px; height:6px; border-radius:50%; flex:none; background:var(--green-mid); }
|
|
730
826
|
.cap-row2 .hn{ font:11px var(--mono); color:var(--text); flex:0 0 148px; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
|
|
@@ -735,6 +831,12 @@
|
|
|
735
831
|
.cap-bar .fill{ height:100%; border-radius:2.5px; background:var(--green-mid); }
|
|
736
832
|
.cap-row2 .st{ font-size:10px; color:var(--faint); font-family:var(--mono); white-space:nowrap; }
|
|
737
833
|
.cap-row2 .st b{ color:var(--dim); font-weight:500; }
|
|
834
|
+
/* fix round 7: the newest-night sub-line on the rolling-window rows — a
|
|
835
|
+
full-basis child wraps under the row's primary line (the suffix does NOT
|
|
836
|
+
fit beside "N sess · X MB" without pushing past the card edge); same
|
|
837
|
+
faint mono tone, theme token only, both themes free. Rows without a
|
|
838
|
+
newest night stay single-line. */
|
|
839
|
+
.cap-row2 .cap-ton{ flex-basis:100%; text-align:right; font:10px var(--mono); color:var(--faint); line-height:1.1; }
|
|
738
840
|
.cap-held{ color:var(--amber); margin-left:5px; }
|
|
739
841
|
/* #423 phase 3: the capture-pause badge — the same amber family as the
|
|
740
842
|
held badge (an operator state, not an error) */
|
|
@@ -919,6 +1021,7 @@
|
|
|
919
1021
|
.brow{ grid-template-columns:1fr; }
|
|
920
1022
|
.mrow .sn{ font-size:10px; }
|
|
921
1023
|
#rp-label{ display:none; }
|
|
1024
|
+
#replay{ flex-wrap:wrap; } /* #452: range pills + controls wrap rather than crush the track */
|
|
922
1025
|
#rp-dates span:nth-child(n+3){ display:none; }
|
|
923
1026
|
.acct-trigger .who{ display:none; }
|
|
924
1027
|
.field-head{ padding:8px 12px 7px; }
|
|
@@ -943,6 +1046,62 @@
|
|
|
943
1046
|
</script>
|
|
944
1047
|
</head>
|
|
945
1048
|
<body class="booting">
|
|
1049
|
+
<!-- ============ entry loading screen (#409 fix round 6 — the owner-
|
|
1050
|
+
approved proto of record mockups/progress-bar.html, variant 3 + copy
|
|
1051
|
+
A). Sits at the very top of the body with its OWN style block: it paints
|
|
1052
|
+
on URL entry with ZERO script, fully opaque black in both themes, and
|
|
1053
|
+
the engine only ever fades it away (entrySplashDismiss at the boot
|
|
1054
|
+
reveal). On failure the copy line swaps to an honest error — never an
|
|
1055
|
+
infinite loop. All motion is opacity/transform only: the compositor
|
|
1056
|
+
keeps it animating while the engine below blocks the main thread. ============ -->
|
|
1057
|
+
<style>
|
|
1058
|
+
#entry-load{
|
|
1059
|
+
position:fixed; inset:0; z-index:1000; background:#0a0a0a;
|
|
1060
|
+
display:flex; flex-direction:column; align-items:center; justify-content:center; gap:17px;
|
|
1061
|
+
transition:opacity .4s ease;
|
|
1062
|
+
}
|
|
1063
|
+
#entry-load.gone{ opacity:0; pointer-events:none; }
|
|
1064
|
+
/* variant 3's dot: the header healthy mark's look, breathing */
|
|
1065
|
+
#entry-load .el-dot{
|
|
1066
|
+
width:10px; height:10px; border-radius:50%;
|
|
1067
|
+
background:radial-gradient(circle at 35% 35%, color-mix(in srgb, #3dff7a 95%, white), color-mix(in srgb, #3dff7a 25%, transparent) 55%, transparent 70%);
|
|
1068
|
+
box-shadow:0 0 12px color-mix(in srgb, #3dff7a 55%, transparent);
|
|
1069
|
+
animation:el-breathe 3.4s ease-in-out infinite;
|
|
1070
|
+
}
|
|
1071
|
+
@keyframes el-breathe{ 0%,100%{opacity:1} 50%{opacity:.5} }
|
|
1072
|
+
#entry-load .el-line{ font-size:13.5px; color:#9a9a9a; text-align:center; padding:0 24px; }
|
|
1073
|
+
/* the indeterminate line: two brand-orange segments on a faint track,
|
|
1074
|
+
uneven pace, varying length, looping forever; the base transform is
|
|
1075
|
+
the reduced-motion rest pose. Never a percentage, never an ETA. */
|
|
1076
|
+
#entry-load .el-bar{
|
|
1077
|
+
width:min(300px,72%); height:2.5px; border-radius:99px;
|
|
1078
|
+
background:#1e1e1e; overflow:hidden; position:relative;
|
|
1079
|
+
}
|
|
1080
|
+
#entry-load .el-bar i{
|
|
1081
|
+
position:absolute; top:0; bottom:0; left:0; width:100%;
|
|
1082
|
+
background:#ff9f43; border-radius:99px;
|
|
1083
|
+
transform-origin:0 50%; transform:translateX(32%) scaleX(.42);
|
|
1084
|
+
box-shadow:0 0 8px color-mix(in srgb, #ff9f43 45%, transparent);
|
|
1085
|
+
}
|
|
1086
|
+
#entry-load .el-bar i.a{ animation:el-indet-a 2.2s cubic-bezier(.55,.1,.7,.5) infinite backwards; }
|
|
1087
|
+
#entry-load .el-bar i.b{ opacity:.6; animation:el-indet-b 2.2s cubic-bezier(.65,.05,.75,.5) 1.15s infinite backwards; }
|
|
1088
|
+
@keyframes el-indet-a{
|
|
1089
|
+
0% { transform:translateX(-38%) scaleX(.12); animation-timing-function:cubic-bezier(.6,.05,.8,.5); }
|
|
1090
|
+
55% { transform:translateX(46%) scaleX(.62); animation-timing-function:cubic-bezier(.25,.65,.35,1); }
|
|
1091
|
+
80% { transform:translateX(84%) scaleX(.68); animation-timing-function:cubic-bezier(.55,.05,.8,.45); }
|
|
1092
|
+
100% { transform:translateX(140%) scaleX(.16); }
|
|
1093
|
+
}
|
|
1094
|
+
@keyframes el-indet-b{
|
|
1095
|
+
0% { transform:translateX(-24%) scaleX(.08); animation-timing-function:cubic-bezier(.65,.05,.8,.5); }
|
|
1096
|
+
70% { transform:translateX(52%) scaleX(.30); animation-timing-function:cubic-bezier(.2,.6,.45,1); }
|
|
1097
|
+
100% { transform:translateX(116%) scaleX(.10); }
|
|
1098
|
+
}
|
|
1099
|
+
</style>
|
|
1100
|
+
<div id="entry-load" role="status" aria-live="polite">
|
|
1101
|
+
<span class="el-dot" aria-hidden="true"></span>
|
|
1102
|
+
<div class="el-line" id="entry-load-line">Entering the Hicortex fleet brain</div>
|
|
1103
|
+
<div class="el-bar" aria-hidden="true"><i class="a"></i><i class="b"></i></div>
|
|
1104
|
+
</div>
|
|
946
1105
|
<header>
|
|
947
1106
|
<div class="h-left">
|
|
948
1107
|
<div class="h-search">
|
|
@@ -1045,10 +1204,11 @@
|
|
|
1045
1204
|
<div id="feel-rows"></div>
|
|
1046
1205
|
</div>
|
|
1047
1206
|
<div class="bseg c-grey" id="model-card">
|
|
1048
|
-
<div class="gh"><span class="t">Model</span
|
|
1049
|
-
<div class="big"><span class="n" id="model-provider">—</span
|
|
1207
|
+
<div class="gh"><span class="t">Model</span></div>
|
|
1208
|
+
<div class="big"><span class="n" id="model-provider">—</span></div>
|
|
1050
1209
|
<div class="csub mono" id="model-id"> </div>
|
|
1051
1210
|
<div class="csub">detected from config</div>
|
|
1211
|
+
<button class="limit-cta" id="model-cfg" hidden title="edit the model knobs (config.json llm* keys)">configure →</button>
|
|
1052
1212
|
</div>
|
|
1053
1213
|
</div>
|
|
1054
1214
|
</div>
|
|
@@ -1079,13 +1239,17 @@
|
|
|
1079
1239
|
</div>
|
|
1080
1240
|
<div class="replay-foot">
|
|
1081
1241
|
<div id="replay">
|
|
1242
|
+
<div id="rp-range" role="group" aria-label="range"></div>
|
|
1082
1243
|
<span id="rp-label">nights</span>
|
|
1083
1244
|
<div id="rp-track">
|
|
1084
1245
|
<div id="rp-rail"></div>
|
|
1085
1246
|
<div id="rp-fill"></div>
|
|
1086
1247
|
<div id="rp-dates"></div>
|
|
1087
1248
|
<div id="rp-cursor"></div>
|
|
1249
|
+
<span id="rp-date"></span>
|
|
1088
1250
|
</div>
|
|
1251
|
+
<button id="rp-pause">pause</button>
|
|
1252
|
+
<button id="rp-again">replay again</button>
|
|
1089
1253
|
<button id="rp-now">return to now</button>
|
|
1090
1254
|
</div>
|
|
1091
1255
|
</div>
|
|
@@ -1151,14 +1315,62 @@
|
|
|
1151
1315
|
</div>
|
|
1152
1316
|
<script>
|
|
1153
1317
|
'use strict';
|
|
1318
|
+
/* =====================================================================
|
|
1319
|
+
ENTRY SPLASH (#409 fix round 6) — the loading screen is already painted
|
|
1320
|
+
by the static markup at the top of the body; the engine only ever fades it
|
|
1321
|
+
away. entrySplashDismiss hooks the boot REVEAL (reveal() in boot(): the
|
|
1322
|
+
single handover — data landed, world built, app unhidden — NOT
|
|
1323
|
+
DOMContentLoaded). State rides the node's dataset so both functions stay
|
|
1324
|
+
pure w.r.t. the DOM (extract-and-run test convention). Failure seams
|
|
1325
|
+
swap the copy line honestly and NEVER loop silently: total data failure
|
|
1326
|
+
holds the error line readable >= ENTRY_ERROR_HOLD before fading (even if
|
|
1327
|
+
the reveal fires earlier — and on its own if the reveal never comes); a
|
|
1328
|
+
boot() throw does the same and clears the booting class; a 30 s deadline
|
|
1329
|
+
(below) flags a server that never answered without dismissing — data
|
|
1330
|
+
may still land, and the reveal then owns the dismissal.
|
|
1331
|
+
===================================================================== */
|
|
1332
|
+
const ENTRY_FAIL_COPY='could not reach the Hicortex server — check that it is running, then reload';
|
|
1333
|
+
const ENTRY_BOOTFAIL_COPY='the dashboard failed to start — reload to retry';
|
|
1334
|
+
const ENTRY_SLOW_COPY='still waiting for the server — it has not answered yet';
|
|
1335
|
+
const ENTRY_ERROR_HOLD=2800; /* ms the failure line stays readable */
|
|
1336
|
+
function entrySplashFail(msg){
|
|
1337
|
+
const s=document.getElementById('entry-load'), l=document.getElementById('entry-load-line');
|
|
1338
|
+
if(!s || !l || s.dataset.gone==='1') return;
|
|
1339
|
+
l.textContent=msg;
|
|
1340
|
+
s.dataset.failed='1'; s.dataset.failAt=String(Date.now());
|
|
1341
|
+
setTimeout(entrySplashDismiss, ENTRY_ERROR_HOLD); /* owns the beat even if the reveal never comes */
|
|
1342
|
+
}
|
|
1343
|
+
function entrySplashDismiss(){
|
|
1344
|
+
const s=document.getElementById('entry-load');
|
|
1345
|
+
if(!s) return;
|
|
1346
|
+
if(s.dataset.failed==='1'){ /* hold the error line readable */
|
|
1347
|
+
const remain=ENTRY_ERROR_HOLD-(Date.now()-Number(s.dataset.failAt||0));
|
|
1348
|
+
if(remain>0){ setTimeout(entrySplashDismiss, remain); return; }
|
|
1349
|
+
}
|
|
1350
|
+
if(s.dataset.gone==='1') return; /* idempotent */
|
|
1351
|
+
s.dataset.gone='1';
|
|
1352
|
+
const instant=document.body.classList.contains('instant') ||
|
|
1353
|
+
(window.matchMedia && window.matchMedia('(prefers-reduced-motion: reduce)').matches);
|
|
1354
|
+
if(instant){ s.remove(); return; } /* #still / reduced motion: no fade */
|
|
1355
|
+
s.classList.add('gone');
|
|
1356
|
+
s.addEventListener('transitionend', function(){ s.remove(); }, {once:true});
|
|
1357
|
+
setTimeout(function(){ s.remove(); }, 900); /* fallback — transitionend is not guaranteed */
|
|
1358
|
+
}
|
|
1359
|
+
/* honest slow-wait flag: boot fetches still unsettled 30 s in → say so on
|
|
1360
|
+
the splash (no dismissal — the reveal still owns that if data lands) */
|
|
1361
|
+
setTimeout(function(){
|
|
1362
|
+
const s=document.getElementById('entry-load'), l=document.getElementById('entry-load-line');
|
|
1363
|
+
if(s && l && s.dataset.gone!=='1' && s.dataset.failed!=='1') l.textContent=ENTRY_SLOW_COPY;
|
|
1364
|
+
}, 30000);
|
|
1365
|
+
|
|
1154
1366
|
/* #still (or ?still): skip the boot reveal — deterministic screenshots/tests. */
|
|
1155
1367
|
const STILL = /(?:#|[?&])still\b/.test(location.hash + ' ' + location.search);
|
|
1156
|
-
if(STILL){ document.body.classList.remove('booting'); document.body.classList.add('instant'); }
|
|
1368
|
+
if(STILL){ document.body.classList.remove('booting'); document.body.classList.add('instant'); entrySplashDismiss(); }
|
|
1157
1369
|
|
|
1158
1370
|
/* =====================================================================
|
|
1159
1371
|
The approved #409 r6-flight engine, ported to live data. One canvas,
|
|
1160
1372
|
three verbs (look / travel / time) plus the quiet fourth: WATCH — ambient
|
|
1161
|
-
signals loop slowly along REAL stored links,
|
|
1373
|
+
signals loop slowly along REAL stored links, always on.
|
|
1162
1374
|
Stages are DERIVED presentation (server-side via /dashboard/field) over
|
|
1163
1375
|
the one numeric strength signal — never stored state.
|
|
1164
1376
|
===================================================================== */
|
|
@@ -1275,9 +1487,12 @@ function bubbleRadius(s){ return 6 + Math.sqrt(clamp(s??0,0,1))*3; }
|
|
|
1275
1487
|
function depthSizeDivisor(dz){ return Math.pow(Math.max(.14,dz),0.35); }
|
|
1276
1488
|
|
|
1277
1489
|
/* ---------- live-data state ---------- */
|
|
1278
|
-
let
|
|
1490
|
+
let RANGE=30; // days — the range selector's value (#452; 30-day default for
|
|
1491
|
+
// ALL views, owner directive 2026-09-16). NO persistence: every
|
|
1492
|
+
// load starts at 30 — it loads fast; longer is opt-in per click.
|
|
1493
|
+
let D=null; // /dashboard/data?range=RANGE d — band, series, digest
|
|
1279
1494
|
let F=null; // /dashboard/field
|
|
1280
|
-
let EV=null; // /dashboard/events?days=
|
|
1495
|
+
let EV=null; // /dashboard/events?days=RANGE — the replay ledger
|
|
1281
1496
|
let M=null; // /dashboard/model (#422) — null on failure: label-only degrade, no configure pill
|
|
1282
1497
|
let TOTAL=0, removedNow=0;
|
|
1283
1498
|
/* the store's static LIVE count (headline.live_memories ?? total): the
|
|
@@ -1488,13 +1703,19 @@ function buildWorld(){
|
|
|
1488
1703
|
|
|
1489
1704
|
// the flattened replay ledger (events within a night fire in cruise order);
|
|
1490
1705
|
// added/merged reuse the staggered born/merge times so blooms and births
|
|
1491
|
-
// coincide; enriched/linked carry their own salted staggers.
|
|
1706
|
+
// coincide; enriched/linked carry their own salted staggers. #452 adds the
|
|
1707
|
+
// truth-management captions — stagger order enriched .55 < retracted .65 <
|
|
1708
|
+
// superseded .70 < rewritten .75 < learned .80 keeps the cruise sane (each
|
|
1709
|
+
// new type gets its own salt, so per-id positions never collide).
|
|
1492
1710
|
EVENTS = [];
|
|
1493
1711
|
NIGHTS.forEach((n,i)=>{
|
|
1494
1712
|
for(const id of n.added) EVENTS.push({t:bornOf.get(id), type:'learned', id, cap:'Learned — “'+trunc(titleOf(id),52)+'”'});
|
|
1495
1713
|
for(const id of n.enriched) EVENTS.push({t:i+.55+stag(id,19)*.08, type:'enriched', id, cap:'Enriched — “'+trunc(titleOf(id),52)+'”'});
|
|
1496
1714
|
for(const mg of n.merged) EVENTS.push({t:mergeOf.get(mg.loser).t, type:'merged', loser:mg.loser, id:mg.loser, cap:'Merged — “'+trunc(titleOf(mg.loser),40)+'”'});
|
|
1497
1715
|
for(const lk of n.linked) EVENTS.push({t:i+.90+stag(lk.a+lk.b,23)*.08, type:'linked', a:lk.a, b:lk.b, cap:null});
|
|
1716
|
+
for(const sp of n.superseded) EVENTS.push({t:i+.70+stag(sp.old,29)*.08, type:'superseded', id:sp.old, cap:'Superseded — “'+trunc(titleOf(sp.old),52)+'”'});
|
|
1717
|
+
for(const rt of n.retracted) EVENTS.push({t:i+.65+stag(rt.old,31)*.08, type:'retracted', id:rt.old, cap:'Retracted — “'+trunc(titleOf(rt.old),52)+'”'});
|
|
1718
|
+
for(const id of n.rewritten) EVENTS.push({t:i+.75+stag(id,37)*.08, type:'rewritten', id, cap:'Rewritten — “'+trunc(titleOf(id),52)+'”'});
|
|
1498
1719
|
});
|
|
1499
1720
|
EVENTS.sort((x,y)=>x.t-y.t);
|
|
1500
1721
|
|
|
@@ -2338,14 +2559,21 @@ function toast(msg, ms){
|
|
|
2338
2559
|
function wake(){ lastActivity=performance.now(); }
|
|
2339
2560
|
|
|
2340
2561
|
/* ---------- replay — time travel: drag to take the helm, else the G1
|
|
2341
|
-
auto-cruise walks the REAL event ledger (one memory action at a time
|
|
2342
|
-
|
|
2562
|
+
auto-cruise walks the REAL event ledger (one memory action at a time)
|
|
2563
|
+
ONCE to today, then rests there (#452, owner ruling 2026-09-16 —
|
|
2564
|
+
supersedes the 2026-09-13 auto-loop). Pause/resume + "replay again" sit
|
|
2565
|
+
in the foot beside "return to now"; a manual take still pauses the
|
|
2566
|
+
cruise and re-engages ~12 s after idle, continuing toward today — where
|
|
2567
|
+
it now stops instead of looping.
|
|
2343
2568
|
With fewer than 2 nights the instrument renders its "now" state and
|
|
2344
2569
|
disables itself (degrade quietly). ---------- */
|
|
2345
2570
|
const track=el('rp-track');
|
|
2346
2571
|
function trackFrac(e){ const r=track.getBoundingClientRect(); return clamp((e.clientX-r.left)/r.width,0,1); }
|
|
2347
2572
|
let autoPlay=false, autoEvIdx=0, autoNextAt=0, manualUntil=3600, pendingApply=null;
|
|
2573
|
+
let paused=false; /* #452: pause holds the cruise position; resume continues */
|
|
2574
|
+
let played=false; /* #452: a full playback completed → rest at today, never loop */
|
|
2348
2575
|
function manualTake(){ autoPlay=false; pendingApply=null; manualUntil=performance.now()+12000; wake(); }
|
|
2576
|
+
function setPaused(v){ paused=v; el('rp-pause').textContent=paused?'resume':'pause'; }
|
|
2349
2577
|
/* HERALD — before each cruise step's change, a bright pulse flies in along
|
|
2350
2578
|
a REAL link touching the target (else from its nearest same-domain
|
|
2351
2579
|
neighbor) and LANDS: the update we see is the arrival */
|
|
@@ -2369,18 +2597,25 @@ function heraldTo(id){
|
|
|
2369
2597
|
signals.push({ path:p, i:0, s:0, dur:.45, tail:[], showcase:true, herald:true }); /* dur is seconds */
|
|
2370
2598
|
}
|
|
2371
2599
|
function autoTick(now){
|
|
2372
|
-
if(REDUCED||booting||!REPLAY_OK) return;
|
|
2600
|
+
if(REDUCED||booting||!REPLAY_OK||paused) return;
|
|
2373
2601
|
if(!autoPlay){ if(now>manualUntil && !scrubbing) autoPlay=true; else return; }
|
|
2374
2602
|
if(pendingApply){
|
|
2375
2603
|
if(now>=pendingApply.at){ seek(pendingApply.t); pendingApply=null; autoNextAt=now+820; }
|
|
2376
2604
|
return; /* the herald is in flight */
|
|
2377
2605
|
}
|
|
2378
2606
|
if(now<autoNextAt) return;
|
|
2379
|
-
if(T>=NOW_T-.001){
|
|
2607
|
+
if(T>=NOW_T-.001){
|
|
2608
|
+
/* a beat at "now": re-kick to the range start ONLY before the first
|
|
2609
|
+
full playback; after it, REST here — plays once, never loops (#452) */
|
|
2610
|
+
if(played){ autoPlay=false; autoNextAt=now+3200; return; }
|
|
2380
2611
|
autoEvIdx=0; autoNextAt=now+3200; seek(.001); return;
|
|
2381
2612
|
}
|
|
2382
2613
|
while(autoEvIdx<EVENTS.length && EVENTS[autoEvIdx].t<=T+.0001) autoEvIdx++;
|
|
2383
|
-
if(autoEvIdx>=EVENTS.length){
|
|
2614
|
+
if(autoEvIdx>=EVENTS.length){
|
|
2615
|
+
/* ledger exhausted: arrive at today and complete the playback (the
|
|
2616
|
+
flags go BEFORE the seek so onTChanged's control swap sees them) */
|
|
2617
|
+
autoNextAt=now+3200; played=true; autoPlay=false; seek(WINDOW); return;
|
|
2618
|
+
}
|
|
2384
2619
|
const ev=EVENTS[autoEvIdx++];
|
|
2385
2620
|
if(ev.id && signals.length<8){ heraldTo(ev.id); pendingApply={t:ev.t+.001, at:now+430}; }
|
|
2386
2621
|
else { seek(ev.t+.001); autoNextAt=now+820; }
|
|
@@ -2401,7 +2636,10 @@ function onTChanged(fromSeek){
|
|
|
2401
2636
|
const b=mems.get(ev.id);
|
|
2402
2637
|
if(!b||!aliveAt(b,ev.t)||b._sx===undefined) continue;
|
|
2403
2638
|
if(blooms.length>=6) break;
|
|
2404
|
-
|
|
2639
|
+
/* #452: superseded/retracted bloom in the FADING color (demoted
|
|
2640
|
+
claims); rewritten falls to the default truth color (a rewrite is
|
|
2641
|
+
the claim surviving, corrected) */
|
|
2642
|
+
const rgb= ev.type==='merged'||ev.type==='superseded'||ev.type==='retracted'? PAL.stages.fading.rgb : (ev.type==='learned'? PAL.stages.forming.rgb : PAL.stages.truth.rgb);
|
|
2405
2643
|
blooms.push({x:b._sx,y:b._sy,r0:2,r1:24+b._sr,t0:performance.now(),dur:1400,cap:ev.cap,rgb});
|
|
2406
2644
|
}
|
|
2407
2645
|
}
|
|
@@ -2410,6 +2648,18 @@ function onTChanged(fromSeek){
|
|
|
2410
2648
|
el('rp-cursor').style.left=(T/WINDOW*100)+'%';
|
|
2411
2649
|
const isNow=T>=NOW_T-.001;
|
|
2412
2650
|
el('rp-now').classList.toggle('show',!isNow&&REPLAY_OK);
|
|
2651
|
+
/* #452 date label above the scrub dot — same left % as the cursor, the
|
|
2652
|
+
night being viewed ("now" at the end); hidden when there is no ledger */
|
|
2653
|
+
const dlab=el('rp-date');
|
|
2654
|
+
if(REPLAY_OK&&NIGHTS.length){
|
|
2655
|
+
dlab.style.left=(T/WINDOW*100)+'%';
|
|
2656
|
+
dlab.textContent=isNow?'now':fmtDay(NIGHTS[Math.min(Math.floor(T),NIGHTS.length-1)].date);
|
|
2657
|
+
dlab.classList.add('show');
|
|
2658
|
+
}else dlab.classList.remove('show');
|
|
2659
|
+
/* #452 lifecycle pills: pause while a playback is live, "replay again"
|
|
2660
|
+
once it completed — at most ONE of them beside "return to now" */
|
|
2661
|
+
el('rp-pause').classList.toggle('show',REPLAY_OK&&!played);
|
|
2662
|
+
el('rp-again').classList.toggle('show',REPLAY_OK&&played);
|
|
2413
2663
|
updateMeasures();
|
|
2414
2664
|
/* #422 owner ruling: the overview counter is STATIC (the store's live
|
|
2415
2665
|
count, set once at boot + after writes) — scrubbing the replay no longer
|
|
@@ -2417,10 +2667,13 @@ function onTChanged(fromSeek){
|
|
|
2417
2667
|
dirty=true;
|
|
2418
2668
|
}
|
|
2419
2669
|
/* warp-to-now — accelerates through every remaining event, blooms firing,
|
|
2420
|
-
and arrives ~2.4 s later (ease in-out)
|
|
2670
|
+
and arrives ~2.4 s later (ease in-out). #452: arriving at now by hand
|
|
2671
|
+
completes the playback too — the cruise rests instead of looping after */
|
|
2421
2672
|
let warpUntil=0, warpFrom=0;
|
|
2422
2673
|
function warpToNow(){
|
|
2423
2674
|
manualTake();
|
|
2675
|
+
setPaused(false);
|
|
2676
|
+
played=true;
|
|
2424
2677
|
if(REDUCED||T>=NOW_T-.001||!REPLAY_OK){ seek(WINDOW); return; }
|
|
2425
2678
|
warpFrom=T; warpUntil=performance.now()+2400;
|
|
2426
2679
|
}
|
|
@@ -2433,6 +2686,72 @@ function warpTick(now){
|
|
|
2433
2686
|
}
|
|
2434
2687
|
el('rp-now').onclick=warpToNow;
|
|
2435
2688
|
|
|
2689
|
+
/* #452 — pause/resume + replay again. Pause holds the cruise position
|
|
2690
|
+
(autoTick returns early, pending heralds included); resume continues
|
|
2691
|
+
toward today. Replay again restarts from the range start and plays once.
|
|
2692
|
+
Visibility is onTChanged's job: pause renders only while a playback is
|
|
2693
|
+
live, "replay again" only once one completed — least clutter. */
|
|
2694
|
+
el('rp-pause').onclick=()=>{
|
|
2695
|
+
setPaused(!paused);
|
|
2696
|
+
if(!paused) autoPlay=true; /* resume continues the cruise immediately */
|
|
2697
|
+
wake();
|
|
2698
|
+
};
|
|
2699
|
+
el('rp-again').onclick=()=>{
|
|
2700
|
+
manualTake(); setPaused(false);
|
|
2701
|
+
played=false; autoEvIdx=0; pendingApply=null; autoPlay=true;
|
|
2702
|
+
seek(.001); wake();
|
|
2703
|
+
};
|
|
2704
|
+
|
|
2705
|
+
/* #452 — the range selector: 7/30/90/180 days (the owner-fixed option
|
|
2706
|
+
set), default 30, NO persistence — every load starts at 30 (loads fast).
|
|
2707
|
+
A click refetches BOTH range-scoped sources and rebuilds the world
|
|
2708
|
+
exactly like boot — the agents rail included (its "last night" line
|
|
2709
|
+
reads NIGHTS, so it must follow the new window); the replay then
|
|
2710
|
+
auto-starts on the new range via the idle re-engage (mirrors boot).
|
|
2711
|
+
Fail-soft: a failed fetch toasts, keeps the OLD data and the OLD pills
|
|
2712
|
+
— never a half-switched view. One switch at a time (refreshAfterWrite's
|
|
2713
|
+
refreshInFlight pattern): a click while a switch is in flight is
|
|
2714
|
+
IGNORED — a racing second fetch could resolve out of order and leave
|
|
2715
|
+
pills and data disagreeing. */
|
|
2716
|
+
const RANGE_OPTIONS=[7,30,90,180];
|
|
2717
|
+
function renderRangePills(){
|
|
2718
|
+
const host=el('rp-range'); host.innerHTML='';
|
|
2719
|
+
for(const n of RANGE_OPTIONS){
|
|
2720
|
+
const b=document.createElement('button');
|
|
2721
|
+
b.type='button'; b.className='rp-seg'; b.textContent=n+'d';
|
|
2722
|
+
b.title='last '+n+' days';
|
|
2723
|
+
b.setAttribute('aria-pressed', String(n===RANGE));
|
|
2724
|
+
b.onclick=()=>{ if(n!==RANGE) switchRange(n); };
|
|
2725
|
+
host.appendChild(b);
|
|
2726
|
+
}
|
|
2727
|
+
}
|
|
2728
|
+
let rangeSwitchInFlight=false;
|
|
2729
|
+
async function switchRange(n){
|
|
2730
|
+
if(rangeSwitchInFlight) return; rangeSwitchInFlight=true;
|
|
2731
|
+
const prev=RANGE; RANGE=n; renderRangePills();
|
|
2732
|
+
try{
|
|
2733
|
+
const [dd,ee]=await Promise.all([
|
|
2734
|
+
api('/dashboard/data?range='+RANGE+'d'),
|
|
2735
|
+
api('/dashboard/events?days='+RANGE),
|
|
2736
|
+
]);
|
|
2737
|
+
D=dd;
|
|
2738
|
+
if(ee&&Array.isArray(ee.nights)){ EV=ee; NIGHTS=EV.nights; } else { NIGHTS=[]; }
|
|
2739
|
+
}catch(e){
|
|
2740
|
+
RANGE=prev; renderRangePills();
|
|
2741
|
+
toast('Range switch failed: '+e.message);
|
|
2742
|
+
return;
|
|
2743
|
+
}finally{
|
|
2744
|
+
rangeSwitchInFlight=false;
|
|
2745
|
+
}
|
|
2746
|
+
buildWorld(); buildPaths(); buildTimeline(); renderBand(); buildRoll(); buildCapture(); buildAgents();
|
|
2747
|
+
/* reset the replay over the new window (boot's pattern): rest at "now"
|
|
2748
|
+
first, the idle re-engage then walks the new range once from the start */
|
|
2749
|
+
T=WINDOW; lastT=WINDOW; played=false; autoEvIdx=0; pendingApply=null; autoPlay=false;
|
|
2750
|
+
setPaused(false);
|
|
2751
|
+
onTChanged(true);
|
|
2752
|
+
}
|
|
2753
|
+
renderRangePills();
|
|
2754
|
+
|
|
2436
2755
|
/* the timeline: dots per night (green when busy adding, grey when the
|
|
2437
2756
|
night merged ≥3 — the prototype's thresholds), date labels at the ends */
|
|
2438
2757
|
function buildTimeline(){
|
|
@@ -2445,6 +2764,7 @@ function buildTimeline(){
|
|
|
2445
2764
|
dots.style.cssText='position:absolute;inset:0;pointer-events:none;';
|
|
2446
2765
|
if(REPLAY_OK){
|
|
2447
2766
|
label.textContent=NIGHTS.length+' nights';
|
|
2767
|
+
el('replay').classList.remove('off'); /* a range switch can revive the instrument (#452) */
|
|
2448
2768
|
const s1=document.createElement('span'); s1.style.left='0%'; s1.style.transform='none';
|
|
2449
2769
|
s1.textContent=fmtDay(NIGHTS[0].date); dates.appendChild(s1);
|
|
2450
2770
|
const s2=document.createElement('span'); s2.style.left='100%'; s2.style.transform='translateX(-100%)';
|
|
@@ -2454,7 +2774,10 @@ function buildTimeline(){
|
|
|
2454
2774
|
d.style.left=((i+.5)/WINDOW*100)+'%';
|
|
2455
2775
|
if(mergedL[i]>=3) d.classList.add('w');
|
|
2456
2776
|
else if(learnedL[i]>=8) d.classList.add('g');
|
|
2457
|
-
d.title=fmtDay(n.date)+' — learned '+(learnedL[i]||0)+(enrichedL[i]?' · enriched '+enrichedL[i]:'')+(mergedL[i]?' · merged '+mergedL[i]:'')
|
|
2777
|
+
d.title=fmtDay(n.date)+' — learned '+(learnedL[i]||0)+(enrichedL[i]?' · enriched '+enrichedL[i]:'')+(mergedL[i]?' · merged '+mergedL[i]:'')
|
|
2778
|
+
+(n.superseded.length?' · superseded '+n.superseded.length:'')
|
|
2779
|
+
+(n.retracted.length?' · retracted '+n.retracted.length:'')
|
|
2780
|
+
+(n.rewritten.length?' · rewritten '+n.rewritten.length:'');
|
|
2458
2781
|
frag.appendChild(d);
|
|
2459
2782
|
});
|
|
2460
2783
|
}else{
|
|
@@ -2904,26 +3227,49 @@ function renderRecBand(ups){
|
|
|
2904
3227
|
}
|
|
2905
3228
|
|
|
2906
3229
|
let MODEL_LABEL='';
|
|
3230
|
+
/* #409 fix round 5 — owner ruling 2026-09-14 (verbatim): "I do NOT want to
|
|
3231
|
+
know what it is compatible with, I want to know the fucking model." The
|
|
3232
|
+
card face shows THE MODEL, nothing else: hero = the namespace-stripped
|
|
3233
|
+
model name (fit-scaled, floor 20px), mono line = the raw config id,
|
|
3234
|
+
caption "detected from config", configure pill. NO protocol line, NO
|
|
3235
|
+
provider line — local and cloud endpoints render identically. (The
|
|
3236
|
+
configure modal keeps its technical fields: that is the operator surface.)
|
|
3237
|
+
The degrade fires ONLY when there is no model AT ALL — neither the config
|
|
3238
|
+
echo (M.model) nor the runtime label (/health/detail) carries one. A
|
|
3239
|
+
configured model with null provider/backend still renders: the daemon may
|
|
3240
|
+
legitimately run no LLM (resolveExplicitLlmConfig requires llmApiKey
|
|
3241
|
+
alongside llmBaseUrl; a keyless local endpoint is recall-only) while the
|
|
3242
|
+
config declares the model — the card keys on the MODEL, not the runtime. */
|
|
2907
3243
|
function renderModelCard(){
|
|
2908
3244
|
/* configure pill only when GET /dashboard/model succeeded (M truthy) —
|
|
2909
3245
|
an old server / load error degrades to the phase-1 label-only card */
|
|
2910
3246
|
el('model-cfg').hidden=!M;
|
|
2911
|
-
|
|
3247
|
+
const hero=el('model-provider');
|
|
3248
|
+
hero.style.fontSize=''; // undo a previous hero fit (below)
|
|
3249
|
+
/* The config echo is the display truth (the RAW config id, never a
|
|
3250
|
+
provider-prefixed composition); the runtime label is only the fallback
|
|
3251
|
+
for pre-#422 servers without /dashboard/model. */
|
|
3252
|
+
const raw=(M&&M.model)||(MODEL_LABEL&&MODEL_LABEL!=='not configured'?MODEL_LABEL:'');
|
|
3253
|
+
if(!raw){
|
|
2912
3254
|
/* #409 fix round — the no-LLM state says so ON THE FACE (a muted hint),
|
|
2913
3255
|
not a bare dash: correct degraded state, self-explanatory rendering */
|
|
2914
|
-
|
|
2915
|
-
|
|
2916
|
-
el('model-tail').textContent='';
|
|
3256
|
+
hero.textContent='no model configured';
|
|
3257
|
+
hero.classList.add('noconf');
|
|
2917
3258
|
el('model-id').textContent='';
|
|
2918
3259
|
return;
|
|
2919
3260
|
}
|
|
2920
|
-
|
|
2921
|
-
const
|
|
2922
|
-
|
|
2923
|
-
|
|
2924
|
-
|
|
2925
|
-
|
|
2926
|
-
|
|
3261
|
+
hero.classList.remove('noconf');
|
|
3262
|
+
const ns=raw.lastIndexOf('/');
|
|
3263
|
+
hero.textContent=ns>=0?raw.slice(ns+1):raw; // the model name = hero
|
|
3264
|
+
/* Long ids are the norm (mlx-community/Qwen3.8-27B-4bit et al); the hero
|
|
3265
|
+
NEVER clips — it scales down to the slot (floor 20px) while the full id
|
|
3266
|
+
rides the mono line. One pass is exact: text width is linear in
|
|
3267
|
+
font-size. */
|
|
3268
|
+
if(hero.scrollWidth>hero.clientWidth){
|
|
3269
|
+
const fs=parseFloat(getComputedStyle(hero).fontSize);
|
|
3270
|
+
hero.style.fontSize=Math.max(20,Math.floor(fs*hero.clientWidth/hero.scrollWidth))+'px';
|
|
3271
|
+
}
|
|
3272
|
+
el('model-id').textContent=raw;
|
|
2927
3273
|
}
|
|
2928
3274
|
|
|
2929
3275
|
/* ---------- model settings modal (#422 phase 2) — the configure pill's
|
|
@@ -3059,7 +3405,14 @@ function buildRoll(){
|
|
|
3059
3405
|
|
|
3060
3406
|
/* ---------- capture health — phase 2: per machine × harness rows from the
|
|
3061
3407
|
/dashboard/data capture_health accounting; the counts-only rows remain
|
|
3062
|
-
the degrade for old servers / nothing recorded yet
|
|
3408
|
+
the degrade for old servers / nothing recorded yet.
|
|
3409
|
+
Fix round 7 (owner ruling 2026-09-14 — "the normal 30 day as the other
|
|
3410
|
+
cards"): the card's face is the ROLLING window (capture_health
|
|
3411
|
+
window_rows, summed over the last window_days calendar days); the newest
|
|
3412
|
+
night rides as a per-row secondary suffix ("tonight: N sess" / the
|
|
3413
|
+
night's date when it is not today). Degradation ladder: pre-round-7
|
|
3414
|
+
server (no window_rows) → the newest-night view below; no activity rows
|
|
3415
|
+
at all → the counts-only rows. ---------- */
|
|
3063
3416
|
/* ---------- machine × harness grouping (#424 review fixes 4+5 + the owner's
|
|
3064
3417
|
model-B direction): source_agent is "harness/profile" and each context may
|
|
3065
3418
|
carry a source_machine stamp (migration v15+). The rows a stats card and
|
|
@@ -3171,6 +3524,72 @@ function buildCapture(){
|
|
|
3171
3524
|
host.innerHTML='';
|
|
3172
3525
|
const sub=el('cap-sub');
|
|
3173
3526
|
const ch=D&&D.capture_health?D.capture_health:null;
|
|
3527
|
+
/* fix round 7: the rolling-window view — the card shows the normal 30 day
|
|
3528
|
+
like the band's other cards. window_rows is ABSENT on a pre-round-7
|
|
3529
|
+
server (and empty when nothing is recorded) → fall through to the
|
|
3530
|
+
newest-night view / counts degrade below. */
|
|
3531
|
+
if(ch&&Array.isArray(ch.window_rows)&&ch.window_rows.length){
|
|
3532
|
+
const wd=ch.window_days||30;
|
|
3533
|
+
if(sub) sub.textContent='last '+wd+' days';
|
|
3534
|
+
/* group the window's machine × agent rows into machine × harness
|
|
3535
|
+
bundles — same grouping as every other card surface */
|
|
3536
|
+
const groups=new Map();
|
|
3537
|
+
for(const r of ch.window_rows){
|
|
3538
|
+
const agent=String(r.agent||'unknown');
|
|
3539
|
+
const harness=agent.split('/')[0]||agent;
|
|
3540
|
+
const key=(r.machine||'')+'|'+harness;
|
|
3541
|
+
let g=groups.get(key);
|
|
3542
|
+
if(!g){ g={machine:r.machine||null, harness, posts:0, sessions:0, bytes:0, held:0, retried:0, agents:[]}; groups.set(key,g); }
|
|
3543
|
+
g.posts+=r.posts||0; g.sessions+=r.sessions||0; g.bytes+=r.bytes||0;
|
|
3544
|
+
g.held+=r.held||0; g.retried+=r.retried||0;
|
|
3545
|
+
g.agents.push(agent+' — '+r.posts+' post'+(r.posts===1?'':'s')+' · '+r.sessions+' sess · '+mbFmt(r.bytes||0)
|
|
3546
|
+
+(r.held?' · '+r.held+' held':'')+(r.retried?' · '+r.retried+' retried':''));
|
|
3547
|
+
}
|
|
3548
|
+
/* newest-night secondary: per-bundle sessions + held off capture_health
|
|
3549
|
+
rows (the newest day with rows). held keys the badge — a held post is
|
|
3550
|
+
actionable NOW (the client retries it next run); the window's own
|
|
3551
|
+
held/retried live in the row tooltip. */
|
|
3552
|
+
const night=new Map();
|
|
3553
|
+
if(ch.day&&Array.isArray(ch.rows)) for(const r of ch.rows){
|
|
3554
|
+
const agent=String(r.agent||'unknown');
|
|
3555
|
+
const harness=agent.split('/')[0]||agent;
|
|
3556
|
+
const key=(r.machine||'')+'|'+harness;
|
|
3557
|
+
const prev=night.get(key)||{sessions:0,held:0};
|
|
3558
|
+
prev.sessions+=r.sessions||0; prev.held+=r.held||0;
|
|
3559
|
+
night.set(key,prev);
|
|
3560
|
+
}
|
|
3561
|
+
const today=new Date().toISOString().slice(0,10);
|
|
3562
|
+
const nightLabel=ch.day===today?'tonight':fmtDay(ch.day);
|
|
3563
|
+
const gs2=[...groups.values()].sort((a,b)=>b.bytes-a.bytes);
|
|
3564
|
+
const maxB=gs2[0].bytes;
|
|
3565
|
+
const pausedKeys=fleetPausesByKey();
|
|
3566
|
+
for(const g of gs2){
|
|
3567
|
+
const n=night.get((g.machine||'')+'|'+g.harness);
|
|
3568
|
+
const row=document.createElement('div');
|
|
3569
|
+
row.className='cap-row2';
|
|
3570
|
+
const label=g.machine?g.machine+' · '+g.harness:g.harness;
|
|
3571
|
+
const paused=pausedKeys.has((g.machine||'')+'|'+g.harness);
|
|
3572
|
+
row.title=label+' — last '+wd+' days: '+fmt(g.sessions)+' sessions · '+mbFmt(g.bytes)+' posted'
|
|
3573
|
+
+(g.held>0?' · '+g.held+' held in window':'')
|
|
3574
|
+
+(g.retried>0?' · '+g.retried+' retried in window':'')
|
|
3575
|
+
+(n?' · '+nightLabel+': '+n.sessions+' sess'+(n.held>0?' ('+n.held+' held)':''):'')
|
|
3576
|
+
+(paused?' · CAPTURE PAUSED (posts are skipped — sessions are not backfilled)':'')
|
|
3577
|
+
+':\n'+g.agents.join('\n');
|
|
3578
|
+
row.innerHTML='<span class="cap-dot"></span>'+
|
|
3579
|
+
'<span class="hn" title="'+esc(label)+'">'+esc(label)+'</span>'+
|
|
3580
|
+
'<span class="cap-bar"><span class="fill" style="width:'+(g.bytes/Math.max(maxB,1)*100)+'%"></span></span>'+
|
|
3581
|
+
'<span class="st"><b>'+fmt(g.sessions)+'</b> sess · '+mbFmt(g.bytes)
|
|
3582
|
+
+(paused?'<span class="cap-paused" title="capture paused — posts are skipped (200) and sessions are not backfilled">PAUSED</span>':'')
|
|
3583
|
+
+(n&&n.held>0?'<span class="cap-held" title="'+nightLabel+' — failed posts the client will retry — cursor held">⚠ '+n.held+' held</span>':'')
|
|
3584
|
+
+'</span>'
|
|
3585
|
+
/* the newest-night sub-line — a full-basis flex child, so it wraps
|
|
3586
|
+
UNDER the primary line (the suffix never fits beside it; see the
|
|
3587
|
+
.cap-ton rule) and only exists for bundles with newest-night rows */
|
|
3588
|
+
+(n?'<span class="cap-ton" title="newest capture night ('+nightLabel+') — '+n.sessions+' session'+(n.sessions===1?'':'s')+' posted that night">'+nightLabel+': '+n.sessions+' sess</span>':'');
|
|
3589
|
+
host.appendChild(row);
|
|
3590
|
+
}
|
|
3591
|
+
return;
|
|
3592
|
+
}
|
|
3174
3593
|
if(ch&&Array.isArray(ch.rows)&&ch.rows.length){
|
|
3175
3594
|
/* phase-2 live view: the /distill accounting rows, grouped machine ×
|
|
3176
3595
|
harness — width encodes capture volume (each bundle's byte share of
|
|
@@ -3249,7 +3668,9 @@ function buildCapture(){
|
|
|
3249
3668
|
function lastNightByAgent(){
|
|
3250
3669
|
for(let i=NIGHTS.length-1;i>=0;i--){
|
|
3251
3670
|
const n=NIGHTS[i];
|
|
3252
|
-
|
|
3671
|
+
/* #452: the truth-management events count too — a superseded-only night
|
|
3672
|
+
is a real night */
|
|
3673
|
+
if(n.added.length+n.enriched.length+n.merged.length+n.linked.length+n.superseded.length+n.retracted.length+n.rewritten.length>0) return {date:n.date,by:n.by_agent};
|
|
3253
3674
|
}
|
|
3254
3675
|
return null;
|
|
3255
3676
|
}
|
|
@@ -3487,7 +3908,7 @@ function buildAgents(){
|
|
|
3487
3908
|
* counts — the paused state and dots re-derive from the fresh payload. */
|
|
3488
3909
|
async function refreshFleet(){
|
|
3489
3910
|
try{
|
|
3490
|
-
const dd=await api('/dashboard/data?range=
|
|
3911
|
+
const dd=await api('/dashboard/data?range='+RANGE+'d');
|
|
3491
3912
|
D=dd;
|
|
3492
3913
|
buildAgents();
|
|
3493
3914
|
buildCapture();
|
|
@@ -3868,7 +4289,7 @@ let refreshInFlight=false;
|
|
|
3868
4289
|
async function refreshAfterWrite(){
|
|
3869
4290
|
if(refreshInFlight) return; refreshInFlight=true;
|
|
3870
4291
|
try{
|
|
3871
|
-
const [ff,dd]=await Promise.all([api('/dashboard/field'),api('/dashboard/data?range=
|
|
4292
|
+
const [ff,dd]=await Promise.all([api('/dashboard/field'),api('/dashboard/data?range='+RANGE+'d').catch(()=>null)]);
|
|
3872
4293
|
if(ff){ F=ff; buildWorld(); }
|
|
3873
4294
|
if(dd){ D=dd; }
|
|
3874
4295
|
renderBand(); buildRoll(); buildCapture(); renderLegend(); renderFeel();
|
|
@@ -4179,9 +4600,9 @@ async function pollHealth(){
|
|
|
4179
4600
|
===================================================================== */
|
|
4180
4601
|
async function boot(){
|
|
4181
4602
|
const [dd,ff,ee,hh,hd,mm]=await Promise.allSettled([
|
|
4182
|
-
api('/dashboard/data?range=
|
|
4603
|
+
api('/dashboard/data?range='+RANGE+'d'),
|
|
4183
4604
|
api('/dashboard/field'),
|
|
4184
|
-
api('/dashboard/events?days=
|
|
4605
|
+
api('/dashboard/events?days='+RANGE),
|
|
4185
4606
|
fetch('/health'),
|
|
4186
4607
|
api('/health/detail'),
|
|
4187
4608
|
api('/dashboard/model'),
|
|
@@ -4194,6 +4615,12 @@ async function boot(){
|
|
|
4194
4615
|
if(hd.status==='fulfilled'&&hd.value&&typeof hd.value.llm==='string') MODEL_LABEL=hd.value.llm;
|
|
4195
4616
|
else if(hd.status==='fulfilled'&&hd.value&&hd.value.model) MODEL_LABEL=String(hd.value.model);
|
|
4196
4617
|
if(hh.status==='fulfilled'&&hh.value&&hh.value.status) { /* health body shape varies; the poll handles the dot */ }
|
|
4618
|
+
/* entry splash (#409 fix round 6): BOTH core sources rejected = total
|
|
4619
|
+
failure — swap the splash line to the honest error (the dismissal holds
|
|
4620
|
+
it readable, then fades into this degraded page, whose own error
|
|
4621
|
+
surfaces — the toast above, the unreachable dot — carry the detail). */
|
|
4622
|
+
if(dd.status==='rejected' && ff.status==='rejected')
|
|
4623
|
+
entrySplashFail(ENTRY_FAIL_COPY);
|
|
4197
4624
|
|
|
4198
4625
|
buildWorld(); // no-op when F failed — the empty field still renders
|
|
4199
4626
|
buildPaths();
|
|
@@ -4219,13 +4646,21 @@ async function boot(){
|
|
|
4219
4646
|
loadAccount();
|
|
4220
4647
|
pollHealth();
|
|
4221
4648
|
|
|
4222
|
-
const reveal=()=>{ booting=false; document.body.classList.remove('booting'); dirty=true; };
|
|
4649
|
+
const reveal=()=>{ booting=false; document.body.classList.remove('booting'); dirty=true; entrySplashDismiss(); };
|
|
4223
4650
|
if(REDUCED||STILL) reveal();
|
|
4224
4651
|
else setTimeout(reveal, 1400);
|
|
4225
4652
|
setTimeout(()=>{ const h=el('hint'); if(h) h.style.opacity='0'; }, 9500);
|
|
4226
4653
|
setTimeout(()=>{ if(!REDUCED&&!STILL){ spawnSignal(true); nextSpawnAt=performance.now()+4200; } }, (REDUCED||STILL)?0:2650);
|
|
4227
4654
|
}
|
|
4228
|
-
boot()
|
|
4655
|
+
boot().catch(err=>{
|
|
4656
|
+
/* a thrown boot must never leave the splash looping: honest line on the
|
|
4657
|
+
splash, the static shell visible after the readable hold, toast per the
|
|
4658
|
+
page's error pattern (the catch mirrors reveal minus the redraw). */
|
|
4659
|
+
try{ console.error('dashboard boot failed', err); }catch(e2){}
|
|
4660
|
+
booting=false; document.body.classList.remove('booting');
|
|
4661
|
+
toast('Boot failed — '+(err && err.message ? err.message : 'unknown error'));
|
|
4662
|
+
entrySplashFail(ENTRY_BOOTFAIL_COPY);
|
|
4663
|
+
});
|
|
4229
4664
|
rafId=requestAnimationFrame(frame);
|
|
4230
4665
|
resize();
|
|
4231
4666
|
draw(performance.now(),16); /* one synchronous paint — the field exists before the first rAF */
|