diff --git a/CAMPAIGN.md b/CAMPAIGN.md index 06717ba..a42ba84 100644 --- a/CAMPAIGN.md +++ b/CAMPAIGN.md @@ -23,30 +23,9 @@ Update after every run: date, scope, target, imaging minutes, points charged | Date | Scope | Target | Plan file | Imaging | Est. pts | Actual pts | Notes | |---|---|---|---|---|---|---|---| -| 2026-07-19 | T33 | 47 Tuc | ngc104-test.txt | 17 min | 0 (free) | DID NOT RUN | reservation 749228 expired unused: no images, no logs on T33 (checked 20 Jul); grid shows reschedule=No despite the flag being set at booking | -| 2026-07-19 | T59 | NGC 6744 | ngc6744-t59.txt | 180 min | ~240 (0% moon disc. until 21 Jul) | **381** | SUCCESS (txn 851148): ran 00:05:22-03:29:56 SSO, Time Used 3h24m32s, 0% moon disc. Full dataset on scope server: L 15x300s BIN1, RGB 7x300s each BIN2; previews checked, round stars, galaxy clean: no refund case. Effective rate ~112 pts/hr of WALL time (not the 80/hr imaging-time estimate: see NOTES rate correction) | -| 2026-07-20 | T33 | NGC 6752 | ngc6752.txt | 17 min | 0 (free) | DID NOT RUN | reservation 749229 expired unused: no images, no logs (checked 20 Jul 20:30 UTC, after window) | -| 2026-07-21 | T32 | Centaurus A | cena-t32.txt | 120 min | ~350 (x1.6 model) | **160** | SUCCESS (txn 851344): ran 18:50:24-21:16:19 SSO local (08:50-11:16 UTC), Time Used 2h25m55s, 25% moon discount. Images present: /images/qisback/T32/NGC5128 on t32.itelescope.net:8032. Came in at 0.46x the x1.6 model, i.e. LESS than the original estimate: see the rate correction below | - -Cumulative spend: 541. Balance after (verified GetAccountStatus 2026-07-21 -11:40 UTC): **2,097**. - -**Rate correction (2026-07-21, supersedes the 2026-07-20 x1.6 warning).** Two -paid runs now bracket the question, and both bill EXACTLY the plan's imaging -minutes, not wall-clock: - -| Scope | Plan imaging | Wall (Time Used) | Billed | Points per imaging hour | -|---|---|---|---|---| -| T59 | 180 min (36x300s) | 3h24m32s | 381 | **127** (0% moon) | -| T32 | 120 min (24x300s) | 2h25m55s | 160 | **80** (25% moon) | - -381 = 3.00 h x 127 and 160 = 2.00 h x 80, both to the point. So billing is on -imaging time as originally documented, and the wall-clock theory is dead. What -is wrong is the rate card in NOTES.md: it lists T59 at 80/hr (actual 127) and -T32 at 146/hr (actual 80 even after the 25% moon discount, so a base near -107/hr). The two look transposed, but two samples cannot prove that. **Do not -apply a blanket multiplier.** Derive each scope's effective rate from its first -actual bill, and treat any not-yet-observed scope's card rate as +/-60%. +| 2026-07-19 | T33 | 47 Tuc | ngc104-test.txt | 17 min | 0 (free) | | first-light test; reservation 749228, 00:05-00:50 SSO local 20 Jul, auto-runs | +| 2026-07-19 | T59 | NGC 6744 | ngc6744-t59.txt | 180 min | ~240 (0% moon disc. until 21 Jul) | | first paid run; reservation 749227, 00:05-03:45 SSO local 20 Jul, auto-runs, reschedule flag on; astrosharp holds 03:50-04:40 | +| 2026-07-20 | T33 | NGC 6752 | ngc6752.txt | 17 min | 0 (free) | | reservation 749229, 00:05-00:50 SSO local 21 Jul, auto-runs | ## Planned allocation (~2,600 points, adapt as scopes/weather allow) @@ -72,21 +51,12 @@ actual bill, and treat any not-yet-observed scope's card rate as +/-60%. ## Booked reservations (all with plan attached + reschedule flag) -| ID | Scope | Night of (SSO/obs local) | Slot | Plan | Est. pts | Status (2026-07-20) | -|---|---|---|---|---|---|---| -| 749227 | T59 | 19 Jul | 00:05-03:45 | ngc6744-t59.txt | ~240 | COMPLETED, billed 381 | -| 749228 | T33 | 19 Jul | 00:05-00:50 | ngc104-test.txt | 0 | EXPIRED unused (no run, no auto-reschedule) | -| 749229 | T33 | 20 Jul | 00:05-00:50 | ngc6752.txt | 0 | EXPIRED unused (no run, no auto-reschedule) | -| 749230 | T8 | 26 Jul | 21:35-23:55 | catspaw-t8.txt | ~146 (25% moon disc.) | still booked | -| 749302 | T32 | 21 Jul | 18:50-21:50 | cena-t32.txt | ~350 (x1.6 rate, 25% moon disc.) | COMPLETED, billed 160. EVENING slot: T32 has a 35 deg elevation floor and Cen A culminates ~17:10 before dark, sinking to 11 deg by the old 00:35 slot (which could never have run). Ran 18:50-21:16, finishing 34 min early (plan complete, not a fault); images landed under /images/qisback/T32/NGC5128 | -| (id not captured) | T8 | 22 Jul | 21:45-00:45 | cra-t8.txt | ~250 | booked + MySummary-verified 2026-07-21 11:45 UTC. This is queue #2 finally placed: see the resolution note below. Runs 11:45-14:45 UTC 22 Jul | -| (id not captured) | T32 | 22 Jul | 18:50-19:50 | hdrcore-t32.txt | ~25 | **CANCELLED 2026-07-21 ~15:00 UTC, never ran, 0 points spent.** Booked to fix a saturated Cen A nucleus; the nucleus is not saturated (see the correction below). Deleted from MySummary with the other three reservations verified intact afterwards; quota back to 6:05/8:00 | -| 749314 | T33 | 21 Jul | 00:05-00:50 | ngc104-test.txt | 0 (free) | booked + grid-verified 2026-07-21 08:55 UTC. 47 Tuc first-light retry, third attempt. Runs 14:05-14:50 UTC 21 Jul. Reschedule flag again No (free scopes never rebook) | - -The T33 first-light test is now on its THIRD booking (749314); the two prior free -reservations expired unused. If this one also fails to run, the problem is not the -booking but the auto-start on free scopes: escalate to walk-in (POST plan.asp per -plans/README.md) or drop the free-imaging objective. +| ID | Scope | Night of (SSO/obs local) | Slot | Plan | Est. pts | +|---|---|---|---|---|---| +| 749227 | T59 | 19 Jul | 00:05-03:45 | ngc6744-t59.txt | ~240 | +| 749228 | T33 | 19 Jul | 00:05-00:50 | ngc104-test.txt | 0 | +| 749229 | T33 | 20 Jul | 00:05-00:50 | ngc6752.txt | 0 | +| 749230 | T8 | 26 Jul | 21:35-23:55 | catspaw-t8.txt | ~146 (25% moon disc.) | ## Booking queue (blocked by the rolling 8-hour reservation allowance) @@ -96,102 +66,13 @@ Verify every booking via Edit.aspx?id= probing (sequential ids), NOT the grid. | # | Scope id | start | end | start sel | end sel | Plan | Est. pts | |---|---|---|---|---|---|---|---| -| 1 | BOOKED as 749302 (see table above); the original 00:35 slot was an error, Cen A is an EVENING target (35 deg floor on T32) | | | | | cena-t32.txt | ~350 | -| 2 | BOOKED 2026-07-21 for the night of 22 Jul, 21:45-00:45 (see table above); the 01:35 slot was blocked by hajo and low anyway | | | | | cra-t8.txt | ~250 | +| 1 | GRAS032 | 2026-07-21T00:35:00 | 2026-07-21T03:35:00 | 12:35 AM | 3:35 AM | cena-t32.txt | ~292 | +| 2 | GRAS008 | 2026-07-22T01:35:00 | 2026-07-22T04:35:00 | 1:35 AM | 4:35 AM | cra-t8.txt | ~250 | | 3 | GRAS059 | 2026-07-25T20:35:00 | 2026-07-25T23:55:00 | 8:35 PM | 11:55 PM | ngc6188-haoiii-t59.txt | ~120 (disc.) | | 4 | GRAS059 | 2026-07-27T20:35:00 | 2026-07-27T23:05:00 | 8:35 PM | 11:05 PM | ngc6188-sii-t59.txt | ~90 (disc.) | | 5 | GRAS008 | 2026-08-08T02:35:00 | 2026-08-08T06:05:00 | 2:35 AM | 6:05 AM | smc47tuc-t8.txt | ~390 | | 6 | GRAS032 | 2026-08-09T03:05:00 | 2026-08-09T06:05:00 | 3:05 AM | 6:05 AM | tarantula-t32.txt | ~365 | | 7 | (T70, blocked: server unreachable) | night TBD before 5 Aug | ~2.5h | | | scorpius-t70.txt (in scratch, re-upload needed) | ~238 | -| 8 | WITHDRAWN: booked then cancelled 2026-07-21, premise was wrong (see below) | | | | | hdrcore-t32.txt | 0 | - -### CORRECTION (2026-07-21): the Cen A nucleus was never saturated - -An earlier entry here claimed the 21 Jul Cen A data came back with a saturated -nucleus, and booked a short-exposure core set on that basis. **The claim was -wrong and the booking has been cancelled without running, at a cost of zero -points.** - -The 65313 ADU figure came from taking the maximum pixel inside a 300 x 300 px -box centred on the frame. That measures the brightest thing near the middle of -the image, which is not the same as measuring the galaxy: it was a foreground -star 128 px (69 arcsec) from the nucleus, sitting inside the box. Median -filtering the inner 800 x 800 px to remove stars leaves the galaxy peaking at -**1944 ADU**, about a thirtieth of the clip level, with **not one star-free -pixel above 30000 ADU**. The core looked blown in the first processed image -because of the display stretch, whose white point was set by field stars, and -re-rendering the same data with a second tone curve scaled to the galaxy -recovered it fully. - -**The habit that IS worth keeping, in corrected form:** before booking extra -time to fix a saturated core, *measure whether it is saturated* - median-filter -the inner region to remove stars first, and check where the frame's bright -pixels actually are. The check costs a minute. 47 Tuc and the SMC (queue #5) -may still genuinely saturate, a dense cluster core being the fastest thing there -is to clip, so run the same measurement on their data before assuming either -way. plans/hdrcore-t32.txt stays in the repo (and on T32) as a ready-made -ladder if one of them turns out to need it. - -### Session 2026-07-21 ~08:55 UTC (booking round run early, before Cen A ran) - -Done this session: free T33 47 Tuc retry booked as 749314 (grid-verified). NOT -done, both correctly deferred: -- **Cen A verify**: 749302 had not run yet (starts 08:50 UTC, this session ran at - 08:48). Verify after 11:50 UTC. -- **cra-t8 (#2)**: blocked twice over. (1) Quota: Cen A holds 3h until it - completes ~11:50 UTC; with Cat's Paw (2h20m) + Cen A (3h) + T33 (45m) held, - under ~1h55m is free, not enough for a 3h slot. (2) Conflict: user hajo holds - T8 00:25-02:25 SSO local (res 749207), overlapping our planned 01:35-04:35. - -**cra-t8 resolution options** (pick with a live grid re-check when quota frees): -- (a) Shift to 02:25-05:25 after hajo. Downside: Corona Australis culminates - ~22:30 SSO local, so by 02:25-05:25 it is 4-6.5h past meridian and low; poor. -- (b) PREFERRED: move cra-t8 to the NEXT night, 2026-07-23T01:35-04:35 (night of - 22 Jul SSO), same clock time, avoids hajo, keeps decent altitude. Re-check that - night's T8 grid first. One night's slip is fine inside the drain window. -- (c) Earlier slot 21:25-00:25 (before hajo) is astronomically best but starts - 11:25 UTC, before Cen A releases quota at 11:50; not bookable in time. - -The scheduled 12:13 UTC job (ae3af497) fires after Cen A completes: it should -verify Cen A + bill, then book cra-t8 by option (b) unless the grid has changed -(hajo may cancel, opening option (a)/original). At corrected (x1.6) rates the -full remaining queue runs ~2,530 vs 2,257 balance (~270 over): once actual bills -for Cen A and CrA land, trim Phase 3 (shorten Tarantula #6 or the SMC slot) -to land the balance near zero. - -### Session 2026-07-21 ~11:40 UTC (Cen A verify + cra-t8 booked) - -Ran ahead of the 12:13 UTC scheduled job, after Cen A had finished at 11:16 UTC. - -- **Cen A verified**: completed, billed 160, NGC5128 images on the scope server. - Ledger and rate model updated (the x1.6 model is withdrawn: see the rate - correction above). -- **cra-t8 booked**, but on a better slot than the deferred option (b). Probing - New.aspx for the original 01:35-04:35 returned "Conflicting reservation. Start - time must be later than 2:25 AM" (hajo, as predicted), confirming that option - (a) was all that remained on the night of 21 Jul, and it puts Corona Australis - 3.5-6.5 h past the meridian. Instead the run went to the EVENING of the next - night: **2026-07-22T21:45 - 2026-07-23T00:45 SSO local**, which brackets the - ~22:30 culmination, so the whole 115 min of imaging happens near the zenith. - This is option (c)'s geometry on option (b)'s night; the reason (c) was - unbookable earlier (quota held by Cen A) had expired by the time Cen A - completed. Grid probing showed no conflict on either side of the slot. - Verified in Reservation/MySummary.aspx; quota now 6:50 / 8:00. -- Moon on the night of 22 Jul is ~57% illuminated, the last usable night before - the bright-week narrowband phase. LRGB on a dust complex under a half moon is - a compromise, but the alternative is slipping a Phase 1 broadband target into - the bright week, which is worse. -- Reservation id NOT captured: the confirm response carries only - `ModalStatic.result("OK")` and MySummary does not print ids. Nothing depends - on the id unless the booking must be edited or deleted, in which case find it - by probing Edit.aspx?id= around the current sequence (749314 was issued - ~08:55 UTC on 21 Jul). -- Budget: 2,097 left, 19 nights to 10 Aug. Remaining queue at card rates is - roughly 1,300, which UNDER-spends by ~800. With the x1.6 model withdrawn, the - next planning session should add work rather than trim it: candidates are a - second narrowband night on T59 (observed 127/hr, the only scope with a - trustworthy rate), or extending the Phase 3 SMC and Tarantula runs, both of - which have the sky time to absorb longer plans. Substitution rules: if T71 returns to service, move #2 and #7 to T71 (faster f/2.8 optics, cheaper 106 pts/hr; re-cut the plans for 1.55"/px). If T70's diff --git a/CLAUDE.md b/CLAUDE.md index 27134d8..6a35dc1 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1,14 +1,3 @@ -> [!IMPORTANT] -> **This repository has moved.** Since 2026-07-21 it lives on as the -> `itelescope/` directory of -> **[astrophotography](https://git.discworld.casa/laurence/astrophotography)**, -> merged with the processing pipeline. Both histories were preserved. -> -> **Do new work there, not here.** This copy is kept read-only-in-spirit until -> the drain campaign closes (10-12 Aug 2026), because sessions are running most -> nights and nothing should break mid-campaign. Anything committed here after -> the merge will be stranded. - # Default Workflow This file is the entry point for any Claude Code session working under the Default diff --git a/README.md b/README.md index bb46ee9..1007f33 100644 --- a/README.md +++ b/README.md @@ -1,14 +1,3 @@ -> [!IMPORTANT] -> **This repository has moved.** Since 2026-07-21 it lives on as the -> `itelescope/` directory of -> **[astrophotography](https://git.discworld.casa/laurence/astrophotography)**, -> merged with the processing pipeline. Both histories were preserved. -> -> **Do new work there, not here.** This copy is kept read-only-in-spirit until -> the drain campaign closes (10-12 Aug 2026), because sessions are running most -> nights and nothing should break mid-campaign. Anything committed here after -> the merge will be stranded. - # itelescope Reference and review of the [iTelescope.net](https://www.itelescope.net/) remote diff --git a/api/.env.example b/api/.env.example new file mode 100644 index 0000000..fafa8ea --- /dev/null +++ b/api/.env.example @@ -0,0 +1,3 @@ +# Copy to .env and fill in. Never commit the real .env. +ITELESCOPE_USERNAME=your_itelescope_username +ITELESCOPE_PASSWORD=your_itelescope_password diff --git a/api/.gitignore b/api/.gitignore new file mode 100644 index 0000000..cff5543 --- /dev/null +++ b/api/.gitignore @@ -0,0 +1,3 @@ +.env +__pycache__/ +*.pyc diff --git a/api/README.md b/api/README.md new file mode 100644 index 0000000..841c4ed --- /dev/null +++ b/api/README.md @@ -0,0 +1,44 @@ +# iTelescope API + Console + +A small FastAPI wrapper over the iTelescope.net member portal, plus a web GUI on +top of it. Credentials come from a `.env` file - nothing is hard-coded. + +## What it exposes (all read-only) + +| Endpoint | What | +|---|---| +| `GET /api/health` | is it configured, known site codes | +| `GET /api/account` | points balance, plan, renewal, membership | +| `GET /api/plans` | per-scope access discount for the current plan | +| `GET /api/telescopes` | the telescope roster (from `../data/itelescope-telescopes.csv`) | +| `GET /api/weather` | safe/unsafe state for all six observatory sites | +| `GET /api/weather/{site}` | one site (SSO, UDRO, DSC, SRO, AC, EYE) | +| `GET /api/scope/{tid}/status` | reachability of a scope's own ACP control server | +| `GET /api/reservations` | legacy reservation grid + pointer to the live system | +| `GET /api/raw?path=...` | escape hatch: fetch any portal path (read-only) | + +The GUI dashboard is at `/`; interactive OpenAPI docs at `/docs`. + +## Run + +```bash +cd api +python -m pip install -r requirements.txt +cp .env.example .env # then edit .env with your username/password +uvicorn main:app --reload +# open http://127.0.0.1:8000/ +``` + +## Deliberate omissions + +- **Booking is not exposed.** Reservations spend real points, and the live + booking system (`lookup.itelescope.online`) is Cloudflare-protected and needs a + browser session. If booking is ever added it must sit behind an explicit, + confirmed action - see the drain campaign policy in `../CAMPAIGN.md`. +- Session cookies live only in memory for the running process. + +## Notes + +- Auth is the portal's WebForms login (viewstate POST) captured in + `itelescope_client.py`; the session re-authenticates automatically if it expires. +- `DataService.svc` methods are WCF (HTTP GET, JSON under a `d` key). diff --git a/api/itelescope_client.py b/api/itelescope_client.py new file mode 100644 index 0000000..ecce524 --- /dev/null +++ b/api/itelescope_client.py @@ -0,0 +1,170 @@ +"""Thin client for iTelescope.net's member portal (go.itelescope.net). + +Authenticates once with the WebForms login (username/password from the +environment), holds the session cookie, and exposes the portal's read features +as plain Python. Booking is deliberately NOT a convenience method here - it +spends real points and lives behind an explicit, guarded call. + +Env: ITELESCOPE_USERNAME, ITELESCOPE_PASSWORD (see .env.example). +""" +from __future__ import annotations +import os +import re +import html +import time +import threading +from typing import Any + +import requests + +BASE = "https://go.itelescope.net" +LOGIN = BASE + "/login.aspx?PreviousPage=%2fdefault.aspx" +# The six observatory site codes used by the weather pages. +SITES = ["SSO", "UDRO", "DSC", "SRO", "AC", "EYE"] + + +class ITelescopeError(RuntimeError): + pass + + +class NotConfigured(ITelescopeError): + pass + + +def _strip_html(s: str) -> str: + return html.unescape(re.sub(r"<[^>]+>", " ", s or "")).replace("", "").strip() + + +class ITelescopeClient: + """One authenticated session against the iTelescope portal. + + Thread-safe for the simple case: a lock serialises (re)login so concurrent + FastAPI requests share a single cookie jar. Re-authenticates automatically + if the session has expired (a portal call bounces to the login page). + """ + + def __init__(self, username: str | None = None, password: str | None = None, + timeout: int = 30): + self.username = username or os.environ.get("ITELESCOPE_USERNAME", "") + self.password = password or os.environ.get("ITELESCOPE_PASSWORD", "") + self.timeout = timeout + self._s = requests.Session() + self._s.headers["User-Agent"] = "itelescope-api/1.0" + self._authed = False + self._lock = threading.RLock() # reentrant: a requests.Session is not + # thread-safe, so all portal calls (and + # the login they may trigger) serialise. + + # -- auth --------------------------------------------------------------- + def _hidden(self, page_text: str, name: str) -> str: + m = re.search(r'name="' + re.escape(name) + r'"[^>]*value="([^"]*)"', page_text) + return m.group(1) if m else "" + + def login(self) -> None: + if not self.username or not self.password: + raise NotConfigured("ITELESCOPE_USERNAME / ITELESCOPE_PASSWORD not set") + with self._lock: + r = self._s.get(LOGIN, timeout=self.timeout) + form = { + "__EVENTTARGET": "", "__EVENTARGUMENT": "", "__LASTFOCUS": "", + "__VIEWSTATE": self._hidden(r.text, "__VIEWSTATE"), + "__VIEWSTATEGENERATOR": self._hidden(r.text, "__VIEWSTATEGENERATOR"), + "__EVENTVALIDATION": self._hidden(r.text, "__EVENTVALIDATION"), + "UsernameTextBox": self.username, "PasswordTextBox": self.password, + "LoginButton": "Login", + } + r2 = self._s.post(LOGIN, data=form, timeout=self.timeout, allow_redirects=True) + ok = ("login.aspx" not in r2.url.lower()) or ("logout" in r2.text.lower()) + if not ok: + raise ITelescopeError("login failed (check credentials)") + self._authed = True + + def _ensure(self) -> None: + if not self._authed: + self.login() + + def _get(self, path: str, **kw) -> requests.Response: + with self._lock: + self._ensure() + url = path if path.startswith("http") else BASE + "/" + path.lstrip("/") + r = self._s.get(url, timeout=self.timeout, **kw) + if "login.aspx" in r.url.lower(): # session expired -> re-auth once + self._authed = False + self.login() + r = self._s.get(url, timeout=self.timeout, **kw) + return r + + def _svc(self, method: str) -> Any: + """Call a DataService.svc method (WCF, HTTP GET, JSON reply under 'd').""" + r = self._get(f"DataService.svc/{method}") + if r.status_code != 200: + raise ITelescopeError(f"{method} -> HTTP {r.status_code}") + try: + return r.json().get("d", r.json()) + except ValueError: + raise ITelescopeError(f"{method} returned non-JSON") + + # -- read features ------------------------------------------------------ + def account_status(self) -> dict: + d = self._svc("GetAccountStatus") + clean = {k: _strip_html(v) if isinstance(v, str) else v + for k, v in d.items() if not k.startswith("__")} + # pull the numeric balance out of e.g. "2001 points" + bal = clean.get("Balance", "") + m = re.search(r"([\d,]+)", bal) + clean["BalancePoints"] = int(m.group(1).replace(",", "")) if m else None + return clean + + def available_plans(self) -> Any: + """Per-scope access discount for the current membership plan.""" + return self._svc("GetMyAvailablePlans") + + def weather(self, site: str) -> dict: + site = site.upper() + if site not in SITES: + raise ITelescopeError(f"unknown site {site!r}; one of {SITES}") + r = self._get(f"Weather/Weather.aspx?site={site}") + text = _strip_html(r.text) + # surface any obvious "safe/unsafe/open/closed" state words + state = None + for w in ("UNSAFE", "SAFE", "OPEN", "CLOSED", "ROOF"): + if re.search(rf"\b{w}\b", r.text, re.I): + state = w.lower(); break + return {"site": site, "state": state, "http": r.status_code, + "excerpt": text[:600]} + + def scope_status(self, tid: str) -> dict: + """Best-effort reach of a telescope's own ACP control web server.""" + n = re.sub(r"\D", "", tid) + out = {"telescope": f"T{n}", "reachable": False, "url": None, "http": None} + for url in (f"https://t{n}.itelescope.online/", f"http://t{n}.itelescope.net:80{n}/"): + try: + with self._lock: + r = self._s.get(url, auth=(self.username, self.password), timeout=15) + out.update(reachable=r.status_code < 500, url=url, http=r.status_code) + if r.ok: + break + except requests.RequestException: + continue + return out + + def reservations(self) -> dict: + """Current reservations. The live booking system moved to the + Cloudflare-protected lookup.itelescope.online, which needs a browser, so + this returns the legacy grid when present and flags the new system.""" + r = self._get("Reservation/Default.aspx") + rows = [] + for tr in re.findall(r"]*>(.*?)", r.text, re.S): + cells = [_strip_html(c) for c in re.findall(r"]*>(.*?)", tr, re.S)] + cells = [c for c in cells if c] + if cells and re.search(r"20\d\d|T\d\d|GRAS", " ".join(cells)): + rows.append(cells) + return {"legacy_rows": rows, + "note": "Live reservations are managed at lookup.itelescope.online " + "(Cloudflare-protected; a browser session is required to read/write them).", + "count": len(rows)} + + def raw(self, path: str) -> dict: + """Escape hatch: fetch any portal path (read-only) for exploring.""" + r = self._get(path) + return {"url": r.url, "http": r.status_code, "excerpt": _strip_html(r.text)[:1500]} diff --git a/api/main.py b/api/main.py new file mode 100644 index 0000000..0e43d5d --- /dev/null +++ b/api/main.py @@ -0,0 +1,108 @@ +"""FastAPI wrapper over the iTelescope member portal. + +Run: uvicorn main:app --reload (from this directory, with .env present) +GUI: http://127.0.0.1:8000/ (dashboard) +Docs: http://127.0.0.1:8000/docs (OpenAPI) + +Credentials come from the environment / a .env file - never hard-coded. Every +endpoint here is READ-ONLY. Booking spends real points and is intentionally not +exposed; add it deliberately behind its own confirmation if ever needed. +""" +from __future__ import annotations +import os +from functools import lru_cache +from pathlib import Path + +from dotenv import load_dotenv +from fastapi import FastAPI, HTTPException +from fastapi.responses import FileResponse +from fastapi.staticfiles import StaticFiles + +from itelescope_client import ITelescopeClient, ITelescopeError, NotConfigured, SITES + +load_dotenv() +HERE = Path(__file__).parent +app = FastAPI(title="iTelescope API", version="1.0", + description="Read-only wrapper over the iTelescope.net member portal.") + + +@lru_cache(maxsize=1) +def client() -> ITelescopeClient: + return ITelescopeClient() + + +def _guard(fn): + try: + return fn() + except NotConfigured as e: + raise HTTPException(503, str(e)) + except ITelescopeError as e: + raise HTTPException(502, str(e)) + + +@app.get("/api/health") +def health(): + c = client() + return {"configured": bool(c.username and c.password), "sites": SITES} + + +@app.get("/api/account") +def account(): + return _guard(lambda: client().account_status()) + + +@app.get("/api/plans") +def plans(): + return _guard(lambda: client().available_plans()) + + +@app.get("/api/telescopes") +def telescopes(): + """The telescope roster from the repo's snapshotted spec sheet.""" + csv = HERE.parent / "data" / "itelescope-telescopes.csv" + if not csv.exists(): + raise HTTPException(404, "telescope csv not found") + import csv as _csv + with open(csv, encoding="utf-8-sig", newline="") as fh: + return list(_csv.DictReader(fh)) + + +@app.get("/api/weather/{site}") +def weather(site: str): + return _guard(lambda: client().weather(site)) + + +@app.get("/api/weather") +def weather_all(): + c = client() + out = {} + for s in SITES: + try: + out[s] = c.weather(s) + except ITelescopeError as e: + out[s] = {"site": s, "error": str(e)} + return out + + +@app.get("/api/scope/{tid}/status") +def scope_status(tid: str): + return _guard(lambda: client().scope_status(tid)) + + +@app.get("/api/reservations") +def reservations(): + return _guard(lambda: client().reservations()) + + +@app.get("/api/raw") +def raw(path: str): + return _guard(lambda: client().raw(path)) + + +# --- GUI ------------------------------------------------------------------ +app.mount("/static", StaticFiles(directory=HERE / "static"), name="static") + + +@app.get("/") +def index(): + return FileResponse(HERE / "static" / "index.html") diff --git a/api/requirements.txt b/api/requirements.txt new file mode 100644 index 0000000..09a23b1 --- /dev/null +++ b/api/requirements.txt @@ -0,0 +1,4 @@ +fastapi>=0.110 +uvicorn[standard]>=0.27 +requests>=2.31 +python-dotenv>=1.0 diff --git a/api/static/index.html b/api/static/index.html new file mode 100644 index 0000000..9af54a0 --- /dev/null +++ b/api/static/index.html @@ -0,0 +1,160 @@ + + + + + +iTelescope Console + + + +
+
+ iTelescope

