# Supabase Admin Dashboard Implementation Plan

> **For agentic workers:** REQUIRED: Use superpowers:subagent-driven-development (if subagents available) or superpowers:executing-plans to implement this plan. Steps use checkbox (`- [ ]`) syntax for tracking.

**Goal:** Build an internal-only `apps/admin` operations console for production Supabase data with privileged token-gated access, safe row editing, curated analytics, and audited admin actions.

**Architecture:** Add a standalone React app in `apps/admin`, but keep privileged trust on the server by introducing admin-only API routes, short-lived admin sessions, and registry-driven table, dataset, and action definitions. Deliver the product in slices: access foundation first, then explorer and auditability, then operations home, analytics, and custom actions.

**Tech Stack:** React 19, Vite, React Router, TanStack Query, Tailwind, `@xtablo/ui`, Hono, Supabase, Zod, AG Grid, Recharts, Vitest, React Testing Library

---

## File Map

### New app

- Create: `apps/admin/package.json`
- Create: `apps/admin/tsconfig.json`
- Create: `apps/admin/vite.config.ts`
- Create: `apps/admin/wrangler.toml`
- Create: `apps/admin/index.html`
- Create: `apps/admin/worker/index.ts`
- Create: `apps/admin/src/main.tsx`
- Create: `apps/admin/src/App.tsx`
- Create: `apps/admin/src/routes.tsx`
- Create: `apps/admin/src/main.css`
- Create: `apps/admin/src/lib/api.ts`
- Create: `apps/admin/src/lib/adminSession.ts`
- Create: `apps/admin/src/hooks/useAdminSession.ts`
- Create: `apps/admin/src/hooks/useAdminTables.ts`
- Create: `apps/admin/src/hooks/useAdminDatasets.ts`
- Create: `apps/admin/src/hooks/useAdminActions.ts`
- Create: `apps/admin/src/components/AdminLayout.tsx`
- Create: `apps/admin/src/components/AdminNavigation.tsx`
- Create: `apps/admin/src/components/ProductionBadge.tsx`
- Create: `apps/admin/src/components/PrivilegedGate.tsx`
- Create: `apps/admin/src/components/data-explorer/AdminGrid.tsx`
- Create: `apps/admin/src/components/data-explorer/RowDetailDrawer.tsx`
- Create: `apps/admin/src/components/data-explorer/RowEditForm.tsx`
- Create: `apps/admin/src/components/analytics/ChartBuilder.tsx`
- Create: `apps/admin/src/components/analytics/SavedDashboardList.tsx`
- Create: `apps/admin/src/components/actions/ActionRunner.tsx`
- Create: `apps/admin/src/pages/OperationsHomePage.tsx`
- Create: `apps/admin/src/pages/DataExplorerPage.tsx`
- Create: `apps/admin/src/pages/AnalyticsStudioPage.tsx`
- Create: `apps/admin/src/pages/ActionCenterPage.tsx`
- Create: `apps/admin/src/registry/tables.ts`
- Create: `apps/admin/src/registry/datasets.ts`
- Create: `apps/admin/src/registry/actions.ts`
- Test: `apps/admin/src/routes.test.tsx`
- Test: `apps/admin/src/components/PrivilegedGate.test.tsx`
- Test: `apps/admin/src/pages/DataExplorerPage.test.tsx`
- Test: `apps/admin/src/pages/AnalyticsStudioPage.test.tsx`
- Test: `apps/admin/src/pages/ActionCenterPage.test.tsx`

### Shared types and docs

- Create: `packages/shared-types/src/admin.types.ts`
- Modify: `packages/shared-types/src/index.ts`
- Create: `docs/ADMIN_APP_ACCESS_SETUP.md`

### API and auth

- Create: `apps/api/src/helpers/adminTokens.ts`
- Create: `apps/api/src/helpers/adminAudit.ts`
- Create: `apps/api/src/helpers/adminRegistry.ts`
- Create: `apps/api/src/routers/adminAuth.ts`
- Create: `apps/api/src/routers/adminTables.ts`
- Create: `apps/api/src/routers/adminDatasets.ts`
- Create: `apps/api/src/routers/adminActions.ts`
- Create: `apps/api/src/routers/admin.ts`
- Modify: `apps/api/src/config.ts`
- Modify: `apps/api/src/index.ts`
- Modify: `apps/api/src/routers/index.ts`
- Modify: `apps/api/src/middlewares/middleware.ts`
- Test: `apps/api/src/__tests__/routes/adminAuth.test.ts`
- Test: `apps/api/src/__tests__/routes/adminTables.test.ts`
- Test: `apps/api/src/__tests__/routes/adminDatasets.test.ts`
- Test: `apps/api/src/__tests__/routes/adminActions.test.ts`
- Test: `apps/api/src/__tests__/middlewares/adminAuth.test.ts`

