Initial Commit

This commit is contained in:
2026-04-09 23:23:31 +02:00
commit a88a905d52
94 changed files with 15170 additions and 0 deletions
+9
View File
@@ -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);
}
}
+66
View File
@@ -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;
}
+229
View File
@@ -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(())
}
+29
View File
@@ -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");
}
}
});
}