Skip to content

PRD run: onboarding, unified Event, maintenance drift, mapper engine + logbook profiles - #51

Open
jeandavidt wants to merge 46 commits into
stagefrom
prd-get-started
Open

PRD run: onboarding, unified Event, maintenance drift, mapper engine + logbook profiles#51
jeandavidt wants to merge 46 commits into
stagefrom
prd-get-started

Conversation

@jeandavidt

Copy link
Copy Markdown
Contributor

Bundles the full PRD milestone run (branch prd-get-started, cut off stage). One focused commit per issue for bisectability; unit suite green throughout (592 passed / 4 skipped).

Milestones (all issues closed)

This session's slices (PRD-3 S6 → PRD-4)

Issue What landed
#46 Long-format lab profile reusing the wide resolve→submit pipeline unchanged
#47 Général logbook → Event; resolve_target picks smallest logical unit
#48 Target confirm UX: level heuristic (hint only) + explicit override + bulk-apply
#49 Per-equipment maintenance sheets → spanning maintenance Events
#50 Drift read-back endpoint + pure derive_before_after + "drift since last cleaning" panel

Guardrails

  • No dbo.Value writes (drift read-back reads via value_repository only).
  • No schema changes in the final slices → no migration scripts / version bump needed.
  • Tests: unit suite only (integration tests are known-broken per project policy).

Test plan

  • uv run pytest tests/unit/ → 592 passed / 4 skipped.

The Data Explorer force-refetched and reset its time range on actions that
didn't require it, causing slow plots and the date inputs snapping back to
defaults:

- Adding a stream called _invalidate_data_cache(), wiping the range-keyed
  cache and forcing a full reload of every already-plotted stream. Removed —
  the cache is keyed by (stream, start, end), so a new stream loads lazily
  while existing streams stay cached (incremental, not full reload).
- The date-input on_change callback called st.rerun() inside the callback,
  which races the widget commit and snapped the date back to its default.
  The callback now only clears the id-keyed annotation/event caches; Streamlit
  reruns automatically after the change and the range-keyed timeseries cache
  refreshes on its own.

Adds a red→green AppTest regression: adding a second stream must not re-fetch
an already-plotted stream.
…native zoom + brush)

Swaps the interactive scalar view on the Data Explorer from Plotly to ECharts
(streamlit-echarts), the slowest part of the old UX. Plotly's datetime x-axis
rendering and full-rerun-per-selection are gone; ECharts canvas gives native
dataZoom (slider + scroll/pinch) and a toolbox brush (rect / polygon / lineX).

The annotation / quality-flag / equipment-event workflow is preserved: the brush
round-trip keeps the old identity contract. A JS handler returns the brushed
(seriesIndex, dataIndex) list and resolve_brush_selection maps it back to the
same {x, y, obs_id, id} the buttons consumed from Plotly customdata.

- New pure module app/components/explore_echarts.py:
  build_scalar_echarts_option (option + series_index_map + overlay_rows) and
  resolve_brush_selection — all decision-bearing logic, fully unit-tested.
- _render_scalar_view wires st_echarts + the resolver; quality-code colours,
  annotation/event markArea+markLine overlays, and the summary table are kept.
- Campaign Story keeps the Plotly _build_scalar_figure (read-only, unchanged).
- Pin streamlit-echarts==0.4.0: 0.7.x moved to st.components.v2 and fails to
  import on Streamlit 1.55.
- Tests: new unit suite for the builder/resolver; the Plotly-coupled Explore
  AppTests updated to the brush-payload shape (component value is injectable via
  session_state["scalar_chart"]).

Browser verification of the live brush event still pending (AppTest can't run
component JS).
Adds the ability to spread scalar streams across several plots instead of
overlaying everything on one chart. Streams stay in the canonical
explore_active_channels/series lists (so the global export is unaffected); a thin
assignment layer (explore_plot_of) groups them per plot.

- "➕ Add plot" creates a plot and makes it the target for newly added streams;
  each plot block has an "Add here" button to retarget.
