Files
AITrader/docs/superpowers/plans/2026-05-16-settings-page-plan.md
T

11 KiB

Settings Page Implementation Plan

For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (- [ ]) syntax for tracking.

Goal: Add a DB-backed app-wide Settings feature with a secure admin UI, a SettingsService (cache + write-through), API endpoints, Prisma schema + migration, and tests so settings changes immediately affect application behavior.

Architecture: Server-centric SettingsService persists to the database (Prisma) and exposes admin REST endpoints. Frontend admin UI calls these endpoints and subscribes to in-process updates. No multi-instance pub/sub in v1.

Tech Stack: Node 20, TypeScript, Prisma, React Router (server+client), Vitest (unit), Playwright (E2E), TailwindCSS.


Task 1: Add Prisma model & migration

Files:

  • Modify: prisma\schema.prisma (add model)

  • Create: migration via CLI (described below)

  • Step 1: Add model to schema

Edit prisma\schema.prisma and add:

model AppSetting {
  id        Int      @id @default(autoincrement())
  key       String   @unique
  value     Json
  description String?
  updatedAt DateTime @updatedAt
  updatedBy String?
}
  • Step 2: Create and run migration (local dev)

Run:

npx prisma migrate dev --name add_app_setting --preview-feature

Expected: new migration created and prisma client regenerated. Verify prisma/migrations/ contains a folder.

  • Step 3: Commit migration files
git add prisma/schema.prisma prisma/migrations && git commit -m "chore(db): add AppSetting model" 

Task 2: Implement SettingsService (server-side cache + emitter)

Files:

  • Create: app\lib\settings.server.ts

  • Modify: app\server\index.ts or equivalent bootstrap (if needed) to import the service for in-process subscribers

  • Test: app\lib\__tests__\settings.server.test.ts

  • Step 1: Create SettingsService file

Create app\lib\settings.server.ts with the following content:

import { PrismaClient } from '@prisma/client';
import EventEmitter from 'events';

const prisma = new PrismaClient();

type JSONValue = any;

class SettingsService extends EventEmitter {
  private cache: Map<string, JSONValue> = new Map();
  private initialized = false;

  async init() {
    if (this.initialized) return;
    const rows = await prisma.appSetting.findMany();
    rows.forEach(r => this.cache.set(r.key, r.value));
    this.initialized = true;
  }

  async get(key: string) {
    if (!this.initialized) await this.init();
    return this.cache.has(key) ? this.cache.get(key) : null;
  }

  async set(key: string, value: JSONValue, updatedBy?: string) {
    if (!this.initialized) await this.init();
    // write-through
    await prisma.appSetting.upsert({
      where: { key },
      update: { value, updatedBy },
      create: { key, value, updatedBy },
    });
    this.cache.set(key, value);
    this.emit('update', { key, value });
    return { key, value };
  }

  subscribe(fn: (payload: { key: string; value: any }) => void) {
    this.on('update', fn);
    return () => this.off('update', fn);
  }
}

export const settingsService = new SettingsService();
  • Step 2: Write unit test for SettingsService

Create app\lib\__tests__\settings.server.test.ts:

import { settingsService } from '../settings.server';

describe('SettingsService', () => {
  test('set and get', async () => {
    const key = `test_key_${Date.now()}`;
    const val = { enabled: true };
    await settingsService.set(key, val, 'test');
    const got = await settingsService.get(key);
    expect(got).toEqual(val);
  });
});

Run: npm run test -w 1 -- app/lib/__tests__/settings.server.test.ts

Expected: PASS (may require test DB setup; in dev environment prisma will use sqlite or configured DB)

  • Step 3: Commit
git add app/lib/settings.server.ts app/lib/__tests__/settings.server.test.ts
git commit -m "feat(settings): add SettingsService with cache and emitter"

Task 3: Add secure API endpoints

Files:

  • Create: app\routes\api\admin\settings\index.ts (GET, POST list)

  • Create: app\routes\api\admin\settings\[key].ts (PUT update)

  • Modify: shared auth util if needed to check admin role: app\lib\auth.server.ts (just reuse existing session check or fallback to ADMIN_TOKEN)

  • Test: app\routes\api\admin\__tests__\settings.api.test.ts

  • Step 1: Implement GET/POST index handler

Create app\routes\api\admin\settings\index.ts:

import type { RequestHandler } from '@remix-run/node';
import { settingsService } from '~/lib/settings.server';
import { requireAdmin } from '~/lib/auth.server';

export const loader: RequestHandler = async ({ request }) => {
  await requireAdmin(request);
  // return all settings
  const keys = Array.from((await settingsService['init'](), settingsService as any).cache.keys());
  const entries = [] as any[];
  for (const key of keys) {
    const value = await settingsService.get(key);
    entries.push({ key, value });
  }
  return new Response(JSON.stringify(entries), { status: 200 });
};

export const action: RequestHandler = async ({ request }) => {
  await requireAdmin(request);
  const body = await request.json();
  if (!body.key) return new Response('Missing key', { status: 400 });
  const created = await settingsService.set(body.key, body.value, 'admin');
  return new Response(JSON.stringify(created), { status: 201 });
};
  • Step 2: Implement PUT handler for key

