@yuneta/gobj-ui 1.0.1 → 2.2.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.
Files changed (46) hide show
  1. package/README.md +40 -363
  2. package/dist/gobj-ui.cjs.js +11154 -5506
  3. package/dist/gobj-ui.es.js +11131 -5508
  4. package/index.js +41 -14
  5. package/package.json +11 -9
  6. package/src/c_g6_nodes_tree.js +6 -1
  7. package/src/c_yui_form.js +1 -1
  8. package/src/c_yui_gobj_tree_js.js +1 -1
  9. package/src/c_yui_json_graph.js +1 -1
  10. package/src/c_yui_main.js +3 -3
  11. package/src/c_yui_map.js +6 -1
  12. package/src/c_yui_nav.js +881 -0
  13. package/src/c_yui_pager.js +545 -0
  14. package/src/c_yui_routing.css +1 -1
  15. package/src/c_yui_routing.js +1 -1
  16. package/src/c_yui_shell.css +571 -0
  17. package/src/c_yui_shell.js +2474 -0
  18. package/src/c_yui_tabs.js +1 -1
  19. package/src/c_yui_treedb_graph.js +35 -9
  20. package/src/c_yui_treedb_topic_with_form.js +1 -1
  21. package/src/c_yui_treedb_topics.js +36 -8
  22. package/src/c_yui_uplot.js +1 -1
  23. package/src/c_yui_window.js +242 -21
  24. package/src/c_yui_window_manager.js +602 -0
  25. package/src/c_yui_wizard.js +612 -0
  26. package/src/pager_helpers.js +138 -0
  27. package/src/pager_helpers.test.js +140 -0
  28. package/src/route_resolver.js +53 -0
  29. package/src/route_resolver.test.js +82 -0
  30. package/src/shell_focus_trap.js +123 -0
  31. package/src/shell_focus_trap.test.js +299 -0
  32. package/src/shell_modals.js +445 -0
  33. package/src/shell_show_on.js +91 -0
  34. package/src/shell_show_on.test.js +86 -0
  35. package/src/shell_toolbar_helpers.js +221 -0
  36. package/src/shell_toolbar_helpers.test.js +207 -0
  37. package/src/tabulator.css +53 -0
  38. package/src/wizard_helpers.js +117 -0
  39. package/src/wizard_helpers.test.js +122 -0
  40. package/src/yui_dev.js +1257 -362
  41. package/src/yui_icons.css +5 -0
  42. package/src/yui_inputs.css +32 -0
  43. package/src/yui_inputs.js +71 -0
  44. package/vite-plugin-yuneta-html.js +2 -2
  45. package/skeleton/config.json +0 -20
  46. package/skeleton/index.html +0 -37
package/src/yui_dev.js CHANGED
@@ -1,37 +1,825 @@
1
1
  /***********************************************************************
2
2
  * ui_dev.js
3
3
  *
4
- * Development Tools
4
+ * Development Tools — yuno monitor / audit console
5
5
  *
6
- * Copyright (c) 2024, ArtGins.
6
+ * Copyright (c) 2024-2026, ArtGins.
7
7
  * All Rights Reserved.
8
8
  ***********************************************************************/
9
9
  import {
10
10
  gobj_yuno,
11
- log_error,
12
11
  is_string,
13
12
  createElement2,
14
13
  kw_get_local_storage_value,
15
14
  kw_set_local_storage_value,
16
15
  gobj_write_attr,
17
16
  gobj_create_service,
17
+ gobj_find_service,
18
+ set_log_callback,
19
+ gobj_set_trace_machine_format,
18
20
  trace_json,
19
21
  } from "@yuneta/gobj-js";
20
22
 
21
23
  import i18next from 'i18next';
22
24
 
23
- import { JSONEditor } from 'vanilla-jsoneditor';
24
- import "vanilla-jsoneditor/themes/jse-theme-dark.css";
25
+ /***********************************************************************
26
+ * Traffic model (bounded ring buffer)
27
+ *
28
+ * Every inter-event message is kept as a lightweight record so view
29
+ * and filter changes re-render instantly from memory instead of
30
+ * losing history. Reopening the window repaints the buffer.
31
+ ***********************************************************************/
32
+ const TRAFFIC_MAX = 600; // capped history
33
+ const PERIODIC_THRESHOLD = 5; // a signature seen >= N times reads as recurring
34
+ const PERIODIC_RE = /PERIODIC|TIMEOUT|HEARTBEAT|PING/i;
35
+
36
+ let TRAFFIC_LOG = []; // [{title,event,command,sig,dir,size,ts,kw,jn,hay,$node}]
37
+ let TRAFFIC_COUNTS = new Map(); // signature -> occurrences (for periodic detection)
38
+ let SEARCH_TEXT = ""; // session-only free-text filter (not persisted)
39
+
40
+ /* Field names whose numeric value is a Unix timestamp (seconds). */
41
+ const TRAFFIC_TS_FIELDS = {
42
+ "__t__": 1, "__tm__": 1, "tm": 1, "t": 1,
43
+ "from_t": 1, "to_t": 1, "from_tm": 1, "to_tm": 1, "time": 1,
44
+ };
45
+
46
+ /* Trace toggles: [localStorage key, display label, handler]. */
47
+ const TRACE_DEFS = [
48
+ ["trace_automata", "Automata", trace_automata],
49
+ ["trace_creation", "Creation", trace_creation],
50
+ ["trace_start_stop", "Start/Stop", trace_start_stop],
51
+ ["trace_subscriptions", "Subscriptions", trace_subscriptions],
52
+ ["trace_i18n", "I18n", trace_i18n],
53
+ ["trace_traffic", "Traffic", trace_traffic],
54
+ ["no_poll", "No Poll", set_no_poll],
55
+ ];
56
+
57
+
58
+ /******************************
59
+ * Small helpers
60
+ ******************************/
61
+
62
+
63
+ /************************************************************
64
+ * hh:mm:ss.SSS wall-clock of the moment a message arrives.
65
+ ************************************************************/
66
+ function traffic_now()
67
+ {
68
+ let now = new Date();
69
+ let pad = (num, len) => ('000' + num).slice(len * -1);
70
+ let hours = pad(now.getHours(), 2);
71
+ let minutes = pad(now.getMinutes(), 2);
72
+ let seconds = pad(now.getSeconds(), 2);
73
+ let ms = pad(now.getMilliseconds(), 3);
74
+ return `${hours}:${minutes}:${seconds}.${ms}`;
75
+ }
76
+
77
+ /************************************************************
78
+ * Human byte size (B / KB / MB).
79
+ ************************************************************/
80
+ function traffic_size(n)
81
+ {
82
+ n = Number(n) || 0;
83
+ if(n < 1024) {
84
+ return n + " B";
85
+ }
86
+ if(n < 1024 * 1024) {
87
+ return (n / 1024).toFixed(1) + " KB";
88
+ }
89
+ return (n / (1024 * 1024)).toFixed(1) + " MB";
90
+ }
91
+
92
+ /************************************************************
93
+ * Seconds-since-epoch → ISO string, or null if not a plausible
94
+ * timestamp (guards against 0 / NaN / out-of-range values).
95
+ ************************************************************/
96
+ function traffic_iso(value)
97
+ {
98
+ let n = Number(value);
99
+ if(!isFinite(n) || n <= 0) {
100
+ return null;
101
+ }
102
+ try {
103
+ return new Date(n * 1000).toISOString();
104
+ } catch(e) {
105
+ return null;
106
+ }
107
+ }
108
+
109
+ /************************************************************
110
+ * Clip a string for inline display (full text kept elsewhere).
111
+ ************************************************************/
112
+ function traffic_clip(s, n)
113
+ {
114
+ s = String(s);
115
+ return s.length > n ? s.slice(0, n) + "…" : s;
116
+ }
117
+
118
+ /************************************************************
119
+ * A scalar rendered as a short inline token (for summaries).
120
+ ************************************************************/
121
+ function traffic_scalar_text(v)
122
+ {
123
+ if(v === null) {
124
+ return "null";
125
+ }
126
+ if(typeof v === "string") {
127
+ return traffic_clip(v, 40);
128
+ }
129
+ return String(v);
130
+ }
131
+
132
+ function dir_class(dir)
133
+ {
134
+ return (dir === 2) ? "dir-in" : (dir === 3) ? "dir-err" : "dir-out";
135
+ }
136
+
137
+ function dir_arrow(dir)
138
+ {
139
+ return (dir === 2) ? "⇠" : (dir === 3) ? "⚠" : "⇢";
140
+ }
141
+
142
+
143
+ /******************************
144
+ * Preferences
145
+ ******************************/
146
+
147
+
148
+ function dev_num(key, def)
149
+ {
150
+ return Number(kw_get_local_storage_value(key, (def === undefined ? 0 : def), false));
151
+ }
152
+
153
+ function dev_view()
154
+ {
155
+ let v = kw_get_local_storage_value("dev_view_mode", "detailed", false);
156
+ return (v === "compact" || v === "name" || v === "full") ? v : "detailed";
157
+ }
158
+
159
+ function dev_hide_periodic()
160
+ {
161
+ return dev_num("dev_hide_periodic", 0) ? true : false;
162
+ }
163
+
164
+ function dev_muted()
165
+ {
166
+ let a = kw_get_local_storage_value("dev_muted_events", [], false);
167
+ if(!Array.isArray(a)) {
168
+ a = [];
169
+ }
170
+ return new Set(a);
171
+ }
172
+
173
+ function dev_set_muted(set)
174
+ {
175
+ kw_set_local_storage_value("dev_muted_events", Array.from(set));
176
+ }
177
+
178
+ function set_view(v)
179
+ {
180
+ kw_set_local_storage_value("dev_view_mode", v);
181
+ rerender_all();
182
+ refresh_dev_chrome();
183
+ }
184
+
185
+ function toggle_pref(key, def)
186
+ {
187
+ let v = dev_num(key, def) ? 0 : 1;
188
+ kw_set_local_storage_value(key, v);
189
+ rerender_all();
190
+ refresh_dev_chrome();
191
+ }
192
+
193
+ function mute_signature(sig)
194
+ {
195
+ let set = dev_muted();
196
+ set.add(sig);
197
+ dev_set_muted(set);
198
+ rerender_all();
199
+ refresh_dev_chrome();
200
+ }
201
+
202
+ function unmute_signature(sig)
203
+ {
204
+ let set = dev_muted();
205
+ set.delete(sig);
206
+ dev_set_muted(set);
207
+ rerender_all();
208
+ refresh_dev_chrome();
209
+ }
210
+
211
+
212
+ /******************************
213
+ * Filtering
214
+ ******************************/
215
+
216
+
217
+ /* Signature identifies a "kind" of message. Command answers share
218
+ * the generic EV_MT_COMMAND event, so fold the command in to tell
219
+ * a get-stats poll apart from a user action. */
220
+ function traffic_signature(event, command)
221
+ {
222
+ return command ? (event + " · " + command) : event;
223
+ }
224
+
225
+ function traffic_is_periodic(sig)
226
+ {
227
+ if(PERIODIC_RE.test(sig)) {
228
+ return true;
229
+ }
230
+ return (TRAFFIC_COUNTS.get(sig) || 0) >= PERIODIC_THRESHOLD;
231
+ }
232
+
233
+ function build_filter_ctx()
234
+ {
235
+ return {
236
+ out: dev_num("dev_filter_out", 1),
237
+ inc: dev_num("dev_filter_in", 1),
238
+ err: dev_num("dev_filter_err", 1),
239
+ muted: dev_muted(),
240
+ hide_periodic: dev_hide_periodic(),
241
+ search: SEARCH_TEXT,
242
+ };
243
+ }
244
+
245
+ function entry_hidden(e, ctx)
246
+ {
247
+ if(e.kind === "log") {
248
+ /* Mirrored console logs respect the search box only — not the
249
+ * in/out/err/periodic traffic filters. */
250
+ return !!(ctx.search && e.hay.indexOf(ctx.search) < 0);
251
+ }
252
+ if(e.dir === 1 && !ctx.out) {
253
+ return true;
254
+ }
255
+ if(e.dir === 2 && !ctx.inc) {
256
+ return true;
257
+ }
258
+ if(e.dir === 3 && !ctx.err) {
259
+ return true;
260
+ }
261
+ if(ctx.muted.has(e.sig)) {
262
+ return true;
263
+ }
264
+ if(ctx.hide_periodic && traffic_is_periodic(e.sig)) {
265
+ return true;
266
+ }
267
+ if(ctx.search && e.hay.indexOf(ctx.search) < 0) {
268
+ return true;
269
+ }
270
+ return false;
271
+ }
272
+
273
+
274
+ /******************************
275
+ * Style
276
+ ******************************/
277
+
25
278
 
