Commit Graph

382 Commits

Author SHA1 Message Date
m
0587fc2296 Merge: t-paliad-119 — Fristenrechner shift-reason explainer (UPC vacation, holiday name, weekend) 2026-05-04 18:37:17 +02:00
m
d688ebde90 feat(t-paliad-119): explain WHY a Fristenrechner deadline was shifted
The current "Wochenende/Feiertag" / "weekend/holiday" label hides the cause
of long shifts — m's reproduction had a deadline jump from 4.8.2026 to
31.8.2026 (+27 calendar days) across UPC Summer Vacation, and the UI made
it look like a bug. The math was correct; the explanation was lying.

Backend:
- AdjustForNonWorkingDaysWithReason returns an AdjustmentReason alongside
  the adjusted date. Walks the same 60-iter loop, classifies the dominant
  cause (vacation > public_holiday > weekend), collects every named
  holiday hit, and for vacations scans outward to report the contiguous
  block boundary (27.7.–28.8., not the 25 individual rows).
- AdjustForNonWorkingDays now wraps the new method, preserving its
  3-tuple signature for existing callers (deadline_calculator,
  event_deadline_service).
- UIDeadline gains an AdjustmentReason field; FristenrechnerService
  populates it on every shifted deadline.
- Date fields serialise as YYYY-MM-DD strings (HolidayDTO + string
  vacation span) — the Fristenrechner client already speaks that format.

Frontend:
- AdjustmentReason → human-readable phrase via renderAdjustmentReason:
    vacation       → "{vacation_name} ({span})"
    public_holiday → "Feiertag ({first_holiday_name})" / "{name} holiday"
    weekend        → "Wochenende" / localised weekday
- Surrounding format becomes "Verschoben wegen X: A → B" (DE) or
  "Shifted (X): A → B" (EN). Falls back to the legacy reason string
  when the backend hasn't sent a structured reason.
- Vacation names render verbatim from paliad.holidays — no hardcoded
  i18n mapping for individual closures (those rotate via the seed, not
  via i18n.ts).

Tests cover the three Kind paths plus the no-shift case; UPC vacation
test injects the migration-010 seed into the cache so the assertion
runs without a live DB.

Out of scope (raised in conversation, deferred):
- Whether "UPC Summer Vacation" / "UPC Winter Vacation" are the right
  names for the seeded rows, and whether the winter block belongs in
  paliad.holidays at all (m flagged this as BS while reviewing the
  task — needs a data-side decision before renaming/removing).
- holidays.country isn't filtered by proceeding-type jurisdiction, so
  UPC vacation currently shifts EP_GRANT / EPA_APP / German national
  deadlines too. Bigger fix; flagged for a follow-up issue.
2026-05-04 18:31:55 +02:00
m
6940c1e030 Merge: t-paliad-120 — /events Termin-Typ chip dark-mode color fix 2026-05-04 18:28:01 +02:00
m
f79dbdba4a fix(t-paliad-120): /events Termin-Typ chip — dark-mode text invisible
Add dark-mode rules for `.termin-type-chip.termin-type-XYZ`. Without them
the chip inherited the bare `.termin-type-XYZ` swatch rule (line 8407+),
which intentionally paints text the same colour as the background for
the dot/border-left swatch use case → text invisible in the chip context.

Mirror the existing badge dark rules (lines 8413-8415) so chip and badge
share the same colour family. Adds the missing `consultation` variant for
the chip (badge consultation dark is also missing but out of scope).
2026-05-04 18:25:59 +02:00
m
ecfd62e330 Merge: t-paliad-118 — /admin/email-templates Verfügbare Variablen single-column stack 2026-05-04 18:13:12 +02:00
m
7581444cd4 Merge: t-paliad-117 — /events filter polish (i18n leak + label position + panel anchoring) 2026-05-04 18:08:20 +02:00
m
e9e445fddf fix(t-paliad-118): /admin/email-templates Verfügbare Variablen — single-column stack
The `.admin-et-variables-list` was a 4-track CSS grid designed for flat
name/type/desc/sample siblings. The renderer wraps each variable in a
`.admin-et-variable-row` div, so each variable became a single grid item —
and 4 rows packed into one visual row, producing the wide multi-column
overflow.

Switch the outer container to `flex-direction: column` (one variable per row)
and the row to a baseline-aligned wrapping flex (name, type pill, desc, sample
left-to-right). `margin-left: auto` pushes the sample right when there's room
and lets it wrap below on narrow widths.

Pre-existing dark-mode issue (`.admin-et-variable-name` hardcoded to #1c1917,
unreadable on dark background) is unchanged by this fix and predates the
layout regression — flagged separately, not in scope here.
2026-05-04 18:08:06 +02:00
m
5875a62aba fix(t-paliad-117): /events filter polish — i18n leak, label position, panel anchoring
Three small polish bugs on the unified /events filter row.

A) i18n leak on lang change. The static [data-i18n] options in the appointment-
type select are retranslated by initI18n's applyTranslations(), but the project
select is rebuilt at runtime via populateProjectFilter() and the multi-select
trigger label is set via t() inside attachEventTypeMultiSelectFilter. Neither
re-ran on lang change, so DE→EN→DE left "All matters" / "All types" stuck on
the page. Wire onLangChange:
  - events.ts: re-call populateProjectFilter() inside the existing handler.
  - event-types.ts: attachEventTypeMultiSelectFilter now subscribes to
    onLangChange itself and re-renders updateLabel() + (when open) renderPanel().
    Self-contained side effect — also fixes /agenda's multi-select for free.

