New "Schriftsätze" tab on /projects/{id}, lazy-loaded by the
existing tab switcher (same pattern as the Checklisten tab — only
hits the API when the user actually opens it). Lists the project's
filing rules in a 4-column table: name (with submission_code under
it), party, legal basis, action button.
Action column shows [Generieren] for rules with a resolvable
template and "Keine Vorlage" / "No template" for rules without one.
The generate button fetches the .docx via XHR, parses the
Content-Disposition filename, creates an object URL, and triggers
the browser download via a hidden <a download>. Disabled
mid-flight to prevent double-submits.
The table opts into the `.entity-table--readonly` modifier — rows
themselves don't navigate; only the inline button does (avoids the
"clickable row that isn't" UX lie called out in the project
CLAUDE.md frontend conventions).
11 new i18n keys per language. New CSS block for the submission-row
typography (name + dim-grey code stacked vertically, right-aligned
action cell, italic no-template hint).
Two endpoints under /api/projects/{id}/:
GET /submissions
Lists the project's filing-type rules (event_type='filing',
lifecycle_state='published') for the project's proceeding,
each annotated with has_template via the registry's cheap
SHA-only probe. Powers the SubmissionsPanel.
GET /submissions/{code}/generate
Renders the .docx and streams it back as an attachment with
Content-Disposition: attachment; filename="…". Writes three
audit records: paliad.system_audit_log (event_type=
'submission.generated'), paliad.project_events (event_type=
'submission_generated', surfaces in Verlauf / SmartTimeline),
and paliad.documents (doc_type='generated_submission',
file_path NULL — bytes are regenerable from inputs per m's
Q3 pick, no server-side binary). All three writes use a 10s
background context so the user still gets the download if
audit insertion races a slow DB.
File naming follows §7 of the design:
{rule.name}-{project.case_number}-{YYYY-MM-DD}.docx with locale-
aware rule.name and slash→underscore sanitisation on
case_number. Empty case_number falls back to an 8-hex-char id from
the project UUID.
Visibility: ProjectService.GetByID gates every request; 404 (not
403) on no-access to avoid project enumeration. No profession floor
— matches every other write surface in paliad.
Wired into handlers.Services + dbServices + cmd/server/main.go.
Singletons constructed once at boot; no per-request allocation. No
migration needed — paliad.documents has no CHECK on doc_type, so
'generated_submission' is purely additive.
TemplateRegistry (services/submission_templates.go) walks the
m-locked Q4 fallback chain — templates/{FIRM_NAME}/{code}.docx →
templates/_base/{code}.docx → templates/_base/{family}.docx →
templates/_base/_skeleton.docx — against the Gitea repo
HL/mWorkRepo. SHA-cache + 5-min refresh check, identical pattern to
internal/handlers/files.go's HL Patents Style proxy. Distinguishes
"no template" (chain fallthrough) from "Gitea down" so the handler
can render different UI for each.
SubmissionVarsService (services/submission_vars.go) assembles the
~30-placeholder bag from project + parties + rule + next-deadline +
user + firm + today. Locale-aware long-date forms (DE + EN) and a
legal_source pretty-printer that rewrites DE.ZPO.276.1 → "§ 276 Abs.
1 ZPO" / "Section 276(1) ZPO" for the prefixes the 254-rule corpus
uses today. Unknown prefixes pass through unchanged.
Visibility inherits from ProjectService.GetByID
(paliad.can_see_project) — unauthorised callers get the same
ErrNotVisible that every project surface returns.
Pure-Go {{path.dot.notation}} placeholder engine + unit tests
(t-paliad-215, design docs/design-submission-generator-2026-05-19.md
§6). Chosen over github.com/lukasjarosch/go-docx because that library
treats sibling placeholders inside one <w:t> run as nested and
refuses to replace them — patent submissions routinely carry multiple
placeholders per paragraph (party blocks especially), so the library
is a non-starter.
Two-pass strategy preserves run-level formatting on the common path:
1. Pass 1: regex replace inside each <w:t>…</w:t> independently —
no format loss for the 99% case where placeholders are intact.
2. Pass 2: paragraph-level merge for paragraphs that still contain
orphan "{{" or "}}" markers (Word fragmented the placeholder
across runs).
Missing placeholders render [KEIN WERT: <key>] / [NO VALUE: <key>]
markers so the lawyer sees the gap in Word rather than getting a 400.
Tests cover: single-run, multi-per-run (the go-docx failure mode),
cross-run merge, missing-marker (DE+EN), XML escaping of special
chars, non-document zip entries preserved, placeholder regex
grammar.
DESIGN READY FOR REVIEW — copernicus inventor pass on the submission
generator (t-paliad-215). 5 questions answered with m's picks captured
in §2; awaiting head's go/no-go on coder shift.
Locked decisions:
- Scope: template-render to .docx (no LLM in v1)
- Template registry: Gitea (mWorkRepo proxy, same pattern as
HL Patents Style)
- Output: direct download, no server-side binary persistence
- Mapping: fallback chain (firm → base/code → base/family → skeleton)
- Slice 1: one template end-to-end on one project
(de.inf.lg.erwidg / Klageerwiderung)
No code, no migrations, no schema additions. Read-only design phase
per inventor SKILL.md.
Replaces the one-sentence "endpoint" stub with a proper landing: features list, update flow explainer, fresh-install download link, contact line. Renders the served version live from version.json. Paliad palette (midnight/lime). This is what the HL Patents Style ribbon's Info dialog now links to on OK.
m hit two bugs opening the Slice 1 export in Excel / Windows:
1. **Excel showed a "Repairs required" prompt** on open. Root cause:
the SetPanes call passed only `{Freeze: true, YSplit: 1}` — the
obvious-but-wrong shape. The resulting <pane> XML missed the
`topLeftCell` and `activePane` attributes that Excel requires for
a frozen-row pane (excelize's parser is permissive on re-read but
Excel is strict). Fix: complete the Panes struct (TopLeftCell="A2",
ActivePane="bottomLeft", Selection on bottomLeft) and surface
SetPanes errors instead of `_ =`-ignoring them.
2. **Windows Explorer / Excel's File→Info showed Modified=2006-09-16
("xuri")** — excelize's hardcoded first-commit defaults. Root cause:
buildXLSX never called SetDocProps so the canned defaults leaked.
Fix: SetDocProps({Created, Modified} = meta.GeneratedAt;
Creator = "Paliad (<firm>)"; Title/Description scoped per export).
3. **Bonus**: the outer-zip entry mtimes were stamped 2000-01-01 (the
deterministic constant) so extracted files showed a Y2K Modified
date in Explorer. Now stamped meta.GeneratedAt, which preserves
determinism within an export (same row state + same GeneratedAt →
same bytes, the actual m's-Q6 contract).
Also: set the active sheet to __meta (index 0) after sheet creation so
a future code path that adds/removes sheets can't leave an out-of-range
active-sheet index that would trip a separate "repairs required" path.
Regression tests in dump_export_test.go pin all three fixes by re-opening
the generated xlsx via excelize.OpenReader and asserting:
- docProps Created/Modified == meta.GeneratedAt (RFC 3339 UTC)
- docProps Creator contains "Paliad"
- xlsx bytes never contain "2006-09-16T00:00:00Z" or "<dc:creator>xuri</dc:creator>"
- sheet2/sheet3 raw XML carries topLeftCell + activePane + state=frozen
- outer-zip entries' Modified is within ±2s of GeneratedAt
- developer hatch: DUMP_EXPORT=1 writes /tmp/paliad-export-debug.{zip,xlsx}
for opening in real Excel.
Adds a 4th tab "Datenexport" to /settings (after Profil /
Benachrichtigungen / CalDAV) with a single-button card that triggers
GET /api/me/export. Browser handles the download via
Content-Disposition: attachment.
i18n: 12 new keys under einstellungen.export.* (DE primary, EN
secondary) — subtitle, bullets per format, scope notice, audit
notice, button label, post-click hint.
The tab is loaded lazily (idempotent loadExportTab) like every other
settings tab, and the runExport handler swaps in a transient <a download>
to use the browser's normal download pipeline.
Schema-only landing for Slice 1 of the CalDAV multi-calendar design
(docs/design-caldav-multi-calendar-2026-05-19.md). Sync engine NOT
touched — Slice 2 wires the per-binding fan-out. After this migration:
- paliad.user_calendar_bindings — N bindings per user with scope_kind
∈ {all_visible, personal_only, project, client, litigation, patent,
case}. Hierarchy scopes anchor scope_id at paliad.projects(id).
Partial unique indexes enforce one binding per (user, scope_kind,
scope_id) for hierarchical scopes and one per (user, scope_kind)
for the scope-less roots. RLS mirrors user_caldav_config.
- paliad.appointment_caldav_targets — per-(appointment, binding) join
carrying caldav_uid + caldav_etag. UID stays canonical per
appointment so the same event in N cals shares one UID.
- Backfill — one all_visible binding per existing user_caldav_config
row, one target row per appointment already pushed. Maps target to
the creator's binding, matching today's Phase F semantics where the
creator's goroutine owns the etag.
Legacy paliad.appointments.caldav_uid / caldav_etag columns are
untouched (kept as denormalised pointers through Slice 1+2; dropped
in Slice 4 after telemetry).
Dry-run verified against live Supabase (PG 15.8): synthetic config +
appointment backfill creates exactly 1 binding + 1 target; re-run is a
no-op; all CHECK + unique-index constraints enforce as designed; final
assertions pass with 0 missing rows.
Prod impact at landing: 0 rows in user_caldav_config and 0 appointments
with caldav_uid — backfill is a true no-op. Slice 1 ships invisible.
Slice 1 of docs/design-paliad-test-strategy-2026-05-19.md — the test
infrastructure that would have caught mig 098 (digit-regex) and mig 099
(missing audit_reason) before the deploy hit prod.
Three new files + one route addition:
- Makefile: `make verify-migrations` (alias `verify-mig`) runs the
per-migration dry-run + boot smoke against TEST_DATABASE_URL. Fails
fast with a clear error if TEST_DATABASE_URL is unset so CI can't
silently pass a missing env var. `make test` and `make test-go`
cover the rest of the short / full Go suites.
- internal/db/migrate_test.go (TestMigrations_DryRun): walks every
pending *.up.sql in numeric order, applies each inside its own
BEGIN..ROLLBACK transaction, fails on the first SQL error with the
file name + Postgres error. "Pending" = greater than the scratch
DB's current tracker version, so fresh-DB CI runs verify everything
while developer scratch DBs only re-verify the new pending migration.
Always non-destructive — the rollback runs even on success.
- cmd/server/main_smoke_test.go (TestBootSmoke): boots the apply path
end-to-end, asserts (a) db.ApplyMigrations returns nil, (b) the
tracker advanced to the highest *.up.sql version on disk with
dirty=false, (c) GET /healthz on the registered mux returns 200.
The dry-run catches per-migration syntax errors; this catches the
apply+bind path the container actually runs.
- internal/handlers/handlers.go: adds a GET /healthz public route — a
no-auth, no-DB liveness probe. Used by the boot smoke; also safe
for any future orchestrator or uptime check.
Both live-DB tests gate on TEST_DATABASE_URL and skip cleanly without
it, matching the rest of paliad's live-DB test pattern.
Verification: go build ./... clean, go vet ./... clean,
go test -short ./internal/... ./cmd/... clean (all packages pass,
live-DB tests skip), bun run build clean (2436 i18n keys unchanged).
Per CLAUDE.md inventor → coder gate, NOT self-merged.
t-paliad-214. m walked all 9 questions live; deviated on Q2 (project-scope
floor = any team member, not associate), Q3 (retention 90d, not 7d), Q5
(paliadin_turns hard-excluded from org scope, not opt-in). Other 6
matched inventor picks. Net slice-plan deltas captured in §12.
Addendum after §10 captures m's picks (2026-05-19, via AskUserQuestion):
§8.1 bidirectional default: YES; §8.2 personal_only: KEEP first-class;
§8.3 MKCALENDAR: Slice 2 with Google-degrade; §8.4 soft caps: NONE in
v1 (add later if telemetry warrants); §8.5 admin view: don't ship;
§8.6 approval-flow remote-edit gap: separate task under t-138.
Net effect: drops the 20-warn/80-block UI guards from §6 and the
`read_only` flag from §3; Slice 2 gains MKCALENDAR + binding-count
telemetry; §8.6 fix filed separately so multi-cal slices stay clean.
m's 2026-05-19 picks via AskUserQuestion interview:
- Q1 budget: 60–90s gate, 3–4min full (inventor's call — m deferred)
- Q2 CI: Gitea Actions, gate tier only
- Q3 test DB: YouPC for devs + ephemeral docker for CI
- Q4 coverage: critical-path only, no % gate
- Q5 floor: Slices 1+4+5 before new feature work
- Q6 ownership: head decides + rotate per profile
All six matched inventor's recommendation. Slice 1 (migration
dry-run + boot smoke) starts first; Slices 4+5 in parallel after.
t-paliad-214. Covers scope definitions, format choices (xlsx + JSON + CSV
in one zip, deterministic, schema_version 1), authorization model
(global_admin / project-team-with-associate-floor / authenticated-self),
trigger model (sync personal+project, async org), storage on
PALIAD_EXPORT_DIR with 7-day retention, PII/GDPR posture, 3-slice plan,
and 9 open questions for m. No code touches — design only.
t-paliad-213 — six-layer pyramid (migration dry-run, Go/frontend unit,
frontend DOM, service live-DB, handler integration, Playwright E2E),
audit of current coverage (323 test funcs, 24 untested services, 53
untested handlers, 4/90 frontend modules), eight-slice tracer-bullet
roll-out, six open questions for m.
Read-only design phase per CLAUDE.md inventor gate — no test files,
make targets or CI configs touched. Awaiting m go/no-go on §5 slice
plan + §6 open questions before any coder shift.
Inventor design for letting users connect Paliad's CalDAV sync to N
external calendars per user, with scope filters (master / personal /
per-project / per-client / per-litigation / per-patent / per-case)
rather than today's single-target push. Splits credentials (per user,
unchanged) from bindings (new join table). Adds a per-target join for
push state so the same Appointment can live in multiple calendars at
once. Includes per-provider limit research (iCloud 100, Google ~100,
Fastmail no cap, Nextcloud 30 default), a 4-slice rollout plan, and 6
open questions for m. READ-ONLY design — no schema or code changes.
m's call 2026-05-19: opening /events with type=appointment was
defaulting status='all' which surfaces every past appointment in
the corpus. The default should hide past events; 'Alle (auch
vergangene)' is opt-in for the one user who actually wants the
historical view.
Replaces the default with the existing DeadlineFilterUpcoming bucket
(already implemented backend-side at internal/services/deadline_service.go:132
as 'today + future'). New status option 'upcoming' at the top of the
appointment list; existing 'all' moves to the bottom with a clearer
label that calls out 'incl. past'.
Deadlines unaffected — they still default to 'pending'.
i18n keys added in both DE + EN slots (events.filter.status.upcoming
'Ab heute' / 'From today'; .all reframed as 'Alle (auch vergangene)'
/ 'All (incl. past)').
m's call 2026-05-19: the /files/hl-patents-style.dotm link on the
anonymous frontpage shouldn't tempt visitors to try downloading. The
/files/{filename} route IS already auth-gated (302 to /login on
anon click), and the macro-update endpoint at /patentstyle/* stays
public for the in-Word update logic per m's note ('with knowledge
of the direct source link it needs to be available').
Authenticated users never see this page anyway — handleRootPage 302s
them to /dashboard. So removing the section costs them nothing and
removes the obvious affordance for anon visitors. ICON_DOWNLOAD
const dropped along with it.
The Downloads page itself (/downloads + Sidebar nav entry) stays —
that's auth-gated and works for logged-in users.
Leftover surface: /patentstyle/HL-Patents-Style.dotm is still anon-
downloadable (necessary for the Word macro's auto-update poll).
That's m's stated requirement — flagged as the known leak path for
anyone who knows the URL.
Hosts the manifest + .dotm that the Word ribbon's Check-for-Updates button polls. paliad.msbls.de is the primary endpoint; hihlc.msbls.de mirrors it (hihlc/main b871ded). Files live in frontend/public/patentstyle/, copied into dist/ by the frontend build. Cache-Control: no-cache via noCacheAssets so version.json never serves stale after a release.
m's observation 2026-05-18 (interactive session): toggling "Mit Nichtig-
keitswiderklage" surfaces the response rules (def_to_ccr, reply, rejoin,
…) but the triggering event itself — the act of filing the CCR — is
invisible. Per R.25 VerfO the CCR is filed AS PART OF the Statement of
Defence with the same 3-month deadline, so the corpus author (mig 028)
skipped it. UX problem: users see consequences without the cause.
**New rule** `upc.inf.cfi.ccr`:
- parent: `upc.inf.cfi.soc` (root anchor, same as SoD)
- duration: 3 months (same as SoD — no separate deadline)
- party: defendant
- legal_source: `UPC.RoP.25.1`
- condition_expr: `{"flag":"with_ccr"}`
- priority: **`informational`** — renders as a notice card, no save
action, no duplicate write into paliad.deadlines (the SoD's row
already covers the calendar date).
**Sequence reshuffle** — inserting at sequence_order=11 pushes
def_to_ccr 11→12 and app_to_amend 12→13 so the timeline reads
SoD → CCR → def_to_ccr → app_to_amend (cause before effect).
**Idempotency** — INSERT uses NOT EXISTS keyed on
(proceeding_type_id, submission_code, lifecycle_state='published');
UPDATEs are guarded by the source sequence_order so re-apply is a
no-op. audit_reason set via set_config('paliad.audit_reason', ...,
true) at the top per the mig 099 hotfix pattern.
Migration counter re-checked against origin/main + ls
internal/db/migrations/ | tail before picking 100 — per the friction
note from msg 2016.
Build hygiene: go build/vet clean; bun run build clean (no i18n
changes). Down.sql restores both sequence values + DELETEs the new
row. Branch: mai/fermi/interactive-session.
The /views/{slug} runner now mounts the same FilterBar primitive that
/events and /inbox use. The saved view's filter_spec becomes the bar's
baseline, axes are picked client-side per the view's data sources so a
deadline-only view exposes deadline_status, an approval-driven view
exposes approval_viewer_role + approval_status + approval_entity_type,
etc. Universal axes (time, personal_only, sort) always render.
Per-session tweaks overlay the saved baseline without mutating the
stored row; the URL round-trips state through the bar's existing codec
so deep-links share the active narrow. "Speichern als Sicht" stays
available on user-owned views so a tweaked narrow can be forked into a
new saved view.
Shape axis is intentionally excluded from the bar — the existing
top-of-page shape chip cluster (list / cards / calendar / timeline)
already plays that role and switching now mutates the cached render
spec without re-hitting the substrate.
Empty-state hint reuses the saved filter summary as before; the bar's
onResult handler hides all shape hosts when the rows array is empty.
shape-timeline-cv now wraps the chart host with a toolbar carrying
+/- zoom buttons and 1y/2y/all chips. Active zoom persists in the URL as
?tl_zoom=1y|2y|all (URL > render-spec range_preset > "1y" default), so
saved views still control the initial zoom but per-session navigation is
deep-linkable.
shape-timeline-chart paints lane labels inside a foreignObject containing
an HTML <div> with overflow:hidden + text-overflow:ellipsis + a title
attribute carrying the full text. Long project names no longer bleed
across the chart canvas; hover reveals the full label.
i18n: views.timeline.zoom.{label,in,out,1y,2y,all} (DE+EN).
shape-calendar now renders month, week, and day views with a chip switcher
above the grid. Active view + anchor date persist in the URL as
?cal_view=month|week|day&cal_date=YYYY-MM-DD so per-view navigation is
deep-linkable.
Month view: weekday header row now lives inside the same CSS grid as the
day cells (one shared grid-template-columns: repeat(7,1fr)), so day labels
no longer drift relative to the columns below. Day-number is a button
that switches to day view scoped to that date; +N more pill also drills
to day view. Individual row pills route to /deadlines/{id} /
/appointments/{id} via inner anchors with click stopPropagation so they
don't trigger the day-drill.
Week view: 7 columns, full row list per column (no 3-row cap), per-column
vertical scroll for busy days.
Day view: single chronological list. Prev/next-day nav reuses the same
toolbar; week/day views also expose a "Zurück zum Monat" link.
i18n: cal.view.month|week|day + per-view prev/next labels +
cal.day.back_to_month + cal.day.open_day + cal.day.no_entries (DE+EN).
Mig 099 (drop_with_po_flag) crash-looped paliad.de prod immediately
after deploy: the mig 079 trigger on paliad.deadline_rules raises
EXCEPTION 'audit reason required' on UPDATE when paliad.audit_reason
is unset. Original file (fermi, t-paliad-207) only had the UPDATE,
no set_config wrapper.
Patch: prepend the standard 'SELECT set_config(paliad.audit_reason,
...)' at the top so the trigger sees the reason. Same shape as every
other migration that mutates deadline_rules.
Manual recovery already applied via head MCP — UPDATE'd the 2 rows
with audit_reason set, marked tracker version=99 dirty=false,
force-restarted the container which booted clean. This commit aligns
the in-repo file with the recovered prod state. Idempotent: the
WHERE clause matches only rows that still carry with_po, so re-apply
is a no-op.
m's correction 2026-05-18: the R.19 Einspruch (preliminary objection)
should not be flag-gated. It's an always-available optional submission
the defendant can make once the SoC is served — same logic as the
appeal-spawn rules in t-paliad-203 F2.3 ("the appeal is always a
possibility"). Removing the gate makes the row a normal optional rule:
priority='optional' (unchanged, set by mig 095) gives the save-modal
the existing pre-uncheck behaviour without a separate checkbox.
**Migration 098** (idempotent): NULLs condition_expr on the two RoP.019.1
rows pinned by proceeding code (`upc.inf.cfi` + `upc.rev.cfi`). Re-apply
is a no-op via the WHERE clause matching the live shape. Live DB row
state will sync when Dokploy applies the migration on next deploy — no
raw prod-write this turn (lesson from the previous shift's friction note).
**Frontend cleanup** — removes the two flag rows added to
verfahrensablauf.tsx + fristenrechner.tsx in the parent t-paliad-207
commit (inf-po-flag-row, rev-po-flag-row), the readFlags()/calculate()
push branches, the syncFlagRows() show/hide entries, and the change
listeners. Drops the 4 i18n keys (deadlines.flag.inf_po + rev_po,
DE + EN). Bun build clean: 2417 keys (was 2419, -2 keys × 2 langs).
Branch: mai/fermi/interactive-session @ third commit on top of Path A.
m's 2026-05-18 ask: the 5 DE proceeding tiles followed three different
labelling conventions ("Verletzungsklage (LG)" / "Berufung OLG" /
"Nichtigkeitsverfahren" — instance in brackets vs not vs not even
present). Path A reshapes both the picker and the labels so a user
scanning "Deutsche Gerichte" sees the type→instance hierarchy at a
glance and every tile reads <court> (<procedural role>) in parallel.
**Picker structure (verfahrensablauf.tsx + fristenrechner.tsx):**
Inside the existing `<.proceeding-group data-forum="de">` block, the
single flat row of 5 tiles is now two sub-groups with mixed-case h5
headings — Verletzungsverfahren over LG/OLG/BGH, Nichtigkeitsverfahren
over BPatG/BGH. DE_TYPES split into DE_INF_TYPES (3) + DE_NULL_TYPES (2)
in both page shells.
**Labels (i18n.ts, DE + EN parallel):**
| Code | Old DE | New DE |
|--- |--- |--- |
| de.inf.lg | Verletzungsklage (LG) | LG (1. Instanz) |
| de.inf.olg | Berufung OLG | OLG (Berufung) |
| de.inf.bgh | Revision/NZB BGH | BGH (Revision / NZB) |
| de.null.bpatg | Nichtigkeitsverfahren | BPatG (1. Instanz) |
| de.null.bgh | Berufung BGH (Nichtigk.) | BGH (Berufung) |
Two new i18n keys carry the sub-group headings:
- deadlines.de.group.inf — "Verletzungsverfahren" / "Infringement proceedings"
- deadlines.de.group.null — "Nichtigkeitsverfahren" / "Nullity proceedings"
**CSS (global.css):**
New `.proceeding-subgroup` + `.proceeding-subgroup-heading` rules,
co-located with `.proceeding-group h4`. Sub-heading sits one tier below
the h4 (mixed-case, no upper-tracking) so the two-level hierarchy reads
at a glance.
**What this does NOT do** — the "one long sequence" combined-timeline
behaviour (m's same ask, larger scope: spawn rules + de-duplication +
multi-instance UI) is filed as m/paliad#41 and stays a separate
delivery. Per-instance tiles keep their meaning either way.
Build hygiene: go build/vet clean; bun run build clean (2419 keys, +2).
Five intertwined fixes m surfaced in the interactive session:
1. **Jurisdiction prefix on the picked proceeding** — the collapsed
summary chip and the result header now read "UPC Verletzungsverfahren"
/ "DE Verletzungsklage (LG)" instead of the bare proceeding name.
Disambiguates the 4 redundancies in the corpus once the picker
collapses. Driven by .proceeding-group[data-forum] which is already
on every group.
2. **Trigger Event label = root rule** — step 2's "Auslösendes Ereignis"
line now shows the first event in the proceeding (e.g. Klageerhebung,
Nichtigkeitsklage) instead of the proceeding name. Populated from
the calc response (isRootEvent=true) on every render; em-dash
placeholder while step 3 hasn't rendered yet. lang-change keeps it
coherent.
3. **Flag rows on /tools/verfahrensablauf** — Slice 1 of t-paliad-179
stripped the with_ccr / with_amend / with_cci toggles when it lifted
the shared renderer; they never came back. Lifted the 4 existing
rows from fristenrechner.tsx plus 2 new with_po rows (RoP 19.1
preliminary objection, mig 095) — same wiring + show/hide rules on
both surfaces. with_amend stays nested under with_ccr on upc.inf.cfi
(R.30 only with a CCR).
4. **Rule references → youpc.org/laws links** — new
BuildLegalSourceURL(src) maps the structured legal_source code to
the youpc permalink for the UPC corpus (UPCRoP / UPCA / UPCS today;
39 of 91 active rules carry UPC.RoP.* and now link). DE/EPA/EU
bodies have no youpc home yet and render as plain display text —
filed as m/paliad#39. Wired through UIDeadline.LegalSourceDisplay +
LegalSourceURL so deadlineCardHtml can render <a target="_blank"
rel="noopener"> when the URL is set.
5. **R.19 label: "Vorab-Einrede" → "Einspruch"** — m's correction. DE
only (EN canonical UPC RoP term stays "Preliminary objection").
Client-side change only — i18n + JSX fallbacks. The matching DB
rename on the two rule-name rows folds into joule's broader mig 097
(legal-citation backfill, t-paliad-208 follow-up). The live UPDATE
applied during the session is captured under that audit reason; the
no-op when joule's mig re-applies is harmless.
Build hygiene:
- go build ./... + go vet ./... clean
- new test TestBuildLegalSourceURL covers UPC corpus + DE/EPA/EU
fall-through + edge cases (empty input, malformed source)
- bun run build clean (2417 i18n keys total)
Rebased on origin/main @ d126913 (ohm's submission_code rename
workstream B) — no conflicts in this commit's surface area.
Branch: mai/fermi/interactive-session. NOT self-merged.
Recovery during the prod outage uncovered a second mig 098 bug: §6.2
assertion '0 NULL submission_code on active+published rows' counted
the 77 orphan rules (proceeding_type_id IS NULL, cross-cutting
Wiedereinsetzung / Schriftsatznachreichung pattern) and rejected the
migration. Patch: gate the NULL count on `proceeding_type_id IS NOT
NULL` so orphans pass through. Migration already applied to prod via
manual recovery with the same patched assertion; this commit aligns
the in-repo file with the deployed state.
Mig 098 (t-paliad-209, ohm) crash-looped paliad.de prod for ~2h: §6.1
assertion regex `^[a-z_]+\.[a-z_]+\.[a-z_]+\.[a-z_]+(\..*)?$` rejects
EPA rule codes that carry the statutory rule number in the suffix —
e.g. `epa.opp.boa.r106`, `epa.grant.exa.r71_3`, `epa.opp.opd.r116`,
`epa.opp.opd.r79_further`, `epa.opp.boa.entsch2`, `epa.opp.boa.r116`.
Migration's UPDATE step succeeds against these rows; the transactional
assertion blows them up; rollback leaves the migration tracker dirty
at version 98 and the container refuses to start.
Patch: allow `[a-z_0-9]` per segment instead of `[a-z_]` in both the
SQL assertion (mig 098 §6.1) and the matching Go shape regex
(submission_codes_shape_test.go). Same change in both spots so the
runtime sanity test stays aligned with the SQL invariant.
Manual recovery already applied: forced
`paliad.paliad_schema_migrations.version` back to 97 with `dirty=false`
so the next deploy retries mig 098 from scratch against the patched
file. No data state changed (mig 098 ran inside a transaction and
fully rolled back — snapshot table, prefix UPDATE, and column rename
all reverted).
go build ./... clean. TestProceedingCodeShapeRegexStandalone green.