@yuneta/gobj-ui 2.0.0 → 2.2.3

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 (58) hide show
  1. package/README.md +25 -20
  2. package/dist/gobj-ui.cjs.js +2340 -528
  3. package/dist/gobj-ui.es.js +2340 -530
  4. package/index.js +32 -31
  5. package/package.json +6 -7
  6. package/{c_g6_nodes_tree.js → src/c_g6_nodes_tree.js} +6 -1
  7. package/{c_yui_form.js → src/c_yui_form.js} +1 -1
  8. package/{c_yui_gobj_tree_js.js → src/c_yui_gobj_tree_js.js} +1 -1
  9. package/{c_yui_json_graph.js → src/c_yui_json_graph.js} +1 -1
  10. package/{c_yui_main.js → src/c_yui_main.js} +3 -3
  11. package/{c_yui_map.js → src/c_yui_map.js} +6 -1
  12. package/{c_yui_nav.js → src/c_yui_nav.js} +134 -26
  13. package/{c_yui_pager.js → src/c_yui_pager.js} +1 -1
  14. package/{c_yui_routing.css → src/c_yui_routing.css} +1 -1
  15. package/{c_yui_routing.js → src/c_yui_routing.js} +1 -1
  16. package/{c_yui_shell.css → src/c_yui_shell.css} +110 -0
  17. package/{c_yui_shell.js → src/c_yui_shell.js} +175 -13
  18. package/{c_yui_tabs.js → src/c_yui_tabs.js} +1 -1
  19. package/{c_yui_treedb_graph.js → src/c_yui_treedb_graph.js} +35 -9
  20. package/{c_yui_treedb_topic_with_form.js → src/c_yui_treedb_topic_with_form.js} +1 -1
  21. package/{c_yui_treedb_topics.js → src/c_yui_treedb_topics.js} +36 -8
  22. package/{c_yui_uplot.js → src/c_yui_uplot.js} +1 -1
  23. package/{c_yui_window.js → src/c_yui_window.js} +242 -21
  24. package/src/c_yui_window_manager.js +602 -0
  25. package/{c_yui_wizard.js → src/c_yui_wizard.js} +1 -1
  26. package/{shell_modals.js → src/shell_modals.js} +53 -13
  27. package/src/tabulator.css +53 -0
  28. package/src/yui_dev.js +1484 -0
  29. package/{yui_icons.css → src/yui_icons.css} +5 -0
  30. package/src/yui_inputs.css +32 -0
  31. package/src/yui_inputs.js +71 -0
  32. package/vite.config.js +0 -131
  33. package/yui_dev.js +0 -583
  34. /package/{c_yui_main.css → src/c_yui_main.css} +0 -0
  35. /package/{c_yui_map.css → src/c_yui_map.css} +0 -0
  36. /package/{c_yui_treedb_topic_with_form.css → src/c_yui_treedb_topic_with_form.css} +0 -0
  37. /package/{g6_drag_canvas_touch.js → src/g6_drag_canvas_touch.js} +0 -0
  38. /package/{lib_graph.css → src/lib_graph.css} +0 -0
  39. /package/{lib_graph.js → src/lib_graph.js} +0 -0
  40. /package/{lib_icons.js → src/lib_icons.js} +0 -0
  41. /package/{lib_maplibre.js → src/lib_maplibre.js} +0 -0
  42. /package/{pager_helpers.js → src/pager_helpers.js} +0 -0
  43. /package/{pager_helpers.test.js → src/pager_helpers.test.js} +0 -0
  44. /package/{route_resolver.js → src/route_resolver.js} +0 -0
  45. /package/{route_resolver.test.js → src/route_resolver.test.js} +0 -0
  46. /package/{shell_focus_trap.js → src/shell_focus_trap.js} +0 -0
  47. /package/{shell_focus_trap.test.js → src/shell_focus_trap.test.js} +0 -0
  48. /package/{shell_show_on.js → src/shell_show_on.js} +0 -0
  49. /package/{shell_show_on.test.js → src/shell_show_on.test.js} +0 -0
  50. /package/{shell_toolbar_helpers.js → src/shell_toolbar_helpers.js} +0 -0
  51. /package/{shell_toolbar_helpers.test.js → src/shell_toolbar_helpers.test.js} +0 -0
  52. /package/{themes.js → src/themes.js} +0 -0
  53. /package/{wizard_helpers.js → src/wizard_helpers.js} +0 -0
  54. /package/{wizard_helpers.test.js → src/wizard_helpers.test.js} +0 -0
  55. /package/{ytable.css → src/ytable.css} +0 -0
  56. /package/{ytable.js → src/ytable.js} +0 -0
  57. /package/{yui_toolbar.css → src/yui_toolbar.css} +0 -0
  58. /package/{yui_toolbar.js → src/yui_toolbar.js} +0 -0
