Recent changes

What landed in each shipped version, most recent first.

v22.2.10 - the first real dome in the south

with the northern-hemisphere convention; south of the equator the declination axis direction in the model reverses, so d_ota pushed the slit the wrong way and looked "reversed". Three sync points on a 3 m dome at -36 deg missed by +17 / -24 / -36 deg; with the flip they miss by the same -4 deg (the dome's azimuth zero). "Pier-side sign" is back to meaning only "my mount reports SideOfPier the other way round".

radius, offset, GEM offset, pier sign or latitude - or before this version - is set aside as dome_sync.json.stale-<time>.json (never deleted) and the page says so; capture new ones.

reported azimuth (it used to be painted at north whatever the dome did), with the ASDM target, the telescope's pointing and the remembered home.

home-sensor search and returns at once (a driver may block for the whole rotation); Starship remembers the azimuth the dome reports At home at. Go home rotates there without the search ([observatory] dome_home_azimuth_deg overrides). Both on the Dome page and as the dome_find_home / dome_home sequence actions; POST /api/console/dome/gohome.

altitude had the slit heading for 81 deg), and the Slave button takes one click at a time.

v22.2.9 - what a customer's log taught

NotImplemented to Find Home (a ZWO AM5 behind an Alpaca bridge did, eight times in ten minutes) now gets "this mount driver does not implement Find Home - home it from its own controller or app, or use Park; turn off 'Find home after a mount power reset' for it", instead of the raw HTTP 400 body.

Safety on and the Solo host left pointing at nothing, the heartbeat check escalated to safe-state every few minutes ("no safety tick") and tried to park a mount that was not there. While the Solo poll keeps erroring and no reading has ever arrived, the check now reports DEGRADED with the advice (the verdict is already UNKNOWN, so nothing runs); a poll thread that is silent altogether still escalates.

percentage.** 8 % of a 32 GB PC is 2.6 GB; a customer's rig went to safe-state twice on it. The percentage rule now only counts below [health] sys_mem_critical_pct_max_mb (2048 MB); the 512 MB floor is unchanged.