- Each active-stream chip gets a "Plot" selectbox to move it between plots.
- _render_scalar_view takes a key `suffix` so it renders once per plot without
  colliding Streamlit widget keys; one ECharts chart + brush per plot.
- Vector / matrix / image stay single-stream pickers (rendered once over all
  active streams — multi-plot adds nothing there).

Tests: new test_explore_multiplot.py (add-plot retargets + new stream lands in
target; chip move control reassigns). Existing lab AppTests updated for the
per-plot "_p1" key suffix.
…nnotation

Refines the Explore scalar UX per follow-up feedback:

- Plot-management zone: a badge per plot (click to make it the target for new
  streams, ✕ to delete). Deleting a plot reassigns its streams to the first
  remaining plot (streams keep their own ✕); the last plot can't be deleted.
  Replaces the per-plot inline "Add here" buttons.
- Selected points now render in a table (stream, kind, timestamp, value,
  observation) below the chart.
- Annotation is selection-driven: the sensor "Annotate selected points" button
  targets only the streams that have brushed points (not every channel in the
  plot), scoped to the selected points' span / pinned observation. Removed the
  two "view range" annotation controls — annotations no longer apply to the
  plotted range.

Tests: new test_explore_selection.py (selection table + scoped annotation), plot
delete in test_explore_multiplot.py; homogeneous-lab-dialog test updated to the
selection-driven button.
F4: get_equipment_installations queried the dropped EquipmentInstallation
table, 500ing GET /equipment/{id}/lifecycle. Repoint to EquipmentLocationHistory
(maps 1:1 to InstallationOut; date filters use ValidFrom/ValidTo).

F8: relocate_equipment/rewire_equipment/deploy_das now raise before any write
when the destination equals the current active row, instead of churning an
identical history row.

F12: drop the 5 dead SignalPort* NotImplementedError stubs (keep the live
find_signal_port_type_by_name shim); fix stale SignalPort docstrings in ingest.
The port a Channel is gated through was stored both in ChannelPortHistory
(temporal source of truth) and a denormalised Channel.SignalInterfacePort_ID
column that nothing kept in sync — classic drift risk (consistency audit F3,
decision D1=DROP).

Drop the column. The current port is now resolved from the active
ChannelPortHistory row through a new view, vw_ChannelResolved; every read that
needs the port selects FROM that view instead of the Channel base table, so the
equipment-resolution join predicates are byte-for-byte unchanged. Writes route
through channel_repository.set_channel_active_port — the single writer of the
CPH active-row invariant — which closes the prior active row before opening the
new one. That also fixes the POST /channels/{id}/port-history 500 on a second
post (it collided with UQ_ChannelPortHistory_ActiveRow).

Schema bump 2.0.0 -> 2.1.0 (pre-release, fresh install only; init.sql repointed,
DDL + docs regenerated). The channel CRUD form re-adds signal_interface_port_id
explicitly (form_specs) since it is no longer YAML-derived.

Reads repointed in: channel_repository, sensor_status_repository,
temporal_history_repository, vw_ChannelEquipmentAtTime, vw_ChannelStatus,
vw_DeviceStatus.

NOTE: channel_repository.py also carries the other agent's uncommitted
stream-pedigree WIP (it was dirty in the same file); reconcile separately.
Adds the detection layer for the original question (does the system notice when
a DAS moves and its equipment is left behind?):

- vw_DeploymentCoherence (F1): lists equipment whose active location Site
  differs from the Site its connected DAS is currently deployed to. A standing
  reconciliation report of DAS-move drift.
- get_das_move_equipment_conflicts(das_id, new_site_id) (F1): the equipment a
  pending DAS move would strand, so the move flow can offer to relocate them.
- get_active_campaign_deployment(equipment_id) (F13): the still-running campaign
  whose deployment placed an equipment, so reconfiguring it can warn that the
  campaign's deployment will be closed.

