Make subreddit list editable from the dashboard

Adds an inline subreddit editor on the dashboard that lets you add,
remove, and edit (tone, type, weight) the subreddit list, with changes
persisted to config.yaml on disk so they survive restarts.

Backend
- src/config/persistence.ts: round-trips config.yaml via
  YAML.parseDocument, mutating existing row nodes in place so their
  trailing inline `# ...` comments survive an edit; new rows are
  appended as flow-style maps to match the on-disk idiom; removed rows
  are dropped. Default values ('default' tone, 'image' type) are
  omitted on write so the file stays clean. Atomic via tmp+rename.
- src/web/routes/config.ts: GET /api/config now also returns the
  current subreddit list and the available tone keys. PUT /api/config
  accepts a `subreddits` field, validates with the existing
  SubredditConfigSchema, rejects duplicates, persists, then mutates
  ctx.config.reddit.subreddits in place so the processor picks up the
  new tone immediately without a restart.

Frontend
- public/index.html: replaces the read-only subreddit chip list in the
  Configuration snapshot section with a grid of editable rows (name,
  type, tone dropdown, weight, remove). + Add / Save buttons with
  inline status. Tones are sourced from /api/config so the catalog
  drives the dropdown.

Tests
- tests/config-persistence.test.ts: covers in-place mutation comment
  preservation, removal, appends, reorder, default omission, and
  atomic write cleanup.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
jjbruntonandClaude Opus 4.7 committed 2026-05-22 22:43:05 +01:00
1 parent 46a213e3e0
commit d9b9a79bdd
4 files changed
+476 -5

No files matched your search

+74
View File
@@ -0,0 +1,74 @@
import fs from 'fs';
import path from 'path';
import YAML from 'yaml';
import type { SubredditConfig } from './index.js';
/**
* Writes an updated subreddit list back to `config.yaml`, preserving the
* surrounding YAML structure, comment blocks, and per-row inline comments
* for entries that survive the update.
*
* Strategy:
* - Parse with `YAML.parseDocument` so the CST (including comments) is
* intact.
* - For each updated entry that matches an existing row by name, mutate
* the existing node's fields in place — its `.comment` (the trailing
* `# ...` annotation on the line) is preserved.
* - New entries become fresh flow-style maps appended in the caller's
* order; entries no longer in the list are dropped (and their comments
* go with them, since the comment is attached to the row).
* - Atomic write via tmp file + rename so a crash mid-write can't leave
* a half-written config on disk.
*/
export function persistSubreddits(
subreddits: SubredditConfig[],
configPath: string = path.resolve(process.cwd(), 'config.yaml'),
): void {
const raw = fs.readFileSync(configPath, 'utf-8');
const doc = YAML.parseDocument(raw);
const seq = doc.getIn(['reddit', 'subreddits']) as YAML.YAMLSeq | undefined;
if (!seq || !YAML.isSeq(seq)) {
throw new Error('reddit.subreddits is not a sequence in config.yaml');
}
// Map existing items by their `name` so we can reuse YAMLMap nodes
// (and the trailing comments attached to them).
const existingByName = new Map<string, YAML.YAMLMap>();
for (const item of seq.items) {
if (!YAML.isMap(item)) continue;
const name = item.get('name');
if (typeof name === 'string') existingByName.set(name, item);
}
const nextItems: YAML.YAMLMap[] = [];
for (const sub of subreddits) {
const existing = existingByName.get(sub.name);
const node = existing ?? new YAML.YAMLMap();
if (!existing) {
// Match the on-disk style: flow-mapped, single-quoted strings.
node.flow = true;
node.set('name', sub.name);
}
setOrDelete(node, 'type', sub.type === 'image' ? undefined : sub.type);
setOrDelete(node, 'weight', sub.weight);
setOrDelete(node, 'tone', sub.tone === 'default' ? undefined : sub.tone);
setOrDelete(node, 'scripted', sub.scripted);
nextItems.push(node);
}
seq.items = nextItems;
const out = doc.toString();
const tmp = configPath + '.tmp';
fs.writeFileSync(tmp, out, 'utf-8');
fs.renameSync(tmp, configPath);
}
function setOrDelete(node: YAML.YAMLMap, key: string, value: unknown): void {
if (value === undefined || value === null || value === '') {
if (node.has(key)) node.delete(key);
return;
}
node.set(key, value);
}