streak, then one every ten minutes - instead of 2,700 rows a day (90 % of that customer's log). The safety supervisor still sees every one.

v22.2.8 - Auto Flat meets a real dimmable panel; sky flats meet a real dusk

authority.** CDS Strasbourg moved to a certificate from the European academic CA (HARICA); Windows only fetches a root certificate when a browser first meets it, and Starship's own TLS never asks for it, so both Strasbourg mirrors failed with "certificate verify failed" - and the message showed only the third mirror's "timed out". Starship now ships its own root bundle, uses a mirror over plain http when its certificate still cannot be verified (a public catalogue lookup, labelled as such), reports every mirror's reason in plain words, bounds the whole attempt to about 20 s, and resolves NGC / IC / Messier numbers and common names from the built-in OpenNGC catalogue when no server answers at all.

taken the instant the calibrator was switched on: it read the camera's bias, and the exposure search ran away from that. The runner now waits for the light (three seconds, and for as long as the device says NotReady).

exposure by target / 65535 as if it were one, and could win "closest to target". It now steps the exposure down by five and never wins.

allowed exposure still clips, a dimmable panel is dimmed (and brightened when the longest exposure is still too dark); each step shows in the log.

metering never left saturation (or never saw light) is reported - "not saved: saturated even at the shortest exposure and lowest brightness" - instead of banking fifteen useless frames labelled FLAT.

delivers its light in pulses, and an exposure only a few pulses long is quantised in level and banded by the rolling shutter. If the lowest brightness still needs milliseconds, put a sheet of paper or ND film over the panel. The Auto Flat topic says so.

dusk run lost its twilight to a roof that takes over three minutes: the runner waited for it, then unparked and slewed. It now issues the roof command, unparks, slews to the flat spot and stops tracking during the roof's travel, and looks at the sky the moment the roof reports open. A site whose roof can hit an unparked mount ([observatory] roof_motion_requires_safe_mount) keeps the old order.

verify_shutter_timeout_s is only the patience for a roof that says nothing. The run used to end on "roof did not report open within 60 s" with the roof half way.

UNSAFE again while the roof is opening (the hardware shuts it again), the run goes back to waiting for SAFE and asks again while the window lasts.

at which the camera first saw the sky, when every filter is already out of reach at the first look - not "no flats saved (4 of 4 filter(s) failed)".

sky_cool_camera, on by default; towards [cooling] default_setpoint_c or the set-point the camera still holds). The run never waits for it; a set about to be shot warm carries a warning and the result records ccd_temp_c`. The first real set was shot at +25 C with the cooler off: hot pixels do not stack out.

changing during the frame. Nothing changes at 2 s; a 41 s frame at dusk came out 7.5 % under target before, and each frame lower than the last.

subfolders** whatever [fits] watch_recursive says: nine new flats in flats_l/ were invisible to "what was written in the last ten minutes?".

v22.2.7 - sky flats, and what two more nights on the rigs taught

Sky flats (dusk) buttons, the API has POST /api/console/flat/sky, and a plan can run the sky_flats action in on_start (dusk) or on_end (dawn). The runner waits for the sun window ([flat] sky_sun_alt_min_deg .. sky_sun_alt_max_deg, default -7 .. -1.5 deg), sets the rig up three minutes before it opens - roof claim, flat cover, unpark, a slew to the flat spot one hour from the meridian away from the sun at the site's latitude, tracking off and read back - and shoots late and short (0.5 .. 5 s): with the mount stopped a star's trail is as bright per pixel in any twilight, so what a brighter sky buys is a short trail. Each filter is probed at two exposures (the rate is the slope, the camera's bias the intercept), the filter about to leave its exposure window is shot first (the most sensitive at dawn, the least at dusk), the brightening rate is learnt from the run's own frames, and every saved frame is metered from the file. Off-target frames are moved to skyflat_rejects (never deleted). The run stops the moment the safety verdict goes UNSAFE or a frame comes out dark - the roof closing - instead of saving dark flats. See the Auto Flat topic.

when the capture job was accepted, so a plan of 12 frames fired 12 captures in five seconds at one camera and three files landed. Each frame is now on disk before the next is taken. POST /api/console/capture accepts frame_type and subfolder (the console had them; the route dropped them).

before the focus sweep and restarts it after (a sweep through an OAG or a guider on the imaging OTA defocuses the guide star and PHD2 chased a donut). Start-guiding deselects the old star and picks a fresh one in the central part of the guide frame (fresh_star, star_central_frac in the plan's guiding policy) instead of reusing a star that is not there after a slew or a flip. The pre-sub guiding hold now waits for PHD2 to finish settling; no dither is attempted on a lost star or a guider that is not guiding; the settle tolerance is [phd2].settle_px (default 1.5 px) or the plan's settle_px; and the driver's RMS no longer includes guide steps taken during a dither.

sent of-date coordinates as J2000, precessing them a second time (about 17 arcminutes at 2026). Fixed.

the same target no longer resets the refocus counter or counts as a new target in the run outcome; a checkpoint-resumed run refocuses before its first sub.

A plan whose End phase had Close roof before Park mount never parked: the console refuses to close the roof on an unparked mount, the refusal cancelled the rest of the phase, and the park - the one action that would have satisfied the interlock - was skipped ("MAKE-SAFE INCOMPLETE ... close_roof failed ...; skipped: park"), leaving the mount tracking into the morning. The on_end, on_error and on_suspend phases now order that pair by [observatory] park_before_roof_close (park first by default), as the blockscript engine always has, and say so in the journal.

halts a focuser that is moving; on a GRBL focuser a halt is a feed hold that silently ignored every later move until a reconnect.

end_sun_alt_deg were only checked between steps, so a 110-sub expose step that started a second before the end time ran its full length. The step now ends after the current sub and the run ends as planned.

the setpoint with the cooler idle counts as settled (a TEC cannot heat); the wait notes it instead of failing the run after the settle timeout.

time, time_local, sun_altitude_deg), and sun_altitude_deg now runs: wait until the Sun is at or below the given altitude.

prefix.

request (about 1 GB and minutes of CPU on a 26 Mpx sub) is skipped while a run is active; grade on demand.

v22.2.6 - the folder layout setting survives a Settings save

API's field list did not carry them: the Settings page always showed "Everything in the images directory" whatever the file said, and saving any setting could revert a night_target file to flat. Both keys are in the settings document now, and changing the layout reports that a restart is needed (the listing mode and the service-folder wipe are read at start).

[camera] imagearray_fast_path and [health] sys_mem_critical_polls (v22.2.3) were missing from the settings document, and [mount] precess_targets (v22.2.0) was missing from the document AND from the file writer, so any save silently switched precession back on. A test now pins every scalar config field to the settings document and to the written file.

v22.2.5 - a frame is labelled with the filter's name, and filed by night and target

a fresh install; an existing config keeps flat until you change it in Settings) files science subs in <night>/<target>/ - the night rolls over at local noon so a session across midnight stays together - calibration frames in <night>/calibration/, and pointing / recenter / autofocus / focus-star frames in one _service/ folder that Starship empties when the service starts. Nothing already on disk is moved.

A wheel whose driver reports generic slot labels (a QHYCFW says "Filter1" to "Filter29") had every sub of an LRGB night named and tagged Filter4, although the plan asked for b and the library resolved it to that slot correctly: the capture stamped the driver's name. The label is now the library name for the slot that was used, then the driver's name, then the text you typed.

(and the driver's own slot names, and slot numbers) all find the right library entry, in captures and in sequencer filter steps alike, so renaming your library to Luminance, Red, Green, Blue, Ha, OIII, SII does not break a single saved plan.

v22.2.4 - the help describes this build

are compiled out of the public build, but their help topics (collimation, Navigator, Users & Access, Research) were still listed, served and shipped, and shared topics carried Professional passages. Topics are now keyed to the capability that defines them and passages are fenced, both dropped when the capability is absent; the public installer ships a copy of the help with them already removed, and the build check refuses a build that leaks one.

generated from the route table at release time, with request bodies, response notes for the routes that matter, and the playbook an automation agent should follow (connect, validate/save/start a plan, poll state and health, make safe).

what has to be on the PC (ASCOM Platform, PHD2, ASTAP) instead of a source checkout; the Roadmap reflects v22.2.4; the Configuration file and Health monitor topics document imagearray_fast_path and sys_mem_critical_polls.

v22.2.3 - the ImageArray no longer costs a gigabyte

Two unattended nights on an 8 GB rig, and a customer's report, had the same shape: the first bin-1 download of a 26 Mpx frame pushed system memory under the health monitor's floor, and the safe-state parked the mount mid-run.

pywin32 converts the camera's SAFEARRAY into nested tuples of Python ints inside the property call itself (~1.1 GB for 26 Mpx, for the seconds numpy needs to read them back). Starship now calls IDispatch::Invoke on the very interface pointer pywin32 holds and copies the SAFEARRAY into numpy in one step: about 200 MB peak and 0.1 s at 26 Mpx instead of ~1.1 GB and 3 s. Same thread, same call, same values. The classic conversion stays as the fallback (logged once) and [camera] imagearray_fast_path = false turns the new path off for a driver that misbehaves. Needs comtypes (bundled).

floor now has to hold for [health] sys_mem_critical_polls consecutive polls (default 3, i.e. 15 s) before it counts as imminent OOM, and a dip during a frame download never counts at all - the download is the dip.

make the rig safe, it stops the run first (journal safety.stop, reason on the run) instead of parking underneath a sequencer that kept exposing.

v22.2.2 - the meridian flip finishes the job

The first fully unattended night on v22.2.1 (Antwerp, 2026-09-10) put a real meridian flip through GS Server: the SideOfPier write flipped the mount and the pier side verified. Two things around the flip were wrong, both fixed and pinned by tests that fail on v22.2.1.

The capture worker returns its result under job_result; the recenter looked one level up, so every real flip with recenter_after_flip = true ended in "recenter capture produced no file path" -> on_error -> park, with the recenter frame sitting on disk and the flip already verified.

multi-sub expose stops at the meridian so the flip (or halt) can happen between subs. The run loop then moved on to the NEXT step, and the handler only runs at the top of an expose step, so when the truncated expose was the plan's last one the remaining subs were dropped, nothing flipped, and the run reported SUCCEEDED with the mount still tracking on the pre-flip side. The loop now runs the meridian handler right there and resumes the same step for the subs it still owes (journal: meridian.resume); a halt or a failed flip ends the run exactly as before.

ignored the pier side, so after a successful flip it stayed true for the rest of the target and every later multi-sub expose ended after one sub.

v22.2.1 - the park that did not happen, told straight

A customer's EQ6 on GS Server "did not park" at the end of the night. His diagnostics bundle could not say why, and neither could Starship: the journal read park: timeout - timed out after 120s waiting for mount parked (at_park=False), the rest of make-safe was cancelled, and the run still ended with "Sequence complete". Two things were wrong on Starship's side, and one is worth knowing about GS Server.

from the driver once a second instead of trusting the five-second snapshot, remembers whether a park slew was ever seen, and a timeout names one of three very different nights: the driver accepted Park but never started a park slew, the park slew ran but ended without AtPark (aborted or interrupted), or the park slew is still in progress. The budget comes from [observatory] verify_park_timeout_s (it was a 120 s literal that ignored the setting).

treats Park as a no-op then. The journal, and the dashboard Park button, say so before the wait passes in one poll, so "I pressed Park and nothing happened" gets an explanation instead of a green tick.

action) that fails still cancels the rest of the phase, because a closing roof over an unparked mount can strike the telescope. Until now that was one journal line: the run ended SUCCEEDED, the alert said "Sequence complete", and the roof stayed open. It now sends a critical notification naming the failed action and every action it skipped, the run outcome carries MAKE-SAFE INCOMPLETE, and the completion alert says so in its subject. (Alerts must be enabled to reach your phone.)

live check and a mount_park verify line in the console log with the reason when the mount did not get there.

slew cancels the slew job, which sends AbortSlew from its own thread on its next poll. That could land after on_end had already issued Park, and per ITelescope an AbortSlew aborts a park in progress: the mount stopped part-way and "did not park". Stop, weather suspend and the console's abort now wait (a few seconds at most) for a cancelled mount job to actually stop before anything else commands the mount.

whose AtPark could not be read this poll is "cannot confirm", not "not unparked"; the close is refused unless you confirm it.

About GS Server. Its source shows why a park can silently do nothing: Park is ignored when GS Server already believes the mount is parked - and it remembers that across restarts, assuming the axes are exactly at the stored park position - and when no park position is selected; a park slew that stalls is still recorded as parked. If your mount does not park: look at the GS Server window before you send Park (if it already shows Parked, unpark and park again), make sure a park position is selected in the dropdown on its main window (when GS Server starts unparked it picks the first position in the list, not the one you used last), and keep your park position above the horizon limit. Starship's new park messages tell you which of these you are looking at.

v22.2.0 - nothing answers "fine" when it does not know

This release carries the v22.1.0 polar-alignment rebuild (finished and verified on 2026-07-31, never published on its own) plus everything found since. Two more device audits and a sequencer audit went line by line through what the equipment layer, the capture path and the sequencer do when a driver misbehaves, and confirmed twenty defects. They share one shape: something reported success, or "done", or "healthy", on evidence it did not have. Every one of them is now pinned by a test that fails on the v22.1.0 tree.

Equipment: a hung driver no longer takes the night with it

used to share one worker thread. On 2026-08-17 a flat-cover driver blocked inside Connect for over twenty seconds, and the park and close-roof requests queued behind it - an accessory could veto making the observatory safe. There are now three lanes: the mount, the roof or dome, and everything else. Each has its own queue, liveness beacon and watchdog, so a wedged accessory leaves the mount and the roof responsive.

waited forever - which is how a blockscript's device-lost retry could hang the engine thread for the rest of the night. The queue is drained with ASCOM <lane> worker wedged: unresponsive inside <device>.<op>, and a disconnect that could not complete is reported instead of assumed.

lane's devices to "not connected", and every health check skipped not-connected devices, so /api/health said HEALTHY while the mount was frozen. A wedged lane is now a CRITICAL condition with one notification, the "reconnect stale device" rung actually reconnects on both transports within a 45-second budget, and Guardian's blast-radius guard treats a device it cannot read fresh as BUSY - it will not cut a shared relay under a mount whose driver is hung.

published as a fresh, error-free success with the properties wiped, which every safety check read as "nothing to worry about". Both transports now report it as unreadable, and the console status lists what is unreadable.

Connected was still reported ok, so a cold start could log "connected 5/5", unpark, open the roof and then fail every exposure with "no camera connected". The connect now fails with the reason. Platform 7's asynchronous connect also gets the budget the old blocking write always had (up to three minutes while the driver says Connecting), so a slow camera is slow rather than failed, and a half-connected device is disconnected rather than left dangling.

on_connect_action write is no longer issued inside a connect sequence. The camera-connected event only arms it; it runs afterwards as its own bounded job (visible in the job list as cooling_on_connect and in /api/ascom). Measured on the rig on 2026-08-15, a ToupTek camera accepted the cooler write and never returned, which left Connect all hung at 2/5. If the driver blocks, the warning names the camera and the setting, and the other devices on that lane say which write blocked them. The action is keyed on the driver's own per-connect event, so it can no longer re-fire "cooler off" mid-run when a watchdog demotion clears.

operational state in one call per poll instead of one call per member - up to ten fewer synchronous COM calls, or HTTP round trips, every tick. It is self-checking: for the first three quiet polls after connect Starship still reads every member the old way and compares; any disagreement sends that device back to per-member reads for the session with one warning naming the member. Drivers without it are read as before. Each slot in /api/ascom and /api/alpaca reports interface_version and a device_state block. Kill switch: use_device_state under [ascom] and [alpaca] (also on the Settings page).

is only read while the camera is busy; some drivers throw when it is read idle, once per poll, all night.

is J2000 - the resolver, the catalogue, focus stars, plate solves - but most ASCOM mounts expect the equator of date, about 22 arcmin away in 2026. Slews and syncs to a mount that reports a topocentric EquatorialSystem are now precessed first, so blind pointing lands and a plate-solve sync no longer writes a frame offset into the mount's model. A mount that reports J2000 gets the numbers unchanged; one that reports another frame, or cannot say, gets raw J2000 and one warning. Coordinates read back from the mount itself (the meridian flip, the return from a focus star) are never converted. Kill switch: [mount] precess_targets = false.

Capture: a failed exposure is reported when the camera says so

- the specification's only failure signal - and a camera entering its Error state were retried as "transient" until exposure time plus the download timeout, then reported as "did not become ready", which Guardian did not classify as a device fault. Three attempts at a 300 s sub was eighteen minutes of dark sky per episode. The frame now fails within seconds with the driver's own message ("camera error ...", "camera timed out ..."), and transport blips are still tolerated for a short grace.

raw ImageArray and ImageReady reads had no time bound; a hung download kept the job RUNNING, with no error, no Guardian ladder and no alert, until a six-hour ceiling. They are now bounded by [camera] download_save_timeout_s and a hang fails the job as a timeout.

or a value outside its range, silently shot the whole night at whatever gain it held, with one warning per frame in the log. The frame now fails with camera rejected gain=... so the plan's error policy runs. Binning is still clamped by the camera and recorded honestly.

reported only a number; the server's UTF-8 message (a 32-bit ASCOM Remote out of memory, "no image available") is now shown, a memory error gets the same guidance as the JSON path in English, German, Italian and Spanish, and a server that answered the ImageBytes request with JSON is not asked again: from the second frame it gets the compressed JSON request directly, instead of an uncompressed 250 MB frame on every sub.

Sequencer: waits and make-safe steps act on evidence they have

300 s wait budget could never outlast the ramp, so the obvious plan the editor offers timed out every night at on_start. The budget now follows the ramp, and a timeout cancels the orphaned ramp. Per-action timeout_s, wait_s and settle_s are accepted on every phase action; the plan normaliser used to strip them, which left open_roof and close_roof with a hard 60 s cap that failed slow-but-healthy hardware.

the stop flag and cancelled the rest of the phase - park, close roof, close cover never issued. The warm-up in on_end ran BEFORE the park, so after a meridian halt the mount kept tracking past the meridian for the whole warm-up. Order is now: your on_end actions (park first), then the policy warm-up; after a halt the mount's tracking is stopped; a second Stop no longer cancels a park already in progress; and a script run in a make-safe phase is not aborted by the stop flag.

or shutter state counted as complete (the v22.0 fix conflated the two), and the observatory state used for these decisions is now read fresh instead of from the five-second snapshot. blind_solve and blind_resync waited on a job kind that was never created and returned "done" before the mount had moved; the phase now follows the job it actually started.

after a weather hold unparked and slewed to the target BEFORE on_resume ran - under a closed roof. on_resume runs first, then the re-point.

driver re-slewed to the same pier side and the (correct) post-flip check halted the run. Before the flip re-slew Starship now asks the driver which side it would land on (DestinationSideOfPier); if that would not flip, it writes SideOfPier where the driver allows it, and otherwise holds exposures for up to 30 minutes waiting for the driver's own flip while watching the reported side. The post-flip verification is unchanged and still halts on a side that did not change.

[mount] flip_mode = "off". Runtime precedence stays - an explicit plan mode is the operator's decision - but the silence goes: inherit is the editors' default and is carried explicitly, the run-view chip resolves the mode the same way the acting path does, and a run whose plan widens the mount setting shows a banner for the whole run and journals it once.

the sequencer journal, which is capped at the last 200 events for the UI, so a long chunk silently lost its earliest exposures and an unattended campaign could never complete. The sequencer publishes a total over the full journal and the scheduler uses it.

Polar alignment

information: a hand slip in Dec during the turn reads as polar error, and nothing inside the pair can tell. After position 2 Starship asks Add a third position (recommended) or Continue with two. Turn a further ~60 degrees the same way; three positions give two independent turns whose axes must agree. The disagreement is reported as the spread and the run refuses above [polar] max_spread_arcmin (0.75'), naming the lowest position when refraction near the horizon is the likely cause. It is the only cross-check that works with no mount connection at all. positions = 2 never asks.

tracking" detector rotated the field about the hemisphere-signed pole. The sky turns about the north pole of date everywhere, so on a southern rig the readout walked about 0.5'/min hands-off and an untracked mount could not be detected. Both use the pole of date now, and the two-position fit accounts for the minutes between its two solves (up to ~1' at 3 degrees off with 90 s between them).

position 1 (it wrote progress to a job field that did not exist); the tracking-off halt crashed in its own log line and marked the job failed. Both run cleanly now.

Diagnostics

first-chance exceptions that the fault tracer cannot filter, so the file grew to megabytes of identical dumps and buried the one real access violation. It is now compacted every time the tracer is armed (the handled-exception dumps are dropped and counted in one line), capped at 1 MiB with one rotated generation, and the diagnostics bundle carries the filtered version with a count of what was omitted.

Skyhunter

225 minutes or more (geostationary, Molniya, GPS) use the SDP4 model, validated against the published test vectors. Before, they were skipped, and propagated with the near-earth model they were wrong by thousands of kilometres. A Molniya gets ordinary rise, peak and set passes; an object that never sets over your site is listed once as always up rather than as a fake day-long pass. The bundled satellite catalogue is refreshed (space stations and the visual-pass set).

Under the hood

observing-programme interface are in the tree; the dispatcher is not yet wired to them, so scheduling behaviour is unchanged.

edition).

driver that blocks inside a COM call can be judged dead by its process handle and replaced without restarting Starship. Designed, not built.

New configuration keys, all with safe defaults: [mount] precess_targets (true), [ascom] / [alpaca] use_device_state (true), [polar] positions (3) and max_spread_arcmin (0.75). No changes are required to an existing config.toml.

v22.1.0 - polar alignment, rebuilt

Robert, running v22.0.0 in Germany, reported polar alignment landing "way off the real celestial pole", and asked whether it was a north/south hemisphere problem.

It was not. The maths was right in both hemispheres and the tests prove it. The problem was the slew. The old routine drove the mount itself across three to five RA positions and fit a circle through the solved pointings, which made it hostage to everything about that motion: mounts Starship cannot drive, a mis-reported slew completion, and - most damagingly - a meridian flip landing mid-sweep, which puts the measurement points on two different arcs and fits an axis nowhere near the pole.

So the slew is gone. Starship no longer moves your mount during polar alignment at all.

The new routine solves one frame, asks you to turn the mount about 80 degrees in RA by hand, and solves again. Turning in RA rotates the whole camera frame rigidly about the polar axis, so the rotation carrying the first frame onto the second is a rotation about that axis - and that axis is the measurement. The plate solve's roll angle is what makes two positions enough where position alone would need three.

Turn too little to condition the fit and it refuses, and tells you how far you actually turned. A confidently wrong pole is worse than no answer — which is the principle behind the other two guards as well:

Starship reads any movement of the star field as your knob adjustment. With tracking off the field drifts on its own — and the displayed number does not run away, it converges tidily onto your target while the real axis walks off the pole. You would be congratulated while being steered wrong. Starship now spots the signature of an untracked field in the solved frames themselves and stops with an explanation. That works even with no mount connected, which is exactly where a Tracking flag is unavailable.

carry no redundancy, so a Dec slip is mathematically indistinguishable from polar error — about 1.3' of it fabricates 1' of error. When a mount is connected Starship compares its Dec either side of the turn. When there is no mount, it records that the turn's purity was unverified rather than implying it was checked.

Then it simply keeps solving and shows you the error, live: the pole at the centre, your axis as a dot, a target ring at your tolerance, and a short trail so you can see which way you are driving it. Altitude, azimuth and total in arcminutes underneath. You turn the knobs and watch the number fall.

It deliberately does not tell you which bolt to turn. Which knob moves which way is mount-specific and easy to get backwards; a number that falls while you turn is unambiguous, and if it rises you turn the other way.

Two consequences worth knowing:

hand controller with no ASCOM connection at all. It needs a camera, a plate solver, and your site coordinates. Nothing else.

any movement of the star field is read as your knob adjustment; if the mount is not tracking, the field drifts on its own and the number quietly goes wrong. If a mount is connected and reports tracking off, the panel now says so.

v22.0.0 - the equipment layer, told straight

Two independent compliance audits went through every line of how Starship talks to your equipment, against the ASCOM Platform 7 and Alpaca specifications. They found the same mistake in sixteen places, and it has two halves:

Starship could not tell "your driver has no such property" from "I asked and got no answer." Both arrived as the same silence, and silence was read as "not applicable, carry on."

Starship could not tell "the operation finished" from "it has not started yet." Many checks read a status that is refreshed every five seconds, so they saw the state from BEFORE the command was sent and called it done.

What that was actually doing to your nights

stalled, hit a limit, or the driver reported a fault, Starship waited its full ten minutes and then said "on target" - and the sequence went on to plate solve, sync or expose on the wrong piece of sky.

meridian flip Starship confirms the mount really changed sides. A mount that cannot report which side it is on, and a mount whose reading just timed out, looked identical - so a momentary glitch declared the flip verified. It now reasons the other way round: if the mount could report its side going in, it can report it coming out, and anything else halts the run.

could not be worked out, the whole flip-or-halt decision was skipped with no message. Starship now falls back to computing it from your site longitude, and stops the run if it genuinely cannot tell.

accepted** rather than when the hardware stopped moving. A flat cover takes ten to twenty seconds; the first frames were being exposed through it.

setpoint was reading a value that was never there, so it could only ever run out its five-minute limit - on a camera that had cooled perfectly.

If a gain was rejected or a binning was clamped, the frame was still stamped with your request. Those frames then get matched against the wrong darks and flats, and it compounds quietly across every session. The header now records what the sensor actually did, and says so when the two differ.

device has no switches" as though it were a healthy answer - which also disarmed the automatic recovery that exists for exactly that kind of glitch.

re-stamped with the current time, so nothing ever aged, and the self-healing never triggered.

a routine autofocus into a full-travel move to the inward hard stop.

The refusal was noted and the run carried on - exposing at the park position for the rest of the night, with the target's name and coordinates written into every file.

Two more, found along the way

Stopping a run did not make the rig safe. The on-end phase - park, close the roof, close the cover - is supposed to run whether a sequence finishes or is stopped. It did not: pressing Stop, reaching a dawn deadline, a meridian halt or an on-failure "go to end" all skipped it entirely. It now runs.

A plan asking for FLAT frames was taking DARKS. Plans can specify light, dark, flat or bias, but everything that was not a light was being shot with the shutter closed and labelled DARK - so plan-authored flats and bias frames were both wrong.

Also

the modern non-blocking connect to network (Alpaca) devices only; it now applies to locally installed drivers as well, so a slow camera is no longer reported as a failed one there either.

previously could not connect to one at all. Set them under [alpaca] as auth_user and auth_password.

sensors over a network bridge.

---

Upgrading: install over your existing copy. Your configuration, plans and databases are left alone.

A note on this one: these changes touch the layer that talks to your mount and camera, and there are a lot of them. They follow the published specifications and are covered by tests, but they are new. If anything behaves oddly, please send a diagnostics bundle (Help -> Diagnostics).

v21.0.7 - ASCOM Platform 7 connections, and slow devices stop being errors

Two changes to how Starship talks to equipment, both aimed at the same complaint: a device that is simply SLOW was being reported as a device that had FAILED.

Connecting now uses the modern, non-blocking method

ASCOM Platform 7 added a proper asynchronous way to connect to equipment, and the specification is explicit that the old way is deprecated for applications like Starship:

> "writing to Connected to effect connection and disconnection is now explicitly > deprecated for clients but must still be implemented by drivers to ensure > backward compatibility with earlier software." > - ASCOM Master Interfaces 1.0.24, s3.2

Starship was using the old way. The problem with it is not tidiness: it BLOCKS for as long as the device takes to connect, and a camera doing USB enumeration and cooler start-up routinely takes 10-30 seconds. Past a fixed limit Starship gave up and reported a failure, on a device that was connecting perfectly well.

Starship now asks each device which interface it implements and uses the asynchronous method where it is available, waiting properly for completion instead of guessing. Older equipment keeps the previous behaviour - drivers are required to support it, so nothing is left behind.

Slow commands are given the time they need

The ASCOM Remote documentation asks clients to use three different waiting times: one to open the connection, one for quick questions like "what is your temperature", and a longer one for genuinely slow commands.

Starship used a single five-second limit for everything. Five seconds is right for a temperature reading and far too short for connecting a camera, moving a filter wheel, or homing a dome - all of which were reported as timeouts.

Slow commands now get a longer allowance that scales with your configured timeout, while status polling stays as responsive as before.

v21.0.6 - Alpaca images decoded correctly

If you captured with an Alpaca camera on v21.0.5, check your frames. They may be unusable, and this release is the fix.

Alpaca frames could come out as flat garbage

On v21.0.5, images from some Alpaca servers decoded into nonsense - a user saw every pixel come out as the same value, with the file's size and 16-bit header all perfectly correct. Nothing reported an error.

Alpaca labels its pixel format with a number, and two of those numbers were listed the wrong way round in Starship: the code that reads them believed the label for "16-bit unsigned" meant "8-bit". It read every frame one byte at a time and produced a full-size image of rubbish.

That mistake was in Starship for a long time but never reachable, because the old code looked at a different field that never carried those two values. The v21.0.5 fix - which was itself needed, and did fix the failed downloads - is what made it reachable.

Starship now also refuses a frame whose size doesn't match the format it was told to expect, instead of decoding it anyway. That is what turned this into silent corruption rather than an error message, and it should not be possible to repeat.

v21.0.5 - reliability fixes

A fix-only release. Everything here was found either by a customer report or by testing the COMPILED build rather than the source - which is where several of these could only ever have shown up.

Starship could die while the dashboard kept answering

If you use an ASCOM camera and do not have a separate filter wheel, Starship could crash outright - not raise an error, but terminate - and it needed no action from you at all. The dashboard polls the filter wheel status; with no standalone wheel to ask, it asked the camera whether it had one built in, and that question was being put to the camera from the web server's own thread rather than the thread that owns the driver. On Windows that is not an error you can catch. It is a hard stop.

What you saw: the web page still loading and responding, while nothing behind it worked. Restarting didn't help, because connecting automatically at startup put it straight back into the same state.

Reported by a user whose crash report contained fourteen of these in one session. The same unsafe pattern was also sitting behind the Abort button and the cooler controls, where nobody had hit it yet; all three now go through the driver properly.

If you have a filter wheel, you were never affected - which is why this survived so long.

Alpaca cameras: every capture failed with "pixel data short"

A camera served over Alpaca could connect, cool, and expose, and then fail every single download with ImageBytes pixel data short: N < 2N - always exactly double. Reported against three separate rigs (an ASI2600MM over an ASIAIR Alpaca bridge, and an ASI6200MM over ASCOM Remote on Windows).

Alpaca sends two type fields: the camera's logical pixel type, and the type actually used on the wire - servers routinely narrow to 16 bits to halve the bandwidth. Starship sized its buffer from the logical type, so it demanded twice the bytes the server had sent, and rejected a perfectly good image.

This is separate from the v21.0.4 fix, which addressed the JSON fallback path. These rigs negotiate the binary transfer successfully and then failed here.

Also fixed in the same place: when a server narrows into a signed 16-bit type, every pixel brighter than half-scale was being read as negative - the brightest parts of the frame coming out darkest.

Switching between saved profiles could break an install

Reported by a customer running two rigs (a native ASCOM machine and an Alpaca bridge): saving each as a profile and switching between them left Starship unusable, and reinstalling didn't help.

Loading a profile copied it straight over your config.toml with no checks. If the profile couldn't be read, the unusable file landed on disk anyway and the next start fell back to built-in defaults - which meant the dashboard stopped answering on the network, the site coordinates reverted to 0,0 (quietly breaking focus-star slews, airmass and the scheduler) and the device transports switched off. Reinstalling didn't help because the installer deliberately never touches your settings, so the broken file survived.

Now: a profile is read and checked BEFORE anything is replaced, so one that can't be used changes nothing and tells you why; a backup is written first, so Restore backup can undo a switch; and your web address and port stay put - switching rigs no longer moves the dashboard out from under you.

Your saved passwords were readable over the network

The settings page fetches your configuration from the app, and that copy included your email (SMTP) password, your Pushover keys and the dashboard password hash in plain text. With dashboard sign-in turned off - the default - anything on the same network could read them. They are no longer sent; the page is told only whether each one is set.

Saving Settings no longer wipes a stored password either. Because they are no longer sent to the page, they came back empty on save; a blank password now means "leave it alone", and that rule lives in the app rather than the page, so it holds no matter what does the saving.

After an unexpected restart, Starship could come back as a different rig

If you start Starship with a specific configuration file and it restarted after an unexpected exit, it came back without that file - falling back to whatever it found, or to built-in defaults. Same observatory, different settings. It now remembers and reuses exactly what it was started with.

Related: an automatic restart could start Starship twice (only one survived, but the extra one counted against the crash-recovery limit, so a genuine crash later could go unrecovered), and a recovering copy could pop a browser window in the middle of the night. Both fixed.

First run of a fresh install now creates your config file

Installing somewhere new started on built-in defaults instead of copying in the documented example configuration, so you never got the commented starting file that explains what each setting does.

Running your own scripts from a sequence could fail

The run_script action worked out which program to run in a way that was only sometimes correct in a compiled build, so it could fail with "file not found" depending on what else had happened first. It now resolves the running program directly.

Signing in returned a server error instead of an explanation

Access control isn't included in this edition, and the sign-in endpoint tried to use it before checking whether it was there - turning "access control is disabled" into a server error. The access-catalogue endpoint had the same fault.

Also

Starting with a configuration file that doesn't exist now says so plainly, and warns that safety monitoring is off, instead of going quiet.

v21.0.4 — big-sensor cameras over Alpaca no longer fail on download

A camera could connect fine and then fail every capture — the image download died at about 88% with a cryptic "Insufficient memory to continue the execution of the program", even on a PC with tens of gigabytes free.

The cause was a large sensor over an Alpaca / ASCOM-Remote bridge. When the Alpaca server doesn't support the compact binary ImageBytes transfer, Starship falls back to the JSON ImageArray — and for a 61 MP frame that JSON is ~250–400 MB of text. It's the server (often a small bridge box, or a 32-bit ASCOM Remote), not Starship, that runs out of memory building it.

Three fixes:

"the Alpaca camera server ran out of memory building the JSON image … it does not support binary ImageBytes … update / 64-bit your ASCOM Remote, or connect the camera via native ASCOM."

that image is reused instead of being fetched a second time.

million pixels) is replaced with numpy on both the JSON and ImageBytes paths, ImageBytes frames are writable, and a malformed image reports a clear error.

Diagnosed from a diagnostics bundle sent in by a user in Germany. If you hit this, the real fix on your side is a 64-bit ASCOM Remote that supports ImageBytes, or connecting the camera via native ASCOM.

v21.0.3 — blockscripts can power the gear on

A gap found by asking a simple question: could a user reproduce, entirely in the GUI, an unattended "start when safe" run set up on a real rig? Almost — except for the one step that matters most on a rig whose gear is fed by a power switch: turning the power on.

A blockscript runs its own small set of actions, separate from a sequence's. That set had park, unpark, open_roof, guiding, cooling, run_sequence, retry_connect, wait, notify, run_script — but no way to switch a power outlet on, and no connect_all. So a cold-start blockscript that must power the mount/camera up before connecting to them had no way to say so: it returned "unknown action" and dropped to EMERGENCY. You couldn't build it in the GUI, and you couldn't hand-edit it in either.

Now a blockscript can do the full cold start: connect_power → power_on → connect_all → unpark → open_roof. New actions connect_power, power_on, power_off, connect_all, disconnect_all, disconnect_power. power_on/power_off take a switch channel, and in the Blockscripts editor that's a picker showing your switch names ("Quad Power"), not a raw number. connect_all matches the dashboard's Connect-all — it leaves power alone, because power is connected and switched on its own, first. The "typical startup" template now shows the right order out of the box. (connect_all connecting around power is deliberate: v20.91.)

v21.0.2 — found on a real rig

Four fixes from a night of testing against live hardware. None of these could have been caught by the test suite as it stood, and one of them was hiding inside the previous release.

Click-to-center did nothing. ASTAP doesn't write the image size the way we were reading it — it reports DIMENSIONS, not NAXIS1/NAXIS2, and we only looked for the latter. So the frame size was always missing, and v21.0.1's rewritten click-to-center (which needs it to turn your click into a sensor position) refused every click. Before v21.0.1 the same missing value made it skip its bounds check instead, which is why it slewed to the wrong place: one bug was hiding the other. The plate solver also now reports the field of view again.

The focus wizard could park the focuser in the wrong place — and then refocus never worked again. When a sweep starts far outside focus, its first frames are a flat smear where only a handful of stars are detected. Those points aren't part of the V-curve, but they were being fitted as though they were, and they dragged the computed best-focus dozens of steps away from the frame that actually had the smallest stars. The wizard trusted that answer, moved the focuser there, and saved it. Refocus searches around wherever the focuser is, so it kept searching the wrong place and rejecting its own results with "fit quality too low". The fit now refuses an answer that contradicts the sharpest frame it actually measured, and re-fits without the star-starved frames.

A failed sequence now tells you why. The run view showed a red "failed" badge and an empty journal. But a run that stops at a precondition — guiding switched on with PHD2 off, or cooling at start with no target temperature — aborts before its first step, so there's nothing in the journal to read. The reason is now shown in the run view.

The remote Operate page had the same stale "still imaging" label that the console had in v21.0.1, and is fixed too.

Also, from the same night

Clouds now stop guiding. When the weather turns and a run is put on hold, the roof is closing or closed — but PHD2 kept "guiding" on a star it could no longer see, still nudging the mount. Guiding is now stopped as part of holding the rig, and restarted on the re-pointed field when the sky comes back. (A manual pause is untouched: nothing is closing, so nothing drops your star.) A guided plan still refuses to expose unless PHD2 is actually guiding.

Dithers are recorded. They happened, but appeared in neither the log nor the run journal, so a night report couldn't tell you whether the night dithered at all.

Settings that need a restart now say which ones. Every save used to claim "needs restart", which taught everyone to ignore it — so enabling PHD2, or moving the images folder, could silently do nothing for hours. Saves now name exactly the settings that won't take effect until Starship restarts, and stay quiet when none do.

Missing optics are no longer silent. With focal length or pixel size unset, the plate solver gets no scale hint and has to guess, which makes solving slow and unreliable and quietly degrades precise pointing and click-to-center. Starship now says so, and reports it to the UI.

A whole-sky plate solve gets its own time budget. The blind fallback was inheriting the timeout meant for a solve that already knows roughly where it's pointing, so it could never succeed where the hinted attempt had just failed.

A plan that flips a mount configured not to flip now says so. Flip mode: off on the mount looks like a master switch, but a plan carrying its own meridian setting overrides it. That precedence is unchanged — a plan is an explicit choice — but it is now written to the run journal instead of happening quietly. Plans can also now say inherit to let the mount decide.

The focus wizard no longer parks on an answer it doesn't trust. If the V-curve fit comes out poor, it now moves to the frame that actually had the smallest stars rather than to the fit's computed minimum.

v21.0.1 — on-rig fixes

Four bugs found testing v21.0 on the rig. Two were in the console UI, where this project has no automated browser coverage; both fixes were verified in a real browser against a running server, and all four are now pinned by tests.

The filter dropdown on the console vanished before you could pick a filter. The console's periodic refresh rebuilt the dropdown on every tick, so an open list was destroyed mid-click. (The same re-render also wiped whatever you were typing into the free-text box when no wheel is connected.) The dropdown is now rebuilt only when the filter set genuinely changes, and never while it is open.

**The sequencer and scheduler filter dropdowns listed your Filter Library and the wheel's raw "Filter 1, Filter 2…" slot labels. They now offer exactly the library names — or the wheel's own names if you keep no library, plus OSC** for a colour camera. Filter selection is unchanged: a saved plan that still refers to a raw driver name or a slot index keeps working.

"Center here" slewed the mount the wrong way. Not a sign error — a scale one. v20.98 retired the full-res FITS viewer and moved click-to-center onto the downsampled preview, but the browser kept sending its preview pixel as though it were a sensor pixel. On a large sensor that put almost every click up and to the left of where you actually clicked. The console now sends a position that doesn't depend on the preview's size at all. Click-to-center also refuses to slew — rather than guess — when the plate solve fails, the WCS is incomplete, or the click lands outside the frame.

Stopping a sequence left the top bar saying "Sequence running", and manual captures stopped showing a preview. Those are the same bug: while a run is active the console deliberately holds full-res science frames behind Show last image, and the run never looked finished. In fact any completed sequence — even a clean success — latched it, not just a stopped one. A finished run now reports itself finished, from a single source of truth shared by the status bar, the preview, and the API. The remote Operate page carried the same stale label and is fixed too.

Also hardened while we were in there: click-to-center refuses bad input instead of guessing. A non-finite or out-of-range coordinate posted to the API used to clamp silently to the edge of the frame and slew there; it is now rejected before the plate solve, as is a solve that doesn't report the image size (without it the target pixel cannot be bounds-checked).

v21.0 — "Night Watch": the biggest reliability release yet

Three coordinated waves — Night Watch (P0 safety), Morning Report, and Quick Wins — about 40 shipped items, hardened by a multi-agent adversarial review that caught and fixed 60+ real bugs before ship. This is the release that makes an unattended night survive, keeps the equipment safe, and makes the data honest.

Night Watch — the night survives, the gear stays safe

during a supervised sequence, the sequence body now suspends first — it aborts the in-flight exposure and stops guiding — and only then does the rig park and close. On resume it re-slews to the target and restarts guiding, so you never again image the park position after a weather hold.

Starship re-reads the actuator ~5 s later; an ACK'd-but-unmoved close or park now fails the action and pages you once, instead of trusting a lying driver.

next-dawn deadline: a never-clearing hold tears down cleanly at dawn (bounded warm-up → park → close → alert) instead of waiting to the next dusk. Suspend/ resume cycles are now unlimited (bounded by that dawn deadline, not a counter), and resume waits for ≥60 s of continuously-safe readings (suspend still trips on the first unsafe one).

~30 min escalates to EMERGENCY (park + close + critical alert), not a mere hold.

before opening the roof and ends cleanly if the weather never clears — no more booting into EMERGENCY → HALTED.

(stop/restart PHD2, fresh star) → SKIPS the target with reason → pages, instead of grinding unguided for minutes and reporting success. The ladder yields promptly to a weather suspend so a teardown never runs while guiding is active.

target counts ("finished: 42 subs across 2 targets, 1 target skipped (guiding)").

glass and labeled as the intended filter.

immediately (you own safety) instead of waiting forever.

Default keeps today's park-first behavior; roll-off owners whose roof clears the OTA can switch to close-first so the roof shuts fast in bad weather.

Guardian — surgical, blast-radius-aware recovery

can feed several devices. Guardian then resets the exact USB port or power relay for the device that failed, re-initializes every co-powered device after a shared cut (mount → find home, cameras → re-cool), and refuses to cut a shared output while a co-powered device is busy (slewing, exposing, downloading) or its state is unknown. Unmapped rigs behave exactly as before (soft reconnect only).

Morning Report — you find out what happened

failure, guiding loss / PHD2 disconnect, a zero-frame night, and blockscript terminal states. Alert settings apply live, with a sent-history view, a test-send button, and rate limiting (safety alerts are never limited).

HFD and guiding trends, alerts sent, and skips with reasons.

backoff so a failing target isn't re-picked every minute. Meridian-aware scheduling (flip handling is opt-in — the first live flip should be supervised).

Quick Wins

restore of the automatic backup (and a native message box on the windowless build) — never a silent death.

chip. RA/Dec fields accept sexagesimal ("05:35:17") everywhere or reject it loudly — no more silent slew to RA 0. "Connect all" really launches PHD2. Blockscript "Run selected" saves first. Framing → Add to Scheduler keeps the position angle. "Warm up" returns immediately and reports progress. Auto Flat reports failure when zero flats were saved. A licence expiry banner appears a week out, and park/roof stay reachable after lockout so a lapsed subscription can never strand a roof open.

v20.99 — "Night Polish": 13 on-rig fixes on top of Framing

Thirteen items straight from a night on the rig — framing, PHD2, mount, and timing.

focus-quality readout only appeared if per-frame grading was switched on (off by default), so most rigs just saw "HFD —". The preview now measures the frame it shows, so the HFD and star count light up on every captured frame.

checkbox (default on) and "PA °" input already worked from the Add-target form; now they render on every sequence target row, so you can turn precise pointing off — or set a per-target position angle — right where you build the sequence.

end-action re-runs the plan you just finished (great for repeating a short loop). Supervised runs skip it automatically.

end-action hands off to any saved plan when the current one ends. Guarded so a plan can't re-launch itself and two plans can't ping-pong.

image moved out of the hidden right-click menu onto a visible toolbar on the preview header. Center-here shows a crosshair cursor** and still plate-solves + precisely slews so the point you click becomes the new centre.

the preview shows a hand-drawn galaxy with a caption instead of a blank panel.

flat device now stack neatly beneath it in one column.

behind extra flags so it silently never ran; now the single "Auto-launch PHD2 on startup" toggle is all it takes.

equipment is disconnected, Starship now connects it for you — and a guided sequence gives PHD2 one more chance to get ready before it gives up.

Voyager logs, the settle/dwell defaults were retuned for steadier results: longer settle after a slew (10 s) and after centering (10 s), tighter pointing tolerance (~10″, now editable in arcseconds on Plate-solver settings), up to 5 centering iterations, cooling held to 0.5 °C, a 5 s filter settle, a gentler 3-pixel dither with a 13 s settle, an 18 s wait after guiding starts, and a new "Refocus after meridian flip" option (off by default).

— the console stays a monitoring surface, not a slew joystick.

control (iOptron calls this the zero position — it's distinct from Find Home). It appears only when your mount driver exposes the command.

v20.98 — "Framing": one image surface, smarter preview, per-target pointing

A framing-and-focus rework of the console.

per-capture preview is now the single place images appear. For deep inspection, open the saved FITS in ASTAP / PixInsight / your tool of choice.

right-click it for Zoom in or Center here (Center here plate-solves the frame and precisely slews so the point you clicked becomes the new centre); and it's frame-aware — the quick bin2 pointing/framing frames appear live even during a sequence, while full-res science subs are held behind a "Show last image" button so a run stays calm on a small PC.

target / PA) always on show — no longer buried in the Camera and Mount cards.

what's idle, working, warning, or faulted (neutral when disconnected, so a cold rig isn't a wall of red).

to a new target it now plate-solves and precisely centres before imaging, so your framing is repeatable night to night. It's default on with a per-target "Precise point" checkbox to turn it off, and a per-target position angle — if you have a rotator, it rotates the field to that PA. It's fail-soft: with no plate solver or no mount (a backyard rig), or on a transient solve hiccup, it simply falls back to a normal slew and keeps imaging — it never stops your night.

v20.97 — "Steady Link": Alpaca devices no longer drop on a first blip

Some Alpaca devices — especially serial-bridged DIY controllers like a GRBL focuser — auto-reset the instant their serial port opens and need a second or two to come back. During that window they briefly report "not connected" or time out. Starship used to disconnect on the very first bad reading, so the device would connect and then drop ~3 seconds later, while other clients (e.g. Voyager) rode out the blip and stayed connected.

failed liveness reads before giving up on a device — the slot shows "reconnecting (n/N)" during the grace window instead of vanishing. A real unplug is still caught within a few poll cycles, and any good reading resets the counter.

consecutive blips to ride out. Set it to 0 to restore the old drop-on-first-failure behaviour.

([alpaca] connect_verify_s, was a fixed 3) for the device to report ready — a board whose reset takes 4–8 seconds used to fail the connect outright. Healthy devices connect just as fast as before.

per-request timeout, for bridges that are legitimately slow but alive.

existed but were never wired in — an Alpaca mount showed no RA/Dec/tracking at all. Fixed.

health monitor no longer sees the device as "stale" — so it can't "help" by reconnecting it mid-reset (which would just reset the board again).

still drops, just not on a momentary hiccup.

v20.96 — "Calm Night": stable, unhurried sequence execution

A sequence run should be calm — one action at a time, waiting for each to finish, never fighting itself for the CPU. v20.96 makes that true, after a real night where a run raced through actions, pegged the PC at 98%, ignored Stop, and didn't park. What changed:

preview render, plate solving — now runs strictly one at a time (a "load governor"), so they can't stack up and freeze the app. On a small observatory PC this is night and day.

convenience) is skipped while a sequence runs and previews are now downsampled instead of rendered at full 24-megapixel size. Open any frame in the viewer manually whenever you want it.

while the filter wheel is still turning — Starship waits for the wheel to report it arrived, then settles.

(which is normal — mounts take their time) no longer trips the watchdog into showing every device "disconnected".

physically possible, the run halts itself safely (parks / closes) instead of machine-gunning the gear — one log line instead of a wrecked night.

heard), plate solving runs at lower priority so it can't starve guiding, and an On End with no actions now says so in the log (so a "didn't park" is never silent). New Settings → performance knobs tune all of it.

v20.89d — manual-mode default, factory reset, recovery polish

have no CloudWatcher — a new install no longer sits in UNKNOWN hunting for one. Observatory owners: turn monitoring on in Settings → Safety.

defaults…" moves your config.toml to a timestamped backup, writes a fresh first-install config, and restarts — a clean slate in one click.

window — your existing window simply reconnects.

v20.89c — follower Choose fix + single-instance guard

showed [object Object] after picking a follower camera/focuser/wheel (an internal result object was saved instead of the driver id). A cancelled Chooser no longer wipes an existing choice. If you saw [object Object], just Choose the device again after updating.

the running instance, opens its dashboard, and exits — previously a duplicate would fight the original for the port and devices (and with the new crash watchdog, each copy kept resurrecting itself). The installer also closes a running Starship before updating files.

v20.89b — live-test fixes + the crash guard

unexpectedly (e.g. a native driver fault the app cannot catch), a tiny watchdog relaunches it within seconds — and a crash tracer writes the exact state of every thread to starship-crash.txt so the fault can be diagnosed and fixed for good. Clean shutdowns (the Terminate button) do NOT restart, and a broken install won't relaunch-loop (3 restarts / 30 min cap).

always rendered unticked, even while monitoring was actively running.

warning plus a ~10-minute summary instead of two lines every poll, and the dashboard now explains what UNKNOWN means and points to manual mode.

to be lost if the sky view was still loading; it's now queued and framed the moment the atlas is ready.

v20.89 — "Trust the Night": 20+ reliability fixes from a full product audit

A deep audit of every subsystem — walked end-to-end the way a real imaging night runs — found and fixed the failures that used to bite unattended at 2am:

Weather & safety now actually interrupt the night

unattended engine never received safety verdicts — its whole UNSAFE → park → close → resume ladder was dead. Fixed (and covered by tests).

your On Suspend actions), cancels the in-flight exposure, and auto-resumes — running On Resume — when conditions are safe again. Manual pauses still wait for you. A scheduler chunk now stops immediately on UNSAFE instead of exposing for the rest of the chunk.

open, mount tracking, all next day) — a failed body now drives the engine's safe path and a stopped body ends the session cleanly.

health escalations now go out via your email/Pushover alert channels (they were log-only).

The meridian flip is now guided end-to-end

after the recenter** — previously guiding was never cycled, stranding PHD2 for the second half of the night. A failed guiding restart fails the run safely instead of silently imaging unguided.

swapped; East showed as "—"). Fixed.

Focus you can trust

R², max HFD, minimum-in-range). A cloud-spoiled fit fails cleanly and returns the focuser to where it started — no more imaging defocused until dawn because one fit went bad.

the working store was a hidden JSON field). Library values win; existing configs keep working.

flag clears as soon as PHD2 guides again.

Your data is correct

minutes late — breaking comets, asteroids, photometry and stacking math).

with the shutter OPEN and are labelled FLAT (they were shot as DARKs), bias frames are labelled BIAS.

rejected subs no longer count as progress, so goals finish with real data.

Quality of life

wipes the multi-night acquired tally.

dawn cutoff that never goes stale like a fixed end date (-6 = civil twilight; needs site coordinates).

lost-mount recovery works when the mount is more than a few degrees lost.

discarded); the ASCOM watchdog setting survives Settings saves; the footer safety readout updates; the Run monitor's duplicate Resume button (which triggered the wrong action) was removed — ⏸ Pause toggles to ▶ Resume.

parks in a visible ERROR state instead of silently spinning until dawn.

v20.87 — Piggyback: a second imaging train

filter wheel) on the same mount as your lead train — two scopes imaging in tandem. Turn it on in Settings → Piggyback, set up the follower + its capture plan (filter, exposure, cooling, refocus cadence) in Settings → Follower train, and start it from the dashboard's Follower panel — or arm it to run automatically with your sequence. The public edition supports one follower.

flip / dither, and the lead and follower never autofocus at the same time — the two are serialised, so the PC is never running two focus sweeps at once.

single-rig setup. See the Piggyback help topic for the full walkthrough.

v20.86 — ASCOM on by default for new installs

transport enabled, so the Chooser opens the first time you use it. (Before, a new install started with ASCOM off and showed a red "ASCOM disabled" until you turned it on in Settings and restarted.) If ASCOM is ever off, the Chooser now tells you exactly how to turn it back on.

v20.85 — Runs on more machines (Python 3.12 rebuild)

3.12 and ships as a self-contained runtime** — it runs on a wider range of Windows 10 machines (including older long-term-servicing builds such as 1607 LTSB) with nothing to install alongside it. No behaviour changed; this is purely a packaging/compatibility rebuild so more rigs can run the same build.

Commercial data-hiding protection and the console window hidden, and signed by the production** licence key.

v20.84 — On-rig fixes + protected production build

exposing if PHD2 isn't actually Guiding (looping/stopped/star-lost) — it holds, then fails the sub so your on-error policy decides. No more silent unguided subs.

shortcut, Starship now resolves it to the real phd2.exe (and falls back to the standard install location).

the reason (e.g. meridian halt), and flip/abort failures include the cause. (Alerts still need to be enabled + an email/Pushover channel configured.)

alerts instead of quietly carrying on with half the gear. (Disconnect-all still releases everything.)

moment on the final curve), not inline in the run journal.

window hidden, and signed by a fresh production licence key.

v20.83 — Online supporter licence (client) + rocket logo

edition and licence status (licensed-to, valid-through, days left), this computer's licence ID, and a field to paste the activation key you receive when you subscribe — with Activate and Refresh now.

{activation_key, machine_id} with the licence server (api.astroworxstarship.com) for a fresh, machine-bound, signed licence about once a day. If it can't reach the server it keeps the cached licence and keeps running until that licence's own expiry — your paid period plus a 10-day grace. A flaky network never ends a night. The startup also does one best-effort refresh before the gate, so a renewed subscription unlocks immediately.

/api/license/activate, /api/license/refresh endpoints, and a daily refresh thread; the licence file is cached at ~/.starship/license.key and verified offline by the existing signed-licence gate.

splash, the top bar, and as the favicon — and across the marketing website. The product name stays Astroworx Starship.

v20.82 — Startup splash + Acknowledgements

developed in Australia, the mission — a functional, reliable tool for the astronomy community — and a note of gratitude that Starship only builds upon the experience and learnings of the giants before it. It links to www.astroworx.com.au, shows once per browser session, and dismisses on the timer, the Enter Starship → button, or Esc.

credits the standards, projects, libraries and data sources Starship is built on — ASCOM/Alpaca, PHD2, ASTAP, Cartes du Ciel, OpenNGC, NumPy, psutil, the Ed25519 reference, Meeus, and the wider open-source astronomy community.

v20.81 — Two editions: Astroworx Starship + Professional

Starship** build excludes telescope collimation, the research module (Navigator), the rental web dashboard, and user booking — these are compiled out, not just hidden, for the Astroworx Starship Professional build used on special projects. Everything else is fully available in both. The build self-identifies (title bar, About, /api/version) by which modules are present — no manual flag to keep in sync — and the UI automatically hides whatever isn't in the build. Set STARSHIP_EDITION=public to preview the public edition from source.

v20.80 — License protection for distributed builds

compiled copy, it refuses to start without a license.key that is bound to that machine and an expiry date — so a copied build won't run on another machine, and every license dies on its expiry day. You mint licenses with a private key only you hold; the build embeds only the matching public key, so they can't be forged. Source/dev runs are never gated. See docs/LICENSING.md for the workflow (generate a keypair once, then mint a license.key per recipient from the machine ID their build prints). Built on a vendored Ed25519 (RFC 8032) — no new build dependency — and hardened against a battery of forge/tamper/bypass tests.

monotonic clock against a zero baseline, so on a freshly-booted machine the very first digest could be silently suppressed. It now always sends the first one.

v20.79 — Sequence controls that actually update

stop→start the buttons never changed, so it wasn't clear what state the run was in. Now Start is disabled while a run is active, Stop only enables while active, and Pause flips to Resume when paused. The badge + buttons also refresh the instant you press one (instead of waiting for the next poll), so it's obvious when a sequence has actually stopped.

its own — it also needs the executable path filled in. The settings now show a clear warning when auto-start is on but the path is blank, and note that PHD2 launches on Starship startup or via Connect all (not when a sequence starts).

v20.78 — Meridian flip: stop holding a mount that's already flipped

The flip manager decided whether to flip purely from hour angle (and whether it had flipped recently) — it never checked which side of the pier the mount was actually on. So a mount that was already on the correct post-meridian side (slewed straight onto a western target, or already flipped) got wrongly held / re-flipped, stalling the sequence. Now it reads SideOfPier: if the mount is already on pierEast ("looking west", the correct side past the meridian), no flip is needed and the sequence continues. A flip is only triggered from pierWest. New [mount] flip_pier_side_check (default on) gates this; set it false only for a driver that reports SideOfPier with an inverted sense.

v20.77 — Four UI fixes from the rig

on each row (it was missing entirely).

a target — was silently doing nothing: an internal form-sync step could throw on a plan with no title set, which aborted the delete before it happened. Fixed at the root, and hardened so a sync hiccup can never block a delete.

as gray hint text (e.g. "530") that didn't actually count until you retyped them. They're now filled in from your camera config, so the field-of-view calculates immediately.

PHD2 didn't come up you'll see why (e.g. "open PHD2, or set its executable path + auto-start"). Those settings already live in Settings → PHD2 guider; set the PHD2 executable path and turn on Auto-start for the one-click launch to work.

v20.76 — Guiding guardrails, Sky Atlas, and a centred night

A batch from a real night at the rig:

wipe the targets already in your editor; it now keeps them and appends the new one.

connected, the run now stops immediately with a clear message ("open PHD2 and connect the guide camera + mount") instead of silently skipping guiding and spamming connection retries.

executable path is configured) and connects its guide camera + mount, so guiding is ready in one click.

the V-curve now draws live in the run panel as samples come in — no need to open the console.

so tonight's dark window sits in the middle of the plot (with a "now" marker), instead of the daytime splitting it at the edges.

property as if it were a method); it now actually holds exposures when guiding degrades.

v20.75 — Refocus actually lands on focus

A real-rig night exposed it: the Adaptive First Light wizard nailed focus at position 2361, but a later Refocus parked the focuser at 2279 — ~80 steps soft — even though it had measured a perfect V-curve with the minimum plainly at 2361.

wide range). At that coarseness the sharp bottom of the V got a single sample, and the noisy outer wings of the wide sweep dragged the curve-fit ~80 steps inward.

step over a tighter range** around the current (already near-focus) position, using the same number of exposures. On the real curve this moves the fit from 2279 to ~2360. Two new knobs ([autofocus] refocus_step_frac, refocus_points_per_arm) tune it; set refocus_step_frac = 1.0 for the old behaviour.

correct. Backlash handling was also checked and confirmed correct (a single outward over-travel, then an inward settle; no double compensation).

v20.74 — Sharper collimation: adaptive poke + damped solver

Two opt-in upgrades to the closed-loop collimation math, both off by default (the default path is byte-for-byte what it was). Turn them on in [collimation].

poke when measuring the influence matrix, each actuator's push-pull is grown until the donut's response clearly clears the measurement noise floor — so the matrix is built on real signal, not noise. It starts at auto_cal_step and grows (bounded by the mirror's travel range); a stalled actuator is still caught by the existing zero-response / ill-conditioned guards.

solver that works in the pure-tilt (focus-held) subspace. On a healthy matrix it matches the old pseudo-inverse exactly; on an ill-conditioned one it damps the correction instead of overshooting or refusing — a gentler, safer move.

cumulative travel backstop, divergence abort, and focus-holding piston projection all still apply. The math is validated against the desk simulator; the physical mirror moves still want a supervised first run on the rig.

v20.73 — Auto-defocus: size the donut for an accurate measurement

The collimation measurement only works if the defocused-star donut is the right size. Too small and you're measuring noise (your real test frame came in at ~14 px when we need ~50); too big and the donuts overlap.

focuser (not the mirror) until the donut hits the target diameter: probes at a test offset, scales to the target, verifies with one correction, and reports "donut 48 px — in range ✓ · +2500 steps from focus".

just measure-and-scale. Doubles the probe if the first try is too small; clamps to a safety limit so it can't run the focuser into the stops.

actuators connected.

defocus_max_px (the 35–80 acceptance band), defocus_initial_steps, defocus_max_steps, defocus_direction.

v20.72 — Tell me what Starship is thinking

make tonight's plan is listed with the reason ("below 30° all night", "moon-washed", "complete", or "didn't win a slot — lower priority").

it's up and which filters the Moon allows tonight ("up 52° · narrowband only, 28° from a 90% Moon").

(Running: "run scheduler for a multi-target campaign, or run sequence for a fixed plan").

resumes — so you know what to pick.

v20.71 — Decision help where you actually decide

From walking the setup as a new user would:

exposure, e.g. "= 2.5 h") and a per-target total, so you can tell at a glance whether a plan is enough. Pick a narrowband filter and the exposure jumps off the 120 s broadband default to a sensible 300 s.

its typical actions ("typical: open roof, unpark, cool to") with a + add typical one-click fill, so you start from a sensible default and edit from there.

v20.70 — Scheduler readiness bar + blockscript lifecycle map

Two big clarity upgrades, one shared colour language (green = ready, amber = needs you, blue = waiting on nature, red = blocked).

Scheduler — a step-by-step readiness bar up top shows exactly what to set up, in order: location → strategy → targets → filters & frames → simulate & start. A colour-coded banner always names the single next thing to do (e.g. which target has no filters), Start is locked until you're ready, and every target card is tinted by its status.

Blockscript — a lifecycle map sits above the action lists: the seven states (startup → running → shutdown, plus the warning / unsafe / resume safety loop and emergency) as colour-coded nodes you can click to jump to. Each list now states its own trigger and exit ("in: safety UNSAFE · out: safe + dwell → Resume"), and during a real run the current state lights up live — the editor doubles as a status panel.

v20.69 — Collimation: verify the measurement by eye

The Measure view on the collimation page now overlays, on each detected donut, a green ring + centre cross, a cyan dot on the detected shadow centre, and an amber line showing that donut's decenter — plus the net-error arrow. Read it as an accuracy check: rings should hug the donuts and cyan dots sit centred in each shadow; when the amber/cyan offsets all point the same way it's a real collimation error, and when they scatter it's noise or poor detection (fix focus/exposure/ defocus before trusting it).

v20.68 — Collimation: trustworthy measurements

Groundwork for closed-loop collimation you can trust on the real mirror (all behind the existing arm flag and safety gates):

shifted between iterations and stops rather than act on a reading taken after the donut population changed. (Set [collimation] auto_max_field_drift_px; default is report-only so it never surprises a working rig.)

decenter measurement, so corrections and convergence aren't chasing noise.

have hurt accuracy — the loop measures the absolute decenter, which is already robust to field shift.

Next: adaptive defocus sizing, adaptive calibration poke, and a damped-least-squares solver.

v20.67 — Pick a schedule + wait for dark

action now has a dropdown of your saved schedules. Pick one and it loads that schedule's targets before starting (leave it blank to run the current pool). It won't swap the pool out from under an already-running campaign.

until_dark (Sun below −18°, astronomical, from your site location) and until_safe_dark (waits for SAFE and dark).

v20.66 — Action qualifiers & focus-with-filter

to Refocus; pick a filter and it moves the wheel there, then focuses at that filter's exposure (or leave it on Current filter).

category (roof, mount, dome, power, camera, utility) for easy separation.

(blockscript) take an "over X minutes" ramp, so you can cool to 0 over 5 min instead of slamming the setpoint.

device's named outlets (when connected).

lazy-loads them).

v20.65 — Auto Flat planner

device, the per-filter plan is a table: one row per filter with shots, target ADU, tolerance %, binning and initial exposure (blank cells use the global defaults).

filter** (flats_<filter>/).

v20.64 — Moon × filter awareness in Skyhunter

now shows BB / NB chips (broadband / narrowband), struck through when the moon washes that class out. A Hide moon-washed toggle drops targets that nothing can shoot. The object detail shows Usable filters at transit.

(separation scaled by moon brightness, narrowband relaxed), now surfaced for planning. Tune the thresholds under Scheduler → moon avoidance.

v20.63 — Wait modes, FOV calculator, Skyhunter → scheduler

wait until a time (either HH:MM tonight or a full date/time). A timeout caps the gated waits.

and the field-of-view fills in automatically, no connected camera needed.

prefills the Add-project form and jumps to the Schedule page.

v20.62 — See your enclosure

now draws your enclosure (roll-off roof or dome) and colours it by shutter state: green when open, muted when closed, amber while moving, red on error.

now an editable form with a live top-down diagram showing what each offset means (dome circle, North, the open slit, the mount offset and GEM arm). Save writes straight to your config.

v20.61 — Save & recall schedules

named schedule, then Recall it (adds the targets) or Replace the current pool with it later. Recalling starts a fresh campaign at 0 frames.

instead of a free-text box — no more typing the exact name.

and save it as a scheduler template; the scheduler fills that slot with each chosen target. Any other targets in the template stay fixed.

v20.60 — Sequence actions & smarter refocus

Sequence editor

Mount & pointing, Dome, Power & equipment, Camera & focus, Utility) — much easier to find the action you want.

gear), Home mount, Plate-solve & sync, and Blind-solve & sync (recover) — a true whole-sky solve that re-syncs a mount that has lost its place.

altitude (default −18° astronomical), with an optional extra delay. Uses your site location.

Console

or pick another; it moves the wheel there first and uses that filter's focus exposure.

v20.59 — Enclosure type is now pickable

accidentally locked (disabled) even though dome support has been built since v20.38 — so picking Dome to enable dome slaving was impossible. Now it's a normal dropdown with friendly labels (Roll-off roof / Dome).

v20.58 — Console & sequencer polish (batch 1)

then switching tabs, validating/saving/starting no longer falsely says the plan is empty. It now always reads your live edits.

the frame and immediately syncs the mount's pointing model (no scope movement).

Blockscript in operation so you can see at a glance what the rig is doing.

More of the requested console/sequencer improvements (focuser refocus-filter dropdown, new sequence actions with category grouping, schedule save/recall) are coming next.

v20.57 — Scheduler page redesign (the glass box)

The Schedule page now explains itself, so programming a campaign is intuitive:

shows the campaign state, roof, darkness, Moon, and how much of tonight's goals are done.

now, ready — waiting its turn, Moon too close — 18° from a 64% Moon, still rising — 24° (needs 30°), sets in 22 min, complete, paused*.

plain meta ("priority high · won't shoot below 30° (now 41°)") instead of cryptic shorthand.

finish-first), with a short "how it works" intro on adding a target.

the night with a legend — instead of a text list.

This completes the filters + scheduler rework (filter library, per-filter refocus exposure, OSC, multi-filter plans, and this glass-box redesign).

v20.56 — Multi-filter scheduler plans

Adding a target to the scheduler is now a real plan builder. Type the name → Resolve (coordinates fill in) → add one row per filter (each with its own exposure and frame count), hit + filter for more, and one Add project saves the whole plan. The filter dropdown pulls from your filter library, including OSC. You build a broadband (L/R/G/B) or narrowband (Ha/OIII/SII) plan simply by which filters you add — no more one-filter-at-a-time.

v20.55 — Filter library + per-filter refocus exposure

Settings → Filter wheel → Filter library lets you name the filters in your wheel and give each one a refocus exposure — the autofocus exposure used when it refocuses on that filter. Dim narrowband needs longer than broadband (e.g. L 4 s, RGB 6 s, narrowband 12 s); blank uses the global autofocus exposure. Every refocus path now uses the right per-filter exposure — both the manual Refocus and the in-sequence refocus on a filter change.

For a one-shot-colour camera, add a filter named OSC — and because an OSC camera can still sit behind a wheel of dual-band filters, you add those too. An empty library falls back to the wheel driver's reported names.

This is the foundation for the multi-filter scheduler plans and the scheduler-page redesign, which land next.

v20.54 — Theme selector

System → Appearance now offers four themes, saved per browser and applied instantly (no restart):

the eyepiece.

legibility.

The choice is remembered and applied before the page paints, so there's no flash on reload.

v20.53 — Manual mode (backyard / portable / no weather sensor)

You can now run Starship without an observatory or a weather sensor. Settings → Safety → "Safety monitoring enabled" (or [safety].enabled = false) turns the weather supervisor off: the status reads MANUAL MODE instead of a red UNSAFE, nothing is gated (you're the weather sensor), and the weather poll / loss-escalation / response engine don't run — so there's no spurious "no contact" fault. Leave it on for a real observatory, where the roof gate matters.

Also clarified: flat panels / cover calibrators and Auto Flat work without an observatory — a flat device is its own connected accessory, not tied to the roof. A backyard imager with a flat panel gets full flat-cover and auto-flat functionality. The only things a no-enclosure setup skips are the roof/dome actions and the fully-unattended roof-cycling campaign.

v20.52 — Smarter target picking (ideas from APSCHED)

After studying a friend's Voyager scheduler (APSCHED), three of its ideas made the scheduler pick better:

is lowest — a score boost that peaks at transit and fades over ± transit_window_min (default ±2 h). You can also make it a hard cut (transit_gate) to image only near the meridian. On by default as a gentle preference.

illumination, so a thin crescent needs far less clearance than a full Moon (continuous, not a fixed step). A 20°-from-Moon target that the old rule always skipped is now shot under a slim crescent and only skipped as the Moon brightens.

constraint, the scheduler relaxes the soft ones — transit window first, then Moon — and shoots the best of what's left, instead of idling.

All three are config-driven, the existing scheduler behaviour is unchanged unless you opt in to the gate/relaxation, and the test suite is at 684 passing. Run scripts/_meridian_demo.py to see the meridian and Moon-scaling effects side by side.

v20.51 — Sakana Edition: smarter collimation + focus calibration

Integrated the "Sakana Edition" work and hardened it after an independent review. All of it is additive — the existing focus and collimation methods are untouched, and (verified) every primary-mirror actuator move still flows through the unchanged, tested auto-collimation core, with its clamps, damping, and divergence-abort intact.

measurement, makes guarded automatic actuator moves, takes a second stable measurement, and gives you an explicit verdict (PASS / IMPROVED / ALREADY_GOOD / FAILED) with a confidence and reasons. It stays default-off and disarmed (needs GRBL connected, a calibrated influence matrix, and the auto-collimate arm flag) and can only measure and classify — it has no direct actuator authority of its own.

guess a step and range, it measures the focus noise, probes for the smallest focuser move that actually changes focus, derives the sweep, and builds the V-curve with the existing wizard.

and writes a characterization report you approve in Settings → Focuser before it's used; the values then travel with your equipment profile.

Fixes applied during integration: backlash measurement now skips a sample if a focuser pre-move is refused by the travel limit (instead of trusting a corrupt number) and won't claim high confidence from only two samples; equipment profiles carry only the focuser characterization fields, never the operational ones like direction-reverse or travel limits; the first-run auto-calibrate default is off so the mirror never moves to calibrate without an explicit step; and two corrupted UI glyphs were restored. Full test suite: 678 passing.

v20.50 — Review fixes for the restart + simulation work

A review of the v20.49 changes found and fixed three real issues:

were reading the safety verdict the wrong way, which silently failed and got swallowed. The roof still always closed on unsafe — CloudWatcher is hardwired and the roof-open command independently refuses an unsafe sky — but the scheduler's own "close on unsafe" backup was doing nothing. Now fixed, so the safety net has all its layers again.

Starship now reads the roof's actual position instead of assuming it's closed — so if a power cut left it open, the next dawn/unsafe close actually fires.

empty target list now reports "not complete," the way the real run behaves).

v20.49 — Watch a week run + survive a restart

forward night by night — same target-picking, moon avoidance, set/dawn-aware chunking, roof open/close and completion as the live system, with the real sun and moon, and optional clouded nights. python scripts/_campaign_sim.py prints a 7-night run you can read; nothing moves. (In the example week the narrowband targets finished while the broadband one waited for the moon to set — the gate doing its job.)

list ([blockscript].auto_start). After a power cut or reboot, Starship relaunches that blockscript, which runs the scheduler again — and because the progress ledger is saved in scheduler.db, the campaign continues exactly where it left off (at most the one in-progress chunk is re-shot; its frames are already on disk). It only opens the roof once it's dark and safe, so resuming at any hour is safe.

v20.48 — Scheduler manages the roof + campaign hardening

The last piece of hands-off nights: Starship now opens and closes the roof itself. The scheduler opens (and unparks) when it goes dark and safe, and closes (and parks) at dawn, on an unsafe verdict, or when the whole campaign is complete — Schedule → Campaign settings → Manage roof (on by default). CloudWatcher stays hardwired to the roof as the hardware guarantee (it always closes on unsafe and won't open against one), so these are command-and-report on top of that. Voyager is no longer in the loop for the roof.

Also: an adversarial review of the campaign found and fixed six issues — the most important being that a post-emergency auto-recovery now correctly relaunches the scheduler (it was leaving the night without a dispatcher), and a graceful restart now stops the campaign cleanly. Plus smaller fixes: a template's altitude floor can no longer fight the scheduler's, chunks never overrun a target's set time by a frame, and same-named templates can't silently clobber each other.

v20.47 — Unattended campaign complete: set targets, let it run for nights

The whole thing is now wired end to end. Build a target pool (each target its filters + subs), an observing template whose target is a variable, and a blockscript that runs the scheduler unattended until every goal is acquired.

re-score each chunk) or finish_target (stay on a target until it's done or sets); per-filter moon avoidance (separation + illumination, narrowband relaxed — a target with no shootable filter right now simply isn't picked); and chunks that never overrun a target's set time or dawn.

the campaign body. It images night after night; when all goals are complete the session shuts down. Weather pauses and device-loss recovery work throughout.

sequence into a recipe (its first target becomes the variable); pick a template per project on the Schedule page; set strategy / moon avoidance / default recipe in the new Campaign-settings card.

Roof actuation between nights (closing during the day) remains tied to the verified roof-control layer and is the next step before fully hands-off nights.

v20.46 — Unattended campaign (1/4): scheduler observing templates

First piece of the "set a target list, let it run unattended for nights" system. The scheduler now images each target through an observing template whose target is a variable — you design the recipe once (slew/focus/guide/dither), and the scheduler swaps in a target from the pool and injects that target's filters/subs. The built-in default template reproduces the previous behaviour exactly, so nothing changes until you start using custom templates. Each project can carry its own template (per-target recipes). Settings: [scheduler] default_template and strategy. (Next pieces: target strategies + moon/dawn gating, then the unattended blockscript that loops the scheduler across nights.)

v20.45 — Resume + collimation review fixes

An adversarial review of the checkpoint/resume and collimation work found five real issues, all fixed:

hand-authored guiding/arm step placed once before the first target is no longer dropped on resume.

interrupted at sub 20 now re-shoots only the remaining 10, not all 30 (the default editor path was already per-sub; this fixes raw/count=N plans).

precise-point / autofocus now uses the resume target's coordinates, not the first target's.

isn't connected (instead of a job that fails a moment later), and the progress log shows clean messages instead of raw dicts.

v20.44 — Frame acceptance: per-filter thresholds + live tally

Frame acceptance (rejecting bad subs as they're captured) was already live; two refinements:

than broadband, so a single global "min star count" wrongly rejects them. You can now set per-filter overrides as JSON in Settings → Frame acceptance, e.g. {"Ha":{"min_star_count":4,"max_hfr_px":5.0}} — anything not overridden falls back to the global gate.

rejected, and re-shot this run.

(The out-of-process ASCOM driver host — isolating a wedged driver in its own process — is deliberately deferred: it's a large refactor that only pays off if the existing fail-closed watchdog proves insufficient in the field.)

v20.43 — Sequencer checkpoint / resume (survive a crash mid-night)

A crash, power blip, or restart in the middle of a sequence used to lose the whole session. Now a standalone run checkpoints itself at every step, and if it's interrupted you can pick up where it left off:

interrupted run (it shows how far it got, e.g. Resume (42/120)).

because conditions drift during downtime — then continues from the target it was on, skipping the frames already captured (it only re-shoots the interrupted sub onward, not the whole target).

longer matches), and a clean finish clears the checkpoint so the next run starts fresh.

Supervised runs (blockscript / scheduler) are unchanged — they own their own night-level recovery. The normal start path is byte-for-byte identical.

v20.42 — Collimation: the closed loop (calibrate + auto-collimate)

The Collimation page could already measure the error and let you nudge the mirror by hand. Now it can close the loop:

the influence matrix (rejecting an ill-conditioned result), with a live progress log and the condition number reported.

mirror to minimum — gain-damped and travel-limited. It stays disarmed until you set [collimation] auto_collimate_enabled = true (the button is disabled until calibrated, and the loop refuses to run while disarmed).

Both run as cancellable background jobs. As always with collimation, the capture and motor moves are real hardware — watch the first moves at the mirror.

v20.41 — Flat cover for real + Skyhunter staleness nudge

end-of-run action used to be a journaled placeholder; it now drives the CoverCalibrator (OpenCover/CloseCover) for real — and the same fix makes warm-up camera at end real too. There are also new Open flat cover / Close flat cover phase actions you can drop into any sequence phase.

drift over weeks and satellite TLEs over days, so the Skyhunter page now shows a banner when the asteroids/comets catalogs are >30 days old (or the TLEs are >14 days old, or not fetched yet), pointing you at the refresh script to run.

v20.40 — Skyhunter: moving small bodies (asteroids, comets, satellites)

Skyhunter can now find and plan things that move — all computed offline from elements you fetch when online, the same way the planets work.

Pallas, Juno…). Search "Vesta", and they show up in Tonight's Best and the finder chart with their current position and brightness (IAU H,G model). Refresh with scripts/build_skyhunter_asteroids.py (JPL SBDB, or a local MPCORB.DAT for an all-MPC pipeline).

comets from the MPC and keeps the ones worth chasing. A new solver handles the near-parabolic and hyperbolic orbits comets actually have (the planet engine only did ellipses). Predicted comet magnitudes are rough MPC estimates — the detail view says so.

record, or a plain labelled set (a, e, i, node, peri, M, epoch…), for anything not in the bundle. Computed with the same engine.

the ISS (or any bright satellite) crosses your sky, how high, which way ("NW→SE, peak 72°"), how bright, and whether it'll be lit. A real near-Earth SGP4 propagator (validated to nanometres against the standard test vectors), with Sun/Moon transit-candidate flags. Refresh TLEs with scripts/fetch_skyhunter_tle.py. Geostationary/GPS-type orbits are flagged and skipped (they need a different model and don't make fast visible passes).

The astronomy was cross-checked against JPL Horizons (asteroid + three comets spanning all orbit types) and the canonical SGP4 verification vectors; the planet ephemeris was left untouched.

v20.39 — Dome control + live slaving (deployable for hardware test)

The full dome subsystem, so you can put it on a real dome in the next few weeks:

park, slewing, slaved), a big Slave / Unslave button, motion controls (rotate-to-azimuth, find home, park, open/close shutter, abort), and the sync calibration workflow (capture a sync at the current pointing, list per pier side, delete, clear).

HA/Dec/pier-side, runs ASDM, and rotates the dome to keep the slit on the optical axis. Dead-banded (no motor hunting), frozen near the zenith, and any error stops commanding rather than driving to a wrong azimuth.

dome_find_home, and dome_goto (azimuth), so a night can slave on start and unslave/park on end.

(refuses to slave if the mount + GEM offset reach outside the dome).

Still hardware-pending: this is logic-complete and capability-guarded, the ASDM engine is unit-tested (16 tests), but the actual dome motion can only be proven on the real dome. The calibration solver remains a later, gated stage.

v20.38 — Domes: enclosure type + the ASDM slit model (engine)

Starship now knows whether your observatory is a roll-off roof or a dome. Set it in Settings → Observatory → Enclosure type. A roll-off roof needs no azimuth tracking; a dome unlocks ASDM (Adaptive Sync Dome Modelling), which keeps the slit aligned with the telescope's optical axis.

This release builds the ASDM engine (geometry + the adaptive correction layer) as a pure, unit-tested module — deliberately not wired to hardware yet, exactly as the spec demands ("don't slave hardware until the model passes its tests"):

pier-side offset handled correctly and the sign conventions pinned for both hemispheres (the southern-hemisphere azimuth sign is the classic silent failure — it's now a test).

model (per pier side), interpolated without overshoot; where there's no nearby sync it falls back to pure geometry and says so.

offset ⇒ slit azimuth equals pointing azimuth.

The dome parameters live in the Observatory settings. Next stages (not yet built): the operator sync-capture workflow, the calibration solver, and live slaving.

v20.37 — Skyhunter: planets, a finder chart, moon-aware ranking

Four follow-ups to Skyhunter:

they appear in Tonight's Best with their current position and brightness (computed locally, no internet). The astronomy was validated: the internal Sun position matches to 0.4 arcmin, and every planet's distance/magnitude checks out.

bundled catalogs (bright stars + nearby objects + planets) with your camera's FOV box overlaid, N-up / E-left. Framing now works with no internet — exactly what Aladin couldn't guarantee.

a bright, high moon, so a 76%-lit moon pushes nearby targets down and favours the dark side of the sky. It shows the moon's altitude and the adjusted score.

planet's coordinates are a snapshot (they move) if you'll run the plan later.

v20.36 — Skyhunter: offline targets + tonight's visibility

A new module for choosing and planning targets without the internet — because a remote rig shouldn't depend on an online name-resolver or a second planetarium app (Sky Atlas's Aladin just proved that point).

objects** (106 Messier), 242 KB, with type / magnitude / size. Search by name, Messier number, type or constellation.

transit, the observable window (above your altitude floor and in darkness), and moon separation + illumination. All computed locally from your site.

window, so you can see what's well-placed at a glance.

sequence editor; "Frame in Sky Atlas" hands it to the framing view.

Find it in the left nav as Skyhunter. (The astronomy was validated end-to-end: M42's transit altitude, the Sun at solstice, the Moon's motion and phase all check out.) It's renameable — Object Hunter / Plan Commander — in one place.

v20.35 — Sequence shot-row column alignment

The per-shot table's number columns (Exposure / Count / Bin / Gain) had right-aligned headers but their input cells weren't aligned, so in the full-width table the inputs sat at the left of their stretched columns and drifted out from under their labels — the "field/label mismatch." The data was always correct; this is purely the visual alignment. Each value now sits under its header.

v20.34 — Binning fix, sequence recall fix, wizard redesign

Binning. A saved plan's expose step had binning = 10 — the binning box is max=4, but browsers don't enforce that on a typed value, so "10" got saved and the sequencer shot heavily-binned subs (the camera clamped 10→4). Now the binning is clamped to 1–4 both when the form is read and at compile time, the camera is restored to full-frame/bin 1 after an in-sequence refocus, and the m8 m20 plan is corrected to bin 1. Re-run it for full 26 MP subs.

Sequence recall. Recalling a saved plan showed an empty form (targets gone) whenever the plan used guiding — the form rebuilder bailed on the start_guiding / stop_guiding steps. Those are now skipped during reconstruction, so a normal guided plan round-trips and keeps its targets and shots.

Focus Wizard — single adaptive pass. The wizard no longer traces a full coarse V then a separate fine V. It now starts from where you are, racks out, samples inward, and stops a few points past the minimum (no need to climb the whole far arm). If it can't bracket a clean V it halves the step and retries, and only gives up (with a "re-center near focus" message) after a couple of tries. Leave the step blank to reuse what worked last time. Faster, and it keeps a usable spread of samples either side of focus.

v20.33 — Sky Atlas fix (Aladin pinned)

The Aladin sky view loaded from the CDN's /v3/latest/ path, which drifts as Aladin ships new builds — latest no longer matches the last release (3.6.1), and the newer build broke the embed. Now pinned to the stable 3.6.1 release.

Also fixed a no-retry bug: a single (even transient) load failure set a "tried" flag that was never reset, so Sky Atlas stayed permanently disabled until a full page reload. It now retries the next time you open the tab.

> Milestone this session: the first successful unattended sequence — m8 m20 > opened the roof, plate-solved, refocused in-sequence (pos 2362, HFD ~2), guided > at RMS ~0.1, shot 10×60 s, and parked — zero errors. The v20.32 sequencer > refocus and filter no-op both held live.

v20.32 — Sequencer + focus fixes from a live test

Five issues surfaced running a real sequence:

step fired the old run_autofocus() as a detached background job — it walked the focuser ~1000 steps off the new calibration, and because it ran outside the sequence, Stop / Halt-all couldn't reach it. Now the autofocus action and the auto-refocus policy both run the validated run_refocus synchronously and cancelably (a watchdog wires the sequence's Stop to the focus run). Stop now actually aborts an in-progress refocus.

dropdown gains a "— none —" option (a blank filter was silently defaulting to "L" and then failing the run), and a filter step is a harmless no-op when no wheel is connected instead of erroring "filter not found in wheel".

focus <pos> · step <n>, and a new "Go to focus" button drives the focuser straight back to the last saved focus position — handy after anything moves it off.

click captures the mount's live RA/Dec (+ rotator PA) into that target.

v20.31 — Focus Wizard auto-derives its own step + sweep range

The Wizard used to sweep a fixed step (50) for the fine V — too coarse on a sensitive focuser, so the V looked blocky and the sweep ran wider than it needed to. Now the coarse step only finds the V; the fine step and range are derived from the V's slope for your rig.

and an arm moves 2×slope per step). A steeper, more sensitive focuser gets a smaller step automatically; every arm ends up sampled at the same resolution. Floored/ceilinged at the focuser's usable step (8…150).

linear part of each arm — with a small margin, never past the V-curve edge. Points per arm stay in the 4–6 band for a robust fit.

On the rig this matches by itself the step 25 / ±125 / 5-points-per-arm that gave the cleanest, fastest V by hand (vs the old fixed 50 / ±250).

The derived step and range are written into the Refocus recipe (so Refocus inherits the tighter sweep), and refined per-rig across runs — each Wizard run blends its result with the last one so the recipe settles instead of chasing the seeing. The recipe records why it chose what it did (slope, HFD-per-step, how far out the outer sample sits). The sequencer's run_autofocus is unaffected.

v20.30 — HFD measurement fix (the real reason focus was off)

A well-focused frame that Voyager measured at HFD 3.1 read ~11.5 in Starship — the half-flux measurement itself was inflated ~7×, so every V-curve, the "baseline HFR too high" warnings, and per-frame acceptance were all built on a broken number.

Cause: compute_hfr summed flux out to an aperture with a 20px floor. For a compact, well-focused star (~2px), a 20px aperture covers ~1250 pixels, and with only a global sky background subtracted the residual pedestal dominates the half-flux sum — pushing the measured radius out toward the aperture's geometry (~R/√2 ≈ 14) instead of the star.

Fix: aperture floor 20 → 6 px (the adaptive est_radius × 1.6 still grows it for real defocus donuts), plus a local annulus background (median of the 0.7R–R ring) instead of the global frame background. Verified on a real on-rig frame: 399 stars now read HFR 1.68 / HFD 3.37 (was 11.86), matching Voyager's 3.1.

The focus dashboard and ops log now display HFD (= 2× the internal HFR) so the numbers read the same as Voyager. The internal metric, thresholds and saved calibration stay in HFR, so the sequencer's run_autofocus is unaffected.

v20.29 — Autofocus redesign: two buttons (Wizard + Refocus)

Autofocus is now two buttons with one consistent method. Global rules for every focusing action: no slew-to-star, bin 3 + 5 s fixed, full-frame multi-star median HFD, and every sample reached by a final inward move so backlash is always taken up the same way (focuser OUT = increasing on this rig).

Wizard (assumes you start near focus):

  1. Racks OUT by the coarse offset (550), moves IN to clear backlash, then

samples every step (50) inward until HFD drops to a minimum and climbs back up to the starting HFD — a full V.

  1. From that coarse V it derives a tight range (~4–6 samples per arm).
  2. Runs a fine V-curve over that range, moves to best focus, and saves both a

calibration and a Refocus recipe (range / step / focus).

Refocus: a tight, always-inward sweep around the current position using the saved recipe → fits the V → moves to the mathematical minimum and reports "focus point found at position X", with the two convergence lines and the focus position drawn on the graph.

New endpoints POST /api/console/autofocus/{focus-wizard,refocus}. The Console now shows just Wizard… / Refocus (+ Clear cal); the Settings shortcuts were repointed. The old First Light Wizard / Linear sweep / Run-autofocus paths remain in the backend (the sequencer's auto-refocus still uses them) but are no longer shown in the UI. The sweep's capture + focuser loop is written-until-rig.

v20.28 — Linear sweep autofocus (full-frame, multi-star)

A second autofocus mode alongside the powers-of-2 First Light Wizard, built to replace a real on-rig failure: the wizard picked a single faint anchor star and its auto-exposure ramped to 30 s. The linear sweep is simpler and more robust on a wide field:

star** — no single anchor to land on, no slew to a bright star needed.

settings (now full-frame, bin 3, 5 s by default).

moves OUT by the start offset (default 200), then steps IN by the step (default 50), sampling until the V is bracketed (HFD drops then rises). It fits the V with the same fitter as the wizard, moves to best focus, and saves a calibration in the same file — so a linear-derived calibration and a wizard-derived one are interchangeable.

Run it from Console → "Linear sweep…" (a modal with start-offset / step inputs and the live V-curve plot), or the Linear sweep shortcut in Settings → Autofocus. New endpoint POST /api/console/autofocus/linear. The sweep's capture + focuser loop is written-until-rig.

v20.27 — Site / Location settings tab

The observatory's coordinates were tucked away as a sub-group inside the Mount tab — easy to miss, and leaving them at 0,0 silently breaks the focus-star slew (that's what stopped "slew to a suitable star" on the rig), AIRMASS headers, the scheduler's observability check, and the sky charts. They now have their own discoverable tab: Settings → Site / Location. Set latitude / longitude / elevation by hand, or Get from mount to pull SiteLatitude / SiteLongitude / SiteElevation from a connected mount. The Mount tab points you there.

v20.26 — collimation reset, made safe

The collimation motors already store their position (persisted to disk after every move, reloaded on connect, so it survives a restart) and Reset to start already returns them to the marked origin. Two safety guards added for confident on-rig testing (the hardware-validated GRBL motor code itself is untouched):

origin mid-session and lose your known-good reference.

steps) before it runs, and reports what it moved afterwards.

v20.25 — running version shown at the top

The build Starship is running now appears at the top of the app, next to the brand (e.g. v20.25). It's fetched from the server (GET /api/version), so it reflects the actual running build — and if the page you're looking at is stale relative to the server, the badge turns amber and tells you to hard-refresh.

v20.24 — the AstroWorx client experience (skin + guts, slice 1)

Your AstroWorx Voyager-broker client dashboard, ported onto Starship — the same branded experience, now powered by Starship and gated by your roles + bookings:

Telescope · Operator Admin.

astro theme): sign-in, role badge, live booking countdown, SAFE/UNSAFE banner + weather chips, live frame (latest capture + fullscreen viewer + exposure progress), Camera/Mount/Focus/Guiding sparkline, point→slew, capture, cooler, focuser, track/park, Sequence runner, Observatory roof, AllSky + CCTV feeds, a Tonight altitude + moon graph, and a Captures gallery — every control shown only if your role allows it, and enforced on the server.

photos.

Next slice: self-signup, a booking calendar with busy-view + self-cancel + auto-approve, per-session photo tagging, Framing/Aladin FOV, and the mini sequence editor.

v20.23 — "Preview client dashboard" button

Settings → Web server now has a Preview client dashboard button that opens the /operate page (your remote client/student console) in a new tab, plus a Copy link button for the URL to point your Cloudflare tunnel at.

v20.22 — Equipment tiles show configured devices (even when offline)

The Devices grid said "no devices configured" whenever nothing was connected — because it only listened to live device events. It now also reads the configured slots the server already knows about, so every assigned device gets a tile (shown offline with its ProgID) and fills with live data the moment it connects. Each tile has its own Connect / Disconnect button. So your mount, camera, focuser, guide cam, roof and power show up as tiles right away — click Connect (or Connect all) and they go live.

v20.21 — richer Equipment tiles (trends, exposure, sky position)

Three additions to the device tiles:

(from graded frames) on the camera tile, and guiding RMS on the guiding card. They build up live from the WebSocket feed.

progress bar with the exposure message and percentage.

right, zenith at centre, horizon at the rim) showing where the scope is pointing.

Presentation only — reload to see it.

v20.20 — the student / client /operate dashboard

A separate, hardened page for renters and schools to operate the rig over your Cloudflare tunnel — dark-theme and touch-friendly. Open it at /operate.

booking countdown ("session ends in 42m 10s"), and Log out.

subsystems, activity — over the live feed.

name → Resolve → Slew) and a Capture panel for those allowed, plus per-device tile actions (track, cool/warm, focus, filter, rotate). A control only appears if your role grants the capability — and it's still enforced on the server, so hiding a button is never the only thing stopping it. Viewers get a read-only dashboard.

Set up types + users in Admin → Users & Access, enable [access], and point clients at /operate.

v20.19 — Equipment dashboard: device control tiles

Following the Voyager dashboard model, every device is now a proper tile with status and actions, not just a readout:

(mount altitude, camera sensor temp, focuser steps, current filter, rotator PA), live values and status chips, and

Park / Unpark / Abort; camera Cool (type a target °C) / Off / Warm; focuser Go / −100 / +100 / Halt; a filter dropdown; rotator Rotate / Halt; observatory Open / Close roof; and Connect all / Disconnect all**.

Every action is enforced server-side by capability (a role without the permission gets "not allowed"), and the live re-render won't wipe a value you're mid-typing.

v20.18 — the Equipment page is now a live status dashboard

The Equipment section was a thin read-only ASCOM probe; it's now a "mission control" view of the whole rig, updating live. No new backend — it presents the telemetry already streaming over the WebSocket:

brightness, with per-sensor safe/warn/unsafe chips.

scheduler's current target, a sequence progress bar, the active job.

parked, camera cooler temp→setpoint + a power bar + gain, focuser position·temp· moving, current filter, rotator angle.

Opening the page hydrates from the REST snapshots; after that it's driven live by the WS feed. CSS/JS only — reload to see it.

v20.17 — configurable user types + the Users & Access page

You can now declare your own user types and tick exactly what access each one gets. Roles became editable records instead of hardcoded sets:

operator / student / viewer) are seeded and editable, and you can add custom types with their own capability mix and a "needs a booking to operate" flag. Capabilities are the fixed, server-enforced gates — a type can only bundle existing powers, never invent new ones. The owner type stays a locked superuser so you can never strip your own access.

deactivate, reset passwords.

localhost first, then flip [access] on for the tunnel.

Security review fixes (an independent adversarial pass — the core authorization came back clean): capability grants are now clamped so an admin can't hand out a power it doesn't hold; an error reading the access flag now fails closed; the last owner can't be demoted/deactivated/deleted; brute-force lockout keys on the real connection (not a spoofable header); and the role registry/build paths were hardened. 12 new tests.

v20.16 — access control spine (multi-user / rent-the-rig, default OFF)

The foundation for renting imaging time and giving schools/test-drive access over a Cloudflare tunnel. Default off — nothing changes for the single-operator localhost setup until you enable [access].

(test-drive) / viewer, each mapped to a capability set. Clients and students never get settings, device assignment, the roof, parking, or the safety supervisor — those stay with your supervising blockscript.

cookies (HttpOnly, SameSite, optional Secure). Brute-force lockout throttles failed logins. Sessions live in memory and clear on restart.

approved booking window; view-only and self-service booking requests are allowed any time. Owners/admins approve, deny, and schedule.

route→capability table. Any endpoint not explicitly opened to clients defaults to an owner/admin capability, so a new or forgotten route is locked by construction. The UI hiding controls is only cosmetic; this is the real gate.

/api/access/{catalog,users,bookings,audit,sessions,…}. Enabling seeds an owner from your [web] credential so you can never lock yourself out.

Still to come (next slices): the hardened /operate client page (the live status dashboard, restricted to what the role allows), the admin Users & Access UI + booking calendar, and an independent security review.

v20.15 — finished the buttons too (defined .btn)

Your screenshots showed the inputs were fixed (v20.14) but lots of buttons were still white — Start, Stop, Resolve, Add project, remove plan, "+ Add action", the scheduler's Ask Pete. Root cause: the .btn class was never actually defined, so every class="btn" button without its own inline background fell back to the browser's white default. Defined it — a dark secondary-button look with hover / active / disabled states. Buttons that set their own background (the blue "Save & restart", the accent primaries) are unchanged. With v20.14, the text fields and buttons are now consistently themed across the whole app. CSS-only — reload to see it.

v20.14 — finished the text fields (global field theming)

Some text inputs across the app were rendering with the browser's default white background — unfinished against the dark theme. The codebase had only patched two modules by hand (Response engine, Schedule). Replaced that whack-a-mole with one global rule: every text-like input / textarea / select (text, number, password, search, email, url, tel, time, date, datetime-local, month, and untyped) now gets the dark field look by default, plus a consistent accent focus ring. No control ever falls back to white again, and any input added in future is themed automatically. Inline + per-module styles still win where they're set, so nothing that was already styled changes. CSS-only — reload to see it.

v20.13 — "Ask Pete" in the editors

The pre-flight checker is now a button, not just an API. An "Ask Pete" button sits in the sequence, blockscript, and scheduler editors; clicking it dry-runs the current config (the sequence you're editing, the selected blockscript even unsaved, your live project list) and pops a report modal — Pete's avatar, a verdict pill (errors / warnings / all clear), the findings with ✓ / ⚠ / ✗ icons and where each one is, and his one-line take ("I wouldn't run this yet — 3 things are broken" / "Looks clean, I'd hit start"). Reload to see it.

v20.12 — "Ask Pete" pre-flight checker (slice 1)

Point Pete at a sequence, blockscript, or scheduler config and he tells you what would break — before the night does. Two layers:

caught: a filter that isn't in your wheel, a slew with no coordinates or target, a run_sequence naming a plan that doesn't exist, a blockscript goto to a state that isn't there, an unknown action type, a retry_connect for a device you don't have.

estimate + value sanity; the scheduler is scanned against the sky to flag a target that never clears your horizon** tonight (it would sit idle forever); a blockscript is checked for goto self-loops.

problem — no tool can. Pete instead flags the structural mistakes (a self-loop, a goal that can never complete) and is upfront that he can't certify termination.

returns Pete's report (findings + a friendly summary). 14 new tests (517 total).

reachability, and scheduler nights-to-complete.

v20.11 — the simulation gauntlet ("Pete's week")

Verification infrastructure (no runtime change) — a repeatable, deterministic way to exercise the whole unattended stack without hardware, narrated through the operator persona "Pete":

night the REAL blockscript action runner opens up (unpark → open roof → cool), the REAL scheduler dispatches targets onto the REAL sequencer run loop, the REAL engine handlers react to trouble, and the runner shuts down (warm → park → close roof). Across the week it drives + asserts recovery from a dropped sub (sequencer retry), a focuser USB drop (retry_connect reconnects), a clouded-out night (never opens), mid-night cloud (close + reopen), and a mount lost past the retry cap (engine escalates to EMERGENCY → HALTED and the rig is parked + shut). The invariant it guarantees: roof closed + mount parked every dawn, and the goals complete across the week despite the lost nights.

to print the night narrative.

v20.10 — scheduler ↔ blockscript integration + night simulation

The dispatch scheduler (target scoring, chunked dispatch, frame ledger — already built) is now joined to the blockscript supervisor, and a full night is simulated end-to-end.

the dispatcher runs each chunk as an injected run, so the blockscript owns the observatory (roof/mount/safety) and the scheduler just chooses + images targets.

every injected run — which would have shut the blockscript down after the first scheduler chunk. Separated injected (skip observatory actions) from signals_body_done (RUNNING→SHUTDOWN): only the run_sequence action's own body signals done; scheduler chunks don't, so a many-target night runs to completion.

REAL scoring, REAL dispatcher chunk, REAL sequencer run loop (on a simulated rig), and REAL ledger, plus a REAL blockscript engine. It demonstrates: the scheduler picking M81/M51 by score, a transient camera glitch caught + recovered by the sequencer's retry handler, goals completing in the ledger, and the blockscript handling a device-loss (WARNING + retry) and a weather UNSAFE→resume. Run it with pytest tests/test_night_simulation.py -s to read the night narrative.

v20.9 — sequence + blockscript test-readiness

Wired the three gaps (found by an end-to-end audit + adversarial review) that would have silently broken a supervised test. The blockscript engine logic was already built and tested — only the connections were missing.

which launches a sequence as an injected (supervised) run — the sequencer skips observatory-domain phase actions (roof/mount/park) so it doesn't fight the blockscript that owns them. The blockscript run_sequence action used to silently no-op (it returned OK but started nothing); now it really runs the plan.

equipment.device events into on_device_lost / on_device_recovered, so a device dropping mid-night triggers the engine's WARNING → retry → EMERGENCY path. A loss (unexpected drop with an error) escalates; a deliberate disconnect (no error) is ignored; healthy polls are de-duplicated so they don't spam recovery.

signals body_done, moving the blockscript RUNNING → SHUTDOWN — so the night packs up (per your shutdown actions) when imaging finishes. Stand-alone runs are unaffected (they never touch a blockscript).

action in tomorrow's plan — it's still a journaled placeholder (real CoverCalibrator dispatch is a separate item). Hardware paths are WRITTEN-until-rig.

v20.8 — Auto Flat acquisition

Automatic flat-frame capture, modeled on Voyager's Auto Flat. It has its own Operations → Auto Flat panel (plan summary + Run/Cancel + live progress); the plan and settings are edited in Settings → Flat device. Also POST /api/console/flat/auto. It runs as a background job.

a test frame, scales the exposure linearly toward the target mean ADU (flats are linear above the bias), and once it's within max_err_pct saves count flats. Bounded by min/max exposure and a metering-iteration cap; if the panel can't reach target within bounds it saves the best effort and flags it.

on (per-filter brightness) for metering + capture and off at the end; manual mode leaves the light to you. Optional park + cover-close on completion.

bounds, target_adu, brightness}`; blank fields fall back to the global defaults. Editable as JSON in the Flat settings tab.