Schema 2.1.0 -> 2.2.0 (pre-release; init.sql repointed, DDL+docs regenerated).
Tests: tests/unit/test_deployment_coherence.py (6). Full unit suite 461 passed,
3 skipped (3 failing test_stream_pedigree.py are the other agent's untracked WIP).

F2/F9 from the plan were found moot: create_campaign_deployment establishes
campaign↔SP membership idempotently, so there is no non-member-SP path to reject.
Deferred to morning (needs browser verification): the F1c inline 'relocate these
too' UI in the DAS-move/campaign wizard, plus thin GET endpoints exposing these
helpers.
GET /lineage/streams/{id}/pedigree returns a stream's organizational/spatial
pedigree (distinct from its processing provenance): time-invariant identity plus
a time-bound deployment timeline — sampling location, process unit, site,
campaign, and responsible person per deployment segment.

- A sensor channel's location/campaign are historical (equipment is rewired and
  moved over time), so the timeline carries one segment per EquipmentLocationHistory
  it spanned (EWH×ELH temporal join, via vw_ChannelResolved). A lab series returns
  one fixed open segment. Optional from/to restrict to segments overlapping the
  exported window.
- Also fixes the missing Query/datetime imports in lineage.py that blocked
  api.main from importing.
- Regenerate OpenAPI spec for the new route.

