Skip to content

Refactor application/ — LibraryContext + decomposed module - #663

Open
justinf555 wants to merge 12 commits into
mainfrom
refactor/library-context-step1-introduce-context
Open

Refactor application/ — LibraryContext + decomposed module#663
justinf555 wants to merge 12 commits into
mainfrom
refactor/library-context-step1-introduce-context

Conversation

@justinf555

Copy link
Copy Markdown
Owner

Summary

  • Introduces LibraryContext, a pure-Rust pub(in crate::application) container for the domain + infrastructure (Library, RenderPipeline, Tokio handle, purge-task handle). UI code physically cannot name the type — the architectural boundary is now enforced by the type system, not by convention.
  • Decomposes the 898-line application/mod.rs god-object into seven sibling files with clear roles (context.rs, startup.rs, lifecycle.rs, library_loader.rs, import.rs, actions.rs, plus the existing keyring.rs). mod.rs drops to 369 lines — essentially the GObject subclass machinery + public accessor API.
  • Replaces 13 RefCell<Option<T>> fields on MomentsApplication with OnceCell<T> for clients and LibraryContext-owned shapes for domain state. Tightens client constructor signatures to declare minimal sub-service dependencies via a build() convention enforced inside application/.

Triggered by an external engineering review (Downloads/moments_repository_engineering_review.md) which flagged "Application State Container Is Trending Toward a God Object" as the highest-severity architectural finding.

Full design doc landed as the first commit of this PR: docs/design-library-context.md.

Architectural property achieved

  • LibraryContext is pub(in crate::application) — unspellable in ui/, client/, library/. UI code cannot reach the domain except through a Client.
  • Clients are constructed via Client::build(...) inside application/ only. UI code cannot construct a Client because it cannot acquire the constructor arguments (sub-services live behind pub(in crate::application) accessors on LibraryContext).
  • All client storage is OnceCell<T> (post-startup contract), with Option<&SyncClient> exposed for the Immich-only sync client.
  • Startup is four typed phase functions (phase1_domain, phase2_background_services, phase3_clients, phase4_install) — compiler enforces ordering.
  • SyncEngine has its own typed identity on Application; SyncClient holds an Arc<SyncEngine> for UI-driven engine control.

Per-step breakdown (11 commits)

Step Commit What
Doc 97dcb41 docs/design-library-context.md — the design
1 7ea02bc Introduce LibraryContext; populate alongside legacy fields
2 05b18a1 Switch client construction to build() taking concrete deps
3 85cc084 Migrate client fields to OnceCell<T>
4 395e2e8 Remove deprecated accessors + legacy domain fields
5 0f84c33 Split startup into typed phase functions (startup.rs)
6 8d4abb0 Reshape SyncHandleSyncEngine top-level service
7 98c018d Extract LibraryLoader from duplicated wizard/open paths
8 cac9c2c Extract actions.rs + import.rs from mod.rs
9 c97b226 Extract library-open lifecycle into lifecycle.rs
10 3923cf5 Tighten Album/People clients to minimal sub-services

Steps 7–10 emerged during implementation as architecturally-adjacent cleanups (Step 7 is documented in the design doc; Steps 8–10 are pure decomposition / dependency-tightening). They were bundled into this PR because they live in application/ and the cost of doing them separately later would be higher.

application/ module shape after

mod.rs            369  — GObject subclass + public accessors + shutdown/activate
startup.rs        503  — 4 typed startup phases
lifecycle.rs      186  — setup wizard + open-library orchestration
library_loader.rs 157  — shared bundle-open + keyring pipeline (+ 4 unit tests)
keyring.rs        144  — token resolution (unchanged)
import.rs         124  — import dialog + flow
actions.rs        108  — app actions + about/shortcuts/prefs dialogs
context.rs        106  — LibraryContext (domain owner)

Notable deviations from the design doc

Two deliberate deviations are documented inline in the code:

  1. library_context field is RefCell<Option<Arc<LibraryContext>>>, not the doc's sketched OnceCell<Arc<...>>. GObject shutdown takes &self, but OnceCell::take requires &mut self. The existing shutdown path needs to clear the Arc<Library> (and the SqlitePool it wraps) before main() drops the Tokio runtime. Converting requires a shutdown-ordering redesign — out of scope for this refactor. Documented at the field declaration in mod.rs.

  2. MediaClientV2 and ImportClient keep Arc<Library> rather than splitting into N sub-services. Both genuinely touch 6+ services plus Library-level cross-service methods; splitting would harm readability without architectural benefit. The doc explicitly allowed this ("If a client uses the whole Arc<Library> because it touches many services, that's acceptable"). AlbumClientV2, PeopleClientV2, and SyncClient were tightened (Step 10).

