Commit Graph

301 Commits

Author SHA1 Message Date
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
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
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
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
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
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
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
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
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
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
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
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
m
1e97eccaed feat(deadlines/new): auto-link Typ to Regel's concept
When the user picks a Regel on /projects/{id}/deadlines/new (or the
global /deadlines/new), auto-populate the Typ chip with the rule's
concept's canonical event_type — using the
concept_default_event_type_id field server-side hydrated by mig 072.

Soft hint "Typ vorgegeben durch Regel — entfernen, um zu überschreiben"
when the chip exactly matches the rule's suggestion. Soft warning
"Hinweis: Typ widerspricht Regel" when the user has picked an event_type
that contradicts the rule's concept.

The picker is replaced silently when it still reflects the previous
rule's auto-fill (or is empty); leaves a manually-edited picker alone.
DE+EN i18n via deadlines.field.rule.{autofill,mismatch}. Reuses the
existing .form-hint--warning yellow-tint style; no new CSS.

Closes m/paliad#18 Item A — rule-vs-event redundancy on the manual
deadline create form.
2026-05-08 21:59:22 +02:00
m
3a41acee07 feat(fristenrechner): predefine Determinator perspective from our_side (t-paliad-164 slice 3)
Closes m's 2026-05-08 21:42 dogfood loop: when the user picks an Akte
that knows its own side, the Determinator perspective chip should be
locked to that side instead of asking the user to re-pick something
the project already knows.