### Database

- Create: `supabase/migrations/20260424110000_create_admin_audit_log.sql`
- Create: `supabase/migrations/20260424111000_create_admin_dataset_views.sql`

### Workspace wiring

- Modify: `package.json`
- Modify: `turbo.json` only if new task inputs or outputs are needed

## Chunk 1: Access Foundation And App Scaffolding

### Task 1: Scaffold `apps/admin` and workspace wiring

**Files:**
- Create: `apps/admin/package.json`
- Create: `apps/admin/tsconfig.json`
- Create: `apps/admin/vite.config.ts`
- Create: `apps/admin/wrangler.toml`
- Create: `apps/admin/index.html`
- Create: `apps/admin/worker/index.ts`
- Create: `apps/admin/src/main.tsx`
- Create: `apps/admin/src/App.tsx`
- Create: `apps/admin/src/routes.tsx`
- Create: `apps/admin/src/main.css`
- Create: `apps/admin/src/routes.test.tsx`
- Modify: `package.json`

- [ ] **Step 1: Write the failing route smoke test**

```tsx
import { MemoryRouter } from "react-router-dom";
import { render, screen } from "@testing-library/react";
import AppRoutes from "./routes";

it("renders the privileged gate on the root route", () => {
  render(
    <MemoryRouter initialEntries={["/"]}>
      <AppRoutes />
    </MemoryRouter>
  );

  expect(screen.getByText(/admin access token/i)).toBeInTheDocument();
});
```

- [ ] **Step 2: Run the new app test and verify it fails**

Run: `pnpm --filter @xtablo/admin test -- src/routes.test.tsx`
Expected: FAIL because `@xtablo/admin` and `src/routes.tsx` do not exist yet

- [ ] **Step 3: Copy the standalone app structure from `apps/clients`**

Create a minimal app shell modeled on:

- `apps/clients/package.json`
- `apps/clients/vite.config.ts`
- `apps/clients/wrangler.toml`
- `apps/clients/src/main.tsx`
- `apps/clients/src/App.tsx`

Use:

- package name `@xtablo/admin`
- dev port `5176`
- Cloudflare worker name `xtablo-admin`
- private hostname route placeholders in `wrangler.toml`

- [ ] **Step 4: Add root workspace scripts**

Add scripts to `package.json`:

```json
{
  "dev:admin": "turbo dev --filter=@xtablo/admin",
  "deploy:admin": "turbo deploy --filter=@xtablo/admin"
}
```

- [ ] **Step 5: Add a minimal gate screen so the test passes**

Stub the root route to render:

```tsx
export function PrivilegedGatePlaceholder() {
  return <main>Admin access token required</main>;
}
```

- [ ] **Step 6: Re-run the route smoke test**

Run: `pnpm --filter @xtablo/admin test -- src/routes.test.tsx`
Expected: PASS

- [ ] **Step 7: Verify the app typechecks**

Run: `pnpm --filter @xtablo/admin typecheck`
Expected: PASS

- [ ] **Step 8: Commit the scaffold**

```bash
git add package.json apps/admin
git commit -m "feat(admin): scaffold internal admin app"
```

### Task 2: Add privileged token exchange and admin session verification in the API

**Files:**
- Create: `apps/api/src/helpers/adminTokens.ts`
- Create: `apps/api/src/routers/adminAuth.ts`
- Create: `apps/api/src/routers/admin.ts`
- Create: `apps/api/src/__tests__/routes/adminAuth.test.ts`
- Create: `apps/api/src/__tests__/middlewares/adminAuth.test.ts`
- Modify: `apps/api/src/config.ts`
- Modify: `apps/api/src/index.ts`
- Modify: `apps/api/src/routers/index.ts`
- Modify: `apps/api/src/middlewares/middleware.ts`
- Create: `packages/shared-types/src/admin.types.ts`
- Modify: `packages/shared-types/src/index.ts`

- [ ] **Step 1: Write failing tests for token exchange and protected-session rejection**

```ts
it("rejects requests without a valid privileged token", async () => {
  const res = await app.request("/api/v1/admin/auth/exchange", {
    method: "POST",
    body: JSON.stringify({ accessToken: "bad-token" }),
    headers: { "Content-Type": "application/json" },
  });

  expect(res.status).toBe(401);
});

it("rejects admin routes without an admin session", async () => {
  const res = await app.request("/api/v1/admin/tables/profiles");
  expect(res.status).toBe(401);
});
```

- [ ] **Step 2: Run the targeted API tests and verify failure**

