@zolomedia/bifrost-client 1.7.74

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 (140) hide show
  1. package/L1_Foundation/L1_Foundation.js +13 -0
  2. package/L1_Foundation/bootstrap/bootstrap.js +11 -0
  3. package/L1_Foundation/bootstrap/bootstrap_hooks.js +123 -0
  4. package/L1_Foundation/bootstrap/bootstrap_index.js +15 -0
  5. package/L1_Foundation/bootstrap/bootstrap_logger.js +135 -0
  6. package/L1_Foundation/bootstrap/cdn_loader.js +217 -0
  7. package/L1_Foundation/bootstrap/module_registry.js +102 -0
  8. package/L1_Foundation/bootstrap/prism_loader.js +164 -0
  9. package/L1_Foundation/config/client_config.js +110 -0
  10. package/L1_Foundation/config/config.js +7 -0
  11. package/L1_Foundation/connection/connection.js +8 -0
  12. package/L1_Foundation/connection/websocket_connection.js +122 -0
  13. package/L1_Foundation/constants/bifrost_constants.js +284 -0
  14. package/L1_Foundation/constants/constants.js +7 -0
  15. package/L1_Foundation/logger/logger.js +10 -0
  16. package/L2_Handling/L2_Handling.js +15 -0
  17. package/L2_Handling/cache/cache.js +22 -0
  18. package/L2_Handling/cache/cache_constants.js +69 -0
  19. package/L2_Handling/cache/orchestration/cache_manager.js +299 -0
  20. package/L2_Handling/cache/orchestration/cache_orchestrator.js +260 -0
  21. package/L2_Handling/cache/orchestration/orchestration.js +12 -0
  22. package/L2_Handling/cache/storage/session_manager.js +289 -0
  23. package/L2_Handling/cache/storage/storage.js +10 -0
  24. package/L2_Handling/cache/storage/storage_manager.js +590 -0
  25. package/L2_Handling/display/composite/composite.js +13 -0
  26. package/L2_Handling/display/composite/dashboard_renderer.js +221 -0
  27. package/L2_Handling/display/composite/swiper_renderer.js +564 -0
  28. package/L2_Handling/display/composite/terminal_renderer.js +922 -0
  29. package/L2_Handling/display/composite/wizard_conditional_renderer.js +274 -0
  30. package/L2_Handling/display/display.js +30 -0
  31. package/L2_Handling/display/feedback/feedback.js +11 -0
  32. package/L2_Handling/display/feedback/progressbar_renderer.js +418 -0
  33. package/L2_Handling/display/feedback/spinner_renderer.js +246 -0
  34. package/L2_Handling/display/inputs/button_renderer.js +634 -0
  35. package/L2_Handling/display/inputs/form_renderer.js +583 -0
  36. package/L2_Handling/display/inputs/input_renderer.js +658 -0
  37. package/L2_Handling/display/inputs/inputs.js +12 -0
  38. package/L2_Handling/display/navigation/menu_renderer.js +206 -0
  39. package/L2_Handling/display/navigation/navigation.js +11 -0
  40. package/L2_Handling/display/navigation/navigation_renderer.js +703 -0
  41. package/L2_Handling/display/orchestration/orchestration.js +11 -0
  42. package/L2_Handling/display/orchestration/renderer.js +430 -0
  43. package/L2_Handling/display/orchestration/zdisplay_orchestrator.js +1759 -0
  44. package/L2_Handling/display/outputs/alert_renderer.js +161 -0
  45. package/L2_Handling/display/outputs/audio_renderer.js +94 -0
  46. package/L2_Handling/display/outputs/card_renderer.js +229 -0
  47. package/L2_Handling/display/outputs/code_renderer.js +66 -0
  48. package/L2_Handling/display/outputs/dl_renderer.js +131 -0
  49. package/L2_Handling/display/outputs/header_renderer.js +162 -0
  50. package/L2_Handling/display/outputs/icon_renderer.js +107 -0
  51. package/L2_Handling/display/outputs/image_renderer.js +145 -0
  52. package/L2_Handling/display/outputs/list_renderer.js +190 -0
  53. package/L2_Handling/display/outputs/outputs.js +19 -0
  54. package/L2_Handling/display/outputs/table_renderer.js +765 -0
  55. package/L2_Handling/display/outputs/text_renderer.js +818 -0
  56. package/L2_Handling/display/outputs/typography_renderer.js +293 -0
  57. package/L2_Handling/display/outputs/video_renderer.js +116 -0
  58. package/L2_Handling/display/primitives/document_structure_primitives.js +319 -0
  59. package/L2_Handling/display/primitives/form_primitives.js +526 -0
  60. package/L2_Handling/display/primitives/generic_containers.js +109 -0
  61. package/L2_Handling/display/primitives/interactive_primitives.js +305 -0
  62. package/L2_Handling/display/primitives/link_primitives.js +552 -0
  63. package/L2_Handling/display/primitives/lists_primitives.js +262 -0
  64. package/L2_Handling/display/primitives/media_primitives.js +383 -0
  65. package/L2_Handling/display/primitives/primitives.js +19 -0
  66. package/L2_Handling/display/primitives/semantic_element_primitive.js +226 -0
  67. package/L2_Handling/display/primitives/table_primitives.js +528 -0
  68. package/L2_Handling/display/primitives/typography_primitives.js +175 -0
  69. package/L2_Handling/display/specialized/input_request_renderer.js +467 -0
  70. package/L2_Handling/display/specialized/specialized.js +10 -0
  71. package/L2_Handling/hooks/hooks.js +9 -0
  72. package/L2_Handling/hooks/menu_integration.js +57 -0
  73. package/L2_Handling/hooks/widget_hook_manager.js +292 -0
  74. package/L2_Handling/message/message.js +8 -0
  75. package/L2_Handling/message/message_handler.js +701 -0
  76. package/L2_Handling/navigation/navigation.js +8 -0
  77. package/L2_Handling/navigation/navigation_manager.js +403 -0
  78. package/L2_Handling/zhooks/features/cache_live.js +287 -0
  79. package/L2_Handling/zhooks/features/crumbs_live.js +292 -0
  80. package/L2_Handling/zhooks/zhooks_manager.js +65 -0
  81. package/L2_Handling/zvaf/zvaf.js +8 -0
  82. package/L2_Handling/zvaf/zvaf_manager.js +334 -0
  83. package/L3_Abstraction/L3_Abstraction.js +12 -0
  84. package/L3_Abstraction/orchestrator/container_unwrapper.js +101 -0
  85. package/L3_Abstraction/orchestrator/group_renderer.js +698 -0
  86. package/L3_Abstraction/orchestrator/input_event_handler.js +797 -0
  87. package/L3_Abstraction/orchestrator/metadata_processor.js +249 -0
  88. package/L3_Abstraction/orchestrator/navbar_builder.js +201 -0
  89. package/L3_Abstraction/orchestrator/orchestrator.js +13 -0
  90. package/L3_Abstraction/orchestrator/wizard_gate_handler.js +360 -0
  91. package/L3_Abstraction/renderer/renderer.js +1 -0
  92. package/L3_Abstraction/session/session.js +1 -0
  93. package/L4_Orchestration/L4_Orchestration.js +11 -0
  94. package/L4_Orchestration/client/client.js +1 -0
  95. package/L4_Orchestration/facade/facade.js +9 -0
  96. package/L4_Orchestration/facade/manager_registry.js +118 -0
  97. package/L4_Orchestration/facade/renderer_registry.js +274 -0
  98. package/L4_Orchestration/lifecycle/asset_loader.js +255 -0
  99. package/L4_Orchestration/lifecycle/initializer.js +135 -0
  100. package/L4_Orchestration/lifecycle/lifecycle.js +8 -0
  101. package/L4_Orchestration/rendering/facade.js +94 -0
  102. package/L4_Orchestration/rendering/rendering.js +7 -0
  103. package/LICENSE +21 -0
  104. package/README.md +82 -0
  105. package/bifrost_client.js +204 -0
  106. package/bifrost_core.js +1686 -0
  107. package/docs/ARCHITECTURE.md +111 -0
  108. package/docs/PROTOCOL.md +106 -0
  109. package/docs/RENDERERS.md +101 -0
  110. package/docs/SECURITY.md +92 -0
  111. package/package.json +24 -0
  112. package/syntax/prism-zconfig.js +41 -0
  113. package/syntax/prism-zenv.js +69 -0
  114. package/syntax/prism-zolo-theme.css +288 -0
  115. package/syntax/prism-zolo.js +380 -0
  116. package/syntax/prism-zschema.js +38 -0
  117. package/syntax/prism-zspark.js +25 -0
  118. package/syntax/prism-zui.js +68 -0
  119. package/zSys/accessibility/accessibility.js +10 -0
  120. package/zSys/accessibility/emoji_accessibility.js +173 -0
  121. package/zSys/dom/block_utils.js +122 -0
  122. package/zSys/dom/container_utils.js +370 -0
  123. package/zSys/dom/dom.js +13 -0
  124. package/zSys/dom/dom_utils.js +328 -0
  125. package/zSys/dom/encoding_utils.js +117 -0
  126. package/zSys/dom/style_utils.js +71 -0
  127. package/zSys/errors/error_display.js +299 -0
  128. package/zSys/errors/errors.js +10 -0
  129. package/zSys/theme/color_utils.js +274 -0
  130. package/zSys/theme/dark_mode_utils.js +272 -0
  131. package/zSys/theme/size_utils.js +256 -0
  132. package/zSys/theme/spacing_utils.js +405 -0
  133. package/zSys/theme/theme.js +14 -0
  134. package/zSys/theme/zbase.css +1735 -0
  135. package/zSys/theme/zbase_inject.js +161 -0
  136. package/zSys/theme/ztheme_utils.js +305 -0
  137. package/zSys/validation/error_boundary.js +201 -0
  138. package/zSys/validation/validation.js +11 -0
  139. package/zSys/validation/validation_utils.js +238 -0
  140. package/zSys/zSys.js +14 -0
