238 lines
11 KiB
Markdown
238 lines
11 KiB
Markdown
# 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<BatchProcessResult>` 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.
|