now tags every frame LIGHT/FLAT/DARK), so stacking software classifies them right.

metering, exposure-scaling and per-filter loop are validated against a simulator. Sky dawn/dusk flats are structured in the config but not yet driven.

v20.7 — Solo driver-death heartbeat (closes the v20.5 known limit)

follow-up flagged in v20.5. The safety supervisor is event-driven, so the weather-loss → EMERGENCY timer only advances while the Solo keeps publishing solo.reading / solo.error. In the normal comms-loss case the driver keeps erroring and publishing, so the timer runs — but if the poll thread itself dies silently, no events fire, the supervisor stops ticking, and the timer freezes, so a dead driver would never escalate. The health monitor now taps the raw Solo event stream directly and, on silence past solo_heartbeat_poll_multiple × the Solo poll interval (default 3×, floored at solo_heartbeat_min_age_s), drives the same force_emergency safe-state — the event-stream twin of the wedged-ASCOM-worker detector. It arms only after at least one Solo event has been seen (a never-started driver is a startup fault, caught by the supervisor heartbeat), and stands down if the supervisor's own loss escalation already fired, so the two never double-fire. Default on (solo_heartbeat_enabled = true): it never fires spuriously — only on multi-cycle silence — and closing the gap is fail-closed.

force_emergency escalation path; no new hardware-touching code.

