Each month was a single virtual item that rendered all of its tiles, so
scrolling into a busy month mounted thousands of ImageTiles at once. Flatten
months into a fixed-height row list (header rows + tile rows of `cols` images),
mirroring the Gallery grid, so only on-screen rows render and thumbnails stream
in as you scroll. Active-month tracking and scrubber jump-to-month are remapped
to the flat row model.
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.
Make the onboarding tour theme-aware across the app themes, add the first-run theme picker, and keep fake media previews on the dark media surface. Update light-mode secondary controls in onboarding, settings, dropdowns, toolbar controls, and duplicate actions so they no longer render as dark buttons on subtle-light.
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.
Single-page marketing site built with React 19, Vite 7, and Tailwind v4.
Covers the full product story: local-first privacy, semantic search, explore/timeline,
curation, deduplication, and download. Screenshots transcoded to AVIF/WebP at build
(~5.3 MB masters → ~0.5 MB served). Self-hosted Inter and Space Grotesk variable fonts.
Mobile-first redesign with phone-optimised hero, swipeable feature cards, and touch-friendly
navigation. Includes the standalone Phokus aperture SVG asset.
CI: exclude website/ and docs/ from the Rust/Tauri check workflow.
Replace the stacked desktop layout with a phone-first product flow featuring an image-led hero, inline privacy proof, compact semantic search, swipeable feature cards with live pagination, expandable technical details, and touch-friendly navigation and calls to action. Add the standalone Phokus aperture SVG asset.
Self-host the Inter and Space Grotesk variable fonts, tighten the mobile layout, hide oversized edge marks on narrow screens, and add keyboard focus plus reduced-motion safeguards. Exclude website and documentation-only changes from the desktop CI workflow.
Single-page marketing site for phokus.jezz.wtf in the website/ pnpm workspace
(React 19 + Vite 7 + Tailwind v4). Sections: Hero, Local-first, Search, Explore
& timeline, Curate, Cleanup, Tech facts, Download. Product-first composition with
a recurring aperture motif (EdgeMark) bleeding off alternating edges.
Screenshots are transcoded to AVIF/WebP at build via vite-imagetools (sharp);
~5.3MB of masters -> ~0.5MB served. Commits only the 7 in-use captures.
The global *.json rule (added for transient ComfyUI workflow dumps) forced
git add -f on real configs and hid tauri.conf.json from tauri-action's
gitignore-aware globbing. Removed it and the negation workarounds; local-only
ignores (e.g. skills-lock.json) live in .git/info/exclude instead.
- tauri-action globs for tauri.conf.json honoring .gitignore; the global
*.json rule hid it (force-added but still ignore-matched), so detection
failed with 'Failed to resolve Tauri path' and it tried to re-init.
Negate it like the cuda overlay already is.
- rename uploadUpdaterJson -> includeUpdaterJson (valid v0.6.2 input).
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.