26
279
  /************************************************************
280
+ * Inject the monitor stylesheet once. Theme-aware via
281
+ * <html data-theme>; direction-coloured (out / in / error).
282
+ ************************************************************/
283
+ function ensure_dev_style()
284
+ {
285
+ if(document.getElementById('yui-dev-style')) {
286
+ return;
287
+ }
288
+ let css = `
289
+ /* -------- layout -------- */
290
+ .YDEV_BODY {
291
+ display: flex;
292
+ flex-direction: column;
293
+ height: 100%;
294
+ min-height: 0;
295
+ box-sizing: border-box;
296
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
297
+ }
298
+ .YDEV_LOG { flex: 1 1 auto; min-height: 0; overflow: auto; padding: 6px 10px; }
299
+ .YDEV_MUTED {
300
+ display: flex; flex-wrap: wrap; gap: 6px; align-items: center;
301
+ padding: 4px 10px; border-bottom: 1px solid rgba(0,0,0,0.08); font-size: 12px;
302
+ }
303
+ .YDEV_MUTED:empty { display: none; }
304
+ .YDEV_STATS {
305
+ flex: 0 0 auto; display: flex; flex-wrap: wrap; gap: 14px;
306
+ padding: 6px 10px; border-top: 1px solid rgba(0,0,0,0.1);
307
+ background: rgba(0,0,0,0.03);
308
+ font-family: "DejaVu Sans Mono", monospace; font-size: 11px;
309
+ opacity: 0.9; font-variant-numeric: tabular-nums;
310
+ }
311
+ /* -------- control bar -------- */
312
+ .YDEV_BAR {
313
+ display: flex; flex-wrap: wrap; align-items: center; gap: 8px 12px;
314
+ padding: 8px 10px; border-bottom: 1px solid rgba(0,0,0,0.1);
315
+ background: rgba(0,0,0,0.03);
316
+ }
317
+ .YDEV_GROUP { display: inline-flex; align-items: center; gap: 5px; }
318
+ .YDEV_LABEL {
319
+ font-size: 10px; text-transform: uppercase; letter-spacing: 0.08em;
320
+ opacity: 0.5; align-self: center;
321
+ }
322
+ .YDEV_SEP { width: 1px; align-self: stretch; background: rgba(0,0,0,0.12); }
323
+ .YDEV_CHIP {
324
+ font: inherit; font-size: 12px; line-height: 1.4; padding: 3px 9px;
325
+ border: 1px solid rgba(0,0,0,0.18); border-radius: 999px;
326
+ background: transparent; color: inherit; cursor: pointer;
327
+ display: inline-flex; align-items: center; gap: 5px;
328
+ }
329
+ .YDEV_CHIP:hover { border-color: currentColor; }
330
+ .YDEV_CHIP.is-active { background: rgba(37,99,235,0.14); border-color: #2563eb; color: #2563eb; font-weight: 600; }
331
+ .YDEV_CHIP.s-out.is-active { background: rgba(37,99,235,0.16); border-color: #2563eb; color: #2563eb; }
332
+ .YDEV_CHIP.s-in.is-active { background: rgba(5,150,105,0.16); border-color: #059669; color: #059669; }
333
+ .YDEV_CHIP.s-err.is-active { background: rgba(220,38,38,0.16); border-color: #dc2626; color: #dc2626; }
334
+ .YDEV_CHIP[data-dir]:not(.is-active) { opacity: 0.4; text-decoration: line-through; }
335
+ .YDEV_CHIP[data-toggle="periodic"].is-active { background: rgba(217,119,6,0.16); border-color: #d97706; color: #b45309; font-weight: 600; }
336
+ .YDEV_SEG { display: inline-flex; border: 1px solid rgba(0,0,0,0.18); border-radius: 7px; overflow: hidden; }
337
+ .YDEV_SEG_BTN {
338
+ font: inherit; font-size: 12px; padding: 4px 10px; border: 0;
339
+ border-right: 1px solid rgba(0,0,0,0.12);
340
+ background: transparent; color: inherit; cursor: pointer;
341
+ }
342
+ .YDEV_SEG_BTN:last-child { border-right: 0; }
343
+ .YDEV_SEG_BTN.is-active { background: #2563eb; color: #fff; font-weight: 600; }
344
+ .YDEV_SEARCH {
345
+ font: inherit; font-size: 12px; padding: 4px 9px; min-width: 170px;
346
+ border: 1px solid rgba(0,0,0,0.18); border-radius: 7px;
347
+ background: transparent; color: inherit;
348
+ }
349
+ .YDEV_MUTED_CHIP {
350
+ font: inherit; font-family: "DejaVu Sans Mono", monospace; font-size: 12px;
351
+ display: inline-flex; align-items: center; gap: 6px; padding: 2px 8px;
352
+ border: 1px solid rgba(217,119,6,0.5); border-radius: 999px;
353
+ background: rgba(217,119,6,0.12); color: #b45309; cursor: pointer;
354
+ }
355
+ .YDEV_STAT.s-out { color: #2563eb; } .YDEV_STAT.s-in { color: #059669; } .YDEV_STAT.s-err { color: #dc2626; }
356
+ .YDEV_TITLE { display: flex; align-items: baseline; gap: 8px; }
357
+ .YDEV_TITLE_MAIN { font-weight: 700; }
358
+ .YDEV_TITLE_SUB { opacity: 0.7; font-size: 12px; }
359
+ /* -------- entries (shared) -------- */
360
+ .TRAFFIC_ENTRY, .TRAFFIC_LINE, .TRAFFIC_NAME {
361
+ border-left: 3px solid #94a3b8; border-radius: 3px;
362
+ font-family: "DejaVu Sans Mono", monospace, consolas, monaco; font-size: 13px;
363
+ background: rgba(0,0,0,0.02);
364
+ }
365
+ .TRAFFIC_ENTRY { margin: 6px 0; padding: 4px 8px; line-height: 1.55; }
366
+ .TRAFFIC_LINE { margin: 2px 0; padding: 2px 8px; display: flex; align-items: baseline; gap: 8px; }
367
+ .TRAFFIC_NAME { margin: 1px 0; padding: 1px 8px; display: flex; align-items: baseline; gap: 8px; background: transparent; }
368
+ .TRAFFIC_ENTRY.dir-out, .TRAFFIC_LINE.dir-out, .TRAFFIC_NAME.dir-out { border-left-color: #2563eb; }
369
+ .TRAFFIC_ENTRY.dir-in, .TRAFFIC_LINE.dir-in, .TRAFFIC_NAME.dir-in { border-left-color: #059669; }
370
+ .TRAFFIC_ENTRY.dir-err, .TRAFFIC_LINE.dir-err, .TRAFFIC_NAME.dir-err { border-left-color: #dc2626; }
371
+ .TRAFFIC_HEADER { display: flex; align-items: baseline; gap: 8px; }
372
+ .TRAFFIC_ARROW { font-weight: 700; }
373
+ .TRAFFIC_EVENT { font-weight: 700; }
374
+ .TRAFFIC_CMD { opacity: 0.75; font-weight: 600; }
375
+ .dir-out .TRAFFIC_ARROW, .dir-out .TRAFFIC_EVENT { color: #2563eb; }
376
+ .dir-in .TRAFFIC_ARROW, .dir-in .TRAFFIC_EVENT { color: #059669; }
377
+ .dir-err .TRAFFIC_ARROW, .dir-err .TRAFFIC_EVENT { color: #dc2626; }
378
+ .TRAFFIC_SUMMARY { opacity: 0.6; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; flex: 1 1 auto; min-width: 0; }
379
+ .TRAFFIC_META { margin-left: auto; opacity: 0.6; font-size: 11px; white-space: nowrap; }
380
+ .TRAFFIC_MUTE { border: 0; background: transparent; color: inherit; cursor: pointer; opacity: 0; font-size: 12px; padding: 0 2px; flex: 0 0 auto; }
381
+ .TRAFFIC_ENTRY:hover .TRAFFIC_MUTE, .TRAFFIC_LINE:hover .TRAFFIC_MUTE, .TRAFFIC_NAME:hover .TRAFFIC_MUTE { opacity: 0.5; }
382
+ .TRAFFIC_MUTE:hover { opacity: 1 !important; color: #d97706; }
383
+ .TRAFFIC_KW { margin: 2px 0 0 16px; }
384
+ .TRAFFIC_FULL { margin: 4px 0 0 16px; padding: 6px 8px; font-family: monospace; font-size: 11px; line-height: 1.4; white-space: pre-wrap; word-break: break-word; background: rgba(0,0,0,0.04); border-radius: 4px; overflow-x: auto; }
385
+ .TRAFFIC_ROW { display: flex; gap: 6px; align-items: baseline; }
386
+ .TRAFFIC_BULLET { opacity: 0.45; flex: 0 0 auto; }
387
+ .TRAFFIC_KEY { opacity: 0.85; flex: 0 0 auto; }
388
+ .TRAFFIC_VAL { word-break: break-word; }
389
+ .TRAFFIC_VAL.t-num { color: #0891b2; }
390
+ .TRAFFIC_VAL.t-bool { color: #9333ea; }
391
+ .TRAFFIC_VAL.t-null { color: #9333ea; font-style: italic; }
392
+ .TRAFFIC_VAL.t-empty { opacity: 0.5; }
393
+ .TRAFFIC_TS { opacity: 0.6; margin-left: 8px; }
394
+ details.TRAFFIC_NEST > summary { cursor: pointer; list-style: none; display: flex; gap: 6px; align-items: baseline; }
395
+ details.TRAFFIC_NEST > summary::-webkit-details-marker { display: none; }
396
+ .TRAFFIC_NEST_KEY { opacity: 0.85; }
397
+ .TRAFFIC_NEST_HINT { opacity: 0.5; margin-left: 4px; }
398
+ .YDEV_EMPTY { opacity: 0.5; font-size: 12px; padding: 18px 10px; text-align: center; }
399
+ /* -------- mirrored console logs (error/warning/info/debug/msg; the automata trace shows as debug) -------- */
400
+ .YDEV_LOGROW { display: flex; align-items: baseline; gap: 8px; margin: 1px 0; padding: 2px 8px; border-left: 3px solid #94a3b8; border-radius: 3px; font-family: "DejaVu Sans Mono", monospace, consolas, monaco; font-size: 12px; background: rgba(0,0,0,0.015); }
401
+ .YDEV_LOG_LVL { flex: 0 0 auto; text-transform: uppercase; font-size: 9px; font-weight: 700; letter-spacing: 0.04em; opacity: 0.8; min-width: 48px; }
402
+ .YDEV_LOG_TXT { flex: 1 1 auto; min-width: 0; white-space: pre-wrap; word-break: break-word; opacity: 0.9; }
403
+ .YDEV_LOG_error { border-left-color: #dc2626; } .YDEV_LOG_error .YDEV_LOG_LVL { color: #dc2626; }
404
+ .YDEV_LOG_warning { border-left-color: #d97706; } .YDEV_LOG_warning .YDEV_LOG_LVL { color: #d97706; }
405
+ .YDEV_LOG_info { border-left-color: #2563eb; } .YDEV_LOG_info .YDEV_LOG_LVL { color: #2563eb; }
406
+ .YDEV_LOG_msg { border-left-color: #0891b2; } .YDEV_LOG_msg .YDEV_LOG_LVL { color: #0891b2; }
407
+ .YDEV_LOG_debug { border-left-color: #94a3b8; } .YDEV_LOG_debug .YDEV_LOG_LVL { color: #94a3b8; } .YDEV_LOG_debug .YDEV_LOG_TXT { opacity: 0.72; }
408
+ .YDEV_LOG_json { border-left-color: #9333ea; align-items: flex-start; } .YDEV_LOG_json .YDEV_LOG_LVL { color: #9333ea; } .YDEV_LOG_json .YDEV_LOG_TXT { font-size: 11px; line-height: 1.35; opacity: 0.8; }
409
+ /* -------- dark theme -------- */
410
+ :root[data-theme="dark"] .TRAFFIC_FULL { background: rgba(255,255,255,0.05); }
411
+ :root[data-theme="dark"] .YDEV_BAR, :root[data-theme="dark"] .YDEV_STATS { background: rgba(255,255,255,0.04); }
412
+ :root[data-theme="dark"] .YDEV_SEP { background: rgba(255,255,255,0.14); }
413
+ :root[data-theme="dark"] .YDEV_CHIP, :root[data-theme="dark"] .YDEV_SEG, :root[data-theme="dark"] .YDEV_SEARCH { border-color: rgba(255,255,255,0.2); }
414
+ :root[data-theme="dark"] .TRAFFIC_ENTRY, :root[data-theme="dark"] .TRAFFIC_LINE { background: rgba(255,255,255,0.03); }
415
+ :root[data-theme="dark"] .TRAFFIC_ENTRY.dir-out, :root[data-theme="dark"] .TRAFFIC_LINE.dir-out, :root[data-theme="dark"] .TRAFFIC_NAME.dir-out { border-left-color: #60a5fa; }
416
+ :root[data-theme="dark"] .TRAFFIC_ENTRY.dir-in, :root[data-theme="dark"] .TRAFFIC_LINE.dir-in, :root[data-theme="dark"] .TRAFFIC_NAME.dir-in { border-left-color: #34d399; }
417
+ :root[data-theme="dark"] .TRAFFIC_ENTRY.dir-err, :root[data-theme="dark"] .TRAFFIC_LINE.dir-err, :root[data-theme="dark"] .TRAFFIC_NAME.dir-err { border-left-color: #f87171; }
418
+ :root[data-theme="dark"] .dir-out .TRAFFIC_ARROW, :root[data-theme="dark"] .dir-out .TRAFFIC_EVENT { color: #60a5fa; }
419
+ :root[data-theme="dark"] .dir-in .TRAFFIC_ARROW, :root[data-theme="dark"] .dir-in .TRAFFIC_EVENT { color: #34d399; }
420
+ :root[data-theme="dark"] .dir-err .TRAFFIC_ARROW, :root[data-theme="dark"] .dir-err .TRAFFIC_EVENT { color: #f87171; }
421
+ :root[data-theme="dark"] .YDEV_STAT.s-out { color: #60a5fa; } :root[data-theme="dark"] .YDEV_STAT.s-in { color: #34d399; } :root[data-theme="dark"] .YDEV_STAT.s-err { color: #f87171; }
422
+ :root[data-theme="dark"] .TRAFFIC_VAL.t-num { color: #22d3ee; }
423
+ :root[data-theme="dark"] .TRAFFIC_VAL.t-bool, :root[data-theme="dark"] .TRAFFIC_VAL.t-null { color: #c084fc; }
424
+ :root[data-theme="dark"] .YDEV_CHIP.is-active { background: rgba(96,165,250,0.2); border-color: #60a5fa; color: #93c5fd; }
425
+ :root[data-theme="dark"] .YDEV_SEG_BTN.is-active { background: #2563eb; color: #fff; }
426
+ :root[data-theme="dark"] .YDEV_CHIP[data-toggle="periodic"].is-active { background: rgba(217,119,6,0.24); border-color: #f59e0b; color: #fbbf24; }
427
+ :root[data-theme="dark"] .YDEV_MUTED_CHIP { border-color: rgba(245,158,11,0.5); background: rgba(245,158,11,0.16); color: #fbbf24; }
428
+ `;
429
+ let $style = document.createElement('style');
430
+ $style.id = 'yui-dev-style';
431
+ $style.textContent = css;
432
+ document.head.appendChild($style);
433
+ }
434
+
435
+
436
+ /******************************
437
+ * kw bullet rendering
438
+ ******************************/
439
+
440
+
441
+ /************************************************************
442
+ * One scalar field as a bullet row: `• key: value`.
443
+ * Type-coloured; long strings clipped (full text on hover);
444
+ * timestamp fields get an ISO annotation.
445
+ ************************************************************/
446
+ function traffic_scalar_row(key, value)
447
+ {
448
+ let cls;
449
+ let text;
450
+ if(value === null) {
451
+ cls = "t-null";
452
+ text = "null";
453
+ } else if(typeof value === "boolean") {
454
+ cls = "t-bool";
455
+ text = value ? "true" : "false";
456
+ } else if(typeof value === "number") {
457
+ cls = "t-num";
458
+ text = String(value);
459
+ } else {
460
+ cls = "t-str";
461
+ text = String(value);
462
+ }
463
+
464
+ let full = text;
465
+ if(text.length > 200) {
466
+ text = text.slice(0, 200) + "…";
467
+ }
468
+
469
+ let val_children = [
470
+ ['span', {class: 'TRAFFIC_VAL ' + cls, title: full}, text],
471
+ ];
472
+ if((key in TRAFFIC_TS_FIELDS) && typeof value === "number") {
473
+ let iso = traffic_iso(value);
474
+ if(iso) {
475
+ val_children.push(['span', {class: 'TRAFFIC_TS'}, iso]);
476
+ }
477
+ }
478
+
479
+ return ['div', {class: 'TRAFFIC_ROW'}, [
480
+ ['span', {class: 'TRAFFIC_BULLET'}, '•'],
481
+ ['span', {class: 'TRAFFIC_KEY'}, key + ':'],
482
+ ['span', {}, val_children],
483
+ ]];
484
+ }
485
+
486
+ /************************************************************
487
+ * One field of any type. Scalars → a bullet row; objects and
488
+ * arrays → a collapsed <details> so metadata / nested payloads
489
+ * stay folded. Empty containers render inline.
490
+ ************************************************************/
491
+ function traffic_value_node(key, value)
492
+ {
493
+ if(value === null || typeof value !== "object") {
494
+ return traffic_scalar_row(key, value);
495
+ }
496
+
497
+ let is_arr = Array.isArray(value);
498
+ let count = is_arr ? value.length : Object.keys(value).length;
499
+ if(count === 0) {
500
+ return ['div', {class: 'TRAFFIC_ROW'}, [
501
+ ['span', {class: 'TRAFFIC_BULLET'}, '•'],
502
+ ['span', {class: 'TRAFFIC_KEY'}, key + ':'],
503
+ ['span', {class: 'TRAFFIC_VAL t-empty'}, is_arr ? '[ ]' : '{ }'],
504
+ ]];
505
+ }
506
+
507
+ let hint = is_arr ? `[${count}]` : `{${count}}`;
508
+ return ['details', {class: 'TRAFFIC_NEST'}, [
509
+ ['summary', {}, [
510
+ ['span', {class: 'TRAFFIC_BULLET'}, '▸'],
511
+ ['span', {class: 'TRAFFIC_NEST_KEY'}, key],
512
+ ['span', {class: 'TRAFFIC_NEST_HINT'}, hint],
513
+ ]],
514
+ ['div', {class: 'TRAFFIC_KW'}, traffic_bullets(value)],
515
+ ]];
516
+ }
517
+
518
+ /************************************************************
519
+ * A whole object/array → an array of bullet nodes.
520
+ ************************************************************/
521
+ function traffic_bullets(obj)
522
+ {
523
+ let out = [];
524
+ if(Array.isArray(obj)) {
525
+ for(let i = 0; i < obj.length; i++) {
526
+ out.push(traffic_value_node(String(i), obj[i]));
527
+ }
528
+ } else {
529
+ for(let k of Object.keys(obj)) {
530
+ out.push(traffic_value_node(k, obj[k]));
531
+ }
532
+ }
533
+ return out;
534
+ }
535
+
536
+
537
+ /******************************
538
+ * Entry rendering (per view)
539
+ ******************************/
540
+
541
+
542
+ /* A small mute affordance that silences this signature (persistent). */
543
+ function mute_button(sig)
544
+ {
545
+ return ['button', {class: 'TRAFFIC_MUTE', type: 'button', title: 'Mute ' + sig}, '⊘', {
546
+ click: (ev) => {
547
+ ev.stopPropagation();
548
+ ev.preventDefault();
549
+ mute_signature(sig);
550
+ }
551
+ }];
552
+ }
553
+
554
+ function event_spans(e)
555
+ {
556
+ let spans = [['span', {class: 'TRAFFIC_ARROW'}, dir_arrow(e.dir)],
557
+ ['span', {class: 'TRAFFIC_EVENT'}, e.event]];
558
+ if(e.command) {
559
+ spans.push(['span', {class: 'TRAFFIC_CMD'}, e.command]);
560
+ }
561
+ return spans;
562
+ }
563
+
564
+ /* One-line summary of the kw for the compact view. */
565
+ function compact_summary(kw)
566
+ {
567
+ if(!kw) {
568
+ return "";
569
+ }
570
+ let parts = [];
571
+ if("result" in kw) {
572
+ parts.push("result=" + traffic_scalar_text(kw.result));
573
+ }
574
+ if(typeof kw.comment === "string" && kw.comment) {
575
+ parts.push(traffic_clip(kw.comment, 80));
576
+ }
577
+ if(!parts.length) {
578
+ let n = 0;
579
+ for(let k of Object.keys(kw)) {
580
+ if(k === "command") {
581
+ continue;
582
+ }
583
+ let v = kw[k];
584
+ if(v === null || typeof v !== "object") {
585
+ parts.push(k + "=" + traffic_scalar_text(v));
586
+ if(++n >= 3) {
587
+ break;
588
+ }
589
+ }
590
+ }
591
+ }
592
+ return traffic_clip(parts.join(" · "), 140);
593
+ }
594
+
595
+ function render_detailed(e)
596
+ {
597
+ let head = event_spans(e);
598
+ head.push(mute_button(e.sig));
599
+ head.push(['span', {class: 'TRAFFIC_META'}, `${traffic_size(e.size)} · ${e.ts}`]);
600
+
601
+ let children = [['div', {class: 'TRAFFIC_HEADER'}, head]];
602
+ let kw = e.kw;
603
+ if(kw && Object.keys(kw).length > 0) {
604
+ children.push(['div', {class: 'TRAFFIC_KW'}, traffic_bullets(kw)]);
605
+ } else if(!kw) {
606
+ children.push(['div', {class: 'TRAFFIC_KW'}, traffic_bullets(e.jn)]);
607
+ }
608
+ return createElement2(['div', {class: 'TRAFFIC_ENTRY ' + dir_class(e.dir), title: e.title}, children]);
609
+ }
610
+
611
+ /* Whether an Expanded-view section is shown (persisted toggles). schema
612
+ * defaults OFF (rarely wanted); data + metadata default as noted. */
613
+ function full_show(key)
614
+ {
615
+ let def = (key === "dev_full_data") ? 1 : 0;
616
+ return !!dev_num(key, def);
617
+ }
618
+
619
+ /* Filter a payload's top-level keys for the Expanded view: the `schema`
620
+ * and `data` keys and the `__…__` metadata markers are each shown only
621
+ * when their toggle is on; everything else is always kept. */
622
+ function full_sections(payload)
623
+ {
624
+ if(!payload || typeof payload !== "object" || Array.isArray(payload)) {
625
+ return payload;
626
+ }
627
+ let show_schema = full_show("dev_full_schema");
628
+ let show_data = full_show("dev_full_data");
629
+ let show_meta = full_show("dev_full_meta");
630
+ let out = {};
631
+ for(let k of Object.keys(payload)) {
632
+ if(k === "schema") {
633
+ if(show_schema) { out[k] = payload[k]; }
634
+ continue;
635
+ }
636
+ if(k === "data") {
637
+ if(show_data) { out[k] = payload[k]; }
638
+ continue;
639
+ }
640
+ if(/^__.*__$/.test(k)) {
641
+ if(show_meta) { out[k] = payload[k]; }
642
+ continue;
643
+ }
644
+ out[k] = payload[k];
645
+ }
646
+ return out;
647
+ }
648
+
649
+ /* Full view: the message payload pretty-printed and fully expanded
650
+ * (nothing folded) — for reading / copying a whole payload. The
651
+ * schema / data / metadata sections are toggled by the Expand chips. */
652
+ function render_full(e)
653
+ {
654
+ let head = event_spans(e);
655
+ head.push(mute_button(e.sig));
656
+ head.push(['span', {class: 'TRAFFIC_META'}, `${traffic_size(e.size)} · ${e.ts}`]);
657
+
658
+ let payload = full_sections(e.kw ? e.kw : e.jn);
659
+ let text;
660
+ try {
661
+ text = JSON.stringify(payload, null, 2);
662
+ } catch(err) {
663
+ text = String(payload);
664
+ }
665
+ let children = [
666
+ ['div', {class: 'TRAFFIC_HEADER'}, head],
667
+ ['pre', {class: 'TRAFFIC_FULL'}, text],
668
+ ];
669
+ return createElement2(['div', {class: 'TRAFFIC_ENTRY ' + dir_class(e.dir), title: e.title}, children]);
670
+ }
671
+
672
+ function render_compact(e)
673
+ {
674
+ let kids = event_spans(e);
675
+ kids.push(['span', {class: 'TRAFFIC_SUMMARY'}, compact_summary(e.kw)]);
676
+ kids.push(mute_button(e.sig));
677
+ kids.push(['span', {class: 'TRAFFIC_META'}, `${traffic_size(e.size)} · ${e.ts}`]);
678
+ return createElement2(['div', {class: 'TRAFFIC_LINE ' + dir_class(e.dir), title: e.title}, kids]);
679
+ }
680
+
681
+ function render_name(e)
682
+ {
683
+ let kids = event_spans(e);
684
+ kids.push(mute_button(e.sig));
685
+ kids.push(['span', {class: 'TRAFFIC_META'}, e.ts]);
686
+ return createElement2(['div', {class: 'TRAFFIC_NAME ' + dir_class(e.dir), title: e.title}, kids]);
687
+ }
688
+
689
+ /* A mirrored framework log line (error/warning/info/debug/msg). */
690
+ function render_log(e)
691
+ {
692
+ return createElement2(
693
+ ['div', {class: 'YDEV_LOGROW YDEV_LOG_' + e.level, title: e.level}, [
694
+ ['span', {class: 'YDEV_LOG_LVL'}, e.level],
695
+ ['span', {class: 'YDEV_LOG_TXT'}, e.text],
696
+ ['span', {class: 'TRAFFIC_META'}, e.ts],
697
+ ]]
698
+ );
699
+ }
700
+
701
+ function render_entry(e)
702
+ {
703
+ if(e.kind === "log") {
704
+ return render_log(e);
705
+ }
706
+ let view = dev_view();
707
+ if(view === "full") {
708
+ return render_full(e);
709
+ }
710
+ if(view === "compact") {
711
+ return render_compact(e);
712
+ }
713
+ if(view === "name") {
714
+ return render_name(e);
715
+ }
716
+ return render_detailed(e);
717
+ }
718
+
719
+
720
+ /******************************
721
+ * Log painting
722
+ ******************************/
723
+
724
+
725
+ function clear_traffic()
726
+ {
727
+ TRAFFIC_LOG.length = 0;
728
+ TRAFFIC_COUNTS.clear();
729
+ let logger = document.getElementById('developer-traffic-logger');
730
+ if(logger) {
731
+ logger.replaceChildren();
732
+ }
733
+ update_stats();
734
+ }
735
+
736
+ /* Full repaint from the buffer (view / filter changes, reopen). */
737
+ function rerender_all()
738
+ {
739
+ let logger = document.getElementById('developer-traffic-logger');
740
+ if(!logger) {
741
+ return;
742
+ }
743
+ ensure_dev_style();
744
+ let ctx = build_filter_ctx();
745
+ let frag = document.createDocumentFragment();
746
+ let shown = 0;
747
+ for(let e of TRAFFIC_LOG) {
748
+ e.$node = null;
749
+ if(!entry_hidden(e, ctx)) {
750
+ let node = render_entry(e);
751
+ e.$node = node;
752
+ frag.appendChild(node);
753
+ shown++;
754
+ }
755
+ }
756
+ logger.replaceChildren(frag);
757
+ if(shown === 0) {
758
+ let $empty = document.createElement('div');
759
+ $empty.className = 'YDEV_EMPTY';
760
+ $empty.textContent = TRAFFIC_LOG.length
761
+ ? "No messages match the current filters."
762
+ : "Waiting for activity — console logs and errors show automatically; enable Traffic or Automata for more.";
763
+ logger.appendChild($empty);
764
+ }
765
+ logger.scrollTop = logger.scrollHeight;
766
+ update_stats();
767
+ }
768
+
769
+ /* Live counters in the status strip. */
770
+ function update_stats()
771
+ {
772
+ let $s = document.getElementById('ydev-stats');
773
+ if(!$s) {
774
+ return;
775
+ }
776
+ let ctx = build_filter_ctx();
777
+ let total = TRAFFIC_LOG.length;
778
+ let shown = 0, out = 0, inc = 0, err = 0, hidden = 0, bytes = 0;
779
+ for(let e of TRAFFIC_LOG) {
780
+ bytes += e.size || 0;
781
+ if(e.dir === 1) {
782
+ out++;
783
+ } else if(e.dir === 2) {
784
+ inc++;
785
+ } else if(e.dir === 3) {
786
+ err++;
787
+ }
788
+ if(entry_hidden(e, ctx)) {
789
+ hidden++;
790
+ } else {
791
+ shown++;
792
+ }
793
+ }
794
+ $s.replaceChildren();
795
+ let cells = [
796
+ [`${shown}/${total} shown`, ''],
797
+ [`⇢ ${out}`, 's-out'],
798
+ [`⇠ ${inc}`, 's-in'],
799
+ [`⚠ ${err}`, 's-err'],
800
+ [`⊘ ${hidden} hidden`, ''],
801
+ [`${traffic_size(bytes)}`, ''],
802
+ ];
803
+ cells.forEach(([text, cls]) => {
804
+ let d = document.createElement('span');
805
+ d.className = 'YDEV_STAT' + (cls ? ' ' + cls : '');
806
+ d.textContent = text;
807
+ $s.appendChild(d);
808
+ });
809
+ }
810
+
811
+ /************************************************************
812
+ * Append one inter-event message. Kept in a bounded buffer so
813
+ * view/filter switches repaint from memory. Shared by the legacy
814
+ * C_YUI_WINDOW (setup_dev) and the modal (build_dev_panel).
27
815
  *
816
+ * direction: 1 outgoing (⇢), 2 incoming (⇠), 3 error (⚠).
817
+ * With no logger mounted, fall back to a console dump.
28
818
  ************************************************************/
