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:
@@ -5,9 +5,9 @@ import {
|
||||
canonicalIdForFrequency,
|
||||
serializeCategory,
|
||||
serializeColorPalette,
|
||||
serializeGroup,
|
||||
serializeLicense,
|
||||
serializeProclamation,
|
||||
serializeSubscriptionLevel
|
||||
serializeProclamation
|
||||
} from './v2-serializers';
|
||||
|
||||
describe('canonicalIdForFrequency', () => {
|
||||
@@ -61,7 +61,7 @@ describe('serializeProclamation', () => {
|
||||
name: 'Peace & Relaxation',
|
||||
content: '<p>The text content, HTML formatted</p>',
|
||||
categoryId: 5,
|
||||
subscriptionLevelId: 1
|
||||
groupId: 1
|
||||
};
|
||||
|
||||
it('derives solfeggioFrequencyId from the Hz value, not the DB id', () => {
|
||||
@@ -84,13 +84,13 @@ describe('serializeProclamation', () => {
|
||||
expect(record).toBeNull();
|
||||
});
|
||||
|
||||
it('passes through a null subscriptionLevelId (kiosk treats as level 1)', () => {
|
||||
it('passes through categoryId and groupId unchanged', () => {
|
||||
const record = serializeProclamation({
|
||||
...base,
|
||||
subscriptionLevelId: null,
|
||||
solfeggioFrequency: solarPlexus528
|
||||
});
|
||||
expect(record?.subscriptionLevelId).toBeNull();
|
||||
expect(record?.categoryId).toBe(5);
|
||||
expect(record?.groupId).toBe(1);
|
||||
});
|
||||
|
||||
it('embeds a 7-color palette when provided', () => {
|
||||
@@ -116,64 +116,39 @@ describe('serializeProclamation', () => {
|
||||
});
|
||||
|
||||
describe('serializeCategory', () => {
|
||||
it('keeps parentId null for root categories', () => {
|
||||
expect(serializeCategory({ id: 4, name: 'Spiritual', sortOrder: 0, parentId: null })).toEqual({
|
||||
it('maps rows to a flat {id, name, sortOrder} with no parentId', () => {
|
||||
expect(serializeCategory({ id: 4, name: 'Spiritual', sortOrder: 0 })).toEqual({
|
||||
id: 4,
|
||||
name: 'Spiritual',
|
||||
sortOrder: 0,
|
||||
parentId: null
|
||||
sortOrder: 0
|
||||
});
|
||||
expect(
|
||||
Object.keys(serializeCategory({ id: 4, name: 'Spiritual', sortOrder: 0 }))
|
||||
).not.toContain('parentId');
|
||||
});
|
||||
});
|
||||
|
||||
it('keeps parentId int for children', () => {
|
||||
expect(serializeCategory({ id: 5, name: 'General', sortOrder: 0, parentId: 4 })).toEqual({
|
||||
id: 5,
|
||||
describe('serializeGroup', () => {
|
||||
it('maps rows to {id, name, sortOrder}', () => {
|
||||
expect(serializeGroup({ id: 1, name: 'General', sortOrder: 0 })).toEqual({
|
||||
id: 1,
|
||||
name: 'General',
|
||||
sortOrder: 0,
|
||||
parentId: 4
|
||||
sortOrder: 0
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('serializeLicense', () => {
|
||||
it('returns subscription_level as an object when the level row exists', () => {
|
||||
it('returns only key and email_address (levels removed)', () => {
|
||||
const result = serializeLicense({
|
||||
key: 'XXXX-XXXX-XXXX-XXXX',
|
||||
emailAddress: 'user@example.com',
|
||||
levelName: 'Advanced',
|
||||
level: { id: 2, name: 'Advanced', description: 'General + Specific' }
|
||||
emailAddress: 'user@example.com'
|
||||
});
|
||||
expect(result).toEqual({
|
||||
key: 'XXXX-XXXX-XXXX-XXXX',
|
||||
email_address: 'user@example.com',
|
||||
subscription_level: { id: 2, name: 'Advanced', description: 'General + Specific' }
|
||||
});
|
||||
});
|
||||
|
||||
it('falls back to a structurally consistent object when no level row matches', () => {
|
||||
const result = serializeLicense({
|
||||
key: 'XXXX-XXXX-XXXX-XXXX',
|
||||
emailAddress: 'user@example.com',
|
||||
levelName: 'SomeCustomLevel',
|
||||
level: null
|
||||
});
|
||||
expect(result).toEqual({
|
||||
key: 'XXXX-XXXX-XXXX-XXXX',
|
||||
email_address: 'user@example.com',
|
||||
subscription_level: { id: null, name: 'SomeCustomLevel', description: null }
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('serializeSubscriptionLevel', () => {
|
||||
it('maps rows to {id, name, description}', () => {
|
||||
expect(
|
||||
serializeSubscriptionLevel({ id: 1, name: 'Basic', description: 'General only' })
|
||||
).toEqual({
|
||||
id: 1,
|
||||
name: 'Basic',
|
||||
description: 'General only'
|
||||
email_address: 'user@example.com'
|
||||
});
|
||||
expect(Object.keys(result)).not.toContain('subscription_level');
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -55,8 +55,8 @@ export type V2Proclamation = {
|
||||
name: string;
|
||||
content: string;
|
||||
categoryId: number;
|
||||
groupId: number;
|
||||
solfeggioFrequencyId: number;
|
||||
subscriptionLevelId: number | null;
|
||||
colorPalette?: string[];
|
||||
};
|
||||
|
||||
@@ -65,7 +65,7 @@ export type ProclamationWithFrequency = {
|
||||
name: string;
|
||||
content: string;
|
||||
categoryId: number;
|
||||
subscriptionLevelId: number | null;
|
||||
groupId: number;
|
||||
solfeggioFrequency: { value: number };
|
||||
};
|
||||
|
||||
@@ -83,8 +83,8 @@ export function serializeProclamation(
|
||||
name: p.name,
|
||||
content: p.content,
|
||||
categoryId: p.categoryId,
|
||||
solfeggioFrequencyId,
|
||||
subscriptionLevelId: p.subscriptionLevelId
|
||||
groupId: p.groupId,
|
||||
solfeggioFrequencyId
|
||||
};
|
||||
|
||||
if (palette && palette.length > 0) {
|
||||
@@ -98,53 +98,32 @@ export type V2Category = {
|
||||
id: number;
|
||||
name: string;
|
||||
sortOrder: number;
|
||||
parentId: number | null;
|
||||
};
|
||||
|
||||
export function serializeCategory(c: {
|
||||
export function serializeCategory(c: { id: number; name: string; sortOrder: number }): V2Category {
|
||||
return { id: c.id, name: c.name, sortOrder: c.sortOrder };
|
||||
}
|
||||
|
||||
export type V2Group = {
|
||||
id: number;
|
||||
name: string;
|
||||
sortOrder: number;
|
||||
parentId: number | null;
|
||||
}): V2Category {
|
||||
return { id: c.id, name: c.name, sortOrder: c.sortOrder, parentId: c.parentId };
|
||||
}
|
||||
|
||||
export type V2SubscriptionLevel = {
|
||||
id: number;
|
||||
name: string;
|
||||
description: string | null;
|
||||
};
|
||||
|
||||
export function serializeSubscriptionLevel(level: {
|
||||
id: number;
|
||||
name: string;
|
||||
description: string | null;
|
||||
}): V2SubscriptionLevel {
|
||||
return { id: level.id, name: level.name, description: level.description };
|
||||
export function serializeGroup(g: { id: number; name: string; sortOrder: number }): V2Group {
|
||||
return { id: g.id, name: g.name, sortOrder: g.sortOrder };
|
||||
}
|
||||
|
||||
export type V2License = {
|
||||
key: string;
|
||||
email_address: string;
|
||||
subscription_level: { id: number | null; name: string; description: string | null };
|
||||
};
|
||||
|
||||
/** Build the POST /license response. `subscription_level` is always an object;
|
||||
* when no SubscriptionLevel row matches the license's level name, fall back to
|
||||
* a structurally consistent object with a null id/description. */
|
||||
export function serializeLicense(opts: {
|
||||
key: string;
|
||||
emailAddress: string;
|
||||
levelName: string;
|
||||
level: { id: number; name: string; description: string | null } | null;
|
||||
}): V2License {
|
||||
/** Build the POST /license response. */
|
||||
export function serializeLicense(opts: { key: string; emailAddress: string }): V2License {
|
||||
return {
|
||||
key: opts.key,
|
||||
email_address: opts.emailAddress,
|
||||
subscription_level: opts.level
|
||||
? { id: opts.level.id, name: opts.level.name, description: opts.level.description }
|
||||
: { id: null, name: opts.levelName, description: null }
|
||||
email_address: opts.emailAddress
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@ function signedRequest(opts: {
|
||||
const path = '/api/v2/automations/create/account';
|
||||
const timestamp = String(Math.floor((opts.now ?? new Date()).getTime() / 1000));
|
||||
const nonce = 'a48c0f2e-5f34-4b0b-9a12-3c7d6b8e1f00';
|
||||
const body = '{"email":"user@example.com","subscription_level":"Basic"}';
|
||||
const body = '{"email":"user@example.com"}';
|
||||
|
||||
let ts = timestamp;
|
||||
let nonceValue = nonce;
|
||||
|
||||
Reference in New Issue
Block a user