1178 lines
38 KiB
Markdown
1178 lines
38 KiB
Markdown
# Claude Code Prompt — Astronome: Personal Deep-Sky Observatory App
|
||
## Stack: Rust · Axum · SQLx · SQLite · React · Vite · Docker Compose
|
||
|
||
---
|
||
|
||
## 0. Project Intent
|
||
|
||
Build a self-hosted personal astrophotography planning and logging web application
|
||
deployed on a NAS via Docker Compose. This is a single-user tool — no auth, no
|
||
multi-tenancy. All decisions are pre-encoded below. Build everything without asking
|
||
clarifying questions.
|
||
|
||
---
|
||
|
||
## 1. Repository Structure
|
||
|
||
```
|
||
astronome/
|
||
├── backend/
|
||
│ ├── Cargo.toml
|
||
│ ├── Dockerfile
|
||
│ ├── src/
|
||
│ │ ├── main.rs
|
||
│ │ ├── config.rs # OBS constants, compile-time
|
||
│ │ ├── db/
|
||
│ │ │ ├── mod.rs
|
||
│ │ │ ├── schema.sql # CREATE TABLE statements
|
||
│ │ │ └── migrations/ # sqlx migrations
|
||
│ │ ├── catalog/
|
||
│ │ │ ├── mod.rs
|
||
│ │ │ ├── fetch.rs # OpenNGC CSV fetch + parse
|
||
│ │ │ ├── filter.rs # suitability filter for this setup
|
||
│ │ │ └── popular_names.rs
|
||
│ │ ├── astronomy/
|
||
│ │ │ ├── mod.rs
|
||
│ │ │ ├── time.rs # JD, LST
|
||
│ │ │ ├── coords.rs # RA/Dec → Alt/Az, airmass
|
||
│ │ │ ├── solar.rs # sun altitude, dusk/dawn
|
||
│ │ │ ├── lunar.rs # moon phase, rise/set, position
|
||
│ │ │ ├── horizon.rs # custom horizon interpolation
|
||
│ │ │ └── visibility.rs # per-object tonight summary
|
||
│ │ ├── filters/
|
||
│ │ │ └── mod.rs # recommendation engine
|
||
│ │ ├── weather/
|
||
│ │ │ ├── mod.rs
|
||
│ │ │ ├── seventimer.rs # 7timer ASTRO API client
|
||
│ │ │ └── openmeteo.rs # Open-Meteo current conditions
|
||
│ │ ├── jobs/
|
||
│ │ │ ├── mod.rs
|
||
│ │ │ ├── nightly.rs # precompute tonight's data
|
||
│ │ │ ├── weather_poll.rs
|
||
│ │ │ └── catalog_refresh.rs
|
||
│ │ ├── phd2/
|
||
│ │ │ └── mod.rs # PHD2 CSV log parser
|
||
│ │ └── api/
|
||
│ │ ├── mod.rs
|
||
│ │ ├── targets.rs
|
||
│ │ ├── tonight.rs
|
||
│ │ ├── calendar.rs
|
||
│ │ ├── log.rs
|
||
│ │ ├── weather.rs
|
||
│ │ ├── phd2.rs
|
||
│ │ ├── horizon.rs
|
||
│ │ └── stats.rs
|
||
├── frontend/
|
||
│ ├── package.json
|
||
│ ├── vite.config.ts
|
||
│ ├── Dockerfile
|
||
│ ├── src/
|
||
│ │ ├── main.tsx
|
||
│ │ ├── App.tsx
|
||
│ │ ├── api/ # typed fetch wrappers per endpoint
|
||
│ │ ├── hooks/ # useTargets, useTonight, useWeather, etc.
|
||
│ │ ├── pages/
|
||
│ │ │ ├── Dashboard.tsx
|
||
│ │ │ ├── Targets.tsx
|
||
│ │ │ ├── Calendar.tsx
|
||
│ │ │ ├── Stats.tsx
|
||
│ │ │ └── Settings.tsx
|
||
│ │ ├── components/
|
||
│ │ │ ├── layout/ # Sidebar, TopBar, PageShell
|
||
│ │ │ ├── targets/ # TargetRow, DetailDrawer, tabs
|
||
│ │ │ ├── charts/ # AltitudeCurve, AirmassBar, VisBar
|
||
│ │ │ ├── sky/ # AladinEmbed, MoonPhaseIcon
|
||
│ │ │ ├── weather/ # WeatherCard, DewAlert, GoNogo
|
||
│ │ │ ├── log/ # LogForm, SessionList, QualityFlag
|
||
│ │ │ ├── phd2/ # PHD2UploadZone, GuidingStats
|
||
│ │ │ └── gallery/ # ImageUploadZone, LightboxView
|
||
│ │ └── styles/
|
||
│ │ ├── tokens.css
|
||
│ │ └── global.css
|
||
├── docker-compose.yml
|
||
└── nginx.conf
|
||
```
|
||
|
||
---
|
||
|
||
## 2. Observer Constants (hardcode everywhere, never configurable)
|
||
|
||
```rust
|
||
// backend/src/config.rs
|
||
pub const LAT: f64 = 43.8167; // Villevieille, France
|
||
pub const LON: f64 = 4.1167;
|
||
pub const BORTLE: u8 = 5;
|
||
|
||
pub const FOCAL_MM: f64 = 490.0;
|
||
pub const APERTURE_MM: f64 = 71.0;
|
||
pub const FOCAL_RATIO: f64 = 6.9;
|
||
|
||
// ToupTek ATR2600C / IMX571
|
||
pub const PIXEL_UM: f64 = 3.76;
|
||
pub const RES_X: u32 = 6248;
|
||
pub const RES_Y: u32 = 4176;
|
||
|
||
// Derived — never recompute
|
||
pub const PLATE_SCALE_ARCSEC: f64 = 1.584;
|
||
pub const FOV_DEG_W: f64 = 2.75;
|
||
pub const FOV_DEG_H: f64 = 1.84;
|
||
pub const FOV_ARCMIN_W: f64 = 165.0;
|
||
pub const FOV_ARCMIN_H: f64 = 110.4;
|
||
|
||
pub const MIN_ALT_DEG: f64 = 15.0;
|
||
pub const MIN_DURATION_MIN: u32 = 45;
|
||
```
|
||
|
||
---
|
||
|
||
## 3. Database Schema
|
||
|
||
```sql
|
||
-- backend/src/db/schema.sql
|
||
|
||
-- OpenNGC catalog cache (refreshed weekly)
|
||
CREATE TABLE IF NOT EXISTS catalog (
|
||
id TEXT PRIMARY KEY, -- "NGC1234", "IC0434", "M42"
|
||
name TEXT NOT NULL,
|
||
common_name TEXT, -- "Orion Nebula", "Horsehead Nebula"
|
||
obj_type TEXT NOT NULL, -- normalized: galaxy, emission_nebula, etc.
|
||
ra_deg REAL NOT NULL,
|
||
dec_deg REAL NOT NULL,
|
||
ra_h TEXT NOT NULL, -- "05h 34m 32s"
|
||
dec_dms TEXT NOT NULL, -- "+22° 00′ 52″"
|
||
constellation TEXT,
|
||
size_arcmin_maj REAL,
|
||
size_arcmin_min REAL,
|
||
pos_angle_deg REAL,
|
||
mag_v REAL,
|
||
surface_brightness REAL,
|
||
hubble_type TEXT,
|
||
messier_num INTEGER,
|
||
is_highlight BOOLEAN DEFAULT FALSE,
|
||
-- derived for this setup
|
||
fov_fill_pct REAL, -- (size_arcmin_maj / FOV_ARCMIN_H) * 100
|
||
mosaic_flag BOOLEAN DEFAULT FALSE,
|
||
mosaic_panels_w INTEGER DEFAULT 1,
|
||
mosaic_panels_h INTEGER DEFAULT 1,
|
||
difficulty INTEGER, -- 1 (easy) to 5 (very hard)
|
||
guide_star_density TEXT, -- "rich", "moderate", "sparse"
|
||
fetched_at INTEGER NOT NULL -- unix timestamp
|
||
);
|
||
|
||
-- Nightly precomputed visibility (refreshed each evening at sunset)
|
||
CREATE TABLE IF NOT EXISTS nightly_cache (
|
||
catalog_id TEXT NOT NULL,
|
||
night_date TEXT NOT NULL, -- "2026-04-08" (local date of the evening)
|
||
max_alt_deg REAL,
|
||
transit_utc TEXT,
|
||
rise_utc TEXT, -- above MIN_ALT_DEG
|
||
set_utc TEXT,
|
||
best_start_utc TEXT, -- above 30°
|
||
best_end_utc TEXT,
|
||
usable_min INTEGER,
|
||
meridian_flip_utc TEXT,
|
||
airmass_at_transit REAL,
|
||
extinction_mag REAL,
|
||
moon_sep_deg REAL,
|
||
recommended_filter TEXT,
|
||
visibility_json TEXT, -- JSON array of {t, alt, az} every 10 min
|
||
PRIMARY KEY (catalog_id, night_date)
|
||
);
|
||
|
||
-- Tonight summary (single row, refreshed at sunset)
|
||
CREATE TABLE IF NOT EXISTS tonight (
|
||
id INTEGER PRIMARY KEY CHECK (id = 1),
|
||
date TEXT NOT NULL,
|
||
astro_dusk_utc TEXT NOT NULL,
|
||
astro_dawn_utc TEXT NOT NULL,
|
||
moon_rise_utc TEXT,
|
||
moon_set_utc TEXT,
|
||
moon_illumination REAL,
|
||
moon_phase_name TEXT,
|
||
moon_ra_deg REAL,
|
||
moon_dec_deg REAL,
|
||
true_dark_start_utc TEXT, -- moon below horizon AND astro dark
|
||
true_dark_end_utc TEXT,
|
||
true_dark_minutes INTEGER,
|
||
computed_at INTEGER
|
||
);
|
||
|
||
-- Custom horizon profile
|
||
CREATE TABLE IF NOT EXISTS horizon (
|
||
az_deg INTEGER PRIMARY KEY, -- 0–359, every degree
|
||
alt_deg REAL NOT NULL DEFAULT 15.0
|
||
);
|
||
|
||
-- Imaging log
|
||
CREATE TABLE IF NOT EXISTS imaging_log (
|
||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||
catalog_id TEXT NOT NULL,
|
||
session_date TEXT NOT NULL, -- "2026-04-08"
|
||
filter_id TEXT NOT NULL, -- "sv220", "c2", "uvir", "sv260"
|
||
integration_min INTEGER NOT NULL,
|
||
quality TEXT NOT NULL DEFAULT 'pending', -- 'keeper','needs_more','rejected','pending'
|
||
notes TEXT,
|
||
guiding_rms REAL, -- populated from PHD2 log if uploaded
|
||
mean_temp_c REAL,
|
||
phd2_log_id INTEGER,
|
||
created_at INTEGER NOT NULL DEFAULT (unixepoch())
|
||
);
|
||
|
||
-- Target gallery images
|
||
CREATE TABLE IF NOT EXISTS gallery (
|
||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||
catalog_id TEXT NOT NULL,
|
||
log_id INTEGER,
|
||
filename TEXT NOT NULL, -- stored in /data/gallery/
|
||
caption TEXT,
|
||
created_at INTEGER NOT NULL DEFAULT (unixepoch())
|
||
);
|
||
|
||
-- PHD2 guiding log analysis results
|
||
CREATE TABLE IF NOT EXISTS phd2_logs (
|
||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||
session_date TEXT NOT NULL,
|
||
filename TEXT NOT NULL,
|
||
rms_total REAL,
|
||
rms_ra REAL,
|
||
rms_dec REAL,
|
||
peak_error REAL,
|
||
star_lost_count INTEGER,
|
||
duration_min INTEGER,
|
||
guide_star_snr REAL,
|
||
created_at INTEGER NOT NULL DEFAULT (unixepoch())
|
||
);
|
||
|
||
-- Weather cache
|
||
CREATE TABLE IF NOT EXISTS weather_cache (
|
||
id INTEGER PRIMARY KEY CHECK (id = 1),
|
||
seventimer_json TEXT, -- raw 7timer ASTRO response
|
||
openmeteo_json TEXT, -- raw Open-Meteo current conditions
|
||
dew_point_c REAL,
|
||
temp_c REAL,
|
||
humidity_pct REAL,
|
||
go_nogo TEXT, -- "go", "marginal", "nogo"
|
||
fetched_at INTEGER
|
||
);
|
||
|
||
-- App settings
|
||
CREATE TABLE IF NOT EXISTS settings (
|
||
key TEXT PRIMARY KEY,
|
||
value TEXT NOT NULL
|
||
);
|
||
```
|
||
|
||
---
|
||
|
||
## 4. Filter Arsenal (hardcode in Rust)
|
||
|
||
```rust
|
||
// backend/src/filters/mod.rs
|
||
|
||
pub enum FilterId { UvIr, Sv260, C2, Sv220 }
|
||
|
||
pub struct Filter {
|
||
pub id: FilterId,
|
||
pub name: &'static str,
|
||
pub filter_type: &'static str,
|
||
pub moon_limit_pct: u8,
|
||
pub suitable_types: &'static [ObjType],
|
||
pub lp_reduction: &'static str,
|
||
pub extraction_method: Option<&'static str>,
|
||
pub channels: &'static [&'static str],
|
||
}
|
||
|
||
pub const FILTERS: [Filter; 4] = [
|
||
Filter {
|
||
id: FilterId::UvIr,
|
||
name: "ZWO UV/IR Cut",
|
||
filter_type: "broadband",
|
||
moon_limit_pct: 40,
|
||
suitable_types: &[Galaxy, ReflectionNebula, OpenCluster, GlobularCluster],
|
||
lp_reduction: "low",
|
||
extraction_method: None,
|
||
channels: &[],
|
||
},
|
||
Filter {
|
||
id: FilterId::Sv260,
|
||
name: "SVBony SV260",
|
||
filter_type: "broadband_lp",
|
||
moon_limit_pct: 55,
|
||
suitable_types: &[Galaxy, EmissionNebula, ReflectionNebula, OpenCluster, GlobularCluster],
|
||
lp_reduction: "medium",
|
||
extraction_method: None,
|
||
channels: &[],
|
||
},
|
||
Filter {
|
||
id: FilterId::C2,
|
||
name: "Askar C2",
|
||
filter_type: "dual_narrowband",
|
||
moon_limit_pct: 95,
|
||
suitable_types: &[EmissionNebula, Snr, PlanetaryNebula],
|
||
lp_reduction: "high",
|
||
extraction_method: Some("DBXtract"),
|
||
channels: &["SII_15nm", "OIII_35nm"],
|
||
},
|
||
Filter {
|
||
id: FilterId::Sv220,
|
||
name: "SVBony SV220",
|
||
filter_type: "dual_narrowband",
|
||
moon_limit_pct: 98,
|
||
suitable_types: &[EmissionNebula, Snr, PlanetaryNebula],
|
||
lp_reduction: "high",
|
||
extraction_method: Some("DBXtract"),
|
||
channels: &["Ha_7nm", "OIII_7nm"],
|
||
},
|
||
];
|
||
```
|
||
|
||
---
|
||
|
||
## 5. Catalog Pipeline
|
||
|
||
### 5.1 Fetch & Parse
|
||
|
||
Fetch both CSVs from GitHub raw in parallel on first startup and weekly thereafter
|
||
(7-day TTL via `catalog.fetched_at` in SQLite):
|
||
|
||
```
|
||
https://raw.githubusercontent.com/mattiaverga/OpenNGC/master/database_files/NGC.csv
|
||
https://raw.githubusercontent.com/mattiaverga/OpenNGC/master/database_files/IC.csv
|
||
```
|
||
|
||
Parse with the `csv` crate. Strip invalid rows (missing RA/Dec, invalid type).
|
||
|
||
### 5.2 Filtering for this setup
|
||
|
||
Keep objects where ALL of:
|
||
- Type in: `GX, GC, OC, EN, RN, PN, SNR, BN, NF, DN`
|
||
- Dec between −30° and +75°
|
||
- MajAx > 0.1 arcmin (not stellar)
|
||
|
||
### 5.3 Derived fields (compute once, store in DB)
|
||
|
||
**FOV fill ratio:**
|
||
```rust
|
||
let fill_pct = (size_arcmin_maj / FOV_ARCMIN_H).min(1.0) * 100.0;
|
||
```
|
||
|
||
**Mosaic flag and panel count:**
|
||
```rust
|
||
let panels_w = (size_arcmin_maj / FOV_ARCMIN_W).ceil() as u32;
|
||
let panels_h = (size_arcmin_maj / FOV_ARCMIN_H).ceil() as u32;
|
||
let mosaic_flag = panels_w > 1 || panels_h > 1;
|
||
// panels_w × panels_h e.g. "2×1", "2×2", "3×2"
|
||
```
|
||
|
||
**Difficulty rating (1–5):**
|
||
```rust
|
||
fn difficulty(obj_type, size_arcmin_maj, mag_v, surface_brightness, bortle) -> u8 {
|
||
// Start at 2
|
||
// +1 if SB > 13 mag/arcsec² (faint extended)
|
||
// +1 if size < 2 arcmin (small, needs good seeing)
|
||
// +1 if mag_v > 11.0
|
||
// +1 if obj_type == DarkNebula
|
||
// -1 if obj_type == OpenCluster
|
||
// -1 if mosaic (just flag it, not harder per se, but different)
|
||
// clamp 1..=5
|
||
}
|
||
```
|
||
|
||
**Guide star density:**
|
||
Query the number of catalog stars within a 1° radius at the target's RA/Dec
|
||
using a bundled Tycho-2 subset. Return `"rich"` (>50), `"moderate"` (15–50),
|
||
`"sparse"` (<15).
|
||
|
||
Because bundling Tycho-2 is large, use a simpler proxy:
|
||
- Galactic latitude |b| > 30°: likely sparse for emission nebulae in the halo
|
||
- |b| < 10°: rich (galactic plane)
|
||
- 10–30°: moderate
|
||
- Always mark targets with Galactic longitude 0–30° as rich (galactic centre direction)
|
||
|
||
**Surface brightness:** use `SurfBr` from OpenNGC CSV directly.
|
||
|
||
### 5.4 Popular names
|
||
|
||
Hardcode a HashMap of NGC/IC/Messier → common name:
|
||
|
||
Include at minimum: M1→Crab Nebula, M8→Lagoon Nebula, M16→Eagle Nebula,
|
||
M17→Omega Nebula, M20→Trifid Nebula, M27→Dumbbell Nebula, M31→Andromeda Galaxy,
|
||
M33→Triangulum Galaxy, M42→Orion Nebula, M43→De Mairan's Nebula,
|
||
M45→Pleiades, M51→Whirlpool Galaxy, M57→Ring Nebula, M63→Sunflower Galaxy,
|
||
M64→Black Eye Galaxy, M78→McNeil's Nebula area, M81→Bode's Galaxy,
|
||
M82→Cigar Galaxy, M97→Owl Nebula, M101→Pinwheel Galaxy, M104→Sombrero Galaxy,
|
||
NGC224=M31, NGC598=M33, NGC1499→California Nebula, NGC1952=M1,
|
||
NGC1976=M42, NGC2068=M78, NGC2237→Rosette Nebula, NGC2244→Rosette Cluster,
|
||
NGC3372→Eta Carinae Nebula, NGC5128→Centaurus A, NGC6992→Eastern Veil Nebula,
|
||
NGC6960→Western Veil Nebula, NGC6979→Pickering's Triangle,
|
||
NGC7000→North America Nebula, IC405→Flaming Star Nebula,
|
||
IC410→Tadpoles Nebula, IC434→Horsehead Nebula, IC1805→Heart Nebula,
|
||
IC1848→Soul Nebula, IC2118→Witch Head Nebula, IC5146→Cocoon Nebula,
|
||
IC1396→Elephant Trunk Nebula, Sh2-132→Lion Nebula, Sh2-155→Cave Nebula,
|
||
Sh2-308→Dolphin Nebula (add ~80 total).
|
||
|
||
---
|
||
|
||
## 6. Astronomy Engine (Rust)
|
||
|
||
### 6.1 Julian Date
|
||
Standard formula. Input: chrono::DateTime<Utc>. Output: f64.
|
||
|
||
### 6.2 Local Sidereal Time
|
||
Output in degrees 0–360. Use IAU formula via JD.
|
||
|
||
### 6.3 Hour Angle → Alt/Az
|
||
Standard spherical trig. Input: ra_deg, dec_deg, lst_deg, lat_deg. Output: (alt_deg, az_deg).
|
||
|
||
### 6.4 Airmass & Extinction
|
||
```rust
|
||
fn airmass(alt_deg: f64) -> f64 {
|
||
// Rozenberg formula — valid to horizon
|
||
let z = (90.0 - alt_deg).to_radians();
|
||
1.0 / (z.cos() + 0.025 * (-11.0 * z.cos()).exp())
|
||
}
|
||
|
||
fn extinction_mag(alt_deg: f64) -> f64 {
|
||
// k = 0.20 mag/airmass (typical Bortle 5 site)
|
||
airmass(alt_deg) * 0.20
|
||
}
|
||
```
|
||
|
||
### 6.5 Custom Horizon Alt at Azimuth
|
||
```rust
|
||
fn horizon_alt(az_deg: f64, horizon: &[HorizonPoint]) -> f64 {
|
||
// linear interpolation between stored 1°-resolution points
|
||
}
|
||
```
|
||
|
||
An object is considered visible when `alt > max(MIN_ALT_DEG, horizon_alt(az))`.
|
||
|
||
### 6.6 Sun altitude solver
|
||
Binary search to find exact UTC times when sun alt = −18° (astronomical twilight)
|
||
for tonight. Use 1-minute resolution. Return `(dusk_utc, dawn_utc)`.
|
||
|
||
### 6.7 Moon
|
||
```rust
|
||
struct MoonState {
|
||
illumination: f64, // 0.0–1.0
|
||
age_days: f64,
|
||
phase_name: String, // "Waxing Crescent", "Full Moon", etc.
|
||
ra_deg: f64,
|
||
dec_deg: f64,
|
||
rise_utc: Option<DateTime<Utc>>,
|
||
set_utc: Option<DateTime<Utc>>,
|
||
// above horizon currently
|
||
alt_deg: f64,
|
||
}
|
||
```
|
||
|
||
Moon rise/set: step through the night window at 5-minute intervals, detect sign
|
||
change in altitude, linearly interpolate crossing.
|
||
|
||
### 6.8 True Dark Window
|
||
```rust
|
||
fn true_dark_window(dusk: DateTime<Utc>, dawn: DateTime<Utc>, moon: &MoonState)
|
||
-> Option<(DateTime<Utc>, DateTime<Utc>)>
|
||
{
|
||
// Walk dusk→dawn in 5-min steps
|
||
// True dark = sun alt < -18° AND moon alt < 0°
|
||
// Return the longest continuous such interval
|
||
}
|
||
```
|
||
|
||
### 6.9 Meridian Flip Time (GEM28)
|
||
```rust
|
||
fn meridian_flip_utc(ra_deg: f64, lat_deg: f64, lon_deg: f64, date: DateTime<Utc>)
|
||
-> DateTime<Utc>
|
||
{
|
||
// Time when HA = 0 (transit) + mount's flip buffer
|
||
// GEM28 default flip at HA = +5° past meridian
|
||
// Return transit_utc + duration_for_ha_5deg
|
||
}
|
||
```
|
||
|
||
### 6.10 Visibility Summary per Object (tonight)
|
||
```rust
|
||
struct VisibilitySummary {
|
||
max_alt_deg: f64,
|
||
transit_utc: DateTime<Utc>,
|
||
rise_utc: Option<DateTime<Utc>>, // above custom horizon
|
||
set_utc: Option<DateTime<Utc>>,
|
||
best_start_utc: Option<DateTime<Utc>>, // alt > 30°
|
||
best_end_utc: Option<DateTime<Utc>>,
|
||
usable_min: u32,
|
||
is_visible_tonight: bool,
|
||
meridian_flip_utc: Option<DateTime<Utc>>,
|
||
airmass_at_transit: f64,
|
||
extinction_at_transit: f64,
|
||
moon_sep_deg: f64,
|
||
curve: Vec<CurvePoint>, // every 10 min, dusk to dawn
|
||
}
|
||
|
||
struct CurvePoint {
|
||
utc: DateTime<Utc>,
|
||
alt_deg: f64,
|
||
az_deg: f64,
|
||
airmass: f64,
|
||
above_custom_horizon: bool,
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## 7. Filter Recommendation Engine
|
||
|
||
```rust
|
||
fn recommend_filters(
|
||
obj_type: ObjType,
|
||
moon_illumination_pct: f64,
|
||
moon_alt_deg: f64,
|
||
moon_sep_deg: f64,
|
||
) -> Vec<FilterRecommendation> {
|
||
// Returns ordered recommendations with reason strings
|
||
}
|
||
|
||
struct FilterRecommendation {
|
||
filter_id: String,
|
||
suitability: Suitability, // Ideal, Good, Marginal, Unsuitable
|
||
reason: String,
|
||
warning: Option<String>,
|
||
}
|
||
```
|
||
|
||
Decision matrix (implement exactly):
|
||
|
||
| Moon % | Object type | Order |
|
||
|--------|-------------|-------|
|
||
| 0–25 | emission/snr/pn | sv220, c2, sv260, uvir |
|
||
| 25–60 | emission/snr/pn | sv220, c2, sv260 |
|
||
| 60–95 | emission/snr/pn | sv220, c2 |
|
||
| >95 | emission/snr/pn | sv220 only |
|
||
| 0–40 | galaxy/reflection | uvir, sv260 |
|
||
| 40–55 | galaxy/reflection | sv260, uvir |
|
||
| >55 | galaxy/reflection | sv260 (warn: moon very high); skip uvir |
|
||
| any | open_cluster | uvir, sv260 |
|
||
| any | globular_cluster | uvir, sv260 |
|
||
| any | dark_nebula | uvir |
|
||
|
||
Additional flags:
|
||
- `moon_proximity_warning: true` if moon_sep_deg < 30°
|
||
- `moon_below_horizon_bonus: true` if moon_alt_deg < 0° (upgrade marginal → good)
|
||
|
||
---
|
||
|
||
## 8. Processing Workflow Definitions
|
||
|
||
```rust
|
||
struct Workflow {
|
||
name: &'static str,
|
||
steps: &'static [&'static str],
|
||
plugins: &'static [(&'static str, &'static str)], // (plugin, one-line purpose)
|
||
notes: &'static str,
|
||
}
|
||
```
|
||
|
||
Define these workflows (map from `(obj_type, filter_id)` → workflow):
|
||
|
||
**BROADBAND_OSC** (galaxy/reflection + uvir or sv260):
|
||
steps: WBPP → SPCC (GAIA DR3) → BlurXTerminator → NoiseXTerminator v3 → GHS →
|
||
DarkStructureEnhance → StarXTerminator (optional) → SetiAstro Statistical Stretch
|
||
|
||
**HA_OIII_DUAL** (emission + sv220):
|
||
steps: WBPP → DBXtract (Ha + OIII channels) → SPCC on each channel →
|
||
NarrowBandNormalization → BlurXTerminator per channel → NoiseXTerminator v3 →
|
||
StarXTerminator → HOO composition (Ha→R, OIII→G+B) → GHS
|
||
|
||
**SII_OIII_DUAL** (emission + c2):
|
||
steps: WBPP → DBXtract (SII + OIII channels) → NarrowBandNormalization →
|
||
BlurXTerminator → NoiseXTerminator v3 → StarXTerminator → SHO-like composition
|
||
(SII→R, Ha from sv220 if available→G, OIII→B) → GHS
|
||
note: "Combine OIII from C2 with OIII from SV220 if both sessions available.
|
||
SII at 15nm is faint — prioritize long integrations."
|
||
|
||
**CLUSTER** (open/globular + uvir):
|
||
steps: WBPP → SPCC → BlurXTerminator (star-optimised) → NoiseXTerminator v3 →
|
||
GHS (gentle S-curve only) → no star removal
|
||
|
||
---
|
||
|
||
## 9. External API Integrations
|
||
|
||
### 9.1 7timer ASTRO API (weather intelligence)
|
||
|
||
Endpoint:
|
||
```
|
||
http://www.7timer.info/bin/api.pl?lon=4.1167&lat=43.8167&product=astro&output=json
|
||
```
|
||
|
||
No API key required. Poll every 3 hours. Store raw JSON in `weather_cache`.
|
||
|
||
Parse and display these fields per forecast slot (3-hourly, 8 days):
|
||
- `cloudcover` (1–9 → clear to overcast)
|
||
- `seeing` (1–8 → excellent to bad, map to arcsec)
|
||
- `transparency` (1–8)
|
||
- `lifted_index` (atmospheric stability, negative = unstable)
|
||
- `rh2m` (relative humidity at 2m)
|
||
- `temp2m`
|
||
- `wind10m` → speed + direction
|
||
|
||
**Go/No-go logic:**
|
||
```rust
|
||
fn go_nogo(cloudcover: u8, seeing: u8, transparency: u8) -> GoNogo {
|
||
if cloudcover <= 2 && seeing <= 3 && transparency <= 3 { GoNogo::Go }
|
||
else if cloudcover <= 4 && seeing <= 5 { GoNogo::Marginal }
|
||
else { GoNogo::NoGo }
|
||
}
|
||
```
|
||
|
||
### 9.2 Open-Meteo current conditions (dew point alert)
|
||
|
||
Endpoint:
|
||
```
|
||
https://api.open-meteo.com/v1/forecast
|
||
?latitude=43.8167&longitude=4.1167
|
||
¤t=temperature_2m,relative_humidity_2m,dew_point_2m
|
||
&wind_speed_unit=ms
|
||
```
|
||
|
||
No API key required. Poll every 15 minutes.
|
||
|
||
Dew point alert logic:
|
||
```rust
|
||
fn dew_alert(temp_c: f64, dew_point_c: f64) -> Option<DewAlert> {
|
||
let margin = temp_c - dew_point_c;
|
||
if margin < 2.0 { Some(DewAlert::Critical) }
|
||
else if margin < 4.0 { Some(DewAlert::Warning) }
|
||
else { None }
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## 10. Background Jobs (Tokio tasks)
|
||
|
||
### 10.1 Catalog Refresh (weekly)
|
||
- Check `catalog.fetched_at` on startup
|
||
- If > 7 days old or table empty: fetch OpenNGC CSVs, parse, rebuild catalog table
|
||
- Log progress to stdout
|
||
|
||
### 10.2 Nightly Precomputation (daily at astronomical dusk)
|
||
Tokio task sleeping until computed dusk time each day, then running:
|
||
1. Compute tonight's sun/moon window (dusk, dawn, moon rise/set, true dark)
|
||
2. Upsert `tonight` table
|
||
3. Load custom horizon from DB
|
||
4. For every catalog object: compute full `VisibilitySummary`
|
||
5. Upsert all rows into `nightly_cache` for today's date
|
||
6. Compute recommended filter per object given moon state
|
||
7. Log: "Nightly precompute complete: N objects processed in Xs"
|
||
|
||
Also precompute the next 90 nights (without full visibility curve — just
|
||
`max_alt_deg`, `transit_utc`, `usable_min`, `recommended_filter`) for the
|
||
seasonal calendar.
|
||
|
||
### 10.3 Weather Poll (every 3 hours)
|
||
Fetch 7timer + Open-Meteo. Upsert `weather_cache`. Compute go/nogo.
|
||
|
||
### 10.4 Dew Point Poll (every 15 minutes)
|
||
Only fetches Open-Meteo current conditions. Update `weather_cache.dew_point_c`.
|
||
|
||
---
|
||
|
||
## 11. PHD2 Log Parser
|
||
|
||
PHD2 writes a CSV with a header block then data columns:
|
||
`Frame,Time,mount,dx,dy,RARawDistance,DECRawDistance,RAGuideDistance,DECGuideDistance,
|
||
RADuration,RADirection,DECDuration,DECDirection,XStep,YStep,StarMass,SNR,ErrorCode`
|
||
|
||
```rust
|
||
pub struct Phd2Analysis {
|
||
pub duration_min: u32,
|
||
pub total_frames: u32,
|
||
pub rms_ra_arcsec: f64,
|
||
pub rms_dec_arcsec: f64,
|
||
pub rms_total_arcsec: f64,
|
||
pub peak_error_arcsec: f64,
|
||
pub star_lost_count: u32,
|
||
pub mean_snr: f64,
|
||
pub drift_ra_arcsec_per_min: f64,
|
||
pub drift_dec_arcsec_per_min: f64,
|
||
}
|
||
```
|
||
|
||
Parse the CSV, skip `ErrorCode != 0` rows (guide star lost events — count them),
|
||
compute RMS for RA and Dec columns in arcsec (multiply pixels by plate scale
|
||
only if pixel scale is embedded in the PHD2 header — otherwise use arcsec columns
|
||
if available). Store in `phd2_logs` table.
|
||
|
||
The `/api/phd2/upload` endpoint accepts multipart form upload of the `.log` file.
|
||
|
||
---
|
||
|
||
## 12. Image Gallery
|
||
|
||
Images stored on disk at `/data/gallery/{catalog_id}/{filename}`.
|
||
The Docker volume maps `/data` to a NAS path.
|
||
|
||
Accepted formats: JPEG, PNG, TIFF (convert TIFF to JPEG on ingest via the
|
||
`image` crate). Max size: 50 MB per file.
|
||
|
||
Return gallery images via `/api/gallery/{catalog_id}/{filename}` with
|
||
`Cache-Control: max-age=31536000`.
|
||
|
||
---
|
||
|
||
## 13. REST API Contract
|
||
|
||
All responses are JSON. All timestamps are ISO 8601 UTC strings.
|
||
|
||
```
|
||
GET /api/targets?type=&constellation=&filter=&tonight=true&search=&sort=&page=&limit=
|
||
GET /api/targets/:id
|
||
GET /api/targets/:id/visibility # tonight's full visibility summary
|
||
GET /api/targets/:id/curve # visibility curve JSON for tonight
|
||
GET /api/targets/:id/filters # filter recommendations given tonight's moon
|
||
GET /api/targets/:id/workflow/:filter_id # processing workflow
|
||
|
||
GET /api/tonight # full tonight summary (moon, dark window, etc.)
|
||
GET /api/calendar?months=3 # 90-day grid of nightly summaries
|
||
GET /api/calendar/:date # one night's summary with top targets
|
||
|
||
GET /api/weather # current conditions + 7-day forecast
|
||
GET /api/weather/forecast # 8-day 3-hourly 7timer data
|
||
|
||
GET /api/stats # imaging statistics aggregates
|
||
GET /api/log # all log entries, paginated
|
||
GET /api/log/:catalog_id # log entries for one target
|
||
POST /api/log # create log entry
|
||
PUT /api/log/:id # update log entry (quality, notes)
|
||
DELETE /api/log/:id # delete log entry
|
||
|
||
POST /api/phd2/upload # multipart: upload + parse PHD2 log
|
||
GET /api/phd2 # list all parsed PHD2 sessions
|
||
GET /api/phd2/:id # one PHD2 analysis
|
||
|
||
GET /api/gallery/:catalog_id # list images for target
|
||
POST /api/gallery/:catalog_id # upload image (multipart)
|
||
DELETE /api/gallery/:id
|
||
|
||
GET /api/horizon # current horizon profile (360 points)
|
||
PUT /api/horizon # replace entire horizon (JSON array)
|
||
|
||
GET /api/health # { status: "ok", catalog_size: N, db_version: "..." }
|
||
```
|
||
|
||
---
|
||
|
||
## 14. Docker Compose
|
||
|
||
```yaml
|
||
# docker-compose.yml
|
||
services:
|
||
backend:
|
||
build: ./backend
|
||
restart: unless-stopped
|
||
volumes:
|
||
- ./data:/data # SQLite DB + gallery images
|
||
environment:
|
||
- DATABASE_URL=sqlite:///data/astronome.db
|
||
- RUST_LOG=info
|
||
ports:
|
||
- "3001:3001"
|
||
|
||
frontend:
|
||
build: ./frontend
|
||
restart: unless-stopped
|
||
ports:
|
||
- "3000:80"
|
||
depends_on:
|
||
- backend
|
||
|
||
nginx:
|
||
image: nginx:alpine
|
||
restart: unless-stopped
|
||
ports:
|
||
- "80:80"
|
||
volumes:
|
||
- ./nginx.conf:/etc/nginx/nginx.conf:ro
|
||
depends_on:
|
||
- backend
|
||
- frontend
|
||
```
|
||
|
||
```nginx
|
||
# nginx.conf — reverse proxy, single origin for browser
|
||
upstream backend { server backend:3001; }
|
||
upstream frontend { server frontend:80; }
|
||
|
||
server {
|
||
listen 80;
|
||
location /api/ { proxy_pass http://backend; }
|
||
location / { proxy_pass http://frontend; }
|
||
client_max_body_size 60M; # for image uploads
|
||
}
|
||
```
|
||
|
||
### Rust Dockerfile (multi-stage, minimal image)
|
||
```dockerfile
|
||
FROM rust:1.77-slim AS builder
|
||
WORKDIR /app
|
||
COPY . .
|
||
RUN cargo build --release
|
||
|
||
FROM debian:bookworm-slim
|
||
RUN apt-get update && apt-get install -y libssl-dev ca-certificates && rm -rf /var/lib/apt/lists/*
|
||
COPY --from=builder /app/target/release/astronome /usr/local/bin/
|
||
CMD ["astronome"]
|
||
```
|
||
|
||
### Frontend Dockerfile
|
||
```dockerfile
|
||
FROM node:20-alpine AS builder
|
||
WORKDIR /app
|
||
COPY package*.json ./
|
||
RUN npm ci
|
||
COPY . .
|
||
RUN npm run build
|
||
|
||
FROM nginx:alpine
|
||
COPY --from=builder /app/dist /usr/share/nginx/html
|
||
COPY nginx-frontend.conf /etc/nginx/conf.d/default.conf
|
||
```
|
||
|
||
---
|
||
|
||
## 15. Frontend Architecture
|
||
|
||
### 15.1 Dependencies
|
||
|
||
```json
|
||
{
|
||
"dependencies": {
|
||
"react": "^18",
|
||
"react-dom": "^18",
|
||
"react-router-dom": "^6",
|
||
"recharts": "^2",
|
||
"react-query": "^5",
|
||
"date-fns": "^3"
|
||
},
|
||
"devDependencies": {
|
||
"typescript": "^5",
|
||
"vite": "^5",
|
||
"@types/react": "^18"
|
||
}
|
||
}
|
||
```
|
||
|
||
Load Aladin Lite via CDN script tag injected in index.html:
|
||
```html
|
||
<script src="https://aladin.u-strasbg.fr/AladinLite/api/v3/latest/aladin.min.js"></script>
|
||
```
|
||
|
||
### 15.2 Design System
|
||
|
||
**Aesthetic direction:** Deep-space observatory. Industrial-refined dark UI.
|
||
Feels like a professional astronomy workstation, not a consumer app.
|
||
Monospace coordinates. Amber accent on black. Sparse, purposeful layout.
|
||
|
||
**Typefaces** (load from Google Fonts):
|
||
- Display / nav labels: `"Syne"` — geometric, authoritative
|
||
- Body / data: `"IBM Plex Mono"` — coordinates, numbers, timestamps feel native
|
||
- Prose / notes: `"IBM Plex Sans"` — readable at small sizes
|
||
|
||
**CSS custom properties:**
|
||
```css
|
||
:root {
|
||
/* Backgrounds */
|
||
--bg-void: #080a0f; /* page bg — near black with blue tint */
|
||
--bg-deep: #0d1017; /* primary panels */
|
||
--bg-panel: #111520; /* cards */
|
||
--bg-row: #141825; /* table rows */
|
||
--bg-hover: #1a2035;
|
||
|
||
/* Accent palette */
|
||
--amber: #e8832a; /* primary — warm, telescope-brass */
|
||
--amber-dim: #7a4415;
|
||
--amber-glow: rgba(232,131,42,0.12);
|
||
--blue: #4d9de0; /* secondary — cool sky */
|
||
--blue-dim: #1a3d5c;
|
||
--teal: #2ab8a0; /* tertiary */
|
||
|
||
/* Semantic */
|
||
--good: #3dba72;
|
||
--warn: #e8c030;
|
||
--danger: #e05252;
|
||
--info: #4d9de0;
|
||
--muted: #3a4258;
|
||
|
||
/* Text */
|
||
--text-hi: #edf0f5;
|
||
--text-mid: #8892a8;
|
||
--text-lo: #4a5268;
|
||
--text-amber: #e8832a;
|
||
|
||
/* Borders */
|
||
--border: #1e2538;
|
||
--border-hi: #2e3858;
|
||
|
||
/* Type */
|
||
--font-display: 'Syne', sans-serif;
|
||
--font-mono: 'IBM Plex Mono', monospace;
|
||
--font-sans: 'IBM Plex Sans', sans-serif;
|
||
}
|
||
```
|
||
|
||
**Grid:** left sidebar (220px, fixed) + main content area. No top navbar.
|
||
|
||
**Motion:** Subtle only. Row expand with `max-height` CSS transition (200ms ease).
|
||
Drawer tabs fade in (150ms). No page transitions. No loading skeletons that bounce.
|
||
|
||
**Type badges:** each object type gets a distinctive pill:
|
||
- GX: `--blue` bg, "GX" label
|
||
- EN: `--teal` bg, "EN"
|
||
- PN: `--good` bg, "PN"
|
||
- SNR: `--amber` bg, "SNR"
|
||
- GC: `#9b59b6` bg, "GC"
|
||
- OC: `#f1c40f` bg dark text, "OC"
|
||
- RN: `#e67e22` bg, "RN"
|
||
- DN: `--muted` bg, "DN"
|
||
|
||
**Quality flag chips:**
|
||
- keeper: `--good` bg + checkmark
|
||
- needs_more: `--blue` bg + arrow
|
||
- rejected: `--danger` bg + cross
|
||
- pending: `--muted` bg + dot
|
||
|
||
### 15.3 Sidebar Navigation
|
||
|
||
```
|
||
ASTRONOME ← logo in --amber, font-display
|
||
|
||
[icon] Dashboard
|
||
[icon] Targets
|
||
[icon] Calendar
|
||
[icon] Statistics
|
||
[icon] Settings
|
||
|
||
─────────────────
|
||
Tonight
|
||
Dusk 21:34
|
||
Dawn 05:12
|
||
Dark 23:10–02:45
|
||
Moon 34% ◑
|
||
|
||
─────────────────
|
||
Conditions
|
||
[go/nogo badge]
|
||
Seeing: 2.1″
|
||
Transp: Good
|
||
Temp: 12°C
|
||
Dew Δ: 6°C ✓
|
||
```
|
||
|
||
Sidebar data updates every 60 seconds via react-query polling.
|
||
|
||
---
|
||
|
||
## 16. Pages
|
||
|
||
### 16.1 Dashboard
|
||
|
||
Four stat cards at top:
|
||
- Go/No-go indicator for tonight (large, colored)
|
||
- Moon illumination + phase icon
|
||
- True dark window duration ("3h 35min of full dark")
|
||
- Best target tonight (highest-altitude, best-filtered)
|
||
|
||
Below: 2-column layout
|
||
- Left: 7-day weather forecast (cloudcover + seeing bars per day)
|
||
- Right: Top 5 targets tonight (compact list with altitude + filter pill)
|
||
|
||
Dew point alert banner (full-width, --danger bg) if margin < 4°C.
|
||
|
||
### 16.2 Targets Page
|
||
|
||
**Filter bar** (horizontal, sticky below header):
|
||
- Type chips: All · Galaxy · Emission · Reflection · Planetary · SNR · Cluster · Dark
|
||
- Constellation dropdown (grouped alphabetically)
|
||
- Filter fit: UV/IR · SV260 · C2 · SV220
|
||
- Status: Tonight only (default ON) · Not yet imaged · Needs more data · All
|
||
- Sort: Best alt tonight · Transit · Size · Magnitude · Difficulty · Total integration
|
||
- Search input (fuzzy: name, common name, NGC, IC, M)
|
||
|
||
**Target list:**
|
||
Compact rows. Columns:
|
||
```
|
||
[TYPE] [NAME / COMMON NAME] [CONST] [SIZE ′] [FILL %] [MOSAIC?] [MAG] [SB] [★] [FILTER] [ALT NOW] [VIS BAR] [QUALITY]
|
||
```
|
||
|
||
- SIZE: `maj × min` in arcmin
|
||
- FILL %: color-coded: >80% green, 40–80% amber, <40% muted
|
||
- MOSAIC?: if true, show `2×1` or `3×2` etc. in --warn color
|
||
- SB: surface brightness mag/arcsec² — only for nebulae/galaxies
|
||
- ★: difficulty 1–5 as dot cluster
|
||
- ALT NOW: live, updates every 30s, color-coded
|
||
- VIS BAR: 80px inline SVG showing tonight arc
|
||
- QUALITY: quality chip from last log entry, or "—" if never imaged
|
||
|
||
Rows with `is_visible_tonight = false`: opacity 0.35, italic, pushed to bottom.
|
||
|
||
Click anywhere on row → expands detail drawer below (accordion, one open at a time).
|
||
|
||
### 16.3 Detail Drawer (4 tabs)
|
||
|
||
**Tab 1 — Tonight**
|
||
- Altitude curve (Recharts LineChart): x = dusk to dawn UTC, y = 0–90°
|
||
- Custom horizon line (from DB, render as step function)
|
||
- 15° line (dashed muted)
|
||
- 30° line (dashed good)
|
||
- Moon altitude curve (dimmed --blue line)
|
||
- Object altitude curve: colored segment: >30° = --good, 15–30° = --warn, <15° = --danger
|
||
- Vertical line at current time (--amber)
|
||
- Meridian flip marker (--amber dashed vertical)
|
||
- True dark window shaded background region (subtle --amber-glow)
|
||
- Key times table (monospace): Rise / Transit / Set / Best window / Flip / Moon sep
|
||
- Airmass at transit + extinction in magnitudes
|
||
|
||
**Tab 2 — Target**
|
||
- Left: DSS image thumbnail (fetch from STScI DSS endpoint)
|
||
`https://archive.stsci.edu/cgi-bin/dss_search?v=poss2ukstu_red&r={ra}&d={dec}&e=J2000&h={fov_h_arcmin}&w={fov_w_arcmin}&f=gif`
|
||
- Right: metadata table (type, constellation, RA/Dec sexagesimal + decimal, size, mag, SB, hubble type)
|
||
- Below: Aladin Lite embed (lazy-init on tab open)
|
||
Survey: P/DSS2/color. FOV: 4.0°. Draw amber rectangle 2.75°×1.84° centered on target.
|
||
If mosaic: draw all panels as individual rectangles with overlap shading.
|
||
- Guiding context badge: star density + note e.g. "Sparse field — OAG may struggle. Consider bright guide star nearby."
|
||
|
||
**Tab 3 — Filters & Workflow**
|
||
Filter recommendation table (all 4 filters, ordered by suitability):
|
||
```
|
||
[FILTER NAME] [SUITABILITY PILL] [REASON] [EST. INTEGRATION] [SESSIONS NEEDED]
|
||
```
|
||
Integration estimates table:
|
||
```rust
|
||
// hours to usable result: Bortle 5, f/6.9, OSC
|
||
galaxy: { uvir: 4.0, sv260: 6.0 }
|
||
emission_nebula: { sv220: 3.0, c2: 4.0, sv260: 8.0, uvir: 12.0 }
|
||
reflection_nebula: { uvir: 3.0, sv260: 5.0 }
|
||
planetary_nebula: { sv220: 2.0, c2: 3.0 }
|
||
snr: { sv220: 5.0, c2: 6.0 }
|
||
open_cluster: { uvir: 1.0 }
|
||
globular_cluster: { uvir: 1.5 }
|
||
dark_nebula: { uvir: 3.0 }
|
||
```
|
||
Sessions needed = ceil(hours / 2).
|
||
|
||
Below: Processing Workflow card (collapsible):
|
||
- Workflow name + numbered step list
|
||
- Plugin table: plugin name | purpose in one line
|
||
- Notes in italic
|
||
|
||
**Tab 4 — Log & Gallery**
|
||
Two sub-columns:
|
||
- Left: Session log list (most recent first). Each entry: date · filter pill · duration · quality chip · guiding RMS if available · notes. Edit button on each.
|
||
Add session form at top: date picker · filter select · duration (min) · quality select · notes · PHD2 log file upload (optional) · Save.
|
||
Total accumulated: "X sessions · Y h Z min total"
|
||
- Right: Image gallery grid. Thumbnail grid (3 cols). Click → lightbox fullscreen.
|
||
Upload zone (drag-and-drop or click) with JPEG/PNG/TIFF accept.
|
||
|
||
### 16.4 Calendar Page
|
||
|
||
12-month grid (or 3-month depending on API param). Each month = a calendar grid.
|
||
Each day cell shows:
|
||
- Moon phase icon (SVG crescent drawn from illumination %)
|
||
- Usable dark hours bar (0–8h range, colored by go/nogo if forecast available)
|
||
- Color-coded bg: full dark (>4h) = deep teal; partial dark = amber; full moon = muted
|
||
|
||
Clicking a day → side panel showing:
|
||
- That night's summary (dusk/dawn, moon rise/set, true dark window)
|
||
- Top 10 targets for that night (pre-computed)
|
||
- Weather forecast for that night (7timer data)
|
||
|
||
Lunar cycle overlay: new moon nights have a `●` marker. Full moon nights `○`.
|
||
Best narrowband nights (illumination < 20%) highlighted with --amber border.
|
||
|
||
### 16.5 Statistics Page
|
||
|
||
Header stat cards (from SQL aggregates):
|
||
- Total sessions logged
|
||
- Total integration time (h)
|
||
- Objects imaged (unique catalog_ids with at least one keeper)
|
||
- Filters usage pie (Recharts PieChart)
|
||
|
||
Charts:
|
||
- Integration per month (bar chart, last 12 months)
|
||
- Object type breakdown (horizontal bar)
|
||
- Filter usage per target type (stacked bar)
|
||
- Guiding RMS over time (scatter plot from phd2_logs — x = date, y = rms_total)
|
||
- Best and worst seeing nights correlation chart (7timer seeing vs guiding RMS)
|
||
|
||
Target list ordered by: most integrated · most sessions · most recent
|
||
|
||
Quality breakdown table: keeper vs needs_more vs rejected per object type.
|
||
|
||
### 16.6 Settings Page
|
||
|
||
**Custom Horizon:**
|
||
- Interactive polar chart (SVG) showing current horizon profile
|
||
- Upload CSV button: format `az_deg,alt_deg` one row per degree
|
||
- Manual edit mode: click on the polar chart to set a horizon point
|
||
- Reset to flat 15° button
|
||
- Preview: shows how current horizon affects tonight's top 10 targets
|
||
|
||
**App info:**
|
||
- Catalog stats: N objects, last refreshed date, Refresh now button
|
||
- DB path, DB size
|
||
- Backend version
|
||
|
||
---
|
||
|
||
## 17. New Moon Calendar View
|
||
|
||
Accessible from the Calendar page as a "New Moon" toggle.
|
||
Shows the next 12 months as a horizontal timeline.
|
||
Each lunar cycle (29.5 days) rendered as a row with:
|
||
- Dark green zone: illumination < 20% (prime narrowband window)
|
||
- Amber zone: 20–50%
|
||
- Red zone: >50% (broadband only)
|
||
- Markers: new moon date, first/last quarter, full moon date
|
||
|
||
Overlay: for each new moon window, show the 3 best emission nebula targets that
|
||
transit highest in that window (from the seasonal precomputed data).
|
||
|
||
---
|
||
|
||
## 18. What NOT to Build
|
||
|
||
- No NINA integration or sequence export
|
||
- No planetary or solar targets
|
||
- No mono / LRGB workflows
|
||
- No user authentication
|
||
- No cloud sync
|
||
- No mobile layout (desktop-first, min-width 1280px)
|
||
- No live telescope control
|
||
- No PixInsight scripting automation
|
||
|
||
---
|
||
|
||
## 19. Code Quality Standards
|
||
|
||
**Rust:**
|
||
- Edition 2021
|
||
- `thiserror` for error types, `anyhow` for application-level errors
|
||
- `tracing` + `tracing-subscriber` for structured logging
|
||
- `tokio` multi-thread runtime
|
||
- All DB access via `sqlx` with compile-time query checking (`query!` macro)
|
||
- No `unwrap()` in non-test code — use `?` operator throughout
|
||
- Separate `AppState` struct passed via Axum state
|
||
- Run `cargo clippy -- -D warnings` clean
|
||
|
||
**React / TypeScript:**
|
||
- Strict TypeScript (`"strict": true`)
|
||
- All API responses typed via interfaces mirroring Rust structs
|
||
- `react-query` for all data fetching — no raw `useEffect` fetches
|
||
- Components under 200 lines; extract sub-components aggressively
|
||
- No inline styles except for dynamic values (chart colors, animation delays)
|
||
- All CSS in `*.module.css` files or `tokens.css` custom properties
|
||
|
||
**Both:**
|
||
- Comments in English
|
||
- No secrets or API keys in code (all external APIs used are keyless) |