Commit Graph

462 Commits

Author SHA1 Message Date
mAi
a75731a902 Merge: t-paliad-302 — Verfahrensablauf duration indicator (hover + toggle, +3 lp.TimelineEntry fields) (m/paliad#133)
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 15:45:15 +02:00
mAi
3097df3918 mAi: #133 — Verfahrensablauf duration affordance (hover + toggle)
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-302 / m/paliad#133. Surface each event card's rule duration
("2 Mo. nach") on /tools/verfahrensablauf — by default as a hover
tooltip on the date span, and optionally inline via a new
"Dauern anzeigen" header toggle (localStorage key
paliad.verfahrensablauf.durations-show).

The issue scoped this as pure-frontend on the assumption that the
duration fields were already on the /api/tools/fristenrechner payload.
They were not: lp.TimelineEntry exposed only the computed dueDate, not
the rule's (duration_value, duration_unit, timing) tuple. Added these
as three additive optional fields and populated them in both engine
emission sites (Calculate + CalculateByTriggerEvent) from the rule
row directly. Source values are the base rule fields, not the
post-alt-swap arithmetic — the tooltip reads as a property of the
rule rather than a recap of which branch fired.

Frontend wiring:
- formatDurationLabel() in verfahrensablauf-core builds the
  "<value> <unit> <timing>" string from the existing
  deadlines.event.unit.<unit>.{one,many} + deadlines.event.timing.*
  i18n keys, reused from /tools/fristenrechner's event-mode renderer.
- deadlineCardHtml attaches the label as title= on the date span
  (hover, default) and, when CardOpts.showDurations is on, emits an
  inline <span class="timeline-duration"> in the meta row.
- Court-set / zero-duration rules (trigger event, hearings) skip the
  affordance — durationValue <= 0 short-circuits in
  formatDurationLabel.
- Toggle persisted in localStorage under
  paliad.verfahrensablauf.durations-show, default off; sits next to
  the existing "Hinweise anzeigen" toggle.

bun run build clean, go test ./pkg/litigationplanner/... and
./internal/... clean, bun test src/client/views clean (89/89).
2026-05-26 15:43:30 +02:00
mAi
9da4715137 feat(litigationplanner): Berufung tile UX — collapse side selectors + appeal-target trigger label (t-paliad-301, m/paliad#132)
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
Two bugs from the Slice B1 Berufung rollout, one fix surface:

Bug A — duplicate side selectors collapse into ONE proactive-side
picker with per-proceeding role labels. The Verfahrensablauf used to
show both ?side= (Klägerseite/Beklagtenseite) AND ?appellant= (same
labels in case-form) on the Berufung tile. Now: one side picker, with
labels that swap to Berufungskläger/Berufungsbeklagter on the unified
upc.apl.unified tile (and Antragsteller/Antragsgegner Nichtigkeit on
upc.rev.cfi, Einsprechende(r)/Patentinhaber(in) on epa.opp.*).

Bug B — 'Auslösendes Ereignis' label derives from appeal_target on
the unified Berufung tile (5 target-specific strings) instead of the
proceeding's own trigger_event_label. Endentscheidung (R.118) /
Kostenentscheidung / Anordnung / Entscheidung im
Schadensbemessungsverfahren / Anordnung der Bucheinsicht.

Migration 137 (additive, no triggers on proceeding_types — verified
via mcp__supabase__execute_sql before drafting; no updated_at on the
table — lesson from mig 134 HOTFIX 3; no audit_reason setup needed):
  - ADD COLUMN role_proactive_label_de  (text NULL)
  - ADD COLUMN role_proactive_label_en  (text NULL)
  - ADD COLUMN role_reactive_label_de   (text NULL)
  - ADD COLUMN role_reactive_label_en   (text NULL)
  - Audit-first DO block lists the rows the UPDATE will touch.
  - Backfill 4 proceedings (upc.apl.unified + upc.rev.cfi +
    epa.opp.opd + epa.opp.boa); every other proceeding stays NULL
    and the renderer falls back to default labels.
  - Down drops the 4 columns.

Package additions (pkg/litigationplanner):
  - ProceedingType gains 4 *string fields (RoleProactive/Reactive
    LabelDE/EN) — db tags match the new columns; existing scans pick
    them up via the proceedingTypeColumns extension.
  - TriggerEventLabelForAppealTarget(target, lang) — Go-side map of
    the 5 appeal-target slugs to their DE/EN trigger-event labels.
    Empty result on unknown target signals "fall back to proceeding's
    own trigger_event_label".
  - Engine override: when CalcOptions.AppealTarget is set, the
    resulting Timeline.TriggerEventLabel/EN are replaced from the
    per-target map.

Frontend:
  - Removed #appellant-row div (was a separate 3-radio selector
    duplicating side).
  - Dropped ?appellant= URL state + the change handler + the init
    readback. The engine still consumes "appellant" — sourced from
    currentSide for role-swap proceedings; null otherwise.
  - applyRoleLabels(proceedingType) swaps the side-row radio labels
    from a hardcoded ROLE_LABELS map mirroring mig 137's backfill.
    Falls back to deadlines.side.claimant/defendant i18n keys for
    proceedings without overrides.
  - syncTriggerEventLabel reads data.triggerEventLabel from the calc
    response — which the engine override now sets per appeal_target,
    so no client-side mapping needed.
  - i18n cleanup: removed orphan deadlines.appellant.* keys (label /
    claimant / defendant / none) in both DE + EN.

Tests:
  - pkg/litigationplanner/appeal_target_label_test.go pins the 5×2
    label matrix + a coverage test that fails if a new entry in
    AppealTargets is added without populating the label switch.

Acceptance:
  - go build + go test all green (incl. new lp test).
  - bun run build clean (i18n codegen drops 4 keys, regenerates).
  - Live-DB audit before drafting confirmed: 4 target columns don't
    exist on proceeding_types, zero triggers on the table, exact
    column inventory matches the design.
2026-05-26 15:37:10 +02:00
mAi
e2d75c391d fix(litigationplanner): rename upc.apl → upc.apl.unified (HOTFIX, t-paliad-299, m/paliad#130)
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
mig 134 was inserting code='upc.apl' (2 segments) into paliad.proceeding_types,
which carries paliad_proceeding_code_shape CHECK requiring 3 dot-segments OR
'^_archived_'. Every container restart hit the constraint, rolled the migration
TXN back, and crash-looped paliad.de.

Rename the unified Berufung code to 'upc.apl.unified' (3 segments, satisfies the
constraint, preserves design intent). The pre-existing constraint is a useful
jurisdiction.category.specific invariant — keep it, fix the new row.

Touched only string literals:
- mig 134 up.sql + down.sql (insert, lookups, post-checks)
- frontend/src/verfahrensablauf.tsx (UPC_TYPES code + i18nKey)
- frontend/src/client/verfahrensablauf.ts (APPELLANT_AXIS + APPEAL_TARGET sets)
- frontend/src/client/i18n.ts (DE + EN translation rows)
- frontend/src/i18n-keys.ts (auto-regen via bun build)
- internal/services/lookup_events_test.go (anchor-row assertion)

Verified: `grep -rn "'upc\.apl'\|\"upc\.apl\""` returns zero hits.
go build, bun run build, go test ./... all green.
2026-05-26 15:09:12 +02:00
mAi
07acf7b4a2 feat(litigationplanner): Berufung unification — one upc.apl + 5 appeal_target chips (Slice B1, m/paliad#124 §18.1)
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
Collapses the 3 UPC appeal proceeding_types (upc.apl.merits 7 rules,
upc.apl.cost 2, upc.apl.order 7 = 16 total across 3 codes) into ONE
unified upc.apl proceeding type + a per-rule applies_to_target[]
discriminator. The verfahrensablauf picker now shows one "Berufung"
tile; after picking it, the user selects which decision the appeal is
directed AT via a 5-chip group (Endentscheidung / Kostenentscheidung /
Anordnung / Schadensbemessung / Bucheinsicht) and the engine filters
rules whose applies_to_target contains the picked slug.

m's 2026-05-26 decision: Schadensbemessung-as-appeal is a NEW first-
class target with its OWN rule set (no shared inheritance from
merits). The 5 enum values are all defined + addressable; for now
schadensbemessung and bucheinsicht return empty timelines until rules
are seeded in a follow-up slice (likely via /admin/rules or pairing
with t-paliad-193 orphan-concept-seed).

Migration 134 (additive only):
  - ADD proceeding_types.appeal_target text (CHECK on 5 slugs OR NULL)
  - ADD deadline_rules.applies_to_target text[] (CHECK each element
    in the 5 slugs)
  - INSERT the unified upc.apl row (inherits sort/color from
    upc.apl.merits)
  - Audit-first RAISE NOTICE pass listing every row about to be
    touched + a post-migration sanity check
  - Reassign rule rows: merits → applies_to_target={endentscheidung},
    cost → {kostenentscheidung}, order → {anordnung}
  - Archive (is_active=false, NOT DELETE) the 3 old proceeding_types
    so historical FKs stay intact
  - Down migration restores is_active=true on the 3 old types, points
    rules back by their applies_to_target stamp, drops the unified
    row, drops both columns. Safe.

Package additions (pkg/litigationplanner):
  - AppealTarget* constants + AppealTargets[] ordered list +
    IsValidAppealTarget(s) predicate (silent no-op on unknown slugs
    so a stale frontend chip doesn't break the render)
  - ProceedingType.AppealTarget *string field (top-level marker;
    NULL on non-appeal proceedings)
  - Rule.AppliesToTarget pq.StringArray field (per-row applies-to set)
  - CalcOptions.AppealTarget string (engine filter — when set,
    keeps only rules whose AppliesToTarget contains the slug)

Engine filter runs after ApplyRuleOverrides but before the rule walk
so the existing condition_expr / spawn / appellant-context machinery
operates on the filtered subset transparently.

paliad-side wiring:
  - deadline_rule_service.go: ruleColumns + proceedingTypeColumns
    extended to scan the new columns
  - handlers/fristenrechner.go: AppealTarget JSON field on the
    request payload, threaded into CalcOptions

Frontend (verfahrensablauf surface only):
  - Single "Berufung" tile replaces the 3 separate Berufung tiles
  - New 5-chip appeal-target row, shown only when upc.apl is picked
  - URL state ?target=<slug>; default endentscheidung when none set
  - APPELLANT_AXIS_PROCEEDINGS updated: upc.apl.* (3 entries) →
    upc.apl (1 entry)
  - i18n keys (DE + EN) for the new tile + the 5 chip labels +
    the "Worauf richtet sich die Berufung?" / "Appeal against:" prompt
  - calculateDeadlines threads appealTarget through to the API

Acceptance:
  - go build clean, go test all green (existing test suite — no new
    tests on the engine filter as a follow-up; the migration's
    sanity-check DO block guards the rule-reassignment count)
  - Live audit before drafting confirmed: 3 active UPC appeal
    proceeding_types, 16 rules total, primary_party already conforms
    to 4-value vocab on all proceeding-bound rules
2026-05-26 13:49:03 +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
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
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
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
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
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
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
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
d4df81e374 mAi: #106 - t-paliad-274 — bidirectional draft editor link + click-field-highlights
Extension of #92 (m/paliad/issues/106). Two related polish fixes for the
submission draft editor's preview ↔ sidebar wiring.

Concern A — link persists after fill (regression coverage + UX visibility)

  Audited the Go renderer: substituteInTextNodes / substituteAcrossRuns
  already pass both filled and missing values through htmlPreviewWrapper,
  so the <span class="draft-var" data-var="…"> wrapping is present for
  every substituted placeholder regardless of source (resolved bag,
  lawyer override, missing marker). What looked broken to m was a
  visibility problem: the always-on rgba(198, 244, 28, 0.12) tint is
  imperceptible against the serif preview prose, so a filled value
  reads as plain text and the user concludes "the link is gone".

  Added TestRenderHTML_WrapsOverriddenValueSameAsResolved that pins the
  invariant explicitly — an override (project.case_number = "UPC_CFI_
  42/2026") and a resolved value (firm.name = "HLC") both end up in
  matching draft-var spans. Locks future refactors out of dropping the
  wrap on either path.

  CSS rewrite per m's "prose stays clean when not interacting" guidance
  (issue body): drop the always-on background; on hover of a
  --has-input span, layer a dotted-underline + brighter lime tint so
  the click affordance reveals itself. Missing markers carry their own
  [KEIN WERT: …] / [NO VALUE: …] gap-text and don't need extra visual.

Concern B — sidebar-field-focus → preview-occurrence highlight (new)

  Reverse direction of the click-to-jump from #92. focusin on any
  .submission-draft-var-input applies .draft-var--active to every
  matching span in the preview; focusout (or focus shift via Tab)
  clears them. Sticky-while-focused, not a one-shot flash — the lawyer
  can scan "where does this variable land in my prose?" while the
  field stays focused.

  New CSS class .draft-var--active uses a brighter lime + box-shadow
  ring so all occurrences pop at once. Handlers are wired in
  paintVariables and re-applied at the end of both paintVariables AND
  paintPreview because:
    - paintVariables runs after autosave and re-creates inputs via
      innerHTML, so the focusin listener attached to the old input is
      gone; restoreVarFocus puts focus back programmatically without
      firing focusin again. We re-apply explicitly to bridge.
    - paintPreview blows away the preview HTML on every autosave, so
      any prior --active class is gone too. Re-apply based on the
      currently-focused sidebar input.

Files

  internal/services/submission_merge_test.go — new regression test
  frontend/src/client/submission-draft.ts    — focus handlers + re-apply
  frontend/src/styles/global.css             — draft-var rewrite, --active

Hard rules

  - .docx export path unchanged (Render passes nil wrap, covered by
    existing TestRender_DocxOutputUnchangedByPreviewWrap).
  - Both directions survive autosave-driven preview re-renders (see
    paintPreview re-apply + paintVariables re-apply).
  - go build ./... && go test ./internal/... && bun run build all clean.
2026-05-25 16:32:45 +02:00
mAi
8be7af7cd6 Merge: t-paliad-262 Slice A — procedural-events prose-only rename + {{rule.X}}↔{{procedural_event.X}} bidirectional aliases (m/paliad#93) 2026-05-25 16:03:42 +02:00
mAi
d52995a4d6 feat(procedural-events): t-paliad-262 Slice A — prose-only rename (m/paliad#93)
Renames the procedural-event surface of paliad.deadline_rules from
"rule" wording to "procedural event" / "Verfahrensschritt" wording.
No DB change, no API change, no Go-type rename. Fully reversible.

m's locks via head (2026-05-25):
- Q1=C: cosmetic now, structural rework (Slice B) as planned t-paliad-273.
- Q2: umbrella term = procedural event / Verfahrensschritt.
- Q7: legacy {{rule.X}} placeholder aliases kept forever (@deprecated).
- Q9: Slice B filed as on-hold task immediately.

Changes:
- internal/services/submission_vars.go: emit procedural_event.* keys
  alongside legacy rule.* keys with identical values. Package + function
  comments updated. Function name kept (addRuleVars) to avoid coupling
  Slice A to the Go-type rename which is Slice B (B.5).
- internal/services/submission_vars_aliases_test.go (new): regression
  test asserts (a) every (canonical, legacy) key pair resolves to the
  same string for both DE and EN; (b) NULL source columns still emit
  both keys with "". Removing either guard surfaces here.
- frontend/src/client/submission-draft.ts: placeholder catalog now
  shows canonical procedural_event.* labels first; legacy rule.*
  entries kept as "(legacy)"-marked aliases.
- frontend/src/client/i18n.ts: admin labels updated in place
  ("Regeln verwalten" → "Verfahrensschritte verwalten", etc.) under
  existing admin.rules.* keys; canonical admin.procedural_events.*
  keys added with identical values so .tsx files can rebind in Slice B.
- frontend/src/i18n-keys.ts: auto-regenerated by build pipeline.

Design doc: docs/design-procedural-events-model-2026-05-25.md (shipped
on the inventor branch mai/cronus/inventor-procedural).

Slice B (planned, on-hold): t-paliad-273.
2026-05-25 16:03:03 +02:00
mAi
f0c343c638 Merge: t-paliad-267 — Auto-rule resolved name on its own row in deadline form (m/paliad#98) 2026-05-25 16:02:41 +02:00
mAi
f11390d18b Merge: t-paliad-270 — i18n event.title.approval_decided + member_role_changed (m/paliad#101) 2026-05-25 16:01:56 +02:00
mAi
aa2f4aacc6 mAi: #98 - move Auto-rule resolved name to its own row
The Auto-mode resolved rule name was rendered as an inline-flex pill
that sat visually crammed next to the [Eigene Regel eingeben] toggle.
Promote .rule-mode-auto to a full-width block-level flex row (width:
100%, margin-top: 0.35rem) so it sits cleanly on its own line beneath
the toggle, and render the rule label via the canonical
formatRuleLabelHTML helper so the citation gets the muted-secondary
styling from rule-label.ts.

Applies to both /deadlines/new and /deadlines/:id edit form. Custom
mode (free-text input) is unaffected — the input already filled the
column.

Refs: m/paliad#98 (t-paliad-267), addendum to t-paliad-258 / m/paliad#89.
2026-05-25 16:01:15 +02:00
mAi
f72e8a7b85 mAi: #101 - add missing event.title.approval_decided + member_role_changed i18n
The FilterBar project_event_kind chip cluster (frontend/src/client/
filter-bar/axes.ts) renders one chip per KnownProjectEventKind via
tDyn(`event.title.${kind}`), which falls back to the raw key when the
catalog is missing the entry. Two kinds were uncovered:

  - approval_decided      → "Genehmigung entschieden" / "Approval decided"
  - member_role_changed   → "Teamrolle geändert"      / "Team role changed"

Both are now present in DE + EN. i18n-keys.ts regenerated by the build.

Audit of KnownProjectEventKinds (filter_spec.go:200) vs. the catalog —
all 18 kinds now have DE + EN labels.
2026-05-25 16:00:17 +02:00
mAi
013facb9db mAi: #100 - paliadin trigger: lift above bottom-nav at <=767px (t-paliad-269)
The Paliadin floating-button trigger was overlapping the PWA bottom-nav
on mobile because its lift rule was scoped to @media (max-width: 640px)
while .bottom-nav itself appears at @media (max-width: 767px). Phones in
landscape and small tablets between those breakpoints saw the desktop
bottom: 20px and got covered by the navbar.

Two changes:
- Widen the trigger lift breakpoint to 767px (matches .bottom-nav).
- Replace hardcoded 72px with calc(var(--bottom-nav-height) + 16px +
  env(safe-area-inset-bottom, 0px)) so the math tracks the navbar
  height variable already used elsewhere (e.g. dashboard-save-toast).

The drawer's full-screen rule (.paliadin-widget-drawer width: 100vw)
stays at <=640px — only the trigger lift moves.

Desktop layout (bottom: 20px) unchanged; widget open/close animation
unchanged.
2026-05-25 15:58:38 +02:00
mAi
247e9005db Merge: t-paliad-248 Slice A — symmetric date-range picker + filter-bar wiring (m/paliad#79)
# Conflicts:
#	frontend/src/client/filter-bar/axes.ts
2026-05-25 15:51:36 +02:00
mAi
bcfde73815 feat(inbox): t-paliad-249 Slice A frontend — inbox dispatch + UI axes (m/paliad#80)
The /inbox surface drops "Genehmigungen" framing in favour of "Inbox"
and renders the unified feed.

- shape-list.ts: factor renderApprovalRow out of renderApprovalList so
  it can be reused alongside renderProjectEventInboxRow inside the new
  renderInboxList (row_action="inbox"). Project_event rows show a
  compact stream layout with an Öffnen link pointing at the right
  project tab (deadlines / appointments / notes).
- filter-bar gets two new axes: unread_only (binary chip cluster) +
  inbox_focus (4-chip coarse cluster: Alles / Genehmigungen / +Termine
  / +Fristen). Both round-trip via url-codec; inbox_focus translates
  to (sources, project_event.event_types, approval_request.entity_types)
  at the bar's resolve step (applyInboxFocusOverlay).
- FilterSpec gains a top-level unread_only flag; the bar writes it
  when the user toggles the chip; the server overlays the cursor.
- /inbox header: new "Alles als gelesen markieren" button POSTs
  /api/inbox/mark-all-seen with up_to=<newest visible row> for
  race-safety against a second tab.
- INBOX_AXES adds project + project_event_kind as advanced override
  chips so power users can still narrow per kind.
- i18n: inbox.title.feed / inbox.heading.feed / inbox.action.mark_all_seen
  / inbox.action.open / inbox.empty.feed / views.bar.unread_only.*  /
  views.bar.inbox_focus.* (DE + EN).
- url-codec round-trip tests for the two new axes.
2026-05-25 15:49:54 +02:00
mAi
31d78526cf feat(date-range-picker): t-paliad-248 — symmetric picker + filter-bar wiring
Slice A complete. Builds on the additive backend constants (commit
34e3d71) by shipping the user-facing surface.

# Pure helpers (no DOM)

frontend/src/client/date-range-picker-pure.ts (190 LoC) — TimeSpec
shape, ALL_HORIZONS / PAST_HORIZONS / NEXT_HORIZONS registries,
horizonBounds (mirrors view_service.go), isValidHorizon, isValidISODate
(strict — rejects 2026-02-30 etc.), validateCustomRange, parseURL /
serializeURL (canonical ?horizon=...&horizon_from=...&horizon_to=...
with default-omission), isDefault.

frontend/src/client/date-range-picker-pure.test.ts (38 bun tests,
118 expect calls): registries, horizon bounds for all 14 values,
ISO-date validity rejects calendar-impossible dates, validateCustomRange
on every error branch, parseURL fallback to default, serializeURL
default-omission + key-override + custom-bounds, full round-trip.

# DOM mount

frontend/src/client/date-range-picker.ts (290 LoC) — mountDateRangePicker
returns {element, getValue, setValue, close, destroy}. Trigger button
in a .multi-anchor wrapper, popover panel reusing .multi-panel
positioning. Symmetric chip row: past fan (right-aligned) | ALLES
centre (target glyph U+2316) | next fan. 'Anpassen' chip toggles an
inline date-pair editor with Apply / Cancel + a live validation
message that surfaces only the meaningful 'inverted range' error
during typing (empty/format errors are visible via the disabled
Apply button). Outside-click + Esc close the popover, focus returns
to the trigger. setValue lets the host sync from URL changes.

# Filter-bar wiring

frontend/src/client/filter-bar/axes.ts:renderTimeAxis — the disabled
'Anpassen' stub (t-paliad-163 Phase 2 placeholder) is gone; the axis
mounts the picker instead. New default presets surface 6 chips +
ALLES centre + Anpassen, plus the per-surface timePresets override
filters down to whatever subset the surface declares. 'any' still
maps to BarState.time = undefined to keep the canonical URL short
and preserve the existing 'no overlay' semantics.

frontend/src/client/filter-bar/types.ts — TimeOverlay.horizon union
extended with next_1d / next_14d / next_all / past_1d / past_14d /
past_all.

frontend/src/client/filter-bar/url-codec.ts — parseHorizon accepts
the six new values; existing 9 values continue to round-trip.

frontend/src/client/filter-bar/url-codec.test.ts — round-trip
iteration extended to all 14 horizons.

frontend/src/client/views/types.ts — TimeHorizon TS mirror extended.

frontend/src/client/projects-detail.ts — horizonBounds covers the
six new values (open-ended for next_all/past_all so the upstream
filter treats nil bounds as 'no narrowing in that direction').

# i18n + retired legacy keys

frontend/src/client/i18n.ts — 30 new keys per language (date_range.*
namespace for the picker + 6 missing views.horizon.* labels for
existing dynamic-key composition in views.ts:317). Legacy
views.bar.time.* keys (10 per language) retired with a one-line
breadcrumb comment pointing at the date_range.* namespace.

frontend/src/i18n-keys.ts — regenerated by build.ts.

# CSS

frontend/src/styles/global.css — date-range-* class block (256 LoC).
Trigger button, popover panel, past/centre/next groups, custom-range
editor, mobile stack at <540px. Reuses --color-accent /
--color-accent-light / --color-bg-lime-tint / --color-border /
--color-text + .agenda-chip / .agenda-chip-active for chip styling
so every active state lights up with the same lime accent as every
other paliad filter chip — no new tokens, no fresh dark-mode
contrast risk (t-paliad-150 / fritz lesson held).

# Surfaces lit up by this single change

- /projects/:id Verlauf (filter-bar consumer)
- /views runtime
- /views/:id Custom-Views editor
- /inbox InboxFilterBar

All four pick up the picker on their next page load. Per-surface
presets (timePresets MountOpt) preserved exactly; Verlauf still
shows the past-only subset, /inbox the forward-leaning subset etc.
The custom chip that's been disabled-with-coming_soon since
t-paliad-163 now works.

# Tests + build hygiene

- go build ./... clean
- go test ./internal/services/ clean (filter_spec + new bounds test)
- bun test passes (150 tests, 8 files, 377 expect calls)
- bun run build clean (2848 i18n keys, data-i18n scan clean)

# What's NOT in this slice

- /agenda chip-row migration (Slice B).
- /admin/audit-log + /projects/:id/chart migration (Slice C).
- upckommentar-style range slicer for custom mode (Slice D, separate
  task).
2026-05-25 15:47:51 +02:00
mAi
51fca9383f Merge: t-paliad-246 — Backup Mode Slice A (on-demand admin org export, local disk, .zip bundle, mig 123) (m/paliad#77) 2026-05-25 15:29:48 +02:00
mAi
99c9d89daa feat(backups): t-paliad-246 — Backup Mode Slice A (on-demand admin org export)
m/paliad#77 Slice A. Folds the unbuilt t-paliad-214 Slice 3 (org async
export) into a new "Backup Mode" surface gated by adminGate.

m's calls (all 4 material picks per design §2):
- Storage: local disk PALIAD_EXPORT_DIR (LocalDiskStore only)
- Format: .zip bundle (xlsx + JSON + CSV + README) — no-lock-in preserved
- paliadin_turns + paliadin_aichat_conversation: EXCLUDE structurally
- Scheduler (Slice B): nightly 03:00 UTC, env-tunable

Wiring:
- mig 123 adds paliad.backups catalog table (kind/status/storage_uri/
  size/row_counts/warnings/error/deleted_at + admin-only RLS).
- ExportService.WriteOrg + orgSheetQueries enumerate 37 entity sheets
  + 12 ref sheets; REPEATABLE READ READ ONLY tx wraps the dump for
  snapshot consistency (design §3.3).
- writeBundle + runSheetQuery refactored to take a sqlx.QueryerContext
  so both *sqlx.DB (personal/project paths, unchanged) and *sqlx.Tx
  (org snapshot path) work.
- BackupRunner orchestrates: catalog INSERT → audit INSERT
  (event_type='backup_created') → WriteOrg → ArtifactStore.Put → patch
  catalog + audit on success/failure.
- ArtifactStore interface + LocalDiskStore impl (defense-in-depth key
  validation + URI-outside-dir guard).
- Sentinel actor for scheduled runs: actor_email='system@paliad',
  actor_id=NULL — no phantom user in paliad.users.
- Admin handlers POST /api/admin/backups/run + GET list/get/download
  behind adminGate(users, …); /admin/backups page + sidebar entry +
  bilingual i18n keys.
- BackupRunner only wired when PALIAD_EXPORT_DIR is set; routes return
  503 otherwise (same shape as requireDB).

Tests: 8 pure-function tests cover registry shape (no dups, paliadin
absent both as sheet name and SQL substring, ref__* sheets unscoped,
every sheet has ORDER BY) and LocalDiskStore (round-trip, bad-key
rejection, URI-traversal rejection, mkdir on construction).

go build ./... + go test ./internal/... clean. bun run build clean.

Slice B (BackupScheduler + retention cleanup) and Slice C (UI polish)
are separate follow-ups per head's instruction.
2026-05-25 15:28:37 +02:00
mAi
7e66da8def mAi: #92 - t-paliad-261 — submission-draft autosave focus + click-variable-in-preview jump
Two related editor polish fixes.

(A) Autosave-refresh focus preservation
  paintVariables() replaces every input via innerHTML, blowing away
  the focused-input reference and dropping the cursor mid-edit. Fix:
  capture the active variable input's data-var key + selectionStart/
  End/Direction before the repaint, restore on the new element after
  (by data-var lookup + setSelectionRange). Cursor stays put across
  autosave, rename, and reset cycles. Works for <input> and
  <textarea> via the shared selectionRange contract.

(B) Click variable in preview → jump to sidebar input
  Go renderer wraps every substituted placeholder value in the HTML
  preview with <span class="draft-var" data-var="key">…</span>.
  Implemented via a valueWrapperFn plumbed through
  substituteInDocumentXML → substituteInTextNodes /
  substituteAcrossRuns → replacePlaceholders. RenderHTML passes
  htmlPreviewWrapper which marks values with three PUA sentinels
  (U+E100/U+E101/U+E102) that emitTextWithDraftVars converts to the
  span pair inside docXMLToHTML. Missing-marker text is wrapped too
  so a clicked [KEIN WERT: foo] jumps to the empty field.

  Render() (.docx export) passes nil for wrap → output is byte-
  identical to pre-261. New test
  TestRender_DocxOutputUnchangedByPreviewWrap asserts the .docx never
  carries draft-var/data-var markup or PUA sentinels.

  Client wireDraftVars() adds .draft-var--has-input only to spans
  whose key resolves to a sidebar input — derived variables (e.g.
  today.iso) stay non-clickable. Click handler:
    scrollIntoView(smooth, center) → focus + select after 50ms →
    1.2s lime flash on the row.
  Keyboard accessible (Enter / Space) with role=button + aria-label.

CSS adds a subtle lime tint to every .draft-var so the user sees
what was substituted; --has-input layers cursor: pointer + brighter
hover background. Flash animation respects prefers-reduced-motion
via a steps(1, end) fallback.

Tests: TestRenderHTML_ExtractsParagraphsAndFormatting updated to
assert the new span wrap. New tests for missing-marker wrap +
.docx-path-untouched. Go + frontend builds clean.
2026-05-25 15:12:10 +02:00
mAi
ef21e43375 Merge: t-paliad-260 — submission-draft mobile layout (m/paliad#91) 2026-05-25 14:59:53 +02:00
mAi
4cb99fb627 mAi: #91 - t-paliad-260 — submission-draft mobile layout: drop sticky on sidebar at ≤900px
Approach A: stack vertically. At single-column widths the variable
editor was sticky + max-height: calc(100vh - 2rem), so it stayed
pinned at the top of the viewport while the user scrolled down to
read the preview, visually overlaying the preview pane.

Add a media-query override that switches the sidebar to position:
static, max-height: none, overflow-y: visible at the same ≤900px
breakpoint where the grid already collapses to one column. The
sidebar now reflows above the preview, takes its natural height,
and scrolls away as the user moves down — no overlay, no
horizontal scroll. Desktop (≥901px) layout unchanged: sidebar
keeps its sticky behavior side-by-side with the preview.

Verified at 375 / 414 / 768 / 1280 px in Playwright on the
populated editor body — same renderer serves both URL shapes
(/submissions/draft/{id} and
/projects/{id}/submissions/{code}/draft/{id}).
2026-05-25 14:58:21 +02:00
mAi
452ccdf127 Merge: t-paliad-258 — Deadline form Auto/Custom rule field + canonical rule-label display (m/paliad#89) 2026-05-25 14:56:18 +02:00
mAi
045accc6d9 mAi: #89 - deadline rule field binary Auto/Custom + canonical rule-label display
t-paliad-258. m's verdict on t-paliad-251's rule UI: "too many options"
(4 'Oral hearings' across courts, etc.). Replace the full deadline_rules
catalog dropdown + sort selector with a binary model and unify the rule
display contract across every surface that prints a rule label.

Binary Rule field on the deadline form
- Auto (default): rule_id is derived from the chosen Type. The resolved
  rule renders read-only as 'Auto | <Name · Citation>' next to the
  field. No catalog picker, no sort options.
- Custom: free-text input. Stored as deadlines.custom_rule_text (new
  nullable column, migration 122). Mutually exclusive with rule_id at
  the persistence boundary.
- Toggle link flips between modes. Re-toggling to Auto re-resolves from
  the current Type — no stale state.

Schema + service (additive)
- migration 122 adds paliad.deadlines.custom_rule_text (nullable).
  Existing rows: empty custom_rule_text + non-null rule_id = Auto-
  equivalent. Both NULL = "keine Regel" (consistent with today).
- models.Deadline.CustomRuleText + service SELECTs include the column.
- CreateDeadlineInput accepts custom_rule_text; the service drops it
  when rule_id is set (catalog wins; simple invariant at the boundary).
- UpdateDeadlineInput grows a {RuleSet, RuleID, CustomRuleText} triple.
  RuleSet=true is the discriminator so absent fields don't overwrite
  the row (PATCH semantics). RuleID and CustomRuleText are mutually
  exclusive in one request; service rejects "both set".
- EventListItem (the /api/events union) carries CustomRuleText so list
  surfaces can render it.

Frontend: deadlines-new
- Drop the rule <select>, the by_proceeding/by_court/alpha sort
  dropdown, the override-warning slot, and the collapsed-by-Regel Typ
  view. Strip the (Rule→Type) auto-fill machinery — direction is now
  one-way (Type → Auto-resolved Rule).
- Keep Type→Rule resolution: resolveAutoRuleForType picks the canonical
  rule by project's proceeding, then jurisdiction match, then first
  candidate. Same logic, just re-aimed at the read-only display.
- Standardtitel preserves the chain (event type → Auto rule label →
  Custom text → proceeding → fallback) so the recipe still produces a
  sensible title even when Custom is used.

Frontend: deadlines-detail
- Read-only display: catalog rule → Name · Citation, else
  custom_rule_text + Custom badge, else legacy rule_code, else "—".
- Edit mode: mirror the create form with the Auto/Custom toggle.
  enterEdit initialises the mode from the persisted deadline; Save
  PATCHes with rule_set:true + the chosen rule pointer.

Rule-label addendum (m's 14:31 follow-up)
- Canonical contract everywhere: Name primary, Citation muted secondary
  ("Notice of Appeal · UPC.RoP.220.1"). Custom rules render the text
  with a "Custom" pill.
- New frontend/src/client/rule-label.ts exports formatRuleLabel /
  formatRuleLabelHTML / formatCustomRuleLabelHTML — one helper per
  shape (plain text vs muted-citation HTML).
- Wired into: deadlines-new Auto display, deadlines-detail read +
  Standardtitel, events.ts ruleDisplay (REGEL column on /events),
  projects-detail.ts Fristen table, views/shape-list.ts generic
  rule column.
- Verfahrensablauf (views/verfahrensablauf-core.ts) already renders
  name + citation chip separately and matches the canonical pattern;
  no change needed. Schriftsätze table is column-shaped (name + code
  in distinct columns) and out of scope per the addendum.

CSS
- New .rule-mode-auto / .rule-mode-custom / .rule-label-* family.
- Drop the dead .rule-sort-select rule and the .event-type-collapsed*
  family (retired with the catalog dropdown).

i18n
- DE+EN. Remove 10 stale keys (rule.none, autofill, autofill_inline,
  mismatch, override, override_warn, sort.*). Add 6 (auto_no_match,
  auto_pick_type, custom_badge, custom_placeholder,
  mode.toggle_to_auto, mode.toggle_to_custom).

Build hygiene
- go build + go test ./internal/... clean.
- frontend bun build clean (2803 keys, scan clean).

Out of scope (per issue)
- Promoting Custom entries back to the catalog ("save as new rule").
- Filtering/searching custom_rule_text in deadline lists.
- Touching the event-type browse modal (Part 1 of #82 — that stays).

Files
- internal/db/migrations/122_deadlines_custom_rule_text.{up,down}.sql
- internal/models/models.go
- internal/services/deadline_service.go (Create+Update+SELECT)
- internal/services/event_service.go (union projection)
- frontend/src/client/rule-label.ts (new helper)
- frontend/src/client/deadlines-new.ts (rewrite)
- frontend/src/client/deadlines-detail.ts (Auto/Custom editor + display)
- frontend/src/client/events.ts (REGEL column)
- frontend/src/client/projects-detail.ts (Fristen table cell)
- frontend/src/client/views/shape-list.ts (generic rule column)
- frontend/src/client/i18n.ts + i18n-keys.ts (DE+EN delta)
- frontend/src/deadlines-new.tsx (strip dropdown+sort, add toggle)
- frontend/src/deadlines-detail.tsx (Auto/Custom edit slots)
- frontend/src/styles/global.css (rule-mode + rule-label families)
2026-05-25 14:54:51 +02:00
mAi
538c2d2da9 Merge: t-paliad-257 — Verfahrensablauf user-perspective column axis (Unsere Seite / Gericht / Gegnerseite) (m/paliad#88) 2026-05-25 14:34:38 +02:00