Run: `pnpm --filter @xtablo/api test -- src/__tests__/routes/adminAuth.test.ts src/__tests__/middlewares/adminAuth.test.ts`
Expected: FAIL because the admin auth router and middleware do not exist

- [ ] **Step 3: Extend config for privileged token validation**

Add to `apps/api/src/config.ts`:

```ts
ADMIN_TOKEN_SIGNING_SECRET: string;
ADMIN_TOKEN_AUDIENCE: string;
ADMIN_APP_URL: string;
```

Load them from env or secrets and fail fast if missing outside test mode.

- [ ] **Step 4: Add admin token helper and session payload types**

In `apps/api/src/helpers/adminTokens.ts`, implement helpers shaped like:

```ts
export type AdminSessionClaims = {
  sub: string;
  role: "viewer" | "operator" | "superadmin";
  aud: string;
  exp: number;
};

export async function exchangePrivilegedToken(input: string, config: AppConfig) {
  // verify privileged token, then issue short-lived admin session
}

export async function verifyAdminSession(token: string, config: AppConfig) {
  // decode and validate admin session claims
}
```

- [ ] **Step 5: Add admin auth middleware**

Extend `apps/api/src/middlewares/middleware.ts` with a dedicated admin middleware that:

- reads `Authorization: Bearer <admin-session>`
- verifies session claims
- attaches `adminSession` to the Hono context
- never falls back to normal product auth

- [ ] **Step 6: Add `/api/v1/admin/auth/exchange` and `/api/v1/admin/auth/session`**

`adminAuth.ts` should expose:

- `POST /auth/exchange`
- `GET /auth/session`
- `POST /auth/logout`

Use `zod` request validation and structured error responses.

- [ ] **Step 7: Register the new admin router**

Mount it under `/api/v1/admin` without reusing the normal authenticated router chain.

- [ ] **Step 8: Add shared admin types**

Export common types like:

```ts
export type AdminRole = "viewer" | "operator" | "superadmin";

export type AdminSessionResponse = {
  sessionToken: string;
  expiresAt: string;
  role: AdminRole;
  operatorEmail: string;
};
```

- [ ] **Step 9: Re-run API tests**

Run: `pnpm --filter @xtablo/api test -- src/__tests__/routes/adminAuth.test.ts src/__tests__/middlewares/adminAuth.test.ts`
Expected: PASS

- [ ] **Step 10: Commit the access backend**

```bash
git add apps/api/src/config.ts apps/api/src/index.ts apps/api/src/routers/index.ts apps/api/src/middlewares/middleware.ts apps/api/src/helpers/adminTokens.ts apps/api/src/routers/adminAuth.ts apps/api/src/routers/admin.ts apps/api/src/__tests__/routes/adminAuth.test.ts apps/api/src/__tests__/middlewares/adminAuth.test.ts packages/shared-types/src/admin.types.ts packages/shared-types/src/index.ts
git commit -m "feat(admin): add privileged admin session exchange"
```

### Task 3: Build the privileged gate and admin session client in `apps/admin`

**Files:**
- Create: `apps/admin/src/lib/api.ts`
- Create: `apps/admin/src/lib/adminSession.ts`
- Create: `apps/admin/src/hooks/useAdminSession.ts`
- Create: `apps/admin/src/components/PrivilegedGate.tsx`
- Create: `apps/admin/src/components/PrivilegedGate.test.tsx`
- Modify: `apps/admin/src/routes.tsx`
- Modify: `apps/admin/src/main.tsx`

- [ ] **Step 1: Write the failing component test for exchanging a privileged token**

```tsx
it("exchanges a privileged token and enters the admin shell", async () => {
  render(<PrivilegedGate />);

  await userEvent.type(screen.getByLabelText(/access token/i), "valid-token");
  await userEvent.click(screen.getByRole("button", { name: /unlock admin/i }));

  expect(await screen.findByText(/operations home/i)).toBeInTheDocument();
});
```

- [ ] **Step 2: Run the test to confirm it fails**

Run: `pnpm --filter @xtablo/admin test -- src/components/PrivilegedGate.test.tsx`
Expected: FAIL because no session client or gate exists

- [ ] **Step 3: Create an admin-only API client**

Implement `apps/admin/src/lib/api.ts` with:

```ts
export const adminApi = buildApi(import.meta.env.VITE_API_URL);

adminApi.interceptors.request.use((config) => {
  const token = getAdminSessionToken();
  if (token) config.headers.Authorization = `Bearer ${token}`;
  return config;
});
```

- [ ] **Step 4: Create local admin session storage helpers**

In `apps/admin/src/lib/adminSession.ts`, keep the stored shape minimal:

