@booboo-brain/panel 0.3.4 → 0.4.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/dist/index.js CHANGED
@@ -1,5 +1,1678 @@
1
+ // src/Panel.tsx
2
+ import { createContext, useCallback, useContext, useEffect, useLayoutEffect, useMemo, useRef, useState } from "react";
3
+ import { orgBootSlice } from "@booboo-brain/spec";
4
+
5
+ // src/panel-css.ts
6
+ var PANEL_CSS = String.raw`
7
+ /* Booboo Organigram — alive, simple, for everyone. One accent (verdigris,
8
+ the dachshund's collar), a slow aurora breathing behind everything, cards
9
+ that lift and settle, numbers that count up. Dark by default. */
10
+
11
+ :root {
12
+ --bg: #0b0c10;
13
+ --bg-1: #10131a;
14
+ --bg-2: #171b24;
15
+ --line: #242a36;
16
+ --ink: #e9edf3;
17
+ --ink-2: #9aa7b6;
18
+ --ink-3: #5d6776;
19
+ --accent: #4fb8a0;
20
+ --accent-2: #7fd4c0;
21
+ --accent-dim: rgba(79, 184, 160, 0.13);
22
+ --warn: #e0b45f;
23
+ --font: -apple-system, "Segoe UI", Inter, system-ui, sans-serif;
24
+ --mono: ui-monospace, "SF Mono", Menlo, monospace;
25
+ }
26
+
27
+ /* LIGHT theme — same verdigris accent, swapped surfaces. Toggle in the top bar. */
28
+ :root[data-theme="light"] {
29
+ --bg: #f5f7f9;
30
+ --bg-1: #ffffff;
31
+ --bg-2: #eef1f4;
32
+ --line: #dbe1e8;
33
+ --ink: #182029;
34
+ --ink-2: #55636f;
35
+ --ink-3: #8b97a3;
36
+ --accent: #1e8f79;
37
+ --accent-2: #12796a;
38
+ --accent-dim: rgba(30, 143, 121, 0.12);
39
+ --warn: #b57b1e;
40
+ }
41
+ [data-theme="light"] .bar,
42
+ [data-theme="light"] .tabs { background: rgba(255, 255, 255, 0.82); }
43
+ [data-theme="light"] .btn.primary { color: #ffffff; }
44
+ [data-theme="light"] .pnl-aurora { opacity: 0.32; }
45
+ [data-theme="light"] .doss-rep.tap:hover { background: rgba(0, 0, 0, 0.03); }
46
+ [data-theme="light"] .bk-card:hover,
47
+ [data-theme="light"] .mem-row:hover { box-shadow: 0 8px 22px rgba(20, 40, 60, 0.08); }
48
+ /* surfaces that hardcode a dark tint — retint for the light theme */
49
+ [data-theme="light"] .oc-row.wrap { background: rgba(20, 40, 60, 0.04); }
50
+ [data-theme="light"] .doss { background: rgba(255, 255, 255, 0.9); }
51
+ [data-theme="light"] .ag {
52
+ background: linear-gradient(180deg, hsl(var(--h) 42% 93% / 0.9), var(--bg-1) 60%);
53
+ border-color: hsl(var(--h) 30% 78%);
54
+ }
55
+ [data-theme="light"] .tl-ava { background: hsl(var(--h) 45% 90% / 0.9); border-color: hsl(var(--h) 40% 75% / 0.5); }
56
+ [data-theme="light"] .oc-tray { background: linear-gradient(180deg, #e7ebef, #eef1f4); box-shadow: inset 0 3px 7px rgba(20, 40, 60, 0.08); }
57
+ [data-theme="light"] .oc-mac { background: rgba(20, 40, 60, 0.05); border-color: rgba(20, 40, 60, 0.1); }
58
+
59
+ * { box-sizing: border-box; }
60
+ body { font-family: var(--font); color: var(--ink); background: var(--bg); overflow-x: hidden; }
61
+
62
+ .pnl { display: flex; flex-direction: column; position: relative; overflow: hidden; }
63
+
64
+ /* the breath — a slow aurora drifting behind everything */
65
+ .pnl-aurora {
66
+ position: fixed; inset: -30%; pointer-events: none; z-index: 0; opacity: 0.5;
67
+ background:
68
+ radial-gradient(38% 30% at 24% 30%, rgba(79, 184, 160, 0.10), transparent 70%),
69
+ radial-gradient(30% 26% at 78% 64%, rgba(90, 130, 200, 0.08), transparent 70%),
70
+ radial-gradient(24% 22% at 55% 15%, rgba(79, 184, 160, 0.06), transparent 70%);
71
+ animation: aurora 48s ease-in-out infinite alternate;
72
+ }
73
+ @keyframes aurora {
74
+ 0% { transform: translate(0, 0) scale(1); }
75
+ 50% { transform: translate(3%, -2%) scale(1.06); }
76
+ 100% { transform: translate(-3%, 2%) scale(1.02); }
77
+ }
78
+ @media (prefers-reduced-motion: reduce) {
79
+ /* everything lands at its final state — honest for users, crisp for captures */
80
+ .pnl-aurora, .ag.root::before, .ag.root::after, .ag, .content, .bk-card, .tl-row, .mem-row,
81
+ .rule-card, .doss, .doss-rep, .bar-draft, .ag-light.fail, .mac-dot.fail { animation: none !important; }
82
+ }
83
+ /* the fit is a TRANSITION not an animation — suppress it explicitly so the org
84
+ snaps to its fitted size instantly under reduced-motion (honest for captures) */
85
+ @media (prefers-reduced-motion: reduce) { .chart { transition: none !important; } }
86
+
87
+ /* top bar */
88
+ .bar {
89
+ position: relative; z-index: 2; display: flex; align-items: center; gap: 22px;
90
+ padding: 14px 26px; border-bottom: 1px solid var(--line);
91
+ background: rgba(16, 19, 26, 0.82); backdrop-filter: blur(10px);
92
+ flex-wrap: wrap;
93
+ }
94
+ .bar-brand { font-size: 15px; }
95
+ .bar-brand b { font-weight: 650; }
96
+ .bar-stats { display: flex; gap: 18px; color: var(--ink-2); font-size: 12.5px; }
97
+ .bar-stats b { color: var(--ink); font-variant-numeric: tabular-nums; font-size: 14px; }
98
+ .bar-stats .tap { cursor: pointer; transition: color 0.15s; }
99
+ .bar-stats .tap:hover { color: var(--accent); }
100
+ .bar-stats .tap:hover b { color: var(--accent); }
101
+ .bar-actions { margin-left: auto; display: flex; align-items: center; gap: 10px; }
102
+ .bar-ok { color: var(--accent); font-size: 12px; }
103
+ .bar-draft { color: var(--warn); font-size: 12px; animation: pulse 1.6s ease-in-out infinite; }
104
+ @keyframes pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.5; } }
105
+
106
+ .btn {
107
+ border: 1px solid var(--line); background: var(--bg-2); color: var(--ink);
108
+ border-radius: 9px; padding: 8px 15px; font-size: 12.5px; cursor: pointer;
109
+ transition: border-color 0.15s, transform 0.1s;
110
+ }
111
+ .btn:hover { border-color: var(--accent); }
112
+ .btn:active { transform: scale(0.97); }
113
+ .btn.primary { background: var(--accent); border-color: var(--accent); color: #08110e; font-weight: 650; }
114
+ .btn.primary:disabled { opacity: 0.5; cursor: wait; }
115
+ .btn.ghost { background: transparent; color: var(--ink-2); }
116
+
117
+ /* tabs */
118
+ .tabs {
119
+ position: relative; z-index: 2; display: flex; gap: 4px; padding: 0 22px;
120
+ border-bottom: 1px solid var(--line); background: rgba(16, 19, 26, 0.7); backdrop-filter: blur(8px);
121
+ overflow-x: auto;
122
+ }
123
+ .tab {
124
+ background: none; border: none; border-bottom: 2px solid transparent; color: var(--ink-2);
125
+ font-size: 12.5px; padding: 12px 15px 11px; cursor: pointer; white-space: nowrap;
126
+ transition: color 0.15s, border-color 0.2s;
127
+ }
128
+ .tab:hover { color: var(--ink); }
129
+ .tab.on { color: var(--accent); border-bottom-color: var(--accent); font-weight: 600; }
130
+ .tab-glyph { margin-right: 5px; opacity: 0.85; }
131
+
132
+ /* every tab's content rises in — the key on .content retriggers it */
133
+ .content { position: relative; z-index: 1; flex: 1; display: flex; flex-direction: column; min-height: 0; animation: rise 0.28s ease both; }
134
+ @keyframes rise { from { opacity: 0; transform: translateY(8px); } to { opacity: 1; transform: none; } }
135
+
136
+ .screen { flex: 1; overflow-y: auto; padding: 30px 34px 60px; max-width: 900px; }
137
+ .scr-title { display: flex; align-items: center; gap: 10px; font-size: 20px; margin: 0 0 6px; }
138
+ .scr-count { font-size: 12px; background: var(--accent-dim); color: var(--accent); border-radius: 99px; padding: 3px 10px; font-variant-numeric: tabular-nums; }
139
+ .scr-sub { color: var(--ink-2); font-size: 12.5px; margin: 0 0 24px; }
140
+ .scr-empty { color: var(--ink-3); font-size: 13px; line-height: 1.6; }
141
+ .scr-empty code, .scr-sub code { font-family: var(--mono); background: var(--bg-2); padding: 1px 6px; border-radius: 5px; font-size: 11.5px; }
142
+ .pnl-back { background: none; border: 1px solid var(--line); color: var(--ink-2); border-radius: 8px; padding: 6px 12px; font-size: 12px; cursor: pointer; margin-bottom: 16px; }
143
+ .pnl-back:hover { border-color: var(--accent); color: var(--accent); }
144
+
145
+ /* buckets */
146
+ .bk-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); gap: 13px; }
147
+ .bk-card {
148
+ text-align: left; background: var(--bg-1); border: 1px solid var(--line); border-radius: 15px;
149
+ padding: 17px 18px; cursor: pointer; transition: border-color 0.15s, transform 0.12s, box-shadow 0.2s;
150
+ animation: settle 0.4s ease both;
151
+ }
152
+ .bk-card:hover { border-color: hsl(var(--h) 45% 55% / 0.6); transform: translateY(-2px); box-shadow: 0 8px 26px hsl(var(--h) 50% 50% / 0.10); }
153
+ .bk-top { display: flex; align-items: center; gap: 8px; margin-bottom: 12px; }
154
+ .bk-dot { width: 9px; height: 9px; border-radius: 50%; background: hsl(var(--h) 55% 60%); box-shadow: 0 0 9px hsl(var(--h) 55% 60% / 0.7); display: inline-block; }
155
+ .bk-name { font-size: 14px; font-weight: 650; }
156
+ .bk-n { font-size: 27px; font-weight: 750; font-variant-numeric: tabular-nums; line-height: 1; }
157
+ .bk-n.dim { color: var(--ink-3); }
158
+ .bk-l { font-size: 9.5px; color: var(--ink-3); text-transform: uppercase; letter-spacing: 0.06em; margin-top: 4px; }
159
+ .bk-crew { display: flex; gap: 4px; margin-top: 13px; flex-wrap: wrap; }
160
+ .bk-face {
161
+ width: 24px; height: 24px; display: grid; place-items: center; font-size: 12px;
162
+ border-radius: 7px; background: var(--bg-2); border: 1px solid var(--line);
163
+ }
164
+ .bk-more { font-size: 10px; color: var(--ink-3); align-self: center; }
165
+ .bk-agents { margin-top: 8px; font-size: 10.5px; color: var(--ink-3); line-height: 1.5; }
166
+
167
+ .mem-list { display: flex; flex-direction: column; gap: 6px; }
168
+ .mem-row {
169
+ display: flex; align-items: baseline; gap: 13px; background: var(--bg-1);
170
+ border: 1px solid var(--line); border-radius: 10px; padding: 10px 14px; animation: settle 0.35s ease both;
171
+ }
172
+ .mem-when { font-size: 10.5px; color: var(--accent-2); white-space: nowrap; min-width: 68px; font-variant-numeric: tabular-nums; }
173
+ .mem-label { font-size: 13px; }
174
+
175
+ /* reports timeline */
176
+ .rep-filter { display: flex; gap: 7px; flex-wrap: wrap; margin-bottom: 20px; }
177
+ .chip.tap { cursor: pointer; transition: transform 0.1s, background 0.15s; }
178
+ .chip.tap:hover { transform: translateY(-1px); }
179
+ .timeline { position: relative; padding-left: 22px; }
180
+ .timeline::before { content: ""; position: absolute; left: 5px; top: 6px; bottom: 6px; width: 1px; background: var(--line); }
181
+ .tl-row { position: relative; margin-bottom: 15px; animation: settle 0.35s ease both; }
182
+ .tl-dot {
183
+ position: absolute; left: -21px; top: 12px; width: 9px; height: 9px; border-radius: 50%;
184
+ background: hsl(var(--h) 55% 60%); box-shadow: 0 0 9px hsl(var(--h) 55% 60% / 0.7);
185
+ }
186
+ .tl-body { background: var(--bg-1); border: 1px solid var(--line); border-radius: 12px; padding: 11px 15px; transition: border-color 0.15s, transform 0.12s; }
187
+ .tl-row:hover .tl-body { border-color: hsl(var(--h) 45% 50% / 0.5); transform: translateX(2px); }
188
+ .tl-top { display: flex; align-items: center; gap: 8px; }
189
+ .tl-ava {
190
+ width: 24px; height: 24px; flex: 0 0 24px; display: grid; place-items: center; font-size: 12px;
191
+ border-radius: 7px; background: hsl(var(--h) 42% 22% / 0.8); border: 1px solid hsl(var(--h) 45% 45% / 0.3);
192
+ }
193
+ .tl-agent { font-size: 12.5px; font-weight: 650; }
194
+ .tl-when { font-size: 10.5px; color: var(--ink-3); margin-left: auto; white-space: nowrap; font-variant-numeric: tabular-nums; }
195
+ .tl-label { font-size: 12px; color: var(--ink-3); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
196
+ .tl-sum { margin: 7px 0 0 32px; font-size: 12.5px; line-height: 1.6; color: var(--ink-2); }
197
+
198
+ /* rules */
199
+ .rule-list { display: flex; flex-direction: column; gap: 9px; }
200
+ .rule-card { background: var(--bg-1); border: 1px solid var(--line); border-left: 2px solid var(--accent); border-radius: 11px; padding: 12px 15px 10px; animation: settle 0.35s ease both; transition: border-color 0.15s; }
201
+ .rule-card:hover { border-color: var(--accent); }
202
+ .rule-top { display: flex; align-items: baseline; gap: 10px; margin-bottom: 5px; }
203
+ .rule-ref { font-family: var(--mono); font-size: 12.5px; }
204
+ .rule-scope { font-size: 9.5px; text-transform: uppercase; letter-spacing: 0.07em; color: var(--accent); background: var(--accent-dim); border-radius: 5px; padding: 2px 7px; }
205
+ .rule-meta { font-size: 11.5px; color: var(--ink-3); line-height: 1.6; }
206
+ .rule-meta b { color: var(--ink-2); font-weight: 600; }
207
+ .rule-bar { margin-top: 9px; height: 3px; border-radius: 99px; background: var(--bg-2); overflow: hidden; }
208
+ .rule-bar i { display: block; height: 100%; border-radius: 99px; background: linear-gradient(90deg, var(--accent), var(--accent-2)); transition: width 0.6s ease; }
209
+
210
+ /* graph */
211
+ .graph-wrap { flex: 1; min-height: 0; display: flex; }
212
+ .graph-frame { flex: 1; border: none; background: #06080e; }
213
+
214
+ /* dossier extras */
215
+ .doss-head-actions { margin-left: auto; display: flex; gap: 6px; }
216
+ .doss-3d { background: var(--accent-dim); border: 1px solid rgba(79, 184, 160, 0.35); color: var(--accent); border-radius: 8px; padding: 6px 10px; font-size: 11px; cursor: pointer; white-space: nowrap; }
217
+ .doss-3d:hover { border-color: var(--accent); }
218
+
219
+ /* dossier edit mode — every field of the agent, saved into the draft */
220
+ .doss-edit { display: flex; flex-direction: column; gap: 10px; background: var(--bg-1); border: 1px solid rgba(79, 184, 160, 0.3); border-radius: 12px; padding: 13px 14px; margin-bottom: 20px; animation: settle 0.25s ease both; }
221
+ .doss-edit label { display: flex; flex-direction: column; gap: 4px; font-size: 10px; text-transform: uppercase; letter-spacing: 0.07em; color: var(--ink-3); }
222
+ .doss-edit label em { font-style: normal; text-transform: none; letter-spacing: 0; color: var(--ink-3); font-size: 9.5px; }
223
+ .doss-edit input, .doss-edit textarea { background: var(--bg-2); border: 1px solid var(--line); color: var(--ink); border-radius: 8px; padding: 8px 10px; font-size: 12.5px; font-family: var(--font); resize: vertical; }
224
+ .doss-edit input:focus, .doss-edit textarea:focus { outline: none; border-color: var(--accent); }
225
+ .doss-edit-row2 { display: grid; grid-template-columns: 1fr 72px; gap: 8px; }
226
+ .doss-edit-actions { display: flex; gap: 8px; margin-top: 2px; }
227
+
228
+ /* machine trays — the automations an agent operates, chips with health lights */
229
+ /* the machine rack — a recessed slot flush under the owner's card */
230
+ .oc-tray {
231
+ display: flex; flex-wrap: wrap; gap: 4px; justify-content: center;
232
+ max-width: 210px; margin-top: -6px; padding: 10px 8px 7px;
233
+ background: linear-gradient(180deg, #0a0c11, #0d1017);
234
+ border: 1px solid var(--line); border-top: 0; border-radius: 0 0 14px 14px;
235
+ box-shadow: inset 0 3px 8px rgba(0, 0, 0, 0.55);
236
+ }
237
+ .oc-tray.fail { border-color: rgba(224, 123, 106, 0.55); box-shadow: inset 0 3px 8px rgba(0, 0, 0, 0.55), 0 6px 18px -8px rgba(224, 123, 106, 0.3); }
238
+ .oc-tray.warn { border-color: rgba(224, 180, 95, 0.45); }
239
+ .oc-mac {
240
+ display: inline-flex; align-items: center; gap: 5px;
241
+ background: rgba(255, 255, 255, 0.03); border: 1px solid rgba(255, 255, 255, 0.05);
242
+ border-radius: 6px; padding: 4px 8px; cursor: pointer;
243
+ font-family: var(--mono); font-size: 10px; color: var(--ink-2);
244
+ transition: border-color 0.15s, transform 0.1s;
245
+ }
246
+ .oc-mac:hover { border-color: var(--accent); color: var(--ink); transform: translateY(-1px); }
247
+ .oc-mac.sel { border-color: var(--accent); background: var(--accent-dim); color: var(--ink); }
248
+ .oc-mac.more { color: var(--ink-3); font-style: italic; }
249
+ .oc-mac.more:hover { color: var(--accent); }
250
+ .mac-emoji { font-size: 11px; }
251
+ .mac-name { white-space: nowrap; max-width: 76px; overflow: hidden; text-overflow: ellipsis; }
252
+ .mac-dot { width: 7px; height: 7px; border-radius: 50%; flex: 0 0 7px; display: inline-block; }
253
+ .mac-dot.ok { background: var(--accent); box-shadow: 0 0 6px rgba(79, 184, 160, 0.7); }
254
+ .mac-dot.warn { background: var(--warn); box-shadow: 0 0 6px rgba(224, 180, 95, 0.7); }
255
+ .mac-dot.fail { background: #e07b6a; box-shadow: 0 0 7px rgba(224, 123, 106, 0.8); animation: pulse 1.4s ease-in-out infinite; }
256
+ .mac-dot.none { background: var(--ink-3); }
257
+ /* green, but it failed earlier in the window — a faint amber ring, never a tint */
258
+ .mac-dot.ok.unstable { box-shadow: 0 0 6px rgba(79, 184, 160, 0.7), 0 0 0 2px rgba(224, 180, 95, 0.3); }
259
+
260
+ /* automation dossier health header */
261
+ .doss-health {
262
+ display: flex; align-items: center; gap: 10px; flex-wrap: wrap;
263
+ background: var(--bg-1); border: 1px solid var(--line); border-radius: 10px;
264
+ padding: 10px 13px; margin-bottom: 16px; font-size: 11.5px; color: var(--ink-2);
265
+ }
266
+ .doss-health b { color: var(--ink); font-size: 12px; }
267
+ .doss-health.fail { border-color: rgba(224, 123, 106, 0.5); }
268
+ .doss-health.warn { border-color: rgba(224, 180, 95, 0.4); }
269
+ .doss-health.ok { border-color: rgba(79, 184, 160, 0.35); }
270
+ .doss-auto-when { margin-left: auto; font-size: 10px; color: var(--ink-3); white-space: nowrap; }
271
+
272
+ /* automations — machines an agent operates, listed in its dossier */
273
+ .auto-badge { font-size: 9px; text-transform: uppercase; letter-spacing: 0.08em; color: var(--warn); background: rgba(224, 180, 95, 0.1); border: 1px solid rgba(224, 180, 95, 0.3); border-radius: 5px; padding: 2px 7px; vertical-align: middle; margin-left: 6px; }
274
+ .doss-autos { display: flex; flex-direction: column; gap: 5px; }
275
+ .doss-auto { display: flex; align-items: baseline; gap: 9px; text-align: left; background: var(--bg-1); border: 1px solid var(--line); border-radius: 9px; padding: 8px 11px; cursor: pointer; transition: border-color 0.15s; }
276
+ .doss-auto:hover { border-color: var(--warn); }
277
+ .doss-auto-emoji { font-size: 13px; }
278
+ .doss-auto-name { font-size: 12px; font-weight: 600; white-space: nowrap; }
279
+ .doss-auto-role { font-size: 10.5px; color: var(--ink-3); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
280
+
281
+ .doss-foot { display: flex; flex-direction: column; gap: 8px; margin-top: 6px; padding-top: 14px; border-top: 1px solid var(--line); }
282
+ .btn.danger { border-color: rgba(224, 123, 106, 0.45); color: #e07b6a; background: transparent; }
283
+ .btn.danger:hover { background: rgba(224, 123, 106, 0.1); }
284
+ .bk-agents.unassigned { color: var(--ink-3); font-style: italic; }
285
+
286
+ .pending {
287
+ position: relative; z-index: 2; display: flex; gap: 8px; flex-wrap: wrap;
288
+ padding: 10px 26px; border-bottom: 1px solid var(--line); background: rgba(224, 180, 95, 0.06);
289
+ }
290
+ .pending-item { font-size: 11.5px; color: var(--warn); font-family: var(--mono); }
291
+ .pnl-err { position: relative; z-index: 2; padding: 10px 26px; color: #e07b6a; font-size: 12.5px; }
292
+
293
+ /* layout */
294
+ .pnl { height: 100vh; }
295
+ .body { position: relative; z-index: 1; flex: 1; display: flex; min-height: 0; }
296
+ .tree { flex: 1; min-width: 0; padding: 24px 26px 24px; overflow: hidden;
297
+ display: flex; flex-direction: column;
298
+ /* the ground plane: a faint dot grid (engineered surface, drag affordance)
299
+ + a verdigris pool anchoring the eye at the root. Static — capture-safe. */
300
+ background:
301
+ radial-gradient(900px 420px at 50% 120px, rgba(79, 184, 160, 0.05), transparent 70%),
302
+ radial-gradient(circle at 1px 1px, rgba(255, 255, 255, 0.035) 1px, transparent 1.5px);
303
+ background-size: auto, 28px 28px;
304
+ }
305
+ .tree-hint { color: var(--ink-3); font-size: 11.5px; margin: 0 0 20px; text-align: center; }
306
+
307
+ /* THE COMPANY CHART — root on top, branches fanning beneath. Connectors are
308
+ crafted: curved elbows at the row's ends, branch-hued drops, a glowing
309
+ junction node where the parent's line meets the bar. Pure CSS. */
310
+ /* the measuring viewport — flex:1 + min-height:0 lets it consume the height
311
+ .tree gives it; align/justify center the scaled block for narrow orgs. */
312
+ .chart-fit { position: relative; flex: 1; min-height: 0; display: flex; align-items: flex-start; justify-content: center; overflow: hidden; }
313
+ .fit-badge {
314
+ position: absolute; right: 10px; bottom: 8px; font-size: 10px; font-family: var(--mono);
315
+ color: var(--ink-3); background: var(--bg-2); border: 1px solid var(--line); border-radius: 6px;
316
+ padding: 2px 7px; opacity: 0.7; pointer-events: none; animation: settle 0.4s ease both;
317
+ }
318
+ /* min-width:max-content keeps connectors geometrically correct at natural size
319
+ BEFORE scaling; the measured --fit shrinks the whole block toward its head. */
320
+ .chart {
321
+ min-width: max-content; padding: 8px 14px 20px;
322
+ transform: scale(var(--fit, 1)); transform-origin: top center;
323
+ transition: transform 0.35s cubic-bezier(0.22, 1, 0.36, 1); will-change: transform;
324
+ }
325
+ .ocn { display: flex; flex-direction: column; align-items: center; }
326
+ .oc-down { width: 1.5px; height: 18px; position: relative;
327
+ background: linear-gradient(180deg, hsl(165 40% 45% / 0.5), var(--line)); }
328
+ .oc-down::after { content: ""; position: absolute; bottom: -2px; left: 50%;
329
+ width: 5px; height: 5px; transform: translateX(-50%); border-radius: 50%;
330
+ background: var(--accent); box-shadow: 0 0 8px rgba(79, 184, 160, 0.5); }
331
+ .oc-row { display: flex; align-items: flex-start; }
332
+ .oc-child { position: relative; padding: 18px 5px 0; display: flex; flex-direction: column; align-items: center; }
333
+ .oc-child::before { content: ""; position: absolute; top: 0; left: 50%; width: 1.5px; height: 18px;
334
+ background: linear-gradient(180deg, var(--line), hsl(var(--h, 165) 32% 34%)); }
335
+ .oc-child::after { content: ""; position: absolute; top: 0; left: 0; right: 0; height: 16px;
336
+ border-top: 1.5px solid var(--line); }
337
+ .oc-child:first-child::after { left: 50%; border-left: 1.5px solid var(--line); border-top-left-radius: 14px; }
338
+ .oc-child:last-child::after { right: 50%; border-right: 1.5px solid var(--line); border-top-right-radius: 14px; }
339
+ .oc-child:first-child::before, .oc-child:last-child::before { display: none; }
340
+ .oc-child:only-child::after { display: none; }
341
+ .oc-child:only-child::before { display: block; }
342
+
343
+ /* the branch wakes when you hover its parent */
344
+ .ocn:has(> .ag:hover) > .oc-down,
345
+ .ocn:has(> .ag:hover) > .oc-row > .oc-child::before { background: hsl(165 45% 45% / 0.6); }
346
+ .ocn:has(> .ag:hover) > .oc-row > .oc-child::after { border-color: hsl(165 45% 45% / 0.6); }
347
+
348
+ /* wide sibling sets — grouped subtrees that wrap to natural width (never crush a
349
+ branch into a fixed cell — that overflowed and collided with the next branch) */
350
+ .oc-row.wrap {
351
+ display: flex; flex-wrap: wrap; justify-content: center; align-items: flex-start;
352
+ gap: 10px 18px; margin: 0 auto; padding: 12px 14px 14px;
353
+ max-width: min(100%, 840px);
354
+ border: 1px solid var(--line); border-radius: 18px; background: rgba(16, 19, 26, 0.35);
355
+ }
356
+ .oc-row.wrap > .oc-child { padding: 8px 4px 0; flex: 0 0 auto; }
357
+ .oc-row.wrap > .oc-child::before, .oc-row.wrap > .oc-child::after { display: none; }
358
+
359
+ /* agent cards — company badges with a rank system (size + saturation recede
360
+ with depth) and a light-from-above material model. */
361
+ .ag {
362
+ position: relative; display: flex; flex-direction: column; align-items: center; gap: 6px;
363
+ width: calc(160px - min(var(--d, 0), 2) * 12px); /* 160 / 148 / 136 by rank */
364
+ padding: 13px 11px 10px; text-align: center;
365
+ background: linear-gradient(180deg, hsl(var(--h) 30% 14% / 0.55), var(--bg-1) 55%);
366
+ border: 1px solid rgba(255, 255, 255, 0.06); border-radius: 15px; cursor: grab; overflow: hidden;
367
+ filter: saturate(calc(1 - min(var(--d, 0), 2) * 0.15));
368
+ box-shadow:
369
+ inset 0 1px 0 rgba(255, 255, 255, 0.05),
370
+ inset 0 0 0 1px hsl(var(--h) 40% 40% / 0.07),
371
+ 0 1px 2px rgba(0, 0, 0, 0.5),
372
+ 0 16px 32px -18px hsl(var(--h) 55% 40% / 0.28);
373
+ transition: border-color 0.15s, transform 0.15s, box-shadow 0.2s, opacity 0.15s, filter 0.2s;
374
+ animation: settle 0.4s ease both;
375
+ }
376
+ .ag::before { content: ""; position: absolute; top: 0; left: 0; right: 0;
377
+ height: calc(4px - min(var(--d, 0), 2) * 1px); /* lanyard thins with rank */
378
+ background: linear-gradient(90deg, hsl(var(--h) 55% 55% / 0.9), hsl(var(--h) 55% 45% / 0.25)); }
379
+ @keyframes settle { from { opacity: 0; transform: translateY(6px); } to { opacity: 1; transform: none; } }
380
+ .ag:hover { border-color: hsl(var(--h) 45% 50% / 0.6); transform: translateY(-2px); filter: saturate(1);
381
+ box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.08), 0 2px 4px rgba(0, 0, 0, 0.5), 0 24px 44px -18px hsl(var(--h) 55% 45% / 0.4); }
382
+ .ag.sel { border-color: hsl(var(--h) 55% 55%); filter: saturate(1);
383
+ box-shadow: 0 0 0 2px hsl(var(--h) 55% 55% / 0.18), 0 10px 28px hsl(var(--h) 50% 45% / 0.2); }
384
+ .ag:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
385
+ .ag.over { border-color: var(--accent-2); box-shadow: 0 0 0 3px var(--accent-dim), 0 12px 30px rgba(79, 184, 160, 0.2); transform: scale(1.03); }
386
+ .ag.dragging { opacity: 0.45; cursor: grabbing; }
387
+
388
+ .ag-light { position: absolute; top: 10px; right: 10px; width: 7px; height: 7px; border-radius: 50%; }
389
+ .ag-light.ok { background: var(--accent); box-shadow: 0 0 6px rgba(79, 184, 160, 0.7); }
390
+ .ag-light.warn { background: var(--warn); box-shadow: 0 0 6px rgba(224, 180, 95, 0.7); }
391
+ .ag-light.fail { background: #e07b6a; box-shadow: 0 0 8px rgba(224, 123, 106, 0.9); animation: pulse 1.4s ease-in-out infinite; }
392
+
393
+ .ag.root { cursor: default; width: 200px; padding: 16px 13px 11px;
394
+ border-color: rgba(79, 184, 160, 0.35); filter: none;
395
+ box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.07), 0 0 0 1px rgba(79, 184, 160, 0.25), 0 24px 60px -20px rgba(79, 184, 160, 0.35); }
396
+ .ag.root .ag-ava { width: 46px; height: 46px; font-size: 23px; }
397
+ .ag.root .ag-name { font-size: 15px; font-weight: 700; letter-spacing: -0.01em; }
398
+ .ag.root::after {
399
+ content: ""; position: absolute; inset: 0; border-radius: 15px; pointer-events: none;
400
+ box-shadow: inset 0 0 26px rgba(79, 184, 160, 0.08); animation: ring 3.2s ease-in-out infinite;
401
+ }
402
+ @keyframes ring { 0%, 100% { opacity: 0.35; } 50% { opacity: 1; } }
403
+
404
+ .ag-ava {
405
+ width: 38px; height: 38px; display: grid; place-items: center; font-size: 19px; margin-top: 2px;
406
+ border-radius: 13px; background: linear-gradient(140deg, hsl(var(--h) 42% 26% / 0.85), hsl(var(--h) 45% 14% / 0.9));
407
+ border: 1px solid hsl(var(--h) 45% 45% / 0.35);
408
+ box-shadow: inset 0 1px 0 hsl(var(--h) 50% 60% / 0.25), 0 4px 10px -4px hsl(var(--h) 50% 30% / 0.6);
409
+ }
410
+ .ag-name { font-size: 13px; font-weight: 650; letter-spacing: -0.01em; white-space: nowrap; max-width: 100%; overflow: hidden; text-overflow: ellipsis; }
411
+ .ag-role { font-size: 11px; color: var(--ink-2); line-height: 1.35;
412
+ display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; }
413
+ .ag-meta { display: flex; align-items: center; justify-content: center; gap: 8px; margin-top: 2px; }
414
+ .ag-meta em { font-style: normal; font-size: 10.5px; color: var(--ink-3); font-variant-numeric: tabular-nums; white-space: nowrap; }
415
+ .ag-kids {
416
+ font-size: 10px; background: var(--accent-dim); color: var(--accent); border-radius: 99px;
417
+ padding: 2px 8px; font-variant-numeric: tabular-nums; font-weight: 650; text-transform: uppercase; letter-spacing: 0.06em;
418
+ }
419
+
420
+ /* the dossier */
421
+ .doss {
422
+ width: 340px; flex: 0 0 340px; border-left: 1px solid var(--line);
423
+ background: rgba(16, 19, 26, 0.75); backdrop-filter: blur(10px);
424
+ padding: 26px 24px 40px; overflow-y: auto; animation: slidein 0.25s ease both;
425
+ }
426
+ @keyframes slidein { from { opacity: 0; transform: translateX(14px); } to { opacity: 1; transform: none; } }
427
+ .doss-head { display: flex; gap: 12px; align-items: center; margin-bottom: 14px; }
428
+ .doss-emoji { font-size: 30px; }
429
+ .doss h2 { margin: 0; font-size: 18px; }
430
+ .doss-role { margin: 3px 0 0; color: var(--ink-2); font-size: 12.5px; }
431
+
432
+ .doss-chain { font-size: 11px; color: var(--ink-3); margin-bottom: 18px; line-height: 1.7; }
433
+ .doss-chain i { font-style: normal; margin: 0 5px; }
434
+ .doss-chain b { font-weight: 500; }
435
+ .doss-chain b.me { color: var(--accent); font-weight: 650; }
436
+
437
+ .doss-stats { display: grid; grid-template-columns: repeat(3, 1fr); gap: 8px; margin-bottom: 22px; }
438
+ .doss-stat { background: var(--bg-1); border: 1px solid var(--line); border-radius: 11px; padding: 11px 8px; text-align: center; }
439
+ .doss-n { font-size: 19px; font-weight: 700; font-variant-numeric: tabular-nums; }
440
+ .doss-l { font-size: 9.5px; color: var(--ink-3); margin-top: 3px; text-transform: uppercase; letter-spacing: 0.05em; }
441
+
442
+ .doss section { margin-bottom: 20px; }
443
+ .doss h3 { font-size: 10px; text-transform: uppercase; letter-spacing: 0.09em; color: var(--ink-3); margin: 0 0 9px; }
444
+ .doss-empty { color: var(--ink-3); font-size: 12px; margin: 0; }
445
+ .doss-rules { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 5px; }
446
+ .doss-rules li { font-family: var(--mono); font-size: 11px; color: var(--ink-2); background: var(--bg-1); border: 1px solid var(--line); border-radius: 7px; padding: 6px 9px; word-break: break-all; }
447
+ .doss-rules li.own { color: var(--ink); border-color: rgba(79, 184, 160, 0.35); }
448
+ .doss-rules em { color: var(--ink-3); font-style: normal; font-size: 9.5px; margin-left: 5px; }
449
+ .doss-chips { display: flex; gap: 6px; flex-wrap: wrap; }
450
+ .doss-reps { display: flex; flex-direction: column; gap: 7px; }
451
+ .doss-rep {
452
+ background: var(--bg-1); border: 1px solid var(--line); border-left: 2px solid var(--accent);
453
+ border-radius: 9px; padding: 9px 11px; animation: settle 0.35s ease both;
454
+ }
455
+ .doss-rep-top { display: flex; align-items: baseline; gap: 9px; }
456
+ .doss-rep-when { font-size: 10px; color: var(--accent-2); font-variant-numeric: tabular-nums; white-space: nowrap; }
457
+ .doss-rep-label { font-size: 12px; font-weight: 600; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
458
+ .doss-rep-sum { margin: 5px 0 0; font-size: 11.5px; color: var(--ink-2); line-height: 1.55; }
459
+ .doss-rep.tap { cursor: pointer; transition: border-color .12s ease, background .12s ease; }
460
+ .doss-rep.tap:hover { border-left-color: var(--warn); background: rgba(255, 255, 255, 0.03); }
461
+ .doss-rep.tap:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }
462
+ .doss-rep-caret { margin-left: auto; font-size: 10px; color: var(--ink-2); align-self: center; }
463
+ .rep-dot { width: 7px; height: 7px; border-radius: 50%; flex: 0 0 auto; align-self: center; }
464
+ .rep-dot.ok { background: var(--accent); } .rep-dot.warn { background: var(--warn); } .rep-dot.fail { background: #e07b6a; }
465
+ .rep-ok { color: var(--accent); } .rep-warn { color: var(--warn); } .rep-fail { color: #e07b6a; }
466
+ .doss-rep-detail { margin-top: 8px; padding-top: 8px; border-top: 1px solid var(--line); font-size: 11px; line-height: 1.7; color: var(--ink-2); font-family: var(--mono); word-break: break-word; }
467
+ .doss-rep-detail .doss-l { display: inline-block; min-width: 74px; opacity: .6; text-transform: uppercase; letter-spacing: .04em; font-size: 9.5px; }
468
+ .chip { font-size: 11px; background: var(--accent-dim); color: var(--accent-2); border-radius: 7px; padding: 4px 10px; }
469
+ .chip.alt { background: var(--bg-2); color: var(--ink-2); }
470
+ .doss-boot { font-size: 12px; color: var(--ink-2); line-height: 1.6; margin: 0; white-space: pre-wrap; }
471
+ /* contract — one click to open, edit in place, save flows to the org file */
472
+ .doss-contract h3 { display: flex; align-items: baseline; gap: 8px; flex-wrap: wrap; }
473
+ .contract-card {
474
+ position: relative; display: block; width: 100%; text-align: left; cursor: pointer;
475
+ background: var(--bg-1); border: 1px solid var(--line); border-radius: 12px;
476
+ padding: 13px 14px; transition: border-color 0.15s, box-shadow 0.15s, transform 0.1s;
477
+ }
478
+ .contract-card:hover { border-color: var(--accent); box-shadow: 0 0 0 3px var(--accent-dim); }
479
+ .contract-card:hover .contract-hint { opacity: 1; }
480
+ .contract-card.empty { border-style: dashed; }
481
+ .contract-hint {
482
+ position: absolute; top: 9px; right: 11px; font-size: 10.5px; font-weight: 650;
483
+ color: var(--accent-2); opacity: 0.55; transition: opacity 0.15s; letter-spacing: 0.02em;
484
+ }
485
+ .contract-edit { display: flex; flex-direction: column; gap: 8px; }
486
+ .contract-ta {
487
+ width: 100%; min-height: 220px; resize: vertical; box-sizing: border-box;
488
+ background: var(--bg-1); border: 1px solid var(--accent); border-radius: 12px;
489
+ padding: 12px 13px; color: var(--ink); font-family: var(--mono); font-size: 12px; line-height: 1.6;
490
+ outline: none; box-shadow: 0 0 0 3px var(--accent-dim);
491
+ }
492
+ .doss-edit-hint { font-size: 11px; color: var(--ink-3); margin: 2px 0 0; font-style: italic; }
493
+ .doss-note { color: var(--ink-3); font-size: 11px; line-height: 1.6; }
494
+ .doss-note code { font-family: var(--mono); background: var(--bg-2); padding: 1px 5px; border-radius: 4px; }
495
+
496
+ .pnl-fatal { min-height: 100vh; display: grid; place-items: center; color: #e07b6a; font-size: 14px; padding: 30px; text-align: center; }
497
+ .pnl-fatal.calm { color: var(--ink-2); }
498
+
499
+ @media (max-width: 760px) {
500
+ .doss { position: fixed; right: 0; top: 0; bottom: 0; z-index: 5; width: min(340px, 88vw); box-shadow: -18px 0 50px rgba(0, 0, 0, 0.5); }
501
+ .tree { padding: 20px 14px 60px; }
502
+ .ag { max-width: 100%; }
503
+ .ag-role { display: none; }
504
+ }
505
+ `;
506
+
507
+ // src/Panel.tsx
508
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
509
+ var defaultApi = (path, init) => fetch(`/api${path}`, init).then((r) => r.ok ? r.json() : r.json().then((b) => Promise.reject(b)));
510
+ var ApiCtx = createContext(defaultApi);
511
+ var useApi = () => useContext(ApiCtx);
512
+ var TABS = [
513
+ { id: "org", glyph: "\u2302", label: "organigram" },
514
+ { id: "buckets", glyph: "\u25A4", label: "buckets" },
515
+ { id: "reports", glyph: "\u23F1", label: "reports" },
516
+ { id: "rules", glyph: "\xA7", label: "rules" },
517
+ { id: "graph", glyph: "\u25C9", label: "graph" }
518
+ ];
519
+ function relTime(iso) {
520
+ if (typeof iso !== "string") return "";
521
+ const t = new Date(iso).getTime();
522
+ if (!t) return "";
523
+ const s = (Date.now() - t) / 1e3;
524
+ if (s < 60) return "just now";
525
+ if (s < 3600) return `${Math.floor(s / 60)}m ago`;
526
+ if (s < 86400) return `${Math.floor(s / 3600)}h ago`;
527
+ if (s < 172800) return "yesterday";
528
+ return `${Math.floor(s / 86400)}d ago`;
529
+ }
530
+ function nodeAt(n) {
531
+ const d = n.data ?? {};
532
+ for (const k of ["at", "ts", "time", "created_at", "date", "bst", "ts_bst", "finished_at"]) {
533
+ const v = d[k];
534
+ if (typeof v === "string" && v) return v;
535
+ }
536
+ return "";
537
+ }
538
+ function timeMs(at) {
539
+ if (!at) return NaN;
540
+ const t = Date.parse(at);
541
+ return Number.isFinite(t) ? t : Number(at);
542
+ }
543
+ function buildHealthMap(nodes) {
544
+ const m = /* @__PURE__ */ new Map();
545
+ for (const r of nodes) {
546
+ if (!r.cluster) continue;
547
+ const d = r.data ?? {};
548
+ if (typeof d.status !== "string") continue;
549
+ const status = d.status;
550
+ const at = nodeAt(r);
551
+ const atMs = timeMs(at);
552
+ const cur = m.get(r.cluster) ?? { lastAt: "", lastMs: -Infinity, lastStatus: "", n: 0, fails: 0 };
553
+ cur.n++;
554
+ if (status === "fail") cur.fails++;
555
+ if (Number.isFinite(atMs) && atMs >= cur.lastMs) {
556
+ cur.lastMs = atMs;
557
+ cur.lastAt = at;
558
+ cur.lastStatus = status;
559
+ }
560
+ m.set(r.cluster, cur);
561
+ }
562
+ return m;
563
+ }
564
+ function pulseFor(a, health) {
565
+ if (!health) return null;
566
+ const keys = [a.id, ...a.buckets ?? []];
567
+ let best = null;
568
+ for (const k of keys) {
569
+ const p = health.get(k);
570
+ if (p && (!best || p.lastMs > best.lastMs)) best = p;
571
+ }
572
+ return best;
573
+ }
574
+ function lightFor(a, health) {
575
+ const p = pulseFor(a, health);
576
+ if (!p || !p.lastAt) return "none";
577
+ if (p.lastStatus === "fail") return "fail";
578
+ const ageH = (Date.now() - p.lastMs) / 36e5;
579
+ const cadence = typeof a.cadence === "number" && a.cadence > 0 ? a.cadence : 26;
580
+ if (ageH > cadence * 2) return "warn";
581
+ if (p.lastStatus === "warn") return "warn";
582
+ return "ok";
583
+ }
584
+ function unstableFor(a, health) {
585
+ return lightFor(a, health) === "ok" && (pulseFor(a, health)?.fails ?? 0) > 0;
586
+ }
587
+ var worst = (ls) => ls.includes("fail") ? "fail" : ls.includes("warn") ? "warn" : ls.includes("ok") ? "ok" : "none";
588
+ function nodeSummary(n) {
589
+ const d = n.data ?? {};
590
+ for (const k of ["summary", "title", "detail"]) {
591
+ const v = d[k];
592
+ if (typeof v === "string" && v && v !== n.label) return v;
593
+ }
594
+ return "";
595
+ }
596
+ async function fetchReports(api, cluster, limit = 500) {
597
+ const q = async (t) => {
598
+ try {
599
+ const base = `/nodes?type=${t}${cluster ? `&cluster=${encodeURIComponent(cluster)}` : ""}`;
600
+ const first = await api(`${base}&limit=${limit}`);
601
+ const nodes2 = first.nodes ?? [];
602
+ const want = Math.min(first.total ?? 0, limit);
603
+ while (nodes2.length < want) {
604
+ const page = await api(`${base}&limit=${limit}&offset=${nodes2.length}`);
605
+ if (!page.nodes?.length) break;
606
+ nodes2.push(...page.nodes);
607
+ }
608
+ return { total: first.total ?? 0, nodes: nodes2 };
609
+ } catch {
610
+ return { total: 0, nodes: [] };
611
+ }
612
+ };
613
+ const [r, d] = await Promise.all([q("report"), q("decision")]);
614
+ const nodes = [...r.nodes ?? [], ...d.nodes ?? []].sort((a, b) => nodeAt(b).localeCompare(nodeAt(a)));
615
+ return { total: (r.total ?? 0) + (d.total ?? 0), nodes };
616
+ }
617
+ function slugify(s) {
618
+ return s.toLowerCase().trim().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
619
+ }
620
+ var REDUCED = typeof window !== "undefined" && window.matchMedia?.("(prefers-reduced-motion: reduce)").matches;
621
+ function readTheme() {
622
+ try {
623
+ return localStorage.getItem("booboo-theme") === "light" ? "light" : "dark";
624
+ } catch {
625
+ return "dark";
626
+ }
627
+ }
628
+ function useTheme() {
629
+ const [theme, setTheme] = useState(readTheme);
630
+ useEffect(() => {
631
+ document.documentElement.setAttribute("data-theme", theme);
632
+ try {
633
+ localStorage.setItem("booboo-theme", theme);
634
+ } catch {
635
+ }
636
+ }, [theme]);
637
+ return [theme, () => setTheme((t) => t === "dark" ? "light" : "dark")];
638
+ }
639
+ function useCountUp(target, ms = 900) {
640
+ const [v, setV] = useState(REDUCED ? target : 0);
641
+ const fromRef = useRef(0);
642
+ useEffect(() => {
643
+ if (REDUCED) {
644
+ setV(target);
645
+ return;
646
+ }
647
+ const from = fromRef.current;
648
+ const t0 = performance.now();
649
+ let raf = 0;
650
+ const tick = (t) => {
651
+ const k = Math.min(1, (t - t0) / ms);
652
+ const eased = 1 - Math.pow(1 - k, 3);
653
+ setV(Math.round(from + (target - from) * eased));
654
+ if (k < 1) raf = requestAnimationFrame(tick);
655
+ else fromRef.current = target;
656
+ };
657
+ raf = requestAnimationFrame(tick);
658
+ return () => cancelAnimationFrame(raf);
659
+ }, [target, ms]);
660
+ return v;
661
+ }
662
+ function useRoute() {
663
+ const parse = () => {
664
+ const h = window.location.hash.replace(/^#\/?/, "");
665
+ const [tab, ...rest] = h.split("/");
666
+ const known = TABS.some((t) => t.id === tab) ? tab : "org";
667
+ return [known, rest.length ? decodeURIComponent(rest.join("/")) : null];
668
+ };
669
+ const [route, setRoute] = useState(parse);
670
+ useEffect(() => {
671
+ const onHash = () => setRoute(parse());
672
+ window.addEventListener("hashchange", onHash);
673
+ return () => window.removeEventListener("hashchange", onHash);
674
+ }, []);
675
+ return route;
676
+ }
677
+ var nav = (path) => {
678
+ window.location.hash = path;
679
+ };
680
+ function bucketHue(name) {
681
+ let h = 0;
682
+ for (const c of name) h = (h * 31 + c.charCodeAt(0)) % 360;
683
+ return h;
684
+ }
685
+ function AgentCard({
686
+ a,
687
+ isRoot,
688
+ depth,
689
+ order,
690
+ selected,
691
+ dragId,
692
+ onSelect,
693
+ onDragStart,
694
+ onDropOn,
695
+ childCount,
696
+ light = "none"
697
+ }) {
698
+ const [over, setOver] = useState(false);
699
+ const nBuckets = a.buckets?.length ?? 0;
700
+ const nSkills = a.skills?.length ?? 0;
701
+ return /* @__PURE__ */ jsxs(
702
+ "div",
703
+ {
704
+ className: `ag${isRoot ? " root" : ""}${selected ? " sel" : ""}${over ? " over" : ""}${dragId === a.id ? " dragging" : ""}`,
705
+ style: { ["--h"]: bucketHue(a.id), ["--d"]: depth, animationDelay: `${Math.min(depth * 70 + order * 45, 600)}ms` },
706
+ draggable: !isRoot,
707
+ tabIndex: 0,
708
+ onKeyDown: (e) => {
709
+ if (e.key === "Enter" || e.key === " ") {
710
+ e.preventDefault();
711
+ onSelect(a.id);
712
+ }
713
+ },
714
+ onClick: (e) => {
715
+ e.stopPropagation();
716
+ onSelect(a.id);
717
+ },
718
+ onDragStart: (e) => {
719
+ e.dataTransfer.effectAllowed = "move";
720
+ onDragStart(a.id);
721
+ },
722
+ onDragOver: (e) => {
723
+ if (dragId && dragId !== a.id) {
724
+ e.preventDefault();
725
+ setOver(true);
726
+ }
727
+ },
728
+ onDragLeave: () => setOver(false),
729
+ onDrop: (e) => {
730
+ e.preventDefault();
731
+ setOver(false);
732
+ onDropOn(a.id);
733
+ },
734
+ children: [
735
+ light !== "none" && /* @__PURE__ */ jsx("i", { className: `ag-light ${light}`, title: `fleet health: ${light}` }),
736
+ /* @__PURE__ */ jsx("span", { className: "ag-ava", children: a.emoji || "\u{1F916}" }),
737
+ /* @__PURE__ */ jsx("span", { className: "ag-name", children: a.name }),
738
+ /* @__PURE__ */ jsx("span", { className: "ag-role", children: a.role || " " }),
739
+ /* @__PURE__ */ jsxs("span", { className: "ag-meta", children: [
740
+ nBuckets > 0 && /* @__PURE__ */ jsxs("em", { title: "memory buckets", children: [
741
+ "\u25A4 ",
742
+ nBuckets
743
+ ] }),
744
+ nSkills > 0 && /* @__PURE__ */ jsxs("em", { title: "skills", children: [
745
+ "\u2726 ",
746
+ nSkills
747
+ ] }),
748
+ childCount > 0 && /* @__PURE__ */ jsxs("span", { className: "ag-kids", title: "direct reports", children: [
749
+ childCount,
750
+ " reports"
751
+ ] })
752
+ ] })
753
+ ]
754
+ }
755
+ );
756
+ }
757
+ function ChartNode({
758
+ org,
759
+ a,
760
+ depth,
761
+ order,
762
+ ...cardProps
763
+ }) {
764
+ const kids = org.agents.filter((c) => c.parent === a.id && c.kind !== "automation");
765
+ const machines = (() => {
766
+ const out = [];
767
+ const walk = (pid) => {
768
+ for (const c of org.agents.filter((x) => x.parent === pid && x.kind === "automation")) {
769
+ const hasKids = org.agents.some((x) => x.parent === c.id);
770
+ if (hasKids) walk(c.id);
771
+ else out.push(c);
772
+ }
773
+ };
774
+ walk(a.id);
775
+ return out;
776
+ })();
777
+ const lights = machines.map((m) => lightFor(m, cardProps.health));
778
+ const trayLight = worst(lights);
779
+ const TRAY_MAX = 8;
780
+ const hidden = machines.length - TRAY_MAX;
781
+ const hiddenBad = machines.slice(TRAY_MAX).reduce(
782
+ (n, m) => n + (lightFor(m, cardProps.health) !== "ok" && lightFor(m, cardProps.health) !== "none" ? 1 : 0),
783
+ 0
784
+ );
785
+ return /* @__PURE__ */ jsxs("div", { className: "ocn", children: [
786
+ /* @__PURE__ */ jsx(
787
+ AgentCard,
788
+ {
789
+ a,
790
+ isRoot: a.id === org.root,
791
+ depth,
792
+ order,
793
+ selected: cardProps.selected === a.id,
794
+ dragId: cardProps.dragId,
795
+ onSelect: cardProps.onSelect,
796
+ onDragStart: cardProps.onDragStart,
797
+ onDropOn: cardProps.onDropOn,
798
+ childCount: kids.length,
799
+ light: trayLight
800
+ }
801
+ ),
802
+ machines.length > 0 && /* @__PURE__ */ jsxs("div", { className: `oc-tray ${trayLight}`, children: [
803
+ machines.slice(0, TRAY_MAX).map((m) => /* @__PURE__ */ jsxs(
804
+ "button",
805
+ {
806
+ type: "button",
807
+ className: `oc-mac${cardProps.selected === m.id ? " sel" : ""}`,
808
+ title: `${m.name}${m.role ? ` \u2014 ${m.role}` : ""}`,
809
+ onClick: (e) => {
810
+ e.stopPropagation();
811
+ cardProps.onSelect(m.id);
812
+ },
813
+ children: [
814
+ /* @__PURE__ */ jsx("i", { className: `mac-dot ${lightFor(m, cardProps.health)}${unstableFor(m, cardProps.health) ? " unstable" : ""}` }),
815
+ /* @__PURE__ */ jsx("span", { className: "mac-emoji", children: m.emoji || "\u2699\uFE0F" }),
816
+ /* @__PURE__ */ jsx("span", { className: "mac-name", children: m.name })
817
+ ]
818
+ },
819
+ m.id
820
+ )),
821
+ hidden > 0 && /* @__PURE__ */ jsxs(
822
+ "button",
823
+ {
824
+ type: "button",
825
+ className: "oc-mac more",
826
+ title: "open the full machine list in the dossier",
827
+ onClick: (e) => {
828
+ e.stopPropagation();
829
+ cardProps.onSelect(a.id);
830
+ },
831
+ children: [
832
+ "+",
833
+ hidden,
834
+ " machines",
835
+ hiddenBad > 0 ? ` \xB7 ${hiddenBad} not green` : ""
836
+ ]
837
+ }
838
+ )
839
+ ] }),
840
+ kids.length > 0 && /* @__PURE__ */ jsxs(Fragment, { children: [
841
+ /* @__PURE__ */ jsx("div", { className: "oc-down" }),
842
+ /* @__PURE__ */ jsx("div", { className: `oc-row${kids.length > 3 ? " wrap" : ""}`, children: kids.map((k, i) => /* @__PURE__ */ jsx("div", { className: "oc-child", style: { ["--h"]: bucketHue(k.id) }, children: /* @__PURE__ */ jsx(ChartNode, { org, a: k, depth: depth + 1, order: i, ...cardProps }) }, k.id)) })
843
+ ] })
844
+ ] });
845
+ }
846
+ function Chip({ children, tone, onClick }) {
847
+ return /* @__PURE__ */ jsx("span", { className: `chip${tone ? ` ${tone}` : ""}${onClick ? " tap" : ""}`, onClick, children });
848
+ }
849
+ function Dossier({
850
+ org,
851
+ id,
852
+ hasSnapshot,
853
+ onUpdate,
854
+ onAdd,
855
+ onRemove,
856
+ onSelect,
857
+ health = null
858
+ }) {
859
+ const api = useApi();
860
+ const slice = useMemo(() => orgBootSlice(org, id), [org, id]);
861
+ const autos = useMemo(() => {
862
+ const out = [];
863
+ const walk = (pid) => {
864
+ for (const c of org.agents.filter((x) => x.parent === pid && x.kind === "automation")) {
865
+ const hasKids = org.agents.some((x) => x.parent === c.id);
866
+ if (hasKids) walk(c.id);
867
+ else out.push(c);
868
+ }
869
+ };
870
+ walk(id);
871
+ return out;
872
+ }, [org, id]);
873
+ const [memCount, setMemCount] = useState(null);
874
+ const [repCount, setRepCount] = useState(null);
875
+ const [reports, setReports] = useState(null);
876
+ const [edit, setEdit] = useState(false);
877
+ const [form, setForm] = useState({});
878
+ const [openRep, setOpenRep] = useState(null);
879
+ const [editContract, setEditContract] = useState(false);
880
+ const [contractText, setContractText] = useState("");
881
+ useEffect(() => {
882
+ setEdit(false);
883
+ setOpenRep(null);
884
+ setEditContract(false);
885
+ }, [id]);
886
+ useEffect(() => {
887
+ setMemCount(null);
888
+ setRepCount(null);
889
+ setReports(null);
890
+ if (!hasSnapshot || !slice) return;
891
+ Promise.all(
892
+ slice.buckets.map(
893
+ (b) => api(`/nodes?type=memory&cluster=${encodeURIComponent(b)}&limit=1`).then((j) => j.total).catch(() => 0)
894
+ )
895
+ ).then((counts) => setMemCount(counts.reduce((s, n) => s + n, 0)));
896
+ fetchReports(api, id, 100).then(({ total, nodes }) => {
897
+ setRepCount(total);
898
+ setReports(nodes.slice(0, 4));
899
+ });
900
+ }, [id, hasSnapshot, slice, api]);
901
+ const mem = useCountUp(memCount ?? 0);
902
+ const rep = useCountUp(repCount ?? 0);
903
+ const startEdit = () => {
904
+ if (!slice) return;
905
+ const ag = slice.agent;
906
+ setForm({
907
+ name: ag.name ?? "",
908
+ emoji: ag.emoji ?? "",
909
+ role: ag.role ?? "",
910
+ boot: ag.boot ?? "",
911
+ rules: (ag.rules ?? []).join(", "),
912
+ skills: (ag.skills ?? []).join(", "),
913
+ buckets: (ag.buckets ?? []).join(", ")
914
+ });
915
+ setEdit(true);
916
+ };
917
+ const saveEdit = () => {
918
+ const list = (s) => s.split(",").map((x) => x.trim()).filter(Boolean);
919
+ onUpdate(id, {
920
+ name: form.name.trim() || slice?.agent.name || id,
921
+ emoji: form.emoji.trim() || void 0,
922
+ role: form.role.trim() || void 0,
923
+ boot: form.boot.trim() || void 0,
924
+ rules: list(form.rules),
925
+ skills: list(form.skills),
926
+ buckets: list(form.buckets)
927
+ });
928
+ setEdit(false);
929
+ };
930
+ if (!slice) return null;
931
+ const a = slice.agent;
932
+ const isRoot = a.id === org.root;
933
+ const own = new Set(a.rules ?? []);
934
+ return /* @__PURE__ */ jsxs("aside", { className: "doss", onClick: (e) => e.stopPropagation(), children: [
935
+ /* @__PURE__ */ jsxs("div", { className: "doss-head", children: [
936
+ /* @__PURE__ */ jsx("span", { className: "doss-emoji", children: a.emoji || "\u{1F916}" }),
937
+ /* @__PURE__ */ jsxs("div", { children: [
938
+ /* @__PURE__ */ jsxs("h2", { children: [
939
+ a.name,
940
+ " ",
941
+ a.kind === "automation" && /* @__PURE__ */ jsx("span", { className: "auto-badge", children: "automation" })
942
+ ] }),
943
+ a.role && /* @__PURE__ */ jsx("p", { className: "doss-role", children: a.role })
944
+ ] }),
945
+ /* @__PURE__ */ jsxs("div", { className: "doss-head-actions", children: [
946
+ !edit && /* @__PURE__ */ jsx("button", { className: "doss-3d", title: "edit this agent", onClick: startEdit, children: "\u270E edit" }),
947
+ hasSnapshot && /* @__PURE__ */ jsx("button", { className: "doss-3d", title: "see the whole brain in 3D", onClick: () => nav("/graph"), children: "\u25C9 3D" })
948
+ ] })
949
+ ] }),
950
+ edit && /* @__PURE__ */ jsxs("div", { className: "doss-edit", children: [
951
+ /* @__PURE__ */ jsxs("div", { className: "doss-edit-row2", children: [
952
+ /* @__PURE__ */ jsxs("label", { children: [
953
+ "name",
954
+ /* @__PURE__ */ jsx("input", { value: form.name, onChange: (e) => setForm({ ...form, name: e.target.value }) })
955
+ ] }),
956
+ /* @__PURE__ */ jsxs("label", { className: "narrow", children: [
957
+ "emoji",
958
+ /* @__PURE__ */ jsx("input", { value: form.emoji, onChange: (e) => setForm({ ...form, emoji: e.target.value }) })
959
+ ] })
960
+ ] }),
961
+ /* @__PURE__ */ jsxs("label", { children: [
962
+ "role",
963
+ /* @__PURE__ */ jsx("input", { value: form.role, onChange: (e) => setForm({ ...form, role: e.target.value }), placeholder: "one line \u2014 what this agent is for" })
964
+ ] }),
965
+ /* @__PURE__ */ jsxs("label", { children: [
966
+ "rules ",
967
+ /* @__PURE__ */ jsx("em", { children: "comma-separated refs \xB7 inherited by everyone beneath" }),
968
+ /* @__PURE__ */ jsx("input", { value: form.rules, onChange: (e) => setForm({ ...form, rules: e.target.value }), placeholder: "rules/GLOBAL.md, rules/CONTENT.md" })
969
+ ] }),
970
+ /* @__PURE__ */ jsxs("label", { children: [
971
+ "memory buckets ",
972
+ /* @__PURE__ */ jsx("em", { children: "comma-separated" }),
973
+ /* @__PURE__ */ jsx("input", { value: form.buckets, onChange: (e) => setForm({ ...form, buckets: e.target.value }), placeholder: "shared, content" })
974
+ ] }),
975
+ /* @__PURE__ */ jsxs("label", { children: [
976
+ "skills ",
977
+ /* @__PURE__ */ jsx("em", { children: "comma-separated" }),
978
+ /* @__PURE__ */ jsx("input", { value: form.skills, onChange: (e) => setForm({ ...form, skills: e.target.value }), placeholder: "humanizer, deep-research" })
979
+ ] }),
980
+ /* @__PURE__ */ jsx("p", { className: "doss-edit-hint", children: "\u2193 edit the contract below \u2014 the agent's operating prompt lives in its own section." }),
981
+ /* @__PURE__ */ jsxs("div", { className: "doss-edit-actions", children: [
982
+ /* @__PURE__ */ jsx("button", { className: "btn primary", onClick: saveEdit, children: "save to draft" }),
983
+ /* @__PURE__ */ jsx("button", { className: "btn ghost", onClick: () => setEdit(false), children: "cancel" })
984
+ ] })
985
+ ] }),
986
+ a.kind === "automation" && (() => {
987
+ const p = pulseFor(a, health);
988
+ const l = lightFor(a, health);
989
+ return /* @__PURE__ */ jsxs("div", { className: `doss-health ${l}`, children: [
990
+ /* @__PURE__ */ jsx("i", { className: `mac-dot ${l}` }),
991
+ /* @__PURE__ */ jsx("b", { children: l === "ok" ? "healthy" : l === "fail" ? "last run failed" : l === "warn" ? "stale or degraded" : "no runs recorded" }),
992
+ p?.lastAt && /* @__PURE__ */ jsxs("span", { children: [
993
+ "last run ",
994
+ relTime(p.lastAt)
995
+ ] }),
996
+ p && p.n > 0 && /* @__PURE__ */ jsxs("span", { children: [
997
+ Math.round((p.n - p.fails) / p.n * 100),
998
+ "% ok of ",
999
+ p.n
1000
+ ] }),
1001
+ typeof a.cadence === "number" && /* @__PURE__ */ jsxs("span", { children: [
1002
+ "expected every ",
1003
+ a.cadence,
1004
+ "h"
1005
+ ] })
1006
+ ] });
1007
+ })(),
1008
+ /* @__PURE__ */ jsx("div", { className: "doss-chain", children: slice.chain.map((c, i) => /* @__PURE__ */ jsxs("span", { children: [
1009
+ i > 0 && /* @__PURE__ */ jsx("i", { children: "\u203A" }),
1010
+ /* @__PURE__ */ jsx("b", { className: c.id === a.id ? "me" : "", children: c.name })
1011
+ ] }, c.id)) }),
1012
+ hasSnapshot && /* @__PURE__ */ jsxs("div", { className: "doss-stats", children: [
1013
+ /* @__PURE__ */ jsxs("div", { className: "doss-stat", children: [
1014
+ /* @__PURE__ */ jsx("div", { className: "doss-n", children: memCount === null ? "\u2026" : mem.toLocaleString() }),
1015
+ /* @__PURE__ */ jsx("div", { className: "doss-l", children: "memories in reach" })
1016
+ ] }),
1017
+ /* @__PURE__ */ jsxs("div", { className: "doss-stat", children: [
1018
+ /* @__PURE__ */ jsx("div", { className: "doss-n", children: repCount === null ? "\u2026" : rep.toLocaleString() }),
1019
+ /* @__PURE__ */ jsx("div", { className: "doss-l", children: "reports filed" })
1020
+ ] }),
1021
+ /* @__PURE__ */ jsxs("div", { className: "doss-stat", children: [
1022
+ /* @__PURE__ */ jsx("div", { className: "doss-n", children: slice.children.filter((c) => c.kind !== "automation").length }),
1023
+ /* @__PURE__ */ jsx("div", { className: "doss-l", children: "direct reports" })
1024
+ ] })
1025
+ ] }),
1026
+ hasSnapshot && /* @__PURE__ */ jsxs("section", { children: [
1027
+ /* @__PURE__ */ jsx("h3", { children: "latest reports" }),
1028
+ reports === null ? /* @__PURE__ */ jsx("p", { className: "doss-empty", children: "loading\u2026" }) : reports.length === 0 ? /* @__PURE__ */ jsx("p", { className: "doss-empty", children: "no reports filed yet \u2014 they appear here as this agent closes work." }) : /* @__PURE__ */ jsx("div", { className: "doss-reps", children: reports.map((r) => {
1029
+ const when = relTime(nodeAt(r));
1030
+ const sum = nodeSummary(r);
1031
+ const st = typeof r.data?.status === "string" ? String(r.data.status) : void 0;
1032
+ const open = openRep === r.id;
1033
+ const skip = /* @__PURE__ */ new Set(["summary", "status", "at", "ts", "time", "created_at", "date", "bst", "ts_bst", "finished_at"]);
1034
+ const extra = r.data ? Object.entries(r.data).filter(([k, v]) => !skip.has(k) && v != null && v !== "") : [];
1035
+ return /* @__PURE__ */ jsxs(
1036
+ "div",
1037
+ {
1038
+ className: `doss-rep tap${open ? " open" : ""}`,
1039
+ role: "button",
1040
+ tabIndex: 0,
1041
+ "aria-expanded": open,
1042
+ title: "click for the full report",
1043
+ onClick: () => setOpenRep(open ? null : r.id),
1044
+ onKeyDown: (e) => {
1045
+ if (e.key === "Enter" || e.key === " ") {
1046
+ e.preventDefault();
1047
+ setOpenRep(open ? null : r.id);
1048
+ }
1049
+ },
1050
+ children: [
1051
+ /* @__PURE__ */ jsxs("div", { className: "doss-rep-top", children: [
1052
+ st && /* @__PURE__ */ jsx("span", { className: `rep-dot ${st}`, "aria-label": st }),
1053
+ when && /* @__PURE__ */ jsx("span", { className: "doss-rep-when", children: when }),
1054
+ /* @__PURE__ */ jsx("span", { className: "doss-rep-label", children: r.label }),
1055
+ /* @__PURE__ */ jsx("span", { className: "doss-rep-caret", "aria-hidden": true, children: open ? "\u25BE" : "\u25B8" })
1056
+ ] }),
1057
+ sum && /* @__PURE__ */ jsx("p", { className: "doss-rep-sum", children: sum }),
1058
+ open && /* @__PURE__ */ jsxs("div", { className: "doss-rep-detail", children: [
1059
+ st && /* @__PURE__ */ jsxs("div", { children: [
1060
+ /* @__PURE__ */ jsx("span", { className: "doss-l", children: "status" }),
1061
+ " ",
1062
+ /* @__PURE__ */ jsx("b", { className: `rep-${st}`, children: st })
1063
+ ] }),
1064
+ extra.map(([k, v]) => /* @__PURE__ */ jsxs("div", { children: [
1065
+ /* @__PURE__ */ jsx("span", { className: "doss-l", children: k }),
1066
+ " ",
1067
+ String(v)
1068
+ ] }, k)),
1069
+ extra.length === 0 && !st && /* @__PURE__ */ jsx("div", { className: "doss-empty", children: "no extra detail on this report" })
1070
+ ] })
1071
+ ]
1072
+ },
1073
+ r.id
1074
+ );
1075
+ }) })
1076
+ ] }),
1077
+ autos.length > 0 && /* @__PURE__ */ jsxs("section", { children: [
1078
+ /* @__PURE__ */ jsxs("h3", { children: [
1079
+ "automations \xB7 ",
1080
+ autos.length,
1081
+ " \u2014 machines this agent operates"
1082
+ ] }),
1083
+ /* @__PURE__ */ jsx("div", { className: "doss-autos", children: autos.map((m) => {
1084
+ const p = pulseFor(m, health);
1085
+ return /* @__PURE__ */ jsxs("button", { className: "doss-auto", onClick: () => onSelect(m.id), children: [
1086
+ /* @__PURE__ */ jsx("i", { className: `mac-dot ${lightFor(m, health)}${unstableFor(m, health) ? " unstable" : ""}` }),
1087
+ /* @__PURE__ */ jsx("span", { className: "doss-auto-emoji", children: m.emoji || "\u2699\uFE0F" }),
1088
+ /* @__PURE__ */ jsx("span", { className: "doss-auto-name", children: m.name }),
1089
+ m.role && /* @__PURE__ */ jsx("span", { className: "doss-auto-role", children: m.role }),
1090
+ p?.lastAt && /* @__PURE__ */ jsx("span", { className: "doss-auto-when", children: relTime(p.lastAt) })
1091
+ ] }, m.id);
1092
+ }) })
1093
+ ] }),
1094
+ /* @__PURE__ */ jsxs("section", { children: [
1095
+ /* @__PURE__ */ jsx("h3", { children: "rules \xB7 inherited top-down" }),
1096
+ slice.rules.length === 0 ? /* @__PURE__ */ jsx("p", { className: "doss-empty", children: "no rules anywhere on this branch yet." }) : /* @__PURE__ */ jsx("ul", { className: "doss-rules", children: slice.rules.map((r) => /* @__PURE__ */ jsxs("li", { className: own.has(r) ? "own" : "", children: [
1097
+ r,
1098
+ " ",
1099
+ !own.has(r) && /* @__PURE__ */ jsx("em", { children: "inherited" })
1100
+ ] }, r)) })
1101
+ ] }),
1102
+ /* @__PURE__ */ jsxs("section", { children: [
1103
+ /* @__PURE__ */ jsx("h3", { children: "memory buckets" }),
1104
+ slice.buckets.length === 0 ? /* @__PURE__ */ jsx("p", { className: "doss-empty", children: "no bucket access \u2014 this agent remembers nothing." }) : /* @__PURE__ */ jsx("div", { className: "doss-chips", children: slice.buckets.map((b) => /* @__PURE__ */ jsxs(Chip, { onClick: () => nav(`/buckets/${encodeURIComponent(b)}`), children: [
1105
+ b,
1106
+ " \u203A"
1107
+ ] }, b)) })
1108
+ ] }),
1109
+ /* @__PURE__ */ jsxs("section", { children: [
1110
+ /* @__PURE__ */ jsx("h3", { children: "skills" }),
1111
+ slice.skills.length === 0 ? /* @__PURE__ */ jsx("p", { className: "doss-empty", children: "no skills declared." }) : /* @__PURE__ */ jsx("div", { className: "doss-chips", children: slice.skills.map((s) => /* @__PURE__ */ jsx(Chip, { tone: "alt", children: s }, s)) })
1112
+ ] }),
1113
+ !edit && /* @__PURE__ */ jsxs("section", { className: "doss-contract", children: [
1114
+ /* @__PURE__ */ jsxs("h3", { children: [
1115
+ "contract ",
1116
+ /* @__PURE__ */ jsx("em", { children: "the operating prompt \u2014 loaded first, every session" })
1117
+ ] }),
1118
+ editContract ? /* @__PURE__ */ jsxs("div", { className: "contract-edit", children: [
1119
+ /* @__PURE__ */ jsx(
1120
+ "textarea",
1121
+ {
1122
+ className: "contract-ta",
1123
+ autoFocus: true,
1124
+ value: contractText,
1125
+ onChange: (e) => setContractText(e.target.value),
1126
+ placeholder: "Who this agent is \xB7 what it decides \xB7 how it verifies \xB7 when it stops. This is the first thing it reads."
1127
+ }
1128
+ ),
1129
+ /* @__PURE__ */ jsxs("div", { className: "doss-edit-actions", children: [
1130
+ /* @__PURE__ */ jsx("button", { className: "btn primary", onClick: () => {
1131
+ onUpdate(id, { boot: contractText.trim() || void 0 });
1132
+ setEditContract(false);
1133
+ }, children: "save to draft" }),
1134
+ /* @__PURE__ */ jsx("button", { className: "btn ghost", onClick: () => setEditContract(false), children: "cancel" })
1135
+ ] })
1136
+ ] }) : /* @__PURE__ */ jsxs(
1137
+ "button",
1138
+ {
1139
+ type: "button",
1140
+ className: `contract-card${a.boot ? "" : " empty"}`,
1141
+ title: "click to open \xB7 edit this agent's contract",
1142
+ onClick: () => {
1143
+ setContractText(a.boot ?? "");
1144
+ setEditContract(true);
1145
+ },
1146
+ children: [
1147
+ a.boot ? /* @__PURE__ */ jsx("p", { className: "doss-boot", children: a.boot }) : /* @__PURE__ */ jsx("p", { className: "doss-empty", children: "no contract yet \u2014 click to write one." }),
1148
+ /* @__PURE__ */ jsx("span", { className: "contract-hint", children: "\u270E edit" })
1149
+ ]
1150
+ }
1151
+ )
1152
+ ] }),
1153
+ /* @__PURE__ */ jsxs("div", { className: "doss-foot", children: [
1154
+ /* @__PURE__ */ jsxs("button", { className: "btn ghost", onClick: () => onAdd(id), children: [
1155
+ "\uFF0B add agent under ",
1156
+ a.name
1157
+ ] }),
1158
+ !isRoot && /* @__PURE__ */ jsxs("button", { className: "btn danger", onClick: () => onRemove(id), children: [
1159
+ "\u2212 remove ",
1160
+ a.name
1161
+ ] })
1162
+ ] })
1163
+ ] });
1164
+ }
1165
+ function OrgScreen({
1166
+ draft,
1167
+ selected,
1168
+ dragId,
1169
+ setSelected,
1170
+ setDragId,
1171
+ dropOn,
1172
+ hasSnapshot,
1173
+ onUpdate,
1174
+ onAdd,
1175
+ onRemove
1176
+ }) {
1177
+ const api = useApi();
1178
+ const [health, setHealth] = useState(null);
1179
+ useEffect(() => {
1180
+ if (!hasSnapshot) return;
1181
+ fetchReports(api, null, 1e4).then(({ nodes }) => setHealth(buildHealthMap(nodes))).catch(() => {
1182
+ });
1183
+ }, [hasSnapshot, api]);
1184
+ const fitRef = useRef(null);
1185
+ const chartRef = useRef(null);
1186
+ const [fit, setFit] = useState(1);
1187
+ useLayoutEffect(() => {
1188
+ const vp = fitRef.current, chart = chartRef.current;
1189
+ if (!vp || !chart) return;
1190
+ const PAD = 24;
1191
+ const measure = () => {
1192
+ const natW = chart.scrollWidth, natH = chart.scrollHeight;
1193
+ const availW = vp.clientWidth - PAD * 2, availH = vp.clientHeight - PAD * 2;
1194
+ if (natW <= 0 || natH <= 0) return;
1195
+ const k = Math.min(1, availW / natW, availH / natH);
1196
+ setFit(k > 0 ? k : 1);
1197
+ };
1198
+ measure();
1199
+ const ro = new ResizeObserver(measure);
1200
+ ro.observe(vp);
1201
+ ro.observe(chart);
1202
+ return () => ro.disconnect();
1203
+ }, [draft, health, selected]);
1204
+ const root = draft.agents.find((a) => a.id === draft.root);
1205
+ return /* @__PURE__ */ jsxs("div", { className: "body", onClick: () => setSelected(null), children: [
1206
+ /* @__PURE__ */ jsxs("main", { className: "tree", onClick: (e) => e.stopPropagation(), children: [
1207
+ /* @__PURE__ */ jsx("p", { className: "tree-hint", children: "drag an agent onto its new parent \xB7 click for its dossier \xB7 machine trays show live health" }),
1208
+ /* @__PURE__ */ jsxs("div", { className: "chart-fit", ref: fitRef, children: [
1209
+ /* @__PURE__ */ jsx("div", { className: "chart", ref: chartRef, style: { ["--fit"]: fit }, children: root && /* @__PURE__ */ jsx(
1210
+ ChartNode,
1211
+ {
1212
+ org: draft,
1213
+ a: root,
1214
+ depth: 0,
1215
+ order: 0,
1216
+ selected,
1217
+ dragId,
1218
+ health,
1219
+ onSelect: setSelected,
1220
+ onDragStart: setDragId,
1221
+ onDropOn: dropOn
1222
+ }
1223
+ ) }),
1224
+ fit < 0.999 && /* @__PURE__ */ jsxs("span", { className: "fit-badge", children: [
1225
+ "fit ",
1226
+ Math.round(fit * 100),
1227
+ "%"
1228
+ ] })
1229
+ ] })
1230
+ ] }),
1231
+ selected && /* @__PURE__ */ jsx(
1232
+ Dossier,
1233
+ {
1234
+ org: draft,
1235
+ id: selected,
1236
+ hasSnapshot,
1237
+ onUpdate,
1238
+ onAdd,
1239
+ onRemove,
1240
+ onSelect: setSelected,
1241
+ health
1242
+ }
1243
+ )
1244
+ ] });
1245
+ }
1246
+ function BucketCount({ bucket }) {
1247
+ const api = useApi();
1248
+ const [n, setN] = useState(null);
1249
+ useEffect(() => {
1250
+ api(`/nodes?type=memory&cluster=${encodeURIComponent(bucket)}&limit=1`).then((j) => setN(j.total)).catch(() => setN(0));
1251
+ }, [bucket, api]);
1252
+ const v = useCountUp(n ?? 0);
1253
+ return /* @__PURE__ */ jsx("div", { className: "bk-n", children: n === null ? "\u2026" : v.toLocaleString() });
1254
+ }
1255
+ function BucketsScreen({ org, param, hasSnapshot }) {
1256
+ const api = useApi();
1257
+ const [discovered, setDiscovered] = useState(null);
1258
+ useEffect(() => {
1259
+ if (!hasSnapshot) {
1260
+ setDiscovered({});
1261
+ return;
1262
+ }
1263
+ api("/clusters?type=memory").then((j) => setDiscovered(j.clusters ?? {})).catch(() => setDiscovered({}));
1264
+ }, [hasSnapshot, api]);
1265
+ const buckets = useMemo(() => {
1266
+ const map = /* @__PURE__ */ new Map();
1267
+ for (const a of org.agents) {
1268
+ const slice = orgBootSlice(org, a.id);
1269
+ for (const b of slice?.buckets ?? []) {
1270
+ const arr = map.get(b) ?? [];
1271
+ arr.push(a);
1272
+ map.set(b, arr);
1273
+ }
1274
+ }
1275
+ for (const b of Object.keys(discovered ?? {})) if (!map.has(b)) map.set(b, []);
1276
+ return [...map.entries()].sort(
1277
+ (x, y) => (y[1].length ? 1 : 0) - (x[1].length ? 1 : 0) || (discovered?.[y[0]] ?? 0) - (discovered?.[x[0]] ?? 0) || x[0].localeCompare(y[0])
1278
+ );
1279
+ }, [org, discovered]);
1280
+ const [items, setItems] = useState(null);
1281
+ useEffect(() => {
1282
+ setItems(null);
1283
+ if (!param || !hasSnapshot) return;
1284
+ api(`/nodes?type=memory&cluster=${encodeURIComponent(param)}&limit=500`).then((j) => {
1285
+ setItems([...j.nodes].sort((a, b) => nodeAt(b).localeCompare(nodeAt(a))));
1286
+ }).catch(() => setItems([]));
1287
+ }, [param, hasSnapshot, api]);
1288
+ if (param) {
1289
+ return /* @__PURE__ */ jsxs("div", { className: "screen", children: [
1290
+ /* @__PURE__ */ jsx("button", { className: "pnl-back", onClick: () => nav("/buckets"), children: "\u2190 all buckets" }),
1291
+ /* @__PURE__ */ jsxs("h2", { className: "scr-title", children: [
1292
+ /* @__PURE__ */ jsx("i", { className: "bk-dot", style: { ["--h"]: bucketHue(param) } }),
1293
+ " ",
1294
+ param
1295
+ ] }),
1296
+ !hasSnapshot ? /* @__PURE__ */ jsxs("p", { className: "scr-empty", children: [
1297
+ "start with ",
1298
+ /* @__PURE__ */ jsx("code", { children: "--snapshot" }),
1299
+ " to browse this bucket's memories."
1300
+ ] }) : items === null ? /* @__PURE__ */ jsx("p", { className: "scr-empty", children: "loading\u2026" }) : items.length === 0 ? /* @__PURE__ */ jsx("p", { className: "scr-empty", children: "this bucket is empty \u2014 nothing remembered here yet." }) : /* @__PURE__ */ jsx("div", { className: "mem-list", children: items.slice(0, 150).map((m) => {
1301
+ const when = relTime(nodeAt(m));
1302
+ return /* @__PURE__ */ jsxs("div", { className: "mem-row", children: [
1303
+ when && /* @__PURE__ */ jsx("span", { className: "mem-when", children: when }),
1304
+ /* @__PURE__ */ jsx("span", { className: "mem-label", children: m.label })
1305
+ ] }, m.id);
1306
+ }) })
1307
+ ] });
1308
+ }
1309
+ return /* @__PURE__ */ jsxs("div", { className: "screen", children: [
1310
+ /* @__PURE__ */ jsx("h2", { className: "scr-title", children: "memory buckets" }),
1311
+ /* @__PURE__ */ jsx("p", { className: "scr-sub", children: "every bucket in the organigram, who reaches it, and how much lives inside." }),
1312
+ buckets.length === 0 ? /* @__PURE__ */ jsxs("p", { className: "scr-empty", children: [
1313
+ "no buckets declared on any agent yet \u2014 add ",
1314
+ /* @__PURE__ */ jsx("code", { children: '"buckets"' }),
1315
+ " to agents in the org file."
1316
+ ] }) : /* @__PURE__ */ jsx("div", { className: "bk-grid", children: buckets.map(([b, agents], i) => /* @__PURE__ */ jsxs(
1317
+ "button",
1318
+ {
1319
+ className: "bk-card",
1320
+ style: { ["--h"]: bucketHue(b), animationDelay: `${i * 70}ms` },
1321
+ onClick: () => nav(`/buckets/${encodeURIComponent(b)}`),
1322
+ children: [
1323
+ /* @__PURE__ */ jsxs("div", { className: "bk-top", children: [
1324
+ /* @__PURE__ */ jsx("i", { className: "bk-dot" }),
1325
+ /* @__PURE__ */ jsx("span", { className: "bk-name", children: b })
1326
+ ] }),
1327
+ hasSnapshot ? /* @__PURE__ */ jsx(BucketCount, { bucket: b }) : /* @__PURE__ */ jsx("div", { className: "bk-n dim", children: "\u2014" }),
1328
+ /* @__PURE__ */ jsx("div", { className: "bk-l", children: "memories" }),
1329
+ agents.length > 0 ? /* @__PURE__ */ jsxs(Fragment, { children: [
1330
+ /* @__PURE__ */ jsxs("div", { className: "bk-crew", children: [
1331
+ agents.slice(0, 7).map((a) => /* @__PURE__ */ jsx("span", { className: "bk-face", title: a.name, children: a.emoji || "\u{1F916}" }, a.id)),
1332
+ agents.length > 7 && /* @__PURE__ */ jsxs("span", { className: "bk-more", children: [
1333
+ "+",
1334
+ agents.length - 7
1335
+ ] })
1336
+ ] }),
1337
+ /* @__PURE__ */ jsxs("div", { className: "bk-agents", children: [
1338
+ agents.slice(0, 3).map((a) => a.name).join(" \xB7 "),
1339
+ agents.length > 3 ? ` +${agents.length - 3}` : ""
1340
+ ] })
1341
+ ] }) : /* @__PURE__ */ jsx("div", { className: "bk-agents unassigned", children: "unassigned \u2014 no agent declares this bucket; add it to an agent's buckets to claim it" })
1342
+ ]
1343
+ },
1344
+ b
1345
+ )) })
1346
+ ] });
1347
+ }
1348
+ function ReportsScreen({ org, hasSnapshot }) {
1349
+ const api = useApi();
1350
+ const [rows, setRows] = useState(null);
1351
+ const [who, setWho] = useState("");
1352
+ const nameOf = useMemo(() => new Map(org.agents.map((a) => [a.id, a])), [org]);
1353
+ useEffect(() => {
1354
+ if (!hasSnapshot) return;
1355
+ fetchReports(api, null, 1e3).then(({ nodes }) => setRows(nodes)).catch(() => setRows([]));
1356
+ }, [hasSnapshot, api]);
1357
+ const agents = useMemo(() => [...new Set((rows ?? []).map((r) => r.cluster ?? ""))].filter(Boolean), [rows]);
1358
+ const shown = (rows ?? []).filter((r) => !who || r.cluster === who);
1359
+ const total = useCountUp(shown.length);
1360
+ if (!hasSnapshot)
1361
+ return /* @__PURE__ */ jsxs("div", { className: "screen", children: [
1362
+ /* @__PURE__ */ jsx("h2", { className: "scr-title", children: "reports" }),
1363
+ /* @__PURE__ */ jsxs("p", { className: "scr-empty", children: [
1364
+ "start with ",
1365
+ /* @__PURE__ */ jsx("code", { children: "--snapshot" }),
1366
+ " to see the portfolio timeline."
1367
+ ] })
1368
+ ] });
1369
+ return /* @__PURE__ */ jsxs("div", { className: "screen", children: [
1370
+ /* @__PURE__ */ jsxs("h2", { className: "scr-title", children: [
1371
+ "reports ",
1372
+ /* @__PURE__ */ jsx("span", { className: "scr-count", children: total })
1373
+ ] }),
1374
+ /* @__PURE__ */ jsx("p", { className: "scr-sub", children: "what the fleet has been closing, newest first." }),
1375
+ agents.length > 1 && /* @__PURE__ */ jsxs("div", { className: "rep-filter", children: [
1376
+ /* @__PURE__ */ jsx(Chip, { tone: who === "" ? "" : "alt", onClick: () => setWho(""), children: "everyone" }),
1377
+ agents.map((a) => /* @__PURE__ */ jsxs(Chip, { tone: who === a ? "" : "alt", onClick: () => setWho(who === a ? "" : a), children: [
1378
+ nameOf.get(a)?.emoji ?? "",
1379
+ " ",
1380
+ nameOf.get(a)?.name ?? a
1381
+ ] }, a))
1382
+ ] }),
1383
+ rows === null ? /* @__PURE__ */ jsx("p", { className: "scr-empty", children: "loading\u2026" }) : shown.length === 0 ? /* @__PURE__ */ jsxs("p", { className: "scr-empty", children: [
1384
+ "no reports filed yet \u2014 they land here as agents close work (node type ",
1385
+ /* @__PURE__ */ jsx("code", { children: "report" }),
1386
+ ")."
1387
+ ] }) : /* @__PURE__ */ jsxs("div", { className: "timeline", children: [
1388
+ shown.slice(0, 100).map((r) => {
1389
+ const a = r.cluster ? nameOf.get(r.cluster) : void 0;
1390
+ const when = relTime(nodeAt(r));
1391
+ const sum = nodeSummary(r);
1392
+ return /* @__PURE__ */ jsxs("div", { className: "tl-row", style: { ["--h"]: bucketHue(r.cluster ?? "x") }, children: [
1393
+ /* @__PURE__ */ jsx("div", { className: "tl-dot" }),
1394
+ /* @__PURE__ */ jsxs("div", { className: "tl-body", children: [
1395
+ /* @__PURE__ */ jsxs("div", { className: "tl-top", children: [
1396
+ /* @__PURE__ */ jsx("span", { className: "tl-ava", children: a?.emoji ?? "\u{1F916}" }),
1397
+ /* @__PURE__ */ jsx("span", { className: "tl-agent", children: a?.name ?? r.cluster ?? "unknown" }),
1398
+ when && /* @__PURE__ */ jsx("span", { className: "tl-when", children: when })
1399
+ ] }),
1400
+ /* @__PURE__ */ jsx("p", { className: "tl-sum", children: sum || r.label })
1401
+ ] })
1402
+ ] }, r.id);
1403
+ }),
1404
+ shown.length > 100 && /* @__PURE__ */ jsxs("p", { className: "scr-empty", children: [
1405
+ "showing the latest 100 of ",
1406
+ shown.length,
1407
+ "."
1408
+ ] })
1409
+ ] })
1410
+ ] });
1411
+ }
1412
+ function RulesScreen({ org }) {
1413
+ const rules = useMemo(() => {
1414
+ const declared = /* @__PURE__ */ new Map();
1415
+ for (const a of org.agents) for (const r of a.rules ?? []) {
1416
+ const arr = declared.get(r) ?? [];
1417
+ arr.push(a.id);
1418
+ declared.set(r, arr);
1419
+ }
1420
+ const inherited = /* @__PURE__ */ new Map();
1421
+ for (const a of org.agents) {
1422
+ const own = new Set(a.rules ?? []);
1423
+ const slice = orgBootSlice(org, a.id);
1424
+ for (const r of slice?.rules ?? []) if (!own.has(r)) {
1425
+ const arr = inherited.get(r) ?? [];
1426
+ arr.push(a.id);
1427
+ inherited.set(r, arr);
1428
+ }
1429
+ }
1430
+ const name = (id) => org.agents.find((a) => a.id === id)?.name ?? id;
1431
+ return [...declared.entries()].sort((x, y) => x[0].localeCompare(y[0])).map(([r, by]) => ({
1432
+ rule: r,
1433
+ global: by.includes(org.root),
1434
+ declaredBy: by.map(name),
1435
+ inheritedBy: (inherited.get(r) ?? []).map(name)
1436
+ }));
1437
+ }, [org]);
1438
+ const maxBind = Math.max(1, org.agents.length - 1);
1439
+ return /* @__PURE__ */ jsxs("div", { className: "screen", children: [
1440
+ /* @__PURE__ */ jsxs("h2", { className: "scr-title", children: [
1441
+ "rules ",
1442
+ /* @__PURE__ */ jsx("span", { className: "scr-count", children: rules.length })
1443
+ ] }),
1444
+ /* @__PURE__ */ jsx("p", { className: "scr-sub", children: "every rule in the organigram \u2014 who declares it, and who lives under it." }),
1445
+ rules.length === 0 ? /* @__PURE__ */ jsxs("p", { className: "scr-empty", children: [
1446
+ "no rules declared yet \u2014 add ",
1447
+ /* @__PURE__ */ jsx("code", { children: '"rules"' }),
1448
+ " refs to agents in the org file."
1449
+ ] }) : /* @__PURE__ */ jsx("div", { className: "rule-list", children: rules.map((r, i) => /* @__PURE__ */ jsxs("div", { className: "rule-card", style: { animationDelay: `${i * 60}ms` }, children: [
1450
+ /* @__PURE__ */ jsxs("div", { className: "rule-top", children: [
1451
+ /* @__PURE__ */ jsx("span", { className: "rule-ref", children: r.rule }),
1452
+ r.global && /* @__PURE__ */ jsx("span", { className: "rule-scope", children: "global \u2014 binds everyone" })
1453
+ ] }),
1454
+ /* @__PURE__ */ jsxs("div", { className: "rule-meta", children: [
1455
+ /* @__PURE__ */ jsxs("span", { children: [
1456
+ "declared by ",
1457
+ /* @__PURE__ */ jsx("b", { children: r.declaredBy.join(", ") })
1458
+ ] }),
1459
+ r.inheritedBy.length > 0 && /* @__PURE__ */ jsxs("span", { children: [
1460
+ " \xB7 binds ",
1461
+ /* @__PURE__ */ jsx("b", { children: r.inheritedBy.length }),
1462
+ " below: ",
1463
+ r.inheritedBy.join(", ")
1464
+ ] })
1465
+ ] }),
1466
+ /* @__PURE__ */ jsx("div", { className: "rule-bar", children: /* @__PURE__ */ jsx("i", { style: { width: `${Math.round(r.inheritedBy.length / maxBind * 100)}%` } }) })
1467
+ ] }, r.rule)) })
1468
+ ] });
1469
+ }
1470
+ function GraphScreen({ hasSnapshot }) {
1471
+ if (!hasSnapshot)
1472
+ return /* @__PURE__ */ jsxs("div", { className: "screen", children: [
1473
+ /* @__PURE__ */ jsx("h2", { className: "scr-title", children: "graph" }),
1474
+ /* @__PURE__ */ jsxs("p", { className: "scr-empty", children: [
1475
+ "start with ",
1476
+ /* @__PURE__ */ jsx("code", { children: "--snapshot graph.json" }),
1477
+ " and the 3D brain renders right here."
1478
+ ] })
1479
+ ] });
1480
+ return /* @__PURE__ */ jsx("div", { className: "graph-wrap", children: /* @__PURE__ */ jsx("iframe", { className: "graph-frame", src: "/view/?file=/snapshot.json", title: "Booboo 3D brain" }) });
1481
+ }
1482
+ function App() {
1483
+ const api = useApi();
1484
+ const [tab, param] = useRoute();
1485
+ const [saved, setSaved] = useState(null);
1486
+ const [draft, setDraft] = useState(null);
1487
+ const [selected, setSelected] = useState(null);
1488
+ const [dragId, setDragId] = useState(null);
1489
+ const [stats, setStats] = useState(null);
1490
+ const [err, setErr] = useState("");
1491
+ const [applying, setApplying] = useState(false);
1492
+ const [totals, setTotals] = useState(null);
1493
+ useEffect(() => {
1494
+ api("/org").then((o) => {
1495
+ setSaved(o);
1496
+ setDraft(o);
1497
+ setSelected(o.root);
1498
+ }).catch(() => setErr("Can't load the organigram \u2014 is `booboo panel --org` running?"));
1499
+ api("/stats").then(setStats).catch(() => setStats(null));
1500
+ Promise.all([
1501
+ api("/nodes?type=memory&limit=1").then((j) => j.total),
1502
+ fetchReports(api, null, 1).then((r) => r.total)
1503
+ ]).then(([mem, rep]) => setTotals({ mem, rep })).catch(() => setTotals(null));
1504
+ }, [api]);
1505
+ const changes = useMemo(() => {
1506
+ if (!saved || !draft) return [];
1507
+ const before = new Map(saved.agents.map((a) => [a.id, a]));
1508
+ const after = new Map(draft.agents.map((a) => [a.id, a]));
1509
+ const name = (id) => after.get(id)?.name ?? before.get(id)?.name ?? id;
1510
+ const noParent = ({ parent: _p, ...rest }) => rest;
1511
+ const out = [];
1512
+ for (const a of draft.agents) {
1513
+ const b = before.get(a.id);
1514
+ if (!b) {
1515
+ out.push(`\uFF0B ${a.name} under ${a.parent ? name(a.parent) : "root"}`);
1516
+ continue;
1517
+ }
1518
+ if ((b.parent ?? null) !== (a.parent ?? null)) out.push(`${a.name} \u2192 now under ${a.parent ? name(a.parent) : "root"}`);
1519
+ if (JSON.stringify(noParent(b)) !== JSON.stringify(noParent(a))) out.push(`\u270E ${a.name} edited`);
1520
+ }
1521
+ for (const b of saved.agents) if (!after.has(b.id)) out.push(`\u2212 ${b.name} removed`);
1522
+ return out;
1523
+ }, [saved, draft]);
1524
+ const updateAgent = useCallback((id, patch) => {
1525
+ setDraft((d) => d ? { ...d, agents: d.agents.map((a) => a.id === id ? { ...a, ...patch } : a) } : d);
1526
+ }, []);
1527
+ const addAgent = useCallback(
1528
+ (parentId) => {
1529
+ if (!draft) return;
1530
+ const nm = window.prompt("Name for the new agent:");
1531
+ if (!nm || !nm.trim()) return;
1532
+ const base = slugify(nm) || "agent";
1533
+ let newId = base;
1534
+ let i = 2;
1535
+ while (draft.agents.some((a) => a.id === newId)) newId = `${base}-${i++}`;
1536
+ setDraft({ ...draft, agents: [...draft.agents, { id: newId, name: nm.trim(), emoji: "\u{1F916}", parent: parentId }] });
1537
+ setSelected(newId);
1538
+ },
1539
+ [draft]
1540
+ );
1541
+ const removeAgent = useCallback(
1542
+ (id) => {
1543
+ if (!draft || id === draft.root) return;
1544
+ const ag = draft.agents.find((a) => a.id === id);
1545
+ if (!ag) return;
1546
+ const parentName = draft.agents.find((a) => a.id === ag.parent)?.name ?? "the root";
1547
+ const kids = draft.agents.filter((a) => a.parent === id).length;
1548
+ if (!window.confirm(`Remove ${ag.name}?${kids ? ` Its ${kids} direct report${kids > 1 ? "s" : ""} move up to ${parentName}.` : ""}`)) return;
1549
+ setDraft({
1550
+ ...draft,
1551
+ agents: draft.agents.filter((a) => a.id !== id).map((a) => a.parent === id ? { ...a, parent: ag.parent ?? draft.root } : a)
1552
+ });
1553
+ setSelected(ag.parent ?? draft.root);
1554
+ },
1555
+ [draft]
1556
+ );
1557
+ const isDescendant = useCallback((org, maybeChild, of) => {
1558
+ const byId = new Map(org.agents.map((a) => [a.id, a]));
1559
+ let cur = byId.get(maybeChild);
1560
+ const guard = /* @__PURE__ */ new Set();
1561
+ while (cur?.parent && !guard.has(cur.id)) {
1562
+ guard.add(cur.id);
1563
+ if (cur.parent === of) return true;
1564
+ cur = byId.get(cur.parent);
1565
+ }
1566
+ return false;
1567
+ }, []);
1568
+ const dropOn = useCallback(
1569
+ (targetId) => {
1570
+ if (!draft || !dragId || dragId === targetId) return;
1571
+ if (dragId === draft.root) return;
1572
+ if (isDescendant(draft, targetId, dragId)) return;
1573
+ setDraft({ ...draft, agents: draft.agents.map((a) => a.id === dragId ? { ...a, parent: targetId } : a) });
1574
+ setDragId(null);
1575
+ },
1576
+ [draft, dragId, isDescendant]
1577
+ );
1578
+ const apply = useCallback(async () => {
1579
+ if (!draft) return;
1580
+ setApplying(true);
1581
+ setErr("");
1582
+ try {
1583
+ await api("/org", { method: "PUT", headers: { "Content-Type": "application/json" }, body: JSON.stringify(draft) });
1584
+ setSaved(draft);
1585
+ } catch (e) {
1586
+ setErr(`apply failed: ${e?.errors?.join("; ") ?? "unknown error"}`);
1587
+ } finally {
1588
+ setApplying(false);
1589
+ }
1590
+ }, [draft, api]);
1591
+ const agentCount = useCountUp(draft?.agents.length ?? 0);
1592
+ const nodeCount = useCountUp(stats?.nodes ?? 0, 1200);
1593
+ const memTotal = useCountUp(totals?.mem ?? 0, 1100);
1594
+ const repTotal = useCountUp(totals?.rep ?? 0, 1300);
1595
+ const [theme, toggleTheme] = useTheme();
1596
+ if (err && !draft) return /* @__PURE__ */ jsx("div", { className: "pnl-fatal", children: err });
1597
+ if (!draft) return /* @__PURE__ */ jsx("div", { className: "pnl-fatal calm", children: "waking the organigram\u2026" });
1598
+ return /* @__PURE__ */ jsxs("div", { className: "pnl", children: [
1599
+ /* @__PURE__ */ jsx("div", { className: "pnl-aurora", "aria-hidden": true }),
1600
+ /* @__PURE__ */ jsxs("header", { className: "bar", children: [
1601
+ /* @__PURE__ */ jsxs("div", { className: "bar-brand", children: [
1602
+ "\u{1F43E} ",
1603
+ /* @__PURE__ */ jsx("b", { children: draft.title || "the organigram" })
1604
+ ] }),
1605
+ /* @__PURE__ */ jsxs("div", { className: "bar-stats", children: [
1606
+ /* @__PURE__ */ jsxs("span", { children: [
1607
+ /* @__PURE__ */ jsx("b", { children: agentCount }),
1608
+ " agents"
1609
+ ] }),
1610
+ stats && /* @__PURE__ */ jsxs("span", { children: [
1611
+ /* @__PURE__ */ jsx("b", { children: nodeCount.toLocaleString() }),
1612
+ " nodes"
1613
+ ] }),
1614
+ totals && /* @__PURE__ */ jsxs("span", { onClick: () => nav("/buckets"), className: "tap", children: [
1615
+ /* @__PURE__ */ jsx("b", { children: memTotal.toLocaleString() }),
1616
+ " memories"
1617
+ ] }),
1618
+ totals && /* @__PURE__ */ jsxs("span", { onClick: () => nav("/reports"), className: "tap", children: [
1619
+ /* @__PURE__ */ jsx("b", { children: repTotal.toLocaleString() }),
1620
+ " reports"
1621
+ ] })
1622
+ ] }),
1623
+ /* @__PURE__ */ jsxs("div", { className: "bar-actions", children: [
1624
+ /* @__PURE__ */ jsx("button", { className: "btn ghost theme-toggle", title: "light / dark", "aria-label": "toggle light or dark theme", onClick: toggleTheme, children: theme === "dark" ? "\u2600" : "\u263E" }),
1625
+ changes.length > 0 ? /* @__PURE__ */ jsxs(Fragment, { children: [
1626
+ /* @__PURE__ */ jsxs("span", { className: "bar-draft", children: [
1627
+ changes.length,
1628
+ " unapplied change",
1629
+ changes.length > 1 ? "s" : ""
1630
+ ] }),
1631
+ /* @__PURE__ */ jsx("button", { className: "btn ghost", onClick: () => setDraft(saved), children: "discard" }),
1632
+ /* @__PURE__ */ jsx("button", { className: "btn primary", disabled: applying, onClick: apply, children: applying ? "applying\u2026" : "apply \u2192 org file" })
1633
+ ] }) : /* @__PURE__ */ jsx("span", { className: "bar-ok", children: "\u25CF in sync with the org file" })
1634
+ ] })
1635
+ ] }),
1636
+ /* @__PURE__ */ jsx("nav", { className: "tabs", children: TABS.map((t) => /* @__PURE__ */ jsxs("button", { className: `tab${tab === t.id ? " on" : ""}`, onClick: () => nav(`/${t.id === "org" ? "" : t.id}`), children: [
1637
+ /* @__PURE__ */ jsx("span", { className: "tab-glyph", children: t.glyph }),
1638
+ " ",
1639
+ t.label
1640
+ ] }, t.id)) }),
1641
+ changes.length > 0 && tab === "org" && /* @__PURE__ */ jsx("div", { className: "pending", children: changes.map((c) => /* @__PURE__ */ jsx("span", { className: "pending-item", children: c }, c)) }),
1642
+ err && /* @__PURE__ */ jsx("div", { className: "pnl-err", children: err }),
1643
+ /* @__PURE__ */ jsxs("div", { className: "content", children: [
1644
+ tab === "org" && /* @__PURE__ */ jsx(
1645
+ OrgScreen,
1646
+ {
1647
+ draft,
1648
+ selected,
1649
+ dragId,
1650
+ setSelected,
1651
+ setDragId,
1652
+ dropOn,
1653
+ hasSnapshot: !!stats,
1654
+ onUpdate: updateAgent,
1655
+ onAdd: addAgent,
1656
+ onRemove: removeAgent
1657
+ }
1658
+ ),
1659
+ tab === "buckets" && /* @__PURE__ */ jsx(BucketsScreen, { org: draft, param, hasSnapshot: !!stats }),
1660
+ tab === "reports" && /* @__PURE__ */ jsx(ReportsScreen, { org: draft, hasSnapshot: !!stats }),
1661
+ tab === "rules" && /* @__PURE__ */ jsx(RulesScreen, { org: draft }),
1662
+ tab === "graph" && /* @__PURE__ */ jsx(GraphScreen, { hasSnapshot: !!stats })
1663
+ ] }, tab + (param ?? ""))
1664
+ ] });
1665
+ }
1666
+ function Panel({ api = defaultApi } = {}) {
1667
+ return /* @__PURE__ */ jsxs(ApiCtx.Provider, { value: api, children: [
1668
+ /* @__PURE__ */ jsx("style", { children: PANEL_CSS }),
1669
+ /* @__PURE__ */ jsx(App, {})
1670
+ ] });
1671
+ }
1672
+
1
1673
  // src/index.ts
2
- var PANEL_VERSION = "0.1.0";
1674
+ var PANEL_VERSION = "0.4.0";
3
1675
  export {
4
- PANEL_VERSION
1676
+ PANEL_VERSION,
1677
+ Panel
5
1678
  };