Skip to content

Release v0.6.72 - #372

Merged
roncodes merged 128 commits into
mainfrom
release/v0.6.72
Oct 14, 2026
Merged

roncodes merged 128 commits into
mainfrom
release/v0.6.72

Conversation

@roncodes

@roncodes roncodes commented Oct 7, 2026

Copy link
Copy Markdown
Member

Release branch for v0.6.72, cut from main. It bumps package.json, composer.json and extension.json to 0.6.72 and rewrites RELEASE.md.

What this release collects

These PRs now target this branch:

PR Change
#352 Public order lookup: per-address rate limit and a minimal public resource
#353 Signatures stored without a public ACL; stored avatar URLs re-signed
#354 Customer credentials email links to /customer-access/{slug}
#355 Tracking page settings backend
#356 Structured tracking-URL QR codes, Code 128 barcodes, backward-compatible scan resolver
#357 Tracking page settings screen
#370 Parcel box images after production fingerprinting

Merge order

Upgrade note

Run php artisan fleetbase:create-permissions to register the new tracking-page-settings permission.

…resource

The public Track Order page's GET int/v1/fleet-ops/lookup had no session,
no rate limit, and answered with the full OrderResource: notes, meta,
internal ids, files (possible POD photos and signatures), purchase rate,
and live driver location, to anyone holding a tracking number.

- Rate limit it per address on its own limiter (throttle:30,1,tracking-lookup),
  as the public inspection routes do.
- Answer with PublicOrderTracking, which carries only what
  order-tracking-lookup renders: tracking number, status, timeline, ETA and
  progress, stop coordinates for the route map, and the item list. Records
  are keyed by public id, so no internal uuid reaches the page. The driver
  position is included only while the order is started and unfinished. It
  extends JsonResource so registered resource transformers cannot add fields.
- Missing, malformed and unknown tracking numbers get one identical error,
  and malformed input never reaches the query.
… avatar URLs

- ProofController::storeSignature no longer passes 'public': the media bucket
  enforces bucket-owner ownership, rejects ACL'd PUTs, and put() silently
  returned false (no signature objects written since 2024-11-25).
- Vehicle/Driver/Place avatar_url accessors re-sign legacy absolute bucket URLs
  via core-api File::signStoredUrl when available.
The customer portal resolves a company's access slug only under
/customer-access/{slug}; the credentials email linked /{slug}, which the
console does not route. Without a configured slug the link still points
at the portal's own /customer-portal sign-in.
…page

Add the settings backend for the customer tracking page refactor (PR 2):

- TrackingPageConfig: the company config's shape, defaults and sanitizing
  (pages, branding, access, what verified visitors see), instance defaults
  for the generic page, slug rules with a reserved list, and accent contrast
  helpers (ink colour and WCAG ratio).
- SettingController: get/save tracking-page-settings, a live slug check, and
  admin defaults. Saved slugs are indexed in fleet-ops.tracking-page-slugs.*
  so the public page resolves a company in one read; an enabled organization
  page refuses an invalid, reserved or taken slug. The read-back includes the
  slug status, the accent's ink and contrast, whether SMS is configured
  (Twilio counts only with credentials), and whether the customer portal is
  installed.
- Routes under settings/, and a tracking-page-settings permission resource.
QR codes now carry a versioned tracking url naming the tracking number and
the owner's public id (https://<console>/track-order?order=<tn>&r=<public_id>&v=1)
instead of the owner's bare uuid, so a phone camera opens the tracking page
while scanners read the parameters. The barcode is now Code 128 with the bare
tracking number, matching the text printed beneath it.

capture-qr and tracking-numbers/from-qr resolve scans through one parser that
accepts the new url, a bare tracking number or public id, and the bare owner
uuid printed on labels already in circulation. from-qr is now scoped to the
session's company and resolves waypoint and place owners too.

qr_code_content is published in every environment: it no longer exposes a uuid.
Labels print the QR as a fixed square instead of cropping it with object-fit.
…can resolver

- TrackingCode: generation, parsing of every format in circulation (legacy uuid,
  tracking url from any host, bare tracking number or public id, hostile urls),
  and subject matching for capture-qr
- TrackingNumber::findOwnerByCode against SQLite: company scoping, disagreeing
  url identifiers, unknown numbers and unmapped prefixes
- insert path, observer and resource expectations moved off the owner uuid
capture-qr reads `code` straight from the request. The navigator v3 branch
sends the scanner's whole result object; the old strict comparison answered
400, and the typed matcher would have thrown. Non-strings now match nothing.
PR 4 of the tracking pages refactor: FleetOps -> Settings -> Tracking Page.

