Remove stale admin overhaul and API update docs

This commit is contained in:
NeilClack
2026-09-10 16:19:52 -05:00
parent 10aad07e6d
commit 4d929df604
2 changed files with 0 additions and 515 deletions
-185
View File
@@ -1,185 +0,0 @@
# Admin Overhaul & Bug Fix Plan
Status: **IN PROGRESS** — checkboxes are ticked as work is completed and committed.
## Context
The admin section is mid-refactor. The generic `$lib/svelte-admin/*` CRUD layer and
`AdminTable.svelte` were deleted; the **Profiles** page was rebuilt by hand with
`DataTable.svelte`, `AdminModal.svelte`, and route-specific form component(s). The
remaining admin pages still import the deleted modules, so the app does not compile.
This plan:
1. Fixes the reported Profiles-page bugs (stale buttons, unified User/Profile identity,
in-modal status messages, centered modal with backdrop).
2. Rebuilds every other admin page to match the Profiles page look, style, and
functionality — KISS, no clever abstractions.
3. Removes the **Admin dashboard** — the nav links are the navigation.
4. Uses `DataTable` / `AdminModal` / `Toast` as dumb containers; each page owns its
own form logic.
Roles: `SUPER = 1`, `ADMIN = 2`, `STAFF = 3`, `USER = 4`. Admin area requires `roleId <= 3`.
## Decisions Locked In
- **Dashboard**: removed. `/admin` redirects to `/admin/profiles`.
- **Profile email is the identity.** The User dropdown is removed from the Profiles
form. One Email field:
- Linked profile (`userId` set): field shows/edits the User's email; saving
syncs the change to the `User` record.
- Unlinked profile: email lives on the Profile only.
- On save, if an existing `User` matches the entered email, the profile is linked
to it automatically.
- **Users page** is where Users are edited (Name, Email, Role) — not the Profiles page.
- **Status messages live inside modals**, at the top, not in a page-level toast.
- **API Keys**: pending operator review. Applied: compile fix (remove `$lib/svelte-admin/roles`
dependency) + consistent styling. Modal-based rotation flow is deferred (see appendix).
---
## Checklist
### Phase 0 — Documentation & scaffolding
- [x] Document this plan at `docs/ADMIN_OVERHAUL.md` (this file)
- [x] Remove Admin dashboard: `/admin` redirects to `/admin/profiles`
- [x] Delete `(protected)/admin/+page.svelte`
- [x] Rewrite `(protected)/admin/+page.server.ts` as a `redirect(307, '/admin/profiles')`
### Phase 1 — Shared components
- [x] Rewrite `src/lib/components/admin/AdminModal.svelte`
- [x] Fixed overlay, centered panel (`fixed inset-0 z-50`)
- [x] Dark translucent backdrop (`bg-slate-900/50`), click/`Esc` to close
- [x] Panel: `max-w-2xl`, `max-h-[90vh]`, scrollable body
- [x] Header: title, Delete (when `canDelete && recordId`), close `×`
- [x] Extend `src/lib/components/DataTable.svelte`
- [x] Optional `copyableFields` → copy button per matching cell (`stopPropagation`)
- [x] Optional `draggable` + `onReorder` → drag-handle reorder column
- [x] Restyle `src/lib/components/admin/Toast.svelte` (fixed, dark chip, countdown/dismiss)
### Phase 2 — Profiles page (reference implementation)
- [x] `+page.server.ts`
- [x] Remove user-options relation data / `updateUser` action
- [x] New schema: `email` (required), `firstName`, `lastName`, `phoneNumber`
- [x] Upsert: create links to matching `User` by email; update syncs linked `User.email`;
guard `P2002` with friendly message
- [x] `ProfileFields.svelte`
- [x] Single Email field (bound from `user.email ?? record.email`); remove User select
- [x] `CreateProfileModal.svelte`
- [x] Fix skip-first `lastForm` effect bug
- [x] In-modal success banner ("Profile created") then auto-close
- [x] In-modal error banner + field errors
- [x] `EditProfileModal.svelte`
- [x] Fix skip-first `lastForm` effect bug
- [x] Reset sub-state per record (addresses, toggles, errors) on record change
- [x] Remove the "Edit User" / "Save User" section
- [x] In-modal success/error banners; keep open on successful edit
- [x] Clear & close Add-Address form on success; append returned address live
- [x] `+page.svelte`
- [x] Email column shows `user.email ?? email`; remove `users` prop plumbing
- [x] `{#key editRecord?.id}` so the edit modal remounts per record
- [x] Remove page-level `Toast` for modal feedback
- [x] New spec: `profiles/+page.server.spec.ts`
- [x] Create links by matched email; unlinked stores email only
- [x] Update syncs linked `User.email`; rejects email owned by another user
- [x] Validation failures and `P2002` handling
### Phase 3 — Users page
- [x] `+page.server.ts` — load users + role options (role-filtered for non-super);
`upsert` name/email/roleId; `delete`; `P2002` guard
- [x] `+page.svelte` — `DataTable` (Name, Email, Role, Created) + edit `AdminModal`
(Name, Email, Role select, Delete)
- [x] Replace `UserForm.svelte` usage (or convert to inline form) — no `svelte-admin`
### Phase 4 — User Roles page
- [x] `+page.server.ts` — direct `prisma.userRole` load/upsert/delete (P2003 guard)
- [x] `+page.svelte` — `DataTable` (Id, Name) + modal
### Phase 5 — Solfeggio Frequencies page
- [x] `+page.server.ts` — direct `prisma.solfeggioFrequency` load/upsert/delete (P2003 guard)
- [x] `+page.svelte` — `DataTable` (Name, Value) + modal
### Phase 6 — Proclamation Categories page
- [x] `+page.server.ts` — direct `prisma.proclamationCategory` load/upsert/delete/reorder
(P2002/P2003 guards, maxSort for new rows)
- [x] `+page.svelte` — draggable `DataTable` + modal (Name), reorder via fetch + invalidateAll
### Phase 7 — Proclamation Groups page
- [x] `+page.server.ts` — direct `prisma.proclamationGroup` load/upsert/delete/reorder
(P2002/P2003 guards)
- [x] `+page.svelte` — draggable `DataTable` (Name, Sort) + modal
### Phase 8 — License Keys page
- [x] `+page.server.ts` — direct `prisma.licenseKey` load (with user) + `createKey` + `replace`
- [x] `+page.svelte` — `DataTable` with copyable Key column; New-by-email modal; row → Replace modal
- [x] In-modal success/error banners for both flows
### Phase 9 — Proclamations page
- [x] `+page.server.ts` — direct load (strip content for non-super) + upsert (Super only,
P2002) + delete (Super only, P2003); category/group/frequency option data
- [x] `+page.svelte` — `DataTable` (Name, Category, Group, Frequency) + modal with
TipTap content editor for Super users
- [x] Update `page.server.spec.ts` and `page.svelte.spec.ts` (drop `svelte-admin` mocks)
### Phase 10 — API Keys page (operator page)
- [x] `+page.server.ts` — remove `ROLES` import (`$lib/svelte-admin/roles` deleted); use literal
- [x] `+page.svelte` — consistent styling; keep single-page rotate flow (modal conversion deferred)
- [x] Document deferred modal conversion in appendix
### Phase 11 — Tests & validation
- [x] Update `license-keys/page.server.spec.ts` (drop `svelte-admin` mocks)
- [x] `npm run check`
- [x] `npm run lint` (pre-existing `no-explicit-any` spec debt remains — see Appendix C)
- [x] `npm run test`
- [x] `npm run build` (needs env vars exported during `vite build`)
- [x] Final commit; mark every checkbox complete
---
## Appendix A — Profiles bug root causes
1. **Skip-first `lastForm` bug.** Both modals guard against the initial `null` form
with `if (lastForm === null) { lastForm = form; return; }`, which also swallows the
_first real submit result_ — the first "Add Address" save creates the DB row but the
UI never reacts (no close, no toast, no appended row). Fix: `if (!form || form === lastForm) return;`.
2. **Stale modal state across records.** `{#if editRecord}` keeps the modal mounted
between different rows, while the `seeded` flag never resets — the previous row's
addresses/user data leak in, and `record` stays stale (the reported
"my name didn't update"). Fix: `{#key editRecord?.id}` remount + per-record reseed.
3. **User data is only a snapshot.** The modal rendered `record.user.name / email`
directly. With the unified email model this display goes away; the form fields are
controlled by local `$state` and the server keeps Profile + User in sync.
## Appendix B — API Keys decision note
The API Keys page is an operator page (key rotation), not a CRUD table. It does not use
`DataTable`. Recommended future change: present the rotation confirmation and the new
key in shared `AdminModal` panels with in-modal status. That conversion is **deferred**
pending review. What was done now: compile fix (replaced the deleted `ROLES` import)
and consistent styling (simple slate card layout, indigo action buttons, red emergency
button) so the page compiles and matches the site visually.
## Appendix C — Validation caveats
1. **`npm run check`** required renaming `src/routes/(protected)/admin/profiles/+page.server.spec.ts`
to `page.server.spec.ts`: SvelteKit reserves files whose names start with `+`. The `+`-prefix
broke `svelte-kit sync` and thus the whole `check`/`build` pipeline. Renamed.
2. **`npm run lint`** previously reported 142 `no-explicit-any` errors across 14 pre-existing spec
files (user/account, dashboard, license-keys, login/register/reset, etc.). A scoped override now
disables that rule for `**/*.spec.ts`, since test doubles commonly need `any`. All non-spec lint
errors were fixed; `npm run lint` is green.
3. **`npm run build`** needs `BETTER_AUTH_SECRET`/`BETTER_AUTH_URL`/`DATABASE_URL` exported in the
shell, because `src/lib/auth.ts` reads `$env/dynamic/private` (empty at `vite build` time) and
Better Auth rejects the default secret. Export the vars before building.
-330
View File
@@ -1,330 +0,0 @@
# API Updates — Category Flattening, Groups, Subscription Level Removal
**Release:** v2 schema change (migration `20260821000000_flat_categories_groups_drop_subscription_levels`)
**Status:** Breaking changes. Clients must be updated.
## Summary of changes
| Endpoint | Change |
| ----------------------------------------- | ------------------------------------------------------------------------------------ |
| `GET /api/v2/categories` | **Breaking**: `parentId` removed from every record. Categories are now a flat list. |
| `GET /api/v2/groups` | **New**: returns the group list (former sub-categories). |
| `GET /api/v2/proclamations` | **Breaking**: `groupId` added; `subscriptionLevelId` removed. |
| `GET /api/v2/subscription-levels` | **Removed**: subscription levels no longer exist. |
| `POST /api/v2/license` | **Breaking**: response no longer contains `subscription_level`. |
| `POST /api/v2/automations/create/account` | **Breaking**: `subscription_level` is no longer accepted or required in the payload. |
| `POST /api/v1/license` | **Breaking**: response no longer contains `subscription_level`. |
| `POST /api/v1/automations/create/account` | **Breaking**: `subscription_level` is no longer accepted or required in the payload. |
| `GET /api/v1/categories` | Payload implicitly changed: rows no longer contain `parentId`. |
### Data model mapping
- Former **parent** categories (`Spiritual`, `Physical`, `Mental`, `Emotional`, `Financial`,
`Social`, `Biblical`, ...) became the flat **Category** list.
- Former **child** sub-categories (`General`, `Specific`, `Precise`, `Prayers`, `Scripture`)
became **Groups**.
- A proclamation's former child category becomes its `group`; its former parent category becomes
its `category`. A proclamation may move between categories without changing its group.
- Proclamations that had no sub-category (e.g. under `Custom (Client Specific)`) were assigned to
the `General` group.
- Every proclamation now has exactly one category and one group.
---
## OpenAPI 3.0 specification
```yaml
openapi: 3.0.3
info:
title: Radiant Wave Portal API
version: 2.1.0
description: |
Changes in this release:
* ProclamationCategory hierarchy flattened; parentId removed everywhere.
* New ProclamationGroup resource replaces sub-categories as a movable filter.
* SubscriptionLevel resource and all level-based gating removed.
paths:
/api/v2/categories:
get:
summary: List categories (flat)
description: >
Returns all proclamation categories as a flat, ordered list.
BREAKING: the previous version returned a hierarchical list where each
record carried a nullable parentId linking children to parents. That
field no longer exists.
tags: [Categories]
responses:
'200':
description: Flat category list.
content:
application/json:
schema:
type: array
items:
$ref: '#/components/schemas/Category'
examples:
newResponse:
summary: New response
value:
- id: 4
name: Spiritual
sortOrder: 0
- id: 2
name: Physical
sortOrder: 1
oldResponseDeprecated:
summary: Old (removed) response shape
value:
- id: 5
name: General
sortOrder: 0
parentId: 4
- id: 9
name: Specific
sortOrder: 1
parentId: 4
/api/v2/groups:
get:
summary: List groups
description: >
NEW ENDPOINT. Returns all proclamation groups ordered by sortOrder.
Groups replace the former sub-categories and act as a movable filter:
a proclamation can be moved from one category to another while keeping
its group.
tags: [Groups]
responses:
'200':
description: Group list.
content:
application/json:
schema:
type: array
items:
$ref: '#/components/schemas/Group'
example:
- id: 1
name: General
sortOrder: 0
- id: 2
name: Specific
sortOrder: 1
/api/v2/proclamations:
get:
summary: List proclamations
description: >
Returns proclamations with their canonical frequency id and optional
color palette. BREAKING: each record now carries groupId (required,
always present) and no longer carries subscriptionLevelId. When the
optional entitlement headers are supplied, only the caller's entitled
proclamations are returned; there is no longer any per-level gating.
tags: [Proclamations]
parameters:
- in: header
name: x-license-key
required: false
schema: { type: string }
description: License key. Must be paired with x-email.
- in: header
name: x-email
required: false
schema: { type: string }
description: Account email. Must be paired with x-license-key.
responses:
'200':
description: Proclamation list.
content:
application/json:
schema:
type: array
items:
$ref: '#/components/schemas/Proclamation'
examples:
newResponse:
summary: New response
value:
- id: b40fdcb9-703c-420e-af32-a081964c6f08
name: Peace & Relaxation
content: <p>The text content</p>
categoryId: 4
groupId: 1
solfeggioFrequencyId: 5
colorPalette:
[
'#32CD32',
'#3CB371',
'#90EE90',
'#98FB98',
'#ADFFA9',
'#C8FFD2',
'#F0FFF0'
]
oldResponseDeprecated:
summary: Old (removed) response shape
value:
- id: b40fdcb9-703c-420e-af32-a081964c6f08
name: Peace & Relaxation
content: <p>The text content</p>
categoryId: 5
solfeggioFrequencyId: 5
subscriptionLevelId: 1
'400':
description: Entitlement headers incomplete or invalid.
/api/v2/subscription-levels:
get:
summary: REMOVED
deprecated: true
description: >
This endpoint was removed. Subscription levels no longer exist in the
data model; clients must stop calling it.
tags: [Removed]
responses:
'404':
description: Route no longer exists.
/api/v2/license:
post:
summary: Validate a license key
description: >
Validates a key/email pair. BREAKING: the response no longer contains a
subscription_level object.
tags: [License]
requestBody:
required: true
content:
application/json:
schema:
type: object
required: [license_key, email_address]
properties:
license_key: { type: string, example: ABCD-EFGH-IJKL-MNOP }
email_address: { type: string, format: email }
responses:
'200':
description: Key is valid.
content:
application/json:
schema:
$ref: '#/components/schemas/LicenseResponse'
examples:
newResponse:
summary: New response
value:
key: ABCD-EFGH-IJKL-MNOP
email_address: user@example.com
oldResponseDeprecated:
summary: Old (removed) response shape
value:
key: ABCD-EFGH-IJKL-MNOP
email_address: user@example.com
subscription_level:
id: 2
name: Advanced
description: General + Specific
'400':
description: Validation failure (missing fields, unknown key, wrong owner).
/api/v2/automations/create/account:
post:
summary: Create account + license key (GoHighLevel automation)
description: >
Creates a portal account when needed and issues a license key.
BREAKING: subscription_level was removed from the request contract. The
field is neither required nor validated anymore; sending it is ignored.
Signature headers (x-timestamp, x-nonce, x-signature) are unchanged.
tags: [Automations]
requestBody:
required: true
content:
application/json:
schema:
type: object
required: [email]
properties:
email: { type: string, format: email }
name: { type: string }
responses:
'200':
description: Account and license key created.
'400':
description: Missing/invalid email.
'401':
description: Invalid signature.
components:
schemas:
Category:
type: object
required: [id, name, sortOrder]
properties:
id: { type: integer }
name: { type: string }
sortOrder: { type: integer }
Group:
type: object
required: [id, name, sortOrder]
properties:
id: { type: integer }
name: { type: string }
sortOrder: { type: integer }
Proclamation:
type: object
required: [id, name, content, categoryId, groupId, solfeggioFrequencyId]
properties:
id:
type: string
format: uuid
name: { type: string }
content: { type: string, description: HTML formatted content. }
categoryId:
type: integer
description: Flat category this proclamation belongs to.
groupId:
type: integer
description: >
Group membership (movable filter). Independent of categoryId;
changing the category does not change the group.
solfeggioFrequencyId:
type: integer
minimum: 1
maximum: 9
description: Canonical kiosk frequency id (not the DB autoincrement id).
colorPalette:
type: array
items: { type: string }
minItems: 7
maxItems: 7
description: Present only when a palette exists for the frequency.
LicenseResponse:
type: object
required: [key, email_address]
properties:
key: { type: string }
email_address: { type: string, format: email }
```
---
## Client migration checklist
1. **Kiosk / API consumers**
- Stop reading `parentId`; treat `/api/v2/categories` as flat.
- Fetch `/api/v2/groups` once at startup for the filter list.
- Read `groupId` from each proclamation record for filtering.
- Remove all references to `subscriptionLevelId` and `/api/v2/subscription-levels`.
- Remove `subscription_level` handling from license validation responses.
2. **GoHighLevel automation**
- Remove `subscription_level` from the account-creation webhook payload
(optional but recommended; the field is now ignored).
3. **Admin portal**
- Manage categories under Admin → Proclamation Categories (flat list, drag to reorder).
- Manage groups under Admin → Proclamation Groups (drag to reorder).
- Proclamations now select one Category and one Group independently.