Fix catastrophic targets query and remove emoji from night mode buttons

The seasonal peak subquery used a correlated SELECT inside a GROUP BY,
causing a full nightly_cache scan per object (210-270s for 14k objects).
Replaced with a simple MAX() GROUP BY — now instant.

Also added three indexes on nightly_cache(night_date) that were missing
and causing all dashboard queries to run 2+ second full table scans.

Replaced 🔴 emoji in night mode buttons with CSS circles.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-17 11:02:24 +02:00
parent 4fbc578413
commit a562dae1e3
3 changed files with 31 additions and 12 deletions
+15 -9
View File
@@ -269,8 +269,7 @@ pub async fn list_targets(
nc.best_start_utc, nc.best_end_utc, nc.moon_sep_deg,
COALESCE(nc.is_visible_tonight, CASE WHEN nc.max_alt_deg >= 15 THEN 1 ELSE 0 END) as is_visible_tonight,
COALESCE(log_sum.total_min, 0) as total_integration,
seas.peak_alt as seasonal_peak_alt,
seas.peak_date as seasonal_peak_date
seas.peak_alt as seasonal_peak_alt
FROM catalog c
LEFT JOIN nightly_cache nc ON nc.catalog_id = c.id AND nc.night_date = '{today}'
LEFT JOIN (
@@ -278,10 +277,8 @@ pub async fn list_targets(
FROM imaging_log GROUP BY catalog_id
) log_sum ON log_sum.catalog_id = c.id
LEFT JOIN (
SELECT catalog_id,
MAX(max_alt_deg) as peak_alt,
MIN(CASE WHEN max_alt_deg = (SELECT MAX(max_alt_deg) FROM nightly_cache n2 WHERE n2.catalog_id = n1.catalog_id AND n2.night_date BETWEEN '{today}' AND date('{today}', '+90 days')) THEN night_date ELSE NULL END) as peak_date
FROM nightly_cache n1
SELECT catalog_id, MAX(max_alt_deg) as peak_alt
FROM nightly_cache
WHERE night_date BETWEEN '{today}' AND date('{today}', '+90 days')
GROUP BY catalog_id
) seas ON seas.catalog_id = c.id
@@ -311,14 +308,23 @@ pub async fn list_targets(
use sqlx::Row;
let tonight_alt: f64 = row.try_get::<Option<f64>, _>("max_alt_deg").unwrap_or_default().unwrap_or(0.0);
let peak_alt: f64 = row.try_get::<Option<f64>, _>("seasonal_peak_alt").unwrap_or_default().unwrap_or(0.0);
let peak_date: Option<String> = row.try_get("seasonal_peak_date").unwrap_or_default();
// Urgency: compare tonight's altitude to the 90-day seasonal peak.
// Direction (rising/declining) is estimated from the object's RA: objects whose
// transit RA is ahead of the current sidereal time are rising into season.
let ra_deg: f64 = row.try_get::<f64, _>("ra_deg").unwrap_or_default();
let urgency: serde_json::Value = if peak_alt >= 15.0 && tonight_alt >= 15.0 {
let ratio = tonight_alt / peak_alt;
if ratio >= 0.90 {
serde_json::json!("peak")
} else if ratio >= 0.70 {
let before_peak = peak_date.as_deref().map(|d| d > today.as_str()).unwrap_or(true);
serde_json::json!(if before_peak { "rising" } else { "declining" })
// Rising if the object's RA puts it transiting later this season:
// April 17 → sun RA ≈ 27°. Objects with RA 90270° ahead are rising.
let sun_ra = {
let day_of_year = chrono::Utc::now().format("%j").to_string().parse::<f64>().unwrap_or(107.0);
(day_of_year / 365.25 * 360.0 + 280.46) % 360.0
};
let diff = (ra_deg - sun_ra).rem_euclid(360.0);
serde_json::json!(if diff > 90.0 && diff < 270.0 { "rising" } else { "declining" })
} else {
serde_json::Value::Null
}
+4
View File
@@ -44,6 +44,10 @@ async fn run_migrations(pool: &SqlitePool) -> anyhow::Result<()> {
"ALTER TABLE catalog ADD COLUMN arp_num INTEGER",
"ALTER TABLE catalog ADD COLUMN melotte_num INTEGER",
"ALTER TABLE catalog ADD COLUMN collinder_num INTEGER",
// Performance indexes for nightly_cache date-range queries
"CREATE INDEX IF NOT EXISTS idx_nc_date ON nightly_cache(night_date)",
"CREATE INDEX IF NOT EXISTS idx_nc_date_alt ON nightly_cache(night_date, max_alt_deg)",
"CREATE INDEX IF NOT EXISTS idx_nc_catalog_date ON nightly_cache(catalog_id, night_date)",
];
for sql in migrations {
match sqlx::query(sql).execute(pool).await {
+12 -3
View File
@@ -49,7 +49,6 @@ export function BottomNav() {
background: on ? 'rgba(160,0,0,0.85)' : 'var(--bg-panel)',
border: `1px solid ${on ? '#800000' : 'var(--border)'}`,
color: on ? '#ff6666' : 'var(--text-lo)',
fontSize: 16,
display: 'none', // shown by .bottom-nav display:flex breakpoint via CSS class
alignItems: 'center',
justifyContent: 'center',
@@ -59,7 +58,12 @@ export function BottomNav() {
}}
className="night-fab"
>
🔴
<span style={{
display: 'inline-block', width: 10, height: 10, borderRadius: '50%',
background: on ? '#ff4444' : 'var(--text-lo)',
boxShadow: on ? '0 0 6px #ff4444' : 'none',
transition: 'all 0.3s',
}} />
</button>
<nav className="bottom-nav">
{navItems.map(item => (
@@ -192,7 +196,12 @@ export default function Sidebar() {
letterSpacing: '0.06em', transition: 'all 0.2s',
}}
>
<span style={{ fontSize: 13 }}>🔴</span>
<span style={{
display: 'inline-block', width: 8, height: 8, borderRadius: '50%',
background: nightOn ? '#ff4444' : 'var(--text-lo)',
boxShadow: nightOn ? '0 0 5px #ff4444' : 'none',
flexShrink: 0, transition: 'all 0.2s',
}} />
{nightOn ? 'Exit Night Mode' : 'Night Mode'}
</button>
</div>