(channel_repository.get_stream_pedigree shipped earlier in 86bd74f.)
Data download now produces one zip for all active streams: per stream a CSV
(UTC timestamps, value, plus annotation/equipment-event overlay columns and the
sampling-location/campaign active at each row's timestamp) paired with a pedigree
YAML; image streams embed their files. Two-step Generate → Download UX so the
heavy build (per-stream pedigree + image fetches) runs once, not every rerun.

Adds a page-level campaign filter: selecting a campaign snaps the time window to
its span and scopes the pickable streams (_scope_to_campaign), so both the plot
and the export stay within the campaign.

- New pure builder app/components/explore_export.py (UTC compare, overlay/segment
  matching, CSV shaping, image embedding) — unit tested.
- api_client.get_stream_pedigree wrapper (forwards the export window).
- Remove the old per-type CSV download buttons and now-dead flat-CSV helpers
  (+ their obsolete test); drop unused io/csv imports.
- Queries reconciled to the F3 vw_ChannelResolved refactor.
Introduce CONTEXT.md with canonical definitions: Stream, Provenance (the
processing-step lineage DAG) and Pedigree (the time-bound organizational/spatial
context — a deployment timeline). Disambiguates the two axes the export and the
existing Provenance panel each cover.
…acing

Batch 2 part 2 (F1c). Surfaces the detection layer landed in 6648ff3 so a
DAS move no longer silently strands its equipment.

- GET /das/{id}/move-conflicts?site_id= — equipment a pending DAS move would
  strand (active wiring to this DAS, active location at a different Site).
- GET /equipment/{id}/active-campaign — the still-running campaign whose
  deployment placed this equipment (reconfigure warning, F13).
- Campaign wizard: when a chosen DAS is active at another site, the existing
  conflict warning now also names the equipment the move would strand and
  points to Equipment Move to relocate them.
- api_client: get_das_move_conflicts / get_active_campaign_deployment.
- Tests: contract (both endpoints) + AppTest (wizard names stranded equipment).
- openapi.json regenerated. No schema change (endpoints/app only).
The generated CREATE script listed views alphabetically. Once F3 made
vw_ChannelEquipmentAtTime select from vw_ChannelResolved, the alphabetical
order created the dependent view first — SQL Server resolves view references
at CREATE time, so db-init failed with 'Invalid object name
dbo.vw_ChannelResolved' (exit 16, whole compose stack blocked).

- render.py (the live docs/init generator) and generate_sql.py (legacy, used
  by test_sql_generator) both now topologically order views: a view that
  references another is emitted after it. Dependencies are read from each
  view's SQL text (whole-word match), Kahn's algorithm, alphabetical tiebreak
  for a stable script. Raises on a cycle rather than emitting broken SQL.
- Regenerated v2.2.0_create_mssql.sql (view reorder only; SchemaVersion stamp
  preserved). db-init now exits 0 against the real MSSQL container.
- Regression test: every view is created after the views it references.
Make broken equipment/wiring links observable instead of silently NULL.

- F6: vw_ChannelLocationAtTime now passes through Resolution (equipment leg)
  and adds LocationResolution (location leg: resolved / no-location /
  no-equipment), so a NULL SamplingPoint from a broken chain is
  distinguishable from a genuinely absent location.
- F5: vw_UnlinkedChannels — raw channels (SignalInterface_ID NOT NULL) with
  observations but no active EquipmentWiringHistory ('N channels need wiring').
  Derived channels are excluded (deliberately unwired, not forgotten).
- F11: vw_InactiveParentReferences — active wiring rows still pointing at a
  soft-deleted (IsActive=0) SignalInterface/SignalInterfacePort.
- Endpoints GET /data-health/unlinked-channels and
  /data-health/inactive-parent-references (the latter filterable by
  interface/port) + api_client wrappers + a 'Data Health' report page.
- version 2.2.0->2.3.0; DDL+seed+docs+openapi regenerated; init.sql repointed.
  db-init verified exit 0 against the real MSSQL container.
- Tests: contract (2 endpoints), AppTest (Data Health page warn/clean),
  view-contract guards (F6 discriminators), and a version-description-length
  guard so an over-long stamp fails in unit tests instead of in docker.
The *History tables guard only one *open* row (filtered unique index); nothing
stopped a backdated ValidFrom from inverting the row it closes or landing
inside a closed [ValidFrom, ValidTo) interval, after which get_*_at_time can
match two rows or none and vw_ChannelEquipmentAtTime silently picks one.

- Shared _assert_valid_from_ok guard: before the close-and-open swap, reject a
  valid_from that is not strictly after the active row's start, or that falls
  inside any closed interval, for the entity. Wired into rewire_equipment,
  relocate_equipment, open_location_for_campaign, deploy_das, and backdated
  register_equipment_at_interface (a None start_time = 'now' skips it).
- All writes already funnel through these repos, so this is the right altitude
  (no trigger needed).
- Test: backdated overlap rejected before any write/commit; guard SQL shape +
  params pinned. Updated two existing repo tests for the extra guard SELECT.
…lete (F10, Batch 5)

Per decision D2, CampaignEquipment/CampaignSamplingLocation are the source of
truth for campaign membership; ELH.Campaign_ID is provenance only.
delete_campaign_deployment dropped the CampaignSamplingLocation link only when
an *open* ELH row still placed equipment there, so once the last placement was
closed the SP link vanished while closed ELH rows kept the campaign tag —
junction-derived scope and ELH-derived scope could then disagree.

- Remove the CampaignEquipment membership row first, then the active ELH
  placement (closed rows keep their provenance untouched), then drop the
  CampaignSamplingLocation link only if no *other* equipment still in the
  campaign has any placement provenance at that SP — junction-driven and
  agnostic to ELH open/closed state.
- list_campaign_deployments already drives from the junctions; no change.
- Test: delete removes both junction rows, the SP-link SQL is junction-based
  and carries no ValidTo coupling, CE is deleted before the SP-link check, and
  the no-SP path skips the link.
…, Batch 6)

A campaign's sites are now derived from its sampling-location membership
(CampaignSamplingLocation -> SamplingPoint.Site) rather than stored as a
single FK, so a campaign may span several sites. version 2.3.0 -> 2.4.0.
Regenerated create/seed scripts + reference docs from the dictionary; init.sql
repointed to v2.4.0; demo seed Campaign inserts drop Site_ID (membership comes
from CampaignSamplingLocation). Verified db-init exits 0 on a fresh install.
- campaign_repository: _campaign_sites() derives sites via CSL->SamplingPoint->Site;
  list/get expose site_ids/site_names lists; list filter uses junction EXISTS;
  insert/update/patch + overview watershed no longer touch Campaign.Site_ID.
- channel_repository: pedigree site_fallback derived from membership, used only
  when the campaign is unambiguously single-site.
