# Plan: Anlagen-Bezeichnung — Anlagen nummerieren und benennen ## Goal Add a new tool **„Anlagen nummerieren“** (`/tools/exhibits`): users drop any number of PDFs, sort them, choose an exhibit label („Anlage“, „Anlage K“, „Anlage B“, „Anlage XYZ“ or free text), a start number and the page scope, and receive each PDF stamped with its exhibit number — optionally renamed to `Anlage_K_06_Originalname.pdf` for court submission. Context: mail thread with Nikolai Klute (26.09.2026 / 06.10.2026). The requirement is explicitly _one PDF = one Anlage = one number_, with sortable order, configurable label and counter start, and optional filename prefixing. Per-page counters are the separate pagination plan. Related: `plan/done/page-numbers-plan.md` (shared overlay/renderer), `plan/done/stamp-plan.md`, `plan/done/batch-processing-plan.md`. --- ## Open decisions (defaults marked ✅) ### D1 — Numbering unit - ✅ **A. One uploaded PDF = one Anlage = one number.** An „Anlage K 6“ may have many pages; the stamp identifies the document, not the sheet. - B. Group several PDFs into one Anlage. Not requested; deferred (a group would need its own reorder UI inside a group). ### D2 — Stamped pages - ✅ **A. First page by default,** checkbox „Auf jeder Seite stempeln“ for every page of that exhibit. - B. Every page by default. Real exhibit stamps usually mark the first page; the stamp tool already covers „everywhere“. ### D3 — Look - ✅ **A. Framed exhibit stamp:** `Anlage K 6` in title case, HelveticaBold 14 pt (slider 10–24), black `#1a1a1a` (color input included, same control as the stamp tool), 2 pt border, 6 pt padding, no rotation, no date, default position **oben rechts**, 20 pt margin; the six other positions are selectable. - B. Reuse the red authority stamp look. Wrong signal: an exhibit label is a designation, not an endorsement. ### D4 — Label and presets - ✅ **A. Preset chips + editable text.** Chips `Anlage`, `Anlage K`, `Anlage B`, `Anlage XYZ` (`maxlength="40"`, default `Anlage`). The stamp text is `${prefix} ${number}` with a single space — no leading zeros in the stamp (`Anlage K 6`, not `Anlage K 06`). - B. Fixed list only. Firms use their own exhibit letters. ### D5 — Filename renaming - ✅ **A. Optional, default on.** Pattern `{prefixSlug}_{paddedNumber}_{originalBase}.pdf`, e.g. `Anlage_K_06_Klageschrift.pdf`. Prefix slug: trim, collapse whitespace to `_`, strip `/\:*?"<>|` and control characters, keep German letters. Padding = `max(2, digits of highest assigned number)`, so 1…99 → `06`, 1…100 → `006`. Renaming off keeps the original file name exactly (ZIP duplicate names are still deduplicated by the batch ZIP helper). - B. Always rename. Users sometimes want to control file names themselves. ### D6 — Ordering - ✅ **A. Drag-and-drop with a visible handle, plus up/down buttons.** Buttons are the keyboard and Playwright path; both call the same `moveItem(from, to)`. Reordering is disabled while a run is active. - B. Up/down only. Too slow for 30 exhibits. ### D7 — Number freezing - ✅ **A. Numbers are frozen when a run starts** (`itemId → start + index`). Results keep the number they were stamped with even if the user reorders afterwards; the list shows „Bereits erstellte Anlagen behalten ihre Nummer.“ Retry uses the frozen number. A clean renumber means removing results and running again. - B. Live renumbering after processing. Would silently invalidate downloaded files and produce duplicate numbers. ### D8 — Deliverables - ✅ **A. Per-row downloads + one ZIP „Alle herunterladen“** via the existing batch ZIP engine (`anlagen_2026-10-06.zip`). A merged „Anlagenkonvolut“ stays out of scope — „PDFs zusammenfügen“ exists. - B. Always one merged PDF. Not requested and destroys per-exhibit files. ### D9 — Preview - ✅ **A. Preview only for the selected row**, page 1 rendered on demand (shared `loadPdfPageRenderer` from the pagination plan), overlay scaled like the stamp preview; selecting another row disposes the previous handle. - B. Thumbnail per row. Renders every dropped PDF completely — memory trap. --- ## UX Route `/tools/exhibits`, title „Anlagen nummerieren“, registry entry next to `stamp`: ```ts { slug: 'exhibits', title: 'Anlagen nummerieren', description: 'Nummerieren und benennen Sie Anlagen für die Einreichung — mit Anlagenstempel.', icon: 'files' } ``` - **Header/description:** „Versehen Sie mehrere PDFs mit fortlaufenden Anlagen-Bezeichnungen und benennen Sie die Dateien passend um.“ - **FileDropzone** (multiple) — „PDFs als Anlagen hinzufügen“; appending keeps the current list (same dedupe by name + size as the stamp tool). - **Numbered list** (`ExhibitFileList.svelte`, not `BatchFileList` — it needs reordering and number labels): - per row: drag handle, order badge, computed label („Anlage K 6“), file name, renamed preview (`→ Anlage_K_06_Klageschrift.pdf`), status chip, per-row download / retry / remove, and a „Vorschau“ selection. - live label updates for pending rows when order, prefix or start number changes; done rows show the frozen label. - **Options card** - Bezeichnung: preset chips + text input (`maxlength="40"`), live example „Anlage K 6“. - Startnummer (number input, default 1, min 0) + hint „3 Dateien → Anlage K 6, 7, 8“. - „Auf jeder Seite stempeln“ checkbox (default off). - Position (default oben rechts), Schriftgröße slider 10–24 (default 14), Farbe (default `#1a1a1a`). - „Dateien umbenennen“ checkbox (default on) with the filename pattern as helper text. - **Preview card:** selected row, page 1, overlay of the row's label at the chosen position; page count of the selected file shown (requires loading it — the renderer reports both). - **Primary action** „Anlagen erstellen“ → `runBatch` over all pending rows; then per-row downloads and the ZIP action. While running: „Datei 3 von 7 wird verarbeitet…“ (batch pattern) and reordering disabled. - Errors: encrypted PDFs become per-row error rows with the shared decryption hint; an empty prefix blocks the run with a German validation hint; too-long labels use the overlay's fit/error path. ## Technical approach ### Shared overlay and errors - Uses `drawTextOverlay` from `src/lib/pdf-text-overlay.ts` (introduced by `plan/done/page-numbers-plan.md`) with `framed: true`, bold font, `compensatePageRotation: true`. - Uses `src/lib/pdf-errors.ts` for the encrypted-PDF message. - If the exhibit plan ships first, it creates both modules and the pagination plan reuses them. ### Batch engine - Extend `runBatch`'s callback to `(file: File, item: BatchItem) => Promise` so the tool can resolve the frozen number per row (existing `(file) => …` callbacks remain valid). - Keep `shouldProcess`, per-row error formatting and the sequential loop unchanged. - Extract the ZIP/status logic that currently lives inline in `BatchFileList.svelte` into `src/lib/batch.ts` (`downloadBatchZip(items, zipBaseName)`, `batchStatusLabel`, `batchStatusClass`) and reuse it from `BatchFileList` and `ExhibitFileList` without behaviour changes. ### Naming helper (`src/lib/exhibit-naming.ts`) ```ts export const buildExhibitLabel = (prefix: string, number: number) => `${prefix.trim()} ${number}`; export const buildExhibitFileName = (options: { prefix: string; number: number; highestNumber: number; originalName: string; }): string => { const base = options.originalName.replace(/\.pdf$/i, ''); const slug = options.prefix .trim() .replace(/\s+/g, '_') .replace(/[/\\:*?"<>|\u0000-\u001f]/g, ''); const width = Math.max(2, String(options.highestNumber).length); return `${slug}_${String(options.number).padStart(width, '0')}_${base}.pdf`; }; ``` ### Stamping - Load the PDF with pdf-lib, embed `StandardFonts.HelveticaBold`, draw the label on page 0 (or all pages), save. - Result name from the naming helper (or the original name when renaming is off). The batch result keeps the bytes in memory for download/ZIP. - Numbering is frozen at run start; `process` resolves `numbers.get(item.id) ?? freeze(item)`. ## Files changed - `src/routes/tools/exhibits/+page.svelte` — new route. - `src/lib/components/ExhibitFileList.svelte` — reorderable list with status/downloads. - `src/lib/exhibit-naming.ts` — label + filename helpers. - `src/lib/batch.ts`, `src/lib/components/BatchFileList.svelte` — `runBatch` item context, shared ZIP/status helpers. - `src/lib/pdf-text-overlay.ts`, `src/lib/pdf-errors.ts`, `src/lib/pdf-thumbnails.ts` — shared helpers (whichever plan ships first creates them). - `src/lib/tools.ts`, `src/routes/+page.svelte` — registry + icon map (`Files`). - `tests/e2e/exhibits.spec.ts` — new spec. - `agents.md` — route, batch and shared-helper documentation. ## Implementation phases 1. Shared helpers (`pdf-text-overlay`, `pdf-errors`, page renderer) + stamp/compress regression. 2. Batch refactor: `runBatch` item context, ZIP/status extraction, `BatchFileList` unchanged in behaviour. 3. Naming helper + list component with reorder, live labels and selection. 4. Route: options, stamping, per-row results, ZIP, error rows. 5. Preview panel. 6. Registry/icon/docs. 7. Playwright coverage + command gauntlet. ## Verification - Three PDFs (1, 2, 3 pages), reorder the second to the front, prefix `Anlage K`, start 6, renaming on → labels „Anlage K 6/7/8“, downloads named `Anlage_K_06_…`, `Anlage_K_07_…`, `Anlage_K_08_…`; page 1 of each contains the label, page 2 is untouched (`getPageContentStreamCount` unchanged). - Start 98 with three files → padding switches to three digits (`098`, `099`, `100`). - Renaming off → downloads keep the original names; ZIP entries deduplicate on collisions. - „Auf jeder Seite stempeln“ → every page of every exhibit contains the label. - Reorder after a run → done rows keep their label and the hint is visible. - Prefix validation (empty), encrypted PDF (row error, queue continues), 30-file ZIP. - Single-page PDFs do not break the run. - `pnpm check`, targeted `pnpm test:e2e`, `pnpm build`, Prettier on touched files. ## Risks - **Number drift after edits:** mitigated by frozen numbers + hint; no silent renumber. - **Drag-and-drop accessibility:** buttons remain the primary path and the tested one; DnD must have a visible handle and keyboard alternative. - **Memory:** results stay in memory like every batch tool; the existing warning (>20 files or > 25 MB) applies. The preview renders only one page — never a thumbnail array. - **Filename edge cases:** slugs from unusual prefixes (`Anlage K/1`) must not produce path separators or empty names; validate and fall back to `Anlage`. - **Rotated scans:** the shared overlay compensates page rotation for the new tools; stamp output must stay unchanged. - **Already-prefixed input files:** re-running the tool on its own output stacks prefixes. Document it; no auto-detection in v1. ## Out of scope - Grouping multiple PDFs into one Anlage. - Anlagenverzeichnis / index page (a merged table of contents is a follow-up candidate). - One merged „Anlagenkonvolut“ export. - Per-page counters (pagination plan). - Persisted prefixes or exhibit libraries.