v20.6 — PHD2 auto-start + per-filter focuser offsets

Two more from the Voyager gap roadmap, both gated default-off:

connect_equipment) — when on, Starship launches PHD2 if it isn't already running, selects the equipment profile (by name or id), and connects its equipment, so an unattended rig needs no human to start the guider. It's self-healing — a crashed PHD2 is relaunched on the next connect retry (a reachability check prevents a second instance). Recalibrate-on-flip is deliberately left to PHD2, which already flips the calibration on a pier-side change. Off (default) = connect to a PHD2 you started yourself; Starship never launches or reconfigures it.

focuser steps relative to a reference filter. On a filter change the focuser shifts by the chromatic delta instead of running a full autofocus; if a filter isn't in the map it falls back to the existing refocus-on-filter-change. The Navigator already computes suggested values from your real focus runs — copy them into the map (editable as JSON in Settings → Autofocus).

v20.5 — closing the loop on an unattended night

The three highest-value gaps vs Voyager for a remote rig, from the gap analysis — all gated default-off (a run at defaults is byte-identical to v20.4) and adversarially reviewed before shipping:

it is captured (HFR / star-count / eccentricity / background, via the same metrics engine autofocus uses) and rejects bad frames (cloud, dew, wind, satellites). A reject is journaled and moved to a rejected/ subfolder so the stack stays clean; it can be re-shot, and abort_after_consecutive stops a clouded-out run. Fails open — a grading glitch never drops a frame.

