Admin health report (Plex, mail accounts, disk, last backup)
This commit is contained in:
1 parent
2c8afdef79
commit
1981e66b8a
4 files changed
+30
-3
No files matched your search
@@ -335,4 +335,8 @@ async function newest() {
|
||||
for (const a of accounts()) { try { const r = await withTimeout(providerOf(a).list(a, { folder: 'inbox', skip: 0, q: '' }), 15000); for (const m of r.items.slice(0, 10)) out.push({ id: a.id + ':' + m.id, subject: m.subject, from: (m.from && (m.from.name || m.from.address)) || '', read: !!m.read }); } catch { /* skipped this round */ } }
|
||||
return out;
|
||||
}
|
||||
module.exports = { newest, enabled, outlookOn, handle, callback, secretStatus, recipients, textToHtml };
|
||||
/** For the admin health page: can each connected account be reached right now? */
|
||||
async function health() {
|
||||
return Promise.all(accounts().map(async a => { const t = Date.now(); try { await withTimeout(providerOf(a).unread(a), 8000); return { address: a.address, type: a.type, ok: true, ms: Date.now() - t }; } catch (e) { return { address: a.address, type: a.type, ok: false, error: e.message }; } }));
|
||||
}
|
||||
module.exports = { health, newest, enabled, outlookOn, handle, callback, secretStatus, recipients, textToHtml };
|
||||
@@ -92,6 +92,7 @@ function viewSettings() {
|
||||
</div>
|
||||
<div class="sgroup" data-g="admin">${isAdmin ? `<h2>People</h2><div class="list" id="people"><div class="row"><span class="grow"><small>Loading…</small></span></div></div>
|
||||
<div class="bar"><button class="btn" id="addperson">Add a person</button></div>
|
||||
<h2>Health</h2><div class="list" id="health"><div class="row"><span class="grow"><small>Checking…</small></span></div></div>
|
||||
<h2>Storage</h2><div class="list" id="stor"><div class="row"><span class="grow"><small>Checking…</small></span></div></div>
|
||||
<div class="bar"><a class="btn ghost" href="/api/backup" download>Download a backup</a></div><p class="sub">A .tar.gz of everything in /data (accounts, passkeys, tasks, events, settings, Signal history). Keep it private: it holds the session secret and encrypted sign-in tokens. Restore by unpacking it into the data volume.</p>` : ''}
|
||||
${isAdmin ? `<h2>Activity</h2><div class="list" id="audit"><div class="row"><span class="grow"><small>Loading…</small></span></div></div>` : ''}
|
||||
@@ -128,6 +129,17 @@ function viewSettings() {
|
||||
if (st.signedIn) { const b = $('#plexout'); b.hidden = false; b.onclick = async () => { await jfetch('/api/plex/logout', { method: 'POST' }); pl.a.pause(); pl.q = []; pl.i = -1; renderMini(); toast('Signed out of Plex'); viewSettings(); }; }
|
||||
}).catch(() => { const el = $('#plexst'); if (el) el.textContent = 'Unavailable'; });
|
||||
if (isAdmin) { loadPeople(); $('#addperson').onclick = () => addPersonSheet(); }
|
||||
if (isAdmin) jfetch('/api/health').then(h => {
|
||||
const row = (t, d, bad) => `<div class="row"><span class="grow"><b>${bad ? '⚠ ' : '✓ '}${t}</b><small style="white-space:normal;${bad ? 'color:var(--danger)' : ''}">${d}</small></span></div>`;
|
||||
const gb = n => (n / 1e9).toFixed(1) + ' GB', days = t => Math.floor((Date.now() - t) / 864e5);
|
||||
const up = h.uptime > 86400 ? Math.floor(h.uptime / 86400) + ' days' : h.uptime > 3600 ? Math.floor(h.uptime / 3600) + ' hours' : Math.max(1, Math.floor(h.uptime / 60)) + ' min';
|
||||
$('#health').innerHTML =
|
||||
(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.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.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)
|
||||
+ row('Server', 'Up ' + up + ' · Node ' + esc(h.node) + ' · lyrics ' + (h.lyrics ? 'on' : 'off') + ' · Signal ' + (h.signal ? 'on' : 'off'), false);
|
||||
}).catch(() => { const e = $('#health'); if (e) e.innerHTML = '<div class="row"><small>Could not read the health report</small></div>'; });
|
||||
if (isAdmin) jfetch('/api/system').then(i => {
|
||||
const anon = i.source && i.source.kind === 'anonymous', ok = i.mounted !== false && !anon && i.writable;
|
||||
const row = (t, d, bad) => `<div class="row"><span class="grow"><b>${t}</b><small style="white-space:normal;${bad ? 'color:var(--danger)' : ''}">${d}</small></span></div>`;
|
||||
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
// App-shell cache only; Seafile/API traffic always goes to the network.
|
||||
const CACHE = 'hub-v21';
|
||||
const CACHE = 'hub-v22';
|
||||
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'];
|
||||
|
||||
|
||||
@@ -589,7 +589,7 @@ async function handleRequest(req, res) {
|
||||
return users.runAs(user, async () => {
|
||||
const rule = ACCESS.find(([re]) => re.test(pathname));
|
||||
if (rule && !rule[1].some(t => users.can(user, t))) return forbidden(res);
|
||||
if ((pathname === '/api/system' || pathname === '/api/audit' || pathname.startsWith('/api/users')) && user.role !== 'admin') return forbidden(res, 'Only an admin can do this');
|
||||
if ((pathname === '/api/system' || pathname === '/api/health' || pathname === '/api/audit' || pathname.startsWith('/api/users')) && user.role !== 'admin') return forbidden(res, 'Only an admin can do this');
|
||||
if (pathname === '/api/me') { res.writeHead(200, { 'content-type': 'application/json' }); return res.end(JSON.stringify({ ...users.publicView(user), passkeys: auth.passkeyCount(user.id), people: user.role === 'admin' ? users.all().length : undefined })); }
|
||||
if (pathname.startsWith('/api/users')) return admin.handle(req, res, user);
|
||||
if (req.url === '/api/config') { // only reveal the addresses of things this person may use
|
||||
@@ -603,6 +603,17 @@ async function handleRequest(req, res) {
|
||||
if (pathname === '/api/ddg') return ddgAnswer(req, res);
|
||||
if (pathname.startsWith('/api/push/')) return push.handle(req, res);
|
||||
if (pathname === '/api/audit') { res.writeHead(200, { 'content-type': 'application/json', 'cache-control': 'no-store' }); return res.end(JSON.stringify({ events: require('./audit').recent(200) })); }
|
||||
if (pathname === '/api/health') { // everything an admin wants to know at a glance
|
||||
const fs = require('fs'), dir = path.resolve(process.env.DATA_DIR || path.join(__dirname, 'data')), out = { at: Date.now() };
|
||||
try { const f = fs.statfsSync(dir); out.disk = { free: f.bavail * f.bsize, total: f.blocks * f.bsize }; } catch { out.disk = null; }
|
||||
const bk = require('./audit').recent(2000).find(x => x.event === 'backup downloaded'); out.backup = bk ? { t: bk.t, who: bk.who } : null;
|
||||
out.uptime = Math.round((Date.now() - STARTED) / 1000); out.node = process.version;
|
||||
const ps = plexLoad();
|
||||
out.plex = !ps.srv ? { configured: false } : await (async () => { const t = Date.now(); try { const r = await fetch(ps.srv + '/identity', { headers: { 'X-Plex-Token': ps.stk || ps.token || '', Accept: 'application/json' }, signal: AbortSignal.timeout(6000) }); return { configured: true, ok: r.ok, ms: Date.now() - t, user: ps.user ? ps.user.title : '' }; } catch (e) { return { configured: true, ok: false, error: e.name === 'TimeoutError' ? 'No answer in 6 seconds' : e.message }; } })();
|
||||
try { out.mail = await mail.health(); } catch { out.mail = []; }
|
||||
out.lyrics = lyrics.enabled(); out.signal = signal.enabled();
|
||||
res.writeHead(200, { 'content-type': 'application/json', 'cache-control': 'no-store' }); return res.end(JSON.stringify(out));
|
||||
}
|
||||
if (pathname === '/api/system') { res.writeHead(200, { 'content-type': 'application/json' }); return res.end(JSON.stringify(storageInfo())); }
|
||||
if (pathname === '/api/settings') return settings.handle(req, res);
|
||||
if (pathname === '/api/search') return search.handle(req, res);
|
||||
|
||||
Reference in new issue
Block a user