Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
148 changes: 148 additions & 0 deletions .ai/plans/2026-05-28-cms-246-settings-page-design.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
# CMS-246 Settings Page Design 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:** Apply the new MDCMS Studio visual system to Settings while preserving the existing API key and capability contracts.

**Architecture:** Keep Settings as a Studio-runtime page and avoid backend contract changes. Add a small read-only schema summary query for the General tab using the existing schema route contract, and keep API key create/list/revoke through the current `useApiKeyList` flow.

**Tech Stack:** React 19, TanStack Query, Bun tests, Tailwind runtime styles, Lucide icons.

---

## Spec Delta

- No new spec behavior is required. The implementation uses existing spec-owned contracts:
- `SPEC-006` owns `/admin/settings` as an admin/settings-managed Studio route and capability-gated UI.
- `SPEC-005` owns API key metadata, create, one-time secret reveal, revoke, operation scopes, and context allowlists.
- `SPEC-004` owns schema sync as code-first/CLI-owned and supports read-only schema metadata.
- Affected UI/contract surface: Settings page only. Existing API calls remain `GET /api/v1/auth/api-keys`, `POST /api/v1/auth/api-keys`, `POST /api/v1/auth/api-keys/:id/revoke`, and `GET /api/v1/schema`.
- Acceptance criteria covered: visual redesign, read-only General context, API key lifecycle preservation, `capabilities.settings.manage` gate, loading/empty/error/mutation states, responsive layout, and Studio review check. `apps/studio-review` is not present in this branch.

## Files

- Modify: `packages/studio/src/lib/runtime-ui/app/admin/settings-page.tsx`
- Modify: `packages/studio/src/lib/runtime-ui/app/admin/settings-page.test.tsx`
- Modify: `packages/studio/src/lib/runtime-ui/components/api-key-create-dialog.tsx`
- Modify: `apps/docs/guide/studio/settings.mdx`
- Create: no new source files unless the Settings page becomes too large during refactor.

## Task 1: Failing Settings Coverage

- [x] **Step 1: Add tests for General read-only design**

Add tests in `packages/studio/src/lib/runtime-ui/app/admin/settings-page.test.tsx` that assert:

```ts
assert.match(markup, /data-mdcms-settings-subnav/);
assert.match(markup, /Schema hash/);
assert.match(markup, /Last schema sync/);
assert.match(markup, /mdcms schema sync/);
assert.doesNotMatch(markup, /Save changes/);
```

- [x] **Step 2: Add tests for API key ready state and revoke controls**

Add a pure view test or hook-friendly render test that supplies one `ApiKeyMetadata` and asserts the API keys tab renders `keyPrefix`, operation scopes, context allowlist, computed `Active` status, and a `Revoke` action.

- [x] **Step 3: Add tests for create dialog lifecycle helpers**

Export internal helper functions from `api-key-create-dialog.tsx` and test:

```ts
const input = buildApiKeyCreateInput({
label: " CI ",
selectedScopes: new Set(["content:read"]),
expiresAt: "2026-06-01",
project: "marketing-site",
environment: "production",
});
assert.equal(input.label, "CI");
assert.deepEqual(input.contextAllowlist, [
{ project: "marketing-site", environment: "production" },
]);
```

Also verify a successful reducer transition reaches `step: "created"` with the one-time `key` present.

- [x] **Step 4: Verify red**

Run:

```bash
bun test --cwd packages/studio ./src/lib/runtime-ui/app/admin/settings-page.test.tsx ./src/lib/runtime-ui/components/api-key-create-dialog.test.ts
```

Expected: new tests fail because schema summary, view helper, and dialog helpers are not implemented yet.

## Task 2: Settings Page Implementation

- [x] **Step 1: Add read-only schema summary query**

Use `createStudioSchemaRouteApi` and `useQuery` in `settings-page.tsx` to fetch `GET /api/v1/schema` only when `project`, `environment`, and `apiBaseUrl` are present. Derive `schemaHash` from `response.schemaHash` first, then from consistent entry hashes if needed. Derive `syncedAt` from consistent entry timestamps.