from a lost or high-RMS guide star, abort-aware, with a recover-timeout that fails the step into your on-error policy. Pairs with frame acceptance (which catches a sub ruined mid-exposure).

silence/loss for this long latches EMERGENCY (park + close, manual reset) instead of flapping the roof under unknown skies. Fires once, resets when contact returns, and only on loss — reported rain/cloud still auto-recovers on dwell. Known limit: the timer relies on the Solo driver still publishing (a driver-death heartbeat is a tracked follow-up — closed in v20.7).

WRITTEN-until-rig; the control flow is validated.

v20.4 — autofocus watchdog + adaptive exposure

The two deferred autofocus pieces, both gated default-off so a run at default settings is byte-identical to before:

after this many consecutive frames detect no stars (clouds rolled in, the star drifted out of the ROI) instead of grinding through the whole sweep and then failing at the fit. 0 = off (default). The sweep keeps its existing coarse-approach retry and R²-fit gate; this just stops a hopeless run sooner.

detects no stars, retry it at a longer exposure (doubling from the AF exposure, floored at Min, up to Max) before giving up — recovers a faint star at large defocus. Off (default) = always one exposure. Min/Max exposure are now live in Settings.

default of 5 but was never actually read until now), so nothing changes on the rig unless you opt in.

aborts at the Nth frame) and a faint-star-at-defocus (exposure ramps up to find it). 436 tests total. The on-sky capture path stays WRITTEN-until-rig.