Out of scope / deferred

These were flagged in the design doc as future work:

  • BackgroundContext — anticipated holder for long-running background services (SyncEngine, PurgeTask) once a coordinated operation (pause_all, shutdown_in_order, apply_config) is needed. Trigger condition: ≥3 background-task fields or first concrete coordination need.
  • ClientContext — speculative, depends on D-Bus design. Deferred until cross-cutting policy actually differs between callers.
  • AppConfig — typed config struct read once from gio::Settings. Open question in the design doc.

Test plan

  • make lint clean (cargo fmt + clippy -D warnings, both default and --features dhat-heap)
  • make test — 615 passed, 0 failed (baseline was 611; added 4 LibraryLoader unit tests in Step 7)
  • make typos clean
  • make test-integration passes
  • Manual: launch via make run-dev, verify Local-backend setup flow (new library creation) — reviewer to confirm
  • Manual: launch via make run-dev, verify Immich-backend setup flow and sync — reviewer to confirm
  • Manual: launch with stale GSettings library path (delete the library dir, then start) — verify recovery into setup wizard with the stale-path dialog
  • Manual: trigger an import — verify the import dialog and run flow

🤖 Generated with Claude Code

justinfowler and others added 11 commits May 27, 2026 17:18
Introduces the design for replacing the god-object MomentsApplication
state container (13 RefCell<Option<T>> fields, 270-line load_library_async)
with a typed LibraryContext owning the domain plus minimal-surface clients
constructed via build() inside application/.

Locks in the LibraryContext naming, the pub(in crate::application) privacy
boundary, concrete sub-service dependencies (no preemptive traits), the
build() constructor convention, and the 6-step migration plan. Future
BackgroundContext and ClientContext scopes documented with named trigger
conditions, intentionally deferred.

Triggered by the engineering review at
Downloads/moments_repository_engineering_review.md flagging god-object
proliferation as the highest-severity architectural finding.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Add `LibraryContext` — a pure-Rust container that owns the long-lived
domain + infrastructure objects (`Arc<Library>`, `Arc<RenderPipeline>`,
`tokio::runtime::Handle`, and the periodic-purge `JoinHandle`). The
type is `pub(in crate::application)` so it is unnameable outside the
`application/` module tree: UI widgets and clients cannot reach the
domain by name, only through their Client facades.

This is Step 1 of the migration plan documented in
`docs/design-library-context.md` — purely additive. Existing
`RefCell<Option<T>>` slots on `MomentsApplication::imp` keep being
populated; the new `library_context` field is populated alongside
them. Later steps will collapse the duplication and migrate clients
to consume sub-services from the context.

Deviation from the doc shape: `library_context` is stored as
`RefCell<Option<Arc<LibraryContext>>>` (not `OnceCell`) so the
existing `shutdown()` path can clear it before `main()` drops the
Tokio runtime. The eventual `OnceCell` shape will return when the
legacy slots are removed in Step 4.

The purge-task `JoinHandle` moves into the context immediately —
`JoinHandle` is not `Clone`, so it can only have one home, and the
context is the canonical one per the design doc. `shutdown()` now
aborts that handle explicitly before dropping the context.

`MomentsApplication::render_pipeline()` is marked `#[deprecated]`
with delegation through the context; the three existing callers
(`client/media/client_v2.rs`, `ui/viewer/loading.rs` x2) carry
narrow `#[allow(deprecated)]` annotations with TODOs pointing at
Step 2 (Client constructors taking sub-services explicitly).

Verified: `make lint`, `make test` (611 passing), `make typos` all
clean.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…(step 2)

Step 2 of the LibraryContext refactor (docs/design-library-context.md).
Each client now has a single `build(...)` constructor that takes its
concrete dependencies and returns a fully-wired GObject, replacing the
two-call `new() + configure()` pattern.

