webui v165: a Debug page

The API timings went into the phone status sheet in v162; a page of
their own reads better and works in the wide layout too. /debug, in the
rail and in the phone's More sheet: the web UI build and the core
version with its round trip, a Reload button, requests in flight, the
last sendChat round trip, the five slowest and the twenty last requests,
the event stream (bytes, last event, reconnections), and a reset. The
status sheet keeps the version label and its Reload button only.
This commit is contained in:
jolavillette 2026-08-29 13:01:50 +02:00
parent 4e3530cc20
commit 410359b4fa
7 changed files with 219 additions and 41 deletions

View File

@ -0,0 +1,110 @@
const m = require('mithril');
const rs = require('rswebui');
// A page for what is otherwise invisible from a phone: which build this is,
// what the core answers, and what the API is doing from this browser --
// requests in flight, the round trip of the last chat message, the slowest
// calls, the health of the event stream. Numbers, not a console.
const Debug = () => {
let timer = null;
let coreVersion = null;
let coreVersionAt = 0;
const ago = (t) => (t ? Math.round((Date.now() - t) / 1000) + ' s ago' : 'never');
const short = (p) => String(p || '').replace(/^\/rs/, '');
const loadCoreVersion = () => {
const startedAt = performance.now();
rs.rsJsonApiRequest('/rsJsonApi/version', {}, (data, success) => {
coreVersionAt = Math.round(performance.now() - startedAt);
coreVersion = success && data ? data : null;
m.redraw();
});
};
return {
oninit: () => {
loadCoreVersion();
// The counters move on their own; redraw once a second while here.
timer = setInterval(() => m.redraw(), 1000);
},
onremove: () => {
if (timer) clearInterval(timer);
},
view: (vnode) => {
const s = rs.apiStats;
const version = vnode.attrs.version || '';
const core = coreVersion
? `${coreVersion.major}.${coreVersion.minor}.${coreVersion.mini}${coreVersion.extra || ''} (${coreVersion.human || ''})`
: 'unknown';
return m('.debug-page', [
m('h2', 'Debug'),
m('.debug-section', [
m('h3', 'Build'),
m('.debug-grid', [
m('span', 'Web UI'), m('strong', version),
m('span', 'Core'), m('strong', core),
m('span', 'Core round trip'), m('strong', coreVersion ? coreVersionAt + ' ms' : '-'),
m('span', 'Page loaded'), m('strong', ago(s.startedAt)),
m('span', 'Viewport'), m('strong', `${window.innerWidth} x ${window.innerHeight} px`),
]),
m('.debug-actions', [
// The page keeps the code it loaded until reloaded, and a phone
// browser hides that action away: a new build shows only after this.
m('button', { onclick: () => window.location.reload(true) }, [m('i.fas.fa-sync-alt'), ' Reload the web UI']),
m('button', { onclick: loadCoreVersion }, [m('i.fas.fa-stopwatch'), ' Ping the core']),
]),
]),
m('.debug-section', [
m('h3', 'API from this browser'),
m('.debug-grid', [
m('span', 'Requests in flight'), m('strong', s.pending),
m('span', 'Requests since load'), m('strong', s.total),
m('span', 'Last sendChat'), m('strong', s.lastSend ? `${s.lastSend.ms} ms, ${ago(s.lastSend.at)}` : 'none yet'),
]),
m('h4', 'Slowest requests'),
s.slowest.length === 0
? m('p.debug-empty', 'Nothing yet.')
: m('table.debug-table', [
m('thead', m('tr', [m('th', 'Request'), m('th', 'Time'), m('th', 'When')])),
m('tbody', s.slowest.map((e) => m('tr', [
m('td', short(e.path)),
m('td', e.ms + ' ms'),
m('td', ago(e.at)),
]))),
]),
m('h4', 'Last requests'),
s.recent.length === 0
? m('p.debug-empty', 'Nothing yet.')
: m('table.debug-table', [
m('thead', m('tr', [m('th', 'Request'), m('th', 'Time'), m('th', 'When')])),
m('tbody', s.recent.map((e) => m('tr', [
m('td', short(e.path)),
m('td', e.ms + ' ms'),
m('td', ago(e.at)),
]))),
]),
m('.debug-actions', [
m('button', { onclick: () => rs.resetApiStats() }, [m('i.fas.fa-eraser'), ' Reset counters']),
]),
]),
m('.debug-section', [
m('h3', 'Event stream'),
m('.debug-grid', [
m('span', 'Received'), m('strong', rs.formatBytes(s.eventsBytes)),
m('span', 'Last event'), m('strong', ago(s.lastEventAt)),
m('span', 'Reconnections'), m('strong', s.eventsRestarts),
]),
m('p.debug-hint', 'The stream carries every event of the core over one long request; it holds one of the six connections a browser keeps to a host, the others queue the requests above.'),
]),
]);
},
};
};
module.exports = Debug;