@@ -0,0 +1,287 @@
1
+ /**
2
+ * zHook: cache_live — live cache inspector (INTERNAL DEV TOOL)
3
+ *
4
+ * A self-contained zHook. Enable from the bootstrap with:
5
+ *
6
+ * new BifrostClient({ zHooks: { cache_live: true } });
7
+ *
8
+ * Renders a dark-glass dev panel pinned to the bottom of the viewport that
9
+ * surfaces BOTH sides of the caching system so we can dogfood it:
10
+ * - Frontend: the client TrailStore — the offline-browse cache of visited,
11
+ * rendered pages ("rendered" tier). This is NOT a mirror of zLoader; it is
12
+ * a bfcache-style freeze of pages the user has already seen.
13
+ * - Backend: the server zLoader cache (system / pinned / schema / plugin),
14
+ * echoed over the live socket via &zdebug.cache().
15
+ *
16
+ * It also writes a server-side zCache.log (sibling to zNav.log) so a run's cache
17
+ * behaviour is legible after the fact. Controls let us clear tiers AND drop /
18
+ * restore the WebSocket — the "drop ws" button simulates an offline / disrupted
19
+ * connection so we can dogfood the trail-replay + auto-retry offline experience
20
+ * without pulling the network in DevTools.
21
+ *
22
+ * Like crumbs_live this is a dev tool — it depends on plugins/zdebug.py and is a
23
+ * safe no-op where that plugin is absent. Both will later be refined for users
24
+ * or dropped from zHooks; for now they are dogfood instruments.
25
+ *
26
+ * @module L2_Handling/zhooks/features/cache_live
27
+ * @layer 2 (Handling)
28
+ */
29
+
30
+ const STYLE_ID = 'zhook-cache-live-style';
31
+ const EL_TAG = 'zCache_Debugging';
32
+
33
+ // Dark-glass dev palette with an amber accent — aligned with crumbs_live so the
34
+ // two dogfood panels read as one toolset, while amber keeps "cache" distinct.
35
+ const CSS = `
36
+ ${EL_TAG} {
37
+ position: fixed; bottom: 0; left: 0; right: 0; z-index: 99998;
38
+ box-sizing: border-box; max-height: 42vh; overflow: auto;
39
+ font: 11px/1.45 ui-monospace, SFMono-Regular, Menlo, monospace;
40
+ background: rgba(16,14,10,.93); -webkit-backdrop-filter: blur(8px); backdrop-filter: blur(8px);
41
+ color: #ffe9b0; border-top: 1px solid rgba(255,179,0,.45);
42
+ padding: 8px 14px 10px; box-shadow: 0 -8px 28px rgba(0,0,0,.5);
43
+ }
44
+ ${EL_TAG} .zcl-title {
45
+ display: flex; align-items: center; gap: 10px; flex-wrap: wrap;
46
+ color: #ffd24d; font-weight: 700; letter-spacing: .03em; margin-bottom: 6px;
47
+ }
48
+ ${EL_TAG} .zcl-tag {
49
+ display: inline-flex; align-items: center; gap: 6px;
50
+ color: #ffb300; font-weight: 800;
51
+ }
52
+ ${EL_TAG} .zcl-sub { color: #8a7a52; font-weight: 600; }
53
+ ${EL_TAG} .zcl-spacer { flex: 1 1 auto; }
54
+ ${EL_TAG} .zcl-pill {
55
+ display: inline-flex; align-items: center; gap: 6px;
56
+ padding: 2px 9px; border-radius: 999px; font-weight: 700;
57
+ border: 1px solid transparent;
58
+ }
59
+ ${EL_TAG} .zcl-pill .zcl-dot { width: 7px; height: 7px; border-radius: 50%; }
60
+ ${EL_TAG} .zcl-pill.up { color: #7ee0a6; background: rgba(40,200,120,.10); border-color: rgba(40,200,120,.35); }
61
+ ${EL_TAG} .zcl-pill.up .zcl-dot { background: #34d27e; box-shadow: 0 0 6px #34d27e; }
62
+ ${EL_TAG} .zcl-pill.down { color: #ff9b9b; background: rgba(255,80,80,.10); border-color: rgba(255,80,80,.35); }
63
+ ${EL_TAG} .zcl-pill.down .zcl-dot { background: #ff5a5a; box-shadow: 0 0 6px #ff5a5a; }
64
+ ${EL_TAG} button {
65
+ cursor: pointer; font: inherit; font-weight: 700; line-height: 1;
66
+ color: #ffd24d; background: rgba(255,179,0,.10); border: 1px solid rgba(255,179,0,.30);
67
+ border-radius: 6px; padding: 4px 10px; transition: background .12s ease, border-color .12s ease;
68
+ }
69
+ ${EL_TAG} button:hover { background: rgba(255,179,0,.20); border-color: rgba(255,179,0,.55); }
70
+ ${EL_TAG} button.zcl-ws-drop {
71
+ color: #ffd2d2; background: rgba(255,80,80,.14); border-color: rgba(255,80,80,.40);
72
+ }
73
+ ${EL_TAG} button.zcl-ws-drop:hover { background: rgba(255,80,80,.26); border-color: rgba(255,80,80,.7); }
74
+ ${EL_TAG} button.zcl-ws-up {
75
+ color: #c9ffe0; background: rgba(40,200,120,.16); border-color: rgba(40,200,120,.45);
76
+ }
77
+ ${EL_TAG} button.zcl-ws-up:hover { background: rgba(40,200,120,.28); border-color: rgba(40,200,120,.75); }
78
+ ${EL_TAG} .zcl-cols { display: flex; gap: 28px; flex-wrap: wrap; }
79
+ ${EL_TAG} .zcl-col { min-width: 220px; }
80
+ ${EL_TAG} .zcl-head { color: #ff8f00; font-weight: 700; margin-bottom: 3px; text-transform: uppercase; font-size: 10px; letter-spacing: .06em; }
81
+ ${EL_TAG} .zcl-row { color: #ffe9b0; white-space: pre; }
82
+ ${EL_TAG} .zcl-key { color: #ffd24d; }
83
+ ${EL_TAG} .zcl-empty { color: #9a8350; font-style: italic; }
84
+ ${EL_TAG} .zcl-meta { color: #9a8350; font-size: 10px; margin-top: 6px; }
85
+ ${EL_TAG}.zcl-collapsed { max-height: none; overflow: visible; }
86
+ ${EL_TAG}.zcl-collapsed .zcl-body { display: none; }
87
+ `;
88
+
89
+ function injectStyle() {
90
+ if (document.getElementById(STYLE_ID)) return;
91
+ const style = document.createElement('style');
92
+ style.id = STYLE_ID;
93
+ style.textContent = CSS;
94
+ document.head.appendChild(style);
95
+ }
96
+
97
+ function ensureElement() {
98
+ let el = document.querySelector(EL_TAG);
99
+ if (!el) {
100
+ el = document.createElement(EL_TAG);
101
+ document.body.appendChild(el);
102
+ }
103
+ return el;
104
+ }
105
+
106
+ // Reduce a tier's getStats() result to a short, readable count line.
107
+ function summarize(stats) {
108
+ if (stats == null) return '—';
109
+ if (typeof stats === 'number') return String(stats);
110
+ if (typeof stats !== 'object') return String(stats);
111
+ // Prefer an explicit count-ish field; else count own keys.
112
+ for (const k of ['count', 'size', 'entries', 'length']) {
113
+ if (typeof stats[k] === 'number') return `${stats[k]}`;
114
+ }
115
+ const keys = Object.keys(stats);
116
+ return keys.length ? `${keys.length} keys` : 'empty';
117
+ }
118
+
119
+ function renderTiers(stats) {
120
+ if (!stats || typeof stats !== 'object' || !Object.keys(stats).length) {
121
+ return '<span class="zcl-empty">— none —</span>';
122
+ }
123
+ return Object.keys(stats).map(function (tier) {
124
+ return '<div class="zcl-row"><span class="zcl-key">' + tier + '</span>: ' + summarize(stats[tier]) + '</div>';
125
+ }).join('');
126
+ }
127
+
128
+ export function activate(client) {
129
+ injectStyle();
130
+ const el = ensureElement();
131
+
132
+ const TAG = 'zCACHE-DBG';
133
+ const STY = 'color:#ffb300;font-weight:700';
134
+ let collapsed = localStorage.getItem('zclCollapsed') === '1';
135
+ let feStats = null;
136
+ let beStats = null;
137
+ let lastSentFe = null; // dedupe: only echo to the server when FE stats change
138
+ function log(...args) { console.log('%c' + TAG, STY, ...args); }
139
+
140
+ function wsUp() {
141
+ return !!(client && typeof client.isConnected === 'function' && client.isConnected());
142
+ }
143
+
144
+ function render() {
145
+ el.classList.toggle('zcl-collapsed', collapsed);
146
+ const up = wsUp();
147
+ const pill = up
148
+ ? '<span class="zcl-pill up"><span class="zcl-dot"></span>ws online</span>'
149
+ : '<span class="zcl-pill down"><span class="zcl-dot"></span>ws offline</span>';
150
+ const wsBtn = up
151
+ ? '<button data-act="ws-toggle" class="zcl-ws-drop" type="button">drop ws</button>'
152
+ : '<button data-act="ws-toggle" class="zcl-ws-up" type="button">reconnect</button>';
153
+ const head = '<div class="zcl-title">'
154
+ + '<span class="zcl-tag">⚡ zCache</span><span class="zcl-sub">live · dev</span>'
155
+ + pill
156
+ + '<span class="zcl-spacer"></span>'
157
+ + '<button data-act="refresh" type="button">refresh</button>'
158
+ + '<button data-act="clear-blocks" type="button">clear trail</button>'
159
+ + '<button data-act="clear-be" type="button">clear BE</button>'
160
+ + wsBtn
161
+ + '<button data-act="toggle" type="button">' + (collapsed ? '+' : '–') + '</button>'
162
+ + '</div>';
163
+ const body = '<div class="zcl-body"><div class="zcl-cols">'
164
+ + '<div class="zcl-col"><div class="zcl-head">frontend · trail (visited pages)</div>' + renderTiers(feStats) + '</div>'
165
+ + '<div class="zcl-col"><div class="zcl-head">backend · zLoader</div>' + renderTiers(beStats) + '</div>'
166
+ + '</div><div class="zcl-meta">FE: client TrailStore (offline-browse) · BE: server zLoader (echoed via &zdebug.cache) · logged to zCache.log · "drop ws" simulates offline</div></div>';
167
+ el.innerHTML = head + body;
168
+ }
169
+
170
+ // ── frontend stats ────────────────────────────────────────────────────────
171
+ async function pollFrontend() {
172
+ try {
173
+ if (client.cache && typeof client.cache.getStats === 'function') {
174
+ feStats = await client.cache.getStats();
175
+ }
176
+ } catch (e) { log('FE getStats error →', e); }
177
+ }
178
+
179
+ // ── backend echo + log via &zdebug.cache(action, payload) ──────────────────
180
+ function sendBackend(action) {
181
+ const conn = client && client.connection;
182
+ if (!conn || typeof conn.send !== 'function' || !wsUp()) return;
183
+ installWrap();
184
+ try {
185
+ const payload = JSON.stringify(feStats || {});
186
+ const a = [JSON.stringify(String(action || 'report')), JSON.stringify(payload)];
187
+ conn.send(JSON.stringify({
188
+ event: 'execute_zfunc',
189
+ zfunc: '&zdebug.cache(' + a.join(', ') + ')',
190
+ requestId: 'zdebug-cache-' + Date.now()
191
+ }));
192
+ } catch (e) { /* not connected yet */ }
193
+ }
194
+
195
+ // Claim only our own reply prefix; pass everything else to the prior handler.
196
+ function installWrap() {
197
+ if (!client || !client.hooks || !client.hooks.hooks) return false;
198
+ const orig = client.hooks.hooks.onZFuncResponse;
199
+ if (orig && orig._zCacheWrapped) return true;
200
+ const wrapped = function (msg) {
201
+ const rid = (msg && typeof msg.requestId === 'string') ? msg.requestId : '';
202
+ if (rid.indexOf('zdebug-cache') === 0) {
203
+ if (msg.success) { beStats = msg.result; render(); }
204
+ else { log('backend poll error →', msg.error); }
205
+ return; // claimed
206
+ }
207
+ if (typeof orig === 'function') return orig(msg);
208
+ };
209
+ wrapped._zCacheWrapped = true;
210
+ client.hooks.hooks.onZFuncResponse = wrapped;
211
+ return true;
212
+ }
213
+
214
+ // Drop or restore the live socket to simulate an offline / disrupted
215
+ // connection. disconnect() is a *clean* close, so the connection's
216
+ // auto-reconnect stays out of the way — the socket stays down until we
217
+ // explicitly reconnect, which is exactly the manual offline toggle we want.
218
+ // Reconnect re-creates the ws and re-binds the message handler; ws.onopen
219
+ // fires the onConnected hook, which fulfils any pending offline navigation.
220
+ function toggleWs() {
221
+ if (wsUp()) {
222
+ try { client.disconnect(); log('ws dropped — simulating offline (trail-replay active)'); }
223
+ catch (e) { log('drop ws error →', e); }
224
+ render();
225
+ return;
226
+ }
227
+ log('reconnecting ws…');
228
+ const conn = client && client.connection;
229
+ if (!conn || typeof conn.connect !== 'function') { log('no connection to restore'); return; }
230
+ conn.connect().then(function () {
231
+ if (client.messageHandler && typeof conn.onMessage === 'function') {
232
+ conn.onMessage(function (event) { client.messageHandler.handleMessage(event.data); });
233
+ }
234
+ log('ws reconnected');
235
+ tick(true);
236
+ }).catch(function (e) { log('reconnect failed →', e); render(); });
237
+ }
238
+
239
+ // The real BE cache events stream straight to zCache.log via the server-side
240
+ // logging tap, so the panel no longer needs to poll on a blind timer. We only
241
+ // echo to the server when the FE stats actually change — that kills the 2s
242
+ // identical-payload spam while still refreshing BE counts on real activity.
243
+ async function tick(force) {
244
+ await pollFrontend();
245
+ render();
246
+ const sig = JSON.stringify(feStats || {});
247
+ if (force || sig !== lastSentFe) {
248
+ lastSentFe = sig;
249
+ sendBackend('report');
250
+ }
251
+ }
252
+
253
+ // Delegated controls — survive innerHTML rebuilds.
254
+ el.addEventListener('click', async function (e) {
255
+ const btn = e.target.closest('button');
256
+ if (!btn) return;
257
+ const act = btn.getAttribute('data-act');
258
+ if (act === 'toggle') {
259
+ collapsed = !collapsed;
260
+ localStorage.setItem('zclCollapsed', collapsed ? '1' : '0');
261
+ render();
262
+ return;
263
+ }
264
+ if (act === 'ws-toggle') { toggleWs(); return; }
265
+ if (act === 'refresh') { tick(true); return; }
266
+ if (act === 'clear-blocks') {
267
+ try { await client.cache.clear('rendered'); log('cleared FE trail (visited pages)'); } catch (err) { log('clear trail error →', err); }
268
+ tick();
269
+ return;
270
+ }
271
+ if (act === 'clear-be') { sendBackend('clear'); log('requested BE clear'); return; }
272
+ });
273
+
274
+ render();
275
+
276
+ // Setup loop: wait for the socket, install the reply wrap, then poll on an
277
+ // interval (cache state drifts over time, unlike event-driven crumbs).
278
+ const setup = setInterval(function () {
279
+ if (installWrap()) {
280
+ clearInterval(setup);
281
+ tick(true); // first paint always echoes once
282
+ setInterval(tick, 2000); // subsequent ticks only echo on FE change
283
+ }
284
+ }, 300);
285
+
286
+ log('cache inspector armed — FE trail + BE zLoader echo → zCache.log');
287
+ }
@@ -0,0 +1,292 @@
1
+ /**
2
+ * zHook: crumbs_live — live zCrumbs session overlay (INTERNAL DEV TOOL)
3
+ *
4
+ * A first-class, self-contained zHook. Enable from the bootstrap with:
5
+ *
6
+ * new BifrostClient({ zHooks: { crumbs_live: true } });
7
+ *
8
+ * When active it injects its own CSS + <zCrumbs_Debugging> element, taps the
9
+ * live client WebSocket, and paints session['zCrumbs'] in real time. It polls
10
+ * the server via &zdebug.crumbs() and records lifecycle events via &zdebug.nav()
11
+ * — both server-side zfuncs that only exist where the developer installed them
12
+ * (plugins/zdebug.py). On a deployment without those zfuncs the polls fail
13
+ * silently and the overlay simply shows "no trails".
14
+ *
15
+ * This is the proof case for the zHook abstraction: a shipped, opt-in feature
16
+ * toggled by a data flag — never bespoke page script. Disable by omitting the
17
+ * flag; nothing is created.
18
+ *
19
+ * @module L2_Handling/zhooks/features/crumbs_live
20
+ * @layer 2 (Handling)
21
+ */
22
+
23
+ const STYLE_ID = 'zhook-crumbs-live-style';
24
+ const EL_TAG = 'zCrumbs_Debugging';
25
+
26
+ const CSS = `
27
+ ${EL_TAG} {
28
+ position: fixed; left: 10px; bottom: 10px; z-index: 99999;
29
+ max-width: 380px; max-height: 45vh; overflow: auto;
30
+ font: 11px/1.45 ui-monospace, SFMono-Regular, Menlo, monospace;
31
+ background: rgba(12,14,20,.92); color: #d7e0ee;
32
+ border: 1px solid #2c3340; border-radius: 8px;
33
+ padding: 8px 10px; box-shadow: 0 6px 24px rgba(0,0,0,.45);
34
+ white-space: pre; pointer-events: auto;
35
+ }
36
+ ${EL_TAG} .zdbg-title {
37
+ display: flex; align-items: center; justify-content: space-between;
38
+ gap: 10px; color: #7fd1ff; font-weight: 700;
39
+ letter-spacing: .04em; margin-bottom: 4px;
40
+ }
41
+ ${EL_TAG} .zdbg-toggle {
42
+ flex: none; cursor: pointer; pointer-events: auto;
43
+ border: 1px solid #2c3340; background: rgba(255,255,255,.04);
44
+ color: #7fd1ff; border-radius: 5px; font: inherit; font-weight: 700;
45
+ line-height: 1; padding: 2px 8px;
46
+ }
47
+ ${EL_TAG} .zdbg-toggle:hover { background: rgba(127,209,255,.15); }
48
+ ${EL_TAG} .zdbg-scope { color: #ffd479; }
49
+ ${EL_TAG} .zdbg-empty { color: #6b7585; font-style: italic; }
50
+ ${EL_TAG} .zdbg-meta { color: #6b7585; font-size: 10px; }
51
+ ${EL_TAG}.zdbg-collapsed {
52
+ max-height: none; overflow: visible; white-space: nowrap;
53
+ }
54
+ ${EL_TAG}.zdbg-collapsed .zdbg-body { display: none; }
55
+ `;
56
+
57
+ function injectStyle() {
58
+ if (document.getElementById(STYLE_ID)) return;
59
+ const style = document.createElement('style');
60
+ style.id = STYLE_ID;
61
+ style.textContent = CSS;
62
+ document.head.appendChild(style);
63
+ }
64
+
65
+ function ensureElement() {
66
+ let el = document.querySelector(EL_TAG);
67
+ if (!el) {
68
+ el = document.createElement(EL_TAG);
69
+ document.body.appendChild(el);
70
+ }
71
+ return el;
72
+ }
73
+
74
+ /**
75
+ * Activate the live-crumbs overlay against a connected BifrostCore.
76
+ * @param {Object} client - the BifrostCore instance (window.bifrostClient)
77
+ */
78
+ export function activate(client) {
79
+ injectStyle();
80
+ const el = ensureElement();
81
+
82
+ // Single filterable console tag — type "zCRUMBS-DBG" into the browser
83
+ // console filter to watch only the live crumbs trail + status.
84
+ const TAG = 'zCRUMBS-DBG';
85
+ const STY = 'color:#7fd1ff;font-weight:700';
86
+ let lastJson = null;
87
+ let lastPayload = null;
88
+ let collapsed = localStorage.getItem('zdbgCollapsed') === '1';
89
+ function log(...args) {
90
+ console.log('%c' + TAG, STY, ...args);
91
+ }
92
+
93
+ // Per-tab id: sessionStorage is scoped to THIS tab and survives a reload, so
94
+ // two tabs of the same app get distinct ids while an incognito window gets
95
+ // its own (and its own cookie/zS session). This is the discriminator for the
96
+ // multi-tab / guest-vs-logged-in drift hunt in zNav.log.
97
+ let TAB = sessionStorage.getItem('zdbgTab');
98
+ if (!TAB) {
99
+ TAB = Math.random().toString(36).slice(2, 8);
100
+ sessionStorage.setItem('zdbgTab', TAB);
101
+ }
102
+ log('overlay armed — tab=' + TAB + ' — logs nav lifecycle to zNav.log');
103
+
104
+ function shortScope(s) {
105
+ // Drop the @.UI. prefix and the .zUI. file marker for readability.
106
+ return String(s).replace(/^@\.UI\./, '').replace(/\.zUI\./, ' · ');
107
+ }
108
+
109
+ function render(payload) {
110
+ lastPayload = payload;
111
+ el.classList.toggle('zdbg-collapsed', collapsed);
112
+ const head = '<span class="zdbg-title">zCrumbs · live session'
113
+ + '<button class="zdbg-toggle" type="button" '
114
+ + 'aria-label="' + (collapsed ? 'Expand' : 'Minimize') + '">'
115
+ + (collapsed ? '+' : '–') + '</button></span>';
116
+ // Real session shape: { trails: {scope:[keys]}, _context, _depth_map, _navbar_navigation }
117
+ const trails = (payload && typeof payload === 'object' && payload.trails)
118
+ ? payload.trails : (payload || {});
119
+ const scopes = Object.keys(trails || {});
120
+ if (!scopes.length) {
121
+ el.innerHTML = head + '<div class="zdbg-body">'
122
+ + '<span class="zdbg-empty">— no trails —</span></div>';
123
+ } else {
124
+ const body = scopes.map(function (scope, i) {
125
+ const trail = trails[scope];
126
+ const hasKeys = Array.isArray(trail) && trail.length;
127
+ const t = hasKeys ? trail.join(' › ')
128
+ : '<span class="zdbg-empty">(empty)</span>';
129
+ const here = (i === scopes.length - 1) ? ' ◂ here' : '';
130
+ return '<span class="zdbg-scope">' + shortScope(scope) + here + '</span>\n ' + t;
131
+ }).join('\n');
132
+ const meta = [];
133
+ if (payload && payload._navbar_navigation) meta.push('navbar');
134
+ meta.push(scopes.length + ' scope' + (scopes.length === 1 ? '' : 's'));
135
+ el.innerHTML = head + '<div class="zdbg-body">' + body
136
+ + '\n\n<span class="zdbg-meta">' + meta.join(' · ') + '</span></div>';
137
+ }
138
+ // Log only on change so the filtered console reads as a clean event log.
139
+ const json = JSON.stringify(payload || {});
140
+ if (json !== lastJson) {
141
+ log('trail changed →', payload);
142
+ lastJson = json;
143
+ }
144
+ }
145
+
146
+ // Delegated toggle — survives the full innerHTML rebuild on each render.
147
+ el.addEventListener('click', function (e) {
148
+ if (!e.target.closest('.zdbg-toggle')) return;
149
+ collapsed = !collapsed;
150
+ localStorage.setItem('zdbgCollapsed', collapsed ? '1' : '0');
151
+ render(lastPayload);
152
+ });
153
+ render(null);
154
+
155
+ // Wrap onZFuncResponse so our poll replies render (and are swallowed, avoiding
156
+ // the orchestrator's "no resolver" warn) while real zFuncs still reach their
157
+ // handler.
158
+ function installWrap() {
159
+ if (!client || !client.hooks || !client.hooks.hooks) return false;
160
+ const orig = client.hooks.hooks.onZFuncResponse;
161
+ if (orig && orig._zCrumbsWrapped) return true;
162
+ // Claim ONLY our own requestId prefixes and pass everything else to `orig`.
163
+ // This makes the wrap composable: other zHooks (e.g. cache_live) chain their
164
+ // own wrap and each claims its own replies — no single feature swallows
165
+ // another's. Distinct flag (_zCrumbsWrapped) so chained wraps don't mistake
166
+ // each other for "already installed".
167
+ const wrapped = function (msg) {
168
+ const rid = (msg && typeof msg.requestId === 'string') ? msg.requestId : '';
169
+ if (rid.indexOf('zdebug-crumbs') === 0) {
170
+ if (msg.success) { render(msg.result); }
171
+ else { log('poll error →', msg.error); }
172
+ return; // claimed: crumbs poll reply repaints the overlay
173
+ }
174
+ if (rid.indexOf('zdebug-nav') === 0) {
175
+ return; // claimed: nav() is fire-and-forget — swallow our own reply
176
+ }
177
+ if (typeof orig === 'function') return orig(msg);
178
+ };
179
+ wrapped._zCrumbsWrapped = true;
180
+ client.hooks.hooks.onZFuncResponse = wrapped;
181
+ return true;
182
+ }
183
+
184
+ // Fire a single crumbs fetch over the live socket (initial paint + after each
185
+ // navigation — never on a timer).
186
+ function fetchCrumbs() {
187
+ const conn = client && client.connection;
188
+ // Re-assert the reply wrap before every poll. The display orchestrator
189
+ // registers its own onZFuncResponse during init (after our first install)
190
+ // and `register` overwrites the single hook slot — clobbering our wrap, so
191
+ // later crumbs replies fall through to the orchestrator ("No resolver" warn)
192
+ // and the overlay freezes. installWrap is idempotent and re-captures the
193
+ // orchestrator handler as `orig`, so real zFuncs still pass through.
194
+ installWrap();
195
+ if (conn && typeof conn.send === 'function') {
196
+ try {
197
+ conn.send(JSON.stringify({
198
+ event: 'execute_zfunc',
199
+ zfunc: '&zdebug.crumbs()',
200
+ requestId: 'zdebug-crumbs-' + Date.now()
201
+ }));
202
+ } catch (e) { /* not connected yet */ }
203
+ }
204
+ }
205
+
206
+ // Record a client-observed lifecycle event into zNav.log via the server-side
207
+ // &zdebug.nav() zfunc. Positional args only (the zfunc invoker has no kwargs);
208
+ // JSON.stringify keeps quoting/escaping safe and lets the comma-aware arg
209
+ // splitter keep values intact.
210
+ function sendNav(tag, url, detail) {
211
+ const conn = client && client.connection;
212
+ if (!conn || typeof conn.send !== 'function') return;
213
+ try {
214
+ const a = [
215
+ JSON.stringify(String(tag || '')),
216
+ JSON.stringify(String(url || '')),
217
+ JSON.stringify(String(TAB)),
218
+ JSON.stringify(String(detail || ''))
219
+ ];
220
+ conn.send(JSON.stringify({
221
+ event: 'execute_zfunc',
222
+ zfunc: '&zdebug.nav(' + a.join(', ') + ')',
223
+ requestId: 'zdebug-nav-' + Date.now()
224
+ }));
225
+ } catch (e) { /* not connected yet */ }
226
+ }
227
+
228
+ // Browser-history + tab lifecycle taps. popstate fires on Back/Fwd (and on
229
+ // server-driven history.back()); beforeunload fires when the tab/window is
230
+ // closed or reloaded — the "browser closed but zApp still running" signal.
231
+ window.addEventListener('popstate', function () {
232
+ sendNav('popstate', location.pathname);
233
+ });
234
+ window.addEventListener('beforeunload', function () {
235
+ sendNav('unload', location.pathname);
236
+ });
237
+
238
+ // WebSocket open/close — logged on TRANSITION only (no per-tick noise). A
239
+ // close→open gap in the log = the socket broke and reconnected; a lone close
240
+ // with no reopen = zApp lost this tab.
241
+ let wsUp = null;
242
+ setInterval(function () {
243
+ const up = !!(client && typeof client.isConnected === 'function' && client.isConnected());
244
+ if (up === wsUp) return;
245
+ wsUp = up;
246
+ sendNav(up ? 'ws_open' : 'ws_close', location.pathname);
247
+ }, 1000);
248
+
249
+ // Refresh on NAVIGATION ONLY: wrap connection.send so every outgoing
250
+ // execute_walker (forward nav, menu/brand pick, browser Back/popstate — all
251
+ // route through execute_walker in Bifrost) schedules one crumbs fetch after
252
+ // the server has updated the trail. No interval polling.
253
+ function installSendWrap() {
254
+ const conn = client && client.connection;
255
+ if (!conn || typeof conn.send !== 'function') return false;
256
+ if (conn.send._zdebugWrapped) return true;
257
+ const origSend = conn.send.bind(conn);
258
+ const wrapped = function (data) {
259
+ const r = origSend(data);
260
+ try {
261
+ if (typeof data === 'string' &&
262
+ data.indexOf('"event":"execute_walker"') !== -1) {
263
+ setTimeout(fetchCrumbs, 200);
264
+ }
265
+ } catch (e) { /* ignore */ }
266
+ return r;
267
+ };
268
+ wrapped._zdebugWrapped = true;
269
+ conn.send = wrapped;
270
+ return true;
271
+ }
272
+
273
+ // One-time bootstrap: install BOTH wraps once the socket exists, paint the
274
+ // initial trail, then stop. This is setup — NOT a state poll. Both must
275
+ // succeed before we stop: installWrap is the reply→repaint wrap (renders each
276
+ // crumbs() poll) and installSendWrap is the nav→fetch wrap (schedules the
277
+ // poll). Clearing the loop on installSendWrap alone could strand the overlay
278
+ // with fetches firing but nothing repainting — the "live crumbs never update"
279
+ // symptom.
280
+ const setup = setInterval(function () {
281
+ const responseWrapped = installWrap();
282
+ const sendWrapped = installSendWrap();
283
+ if (responseWrapped && sendWrapped) {
284
+ // 'load' first: it clears the server-side CRUMBS dedupe so the
285
+ // fetchCrumbs() right after always paints a fresh trail snapshot for this
286
+ // (re)load.
287
+ sendNav('load', location.pathname);
288
+ fetchCrumbs();
289
+ clearInterval(setup);
290
+ }
291
+ }, 300);
292
+ }
@@ -0,0 +1,65 @@
1
+ /**
2
+ * zHooks Manager — declarative, opt-in client features (SSOT registry)
3
+ *
4
+ * A zHook is NOT a callback (that is `registerHook(fn)`). A zHook is a *data
5
+ * flag* that toggles a feature the client already ships — closer to WordPress
6
+ * `add_theme_support()` than to a WooCommerce action hook. The bootstrap script
7
+ * declares them:
8
+ *
9
+ * new BifrostClient({ zHooks: { crumbs_live: true } });
10
+ *
11
+ * Trust: zHooks are data (booleans), never code, so they cannot inject behavior
12
+ * into the page — they only switch on capabilities the audited client owns. A
13
+ * deployment (or the server) can therefore reason about exactly what is enabled.
14
+ *
15
+ * Adding a feature: register its module path below and ship a module that
16
+ * exports `activate(client)`. Nothing else in the core needs to change.
17
+ *
18
+ * @module L2_Handling/zhooks/zhooks_manager
19
+ * @layer 2 (Handling)
20
+ */
21
+
22
+ // name → module path (resolved against the client BASE_URL at load time).
23
+ // SSOT for the set of module zHooks (opt-in, dynamically imported features).
24
+ const ZHOOK_REGISTRY = {
25
+ crumbs_live: 'L2_Handling/zhooks/features/crumbs_live.js',
26
+ cache_live: 'L2_Handling/zhooks/features/cache_live.js',
27
+ };
28
+
29
+ // Core zHooks are gated where they live in the client (not imported as modules)
30
+ // — typically default-ON chrome with opt-out, e.g. the connection badge handled
31
+ // in zvaf_manager. Listed here only so explicit toggles aren't flagged unknown.
32
+ const CORE_ZHOOKS = new Set(['badge']);
33
+
34
+ /**
35
+ * Activate every enabled zHook declared in the config.
36
+ * @param {Object} client - the BifrostCore instance
37
+ * @param {Object} config - { <featureName>: true|false }
38
+ * @param {string} baseUrl - client BASE_URL for dynamic feature import
39
+ */
40
+ export async function activateZHooks(client, config, baseUrl) {
41
+ if (!config || typeof config !== 'object') return;
42
+ const logger = client.logger || console;
43
+
44
+ for (const [name, enabled] of Object.entries(config)) {
45
+ if (!enabled) continue;
46
+ // Core zHooks are gated in-core (e.g. badge in zvaf_manager) — not imported.
47
+ if (CORE_ZHOOKS.has(name)) continue;
48
+ const path = ZHOOK_REGISTRY[name];
49
+ if (!path) {
50
+ logger.warn(`[zHooks] Unknown zHook "${name}" — ignored. Known: ${[...Object.keys(ZHOOK_REGISTRY), ...CORE_ZHOOKS].join(', ')}`);
51
+ continue;
52
+ }
53
+ try {
54
+ const mod = await import(`${baseUrl}${path}`);
55
+ if (typeof mod.activate !== 'function') {
56
+ logger.error(`[zHooks] Feature "${name}" has no activate(client) export`);
57
+ continue;
58
+ }
59
+ mod.activate(client);
60
+ logger.debug(`[zHooks] Activated: ${name}`);
61
+ } catch (err) {
62
+ logger.error(`[zHooks] Failed to activate "${name}":`, err);
63
+ }
64
+ }
65
+ }
@@ -0,0 +1,8 @@
1
+ /**
2
+ * L2_Handling/zvaf - zVaF Element Management
3
+ *
4
+ * zVaF element initialization, connection badge, NavBar population.
5
+ * Depends on: L1_Foundation (Logger)
6
+ */
7
+
8
+ export { ZVaFManager } from './zvaf_manager.js';