Skip to content

BeaconClient: fetch-based promise core + standardised API - #448

Merged
OSPFNeighbour merged 4 commits into
master-devfrom
refactor/beaconclient-core
Sep 9, 2026
Merged

BeaconClient: fetch-based promise core + standardised API#448
OSPFNeighbour merged 4 commits into
master-devfrom
refactor/beaconclient-core

Conversation

@OSPFNeighbour

@OSPFNeighbour OSPFNeighbour commented Sep 9, 2026

Copy link
Copy Markdown
Collaborator

What

Rewrites BeaconClient (our interface to the Beacon REST API) from callback-style $.ajax/XHR into a single fetch-based promise layer with a standardised, consistent API surface.

Net −600 lines across 41 files. No feature or endpoint changes — same URLs, same payloads, same query strings.

1. New core — src/shared/BeaconClient/core/request.js

  • request(url, { method, token, json, form, responseType, headers, signal, nullOnError }) — one transport, one place for the Authorization: Bearer header, JSON/form serialisation and error semantics.
  • requestPaginated(url, { token, pageLimit, pageSize, onProgress, onPage, signal }) — replaces json.js's getJsonPaginated; returns { results, totalItems }.
  • toCollection(payload) — normalises Beacon's { Results, TotalItems } to { results, totalItems }.
  • BeaconApiError (carries status, url, body) — thrown on any non-2xx.
  • toFormUrlEncoded moved here from the barrel (still re-exported).
  • json.js deleted.

2. All 23 domain modules rewritten

No $.ajax, no callbacks. Every function returns a Promise. jquery import remains only in team.js (needs $.param(params, true) for traditional array serialisation).

3. Standardised inputs

Every function now takes its resource id(s) / domain args positionally and a single trailing context object:

client.job.get(id, { host, userId, token })
client.job.search(unit, startDate, endDate, { host, userId, token, onProgress, onPage })
client.team.search(unit, startDate, endDate, { host, userId, token, statusTypes, onPage })
client.tasking.callOffTeam(taskingId, payload, { host, token })
client.operationslog.create(payload, { host, token })

Gone: host/userId/token scattered through positional slots, viewModelType wedged mid-list, host-first tasking.* calls, the -1/-1 progress-callback error convention.

main.js gets a beaconCtx(token, extra) helper to build the context object.

4. Standardised outputs

Kind Returns
collection (*.search, getTasking, getHistory, unresolvedActionsLog) { results, totalItems }
single entity (get, getName, summary, unitBoundary, getMessageById, …) the entity, or null
mutations (job.cancel/reopen/reject/acknowledge/complete, tasking.sequence, sectors.setSector/unSetSector) resolve on success, reject on failure — the bare true/false returns are gone
spurious-500 writes (tasking.task/updateTeamStatus/callOffTeam/untaskTeam, messages.send) body, or null — Beacon intermittently 500s these despite applying the change, and callers re-sync from SignalR (nullOnError)
genuine-array endpoints (entities.children, notifications.unaccepted, tags.getGroup) a plain array
special images.getImageDataBlob; team.getTeamGeoJson → GeoJSON; asset.filter → asset array

5. Renames

  • entities.fetchentities.get (it shadowed the global fetch)
  • team.teamSearchteam.search
  • deleted the unused job.searchwithFilter

6. Call sites + downstream readers updated

tasking/main.js (~45 sites), Team.js, the reporting pages (stats / summary / advexport / nitcexport / teamsummary), the tasking map layers, IncidentImagesModalVM, ContentScriptMapManager, the jobs inject scripts, and stats jobparsing.js + wordcloud.js (now read .results).

Behaviour changes to be aware of

  1. Errors now reject instead of silently hanging — old $.ajax complete handlers frequently never fired the callback on failure. Every caller got a real .catch / try-catch wired to the existing user-facing alert.
  2. icems.acknowledgeIum was $.param(vm) (deep serialisation), now toFormUrlEncoded (flat + key[] arrays only). Fine if the IUM payload is flat — verify the IUM acknowledge flow.
  3. asset.js caching logic rewritten (the 20s localStorage cache is preserved); a failed radio/telematics sub-fetch now logs + returns partial data.
  4. Job status changes surface a real error toast on failure now, where a spurious false used to.

Verification done

  • npm run lint — clean
  • npm run dev — builds
  • Not yet exercised against a live Beacon backend (OKTA-gated) — needs the manual pass below