package/src/yui_dev.js ADDED
@@ -0,0 +1,1484 @@
1
+ /***********************************************************************
2
+ * ui_dev.js
3
+ *
4
+ * Development Tools — yuno monitor / audit console
5
+ *
6
+ * Copyright (c) 2024-2026, ArtGins.
7
+ * All Rights Reserved.
8
+ ***********************************************************************/
9
+ import {
10
+ gobj_yuno,
11
+ is_string,
12
+ createElement2,
13
+ kw_get_local_storage_value,
14
+ kw_set_local_storage_value,
15
+ gobj_write_attr,
16
+ gobj_create_service,
17
+ gobj_find_service,
18
+ set_log_callback,
19
+ gobj_set_trace_machine_format,
20
+ trace_json,
21
+ } from "@yuneta/gobj-js";
22
+
23
+ import i18next from 'i18next';
24
+
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
+
278
+
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).
815
+ *
816
+ * direction: 1 outgoing (⇢), 2 incoming (⇠), 3 error (⚠).
817
+ * With no logger mounted, fall back to a console dump.
818
+ ************************************************************/
819
+ function info_traffic(title, msg, direction, size)
820
+ {
821
+ let logger = document.getElementById('developer-traffic-logger');
822
+ if(!logger) {
823
+ trace_json(msg);
824
+ return;
825
+ }
826
+
827
+ if(!size) {
828
+ size = 0;
829
+ }
830
+
831
+ let jn;
832
+ try {
833
+ jn = is_string(msg) ? JSON.parse(msg) : JSON.parse(JSON.stringify(msg));
834
+ } catch(e) {
835
+ return;
836
+ }
837
+
838
+ ensure_dev_style();
839
+
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
+ }
851
+
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
+ };
857
+
858
+ TRAFFIC_LOG.push(entry);
859
+ TRAFFIC_COUNTS.set(sig, (TRAFFIC_COUNTS.get(sig) || 0) + 1);
860
+
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);
865
+
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);
871
+ } else {
872
+ TRAFFIC_COUNTS.set(old.sig, c);
873
+ }
874
+ if(old.$node && old.$node.parentNode) {
875
+ old.$node.parentNode.removeChild(old.$node);
876
+ }
877
+ }
878
+
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"});
891
+ }
892
+ update_stats();
893
+ }
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
+
900
+ /************************************************************
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).
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
+
974
+ function trace_traffic()
975
+ {
976
+ let v = Number(kw_get_local_storage_value("trace_traffic"));
977
+ if(v) {
978
+ gobj_write_attr(gobj_yuno(), "trace_inter_event", false);
979
+ v = 0;
980
+ } else {
981
+ gobj_write_attr(gobj_yuno(), "trace_inter_event", true);
982
+ gobj_write_attr(gobj_yuno(), "trace_ievent_callback", info_traffic);
983
+ v = 1;
984
+ }
985
+ kw_set_local_storage_value("trace_traffic", v);
986
+ refresh_dev_chrome();
987
+ }
988
+
989
+ function trace_automata()
990
+ {
991
+ let v = Number(kw_get_local_storage_value("trace_automata"));
992
+ if(v === 0) {
993
+ v = 1;
994
+ } else if(v === 1) {
995
+ v = 2;
996
+ } else {
997
+ v = 0;
998
+ }
999
+ gobj_write_attr(gobj_yuno(), "tracing", v);
1000
+ kw_set_local_storage_value("trace_automata", v);
1001
+ refresh_dev_chrome();
1002
+ }
1003
+
1004
+ function trace_creation()
1005
+ {
1006
+ let v = Number(kw_get_local_storage_value("trace_creation"));
1007
+ v = v === 0 ? 1 : 0;
1008
+ gobj_write_attr(gobj_yuno(), "trace_creation", v);
1009
+ kw_set_local_storage_value("trace_creation", v);
1010
+ refresh_dev_chrome();
1011
+ }
1012
+
1013
+ function trace_start_stop()
1014
+ {
1015
+ let v = Number(kw_get_local_storage_value("trace_start_stop"));
1016
+ v = v === 0 ? 1 : 0;
1017
+ gobj_write_attr(gobj_yuno(), "trace_start_stop", v);
1018
+ kw_set_local_storage_value("trace_start_stop", v);
1019
+ refresh_dev_chrome();
1020
+ }
1021
+
1022
+ function trace_subscriptions()
1023
+ {
1024
+ let v = Number(kw_get_local_storage_value("trace_subscriptions"));
1025
+ v = v === 0 ? 1 : 0;
1026
+ gobj_write_attr(gobj_yuno(), "trace_subscriptions", v);
1027
+ kw_set_local_storage_value("trace_subscriptions", v);
1028
+ refresh_dev_chrome();
1029
+ }
1030
+
1031
+ function trace_i18n()
1032
+ {
1033
+ let v = Number(kw_get_local_storage_value("trace_i18n"));
1034
+ v = v === 0 ? 1 : 0;
1035
+ i18next.options.debug = v ? true : false;
1036
+ kw_set_local_storage_value("trace_i18n", v);
1037
+ refresh_dev_chrome();
1038
+ }
1039
+
1040
+ function set_no_poll()
1041
+ {
1042
+ let v = Number(kw_get_local_storage_value("no_poll"));
1043
+ v = v ? 0 : 1;
1044
+ gobj_write_attr(gobj_yuno(), "no_poll", v);
1045
+ kw_set_local_storage_value("no_poll", v);
1046
+ refresh_dev_chrome();
1047
+ }
1048
+
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()
1059
+ {
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
+ });
1067
+
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
+ });
1082
+
1083
+ document.querySelectorAll('.YDEV_CHIP[data-dir]').forEach(($b) => {
1084
+ $b.classList.toggle('is-active', !!dev_num($b.getAttribute('data-dir'), 1));
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
+ if(e.kind === "log") {
1134
+ /* Mirrored log/automata line: no event/kw — serialize as shown. */
1135
+ out.push(`${e.ts} ${e.level}: ${e.text}`);
1136
+ out.push("");
1137
+ continue;
1138
+ }
1139
+ let head = `${e.ts} ${dir_arrow(e.dir)} ` +
1140
+ `${e.title ? "[" + e.title + "] " : ""}${e.event}` +
1141
+ `${e.command ? " " + e.command : ""}`;
1142
+ out.push(head);
1143
+ let payload = e.kw ? e.kw : e.jn;
1144
+ try {
1145
+ out.push(JSON.stringify(payload, null, 2));
1146
+ } catch(err) {
1147
+ out.push(String(payload));
1148
+ }
1149
+ out.push("");
1150
+ }
1151
+ return out.join("\n");
1152
+ }
1153
+
1154
+ /* Copy text to the clipboard, with a fallback for insecure contexts. */
1155
+ function dev_copy_text(text)
1156
+ {
1157
+ if(navigator.clipboard && navigator.clipboard.writeText) {
1158
+ return navigator.clipboard.writeText(text).catch(() => {
1159
+ dev_fallback_copy(text);
1160
+ });
1161
+ }
1162
+ dev_fallback_copy(text);
1163
+ return Promise.resolve();
1164
+ }
1165
+
1166
+ function dev_fallback_copy(text)
1167
+ {
1168
+ let ta = document.createElement("textarea");
1169
+ ta.value = text;
1170
+ ta.style.position = "fixed";
1171
+ ta.style.left = "-9999px";
1172
+ document.body.appendChild(ta);
1173
+ ta.select();
1174
+ try {
1175
+ document.execCommand("copy");
1176
+ } catch(e) {
1177
+ /* nothing else to try */
1178
+ }
1179
+ document.body.removeChild(ta);
1180
+ }
1181
+
1182
+ /* The control bar: trace toggles, view selector, direction /
1183
+ * periodic filters, free-text search, copy, clear. Returns an element. */
1184
+ function build_control_bar()
1185
+ {
1186
+ let trace_chips = TRACE_DEFS.map(([key, label, fn]) => ['button', {
1187
+ class: 'YDEV_CHIP', 'data-trace': key, 'data-label': label, type: 'button',
1188
+ }, label, {
1189
+ click: (ev) => {
1190
+ ev.stopPropagation();
1191
+ fn();
1192
+ }
1193
+ }]);
1194
+
1195
+ /* Compact automata format (like the C kernel's trace_machine_format==1):
1196
+ * one short line per transition, no return line. Applies to the FSM trace
1197
+ * emitted while Automata is on. */
1198
+ let simple_mach = ['button', {
1199
+ class: 'YDEV_CHIP', 'data-toggle': 'automata-simple', type: 'button',
1200
+ title: 'Compact automata format (one line per transition, like C)',
1201
+ }, 'Simple mach', {
1202
+ click: (ev) => {
1203
+ ev.stopPropagation();
1204
+ let v = dev_num('dev_automata_simple', 0) ? 0 : 1;
1205
+ kw_set_local_storage_value('dev_automata_simple', v);
1206
+ gobj_set_trace_machine_format(v);
1207
+ refresh_dev_chrome();
1208
+ }
1209
+ }];
1210
+
1211
+ let mk_view = (v, label) => ['button', {class: 'YDEV_SEG_BTN', 'data-view': v, type: 'button'}, label, {
1212
+ click: (ev) => {
1213
+ ev.stopPropagation();
1214
+ set_view(v);
1215
+ }
1216
+ }];
1217
+
1218
+ let view_seg = ['div', {class: 'YDEV_SEG', id: 'ydev-seg'}, [
1219
+ mk_view('detailed', 'Detailed'),
1220
+ mk_view('full', 'Expanded'),
1221
+ mk_view('compact', 'Compact'),
1222
+ mk_view('name', 'Name only'),
1223
+ ]];
1224
+
1225
+ /* Expanded-view section toggles (only meaningful in the 'full' view;
1226
+ * the group is shown/hidden by refresh_dev_chrome). */
1227
+ let mk_expand = (key, label) => ['button', {
1228
+ class: 'YDEV_CHIP', 'data-expand': key, type: 'button',
1229
+ title: 'Show ' + label + ' in the Expanded view',
1230
+ }, label, {
1231
+ click: (ev) => {
1232
+ ev.stopPropagation();
1233
+ toggle_pref(key, (key === 'dev_full_data') ? 1 : 0);
1234
+ }
1235
+ }];
1236
+ let expand_grp = ['div', {class: 'YDEV_GROUP', id: 'ydev-expand-grp'}, [
1237
+ ['span', {class: 'YDEV_LABEL'}, 'Expand'],
1238
+ mk_expand('dev_full_schema', 'Schema'),
1239
+ mk_expand('dev_full_data', 'Data'),
1240
+ mk_expand('dev_full_meta', 'Metadata'),
1241
+ ]];
1242
+
1243
+ let mk_dir = (dir, glyph, key, title) => ['button', {
1244
+ class: 'YDEV_CHIP s-' + dir, 'data-dir': key, type: 'button', title: title,
1245
+ }, glyph, {
1246
+ click: (ev) => {
1247
+ ev.stopPropagation();
1248
+ toggle_pref(key, 1);
1249
+ }
1250
+ }];
1251
+
1252
+ let dir_chips = [
1253
+ mk_dir('out', '⇢', 'dev_filter_out', 'Outgoing'),
1254
+ mk_dir('in', '⇠', 'dev_filter_in', 'Incoming'),
1255
+ mk_dir('err', '⚠', 'dev_filter_err', 'Errors'),
1256
+ ];
1257
+
1258
+ let periodic_chip = ['button', {
1259
+ class: 'YDEV_CHIP', 'data-toggle': 'periodic', type: 'button',
1260
+ title: 'Hide recurring / periodic events (polls, heartbeats)',
1261
+ }, '⊘ Periodic', {
1262
+ click: (ev) => {
1263
+ ev.stopPropagation();
1264
+ toggle_pref('dev_hide_periodic', 0);
1265
+ }
1266
+ }];
1267
+
1268
+ let search = ['input', {
1269
+ class: 'YDEV_SEARCH', type: 'search', placeholder: 'filter events / payload…', 'data-role': 'search',
1270
+ }, '', {
1271
+ input: (ev) => {
1272
+ SEARCH_TEXT = String(ev.target.value || '').toLowerCase().trim();
1273
+ rerender_all();
1274
+ }
1275
+ }];
1276
+
1277
+ let copy = ['button', {class: 'YDEV_CHIP', type: 'button', title: 'Copy visible traffic to clipboard'}, 'Copy', {
1278
+ click: (ev) => {
1279
+ ev.stopPropagation();
1280
+ let btn = ev.currentTarget;
1281
+ dev_copy_text(traffic_to_text()).then(() => {
1282
+ let prev = btn.textContent;
1283
+ btn.textContent = 'Copied';
1284
+ setTimeout(() => { btn.textContent = prev; }, 1000);
1285
+ });
1286
+ }
1287
+ }];
1288
+
1289
+ let clear = ['button', {class: 'YDEV_CHIP', type: 'button', title: 'Clear captured traffic'}, 'Clear', {
1290
+ click: (ev) => {
1291
+ ev.stopPropagation();
1292
+ clear_traffic();
1293
+ }
1294
+ }];
1295
+
1296
+ let grp = (label, items) => ['div', {class: 'YDEV_GROUP'}, [['span', {class: 'YDEV_LABEL'}, label], ...items]];
1297
+ let sep = () => ['span', {class: 'YDEV_SEP'}, ''];
1298
+
1299
+ return createElement2(['div', {class: 'YDEV_BAR'}, [
1300
+ grp('Traces', [...trace_chips, simple_mach]), sep(),
1301
+ grp('View', [view_seg]), expand_grp, sep(),
1302
+ grp('Show', [...dir_chips, periodic_chip]), sep(),
1303
+ grp('Find', [search]), sep(),
1304
+ grp('Log', [copy, clear]),
1305
+ ]]);
1306
+ }
1307
+
1308
+ /* The window title strip (draggable header of C_YUI_WINDOW). */
1309
+ function build_title_header()
1310
+ {
1311
+ return createElement2(['div', {class: 'YDEV_TITLE'}, [
1312
+ ['span', {class: 'YDEV_TITLE_MAIN'}, 'Developer'],
1313
+ ['span', {class: 'YDEV_TITLE_SUB'}, 'yuno monitor · traffic & traces'],
1314
+ ]]);
1315
+ }
1316
+
1317
+ /* The monitor body: control bar + muted row + log + stats strip. */
1318
+ function build_dev_body()
1319
+ {
1320
+ return createElement2(['div', {class: 'YDEV_BODY'}, [
1321
+ build_control_bar(),
1322
+ ['div', {class: 'YDEV_MUTED', id: 'ydev-muted'}, []],
1323
+ ['div', {class: 'YDEV_LOG', id: 'developer-traffic-logger'}, []],
1324
+ ['div', {class: 'YDEV_STATS', id: 'ydev-stats'}, []],
1325
+ ]]);
1326
+ }
1327
+
1328
+
1329
+ /******************************
1330
+ * Public API
1331
+ ******************************/
1332
+
1333
+
1334
+ /************************************************************
1335
+ * Was the developer window open last session? setup_dev()
1336
+ * persists open_developer_window (1 on open, 0 on close), so
1337
+ * the host can reopen it on refresh and keep collecting
1338
+ * traffic/traces it had enabled.
1339
+ ************************************************************/
1340
+ function dev_window_was_open()
1341
+ {
1342
+ return Number(kw_get_local_storage_value("open_developer_window", 0, false))
1343
+ ? true : false;
1344
+ }
1345
+
1346
+ /************************************************************
1347
+ * Apply ALL persisted developer-trace flags to the running
1348
+ * yuno. Independent of the dev window — call it once at app
1349
+ * startup so a refresh keeps logging whatever was enabled.
1350
+ ************************************************************/
1351
+ function apply_dev_traces()
1352
+ {
1353
+ let traffic = Number(kw_get_local_storage_value("trace_traffic", 0, false));
1354
+ let trace = Number(kw_get_local_storage_value("trace_automata", 0, false));
1355
+ let creation = Number(kw_get_local_storage_value("trace_creation", 0, false));
1356
+ let start_stop = Number(kw_get_local_storage_value("trace_start_stop", 0, false));
1357
+ let subscriptions = Number(kw_get_local_storage_value("trace_subscriptions", 0, false));
1358
+ let i18n = Number(kw_get_local_storage_value("trace_i18n", 0, false));
1359
+ let no_poll = Number(kw_get_local_storage_value("no_poll", 0, false));
1360
+
1361
+ if(traffic) {
1362
+ gobj_write_attr(gobj_yuno(), "trace_inter_event", true);
1363
+ gobj_write_attr(gobj_yuno(), "trace_ievent_callback", info_traffic);
1364
+ } else {
1365
+ gobj_write_attr(gobj_yuno(), "trace_inter_event", false);
1366
+ }
1367
+ gobj_write_attr(gobj_yuno(), "tracing", trace);
1368
+ gobj_write_attr(gobj_yuno(), "trace_creation", creation);
1369
+ gobj_write_attr(gobj_yuno(), "trace_start_stop", start_stop);
1370
+ gobj_write_attr(gobj_yuno(), "trace_subscriptions", subscriptions);
1371
+ gobj_write_attr(gobj_yuno(), "no_poll", no_poll);
1372
+ i18next.options.debug = i18n ? true : false;
1373
+
1374
+ /* Compact vs verbose automata trace format (persisted). */
1375
+ gobj_set_trace_machine_format(
1376
+ Number(kw_get_local_storage_value("dev_automata_simple", 0, false)) ? 1 : 0);
1377
+
1378
+ /* Mirror the browser console (log_error/warning/info/debug/msg — the
1379
+ * automata FSM trace arrives as debug) into the monitor. info_log no-ops
1380
+ * while the window is closed, so this is safe to leave armed. */
1381
+ set_log_callback(info_log);
1382
+ }
1383
+
1384
+ /************************************************************
1385
+ * Open the developer monitor inside a non-modal C_YUI_WINDOW
1386
+ * (title bar + maximize + close + resize).
1387
+ *
1388
+ * Shell-agnostic: the legacy C_YUI_MAIN shell has a
1389
+ * '#top-layer' stacking element; the new C_YUI_SHELL does not.
1390
+ * We pass that element when present, otherwise null — C_YUI_WINDOW
1391
+ * falls back to document.body by contract.
1392
+ ************************************************************/
1393
+ function setup_dev(self, show)
1394
+ {
1395
+ if(show) {
1396
+ ensure_dev_style();
1397
+
1398
+ gobj_create_service(
1399
+ "Developer-Window",
1400
+ "C_YUI_WINDOW",
1401
+ {
1402
+ $parent: document.getElementById('top-layer') || null,
1403
+ subscriber: null,
1404
+ showMax: true,
1405
+ modal: false,
1406
+ header: build_title_header(),
1407
+ body: build_dev_body(),
1408
+ showFooter: false,
1409
+ auto_save_size_and_position: true,
1410
+ center: false,
1411
+ title: "Developer",
1412
+ icon: "yi-terminal",
1413
+ /* Opt into the dock/taskbar if the app provides one. `|| null`
1414
+ * because gobj_find_service returns undefined when absent, and
1415
+ * an undefined attr value logs "attr undefined: manager" (apps
1416
+ * without a window manager, e.g. wattyzer). null = no dock. */
1417
+ manager: gobj_find_service("__window_manager__", false) || null,
1418
+ on_close: function() {
1419
+ kw_set_local_storage_value("open_developer_window", 0);
1420
+ }
1421
+ },
1422
+ self
1423
+ );
1424
+
1425
+ kw_set_local_storage_value("open_developer_window", 1);
1426
+
1427
+ /* Mounted synchronously above; paint state + buffered history
1428
+ * on the next tick to be safe against mount ordering. */
1429
+ setTimeout(() => {
1430
+ refresh_dev_chrome();
1431
+ rerender_all();
1432
+ }, 0);
1433
+ }
1434
+
1435
+ apply_dev_traces();
1436
+ }
1437
+
1438
+ /************************************************************
1439
+ * Build the developer monitor as a self-contained DOM subtree,
1440
+ * to be mounted by the new declarative shell via
1441
+ * yui_shell_show_modal (no C_YUI_WINDOW, no 'top-layer').
1442
+ *
1443
+ * Returns { $el, dispose }:
1444
+ * - $el: the panel element (control bar + log + stats).
1445
+ * - dispose: stops the inter-event traffic trace; call it from
1446
+ * the modal's on_close.
1447
+ ************************************************************/
1448
+ function build_dev_panel()
1449
+ {
1450
+ ensure_dev_style();
1451
+
1452
+ let dark = (typeof document !== "undefined") &&
1453
+ document.documentElement.getAttribute("data-theme") === "dark";
1454
+ let surface = dark ? "#1f2733" : "#ffffff";
1455
+ let fg = dark ? "#e8eaed" : "#0f172a";
1456
+ let bd = dark ? "#3a4250" : "#cbd5e1";
1457
+
1458
+ let $el = createElement2(
1459
+ ['div', {
1460
+ class: 'yui-dev-panel',
1461
+ style:
1462
+ 'display:flex;flex-direction:column;box-sizing:border-box;' +
1463
+ 'width:100%;height:min(72vh,720px);max-height:82vh;' +
1464
+ 'background:' + surface + ';color:' + fg + ';' +
1465
+ 'border:1px solid ' + bd + ';border-radius:10px;' +
1466
+ 'box-shadow:0 10px 30px rgba(0,0,0,0.35);overflow:hidden;',
1467
+ }, [build_dev_body()]]
1468
+ );
1469
+
1470
+ apply_dev_traces();
1471
+
1472
+ setTimeout(() => {
1473
+ refresh_dev_chrome();
1474
+ rerender_all();
1475
+ }, 0);
1476
+
1477
+ let dispose = function() {
1478
+ gobj_write_attr(gobj_yuno(), "trace_inter_event", false);
1479
+ };
1480
+
1481
+ return {$el: $el, dispose: dispose};
1482
+ }
1483
+
1484
+ export {info_traffic, setup_dev, build_dev_panel, apply_dev_traces, dev_window_was_open};