- 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>
59 lines
2.2 KiB
JavaScript
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');
|
|
}
|