Admin health: push devices, pushes sent, expired devices and the last failure
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
551f970721
commit
c4b830dd43
5 files changed
+18
-4
No files matched your search
@@ -158,6 +158,7 @@ function viewSettings(args) {
|
||||
(h.plex.configured ? row('Plex', h.plex.ok ? 'Answering in ' + h.plex.ms + ' ms' + (h.plex.user ? ' · using ' + esc(h.plex.user) : '') : esc(h.plex.error || 'Not answering'), !h.plex.ok) : row('Plex', 'Not set up', false))
|
||||
+ (h.services || []).map(x => row(esc(x.name), x.ok ? 'Answering in ' + x.ms + ' ms' : esc(x.error || 'Not answering'), !x.ok)).join('')
|
||||
+ (h.mail.length ? h.mail.map(m => row('Mail: ' + esc(m.address), m.ok ? 'Connected (' + m.ms + ' ms)' : esc(m.error || 'Could not connect'), !m.ok)).join('') : row('Mail', 'No account connected', false))
|
||||
+ (h.push ? row('Reminders and push', h.push.devices ? h.push.devices + ' device' + (h.push.devices === 1 ? '' : 's') + ' for ' + h.push.people + (h.push.people === 1 ? ' person' : ' people') + ' · ' + h.push.sent + ' sent since the server started' + (h.push.expired ? ' · ' + h.push.expired + ' expired device' + (h.push.expired === 1 ? '' : 's') + ' removed' : '') + (h.push.failed ? ' · ' + h.push.failed + ' failed (last: ' + esc(h.push.lastErr) + ')' : '') : 'No device has turned reminders on yet', h.push.failed > 0 && h.push.lastErrAt > h.push.lastOk) : '')
|
||||
+ (h.disk ? row('Disk', gb(h.disk.free) + ' free of ' + gb(h.disk.total), h.disk.free / h.disk.total < .1 || h.disk.free < 2e9) : '')
|
||||
+ row('Last backup download', h.backup ? days(h.backup.t) + ' day' + (days(h.backup.t) === 1 ? '' : 's') + ' ago (by ' + esc(h.backup.who || '?') + ')' : 'Never. Use Download a backup below.', !h.backup || days(h.backup.t) > 30)
|
||||
+ (h.transcripts && h.transcripts.enabled ? row('Transcripts', `${h.transcripts.saved} saved · ${h.transcripts.working ? 'working on ' + h.transcripts.working : 'idle'}${h.transcripts.failed ? ' · ' + h.transcripts.failed + ' failed recently' : ''} · ${h.transcripts.auto ? 'automatic for the newest ' + h.transcripts.keep + ' of each show' : 'automatic transcribing is off'}`, false) : '')
|
||||
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
// App-shell cache only; Seafile/API traffic always goes to the network.
|
||||
const CACHE = 'hub-v139';
|
||||
const CACHE = 'hub-v140';
|
||||
const KEEP = /^hub-(offline|share|who|data-)/; // caches that outlive a new version
|
||||
const SHARE = 'hub-share'; // files shared into the app from other apps, held until the person picks a folder
|
||||
const SHELL = ['/icon.svg', '/manifest.webmanifest', '/icons/icon-192.png', '/icons/apple-touch-icon.png'];
|
||||
|
||||
@@ -36,13 +36,23 @@ function localNow(tz, at = new Date()) {
|
||||
return { day: `${p.year}-${p.month}-${p.day}`, hm: `${p.hour}:${p.minute}` };
|
||||
}
|
||||
|
||||
// Since the server started: how many pushes went out, and what the push service last refused (shown to the admin in Settings > Admin)
|
||||
const stats = { sent: 0, failed: 0, expired: 0, lastOk: 0, lastErr: '', lastErrAt: 0 };
|
||||
function health() {
|
||||
let devices = 0, people = 0;
|
||||
for (const u of users.all()) { if (u.disabled) continue; const n = load(users.dirOf(u.id)).subs.length; devices += n; if (n) people++; }
|
||||
return { ...stats, devices, people };
|
||||
}
|
||||
async function send(dir, st, uid, payload) {
|
||||
if (uid) inbox.add(uid, { kind: 'remind', title: payload.title, body: payload.body, url: payload.url, tag: payload.tag, ref: payload.ref }); // (reminders also land in the Notifications list)
|
||||
keys(); const { ref, ...wire } = payload;
|
||||
let changed = false;
|
||||
await Promise.all(st.subs.map(async s => {
|
||||
try { await webpush.sendNotification(s.sub, JSON.stringify(wire), { TTL: 3600 }); }
|
||||
catch (e) { if (e.statusCode === 404 || e.statusCode === 410) { s.dead = true; changed = true; } else console.warn('[push] send failed:', e.statusCode || e.message); }
|
||||
try { await webpush.sendNotification(s.sub, JSON.stringify(wire), { TTL: 3600 }); stats.sent++; stats.lastOk = Date.now(); }
|
||||
catch (e) {
|
||||
if (e.statusCode === 404 || e.statusCode === 410) { s.dead = true; changed = true; stats.expired++; }
|
||||
else { stats.failed++; stats.lastErr = String(e.statusCode || e.message).slice(0, 120); stats.lastErrAt = Date.now(); console.warn('[push] send failed:', e.statusCode || e.message); }
|
||||
}
|
||||
}));
|
||||
if (changed) { st.subs = st.subs.filter(s => !s.dead); save(dir, st); }
|
||||
}
|
||||
@@ -182,4 +192,4 @@ async function handle(req, res) {
|
||||
}
|
||||
/** A notification for one person's devices (a chat message, say), unless they turned that kind off. */
|
||||
async function notify(uid, payload) { const dir = users.dirOf(uid), st = load(dir); if (!st.subs.length || st.chat === false) return; await send(dir, st, null, payload); }
|
||||
module.exports = { handle, start, tick, localNow, notify };
|
||||
module.exports = { handle, start, tick, localNow, notify, health };
|
||||
@@ -687,6 +687,7 @@ async function handleRequest(req, res) {
|
||||
const checks = [probe('Seafile (Files)', SEAFILE_URL + '/api2/ping/'), probe('OnlyOffice', ONLYOFFICE_URL + '/healthcheck'), probe('Audiobooks', (process.env.OPENAUDIBLE_URL || AUDIOBOOKS_URL).replace(/\/$/, '') + '/'), probe('Podcasts (Podgrab)', PODCAST_URL)];
|
||||
if (recipes.enabled()) checks.push(probe('Recipes (Mealie)', (process.env.MEALIE_URL || 'https://mealie.necloud.us').replace(/\/+$/, '') + '/api/app/about'));
|
||||
out.services = await Promise.all(checks);
|
||||
try { out.push = require('./push').health(); } catch { out.push = null; }
|
||||
out.lyrics = lyrics.enabled(); out.signal = signal.enabled(); out.transcripts = transcripts.status();
|
||||
res.writeHead(200, { 'content-type': 'application/json', 'cache-control': 'no-store' }); return res.end(JSON.stringify(out));
|
||||
}
|
||||
|
||||
@@ -78,6 +78,8 @@ const users = require('../users'), push = require('../push');
|
||||
dead = true; writePush({ tasks: true, tasksAt: '00:00', tasksDay: '' }); fs.writeFileSync(path.join(d, 'tasks.json'), JSON.stringify({ lists: [{ id: 'inbox', name: 'Tasks' }], tasks: [{ id: 't1', list: 'inbox', title: 'X', due: '2000-01-01', done: false }] }));
|
||||
await push.tick(new Date());
|
||||
ok('a device that has unsubscribed (410) is removed', JSON.parse(fs.readFileSync(path.join(d, 'push.json'), 'utf8')).subs.length === 0, fs.readFileSync(path.join(d, 'push.json'), 'utf8'));
|
||||
const h = push.health();
|
||||
ok('the admin health report counts the pushes sent and the expired device removed', h.sent > 0 && h.expired >= 1 && typeof h.devices === 'number' && typeof h.people === 'number', h);
|
||||
|
||||
fs.rmSync(dir, { recursive: true, force: true });
|
||||
console.log(failed ? `\n${failed} check(s) failed` : '\nAll push checks passed'); process.exit(failed ? 1 : 0);
|
||||
|
||||
Reference in new issue
Block a user