# 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. 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>, set_utc: Option>, // 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, dawn: DateTime, moon: &MoonState) -> Option<(DateTime, DateTime)> { // 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) -> DateTime { // 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, rise_utc: Option>, // above custom horizon set_utc: Option>, best_start_utc: Option>, // alt > 30° best_end_utc: Option>, usable_min: u32, is_visible_tonight: bool, meridian_flip_utc: Option>, airmass_at_transit: f64, extinction_at_transit: f64, moon_sep_deg: f64, curve: Vec, // every 10 min, dusk to dawn } struct CurvePoint { utc: DateTime, 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 { // Returns ordered recommendations with reason strings } struct FilterRecommendation { filter_id: String, suitability: Suitability, // Ideal, Good, Marginal, Unsuitable reason: String, warning: Option, } ``` 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 { 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 ``` ### 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)