ovtools 0.0.0-stage → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +121 -2
- package/assets/common/bond-inspect.sh +368 -0
- package/assets/common/config.sh +263 -0
- package/assets/common/extract-data.sh +197 -0
- package/assets/common/harden-host.sh +37 -0
- package/assets/common/install-deps.sh +93 -0
- package/assets/common/node-enroll.sh +65 -0
- package/assets/common/node-setup.sh +150 -0
- package/assets/common/node-start.sh +90 -0
- package/assets/common/node-stop.sh +18 -0
- package/assets/common/node-upgrade.sh +45 -0
- package/assets/common/pre-run.sh +497 -0
- package/assets/common/promote-validator.sh +104 -0
- package/assets/deploy/10-prepare-host.sh +98 -0
- package/assets/deploy/20-install-toolchain.sh +93 -0
- package/assets/deploy/30-setup-node.sh +82 -0
- package/assets/deploy/40-start-node.sh +75 -0
- package/assets/deploy/50-verify.sh +259 -0
- package/assets/deploy/60-enroll.sh +304 -0
- package/assets/deploy/70-promote.sh +152 -0
- package/assets/deploy/lib.sh +398 -0
- package/assets/manage/10-bond-increase.sh +149 -0
- package/assets/manage/10-diagnose-node.sh +169 -0
- package/assets/manage/20-exit-validator.sh +259 -0
- package/assets/manage/25-recover-node.sh +291 -0
- package/assets/manage/30-status.sh +64 -0
- package/assets/manage/40-exit-withdraw.sh +133 -0
- package/assets/manage/bond_inc.sh +318 -0
- package/assets/manage/dashboard_monitor/app.js +1544 -0
- package/assets/manage/dashboard_monitor/assets/v2.2/css/base.css +113 -0
- package/assets/manage/dashboard_monitor/assets/v2.2/css/components/content.css +133 -0
- package/assets/manage/dashboard_monitor/assets/v2.2/css/components/controls.css +229 -0
- package/assets/manage/dashboard_monitor/assets/v2.2/css/components/data.css +374 -0
- package/assets/manage/dashboard_monitor/assets/v2.2/css/components/icons.css +35 -0
- package/assets/manage/dashboard_monitor/assets/v2.2/css/components/motion.css +57 -0
- package/assets/manage/dashboard_monitor/assets/v2.2/css/components/shell.css +51 -0
- package/assets/manage/dashboard_monitor/assets/v2.2/css/components/sidebar.css +242 -0
- package/assets/manage/dashboard_monitor/assets/v2.2/css/components/topbar.css +138 -0
- package/assets/manage/dashboard_monitor/assets/v2.2/css/fonts.css +30 -0
- package/assets/manage/dashboard_monitor/assets/v2.2/css/synteq-ui-v2.css +25 -0
- package/assets/manage/dashboard_monitor/assets/v2.2/css/theme-dark.css +38 -0
- package/assets/manage/dashboard_monitor/assets/v2.2/css/theme-light.css +36 -0
- package/assets/manage/dashboard_monitor/assets/v2.2/css/tokens.css +61 -0
- package/assets/manage/dashboard_monitor/assets/v2.2/fonts/GoogleSansCode-Medium.ttf +0 -0
- package/assets/manage/dashboard_monitor/assets/v2.2/fonts/GoogleSansCode-Regular.ttf +0 -0
- package/assets/manage/dashboard_monitor/assets/v2.2/fonts/GoogleSansCode-SemiBold.ttf +0 -0
- package/assets/manage/dashboard_monitor/assets/v2.2/js/synteq-ui-v2.js +869 -0
- package/assets/manage/dashboard_monitor/deploy-dashboard.sh +230 -0
- package/assets/manage/dashboard_monitor/favicon.svg +13 -0
- package/assets/manage/dashboard_monitor/index.html +1197 -0
- package/assets/manage/dashboard_monitor/nginx-octra.conf +76 -0
- package/assets/manage/dashboard_monitor/octra-dashboard.service +29 -0
- package/assets/manage/dashboard_monitor/octra-node-prune.cron +12 -0
- package/assets/manage/dashboard_monitor/octra-node-prune.service +16 -0
- package/assets/manage/dashboard_monitor/octra-node-prune.timer +12 -0
- package/assets/manage/dashboard_monitor/prune-node-junk.sh +329 -0
- package/assets/manage/dashboard_monitor/server.py +1887 -0
- package/assets/manage/dashboard_monitor/ssl-certbot.sh +90 -0
- package/assets/manage/dashboard_monitor/styles.css +327 -0
- package/assets/manage/lib.sh +347 -0
- package/assets/manifest.json +143 -0
- package/assets/migrate/00-backup-identity.sh +67 -0
- package/assets/migrate/01-prepare-target.sh +145 -0
- package/assets/migrate/02-authorize-pull.sh +43 -0
- package/assets/migrate/03-pull-state.sh +102 -0
- package/assets/migrate/04-stop-source.sh +85 -0
- package/assets/migrate/05-finalize-target.sh +140 -0
- package/assets/migrate/06-start-target.sh +57 -0
- package/assets/migrate/07-verify.sh +237 -0
- package/assets/migrate/08-rollback.sh +54 -0
- package/assets/migrate/lib.sh +149 -0
- package/assets/upgrade/10-preflight.sh +120 -0
- package/assets/upgrade/20-upgrade.sh +142 -0
- package/assets/upgrade/30-manual-upgrade.sh +129 -0
- package/assets/upgrade/40-storage-prune.sh +65 -0
- package/assets/upgrade/50-status.sh +58 -0
- package/assets/upgrade/lib.sh +232 -0
- package/bin/ovtools.js +32 -0
- package/dist/api/checks.d.ts +6 -0
- package/dist/api/checks.d.ts.map +1 -0
- package/dist/api/checks.js +173 -0
- package/dist/api/checks.js.map +1 -0
- package/dist/api/hostkeys.d.ts +10 -0
- package/dist/api/hostkeys.d.ts.map +1 -0
- package/dist/api/hostkeys.js +58 -0
- package/dist/api/hostkeys.js.map +1 -0
- package/dist/api/jobs.d.ts +33 -0
- package/dist/api/jobs.d.ts.map +1 -0
- package/dist/api/jobs.js +483 -0
- package/dist/api/jobs.js.map +1 -0
- package/dist/api/nodes.d.ts +16 -0
- package/dist/api/nodes.d.ts.map +1 -0
- package/dist/api/nodes.js +126 -0
- package/dist/api/nodes.js.map +1 -0
- package/dist/api/server.d.ts +3 -0
- package/dist/api/server.d.ts.map +1 -0
- package/dist/api/server.js +443 -0
- package/dist/api/server.js.map +1 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +301 -0
- package/dist/cli.js.map +1 -0
- package/dist/constants.d.ts +76 -0
- package/dist/constants.d.ts.map +1 -0
- package/dist/constants.js +150 -0
- package/dist/constants.js.map +1 -0
- package/dist/index.d.ts +18 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +60 -0
- package/dist/index.js.map +1 -0
- package/dist/orchestrator/base.d.ts +52 -0
- package/dist/orchestrator/base.d.ts.map +1 -0
- package/dist/orchestrator/base.js +219 -0
- package/dist/orchestrator/base.js.map +1 -0
- package/dist/orchestrator/deploy.d.ts +33 -0
- package/dist/orchestrator/deploy.d.ts.map +1 -0
- package/dist/orchestrator/deploy.js +313 -0
- package/dist/orchestrator/deploy.js.map +1 -0
- package/dist/orchestrator/manage.d.ts +28 -0
- package/dist/orchestrator/manage.d.ts.map +1 -0
- package/dist/orchestrator/manage.js +267 -0
- package/dist/orchestrator/manage.js.map +1 -0
- package/dist/orchestrator/migrate.d.ts +31 -0
- package/dist/orchestrator/migrate.d.ts.map +1 -0
- package/dist/orchestrator/migrate.js +442 -0
- package/dist/orchestrator/migrate.js.map +1 -0
- package/dist/ssh/client.d.ts +39 -0
- package/dist/ssh/client.d.ts.map +1 -0
- package/dist/ssh/client.js +418 -0
- package/dist/ssh/client.js.map +1 -0
- package/dist/test/core.test.d.ts +2 -0
- package/dist/test/core.test.d.ts.map +1 -0
- package/dist/test/core.test.js +188 -0
- package/dist/test/core.test.js.map +1 -0
- package/dist/types.d.ts +267 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +3 -0
- package/dist/types.js.map +1 -0
- package/dist/utils/assets.d.ts +28 -0
- package/dist/utils/assets.d.ts.map +1 -0
- package/dist/utils/assets.js +583 -0
- package/dist/utils/assets.js.map +1 -0
- package/dist/utils/browser.d.ts +2 -0
- package/dist/utils/browser.d.ts.map +1 -0
- package/dist/utils/browser.js +23 -0
- package/dist/utils/browser.js.map +1 -0
- package/dist/utils/dashboard.d.ts +16 -0
- package/dist/utils/dashboard.d.ts.map +1 -0
- package/dist/utils/dashboard.js +177 -0
- package/dist/utils/dashboard.js.map +1 -0
- package/dist/utils/helpers.d.ts +29 -0
- package/dist/utils/helpers.d.ts.map +1 -0
- package/dist/utils/helpers.js +204 -0
- package/dist/utils/helpers.js.map +1 -0
- package/dist/utils/scripts.d.ts +60 -0
- package/dist/utils/scripts.d.ts.map +1 -0
- package/dist/utils/scripts.js +174 -0
- package/dist/utils/scripts.js.map +1 -0
- package/dist/utils/state.d.ts +12 -0
- package/dist/utils/state.d.ts.map +1 -0
- package/dist/utils/state.js +44 -0
- package/dist/utils/state.js.map +1 -0
- package/dist/utils/tar.d.ts +11 -0
- package/dist/utils/tar.d.ts.map +1 -0
- package/dist/utils/tar.js +93 -0
- package/dist/utils/tar.js.map +1 -0
- package/dist/utils/update.d.ts +9 -0
- package/dist/utils/update.d.ts.map +1 -0
- package/dist/utils/update.js +134 -0
- package/dist/utils/update.js.map +1 -0
- package/dist/utils/websocket.d.ts +10 -0
- package/dist/utils/websocket.d.ts.map +1 -0
- package/dist/utils/websocket.js +203 -0
- package/dist/utils/websocket.js.map +1 -0
- package/package.json +55 -4
- package/public/app.js +2712 -0
- package/public/assets/v2.2/css/base.css +113 -0
- package/public/assets/v2.2/css/components/content.css +133 -0
- package/public/assets/v2.2/css/components/controls.css +229 -0
- package/public/assets/v2.2/css/components/data.css +373 -0
- package/public/assets/v2.2/css/components/icons.css +35 -0
- package/public/assets/v2.2/css/components/motion.css +57 -0
- package/public/assets/v2.2/css/components/shell.css +51 -0
- package/public/assets/v2.2/css/components/sidebar.css +242 -0
- package/public/assets/v2.2/css/components/topbar.css +138 -0
- package/public/assets/v2.2/css/fonts.css +30 -0
- package/public/assets/v2.2/css/synteq-ui-v2.css +25 -0
- package/public/assets/v2.2/css/theme-dark.css +38 -0
- package/public/assets/v2.2/css/theme-light.css +36 -0
- package/public/assets/v2.2/css/tokens.css +61 -0
- package/public/assets/v2.2/fonts/GoogleSansCode-Medium.ttf +0 -0
- package/public/assets/v2.2/fonts/GoogleSansCode-Regular.ttf +0 -0
- package/public/assets/v2.2/fonts/GoogleSansCode-SemiBold.ttf +0 -0
- package/public/assets/v2.2/js/synteq-ui-v2.js +869 -0
- package/public/favicon.svg +7 -0
- package/public/index.html +1080 -0
- package/public/styles.css +552 -0
|
@@ -0,0 +1,1544 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Octra Live Monitor - dashboard runtime
|
|
3
|
+
* =====================================
|
|
4
|
+
* Polls /api/status and /api/logs, then renders every section in place.
|
|
5
|
+
* Navigation, theme, dock and gauge CSS are handled by Synteq UI V2.
|
|
6
|
+
*
|
|
7
|
+
* Design notes
|
|
8
|
+
* - Values are written with textContent (never innerHTML) so node output can
|
|
9
|
+
* never inject markup.
|
|
10
|
+
* - A failed poll never blanks the dashboard: the last good snapshot stays on
|
|
11
|
+
* screen and the topbar switches to "stale" so the operator sees its age.
|
|
12
|
+
* - Derived numbers come from the backend snapshot, keeping the browser free
|
|
13
|
+
* of heuristics that could disagree with the server.
|
|
14
|
+
*/
|
|
15
|
+
(function () {
|
|
16
|
+
'use strict';
|
|
17
|
+
|
|
18
|
+
var POLL_MS = 5000;
|
|
19
|
+
var LOG_POLL_MS = 5000;
|
|
20
|
+
var STALE_AFTER = 15000; // ms without a good poll before flagging stale
|
|
21
|
+
var LOG_LINES = 200;
|
|
22
|
+
|
|
23
|
+
var state = {
|
|
24
|
+
status: null,
|
|
25
|
+
lastOkAt: 0,
|
|
26
|
+
failures: 0,
|
|
27
|
+
paused: false,
|
|
28
|
+
logs: [],
|
|
29
|
+
logLevel: 'all',
|
|
30
|
+
logQuery: '',
|
|
31
|
+
logPaused: false,
|
|
32
|
+
fetching: false
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
/* -- tiny DOM helpers ------------------------------------------- */
|
|
36
|
+
function $(id) { return document.getElementById(id); }
|
|
37
|
+
|
|
38
|
+
function setText(id, value) {
|
|
39
|
+
var node = $(id);
|
|
40
|
+
if (!node) return;
|
|
41
|
+
var next = (value === undefined || value === null) ? '-' : String(value);
|
|
42
|
+
if (node.textContent !== next) node.textContent = next;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function clearBadge(node, keep) {
|
|
46
|
+
if (!node) return;
|
|
47
|
+
node.classList.remove('v2-badge--ok', 'v2-badge--warn', 'v2-badge--err', 'v2-badge--brand');
|
|
48
|
+
if (keep) node.classList.add(keep);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/* -- formatting ------------------------------------------------- */
|
|
52
|
+
function isNum(v) { return typeof v === 'number' && isFinite(v); }
|
|
53
|
+
|
|
54
|
+
function fmtInt(v) {
|
|
55
|
+
if (!isNum(v)) return '-';
|
|
56
|
+
return v.toLocaleString('en-US');
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function fmtFixed(v, digits) {
|
|
60
|
+
if (!isNum(v)) return '-';
|
|
61
|
+
return v.toFixed(digits === undefined ? 1 : digits);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function fmtAge(sec) {
|
|
65
|
+
if (!isNum(sec)) return '-';
|
|
66
|
+
if (sec < 1) return '<1s';
|
|
67
|
+
if (sec < 60) return Math.round(sec) + 's';
|
|
68
|
+
if (sec < 3600) return Math.round(sec / 60) + 'm';
|
|
69
|
+
if (sec < 86400) return Math.floor(sec / 3600) + 'h ' + Math.round((sec % 3600) / 60) + 'm';
|
|
70
|
+
return Math.floor(sec / 86400) + 'd ' + Math.floor((sec % 86400) / 3600) + 'h';
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function fmtCompact(v) {
|
|
74
|
+
if (!isNum(v)) return '-';
|
|
75
|
+
var abs = Math.abs(v);
|
|
76
|
+
if (abs >= 1e12) return (v / 1e12).toFixed(2) + 'T';
|
|
77
|
+
if (abs >= 1e9) return (v / 1e9).toFixed(2) + 'B';
|
|
78
|
+
if (abs >= 1e6) return (v / 1e6).toFixed(2) + 'M';
|
|
79
|
+
if (abs >= 1e3) return (v / 1e3).toFixed(1) + 'K';
|
|
80
|
+
return String(Math.round(v));
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** Ledger amounts: 6 decimals of precision, never trailing noise. */
|
|
84
|
+
function fmtToken(v, withUnit) {
|
|
85
|
+
if (!isNum(v)) return '-';
|
|
86
|
+
var text = v.toLocaleString('en-US', {
|
|
87
|
+
minimumFractionDigits: 2,
|
|
88
|
+
maximumFractionDigits: 6
|
|
89
|
+
});
|
|
90
|
+
// Non-breaking space keeps the unit glued to the amount when a stat wraps.
|
|
91
|
+
return withUnit === false ? text : (text + '\u00A0OCT');
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function fmtBytes(bytes) {
|
|
95
|
+
if (!isNum(bytes) || bytes <= 0) return '0 B';
|
|
96
|
+
if (bytes >= 1024 * 1024 * 1024) return (bytes / (1024 * 1024 * 1024)).toFixed(2) + ' GB';
|
|
97
|
+
if (bytes >= 1024 * 1024) return (bytes / (1024 * 1024)).toFixed(1) + ' MB';
|
|
98
|
+
if (bytes >= 1024) return (bytes / 1024).toFixed(0) + ' KB';
|
|
99
|
+
return Math.round(bytes) + ' B';
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function fmtClock(iso) {
|
|
103
|
+
if (!iso) return '-';
|
|
104
|
+
var d = new Date(iso);
|
|
105
|
+
if (isNaN(d.getTime())) return '-';
|
|
106
|
+
return d.toLocaleTimeString('en-GB', { hour12: false });
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function fmtShortHash(hash, head, tail) {
|
|
110
|
+
if (!hash || hash === '-') return '-';
|
|
111
|
+
var h = String(hash);
|
|
112
|
+
if (h.length <= (head + tail + 1)) return h;
|
|
113
|
+
return h.slice(0, head) + '...' + h.slice(-tail);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function shortAddr(addr) { return fmtShortHash(addr, 12, 8); }
|
|
117
|
+
|
|
118
|
+
function num(v) { return isNum(v) ? v : 0; }
|
|
119
|
+
|
|
120
|
+
/* -- gauges ----------------------------------------------------- */
|
|
121
|
+
/** Quorum-style bar: brand coloured, no threshold colouring. */
|
|
122
|
+
function setBar(fillId, trackId, pct) {
|
|
123
|
+
var fill = $(fillId);
|
|
124
|
+
if (!fill) return;
|
|
125
|
+
var value = num(pct);
|
|
126
|
+
if (window.SynteqV2 && SynteqV2.renderGauge) {
|
|
127
|
+
SynteqV2.renderGauge(fill, value, { kind: 'brand' });
|
|
128
|
+
} else {
|
|
129
|
+
fill.style.width = Math.max(0, Math.min(100, value)) + '%';
|
|
130
|
+
}
|
|
131
|
+
var track = $(trackId);
|
|
132
|
+
if (track) track.setAttribute('aria-valuenow', String(Math.round(value)));
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/** Resource bar: framework threshold colouring (ok / warn / err). */
|
|
136
|
+
function setResourceBar(fillId, trackId, pct) {
|
|
137
|
+
var fill = $(fillId);
|
|
138
|
+
if (!fill) return;
|
|
139
|
+
var value = num(pct);
|
|
140
|
+
if (window.SynteqV2 && SynteqV2.renderGauge) {
|
|
141
|
+
SynteqV2.renderGauge(fill, value);
|
|
142
|
+
} else {
|
|
143
|
+
fill.style.width = Math.max(0, Math.min(100, value)) + '%';
|
|
144
|
+
}
|
|
145
|
+
var track = $(trackId);
|
|
146
|
+
if (track) track.setAttribute('aria-valuenow', String(Math.round(value)));
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/* -- status vocabulary ------------------------------------------ */
|
|
150
|
+
/* `short` is the one-word form the bottom navigation badge shows ("Active"),
|
|
151
|
+
`text` the sentence-style form the topbar/hero use. */
|
|
152
|
+
var STATUS_MAP = {
|
|
153
|
+
validator_active: { text: 'validator active', short: 'Active', cls: 'v2-badge--ok', dot: '' },
|
|
154
|
+
observer_synced: { text: 'observer synced', short: 'Observer', cls: 'v2-badge--ok', dot: '' },
|
|
155
|
+
syncing: { text: 'syncing', short: 'Syncing', cls: 'v2-badge--warn', dot: 'v2-dot--warn' },
|
|
156
|
+
offline: { text: 'offline', short: 'Offline', cls: 'v2-badge--err', dot: 'v2-dot--err' },
|
|
157
|
+
connecting: { text: 'connecting', short: 'Connecting', cls: '', dot: 'v2-dot--idle' },
|
|
158
|
+
unreachable: { text: 'api unreachable', short: 'Unreachable', cls: 'v2-badge--err', dot: 'v2-dot--err' }
|
|
159
|
+
};
|
|
160
|
+
|
|
161
|
+
function paintDot(node, dotClass) {
|
|
162
|
+
if (!node) return;
|
|
163
|
+
node.classList.remove('v2-dot--ok', 'v2-dot--warn', 'v2-dot--err', 'v2-dot--idle',
|
|
164
|
+
'v2-dot--stopped', 'v2-dot--reboot');
|
|
165
|
+
if (dotClass) node.classList.add(dotClass);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/* Hero status and the bottom-navigation badge: a coloured dot plus coloured
|
|
169
|
+
text (no badge chrome). The tone class is what the smoke test asserts, and
|
|
170
|
+
what CSS colours. */
|
|
171
|
+
var HERO_TONE = {
|
|
172
|
+
'v2-badge--ok': 'dash-status--ok',
|
|
173
|
+
'v2-badge--warn': 'dash-status--warn',
|
|
174
|
+
'v2-badge--err': 'dash-status--err'
|
|
175
|
+
};
|
|
176
|
+
|
|
177
|
+
function paintStatusTone(node, info) {
|
|
178
|
+
if (!node) return;
|
|
179
|
+
var tone = HERO_TONE[info.cls] || 'dash-status--info';
|
|
180
|
+
['dash-status--ok', 'dash-status--warn', 'dash-status--err', 'dash-status--info']
|
|
181
|
+
.forEach(function (name) { node.classList.remove(name); });
|
|
182
|
+
node.classList.add(tone);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function setHeroStatus(info) {
|
|
186
|
+
setText('hero-status-text', info.text);
|
|
187
|
+
paintDot($('hero-status-dot'), info.dot);
|
|
188
|
+
paintStatusTone($('hero-status'), info);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/* Bottom-right of the navigation dock: one word, the validator state. */
|
|
192
|
+
function setDockStatus(info) {
|
|
193
|
+
setText('dock-status-text', info.short || info.text);
|
|
194
|
+
paintDot($('dock-dot'), info.dot);
|
|
195
|
+
paintStatusTone($('dock-status'), info);
|
|
196
|
+
var host = $('dock-status');
|
|
197
|
+
if (host) host.title = 'Validator status: ' + info.text;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function applyStatus(key) {
|
|
201
|
+
var info = STATUS_MAP[key] || STATUS_MAP.connecting;
|
|
202
|
+
|
|
203
|
+
clearBadge($('topbar-status'), info.cls);
|
|
204
|
+
setText('topbar-status-text', info.text);
|
|
205
|
+
paintDot($('topbar-dot'), info.dot);
|
|
206
|
+
|
|
207
|
+
setHeroStatus(info);
|
|
208
|
+
setDockStatus(info);
|
|
209
|
+
|
|
210
|
+
document.body.setAttribute('data-node-status', key);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/* -- health checks ---------------------------------------------- */
|
|
214
|
+
function buildChecks(d) {
|
|
215
|
+
var n = d.node || {}, s = d.system || {}, c = d.consensus || {}, p = d.peers || {};
|
|
216
|
+
var proc = n.process || {};
|
|
217
|
+
var procOnline = String(proc.status || '').toLowerCase() === 'online';
|
|
218
|
+
var checks = [];
|
|
219
|
+
|
|
220
|
+
checks.push({
|
|
221
|
+
label: 'RPC endpoint',
|
|
222
|
+
status: n.rpc_ready ? 'ok' : 'err',
|
|
223
|
+
detail: n.rpc_ready ? ('port ' + n.api_port + ' -> 200 OK') : ('no answer on port ' + n.api_port)
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
checks.push({
|
|
227
|
+
label: 'Node process',
|
|
228
|
+
status: procOnline ? 'ok' : 'err',
|
|
229
|
+
detail: proc.name ? (proc.name + ' | ' + (proc.status || 'unknown')) : 'pm2 state unavailable'
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
checks.push({
|
|
233
|
+
label: 'Voting',
|
|
234
|
+
status: c.voting === 'enabled' ? 'ok' : 'warn',
|
|
235
|
+
detail: c.voting === 'enabled' ? 'validator is voting' : (c.voting_reason || c.voting || 'unknown')
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
var rate = (n.epochs || {}).rate_per_min;
|
|
239
|
+
var stalled = !!(n.epochs || {}).stalled;
|
|
240
|
+
var lastAdvance = num((n.epochs || {}).last_change_age_sec);
|
|
241
|
+
checks.push({
|
|
242
|
+
label: 'Epoch progression',
|
|
243
|
+
status: stalled ? 'err' : (num(rate) > 0 ? 'ok' : (lastAdvance < 90 ? 'ok' : 'warn')),
|
|
244
|
+
detail: stalled
|
|
245
|
+
? 'head epoch stopped advancing'
|
|
246
|
+
: (num(rate) > 0
|
|
247
|
+
? (fmtFixed(rate, 1) + ' epochs/min')
|
|
248
|
+
: 'collecting samples... (' + fmtAge(lastAdvance) + ' since last advance)')
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
var lag = num(p.lag_max);
|
|
252
|
+
checks.push({
|
|
253
|
+
label: 'Sync vs network',
|
|
254
|
+
status: lag > 20 ? 'err' : (lag > 2 ? 'warn' : 'ok'),
|
|
255
|
+
detail: lag > 0
|
|
256
|
+
? (lag + ' epoch' + (lag === 1 ? '' : 's') + ' behind network head')
|
|
257
|
+
: 'in sync with network head'
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
var connected = num(p.connected);
|
|
261
|
+
checks.push({
|
|
262
|
+
label: 'Peer connectivity',
|
|
263
|
+
status: connected >= 8 ? 'ok' : (connected > 0 ? 'warn' : 'err'),
|
|
264
|
+
detail: connected + ' connected | ' + num(p.known) + ' known | ' + num(p.consensus_peers) + ' consensus'
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
var q = c.quorum || {};
|
|
268
|
+
var quorumPct = Math.max(num(q.prevote_pct), num(q.precommit_pct));
|
|
269
|
+
var step = String(c.step || '').toLowerCase();
|
|
270
|
+
var roundFresh = step === 'propose' || !isNum((c.participants || {}).newest_age_sec);
|
|
271
|
+
checks.push({
|
|
272
|
+
label: 'Quorum',
|
|
273
|
+
status: roundFresh ? 'ok' : (quorumPct >= 100 ? 'ok' : (quorumPct >= 60 ? 'warn' : 'err')),
|
|
274
|
+
detail: roundFresh
|
|
275
|
+
? 'round starting - ' + num(q.prevotes) + '/' + num(q.quorum) + ' prevotes so far'
|
|
276
|
+
: (num(q.prevotes) + '/' + num(q.quorum) + ' prevotes | ' + num(q.precommits) + '/' + num(q.quorum) + ' precommits')
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
var memPct = num(s.mem_percent);
|
|
280
|
+
var cpuPct = num(s.cpu_percent);
|
|
281
|
+
checks.push({
|
|
282
|
+
label: 'Compute headroom',
|
|
283
|
+
status: (memPct >= 90 || cpuPct >= 95) ? 'err' : ((memPct >= 80 || cpuPct >= 80) ? 'warn' : 'ok'),
|
|
284
|
+
detail: 'cpu ' + fmtFixed(cpuPct, 1) + '% | mem ' + fmtFixed(memPct, 1) + '%'
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
var diskPct = Math.max(num(s.disk_percent), num(s.disk_root_percent));
|
|
288
|
+
checks.push({
|
|
289
|
+
label: 'Disk space',
|
|
290
|
+
status: diskPct >= 90 ? 'err' : (diskPct >= 80 ? 'warn' : 'ok'),
|
|
291
|
+
detail: 'data ' + fmtFixed(s.disk_percent, 1) + '% | root ' + fmtFixed(s.disk_root_percent, 1) + '%'
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
var restarts = num(proc.restarts);
|
|
295
|
+
checks.push({
|
|
296
|
+
label: 'Restart count',
|
|
297
|
+
status: restarts === 0 ? 'ok' : (restarts < 3 ? 'warn' : 'err'),
|
|
298
|
+
detail: restarts === 0 ? 'no restarts since pm2 start' : (restarts + ' restart(s) recorded')
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
var counts = (d.logs || {}).counts || {};
|
|
302
|
+
var errors = num(counts.error) + num(counts.fatal);
|
|
303
|
+
checks.push({
|
|
304
|
+
label: 'Log errors',
|
|
305
|
+
status: errors === 0 ? 'ok' : 'warn',
|
|
306
|
+
detail: errors === 0 ? 'no errors in the last 120 lines' : (errors + ' error line(s) in the last 120 lines')
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
return checks;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
var CHECK_BADGE = {
|
|
313
|
+
ok: { cls: 'v2-badge--ok', text: 'OK' },
|
|
314
|
+
warn: { cls: 'v2-badge--warn', text: 'WARN' },
|
|
315
|
+
err: { cls: 'v2-badge--err', text: 'FAIL' }
|
|
316
|
+
};
|
|
317
|
+
|
|
318
|
+
function renderHealth(d) {
|
|
319
|
+
var list = $('health-list');
|
|
320
|
+
if (!list) return [];
|
|
321
|
+
var checks = buildChecks(d);
|
|
322
|
+
var okCount = 0, warnCount = 0, errCount = 0;
|
|
323
|
+
|
|
324
|
+
list.textContent = '';
|
|
325
|
+
checks.forEach(function (check) {
|
|
326
|
+
var info = CHECK_BADGE[check.status] || CHECK_BADGE.ok;
|
|
327
|
+
if (check.status === 'ok') okCount++;
|
|
328
|
+
else if (check.status === 'warn') warnCount++;
|
|
329
|
+
else errCount++;
|
|
330
|
+
|
|
331
|
+
var row = document.createElement('div');
|
|
332
|
+
row.className = 'health__row';
|
|
333
|
+
row.setAttribute('role', 'listitem');
|
|
334
|
+
|
|
335
|
+
var badge = document.createElement('span');
|
|
336
|
+
badge.className = 'v2-badge ' + info.cls;
|
|
337
|
+
badge.textContent = info.text;
|
|
338
|
+
|
|
339
|
+
var label = document.createElement('span');
|
|
340
|
+
label.className = 'health__label';
|
|
341
|
+
label.textContent = check.label;
|
|
342
|
+
|
|
343
|
+
var detail = document.createElement('span');
|
|
344
|
+
detail.className = 'health__detail';
|
|
345
|
+
detail.textContent = check.detail;
|
|
346
|
+
detail.title = check.detail;
|
|
347
|
+
|
|
348
|
+
row.appendChild(badge);
|
|
349
|
+
row.appendChild(label);
|
|
350
|
+
row.appendChild(detail);
|
|
351
|
+
list.appendChild(row);
|
|
352
|
+
});
|
|
353
|
+
|
|
354
|
+
setText('health-meta', okCount + ' ok | ' + warnCount + ' warn | ' + errCount + ' fail');
|
|
355
|
+
return checks;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
/* -- alerts ----------------------------------------------------- */
|
|
359
|
+
function collectAlerts(d, checks) {
|
|
360
|
+
var alerts = [];
|
|
361
|
+
var n = d.node || {}, c = d.consensus || {}, p = d.peers || {}, s = d.system || {};
|
|
362
|
+
var proc = n.process || {};
|
|
363
|
+
|
|
364
|
+
(checks || []).forEach(function (check) {
|
|
365
|
+
if (check.status === 'err') {
|
|
366
|
+
alerts.push({ id: 'check|' + check.label, level: 'error', title: check.label, text: check.detail });
|
|
367
|
+
}
|
|
368
|
+
});
|
|
369
|
+
|
|
370
|
+
if (!n.rpc_ready) {
|
|
371
|
+
alerts.push({
|
|
372
|
+
id: 'rpc',
|
|
373
|
+
level: 'error',
|
|
374
|
+
title: 'Node RPC unreachable',
|
|
375
|
+
text: 'The node process is ' + (proc.status || 'unknown') + ' and not answering on port ' + n.api_port + '.'
|
|
376
|
+
});
|
|
377
|
+
}
|
|
378
|
+
if (n.rpc_ready && c.voting === 'disabled') {
|
|
379
|
+
alerts.push({
|
|
380
|
+
id: 'voting',
|
|
381
|
+
level: 'warn',
|
|
382
|
+
title: 'Voting disabled',
|
|
383
|
+
text: c.voting_reason || 'The node runs as an observer until voting resumes.'
|
|
384
|
+
});
|
|
385
|
+
}
|
|
386
|
+
var ahead = num(p.ahead_count);
|
|
387
|
+
if (ahead > 0) {
|
|
388
|
+
alerts.push({ id: 'peers-ahead', level: 'warn', title: 'Behind ' + ahead + ' peer(s)', text: 'Peers report a newer head epoch than this node.' });
|
|
389
|
+
}
|
|
390
|
+
var lag = num(p.lag_max);
|
|
391
|
+
if (lag > 2 && ahead === 0) {
|
|
392
|
+
alerts.push({
|
|
393
|
+
id: 'lag',
|
|
394
|
+
level: lag > 20 ? 'error' : 'warn',
|
|
395
|
+
title: 'Sync lag ' + lag + ' epoch(s)',
|
|
396
|
+
text: 'Local head is ' + lag + ' epoch(s) behind the network head.'
|
|
397
|
+
});
|
|
398
|
+
}
|
|
399
|
+
if ((n.epochs || {}).stalled) {
|
|
400
|
+
alerts.push({
|
|
401
|
+
id: 'stall',
|
|
402
|
+
level: 'error',
|
|
403
|
+
title: 'Epoch seems stalled',
|
|
404
|
+
text: 'Head epoch has not advanced for ' + fmtAge((n.epochs || {}).last_change_age_sec) + '.'
|
|
405
|
+
});
|
|
406
|
+
}
|
|
407
|
+
if (num(proc.restarts) > 0) {
|
|
408
|
+
alerts.push({
|
|
409
|
+
id: 'restarts',
|
|
410
|
+
level: 'info',
|
|
411
|
+
title: 'Process restarted ' + num(proc.restarts) + 'x',
|
|
412
|
+
text: 'pm2 counters reset only when the process is restarted.'
|
|
413
|
+
});
|
|
414
|
+
}
|
|
415
|
+
if (num(s.swap_percent) >= 50) {
|
|
416
|
+
alerts.push({
|
|
417
|
+
id: 'swap',
|
|
418
|
+
level: 'warn',
|
|
419
|
+
title: 'Swap in use',
|
|
420
|
+
text: fmtFixed(s.swap_percent, 1) + '% of swap is allocated - memory pressure is likely.'
|
|
421
|
+
});
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
var order = { error: 0, warn: 1, info: 2 };
|
|
425
|
+
alerts.sort(function (a, b) { return order[a.level] - order[b.level]; });
|
|
426
|
+
return alerts;
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
var ALERT_ICON = {
|
|
430
|
+
error: '<path d="M10.29 3.86 1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"/><line x1="12" y1="9" x2="12" y2="13"/><line x1="12" y1="17" x2="12.01" y2="17"/>',
|
|
431
|
+
warn: '<circle cx="12" cy="12" r="9"/><line x1="12" y1="8" x2="12" y2="13"/><line x1="12" y1="17" x2="12.01" y2="17"/>',
|
|
432
|
+
info: '<circle cx="12" cy="12" r="9"/><line x1="12" y1="11" x2="12" y2="16"/><line x1="12" y1="8" x2="12.01" y2="8"/>'
|
|
433
|
+
};
|
|
434
|
+
|
|
435
|
+
/* -- alert dismissal -------------------------------------------- */
|
|
436
|
+
/* "Sync lag 3 epoch(s)" / "Process restarted 1x" came back on every 5 s poll,
|
|
437
|
+
so the operator could not get rid of them. Each row now carries a close
|
|
438
|
+
button; the dismissal is remembered per alert kind (localStorage) and is
|
|
439
|
+
dropped as soon as the condition is gone, so a returning problem shows up
|
|
440
|
+
again. A link offers to bring the hidden rows back (2026-11-06). */
|
|
441
|
+
var DISMISS_KEY = 'octra-dash-dismissed-alerts';
|
|
442
|
+
var dismissedIds = {};
|
|
443
|
+
var lastAlerts = [];
|
|
444
|
+
|
|
445
|
+
function storageGet(key) {
|
|
446
|
+
try {
|
|
447
|
+
if (typeof localStorage !== 'undefined' && localStorage) return localStorage.getItem(key);
|
|
448
|
+
} catch (err) { /* private mode: memory only */ }
|
|
449
|
+
return null;
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
function storageSet(key, value) {
|
|
453
|
+
try {
|
|
454
|
+
if (typeof localStorage !== 'undefined' && localStorage) localStorage.setItem(key, value);
|
|
455
|
+
} catch (err) { /* private mode: memory only */ }
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
function loadDismissed() {
|
|
459
|
+
dismissedIds = {};
|
|
460
|
+
var raw = storageGet(DISMISS_KEY);
|
|
461
|
+
if (!raw) return dismissedIds;
|
|
462
|
+
try {
|
|
463
|
+
var parsed = JSON.parse(raw);
|
|
464
|
+
if (parsed && typeof parsed === 'object') dismissedIds = parsed;
|
|
465
|
+
} catch (err) {
|
|
466
|
+
dismissedIds = {};
|
|
467
|
+
}
|
|
468
|
+
return dismissedIds;
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
function saveDismissed() {
|
|
472
|
+
storageSet(DISMISS_KEY, JSON.stringify(dismissedIds));
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
function alertId(alert) {
|
|
476
|
+
var raw = String((alert && (alert.id || alert.title)) || 'alert');
|
|
477
|
+
return raw.toLowerCase().replace(/[^a-z0-9|]+/g, '-');
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
function renderAlerts(alerts) {
|
|
481
|
+
var box = $('alerts');
|
|
482
|
+
if (!box) return;
|
|
483
|
+
lastAlerts = alerts || [];
|
|
484
|
+
box.textContent = '';
|
|
485
|
+
|
|
486
|
+
var stored = loadDismissed();
|
|
487
|
+
var live = {}, shown = [], hidden = 0;
|
|
488
|
+
lastAlerts.forEach(function (alert) {
|
|
489
|
+
var id = alertId(alert);
|
|
490
|
+
live[id] = true;
|
|
491
|
+
if (stored[id]) { hidden++; return; }
|
|
492
|
+
shown.push(alert);
|
|
493
|
+
});
|
|
494
|
+
// forget dismissals whose alert is gone: a problem that returns is news
|
|
495
|
+
var pruned = false;
|
|
496
|
+
Object.keys(stored).forEach(function (id) {
|
|
497
|
+
if (!live[id]) { delete stored[id]; pruned = true; }
|
|
498
|
+
});
|
|
499
|
+
if (pruned) saveDismissed();
|
|
500
|
+
|
|
501
|
+
if (!lastAlerts.length) {
|
|
502
|
+
box.hidden = true;
|
|
503
|
+
return;
|
|
504
|
+
}
|
|
505
|
+
box.hidden = false;
|
|
506
|
+
|
|
507
|
+
shown.slice(0, 5).forEach(function (alert) {
|
|
508
|
+
var row = document.createElement('div');
|
|
509
|
+
row.className = 'alert alert--' + alert.level;
|
|
510
|
+
|
|
511
|
+
var svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
|
|
512
|
+
svg.setAttribute('class', 'v2-icon v2-icon--14');
|
|
513
|
+
svg.setAttribute('viewBox', '0 0 24 24');
|
|
514
|
+
svg.setAttribute('aria-hidden', 'true');
|
|
515
|
+
svg.innerHTML = ALERT_ICON[alert.level] || ALERT_ICON.info;
|
|
516
|
+
|
|
517
|
+
var text = document.createElement('div');
|
|
518
|
+
text.className = 'alert__text';
|
|
519
|
+
var strong = document.createElement('strong');
|
|
520
|
+
strong.textContent = alert.title + ' - ';
|
|
521
|
+
text.appendChild(strong);
|
|
522
|
+
text.appendChild(document.createTextNode(alert.text));
|
|
523
|
+
|
|
524
|
+
var close = document.createElement('button');
|
|
525
|
+
close.type = 'button';
|
|
526
|
+
close.className = 'alert__close';
|
|
527
|
+
close.setAttribute('aria-label', 'Dismiss: ' + alert.title);
|
|
528
|
+
close.title = 'Dismiss this message';
|
|
529
|
+
var x = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
|
|
530
|
+
x.setAttribute('class', 'v2-icon v2-icon--12');
|
|
531
|
+
x.setAttribute('viewBox', '0 0 24 24');
|
|
532
|
+
x.setAttribute('aria-hidden', 'true');
|
|
533
|
+
x.innerHTML = '<line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/>';
|
|
534
|
+
close.appendChild(x);
|
|
535
|
+
close.addEventListener('click', function () {
|
|
536
|
+
dismissedIds[alertId(alert)] = 1;
|
|
537
|
+
saveDismissed();
|
|
538
|
+
renderAlerts(lastAlerts);
|
|
539
|
+
});
|
|
540
|
+
|
|
541
|
+
row.appendChild(svg);
|
|
542
|
+
row.appendChild(text);
|
|
543
|
+
row.appendChild(close);
|
|
544
|
+
box.appendChild(row);
|
|
545
|
+
});
|
|
546
|
+
|
|
547
|
+
var rest = shown.length - Math.min(shown.length, 5);
|
|
548
|
+
if (rest > 0) {
|
|
549
|
+
var more = document.createElement('div');
|
|
550
|
+
more.className = 'alert alert--info';
|
|
551
|
+
more.textContent = rest + ' more issue(s) - see the health checks below.';
|
|
552
|
+
box.appendChild(more);
|
|
553
|
+
}
|
|
554
|
+
if (hidden > 0) {
|
|
555
|
+
var link = document.createElement('div');
|
|
556
|
+
link.className = 'alerts__restore';
|
|
557
|
+
link.textContent = hidden + ' message(s) hidden - show them';
|
|
558
|
+
link.addEventListener('click', function () {
|
|
559
|
+
dismissedIds = {};
|
|
560
|
+
saveDismissed();
|
|
561
|
+
renderAlerts(lastAlerts);
|
|
562
|
+
});
|
|
563
|
+
box.appendChild(link);
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
function renderBadgeInto(id, text, cls) {
|
|
568
|
+
var host = $(id);
|
|
569
|
+
if (!host) return;
|
|
570
|
+
host.textContent = '';
|
|
571
|
+
var badge = document.createElement('span');
|
|
572
|
+
badge.className = 'v2-badge ' + (cls || '');
|
|
573
|
+
badge.textContent = text;
|
|
574
|
+
host.appendChild(badge);
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
/* -- overview --------------------------------------------------- */
|
|
578
|
+
function renderOverview(d) {
|
|
579
|
+
var n = d.node || {}, s = d.system || {}, p = d.peers || {}, c = d.consensus || {};
|
|
580
|
+
var q = c.quorum || {}, e = n.epochs || {}, proc = n.process || {}, be = d.backend || {};
|
|
581
|
+
|
|
582
|
+
var hostname = s.hostname || n.name || 'node';
|
|
583
|
+
setText('hero-hostname', hostname);
|
|
584
|
+
/* The topbar title is fixed ("Dashboard Monitoring") and the role/chain
|
|
585
|
+
badges next to the hostname were removed on request: the hostname stays
|
|
586
|
+
in the hero, the chain is shown in the System section. */
|
|
587
|
+
|
|
588
|
+
var address = n.address || 'unknown';
|
|
589
|
+
setText('hero-address', address === 'unknown' ? 'not configured' : shortAddr(address));
|
|
590
|
+
var addrBtn = $('hero-address-btn');
|
|
591
|
+
if (addrBtn) {
|
|
592
|
+
addrBtn.setAttribute('data-v2-copy', address === 'unknown' ? '' : address);
|
|
593
|
+
addrBtn.setAttribute('aria-label', 'Copy validator address');
|
|
594
|
+
addrBtn.title = address;
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
setText('hero-node-uptime', proc.uptime || '-');
|
|
598
|
+
setText('hero-updated', fmtClock(d.timestamp));
|
|
599
|
+
|
|
600
|
+
setText('kpi-head', fmtInt(n.head_epoch));
|
|
601
|
+
setText('kpi-head-sub', 'current epoch ' + fmtInt(n.current_epoch));
|
|
602
|
+
|
|
603
|
+
var lag = num(p.lag_max);
|
|
604
|
+
setText('kpi-lag', lag + ' epoch' + (lag === 1 ? '' : 's'));
|
|
605
|
+
setText('kpi-lag-sub', 'network head ' + fmtInt(p.network_head));
|
|
606
|
+
var lagNode = $('kpi-lag');
|
|
607
|
+
if (lagNode) {
|
|
608
|
+
lagNode.style.color = lag > 20 ? 'var(--v2-status-error)'
|
|
609
|
+
: (lag > 2 ? 'var(--v2-status-warning)' : '');
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
setText('kpi-quorum', fmtFixed(q.prevote_pct, 0) + '%');
|
|
613
|
+
setText('kpi-quorum-sub',
|
|
614
|
+
num(q.prevotes) + ' / ' + num(q.quorum) + ' voters | ' + fmtFixed(q.prevote_weight_pct, 0) + '% weight');
|
|
615
|
+
|
|
616
|
+
setText('kpi-peers', fmtInt(p.consensus_peers));
|
|
617
|
+
setText('kpi-peers-sub', num(p.connected) + ' connected | ' + num(p.known) + ' known');
|
|
618
|
+
|
|
619
|
+
setText('kpi-rate', fmtFixed(e.rate_per_min, 1) + '/min');
|
|
620
|
+
setText('kpi-rate-sub', isNum(e.interval_sec) ? (fmtFixed(e.interval_sec, 1) + ' s per epoch') : 'measuring...');
|
|
621
|
+
|
|
622
|
+
setText('kpi-avail', fmtFixed(be.availability_pct, 1) + '%');
|
|
623
|
+
setText('kpi-avail-sub', fmtInt(be.samples) + ' samples | up ' + (be.uptime || '-'));
|
|
624
|
+
|
|
625
|
+
/* consensus round panel */
|
|
626
|
+
setText('con-step-badge', c.step || '-');
|
|
627
|
+
clearBadge($('con-step-badge'), c.agreed ? 'v2-badge--ok' : '');
|
|
628
|
+
setText('con-epoch', fmtInt(c.epoch));
|
|
629
|
+
setText('con-round', isNum(c.round) && c.round >= 0 ? c.round : '-');
|
|
630
|
+
setText('con-step', c.step || '-');
|
|
631
|
+
renderBadgeInto('con-agreed', c.agreed ? 'agreed' : 'pending', c.agreed ? 'v2-badge--ok' : 'v2-badge--warn');
|
|
632
|
+
var parts = c.participants || {};
|
|
633
|
+
setText('con-participants', num(parts.total) + ' validators');
|
|
634
|
+
renderBadgeInto('con-voting',
|
|
635
|
+
c.voting === 'enabled' ? 'voting' : (c.voting === 'disabled' ? 'observer' : 'unknown'),
|
|
636
|
+
c.voting === 'enabled' ? 'v2-badge--ok' : 'v2-badge--warn');
|
|
637
|
+
|
|
638
|
+
setText('prevote-count', fmtInt(q.prevotes));
|
|
639
|
+
setText('prevote-quorum', '/ ' + fmtInt(q.quorum));
|
|
640
|
+
setText('prevote-sub',
|
|
641
|
+
fmtFixed(q.prevote_weight_pct, 1) + '% of quorum weight | ' + num(q.prevote_proposals) + ' proposal(s)');
|
|
642
|
+
setBar('prevote-fill', 'gauge-prevote', q.prevote_pct);
|
|
643
|
+
|
|
644
|
+
setText('precommit-count', fmtInt(q.precommits));
|
|
645
|
+
setText('precommit-quorum', '/ ' + fmtInt(q.quorum));
|
|
646
|
+
setText('precommit-sub', fmtFixed(q.precommit_weight_pct, 1) + '% of quorum weight');
|
|
647
|
+
setBar('precommit-fill', 'gauge-precommit', q.precommit_pct);
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
/* -- consensus section ------------------------------------------ */
|
|
651
|
+
function renderConsensus(d) {
|
|
652
|
+
var n = d.node || {}, c = d.consensus || {}, q = c.quorum || {}, e = n.epochs || {};
|
|
653
|
+
var parts = c.participants || {};
|
|
654
|
+
|
|
655
|
+
setText('c-step', c.step || '-');
|
|
656
|
+
setText('c-round', isNum(c.round) && c.round >= 0 ? c.round : '-');
|
|
657
|
+
setText('c-participants', fmtInt(parts.total));
|
|
658
|
+
setText('c-participants-sub',
|
|
659
|
+
num(parts.propose) + ' propose | ' + num(parts.prevote) + ' prevote | ' + num(parts.precommit) + ' precommit');
|
|
660
|
+
setText('c-vote-age', isNum(parts.newest_age_sec) ? fmtAge(parts.newest_age_sec) : '-');
|
|
661
|
+
setText('c-interval', isNum(e.interval_sec) ? (fmtFixed(e.interval_sec, 1) + ' s') : '-');
|
|
662
|
+
setText('c-last-advance', fmtAge(e.last_change_age_sec));
|
|
663
|
+
setText('c-last-advance-sub', e.stalled ? 'stalled - investigate' : 'head epoch advanced');
|
|
664
|
+
|
|
665
|
+
setText('c-quorum-weight', fmtCompact(q.quorum_weight) + ' weight');
|
|
666
|
+
setText('c-quorum-weight-kv', fmtCompact(q.quorum_weight));
|
|
667
|
+
setText('c-proposals', fmtInt(q.prevote_proposals));
|
|
668
|
+
renderBadgeInto('c-agreed', c.agreed ? 'agreed' : 'pending', c.agreed ? 'v2-badge--ok' : 'v2-badge--warn');
|
|
669
|
+
|
|
670
|
+
setText('prevote-w-pct', fmtFixed(q.prevote_weight_pct, 1) + '%');
|
|
671
|
+
setText('prevote-w-raw', fmtCompact(q.prevote_weight) + ' of ' + fmtCompact(q.quorum_weight) + ' quorum weight');
|
|
672
|
+
setBar('prevote-w-fill', 'gauge-prevote-w', q.prevote_weight_pct);
|
|
673
|
+
|
|
674
|
+
setText('precommit-w-pct', fmtFixed(q.precommit_weight_pct, 1) + '%');
|
|
675
|
+
setText('precommit-w-raw', fmtCompact(q.precommit_weight) + ' of ' + fmtCompact(q.quorum_weight) + ' quorum weight');
|
|
676
|
+
setBar('precommit-w-fill', 'gauge-precommit-w', q.precommit_weight_pct);
|
|
677
|
+
|
|
678
|
+
setText('c-head', fmtInt(n.head_epoch));
|
|
679
|
+
setText('c-current', fmtInt(n.current_epoch));
|
|
680
|
+
setText('c-pending', fmtInt(n.pending_epochs));
|
|
681
|
+
setText('c-rate', fmtFixed(e.rate_per_min, 2) + ' / min');
|
|
682
|
+
|
|
683
|
+
/* epoch progression sparkline (series kept by the backend) */
|
|
684
|
+
var history = e.history || [];
|
|
685
|
+
var heads = [];
|
|
686
|
+
for (var i = 0; i < history.length; i++) {
|
|
687
|
+
if (Array.isArray(history[i]) && isNum(history[i][1])) heads.push(history[i][1]);
|
|
688
|
+
}
|
|
689
|
+
var spark = $('epoch-spark');
|
|
690
|
+
if (spark && heads.length >= 2) {
|
|
691
|
+
SynteqV2.renderSparkline(spark, heads);
|
|
692
|
+
}
|
|
693
|
+
setText('epoch-spark-meta', heads.length >= 2
|
|
694
|
+
? (heads.length + ' samples | ' + fmtFixed(e.rate_per_min, 1) + ' ep/min')
|
|
695
|
+
: 'collecting samples...');
|
|
696
|
+
|
|
697
|
+
/* prevote proposal table */
|
|
698
|
+
var body = $('votes-body');
|
|
699
|
+
if (!body) return;
|
|
700
|
+
var proposals = c.proposals || [];
|
|
701
|
+
setText('votes-meta', proposals.length + ' proposal(s) | ' + num(q.prevotes) + ' voters');
|
|
702
|
+
body.textContent = '';
|
|
703
|
+
|
|
704
|
+
if (!proposals.length) {
|
|
705
|
+
var empty = document.createElement('tr');
|
|
706
|
+
var cell = document.createElement('td');
|
|
707
|
+
cell.colSpan = 3;
|
|
708
|
+
cell.className = 'v2-table__mute';
|
|
709
|
+
cell.textContent = c.available ? 'No prevotes recorded in this round yet.' : 'Consensus RPC unavailable.';
|
|
710
|
+
empty.appendChild(cell);
|
|
711
|
+
body.appendChild(empty);
|
|
712
|
+
return;
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
proposals.forEach(function (proposal) {
|
|
716
|
+
var tr = document.createElement('tr');
|
|
717
|
+
|
|
718
|
+
var tdId = document.createElement('td');
|
|
719
|
+
tdId.className = 'v2-table__mono';
|
|
720
|
+
tdId.textContent = fmtShortHash(proposal.proposal_id, 14, 8);
|
|
721
|
+
tdId.title = proposal.proposal_id;
|
|
722
|
+
|
|
723
|
+
var tdVoters = document.createElement('td');
|
|
724
|
+
tdVoters.className = 'v2-table__num';
|
|
725
|
+
tdVoters.textContent = fmtInt(proposal.voters);
|
|
726
|
+
|
|
727
|
+
var tdWeight = document.createElement('td');
|
|
728
|
+
tdWeight.className = 'v2-table__num';
|
|
729
|
+
tdWeight.textContent = fmtCompact(proposal.weight);
|
|
730
|
+
|
|
731
|
+
tr.appendChild(tdId);
|
|
732
|
+
tr.appendChild(tdVoters);
|
|
733
|
+
tr.appendChild(tdWeight);
|
|
734
|
+
body.appendChild(tr);
|
|
735
|
+
});
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
/* -- resources -------------------------------------------------- */
|
|
739
|
+
function renderResources(d) {
|
|
740
|
+
var s = d.system || {}, n = d.node || {};
|
|
741
|
+
var load = s.load_avg || [0, 0, 0];
|
|
742
|
+
|
|
743
|
+
setText('res-meta', 'live | ' + fmtClock(d.timestamp));
|
|
744
|
+
setText('res-cores-meta', num(s.cpu_cores) + ' vCPU | ' + (s.arch || '-'));
|
|
745
|
+
|
|
746
|
+
setText('res-cpu-pct', fmtFixed(s.cpu_percent, 1) + '%');
|
|
747
|
+
setText('res-cpu-sub', 'load 1m ' + fmtFixed(load[0], 2) + ' on ' + num(s.cpu_cores) + ' cores');
|
|
748
|
+
setResourceBar('res-cpu-fill', 'gauge-cpu', s.cpu_percent);
|
|
749
|
+
|
|
750
|
+
setText('res-mem-pct', fmtFixed(s.mem_percent, 1) + '%');
|
|
751
|
+
setText('res-mem-sub',
|
|
752
|
+
fmtFixed(s.mem_used_gb, 1) + ' / ' + fmtFixed(s.mem_total_gb, 1) + ' GB | ' +
|
|
753
|
+
fmtFixed(s.mem_avail_gb, 1) + ' GB available');
|
|
754
|
+
setResourceBar('res-mem-fill', 'gauge-mem', s.mem_percent);
|
|
755
|
+
|
|
756
|
+
setText('res-swap-pct', fmtFixed(s.swap_percent, 1) + '%');
|
|
757
|
+
setText('res-swap-sub',
|
|
758
|
+
fmtFixed(s.swap_used_gb, 1) + ' / ' + fmtFixed(s.swap_total_gb, 1) + ' GB');
|
|
759
|
+
setResourceBar('res-swap-fill', 'gauge-swap', s.swap_percent);
|
|
760
|
+
|
|
761
|
+
setText('res-disk-pct', fmtFixed(s.disk_percent, 1) + '%');
|
|
762
|
+
setText('res-disk-sub',
|
|
763
|
+
fmtFixed(s.disk_used_gb, 1) + ' / ' + fmtFixed(s.disk_total_gb, 1) + ' GB | ' +
|
|
764
|
+
fmtFixed(s.disk_free_gb, 1) + ' GB free');
|
|
765
|
+
setResourceBar('res-disk-fill', 'gauge-disk', s.disk_percent);
|
|
766
|
+
|
|
767
|
+
setText('res-root-pct', fmtFixed(s.disk_root_percent, 1) + '%');
|
|
768
|
+
setText('res-root-sub',
|
|
769
|
+
fmtFixed(s.disk_root_used_gb, 1) + ' / ' + fmtFixed(s.disk_root_total_gb, 1) + ' GB');
|
|
770
|
+
setResourceBar('res-root-fill', 'gauge-root', s.disk_root_percent);
|
|
771
|
+
|
|
772
|
+
setText('res-load',
|
|
773
|
+
fmtFixed(load[0], 2) + ' / ' + fmtFixed(load[1], 2) + ' / ' + fmtFixed(load[2], 2));
|
|
774
|
+
setText('res-load-pct', fmtFixed(s.load_percent, 1) + '% of ' + num(s.cpu_cores) + ' cores');
|
|
775
|
+
setText('res-uptime', s.uptime || '-');
|
|
776
|
+
setText('res-disk-read', fmtFixed(s.disk_read_mbps, 2) + ' MB/s');
|
|
777
|
+
setText('res-disk-write', fmtFixed(s.disk_write_mbps, 2) + ' MB/s');
|
|
778
|
+
setText('res-data-dir', n.data_dir || '-');
|
|
779
|
+
setText('res-storage-meta', fmtFixed(s.disk_total_gb, 0) + ' GB volume');
|
|
780
|
+
|
|
781
|
+
var history = s.cpu_history || [];
|
|
782
|
+
var spark = $('cpu-spark');
|
|
783
|
+
if (spark && history.length >= 2) {
|
|
784
|
+
SynteqV2.renderSparkline(spark, history);
|
|
785
|
+
}
|
|
786
|
+
var sum = 0;
|
|
787
|
+
for (var i = 0; i < history.length; i++) sum += num(history[i]);
|
|
788
|
+
var avg = history.length ? (sum / history.length) : 0;
|
|
789
|
+
setText('cpu-spark-meta',
|
|
790
|
+
history.length >= 2
|
|
791
|
+
? ('now ' + fmtFixed(s.cpu_percent, 1) + '% | avg ' + fmtFixed(avg, 1) + '% | ' + history.length + ' samples')
|
|
792
|
+
: 'collecting samples...');
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
/* -- peers ------------------------------------------------------ */
|
|
796
|
+
/* Column budget for the Network I/O chart. The backend keeps a 240-sample
|
|
797
|
+
ring buffer (20 min @ 5 s); drawing one grid track per sample needs
|
|
798
|
+
239 x 4px of gap alone - wider than the panel - so every 1fr track
|
|
799
|
+
collapsed to 0 px and the chart looked empty after the buffer filled.
|
|
800
|
+
Peak per bucket keeps the spikes, 64 columns always fit. */
|
|
801
|
+
var NET_COLS = 64;
|
|
802
|
+
|
|
803
|
+
function netBuckets(history, maxCols) {
|
|
804
|
+
var n = history.length;
|
|
805
|
+
if (!n) return [];
|
|
806
|
+
var groups = Math.min(maxCols, n);
|
|
807
|
+
var out = [];
|
|
808
|
+
for (var i = 0; i < groups; i++) {
|
|
809
|
+
var from = Math.floor(i * n / groups);
|
|
810
|
+
var to = Math.floor((i + 1) * n / groups);
|
|
811
|
+
if (to <= from) to = from + 1;
|
|
812
|
+
var inPeak = 0, outPeak = 0;
|
|
813
|
+
for (var j = from; j < to; j++) {
|
|
814
|
+
var pair = history[j] || [0, 0];
|
|
815
|
+
inPeak = Math.max(inPeak, num(pair[0]));
|
|
816
|
+
outPeak = Math.max(outPeak, num(pair[1]));
|
|
817
|
+
}
|
|
818
|
+
out.push([inPeak, outPeak]);
|
|
819
|
+
}
|
|
820
|
+
return out;
|
|
821
|
+
}
|
|
822
|
+
|
|
823
|
+
function renderPeers(d) {
|
|
824
|
+
var p = d.peers || {}, s = d.system || {};
|
|
825
|
+
|
|
826
|
+
setText('peers-meta', 'live | ' + fmtClock(d.timestamp));
|
|
827
|
+
setText('p-connected', fmtInt(p.connected));
|
|
828
|
+
setText('p-known-sub', 'of ' + fmtInt(p.known) + ' known');
|
|
829
|
+
setText('p-consensus', fmtInt(p.consensus_peers));
|
|
830
|
+
setText('p-lag-max', fmtInt(p.lag_max));
|
|
831
|
+
setText('p-drift-median', fmtFixed(p.drift_median, 0));
|
|
832
|
+
setText('p-behind', fmtInt(p.behind_count));
|
|
833
|
+
setText('p-stale', fmtInt(p.stale_count));
|
|
834
|
+
|
|
835
|
+
setText('p-synced', fmtInt(p.synced_count));
|
|
836
|
+
setText('p-drift-max', fmtInt(p.drift_max));
|
|
837
|
+
setText('p-ahead', fmtInt(p.ahead_count));
|
|
838
|
+
setText('p-sources', fmtInt(p.public_sources));
|
|
839
|
+
renderBadgeInto('p-banned', fmtInt(p.banned_count), num(p.banned_count) > 0 ? 'v2-badge--err' : '');
|
|
840
|
+
renderBadgeInto('p-paused', fmtInt(p.paused_count), num(p.paused_count) > 0 ? 'v2-badge--warn' : '');
|
|
841
|
+
|
|
842
|
+
setText('net-in', fmtFixed(s.net_in_mbps, 2) + ' Mbps');
|
|
843
|
+
setText('net-out', fmtFixed(s.net_out_mbps, 2) + ' Mbps');
|
|
844
|
+
|
|
845
|
+
var history = s.net_history || [];
|
|
846
|
+
var peakIn = 0, peakOut = 0;
|
|
847
|
+
for (var i = 0; i < history.length; i++) {
|
|
848
|
+
if (!Array.isArray(history[i])) continue;
|
|
849
|
+
peakIn = Math.max(peakIn, num(history[i][0]));
|
|
850
|
+
peakOut = Math.max(peakOut, num(history[i][1]));
|
|
851
|
+
}
|
|
852
|
+
var peak = Math.max(peakIn, peakOut, 1);
|
|
853
|
+
var cols = netBuckets(history, NET_COLS);
|
|
854
|
+
var bars = [];
|
|
855
|
+
for (var j = 0; j < cols.length; j++) {
|
|
856
|
+
bars.push([cols[j][0] / peak * 100, cols[j][1] / peak * 100]);
|
|
857
|
+
}
|
|
858
|
+
var bucketSec = cols.length
|
|
859
|
+
? (history.length / cols.length) * (POLL_MS / 1000)
|
|
860
|
+
: POLL_MS / 1000;
|
|
861
|
+
// Pad short series so freshly started backends still draw slim columns
|
|
862
|
+
// instead of a handful of very wide bars.
|
|
863
|
+
var barsEl = $('net-bars');
|
|
864
|
+
if (barsEl && bars.length) {
|
|
865
|
+
SynteqV2.renderBars(barsEl, bars, {
|
|
866
|
+
minCols: 24,
|
|
867
|
+
stepSec: Math.max(POLL_MS / 1000, bucketSec)
|
|
868
|
+
});
|
|
869
|
+
}
|
|
870
|
+
setText('net-bars-meta',
|
|
871
|
+
history.length
|
|
872
|
+
? ('peak v ' + fmtFixed(peakIn, 2) + ' | ^ ' + fmtFixed(peakOut, 2) +
|
|
873
|
+
' Mbps over ' + history.length + ' samples in ' + cols.length + ' columns')
|
|
874
|
+
: 'collecting samples...');
|
|
875
|
+
|
|
876
|
+
var body = $('peers-body');
|
|
877
|
+
if (!body) return;
|
|
878
|
+
var rows = p.top || [];
|
|
879
|
+
body.textContent = '';
|
|
880
|
+
|
|
881
|
+
if (!rows.length) {
|
|
882
|
+
var empty = document.createElement('tr');
|
|
883
|
+
var cell = document.createElement('td');
|
|
884
|
+
cell.colSpan = 6;
|
|
885
|
+
cell.className = 'v2-table__mute';
|
|
886
|
+
cell.textContent = p.available ? 'No peer records received yet.' : 'Consensus RPC unavailable.';
|
|
887
|
+
empty.appendChild(cell);
|
|
888
|
+
body.appendChild(empty);
|
|
889
|
+
setText('peers-table-meta', '-');
|
|
890
|
+
return;
|
|
891
|
+
}
|
|
892
|
+
|
|
893
|
+
rows.forEach(function (peer) {
|
|
894
|
+
var tr = document.createElement('tr');
|
|
895
|
+
if (peer.stale) tr.className = 'is-stale';
|
|
896
|
+
|
|
897
|
+
var tdAddr = document.createElement('td');
|
|
898
|
+
tdAddr.className = 'v2-table__mono';
|
|
899
|
+
tdAddr.textContent = peer.short || shortAddr(peer.addr);
|
|
900
|
+
tdAddr.title = peer.addr;
|
|
901
|
+
|
|
902
|
+
var tdHead = document.createElement('td');
|
|
903
|
+
tdHead.className = 'v2-table__num';
|
|
904
|
+
tdHead.textContent = fmtInt(peer.head_epoch);
|
|
905
|
+
|
|
906
|
+
var tdLag = document.createElement('td');
|
|
907
|
+
tdLag.className = 'v2-table__num';
|
|
908
|
+
tdLag.textContent = fmtInt(peer.lag);
|
|
909
|
+
if (num(peer.lag) > 20) tdLag.classList.add('lag-err');
|
|
910
|
+
else if (num(peer.lag) > 2) tdLag.classList.add('lag-warn');
|
|
911
|
+
|
|
912
|
+
var tdDrift = document.createElement('td');
|
|
913
|
+
tdDrift.className = 'v2-table__num';
|
|
914
|
+
tdDrift.textContent = fmtInt(peer.drift);
|
|
915
|
+
if (num(peer.drift) > 10) tdDrift.classList.add('lag-err');
|
|
916
|
+
else if (num(peer.drift) > 3) tdDrift.classList.add('lag-warn');
|
|
917
|
+
|
|
918
|
+
var tdAge = document.createElement('td');
|
|
919
|
+
tdAge.className = 'v2-table__num';
|
|
920
|
+
tdAge.textContent = fmtAge(peer.age_sec);
|
|
921
|
+
|
|
922
|
+
var tdSrc = document.createElement('td');
|
|
923
|
+
tdSrc.className = 'v2-table__mute';
|
|
924
|
+
tdSrc.textContent = peer.source || '-';
|
|
925
|
+
|
|
926
|
+
tr.appendChild(tdAddr);
|
|
927
|
+
tr.appendChild(tdHead);
|
|
928
|
+
tr.appendChild(tdLag);
|
|
929
|
+
tr.appendChild(tdDrift);
|
|
930
|
+
tr.appendChild(tdAge);
|
|
931
|
+
tr.appendChild(tdSrc);
|
|
932
|
+
body.appendChild(tr);
|
|
933
|
+
});
|
|
934
|
+
|
|
935
|
+
setText('peers-table-meta', rows.length + ' of ' + fmtInt(p.consensus_peers) + ' records | drift = lag beyond record age');
|
|
936
|
+
}
|
|
937
|
+
|
|
938
|
+
/* -- process ---------------------------------------------------- */
|
|
939
|
+
function renderProcess(d) {
|
|
940
|
+
var n = d.node || {}, proc = n.process || {}, logs = d.logs || {};
|
|
941
|
+
var counts = logs.counts || {};
|
|
942
|
+
var online = String(proc.status || '').toLowerCase() === 'online';
|
|
943
|
+
|
|
944
|
+
setText('pr-meta', 'pm2 | ' + (proc.name || 'unknown'));
|
|
945
|
+
renderBadgeInto('pr-status', proc.status || 'unknown',
|
|
946
|
+
online ? 'v2-badge--ok' : (proc.status === 'unavailable' ? 'v2-badge--warn' : 'v2-badge--err'));
|
|
947
|
+
setText('pr-name-sub', proc.name || '-');
|
|
948
|
+
setText('pr-pid', proc.pid ? fmtInt(proc.pid) : '-');
|
|
949
|
+
setText('pr-uptime', proc.uptime || '-');
|
|
950
|
+
setText('pr-restarts', fmtInt(proc.restarts));
|
|
951
|
+
setText('pr-cpu', fmtFixed(proc.cpu, 1) + '%');
|
|
952
|
+
setText('pr-mem', fmtFixed(proc.memory_mb, 0) + ' MB');
|
|
953
|
+
|
|
954
|
+
setText('pr-name', proc.name || '-');
|
|
955
|
+
setText('pr-exec', proc.exec_mode || '-');
|
|
956
|
+
setText('pr-instances', fmtInt(proc.instances));
|
|
957
|
+
setText('pr-commit', n.source_commit || '-');
|
|
958
|
+
setText('pr-rpc', '127.0.0.1:' + (n.api_port || '-'));
|
|
959
|
+
setText('pr-runtime-meta', proc.node_version && proc.node_version !== 'none' ? proc.node_version : 'pm2 daemon');
|
|
960
|
+
|
|
961
|
+
var warns = num(counts.warn);
|
|
962
|
+
var errors = num(counts.error) + num(counts.fatal);
|
|
963
|
+
renderBadgeInto('pr-warn', fmtInt(warns) + ' lines', warns > 0 ? 'v2-badge--warn' : '');
|
|
964
|
+
|
|
965
|
+
var lastWarn = logs.last_warn || {};
|
|
966
|
+
var lastError = logs.last_error || {};
|
|
967
|
+
renderBadgeInto('pr-error', fmtInt(errors) + ' lines', errors > 0 ? 'v2-badge--err' : '');
|
|
968
|
+
|
|
969
|
+
setText('pr-last-warn', warns ? shortEvent(lastWarn) : 'none');
|
|
970
|
+
setText('pr-last-error', errors ? shortEvent(lastError) : 'none');
|
|
971
|
+
var lw = $('pr-last-warn');
|
|
972
|
+
if (lw) lw.title = warns ? (lastWarn.msg || '') : 'No warnings in the last 120 lines';
|
|
973
|
+
var le = $('pr-last-error');
|
|
974
|
+
if (le) le.title = errors ? (lastError.msg || '') : 'No errors in the last 120 lines';
|
|
975
|
+
|
|
976
|
+
setText('pr-log-meta', fmtInt(logs.total) + ' lines sampled');
|
|
977
|
+
}
|
|
978
|
+
|
|
979
|
+
function shortEvent(entry) {
|
|
980
|
+
if (!entry || !entry.msg) return '-';
|
|
981
|
+
var parts = [];
|
|
982
|
+
if (entry.component) parts.push(entry.component);
|
|
983
|
+
if (entry.event) parts.push(entry.event);
|
|
984
|
+
if (entry.reason) parts.push('reason=' + entry.reason);
|
|
985
|
+
if (parts.length) return parts.join(' | ');
|
|
986
|
+
return entry.msg.slice(0, 60);
|
|
987
|
+
}
|
|
988
|
+
|
|
989
|
+
/* -- system ----------------------------------------------------- */
|
|
990
|
+
function renderSystem(d) {
|
|
991
|
+
var n = d.node || {}, s = d.system || {}, be = d.backend || {};
|
|
992
|
+
|
|
993
|
+
setText('sys-meta', 'backend v' + (be.version || '-') + ' | up ' + (be.uptime || '-'));
|
|
994
|
+
setText('sys-chain', n.chain || '-');
|
|
995
|
+
setText('sys-role', n.role || '-');
|
|
996
|
+
setText('sys-api', n.api_port ? ('127.0.0.1:' + n.api_port) : '-');
|
|
997
|
+
setText('sys-network-version', n.network_version || '-');
|
|
998
|
+
setText('sys-storage', n.storage_backend || '-');
|
|
999
|
+
setText('sys-data-dir', n.data_dir || '-');
|
|
1000
|
+
|
|
1001
|
+
setText('sys-hostname', s.hostname || '-');
|
|
1002
|
+
setText('sys-os', s.os || '-');
|
|
1003
|
+
setText('sys-kernel', s.kernel || '-');
|
|
1004
|
+
setText('sys-arch', s.arch || '-');
|
|
1005
|
+
setText('sys-cores', fmtInt(s.cpu_cores));
|
|
1006
|
+
setText('sys-uptime', s.uptime || '-');
|
|
1007
|
+
|
|
1008
|
+
setText('sys-accounts', fmtInt(n.accounts));
|
|
1009
|
+
setText('sys-active-accounts', fmtInt(n.active_accounts));
|
|
1010
|
+
setText('sys-supply', fmtCompact(parseFloat(n.total_supply)) + ' OCT');
|
|
1011
|
+
setText('sys-circulating', fmtCompact(parseFloat(n.circulating_supply)) + ' OCT');
|
|
1012
|
+
setText('sys-encrypted', fmtCompact(parseFloat(n.encrypted_supply)) + ' OCT');
|
|
1013
|
+
setText('sys-txid', n.txid_hi || '-');
|
|
1014
|
+
|
|
1015
|
+
setText('sys-state-root', fmtShortHash(n.state_root, 16, 12));
|
|
1016
|
+
setText('sys-irmin', fmtShortHash(n.irmin_commit, 16, 12));
|
|
1017
|
+
setText('sys-commit', n.source_commit || '-');
|
|
1018
|
+
setText('sys-backend', 'octra-dashboard | python3 | 127.0.0.1:9090');
|
|
1019
|
+
setText('sys-backend-avail',
|
|
1020
|
+
fmtFixed(be.availability_pct, 1) + '% of ' + fmtInt(be.samples) + ' samples');
|
|
1021
|
+
|
|
1022
|
+
var rootBtn = $('copy-state-root');
|
|
1023
|
+
if (rootBtn) {
|
|
1024
|
+
rootBtn.setAttribute('data-v2-copy', n.state_root || '');
|
|
1025
|
+
rootBtn.title = n.state_root || '';
|
|
1026
|
+
}
|
|
1027
|
+
var irminBtn = $('copy-irmin');
|
|
1028
|
+
if (irminBtn) {
|
|
1029
|
+
irminBtn.setAttribute('data-v2-copy', n.irmin_commit || '');
|
|
1030
|
+
irminBtn.title = n.irmin_commit || '';
|
|
1031
|
+
}
|
|
1032
|
+
|
|
1033
|
+
setText('sys-foot',
|
|
1034
|
+
'Octra live monitor - read-only telemetry from the node RPC, /proc and pm2 | ' +
|
|
1035
|
+
'poll ' + (POLL_MS / 1000) + ' s | snapshot ' + fmtClock(d.timestamp) + ' UTC+0');
|
|
1036
|
+
}
|
|
1037
|
+
|
|
1038
|
+
/* -- wallets ---------------------------------------------------- */
|
|
1039
|
+
function renderWallets(d) {
|
|
1040
|
+
var w = d.wallet || {};
|
|
1041
|
+
var r = w.rewards || {};
|
|
1042
|
+
var t = w.tracking || {};
|
|
1043
|
+
var b = w.baseline || {};
|
|
1044
|
+
var m = w.membership || {};
|
|
1045
|
+
var duty = w.duty || {};
|
|
1046
|
+
var deltaRaw = parseFloat(b.delta_raw || '0');
|
|
1047
|
+
if (!isNum(deltaRaw)) deltaRaw = 0;
|
|
1048
|
+
|
|
1049
|
+
setText('wal-meta', w.available ? ('wallet | ' + fmtClock(d.timestamp)) : (w.reason || 'unavailable'));
|
|
1050
|
+
|
|
1051
|
+
/* KPI strip */
|
|
1052
|
+
setText('wal-balance', fmtToken(w.balance));
|
|
1053
|
+
setText('wal-balance-sub', 'raw ' + (w.balance_raw || '0') + ' | 6 dp');
|
|
1054
|
+
setText('wal-bond', fmtToken(w.bond));
|
|
1055
|
+
setText('wal-bond-sub', w.bonded_epoch
|
|
1056
|
+
? ('since epoch ' + fmtInt(w.bonded_epoch) + ' | ' + fmtInt(w.epochs_in_set) + ' epochs')
|
|
1057
|
+
: 'no bond recorded');
|
|
1058
|
+
setText('wal-nonce', fmtInt(w.nonce));
|
|
1059
|
+
setText('wal-nonce-sub', w.tx_count_known
|
|
1060
|
+
? (fmtInt(w.tx_count) + ' tx | pending ' + fmtInt(w.pending_nonce))
|
|
1061
|
+
: ('pending ' + fmtInt(w.pending_nonce) + ' | tx count sampling...'));
|
|
1062
|
+
setText('wal-proposals', fmtInt(r.proposer_epochs));
|
|
1063
|
+
setText('wal-proposals-sub', num(t.span_epochs)
|
|
1064
|
+
? (fmtFixed(r.share_pct, 1) + '% of ' + fmtInt(t.span_epochs) + ' reward epochs')
|
|
1065
|
+
: 'waiting for the log index');
|
|
1066
|
+
setText('wal-reward', fmtToken(r.total));
|
|
1067
|
+
setText('wal-reward-sub', num(r.epochs)
|
|
1068
|
+
? (fmtInt(r.epochs) + ' epochs | from ' + fmtInt(t.first_epoch))
|
|
1069
|
+
: 'no rewards indexed yet');
|
|
1070
|
+
/* A balance-delta reward total is an estimate: the operator has to see it. */
|
|
1071
|
+
setText('wal-reward-tag', r.est ? '(EST)' : '');
|
|
1072
|
+
setText('wal-reward-rate', fmtToken(r.est_per_day));
|
|
1073
|
+
setText('wal-reward-rate-sub', num(r.avg_per_epoch)
|
|
1074
|
+
? ('avg ' + fmtToken(r.avg_per_epoch) + ' / epoch' +
|
|
1075
|
+
(num(r.epochs_per_day) ? ' | ' + fmtInt(Math.round(r.epochs_per_day)) + ' epochs/day' : ''))
|
|
1076
|
+
: 'measuring epoch interval...');
|
|
1077
|
+
|
|
1078
|
+
/* Account panel */
|
|
1079
|
+
setText('wal-address', w.address ? shortAddr(w.address) : 'not configured');
|
|
1080
|
+
var copyBtn = $('wal-copy-address');
|
|
1081
|
+
if (copyBtn) {
|
|
1082
|
+
copyBtn.setAttribute('data-v2-copy', w.address || '');
|
|
1083
|
+
copyBtn.title = w.address || '';
|
|
1084
|
+
}
|
|
1085
|
+
clearBadge($('wal-state'), w.state === 'ready' ? 'v2-badge--ok' : 'v2-badge--warn');
|
|
1086
|
+
setText('wal-state', w.state || 'unknown');
|
|
1087
|
+
setText('wal-balance-raw', w.balance_raw || '-');
|
|
1088
|
+
setText('wal-holdings', fmtToken(w.holdings));
|
|
1089
|
+
setText('wal-pending', fmtInt(w.pending_nonce));
|
|
1090
|
+
setText('wal-tx-count', w.tx_count_known ? fmtInt(w.tx_count) : 'sampling...');
|
|
1091
|
+
renderBadgeInto('wal-pubkey', w.has_public_key ? 'registered' : 'missing',
|
|
1092
|
+
w.has_public_key ? 'v2-badge--ok' : 'v2-badge--warn');
|
|
1093
|
+
renderBadgeInto('wal-encrypted',
|
|
1094
|
+
!w.detail_known ? 'sampling...' : (w.encrypted_balance ? 'enabled' : 'not set'),
|
|
1095
|
+
!w.detail_known ? '' : (w.encrypted_balance ? 'v2-badge--brand' : ''));
|
|
1096
|
+
|
|
1097
|
+
/* Stake & duty panel */
|
|
1098
|
+
setText('wal-duty', num(duty.required_span)
|
|
1099
|
+
? ('span ' + fmtInt(duty.required_span) + ' | gap ' + fmtInt(duty.max_gap))
|
|
1100
|
+
: '-');
|
|
1101
|
+
setText('wal-bond-raw', w.bond_raw || '0');
|
|
1102
|
+
setText('wal-bonded-epoch', w.bonded_epoch ? fmtInt(w.bonded_epoch) : '-');
|
|
1103
|
+
setText('wal-ready-epoch', w.ready_epoch ? fmtInt(w.ready_epoch) : '-');
|
|
1104
|
+
setText('wal-epochs-in-set', fmtInt(w.epochs_in_set));
|
|
1105
|
+
renderBadgeInto('wal-membership',
|
|
1106
|
+
m.active ? 'active' : (m.scheduled ? 'scheduled' : 'inactive'),
|
|
1107
|
+
m.active ? 'v2-badge--ok' : (m.scheduled ? 'v2-badge--warn' : ''));
|
|
1108
|
+
setText('wal-next-set',
|
|
1109
|
+
(m.activate_epoch ? fmtInt(m.activate_epoch) : '-') + ' / ' +
|
|
1110
|
+
(m.next_set_epoch ? fmtInt(m.next_set_epoch) : '-'));
|
|
1111
|
+
setText('wal-exit', w.exit_epoch ? fmtInt(w.exit_epoch) : 'none');
|
|
1112
|
+
|
|
1113
|
+
/* Reward attribution panel */
|
|
1114
|
+
setText('wal-reward-meta', num(r.epochs) ? (fmtInt(r.epochs) + ' epochs attributed') : 'no data');
|
|
1115
|
+
setText('wal-reward-proposer', fmtToken(r.proposer));
|
|
1116
|
+
setText('wal-reward-validator', fmtToken(r.validator));
|
|
1117
|
+
setText('wal-avg', fmtToken(r.avg_per_epoch));
|
|
1118
|
+
setText('wal-share', fmtFixed(r.share_pct, 1) + '%');
|
|
1119
|
+
setText('wal-last-reward', r.last_epoch
|
|
1120
|
+
? (fmtToken(r.last_amount) + ' | epoch ' + fmtInt(r.last_epoch))
|
|
1121
|
+
: '-');
|
|
1122
|
+
renderBadgeInto('wal-last-role', r.last_role || '-',
|
|
1123
|
+
r.last_role === 'proposer' ? 'v2-badge--brand' : '');
|
|
1124
|
+
var lastNode = $('wal-last-reward');
|
|
1125
|
+
if (lastNode) {
|
|
1126
|
+
lastNode.title = isNum(r.epochs_since)
|
|
1127
|
+
? (fmtInt(r.epochs_since) + ' epoch(s) since the last attributed reward')
|
|
1128
|
+
: '';
|
|
1129
|
+
}
|
|
1130
|
+
|
|
1131
|
+
/* Baseline & index panel */
|
|
1132
|
+
setText('wal-baseline-age', isNum(b.age_sec) && b.age_sec >= 0
|
|
1133
|
+
? ('captured ' + fmtAge(b.age_sec) + ' ago')
|
|
1134
|
+
: 'no baseline');
|
|
1135
|
+
setText('wal-baseline', fmtToken(b.balance));
|
|
1136
|
+
setText('wal-baseline-epoch', b.epoch ? fmtInt(b.epoch) : '-');
|
|
1137
|
+
setText('wal-delta', (deltaRaw >= 0 ? '+' : '') + fmtToken(b.delta));
|
|
1138
|
+
var deltaNode = $('wal-delta');
|
|
1139
|
+
if (deltaNode) {
|
|
1140
|
+
deltaNode.style.color = deltaRaw < 0 ? 'var(--v2-status-warning)' : '';
|
|
1141
|
+
deltaNode.title = 'Balance movement since the setup snapshot - rewards, transfers and fees combined';
|
|
1142
|
+
}
|
|
1143
|
+
setText('wal-index', fmtFixed(t.progress_pct, 1) + '% | ' +
|
|
1144
|
+
fmtBytes(t.indexed_bytes) + ' / ' + fmtBytes(t.log_size));
|
|
1145
|
+
setText('wal-epoch-span', num(t.span_epochs)
|
|
1146
|
+
? (fmtInt(t.first_epoch) + ' -> ' + fmtInt(t.last_epoch) + ' (' + fmtInt(t.span_epochs) + ')')
|
|
1147
|
+
: '-');
|
|
1148
|
+
setText('wal-since', t.since ? fmtClock(t.since) : '-');
|
|
1149
|
+
setText('wal-log', t.log_path || '-');
|
|
1150
|
+
var logNode = $('wal-log');
|
|
1151
|
+
if (logNode) logNode.title = t.log_path || '';
|
|
1152
|
+
/* The log fold is the gross figure the EST total is measured against; it is
|
|
1153
|
+
kept visible so a suspicious estimate can be checked by hand. */
|
|
1154
|
+
setText('wal-log-total', isNum(r.log_total) ? fmtToken(r.log_total) : '-');
|
|
1155
|
+
setText('wal-log-ratio', isNum(r.log_ratio_pct)
|
|
1156
|
+
? (fmtFixed(r.log_ratio_pct, 1) + '% of the log fold')
|
|
1157
|
+
: '-');
|
|
1158
|
+
|
|
1159
|
+
/* Recent epoch rewards */
|
|
1160
|
+
var body = $('wal-rewards-body');
|
|
1161
|
+
if (!body) return;
|
|
1162
|
+
var rows = r.recent || [];
|
|
1163
|
+
setText('wal-rewards-meta', rows.length
|
|
1164
|
+
? (rows.length + ' most recent | ' + fmtInt(r.epochs) + ' tracked')
|
|
1165
|
+
: '-');
|
|
1166
|
+
body.textContent = '';
|
|
1167
|
+
|
|
1168
|
+
if (!rows.length) {
|
|
1169
|
+
var empty = document.createElement('tr');
|
|
1170
|
+
var cell = document.createElement('td');
|
|
1171
|
+
cell.colSpan = 4;
|
|
1172
|
+
cell.className = 'v2-table__mute';
|
|
1173
|
+
cell.textContent = w.available
|
|
1174
|
+
? 'No epoch rewards indexed yet - the index fills while the node keeps producing epochs.'
|
|
1175
|
+
: (w.reason || 'Wallet telemetry unavailable.');
|
|
1176
|
+
empty.appendChild(cell);
|
|
1177
|
+
body.appendChild(empty);
|
|
1178
|
+
} else {
|
|
1179
|
+
rows.forEach(function (row) {
|
|
1180
|
+
var tr = document.createElement('tr');
|
|
1181
|
+
|
|
1182
|
+
var tdEpoch = document.createElement('td');
|
|
1183
|
+
tdEpoch.className = 'v2-table__num';
|
|
1184
|
+
tdEpoch.textContent = fmtInt(row.epoch);
|
|
1185
|
+
|
|
1186
|
+
var tdRole = document.createElement('td');
|
|
1187
|
+
tdRole.className = 'v2-table__mute';
|
|
1188
|
+
tdRole.textContent = row.role || '-';
|
|
1189
|
+
|
|
1190
|
+
var tdAmount = document.createElement('td');
|
|
1191
|
+
tdAmount.className = 'v2-table__num';
|
|
1192
|
+
tdAmount.textContent = fmtToken(row.amount);
|
|
1193
|
+
if (row.role === 'proposer') tdAmount.style.color = 'var(--v2-brand)';
|
|
1194
|
+
|
|
1195
|
+
var tdTime = document.createElement('td');
|
|
1196
|
+
tdTime.className = 'v2-table__num';
|
|
1197
|
+
tdTime.textContent = row.at ? fmtClock(row.at) : '-';
|
|
1198
|
+
if (row.at) tdTime.title = row.at;
|
|
1199
|
+
|
|
1200
|
+
tr.appendChild(tdEpoch);
|
|
1201
|
+
tr.appendChild(tdRole);
|
|
1202
|
+
tr.appendChild(tdAmount);
|
|
1203
|
+
tr.appendChild(tdTime);
|
|
1204
|
+
body.appendChild(tr);
|
|
1205
|
+
});
|
|
1206
|
+
}
|
|
1207
|
+
|
|
1208
|
+
setText('wal-foot',
|
|
1209
|
+
'Wallet & rewards - rewards are folded per epoch from the node operator log ' +
|
|
1210
|
+
'(proposer_total + each when this wallet proposed, each otherwise); the balance ' +
|
|
1211
|
+
'baseline captured at deploy is the cross-check. Poll ' + (POLL_MS / 1000) +
|
|
1212
|
+
' s | snapshot ' + fmtClock(d.timestamp) + ' UTC+0');
|
|
1213
|
+
}
|
|
1214
|
+
|
|
1215
|
+
/* -- logs ------------------------------------------------------- */
|
|
1216
|
+
function fmtLogTime(ts) {
|
|
1217
|
+
if (!ts) return '--:--:--';
|
|
1218
|
+
var m = /T(\d{2}:\d{2}:\d{2})/.exec(String(ts));
|
|
1219
|
+
return m ? m[1] : String(ts).slice(0, 8);
|
|
1220
|
+
}
|
|
1221
|
+
|
|
1222
|
+
function levelKey(entry) {
|
|
1223
|
+
var lvl = entry.level || 'info';
|
|
1224
|
+
if (lvl === 'fatal') return 'error';
|
|
1225
|
+
if (lvl === 'trace' || lvl === 'debug') return 'info';
|
|
1226
|
+
return lvl;
|
|
1227
|
+
}
|
|
1228
|
+
|
|
1229
|
+
function logMatches(entry, level, query) {
|
|
1230
|
+
if (level !== 'all' && levelKey(entry) !== level) return false;
|
|
1231
|
+
if (!query) return true;
|
|
1232
|
+
var hay = (entry.msg || '') + ' ' + (entry.component || '') + ' ' + (entry.event || '');
|
|
1233
|
+
return hay.toLowerCase().indexOf(query) !== -1;
|
|
1234
|
+
}
|
|
1235
|
+
|
|
1236
|
+
function logRow(entry) {
|
|
1237
|
+
var row = document.createElement('div');
|
|
1238
|
+
row.className = 'v2-log__row' +
|
|
1239
|
+
(entry.level === 'warn' ? ' v2-log__row--warn' : '') +
|
|
1240
|
+
((entry.level === 'error' || entry.level === 'fatal') ? ' v2-log__row--error' : '');
|
|
1241
|
+
|
|
1242
|
+
var ts = document.createElement('span');
|
|
1243
|
+
ts.className = 'v2-log__ts';
|
|
1244
|
+
ts.textContent = fmtLogTime(entry.ts);
|
|
1245
|
+
|
|
1246
|
+
var level = document.createElement('span');
|
|
1247
|
+
level.className = 'v2-log__level v2-log__level--' + levelKey(entry);
|
|
1248
|
+
level.textContent = (entry.level || 'info').toUpperCase();
|
|
1249
|
+
|
|
1250
|
+
var comp = document.createElement('span');
|
|
1251
|
+
comp.className = 'v2-log__comp';
|
|
1252
|
+
comp.textContent = entry.component || '|';
|
|
1253
|
+
|
|
1254
|
+
var msg = document.createElement('span');
|
|
1255
|
+
msg.className = 'v2-log__msg';
|
|
1256
|
+
msg.textContent = entry.detail || entry.msg || '';
|
|
1257
|
+
|
|
1258
|
+
row.appendChild(ts);
|
|
1259
|
+
row.appendChild(level);
|
|
1260
|
+
row.appendChild(comp);
|
|
1261
|
+
row.appendChild(msg);
|
|
1262
|
+
return row;
|
|
1263
|
+
}
|
|
1264
|
+
|
|
1265
|
+
function renderLogs() {
|
|
1266
|
+
var body = $('log-body');
|
|
1267
|
+
if (!body) return;
|
|
1268
|
+
|
|
1269
|
+
var visible = [];
|
|
1270
|
+
for (var i = 0; i < state.logs.length; i++) {
|
|
1271
|
+
if (logMatches(state.logs[i], state.logLevel, state.logQuery)) visible.push(state.logs[i]);
|
|
1272
|
+
}
|
|
1273
|
+
|
|
1274
|
+
var nearBottom = (body.scrollHeight - body.scrollTop - body.clientHeight) < 48;
|
|
1275
|
+
body.textContent = '';
|
|
1276
|
+
|
|
1277
|
+
if (!visible.length) {
|
|
1278
|
+
var empty = document.createElement('div');
|
|
1279
|
+
empty.className = 'v2-log__empty';
|
|
1280
|
+
empty.textContent = state.logs.length ? 'No lines match the current filter.' : 'Waiting for log stream...';
|
|
1281
|
+
body.appendChild(empty);
|
|
1282
|
+
} else {
|
|
1283
|
+
var frag = document.createDocumentFragment();
|
|
1284
|
+
for (var j = 0; j < visible.length; j++) frag.appendChild(logRow(visible[j]));
|
|
1285
|
+
body.appendChild(frag);
|
|
1286
|
+
}
|
|
1287
|
+
|
|
1288
|
+
if (nearBottom) body.scrollTop = body.scrollHeight;
|
|
1289
|
+
|
|
1290
|
+
var counts = { warn: 0, error: 0 };
|
|
1291
|
+
for (var k = 0; k < state.logs.length; k++) {
|
|
1292
|
+
var key = levelKey(state.logs[k]);
|
|
1293
|
+
if (key === 'warn') counts.warn++;
|
|
1294
|
+
else if (key === 'error') counts.error++;
|
|
1295
|
+
}
|
|
1296
|
+
|
|
1297
|
+
setText('log-meta', visible.length + ' / ' + state.logs.length + ' lines');
|
|
1298
|
+
setText('log-section-meta',
|
|
1299
|
+
state.logs.length
|
|
1300
|
+
? ('tail ' + state.logs.length + ' | ' + counts.warn + ' warn | ' + counts.error + ' err')
|
|
1301
|
+
: '-');
|
|
1302
|
+
|
|
1303
|
+
var badge = $('nav-log-badge');
|
|
1304
|
+
if (badge) {
|
|
1305
|
+
var issues = counts.warn + counts.error;
|
|
1306
|
+
badge.textContent = issues > 99 ? '99+' : String(issues);
|
|
1307
|
+
badge.hidden = issues === 0;
|
|
1308
|
+
}
|
|
1309
|
+
}
|
|
1310
|
+
|
|
1311
|
+
/* -- data fetching ---------------------------------------------- */
|
|
1312
|
+
function fetchJSON(path, timeoutMs) {
|
|
1313
|
+
var controller = typeof AbortController !== 'undefined' ? new AbortController() : null;
|
|
1314
|
+
var timer = null;
|
|
1315
|
+
var options = { cache: 'no-store', headers: { Accept: 'application/json' } };
|
|
1316
|
+
if (controller) {
|
|
1317
|
+
options.signal = controller.signal;
|
|
1318
|
+
timer = setTimeout(function () { controller.abort(); }, timeoutMs || 9000);
|
|
1319
|
+
}
|
|
1320
|
+
return fetch(path, options).then(function (res) {
|
|
1321
|
+
if (timer) clearTimeout(timer);
|
|
1322
|
+
if (!res.ok) throw new Error('HTTP ' + res.status);
|
|
1323
|
+
return res.json();
|
|
1324
|
+
}).catch(function (err) {
|
|
1325
|
+
if (timer) clearTimeout(timer);
|
|
1326
|
+
throw err;
|
|
1327
|
+
});
|
|
1328
|
+
}
|
|
1329
|
+
|
|
1330
|
+
function updateLiveAge() {
|
|
1331
|
+
var node = $('live-age');
|
|
1332
|
+
if (!node) return;
|
|
1333
|
+
|
|
1334
|
+
if (!state.lastOkAt) {
|
|
1335
|
+
node.textContent = state.paused ? 'paused' : 'connecting...';
|
|
1336
|
+
node.classList.remove('is-stale');
|
|
1337
|
+
return;
|
|
1338
|
+
}
|
|
1339
|
+
|
|
1340
|
+
var ageSec = (Date.now() - state.lastOkAt) / 1000;
|
|
1341
|
+
var stale = (ageSec * 1000) > STALE_AFTER;
|
|
1342
|
+
node.textContent = state.paused
|
|
1343
|
+
? 'paused | ' + fmtAge(ageSec) + ' old'
|
|
1344
|
+
: ((stale ? 'stale | ' : 'live | ') + fmtAge(ageSec));
|
|
1345
|
+
node.classList.toggle('is-stale', stale && !state.paused);
|
|
1346
|
+
}
|
|
1347
|
+
|
|
1348
|
+
function showUnreachable() {
|
|
1349
|
+
applyStatus('unreachable');
|
|
1350
|
+
var box = $('alerts');
|
|
1351
|
+
if (!box) return;
|
|
1352
|
+
box.textContent = '';
|
|
1353
|
+
box.hidden = false;
|
|
1354
|
+
|
|
1355
|
+
var row = document.createElement('div');
|
|
1356
|
+
row.className = 'alert alert--error';
|
|
1357
|
+
var text = document.createElement('div');
|
|
1358
|
+
text.className = 'alert__text';
|
|
1359
|
+
var strong = document.createElement('strong');
|
|
1360
|
+
strong.textContent = 'Telemetry unreachable - ';
|
|
1361
|
+
text.appendChild(strong);
|
|
1362
|
+
text.appendChild(document.createTextNode(
|
|
1363
|
+
'the dashboard backend did not answer. Last good snapshot: ' +
|
|
1364
|
+
(state.lastOkAt ? fmtClock(new Date(state.lastOkAt).toISOString()) : 'none') +
|
|
1365
|
+
'. Retrying every ' + (POLL_MS / 1000) + ' s.'
|
|
1366
|
+
));
|
|
1367
|
+
row.appendChild(text);
|
|
1368
|
+
box.appendChild(row);
|
|
1369
|
+
}
|
|
1370
|
+
|
|
1371
|
+
function renderSnapshot(d) {
|
|
1372
|
+
var node = d.node || {};
|
|
1373
|
+
applyStatus(node.status || 'connecting');
|
|
1374
|
+
|
|
1375
|
+
var checks = renderHealth(d);
|
|
1376
|
+
renderAlerts(collectAlerts(d, checks));
|
|
1377
|
+
renderOverview(d);
|
|
1378
|
+
renderConsensus(d);
|
|
1379
|
+
renderResources(d);
|
|
1380
|
+
renderPeers(d);
|
|
1381
|
+
renderProcess(d);
|
|
1382
|
+
renderSystem(d);
|
|
1383
|
+
renderWallets(d);
|
|
1384
|
+
|
|
1385
|
+
applyHashSection();
|
|
1386
|
+
}
|
|
1387
|
+
|
|
1388
|
+
/** Re-apply the #section deep link once the first render settled the layout. */
|
|
1389
|
+
var hashApplied = false;
|
|
1390
|
+
function applyHashSection() {
|
|
1391
|
+
if (hashApplied) return;
|
|
1392
|
+
hashApplied = true;
|
|
1393
|
+
var id = (window.location.hash || '').replace('#', '');
|
|
1394
|
+
if (!id || !window.SynteqV2 || !SynteqV2.scrollToSection) return;
|
|
1395
|
+
requestAnimationFrame(function () {
|
|
1396
|
+
SynteqV2.scrollToSection(id, { behavior: 'auto' });
|
|
1397
|
+
});
|
|
1398
|
+
}
|
|
1399
|
+
|
|
1400
|
+
function pollStatus() {
|
|
1401
|
+
if (state.paused || state.fetching) return;
|
|
1402
|
+
state.fetching = true;
|
|
1403
|
+
|
|
1404
|
+
fetchJSON('/api/status', 9000).then(function (data) {
|
|
1405
|
+
state.fetching = false;
|
|
1406
|
+
state.failures = 0;
|
|
1407
|
+
state.lastOkAt = Date.now();
|
|
1408
|
+
state.status = data;
|
|
1409
|
+
renderSnapshot(data);
|
|
1410
|
+
updateLiveAge();
|
|
1411
|
+
}).catch(function () {
|
|
1412
|
+
state.fetching = false;
|
|
1413
|
+
state.failures++;
|
|
1414
|
+
if (state.failures >= 1) showUnreachable();
|
|
1415
|
+
updateLiveAge();
|
|
1416
|
+
});
|
|
1417
|
+
}
|
|
1418
|
+
|
|
1419
|
+
function fetchLogs() {
|
|
1420
|
+
if (state.logPaused) return;
|
|
1421
|
+
fetchJSON('/api/logs?lines=' + LOG_LINES, 9000).then(function (data) {
|
|
1422
|
+
state.logs = (data && data.logs) || [];
|
|
1423
|
+
renderLogs();
|
|
1424
|
+
}).catch(function () {
|
|
1425
|
+
/* keep the previous tail on screen */
|
|
1426
|
+
});
|
|
1427
|
+
}
|
|
1428
|
+
|
|
1429
|
+
/* -- controls --------------------------------------------------- */
|
|
1430
|
+
function swapIcons(root, showPause) {
|
|
1431
|
+
if (!root) return;
|
|
1432
|
+
var pauseIcon = root.querySelector('.icon-pause');
|
|
1433
|
+
var playIcon = root.querySelector('.icon-play');
|
|
1434
|
+
if (pauseIcon) pauseIcon.style.display = showPause ? '' : 'none';
|
|
1435
|
+
if (playIcon) playIcon.style.display = showPause ? 'none' : '';
|
|
1436
|
+
}
|
|
1437
|
+
|
|
1438
|
+
/** Transient feedback next to a control (framework tip primitive). */
|
|
1439
|
+
function showTip(target, message, kind) {
|
|
1440
|
+
if (!target || !window.SynteqV2 || !SynteqV2.tip) return;
|
|
1441
|
+
SynteqV2.tip(target, message, kind ? { kind: kind } : undefined);
|
|
1442
|
+
}
|
|
1443
|
+
|
|
1444
|
+
function initControls() {
|
|
1445
|
+
var refresh = $('btn-refresh');
|
|
1446
|
+
if (refresh) {
|
|
1447
|
+
refresh.addEventListener('click', function () {
|
|
1448
|
+
refresh.classList.add('is-spinning');
|
|
1449
|
+
setTimeout(function () { refresh.classList.remove('is-spinning'); }, 800);
|
|
1450
|
+
state.paused = false;
|
|
1451
|
+
syncPauseButton();
|
|
1452
|
+
pollStatus();
|
|
1453
|
+
fetchLogs();
|
|
1454
|
+
showTip(refresh, 'Refreshed');
|
|
1455
|
+
});
|
|
1456
|
+
}
|
|
1457
|
+
|
|
1458
|
+
var pause = $('btn-pause');
|
|
1459
|
+
if (pause) pause.addEventListener('click', function () {
|
|
1460
|
+
state.paused = !state.paused;
|
|
1461
|
+
syncPauseButton();
|
|
1462
|
+
updateLiveAge();
|
|
1463
|
+
if (!state.paused) {
|
|
1464
|
+
pollStatus();
|
|
1465
|
+
fetchLogs();
|
|
1466
|
+
}
|
|
1467
|
+
showTip(pause,
|
|
1468
|
+
state.paused ? 'Auto-refresh paused' : 'Auto-refresh resumed',
|
|
1469
|
+
state.paused ? 'warn' : 'ok');
|
|
1470
|
+
});
|
|
1471
|
+
|
|
1472
|
+
var logPause = $('log-pause');
|
|
1473
|
+
if (logPause) logPause.addEventListener('click', function () {
|
|
1474
|
+
state.logPaused = !state.logPaused;
|
|
1475
|
+
logPause.setAttribute('aria-pressed', String(state.logPaused));
|
|
1476
|
+
swapIcons(logPause, !state.logPaused);
|
|
1477
|
+
setText('log-pause-text', state.logPaused ? 'Resume' : 'Pause');
|
|
1478
|
+
if (!state.logPaused) fetchLogs();
|
|
1479
|
+
showTip(logPause,
|
|
1480
|
+
state.logPaused ? 'Log stream paused' : 'Log stream resumed',
|
|
1481
|
+
state.logPaused ? 'warn' : 'ok');
|
|
1482
|
+
});
|
|
1483
|
+
|
|
1484
|
+
var search = $('log-search');
|
|
1485
|
+
if (search) search.addEventListener('input', function () {
|
|
1486
|
+
state.logQuery = search.value.trim().toLowerCase();
|
|
1487
|
+
renderLogs();
|
|
1488
|
+
});
|
|
1489
|
+
|
|
1490
|
+
var filter = $('log-filter');
|
|
1491
|
+
if (filter) filter.addEventListener('click', function (event) {
|
|
1492
|
+
var button = event.target.closest('[data-log-level]');
|
|
1493
|
+
if (!button) return;
|
|
1494
|
+
state.logLevel = button.getAttribute('data-log-level') || 'all';
|
|
1495
|
+
var buttons = filter.querySelectorAll('[data-log-level]');
|
|
1496
|
+
for (var i = 0; i < buttons.length; i++) {
|
|
1497
|
+
buttons[i].classList.toggle('v2-seg__btn--active', buttons[i] === button);
|
|
1498
|
+
}
|
|
1499
|
+
renderLogs();
|
|
1500
|
+
});
|
|
1501
|
+
|
|
1502
|
+
document.addEventListener('visibilitychange', function () {
|
|
1503
|
+
if (!document.hidden && !state.paused) {
|
|
1504
|
+
pollStatus();
|
|
1505
|
+
fetchLogs();
|
|
1506
|
+
}
|
|
1507
|
+
});
|
|
1508
|
+
}
|
|
1509
|
+
|
|
1510
|
+
function syncPauseButton() {
|
|
1511
|
+
var pause = $('btn-pause');
|
|
1512
|
+
if (!pause) return;
|
|
1513
|
+
pause.setAttribute('aria-pressed', String(state.paused));
|
|
1514
|
+
pause.setAttribute('aria-label', state.paused ? 'Resume auto-refresh' : 'Pause auto-refresh');
|
|
1515
|
+
pause.title = state.paused ? 'Resume auto-refresh' : 'Pause auto-refresh';
|
|
1516
|
+
swapIcons(pause, !state.paused);
|
|
1517
|
+
}
|
|
1518
|
+
|
|
1519
|
+
/* -- boot ------------------------------------------------------- */
|
|
1520
|
+
function start() {
|
|
1521
|
+
applyStatus('connecting'); // the dock badge must never read "-"
|
|
1522
|
+
initControls();
|
|
1523
|
+
syncPauseButton();
|
|
1524
|
+
updateLiveAge();
|
|
1525
|
+
pollStatus();
|
|
1526
|
+
fetchLogs();
|
|
1527
|
+
|
|
1528
|
+
setInterval(function () {
|
|
1529
|
+
if (!document.hidden) pollStatus();
|
|
1530
|
+
}, POLL_MS);
|
|
1531
|
+
|
|
1532
|
+
setInterval(function () {
|
|
1533
|
+
if (!document.hidden) fetchLogs();
|
|
1534
|
+
}, LOG_POLL_MS);
|
|
1535
|
+
|
|
1536
|
+
setInterval(updateLiveAge, 1000);
|
|
1537
|
+
}
|
|
1538
|
+
|
|
1539
|
+
if (document.readyState === 'loading') {
|
|
1540
|
+
document.addEventListener('DOMContentLoaded', start);
|
|
1541
|
+
} else {
|
|
1542
|
+
start();
|
|
1543
|
+
}
|
|
1544
|
+
})();
|