* MediaClientV2::build(library, tokio, render_pipeline)
* AlbumClientV2::build(library, tokio, events_rx)
* PeopleClientV2::build(library, tokio, events_rx)
* ImportClient::build(library, originals_dir, thumbnails_dir,
  render_pipeline, mode, tokio)
* SyncClient::build(events_rx, tokio)

`load_library_async` now pulls sub-services (`library()`, `tokio()`,
`render_pipeline()`) from the `LibraryContext` it built and passes them
into each `build()`. The `#[allow(dead_code)]` on the context's owner
struct and core accessors is lifted now that they have callers.

`MediaClientV2` takes the `RenderPipeline` as a constructor argument and
holds it in `MediaDeps`, so the pixel-edit save path no longer reaches
back into the global `MomentsApplication::default().render_pipeline()`
deprecated accessor.

`new()` is kept on each client behind `#[cfg(test)]` so unit tests can
exercise pure model-patching helpers without a real `Library`. The
matching `Default` impls (unused) are removed. The
`MomentsApplication::library_context()` accessor stays
`#[allow(dead_code)]` — Step 5 introduces phase functions that need it.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Convert the five client GObject singleton slots on
`MomentsApplication::imp` from `RefCell<Option<T>>` to `OnceCell<T>`,
removing the runtime nullability ceremony for callers.

Accessor signatures change to reflect the post-startup contract:

  - `import_client(&self)   -> &ImportClient`
  - `album_client_v2(&self) -> &AlbumClientV2`
  - `people_client(&self)   -> &PeopleClientV2`
  - `media_client_v2(&self) -> &MediaClientV2`
  - `sync_client(&self)     -> Option<&SyncClient>`  (Immich-only)

Returning `&Client` instead of an owned `Option<Client>` matches the
"set once, read for the rest of the application lifetime" reality and
lets call sites drop the `if let Some(c) = …` guards. Sites that need
ownership (closures captured into spawn_local, etc.) call `.clone()`
explicitly — cheap on a GObject ref-count.

`set_sync_client` and `set_import_client` are now `OnceCell::set` calls
that panic on double-set; the existing notify-property pattern is
preserved. The `import-client` property notification subscription in
ActivityIndicator is dropped (the client is always set before any
widget realizes); the `sync-client` subscription is kept because Immich
sessions install it after the main window is already wired up.

Shutdown no longer attempts to drain client `Arc<Library>` clones —
`OnceCell::take` requires `&mut self` and `ApplicationImpl::shutdown`
gives us `&self`. The canonical `Arc<Library>` drop still happens via
the legacy `self.library` slot and the `self.library_context` clear,
which releases the `SqlitePool` before `main()` drops the Tokio
runtime. Step 4 of the refactor will remove the legacy slots and can
revisit the storage shape if a leak is observed.

See `docs/design-library-context.md` Migration Plan → Step 3.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Step 4 of the LibraryContext refactor. The context is now the single
canonical home for `Arc<Library>` and `Arc<RenderPipeline>`; the
mirrored `RefCell<Option<...>>` slots and the `#[deprecated]`
`Application::render_pipeline()` accessor that Step 1 left in place as
shims are removed.

- Drop `imp::MomentsApplication::{library, render_pipeline}` legacy
  fields. The two in-tree readers (`load_library_async`'s purge-task
  and sync-engine setup) now pull `Arc<Library>` from
  `library_context.library()` directly.
- Drop the deprecated `MomentsApplication::render_pipeline()`
  accessor. The two UI call sites in `viewer/loading.rs` now go
  through `MediaClientV2::render_pipeline()` — the client already
  holds the pipeline as a build-time dependency, so widgets that use
  the client never need a separate path to it.
- Promote `MediaClientV2::render_pipeline()` from private to `pub`.
- Remove the two `#[allow(deprecated)]` annotations that pointed at
  the now-deleted accessor.
- Tighten the shutdown path: the redundant
  `self.library.borrow_mut().take()` is gone; clearing
  `library_context` releases the canonical `Arc<Library>` before the
  Tokio runtime is dropped.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Step 5 of the LibraryContext refactor. Extracts the 270-line
`load_library_async` mega-function into `src/application/startup.rs`
with four typed phase functions:

  phase1_domain               - open Library, build RenderPipeline,
                                wrap in LibraryContext
  phase2_background_services  - conditionally start the Immich
                                SyncHandle (Step 6 will reshape it
                                into a dedicated SyncEngine; today
                                the SyncHandle shape is unchanged)
  phase3_clients              - build UI client GObjects with
                                minimal concrete dependencies
  phase4_install              - populate Application fields, start
                                the periodic purge task, wire the
                                main window

