Rebuild admin pages around DataTable/AdminModal; restructure categories/groups and v2 groups API
Remove the generic $lib/svelte-admin CRUD layer and AdminTable.svelte; each admin route now owns its own form/modal logic. Add DataTable.svelte, superform-errors.ts, and profile-specific modal/field components. Flatten proclamation categories/groups and drop subscription levels; add the v2 groups API; update v1/v2 endpoints, serializers, auth, and regenerated Prisma client with the new migrations. Document the overhaul plan in docs/ADMIN_OVERHAUL.md.
This commit is contained in:
@@ -0,0 +1,184 @@
|
||||
# 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`** still reports 142 `no-explicit-any` errors across 14 pre-existing spec files
|
||||
(user/account, dashboard, license-keys, login/register/reset, etc.). These predate this overhaul
|
||||
and are untouched here; fixing them is a separate cleanup.
|
||||
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.
|
||||
+2
-4
@@ -89,8 +89,8 @@ POST:
|
||||
Payload:
|
||||
|
||||
{
|
||||
"license_key": "...",
|
||||
"email_address": "..."
|
||||
"license_key": "...",
|
||||
"email_address": "..."
|
||||
}
|
||||
|
||||
Requires API key.
|
||||
@@ -153,7 +153,6 @@ Variables:
|
||||
- verifyUrl
|
||||
- loginUrl
|
||||
|
||||
|
||||
license-key-delivery
|
||||
|
||||
Variables:
|
||||
@@ -161,7 +160,6 @@ Variables:
|
||||
- firstName
|
||||
- license_key
|
||||
|
||||
|
||||
email-verification-email
|
||||
|
||||
Variables:
|
||||
|
||||
+13
-12
@@ -36,13 +36,13 @@ Always destructure $props() with defaults.
|
||||
Preferred pattern:
|
||||
|
||||
let {
|
||||
record = {} as Record<string, unknown>,
|
||||
fieldErrors = {} as Record<string, string>,
|
||||
isSuper = false
|
||||
record = {} as Record<string, unknown>,
|
||||
fieldErrors = {} as Record<string, string>,
|
||||
isSuper = false
|
||||
}: {
|
||||
record?: Record<string, unknown>;
|
||||
fieldErrors?: Record<string, string>;
|
||||
isSuper?: boolean;
|
||||
record?: Record<string, unknown>;
|
||||
fieldErrors?: Record<string, string>;
|
||||
isSuper?: boolean;
|
||||
} = $props();
|
||||
|
||||
Do not assume props exist.
|
||||
@@ -66,7 +66,7 @@ Correct:
|
||||
let name = $state('');
|
||||
|
||||
$effect(() => {
|
||||
name = String(record.name ?? '');
|
||||
name = String(record.name ?? '');
|
||||
});
|
||||
|
||||
Use $effect whenever component state needs to follow changing props.
|
||||
@@ -88,11 +88,11 @@ Example:
|
||||
let dirty = $state(false);
|
||||
|
||||
$effect(() => {
|
||||
dirty = false;
|
||||
dirty = false;
|
||||
});
|
||||
|
||||
$effect(() => {
|
||||
name = String(record.name ?? '');
|
||||
name = String(record.name ?? '');
|
||||
});
|
||||
|
||||
This ensures a new record starts clean.
|
||||
@@ -116,9 +116,10 @@ This can fail to update correctly.
|
||||
Use:
|
||||
|
||||
<select onchange={(e) => frequency = e.currentTarget.value}>
|
||||
<option value="1" selected={frequency === '1'}>
|
||||
One
|
||||
</option>
|
||||
|
||||
<option value="1" selected={frequency === '1'}>
|
||||
One
|
||||
</option>
|
||||
</select>
|
||||
|
||||
Use selected on options.
|
||||
|
||||
+29
-29
@@ -30,13 +30,13 @@ Tests are colocated with source files.
|
||||
|
||||
Client tests:
|
||||
|
||||
*.svelte.test.ts
|
||||
*.svelte.spec.ts
|
||||
_.svelte.test.ts
|
||||
_.svelte.spec.ts
|
||||
|
||||
Server tests:
|
||||
|
||||
*.test.ts
|
||||
*.spec.ts
|
||||
_.test.ts
|
||||
_.spec.ts
|
||||
|
||||
---
|
||||
|
||||
@@ -79,7 +79,7 @@ Node.
|
||||
Always reset mocks:
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
---
|
||||
@@ -109,15 +109,15 @@ Mock only the methods used.
|
||||
Example:
|
||||
|
||||
vi.mock('$lib/db', () => ({
|
||||
prisma: {
|
||||
proclamation: {
|
||||
findMany: vi.fn(),
|
||||
findUnique: vi.fn(),
|
||||
create: vi.fn(),
|
||||
update: vi.fn(),
|
||||
delete: vi.fn()
|
||||
}
|
||||
}
|
||||
prisma: {
|
||||
proclamation: {
|
||||
findMany: vi.fn(),
|
||||
findUnique: vi.fn(),
|
||||
create: vi.fn(),
|
||||
update: vi.fn(),
|
||||
delete: vi.fn()
|
||||
}
|
||||
}
|
||||
}));
|
||||
|
||||
Do not create massive unused mocks.
|
||||
@@ -143,7 +143,7 @@ createLocals(mockUser() as any)
|
||||
Specific roles:
|
||||
|
||||
createLocals(
|
||||
mockUser({ roleId: 4 }) as any
|
||||
mockUser({ roleId: 4 }) as any
|
||||
)
|
||||
|
||||
---
|
||||
@@ -172,15 +172,15 @@ Always use exact regex names.
|
||||
Preferred:
|
||||
|
||||
page.getByRole(
|
||||
'textbox',
|
||||
{ name: /^Name$/ }
|
||||
'textbox',
|
||||
{ name: /^Name$/ }
|
||||
)
|
||||
|
||||
Avoid:
|
||||
|
||||
page.getByRole(
|
||||
'textbox',
|
||||
{ name: 'Name' }
|
||||
'textbox',
|
||||
{ name: 'Name' }
|
||||
)
|
||||
|
||||
because substring matching can create strict-mode failures.
|
||||
@@ -192,15 +192,15 @@ because substring matching can create strict-mode failures.
|
||||
Because selects use selected options instead of bind:value:
|
||||
|
||||
const element =
|
||||
await page
|
||||
.getByRole(
|
||||
'combobox',
|
||||
{ name: /^Frequency$/ }
|
||||
)
|
||||
.element();
|
||||
await page
|
||||
.getByRole(
|
||||
'combobox',
|
||||
{ name: /^Frequency$/ }
|
||||
)
|
||||
.element();
|
||||
|
||||
expect(
|
||||
(element as HTMLSelectElement).value
|
||||
(element as HTMLSelectElement).value
|
||||
).toBe('2');
|
||||
|
||||
---
|
||||
@@ -213,9 +213,9 @@ Use:
|
||||
|
||||
vi.mock('$lib/components/example.svelte', () => ({
|
||||
default: ($anchor: Element) => {
|
||||
const div = document.createElement('div');
|
||||
$anchor.replaceWith(div);
|
||||
}
|
||||
const div = document.createElement('div');
|
||||
$anchor.replaceWith(div);
|
||||
}
|
||||
}));
|
||||
|
||||
Imports using mocked modules must come after vi.mock().
|
||||
|
||||
@@ -0,0 +1,330 @@
|
||||
# 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.
|
||||
Reference in New Issue
Block a user