B) Filter caption above the field. The pre-existing .filter-row + .filter-group
pattern (already used on /projects) had .filter-group laid out horizontally
(label-left, select-right). Wrapped each events.tsx label+control pair in
.filter-group and switched .filter-group to flex-direction: column with a 4px
gap, so the caption sits above the select / button trigger. The whole filter-
row stays a horizontal flex-wrap of column cells. Mobile (≤480px) still falls
back to a single-column stack. /projects gets the same polish since it already
used the same wrapper pattern.

C) Multi-panel anchoring. The event-type popover was rendered as a sibling of
the trigger inside .filter-row and absolute-positioned with auto/auto, so it
landed wherever the wrapping flex layout put it (~226px above the trigger in
practice). Wrapped trigger+panel in <div class="multi-anchor"> with
position: relative and pinned the panel via top: 100%; left: 0 — only when
inside .multi-anchor, so /agenda's existing column-flex auto-positioning is
untouched.

toggleFilterPair simplified to hide the wrapping .filter-group via closest()
instead of toggling the label and control separately.
2026-05-04 18:02:26 +02:00
m
0ead001811 fix(dashboard,events): drop "Termine auf einen Blick" rail + fix multi-panel overflow leak
m's call (2026-05-04): the Dashboard's secondary "Termine auf einen
Blick" rail (3 cards) was redundant — the upcoming-Termine list lives
right below it on the same page, and the Fristen rail above is what
matters for the at-a-glance read. Drop the cards.

frontend/src/dashboard.tsx: remove the <section
aria-labelledby="dashboard-appointment-summary-heading"> and its 3 cards.

frontend/src/client/dashboard.ts: drop renderAppointmentSummary +
the AppointmentSummary type + the appointment_summary field on
DashboardData (kept the API-side payload — other consumers may use it
later; just stop wiring the dashboard to it).

Also two related event-page styling bugs:

- frontend/src/client/events.ts:721 was force-stamping
  `style.display = "block"` on the event-type multi-panel popup whenever
  the type filter was anything but appointment. The panel is supposed
  to be a hidden flyout owned by the trigger button via `panel.hidden`;
  the inline display:block trumped the `.multi-panel[hidden]` CSS rule
  and left it visible on larger screens (m flagged the
  `<div ... hidden="" style="display: block;">` artefact). Fix: never
  set inline display from this code path; force-close the panel only
  when switching to appointment view.

- frontend/src/styles/global.css `.multi-list` + `.multi-option`: long
  event-type labels overflowed the 22rem panel horizontally because
  `.multi-list` only had `overflow-y: auto` and the flex options had no
  `min-width: 0` / overflow-wrap rules. Add `overflow-x: hidden` +
  `min-width: 0` on the list and `overflow-wrap: anywhere` on options.
2026-05-04 17:08:34 +02:00
m
c09150e744 Merge: t-paliad-116 — event_deadlines i18n follow-up (title_de + notes_en) 2026-05-04 17:04:19 +02:00
m
4e1213fbd1 fix(t-paliad-116): event_deadlines i18n follow-up — title_de backfill + notes_en
Two adjacent i18n leaks in /tools/fristenrechner "Was kommt nach…" mode,
same pattern as t-paliad-112's deadline_rules fix but on event_deadlines:

A) title_de empty for all 70 rows. The DTO already falls back to title
   silently, so DE locale rendered English titles ("Statement of Defence",
   "Decision of the EPO", …). Backfilled via mig 035 with UPC RoP DE
   terminology that matches the trigger_events.name_de translations from
   mig 033, so the picker label and the deadline row read the same.

B) notes column carries English text on rows 50, 52, 70 (DE-named column
   was DE-only in spec, but seeds slipped EN strings through). Mig 036
   adds a parallel notes_en column following the t-112 mig 032 pattern,
   copies the existing English into notes_en, and replaces notes with
   proper DE for those three rows.

Render path:
- service: select notes_en, plumb through EventDeadlineResult.NotesEN
- frontend: getLang() === "en" ? (notesEN || notes) : notes (mirrors the
  proceeding-tree timeline branch already in fristenrechner.ts)
2026-05-04 17:03:58 +02:00
m
df2b2114df Merge: t-paliad-114 follow-up — SMEFee=2015 (reduced fee per Rule 7a(2)(a-d))
Head's fe8ba15 corrected the standard fee but kept SMEFee=2925 (collapsed
the tier). m's call: the data should reflect the actual reduced fee
(2.015€) even while the UI doesn't surface the tier yet. Taking fritz's
SMEFee=2015 over main's 2925.
2026-05-04 16:58:49 +02:00
m
2f44461275 fix(t-paliad-114): EPA Beschwerdegebühr 2.255€ → 2.925€ (2024-04-01 EPO restructure)
Standard appeal fee per Rule 6 EPC-Gebühren v2024-04-01 was raised from
2.255€ to 2.925€; reduced fee per Rule 7a(2)(a-d) was raised from 1.880€
to 2.015€. Both stale in calc.EPAFees, surfaced wrong amount on
/tools/kostenrechner and /tools/gebuehrentabellen EPA tab.