```ts
type StoredAdminSession = {
  token: string;
  expiresAt: string;
  role: AdminRole;
  operatorEmail: string;
};
```

- [ ] **Step 5: Add `useAdminSession`**

The hook should:

- exchange privileged tokens
- fetch current session metadata
- clear expired sessions
- expose `isAuthenticated`, `role`, `operatorEmail`, `unlock`, and `logout`

- [ ] **Step 6: Build the gate screen**

`PrivilegedGate.tsx` should:

- explain that normal Xtablo login is not sufficient
- accept the special token
- handle loading and invalid-token states
- redirect authenticated operators into the shell

- [ ] **Step 7: Wire root routing through the gate**

Root route behavior:

- no admin session: gate
- active admin session: `AdminLayout`

- [ ] **Step 8: Re-run admin frontend tests and typecheck**

Run: `pnpm --filter @xtablo/admin test -- src/components/PrivilegedGate.test.tsx src/routes.test.tsx`
Expected: PASS

Run: `pnpm --filter @xtablo/admin typecheck`
Expected: PASS

- [ ] **Step 9: Commit the gate flow**

```bash
git add apps/admin/src/lib/api.ts apps/admin/src/lib/adminSession.ts apps/admin/src/hooks/useAdminSession.ts apps/admin/src/components/PrivilegedGate.tsx apps/admin/src/components/PrivilegedGate.test.tsx apps/admin/src/routes.tsx apps/admin/src/main.tsx
git commit -m "feat(admin): add privileged token gate"
```

## Chunk 2: Explorer, Guarded Mutations, And Auditability

### Task 4: Build the admin shell and visual system

**Files:**
- Create: `apps/admin/src/components/AdminLayout.tsx`
- Create: `apps/admin/src/components/AdminNavigation.tsx`
- Create: `apps/admin/src/components/ProductionBadge.tsx`
- Create: `apps/admin/src/pages/OperationsHomePage.tsx`
- Modify: `apps/admin/src/App.tsx`
- Modify: `apps/admin/src/routes.tsx`
- Modify: `apps/admin/src/main.css`
- Test: `apps/admin/src/components/AdminLayout.test.tsx`

- [ ] **Step 1: Write a failing shell test for navigation and production context**

```tsx
it("shows the production badge and admin sections", () => {
  render(<AdminLayout />);

  expect(screen.getByText(/production/i)).toBeInTheDocument();
  expect(screen.getByRole("link", { name: /data explorer/i })).toBeInTheDocument();
  expect(screen.getByRole("link", { name: /analytics studio/i })).toBeInTheDocument();
});
```

- [ ] **Step 2: Run the shell test and verify failure**

Run: `pnpm --filter @xtablo/admin test -- src/components/AdminLayout.test.tsx`
Expected: FAIL because the admin shell does not exist

- [ ] **Step 3: Build the navigation and shell**

The shell should include:

- top-level nav for `Operations Home`, `Data Explorer`, `Analytics Studio`, `Action Center`
- persistent operator identity
- prominent but tasteful production badge
- strong visual hierarchy using the design spec colors and spacing

- [ ] **Step 4: Replace the placeholder route target with the real shell**

Route structure:

```tsx
<Route element={<AdminLayout />}>
  <Route index element={<OperationsHomePage />} />
  <Route path="/explorer/:tableId?" element={<DataExplorerPage />} />
  <Route path="/analytics" element={<AnalyticsStudioPage />} />
  <Route path="/actions" element={<ActionCenterPage />} />
</Route>
```

- [ ] **Step 5: Re-run the shell test**

Run: `pnpm --filter @xtablo/admin test -- src/components/AdminLayout.test.tsx`
Expected: PASS

- [ ] **Step 6: Commit the shell**

```bash
git add apps/admin/src/components/AdminLayout.tsx apps/admin/src/components/AdminNavigation.tsx apps/admin/src/components/ProductionBadge.tsx apps/admin/src/pages/OperationsHomePage.tsx apps/admin/src/App.tsx apps/admin/src/routes.tsx apps/admin/src/main.css
git commit -m "feat(admin): add admin shell and visual system"
```

### Task 5: Add registry-driven table exploration and approved read endpoints

**Files:**
- Create: `apps/admin/src/registry/tables.ts`
- Create: `apps/admin/src/hooks/useAdminTables.ts`
- Create: `apps/admin/src/pages/DataExplorerPage.tsx`
- Create: `apps/admin/src/components/data-explorer/AdminGrid.tsx`
- Create: `apps/admin/src/components/data-explorer/RowDetailDrawer.tsx`
- Create: `apps/admin/src/pages/DataExplorerPage.test.tsx`
- Create: `apps/api/src/helpers/adminRegistry.ts`
- Create: `apps/api/src/routers/adminTables.ts`
- Create: `apps/api/src/__tests__/routes/adminTables.test.ts`

