@lenne.tech/nest-server 11.31.2 → 11.31.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.
- package/.claude/rules/architecture.md +2 -0
- package/.claude/rules/configurable-features.md +2 -0
- package/CLAUDE.md +28 -1
- package/FRAMEWORK-API.md +4 -1
- package/dist/config.env.d.ts +4 -0
- package/dist/config.env.js +32 -2
- package/dist/config.env.js.map +1 -1
- package/dist/core/common/helpers/logging.helper.d.ts +1 -0
- package/dist/core/common/helpers/logging.helper.js +12 -0
- package/dist/core/common/helpers/logging.helper.js.map +1 -1
- package/dist/core/common/helpers/meta.helper.d.ts +1 -0
- package/dist/core/common/helpers/meta.helper.js +32 -1
- package/dist/core/common/helpers/meta.helper.js.map +1 -1
- package/dist/core/common/interfaces/server-options.interface.d.ts +9 -0
- package/dist/core/common/services/email.service.d.ts +3 -1
- package/dist/core/common/services/email.service.js +33 -2
- package/dist/core/common/services/email.service.js.map +1 -1
- package/dist/core/common/services/template.service.js +9 -4
- package/dist/core/common/services/template.service.js.map +1 -1
- package/dist/core/modules/hub/core-hub-actions.controller.d.ts +22 -0
- package/dist/core/modules/hub/core-hub-actions.controller.js +141 -0
- package/dist/core/modules/hub/core-hub-actions.controller.js.map +1 -0
- package/dist/core/modules/hub/core-hub-html.service.d.ts +8 -0
- package/dist/core/modules/hub/core-hub-html.service.js +123 -0
- package/dist/core/modules/hub/core-hub-html.service.js.map +1 -0
- package/dist/core/modules/hub/core-hub.controller.d.ts +56 -0
- package/dist/core/modules/hub/core-hub.controller.js +398 -0
- package/dist/core/modules/hub/core-hub.controller.js.map +1 -0
- package/dist/core/modules/hub/core-hub.module.d.ts +17 -0
- package/dist/core/modules/hub/core-hub.module.js +109 -0
- package/dist/core/modules/hub/core-hub.module.js.map +1 -0
- package/dist/core/modules/hub/core-hub.service.d.ts +18 -0
- package/dist/core/modules/hub/core-hub.service.js +153 -0
- package/dist/core/modules/hub/core-hub.service.js.map +1 -0
- package/dist/core/modules/hub/helpers/hub-client-js.helper.d.ts +1 -0
- package/dist/core/modules/hub/helpers/hub-client-js.helper.js +755 -0
- package/dist/core/modules/hub/helpers/hub-client-js.helper.js.map +1 -0
- package/dist/core/modules/hub/helpers/hub-command-shape.helper.d.ts +1 -0
- package/dist/core/modules/hub/helpers/hub-command-shape.helper.js +33 -0
- package/dist/core/modules/hub/helpers/hub-command-shape.helper.js.map +1 -0
- package/dist/core/modules/hub/helpers/hub-mask.helper.d.ts +1 -0
- package/dist/core/modules/hub/helpers/hub-mask.helper.js +46 -0
- package/dist/core/modules/hub/helpers/hub-mask.helper.js.map +1 -0
- package/dist/core/modules/hub/helpers/hub-mermaid.helper.d.ts +10 -0
- package/dist/core/modules/hub/helpers/hub-mermaid.helper.js +35 -0
- package/dist/core/modules/hub/helpers/hub-mermaid.helper.js.map +1 -0
- package/dist/core/modules/hub/helpers/hub-shell.helper.d.ts +9 -0
- package/dist/core/modules/hub/helpers/hub-shell.helper.js +62 -0
- package/dist/core/modules/hub/helpers/hub-shell.helper.js.map +1 -0
- package/dist/core/modules/hub/hub-action-messages.d.ts +17 -0
- package/dist/core/modules/hub/hub-action-messages.js +21 -0
- package/dist/core/modules/hub/hub-action-messages.js.map +1 -0
- package/dist/core/modules/hub/hub-config.helper.d.ts +11 -0
- package/dist/core/modules/hub/hub-config.helper.js +164 -0
- package/dist/core/modules/hub/hub-config.helper.js.map +1 -0
- package/dist/core/modules/hub/hub-nav.d.ts +12 -0
- package/dist/core/modules/hub/hub-nav.js +43 -0
- package/dist/core/modules/hub/hub-nav.js.map +1 -0
- package/dist/core/modules/hub/hub-ring-buffer.d.ts +22 -0
- package/dist/core/modules/hub/hub-ring-buffer.js +59 -0
- package/dist/core/modules/hub/hub-ring-buffer.js.map +1 -0
- package/dist/core/modules/hub/hub.constants.d.ts +18 -0
- package/dist/core/modules/hub/hub.constants.js +22 -0
- package/dist/core/modules/hub/hub.constants.js.map +1 -0
- package/dist/core/modules/hub/index.d.ts +25 -0
- package/dist/core/modules/hub/index.js +42 -0
- package/dist/core/modules/hub/index.js.map +1 -0
- package/dist/core/modules/hub/interfaces/hub-config.interface.d.ts +115 -0
- package/dist/core/modules/hub/interfaces/hub-config.interface.js +3 -0
- package/dist/core/modules/hub/interfaces/hub-config.interface.js.map +1 -0
- package/dist/core/modules/hub/interfaces/hub-panels.interface.d.ts +231 -0
- package/dist/core/modules/hub/interfaces/hub-panels.interface.js +3 -0
- package/dist/core/modules/hub/interfaces/hub-panels.interface.js.map +1 -0
- package/dist/core/modules/hub/middleware/hub-trace.middleware.d.ts +8 -0
- package/dist/core/modules/hub/middleware/hub-trace.middleware.js +50 -0
- package/dist/core/modules/hub/middleware/hub-trace.middleware.js.map +1 -0
- package/dist/core/modules/hub/services/core-hub-actions.service.d.ts +48 -0
- package/dist/core/modules/hub/services/core-hub-actions.service.js +136 -0
- package/dist/core/modules/hub/services/core-hub-actions.service.js.map +1 -0
- package/dist/core/modules/hub/services/core-hub-db.service.d.ts +19 -0
- package/dist/core/modules/hub/services/core-hub-db.service.js +180 -0
- package/dist/core/modules/hub/services/core-hub-db.service.js.map +1 -0
- package/dist/core/modules/hub/services/core-hub-email.service.d.ts +24 -0
- package/dist/core/modules/hub/services/core-hub-email.service.js +148 -0
- package/dist/core/modules/hub/services/core-hub-email.service.js.map +1 -0
- package/dist/core/modules/hub/services/core-hub-mailbox.service.d.ts +20 -0
- package/dist/core/modules/hub/services/core-hub-mailbox.service.js +103 -0
- package/dist/core/modules/hub/services/core-hub-mailbox.service.js.map +1 -0
- package/dist/core/modules/hub/services/core-hub-migrations.service.d.ts +25 -0
- package/dist/core/modules/hub/services/core-hub-migrations.service.js +115 -0
- package/dist/core/modules/hub/services/core-hub-migrations.service.js.map +1 -0
- package/dist/core/modules/hub/services/core-hub-sources.service.d.ts +28 -0
- package/dist/core/modules/hub/services/core-hub-sources.service.js +187 -0
- package/dist/core/modules/hub/services/core-hub-sources.service.js.map +1 -0
- package/dist/core/modules/hub/services/hub-log-buffer.service.d.ts +24 -0
- package/dist/core/modules/hub/services/hub-log-buffer.service.js +210 -0
- package/dist/core/modules/hub/services/hub-log-buffer.service.js.map +1 -0
- package/dist/core/modules/hub/services/hub-query-profiler.service.d.ts +38 -0
- package/dist/core/modules/hub/services/hub-query-profiler.service.js +235 -0
- package/dist/core/modules/hub/services/hub-query-profiler.service.js.map +1 -0
- package/dist/core/modules/hub/services/hub-trace-buffer.service.d.ts +18 -0
- package/dist/core/modules/hub/services/hub-trace-buffer.service.js +123 -0
- package/dist/core/modules/hub/services/hub-trace-buffer.service.js.map +1 -0
- package/dist/core.module.js +42 -1
- package/dist/core.module.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/tsconfig.build.tsbuildinfo +1 -1
- package/docs/REQUEST-LIFECYCLE.md +1 -0
- package/migration-guides/11.31.2-to-11.31.3.md +135 -0
- package/package.json +5 -4
- package/src/config.env.ts +75 -2
- package/src/core/common/helpers/logging.helper.spec.ts +61 -0
- package/src/core/common/helpers/logging.helper.ts +48 -0
- package/src/core/common/helpers/meta.helper.ts +46 -1
- package/src/core/common/interfaces/server-options.interface.ts +46 -0
- package/src/core/common/services/email.service.ts +33 -1
- package/src/core/common/services/template.service.ts +21 -16
- package/src/core/modules/hub/INTEGRATION-CHECKLIST.md +64 -0
- package/src/core/modules/hub/README.md +159 -0
- package/src/core/modules/hub/core-hub-actions.controller.ts +137 -0
- package/src/core/modules/hub/core-hub-html.service.ts +135 -0
- package/src/core/modules/hub/core-hub.controller.ts +286 -0
- package/src/core/modules/hub/core-hub.module.spec.ts +108 -0
- package/src/core/modules/hub/core-hub.module.ts +159 -0
- package/src/core/modules/hub/core-hub.service.ts +169 -0
- package/src/core/modules/hub/helpers/hub-client-js.helper.ts +768 -0
- package/src/core/modules/hub/helpers/hub-command-shape.helper.spec.ts +48 -0
- package/src/core/modules/hub/helpers/hub-command-shape.helper.ts +47 -0
- package/src/core/modules/hub/helpers/hub-mask.helper.spec.ts +67 -0
- package/src/core/modules/hub/helpers/hub-mask.helper.ts +78 -0
- package/src/core/modules/hub/helpers/hub-mermaid.helper.spec.ts +54 -0
- package/src/core/modules/hub/helpers/hub-mermaid.helper.ts +62 -0
- package/src/core/modules/hub/helpers/hub-shell.helper.spec.ts +106 -0
- package/src/core/modules/hub/helpers/hub-shell.helper.ts +90 -0
- package/src/core/modules/hub/hub-action-messages.ts +47 -0
- package/src/core/modules/hub/hub-config.helper.spec.ts +108 -0
- package/src/core/modules/hub/hub-config.helper.ts +233 -0
- package/src/core/modules/hub/hub-nav.ts +66 -0
- package/src/core/modules/hub/hub-ring-buffer.spec.ts +95 -0
- package/src/core/modules/hub/hub-ring-buffer.ts +101 -0
- package/src/core/modules/hub/hub.constants.ts +84 -0
- package/src/core/modules/hub/index.ts +25 -0
- package/src/core/modules/hub/interfaces/hub-config.interface.ts +265 -0
- package/src/core/modules/hub/interfaces/hub-panels.interface.ts +186 -0
- package/src/core/modules/hub/middleware/hub-trace.middleware.ts +45 -0
- package/src/core/modules/hub/services/core-hub-actions.service.ts +133 -0
- package/src/core/modules/hub/services/core-hub-db.service.ts +185 -0
- package/src/core/modules/hub/services/core-hub-email.service.ts +158 -0
- package/src/core/modules/hub/services/core-hub-mailbox.service.spec.ts +116 -0
- package/src/core/modules/hub/services/core-hub-mailbox.service.ts +121 -0
- package/src/core/modules/hub/services/core-hub-migrations.service.ts +112 -0
- package/src/core/modules/hub/services/core-hub-sources.service.ts +194 -0
- package/src/core/modules/hub/services/hub-log-buffer.service.ts +252 -0
- package/src/core/modules/hub/services/hub-query-profiler.service.ts +274 -0
- package/src/core/modules/hub/services/hub-trace-buffer.service.spec.ts +112 -0
- package/src/core/modules/hub/services/hub-trace-buffer.service.ts +134 -0
- package/src/core.module.ts +61 -1
- package/src/index.ts +6 -0
|
@@ -0,0 +1,755 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.getHubClientJs = getHubClientJs;
|
|
4
|
+
function getHubClientJs() {
|
|
5
|
+
return `/* @lenne.tech/nest-server Hub client runtime */
|
|
6
|
+
(function () {
|
|
7
|
+
'use strict';
|
|
8
|
+
|
|
9
|
+
// Cockpit chrome data — populated from the ADMIN-gated session.json payload by buildLayout(), NOT
|
|
10
|
+
// baked into this public bundle (so an anonymous request reveals nothing of the Hub's structure).
|
|
11
|
+
var PANELS = [];
|
|
12
|
+
var PANEL_GROUPS = [];
|
|
13
|
+
var LINKS = {};
|
|
14
|
+
var LOGOUT = '/iam/sign-out';
|
|
15
|
+
var LAYOUT_BUILT = false;
|
|
16
|
+
var BASE = (window.__HUB_BASE__ || '/hub').replace(/\\/+$/, '');
|
|
17
|
+
var TOKEN = window.__HUB_TOKEN__ || null;
|
|
18
|
+
var POLL_MS = window.__HUB_POLL_MS__ || 5000;
|
|
19
|
+
|
|
20
|
+
// ---- utilities ---------------------------------------------------------
|
|
21
|
+
function el(tag, attrs, children) {
|
|
22
|
+
var node = document.createElement(tag);
|
|
23
|
+
// NOTE: style is applied via the CSSOM (node.style.cssText), NOT setAttribute('style', ...).
|
|
24
|
+
// Under the Hub's strict CSP (style-src 'nonce-...', no 'unsafe-inline') a declarative style
|
|
25
|
+
// attribute is blocked ("Applying inline style violates ..."), but CSSOM style mutations are not,
|
|
26
|
+
// so dynamic styling (bar widths, dialog layout, preview iframes) keeps working.
|
|
27
|
+
if (attrs) { for (var k in attrs) { if (k === 'class') { node.className = attrs[k]; } else if (k === 'text') { node.textContent = attrs[k]; } else if (k === 'style') { node.style.cssText = attrs[k]; } else { node.setAttribute(k, attrs[k]); } } }
|
|
28
|
+
if (children) { for (var i = 0; i < children.length; i++) { var c = children[i]; if (c != null) { node.appendChild(typeof c === 'string' ? document.createTextNode(c) : c); } } }
|
|
29
|
+
return node;
|
|
30
|
+
}
|
|
31
|
+
function clear(node) { while (node.firstChild) { node.removeChild(node.firstChild); } }
|
|
32
|
+
function storedToken() { try { return sessionStorage.getItem('hub.token'); } catch (e) { return null; } }
|
|
33
|
+
function setToken(t) { TOKEN = t; try { if (t) { sessionStorage.setItem('hub.token', t); } else { sessionStorage.removeItem('hub.token'); } } catch (e) {} }
|
|
34
|
+
function authValue() { return TOKEN || storedToken(); }
|
|
35
|
+
|
|
36
|
+
function fmtBytes(n) { if (n == null) { return '–'; } var u = ['B', 'KB', 'MB', 'GB', 'TB']; var i = 0; while (n >= 1024 && i < u.length - 1) { n /= 1024; i++; } return n.toFixed(i ? 1 : 0) + ' ' + u[i]; }
|
|
37
|
+
function fmtMs(n) { if (n == null) { return '–'; } return n < 1 ? n.toFixed(2) + ' ms' : Math.round(n) + ' ms'; }
|
|
38
|
+
function fmtDuration(s) { s = Math.floor(s); var d = Math.floor(s / 86400); var h = Math.floor((s % 86400) / 3600); var m = Math.floor((s % 3600) / 60); var parts = []; if (d) { parts.push(d + 'd'); } if (h) { parts.push(h + 'h'); } if (m) { parts.push(m + 'm'); } parts.push((s % 60) + 's'); return parts.join(' '); }
|
|
39
|
+
function relTime(iso) { try { var t = typeof iso === 'number' ? iso : Date.parse(iso); var diff = (Date.now() - t) / 1000; if (diff < 60) { return Math.floor(diff) + 's ago'; } if (diff < 3600) { return Math.floor(diff / 60) + 'm ago'; } if (diff < 86400) { return Math.floor(diff / 3600) + 'h ago'; } return Math.floor(diff / 86400) + 'd ago'; } catch (e) { return String(iso); } }
|
|
40
|
+
|
|
41
|
+
// ---- fetch -------------------------------------------------------------
|
|
42
|
+
function hubFetch(path, opts) {
|
|
43
|
+
opts = opts || {};
|
|
44
|
+
var headers = opts.headers || {};
|
|
45
|
+
var token = authValue();
|
|
46
|
+
if (token) { headers['Authorization'] = token.indexOf(' ') >= 0 ? token : 'Bearer ' + token; }
|
|
47
|
+
if (opts.method && opts.method !== 'GET') { headers['X-Hub-Request'] = '1'; if (opts.body != null) { headers['Content-Type'] = 'application/json'; } }
|
|
48
|
+
return fetch(BASE + path, {
|
|
49
|
+
body: opts.body != null ? JSON.stringify(opts.body) : undefined,
|
|
50
|
+
credentials: 'same-origin',
|
|
51
|
+
headers: headers,
|
|
52
|
+
method: opts.method || 'GET',
|
|
53
|
+
}).then(function (res) {
|
|
54
|
+
if (res.status === 401 || res.status === 403) { showLogin(); throw new Error('unauthorized'); }
|
|
55
|
+
var ct = res.headers.get('content-type') || '';
|
|
56
|
+
if (ct.indexOf('application/json') >= 0) { return res.json(); }
|
|
57
|
+
return res.text();
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// ---- table -------------------------------------------------------------
|
|
62
|
+
function table(columns, rows) {
|
|
63
|
+
var t = el('table', { class: 'hub-table' });
|
|
64
|
+
var thead = el('thead');
|
|
65
|
+
var htr = el('tr');
|
|
66
|
+
columns.forEach(function (c) { htr.appendChild(el('th', { text: c.label })); });
|
|
67
|
+
thead.appendChild(htr);
|
|
68
|
+
var tbody = el('tbody');
|
|
69
|
+
rows.forEach(function (row) {
|
|
70
|
+
var tr = el('tr');
|
|
71
|
+
columns.forEach(function (c) {
|
|
72
|
+
var v = c.render ? c.render(row) : row[c.key];
|
|
73
|
+
var td = el('td');
|
|
74
|
+
if (v && v.nodeType) { td.appendChild(v); } else { td.textContent = v == null ? '' : String(v); }
|
|
75
|
+
if (c.cls) { td.className = c.cls; }
|
|
76
|
+
tr.appendChild(td);
|
|
77
|
+
});
|
|
78
|
+
tbody.appendChild(tr);
|
|
79
|
+
});
|
|
80
|
+
t.appendChild(thead); t.appendChild(tbody);
|
|
81
|
+
return t;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function tiles(items) {
|
|
85
|
+
var wrap = el('div', { class: 'hub-tiles' });
|
|
86
|
+
items.forEach(function (it) {
|
|
87
|
+
wrap.appendChild(el('div', { class: 'hub-tile' }, [
|
|
88
|
+
el('div', { class: 'hub-tile-label', text: it.label }),
|
|
89
|
+
el('div', { class: 'hub-tile-value ' + (it.cls || ''), text: it.value }),
|
|
90
|
+
it.sub ? el('div', { class: 'hub-tile-sub', text: it.sub }) : null,
|
|
91
|
+
]));
|
|
92
|
+
});
|
|
93
|
+
return wrap;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function jsonView(value) {
|
|
97
|
+
var pre = el('pre', { class: 'hub-json' });
|
|
98
|
+
pre.textContent = JSON.stringify(value, null, 2);
|
|
99
|
+
return pre;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function unavailable(hint) {
|
|
103
|
+
return el('div', { class: 'hub-empty' }, [el('p', { text: hint || 'Not available.' })]);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// ---- confirm dialog ----------------------------------------------------
|
|
107
|
+
function confirmAction(o) {
|
|
108
|
+
var dlg = document.getElementById('hub-confirm');
|
|
109
|
+
clear(dlg);
|
|
110
|
+
var input = el('input', { class: 'hub-input', placeholder: o.keyword });
|
|
111
|
+
var ok = el('button', { class: 'hub-btn hub-btn-danger', text: o.confirmLabel || 'Confirm', disabled: 'disabled' });
|
|
112
|
+
input.addEventListener('input', function () { if (input.value === o.keyword) { ok.removeAttribute('disabled'); } else { ok.setAttribute('disabled', 'disabled'); } });
|
|
113
|
+
ok.addEventListener('click', function () { dlg.close(); o.onConfirm(); });
|
|
114
|
+
var cancel = el('button', { class: 'hub-btn', text: 'Cancel' });
|
|
115
|
+
cancel.addEventListener('click', function () { dlg.close(); });
|
|
116
|
+
dlg.appendChild(el('h3', { text: o.title }));
|
|
117
|
+
dlg.appendChild(el('p', { text: o.message }));
|
|
118
|
+
dlg.appendChild(el('p', { class: 'hub-hint', text: 'Type "' + o.keyword + '" to confirm.' }));
|
|
119
|
+
dlg.appendChild(input);
|
|
120
|
+
dlg.appendChild(el('div', { class: 'hub-dialog-actions' }, [cancel, ok]));
|
|
121
|
+
dlg.showModal();
|
|
122
|
+
input.focus();
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// Run a mutating action: optional type-to-confirm, then POST/DELETE with the X-Hub-Request header.
|
|
126
|
+
function doAction(o) {
|
|
127
|
+
function run() {
|
|
128
|
+
hubFetch(o.path, { body: o.body || {}, method: o.method || 'POST' }).then(function (r) {
|
|
129
|
+
toast((o.label || 'Action') + ': ' + (r && r.ok ? 'done' : 'ok'));
|
|
130
|
+
if (o.onDone) { o.onDone(r); }
|
|
131
|
+
}, function (e) { toast('Failed: ' + e.message, true); });
|
|
132
|
+
}
|
|
133
|
+
if (o.keyword) {
|
|
134
|
+
confirmAction({ confirmLabel: o.confirmLabel, keyword: o.keyword, message: o.message, onConfirm: run, title: o.title });
|
|
135
|
+
} else { run(); }
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function toast(msg, isError) {
|
|
139
|
+
var t = el('div', { class: 'hub-toast' + (isError ? ' hub-toast-err' : ''), text: msg });
|
|
140
|
+
document.body.appendChild(t);
|
|
141
|
+
setTimeout(function () { t.parentNode && t.parentNode.removeChild(t); }, 3500);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// Full-screen login gate: while unauthenticated the app layout stays hidden (body.hub-locked) and
|
|
145
|
+
// ONLY this login card is shown. Email/password → IAM endpoint sets the session cookie (Hub is
|
|
146
|
+
// self-sufficient); a token-paste fallback covers cookie-less setups.
|
|
147
|
+
var loginRendered = false;
|
|
148
|
+
function showLogin() {
|
|
149
|
+
document.body.classList.add('hub-locked');
|
|
150
|
+
stopPoll();
|
|
151
|
+
var gate = document.getElementById('hub-gate');
|
|
152
|
+
if (!gate || loginRendered) { return; }
|
|
153
|
+
loginRendered = true;
|
|
154
|
+
var loginUrl = window.__HUB_LOGIN__ || '/iam/sign-in/email';
|
|
155
|
+
var email = el('input', { autocomplete: 'username', class: 'hub-input', placeholder: 'Email', type: 'email' });
|
|
156
|
+
var pass = el('input', { autocomplete: 'current-password', class: 'hub-input', placeholder: 'Password', type: 'password' });
|
|
157
|
+
var err = el('div', { class: 'hub-hint', style: 'color:var(--err);min-height:16px' });
|
|
158
|
+
var loginBtn = el('button', { class: 'hub-btn hub-btn-primary', style: 'width:100%', text: 'Sign in' });
|
|
159
|
+
function doLogin() {
|
|
160
|
+
err.textContent = '';
|
|
161
|
+
loginBtn.setAttribute('disabled', 'disabled');
|
|
162
|
+
fetch(loginUrl, { body: JSON.stringify({ email: email.value.trim(), password: pass.value }), credentials: 'same-origin', headers: { 'content-type': 'application/json' }, method: 'POST' })
|
|
163
|
+
.then(function (r) { if (r.status >= 200 && r.status < 300) { return r; } throw new Error('Invalid credentials or not an admin.'); })
|
|
164
|
+
.then(function () { enterApp(); })
|
|
165
|
+
.catch(function (e) { err.textContent = e.message; loginBtn.removeAttribute('disabled'); });
|
|
166
|
+
}
|
|
167
|
+
loginBtn.addEventListener('click', doLogin);
|
|
168
|
+
email.addEventListener('keydown', function (e) { if (e.key === 'Enter') { pass.focus(); } });
|
|
169
|
+
pass.addEventListener('keydown', function (e) { if (e.key === 'Enter') { doLogin(); } });
|
|
170
|
+
|
|
171
|
+
var tokenInput = el('input', { class: 'hub-input', placeholder: 'Paste admin token', type: 'password' });
|
|
172
|
+
var tokenBtn = el('button', { class: 'hub-btn', style: 'width:100%', text: 'Use token' });
|
|
173
|
+
tokenBtn.addEventListener('click', function () { setToken(tokenInput.value.trim()); enterApp(); });
|
|
174
|
+
|
|
175
|
+
var card = el('div', { class: 'hub-gate-card' }, [
|
|
176
|
+
// Brand only — no environment badge. The pre-auth login page must reveal nothing about the app.
|
|
177
|
+
el('div', { class: 'hub-gate-brand', text: 'Hub' }),
|
|
178
|
+
el('h2', { text: 'Sign in' }),
|
|
179
|
+
el('p', { class: 'hub-hint', text: 'Admin access required.' }),
|
|
180
|
+
email, pass, err,
|
|
181
|
+
loginBtn,
|
|
182
|
+
el('details', { style: 'margin-top:28px;border-top:1px solid var(--border);padding-top:16px' }, [
|
|
183
|
+
el('summary', { class: 'hub-hint', style: 'cursor:pointer', text: 'Use an access token instead' }),
|
|
184
|
+
tokenInput, tokenBtn,
|
|
185
|
+
]),
|
|
186
|
+
]);
|
|
187
|
+
clear(gate);
|
|
188
|
+
gate.appendChild(card);
|
|
189
|
+
email.focus();
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// Reveal the app after a successful auth check/login and (re)render the active panel. The cockpit
|
|
193
|
+
// chrome is built lazily from the ADMIN-gated session payload (after a fresh login it isn't built
|
|
194
|
+
// yet), so ensure it exists first.
|
|
195
|
+
function enterApp() {
|
|
196
|
+
loginRendered = false;
|
|
197
|
+
if (LAYOUT_BUILT) {
|
|
198
|
+
document.body.classList.remove('hub-locked');
|
|
199
|
+
render(current());
|
|
200
|
+
} else {
|
|
201
|
+
fetchSession();
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// Fetch the ADMIN-gated session payload, build the cockpit chrome from it, reveal the app and render
|
|
206
|
+
// the active panel. A 401/403 keeps only the login form visible.
|
|
207
|
+
function fetchSession() {
|
|
208
|
+
var headers = {};
|
|
209
|
+
var token = authValue();
|
|
210
|
+
if (token) { headers['Authorization'] = token.indexOf(' ') >= 0 ? token : 'Bearer ' + token; }
|
|
211
|
+
return fetch(BASE + '/session.json', { credentials: 'same-origin', headers: headers })
|
|
212
|
+
.then(function (r) {
|
|
213
|
+
if (!r.ok) { showLogin(); return; }
|
|
214
|
+
return r.json().then(function (payload) {
|
|
215
|
+
buildLayout(payload);
|
|
216
|
+
loginRendered = false;
|
|
217
|
+
document.body.classList.remove('hub-locked');
|
|
218
|
+
render(current());
|
|
219
|
+
});
|
|
220
|
+
})
|
|
221
|
+
.catch(function () { showLogin(); });
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// Sign out: clear the session cookie (POST the ADMIN-gated logoutEndpoint, default /iam/sign-out),
|
|
225
|
+
// drop any pasted token, tear down the built layout and show the login gate again. The endpoint is
|
|
226
|
+
// an IAM route (absolute from the origin), NOT under the Hub base path — so no BASE prefix.
|
|
227
|
+
function signOut() {
|
|
228
|
+
var headers = {};
|
|
229
|
+
var token = authValue();
|
|
230
|
+
if (token) { headers['Authorization'] = token.indexOf(' ') >= 0 ? token : 'Bearer ' + token; }
|
|
231
|
+
fetch(LOGOUT, { credentials: 'same-origin', headers: headers, method: 'POST' })
|
|
232
|
+
.then(function () {}, function () {})
|
|
233
|
+
.then(function () {
|
|
234
|
+
setToken(null);
|
|
235
|
+
stopPoll();
|
|
236
|
+
LAYOUT_BUILT = false;
|
|
237
|
+
var appRoot = document.getElementById('hub-app');
|
|
238
|
+
if (appRoot) { clear(appRoot); }
|
|
239
|
+
loginRendered = false;
|
|
240
|
+
showLogin();
|
|
241
|
+
});
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// Build the cockpit chrome (nav + brand/env + external links + topbar + content host) into #hub-app
|
|
245
|
+
// from the session payload. Runs once; a later auth check reuses the built layout.
|
|
246
|
+
function buildLayout(payload) {
|
|
247
|
+
if (LAYOUT_BUILT) { return; }
|
|
248
|
+
payload = payload || {};
|
|
249
|
+
PANELS = payload.panels || [];
|
|
250
|
+
PANEL_GROUPS = payload.panelGroups || [];
|
|
251
|
+
LINKS = payload.links || {};
|
|
252
|
+
LOGOUT = payload.logoutEndpoint || '/iam/sign-out';
|
|
253
|
+
var appRoot = document.getElementById('hub-app');
|
|
254
|
+
if (!appRoot) { return; }
|
|
255
|
+
clear(appRoot);
|
|
256
|
+
|
|
257
|
+
var nav = el('nav', { class: 'hub-nav' });
|
|
258
|
+
nav.appendChild(el('div', { class: 'hub-brand' }, [
|
|
259
|
+
document.createTextNode('Hub '),
|
|
260
|
+
el('span', { class: 'hub-env hub-env-' + (payload.env || ''), text: payload.env || '' }),
|
|
261
|
+
]));
|
|
262
|
+
PANEL_GROUPS.forEach(function (group) {
|
|
263
|
+
var items = PANELS.filter(function (p) { return p.group === group; });
|
|
264
|
+
if (!items.length) { return; }
|
|
265
|
+
nav.appendChild(el('h3', { text: group }));
|
|
266
|
+
items.forEach(function (p) {
|
|
267
|
+
if (p.available === false) {
|
|
268
|
+
// Optional panel whose backing module is absent: greyed out and NOT clickable (no
|
|
269
|
+
// data-panel → the nav click handler ignores it; no href → it is not a link).
|
|
270
|
+
nav.appendChild(el('a', { class: 'hub-nav-disabled', text: p.title, title: p.title + ' — module not enabled' }));
|
|
271
|
+
} else {
|
|
272
|
+
nav.appendChild(el('a', { 'data-panel': p.id, 'data-path': p.path, href: BASE + (p.path ? '/' + p.path : ''), text: p.title }));
|
|
273
|
+
}
|
|
274
|
+
});
|
|
275
|
+
});
|
|
276
|
+
// The external "Permissions" link opens the standalone report — same source as the Routes panel.
|
|
277
|
+
// Hide it when that source is unavailable so there is no dead link (mirrors the greyed nav entry).
|
|
278
|
+
var routesAvailable = PANELS.some(function (p) { return p.id === 'routes' && p.available !== false; });
|
|
279
|
+
var ext = [];
|
|
280
|
+
if (LINKS.swagger) { ext.push(el('a', { class: 'hub-ext', href: LINKS.swagger, rel: 'noopener', target: '_blank', text: 'Swagger' })); }
|
|
281
|
+
if (LINKS.graphql) { ext.push(el('a', { class: 'hub-ext', href: LINKS.graphql, rel: 'noopener', target: '_blank', text: 'GraphQL' })); }
|
|
282
|
+
if (LINKS.permissions && routesAvailable) { ext.push(el('a', { class: 'hub-ext', href: LINKS.permissions, rel: 'noopener', target: '_blank', text: 'Permissions' })); }
|
|
283
|
+
if (LINKS.mailpit) { ext.push(el('a', { class: 'hub-ext', href: LINKS.mailpit, rel: 'noopener', target: '_blank', text: 'Mailpit' })); }
|
|
284
|
+
if (ext.length) { nav.appendChild(el('h3', { text: 'External' })); ext.forEach(function (a) { nav.appendChild(a); }); }
|
|
285
|
+
|
|
286
|
+
var signout = el('button', { class: 'hub-btn hub-signout', text: 'Sign out' });
|
|
287
|
+
signout.addEventListener('click', signOut);
|
|
288
|
+
var topbar = el('header', { class: 'hub-topbar' }, [
|
|
289
|
+
el('h1', { id: 'hub-title', text: 'Dashboard' }),
|
|
290
|
+
el('div', { class: 'hub-meta' }, [
|
|
291
|
+
document.createTextNode('v' + (payload.version || '') + ' '),
|
|
292
|
+
el('span', { class: 'hub-dot hub-dot-ok', id: 'hub-refresh', title: 'auto-refresh' }),
|
|
293
|
+
signout,
|
|
294
|
+
]),
|
|
295
|
+
]);
|
|
296
|
+
var main = el('div', { class: 'hub-main' }, [topbar, el('div', { class: 'hub-content', id: 'hub-content' })]);
|
|
297
|
+
appRoot.appendChild(el('div', { class: 'hub-layout' }, [nav, main]));
|
|
298
|
+
|
|
299
|
+
nav.addEventListener('click', function (e) {
|
|
300
|
+
var a = e.target.closest ? e.target.closest('a[data-panel]') : null;
|
|
301
|
+
if (a) { e.preventDefault(); navigate(findPanel(a.getAttribute('data-path')), true); }
|
|
302
|
+
});
|
|
303
|
+
LAYOUT_BUILT = true;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
// ---- polling manager ---------------------------------------------------
|
|
307
|
+
var activeTimer = null;
|
|
308
|
+
var activeBackoff = POLL_MS;
|
|
309
|
+
function stopPoll() { if (activeTimer) { clearTimeout(activeTimer); activeTimer = null; } }
|
|
310
|
+
function poll(fn, ms) {
|
|
311
|
+
stopPoll();
|
|
312
|
+
activeBackoff = ms || POLL_MS;
|
|
313
|
+
function tick() {
|
|
314
|
+
if (document.hidden) { activeTimer = setTimeout(tick, activeBackoff); return; }
|
|
315
|
+
Promise.resolve(fn()).then(function () { activeBackoff = ms || POLL_MS; setIndicator('ok'); }, function () { activeBackoff = Math.min(activeBackoff * 2, 60000); setIndicator('err'); }).then(function () { activeTimer = setTimeout(tick, activeBackoff); });
|
|
316
|
+
}
|
|
317
|
+
tick();
|
|
318
|
+
}
|
|
319
|
+
function setIndicator(state) { var d = document.getElementById('hub-refresh'); if (d) { d.className = 'hub-dot hub-dot-' + state; } }
|
|
320
|
+
|
|
321
|
+
// ---- router ------------------------------------------------------------
|
|
322
|
+
function current() { var rel = location.pathname.slice(BASE.length).replace(/^\\/+/, ''); return findPanel(rel); }
|
|
323
|
+
function findPanel(rel) { for (var i = 0; i < PANELS.length; i++) { if (PANELS[i].path === rel) { return PANELS[i]; } } return PANELS[0]; }
|
|
324
|
+
|
|
325
|
+
function navigate(panel, push) {
|
|
326
|
+
if (push) { history.pushState({ id: panel.id }, '', BASE + (panel.path ? '/' + panel.path : '')); }
|
|
327
|
+
render(panel);
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
function highlightNav(panel) {
|
|
331
|
+
var links = document.querySelectorAll('.hub-nav a[data-panel]');
|
|
332
|
+
for (var i = 0; i < links.length; i++) { links[i].classList.toggle('active', links[i].getAttribute('data-panel') === panel.id); }
|
|
333
|
+
var title = document.getElementById('hub-title');
|
|
334
|
+
if (title) { title.textContent = panel.title; }
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
function render(panel) {
|
|
338
|
+
stopPoll();
|
|
339
|
+
highlightNav(panel);
|
|
340
|
+
var content = document.getElementById('hub-content');
|
|
341
|
+
clear(content);
|
|
342
|
+
var renderer = Hub.panels[panel.id] || genericRenderer(panel);
|
|
343
|
+
renderer(content, panel);
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
function genericRenderer(panel) {
|
|
347
|
+
return function (content) {
|
|
348
|
+
function load() {
|
|
349
|
+
return hubFetch('/' + panel.json).then(function (data) {
|
|
350
|
+
clear(content);
|
|
351
|
+
if (data && data.available === false) { content.appendChild(unavailable(data.hint)); return; }
|
|
352
|
+
content.appendChild(jsonView(data));
|
|
353
|
+
}, function (e) { clear(content); content.appendChild(unavailable('Failed to load: ' + e.message)); });
|
|
354
|
+
}
|
|
355
|
+
poll(load, POLL_MS);
|
|
356
|
+
};
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
// ---- exported API ------------------------------------------------------
|
|
360
|
+
window.Hub = {
|
|
361
|
+
clear: clear,
|
|
362
|
+
confirmAction: confirmAction,
|
|
363
|
+
doAction: doAction,
|
|
364
|
+
el: el,
|
|
365
|
+
fetch: hubFetch,
|
|
366
|
+
fmtBytes: fmtBytes,
|
|
367
|
+
fmtDuration: fmtDuration,
|
|
368
|
+
fmtMs: fmtMs,
|
|
369
|
+
jsonView: jsonView,
|
|
370
|
+
navigate: navigate,
|
|
371
|
+
panels: {},
|
|
372
|
+
poll: poll,
|
|
373
|
+
relTime: relTime,
|
|
374
|
+
setToken: setToken,
|
|
375
|
+
table: table,
|
|
376
|
+
tiles: tiles,
|
|
377
|
+
unavailable: unavailable,
|
|
378
|
+
};
|
|
379
|
+
|
|
380
|
+
// ---- panel renderers ---------------------------------------------------
|
|
381
|
+
function pollInto(content, json, build) {
|
|
382
|
+
poll(function () {
|
|
383
|
+
return hubFetch('/' + json).then(function (data) {
|
|
384
|
+
clear(content);
|
|
385
|
+
if (data && data.available === false) { content.appendChild(unavailable(data.hint)); return; }
|
|
386
|
+
build(content, data);
|
|
387
|
+
}, function (e) { clear(content); content.appendChild(unavailable('Failed to load: ' + e.message)); });
|
|
388
|
+
}, POLL_MS);
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
Hub.panels.dashboard = function (content) {
|
|
392
|
+
pollInto(content, 'dashboard.json', function (c, d) {
|
|
393
|
+
var mem = d.memory || {};
|
|
394
|
+
c.appendChild(tiles([
|
|
395
|
+
{ label: 'Environment', value: (d.build && d.build.env) || '–' },
|
|
396
|
+
{ label: 'Version', value: (d.build && d.build.version) || '–', sub: d.build && d.build.commit !== 'unknown' ? d.build.commit : '' },
|
|
397
|
+
{ label: 'Uptime', value: fmtDuration(d.uptimeSeconds || 0) },
|
|
398
|
+
{ label: 'Heap used', value: fmtBytes(mem.heapUsed), sub: 'of ' + fmtBytes(mem.heapTotal) },
|
|
399
|
+
{ label: 'RSS', value: fmtBytes(mem.rss) },
|
|
400
|
+
{ label: 'MongoDB', value: (d.mongo && d.mongo.state) || '–', cls: d.mongo && d.mongo.readyState === 1 ? 'ok' : 'warn' },
|
|
401
|
+
]));
|
|
402
|
+
var feats = d.features || {};
|
|
403
|
+
var frow = el('div', { class: 'hub-tiles' });
|
|
404
|
+
Object.keys(feats).sort().forEach(function (k) {
|
|
405
|
+
frow.appendChild(el('div', { class: 'hub-tile' }, [
|
|
406
|
+
el('div', { class: 'hub-tile-label', text: k }),
|
|
407
|
+
el('div', { class: 'hub-tile-value ' + (feats[k] ? 'ok' : ''), text: feats[k] ? 'on' : 'off' }),
|
|
408
|
+
]));
|
|
409
|
+
});
|
|
410
|
+
c.appendChild(el('h3', { text: 'Features' }));
|
|
411
|
+
c.appendChild(frow);
|
|
412
|
+
});
|
|
413
|
+
};
|
|
414
|
+
|
|
415
|
+
Hub.panels.diagnostics = function (content) {
|
|
416
|
+
pollInto(content, 'diagnostics.json', function (c, d) {
|
|
417
|
+
var m = d.memory || {};
|
|
418
|
+
c.appendChild(tiles([
|
|
419
|
+
{ label: 'Node', value: d.nodeVersion }, { label: 'Platform', value: d.platform + '/' + d.arch },
|
|
420
|
+
{ label: 'PID', value: d.pid }, { label: 'Uptime', value: fmtDuration(d.uptimeSeconds || 0) },
|
|
421
|
+
{ label: 'Heap used', value: fmtBytes(m.heapUsed), sub: 'of ' + fmtBytes(m.heapTotal) },
|
|
422
|
+
{ label: 'RSS', value: fmtBytes(m.rss) }, { label: 'External', value: fmtBytes(m.external) },
|
|
423
|
+
{ label: 'ArrayBuffers', value: fmtBytes(m.arrayBuffers) },
|
|
424
|
+
]));
|
|
425
|
+
if (d.buffers && Object.keys(d.buffers).length) {
|
|
426
|
+
c.appendChild(el('h3', { text: 'Collector buffers' }));
|
|
427
|
+
c.appendChild(table([{ key: 'name', label: 'Collector' }, { key: 'state', label: 'State' }, { key: 'fill', label: 'Fill' }],
|
|
428
|
+
Object.keys(d.buffers).map(function (k) { var b = d.buffers[k]; return { fill: b.size + ' / ' + b.capacity, name: k, state: b.enabled ? 'on' : 'off' }; })));
|
|
429
|
+
}
|
|
430
|
+
});
|
|
431
|
+
};
|
|
432
|
+
|
|
433
|
+
Hub.panels.db = function (content) {
|
|
434
|
+
pollInto(content, 'db.json', function (c, d) {
|
|
435
|
+
var s = d.stats || {};
|
|
436
|
+
c.appendChild(tiles([
|
|
437
|
+
{ label: 'Collections', value: s.collections }, { label: 'Objects', value: s.objects },
|
|
438
|
+
{ label: 'Data size', value: fmtBytes(s.dataSize) }, { label: 'Storage', value: fmtBytes(s.storageSize) },
|
|
439
|
+
{ label: 'Index size', value: fmtBytes(s.indexSize) },
|
|
440
|
+
]));
|
|
441
|
+
c.appendChild(table([
|
|
442
|
+
{ key: 'name', label: 'Collection' }, { key: 'count', label: 'Docs' },
|
|
443
|
+
{ cls: '', key: 'size', label: 'Size', render: function (r) { return fmtBytes(r.size); } },
|
|
444
|
+
{ key: 'storageSize', label: 'Storage', render: function (r) { return fmtBytes(r.storageSize); } },
|
|
445
|
+
{ key: 'avgObjSize', label: 'Avg obj', render: function (r) { return r.avgObjSize ? fmtBytes(r.avgObjSize) : '–'; } },
|
|
446
|
+
], d.collections || []));
|
|
447
|
+
});
|
|
448
|
+
};
|
|
449
|
+
|
|
450
|
+
Hub.panels.models = function (content) {
|
|
451
|
+
// Models rarely change; load once instead of polling.
|
|
452
|
+
hubFetch('/models.json').then(function (d) {
|
|
453
|
+
clear(content);
|
|
454
|
+
if (d && d.available === false) { content.appendChild(unavailable(d.hint)); return; }
|
|
455
|
+
content.appendChild(tiles([{ label: 'Models', value: d.modelCount }, { label: 'Relations', value: d.relationCount }]));
|
|
456
|
+
var copyBtn = el('button', { class: 'hub-btn', text: 'Copy Mermaid source' });
|
|
457
|
+
copyBtn.addEventListener('click', function () { try { navigator.clipboard.writeText(d.mermaid); copyBtn.textContent = 'Copied!'; } catch (e) {} });
|
|
458
|
+
var liveLink = el('a', { class: 'hub-btn hub-ext', href: 'https://mermaid.live', target: '_blank', rel: 'noopener', text: 'mermaid.live' });
|
|
459
|
+
content.appendChild(el('div', { class: 'hub-dialog-actions', style: 'justify-content:flex-start' }, [copyBtn, liveLink]));
|
|
460
|
+
var pre = el('pre', { class: 'hub-json' }); pre.textContent = d.mermaid; content.appendChild(pre);
|
|
461
|
+
(d.entities || []).forEach(function (ent) {
|
|
462
|
+
content.appendChild(el('h3', { text: ent.name }));
|
|
463
|
+
content.appendChild(table([{ key: 'name', label: 'Field' }, { key: 'type', label: 'Type' }, { key: 'ref', label: 'Ref', render: function (r) { return r.ref || ''; } }], ent.fields || []));
|
|
464
|
+
});
|
|
465
|
+
}, function (e) { clear(content); content.appendChild(unavailable('Failed to load: ' + e.message)); });
|
|
466
|
+
};
|
|
467
|
+
|
|
468
|
+
Hub.panels.config = function (content) {
|
|
469
|
+
hubFetch('/config.json').then(function (d) { clear(content); content.appendChild(el('p', { class: 'hub-hint', text: 'Secrets are masked.' })); content.appendChild(jsonView(d)); },
|
|
470
|
+
function (e) { clear(content); content.appendChild(unavailable('Failed to load: ' + e.message)); });
|
|
471
|
+
};
|
|
472
|
+
|
|
473
|
+
Hub.panels.migrations = function (content) {
|
|
474
|
+
pollInto(content, 'migrations.json', function (c, d) {
|
|
475
|
+
c.appendChild(tiles([
|
|
476
|
+
{ label: 'Completed', value: (d.completed || []).length, cls: 'ok' },
|
|
477
|
+
{ label: 'Pending', value: (d.pending || []).length, cls: (d.pending || []).length ? 'warn' : '' },
|
|
478
|
+
{ label: 'Last run', value: d.lastRun ? relTime(d.lastRun) : '–' },
|
|
479
|
+
]));
|
|
480
|
+
var runBtn = el('button', { class: 'hub-btn hub-btn-primary', text: 'Run pending' });
|
|
481
|
+
runBtn.addEventListener('click', function () { doAction({ body: { confirm: 'RUN' }, keyword: 'RUN', label: 'Run migrations', message: 'Run all pending migrations?', path: '/actions/migrations/run', title: 'Run pending migrations', onDone: function () { render(current()); } }); });
|
|
482
|
+
var downBtn = el('button', { class: 'hub-btn hub-btn-danger', text: 'Rollback last' });
|
|
483
|
+
downBtn.addEventListener('click', function () { doAction({ body: { confirm: 'DOWN' }, keyword: 'DOWN', label: 'Rollback', message: 'Roll back the last applied migration?', path: '/actions/migrations/down', title: 'Rollback last migration', onDone: function () { render(current()); } }); });
|
|
484
|
+
c.appendChild(el('div', { class: 'hub-dialog-actions', style: 'justify-content:flex-start;margin-bottom:12px' }, [runBtn, downBtn]));
|
|
485
|
+
if (!d.dirAvailable) { c.appendChild(el('p', { class: 'hub-hint', text: 'Migrations directory not found — pending count unavailable.' })); }
|
|
486
|
+
c.appendChild(el('h3', { text: 'Completed' }));
|
|
487
|
+
c.appendChild(table([{ key: 'name', label: 'Migration' }], (d.completed || []).map(function (n) { return { name: n }; })));
|
|
488
|
+
c.appendChild(el('h3', { text: 'Pending' }));
|
|
489
|
+
c.appendChild(table([{ key: 'name', label: 'Migration' }], (d.pending || []).map(function (n) { return { name: n }; })));
|
|
490
|
+
});
|
|
491
|
+
};
|
|
492
|
+
|
|
493
|
+
Hub.panels.files = function (content) {
|
|
494
|
+
pollInto(content, 'files.json', function (c, d) {
|
|
495
|
+
c.appendChild(tiles([{ label: 'Files', value: d.total }, { label: 'Bucket', value: d.bucket }]));
|
|
496
|
+
c.appendChild(table([
|
|
497
|
+
{ key: 'filename', label: 'Filename' },
|
|
498
|
+
{ key: 'length', label: 'Size', render: function (r) { return fmtBytes(r.length); } },
|
|
499
|
+
{ key: 'contentType', label: 'Type', render: function (r) { return r.contentType || '–'; } },
|
|
500
|
+
{ key: 'uploadDate', label: 'Uploaded', render: function (r) { return r.uploadDate ? relTime(r.uploadDate) : '–'; } },
|
|
501
|
+
{ key: 'id', label: '', render: function (r) {
|
|
502
|
+
var b = el('button', { class: 'hub-btn hub-btn-danger', text: 'Delete' });
|
|
503
|
+
b.addEventListener('click', function () { doAction({ body: { confirm: r.filename }, confirmLabel: 'Delete', keyword: r.filename, label: 'Delete file', message: 'Delete "' + r.filename + '"? This cannot be undone.', method: 'DELETE', path: '/actions/files/' + r.id, title: 'Delete file', onDone: function () { render(current()); } }); });
|
|
504
|
+
return b;
|
|
505
|
+
} },
|
|
506
|
+
], d.files || []));
|
|
507
|
+
});
|
|
508
|
+
};
|
|
509
|
+
|
|
510
|
+
function clearButton(name) {
|
|
511
|
+
var b = el('button', { class: 'hub-btn', text: 'Clear buffer' });
|
|
512
|
+
b.addEventListener('click', function () { doAction({ body: { confirm: 'CLEAR' }, keyword: 'CLEAR', label: 'Clear ' + name, message: 'Clear the ' + name + ' buffer?', path: '/actions/collectors/' + name + '/clear', title: 'Clear ' + name, onDone: function () { render(current()); } }); });
|
|
513
|
+
return el('div', { class: 'hub-dialog-actions', style: 'justify-content:flex-start;margin-bottom:12px' }, [b]);
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
Hub.panels.routes = function (content) {
|
|
517
|
+
// Routes / Permissions report (reuses the permissions scanner). Rarely changes — load once.
|
|
518
|
+
hubFetch('/routes.json').then(function (d) {
|
|
519
|
+
clear(content);
|
|
520
|
+
if (d && d.available === false) { content.appendChild(unavailable(d.hint)); return; }
|
|
521
|
+
var s = d.stats || {};
|
|
522
|
+
content.appendChild(tiles([
|
|
523
|
+
{ label: 'Endpoints', value: s.totalEndpoints },
|
|
524
|
+
{ label: 'Modules', value: s.totalModules },
|
|
525
|
+
{ label: 'Models', value: s.totalModels },
|
|
526
|
+
{ label: 'Warnings', value: s.totalWarnings, cls: s.totalWarnings ? 'warn' : '' },
|
|
527
|
+
{ label: 'Endpoint coverage', value: s.endpointCoverage != null ? s.endpointCoverage + '%' : '–' },
|
|
528
|
+
{ label: 'Security coverage', value: s.securityCoverage != null ? s.securityCoverage + '%' : '–' },
|
|
529
|
+
]));
|
|
530
|
+
if ((d.warnings || []).length) {
|
|
531
|
+
content.appendChild(el('h3', { text: 'Warnings' }));
|
|
532
|
+
content.appendChild(table([
|
|
533
|
+
{ key: 'type', label: 'Type', render: function (r) { return el('span', { class: 'hub-chip hub-chip-warn', text: r.type }); } },
|
|
534
|
+
{ key: 'module', label: 'Module' },
|
|
535
|
+
{ key: 'details', label: 'Details' },
|
|
536
|
+
], d.warnings));
|
|
537
|
+
}
|
|
538
|
+
// Flatten every controller/resolver method into one searchable endpoint table.
|
|
539
|
+
var rows = [];
|
|
540
|
+
(d.modules || []).forEach(function (m) {
|
|
541
|
+
[].concat(m.controllers || [], m.resolvers || []).forEach(function (c) {
|
|
542
|
+
(c.methods || []).forEach(function (meth) {
|
|
543
|
+
rows.push({
|
|
544
|
+
className: c.className,
|
|
545
|
+
http: meth.httpMethod || 'GQL',
|
|
546
|
+
module: m.name,
|
|
547
|
+
name: meth.name,
|
|
548
|
+
roles: (meth.roles || []).join(', ') || '–',
|
|
549
|
+
route: meth.route || '',
|
|
550
|
+
});
|
|
551
|
+
});
|
|
552
|
+
});
|
|
553
|
+
});
|
|
554
|
+
var search = el('input', { class: 'hub-input', placeholder: 'Search endpoints…' });
|
|
555
|
+
var host = el('div');
|
|
556
|
+
function draw(filter) {
|
|
557
|
+
clear(host);
|
|
558
|
+
var filtered = rows.filter(function (r) {
|
|
559
|
+
return !filter || (r.module + ' ' + r.className + ' ' + r.name + ' ' + r.route + ' ' + r.roles).toLowerCase().indexOf(filter) >= 0;
|
|
560
|
+
});
|
|
561
|
+
host.appendChild(table([
|
|
562
|
+
{ key: 'module', label: 'Module' },
|
|
563
|
+
{ key: 'className', label: 'Class' },
|
|
564
|
+
{ key: 'http', label: 'Method' },
|
|
565
|
+
{ key: 'name', label: 'Handler' },
|
|
566
|
+
{ key: 'route', label: 'Route' },
|
|
567
|
+
{ key: 'roles', label: 'Roles', render: function (r) {
|
|
568
|
+
// Highlight public / unguarded endpoints (S_EVERYONE or no roles) as a warning.
|
|
569
|
+
var open = r.roles === '–' || r.roles.indexOf('S_EVERYONE') >= 0;
|
|
570
|
+
return el('span', { class: 'hub-chip ' + (open ? 'hub-chip-warn' : 'hub-chip-ok'), text: r.roles });
|
|
571
|
+
} },
|
|
572
|
+
], filtered));
|
|
573
|
+
}
|
|
574
|
+
search.addEventListener('input', function () { draw(search.value.toLowerCase()); });
|
|
575
|
+
content.appendChild(el('h3', { text: 'Endpoints (' + rows.length + ')' }));
|
|
576
|
+
content.appendChild(search);
|
|
577
|
+
content.appendChild(host);
|
|
578
|
+
draw('');
|
|
579
|
+
}, function (e) { clear(content); content.appendChild(unavailable('Failed to load: ' + e.message)); });
|
|
580
|
+
};
|
|
581
|
+
|
|
582
|
+
Hub.panels['error-codes'] = function (content) {
|
|
583
|
+
hubFetch('/error-codes.json').then(function (d) {
|
|
584
|
+
clear(content);
|
|
585
|
+
if (d && d.available === false) { content.appendChild(unavailable(d.hint)); return; }
|
|
586
|
+
var search = el('input', { class: 'hub-input', placeholder: 'Search error codes…' });
|
|
587
|
+
var host = el('div');
|
|
588
|
+
function draw(filter) {
|
|
589
|
+
clear(host);
|
|
590
|
+
var rows = (d.codes || []).filter(function (r) { return !filter || (r.code + ' ' + (r.en || '') + ' ' + (r.de || '')).toLowerCase().indexOf(filter) >= 0; });
|
|
591
|
+
host.appendChild(table([{ key: 'code', label: 'Code' }, { key: 'en', label: 'EN', render: function (r) { return r.en || ''; } }, { key: 'de', label: 'DE', render: function (r) { return r.de || ''; } }], rows));
|
|
592
|
+
}
|
|
593
|
+
search.addEventListener('input', function () { draw(search.value.toLowerCase()); });
|
|
594
|
+
content.appendChild(search); content.appendChild(host); draw('');
|
|
595
|
+
}, function (e) { clear(content); content.appendChild(unavailable('Failed to load: ' + e.message)); });
|
|
596
|
+
};
|
|
597
|
+
|
|
598
|
+
Hub.panels.cron = function (content) {
|
|
599
|
+
pollInto(content, 'cron.json', function (c, d) {
|
|
600
|
+
c.appendChild(table([
|
|
601
|
+
{ key: 'name', label: 'Job' },
|
|
602
|
+
{ key: 'running', label: 'Running', render: function (r) { return el('span', { class: 'hub-chip ' + (r.running ? 'hub-chip-ok' : ''), text: r.running ? 'yes' : 'no' }); } },
|
|
603
|
+
{ key: 'lastDate', label: 'Last', render: function (r) { return r.lastDate ? relTime(r.lastDate) : '–'; } },
|
|
604
|
+
{ key: 'nextDate', label: 'Next', render: function (r) { return r.nextDate ? relTime(r.nextDate) : '–'; } },
|
|
605
|
+
{ key: 'name', label: '', render: function (r) {
|
|
606
|
+
var b = el('button', { class: 'hub-btn', text: 'Trigger' });
|
|
607
|
+
b.addEventListener('click', function () { doAction({ body: { confirm: r.name }, keyword: r.name, label: 'Trigger ' + r.name, message: 'Run cron job "' + r.name + '" now?', path: '/actions/cron/' + encodeURIComponent(r.name) + '/trigger', title: 'Trigger cron job' }); });
|
|
608
|
+
return b;
|
|
609
|
+
} },
|
|
610
|
+
], d.jobs || []));
|
|
611
|
+
if ((d.intervals || []).length || (d.timeouts || []).length) {
|
|
612
|
+
c.appendChild(el('p', { class: 'hub-hint', text: 'Intervals: ' + (d.intervals || []).length + ' · Timeouts: ' + (d.timeouts || []).length }));
|
|
613
|
+
}
|
|
614
|
+
});
|
|
615
|
+
};
|
|
616
|
+
|
|
617
|
+
Hub.panels['auth-migration'] = function (content) {
|
|
618
|
+
pollInto(content, 'auth-migration.json', function (c, d) {
|
|
619
|
+
c.appendChild(tiles([
|
|
620
|
+
{ label: 'Total users', value: d.totalUsers },
|
|
621
|
+
{ label: 'Fully migrated', value: d.fullyMigratedUsers, cls: 'ok' },
|
|
622
|
+
{ label: 'Pending', value: d.pendingMigrationUsers, cls: d.pendingMigrationUsers ? 'warn' : '' },
|
|
623
|
+
{ label: 'Progress', value: (d.migrationPercentage || 0) + '%' },
|
|
624
|
+
{ label: 'Can disable legacy', value: d.canDisableLegacyAuth ? 'yes' : 'no', cls: d.canDisableLegacyAuth ? 'ok' : '' },
|
|
625
|
+
]));
|
|
626
|
+
if ((d.pendingUserEmails || []).length) {
|
|
627
|
+
c.appendChild(el('h3', { text: 'Pending users' }));
|
|
628
|
+
c.appendChild(table([{ key: 'email', label: 'Email' }], (d.pendingUserEmails || []).map(function (e) { return { email: e }; })));
|
|
629
|
+
}
|
|
630
|
+
});
|
|
631
|
+
};
|
|
632
|
+
|
|
633
|
+
Hub.panels.ai = function (content) {
|
|
634
|
+
pollInto(content, 'ai.json', function (c, d) {
|
|
635
|
+
c.appendChild(tiles([{ label: 'Interactions', value: d.totalInteractions }]));
|
|
636
|
+
});
|
|
637
|
+
};
|
|
638
|
+
|
|
639
|
+
Hub.panels.emails = function (content) {
|
|
640
|
+
hubFetch('/emails.json').then(function (d) {
|
|
641
|
+
clear(content);
|
|
642
|
+
if (d && d.available === false) { content.appendChild(unavailable(d.hint)); return; }
|
|
643
|
+
var frame = el('iframe', { class: 'hub-json', sandbox: '', style: 'width:100%;height:520px;background:#fff;border-radius:8px' });
|
|
644
|
+
var wrap = el('div', { style: 'display:flex;gap:16px;align-items:flex-start' });
|
|
645
|
+
var list = el('div', { style: 'flex:0 0 240px' });
|
|
646
|
+
(d.templates || []).forEach(function (t) {
|
|
647
|
+
var localeSel = t.locales && t.locales.length ? ' (' + t.locales.join('/') + ')' : '';
|
|
648
|
+
var btn = el('button', { class: 'hub-btn', style: 'display:block;width:100%;text-align:left;margin-bottom:6px', text: t.name + localeSel });
|
|
649
|
+
btn.addEventListener('click', function () {
|
|
650
|
+
var loc = t.locales && t.locales.length ? '&locale=' + encodeURIComponent(t.locales[0]) : '';
|
|
651
|
+
frame.src = BASE + '/emails/preview?template=' + encodeURIComponent(t.name) + loc;
|
|
652
|
+
});
|
|
653
|
+
list.appendChild(btn);
|
|
654
|
+
});
|
|
655
|
+
wrap.appendChild(list); wrap.appendChild(frame);
|
|
656
|
+
content.appendChild(wrap);
|
|
657
|
+
}, function (e) { clear(content); content.appendChild(unavailable('Failed to load: ' + e.message)); });
|
|
658
|
+
};
|
|
659
|
+
|
|
660
|
+
Hub.panels.logs = function (content) {
|
|
661
|
+
content.appendChild(clearButton('logs'));
|
|
662
|
+
var host = el('div', { class: 'hub-json', style: 'max-height:70vh' });
|
|
663
|
+
content.appendChild(host);
|
|
664
|
+
var cursor;
|
|
665
|
+
var seen = [];
|
|
666
|
+
poll(function () {
|
|
667
|
+
var q = cursor !== undefined ? '?since=' + cursor : '';
|
|
668
|
+
return hubFetch('/logs.json' + q).then(function (d) {
|
|
669
|
+
if (d && d.available === false) { clear(host); host.appendChild(unavailable(d.hint)); stopPoll(); return; }
|
|
670
|
+
cursor = d.cursor;
|
|
671
|
+
(d.records || []).forEach(function (r) {
|
|
672
|
+
seen.push(r); if (seen.length > 1000) { seen.shift(); }
|
|
673
|
+
var line = el('div', { style: 'padding:1px 0' }, [
|
|
674
|
+
el('span', { class: 'hub-chip hub-chip-' + (r.level === 'error' || r.level === 'fatal' ? 'err' : r.level === 'warn' ? 'warn' : 'ok'), text: r.level }),
|
|
675
|
+
document.createTextNode(' ' + (r.context ? '[' + r.context + '] ' : '') + r.message),
|
|
676
|
+
]);
|
|
677
|
+
host.appendChild(line);
|
|
678
|
+
});
|
|
679
|
+
if (d.records && d.records.length) { host.scrollTop = host.scrollHeight; }
|
|
680
|
+
});
|
|
681
|
+
}, 2000);
|
|
682
|
+
};
|
|
683
|
+
|
|
684
|
+
Hub.panels.traces = function (content) {
|
|
685
|
+
pollInto(content, 'traces.json', function (c, d) {
|
|
686
|
+
c.appendChild(clearButton('traces'));
|
|
687
|
+
var s = d.summary || {};
|
|
688
|
+
c.appendChild(tiles([
|
|
689
|
+
{ label: 'Requests', value: s.total }, { label: 'Avg', value: fmtMs(s.avgMs) },
|
|
690
|
+
{ label: 'Slow', value: s.slowCount, cls: s.slowCount ? 'warn' : '' }, { label: 'Errors', value: s.errorCount, cls: s.errorCount ? 'err' : '' },
|
|
691
|
+
]));
|
|
692
|
+
c.appendChild(table([
|
|
693
|
+
{ key: 'method', label: 'Method' },
|
|
694
|
+
{ key: 'path', label: 'Path', render: function (r) { return r.path + (r.graphqlOperation ? ' (' + r.graphqlOperation + ')' : ''); } },
|
|
695
|
+
{ key: 'statusCode', label: 'Status', render: function (r) { return el('span', { class: 'hub-chip hub-chip-' + (r.statusCode >= 500 ? 'err' : r.statusCode >= 400 ? 'warn' : 'ok'), text: String(r.statusCode) }); } },
|
|
696
|
+
{ key: 'durationMs', label: 'Duration', render: function (r) { return fmtMs(r.durationMs); }, cls: '' },
|
|
697
|
+
{ key: 'userId', label: 'User', render: function (r) { return r.userId || '–'; } },
|
|
698
|
+
{ key: 'timestamp', label: 'When', render: function (r) { return relTime(r.timestamp); } },
|
|
699
|
+
], (d.traces || []).slice().reverse()));
|
|
700
|
+
});
|
|
701
|
+
};
|
|
702
|
+
|
|
703
|
+
Hub.panels.queries = function (content) {
|
|
704
|
+
pollInto(content, 'queries.json', function (c, d) {
|
|
705
|
+
c.appendChild(clearButton('queries'));
|
|
706
|
+
var s = d.summary || {};
|
|
707
|
+
c.appendChild(tiles([
|
|
708
|
+
{ label: 'Queries', value: s.total }, { label: 'Avg', value: fmtMs(s.avgMs) },
|
|
709
|
+
{ label: 'Warn', value: s.warnCount, cls: s.warnCount ? 'warn' : '' }, { label: 'Critical', value: s.criticalCount, cls: s.criticalCount ? 'err' : '' },
|
|
710
|
+
{ label: 'Failed', value: s.failedCount, cls: s.failedCount ? 'err' : '' },
|
|
711
|
+
]));
|
|
712
|
+
c.appendChild(el('h3', { text: 'Top templates (N+1 suspects)' }));
|
|
713
|
+
c.appendChild(table([
|
|
714
|
+
{ key: 'count', label: 'Count' }, { key: 'avgMs', label: 'Avg', render: function (r) { return fmtMs(r.avgMs); } },
|
|
715
|
+
{ key: 'maxMs', label: 'Max', render: function (r) { return fmtMs(r.maxMs); } }, { key: 'template', label: 'Shape' },
|
|
716
|
+
], d.topTemplates || []));
|
|
717
|
+
c.appendChild(el('h3', { text: 'Slowest' }));
|
|
718
|
+
c.appendChild(table([
|
|
719
|
+
{ key: 'collection', label: 'Collection' }, { key: 'operation', label: 'Op' },
|
|
720
|
+
{ key: 'durationMs', label: 'Duration', render: function (r) { return el('span', { class: 'hub-chip hub-chip-' + (r.classification === 'critical' ? 'err' : r.classification === 'warn' ? 'warn' : 'ok'), text: fmtMs(r.durationMs) }); } },
|
|
721
|
+
{ key: 'commandSummary', label: 'Shape' },
|
|
722
|
+
], d.slowest || []));
|
|
723
|
+
});
|
|
724
|
+
};
|
|
725
|
+
|
|
726
|
+
Hub.panels.mailbox = function (content) {
|
|
727
|
+
pollInto(content, 'mailbox.json', function (c, d) {
|
|
728
|
+
c.appendChild(el('p', { class: 'hub-hint', text: 'Mode: ' + d.mode + (d.mode === 'capture' ? ' (mail is intercepted, not sent)' : ' (mail is sent and recorded)') }));
|
|
729
|
+
var frame = el('iframe', { class: 'hub-json', sandbox: '', style: 'width:100%;height:420px;background:#fff;border-radius:8px;margin-top:10px' });
|
|
730
|
+
var cols = [
|
|
731
|
+
{ key: 'to', label: 'To' },
|
|
732
|
+
{ key: 'subject', label: 'Subject' },
|
|
733
|
+
{ key: 'templateName', label: 'Template', render: function (r) { return r.templateName || '–'; } },
|
|
734
|
+
{ key: 'timestamp', label: 'When', render: function (r) { return relTime(r.timestamp); } },
|
|
735
|
+
{ key: 'seq', label: '', render: function (r) { var b = el('button', { class: 'hub-btn', text: 'View' }); b.addEventListener('click', function () { frame.src = BASE + '/mailbox/' + r.seq + '/html'; }); return r.hasHtml || r.hasText ? b : el('span', { text: '' }); } },
|
|
736
|
+
];
|
|
737
|
+
c.appendChild(table(cols, (d.mails || []).slice().reverse()));
|
|
738
|
+
c.appendChild(frame);
|
|
739
|
+
});
|
|
740
|
+
};
|
|
741
|
+
|
|
742
|
+
// ---- boot --------------------------------------------------------------
|
|
743
|
+
// Fetch the ADMIN-gated session payload FIRST. The app stays hidden (body.hub-locked, set
|
|
744
|
+
// server-side) and #hub-app empty until it succeeds — so nothing of the cockpit (not even the nav)
|
|
745
|
+
// is ever visible to an unauthenticated visitor; they only see the full-screen login gate. The nav
|
|
746
|
+
// click handler is wired inside buildLayout() once the nav actually exists.
|
|
747
|
+
function boot() {
|
|
748
|
+
window.addEventListener('popstate', function () { if (!document.body.classList.contains('hub-locked')) { render(current()); } });
|
|
749
|
+
fetchSession();
|
|
750
|
+
}
|
|
751
|
+
if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', boot); } else { boot(); }
|
|
752
|
+
})();
|
|
753
|
+
`;
|
|
754
|
+
}
|
|
755
|
+
//# sourceMappingURL=hub-client-js.helper.js.map
|