- schemas: CampaignOut gains site_ids/site_names; CampaignIn/Patch drop site_id.
Campaign create payload + form_specs drop site_id (the wizard's chosen site now
flows only to sampling-location/DAS creation); campaigns list filter matches
against derived site_ids; campaign story shows the joined site_names list.
- test_campaign_multisite (new): derivation returns >1 site, list filter is
  junction membership not Campaign.Site_ID, insert/update never write Site_ID.
- repoint pedigree/contract/wizard tests to the derived site_ids/site_names and
  the campaign payload no longer carrying site_id.
…ep rerun

Selecting an equipment on the Equipment Deployments step triggers a Streamlit
rerun on which the step-1 site / sampling-location widgets aren't rendered, so
their live session_state keys get dropped — the step then read None and showed
'No sampling locations selected', losing the assignments.

Add wizard_helpers.snapshot_get (live state wins, else the step's persistent
snapshot) and use it for the cross-step site / SL reads on the deployment and
review steps. Unit test covers the dropped-key fallback.
The export CSV emitted the raw QualityCode id in the quality_code column. Map it
to the QualityCode name (via list_quality_codes) so rows read 'Accepted' /
'Suspect' etc.; fall back to the raw value for unknown ids. Annotation and
equipment-event kinds already export as their names.
Step 2 (Sites & Sampling Locations) now adds one block per site, each with its
own sampling-location multiselect; the campaign's locations are the union.
_selected_sls() resolves picks per block (SL ids are unique across sites; names
may collide), and deployment/review/execute all read through it. Review re-injects
earlier snapshots so its display survives the cross-step widget drop.

Tests: _selected_sls union/collision (unit) + AppTest add-site, multi-site
deployment list, and the equipment-selection-keeps-SLs bug repro.
…ection lists, Y-axis units

Addresses the regression where annotation + equipment-event controls became
invisible (they were gated behind a brush selection that's easy to miss / not
discover) and the Y-axis lost its parameter+unit label after the ECharts swap.

- Click-to-select: clicking a marker now selects that point (single-obs pin),
  returning the same (seriesIndex, dataIndex) shape as the brush so one resolver
  handles both. Reliable, discoverable path that doesn't depend on the toolbox brush.
- Equipment-event button is always visible again (events are equipment + time
  based, not tied to a point selection); the selected span pre-fills the dialog.
- Under-chart selection summary, next to the action buttons, updates live as you
  click/brush: left column lists the selected observations (stream, time, value,
  obs id) with the Annotate buttons; right column lists the selected equipment
  with the equipment-event button — so it's clear what gets annotated.
- Y-axis is labelled "parameter (unit)" again: the unit comes from the loaded
  time-series payload (the picker meta has no unit), rendered as a centered,
  rotated axis title like the old Plotly one.

Tests: Y-axis label + click-handler shape (unit suite); equipment-event always
available + selected-equipment list (selection suite).
Record the unified Event refactor (generalize EquipmentEvent → Event with
an eight-FK exclusive-arc target spanning both operational hierarchies +
Campaign; EventKind; Annotation repoint; maintenance drift as a derived
Channel). Add CONTEXT glossary entries: Event, EventKind, Annotation, and
the Measurement Range / Variable Range / Control Limit distinction.

