The Telescope Net is the natural language interface for telescope control — so you can point it at real research, not a driver, an app, or a config file. Say "connect my telescope" or "what's the plan tonight" and the network handles the rest.
That research program is the point. Seestar owners donate their telescope's nights. The pipeline schedules observations of scientifically valuable targets, processes the data into calibrated photometry, and submits it to AAVSO under the contributor's name. Members do real astronomy — no manual intervention required.
The Telescope Net is an astronomy charity that gives anyone access to real telescope time through natural language. Automated software, with AI support, schedules variable-star observations, processes photometry, and submits to AAVSO on each member's behalf.
The network can be simulated offline as an evolving instrument: synthetic fleets (5–1,000 nodes), correlated weather, class-specific target physics, and the production CHORUS solver executed unchanged, compared against baseline schedulers on identical nights. Deterministic per seed, no DB, no live APIs.
python -m sim list # scenario library
python -m sim run --scenario beta_5_nodes # one command, full comparison
python -m pytest tests/test_sim.py # CI-safe smoke tests (~8 s)See SIMULATION.md (model + assumptions + how to run) and
docs/FLEET_DIGITAL_TWIN_REPORT.md (partner-facing results).
make preflight # read-only node/config/cloud/storage readiness check
make preflight-active # same, but actually authenticates to the cloud, writes
# to each runtime dir, and launches the solver binary
# — run this before each observing session
make beta-init # create the C1–C7 real-data capture corpus
make beta-audit # verify captured FITS checksums and campaign coverageAdd a frame with
python3 scripts/beta_capture.py add C1 frame.fits --node-id node042.
See docs/validation/FIXTURE_MANIFEST.md for campaign-specific collection
instructions.
When a node receives its cloud credentials, commissioning starts
without another member action. It persists in data/commissioning.json,
checks location, disk, plate solving, telescope/camera connectivity and FITS
ingest, and resumes after restarts or temporarily disconnected hardware.
The first ordinary science FITS adds checksum-pinned timing, WCS, image-shape,
and detector-range evidence. Progress appears in the local web UI and travels
in cloud heartbeat conditions for remote support.
Commissioning is deliberately non-invasive: it never slews or exposes a
telescope outside the scheduler and safety system. POST /api/commissioning/restart re-runs it after hardware changes.
node_v1-main/
├── main.py Dev-mode watchdog (auto-restarts src/dashboard.py)
├── config.yaml Node configuration (edit before first run)
│
├── src/ Node Agent source (moved for clean separation)
│ ├── dashboard.py Flask dashboard + control loop
│ ├── main_service.py Production entry point (used by installers, no subprocess)
│ ├── sleep_prevention.py Cross-platform OS sleep prevention
│ ├── cloud_communicator.py Node → Cloud API client (heartbeat, plan, measurements)
│ ├── photometry.py Aperture photometry pipeline (ASTAP → comp stars → mag)
│ ├── stacking.py RANSAC live sub-pixel stacking
│ ├── image_watcher.py FSEvents/inotify watcher on the Seestar SMB share
│ ├── aavso_submission.py AAVSO Extended File Format + WebObs API (node-side)
│ ├── fits_export.py Enhanced FITS header writer
│ ├── geolocation.py IP-based location detection for auto-registration
│ ├── shared_models.py NodeInfo, TargetInfo, PlanItem, ObservationPlan, Measurement
│ └── __init__.py Package marker
│
├── alpaca/ ALPACA protocol abstraction layer
│ ├── telescope.py Slew, track, park, RA/Dec query
│ ├── camera.py Expose, status, FITS export
│ ├── focuser.py Move, halt, position
│ ├── autofocus.py V-curve sweep → parabolic minimum
│ ├── filterwheel.py Filter position management
│ ├── platesolve.py ASTAP → WCS → closed-loop centering
│ ├── safety_manager.py Altitude limits, auto-park, dawn detection
│ └── device_manager.py Connect / disconnect all devices
│
├── cloud/ Cloud server (runs on a VPS, not on the telescope host)
│ ├── main.py Entry point — Flask API + background loops
│ ├── server.py All API routes (nodes, members, public, admin)
│ ├── db.py SQLite schema + migration system
│ ├── registry.py Node registration, auth, heartbeat, performance refresh
│ ├── auth.py Member registration, login, bearer token auth
│ ├── alerts.py Alert ingestion (ALeRCE, Gaia, TNS, ATLAS, ASAS-SN, AAVSO)
│ ├── scoring.py Composite (target × node) scoring engine
│ ├── scheduler.py Nightly plan generation
│ ├── data_pipeline.py Measurement ingestion, cross-validation, AAVSO batch
│ ├── nights.py Night summary generation, notification dispatch
│ ├── conditions.py Weather, moon, airmass utilities
│ ├── tuning.py Claude-powered weight tuning monitor (observability scoring)
│ └── config.yaml Cloud configuration (fill in AAVSO credentials here)
│
├── cloud_data/ Data files (database, batches, exports)
│ ├── cloud.db SQLite database
│ └── aavso_batches/ Generated AAVSO submission batches
│
├── website/ Marketing site
│ ├── tour.html Interactive founding network experience (main site)
│ └── README.md Website documentation and API integration guide
│
├── app/ Flutter desktop member app — the only member app (macOS, Windows, Linux, Raspberry Pi OS)
│ ├── lib/ Screens (dashboard, observations, help/AI chat, nodes, notifications, more), API client, state, Aladin widgets
│ ├── pubspec.yaml
│ └── README.md App build and accessibility notes
│
├── scripts/
│ ├── manage.py Admin CLI (status, ingest, batch, submit, check-aavso, generate-code)
│ ├── seed_demo.py Demo data generator for testing
│ └── validate_photometry.py Photometry validation engine (offline; `make validate`)
│
├── docs/validation/ Scientific credibility layer
│ ├── VALIDATION_REPORT.md Partner-facing photometry validation report (AAVSO)
│ ├── RISK_MAP.md Ranked photometric-error risk analysis
│ ├── FIXTURE_MANIFEST.md Real-data corpus spec for beta nodes
│ └── evidence/ Latest validation run (report, results.json, figures)
│
├── build/ Installer build system
│ ├── node_agent.spec PyInstaller spec
│ ├── build.py Cross-platform build orchestration
│ ├── config.template.yaml Config template written by installers
│ ├── windows/install.nsi NSIS Windows installer
│ ├── macos/ macOS .pkg + launchd plist
│ └── linux/ systemd unit + install.sh
│
└── archive/ Deactivated code — never built, shipped, or tested
└── ios/ Retired iOS member apps (see archive/README.md)
| Phase | Status | Goal |
|---|---|---|
| 0 — Proof of Concept | ✅ Code complete | First AAVSO-accepted automated observation |
| 1 — Core System | 🚀 In progress | Installers shipped, member accounts live, founding network site live, Flutter app in development, 3–5 beta nodes |
| 2 — Launch | 🔜 Next | 50 nodes, signed desktop app distribution, first ATel, first grant application |
| 3 — Growth | Not started | 200 nodes, 25 countries, 10,000+ AAVSO submissions |
The network now has a safe, staged path to three new operational capabilities:
- a self-calibrating photometric mesh that learns response-specific zero-point, color, extinction, and drift corrections from overlapping stable stars, validates them in shadow, and preserves immutable raw magnitudes;
- global event tiling for GCN transient notices, including event revisions, retractions, probability-map tiling, depth-aware assignment, and event coverage accounting; and
- bounded disconnected-night autonomy, where a qualified node can execute only a signed, expiring UTC plan with durable restart recovery and idempotent reconciliation.
All three ship behind conservative defaults: calibration application is off, autonomy is disabled pending signed-key canaries, and the dedicated GCN worker does not live-dispatch to telescopes. The occultation array is explicitly a documentation-only future program. See docs/NETWORK_SCIENCE_EXPANSION.md for the architecture, safeguards, deployment sequence, and operational boundaries.
Two structural additions on top of CHORUS and Open Aperture — see LIVE_FLEET.md for full design and status.
- Live fleet state — the network keeps a live, second-scale picture of the
fleet (
cloud/live.py+ a dedicatedrealtime/SSE gateway service) instead of learning what happened the next morning. Mid-night reflow (cloud/chorus/reflow.py) re-values a clouded-out node's remaining targets on other dark nodes by calling CHORUS's own marginal-value function — the optimizer itself is untouched. Reflex confirmation (cloud/reflex.py) tasks other dark nodes to confirm a just-promoted discovery candidate within seconds instead of waiting for the next replan. - Universal frame ingestion — any FITS frame, from any camera, live or
archival, can become survey science. The cloud now plate-solves contributed
frames that arrive without a WCS (
src/plate_solve.py,cloud/solver.py), runs a staged triage → solve → extract → ingest pipeline (cloud/ingest_worker.py), and a lightweight companion watcher mode (src/companion.py) turns any astrophotographer's output folder into a feed. Archive frames route through a retrospective path (retro_discoveries) that enriches baselines without ever firing a live alert. - Reflow ships disabled (
scheduler.reflow: false); reflex confirmation is enabled by default. Both are fully tested — seetests/test_live_fleet_cloud.py,tests/test_reflow_reflex.py,tests/test_ingest_worker.py,tests/test_triage.py,tests/test_historical.py.
- App redesign wired to real backend data
- Observation history tab (
observations_tab.dart): recent photometric measurements + per-night summaries - Help tab (
help_tab.dart): AI assistant chat (OpenRouter + Claude Haiku 4.5, 5 messages/week), node diagnostics, and safeconfig.yamlpatch queue for node agents - Science program suggestions (
suggest_program_screen.dart): members can propose new targets/programs - More menu, Me tab (moved to sidebar), target detail screens
- Real data only mode; members can name their telescopes
- Push notifications (Firebase) + night summary alerts
- Aladin Lite sky visualization integrated into the operational dashboard
- Observation history tab (
- Dashboard iterations: institutional/no-scroll layouts, workbench-style IDE UI, mission control previews (see
app/archive/) - Deployment: Railway one-command API deploys (
scripts/deploy-api.sh) and GitHub Actions desktop builds - iOS apps deactivated (2026-07-19): the desktop app is the only member application. Both the standalone SwiftUI app and the Flutter iOS build target moved to
archive/ios/— not built, shipped, or tested. Seearchive/README.md.
- Interactive founding network experience — single-page scrollytelling site
- Beautiful, responsive design with glassmorphism + dark theme
- Animated sky visualization using Aladin Lite (live star field background)
- Real-time stats pulled from the cloud API (
/api/v1/network/status,/api/v1/lightcurves/SS_Cyg) - Seven narrative sections: why it matters, the network idea, how to join, real example (SS Cygni), science impact, accessibility, founding seats
- Email signup + funding donation flow (JavaScript form handling)
- Fully responsive — mobile, tablet, desktop
- No build step, no dependencies — runs as static HTML in any browser or on the cloud server
-
Node Agent source files moved to
src/— cleaner separation of concernssrc/dashboard.py,src/cloud_communicator.py,src/photometry.py, etc.- Main entry point
main.pyandconfig.yamlremain at project root - Installer and launch configurations updated
-
Website structure (
website/)website/tour.html— Main public site (replaces index.html)website/future/— Phase 2+ member portal scaffoldingwebsite/README.md— Site setup and API integration docs
-
Database and demo data organized into
cloud_data/cloud_data/cloud.db— SQLite databasecloud_data/aavso_batches/— Generated submission filesscripts/seed_demo.py— Demo data generator for testing (23 nodes across 14 countries)
cloud/tuning.py— Nightly advisory monitor that reads observation outcomes and proposes adjustments to observability scoring weights- Hot path unchanged — alert ingestion, scoring, and planning remain 100% procedural
- Opt-in — controlled by
tuning.enabledincloud/config.yaml - Audited — every weight change is logged with reasoning and can be rolled back
- Requires
ANTHROPIC_API_KEYenvironment variable when enabled - See Weight Tuning Monitor section for full documentation
- New
tuning_statetable — stores live observability weights - New
weight_historytable — audit trail of all scoring weight changes - Fixed missing
filterscolumn innodestable - All migrations applied automatically on cloud server startup
- ATLAS — fixed token-based authentication endpoint
- All alert sources now use modern auth patterns
requirements.txt— addedanthropic>=0.92(optional, lazy-loaded)- Admin CLI (
scripts/manage.py) — added tuning monitoring support - API server (
cloud/server.py) — new tuning endpoints (GET /api/v1/admin/tuning,POST /api/v1/admin/tuning/rollback)
This is the most important architectural document in the repo. The
nodestable is the single source of truth the AI scheduler reads when deciding which telescope points where. Every column exists for a reason.
| Column | Type | Used for |
|---|---|---|
latitude, longitude |
REAL | Night window, altitude curve, airmass, moon angle |
elevation |
REAL | Pressure correction for airmass calculation |
utc_offset_hours |
REAL | Twilight times in local clock; auto-updated from heartbeat |
city, country |
TEXT | Display, geographic diversity tracking |
light_pollution_mpsas |
REAL | Sky background noise; faint-target magnitude limit adjustment |
bortle |
INT | Human-readable sky quality label; auto-fetched on registration |
horizon_mask |
JSON [[alt,az],…] |
Local obstructions — the scheduler won't assign targets behind trees or buildings |
| Column | Type | Used for |
|---|---|---|
tier |
INT 1–3 | 1=Seestar broadband, 2=Filtered BVRI, 3=Spectroscopy; controls which target classes get assigned |
telescope_model |
TEXT | Display; future per-model aperture lookup |
aperture_mm |
REAL | Faint magnitude limit, integration time scaling |
focal_length_mm, fov_deg, pixel_scale_arcsec |
REAL | Field-of-view matching for targets with extended structure |
mount_type |
TEXT alt_az|equatorial |
Equatorial mounts can take longer sub-frames; alt-az has field rotation |
max_exposure_s |
REAL | Sub-frame cap, primarily field-rotation limited for alt-az mounts |
mag_bright_limit, mag_faint_limit |
REAL | Direct inputs to the brightness_match() scorer |
min_altitude_deg |
REAL | Hard floor — targets below this altitude are never scheduled for this node |
| Column | Type | Used for |
|---|---|---|
camera_model |
TEXT | Display; future per-camera calibration frames |
cooled_camera |
BOOL | TEC cooling lowers read noise → better faint limit; captured but not yet scored |
filter_set |
JSON ["CV","B","V","R","I"] |
Multi-band targets require a matching filter; broadband-only Seestars get ["CV"] |
filters |
TEXT | Legacy comma-separated; kept for compatibility with older node agents |
These four flags determine whether a node can run an entire night without human
intervention. The scheduler does not assign a numeric penalty for missing them today,
but nodes with all four tend to produce better clear_nights_30d and higher
aavso_accepted counts — which flows directly into their reliability_score over time.
| Column | Why it matters for the scheduler |
|---|---|
has_dew_heater |
Prevents lens fogging in humid conditions. Without one, a node fails silently — images blur, plate-solving fails, the whole night is wasted with no measurement data. |
has_power_mgmt |
Smart power box lets the node agent remotely cycle a hung Seestar. Without it, a crashed scope means a missed assignment with no recovery until a human intervenes. |
has_enclosure |
Dome or minidome — the node can observe through light rain, wind, and heavy dew. Dramatically improves clear_nights_30d which feeds the reliability score. |
has_ups |
Brief power cuts don't kill the night. Especially relevant for nodes in regions with unstable grid power — protects both clear_nights_30d and aavso_acceptance_rate. |
| Column | Example |
|---|---|
scheduling_notes |
"south horizon blocked past az 195", "WiFi drops after midnight", "struggles above airmass 2.5" — free text surfaced to the scheduler and admin |
preferred_targets |
["SN","nova"] — scheduler gives this node a soft preference for these target types when scores are close |
These columns are never set by the node agent. The nightly maintenance loop calls
registry.refresh_all_performance() which recomputes them from the measurements
table. They represent ground truth — what a node has actually delivered, not what it
claims it can deliver.
| Column | Formula | What it tells the scheduler |
|---|---|---|
total_observations |
COUNT(*) in measurements |
Activity level; nodes with < 10 observations stay at neutral 0.50 reliability |
aavso_accepted |
SUM(aavso_submitted = 1) |
How much science has been delivered — the ultimate output metric |
aavso_rejected |
Outlier count with good quality flag | How often this node produces data the network can't corroborate |
mean_uncertainty |
AVG(uncertainty) for non-poor measurements |
Typical photometric precision; AAVSO quality ceiling is 0.30 mag |
mean_fwhm |
AVG(fwhm) for non-poor measurements |
Typical image quality / seeing; proxy for focus and atmospheric stability |
clear_nights_30d |
COUNT(DISTINCT date) in last 30 days |
How often the node actually observes; captures weather + hardware reliability combined |
outlier_rate |
outliers / total |
Cross-validation disagreement rate; high values signal that data can't be trusted |
reliability_score is a composite 0..1 value stored per-node and applied as a
multiplier on every scheduler score for that node:
total_score = theoretical_score × (0.5 + 0.5 × reliability_score)
| reliability | multiplier | meaning |
|---|---|---|
| 1.0 | ×1.00 | proven node — no penalty |
| 0.5 | ×0.75 | new or data-sparse node — slight preference for proven peers |
| 0.0 | ×0.50 | persistently poor data — still gets some assignments (floor prevents starvation) |
Formula (computed in cloud/registry.py → refresh_node_performance()):
reliability = 0.40 × aavso_acceptance_rate
+ 0.25 × (1 − outlier_rate)
+ 0.20 × min(1, clear_nights_30d / 30)
+ 0.15 × precision_factor
precision_factor = max(0, 1 − mean_uncertainty / 0.30)
New nodes (< 10 observations) start at 0.50 and converge toward their true value over roughly 20–30 observations. The formula is intentionally multi-dimensional: a node can't inflate its score by gaming one metric — it has to deliver across all four simultaneously.
There are no activation codes. They were retired in July 2026. Members link a telescope from the desktop app — nothing to request, nothing to copy into a config file.
POST /api/v1/me/activation-codeandPOST /api/v1/admin/activation-codesstill exist only to return a 410 pointing at the current flow, andPOST /api/v1/nodes/registerrejects any request that carries anactivation_code.
Linking is what creates the nodes row — every field group described above gets
populated from the attach payload.
Member creates an account and signs in to the desktop app
↓
Member installs the Node Agent and taps "Connect telescope"
↓
App → POST /api/v1/me/nodes/attach (member bearer token)
{latitude, longitude, location_name, telescope_model,
telescope_display_name, telescope_specs, portable}
↓
Cloud creates the nodes row, generates node_id + api_key,
and links it to the member (node_members)
· an identical telescope of this member's that has never
heartbeated is reused instead of minting a duplicate
· passing an existing {node_id, api_key} claims an
already-registered agent instead of creating a new one
↓
App installs the credentials on the local agent:
POST http://localhost:5173/api/cloud/credentials
↓
Agent saves node_id + api_key to data/cloud_state.json
and every later call authenticates with those two headers
If the app can't reach the agent (different machine, firewall, headless box),
the agent falls back to a pairing token. It prints a short token, shows it on its
setup page at http://localhost:5173, and polls
GET /api/v1/nodes/pair/<token>; the app pushes the credentials to that token
with POST /api/v1/nodes/pair. Tokens are short-lived, and failed claims are
rate-limited per IP because the token space is small.
The API now serves the live event stream at https://api.thetelescope.net/api/v1/stream.
For a single telescope, deploy the API with EMBED_REALTIME=1 (the default),
point the node's cloud.realtime_url at the API URL, and then stop or remove
the separate Railway realtime service. Update any already-installed node
configurations before removing that service; old installations may still point
at https://realtime.thetelescope.net. The node's regular plan polling remains
available if a stream disconnects. Set EMBED_REALTIME=0 if using a separate
realtime service for a larger fleet.
The default active heartbeat is now 30 seconds instead of 5 seconds, reducing
active heartbeat requests and database writes by about 83% per node. Existing
node configurations need the same cloud.heartbeat_fast_interval: 30 change.
This reduces live status update frequency to every 30 seconds; plan and
interrupt signals still arrive over the event stream.
Check Railway's per-service usage before changing the other services. The GCN consumer and solver worker are separate always-on processes; stop either only if its corresponding alert or frame-solving feature is not needed. The API and database are still required.
# 1. Clone and set up
git clone https://github.com/telescopenet/node_v1 && cd node_v1-main
python3 -m venv venv && source venv/bin/activate
pip install flask pyyaml numpy astropy pillow requests watchdog photutils astroquery pyongc
# 2. Install ASTAP (required for plate-solving and photometry)
# https://www.hnsky.org/astap.htm — install the binary, then a star
# database: D20 (~435 MB, recommended default) is a good general-purpose
# choice; D05 (~140 MB) works for very wide fields, D50 (~940 MB) or D80
# (~1.3 GB) for narrow-field/long-focal-length setups. See "Plate Solving"
# in the Node Agent's Settings panel for the tradeoffs.
# 3. Configure (for a dry run, only these are required)
nano config.yaml
# observatory.latitude / longitude
# image_watcher.watch_path (Seestar SMB share mount point)
# cloud.url (credentials arrive from the app — there is no code to enter)
# 4. Run (dev mode — auto-restarts src/dashboard.py on file changes)
python3 main.py
# Dashboard: http://localhost:5173Set these in config.yaml to test the dashboard without a Seestar:
cloud:
enabled: false
image_watcher:
enabled: false
photometry:
enabled: false
aavso:
dry_run: trueThen python3 main.py. Everything in the dashboard works except hardware
control. Object catalog, config editor, logs, and API endpoints are all live.
cd cloud/
pip install flask pyyaml requests
# Optional: enable Claude weight tuning (requires ANTHROPIC_API_KEY env var)
export ANTHROPIC_API_KEY="sk-..."
pip install anthropic
# Edit cloud/config.yaml:
# aavso.observer_code: MXXX
# aavso.username / aavso.password
# server.admin_key
# tuning.enabled: true (optional, defaults to false)
python3 -m cloud.main
# API: http://localhost:8800
# Health check: http://localhost:8800/api/v1/healthThe tour.html is a standalone, zero-dependency HTML+CSS+JS site that pitches The Telescope Net as the natural language interface for telescope control, backed by a real research program, and drives member acquisition. It pulls live data from the cloud API but renders gracefully even when the API is offline.
# Terminal 1: Cloud API server (provides data)
cd cloud/
PYTHONPATH="$PWD:$PWD/src" python3 -m cloud.main
# → serves on http://localhost:8800
# Terminal 2: Website (static file server)
cd website/
python3 -m http.server 4180 --directory .
# → open http://localhost:4180/tour.html
# or just http://localhost:4180 (if index.html points to tour.html)| Section | Data source | Fallback |
|---|---|---|
| Founding seats (7 / 100) | /api/v1/network/status → founded_member_count |
Static "07 / 100" |
| Online nodes | /api/v1/network/status → nodes_online |
"7 online" |
| AAVSO submissions | /api/v1/network/status → aavso_submitted |
"—" |
| Network reliability | /api/v1/network/status → highest reliability_score |
Sensible defaults |
| Live example (SS Cygni) | /api/v1/lightcurves/SS_Cyg → recent observations |
Empty chart |
| Network map dots | /api/v1/network/status → node locations |
No pins shown |
| Sky background | Aladin Lite + RA/Dec per section | Star field rendered |
tour.html
├─ Embedded CSS (glassmorphism, dark theme, responsive)
├─ Aladin Lite integration (CDN, sky visualization per scroll section)
├─ API calls (fetch from cloud server, graceful fallback if offline)
├─ Canvas visualization (Earth globe rotation, observer animations)
└─ Email signup form + donation flow (JavaScript state, no backend)
Browser compatibility: Modern browsers (Chrome 90+, Firefox 88+, Safari 14+). Fallback text renders on older clients.
The site looks better with realistic stats. Populate the database once:
python3 scripts/seed_demo.py --wipe
# Creates 23 nodes across 14 countries + light curve for SS Cyg
# Nodes are "online" for 15 minutes after seeding — re-run to refreshThen refresh your browser and watch the stats, map, and light curve populate.
The Flutter desktop app is the place members control a telescope — the only member application, on macOS, Windows, and Linux/Raspberry Pi OS. It provides telescope status, observation history, night summaries, an AI help assistant, science program suggestions, and push notifications. Designed with strong accessibility principles (large text, haptics, audio descriptions, screen reader support).
It runs on the same computer as the node agent and talks to it directly on
http://127.0.0.1:5173 to install credentials and connect the telescope, which
is why linking is a desktop operation. The node's own setup page at that address
shows the pairing code and link state.
iOS is deactivated — see
archive/ios/. There is no mobile member app.
| Tab/Screen | Purpose |
|---|---|
| Dashboard | Live node status, tonight's plan, network activity, Aladin sky view |
| Nodes | List registered telescopes, claim nodes, name/rename |
| Observations | Photometric history + night-by-night summaries; drill into targets |
| Notifications | Alerts, night summaries, system messages (mark read) |
| Help | Contact links + AI chat assistant (Claude Haiku via OpenRouter, weekly limit) + diagnostics + config patch requests |
| More | Science program suggestions, settings, about |
The app uses the member-authenticated cloud endpoints (/api/v1/me/*, /api/v1/plan, /api/v1/observations, /api/v1/help/*, etc.). It supports push via Firebase.
# 1. Ensure cloud is running (see Quick Start — Cloud Server)
cd cloud/
PYTHONPATH="$PWD:$PWD/src" python3 -m cloud.main
# 2. In another terminal, run the Flutter app
cd app
# (first time: flutter create . ; flutter pub get)
flutter run --dart-define=BS_API_BASE=http://localhost:8800
# For a real device on same LAN: use your host IP instead of localhostDesktop builds: flutter build macos, flutter build windows, or flutter build linux.
See app/README.md for full setup, accessibility notes, and API contract details.
- Real-time data from live cloud (no mock data in current main flows)
- AI-powered help that can suggest and queue safe configuration patches for your node agent
- Per-member stats, observation lists, target light curves
- Notifications for completed nights and important events
- Science program suggestion flow (sends to cloud for review)
# 1. Set AAVSO credentials in cloud/config.yaml
# aavso.observer_code: MXXX
# aavso.username: ...
# aavso.password: ...
# 2. Verify the Extended File Format looks correct (no live POST)
python3 scripts/manage.py check-aavso
# Prints a formatted test record with your observer code filled in
# 3. Check that the network has active nodes
python3 scripts/manage.py status
# 4. Trigger alert ingestion (or wait for the hourly background loop)
python3 scripts/manage.py ingest
# 5. Run a night's observations with a connected node
# 6. Preview the AAVSO batch before committing (dry run)
python3 scripts/manage.py batch
# Prints the exact Extended File Format that would be POSTed to WebObs
# 7. Submit live (calls AAVSO WebObs API)
python3 scripts/manage.py submit
# 8. Verify
python3 scripts/manage.py statusSuccess criterion (Phase 0): One observation accepted by AAVSO with magnitude agreeing within 0.15 mag of the known value for the target.
The scoring engine is built on six observability sub-weights that balance how heavily each factor influences target assignment:
{
"light_pollution": 0.20, Weight of sky brightness on assignment
"weather": 0.25, Clearness likelihood (historical)
"moon": 0.15, Lunar illumination + position
"airmass": 0.15, Atmospheric extinction + target altitude
"window": 0.15, Hours the target is visible each night
"telescope": 0.10, Hardware capability match to target
}
By default these are static. When tuning.enabled: true in cloud/config.yaml, a nightly monitor wakes after maintenance and:
-
Gathers evidence from the last N nights of observations
- Measures ingested, quality flags, AAVSO acceptance rate
- Correlates outcomes with the factors above
- Identifies which weights may be misaligned
-
Proposes adjustments (Claude API call)
- Reads the evidence brief + current weights
- Returns proposed values for each sub-weight
- Reasoning is logged but not applied automatically
-
Applies with safeguards (procedural)
- Clamps each weight to ±5% change per night (trust-region limit)
- Renormalizes to sum 1.0
- Persists to the database with full audit trail
- Notifies admins via the API if changes exceed 2% on any weight
The hot path (alert ingestion, scoring, planning) remains 100% procedural — no Claude calls, no latency added. Scoring reads live weights from tuning_state table on every run via scoring.score_all(), so a change takes effect on the next rescore with no restart.
Key points:
- Only the advisory monitor calls Claude; schedulers and scorers use fixed formulas
- Tuning is opt-in and ships disabled; behavior is identical to static weights when disabled
- All weight changes are audited in
weight_historytable — rollback is a single API call - Configuration lives in both
config.yaml(seed values) and the database (live values)
tuning:
enabled: false # set true to enable Claude-powered tuning
model: claude-opus-4-8 # Claude model for proposals (impacts cost/latency)
lookback_days: 14 # analyze the last N nights for evidence
max_delta_per_night: 0.05 # trust-region clamp (±5% per weight per night)
min_nights_for_tuning: 7 # don't tune until ≥ 7 nights of data exist
min_measurements: 30 # require ≥ 30 measurements to tune
min_weight_change: 0.005 # skip notify if no weight moves > 0.5%
# Seed values — used as defaults if tuning is disabled or on fresh install
scoring:
observability_weights:
light_pollution: 0.20
weather: 0.25
moon: 0.15
airmass: 0.15
window: 0.15
telescope: 0.10# Check current live weights
curl -H "X-Admin-Key: your-admin-key" \
https://api.thetelescope.net/api/v1/admin/tuning
# Response:
# {
# "active_weights": { … },
# "last_tuning_run": "2026-06-15T02:30:00Z",
# "weight_history": [ … ],
# "recent_changes": [ … ]
# }
# Rollback to a prior weight set (manual safety valve)
curl -X POST -H "X-Admin-Key: your-admin-key" \
-H "Content-Type: application/json" \
-d '{"rollback_to_id": 5}' \
https://api.thetelescope.net/api/v1/admin/tuning/rollbackpython3 scripts/manage.py [--config PATH] <command>
status Node count, measurement queue, AAVSO batch history, config check
ingest Trigger alert ingestion + scoring + plan regeneration
batch Dry-run preview of the pending AAVSO batch (does not POST)
submit Live AAVSO submission (ignores config dry_run setting)
check-aavso Verify credentials config, print a formatted test observation
nights Generate/backfill night summaries for all active nodes
pip install pyinstaller
# Auto-detect platform and build installer
python3 build/build.py
# Bundle only (PyInstaller one-file, no OS installer wrapper)
python3 build/build.py --bundle-only
# Outputs:
# Windows → dist/TelescopeNetNode-Setup.exe (requires NSIS + NSSM on PATH)
# macOS → dist/TelescopeNetNode-X.Y.Z.pkg
# Linux → dist/TelescopeNetNode-linux-x86_64Windows (NSIS + NSSM)
- Writes
config.yamlfrom the template; no code to enter — the member links the telescope from the desktop app after install - Registers the agent as a Windows service via NSSM; auto-starts at boot
- Calls
powercfg /change standby-timeout-ac 0to disable AC idle sleep
macOS (.pkg)
- Installs to
/Applications/TelescopeNetNode/ - Installs a launchd plist →
/Library/LaunchDaemons/(auto-start at boot) - Calls
pmset -c sleep 0to disable AC idle sleep - Data:
/Library/Application Support/TelescopeNet/NodeAgent/
Linux (systemd)
# One-line install
curl -sSL https://telescopenet.org/install.sh | sudo bash --code BS-2026-XXXXXXXX
# Or manually
sudo bash build/linux/install.sh --code BS-2026-XXXXXXXX- Creates
telescopenetservice user; installs systemd unit; enables at boot - Masks
sleep.target,suspend.target,hibernate.target,hybrid-sleep.target
| Method | Path | Description |
|---|---|---|
| POST | /api/v1/nodes/register |
First-boot self-registration (no member link). Rejects any activation_code with 410 |
| POST | /api/v1/nodes/heartbeat |
60-second keepalive with optional conditions JSON |
| GET | /api/v1/nodes/me |
Own registry entry (all columns including performance metrics) |
| GET | /api/v1/plan |
Tonight's observation plan |
| POST | /api/v1/measurements |
Upload a photometry result |
| POST | /api/v1/images |
Upload raw FITS (retained 14 days) |
| GET | /api/v1/interrupts |
High-priority target alerts |
| Method | Path | Description |
|---|---|---|
| POST | /api/v1/auth/register |
Create member account |
| POST | /api/v1/auth/login |
Authenticate; returns bearer token |
| GET | /api/v1/me |
Member profile |
| GET | /api/v1/me/nodes |
My registered nodes |
| POST | /api/v1/me/nodes/attach |
Link a telescope to this account; returns node_id + api_key |
| POST | /api/v1/me/nodes/<id> |
Claim an existing node by api_key |
| POST | /api/v1/nodes/pair |
Push credentials to a pairing token the local agent is polling |
| GET | /api/v1/me/observations |
Observation history |
| GET | /api/v1/me/stats |
Cumulative totals (observations, AAVSO accepted, targets covered) |
| GET | /api/v1/me/nights |
Night-by-night summaries |
| GET | /api/v1/me/notifications |
Notification inbox |
| POST | /api/v1/me/notifications/<id>/read |
Mark a notification read |
| PUT | /api/v1/me/notifications/prefs |
Update notification preferences |
| Method | Path | Description |
|---|---|---|
| GET | /api/v1/targets |
Active target queue with current composite scores |
| GET | /api/v1/lightcurves/<name> |
Historical photometry for a named target |
| GET | /api/v1/network/status |
Live node count, submission totals |
| GET | /api/v1/health |
Uptime check |
| Method | Path | Description |
|---|---|---|
| POST | /api/v1/admin/ingest |
Trigger alert ingestion + scoring |
| POST | /api/v1/admin/replan |
Trigger rescoring + plan regeneration |
| POST | /api/v1/interrupts |
Broadcast a high-priority target interrupt to nodes |
| GET | /api/v1/admin/tuning |
Get current active weights, history, and last run timestamp |
| POST | /api/v1/admin/tuning/rollback |
Rollback to a previous weight set by history ID |
| Table | Purpose | Columns |
|---|---|---|
tuning_state |
Active observability sub-weights | id, obs_weights (JSON), updated_at |
weight_history |
Audit trail of all weight changes | id, timestamp, old_weights, new_weights, reason, claude_reasoning |
| Table | Change | Notes |
|---|---|---|
nodes |
Added filters column |
Previously missing, needed for multi-band target assignment |
Migration: All schema updates are applied automatically on cloud server startup via db.py migration system.
Runs automatically on each new FITS file when photometry.enabled: true.
FITS file
→ 1. Ensure WCS (check header; run ASTAP plate solve if absent)
→ 2. Locate target (world_to_pixel; reject if too close to edge)
→ 3. Estimate FWHM (DAOStarFinder + second-moment Gaussian stamps)
→ 4. Comparison stars (AAVSO VSP API → Gaia DR3 fallback; merge, deduplicate)
→ 5. Aperture photometry (CircularAperture + sigma-clipped annulus background)
→ 6. Differential photometry (weighted zero-point ensemble; Poisson + ZP scatter)
→ 7. Ancillary data (BJD_TCB via astropy, airmass from Alt/Az or header)
→ 8. Quality flag (good / acceptable / poor based on SNR, uncertainty, comp stars)
→ result dict
Output:
{
"target_name": "SS Cyg",
"bjd": 2460500.123456,
"magnitude": 12.341,
"uncertainty": 0.031,
"filter": "CV",
"airmass": 1.24,
"fwhm": 3.8,
"snr": 52.0,
"comparison_stars": 9,
"quality_flag": "good",
"node_id": "node_001",
"zero_point": 22.413,
"zp_scatter": 0.028,
"fits_file": "seestar_image.fits",
}src/dashboard.py (Flask, port 5173)
│
├─ API endpoints (telescope, camera, schedule, photometry, config, logs, …)
│
├─ SafetyManager (daemon thread)
│ heartbeat monitor, reconnect, dawn parking, horizon mask, SIGTERM handler
│
├─ ImageWatcher (daemon thread, when enabled)
│ OS filesystem events → debounce → photometry pipeline
│
├─ Photometry pipeline (photometry.py)
│ WCS → comp stars → aperture phot → differential → AAVSO submission
│
├─ CloudCommunicator (daemon threads, when cloud.enabled)
│ auto-register → heartbeats → plan polling → measurement upload (retry queue)
│
└─ DeviceManager → AlpacaClient (HTTP/JSON)
Telescope, Camera, Focuser, FilterWheel
cloud/ (Flask, port 8800)
│
├─ Background loops
│ alert-ingest → scoring → (optionally) replan every 60 min
│ replan every 120 min
│ aavso-batch every 360 min
│ weight-tuning (Claude advisory monitor) daily
│ maintenance (image pruning, night summaries, daily
│ performance refresh, LP refresh)
│
├─ nodes table — the AI scheduler's view of each telescope
│ location → observability windows, airmass, moon
│ hardware → what the scope can see and how long it can expose
│ autonomy → likelihood of completing a night unattended
│ performance → what it has actually delivered to AAVSO
│ reliability_score → multiplier on every (target, node) score pair
│
└─ tuning_state + weight_history tables (when tuning enabled)
active observability sub-weights, audit trail of all changes
cloud:
enabled: true
url: https://api.thetelescope.net
# node_id / api_key are written by the app when you link the telescope.
# There is no activation code — do not add one.
auto_run_plans: true # automatically execute observation plans from the cloudNote: See Weight Tuning Monitor section above for detailed documentation.
tuning:
enabled: false # set true to enable Claude-powered observability weight tuning
model: claude-opus-4-8 # Claude model for advisory proposals
lookback_days: 14 # analyze the last N nights of observations
max_delta_per_night: 0.05 # clamp changes to ±5% per weight per night
min_nights_for_tuning: 7 # require ≥ 7 nights of historical data
min_measurements: 30 # require ≥ 30 measurements to enable tuning
min_weight_change: 0.005 # skip notifications if max change < 0.5%Environment variable: ANTHROPIC_API_KEY must be set for tuning to function.
photometry:
enabled: false # set true to auto-run on each new FITS file
node_id: "node_001"
filter_name: "CV"
gain: 1.0 # e-/ADU
read_noise: 5.0
target:
name: "" # leave blank to use FITS header values
ra_deg: ~
dec_deg: ~
astap_path: "astap"
astap_search_radius: 10
aperture_factor: 2.5
annulus_inner: 4.0
annulus_outer: 6.0
field_radius: 0.5
mag_limit: 15.0
min_comparison_stars: 3
snr_threshold: 20
max_uncertainty: 0.3
max_airmass: 3.0
fits_export:
enabled: true
export_dir: "fits_export"aavso:
observer_code: "" # AAVSO OBSCODE ← required
username: "" # AAVSO login ← required to POST
password: ""
audit_dir: "aavso_submissions"
dry_run: false
submit_poor_quality: false
chart_id: ""observatory:
name: ""
latitude: ~ # decimal degrees N
longitude: ~ # decimal degrees E (negative = West)
elevation: 0.0 # metres
telescope: "ZWO Seestar S50"
instrument: "ZWO Seestar S50 IMX462"
observer: ""cloud:
enabled: true
url: https://api.thetelescope.net
# node_id / api_key are written by the app when you link the telescope.
# There is no activation code — do not add one.
auto_run_plans: truesafety:
enabled: true
disconnect_timeout: 600
heartbeat_interval: 30
reconnect_attempts: 3
reconnect_delay: 10
park_at_dawn: true
dawn_type: astronomical # astronomical (-18°), nautical (-12°), civil (-6°)
observer:
latitude: 0.0
longitude: 0.0image_watcher:
enabled: false
watch_path: "" # blank = auto-mount the Seestar's SMB share under the node's data dir
debounce_delay: 2.0- Verify the Seestar is in Station mode (joined to your home WiFi), not its own AP/hotspot. The native ALPACA server only comes up in Station mode; the Seestar app shows "connected" in either mode, so that alone doesn't tell you which one you're in.
- Verify the Seestar's firmware is up to date — native ALPACA support requires a recent firmware version.
- Verify the Seestar is powered on and on the same subnet as the node machine
- Bump
alpaca.discovery_timeoutinconfig.yamlto 8–10s if discovery is intermittent — the native firmware responder can be slower to answer than third-party ALPACA servers - Some routers block UDP broadcast; check with
tcpdump -i en0 udp port 32227 - macOS may require allowing Python in System Preferences → Privacy & Security
The ALPACA server responded but the device isn't initialised inside the Seestar app yet. Open the Seestar app first.
- ASTAP not in PATH — set
photometry.astap_pathto the full binary path - Star database not installed — download D05/D20/D50/D80 from the ASTAP site (see the "Plate Solving" section of Settings for which to pick)
- Increase
astap_search_radiusif the image is very wide field
Check aavso_submissions/YYYY-MM-DD/*_response.txt for the raw WebObs error message. Common causes: invalid observer code, target name not in AAVSO VSX, date out of range. Use dry_run: true to validate format before a live submission.
- Low SNR: increase exposure time or check sky conditions
- Too few comparison stars: Gaia DR3 fallback should help for targets outside AAVSO VSX coverage
- Large ZP scatter: poor seeing or clouds; scatter contributes directly to uncertainty
The ALPACA HTTP server has wedged while the core firmware remains operational. The Seestar app talks to the firmware directly; ALPACA is a separate HTTP service that can fail independently. Recovery: Settings → Restart in the Seestar app (a full firmware reboot, not just app relaunch).
The SafetyManager lost contact for longer than disconnect_timeout and triggered emergency park. The unsafe state does not self-clear. Recovery: restart dashboard.py. The manager initialises safe on startup and re-attaches cleanly.
Root causes: Seestar WiFi drop, Seestar app crash, host machine sleep. Prevention: increase disconnect_timeout, fix WiFi signal quality, ensure sleep_prevention is active.
The Seestar S50 ALPACA driver does not implement Unpark. Unpark from within the Seestar app. The dashboard Unpark button has no effect on this device.
Non-fatal — no cover calibrator device is configured at ALPACA index 0. Suppress with devices.covercalibrator.enabled: false in config.yaml.
observer.latitudeandobserver.longitudemust be non-zero- At mid-latitudes in summer the sun may not reach −18° — try
dawn_type: nautical
- Check that
ANTHROPIC_API_KEYis set in the environment:echo $ANTHROPIC_API_KEY - Verify
tuning.enabled: trueincloud/config.yaml - Tuning is a daily task — runs during the maintenance window, typically 2–3 AM UTC
- Check cloud server logs:
cloud.tuninglogger will show evidence gathered and Claude responses - Insufficient data: tuning requires at least
min_measurements(default 30) andmin_nights_for_tuning(default 7) of observations
- Plans are built during the
replanloop (every 120 min); a weight change takes effect on the next replan, not retroactively - To see weights take effect immediately, trigger a manual rescore:
curl -X POST -H "X-Admin-Key: ..." http://localhost:8800/api/v1/admin/replan - Check the
GET /api/v1/admin/tuningresponse to confirm weights were actually updated
- 429 (rate limited): reduce frequency by increasing
tuning.lookback_daysor temporarily disabling - 401 (auth failed): verify
ANTHROPIC_API_KEYis correct and has quota remaining - Content policy violations: evidence brief triggers a refusal (rare); tuning skips apply and notifies in logs
| Layer | Technology | Notes |
|---|---|---|
| Node Agent | Python 3.10+ / Flask | Runs Windows, macOS, Linux; pip only |
| Telescope control | ALPACA REST — Seestar's native server (Station mode) or seestar_alp | Vendor-neutral; node agent is a pure ALPACA client either way |
| Plate solving | ASTAP + D05/D20/D50/D80 catalogue | Fast, offline, accurate on Seestar images |
| Image stacking | NumPy + RANSAC | Sub-pixel alignment, no external binaries |
| Photometry | astropy + custom aperture code | Differential against AAVSO/Gaia comp stars |
| Cloud server | Python / Flask | Upgrade to FastAPI + async at Phase 2 scale |
| Scoring weights | Claude API (opt-in) | Advisory monitor for observability tuning; hot path stays procedural |
| Database | SQLite (WAL mode) | Zero-config; migrate to Postgres at ~50 nodes |
| Member auth | PBKDF2-SHA256 + bearer tokens | 260K rounds, per-user salt, hashed token storage |
| Sleep prevention | OS-native APIs | SetThreadExecutionState (Win), caffeinate (Mac), systemd-inhibit (Linux) |
| Packaging | PyInstaller one-file | NSIS (Win), pkgbuild/productbuild (Mac), systemd install.sh (Linux) |
| Marketing site | HTML/CSS/JavaScript (static) | tour.html — interactive scrollytelling, Aladin Lite sky vis, live API data |
| Member app | Flutter / Dart (macOS, Windows, Linux, Raspberry Pi OS) | Real backend integration, AI Help assistant, TTS, and strong accessibility focus |
| AI member support | OpenRouter (Claude Haiku) | Rate-limited chat in-app; queues safe config patches for node agents |
- ALPACA Specification
- AAVSO Extended File Format
- AAVSO WebObs
- ASTAP plate solver
- photutils documentation
- Astroquery (Gaia / AAVSO VSP)
The Telescope Net — The night sky belongs to everyone. Founded 2025.