Batch Upload — Design
Date: 2026-07-04
Status: Approved (brainstorm with user)
Modules: CORE feature; ai + semantic decorate it (analysis, suggestions) and it degrades cleanly without them.
Goal
Upload up to 100 files at once through a full-page workbench that preserves everything
the single-file Smart Upload Assistant does per file — AI summary/classification/tags,
content-aware folder suggestion, Doc-ID numbering, preview — with per-file auto-filing
("drop a mixed stack, AI sorts it into folders, user reviews and confirms") and without a
cluttered UI. Nothing is created until "Create all"; leaving the page abandons cleanly.
Decisions (made during brainstorm)
- Scale: medium — up to ~50-100 files. Full-page manager, queued analysis with a
concurrency cap, robust per-row progress states. (Migration-scale hundreds+ is out of
scope — that would be a background-job system.) - Filing: per-file auto-filing. Each file gets its own content-aware folder
suggestion; the top suggestion is pre-selected as that file's destination; the batch
may span many folders. User overrides per file or accepts all. - Layout: master-detail workbench. File list left (status + destination per row),
the full single-file editor right for the selected file, preview as a popup modal,
bulk actions in a top bar. - Commit semantics (carried from the single-file philosophy): staged client-side →
"Create all" runs a queue → per-row created/failed status → failures retry per row,
successes never roll back → cancel/abandon leaves zero rows. - Batch operations on the queue (user addition): shift/ctrl multi-select with a
selection action bar (remove, set classification/format/folder, add tag, accept
suggestions, retry).
Entry points
- The documents toolbar gains Batch upload next to Upload/Write → route
/documents/batch-upload(opened with the current folder as context). - The existing Upload Document modal's dropzone becomes
multiple; dropping 2+ files
forwards them (as in-memoryFileobjects via router state) to the batch page. One
file keeps today's modal exactly as-is. - Files never leave browser memory until creation. A
beforeunloadguard warns while
rows are pending. Hard cap 100 files; the same extension allowlist as AddVersion,
enforced at add-time with a per-file reject message.
The workbench
Top bar
Add files · Accept all suggestions · Defaults ▾ (apply classification / Doc-ID
format / add-tag to ALL rows) · live tally (12 files · 9 ready · 2 analyzing · 1 needs
input) · primary Create all (N) where N = the creatable rows only — blocked rows
(warning badge) are skipped, stay pending, and never hold the rest of the batch hostage.
The button is disabled only when N = 0.
Left: file list
One compact row per file:
- Status dot: queued → analyzing → ready | no-text | failed | created ✓ |
create-failed ✗.
- Title (from filename, editable inline on click-into-text and in the editor).
- Destination line: → /path 88% (accepted suggestion) or the chosen folder; empty +
warning badge when no destination.
- Warning badge when the row is blocked from creation: missing {custom:…} values,
no folder selected, disallowed extension.
- Row click = select (opens in editor). Checkbox column mirrors selection.
Selection model & batch operations (file-manager standard)
- Click = single select ·
Ctrl/Cmd-click= toggle ·Shift-click= range from anchor ·
Ctrl/Cmd-A(list focused) = select all ·Esc= clear. - Selection action bar (Carbon batch-actions pattern) slides in at ≥2 selected with
"N selected": Remove from queue · Set classification ▾ · Set Doc-ID format ▾
· Add tag… · Set folder… (one browse, applied to all selected) · Accept AI
suggestions · Retry failed. Defaults ▾in the top bar always targets ALL rows; the selection bar targets the
selection. Same operations, unambiguous scope.
Right: detail editor
The single-file form, bound to the selected row: Title · Classification (AI-preset with
badge, registry-validated) · AI suggestions panel (editable summary, tag chips — same
component behavior as the upload modal) · Doc-ID format + per-file {custom:…} inputs ·
FolderSuggestBox (that file's own top-3 content-aware suggestions + current folder +
browse) · Preview (popup modal, blob URL, same viewer as the upload modal) · Remove.
With multi-select ≥2, the editor swaps to a compact "N files selected" panel exposing
the selection-bar actions — no half-bound form fields.
AI pipeline per file (existing endpoints only)
A client-side queue, concurrency 3, runs each file through:
1. POST /ai/analyze-upload (multipart; admin page-cap + lang locale apply) →
summary/classification/tags + analysis_id (+truncated).
2. POST /semantic/suggest-folder with {title, filename, text: frozen AI summary} →
top-3 stored on the row, top-1 auto-selected as destination.
Behavior rules:
- The admin upload_suggest_mode is honored per file: immediate → suggestion runs
name-only at add-time and step 2's content pass is skipped; after_analysis (default)
→ suggestion waits for analysis; both → name-only at add, refreshed after analysis.
- Analysis disabled / ai unlicensed → rows skip step 1; suggestions are name-only.
semantic unlicensed → no suggestions at all; destination defaults to the page's
origin folder; FolderSuggestBox degrades to current+browse (existing behavior).
- Budget 429 mid-batch: the AI queue stops for remaining rows, a banner explains
("token budget exhausted — remaining files continue without AI suggestions"), those
rows stay fully usable manually. Creation is unaffected.
- Any per-row analysis failure (no_text, provider error) degrades that row to manual —
never blocks the batch.
Create all
Sequential creation queue, concurrency 2, per row:
POST /documents (confirmed classification, format_id, custom_vars, chosen folder_id) →
POST /documents/{id}/versions with analysis_id + edited summary (seeds content_text
exactly like the single-file path; truncated analyses fall back to async extraction as
already built) → PUT /documents/{id}/tags (selected tags).
- Per-row terminal state:
created ✓(row becomes a link to the document) or
create-failed ✗(row keeps its data +Retry; the tally shows failures). - No rollback of successes; re-running
Create allonly processes non-created rows. - Doc-IDs come from the existing gapless counters at creation. Rows whose chosen format
has{custom:…}tokens are blocked (warning badge + tally "needs input") until
values are filled — creation never burns a counter on an invalid row. - After the queue drains: summary line (
10 created · 2 failed) + the page stays for
retries/links. Created rows are removed from the pending tally.
Backend deltas (deliberately tiny)
analysisCacheMax64 → 256 (go/internal/httpapi/analysis_cache.go): a 100-file
batch must not evict its own entries before Create redeems them. No other backend
change — analyze, suggest, create, version+redeem, and tags all exist.
Error handling
| Situation | Behavior |
|---|---|
| File type not allowed / over 100 cap | rejected at add-time with a toast naming the file(s) |
| Analysis fails / no text / budget 429 | row degrades to manual (name-only suggestion); batch continues |
| No destination folder on a row | warning badge; row excluded from Create all (N) count |
Missing {custom:…} values |
warning badge "needs input"; row blocked until filled |
| Create/upload/tags call fails | row create-failed ✗ + Retry; successes untouched |
| Navigating away with pending rows | beforeunload confirm; nothing persisted |
| Token/session expiry mid-batch | failed rows accumulate with Retry (calls 401 → row failure, not page crash) |
Testing (repo discipline: never go test on the live DB)
- Build/vet +
tsc/vite build. - Curl-scripted e2e of the creation path: 10-file mixed batch (PDF/text/docx) → assert
per-filedocument_enrichmentrows, seededcontent_text, tags, and distinct
destination folders; a truncated row falls back to async extract; a foreign/expired
analysis_idrow still creates (silent normal path); budget-exhaustion mid-batch
degrades remaining rows; cleanup purges all e2e docs. - UI click-through: multi-drop handoff from the modal, shift/ctrl selection, selection
bar ops, preview popup, per-row retry, core-only rendering (no AI/semantic panels).
Out of scope (deliberate)
- Migration-scale imports (hundreds+; background jobs, resumability) — separate project.
- Server-side batch orchestration/staging tables — the client queue over existing
endpoints is sufficient at this scale and keeps cancel-leaves-nothing for free. - Duplicate detection, ZIP expansion, folder-structure import.
- Editing files after
created ✓(that's the document detail page's job).