Reduced-fee tier is updated for data accuracy; UI surfacing of that tier
is deferred per m's call (out of scope for this task).
2026-05-04 16:57:58 +02:00
m
fe8ba15477 fix(t-paliad-114): update EPA Beschwerdegebühr to 2.925€ (post 2024-04-01)
The Kostenrechner showed EUR 2.255 for the EPA appeal fee, a stale
pre-restructure figure. Per Rule 6 EPC-Gebühren (version 2024-04-01)
the standard appeal fee is EUR 2.925 — applies to any legal person not
under Rule 7a(2)(a-d). The reduced fee of EUR 2.015 for natural persons
/ SMEs / non-profits / academic institutions / public-research orgs is
documented in a code comment but not surfaced separately yet — m said
keep it simple; ship the standard fee only.

SMEFee aligned to the same 2925 since we're not exposing a tier toggle.
Updated TestComputeEPAInstance_SME accordingly.

Both /tools/kostenrechner and /tools/gebuehrentabellen read from the
same EPAFees map, so this single edit fixes both surfaces.

Picked up from fritz's worktree — fritz hit a Claude usage rate limit
mid-edit; head completed the same diff plus the test update.
2026-05-04 16:32:38 +02:00
m
53f7eae665 fix(t-paliad-111): renumber colliding migration 032→034 — production was down
Two migrations both named 032 collided when t-111 and t-112 merged in
parallel — 032_deadline_notes_en (t-112, already applied to the DB and
tracker bumped to v33) vs. 032_deadlines_rule_code (t-111). The Go
migration runner refuses to init the driver when two files share a
prefix, so paliad.de was 404 across all routes (container in restart
loop with `migration failed: ... duplicate migration file:
032_deadlines_rule_code.down.sql`).

Renumbering t-111's pair to 034 (033 was used by t-112's
trigger_events_de backfill).
2026-05-04 14:57:54 +02:00
m
c554e865eb Merge: t-paliad-111 — bug bundle correctness (UPC GESAMTKOSTEN, court-set dates, REGEL save) 2026-05-04 14:42:51 +02:00
m
0be2dfb5a0 fix(t-paliad-111): bug bundle (correctness) — UPC GESAMTKOSTEN, court-set dates, REGEL save flow
Three correctness bugs from the t-paliad-101 QA sweep, fixed together since
they all change displayed/saved numbers users rely on.

B1 — Kostenrechner UPC GESAMTKOSTEN double-count
  ComputeUPCInstance was setting InstanceTotal = effectiveCourtFee +
  recoverableCeiling. The R.152 recoverable-cost cap is the OPPOSING
  side's worst-case loss-of-suit liability, not the user's own cost —
  folding it into GESAMTKOSTEN inflated the UPC total under a label
  that means "your outlay," and the DE LG/OLG/BGH branches don't add
  any opponent estimate. Drop it from InstanceTotal; the ceiling
  still surfaces as its own RecoverableCeiling line item.

  Live pre-fix on paliad.de (Streitwert 100k, UPC 1. Instanz only):
    instanceTotal = 52600 = 14600 court fee + 38000 R.152 ceiling
  Post-fix:
    instanceTotal = 14600 (court fee only); RecoverableCeiling stays 38000

B3 — Court-determined Termine emit trigger date as a real-looking date
  Zwischenverfahren / Mündliche Verhandlung / Entscheidung all live in
  paliad.deadline_rules with duration_value=0 and parent_id=NULL, so
  Calculate() classified them as IsRootEvent and emitted the trigger
  date as their own DueDate. Worse, RoP.151 "Antrag auf Kostenentscheidung"
  parents off inf.decision and chained 1 month off the placeholder ->
  bogus deadline that the UI rendered as real.

  Fix: classify a zero-duration rule as IsCourtSet (not IsRootEvent)
  when primary_party = 'court' or event_type ∈ {hearing, decision,
  order}. Track court-set rule IDs and propagate IsCourtSet downstream
  to any rule whose parent is court-set, so RoP.151 also surfaces as
  court-set rather than a fabricated date. Save-modal already greys
  out IsCourtSet rows so the "Gerichtsbestimmte Termine ohne Datum
  werden übersprungen" footnote becomes truthful again.

  Live pre-fix on paliad.de (UPC_INF, trigger 2026-04-29):
    Zwischenverfahren / Oral / Entscheidung -> dueDate 2026-04-29
    Antrag auf Kostenentscheidung -> 2026-05-29 (bogus, +1mo from trigger)

B6 — Fristenrechner save flow stored rule code in TITLE
  Frontend was concatenating "RoP.023 — Klageerwiderung" into the
  title because deadlines had nowhere else to put the citation, and
  the /deadlines REGEL column ended up showing "—". Add migration 032
  with a paliad.deadlines.rule_code text column, plumb it through
  CreateDeadlineInput / insertTx, drop the now-redundant r.code AS
  rule_code JOIN alias on the list query (the deadline owns its
  citation), and render f.rule_code on the project-detail deadlines
  table + /deadlines events list + deadline-detail page.

