Files
Void-Homelab/lib/db/repos/improvements.js
root 3bd8ea399c 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>
2026-06-11 23:35:32 +10:00

59 lines
2.2 KiB
JavaScript

// Dross improvements — versioned CSS-layer changes with instant rollback.
import { pool } from '../pool.js';
const q = (text, params) => pool.query(text, params);
// Same exfil guards as elsewhere: an approved improvement still can't phone home
// or pull remote CSS. Pure visual tweaks only.
const BANNED = /url\s*\(|@import|@charset|expression\s*\(|behavior\s*:|javascript:/i;
const MAX_CSS = 20_000;
export function validateCss(css) {
if (typeof css !== 'string' || !css.trim()) return 'css required';
if (css.length > MAX_CSS) return `css too large (max ${MAX_CSS} chars)`;
if (BANNED.test(css)) return 'css may not use url()/@import/expression — visual tweaks only';
return null;
}
export async function create({ summary, css }) {
const { rows } = await q(
`INSERT INTO dross_improvements (summary, css) VALUES ($1, $2) RETURNING *`,
[String(summary).slice(0, 200), css]);
return rows[0];
}
export async function list() {
const { rows } = await q(
`SELECT id, summary, status, created_at, decided_at, length(css) AS css_len
FROM dross_improvements ORDER BY created_at DESC LIMIT 100`);
return rows;
}
export async function get(id) {
const { rows } = await q(`SELECT * FROM dross_improvements WHERE id = $1`, [id]);
return rows[0] ?? null;
}
// pending→active (approve) · active→rolled_back · rolled_back→active (restore) · pending→rejected
const TRANSITIONS = {
approve: { from: ['pending'], to: 'active' },
rollback: { from: ['active'], to: 'rolled_back' },
restore: { from: ['rolled_back'], to: 'active' },
reject: { from: ['pending'], to: 'rejected' },
};
export async function transition(id, verb, actor) {
const t = TRANSITIONS[verb];
if (!t) return null;
const { rows } = await q(
`UPDATE dross_improvements SET status = $1, decided_at = now(), decided_by = $2
WHERE id = $3 AND status = ANY($4) RETURNING *`,
[t.to, actor ?? 'owner', id, t.from]);
return rows[0] ?? null;
}
export async function activeCss() {
const { rows } = await q(
`SELECT summary, css FROM dross_improvements WHERE status = 'active' ORDER BY created_at`);
return rows.map((r) => `/* dross: ${r.summary.replace(/\*\//g, '')} */\n${r.css}`).join('\n\n');
}