Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 15 additions & 8 deletions RELEASE.md
Original file line number Diff line number Diff line change
@@ -1,20 +1,27 @@
> v0.6.66 ~ "Inspections"
> v0.6.67 ~ "Telematics reliability"

---
## What's New
- **Inspection forms are built from typed fields.** A form is groups of fields laid out on a grid: pass/fail checks with severity and on-fail settings, text, numbers, selections, dates, photo uploads and signatures. Forms are built and published from a form builder in the console.
- **One inspection sheet everywhere.** The same sheet is used to fill in an inspection in the console, to read one back, and on a public link. A failed check opens its severity, unsafe flag, comment and photos in place, and a defects tray summarises what needs attention. It works on phones and tablets.
- **Drivers file inspections through the API.** The `v1` inspection endpoints list published forms, file an inspection against one, and read back submissions and a vehicle's history, for the Navigator app and other integrations.
- **Inspection links for anyone in the organisation.** A link can be assigned to any user, protected by a six-digit PIN, and emailed or texted to them. A failed inspection can raise an issue and open a work order, and every submission records who filed it.
- **Safee / DSCO syncs use a few batched requests.** Each sweep fetches every vehicle's current position, speed, heading and odometer in batches of up to 1,000 vehicles, with the vehicle list cached between sweeps. The old sync made three extra requests per vehicle; a 93-vehicle fleet now uses one request per minute instead of about 281. Temperature, door and history lookups stay out of routine polling.
- **Safee sign-in is reused and refreshed.** Access tokens are cached encrypted and refreshed before they expire, requests stay within Safee's 50-per-second account limit, and rate-limit responses are honoured.
- **Telematics can run on dedicated queue workers.** `TELEMATICS_POLL_QUEUE`, `TELEMATICS_INGESTION_QUEUE` and the new `TELEMATICS_BROADCAST_QUEUE` move polling, position processing and live-map broadcasts off the `default` queue for instances that need it. Nothing changes when they are unset. See `docs/TELEMATICS_QUEUES.md`.
- **Live telemetry status is easier to read.** Connection details show telemetry inside the page layout with a compact grid and collapsible setup and failure sections.

---
## Fixes
- Place, zone and service area details, and the place and point map modals, no longer fail to render with "A resolved helper cannot be passed as a named argument".
- Telematics syncs no longer fail with "SyncTelematicDevicesJob has been attempted too many times" for providers on bounded polling. Poll attempts finish within the queue's 90-second reservation, and older queued sync jobs hand off to it instead of running for up to an hour.
- A transient provider or TLS timeout no longer pauses scheduled polling for several minutes, and connections left in an error state keep being polled.
- Vehicles that have never reported a position no longer mark every sweep as partial, quarantine deliveries, or show the connection as degraded.
- Older or delayed samples cannot move a device or attached vehicle position backwards, and partial messages keep existing device identity and metadata.
- AFAQY recovers from unreadable cached tokens and shares one request deadline across sign-in and unit retrieval.
- Pausing Safee polling no longer queues failing sync jobs every minute.
- Device event lookups use a new index instead of full-table scans.
- The Radar dashboard widget links correctly when rendered outside the Fleet-Ops engine.

---
## Testing
- Model and controller contract tests cover inspection forms, submissions, links and the PIN lockout.
- The Fleetbase Postman collection documents the `v1` inspection endpoints.
- Backend tests cover Safee batching, token refresh, rate limits, ordered ingestion, tenant isolation, delivery checkpoints, paused providers, retry backoff and queue routing.
- A local run on a single default queue worker polled a 93-vehicle Safee fleet: 5 sweeps used 7 API requests and each completed with 91 positions applied and 2 vehicles without a fix.

