mod catalog;
mod content;
mod error;
mod export;
mod fs;
mod lenses_mut;
mod roots;
mod settings;
mod snapshot;
mod state;
#[cfg(feature = "ui")]
mod web_embed;
#[cfg(feature = "ui")]
use std::path::PathBuf;
use std::sync::{Arc, Mutex};
use axum::Router;
use axum::routing::{get, post};
use log::info;
#[cfg(feature = "ui")]
use log::warn;
use panza::{ServeMeta, StaticMount, run as panza_run};
use crate::app::tokio_rt;
use crate::cli::catalog::open_catalog_for_read;
use crate::cli_parser::ServeCli;
use self::catalog::{
get_categories, get_delta, get_duplicates, get_entries, get_entry, get_lens, get_lenses,
};
use self::content::get_entry_content;
use self::export::{post_export_lenses, post_export_zahir};
use self::fs::{post_bulk_rename, post_delete, post_enhance, post_enhance_policy, post_rename};
use self::lenses_mut::{
delete_lens_paths, delete_lens_route, patch_lens, post_lens, post_lens_paths,
};
use self::roots::{get_current_root, get_doctor, get_roots, put_current_root};
use self::settings::{get_settings, patch_settings_route};
use self::snapshot::{get_snapshot, post_snapshot};
use self::state::{AppState, AppStateInner, ServeCatalog, SnapshotJob};
pub fn run(args: &ServeCli) -> Result<(), anyhow::Error> {
let handle = open_catalog_for_read(&args.dir)?;
info!(
"serve catalog ready: dir={} db={}",
handle.paths.dir.display(),
handle.read_path.display()
);
let state: AppState = Arc::new(Mutex::new(AppStateInner {
catalog: ServeCatalog {
dir: handle.paths.dir,
read_path: handle.read_path,
conn: handle.conn,
},
snapshot: SnapshotJob::idle(),
}));
let api = Router::new()
.route("/roots", get(get_roots))
.route(
"/roots/current",
get(get_current_root).put(put_current_root),
)
.route("/doctor", get(get_doctor))
.route("/snapshot", get(get_snapshot).post(post_snapshot))
.route("/categories", get(get_categories))
.route("/entries", get(get_entries))
.route("/entries/{*path}", get(get_entry))
.route("/content/{*path}", get(get_entry_content))
.route("/delta", get(get_delta))
.route("/duplicates", get(get_duplicates))
.route("/lenses", get(get_lenses).post(post_lens))
.route(
"/lenses/{name}",
get(get_lens).patch(patch_lens).delete(delete_lens_route),
)
.route(
"/lenses/{name}/paths",
post(post_lens_paths).delete(delete_lens_paths),
)
.route("/fs/rename", post(post_rename))
.route("/fs/bulk-rename", post(post_bulk_rename))
.route("/fs/delete", post(post_delete))
.route("/fs/enhance", post(post_enhance))
.route("/fs/enhance-policy", post(post_enhance_policy))
.route("/export/zahir", post(post_export_zahir))
.route("/export/lenses", post(post_export_lenses))
.route(
"/settings/{scope}",
get(get_settings).patch(patch_settings_route),
)
.with_state(state);
tokio_rt::runtime().block_on(panza_run(
ServeMeta {
service: "ublx",
version: env!("CARGO_PKG_VERSION"),
},
args.serve.clone(),
api,
static_mount(),
))
}
fn static_mount() -> StaticMount {
#[cfg(feature = "ui")]
{
if let Some(dir) = std::env::var_os("UBLX_WEB_DIST").map(PathBuf::from) {
if !dir.join("index.html").is_file() {
warn!(
"UBLX_WEB_DIST={}/index.html missing — run crates/ublx-web/build.sh",
dir.display()
);
} else {
info!("serve UI static mount (Dir): {}", dir.display());
}
return StaticMount::Dir(dir);
}
let assets = web_embed::embedded_assets();
if assets.contains_key("index.html") {
info!("serve UI static mount (Embedded): {} assets", assets.len());
} else {
warn!(
"feature `ui` Embedded map has no index.html — rebuild after crates/ublx-web/build.sh"
);
}
StaticMount::Embedded(assets)
}
#[cfg(not(feature = "ui"))]
{
StaticMount::None
}
}