feat: 2.14.0 — Eithan terminal toolbar, voice UX, Dross improvements framework

- Terminal renamed Eithan: mobile font A−/A+ (per-URL ttyd opts), same-origin
  xterm Copy/Paste buttons, scroll-to-live, touch-default 17px
- Dross voice: no keyboard pop after transcribe (fine-pointer only focus),
  autogrow textarea to ~5 lines, live amplitude meter on the mic while recording
- Dross improvements: propose_improvement tool (CSS layer, exfil-sanitized,
  owner-approved, per-improvement rollback/restore), public /improvements.css,
  Settings panel. External MCP registry unchanged (no tool leak).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
root
2026-06-11 23:35:32 +10:00
parent 859dedb668
commit 3bd8ea399c
18 changed files with 338 additions and 23 deletions

View File

@@ -40,6 +40,7 @@ import { router as kuttRouter } from './routes/kutt.js';
import { router as themeRouter } from './routes/theme.js';
import { router as drossRouter } from './routes/dross.js';
import { router as voiceRouter } from './routes/voice.js';
import { router as improvementsRouter, cssHandler } from './routes/improvements.js';
export function mountApi(app) {
const api = Router();
@@ -76,6 +77,7 @@ export function mountApi(app) {
api.use('/kutt', kuttRouter);
api.use('/theme', themeRouter);
api.use('/dross', drossRouter);
api.use('/improvements', improvementsRouter);
api.use('/voice', voiceRouter);
api.use('/pending-changes', pendingChangesRouter);
api.use('/audit', auditRouter);
@@ -92,6 +94,7 @@ export function mountApi(app) {
api.use((_req, _res, next) => next(new NotFoundError('route not found')));
api.use(errorMiddleware);
app.get('/improvements.css', cssHandler); // public, exfil-safe (see route file)
app.use('/api', api);
return api;
}

View File

@@ -0,0 +1,33 @@
import { Router } from 'express';
import { asyncWrap } from '../errors.js';
import { requireOwner } from '../cap.js';
import * as repo from '../../db/repos/improvements.js';
import { recordAudit } from '../../db/repos/audit.js';
export const router = Router();
router.get('/', asyncWrap(async (_req, res) => res.json(await repo.list())));
router.get('/:id', asyncWrap(async (req, res) => {
const row = await repo.get(req.params.id);
if (!row) return res.status(404).json({ error: 'not_found' });
res.json(row);
}));
for (const verb of ['approve', 'rollback', 'restore', 'reject']) {
router.post(`/:id/${verb}`, requireOwner, asyncWrap(async (req, res) => {
const row = await repo.transition(req.params.id, verb, 'owner');
if (!row) return res.status(409).json({ error: 'invalid_transition' });
const auditAction = { approve: 'approve', reject: 'reject', rollback: 'update', restore: 'update' }[verb];
await recordAudit({ kind: 'user' }, auditAction, 'improvement', row.id, null, { verb, summary: row.summary });
res.json(row);
}));
}
// Public stylesheet of ACTIVE improvements. Unauthenticated by design: it carries
// no secrets (owner-approved, exfil-sanitized CSS only) and <link> can't send a
// bearer token. Mounted on the app root, outside the /api auth wall.
export async function cssHandler(_req, res) {
res.set({ 'Content-Type': 'text/css; charset=utf-8', 'Cache-Control': 'no-cache' });
res.send(await repo.activeCss());
}