Commit graph

8 commits

Author SHA1 Message Date
4e391b3ecf feat(coverage): cnHeat-style full-bleed map on the show page
Rewire the single-coverage show page to mount MultiCoverageMap (the
same hook the org map uses) with one coverage in the array, and lift
the cnHeat-style overlay panels — LOS/NLOS toggle + height slider on
the left, opacity + RSSI filter on the bottom, distance probe on
click. Parameters move into a togglable right-side drawer so the map
dominates the viewport.

Show LiveView gains toggle_params / probe_point / close_probe /
set_probe_units handlers and delegates the formatters to
CoverageLive.Map. Site is now preloaded so coverages_payload/1 can
read site.name without an Ecto.AssociationNotLoaded crash that was
silently nuking the data-coverages JSON encode.

Also fixes a real bug in coverage_hooks.ts: pngForCoverage was
returning before the antenna-marker creation block, leaving the
marker code as unreachable dead code — towers never rendered. Moved
the marker creation into addCoverageLayer where it belongs and added
a no-PNG guard so a missing png_path logs instead of silently
failing the L.imageOverlay constructor.
2026-05-07 08:27:27 -05:00
98da879399 fix(config-timeline): implement the missing chart hook
ConfigTimelineLive's chart panel was rendering a bare 'Loading chart...'
placeholder forever — the phx-hook="ConfigTimelineChart" had no JS
implementation registered anywhere, so the whole point of the page
(correlating QoE regressions with config edits) was lost.

Add a focused lazy hook in hooks/config_timeline_chart.ts that:

  * Plots Preseem latency / throughput / loss across three Y-axes.
  * Overlays vertical dashed markers at each config-change timestamp
    with a small badge showing how many lines changed.
  * Drops a red triangle on the time axis for every failed check.
  * Reuses the Chart.js vendor chunk (esbuild --splitting factors it
    into a shared chunk with sensor_chart, so visiting either page
    primes the other).

Bundle delta:
  chunk-RSDHLN2L.js (Chart.js, shared)   314.8 KB
  config_timeline_chart                    7.2 KB
  sensor_chart (was 325 KB standalone)    10.6 KB after dedup
2026-05-07 08:11:48 -05:00
7ffc4ebd2b perf(assets): lazy-load three more hooks + trim CopyToClipboard
DeviceListReorder, SortableList, MikrotikPortSync are each used on a
single page (devices index, escalation/schedule show, device edit
form respectively) — push them out of app.js into their own chunks
via lazyHook. Also extract MikrotikPortSync into its own file under
hooks/ for consistency.

Compress CopyToClipboard's flash feedback by collapsing the duplicated
success/error inline-SVG/setTimeout blocks into one helper, drop the
debug console.logs, and remove the bulk Phoenix-template boilerplate
comment block that was just generated noise.

After-bundle (minified):
  app.js                 287.6 KB   (-2 KB on top of prior split)
  device_list_reorder      4.6 KB   loaded on /devices only
  sortable_list            3.1 KB   loaded on escalation+schedule pages
  mikrotik_port_sync       1.3 KB   loaded on device edit form only
2026-05-07 08:05:22 -05:00
3bd4ed303c perf(assets): split big hooks into per-page chunks
app.js was bundling Chart.js (~200 KB), Cytoscape (~500 KB) and four
heavy hooks (SensorChart, NetworkMap, WeathermapViewer, SitesMap)
into a single 1.25 MB blob that every page paid for.

Extract each into its own module under assets/js/hooks/ and reference
them through the existing lazyHook() wrapper so the implementation is
fetched only when a hook actually mounts. esbuild --splitting then
factors the shared cytoscape vendor blob into a chunk that's only
loaded on topology / weathermap pages.

After-bundle layout (minified):
  app.js                  297.8 KB   (was ~1250 KB; -76%)
  chunk-LJGWK7RH (cyto)   555.1 KB   loaded on topology pages only
  sensor_chart            325.3 KB   loaded on sensor pages only
  network_map              24.4 KB   topology hook glue
  weathermap               13.0 KB   weathermap hook glue
  sites_map                 4.1 KB   site map hook glue
  coverage_hooks           25.0 KB   coverage hook glue

LeafletMap also moved to sites_map.ts and now uses ensureLeaflet
instead of polling for window.L with setTimeout.
2026-05-07 07:58:38 -05:00
ce9bd744c7 feat(coverage): LOS/NLOS dual compute + buildings clutter + MS importer
Closes the cnHeat-parity gap on modes and clutter:

Backend
- Towerops.Coverages.Buildings.for_bbox/1 — PostGIS ST_Intersects
  query that returns each building polygon as a compact
  %{height_m, coords} map ready for in-memory point-in-polygon
  testing, so the per-pixel hot loop never round-trips to
  Postgres.
- Towerops.Coverages.Profile.sample/5 — accepts an optional
  :clutter list and adds the building rooftop height to terrain
  whenever a sample point falls inside a polygon. Pure ray-cast
  PIP for portability.