- [x] **Step 2: Redesign Settings shell**

Use an offwhite page canvas, responsive two-column layout (`lg:grid-cols-[220px_1fr]`), left sub-nav with General and API keys, flat 8px surfaces, 1px hairline borders, cobalt primary actions, and mono metadata rows. The mobile layout stacks the sub-nav above content.

- [x] **Step 3: Redesign General**

Render read-only rows for project, environment, server URL, schema hash, and synced timestamp. Include a terse CLI hint for `mdcms schema sync`. Show loading and error treatments for schema metadata without adding write controls.

- [x] **Step 4: Redesign API keys**

Keep existing `useApiKeyList`, create dialog, one-time reveal, and revoke calls. Restyle loading, empty, error, ready, and mutation states. Keep table horizontally scrollable and avoid text overlap by using `break-all`, `min-w`, and responsive grid/card wrappers where needed.

- [x] **Step 5: Verify green**

Run:

```bash
bun test --cwd packages/studio ./src/lib/runtime-ui/app/admin/settings-page.test.tsx ./src/lib/runtime-ui/components/api-key-create-dialog.test.ts
```

Expected: tests pass.

## Task 3: Docs And Review App Check

- [x] **Step 1: Update Settings docs**

Update `apps/docs/guide/studio/settings.mdx` so it describes the current Settings page as General + API Keys, keeps schema/config editing CLI-owned, and leaves post-MVP Webhooks/Media as planned only.

- [x] **Step 2: Check Studio review**

Run:

```bash
test -d apps/studio-review
```

Expected in this branch: command exits non-zero because the app is absent. Document this in the final summary instead of inventing fixtures.

## Task 4: Verification

- [x] **Step 1: Run targeted Studio tests**

Run:

```bash
bun test --cwd packages/studio ./src/lib/runtime-ui/app/admin/settings-page.test.tsx ./src/lib/runtime-ui/components/api-key-create-dialog.test.ts ./src/lib/api-keys-api.test.ts
```

- [x] **Step 2: Run package check**

Run:

```bash
bun run check
```

- [x] **Step 3: Run unit suite if feasible**

Run:

```bash
bun run unit
```

Result: failed in an unrelated CLI loopback callback test with `Failed to start loopback callback listener` / `Failed to start server. Is port 0 in use?`. The clean branch had already shown the same port-0 listener failure mode in `apps/server/src/lib/auth.test.ts`; the changed Studio tests pass independently.
18 changes: 17 additions & 1 deletion apps/docs/guide/studio/settings.mdx
Original file line number Diff line number Diff line change
@@ -1,13 +1,29 @@
---
title: "Settings"
description: "API keys, users, webhooks, and Studio configuration"
description: "Read-only project context and API key management"
---

<Warning>
Settings access requires the **admin** or **owner** role. Users without these
roles see an "Access denied" message.
</Warning>

## General

The **General** tab shows read-only context for the active Studio target:

| Field | Description |
| -------------------- | ---------------------------------------------------------- |
| **Project** | The current project identifier |
| **Environment** | The current environment identifier |
| **Server URL** | The backend URL Studio is using for API requests |
| **Schema hash** | The latest synced schema hash returned by the schema API |
| **Last schema sync** | The timestamp returned by the latest schema registry state |

The schema and project configuration remain code-first. To change content type
definitions or sync schema metadata, update `mdcms.config.ts` in the host
project and run `mdcms schema sync` from the CLI.

## API Keys

API keys provide programmatic access to the MDCMS API for external integrations, CI/CD pipelines, and scripts. Manage them at **Settings > API Keys**.
Expand Down
104 changes: 104 additions & 0 deletions packages/studio/src/lib/runtime-ui/app/admin/settings-page.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,12 @@ import {
} from "./capabilities-context.js";
import { StudioMountInfoProvider } from "./mount-info-context.js";
import SettingsPage from "./settings-page.js";
import {
SettingsPageView,
type SettingsPageApiKeysState,
type SettingsPageSchemaSummaryState,
} from "./settings-page.js";
import type { ApiKeyMetadata } from "../../../api-keys-api.js";