Build, vet, and tests all clean. New unit test
TestIsCourtDeterminedRule pins the B3 discriminator across the
event_type / primary_party combinations seen in migrations 012 + 031.

Repro creds: tester@hlc.de
2026-05-04 14:42:29 +02:00
m
eb6e194684 Merge: t-paliad-115 PR-2 — canonical /events URL + redirect old paths 2026-05-04 14:40:57 +02:00
m
56522adffe feat(t-paliad-115): canonicalise list URL on /events; redirect old paths
PR-2 of t-paliad-115. The unified Fristen + Termine surface now lives at
/events. Old /deadlines and /appointments list URLs 301-redirect to
/events?type=deadline and /events?type=appointment so existing bookmarks
still land on the right view. Detail pages (/deadlines/{id},
/appointments/{id}) stay type-specific.

Backend (Go).
- New `GET /events` route → handleEventsListPage serves dist/events.html.
- `GET /deadlines` → handleDeadlinesListRedirect (301 → /events?type=deadline).
- `GET /appointments` → handleAppointmentsListRedirect (301 → /events?type=appointment).
- /deadlines/new, /deadlines/calendar, /deadlines/{id}, /appointments/new,
  /appointments/calendar, /appointments/{id} unchanged — type-specific
  detail / form / legacy-calendar surfaces stay where they are.

Frontend.
- build.ts now emits ONE events.html (not events-deadlines /
  events-appointments) with defaultType="all" baked in. The page reads
  ?type=… and ?view=… on hydration, so /events?type=deadline lands on
  the Fristen-only Cards view, /events?view=calendar opens the calendar,
  and bare /events shows the Beides view.
- Sidebar Fristen / Termine entries point at /events?type=deadline and
  /events?type=appointment. The SSR active-state matches exactly via
  href === currentPath, so detail/new/calendar pages that pass
  currentPath="/events?type=deadline" (resp. appointment) still
  highlight the right entry.
- events.ts hydration adds applySidebarTypeHighlight(): on bare /events
  the sidebar SSRs with neither entry lit, and we re-highlight the
  matching entry whenever the in-page chip toggle changes the active
  type. Sidebar stays in sync without a server round-trip.
- Updated every list-target reference: palette-actions.ts (Cmd-K
  navigation), deadlines-detail.ts + appointments-detail.ts (post-delete
  redirect), and the back-link / cancel hrefs in the *-new + *-detail +
  *-calendar TSX templates. Detail-page Sidebar/BottomNav currentPath
  also moved from "/deadlines" → "/events?type=deadline" so the new
  highlight contract holds end-to-end.

Out of scope (per task brief).
- A third "Ereignisse / Alle Events" sidebar entry pointing at /events
  bare. m's call: keep two entries; defer until signal.
- Removing /deadlines/calendar + /appointments/calendar standalone
  pages. The new /events?view=calendar covers the same need but the
  legacy URLs stay live for one cycle.

Build clean: `cd frontend && bun run build` + `go build/vet/test ./...`.
2026-05-04 14:40:53 +02:00
m
b88afe8ddd Merge: t-paliad-112 — i18n bug bundle (deadline_notes_en, trigger-event DE, Checkliste PROJECT) 2026-05-04 14:36:57 +02:00
m
341fa6c26f fix(t-paliad-112): i18n leaks — deadline_notes_en, trigger-event DE, Checkliste header
Three i18n bugs from the t-paliad-101 QA sweep, fixed together:

B2 — Fristenrechner deadline notes leaked German into the EN locale.
Migration 032 adds paliad.deadline_rules.deadline_notes_en (TEXT NULL)
and backfills English translations for all 30 rules that carry a
deadline_notes value (UPC RoP / EPC / ZPO terminology). The frontend
prefers _en when locale=EN and falls back to deadline_notes (DE) when
the column is NULL, so future seeds without an EN translation render
in DE rather than empty. UIDeadline DTO gains notesEN. The bulk
"Als Frist(en) speichern" CTA now stores the locale-matched note text
so EN users get an EN note alongside the EN title.

B8 — trigger-event picker labels were English-only when DE locale was
active (102 rows, name_de defaulted to '' in 028, frontend already had
the locale switch but no data). Migration 033 backfills name_de for
all 102 trigger events using standard German UPC RoP terminology
(Klageschrift, Klageerwiderung, Replik, Duplik, Nichtigkeitswiderklage,
Verletzungswiderklage, Berufungsschrift/-begründung, Anschlussberufung,
Schutzschrift, Beweissicherung, etc.).

S3 — frontend/src/client/checklists-instance.ts:154 had a hardcoded
"Project" label in both branches of the locale ternary; the DE branch
now reads "Projekt", matching the surrounding meta-item labels' pattern
(Court / Authority → Gericht / Behörde, Reference → Rechtsgrundlage).
2026-05-04 14:36:50 +02:00
m
45979caf81 Merge: t-paliad-115 PR-1 — events view ⊥ filter + unified calendar view 2026-05-04 14:34:52 +02:00
m
1dad1c7371 feat(t-paliad-115): events view ⊥ filter — view selector + unified calendar view
PR-1 of t-paliad-115. Separates the view axis (cards / list / calendar)
from the filter axis (event type, status, project) on EventsPage. Closes
the duplicate "Kalenderansicht" button that t-paliad-110 left behind on
the Beides view.

