After m/paliad#69's edit-mode overhaul, widgets visually overlapped on
mixed-size rows: a 12-col + 6-col swap, an auto-flow widget landing on
an explicit blocker, or a resize-grow into a sibling all produced
layouts that ignored colspan footprints when computing occupancy.
Extracts placement math from dashboard.ts into a pure ./dashboard-grid
module and adds an occupancy bitmap. Every visible widget is placed
once; explicit-position collisions are resolved by searching downward
from the requested row for the first w×h block that fits, preferring
the requested column. Resize-grow + drag-drop swap now reliably
produce no-overlap layouts because the placer cleans up after them.
x+w > GRID_COLUMNS is clamped in the placer instead of rendered as an
overflow — matches the validator's hard rule on the wire.
Adds 14 dashboard-grid.test.ts regressions covering the mixed-width
swap, resize-grow shifting siblings, multi-row widgets, and the
overflow clamp. Pure tests — no DOM.
Three regressions / gaps on newton's just-shipped Slice B+C addressed.
- **Drag/drop reorder**: rebuilt on a single proper 12-col grid (newton's
implementation had per-row containers which blocked cross-row drops + the
swap heuristic only handled adjacent same-size cells). Drop hit detection
now works across the entire grid; recalc step uses real grid coordinates;
any widget moves anywhere, autosaves.
- **Resize**: bottom-right resize handle added (visible only in edit mode).
Snaps to valid 1x1 / 2x1 / 2x2 grid sizes; sibling widgets reflow on
resize; autosave via the same PUT /api/user/dashboard path.
- **Per-widget options expansion**: widget catalog entries now carry an
option schema (limits, position, content/view-type). Settings pane
renders the right controls dynamically per schema. Deadlines widget
exposes list / calendar / timeline-strip view picker; activity widget
full / compact toggle; etc.
No schema migration — option schema rides on the existing user_dashboard_layouts
jsonb. Backward-compat: legacy layouts (without per-widget options) hydrate
with catalog defaults.
Three issues from Slice B were entangled in the same root cause:
1. **Drag/drop reorder only swapped the first two same-size widgets.**
Widgets lived in two parents (.container + .dashboard-columns); the
old applyLayout used parent.appendChild per widget which physically
moved every .container widget to the END of .container — past the
.dashboard-columns row, edit-footer, and save-toast. Only the two
columns inside .dashboard-columns swapped visibly because they
shared a parent. Cross-row drags appeared to silently no-op.
2. **No resize affordance** — the design's per-widget sizing existed
only on paper.
3. **Per-widget options were thin** — count + horizon dropdowns only.
This change rebuilds the whole layout primitive on a single 12-column
CSS grid:
Backend (internal/services/):
- DashboardWidgetRef gains x/y/w/h grid coordinates. Validator clamps
against catalog MinW/MaxW/MinH/MaxH and rejects x+w > 12.
- WidgetDef gains DefaultW/H + MinW/MaxW/MinH/MaxH for the resize clamps.
- WidgetSettingsSchema gains Views ([{id,label_de,label_en}]), CountMax,
HorizonMax. Validator accepts free-form ints inside [1,CountMax] in
addition to dropdown presets, plus view-id against schema.
- WidgetCatalog wires views for upcoming-deadlines/-appointments (list,
calendar), inline-agenda (timeline, list), recent-activity (full,
compact), plus default sizes per widget.
- FactoryDefaultLayout greedy-packs visible widgets onto the grid,
tracking row-max height so taller previous neighbours never overlap.
Frontend:
- dashboard.tsx: every widget moved into a single .dashboard-grid
wrapper; matter-summary converted to a CollapsibleSection so it
participates in the grid like everything else.
- applyLayout rewritten — never moves DOM nodes; writes inline
grid-column / grid-row from computed placements. computePlacements
trusts explicit positions and auto-flows the rest with the same
rowMaxH-aware packer the backend uses.
- reorderViaDnd swaps (x, y) instead of array order; layout re-sorted
by (y, x) so the persisted array matches visual order.
- Resize handles in edit mode: bottom-right pointer-drag, cellW/cellH
derived from live grid metrics, snaps to grid + clamps to schema,
autosaves on pointerup. Native HTML5 DnD suppressed during resize.
- afterLayoutMutation now materialises every visible widget's
(x,y,w,h) so the spec stays self-describing — no mixed
explicit/auto-flow on next render.
- Gear popover expanded: view segmented control, custom count/horizon
numeric inputs alongside preset dropdowns, size (W/H) + position
(X/Y) spinners. Every visible widget gets a gear in edit mode.
- View-aware renderers:
- upcoming-deadlines / -appointments: list (default) or mini-month
calendar with item dots.
- inline-agenda: timeline (default) or flat list.
- recent-activity: full (default) or compact (one-line per row).
CSS:
- .dashboard-grid (12 cols, dense auto-flow); collapses to single
stack on narrow viewports.
- .dashboard-widget__resize handle (bottom-right diagonal stripes).
- .dashboard-widget__view-group segmented control.
- .dashboard-cal-* mini-calendar.
- .dashboard-activity-list--compact one-line variant.
- Grid items get card chrome via .dashboard-grid > .dashboard-section.
Tests:
- New: AcceptsCustomCountWithinMax, AcceptsValidView,
RejectsUnknownView, RejectsViewOnNoViewWidget, GridPosition,
GridSizeOutsideClamps, NoOverlap (greedy packer regression),
AssignsPositions.
- Updated: BadSettings now asserts a value above CountMax (free-form
values inside [1,CountMax] are valid; presets stay valid too).
Backwards-compatible: a stored layout without x/y/w/h still loads — the
client's auto-flow placer puts widgets into a clean single column until
the user customises. The first drag / resize / settings tweak
materialises all positions so subsequent renders are deterministic.
Final slice of the configurable dashboard. Catalog expansion + firm-wide
default propagation.
- mig 117 paliad.firm_dashboard_default — single-row firm-wide factory
layout, editable by global_admin. New users hydrate from this; existing
users get 'reset to firm default' option alongside the existing
'reset to factory'.
- Catalog expansion: pinned-projects widget brought live (C0 pin-machinery
prerequisite shipped inline); plus 2-3 high-value adds per design
catalog (recent-deadlines-by-type, my-open-approvals, etc.).
- Frontend: admin '/admin/dashboard-default' page to edit the firm shape;
user-side 'Reset auf Firmenstandard' link in the dashboard reset flow.
m/paliad#46 fully shipped (Slices A + B + C).
Three additions on top of Slice B's edit-mode chrome.
**Catalog expansion (2 new widgets, default-hidden — opt-in via picker):**
- pinned-projects: surfaces a list of the user's pinned matters via the
pre-existing PinService (mig 062/063, pre-dates t-paliad-219). New
DashboardService.loadPinnedProjects joins paliad.user_pinned_projects
to paliad.projects under the standard visibility predicate, preserves
pinned-at-DESC order, capped at PinnedProjectsCap=20. PinnedProjects
[]PinnedProjectRef grows DashboardData; SetPinService wired
post-construction to mirror the SetApprovalService pattern.
- quick-actions: pure UI affordance with three buttons linking to the
existing /projects/new, /deadlines/new, /appointments/new routes. No
backend payload, no settings schema.
Both default-hidden — m's brief asked for "high-value adds"; injecting
new widgets into every user's dashboard unannounced would be loud.
Factory test relaxed: visibility now matches catalog.DefaultVisible
instead of the previous "all-visible" invariant.
**Firm-wide admin default (mig 117 + new service + 4 endpoints):**
- paliad.firm_dashboard_default: single-row table (id smallint PK CHECK
id=1) with layout_json + updated_by + updated_at. RLS: SELECT
authenticated, no INSERT/UPDATE policy (writes go through the
service-role connection behind the adminGate).
- FirmDashboardDefaultService Get/Set/Clear. Validates against the
catalog on Set so an admin can't seed an invalid layout.
- DashboardLayoutService.SetFirmDefaultService wires in the firm
source. Both GetOrSeed and ResetToDefault now prefer the firm
default over the code-resident FactoryDefaultLayout when one is set.
Nil-safe — empty firm row falls back to the factory layout, transient
DB errors fall back too (a blip can't strand a user without a
dashboard).
- HTTP: GET / PUT / DELETE /api/admin/firm-dashboard-default (admin-
gated). POST /api/me/dashboard-layout/promote: admin convenience —
reads the admin's own current layout and stashes it as the firm
default (saves the JSON-editor step; admins edit via /dashboard's
normal editor, then click Promote).
**Frontend (Slice B's edit-mode footer grew an admin button):**
- "Als Firmen-Standard speichern" button in the edit footer; hidden via
CSS-inline until syncPromoteButtonVisibility unhides for
global_admin. Confirm() → POST /promote → toast.
- The existing "Auf Standard zurücksetzen" copy stays the same — the
semantics now "firm default if set, else factory", which is the
desired surface: users see one canonical "Standard" link.
i18n: 13 new keys × DE+EN (dashboard.pinned.*, dashboard.quick.*,
dashboard.edit.promote*). i18n-keys.ts regenerated by build.
m/paliad#46.
go build ./... clean; go vet ./... clean
go test ./internal/... clean (Slice C catalog test + factory-default
test relaxation; FirmDashboardDefault round-trip tests gated on
TEST_DATABASE_URL)
Migration 117 dry-run: PASS (other dry-run failures are pre-existing
local-DB collisions on origin/main; mig 117 itself clean)
bun run build clean: dashboard.html carries new section markup + admin
button; dashboard.js bundles renderPinnedProjects + promote handler
+ all new i18n keys
Second slice of the configurable dashboard. Adds the user-facing edit-mode
on top of Slice A's storage + factory render.
- 'Anpassen' toggle button in the dashboard header — off by default.
- Drag handles + x + + buttons appear on widgets when edit mode is on;
invisible otherwise so the reading-only path stays clean.
- Per-widget settings (counts + horizon dropdowns) per widget catalog.
- 12-col grid drag/drop reorder; mobile fallback to single column with
drag-by-handle.
- Autosave 400ms debounced via PUT /api/user/dashboard.
- Reset-to-default link to revert layout to the factory shape.
Frontend-only slice. Net 5 files, +1027/-3 LoC (most of it in
client/dashboard.ts + the new CSS block).
Slice C (catalog expansion + admin firm-wide default) remaining.
Adds the user-facing dashboard customization UI on top of Slice A's
backend (already shipped). Off by default — view-mode DOM and behavior
are byte-identical to the factory render.
Anpassen toggle in the dashboard header flips body.dashboard-editing.
When on, every [data-widget-key] grows a chrome strip with drag handle,
↑/↓ keyboard reorder buttons, hide/show button, and ⚙ gear for widgets
with a settings schema. An edit footer below the activity widget
surfaces "+ Widget hinzufügen" and "Auf Standard zurücksetzen".
Drag-and-drop uses native HTML5 DnD (dragstart / dragover / drop) on
the widget element itself. ↑/↓ buttons are the keyboard + touch
fallback. Hide flips Visible:false in the layout draft; re-showing via
the picker either un-hides in place or appends to the end if the
widget was never added.
Picker modal uses the unified openModal() helper (t-paliad-217). Each
catalog entry shows title + description + active/hidden/absent pill;
tapping an inactive entry mutates the layout and the list re-renders
in place so the user can multi-add.
Gear popover anchors absolutely inside the widget. Per-widget knobs
follow the catalog's WidgetSettingsSchema: count {1,3,5,10,20} for
list widgets, horizon_days {7,14,30,60} for upcoming-deadlines/-appoint-
ments, horizon-only {14,30,60} for inline-agenda, count {1,3,5,10} for
inbox. Selecting a value scheduleSave()s; close on outside-click / Esc.
Autosave: every layout mutation → snapshot rollback target +
400ms-debounced PUT /api/me/dashboard-layout. Success flashes a
"Gespeichert" toast (1.5s); failure rolls back, re-renders, and shows
"Speichern fehlgeschlagen". Reset link → confirm() → POST /api/me/
dashboard-layout/reset, replacing currentLayout with the factory
default returned by the service.
Mobile (≤32rem): toggle becomes full-width tappable, drag handle
hides in favor of ↑/↓ buttons (touch DnD is unreliable), picker uses
the existing modal full-screen breakpoint, toast spans the row.
Frontend-only — Slice A already shipped GET/PUT/POST /api/me/dashboard-
layout, GET /api/dashboard-widget-catalog, and the three-blob shell
hydration (data, layout, catalog). The client reads __PALIAD_DASHBOARD
_CATALOG__ inline; fetch fallback on hydration miss.
i18n: 23 new keys × 2 langs (DE + EN) for the toggle, picker, gear,
toast, and reset confirm. The i18n-keys.ts regenerates on every build.
m/paliad#46.
go build ./... clean
go vet ./... clean
go test ./internal/... clean (24 dashboard-layout/widget-catalog unit tests pass)
go test ./cmd/server/ -run TestBootSmoke: SKIPS without TEST_DATABASE_URL
(CI's clean test DB runs the boot-smoke gate)
bun run build clean: dashboard.html still carries the three placeholder
tokens; dashboard.js bundles the edit-mode code + i18n keys
Final slice. Discoverability + versioning on user-authored checklists.
- mig 116 paliad.checklists.version int NOT NULL DEFAULT 1 +
paliad.checklist_instances.template_version int (snapshot column).
Version bumps on template UPDATE; instance carries the version it was
created from.
- 'Geteilte Vorlagen' tab on /tools/checklists surfacing templates the
user can see via firm/global visibility + checklist_shares. Filter by
author / tag / visibility level. Popularity sort optional (deferred).
- Outdated-template badge on instance detail when
instance.template_version < template.version. Click → modal showing
the diff (template's new sections / items vs the snapshot).
- audit events: checklist_template_versioned emitted on each UPDATE.
t-paliad-225 / m/paliad#61 fully shipped (Slices A + B + C).
m/paliad#61 Slice C frontend pass.
Discovery (Geteilte Vorlagen):
- New 4th tab on /checklists between "Meine Vorlagen" and "Vorhandene
Instanzen". Filters the merged catalog response to authored entries
not owned by the caller (firm-visible OR globally-promoted OR
share-recipient). Tab state round-trips via ?tab=gallery.
- Regime filter pills (UPC / DE / EPA / OTHER) operate independently
from the main Vorlagen tab.
- Cards show regime badge, item count, author line, visibility chip.
- Self-filter relies on /api/me email match — loadMe() fires once on
page boot and is idempotent.
Versioning UI on /checklists/instances/{id}:
- "Vorlage aktualisiert" badge appears when the instance's
template_version is known AND lags the live template version (only
for authored templates; static templates never bump). Shows "v{from}
→ v{to}" delta.
- "Änderungen anzeigen" button opens a diff modal that compares the
instance's template_snapshot against the live template body.
Item-level grouping by (section title, item label). Surfaces added /
removed / changed items with localised section labels. Empty state
when only metadata changed.
i18n: 13 new keys per language (DE + EN) under
checklisten.tab.gallery, checklisten.gallery.*, checklisten.filter.other,
and checklisten.instance.{outdated,diff}.*. Total 2666 keys.
Build hygiene: bun run build clean; i18n scan clean. Go build/vet/test
+ TestBootSmoke ./cmd/server/ all green.
m/paliad#61 Slice C backend.
Schema (mig 116, idempotent):
- ALTER paliad.checklists ADD COLUMN version int NOT NULL DEFAULT 1.
Pre-Slice-C rows default to 1 (the column was added with DEFAULT
so the UPDATE clause is a no-op safety net).
- ALTER paliad.checklist_instances ADD COLUMN template_version int.
NULL on existing rows — instance detail page leaves the "outdated"
badge off when the snapshot version is unknown.
Services:
- ChecklistTemplateService.Update — version bumps on title/body
changes (the meaningful edits that warrant notifying instance
owners). Pure metadata tweaks (description/court/reference/deadline)
update updated_at without bumping. Emits the new 'checklist.versioned'
audit event with prior_version + new_version metadata.
- ChecklistInstanceService.Create — captures snapshot_version
alongside the body snapshot.
- ChecklistCatalogService — CatalogEntry grew a Version field
(1 for static; live column for authored). ListVisible / Find
populate it.
- Models — Checklist.Version int; ChecklistInstance.TemplateVersion *int.
- /api/checklists/{slug} response now includes version so the
instance detail page can compare against the snapshot.
Migration verified live via BEGIN..ROLLBACK against paliad.checklists
and paliad.checklist_instances.
Build hygiene: go build/vet/test ./internal/... + TestBootSmoke
./cmd/server/ all green.
m/paliad#61 Slice B frontend pass.
Detail page (/checklists/{slug}) gains:
- Provenance line ("Erstellt von <author>") for authored templates,
populated from the catalog response's owner_display_name.
- Owner action buttons: Bearbeiten (links to
/checklists/templates/{slug}/edit per the Slice A hotfix), Teilen,
Löschen. Reveal driven by /api/me email match against the catalog
response's owner_email.
- global_admin action buttons: "Als Firmen-Vorlage hinterlegen"
(promote) when visibility != 'global'; "Aus Katalog entfernen"
(demote) when visibility == 'global'. Reveal driven by /api/me
global_role.
Share modal:
- Single modal with a kind-picker (Kollege / Office / Dezernat /
Projekt) and a matching select per kind — sections toggle on the
active kind.
- Recipient pickers populated from /api/users, /api/partner-units,
/api/projects (loaded in parallel on open). Office options use the
canonical 8-key set from internal/offices.
- Existing grants surface in a list under the form with per-row
Entfernen buttons; Revoke confirms before DELETE.
- Errors surface inline (recipient-required, generic share failure).
i18n: 32 new keys per language (DE+EN) under checklisten.share.*
and checklisten.detail.promote/demote/delete.*. Total 2653 keys.
Build hygiene: go build/vet/test ./internal/... + ./cmd/server/ all
green; bun run build clean.
m/paliad#61 Slice B backend. Implements the explicit-share path
(checklist_shares + visibility predicate extension) and the
global_admin-only promotion / demotion of authored templates to and
from the firm catalog.
Schema (mig 115, idempotent):
- paliad.checklist_shares (uuid id, checklist_id FK, polymorphic
recipient via xor-check: recipient_kind in {user, office,
partner_unit, project} with exactly one matching recipient_* column
populated; granted_by FK; granted_at)
- Hot-path lookup index + per-kind partial UNIQUE indexes prevent
duplicate grants
- RLS: SELECT owner OR self-recipient (user-kind) OR global_admin;
INSERT owner-only with granted_by=self; DELETE owner OR global_admin;
no UPDATE (revoke = DELETE)
- can_see_checklist CREATE OR REPLACE — adds 4 share branches; project-
share branch uses inline ltree walk over projects.path because
can_see_project reads auth.uid() (NULL on service-role connection,
same pattern as visibility.go)
- xor-check verified live: rejects kind='user' with recipient_office
set; accepts the matching kind/recipient pair
Services:
- ChecklistShareService — Grant (owner-only, validates recipient kind +
required FK target, friendly 409 on partial-unique-index conflict),
Revoke (owner or global_admin), ListGrants (owner or global_admin;
enriches recipient_label via LEFT JOINs)
- ChecklistPromotionService — Promote (global_admin → visibility=global
+ promoted_at/by + audit), Demote (global_admin → target visibility,
default 'firm', clears promoted_at/by; rejects demote of non-global
rows)
- ChecklistCatalogService.checklistVisibilityPredicate extended to
include all 5 share branches; service-role-friendly (no auth.uid())
- ChecklistTemplateService.normaliseSliceAVisibility now accepts
'shared' as an author-set value; 'global' stays admin-only
Endpoints:
- GET /api/checklists/templates/{slug}/shares — list grants (owner/admin)
- POST /api/checklists/templates/{slug}/shares — grant
- DELETE /api/checklists/shares/{id} — revoke
- POST /api/admin/checklists/{slug}/promote — promote to global
- POST /api/admin/checklists/{slug}/demote — demote (body.target default 'firm')
Audit (paliad.system_audit_log):
- checklist.shared — recipient_kind + recipient_id in metadata
- checklist.unshared — same shape, captured pre-DELETE
- checklist.promoted_global — prior_visibility + owner_id
- checklist.demoted — target_visibility
Tests: validateShareInput covers all 4 kinds (happy + missing-id);
predicate-shape test asserts all 6 visibility branches present;
pqUniqueViolation regex sniff; nullableString helper; SliceB visibility
opens 'shared' but keeps 'global' admin-only.
Hotfix-merge note: head shipped 794617c after Slice A — the
template-edit page route moved from /checklists/{slug}/edit to
/checklists/templates/{slug}/edit to disambiguate from
/checklists/instances/{id}. Slice B routes follow the safe
/<resource>/<noun>/{id} pattern (no new {slug}-then-verb endpoints).
Go ServeMux refused to register patterns 'GET /checklists/{slug}/edit' (from
dirac's Slice A merge b418705) and 'GET /checklists/instances/{id}' (existing)
because both match '/checklists/instances/edit'. Container crash-looped on
boot since 13:32 UTC; paliad.de returned 404 from Traefik because no app was
listening.
Renaming the new template-edit route to /checklists/templates/{slug}/edit
disambiguates — '/templates/...' is a literal segment so the {slug} is now
strictly under a fixed prefix that can't collide with 'instances'.
Touches:
- internal/handlers/handlers.go:257 — route pattern
- frontend/src/client/checklists.ts:290 — Bearbeiten link
- frontend/src/client/checklists-author.ts:52 — URL parser regex
- frontend/src/checklists-author.tsx — doc comment
go build + bun run build clean.
m/paliad#61 Slice A frontend pass.
Pages:
- /checklists gets a third tab "Meine Vorlagen" between Vorlagen and
Vorhandene Instanzen — lists owned authored templates with regime
badge, visibility chip, Bearbeiten / Löschen actions, "Neue Vorlage"
CTA. Tab state round-trips via ?tab=mine.
- /checklists/new and /checklists/{slug}/edit serve a shared bundle
(checklists-author.html). Client reads location.pathname to decide
create vs edit mode; edit mode prefills from /api/checklists/templates/mine.
Wizard:
- Metadata form (title, description, regime UPC/DE/EPA/OTHER, court,
reference, deadline, language de/en, visibility private/firm).
- Repeating section + item editor — add/remove sections, add/remove
items per section, label + optional note + optional rule per item.
- Single-language authoring (lang column on paliad.checklists). The
catalog read layer mirrors the title/description onto both DE and EN
sides so the existing bilingual frontend renders without a special
case for authored entries.
- Save POSTs (create) or PATCHes (edit) the template; visibility flip
on edit goes through its own endpoint so the audit row captures the
transition.
Merged catalog:
- /api/checklists now returns the merged list (static + DB visible);
the Summary shape gained origin / visibility / owner_email /
owner_display_name fields.
i18n: 55 new keys per language (110 total) under
checklisten.tab.mine.*, checklisten.mine.*, checklisten.author.*,
checklisten.detail.* (Bearbeiten/Löschen labels for Slice B). i18n
codegen total: 2621 keys.
Build hygiene: bun run build clean, go build clean, go vet clean,
go test ./internal/... + ./cmd/server/ all green.
918-line design doc covering all three capabilities from m/paliad#61:
authoring, multi-axis sharing, admin-promotion to global.
Load-bearing premise correction: the issue body claims `paliad.checklists`
is an existing table that gets new columns. It is NOT — checklists today
are static Go data in `internal/checklists/templates.go`. Design
introduces `paliad.checklists` from scratch and keeps the static catalog
as a parallel source via a hybrid catalog read layer.
Schema (mig 112): `paliad.checklists` (owner + visibility enum), `paliad.checklist_shares`
(polymorphic recipient: user/office/partner_unit/project),
`paliad.can_see_checklist` predicate, `paliad.checklist_instances.template_snapshot`
column for instance integrity under template edits.
12 decisions ledgered, all defaulted to (R) per task brief (no AskUserQuestion).
Three slices (A foundation, B sharing+promotion, C gallery+backfill).
Three calendar implementations consolidated into one. Custom Views' shape-calendar.ts
becomes the canonical renderer; /events Kalender tab and the orphaned
/deadlines/calendar + /appointments/calendar pages now use the same module.
- frontend/src/client/calendar/mount-calendar.ts — new canon module extracted
from shape-calendar.ts. Month/week/day, URL state via ?cal_view/?cal_date,
drill-down day view, kind-coded pills.
- /events Kalender tab folded onto mountCalendar(); the old modal popup
replaced with day-view drill-down (Q2/(R)).
- /deadlines/calendar + /appointments/calendar become 301 redirects to
/events?type=…&view=calendar (handlers test added to pin the targets).
- .frist-cal-* CSS block dropped (~180 lines). Dead i18n keys removed.
Net: ~700 LOC removed, ~100 added. Zero schema/endpoint changes. Same data-loader
shared across all surfaces. Single PR per Q7(R).
Adds TestStandaloneCalendarHandlers_RedirectToEventsKalender to
internal/handlers/redirects_test.go covering both standalone-
calendar handlers. Each must 301 to the canonical Kalender-tab URL
on /events, preserving the bookmark contract called out in the
handler doc comments. Sister of the existing sub-projects redirect
test.
Delete the four orphan files behind /deadlines/calendar +
/appointments/calendar:
- frontend/src/{deadlines,appointments}-calendar.tsx
- frontend/src/client/{deadlines,appointments}-calendar.ts
The standalone pages were unreachable from the UI since t-paliad-110
(Sidebar/BottomNav point at /events?type=…); their only role was as
bookmark targets.
Handlers in internal/handlers/{deadlines,appointments}_pages.go now
301-redirect to /events?type=…&view=calendar so bookmarks still
work. Route registrations in handlers.go remain unchanged — the
gate + redirect pair gives us the same URL surface with one canonical
renderer.
build.ts: drop the renderDeadlinesCalendar / renderAppointmentsCalendar
imports + entry-point bundle paths + dist HTML writes.
frontend/src/client/paliadin-context.ts: drop the two route-key
matches for the standalone URLs (the client never sees those
pathnames any more — 301 fires server-side).
Dead CSS pruned in frontend/src/styles/global.css (~180 lines):
- .frist-calendar, .frist-cal-{controls,month-label,grid,cell,…}
block (lines 7464-7613 pre-refactor)
- @media (max-width: 700px) { .frist-cal-cell { min-height: 64px; } }
- .termin-cal-legend{,-item}
- .frist-cal-popup-time
- .frist-cal-dot.events-cal-dot-appointment
All verified by grep across frontend/ + internal/ to have no
non-calendar consumers before deletion.
Dead i18n keys removed (DE + EN + i18n-keys.ts union type):
- deadlines.kalender.{title,heading,subtitle,list,today,empty}
- appointments.kalender.{title,heading,subtitle,list,empty}
- deadlines.list.calendar, appointments.list.calendar (button labels
on the deleted standalone routes)
- events.calendar.empty (replaced by cal.day.no_entries inside
mountCalendar's day view)
Per head decisions §11 Q1 + Q8 (drop standalone pages as 301s; drop
dead i18n now).
Tests: go build ./... clean; go test ./internal/... 9 packages pass;
cd frontend && bun run build clean (2535 i18n keys); bun test
frontend/src/client/{calendar,views}/ all 73/73 pass.
The /events Kalender view now mounts the canonical mountCalendar()
module from frontend/src/client/calendar/ — same renderer Custom
Views uses for shape=calendar. Drops the events-page-specific
month-grid + popup code path entirely.
What replaces what
- renderCalendar() / openCalPopup() / calDotClass / fmtMonthYear /
isoDate / itemDateISO and the calYear/calMonth module state →
one mountCalendar() handle (lazy, urlState=true).
- events-cal-prev / events-cal-next / events-cal-today buttons →
toolbar in mountCalendar (includes its own 'Heute' button).
- modal popup on cell click → drill-down to day view (matches
/views; head decision §11 Q2).
- @media min-height shrink on .frist-cal-cell → views-calendar-*
responsive surface (CSS unchanged from /views).
Behavioural deltas vs pre-refactor
- /events Kalender now persists view+anchor in ?cal_view + ?cal_date
(head decision §11 Q3) — refresh / share-link safe.
- Pills are kind-coded (deadline / appointment) rather than urgency-
coded; matches /views (head decision §11 Q4 — drop subtype dot
colouring, file as follow-up).
- Empty-month message gone; the per-day no-entries state from the
day-view replaces it (head decision §11 Q8 — drop dead i18n).
Adapter: toCalendarItem() preserves the pre-refactor bucketing rule
— deadlines bucket on due_date, appointments on start_at, both fall
back to event_date.
events.tsx: 31-line calendar subtree (toolbar + grid + modal +
empty hint) reduces to a single host div. mountCalendar fills it
when the user picks Kalender.
Lift the month/week/day renderer out of shape-calendar.ts into a new
frontend/src/client/calendar/mount-calendar.ts module so /events
Kalender (next commit) and Custom Views shape=calendar both go
through the same code path.
shape-calendar.ts becomes a thin adapter (ViewRow → CalendarItem +
defaultView=render.calendar.default_view, urlState=true). The
extracted module adds:
- update(items) on the returned handle so /events can re-mount on
filter changes without rebuilding state.
- destroy() for clean teardown when /events switches shapes.
- A 'Heute' button in the toolbar (cal.today, DE+EN added to i18n.ts
+ i18n-keys.ts).
- Optional opts.urlPrefix for surfaces that may share a URL with
another calendar.
mountCalendar reads ?cal_view / ?cal_date when opts.urlState=true.
/events will mount with urlState=true after the next commit so the
Kalender tab + day-view drill remain refresh-stable (per §11 Q3 in
the design doc).
Pure-helper test suite (mount-calendar.test.ts) covers isoDate,
startOfDay, startOfWeek, shift, bucketByDate, filterByDay, isToday —
12 assertions, all green. DOM rendering covered by manual smoke (no
jsdom in this repo's bun test setup; see verfahrensablauf-core.test.
ts comment for the convention).
Audit + refactor plan: three calendar implementations live today —
/events tab, standalone /deadlines|appointments/calendar pages, and
Custom Views shape-calendar.ts. Canonicalise on shape-calendar.ts by
extracting a shared mount-calendar.ts module, fold /events into it,
retire the standalone pages as 301 redirects, delete ~180 lines of
duplicated CSS.
Net: ~700 LOC removed, ~100 added, zero schema/endpoint changes.
8 open questions for head in §11; AskUserQuestion is disabled for this
task per role brief, so head answers via mai instruct and decisions
land in §12.
Completes t-paliad-223 (team & admin surface). Slice A (Project Admin role
+ inheritable role-edit) and Slice C (click-to-select) already merged at
111c7c3.
- SupabaseAdminService + AdminCreateUserFull — auth.users create via the
Supabase Admin API (requires SUPABASE_SERVICE_ROLE_KEY env, provisioned
on paliad's Dokploy compose by head 2026-05-20). Best-effort rollback
on paliad.users insert failure: deletes the auth row to keep state
clean.
- Welcome email with magic link sent on create when 'Send welcome email'
checkbox is on (default per Q2).
- POST /api/admin/users/full endpoint, gated on global_admin.
- Frontend modal on /admin/team — 'Add user' button alongside the
existing 'Invite colleague' / 'Onboard existing' actions.
- i18n keys for the new modal and toast feedback.
- Tests: happy path, duplicate-email refusal, paliad.users insert failure
with best-effort auth rollback.
t-paliad-223 fully shipped.
Two related issues bundled in one PR.
## #47 Client Role
- mig 112 widens projects.our_side CHECK with new sub-roles: Active
(claimant/applicant/appellant), Reactive (defendant/respondent),
third_party/other. Drops 'court' + 'both' (semantically odd; backfilled
to NULL).
- ProjectFormFields.tsx hides the field on type='client', 'litigation',
'patent'; shows 'Client Role' on type='case' with 7 grouped options.
- Submission template variable bag — ourSideDE / ourSideEN updated for
the new values. Determinator perspective inference: Active →
claimant-perspective, Reactive → defendant-perspective.
## #50 Auto-derived project codes
- mig 113 adds paliad.projects.opponent_code text on litigations (vs
brittle regex on title).
- New Go helper services/project_code.go: BuildProjectCode(ctx, projectID)
walks the ltree ancestor chain, derives <CLIENT>.<OPPONENT>.<PATENT>.<TYPE>.<COURT>
(each segment optional). Custom override via projects.reference still wins.
- Project JSON gets an eager 'code' field populated by the service (no
per-render lookups; one DB round-trip per list page).
- Rendered as a second header badge on /projects/{id} + in the parent-picker
typeahead so users see the auto code while organising the tree.
Both migrations land cleanly via the new gap-tolerant runner (boltzmann
c85c382). 376-line project_code_test.go covers the segment-derivation
matrix.
t-paliad-222 follow-up — wire .code into the parent-project picker so
two same-titled projects in different trees can be disambiguated by
their auto-derived dotted code. Search includes the code; the badge
renders only when distinct from the manual reference.
Excel __meta sheet still pending — the JSON code field is populated
by PopulateProjectCodes for every list payload, so the export
generator only needs to add one row in a follow-up shift.
t-paliad-222 follow-up — wire the .code field populated by
PopulateProjectCodes into the project-detail header. Shows next to
the manual reference when distinct, hidden when they match (avoid
duplication) or when no segments resolved. CSS `.entity-ref-code`
adds bracket-styling so the user knows the value is derived rather
than typed.
Also extends the frontend Project interface with code + opponent_code
to make TypeScript surface the new fields cleanly across consumers.
Implements m/paliad#47 (Client Role rework) + m/paliad#50 (auto-derived
project codes from the ancestor tree) in one shift.
Migrations:
- mig 112_client_role_rework: widen paliad.projects.our_side CHECK to
seven sub-roles (claimant / defendant / applicant / appellant /
respondent / third_party / other); drop legacy 'court' / 'both'
and backfill rows to NULL (no-op on prod, defensive on staging).
- mig 113_projects_opponent_code: add paliad.projects.opponent_code
text on litigation rows (slug pattern [A-Z0-9-]{1,16}); used as
the middle segment when assembling auto-derived project codes.
Backend:
- internal/services/project_code.go — new package-level helpers
BuildProjectCode (single row) + PopulateProjectCodes (bulk, one
CTE-based round-trip). Walks the existing paliad.projects.path
ltree; custom paliad.projects.reference on the target wins.
- Wired into ProjectService.List, GetByID, ListAncestors, GetTree,
LoadCounterclaimChildrenVisible, BuildTreeWithOptions — every
service entry-point that returns []models.Project / *models.Project
populates .Code before returning.
- Models: Project.OurSide doc widened; new Project.OpponentCode
(db:"opponent_code") and Project.Code (db:"-", projection-only).
- CreateProjectInput / UpdateProjectInput accept OpponentCode;
validateOpponentCode + nullableOpponentCode mirror our_side helpers.
- validateOurSide widens to the seven sub-roles; legacy 'court' /
'both' rejected at the service layer with a clear error before
the DB CHECK fires.
- derivedCounterclaimOurSide CCR flip widened: applicant ↔ respondent,
appellant → respondent; third_party / other / NULL pass through.
- submission_vars: project.code added to the placeholder bag.
ourSideDE / ourSideEN now use the gender-neutral "-Seite" /
"-Partei" suffix shape (Klägerseite / Antragstellerseite / ...);
better legal-prose default for a B2B patent practice, matches the
form labels which already used this shape (cf. head's soft-note on
Q4).
Frontend:
- ProjectFormFields: opponent_code on a new projekt-fields-litigation
block (hidden by default, shown when type=litigation); our_side
moved into projekt-fields-case and re-labelled "Client Role" /
"Mandantenrolle" with three <optgroup>s + seven options.
- project-form.ts: showFieldsForType toggles the new litigation
block; readPayload / prefillForm wire opponent_code; our_side
is now only emitted for type=case.
- fristenrechner: ourSideToPerspective widened to the seven sub-roles
(Active→claimant, Reactive→defendant, Other→null). ProjectOption
type literal updated.
- i18n.ts: new projects.field.client_role.* and
projects.field.opponent_code.* keys (DE+EN). Legacy
projects.field.our_side.* keys stay one release for cached
bundles + Verlauf event-history rendering of the new sub-roles.
Tests:
- TestProjectCodeSegment, TestAssembleProjectCode, TestPatentLast3,
TestSanitizeClientShort, TestProceedingTail, TestValidateOpponentCode,
TestValidateOurSideSubRoles pin the new pure helpers.
- TestOurSideTranslations widened to the seven sub-roles + new
prose shape; 'court'/'both' arms now return "" (legacy rejected).
- TestDerivedCounterclaimOurSide widened to the new flip map.
Migration slot history (this branch was rebumped twice on 2026-05-20):
mig 110 was claimed by m/paliad#51 (project_type_other, euler);
mig 111 was claimed by m/paliad#48 (project_admin_and_select, gauss).
Final slots 112 / 113.
go build && go test ./internal/... && cd frontend && bun run build
all clean.
Backend: mig 110/111 (will be renumbered after merging main),
validators + helpers widened, BuildProjectCode helper + projection
populator wired into List/GetByID/ListAncestors/GetTree/CCR. All
internal Go tests pass.
Frontend: ProjectFormFields conditional render — opponent_code on
litigation, our_side renamed to Client Role on case with grouped
optgroups. i18n keys for both DE and EN. fristenrechner perspective
mapping widened. project-form.ts payload reader/writer + showFieldsForType
toggle for new litigation block.
Migration slots about to be bumped (mig 110 was claimed by euler's
project_type_other on main).
Design doc for paired m/paliad#47 (Client Role rework) + m/paliad#50
(auto-derived project codes from the ancestor tree). Two migrations
(110 widen our_side CHECK + backfill court/both → NULL; 111 add
opponent_code on litigations), one new BuildProjectCode helper that
walks the existing ltree path, plus form / submission-template /
Determinator wiring.
9 open design questions surfaced for the head; recommendations
default to the issue-body (R) picks unless a material concern is
flagged in §2.2 / §3.2.
Verified against live data (2026-05-20): all 12 projects have
our_side=NULL, so the backfill is a no-op on production today.
No 'opponent' field exists yet.
m's 2-part feedback on the UPC Counterclaim-for-Revocation roadmap surface.
1. Backfill missing English label on the trigger event 'Widerklage auf Nichtigkeit'
→ 'Counterclaim for Revocation'. Handled in services/proceeding_mapping.go
(application layer; no corpus migration needed).
2. Generic 'spawn-as-standalone' renderer for sub-track proceeding types that
have no native rules but spawn under a parent flag (CCR under
upc.inf.cfi+with_ccr is the canonical case; the same pattern applies to
R.46 Amendment etc.). When picked standalone, the timeline now renders the
spawned rules with a contextual note explaining the normal parent context.
40 new unit tests in proceeding_mapping_test.go pin the renderer's standalone
detection + the EN label coverage.
m's 2026-05-20 14:08 reports on /tools/verfahrensablauf:
1. "There seems to be a lacking english term here" — picking
UPC CCR shows "Trigger event: Widerklage auf Nichtigkeit" on EN.
2. "Nothing shows in the roadmap" — the timeline is empty because
upc.ccr.cfi has no native rules (it's an illustrative peer that
normally runs as a sub-track of upc.inf.cfi with with_ccr).
Root cause for (1): UIResponse.proceedingName was DE-only. When a
proceeding had no root rule the frontend fell back to that field, so
EN users saw the DE label. The DB already has bilingual names; this
was pure plumbing.
Root cause for (2): the upc.ccr.cfi proceeding-type row exists for
the picker (mig 096) but ResolveCounterclaimRouting — the helper
that maps it to upc.inf.cfi with the with_ccr flag — was defined
but never called. Calculate queried rules directly off upc.ccr.cfi
and got an empty list.
Fix:
* Add ProceedingNameEN, ContextualNote, ContextualNoteEN to
UIResponse. Frontend triggerEventLabelFor now consults the EN
name on EN, falling back to DE only if the EN field is empty.
* New SubTrackRouting registry in proceeding_mapping.go and a
LookupSubTrackRouting lookup — single source of truth for the
"this proceeding has no native rules, route to a parent with
flags + show a contextual note" pattern. Today's only entry is
upc.ccr.cfi → upc.inf.cfi + with_ccr; the pattern generalises
to other sub-tracks via data-only additions.
* Calculate consults the registry at the top: when a hit, the
proceeding type is re-resolved to the parent for rule lookup, the
default flags are merged into the user's flag set (user flags win
on conflict), and the response identity (Code/Name/NameEN) stays
on the user-picked proceeding so the page header still reads
"Counterclaim for Revocation". The bilingual note surfaces in
ContextualNote{,EN}.
* Frontend renderResults paints a lime-accent banner above the
timeline body when the response carries a note
(.timeline-context-note). escHtml already exported from
views/verfahrensablauf-core — imported here for the banner.
No DB migration: SELECTs against paliad.proceeding_types,
paliad.deadline_rules, and paliad.trigger_events confirm every
active row already has a non-empty name_en / name. The bug was
the API + frontend never reading the EN columns through the
proceedingName fallback path.
Tests: TestSubTrackRoutings pins the registry shape (every entry
has matching key/value, non-empty parent+flags, bilingual notes;
CCR's exact shape is asserted; non-sub-tracks miss). The existing
TestResolveCounterclaimRouting continues to pass because the
helper now consults the registry but the CCR semantics are
unchanged.
Two slices on the team/admin surface. Slice B (Add User, m/paliad#49) is
parked pending m's go-ahead on the SUPABASE_SERVICE_ROLE_KEY credential.
## Slice A — Project Admin role (#48)
- mig 111 (renumbered from author's 110 to avoid collision with euler's
project_type_other mig 110 merged immediately prior).
- 'admin' added to project_teams.responsibility CHECK.
- New paliad.effective_project_admin(user_id, project_id) SQL function
walks the ltree path; sees admin on the row, on any ancestor, or
global_admin status.
- ChangeResponsibility service method + last-admin-on-tree safeguard.
- Frontend inline-select on the project team panel, gated on
effective_project_admin for the calling user.
## Slice C — Click-to-select (#53)
- /team gains a checkbox column + selection Set + sticky-footer
broadcast action.
- Selection survives filter changes; drop-out rows de-select; navigation
wipes selection.
- Empty-selection action falls back to the filtered set (no regression
vs. existing broadcast).
- No backend changes; pure frontend.
All builds + tests green.
#53 — adds an explicit selection layer ON TOP of the existing filter
pills on /team. Frontend-only; no backend changes, no migration.
- frontend/src/team.tsx: master "Alle sichtbaren auswählen" checkbox row above the team-list.
- frontend/src/client/team.ts:
- Module-scoped selectedUserIDs Set + renderedUserIDs DOM-order snapshot + lastToggledUserID for Shift-click range expansion.
- renderUserCard gains a per-row checkbox + data-selected attribute mirroring the Set.
- pruneSelectionToVisible(): every render() drops user_ids that no longer match the filter — invariant "selection ⊆ visible".
- wireSelectionCheckboxes() + applyRangeSelection() + refreshCardSelectedAttribute(): plain-click toggles one row, Shift-click extends a contiguous range using renderedUserIDs as the order reference.
- renderSelectionFooter(): fixed-position bar that mounts when selection > 0, hides when empty. Hosts the live "{n} ausgewählt" counter, a "Auswahl aufheben" reset, and an "E-Mail an Auswahl" button that calls openBroadcastModal with selectedRecipients() — reuses the existing modal verbatim.
- syncMasterCheckbox() + onMasterToggle(): tri-state master checkbox (empty / partial / full) for "select all visible".
- frontend/src/styles/global.css: .team-card[data-selected="true"] highlight, .team-card-select checkbox cell, .team-select-master-row, .team-selection-footer (z-index 150 — above mobile bottom-nav at 100, well below modal overlays at 1000+).
- i18n: +10 keys (team.selection.{count,clear,send,select_all,toggle_card}) × DE + EN.
Design picks honoured: surface=/team not /admin/team (Q1), checkbox column not modifier-key (Q2), sticky footer not always-visible (Q3), drop-out de-selects on filter change (Q4), fallback to filtered set when selection empty preserved by leaving the existing top-bar broadcast button intact (Q5), wipe on navigation since the Set is module-scoped in-memory only (Q6).
bun run build clean (2543 i18n keys, data-i18n scan clean). go build + go test -short ./internal/... unchanged (no backend touched).
#48 — adds 'admin' as fifth project_teams.responsibility value, plumbs an
inheritable role-edit gate via the materialised ltree path.
- migration 110: ALTER responsibility CHECK, CREATE paliad.effective_project_admin(uuid,uuid) STABLE SECURITY DEFINER (mirrors can_see_project shape), REPLACE project_teams_update / _insert / _delete RLS policies. Idempotent + down-mig provided. Dry-run BEGIN..ROLLBACK clean on live supabase.
- services/approval_levels.go: ResponsibilityAdmin const + IsValidResponsibility extension. responsibilityOpensGate UNCHANGED — admin is orthogonal to the 4-Augen approval gate.
- services/team_service.go: ChangeResponsibility() with last-admin guard inside tx (counts admins on project + ancestor chain, excludes the row being changed). RemoveMember() also runs the guard when removing an admin row. New IsEffectiveProjectAdmin() driving the frontend affordance. legacyRoleFromResponsibility: admin → 'lead' (deprecated shadow column).
- services/project_service.go: ErrLastProjectAdmin sentinel mapped to 409 in writeServiceError.
- handlers/teams.go: new PATCH /api/projects/{id}/team/{user_id}. RLS-enforced; non-admins get 404 to avoid existence leakage.
- handlers/projects.go: GET /api/projects/{id} now wraps the payload with effective_admin bool so the frontend drives the inline-select affordance without a second round-trip.
- frontend/src/projects-detail.tsx + client/projects-detail.ts: admin appears as 5th option in 'Mitglied hinzufügen' dropdown. Team-list Rolle cell switches to an inline <select> for callers with effective_admin (read-only span otherwise). Optimistic PATCH with rollback on error (last-admin guard / 403 from RLS / etc.) surfaced as transient toast in #team-msg.
- i18n: +6 keys (admin label + admin.hint + 3 error toasts × 2 langs).
- tests: TestIsValidResponsibility now covers admin; new TestLegacyRoleFromResponsibility pins the mapping table.
go build && go test -short ./internal/... && bun run build all clean.
5 small cleanups bundled in one batch per m's 'group sensibly' guidance.
- #51 (projects): 'other' added as real project type via CHECK extension;
synthetic 'Empty' option dropped from the type filter. No NULL-typed rows
in prod today; backfill is a no-op.
- #52 (approvals): density-picker (Compact/Comfortable) active state uses
brand accent #c6f41c. Sourced from a CSS variable so future surfaces
inherit.
- #54 (events): broken 'From Today' appointment filter dropped (frontend
was sending status=upcoming with no matching backend case). Default for
appointments is now today's bucket; 'Alle (auch vergangene)' stays as
the explicit opt-in.
- #56 (deadlines): event type renders before rule; the two are bundled as
a single 'Verfahrenshandlung' visual block on display (Event Type — Rule).
Form-level keeps separate inputs but visually grouped.
- #60 (a11y): label htmlFor=trigger-event dropped — the target was a
<span>, which isn't labelable; the warning surfaced in Chrome Issues tab.
m/paliad#60 (t-paliad-221) — Chrome's Issues tab flagged a label/for
violation on the timeline wizard: <label for="trigger-event"> pointed
at a <span> showing the selected trigger event name. <label for=…>
must target a labelable form control (input/select/textarea/…), never
a span; the browser strips the association and a11y tooling sees a
dangling reference.
Audit found two occurrences — verfahrensablauf.tsx and fristenrechner.tsx
both use the same wizard markup. Switch both captions to plain
<span class="date-label">; the .date-label rule already targets by
class only, so visual styling is unchanged. No other label-for
mismatches surfaced (194 label-fors scanned across frontend/src).
m/paliad#56 (t-paliad-221) — the deadlines editor read Title → Rule →
Event Type, which inverted the conceptual hierarchy (rule is the
citation under an event type, not its peer). Reorder all three
surfaces so the event-type parent comes first and the rule sits
directly beneath it.
- deadlines-new.tsx: pull the Regel select out of the Due-date row and
drop it directly under the Typ picker; Due becomes its own row below.
- deadlines-detail.tsx: swap the Typ and Regel <dt>/<dd> rows in the
detail list.
- approval-edit-modal.ts: remove rule_code from the generic
DEADLINE_FIELDS list and render it inside a new
"Verfahrenshandlung (Typ + Regel)" section beneath the event-type
picker. The shared per-field renderer is extracted so the bundled
section reuses the same dirty-tracking / pre_image-hint wiring.
- New i18n key approvals.suggest.section.event_type_rule (DE/EN).
Form-level inputs stay independent (some rules attach to multiple
event types and vice versa) — the change is purely about visual
grouping and reading order.
m/paliad#54 (t-paliad-221) — fix 92780cf added a status=upcoming option
for appointments and made it the default, but DeadlineFilterUpcoming
only narrowed deadlines. The appointment query had no matching case, so
the bucket fell through to the unfiltered path and past events leaked
into "Ab heute" / "From today".
- Drop the 'upcoming' option from STATUS_OPTIONS_APPOINTMENT — confusing
label that never delivered.
- Default appointments to the 'today' bucket (matches the dashboard
tile; sane lawyer-relevant view).
- Keep 'Alle (auch vergangene)' as the explicit opt-in at the bottom
of the list.
- Defensive backend fix: map DeadlineFilterUpcoming to start_at >= today
in bucketAppointmentWindow so any persisted ?status=upcoming bookmarks
stop leaking past events.
m/paliad#52 (t-paliad-221) — the Compact/Comfortable segmented control
on /approvals was rendering its active pill in plain --color-surface
(white in light mode, midnight-tinted in dark). Switch to the brand
lime so the segmented controls speak the same primary-action language
as the rest of Paliad.
Introduces three semantic tokens (--color-segment-active-bg / fg /
border) so any future surface that adopts .filter-bar-segment
inherits the same accent treatment without a CSS rewrite. The tokens
resolve to --color-accent / --color-accent-dark in both themes,
keeping the midnight foreground WCAG-AA on lime.
m/paliad#51 (t-paliad-221) — the type chip filter on /projects used to
treat unclassified projects as a synthetic "Empty" bucket. Make 'other'
a first-class projects.type value so every row carries a meaningful
label and the filter UI stops needing a NULL/Empty shim.
- mig 110: extend projects.type CHECK to include 'other'; backfill any
NULL rows defensively (production query confirmed zero, but the
NOT NULL constraint isn't load-bearing once the IN-list changes).
- Go: add ProjectTypeOther constant; isValidProjectType + humanProjectType
recognise it; handler doc lists 'other' in the ?type whitelist.
- Frontend: new chip in the projects.tsx type filter, new option in the
Create-Project form, DE "Sonstiges" / EN "Other" labels for the
projects.type and projects.chip.type i18n families.
Also drops a stray data-i18n-text attribute on the existing 'project'
chip checkbox (it had no consumer in i18n.ts and the surrounding markup
was nesting a <span> inside an <input>).
m's 4-part feedback bundled in one PR:
1. Pre-selected project carries through 'Add' on both Pathway A (Save modal) and
Pathway B (card-calc Add).
2. 'Custom' prefix stripped from all four adhoc proceeding-type chips (DE + EN).
3. 'Ich möchte etwas einreichen' option removed from 'Was ist passiert?' picker
via HIDDEN_CASCADE_ROOTS; future forward-workflow tool tracked in m/paliad#65.
4. Same-context-asked-twice on Statement-of-Defence picker: pill-click now locks
context inline (no duplicate 'Which context?' picker on top of the info list).
Four UX cleanups on /tools/fristenrechner per m's 2026-05-20 14:02–14:04
report:
1. **Pre-fill project on 'Add'** — when Step 1 binds an Akte, both the
Pathway A "Save to Project" modal and the Pathway B card-calc inline
'Add' picker now default their <select> to that project. Override
still allowed; the picker lists all projects. New helper
`preselectedProjectId()` reads `currentStep1Context` once so both
surfaces stay in sync.
2. **Drop 'Custom' prefix from UPC/DE/EPA/DPMA adhoc chips** — the
chip context already reads "oder ad-hoc, ohne Akte"; 'Custom' was
redundant signaling. Labels become "UPC-Verfahren" /
"UPC proceeding" (and the three sister jurisdictions).
3. **Remove 'Ich möchte etwas einreichen' from 'Was ist passiert?'** —
the Fristenrechner is a backward-looking calc ("event happened, what
spawns?"); the forward-workflow framing ("I want to file X") needs a
different tool. Filter the `ich-moechte-einreichen` root subtree out
in `loadEventCategoryTree()` (HIDDEN_CASCADE_ROOTS set) so the picker
never offers it. DB rows preserved for the future forward-workflow
tool, tracked in m/paliad#65.
4. **Same-context-asked-twice on Statement-of-Defence picker** —
when the user clicks a specific rule pill on a concept card, the
calc panel now renders a locked "Kontext: <proceeding — rule>"
caption with an "ändern" affordance instead of re-showing the same
five proceedings as a radio fieldset. When the user clicks the card
body (no specific pill), the picker is still the primary surface, but
the card's rule-pill section hides via CSS while expanded
(`fristen-card-pills-section--rules`) so the same options aren't
listed twice. Cross-cutting trigger pills (Wiedereinsetzung,
Weiterbehandlung etc.) stay visible — they're conceptually
different siblings, not the same proceeding context.
m's priority bug 2026-05-20: 'we cannot change the dates in that anymore!
the timeline dates — they seem to be fix, nothing happens when I click on a date.'
Regression introduced when the verfahrensablauf-core renderer was extracted
as the shared source of truth for both /tools/verfahrensablauf and
/tools/fristenrechner — the delegated click handler that opens the inline
date-edit modal was wired on the Fristenrechner side but never re-attached
on the Verfahrensablauf side. Anchor overrides + editable:true flag were
not threading through.
Fix: thread anchorOverrides + editable:true through CardOpts into the
shared renderer; wire the delegated click handler on
/tools/verfahrensablauf; pin the editable → data-rule-code contract with
5 regression tests so this can't re-break silently.
Per-rule due dates on /tools/verfahrensablauf were rendered as plain
spans with no `frist-date-edit` attrs and no delegated click handler,
so clicking a date did nothing (m's "the timeline dates seem to be fix,
nothing happens when I click on a date"). The wiring existed on
/tools/fristenrechner but had never been mirrored onto the abstract-
browse surface introduced in t-paliad-179.
Fix: lift the inline date editor + delegated click wiring out of
fristenrechner.ts into views/verfahrensablauf-core.ts so both pages
share one implementation:
- openInlineDateEditor(span, onCommit) — swaps the date span for
a `<input type=date>`, commits on blur/Enter, cancels on Escape,
fires `onCommit(ruleCode, newValue)` ("" = revert).
- wireDateEditClicks(container, onCommit) — idempotent delegated
click + keyboard handler that resolves `.frist-date-edit
[data-rule-code]` and opens the editor. Survives innerHTML
rewrites because the listener lives on the container.
verfahrensablauf.ts now:
- Owns its own anchorOverrides Map (cleared when proceeding-type
changes — overrides for one proceeding don't apply to another).
- Forwards overrides in calculateDeadlines() so downstream rules
re-anchor on the user's date.
- Passes `editable: true` to renderColumnsBody + renderTimelineBody.
- Calls wireDateEditClicks() once on #timeline-container in
DOMContentLoaded.
fristenrechner.ts shrinks: openInlineDateEditor + the inline click /
keydown blocks are replaced by an `onDateEditCommit` callback handed
to the shared wireDateEditClicks(). No behaviour change there.
Regression test: views/verfahrensablauf-core.test.ts pins the
editable→`data-rule-code` contract on `deadlineCardHtml` so a future
refactor that drops the attrs fails loudly instead of silently
breaking click-to-edit on both pages.