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,121 @@
import { afterEach, beforeEach, expect, test, vi } from 'vitest';
import { render, screen, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
const app = { authUser: { id: 'u1', email: 'a@x.test', loginAlerts: false }, setAuthUser: vi.fn() };
vi.mock('../../context/AppContext.js', () => ({ useApp: () => app }));
const webauthn = vi.hoisted(() => ({ browserSupportsWebAuthn: vi.fn(() => true), startRegistration: vi.fn(), startAuthentication: vi.fn() }));
vi.mock('@simplewebauthn/browser', () => webauthn);
import PasskeysSection from './PasskeysSection.jsx';
import DevicesSection, { whenText } from './DevicesSection.jsx';
import { signInWithPasskey } from '../../utils/passkeys.js';
const routes = {};
beforeEach(() => {
Object.keys(routes).forEach((k) => delete routes[k]);
app.setAuthUser.mockClear(); webauthn.browserSupportsWebAuthn.mockReturnValue(true);
vi.stubGlobal('fetch', vi.fn(async (url, opts) => {
const key = `${opts?.method ?? 'GET'} ${String(url).replace(/^.*\/api/, '')}`;
const r = routes[key] ?? { status: 404, body: { error: `no route ${key}` } };
return { ok: (r.status ?? 200) < 400, status: r.status ?? 200, json: async () => r.body };
}));
});
afterEach(() => vi.unstubAllGlobals());
const passkey = { id: 'k1', name: 'My phone', createdAt: 1_700_000_000_000, lastUsedAt: null };
test('lists passkeys and adds one through the device prompt', async () => {
routes['GET /passkeys'] = { body: [passkey] };
routes['POST /passkeys/options'] = { body: { challenge: 'c' } };
routes['POST /passkeys'] = { body: [passkey, { ...passkey, id: 'k2', name: 'Laptop' }] };
webauthn.startRegistration.mockResolvedValue({ id: 'k2' });
const user = userEvent.setup();
render(<PasskeysSection />);
expect(await screen.findByText('My phone')).toBeInTheDocument();
expect(screen.getByText(/not used yet/)).toBeInTheDocument();
await user.click(screen.getByRole('button', { name: /Add a passkey on this device/ }));
expect(await screen.findByText('Laptop')).toBeInTheDocument();
expect(webauthn.startRegistration).toHaveBeenCalledWith({ optionsJSON: { challenge: 'c' } });
});
test('says so when the device prompt is cancelled', async () => {
routes['GET /passkeys'] = { body: [] };
routes['POST /passkeys/options'] = { body: { challenge: 'c' } };
webauthn.startRegistration.mockRejectedValue(Object.assign(new Error('x'), { name: 'NotAllowedError' }));
const user = userEvent.setup();
render(<PasskeysSection />);
await user.click(await screen.findByRole('button', { name: /Add a passkey/ }));
expect(await screen.findByRole('alert')).toHaveTextContent('cancelled or timed out');
});
test('renames and removes a passkey', async () => {
routes['GET /passkeys'] = { body: [passkey] };
routes['PATCH /passkeys/k1'] = { body: [{ ...passkey, name: 'Pixel' }] };
routes['DELETE /passkeys/k1'] = { body: [] };
vi.spyOn(window, 'confirm').mockReturnValue(true);
const user = userEvent.setup();
render(<PasskeysSection />);
await user.click(await screen.findByRole('button', { name: 'Rename' }));
const input = screen.getByLabelText('Passkey name');
await user.clear(input); await user.type(input, 'Pixel'); await user.click(screen.getByRole('button', { name: 'Save' }));
expect(await screen.findByText('Pixel')).toBeInTheDocument();
await user.click(screen.getByRole('button', { name: 'Remove' }));
await waitFor(() => expect(screen.queryByText('Pixel')).not.toBeInTheDocument());
});
test('hides the button where passkeys cannot work', async () => {
webauthn.browserSupportsWebAuthn.mockReturnValue(false);
routes['GET /passkeys'] = { body: [] };
render(<PasskeysSection />);
expect(await screen.findByText(/can’t use passkeys/)).toBeInTheDocument();
expect(screen.queryByRole('button', { name: /Add a passkey/ })).not.toBeInTheDocument();
});
test('sign-in with a passkey asks the server, then the device, then the server again', async () => {
routes['POST /auth/passkey/options'] = { body: { challenge: 'c' } };
routes['POST /auth/passkey/verify'] = { body: { id: 'u1', email: 'a@x.test' } };
webauthn.startAuthentication.mockResolvedValue({ id: 'k1' });
expect(await signInWithPasskey()).toMatchObject({ ok: true, data: { email: 'a@x.test' } });
webauthn.startAuthentication.mockRejectedValue(Object.assign(new Error('x'), { name: 'NotAllowedError' }));
expect(await signInWithPasskey()).toMatchObject({ ok: false, cancelled: true });
});
test('shows devices, signs one out, and signs out the rest', async () => {
const devices = [
{ id: 'd1', current: true, label: 'Safari on macOS', method: 'password', ip: '1.1.1.1', lastSeen: Date.now() },
{ id: 'd2', current: false, label: 'Chrome on Windows', method: 'passkey', ip: '2.2.2.2', lastSeen: Date.now() - 3 * 86400000 },
];
routes['GET /sessions'] = { body: devices };
routes['DELETE /sessions/d2'] = { body: { ok: true } };
vi.spyOn(window, 'confirm').mockReturnValue(true);
const user = userEvent.setup();
render(<DevicesSection />);
expect(await screen.findByText('Chrome on Windows')).toBeInTheDocument();
expect(screen.getByText('This device')).toBeInTheDocument();
expect(screen.getByText(/signed in with passkey/)).toBeInTheDocument();
routes['GET /sessions'] = { body: [devices[0]] };
await user.click(screen.getByRole('button', { name: 'Sign out' }));
await waitFor(() => expect(screen.queryByText('Chrome on Windows')).not.toBeInTheDocument());
expect(screen.queryByRole('button', { name: /Sign out the other/ })).not.toBeInTheDocument();
});
test('the sign-in alert switch reports a server refusal', async () => {
routes['GET /sessions'] = { body: [] };
routes['PUT /auth/login-alerts'] = { status: 400, body: { error: 'Set up a notification topic first (under Notifications).' } };
const user = userEvent.setup();
render(<DevicesSection />);
await user.click(await screen.findByRole('checkbox'));
expect(await screen.findByRole('alert')).toHaveTextContent('notification topic');
expect(app.setAuthUser).not.toHaveBeenCalled();
routes['PUT /auth/login-alerts'] = { body: { loginAlerts: true } };
await user.click(screen.getByRole('checkbox'));
await waitFor(() => expect(app.setAuthUser).toHaveBeenCalledWith(expect.objectContaining({ loginAlerts: true })));
});
test('times read plainly', () => {
const now = 1_000_000_000_000;
expect(whenText(now - 30_000, now)).toBe('just now');
expect(whenText(now - 5 * 3600_000, now)).toBe('5 hours ago');
expect(whenText(now - 3 * 86400_000, now)).toBe('3 days ago');
});