fix(t-paliad-062): PR-E bug batch — F-02, F-03, F-08, F-09
Four standalone bugs from the 2026-04-27 polish audit (PR-E batch).
F-02 — /admin/team search input: long placeholder ("Nach Name oder
E-Mail suchen…") visually overlapped the absolutely-positioned count
badge ("31 / 31") because .glossar-search reserved only 0.75rem of
right padding. Bumped padding-right to 4.5rem so the badge sits in its
own gutter — same fix protects every other use of the .glossar-search
shell (admin team, glossary, etc.) without touching individual pages.
F-03 — /api/departments?include=members 500 regression. Migration 020
renamed paliad.dezernat_mitglieder → department_members but missed the
dezernat_id column on prod youpc. Application code (DepartmentService.
ListWithMembers / ListMembers / AddMember / RemoveMember) selects
department_id, which doesn't exist there → "column does not exist"
500. New migration 024 renames the column idempotently, plus the
indexes/constraints/policies that postgres did not auto-rename when
their table was renamed (departments_pkey, departments_office_idx,
departments_lead_idx, departments_lead_user_id_fkey,
departments_office_check, department_members_pkey,
department_members_user_idx, department_members_department_id_fkey,
department_members_user_id_fkey, departments_select / _write,
department_members_select / _write). Every rename uses a DO block that
swallows undefined_object / undefined_column so the migration is a
no-op on dev DBs that already had English names from migration 018.
Down step puts the German names back symmetrically.
F-08 — Project detail tabs (/projects/{id}/Verlauf|Team|…) used
href="#", so middle-click and "open in new tab" were broken even
though the SPA already mirrored the canonical path via
history.replaceState. initTabs() now sets each tab anchor's href to
/projects/{id}/{tab} (id resolved from the URL when the project hasn't
loaded yet) and only intercepts plain left-clicks — middle/ctrl/meta/
shift/alt fall through to the browser. Backend gains the previously-
missing /projects/{id}/history and /projects/{id}/children server
routes (both bound to handleProjectsDetailPage like every other tab),
so opening the URL in a fresh tab no longer 404s.
F-09 — /projects?view=tree was silently ignored: viewMode was hard-
coded to "flat" and the URL was never read. parseInitialView() now
seeds viewMode from ?view=, initFilters() syncs the dropdown to the
parsed value before binding the change handler, and changing the
dropdown rewrites the query string via history.replaceState (default
"flat" stays implicit to keep the canonical path clean). Bookmarks,
dashboard links, and copy-shared URLs round-trip correctly.
Verification:
- /api/departments?include=members live-tested after applying 024 to
youpc: returns 200 with members enriched.
- go build ./... + go vet ./... + go test ./... clean.
- bun run build clean.
This commit is contained in:
@@ -724,8 +724,13 @@ function escapeHtml(s: string): string {
|
||||
}
|
||||
|
||||
function initTabs() {
|
||||
const id = project?.id ?? parseProjectID();
|
||||
document.querySelectorAll<HTMLAnchorElement>(".akten-tab").forEach((tab) => {
|
||||
if (id) tab.href = `/projects/${id}/${tab.dataset.tab}`;
|
||||
tab.addEventListener("click", (e) => {
|
||||
// SPA flow on plain left-click; let the browser handle middle-click,
|
||||
// ctrl/meta-click, and "open in new tab" via the real href above.
|
||||
if (e.defaultPrevented || e.button !== 0 || e.metaKey || e.ctrlKey || e.shiftKey || e.altKey) return;
|
||||
e.preventDefault();
|
||||
showTab(tab.dataset.tab as TabId);
|
||||
});
|
||||
|
||||
@@ -19,10 +19,18 @@ interface Project {
|
||||
let allRows: Project[] = [];
|
||||
let typeFilter = "";
|
||||
let statusFilter = "";
|
||||
let viewMode: "flat" | "tree" | "roots" = "flat";
|
||||
let viewMode: "flat" | "tree" | "roots" = parseInitialView();
|
||||
let searchQuery = "";
|
||||
let loadedOK = false;
|
||||
|
||||
// Honour ?view=flat|tree|roots from the URL so dashboard links and bookmarks
|
||||
// land on the right layout. Anything else falls back to "flat".
|
||||
function parseInitialView(): "flat" | "tree" | "roots" {
|
||||
const v = new URLSearchParams(window.location.search).get("view");
|
||||
if (v === "tree" || v === "roots" || v === "flat") return v;
|
||||
return "flat";
|
||||
}
|
||||
|
||||
async function loadProjekte() {
|
||||
const unavailable = document.getElementById("akten-unavailable")!;
|
||||
const table = document.querySelector<HTMLElement>(".akten-table-wrap")!;
|
||||
@@ -179,6 +187,7 @@ function initFilters() {
|
||||
const typeSel = document.getElementById("projekt-type") as HTMLSelectElement;
|
||||
const status = document.getElementById("akten-status") as HTMLSelectElement;
|
||||
const view = document.getElementById("projekt-view") as HTMLSelectElement;
|
||||
view.value = viewMode;
|
||||
typeSel.addEventListener("change", () => {
|
||||
typeFilter = typeSel.value;
|
||||
render();
|
||||
@@ -189,10 +198,20 @@ function initFilters() {
|
||||
});
|
||||
view.addEventListener("change", () => {
|
||||
viewMode = view.value as "flat" | "tree" | "roots";
|
||||
syncViewQuery();
|
||||
render();
|
||||
});
|
||||
}
|
||||
|
||||
// Mirror viewMode into ?view= so the URL is shareable. Default "flat" stays
|
||||
// implicit (drop the param) to keep the canonical path clean.
|
||||
function syncViewQuery() {
|
||||
const url = new URL(window.location.href);
|
||||
if (viewMode === "flat") url.searchParams.delete("view");
|
||||
else url.searchParams.set("view", viewMode);
|
||||
window.history.replaceState({}, "", url.toString());
|
||||
}
|
||||
|
||||
document.addEventListener("DOMContentLoaded", () => {
|
||||
initI18n();
|
||||
initSidebar();
|
||||
|
||||
@@ -1700,7 +1700,7 @@ input[type="range"]::-moz-range-thumb {
|
||||
|
||||
.glossar-search {
|
||||
width: 100%;
|
||||
padding: 0.65rem 0.75rem 0.65rem 2.5rem;
|
||||
padding: 0.65rem 4.5rem 0.65rem 2.5rem;
|
||||
font-size: 0.92rem;
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius);
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
-- Reverse 024_rename_department_columns: put back the German names.
|
||||
DO $$ BEGIN ALTER POLICY department_members_write ON paliad.department_members RENAME TO dezernat_mitglieder_write; EXCEPTION WHEN undefined_object THEN NULL; END $$;
|
||||
DO $$ BEGIN ALTER POLICY department_members_select ON paliad.department_members RENAME TO dezernat_mitglieder_select; EXCEPTION WHEN undefined_object THEN NULL; END $$;
|
||||
DO $$ BEGIN ALTER POLICY departments_write ON paliad.departments RENAME TO dezernate_write; EXCEPTION WHEN undefined_object THEN NULL; END $$;
|
||||
DO $$ BEGIN ALTER POLICY departments_select ON paliad.departments RENAME TO dezernate_select; EXCEPTION WHEN undefined_object THEN NULL; END $$;
|
||||
|
||||
DO $$ BEGIN ALTER INDEX paliad.department_members_user_idx RENAME TO dezernat_mitglieder_user_idx; EXCEPTION WHEN undefined_object THEN NULL; END $$;
|
||||
DO $$ BEGIN ALTER INDEX paliad.departments_lead_idx RENAME TO dezernate_lead_idx; EXCEPTION WHEN undefined_object THEN NULL; END $$;
|
||||
DO $$ BEGIN ALTER INDEX paliad.departments_office_idx RENAME TO dezernate_office_idx; EXCEPTION WHEN undefined_object THEN NULL; END $$;
|
||||
|
||||
DO $$ BEGIN ALTER TABLE paliad.department_members RENAME CONSTRAINT department_members_user_id_fkey TO dezernat_mitglieder_user_id_fkey; EXCEPTION WHEN undefined_object THEN NULL; END $$;
|
||||
DO $$ BEGIN ALTER TABLE paliad.department_members RENAME CONSTRAINT department_members_department_id_fkey TO dezernat_mitglieder_dezernat_id_fkey; EXCEPTION WHEN undefined_object THEN NULL; END $$;
|
||||
DO $$ BEGIN ALTER TABLE paliad.department_members RENAME CONSTRAINT department_members_pkey TO dezernat_mitglieder_pkey; EXCEPTION WHEN undefined_object THEN NULL; END $$;
|
||||
DO $$ BEGIN ALTER TABLE paliad.departments RENAME CONSTRAINT departments_office_check TO dezernate_office_check; EXCEPTION WHEN undefined_object THEN NULL; END $$;
|
||||
DO $$ BEGIN ALTER TABLE paliad.departments RENAME CONSTRAINT departments_lead_user_id_fkey TO dezernate_lead_user_id_fkey; EXCEPTION WHEN undefined_object THEN NULL; END $$;
|
||||
DO $$ BEGIN ALTER TABLE paliad.departments RENAME CONSTRAINT departments_pkey TO dezernate_pkey; EXCEPTION WHEN undefined_object THEN NULL; END $$;
|
||||
|
||||
DO $$ BEGIN ALTER TABLE paliad.department_members RENAME COLUMN department_id TO dezernat_id; EXCEPTION WHEN undefined_column OR undefined_table THEN NULL; END $$;
|
||||
49
internal/db/migrations/024_rename_department_columns.up.sql
Normal file
49
internal/db/migrations/024_rename_department_columns.up.sql
Normal file
@@ -0,0 +1,49 @@
|
||||
-- Rename remaining German names on paliad.departments / paliad.department_members
|
||||
-- (t-paliad-062 — F-03 fix).
|
||||
--
|
||||
-- Migration 020 renamed the *tables* paliad.dezernate → departments and
|
||||
-- paliad.dezernat_mitglieder → department_members, but missed:
|
||||
-- * the dezernat_id column on the link table (still German on prod youpc),
|
||||
-- * the indexes/constraints/policies that postgres did not auto-rename
|
||||
-- when their owning table was renamed.
|
||||
--
|
||||
-- Application code (services.DepartmentService.ListWithMembers,
|
||||
-- ListMembers, AddMember, RemoveMember) selects/inserts on
|
||||
-- department_members.department_id, so the missing column rename surfaces
|
||||
-- as a 500 on /api/departments?include=members and on every /admin/team
|
||||
-- write that touches Dezernat membership.
|
||||
--
|
||||
-- Idempotent: every rename is wrapped in a guard that swallows
|
||||
-- "undefined_column" / "undefined_object" so the migration is a no-op on
|
||||
-- DBs that were already provisioned with English names by migration 018.
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- Column
|
||||
-- ---------------------------------------------------------------------------
|
||||
DO $$ BEGIN ALTER TABLE paliad.department_members RENAME COLUMN dezernat_id TO department_id; EXCEPTION WHEN undefined_column OR undefined_table THEN NULL; END $$;
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- Constraints (primary key + foreign keys + check). Renaming a pkey
|
||||
-- constraint also renames the underlying index of the same name.
|
||||
-- ---------------------------------------------------------------------------
|
||||
DO $$ BEGIN ALTER TABLE paliad.departments RENAME CONSTRAINT dezernate_pkey TO departments_pkey; EXCEPTION WHEN undefined_object THEN NULL; END $$;
|
||||
DO $$ BEGIN ALTER TABLE paliad.departments RENAME CONSTRAINT dezernate_lead_user_id_fkey TO departments_lead_user_id_fkey; EXCEPTION WHEN undefined_object THEN NULL; END $$;
|
||||
DO $$ BEGIN ALTER TABLE paliad.departments RENAME CONSTRAINT dezernate_office_check TO departments_office_check; EXCEPTION WHEN undefined_object THEN NULL; END $$;
|
||||
DO $$ BEGIN ALTER TABLE paliad.department_members RENAME CONSTRAINT dezernat_mitglieder_pkey TO department_members_pkey; EXCEPTION WHEN undefined_object THEN NULL; END $$;
|
||||
DO $$ BEGIN ALTER TABLE paliad.department_members RENAME CONSTRAINT dezernat_mitglieder_dezernat_id_fkey TO department_members_department_id_fkey; EXCEPTION WHEN undefined_object THEN NULL; END $$;
|
||||
DO $$ BEGIN ALTER TABLE paliad.department_members RENAME CONSTRAINT dezernat_mitglieder_user_id_fkey TO department_members_user_id_fkey; EXCEPTION WHEN undefined_object THEN NULL; END $$;
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- Standalone indexes (non-pkey).
|
||||
-- ---------------------------------------------------------------------------
|
||||
DO $$ BEGIN ALTER INDEX paliad.dezernate_office_idx RENAME TO departments_office_idx; EXCEPTION WHEN undefined_object THEN NULL; END $$;
|
||||
DO $$ BEGIN ALTER INDEX paliad.dezernate_lead_idx RENAME TO departments_lead_idx; EXCEPTION WHEN undefined_object THEN NULL; END $$;
|
||||
DO $$ BEGIN ALTER INDEX paliad.dezernat_mitglieder_user_idx RENAME TO department_members_user_idx; EXCEPTION WHEN undefined_object THEN NULL; END $$;
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- RLS policies
|
||||
-- ---------------------------------------------------------------------------
|
||||
DO $$ BEGIN ALTER POLICY dezernate_select ON paliad.departments RENAME TO departments_select; EXCEPTION WHEN undefined_object THEN NULL; END $$;
|
||||
DO $$ BEGIN ALTER POLICY dezernate_write ON paliad.departments RENAME TO departments_write; EXCEPTION WHEN undefined_object THEN NULL; END $$;
|
||||
DO $$ BEGIN ALTER POLICY dezernat_mitglieder_select ON paliad.department_members RENAME TO department_members_select; EXCEPTION WHEN undefined_object THEN NULL; END $$;
|
||||
DO $$ BEGIN ALTER POLICY dezernat_mitglieder_write ON paliad.department_members RENAME TO department_members_write; EXCEPTION WHEN undefined_object THEN NULL; END $$;
|
||||
@@ -256,7 +256,9 @@ func Register(mux *http.ServeMux, client *auth.Client, giteaAPIToken string, svc
|
||||
protected.HandleFunc("GET /projects", gateOnboarded(handleProjectsListPage))
|
||||
protected.HandleFunc("GET /projects/new", gateOnboarded(handleProjectsNewPage))
|
||||
protected.HandleFunc("GET /projects/{id}", gateOnboarded(handleProjectsDetailPage))
|
||||
protected.HandleFunc("GET /projects/{id}/history", gateOnboarded(handleProjectsDetailPage))
|
||||
protected.HandleFunc("GET /projects/{id}/events", gateOnboarded(handleProjectsDetailPage))
|
||||
protected.HandleFunc("GET /projects/{id}/children", gateOnboarded(handleProjectsDetailPage))
|
||||
protected.HandleFunc("GET /projects/{id}/parties", gateOnboarded(handleProjectsDetailPage))
|
||||
protected.HandleFunc("GET /projects/{id}/deadlines", gateOnboarded(handleProjectsDetailPage))
|
||||
protected.HandleFunc("GET /projects/{id}/appointments", gateOnboarded(handleProjectsDetailPage))
|
||||
|
||||
Reference in New Issue
Block a user