Commit Graph

698 Commits

Author SHA1 Message Date
mAi
f5eb84718a chore(t-paliad-179): sidebar maps Verfahrensablauf 1:1 to its own URL
Sidebar.tsx href flips from /tools/fristenrechner?path=a to
/tools/verfahrensablauf. The two Werkzeuge entries now resolve to
distinct pathnames, so the SSR navItem helper picks the right active
class on its own — fixVerfahrensablaufActive (which compared search
params client-side to disambiguate) is deleted along with its call
in initSidebar.
2026-05-13 00:19:16 +02:00
mAi
1255ee049f feat(t-paliad-179): /tools/verfahrensablauf page (TSX + client + build)
The new abstract-browse surface. TSX shell hosts:

  - header (h1 + subtitle)
  - jurisdiction-tabbed proceeding-tile picker (UPC / DE / EPA / DPMA)
  - trigger date input
  - court picker (visible only for proceedings with multiple
    compatible courts — UPC_REV across CD + LD seats etc.)
  - view toggle (Spalten / Zeitstrahl)
  - result container

client/verfahrensablauf.ts wires picker click → calculateDeadlines →
renderColumnsBody/renderTimelineBody via the shared core. Pre-selects
the first proceeding tile on load so users see a timeline immediately,
matching /tools/fristenrechner's auto-render behaviour. No Akte
picker, no Pathway B cascade, no save modal, no anchor-override edit
— Slice 1 is the structural foundation; variant chips + lane view
(Slice 3) and compare (Slice 4) layer on top in later commits.

build.ts wires the new entrypoint + write step. i18n adds
tools.verfahrensablauf.title / .heading / .subtitle in DE + EN; the
existing nav.verfahrensablauf reused.
2026-05-13 00:19:10 +02:00
mAi
0105d35f0c refactor(t-paliad-179): fristenrechner consumes shared renderer module
client/fristenrechner.ts imports renderTimelineBody / renderColumnsBody
/ deadlineCardHtml / formatDate / partyBadge / escAttr / escHtml /
calculateDeadlines / populateCourtPicker from views/verfahrensablauf-
core, deleting the local copies (~480 lines out). The click-to-edit
anchor-override path stays wired by passing { editable: true } to the
shared renderers; the local anchor-override Map / openInlineDateEditor
/ render-on-override path are unchanged.

The "Verfahrensablauf einsehen" Step 2 card (t-paliad-168) is retired
— TSX markup gone, click handler gone. The abstract-browse intent
lives at /tools/verfahrensablauf now (Slice 1 design §9, §10).
2026-05-13 00:19:00 +02:00
mAi
0531e5dbf6 feat(t-paliad-179): lift Fristenrechner renderers into shared core module
frontend/src/client/views/verfahrensablauf-core.ts — pure-functional
module with the proceeding-timeline rendering surface:

  - DeadlineResponse / CalculatedDeadline / CourtRow types
  - escAttr / escHtml / formatDate / partyBadge helpers
  - deadlineCardHtml(dl, { showParty, editable })
  - renderTimelineBody(data, opts)
  - renderColumnsBody(data, opts)
  - calculateDeadlines(params) — POST /api/tools/fristenrechner wrapper
  - courtTypesFor / defaultCourtFor / fetchCourts (cache)
  - populateCourtPicker(rowId, selectId, proceedingType)

Both /tools/fristenrechner and /tools/verfahrensablauf import from
here. No module-level mutable state — the per-page concerns
(anchorOverrides, lastResponse, Akte save) stay in the consumers.

The deadlineCardHtml signature carries an editable flag so the click-
to-edit anchor-override affordance is opt-in per page: fristenrechner
enables it, verfahrensablauf (Slice 1 scope) doesn't.
2026-05-13 00:18:52 +02:00
mAi
0099e2f28c feat(t-paliad-179): register /tools/verfahrensablauf + 302 legacy ?path=a
Backend half of Slice 1: a new dedicated route owns the abstract-browse
intent that was previously emulated by /tools/fristenrechner?path=a +
client-side fix-up. The page handler is a 1-liner that serves
dist/verfahrensablauf.html (no DB dependency).

A naked ?path=a on /tools/fristenrechner now 302s to the new URL so
bookmarked legacy links survive. ?project=<uuid>&path=a still serves
the fristenrechner shell because that's wizard state set by client-
side history.replaceState during Akte-mode Pathway A — refreshing
mid-wizard must not bounce away.

Test covers all four query shapes: naked path=a → redirect, path=a
with project → no redirect, no params → no redirect, path=b → no
redirect.
2026-05-13 00:18:42 +02:00
mAi
cd1a70d08c Merge: t-paliad-178 — Tools surface cleanup design doc (DESIGN READY FOR REVIEW) 2026-05-13 00:00:41 +02:00
mAi
bdb3d8a425 Merge: t-paliad-177 Slice 1 — Project Timeline / Chart (SVG Gantt + standalone /projects/{id}/chart page) 2026-05-12 14:14:01 +02:00
mAi
30f7031e99 feat(t-paliad-177): chart page TSX + boot client + i18n + Verlauf link
Wires the chart surface end-to-end:

- frontend/src/projects-chart.tsx — standalone page shell with title
  row, inert control chips (Slice 3 wires them live), undated hint slot,
  and the mount target for the SVG renderer.
- frontend/src/client/projects-chart.ts — boot client that parses the
  project id from the URL, loads project metadata for the header,
  mounts the renderer, and reveals the undated hint when the layout
  reports clipped/undated rows.
- frontend/build.ts — registers the new bundle + HTML output.
- frontend/src/client/i18n.ts — 11 new DE+EN keys under projects.chart.*
  + projects.detail.smarttimeline.open_chart (the Verlauf link).
- frontend/src/projects-detail.tsx — "Als Chart anzeigen ↗" link in
  the SmartTimeline controls, opens /chart in a new tab.
- frontend/src/client/projects-detail.ts — resolves the chart href in
  renderHeader once project.id is known.

`bun run build` clean, `go build ./...` clean, 27/27 chart tests pass.

Design ref: docs/design-project-chart-2026-05-09.md §8.1 + §8.2 + §12.
2026-05-12 14:12:20 +02:00
mAi
8e9cde6d52 design(t-paliad-178): Tools surface cleanup — split Fristenrechner / Verfahrensablauf
Inventor pass for t-paliad-178. Two intents (deadline determination vs
abstract procedural shape browse) get two dedicated routes:

- /tools/fristenrechner — keeps deadline-determination, gains Step 0
  ("Abstrakt oder Akte?") above today's Step 1.
- /tools/verfahrensablauf — new dedicated abstract-browse surface with
  variant chips (with_ccr / with_cci / with_amend), consolidated-vs-lane
  view, and side-by-side compare.

§0 premise audit corrects three things the task brief got wrong:
  1. projects.court is free-text, not FK — no silent court_id auto-pick.
  2. projects.proceeding_type_id points at litigation-category rows, not
     fristenrechner-category — a mapping helper (litigation × jurisdiction
     → fristenrechner code) is required.
  3. condition_flag variants only exist on UPC_INF + UPC_REV; every other
     proceeding renders a single canonical timeline. Variant chips honour
     this — no dead chips on DE_INF / EPA_OPP / DPMA_*.

Sliced into 4 independent merges: Slice 1 (route + shell split) is the
structural foundation; Slices 2-4 layer Step 0 / variant chips / compare.

DESIGN ONLY — no implementation. Awaiting m's go/no-go before coder shift.
2026-05-12 14:10:20 +02:00
mAi
a3adb6b13b feat(t-paliad-177): chart SVG paint() + mount() + palette CSS tokens
Extends shape-timeline-chart.ts with the DOM-mutation half of the
renderer:

- paint(layout, root, events): hand-rolled SVG using namespaced
  document.createElementNS. Idempotent (clears prior children),
  layers <defs> → grid+axis+lanes → today rule → marks. Each mark
  wraps in <g> with data-* attrs for delegated event handling.
- mount(host, opts): fetches /api/projects/{id}/timeline (defensive
  for both legacy []TimelineEvent and Slice-4 envelope shapes),
  computes a today-1y..today+1y default range (design Q8), wires
  resize debouncing + click delegation. Returns a handle with
  refresh / dispose / getLayout.

CSS palette tokens swap purely via --chart-* custom properties on
.smart-timeline-chart, so future palette slices (Slice 3) toggle
attributes without touching the renderer. Deadlines colour-saturate
by status (open = ring, done = filled, overdue = red). Projected
rows use the hatched/dashed-dot variants from §6.2.

Design ref: docs/design-project-chart-2026-05-09.md §2.3 + §5 + §6.
2026-05-12 14:09:43 +02:00
mAi
ed4e731333 feat(t-paliad-177): chart layout() pure-function + 27 table-driven tests
Slice 1 load-bearing math. Translates TimelineEvent[] + LaneInfo[] +
viewport into deterministic SVG-ready geometry: axis ticks (month /
quarter / year by total span), lane row y/height, mark x/y/shape per
kind+status, today rule. No DOM access — paint() will read this and
mutate the SVG separately.

Tests pin canvas geometry, pxPerDay math, today-rule clipping, lane
stacking, mark bucketing by lane_id, out-of-range clipping, undated
zone, mark-shape mapping, axis tick density. Date math is UTC
throughout so DST doesn't drift day-deltas.

