Skip to content

Validate equipment and GPS form input at the API, not just the browser - #571

Open
brickbots wants to merge 1 commit into
mainfrom
fix/equipment-input-validation
Open

Validate equipment and GPS form input at the API, not just the browser#571
brickbots wants to merge 1 commit into
mainfrom
fix/equipment-input-validation

Conversation

@brickbots

Copy link
Copy Markdown
Owner

Closes #569. Also fixes the GPS-form 500 from the same 2.6.1 Gate 6 pass, and the config-load crash from #291.

The two defects

The equipment forms failed silently. Both handlers parsed with bare float()/int() inside except Exception: logger.error(...) and then fell through to the success template:

POST /equipment/add_eyepiece/-1  focal_length_mm=7,5  field_stop=8,0
  -> HTTP 200 + "Eyepiece added, restart your PiFinder to use"
  -> eyepiece count unchanged (3 -> 3).  Nothing was saved.

/gps/update had no try/except at all, so the same input reached the user as an unhandled 500. #536 fixed this class for /locations; its normalizeDecimal helper appears zero times in views/gps.html.

A decimal comma is the easiest trigger — PiFinder ships de/es/fr/zh — but anything unreadable did it, including the blank instrument name in #569's description.

The rules

Field rules live in one table in equipment.py. The edit forms render them into their client-side check and the API re-checks them before anything reaches config; the ranges are shared so the two can't drift. Tabulated in docs/ax/equipment/CONTEXT.md, with the reasoning in ADR 0027.

Record Field Required Range
Telescope make no ≤ 64 chars
name yes ≤ 64 chars
aperture_mm yes 1 – 2000
focal_length_mm yes 1 – 20000
obstruction_perc no (0) 0 – 100
mount_type yes alt/az | equatorial
Eyepiece make no ≤ 64 chars
name yes ≤ 64 chars
focal_length_mm yes 0.1 – 100
afov yes 1 – 180
field_stop no (0) 0 – 100

Two rules that aren't ranges: a blank field is not a zero (only obstruction_perc and field_stop have a documented zero meaning), and a rejected entry never reports success — the form comes back with the message and the values that were typed.

focal_length_mm: int on Telescope, float on Eyepiece

Settled as part of the issue: all measurements are floats. Optics are fractional — an 11" SCT is 279.4mm, a reducer turns 2032mm into 1280.2mm. That also closes #291, where an aperture of "279.5" written into config made the PiFinder unbootable: Equipment.from_dict raised invalid literal for int() inside main() before the UI came up. Whole millimetres still display as 1000, not 1000.0, through a format_measurement Jinja filter, and no migration is needed — the stored values are already numbers the float fields read.

What changed

  • equipment.py — measurements are floats; TELESCOPE_LIMITS/EYEPIECE_LIMITS/MOUNT_TYPES/NAME_MAX_LENGTH and format_measurement() beside the dataclasses.
  • server.pyparse_measurement / parse_name / eyepiece_from_form / telescope_from_form; both add_* handlers re-render the edit form on ValueError; every <id> route range-checks its index instead of raising IndexError as a 500; the DeepskyLog import re-checks the same limits and skips (and counts) records it can't read.
  • gps_update() — parses everything before locking anything, so a bad clock entry can't half-apply a position; re-renders gps.html with the error.
  • Templates — client-side validation shared by both equipment forms (equipment_validation.html), numeric inputs switched to type=text inputmode=decimal per fix(web): accept comma or period decimal separator when saving locations #536, normalizeDecimal ported into gps.html, and the two decimal→DMS bypasses at locations.html:282,288 fixed.
  • config.py — an undecodable equipment section logs and falls back to the shipped defaults rather than aborting the boot.

Two incidental fixes in files this touches: the eyepiece dedup searched telescopes for an Eyepiece, and the GPS altitude field's input listener rewrote the longitude DMS fields.

Testing

68 new tests at the request level — a Flask test client POSTing 7,5 runs in CI, unlike the web suite, which runs en-US and structurally cannot catch this.

  • pytest -m "smoke or unit"1227 passed (baseline 1159 at 4a83d25b)
  • Selenium test_web_equipment.py + test_web_locations.py against a live PiFinder → 14 passed, 1 skipped
  • Re-ran the original repro live: the comma eyepiece now saves 7.5/8.0, garbage re-renders the form with "must be a number", /gps/update with 34,22 returns 302 (was 500), and the out-of-range and bad-index cases return 200 with a message
  • Client-side rules exercised directly; the inline JS on all four touched pages parses clean

i18n

18 new msgids added to de/es/fr/zh, tagged AI-TRANSLATED (claude): needs human review, .mo recompiled. The .po diffs are additive only (no pybabel update churn — messages.pot is gitignored here, so the .po diff is the only record).

Not included

/gps is kept and fixed rather than retired — it's the only place with the date/time control. G6.5 (the Volume menu-index drift) and the web suite's missing starting-language guard are deliberately left out; both are test-only and written up in the 2.6.1 test plan.

🤖 Generated with Claude Code

…the browser

The equipment handlers parsed with bare float()/int() inside
`except Exception: logger.error(...)` and then rendered the success
template regardless, so an unreadable value reported "Eyepiece added"
and saved nothing:

    POST /equipment/add_eyepiece/-1  focal_length_mm=7,5
      -> HTTP 200 + "Eyepiece added, restart your PiFinder to use"
      -> eyepiece count unchanged

/gps/update had no try/except at all, so the same input reached the user
as an unhandled 500. A decimal comma is the easiest trigger — PiFinder
ships de/es/fr/zh — but any unparseable value did it, including the
blank instrument name from #569.

Field rules now live in one table in equipment.py: the edit forms render
them into their client-side check and the API re-checks them before
anything reaches config. Measurements are floats throughout, so a 279.4mm
aperture is enterable and a config carrying one is loadable (#291); whole
millimetres still display as "1000", not "1000.0", via format_measurement.

- equipment: measurements are validated floats; limits + name length live
  beside the dataclasses (ADR 0027)
- server: parse_measurement/parse_name/*_from_form; failures re-render the
  edit form with the message and the values that were typed; route indexes
  are range-checked instead of raising IndexError as a 500; the DeepskyLog
  import skips records it can't read rather than writing them through
- gps: parse everything before locking anything, so a bad clock entry
  can't half-apply a position; gps.html gets #536's normalizeDecimal,
  which never reached it, and locations.html's two decimal->DMS bypasses
  are fixed
- config: an undecodable equipment section logs and falls back to the
  defaults instead of aborting main() before the UI comes up (#291)

Covered at the request level (the Selenium suite runs en-US and
structurally can't catch a decimal-comma bug): 68 new tests, and the
equipment + locations web suites still pass against a live PiFinder.

Fixes #569

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant