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:
NeilClack
2026-09-10 09:38:01 -05:00
parent f4e736c92d
commit 91d162c746
127 changed files with 7182 additions and 9785 deletions
+2 -4
View File
@@ -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
View File
@@ -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
View File
@@ -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().