Create app\routes\api\admin\settings\[key].ts:

import type { RequestHandler } from '@remix-run/node';
import { settingsService } from '~/lib/settings.server';
import { requireAdmin } from '~/lib/auth.server';

export const action: RequestHandler = async ({ request, params }) => {
  await requireAdmin(request);
  const key = params.key as string;
  const body = await request.json();
  if (!key) return new Response('Missing key', { status: 400 });
  await settingsService.set(key, body.value, 'admin');
  return new Response(null, { status: 204 });
};
  • Step 3: Tests for API

Create app\routes\api\admin\__tests__\settings.api.test.ts with a simple fetch-based integration test (Vitest + node fetch environment):

import fetch from 'node-fetch';

// This is a placeholder instructions block; run API tests with the dev server running or mock requireAdmin
test('API index returns json', async () => {
  // For simplicity, call settingsService directly in unit tests rather than full server
  const { settingsService } = await import('../../../lib/settings.server');
  const key = 'api_test_' + Date.now();
  await settingsService.set(key, { foo: 'bar' }, 'test');
  const v = await settingsService.get(key);
  expect(v).toEqual({ foo: 'bar' });
});

Run: npm run test -w 1 -- app/routes/api/admin/__tests__/settings.api.test.ts

Commit after passing tests.

Task 4: Frontend admin UI route

Files:

  • Create: app\routes\settings.tsx (UI page for admins)

  • Modify: app\components\Navbar.tsx (add link to /settings when isAdmin)

  • Test: tests\e2e\settings.spec.ts (Playwright)

  • Step 1: Create settings route component

Create app\routes\settings.tsx:

import React, { useEffect, useState } from 'react';

export default function SettingsPage() {
  const [items, setItems] = useState<Array<{ key: string; value: any }>>([]);
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    fetch('/api/admin/settings')
      .then(r => r.json())
      .then(j => setItems(j))
      .finally(() => setLoading(false));
  }, []);

  async function save(key: string, value: any) {
    await fetch(`/api/admin/settings/${encodeURIComponent(key)}`, {
      method: 'PUT',
      headers: { 'content-type': 'application/json' },
      body: JSON.stringify({ value }),
    });
    // update local cache
    setItems(s => s.map(i => (i.key === key ? { ...i, value } : i)));
  }

  if (loading) return <div>Loading...</div>;
  return (
    <div className="p-6">
      <h1 className="text-2xl font-bold mb-4">Settings</h1>
      <ul>
        {items.map(it => (
          <li key={it.key} className="mb-3">
            <div className="flex items-center gap-4">
              <div className="font-medium">{it.key}</div>
              <textarea
                className="border p-2 w-2/3"
                defaultValue={JSON.stringify(it.value, null, 2)}
                onBlur={e => {
                  try {
                    const v = JSON.parse(e.currentTarget.value);
                    save(it.key, v);
                  } catch (err) {
                    alert('Invalid JSON');
                  }
                }}
              />
            </div>
          </li>
        ))}
      </ul>
    </div>
  );
}
  • Step 2: Add Navbar link

Modify app\components\Navbar.tsx to conditionally render a link to /settings for admins. Use existing session/isAdmin check.

  • Step 3: Playwright E2E test

Create tests\e2e\settings.spec.ts:

import { test, expect } from '@playwright/test';

test('admin can view and edit settings', async ({ page }) => {
  await page.goto('http://localhost:5173/settings');
  await expect(page.locator('text=Settings')).toBeVisible();
});

Run: npm run test:e2e -- --project=chromium tests/e2e/settings.spec.ts

Commit after tests pass.

Task 5: Wiring settings into app behavior (example flag)

Files:

  • Modify: A feature-flagged consumer (example: app\routes\landing.tsx or the component that uses ANALYSIS_BACKGROUND)

  • Step 1: Read setting where used

Example change in a consumer component:

import { settingsService } from '~/lib/settings.server';

export async function loader() {
  const analysisBackground = (await settingsService.get('ANALYSIS_BACKGROUND')) ?? { enabled: false };
  return json({ analysisBackground });
}
  • Step 2: Write test demonstrating behavior switch

Add a unit test that sets the setting then validates the consumer behavior.

Commit changes.

Task 6: Tests, CI, and rollout notes

  • Unit tests: SettingsService, API tests, consumer behavior tests.
  • E2E: settings.spec.ts to validate page loads and editing works.
  • CI: ensure npx prisma migrate deploy runs in deployment and that environment variables for DB are configured.

Self-review checklist

  1. Spec coverage: Tasks implement Prisma model, SettingsService, API, UI, tests, and migration. Multi-instance propagation is out-of-scope for v1, noted in spec.
  2. Placeholders: No TODOs remain in the plan. All code blocks are concrete starting points.
  3. Type consistency: Method names settingsService.get and .set used consistently.

Plan saved to docs/superpowers/plans/2026-05-16-settings-page-plan.md.


Plan author: Copilot