- Pages: own page on/off, its address under /t/ with a live availability
  check, copy and open links, shared-page opt-out, and which page emails and
  labels link to.
- Branding: display name, accent with a live contrast check (the text colour
  on it is derived), an optional dark-mode accent, support contacts,
  Powered by Fleetbase, light/dark behaviour, default and enabled languages.
- Access: status without verification, SMS and email codes (SMS disabled with
  a reason when the instance has no SMS provider), session length, and
  customer sign-in and account upsell when the customer portal is installed.
- What verified customers see, with the privacy rules that always apply.
- A live light and dark preview themed from the same tokens the page will
  use (utils/tracking-page-theme).
- Admin panel 'Tracking Page' under Fleet-Ops Config for the generic page's
  instance defaults; sidebar item, settings hub card, and translation keys
  in all 10 locales (English text until translated).

Settings load in the route's model hook, not a controller constructor.
filter_var(null, FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE) returns
false, not null, so every unset toggle (generic page allowed, powered by, the
visibility defaults) came out false instead of its default.
…per-customer tracking channel

- Register who may subscribe to every FleetOps channel prefix with the core
  SocketChannelRegistry: company match for console users and API credentials;
  narrow rules for drivers (own record, current vehicle and its devices and
  positions, assigned orders incl. via payload entities) and customers (own
  orders, driver and vehicle of their active orders). tracking.* is denied
  by the registry (reachable only through a scoped tracking token).
- Claim Sanctum users who drive for the current company as driver socket
  principals on v1/socket/token.
- positions/replay validates channel_id with the ChannelAuthorizer when socket
  authentication is on.
- Public tracking channel: TrackingScope (one customer of one order),
  TrackingChannel (name/opaque id/token via core-api), TrackingUpdate
  (sanitized per-customer payload) and TrackingPublisher (gated, throttled,
  queued) publishing on order, stop and position changes.
- Require fleetbase/core-api ^1.6.69.
…s only its own

The scheduler subscribed to company.{id}.orders, which nothing publishes to,
and its teardown closed every socket channel, chat included. It now opens
the per-driver channels only and on teardown closes just those (sweeping
once more for subscriptions still pending).
…rinting

Production console builds fingerprint .png files, so boxes/small.png ships
as boxes/small-<hash>.png. broccoli-asset-rev rewrites literal asset URLs to
the hashed names, but it cannot rewrite the interpolated
/engines-dist/images/boxes/{{parcelFee.size}}.png, so the request misses and
the server answers with index.html.

Add a parcel-box-image helper that maps each size to a literal URL the
fingerprinter can rewrite, and use it in the service rate form and details.
…d vehicles

The Google live map fell back to /engines-dist/images/driver-marker.png and
vehicle-marker.png, neither of which exists in assets/images, so a driver or
vehicle without an avatar rendered a broken marker. Point both at the existing
map-marker.png, as the leaflet live-fleet widget already does. The URLs stay
literal strings so broccoli-asset-rev can fingerprint them in production.
…ource

fix(tracking): throttle the public order lookup and return a minimal resource
fix(files): store signatures without a public ACL; re-sign stored avatar URLs
…l-link

fix(mail): link customer credentials to /customer-access/{slug}
feat(tracking-page): organization settings for the customer tracking page
feat(tracking): structured tracking-url QR codes, Code 128 barcodes, backward-compatible scan resolver
feat(tracking-page): settings screen for the customer tracking page
fix(service-rate): resolve parcel box images after production fingerprinting
…lback

fix(live-map): use the shipped map-marker.png as the driver/vehicle fallback icon
feat(socket-auth): fleetops channel resolvers, driver socket tokens, per-customer tracking channel
Tracking links, the TrackingNumber url field and label QR codes pointed at
/track-order?order=..., which falls into the console's own routes. A signed-out
visitor was sent on to the public /~/track-order page after login, but a
signed-in user (staff or admin) landed on an empty console page.

- TrackingCode::PATH is now ~/track-order, the console's public route prefix,
  which renders whether or not the visitor is signed in. The notifications'
  Track Order buttons build their link from the same constants.
- The tracking page is also registered in the console registry, so links
  already emailed or printed without the prefix show it to signed-in staff.
- The scan parser never reads the tracking page path itself as a tracking
  number; old /track-order codes still parse.