v20.3 — pending settings wired

refused), the same as the focuser limits.

plate_solve_binning_override are applied to frames captured only to plate-solve (precise pointing, polar, rotate-to-PA); science frames are unaffected.

N frames / minutes, or on a filter change, plus a post-focus offset. Opt-in via refocus_enabled (default off, so nothing changes unless you turn it on); the temp trigger pairs with the Navigator's learned temperature coefficient.

autofocus hardware ROI (af_hw_roi_pct) are now editable in Settings (the old redundant pixel-width / legacy ramp fields are retired).

v20.2 — camera FITS headers

[site] coordinates and the time, and written into every capture's FITS header — stacking software weights frames by it.

FITS** toggle that strips your location for sharing frames publicly (AIRMASS stays).

(both Alpaca and ASCOM capture paths) instead of a hardcoded 60 s.

honestly pending (they need the dedicated fresh-solve capture worker).

v20.1 — Sky Atlas, plate-solve robustness, focuser prefs

planning (rows × cols × overlap × rotation), and one-click sequence generation**.

configurable [platesolve] downsample, and pointing exposure raised 4 → 6 s (from a real ASTAP-failure log).

IN/OUT), position-check tolerance, post-move settle, don't-halt-on-HALT-ALL — all isolated from autofocus, applied only to manual/sequence moves.

