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
@@ -0,0 +1,115 @@
-- Flatten ProclamationCategory hierarchy into a flat list and introduce
-- ProclamationGroup (formerly child sub-categories).
--
-- This migration is intentionally NON-DESTRUCTIVE:
-- * ProclamationGroup / Proclamation.groupId are added additively.
-- * Former child categories are ARCHIVED to ProclamationCategoryLegacy
-- before the hierarchy (parentId) is removed, so no category data is lost.
-- * Subscription-level resources (SubscriptionLevel,
-- ProclamationCategorySubscriptionLevel, Proclamation.subscriptionLevelId,
-- LicenseKey.level) are LEFT IN PLACE as unmanaged legacy data. They are no
-- longer modeled by the Prisma schema and are ignored by the application,
-- but their data is preserved and no DROP is attempted on tables the deploy
-- role may not own.
-- CreateTable
CREATE TABLE "ProclamationGroup" (
"id" SERIAL NOT NULL,
"name" TEXT NOT NULL,
"sortOrder" INTEGER NOT NULL DEFAULT 0,
CONSTRAINT "ProclamationGroup_pkey" PRIMARY KEY ("id")
);
-- CreateIndex
CREATE UNIQUE INDEX "ProclamationGroup_name_key" ON "ProclamationGroup"("name");
-- Seed groups from every distinct former sub-category name
INSERT INTO "ProclamationGroup" ("name", "sortOrder")
SELECT "name",
ROW_NUMBER() OVER (ORDER BY "name") - 1
FROM (
SELECT DISTINCT "name" FROM "ProclamationCategory" WHERE "parentId" IS NOT NULL
) AS d;
-- AddColumn (nullable during backfill)
ALTER TABLE "Proclamation" ADD COLUMN "groupId" INTEGER;
-- Backfill: former child categories become the proclamation's group; the
-- parent becomes its flat category.
UPDATE "Proclamation" AS p
SET "groupId" = g."id",
"categoryId" = c."parentId"
FROM "ProclamationCategory" AS c
JOIN "ProclamationGroup" AS g ON g."name" = c."name"
WHERE p."categoryId" = c."id" AND c."parentId" IS NOT NULL;
-- Backfill: proclamations that had no group (e.g. 'Custom (Client Specific)')
-- default to the General group, if it exists.
UPDATE "Proclamation"
SET "groupId" = (SELECT "id" FROM "ProclamationGroup" WHERE "name" = 'General')
WHERE "groupId" IS NULL;
-- Safety net: any proclamation still unmapped takes the first group.
UPDATE "Proclamation"
SET "groupId" = (SELECT "id" FROM "ProclamationGroup" ORDER BY "sortOrder" LIMIT 1)
WHERE "groupId" IS NULL;
ALTER TABLE "Proclamation" ALTER COLUMN "groupId" SET NOT NULL;
-- AddForeignKey
ALTER TABLE "Proclamation" ADD CONSTRAINT "Proclamation_groupId_fkey" FOREIGN KEY ("groupId") REFERENCES "ProclamationGroup"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- Archive former child categories before removing them, so their data is
-- preserved and recoverable. The legacy table also records each child's
-- parent for audit/recovery.
CREATE TABLE "ProclamationCategoryLegacy" (
"id" INTEGER NOT NULL,
"name" TEXT NOT NULL,
"parentId" INTEGER,
"sortOrder" INTEGER NOT NULL DEFAULT 0,
CONSTRAINT "ProclamationCategoryLegacy_pkey" PRIMARY KEY ("id")
);
INSERT INTO "ProclamationCategoryLegacy" ("id", "name", "parentId", "sortOrder")
SELECT "id", "name", "parentId", "sortOrder"
FROM "ProclamationCategory"
WHERE "parentId" IS NOT NULL;
-- Archive category->subscription mappings for the child categories being
-- removed, so the cascade delete below does not destroy this (deprecated)
-- mapping data. Requires read access to the join table, but never ownership.
CREATE TABLE "ProclamationCategorySubscriptionLevelLegacy" (
"categoryId" INTEGER NOT NULL,
"subscriptionLevelId" INTEGER NOT NULL,
CONSTRAINT "ProclamationCategorySubscriptionLevelLegacy_pkey" PRIMARY KEY ("categoryId", "subscriptionLevelId")
);
INSERT INTO "ProclamationCategorySubscriptionLevelLegacy" ("categoryId", "subscriptionLevelId")
SELECT j."categoryId", j."subscriptionLevelId"
FROM "ProclamationCategorySubscriptionLevel" AS j
JOIN "ProclamationCategory" AS c ON c."id" = j."categoryId" AND c."parentId" IS NOT NULL;
-- Remove former child categories (their data is archived above; the
-- subscription-level join rows for them are archived above and the
-- SubscriptionLevel table itself is left untouched).
DELETE FROM "ProclamationCategory" WHERE "parentId" IS NOT NULL;
-- Drop self-relation
ALTER TABLE "ProclamationCategory" DROP CONSTRAINT "ProclamationCategory_parentId_fkey";
-- DropIndex
DROP INDEX "ProclamationCategory_parentId_name_key";
-- AlterTable
ALTER TABLE "ProclamationCategory" DROP COLUMN "parentId";
-- CreateIndex
CREATE UNIQUE INDEX "ProclamationCategory_name_key" ON "ProclamationCategory"("name");
-- Subscription-level resources are intentionally left in place (unmanaged).
-- No DROP TABLE / DROP COLUMN / DROP CONSTRAINT is performed here, so this
-- migration never requires ownership of pre-existing subscription tables and
-- never destroys subscription data.
@@ -0,0 +1,28 @@
-- DropForeignKey
ALTER TABLE "LicenseKey" DROP CONSTRAINT "LicenseKey_userId_fkey";
-- DropIndex
DROP INDEX "Profile_userId_key";
-- AlterTable
ALTER TABLE "LicenseKey" ADD COLUMN "email" TEXT,
ALTER COLUMN "userId" DROP NOT NULL;
-- AlterTable
ALTER TABLE "Profile" ADD COLUMN "email" TEXT,
ALTER COLUMN "userId" DROP NOT NULL;
-- CreateIndex
CREATE INDEX "LicenseKey_userId_idx" ON "LicenseKey"("userId");
-- CreateIndex
CREATE INDEX "LicenseKey_email_idx" ON "LicenseKey"("email");
-- CreateIndex
CREATE INDEX "Profile_userId_idx" ON "Profile"("userId");
-- CreateIndex
CREATE INDEX "Profile_email_idx" ON "Profile"("email");
-- AddForeignKey
ALTER TABLE "LicenseKey" ADD CONSTRAINT "LicenseKey_userId_fkey" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE SET NULL ON UPDATE CASCADE;