v0.11.0 #50
Reference in New Issue
Block a user
Delete Branch "release/0.11.0"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
When a client drops a location from scope mid-project (e.g. the office half of a museum+office monitoring job), operators couldn't previously mark it as no-longer-active without either deleting it (which would orphan historical events) or leaving it in the active list looking deployable. Now there's a proper middle ground. Data model - MonitoringLocation gets two new nullable columns: - removed_at — NULL means active; set means soft-removed - removal_reason — optional operator note Migration: backend/migrate_add_location_removed.py (idempotent) Endpoints - POST /api/projects/{p}/locations/{l}/remove Body: { effective_date?: ISO-datetime, reason?: str } Side effects (cascade): 1. Closes active UnitAssignment rows at this location (assigned_until = effective_date, status = "completed") 2. Cancels pending ScheduledActions at this location 3. Marks location.removed_at = effective_date Returns counts of assignments closed + actions cancelled. - POST /api/projects/{p}/locations/{l}/restore Clears removed_at + removal_reason. Does NOT auto-reopen assignments — operator creates new ones if resuming monitoring. Active-surface filters - locations-json defaults to active-only; pass include_removed=true for historical / reporting views. Schedule modal dropdowns now exclude removed locations automatically. - Metadata-backfill fuzzy matcher excludes removed locations from proposed targets (don't want backfill creating new assignments at decommissioned locations). - Vibration-summary per_location rollup includes removed locations (so historical event totals stay accurate) but tags each with removed_at so the UI can show a badge. UI - Project detail page's Monitoring Locations section now splits into: Active locations (full card with Assign / Edit / Remove / Delete) Removed locations (collapsed <details>, greyed cards, Restore button, shows removal date + reason) - New per-card "Remove" button → opens confirmation modal explaining the cascade, with optional effective-date (defaults to now, backdateable) and reason fields. - Unit detail's SFM Events attribution cell shows a small "removed" badge next to historical attributions whose location is no longer active. Same pattern in vibration_summary's top-locations list. - Soft-removal indicator surfaced through the events_for_unit attribution payload as location_removed_at. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>The buttons used inline `onclick="...({{ name | tojson }})"`, which emits the location name as a JSON-quoted string with double quotes — those double quotes collide with the onclick attribute's own double quotes, terminating the attribute early. Result: the browser parses the attribute as broken HTML and the click handler never fires. Switched both Remove and Restore to the data-attribute pattern the Edit button already uses (data-loc-id / data-loc-name read via this.dataset in the onclick). Robust against any character in the location name. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>When an operator accidentally clicks Assign multiple times on the same location (or assigns the wrong unit), the resulting bogus assignment rows cluttered the location's deployment history with no way to clean them up — Unassign just sets assigned_until to now, which preserves the row. New DELETE /api/projects/{p}/assignments/{a} endpoint hard-deletes the row entirely, intended for mis-clicks that never represented a real deployment. Safety: - Refuses if any MonitoringSession exists in the assignment's window for the same (unit, location). If there's a recording session backing it, this isn't a mis-click — operator should Edit or Unassign instead. - Records UnitHistory `assignment_deleted` so the unit's deployment timeline still shows the deletion happened, even though the row itself is gone. UI: trash icon added next to the existing pencil (Edit) icon on each row of the vibration location's "Deployment History" panel. Confirms intent with a descriptive prompt that explains the consequence (attribution becomes unattributed for that window) and points to Edit/Unassign as alternatives. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>When a unit had its assignment closed-then-reopened (e.g. via the recent location remove/restore flow) or had metadata-backfill auto- create a retroactive window adjacent to a manual one, the deployment timeline showed N stacked rows that represented one continuous deployment. Visual noise that didn't match reality. Merge feature - New endpoint POST /api/projects/{p}/assignments/merge - Body: { assignment_ids: [uuid, ...] } - Keeps earliest record, extends its window to span all inputs, deletes the others, logs `assignment_merged` to UnitHistory - Validates: all assignments share same unit + location, all belong to the same project - deployment_timeline_for_unit() now auto-detects mergeable groups (consecutive same-location assignments within 7-day gap tolerance) and returns them in `merge_groups` as a list of id-lists - Unit detail page shows a blue banner above the timeline list when groups exist, with one "Merge into one" button per group. Each mergeable row gets a small "mergeable" badge to make the relationship obvious. Per-unit Gantt chart (Phase 1 of the deployment-history calendar) - Plain-SVG horizontal timeline rendered above the existing Deployment Timeline list, ~140px tall - One colored bar per assignment, color-keyed by location (auto- assigned palette + legend) - Reduced opacity for closed bars; small white dot at the right edge of active bars; today marker as a dashed orange vertical line - Month gridlines (or every-3-month gridlines when domain > 24 months) - Metadata-backfilled assignments get a blue outline so you spot which were auto-attributed - Mergeable groups get a dashed blue underline tying their bars together visually - Click any bar → smooth-scrolls the matching list row into view and flashes a ring around it - Hover any bar → tooltip with location + window + event count - Auto-hides on units with no deployment history Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>The safety check that refuses to delete assignments with real recording history referenced MonitoringSession.start_time, but the actual column is MonitoringSession.started_at. Every DELETE call to /assignments/{id} crashed with AttributeError before doing anything. Now uses started_at correctly. Verified end-to-end on dev. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>The inline onclick on each typeahead dropdown item was: onclick="onTypeaheadPick(event, 'cid', 'location', 'loc-id', ${JSON.stringify(m.name)})" For any name with spaces/punctuation (i.e. every real location name like "Area 1 - Loc 1 - 87 Jenks"), JSON.stringify emits double quotes around the value, which collide with the onclick attribute's own double quotes and terminate the attribute early. The dropdown rendered fine via .innerHTML, but the browser's HTML parser saw a broken attribute and never bound the click handler — clicks on dropdown items silently did nothing. Same pattern that broke the location Remove button yesterday. Same fix: move args into data-* attributes and dispatch through a tiny trampoline that reads from this.dataset. Robust against any character in project/location names. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>rapidfuzz.fuzz.WRatio inflates scores when two strings share substring tokens, even when the shared tokens are common boilerplate. For project names this is desirable (catches typos like '1-80' vs 'I-80') but for location names it produces obvious false positives: 'Area 2 - Brookville Dam - Loc 2 East' vs 'Area 1 - Loc 1 - 87 Jenks' → WRatio 85.5 (above 0.80 fuzzy threshold) These share only 'area' + 'loc' + a digit but score 85%+ because WRatio weights partial-substring overlap heavily. Operator reported the backfill tool suggesting completely unrelated locations as 86% matches. Fix: introduce `location_similarity()` — token_set_ratio + multi-digit mismatch penalty. Used for location matching everywhere; WRatio stays as the scorer for project names where its leniency is correct. The multi-digit penalty (-0.30) triggers when both strings contain 2+- digit numbers and none overlap. Catches the harder "same project, different address identifier" case: 'Area 1 - Loc 2 - 68 Jenks' vs 'Area 1 - Loc 1 - 87 Jenks' token_set_ratio = 0.91 (would still match without penalty) multi-digit tokens {68} and {87} disjoint → -0.30 → 0.61 (rejected) Single-digit tokens ('Loc 1', 'Area 2') are excluded from the penalty because they're often coincidentally shared. Updated: - backend/services/metadata_backfill.py: new location_similarity() function; _find_best_match() gains a `kind` parameter that selects scorer; cluster-match call site passes kind='location' - backend/routers/metadata_backfill.py: locations_search endpoint (the typeahead dropdown's data source) uses location_similarity instead of similarity for the same reason Verified all six test cases land correctly: - user-reported false positive: 0.85 → 0.59 (rejected) - '87 Jenks' vs '68 Jenks': 0.90 → 0.61 (rejected) - NRL-01 vs NRL-02: 0.83 → 0.53 (rejected) - 'Loc 2 - 735 Bunola' vs 'Loc 2 735 Bunola Rd': 1.00 (still matches) - punctuation-only difference: 1.00 (still matches) Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>Two bugs in the project-merge modal: 1. Dropdown options had the same JSON.stringify quote-collision in their inline onclick that broke the location Remove button and the metadata-backfill typeahead earlier this week: onclick="onMergePickTarget('${id}', ${JSON.stringify(m.name)})" For 'I-80 Area 1' that renders as onclick="...(\"I-80 Area 1\")" — the inner double quotes terminate the onclick attribute early, and the browser never binds the click handler. Operator clicked items in the dropdown and nothing happened. Fixed via data-target-id / data-target-name attributes and a _mergePickFromButton(btn) trampoline. 2. Modal body had `flex-1 overflow-y-auto` with no min-height, so the container shrunk tight around the input. When the typeahead dropdown appeared below the input it got clipped by the body's overflow and the operator had to scroll inside the modal to see the options. Fixed by adding min-height: 480px to the modal container + min-h- [320px] on the body so there's always room for the dropdown + the preview pane that appears below after a target is picked. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>Project location cards now reorderable via drag-and-drop, and the four inline action buttons (Unassign/Edit/Remove/Delete) collapse into a single three-dot kebab menu — much cleaner card layout, especially for projects with many locations. Data - MonitoringLocation.sort_order: nullable Integer, default 0. Migration `migrate_add_location_sort_order.py` adds the column and seeds existing rows with sort_order = alphabetical index per project (so the post-migration display order matches what operators see today — no surprise reordering). - get_project_locations + locations-json: ORDER BY sort_order, name. - Location-create: assigns max(sort_order) + 1 so new locations land at the END of the list rather than being interleaved alphabetically. Reorder endpoint - POST /api/projects/{p}/locations/reorder Body: { location_ids: [uuid, uuid, ...] } Validates: all ids belong to this project; raises 404 on missing. Applies 0-indexed sort_order matching the provided order. UI changes (templates/partials/projects/location_list.html) - Active cards get a draggable="true" attribute + native HTML5 drag/drop handlers. Drop reorders the DOM immediately, then posts the new order to the reorder endpoint. Drop-zone visual feedback (orange ring on hover, opacity on source during drag). - Six-dot drag handle icon on the left of each active card; whole card body is the drag source but the handle is the visual cue. - Right side: small Assign pill (only shown when unassigned) + three-dot kebab menu containing Unassign/Edit/Remove/Delete. Click ⋮ to toggle; click outside or Escape to close. Only one menu open at a time. - Removed locations are NOT draggable (their order is historical) and keep their existing Restore button visible. The card also shows "{N} events" instead of "Sessions: N" when the location_type is vibration AND the backend passes event_count in the payload — which lands in commit 2 of this redesign. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>For vibration projects, "Sessions: 0" on every location card was misleading — monitoring sessions don't exist under the watcher-forward pipeline. The relevant number is how many SFM events have been attributed to the location. get_project_locations now fans out events_for_location() concurrently across all vibration locations in the project (via asyncio.gather) and injects event_count into each item's payload. Sound locations are unchanged — they still show session_count. The template already had the conditional rendering ready from the previous commit: {% if item.event_count is defined and item.location.location_type == 'vibration' %} <span><strong>{{ event_count }}</strong> events</span> {% else %} <span>Sessions: {{ session_count }}</span> {% endif %} so this commit is purely the data-layer change that activates it. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>