The tagging worker claimed a batch from the DB but ran the model one image
at a time, so `tagger_batch_size` had no effect on inference. Batching the
whole claim into a single DirectML forward pass fixed that but introduced a
worse problem: on a shared GPU each wide dispatch locks the device (and the
WebView2 compositor with it) for 1-3.7s, freezing the entire app while
tagging runs — worst of all on first launch with a cold graph compile.
The WD model is compute-bound here (~50-230ms/image of actual GPU work), so
a wide batch buys almost no throughput; it only lengthens each uninterruptible
GPU lock. So decouple DB claim size from GPU granularity: claim
`tagger_batch_size` for DB efficiency, but feed the GPU in TAGGER_INFER_CHUNK
(4) images per forward pass with a brief yield between chunks. Each dispatch
now lasts ~200-900ms, the UI gets windows to paint, peak decode memory is
bounded, and the cold compile is for a smaller shape. As a bonus the wide-batch
throughput spikes disappear — steady ~1.6-1.8s/16 vs the old 0.8-3.7s swings.
- run_batch: parallel (rayon) decode + single forward pass per chunk, with
per-image fallback and decode failures kept attached to their input slot.
- Worker iterates source_paths.chunks(TAGGER_INFER_CHUNK), yielding 40ms
between chunks; outputs zip back to jobs 1:1, write tx unchanged.
- Remove now-dead WdTagger::run() (fallback uses infer_one directly).
Security / robustness / a11y polish on top of the color-search + tooltips work:
- URL opening: route through validated backend commands (open_map_location
with lat/lon bounds-checking, open_changelog_url with a fixed URL) instead
of the frontend opener:allow-open-url capability, which is now removed.
- EXIF GPS parsing: validate coordinate ranges and require the correct
N/S/E/W hemisphere ref byte, then clamp.
- Guard double-submit on album create / add-to-album (Lightbox, BulkActionBar,
Sidebar) and discard stale autocomplete responses in the bulk tag editor.
- Gallery tile: stop nesting <button>s — non-interactive tile div with an
overlay button for open/toggle; checkbox and Similar promoted with z-index
+ focus rings.
- Accessibility: keyboard handlers, focus-visible rings, and aria on album
rows, tag-manage actions, and tile controls; Tooltip uses a block <div>
wrapper in block mode and aria-hidden when hidden.
- Show the destination URL in a tooltip on the "Full changelog" link so the
user can see where it goes before clicking.
- Toolbar "All" filter also clears the color filter; color-filtered views no
longer get unfiltered newly-indexed images injected.
- Sidebar reorder: bail if the album set changed mid-drag.
- Tooling: add cargo fmt scripts; alphabetize package.json scripts.
Add dominant-color palette extraction, storage, filtering, and startup backfill so the gallery and Timeline can be filtered from the toolbar color picker.
Introduce a reusable tooltip component and migrate the color filter, update indicator, and gallery filename hover affordances to it.
Similar search scoping:
- Add current_album as a similar-scope option and remember the source album when similar or region searches are launched from an album.
- Route gallery and lightbox similar actions through scope-aware store helpers so Album/Folder/All choices are applied consistently.
- Keep pagination and scope toggles working for both whole-image and region-search result sets.
Backend filtering:
- Extend find_similar_images and find_similar_by_region params with album_id, giving album scope precedence over folder scope.
- Add album_membership filtering for HNSW whole-image search and brute-force crop embedding search.
EXIF info panel:
- New on-demand get_image_exif command (kamadak-exif) returning camera/lens/
aperture/shutter/ISO/focal-length and decimal GPS; read from the file when the
lightbox opens (no DB schema change, works on already-indexed images).
- Lightbox shows a Camera panel; GPS opens the location in the browser via
OpenStreetMap (adds opener:allow-open-url capability).
Tag management:
- Backend rename_tag (rename, or merge when the target exists) and delete_tag
(library-wide), both clearing the tag-cloud cache; store actions invalidate
tag caches, refresh Explore, and re-point/refresh an active tag-search.
- Explore -> Tag Cloud gains a Manage mode: a flat list with per-tag rename/
merge/delete.
Albums:
- Drag-to-reorder in the sidebar via framer-motion Reorder with a hover handle;
order persists through the existing reorder_albums command.
Reads live store order on drag end and robustly derives GPS hemisphere from raw
EXIF ref bytes (review follow-ups). CHANGELOG updated.
Albums (manual collections):
- New albums/album_images tables with FK cascades; DB functions and Tauri
commands for create/rename/delete/delete-many/reorder/list, add/remove
images, and paginated get_album_images.
- Distinct sidebar "ALBUMS" section with cover thumbnails, create/rename/
delete, and a Manage multi-select mode for bulk album deletion.
- Album view reuses the gallery grid (activeView "album" + selectedAlbumId);
spans folders; add from the bulk bar or the lightbox, remove from within.
Gallery multi-select + bulk actions:
- gallerySelectedIds selection model with a top-left corner checkbox that
reveals on corner hover; click-to-toggle in selection mode, double-click
to open.
- Floating BulkActionBar: tag (inline autocomplete popover), rating,
favorite, add-to-album, and a delete with an explicit "from disk"
confirmation. Batch commands bulk_update_details/bulk_add_tags/
bulk_remove_tag.
Also:
- Duplicate Finder delete now requires confirmation with clear "from disk"
wording (was single-click fire-and-forget).
- CPU/CUDA build-variant badge in Settings (get_build_variant).
- Rating/favorite no longer re-sorts derived collections (similar/region/
semantic/tag/album results); single and bulk paths replace in place there.
- Album-aware indexed-images/media-updated handlers so thumbnails paint and
newly-indexed files don't leak into an album view.
- CHANGELOG updated.
Post-update UX for the next release (0.1.2):
- What's New: after a version change, greet the user with a toast that opens
an in-app release-notes screen (collapsible Added/Changed/Fixed sections)
sourced from the bundled CHANGELOG.md, reopenable from Settings -> Updates.
Backed by a last_seen_version settings file (get/set_last_seen_version
commands) that distinguishes upgrades from fresh installs.
- Updater: the download/install progress toast now reappears when an update is
started from the title-bar indicator or Settings after the prompt was
dismissed; Settings -> Updates gains a real progress bar with a percentage.
- Light theme: fix the recurring subtle-light breakage. Neutral surfaces now
rely on the CSS-variable remap instead of light-theme:bg-white (which forced
surfaces dark because --color-white is remapped dark); the green action
buttons drop the broken light-theme:hover:bg-emerald-200 (remapped dark,
unreadable on hover) for an override-free emerald-500 tint that auto-themes,
across the updater, What's New, and onboarding.
- Debug panel: add What's New triggers (toast / modal / reset).
Bump version to 0.1.1 (package.json, Cargo.toml, Cargo.lock), date the
changelog section (2026-06-23), and add the GitHub release-notes draft.
QoL release on top of 0.1.0: custom multi-folder picker, theme system,
timeline scrubber, folder reordering, gallery/duplicate-finder
virtualisation, video playback settings, rebuild-semantic-index action,
plus AVIF thumbnail, video-embedding, and Subtle Light theme fixes.
Route AVIF thumbnail generation through the bundled FFmpeg path instead of the Rust image decoder, avoiding unsupported-format failures without requiring system dav1d dependencies.
Requeue existing AVIF jobs that previously failed with unsupported-format errors and feed generated JPEG derivatives to embedding/tagging preprocessing while leaving lightbox display on the original AVIF file.
Derive default implementations for captioner and tagger option enums, simplify sorting and progress multiple checks, and remove redundant iterator conversions.
Replace native add-folder dialogs with an in-app folder picker that supports collecting folders from multiple locations before adding them together.
Add backend directory listing and batch add commands with duplicate skipping, plus store actions and a themed picker UI with a dedicated folders-to-add panel.
Drops and recreates the sqlite-vec tables at the current model dimension, then
re-queues every image for embedding. Fixes "dimension mismatch" search errors
that occur when the vector table was built for a different model/dimension (e.g.
after experimenting with 768-dim models against this 512-dim build).
The rebuild runs under the embedding worker's DB write lock and resets the job
queue inside a single transaction, so it can't interleave with an in-flight
embedding batch (per code review).
Add a failed-tag discovery flow for background worker failures.
Changes:
- Add a Failed Tags toolbar filter that appears when tag failures exist.
- Add Locate buttons for failed tag tasks in the background worker prompt.
- Route Locate to the affected folder and filter the gallery to images with tagger errors.
- Fetch and display failed tag filenames/errors in the expanded worker details.
- Add a backend query and gallery filter flag for images with failed AI tagging.
- Improve subtle-light contrast for failed worker chips, filenames, and Locate/Retry buttons.
- Also slightly increases the title bar update indicator pulse size for better visibility.
Timeline:
- Add a right-edge scrubber (year labels + month dots) that jumps to any
period; runs in the same direction as the scrolled content
- Load the full filtered set in Timeline view so the scrubber spans the whole
library instead of just the first page
Gallery & UI:
- Virtualise the gallery grid
- Folder reordering in the sidebar (drag + persisted sort_order) and a themed
dropdown component
- Subtle Light theme contrast fixes for toggles, secondary buttons and the
update toast
Embedding workers now defer video jobs without a thumbnail at claim time and
requeue any previously-failed deferred jobs on startup, so videos no longer
churn through failed embeddings.
QoL polish across BackgroundTasks, DuplicateFinder, Lightbox and VideoPlayer.
- regenerate src-tauri/icons from the new branding/phokus-aperture.svg master
- add reusable PhokusMark component (inline SVG, inherits currentColor)
- use it for the titlebar app mark, retiring the placeholder
ureq's read timeout doesn't fire on a stalled large transfer on some Windows
TLS stacks (schannel), so a stall hangs the download forever with no recovery.
Download the ONNX runtime DLLs and the tagger model through the system
curl.exe (Win10 1803+/Win11), which has a real inactivity timeout
(--speed-time) plus resume (-C -). The size probe uses curl too, so nothing in
the download path depends on ureq.
A stalled download discards its partial when it gives up (no more deleting the
model folder by hand to restart) and fails to a retryable error in a couple of
minutes rather than locking the UI on 'preparing'. Adds progress, extraction,
and runtime-init logging. Verified downloading both models on real Windows 11
hardware.
The default build (default = candle-cuda) load-time-imports the CUDA runtime,
so it crashes on any machine without the toolkit. Add a CUDA release variant
that bundles the redistributable runtime DLLs (cudart/cublas/cublasLt/curand,
verified via dumpbin) so it runs on any NVIDIA machine with just a driver.
- tauri.cuda.conf.json: a 'tauri build --config' overlay (CPU release
untouched) that points the updater at latest-cuda.json so a CUDA install
never self-updates into the CPU build
- windows/cuda-hooks.nsh: NSIS hook embeds the DLLs via File and extracts them
next to phokus.exe, where the Windows loader searches
- build:app:cuda script; cuda-redist/ gitignored (DLLs copied from the toolkit
locally), with a negation so the overlay stays tracked
The 1.3 GB model.onnx downloaded via hf-hub's repo.get() and the ONNX
runtime DLLs via a read_to_end — neither reported bytes, so the user saw
only an indeterminate spinner on a multi-minute download.
- tagger model files now download via repo.download_with_progress with a
HubProgress adapter over hf-hub's Progress trait (throttled 200ms events
carrying downloaded/total bytes)
- captioner's nuget DLL download streams in 64 KB chunks, parsing
content-length, and reports per-chunk byte progress
- TaggerModelProgress carries downloaded_bytes/total_bytes; onboarding and
Settings show a determinate bar plus 'X MB / Y MB' for the current file
On a clean install the WD tagger download failed instantly: only the
(UI-removed) caption model prep ever downloaded the shared ONNX Runtime
DLLs, while ensure_onnx_runtime — despite its comment — only initializes
and errors when the DLL is absent. Worse, the OnceLock cached that error
for the whole session, and the onboarding step swallowed the message.
- new captioner::provision_onnx_runtime downloads missing DLLs; tagger
prep calls it before init
- ensure_onnx_runtime no longer caches failure (Mutex<bool>, success-only
latch) so a later download can recover in-session
- onboarding AI step now displays taggerModelError
Phase 5 of the 0.1.0 release prep:
- FFmpeg no longer blocks startup (or crashes the app offline): provisioning
runs in a background thread emitting throttled ffmpeg-progress events,
with installed/downloading/failed state queryable via get_ffmpeg_status
and a retry command
- video jobs are invisible to claiming and tier-priority checks until FFmpeg
is ready, so they wait as pending without failing — and without starving
image embeddings/tagging (include_videos gating in db.rs)
- 7-step show-don't-tell onboarding wizard: welcome + live FFmpeg progress,
real first-folder picker, faked animating pipeline bar, placeholder
gallery tiles, cycling search-syntax demo, views overview, and an AI
features step with a real opt-in tagger download
- skippable at every point (Escape included), persisted via
settings/onboarding_completed.txt, re-runnable from Settings > General,
which also gains an FFmpeg status/retry row
Auto-fix pass (uninlined format args, needless borrows, to_vec,
size_of_val, map_err->inspect_err) plus hand fixes: fully annotated the
sqlite-vec transmute (c_char for cross-platform correctness), type alias
for the duplicate-scan tuple, slice signatures over &mut Vec/&PathBuf,
and #[allow(dead_code)] documenting the intentionally dormant caption
worker. The CI clippy step now fails on any new warning.
Phase 4 of the 0.1.0 release prep:
- tauri-plugin-log: rotating file log (5 MB, Info) in the app log dir plus
stdout; all 54 backend println!/eprintln! sites migrated to log macros so
release builds finally produce diagnostics
- tauri-plugin-single-instance (registered first): second launches focus the
existing window instead of racing the same SQLite DB with a second worker
fleet; tauri-plugin-window-state persists window geometry
- real CSP replacing null (scripts self-only, media/images via the asset
protocol, IPC endpoints whitelisted, object-src none) with a devCsp
variant for Vite HMR
- asset protocol scope narrowed from '**' to thumbnails statically plus
per-folder runtime grants (setup, add_folder, update_folder_path)
Panic audit (plan item) concluded with no changes: worker loops already
catch and log all Result errors; remaining unwraps are startup fail-fast,
poisoned-lock convention, infallible-by-construction, or test code.
Phase 3 of the 0.1.0 release prep:
- tauri-plugin-updater + tauri-plugin-process wired into the builder, with
updater:default / process:allow-restart capabilities
- bundle.createUpdaterArtifacts: true; endpoint points at the GitHub
releases latest.json, pubkey baked into tauri.conf.json
- store: update status state machine (check / download progress / install)
with a quiet launch check (prod only) that stays silent on network errors
- UI: dismissible update toast with download progress, plus an Updates group
in Settings > General showing current version and manual check/install
Applies the stashed 'deps and cpu mode' work: cuda was hardcoded on the
candle dependency lines, making the candle-cuda feature flag dead and any
non-CUDA build impossible. Now default = [candle-cuda] for the main dev
machine, with pnpm dev:app:cpu / build:app:cpu (--no-default-features) for
CPU-only machines and CI. Also trims unused deps (log, uuid, tokio-full,
chrono-serde). Verified: cargo check --locked --no-default-features passes.
Phase 1 of the 0.1.0 release prep:
- LICENSE (MIT) plus license fields in Cargo.toml, package.json, and bundle config
- tauri.conf.json: publisher/copyright/descriptions/homepage so the NSIS
installer no longer derives 'jezz' from the identifier as manufacturer
- version: null in tauri.conf.json — Cargo.toml is now the version source
- bundle.targets narrowed from 'all' to nsis (updater-friendly, faster builds)
- track pnpm-workspace.yaml (esbuild build-script approval, needed by CI)
Workers now form a priority pipeline (thumbnails -> metadata ->
embeddings -> tagging): a worker only claims jobs when no
higher-priority tier has claimable work, so stages drain one at a time
at full speed instead of all workers contending for CPU (shared rayon
pool), disk, and the DB writer simultaneously.
Each tier gate is a single read-only EXISTS query per idle tick using
the same exclusion set the corresponding claim would (that worker''s
paused folders plus actively-indexing folders), so paused or mid-scan
work never blocks lower tiers, and failed jobs stop gating once they
leave pending. Deferred workers wake within one backoff interval
(250-750ms) of the higher tier draining.
A watcher-driven trickle of new files briefly pauses lower tiers by
design. A user-initiated tag run during a large embedding backlog
waits for it; pausing embeddings per folder overrides if tags are
wanted first.
The metadata worker now sleeps its 250ms backoff only when the queue
is empty or a batch errored, claiming the next batch immediately while
work is pending. Each ffprobe result is committed and emitted
individually (in its own transaction) instead of after the whole
batch, so video metadata progress moves steadily rather than stalling
for up to 16 sequential probes.
Embedding and tagging claims now exclude actively-indexing folders,
matching the thumbnail and metadata workers. Previously the embedding
worker started processing a folder while it was still being scanned,
competing with the scanner for CPU (shared rayon pool), disk, and the
DB writer — slowing scans and skewing the adaptive storage profile
toward Conservative. Video embedding jobs claimed mid-scan also
fail-fasted pointlessly since their thumbnails are deferred until
indexing completes; the existing backfill at scan end covers requeue.
- Embedding and tagging workers now sleep their backoff interval
(500ms/750ms) only when the queue is empty, the model is not ready,
or a batch errored — previously every batch paid the sleep even with
work pending.
- CLIP preprocessing no longer decodes originals at full resolution:
decode_for_thumbnail is generalized into decode_image_scaled with a
cover mode (shortest side >= target) and the embedder decodes JPEGs
at the smallest DCT scale covering its 224px fill-crop, in parallel
via rayon. The forward pass, not image decode, is now the dominant
embedding cost.
- EXIF orientation is now applied before embedding, so rotated photos
embed the way they are displayed (previously embedded sideways).
Existing stored embeddings are unaffected.
Major speedups for large-folder thumbnail generation:
- JPEGs decode through mozjpeg at the smallest DCT scale covering
320px instead of full resolution, with catch_unwind and a fallback
to the image crate for anything mozjpeg rejects. EXIF orientation
preserved in both paths.
- RGB8 pipeline end to end (no RGBA round-trip) and CatmullRom resize
in place of Lanczos3.
- Video posters grab the seeked frame directly instead of running the
ffmpeg thumbnail filter (which decoded ~100 frames), with decode
threads capped at 2 per process.
- Workers only sleep when the queue is empty rather than 250ms after
every batch. Image batches commit before videos start, and videos
run sequentially off the rayon pool (blocking ffmpeg waits were
starving image decode) with per-item commits so progress keeps
moving through slow stretches.
Replace the [scanned, total] tuple progress event with a structured
{phase, processed, total, skipped} payload covering all three scan
phases (checking, hashing, confirming). find_duplicates now returns a
DuplicateScanResult with scanned/candidate/skipped counts, and the UI
surfaces phase labels plus a warning when unreadable files were
skipped. Also adds a clean:app script for cargo clean.
Debounce embedding/tagging completion notifications per folder (6s window)
so rapid file additions from downloaders fire one notification instead of
one per file. Add per-folder mute via the sidebar context menu and a global
pause toggle in Settings > General, both persisted across restarts.
Delete thumbnails alongside DB rows in all three removal paths (watcher
detect, delete-from-disk command, remove-folder command) so orphans no
longer accumulate passively.
Intercept RenameMode::Both watcher events and handle them as in-place
path updates: renames the thumbnail file to the new hash and updates the
DB row without touching the embedding, avoiding a full rebuild on move.
Falls back to thumbnail regeneration if the file rename fails.
Scans the thumbnails directory and cross-references against thumbnail_path
values in the images table, deleting any files not linked to an indexed image.
Surfaces count and reclaimable MB in the General section alongside the
existing Compact database card.
Adds get_database_info and vacuum_database Tauri commands. The General
section now shows current DB size and reclaimable space on load, with a
Compact now button that runs PRAGMA wal_checkpoint(FULL) + VACUUM and
reports how many MB were freed. Button disables automatically when the
database is already compact (< 0.5 MB reclaimable).
- Remove Workers section (read-only status, belongs in background tasks panel)
- Remove Captioning coming-soon placeholder
- Add General section with Open data folder button (tauri-plugin-opener)
- Persist queue scope and folder selection across sessions via settings files
- Show inline validation errors on threshold/batch size inputs instead of silent revert
- Fix acceleration save errors appearing in wrong card
- Disable folder selection controls when target scope is All media
db.rs:
- Move idx_images_taken_at creation to after ensure_column so it never
runs against a schema where taken_at doesn't exist yet; this was causing
a startup panic on any DB that predates Phase 1
- COALESCE(taken_at, created_at) → COALESCE(taken_at, modified_at) since
created_at is never populated (modified_at is always set)
- Remove dead is_tagging_job_cancelled function (superseded by
is_tagging_job_processing) and unused thumbnail_path from ImagePathRecord
indexer.rs:
- Watcher create path now calls update_folder_count after commit_batch and
emits folder-counts-changed so the sidebar count stays in sync; the
notification is only emitted when the DB write actually succeeds
store.ts / Timeline.tsx:
- compareImages taken_asc/taken_desc: fall back to modified_at not
created_at (created_at is always null)
- Timeline groupImages: same fallback fix so images group by month
correctly for libraries not yet re-indexed for EXIF
- subscribeToProgress: listen for folder-counts-changed and call
loadFolders() to keep the sidebar image count live
Monitors all registered folders using OS-native events (ReadDirectoryChangesW
on Windows) so new, modified, and deleted files are reflected in the gallery
automatically without manual reindexing.
Key design points:
- Adaptive blocking: recv() when no events pending (zero CPU), switches to
recv_timeout(earliest_deadline) only when debounce timers are running — wakes
exactly when the soonest event is ready, no busy-polling
- 500 ms per-path debounce coalesces rapid OS event bursts into one action
- Change detection preserved: build_record skips upsert if file_size + mtime
unchanged, preventing spurious thumbnail/embedding re-queues and avoiding
clobbering of existing metadata like thumbnail_path
- Access events (reads) filtered out; only Create/Modify/Remove trigger work
- Deletion path: emits watcher-deleted event with image IDs; frontend removes
those images from state and clears selectedImage if it was deleted
WatcherHandle stored in app state; add_folder / remove_folder / update_folder_path
commands keep the watched path set in sync with the DB.
Adds EXIF date extraction during indexing so photos can be sorted by when
they were actually taken, not just when the file was modified on disk.
- Add `taken_at` (nullable TEXT, ISO 8601) column to the images table via
ensure_column migration and a new idx_images_taken_at index
- Populate taken_at in build_record via extract_exif_date, which tries
DateTimeOriginal → DateTimeDigitized → DateTime tags and rejects all-zero
sentinel dates written by uninitialised cameras
- upsert_image uses taken_at = excluded.taken_at so a re-indexed file gets
fresh EXIF; stale dates from replaced files are not preserved
- Add taken_asc / taken_desc sort using COALESCE(taken_at, created_at) so
images without EXIF gracefully fall back to their creation date
- Surface as "Taken: newest / oldest" in the Toolbar sort dropdown
- Shift map_image_row column indices after the new taken_at (index 10)
Adds a full discovery and organisation layer to the gallery.
## AI Tagger
- WD tagger (ONNX via ort) with DirectML/CPU acceleration
- Per-image and per-folder tag queuing; configurable batch size and
confidence threshold; model downloaded on first use via ureq/zip
- Tags stored with source ('ai' | 'user'); user tags are never
overwritten by AI; AI tags protected from accidental promotion
- Tagger pause/resume per folder; in-flight batches discarded cleanly
on pause or cancellation without leaving jobs stuck in processing
## Semantic & Tag Search
- CLIP text-query embedding via candle (HuggingFace hub model)
- Progressive candidate doubling with filter post-processing so
folder/rating/media-kind filters do not silently under-return results
- Tag search with DB-backed pagination (offset + COUNT total)
- Filename search unchanged; prefix syntax: s:, t:, f: in search bar
## Similarity Search
- HNSW in-memory index (hnsw_rs) with monotonic embedding_revision
counter for cache invalidation; build_index retries if a concurrent
write advances the revision during construction
- Folder-scoped similar search uses brute-force cosine scan (no k limit)
- Region-based similarity: crop an arbitrary rectangle in the lightbox,
embed it with CLIP, find the nearest images globally or within folder
- Pagination for both similar and region results; scope toggle
(all media / current folder) re-runs the query without reopening image
## Duplicate Finder
- Three-phase detection: stat (size) → sample hash (4×16 KB windows)
→ full-file hash (xxh3, large files only) to eliminate false positives
- Filesystem-first deletion: only removes DB rows for files successfully
deleted from disk; failed files remain visible for retry
- Persisted scan cache (SQLite) with invalidation on reindex and deletion;
both global and per-folder scopes invalidated after any deletion
## Tag Cloud & Explore
- Visual cluster explore: k-means over CLIP embeddings, configurable k,
representative thumbnail per cluster; cache keyed on xxh3 of embedding set
- Tag explore: ranked tag list with image counts and representative
thumbnail per tag; invalidated when AI tagging completes or folder removed
- Tag autocomplete for search bar
## Folder Management
- Inline rename (display name only, not OS rename)
- Relocate: file-picker to update folder path; all image paths rewritten
using SUBSTR prefix replacement to avoid corrupting paths that contain
the folder name as a substring
- Missing-folder recovery banner: Locate or Remove, never silent deletion
- Right-click context menu on sidebar items: Reindex, Rename, Locate,
Remove; hover buttons preserved alongside context menu
## Background Tasks
- Per-folder progress panel with embedding, tagging, caption, and
thumbnail stage breakdown
- Retry button per task for failed embeddings and tagging jobs
- Completed-task notifications (system tray via tauri-plugin-notification)
- Scan errors shown inline; WalkDir permission errors protect existing
records from deletion rather than cascading data loss
## Backend correctness
- sqlite-vec DML performed outside transactions throughout (virtual-table
limitation); embedding revision incremented on delete as well as insert
- Tagging job discard checks status = 'processing' so paused jobs reset
to 'pending' are also skipped, not just cancelled ones
- Stale async responses in Lightbox guarded with cancellation flags and
currentImageIdRef for both tag-load and tag-add callbacks
- Duplicate cache cleared on reindex; folder-removal clears vector rows
outside transaction before deleting the folder row
- Fix root cause: embedding_source_path() returns Result<PathBuf>, returning
Err for videos without a thumbnail instead of silently falling back to the
raw .mp4 path that CLIP cannot decode
- indexer: split embedding batch into pre-failed (no source) and embeddable
jobs; pre-failed are marked immediately without hitting the CLIP model
- db: retry_failed_embedding_jobs skips videos still without a thumbnail so
they no longer re-fail immediately on retry
- Add get_failed_embedding_images command listing failed files + error per folder
- Gallery: amber warning badge on tiles with embedding_status = 'failed'
- BackgroundTasks: fetch and show failed filenames/errors in expanded panel
- Toolbar: conditional 'Failed Embeddings' amber filter pill shown when any
folder has embedding_failed > 0; filters at DB level via new
embedding_failed_only param on get_images / count_images
- TagCloud: replaced vocabulary/dictionary label system with representative
image thumbnails per cluster; results cached in SQLite by image-id hash
Introduces an Explore view with a tag cloud that clusters image embeddings
using cosine k-means and labels clusters via vocabulary-nearest-neighbour
CLIP matching. Vocabulary embeddings are disk-cached (FNV hash-keyed) to
avoid redundant inference. Enables CUDA for candle dependencies and adds a
build.rs check that surfaces a clear error when the toolkit is missing.