diff --git a/CLAUDE.md b/CLAUDE.md index e9dae4b..118b9b0 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -84,7 +84,7 @@ cargo run --bin create_user -- ### Core Components **Layered Architecture:** -- **Startup wiring** (`main.rs`): only ~350 lines — env load, migrations, AppState, route registration, server bind. Background jobs are kicked off here but defined elsewhere. +- **Startup wiring** (`main.rs`): only ~350 lines — env load, migrations, AppState, route registration, server bind. Background jobs are kicked off here but defined elsewhere. Every module lives in the `image_api` library (`lib.rs`); `main.rs` and the `src/bin` tools only `use image_api::…`. Never add `mod` declarations to `main.rs` — that compiles the module a second time (doubling release builds and running its tests twice). Anything `main.rs` calls must be `pub`. - **HTTP Layer** (`handlers/{image,video,favorites}.rs`, `files.rs`, `tags.rs`, `faces.rs`, `memories.rs`, `ai/handlers.rs`): the route handlers, grouped by domain. - **Background loops** (`watcher.rs`): the file-watcher tick (`watch_files`, `process_new_files`) and the orphaned-playlist cleanup (`cleanup_orphaned_playlists`). Per-tick drains are factored into `backfill.rs` (`backfill_unhashed_backlog`, `backfill_missing_date_taken`, `backfill_missing_content_hashes`, `process_face_backlog`, `build_face_candidates`). - **Thumbnails** (`thumbnails.rs`): generation pipeline + the `IMAGE_GAUGE` / `VIDEO_GAUGE` Prometheus metrics. diff --git a/src/ai/insight_generator.rs b/src/ai/insight_generator.rs index b4f7302..005800a 100644 --- a/src/ai/insight_generator.rs +++ b/src/ai/insight_generator.rs @@ -4677,10 +4677,6 @@ Return ONLY the summary, nothing else."#, /// /// Calls `execute_tool` with empty file/image context; enabled tools /// never read those fields. - /// - /// Only used by the `reels` module (compiled in `main.rs`, not `lib.rs`), - /// so the `#[allow(dead_code)]` suppresses the lib-target warning. - #[allow(dead_code)] pub(crate) async fn run_readonly_tool_loop( &self, backend: &ResolvedBackend, diff --git a/src/ai/local_llm.rs b/src/ai/local_llm.rs index bf3510c..23c3daa 100644 --- a/src/ai/local_llm.rs +++ b/src/ai/local_llm.rs @@ -13,9 +13,6 @@ //! is request-scoped and stays in `ResolvedBackend`. This is only the //! local stack: embeddings and offline single-shot generation. -// Constructed by binaries, not the server — dead code from main.rs's view. -#![allow(dead_code)] - use std::sync::Arc; use anyhow::Result; diff --git a/src/database/reconcile.rs b/src/database/reconcile.rs index 2fade4e..9427ac0 100644 --- a/src/database/reconcile.rs +++ b/src/database/reconcile.rs @@ -26,11 +26,6 @@ //! The pass operates on the database alone — no filesystem access — //! so it doesn't need the library availability gate. -// The lib doesn't call into this module directly — the watcher (in the -// bin) does. Dead-code analysis at the lib level can't see that, so -// suppress at the module level. Tests still exercise every function. -#![allow(dead_code)] - use diesel::prelude::*; use diesel::sql_query; use diesel::sqlite::SqliteConnection; diff --git a/src/files.rs b/src/files.rs index 3044576..7a01921 100644 --- a/src/files.rs +++ b/src/files.rs @@ -1342,8 +1342,7 @@ pub struct RealFileSystem { } impl RealFileSystem { - #[allow(dead_code)] // Used in main.rs binary and tests - pub(crate) fn new(base_path: String) -> RealFileSystem { + pub fn new(base_path: String) -> RealFileSystem { RealFileSystem { base_path } } } diff --git a/src/lib.rs b/src/lib.rs index be02c53..d9c5048 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -5,6 +5,7 @@ extern crate diesel; pub mod ai; pub mod auth; +pub mod backfill; pub mod bin_progress; pub mod camera_tz; pub mod cleanup; @@ -25,6 +26,9 @@ pub mod file_types; pub mod filename_audit; pub mod files; pub mod geo; +pub mod handlers; +pub mod hls_stats; +pub mod knowledge; pub mod libraries; pub mod library_maintenance; pub mod memories; @@ -33,6 +37,7 @@ pub mod parsers; pub mod perceptual_hash; pub mod personas; pub mod prefs; +pub mod reels; pub mod service; pub mod state; pub mod tags; @@ -42,6 +47,7 @@ pub mod thumbnails; pub mod unified_search; pub mod utils; pub mod video; +pub mod watcher; // Re-export commonly used types pub use data::{Claims, ThumbnailRequest}; diff --git a/src/main.rs b/src/main.rs index 27ef1a5..3511b9c 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,9 +1,7 @@ +//! The server binary. Everything it runs lives in the `image_api` +//! library so the crate compiles once, shared with the `src/bin` tools. #![allow(clippy::too_many_arguments)] -#[macro_use] -extern crate diesel; -extern crate rayon; - use actix_web::web::Data; use actix_web_prom::PrometheusMetricsBuilder; use diesel_migrations::{EmbeddedMigrations, MigrationHarness, embed_migrations}; @@ -18,62 +16,16 @@ use actix_web::{App, HttpResponse, HttpServer, middleware, web}; use diesel::sqlite::Sqlite; use std::error::Error; -use crate::ai::InsightGenerator; -use crate::auth::login; -use crate::data::*; -use crate::database::*; -use crate::files::{RealFileSystem, move_file}; -use crate::service::ServiceBuilder; -use crate::state::AppState; -use crate::tags::*; +use image_api::ai::InsightGenerator; +use image_api::auth::login; +use image_api::database::*; +use image_api::files::{RealFileSystem, move_file}; +use image_api::service::ServiceBuilder; +use image_api::state::AppState; +use image_api::tags::*; +use image_api::*; use log::{error, info}; -mod ai; -mod auth; -mod backfill; -// Display-zone helpers land in the read-side cutover (#119/#120); the -// binary's private copy of this module doesn't use them yet. -#[allow(dead_code)] -mod camera_tz; -mod clip_search; -mod clip_watch; -mod content_hash; -mod data; -mod database; -mod date_resolver; -mod date_view; -mod duplicates; -mod error; -mod exif; -mod face_watch; -mod faces; -mod file_scan; -mod file_types; -mod files; -mod geo; -mod handlers; -mod hls_stats; -mod libraries; -mod library_maintenance; -mod perceptual_hash; -mod state; -mod tags; -mod thumbnails; -mod unified_search; -mod utils; -mod video; -mod watcher; - -mod knowledge; -mod memories; -mod otel; -mod personas; -mod prefs; -mod reels; -mod service; -#[cfg(test)] -mod testhelpers; - pub const MIGRATIONS: EmbeddedMigrations = embed_migrations!(); fn main() -> std::io::Result<()> { @@ -85,7 +37,7 @@ fn main() -> std::io::Result<()> { // Recover orphaned insight generation jobs from a previous crash. { - use crate::database::{InsightGenerationJobDao, SqliteInsightGenerationJobDao}; + use image_api::database::{InsightGenerationJobDao, SqliteInsightGenerationJobDao}; let mut dao = SqliteInsightGenerationJobDao::new(); match dao.recover_orphaned_jobs() { Ok(n) if n > 0 => { @@ -228,9 +180,9 @@ fn main() -> std::io::Result<()> { // Spawn background job to generate daily conversation summaries { - use crate::ai::generate_daily_summaries; - use crate::database::{DailySummaryDao, SqliteDailySummaryDao}; use chrono::NaiveDate; + use image_api::ai::generate_daily_summaries; + use image_api::database::{DailySummaryDao, SqliteDailySummaryDao}; // Configure date range for summary generation // Default: August 2024 ±30 days (July 1 - September 30, 2024) @@ -453,7 +405,7 @@ fn main() -> std::io::Result<()> { Box::new(database::SqlitePersonaDao::new()), ))) .app_data::>>(Data::new(Mutex::new(face_dao))) - .app_data::>(Data::new( + .app_data::>(Data::new( app_data.face_client.clone(), )) .app_data(mp::form::MultipartFormConfig::default().total_limit(1024 * 1024 * 1024)) // 1GB upload limit diff --git a/src/reels/mod.rs b/src/reels/mod.rs index b6871ae..6a5a68f 100644 --- a/src/reels/mod.rs +++ b/src/reels/mod.rs @@ -1045,7 +1045,7 @@ fn load_pregen_params(app_state: &AppState) -> (String, Option, String) } /// Spawn the nightly pre-generation scheduler. Runs behind `REEL_PREGEN_ENABLED`. -pub(crate) async fn spawn_pregen_scheduler(app_state: web::Data) { +pub async fn spawn_pregen_scheduler(app_state: web::Data) { if std::env::var("REEL_PREGEN_ENABLED").ok() != Some("1".to_string()) { log::info!("Reel pre-generation scheduler disabled (REEL_PREGEN_ENABLED != 1)"); return; @@ -1270,7 +1270,7 @@ fn reel_cache_max_age_secs() -> u64 { /// Spawn the periodic on-disk reel-cache sweeper. Runs independently of the /// pre-gen scheduler because the on-demand cache grows whether or not pre-gen /// is enabled. Disable with `REEL_CACHE_SWEEP_ENABLED=0`. -pub(crate) async fn spawn_reel_cache_sweeper(app_state: web::Data) { +pub async fn spawn_reel_cache_sweeper(app_state: web::Data) { if std::env::var("REEL_CACHE_SWEEP_ENABLED").ok().as_deref() == Some("0") { log::info!("Reel cache sweeper disabled (REEL_CACHE_SWEEP_ENABLED=0)"); return; diff --git a/src/watcher.rs b/src/watcher.rs index f226f2f..593d299 100644 --- a/src/watcher.rs +++ b/src/watcher.rs @@ -628,11 +628,11 @@ pub fn watch_files( // See database::reconcile and CLAUDE.md "Multi-library data // model" for the rules. { - let mut conn = image_api::database::connect(); + let mut conn = crate::database::connect(); if is_full_scan { library_maintenance::refresh_planner_stats(&mut conn); } - let _ = image_api::database::reconcile::run(&mut conn); + let _ = crate::database::reconcile::run(&mut conn); // Back-ref refresh: hash-keyed rows whose // (library_id, rel_path) tuple no longer matches any @@ -721,7 +721,7 @@ pub fn process_new_files( // filters. See `file_scan` for why exclusion has to happen at WalkDir // time (filter_entry) rather than at face-detect time. let files: Vec<(PathBuf, String)> = - image_api::file_scan::enumerate_indexable_files(base_path, excluded_dirs, modified_since); + crate::file_scan::enumerate_indexable_files(base_path, excluded_dirs, modified_since); if files.is_empty() { debug!("No files to process");