Each phase consumes the previous phase's typed output, so the
compiler enforces the ordering: building a client before the
LibraryContext exists is no longer expressible. `start()` is
`pub(in crate::application)` and replaces both call sites in
`on_setup_complete` and `open_library`.

mod.rs shrinks from 1005 to 720 lines and now holds only the
GObject subclass, accessors, GTK lifecycle hooks, and the public
API. `load_library_async` is deleted; stale doc references to it
in context.rs and activity_indicator.rs are updated to point at
the new phase function names.

See docs/design-library-context.md, Migration Plan -> Step 5.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Step 6 of the LibraryContext refactor. The existing `SyncHandle`
already owned the long-running work (pull/push managers + watch
channels for shutdown and live interval), but it lived behind a
`RefCell<Option<...>>` on `MomentsApplication` and was accessed
directly by `set_sync_interval`. Reshape it into a dedicated
`SyncEngine` with the architectural property the design doc calls
for: an owned identity on `Application`, with `SyncClient` holding
an `Arc<SyncEngine>` reference for UI binding.

- Rename `SyncHandle` -> `SyncEngine`; `start(...)` -> `build(...)`
  returning `Arc<Self>` to match the construction convention from
  Step 2.
- `SyncClient::build` now takes `Arc<SyncEngine>` alongside the
  events receiver and tokio handle; it stashes the engine ref so
  `SyncClient::set_interval` can forward to the engine directly.
- `imp::MomentsApplication.sync_handle: RefCell<Option<SyncHandle>>`
  becomes `sync_engine: OnceCell<Arc<SyncEngine>>`, with
  `pub(in crate::application)` visibility so widgets cannot reach
  it.
- `set_sync_interval` on `MomentsApplication` now routes through
  the `SyncClient` (which holds the engine), keeping the
  preferences dialog one hop further from the engine.
- Shutdown: `ApplicationImpl::shutdown` reads the engine via
  `OnceCell::get` and calls `shutdown()` on it, signalling the
  pull/push tasks to exit at their next polling boundary. The
  `Arc<SyncEngine>` clones are dropped when the application itself
  is dropped, after `shutdown` returns.
- Phase 2 of startup now produces `Option<Arc<SyncEngine>>` and
  phase 3 receives a clone to hand to `SyncClient::build`; phase 4
  installs the engine on the application via `OnceCell::set`.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The setup-complete and open-on-launch entry points each ran the same
~60-line bundle-open + Immich-token-resolve pipeline, differing only in
error-dialog wording, which window parents the dialog, and the GSettings
path side effect. Extract that pipeline into a pure-Rust LibraryLoader
that returns a typed LoadOutcome (Ready / Failed(LoadFailure)); the
callers own all GTK presentation and the path write/clear decision.
This keeps the loader widget-free and unit-testable while preserving the
exact per-context error messages and path-clearing rules. Step 7 of the
LibraryContext refactor.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Move the four app-level GAction/dialog handlers (setup_gactions,
show_shortcuts, show_about, show_preferences) into actions.rs and the
folder-import entry points (show_import_dialog, run_import) plus their
GIO helper functions (resolve_folder_via_gio, gio_walk) into import.rs.
Pure code movement to shrink application/mod.rs — no behavior changes.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Move show_setup_window, on_setup_complete, and open_library off the
impl MomentsApplication block in mod.rs into a new lifecycle.rs, along
with the setup_failure_dialog, open_failure_dialog, and
show_library_error_dialog helpers (all used only by those methods).

Pure code movement — no behavior changes. The three lifecycle methods
gain pub(in crate::application) visibility so the imp::activate and
startup::start call sites still resolve them across module boundaries;
the dialog helpers stay private to lifecycle.rs. mod.rs drops from 530
to 369 lines.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
AlbumClientV2 and PeopleClientV2 were each handed the whole Arc<Library>
even though they use only one or two sub-services. The design doc's
principle is that a client's constructor signature should declare the
minimal concrete sub-services it actually uses.

