946f37365105fea0b266246773dd7c62aca0ff4c
475 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
| 5834e3dc66 | Merge: Composer Slice D — rich prose (headings, lists, blockquote, hyperlinks) in MD→OOXML walker (m/paliad#141) | |||
| 677849784c |
feat(submissions): Composer Slice D — rich prose (headings, lists, blockquote, hyperlinks) (m/paliad#141)
Extends the Composer's MD → OOXML walker per the design at
docs/design-submission-generator-v2-2026-05-26.md §12 Slice D from
Slice B's paragraphs + B/I baseline to the full rich-prose feature set:
headings 1-3, bullet + numbered lists, blockquote, inline hyperlinks.
MD walker (internal/services/submission_md.go, +320 / -75 LoC):
- RenderMarkdownToOOXMLWithStyles is the new Slice-D entry point;
RenderMarkdownToOOXML stays as a thin back-compat wrapper.
- splitMarkdownBlocks classifies every line into one of:
paragraph, heading_1/2/3, list_bullet, list_numbered, blockquote.
CommonMark-style 3-space indent tolerance; "N. " and "N) " for
numbered. Blank-line spacing semantics preserved from Slice B.
- renderBlockParagraph applies stylemap[blk.styleKey] (with
fall-back to stylemap["paragraph"]). List blocks emit visible
"• " / "N. " prefix runs so the structure surfaces even if Word
isn't configured with auto-list-numbering — lawyer can apply a
real Word list style post-export. Numbered-list ordinals reset
on every non-list block (so "1. A\nplain\n1. C" renders 1./1.,
not 1./2.).
- parseInlineRuns adds `[label](url)` recognition. Each link gets
routed through the optional HyperlinkAllocator; the walker emits
`<w:hyperlink r:id="{rId}">…runs…</w:hyperlink>` with the
"Hyperlink" character style on each child run. Nil allocator
falls back to plain-text label (URL drops, label survives).
Composer (internal/services/submission_compose.go, +130 / -10 LoC):
- composerLinkAllocator hands the walker fresh rIds (rIdComposer1,
rIdComposer2, …) outside the base's existing namespace; same URL
shared across multiple sections dedupes to one rId.
- patchDocumentXMLRels appends matching <Relationship Type="…/hyperlink"
Target="URL" TargetMode="External"/> entries to
word/_rels/document.xml.rels. Idempotent on rIds already present;
synthesizes a fresh rels part when missing (defensive for stripped
bases). Returns the patched parts slice (caller must overwrite
because append may grow the backing array — fixed in this slice).
- Compose now passes the full stylemap (paragraph + heading_1/2/3 +
list_bullet + list_numbered + blockquote) into the walker, not
just the paragraph-style entry.
Frontend (frontend/src/client/submission-draft.ts):
- Toolbar adds H1/H2/H3 buttons (formatBlock h1/h2/h3), bullet
list, numbered list, blockquote, and a link button that prompts
for a URL + wraps the selection via execCommand("createLink").
- domToMarkdown serializer extends to <h1>/<h2>/<h3>, <ul>/<ol>
with per-item ordinal counter for numbered lists, <blockquote>,
and <a href="…"> → `[label](url)`. Nested <li> handling sits in
the ul/ol branch.
Tests (internal/services/submission_md_test.go, internal/services/
submission_compose_test.go):
- TestRenderMarkdownToOOXML_Heading1 / _Heading2And3 — stylemap
applied.
- _BulletList / _NumberedList / _NumberedListResetsOnNonList —
prefixes + ordinal counter.
- _Blockquote — stylemap applied.
- _Hyperlink — allocator called, w:hyperlink rId wired, Hyperlink
character style on label runs.
- _HyperlinkNilAllocatorFallsBackToPlain — label survives, no
hyperlink tag emitted.
- TestDetectBlockMarker — 13 marker / non-marker cases.
- TestComposer_HeadingsAndLists — end-to-end through Compose with
a multi-construct draft; verifies stylemap presence + content +
ordinal prefixes.
- TestComposer_HyperlinkWiresRels — body has the right
<w:hyperlink r:id="rIdComposer{N}">, document.xml.rels has the
matching <Relationship> rows with External target mode.
- TestComposer_HyperlinkDedupesByURL — two `[label](url)` references
to the same URL share one rId; second allocation gets no new
Relationship row.
Build hygiene: go build/vet/test -short clean (all packages); bun run
build clean (2906 i18n keys).
NOT in scope (Slice D's brief was rich-prose + toolbar):
- Numbering.xml audit on bases — current approach emits visible
"• " / "N. " prefix runs without depending on numbering.xml. A
future slice can swap to `<w:numPr>` if firm-style auto-numbering
becomes a hard requirement.
- DOM-from-Markdown on initial editor paint — the editor still uses
textContent=md, so toolbar-applied formatting reverts to literal
Markdown text after autosave + repaint. Acceptable trade-off for
Slice D; a future polish could parse MD into the DOM on paint.
- Tables, images, footnotes (still design §13 out of scope).
Hard rules honoured:
- NO new migrations (Slice D is pure code).
- NO behavior change for pre-Composer drafts (gate on draft.BaseID
unchanged).
- {{rule.X}} aliases preserved (placeholders pass through the walker
verbatim, get substituted by the v1 SubmissionRenderer pass).
- Q2 ratification preserved (no building_block_id lineage).
- Q9 ratification preserved (4-tier BB visibility from Slice C).
t-paliad-316 Slice D
|
|||
| 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
|
|||
| 05ad43aa46 | Merge: t-paliad-308 — Verfahrensablauf URL state hybrid (chips in URL, scenario in localStorage) (m/paliad#137) | |||
| 43de8f9c7b |
feat(verfahrensablauf): URL state hybrid — filter chips in URL, scenario in localStorage (t-paliad-308, m/paliad#137)
Splits /tools/verfahrensablauf persisted state into two namespaces: URL params (timeline kind — paste-able, shareable, refresh-resistant): proceeding, side, target, trigger_date localStorage `paliad.verfahrensablauf.scenario.*` (per-user tweaks that should never leak into a shared link): event_choices, court_id, ccr, inf_amend, rev_amend, rev_cci, show_hidden Hydration order: URL wins. localStorage fills the rest. A shared link reproduces the timeline kind but each user sees their own scenario state. Added trigger_date and proceeding to URL (previously DOM-only — a refresh lost the date and the proceeding tile). Moved event_choices and show_hidden from URL to localStorage (verbose, per-user). Added court_id + flag persistence to localStorage (previously DOM-only). New pure module `views/verfahrensablauf-state.ts` owns the URL + localStorage contract: URL parsers + encoder (`applyFiltersToSearch`), scenario read/write helpers, and a `hydrate()` orchestrator that documents the URL→localStorage order. 31 unit tests pin the contract, including the "shared link doesn't leak scenario state" invariant. Anti-patterns explicitly avoided: - No ?appellant= resurrection (#132 removed it; engine reads from the single side picker for role-swap proceedings). - trigger_date in URL not localStorage (a shared link must reproduce the same dated timeline). - URL→localStorage hydration order is contract; localStorage never overrides an explicit URL value. Project-driven side-fill chip (?project=<id>) still overrides as before — parseSideFromSearch is called before the project's our_side is applied so an explicit ?side= still wins. Build clean: `bun run build`, `bun test` (240 pass / 594 expect calls), `go test ./...`, `go vet ./...`. |
|||
| 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. |
|||
| 446c46e5c5 |
fix(css): repoint 12 var(--color-surface-alt, hex) sites to defined tokens (t-paliad-310, m/paliad#138)
The --color-surface-alt token was never defined in :root or :root[data-theme="dark"], so the var() fallback hex literal always won — leaving 12 surface sites with zero dark-mode treatment. Same pattern as t-paliad-087 / t-paliad-150 / t-paliad-291. Issue #138 surfaced four panels visibly broken in dark mode: 1. submission-draft no-project banner ("Kein Projekt zugeordnet…") — white-on-white 2. submission-draft preview header ("Vorschau / Read-only Vorschau…") — white-on-white 3. smart-timeline rule-chip (e.g. de.null.bpatg.berufung in Vorhersage rows) — grey-on-grey 4. submission-draft addparty manual form (Manuell / Aus DB / Name / …) — white-on-white Eight more latent sites with the same root cause are fixed in the same pass: .submissions-new-chip:hover, .submissions-new-project-item:hover, .submission-draft-import-row, .submission-draft-addparty-search-projref, .collab-invite-hint, .smart-timeline-status-icon, .smart-timeline-kind-chip--projected, .smart-timeline-add-choice:hover. Each site repointed to the semantically correct existing token (--color-surface-2 for #fafafa, --color-surface-muted for #f4f4f4, --color-bg-subtle for #f7f7f0, --color-bg-lime-tint for the lime-tinted collab-invite-hint). All four target tokens are defined in both :root and :root[data-theme="dark"]. No new tokens introduced. Light-mode hex values are functionally identical (#fafafa==#fafafa, #f4f4f4≈#f3f4f6, #f7f7f0≈#f7f3f0). Verified: bun run build clean; Playwright screenshots of the four panels in both light + dark modes show correct rendering. |
|||
| 367627af0d |
fix(verfahrensablauf): appeal side filter + parent in duration label + notes dedup (t-paliad-307, m/paliad#136)
Frontend half of the four Verfahrensablauf appeal bugs. Bug 1 (frontend half) — Side selector dead on appeal. The column bucketer now reads dl.appealRole (engine-stamped under appeal_target) and routes each "both" appeal rule via the user side: side=claimant maps the user to the appellant, so appellant filings land in 'ours' and appellee filings in 'opponent'; side=defendant mirrors. side=null keeps the legacy mirror so every appeal rule renders in both columns (every-rule-visible behaviour the brief calls out). The new appealAware opt gates the path so non-appeal proceedings keep their existing bucketing untouched. Removed upc.apl.unified from APPELLANT_AXIS_PROCEEDINGS — appeal routing is now per-rule via appealRole, not a page-level appellant collapse. Other role-swap proceedings (EPA opp, DE/DPMA appeals) keep the appellant axis since they have no appeal_target metadata. Bug 3 — Duration label appends parent name. formatDurationLabel now takes an optional parent fallback and renders "<n> <unit> <timing> <parent>". deadlineCardHtml resolves the parent per-rule (dl.parentRuleName / EN variant), falling back to opts.trigger EventLabel for root rules with a non-zero duration (e.g. Berufungseinlegung 2 mo. after the Endentscheidung). renderColumns Body + renderTimelineBody auto-derive the trigger event label from the response via the new pickTriggerEventLabel helper unless the caller passes one explicitly. Bug 4 — Duration prefix stripped from deadline_notes. New stripLeadingDurationFromNotes regex peels off leading "Frist N <unit> <vor|nach|ab|seit> …. " (DE) and "<N>-<unit> period from …" / "N <unit> BEFORE …" / "Period is N <unit> from …" (EN) up to the first sentence boundary. Wired into deadlineCardHtml so noteHint + notesBlock both render the deduped text. Per the brief's option (a): conservative regex, composite durations with "ODER" / "whichever is the longer" stay untouched as a follow-up editorial cleanup. deadline_rules DB untouched. Tests: 22 new test cases across appeal-aware bucketing, formatDurationLabel parent append, deadlineCardHtml duration tooltip resolution, and stripLeadingDurationFromNotes regex (positive + negative + composite + EN/DE variants). All 209 frontend tests pass. Engine wire fields added in the preceding commit (AppealRole, IsTriggerEvent). Reads them from CalculatedDeadline without breaking the wire contract for non-appeal callers. |
|||
| 2377f08bd7 | Merge: t-paliad-304 — R.109 anchor + columns-view duplicate fix (topo walk + 'both'→ours collapse) (m/paliad#135) | |||
| 1d704f6e04 |
fix(litigationplanner): R.109.1/R.109.4 mis-anchor + duplicate 'both' row in columns view (t-paliad-304, m/paliad#135)
Two bugs surfaced on /tools/verfahrensablauf?side=defendant for upc.inf.cfi:
1. Anchor regression for timing='before' children of court-set parents.
Rules R.109.1 (translation_request) and R.109.4 (interpreter_cost)
anchor on the oral hearing (parent_id=upc.inf.cfi.oral, IsCourtSet)
but were computing dates BEFORE the Statement of Claim — 1 month
resp. 2 weeks before the SoC instead of before the oral hearing.
Root cause: engine walked rules in sequence_order, and the two
"before"-timed children carry sequence_order 45/46 (their chronological
position, before the oral hearing at 50). Their parent had therefore
not been processed yet when the children were, so courtSet[oral.ID]
was still empty → parentIsCourtSet=false → the engine fell back to
the trigger date as the base.
Fix: walk rules in topological order (parent-first) during the
compute pass, then restore sequence_order on the output slice so
the wire shape and the linear timeline view's render order stay
identical to the legacy behaviour modulo the bug fix.
2. Duplicate "Antrag auf Simultanübersetzung" row in columns view.
With primary_party='both' and an explicit side pick (?side=defendant),
the bucketing mirrored the card into both 'Unsere Seite' and
'Gegnerseite' — the same card on the same row, visible as a
duplicate.
Fix: when the user has committed to a perspective (side picked)
but no appellant axis applies, collapse 'both' rows into ours.
The '↔ beide Seiten' indicator is suppressed in that path to match
the existing appellant-collapse semantics (no sibling row to mirror
to). Legacy mirror behaviour is preserved when side is null.
DB audit ruled out a data-level duplicate: exactly one published+active
row per submission_code in paliad.deadline_rules.
Tests:
- pkg/litigationplanner/before_court_set_anchor_test.go: synthetic
rules pinning the conditional-on-court-set-parent contract plus
the override path (1mo before user-pinned oral).
- frontend/src/client/views/verfahrensablauf-core.test.ts: two new
cases pinning the side-collapse routing for party='both'.
|
|||
| 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. |
|||
| 9d3325bd88 | Merge: t-paliad-291 — dark-mode lime-chip contrast fix across 6 selectors (m/paliad#123) | |||
| 18d2e743ba |
fix(styles): dark-mode contrast on lime-active chips (t-paliad-291)
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
|
|||
| 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 |
|||
| 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 (
|
|||
| 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.
|
|||
| 5efb9f5098 | Merge: t-paliad-281 — admin rules list 'undefined' proceeding name fix (m/paliad#113) | |||
| 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 |
|||
| 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.
|
|||
| 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. |
|||
| 96eab90044 | Merge: t-paliad-280 — search input icon-text padding fix (m/paliad#112) | |||
| 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. |
|||
| b1340e2be4 | Merge: t-paliad-278 — date-range picker 3-column layout Past/NOW/Future (m/paliad#110) | |||
| 1292aa575d | Merge: t-paliad-265 — per-event-card choices Slice A+B (popover + CCR + projection engine, mig 129) (m/paliad#96) | |||
| 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 |
|||
| 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. |
|||
| 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.
|
|||
| f0c343c638 | Merge: t-paliad-267 — Auto-rule resolved name on its own row in deadline form (m/paliad#98) | |||
| f11390d18b | Merge: t-paliad-270 — i18n event.title.approval_decided + member_role_changed (m/paliad#101) | |||
| 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. |