feat: in-app config editor (CodeMirror 6 workbench with live impact preview)#33
Merged
Conversation
…save-back) Design spec for the in-app config editor (Observatory workbench), answering FoxxMD's feedback that raw-wiki editing is too much friction for mods. v1: CodeMirror 6 editor in Devvit expanded mode, YAML-first (JSON5 too), schema autocomplete + hover + inline AJV validation, live rule-impact simulation, AI explainer, config diff, and save-back to the wiki via updateWikiPage. Monaco ruled out (Devvit webview CSP blocks runtime code evaluation and external client fetch). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
… TDD) Bite-sized TDD plan with real code for all 17 tasks across 5 phases: server endpoints (raw/validate/simulate-live/explain/save with validation gate + optimistic lock), CodeMirror 6 editor + schema hints, live Impact + AI Explain + Diff preview, and Devvit build-phase verifications. Reuses simulateRule, explainRule, parseConfig+AJV, configStore, ConfigDiffViewer. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Moves the private resolveOpenaiKey helper from forms.ts into src/lib/resolveOpenaiKey.ts so the upcoming config-editor explain endpoint can share the same Redis-first, settings-fallback key resolution logic without duplication. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Moves the fetchRecentPostsSafe + normalize loop out of the /simulate-rule-submit handler into src/core/recentSample.ts. Caches the result in Redis for 60 s so the future live-impact editor endpoint can reuse it without re-fetching Reddit on every debounced keystroke. Per-post normalize errors are logged and skipped (non-fatal). Cache read/write failures are also non-fatal so a Redis blip never breaks the simulation path. 4 unit tests cover the cache-hit, cache-miss, empty-listing, and cache-write-fail branches. All 96 route tests pass. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds config-editor Hono sub-app mounted at /api/config. The single /raw endpoint is mod-auth gated; it returns the wiki page content + revisionId on success, or the DEFAULT_CONFIG_YAML starter template (isDefaultTemplate:true) when the wiki page does not exist yet. DEFAULT_CONFIG_YAML added to default-config.ts as a hand-written YAML block semantically equivalent to DEFAULT_CONFIG_JSON5 and verifiably parseable via parseConfig. 3 new tests; 99/99 suite green; tsc + lint clean. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ample) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ndpoints Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Implements Task 7. Validation gate (parseConfig) runs before any wiki write so an invalid config can never reach the live moderation wiki. Optimistic lock re-reads the current wiki revisionId before writing and returns 409 if the page moved since the editor loaded, preventing silent concurrent-edit clobbers. On success: updateWikiPage, publish to Redis, best-effort stamp cfgLastWikiRev, logModActivity(edit-config). Added 'edit-config' to ModActivityKind union and the isValidModActivity allow- list. Test mock extended: updateWikiPage now a named vi.fn(); new vi.mock blocks for configStore and modActivity. 3 new tests (400/409/200) green. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…n/save) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Implements the ConfigEditor React component wired to app.schema.json for YAML/JSON autocompletion, lint markers, and hover docs via codemirror-json-schema. Adds server.deps.inline for codemirror-json-schema in vitest.config.ts to handle the package's extensionless ESM sub-imports that Node resolution cannot resolve. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ded mode) Task 11 integration capstone. Creates the ConfigWorkbench full-screen overlay (full-width CodeMirror, no PreviewPane yet), wires the Edit config button into ActionBar with best-effort requestExpandedMode, lazy- loads the workbench in App.tsx so CodeMirror stays out of the main entry chunk, and adds Escape key support. baseRev is refreshed from the wiki after every successful save to prevent false-conflict 409 on subsequent saves in the same session. requestExpandedMode is called via a dynamic import with an unknown cast because @devvit/client exports map has no types condition, making a static import a tsc error. Build chunks: ConfigWorkbench.js 810K (CM6 isolated), client.js 115K (main entry, pre-CM6 size preserved). 882 tests green, tsc clean, lint clean. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds PreviewPane component with three tabs: - Impact: debounced (700ms) simulateLiveSafe showing fire-rate vs recent items - Explain: on-demand explainConfigSafe with AI explanation - Diff: reuses exported simpleDiff from ConfigDiffViewer to show line-level changes between the loaded baseline and current edited text simpleDiff was already exported from ConfigDiffViewer.tsx (line 50). No duplication of LCS logic needed. "No changes." shows when no add/del entries exist (same-only diff = no unsaved edits). Tests (preview-pane.test.tsx): 7 tests covering tab rendering, aria-selected state, Explain button flow, loading placeholder, Diff changed-line detection, Diff no-changes state, Impact error path. All 198 client tests pass. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…k 14)
- Adds loadedText state: set in load() on initial fetch and after
save+reload, NOT on user edits. This gives PreviewPane's Diff tab a
stable baseline (currentText) that resets to '' post-save, so the Diff
is empty when the editor reflects the just-saved content.
- Editor now fills flex-1 on the left; PreviewPane takes flex-[0_0_40%]
on the right, with text={text} currentText={loadedText} props.
- config-workbench.test.tsx mock extended with simulateLiveSafe and
explainConfigSafe stubs so the test suite continues to pass with the
PreviewPane rendered inside the workbench.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…IA tabs Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Part A: add five demo-handler stubs to scripts/dev/mock-server.cjs for all config editor API paths (raw GET + validate/simulate-live/explain/save POSTs). Enables the editor workbench to open in ?demo=1 without a live Devvit install, unlocking E2E and screenshotting in CI. Part B: add tests/e2e/config-editor.spec.ts: light happy-path Playwright E2E covering Edit-config button, .cm-editor visible, three preview tabs present, and Impact tab fire-rate text rendered from the mock server. Part C: conflict-reload test was already present in tests/client/config-workbench.test.tsx (line 72-79, added in Task 11). No changes required there; 6/6 tests pass. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…the Impact preview
Previously /simulate-live delegated to simulateRule which wrapped the editor
text as a bare rule (rules: [${text}]), causing a parse error when the editor
contained a full runs: [...] config. Adds simulateFullConfig to simulateRule.ts
that mirrors dryRunActivity but accepts a caller-supplied AppConfig instead of
reading from configStore, using bypassIdempotency=true and dryRun=true so zero
real Reddit side-effects or idempotency Redis writes occur. The route now parses
the config text first and returns {ok:false, error:'config invalid...'} (200) for
mid-edit invalid text, then calls simulateFullConfig against the cached sample.
firedCount = samples where at least one run triggered at least one action. Tests
updated to mock simulateFullConfig instead of simulateRule and assert the invalid-
config short-circuit path does not invoke simulateFullConfig.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ire) Adds a 4-case regression test that proves: when simulateFullConfig triggers a rule (firedCount >= 1), all 10 Reddit mutation spies (remove, approve, submitComment, report, banUser, setUserFlair, post.lock, post.distinguish, comment.lock, comment.distinguish) remain uncalled. The config under test has dryRun absent to verify the forcing inside simulateFullConfig is responsible, not a config-level flag. Guards against future refactors that accidentally remove the bypassIdempotency early-return in runAction. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Adds an in-app config editor (the ContextMod Observatory workbench) so moderators load, edit, validate, and save their rule config without leaving the dashboard. Replaces the prior "click out to the raw wiki" workflow that FoxxMD flagged as the top friction point in his 2026-05-27 feedback.
What this gives a moderator
Click Edit config in the dashboard. The webview expands. A CodeMirror 6 editor loads the current wiki config with line numbers, YAML/JSON syntax coloring, schema-driven autocomplete + hover docs, and inline AJV validation. Right pane:
Save writes back to the wiki via
reddit.updateWikiPage, validation-gated (an invalid config can never reach the wiki) and optimistic-locked (refuses with a 409 on a concurrent edit), with the revision reason "Edited via ContextMod Observatory by u/...".Architecture
src/routes/configEditor.tsexposesGET /api/config/raw,POST /api/config/validate,POST /api/config/simulate-live,POST /api/config/explain,POST /api/config/save. All mod-auth gated, rate-limited where cost-bearing, structured 503 on transient failures.simulateFullConfiginsrc/core/simulateRule.tsruns the whole config per sample withbypassIdempotency: trueand forceddryRun: true. Zero real actions can fire (proven structurally and locked by a regression test).ConfigWorkbench(CodeMirror 6 +codemirror-json-schema) so the dashboard main entry stays at 118K with zero CodeMirror in it; the workbench chunk loads only when the editor opens.requestExpandedModeprovides the surface.descriptionfields added toapp.schema.jsonso the editor's hover docs are real, sourced from the JSDoc on the TypeScript rule/action/filter interfaces.Verification chain
runActionbefore the action switch, whenbypassIdempotency: true). Locked with a regression test that spies on all 10 Reddit mutation methods (remove,approve,submitComment,lock,report,banUser,setUserFlair,distinguish) and asserts none are called whilefiredCount >= 1./simulate-livereused the single-rulesimulateRule); fixed by routing the full config through the dry-run engine and locked with the safety test above.r/cm_devvit_test:Content-Security-Policy,unsafe-eval, orstyle-srcviolations from cm-devvit in the console.requestExpandedModeopens the editor in expanded mode.Follow-ups (out of scope for this PR)
Download is disallowed ... frame ... sandboxedin the playtest console: the Devvit webview iframe lacks theallow-downloadssandbox flag, so the pre-existing Export CSV button's programmatic download is blocked. Either requestallow-downloadsfrom Devvit, or replace Export CSV with a copy-to-clipboard fallback. Not introduced by this PR.Commits
19 atomic commits on this branch (
git log --oneline 9700f4d..HEAD), grouped by plan phase:Design spec:
docs/superpowers/specs/2026-05-27-config-editor-design.md.Implementation plan:
docs/superpowers/plans/2026-05-27-config-editor.md.🤖 Generated with Claude Code