AlbumClientV2::build now takes (AlbumService, ThumbnailService); the
listener needs only AlbumService. PeopleClientV2::build now takes
FacesService. LibraryContext gains album_service / thumbnail_service /
faces_service accessors returning owned clones (the services are cheap
Clone handles over the DB pool + event emitter). Neither client holds
Arc<Library> any longer. Behavior is unchanged.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@github-actions

github-actions Bot commented May 29, 2026

Copy link
Copy Markdown

Claude finished @justinf555's task in 3m 27s —— View job


PR Review: Refactor application/ — LibraryContext + decomposed module

Tasks

  • Gather context and understand the PR
  • Read core new files (context.rs, startup.rs, lifecycle.rs, library_loader.rs)
  • Review mod.rs changes
  • Review client changes (album, people, sync)
  • Review sync/mod.rs changes
  • Check UI-side changes for regressions
  • Post review findings

Overall Assessment

This is a well-executed architectural refactor. The pub(in crate::application) visibility enforcement is a legitimate compile-time barrier against UI code reaching domain objects directly, the four-phase typed startup meaningfully enforces ordering, and the transition from RefCell<Option<T>> to OnceCell<T> for clients is clean. The design doc deviations are honestly documented and justified. No regressions visible in the UI-side changes — they're all simple accessor renames/removals following the new client build() convention.

Four issues worth addressing before merge:


Issues

1. ImmichClient::new failure is silently swallowed — startup.rs:222–224

let immich_client = immich_info
    .as_ref()
    .and_then(|info| ImmichClient::new(&info.server_url, &info.access_token).ok());

If ImmichClient::new fails (malformed URL, etc.), immich_client is None. Downstream, recorder becomes NoOpRecorder and resolver becomes LocalResolver — the library opens successfully but mutation recording and original-file resolution are silently wrong for a user who configured Immich. There's no warn! log, no error surface. At minimum this deserves a warn! before the .ok():

let immich_client = immich_info.as_ref().and_then(|info| {
    match ImmichClient::new(&info.server_url, &info.access_token) {
        Ok(c) => Some(c),
        Err(e) => {
            warn!("failed to build Immich HTTP client: {e}; continuing without sync");
            None
        }
    }
});

Fix this →


2. thumbnails_dir_for_sync duplicates paths.thumbnails_dirstartup.rs:86–92, start()

DomainArtifacts carries both paths: ImportPaths (with thumbnails_dir: PathBuf) and thumbnails_dir_for_sync: PathBuf. Both are populated from bundle.thumbnails.clone() in start(). phase2_background_services uses domain.thumbnails_dir_for_sync but it could use domain.paths.thumbnails_dir. Removing thumbnails_dir_for_sync would shrink DomainArtifacts and eliminate the question of "why are these the same path stored twice?".

Fix this →


3. PeopleClientV2::insert_into_models lacks the idempotency guard added to AlbumClientV2client/people/client_v2.rs:295–307

AlbumClientV2::insert_into_models (line 628) skips an insert when the album ID is already present — this guards against the command path and AlbumEvent::AlbumAdded both firing for the same ID. PeopleClientV2::insert_into_models has no equivalent guard, so a FacesEvent::PersonAdded arriving after a successful create-command could produce a duplicate row. The pattern to match:

// In insert_into_models, before store.append:
if find_by_id(&store, id.as_str()).is_some() {
    continue; // already present — skip
}

Fix this →


4. library_context() accessor is dead but suppressed — mod.rs:278–281

#[allow(dead_code)]
pub(in crate::application) fn library_context(&self) -> Option<Arc<LibraryContext>> {
    self.imp().library_context.borrow().clone()
}