Design ref: docs/design-project-chart-2026-05-09.md §2.3 + §15.
2026-05-12 14:07:48 +02:00
mAi
b0a6b0998f feat(t-paliad-177): chart page handler + GET /projects/{id}/chart route
Slice 1 backend slice. Tiny static-file server for the new standalone
chart page; visibility piggybacks on the existing /api/projects/{id}/
timeline endpoint (gated through ProjectionService.For), so no new
auth surface.

Design ref: docs/design-project-chart-2026-05-09.md §8.2 + §12.
2026-05-12 14:05:52 +02:00
m
54b227ce7b Merge: t-paliad-176 — FilterBar regression bundle (m/paliad#32 + #33)
maxwell diagnosed and fixed two regressions m hit @ 18:32:

#32 — FilterBar timeline filters don't narrow Verlauf:
- The Verlauf bar mounted only 'time' + 'project_event_kind' axes; the
  Slice 2/3 timeline_status + timeline_track chips never rendered.
- The customRunner drained predicates into the legacy loadEvents()
  array, but the SmartTimeline render reads timelineRows. Filter pass
  was landing on a dead branch.

#33 — Nur direkt always includes sub-projects:
- Frontend correctly sent ?direct_only=true; handler parsed it; the
  loadProjectTrack SQL filter respected DirectOnly. But Slice 3's
  CCR-children loading (forCaseLevel) and Slice 4's lane-per-child
  loading (forAggregatedLevel) ran unconditionally regardless of the
  flag.

Fix:
- Backend: ProjectionService.For() short-circuits to new
  forDirectSelfOnly when opts.DirectOnly. Single 'self' lane, no
  CCR/parent_context/child-case aggregation. Level-policy kind+status
  filter still applies. Added ProjectEventType field to TimelineEvent
  so frontend can filter by project_event_kind end-to-end.
- Frontend: mountFilterBar's customRunner gains a 'state' arg for
  first-run hydration; BarHandle gains getState(). Verlauf bar now
  mounts all three axes (time, project_event_kind, timeline_status,
  timeline_track). customRunner drains state into verlaufFilters;
  renderTimeline calls applyTimelineRowFilters before passing rows
  to renderSmartTimeline.
- Tests: two new live-DB subtests in TestProjectionService_LevelAggregation_Live
  pin the DirectOnly contract at Patent and Case level (+89 LoC).

Verified: go build/vet/test clean, bun build clean. (Pre-existing
pq type-inference seed issue in unrelated projection_service_test
remains; verified independent of this change via stash.)

3-way merge with main preserves faraday's chart design doc unchanged.

Single commit c2f1c29 from mai/maxwell/bug-bundle-filterbar.
2026-05-09 18:53:39 +02:00
m
c2f1c29b10 fix(t-paliad-176): FilterBar timeline narrowing + Nur-direkt subtree skip
Two regressions from SmartTimeline Slices 2-4 dogfood @ 2026-05-09:

m/paliad#32 — clicking timeline_status / timeline_track / project_event_kind
chips changed URL params but the rendered list never narrowed. Two
causes: (1) the Verlauf bar mounted only "time" + "project_event_kind"
axes — the timeline_status / timeline_track chips never appeared. (2)
the customRunner drained predicates into `loadEvents` which writes the
legacy `events` array; the SmartTimeline render reads `timelineRows`,
so the filter pass was a dead branch.

Fix: mount all three axes on the bar; rewrite customRunner to drain
state into `verlaufFilters`; renderTimeline applies them client-side
via `applyTimelineRowFilters` before handing rows to renderSmartTimeline.
project_event_kind is forwarded through the substrate-shaped predicate
map (effective.filter.predicates.project_event.event_types);
timeline_status / timeline_track sit on raw BarState — the customRunner
signature now accepts the BarState snapshot as a second arg so the
bar's first run (before the handle is assigned) can read them.

Backend adds `ProjectEventType` to TimelineEvent + frontend
TimelineEvent — needed so the project_event_kind chip can match against
the underlying paliad.project_events.event_type for milestone rows.

m/paliad#33 — "Nur direkt" pill flipped subtreeMode and re-fetched the
timeline with ?direct_only=true, but ProjectionService.For honoured the
flag only at the deadline / appointment / project_events SQL level. CCR
sub-project lanes (Slice 3) and child-case lanes (Slice 4) loaded
unconditionally, so the "direct" view still showed everything.

Fix: `For` short-circuits to `forDirectSelfOnly` whenever DirectOnly is
set. Single "self" lane, no CCR / parent_context / child-case
aggregation. The level-policy kind/status filter still applies at
higher levels so a Patent-level direct view doesn't leak off_script
custom milestones the aggregated view filters out.

Tests: two new live-DB subtests in TestProjectionService_LevelAggregation_Live
pin the contract — Patent direct_only collapses to a single 'self' lane
and excludes child-case events; Case-A direct_only excludes the CCR
child's milestones (with subtree default still surfacing them).

Build: go build/vet/test clean. bun run build clean (2171 keys).
2026-05-09 18:52:01 +02:00
m
17e96b7a1c Merge: t-paliad-177 — Project Timeline / Chart design doc (DESIGN READY FOR REVIEW)
faraday's inventor pass on m's 18:32 ask. Visualisation layer above the
SmartTimeline substrate; ADDS surfaces (does not replace Verlauf).

607-line doc covers:
- Renderer choice: SVG hand-rolled for horizontal Gantt; DOM (existing
  shape-timeline.ts) for vertical. No D3 / Chart.js dep.
- Layouts: vertical (existing) + horizontal Gantt-strip (new).
- 5 palette presets, 6 export formats (client SVG/PNG/PDF + server
  CSV/JSON/iCal). Chromium-on-Dokploy ruled out for v1.
- Three surfaces: Verlauf embed, standalone /projects/{id}/chart (new),
  Custom Views shape='timeline' (Slice 4).
- 4-slice phasing, ~700 LoC Slice 1 (standalone page + horizontal SVG).
- 12 open questions parked for m's review.

Slice 1 NOT auto-spawned — inventor → coder gate stands.

Single commit 8402002 from mai/faraday/project-timeline-chart.
2026-05-09 18:47:06 +02:00
m
84020022a6 design(t-paliad-177): Project Timeline / Chart — visualisation layer above SmartTimeline
Inventor design pass for m/paliad#35. NO IMPLEMENTATION.

Pinned premises:
- SmartTimeline data substrate (projection_service.go, ResponseEnvelope)
  is shipped through Slice 4. Chart is a presentation-only layer.
- No chart libs / PDF libs / headless browser in repo. Bun + std-Go only.
- Custom Views shapes today are list/cards/calendar; "timeline" slot
  reserved by t-paliad-169 §8.6 but not registered.

Recommended:
- Two renderers coexist: existing DOM/CSS shape-timeline.ts (vertical
  embed, Verlauf tab, no changes) + new hand-rolled SVG shape-timeline-
  chart.ts (horizontal Gantt, /projects/{id}/chart standalone). Both
  consume the same TimelineEvent[] + LaneInfo[] substrate.
- Lane model = substrate's existing LaneInfo. No new lane axis. Chart
  adds only render-side state (layout, columns, density, palette, zoom).
- Five built-in palette presets via CSS-var swap (default / kind-coded
  / track-coded / high-contrast / print). No per-user picker in v1.
- Export pipeline:
  - Client-side: SVG (serialize → blob), PNG (drawImage), PDF
    (window.print() + @media print stylesheet).
  - Server-side: CSV (encoding/csv), JSON (alt content type on existing
    /timeline endpoint), iCal (extends caldav_ical.go formatter).
  - Reject chromedp / server-side PDF for v1 — Chromium runtime weight
    not justified by browser-print quality gap.
- Mobile: vertical-only on <640px (horizontal Gantt unreadable on phone).

Phasing (4 sequential slices):
1. Standalone /chart page + horizontal SVG renderer.
2. Export pipeline (SVG/PNG/PDF/CSV/JSON/iCal).
3. Density / palette / zoom controls.
4. Custom Views shape="timeline" registration (cross-project chart).

12 open questions for m's gate. Files implementer touches in Slice 1
listed (~700 LoC frontend, ~50 LoC backend, zero migrations).

Doc: docs/design-project-chart-2026-05-09.md (607 lines).
2026-05-09 18:44:27 +02:00
m
7930ee0bdb Merge: t-paliad-175 — SmartTimeline Slice 4 (lane aggregation + bubble-up + Client toggle) — DESIGN COMPLETE
schroedinger closes the 4-slice phasing of the SmartTimeline per
docs/design-smart-timeline-2026-05-08.md §5 + §10. Final design slice.

Backend (commit 7da8802):
- ProjectionService.levelPolicy(projectType) returns {Kinds, Statuses,
  LaneGrouping} per design §5.1: Case (all/all/self+CCR), Patent
  (deadline+milestone / done+open+overdue / one-per-child-case),
  Litigation (milestone / done / one-per-child-patent), Client
  (milestone / done / one-per-child-litigation, gated by toggle).
- bubble_up handling on paliad.project_events.metadata: events with
  metadata->>'bubble_up'='true' survive the level kind+status filter
  at higher levels. Defaults: counterclaim_created /
  third_party_intervention / scope_change → bubble_up=true on insert
  (bohr's Slice 3 counterclaim path retroactively gets the flag);
  custom_milestone → bubble_up=false with form-checkbox override.
- Wire shape evolved from []TimelineEvent to {events, lanes} envelope
  with each event carrying LaneID. Frontend has defensive fallback.
- Lane-grouping wire format: lanes []LaneInfo{id, label, project_id,
  primary_track?}; one entry per direct child at parent levels.
- Tests: TestLevelPolicy (matrix per project type) +
  projection_levels_test.go +271 LoC integration suite.

Frontend (commit 7e57507):
- shape-timeline.ts: lane-grouped CSS-grid render when lanes.length > 1;
  per-lane sub-headers, time axis shared across lanes, lane filter chip
  in header (multiselect, defaults all-selected).
- projects-detail.tsx + .ts: at Client-level project pages, Verlauf
  defaults to existing matter-list (project tree) with new
  'Timeline-Ansicht' toggle button. Toggle persists in localStorage
  per project. Patent/Litigation already-default to lane view (no
  toggle needed).
- '+ Eintrag → Eigener Meilenstein' form gains a 'Auf Eltern-Ebenen
  sichtbar?' checkbox (default unchecked) for the bubble_up override.

Locked picks per design §11 (no deviations):
- Q5: bubble-up defaults locked
- Q12: Patent + Litigation default lane view; Client matter-list +
  toggle

Verified: go build ./... clean, go vet clean, go test
./internal/services passing, bun build clean (2171 keys).

This closes the 4-slice phasing of t-paliad-169:
- Slice 1 (3e1bbd3): skeleton — actuals + audit toggle + render shape
- Slice 2 (196f3f7): projection + click-to-anchor + #31 layered
  (lookahead + dependency + sequence enforcement)
- Slice 3 (91d3811): counterclaim sub-project + parallel-track
- Slice 4 (this): lane aggregation at Patent/Litigation/Client levels

The SmartTimeline design is fully shipped end-to-end. m can dogfood
the complete flow: anchor a date on a Case, see it bubble up the
hierarchy; create a counterclaim, see parallel tracks at Case level
and as a milestone bubbled up to Patent and beyond.
2026-05-09 16:30:15 +02:00
m
7e57507a92 feat(t-paliad-175): SmartTimeline Slice 4 — frontend lane render + Client Timeline-Ansicht toggle
shape-timeline.ts gains a third render mode triggered by lanes.length>1:
.smart-timeline-lanes-wrap holds a multiselect lane filter chip-row +
the .smart-timeline-lanes grid (one column per lane, time axis vertical
within each lane). Lanes the user has unchecked render dimmed to
preserve time-axis alignment across the strip; "Alle" pseudo-chip
resets to all selected. Lane mode takes precedence over Track-mode
(different axes — lanes group by direct-child project, tracks group
by CCR-vs-parent on a single Case).

loadTimeline parses the new envelope shape {events, lanes} from
GET /api/projects/{id}/timeline; defensive fallback to the old []
shape during the rolling deploy window. selectedLanes state is
client-side (chip toggles re-render in place without a re-fetch);
disappearing lanes (e.g. CCR child deleted between renders) drop
out of the selection automatically.

Client-level Verlauf toggle (Q12 lock-in): on project.type='client',
the Verlauf tab defaults to the matter-list rendering (simple list
of direct child litigations linking through). Flipping the
"Timeline-Ansicht" toggle (visible only at Client level) swaps to
the lane SmartTimeline. State persists in localStorage per project
so navigating away + back keeps the user's choice. Patent +
Litigation default to the lane view, matching Q12.

Custom-milestone form gains the bubble_up checkbox (§7.2 Q5). When
checked, the milestone surfaces on Patent / Litigation / Client
SmartTimelines via the backend's metadata.bubble_up=true override.
Default OFF for custom_milestone — structural milestones
(counterclaim_created etc.) default ON server-side.

CSS: ~130 lines under .smart-timeline-lanes / -lane / -lane-filter /
-matter-list. Mobile collapses lanes to single-column at ≤640px.

i18n: 12 new keys (DE+EN) under projects.detail.smarttimeline.lane.* /
.client.* / .milestone.bubble_up.

Refs: docs/design-smart-timeline-2026-05-08.md §5 + §10 Slice 4
Refs: m/paliad#31, t-paliad-175
2026-05-09 16:27:39 +02:00
m
7da8802f9b feat(t-paliad-175): SmartTimeline Slice 4 — backend levelPolicy + lane aggregation + bubble-up
ProjectionService now dispatches on project type per design §5.1:
- Case (and unknown) — full detail flow: parent track + CCR sub-projects
  + parent_context for CCR children. Lanes mirror tracks ("self" +
  "counterclaim:<id>" + "parent_context:<id>").
- Patent / Litigation / Client — lane-aggregated: load direct children
  matching the axis (cases / patents / litigations), gather subtree
  events per lane, apply (kinds, statuses) filter, tag rows with
  LaneID = direct-child id. Calculator skipped at higher levels —
  predicted future is a Case-level concern.

levelPolicy(projectType) returns the (kinds, statuses, lane_axis)
triple. Patent = deadlines+milestones with done/open/overdue;
Litigation + Client = milestones with done.

metadata.bubble_up on paliad.project_events (no schema change — uses
existing jsonb column) overrides the kind/status filter at higher
levels. Defaults per Q5: counterclaim_created / third_party_intervention
/ scope_change → true; custom_milestone → false (user opts in via
form checkbox). insertCounterclaimEvent now sets bubble_up=true on
both parent + child audit rows so the counterclaim_created milestone
surfaces at Patent / Litigation / Client.

Wire shape changed from []TimelineEvent to envelope {events, lanes} —
lane metadata can ride alongside the rows without exceeding header-
size limits when a Client-level projection has many lanes. Frontend
reads .events for the per-row contract and .lanes for parallel-column
rendering. X-Projection-* headers preserved for Slice 1-3 affordances
(lookahead toggle, track chip).

RecordCustomMilestone gains a bubbleUp bool param; persisted to
metadata.bubble_up only when true (so existing rows-without-it keep
the default-off behaviour).

Tests: TestLevelPolicy locks the triple table; TestRowSurvivesPolicy_
BubbleUpOverridesFilter pins the override contract; TestExtractBubbleUp
covers all per-event-type defaults + explicit override paths;
TestChildTypeForAxis pins the axis → type map. Live integration test
TestProjectionService_LevelAggregation_Live walks the patent-level
fixture: bubbled-up milestone surfaces, regular custom_milestone is
filtered, deadlines surface at Patent level.

Refs: docs/design-smart-timeline-2026-05-08.md §5 + §10 Slice 4
Refs: m/paliad#31, t-paliad-175
2026-05-09 16:22:07 +02:00
m
91d3811276 Merge: t-paliad-174 — SmartTimeline Slice 3 (counterclaim sub-project + parallel-track render)
bohr's Slice 3 of the SmartTimeline per docs/design-smart-timeline-2026-05-08.md
§4 + §10. Counterclaims now first-class as sub-project rows with their own
proceeding type, our_side perspective, and timeline; parent's SmartTimeline
renders them as a parallel right-track on desktop + vertical-stacked sub-headers
on mobile.

Backend (commits 306bb11 + 82888de):
- Migration 077: paliad.projects.counterclaim_of nullable FK ON DELETE SET NULL,
  partial index, and a deferred trigger paliad.projects_no_two_level_ccr that
  rejects malformed two-level CCR-of-CCR chains at the schema level. Defense in
  depth — service-side ErrInvalidInput AND schema-side trigger.
- ProjectService.CreateCounterclaim: atomic create with parent-id placement
  (sibling under patent — child.parent_id = parent.parent_id, fallback to
  parent.id when parent has no parent), our_side flipped by default
  (claimant↔defendant; both stays both), proceeding_type defaults to UPC_REV,
  bilateral counterclaim_created audit rows on both parent + child.
- ProjectService.LoadCounterclaimChildrenVisible.
- ProjectionService.For loads CCR children for parent view; emits
  Track='counterclaim:<id>' rows. CCR child's view also loads parent context
  faded (Track='parent_context:<id>') per design §4.5. AvailableTracks
  surfaced via new X-Projection-Tracks response header.
- POST /api/projects/{id}/counterclaim handler.
- Tests: TestDerivedCounterclaimOurSide (9 cases) + TestCreateCounterclaim_Live
  (4 sub-tests).

Frontend (commit 483649d):
- shape-timeline.ts: CSS-grid wrapper renders one column per available track;
  ≤640px media query collapses to vertical stacking with sub-headers per
  track. [Track ▼] dropdown filters Beide / Nur Hauptverfahren /
  Nur Widerklage purely client-side (no re-fetch).
- '+ Eintrag → Widerklage (CCR)' inline form: proceeding-type select
  (UPC_REV default; UPC_CCI for R.49.2.b path), title + CCR case_number,
  'Stimmt nicht?' toggle for our_side override. POSTs and navigates to
  the new child's /projects/<id>.

Locked picks per design §11 (no deviations):
- Q1: counterclaim = sub-project
- Q2: default-flip our_side with toggle
- Q4: sibling-under-patent placement
- Q8: parallel right-track + Track chip + mobile-stack collapse

Verified: go build ./... clean, go vet clean, go test
./internal/services ./internal/handlers passing, bun build clean (2161
keys). Migration 077 dry-run on live DB succeeded + rolled back; tracker
advances 76 → 77 on next deploy boot.

Out of scope (Slice 4): lane-grouped rendering at Patent / Litigation /
Client levels; 'Timeline-Ansicht' Client toggle; off-script bubble-up.

Sequence enforcement (#31, Slice 2) keeps working independently per track
— anchoring SoD on parent rejects without parent's SoC, same for the CCR
chain on its own. Cross-track is correctly NOT enforced.
2026-05-09 16:09:24 +02:00
m
483649d9d2 feat(t-paliad-174): SmartTimeline Slice 3 — frontend parallel-track render + CCR creation modal
shape-timeline.ts renders multiple tracks side-by-side via a CSS-grid
wrapper (one column per available track). The pre-Slice-3 single-column
flow is reused per column — each track keeps its own past / today /
future / undated structure and its own lookahead toggle. On ≤640px the
grid collapses to a single column with track sub-headers preserved so
the user knows which track they're reading.

A [Track ▼] selector surfaces above the timeline whenever the response
advertises more than the default "parent" track (read from the new
X-Projection-Tracks header). Options: "Beide" (default — render every
track in parallel) / "Nur Hauptverfahren" / "Nur Widerklage". The
filter is purely client-side, so swapping tracks doesn't re-fetch.

Visual treatment: parent track gets the lime accent; counterclaim track
takes the muted surface-2 background so the lawyer reads "this is the
defended side" at a glance; parent_context track is dashed-bordered and
faded to signal the read-only context view.

The previously-disabled "Widerklage (CCR) — kommt mit Slice 3" button
in the "+ Eintrag" modal is enabled and now opens an inline form with
proceeding-type select (defaulted to UPC_REV; populated lazily on first
open from /api/proceeding-types-db), optional title + CCR case-number,
and a "Stimmt nicht?" toggle for the R.49.2.b CCI edge case. POSTs to
/api/projects/{id}/counterclaim and navigates to the new child page on
success.

i18n: 30 keys (15 DE + 15 EN) under projects.detail.smarttimeline.track.*
+ projects.detail.smarttimeline.counterclaim.*. CSS: ~100 lines for the
grid wrapper, per-track visual modifiers, mobile collapse media query,
and the track-chip styling.
2026-05-09 16:07:58 +02:00
m
82888dea78 feat(t-paliad-174): SmartTimeline Slice 3 — projection parallel tracks + counterclaim handler
ProjectionService.For now composes multiple tracks instead of a single
"parent" stream. The viewed project always emits Track="parent"; visible
CCR children emit Track="counterclaim:<child_id>"; a project that is
itself a CCR (counterclaim_of != nil) pulls its target's events as
Track="parent_context:<parent_id>" so the lawyer working the CCR sees
the main proceeding without leaving the page (§4.5).

Each track runs the actuals + projection pipeline independently with
its own lookahead cap and dependency annotations against its own
proceeding's rule tree. SubProjectID + SubProjectTitle are populated on
non-parent rows so the frontend can render the sub-project title in the
column sub-header.

ProjectionMeta gains AvailableTracks; the handler surfaces it as the
new X-Projection-Tracks response header (CSV) so the wire shape stays
[]TimelineEvent (frozen since Slice 1).

POST /api/projects/{id}/counterclaim wraps ProjectService.CreateCounterclaim
— accepts proceeding_type_id / flip_our_side / title / case_number,
returns the new project's id + canonical /projects/<id> URL.

Tests: pure-function coverage for derivedCounterclaimOurSide (default
flip + R.49.2.b override + court/both pass-through). Live-DB integration
test covers the four invariants — CreateCounterclaim atomicity (parent
audit + child audit + our_side flip + sibling-under-patent placement),
parent's projection surfaces the counterclaim track, child's projection
surfaces parent_context, two-level CCR chains are rejected by both the
service guard and the schema-level trigger.
2026-05-09 16:07:37 +02:00
m
306bb11618 feat(t-paliad-174): SmartTimeline Slice 3 — counterclaim sub-project schema + service
Migration 077 adds paliad.projects.counterclaim_of (nullable FK ON DELETE
SET NULL) plus a partial index. A trigger function rejects two-level CCR
chains: a project with counterclaim_of NOT NULL cannot be the target of
another CCR — UPC practice has no CCR-of-a-CCR shape, so reject it at
the schema level rather than defending in the application layer.

ProjectService gains LoadCounterclaimChildrenVisible (list visible CCR
sub-projects against a parent) and CreateCounterclaim (atomic: project
row + creator-as-lead team membership + audit rows on parent AND child).
The CCR child is placed as a sibling under the same patent (§4.4), our
side flips claimant↔defendant by default with a "Stimmt nicht?" override
for the R.49.2.b CCI edge case, and the proceeding type defaults to
UPC_REV. Title auto-suggests from the patent ancestor's patent_number
when available.

Tracker advances 76 → 77.
2026-05-09 16:07:17 +02:00
m
196f3f74a6 Merge: t-paliad-173 — SmartTimeline Slice 2 + m/paliad#31 layered features
gauss's bundle: Slice 2 base from lagrange's design (FristenrechnerService
projection + click-to-anchor + reflow-on-actuals + rule-skipped path) PLUS
m/paliad#31 layered requirements (7-event lookahead cap, dependency
provenance display, SoC→SoD sequence enforcement at the anchor write path).

Backend (commit 85d7dd4):
- Migration 076: appointments.deadline_rule_id FK (nullable, ON DELETE SET
  NULL) + partial index; deadlines.source CHECK extended with 'anchor';
  project_events validation extended with 'rule_skipped' event_type.
- ProjectionService: FristenrechnerService.Calculate integration with
  AnchorOverrides built from completed actuals (per design §6.1); projected
  rows include Status='predicted' / 'court_set' / 'predicted_overdue';
  7-event lookahead cap with ?lookahead=N override (1..50); skipped-rule
  cascade-suppression via project_events WHERE event_type='rule_skipped';
  dependency annotations (DependsOnRuleCode + DependsOnDate + name).
- POST /api/projects/{id}/timeline/anchor: 200 happy path (idempotent
  re-PATCH), 409 predecessor_missing payload (rule code + bilingual name +
  bilingual message) when sequence violation detected.
- POST /api/projects/{id}/timeline/skip: writes project_events
  rule_skipped+milestone for §6.4 'ist nicht eingetreten' decision.
- Tests: projection_anchor_test.go +294 LoC; projection_service_test
  extended.

Frontend (commit 331efc8):
- shape-timeline.ts +425 LoC: projected/court-set/overdue row variants,
  inline click-to-anchor editor (200 reflow / 409 inline-error +
  'Stattdessen <predecessor> erfassen' link), depends-on footer +
  'Pfad anzeigen' chain expansion, 'Mehr / Weniger anzeigen' lookahead
  toggle persisting in localStorage per project.
- FilterBar +99 LoC: timeline_status (predicted/actual/overdue/done/
  off_script) + timeline_track axes; 'Zukunft anzeigen' / 'Nur
  vergangenes' macro chip pair.
- projects-detail.ts: orphan renderEvents() removed (Slice 1 leftover
  band-aided in fermat's 0835be4 — proper cleanup landed here).
- 58 i18n keys (DE+EN), 191 LoC CSS, no hardcoded colours (CSS variables).

Locked design picks (lagrange §11): Q3 'anchor' source, Q9 court-set →
appointments. m/paliad#31 defaults locked as briefed: 7 fixed lookahead,
footer-on-every-row + expand-on-click for deps, 409 hard-reject for
sequence (no confirm-and-write override in this slice).

Verified: bun build.ts clean (2146 keys), go build ./... clean,
projection_service + projection_anchor unit tests passing; integration
tests gated on TEST_DATABASE_URL run on CI.

Migration tracker: 75 → 76 on next deploy boot.

Slices 3 (counterclaim sub-project + parallel-track) and 4 (lane
aggregation at Patent/Litigation/Client) remain queued for m's pace.
2026-05-09 15:45:43 +02:00
m
331efc8603 feat(t-paliad-173): SmartTimeline Slice 2 frontend + #31 layered features
shape-timeline.ts:
- Renders Kind="projected" rows with Status-driven styling: predicted
  (faded grey), court_set (dashed border), predicted_overdue (amber
  fade with overdue glyph).
- "[Datum setzen]" inline date editor on every projected row with a
  rule_code. Submit POSTs /api/projects/{id}/timeline/anchor; 200
  triggers onChange (re-fetch + re-render); 409 renders the
  predecessor_missing payload as inline error with a "Stattdessen
  <predecessor> erfassen" link that scrolls to + opens the parent's
  editor.
- "Folgt aus: <Name> (<Code>, <Date|Datum offen>)" footer on every row
  with depends_on_rule_code, plus "[Pfad anzeigen]" expander hint.
- "[+ Mehr anzeigen]" / "[− Weniger]" lookahead toggle when backend's
  X-Projection-Total header indicates more projections exist beyond
  the current cap.
- Status pills on projected rows surface the status nuance next to
  the kind chip without overwhelming the title.

projects-detail.ts:
- loadTimeline reads X-Projection-{Total,Lookahead} headers and forwards
  them to renderSmartTimeline.
- Lookahead state persisted in localStorage per project (key
  `paliad.smarttimeline.lookahead.<id>`).
- Removes the renderEvents() orphan (band-aid from t-paliad-172) and
  every call site — renderTimeline is the only project-page render
  path now. Aligns with fermat's commit-message hint in 0835be4.

FilterBar (substrate):
- New axes timeline_status / timeline_track (chip clusters, multi-
  select). Macro chip pair "Zukunft anzeigen" / "Nur vergangenes" on
  the timeline_status axis maps to the predicted+court_set subset
  on/off.
- url-codec round-trips ?tl_status= / ?tl_track= so saved Sichten /
  bookmarks survive.

CSS:
- ~80 LoC for .smart-timeline-row--projected/--court_set/--predicted_overdue,
  status pills, depends-on footer, anchor editor, lookahead toggle.
  All tokens reuse existing CSS variables — no bare-hex fallbacks
  (cf. t-paliad-150 dark-mode lesson).

i18n:
- 31 new keys (DE+EN) for projected statuses, depends-on labels,
  anchor editor states, lookahead chips, FilterBar axis labels +
  values + macro chips. 2102 → 2146 total.

Tests:
- projection_anchor_test.go covers applyLookaheadCap (overdue +
  court_set exemption), applyLookaheadDefault clamping,
  ruleAnchorKind dispatch, extractMetadataString, lang normalisation,
  ruleNameInLang, PredecessorMissingError unwrap, annotateDependsOn
  (including parent-of-parent chain dating).

Migration 076 was applied live during dev (tracker 75 → 76); deploy
re-applies idempotently via the embedded migrate path.
2026-05-09 15:43:22 +02:00
m
85d7dd497c feat(t-paliad-173): SmartTimeline Slice 2 backend — projection + anchor + skip + sequence guard
Slice 2 of the SmartTimeline (docs/design-smart-timeline-2026-05-08.md
§6 + §9 + §10) bundled with m/paliad#31's layered requirements:

Migration 076:
- appointments.deadline_rule_id nullable FK to deadline_rules + partial idx
- deadlines.source CHECK widened to include 'anchor' (alongside existing
  'manual','fristenrechner','rule','import').

ProjectionService (extended):
- Wires FristenrechnerService + DeadlineRuleService.
- For() now emits Kind="projected" rows for any rule lacking a matching
  paliad.deadlines.rule_id / appointments.deadline_rule_id row, with
  Status in {predicted | predicted_overdue | court_set}.
- Lookahead cap (default 7, override via ?lookahead=N, max 50): future
  predicted rows beyond N are dropped; predicted_overdue + court_set
  rows are exempt from the cap (#31 layer 1).
- Dependency annotations DependsOnRuleCode/Date/Name on every row that
  carries a DeadlineRuleID, walked from the rule's parent_id chain
  (#31 layer 2). Date prefers actuals over projections.
- AnchorOverrides built from completed deadlines (completed_at /
  status='completed') + appointments tied via deadline_rule_id.
- triggerDate derives from the proceeding's root rule's anchor when
  present, else today() as placeholder.

Anchor write path (POST /api/projects/{id}/timeline/anchor):
- Sequence guard: if rule.parent_id has no anchored actual, return
  409 predecessor_missing with the missing rule's code/name DE+EN +
  pre-formatted bilingual messages so the frontend can render an
  inline error with a "Stattdessen <predecessor> erfassen" link
  (#31 layer 3, no confirm-and-write override in v1).
- kind dispatch: rules with event_type IN ('hearing','decision','order')
  write paliad.appointments with deadline_rule_id; everything else
  writes paliad.deadlines with source='anchor', status='completed',
  completed_at=actual_date.
- Idempotent: existing (project_id, rule_id) row PATCHes instead of
  inserting (race-safe per design §13).

Skip write path (POST /api/projects/{id}/timeline/skip):
- Writes paliad.project_events with event_type='rule_skipped' +
  metadata.rule_code; subsequent reads drop the matching projected
  row from the cascade (§6.4).

Handlers expose projection meta via X-Projection-{Has,Total,Shown,Overdue,Lookahead}
headers so the wire shape stays []TimelineEvent (frozen since Slice 1).
2026-05-09 15:33:20 +02:00
m
335be29b23 Merge: t-paliad-172 — fix Verlauf-tab-stuck regression from Slice 1
Slice 1 (3e1bbd3) of the SmartTimeline replaced the legacy
<ul#project-events-list> markup with <div#project-smart-timeline> but
left the orphan renderEvents() function and its call site in place.
renderEvents() did getElementById('project-events-list')! — non-null
asserted on a node that no longer existed. main() called renderEvents()
between body.style.display = '' and initTabs(). The null deref threw,
main() aborted, initTabs() never ran, and tab click handlers never
attached. Tab clicks went to <a href='#'> defaults; the URL got '#'
appended but no panel transition happened — m's 'stuck on Verlauf'
report (12:25).

fermat's minimal fix: drop the ! assertions, null-guard, return early
when the legacy DOM nodes are gone. 10 lines, one file
(frontend/src/client/projects-detail.ts:867-883). Comment points at
Slice 2 for the proper removal of the orphan call site.

Verified: bun build.ts clean, go build clean, Playwright reproduces the
TypeError on main and confirms tabs work post-fix. Empty-state Smart
Timeline still renders, '+ Eintrag' modal still opens/closes, Audit-Log
toggle still present.

Single commit 0835be4 from mai/fermat/bug-verlauf-tab-open-can.
2026-05-09 12:39:02 +02:00
m
0835be4a7f fix(t-paliad-172): null-guard renderEvents to unblock tab clicks
Slice 1 of SmartTimeline (t-paliad-171, commit 7057fe5) removed the
legacy <ul#project-events-list> + <p#project-events-empty> markup from
projects-detail.tsx but didn't prune the renderEvents() call sites. The
function still runs from main() and several other paths; with non-null
assertions on getElementById, the null deref threw a TypeError mid-init.

The throw aborted main() between body.style.display = "" and initTabs(),
so the .entity-tab click handlers were never attached. Default-action
clicks on <a href="#"> just appended "#" to the URL while the user was
already viewing whatever panel happened to be the default-display
section (tab-history) — making the Verlauf tab feel "stuck" because the
visible panel never changed.

Fix: drop the non-null assertions, null-guard the legacy DOM lookups,
and return early when the targets are gone. renderEvents() becomes a
silent no-op in the SmartTimeline layout, which matches euler's intent
documented in 7057fe5: "The legacy renderEvents() rendering path stays
as-is (dead, but the function is still called in places). It will be
removed once /timeline?include=audit_full has had a deploy of soak time
… Slice 2 revisits."

Verified locally with the projects-detail.js bundle + a fetch mock:
clicks on Team / Projektbaum / Parteien / etc. now switch the active
tab and panel display, the URL updates via replaceState, the
SmartTimeline still renders its empty state, and the "+ Eintrag" modal
still opens and closes correctly.
2026-05-09 12:38:24 +02:00
m
3e1bbd3c77 Merge: t-paliad-171 — SmartTimeline Slice 1 (skeleton; actuals only, no projection)
euler's first slice of the SmartTimeline per docs/design-smart-timeline-2026-05-08.md
§10. Past actuals + audit-log toggle + new render shape; NO projection logic
yet (Slice 2). NO counterclaim FK (Slice 3). NO lane aggregation (Slice 4).

What lands:
- Migration 075: nullable paliad.project_events.timeline_kind text + partial
  index (auto-applied at server boot via golang-migrate)
- Backend: ProjectionService + GET /api/projects/{id}/timeline + POST
  /api/projects/{id}/timeline/milestone, with unit + integration tests
- Frontend: shape-timeline.ts vertical-two-column render shape, '+ Eintrag'
  modal (Eigener Meilenstein wired; Frist/Termin link out; Widerklage/R.30
  disabled with 'Slice 3' tooltip), 'Audit-Log anzeigen' toggle persisting
  per-project in localStorage
- FilterBar (riemann's t-paliad-170 port) keeps mounting + working — facet
  set unchanged

Two flagged deviations from §3.2 mockup, parked for m's review:
- Render order: chronological top-down (past at top, future at bottom);
  mockup had future-above-past. Trivial CSS flip if m prefers.
- Legacy renderEvents() function not removed — Slice 2 cleanup.

Verified: bun build.ts clean (2117 keys), go build ./... clean, go test
./internal/services ./internal/handlers passing (live integration test
gated on TEST_DATABASE_URL; runs on CI).

Live tracker at v74 pre-deploy → v75 after Dokploy boot.

Commits: 49c260b 49c260b afd3aab 4a5d56d 7057fe5 from
mai/euler/smarttimeline-slice-1.

Slices 2-4 + 12 open inventor questions remain parked for m's morning
review of docs/design-smart-timeline-2026-05-08.md.
2026-05-08 23:42:39 +02:00
m
7057fe5d25 feat(t-paliad-171): mount SmartTimeline + "+ Eintrag" modal in /projects/<id> Verlauf
Replaces the legacy <ul.entity-events> Verlauf rendering with the new
SmartTimeline. Slice 1 wiring:

  - loadTimeline(id) calls /api/projects/{id}/timeline (the new
    endpoint backed by ProjectionService) and renderSmartTimeline
    paints into <div#project-smart-timeline>.
  - "Audit-Log anzeigen" header toggle re-fetches with
    ?include=audit_full, broadening the project_events filter to
    every audit row (legacy Verlauf chronological view). State
    persists per-project in localStorage so flipping it on for one
    case doesn't carry across to others.
  - "+ Eintrag" CTA opens a modal. "Eigener Meilenstein" submits
    via POST /api/projects/{id}/timeline/milestone and re-renders;
    Frist + Termin route to the existing /deadlines/new and
    /appointments/new flows; CCR + R.30 are disabled-with-tooltip
    "kommt mit Slice 3" per the design.
  - Subtree toggle now also drives the timeline (passes
    ?direct_only=true when the user flips off "Inkl. Unterprojekte").
  - Project-appointment add path also re-fetches the timeline so the
    new appointment surfaces immediately.

The legacy renderEvents() rendering path stays as-is (dead, but the
function is still called in places). It will be removed once
/timeline?include=audit_full has had a deploy of soak time and the
audit-toggle is the only path that feeds the legacy markup. Slice 2
revisits.

The FilterBar from t-paliad-170 (riemann's port) keeps mounting and
driving its customRunner — facets still narrow the legacy `events`
array. The bar gaining timeline_* axes lands later in the slice
sequence (design §8); Slice 1 ships the timeline beneath the existing
bar untouched.

Design ref: docs/design-smart-timeline-2026-05-08.md §10 Slice 1.
2026-05-08 23:41:11 +02:00
m
4a5d56d9e6 feat(t-paliad-171): SmartTimeline render shape — shape-timeline.ts + CSS + i18n keys
The vertical-timeline render component for the SmartTimeline (Verlauf
tab redesign). Two-column layout (date / event card), past
chronological → "Heute →" rule → future chronological, status icon +
kind chip per row.

Deep-link is wired via a row-level click handler that skips clicks on
inner <a>/<button>, NOT a ::before overlay — matches the project's
.entity-event whole-card click contract (project CLAUDE.md), keeps
text selection working, and avoids the t-102 overlay regression that
swallowed pointer events on the title text.

i18n: 28 new keys under projects.detail.smarttimeline.* (DE primary,
EN secondary). i18n-keys.ts is regenerated by build.ts on every build,
so the diff there is mechanical.

CSS: ~250 LoC under .smart-timeline-* — vertical layout, status-icon
glyphs per status (✓/…/!/▢/░/⊕), kind-chip pastels, Heute → rule with
borders extending into the spacing.

Design ref: docs/design-smart-timeline-2026-05-08.md §3.1-3.3.
2026-05-08 23:40:49 +02:00
m
afd3aab2b2 feat(t-paliad-171): SmartTimeline backend skeleton — ProjectionService + /timeline endpoint
Slice 1 of the SmartTimeline (Verlauf-tab redesign). Adds a new service
layer + two HTTP endpoints; no projection logic yet (Slice 2). The wire
shape (TimelineEvent) is frozen so future slices add Kind="projected"
rows additively without breaking the frontend consumer.

ProjectionService.For composes three actuals streams for one project:
  - paliad.deadlines           → Kind="deadline"
  - paliad.appointments        → Kind="appointment"
  - paliad.project_events with
    timeline_kind IS NOT NULL  → Kind="milestone"

Visibility goes through the existing inline mirror of
paliad.can_see_project on each underlying service — no new RLS surface.
DirectOnly mirrors the existing "Inkl. Unterprojekte" toggle on
/projects/{id}; IncludeAuditFull broadens project_events to the full
audit log behind the upcoming "Audit-Log anzeigen" header toggle.

ProjectionService.RecordCustomMilestone backs POST /timeline/milestone
("Eigener Meilenstein") — the only write path in Slice 1.

Tests: unit (sort order, status mapping, kind tiebreak — runs by default)
plus a live integration test that seeds one project + dl + appt +
milestone and asserts the merge surfaces all three with the right
ordering. Live test gated on TEST_DATABASE_URL per the existing
convention.

Design ref: docs/design-smart-timeline-2026-05-08.md §2.3 + §9.2 + §10.
2026-05-08 23:34:06 +02:00
m
49c260b888 feat(t-paliad-171): migration 075 — project_events.timeline_kind opt-in column
Adds a nullable text column on paliad.project_events so a subset of
audit rows can opt into surfacing as SmartTimeline content. Existing
rows stay NULL (audit-only); the partial index keeps the lookup tiny
because the SmartTimeline read filter is the indexed predicate.

Value space (enforced in code in internal/services/projection_service.go):
  'milestone'        — structural event (counterclaim_filed, ...)
  'custom_milestone' — free-text "Eigener Meilenstein"
  NULL               — audit only (default)

Design ref: docs/design-smart-timeline-2026-05-08.md §2.2.
2026-05-08 23:33:53 +02:00
m
12b35fc9fe Merge: t-paliad-170 — FilterBar mounted in /projects/<id> Verlauf tab
riemann's Phase 2 slice on top of own 1faffb6 Phase 1: the universal
<FilterBar> is now in the project Verlauf tab. Filter facets:
project_event_kind (chip cluster), time (presets including new
HorizonPast7d), personal_only. Empty URL preserves current behaviour
(unfiltered list); ?time=past_30d&pe_kind=deadline_created narrows.

Two extension points added to the bar primitive (forward-compat with
SmartTimeline t-paliad-169 work):
- customRunner: lets a host page own the data fetch (Verlauf keeps
  the legacy /api/projects/{id}/events pipeline so subtree + cursor
  pagination survive — substrate-side scope-with-descendants stays
  SmartTimeline territory).
- timePresets: opt-in past-only horizon set for backward-looking
  surfaces (vs the default future-leaning set used on /inbox).

3-way merge with main: clean. fourier's t-paliad-168 + lagrange's
SmartTimeline design doc preserved.

bun build clean; frontend/dist regenerated. go test internal/... ok
on riemann's worktree (filter-bar url-codec + filter_spec tests).

ebcda13 from mai/riemann/filterbar-phase-2-slice.
2026-05-08 23:23:49 +02:00
m
ebcda13f88 feat(t-paliad-170): mount <FilterBar> in /projects/<id> Verlauf tab
Phase 2 slice of the universal-filter migration (Phase 1 was
t-paliad-163 → /inbox; remaining /agenda /events /deadlines
/appointments stay queued).

What ships:

- FilterBar gains two non-invasive options that future surfaces will
  also need:
    customRunner — bypass the substrate POST and hand the effective
                   spec to a surface-supplied runner. Required by
                   surfaces whose data path can't move to the substrate
                   yet (Verlauf still uses /api/projects/{id}/events for
                   subtree expansion + cursor pagination, both absent
                   from the substrate's project_event runner).
    timePresets  — per-surface override of the time chip cluster, so
                   backward-looking surfaces can show past_*+all without
                   forcing forward-looking next_* chips on every host.

  systemViewSlug becomes optional; the bar enforces "exactly one of
  customRunner | systemViewSlug" at construction.

- project_event_kind axis renderer (was a null stub) — chip cluster
  over KnownProjectEventKinds, labels reuse the existing
  event.title.<kind> i18n table so the chip text matches the Verlauf
  row title for the same kind.

- HorizonPast7d added end-to-end (substrate validate +
  computeViewSpecBounds; FilterBar TimeOverlay + parseHorizon; views
  TimeHorizon mirror) so the chip value is valid in every layer when a
  later SystemView reuses it.

- Verlauf tab on /projects/<id> mounts the bar with
  axes=["time","project_event_kind"], timePresets=
  ["past_7d","past_30d","past_90d","any"], showSaveAsView=false. The
  customRunner reads predicates.project_event.event_types + time.horizon
  off the effective spec, sets a verlaufFilters global, and routes
  through the legacy loadEvents/loadMoreEvents pipeline (which now
  applies the filter set client-side and tracks raw cursor IDs so
  "Mehr laden" still walks the underlying pagination boundary even when
  most rows get filtered out of a page).

- Subtree toggle drives loadEvents through verlaufBar.refresh() so the
  current filter state survives the toggle.

URL state reuses the bar's existing keys (?time=past_30d, ?pe_kind=…).
Empty filter → identity passthrough → current behaviour preserved.

Out of scope (deferred to t-paliad-169 SmartTimeline):
  - Migrating Verlauf to the substrate (needs scope-with-descendants)
  - Past/future split, dated/undated split, source-track facet

Refs m/paliad#23.
2026-05-08 23:22:23 +02:00
m
487fec2672 Merge: t-paliad-169 — SmartTimeline design doc (DESIGN READY FOR REVIEW)
lagrange's inventor pass on m's 23:02 request: redesign the Verlauf tab on
/projects/{id} as a SmartTimeline composing past actuals + future-projected
(via existing AnchorOverrides reflow on FristenrechnerService.Calculate) +
off-script events. Counterclaim shape: sub-project with new counterclaim_of
FK; parent renders parallel right-track when populated.

Doc covers: data-model recommendation (virtual view, ONE optional column),
UI mockup (3 states), counterclaim shape (defended), parent-node aggregation
(per-level kinds/statuses/lanes rule), date-anchoring + reflow semantics,
off-script event UX, 12 open questions, 4-slice phasing.

12 open questions parked for m's review before Slices 2-4. Slice 1 is the
skeleton (no projection yet) — must merge AFTER riemann's t-paliad-170
FilterBar port; pending riemann.

Issue m/paliad#27. Single commit f8cc86c, 739 lines, design only — no
implementation in this merge.
2026-05-08 23:17:48 +02:00
m
f8cc86cd02 docs(t-paliad-169): SmartTimeline design — Verlauf-tab redesign with past+future+off-script
Inventor design for replacing the project-page Verlauf with a SmartTimeline that
composes past actuals (deadlines, appointments, structural project_events),
present, future-projected (deadline_rules calculator at read time), and
off-script events into one project-scoped vertical timeline.

Key calls:
- virtual view, no new top-level table; single optional column
  paliad.project_events.timeline_kind so a subset of audit rows surface as
  timeline content
- counterclaim = sub-project (new paliad.projects.counterclaim_of FK), parent
  renders parallel tracks; default our_side flips on creation
- date-anchoring reuses fristenrechner CalcOptions.AnchorOverrides — actuals
  anchor downstream projections automatically
- new ProjectionService.For(projectID) thin adapter over FristenrechnerService
- 3 new FilterBar axes (timeline_kind, timeline_status, timeline_track) +
  reuse of time, personal_only, deadline_event_type
- per-level aggregation rule: each level removes one tier of detail and adds
  one tier of grouping (Case → Patent → Litigation → Client)
- 4-slice phasing: skeleton, projection+anchor, counterclaim sub-project,
  parent-node aggregation

12 open questions for m before slice 1 PR opens. Inventor parks per gate
protocol; coder shift only after m's go-ahead.
2026-05-08 23:14:30 +02:00
m
69544bf3fb Merge: t-paliad-168 — Verfahrensablauf entry points (Step 2 third card + sidebar nav)
m's complaint @ 22:49 'Verfahrensablauf section… is gone' — the Pathway A wizard
was reachable only via Step 1 → Step 2 (einreichen) → Step 3a (file), three clicks
deep and framed as 'I'm filing a brief.' fourier restores two top-level entries:

- Step 2 third card 'Verfahrensablauf einsehen' (browse / learn) → ?path=a
- Sidebar Werkzeuge entry 'Verfahrensablauf' (open-book icon) → ?path=a

In both browse paths the save-to-project CTA disables (no Akte to save against).
Deliverable 3 (project-page Verfahrensablauf tab) deferred — the SmartTimeline
redesign (t-paliad-169, lagrange) will determine the right component shape.

Commits: 7238b12 (Step 2 card), 7fef641 (sidebar entry).

Closes part of m/paliad coverage; SmartTimeline tracked separately.
2026-05-08 23:05:47 +02:00
m
7fef64159b feat(sidebar): add Verfahrensablauf nav entry
t-paliad-168 deliverable 2. New "Verfahrensablauf" entry under
Werkzeuge, right after Fristenrechner — opens
/tools/fristenrechner?path=a (Pathway A wizard, browse-/learn-mode).

Uses a distinct open-book icon to read separate from the closed-book
Glossar. Both /tools/fristenrechner sidebar entries share the same
pathname, so SSR navItem matching can't pick the right "active" one
on its own — fixVerfahrensablaufActive() in sidebar.ts disambiguates
based on ?path=a at hydration.

i18n key: nav.verfahrensablauf (DE: "Verfahrensablauf",
EN: "Procedure Roadmap"). i18n-keys.ts is regenerated by build.ts.
2026-05-08 23:04:29 +02:00
m
7238b12b05 feat(fristenrechner): Step 2 third card "Verfahrensablauf einsehen"
t-paliad-168 deliverable 1. Adds a discoverable browse-/learn-mode
entry to the determinator alongside "Etwas einreichen" / "Etwas ist
passiert". Click drops straight into Pathway A's proceeding-tile
picker (navigateToPathway("a")).

The save-to-project CTA disables itself in this mode — extends
isAdhocMode() to also return true when no Step 1 context is set,
mirroring the existing ad-hoc explore behaviour.

i18n keys: deadlines.step2.browse.title / .desc (DE + EN).
2026-05-08 23:03:52 +02:00
m
54cf7ac2f6 Merge: t-paliad-167 — Determinator coverage audit (research)
f4815a9 — Determinator coverage audit @ docs/research-determinator-
coverage-2026-05-08.md (394 lines).

Headline numbers (n=76 true Fristenrechner deadlines across 19 active
proceedings):
  Reachable from cascade:    71  (93 %)
  No concept_id:               1  (1 %)
  Concept exists, dead-end:    4  (5 %)

§4 frames the smart-navigation question into a taxonomy of three
"I don't see my event" failure modes — α (real content gap), β
(reachable but mis-modelled path), γ (court-side trigger needs to be
tagged, not reacted-to) — and maps each to the candidate UX patterns
(P1 free-text search / P2 escape-with-telemetry / P3 weiter-unten-
suchen). The recommendation surfaces:

  - P2 + telemetry for type-α (capture which events users actually
    want; drives prioritised migration backlog rather than guessing)
  - P1 + P3 for type-β (search collapses labelling mismatches; flat-
    branch search recovers from wrong-root entries)
  - Type-γ flagged as a separate "tag, don't react" workstream out of
    this scope

Pure research — no code, no schema. Feeds m's next decision: extend
the row-by-row B1 refactor (m/paliad#25 / minkowski's parked task) or
spin a separate inventor pass on the smart-navigation surface.

Refs m/paliad#26.
2026-05-08 22:35:24 +02:00
m
f4815a9f9a docs(t-paliad-167): Determinator coverage audit + smart-navigation framing
Builds on t-paliad-159's UPC RoP audit. Drives from paliad's own corpus
outward: every active rule, every firm-wide event_type, every cascade
leaf — and asks whether a Determinator user can actually reach the row.

Headline finding: 71/76 (93%) of true Fristenrechner deadlines are
reachable from the cascade. The 5 unreachable cluster into one fix:
EP_GRANT (4 rules) plus UPC_INF.inf.app_to_amend lack cascade entry.
Adding an `ich-moechte-einreichen.ep-erteilung` subtree lifts coverage
to 100%.

Per-jurisdiction inventory (UPC, DE, EPO, DPMA) plus a §2.6 cross-cutting
table for the procedural-order categories m flagged (Hinweisbeschluss,
Beweisbeschluss, Streitwertbeschluss, Versäumnisurteil, R.71(3),
Beanstandungsbescheid, etc.).

§4 frames the smart-navigation choice: recommends P2 (persistent escape
button with capture) + P1 (free-text search per cascade level), defers
P3 (flatten deeper levels) until telemetry justifies it. The captured
"Mein Ereignis ist nicht dabei" texts feed both the gap-fill roadmap
and P1's ranking corpus.

No code changes; one markdown doc, 394 lines.
2026-05-08 22:34:23 +02:00
m
ce180123c3 Merge: t-paliad-165 follow-up — Regel+Typ as one field + jurisdiction-aware defaults
Two slices on mai/noether/collapse-regel-typ-on (after rebase onto main):

  6058d21  fix(deadline-rules): pick rule's jurisdiction-aware event_type default
  7a35cad  feat(deadlines/new): collapse Regel + Typ to ONE field when rule sets type

What ships:

- Migration 074 audits the deadline_concept_event_types seed and adds
  per-jurisdiction defaults so a German rule (RoP.029.b /
  § 276 Abs. 1 S. 2 ZPO) maps to the DE event_type
  (de_klageerwiderung) and a UPC rule maps to the UPC event_type
  (upc_statement_of_defence). The text label "Klageerwiderung" reads
  the same in both — the bug m hit at 22:08 was the seed defaulting
  to UPC for every concept regardless of which rule asked.
  Idempotent (IF NOT EXISTS / DO blocks). Live tracker advanced
  73 → 74 during noether's dev run; deploy will see tracker=74 with
  file 074 present and have nothing to apply.

- Frontend deadline create form (m's "these two are connected — it's
  the same thing", #18 + 22:08 dogfood):
    When a Regel is selected and a default event_type exists for it,
    the Typ chip COLLAPSES into an inline pill beneath the rule:
      "Typ: Klageerwiderung (vorgegeben durch Regel)  [Anderen Typ wählen]"
    Clicking [Anderen Typ wählen] re-expands the picker so the user
    can override.
    When the rule has no junction row (or the user hasn't picked a
    rule), the Typ field stays as today (free-text + chip cluster).

- deadline_rule_service.go switched to the jurisdiction-aware lookup;
  the form receives the right default in one round-trip.

Refs m/paliad#18 + the 2026-05-08 22:08 inline-correction thread.
2026-05-08 22:21:48 +02:00
m
7a35cad09f feat(deadlines/new): collapse Regel + Typ to ONE field when rule sets type
m's 2026-05-08 22:08 dogfood: my first auto-fill landed but kept Regel
and Typ as TWO separate input fields. m wanted ONE — "these two are
connected, it's the same thing".

Now: when a Regel is selected and the rule's concept resolves to a
canonical event_type via the jurisdiction-aware junction, the Typ
chip cluster is HIDDEN and replaced by an inline summary —

    Klageerwiderung (vorgegeben durch Regel)   Anderen Typ wählen

Clicking "Anderen Typ wählen" sets a sticky expandedOverride flag
that forces the picker visible for the rest of the form session.
The chip stays in the picker so the user can edit / remove it.
The picker also stays visible when the rule has no canonical
event_type (fallback to free-text Typ) or when the user has picked
a different event_type from the canonical default (mismatch
warning surfaces yellow next to the picker, never blocking).

DE+EN i18n: deadlines.field.rule.{autofill_inline,override}.
New CSS: .event-type-collapsed{,-label,-source,-override} reusing
the existing lime-tint chip palette.
2026-05-08 22:20:48 +02:00
m
6058d21ce6 fix(deadline-rules): pick rule's jurisdiction-aware event_type default
m's 2026-05-08 22:08 dogfood: rule '§ 276 Abs. 1 S. 2 ZPO — Klageerwiderung'
(DE) auto-filled to 'Klageerwiderung' label but the chosen event_type was
upc_statement_of_defence (UPC). Both render as 'Klageerwiderung' in the
UI, but they are different legal events in different jurisdictions.

Migration 074 adds a jurisdiction column to
paliad.deadline_concept_event_types and swaps the unique-default index
from per-concept to per-(concept, jurisdiction). Backfills jurisdiction
from each event_type's own column, then re-elects DE / DPMA / EPO
defaults where a non-UPC event_type genuinely exists. Idempotent: uses
ADD COLUMN IF NOT EXISTS, ON CONFLICT DO UPDATE, partial unique index.

DeadlineRuleService.hydrateConceptDefaultEventTypes now JOINs
paliad.proceeding_types and matches on (rule.concept, rule.jurisdiction)
with EPA→EPO canonicalisation. Rules whose (concept, jurisdiction) has
no default stay NULL — silent no-op on the form, better than a wrong
jurisdictional default. UPC rules unchanged; DE rules now resolve to
de_klageerwiderung when concept = statement-of-defence, else no autofill.

Live audit confirms: every active rule now resolves to a same-
jurisdiction event_type or no event_type at all. No more cross-
jurisdiction matches in the seed.
2026-05-08 22:16:55 +02:00
m
52caba51ec fix(inbox): default approval_viewer_role chip to "any_visible" so the page lands populated
m's 2026-05-08 22:08 dogfood, after t-paliad-163 Phase 1: "I like the
new inbox filters but now the inbox somehow does not show nothing no
more..." The new bar opened with the chip defaulted to
"approver_eligible" (the legacy "Zur Genehmigung" tab semantics —
requests EXCLUDING ones the caller authored). For users who only
SUBMIT requests and have nothing to approve themselves (incl. m,
who has 4 own pending submissions and 0 incoming), that's an empty
view.

Flip the default to "any_visible" on both ends:

- internal/services/system_views.go InboxSystemView.Filter — base
  spec ViewerRole = "any_visible".
- frontend/src/client/filter-bar/axes.ts approval_viewer_role chip —
  default = "any_visible" when the URL doesn't pin one. The two
  defaults are intentionally redundant: the server narrows on its
  default if the request omits a_role, and the chip highlights the
  same option on the empty URL.

The chip still narrows. "Zur Genehmigung" + "Eigene Anfragen" stay
one click away; the bar just doesn't pre-narrow into "Zur Genehmigung"
on first visit anymore.

The "/views/inbox-mine" SystemView (slug + URL stays "self_requested")
keeps its narrower default — that route exists precisely to land on
the requester's view.
2026-05-08 22:11:19 +02:00
m
1faffb682e Merge: t-paliad-163 Phase 1 — universal <FilterBar> primitive + /inbox migration
Three slices on mai/riemann/inventor-universal:

  d5a01e6  Slice 1 — RenderSpec.list.row_action + validator + tests
  de4e133  Slice 2 — <FilterBar> scaffolding (axes / url-codec / save-modal)
  4670cd6  Slice 3 — /inbox migrates to <FilterBar>; tabs collapse to chips

What ships (Phase 1):

- A new frontend/src/client/filter-bar/ module:
    types.ts        — Spec + RenderSpec + AxisDeclaration types
    axes.ts         — registry of supported filter axes
    url-codec.ts    — URL ↔ FilterSpec serialization (round-tripping)
    save-modal.ts   — "Speichern als Sicht" dialog
    index.ts        — <FilterBar> mounts
  Plus a url-codec.test.ts golden table.
- /inbox surface migrates to the bar:
    Top-level "Zur Genehmigung / Meine Anfragen" tabs collapse into the
    bar's `approval_viewer_role` chip cluster (incoming / outgoing /
    both). One control, three mutually exclusive options. Stateful via
    `?role=` URL param.
    Bookmark-friendly: legacy `?tab=mine` + `?tab=pending-mine` redirect
    to `?role=outgoing` and `?role=incoming` respectively for one
    release.
    Sortable column headers on the result list (list-shape only;
    cards/calendar shape-modes defer their own ordering to the spec).
- RenderSpec.list gains `row_action` ("navigate" | "expand" | "none")
  so list-shape surfaces declare row click behaviour explicitly. The
  validator + tests cover the new field.
- system_views.go gains the inbox SystemView definitions so the bar
  reads its base spec from the same registry that custom views use.

m's locked positions (commit `1e23745` design doc; m's greenlight
2026-05-08 21:47): all 11 default picks honoured. Q4 = collapse
tabs to chips ✓.

Phase 2 surfaces (port /agenda → bar; port /events → bar; port
/deadlines → bar; port /appointments → bar) follow as separate PRs.

Refs m/paliad#23.
2026-05-08 22:03:51 +02:00
m
4b681792ab Merge: t-paliad-165 — Regel ↔ Typ collapse via auto-link on the deadline create form
Two slices on mai/noether/collapse-regel-typ-on:

  0c12644  feat(deadline-rules): expose concept's canonical event_type per rule
  1e97ecc  feat(deadlines/new): auto-link Typ to Regel's concept

What ships:

- New junction paliad.deadline_concept_event_types maps every
  paliad.deadline_concepts row to its canonical paliad.event_types
  row(s). Many-to-many for concepts with multiple legitimate variants
  (statement-of-defence ↔ base + with_ccr + no_ccr; opposition across
  EPO + DPMA). Exactly one row per concept marked is_default = true
  by a partial unique index — that is the row the deadline form
  auto-fills with.

- Backend: paliad.deadline_rules_with_concept_event_type view + the
  deadline-rules read path now expose the rule's default concept
  event_type so the form has the auto-fill target without an extra
  round-trip.

- Frontend deadline create / edit form: when the user picks a Regel,
  the Typ chip auto-fills with the rule's concept's default event_type.
  A small "vorgegeben durch Regel — überschreiben?" hint sits next to
  the chip so the auto-fill is visible. The user can override (free-
  text or pick a different type); the override is explicit, no
  blocking validation.

- Free-text Typ stays available — manual deadlines without a
  matching rule (e.g. "Call me" reminders) keep working as today.

Migration housekeeping
======================

noether authored her migration as 072 on her branch but main had
already taken 072 via minkowski's t-paliad-164 (paliad.projects.our_side).
Renumbered to 073 during merge resolution to resolve the same-number
collision. Added IF NOT EXISTS guards on CREATE TABLE / CREATE INDEX
for re-run safety (the seed INSERT already had ON CONFLICT DO NOTHING).

Live tracker bumped 72 → 73 in the same operation: both effects
(our_side column AND deadline_concept_event_types table) were
applied to live during dev (each worker against the same DB), so
the tracker advance reflects schema reality. Next deploy sees
tracker=73 with file 073 present and has nothing to apply.

Refs m/paliad#18.
2026-05-08 22:01:44 +02:00
m
236bb3270e Merge: t-paliad-164 — project our_side + Determinator perspective predefine
Three slices on mai/minkowski/project-level-our-side:

  188d8ec  Slice 1 — paliad.projects.our_side column + service plumbing
  5d9c62d  Slice 2 — "Wir vertreten" select on the project edit form
  3a41ace  Slice 3 — Determinator predefines perspective from our_side

What ships:

- Migration 072 adds paliad.projects.our_side text with check constraint
  IN ('claimant','defendant','court','both', NULL). Idempotent
  (IF NOT EXISTS / DO blocks). NULL stays the default.
- Project model + service plumbing: OurSide *string on models.Project,
  threaded into Create / Update / SELECT projections + handlers.
- Project edit form: new "Wir vertreten" select with the four options
  + "unbekannt / nicht gesetzt", DE+EN i18n.
- Fristenrechner Determinator (Slice 3c — perspective chip): when a
  project is selected and our_side is set, the chip is predefined to
  that value with a "vorgegeben durch Akte" hint above. The user can
  still override (chip click); the override is explicit. When
  our_side is NULL, the existing free-pick behaviour stays.

m's dogfood (2026-05-08 21:42): "We chose a case of ours where our
side should be predefined - yet I can make a selection for which
side we are." Now resolved end-to-end: edit the project once to set
"Wir vertreten = Klägerseite", and the Determinator perspective chip
auto-locks to that side on every subsequent visit.
2026-05-08 22:00:13 +02:00
m
4670cd660a feat(inbox): migrate to <FilterBar> — t-paliad-163 Slice 3
/inbox is the first surface to consume the universal FilterBar. The
two-tab UI collapses into the bar's approval_viewer_role chip cluster
(per Q4 lock-in 2026-05-08 21:47); status / entity_type / time chips
are new affordances; density toggle gives the activity-feed look the
brief asked for.

Changes:
- system_views.go: InboxSystemView + InboxRequesterSystemView render
  spec gains RowAction=approve so shape-list.ts knows which row
  layout to stamp (entity title + diff + approve/reject/revoke).
- shape-list.ts: row_action='approve' branch — stamps the inbox-row
  markup the surface owned today; surface attaches click handlers
  via data-attrs on .views-approval-action / .views-approval-row.
- inbox.tsx: tab row replaced with <div id='inbox-filter-bar'> +
  <div id='inbox-results'>. Heading + admin nudge unchanged.
- client/inbox.ts: shrunk to mountFilterBar with axes [time,
  approval_viewer_role, approval_status, approval_entity_type,
  density, sort]. Action handlers run via fetch + bar.refresh().
  Legacy ?tab=mine -> ?a_role=self_requested redirect on mount so
  bookmarks / sidebar bell still land on the right sub-view.

Build clean: bun run build + go build/vet/test all pass.
2026-05-08 21:59:44 +02:00