---
## Need help?
Expand Down
2 changes: 1 addition & 1 deletion addon/components/telematic/details.hbs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<Telematic::TelemetryStatus @resource={{@resource}} @provider={{@resource.provider_descriptor}} />
<div class="flex flex-col gap-4 p-4 md:p-6" ...attributes>
<Telematic::TelemetryStatus @resource={{@resource}} @provider={{@resource.provider_descriptor}} />
<section class="grid grid-cols-1 gap-3 lg:grid-cols-3">
{{#each this.healthCards as |card|}}
<div class="fleet-ops-widget fleet-ops-kpi-tile fleetops-connectivity-kpi-tile {{card.accentClass}} relative min-h-[118px] overflow-hidden rounded-md border bg-white shadow-sm transition hover:-translate-y-px hover:shadow-lg dark:bg-gray-800">
Expand Down
127 changes: 93 additions & 34 deletions addon/components/telematic/telemetry-status.hbs
Original file line number Diff line number Diff line change
@@ -1,43 +1,102 @@
<div {{did-update this.resourceChanged @resource.id @resource.public_id this.provider.key this.supported}}>
<div class="contents" {{did-update this.resourceChanged @resource.id @resource.public_id this.provider.key this.supported}}>
{{#if this.supported}}
<section class="space-y-3 rounded-md border border-gray-200 p-3 dark:border-gray-700" data-test-telemetry-status>
<h3 class="text-sm font-bold">Live telemetry</h3>
{{#if this.error}}<p class="text-sm text-red-600" role="alert">{{this.error}}</p>{{/if}}
<section class="min-w-0 space-y-3 rounded-md border border-gray-200 bg-white p-4 dark:border-gray-700 dark:bg-gray-800" data-test-telemetry-status>
<h3 class="text-sm font-bold text-gray-900 dark:text-gray-100">Live telemetry</h3>
{{#if this.error}}<p class="break-words text-xs text-red-600 dark:text-red-400" role="alert">{{this.error}}</p>{{/if}}
{{#if this.diagnostics}}
<dl class="grid grid-cols-2 gap-2 text-xs">
<dt>Polling</dt><dd>{{if this.diagnostics.polling_enabled "Enabled" "Disabled"}}</dd>
<dt>Position webhooks</dt><dd>{{if this.diagnostics.webhooks_enabled "Enabled" "Disabled"}} · {{smart-humanize this.diagnostics.webhook_state}}</dd>
<dt>Last polling sweep</dt><dd>{{n-a this.diagnostics.last_poll.status}} · {{n-a this.diagnostics.last_poll.updated_at}}</dd>
<dt>Units / pages / applied / failed</dt><dd>{{n-a this.diagnostics.last_poll.units}}
/
{{n-a this.diagnostics.last_poll.pages}}
/
{{n-a this.diagnostics.last_poll.applied}}
/
{{n-a this.diagnostics.last_poll.failed}}</dd>
<dt>Last webhook received</dt><dd>{{n-a this.diagnostics.last_webhook.received_at}}</dd>
<dt>Oldest pending delivery</dt><dd>{{n-a this.diagnostics.oldest_pending_at}}</dd>
<dt>Latest queue / source delay (seconds)</dt><dd>{{n-a this.diagnostics.last_ingestion.queue_delay_seconds}}
/
{{n-a this.diagnostics.last_ingestion.source_delay_seconds}}</dd>
<dt>Last polling error</dt><dd>{{n-a this.diagnostics.last_poll.error}}</dd>
<dl class="grid grid-cols-1 gap-x-4 gap-y-3 text-xs sm:grid-cols-2 lg:grid-cols-4">
<div class="min-w-0">
<dt class="text-gray-500 dark:text-gray-400">Polling</dt>
<dd class="mt-1 break-words font-semibold text-gray-900 dark:text-gray-100">{{if this.diagnostics.polling_enabled "Enabled" "Disabled"}}</dd>
</div>
<div class="min-w-0">
<dt class="text-gray-500 dark:text-gray-400">Position webhooks</dt>
<dd class="mt-1 break-words font-semibold text-gray-900 dark:text-gray-100">{{if this.diagnostics.webhooks_enabled "Enabled" "Disabled"}}
·
{{smart-humanize this.diagnostics.webhook_state}}</dd>
</div>
<div class="min-w-0">
<dt class="text-gray-500 dark:text-gray-400">Last polling sweep</dt>
<dd class="mt-1 break-words font-semibold text-gray-900 dark:text-gray-100">{{n-a (smart-humanize this.diagnostics.last_poll.status)}}</dd>
</div>
<div class="min-w-0">
<dt class="text-gray-500 dark:text-gray-400">Sweep updated (UTC)</dt>
<dd class="mt-1 break-words font-semibold text-gray-900 dark:text-gray-100">{{n-a this.diagnostics.last_poll.updated_at}}</dd>
</div>
<div class="min-w-0">
<dt class="text-gray-500 dark:text-gray-400">Units fetched</dt>
<dd class="mt-1 break-words font-semibold text-gray-900 dark:text-gray-100">{{n-a this.diagnostics.last_poll.units}}</dd>
</div>
<div class="min-w-0">
<dt class="text-gray-500 dark:text-gray-400">Pages</dt>
<dd class="mt-1 break-words font-semibold text-gray-900 dark:text-gray-100">{{n-a this.diagnostics.last_poll.pages}}</dd>
</div>
<div class="min-w-0">
<dt class="text-gray-500 dark:text-gray-400">Applied</dt>
<dd class="mt-1 break-words font-semibold text-gray-900 dark:text-gray-100">{{n-a this.diagnostics.last_poll.applied}}</dd>
</div>
<div class="min-w-0">
<dt class="text-gray-500 dark:text-gray-400">Failed</dt>
<dd class="mt-1 break-words font-semibold {{if this.diagnostics.last_poll.failed 'text-red-600 dark:text-red-400' 'text-gray-900 dark:text-gray-100'}}">{{n-a
this.diagnostics.last_poll.failed
}}</dd>
</div>
<div class="min-w-0">
<dt class="text-gray-500 dark:text-gray-400">Last webhook (UTC)</dt>
<dd class="mt-1 break-words font-semibold text-gray-900 dark:text-gray-100">{{n-a this.diagnostics.last_webhook.received_at}}</dd>
</div>
<div class="min-w-0">
<dt class="text-gray-500 dark:text-gray-400">Oldest pending (UTC)</dt>
<dd class="mt-1 break-words font-semibold text-gray-900 dark:text-gray-100">{{n-a this.diagnostics.oldest_pending_at}}</dd>
</div>
<div class="min-w-0">
<dt class="text-gray-500 dark:text-gray-400">Queue delay (seconds)</dt>
<dd class="mt-1 break-words font-semibold text-gray-900 dark:text-gray-100">{{n-a this.diagnostics.last_ingestion.queue_delay_seconds}}</dd>
</div>
<div class="min-w-0">
<dt class="text-gray-500 dark:text-gray-400">Source delay (seconds)</dt>
<dd class="mt-1 break-words font-semibold text-gray-900 dark:text-gray-100">{{n-a this.diagnostics.last_ingestion.source_delay_seconds}}</dd>
</div>
</dl>
{{#if this.diagnostics.last_poll.error}}
<p class="break-words text-xs text-red-600 dark:text-red-400"><span class="font-semibold">Last polling error:</span> {{this.diagnostics.last_poll.error}}</p>
{{/if}}
{{/if}}
{{#if this.supportsSecureWebhooks}}
{{#if this.registrationInstructions}}<p class="text-xs">{{this.registrationInstructions}}</p>{{/if}}
{{#if this.provisional}}<p class="text-xs">The payload adapter is provisional until a real delivery has been verified.</p>{{/if}}
<button type="button" class="btn btn-default btn-sm" disabled={{this.configure.isRunning}} {{on "click" this.reveal}}>Show registration URL</button>
{{#if this.webhookUrl}}
<ClickToCopy @value={{this.webhookUrl}}><Input @value={{this.webhookUrl}} class="form-input w-full" readonly aria-label="Secure telemetry webhook URL" /></ClickToCopy>
<p class="text-xs">This URL contains a secret. Rotating it immediately invalidates the previous URL and requires registration with the provider again.</p>
<button type="button" class="btn btn-default btn-sm" disabled={{this.configure.isRunning}} {{on "click" this.rotate}}>Rotate secret and invalidate old URL</button>
{{/if}}
<details class="border-t border-gray-200 pt-3 text-xs dark:border-gray-700">
<summary class="cursor-pointer font-semibold text-gray-700 dark:text-gray-300">Webhook setup</summary>
<div class="mt-3 space-y-2 text-gray-500 dark:text-gray-400">
{{#if this.registrationInstructions}}<p>{{this.registrationInstructions}}</p>{{/if}}
{{#if this.provisional}}<p>Position webhooks are awaiting verification with a real delivery.</p>{{/if}}
<Button @size="xs" @text="Show registration URL" @disabled={{this.configure.isRunning}} @onClick={{this.reveal}} />
{{#if this.webhookUrl}}
<ClickToCopy @value={{this.webhookUrl}}>
<Input @value={{this.webhookUrl}} class="form-input w-full text-xs" readonly aria-label="Secure telemetry webhook URL" />
</ClickToCopy>
<p>This URL contains a secret. Rotating it immediately invalidates the previous URL and requires registration with the provider again.</p>
<Button @size="xs" @text="Rotate secret" @disabled={{this.configure.isRunning}} @onClick={{this.rotate}} />
{{/if}}
</div>
</details>
{{/if}}
{{#if this.diagnostics.recent_failures.length}}
<details class="border-t border-gray-200 pt-3 text-xs dark:border-gray-700">
<summary class="cursor-pointer font-semibold text-red-600 dark:text-red-400">Recent delivery failures ({{this.diagnostics.recent_failures.length}})</summary>
<ul class="mt-3 max-h-48 space-y-3 overflow-y-auto">
{{#each this.diagnostics.recent_failures as |delivery|}}
<li class="flex flex-wrap items-start justify-between gap-2">
<div class="min-w-0 flex-1 break-words">
<p class="text-gray-500 dark:text-gray-400">{{delivery.received_at}} UTC</p>
<p class="mt-1 text-gray-700 dark:text-gray-300">{{delivery.error}}</p>
</div>
{{#if (eq delivery.status "quarantined")}}
<Button @size="xs" @text="Replay delivery" @onClick={{fn this.replay delivery.uuid}} />
{{/if}}
</li>
{{/each}}
</ul>
</details>
{{/if}}
{{#each this.diagnostics.recent_failures as |delivery|}}
<div class="text-xs"><span>{{delivery.received_at}} · {{delivery.error}}</span>
{{#if (eq delivery.status "quarantined")}}<button type="button" class="btn btn-default btn-sm" {{on "click" (fn this.replay delivery.uuid)}}>Replay delivery</button>{{/if}}
</div>
{{/each}}
</section>
{{/if}}
</div>
2 changes: 1 addition & 1 deletion addon/components/widget/radar.hbs
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
</div>
<div class="mt-auto flex items-center justify-between text-[10px] text-gray-500 dark:text-gray-400">
<span data-test-radar-widget-snoozed>{{this.stats.snoozed}} {{t "widget.radar.snoozed"}}</span>
<LinkTo @route="management.index" class="inline-flex items-center gap-1 font-semibold text-blue-600 hover:text-blue-700 dark:text-blue-300 dark:hover:text-blue-200" data-test-radar-widget-link>
<LinkTo @route={{this.radarRoute}} class="inline-flex items-center gap-1 font-semibold text-blue-600 hover:text-blue-700 dark:text-blue-300 dark:hover:text-blue-200" data-test-radar-widget-link>
{{t "widget.radar.open-radar"}}
<FaIcon @icon="arrow-right" class="text-[9px]" />
</LinkTo>
Expand Down
7 changes: 7 additions & 0 deletions addon/components/widget/radar.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import Component from '@glimmer/component';
import { tracked } from '@glimmer/tracking';
import { inject as service } from '@ember/service';
import { getOwner } from '@ember/application';
import { task } from 'ember-concurrency';
import config from '../../config/environment';

/**
* "Radar · 3 overdue": the dashboard tile that links to the page.
Expand All @@ -21,6 +23,11 @@ export default class WidgetRadarComponent extends Component {
return this.summary?.summary ?? { open: 0, overdue: 0, snoozed: 0, critical: 0 };
}

get radarRoute() {
// Dashboard widgets render under the host owner, outside the engine's routing context.
return getOwner(this).mountPoint ? 'management.index' : `${config.mountedEngineRoutePrefix}.management.index`;
}

get accentClass() {
if (this.stats.critical > 0 || this.stats.overdue > 0) {
return 'kpi-accent-bad';
Expand Down
3 changes: 2 additions & 1 deletion composer.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "fleetbase/fleetops-api",
"version": "0.6.66",
"version": "0.6.67",
"description": "Fleet & Transport Management Extension for Fleetbase",
"keywords": [
"fleetbase-extension",
Expand Down Expand Up @@ -39,6 +39,7 @@
"spatie/icalendar-generator": "^2.8"
},
"require-dev": {
"illuminate/encryption": "^9.0|^10.0",
"friendsofphp/php-cs-fixer": "^3.34.1",
"nunomaduro/collision": "^7.0",
"pestphp/pest": "^2.33.2",
Expand Down
Loading
Loading