Compare commits
No commits in common. "main" and "feature/api-console" have entirely different histories.
main
...
feature/ap
14 changed files with 515 additions and 258 deletions
141
CAMPAIGN.md
141
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 |
|
| 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 | 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) | **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-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) | DID NOT RUN | reservation 749229 expired unused: no images, no logs (checked 20 Jul 20:30 UTC, after window) |
|
| 2026-07-20 | T33 | NGC 6752 | ngc6752.txt | 17 min | 0 (free) | | reservation 749229, 00:05-00:50 SSO local 21 Jul, auto-runs |
|
||||||
| 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%.
|
|
||||||
|
|
||||||
## Planned allocation (~2,600 points, adapt as scopes/weather allow)
|
## 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)
|
## Booked reservations (all with plan attached + reschedule flag)
|
||||||
|
|
||||||
| ID | Scope | Night of (SSO/obs local) | Slot | Plan | Est. pts | Status (2026-07-20) |
|
| ID | Scope | Night of (SSO/obs local) | Slot | Plan | Est. pts |
|
||||||
|---|---|---|---|---|---|---|
|
|---|---|---|---|---|---|
|
||||||
| 749227 | T59 | 19 Jul | 00:05-03:45 | ngc6744-t59.txt | ~240 | COMPLETED, billed 381 |
|
| 749227 | T59 | 19 Jul | 00:05-03:45 | ngc6744-t59.txt | ~240 |
|
||||||
| 749228 | T33 | 19 Jul | 00:05-00:50 | ngc104-test.txt | 0 | EXPIRED unused (no run, no auto-reschedule) |
|
| 749228 | T33 | 19 Jul | 00:05-00:50 | ngc104-test.txt | 0 |
|
||||||
| 749229 | T33 | 20 Jul | 00:05-00:50 | ngc6752.txt | 0 | EXPIRED unused (no run, no auto-reschedule) |
|
| 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.) | still booked |
|
| 749230 | T8 | 26 Jul | 21:35-23:55 | catspaw-t8.txt | ~146 (25% moon disc.) |
|
||||||
| 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.
|
|
||||||
|
|
||||||
## Booking queue (blocked by the rolling 8-hour reservation allowance)
|
## 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 |
|
| # | 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 |
|
| 1 | GRAS032 | 2026-07-21T00:35:00 | 2026-07-21T03:35:00 | 12:35 AM | 3:35 AM | cena-t32.txt | ~292 |
|
||||||
| 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 |
|
| 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.) |
|
| 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.) |
|
| 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 |
|
| 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 |
|
| 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 |
|
| 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
|
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
|
f/2.8 optics, cheaper 106 pts/hr; re-cut the plans for 1.55"/px). If T70's
|
||||||
|
|
|
||||||
11
CLAUDE.md
11
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
|
# Default Workflow
|
||||||
|
|
||||||
This file is the entry point for any Claude Code session working under the Default
|
This file is the entry point for any Claude Code session working under the Default
|
||||||
|
|
|
||||||
11
README.md
11
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
|
# itelescope
|
||||||
|
|
||||||
Reference and review of the [iTelescope.net](https://www.itelescope.net/) remote
|
Reference and review of the [iTelescope.net](https://www.itelescope.net/) remote
|
||||||
|
|
|
||||||
3
api/.env.example
Normal file
3
api/.env.example
Normal file
|
|
@ -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
|
||||||
3
api/.gitignore
vendored
Normal file
3
api/.gitignore
vendored
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
.env
|
||||||
|
__pycache__/
|
||||||
|
*.pyc
|
||||||
44
api/README.md
Normal file
44
api/README.md
Normal file
|
|
@ -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).
|
||||||
170
api/itelescope_client.py
Normal file
170
api/itelescope_client.py
Normal file
|
|
@ -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"<tr[^>]*>(.*?)</tr>", r.text, re.S):
|
||||||
|
cells = [_strip_html(c) for c in re.findall(r"<t[dh][^>]*>(.*?)</t[dh]>", 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]}
|
||||||
108
api/main.py
Normal file
108
api/main.py
Normal file
|
|
@ -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")
|
||||||
4
api/requirements.txt
Normal file
4
api/requirements.txt
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
fastapi>=0.110
|
||||||
|
uvicorn[standard]>=0.27
|
||||||
|
requests>=2.31
|
||||||
|
python-dotenv>=1.0
|
||||||
160
api/static/index.html
Normal file
160
api/static/index.html
Normal file
|
|
@ -0,0 +1,160 @@
|
||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<title>iTelescope Console</title>
|
||||||
|
<style>
|
||||||
|
:root{
|
||||||
|
--bg:#080a0f; --panel:#0f131b; --panel2:#141a25; --line:#1e2634;
|
||||||
|
--ink:#e9ecf3; --muted:#8b95a7; --faint:#5b6577; --star:#e8b658; --cool:#6ea8d8;
|
||||||
|
--good:#4c9f70; --warn:#d8a24a; --bad:#c0473a;
|
||||||
|
--sans:system-ui,-apple-system,"Segoe UI",Roboto,sans-serif;
|
||||||
|
--mono:ui-monospace,"Cascadia Code",Consolas,monospace;
|
||||||
|
}
|
||||||
|
*{box-sizing:border-box} body{margin:0;background:radial-gradient(1100px 640px at 75% -12%,#101725,var(--bg) 60%);
|
||||||
|
color:var(--ink);font-family:var(--sans);line-height:1.5}
|
||||||
|
.wrap{max-width:1100px;margin:0 auto;padding:34px 22px 70px}
|
||||||
|
header{display:flex;align-items:baseline;gap:14px;flex-wrap:wrap;margin-bottom:6px}
|
||||||
|
h1{font-size:24px;margin:0;letter-spacing:-.01em}
|
||||||
|
.tag{font-family:var(--mono);font-size:11px;letter-spacing:.18em;text-transform:uppercase;color:var(--star)}
|
||||||
|
.sub{color:var(--muted);font-size:14px;margin:0 0 24px}
|
||||||
|
.grid{display:grid;gap:16px}
|
||||||
|
.cards{grid-template-columns:repeat(auto-fit,minmax(210px,1fr))}
|
||||||
|
.card{background:var(--panel);border:1px solid var(--line);border-radius:13px;padding:16px 18px}
|
||||||
|
.card h2{font-size:12px;letter-spacing:.14em;text-transform:uppercase;color:var(--muted);margin:0 0 12px;font-weight:600}
|
||||||
|
.stat{font-size:26px;font-weight:650;letter-spacing:-.01em;font-variant-numeric:tabular-nums}
|
||||||
|
.stat.big{font-size:34px;color:var(--star)}
|
||||||
|
.statsub{color:var(--muted);font-size:13px;margin-top:3px}
|
||||||
|
section{margin-top:28px}
|
||||||
|
section > h2{font-size:13px;letter-spacing:.12em;text-transform:uppercase;color:var(--star);margin:0 0 12px;font-family:var(--mono)}
|
||||||
|
table{width:100%;border-collapse:collapse;font-size:13.5px}
|
||||||
|
th,td{text-align:left;padding:8px 10px;border-bottom:1px solid var(--line)}
|
||||||
|
th{color:var(--muted);font-weight:600;font-size:12px;text-transform:uppercase;letter-spacing:.04em}
|
||||||
|
td{font-variant-numeric:tabular-nums}
|
||||||
|
.scroll{overflow-x:auto;border:1px solid var(--line);border-radius:12px;background:var(--panel)}
|
||||||
|
.pill{display:inline-block;font-family:var(--mono);font-size:11px;padding:3px 9px;border-radius:999px;font-weight:700;text-transform:uppercase;letter-spacing:.06em}
|
||||||
|
.pill.good{background:color-mix(in srgb,var(--good) 22%,transparent);color:var(--good)}
|
||||||
|
.pill.warn{background:color-mix(in srgb,var(--warn) 22%,transparent);color:var(--warn)}
|
||||||
|
.pill.bad{background:color-mix(in srgb,var(--bad) 22%,transparent);color:var(--bad)}
|
||||||
|
.pill.na{background:var(--panel2);color:var(--faint)}
|
||||||
|
.wx{grid-template-columns:repeat(auto-fit,minmax(150px,1fr))}
|
||||||
|
.wx .card{padding:14px 16px}
|
||||||
|
.wx .code{font-family:var(--mono);font-size:15px;font-weight:700}
|
||||||
|
.row{display:flex;gap:10px;align-items:center;flex-wrap:wrap;margin-bottom:12px}
|
||||||
|
input,button{font:inherit}
|
||||||
|
input{background:var(--panel2);border:1px solid var(--line);color:var(--ink);border-radius:8px;padding:8px 11px;width:110px}
|
||||||
|
button{background:var(--star);color:#111;border:none;border-radius:8px;padding:8px 15px;font-weight:650;cursor:pointer}
|
||||||
|
button.ghost{background:var(--panel2);color:var(--ink);border:1px solid var(--line)}
|
||||||
|
button:hover{filter:brightness(1.06)}
|
||||||
|
.muted{color:var(--muted)} .mono{font-family:var(--mono)}
|
||||||
|
.err{color:var(--bad)}
|
||||||
|
.note{background:var(--panel);border:1px solid var(--line);border-left:3px solid var(--cool);border-radius:10px;padding:12px 15px;color:var(--muted);font-size:13.5px;margin-top:10px}
|
||||||
|
.foot{margin-top:34px;color:var(--faint);font-size:12px;font-family:var(--mono)}
|
||||||
|
.spin{color:var(--faint);font-size:13px}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="wrap">
|
||||||
|
<header>
|
||||||
|
<span class="tag">iTelescope</span><h1>Console</h1>
|
||||||
|
<span id="acctname" class="muted mono" style="margin-left:auto"></span>
|
||||||
|
</header>
|
||||||
|
<p class="sub">Read-only view of the iTelescope.net member portal. Booking is not exposed here - it spends real points.</p>
|
||||||
|
|
||||||
|
<div id="cards" class="grid cards"></div>
|
||||||
|
|
||||||
|
<section>
|
||||||
|
<h2>Weather - all sites</h2>
|
||||||
|
<div id="wx" class="grid wx"><span class="spin">loading...</span></div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section>
|
||||||
|
<h2>Reservations</h2>
|
||||||
|
<div id="resv"><span class="spin">loading...</span></div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section>
|
||||||
|
<h2>Telescope status check</h2>
|
||||||
|
<div class="row">
|
||||||
|
<input id="tid" placeholder="e.g. T33" value="T33">
|
||||||
|
<button onclick="checkScope()">Check ACP server</button>
|
||||||
|
<span id="scopeout" class="mono muted"></span>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section>
|
||||||
|
<h2>Telescope roster</h2>
|
||||||
|
<div id="scopes" class="scroll"><span class="spin" style="display:block;padding:14px">loading...</span></div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<div class="foot" id="foot">iTelescope API - FastAPI - read-only</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
const $ = s => document.querySelector(s);
|
||||||
|
async function get(p){ const r = await fetch(p); if(!r.ok){ throw new Error((await r.json().catch(()=>({}))).detail || r.status);} return r.json(); }
|
||||||
|
function pill(state){
|
||||||
|
const s=(state||"").toLowerCase();
|
||||||
|
if(["safe","open"].includes(s)) return `<span class="pill good">${s}</span>`;
|
||||||
|
if(["unsafe","closed","roof"].includes(s)) return `<span class="pill bad">${s}</span>`;
|
||||||
|
return `<span class="pill na">${state||"n/a"}</span>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadAccount(){
|
||||||
|
try{
|
||||||
|
const a = await get('/api/account');
|
||||||
|
$('#acctname').textContent = a.CurrentPlan || '';
|
||||||
|
const days = (a.MembershipExpiry||'').match(/\((\d+) days/);
|
||||||
|
$('#cards').innerHTML = `
|
||||||
|
<div class="card"><h2>Points balance</h2><div class="stat big">${a.BalancePoints?.toLocaleString()??a.Balance}</div><div class="statsub">available to spend</div></div>
|
||||||
|
<div class="card"><h2>Plan</h2><div class="stat">${(a.CurrentPlan||'').replace(/\s*\(.*/,'')}</div><div class="statsub">${(a.CurrentPlan||'').match(/\((.*)\)/)?.[1]||''}</div></div>
|
||||||
|
<div class="card"><h2>Renews</h2><div class="stat">${(a.PlanRenewal||'').replace(/^\w+,\s*/,'')}</div><div class="statsub">${days?days[1]+' days left':''}</div></div>
|
||||||
|
<div class="card"><h2>Member since</h2><div class="stat">${(a.SubscribedSince||'').split(/\s+\d+\s*month/)[0].replace(/^\w+,\s*/,'')}</div><div class="statsub">veteran</div></div>`;
|
||||||
|
}catch(e){ $('#cards').innerHTML = `<div class="card err">Account: ${e.message}</div>`; }
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadWeather(){
|
||||||
|
try{
|
||||||
|
const w = await get('/api/weather');
|
||||||
|
$('#wx').innerHTML = Object.entries(w).map(([s,d])=>`
|
||||||
|
<div class="card"><h2>${s}</h2><div class="code">${pill(d.state)}</div>
|
||||||
|
<div class="statsub">${d.error?('<span class=err>'+d.error+'</span>'):('HTTP '+(d.http??'-'))}</div></div>`).join('');
|
||||||
|
}catch(e){ $('#wx').innerHTML = `<span class="err">${e.message}</span>`; }
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadResv(){
|
||||||
|
try{
|
||||||
|
const r = await get('/api/reservations');
|
||||||
|
let h = '';
|
||||||
|
if(r.count){
|
||||||
|
h = '<div class="scroll"><table><tbody>' + r.legacy_rows.map(row=>'<tr>'+row.map(c=>`<td>${c}</td>`).join('')+'</tr>').join('') + '</tbody></table></div>';
|
||||||
|
} else {
|
||||||
|
h = '<p class="muted">No reservations in the legacy grid.</p>';
|
||||||
|
}
|
||||||
|
$('#resv').innerHTML = h + `<div class="note">${r.note}</div>`;
|
||||||
|
}catch(e){ $('#resv').innerHTML = `<span class="err">${e.message}</span>`; }
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadScopes(){
|
||||||
|
try{
|
||||||
|
const t = await get('/api/telescopes');
|
||||||
|
if(!t.length){ $('#scopes').innerHTML='<p class="muted" style="padding:14px">No roster.</p>'; return; }
|
||||||
|
const cols = Object.keys(t[0]).slice(0,7);
|
||||||
|
$('#scopes').innerHTML = '<table><thead><tr>'+cols.map(c=>`<th>${c}</th>`).join('')+
|
||||||
|
'</tr></thead><tbody>'+t.map(r=>'<tr>'+cols.map(c=>`<td>${r[c]??''}</td>`).join('')+'</tr>').join('')+'</tbody></table>';
|
||||||
|
}catch(e){ $('#scopes').innerHTML = `<span class="err" style="padding:14px;display:block">${e.message}</span>`; }
|
||||||
|
}
|
||||||
|
|
||||||
|
async function checkScope(){
|
||||||
|
const tid = $('#tid').value.trim(); $('#scopeout').textContent='checking...';
|
||||||
|
try{
|
||||||
|
const d = await get('/api/scope/'+encodeURIComponent(tid)+'/status');
|
||||||
|
$('#scopeout').innerHTML = `${d.telescope}: ${d.reachable?'<span class="pill good">reachable</span>':'<span class="pill bad">unreachable</span>'} ${d.url?('<span class=muted>'+d.url+' ('+d.http+')</span>'):''}`;
|
||||||
|
}catch(e){ $('#scopeout').innerHTML = `<span class="err">${e.message}</span>`; }
|
||||||
|
}
|
||||||
|
|
||||||
|
loadAccount(); loadWeather(); loadResv(); loadScopes();
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|
@ -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
|
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.
|
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
|
Rules learned: maximum reservation length 4:00; recommended duration 1.5x total
|
||||||
imaging time; the attached plan is auto-started by iTelescope at reservation
|
imaging time; the attached plan is auto-started by iTelescope at reservation
|
||||||
start (no walk-in POST needed); RescheduleCheckbox auto-rebooks the identical
|
start (no walk-in POST needed); RescheduleCheckbox auto-rebooks the identical
|
||||||
|
|
|
||||||
|
|
@ -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
|
|
||||||
|
|
@ -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
|
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.
|
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.
|
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
|
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
|
as the free scopes, points just deduct. Reservations (to guarantee a slot) go
|
||||||
through lookup.itelescope.online, which is Cloudflare-protected: Playwright
|
through lookup.itelescope.online, which is Cloudflare-protected: Playwright
|
||||||
|
|
|
||||||
|
|
@ -13,68 +13,22 @@
|
||||||
from lat 51.5°N), matched to Q62/X07 scopes, seasonal booking calendar, and a
|
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).
|
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
|
## In progress
|
||||||
|
|
||||||
- T33 first-light test on its THIRD booking (749314), which had NOT yet run as of
|
- T33 first-light test: ngc104-test.txt (47 Tuc, ~24 min LRGB) uploaded to
|
||||||
11:40 UTC 21 Jul: it starts 14:05 UTC. Verify after 14:50 UTC (images and logs
|
/plans/qisback/ on T33 and verified. Run window: 2026-07-19 roughly 14:00-19:30
|
||||||
on t33.itelescope.net:8033 were still empty/stale at 11:40, as expected). The
|
UTC when 47 Tuc clears the 20 degree limit. Procedure in plans/README.md.
|
||||||
two prior free reservations (749228, 749229) expired unused with reschedule=No;
|
Remaining: live-run the plan (confirm the plan.asp POST and any scope-connect
|
||||||
if 749314 also fails to run, stop rebooking and either walk it in (POST
|
step), monitor via console, pull images/logs, record outcome in NOTES.
|
||||||
plan.asp per plans/README.md) or drop the free objective.
|
- Second free session scheduled for 2026-07-20 same window: runs
|
||||||
- The T32 HDR core set (plans/hdrcore-t32.txt) was booked and then CANCELLED on
|
plans/ngc6752.txt (uploaded to T33, verified) if the 47 Tuc test succeeded,
|
||||||
2026-07-21 without running: the Cen A nucleus is not saturated, the earlier
|
otherwise retries ngc104-test.txt. Whichever session runs must record its
|
||||||
claim having measured a foreground star. See the correction in CAMPAIGN.md.
|
outcome here and in NOTES so the other knows.
|
||||||
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).
|
|
||||||
- DRAIN CAMPAIGN ACTIVE (see CAMPAIGN.md): spend the full balance by 10 Aug,
|
- 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
|
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
|
cancel itself). First paid run: NGC 6744 on T59 (plans/ngc6744-t59.txt,
|
||||||
is WITHDRAWN: billing is on plan imaging minutes, and per-scope rates must come
|
uploaded and verified), scheduled 2026-07-19 after the free test, ~240 pts.
|
||||||
from actual bills (T59 127/hr, T32 80/hr observed; the NOTES rate card is
|
Update the CAMPAIGN.md ledger after every session from account/history.aspx.
|
||||||
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.
|
|
||||||
|
|
||||||
## Pending
|
## Pending
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue