Add target comparison modal, integration goal progress, and session planning + full catalog expansion

Features added this session:
- Target comparison: side-by-side overlay (CompareModal) from Targets page via ⊕ button on each row; shows altitude curves, key times, filter recommendations and per-filter integration progress for two targets simultaneously
- Integration goal progress dashboard card: per-target keeper minutes vs goal hours (from CLAUDE.md §16.3) broken down by filter, with color-coded progress bars; powered by new stats.integration_goals backend query
- Session planning timeline: Gantt-style "Plan Tonight" section on Dashboard (PlanningTimeline component) — search targets, set durations, sequential scheduling from dusk, overrun warnings, clipboard export
- Slew-optimized run order toggle (nearest-neighbor sort by RA/Dec angular distance)
- Best Nights 14-day card + Monthly Highlights card on Dashboard

Catalog expansions:
- Sharpless (Sh2), VdB, LDN, Barnard dark nebulae, LBN, Melotte, Collinder, Gum, RCW, Abell PN, Abell GC, PGC bright subset
- Caldwell/Arp/Melotte/Collinder number columns + cross-reference maps
- Weather score multiplier applied to composite sort
- galaxy_cluster type (ACO badge) throughout TypeBadge, CSS, filter chips

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-17 07:20:10 +02:00
parent 8f72745bc0
commit 2bb80a8475
45 changed files with 5613 additions and 628 deletions
+21
View File
@@ -17,6 +17,7 @@ pub async fn init_db(database_url: &str) -> anyhow::Result<SqlitePool> {
.context("failed to connect to SQLite")?;
run_schema(&pool).await?;
run_migrations(&pool).await?;
seed_horizon(&pool).await?;
Ok(pool)
@@ -34,6 +35,26 @@ async fn run_schema(pool: &SqlitePool) -> anyhow::Result<()> {
Ok(())
}
/// Additive migrations for columns added after initial schema creation.
/// SQLite doesn't support IF NOT EXISTS for ADD COLUMN, so we check the error and ignore it.
async fn run_migrations(pool: &SqlitePool) -> anyhow::Result<()> {
let migrations: &[&str] = &[
"ALTER TABLE nightly_cache ADD COLUMN is_visible_tonight INTEGER DEFAULT 0",
"ALTER TABLE catalog ADD COLUMN caldwell_num INTEGER",
"ALTER TABLE catalog ADD COLUMN arp_num INTEGER",
"ALTER TABLE catalog ADD COLUMN melotte_num INTEGER",
"ALTER TABLE catalog ADD COLUMN collinder_num INTEGER",
];
for sql in migrations {
match sqlx::query(sql).execute(pool).await {
Ok(_) => tracing::info!("Migration applied: {}", &sql[..sql.len().min(60)]),
Err(e) if e.to_string().contains("duplicate column") => {}
Err(e) => tracing::warn!("Migration skipped ({}): {}", sql, e),
}
}
Ok(())
}
async fn seed_horizon(pool: &SqlitePool) -> anyhow::Result<()> {
let count: i64 = sqlx::query_scalar("SELECT COUNT(*) FROM horizon")
.fetch_one(pool)