From 7a09b9f91c75105ead5fc7dd27b85aadf9c12ea9 Mon Sep 17 00:00:00 2001 From: root Date: Tue, 9 Jun 2026 23:50:10 +1000 Subject: [PATCH] feat(dross): settings endpoint (avatar/accent/persona/voiceMode) Co-Authored-By: Claude Sonnet 4.6 --- lib/api/index.js | 2 ++ lib/api/routes/dross.js | 23 +++++++++++++++++++++++ tests/routes/dross.test.js | 35 +++++++++++++++++++++++++++++++++++ 3 files changed, 60 insertions(+) create mode 100644 lib/api/routes/dross.js create mode 100644 tests/routes/dross.test.js diff --git a/lib/api/index.js b/lib/api/index.js index 88bc540..97f2a5c 100644 --- a/lib/api/index.js +++ b/lib/api/index.js @@ -38,6 +38,7 @@ import { router as storageRouter } from './routes/storage.js'; import { router as backupsRouter } from './routes/backups.js'; import { router as kuttRouter } from './routes/kutt.js'; import { router as themeRouter } from './routes/theme.js'; +import { router as drossRouter } from './routes/dross.js'; export function mountApi(app) { const api = Router(); @@ -73,6 +74,7 @@ export function mountApi(app) { api.use('/links', linksRouter); api.use('/kutt', kuttRouter); api.use('/theme', themeRouter); + api.use('/dross', drossRouter); api.use('/pending-changes', pendingChangesRouter); api.use('/audit', auditRouter); api.use('/search', searchRouter); diff --git a/lib/api/routes/dross.js b/lib/api/routes/dross.js new file mode 100644 index 0000000..09edb21 --- /dev/null +++ b/lib/api/routes/dross.js @@ -0,0 +1,23 @@ +import { Router } from 'express'; +import { z } from 'zod'; +import { validate } from '../validate.js'; +import { asyncWrap } from '../errors.js'; +import { requireOwner } from '../cap.js'; +import * as settings from '../../db/repos/app_settings.js'; + +const DEFAULT_SETTINGS = { avatar: 'soft-eye', accent: '#a86adf', persona: '', voiceMode: 'review' }; + +export const router = Router(); + +async function getCfg() { return { ...DEFAULT_SETTINGS, ...(await settings.get('dross', {})) }; } + +router.get('/settings', asyncWrap(async (_req, res) => res.json(await getCfg()))); + +const settingsBody = z.object({ + avatar: z.enum(['soft-eye', 'wisp', 'motes']), + accent: z.string().regex(/^#[0-9a-fA-F]{6}$/), + persona: z.string().max(8000), + voiceMode: z.enum(['review', 'handsfree', 'action']) +}); +router.put('/settings', requireOwner, validate({ body: settingsBody }), + asyncWrap(async (req, res) => res.json(await settings.set('dross', req.body)))); diff --git a/tests/routes/dross.test.js b/tests/routes/dross.test.js new file mode 100644 index 0000000..160b21f --- /dev/null +++ b/tests/routes/dross.test.js @@ -0,0 +1,35 @@ +import { describe, it, expect, beforeAll } from 'vitest'; +import request from 'supertest'; +import { createApp } from '../../server.js'; +import { resetDb } from '../helpers/db.js'; +import { migrateUp } from '../../lib/db/migrate.js'; + +let app; +const owner = { Authorization: 'Bearer test-token' }; +beforeAll(async () => { + await resetDb(); await migrateUp(); + process.env.OWNER_TOKEN = 'test-token'; + app = createApp(); +}); + +describe('dross settings', () => { + it('GET /api/dross/settings returns defaults', async () => { + const res = await request(app).get('/api/dross/settings').set(owner); + expect(res.status).toBe(200); + expect(res.body).toMatchObject({ avatar: 'soft-eye', accent: '#a86adf', persona: '', voiceMode: 'review' }); + }); + + it('PUT /api/dross/settings persists and round-trips', async () => { + const body = { avatar: 'wisp', accent: '#aa66ff', persona: 'Be terse.', voiceMode: 'handsfree' }; + const put = await request(app).put('/api/dross/settings').set(owner).send(body); + expect(put.status).toBe(200); + const get = await request(app).get('/api/dross/settings').set(owner); + expect(get.body).toMatchObject(body); + }); + + it('PUT rejects a bad avatar (400)', async () => { + const res = await request(app).put('/api/dross/settings').set(owner) + .send({ avatar: 'nope', accent: '#aa66ff', persona: '', voiceMode: 'review' }); + expect(res.status).toBe(400); + }); +});