Console

+ +
+

Read-only view of the iTelescope.net member portal. Booking is not exposed here - it spends real points.

+ +
+ +
+

Weather - all sites

+
loading...
+
+ +
+

Reservations

+
loading...
+
+ +
+

Telescope status check

+
+ + + +
+
+ +
+

Telescope roster

+
loading...
+
+ + +
+ + + + diff --git a/plans/README.md b/plans/README.md index 71c823e..1a0688d 100644 --- a/plans/README.md +++ b/plans/README.md @@ -84,15 +84,6 @@ Reservation. Success = response script `ModalStatic.result("OK")` AND the bookin appearing in the grid: the modal also says "OK" on silent failure, so ALWAYS re-fetch the grid and look for the reservation id + username. -Availability probing (2026-07-21): a plain GET of -`New.aspx?start=...&end=...&r=...` is a cheap, side-effect-free conflict check. -The form comes back with the two time dropdowns populated for +/-60 min around -the requested times, and if the slot collides with someone else's booking it -also renders "Conflicting reservation. Start time must be later than H:MM AM". -No message plus a full pair of dropdowns means the window is clear. This beats -reading the DayPilot grid, whose event divs do not survive a plain -Invoke-WebRequest fetch (they are rendered client-side). - Rules learned: maximum reservation length 4:00; recommended duration 1.5x total imaging time; the attached plan is auto-started by iTelescope at reservation start (no walk-in POST needed); RescheduleCheckbox auto-rebooks the identical diff --git a/plans/hdrcore-t32.txt b/plans/hdrcore-t32.txt deleted file mode 100644 index 908c510..0000000 --- a/plans/hdrcore-t32.txt +++ /dev/null @@ -1,17 +0,0 @@ -; -; Centaurus A (NGC 5128) HDR core set on T32 for qisback - drain campaign -; Companion to cena-t32.txt (2026-07-21, 24x300s LRGB BIN2), whose nucleus is -; saturated: the core reaches 65313 ADU in a single 300s luminance sub. -; This is the short-exposure ladder to blend under that core. -; L 8x60s, 8x15s, 8x5s spans a 60x range below 300s; RGB 5x30s gives the -; nucleus colour. BIN2 throughout so the frames register to the deep stack -; without rescaling. 18.2 min imaging total. -; -#largepreviews -#tiff -#count 8,8,8,5,5,5 -#interval 60,15,5,30,30,30 -#binning 2,2,2,2,2,2 -#filter Luminance,Luminance,Luminance,Red,Green,Blue -NGC5128 13.424333 -43.019139 -#shutdown diff --git a/state/NOTES.md b/state/NOTES.md index 3a4323c..12a6266 100644 --- a/state/NOTES.md +++ b/state/NOTES.md @@ -52,28 +52,6 @@ T73 224, T74 295, T75 202, T80 119. Standouts: T59 at 80/hr is the value big scope (20" CDK, square 37 arcmin field, 900 s subs); T5 at 68/hr the cheapest paid; T26 at 394/hr the dearest. Billing is per minute of imaging (exposure) time, so overhead is not billed. - -**RATE CORRECTION (2026-07-20, from the first real bill):** the paragraph above -underestimates. T59 session 851148: 180 min of exposure, wall clock 00:05:22 to -03:29:56 (3h24m34s), history "Time Used" 3h24m32s = WALL time, billed 381 points -at 0% moon discount. That is ~112 pts/hr of wall time or ~127 pts/hr of exposure -time, vs the 80/hr the card predicted. So either Time Used (wall clock) is what -gets billed, or the card's data-default-rate is stale. Consequence: estimate -sessions at rate x reserved-slot-length (slots run ~1.15-1.5x imaging time), i.e. -multiply old exposure-based estimates by ~1.6 until more bills confirm the model. - -**RATE CORRECTION 2 (2026-07-21, second bill; SUPERSEDES the x1.6 rule above).** -T32 session 851344: 120 min of exposure, Time Used 2h25m55s, 25% moon discount, -billed 160. 160 = 2.00 h x 80 exactly, and the T59 bill is 381 = 3.00 h x 127 -exactly. Both land on the plan's IMAGING minutes to the point, and neither lands -on wall time. So the wall-clock theory is dead and the original "billed per -minute of exposure" documentation was right; what is wrong is the card's -per-scope numbers. Observed effective rates: **T59 127/hr** (card said 80), -**T32 80/hr even after a 25% moon discount**, implying a base near 107/hr (card -said 146). The pair looks transposed, but two samples cannot prove it. Rules: -do NOT apply any blanket multiplier; derive each scope's rate from its first -real bill; until then treat a card rate as +/-60%; and compute estimates from -the plan's exposure total, never from the reserved slot length. Paid imaging needs no reservation when a scope is idle: same walk-in ACP flow as the free scopes, points just deduct. Reservations (to guarantee a slot) go through lookup.itelescope.online, which is Cloudflare-protected: Playwright diff --git a/state/TODO.md b/state/TODO.md index 13a5890..2ba756f 100644 --- a/state/TODO.md +++ b/state/TODO.md @@ -13,68 +13,22 @@ from lat 51.5°N), matched to Q62/X07 scopes, seasonal booking calendar, and a July-August starter campaign (T33 free, T71 narrowband, T73 galaxy, T8 LMC later). -## Done (recent) - -- 2026-07-20: NGC 6744 on T59 SUCCEEDED (reservation 749227, txn 851148): full - LRGB dataset on the scope server (L 15x300s BIN1, RGB 7x300s each BIN2), - previews verified clean. Billed 381 pts (est was ~240: see the rate - correction in NOTES.md). Balance now 2,257. Ledger updated in CAMPAIGN.md. - -## Done (recent) - -- 2026-07-21 ~08:55 UTC: free T33 47 Tuc retry booked as reservation 749314 - (grid-verified), third attempt at the first-light test, runs 14:05-14:50 UTC - 21 Jul, costs 0. - -## Done (recent) - -- 2026-07-21 11:40 UTC: Cen A on T32 verified COMPLETE (billed 160, NGC5128 - images on the scope server), and queue #2 (Corona Australis on T8) booked for - the night of 22 Jul, 21:45-00:45 SSO local, which brackets the target's - culmination instead of chasing it down the western sky. Rate model rebuilt - from two actual bills. Details in the CAMPAIGN.md session note. - ## In progress -- T33 first-light test on its THIRD booking (749314), which had NOT yet run as of - 11:40 UTC 21 Jul: it starts 14:05 UTC. Verify after 14:50 UTC (images and logs - on t33.itelescope.net:8033 were still empty/stale at 11:40, as expected). The - two prior free reservations (749228, 749229) expired unused with reschedule=No; - if 749314 also fails to run, stop rebooking and either walk it in (POST - plan.asp per plans/README.md) or drop the free objective. -- The T32 HDR core set (plans/hdrcore-t32.txt) was booked and then CANCELLED on - 2026-07-21 without running: the Cen A nucleus is not saturated, the earlier - claim having measured a foreground star. See the correction in CAMPAIGN.md. - Zero points spent. The plan file remains available on T32 if a genuinely - saturated target turns up. -- The 21 Jul Cen A data has been fully processed off-repo, in the user's - Downloads\NGC5128\20260721\stacked folder: masters, plate solution (0.27 - arcsec on 237 stars), an LRGB composite, a globular cluster survey (289 - candidates, 62% catalogue-confirmed, density falling 10.5x outward), surface - photometry (Sersic n = 4.27, Re = 400 arcsec, total G = 6.56, which matches - the catalogued brightness and so validates the photometric chain), and a - transient/moving-object search (both null, with measured limits). METHODS.md - there documents the lot. Nothing from that analysis needs to come back into - this repo, but two results affect FUTURE sessions and are recorded here: - **(a)** measure saturation properly before buying short subs, and **(b)** the - faint outer halo in that stack is limited by the sky-plane subtraction - (-17.9 ADU/px of real halo light absorbed), not by exposure time, so more - integration alone would not have gone deeper. -- CrA on T8 runs 11:45-14:45 UTC 22 Jul. Verify after 14:45 UTC: images under - /images/qisback/T8/ on t8.itelescope.net:8008, then the actual bill from - account/history.aspx into the ledger. This is the first T8 bill, so it also - fixes T8's effective rate (card says 130/hr; trust nothing until observed). +- T33 first-light test: ngc104-test.txt (47 Tuc, ~24 min LRGB) uploaded to + /plans/qisback/ on T33 and verified. Run window: 2026-07-19 roughly 14:00-19:30 + UTC when 47 Tuc clears the 20 degree limit. Procedure in plans/README.md. + Remaining: live-run the plan (confirm the plan.asp POST and any scope-connect + step), monitor via console, pull images/logs, record outcome in NOTES. +- Second free session scheduled for 2026-07-20 same window: runs + plans/ngc6752.txt (uploaded to T33, verified) if the 47 Tuc test succeeded, + otherwise retries ngc104-test.txt. Whichever session runs must record its + outcome here and in NOTES so the other knows. - DRAIN CAMPAIGN ACTIVE (see CAMPAIGN.md): spend the full balance by 10 Aug, then cancel before the 12 Aug renewal (user confirmation required for the - cancel itself). 541 of 2,638 spent; 2,097 remain. The x1.6 estimate multiplier - is WITHDRAWN: billing is on plan imaging minutes, and per-scope rates must come - from actual bills (T59 127/hr, T32 80/hr observed; the NOTES rate card is - unreliable). On current numbers the campaign UNDER-spends by ~800: plan extra - or longer runs, do not trim. Reschedule flag never sticks (grid always shows - No): treat every reservation as one-shot and verify the morning after. - Download the NGC 6744 and NGC 5128 data from data.itelescope.net before the - 10 Aug cancellation sweep. Update the CAMPAIGN.md ledger after every session - from account/history.aspx. + cancel itself). First paid run: NGC 6744 on T59 (plans/ngc6744-t59.txt, + uploaded and verified), scheduled 2026-07-19 after the free test, ~240 pts. + Update the CAMPAIGN.md ledger after every session from account/history.aspx. ## Pending