Bug source: frontend/src/events.tsx:53-68 rendered TWO separate
"Kalenderansicht" anchors (events-action-deadline-cal +
events-action-appointment-cal) and frontend/src/client/events.ts:533-534
unhid both when type=all. Reproduced live on paliad.de — screenshot at
.playwright-mcp/paliad-115-duplicate-kalenderansicht-before.png.

Architectural change.
- Removed the per-type calendar buttons from the page header.
- Added a 3-button segmented view selector (Karten / Liste / Kalender)
  next to the type chips. The two controls now sit on a shared
  events-axis-row that flexes side-by-side and stacks on narrow viewports.
- View state lives in `currentView`, defaults to "cards", reads from
  ?view= on init, persists to URL on change. Default ("cards") stays
  out of the URL so existing bookmarks don't change.
- Cards view = original (5-card summary + table). List view = table
  only (cards hidden). Calendar view = month grid; cards + table both
  hidden.

Calendar view.
- Plots both deadlines (due_date) and appointments (start_at local
  date) on a Mo–So month grid. Reuses the existing .frist-cal-* CSS
  scaffold from /deadlines/calendar; only new addition is
  .events-cal-dot-appointment for the appointment hue (--bucket-next-week).
- Inherits the page's filters — `?view=calendar&type=appointment` shows
  appointment-only; `?view=calendar&status=all` shows everything; etc.
  Status, type, project, event-type filters apply orthogonally to view,
  matching the spec's "two axes combine" requirement.
- Click a day with items → existing modal pattern lists them with type
  chip + project ref; clicking an item navigates to its detail page.
- Month nav (prev / next / today) is purely client-side — no refetch,
  cheap pagination over the already-loaded items.

Out of scope (per task brief).
- Standalone /deadlines/calendar + /appointments/calendar pages stay
  untouched. PR-2 (URL canonicalisation) handles that surface.
- Custom time-window controls — future iteration per t-110 spec.

Build clean: `cd frontend && bun run build` + `go build/vet/test ./...`.
2026-05-04 14:34:44 +02:00
m
9dbc55638a Merge: t-paliad-113 — bug bundle polish (save modal + Streitwert + EPO + EN dates) 2026-05-04 14:32:16 +02:00
m
2c346672bb fix(t-paliad-113): bug-bundle polish — save modal court-set + project prefix, negative Streitwert, EPO en label, ISO en dates
5 small polish bugs from the t-paliad-101 QA sweep, bundled as one PR.

B4 — Fristenrechner save modal pre-checked court-determined entries:
treat dl.party === "court" as court-determined alongside dl.isCourtSet
so Zwischenverfahren / Mündliche Verhandlung / Entscheidung pre-uncheck
+ disable in the modal. Their meta now reads "vom Gericht bestimmt"
(deadlines.court.set) instead of the urgency placeholder.

B5 — Save modal project dropdown empty-code prefix: render "(reference)
— title" only when reference is non-empty; bare title otherwise. No
more leading "— Title" rows.

B7 — Negative Streitwert in Kostenrechner: blur handler now snaps
negative entries by clearing the input + resetting the slider to its
min and re-running calc, so a "-50000" entry no longer leaves stale
positive results onscreen. Input handler also drops the slider to min
on negatives so the visual state stays in sync mid-typing.

S2 — Courts EN page filter chip "EPA" → "EPO": added
gerichte.filter.epa i18n key (DE: EPA, EN: EPO) and wired it on the
courts.tsx pill.

