NightScribe — Guided workflows (UX v3)
Master document of the project-centric redesign. Decisions: ADR-019 (projects UX), ADR-020 (horizon & constraints), ADR-021 (sequence & ephemeris exports), ADR-022 (MPC report). This document describes what gets built and in which order; no code exists yet (2026-08-24).
1. The observer's question
"What can I do tonight — or right now?" The app answers by suggesting the most interesting targets under the observatory's real constraints, and from the chosen one a project is born that guides everything else without re-asking anything.
2. Constraints the planner must honour
| Constraint | Source | Effect |
|---|---|---|
| Limiting magnitude | limit_mag in config (equipment) | Hybrid (ADR-025): hard cut for SNe/comets/exoplanets, ⚠ mag>N warning for NEOs/PCCPs — never dropped |
| Local horizon | User's TheSkyX file (ADR-020) | Minimum altitude per azimuth + safety margin; fallback to flat min_alt |
| Moon | ephem_minor (ADR-009) | Warning + score penalty by separation/illumination; not a hard filter |
| Session feasibility | core/exposure.py + horizon | The window must cover the session duration; "safe start until HH:MM" is shown |
| Plate scale | Camera profile (pixel_um, focal_mm) | Maximum trailing-free exposure for NEOs |
3. Anatomy of a project
Persistent entity (ADR-019): kind, target, status (active/done/archived), full JSON context and three guided steps. Context is captured when the project is created from Tonight (target snapshot: coords, mag, rate, window…) and enriched at each step (exported sequence, imported FITS, measurements, posts).
Steps: Object card → Capture → Analysis → Publish (ADR-045, 2026-09-24: "Process" is renamed "Analysis" with migration v8 and the kind-gated Follow-up tab dissolves into it; the Analysis tab is built around the visits manager for every kind — every day you work the object is a visit, and its resources (plates, reports, imports) hang from it, registered in project_files with session_id; nothing attaches without a visit. ADR-019's 2026-09-06 review merged the Capture step into Plan and added real CCDciel control — ADR-030). The how of measuring a series from the visit (photometric sequences, detrend, multi-night, ExoClock and live mode) lives in SEQUENCES.md; a transit's scientific close goes through orchestrating EXOTIC from the app (see SEQUENCES and ADR-052).
4. Flow — supernova / transient
Plan: card with current magnitude, age, host galaxy, window above the real horizon and "safe start until HH:MM". The user sets N frames × exposure and filter; the app checks the session ends inside the safety range.
Capture: export sequence (NINA JSON / CCDciel / CSV) with name, J2000 coords and times; the file is registered in the project. With the observatory connected (ADR-030, ADR-081: CCDciel or NINA/PINS) the same tab can also: point the telescope, run an astrometric goto (plate-solve + correction), stop the movement (safety), send the plan (target, exposure, frame count, filter) and start the capture live. The plan's filter list is the wheel's real one; NINA's snapshot takes one exposure, so its frame count is information only.
Process (external): the user calibrates/stacks with their own tools; back in NightScribe, "Import result FITS" — without asking for target or coordinates (already in context) — then confirm with the blink, matched against PS1-g (pre-filled ADR-018 pipeline), and export GIF/MP4/PNG with the observatory watermark.
Publish: ES/EN story + tweet with the real session data (date, N×t, filter) + observed/posted marks in history.
5. Flow — NEO / PCCP candidate
Plan: card with apparent rate (″/min) and maximum trailing-free exposure computed from the equipment's plate scale; frame count; window and "safe start". Warning if the session would end outside the safety range.
Capture: export ephemerides at a configurable step for TheSkyX and Cartes du Ciel (formats validated against real imports in phase 5; CSV as the base) + sequence for the capture software. With CCDciel connected (ADR-030): send the plan and start the capture live.
Process: the user measures externally (Astrometrica or other) and pastes the measurements into the project → validation (80-col/ADES, MPC code, designation) → packaged file ready to email to the MPC (ADR-022). Sending is done by the user.
Publish: ES/EN story +
orbit_view/sky_view+ observed/posted marks + NEOfixer report if an API key is configured. The orbital interpretation itself (family, MOID, size from H, next close approach — reusesenrich.py+orbits.py) is not a step: it already renders in the Details tab the moment the project is opened.
6. Later flows
Comets and exoplanet transits reuse the same 3-step skeleton once the SN and NEO flows are settled. Out of this initial redesign.
7. Roadmap and entry point
| Phase | Deliverable | Status |
|---|---|---|
| 1 | ADR-019…022 + this document + updated DESIGN | Done (2026-08-24) |
| 2 | Constraint foundation: core/horizon.py, Moon, core/exposure.py, camera profile in config, planner/suggest integration, horizon silhouette in sky_view, tests | Done (2026-08-24) |
| 3 | Project model: db migration (user_version 0→1), core/project.py, step machine, tests | Done (2026-08-24) |
| 4 | GUI v3: 4 tabs, Projects hub with per-kind stepper, contextual blink, Tonight cards with "Create project" and "safe start", Settings (Camera/Horizon/Session/Moon), i18n | Done (2026-08-24) |
| 5 | Exporters: core/sequence.py (NINA/CCDciel/CSV) + ephemerides (CSV + TheSkyX + CdC validated against real imports), tests | Done (2026-08-24); CCDciel with the real .targets format (CONFIG v5) against docs/ccdciel_sequence_sample.targets (2026-09-06) |
| 6 | core/mpc_report.py + step in the NEO flow + minimal CLI project subcommand + polish, i18n and final docs | Done (2026-08-24) |
7bis. Home screen redesign (2026-08-25)
Why: the «Tonight» screen did not match the app's look, it never explained why each target is suggested (the "why tonight" phrase only lived in the tooltip) and the 4×2 grid of mini-cards felt cramped. The app also had no global theme: dark cards floating over the platform's light chrome.
| Phase | Deliverable | Status |
|---|---|---|
| A | Global dark theme: gui/theme.py (palette + chrome QSS + KIND_COLORS), apply_theme(app) in app.py, ADR-026, tests/unit/test_theme.py | Done (2026-08-25) |
| B | «Tonight» as a vertical list of wide rows: name, visible "why tonight" line, chips (mag/alt/window/moon/⚠), 4-segment score mini-bar, 0-100 score, Start/Continue button, subtle metallic top-3 podium, row-click→Explore, loading/empty states | Done (2026-08-25) |
| C | Redesigned «Full target list» table over the theme, double-click→project, ES/EN i18n regeneration of the new strings, smoke tests | Done (2026-08-26) |
Phase C (done, 2026-08-26): the table is kept and now behaves like the wide rows above it — row-level selection (no cell 2×2, no row numbers), every row tinted with its kind color, the top 3 (score order) wearing a stronger tinge as a quiet podium, bold name + score in the kind color, headers/selection/hover from the global theme (ADR-026). Double-click from any column starts/continues the project. Code in nightscribe/gui/main_window.py (_prepare_table, _fill_table, _table_start_project) + tonight_tab.ui (tooltip). New strings translanted ES/EN and .ts/.qm regenerated (ADR-014). Smoke tests in tests/unit/test_tonight_table.py (pattern of test_tonight_rows.py).
7ter. Phase D — the object view as the Projects hub's lead (2026-08-26)
Motivation: opening a project showed the Plan first (frames × exposure), but without an object there is no plan. The object's "business card" — today only inside the modal Explore… dialog — must be the first and always-visible panel. Nothing new is built: everything already exists (enrich, narrative, orbits, charts, exposure); the work is unifying it into a fixed panel, attractively laid out, with no clipped text (multiline, everything visible).
Agreed decisions (2026-08-26):
The object view is a fixed panel above the step tabs (Plan/Capture/Process/ Publish) in the Projects hub. It is not a step: it cannot be skipped or "marked done"; it stays out of the step machine. (Revised 2026-08-27: it is now the "Details" tab — first and the one that stays open on project selection — same guarantee, the object always visible, but owning the full panel height. Revised again 2026-08-28 (ADR-019): the step list is now four — see ADR-019 review.)
It contains (all pre-existing): hook line + outreach bullets + parameter table with multiline ES/EN explanations + the charts (orbit, sky, field, light curve — the ones build_charts cannot produce are hidden; with none, the group disappears) + capture/window block (mag, ″/min rate, max no-trail exposure where applicable, window start–end, hours above).
A shared panel is extracted to
gui/overview.py(ObjectPanel) used by the Projects hub and the ad-hoc Explore… dialog: one source of truth.Untouched: all of
core/, the step machine (core/project.py,_STEP_KEYS) and the Plan panel (its max-exposure calculator keeps living there).
Reused pieces, unmodified (with their current home):
| Piece | Origin |
|---|---|
| Enrich (network, cache) | gui/workers.py ExploreWorker → core/enrich |
| Hook line, bullets | core/narrative.hook, core/narrative.fact_bullets |
| Parameters + ES/EN explanation | core/orbits.explain_elements, core/orbits.explain_neofixer |
| PNGs (orbit/sky/field/light curve) | core/post.build_charts (core/post.py:172) |
| Max no-trail exposure (NEO) | core/exposure.plate_scale + max_exposure_no_trail |
| Mag, rate, window, hours | project.context (snapshot, gui/main_window.py:1474) |
| Chart click → zoom/export | gui/main_window.py:74 _ChartClickFilter |
| ES/EN pair language | orbits.pick (pattern of MainWindow._txt) |
Current state being reorganised: the object presentation currently lives only in the modal dialog gui/main_window.py:1504 (_open_explore_dialog, _dialog_explore_params:1551, _dialog_explore_charts:1588, _render_object_charts:1572), opened via step 4 Analyse (_build_analyse_tab:1197). The Projects hub shows Plan first and a one-line truncated lbl_context header (gui/ui/projects_tab.ui:51).
| Phase | Deliverable | Acceptance | Status |
|---|---|---|---|
| D1 | gui/overview.py: ObjectPanel without charts — loading / not found / ready states, hook line, bullets, parameter table with a wide (multiline) explanation column. show(e, ctx=None) + injectable loader (offscreen, no network). GPL header, ES/EN pairs | Panel built offscreen; with fake e: hook visible, parameter rows with explanation; with {}: not found state | Done (2026-08-26) — gui/overview.py + tests/unit/test_overview_panel.py (9 offscreen tests, no network) |
| D2 | ObjectPanel with a 2×2 chart grid filled by core/post.build_charts; "why not" messages (hyperbolic / no elements / unconfirmed) scaled to the widget | With fake e: 4 chart slots or the right absence messages; without e: empty | Done (2026-08-26) — orbit/sky/families/field grid in gui/overview.py + 5 offscreen tests in tests/unit/test_overview_panel.py (14 total, offline; only field would touch the network and stays on a "why not" line) → revised 2026-08-27 — no families (below), slots orbit/sky/field/transit, panel size (1200×675); unbuildable slots are hidden instead of a "why not" line; with no chart, the whole group disappears → 2026-09-06 — the produced charts are grouped into tabs (QTabWidget in gui/overview.py, one tab per chart, titled; with a single chart the tab bar auto-hides and it stands alone; the 2×2 grid is gone) |
| D3 | ObjectPanel with capture/window blocks from ctx: mag, ″/min rate, max no-trail exposure (neo/pccp only, with rate + pixel_um/focal_mm), window start–end + hours above; missing data is omitted | With a fake neo ctx: full chips; with an sn ctx: no rate/exposure; does not break on empty ctx | Done (2026-08-26) — chip row in gui/overview.py (mag / rate / max no-trail exposure / window / hours, from project.context; rate and exposure only for neo/pccp) + 5 offscreen tests in tests/unit/test_overview_panel.py (19 total, no network). New self.tr() strings pending D6 i18n (D1/D2 pattern) |
| D4 | Integration in the hub: the hub inserts ObjectPanel between lbl_context and tabs_steps (keeping the lbl_context header). On project selection → loader = ExploreWorker (same fallback_target from the context), worker kept via _keep, the in-flight worker is cancelled when switching project | Without network: selecting a project shows loading state and does not crash; step machine, plan panel and buttons stay intact; offscreen smoke tests | Done (2026-08-26) — gui/main_window.py builds the ObjectPanel lazily in _get_proj_panel() (wrapped in a QScrollArea, inserted before tabs_steps); _project_selected loads it with ctx + fallback_target and cancel()s the in-flight worker when switching (a late result never lands on the next) + tests/unit/test_projects_hub.py (6 offscreen tests, no network) → revised 2026-08-27 — the panel now lives inside the Details tab (index 0, new in projects_tab.ui), which is the one left open on project selection; prev/skip/done are disabled on it and next enters step 1 → 2026-09-06 — the hub list is always up to date without "Refresh": on_refresh_projects() is scheduled with QTimer.singleShot(0,…) when the app opens and re-run on every visit to the Projects tab (_on_main_tab_changed, tabs.currentChanged → index 1); the Refresh button stays as a fallback; the selected project is preserved across a refresh (its id is re-selected after re-rendering); +3 offscreen tests in tests/unit/test_projects_hub.py (load on startup, list without the button, selection preserved) |
| D5 | ***Explore…* dialog over the shared panel**: main_window.py:1504 becomes ObjectPanel + a Make post button; the _dialog_explore_* functions delegate to the panel; chart click→zoom is kept (the panel exposes its chart labels); _project_explore (step 4) / _tools_explore keep working | The Tools-menu Explore… dialog is identical in content; the NEO step 4 still opens it pre-filled | Done (2026-08-26) — _open_explore_dialog now builds ObjectPanel(for_post=True) inside a QScrollArea + a Create post button (the panel's post_requested(name, fallback) signal → _open_post_dialog + close); the _dialog_explore* / _orbit_rows / _ChartClickFilter / _set_scaled_pixmap helpers are gone (they live in overview.py already); _render_object_charts stays for the post flow; the orphan explore_tab.ui is deleted. Tests: 6 new offscreen in test_overview_panel.py (post button visible/hidden, post_requested carries name + fallback, in-flight guard, cancel clears) + 2 in test_projects_hub.py (integration of _explore_panel) + the 2 functional tests now drive the panel (parabolic orbit chart + unconfirmed NEO) |
| D6 | i18n + verification + docs: lupdate/lrelease → nightscribe_{es,en}.ts/.qm (ADR-014); pytest tests/unit green; this section with final statuses | New strings translated ES/EN; smoke tests in tests/unit/test_overview_panel.py (D1-D3) + hub smoke (D4) green | Done (2026-08-26) — pyside6-lupdate (sources gui/*.py + gui/ui/*.ui) refreshes both .ts (+17 ObjectPanel strings, −21 obsolete ExploreTab); 17 new strings filled by hand (EN passthrough, ES translated); pyside6-lrelease → 2 .qm (306 strings, 0 unfinished); i18n smoke: test_panel_strings_resolve_in_{spanish,english} loads each .qm and verifies btn_post / grp_params / grp_charts / table headers / chip tooltips → 234 tests green (no network) |
Revision (2026-08-27) — "Details" up front, cleaner charts:
Details tab first: new in
projects_tab.ui(index 0 oftabs_steps). The object's calling card —ObjectPanel— lives inside it (_get_proj_paneldocks it there) and it is the tab left open when a project is selected: all the object info at a glance. The current step is still marked ● on its tab and reached with Next →; prev / Skip / Mark done are disabled on Details (no step to act on), Next stays allowed (enters Plan).Farewell to "Where does it live?":
viz/families_view.pydeleted (it had no more consumers);core/post.build_chartsstops drawing it andCHART_LABELSloses thefamiliesentry. In the panel the slot becomes orbit / sky / field / light curve (exoplanet transits, already covered bytransit_view).Empty slots are hidden: the slots
build_chartscannot generate are hidden instead of a "why not" line; when no chart is generated at all, the whole group disappears. Panel charts use thepanelsize (1200×675, added tostyle.SIZES) for less margin and more legibility in the window.
Chart resolution setting:
Settings > Panel chartsadds a On window resize choice — Fast (the chart is drawn once at thepanelsize and re-scaled, the default) or Sharper (re-drawn at 2×, 2400×1350, so big slots stay crisp). Stored asconfig["chart_zoom"]("scale"/"re-render") and read byObjectPanelbefore everybuild_chartscall; switching the setting while a panel is on screen re-draws it (rebuild_charts). _(Removed 2026-09-04: thechart_zoomoption and the Settings > Charts tab were dropped once the ADR-029 vector widgets arrived, which render at native resolution. Seedocs/adr/ADR-028and the Slice 0 ofdocs/PLANS/approach-chart.md.)_Chart viewer opens fitted: clicking a chart opens
ChartViewer
fitted to the window — the dialog is sized to the image's aspect so the default view fills it without scrolling (wheel zoom / drag pan / 1:1 unchanged). The last window size the user left is remembered per chart (config["chart_viewer_sizes"], restored on the next open).
Tests:
tests/unit/test_overview_panel.py,test_projects_hub.py,test_style.py(panel preset, size override, squeezed margins) and the newtest_chart_viewer.py(fit without scrollbars, fit follows the window, zoom buttons, per-chart size memory) updated to the new contract (6 tabs, panel inside the Details tab, hidden slots without data, group hidden without charts).pytest tests/unitgreen (247 tests, no network).
Per-phase rules: one phase = one commit; each phase leaves the app working with its tests; two phases are never mixed before the previous one is verified (rule of this document, §7). Offscreen tests follow the tests/unit/test_tonight_table.py pattern (QApplication + theme.apply_theme
panel directly + fake payloads, no network).
ENTRY POINT (for any AI or human resuming Phase D):
Read this section 7ter in full, in order.
Start with D1 (
gui/overview.py), validate the base, then proceed D2 → D3 → D4 → D5 → D6 in order.Do not touch
core/or the step machine; if it turns out necessary, stop and open a new ADR/decision before writing the minimum.GPL header mandatory in every new
.py, code in English, GUI-visible strings viaself.tr()and ES/EN pairs viaorbits.pick(ortr()for the static ones). Active language:config.get("language")(patternMainWindow._lang,gui/main_window.py:227).Phase D is independent of the "suggested next steps" that follow below (real horizon, native formats, comet/transit flows).
Before Phase D, the historical ENTRY POINT (UX v3 project, phases 1-6):
Read, in this order:
docs/adr/ADR-019,ADR-020,ADR-021,ADR-022and this full document.Phases 2 and 3 are already implemented on branch
feature/ux-v3-projects: - Phase 2:core/horizon.py,core/exposure.py, helpers incoords.py, Moon penalty insuggest.py, planner integration inplanner.py, silhouette inviz/sky_view.py, extended config, mocktests/fixtures/horizon_sample.txt+ tests. - Phase 3:core/db.pymigration (user_version0→1: tablesprojects,project_steps,project_files;observations+=project_id),core/project.py(model + step machine Plan→Capture→Process→Analyse→Publish), tests. - Phase 4: GUI v3 — 4 tabs (Tonight · Projects · Solar · History),projects_tab.uiwith list + stepper, Explore/Post/Blink as contextual modal dialogs (pre-filled from project), ad-hoc access from the Tools menu, Tonight cards with "Create project" + window + Moon, Settings extended with Camera/Horizon/ Session/Moon groups,sky_viewwith real horizon, i18n (.ts/.qm) updated. - Phase 5:core/sequence.py(NINA JSON / CCDciel.targets/ generic CSV capture sequence exporters) andcore/ephemeris.py(Horizons-based generation + CSV / TheSkyX / Cartes du Ciel ephemeris exporters). "Capture plan" panel in the Projects hub with frames/exposure/filter fields and export buttons. The native NINA/CCDciel/ TheSkyX/CdC formats are starting points that require validation against the user's software versions via real import. - Phase 6:core/mpc_report.py(validator for pasted measurements in MPC 80-col or ADES PSV: format, observatory code, designation; file packaging for MPC submission). "MPC report" panel in the Projects hub. CLI subcommandnightscribe project list|create|advance|show. Full i18n (208 strings ES/EN). All phases complete. The TheSkyX safety horizon is fully in place (E1+E2, 2026-08-28): (E1)core/horizon.pyparses the SkyX limits export (docs/limits-sample.hrz= canonical reference) in addition toaz alttext, with 16 safety tests and rejection of corrupt files. (E2) A valid horizon file wins: Settings disablesmin_altunder it (explanatory tooltip) andhorizon_margin_degis added on top. The safe range is visible app-wide (ADR-020):planner.safe_window_forcomputes the safe span +best_time+latest_safe_start;sky_viewshades the span and marks «start by HH:MM»; the Tonight cards and the project hub show a green «⊕ HH:MM–HH:MM · ≤ HH:MM» chip or a red «⚠ does not fit» chip when the planned session does not fit; the ES/EN prose carries the window and its warning («DO NOT force the instrument»). 313 unit tests green. The native CCDciel format is now real:export_ccdcielwrites the.targetslist (CONFIG Version="5") fixed against a real user export (docs/ccdciel_sequence_sample.targets, 2026-09-06), with Light + Dark + Bias steps (calibration configurable in the project's Capture tab) and the default rise/set window that CCDciel recomputes from its own observatory settings. NINA and generic CSV remain starting points that require validation against the user's software versions via real import (ADR-021).
Suggested next steps (beyond the initial redesign):
(Done 2026-08-28: the real TheSkyX horizon is parsed and now outranks
min_alt— see E1+E2 above.)Real moon icon (2026-08-27):
gui/moon_icon.pycomposes the lunar surface (assets/moon_disk.png, CC BY-SA 3.0 photo — seeassets/ATTRIBUTION.txt) with the terminator drawn by exact geometry (r·cos E), waxing on the right / waning on the left perephem_minor.moon["elong_deg"]. It replaces the % in the Tonight header (tooltip: % now vs. by dawn + why it changes within one night) and the emoji in the Solar almanac. Flat grey disc fallback if the asset is missing. 8 offscreen tests, no network.Validate the sequence and ephemeris export formats against real software.
Add flows for comets and exoplanet transits in the same 4-step skeleton.
Test the GUI thoroughly and refine the stepper and contextual dialog UX.
Standing rules: English code with GPL header and
# @args:comments, GUI strings viaself.tr(), network only fromcore/sources/throughcore/db.py, bilingual documentation (ADR-013), unit tests per module + end-to-end functional tests.
7quater. Object-kind filter across the «Tonight» tab (2026-08-30)
Why: the kind filter (NEO / SN / CMT / PCCP / TRN / ALT) used to live only inside the collapsed table (grp_list / cmb_filter), at the bottom of the tab. The wide rows of the grid above and the podium (ring / stronger tint) were always built over every target of the night, mixing classes. Observers could not tell the app "tonight I only want supernovae" or "stop showing me comets". K1 lifts the filter to the header and makes it rule the whole view (rows + table); K2 turns the shown kinds into a permanent whitelist preference in Settings.
Agreed decisions (2026-08-30):
One filter in the header (K1).
cmb_filterleavesgrp_listand moves to theTonightheader row (next tolbl_contextandlbl_moon). Changing it refreshes both views at once: the grid rebuilds its rows for the chosen kind and recomputes the podium over what is visible (the top 3 of the filtered set, not of the whole night), and the table re-fills with the kind's own columns (TABLE_COLS[kind]). Single source of truth:MainWindow._visible_targets()(whitelist + filter); both grid and table call it.Whitelist of enabled kinds in Settings (K2). Adds
config["enabled_kinds"](default = all 6) and a «Tonight: object kinds» group intab_observingwith 6 checkboxes (NEO, SN, CMT, PCCP, TRN, ALT). On Save, the combo only lists the enabled kinds + «All»; if the active kind is disabled it falls back to «All»; if the whitelist grew, the combo grows too. «All» means every enabled kind, not every target of the night.core/is not touched: all filtering lives ingui/main_window.py—config.py(defaults),ui/tonight_tab.ui(header combo),ui/settings_dialog.ui(kinds group),main_window.py(_rebuild_kind_filters,_enabled_kinds,_visible_targets,_apply_kind_filter).suggest.top_nkeeps diversifying;enabled_kindsonly cuts what is shown on this tab, never the scoring.Persistence:
config["tonight_kind"]("" = All) is written on every combo change and restored on start when still in the whitelist. No new ADR: it is a local UX decision layered on the already-documented ADR-019/ADR-026 plane (single source ofKIND_LABELS/KIND_COLORS).Tests:
tests/unit/test_tonight_kinds.py(offscreen): combo in the header with the 7 items (All + 6), grid and table limited to a kind, podium recomputed over the visible set,tonight_kindpersistence, whitelist shrinking/growing the combo, and the fallback to «All» when the active kind is removed.pytest tests/unitgreen.
### 7quinquies. Per-kind cap for the grid + best-of-kind ring (K3, 2026-08-30)
Why: with K1+K2 the night can bring 15 NEOs from NEOfixer, 90 SNs from Rochester, 15 comets from COBS… and the grid used to paint all of them (a wall of one class that forced the observer to filter to see anything). K3 introduces the best-of-each-kind cap (configurable, not grouped) directly on the grid, and the ring now lands on the best of each visible kind — not on "the first three of the set" (7quater is superseded for the ring semantics).
Agreed decisions (2026-08-30):
core/suggest.best_per_kind(scored, n): input is the already scored, global-score-sorted list (the onetop_nreturns); keeps at mostnof every kind (defaultconfig["best_per_kind_n"] = 5, configurable 1..50);n<=0= no cap. Returns(grid, best_ids):gridkeeps the global score order (not grouped by kind) andbest_idsis the id of the BEST target of each kind present (the "rings").The Tonight grid (
_build_suggestion_grid): filters to the visible set (K1+K2), appliesbest_per_kind, paints the result. The ring lands on the best of each visible kind (not oni<3); the status/action button stays on the right. The grid rebuilds whenbest_per_kind_nchanges from Settings.The Tonight table: unchanged in content — the full list (all 6 groups, all scores, all filters) stays the whole set; only its order and its tint (already best-per-rank 0/1/2) reflect the same best-per-kind rule.
suggest.top_n(CLI) is not touched.Planner
_comet_targets(bottleneck): the 15 Horizons ephemerides now run in parallel (~4-worker pool, COBS still returns the 15 brightest).n_comets=15is preserved.Config:
config["best_per_kind_n"] = 5(new, default 5) andui/settings_dialog.uitab_observing→ rowspn_best_pk(SpinBox 1..50) with ES/EN translations.Tests:
tests/unit/test_best_per_kind.py(6 cases: cap, best-per-kind, no cap, order, empty id, empty input);test_tonight_rows.pyupdated — now every row in the fixture wears the ring (one per kind ⇒ best of each);test_tonight_kinds.pyupdated — the PCCP (rank 4 overall) DOES wear the ring when switching to the "PCCP" filter (and keeps it in "All").pytest tests/unitgreen (345 tests, 13.1 s).Languages: 346 strings (2 new), 0 unfinished.
Entry point (for any AI or human resuming this screen):
Read this section 7quater, ADR-019 (the tab hub) and ADR-026 (single theming source).
Start with K1 (
tonight_tab.ui+main_window.py_apply_kind_filter/_visible_targets), verify the contract (grid + table together, podium over the visible set), then do K2 (config.enabled_kinds+ the kinds group insettings_dialog.ui+ the guards inon_open_settings).Do not touch
core/to filter — if it looks needed, open a new ADR before the minimal change (the planner still returns all 6 groups; scoring keeps diversifying; the whitelist only cuts the view).Mandatory GPL header on every new
.py, English code, GUI-visible strings viaself.tr()(pattern inmain_window.py), ES/EN pairs viaorbits.pick(ortr()for static ones). Active language:config.get("language")(patternMainWindow._lang).
### 7ses. Phase E — single entry point (2026-09-02)
Motivation: with phases D and K done, the "way to get to an object" was scattered: row click → Explore, double-click on the table → silently creates a project, the row's button → project. The observer could not consciously choose between exploring and working on a project, and the table (the fastest view) did not let them see the object before creating anything. Phase E unifies: one single entry point ("Explore"), and the Continue/ Explore buttons become pure shortcuts; the project is created or resumed from inside the Explore dialog.
Agreed decisions (2026-09-02):
Row click → Explore (already the case; kept).
Table double-click → Explore (was a silent project-creator — now
_table_open_explore, same destination as the row and as the Explore button).Row button (
_card_button): green "▶ Continue" when an active project already exists for the object (short-cuts to_start_or_continue, which jumps to the hub); orange "🔭 Explore" when there is none (opens the Explore dialog with that object). The button is a shortcut, not a different action from the row's.
Explore dialog (
_open_explore_dialog): the panel's only action is the single CTA (bottom of the section, full width, 46 px). The D5 "Create post" button is gone: posts are written inside the project (Publish step) or on demand from Tools. The CTA's face depends on theproject_lookupthe window injects: green "Continue project" if one is active for this object, orange "Create project" if not. It emits one of two new signals, each with two arguments (name, fallback):project_create/project_continue(the earlierproject_action(name, fallback, continue_)is gone).MainWindowreacts in_on_create(goes to_create_project) and_on_continue(goes to_goto_active_project; creates a fresh one if the "explored name" does not match the active); the dialog closes after either. The dialog auto-fits to its content with reading floors (resize_to_panel_contentingui/overview.py: width ≥ 780, height = sizeHint + frame headroom) so the text reads comfortably and the CTA is always visible with no vertical scroll._create_projectnow returns the created dict (orNone, with a
status-bar message when the kind is not a valid project kind — e.g. an ad-hoc transit from the Tools menu cannot create a project, only a post).
_goto_active_project(name, fallback=None)(new, private): finds the active project whoseobject_namematchesnameor theid/nameof thefallback(NEOCP/PCCP store the MPC number asidand a tentative name asname; either one may be the one kept with the project), and selects it in the hub. ReturnsTrue/False.
The panel stays testable:
ObjectPanelacceptsproject_lookupas an injected callable (nocore.dbimport —MainWindow._explore_panelbuilds it);for_post=False(hub) → CTA hidden;for_post=True(Explore) → CTA appears once the object is loaded and the lookup reports the intent.
Tests: tests/unit/test_overview_panel.py (7 new: pair hidden without a lookup, "Continue" when there is an active, "Create" when there is not, the signal with the right continue_, ignored while loading, _blank resets them); test_tonight_rows.py (3 new: "Explore" button wired to _open_explore_dialog, "Continue" button wired to _start_or_continue, and the "continue" → "create" fallback path); test_projects_hub.py (4 new: _goto_active_project by name, by fallback id, no match, and the dialog project_lookup honouring the planner's id/name); test_tonight_table.py already covered the double-click. pytest tests/unit green.
Languages: 4 new strings ("Continue", "Explore", "Continue project", "Create project" + their tooltips), ES/EN pairs in both .ts and both .qm recompiled.
Entry point (for whoever resumes this phase):
Read this section, ADR-019 (hub) and ADR-026 (themes).
The four
gui/touchpoints:_card_button,_table_open_explore,_open_explore_dialog,_explore_panel(inmain_window.py) and theObjectPanelconstructor +_refresh_cta/_cta_clicked(inoverview.py).Do not touch
core/project.pyto add kinds —VALID_KINDSstays as
the single source; if a new kind enters, add its case to the creation tests (the dialog already honours it via the kind carried in the target).
Mandatory GPL header, English code,
self.tr()in the GUI,pyside6-lreleaseafter every.tschange.
Correction (2026-09-02, same day): the pair and the i18n smoke tests were failing because the pair's API is now the single CTA. ObjectPanel exposes btn_project (one full-width button, min-height: 46, section bottom) and the two signals project_create(name, fallback) / project_continue(name, fallback) (two args each; post_requested and project_action are retired). The window connects both in _on_create / _on_continue (in main_window.py:1979); the earlier orchestrator (_do_project / _make_post / _explore_post) is removed. The 9 CTA tests in test_overview_panel.py + 4 in test_projects_hub.py
2 in
test_tonight_rows.pypass.
Each phase leaves the app working and ships its own tests. Do not mix phases in one commit without the previous one being verified.
7septies. Unified object card (2026-09-07, ADR-031)
Motivation: the object card only gave a parameters table to NEO/comet/PCCP; SNe and transits were left with hook + bullets, the table clipped long explanations, the coordinates were nowhere to be seen, "Discovered" did not apply to NEOs and the ADR-015 aperture filter was never implemented. Plan: docs/PLANS/object-card.md (branch feature/object-card).
What changes (one subplan = one commit):
Copyable coordinates: block under the hook with RA/Dec in decimal and sexagesimal + a "Copy" button (
_coords_from: same source chain as the sky chart).Multi-line table: wordWrap + row auto-height that follows the stretch column on resizes (beware:
sectionResizedfires beforecolumnWidth()updates — the handler forces the notified width first).SN table:
orbits.explain_transient(type, host galaxy, distance, brightness, discovery; redshift in depth).Transit card: bug fixed — the exoplanet branch of
enrichused to lose the ExoClock event; it is now merged with the ADR-027 pattern.orbits.explain_transittable with start/end UTC, depth (mmag and %), duration and the minimum telescope vs. your aperture verdict.Per-kind chips with one grammar: SN gains event type and freshness ("N d", green when ≤14 days); transit gains depth Δmmag.
Exact "Discovered" for NEOs and PCCPs: SBDB
discovery=1(fallbackfirst_obs), NEOfixer/orbit/for NEOCP, PCCP maps its own column. Parallel pool of 4 + 7-day cache. New singlecore/dates.py.Hard aperture gate for transits:
transits_tonight(aperture_in=)+transit_scope_filtertoggle in Settings > Observing (on by default; no ExoClock datum means no discard — ADR-025 spirit).
Status: subplans 0-6 done; unit suite green (555). Commits: 72d66ee (plan), b163f6c, 376a23c+67ceadf, 02c05bf, 3412c3c, 8c4c336, afab280, 03290ea + the i18n/docs closing commit.
i18n note: the lupdate command documented in CONTRIBUTING now includes gui/widgets/*.py — without it lupdate marks live chart-widget strings as "vanished" and the i18n tests fail on regeneration.
7octies. Fresh-ephemeris goto for moving targets (2026-09-07, ADR-030 rev.)
Motivation: NEOs, comets and PCCP candidates have no fixed coordinates (unlike SN and transits). The goto used the snapshot the planner stored when the project was created — a 5″/min NEO with a 2-hour-old plan accrues 10′ of error, and the astrometric goto solved the wrong field. The card also showed the Horizons row at 00:00 UT (eph[0], daily step), up to 24 h stale. Plan: docs/PLANS/goto-fresh-ephemeris.md.
What changes (one subplan = one commit):
core/ephemeris.py::position_at: queries Horizons at a 2-min step over a ±2 h window (rounded to 30 min to reuse the cache), linearly interpolates to "now" (RA unwrapped at the 0h/24h seam), derives the apparent rate (″/min) and PA. Offline fallback: SBDB+Kepler → NEOfixer preliminary →None.Fresh goto in
main_window.py: forneo/comet/pccptheCcdcielWorkeraction resolvesposition_atbefore the slew/solve and folds the result into the context (coords_epoch/coords_source/rate). SN and transits keep using the snapshot. A "Position at HH:MM:SS UT" label + a warning when it falls back to the snapshot.Card with visible epoch:
_enrich_small_bodyrequests a 30-min step and picks the row nearest now (ephem_epoch); the coords block shows the epoch next to the copyable RA/Dec.Closure: ES/EN i18n (~4 strings), ADR-030 revision, this section,
pytest tests/unitgreen (582).
Status: subplans 0-3 done; unit suite green (582). Out of this iteration: SolarTracking/UpdateCoord=True in the .targets and non-sidereal rates via JSON-RPC (they need validation against the real CCDciel); the per-exposure no-trail cap (max_exposure_no_trail + rate_arcsec_min) already protects the frames.
7terdecies. Project container folder (2026-09-10, ADR-032)
Note: this English file is behind the Spanish master (WORKFLOWS.es.md), which already carries the 7nonies…7duodecies sections (Tracks A–D). The chunk below matches the ADR-032 slice; a backfill of the missing English sections is pending.
No dedicated docs/PLANS/ file: a short jump decided in ADR-032. Everything a project produces (plan, sequences, FITS, posts, charts) fell under data_dir()/projects/<id>-<slug> (fixed, platformdirs): the user neither knew where the files ended up nor could point them at their working disk. Give control of the container folder: a configurable global root and, when needed, a per-project folder.
| Sub | Deliverable | Status |
|---|---|---|
| P1 | user_version 6 migration: root_dir in projects + backfill to the legacy path; config.projects_root; paths.project_dir(root=) stays pure; project.storage_dir() (project root → config → default), set_root_dir(), and create freezes the root; tests test_project_storage.py | Done |
| P2 | All 9 main_window.py call sites write through project.storage_dir(); CLI project show prints folder: | Done |
| P3 | Settings > Observing: "Projects folder" group (browse/reset of projects_root) + a "Change folder…" hub button (re-homes future exports only, v1 never moves files); tests test_settings_storage.py + extended | Done |
| P4 | Closure: ES/EN i18n (598 strings, 11 new), lrelease, ADR-032, this section | Done |
Status: full unit suite green (895). Behaviour: the global root change only affects new projects; legacy ones keep their exact folder frozen in root_dir; the hub button only re-homes what is written from then on.
7quaterdecies. HADS track — high-amplitude variable stars (2026-09-11, ADR-034)
Note: the Spanish master (WORKFLOWS.es.md) carries the authoritative version of this section.
Plan: docs/PLANS/hads-stars.md (master) + docs/PLANS/hads/fase-*.md (20 self-contained subplans). Branch feature/hads.
New hads target kind: high-amplitude δ Scuti stars (1–5 h periods, ΔV ≥ 0.3 mag) — you watch them pulsate live. No known phase → the recommendation is a continuous 2×P capture (not an event); the listing gate demands a contiguous ≥ 1-cycle window. The data source is hybrid: Patrick Wils' Google Sheets workbook (updated daily) is downloaded at runtime with a 12 h cache (core/sources/hads_sheet.py, stdlib-only XLSX parsing, two levels: raw + parsed JSON) and merged over the bundled snapshot (assets/HADS-stars.csv, offline fallback + aliases). The sheet's color legend feeds the score: red/orange (period changes!) +12/+8 urgency, blue (not yet observed) +6, monthly coverage gap +10 — never stacked (max wins).
| Sub | Deliverable | Status |
|---|---|---|
| H0.1–H0.5 | stdlib XLSX reader, 12 h TTL cache, color+coverage parsing, core/hads.py (catalog/merge/derived/sawtooth), data docs | Done |
| A.1–A.3 | hads planner phase (1-cycle gate, 2P session in _visibility), scoring (4 families), ES/EN fragments | Done |
| B.1–B.4 | listable GUI (magenta, icon, columns, phase), config + friendly migration + combos, enrich (before the exoplanet regex), object card with explain_hads + chips | Done |
| C.1–C.3 | hads projects (reported_aavso), ES/EN narrative citing Wils/VVS/VSX, post/tweet | Done |
| D.1–D.5 | plan block (2P, cadence ≤ P/12, checklist), CCDciel sequence (advisory window), Follow-up + process (FotoDif/WebObs), phase folding (widget + PNG) | Done |
Status: unit suite green (989) + network functional green (test_hads_live). Out of this iteration: the native live monitor (parked — FotoDif AUTO already covers live watching); the generalisation to long-period variables and campaigns (agreed model: campaign = orthogonal project attribute + variable kind) is the next track, on its own branch (see "Generalización futura" in the master plan).
7quindecies. Track V — long-period variables and campaigns (2026-09-11, ADR-035)
Plan: docs/PLANS/variables-campaigns.md (master) + docs/PLANS/variables/fase-*.md (40 subplans). Branch feature/variables-campaigns.
New variable kind (the multi-night Track B mould; HADS was the exception) and a campaign entity, orthogonal 1:N (migration v7), taken from the obsSN group's real campaigns (WeSb 1, T CrB). Tonight gains the local campaigns phase (due only, V-d); the object card resolves VSX (the vsx.aavso.org subdomain — www sits behind Cloudflare) → SIMBAD → manual; extremum prediction from the VSX epoch; event advisor (dip/eruption, configurable threshold); ZTF/ALeRCE survey context in the light curve (closes the B12 option); CSV + AAVSO EFF reports with in-app HJD (Schlyter Sun).
| Sub | Deliverable | Status |
|---|---|---|
| V0.1–V0.9 | core/variables.py, core/campaign.py, core/sources/vsx.py (TTL 7 d), core/sources/surveys.py (ALeRCE ZTF, TTL 30 d), migration v7 | Done |
| VA.1–VA.4 | campaigns planner phase (due + visibility), variable sub-dict, campaign scoring, ES/EN fragments | Done |
| VB.1–VB.6 | listable variable GUI, VSX→SIMBAD→manual card, epoch folding, contexts | Done |
| VC.1–VC.10 | variable projects, campaign manager (create/edit/close, targets, attach), hub filter, Follow-up | Done |
| VD.1–VD.9 | generalised Tonight chip + event warning, plan tab, multi-filter sequence, photometry_export.py (CSV + AAVSO EFF), export dialog, survey button, narrative, folded post chart | Done |
| VE.1–VE.2 | full i18n (714 strings, 0 unfinished), documentation close (this track) | Done |
Status: unit suite green (1096). Out of this iteration: eclipse hour windows, campaign sharing, AAVSO Alert Notices, ASAS-SN Sky Patrol, live monitor.
Next track: the usability audit (2026-09-13) found Track V's features correct but hidden (the campaign manager is a selector with no detail view) and the project flow stacked two navigation models (tabs + wizard). The fix is its own track — plan written, pending execution on the independent branch feature/campaigns-ux: docs/PLANS/ux-variables-campaigns.md (master) + docs/PLANS/ux/fase-*.md (23 subplans: the Campaigns fifth tab, bidirectional links, one gesture language, the project as a single checklist page with a Next-action card, and CCDciel moved to its own Observatory tab). Also pending, approved 2026-09-12: the featured-variables discovery add-on (predictable extrema + T CrB/R CrB watch lists).
7sexdecies. Track UX — variables & campaigns usability (2026-09-13, amends ADR-035 + ADR-019)
Plan: docs/PLANS/ux-variables-campaigns.md (master) + docs/PLANS/ux/fase-*.md (23 subplans). Branch feature/campaigns-ux (independent: no merge-back into the feature chain).
The usability audit (2026-09-13) found Track V's features correct but hidden: the campaign manager was a selector with no detail view, no mention of a campaign was a link, the gesture language differed per list, and the project flow stacked two navigation models (tabs + wizard). The track delivers: the Campaigns fifth tab (master-detail: per-campaign health at a glance — members × cadence × events via the new campaign.status_report query — protocol, clickable URLs, members table; the modal manager retires and ADR-035 decision 9 is superseded); bidirectional navigation (project header badge, ⚑ Tonight card chip, per-project clickable cadence chips landing on Follow-up, member double-click → hub, History double-click → project/Explore); one gesture language everywhere (click selects · double-click/Enter opens · right-click menu · hand cursor); the project as a single page (Next-action card fed by project.next_action() + collapsible sections with plain-word state — the step tabs and wizard retire; ADR-019 amended, lifecycle model unchanged); the Observatory sixth tab (CCDciel control leaves the Plan step); and the fixes batch (no silent validation, network off the GUI thread via ResolveWorker/ SurveyWorker, event_mag_threshold in Settings, persisted campaign filter, stale detail cleared, CTA no longer closes on failure).
| Sub | Deliverable | Status |
|---|---|---|
| U0.1–U0.6 | fixes & feedback batch | Done |
| UA.1–UA.7 | Campaigns tab + links | Done |
| UB.1–UB.3 | gesture language | Done |
| UD.1–UD.5 | project single page + Observatory tab | Done |
| UC.1–UC.2 | i18n + docs close (this track) | Done |
Status: unit suite green (1146). Out of this iteration: campaign-level photometric export, campaign sharing, the featured- variables discovery add-on (approved 2026-09-12, its own track).
Next tracks decided (2026-09-16 — ADR-036 + ADR-037)
With the UX track closed, the review with the observer settled the final tab-bar layout and the two tracks that follow, in this order (decisions already documented; execution pending — all on branch feature/campaigns-ux, per the user's decision):
Signals & campaigns —
docs/adr/ADR-037-campaign-signals.md+ plandocs/PLANS/signals-campaigns.md. Executed in full on 2026-09-16 (SC1–SC5): see section 7septdecies below.Journal & Outreach —
docs/adr/ADR-036-journal-and-sunsky.md+ plandocs/PLANS/journal-outreach.md. History leaves the tab bar → "Observing journal…" dialog under the Tools menu (auto-generated derived view, grouped by observing night; the bar keeps 5 tabs: Tonight · Projects · Campaigns · Sun & sky · Observatory) and Solar → "Sun & sky", the outreach tab with a "Render PNG for socials" CTA.
7septdecies. Track SC — Signals & campaigns (2026-09-16, ADR-037)
Plan: docs/PLANS/signals-campaigns.md. Branch feature/campaigns-ux (all the work of this era lives on this branch, per the user's call).
The review with the observer found two gaps: the Campaigns tab was functionally "the hub filtered by campaign", and variable-star events were barely visible (an event in an up-to-date project never showed in Tonight — due_campaigns filtered before evaluating). The track delivers: the triple listing condition (due or detected event or imminent extremum campaign_extremum_days, with campaign.project_signal() as the single source of truth — SC1); the signals console on the Campaigns tab (signals_report: ⚡ events first, ⏳ extrema by arrival, "N of M up to date" coverage — SC2); the extremum chip on Tonight rows (VSX epoch convention — SC3); and the relevant variable-star events over two channels (SC4, re-scoped with the observer the same day): the automatic vigils (core/vigils.py, editable curated T CrB/R CrB list, latest ZTF/ALeRCE magnitude against baseline under its own 12 h cache — never the 30-day context one) and the AAVSO editorial channel (core/sources/aavso.py: forum alerts — Discourse serves native JSON — and the active campaigns parsed off the app; spike validated before writing). All under the fusion rule (SC-g): a signal about a star that already is a project joins its listing reasons, never a duplicate row.
| Sub | Deliverable | Status |
|---|---|---|
| SC1–SC3 | triple condition + signals console + extremum chip | Done (fcdf443, f96b2e6, 50bdf58) |
| SC4a | vigils: core/vigils.py, planner phase, fusion, 👁 chip, cache-only console, Settings editor | Done (24 tests in test_vigils.py) |
| SC4b | AAVSO channel: spike (Discourse JSON + campaigns table viable), core/sources/aavso.py, planner phase, fusion, 📣 chip, Settings checkbox | Done (17 tests in test_aavso.py with real spike titles) |
| SC5 | close: ES/EN i18n (809 strings, 0 unfinished), ADR-035 review (the tab's role), this section | Done |
Status: unit suite green (1244). Out of this iteration: ASAS-SN Sky Patrol; forum topic bodies (the title is enough for the Tonight row); ZTF cadence reminded in the object card (a vigil is a safety net, not an alert network).
7octdecies. Track JO — Observing journal & "Sun & sky" (2026-09-16, ADR-036)
Plan: docs/PLANS/journal-outreach.md. Branch feature/campaigns-ux (all the work of this era lives on this branch, per the user's call).
The History tab was dead by construction (observations had no writer since UX v3.1) and "Solar system" was a showcase with no flow. The track delivers: the observing journal — History leaves the tab bar (five tabs: Tonight · Projects · Campaigns · Sun & sky · Observatory, Ctrl+1..5) for the Tools menu, rebuilt as an auto-generated derived view (core/journal.py: UNION of projects/visits/files/photometry/ campaigns + legacy marks, grouped by observing night noon-to-noon local, with a kind filter, search and project/Explore jumps); the observed-feedback rewiring to project activity (project.activity_for: scoring novelty and the ✔ column — now "Covered" — read the project world, not the orphan table; commitment rows — campaign/vigil/AAVSO — are exempt from the decay); and the "Sun & sky" tab as the outreach home: the "impact on your night" line (Moon → faint targets, Kp → auroras) jumping to Tonight, the "Render PNG for socials" CTA (sun_panel.draw_sun with the observatory watermark, shown in the ChartViewer) and the bilingual "sky post" draft (narrative.sky_draft + copyable dialog).
| Sub | Deliverable | Status |
|---|---|---|
| J0–J2 | journal in the menu + derived view + dialog/CLI | Done |
| J3 | score/✔ rewired to project activity | Done |
| S1–S3 | "Sun & sky" + impact + social PNG + sky post | Done |
Status: unit suite green (1272), i18n 825 strings 0 unfinished. Noted future idea: a "night chronicle" generated from the journal (a post draft of what the session did). Pending live validation with the user's token: the AAVSO bright-vigil path (see 7septdecies).
7noovies. Track VU — survey report & inline curve (2026-09-17, ADR-035 addendum)
Branch feature/campaigns-ux (natural continuation of the B12 option that ADR-035 §7 closes).
The Follow-up survey button was deliberately silent: click → nothing visible (no points, no confirmation), and on top of that the paste path had stored wrong dates (a paste with YMD integers had written 4 rows in a real project with MJDs four years in the past — fixed and repaired in the DB). The track delivers:
Report, never silence (
surveys.fetch_points_detailed): the worker returns{status: ok|empty|error, points, error}and the GUI always reports in a human voice: ok → «%1 new, %2 updated, %3 unchanged, %4 removed; MJD min → max; bands (ZTF via ALeRCE)»; empty → «No survey data for this position»; error → «Survey download failed — <reason>».Re-click = real re-query:
force=Truealways; the 30-day cache does not hide the click.fetch_points(the vigils' path, ADR-037 SC4a) keeps returning[]silently: a watch must not take the app down.Idempotent, protected upsert (
followup.upsert_survey_points): keyed by(mjd, filter); only ever touches rowssource LIKE 'survey:%'(never paste/manual/file/ quicklook) and only removes orphansurvey:%rows when the result is non-empty: an empty answer never wipes stored photometry. Onlysn/variableprojects; anything else gets an explicit «Cannot store survey points: <reason>».Inline curve on Follow-up (
gui/widgets/lightcurve_widget.py): human labels per source («Manual entry», «Pasted data», «From file», «Quick-look · indicative», «Survey · ALeRCE/ZTF»);snprojects withsn_type: SN template folded to the peak (toggle ON by default);variable: period fold + sawtooth; link lines ON by default (manual = solid, quick-look/survey = dashed — the same semantic convention as the PNG, only the label wording differs).One payload, one place (
core/lightcurve_data.py::build_payload): the inline widget,post.py(PNG, viaviz/lightcurve_view.py) andoverview.pyshare the build (points,sn_typepriority, peak, folds, sawtooth); theTypeErrorcrash whenminwasNone(overview branch) is fixed in the common spot (min guard).Paste with feedback: preview with a human date and a «⚠ date outside 1966–2086 — check» warning for out-of-band MJDs (preview only; the save path is unchanged), and the
core/photometry_import.pyparser no longer confuses YMD integers with MJDs (20260909 → 61292instead of58107), accepts JD (minus 2 400 000.5) and plain MJDs, and returnsNonewhen it cannot decide (no exception).
| Sub | Deliverable | Status |
|---|---|---|
| VU.1 | paste parser YMD/MJD/JD fix + pid 18 row repair + tests | Done |
| VU.2 | fetch_points_detailed + worker + ok/empty/error report | Done |
| VU.3 | upsert_survey_points (idempotent, protected, kind-gated) + tests | Done |
| VU.4 | inline curve (labels, template toggle, link lines) + shared build_payload | Done |
| VU.5 | paste warning + ADR-035 addendum + this section | Done |
Status: unit suite green (1293): 17 new tests (test_lightcurve_data.py 10, test_surveys.py +4, test_lightcurve_widget.py +3) and 3 tests updated to the dict payload (test_workers.py ×2, test_projects_hub.py). Out of this iteration: automatic download on project creation (the manual button remains the entry point), survey points in the AAVSO view (the survey:% rows stay reference-only, never uploaded to AAVSO).
7vigies. Track UX-PC — Projects & Campaigns that say "Ohh" (2026-09-17, ADR-038)
Plan: docs/PLANS/ux-proyectos-campanas.md (Spanish). Own independent branch feature/ux-projects-campaigns (branched off feature/campaigns-ux cb457e6; merge target decided at track close).
The 2026-09-17 usability audit (the observer: "nothing to fault functionally, but the UX is crowded; without a manual it isn't intuitive — I want Projects/Campaigns to make you say 'Ohh, that's what I need'") delivers: the "Needs your attention" dashboard as the Projects right pane with no selection (3-5 cards with the reason in plain words and one button landing on the exact section; calm/empty states; source: the new, purely-local, additive core/attention.py); rich rows in both tabs (kind band, next action in words, progress dots, "up tonight HH:MM–HH:MM" chip via planner.safe_window_for, activity age, sparkline of your own measurements for SN/variables, the "Needs you" order; since 2026-10-01 the thumbnail is the project's latest available curve (the newest run, be it the series or an EXOTIC reduction), framed in the chart's own magnitude window); the Next card as the step machine's command center (Mark done/Skip beside Go →; sections keep only a discreet footer); three-level action prominence (primary visible / ⋯ menu / collapsed block with a plain-language title — Calibration and "What you kept from the session" start closed; photometry tools behind one ⋯), sharpened on 2026-10-06: in Photometry and Astrometry the primary is a big button painted in the object kind's colour, with a line saying what it will do, and every knob in closed blocks (ADR-038 rev), and everything, the results included, in bordered cards (a group without a border does not show where it ends); the project lifecycle consolidated into the header ⋯ menu; the CCDciel live control completing its move to Observatory ("Live capture" group — Send plan reads the selected target's saved plan); Campaigns as the war room ("Happening now" — renamed from "Signals" — with full-sentence ⚡/⏳/👁 rows, ●●●○ health cards, detail-header actions with real enablement, "Finish" → "Close"); and the plain-language rule with ⓘ helps (what a campaign/project is, tooltips on all five tabs, teaching empty states). No feature is lost: everything is relocated.
| Sub | Deliverable | Status |
|---|---|---|
| U1 | Projects foundation: Filters ▸ + ⋯ manage menu; Refresh/Campaigns…/footer retired | Done (35f5a34) |
| U2 | core/attention.py + dashboard + rich rows + sparkline | Done (6145880) |
| U3 | Next card command center + collapsed advanced blocks + CCDciel → Observatory | Done (1a08e2e) |
| U4 | Follow-up decluttered (⋯ tools + collapsed block) | Done (0b5f5b3) |
| U5 | Campaigns war room + ⓘ helps + "Close" | Done (a0e09f7) |
| U6 | plain language + i18n ES/EN (876 strings) + ADR-038 + this section | Done |
| U7 | "Happening now" explains itself: scope line + ⓘ icon legend, "Up to date: N of M campaign projects" (+tooltip), no-campaign guide, warm ✨ calm state | Done (2026-09-17) |
Status: unit suite green (1299), i18n 876 strings 0 unfinished. Metrics: Projects with an SN open drops from ~20 to ≤10 visible actions; Campaigns from 19 to ≤4. After the merge with feature/campaigns-ux (Track VU, same day): suite 1320, i18n 885 strings 0 unfinished. After U7: suite 1322, i18n 890 strings 0 unfinished.
7vigprim. Track SC2 — Sky calendar & Jupiter's moons (2026-09-17, ADR-040)
Plan: docs/PLANS/sky-calendar.md (Spanish, with the validation table). Own branch feature/sky-calendar (branched off feature/ux-projects-campaigns).
The solar system becomes an event source: the "Sun & sky" tab leaves the bar (four remain: Tonight · Projects · Campaigns · Observatory, Ctrl+1..4) and all its content moves intact into the Tools-menu "Sky calendar…" dialog, enriched with new sections: next events (60 days) in plain words, the Moon calendar, the planets, and Jupiter's moons this week. The engine (core/skyevents.py, 100 % local) covers true-syzygy phases, perigee/apogee, Moon–planet and planet–planet conjunctions, oppositions and Sun conjunctions as ecliptic-longitude crossings (the Saturn 2026-10-04 lesson: elongation peaks at 177.28 deg with beta≈−2.7 deg), greatest elongations, likely eclipses (honest label) and meteor showers. core/satellites.py adds the Galilean moon and shadow transits for the observer's site — IAU WGCCRE phases + empirical calibration (phase_cal_deg) — validated against 22 Horizons q12 windows, all within ±10 min (worst 9.6), with the "±10 min" label visible. Titan is documented as out of season (~2040). In Tonight, up to 3 event chips (big things first, satellites only when observable, click → the dialog).
| Sub | Deliverable | Status |
|---|---|---|
| SC0 | skyevents engine + longitude-crossing fix (Saturn) + 16 regression tests | Done (f06573a + 46acd46) |
| SD | core/satellites.py + 21 tests + functional, Horizons validation | Done (9060ca9) |
| SC1 | dialog + intact solar content + 4-tab bar + retargeted tests | Done (27fd9da) |
| SC2 | Tonight chips + 5 tests | Done (4756416) |
| SC3 | i18n ES/EN (940 strings, 0 unfinished) + ADR-040 + ADR-036 review + this section | Done |
Status: unit suite green (1366). Out of this iteration: Horizons refinement of satellite phenomena (~1 min, spike first); eclipse contact times; stellar occultations; the "impact" line still reads only Moon+Kp (events could feed it in v2).
Photometric series quality (2026-09-29)
Why: a real observation from the ObSN group (V0526 Per, 244 frames of 40 s, no WCS) came out catastrophic with the series engine: the field drifted 134" in 2.9 h, the star left the aperture in under a minute and the curve came out across 8 magnitudes with 0.5 mag errors. The full diagnosis (with the before/after numbers) is in docs/PLANS/series-quality.md.
What was done (phases A, B and C of the plan; ADR-048 revised and ADR-054):
core/register.pyrewritten: the sky is removed, the stars vote the transform (translation first), quality is physical (matched stars and rms in px) and a rotation only enters when it removes real residual. A frame that cannot be verified inherits the previous one and is flaggedalign_failed.Alignment is on by default (
align="auto"->coords: measured on the native grid, the PSF is never resampled) and is persisted incfg_json.The zero point is tied per comparison star (
_tie_comps): a comp that comes in and out of the frame, or that saturates, no longer moves the curve. The check star never enters the zero point.The aperture follows the seeing in a series and, for
variable/hads, the default detrend is the airmass minimum.The engine says what it does not know in the panel: drift, star residual, comps that never entered or are saturated, and the missing gain.
core/periodogram.py+viz/phase_view.py+gui/phase_dialog.py: period search (generalised Lomb-Scargle + PDM + spectral window, bootstrap FAP), folding and the two-panel report, with cycles covered and alias warnings. Reachable from the visit window and from the editor's Measure tab.
Numbers: 244/244 measurable points (was 5), 0.899 correlation with the observer's independent reduction (was 0.32), 0.0094 mag residual (was 0.13), 0.23 s/frame (was 4.6 s and not converging), unit suite green: 2105.
Following phases (done the same day, see ADR-048 rev.):
G1/G3 · The gain:
core/gain.pymeasures it on the frames themselves (0.772 ± 0.002 e-/ADU on the real series) and Settings gains the two fields nobody could fill. A point's internal error goes from non-existent to 0.0052 mag.A · The chart: robust scale, zoom/pan, bar = photon and band = systematic (
err_internal, migration v13), data flags separated from calibration caveats, and controls in the series block.B · Robustness: the aperture really follows the seeing (the radii belong to the reference FWHM), a
seeingflag againstcloud, and a robust analysis (clip on the folded curve; the clip on the time series was measured and rejected).C · Comps:
validate_on_platemeasures every candidate on the open plate (saturation, linearity, sensor, SNR) and the field is the sensor's real rectangle.D · The curve: AAVSO's
fetch_lightcurve, joint folding, and a bounded search so a decade of history cannot freeze the window (from 60 s to 3-4 s).
Entry point (for whoever picks this up):
G2: the "Measure the gain…" dialog (two pairs, with the read noise) on top of
core/gain.py, which already does the two-level fit; only the window is missing.seeingin the analysis: the per-night zero-point offset in the multi-night fold (the plan's D3) is still open.Declared debt: the AAVSO curve package has no network test; its parsing (
_parse_curve) is covered by the priority-chain tests.The fixture
tests/data/v0526per/(8 cropped real frames, 4 MB) is the safety net: do not replace it with synthetic data.
7novies. Interfaz 1.0: the project hub (2026-10-01, ADR-055)
Motivation: the app was born Tonight-first and grew into a project manager; the order had inverted. Interfaz 1.0 puts the project at the centre without touching any widget's interior.
| Phase | Deliverable | Status |
|---|---|---|
| 0 | Navigable mockup docs/mockups/interface-1.0.html (validated) | Done (2026-10-01) |
| 1 | Shell: vertical "PROJECTS" tab + QStackedWidget (Home · Tonight · Campaigns · Detail), overlay drawer, Home startup offline | Done (bdb69cc) |
| 2 | Welcome = inline wizard (welcome_tab.ui + welcome_setup.py), start-view decision, blocking Data step once per version | Done (18fd38b) |
| 3 | Tonight on demand (_maybe_compute_tonight), embedded search (new_project_bar.py) and manual form (manual_object_panel.ui) | Done (a256ffe) |
| 4 | Embedded UFE: UfeDialog from QDialog to QWidget, shell page with a back bar and shutdown() on leave | Done (bf3082e) |
| 5 | Home alerts: "What's up in the sky" and "Due for a revisit" bands; campaigns as a hub strip | Done (3b51ba5) |
| 6 | ADR-055, amendments to ADR-019/044, i18n ES/EN (2058, 0 unfinished), docs | Done |
Entry point: gui/main_window.py (_build_shell, _show_welcome, _ufe_page) and gui/widgets/ (welcome_setup.py, new_project_bar.py, manual_object_panel.py). Still to polish: campaigns keep their own view (the Home strip only links); the other dialogs (Settings, Sky calendar, Journal) are still windows.
7decies. Navigation with history (2026-10-01, ADR-056)
Motivation: after Interfaz 1.0 there was no global "back". A stack of locations (view, project, tab, campaign), a navigation bar with breadcrumbs (gui/main_window.py + ui/main_window.ui), Alt+←/→/Home shortcuts, mouse side buttons and a permanent Welcome entry (bar, Help and drawer) are added. Creating a project replaces Tonight; the update gate blocks "Back"; action_log moves to Help.
Entry point: navigate/back/forward/home/_apply_location in gui/main_window.py; tests in tests/unit/test_navigation_history.py.
7duodecies. Interfaz 1.4: the Welcome redesign (2026-10-01, ADR-055 rev.)
Motivation: Welcome worked but did not convince. The hero was a gradient with half a panel of dead space, the targets step was a wall of checkboxes and the forms asked for a latitude without ever answering back, so nothing rewarded the first bit of input.
| Piece | Deliverable | Status |
|---|---|---|
| Hero | assets/welcome_sky.svg (our own art) + gui/widgets/welcome_sky.py, which paints tonight's real Moon from ephem_minor.moon(jd) and twinkles a few stars | Done |
| "Your night, now" | Live strip, 100% local (coords.tonight_window, ephem_minor.moon/planet): darkness window, Moon phase and set, planets at dusk; recomputed on every site change | Done |
| Rail | The stepper connector fills and the steps behind you get a tick | Done |
| Targets | Kind cards in KIND_COLORS, clickable as a whole, one-glance blurb with the full text in the tooltip (_setup_kinds(..., card=True)) | Done |
| Data | Receipt with one mark per line | Done |
| CTA | Subtitle plus "Explore first" (blocked while an update gates) | Done |
| Motion | ui_animations preference (Settings ▸ Interface), one-shot fade, timer stopped when hidden | Done |
Entry point: gui/widgets/welcome_setup.py (night strip, rail, cards, fade), gui/widgets/welcome_sky.py (the painted hero), gui/wizard.py (_setup_kinds card mode, _setup_data receipt), gui/ui/welcome_tab.ui and the Welcome block of gui/theme.py. A first run now lands on the observatory step; the page fits a 1280x860 window and one outer scroll covers short screens.
7terdecies. Interfaz 1.5 bis: the site, on a map (2026-10-02, ADR-055 rev.)
Motivation: the observatory step asked for a latitude from someone who knows where they live but not their coordinates, and on an update the screen still told a veteran to "set up your observatory in three steps".
| Piece | Deliverable | Status |
|---|---|---|
| The map | gui/widgets/site_map.py: equirectangular, click to pick, wheel zoom, drag pan, live day/night terminator (local ephemeris), marker and cursor readout. Cover projection, so no side bands | Done |
| Map data | assets/world_map.json (Natural Earth 110m coastlines + borders, public domain) | Done |
| Naming a point | core/places.py + assets/world_places.json (1251 cities, Natural Earth 50m): nearest within 250 km, else the coordinates | Done |
| Settings | "Pick on the map…" opens gui/site_map_dialog.py, sized to a share of the screen (it used to open at its sizeHint, a 300x150 map) | Done |
| Names | Written every time, from the MPC code, from the geolocation and from a map click | Done |
| Update notice | The version badge moves next to "Your data, safe"; the hero explains the update, the rail marks 1 and 2 as done and the single action lives with the report | Done |
Entry point: gui/widgets/site_map.py (the map and its projection), core/places.py (naming), gui/site_map_dialog.py (the Settings dialog), gui/widgets/welcome_setup.py (set_context, _map_picked) and the Observatory step of gui/ui/welcome_tab.ui.
7quaterdecies. Interfaz 1.6: the list and the project, together again (2026-10-02, ADR-055 rev.)
Motivation: splitting the list and the project into two shell pages cost more than it bought. Opening a project threw the list away, switching projects was a round trip, and the list screen (74 px rows, mostly empty) said very little.
| Piece | Deliverable | Status |
|---|---|---|
| One page | VIEW_PROJECTS: list + project side by side, QSplitter between them. VIEW_HOME/VIEW_DETAIL are two names of the same index; the history tells them apart by the pid | Done |
| List width | Draggable and remembered (projects_list_width, 260–560), double click on the handle resets, « folds it | Done |
| Dense rows | 54 px, kind hue as the row's left edge, selection tinted with the object's colour, context numbers on line 2, and the row drops what does not fit | Done |
| Sky bar | gui/widgets/sky_bar.py in the navigation row: the Moon at its real phase, the darkness window, the planets and the event chips, in every view | Done |
| Resting pane | gui/widgets/night_panel.py: the night (same painted sky as Welcome) and the way in to a new project | Done |
| One source | core/night_brief.py: the numbers and the words of the night, shared by the three surfaces | Done |
| Header | Campaigns becomes a header button; the ~105 px of the two bands return to the list | Done |
Entry point: _build_shell (the splitter and the merged page), _sync_projects_pane, _restore_list_width / _apply_list_width in gui/main_window.py; gui/widgets/project_row.py, sky_bar.py, night_panel.py and core/night_brief.py.
7quindecies. Interfaz 1.7: the project tabs stop scrolling (2026-10-02, ADR-041 rev.)
Motivation: the Object card and Analysis had grown past the fold. With a project holding a visit, its frames and a measured curve, the card asked for 499 px and Analysis for 1058 in a 537 px viewport, and the curve's own legend printed on top of its axis labels.
| Piece | Deliverable | Status |
|---|---|---|
| Chrome | The four small buttons and the context line move into the masthead; the "Next" box is a slim band; a page no longer repeats its tab's name (195 -> ~120 px in every tab) | Done |
| Analysis | Visits and curve side by side (master-detail), the list sized to its content instead of a fixed box | Done |
| Charts | ChartView.sizeHint() (640x260) instead of the scene's size, and the light curve's legend geometry derived from its own font | Done |
| Object card | The parameters block carries its "In depth" switch in its header, the table is as tall as its rows, and the chips stop stretching | Done |
| Criterion | tests/unit/test_project_tabs_layout.py: neither tab asks for more than the viewport at 1360x940 and 1360x860 | Done |
Entry point: _section_layout, _build_analysis_tab, _analysis_curve_block in gui/main_window.py; gui/widgets/base_chart.py, gui/widgets/lightcurve_widget.py, gui/widgets/visits_panel.py, gui/overview.py and the masthead of gui/ui/projects_tab.ui.
Polish (2026-10-02, same day). Two things the first pass got wrong:
The visit window covered the workbench. It is a non-modal dialog with a parent, so the window manager keeps it above the main window; with the UFE now being a PAGE, opening a plate from a visit left the editor behind it. It now steps aside while the workbench is on screen and comes back when it leaves (
_ufe_enter/_ufe_leave). See ADR-045.The list could not show the curve. The thumbnail lived in its own right-hand column and stole 106 px from the line where the object's numbers live, so at the default width the two never fitted together. It rides on the first line now, next to the name; the numbers show when they really fit (measured with the real font, never clipped) and the default list width is 500, where the curve AND the numbers fit.
7septendecies. Interfaz 1.8: the night, drawn in the project tabs (2026-10-02, ADR-041 rev.)
Motivation: the three project tabs were correct and told you nothing at a glance, the same complaint the Welcome view had before Interfaz 1.4.
| Piece | Deliverable | Status |
|---|---|---|
| The band | gui/widgets/night_ribbon.py: dusk → dawn with the object's altitude arc, the Moon at its real phase, the "now" marker and coloured blocks on the timeline. One widget, three readings | Done |
| Object card | Two columns (parameters 60% / band + charts 40%), the table as a definition list, the facts in one flowing line, "In depth" on by default | Done |
| Capture | One "Telescope and camera" panel instead of three group boxes, the status in one row, and the plan drawn on the night with its verdict | Done |
| Analysis | The visit's frames drawn on the night they happened, above the visits | Done |
| Colour | The connection chip (green/amber) and the status values tinted by meaning | Done |
| Criterion | All four tabs fit the viewport at 1360x940 and 1360x860, Capture included with CCDciel connected | Done |
Entry point: gui/widgets/night_ribbon.py (the band), _plan_ribbon_refresh and _analysis_ribbon_refresh in gui/main_window.py, _refresh_ribbon in gui/overview.py.
7duodevicies. The card in two columns, and the card as the landing page (2026-10-02, ADR-057 rev. + ADR-041 rev.)
Motivation: the dossier stacked the parameters and, under them, the charts, and opening a project landed on Capture (the Next card's target) instead of on the card that says what the object is.
| Piece | Deliverable | Status |
|---|---|---|
| The row | Parameters left, charts right, 50/50 (row_body); the night band stays full width above and the CTA below | Done |
| The collapse | The columns are widgets and follow their block: with no charts the parameters take the whole width, and the other way round | Done |
| The stack | Below 660 px of panel the row turns vertical (the charts have a 300 px floor) | Done |
| The 50/50 | The SectionCard labels wrap: the grid's minimum fell from 448 to ~180 px, which is what the equal split needed | Done |
| The landing | Opening a project always lands on the card; the Next card's Go still jumps to the step and a double click still goes straight to the work. An in-place refresh (land="keep") and the step machine (land="next") keep their own behaviour | Done |
Entry point: gui/ui/object_panel.ui (row_body), _sync_body_columns and _apply_body_direction in gui/overview.py, _build_project_page in gui/main_window.py.
7undevicies. The hero's Moon, and an observatory you can see (2026-10-02, ADR-055 rev.)
Motivation: the observer reported that the Moon phase looked wrong on Welcome and on the projects view, and asked for the drawing to include an observatory.
| Piece | Deliverable | Status |
|---|---|---|
| The Moon | gui/moon_icon.py had the sign convention inverted, so the sky bar, the night ribbon, Tonight and the sky calendar drew every phase MIRRORED while the hero's own painter drew it right: two Moons on one screen. Fixed, and a new test compares both painters at the same elongation | Done |
| The crescents | The bundled photo is a full moon with a dark left limb and the maria on that side: a waning crescent came out ~5x dimmer than a waxing one (0.7% vs 2.5% of the disc at -30/+30). A flat wash over the lit area evens them out | Done |
| The observatory | It was vector in welcome_sky.svg and the "cover" fit cropped it away (only the dome's edge survived in an 820x170 or 1290x206 hero). The SVG is a pure sky now and the dome is painted by welcome_sky._paint_observatory, sized from the hero, bottom right under the Moon, with the warm lamp in its slit | Done |
Entry point: gui/moon_icon.py (the sign and the wash), gui/widgets/welcome_sky.py (_observatory_geometry, _paint_observatory), assets/welcome_sky.svg (no observatory, no ground).
7vicies. The galaxy, the icon and the invitation (2026-10-03, ADR-026 rev. + ADR-055 rev.)
| Piece | Deliverable | Status |
|---|---|---|
| The galaxy | The band was five hard-edged ellipses (steps) with the same star density as the empty sky (19% of the stars over 20% of the canvas, three in its core). Now: a radial-gradient glow, the Great Rift as a chain of soft dark ellipses, star clouds and 600 granulation stars in the band's frame (3x the density outside). No SVG filters: a blurred group cost 13.6 ms of 15, gradients cost 2.8 | Done |
| The icon | appicon.svg was gold on the navy tile, off the palette (the clash showed next to the blue wordmark on Welcome). Repainted in the accent blue with a near-white star core, and all ten rasters + the .ico + the .icns regenerated with QtSvg + Pillow. Guard: every colour in the icon is neutral or cold | Done |
| The invitation | The resting pane says it on the drawing: "Select a project" / "You will see its card, its plan and its curve. Or create a new one.", painted on the left half of the sky, click-through, with the create button still below | Done |
Entry point: assets/welcome_sky.svg (the band), assets/appicon.svg + installer/ (the icon), gui/ui/night_panel.ui + gui/widgets/night_panel.py (the invitation).
7undecies. Alerts dissolve into the list (2026-10-01, ADR-055 rev.)
Motivation: the "needs your attention" panel and the "due for a revisit" band repeated what every row already says (next action in words, urgency tint, days since the last visit). Both are retired; Home is header + sky band + list + campaigns strip. With the list on screen the side drawer adds nothing: the vertical PROJECTS tab hides on Home and only appears elsewhere. The back/forward arrows grow, and the UFE "Back" button is removed (the general stack covers it).
7unvicies. Interfaz 1.9: the equipment, one step more, and the classic code retires (2026-10-07, ADR-055 rev. + ADR-044 rev.)
Motivation: Welcome had three steps and was missing the one that decides the image scale; and the UFE had been the normal path for months while the three classic dialogs and their switch stayed in the code "just in case".
| Piece | Deliverable | Status |
|---|---|---|
| The equipment step | Welcome goes to four steps (where you are, equipment, targets, data). The new step asks for aperture, pixel, focal length, camera type and a camera preset, and it does NOT block: it comes with the app's defaults | Done |
| The scale answers | The "Your scale, now" strip computes the arcsec/pixel and says whether the sampling fits the seeing of a typical site (exposure.sampling), the same hook as "Your night, now" | Done |
| The binning | plate_scale applies the config's binning: a 2x2 doubles the scale, and its readers (planner, NEO exposure, ASTAP, EXOTIC, field of view, CLI) stop being off by that factor | Done |
| The cleanup | The classic blink dialog, SnAnnotateDialog, the classic chart with its SequenceWorker and the ufe_default switch are gone: the UFE is the only door. About 2,000 lines fewer | Done |
| The modal wizard | wizard.ui, maybe_run_wizard, _build and _page_ok are gone (dead code from Interfaz 1.0); the helpers that are used stay shared in gui/wizard.py | Done |
Entry point: gui/ui/welcome_tab.ui, gui/widgets/welcome_setup.py, gui/wizard.py, core/exposure.py, core/cameras.py; the cleanup in gui/main_window.py, gui/workers.py and gui/ui/settings_dialog.ui; tests in test_welcome_setup.py, test_wizard_site.py, test_exposure.py and test_ufe_integration.py.
7duodetricies. The guide and the website: one source, two renderings (2026-10-08, ADR-070)
Motivation: the user guide lived only in docs/user (inside the app) and the website/ folder was a hand-written landing, with another palette and a screenshot pipeline for the retired shell: it told a different story from the guide's. A clear website was asked for, in the application's style, explaining what the software is and what it lets you do, and being the guide as well.
| Piece | Deliverable | Status |
|---|---|---|
| The generator | website/tools/build_site.py: turns the README and docs/user into HTML, injects the theme's palette and the catalogue's chips, and is idempotent | Done |
| The landing | One page per language with the hero (the app's sky), the README's definition, the kinds as cards, the long chapter folded into an accordion and the chapter index | Done |
| The guide | One page per chapter per language, with a sidebar, previous/next, heading anchors and the "Why …?" boxes as callouts | Done |
| The guard | tests/unit/test_site_build.py: the converter, the byte-for-byte regeneration, the internal links and that the palette comes from the theme | Done |
| In the program | Help → User guide (web), in the OS browser; the in-app viewer stays the offline door | Done |
| Publishing | .github/workflows/pages.yml builds the site and publishes it to GitHub Pages (push to main or release/v0.1, and manual) | Done |
Entry point: website/tools/build_site.py, website/tools/site.css, website/tools/site.js, website/README.md; in the app, gui/main_window.py (on_guide_web) and gui/ui/main_window.ui; test in tests/unit/test_site_build.py.