29
819
  function info_traffic(title, msg, direction, size)
30
820
  {
31
- // Render into the traffic logger if it is present (old shell:
32
- // inside C_YUI_WINDOW; new shell: inside the build_dev_panel()
33
- // modal). Otherwise just dump to the console.
34
- if(!document.getElementById('developer-traffic-logger')) {
821
+ let logger = document.getElementById('developer-traffic-logger');
822
+ if(!logger) {
35
823
  trace_json(msg);
36
824
  return;
37
825
  }
@@ -40,104 +828,152 @@ function info_traffic(title, msg, direction, size)
40
828
  size = 0;
41
829
  }
42
830
 
43
- let jn_msg;
831
+ let jn;
44
832
  try {
45
- if(is_string(msg)) {
46
- jn_msg = JSON.parse(msg);
47
- } else {
48
- jn_msg = JSON.parse(JSON.stringify(msg));
49
- }
50
- } catch (e) {
833
+ jn = is_string(msg) ? JSON.parse(msg) : JSON.parse(JSON.stringify(msg));
834
+ } catch(e) {
51
835
  return;
52
836
  }
53
837
 
54
- let content = {
55
- text: undefined,
56
- json: jn_msg
57
- };
838
+ ensure_dev_style();
58
839
 
59
- function formatCurrentTime() {
60
- let now = new Date();
840
+ let event = (jn && jn.event) ? String(jn.event) : "(no event)";
841
+ let kw = (jn && jn.kw && typeof jn.kw === "object") ? jn.kw : null;
842
+ let command = (kw && typeof kw.command === "string") ? kw.command : "";
843
+ let sig = traffic_signature(event, command);
844
+
845
+ let hay = "";
846
+ try {
847
+ hay = (event + " " + command + " " + (kw ? JSON.stringify(kw) : "")).toLowerCase();
848
+ } catch(e) {
849
+ hay = (event + " " + command).toLowerCase();
850
+ }
61
851
 
62
- // Pad single digit numbers with a leading zero
63
- let pad = (num, size) => ('000' + num).slice(size * -1);
852
+ let entry = {
853
+ title: title || "", event: event, command: command, sig: sig,
854
+ dir: direction, size: size, ts: traffic_now(),
855
+ kw: kw, jn: jn, hay: hay, $node: null,
856
+ };
64
857
 
65
- let hours = pad(now.getHours(), 2);
66
- let minutes = pad(now.getMinutes(), 2);
67
- let seconds = pad(now.getSeconds(), 2);
68
- let milliseconds = pad(now.getMilliseconds(), 4);
858
+ TRAFFIC_LOG.push(entry);
859
+ TRAFFIC_COUNTS.set(sig, (TRAFFIC_COUNTS.get(sig) || 0) + 1);
69
860
 
70
- // Format to hh:mm:ss .SSSS
71
- return `${hours}:${minutes}:${seconds} .${milliseconds}`;
72
- }
861
+ /* When a signature just crosses the "recurring" threshold and
862
+ * the periodic filter is on, its earlier entries must disappear
863
+ * too — a full repaint is the correct, simple answer. */
864
+ let crossed = dev_hide_periodic() && (TRAFFIC_COUNTS.get(sig) === PERIODIC_THRESHOLD);
73
865
 
74
- let element = document.getElementById('developer-traffic-logger');
75
- if(element) {
76
- let style = "background-color:#3883FA;";
77
- if(direction === 2) {
78
- style += "color:yellow;";
79
- } else if(direction === 3) {
80
- style += "color:red;";
866
+ if(TRAFFIC_LOG.length > TRAFFIC_MAX) {
867
+ let old = TRAFFIC_LOG.shift();
868
+ let c = (TRAFFIC_COUNTS.get(old.sig) || 0) - 1;
869
+ if(c <= 0) {
870
+ TRAFFIC_COUNTS.delete(old.sig);
81
871
  } else {
82
- style += "color:white;";
872
+ TRAFFIC_COUNTS.set(old.sig, c);
83
873
  }
874
+ if(old.$node && old.$node.parentNode) {
875
+ old.$node.parentNode.removeChild(old.$node);
876
+ }
877
+ }
84
878
 
85
- let $item = createElement2(
86
- ['div', {class: 'mt-4'}, [
87
- ['div', {class: 'is-flex with-border is-justify-content-space-between', style: style}, [
88
- ['div', {class: 'p-1'}, title],
89
- ['div', {class: 'p-1'}, `(${size} bytes)`],
90
- ['div', {class: 'p-1'}, formatCurrentTime()]
91
- ]],
92
- ['div', {class: 'x-jsoneditor jse-theme-dark'}, []],
93
- ]]
94
- );
95
- let $target = $item.querySelector('.x-jsoneditor');
96
- let font_family = "DejaVu Sans Mono, monospace, consolas, monaco";
97
- let sz = 15;
98
- $target.style.setProperty('--jse-font-size-mono', sz + 'px');
99
- $target.style.setProperty('--jse-font-family-mono', font_family);
100
-
101
- document.getElementById("developer-traffic-logger").appendChild($item);
102
-
103
- let editor = new JSONEditor({
104
- target: $target,
105
- props: {
106
- content: content,
107
- readOnly: true,
108
- timestampTag: function ({field, value, path}) {
109
- if (field === '__t__' || field === '__tm__' || field === 'tm' ||
110
- field === 'from_t' || field === 'to_t' || field === 't' ||
111
- field === 'from_tm' || field === 'to_tm' || field === 'time'
112
- ) {
113
- return true;
114
- }
115
- return false;
116
- },
117
- timestampFormat: function ({field, value, path}) {
118
- if (field === '__t__' || field === '__tm__' || field === 'tm' ||
119
- field === 'from_t' || field === 'to_t' || field === 't' ||
120
- field === 'from_tm' || field === 'to_tm' || field === 'time'
121
- ) {
122
- return new Date(value * 1000).toISOString();
123
- }
124
- return null;
125
- },
126
- }
127
- });
128
- editor.expand(path => path.length < 2);
129
-
130
- element.scrollIntoView({block: "end"});
879
+ if(crossed) {
880
+ rerender_all();
881
+ } else if(!entry_hidden(entry, build_filter_ctx())) {
882
+ let node = render_entry(entry);
883
+ entry.$node = node;
884
+ /* Drop the "no traffic yet" placeholder before the first row. */
885
+ let ph = logger.querySelector('.YDEV_EMPTY');
886
+ if(ph) {
887
+ ph.remove();
888
+ }
889
+ logger.appendChild(node);
890
+ node.scrollIntoView({block: "end"});
131
891
  }
892
+ update_stats();
132
893
  }
133
894
 
895
+
896
+ /* Re-entrancy guard: rendering a captured log line must not itself capture
897
+ * the logs it emits (that would recurse). */
898
+ let __in_info_log__ = false;
899
+
134
900
  /************************************************************
135
- *
901
+ * Mirror one framework log line into the monitor, alongside the
902
+ * inter-event traffic. level ∈ error|warning|info|debug|msg — the automata
903
+ * (FSM) trace arrives here too, as `debug`. No-op while the window is closed
904
+ * (the line already went to the browser console).
136
905
  ************************************************************/
906
+ function info_log(level, msg, hora)
907
+ {
908
+ if(__in_info_log__) {
909
+ return;
910
+ }
911
+ let logger = document.getElementById('developer-traffic-logger');
912
+ if(!logger) {
913
+ return;
914
+ }
915
+ __in_info_log__ = true;
916
+ try {
917
+ ensure_dev_style();
918
+ let lvl = level || "debug";
919
+ let text;
920
+ if(lvl === "json") {
921
+ try {
922
+ text = JSON.stringify(msg, null, 2);
923
+ } catch(e) {
924
+ text = String(msg);
925
+ }
926
+ if(text.length > 4000) {
927
+ text = text.slice(0, 4000) + "\n…(truncated)";
928
+ }
929
+ } else {
930
+ text = is_string(msg) ? msg : String(msg);
931
+ }
932
+ let entry = {
933
+ kind: "log", level: lvl, text: text,
934
+ dir: 0, size: 0, ts: traffic_now(),
935
+ sig: "log:" + lvl, hay: (lvl + " " + text).toLowerCase(), $node: null,
936
+ };
937
+ TRAFFIC_LOG.push(entry);
938
+ if(TRAFFIC_LOG.length > TRAFFIC_MAX) {
939
+ let old = TRAFFIC_LOG.shift();
940
+ if(old.kind !== "log") {
941
+ let c = (TRAFFIC_COUNTS.get(old.sig) || 0) - 1;
942
+ if(c <= 0) {
943
+ TRAFFIC_COUNTS.delete(old.sig);
944
+ } else {
945
+ TRAFFIC_COUNTS.set(old.sig, c);
946
+ }
947
+ }
948
+ if(old.$node && old.$node.parentNode) {
949
+ old.$node.parentNode.removeChild(old.$node);
950
+ }
951
+ }
952
+ if(!entry_hidden(entry, build_filter_ctx())) {
953
+ let node = render_entry(entry);
954
+ entry.$node = node;
955
+ let ph = logger.querySelector('.YDEV_EMPTY');
956
+ if(ph) {
957
+ ph.remove();
958
+ }
959
+ logger.appendChild(node);
960
+ node.scrollIntoView({block: "end"});
961
+ }
962
+ update_stats();
963
+ } finally {
964
+ __in_info_log__ = false;
965
+ }
966
+ }
967
+
968
+
969
+ /******************************
970
+ * Trace toggles
971
+ ******************************/
972
+
973
+
137
974
  function trace_traffic()
138
975
  {
139
- let v = kw_get_local_storage_value("trace_traffic");
140
- v = Number(v);
976
+ let v = Number(kw_get_local_storage_value("trace_traffic"));
141
977
  if(v) {
142
978
  gobj_write_attr(gobj_yuno(), "trace_inter_event", false);
143
979
  v = 0;
@@ -147,147 +983,348 @@ function trace_traffic()
147
983
  v = 1;
148
984
  }
149
985
  kw_set_local_storage_value("trace_traffic", v);
150
- info_user();
986
+ refresh_dev_chrome();
151
987
  }
152
988
 
153
- /************************************************************
154
- *
155
- ************************************************************/
156
989
  function trace_automata()
157
990
  {
158
- let v = kw_get_local_storage_value("trace_automata");
159
- v = Number(v);
160
- if(v===0) {
991
+ let v = Number(kw_get_local_storage_value("trace_automata"));
992
+ if(v === 0) {
161
993
  v = 1;
162
- } else if(v===1) {
994
+ } else if(v === 1) {
163
995
  v = 2;
164
996
  } else {
165
997
  v = 0;
166
998
  }
167
999
  gobj_write_attr(gobj_yuno(), "tracing", v);
168
1000
  kw_set_local_storage_value("trace_automata", v);
169
- info_user();
1001
+ refresh_dev_chrome();
170
1002
  }
171
1003
 
172
- /************************************************************
173
- *
174
- ************************************************************/
175
1004
  function trace_creation()
176
1005
  {
177
- let v = kw_get_local_storage_value("trace_creation");
178
- v = Number(v);
179
- if(v===0) {
180
- v = 1;
181
- } else {
182
- v = 0;
183
- }
1006
+ let v = Number(kw_get_local_storage_value("trace_creation"));
1007
+ v = v === 0 ? 1 : 0;
184
1008
  gobj_write_attr(gobj_yuno(), "trace_creation", v);
185
1009
  kw_set_local_storage_value("trace_creation", v);
186
- info_user();
1010
+ refresh_dev_chrome();
187
1011
  }
188
1012
 
189
- /************************************************************
190
- *
191
- ************************************************************/
192
1013
  function trace_start_stop()
193
1014
  {
194
- let v = kw_get_local_storage_value("trace_start_stop");
195
- v = Number(v);
196
- if(v===0) {
197
- v = 1;
198
- } else {
199
- v = 0;
200
- }
1015
+ let v = Number(kw_get_local_storage_value("trace_start_stop"));
1016
+ v = v === 0 ? 1 : 0;
201
1017
  gobj_write_attr(gobj_yuno(), "trace_start_stop", v);
202
1018
  kw_set_local_storage_value("trace_start_stop", v);
203
- info_user();
1019
+ refresh_dev_chrome();
204
1020
  }
205
1021
 
206
- /************************************************************
207
- *
208
- ************************************************************/
209
1022
  function trace_subscriptions()
210
1023
  {
211
- let v = kw_get_local_storage_value("trace_subscriptions");
212
- v = Number(v);
213
- if(v===0) {
214
- v = 1;
215
- } else {
216
- v = 0;
217
- }
1024
+ let v = Number(kw_get_local_storage_value("trace_subscriptions"));
1025
+ v = v === 0 ? 1 : 0;
218
1026
  gobj_write_attr(gobj_yuno(), "trace_subscriptions", v);
219
1027
  kw_set_local_storage_value("trace_subscriptions", v);
220
- info_user();
1028
+ refresh_dev_chrome();
221
1029
  }
222
1030
 
223
- /************************************************************
224
- *
225
- ************************************************************/
226
1031
  function trace_i18n()
227
1032
  {
228
- let v = kw_get_local_storage_value("trace_i18n");
229
- v = Number(v);
230
- if(v===0) {
231
- v = 1;
232
- } else {
233
- v = 0;
234
- }
235
- i18next.options.debug = v?true:false;
1033
+ let v = Number(kw_get_local_storage_value("trace_i18n"));
1034
+ v = v === 0 ? 1 : 0;
1035
+ i18next.options.debug = v ? true : false;
236
1036
  kw_set_local_storage_value("trace_i18n", v);
237
- info_user();
1037
+ refresh_dev_chrome();
238
1038
  }
239
1039
 
240
- /************************************************************
241
- *
242
- ************************************************************/
243
1040
  function set_no_poll()
244
1041
  {
245
- let v = kw_get_local_storage_value("no_poll");
246
- v = Number(v);
247
- if(v) {
248
- v = 0;
249
- } else {
250
- v = 1;
251
- }
1042
+ let v = Number(kw_get_local_storage_value("no_poll"));
1043
+ v = v ? 0 : 1;
252
1044
  gobj_write_attr(gobj_yuno(), "no_poll", v);
253
1045
  kw_set_local_storage_value("no_poll", v);
254
- info_user();
1046
+ refresh_dev_chrome();
255
1047
  }
256
1048
 
257
- /************************************************************
258
- *
259
- ************************************************************/
260
- function info_user()
1049
+
1050
+ /******************************
1051
+ * Chrome (controls)
1052
+ ******************************/
1053
+
1054
+
1055
+ /* Sync every control's visual state from persisted prefs, plus the
1056
+ * muted-events row and the stats strip. Idempotent; null-guarded so
1057
+ * it is safe to call whether or not the window is mounted. */
1058
+ function refresh_dev_chrome()
261
1059
  {
262
- let $info = document.getElementById("developer-window-info");
1060
+ document.querySelectorAll('.YDEV_CHIP[data-trace]').forEach(($b) => {
1061
+ let key = $b.getAttribute('data-trace');
1062
+ let label = $b.getAttribute('data-label') || '';
1063
+ let v = dev_num(key, 0);
1064
+ $b.textContent = (key === "trace_automata" && v > 0) ? (label + " " + v) : label;
1065
+ $b.classList.toggle('is-active', v > 0);
1066
+ });
263
1067
 
264
- let traffic = Number(kw_get_local_storage_value("trace_traffic", 0, false));
265
- let trace = Number(kw_get_local_storage_value("trace_automata", 0, false));
266
- let creation = Number(kw_get_local_storage_value("trace_creation", 0, false));
267
- let start_stop = Number(kw_get_local_storage_value("trace_start_stop", 0, false));
268
- let subscriptions = Number(kw_get_local_storage_value("trace_subscriptions", 0, false));
1068
+ let view = dev_view();
1069
+ document.querySelectorAll('.YDEV_SEG_BTN[data-view]').forEach(($b) => {
1070
+ $b.classList.toggle('is-active', $b.getAttribute('data-view') === view);
1071
+ });
1072
+
1073
+ /* The Expand section toggles only apply to the Expanded view — show
1074
+ * the group only there, and reflect each toggle's persisted state. */
1075
+ let $eg = document.getElementById('ydev-expand-grp');
1076
+ if($eg) {
1077
+ $eg.style.display = (view === 'full') ? '' : 'none';
1078
+ }
1079
+ document.querySelectorAll('.YDEV_CHIP[data-expand]').forEach(($b) => {
1080
+ $b.classList.toggle('is-active', full_show($b.getAttribute('data-expand')));
1081
+ });
269
1082
 
270
- let i18n = Number(kw_get_local_storage_value("trace_i18n", 0, false));
271
- let no_poll = Number(kw_get_local_storage_value("no_poll", 0, false));
272
-
273
- // Code repeated
274
- // Build with DOM instead of innerHTML to prevent any XSS via localStorage values
275
- $info.replaceChildren();
276
- [
277
- `Automata: ${trace}`,
278
- `Creation: ${creation}`,
279
- `Start/Stop: ${start_stop}`,
280
- `Subscriptions: ${subscriptions}`,
281
- `I18n: ${i18n}`,
282
- `Traffic: ${traffic}`,
283
- `No poll: ${no_poll}`,
284
- ].forEach(text => {
285
- const div = document.createElement('div');
286
- div.textContent = text;
287
- $info.appendChild(div);
1083
+ document.querySelectorAll('.YDEV_CHIP[data-dir]').forEach(($b) => {
1084
+ $b.classList.toggle('is-active', !!dev_num($b.getAttribute('data-dir'), 1));
288
1085
  });
1086
+
1087
+ document.querySelectorAll('.YDEV_CHIP[data-toggle="periodic"]').forEach(($b) => {
1088
+ $b.classList.toggle('is-active', dev_hide_periodic());
1089
+ });
1090
+
1091
+ document.querySelectorAll('.YDEV_CHIP[data-toggle="automata-simple"]').forEach(($b) => {
1092
+ $b.classList.toggle('is-active', !!dev_num('dev_automata_simple', 0));
1093
+ });
1094
+
1095
+ let $m = document.getElementById('ydev-muted');
1096
+ if($m) {
1097
+ $m.replaceChildren();
1098
+ let set = dev_muted();
1099
+ if(set.size) {
1100
+ let $lbl = document.createElement('span');
1101
+ $lbl.className = 'YDEV_LABEL';
1102
+ $lbl.textContent = 'Muted';
1103
+ $m.appendChild($lbl);
1104
+ set.forEach((sig) => {
1105
+ $m.appendChild(createElement2(
1106
+ ['button', {class: 'YDEV_MUTED_CHIP', type: 'button', title: 'Unmute'},
1107
+ '⊘ ' + sig + ' ✕', {
1108
+ click: (ev) => {
1109
+ ev.stopPropagation();
1110
+ unmute_signature(sig);
1111
+ }
1112
+ }]
1113
+ ));
1114
+ });
1115
+ }
1116
+ }
1117
+
1118
+ update_stats();
1119
+ }
1120
+
1121
+ /* Serialize the currently-visible (filtered) traffic to plain text:
1122
+ * one header line per entry (time · direction · title · event/command)
1123
+ * followed by its pretty-printed payload. Honours the active filters and
1124
+ * search so the copy matches exactly what is on screen. */
1125
+ function traffic_to_text()
1126
+ {
1127
+ let ctx = build_filter_ctx();
1128
+ let out = [];
1129
+ for(let e of TRAFFIC_LOG) {
1130
+ if(entry_hidden(e, ctx)) {
1131
+ continue;
1132
+ }
1133
+ let head = `${e.ts} ${dir_arrow(e.dir)} ` +
1134
+ `${e.title ? "[" + e.title + "] " : ""}${e.event}` +
1135
+ `${e.command ? " " + e.command : ""}`;
1136
+ out.push(head);
1137
+ let payload = e.kw ? e.kw : e.jn;
1138
+ try {
1139
+ out.push(JSON.stringify(payload, null, 2));
1140
+ } catch(err) {
1141
+ out.push(String(payload));
1142
+ }
1143
+ out.push("");
1144
+ }
1145
+ return out.join("\n");
1146
+ }
1147
+
1148
+ /* Copy text to the clipboard, with a fallback for insecure contexts. */
1149
+ function dev_copy_text(text)
1150
+ {
1151
+ if(navigator.clipboard && navigator.clipboard.writeText) {
1152
+ return navigator.clipboard.writeText(text).catch(() => {
1153
+ dev_fallback_copy(text);
1154
+ });
1155
+ }
1156
+ dev_fallback_copy(text);
1157
+ return Promise.resolve();
1158
+ }
1159
+
1160
+ function dev_fallback_copy(text)
1161
+ {
1162
+ let ta = document.createElement("textarea");
1163
+ ta.value = text;
1164
+ ta.style.position = "fixed";
1165
+ ta.style.left = "-9999px";
1166
+ document.body.appendChild(ta);
1167
+ ta.select();
1168
+ try {
1169
+ document.execCommand("copy");
1170
+ } catch(e) {
1171
+ /* nothing else to try */
1172
+ }
1173
+ document.body.removeChild(ta);
1174
+ }
1175
+
1176
+ /* The control bar: trace toggles, view selector, direction /
1177
+ * periodic filters, free-text search, copy, clear. Returns an element. */
1178
+ function build_control_bar()
1179
+ {
1180
+ let trace_chips = TRACE_DEFS.map(([key, label, fn]) => ['button', {
1181
+ class: 'YDEV_CHIP', 'data-trace': key, 'data-label': label, type: 'button',
1182
+ }, label, {
1183
+ click: (ev) => {
1184
+ ev.stopPropagation();
1185
+ fn();
1186
+ }
1187
+ }]);
1188
+
1189
+ /* Compact automata format (like the C kernel's trace_machine_format==1):
1190
+ * one short line per transition, no return line. Applies to the FSM trace
1191
+ * emitted while Automata is on. */
1192
+ let simple_mach = ['button', {
1193
+ class: 'YDEV_CHIP', 'data-toggle': 'automata-simple', type: 'button',
1194
+ title: 'Compact automata format (one line per transition, like C)',
1195
+ }, 'Simple mach', {
1196
+ click: (ev) => {
1197
+ ev.stopPropagation();
1198
+ let v = dev_num('dev_automata_simple', 0) ? 0 : 1;
1199
+ kw_set_local_storage_value('dev_automata_simple', v);
1200
+ gobj_set_trace_machine_format(v);
1201
+ refresh_dev_chrome();
1202
+ }
1203
+ }];
1204
+
1205
+ let mk_view = (v, label) => ['button', {class: 'YDEV_SEG_BTN', 'data-view': v, type: 'button'}, label, {
1206
+ click: (ev) => {
1207
+ ev.stopPropagation();
1208
+ set_view(v);
1209
+ }
1210
+ }];
1211
+
1212
+ let view_seg = ['div', {class: 'YDEV_SEG', id: 'ydev-seg'}, [
1213
+ mk_view('detailed', 'Detailed'),
1214
+ mk_view('full', 'Expanded'),
1215
+ mk_view('compact', 'Compact'),
1216
+ mk_view('name', 'Name only'),
1217
+ ]];
1218
+
1219
+ /* Expanded-view section toggles (only meaningful in the 'full' view;
1220
+ * the group is shown/hidden by refresh_dev_chrome). */
1221
+ let mk_expand = (key, label) => ['button', {
1222
+ class: 'YDEV_CHIP', 'data-expand': key, type: 'button',
1223
+ title: 'Show ' + label + ' in the Expanded view',
1224
+ }, label, {
1225
+ click: (ev) => {
1226
+ ev.stopPropagation();
1227
+ toggle_pref(key, (key === 'dev_full_data') ? 1 : 0);
1228
+ }
1229
+ }];
1230
+ let expand_grp = ['div', {class: 'YDEV_GROUP', id: 'ydev-expand-grp'}, [
1231
+ ['span', {class: 'YDEV_LABEL'}, 'Expand'],
1232
+ mk_expand('dev_full_schema', 'Schema'),
1233
+ mk_expand('dev_full_data', 'Data'),
1234
+ mk_expand('dev_full_meta', 'Metadata'),
1235
+ ]];
1236
+
1237
+ let mk_dir = (dir, glyph, key, title) => ['button', {
1238
+ class: 'YDEV_CHIP s-' + dir, 'data-dir': key, type: 'button', title: title,
1239
+ }, glyph, {
1240
+ click: (ev) => {
1241
+ ev.stopPropagation();
1242
+ toggle_pref(key, 1);
1243
+ }
1244
+ }];
1245
+
1246
+ let dir_chips = [
1247
+ mk_dir('out', '⇢', 'dev_filter_out', 'Outgoing'),
1248
+ mk_dir('in', '⇠', 'dev_filter_in', 'Incoming'),
1249
+ mk_dir('err', '⚠', 'dev_filter_err', 'Errors'),
1250
+ ];
1251
+
1252
+ let periodic_chip = ['button', {
1253
+ class: 'YDEV_CHIP', 'data-toggle': 'periodic', type: 'button',
1254
+ title: 'Hide recurring / periodic events (polls, heartbeats)',
1255
+ }, '⊘ Periodic', {
1256
+ click: (ev) => {
1257
+ ev.stopPropagation();
1258
+ toggle_pref('dev_hide_periodic', 0);
1259
+ }
1260
+ }];
1261
+
1262
+ let search = ['input', {
1263
+ class: 'YDEV_SEARCH', type: 'search', placeholder: 'filter events / payload…', 'data-role': 'search',
1264
+ }, '', {
1265
+ input: (ev) => {
1266
+ SEARCH_TEXT = String(ev.target.value || '').toLowerCase().trim();
1267
+ rerender_all();
1268
+ }
1269
+ }];
1270
+
1271
+ let copy = ['button', {class: 'YDEV_CHIP', type: 'button', title: 'Copy visible traffic to clipboard'}, 'Copy', {
1272
+ click: (ev) => {
1273
+ ev.stopPropagation();
1274
+ let btn = ev.currentTarget;
1275
+ dev_copy_text(traffic_to_text()).then(() => {
1276
+ let prev = btn.textContent;
1277
+ btn.textContent = 'Copied';
1278
+ setTimeout(() => { btn.textContent = prev; }, 1000);
1279
+ });
1280
+ }
1281
+ }];
1282
+
1283
+ let clear = ['button', {class: 'YDEV_CHIP', type: 'button', title: 'Clear captured traffic'}, 'Clear', {
1284
+ click: (ev) => {
1285
+ ev.stopPropagation();
1286
+ clear_traffic();
1287
+ }
1288
+ }];
1289
+
1290
+ let grp = (label, items) => ['div', {class: 'YDEV_GROUP'}, [['span', {class: 'YDEV_LABEL'}, label], ...items]];
1291
+ let sep = () => ['span', {class: 'YDEV_SEP'}, ''];
1292
+
1293
+ return createElement2(['div', {class: 'YDEV_BAR'}, [
1294
+ grp('Traces', [...trace_chips, simple_mach]), sep(),
1295
+ grp('View', [view_seg]), expand_grp, sep(),
1296
+ grp('Show', [...dir_chips, periodic_chip]), sep(),
1297
+ grp('Find', [search]), sep(),
1298
+ grp('Log', [copy, clear]),
1299
+ ]]);
1300
+ }
1301
+
1302
+ /* The window title strip (draggable header of C_YUI_WINDOW). */
1303
+ function build_title_header()
1304
+ {
1305
+ return createElement2(['div', {class: 'YDEV_TITLE'}, [
1306
+ ['span', {class: 'YDEV_TITLE_MAIN'}, 'Developer'],
1307
+ ['span', {class: 'YDEV_TITLE_SUB'}, 'yuno monitor · traffic & traces'],
1308
+ ]]);
1309
+ }
1310
+
1311
+ /* The monitor body: control bar + muted row + log + stats strip. */
1312
+ function build_dev_body()
1313
+ {
1314
+ return createElement2(['div', {class: 'YDEV_BODY'}, [
1315
+ build_control_bar(),
1316
+ ['div', {class: 'YDEV_MUTED', id: 'ydev-muted'}, []],
1317
+ ['div', {class: 'YDEV_LOG', id: 'developer-traffic-logger'}, []],
1318
+ ['div', {class: 'YDEV_STATS', id: 'ydev-stats'}, []],
1319
+ ]]);
289
1320
  }
290
1321
 
1322
+
1323
+ /******************************
1324
+ * Public API
1325
+ ******************************/
1326
+
1327
+
291
1328
  /************************************************************
292
1329
  * Was the developer window open last session? setup_dev()
293
1330
  * persists open_developer_window (1 on open, 0 on close), so
@@ -304,9 +1341,6 @@ function dev_window_was_open()
304
1341
  * Apply ALL persisted developer-trace flags to the running
305
1342
  * yuno. Independent of the dev window — call it once at app
306
1343
  * startup so a refresh keeps logging whatever was enabled.
307
- * Single source of truth for "localStorage flag → effect";
308
- * setup_dev() and build_dev_panel() reuse it instead of each
309
- * re-applying a partial subset.
310
1344
  ************************************************************/
311
1345
  function apply_dev_traces()
312
1346
  {
@@ -330,121 +1364,30 @@ function apply_dev_traces()
330
1364
  gobj_write_attr(gobj_yuno(), "trace_subscriptions", subscriptions);
331
1365
  gobj_write_attr(gobj_yuno(), "no_poll", no_poll);
332
1366
  i18next.options.debug = i18n ? true : false;
1367
+
1368
+ /* Compact vs verbose automata trace format (persisted). */
1369
+ gobj_set_trace_machine_format(
1370
+ Number(kw_get_local_storage_value("dev_automata_simple", 0, false)) ? 1 : 0);
1371
+
1372
+ /* Mirror the browser console (log_error/warning/info/debug/msg — the
1373
+ * automata FSM trace arrives as debug) into the monitor. info_log no-ops
1374
+ * while the window is closed, so this is safe to leave armed. */
1375
+ set_log_callback(info_log);
333
1376
  }
334
1377
 
335
1378
  /************************************************************
336
- * Open the developer panel inside a non-modal C_YUI_WINDOW
1379
+ * Open the developer monitor inside a non-modal C_YUI_WINDOW
337
1380
  * (title bar + maximize + close + resize).
338
1381
  *
339
1382
  * Shell-agnostic: the legacy C_YUI_MAIN shell has a
340
1383
  * '#top-layer' stacking element; the new C_YUI_SHELL does not.
341
- * We pass that element when present, otherwise null — and
342
- * C_YUI_WINDOW falls back to document.body by contract. So the
343
- * new shell gets the same windowed dev panel instead of the
344
- * floating build_dev_panel() box. Legacy behaviour is
345
- * unchanged (when '#top-layer' exists it is still used).
1384
+ * We pass that element when present, otherwise null — C_YUI_WINDOW
1385
+ * falls back to document.body by contract.
346
1386
  ************************************************************/
347
1387
  function setup_dev(self, show)
348
1388
  {
349
- let traffic = Number(kw_get_local_storage_value("trace_traffic", 0, false));
350
- let trace = Number(kw_get_local_storage_value("trace_automata", 0, false));
351
- let creation = Number(kw_get_local_storage_value("trace_creation", 0, false));
352
- let start_stop = Number(kw_get_local_storage_value("trace_start_stop", 0, false));
353
- let subscriptions = Number(kw_get_local_storage_value("trace_subscriptions", 0, false));
354
- let i18n = Number(kw_get_local_storage_value("trace_i18n", 0, false));
355
- let no_poll = Number(kw_get_local_storage_value("no_poll", 0, false));
356
-
357
1389
  if(show) {
358
- const $dev_toolbar = createElement2(
359
- ['div', {class: 'buttons'}, [
360
- ['button', {
361
- class: 'button',
362
- }, 'Automata', {
363
- click: (evt) => {
364
- evt.stopPropagation();
365
- trace_automata();
366
- }
367
- }],
368
- ['button', {
369
- class: 'button',
370
- }, 'Creation', {
371
- click: (evt) => {
372
- evt.stopPropagation();
373
- trace_creation();
374
- }
375
- }],
376
- ['button', {
377
- class: 'button',
378
- }, 'Star/Stop', {
379
- click: (evt) => {
380
- evt.stopPropagation();
381
- trace_start_stop();
382
- }
383
- }],
384
- ['button', {
385
- class: 'button',
386
- }, 'Subscriptions', {
387
- click: (evt) => {
388
- evt.stopPropagation();
389
- trace_subscriptions();
390
- }
391
- }],
392
- ['button', {
393
- class: 'button',
394
- }, 'I18n', {
395
- click: (evt) => {
396
- evt.stopPropagation();
397
- trace_i18n();
398
- }
399
- }],
400
- ['button', {
401
- class: 'button',
402
- }, 'Traffic', {
403
- click: (evt) => {
404
- evt.stopPropagation();
405
- trace_traffic();
406
- }
407
- }],
408
- ['button', {
409
- class: 'button',
410
- }, 'No Poll', {
411
- click: (evt) => {
412
- evt.stopPropagation();
413
- set_no_poll();
414
- }
415
- }],
416
- ['button', {
417
- class: 'button',
418
- }, 'Clear Traffic', {
419
- click: (evt) => {
420
- evt.stopPropagation();
421
- document.getElementById("developer-traffic-logger").innerHTML = "";
422
- }
423
- }],
424
- ]]
425
- );
426
-
427
- // TODO repon la position
428
- // onViewResize: function() {
429
- // var record = filter_dict(this.config, self.config.traffic_window_position);
430
- // gobj_update_writable_attrs({traffic_window_position: record});
431
- // gobj_save_persistent_attrs();
432
- // },
433
- // onViewMoveEnd: function() {
434
- // var record = filter_dict(this.config, self.config.traffic_window_position);
435
- // gobj_update_writable_attrs({traffic_window_position: record});
436
- // gobj_save_persistent_attrs();
437
- // }
438
-
439
- // Code repeated
440
- let estados = `
441
- <div>Automata: ${trace}</div>
442
- <div>Creation: ${creation}</div>
443
- <div>Start/Stop: ${start_stop}</div>
444
- <div>Subscriptions: ${subscriptions}</div>
445
- <div>I18n: ${i18n}</div>
446
- <div>Traffic: ${traffic}</div>
447
- <div>No poll: ${no_poll}</div>`;
1390
+ ensure_dev_style();
448
1391
 
449
1392
  gobj_create_service(
450
1393
  "Developer-Window",
@@ -454,12 +1397,18 @@ function setup_dev(self, show)
454
1397
  subscriber: null,
455
1398
  showMax: true,
456
1399
  modal: false,
457
- header: $dev_toolbar,
1400
+ header: build_title_header(),
1401
+ body: build_dev_body(),
1402
+ showFooter: false,
458
1403
  auto_save_size_and_position: true,
459
1404
  center: false,
460
- // resizable: false,
461
- body: '<div style="overflow:scroll;height:100%;"><div id="developer-traffic-logger" style="margin-left:10px;margin-right:10px;"/></div>',
462
- footer: `<div id="developer-window-info" class="is-flex is-justify-content-space-between" style="gap:1.25rem;white-space:nowrap;">${estados}</div>`,
1405
+ title: "Developer",
1406
+ icon: "yi-terminal",
1407
+ /* Opt into the dock/taskbar if the app provides one. `|| null`
1408
+ * because gobj_find_service returns undefined when absent, and
1409
+ * an undefined attr value logs "attr undefined: manager" (apps
1410
+ * without a window manager, e.g. wattyzer). null = no dock. */
1411
+ manager: gobj_find_service("__window_manager__", false) || null,
463
1412
  on_close: function() {
464
1413
  kw_set_local_storage_value("open_developer_window", 0);
465
1414
  }
@@ -469,53 +1418,31 @@ function setup_dev(self, show)
469
1418
 
470
1419
  kw_set_local_storage_value("open_developer_window", 1);
471
1420
 
1421
+ /* Mounted synchronously above; paint state + buffered history
1422
+ * on the next tick to be safe against mount ordering. */
1423
+ setTimeout(() => {
1424
+ refresh_dev_chrome();
1425
+ rerender_all();
1426
+ }, 0);
472
1427
  }
473
1428
 
474
1429
  apply_dev_traces();
475
1430
  }
476
1431
 
477
1432
  /************************************************************
478
- * Build the developer panel as a self-contained DOM subtree,
1433
+ * Build the developer monitor as a self-contained DOM subtree,
479
1434
  * to be mounted by the new declarative shell via
480
1435
  * yui_shell_show_modal (no C_YUI_WINDOW, no 'top-layer').
481
1436
  *
482
1437
  * Returns { $el, dispose }:
483
- * - $el: the panel element (header tabs + traffic logger
484
- * body + footer counters).
1438
+ * - $el: the panel element (control bar + log + stats).
485
1439
  * - dispose: stops the inter-event traffic trace; call it from
486
1440
  * the modal's on_close.
487
- *
488
- * Backwards compatible: setup_dev() (old shell, C_YUI_WINDOW) is
489
- * untouched; the trace_* helpers and info_traffic are shared.
490
1441
  ************************************************************/
491
1442
  function build_dev_panel()
492
1443
  {
493
- let traffic = Number(kw_get_local_storage_value("trace_traffic", 0, false));
494
- let trace = Number(kw_get_local_storage_value("trace_automata", 0, false));
495
- let creation = Number(kw_get_local_storage_value("trace_creation", 0, false));
496
- let start_stop = Number(kw_get_local_storage_value("trace_start_stop", 0, false));
497
- let subscriptions = Number(kw_get_local_storage_value("trace_subscriptions", 0, false));
498
- let i18n = Number(kw_get_local_storage_value("trace_i18n", 0, false));
499
- let no_poll = Number(kw_get_local_storage_value("no_poll", 0, false));
1444
+ ensure_dev_style();
500
1445
 
501
- let mk_btn = (label, fn) => ['button', {
502
- class: 'button is-small',
503
- }, label, {
504
- click: (evt) => {
505
- evt.stopPropagation();
506
- fn();
507
- }
508
- }];
509
-
510
- let counters = [
511
- `Automata: ${trace}`, `Creation: ${creation}`,
512
- `Start/Stop: ${start_stop}`, `Subscriptions: ${subscriptions}`,
513
- `I18n: ${i18n}`, `Traffic: ${traffic}`, `No poll: ${no_poll}`,
514
- ].map(txt => ['div', {style: 'padding:0 8px;'}, txt]);
515
-
516
- // The shell modal drops content into a transparent, unsized
517
- // Bulma .modal-content; the panel must be its own opaque,
518
- // sized window box. Theme-aware (read <html data-theme>).
519
1446
  let dark = (typeof document !== "undefined") &&
520
1447
  document.documentElement.getAttribute("data-theme") === "dark";
521
1448
  let surface = dark ? "#1f2733" : "#ffffff";
@@ -530,50 +1457,18 @@ function build_dev_panel()
530
1457
  'width:100%;height:min(72vh,720px);max-height:82vh;' +
531
1458
  'background:' + surface + ';color:' + fg + ';' +
532
1459
  'border:1px solid ' + bd + ';border-radius:10px;' +
533
- 'box-shadow:0 10px 30px rgba(0,0,0,0.35);' +
534
- 'padding:14px;overflow:hidden;' +
535
- 'font-family:-apple-system,BlinkMacSystemFont,' +
536
- "'Segoe UI',Roboto,Helvetica,Arial,sans-serif;",
537
- }, [
538
- ['div', {
539
- class: 'buttons',
540
- style: 'flex:0 0 auto;display:flex;flex-wrap:wrap;' +
541
- 'gap:6px;margin:0 0 8px 0;',
542
- }, [
543
- mk_btn('Automata', trace_automata),
544
- mk_btn('Creation', trace_creation),
545
- mk_btn('Star/Stop', trace_start_stop),
546
- mk_btn('Subscriptions', trace_subscriptions),
547
- mk_btn('I18n', trace_i18n),
548
- mk_btn('Traffic', trace_traffic),
549
- mk_btn('No Poll', set_no_poll),
550
- mk_btn('Clear Traffic', () => {
551
- let l = document.getElementById("developer-traffic-logger");
552
- if(l) {
553
- l.innerHTML = "";
554
- }
555
- }),
556
- ]],
557
- ['div', {
558
- style: 'flex:1 1 auto;min-height:0;overflow:auto;',
559
- }, [
560
- ['div', {id: 'developer-traffic-logger',
561
- style: 'margin:0 4px;'}, []],
562
- ]],
563
- ['div', {
564
- id: 'developer-window-info',
565
- class: 'is-flex is-justify-content-space-between',
566
- style: 'flex:0 0 auto;border-top:1px solid ' + bd +
567
- ';padding-top:6px;margin-top:6px;font-size:12px;' +
568
- 'opacity:0.85;flex-wrap:nowrap;gap:1.25rem;white-space:nowrap;',
569
- }, counters],
570
- ]]
1460
+ 'box-shadow:0 10px 30px rgba(0,0,0,0.35);overflow:hidden;',
1461
+ }, [build_dev_body()]]
571
1462
  );
572
1463
 
573
1464
  apply_dev_traces();
574
1465
 
1466
+ setTimeout(() => {
1467
+ refresh_dev_chrome();
1468
+ rerender_all();
1469
+ }, 0);
1470
+
575
1471
  let dispose = function() {
576
- // Stop feeding traffic into a detached DOM.
577
1472
  gobj_write_attr(gobj_yuno(), "trace_inter_event", false);
578
1473
  };
579
1474