733d21c930f47f855e4fde9726f16d4c9e17cb08
282 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
| 9359e99a6b |
feat(handlers,frontend): Slice B.6 — admin URL rename /admin/rules → /admin/procedural-events with 301 redirects + .tsx i18n rebind (t-paliad-305 / m/paliad#93)
Closes the procedural-events rename loop opened by m/paliad#93. The admin surface now lives under its canonical URL; the legacy paths remain reachable for one deprecation cycle via 301 redirects so bookmarks, audit-log entries, and curl scripts keep working. * internal/handlers/handlers.go — - Registers the 12 canonical routes under /admin/procedural-events* (page paths and JSON API). Same handlers — just the new URL slot. - Registers the 12 legacy /admin/rules* routes as 301 redirects. * internal/handlers/admin_rules.go — - redirectToProceduralEvents(dst) — fixed-destination redirect for paths without an {id}. - redirectToProceduralEventEdit — page redirect carrying the {id}. - redirectToProceduralEventAPI(suffix) — JSON API redirect carrying {id} + optional suffix (/clone-as-draft, /publish, /archive, /restore, /audit, /preview). Query string is preserved on every redirect. - All three helpers add the IETF Deprecation header + a Link header pointing at the successor-version path. * frontend internal nav + URL strings — Sidebar.tsx, admin.tsx, admin-rules-list.tsx, admin-rules-edit.tsx, client/admin-rules-list.ts, client/admin-rules-edit.ts: every `/admin/rules*` reference flipped to `/admin/procedural-events*`. In-app navigation now hits the canonical paths directly without a redirect round-trip; external callers keep working via the 301s. * frontend .tsx i18n rebind — 9 admin .tsx i18n bindings rebound to the canonical `admin.procedural_events.*` keys that already exist as aliases in i18n.ts (per Slice A from t-paliad-262). Specifically: admin.rules.list.title → admin.procedural_events.list.title admin.rules.list.heading → admin.procedural_events.list.heading admin.rules.list.new → admin.procedural_events.list.new admin.rules.col.submission_code → admin.procedural_events.col.code admin.rules.edit.title → admin.procedural_events.edit.title admin.rules.edit.breadcrumb → admin.procedural_events.edit.breadcrumb admin.rules.edit.field.submission_code → admin.procedural_events.edit.field.code admin.rules.edit.field.event_type → admin.procedural_events.edit.field.event_kind admin.rules.edit.field.parent → admin.procedural_events.edit.field.parent The remaining ~142 admin.rules.* keys do NOT yet have procedural_events aliases. Migrating them is a follow-up slice — each needs a new alias entry in i18n.ts (DE + EN) before the .tsx reference can be flipped. The 9 keys touched here are the most visible (page titles + edit-page field labels) so the admin UI immediately reads as "Verfahrensschritte" everywhere. * frontend/src/client/i18n.ts header comment updated to reflect that the URL rename has shipped (Slice B.6 done) and to flag the remaining i18n-key migration as the next step. Scope (documented, paliadin authorised): - "go everything" applied: backend routes + frontend nav + .tsx rebind of the 9 keys whose canonical aliases exist. - Full migration of all 142 admin.rules.* keys deferred — would require seeding ~142 new alias entries in i18n.ts (DE + EN) plus another 142 .tsx rebinds. Out of scope for tonight; flag as follow-up `feat(i18n): finish admin.rules.* → admin.procedural_events.* alias migration`. - 12 legacy /admin/rules routes still hit a handler (the redirect helper) — they don't 404 yet. Once a deprecation window passes with no traffic on the old paths, a future slice can drop them outright. Build + vet clean. TestMigrations_NoDuplicateSlot passes. This concludes the m/paliad#93 procedural-events rename slice train (Slices A through B.6). curie stays parked persistently for any follow-up the deploy / monitor cycle surfaces. |
|||
| ee98db94fa |
feat(submissions): Composer Slice C — building blocks library (m/paliad#141)
Per the design at docs/design-submission-generator-v2-2026-05-26.md §8
and the Q2 / Q9 ratifications:
- Q2 (m, 2026-05-26): building blocks are plain text paste sources.
No building_block_id reference is stored on submission_sections.
- Q9 (m, 2026-05-26): four visibility tiers — private / team / firm
/ global.
Schema (mig 149):
- paliad.submission_building_blocks — library catalog. Columns: slug,
firm (NULL = cross-firm), section_key (binds to one section kind),
proceeding_family (NULL = any), title_de/_en + description_de/_en
+ content_md_de/_en, author_id, visibility (CHECK in 4-tier set),
is_published, created_at, updated_at, deleted_at (soft delete).
RLS: coarse-grained SELECT — every authenticated user sees
non-deleted non-private rows + own private rows. Tier-specific
predicate (private/team/firm/global) applied in Go-layer service so
semantics evolve without RLS migrations. Mutations admin-only (no
RLS write paths).
- paliad.submission_building_block_admin_versions — append-only
history per block, retention=20. Admin-side only; NOT referenced
from submission_sections (per Q2's plain-text-paste model). Exists
so accidental delete / overwrite are recoverable.
Backend:
- internal/services/submission_building_block_service.go (~510 LoC):
BuildingBlockService. ListVisible applies tier predicate at query
time (private = author_id match; firm = firm column NULL OR matches
branding.Name; team = author shares a project_team with caller via
paliad.project_teams self-join; global = open). ListAllForAdmin
drops the predicate. Create + Update + SoftDelete + RestoreVersion
all transactional; appendVersionTx writes one audit row +
GC-deletes anything past the retention=20 horizon in the same tx.
InsertIntoSection (the paste mechanic) clones content_md_<lang>
into the section row with a "\n\n" separator if section already has
content. NO building_block_id stamped per Q2.
- internal/handlers/submission_building_blocks.go (~480 LoC): nine
handlers split between the lawyer-facing picker (list, insert) and
the admin editor (list, get, create, update, delete, list-versions,
restore-version, page). buildingBlockUpdateInput uses presence-
tracking UnmarshalJSON for the four nullable fields (firm,
proceeding_family, description_de/_en) so PATCH can distinguish
"no change" from "set to null".
- Routes registered: lawyer-facing under /api/submission-building-blocks,
admin-gated under /api/admin/submission-building-blocks/* and
/admin/submission-building-blocks (page).
- Wiring: handlers.Services + dbServices + cmd/server/main.go all
gain SubmissionBuildingBlock. NewBuildingBlockService takes the
branding.Name firm hint for the visibility predicate.
Frontend:
- frontend/src/admin-submission-building-blocks.tsx (~85 LoC):
three-pane admin shell (list / editor / version log) registered
in build.ts.
- frontend/src/client/admin-submission-building-blocks.ts (~370
LoC): admin client — list paint, edit form (slug + firm +
section_key + proceeding_family + title/desc/content per lang +
visibility radio + is_published toggle), per-block version log
with restore button. Bilingual labels.
- frontend/src/client/submission-draft.ts: per-section "+ Baustein"
button on the Composer editor toolbar (Slice B substrate gets one
more affordance). openBlockPicker opens a modal filtered to the
section's section_key, 200ms-debounced search by free text against
title/description/content. Click a hit → POST insert-into-section
→ section row's content_md_<lang> gains the block's content
appended at the end (Q2's plain-text paste semantic, no lineage).
- ~240 LoC of CSS: modal overlay + picker rows with tier-colored
visibility chips + admin editor 3-pane grid + form rows + version
list.
- 12 new i18n keys × 2 langs (admin.building_blocks.*).
Tests:
- TestValidVisibility (8 cases including case-sensitivity + empty).
- TestAppendBlockContent (8 cases covering empty-existing / empty-
addition / whitespace-only / trailing newline collapse).
- TestBuildingBlockVisibilityConstants pins the 4 string literals
against drift (RLS predicate + DB CHECK depend on them).
Build hygiene: go build/vet/test -short clean; bun run build clean
(2906 i18n keys, data-i18n scan clean).
Hard rules per ratifications honoured:
- Q2: no building_block_id lineage on sections (paste is plain text).
- Q9: 4 visibility tiers (private/team/firm/global).
- NO behavior change for pre-Composer drafts (the picker just doesn't
show — section list is hidden for base_id NULL drafts).
- {{rule.X}} aliases preserved (block content goes through the same
v1 placeholder pass on export as section prose).
NOT in scope per Slice C brief:
- User-authored private blocks (Slice C ships admin curation only;
any-user create is a follow-up).
- Tier promotion review workflow (admin sets tier directly today).
- Per-section "where is this block used" reverse lookup (no lineage
to query).
- Slice D's rich-prose features (headings, lists, blockquote) still
Slice D's job; this Slice doesn't extend the MD walker.
t-paliad-315 Slice C
|
|||
| f963b0df34 |
feat(submissions): Composer Slice B — editable prose sections + anchor-spliced render (m/paliad#141)
The "Composer actually works" milestone per the design at
docs/design-submission-generator-v2-2026-05-26.md §12 Slice B. Builds on
Slice A's substrate (submission_bases, submission_sections, base_id on
drafts); no new migrations needed.
Backend additions:
- internal/services/submission_md.go (~240 LoC): Markdown → OOXML
walker. Per the head's Slice B brief, scope is paragraphs +
bold/italic + blank-line spacing. Placeholders pass through
unchanged for the v1 substitution pass. CRLF normalisation; nested
formatting (***bold-italic***); two delimiter forms (* and _);
XML-escaping for &/</>; explicit empty-paragraph emit so blank
lines round-trip. 12 unit tests.
- internal/services/submission_compose.go (~470 LoC): SubmissionComposer
service. Pipeline: ConvertDotmToDocx pre-pass → extract
word/document.xml → render each included section's content_md_<lang>
→ splice via {{#section:KEY}}/{{/section:KEY}} anchor pairs in
the body → strip anchors for excluded sections → append unanchored
sections before <w:sectPr> → repack zip → run v1 placeholder pass.
RE2-friendly anchor scanner walks markers in body-order and matches
open/close pairs with a stack (handles unbalanced anchors
defensively). 6 unit tests covering anchor-mode splice,
append-mode-no-anchors, excluded-section drop, placeholder
resolution, lang column pick, order_index ASC.
- internal/services/submission_section_service.go: SectionPatch +
Update method. Six optional fields (content_md_de/en, included,
label_de/en, order_index). Sentinel ErrSubmissionSectionNotFound on
RLS-filtered miss.
- internal/handlers/submission_sections.go (NEW, ~150 LoC):
PATCH /api/submission-drafts/{draft_id}/sections/{section_id}.
Owner-scoped via SubmissionDraftService.Get; section-belongs-to-draft
cross-check. 404 on both missing-draft and section-belongs-elsewhere
paths.
- internal/handlers/files.go: fetchComposerBaseBytes + composerBaseSlugMap
reuse the existing Gitea proxy cache for base .docx bytes. hlc-letterhead
→ existing firmSkeletonSubmissionSlug, neutral → existing
skeletonSubmissionSlug.
- internal/handlers/submission_drafts.go: exportSubmissionDraft helper
branches on draft.BaseID. When set AND base + bytes + sections all
resolve → Composer pipeline. Else v1 fallback render path stays.
Audit metadata jsonb gains "composer": true + "base_id" flag when
composer was used.
Wiring:
- handlers.Services gains SubmissionComposer.
- dbServices.submissionComposer wired from svc.SubmissionComposer.
- main.go instantiates NewSubmissionComposer with the existing
SubmissionRenderer (so the {{rule.X}} alias contract stays preserved
inside section content).
Frontend additions (~400 LoC):
- client/submission-draft.ts: paintSectionList rewritten to render a
contentEditable per included section with a per-section B/I
toolbar. Per-section autosave debounced 500ms; mousedown handlers on
toolbar buttons preserve editor focus mid-command. domToMarkdown
walks the contentEditable's DOM tree back to Markdown source-of-
truth (b/strong → **…**, i/em → *…*, div/p → paragraph break, br
→ newline). Updated state.view.sections in-place on PATCH success
without re-painting (avoids focus-stealing on every keystroke);
re-paints only on structural changes (included toggle, label edits,
order changes).
- client/submission-draft.ts: onSectionToggleIncluded hides/shows a
section via PATCH. flushSectionAutosave on blur force-flushes
pending edits so leaving an editor doesn't strand unsynced changes.
- styles/global.css: editor surface (contentEditable area with focus
ring + placeholder), toolbar buttons (B/I 1.8rem squares),
per-section "Hide"/"Include" toggle in the head row.
- Updated i18n hint copy: "Inhalt pro Abschnitt — Autosave nach
500ms. Letztes Layout in Word."
Templates regenerated on Gitea:
- _skeleton.docx → composer-mode body (anchors only): blob SHA
ac0cdeaf49f7cd417ec143e2319ffbb02ec65644.
- _firm-skeleton.docx → composer-mode body (anchors only, preserves
sectPr → firm header/footer rIds): blob SHA
f1e9a9fb9a29ca01bf7bee709a45c5dda2a8e317.
- Both uploaded as mAi via --netrc-file ~/.netrc-mai.
- gen-skeleton-submission-template script gains an -anchors flag
(default true) so future regens emit composer-ready bodies. The
_firm-skeleton.docx regen was done via a one-off /tmp helper since
the gen-hl-skeleton-template script requires the proprietary .dotm
source which lives in HL/mWorkRepo; extending that script to accept
an existing .docx as input is a follow-up cleanup.
Build hygiene: go build/vet/test -short ./internal/... ./cmd/... all
clean; bun run build clean (2900 i18n keys, data-i18n scan clean).
NO behavior change for pre-Composer drafts (base_id NULL → v1
fallback render path stays compiled in). NO migrations needed in this
slice — sections were already in the schema from Slice A; only
content_md_de/en UPDATEs happen via the new PATCH endpoint.
Hard rules per Q2/Q10 ratification still honoured:
- No building_block_id lineage (Slice C territory; Q2).
- Caption/letterhead/signature are regular prose sections, seeded from
base spec; lawyer can edit/hide freely (Q10).
- {{rule.X}} aliases preserved (renderer pass unchanged).
NOT in scope per Slice B brief:
- Headings 1–3, lists, blockquote (Slice D's MD walker extension).
- Building blocks library (Slice C).
- Reorder / add-custom-section (Slice F).
- Auto-upgrade of pre-Composer drafts (Slice C — explicitly NOT in
this slice per head's brief msg #2393).
t-paliad-313 Slice B
|
|||
| e2969fc358 |
feat(submissions): Composer Slice A — base picker + read-only section list (m/paliad#141)
The first slice of the Submission generator v2 ("Composer") per the
design at docs/design-submission-generator-v2-2026-05-26.md §12 Slice A.
Ships the base concept + per-draft section seeding end-to-end with NO
change to the .docx render path — v1 export still works exactly as
today.
Schema (mig 146/147/148):
- paliad.submission_bases — catalog table; one row per template base
(slug, firm, proceeding_family, label_de/en, gitea_path, section_spec
jsonb, is_default_for[]). RLS: wide-open SELECT for authenticated
users, mutations admin-only (handler-enforced, no RLS write paths).
Seeded with 2 rows: hlc-letterhead → _firm-skeleton.docx; neutral →
_skeleton.docx. Each section_spec carries the 10-section default
(letterhead, caption, introduction, requests, facts, legal_argument,
evidence, exhibits, closing, signature) with bilingual labels +
bag-driven seed Markdown for caption/letterhead/signature.
- paliad.submission_drafts gains base_id (FK SET NULL, optional) +
composer_meta jsonb (default '{}'). Purely additive; pre-Composer
drafts keep base_id NULL → v1 fallback render path stays active.
- paliad.submission_sections — per-draft section rows (draft_id,
section_key, order_index, kind ∈ {prose,requests,evidence},
label_de/en, included, content_md_de/en). RLS mirrors
submission_drafts (owner-scoped + can_see_project, four policies).
Backend:
- BaseService (read-only Slice A): List + GetByID + GetBySlug +
GetDefaultForCode (firm/family fallback chain).
- SectionService: ListForDraft + Get + SeedFromSpec (transactional
multi-INSERT).
- SubmissionDraftService.AttachComposer wires both; Create resolves
the firm default base and seeds base_id + section rows in one tx.
Composer wiring is additive — when bases==nil the service stays
v1-shaped.
- Update accepts BaseID **uuid.UUID (set / clear / no-change).
- submissionDraftView gains BaseID, ComposerMeta, Sections fields.
- Routes: GET /api/submission-bases (catalog list). PATCH endpoints
on both project-scoped and global drafts accept "base_id".
Frontend:
- submission-draft.tsx: base picker dropdown above language toggle
(hidden until catalog loads); section-list pane above the preview
(hidden when no rows).
- client/submission-draft.ts: loadBases() parallel-fetches on boot;
paintBasePicker rebuilds <option> list on every paint; onBaseChange
PATCHes base_id and repaints; paintSectionList renders each section
read-only (label + kind chip + excluded badge + Markdown body).
- Per the brief: NO auto-upgrade of existing 11 drafts (that's Slice C).
Pre-Composer drafts get the picker (catalog still loads) but the
section pane stays hidden until they pick a base on a new draft.
Tests:
- TestFamilyOfCode + TestBaseSectionSpec_DecodeShape + _EmptyDecode
(pure unit, no DB).
- TestComposerSeedFlow (live, TEST_DATABASE_URL-gated): asserts mig 146
seeded 10 default sections on both bases; GetDefaultForCode picks
hlc-letterhead for HLC/de.inf.lg.erwidg; new draft via Create seeds
base_id + 10 section rows in tx with ascending order_index and
bilingual labels populated.
NO behavior change to .docx export — the v1 path stays sole render
path this slice. Composer's anchor-based assembly engine + MD→OOXML
walker land in Slice B.
Build hygiene: go build/vet/test -short clean; bun run build clean
(2900 i18n keys, data-i18n scan clean).
t-paliad-313
|
|||
| b97f170c1d |
chore: footer "by" + paliadin diagnostic logs
- Footer: "© 2026 Paliad — ein Werkzeug von / a tool by" → "© 2026 Paliad — by" (both DE + EN). - Paliadin streaming handler now log.Printf on every error path (StreamError, silence_timeout, backend nil/err) so the next "Verbindung verloren" failure produces a server-side trace. Previous behaviour: silent SSE close + empty paliad logs, impossible to diagnose. |
|||
| a75731a902 | Merge: t-paliad-302 — Verfahrensablauf duration indicator (hover + toggle, +3 lp.TimelineEntry fields) (m/paliad#133) | |||
| 3097df3918 |
mAi: #133 — Verfahrensablauf duration affordance (hover + toggle)
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). |
|||
| 9da4715137 |
feat(litigationplanner): Berufung tile UX — collapse side selectors + appeal-target trigger label (t-paliad-301, m/paliad#132)
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.
|
|||
| e2d75c391d |
fix(litigationplanner): rename upc.apl → upc.apl.unified (HOTFIX, t-paliad-299, m/paliad#130)
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. |
|||
| 07acf7b4a2 |
feat(litigationplanner): Berufung unification — one upc.apl + 5 appeal_target chips (Slice B1, m/paliad#124 §18.1)
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
|
|||
| 593e6243e0 | Merge: t-paliad-295 — side-aware Verfahrensablauf column headers (Proaktiv/Reaktiv ↔ Unsere/Gegenseite) (m/paliad#127) | |||
| 15cc5e418c |
feat(verfahrensablauf): side-aware column header labels (t-paliad-295)
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. |
|||
| cc13a5b857 |
chore(admin): remove /admin/rules/export page + export-migrations API (t-paliad-297)
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).
|
|||
| 7ca6b2d643 |
feat(verfahrensablauf): event-card overhaul — iconified state + caret-popover unhide (t-paliad-293)
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). |
|||
| 293e612582 |
feat(projection): IsConditional for uncertain-anchor rules (t-paliad-289)
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. |
|||
| 07d2eb472c | Merge: t-paliad-287 — submission form revision (Frist drop + grouped sections + Add Party + DB picker) (m/paliad#119) | |||
| 7cdccd55ae |
feat(submission-draft): grouped sections + per-side Add Party with DB picker (t-paliad-287)
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. |
|||
| c3eaa9b1d4 | Merge: t-paliad-290 — show-hidden toggle + un-hide chip on Verfahrensablauf (m/paliad#122) | |||
| 80883eaac5 |
feat(verfahrensablauf): re-surface hidden optional events — show-hidden toggle + un-hide chip (t-paliad-290)
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. |
|||
| 0e1f62e375 |
feat(verfahrensablauf): replace 'Beide' chip with 'Nicht festgelegt' (t-paliad-288)
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 |
|||
| 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.
|
|||
| c6267e4e6d | Merge: t-paliad-277 — submission party selector + import-from-project (mig 131) (m/paliad#109) | |||
| 8e696487e0 |
Merge: t-paliad-279 — Verfahrensablauf form reorder, party-after-proceeding-type (m/paliad#111)
# Conflicts: # frontend/src/client/verfahrensablauf.ts |
|||
| 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).
|
|||
| 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. |
|||
| b1340e2be4 | Merge: t-paliad-278 — date-range picker 3-column layout Past/NOW/Future (m/paliad#110) | |||
| 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). |
|||
| 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 |
|||
| 8be7af7cd6 | Merge: t-paliad-262 Slice A — procedural-events prose-only rename + {{rule.X}}↔{{procedural_event.X}} bidirectional aliases (m/paliad#93) | |||
| 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.
|
|||
| 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.
|
|||
| 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 |
|||
| 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. |
|||
| 31d78526cf |
feat(date-range-picker): t-paliad-248 — symmetric picker + filter-bar wiring
Slice A complete. Builds on the additive backend constants (commit
|
|||
| 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. |
|||
| 452ccdf127 | Merge: t-paliad-258 — Deadline form Auto/Custom rule field + canonical rule-label display (m/paliad#89) | |||
| 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)
|
|||
| 538c2d2da9 | Merge: t-paliad-257 — Verfahrensablauf user-perspective column axis (Unsere Seite / Gericht / Gegnerseite) (m/paliad#88) | |||
| a9a9adbd2a |
mAi: #88 - Verfahrensablauf: column axis reframed to user-perspective
Replaces the misleading Proaktiv/Reaktiv column pair with a static
"Unsere Seite" / "Gericht" / "Gegnerseite" axis ("WE always on the
left", per m's t-paliad-257 ask). The side toggle now drives row
PLACEMENT into the ours/opponent buckets — the column labels stay
truthful regardless of which physical party occupies them.
Old framing lied half the time: Klägerseite is sometimes proactive
(filing the claim) and sometimes reactive (responding to a CCR),
so "Proaktiv (Klägerseite)" was wrong whenever the user's perspective
flipped. New axis is purely positional with semantic labels.
Changes:
- frontend/src/client/views/verfahrensablauf-core.ts:
• ColumnsRow fields proactive/reactive → ours/opponent.
• renderColumnsBody picks static "Unsere Seite" / "Gegnerseite"
labels — no more variant-by-side label keys.
• bucketDeadlinesIntoColumns routes the user's party into `ours`
when opts.side ∈ {"defendant"}; default (null) keeps the legacy
"we are claimant" fallback so claimant-on-left layout survives.
- verfahrensablauf-core.test.ts: rewritten expectations on the new
ours/opponent fields. Added two new tests pinning the WE-on-left
semantics and the side+appellant interaction (side=defendant +
appellant=claimant → "both" collapses into opponent).
- fristenrechner.ts: wires currentPerspective into renderColumnsBody
as `side` so the columns honour the chip-strip perspective.
Without this, a defendant-perspective user would see claimant
filings under the "Unsere Seite" header — the old code didn't
need the wire-up because the labels weren't perspective-aware.
- i18n.ts: replaces deadlines.col.proactive(.defendant) +
deadlines.col.reactive(.claimant) with deadlines.col.ours +
deadlines.col.opponent ("Unsere Seite"/"Client Side",
"Gegnerseite"/"Opponent Side"). Court key unchanged.
- i18n-keys.ts: regenerated key union.
- global.css: .fr-col-proactive/.fr-col-reactive renamed to
.fr-col-ours/.fr-col-opponent.
Out of scope (kept intact):
- Side and appellant URL-state plumbing.
- Appellant selector for Appeal-type proceedings (separate axis).
- Project-default side-from-our_side wiring — /tools/verfahrensablauf
has no project context, and /tools/fristenrechner already does this
via applyOurSidePredefine().
Build: bun run build clean (2794 keys), go build ./... clean.
Tests: 112 frontend tests pass (was 110, +2 new); all Go tests
cached green.
|
|||
| 72b64140e9 |
mAi: #83 - approval withdraw warning modal + edit-instead path
t-paliad-252. Replace the silent confirm()-then-DELETE with a three-path
warning modal: Cancel / Edit event (primary) / Withdraw and delete
(destructive). The edit-instead path lets the requester revise the
in-flight entity without withdrawing the approval request.
Backend — new service method + endpoint
- ApprovalService.EditPendingEntity(requestID, callerID, fields):
- validates caller == requested_by AND status = pending
- reuses the existing wider counter-allowlist (buildCounterSetClauses
from SuggestChanges) — every editable field on the entity, not just
the date triggers
- applies the field updates to the entity row via applyEntityUpdate
(including the event_type_ids junction rewrite for deadlines)
- merges new fields into approval_requests.payload (jsonb) so the
approver inbox sees what was revised
- emits a distinct *_approval_edited_by_requester project_event so the
Verlauf surfaces the revision separately from the original *_requested
row and any decision row
- request stays pending; entity.approval_status stays pending
- POST /api/approval-requests/{id}/edit-entity
- Body: {"fields": {<entity-shape>}}
- Errors reuse the existing mapApprovalError mapping:
400 suggestion_requires_change, 403 not_authorized,
404, 409 request_not_pending
- Distinguishing audit event types per the spec:
- destructive Withdraw path: existing <entity>_approval_revoked
(no behaviour change — for CREATE deletes the entity, for UPDATE /
COMPLETE reverts to pre_image, for DELETE cancels the delete request)
- edit-instead path: new <entity>_approval_edited_by_requester
Frontend — shared withdraw warning modal
- frontend/src/client/components/withdraw-warning-modal.ts
- Built on the unified openModal() primitive (t-paliad-217 Slice A)
- Primary CTA "Termin bearbeiten" highlights the non-destructive path
- Secondary defaults to "Abbrechen" (handled by openModal)
- Destructive button "Endgültig zurückziehen und löschen" lives inside
the body (red, separated by a dashed border) so the safe path stays
visually primary in the footer
- Copy adapts per lifecycle:
CREATE → "Wenn Sie zurückziehen, wird die Frist/der Termin gelöscht."
UPDATE → "Ihre vorgeschlagenen Änderungen werden verworfen."
DELETE → "Der Eintrag bleibt bestehen."
Frontend — wiring on both detail pages
- deadlines-detail.ts + appointments-detail.ts:
- Replace confirm() in withdraw flow with openWithdrawWarningModal()
- Edit path: set module-level pendingEditMode = true + enter edit mode
(override existing pending-state freeze on appointments; expose
enterEdit() via late-bound pendingEnterEdit on deadlines)
- Save handler in pendingEditMode routes to /edit-entity instead of
PATCH /api/<entity>/{id} (which still 409s on pending state)
- Destructive Withdraw path: existing /revoke endpoint unchanged
- For CREATE-lifecycle revokes the entity is gone — bounce to the
/events list instead of trying to re-fetch (was reload() before)
i18n: +14 keys DE+EN under approvals.withdraw.* (modal title, primary,
destructive, cancel, lead.create.{deadline,appointment}, lead.update,
lead.delete, sub.create, sub.update, sub.delete)
CSS: .withdraw-warning-body + .withdraw-warning-{intro,sub,
destructive-row,destructive-btn} — lime-tint sibling palette consistent
with the existing form-hint pattern; destructive button uses .btn-danger.
Build hygiene:
- go build + go vet + go test ./internal/... clean
- frontend bun run build clean (2807 keys, +14 new, scan clean)
Files of note:
- internal/services/approval_service.go (EditPendingEntity + sortedKeys
helper; maps.Copy for the payload merge)
- internal/handlers/approvals.go (handleEditPendingEntity)
- internal/handlers/handlers.go (route registration)
- frontend/src/client/components/withdraw-warning-modal.ts (new shared
component)
- frontend/src/client/deadlines-detail.ts (initWithdraw rewrite + Save
pending-edit branch)
- frontend/src/client/appointments-detail.ts (withdrawAppointmentRequest
rewrite + Save pending-edit branch + form-freeze respects
pendingEditMode)
Out of scope (intentionally):
- Reopening already-deleted approval requests (the destructive path
stays final).
- Approval-request analytics / metrics.
- Notifying the original approval-requester via channel.
|
|||
| 1bf62c78e3 | Merge: t-paliad-251 — Deadline form overhaul (m/paliad#82) | |||
| 8caaf6a631 |
mAi: #82 - deadline form overhaul: type-modal filter chips, type→rule autofill, Auto mode, Standardtitel
t-paliad-251. Four bundled concerns from m's 2026-05-25 reports, one
worker, one branch.
Part 1 — Event-type browse modal (search + filters)
- Modal already had a search input; added court-type filter chips
(UPC / EPA / DPMA / DE / Allgemein) under the search.
- Chips render only the jurisdictions actually present in the data;
any future flavour lands at the end of the row.
- Active chip uses the lime-tint chip palette already established by
the .event-type-collapsed* family (t-paliad-165).
- Search input keeps autofocus; chip + search filters intersect.
Part 2 — Type → Rule auto-fill + sort options
- Inverted the existing rule.concept_default_event_type_id mapping
client-side: given a chosen event_type X, candidate rules are
those with concept_default_event_type_id === X.
- Resolution picks (1) exact match on the project's
proceeding_type_id, (2) jurisdiction match on the rule's
proceeding (EPA→EPO canonicalised), (3) first candidate.
- Sort dropdown next to the Rule label: by proceeding sequence,
by court (jurisdiction grouping with optgroup), alphabetical.
Defaults to "by court"; localStorage-persisted per browser.
- All sorts are client-side over the existing /api/deadline-rules
payload — no new endpoint.
Part 3 — Auto rule mode + clearer override warning
- Auto badge (.form-hint--auto, lime-tint pill + " — <rule name>")
surfaces whenever the Rule was derived from the chosen Type.
Disappears the moment the user manually picks a different rule.
- Override warning names BOTH sides + the actually-applied rule:
"Typ ergibt Regel: X. Gewählte Regel: Y. Es wird Y angewendet."
- Symmetric `lastAutoFilledRuleID` sticky-replace flag mirrors the
existing `lastAutoFilledEventTypeID` (t-paliad-165) so the auto-
fill only replaces its own previous suggestion, never a manual
pick.
- Collapsed Typ view (t-paliad-165) is suppressed when the rule was
auto-derived from the type — the "vorgegeben durch Regel" copy
reads backwards in that case; show picker + Auto badge instead.
Part 4 — Standardtitel button (create + edit)
- Button rendered next to the Title field on both /deadlines/new
and /deadlines/{id} (edit mode only).
- Recipe (recipe-docs-here-so-future-templates-can-mirror-it):
head =
1. event_type label (if exactly one Typ chip is set)
2. rule code+name (when a Rule is set — "RoP.023 — Klageerwiderung")
3. proceeding type name from project (create form only)
4. fallback: t("deadlines.field.title.default_fallback")
suffix = " — <project.reference>" when ref is set and not
already in head.
Examples:
Klageerwiderung — C-UPC-0042 (type known)
RoP.023 — Klageerwiderung — REF (rule known, no type)
UPC — Verletzungsverfahren — REF (only proceeding type)
Neue Frist — REF (fallback)
- Click REPLACES current title; no destructive confirmation
because the user invoked it explicitly. Focus moves into the
title input afterwards so the user can fine-tune.
Build hygiene:
- go build + go vet + go test ./internal/... clean.
- frontend/build.ts clean (2786 keys, +10 new DE+EN, scan clean).
- All changes client-side / CSS / i18n + 2 small TSX edits; no
schema, no service, no migration.
Files touched:
- frontend/src/client/event-types.ts (browse-modal chips)
- frontend/src/client/deadlines-new.ts (rewrite — Type→Rule, sort,
Auto badge, override warn, Standardtitel)
- frontend/src/client/deadlines-detail.ts (edit-mode Standardtitel
+ show/hide on enter/exit edit)
- frontend/src/deadlines-new.tsx (label-row + sort dropdown + Auto
badge slot + override-warn slot + Standardtitel button)
- frontend/src/deadlines-detail.tsx (Standardtitel button)
- frontend/src/styles/global.css (.event-type-browse-chip*,
.form-hint--auto, .form-hint-badge, .form-field-label-row,
.btn-link-action, .rule-sort-select)
- frontend/src/client/i18n.ts (+10 keys DE+EN)
|
|||
| 02255c4234 |
mAi: #81 - verfahrensablauf side+appellant selectors + UPC Appeal trigger label
Concerns A + B + C from m/paliad#81:
A. Browse-a-proceeding (/tools/verfahrensablauf) gains a side selector
(Kläger/Beklagter/Beide) and an appellant selector. The side selector
swaps which column labels which user-side; the appellant selector
collapses party='both' rules into the appellant's column (no mirror)
so role-swap proceedings (Appeal, etc.) stop showing every row
twice in the timeline. Both selectors are URL-driven (?side= +
?appellant=) and re-render without a backend round-trip.
The appellant row hides itself for proceedings without an appellant
axis (first-instance Inf/Rev/Opp) via a small allowlist.
B. UPC Appeal trigger-event caption now reads "Anfechtbare Entscheidung"
/ "Appealable Decision" instead of falling back to the proceeding
name ("Berufungsverfahren" / "Appeal"). Implemented as an optional
trigger_event_label_{de,en} column on paliad.proceeding_types (mig
121); the frontend prefers it over the proceedingName fallback that
fires when no rule has IsRootEvent=true. No new deadline rules, no
slug changes (hard rule from the issue).
C. Parameter contract for the column projection is unified in
bucketDeadlinesIntoColumns(deadlines, {side, appellant}) — a pure
helper extracted from renderColumnsBody so the routing behaviour
stays unit-testable without a DOM. Tests cover the default mirror,
appellant-collapse for both sides, side-swap of column ownership,
the combined case, and row alignment by dueDate.
Verification
- go build ./... clean
- go test ./... all green
- bun run build (frontend) clean
- bun test (frontend/src) 110/110 pass (12 new + 98 prior)
- Migration 121 applied to paliad schema; UPC Appeal proceeding now
carries the curated trigger label pair.
Out of scope (filed for follow-up): per-rule role tagging so
respondent-side filings (Response to Appeal, Cross-Appeal) land in
the respondent's column when an appellant is selected. The current
issue scope (one-row-per-deadline collapse) is delivered; the
realistic-per-row routing needs a deadline_rules schema bump that
the hard rules of #81 excluded.
|
|||
| 1714b788d2 |
feat(projects-detail): t-paliad-245 — demote Daten Export into Verwaltung tab
m/paliad#76. The export button no longer pokes out of the tabs nav with a non-tab styling — instead it lives inside a new "Verwaltung" tab (last in the project tab list) as a normal section with heading, description, and a plain btn-secondary trigger. Same gate as before (canExportProject). Archive co-locates in the same tab as a pointer to the Edit-modal danger zone: click "Bearbeiten öffnen" → modal opens scrolled to the archive button. Single source of truth for the destructive action stays in the modal; the Verwaltung pointer just gives it discoverability. If neither sub-section is visible to the caller (no export entitlement, not global_admin), the Verwaltung tab hides itself — an empty tab is worse UX than no tab. |
|||
| a911a2d0ee |
feat(submissions): t-paliad-243 — global Schriftsätze drafts without project
Adds an end-to-end project-optional path for Schriftsatz drafts:
- Migration 120 drops NOT NULL on paliad.submission_drafts.project_id
and rewrites the four RLS policies to gate purely on user_id when
project_id IS NULL, otherwise on paliad.can_see_project. Down
refuses to run if project-less rows exist (safer than silent
data corruption).
- SubmissionDraft.ProjectID becomes *uuid.UUID end-to-end. Service
layer skips project/parties/deadline lookups when nil and exposes
DraftPatch.ProjectID for the "Projekt zuweisen" affordance.
ListAllForUser LEFT JOINs paliad.projects so project-less drafts
surface in the global index next to project-scoped ones.
- New HTTP surface:
GET /submissions/new (picker page)
GET /submissions/draft/{draft_id} (editor for any draft)
GET /api/submissions/catalog (catalog without project)
POST /api/submission-drafts (project-less or attached)
GET/PATCH/DELETE /api/submission-drafts/{draft_id}
POST /api/submission-drafts/{draft_id}/export
Existing /api/projects/{id}/submissions/... routes remain bit-
identical so the project-scoped flow keeps working unchanged.
- Frontend: /submissions/new lists the full cross-proceeding catalog
grouped by proceeding, filterable by text + chip. Each row offers
"Ohne Projekt" (instant draft) or "Mit Projekt…" (modal picker
with autocomplete over visible projects). /submissions index gains
a prominent "Neuer Entwurf" CTA and an empty-state CTA pointing at
the picker. The editor renders a banner + "Projekt zuweisen"
action when project_id is null; assigning persists project_id and
redirects to the project-scoped URL.
Audit + project-event writes detect d.ProjectID == nil; the audit
row's scope flips to 'user' (scope_root = user_id) and the
project_events row is skipped entirely.
|
|||
| 8e195cb497 |
feat(submissions): t-paliad-242 — Schriftsätze tab shows full catalog grouped by proceeding
Per m's 2026-05-23 ask: from any project, surface every available
template/generator instead of just the project's own proceeding.
Backend (GET /api/projects/{id}/submissions):
- drop the proceeding_type_id filter; JOIN deadline_rules with
proceeding_types to return every active+published filing rule
across every active proceeding
- response gains proceeding_code, proceeding_name, proceeding_name_en
per row plus project_proceeding_code at the top so the frontend
can pin the project's own group
- has_template now reflects "per-submission .docx wired in
submissionTemplateRegistry"; the editor still falls back to the
universal HL Patents Style for everything else (t-paliad-238)
- can_see_project gate unchanged; rules are static reference data
- sorted by (proceeding_code, submission_code)
Frontend:
- client/submissions.ts renders a grouped table: project's own
proceeding pinned to the top with a lime border + "(dieses
Projekt)" suffix, every other proceeding alphabetised below
- "Generieren" + "Bearbeiten" buttons stay on every row (editor
handles missing variables via [KEIN WERT: …])
- "universell"/"universal" badge surfaces for rules without a
per-submission template — informational, not blocking
- soften the no_proceeding hint so the catalog still renders below
- entity-table-group-header CSS, including --own modifier and a
read-only override so group rows don't pretend to be clickable
Verified: 103 filing rules across 19 proceedings surface (de.inf.lg,
upc.inf.cfi, epa.opp.opd, etc.). go build + go vet + go test
./internal/... + bun run build clean.
|
|||
| 436c1b41bb |
feat(submissions): t-paliad-240 — Schriftsätze sidebar + global drafts index
Add a top-level Schriftsätze entry under the Werkzeuge sidebar group
plus a new /submissions page that lists every draft the caller owns
across visible projects. Each row links to the per-project editor at
/projects/{id}/submissions/{code}/draft/{draft_id}.
Backend: SubmissionDraftService.ListAllForUser joins paliad.submission_drafts
with paliad.projects, gated by paliad.can_see_project for visibility. New
GET /api/user/submission-drafts endpoint exposes the rows; the page route
GET /submissions is gateOnboarded'd alongside the other project surfaces.
Frontend: submissions-index.tsx renders an entity-table; submissions-index.ts
hydrates from /api/user/submission-drafts and wires the row-click contract
(skip clicks on inner a/button). DE primary, EN secondary i18n.
|
|||
| 2c5f85b802 | Merge: t-paliad-238 Slice A — dedicated Submissions draft editor + merge engine | |||
| d3aade5aac |
feat(submissions): t-paliad-238 Slice A — dedicated draft editor page
Adds the dedicated Submissions/Schriftsätze editor at
/projects/{id}/submissions/{code}/draft (and …/draft/{draft_id}) per
docs/design-submission-page-2026-05-22.md.
Lawyer picks (or creates) a named draft, edits placeholder variables
in a sticky sidebar, sees a read-only HTML preview of the merged
document body, and exports a .docx with project state + lawyer
overrides resolved. Drafts persist in paliad.submission_drafts
keyed on (project_id, submission_code, user_id, name) with RLS via
can_see_project; updates and deletes additionally gated on owner-only
(Q-E4 owner-scoped pick, m-confirmed).
Resurrected from git history per the design's "no rewrite" plan:
SubmissionVarsService ← commit
|
|||
| c6a5416611 |
feat(projects): t-paliad-239 — add Checklist button on project Checklists tab
The project-detail Checklists tab now exposes an "Add Checklist"
button that opens a template picker modal. Picking a template POSTs
to /api/checklists/{slug}/instances with the current project_id and
the template title as the instance name; the table refreshes and a
transient success banner confirms the add. Reuses the catalog cache
across the tab renderer and modal so the second open doesn't refetch.
Closes the UX cul-de-sac in the previous empty-state copy that told
users to leave the page and create instances on the Vorlagen-Seite.
|