- Towerops.Workers.CoverageWorker — fetches buildings once per
  job and computes BOTH LOS and NLOS variants per SM-height
  tier (12 PNGs total). LOS passes clutter: [] (height-above-
  clutter view), NLOS passes the prefetched list (height-above-
  ground view with rooftop diffraction). Pixel-loop signature
  bundled into a ctx map to keep the function arity within
  Credo's max-8 limit.
- Towerops.Workers.MsBuildingsImportWorker — streams Microsoft
  GlobalMLBuildingFootprints GeoJSONSeq exports line-by-line,
  upserts into coverage_buildings in 500-row batches via
  insert_all + ON CONFLICT (source, ms_footprint_id). Handles
  Polygon and MultiPolygon geometries; stable-hashes the bbox
  when an explicit feature id is missing so re-imports are
  idempotent. Public import_file/2 lets ops drive a state at
  a time from IEx.
- Schema: nlos_tiers JSONB column on coverages, status_changeset
  whitelist, payload exposed via list_ready_for_organization.

Frontend
- LOS / NLOS toggle pills above the height slider (left panel).
  Selecting NLOS swaps each L.imageOverlay's URL via setUrl to
  the matching nlos_tiers PNG, drops the cached pixel data, and
  re-decodes for the RSSI filter; falls back to the other
  mode's tiers when one is empty so older coverages still
  render. coverage_hooks chunk: 22 KB → 25 KB.
2026-05-06 16:50:05 -05:00
434f5782d0 feat(coverage): multi-SM-height compute + cnHeat-style height slider
Vertical 'Height above clutter' slider on the left of the map now
matches cnHeat's experience: pick an SM install height, the heatmap
re-renders to show only the coverage achievable from that height.

Backend
- New JSONB height_tiers column on coverages keyed by string metres
  → relative PNG path.
- CoverageWorker runs compute_pixels once per tier in
  [1.83, 3.05, 4.57, 6.10, 9.14, 12.19] m (~6/10/15/20/30/40 ft)
  by varying receiver_height_m on a copied struct. Each tier
  writes its own raster + PNG via Raster.write/4 with a per-height
  filename suffix; the default 3.05 m tier also populates the
  legacy raster_path / png_path so existing show pages keep
  working.

Frontend
- Vertical range slider (writing-mode: vertical-lr) anchored
  centre-left. Step picks one of HEIGHT_TIERS_FT; label updates
  to '6 ft'..'40 ft'.
- pngForCoverage/2 picks the closest available tier from the
  payload's height_tiers map, falling back to the original png
  for coverages not yet recomputed.
- On slider change the JS swaps each L.imageOverlay's URL via
  setUrl, drops the cached pixel data, and re-decodes so the RSSI
  filter still applies. coverage_hooks chunk: 20 KB → 22 KB.
2026-05-06 16:38:07 -05:00
5d1adcaacc feat(coverage): client-side signal-strength filter slider
Adds the cnHeat-style horizontal 'Min RSSI' slider beside the opacity
control. Pixels weaker than the threshold turn fully transparent,
client-side, with no server round-trip — same compute output now
powers an interactive 'where can I get -75 dBm or better' exploration.

- Slider range -100..-40 dBm step 1; -100 disables filtering.
- addCoverageLayer fetches each PNG into a hidden canvas and caches
  its ImageData. crossOrigin=anonymous keeps reads tainted-free.
- Each pixel is classified by nearest palette colour (-50/-65/-75/
  -85/-95 dBm centres matching Raster.write/3) and dropped if its
  dBm is below the threshold. Result re-encoded via toDataURL and
  swapped onto the L.imageOverlay via setUrl.
- destroyed() revokes blob URLs and clears the filter cache to
  avoid leaks during LiveView nav. Bundle: coverage_hooks chunk
  16 KB → 20 KB; main bundle unchanged at 1.2 MB.
2026-05-06 16:29:24 -05:00
e97748437c build(assets): split coverage hooks into a lazy-loaded chunk
Switch esbuild to ESM with --splitting and load app.js as type=module.
Heavy hooks now ship in their own chunk and only download when their
LiveView mounts.

- assets/js/lib/leaflet.ts: extract ensureLeaflet (vendored Leaflet
  loader) into a shared module with a typed window.L declaration.
- assets/js/hooks/coverage_hooks.ts: extract CoverageMap,
  MultiCoverageMap, and CoverageLocationPicker (~510 lines) with
  proper interface types for hook context and tightened typing on
  payloads / event handlers (CoveragePayload is now a real type).
- app.ts: replace those three inline hook bodies with lazyHook
  stubs that call import('./hooks/coverage_hooks') on first mount,
  forwarding lifecycle hooks once the chunk resolves. Also adds a
  scoped `declare const L: any` so the remaining Leaflet hooks
  type-check.
- root.html.heex: switch the script tag to type=module so the
  emitted ESM bundle can use dynamic import().
- esbuild config: add --splitting --format=esm.

Build output now produces:
  app.js                      1.2 MB
  coverage_hooks-<hash>.js   16  KB  (lazy)
  chunk-<hash>.js             2.4 KB  (shared, lazy)
2026-05-06 16:11:32 -05:00