Merge chore/release-prep into main for the first public release.
- Release pipeline: CI (fmt, clippy -D warnings, tsc) + tag-triggered
GitHub Actions building the NSIS installer as a draft release.
- Self-updater via GitHub Releases — launch check, Settings UI, update
toast, and a title-bar focal-point indicator that lights up when a new
version is ready.
- Production hardening: real CSP, scoped asset protocol, rotating file
logging, single-instance, window-state.
- Guided first-run onboarding with background FFmpeg provisioning and
resilient (curl-based, resumable) model/DLL downloads.
- Branding: Phokus aperture mark across the icon set + title bar, with a
reusable PhokusMark component.
- CUDA installer variant with bundled runtime DLLs (built locally).
- MIT license, bundle metadata, NSIS-only target, and docs (README
install/privacy, CHANGELOG, 0.1.0 release notes).
- new StepUpdates: explains the title-bar update indicator with a mini
app-window mockup, and introduces the aperture mark as the app's identity
- move the tour-closing copy off the AI features step onto this final step
- changelog: note the title-bar update indicator + one-click install, and the
new onboarding step
- light up the Phokus iris's focal point (pulsing amber) in the titlebar when
an update is pending; click goes straight to install, with a fast custom
tooltip (native title delay was too slow)
- PhokusMark gains an optional dotClassName to render the central focal point
- DemoPanel: inject every updater state (available/downloading/installing/
error/up-to-date), which is otherwise unreachable without a remote latest.json
- 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
The background-worker pipeline drains in under a second on a fast machine,
making it impossible to screenshot live. This adds a dev-only panel
(Ctrl+Shift+D) that injects a frozen mid-pipeline state into mediaJobProgress
so the worker bar can be captured at leisure, then cleared. Gated by
import.meta.env.DEV and verified absent from the production bundle, so it
never ships. A first rough cut of the broader screenshot/demo mode.
Removing a folder only reloaded the gallery when that exact folder was the
active selection, so on All Media (or another view) its images stayed on
screen until a manual refresh. Now the folder is dropped from the sidebar
optimistically (instant feedback while the backend deletes its images), the
gallery is always reloaded, and a failed delete resyncs the folder list.
Switching search modes briefly showed the next demo's result images because
the results container stayed mounted and faded its opacity down from the
previous demo's visible state. Key it by demoIndex so it remounts fresh at
opacity-0 and only fades in once the new query finishes typing.
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
Phase 6 release collateral:
- README: Installation (unsigned-build/SmartScreen note, requirements,
first-run downloads) and Privacy & local-first sections
- CHANGELOG.md seeded with the 0.1.0 feature set
- docs/release-notes-0.1.0.md draft for the GitHub Release body
Also corrects the CLIP model download size from ~330 MB to ~580 MB (the
actual model.safetensors is 577 MB) across the docs and the onboarding UI.
The onboarding tour tells users they can right-click a folder to pause its
background work, but only the per-worker buttons in the BackgroundTasks bar
existed. Add a 'Pause/Resume background work' context-menu item that toggles
all four workers (thumbnail/metadata/embedding/tagging) for the folder.
To keep the bar and the menu in sync, worker-pause state moves from
BackgroundTasks' local state into the store (the canonical owner of app
state), so pausing from either surface reflects in both. Reuses the existing
set_worker_paused / get_worker_states commands; no backend change.
Replace the two near-identical pale ocean sunsets with three distinct
'golden sunset over water' stills — a vivid coastal sunset, a soft golden
ocean, and a golden alpine lake — so the /s demo shows a convincing,
varied result set. Drops the redundant sunset2 asset.
- search demo results were greyed-but-visible while the query was still
typing, as if the app pre-knew the search; they're now invisible (space
reserved, no layout jump) and fade in only once typing completes
- gallery preview dropped from 3 rows to 2 so the 'Click any tile' explainer
text is visible without scrolling
Two refinements from testing:
- the gallery-reveal, pipeline-drain, and search-typing animations looped
forever; they now play once and stop, with a Replay button to re-run
- the search demo's result tiles didn't match the queries. Now: a filename
search returns one exact file, '/s golden sunset over water' shows only
ocean-sunset stills (added a second), and '/t landscape' shows the three
actual landscapes (valleys + alpine lake), not a foggy forest
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
Swap the fake gallery tiles' gradient stand-ins for 13 rights-clean
generated WebP stills (512px, ~25 KB each). Search demo now returns
thematically matched results — beach/dunes for the filename query, sunset
and water for /s, landscapes for /t — so the show-don't-tell steps look
like a real library. tileGradient retained for the abstract Explore and
Timeline mini-previews.
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
Semantic search is part of the standard pipeline (CLIP auto-downloads on
first embedding batch), not opt-in — only AI tagging is. Reword the step
so 'optional' applies to what is actually optional. A true embedding
opt-out toggle is tracked as a follow-up.
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
Phase 2 of the 0.1.0 release prep, running on the GitHub mirror:
- ci.yml: frontend type-check/build, rustfmt gate, clippy (CPU-only) on
windows-latest with pnpm + rust caching
- release.yml: v* tags build the NSIS installer via tauri-action and attach
it to a draft GitHub Release with updater JSON; updater signing secrets
referenced ahead of Phase 3
- both build --no-default-features: runners have no CUDA toolkit, and the
shipped artifact must be the CPU/DirectML build
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)
UI/UX pass delivering:
- feat(settings): flat desktop redesign — VS Code-style form rows
replace the card-based layout, dialog sizes proportionally with the
app window (85% capped at 1400x900); fixes thumbnail-cleanup stats
blanking mid-clean and the Compact now button staying clickable
after compaction
- feat(views): in-view folder scope switching — Timeline, Explore, and
Duplicates get a header scope dropdown so changing folders no longer
bounces through the sidebar
- feat(lightbox): custom immersive video player — edge-to-edge
playback with auto-hiding controls, drag scrubbing, volume, speed,
loop, fullscreen, and full keyboard support, replacing the native
browser player
- docs(readme): feature list and architecture brought up to date
Adds everything shipped since the last README pass: live file tracking
with rename-aware watching, Timeline view, batched notifications with
mute/pause, the custom lightbox video player, in-view folder scoping
for Explore/Timeline/Duplicates, three-phase duplicate scanning,
maintenance tools (database compaction, thumbnail cleanup), per-folder
worker pausing, and the strict-priority background worker pipeline.
Features are now grouped by area, and the stack/how-it-works sections
mention mozjpeg scaled decoding, EXIF capture dates, and the watcher.
Replace the native <video controls> element with a custom player.
Videos now fill the lightbox media area edge-to-edge on black instead
of sitting in a padded, rounded box, with an auto-hiding control bar
over a bottom gradient (2.5s idle, always visible while paused):
play/pause, drag scrubber with buffered ranges, time readout, volume
slider with session-persisted volume/mute, playback speed (0.25-2x),
loop, and fullscreen.
Click toggles playback, double-click toggles fullscreen. Keyboard:
Space play/pause, M mute, F fullscreen, L loop, Shift+Left/Right seek
5s, Up/Down volume (unmutes) — plain arrows still navigate between
media, and all player keys are ignored while typing in the info-panel
inputs. Videos autoplay on open, falling back to paused-with-controls
if the webview blocks it.
Timeline, Explore, and Duplicates get a folder-scope dropdown in their
headers (Timeline via the shared Toolbar), so changing scope no longer
means leaving the feature and bouncing through the sidebar. The new
setViewFolderScope store action updates selectedFolderId while
preserving activeView: Timeline/Gallery reload images, Explore reloads
via its existing selectedFolderId effect, and Duplicates loads the
cached results for the new scope (fresh scans remain an explicit
Rescan). Sidebar behavior is unchanged — clicking a library still
opens its gallery.
Complete presentation rework of the Settings modal away from the
card-based tablet look:
- Dialog now sizes proportionally (85vw x 85vh, capped at 1400x900)
and resizes with the app window instead of a fixed centred square.
- Content rebuilt as flat form rows in the VS Code preferences style:
uppercase group headings with hairline-divided label/control rows —
no nested cards, panels, or boxed strips. Sidebar retained.
- Duplicated content header removed; close button floats top-right.
- Folder selection restyled from bordered chips to flat divider rows;
model path and runtime check render as plain monospace text.
Also fixes two maintenance-section bugs: thumbnail-cache stats no
longer blank to em-dashes while a cleanup is running, and the Compact
now button disables immediately after compaction instead of allowing
pointless re-runs until the section remounted.
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).
- folder_ids: use snake_case in both set_tagging_queue_folder_ids invoke
calls (toggleTaggingQueueFolder and setTaggingQueueFolderIds) to match
the Rust SetTaggingQueueFolderIdsParams serde field name
- threshold save error: route setTaggerThreshold() catch to
setTaggerThresholdError instead of setTaggerQueueStatus so the error
surfaces next to the threshold input, not the queue targets card
- 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
Merges feat/watchdog-exif-timeline into main.
- Adaptive filesystem watchdog: switches between inotify/ReadDirectoryChanges
and polling with zero CPU when idle; watcher events update sidebar counts live
- EXIF taken_at: extracted at index time and stored as a sortable column;
new taken_asc/taken_desc sort orders; migration is safe on existing DBs
- Timeline view: virtualised month-grouped grid (react-virtual v3), reuses
ImageTile/ContextMenu, Calendar icon in Sidebar, defaults to taken_asc sort
- Bug fixes: startup migration panic on existing DBs without taken_at column,
"Unknown Date" grouping due to null created_at fallback, watcher not updating
folder image counts, cols overflow in Timeline grid
- Fix cols overflow: use (containerWidth - GAP) formula so the grid
never exceeds the container width
- Explicitly pin "unknown" date keys to the end of the sort rather than
relying on alphabetical accident ("u" > digits)
- Guard buildLabel against non-YYYY-MM keys with finite/range checks
and an isNaN fallback to "Unknown Date"
- Clear similarSourceFolderId, similarFolderId, and similarCrop when
switching to the timeline view so all similar-state is consistently
reset
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