Two editor regressions from the t-paliad-370 S2/S5 relayout, both rooted in
ONE CSS bug.
BUG 1 — preview pane wrapped under the sidebar with no space.
.submission-draft-grid was a two-column track (minmax(280px,360px) 1fr) but
the grid has THREE children: sidebar, section panel, preview. Before S5 the
section panel hid itself (display:none) when a draft had no sections, so two
tracks happened to suffice. S5 (t-paliad-370) made the panel ALWAYS present
(it signposts its empty state instead of vanishing), so all three children
became permanently visible — and the third (preview) wrapped onto a new grid
row under the sidebar, squeezed to the sidebar's width and pushed far down
the page (repro: preview at y=1340, w=360 under the sidebar).
FIX: three named-area tracks — sidebar | sections | preview
(minmax(240px,300px) minmax(0,1fr) minmax(0,1.4fr), preview widest). Explicit
responsive collapse: <=1200px keeps the sidebar in its own column and stacks
section+preview in the second; <=900px single column. grid-template-areas on
the container + grid-area on each child keep placement deterministic (no
auto-placement wrap). Verified at 1400/1100/800px in a built-bundle harness;
all three panels lay out cleanly, preview fully visible, both the with-sections
and empty-state (merge-path) cases.
BUG 2 — language DE/EN toggle 'does nothing'. ROOT CAUSE: NOT a wiring bug.
Runtime repro against the built bundle proves the handlers attach
(de/en.onchange set), a click fires PATCH {language:"en"}, the radio flips,
and the preview + header re-render correctly. The toggle only LOOKED dead
because its primary visible output — the live preview — was the very pane
bug 1 displaced off-screen under the sidebar. Fixing the grid restores the
preview to a visible column, so toggling DE/EN now visibly swaps it
(repro: preview text DE->EN with the pane at x=849,w=399, top of viewport).
No client/markup change needed; the S2 toolbar move preserved every id and
the t-paliad-276 wiring was intact throughout.
CSS-only. bun build clean (i18n scan clean). No backend touched.
docforge UX slice S5 (PRD §3 S5) — the final slice; completes the train.
- Section panel: instead of silently hiding when a draft has no sections
(the §1.4 2-vs-3-panel inconsistency), the panel now always signposts its
state. A Composer draft (base_id set) shows 'Dieser Entwurf hat noch keine
Abschnitte' + a '+ Abschnitt hinzufügen' affordance; a fixed-template draft
(merge path / uploaded template, base_id NULL) is told it uses a fixed
template with no editable sections. The section editor is now discoverable
and the panel's presence/absence is explicit.
- Live preview: unresolved placeholders render as a muted '‹noch leer: key›'
prompt instead of the harsh [KEIN WERT: key] marker — a preview-display
nicety done client-side in paintPreview. The EXPORTED .docx is unchanged and
keeps the real marker (an honest blank the lawyer fills in Word).
- global.css: .submission-draft-sections-empty / -note + .preview-empty.
Frontend only (submission-draft.ts + global.css), no backend.
bun build (i18n scan clean) + go vet ./... + go test ./... (15 ok, 0 fail).
docforge UX slice S4 (PRD §2.3). Extends the S3 /api/submission-preview
endpoint with a truthful page-image render behind the SAME modal+endpoint
shape (the swappable body region). Resolves the S3 caveat: anchors-only
Composer/Gitea bases now render their REAL letterhead/Rubrum styling instead
of falling back to a base-agnostic structural view.
Pipeline (PRD §2.3): build the .docx via the EXISTING export pipeline
(byte-faithful) → Gotenberg sidecar (.docx→PDF over HTTP, keeps the Go image
lean) → poppler pdftoppm (PDF→PNG-per-page) → cache + single-flight.
Code only — NO Dokploy/compose change (head provisions the sidecar + fonts).
When GOTENBERG_URL is unset or poppler is absent, Available()=false and the
endpoint gracefully falls back to S3 structural HTML, so merging this is safe
before the infra lands.
Backend:
- internal/services/preview_render.go: PreviewImageRenderer interface +
GotenbergRenderer (.docx→PDF→PNG) + PreviewImageCache (bounded FIFO,
single-flight per key, serialised conversions) + NewPreviewImageCacheFromEnv
(GOTENBERG_URL / PREVIEW_DPI). The PNG cache is regenerable, not a retained
document (no disk/DB persistence; rebuildable from inputs).
- /api/submission-preview gains fidelity=truthful: builds the truthful .docx
(editor = clone draft + base override → full export pipeline incl. Composer;
catalog = context bag → render), caches by (draft id+updated_at | code+
project) × base × lang × data-mode, returns page data-URIs. Structural
responses now carry truthful:false.
- exportSubmissionDraft + Export gain a pluggable missing-marker (nil = default,
so every existing export path is byte-identical); enables sample-data truthful
render. New SubmissionDraftService.{ExportWithMarker,RenderContextPreviewDocx}.
- Unit tests: cache hit/miss, single-flight, eviction, availability, gotenberg
multipart request shape (httptest, no poppler needed).
Frontend:
- base-preview-modal.ts requests fidelity=truthful; renders <img> page(s) with
a ‹ n/N › prev/next pager (instant — pages held client-side); falls back to
the S3 structural sheet when truthful:false. CSS for .base-preview-img +
page-nav.
bun build (i18n scan clean) + go vet ./... + go test ./... (15 ok, 0 fail;
+6 new service tests).
docforge UX slice S3 (PRD §2.1 / §3 S3). A shared, body-attached modal lets
the lawyer browse template bases, flip output language + data-mode, and — in
the editor — commit a base with 'Diese Basis verwenden', replacing the blind
<select> as the chooser. Opened from the editor 👁 button AND both catalog row
kebabs (the S1/S2 stubs are now live).
Body renders the EXISTING structural preview on cheap rails; the body region is
swappable so S4 drops a truthful .docx→image render behind the same modal +
endpoint shape. Spinner + pager are the shell S4 expects.
Backend (minimal, no truthful render — that's S4):
- GET /api/submission-preview?base&code&lang&data&draft?&project? → {preview_html}.
Draft+mine uses the draft's resolved bag (previews a base the draft has NOT
committed to — no persistence); else a fresh context bag (project-less fill,
t-paliad-364). data=sample swaps a sample missing-marker so unresolved keys
render readable sample text, not [KEIN WERT].
- SubmissionDraftService.RenderPreviewWithMarker + RenderContextPreviewHTML
(thin: reuse BuildRenderBag / vars.Build + RenderHTML).
- resolvePreviewTemplateBytes: tpl:→carrier, base_id→bytes IFF it carries merge
placeholders, else fall back to the code's merge template (anchors-only
Composer bases can't be shown as structural HTML — their letterhead/styling
surfaces in S4's truthful render). sampleMissingMarker by key suffix.
- Unit tests: sampleMissingMarker + normalizePreviewLang.
Frontend:
- New shared client/base-preview-modal.ts (built once, body-attached; serves
editor + project tab + global picker). Base-switcher, DE/EN + meine/Beispiel
toggles, swappable body, spinner, pager stub, conditional 'Diese Basis
verwenden' (editor commits via onBaseChange; catalog = look-only).
- Editor 👁 button un-stubbed + wired; both catalog kebab 'Vorschau' items
un-stubbed + wired. global.css: .base-preview-* modal styles.
bun build (i18n scan clean) + go vet ./... + go test ./... (15 ok, 0 fail).
docforge UX slice S2 (PRD docs/plans/prd-docforge-ux-2026-06-01.md §3).
De-densifies the draft editor (G1-d): draft management + template controls
move out of the variable sidebar into a horizontal, wrap-friendly header
strip above the working grid. The sidebar then holds only the fill-in work
— 'Aus Projekt importieren', parties, and the variable fields.
- submission-draft.tsx: new .submission-draft-toolbar strip carrying the
switcher (+ '+ Neuer Entwurf'), name (+ Löschen), Stichwort (+ hint),
Vorlagenbasis picker, language toggle, fallback notice + savestatus. The
'Als .docx exportieren' button stays top-right in the header.
- New 👁 Vorschau button next to the base picker (id
submission-draft-preview-base-btn), disabled 'Bald verfügbar' stub — S3
wires it to the truthful base-preview modal (PRD §2).
- global.css: .submission-draft-toolbar* layout + .submission-draft-base-
controls (select + 👁 on one row); single-column collapse < 900px.
- i18n: submissions.draft.base.preview(.soon) (DE/EN).
Pure relayout — NOT a behavior change. Every control keeps its id, so the
existing wiring is untouched (switcher, name save, keyword→composer_meta,
base picker PATCH base_id, language autosave, savestatus). Grounded in the
2-panel reality (§1.4): the conditional Abschnitte section panel is
unchanged (still display:none when a draft has no sections; S5 addresses
its discoverability). TS/CSS only, no backend.
bun build (i18n scan clean) + go vet ./... + go test ./... (15 ok, 0 fail).
docforge UX slice S1 (PRD docs/plans/prd-docforge-ux-2026-06-01.md §3).
Kills the invisible two-equal-buttons confusion (G1-b) on both catalog
surfaces, consistently, while keeping each surface's distinct context.
- New shared kebab helper frontend/src/client/row-action-menu.ts: an
accessible ⋯ trigger + body-attached, position:fixed popover (the
.entity-table-wrap clips with overflow-x:auto, so the popover is
body-attached like the event-card choices popover). Outside-click /
Escape / scroll / resize close it; one open at a time.
- Project Schriftsätze tab (client/submissions.ts): primary 'Entwurf
öffnen' (was 'Bearbeiten') + ⋯ menu { Direkt exportieren (.docx) [was
the 'Generieren' button], Vorschau Vorlagenbasis [disabled stub, S3
wires it] }. onGenerateClick→generateAndDownload (busy-cursor feedback,
no button to relabel).
- Global picker (client/submissions-new.ts): primary 'Entwurf starten'
(project-less free-start, kept first-class) + ⋯ menu { Mit Projekt
verknüpfen… [the modal picker], Vorschau Vorlagenbasis [disabled stub] }.
- i18n: new projects.detail.submissions.action.open (DE/EN); kebab item
labels inline isEN() (matches the picker's dynamic-row convention).
- CSS: .row-action-menu* in global.css.
The 'Vorschau Vorlagenbasis' entries are disabled stubs ('Bald verfügbar')
until S3 lands the truthful base-preview modal. TS/CSS only, no backend.
bun build + go vet ./... + go test ./... clean.
Audit of the end-to-end docforge journey (project Schriftsätze tab, global
/submissions/new picker, draft editor, /admin/templates authoring) + a
prioritized UX plan. Captures m's grill answers + 4 option-picker decisions:
- base preview = truthful (LibreOffice .docx→image, on-demand+cached), via a
'Vorschau' button → modal with a base-switcher
- catalog rows = one primary 'Entwurf öffnen' + ⋯ kebab (kills the invisible
Bearbeiten-vs-Generieren split)
- editor meta → header toolbar (de-densifies the sidebar)
- free-start stays first-class; entry points kept distinct-but-consistent
- live editing preview stays structural (only the base preview is pixel-true)
Resolved the 2-vs-3-panel discrepancy: the Abschnitte panel is display:none
when a draft has no seeded sections, so m correctly sees 2 panels.
Design only — no engine redesign, no code. Tracer-bullet slice train S1..S5.
m mandate (2026-06-01): 'I dont want that formatting in code! it should use
our word files and styles.' The t-paliad-364 generator synthesised a flat
Rubrum in Go, borrowing only the firm's style NAMES. The HLC Patents Style
.dotm already contains the real, firm-authored letterhead + Rubrum in Word —
header table (logo via header2 + address blocks), case-info table, recitals
table — all in HLCpat- styles.
This rewrites gen-hl-skeleton-template to LIFT that authored layout verbatim:
- ConvertDotmToDocx strips macros → clean .docx carrier (idempotent on .docx)
- footer1 firm-NAME SDT → {{firm.name}} (A-S3); footer2 legal block kept
- document.xml body kept verbatim through the recitals table; the TOC +
checklist demo + 'Template Info And Manual' style-guide section truncated
- carrier <w:document> open tag + <w:sectPr> reused → header1/2 + footer1/2 +
titlePg letterhead wiring preserved
- structure-aware, occurrence-ordered paragraph walk swaps the firm's example
text for paliad merge tokens (parties/caption/project/user/today/proc-event);
transformed paragraphs keep their <w:pPr> verbatim — the named style supplies
formatting, none authored in code
Reconciles t-paliad-366 item 1 (HLpat→HLCpat drift): carrier + Rubrum both HLCpat-.
DATA GAP flagged to m: no structured HL office postal addresses + no court_id FK
(project.court is free text), so the sender address block + recipient court
address lines are kept as the .dotm's authored editable text; only the court
NAME is wired to {{project.court}}. Rules flip to placeholders when that data lands.
Verified: HasMergePlaceholders=true (de+en); header2/footer2/media/styles.xml/
theme/numbering byte-identical to the .dotm; package integrity clean (macros
stripped, no dangling rels, main type demoted); merge dry-run fills with zero
leftover {{}} / zero [KEIN WERT]. go vet + go test ./... + bun build green.
P3(b) fill-what-we-can (submission_vars.go):
The project-less branch of SubmissionVarsService.Build ran only firm/today/
user/proceduralEvent resolvers, so caption.*/project.proceeding.* never
populated and every Rubrum value rendered [KEIN WERT]. The rule is already
loaded on this path and carries ProceedingTypeID. Now the branch loads the
proceeding type via loadProceedingType(rule.ProceedingTypeID) (tolerates a nil
id) and appends a proceeding-only projectResolver{project:nil} + a
captionResolver{project:nil} — both nil-project safe. Result: caption heading /
designations / versus / subject + the proceeding line fill from the
submission_code's proceeding; only party names / case number / court stay blank
for the lawyer. Preserves the "Ohne Projekt" affordance (t-paliad-243).
addProjectVars is now nil-project safe (guards the project.* direct fields,
keeps the pt-driven project.proceeding.* block) so projectResolver can serve as
the proceeding-only resolver. Pinned by TestProjectlessFill_* +
TestAddProjectVars_NilProjectFillsProceedingOnly (no DB).
P3(a) styling — merge-safe styled firm-skeleton (scripts/gen-hl-skeleton-template):
Generation landed on docx.BuildFallbackSkeleton (generic Heading1/2/Normal)
because the firm-skeleton's body had been repurposed into an anchors-only
Composer base, which HasMergePlaceholders rejects. Rewrote the generator to take
the deployed clean .docx carrier and replace ONLY word/document.xml with a clean
caption-driven Rubrum that uses the firm Rubrum styles (Table-Recitals-Party/
PartyDetails/PartyRoles/Sequencers, Heading-H2, Signature, Body-B0) and the same
{{key}}/{{caption.*}} placeholders the in-process fallback uses — preserving the
carrier's styles/theme/numbering/letterhead/logo and its sectPr verbatim. Adds a
-lang flag (DE _firm-skeleton.docx, EN _skeleton.en.docx) and auto-detects the
firm style prefix (HLpat-/HLCpat-) so it stays correct across the .dotm rebrand
drift. The resolver's tier-4/tier-3 merge-safe guard auto-prefers the restored
templates — no handler change.
Dry-run (TEST_DATABASE_URL, de.inf.lg.erwidg): project-less render fills caption
heading="In dem Rechtsstreit"/Klägerin/gegen/Beklagte/Patentverletzung + the
proceeding line, only party/case/court blank; full-project render additionally
fills parties + case number + court. Both carry the HLpat-Table-Recitals-* /
HLpat-Heading-H2 / HLpat-Signature styles, HasMergePlaceholders=true, no
{{#section}} junk.
P1: 8 anchor/trigger rows (Zustellung des Urteils / Veröffentlichung der
Erteilung) were mislabeled event_type='filing'+primary_party='both' and leaked
into the /submissions/new draftable-submission picker. Migration 164 re-kinds
them to event_type='decision'+primary_party='court', aligning them with the 16
sibling court-act rows the model already has; the picker's event_type='filing'
filter then excludes them. Defensive guard added to loadSubmissionCatalog
(primary_party IS DISTINCT FROM 'court') as belt-and-braces against future drift.
Safety: the only event_type coupling for these rows is ruleAnchorKind
(projection_service.go) — they now anchor as appointments (correct, sibling-
consistent); deadline computation keys off is_court_set not event_type, and the
child sequence guard parentHasAnchoredActual UNIONs both anchor tables, so no
chain breaks. Verified: catalog 113->105, 8 court acts gone, 48 'both' party
submissions (incl. UPC appeal briefs) retained.
P2: the grouped picker table already renders a correct colspan group-header row
(a911a2d); the defect was contrast — the band used --color-bg-subtle, the SAME
token as the thead, so groups read as undelimited floating text. New
--color-bg-group-header token (cool/deeper in light, raised-cream in dark) +
heavier top divider + neutral left accent + darker label make each proceeding a
distinct section. Live deployed bundle confirmed current (not stale).
t-paliad-361, follow-up to t-paliad-358 A-S2. m ruled on the 7 lexy-wording
flags (AskUserQuestion 2026-06-01 14:30). Most flags CONFIRMED the live
wording; three changes land here, all caption (Rubrum) wording, all in one
reversible migration 163.
Change 1 — UPC appeal responding party (EN): 'Appellee' → 'Respondent'.
Fixed at the data source: the mig-137 role-label override on
upc.apl.unified (the only place 'Appellee' was stored). The caption
resolver's instance-derived EN fallback already said 'Respondent', so no
code change. DE side (Berufungsbeklagter) untouched per m.
Change 2 — restore the standalone 'Streitpatent: {{project.patent_number_upc}}'
(DE) / 'Patent in suit:' (EN) line in the upc-formal Composer caption seed,
dropped in A-S2 (mig 161). Keeps the parametric 'In der Sache' heading (m did
not revert that). Only the upc-formal base is touched; grouped with the case
number ahead of {{project.court}}.
Change 3 — backfill lexy-confirmed role-label overrides for the four DE
appeal/nullity proceedings that carried none, so designations are correct
even when project.instance_level is unset (statute-grounded: §§ 511/542/544
ZPO, §§ 81/110 PatG; bracketed-inclusive gender style):
de.inf.olg Berufungskläger(in) / Berufungsbeklagte(r) // Appellant / Respondent
de.inf.bgh Revisionskläger(in) / Revisionsbeklagte(r) // Appellant / Respondent
de.null.bpatg Nichtigkeitskläger(in) / Beklagte(r) (Patentinhaber(in)) // Nullity claimant / Defendant (patent proprietor)
de.null.bgh Berufungskläger(in) / Berufungsbeklagte(r) // Appellant / Respondent
Updates submission_vars_caption_test.go: adds EN assertions + cases pinning the
Respondent change and the four backfilled designations (each with instance_level
unset, proving the override path). go vet + go test ./... + bun build clean.
A-S3 part 1 (merge-fallback letterhead, in-repo): the fallback skeleton
(docx.BuildFallbackSkeleton) gains a minimal Word page-header letterhead
(word/header1.xml) carrying only {{firm.name}}, filled from branding by the
variable bag. A generated fallback-path document now repeats a correct,
firm-agnostic firm identity on every page (the firm name moved out of the body
into the header; no hard-coded firm name anywhere). Wired the header part:
Content_Types override + document.xml.rels relationship (rId2) + sectPr
headerReference; document gains xmlns:r. Deliberately minimal — the fallback is
a starter, not full firm chrome.
A-S3 part 2 (firm-skeleton .docx, mWorkRepo — separate commit): _firm-skeleton.docx
footer1's "Firm name" SDT content control hard-coded "Hogan Lovells" → now
{{firm.name}}, filled by the Composer's final renderer pass / merge.go (both run
SubmissionRenderer over header/footer parts). Surgical <w:t> edit + repack (all
38 parts preserved, verified renders + fills cleanly, integrity OK). Pushed to
HL/mWorkRepo as mAi (commit 5a3a1722).
Path taken: option (a) for the firm NAME (cleanly placeholderised). NOT option
(a) for footer2's HL legal-entity boilerplate (registered no. OC323639, LLP
structure, 40+ office cities) — token-swapping the name into it would assert
false legal facts for a non-HLC firm; left intact and flagged (needs per-firm
legal-footer config, not templating). No corrupt .docx shipped.
Completes the Rubrum + letterhead auto-fill Option A train (A-S1 signature_block
+ generate-fallback fix; A-S2 parametric caption.*; A-S3 firm-agnostic letterhead).
Tests: fallback header asserts {{firm.name}} present + renders firm-agnostically
(de+en); patched firm-skeleton verified to render + fill (transient check).
go vet ./... + bun build clean; touched packages green.
Completes the nomen train (S1–S5). Adds the FIRM tier of the name-composition
precedence chain — per-document → user → FIRM → system (PRD §3.1/§3.2) —
mirroring firm_dashboard_default exactly.
Storage + service:
- Migration 162: paliad.firm_name_compositions singleton (id=1, CHECK id=1,
RLS read-all + service-role writes) — same shape as firm_dashboard_default
(mig 117), holding a validated { artifact_id: Composition } jsonb map.
- FirmNameCompositionService (Get/Set/Clear) + getFirmNameCompositions /
setFirmNameCompositions / clearFirmNameCompositions singleton helpers in
name_composition_spec.go.
Resolution:
- resolveComposition is now variadic over ordered specs (user, firm); first
valid wins, else system default. Existing single-spec callers unchanged.
- Render path threads the firm tier: renderSubmissionDraftTitle /
RenderSubmissionFilenameFor gain a firm param; newDraftName +
submissionDownloadFilename load it (nil-safe). A firm default thus changes
the effective name for every user without a personal override.
Admin surface (mirrors firm_dashboard_default):
- GET/PUT/DELETE /api/admin/name-compositions{/artifact_id} (adminGate) read
back / set / clear the firm default per artifact.
- /settings Namensschemata cards gain an admin-only "Firmenstandard" block
(set from the current template field / clear) revealed via is_admin, plus a
"Firmenstandard" badge for non-admin users whose effective name comes from
the firm tier. SettingsNameArtifact now resolves user→firm→system and
exposes firm_is_set/firm_template.
Tests: pure precedence (user>firm>system) + firm-tier view + live firm
round-trip/Validate-rejection (via db.ApplyMigrations). go vet, go test ./...,
bun build all clean; gated live tests green against TEST_DATABASE_URL.
NOTE (merge ordering): golang-migrate is forward-only. Migration 162 must not
reach a DB before bohr's 161 (Rubrum Composer seed) exists, or 161 will be
skipped (current>161 → never applied). Merge 161 before/with 162.
Browser Playwright of the admin firm controls deferred to post-deploy
mai-tester — shared Supabase login wall blocks pre-merge browser login (same
ceiling as t-paliad-354).