There is no automated test suite in the repo. A follow-up will seed Vitest against the new core/request.js (mock fetch, assert headers/body/error/pagination) as the start of real coverage.


Manual test checklist

Load the built extension against testbeacon / devbeacon with a real session. Tick each as verified.

Tasking page (/Jobs/Tasking) — largest surface

Load & refresh

  • Page loads; jobs and teams populate
  • Jobs auto-refresh on the timer (watch console for the poll)
  • Teams auto-refresh on the timer
  • Batch job-tasking refresh runs (stale jobs get re-fetched)
  • Tag groups load (no "Failed to fetch tags" in console)
  • Sectors list loads for the current HQ filter

Jobs

  • Open a job popup — details, tasking, ops log all render
  • Job hover tooltip on the map marker
  • Change a job's status: Acknowledge
  • Change a job's status: Complete (with text)
  • Change a job's status: Reject (with text)
  • Change a job's status: Cancel (with text)
  • Change a job's status: Reopen
  • Confirm the status-confirm modal closes on success and shows an error toast on failure
  • A job that drops out of the feed gets refreshed/removed correctly

Teams & tasking

  • Task a team to a job (drag or button) — success toast, tasking appears on both team and job
  • Untask a team
  • Call off a team
  • Change a team's tasking status (on route / on site / etc.)
  • Drag-reorder taskings in a team panel and save — "Tasking order saved"
  • Team panel shows that team's tasking history

Sectors

  • Assign an incident to a sector — success toast, job refreshes
  • Remove an incident from a sector
  • Sector filter chips reflect the loaded sectors; stale filters get pruned

Assets

  • Trackable assets layer loads and asset markers appear
  • Assets match to teams (default-asset mappings)
  • Asset popup content

Map layers (each: toggle on, data draws, toggle off)

  • Filtered Unit Boundaries
  • Active FRAOs (polygons + labels)
  • Transport cameras / incidents (uses an ops-log entry for the API key)
  • SES teams layer

Modals & pickers

  • Config modal → HQ / entity search returns results
  • Config modal → member (Users) search
  • Config modal → event search
  • Send SMS modal → recipient search (contacts + contact groups), send a message
  • Ops Log modal → view entries, create an entry
  • Radio Log modal
  • Incident Images modal → thumbnails load, full image opens
  • Unresolved "action required" tags show on a job with ICEMS entries

ICEMS

  • ICEMS incident lookup on an ICEMS-originated job
  • Unaccepted notifications appear for an ICEMS job
  • Acknowledge an unaccepted notification
  • Acknowledge an IUM message (this is the $.paramtoFormUrlEncoded change — verify the payload lands)

People

  • "Created by" / person names resolve on collab-layer rows and history

Reporting pages

Job Summary (pages/summary.html)

  • Single HQ (?hq=<id>) — loads, all the status counts render
  • Multiple HQs (?hq=<id>,<id>,<id>) — loads, progress bar advances
  • Whole-state (no hq) — loads

Job Statistics (pages/stats.html)

  • Single HQ — charts render
  • Multiple HQs — charts render, progress bar advances
  • The situation-on-scene word cloud renders (reads jobs.results)
  • Time-window radio buttons re-query and redraw
  • Refresh button re-queries

Job Export (pages/advexport.html)

  • Select fields, export — CSV downloads with the right rows
  • Include the "ICEMS - Number Of IUM Transactions" column — the per-job ops-log count runs and the CSV is correct
  • Export with 0 results — "No job results returned", no crash

Team Summary (pages/teamsummary.html)

  • Single HQ — team cards render
  • Multiple HQs — cluster/zone grouping in the title, progress advances
  • Per-team history panel populates (reads history.results)
  • A team-history job entry shows the resolved job address (nested job.get)
  • Job popup from a history row

NITC Export (pages/nitcexport.html)

  • Activity list export — CSV correct
  • Member list export — CSV correct
  • With type / tag / completed filters applied