- [ ] **Step 1: Write failing API tests for approved-table reads**

```ts
it("returns table metadata for an approved table", async () => {
  const res = await authedAdminRequest("/api/v1/admin/tables/profiles/meta");
  expect(res.status).toBe(200);
});

it("rejects a table that is not in the registry", async () => {
  const res = await authedAdminRequest("/api/v1/admin/tables/secrets/rows");
  expect(res.status).toBe(404);
});
```

- [ ] **Step 2: Write a failing frontend test for switching tables**

```tsx
it("loads rows for the selected table", async () => {
  render(<DataExplorerPage />);

  await userEvent.click(screen.getByRole("button", { name: /profiles/i }));

  expect(await screen.findByText(/email/i)).toBeInTheDocument();
});
```

- [ ] **Step 3: Run API and frontend tests to verify failure**

Run: `pnpm --filter @xtablo/api test -- src/__tests__/routes/adminTables.test.ts`
Expected: FAIL

Run: `pnpm --filter @xtablo/admin test -- src/pages/DataExplorerPage.test.tsx`
Expected: FAIL

- [ ] **Step 4: Define the table registry**

Seed the registry with 3 to 5 core resources only:

```ts
export const adminTables = {
  profiles: {
    label: "Users",
    source: "profiles",
    editableColumns: ["first_name", "last_name", "plan"],
  },
  organizations: {
    label: "Organizations",
    source: "organizations",
    editableColumns: ["name"],
  },
  tablo_access: {
    label: "Tablo Access",
    source: "tablo_access",
    editableColumns: ["is_active", "is_admin"],
  },
};
```

- [ ] **Step 5: Add the admin tables API**

Endpoints:

- `GET /api/v1/admin/tables`
- `GET /api/v1/admin/tables/:tableId/meta`
- `GET /api/v1/admin/tables/:tableId/rows`
- `GET /api/v1/admin/tables/:tableId/rows/:rowId`

Keep all queries registry-driven. Do not allow arbitrary table names.

- [ ] **Step 6: Build the explorer UI**

Implement:

- left rail of table groups
- AG Grid-based row list
- row detail drawer with linked values and metadata
- saved filter state scoped per table

- [ ] **Step 7: Re-run table API and explorer tests**

Run: `pnpm --filter @xtablo/api test -- src/__tests__/routes/adminTables.test.ts`
Expected: PASS

Run: `pnpm --filter @xtablo/admin test -- src/pages/DataExplorerPage.test.tsx`
Expected: PASS

- [ ] **Step 8: Commit approved reads**

```bash
git add apps/admin/src/registry/tables.ts apps/admin/src/hooks/useAdminTables.ts apps/admin/src/pages/DataExplorerPage.tsx apps/admin/src/components/data-explorer/AdminGrid.tsx apps/admin/src/components/data-explorer/RowDetailDrawer.tsx apps/admin/src/pages/DataExplorerPage.test.tsx apps/api/src/helpers/adminRegistry.ts apps/api/src/routers/adminTables.ts apps/api/src/__tests__/routes/adminTables.test.ts
git commit -m "feat(admin): add registry-driven data explorer reads"
```

### Task 6: Add guarded row updates and audit logging

**Files:**
- Create: `supabase/migrations/20260424110000_create_admin_audit_log.sql`
- Create: `apps/api/src/helpers/adminAudit.ts`
- Modify: `apps/api/src/routers/adminTables.ts`
- Create: `apps/admin/src/components/data-explorer/RowEditForm.tsx`
- Modify: `apps/admin/src/components/data-explorer/RowDetailDrawer.tsx`
- Create: `apps/api/src/__tests__/routes/adminTableEdits.test.ts`
- Create: `apps/admin/src/components/data-explorer/RowEditForm.test.tsx`

- [ ] **Step 1: Write failing tests for guarded edits and audit emission**

```ts
it("writes an audit log entry for a successful update", async () => {
  const res = await authedOperatorRequest("/api/v1/admin/tables/profiles/rows/user-1", {
    method: "PATCH",
    body: JSON.stringify({ first_name: "Ada" }),
  });

  expect(res.status).toBe(200);
  expect(mockInsertAuditLog).toHaveBeenCalled();
});
```

```tsx
it("shows a diff preview before saving a sensitive record", async () => {
  render(<RowEditForm record={record} />);

  await userEvent.type(screen.getByLabelText(/plan/i), "team");
  await userEvent.click(screen.getByRole("button", { name: /review changes/i }));

  expect(screen.getByText(/before/i)).toBeInTheDocument();
  expect(screen.getByText(/after/i)).toBeInTheDocument();
});
```