S4 — Fristenrechner EN date format: switch from en-GB (dd/mm/yyyy,
ambiguous for US readers) to ISO ("Tue, 2026-09-01"). DE format
unchanged.
2026-05-04 14:31:43 +02:00
m
7463831932 Merge: t-paliad-109 — design doc for Fristen/Termine unification 2026-05-04 14:23:01 +02:00
m
165f0c1717 Merge: t-paliad-110 fix — hide Überfällig on Beides view 2026-05-04 13:57:47 +02:00
m
82421b3c86 fix(t-paliad-110): hide Überfällig card on Beides view
Spec is explicit that Überfällig is deadline-view-only — only showing
it on type=deadline matches the rationale that 'overdue' is a deadline-
specific concept (appointments don't go overdue, they happen). The
previous logic also surfaced the card on Beides whenever the
deadline-side overdue count > 0, which would have rendered an alarm
card on a mixed view.

Caught during live verification on paliad.de.
2026-05-04 13:57:40 +02:00
m
b2dfc87f57 Merge: t-paliad-110 PR-4 — Dashboard Termine rail + Fristen rail refactor 2026-05-04 13:52:56 +02:00
m
57237a55a3 feat(t-paliad-110): refactor Dashboard rails — drop Erledigt card, add Später + Termine rail
PR-4 of the Fristen+Termine unification, closing out t-paliad-110.

Fristen rail (was 5 cards):
- Erledigt card removed (status=completed stays reachable via the EventsPage
  filter dropdown — no card on the rail per the new model)
- Später card added (pending deadlines past Mon-week-after, click filters
  to /deadlines?status=later)
- 4+1 final shape: Überfällig (conditional alarm) · Heute · Diese Woche ·
  Nächste Woche · Später

Termine rail (new): 3 cards — Heute · Diese Woche · Später. No Überfällig
(past appointments aren't urgent), no Nächste Woche (low-value distinction
for appointments per the design rationale). Cards click through to
/appointments?status=… so users land in the matching EventsPage view.

Backend (DashboardService.loadSummary):
- DeadlineSummary.CompletedThisWeek dropped, .Later added
- AppointmentSummary added (Today / ThisWeek / Later)
- One CTE-based query computes both rails alongside MatterSummary; bucket
  cutoffs share computeDeadlineBucketBounds with /api/events/summary +
  /api/deadlines/summary so all three surfaces stay in lockstep

Frontend:
- dashboard.tsx: Erledigt card removed, Später card + Termine section added
- client/dashboard.ts: types updated, renderAppointmentSummary added
- 4 new i18n keys (DE+EN): dashboard.summary.later +
  dashboard.appointment_summary.heading
- CSS: .dashboard-card-later (muted blue) + 3 .dashboard-card-appt-* rules
  reusing the existing --bucket-* tokens

go build/vet/test ./... clean. bun run build clean (1396 keys).
2026-05-04 13:52:49 +02:00
m
88af8d3487 Merge: t-paliad-110 PR-3 — mount EventsPage on /deadlines + /appointments 2026-05-04 13:48:58 +02:00
m
50ac065c7d feat(t-paliad-110): mount unified EventsPage on /deadlines + /appointments
PR-3 of the Fristen+Termine unification. Both routes now serve the shared
shell built by renderEvents() — the per-type pages (deadlines.tsx /
appointments.tsx and their client bundles) are deleted entirely.

Hydration is baked at build time, not at request time: build.ts emits
events-deadlines.html and events-appointments.html, each carrying an
inline `window.__PALIAD_EVENTS__={"defaultType":"…"}` script in <head>.
The Go handlers ServeFile the matching artefact, no placeholder swap
needed (cleaner than the dashboard pattern for a single static flag).

Sidebar entries unchanged — "Fristen" still points at /deadlines,
"Termine" at /appointments. Both highlight correctly because each
artefact passes the matching currentPath into <Sidebar />.

Detail / new / calendar pages stay type-specific (out of scope per
task brief). Old endpoints /api/deadlines + /api/appointments remain
live for the calendars, project-detail panes, and CalDAV consumers.

Net: -981 lines (drops the duplicated chrome of the two old pages
in favour of one shared shell).

go build/vet/test ./... clean. bun run build clean.
2026-05-04 13:48:53 +02:00
m
285e97203a Merge: t-paliad-110 PR-2 — shared EventsPage component 2026-05-04 13:46:38 +02:00
m
fe9c1b7de2 feat(t-paliad-110): add shared EventsPage component + bucket-aware backend tweaks
PR-2 of the Fristen+Termine unification. Pure additive change — the existing
deadlines.tsx + appointments.tsx pages stay live; this PR introduces the new
events.tsx shell + client/events.ts runtime that PR-3 will mount onto the
two routes.

Frontend (new):
- frontend/src/events.tsx — shared shell with the 3-chip type toggle
  (Fristen / Termine / Beides), the 5-card summary row (Überfällig
  conditional + 4 universal cards), the union filter row, and the unified
  table that renders a discriminated row per type. Two header CTAs ("Neue
  Frist" + "Neuer Termin") collapse to the relevant one in single-type mode.
- frontend/src/client/events.ts — runtime. Reads window.__PALIAD_EVENTS__
  (PR-3 will inject defaultType from the Go handler), derives the rest from
  ?type query param. Card click sets status filter; the events endpoint
  takes care of bucket-aware appointment-side date windowing so both rails
  stay in sync in Beides mode. Hide-on-uniform pattern applied per column
  (rule, event_type, location, appointment_type, status, row-type chip).
- frontend/build.ts — emits events-deadlines.html + events-appointments.html
  from one renderEvents(currentPath) so each output gets the right Sidebar
  highlight; client/events.ts bundle added.
- 16 i18n keys (DE+EN): events.toggle.*, events.summary.later,
  events.col.*, events.row.type.*, events.empty.*, events.unavailable plus
  the new deadlines.summary.later / deadlines.filter.later pair for the
  Später bucket.
- CSS: --bucket-later (#1d4ed8 light / #60a5fa dark) for the Später card,
  matching events-table--hide-* column hiders, .events-row-type-chip
  styling, .event-type-chip-row spacing.

Backend tweaks (small):
- DeadlineFilterLater (`later`): pending deadlines past Mon-week-after.
  Click-target for the Später card.
- EventService.ListVisibleForUser now derives an appointment-side date
  window from a bucket-style status (today/this_week/next_week/later) so
  card clicks filter both rails consistently. Overdue/Completed exclude
  appointments entirely (no appointment analogue).
- pickLater / pickEarlier helpers intersect the bucket-derived window with
  any caller-supplied from/to.

go build/vet/test ./... clean. bun run build clean (1394 keys, IIFE prologue
guard passes).
2026-05-04 13:46:33 +02:00
m
49eb3c97e1 Merge: t-paliad-110 PR-1 — backend EventService + /api/events 2026-05-04 13:37:36 +02:00
m
2102dfd07d feat(t-paliad-110): add EventService + /api/events + /api/events/summary
PR-1 of the Fristen+Termine unification (t-paliad-110). Backend layer
only — no frontend changes; the existing /deadlines and /appointments
pages still render the type-specific UIs.

EventService delegates to DeadlineService + AppointmentService for the
actual reads (no duplicate visibility logic, no duplicate event_type
hydration), then projects both into the discriminated EventListItem
union and merges/sorts by event_date asc. The handler exposes:

  GET /api/events?type=deadline|appointment|all&status=…&project_id=…
                  &event_type=…&type_filter=…&from=…&to=…
  GET /api/events/summary?type=…&project_id=…

Bucket model (per t-paliad-110 spec, supersedes t-106):
- four universal cards: Heute · Diese Woche · Nächste Woche · Später
- Überfällig is deadline-only, conditional, alarm-styled when > 0
- Erledigt drops from the card row; stays available as a filter option
- appointments have no completed_at — past appointments aren't bucketed

The deadline-side cutoffs reuse computeDeadlineBucketBounds so
/api/events/summary and /api/deadlines/summary can never disagree.

Existing /api/deadlines and /api/appointments stay untouched —
calendars, project-detail panes, and CalDAV consumers still call them
directly.
2026-05-04 13:37:20 +02:00
m
25efce0c76 design(t-paliad-109): unify Fristen + Termine as filtered Events views
Design doc only — no code touched. Recommends keeping /deadlines + /appointments
URLs but rendering one EventsPage component (smallest-diff Option A1), backed
by a new EventService that delegates to existing Deadline/AppointmentService
(Option B1). Two-rail bucket summary on Beides (5 deadline + 3 appointment),
detail pages stay separate, /agenda timeline left alone. §F lists 17 questions
gating m's greenlight, including a premise correction: the brief described
/agenda as the appointment list — actually it's a pre-existing cross-type
timeline; the appointment list is /appointments.
2026-05-04 13:14:52 +02:00
m
1def9e86b9 Merge: t-paliad-108 — bucket card colors (Heute dark-orange, Diese Woche yellow) 2026-05-04 13:02:21 +02:00
m
db09f6e7d5 feat(t-paliad-108): bucket card palette — Heute orange, Diese Woche yellow
5-bucket urgency cards (Dashboard "Fristen auf einen Blick" + /deadlines
summary) now share a single source of truth via new --bucket-* tokens:

  Überfällig red → Heute orange → Diese Woche yellow → Nächste Woche green → Erledigt grey

Light values pass WCAG AA-large (3:1+) on the white surface for the
colored count (1.6–2.25rem, 600–700 weight = large text):
  red #ef4444 · orange #c2410c · yellow #a16207 · green #16a34a · grey #6b7280
Dark mode lifts to bright pastels for readability on midnight:
  red #fca5a5 · orange #fb923c · yellow #fbbf24 · green #4ade80 · grey #9ca3af

Both surfaces (.dashboard-card-* and .frist-card-*) now wire through the
same tokens, so the visual hierarchy reads identically. The Überfällig
alarm pulse (saturated red bg + white text, t-paliad-105/106) remains
unchanged. The dashboard-card-green border previously used --color-accent
(brand lime); it now uses --bucket-next-week to match the Fristen page.
2026-05-04 13:02:12 +02:00
m
99af714d65 Merge: t-paliad-107 — event-type picker browse-all modal 2026-05-04 12:06:26 +02:00
m
de03f3ddcb feat(t-paliad-107): add Alle anzeigen browse-all modal to event-type picker
The picker on /deadlines/new and /deadlines/{id} edit was search-only —
users had to know what to type. Add a third affordance: a "Alle anzeigen"
button next to search and "+ Neuer Typ" that opens a modal listing every
available event type grouped by category, with sticky search and
multi-select checkboxes pre-populated from the picker's current
selection. Apply replaces; Cancel discards.

Modal a11y: focus-trap (Tab cycles in modal), Esc to close, click-outside
to dismiss. Each row shows the localized label, optional jurisdiction
badge, and a checkbox. Empty-search state renders a muted message.

Side effect: added a base `.modal` rule giving every `<div class="modal">`
a proper card surface (background, padding, radius, shadow). The existing
.event-type-add-modal previously had only width — the latent gap meant
its form fields rendered floating over the dim overlay. Now both modals
share the scaffold.

Files:
- frontend/src/client/event-types.ts — openBrowseEventTypesModal +
  browse button on the picker.
- frontend/src/styles/global.css — .modal base + .event-type-browse-*
  + flex-wrap on .event-type-search-row to accommodate three children.
- frontend/src/client/i18n.ts — DE/EN keys: event_types.picker.browse_all,
  event_types.browse.{title,search,empty,apply,cancel,selected_count,
  jurisdiction.none}.

Out of scope per brief: /deadlines + /agenda multi-select filter (already
has its own browse-style listbox-panel), admin moderation panel,
appointment forms (don't carry event types).
2026-05-04 12:06:00 +02:00
m
d19e35bfaf Merge: t-paliad-106 — Deadline summary 5-bucket harmonization 2026-05-04 12:04:20 +02:00
m
37a925d3b2 feat(t-paliad-106): harmonize deadline summary — 5 disjoint buckets across Dashboard + Fristen
Both surfaces now show the same buckets with the same labels and the same
cutoffs: Überfällig (conditional, alarming) · Heute · Diese Woche ·
Nächste Woche · Erledigt. Single-source bucket math via
computeDeadlineBucketBounds — Heute = today, Diese Woche = tomorrow
through the upcoming Sunday inclusive, Nächste Woche = next Monday
through next Sunday inclusive, all disjoint. Items past next Sunday
are visible only via "All open"/"Upcoming" filters; the Überfällig
card stays hidden when count == 0 and switches to a saturated red
pulse + bold white text when count > 0.

Filter dropdown on /deadlines gains today / next_week entries; old
"upcoming" filter still works as a back-compat alias for everything
pending past this Sunday so legacy bookmarks don't 4xx.

Tests: 8 deterministic table cases for the bucket pivots (every
weekday + a 21-day disjointness walk).
2026-05-04 12:03:56 +02:00
m
16eb73bf44 Merge: t-paliad-105 — Überfällig hide-when-zero / alarm-when-nonzero 2026-05-04 11:52:45 +02:00
m
2bbbe562d7 feat(t-paliad-105): hide Überfällig card at zero, alarm at >0
Überfällig is an emergency category — never a normal-state tile. Replace the
prior `.dashboard-card-quiet` dim-but-visible behavior with two states:

- overdue === 0 → card removed from the layout (`*-overdue-hidden`).
  The Dashboard summary grid and the Fristen summary cards now use
  `repeat(auto-fit, minmax(180px, 1fr))` so the row re-flows to 3 cards
  instead of leaving an empty 4th column.
- overdue > 0 → saturated red surface, white text, soft pulsing red ring
  via `paliad-alarm-pulse`. Honors `prefers-reduced-motion`. Dark theme
  uses a slightly lighter red so the alarm still pops on midnight.

Applied on both surfaces (`#dashboard-card-overdue` and the Fristen
`.frist-summary-card[data-status="overdue"]`).
2026-05-04 11:52:35 +02:00
m
102d0168e9 Merge: t-paliad-104 — harmonize light-theme sidebar with cream body 2026-05-04 11:51:46 +02:00
m
e6369bc4c2 feat(t-paliad-104): harmonize light-theme sidebar with cream body
Reverses the t-paliad-083 carve-out that kept the sidebar on midnight in
both themes. m's feedback: in light mode the dark midnight column read as
a separate panel bolted onto the cream body. The brand lime stripe on the
active item is preserved (decoupled from --sidebar-text-active onto
--color-accent) so the active cue stays anchored across themes.

Light mode: --sidebar-bg = --color-bg-subtle (one step up from body
cream), --sidebar-text/-muted track the body palette, --sidebar-text-active
= midnight (full contrast against muted-grey inactive items),
hover/input/scrollbar tints switch to midnight-channel alphas.

Dark mode: original midnight + cream + lime palette restored inside the
:root[data-theme="dark"] override block. No regression there.
2026-05-04 11:51:40 +02:00
m
1a815979f8 docs(t-paliad-103): warn against ::before block-link overlays
Append a sibling note to the .entity-table contract (t-paliad-099)
explaining that pointer-event overlays for whole-card click break
text selection. Steer future hands at the row-handler pattern from
t-098/099/102/103 instead.
2026-05-04 10:54:09 +02:00
m
8a9b9c6611 Merge: t-paliad-103 — replace Verlauf ::before with row-click for text-selectable cards 2026-05-04 10:34:54 +02:00
m
73d108d878 fix(t-paliad-103): replace Verlauf ::before block-link with row-click handler
The t-102 ::before overlay (`inset: 0` on `.entity-event-link`) made the whole
card clickable but also captured pointer events on the title and description
text — users couldn't select-to-copy. Same trap noted in brunel's t-102
debrief: when text-selection matters, switch to a row-level click handler that
skips inner <a>/<button>, matching the .entity-table pattern from t-098/099.

CSS (global.css):
  - drop `.entity-event-link::before` overlay
  - drop `position: relative` from `.entity-event` and `position: static` from
    `.entity-event-link` (no longer anchoring an absolute pseudo-element)
  - keep cursor: pointer + hover-lift on `.entity-event:has(.entity-event-link)`
    so the affordance still telegraphs "clickable"
  - card hover-lift now keys off the card itself, not the link's :hover, so the
    lift triggers from anywhere on the card (matching the new click surface)
  - mirror `.dashboard-activity-item`: cursor + lime-tint hover row-highlight

TS:
  - projects-detail.ts:renderEvents: after innerHTML, attach a row-level click
    handler that reads `.entity-event-link.href` and skips clicks on inner
    <a>/<button>. Cards without a link have no `.entity-event-link` and stay
    non-clickable (cursor stays default via the `:has()` selector).
  - dashboard.ts:renderActivity: same handler reading `.dashboard-activity-project.href`.

Acceptance:
  - Title and description text on /projects/{id} → Verlauf cards and on
    /dashboard activity rows is selectable again.
  - Click anywhere on a card still navigates (no regression from t-102).
  - Title link still navigates; Cmd-/Ctrl-click opens in new tab; keyboard
    tabbing still hits the inner link.
  - `cd frontend && bun run build` clean; `go build/vet/test ./...` clean.
2026-05-04 10:34:45 +02:00