add md describing webpage design

This commit is contained in:
x01dc
2026-07-13 16:36:29 +02:00
parent 129ad05119
commit 2a8959b3f7
@@ -0,0 +1,239 @@
# flOMNI / cSAXS status monitoring — development handoff
Consolidated context for continuing development in Claude Code. Supersedes nothing;
read alongside `project_briefing_update5.md` and `multi_setup_webpage_architecture.md`
for the deepest detail on the last session.
---
## 1. What this project is
A modern Python-based experiment status monitoring system for the **flOMNI**
ptychographic tomography instrument on the **cSAXS beamline (X12SA)** at the
**Swiss Light Source (SLS), PSI**. It replaces a legacy bash/spec-based approach.
Purpose: give colleagues a live view of experiment status while monitoring
remotely, via a public-facing authenticated web page with audio alerting.
Owner: Mirko (beamline scientist/engineer, PSI).
### Repos
- `gitea.psi.ch/OMNY/BEC` — Python / BEC side
- `gitea.psi.ch/OMNY/Webserver` — web server files
---
## 2. Architecture
```
┌──────────────────────────────┐ ┌────────────────────────────┐
│ PSI internal network (trusted)│ HTTPS │ omny.online (IONOS) │
│ │ POST │ lighttpd + PHP │
│ BEC iPython session ├────────►│ - auth_gate.php / login.php│
│ flomni_webpage_generator.py │ │ - status.html / status.json│
│ (polls BEC every ~15s) │ │ - IP-whitelisted upload │
└──────────────────────────────┘ └────────────────────────────┘
fallback: Raspberry Pi "tamipi"
(FritzBox DynDNS)
```
### Producer — `flomni_webpage_generator.py`
- Background service inside a **BEC iPython session**.
- Polls BEC client objects every **~15 s**, generates `status.html` + `status.json`,
uploads via **HTTP POST** to `omny.online`.
- Plugin architecture:
- `WebpageGeneratorBase``FlomniWebpageGenerator` (and stub `LamniWebpageGenerator`)
- Factory `make_webpage_generator()` selects the plugin by BEC session name.
- **`stop()` / `start()` cycle is required after any edit** to regenerate and
re-upload `status.html`.
### Web server
- **lighttpd** on IONOS Webhosting Plus (`omny.online`); Raspberry Pi 3B
("tamipi") behind FritzBox DynDNS as fallback.
- **PHP auth**: HMAC-SHA256 signed cookies. Two htpasswd files:
- `users.htpasswd` — permanent beamline staff
- `session.htpasswd` — the currently active rotating e-account (e.g. `p23092`)
- **Upload endpoint** IP-whitelisted to the PSI subnet `129.129.122.0/24`.
- **SSL**: Let's Encrypt via **webroot** authenticator. (Standalone previously
caused *silent* renewal failures — do not go back to it.)
- **Cookie generation-binding**: a monotonic `session.gen` counter invalidates old
session cookies after an e-account rotation. Fixed staff users get sentinel
`-1`, exempting them from the generation check.
### Monitoring
- Raspberry Pi crontab `*/5 * * * *` runs `omny_upload_monitor.sh`, checking
`status.json` age against a 300 s threshold.
---
## 3. BEC internals (hard-won, do not re-derive)
- **Scan activity**: `primary.info[0].active_request_block is not None`.
Do **not** use the queue `status` field — it is always `'RUNNING'` while BEC is alive.
- **Beamline states and scan interlock are independent.**
`bec.builtin_actors.scan_interlock.states_watched` returns **accepted** (not blocking) statuses.
- **`blocked` status** uses `primary.locks`.
- **A block interrupts the running scan**, clearing `active_request_block`
(→ `queue_has_active_scan = False`). So `_derive_status()` must return `blocked` on
`beamline_blocking OR queue_locks` **independent of active scan**; only a
fresh-heartbeat `scanning` outranks it.
- `beamline_blocking = (enabled is True) AND any(state mismatched)` — gated on
`enabled` so a disabled/unknown interlock never raises BLOCKED.
- **Tomo projection count**:
- type 1: `floor(range / stepsize) * 8`**truncation, not rounding**; 8 sub-tomograms.
- types 2/3: `golden_max_number_of_projections`.
- Now generalised in JS as `calcProjections()` with kinds `equally_spaced_grid`
and `golden_capped`, driven by the `TOMO_TYPES` data blob.
---
## 4. Multi-setup design (current)
Capability flags live as **class attributes** on `WebpageGeneratorBase`
(not constructor args — so startup code is unchanged):
- `HAS_TOMO_QUEUE: bool` — gates the `tomo_queue` global-var read in `_cycle()`
and the "Tomo queue" card in `_render_html()`.
- `TOMO_TYPES: dict` — declarative config, embedded as JSON into the page; drives
`calcProjections()`.
Defaults reproduce flomni behavior exactly. BEC's own primary scan queue
(`queue_status` / `queue_locks`) and beamline states are **common — not gated**.
**Principle**: setup-specific behavior = class-level capability flag / declarative
config with a flomni-preserving default. Never a hardcoded per-setup branch in JS.
**Principle**: when one concept appears in two places (e.g. "blocking" on the pill
*and* the card; params in the queue *and* the instrument pane), derive both from a
single identically-defined signal or shared helper (`buildParamRows()`) so they
cannot drift.
---
## 5. Current sound map (flomni)
| Transition | Sound |
|---|---|
| `scanning → idle` (normal finish) | rising success arpeggio (C5-E5-G5), repeats every 30 s until Confirm |
| `scanning → running` / other non-blocked stop | falling `warningChime`, 30 s until Confirm |
| `scanning → blocked` | **silent**; Blocked LED pulses; `blockedChime` only after 60 min, then hourly, auto-clears |
| feed stale / 3 failed fetches | high triple pip, 30 s until Confirm feed |
| Enable/test button | 880-1100-880 |
---
## 6. Known-good implementation gotchas
- **iOS Web Audio**: must use a **synchronous** silent 1-sample `BufferSource`
started directly in the gesture handler, plus fire-and-forget `ctx.resume()` with
~80 ms `setTimeout` offsets. Promise `.then()` chains **fail** outside WebKit's
gesture scope.
- **f-string escaping**: all JS braces must be doubled (`{{`, `}}`) in Python
f-string HTML generation.
- **lighttpd**: negative-lookahead regex for pass-through rules is unreliable — use
explicit pass-through entries. `%0` in redirect rules evaluates empty if the
parent regex didn't match — use a hardcoded hostname + `$0`.
- **`handle_error` placement**: belongs on the **server class** (`BaseServer`
subclass), not on the request handler — overriding it on the handler has no effect
for this error path.
- **Mobile viewport**: `body { min-height: 100dvh }` (with `100vh` fallback); plain
`100vh` on iOS leaves empty scroll room below the page.
- **File discipline**: editing stale base files has been a recurring, costly error.
**Always verify feature presence with `grep -c` before editing any file.**
---
## 7. Security model
- PSI internal network is **trusted**. The primary threat is the **inbound path**
from the public web server back into the internal network.
- Key risks and mitigations:
- HTTP redirect-following in `requests.post()` → SSRF. Mitigated with
`allow_redirects=False`.
- DNS rebinding on the DynDNS hostname when `verify=False`.
- Unbounded response-body reading.
- `session_query.php` / `set_password.php` form the **one** feedback loop where the
trusted side acts on public-server responses — treat with care.
- **Principle**: implement the safest correct solution, not a minimal patch.
---
## 8. Open items
### Verify on next flomni restart
- **Blocked fix**: on the next real block, confirm the pill flips to BLOCKED and
the LED pulses (this was the reported bug).
- **`_CURRENT_PARAM_KEYS`**: 7 names are proven (`fovx`, `fovy`, `stitch_x`,
`stitch_y`, `tomo_shellstep`, `tomo_countingtime`, `tomo_angle_stepsize`).
5 are **assumptions** to verify as global vars — `tomo_type`, `tomo_angle_range`,
`frames_per_trigger`, `single_point_instead_of_fermat_scan`,
`ptycho_reconstruct_foldername`. Any that isn't a real global var is silently
skipped (no row); remap in `_CURRENT_PARAM_KEYS` if a name differs.
- **Projections parity**: the queue-card Projections number for the next job should
be identical to before (`floor(range/step) * n_subtomos`).
- **Let's Encrypt auto-renewal**: confirm it is functioning (~90 days after the last
manual renewal).
### Producer-side, not yet implemented (from briefing 4)
- `estimated_finish_time` in `flomni.py`
- `tomo_start_scan_number` in `flomni.py`
- `set_web_password()` shortcut on the `Flomni` class in `flomni.py`
### EPS / machine status page (in progress)
- **Starting point**: `X_X12SA_BLStatus.ui` — flat, macro-free caQtDM beamline status
panel covering undulator gap, EPS permit, vacuum, temperatures, monochromator /
mirror / slit positions, BPMs, filter states.
- **Decided architecture**:
- **Separate daemon**, decoupled from the main webpage generator.
- **CA monitors preferred over polling.**
- **Two-tier update cadence**: fast (shutters, EPS-OK, ring current) / slow
(vacuum, temperature).
- **Card/table layout first**; SVG synoptic deferred.
- **Blocked on**: ~110 unresolved PVs from `caInclude` sub-templates
(`EPS_Temp.ui`, `EPS_Gauge.ui`, `EPS_IonPump.ui`, `EPS_Valve.ui`, `EPS_BST.ui`, …)
— need the template files.
- **Next deliverables requested**: `index.html`, the JS, and the JSON schema files,
so the PV manifest and `eps_status.json` schema can be built.
- **Consider**: the PSI **EPICS archiver REST interface** to sidestep CA firewall
concerns.
### Deferred
- **LamNI**: fill in `LamniWebpageGenerator` — real `tomo_type` key values in
`TOMO_TYPES`, device paths / `_collect_setup_data()`, `LamNI.png`. Add a
session-name branch to `make_webpage_generator()`.
- **Omny**: new subclass; `TOMO_TYPES` with a public 2-subtomo type plus a hidden
8-subtomo type restricted to staff. The staff-only visibility mechanism is
**still to be designed** (likely a role/visibility field filtered out before the
config reaches a standard user's page). Add to the factory.
- **SVG synoptic view** for the EPS page.
- **Edge case (noted only)**: `scanning → blocked → idle` directly (block clears
exactly as the scan finishes) yields no success tone, since the transition into
idle came from `blocked`. Normal flow clears back to `scanning` first, so the
success tone fires on the real finish. Revisit only if BEC ever exposes an
explicit scan-aborted signal.
---
## 9. Tools & environment
- **Languages**: Python (BEC iPython env), PHP (lighttpd), JavaScript (frontend), Bash
- **Python libs**: `requests` (upload), `Pillow` (thumbnails), `http.server`
(local server, port 8080)
- **PHP**: `password_hash()` / `password_verify()` (bcrypt)
- **Beamline systems**: EPICS (PVs), caQtDM (`.ui` panels), BEC client
(`bec.queue`, `bec.get_global_var`, `bec.builtin_actors.scan_interlock`)
- **Infra**: lighttpd, certbot / Let's Encrypt (webroot), IONOS Webhosting Plus,
Raspberry Pi 3B, PSI Gitea
---
## 10. Working style
- Discuss architecture and confirm technical details **before** implementing code.
- Live testing happens on the actual beamline system; results are reported back
iteratively.
- Write a project briefing `.md` after major sessions to enable seamless continuation.
- Prefer clean, non-redundant UI; be decisive about **removing** elements rather than
replacing them with alternatives.
- Decoupled daemon architecture for new monitoring components.