Initial Commit
This commit is contained in:
@@ -0,0 +1,9 @@
|
||||
use sqlx::SqlitePool;
|
||||
|
||||
use crate::catalog::refresh_catalog;
|
||||
|
||||
pub async fn run_catalog_refresh(pool: SqlitePool) {
|
||||
if let Err(e) = refresh_catalog(&pool).await {
|
||||
tracing::error!("Catalog refresh failed: {}", e);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
pub mod catalog_refresh;
|
||||
pub mod nightly;
|
||||
pub mod weather_poll;
|
||||
|
||||
use sqlx::SqlitePool;
|
||||
use tokio::time::sleep;
|
||||
|
||||
use self::catalog_refresh::run_catalog_refresh;
|
||||
pub use self::nightly::precompute_tonight;
|
||||
use self::weather_poll::start_weather_scheduler;
|
||||
use crate::astronomy::astro_twilight;
|
||||
use crate::config::{LAT, LON};
|
||||
|
||||
pub fn start_all_jobs(pool: SqlitePool) {
|
||||
// Catalog refresh on startup (respects TTL)
|
||||
let pool_cat = pool.clone();
|
||||
tokio::spawn(async move {
|
||||
run_catalog_refresh(pool_cat).await;
|
||||
});
|
||||
|
||||
// Initial weather poll
|
||||
let pool_wx = pool.clone();
|
||||
tokio::spawn(async move {
|
||||
if let Err(e) = crate::weather::poll_weather(&pool_wx).await {
|
||||
tracing::error!("Initial weather poll failed: {}", e);
|
||||
}
|
||||
});
|
||||
|
||||
// Weather scheduler
|
||||
start_weather_scheduler(pool.clone());
|
||||
|
||||
// Nightly precompute: run at dusk each day
|
||||
let pool_night = pool.clone();
|
||||
tokio::spawn(async move {
|
||||
loop {
|
||||
// Run once immediately on startup
|
||||
if let Err(e) = precompute_tonight(&pool_night).await {
|
||||
tracing::error!("Nightly precompute failed: {}", e);
|
||||
}
|
||||
|
||||
// Sleep until next dusk
|
||||
sleep_until_next_dusk().await;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async fn sleep_until_next_dusk() {
|
||||
// Compute tonight's dusk and sleep until then
|
||||
let today = chrono::Utc::now().naive_utc().date();
|
||||
let tomorrow = today + chrono::Duration::days(1);
|
||||
|
||||
let dusk = astro_twilight(tomorrow, LAT, LON)
|
||||
.map(|(d, _)| d)
|
||||
.unwrap_or_else(|_| chrono::Utc::now() + chrono::Duration::hours(24));
|
||||
|
||||
let now = chrono::Utc::now();
|
||||
let wait = if dusk > now {
|
||||
(dusk - now).to_std().unwrap_or(std::time::Duration::from_secs(3600))
|
||||
} else {
|
||||
std::time::Duration::from_secs(3600)
|
||||
};
|
||||
|
||||
tracing::info!("Next nightly precompute scheduled in {:.0}h", wait.as_secs_f32() / 3600.0);
|
||||
let tokio_dur = tokio::time::Duration::from_secs(wait.as_secs());
|
||||
sleep(tokio_dur).await;
|
||||
}
|
||||
@@ -0,0 +1,229 @@
|
||||
use chrono::{DateTime, Duration, NaiveDate, Utc};
|
||||
use sqlx::SqlitePool;
|
||||
|
||||
use crate::astronomy::{
|
||||
astro_twilight, compute_visibility, julian_date, moon_age_days, moon_altitude,
|
||||
moon_illumination, moon_phase_name, moon_position, moon_rise_set, true_dark_window,
|
||||
HorizonPoint, MoonState, TonightWindow,
|
||||
};
|
||||
use crate::config::{LAT, LON};
|
||||
use crate::filters::top_filter;
|
||||
|
||||
struct CatalogObj {
|
||||
id: String,
|
||||
ra_deg: f64,
|
||||
dec_deg: f64,
|
||||
obj_type: String,
|
||||
}
|
||||
|
||||
pub async fn precompute_tonight(pool: &SqlitePool) -> anyhow::Result<()> {
|
||||
let today = Utc::now().naive_utc().date();
|
||||
precompute_for_date(pool, today).await?;
|
||||
|
||||
// Also precompute next 90 nights (lightweight)
|
||||
for i in 1..=90i64 {
|
||||
let date = today + Duration::days(i);
|
||||
if let Err(e) = precompute_lightweight(pool, date).await {
|
||||
tracing::warn!("Lightweight precompute for {} failed: {}", date, e);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn precompute_for_date(pool: &SqlitePool, date: NaiveDate) -> anyhow::Result<()> {
|
||||
let start = std::time::Instant::now();
|
||||
tracing::info!("Nightly precompute for {}", date);
|
||||
|
||||
// 1. Compute twilight
|
||||
let (dusk, dawn) = astro_twilight(date, LAT, LON)?;
|
||||
|
||||
// 2. Moon state
|
||||
let midnight = dusk + (dawn - dusk) / 2;
|
||||
let jd = julian_date(midnight);
|
||||
let (moon_ra, moon_dec) = moon_position(jd);
|
||||
let moon_illum = moon_illumination(jd);
|
||||
let moon_age = moon_age_days(jd);
|
||||
let moon_phase = moon_phase_name(moon_illum, moon_age);
|
||||
let moon_alt = moon_altitude(jd, LAT, LON);
|
||||
let (moon_rise, moon_set) = moon_rise_set(dusk, dawn, LAT, LON);
|
||||
let true_dark = true_dark_window(dusk, dawn, LAT, LON);
|
||||
let (true_dark_start, true_dark_end, true_dark_min) = match true_dark {
|
||||
Some((s, e)) => (Some(s), Some(e), Some((e - s).num_minutes() as i32)),
|
||||
None => (None, None, Some(0)),
|
||||
};
|
||||
|
||||
// 3. Upsert tonight table
|
||||
let now_ts = Utc::now().timestamp();
|
||||
sqlx::query(
|
||||
r#"INSERT OR REPLACE INTO tonight
|
||||
(id, date, astro_dusk_utc, astro_dawn_utc,
|
||||
moon_rise_utc, moon_set_utc, moon_illumination, moon_phase_name,
|
||||
moon_ra_deg, moon_dec_deg,
|
||||
true_dark_start_utc, true_dark_end_utc, true_dark_minutes, computed_at)
|
||||
VALUES (1, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"#,
|
||||
)
|
||||
.bind(date.to_string())
|
||||
.bind(dusk.to_rfc3339())
|
||||
.bind(dawn.to_rfc3339())
|
||||
.bind(moon_rise.map(|t| t.to_rfc3339()))
|
||||
.bind(moon_set.map(|t| t.to_rfc3339()))
|
||||
.bind(moon_illum)
|
||||
.bind(&moon_phase)
|
||||
.bind(moon_ra)
|
||||
.bind(moon_dec)
|
||||
.bind(true_dark_start.map(|t| t.to_rfc3339()))
|
||||
.bind(true_dark_end.map(|t| t.to_rfc3339()))
|
||||
.bind(true_dark_min)
|
||||
.bind(now_ts)
|
||||
.execute(pool)
|
||||
.await?;
|
||||
|
||||
// 4. Load horizon
|
||||
let horizon: Vec<HorizonPoint> = sqlx::query_as(
|
||||
"SELECT az_deg, alt_deg FROM horizon ORDER BY az_deg",
|
||||
)
|
||||
.fetch_all(pool)
|
||||
.await?;
|
||||
|
||||
let moon_state = MoonState {
|
||||
ra_deg: moon_ra,
|
||||
dec_deg: moon_dec,
|
||||
illumination: moon_illum,
|
||||
alt_at_midnight: moon_alt,
|
||||
};
|
||||
|
||||
let window = TonightWindow { dusk, dawn };
|
||||
|
||||
// 5. Load all catalog objects
|
||||
let objects: Vec<CatalogObj> = sqlx::query_as::<_, (String, f64, f64, String)>(
|
||||
"SELECT id, ra_deg, dec_deg, obj_type FROM catalog",
|
||||
)
|
||||
.fetch_all(pool)
|
||||
.await?
|
||||
.into_iter()
|
||||
.map(|(id, ra, dec, obj_type)| CatalogObj { id, ra_deg: ra, dec_deg: dec, obj_type })
|
||||
.collect();
|
||||
|
||||
let n_objects = objects.len();
|
||||
|
||||
// 6. Compute visibility for each object and upsert nightly_cache
|
||||
let date_str = date.to_string();
|
||||
let mut tx = pool.begin().await?;
|
||||
|
||||
for obj in &objects {
|
||||
let vis = compute_visibility(obj.ra_deg, obj.dec_deg, &window, &horizon, &moon_state);
|
||||
let rec_filter = top_filter(
|
||||
&obj.obj_type,
|
||||
moon_illum * 100.0,
|
||||
moon_alt,
|
||||
vis.moon_sep_deg,
|
||||
);
|
||||
let vis_json = serde_json::to_string(&vis.curve).unwrap_or_default();
|
||||
|
||||
sqlx::query(
|
||||
r#"INSERT OR REPLACE INTO nightly_cache
|
||||
(catalog_id, night_date, max_alt_deg, transit_utc, rise_utc, set_utc,
|
||||
best_start_utc, best_end_utc, usable_min, meridian_flip_utc,
|
||||
airmass_at_transit, extinction_mag, moon_sep_deg, recommended_filter, visibility_json)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"#,
|
||||
)
|
||||
.bind(&obj.id)
|
||||
.bind(&date_str)
|
||||
.bind(vis.max_alt_deg)
|
||||
.bind(vis.transit_utc.map(|t| t.to_rfc3339()))
|
||||
.bind(vis.rise_utc.map(|t| t.to_rfc3339()))
|
||||
.bind(vis.set_utc.map(|t| t.to_rfc3339()))
|
||||
.bind(vis.best_start_utc.map(|t| t.to_rfc3339()))
|
||||
.bind(vis.best_end_utc.map(|t| t.to_rfc3339()))
|
||||
.bind(vis.usable_min as i32)
|
||||
.bind(vis.meridian_flip_utc.map(|t| t.to_rfc3339()))
|
||||
.bind(vis.airmass_at_transit)
|
||||
.bind(vis.extinction_at_transit)
|
||||
.bind(vis.moon_sep_deg)
|
||||
.bind(&rec_filter)
|
||||
.bind(&vis_json)
|
||||
.execute(&mut *tx)
|
||||
.await?;
|
||||
}
|
||||
tx.commit().await?;
|
||||
|
||||
tracing::info!(
|
||||
"Nightly precompute complete: {} objects processed in {:.1}s",
|
||||
n_objects,
|
||||
start.elapsed().as_secs_f32()
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Lightweight precompute: only max_alt, transit, usable_min, recommended_filter.
|
||||
/// Skips full visibility curve for performance.
|
||||
async fn precompute_lightweight(pool: &SqlitePool, date: NaiveDate) -> anyhow::Result<()> {
|
||||
// Check if already computed
|
||||
let existing: i64 = sqlx::query_scalar(
|
||||
"SELECT COUNT(*) FROM nightly_cache WHERE night_date = ?",
|
||||
)
|
||||
.bind(date.to_string())
|
||||
.fetch_one(pool)
|
||||
.await?;
|
||||
|
||||
if existing > 0 {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let (dusk, dawn) = astro_twilight(date, LAT, LON)?;
|
||||
let midnight = dusk + (dawn - dusk) / 2;
|
||||
let jd = julian_date(midnight);
|
||||
let (moon_ra, moon_dec) = moon_position(jd);
|
||||
let moon_illum = moon_illumination(jd);
|
||||
let moon_alt = moon_altitude(jd, LAT, LON);
|
||||
|
||||
let horizon: Vec<HorizonPoint> = sqlx::query_as(
|
||||
"SELECT az_deg, alt_deg FROM horizon ORDER BY az_deg",
|
||||
)
|
||||
.fetch_all(pool)
|
||||
.await?;
|
||||
|
||||
let moon_state = MoonState {
|
||||
ra_deg: moon_ra,
|
||||
dec_deg: moon_dec,
|
||||
illumination: moon_illum,
|
||||
alt_at_midnight: moon_alt,
|
||||
};
|
||||
let window = TonightWindow { dusk, dawn };
|
||||
|
||||
let objects: Vec<CatalogObj> = sqlx::query_as::<_, (String, f64, f64, String)>(
|
||||
"SELECT id, ra_deg, dec_deg, obj_type FROM catalog",
|
||||
)
|
||||
.fetch_all(pool)
|
||||
.await?
|
||||
.into_iter()
|
||||
.map(|(id, ra, dec, ot)| CatalogObj { id, ra_deg: ra, dec_deg: dec, obj_type: ot })
|
||||
.collect();
|
||||
|
||||
let date_str = date.to_string();
|
||||
let mut tx = pool.begin().await?;
|
||||
|
||||
for obj in &objects {
|
||||
let vis = compute_visibility(obj.ra_deg, obj.dec_deg, &window, &horizon, &moon_state);
|
||||
let rec_filter = top_filter(&obj.obj_type, moon_illum * 100.0, moon_alt, vis.moon_sep_deg);
|
||||
|
||||
sqlx::query(
|
||||
r#"INSERT OR IGNORE INTO nightly_cache
|
||||
(catalog_id, night_date, max_alt_deg, transit_utc, usable_min, recommended_filter)
|
||||
VALUES (?, ?, ?, ?, ?, ?)"#,
|
||||
)
|
||||
.bind(&obj.id)
|
||||
.bind(&date_str)
|
||||
.bind(vis.max_alt_deg)
|
||||
.bind(vis.transit_utc.map(|t: DateTime<Utc>| t.to_rfc3339()))
|
||||
.bind(vis.usable_min as i32)
|
||||
.bind(&rec_filter)
|
||||
.execute(&mut *tx)
|
||||
.await?;
|
||||
}
|
||||
tx.commit().await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
use sqlx::SqlitePool;
|
||||
use tokio::time::{sleep, Duration};
|
||||
|
||||
use crate::weather::poll_weather;
|
||||
|
||||
pub fn start_weather_scheduler(pool: SqlitePool) {
|
||||
// 3-hour weather poll
|
||||
let pool_3h = pool.clone();
|
||||
tokio::spawn(async move {
|
||||
loop {
|
||||
if let Err(e) = poll_weather(&pool_3h).await {
|
||||
tracing::error!("Weather poll (3h) failed: {}", e);
|
||||
}
|
||||
sleep(Duration::from_secs(3 * 3600)).await;
|
||||
}
|
||||
});
|
||||
|
||||
// 15-minute dew point poll (open-meteo only)
|
||||
tokio::spawn(async move {
|
||||
loop {
|
||||
sleep(Duration::from_secs(15 * 60)).await;
|
||||
if let Err(e) = crate::weather::openmeteo::fetch_openmeteo().await {
|
||||
tracing::warn!("Dew point poll failed: {}", e);
|
||||
} else {
|
||||
tracing::debug!("Dew point poll OK");
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user