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
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:
1 parent
207bfcb919
commit
bdb04cdedc
35 files changed
+1860
-22
No files matched your search
@@ -0,0 +1,50 @@
|
||||
// @vitest-environment node
|
||||
import { afterEach, beforeEach, expect, test, vi } from 'vitest';
|
||||
import { mkdtempSync, rmSync } from 'fs';
|
||||
import { tmpdir } from 'os';
|
||||
import { join } from 'path';
|
||||
|
||||
let dir;
|
||||
beforeEach(() => { dir = mkdtempSync(join(tmpdir(), 'health-')); process.env.DATA_DIR = dir; vi.resetModules(); });
|
||||
afterEach(() => { rmSync(dir, { recursive: true, force: true }); delete process.env.DATA_DIR; });
|
||||
|
||||
const load = async () => {
|
||||
const db = await import('./db.js'); db.initDb();
|
||||
return { db, backups: await import('./backups.js'), health: await import('./health.js') };
|
||||
};
|
||||
|
||||
test('the event log keeps the newest first, filters by kind and is capped', async () => {
|
||||
const { health } = await load();
|
||||
health.logEvent('signin', 'a'); health.logEvent('notify', 'b'); health.logEvent('signin', 'c');
|
||||
expect(health.recentEvents('signin').map((e) => e.detail)).toEqual(['c', 'a']);
|
||||
for (let i = 0; i < 300; i += 1) health.logEvent('signin', String(i));
|
||||
expect(health.recentEvents(null, 1000)).toHaveLength(200);
|
||||
});
|
||||
|
||||
test('a snapshot can be restored, and what was there is kept as a safety copy', async () => {
|
||||
const { db, backups } = await load();
|
||||
db.createUser({ id: 'u1', email: 'a@x.test', passwordHash: 'h' });
|
||||
await backups.runBackup(new Date('2026-10-01T10:00:00Z'));
|
||||
db.createUser({ id: 'u2', email: 'b@x.test', passwordHash: 'h' });
|
||||
expect(db.countUsers()).toBe(2);
|
||||
const [snap] = backups.listSnapshots();
|
||||
const result = await backups.restoreSnapshot(snap.name, new Date('2026-10-02T10:00:00Z'));
|
||||
expect(db.countUsers()).toBe(1); // back to when the snapshot was taken
|
||||
expect(backups.listSnapshots().map((s) => s.name)).toContain(result.safetyCopy.split('/').pop());
|
||||
});
|
||||
|
||||
test('only real snapshot names resolve', async () => {
|
||||
const { backups } = await load();
|
||||
expect(backups.snapshotPath('../projects.db')).toBeNull();
|
||||
expect(backups.snapshotPath('projects-2026-01-01-00-00.db')).toBeNull(); // valid name, but no such file
|
||||
await expect(backups.restoreSnapshot('nope')).rejects.toThrow('No such snapshot');
|
||||
});
|
||||
|
||||
test('disk use reports the database and snapshot sizes', async () => {
|
||||
const { backups, health } = await load();
|
||||
await backups.runBackup();
|
||||
const use = health.diskUse();
|
||||
expect(use.db).toBeGreaterThan(0);
|
||||
expect(use.backups).toBeGreaterThan(0);
|
||||
expect(health.backupStatus().latest).not.toBeNull();
|
||||
});
|
||||
Reference in new issue
Block a user