- [ ] **Step 2: Run the edit tests and verify failure**

Run: `pnpm --filter @xtablo/api test -- src/__tests__/routes/adminTableEdits.test.ts`
Expected: FAIL

Run: `pnpm --filter @xtablo/admin test -- src/components/data-explorer/RowEditForm.test.tsx`
Expected: FAIL

- [ ] **Step 3: Add the audit log migration**

Create a migration like:

```sql
create table public.admin_audit_log (
  id bigserial primary key,
  operator_id text not null,
  operator_email text not null,
  role text not null,
  action text not null,
  target_type text not null,
  target_id text not null,
  before jsonb,
  after jsonb,
  created_at timestamptz not null default now()
);
```

- [ ] **Step 4: Implement audit helper**

In `apps/api/src/helpers/adminAudit.ts`:

```ts
export async function recordAdminAuditLog(args: {
  operatorId: string;
  operatorEmail: string;
  role: string;
  action: string;
  targetType: string;
  targetId: string;
  before?: unknown;
  after?: unknown;
}) {
  // insert into admin_audit_log
}
```

- [ ] **Step 5: Add PATCH support to `adminTables.ts`**

Rules:

- only registry-approved columns may change
- role-aware permissions enforce `viewer` vs `operator` vs `superadmin`
- sensitive fields return a diff payload before final confirmation
- every successful write records an audit entry

- [ ] **Step 6: Add the row edit form and diff preview**

Support typed editors for:

- text
- booleans
- enums
- nullable values
- timestamps rendered read-only

- [ ] **Step 7: Re-run edit tests**

Run: `pnpm --filter @xtablo/api test -- src/__tests__/routes/adminTableEdits.test.ts`
Expected: PASS

Run: `pnpm --filter @xtablo/admin test -- src/components/data-explorer/RowEditForm.test.tsx`
Expected: PASS

- [ ] **Step 8: Commit guarded edits**

```bash
git add supabase/migrations/20260424110000_create_admin_audit_log.sql apps/api/src/helpers/adminAudit.ts apps/api/src/routers/adminTables.ts apps/api/src/__tests__/routes/adminTableEdits.test.ts apps/admin/src/components/data-explorer/RowEditForm.tsx apps/admin/src/components/data-explorer/RowEditForm.test.tsx apps/admin/src/components/data-explorer/RowDetailDrawer.tsx
git commit -m "feat(admin): add guarded row edits with audit logging"
```

## Chunk 3: Operations Views, Analytics, Actions, And Rollout

### Task 7: Build `Operations Home` with curated operational cards

**Files:**
- Modify: `apps/admin/src/pages/OperationsHomePage.tsx`
- Create: `apps/api/src/routers/adminOverview.ts`
- Create: `apps/api/src/__tests__/routes/adminOverview.test.ts`
- Create: `apps/admin/src/pages/OperationsHomePage.test.tsx`
- Modify: `apps/api/src/routers/admin.ts`

- [ ] **Step 1: Write failing tests for overview cards**

```ts
it("returns overview sections for the operations home", async () => {
  const res = await authedAdminRequest("/api/v1/admin/overview");
  expect(res.status).toBe(200);
});
```

```tsx
it("renders anomaly and recent-signup cards", async () => {
  render(<OperationsHomePage />);
  expect(await screen.findByText(/recent signups/i)).toBeInTheDocument();
  expect(await screen.findByText(/operational anomalies/i)).toBeInTheDocument();
});
```

- [ ] **Step 2: Run the overview tests and confirm failure**

Run: `pnpm --filter @xtablo/api test -- src/__tests__/routes/adminOverview.test.ts`
Expected: FAIL

Run: `pnpm --filter @xtablo/admin test -- src/pages/OperationsHomePage.test.tsx`
Expected: FAIL

- [ ] **Step 3: Add the overview endpoint**

Return a small curated payload:

- recent signups
- recent organizations
- subscription exceptions
- pinned saved explorer views
- anomaly counts

- [ ] **Step 4: Build the home page cards**

Make the first screen useful even if analytics is unfinished.

- [ ] **Step 5: Re-run overview tests**

Run: `pnpm --filter @xtablo/api test -- src/__tests__/routes/adminOverview.test.ts`
Expected: PASS

Run: `pnpm --filter @xtablo/admin test -- src/pages/OperationsHomePage.test.tsx`
Expected: PASS

- [ ] **Step 6: Commit operations home**

```bash
git add apps/api/src/routers/adminOverview.ts apps/api/src/__tests__/routes/adminOverview.test.ts apps/api/src/routers/admin.ts apps/admin/src/pages/OperationsHomePage.tsx apps/admin/src/pages/OperationsHomePage.test.tsx
git commit -m "feat(admin): add operations home dashboard"
```