View File

@ -1,7 +1,7 @@
const m = require('mithril');
// Bumped at every change of the web UI (two places used to carry it).
const WEBUI_VERSION = 'v164';
const WEBUI_VERSION = 'v165';
const login = require('login');
const rs = require('rswebui');
@ -16,6 +16,7 @@ const forums = require('forums/forums');
const boards = require('boards/boards');
const config = require('config/config_resolver');
const statistics = require('statistics/statistics');
const debug = require('debug/debug');
const statusbar = require('statusbar');
const networkState = require('network/network_state');
const peopleState = require('people/people_state');
@ -46,6 +47,7 @@ const navIcon = {
boards: 'i.fas.fa-globe.sidenav-icon',
config: 'i.fas.fa-cogs.sidenav-icon',
statistics: 'i.fas.fa-chart-pie.sidenav-icon',
debug: 'i.fas.fa-bug.sidenav-icon',
};
const navbar = () => {
@ -201,6 +203,7 @@ const mobileMoreLinks = {
boards: '/boards/MyBoards',
config: '/config/network',
statistics: '/statistics',
debug: '/debug',
};
const MobileStatus = () => {
@ -264,23 +267,6 @@ const MobileStatus = () => {
m('small', statusbar.formatBytes(state.totalOut)),
]),
]),
// Where the seconds go, seen from this phone. A request slow on its
// own points at the core; many pending at once points at the
// browser's six sockets, one of them held by the event stream.
(() => {
const s = rs.apiStats;
const ago = (t) => (t ? Math.round((Date.now() - t) / 1000) + 's ago' : 'never');
const short = (p) => String(p || '').replace(/^\/rs/, '');
return m('.mobile-status-sheet__diag', [
m('h4', 'API from this browser'),
m('div', `pending ${s.pending} · total ${s.total} · up ${Math.round((Date.now() - s.startedAt) / 1000)}s`),
m('div', s.lastSend
? `last sendChat ${s.lastSend.ms} ms (${ago(s.lastSend.at)})`
: 'no sendChat yet'),
m('div', `events: ${statusbar.formatBytes(s.eventsBytes)}, last ${ago(s.lastEventAt)}, restarts ${s.eventsRestarts}`),
s.slowest.length > 0 && m('div', 'slowest: ' + s.slowest.map((e) => `${short(e.path)} ${e.ms}ms`).join(', ')),
]);
})(),
m('.mobile-status-sheet__version', [
'WebUI ' + WEBUI_VERSION,
// The page keeps the code it loaded until it is reloaded, and a
@ -381,6 +367,7 @@ const Layout = () => {
boards: '/boards/MyBoards',
statistics: '/statistics',
config: '/config/network',
debug: '/debug',
},
}),
m(
@ -474,6 +461,9 @@ m.route(document.getElementById('main'), '/', {
'/statistics': {
render: () => m(Layout, m(statistics)),
},
'/debug': {
render: () => m(Layout, m(debug, { version: WEBUI_VERSION })),
},
});
// v51 architectural fix: ensure event queue starts on direct route refresh

View File

@ -106,6 +106,8 @@ const apiStats = {
lastSend: null,
// The five slowest requests since load, newest first on a tie.
slowest: [],
// The last twenty requests, newest first.
recent: [],
// Event stream: bytes received since (re)connection, last event time,
// number of reconnections.
eventsBytes: 0,
@ -121,6 +123,16 @@ function recordRequestTime(path, ms) {
apiStats.slowest.push(entry);
apiStats.slowest.sort((a, b) => b.ms - a.ms);
if (apiStats.slowest.length > 5) apiStats.slowest.length = 5;
apiStats.recent.unshift(entry);
if (apiStats.recent.length > 20) apiStats.recent.length = 20;
}
function resetApiStats() {
apiStats.total = 0;
apiStats.lastSend = null;
apiStats.slowest = [];
apiStats.recent = [];
apiStats.eventsRestarts = 0;
}
const connectionState = {
@ -577,6 +589,7 @@ module.exports = {
idToHex: hexId,
connectionState,
apiStats,
resetApiStats,
setKeys,
setBackgroundTask,
logon,

View File

@ -616,28 +616,6 @@
}
}
/* API diagnostics in the phone status sheet (main.js MobileStatus): request
* timings seen from this browser, so a slow phone can be measured without a
* console. */
.mobile-status-sheet__diag {
margin-top: 0.75rem;
padding: 0.5rem 0.75rem;
border-radius: 0.5rem;
background: rgba(15, 23, 42, 0.06);
font-size: 0.75rem;
line-height: 1.4;
color: #334155;
word-break: break-word;
h4 {
margin: 0 0 0.25rem;
font-size: 0.7rem;
text-transform: uppercase;
letter-spacing: 0.05em;
color: #64748b;
}
}
/* Version label in the phone header, and the reload button beside the
* version in the status sheet (main.js MobileStatus). */
.mobile-app-header__version {

View File

@ -0,0 +1,86 @@
// The debug page (debug/debug.js): build, API timings, event stream.
.debug-page {
padding: 1.25rem;
max-width: 720px;
h2 {
margin: 0 0 1rem;
font-size: 1.25rem;
}
h3 {
margin: 0 0 0.5rem;
font-size: 0.8rem;
text-transform: uppercase;
letter-spacing: 0.05em;
color: #64748b;
}
h4 {
margin: 0.75rem 0 0.25rem;
font-size: 0.8rem;
color: #475569;
}
}
.debug-section {
background: #ffffff;
border: 1px solid #e2e8f0;
border-radius: 0.5rem;
padding: 0.75rem 1rem;
margin-bottom: 1rem;
}
.debug-grid {
display: grid;
grid-template-columns: max-content 1fr;
gap: 0.25rem 1rem;
font-size: 0.9rem;
color: #334155;
strong {
word-break: break-word;
}
}
.debug-actions {
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
margin-top: 0.75rem;
button {
border: 1px solid #cbd5e1;
background: #f8fafc;
color: #334155;
border-radius: 0.375rem;
padding: 0.4rem 0.75rem;
font-size: 0.85rem;
font-weight: 600;
cursor: pointer;
}
}
.debug-table {
width: 100%;
border-collapse: collapse;
font-size: 0.85rem;
th, td {
text-align: left;
padding: 0.25rem 0.5rem;
border-bottom: 1px solid #f1f5f9;
white-space: nowrap;
}
td:first-child {
white-space: normal;
word-break: break-all;
}
}
.debug-empty, .debug-hint {
font-size: 0.8rem;
color: #64748b;
margin: 0.25rem 0 0;
}

View File

@ -10,3 +10,4 @@
@forward "board";
@forward "config";
@forward "statistics";
@forward "debug";

File diff suppressed because one or more lines are too long