feat(monitoring): T23 - Surveillance automatique des libraries

- Ajout scheduler dans l'indexer (vérifie toutes les minutes)
- Migration 0004: colonnes monitor_enabled, scan_mode, next_scan_at
- API: GET /libraries avec champs monitoring
- API: PATCH /libraries/:id/monitoring pour configuration
- Composant MonitoringForm (client) avec checkbox et select
- Badge Auto/Manual avec couleurs différentes
- Affichage temps restant avant prochain scan
- Proxy route /api/libraries/:id/monitoring

Le scheduler crée automatiquement des jobs quand next_scan_at <= NOW()
This commit is contained in:
2026-03-06 11:42:41 +01:00
parent 5f51955f4d
commit 6e0a77fae0
7 changed files with 302 additions and 1 deletions

View File

@@ -92,6 +92,18 @@ async fn ready(State(state): State<AppState>) -> Result<Json<serde_json::Value>,
async fn run_worker(state: AppState, interval_seconds: u64) {
let wait = Duration::from_secs(interval_seconds.max(1));
// Start scheduler task for auto-monitoring
let scheduler_state = state.clone();
let _scheduler_handle = tokio::spawn(async move {
let scheduler_wait = Duration::from_secs(60); // Check every minute
loop {
if let Err(err) = check_and_schedule_auto_scans(&scheduler_state.pool).await {
error!("[SCHEDULER] Error: {}", err);
}
tokio::time::sleep(scheduler_wait).await;
}
});
loop {
match claim_next_job(&state.pool).await {
Ok(Some((job_id, library_id))) => {
@@ -115,6 +127,69 @@ async fn run_worker(state: AppState, interval_seconds: u64) {
}
}
async fn check_and_schedule_auto_scans(pool: &sqlx::PgPool) -> anyhow::Result<()> {
let libraries = sqlx::query(
r#"
SELECT id, scan_mode, last_scan_at
FROM libraries
WHERE monitor_enabled = TRUE
AND (
next_scan_at IS NULL
OR next_scan_at <= NOW()
)
AND NOT EXISTS (
SELECT 1 FROM index_jobs
WHERE library_id = libraries.id
AND status IN ('pending', 'running')
)
"#
)
.fetch_all(pool)
.await?;
for row in libraries {
let library_id: Uuid = row.get("id");
let scan_mode: String = row.get("scan_mode");
info!("[SCHEDULER] Auto-scanning library {} (mode: {})", library_id, scan_mode);
let job_id = Uuid::new_v4();
let job_type = match scan_mode.as_str() {
"full" => "full_rebuild",
_ => "rebuild",
};
sqlx::query(
"INSERT INTO index_jobs (id, library_id, type, status) VALUES ($1, $2, $3, 'pending')"
)
.bind(job_id)
.bind(library_id)
.bind(job_type)
.execute(pool)
.await?;
// Update next_scan_at
let interval_minutes = match scan_mode.as_str() {
"hourly" => 60,
"daily" => 1440,
"weekly" => 10080,
_ => 1440, // default daily
};
sqlx::query(
"UPDATE libraries SET last_scan_at = NOW(), next_scan_at = NOW() + INTERVAL '1 minute' * $2 WHERE id = $1"
)
.bind(library_id)
.bind(interval_minutes)
.execute(pool)
.await?;
info!("[SCHEDULER] Created job {} for library {}", job_id, library_id);
}
Ok(())
}
async fn claim_next_job(pool: &sqlx::PgPool) -> anyhow::Result<Option<(Uuid, Option<Uuid>)>> {
let mut tx = pool.begin().await?;
let row = sqlx::query(