Fix SNR calculator, add score weight controls, highlight selected filter

SNR calculator: signalFromSB calibration was 4750× too small (0.001 vs ~4.75
at SB=21). Calibration is now derived consistently from the sky background
constants: C[filter] = sky_e_s[filter] / 10^((21 - 21.5) / 2.5). Also made
it filter-aware so narrowband filters use their own reference. Replaced the
broken 500+/billions display with a proper per-filter result or a
'too faint for this setup' message when signal ≈ 0.

Score weights: 'Best score tonight' sort now accepts score_alt/fov/time/moon
query params (0.0–1.0, server-side normalised to sum=1). Frontend adds a
⚙ weights button next to the sort dropdown that reveals 4 sliders showing
effective %, persisted to localStorage. Weights default to 40/30/20/10.

Selected filter: clicking a filter pill in the Filters tab now highlights the
row (bg + amber outline on the pill + ▶ marker) so it's clear which filter
the SNR calculator and workflow card are showing.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-17 11:39:34 +02:00
parent e94f7a0ca6
commit e9f7741feb
4 changed files with 163 additions and 27 deletions
+27 -8
View File
@@ -65,6 +65,11 @@ pub struct TargetsQuery {
pub mosaic_only: Option<bool>,
pub not_imaged: Option<bool>,
pub show_custom: Option<bool>,
// Score weights for "best tonight" sort (0.01.0 each, auto-normalised server-side)
pub score_alt: Option<f64>,
pub score_fov: Option<f64>,
pub score_time: Option<f64>,
pub score_moon: Option<f64>,
}
#[derive(Debug, Serialize, sqlx::FromRow)]
@@ -233,18 +238,32 @@ pub async fn list_targets(
.unwrap_or(1.0);
// "best" sort: composite score balancing altitude, FOV fill, usable time, moon separation.
// Weights are normalised to sum=1.0 so changing one factor doesn't need all others to change.
let raw_alt = params.score_alt.unwrap_or(0.40).clamp(0.0, 1.0);
let raw_fov = params.score_fov.unwrap_or(0.30).clamp(0.0, 1.0);
let raw_time = params.score_time.unwrap_or(0.20).clamp(0.0, 1.0);
let raw_moon = params.score_moon.unwrap_or(0.10).clamp(0.0, 1.0);
let weight_sum = raw_alt + raw_fov + raw_time + raw_moon;
let (w_alt, w_fov, w_time, w_moon) = if weight_sum > 0.0 {
(raw_alt / weight_sum, raw_fov / weight_sum, raw_time / weight_sum, raw_moon / weight_sum)
} else {
(0.4, 0.3, 0.2, 0.1)
};
// Multiplied by weather_weight so cloudy nights rank all targets lower.
let best_score_expr = format!(r#"(
COALESCE(nc.max_alt_deg, 0) / 90.0 * 0.40
COALESCE(nc.max_alt_deg, 0) / 90.0 * {w_alt:.4}
+ CASE
WHEN c.fov_fill_pct IS NULL THEN 0.15
WHEN c.fov_fill_pct BETWEEN 20 AND 80 THEN (1.0 - ABS(c.fov_fill_pct - 50) / 50.0) * 0.30
WHEN c.fov_fill_pct > 80 THEN 0.10
ELSE 0.05
WHEN c.fov_fill_pct IS NULL THEN {w_fov:.4} * 0.5
WHEN c.fov_fill_pct BETWEEN 20 AND 80 THEN (1.0 - ABS(c.fov_fill_pct - 50) / 50.0) * {w_fov:.4}
WHEN c.fov_fill_pct > 80 THEN {w_fov:.4} * 0.33
ELSE {w_fov:.4} * 0.17
END
+ MIN(COALESCE(nc.usable_min, 0), 300) / 300.0 * 0.20
+ COALESCE(nc.moon_sep_deg, 90) / 180.0 * 0.10
) * {weather_weight:.2} DESC"#, weather_weight = weather_weight);
+ MIN(COALESCE(nc.usable_min, 0), 300) / 300.0 * {w_time:.4}
+ COALESCE(nc.moon_sep_deg, 90) / 180.0 * {w_moon:.4}
) * {weather_weight:.2} DESC"#,
w_alt = w_alt, w_fov = w_fov, w_time = w_time, w_moon = w_moon,
weather_weight = weather_weight
);
let sort_col_owned: String = match params.sort.as_deref() {
Some("transit") => "nc.transit_utc".to_string(),
Some("size") => "c.size_arcmin_maj DESC".to_string(),