Inject scripts (on Beacon pages, main-world)

  • Job view page → "Nearest Assets" map: assets load, routing draws, distance rings
  • Job view page → asset filter modal: list populates, filtering works, headless "just load" path works
  • Situational Awareness page → asset filter modal
  • /Teams map → SES teams layer + filtered-assets layer (via ContentScriptMapManager)
  • Confirm the inject↔content-script boundary still works (these run in the page's main world, not the isolated world)

Regression sweep

  • No unhandled promise rejections in the console during a normal session
  • Session-expiry path: let a token lapse (or revoke it) and confirm the "your session may have expired" alerts fire instead of a silent hang
  • Watch for any BeaconApiError in the console that isn't surfaced to the user

🤖 Generated with Claude Code

OSPFNeighbour and others added 3 commits September 9, 2026 10:46
Add BeaconClient/core/request.js: request() / requestPaginated() /
BeaconApiError / toFormUrlEncoded. One fetch-based transport, one place for
the Bearer header, JSON handling, error semantics and Beacon's paged-search
loop.

Rewrite all 23 domain modules to return Promises instead of taking
callbacks, delegating transport to the core. Delete BeaconClient/json.js
(getJsonPaginated -> requestPaginated). Non-2xx responses reject with
BeaconApiError, except the tasking/message write endpoints Beacon
spuriously 500s (nullOnError -> resolve null).

Call sites updated in follow-up commits.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Update every caller of BeaconClient to the promise API: tasking main.js
(~45 sites), the reporting pages (stats/summary/advexport/nitcexport/
teamsummary), the tasking map layers, IncidentImagesModalVM,
ContentScriptMapManager and the jobs inject scripts.

Callback pyramids collapse to await / .then; the -1/-1 progress-callback
error convention is replaced by real .catch handlers. Paginated searches
(job/team/sector/frao/nitc) now take an { onProgress, onPage } options
object in place of trailing positional callbacks.

npm run lint + npm run dev both pass. Not yet exercised against a live
Beacon backend.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Inputs: every function now takes its resource id(s) / domain args
positionally and a single trailing context object
{ host, userId, token, signal, ...perCallOpts }. No more host/userId/token
scattered through positional slots, no viewModelType wedged mid-list, no
host-first tasking calls. Search options (onProgress/onPage/statusTypes/
typeIds) live in that context object too.

Outputs:
- collection endpoints return { results, totalItems } (core toCollection /
  requestPaginated); callers read .results
- single-entity fetches return the entity or null
- mutations (job cancel/reopen/reject/acknowledge/complete, tasking
  sequence) resolve on success and reject on failure -- the bare
  true/false returns are gone
- the tasking/message writes Beacon spuriously 500s keep nullOnError
- bare-array endpoints (entities.children, notifications.unaccepted,
  tags.getGroup) stay arrays

Renames: entities.fetch -> entities.get (fetch shadowed the global);
team.teamSearch -> team.search. Deleted the unused job.searchwithFilter.

main.js gets a beaconCtx(token, extra) helper for the context object.
All call sites and downstream .Results readers updated (tasking main.js,
Team.js, the reporting pages, map layers, inject scripts, stats jobparsing
+ wordcloud).

npm run lint + npm run dev pass. Not yet exercised against live Beacon.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@OSPFNeighbour
OSPFNeighbour force-pushed the refactor/beaconclient-core branch from c0deba3 to fa59598 Compare September 9, 2026 00:48
@OSPFNeighbour
OSPFNeighbour changed the base branch from master to master-dev September 9, 2026 00:48
@OSPFNeighbour

Copy link
Copy Markdown
Collaborator Author

Rebased onto master-dev (was branched off the older master). One real integration point: the team-type filter added in #431team.teamSearch's trailing typeIds positional arg is now ctx.typeIds, and fetchAllTeamData passes typeIds: typeFilterToView through the standardised call. Everything else auto-merged.

npm run lint + npm run dev pass on the rebased branch.

Extra checklist item: Tasking → Team & Tasking Filters → team-type filter (Field / Operations / Aviation) still narrows the team list.

The Beacon API authenticates on the Authorization: Bearer header only.
Chrome attaches a host's cookies to fetch() from an extension page whose
manifest grants that host, even under the spec-default 'same-origin'
credentials mode -- the old jQuery cross-origin XHR never sent them. A
stale Beacon session cookie riding along can make the API 401 the request
despite a valid bearer token (seen on Team Summary -> job popup from a
history row).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@OSPFNeighbour
OSPFNeighbour added this pull request to stack #455 September 9, 2026 06:11
@OSPFNeighbour
OSPFNeighbour merged commit bd9e9b0 into master-dev Sep 9, 2026
1 check passed
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