Commit Graph

1128 Commits

Author SHA1 Message Date
mAi
5f0a85fa83 refactor(litigationplanner): extract Fristen/Verfahrensablauf calc into pkg/litigationplanner (Slice A, t-paliad-298 / m/paliad#124)
Some checks failed
Paliad CI gate / build (push) Has been cancelled
Paliad CI gate / test-go (push) Has been cancelled
Paliad CI gate / deploy (push) Has been cancelled
Atomic extraction of the deadline-rule compute engine + types from
internal/services into a new pkg/litigationplanner package that paliad
+ youpc.org can both import. No behaviour change — every existing test
passes against the post-move shape.

Package contents (~1850 LoC):
- doc.go              package docstring + reuse manifesto
- types.go            Rule, ProceedingType, NullableJSON, AdjustmentReason,
                      HolidayDTO, CalcOptions, CalcRuleParams, Timeline,
                      TimelineEntry, RuleCalculation*, FristenrechnerType,
                      ProjectHint, sentinel errors
- catalog.go          Catalog interface (proceeding + rule lookups)
- holidays.go         HolidayCalendar interface
- courts.go           CourtRegistry interface + DefaultsForJurisdiction +
                      country/regime constants
- expr.go             EvalConditionExpr + HasConditionExpr +
                      ExtractFlagsFromExpr (jsonb gate evaluator)
- durations.go        ApplyDuration + AddWorkingDays (pure compute)
- subtrack.go         SubTrackRouting + LookupSubTrackRouting registry
- legal_source.go     FormatLegalSourceDisplay + BuildLegalSourceURL
- proceeding_mapping.go  MapLitigationToFristenrechner + code constants
                      (CodeUPCInfringement, CodeDEInfringementLG, ...)
- engine.go           Calculate + CalculateRule + the trigger-event
                      branch + applyRuleOverrides (the big move)

paliad side (~1900 LoC net deletion):
- internal/services/fristenrechner.go shrinks from 1505 → ~290 lines
  (thin paliad Catalog adapter + type aliases for back-compat).
- internal/models/models.go: DeadlineRule, ProceedingType, NullableJSON
  become type aliases to litigationplanner.* — every sqlx scan and
  every projection_service caller compiles unchanged.
- internal/services/holidays.go: AdjustmentReason + HolidayDTO become
  aliases to lp.* (canonical definitions now in the package).
- internal/services/proceeding_mapping.go: rewritten as thin re-exports
  of lp constants + helpers.
- internal/services/deadline_search_service.go: FormatLegalSourceDisplay
  + BuildLegalSourceURL replaced with delegating wrappers to lp.

Catalog interface satisfaction:
- DeadlineRuleService → paliadCatalog adapter (wraps the existing
  service, replicates the original SELECT shapes).
- HolidayService → satisfies lp.HolidayCalendar directly (compile-
  time assertion at end of fristenrechner.go).
- CourtService → satisfies lp.CourtRegistry directly.

Wire shape is byte-identical. JSON tags on Rule / ProceedingType /
Timeline / TimelineEntry / RuleCalculation match the historical
UIResponse / UIDeadline shape; the frontend reads the same bytes.

Slice B (Catalog interface + paliad loader cleanup) is folded into
this commit since Slice A already needs the interfaces to call
Calculate across the boundary. Slice C (embedded UPC snapshot +
generator) is the next coder shift; the Berufung unification m
called out lands in Slice B/C per head's brief.

Refs: docs/design-litigation-planner-2026-05-26.md
2026-05-26 13:01:07 +02:00
mAi
6e585951ee docs(litigation-planner): fold m's AskUserQuestion picks — new paliad.scenarios table + jsonb spec, no user-authored rules (t-paliad-292)
m's 2026-05-26 decisions:
- Q1 composition: primary+spawned (v1) with multi-proceeding peer compose as v2 goal — jsonb spec architected for N entries from day 1
- Q2 scope: per-project + abstract (project_id NULL = abstract saved templates)
- Q3 dates: per-anchor overrides over one base date (matches today's compute)
- Q4 storage: new paliad.scenarios table with jsonb spec (NOT project_event_choices column extension)
- "users should not add their own rules" — original Slice E (user-authored rules) DROPPED, replaced with abstract scenarios surface on /tools/verfahrensablauf

§5 rewritten with new schema (paliad.scenarios + active_scenario_id FK), jsonb spec shape (proceedings[] array, version-tagged), validate-on-load discipline, multi-peer v2 path. §6 struck-through with original body preserved as historical context. §10 slice plan revised: Slice E = abstract scenarios surface, not user-authored rules. §0.5 added with decision matrix; §13 marked resolved.

Package shape (§2 §3) unchanged — library was decoupled from persistence/UI choices by design.
2026-05-26 12:55:52 +02:00
mAi
8240717b5a docs(litigation-planner): pkg/litigationplanner design for paliad + youpc.org reuse (t-paliad-292)
Inventor design for m/paliad#124. Atomic extract of FristenrechnerService /
DeadlineCalculator / proceeding_mapping / SubTrackRoutings / legal-source
helpers into pkg/litigationplanner with Catalog / HolidayCalendar /
CourtRegistry interfaces. youpc.org reuse via embedded UPC snapshot
(catalog.json + holidays.json + courts.json) shipped inside the package.

6 slices: A extract, B catalog interface, C embedded snapshot + generator,
D scenarios persistence (project_event_choices.scenario_name), E
user-authored rules (deadline_rules.project_id), F youpc-side PR.

Q1 + Q2 (material) escalated to head per inventor protocol — NOT
AskUserQuestion. Q3-Q5 locked. Decision picks (R) noted; doc holds together
under any answer to the open Qs because pkg shape is decoupled from
persistence choices.
2026-05-26 12:55:52 +02:00
mAi
593e6243e0 Merge: t-paliad-295 — side-aware Verfahrensablauf column headers (Proaktiv/Reaktiv ↔ Unsere/Gegenseite) (m/paliad#127)
Some checks failed
Paliad CI gate / build (push) Has been cancelled
Paliad CI gate / test-go (push) Has been cancelled
Paliad CI gate / deploy (push) Has been cancelled
2026-05-26 11:59:29 +02:00
mAi
15cc5e418c feat(verfahrensablauf): side-aware column header labels (t-paliad-295)
Some checks failed
Paliad CI gate / build (push) Has been cancelled
Paliad CI gate / test-go (push) Has been cancelled
Paliad CI gate / deploy (push) Has been cancelled
m/paliad#127 — m's correction to #88. The user-perspective labels
"Unsere Seite" / "Gegnerseite" only make sense once the user has picked
a side; while side === null (Nicht festgelegt, the default after #120)
the column headers fall back to the semantic-neutral pair
"Proaktiv" / "Reaktiv". Picking a side re-enables the #88 labels.

renderColumnsBody now branches the leftLabel / rightLabel pair on the
incoming side. Bucketing primitive untouched: column placement is
unchanged, only the column-header text differs.

New i18n keys deadlines.col.proactive / deadlines.col.reactive (DE +
EN). The label fallback is documented inline in
verfahrensablauf-core.ts so a future reader sees why the columns have
two header modes.

Tests: four renderColumnsBody assertions covering side=null (explicit
+ default), side=claimant, side=defendant. Existing bucketing tests
unchanged.
2026-05-26 11:57:39 +02:00
mAi
abf0328dcd Merge: t-paliad-297 — remove /admin/rules/export page + export-migrations API (m/paliad#129)
Some checks failed
Paliad CI gate / build (push) Has been cancelled
Paliad CI gate / test-go (push) Has been cancelled
Paliad CI gate / deploy (push) Has been cancelled
2026-05-26 11:51:48 +02:00
mAi
cc13a5b857 chore(admin): remove /admin/rules/export page + export-migrations API (t-paliad-297)
Some checks failed
Paliad CI gate / build (push) Has been cancelled
Paliad CI gate / test-go (push) Has been cancelled
Paliad CI gate / deploy (push) Has been cancelled
Workflow shifted to hand-written numbered migrations; the audit-row SQL
export tool no longer has any consumers. Pure deletion — /admin/rules
and /admin/rules/{id}/edit stay; only the export-to-SQL flow goes.

Deleted:
- frontend/src/admin-rules-export.tsx
- frontend/src/client/admin-rules-export.ts

Removed:
- routes GET /admin/rules/export and GET /admin/api/rules/export-migrations
- handleAdminExportRuleMigrations + handleAdminRulesExportPage
- RuleEditorService.ExportMigrationsSince + ExportResult + sqlEscape helper
- build.ts entries (import, client bundle, dist HTML write)
- Sidebar "Regel-Migrations" nav item + "Migrations exportieren" button on /admin/rules
- all admin.rules.export.* + nav.admin.rules_export + admin.rules.list.export i18n keys (DE+EN)
- .admin-rules-export-* CSS rules (dead after page deletion)

Doc references in design-fristen-phase2-2026-05-15.md and
design-paliad-data-export-2026-05-19.md updated to mark the endpoint as
removed (acceptance #2 requires grep to return zero hits).
2026-05-26 11:50:14 +02:00
mAi
abef74fe63 Merge: t-paliad-296 — sort post-trigger optional events by duration ascending (m/paliad#128)
Some checks failed
Paliad CI gate / build (push) Has been cancelled
Paliad CI gate / test-go (push) Has been cancelled
Paliad CI gate / deploy (push) Has been cancelled
2026-05-26 11:22:33 +02:00
mAi
49ddaa4eb8 feat(fristenrechner): sort post-trigger events by duration ASC within parent group (t-paliad-296)
Some checks failed
Paliad CI gate / build (push) Has been cancelled
Paliad CI gate / deploy (push) Has been cancelled
Paliad CI gate / test-go (push) Has been cancelled
Optional events anchored on the same trigger (e.g. the four
post-Entscheidung rules in upc.inf.cfi) used to render in catalog
sequence_order, so a 2-month rule (R.118.4 Folgeentscheidungen)
would precede a 1-month rule (R.151 Kostenentscheidung) chained
off the same decision. Now the calculator does a post-evaluation
permutation pass that sorts consecutive same-parent rows by
duration ascending — days < weeks < months < years, ties broken
by duration_value then submission_code.

Different trigger groups keep their proceeding-sequence position
— the walk only ever permutes rows that already share a parent.
Root rules (no parent) are never sorted against each other.
Court-set / conditional rows whose date isn't in the duration
ladder sort LAST within their group.

Verified order against m's report: R.151 cost_app + R.353
rectification (1-month tier) now render before R.220.1
appeal_spawn + R.118.4 cons_orders (2-month tier).

Issue: m/paliad#128
2026-05-26 11:21:29 +02:00
mAi
1bd2ebb4ae Merge: t-paliad-294 — conditional label uses trigger_event name (R.262(2) → Vertraulichkeitsantrag) (m/paliad#126)
Some checks failed
Paliad CI gate / build (push) Has been cancelled
Paliad CI gate / test-go (push) Has been cancelled
Paliad CI gate / deploy (push) Has been cancelled
2026-05-26 11:19:40 +02:00
mAi
f6c8eb5bcf fix(projection): conditional label uses trigger_event_id, not parent_id
Some checks failed
Paliad CI gate / build (push) Has been cancelled
Paliad CI gate / test-go (push) Has been cancelled
Paliad CI gate / deploy (push) Has been cancelled
t-paliad-294 / m/paliad#126. knuth's #121 conditional-rendering
defaulted the "abhängig von <parent>" chip to the rule's parent_id
display name. For R.262(2) Erwiderung auf Vertraulichkeitsantrag the
parent_id resolves to the SoC (Klageerhebung), but the rule's real
semantic anchor is the opposing party's confidentiality application
(paliad.trigger_events id=25). The chip read "abhängig von
Klageerhebung", which is wrong.

Fix: when a rule has a non-NULL trigger_event_id, the engine stamps
ParentRuleCode / ParentRuleName / ParentRuleNameEN from the
trigger_events catalog row instead of from the parent_id chain. The
parent_id stays as the calc-time arithmetic anchor — only the user-
facing dependency identity shifts.

Generalises across every rule with a real trigger_event_id (2 rows
in the live corpus today: confidentiality_response and
translations_lodge — both relabel correctly).

Touches both surfaces in one shot: verfahrensablauf-core's chip
("abhängig von …") and shape-timeline's "Folgt aus …" footer both
read from ParentRule*, so no frontend change needed.

Tests: extend TestUIDeadline_IsConditional_UncertainAnchors with a
DE+EN string-pinning case for R.262(2) plus a generalisation guard
for translations_lodge. Negative guard asserts the chip no longer
leaks "Klageerhebung" / "Statement of Claim".
2026-05-26 11:19:01 +02:00
mAi
5ba4df9d55 Merge: t-paliad-293 — event-card overhaul (caret menu + iconified state + no-scroll unhide) (m/paliad#125)
Some checks failed
Paliad CI gate / build (push) Has been cancelled
Paliad CI gate / test-go (push) Has been cancelled
Paliad CI gate / deploy (push) Has been cancelled
2026-05-26 10:48:22 +02:00
mAi
7ca6b2d643 feat(verfahrensablauf): event-card overhaul — iconified state + caret-popover unhide (t-paliad-293)
Some checks failed
Paliad CI gate / build (push) Has been cancelled
Paliad CI gate / test-go (push) Has been cancelled
Paliad CI gate / deploy (push) Has been cancelled
m/paliad#125 — concern A (horizontal scroll) and concern B (compact
event-card UX).

Concern A: the inline "Wieder einblenden" chip from t-paliad-290 pushed
hidden cards past their column width on 375/414/768, causing horizontal
page scroll. Fix: drop the chip entirely; surface the un-hide as a
prominent "Wieder einblenden" entry inside the caret popover (matches
the m's "actions live in the caret menu" framing). The card title row
now also wraps + shrinks (flex-wrap + min-width:0 + overflow-wrap)
so no inline child can ever blow the row width.

Concern B (the bigger UX): cards now speak m's "cut the tree of
possibilities" vocabulary via iconified state markers in the title row:
  - Optional event → ⊙ (timeline-state-icon--optional)
  - Hidden by user → 👁⃠ (timeline-state-icon--hidden)
  - Conditional anchor → already covered by the "abhängig von <parent>"
    chip on the date column (t-paliad-289); no duplicate marker.
  - CCR-included / appellant picks → already on the per-card chip.

The legacy `.optional-badge` text chip and `.event-card-choices-unhide`
inline chip are gone — both replaced by the icon language + popover
entry.

Renderer wires the unhide path with two contracts:
  - data-is-hidden="1" on the caret button when isHidden=true, so the
    popover knows to render the prominent unhide block on top.
  - Defensive fallback: if a rule's choices_offered was edited away
    after the user had already saved skip=true (so isHidden=true but
    choicesOffered is empty), the renderer synthesizes {skip:[true,
    false]} so the popover still has an un-hide path.

CSS:
  - .timeline-item min-height 4rem → 2.75rem (less vertical air).
  - .timeline-content padding-bottom 1rem → 0.6rem (tighter gutter).
  - .timeline-item-header gains flex-wrap + min-width:0.
  - .timeline-name gains min-width:0 + overflow-wrap:anywhere
    (long German compounds wrap mid-word instead of overflowing).
  - New: .timeline-state-icon[--optional|--hidden] icon-style markers.
  - New: .event-card-choices-unhide-btn — prominent full-width lime
    pill inside the popover, midnight-text in both themes (matches
    the active-option pin from m/paliad#123).

i18n:
  - state.optional.tooltip — "Optionales Ereignis" / "Optional event"
  - state.hidden.tooltip — "Ausgeblendet — über Optionen-Menü wieder
    einblenden" / "Hidden — restore via the options menu"
  - choices.unhide.chip kept (now used as the popover button label).

Tests: 27 → 29 tests in verfahrensablauf-core.test.ts. Old isHidden
inline-chip cases replaced by state-icon + caret-data-is-hidden
contract cases. Added defensive-fallback case for the synthesized
skip offer. Added regression guard that the legacy
.event-card-choices-unhide class is no longer emitted. Added
optional-priority → ⊙ icon contract pair.

Hard rules respected:
  - Title + date + Rule citation unchanged (m likes these).
  - Click-to-edit on date span (.frist-date-edit) untouched.
  - Conditional rendering (t-paliad-289 chip + dotted border) untouched.
  - Per-card actions (skip, appellant pick, include-CCR, unhide) all
    reachable via the caret popover.

go build ./... && go test ./internal/... && cd frontend && bun run
build && bun test — all green (181 tests).
2026-05-26 10:11:02 +02:00
mAi
ed8af0dca9 Merge: t-paliad-289 — conditional rule projection (post-rebase) (m/paliad#121)
Some checks failed
Paliad CI gate / build (push) Has been cancelled
Paliad CI gate / test-go (push) Has been cancelled
Paliad CI gate / deploy (push) Has been cancelled
2026-05-26 09:58:48 +02:00
mAi
293e612582 feat(projection): IsConditional for uncertain-anchor rules (t-paliad-289)
Some checks failed
Paliad CI gate / build (push) Has been cancelled
Paliad CI gate / test-go (push) Has been cancelled
Paliad CI gate / deploy (push) Has been cancelled
Rules anchored on uncertain triggers (R.109 backward-anchor without
oral-hearing date; R.118(4) without validity decision; R.262(2)
without recorded Vertraulichkeitsantrag) previously rendered concrete
dates fabricated off the trigger date. Add IsConditional projection
flag so the SmartTimeline + Verfahrensablauf surfaces "abhängig von
<parent>" instead of a misleading date.

Backend (fristenrechner.go):
- Add IsConditional + ParentRuleCode/Name/NameEN to UIDeadline.
- Pre-pass populates courtSet from rule.is_court_set=true BEFORE the
  main loop, so order-of-evaluation in sequence_order no longer matters
  for the parent-court-set check. Fixes R.109(1) "Antrag auf
  Simultanübersetzung" (sequence_order=45 < Mündliche Verhandlung's
  sequence_order=50): the timing='before' backward arithmetic was
  computing 1 month before the trigger date because the court-set
  parent hadn't been classified yet.
- Set IsConditional=true on every IsCourtSetIndirect branch (catches
  R.109 backward + R.118(4) cons_orders chain off the decision).
- Set IsConditional=true for priority='optional' + primary_party='both'
  rules whose data-model parent is the trigger anchor (covers R.262(2)
  confidentiality_response: the data anchors on SoC, but the real
  trigger is the opposing party's confidentiality motion which may
  never happen). Suppressed by IsOverridden so user anchors win.

Backend (projection_service.go):
- Add IsConditional to TimelineEvent + propagate from UIDeadline.
- New Status="conditional" for projected rows; clears Date, populates
  DependsOnRuleCode/Name from UIDeadline.ParentRule* so the row
  carries the "abhängig von <parent>" payload even when the parent
  has no computed date for annotateDependsOn to discover.

Frontend (verfahrensablauf-core.ts + CSS + i18n):
- CalculatedDeadline gains isConditional + parentRule* fields.
- deadlineCardHtml renders "abhängig von <parent>" chip with
  click-to-edit affordance in place of the date column when
  isConditional=true. IsConditional wins over IsCourtSet for the
  date column (they overlap; "abhängig von <parent>" names the
  specific blocker).
- .timeline-item--conditional / .fr-col-item--conditional CSS:
  dotted border + faded text so the conditional state reads at glance.
- Replaced escHtml's DOM-backed implementation with a pure-JS regex
  escape so the module is testable in bun test without jsdom (the
  old form forced fixtures to leave several fields empty just to
  avoid the DOM dependency).

Tests:
- TestApplyLookaheadCap_ConditionalRowsPassThrough: pure-function lock
  that conditional rows pass through applyLookaheadCap untouched
  (don't count against ProjectedTotal/Shown, don't get capped).
- TestUIDeadline_IsConditional_UncertainAnchors (TEST_DATABASE_URL):
  asserts R.109(1)/(4), R.118(4) chain, and R.262(2) all render
  IsConditional=true with empty DueDate + populated ParentRule*; SoD
  stays non-conditional; override on the oral hearing flips R.109(1)
  back to concrete date.
- 4 new bun tests for the conditional rendering branches in
  deadlineCardHtml.

UX path verified by tests + manual review of the live rule corpus:
opening a UPC inf project without oral-hearing date now surfaces
R.109(1) + R.109(4) as conditional; recording the Vertraulichkeitsantrag
(anchoring R.262(2) via the existing "Datum setzen" flow) flips it
back to a concrete date.

go build / go test / bun test / bun run build all clean.
2026-05-26 09:56:15 +02:00
mAi
9d3325bd88 Merge: t-paliad-291 — dark-mode lime-chip contrast fix across 6 selectors (m/paliad#123)
Some checks failed
Paliad CI gate / build (push) Has been cancelled
Paliad CI gate / test-go (push) Has been cancelled
Paliad CI gate / deploy (push) Has been cancelled
2026-05-26 09:47:02 +02:00
mAi
18d2e743ba fix(styles): dark-mode contrast on lime-active chips (t-paliad-291)
Some checks failed
Paliad CI gate / build (push) Has been cancelled
Paliad CI gate / test-go (push) Has been cancelled
Paliad CI gate / deploy (push) Has been cancelled
Six surfaces paired a lime background with var(--color-text), which
flips to cream in dark mode and collapses contrast on the high-luminance
brand lime. Switch them to var(--color-accent-dark) — the design token
already defined to stay midnight in both themes as the WCAG-AA fg on
lime.

Affected:
  - .event-card-choices-option--active  (Berufung durch … popover —
    m's primary report on m/paliad#123)
  - .fristen-row.is-active .fristen-row-num
  - .form-hint-badge
  - .paliadin-widget-send-btn
  - .smart-timeline-anchor-submit
  - .admin-rules-chip.active

Lime hue and non-active states untouched.

Refs: m/paliad#123
2026-05-26 09:45:59 +02:00
mAi
07d2eb472c Merge: t-paliad-287 — submission form revision (Frist drop + grouped sections + Add Party + DB picker) (m/paliad#119)
Some checks failed
Paliad CI gate / build (push) Has been cancelled
Paliad CI gate / test-go (push) Has been cancelled
Paliad CI gate / deploy (push) Has been cancelled
2026-05-26 09:42:58 +02:00
mAi
7cdccd55ae feat(submission-draft): grouped sections + per-side Add Party with DB picker (t-paliad-287)
Some checks failed
Paliad CI gate / build (push) Has been cancelled
Paliad CI gate / test-go (push) Has been cancelled
Paliad CI gate / deploy (push) Has been cancelled
Restructures the submission-draft sidebar per m's m/paliad#119 review.

Three changes on the variable form (Part B):
- VARIABLE_GROUPS collapses into four lawyer-facing sections: Mandant
  & Verfahren (firm.* + project.* + procedural_event.*), Parteien
  (manual {{parties.<role>.*}} overrides), Frist (the now-internal
  deadline.* block, COLLAPSED by default since the skeletons no
  longer render it), Sonstiges (today.* / user.* trim).
- Group sections are click-to-collapse via a sticky state map; the
  Frist + Parteien-override sections open closed so the visible form
  stays tight on first load.
- The legacy {{rule.*}} aliases drop off the sidebar — still resolved
  by SubmissionVarsService for old templates, no longer surfaced as
  override rows (they cluttered the form and the canonical
  procedural_event.* names cover the same ground).

Multi-party + Add Party (Part C):
- The party picker now renders all three role buckets (claimants /
  defendants / others) even when empty, so the lawyer can populate via
  Add Party. The block is hidden only when no project is attached.
- Each side gets a "+ Partei hinzufügen (Klägerseite / Beklagtenseite
  / Weitere Parteien)" button that opens an inline panel with two
  tabs:
  - Manual entry — name, role (pre-filled from side), representative.
    Submits to POST /api/projects/{id}/parties, creating a real
    paliad.parties row that immediately surfaces in available_parties.
  - Aus DB übernehmen — debounced (200ms) search against the new
    GET /api/parties/search endpoint. Returns hits across every
    visible project with project_title + reference for context.
    Already-on-this-project rows are filtered out client-side. Picking
    a hit clones name/role/representative into a fresh row on the
    current project — the simplest semantics that survives the
    paliad.parties.project_id NOT NULL contract while honouring m's
    "no manual re-typing" requirement.
- Newly-added parties land in selected_parties immediately so the new
  party is rendered in the next preview round-trip without an extra
  click. Implicit-"all" default is preserved (empty selected_parties
  still means "every party on the project, including this new one").
- Search-result repaints reach only into the <ul>, not the whole
  picker — keeps focus + selection on the search input across
  keystrokes.

CSS:
- Collapsible-section caret rotation, busy/disabled form states, tab
  highlights, DB-picker result rows with project chip + hover, all
  inherit the existing lime-tint accent so the new affordances look
  native to the editor.

TSX:
- Comment update on the parties block; no structural change. The
  bilingual hint copy in i18n.ts now nudges towards Add Party.
2026-05-26 09:41:36 +02:00
mAi
d4ed989b8f feat(parties): cross-project party search endpoint for submission picker (t-paliad-287)
Adds PartyService.Search returning paliad.parties rows from every
project the caller can see, matched by case-insensitive substring on
name or representative. Wired via GET /api/parties/search?q=... — used
by the submission-draft Add-Party panel's "Aus DB übernehmen" tab.

Visibility flows through the same visibilityPredicatePositional helper
every project-scoped read uses; invisible projects' parties never
surface. Capped at 25 hits per call (no pagination — typical lookup is
"the party I'm thinking of by name", not a browse).

Result shape carries project_title + project_reference so the picker
can disambiguate identically-named parties across cases.
2026-05-26 09:41:07 +02:00
mAi
54fb676db5 chore(templates): drop 'Frist' block from skeleton + HL-firm-skeleton (t-paliad-287)
Per m's m/paliad#119 report: the {{deadline.*}} block was leaking
internal/admin context (Frist-Bezeichnung, Fälligkeit, "berechnet aus",
Quelle) into court-bound submissions. The dedicated Frist heading and
its 4 body lines are removed from both gen-skeleton-submission-template
(_skeleton.docx) and gen-hl-skeleton-template (_firm-skeleton.docx).
The {{deadline.due_date_long_en}} reference in the locale-aware
verification footer is also dropped. {{deadline.*}} placeholders stay
resolvable in SubmissionVarsService — a custom template can still pick
them up — but the default skeletons no longer render them in the body.

Regenerated .docx files uploaded to HL/mWorkRepo:
- 6 - material/Templates/Word/Paliad/HLC/_skeleton.docx → d0ecc0e
- 6 - material/Templates/Word/Paliad/HLC/_firm-skeleton.docx → 25954c9
2026-05-26 09:41:01 +02:00
mAi
c3eaa9b1d4 Merge: t-paliad-290 — show-hidden toggle + un-hide chip on Verfahrensablauf (m/paliad#122)
Some checks failed
Paliad CI gate / build (push) Has been cancelled
Paliad CI gate / test-go (push) Has been cancelled
Paliad CI gate / deploy (push) Has been cancelled
2026-05-26 09:39:52 +02:00
mAi
80883eaac5 feat(verfahrensablauf): re-surface hidden optional events — show-hidden toggle + un-hide chip (t-paliad-290)
Some checks failed
Paliad CI gate / build (push) Has been cancelled
Paliad CI gate / test-go (push) Has been cancelled
Paliad CI gate / deploy (push) Has been cancelled
m/paliad#122. atlas's #96 Slice A added per-card 'Überspringen' but no
un-skip path — hidden cards just disappeared from the timeline. This
adds the missing return path:

- CalcOptions.IncludeHidden (default false) tells the calculator to
  re-surface skipRules entries as faded rows instead of dropping them.
  When true, the rule renders with UIDeadline.IsHidden=true and the
  descendant-suppression cascade is bypassed so children compute their
  dates off the un-suppressed parent.
- UIResponse.HiddenCount always reflects the projection's hide count
  (gate-passed rules whose submission_code is in skipRules) so the
  "Ausgeblendete (N)" badge stays accurate regardless of toggle state.
- /tools/verfahrensablauf gets a "Ausgeblendete anzeigen" checkbox next
  to the perspective + appellant selectors. URL-driven (?show_hidden=1)
  so the state is shareable and survives reload. The row hides itself
  on projections with zero hidden cards.
- Hidden cards render via .timeline-item--hidden / .fr-col-item--hidden
  (opacity 0.55 + dotted border, mirroring the existing
  --skipped fade) and carry an inline "Wieder einblenden" chip. Clicking
  the chip removes the skip choice via the page's existing
  attachEventCardChoices remove callback (URL state + recalc included)
  and runs through a new delegated handler in event-card-choices.ts.
- 3 new i18n keys (DE+EN): choices.show_hidden.label,
  choices.show_hidden.count, choices.unhide.chip.

The skip-choice storage shape (paliad.project_event_choices, atlas's
table) is unchanged — un-hide is just a delete of the skip row.

Tests: 3 new bun-test cases pin the chip contract (emits on isHidden=
true with submission_code, suppressed otherwise); go test ./internal/...
+ bun run build clean.
2026-05-26 09:38:31 +02:00
mAi
5e17de6e07 Merge: t-paliad-288 — Verfahrensablauf 'Beide' → 'Nicht festgelegt' (m/paliad#120)
Some checks failed
Paliad CI gate / build (push) Has been cancelled
Paliad CI gate / test-go (push) Has been cancelled
Paliad CI gate / deploy (push) Has been cancelled
2026-05-26 09:35:19 +02:00
mAi
0e1f62e375 feat(verfahrensablauf): replace 'Beide' chip with 'Nicht festgelegt' (t-paliad-288)
Some checks failed
Paliad CI gate / build (push) Has been cancelled
Paliad CI gate / test-go (push) Has been cancelled
Paliad CI gate / deploy (push) Has been cancelled
The Verfahrensablauf side selector offered Klägerseite / Beklagtenseite /
Beide. 'Beide' is legally impossible (no party is on both sides) — the
state being modelled is "perspective not yet picked", not "both sides".
Rename the chip to 'Nicht festgelegt' (DE) / 'Undefined' (EN) without
changing the underlying state value or projection behaviour.

- frontend/src/verfahrensablauf.tsx: chip label flips to
  deadlines.side.undefined; add inline hint chip
  "Wählen Sie eine Seite, um die Spalten zu fokussieren." next to the
  radio cluster, shown only while no side is picked.
- frontend/src/client/verfahrensablauf.ts: sideLabelI18n() returns the
  new key for null; syncSideHintVisibility() toggles hint display from
  initPerspectiveControls, the side-radio change handler, and
  showSideRadioCluster (chip→radio override path).
- frontend/src/client/i18n.ts: rename deadlines.side.both →
  deadlines.side.undefined (DE: Nicht festgelegt, EN: Undefined); add
  deadlines.side.hint in both languages.
- frontend/src/i18n-keys.ts: rename in the union, keep alphabetical
  order.
- frontend/src/styles/global.css: .side-radio-cluster becomes inline-flex
  so the hint sits next to the toggle; .side-hint styled muted+italic.

URL backward-compat: ?side=both is already silently treated as null by
readSideFromURL (only accepts claimant|defendant) — same column
behaviour as before, no migration needed. projects.field.our_side.both
is a different concept (a project being a multi-party participant) and
stays untouched.

Tests: 17/17 in verfahrensablauf-core.test.ts still pass; the
"default (no opts) mirrors 'both' rules into ours AND opponent" case
already covers the unchanged null-side projection. Go build + tests
clean. Frontend build clean (i18n scan: 2901 keys, data-i18n
attributes clean).

m/paliad#120
2026-05-26 09:33:00 +02:00
mAi
cca5e72c57 ci: trigger workflow run to verify Slice A pre-deploy gate (post DOKPLOY_TOKEN setup)
Some checks failed
Paliad CI gate / build (push) Has been cancelled
Paliad CI gate / test-go (push) Has been cancelled
Paliad CI gate / deploy (push) Has been cancelled
2026-05-26 09:19:27 +02:00
mAi
4d923562f5 Merge: t-paliad-283 — /views/any filter-bar Predicates flatten fix (m/paliad#115)
Some checks failed
Paliad CI gate / build (push) Has been cancelled
Paliad CI gate / test-go (push) Has been cancelled
Paliad CI gate / deploy (push) Has been cancelled
2026-05-25 17:47:57 +02:00
mAi
c70914c2a0 fix(filter-bar): flatten FilterSpec.Predicates wire shape (t-paliad-283)
The bar's chip clicks POST a payload shaped as `predicates: {<source>:
<per-source>}` — flat, one entry per data source. Go declared
`Predicates map[DataSource]Predicates` — a doubled-nested wrapper where
each map value was itself a Predicates struct with named per-source
fields. The JSON shape Go expected was
`{"deadline": {"deadline": {"status": [...]}}}`; the shape the bar
emitted was `{"deadline": {"status": [...]}}`. Go silently unmarshalled
the bar's payload as `Predicates{}` (all source fields nil), so every
chip click on /views/any was a server-side no-op — the regression in
#115.

The latent contract bug was present since t-paliad-144 A1 (b516201) but
only surfaced now: /inbox uses the InboxSystemView's code-resident
predicates (built in Go directly, doubled shape works) and saved views
never carried predicates in the DB, so chip-click overlays were the
only path that exercised the wire-format wrong way. /views/any made
that path visible because all four sources need narrowing.

Fix: align Go to the flat shape the frontend already emits.

- FilterSpec.Predicates: `map[DataSource]Predicates` → `*Predicates`.
- All `spec.Predicates[SourceX]` access sites in view_service.go +
  approvalStatusMatches + allowed* helpers + system_views literals
  + tests rewritten to `spec.Predicates.X` with a nil-spec.Predicates
  guard.
- Frontend FilterSpec.predicates type tightened from
  `Partial<Record<DataSource, Predicates>>` (which silently allowed
  the wrong runtime write) to `Predicates`.

Regression coverage:

- `filter_spec_predicates_test.go` (new, Go) pins three contracts:
  the bar's exact wire payload unmarshals into a non-nil per-source
  predicate; marshalling a Go-constructed spec produces the same flat
  shape; the "Erledigt" chip's request narrows to completed deadlines.
- `compute-effective.test.ts` (new, bun:test) pins 12 chip-overlay
  cases for /views/any (every axis the saved view's sources expose).

Build hygiene:
- `go build ./...` clean.
- `go test ./... -count 1` clean (existing inbox + filter_spec tests
  updated for the new struct shape; new tests pass).
- `cd frontend && bun run build` clean.
- `cd frontend && bun test src/` — 169 pass, 0 fail.

No migration: paliad.user_views.filter_spec jsonb rows live with
`predicates: {}` or no predicates field; both unmarshal as nil
*Predicates under the new type, identical to the no-narrowing behaviour
the old map type produced for the same rows.
2026-05-25 17:46:58 +02:00
mAi
016ac2532a Merge: t-paliad-282 Slice A — CI/CD pre-deploy gate + snapshot-based migration smoke (m/paliad#114)
Some checks failed
Paliad CI gate / build (push) Has been cancelled
Paliad CI gate / test-go (push) Has been cancelled
Paliad CI gate / deploy (push) Has been cancelled
2026-05-25 17:42:51 +02:00
mAi
c901293c9c feat(cicd): Slice A — pre-deploy gate + role-split migration smoke
Some checks failed
Paliad CI gate / build (push) Has been cancelled
Paliad CI gate / test-go (push) Has been cancelled
Paliad CI gate / deploy (push) Has been cancelled
Adds .gitea/workflows/test.yaml that gates every push on `go build`,
`bun run build`, `go vet`, the migration coordination check, and the
role-split end-to-end migration smoke. On push to main + green, calls
Dokploy's compose.deploy API and polls /health/ready until 200.

t-paliad-282 / m/paliad#114. Design: docs/design-cicd-pre-deploy-gate-2026-05-25.md
(inventor shift on mai/cronus/inventor-ci-cd-pre).

Catches all three of today's outage classes:

  brunel (~13:20) slot collision     -> TestMigrations_NoDuplicateSlot
  hermes (~16:05) dropped-col refs   -> TestBootSmoke
  mig 129 (~14:56) 42501 ownership   -> TestMigrations_EndToEndAsAppRole

Snapshot approach. internal/db/testdata/prod-snapshot.sql is a pg_dump
of youpc-supabase paliad schema + applied_migrations rows. CI restores
this into a fresh `supabase/postgres:15.8.1.060` (same image, same role
topology as prod) and runs ApplyMigrations as the `postgres` role
(which is NOT a superuser on supabase/postgres, matching prod). Existing
migrations are skipped (already in applied_migrations); only NEW migs
from the PR run end-to-end. This sidesteps the fresh-DB idempotence
debt in some historical migrations (mig 037 missing pg_trgm, mig 051
inner COMMIT) — those are tracked separately and don't block the gate.

Sub-changes:

- internal/handlers/handlers.go — new /health/ready endpoint distinct
  from /healthz. /healthz stays liveness (process alive, no DB); /ready
  is readiness (DB pool pings within 2 s). Returns 503 when svc or pool
  is nil (DB-less deploys are intentionally not-ready). svc.Pool added
  to handlers.Services, wired in cmd/server/main.go.

- internal/db/migrate_test.go — TestMigrations_NoDuplicateSlot (pure
  unit, catches brunel) and TestMigrations_EndToEndAsAppRole (snapshot-
  gated, catches the 42501 class).

- cmd/server/main_smoke_test.go — TestBootSmoke now also asserts
  /health/ready returns 503 with a nil svc. New TestHealthReady_Live
  asserts 200 against a live pool.

- internal/db/migrations/024_rename_department_columns.up.sql and
  027_rename_to_partner_units.up.sql — ALTER INDEX / ALTER POLICY
  exception handlers now catch undefined_object OR undefined_table OR
  duplicate_object. Old handler only caught undefined_object; Postgres
  raises undefined_table when source object never existed, and
  duplicate_object when destination already exists. The expanded
  handlers make these migrations truly idempotent across all plausible
  starting states.

- Makefile — verify-mig-app, test-frontend, refresh-snapshot targets.
  refresh-snapshot pg_dumps youpc-supabase prod (needs PALIAD_PROD_DATABASE_URL),
  strips pg16 \restrict commands for pg15 restore compat, and filters
  applied_migrations rows to this branch's max on-disk version.

- internal/db/testdata/README.md — explains the snapshot's purpose,
  refresh procedure, and how to verify locally.

- docs/cicd-runner-setup-2026-05-25.md — one-time admin steps for
  registering a Gitea Actions runner on mriver and wiring DOKPLOY_TOKEN
  as a repo secret. Documents soft-launch plan per m's Q11.4 (keep
  Dokploy's autoDeploy=true webhook alive for one week, disable after
  the workflow has gated 5 successful deploys).

Build clean. Full go test ./internal/... ./cmd/... green without
TEST_DATABASE_URL. With TEST_DATABASE_URL + TEST_APP_DATABASE_URL set
to a supabase/postgres scratch + snapshot restored:
TestMigrations_NoDuplicateSlot, TestMigrations_EndToEndAsAppRole,
TestBootSmoke, TestHealthReady_Live all pass. Live-DB service tests in
internal/services/* fail under supabase/postgres 15.8 with a 42P08
parameter-binding error (unrelated to Slice A — tracked as a follow-up).
2026-05-25 17:42:06 +02:00
mAi
0b1653c2bf Merge: t-paliad-284 — Wave 1 Tier 1 rule additions + Q6 archived cleanup + audit FK fix (mig 132) (m/paliad#116) 2026-05-25 17:38:42 +02:00
mAi
a6cf6ff4c9 feat: t-paliad-284 Wave 1 Tier 1 deadline-rule additions (mig 132)
Add 12 Tier 1 procedural deadline rules from curie's audit §10
(docs/research-deadlines-completeness-2026-05-25.md), backfill the
UPC R.104/R.105 Interim Conference citation on upc.inf.cfi.interim
(m/paliad#116 / m's 2026-05-25 report), and fold in the audit Q6
cleanup of the 40 _archived_litigation.* rows.

New rules:
  T1.1  upc.inf.cfi.cmo_review           15d / R.333.2
  T1.2  upc.inf.cfi.confidentiality_response 14d / R.262.2 (trigger 25)
  T1.3  upc.apl.order.grounds_orders     15d / R.224.2(b)
  T1.4  upc.apl.order.response_orders    15d / R.235.2
  T1.5  upc.inf.cfi.cons_orders          2mo / R.118.4
  T1.6  upc.inf.cfi.rectification        1mo / R.353
  T1.7  upc.pi.cfi.deficiency            14d / R.207.6(a)
  T1.8  upc.pi.cfi.merits_start          31d OR 20wd (max) / R.213 + R.198.1
  T1.9  upc.inf.cfi.translation_request  1mo BEFORE oral / R.109.1
  T1.10 upc.inf.cfi.interpreter_cost     2wk BEFORE oral / R.109.4
  T1.11 upc.inf.cfi.translations_lodge   2wk / R.109.5 (trigger 113)
  T1.12 upc.pi.cfi.response              UPDATE: re-anchor on .app, court-set

T1.8 uses Wave 2 Slice A primitives (mig 128: working_days unit +
combine_op='max'). T1.9/T1.10 use timing='before' with the
backward-snap path in deadline_calculator.go.

Also drops the deadline_rule_audit.rule_id FK constraint. The mig 079
audit trigger had a latent bug — it could not log DELETEs because the
FK rejected the post-delete INSERT (count(*) WHERE action='delete'
was 0 across the entire history). Audit tables are append-only
history and should not FK-constrain on live entity tables; before_json
preserves the full row state. Unblocking this also unblocks the §13b
Q6 cleanup.

Verified on Supabase: 13 rows present in post-fix shape, all
assertions in the DO-block pass, audit log now records 11 creates +
2 updates + 40 deletes for this migration.
2026-05-25 17:29:13 +02:00
mAi
191d8e7268 Merge: t-paliad-285+286 — UPC Damages + PM appeal route deadlines (mig 133) (m/paliad#117, m/paliad#118) 2026-05-25 17:26:04 +02:00
mAi
cb44b3b8cc mAi: #117 + #118 - t-paliad-285/-286 UPC dmgs+pi court followup (mig 133)
Adds the post-submission court phase to upc.dmgs.cfi and the appeal
route to upc.pi.cfi. The Verfahrensablauf timeline currently stops at
the last party submission (dmgs.rejoin / pi.order); without these rows
the interim conference / oral hearing / decision / appeal sub-tree
never renders, even though atlas's #96 spawn mechanism is in place.

Migration 133 (single slot, coordinated with knuth's #116 on 132):

Section A — UPC Damages tree end (#117):
- upc.dmgs.cfi.interim       court-set, R.105
- upc.dmgs.cfi.oral          court-set, R.118 / R.250
- upc.dmgs.cfi.decision      court-set, R.118 / R.144
- upc.dmgs.cfi.appeal_spawn  2mo, R.220.1(a) / R.224.1(a), spawn → upc.apl.merits

Section B — UPC PI appeal route (#118):
- upc.pi.cfi.appeal_spawn    2mo, R.220.1(a) / R.224.1(a), spawn → upc.apl.merits
  PI orders under R.211 dispose of the urgent question and ride the
  main 2-month track; the 15-day R.220.1(c) order track does not apply.

Same shape as mig 095 inf.appeal_spawn and the upc.inf.cfi
interim/oral/decision rows from mig 012. Court-set rows reuse the
shared interim-conference / oral-hearing / decision concepts.

Citations: docs/research-deadlines-completeness-2026-05-25.md §D + Tier 4 (R.144), docs/audit-upc-rop-deadlines-2026-05-08.md §D R.144 + §F R.220.1(a)/R.224.1(a). Per-row RoP citation in the migration header.

Idempotent INSERT NOT EXISTS guards per row + post-insert DO block that RAISEs EXCEPTION if any expected row is missing or the spawn shape (is_spawn / spawn_proceeding_type_id / parent_id) is wrong.

go build ./... clean, go test ./internal/... clean, bun run build clean.
2026-05-25 17:25:19 +02:00
mAi
c4e9875ff4 Merge: t-paliad-276 — submission generator DE/EN language selector, post-rebase (mig 130) (m/paliad#108) 2026-05-25 17:04:23 +02:00
mAi
e4c694e01c mAi: #108 - t-paliad-276 submission generator language selector (DE/EN)
Per-draft `language` column drives the .docx output language for the
submission generator. The lawyer picks DE or EN on the draft editor's
sidebar; the generator selects the language-matched template variant
(falling back through {code}.{lang} → {code} → _skeleton.{lang} →
_skeleton → letterhead) and resolves language-aware variables
({{procedural_event.name}} → name_de vs name_en).

Schema (mig 130 — bumped from 129 to deconflict with atlas's #96):
- paliad.submission_drafts.language text NOT NULL DEFAULT 'de'
  CHECK IN ('de','en'). Existing rows inherit 'de' via the default,
  preserving every legacy draft's behaviour byte-for-byte.

Backend (Go):
- SubmissionVarsContext.Lang overrides the user's UI lang. Build()
  uses it when set; falls back to user.Lang otherwise — Slice 1's
  format-only /generate path keeps working unchanged.
- SubmissionDraftService.BuildRenderBag now threads draft.Language
  through. Create/EnsureLatest seed from the UI lang (DE default).
- DraftPatch.Language landed; Update validates and rejects values
  outside {de,en}. Project-scoped + global PATCH endpoints both
  surface the field.
- resolveSubmissionTemplate(ctx, code, lang) replaces the lang-less
  predecessor. Returns the matched tier (per_code_lang / per_code /
  skeleton_lang / skeleton / letterhead) so the editor knows whether
  to surface the "Fallback: universelles Skelett" notice.
- fileRegistry registers the EN skeleton sibling (`_skeleton.en.docx`)
  alongside the DE one; per-code EN variants land in a parallel
  submissionTemplateENRegistry (empty for now — EN templates land per
  HLC authoring). 404s from Gitea fall through silently.
- /api/projects/{id}/submissions/{code}/generate accepts
  `?language=de|en` query override (one-shot path, no draft row to
  pull the column from); defaults to the user's UI lang.

Frontend (TS/JSX):
- DE/EN radio above the variables list in the draft editor sidebar.
  Switching the radio PATCHes `language` and the server returns the
  freshly-resolved bag + preview HTML so the lawyer sees EN values
  immediately.
- Fallback notice ("Fallback: universelles Skelett (keine
  sprachspezifische Vorlage)") shows when the resolved tier doesn't
  match the requested language.
- 4 new i18n keys (DE + EN) + CSS for the toggle.

Tests:
- normalizeDraftLanguage covers DE/EN/case/whitespace/unknown.
- addRuleVars language-pick test pins procedural_event.name and the
  rule.name alias to the language-matched value.
- languageFallback truth table covers all 10 (lang × tier) combos.

Build hygiene: go build/vet/test clean; bun run build clean.
2026-05-25 17:03:34 +02:00
mAi
5efb9f5098 Merge: t-paliad-281 — admin rules list 'undefined' proceeding name fix (m/paliad#113) 2026-05-25 16:54:39 +02:00
mAi
c6267e4e6d Merge: t-paliad-277 — submission party selector + import-from-project (mig 131) (m/paliad#109) 2026-05-25 16:53:50 +02:00
mAi
8e696487e0 Merge: t-paliad-279 — Verfahrensablauf form reorder, party-after-proceeding-type (m/paliad#111)
# Conflicts:
#	frontend/src/client/verfahrensablauf.ts
2026-05-25 16:53:32 +02:00
mAi
001542a3ce mAi: #113 - fix admin rules list 'undefined' proceeding name
ProceedingType TS interface in admin-rules-list.ts and admin-rules-edit.ts
declared `name_de` but the Go ProceedingType model serialises `db:"name"`
as JSON key `name` (DE is the primary on the wire). Result: `pt.name_de`
was undefined for every row, so `${pt.code} · ${pt.name_de}` produced the
literal "upc.apl.cost · undefined" in the list (and the same in proceeding
selects of the edit page).

Frontend-only fix:
- Rename the field to `name` to match the API contract.
- Guard the label builder: if the active-language name is missing, fall
  back to just the proceeding code rather than rendering "code · " (or
  worse, the original "code · undefined" string).

Other admin pages that fetch /api/proceeding-types-db (deadlines-new,
deadlines-detail, project-form, fristenrechner) already read `pt.name`
correctly, so the bug was scoped to these two files. TriggerEvent's
`name_de` field is real and stays untouched.
2026-05-25 16:52:07 +02:00
mAi
4fc3005db8 mAi: #109 - t-paliad-277 submission generator party selector + import-from-project
Multi-select party picker on the dedicated submission draft editor —
lawyer picks which of the project's parties to mention in this
specific submission. Adds the t-paliad-277 variable-bag multi-party
shape ({{parties.claimants}}, {{parties.claimant.0.name}}) while
keeping the legacy flat aliases ({{parties.claimant.name}}) for every
existing .docx template authored before the rename.

Surfaces an explicit "Aus Projekt importieren" button + last-imported
timestamp at the top of the variable sidebar so the lawyer can re-pull
project-derived variables (project.*, parties.*, deadline.*,
procedural_event.*, rule.*) when the project data drifts away from the
saved draft overrides. firm.*, today.*, user.* overrides survive the
import — those values aren't sourced from the project record.

Schema: mig 131 adds two columns to paliad.submission_drafts:
  - selected_parties uuid[] DEFAULT '{}'::uuid[]
    Empty = include every party (legacy default).
    Non-empty = restrict to the subset, grouped by role at substitution.
  - last_imported_at timestamptz NULL
    Bumped each "Aus Projekt importieren" click; surfaced in UI.

Backend:
  - SubmissionVarsContext gains SelectedParties — filterPartiesBySelection
    restricts the resolved bag before role bucketing.
  - addPartyVars emits THREE coexisting forms per role: comma-joined
    (parties.claimants), indexed (parties.claimant.0.name), and flat
    legacy (parties.claimant.name → first selected claimant). Flat
    aliases are kept forever per the issue's backward-compat contract.
  - SubmissionDraftService.ImportFromProject strips overrides for
    project-derived prefixes and bumps last_imported_at; rejects
    project-less drafts (nothing to import from).
  - New endpoint POST /api/submission-drafts/{id}/import-from-project.
  - DraftPatch + PATCH handlers accept selected_parties.
  - submissionDraftView now ships available_parties so the editor can
    render the picker without an extra round-trip.

Frontend:
  - submission-draft.tsx: new import-row + parties block in the sidebar.
  - client/submission-draft.ts: paintImportRow / paintPartyPicker /
    onPartySelectionChange / onImportFromProject; group parties by
    role bucket (claimant / defendant / other) with DE+EN role-string
    matching to mirror the backend bucketing.
  - 3 new i18n keys (DE+EN): import.button, parties.title, parties.hint.
  - CSS for the picker + import row in global.css.

Tests: 6 new unit tests in submission_vars_parties_test.go covering
the multi-party bag emission, German role-string bucketing, flat-alias
first-of-role resolution, empty-selection-means-all default, non-empty
restriction, and the isProjectDerivedKey policy that powers the
import path.

Build hygiene: go build/vet clean; go test -short ./internal/... pass;
bun run build clean (2876 i18n keys, scan clean).
2026-05-25 16:51:35 +02:00
mAi
a6d0acbcb4 mAi: #111 - t-paliad-279 — Verfahrensablauf form reorder + project auto-fill chip
Reorder Verfahrensablauf 'Browse a proceeding' so the user-input flow
matches the importance hierarchy: proceeding-type → side → appellant →
date / court / flags. Side was previously below the date input; it is
the most-defining input after proceeding-type, so it belongs above.

- frontend/src/verfahrensablauf.tsx: move .verfahrensablauf-perspective
  block above .date-input-group inside step-2. Wrap the side radio
  cluster in #side-radio-cluster and add a sibling #side-chip (hidden by
  default) that the client swaps in when a project pre-fills the side.
  Add a 1px divider between perspective and date-input groups. Update
  step-2 heading from "Ausgangsdatum eingeben" → "Perspektive und Datum"
  to honestly describe both controls now under the heading.

- frontend/src/client/verfahrensablauf.ts: read ?project=<id> on init,
  fetch /api/projects/<id>, map our_side onto the side axis (mirrors
  fristenrechner.ts ourSideToPerspective: claimant/applicant/appellant
  → claimant, defendant/respondent → defendant, else null) and render
  the side row as a read-only chip + "Andere Seite wählen" override
  link. The chip respects ?side= as an explicit user pick — URL wins
  over project auto-fill, same precedence as fristenrechner. Override
  swaps back to the radio cluster and drops ?project= from the URL.
  Side-chip label is language-aware via onLangChange.

- frontend/src/styles/global.css: .verfahrensablauf-step2-divider
  (1px hr between perspective and date blocks); .side-chip / -tag /
  -value / -override styles mirror .proceeding-summary's chip look so
  the two read as the same visual family.

- frontend/src/client/i18n.ts + i18n-keys.ts: 3 new keys
  (deadlines.step2.perspective, deadlines.side.from_project,
  deadlines.side.override) in DE + EN.

URL state stays backward-compatible: ?side= and ?appellant= survive
the reorder unchanged. Adding ?project= opts in to auto-fill; without
it the page behaves identically to before.

No backend / projection logic change.
2026-05-25 16:51:27 +02:00
mAi
96eab90044 Merge: t-paliad-280 — search input icon-text padding fix (m/paliad#112) 2026-05-25 16:51:26 +02:00
mAi
5348cb548f mAi: #112 - fix Fristenrechner Akte-picker icon overlap
The Akte-picker (Step 1) wraps its magnifying-glass icon + input in a
flexbox row (`.fristen-step1-search-row`) with `gap: 0.5rem`, expecting
the icon to participate in the flex layout. But the shared
`.fristen-search-icon` rule (used by the B2 search input) sets
`position: absolute; left: 0.875rem;` — and the step1-scoped override
only tweaked color + flex-shrink without resetting `position`.

Result: the icon was absolutely-positioned out of the flex flow and
overlapped the input text (since `.fristen-akte-search` has no
padding-left). Resetting `position: static` for the step1 context lets
flexbox + gap handle the spacing naturally — same pattern as
`.fristen-row-search-panel-input-wrap`, which already works.

Audited other search inputs with leading magnifying-glass icons:

- `.glossar-search` (Glossary, Courts, Links, Team, AdminTeam,
  AdminEventTypes) — wrap `.glossar-search-wrap` is `position: relative`,
  input has `padding: 0.65rem 4.5rem 0.65rem 2.5rem`. Fine.
- `.projects-search-input` (/projects index) — wrap is
  `position: relative`, input has `padding: 0.5rem 0.75rem 0.5rem 2.4rem`.
  Fine.
- `.fristen-search-input` (Fristenrechner B2) — wrap `.fristen-search-row`
  is `position: relative`, input has
  `padding: 0.75rem 2.5rem 0.75rem 2.6rem`. Fine.
- `.fristen-row-search-panel-input` (Fristenrechner row-search panel) —
  pure flex layout with `gap`, icon non-positioned. Fine.
- `.sidebar-search-input` (global sidebar search) — pure flex layout.
  Fine.
- Other search inputs (`event-search-input`, `event-type-search`,
  `submissions-new-search`, submissions index) have no leading icon.
  N/A.
2026-05-25 16:50:05 +02:00
mAi
b1340e2be4 Merge: t-paliad-278 — date-range picker 3-column layout Past/NOW/Future (m/paliad#110) 2026-05-25 16:46:59 +02:00
mAi
1292aa575d Merge: t-paliad-265 — per-event-card choices Slice A+B (popover + CCR + projection engine, mig 129) (m/paliad#96) 2026-05-25 16:46:15 +02:00
mAi
87c200a47e feat(t-paliad-265): caret + popover + chip on Verfahrensablauf cards
m/paliad#96 — frontend wiring of the per-event-card choice flow on
both consumer surfaces.

Shared rendering core (verfahrensablauf-core.ts):
- CalculatedDeadline gains choicesOffered + appellantContext (mirror
  the new server fields).
- deadlineCardHtml emits a ▾ caret next to the date when a rule
  carries a non-empty choicesOffered, plus an inert chip span next to
  the title that the popover module rehydrates after every render.
- bucketDeadlinesIntoColumns prefers appellantContext over the
  page-level appellant for "both" rows when the per-card context is
  set to claimant or defendant. "both" / "none" / "" all fall back to
  the existing collapse logic. New test cases cover all three paths.
- CalcParams + calculateDeadlines pass projectId / perCardChoices
  through to the backend.

New module (client/views/event-card-choices.ts):
- attachEventCardChoices wires a delegated click handler on the
  result container; the caret opens a body-anchored popover with one
  block per choice-kind the rule offers (appellant: 4 radio-style
  buttons; include_ccr + skip: 2-way toggle).
- Active picks render as small chips on the card title; reseedChips()
  repaints them after every renderResults() innerHTML rewrite.
- Skipped rows fade to 55% opacity via the timeline-item--skipped
  class.

Page wiring:
- /tools/verfahrensablauf (unbound): commits mutate an in-memory list
  + the ?event_choices= URL param, then schedule a recalc. Shareable
  via link, no persistence — same idiom as ?side= / ?appellant=.
- /tools/fristenrechner (project-bound): commits POST/DELETE to
  /api/projects/{id}/event-choices. The next calculate() call sends
  projectId so the server folds the persisted choices in.

i18n: 17 new keys under choices.* (DE primary + EN secondary). Caret
title, appellant/include_ccr/skip block titles + value labels, chip
labels, reset action, commit error toast.

CSS: caret, popover, options, chip parts, skipped-row fade.

Tests: 3 new bucketer cases covering AppellantContext propagation
(157 frontend tests pass).
2026-05-25 16:45:39 +02:00
mAi
4f910e31ea mAi: #110 - t-paliad-278 — 3-column date-range picker (Past/NOW/Future, closeness-to-NOW sort)
Restructures atlas's #79 horizontal row into 3 vertical columns: Past
(left), NOW (middle), Future (right). Each column sorts by closeness
to NOW (closest at top, farthest at bottom) — the picker now reads as
a spatial map of time around the current moment instead of a flat
horizontal fan.

Layout

  Vergangenheit          ⌖              Zukunft
  Letzte 7 Tage          Heute          Nächste 7 Tage
  Letzte 30 Tage         Alles          Nächste 30 Tage
  Letzte 90 Tage                        Nächste 90 Tage
  Ganze Vergangenheit                   Ganze Zukunft

Changes

- date-range-picker.ts — renderPanel builds .date-range-grid with
  three vertical .date-range-col children. Past column iterates
  PAST_HORIZONS reversed (past_1d → past_all top-to-bottom). NOW
  column hosts next_1d ("Heute") + any ("Alles") plus a ⌖ glyph
  header. Future column iterates NEXT_HORIZONS minus next_1d (which
  moved to NOW). Legacy "all" horizon still lights up the Alles chip
  for saved-Custom-View back-compat.
- global.css — replace .date-range-row/.date-range-fan/.date-range-
  center{,-btn,-glyph,-label} with .date-range-grid + .date-range-col
  + .date-range-col-heading. Chips stretch to 100% column width for a
  clean vertical stack. Panel widened from 32rem to 34rem so "Ganze
  Vergangenheit" never wraps. Mobile (max-width 540px) collapses the
  grid to a single column, preserving in-column sort.
- i18n.ts — next_1d label fixed from "Morgen"/"Tomorrow" to "Heute"/
  "Today". next_1d's bounds are [today, tomorrow) = single-day today,
  so the prior label was semantically wrong; renaming aligns the
  label with the bounds and matches m's "Heute" spec for the NOW
  column.
- axes.ts — DEFAULT_TIME_PRESETS updated to match m's spec (4 past +
  Heute + Alles + 4 future + custom). projects-detail.ts continues
  to override via timePresets for its past-only Verlauf surface.

12 horizon values in the union remain unchanged — PAST_HORIZONS /
NEXT_HORIZONS registries and parseURL still accept past_1d / past_14d
/ next_14d for back-compat with saved URLs; the default picker UI
just no longer surfaces chips for them. Surfaces that want the
finer granularity can opt back in via timePresets.

Verification

- bun test src/client/date-range-picker-pure.test.ts — 38 pass
- bun run build — i18n + branding + bundle clean
- go build ./... — clean
- go test ./internal/... — pass
2026-05-25 16:45:30 +02:00
mAi
bf60fc1400 feat(t-paliad-265): projection engine + HTTP handlers for per-card choices
m/paliad#96 — slice A engine + slice B engine wired together (per
m's Q4 bundling decision in §11 of the design doc).

Engine (internal/services/fristenrechner.go):
- CalcOptions gains PerCardAppellant map, SkipRules set, IncludeCCRFor
  set. All three keyed by paliad.deadline_rules.submission_code (same
  key AnchorOverrides uses).
- UIDeadline gains AppellantContext (per-decision pick that propagates
  to descendants via parent_id chain) + ChoicesOffered (passes the
  jsonb through to the frontend so the caret renders).
- Calculate honours all three:
  * IncludeCCRFor non-empty → append with_ccr to flag set before gate
    evaluation (v1 simplification documented in CalcOptions comment;
    correct for single-CCR-entry-point proceedings).
  * SkipRules suppression via submission_code match AND parent_id
    cascade (descendants suppress too — one-pass walk in sequence_order).
  * AppellantContext: each rule with its own per-card pick stamps its
    UUID; descendants inherit via parent_id lookup; "" = no override.

HTTP:
- /api/projects/{id}/event-choices GET / PUT / DELETE — full CRUD
  with visibility gate, audit-logged via paliad.system_audit_log.
- POST /api/tools/fristenrechner accepts either projectId (server
  pulls choices from project_event_choices) OR inline perCardChoices
  (unbound /tools/verfahrensablauf surface). Inline wins when both.

Services wiring:
- EventChoiceService instantiated in cmd/server/main.go; threaded into
  handlers.dbServices.eventChoice.
2026-05-25 16:45:21 +02:00
mAi
dc47ea7f43 feat(t-paliad-265): migration 129 + EventChoiceService (Slice A foundation)
m/paliad#96 — per-event-card optional choices on the Verfahrensablauf
timeline. This commit lands the schema + service layer.

Migration 129:
- paliad.project_event_choices table (project_id, submission_code,
  choice_kind ∈ {appellant, include_ccr, skip}, choice_value) with
  UNIQUE(project_id, submission_code, choice_kind) for idempotent
  re-pick, RLS via paliad.can_see_project.
- paliad.deadline_rules.choices_offered jsonb — opt-in declaration of
  which choice-kinds each rule offers. Seeded for every decision rule
  (appellant), every priority='optional' rule (skip), and the two
  Klageerwiderung rules (upc.inf.cfi.sod + de.inf.lg.erwidg) with
  include_ccr.

Live verification before authoring:
- rule_code is NULL on every decision row → submission_code is the
  join key (matches AnchorOverrides plumbing in fristenrechner.go).
- upc.inf.cfi.sod is the UPC Klageerwiderung, not upc.inf.cfi.def
  (rejected the design doc's first guess; SELECT name ILIKE
  'Klageerwiderung' confirmed).

Go service:
- models.ProjectEventChoice + DeadlineRule.ChoicesOffered.
- EventChoiceService: ListForProject / Upsert (with audit-log row to
  paliad.system_audit_log) / Delete. Pure-helper ToCalcOptionsAddendum
  + per-kind value validation + unit tests.

Design: docs/design-event-card-choices-2026-05-25.md §3 + §6.
2026-05-25 16:45:07 +02:00