Latest Results
feat(link): add container import UI with drag-drop decryption (task 42) (#155)
* feat(link): import containers via plugin (task 42)
Drag-drop .dlc/.ccf/.rsdf/.metalink/.meta4 into Link Grabber paste
zone now decrypts via vortex-mod-containers plugin (task 41) and
feeds extracted URLs into the existing resolve / online-check /
duplicate-detect pipeline. Auto-creates a `source_type=Container`
package per imported file.
New IPC `link_import_container(file_name, file_bytes)` validates
extension + caps payload at 1 MiB, calls the new
`PluginLoader::decrypt_container(bytes)` port, parses the plugin
JSON, and persists the container package. The Extism adapter scans
for the first enabled `Container`-category plugin exporting
`decrypt` and ships raw bytes via the new
`PluginRegistry::call_plugin_bytes` helper (existing
`call_plugin` only takes `&str`, would corrupt non-UTF-8 blobs).
`PasteZone` now exports `CONTAINER_EXTENSIONS` + `isContainerFile`
predicate and surfaces dropped containers through a dedicated
`onContainerFiles(File[])` callback rather than synthesising fake
`container:<name>` URLs that LinkGrabberView used to drop on the
floor (the original `LinkGrabberView.tsx:67` TODO).
Container password protection is wired in spec but vacuously
satisfied: vortex-mod-containers v1.0 uses fixed historic AES keys
per ADR-001 and none of the four supported formats has a per-file
password layer. CCF v2 + DLC v3 service-fetch are deferred to
v1.1, so no `password_required` state can flow through
`decrypt_container` until the plugin gains the capability.
11 new tests:
- 8 backend (`import_container::tests`): golden path, validation
rejections (blank/extension/empty/oversize), plugin NotFound,
zero-link response, invalid JSON
- 1 port default test
- 1 adapter "no container plugin loaded" test
- 4 frontend `PasteZone.test.tsx` cases (drop callback,
callback-missing fallback, text-only drop, isContainerFile)
- 2 frontend `LinkGrabberView.test.tsx` cases (drop → import_container
→ resolve chain ; IPC failure → toast.error)
cargo test --workspace: 1493 pass / 7 ignored
cargo clippy + cargo fmt clean
vitest run: 702 pass
oxlint + tsc -b clean
* refactor(link): /simplify pass on container import (task 42)
- Generic `PluginRegistry::call_plugin_inner` shared by `call_plugin`
and `call_plugin_bytes`; drops ~25 duplicated lines.
- `tracing::info!` → `debug!` on plugin call (per-call hot path).
- Drop redundant `MAX_CONTAINER_BYTES` check in IPC layer; Tauri has
already deserialized the buffer before the function body runs, so
the "defensive cap" was a dead branch.
- `handle_import_container` delegates package creation to the
existing `handle_create_package` instead of inlining UUID mint +
`Package::new` + `repo.save` + event publish.
- Drop redundant `file_name` field from `ImportContainerOutcome` /
`ImportContainerResultDto` (always equal to `package_name`).
- Move inline IPC type to `src/types/container.ts` per project
convention.
- Frontend batches all extracted URLs into a single `link_resolve`
call after the import loop instead of one IPC per container.
- Test fixture sprawl collapsed into `Fixture` / `fixture(loader)` /
`stub_fixture()` helpers (~40 lines saved).
- Trim WHAT comments (4 sites): `LinkGrabberView::handleContainerFiles`,
`PasteZone::handleDrop`, `import_container::MAX_CONTAINER_BYTES`,
`extism_loader::decrypt_container`. Keep WHY-only one-liners.
- Drop "(task 41)" reference in `import_container` module doc; the
CHANGELOG carries that lineage.
cargo test --workspace: 1493 pass / 7 ignored
cargo clippy + cargo fmt clean
vitest run: 702 pass
oxlint + tsc -b clean
Net: -173 / +98 lines.
* fix(link): address PR #155 review comments
- Pre-check container file size before reading into memory (avoid wasting RAM on oversized drops)
- Batch aggregated container URLs into 500-URL chunks so large imports don't trip the resolve cap
- Surface container plugin probe failures as PluginError instead of NotFound (preserves load-failure context)
- Trim whitespace from decrypted URLs before collecting
- Append `.meta4` to install-hint message
- Document alphabetical precedence rule on `decrypt_container` port (matches adapter behavior)
- Fix CHANGELOG test count: 11 -> 16
* fix(link): merge chunked container resolves before applying state
Each `link_resolve` mutation onSuccess called `setResolvedLinks(resolved)` and
overwrote the previous chunk's response. With >500 URLs the user only saw the
last chunk. Extract the post-resolve state mutation as `applyResolvedBatch` and,
on the chunked path, dispatch resolves directly via `invoke`, accumulate the
results, then apply the merged batch once.
* fix(link): chunk online and duplicate probes to backend cap
`link_check_online` and `link_detect_duplicates` both reject batches
above 500 URLs. Container imports above that threshold previously sent
a single oversized call, which the backend rejected and tripped the
`onError` fallback for every row — leaving large imports unable to
bulk-start (status stuck on `unknown`) and bypassing the duplicate gate.
Both helpers now slice into 500-URL chunks; each chunk's onError path
operates on its own `inFlightChunk` set so a failed chunk does not
overwrite a sibling chunk's resolved duplicate state.
* fix(link): guard chunked container imports against stale completion
Three race conditions surfaced in the chunked container import path:
1. The chunked `applyResolvedBatch(merged)` call ran unconditionally
after a long async loop, so a fresh paste-resolve started while
chunks were in flight could be overwritten when the loop completed.
Add `resolveBatchRef`: `applyResolvedBatch` increments it on every
call, and the chunked container handler reserves an id before the
loop and bails if a newer batch landed in the meantime.
2. The per-chunk `link_check_online` `onError` fallback marked rows
`unknown` with no batch guard; an older batch erroring after a
newer batch had pre-seeded `checking` could clobber the live
statuses. Gate the fallback on the captured `batchId`.
3. Overlapping container drops both ran to completion since the
handler lived outside the resolve mutation. Add an
`isImportingContainers` state, bail immediately on a re-entrant
call, and feed the flag into `PasteZone`'s `isLoading` so the
Analyze button reflects that an import is still resolving.
* fix(link): surface backend reason in container import failure toast
`containerImportFailed` translation only carried `{{fileName}}`, so the
`defaultValue` carrying `String(err)` was discarded whenever the key
resolved (always, in practice). Users lost actionable diagnostics —
the IPC layer's "Install vortex-mod-containers..." hint never reached
the surface. Add a `{{reason}}` interpolation to en + fr, pass the
backend message (or a translated `containerTooLarge` for the local
size cap) through the toast call.
---------
Co-authored-by: Mathieu Piton <27002047+mpiton@users.noreply.github.com> Latest Branches
-16%
dependabot/cargo/src-tauri/tauri-2.11.1 -19%
feat/task-42-import-containers-ui 0%
dependabot/cargo/src-tauri/openssl-0.10.79 © 2026 CodSpeed Technology