Grounded in IWA Metadata Collection Ch3 (cause/effect annotation split,
calibration-curve history) and Ch5 (sensor QA vocabulary, controlled-
reference requirement for trueness). Pre-release: no migration script.
…arc + seed EventKind vocab (#35)

- New Event table: 8 nullable FK exclusive arc (Channel, Equipment,
  SignalInterface, DataAcquisitionSystem, SamplingPoint, ProcessUnit,
  Site, Campaign) with CHECK CK_Event_ExclusiveArc (exactly-one non-NULL)
- New EventKind table: 15-entry seed vocab (Calibration, Cleaning, Repair,
  PartReplacement, Replacement, SoftwareUpdate, Validation, Verification,
  VisualInspection, Commissioning, Decommissioning, OutOfService,
  PowerOutage, ControllerCrash, OperationalChange)
- EquipmentEvent + EquipmentEventKind moved to schema_dictionary/deprecated/
- Annotation.EquipmentEvent_ID FK repointed → Event.Event_ID (col rename in S3)
- app/components/form_specs.py: load EventKind YAML for equipment_event_kind slug
- DDL regenerated as v2.5.0; sql/init.sql updated; version.yaml bumped to 2.5.0
- 7 new red→green unit tests in tests/unit/test_event_schema_s1.py
- Pre-release: no migration script (fresh install only)

Closes-related: #35
… with exclusive-arc validator (#36)

Add Pydantic schemas (EventIn/Out, EventKindIn/Out) with @model_validator
enforcing exactly one of eight arc-target FKs; event_repository with full
CRUD for both Event and EventKind; FastAPI routers /events + /event-kinds
wired into the v1 protected router. 16 unit tests, all green.
…copied to deprecated/ in S1, not deleted); regen DDL
…; form-coverage green (#38)

- api_client: add list/create/update/delete for /event-kinds and /events + list_event_kinds_lookup
- form_specs: add 'event_kind' slug pointing to EventKind YAML
- app/pages/event_kinds.py: new CRUD page using render_crud_page + get_form_fields('event_kind')
- app/pages/events.py: new Event list/create/delete page with arc-FK target picker
- Home.py: new 'Events' nav section wiring both pages; equipment_event_kinds.py preserved
- test_form_coverage.py: add EventKindIn contract for 'event_kind' slug
- test_event_app_s4.py: red→green smoke tests for new form spec + page imports
… follow-on (#34)

- Add dismiss button that sets onboarding_dismissed in session_state and reruns
- Panel skips rendering when onboarding_dismissed is True (reappears next session)
- Add icons to all step bullets (🏭 Site, 👤 Person, 📍 SamplingPoint, 📡 DAS, 🔌 SignalInterface, 📊 Channel, 🧪 Laboratory, 🔬 Lab)
- Add campaign optional caption at bottom of panel
- Add test_panel_dismissed_hides_panel covering the new dismiss behaviour
…ingStep (#39)

- Add MaintenanceDriftIn / MaintenanceDriftOut Pydantic schemas
- Add maintenance_drift_repository.create_drift_channel: inserts
  ProcessingStep (method='maintenance_drift', OperationKind=DriftCorrection),
  ProcessingLineage edge, and derived Channel; no dbo.Value reads or writes
- Add POST /channels/derived/maintenance-drift endpoint (201)
- Wire router into api/v1/router.py under /channels prefix
- Add 9 unit tests (red→green) covering schema validation; 542 passed 0 failed
…t to /ingest/lab (#43)

- resolver.py: exact short_name match before fuzzy name match for parameters
- mapper.py: _resolve_all() resolves col-level (param+unit) and row-level (sp, datetime, replicate, values)
- Preview ingest table with per-row resolved/skipped status
- Submit button → _do_submit() builds sample + measurements payloads, reports per-row ok/error
- Unresolved rows are never silently submitted
- tests/unit/test_lab_profile_s3.py: 7 red→green tests covering resolution + submit flow
- New page app/pages/maintenance_control_chart.py: drift channel picker,
  configurable UCL/LCL number inputs, ECharts line+scatter chart with
  out-of-limit (red triangle) and quality-flagged (orange diamond) markers,
  source channel overlay series, stats summary row, OOL table
- Wired into Home.py navigation under Operations section
- 8 unit tests in tests/unit/test_maintenance_chart_s2.py covering
  dataframe build, chart option structure, limit/flag separation,
  source series inclusion, and nav wiring (all red→green)
Thin profile reusing the shared mapper engine: tag timestamp/tag/parameter/
unit/value columns, resolve parameter+unit to DB entities, group resolved
rows by (tag, parameter, unit) channel, and submit tagged /ingest/sensor
payloads. Unresolved rows are surfaced, never silently submitted. A
required DAS-name input names the source system (no empty-named DAS).
…GET /events/{id}/maintenance-drift + before/after UI) (#50)
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