shutdown() accesses the RefCell field directly (.borrow_mut().take()), never through this accessor. Either route shutdown through the accessor (making #[allow(dead_code)] unnecessary) or remove the accessor. Suppressing a dead-code warning with #[allow] is a maintenance hazard since it masks future true dead code.


Minor notes (no action required)

  • library_context field comment (mod.rs:52–57) says clearing it drops Arc<Library> before the runtime. That's partially true — it drops the LibraryContext's clone — but MediaClientV2 and ImportClient in OnceCell<T> also hold Arc<Library> and can't be cleared from &self. The comment deeper in shutdown() correctly acknowledges this; the field-level doc is a bit misleading. Worth a one-line clarification.

  • SyncClient outbox methods reach for MomentsApplication::default().tokio_handle() rather than storing the Tokio handle during build(). This is consistent with the rest of the client surface, so no change needed — just worth documenting if the design doc is updated.

  • Phase structure is clean. The typed DomainArtifacts / BackgroundServices / BuiltClients result types make the data-flow between phases readable and the compile-time ordering guarantee is real.

@github-actions

github-actions Bot commented May 29, 2026

Copy link
Copy Markdown

📊 Code Complexity Report

Complexity    SLOC  Function
──────────  ──────  ──────────────────────────────────────────────────
        34     167  sync/providers/immich/pull.rs::run_sync
        32      47  ui/preferences_dialog/mod.rs::spawn_library_stats
        32      37  ui/preferences_dialog/mod.rs::<anonymous>
        25      66  client/people/client_v2.rs::listen
        25      65  client/album/client_v2.rs::listen
        24      59  client/media/client_v2.rs::listen_media
        23      36  client/media/client_v2.rs::on_media_changed
        22      85  ui/viewer/info_panel/camera_section.rs::set_item
        20      47  renderer/format/detect.rs::detect_from_bytes
        19     114  renderer/edits.rs::apply_pixel_adjustments
        19      70  ui/sidebar/mod.rs::build_pinned_section
        19      69  ui/album_grid/selection.rs::wire_batch_delete
        19      52  ui/album_grid/selection.rs::<anonymous>
        18     193  ui/photo_grid/mod.rs::setup
        17     184  ui/photo_grid/mod.rs::set_store
        17      28  ui/preferences_dialog/mod.rs::spawn_server_stats
        17      20  ui/preferences_dialog/mod.rs::<anonymous>
        16      92  ui/viewer/edit_panel/filter_section.rs::setup
        16      72  ui/people_grid/factory.rs::build_factory
        16      71  ui/viewer/edit_panel/mod.rs::save_to_db

Top 20 functions by cognitive complexity · job log

@github-actions

github-actions Bot commented May 29, 2026

Copy link
Copy Markdown

🔒 Security Report

Check Status
Cargo audit ❌ Fail
Cargo deny ❌ Fail

job log

@github-actions

github-actions Bot commented May 29, 2026

Copy link
Copy Markdown

CI Summary

Check Status
Typo check ✅ Pass
Build & test (fmt + clippy + unit + integration) ✅ Pass

full run log

- Log Immich client build failures via warn! instead of silently
  swallowing the error and falling back to the Local code path.
- Drop the duplicate `thumbnails_dir_for_sync` field from
  `DomainArtifacts`; phase 2 now reads `paths.thumbnails_dir`.
- Add an idempotency guard to `PeopleClientV2::insert_into_models`
  mirroring the album-side one, with a matching unit test.
- Remove the dead `library_context()` accessor on
  `MomentsApplication`; `shutdown` accesses the field directly.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@github-actions

github-actions Bot commented May 29, 2026

Copy link
Copy Markdown

Claude finished @justinf555's task in 51s —— View job


Re-review: PR #663 review findings addressed

Tasks

  • Gather context
  • Check latest commit addressing review findings
  • Verify each of the 4 issues from prior review
  • Post updated assessment

All four issues flagged in the prior review are cleanly resolved in 3dbec5e:

1. ImmichClient::new failure logged — startup.rs
The silent .ok() is replaced with a match that emits warn!("failed to build Immich HTTP client: {e}; continuing without sync"). Correct.

2. thumbnails_dir_for_sync removed — startup.rs
Field dropped from DomainArtifacts, removed from start() extraction, and phase2_background_services now reads domain.paths.thumbnails_dir.clone(). Cleaner than before.

3. PeopleClientV2::insert_into_models idempotency guard
Guard added matching the album-side pattern, plus a unit test (insert_into_models_is_idempotent) covering the exact double-insert race scenario. Good coverage.

4. Dead library_context() accessor removed
Accessor and its #[allow(dead_code)] suppression are gone. shutdown continues to access the RefCell field directly, which is the right call given the OnceCell::take/&self constraint documented in the field comment.


No new issues found. The PR is in good shape — the architectural goals are achieved, the deviations are honestly documented, CI is green, and all prior review findings are resolved. Ready to merge pending manual smoke-test sign-off (Local setup flow, Immich flow, stale-path recovery, import dialog) as noted in the test plan.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants