diff --git a/docs/plans/prd-docforge-ux-2026-06-01.md b/docs/plans/prd-docforge-ux-2026-06-01.md new file mode 100644 index 0000000..8ec4a77 --- /dev/null +++ b/docs/plans/prd-docforge-ux-2026-06-01.md @@ -0,0 +1,344 @@ +# PRD — docforge UX: truthful base preview + a coherent submission-drafting flow + +**Task:** t-paliad-369 (m 2026-06-01 17:16) · **Author:** leibniz (inventor) · **Date:** 2026-06-01 +**Status:** DESIGN — awaiting head's go/no-go on the coder shift. **No code in this task.** +**Scope:** the **UX/flow around** the generation engine. The engine itself (Rubrum styling, +caption, letterhead from the HLC `.dotm`) was made correct today — t-paliad-364 / -365 / -367 — +and is **explicitly out of scope to redesign.** + +**Reading order for the reviewer:** §0 (m's decisions) → §1 (journey map + hacky inventory) → +§2 (the base-preview feature, with wireframes) → §3 (prioritized plan) → §4 (out of scope). + +--- + +## §0 m's decisions (2026-06-01) + +m was grilled in prose on the UX *vision*, then answered four concrete option-pickers. +Both rounds are folded in here; the open-question record is §5. + +### 0.1 Prose-grill answers (UX vision) + +| # | Question | m's answer | +|---|---|---| +| G1 | What reads as "hacky"? | **(a) blind base dropdown, (b) invisible Bearbeiten-vs-Generieren, (c) preview ≠ real output, (d) dense panels — ALL bad.** NOT (e) `[KEIN WERT]` walls, NOT (f) duplicate catalog. | +| G1′ | Panel count | **m sees TWO panels, not three.** (Resolved — see §1.4: the section panel is conditional and silently absent for his drafts.) | +| G2 | Akte-first vs free-start | **Keep free-start first-class.** No Akte gating. Teach project-less drafts to fill what they can from the proceeding (already done, t-paliad-364). | +| G3 | Base-preview fidelity | **Truthful.** "truthful would be awesome" — a real image of the actual Word page (letterhead logo, fonts, Rubrum table). | +| G4 | Editor live-preview fidelity | **Structural suffices** ("a, I think") — show where vars/prose land. The live editing preview stays approximate; pixel-fidelity is **only** required for the base preview. | +| G5 | Entry points | **Keep distinct, make consistent.** "in a project it is a bit different than the free approach." Don't converge the project tab and the global picker. | + +**The load-bearing split (G3 vs G4):** *base preview = truthful (pixel-true .docx render)*; +*live editing preview = structural (approximate HTML)*. The truthful infra built for the base +preview *could* later upgrade the editing preview, but m does not require that now. + +### 0.2 Concrete option-picker answers + +| # | Decision | m's pick | Note | +|---|---|---|---| +| Q1 | Base-preview render infra | **LibreOffice on-demand + cached** | Renders the *actual* base with the draft's real data (`.docx`→PDF→image), cached. Truest "what you preview is what you generate". Cost: headless LibreOffice in the container + cold-render latency — scoped in §2.3. | +| Q2 | Where the preview surfaces | **"Vorschau" button → modal** | Not an always-on inline pane. Pairs cleanly with on-demand rendering — we only render when the lawyer asks. | +| Q3 | Bearbeiten vs Generieren | **One primary + ⋯ menu** | Primary "Entwurf öffnen"; a kebab offers the fast "Direkt exportieren" path. Kills the two-equal-buttons ambiguity, keeps the capability. | +| Q4 | Editor sidebar density | **Meta → header toolbar** | Draft-meta (name, keyword, base+Vorschau, language) moves to a top header strip; the sidebar keeps only the fill-in work (parties + variables). | + +--- + +## §1 Current journey map + the hacky inventory + +Three entry points, one editor, one authoring page. All gated behind auth; knowledge-platform +pages are separate. Grounded in the live tree (verified 2026-06-01). + +### 1.1 Entry A — the project "Schriftsätze" tab +`frontend/src/client/submissions.ts` · per-project, opened from a project detail page. + +- Shows the **full cross-proceeding catalog** grouped by proceeding, with the project's own + proceeding pinned at the top (lime border, " (dieses Projekt)" suffix) — m's 2026-05-23 decree. +- **Each row carries two buttons:** `Bearbeiten` (→ editor at `/projects/{id}/submissions/{code}/draft`) + and `Generieren` (POST `…/generate` → immediate `.docx` download, **skipping the editor**). +- A `universell` badge marks rows without a dedicated per-code template. + +**Hacky (G1-b):** the two buttons read as equals, but their behaviour is wildly different — one +opens an editor, the other silently downloads a file. And the one-click `Generieren` runs the +**merge path** (`onGenerateClick`, `submissions.ts:212`), which historically produced an *unfilled* +doc (the t-paliad-363 P3 finding); even after today's fixes, "download a finished doc without ever +seeing it" is a foot-gun next to "open the editor". + +### 1.2 Entry B — the global `/submissions/new` picker +`frontend/src/submissions-new.tsx` + `client/submissions-new.ts` · cross-proceeding catalog. + +- Search box + proceeding chips + a grouped, read-only catalog table (just got the filter + + group-header contrast fixes, t-paliad-365). +- Each row: **"Entwurf starten"** → choose **"Ohne Projekt"** (jump straight to the draft) or + **"Mit Projekt verknüpfen"** (a project-picker modal → project-scoped draft). + +**Consistent-but-distinct (G5):** this serves a different moment than Entry A — *browsing the whole +catalog and starting fresh* vs *working inside a known case*. m wants these kept distinct. The job +is **consistency of affordances** (same row buttons, same naming, same kebab), not convergence. +The duplicate-catalog concern (G1-f) m explicitly waved off. + +### 1.3 Entry C — the draft editor +`frontend/src/submission-draft.tsx` (shell) + `client/submission-draft.ts` (2873-line bundle). + +Layout today (`.submission-draft-grid`): +- **Sidebar** (`aside.submission-draft-sidebar`): draft switcher + "+ Neuer Entwurf" → name → keyword + (filename) → **base picker** → language toggle → save status → "Aus Projekt importieren" → party + picker → **~20 variable fields**. Everything stacked vertically. +- **Section list** (`section.submission-draft-sections-wrap`, **`display:none` by default**) — the + Composer prose-section editor. Painted *only* when the draft has seeded section rows. +- **Preview pane** (`section.submission-draft-preview-wrap`): a read-only, lossy HTML render + (`paintPreview`, `submission-draft.ts:1209`; `preview_html` from the server). + +**Hacky (G1-a):** the base picker is a bare `` as the primary base-selection surface. +- **Daten toggle:** *meine Daten* renders the draft's resolved bag (truthful to the export); + *Beispiel* substitutes canned sample data (Mustermandant ./. Musterbeklagte, Az. 4c O 12/23) so a + fresh/project-less draft previews a *full* page instead of `[KEIN WERT]` markers. Default: *meine + Daten* when the draft has resolved values, *Beispiel* when it is essentially empty. +- **Paging:** multi-page docs render page-by-page (`‹ Seite n / N ›`). + +### 2.2 Where the button lives + +Per Q4, the base picker moves into the **editor header toolbar**, and the "Vorschau" button sits +right next to it (§3 S2 wireframe). The same modal is also reachable from a **catalog-row kebab** +("Vorschau Vorlagenbasis", §3 S1) so a lawyer can eyeball a base *before* even opening the editor. + +### 2.3 The truthful-render infra (Q1 = LibreOffice on-demand + cached) + +**Pipeline (server-side, new `GET` preview endpoint):** + +1. Resolve the request: `(draft_id | base identity, lang, data-mode)`. +2. Run the **existing export pipeline** to produce `.docx` bytes — *the same compose/merge code an + export uses*, so the preview is byte-faithful by construction. (Project-less / sample-data mode + feeds a canned bag; "meine Daten" feeds the draft's resolved bag.) +3. `soffice --headless --convert-to pdf` (headless LibreOffice) → PDF. +4. PDF → PNG per page (poppler `pdftoppm`, or equivalent) at a sensible DPI. +5. Return the PNG(s); the modal renders them. + +**Caching** (this is what makes "on-demand" affordable): +- Key on `(template identity, lang, data-mode, hash(resolved-bag))`. Identical inputs → cached PNG, + no re-render. The bag-hash means editing a variable invalidates only that draft's preview. +- Sample-data mode caches per `(base, lang)` only (data is constant) — so base *browsing/compare* is + effectively free after the first render of each base. +- Cache store: on-disk under the response/temp dir, or a small table — coder's call. No binary is + retained as a *document* (the §0.5.7 no-retention invariant is about exported documents; preview + PNGs are a regenerable cache, not a stored artifact — flag for the coder to confirm framing). + +**Cost / risk to flag for m + coder:** +- **Container dependency:** headless LibreOffice (~hundreds of MB) must be added to the Dokploy + image (or a sidecar). This is the single biggest cost of the truthful path. *Recommend a sidecar/ + separate stage so the main Go image stays lean — coder evaluates.* +- **Cold-render latency:** first render of a given `(base, lang, data)` is seconds (LibreOffice + spin-up + convert). The modal shows a spinner ("⟳ wird gerendert…"); the cache makes repeats + instant. A warm-cache pass over the 4–5 known bases in *sample-data* mode can pre-render the common + cases at deploy time. +- **Concurrency:** LibreOffice headless is single-instance-touchy; serialise conversions through a + small worker/queue (one `soffice` at a time, or a pool). Flag for the coder. + +**Tracer-bullet sequencing (important):** the modal **UX ships before the LibreOffice infra** — see +§3 S3 (modal scaffold rendering the *existing structural HTML* first) → S4 (swap the modal body to +the real PNG once LibreOffice lands). This de-risks the heavy infra: the base-compare UX is usable +and reviewable immediately, and the truthful render drops in behind the same modal. + +--- + +## §3 Prioritized UX-improvement plan (tracer-bullet first) + +Cheap → meaty. Each slice independently shippable and independently reviewable. **S1/S2 are the +quick wins that fix three of m's four "hacky" complaints with no new infra.** + +### Slice 1 — Catalog row: one primary + ⋯ menu, consistent across both entry points *(quick win — kills G1-b, delivers G5-consistency)* +TS/CSS only, no backend. +- Replace the two equal buttons with a **primary `Entwurf öffnen`** + a **`⋯` kebab**: + `Direkt exportieren (.docx)` and `Vorschau Vorlagenbasis`. +- Apply the **same row component / vocabulary** to the project Schriftsätze tab + (`client/submissions.ts`) *and* the global picker (`client/submissions-new.ts`) so they read + consistently — while keeping each surface's distinct context (own-proceeding pin on the tab; + search + chips + project-link modal on the global picker). +- Drop the bare `universell` jargon badge for a clearer tooltip/label. + +``` +Klageerwiderung Beklagte § 277 ZPO [ Entwurf öffnen ] [ ⋯ ] +de.inf.lg.erwidg └─┐ + • Direkt exportieren (.docx) + • Vorschau Vorlagenbasis +``` + +### Slice 2 — Editor header toolbar: meta out of the sidebar *(quick win — kills G1-d)* +TS/CSS layout, no backend. +- Lift draft-meta (switcher, name, keyword, **base picker + 👁 Vorschau button**, language) into a + **header strip** above the working area. Sidebar keeps only **parties + variables** (the fill-in + work). Export button stays top-right. + +``` +┌─ Klageerwiderung · de.inf.lg.erwidg ───────────────────────────────────────────┐ +│ Entwurf:[ Entwurf v2 ▾ ][+ Neu] Name:[__________] Stichwort:[__________] │ +│ Vorlagenbasis:[ HLC Briefkopf ▾ ][👁 Vorschau] Sprache:(•DE)(EN) [Als .docx ⤓]│ +├──────────────────────────────────┬──────────────────────────────────────────────┤ +│ PARTEIEN │ VORSCHAU (Struktur — wo Daten/Text landen) │ +│ ☑ Mustermandant (Klägerin) │ [letterhead] │ +│ ☑ Musterbeklagte (Beklagte) │ In dem Rechtsstreit … │ +│ VARIABLEN │ Az. «project.case_number» │ +│ project.case_number [________] │ … │ +│ … │ │ +└──────────────────────────────────┴──────────────────────────────────────────────┘ +``` +(When the draft has sections, the Composer "Abschnitte" panel sits between sidebar and preview — +see S5 for making that presence explicit.) + +### Slice 3 — Vorschau modal scaffold (structural render first) *(tracer bullet for the base preview)* +- Build the modal shell (§2.1): base-switcher, Sprache toggle, Daten toggle, paging frame, + "Diese Basis verwenden", spinner state. +- Wire the `👁 Vorschau` button (S2 header) and the row kebab (S1) to open it. +- **Body initially renders the existing structural `preview_html`** for the selected base — so the + whole base-compare + choose UX is live and reviewable *before* any LibreOffice work. + +### Slice 4 — Truthful render: LibreOffice on-demand + cached *(the meaty infra — delivers G3)* +- Add the preview endpoint + the `.docx`→PDF→PNG pipeline + cache (§2.3). +- Add headless LibreOffice to the deploy (sidecar recommended) + a single-flight conversion worker. +- **Swap the modal body** from structural HTML (S3) to the real page PNG(s). Same modal, same UX. +- Warm-cache the known bases in sample-data mode at deploy. +- *This is the slice that carries the container-dependency + latency cost — gate it on m's explicit + OK for the LibreOffice dependency.* + +### Slice 5 — Section-panel discoverability + small honesty polish *(polish)* +- Make the conditional "Abschnitte" panel **explicit**: when a draft *could* have sections but has + none, show a slim empty-state ("Dieser Entwurf hat keine Abschnitte — [+ Abschnitt hinzufügen]") + instead of silently rendering nothing — so the section editor is discoverable (fixes the §1.4 + inconsistency). When sections genuinely don't apply (pure merge-path draft), say so once. +- Light `[KEIN WERT]` softening in the *live* preview (e.g. a muted "‹noch leer: …›" treatment) + so honest gaps read as gentle prompts, not errors. (G1-e is low priority; keep it light.) + +**Ordering rationale:** S1+S2 ship the visible "less hacky" wins immediately (rows + editor layout), +no infra. S3 lands the base-preview *experience* on cheap rails. S4 makes it *truthful* — the one +slice with real infra cost, isolated and gated. S5 is discoverability polish. + +--- + +## §4 Out of scope + +- **Implementation / code / migration SQL.** Design only. +- **Redesigning the generation engine** — Rubrum/caption/letterhead/HLpat styling are correct as of + t-paliad-364/-365/-367. This PRD touches only the UX *around* it. +- **Upgrading the LIVE editing preview to pixel-true** — m: structural suffices (G4). The truthful + infra (post-S4) *could* later power a "truthful full-document" view in the editor, but that is a + future opt-in, not this work. +- **Converging the two entry points** — m: keep distinct (G5). We make them *consistent*, not one. +- **Akte-first gating** — m: free-start stays first-class (G2). +- **`/admin/templates` authoring redesign** — only consistency touch is that uploaded templates must + be previewable via §2; no workflow rework. +- **Multi-user concurrent editing** of one draft. +- **The `[KEIN WERT]` product question** (require-Akte vs fill-what-we-can) — already resolved by + t-paliad-364 (fill-what-we-can); not reopened here. + +--- + +## §5 Open-question record (historical) + +These were the open questions before m ratified them; kept for the record. All are now closed in §0. + +- **OQ-G1** What specifically feels hacky? → closed (G1: a/b/c/d, not e/f). +- **OQ-G2** Akte-first vs free-start? → closed (free-start first-class). +- **OQ-G3** Base-preview fidelity? → closed (truthful). +- **OQ-G4** Editor live-preview fidelity? → closed (structural suffices). +- **OQ-G5** Converge or keep entry points distinct? → closed (distinct + consistent). +- **OQ-Q1** Base-preview render infra? → closed (LibreOffice on-demand + cached). +- **OQ-Q2** Where does the preview surface? → closed ("Vorschau" button → modal). +- **OQ-Q3** Bearbeiten vs Generieren? → closed (one primary + ⋯ menu). +- **OQ-Q4** Sidebar density? → closed (meta → header toolbar). + +### Flags for the eventual coder (resolve in implementation chat) +1. **LibreOffice deployment shape** — sidecar vs. in-image; the single-flight conversion worker. +2. **Preview cache** — on-disk vs. a small table; eviction policy; confirm preview PNGs are framed as + a regenerable cache, not a retained "document" (vs the §0.5.7 no-retention invariant). +3. **DPI / page-image size** — fidelity vs. payload weight for the modal. +4. **Sample-data source** — a single canned bag, or per-jurisdiction sample bags for nicer previews. +5. **Base-picker vocabulary** — with the modal as the chooser, whether to keep "Vorlagenbasis" vs a + plainer "Vorlage" label, and how to present Gitea bases vs uploaded templates as one list.