### Task 8: Add curated analytics datasets and dashboard building

**Files:**
- Create: `supabase/migrations/20260424111000_create_admin_dataset_views.sql`
- Create: `apps/api/src/routers/adminDatasets.ts`
- Create: `apps/api/src/__tests__/routes/adminDatasets.test.ts`
- Create: `apps/admin/src/registry/datasets.ts`
- Create: `apps/admin/src/hooks/useAdminDatasets.ts`
- Create: `apps/admin/src/pages/AnalyticsStudioPage.tsx`
- Create: `apps/admin/src/components/analytics/ChartBuilder.tsx`
- Create: `apps/admin/src/components/analytics/SavedDashboardList.tsx`
- Create: `apps/admin/src/pages/AnalyticsStudioPage.test.tsx`

- [ ] **Step 1: Write failing analytics dataset tests**

```ts
it("lists curated datasets only", async () => {
  const res = await authedAdminRequest("/api/v1/admin/datasets");
  expect(res.status).toBe(200);
  expect(await res.json()).toMatchObject({
    datasets: expect.arrayContaining([expect.objectContaining({ id: "signups" })]),
  });
});
```

```tsx
it("builds a chart from a curated dataset", async () => {
  render(<AnalyticsStudioPage />);

  await userEvent.selectOptions(screen.getByLabelText(/dataset/i), "signups");
  await userEvent.selectOptions(screen.getByLabelText(/metric/i), "count");

  expect(await screen.findByRole("img", { name: /signups chart/i })).toBeInTheDocument();
});
```

- [ ] **Step 2: Run the analytics tests and confirm failure**

Run: `pnpm --filter @xtablo/api test -- src/__tests__/routes/adminDatasets.test.ts`
Expected: FAIL

Run: `pnpm --filter @xtablo/admin test -- src/pages/AnalyticsStudioPage.test.tsx`
Expected: FAIL

- [ ] **Step 3: Create dataset views**

Back them with curated views such as:

- `admin_signups_daily`
- `admin_organizations_daily`
- `admin_subscription_exceptions`

- [ ] **Step 4: Add dataset registry and API**

Expose:

- `GET /api/v1/admin/datasets`
- `GET /api/v1/admin/datasets/:datasetId/query`

Only allow registry-defined dimensions, metrics, and grains.

- [ ] **Step 5: Build Analytics Studio**

Implement:

- dataset picker
- metric and dimension controls
- date-range filters
- chart type picker
- saved dashboard list

Use `recharts` and accessible titles for chart containers.

- [ ] **Step 6: Re-run analytics tests**

Run: `pnpm --filter @xtablo/api test -- src/__tests__/routes/adminDatasets.test.ts`
Expected: PASS

Run: `pnpm --filter @xtablo/admin test -- src/pages/AnalyticsStudioPage.test.tsx`
Expected: PASS

- [ ] **Step 7: Commit analytics**

```bash
git add supabase/migrations/20260424111000_create_admin_dataset_views.sql apps/api/src/routers/adminDatasets.ts apps/api/src/__tests__/routes/adminDatasets.test.ts apps/admin/src/registry/datasets.ts apps/admin/src/hooks/useAdminDatasets.ts apps/admin/src/pages/AnalyticsStudioPage.tsx apps/admin/src/components/analytics/ChartBuilder.tsx apps/admin/src/components/analytics/SavedDashboardList.tsx apps/admin/src/pages/AnalyticsStudioPage.test.tsx
git commit -m "feat(admin): add curated analytics studio"
```

### Task 9: Add `Action Center` and first custom admin workflows

**Files:**
- Create: `apps/admin/src/registry/actions.ts`
- Create: `apps/admin/src/hooks/useAdminActions.ts`
- Create: `apps/admin/src/pages/ActionCenterPage.tsx`
- Create: `apps/admin/src/components/actions/ActionRunner.tsx`
- Create: `apps/admin/src/pages/ActionCenterPage.test.tsx`
- Create: `apps/api/src/routers/adminActions.ts`
- Create: `apps/api/src/__tests__/routes/adminActions.test.ts`
- Modify: `apps/api/src/routers/admin.ts`

- [ ] **Step 1: Write failing tests for action discovery and execution**

```ts
it("executes a registry-defined admin action", async () => {
  const res = await authedSuperadminRequest("/api/v1/admin/actions/repair-membership", {
    method: "POST",
    body: JSON.stringify({ userId: "user-1", organizationId: "org-1" }),
  });

  expect(res.status).toBe(200);
});
```

