Slice 3 step 1. Four range presets per design §10 + faraday-Q8 default:
1y (today-1y..today+1y, default), 2y, all (derives bounds from loaded
events with a +30d right pad), and custom (date-pair inputs).
mount() grows currentRangePreset + customRangeFrom + customRangeTo so
the layout-time viewport is computed from the live preset, not the
constructor-time opts. resolveRange() handles the four cases; "all"
calls rangeFromEvents() over the last fetched timeline so completing
or adding a row reflows on next repaint.
URL state in ?range=1y|2y|all|custom (omit when 1y); custom adds
?from=&to=. ISO_DATE_RE guards malformed input. Custom date-pair
shows / hides based on the preset.
i18n: 7 new keys DE+EN under projects.chart.range.*.
Design ref: docs/design-project-chart-2026-05-09.md §8.2 + §10 + §14 Q8.
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.
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.
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.
Server-side endpoint GET /api/projects/{id}/timeline.ics returns a
VCALENDAR + one VEVENT per actual deadline (VALUE=DATE all-day) and
appointment (UTC timestamp). Projected / milestone / off_script rows
are deliberately skipped — faraday-Q6 / m's pick: a calendar feed
must never carry predicted dates the user never confirmed, otherwise
Outlook fills with rule_code-derived events that erode trust.
FormatTimelineICS reuses the existing caldav_ical.go escape helpers
and writes through the same canonical UIDs (paliad-deadline-<id> +
paliad-appointment-<id>) so a re-subscribe updates entries instead
of duplicating them. Stable across re-exports = lawyer-safe.
Visibility piggybacks on ProjectionService.For + ProjectService.GetByID
(same gates as the chart page handler). Content-Disposition filename
slugged for portable ASCII so Outlook + Apple Calendar agree.
4 tests pin the contract: only deadline/appointment kinds emit
VEVENTs; undated rows skip cleanly; RFC 5545 §3.3.11 escaping for
; , \ \\n; empty input still produces a valid VCALENDAR.
i18n: 1 new key DE+EN.
Design ref: docs/design-project-chart-2026-05-09.md §7.8.
Five client-side export paths per design §7 (faraday-Q4: rule out
chromedp, browser-print is good enough).
- SVG: XMLSerializer over a clone of the live SVGSVGElement, with
--chart-* tokens inlined so the standalone file paints the same way
when opened in an image viewer (no document.css context).
- PNG: SVG → Image → Canvas at 2× DPR, toBlob("image/png"). White
background painted first so transparent SVG stays printable.
- PDF: window.print() → @media print stylesheet hides chrome, forces
the print palette tokens, locks A4 landscape via @page. User picks
"Save as PDF" in the browser print dialog. No chromedp dep.
- CSV: 20-column flat schema mirroring TimelineEvent, UTF-8 BOM for
Excel-DE, RFC 4180 escaping.
- JSON: events + lanes envelope + export-metadata header (project_id,
project_title, exported_at).
Export menu uses native <details>/<summary> so it's keyboard-accessible
without JS. The chart handle exposes getSVGElement() + getData() so
chart-export.ts stays pure: it never reads DOM state outside the SVG
it's handed.
Filenames are sanitised + dated: paliad-{title}-{yyyy-mm-dd}.{ext}.
i18n: 7 new keys DE+EN under projects.chart.export.*.
Design ref: docs/design-project-chart-2026-05-09.md §7.
Density flips lane height (24/40/64) and mark radius (5/7/10) via the
existing LANE_HEIGHT / MARK_RADIUS tables in shape-timeline-chart.ts.
Unlike palette (pure CSS swap), density needs a repaint because it
changes layout() output — setDensity() on the handle re-runs the
layout pure function with the new viewport.density.
URL state in ?density=<compact|standard|spacious>, default omitted.
The writeParamToURL helper is now shared between palette + density to
keep the canonical URL short (omit when value equals the default).
i18n: 4 new keys DE+EN under projects.chart.density.*.
Design ref: docs/design-project-chart-2026-05-09.md §6.1.
Slice 2 ships all 5 palettes from design §5.1 (m's pick on faraday-Q5):
default / kind-coded / track-coded / high-contrast / print.
Each palette is a pure data-attribute swap of the --chart-* tokens on
.smart-timeline-chart[data-palette="..."]. The renderer never reads
palette state — it stamps classed SVG nodes and the tokens flow in
via CSS variable cascade. setPalette() on the chart handle is a
one-line attribute write; no repaint.
URL state lives in ?palette=<name>; default omits the param so the
canonical URL stays clean. Initial paint reads the URL, every change
writes via history.replaceState — bookmarkable per design §8.2.
Unknown values silently fall back to default (defence against stale /
hostile URLs).
i18n: 6 new keys DE+EN under projects.chart.palette.*.
Design ref: docs/design-project-chart-2026-05-09.md §5 + §8.2.
Slice 1 served dist/projects-chart.html unconditionally, leaking a 200
for any well-formed UUID guesser. Slice 2 resolves the project via
ProjectService.GetByID before serving — ErrNotVisible (and any other
visibility error) collapses to 404 + the standard notfound chrome,
matching the JSON-API contract that already lives in writeServiceError.
A genuine DB error logs through writeServiceError's existing path but
still renders 404 chrome to the user (httpDevNullJSON wrapper discards
the JSON body writeServiceError would otherwise emit, keeping the log
side-effect intact).
Test pins serveChartNotFound: 404 + non-empty body, degrading
gracefully when dist/notfound.html is absent (test env).
Closes Slice 1 edge case #2 flagged at m/paliad#35 issuecomment-7710.
Design ref: docs/design-project-chart-2026-05-09.md §8.2.
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.
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.
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.
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.
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.
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.
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).
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.
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
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
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.
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.
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.
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.
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.
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).
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.
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.
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: 49c260b49c260bafd3aab4a5d56d7057fe5 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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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).