@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.
- package/L1_Foundation/L1_Foundation.js +13 -0
- package/L1_Foundation/bootstrap/bootstrap.js +11 -0
- package/L1_Foundation/bootstrap/bootstrap_hooks.js +123 -0
- package/L1_Foundation/bootstrap/bootstrap_index.js +15 -0
- package/L1_Foundation/bootstrap/bootstrap_logger.js +135 -0
- package/L1_Foundation/bootstrap/cdn_loader.js +217 -0
- package/L1_Foundation/bootstrap/module_registry.js +102 -0
- package/L1_Foundation/bootstrap/prism_loader.js +164 -0
- package/L1_Foundation/config/client_config.js +110 -0
- package/L1_Foundation/config/config.js +7 -0
- package/L1_Foundation/connection/connection.js +8 -0
- package/L1_Foundation/connection/websocket_connection.js +122 -0
- package/L1_Foundation/constants/bifrost_constants.js +284 -0
- package/L1_Foundation/constants/constants.js +7 -0
- package/L1_Foundation/logger/logger.js +10 -0
- package/L2_Handling/L2_Handling.js +15 -0
- package/L2_Handling/cache/cache.js +22 -0
- package/L2_Handling/cache/cache_constants.js +69 -0
- package/L2_Handling/cache/orchestration/cache_manager.js +299 -0
- package/L2_Handling/cache/orchestration/cache_orchestrator.js +260 -0
- package/L2_Handling/cache/orchestration/orchestration.js +12 -0
- package/L2_Handling/cache/storage/session_manager.js +289 -0
- package/L2_Handling/cache/storage/storage.js +10 -0
- package/L2_Handling/cache/storage/storage_manager.js +590 -0
- package/L2_Handling/display/composite/composite.js +13 -0
- package/L2_Handling/display/composite/dashboard_renderer.js +221 -0
- package/L2_Handling/display/composite/swiper_renderer.js +564 -0
- package/L2_Handling/display/composite/terminal_renderer.js +922 -0
- package/L2_Handling/display/composite/wizard_conditional_renderer.js +274 -0
- package/L2_Handling/display/display.js +30 -0
- package/L2_Handling/display/feedback/feedback.js +11 -0
- package/L2_Handling/display/feedback/progressbar_renderer.js +418 -0
- package/L2_Handling/display/feedback/spinner_renderer.js +246 -0
- package/L2_Handling/display/inputs/button_renderer.js +634 -0
- package/L2_Handling/display/inputs/form_renderer.js +583 -0
- package/L2_Handling/display/inputs/input_renderer.js +658 -0
- package/L2_Handling/display/inputs/inputs.js +12 -0
- package/L2_Handling/display/navigation/menu_renderer.js +206 -0
- package/L2_Handling/display/navigation/navigation.js +11 -0
- package/L2_Handling/display/navigation/navigation_renderer.js +703 -0
- package/L2_Handling/display/orchestration/orchestration.js +11 -0
- package/L2_Handling/display/orchestration/renderer.js +430 -0
- package/L2_Handling/display/orchestration/zdisplay_orchestrator.js +1759 -0
- package/L2_Handling/display/outputs/alert_renderer.js +161 -0
- package/L2_Handling/display/outputs/audio_renderer.js +94 -0
- package/L2_Handling/display/outputs/card_renderer.js +229 -0
- package/L2_Handling/display/outputs/code_renderer.js +66 -0
- package/L2_Handling/display/outputs/dl_renderer.js +131 -0
- package/L2_Handling/display/outputs/header_renderer.js +162 -0
- package/L2_Handling/display/outputs/icon_renderer.js +107 -0
- package/L2_Handling/display/outputs/image_renderer.js +145 -0
- package/L2_Handling/display/outputs/list_renderer.js +190 -0
- package/L2_Handling/display/outputs/outputs.js +19 -0
- package/L2_Handling/display/outputs/table_renderer.js +765 -0
- package/L2_Handling/display/outputs/text_renderer.js +818 -0
- package/L2_Handling/display/outputs/typography_renderer.js +293 -0
- package/L2_Handling/display/outputs/video_renderer.js +116 -0
- package/L2_Handling/display/primitives/document_structure_primitives.js +319 -0
- package/L2_Handling/display/primitives/form_primitives.js +526 -0
- package/L2_Handling/display/primitives/generic_containers.js +109 -0
- package/L2_Handling/display/primitives/interactive_primitives.js +305 -0
- package/L2_Handling/display/primitives/link_primitives.js +552 -0
- package/L2_Handling/display/primitives/lists_primitives.js +262 -0
- package/L2_Handling/display/primitives/media_primitives.js +383 -0
- package/L2_Handling/display/primitives/primitives.js +19 -0
- package/L2_Handling/display/primitives/semantic_element_primitive.js +226 -0
- package/L2_Handling/display/primitives/table_primitives.js +528 -0
- package/L2_Handling/display/primitives/typography_primitives.js +175 -0
- package/L2_Handling/display/specialized/input_request_renderer.js +467 -0
- package/L2_Handling/display/specialized/specialized.js +10 -0
- package/L2_Handling/hooks/hooks.js +9 -0
- package/L2_Handling/hooks/menu_integration.js +57 -0
- package/L2_Handling/hooks/widget_hook_manager.js +292 -0
- package/L2_Handling/message/message.js +8 -0
- package/L2_Handling/message/message_handler.js +701 -0
- package/L2_Handling/navigation/navigation.js +8 -0
- package/L2_Handling/navigation/navigation_manager.js +403 -0
- package/L2_Handling/zhooks/features/cache_live.js +287 -0
- package/L2_Handling/zhooks/features/crumbs_live.js +292 -0
- package/L2_Handling/zhooks/zhooks_manager.js +65 -0
- package/L2_Handling/zvaf/zvaf.js +8 -0
- package/L2_Handling/zvaf/zvaf_manager.js +334 -0
- package/L3_Abstraction/L3_Abstraction.js +12 -0
- package/L3_Abstraction/orchestrator/container_unwrapper.js +101 -0
- package/L3_Abstraction/orchestrator/group_renderer.js +698 -0
- package/L3_Abstraction/orchestrator/input_event_handler.js +797 -0
- package/L3_Abstraction/orchestrator/metadata_processor.js +249 -0
- package/L3_Abstraction/orchestrator/navbar_builder.js +201 -0
- package/L3_Abstraction/orchestrator/orchestrator.js +13 -0
- package/L3_Abstraction/orchestrator/wizard_gate_handler.js +360 -0
- package/L3_Abstraction/renderer/renderer.js +1 -0
- package/L3_Abstraction/session/session.js +1 -0
- package/L4_Orchestration/L4_Orchestration.js +11 -0
- package/L4_Orchestration/client/client.js +1 -0
- package/L4_Orchestration/facade/facade.js +9 -0
- package/L4_Orchestration/facade/manager_registry.js +118 -0
- package/L4_Orchestration/facade/renderer_registry.js +274 -0
- package/L4_Orchestration/lifecycle/asset_loader.js +255 -0
- package/L4_Orchestration/lifecycle/initializer.js +135 -0
- package/L4_Orchestration/lifecycle/lifecycle.js +8 -0
- package/L4_Orchestration/rendering/facade.js +94 -0
- package/L4_Orchestration/rendering/rendering.js +7 -0
- package/LICENSE +21 -0
- package/README.md +82 -0
- package/bifrost_client.js +204 -0
- package/bifrost_core.js +1686 -0
- package/docs/ARCHITECTURE.md +111 -0
- package/docs/PROTOCOL.md +106 -0
- package/docs/RENDERERS.md +101 -0
- package/docs/SECURITY.md +92 -0
- package/package.json +24 -0
- package/syntax/prism-zconfig.js +41 -0
- package/syntax/prism-zenv.js +69 -0
- package/syntax/prism-zolo-theme.css +288 -0
- package/syntax/prism-zolo.js +380 -0
- package/syntax/prism-zschema.js +38 -0
- package/syntax/prism-zspark.js +25 -0
- package/syntax/prism-zui.js +68 -0
- package/zSys/accessibility/accessibility.js +10 -0
- package/zSys/accessibility/emoji_accessibility.js +173 -0
- package/zSys/dom/block_utils.js +122 -0
- package/zSys/dom/container_utils.js +370 -0
- package/zSys/dom/dom.js +13 -0
- package/zSys/dom/dom_utils.js +328 -0
- package/zSys/dom/encoding_utils.js +117 -0
- package/zSys/dom/style_utils.js +71 -0
- package/zSys/errors/error_display.js +299 -0
- package/zSys/errors/errors.js +10 -0
- package/zSys/theme/color_utils.js +274 -0
- package/zSys/theme/dark_mode_utils.js +272 -0
- package/zSys/theme/size_utils.js +256 -0
- package/zSys/theme/spacing_utils.js +405 -0
- package/zSys/theme/theme.js +14 -0
- package/zSys/theme/zbase.css +1735 -0
- package/zSys/theme/zbase_inject.js +161 -0
- package/zSys/theme/ztheme_utils.js +305 -0
- package/zSys/validation/error_boundary.js +201 -0
- package/zSys/validation/validation.js +11 -0
- package/zSys/validation/validation_utils.js +238 -0
- 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
|
+
}
|