ProjectOption gains our_side; the JSON already carries it from
slice 1 (ProjectService.projectColumns). New helper
applyOurSidePredefine maps project.our_side onto the chip:

  claimant  → "claimant"   chip active
  defendant → "defendant"  chip active
  court     → null          chip cleared (court actions are neutral
                            to the user's side, so no narrowing)
  both      → null          explicit "Beide" intent
  null/undef → no-op

URL wins: if ?role= is present at call time the user (or a shared
link) chose it explicitly and we don't overwrite. When we do predefine,
we write the same value to the URL so refresh + back/forward round-trip
correctly. Two call sites:

- selectProject: in-page Akte pick. push history (replaceURL=false) so
  back-button restores the prior state.
- post-fetchProjects hydration: the deep-link / refresh path. Use
  history replace so the URL stays clean.

A small "vorgegeben durch Akte" / "predefined from project" hint
renders next to the chip strip (italic muted). Visible whenever the
active perspective came from the project; cleared on any chip click
(explicit override) and on Step-1 reselect (no Akte = no hint).
popstate restores hint visibility by recomputing from
project.our_side ↔ currentPerspective so back/forward feels right.

Free-pick is preserved: clicking another chip overrides the
predefine and the cascade re-narrows immediately.
2026-05-08 21:58:44 +02:00
m
de4e133f03 feat(filter-bar): scaffolding — t-paliad-163 Slice 2
The universal FilterBar primitive: one client component every list-
shaped paliad surface mounts. Owns URL state (within an optional
namespace), localStorage prefs (density / shape / sort), the per-axis
chrome, and the round-trip to /api/views/{slug}/run with a transient
filter override.

Files:
- client/filter-bar/types.ts       — AxisKey, BarState, MountOpts, BarHandle
- client/filter-bar/url-codec.ts   — parseBar/encodeBar with namespace prefix
- client/filter-bar/url-codec.test.ts — 12 round-trip cases (bun test pass)
- client/filter-bar/axes.ts        — per-axis renderers (10 axes shipped;
  deadline_event_type + project_event_kind stubs land with their surfaces)
- client/filter-bar/save-modal.ts  — Speichern-als-Sicht <dialog>
- client/filter-bar/index.ts       — mountFilterBar + computeEffective overlay

Plus i18n (DE+EN, ~50 keys under views.bar.*) and CSS (.filter-bar*
scoped, reuses .agenda-chip / .filter-group / .entity-select for
parity).

No surface uses the bar yet — Slice 3 wires /inbox.
2026-05-08 21:55:29 +02:00
m
5d9c62d858 feat(projects-form): "Wir vertreten" select for our_side (t-paliad-164 slice 2)
ProjectFormFields gains a fifth select between case-specific block and
the description textarea: "Wir vertreten" with options claimant /
defendant / court / both / "" (the unset sentinel labelled
"Unbekannt / nicht gesetzt"). Type-agnostic — every project type
carries it because the Determinator picks it up regardless. Form-hint
explains it predefines the Determinator perspective and stays
overridable.

client/project-form.ts: readPayload writes our_side as a normal
stringField (empty string in edit mode clears the column via the
nullableOurSide helper on the service); prefillForm hydrates the
select from p.our_side. Both gate on tryGet so /projects/new (which
shares the form) still loads if the field is later removed.

i18n already in slice 1; this commit only wires the markup +
client logic.
2026-05-08 21:55:00 +02:00
m
188d8ec9ba feat(projects): add projects.our_side column + service plumbing (t-paliad-164 slice 1)
m's 2026-05-08 21:42 dogfood feedback on the Determinator perspective
chip: when an Akte is selected, the chip should be locked to the firm's
known side instead of asking the user to re-pick. paliad didn't track
that anywhere — paliad.parties.role records each party's role but no
flag for "this is the side we represent".

Migration 072 adds paliad.projects.our_side text with a CHECK
constraint (claimant | defendant | court | both | NULL). NULL stays the
default so existing rows are neutral and the Determinator falls back to
free-pick. Idempotent (ADD COLUMN IF NOT EXISTS + DO-block guarded
constraint) so a re-run against a partially-applied state is safe —
paliad has been bitten by collision twice this week.

Project model + ProjectService:
- OurSide *string field on models.Project
- CreateProjectInput / UpdateProjectInput accept our_side
- INSERT and partial UPDATE thread the value through; validateOurSide
  rejects unknown enum values with ErrInvalidInput before the DB
  constraint would; nullableOurSide turns "" into NULL so the form's
  "unset" sentinel can clear the column
- Update logs an our_side_changed audit event with "<from> → <to>"
  description (matching status_changed / project_type_changed
  shape); both ends use the literal "none" sentinel for NULL so the
  frontend renderer can map it to projects.field.our_side.none

i18n: event.title.our_side_changed (DE/EN), dashboard.action.short
verb form, projects.field.our_side.{label,hint,unset,claimant,
defendant,court,both,none} for the upcoming Slice 2 select.

Frontend translateEventDescription gets an our_side_changed branch
that runs translateArrowSlugs over the projects.field.our_side.*
prefix so the Verlauf tab renders localized labels.

Slice 2 wires the form, Slice 3 wires the Determinator.
2026-05-08 21:52:50 +02:00
m
d5a01e6682 feat(render-spec): add list.row_action — t-paliad-163 Slice 1
Schema bump that lets the universal <FilterBar> tell shape-list which
row interaction to wire (navigate / complete_toggle / approve / none).
Defaults to navigate when empty so existing SystemView definitions and
saved user views continue to render rows that route to the per-kind
detail page.

Validator extended; pure-Go test cases over every enum value + reject.
TS mirror updated in client/views/types.ts. No DB migration — the
field is purely additive on the JSON shape.
2026-05-08 21:49:00 +02:00
m
1782dfa910 feat(paliadin/cross-surface-sync): t-paliad-161 Slice F — DB-driven history hydrate
Two Paliadin chat surfaces shared a user but not their conversation:
the inline drawer (paliadin-widget.ts) maintained `paliadin:widget:session`
+ `paliadin:widget:history:` while the standalone /paliadin page used
`paliadin:session` + `paliadin:history:`. A turn typed in the drawer
never surfaced on /paliadin and vice versa, and a localStorage wipe
tossed everything.

Fix in three coordinated parts:

1. **Shared session id.** The widget now uses the same `paliadin:session`
   key the standalone page already uses. One-time migration in
   bootSession copies any legacy `paliadin:widget:session` across so
   existing users keep their conversation thread, then deletes the legacy
   key. The widget's HISTORY_PREFIX also drops the `widget:` namespace
   so both surfaces' render-caches address the same bucket.

2. **DB-driven history.** New endpoint:

       GET /api/paliadin/history?session=<id>&limit=<N>

   Returns the caller's turns for the session, oldest → newest,
   gated by PaliadinOwnerEmail (same gate as POST /api/paliadin/turn).
   Backed by paliadinDB.ListHistoryForSession, which mirrors the
   existing visibility predicate (own rows always; all rows for
   global_admin). Default limit 50, capped at 200.

3. **Hydrate-on-mount, hydrate-on-open.**
   - paliadin.ts (standalone page): DOMContentLoaded calls
     hydrateFromServer() right after renderHistory() seeds from
     localStorage. DB rows replace the cache when present.
   - paliadin-widget.ts (inline drawer): revealIfOwner kicks
     hydrateFromServer in the background after rehydrateHistory paints
     the cache. openDrawer() also calls hydrateFromServer so a turn the
     user typed on /paliadin since the last drawer-open shows up
     without a manual reload.

   Reconciliation: DB > localStorage when DB has rows. DB call fails or
   returns empty → keep showing whatever's in cache (offline cushion).
   This kills the trap klaus warned about (paliad#19): every render
   reconciles against the server, no first-paint short-circuits.

Schema: zero migrations. paliad.paliadin_turns already carries
session_id + user_message + response + ts since the t-paliad-146 PoC;
this slice just adds a typed read path.

Backwards compatible: the standalone /paliadin page's session key is
unchanged; only the widget migrates onto it.

Builds + tests green; i18n unchanged.

Refs: m/paliad#19 (localStorage short-circuit), m/paliad#20 (inline modal),
      docs/design-paliadin-inline-2026-05-08.md §3.4.
2026-05-08 21:43:51 +02:00
m
936aca5925 refactor(projects-detail/projektbaum): reuse the /projects tree component
m's 2026-05-08 21:28: "The Projektbaum inside a Project in the tab
with the Unterordner should just be the same as the Tree in Projects.
It has symbols, everything. That should be a shared component."

Drop the inline mini-tree renderer (renderTreeNode / loadProjectTree /
~50 lines of duplicate logic) in client/projects-detail.ts and mount
the existing client/project-tree.ts module into the tab's container.
The shared component carries:
  - per-type icons (Mandant / Litigation / Patent / Case)
  - pin star (touch-friendly)
  - overdue / open-deadline badges with subtree counts
  - status chip + type chip
  - expand / collapse toggles
  - inherited-visibility marking
  - search highlighting (no-op when no search params are passed)

Current project highlight: set aria-current="true" on the matching
.projekt-tree-node after mount. The shared CSS already styles
.projekt-tree-node[aria-current="true"] > .projekt-tree-row with the
lime accent (global.css :5853).

Removed the now-dead mini-tree CSS block that was also accidentally
overriding .projekt-tree-title from the real tree (later-defined rule
won the cascade and erased the shared title weight).

loadChildren() still fetches /api/projects/<id>/children for the
empty-state gate ("Keine untergeordneten Projekte" when this node has
no direct children) and the create-link parent_id pre-fill — both
predicates depend on direct children, not the visible tree.
2026-05-08 21:31:16 +02:00
m
f31307afcb feat(sidebar): newspaper icon for "Neuigkeiten", reserve sparkle for Paliadin
m's 2026-05-08 21:11: the changelog entry was sharing the sparkle 
glyph with the new Paliadin AI surface (inline widget trigger, agent-
suggested provenance pill, /paliadin entry). Now that  carries an
explicit AI semantic in paliad's visual language, swapping the
changelog to a newspaper SVG keeps the two affordances orthogonal.
2026-05-08 21:11:57 +02:00
m
d2790a0461 feat(paliadin): reconcile late responses via janitor + chat polling
When Claude writes the response file after the 60 s pollForResponse
window expires (e.g. the tmux pane was busy mid-turn when the message
arrived), the SSE stream has already closed with an error and the
file sits unread on disk forever. The chat shows a permanent timeout
even though the answer exists.

Backend:
- LocalPaliadinService.StartJanitor: scans responseDir every 2 s and
  patches rows whose response is still NULL when the file lands.
  completeTurnLate stamps error_code='late' so the FE can render a
  marker. Guarded with WHERE response IS NULL to never overwrite a
  real response if RunTurn races.
- Paliadin.GetTurn(callerID, turnID) on the shared paliadinDB. Same
  visibility predicate as ListRecentTurns.
- GET /api/paliadin/turns/{id} — owner-gated; lets the chat UI
  discover late-arrived responses without a refresh.

Frontend:
- paliadin-late-poll.ts: shared 3 s / 10 min poller.
- paliadin.ts + paliadin-widget.ts: on SSE error, show
  "wartet auf späte Antwort", kick off the poller, swap bubble in
  place when response arrives + retroactively persist to history.
- i18n: paliadin.late.waiting + paliadin.late.marker (DE/EN).
- CSS: --late-pending opacity tweak, --late neutral background,
  italic-grey "verspätet" tag.
2026-05-08 20:56:53 +02:00
m
5b08bfcb96 fix(views/sidebar): pad fieldset sections + consolidate Ansichten / Meine Sichten
m's dogfood findings 2026-05-08 20:32:

1. /views/new: form sections (.form-section fieldsets) had no
   container CSS, so content rendered against the bare browser-default
   fieldset border with effectively zero padding. Adds proper padding
   (1.25rem 1.5rem 1.5rem), 8px radius, surface background, and tidies
   the legend / hint typography. Reused by every entity-form fieldset
   that adopts .form-section.

2. Sidebar nav: collapses the prior split between the "Ansichten"
   group (Fristen + Termine) and the "Meine Sichten" group (user-
   defined views + "+ Neue Sicht") into a single "Ansichten" group.
   Same DOM hook (#sidebar-views-items, .sidebar-views-new) so
   client/sidebar.ts's user-view hydration keeps working unchanged —
   the entries just sit alongside the built-ins now instead of in
   their own labelled section.
2026-05-08 20:35:14 +02:00
m
fc048c578e fix(paliadin-widget): render markdown + chips in inline bubbles, fix lime-trigger contrast
m's dogfood findings on the inline drawer:

1. Assistant responses (markdown headings, bold, lists, [chip:nav:…]
   tokens, [#deadline-OPEN:<id>] tokens) showed as raw text. The widget
   was setting body.textContent and skipping the renderer. Extracted
   the standalone /paliadin page's pipeline into client/paliadin-render.ts
   (renderResponseHTML + chip helpers + block markdown parser) so both
   surfaces share one source of truth. The widget now feeds assistant
   bubbles through innerHTML; user bubbles still go through textContent
   (no point parsing the user's typed markup).

2. Floating trigger button rendered the sparkle glyph in white-on-lime
   in dark mode — color: var(--color-text) inherits the dark-mode light
   foreground and washes out completely on a lime background. Lime is
   inherently a light-background colour, so the trigger pins its
   foreground to --hlc-midnight in both themes.

Bubble CSS additions: assistant bubbles get white-space: normal (the
base pre-wrap rule was forcing every source newline to a literal break
and breaking <p>/<h2>/<ul> spacing) plus tight h2/h3/p/ul margins so
the rendered markdown reads as a chat bubble, not a doc page.
2026-05-08 20:31:44 +02:00
m
d0e8c995fe Merge: t-paliad-157 Determinator Slices 3b + 3c — proceeding-type + perspective narrowing
2f27620 — Slice 3b: B1 cascade narrows by the project's proceeding
type. Three-input priority chain (inbox chip > ad-hoc context >
project's proceeding_type). cachedProceedingTypes lookup via
/api/proceeding-types-db; forumFromProceedingCode maps UPC_/DE_/EPA_/
EP_/DPMA_ → upc/de/epa/dpma. /tools/fristenrechner?project=<uuid>&path=b
auto-narrows the cascade without needing chip clicks.

6fcf34a — Slice 3c: perspective chip (Klägerseite/Beklagtenseite/Beide)
at top of the B1 panel. Mig 071 adds paliad.event_categories.party
text[] (claimant|defendant|both|court) with conservative backfill —
claimant on klage.* + replik-*, defendant on widerklage.* + duplik-*.
Cross-appeal/Anschlussberufung leaves stay NULL pending dogfood
(role flips depending on who appealed first). Cascade hides leaves
whose party tag contradicts the chip; both/court tags always pass;
NULL stays neutral. URL-only state (?role=claimant|defendant).

Live tracker is already at v71 (feynman applied 071 during dev).
Deploy will see tracker=71 with file 071 present — no work to apply.

The full Determinator scaffold is now in place:
  Step 1 (Akte/ad-hoc) → Step 2 (do/happened) → Step 3a (File/Draft/Enter)
  for outgoing or Pathway B with auto-narrowing for incoming.

Open follow-ups (small, can wait for dogfood):
- Tag cms-eingang.gegenseite.upc-rev/upc-app/de-bgh-* leaves with party
  (currently neutral; appellate leaves should arguably be tagged based
  on who appealed)
- Persistence for perspective if dogfood says it's wanted
- /drafts route + Step 3a Draft card wiring (proper drafting surface
  is a separate workstream)
2026-05-08 20:22:50 +02:00
m
dd0cee226d Merge: t-paliad-162 — sidebar reorg, inline Agenda on dashboard, collapsible sections
e824898 — feat(navbar/dashboard): per m's 2026-05-08 20:05 design
decisions:

- Sidebar restructured into named groups: Home → Paliadin (gated to
  PaliadinOwnerEmail) → Overview (Projekte) → Views (Fristen,
  Termine) → Tools (Fristenrechner et al). Group headers render as
  small uppercase muted labels.
- Agenda removed from sidebar + BottomNav. Direct link /agenda still
  routes; the dashboard now renders Agenda inline as a section.
- "Letzte Aktivität" relocated to sit under Agenda on the dashboard.
- All dashboard sections become collapsible with a chevron toggle;
  open/collapsed state persists per-section in localStorage under
  paliad:dashboard:collapse:<section>.
- Agenda rendering primitives extracted into client/agenda-render.ts
  so the standalone /agenda page and the dashboard's inline Agenda
  share identical rendering with no fork.

Pure frontend change — no Go work, no migrations.
2026-05-08 20:21:49 +02:00
m
6fcf34a3e3 feat(determinator/slice-3c): perspective chip + party-tagged cascade narrowing
m's 2026-05-08 18:09 spec — Slice 3c. Adds a Klägerseite / Beklagtenseite
chip strip at the top of the B1 cascade panel; cascade leaves tagged
with a contradictory party get hidden. Klägerseite never files
Klageerwiderung; Beklagtenseite never files Klageschrift.

Migration 071 adds `paliad.event_categories.party text[]` (CHECK on
{claimant, defendant, both, court}) plus a partial GIN index. Backfill
is conservative — only the obvious leaves get tagged on this pass:

  - claimant   ich-moechte-einreichen.klage.* (9 leaves)
                ich-moechte-einreichen.spaetere-schriftsaetze.replik-*
  - defendant  ich-moechte-einreichen.widerklage.*
                ich-moechte-einreichen.spaetere-schriftsaetze.duplik-*

cms-eingang.* (incoming) and frist-verpasst.* (anyone misses a
deadline) stay NULL because the user can be on either side and still
receive the same court communication. Cross-appeal / Anschluss-
berufung / Reply-to-cross-appeal also stay NULL — the role flips
depending on who appealed first; the cascade doesn't have that
context yet. Tag in a follow-up once dogfood validates the chip.

Backend: EventCategoryNode JSON gains optional `party` array;
EventCategoryService.Tree SELECT picks it up via pq.StringArray.

Frontend: new Perspective type + URL state (?role=claimant|defendant)
+ perspective chip strip styled identically to the inbox-channel chip
strip. perspectiveAllowsParty(party) gates each cascade child;
"both"/"court" tagged nodes always pass; neutral nodes always pass.
Persistence is URL-only — dogfood will tell us whether to add a saved
default later.

Migration applied to live Supabase; tracker at v71.

Refs t-paliad-157 / m/paliad#15.
2026-05-08 20:21:13 +02:00
m
e824898a6d feat(navbar/dashboard): t-paliad-162 reorg sidebar groups + inline Agenda + collapsible sections
Sidebar:
- Paliadin lifted out of Übersicht to a top-level entry directly under
  Home (owner-only reveal logic unchanged — same id reused).
- Agenda removed from sidebar; the standalone /agenda route stays for
  direct-link compatibility but the dashboard hosts its content inline.
- Projekte moved into Übersicht; Fristen + Termine moved into a new
  Ansichten group; the Arbeit group is gone.
- Werkzeuge / Wissen / Ressourcen collapsed into one Werkzeuge group
  per m's brief order (calculators → reference → content).
- BottomNav agenda slot repointed to /events?type=deadline so the
  overdue+today badge still has a sensible target on mobile.

Dashboard:
- Agenda renders inline as a new collapsible section between the
  upcoming-rails grid and Letzte Aktivität, with a "Vollständige Agenda
  öffnen →" link to the standalone page.
- Letzte Aktivität moved under Agenda per m's design call.
- Sections (summary, deadlines, appointments, agenda, activity) become
  collapsible via a chevron toggle; state persists in
  localStorage[paliad:dashboard:collapse:<section>]. Matters card stays
  whole-card-tappable, so it's intentionally left non-collapsible.
- Inline agenda fetches /api/agenda directly with a 30-day window and
  refreshes on the existing 60s dashboard poll.

Render primitives:
- New client/agenda-render.ts hosts renderAgendaTimeline + AgendaItem
  type, shared by client/agenda.ts and client/dashboard.ts. Standalone
  agenda.ts shrinks accordingly; behaviour is identical.

i18n:
- Added nav.group.ansichten + dashboard.agenda.* + dashboard.section.*
  keys (DE/EN). Removed nav.group.{arbeit,wissen,ressourcen} (no other
  callers; i18n-keys.ts auto-regenerated).
2026-05-08 20:20:57 +02:00
m
2f27620a5b feat(determinator/slice-3b): scope B1 cascade by project's proceeding type
m's 2026-05-08 18:09 spec: "if we have the project type defined, we
should only have events available that match the type of project /
type of case." Slice 3b wires the project's proceeding_type into the
cascade narrowing alongside the inbox chip and ad-hoc context.

Three inputs feed the cascade now, in priority order:

  1. Inbox chip            (cms / bea / posteingang) — user override.
  2. Ad-hoc Step 1 chip    (upc / de / epa / dpma).
  3. Project's proceeding  (Step 1 picked Akte → proceeding_type_id →
     proceeding_types.code → forum prefix).

activeForumOnPage() returns the first non-null value. The B1
cascade's inboxFilterAllowsForums consults this so a user landing on
/tools/fristenrechner?project=<uuid>&path=b&mode=tree gets the
narrowed cascade automatically — no chip clicks required. The chip
can still override at the top of the panel.

Pieces:

  - ProjectOption gains optional proceeding_type_id (already on the
    JSON; just declared so TypeScript can read it).
  - cachedProceedingTypes Map<int, string> is populated once on init
    via /api/proceeding-types-db and cached for the page lifetime.
  - forumFromProceedingCode() maps "UPC_INF" / "DE_NULL" / "EPA_OPP"
    / "EP_GRANT" / "DPMA_OPP" → upc / de / epa / dpma. EP_ and EPA_
    both hit the EPA branch since EP_GRANT belongs to the EPA forum.
  - triggerCascadeRefresh() is called from selectProject /
    selectAdhoc / clearStep1Context + after the async load completes
    so the cascade re-renders when the context changes.

The role variants (Klägerseite vs Beklagtenseite, Berufungskläger vs
-beklagte) are Slice 3c — they require fetching the user's
project_teams.responsibility for the selected project. Project's
forum lands first; role layers on after.

Refs t-paliad-157 / m/paliad#15. Folds in part of #18 (Item A
rule-vs-event collapse) — when the project context narrows the cascade
to one jurisdiction, the rule-vs-event mismatch surface shrinks.
2026-05-08 20:15:50 +02:00
m
75dc842b8e feat(team-broadcast): add "open in mail client" mailto link to broadcast modal
m's request 2026-05-08 20:12: alongside Paliad's per-recipient
"E-Mail an Auswahl" broadcast (which sends individual envelopes from
the server), users want a one-click way to compose a single multi-
recipient email in their own mail client. Common use case: writing
to a specific team where the response thread should stay client-side
and be visible to every recipient (unlike the privacy-preserving
broadcast where each recipient sees only themselves).

Adds a "Im Mail-Client öffnen" / "Open in mail client" link to the
broadcast modal's recipient summary, alongside the existing
"Alle anzeigen" toggle. Clicking it opens a `mailto:` URL with every
selected recipient comma-separated in the To: line per RFC 6068.

`buildMailtoHref` is exported so it can be unit-tested independently
and reused by other selection surfaces (admin team table, project
team tab) without a refactor.

The existing server-driven broadcast path is unchanged — both options
coexist.
2026-05-08 20:13:49 +02:00
m
6224898f9e Merge: t-paliad-161 — inline Paliadin chat modal + agent-suggested write path
Six commits from mai/dirac/inventor-inline-paliadin (all sliced per
the design's §10 phasing):

  142edca docs(paliadin): t-paliad-161 inventor design
  282e0bb feat(paliadin/migration-070): Slice A — schema + relay seam
  0d1a7ba feat(paliadin/context): Slice B — structured page-context payload
  ba2408e feat(paliadin/inline-widget): Slice C — floating button + drawer
  a3052eb feat(paliadin/suggest): Slice D — agent-suggested write path
  4ecea7a feat(paliadin/agent-glyph): Slice E —  alongside 👀

What ships:

- Floating Paliadin trigger bottom-right + Cmd/Ctrl-K shortcut, opening
  a 420px right slide-out drawer (full-screen on mobile). Visible on
  every authenticated page except /paliadin, /login, /onboarding.
  Same PaliadinOwnerEmail gate as today — no scope expansion.
- Per-route starter-prompt registry in client/paliadin-starters.ts —
  context-aware empty-state nudges users into useful first prompts.
- Structured PaliadinContext payload (route_name + primary_entity_type
  + primary_entity_id + user_selection_text + view hints) flowing from
  the widget through Go into the tmux envelope. SKILL.md gains [ctx …]
  parsing so the persona can use it.
- Agent-suggested write path: paliad__suggest_deadline +
  paliad__suggest_appointment + paliad__suggest_note tools that draft
  rows straight into the existing approval pipeline. Suggestions land
  as approval_requests with requester_kind='agent' and an
  agent_turn_id pointer back to the originating turn.
- Visual provenance:  glyph alongside 👀 on pending-approval rows
  whose request was agent-drafted; persistent  on approved-from-agent
  rows in the audit log. Lives in events.ts/agenda.ts/inbox.ts.

Migration 070 is idempotent (every ALTER guarded by IF NOT EXISTS,
constraints/index inside DO blocks). Live tracker is at v69; deploy
will apply 070 cleanly. Adds:
  paliad.approval_requests.requester_kind text + xor-check
  paliad.approval_requests.agent_turn_id uuid
  paliad.paliadin_turns.context jsonb

m greenlit all 5 inventor decisions (a-a-a-a-a) on 2026-05-08 19:39:
owner-only gate, tmux relay v1, create-only suggestion verbs,
-alongside-👀 visual, selection-text default-on.

Refs m/paliad#20, design doc docs/design-paliadin-inline-2026-05-08.md.
2026-05-08 20:06:07 +02:00
m
4ecea7a4bb feat(paliadin/agent-glyph): t-paliad-161 Slice E — alongside 👀
When a pending row was drafted by Paliadin (requester_kind='agent' on
its in-flight approval_request), surface a sparkle  next to the
existing eye-pill 👀. The two glyphs are orthogonal: 👀 = "needs
approval",  = "Paliadin drafted this". Either can change without the
other, so the visual taxonomy stays decomposable for any future
autopilot mode where 👀 disappears but  stays.

Read-path:

- DeadlineService.ListVisibleForUser + AppointmentService.ListVisibleForUser
  LEFT JOIN paliad.approval_requests on pending_request_id and project
  ar.requester_kind into the row. NULL when no request is pending.
- models.DeadlineWithProject + AppointmentWithProject grow
  RequesterKind *string. The list-projection helpers
  (projectDeadline / projectAppointment in event_service.go) carry it
  into EventListItem.
- /api/events response now includes requester_kind on every pending
  row; /api/inbox already does (Slice D extended approvalRequestViewColumns).

Render-path:

- frontend/src/client/events.ts — new AGENT_PILL_GLYPH constant (""),
  agentPill rendered into the title cell next to the existing
  pendingPill when item.approval_status='pending' AND
  item.requester_kind='agent'. EventListItem TS shape gains
  `requester_kind?: "user" | "agent"`.
- frontend/src/client/agenda.ts — same pattern, agendaItem TS shape
  + agentPill rendered next to pendingPill in the headline span.
- frontend/src/client/inbox.ts — ApprovalRequestView gains
  requester_kind + agent_turn_id; the meta line replaces the
  requester's plain name with "Anna  Paliadin" when the request was
  drafted by the agent.

CSS: new .approval-pill--agent modifier in global.css using only
existing tokens (--color-bg-lime-tint / --color-surface-2 /
--color-text), mirroring the .approval-pill--icon shape so the two
glyphs sit side-by-side at the same baseline.

i18n: 3 new keys × 2 langs (approvals.agent.label /
approvals.agent.byline / approvals.agent.suggestion_pending) — total
1966 → 1969.

Build clean (frontend + go), tests green.

Refs: docs/design-paliadin-inline-2026-05-08.md §8.
2026-05-08 20:04:10 +02:00
m
34e82ead06 feat(determinator/slice-3a): outgoing-intent chooser (File / Draft / Enter)
m's 2026-05-08 18:09 spec: Step 3a is itself a 3-option fan-out. When
the user picks "Etwas einreichen" on Step 2 we no longer drop straight
into the Pathway A wizard; we ask "what kind of einreichen?" first.

Three cards:

  - **File** (Schriftsatz einreichen) → navigates to Pathway A — the
    existing wizard with proceeding picker, trigger date, flags,
    timeline, save modal. The rule-library entry point.
  - **Draft** (Schriftsatz entwerfen) → v1 placeholder. Disabled
    button with a "kommt bald" pill in the corner. m specced this
    as a link to a future drafting surface; for now we show the
    intent without doing anything so the surface exists in the IA.
  - **Enter** (Frist manuell erfassen) → routes to
    `/projects/{id}/deadlines/new` (or `/deadlines/new` in ad-hoc
    mode where there's no project to anchor against).

Pathway type extends to include "outgoing"; readPathwayFromURL +
showPathway both handle it. The Step 3a panel reuses .fristen-step2-
card visuals so File / Draft / Enter look consistent with the parent
Step 2 cards but distinct from Pathway A's proceeding picker.

Back-button policy:

  - Step 3a back → Step 2 (the new "fork" state).
  - Pathway A back → Step 3a (since that's where the user came from
    in the new flow). Two clicks back to the fork.
  - Pathway B back → fork directly (Step 2 happened-card jumps
    straight to Pathway B; no intermediate chooser).

Out of scope for this slice:

  - Step 3b's project-type-scoped event picker (Slice 3b).
  - Klägerseite/Beklagtenseite role variants (Slice 3c).
  - Real /drafts route — Draft stays a soft placeholder.

Refs t-paliad-157 / m/paliad#15.
2026-05-08 19:58:21 +02:00
m
ba2408eb51 feat(paliadin/inline-widget): t-paliad-161 Slice C — floating button + slide-out drawer
The inline Paliadin chat surface — reachable from every authenticated
page, replacing the standalone /paliadin route as the primary entry
point. The standalone page survives as the dedicated full-screen mode
(the drawer's "↗ fullscreen" action links to it).

Components:

- frontend/src/components/PaliadinWidget.tsx — emits the floating
  trigger button (bottom-right, lime , owner-revealed by JS), a
  scrim, and the right-edge slide-out drawer with header (reset /
  fullscreen / close), context chip, message stream, empty-state
  starter list, and textarea+send form. Loads /assets/paliadin-widget.js.

- frontend/src/client/paliadin-widget.ts — runtime. /api/me probe
  reveals the trigger when caller matches PaliadinOwnerEmail (with
  optional is_paliadin_owner flag fast-path); Cmd+J / Ctrl+J shortcut
  toggles open/close (Cmd+K stays reserved for global search per
  client/search.ts). Uses computePaliadinContext() (Slice B) per send
  so route + entity + selection flow into every turn. SSE consumer
  writes assistant bubbles; localStorage persists per-session history.

- frontend/src/client/paliadin-starters.ts — per-route starter prompt
  registry. 14 routes covered (dashboard, projects.*, deadlines.*,
  appointments.*, agenda, events, inbox, tools.*, glossary, courts) +
  a _default fallback. Bilingual (DE/EN); prompts ending in `: ` seed
  the textarea for the user to finish; fully-formed prompts auto-send.

- 39 authenticated TSX pages get a `<PaliadinWidget />` element after
  `<Footer />` via a mechanical pass. paliadin.tsx (the standalone)
  is intentionally excluded — its dedicated UI is the widget's
  fullscreen escape hatch, not a place to overlay another widget.

- frontend/build.ts registers the new bundle.
- frontend/src/styles/global.css gains ~280 lines of widget CSS
  (trigger / scrim / drawer / header / context-chip / messages /
   bubbles / starters / form / send-btn) using only existing tokens.
   Mobile (≤640px): drawer goes full-screen; trigger lifts above
   bottom-nav slots.
- 11 new i18n keys × 2 langs = 22 entries under paliadin.widget.*.

Visibility predicate (paliadin-context.shouldSendContext) hides the
widget on /paliadin, /login, /onboarding. Owner-only gate stays on
PaliadinOwnerEmail.

Build clean: i18n 1955 → 1966 keys, IIFE-wrapped 218KB bundle, go test
green.

Refs: docs/design-paliadin-inline-2026-05-08.md §3, §5.
2026-05-08 19:54:18 +02:00
m
dba8ad3fdd feat(determinator/slice-2): /projects/new return-bounce + Step 1 preselect
m's 2026-05-08 Slice 2: "Neue Akte anlegen" on the Fristenrechner now
round-trips cleanly. The Step 1 link sends `?return=/tools/fristenrechner`
on the way out; projects-new.ts honours the param after a successful
POST and redirects back with `?project=<new_uuid>` appended so the
just-created Akte preselects itself in Step 1.

Two pieces:

  - frontend/src/client/projects-new.ts — new sanitizeReturnUrl()
    rejects anything that could escape to a different origin
    (protocol-relative `//foo`, absolute `https://...`, non-rooted
    relative paths). On submit success, if a sanitized return URL
    exists, build the destination via URL() so existing query params
    on the return path stay intact and ?project= is set without
    clobbering, then redirect there. Falls back to /projects/{id}
    when no return param is present (existing behaviour preserved).
  - frontend/src/fristenrechner.tsx — Step 1 link gets the
    ?return=/tools/fristenrechner query string so the bounce-back
    knows where to land.

Step 1 hydration from Slice 1 already handles `?project=<uuid>` —
fetchProjects() repopulates cachedAkten, the projectId looks up its
ProjectOption record, renderStep1Summary() renders the collapsed
state, Step 2 cards become visible. No client-side state coordination
needed; the URL is the contract.

Refs t-paliad-157 / m/paliad#15.
2026-05-08 19:54:11 +02:00
m
d4c129f0d6 Merge: t-paliad-157 Determinator Slice 1 — project picker + do/happened bifurcation
df04e50 — feat(fristenrechner/determinator): the legacy "Was möchten
Sie tun?" landing fork is replaced by:

  Step 1: filtered Akte picker + "Neue Akte anlegen" link (bare; the
    bounce-back to the wizard after creation is Slice 2 scope) +
    4 ad-hoc chips driving ?ad_hoc=upc|de|epa|dpma.
  Step 2: "Etwas einreichen" / "Etwas ist passiert" cards driving
    showPathway('a' | 'b'). Quick-pick chips moved here from the old
    fork. Pathway A/B back buttons return to Step 2.

Save CTA on Pathway A's wizard disables in ad-hoc mode with hint
"Ad-hoc — kein Projekt, kein Speichern" (DE+EN). The locked context
collapses to a one-line summary; Reselect re-expands.

URL contract:
  ?project=<uuid> | ?ad_hoc=upc|de|epa|dpma  — Step 1 result
  ?path=a|b                                   — Step 2 result (back-compat)
  ?mode=tree|filter                           — Pathway B sub-mode

Pathway A/B sub-routing primitives (showPathway, showBMode) unchanged
— Step 2 cards just drive the same hooks.

Still open:
  Slice 2 — /projects/new return-bounce on save.
  Slice 3+ — scoping the picker / cascade by project's proceeding-type
    + role; replacing the wizard with the Step 3a File/Draft/Enter
    chooser.
2026-05-08 19:52:32 +02:00
m
df04e500f7 feat(fristenrechner/determinator): Slice 1 — project picker + do/happened bifurcation
m's 2026-05-08 18:08 Determinator redesign Slice 1. Replaces the
legacy "Was möchten Sie tun?" fork (Pathway A vs B) with a two-step
funnel that puts the project (Akte) at the foundation:

  Step 1 — Welche Akte?
    - Filtered list of visible projects, search-as-you-type.
    - "Neue Akte anlegen" link → /projects/new (bare; the bounce-back
      with auto-preselect lands as Slice 2 per Maria's gating).
    - Four ad-hoc explore-mode chips (Custom UPC / DE / EPA / DPMA
      proceeding) for users who just want to look up a rule. No DB
      write; URL becomes ?ad_hoc=upc|de|epa|dpma.

  Step 2 — Was möchten Sie tun?
    - Two cards: "Etwas einreichen" → Pathway A (Verfahrensablauf
      wizard) and "Etwas ist passiert" → Pathway B (cascade, mode=tree).
    - Quick-pick chips moved here from the old fork's shortcut row.

Once Step 1 picks a context, the picker collapses to a one-line
summary "Akte: X · [Andere Akte]" mirroring the proceeding-summary
collapse pattern (097e21c). Reselect re-expands and clears downstream
state.

State on URL:
  ?project=<uuid>     project context
  ?ad_hoc=upc|...     ad-hoc explore-mode
  ?path=a|b           Step 2 outcome (kept for back-compat)
  ?mode=tree|filter   Pathway B sub-mode (kept)

The legacy back-from-Pathway buttons now return to Step 2 (the new
"fork" state). showPathway() / showBMode() unchanged — Step 2 cards
just drive the same primitive.

Save-to-project CTA on Pathway A's wizard detects ad-hoc mode and
disables itself with the hint "Ad-hoc — kein Projekt, kein Speichern"
(EN: "Ad-hoc — no matter, no save"). Hiding the CTA would leave the
user wondering where the action went; disabling makes the constraint
legible (per m's lock #2).

Frontend pieces:
  - fristenrechner.tsx — Step 1 + Step 2 markup; legacy
    fristen-pathway-fork removed wholesale.
  - client/fristenrechner.ts — new Step1Context type + URL hydration
    + render helpers; initPathwayFork rewired to drive the new
    cards; renderProcedureResults gates the save CTA on
    isAdhocMode().
  - client/i18n.ts — 19 new keys (DE+EN) under deadlines.step1.* +
    deadlines.step2.* + the save CTA hint.
  - styles/global.css — .fristen-step1 / .fristen-step2 block + chip
    + summary styles, all bound to the existing --color-* token
    palette. Mobile breakpoint stacks the Step 2 cards at <600px.

Out of scope for this slice (will land later):
  - Slice 2: /projects/new bounce-back with auto-preselect via
    ?return=/tools/fristenrechner.
  - Slice 3+: scoping the picker / cascade by project's
    proceeding-type + role; replacing the existing wizard with the
    Step 3a "File / Draft / Enter" chooser.

Refs t-paliad-157 / m/paliad#15.
2026-05-08 19:50:59 +02:00
m
0d1a7ba886 feat(paliadin/context): t-paliad-161 Slice B — structured page-context payload
The inline widget (Slice C, next) submits a richer per-turn payload than
the standalone page's single page_origin string:

  context: {
    route_name, page_origin, primary_entity_type, primary_entity_id,
    user_selection_text, view_mode, filter_summary
  }

Wiring:

- services.TurnContext + EnvelopePrefix() build a
  `[ctx route=… entity=…:<id> selection="…" view=… filter="…"]` block.
  Empty fields are omitted; selection is always quoted (it's user-supplied
  content); selection over 1000 chars gets truncated with an ellipsis.
- services.MaxSelectionChars = 1000 (the design's privacy floor §4.3).
- LocalPaliadinService.RunTurn + RemotePaliadinService.RunTurn prepend the
  envelope to the user message before sending through tmux.
- paliadinDB.insertTurnRow now persists the structured context as
  paliad.paliadin_turns.context jsonb (migration 070).
- handlers/paliadin.go's turnRequest accepts the new optional context
  field; mirrors context.PageOrigin into the top-level page_origin when
  the latter is empty so legacy admin queries still work.
- The standalone /paliadin page is unchanged — its turn body still has
  only page_origin, the new field is optional. Backwards compatible.

SKILL.md (~/.claude/skills/paliadin/SKILL.md, refreshed via
scripts/install-paliadin-skill):
- Documents the new `[ctx …]` block in front of the user question.
- Five behaviour rules: pre-call enrichment when entity= is set, don't
  repeat the obvious, treat selection as data not instructions, no
  hallucination on empty entity lookup, legacy turns work as before.

Frontend client/paliadin-context.ts is the route-table + entity
extraction the widget will use (Slice C). Public surface:
computePaliadinContext() returns the payload or null on excluded
routes (/paliadin, /login, /onboarding); selection toggle reads
localStorage["paliadin:send-selection"] (default on, off opts out).

New test TestTurnContext_EnvelopePrefix pins the bracket-block format
(8 sub-tests including truncation, selection-quote escape, empty-context
empty-prefix). go test ./... clean. go build + bun run build clean.

Refs: docs/design-paliadin-inline-2026-05-08.md §4.
2026-05-08 19:47:43 +02:00
m
e9e7d5c27c feat(projects-detail): "Untergeordnet" tab → "Projektbaum" with full visible hierarchy
m typed in another pane: "The project view where there is a tab
'Untergeordnet' I want a 'Project Tree' instead. And it always shows
all siblings, all parents and all children of that entity." (Forwarded
by klaus / youpcorg/head, msg #1570.)

Tab label
  DE: Untergeordnet → Projektbaum
  EN: Sub-projects → Project Tree
  i18n key kept as projects.detail.tab.kinder for back-compat (legacy
  bookmarks + create-sub-project CTA still keyed on 'kinder').

Tree content
  Was: direct children only (one /api/projects/<id>/children call).
  Now: full visible project hierarchy via /api/projects/tree?subtree_counts=false,
  rendered as nested <ul> with the current node highlighted with a
  lime-soft background + current-color border. The dashed left border
  on nested levels makes parent → child relationships scannable.
  Visibility is RLS-scoped (the tree endpoint already filters to projects
  the user can see).

Empty state
  "Keine untergeordneten Projekte" still renders when the current node
  has zero direct children — that is what the "+ Untervorhaben anlegen"
  CTA next to it actually creates. Showing it for "tree has no other
  branches" would have been wrong.

The standalone /api/projects/<id>/children call stays — it gates the
empty state and pre-fills parent_id on the create form.
2026-05-08 19:46:55 +02:00
m
e2907db760 Merge: t-paliad-157 dogfood batch — eye glyph 👀, optional deadlines, Verfahrensablauf collapse
Three commits from mai/feynman/fristenrechner:

- 614f9af fix(approval-pill): two-eyes glyph 👀 instead of single SVG eye
  on /deadlines + /appointments + /agenda. m's preference: emoji denotes
  "being looked at" closer to "wartet auf Genehmigung" semantics.

- 2d6ea3e feat(deadline-rules/is-optional): conditional rules opt-in via
  save modal. Adds paliad.deadline_rules.is_optional. Distinct from
  is_mandatory: a rule can be statutorily fixed when it applies AND
  conditional on whether it applies (RoP.151 cost-decision request,
  appeal-related deadlines). Save-modal pre-unchecks optional rows;
  user toggles to opt in. Timeline shows "auf Antrag" pill.

- 097e21c feat(fristenrechner): proceeding-picker collapses to one-line
  "Verfahren: X · [Reselect]" pill after pick (saves vertical space).
  Column view becomes the default for the timeline (was previously
  whichever-default; m wants Column on first render).

Migration housekeeping:
  feynman's migration was authored as 066 on his branch but main has
  already taken 066/067 via shannon's t-paliad-160 (approval policy
  split + drop required_role). Renumbered to 068 during merge to
  resolve the same-number collision. Added ADD COLUMN IF NOT EXISTS
  to make the up-migration idempotent (defensive for environments
  where the column was already applied out-of-band during dev). The
  RoP.151 backfill UPDATE is naturally idempotent.

  Live tracker bumped from 66 → 68 to reflect schema reality before
  this merge: shannon's 066+067 effects and feynman's is_optional
  column are all already present in the live youpc Supabase. The
  next deploy will see tracker=68 and have nothing to apply.

Refs m/paliad#15, m/paliad#18 (rule-Typ contradiction filed against
Item A scope, not part of this batch).
2026-05-08 19:15:44 +02:00
m
097e21c8db feat(fristenrechner): collapse proceeding-picker after pick + columns view default
m's 2026-05-08 18:26 dogfood batch — two pure UX tweaks on the
Verfahrensablauf wizard:

1) Collapse the proceeding-picker once a Verfahren is chosen. Replaces
   the four-group block (UPC / DE / EPA / DPMA, ~25 buttons total)
   with a one-line "Verfahren: X · [Anderes Verfahren wählen]" pill.
   Reselect re-expands without throwing away the rest of the wizard
   state (trigger date, flags, calc result stay put until the user
   actually picks again). reset() also re-expands.

2) Column view as the default for step 3. The proactive / court /
   reactive grid reads more naturally for the HLC team than the
   single vertical timeline. URL semantics flipped: ?view=timeline
   now opts back into the legacy view; absence of ?view= yields
   columns. Share links stay clean.

Files:
  - frontend/src/fristenrechner.tsx — new .proceeding-summary
    markup; the view-toggle radio order swapped so "Spalten" is the
    first / checked option.
  - frontend/src/client/fristenrechner.ts — setProceedingPickerCollapsed
    helper toggles the four .proceeding-group blocks vs the summary;
    selectProceeding collapses, reset() + Reselect re-expand.
    procedureView default flipped to "columns"; initViewToggle URL
    semantics inverted.
  - frontend/src/client/i18n.ts — 2 new keys (DE+EN) for the
    summary label + Reselect button.
  - frontend/src/styles/global.css — .proceeding-summary +
    .proceeding-summary-reselect styles, all bound to the existing
    --color-* token palette.

Refs m/paliad#15 dogfood thread (m's 2026-05-08 18:26 batch).
2026-05-08 18:31:35 +02:00
m
2d6ea3ee33 feat(deadline-rules/is-optional): conditional rules opt-in via save modal
m's 2026-05-08 batch Item 2: some rules don't always apply per-instance.
Antrag auf Kostenentscheidung (RoP.151) only fires when a party files
for it; some appeal-related deadlines depend on specific facts. Today
they render in the timeline as if always applicable; the save-to-
project modal pre-checks them so the user has to remember to uncheck.

New paliad.deadline_rules.is_optional bool flag (default false). Threads
through the Go model, ruleColumns SELECT, UIDeadline JSON, and the
frontend save modal:

  - Migration 066 adds the column + comment + a starter UPDATE that
    flips RoP.151 to is_optional=true. m can flip more via SQL as he
    reviews the rule library — distinct from is_mandatory, which is
    about statutory strictness once the rule applies (an "auf Antrag"
    rule can be is_mandatory=true once requested).
  - Save modal: optional rows pre-uncheck (the user opts in) and a
    small "auf Antrag" / "on request" pill renders in the meta line.
    Court-determined rows still pre-uncheck via the existing disabled
    path; isOptional doesn't override that.

Migration applied to live Supabase; tracker at v66.

Refs m/paliad#15 (m's 2026-05-08 18:21 follow-up batch Item 2).
2026-05-08 18:26:26 +02:00