From cc13a5b8579428d2fa5c1c5dea6247ded1f7ca82 Mon Sep 17 00:00:00 2001
From: mAi
Date: Tue, 26 May 2026 11:50:14 +0200
Subject: [PATCH] chore(admin): remove /admin/rules/export page +
export-migrations API (t-paliad-297)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Workflow shifted to hand-written numbered migrations; the audit-row SQL
export tool no longer has any consumers. Pure deletion — /admin/rules
and /admin/rules/{id}/edit stay; only the export-to-SQL flow goes.
Deleted:
- frontend/src/admin-rules-export.tsx
- frontend/src/client/admin-rules-export.ts
Removed:
- routes GET /admin/rules/export and GET /admin/api/rules/export-migrations
- handleAdminExportRuleMigrations + handleAdminRulesExportPage
- RuleEditorService.ExportMigrationsSince + ExportResult + sqlEscape helper
- build.ts entries (import, client bundle, dist HTML write)
- Sidebar "Regel-Migrations" nav item + "Migrations exportieren" button on /admin/rules
- all admin.rules.export.* + nav.admin.rules_export + admin.rules.list.export i18n keys (DE+EN)
- .admin-rules-export-* CSS rules (dead after page deletion)
Doc references in design-fristen-phase2-2026-05-15.md and
design-paliad-data-export-2026-05-19.md updated to mark the endpoint as
removed (acceptance #2 requires grep to return zero hits).
---
docs/design-fristen-phase2-2026-05-15.md | 2 +-
docs/design-paliad-data-export-2026-05-19.md | 4 +-
frontend/build.ts | 3 -
frontend/src/admin-rules-export.tsx | 80 ---------------
frontend/src/admin-rules-list.tsx | 3 -
frontend/src/client/admin-rules-export.ts | 100 -------------------
frontend/src/client/i18n.ts | 38 -------
frontend/src/components/Sidebar.tsx | 1 -
frontend/src/i18n-keys.ts | 18 ----
frontend/src/styles/global.css | 36 -------
internal/handlers/admin_rules.go | 19 ----
internal/handlers/handlers.go | 2 -
internal/services/rule_editor_service.go | 89 -----------------
13 files changed, 3 insertions(+), 392 deletions(-)
delete mode 100644 frontend/src/admin-rules-export.tsx
delete mode 100644 frontend/src/client/admin-rules-export.ts
diff --git a/docs/design-fristen-phase2-2026-05-15.md b/docs/design-fristen-phase2-2026-05-15.md
index 20741ea..0b447da 100644
--- a/docs/design-fristen-phase2-2026-05-15.md
+++ b/docs/design-fristen-phase2-2026-05-15.md
@@ -421,7 +421,7 @@ The editor is the **largest single surface** in Phase 3. ~3-4 PRs of work depend
| `POST /api/admin/rules` | POST | global_admin | Create a new rule from scratch (starts as `lifecycle_state='draft'`). |
| `GET /admin/rules/{id}/audit` | GET | global_admin | Audit log for this rule. |
| `POST /admin/rules/{id}/preview` | POST | global_admin | Preview-on-trigger-date — runs calculator with this draft replacing its published peer; returns the resulting timeline (no persistence). |
-| `POST /admin/rules/export-migration` | POST | global_admin | Export pending (draft + audit-since-last-export) rules as a `*.up.sql` blob the human can paste into `internal/db/migrations/`. Sets `migration_exported=true` on the audit rows. |
+| _(removed t-paliad-297)_ migration-export endpoint | — | — | Was a SQL-export tool generating `*.up.sql` from audit rows. Workflow shifted to hand-written numbered migrations; tool removed in m/paliad#129. |
### 4.2 Draft → published lifecycle
diff --git a/docs/design-paliad-data-export-2026-05-19.md b/docs/design-paliad-data-export-2026-05-19.md
index 828a133..3bf3d64 100644
--- a/docs/design-paliad-data-export-2026-05-19.md
+++ b/docs/design-paliad-data-export-2026-05-19.md
@@ -43,7 +43,7 @@ A full org export today is **< 600 rows of user content** plus reference data
**Audit trail.** Lives in `paliad.project_events` (93 rows). One row per lifecycle event with `event_type`, `metadata jsonb`, `event_date`, `created_by`. The auditing union (`AuditService.ListEntries`) joins 5 sources (project_events, partner_unit_events, deadline_rule_audit, policy_audit_log, reminder_log). For the export we treat `project_events` as primary; the four auxiliary logs are scope-specific.
-**Existing export precedent.** `/admin/rules/export` + `/admin/api/rules/export-migrations` (handlers/admin_rules.go) — admin-gated, streams a generated SQL artifact. Same shape as what we want for the Excel exports. Re-use the gating helper.
+**Existing export precedent.** _(Originally pointed at the admin rule-migration export. That tool was deleted in m/paliad#129 / t-paliad-297. The gating pattern — `adminGate(users, …)` on a download endpoint that streams a generated artifact — still lives on other admin handlers, e.g. `handleAdminDownloadBackup` for `/api/admin/backups/{id}/file`.)_ Re-use the gating helper.
**No Go xlsx library on `go.mod` today.** This design picks **`github.com/xuri/excelize/v2`** in §3.
@@ -591,7 +591,7 @@ No other slice deltas. v1 still ships slices 1+2+3.
- `docs/design-data-model-v2.md` — projects + mandanten + ltree path + can_see_project predicate.
- `docs/design-approval-policy-ui-2026-05-07.md` — 5-source audit union (this design adds the 6th source).
- `docs/design-profession-vs-project-role-2026-05-07.md` — profession ladder for the §4 project gate.
-- `internal/handlers/admin_rules.go:303` — `handleAdminExportRuleMigrations` (precedent for admin-gated export-as-download).
+- `internal/handlers/backups.go` — `handleAdminDownloadBackup` (precedent for admin-gated artifact download; the older rule-migration export precedent was removed in t-paliad-297).
- `internal/services/project_service.go:15` — visibility predicate.
- `internal/services/derivation_service.go` — `EffectiveProjectRole` for the project gate.
- `github.com/xuri/excelize/v2` — chosen xlsx library.
diff --git a/frontend/build.ts b/frontend/build.ts
index c22076e..b6041af 100644
--- a/frontend/build.ts
+++ b/frontend/build.ts
@@ -46,7 +46,6 @@ import { renderAdminApprovalPolicies } from "./src/admin-approval-policies";
import { renderAdminBroadcasts } from "./src/admin-broadcasts";
import { renderAdminRulesList } from "./src/admin-rules-list";
import { renderAdminRulesEdit } from "./src/admin-rules-edit";
-import { renderAdminRulesExport } from "./src/admin-rules-export";
import { renderPaliadin } from "./src/paliadin";
import { renderAdminPaliadin } from "./src/admin-paliadin";
import { renderAdminBackups } from "./src/admin-backups";
@@ -284,7 +283,6 @@ async function build() {
join(import.meta.dir, "src/client/admin-broadcasts.ts"),
join(import.meta.dir, "src/client/admin-rules-list.ts"),
join(import.meta.dir, "src/client/admin-rules-edit.ts"),
- join(import.meta.dir, "src/client/admin-rules-export.ts"),
join(import.meta.dir, "src/client/paliadin.ts"),
// t-paliad-161 — inline Paliadin widget. Loaded via the
// PaliadinWidget component on every authenticated page, so the
@@ -416,7 +414,6 @@ async function build() {
await Bun.write(join(DIST, "admin-broadcasts.html"), renderAdminBroadcasts());
await Bun.write(join(DIST, "admin-rules-list.html"), renderAdminRulesList());
await Bun.write(join(DIST, "admin-rules-edit.html"), renderAdminRulesEdit());
- await Bun.write(join(DIST, "admin-rules-export.html"), renderAdminRulesExport());
await Bun.write(join(DIST, "paliadin.html"), renderPaliadin());
await Bun.write(join(DIST, "admin-paliadin.html"), renderAdminPaliadin());
await Bun.write(join(DIST, "admin-backups.html"), renderAdminBackups());
diff --git a/frontend/src/admin-rules-export.tsx b/frontend/src/admin-rules-export.tsx
deleted file mode 100644
index f7e99d1..0000000
--- a/frontend/src/admin-rules-export.tsx
+++ /dev/null
@@ -1,80 +0,0 @@
-import { h } from "./jsx";
-import { Sidebar } from "./components/Sidebar";
-import { PaliadinWidget } from "./components/PaliadinWidget";
-import { BottomNav } from "./components/BottomNav";
-import { Footer } from "./components/Footer";
-import { PWAHead } from "./components/PWAHead";
-
-// /admin/rules/export — Slice 11b (t-paliad-192). Surfaces the
-// GET /admin/api/rules/export-migrations endpoint as a SQL preview the
-// editor can copy or download. Optional ?since= query lets
-// the editor scope the export to a particular audit window — empty =
-// every un-exported audit row.
-export function renderAdminRulesExport(): string {
- return "" + (
-
-
-
-
-
-
-
-
- Regel-Migrations exportieren — Paliad
-
-
-
-
-
-
-
-
-
-
-
-
- ← Regeln verwalten
-
-
Regel-Migrations exportieren
-
- Generiert ein *.up.sql-Blob mit allen unsynchronisierten Audit-Veränderungen.
- Manuell in internal/db/migrations/ einchecken.
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- );
-}
diff --git a/frontend/src/admin-rules-list.tsx b/frontend/src/admin-rules-list.tsx
index 627e317..3ce437a 100644
--- a/frontend/src/admin-rules-list.tsx
+++ b/frontend/src/admin-rules-list.tsx
@@ -39,9 +39,6 @@ export function renderAdminRulesList(): string {
-
- Migrations exportieren
-
diff --git a/frontend/src/client/admin-rules-export.ts b/frontend/src/client/admin-rules-export.ts
deleted file mode 100644
index 2d29ff4..0000000
--- a/frontend/src/client/admin-rules-export.ts
+++ /dev/null
@@ -1,100 +0,0 @@
-import { initI18n, t } from "./i18n";
-import { initSidebar } from "./sidebar";
-
-// admin-rules-export.ts — /admin/rules/export. Calls
-// GET /admin/api/rules/export-migrations[?since=
] and renders the
-// SQL blob server-side. Download builds a Blob URL and triggers a
-// fake click; copy uses navigator.clipboard.
-
-interface ExportResult {
- migration_sql: string;
- count: number;
- latest_audit_id: string;
-}
-
-let latest: ExportResult | null = null;
-
-function showFeedback(msg: string, isError: boolean) {
- const el = document.getElementById("export-feedback") as HTMLElement | null;
- if (!el) return;
- el.textContent = msg;
- el.className = "form-msg " + (isError ? "form-msg-error" : "form-msg-success");
- el.style.display = "block";
- if (!isError) setTimeout(() => { el.style.display = "none"; }, 4000);
-}
-
-async function runExport() {
- const since = (document.getElementById("export-since") as HTMLInputElement).value.trim();
- const qs = new URLSearchParams();
- if (since) qs.set("since", since);
- const url = "/admin/api/rules/export-migrations" + (qs.toString() ? "?" + qs.toString() : "");
- const out = document.getElementById("export-output") as HTMLElement;
- const summary = document.getElementById("export-summary") as HTMLElement;
- const dl = document.getElementById("export-download") as HTMLElement;
- const cp = document.getElementById("export-copy") as HTMLElement;
- out.textContent = t("admin.rules.export.running") || "Lade...";
- summary.style.display = "none";
- dl.style.display = "none";
- cp.style.display = "none";
-
- const resp = await fetch(url);
- if (!resp.ok) {
- const body = await resp.json().catch(() => ({ error: resp.statusText }));
- showFeedback(body.error || (t("admin.rules.export.error") || "Export fehlgeschlagen."), true);
- out.textContent = "";
- return;
- }
- latest = await resp.json() as ExportResult;
- out.textContent = latest.migration_sql;
- summary.style.display = "";
- const countEl = document.getElementById("export-summary-count") as HTMLElement;
- const latestEl = document.getElementById("export-summary-latest") as HTMLElement;
- countEl.textContent = (t("admin.rules.export.count") || "Audit-Zeilen: {n}").replace("{n}", String(latest.count));
- if (latest.latest_audit_id) {
- latestEl.textContent = (t("admin.rules.export.latest") || "Letzte Audit-ID: {id}").replace("{id}", latest.latest_audit_id);
- } else {
- latestEl.textContent = "";
- }
- if (latest.count > 0) {
- dl.style.display = "";
- cp.style.display = "";
- showFeedback((t("admin.rules.export.ok") || "{n} Audit-Zeilen exportiert.").replace("{n}", String(latest.count)), false);
- } else {
- showFeedback(t("admin.rules.export.no_pending") || "Keine offenen Audit-Zeilen zum Export.", false);
- }
-}
-
-function downloadFile() {
- if (!latest) return;
- const ts = new Date().toISOString().replace(/[:.]/g, "-").slice(0, 19);
- const name = `rules-export-${ts}.up.sql`;
- const blob = new Blob([latest.migration_sql], { type: "application/sql" });
- const url = URL.createObjectURL(blob);
- const a = document.createElement("a");
- a.href = url;
- a.download = name;
- document.body.appendChild(a);
- a.click();
- document.body.removeChild(a);
- URL.revokeObjectURL(url);
-}
-
-async function copyToClipboard() {
- if (!latest) return;
- try {
- await navigator.clipboard.writeText(latest.migration_sql);
- showFeedback(t("admin.rules.export.copied") || "In Zwischenablage kopiert.", false);
- } catch (e) {
- showFeedback(t("admin.rules.export.copy_failed") || "Kopieren fehlgeschlagen.", true);
- }
-}
-
-function init() {
- initI18n();
- initSidebar();
- (document.getElementById("export-run") as HTMLElement).addEventListener("click", runExport);
- (document.getElementById("export-download") as HTMLElement).addEventListener("click", downloadFile);
- (document.getElementById("export-copy") as HTMLElement).addEventListener("click", copyToClipboard);
-}
-
-document.addEventListener("DOMContentLoaded", init);
diff --git a/frontend/src/client/i18n.ts b/frontend/src/client/i18n.ts
index 66c998f..0a456cc 100644
--- a/frontend/src/client/i18n.ts
+++ b/frontend/src/client/i18n.ts
@@ -2892,7 +2892,6 @@ const translations: Record> = {
// `admin.procedural_events.*` aliases live after the EN block — they
// pin the contract for when .tsx files rebind in Slice B (B.5).
"nav.admin.rules": "Verfahrensschritte verwalten",
- "nav.admin.rules_export": "Verfahrensschritt-Migrations",
"admin.card.rules.title": "Verfahrensschritte verwalten",
"admin.card.rules.desc": "Verfahrensschritte anlegen, bearbeiten, publishen. Audit-Log, Preview, Migration-Export.",
@@ -2900,7 +2899,6 @@ const translations: Record> = {
"admin.rules.list.heading": "Verfahrensschritte verwalten",
"admin.rules.list.subtitle": "Verfahrensschritte (Schriftsätze, Anhörungen, Entscheidungen, …) anlegen, bearbeiten und freigeben. Lifecycle: draft → published → archived.",
"admin.rules.list.new": "+ Neuer Verfahrensschritt",
- "admin.rules.list.export": "Migrations exportieren",
"admin.rules.tab.rules": "Regeln",
"admin.rules.tab.orphans": "Orphans",
"admin.rules.loading": "Lade…",
@@ -3062,23 +3060,6 @@ const translations: Record> = {
"admin.rules.edit.modal.restore.title": "Wiederherstellen",
"admin.rules.edit.modal.restore.body": "Regel wird wiederhergestellt (archived → published).",
- "admin.rules.export.title": "Regel-Migrations exportieren — Paliad",
- "admin.rules.export.heading": "Regel-Migrations exportieren",
- "admin.rules.export.subtitle": "Generiert ein *.up.sql-Blob mit allen unsynchronisierten Audit-Veränderungen. Manuell in internal/db/migrations/ einchecken.",
- "admin.rules.export.breadcrumb": "← Regeln verwalten",
- "admin.rules.export.field.since": "Startend ab Audit-ID (optional)",
- "admin.rules.export.run": "Export generieren",
- "admin.rules.export.running": "Lade…",
- "admin.rules.export.download": "Als Datei herunterladen",
- "admin.rules.export.copy": "In Zwischenablage kopieren",
- "admin.rules.export.copied": "In Zwischenablage kopiert.",
- "admin.rules.export.copy_failed": "Kopieren fehlgeschlagen.",
- "admin.rules.export.count": "Audit-Zeilen: {n}",
- "admin.rules.export.latest": "Letzte Audit-ID: {id}",
- "admin.rules.export.ok": "{n} Audit-Zeilen exportiert.",
- "admin.rules.export.error": "Export fehlgeschlagen.",
- "admin.rules.export.no_pending": "Keine offenen Audit-Zeilen zum Export.",
-
// Date-range picker (t-paliad-248). Symmetric past/future chip fan
// around an ALLES centre. Used by the filter-bar 'time' axis from
// Slice A onwards; future slices will migrate /agenda and
@@ -5966,7 +5947,6 @@ const translations: Record> = {
// t-paliad-192 Slice 11b — Admin rule-editor UI.
// t-paliad-262 Slice A — "Rule" relabelled as "Procedural event".
"nav.admin.rules": "Manage procedural events",
- "nav.admin.rules_export": "Procedural-event migrations",
"admin.card.rules.title": "Manage procedural events",
"admin.card.rules.desc": "Author, edit and publish procedural-event templates. Audit log, preview, migration export.",
@@ -5974,7 +5954,6 @@ const translations: Record> = {
"admin.rules.list.heading": "Manage procedural events",
"admin.rules.list.subtitle": "Author, edit and publish procedural events (filings, hearings, decisions, …). Lifecycle: draft → published → archived.",
"admin.rules.list.new": "+ New procedural event",
- "admin.rules.list.export": "Export migrations",
"admin.rules.tab.rules": "Rules",
"admin.rules.tab.orphans": "Orphans",
"admin.rules.loading": "Loading…",
@@ -6136,23 +6115,6 @@ const translations: Record> = {
"admin.rules.edit.modal.restore.title": "Restore",
"admin.rules.edit.modal.restore.body": "Rule will be restored (archived → published).",
- "admin.rules.export.title": "Export rule migrations — Paliad",
- "admin.rules.export.heading": "Export rule migrations",
- "admin.rules.export.subtitle": "Generates a *.up.sql blob with every un-exported audit change. Commit manually into internal/db/migrations/.",
- "admin.rules.export.breadcrumb": "← Manage Rules",
- "admin.rules.export.field.since": "Starting from audit id (optional)",
- "admin.rules.export.run": "Generate export",
- "admin.rules.export.running": "Loading…",
- "admin.rules.export.download": "Download as file",
- "admin.rules.export.copy": "Copy to clipboard",
- "admin.rules.export.copied": "Copied to clipboard.",
- "admin.rules.export.copy_failed": "Copy failed.",
- "admin.rules.export.count": "Audit rows: {n}",
- "admin.rules.export.latest": "Latest audit id: {id}",
- "admin.rules.export.ok": "{n} audit rows exported.",
- "admin.rules.export.error": "Export failed.",
- "admin.rules.export.no_pending": "No pending audit rows to export.",
-
// Date-range picker (t-paliad-248). See DE block above for details.
"date_range.button.label": "Time range",
"date_range.button.label.custom_range": "From {from} to {to}",
diff --git a/frontend/src/components/Sidebar.tsx b/frontend/src/components/Sidebar.tsx
index d237da3..aad4044 100644
--- a/frontend/src/components/Sidebar.tsx
+++ b/frontend/src/components/Sidebar.tsx
@@ -205,7 +205,6 @@ export function Sidebar({ currentPath, authenticated = true }: SidebarProps): st
{navItem("/admin/partner-units", ICON_BUILDING, "nav.admin.partner_units", "Partner Units", currentPath)}
{navItem("/admin/event-types", ICON_TABLE, "nav.admin.event_types", "Event-Typen", currentPath)}
{navItem("/admin/rules", ICON_BOOK, "nav.admin.rules", "Regeln verwalten", currentPath)}
- {navItem("/admin/rules/export", ICON_DOWNLOAD, "nav.admin.rules_export", "Regel-Migrations", currentPath)}
{navItem("/admin/audit-log", ICON_AUDIT_LOG, "nav.admin.audit", "Audit-Log", currentPath)}
{navItem("/admin/backups", ICON_DOWNLOAD, "nav.admin.backups", "Backups", currentPath)}
{/* Paliadin Monitor — owner-only sub-entry; revealed by sidebar.ts together with the /paliadin link. */}
diff --git a/frontend/src/i18n-keys.ts b/frontend/src/i18n-keys.ts
index c991e15..28c1afe 100644
--- a/frontend/src/i18n-keys.ts
+++ b/frontend/src/i18n-keys.ts
@@ -401,22 +401,6 @@ export type I18nKey =
| "admin.rules.edit.title"
| "admin.rules.empty"
| "admin.rules.error.load"
- | "admin.rules.export.breadcrumb"
- | "admin.rules.export.copied"
- | "admin.rules.export.copy"
- | "admin.rules.export.copy_failed"
- | "admin.rules.export.count"
- | "admin.rules.export.download"
- | "admin.rules.export.error"
- | "admin.rules.export.field.since"
- | "admin.rules.export.heading"
- | "admin.rules.export.latest"
- | "admin.rules.export.no_pending"
- | "admin.rules.export.ok"
- | "admin.rules.export.run"
- | "admin.rules.export.running"
- | "admin.rules.export.subtitle"
- | "admin.rules.export.title"
| "admin.rules.filter.lifecycle"
| "admin.rules.filter.lifecycle.any"
| "admin.rules.filter.proceeding"
@@ -428,7 +412,6 @@ export type I18nKey =
| "admin.rules.lifecycle.archived"
| "admin.rules.lifecycle.draft"
| "admin.rules.lifecycle.published"
- | "admin.rules.list.export"
| "admin.rules.list.heading"
| "admin.rules.list.new"
| "admin.rules.list.subtitle"
@@ -1992,7 +1975,6 @@ export type I18nKey =
| "nav.admin.paliadin"
| "nav.admin.partner_units"
| "nav.admin.rules"
- | "nav.admin.rules_export"
| "nav.admin.team"
| "nav.agenda"
| "nav.akten"
diff --git a/frontend/src/styles/global.css b/frontend/src/styles/global.css
index 3722e33..9ddf8a7 100644
--- a/frontend/src/styles/global.css
+++ b/frontend/src/styles/global.css
@@ -18185,42 +18185,6 @@ dialog.quick-add-sheet::backdrop {
border-top: 1px solid var(--color-border, #d4d4d8);
}
-/* Export page */
-
-.admin-rules-export-controls {
- display: flex;
- gap: 0.5rem;
- align-items: flex-end;
- flex-wrap: wrap;
- margin-bottom: 1rem;
-}
-
-.admin-rules-export-controls .form-field {
- flex: 1 1 240px;
-}
-
-.admin-rules-export-summary {
- display: flex;
- gap: 1.5rem;
- flex-wrap: wrap;
- font-size: 0.9rem;
- color: var(--color-text-muted, #71717a);
- margin-bottom: 0.75rem;
-}
-
-.admin-rules-export-pre {
- background: var(--color-bg-subtle, #f4f4f5);
- border: 1px solid var(--color-border, #d4d4d8);
- border-radius: 6px;
- padding: 1rem;
- overflow: auto;
- max-height: 60vh;
- font-family: var(--font-mono, ui-monospace, monospace);
- font-size: 0.8rem;
- white-space: pre;
- margin: 0;
-}
-
/* Date-range picker (t-paliad-248) ------------------------------------
Symmetric past/future chip fan around an ALLES centre, in a popover
anchored under a closed-state trigger button. Reuses .agenda-chip /
diff --git a/internal/handlers/admin_rules.go b/internal/handlers/admin_rules.go
index b288fe0..e3372ed 100644
--- a/internal/handlers/admin_rules.go
+++ b/internal/handlers/admin_rules.go
@@ -299,21 +299,6 @@ func handleAdminPreviewRule(w http.ResponseWriter, r *http.Request) {
writeJSON(w, http.StatusOK, resp)
}
-// GET /admin/api/rules/export-migrations?since=
-func handleAdminExportRuleMigrations(w http.ResponseWriter, r *http.Request) {
- if dbSvc == nil || dbSvc.ruleEditor == nil {
- writeJSON(w, http.StatusServiceUnavailable, map[string]string{"error": "rule editor unavailable"})
- return
- }
- since := r.URL.Query().Get("since")
- out, err := dbSvc.ruleEditor.ExportMigrationsSince(r.Context(), since)
- if err != nil {
- writeRuleEditorError(w, err)
- return
- }
- writeJSON(w, http.StatusOK, out)
-}
-
// =============================================================================
// Page handlers — serve the static SPA shells. Auth + admin gate live
// at the route registration in handlers.go.
@@ -327,10 +312,6 @@ func handleAdminRulesEditPage(w http.ResponseWriter, r *http.Request) {
http.ServeFile(w, r, "dist/admin-rules-edit.html")
}
-func handleAdminRulesExportPage(w http.ResponseWriter, r *http.Request) {
- http.ServeFile(w, r, "dist/admin-rules-export.html")
-}
-
// =============================================================================
// helpers
// =============================================================================
diff --git a/internal/handlers/handlers.go b/internal/handlers/handlers.go
index 6912f16..c9fa532 100644
--- a/internal/handlers/handlers.go
+++ b/internal/handlers/handlers.go
@@ -670,10 +670,8 @@ func Register(mux *http.ServeMux, client *auth.Client, giteaAPIToken string, svc
// t-paliad-191 Slice 11a — admin rule-editor API.
// t-paliad-192 Slice 11b — admin rule-editor UI pages + orphan list/resolve.
protected.HandleFunc("GET /admin/rules", adminGate(users, gateOnboarded(handleAdminRulesListPage)))
- protected.HandleFunc("GET /admin/rules/export", adminGate(users, gateOnboarded(handleAdminRulesExportPage)))
protected.HandleFunc("GET /admin/rules/{id}/edit", adminGate(users, gateOnboarded(handleAdminRulesEditPage)))
protected.HandleFunc("GET /admin/api/rules", adminGate(users, handleAdminListRules))
- protected.HandleFunc("GET /admin/api/rules/export-migrations", adminGate(users, handleAdminExportRuleMigrations))
protected.HandleFunc("GET /admin/api/rules/{id}", adminGate(users, handleAdminGetRule))
protected.HandleFunc("POST /admin/api/rules", adminGate(users, handleAdminCreateRule))
protected.HandleFunc("PATCH /admin/api/rules/{id}", adminGate(users, handleAdminPatchRule))
diff --git a/internal/services/rule_editor_service.go b/internal/services/rule_editor_service.go
index 84b7d20..2b68f08 100644
--- a/internal/services/rule_editor_service.go
+++ b/internal/services/rule_editor_service.go
@@ -604,92 +604,6 @@ func (s *RuleEditorService) getByID(ctx context.Context, id uuid.UUID) (*models.
return &r, nil
}
-// ExportMigrationsSince returns a SQL blob containing one UPDATE / INSERT
-// per audited rule change after the given audit row id. Used by the
-// admin "export changes to a migration file" flow (Q-H-5: pure SQL
-// format). Returns SQL + count + the latest audit id seen so the
-// caller can pass it as ?since= on the next call.
-//
-// v1 generates one UPDATE per audit row using the after_json snapshot.
-// Slice 11b will polish the output (re-order so foreign-key edges
-// resolve, collapse consecutive UPDATEs on the same row, format the
-// header comment with author + reason). v1 emits one statement per
-// audit row in chronological order — sufficient for hand-review.
-type ExportResult struct {
- MigrationSQL string `json:"migration_sql"`
- Count int `json:"count"`
- LatestAuditID string `json:"latest_audit_id"`
-}
-
-func (s *RuleEditorService) ExportMigrationsSince(ctx context.Context, sinceAuditID string) (*ExportResult, error) {
- type auditRow struct {
- ID uuid.UUID `db:"id"`
- RuleID uuid.UUID `db:"rule_id"`
- ChangedAt time.Time `db:"changed_at"`
- Action string `db:"action"`
- AfterJSON json.RawMessage `db:"after_json"`
- Reason string `db:"reason"`
- }
- var rows []auditRow
- q := `SELECT id, rule_id, changed_at, action, after_json, reason
- FROM paliad.deadline_rule_audit
- WHERE migration_exported = false`
- args := []any{}
- if sinceAuditID != "" {
- sid, err := uuid.Parse(sinceAuditID)
- if err != nil {
- return nil, fmt.Errorf("%w: invalid since= uuid", ErrInvalidInput)
- }
- q += ` AND changed_at >= (SELECT changed_at FROM paliad.deadline_rule_audit WHERE id = $1)`
- args = append(args, sid)
- }
- q += ` ORDER BY changed_at ASC`
- if err := s.db.SelectContext(ctx, &rows, q, args...); err != nil {
- return nil, fmt.Errorf("list audit since: %w", err)
- }
-
- var sb strings.Builder
- sb.WriteString("-- Auto-generated rule-editor migration export.\n")
- sb.WriteString("-- Generated at: " + time.Now().UTC().Format(time.RFC3339) + "\n")
- sb.WriteString("-- Rows: " + fmt.Sprintf("%d", len(rows)) + "\n\n")
- sb.WriteString("SELECT set_config('paliad.audit_reason',\n")
- sb.WriteString(" 'rule-editor export: replay of " + fmt.Sprintf("%d", len(rows)) + " edits', true);\n\n")
-
- latest := ""
- for _, r := range rows {
- sb.WriteString("-- audit " + r.ID.String() + " (" + r.Action + " " + r.ChangedAt.Format(time.RFC3339) + "): " + sqlEscape(r.Reason) + "\n")
- switch r.Action {
- case "create", "update":
- if len(r.AfterJSON) == 0 {
- sb.WriteString("-- (no after_json — skipped)\n\n")
- continue
- }
- sb.WriteString("INSERT INTO paliad.deadline_rules\n")
- sb.WriteString(" SELECT (jsonb_populate_record(NULL::paliad.deadline_rules, '")
- sb.WriteString(sqlEscape(string(r.AfterJSON)))
- sb.WriteString("'::jsonb)).*\n")
- sb.WriteString("ON CONFLICT (id) DO UPDATE SET name = EXCLUDED.name, name_en = EXCLUDED.name_en,\n")
- sb.WriteString(" duration_value = EXCLUDED.duration_value, duration_unit = EXCLUDED.duration_unit,\n")
- sb.WriteString(" timing = EXCLUDED.timing, priority = EXCLUDED.priority,\n")
- sb.WriteString(" is_court_set = EXCLUDED.is_court_set,\n")
- sb.WriteString(" condition_expr = EXCLUDED.condition_expr,\n")
- sb.WriteString(" lifecycle_state = EXCLUDED.lifecycle_state,\n")
- sb.WriteString(" updated_at = now();\n\n")
- case "delete", "archive":
- sb.WriteString("UPDATE paliad.deadline_rules SET lifecycle_state='archived', updated_at=now() WHERE id='")
- sb.WriteString(r.RuleID.String())
- sb.WriteString("';\n\n")
- }
- latest = r.ID.String()
- }
-
- return &ExportResult{
- MigrationSQL: sb.String(),
- Count: len(rows),
- LatestAuditID: latest,
- }, nil
-}
-
// =============================================================================
// Internal helpers
// =============================================================================
@@ -814,6 +728,3 @@ func nullableJSON(b json.RawMessage) any {
return []byte(b)
}
-func sqlEscape(s string) string {
- return strings.ReplaceAll(s, "'", "''")
-}