First Light Wizard to auto-measure + persist backlash).

written (docs/competitor_pain_points.md) as the reliability roadmap.

v20 — reliability, auto-collimation, Navigator, scripts & plugins

The first major milestone after the v19 build line (v19.74–v19.82), consolidated into one release. The reliability + features push:

could expose on the wrong side of the pier); the flip now runs on_error, marks the run FAILED, and stops. Plus 14 more sequencer / blockscript / scheduler fixes (blockscript emergency-recursion guard, force-override abort, scheduler frame counting, plan kind validation, guarded DB writes).

over GRBL steppers: influence-matrix calibration, gain-damped correction with project-out-piston, per-move + cumulative-travel clamps, divergence abort, and an arm flag. Disarmed by default.

(bright stars saturating at focus no longer invert the multi-star median).

non-dismissible hard floor), disk fill-rate forecast, CPU load/temp check, and a wedged-ASCOM-worker detector.

collimation telemetry, builds per-rig models (focus temperature coefficient, filter offsets, bad-run detector, collimation drift, guiding baseline), and can email you a digest of suggestions / progress / ideas. Opt-in, default off.

in a sequence (phase action OR body step) or a blockscript state action, through one sandboxed executor (allowed-dir, no shell, hard timeout, abort-aware). Opt-in via [scripts].

at startup. Failures are isolated. Opt-in via [plugins].

status; polar alignment now forces tracking ON before the measurement sweep.

Health monitor, External scripts & plugins.

v18 — help docs audit

v17 — autofocus

v16 — in-app help

v15 — camera cooling

v14 — Voyager-style policy tabs

v13 — structured editor

v12 — sequencer foundation

v11 — visual identity

Earlier

Astroworx Starship v22.2.10 · this page is the in-app help of that buildDownload · HTTP API