Add cross-reference map, admin health page, passkeys and device list, settings tabs
Build and Push Docker Image / build-and-push (push) Successful in 10m22s

- Reader: chapter cross-reference map (M) from IQ Bible's reference graph
- Admin: Health tab with disk use, snapshots (back up, download, restore), failure logs
- Passkey sign-in (WebAuthn), signed-in device list, sign out others, optional sign-in alerts
- Settings split into Account, Preferences, Notifications, Security, Your data tabs
- ntfy server field: catch a topic typed into the server box

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
nmemmertandClaude Sonnet 5.5 committed 2026-10-06 12:52:49 -04:00
1 parent 207bfcb919
commit bdb04cdedc
35 files changed
+1860 -22

No files matched your search

@@ -0,0 +1,77 @@
// Settings → Security: where you're signed in, with a way to sign the others out, and an optional alert for new sign-ins.
import { useEffect, useState } from 'react';
import { useApp } from '../../context/AppContext.js';
import { listDevices, setLoginAlerts, signOutDevice, signOutOtherDevices } from '../../syncService.js';
const btn = 'rounded-lg border border-[var(--r-line)] px-3 py-1.5 text-xs hover:bg-black/5 disabled:opacity-50';
const METHODS = { password: 'password', passkey: 'passkey', mfa: 'password and code', register: 'new account' };
export function whenText(at, now = Date.now()) {
const m = Math.round((now - at) / 60000);
if (m < 2) return 'just now';
if (m < 120) return `${m} minutes ago`;
if (m < 2880) return `${Math.round(m / 60)} hours ago`;
return `${Math.round(m / 1440)} days ago`;
}
export default function DevicesSection() {
const { authUser, setAuthUser } = useApp();
const [devices, setDevices] = useState(null);
const [error, setError] = useState('');
const [busy, setBusy] = useState(false);
const load = async () => { const r = await listDevices(); if (r.ok) setDevices(r.data); else setError(r.error ?? 'Could not load your devices.'); };
useEffect(() => { load(); }, []);
const signOut = async (d) => {
if (!window.confirm(`Sign out ${d.label}?`)) return;
const r = await signOutDevice(d.id);
if (!r.ok && r.status !== 404) setError(r.error ?? 'Could not sign it out.');
load();
};
const signOutOthers = async () => {
if (!window.confirm('Sign out every other device? They’ll need to sign in again.')) return;
setBusy(true);
const r = await signOutOtherDevices();
setBusy(false);
if (!r.ok) setError(r.error ?? 'Could not sign them out.');
load();
};
const toggleAlerts = async (e) => {
setError('');
const r = await setLoginAlerts(e.target.checked);
if (r.ok) setAuthUser({ ...authUser, loginAlerts: r.data.loginAlerts }); else setError(r.error ?? 'Could not change that.');
};
const others = (devices ?? []).filter((d) => !d.current);
return (
<section aria-label="Devices" className="space-y-3 rounded-2xl border border-[var(--r-line)] bg-[var(--r-bar)] p-6">
<div>
<h2 className="text-base font-medium text-[var(--r-text)]">Where you’re signed in</h2>
<p className="mt-1 text-sm text-[var(--r-sub)]">If you don’t recognise one, sign it out and change your password.</p>
</div>
{devices === null && !error && <p className="text-sm text-[var(--r-sub)]">Loading…</p>}
{devices && (
<ul className="divide-y divide-[var(--r-line)] text-sm">
{devices.map((d) => (
<li key={d.id} className="flex flex-wrap items-center justify-between gap-2 py-2">
<span className="min-w-0">
<span className="font-medium">{d.label}</span>{d.current && <span className="ml-2 rounded-full bg-black/10 px-2 py-0.5 text-[11px]">This device</span>}
<span className="block text-xs text-[var(--r-sub)]">
{d.current ? 'Active now' : `Last active ${whenText(d.lastSeen)}`}{d.method ? ` · signed in with ${METHODS[d.method] ?? d.method}` : ''}{d.ip ? ` · ${d.ip}` : ''}
</span>
</span>
{!d.current && <button type="button" onClick={() => signOut(d)} className={btn}>Sign out</button>}
</li>
))}
</ul>
)}
{others.length > 0 && <button type="button" onClick={signOutOthers} disabled={busy} className={btn}>{busy ? 'Signing out…' : `Sign out the other ${others.length === 1 ? 'device' : `${others.length} devices`}`}</button>}
{error && <p role="alert" className="text-sm text-[var(--r-danger,#b42318)]">{error}</p>}
<label className="flex items-start gap-2 border-t border-[var(--r-line)] pt-3 text-sm">
<input type="checkbox" checked={!!authUser?.loginAlerts} onChange={toggleAlerts} className="mt-1" />
<span>Tell me when someone signs in<span className="block text-xs text-[var(--r-sub)]">Sends a message to your notification topic (set up under Notifications) each time a device signs in.</span></span>
</label>
</section>
);
}