function renderSettingsPage(input: {
initialTab: string;
Expand Down Expand Up @@ -86,6 +92,7 @@ test("SettingsPage renders the API keys tab header and create button", () => {

assert.match(markup, /Create API Key/);
assert.match(markup, /Manage API keys for external integrations/);
assert.match(markup, /data-mdcms-settings-subnav/);
});

test("SettingsPage does not render a Schema tab", () => {
Expand Down Expand Up @@ -130,5 +137,102 @@ test("SettingsPage General tab shows read-only project context", () => {
capabilities: { canManageSettings: true },
});
assert.match(markup, /read-only/i);
assert.match(markup, /Schema hash/);
assert.match(markup, /Last schema sync/);
assert.match(markup, /mdcms schema sync/);
assert.doesNotMatch(markup, /Save changes/);
});

const readySchemaSummary: SettingsPageSchemaSummaryState = {
status: "ready",
schemaHash: "server-hash",
syncedAt: "2026-03-31T12:00:00.000Z",
};

const readyKey: ApiKeyMetadata = {
id: "key-1",
label: "CI deploy",
keyPrefix: "mdcms_key_abc123",
scopes: ["content:read", "content:publish", "schema:read"],
contextAllowlist: [{ project: "test-project", environment: "production" }],
createdByUserId: "user-1",
createdAt: "2026-03-01T00:00:00.000Z",
expiresAt: null,
revokedAt: null,
lastUsedAt: null,
};

function renderSettingsPageView(input: {
initialTab: string;
apiKeysState?: Partial<SettingsPageApiKeysState>;
schemaSummary?: SettingsPageSchemaSummaryState;
canManageSettings?: boolean;
}): string {
return renderToStaticMarkup(
createElement(
ThemeProvider,
null,
createElement(SettingsPageView, {
activeTab: input.initialTab,
setActiveTab: () => {},
canManageSettings: input.canManageSettings ?? true,
mountInfo: {
project: "test-project",
environment: "production",
apiBaseUrl: "https://api.example.com",
},
schemaSummary: input.schemaSummary ?? readySchemaSummary,
apiKeysState: {
status: "ready",
keys: [readyKey],
isRevoking: false,
revokeError: null,
onRevoke: () => {},
...input.apiKeysState,
},
createDialogOpen: false,
setCreateDialogOpen: () => {},
createKey: async () => ({
...readyKey,
key: "mdcms_key_secret",
}),
isCreating: false,
createError: null,
}),
),
);
}

test("SettingsPageView renders schema summary metadata from the existing schema contract", () => {
const markup = renderSettingsPageView({ initialTab: "general" });

assert.match(markup, /data-mdcms-settings-general-state="ready"/);
assert.match(markup, /server-hash/);
assert.match(markup, /2026-03-31T12:00:00.000Z/);
assert.match(markup, /mdcms schema sync/);
});

test("SettingsPageView renders API key metadata and revoke affordance", () => {
const markup = renderSettingsPageView({ initialTab: "api-keys" });

assert.match(markup, /data-mdcms-settings-api-keys-state="ready"/);
assert.match(markup, /CI deploy/);
assert.match(markup, /mdcms_key_abc123/);
assert.match(markup, /content:read/);
assert.match(markup, /schema:read/);
assert.match(markup, /test-project/);
assert.match(markup, /production/);
assert.match(markup, /Active/);
assert.match(markup, /Revoke/);
});

test("SettingsPageView keeps forbidden state capability-gated", () => {
const markup = renderSettingsPageView({
initialTab: "api-keys",
canManageSettings: false,
});

assert.match(markup, /data-mdcms-settings-state="forbidden"/);
assert.match(markup, /Access denied/);
assert.doesNotMatch(markup, /Create API Key/);
});
Loading
Loading