```tsx
it("shows structured action results", async () => {
  render(<ActionCenterPage />);
  await userEvent.click(screen.getByRole("button", { name: /repair membership/i }));
  expect(await screen.findByText(/result/i)).toBeInTheDocument();
});
```

- [ ] **Step 2: Run the action tests and confirm failure**

Run: `pnpm --filter @xtablo/api test -- src/__tests__/routes/adminActions.test.ts`
Expected: FAIL

Run: `pnpm --filter @xtablo/admin test -- src/pages/ActionCenterPage.test.tsx`
Expected: FAIL

- [ ] **Step 3: Define the action registry**

Start with 2 to 4 actions only:

- `repair-membership`
- `resync-subscription`
- `merge-duplicate-profile`

Each entry must define:

- role requirement
- form schema
- confirmation copy
- handler route

- [ ] **Step 4: Add admin action endpoints**

Pattern:

```ts
adminActions.post("/:actionId", async (c) => {
  // load registry entry
  // validate input
  // execute handler
  // write audit log
  // return structured result
});
```

- [ ] **Step 5: Build the Action Center UI**

Include:

- action catalog
- permission-aware disabled states
- typed form rendering
- confirmation step
- structured success and partial-failure results

- [ ] **Step 6: Re-run action tests**

Run: `pnpm --filter @xtablo/api test -- src/__tests__/routes/adminActions.test.ts`
Expected: PASS

Run: `pnpm --filter @xtablo/admin test -- src/pages/ActionCenterPage.test.tsx`
Expected: PASS

- [ ] **Step 7: Commit custom actions**

```bash
git add apps/admin/src/registry/actions.ts apps/admin/src/hooks/useAdminActions.ts apps/admin/src/pages/ActionCenterPage.tsx apps/admin/src/components/actions/ActionRunner.tsx apps/admin/src/pages/ActionCenterPage.test.tsx apps/api/src/routers/adminActions.ts apps/api/src/__tests__/routes/adminActions.test.ts apps/api/src/routers/admin.ts
git commit -m "feat(admin): add audited action center workflows"
```

### Task 10: Deployment hardening, access docs, and final verification

**Files:**
- Create: `docs/ADMIN_APP_ACCESS_SETUP.md`
- Modify: `apps/admin/wrangler.toml`
- Modify: `apps/api/src/config.ts` if any env names changed during implementation

- [ ] **Step 1: Write the failing docs checklist**

Create a checklist in `docs/ADMIN_APP_ACCESS_SETUP.md` covering:

- private hostname or protected subdomain
- Cloudflare Access or VPN requirements
- privileged token issuance and rotation
- required env vars
- rollback path

- [ ] **Step 2: Add private-route deployment details**

Document placeholders like:

```toml
[env.production]
route = { pattern = "admin.internal.xtablo.com", custom_domain = true }
```

Also document that public DNS alone is not sufficient; edge access policy must be enabled.

- [ ] **Step 3: Run focused verification**

Run:

```bash
pnpm --filter @xtablo/api test -- src/__tests__/routes/adminAuth.test.ts src/__tests__/routes/adminTables.test.ts src/__tests__/routes/adminDatasets.test.ts src/__tests__/routes/adminActions.test.ts
pnpm --filter @xtablo/admin test -- src/routes.test.tsx src/components/PrivilegedGate.test.tsx src/pages/DataExplorerPage.test.tsx src/pages/AnalyticsStudioPage.test.tsx src/pages/ActionCenterPage.test.tsx
pnpm --filter @xtablo/api typecheck
pnpm --filter @xtablo/admin typecheck
```

Expected: all PASS

- [ ] **Step 4: Run broader workspace checks before merge**

Run:

```bash
pnpm lint
pnpm typecheck
```

Expected: PASS, or only pre-existing unrelated failures clearly identified

- [ ] **Step 5: Commit docs and hardening**

```bash
git add docs/ADMIN_APP_ACCESS_SETUP.md apps/admin/wrangler.toml apps/api/src/config.ts
git commit -m "docs(admin): add deployment and access runbook"
```

## Notes For Execution

- Do not bypass the privileged token model by reusing `apps/main/src/lib/supabase.ts` directly in the admin app.
- Keep the initial registry intentionally small; expand after the guarded end-to-end slice is working.
- Prefer API-mediated reads and writes for admin resources even where direct browser Supabase access would be faster to code.
- When a task touches both API and frontend, land the contract tests first, then the server, then the UI.
- If the private edge boundary cannot be fully provisioned in code, keep the repo changes ready and document the exact manual platform steps in `docs/ADMIN_APP_ACCESS_SETUP.md`.

Plan complete and saved to `docs/superpowers/plans/2026-04-24-supabase-admin-dashboard.md`. Ready to execute?
