Settings: General, Connections, Security and Admin tabs (last one remembered); Reminders no longer hangs when the service worker does not start

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
nmemmertandClaude Sonnet 5.5 committed 2026-10-22 13:50:17 -04:00
1 parent a3010299d0
commit 668c7241a1
4 files changed
+44 -15

No files matched your search

+3 -1
View File
@@ -13,7 +13,9 @@ async function remindersInit() {
if (!(allowed('tasks') || allowed('calendar'))) return document.getElementById('sec-rem').remove();
if (!remSupported()) { box.innerHTML = `<div class="row"><span class="grow"><b>Not available here</b><small style="white-space:normal">${/iPhone|iPad/.test(navigator.userAgent) ? 'On iPhone and iPad, add NE Hub to the Home Screen first (Share → Add to Home Screen), then open it from there.' : 'This browser does not support push notifications.'}</small></span></div>`; return; }
let info; try { info = await jfetch('/api/push/key'); } catch (e) { box.innerHTML = `<div class="row"><span class="grow"><small>${esc(e.message)}</small></span></div>`; return; }
const reg = await navigator.serviceWorker.ready, sub = await reg.pushManager.getSubscription();
const reg = await Promise.race([navigator.serviceWorker.ready, new Promise(ok => setTimeout(ok, 4000))]); // (never ready: the browser refused the service worker)
if (!reg) { box.innerHTML = '<div class="row"><span class="grow"><b>Not available here</b><small style="white-space:normal">The browser did not start the background service reminders need. Reload the page, or open NE Hub over https in a regular browser.</small></span></div>'; return; }
const sub = await reg.pushManager.getSubscription();
const on = !!sub && Notification.permission === 'granted';
const post = (p, body) => jfetch('/api/push/' + p, { method: 'POST', body: JSON.stringify({ tz: myTz(), ...body }) });
box.innerHTML = `<label class="row" style="cursor:pointer"><span class="grow"><b>Reminders on this device</b><small id="remst" style="white-space:normal">${Notification.permission === 'denied' ? 'Blocked in the browser settings for this site; allow notifications there first' : on ? 'On' : 'Off'}${info.devices ? ' · ' + info.devices + (info.devices === 1 ? ' device' : ' devices') + ' in total' : ''}</small></span><input type="checkbox" id="remon" ${on ? 'checked' : ''} ${Notification.permission === 'denied' ? 'disabled' : ''}></label>
+35 -14
View File
@@ -67,16 +67,13 @@ function viewSettings() {
const isAdmin = ME && ME.role === 'admin';
const tabNames = ME ? TABS.filter(t => ME.tabs.includes(t[0]) && !['home', 'settings'].includes(t[0])).map(t => t[1]).join(', ') : '';
$('#view').innerHTML = `<div class="wrap"><h1>Settings</h1>
${ME ? `<h2>Signed in as</h2><div class="list"><div class="row"><span class="grow"><b>${esc(ME.name)}</b><small style="white-space:normal">${isAdmin ? 'Admin · can use everything and manage people' : 'Can use: ' + esc(tabNames || 'Home and Settings only')}</small></span></div></div>` : ''}
${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>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>` : ''}
<h2>Passkeys</h2><div class="list" id="pks"><div class="row"><span class="grow"><small>Loading…</small></span></div></div>
<div class="bar"><button class="btn ghost" id="addpk">Add a passkey</button><button class="btn ghost" id="invpk">Invite a device</button><button class="btn danger" id="hubout">Sign out of NE Hub</button></div>
<h2>Recovery codes</h2><div class="list"><div class="row"><span class="grow"><b id="rcn">Checking…</b><small style="white-space:normal">One-time codes that let you add a new passkey if you lose every device. Keep them somewhere safe (a password manager or on paper).</small></span><button class="btn ghost" id="rcgen">Make codes</button></div></div>
${isAdmin ? `<h2>Activity</h2><div class="list" id="audit"><div class="row"><span class="grow"><small>Loading…</small></span></div></div>` : ''}
<div id="sec-conn"><h2>Connections</h2><div class="list">
<div class="sgrp" id="settabs" role="tablist" aria-label="Settings sections">${[['general', 'General'], ['connections', 'Connections'], ['security', 'Security'], ...(isAdmin ? [['admin', 'Admin']] : [])].map(([k, l]) => `<a href="#" role="tab" data-g="${k}">${l}</a>`).join('')}</div>
<div class="sgroup" data-g="general">${ME ? `<h2>Signed in as</h2><div class="list"><div class="row"><span class="grow"><b>${esc(ME.name)}</b><small style="white-space:normal">${isAdmin ? 'Admin · can use everything and manage people' : 'Can use: ' + esc(tabNames || 'Home and Settings only')}</small></span></div></div>` : ''}
<div id="sec-signal"><h2>Signal alerts</h2><p class="sub">How this computer gets your attention when a Signal message arrives (not while you are looking at that chat). Saved on this device only.</p>
<div class="list"><label class="row" style="cursor:pointer"><span class="grow"><b>Play a sound</b><small>A short chime</small></span><input type="checkbox" id="sga-s"></label>
<label class="row" style="cursor:pointer"><span class="grow"><b>Desktop notification</b><small id="sga-ns">Pops up even when this tab is in the background</small></span><input type="checkbox" id="sga-n"></label></div>
<div class="bar"><button class="btn ghost" id="sga-t">Play test sound</button></div></div>${remindersHtml()}<h2>Install</h2>${installHtml()}</div>
<div class="sgroup" data-g="connections"><div id="sec-conn"><h2>Connections</h2><div class="list">
<div class="row" id="row-sf"><span class="grow"><b>Seafile</b><small id="sfst">${esc(cfg.seafile)} · ${signed ? 'Checking…' : 'signed out'}</small></span></div>
<div class="row" id="row-plex"><span class="grow"><b>Plex</b><small id="plexst">Checking…</small></span><button class="btn danger" id="plexout" hidden>Sign out</button></div>
<div class="row" id="row-oo"><span class="grow"><b>OnlyOffice</b><small>${esc(cfg.onlyoffice)}</small></span></div>
@@ -88,10 +85,17 @@ function viewSettings() {
<input type="text" id="ab-u" autocomplete="off" autocapitalize="off" value="${esc(abCreds().u)}">
<label for="ab-p">Password</label>
<input type="password" id="ab-p" autocomplete="new-password" value="${esc(abCreds().p)}"></div>
</div><div id="sec-signal"><h2>Signal alerts</h2><p class="sub">How this computer gets your attention when a Signal message arrives (not while you are looking at that chat). Saved on this device only.</p>
<div class="list"><label class="row" style="cursor:pointer"><span class="grow"><b>Play a sound</b><small>A short chime</small></span><input type="checkbox" id="sga-s"></label>
<label class="row" style="cursor:pointer"><span class="grow"><b>Desktop notification</b><small id="sga-ns">Pops up even when this tab is in the background</small></span><input type="checkbox" id="sga-n"></label></div>
<div class="bar"><button class="btn ghost" id="sga-t">Play test sound</button></div></div>${remindersHtml()}<h2>Install</h2>${installHtml()}</div>`;
</div></div>
<div class="sgroup" data-g="security"><h2>Passkeys</h2><div class="list" id="pks"><div class="row"><span class="grow"><small>Loading…</small></span></div></div>
<div class="bar"><button class="btn ghost" id="addpk">Add a passkey</button><button class="btn ghost" id="invpk">Invite a device</button><button class="btn danger" id="hubout">Sign out of NE Hub</button></div>
<h2>Recovery codes</h2><div class="list"><div class="row"><span class="grow"><b id="rcn">Checking…</b><small style="white-space:normal">One-time codes that let you add a new passkey if you lose every device. Keep them somewhere safe (a password manager or on paper).</small></span><button class="btn ghost" id="rcgen">Make codes</button></div></div>
</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>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>` : ''}
</div></div>`;
remindersInit();
installInit();
// only show what this person may use
@@ -99,6 +103,7 @@ function viewSettings() {
hide(!((allowed('mail') && cfg.mail) || cfg.mailAdmin), '#sec-mail'); hide(!allowed('audio'), '#sec-audio'); hide(!allowed('signal'), '#sec-signal'); hide(!(allowed('files') || allowed('office')), '#row-sf, #out');
hide(!allowed('music'), '#row-plex'); hide(!allowed('office'), '#row-oo');
if (!document.querySelector('#sec-conn .list .row')) hide(true, '#sec-conn'); // nothing to show here for this person
settingsTabs();
if ($('#sga-s')) {
const a = sgAlerts(), ns = $('#sga-ns'); $('#sga-s').checked = a.sound; $('#sga-n').checked = a.notify && 'Notification' in window && Notification.permission === 'granted';
if (!('Notification' in window)) { $('#sga-n').disabled = true; ns.textContent = 'This browser does not support notifications'; }
@@ -190,3 +195,19 @@ function installInit() {
b.hidden = false;
b.onclick = async () => { const ev = installEvt; installEvt = null; b.hidden = true; try { await ev.prompt(); } catch {} };
}
// ---- Settings sections: one group at a time (General, Connections, Security, Admin), the last one is remembered on this device
function settingsTabs() {
const tabs = [...document.querySelectorAll('#settabs a')], groups = [...document.querySelectorAll('.sgroup')];
const empty = g => !g.textContent.trim() && !g.querySelector('input, button');
for (const g of groups) if (empty(g)) { g.remove(); const a = tabs.find(x => x.dataset.g === g.dataset.g); if (a) a.remove(); }
const left = [...document.querySelectorAll('#settabs a')]; if (left.length < 2) { const b = $('#settabs'); if (b) b.hidden = true; }
const show = k => {
if (!left.some(a => a.dataset.g === k)) k = left[0] && left[0].dataset.g;
left.forEach(a => { a.classList.toggle('on', a.dataset.g === k); a.setAttribute('aria-selected', a.dataset.g === k); });
document.querySelectorAll('.sgroup').forEach(g => { g.hidden = g.dataset.g !== k; });
try { store.set('settings_tab', k); } catch {}
};
left.forEach(a => a.onclick = e => { e.preventDefault(); show(a.dataset.g); $('#view').scrollTop = 0; });
show(store.get('settings_tab', 'general'));
}
+5
View File
@@ -734,3 +734,8 @@ body.nav-mini .ngrp span { display: none; }
.homeapps { order: 4; }
.homeapps h2:first-child { margin-top: 18px; }
}
#settabs { margin: 12px 0 0; }
.sgroup[hidden] { display: none; }
.sgroup > h2:first-child, .sgroup > div:first-child > h2:first-child { margin-top: 20px; }
@media (max-width: 560px) { #settabs { display: flex; } #settabs a { flex: 1; justify-content: center; padding: 6px 8px; } }