The public tracking page the settings configure, at /~/track (shared) and
/~/track?org={slug} (a company's own), with /~/track-order kept for links
already sent. Works signed in or out; signed-in staff see what the customer
sees.

Server (public/track/*, FleetOps):
- TrackingResolver: a tracking number (order, stop, item or service-stop
  place) opens its order for one customer within it (TrackingScope).
  Unknown, malformed, opted-out and other-company numbers all resolve to
  nothing and get one identical 404; lookups throttle inside the controller
  so a throttled lookup looks the same.
- TrackingAccess: one-time codes hashed via VerificationCode::issue(), sent
  only to contact details on the order (masked), with a 30 s resend
  cooldown, 3 sends per scope and 5 per address per 15 min, and a 5-minute
  pause after 3 wrong codes. A correct code gives the device an httpOnly
  session cookie (fb_track, stored as SHA-256) with a grant per customer.
- TrackingView: level 0 (stage, last update, how to verify) and levels 1/2
  built on TrackingUpdate, filtered to the customer's own stops, items,
  timeline and proof of delivery. Never sends uuids, notes, meta, rates,
  the sender's address or other customers' stops; the route line only when
  every stop is the customer's; the driver and position only while en
  route to them.
- Endpoints for codes, sign-out, delivery instructions, proof images,
  reporting a problem, the socket token, and a signed-in customer's orders.
- Links (TrackingNumber url, notification buttons) follow the company's
  'links in emails and labels' setting.

Page (Ember): lookup, limited view, verify (every code state), the full
view with live map, ETA, driver, updates, items, proof lightbox, delivery
instructions and reports, the order list, toasts, light/dark from the
company's tokens, RTL, and polling live updates behind a swappable source.

Tests for the resolver, recipient, access, view, controller, links and routes.
- Wrap the code input's normalizing chain the way prettier wants.
- Rename the locale loop's block param so it no longer shadows <option>.
- Describe the map with screen-reader text instead of role=img, which
  can't contain the map's interactive controls.
…umber}

The company page was /~/track?org={slug}. Links now carry the company and
tracking number in the path; the page still opens ?org= and ?order= links.
QR codes keep /~/track-order?order=, which printed labels carry.

Needs the console's /~/:slug/*path route (fleetbase/fleetbase).
roncodes and others added 15 commits October 13, 2026 17:32
Assigning a driver now starts an offer on the order, kept in meta.offer as
{ driver, offered_at, expires_at, accepted_at, declined_at, status } with
status offered, accepted, declined or expired. The offer is written in the
same save as the assignment (order observer "saving"), and by the bulk
assign path. The window is the company setting
fleetops.offer_timeout_minutes (default 5; 0 disables expiry). Adhoc
drivers who take an order through `start` with `assign` start accepted,
and starting an order accepts an open offer.

New public endpoints for the authenticated driver (or a `driver` id):
- POST v1/orders/{id}/accept: accepts the offer (assigns an unassigned
  adhoc order to the driver) and broadcasts `order.offer_accepted`.
- POST v1/orders/{id}/decline { reason? }: records the decline, unassigns
  the driver, broadcasts `order.offer_declined` and notifies dispatch
  (OrderOfferDeclined).

The scheduled fleetops:expire-offers command (every minute) expires offers
past expires_at, raises the `driver_not_accepted` operational alert once per
offer (DriverNotAccepted, meta.operational_alerts.driver_not_accepted) and
broadcasts `order.offer_expired` on order.{public_id}, order.{uuid} and
company.{uuid}. The driver stays assigned. The order resources expose `offer`.
DELETE v1/orders/{id}/cancel and PATCH int/v1/orders/cancel accept an
optional `reason` (string, up to 1000 characters). Order::cancelWithReason()
records it as the details of the canceled tracking status (the config's
activity is cloned, not changed) and carries that activity on the
OrderCanceled event, so NotifyOrderEvent and the driver's canceled
notification include it. The canceled notification data gains `reason` when
one was given. Order::cancel() keeps its signature and behaviour.
fix(tenancy): scope inspection, maintenance, telematics, warranty, proof and route data to the session company
…e verification

Phase 1 of the SMS guard on the FleetOps side (needs core-api with the
hashed-code APIs, fleetbase/core-api#307):

- The customer API no longer persists a User for an identity that merely asked
  for a creation code; the code's subject is the existing user or an unsaved one.
  A users row per unverified identity was a way to fill the table from a leaked
  credential.
- Every SMS code the driver and customer APIs send is issued hashed (HMAC on the
  row, 10 minute expiry, plain code only in the message).
- One verify-and-consume path: create, verify-code, and the password resets check
  the submitted code against the live codes for the identity or user, newest
  first, lock a code after five wrong guesses, and mark the matching one used.
  Plaintext rows written before this verify the same way.
- The public tracking page consumes a verified code instead of deleting it, so
  the row stays for the ledger.
fix(routes): throttle the public SMS-sending routes per address
feat(auth): hashed single-use verification codes, no stub users before verification
The customer and driver login, creation-code and forgot-password endpoints
wrap their SMS send in a catch-all, so a refusal from the guard (a
RuntimeException) was flattened into the generic 400 send error, or into
the email fallback, and an app could neither explain it nor back off.

The refusal now reaches the caller with the status it carries: 422 for a
number that can never be texted, 429 with Retry-After for a cooldown or
budget, 423 while texting is paused. Login still falls back to email when
the account has an address; the refusal is the answer only when nothing
else can carry the code.
feat(orders): status-guarded edits, order.changed events, driver offers
fix(api): answer an SMS guard refusal with its status
…rivers

Assigning a driver started an expiring "offer" in meta.offer that the driver
had to accept, and raised "driver not accepted" when they did not. That is
not how dispatch works: an order assigned to a driver (by dispatch or
auto-assign) is simply theirs. Only adhoc orders are offered, and the
existing adhoc flow already does it: dispatching pings drivers within
adhoc_distance (OrderPing), fleetops:dispatch-adhoc pings again every minute
while nobody has taken it, and a driver takes it with `start` + `assign`.

This reverts "feat(orders): driver offers with accept, decline and expiry":

- meta.offer and its sync on assignment (observer, bulk assign)
- POST v1/orders/{id}/accept and /decline
- fleetops:expire-offers, the driver_not_accepted alert (DriverNotAccepted),
  OrderOfferDeclined, the order.offer_* broadcasts
- `offer` on the order resources and fleetops.offer_timeout_minutes

The cancel reason keeps its request helper, which it shared with decline.
The fixture customer has an email, so the refusal fell back to email and
answered 200 where the test expects the guard's 429. Never ran green: PHP CI
could not resolve core-api ^1.6.69 until it was tagged.
chore(deps): ember-ui 0.4.6, ember-core 0.3.26 and fleetops-data 0.2.3
test(customers): the over-budget login case has no email on file
… its own row

Against core-api 1.6.69 the suite hit the real guard with its counters in the
shared cache: a second text to the same number inside a file tripped the
60-second cooldown, so driver login tests got a 429 refusal where they expect
the transport failure or the refusal they staged themselves. The bootstrap now
seeds sms.guard.enabled = false, as core-api's and storefront's do; the tests
that exercise refusals raise them from the provider fake.

The helper-seams test reused one code for two lookups and omitted the purpose;
a code is used up by the lookup that accepts it and the live-code query is per
purpose, so each check gets its own row and carries 'for'.
The gate never ran before: the suite failed first. With the tests passing it
reports 57 statements short across the order edit, its event, notifications,
policy, tracking publisher, payload sync and the controllers that begin and
refuse edits. This covers them: an edit's policy, marked changes, schedule,
cash, contact and item detection with the kitchen flag, orders with no or an
unsaved payload, the summary's fallbacks; order.changed's customer channel,
session editor, missing driver and null place; push builders reaching for the
FCM and APN clients; a config that cannot answer, cannot load, and times that
do not parse; route changes queued for tracking; caching switched off; the
controllers' begin, locked-edit and switched-payload cash paths; replaced
drivers told on bulk assignment; and the entity sync's non-array inputs.

The older-core branch in ApiCacheThrottle::telemetryWrite() cannot run against
the core this package requires and is marked for coverage with the reason.
test: run the suite against core-api 1.6.69 and cover what its coverage gate flags
@codecov

codecov Bot commented Oct 14, 2026 •

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 100.00%. Comparing base (9ba5d0d) to head (c8071ff).
⚠️ Report is 11 commits behind head on main.

Additional details and impacted files
@@              Coverage Diff              @@
##                main      #372     +/-   ##
=============================================
  Coverage     100.00%   100.00%             
- Complexity     12379     13585   +1206     
=============================================
  Files            600       634     +34     
  Lines          46496     49308   +2812     
=============================================
+ Hits           46496     49308   +2812     
Flag Coverage Δ
backend 100.00% <100.00%> (ø)

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

The contract caller pinned fleetbase/fleetbase at a commit from before the run-order
fixes that the Fleetbase API collection now needs (tracking deletes ahead of the
order delete, the self-terminating activity cycle). Storefront's caller is
deliberately unpinned for the same reason; this matches it. Each run still records
the image digest it resolved.
ci: track the shared API contract workflow on main
@roncodes
roncodes merged commit 4a4ce41 into main Oct 14, 2026
12 of 13 checks passed
@roncodes
roncodes deleted the release/v0.6.72 branch October 14, 2026 09:20
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.

2 participants