@fleetbase/fleetops-engine 0.6.66 → 0.6.68
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/RELEASE.md +9 -8
- package/addon/components/fuel-integration/form.hbs +2 -2
- package/addon/components/fuel-integration/form.js +8 -0
- package/addon/components/fuel-integration/hub.js +1 -1
- package/addon/components/telematic/details.hbs +1 -1
- package/addon/components/telematic/telemetry-status.hbs +93 -34
- package/addon/components/widget/radar.hbs +1 -1
- package/addon/components/widget/radar.js +7 -0
- package/addon/templates/connectivity/fuel-providers/details/index.hbs +1 -1
- package/addon/utils/fuel-transaction.js +2 -0
- package/composer.json +2 -1
- package/docs/AFAQY.md +71 -17
- package/docs/SAFEE.md +130 -0
- package/docs/TELEMATICS_QUEUES.md +230 -0
- package/extension.json +1 -1
- package/package.json +1 -1
- package/server/config/fuel-providers.php +53 -0
- package/server/config/safee.php +10 -0
- package/server/config/telematics.php +8 -1
- package/server/config/telemetry.php +4 -1
- package/server/migrations/2026_09_17_000001_add_device_event_uuid_lookup_index.php +90 -0
- package/server/src/Console/Commands/SyncTelematics.php +11 -5
- package/server/src/Events/DeviceTelemetryUpdated.php +2 -1
- package/server/src/Events/TrailerLocationChanged.php +2 -0
- package/server/src/Events/VehicleLocationChanged.php +5 -0
- package/server/src/Jobs/PollTelematicTelemetry.php +163 -12
- package/server/src/Jobs/ProcessTelematicDelivery.php +64 -10
- package/server/src/Jobs/SyncTelematicDevicesJob.php +77 -1
- package/server/src/Jobs/TestTelematicConnectionJob.php +1 -0
- package/server/src/Providers/FleetOpsServiceProvider.php +5 -2
- package/server/src/Support/FuelProviders/Providers/SascoFuelProvider.php +272 -0
- package/server/src/Support/Telematics/Providers/AfaqyProvider.php +65 -10
- package/server/src/Support/Telematics/Providers/SafeeProvider.php +213 -139
- package/server/src/Support/Telematics/Safee/Transport.php +210 -0
- package/server/src/Support/Telematics/TelematicService.php +45 -1
- package/server/src/Support/Telematics/Telemetry/Configuration.php +16 -0
- package/server/src/Support/Telematics/Telemetry/Inbox.php +49 -3
- package/server/src/Support/Telematics/Telemetry/Ingestor.php +1 -1
- package/server/tests/Feature/Http/AfaqyRealtimeIngestionTest.php +147 -58
- package/server/tests/Feature/Http/Api/DeviceEventUuidIndexMigrationTest.php +106 -0
- package/server/tests/Feature/Http/ManualTelemetryPollingTest.php +545 -0
- package/server/tests/Feature/Http/SafeeRealtimeIngestionTest.php +208 -0
- package/server/tests/Feature/Http/TelematicDeliveryCheckpointTest.php +198 -0
- package/server/tests/Feature/Http/TelematicMixedFleetDeliveryTest.php +166 -0
- package/server/tests/ProviderContractsTest.php +50 -1
- package/server/tests/Support/AfaqyTestCrypto.php +6 -23
- package/server/tests/SupportJobAndAiCoverageTest.php +102 -5
- package/server/tests/TelematicsHardeningTest.php +18 -13
- package/server/tests/Unit/Console/SyncTelematicsCommandTest.php +140 -0
- package/server/tests/Unit/Support/SascoFuelProviderTest.php +338 -0
- package/server/tests/Unit/Support/Telematics/Providers/AfaqyProviderTransportTest.php +132 -0
- package/server/tests/Unit/Support/Telematics/Providers/ProviderTransportSeamsTest.php +1 -1
- package/server/tests/Unit/Support/Telematics/Providers/SafeeLiveContractTest.php +335 -0
- package/server/tests/Unit/Support/Telematics/Providers/SafeeProviderTest.php +17 -27
- package/server/tests/Unit/Support/Telematics/TelematicsQueueRoutingTest.php +36 -0
package/RELEASE.md
CHANGED
|
@@ -1,20 +1,21 @@
|
|
|
1
|
-
> v0.6.
|
|
1
|
+
> v0.6.68 ~ "Telematics sync status and SASCO fuel"
|
|
2
2
|
|
|
3
3
|
---
|
|
4
4
|
## What's New
|
|
5
|
-
- **
|
|
6
|
-
- **
|
|
7
|
-
- **
|
|
8
|
-
- **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.
|
|
5
|
+
- **SASCO is a native fuel provider.** Connect a SASCO B2B account alongside PetroApp to import fuel transactions, with sandbox and production environments, amounts in SAR, driver name and phone, and receipt images. Transactions are matched to vehicles by plate.
|
|
6
|
+
- **"Sync Devices" works while telematics polling is running.** A manual sync requested while a scheduled sweep is queued or running is recorded and completed by the next sweep, instead of failing with "already queued or running".
|
|
7
|
+
- **Telematics connection status stays current.** Each completed scheduled sweep updates the connection status and last sync time, so a connection that has recovered no longer shows "Needs attention" or an old last sync date.
|
|
9
8
|
|
|
10
9
|
---
|
|
11
10
|
## Fixes
|
|
12
|
-
-
|
|
11
|
+
- Manual telematics sync requests can no longer stay queued or failed indefinitely after their job is lost; the next scheduled sweep completes them.
|
|
12
|
+
- Fuel provider environment labels and sync run details no longer refer to PetroApp for other providers.
|
|
13
13
|
|
|
14
14
|
---
|
|
15
15
|
## Testing
|
|
16
|
-
-
|
|
17
|
-
-
|
|
16
|
+
- Backend tests cover manual syncs during scheduled sweeps, stale failure recovery, partial sweeps, and adoption of abandoned requests.
|
|
17
|
+
- SASCO provider tests cover login, token caching, re-login after a 401, connection tests, pagination limits, error handling and transaction normalization.
|
|
18
|
+
- `docs/TELEMATICS_QUEUES.md` now documents that dedicated telematics workers must share the queue worker's image and `APP_KEY`, with verification commands and troubleshooting.
|
|
18
19
|
|
|
19
20
|
---
|
|
20
21
|
## Need help?
|
|
@@ -99,8 +99,8 @@
|
|
|
99
99
|
<div class="grid grid-cols-1 gap-2 lg:grid-cols-2 no-input-group-padding text-xs">
|
|
100
100
|
<InputGroup @name="Environment">
|
|
101
101
|
<select class="form-select w-full" data-test-fuel-environment {{on "change" this.setEnvironment}}>
|
|
102
|
-
<option value="sandbox" selected={{eq this.environment "sandbox"}}>
|
|
103
|
-
<option value="production" selected={{eq this.environment "production"}}>
|
|
102
|
+
<option value="sandbox" selected={{eq this.environment "sandbox"}}>{{this.sandboxLabel}}</option>
|
|
103
|
+
<option value="production" selected={{eq this.environment "production"}}>{{this.productionLabel}}</option>
|
|
104
104
|
</select>
|
|
105
105
|
</InputGroup>
|
|
106
106
|
{{#each this.credentialFields as |field|}}
|
|
@@ -69,6 +69,14 @@ export default class FuelIntegrationFormComponent extends Component {
|
|
|
69
69
|
return this.args.resource?.environment ?? 'production';
|
|
70
70
|
}
|
|
71
71
|
|
|
72
|
+
get sandboxLabel() {
|
|
73
|
+
return this.selectedProvider?.metadata?.environment_labels?.sandbox ?? 'Sandbox';
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
get productionLabel() {
|
|
77
|
+
return this.selectedProvider?.metadata?.environment_labels?.production ?? 'Production';
|
|
78
|
+
}
|
|
79
|
+
|
|
72
80
|
get baseUrl() {
|
|
73
81
|
return this.credentials.base_url?.trim() || this.selectedProvider?.metadata?.base_urls?.[this.environment];
|
|
74
82
|
}
|
|
@@ -116,7 +116,7 @@ export default class FuelIntegrationHubComponent extends Component {
|
|
|
116
116
|
get formatSpend() {
|
|
117
117
|
const currencies = new Map();
|
|
118
118
|
for (const connection of this.connections) {
|
|
119
|
-
const currency = connection.last_sync_state?.summary?.currency || (connection.provider
|
|
119
|
+
const currency = connection.last_sync_state?.summary?.currency || (['petroapp', 'sasco'].includes(connection.provider) ? 'SAR' : null);
|
|
120
120
|
if (currency) currencies.set(currency, (currencies.get(currency) || 0) + Number(connection.last_sync_state?.summary?.amount || 0));
|
|
121
121
|
}
|
|
122
122
|
return [...currencies].map(([currency, amount]) => fuelMoney(amount, currency)).join(' · ') || 'No spend recorded';
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
<Telematic::TelemetryStatus @resource={{@resource}} @provider={{@resource.provider_descriptor}} />
|
|
2
1
|
<div class="flex flex-col gap-4 p-4 md:p-6" ...attributes>
|
|
2
|
+
<Telematic::TelemetryStatus @resource={{@resource}} @provider={{@resource.provider_descriptor}} />
|
|
3
3
|
<section class="grid grid-cols-1 gap-3 lg:grid-cols-3">
|
|
4
4
|
{{#each this.healthCards as |card|}}
|
|
5
5
|
<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">
|
|
@@ -1,43 +1,102 @@
|
|
|
1
|
-
<div {{did-update this.resourceChanged @resource.id @resource.public_id this.provider.key this.supported}}>
|
|
1
|
+
<div class="contents" {{did-update this.resourceChanged @resource.id @resource.public_id this.provider.key this.supported}}>
|
|
2
2
|
{{#if this.supported}}
|
|
3
|
-
<section class="space-y-3 rounded-md border border-gray-200 p-
|
|
4
|
-
<h3 class="text-sm font-bold">Live telemetry</h3>
|
|
5
|
-
{{#if this.error}}<p class="text-
|
|
3
|
+
<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>
|
|
4
|
+
<h3 class="text-sm font-bold text-gray-900 dark:text-gray-100">Live telemetry</h3>
|
|
5
|
+
{{#if this.error}}<p class="break-words text-xs text-red-600 dark:text-red-400" role="alert">{{this.error}}</p>{{/if}}
|
|
6
6
|
{{#if this.diagnostics}}
|
|
7
|
-
<dl class="grid grid-cols-
|
|
8
|
-
<
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
<
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
7
|
+
<dl class="grid grid-cols-1 gap-x-4 gap-y-3 text-xs sm:grid-cols-2 lg:grid-cols-4">
|
|
8
|
+
<div class="min-w-0">
|
|
9
|
+
<dt class="text-gray-500 dark:text-gray-400">Polling</dt>
|
|
10
|
+
<dd class="mt-1 break-words font-semibold text-gray-900 dark:text-gray-100">{{if this.diagnostics.polling_enabled "Enabled" "Disabled"}}</dd>
|
|
11
|
+
</div>
|
|
12
|
+
<div class="min-w-0">
|
|
13
|
+
<dt class="text-gray-500 dark:text-gray-400">Position webhooks</dt>
|
|
14
|
+
<dd class="mt-1 break-words font-semibold text-gray-900 dark:text-gray-100">{{if this.diagnostics.webhooks_enabled "Enabled" "Disabled"}}
|
|
15
|
+
·
|
|
16
|
+
{{smart-humanize this.diagnostics.webhook_state}}</dd>
|
|
17
|
+
</div>
|
|
18
|
+
<div class="min-w-0">
|
|
19
|
+
<dt class="text-gray-500 dark:text-gray-400">Last polling sweep</dt>
|
|
20
|
+
<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>
|
|
21
|
+
</div>
|
|
22
|
+
<div class="min-w-0">
|
|
23
|
+
<dt class="text-gray-500 dark:text-gray-400">Sweep updated (UTC)</dt>
|
|
24
|
+
<dd class="mt-1 break-words font-semibold text-gray-900 dark:text-gray-100">{{n-a this.diagnostics.last_poll.updated_at}}</dd>
|
|
25
|
+
</div>
|
|
26
|
+
<div class="min-w-0">
|
|
27
|
+
<dt class="text-gray-500 dark:text-gray-400">Units fetched</dt>
|
|
28
|
+
<dd class="mt-1 break-words font-semibold text-gray-900 dark:text-gray-100">{{n-a this.diagnostics.last_poll.units}}</dd>
|
|
29
|
+
</div>
|
|
30
|
+
<div class="min-w-0">
|
|
31
|
+
<dt class="text-gray-500 dark:text-gray-400">Pages</dt>
|
|
32
|
+
<dd class="mt-1 break-words font-semibold text-gray-900 dark:text-gray-100">{{n-a this.diagnostics.last_poll.pages}}</dd>
|
|
33
|
+
</div>
|
|
34
|
+
<div class="min-w-0">
|
|
35
|
+
<dt class="text-gray-500 dark:text-gray-400">Applied</dt>
|
|
36
|
+
<dd class="mt-1 break-words font-semibold text-gray-900 dark:text-gray-100">{{n-a this.diagnostics.last_poll.applied}}</dd>
|
|
37
|
+
</div>
|
|
38
|
+
<div class="min-w-0">
|
|
39
|
+
<dt class="text-gray-500 dark:text-gray-400">Failed</dt>
|
|
40
|
+
<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
|
|
41
|
+
this.diagnostics.last_poll.failed
|
|
42
|
+
}}</dd>
|
|
43
|
+
</div>
|
|
44
|
+
<div class="min-w-0">
|
|
45
|
+
<dt class="text-gray-500 dark:text-gray-400">Last webhook (UTC)</dt>
|
|
46
|
+
<dd class="mt-1 break-words font-semibold text-gray-900 dark:text-gray-100">{{n-a this.diagnostics.last_webhook.received_at}}</dd>
|
|
47
|
+
</div>
|
|
48
|
+
<div class="min-w-0">
|
|
49
|
+
<dt class="text-gray-500 dark:text-gray-400">Oldest pending (UTC)</dt>
|
|
50
|
+
<dd class="mt-1 break-words font-semibold text-gray-900 dark:text-gray-100">{{n-a this.diagnostics.oldest_pending_at}}</dd>
|
|
51
|
+
</div>
|
|
52
|
+
<div class="min-w-0">
|
|
53
|
+
<dt class="text-gray-500 dark:text-gray-400">Queue delay (seconds)</dt>
|
|
54
|
+
<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>
|
|
55
|
+
</div>
|
|
56
|
+
<div class="min-w-0">
|
|
57
|
+
<dt class="text-gray-500 dark:text-gray-400">Source delay (seconds)</dt>
|
|
58
|
+
<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>
|
|
59
|
+
</div>
|
|
24
60
|
</dl>
|
|
61
|
+
{{#if this.diagnostics.last_poll.error}}
|
|
62
|
+
<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>
|
|
63
|
+
{{/if}}
|
|
25
64
|
{{/if}}
|
|
26
65
|
{{#if this.supportsSecureWebhooks}}
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
66
|
+
<details class="border-t border-gray-200 pt-3 text-xs dark:border-gray-700">
|
|
67
|
+
<summary class="cursor-pointer font-semibold text-gray-700 dark:text-gray-300">Webhook setup</summary>
|
|
68
|
+
<div class="mt-3 space-y-2 text-gray-500 dark:text-gray-400">
|
|
69
|
+
{{#if this.registrationInstructions}}<p>{{this.registrationInstructions}}</p>{{/if}}
|
|
70
|
+
{{#if this.provisional}}<p>Position webhooks are awaiting verification with a real delivery.</p>{{/if}}
|
|
71
|
+
<Button @size="xs" @text="Show registration URL" @disabled={{this.configure.isRunning}} @onClick={{this.reveal}} />
|
|
72
|
+
{{#if this.webhookUrl}}
|
|
73
|
+
<ClickToCopy @value={{this.webhookUrl}}>
|
|
74
|
+
<Input @value={{this.webhookUrl}} class="form-input w-full text-xs" readonly aria-label="Secure telemetry webhook URL" />
|
|
75
|
+
</ClickToCopy>
|
|
76
|
+
<p>This URL contains a secret. Rotating it immediately invalidates the previous URL and requires registration with the provider again.</p>
|
|
77
|
+
<Button @size="xs" @text="Rotate secret" @disabled={{this.configure.isRunning}} @onClick={{this.rotate}} />
|
|
78
|
+
{{/if}}
|
|
79
|
+
</div>
|
|
80
|
+
</details>
|
|
81
|
+
{{/if}}
|
|
82
|
+
{{#if this.diagnostics.recent_failures.length}}
|
|
83
|
+
<details class="border-t border-gray-200 pt-3 text-xs dark:border-gray-700">
|
|
84
|
+
<summary class="cursor-pointer font-semibold text-red-600 dark:text-red-400">Recent delivery failures ({{this.diagnostics.recent_failures.length}})</summary>
|
|
85
|
+
<ul class="mt-3 max-h-48 space-y-3 overflow-y-auto">
|
|
86
|
+
{{#each this.diagnostics.recent_failures as |delivery|}}
|
|
87
|
+
<li class="flex flex-wrap items-start justify-between gap-2">
|
|
88
|
+
<div class="min-w-0 flex-1 break-words">
|
|
89
|
+
<p class="text-gray-500 dark:text-gray-400">{{delivery.received_at}} UTC</p>
|
|
90
|
+
<p class="mt-1 text-gray-700 dark:text-gray-300">{{delivery.error}}</p>
|
|
91
|
+
</div>
|
|
92
|
+
{{#if (eq delivery.status "quarantined")}}
|
|
93
|
+
<Button @size="xs" @text="Replay delivery" @onClick={{fn this.replay delivery.uuid}} />
|
|
94
|
+
{{/if}}
|
|
95
|
+
</li>
|
|
96
|
+
{{/each}}
|
|
97
|
+
</ul>
|
|
98
|
+
</details>
|
|
35
99
|
{{/if}}
|
|
36
|
-
{{#each this.diagnostics.recent_failures as |delivery|}}
|
|
37
|
-
<div class="text-xs"><span>{{delivery.received_at}} · {{delivery.error}}</span>
|
|
38
|
-
{{#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}}
|
|
39
|
-
</div>
|
|
40
|
-
{{/each}}
|
|
41
100
|
</section>
|
|
42
101
|
{{/if}}
|
|
43
102
|
</div>
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
</div>
|
|
19
19
|
<div class="mt-auto flex items-center justify-between text-[10px] text-gray-500 dark:text-gray-400">
|
|
20
20
|
<span data-test-radar-widget-snoozed>{{this.stats.snoozed}} {{t "widget.radar.snoozed"}}</span>
|
|
21
|
-
<LinkTo @route=
|
|
21
|
+
<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>
|
|
22
22
|
{{t "widget.radar.open-radar"}}
|
|
23
23
|
<FaIcon @icon="arrow-right" class="text-[9px]" />
|
|
24
24
|
</LinkTo>
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import Component from '@glimmer/component';
|
|
2
2
|
import { tracked } from '@glimmer/tracking';
|
|
3
3
|
import { inject as service } from '@ember/service';
|
|
4
|
+
import { getOwner } from '@ember/application';
|
|
4
5
|
import { task } from 'ember-concurrency';
|
|
6
|
+
import config from '../../config/environment';
|
|
5
7
|
|
|
6
8
|
/**
|
|
7
9
|
* "Radar · 3 overdue": the dashboard tile that links to the page.
|
|
@@ -21,6 +23,11 @@ export default class WidgetRadarComponent extends Component {
|
|
|
21
23
|
return this.summary?.summary ?? { open: 0, overdue: 0, snoozed: 0, critical: 0 };
|
|
22
24
|
}
|
|
23
25
|
|
|
26
|
+
get radarRoute() {
|
|
27
|
+
// Dashboard widgets render under the host owner, outside the engine's routing context.
|
|
28
|
+
return getOwner(this).mountPoint ? 'management.index' : `${config.mountedEngineRoutePrefix}.management.index`;
|
|
29
|
+
}
|
|
30
|
+
|
|
24
31
|
get accentClass() {
|
|
25
32
|
if (this.stats.critical > 0 || this.stats.overdue > 0) {
|
|
26
33
|
return 'kpi-accent-bad';
|
|
@@ -30,7 +30,7 @@
|
|
|
30
30
|
<p class="mt-2 text-xs text-gray-500 dark:text-gray-400">Requested dates: {{this.details.latestRun.window}}</p>
|
|
31
31
|
{{#unless this.details.latestRun.received}}
|
|
32
32
|
<p class="mt-3 text-xs leading-5 text-gray-500 dark:text-gray-400">If you expected purchases, choose their dates on the Sync tab and confirm that they belong to this
|
|
33
|
-
|
|
33
|
+
provider account.</p>
|
|
34
34
|
{{/unless}}
|
|
35
35
|
{{else}}
|
|
36
36
|
<p class="text-sm text-gray-900 dark:text-gray-100">No syncs yet</p>
|
|
@@ -99,6 +99,8 @@ export function purchaseGroups(transaction) {
|
|
|
99
99
|
['Account branch', value('branch_name')],
|
|
100
100
|
['Representative', value('delegate_name')],
|
|
101
101
|
['Representative phone', petroapp ? value('delegate_mobile') : null],
|
|
102
|
+
['Driver', value('driver_name')],
|
|
103
|
+
['Driver phone', value('driver_phone')],
|
|
102
104
|
]),
|
|
103
105
|
group('Vehicle & trip references', [
|
|
104
106
|
['Plate number', transaction.plate_number],
|
package/composer.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "fleetbase/fleetops-api",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.68",
|
|
4
4
|
"description": "Fleet & Transport Management Extension for Fleetbase",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"fleetbase-extension",
|
|
@@ -39,6 +39,7 @@
|
|
|
39
39
|
"spatie/icalendar-generator": "^2.8"
|
|
40
40
|
},
|
|
41
41
|
"require-dev": {
|
|
42
|
+
"illuminate/encryption": "^9.0|^10.0",
|
|
42
43
|
"friendsofphp/php-cs-fixer": "^3.34.1",
|
|
43
44
|
"nunomaduro/collision": "^7.0",
|
|
44
45
|
"pestphp/pest": "^2.33.2",
|
package/docs/AFAQY.md
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
AFAQY uses the provider-neutral telemetry infrastructure described in [TELEMETRY_ARCHITECTURE.md](TELEMETRY_ARCHITECTURE.md). Its adapter owns authentication and payload normalization; shared services own persistence, ingestion, and diagnostics.
|
|
6
6
|
|
|
7
|
-
The existing AFAQY integration already used Units List. The new path requests 1,000 units per page with `simplify: 0` and the documented `basic` / `last_update` projection groups. It fetches all pages, places batches of 100
|
|
7
|
+
The existing AFAQY integration already used Units List. The new path requests 1,000 units per page with `simplify: 0` and the documented `basic` / `last_update` projection groups. It fetches all pages, places configurable batches of units (100 by default) in an encrypted durable inbox, and ingests them asynchronously. A connection in transient `error` status remains eligible; `disabled` connections and connections with `meta.telemetry_sync_enabled: false` do not.
|
|
8
8
|
|
|
9
9
|
Position webhooks use the same ingestion path and do not authenticate against AFAQY. The adapter provisionally accepts a unit object, a unit array, or a `data` envelope containing either, including `_id` with nested `data.last_update`. This is an assumption, not a verified AFAQY webhook contract. Unsupported authenticated JSON is quarantined and can be replayed after an adapter correction. Vehicle event webhooks and Signals history are outside this implementation.
|
|
10
10
|
|
|
@@ -12,30 +12,29 @@ Position webhooks use the same ingestion path and do not authenticate against AF
|
|
|
12
12
|
|
|
13
13
|
## Stage 1 deployment
|
|
14
14
|
|
|
15
|
-
1. Apply the Fleet-Ops
|
|
16
|
-
2. Use a shared cache with distributed locks (Redis recommended) across all scheduler and worker instances. An array cache is suitable only for isolated tests. Cached login tokens are encrypted, scoped to account/host/password, and reused for up to 29 days; a credential change selects a new cache namespace. All API attempts for the same host/account share a rolling 60-request/minute budget.
|
|
15
|
+
1. Apply the Fleet-Ops migrations `2026_09_15_000001_create_telematic_telemetry_tables` and `2026_09_17_000001_add_device_event_uuid_lookup_index`. The first adds shared `telematic_deliveries`, `telematic_sync_runs`, and `telematic_webhook_credentials` tables plus a composite device lookup index. The second ensures `device_events.uuid` has a usable leading index, including installations upgraded from the legacy events table. Existing telemetry data is preserved.
|
|
16
|
+
2. Use a shared cache with distributed locks (Redis recommended) across all scheduler and worker instances. An array cache is suitable only for isolated tests. API, queue, and scheduler services must load the same effective `APP_KEY`, cache configuration, and telemetry settings, including Compose overrides and cached Laravel configuration. Preserve the established key that decrypts existing application data; do not generate a replacement as a troubleshooting step. Cached login tokens are encrypted, scoped to account/host/password, and reused for up to 29 days; a credential change selects a new cache namespace. All API attempts for the same host/account share a rolling 60-request/minute budget.
|
|
17
17
|
3. Set the following environment variables and rebuild the application's configuration cache:
|
|
18
18
|
|
|
19
19
|
```dotenv
|
|
20
20
|
AFAQY_POLLING_ENABLED=true
|
|
21
21
|
AFAQY_WEBHOOKS_ENABLED=false
|
|
22
|
-
AFAQY_POLL_QUEUE=afaqy-poll
|
|
23
|
-
AFAQY_INGESTION_QUEUE=afaqy-ingest
|
|
24
22
|
```
|
|
25
23
|
|
|
26
|
-
4.
|
|
24
|
+
4. Keep the existing worker consuming the application's `default` queue, and restart long-running workers after configuration or code changes. Polling, ingestion, and broadcasts use that queue by default. The baseline requires no additional containers, dedicated queues, or custom batch size; ingestion batches default to 100 units.
|
|
27
25
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
```
|
|
26
|
+
One worker can execute the whole pipeline: the polling job enqueues ingestion and returns without waiting for those jobs. Ingestion, broadcasts, and unrelated jobs then share that worker serially. Pending poll ingestion coalesces later polling ticks. This avoids accumulating obsolete sweeps, but it does not establish a particular freshness or throughput bound. Measure complete cycles using the deployment's actual worker count and workload.
|
|
27
|
+
|
|
28
|
+
Remove stale `TELEMATICS_POLL_QUEUE`, `TELEMATICS_INGESTION_QUEUE`, `AFAQY_POLL_QUEUE`, and `AFAQY_INGESTION_QUEUE` overrides unless the existing worker consumes those queues. Jobs sent to an unconsumed queue cannot progress.
|
|
32
29
|
|
|
33
|
-
|
|
30
|
+
Check that the effective queue reservation/retry period is compatible with the longest job the worker consumes, so the broker cannot make a still-running job available again. A worker's CLI timeout does not override a timeout declared by a job; manual discovery declares 3,600 seconds. This is a deployment compatibility check, not a requirement to add queues or prescribe a new reservation period as part of this patch.
|
|
34
31
|
|
|
35
|
-
5. Confirm the application scheduler executes every minute. It runs both `fleetops:sync-telematics` and `fleetops:drain-telematic-inbox`. The drain recovers accepted deliveries if broker dispatch failed or a worker stopped; no payload is acknowledged before durable persistence.
|
|
32
|
+
5. Confirm the application scheduler executes every minute. It runs both `fleetops:sync-telematics` and `fleetops:drain-telematic-inbox`. The drain recovers accepted deliveries if broker dispatch failed or a worker stopped; no payload is acknowledged before durable persistence. Both commands use two-minute overlap leases so an interrupted scheduler cannot block telemetry for the former 24-hour default.
|
|
36
33
|
6. Open the connection's live telemetry panel. Verify a full polling run transitions from fetching to ingesting to completed, with expected unit/page/applied counts. `incomplete` and `partial` are failures to investigate, not complete fleet coverage.
|
|
37
34
|
|
|
38
|
-
|
|
35
|
+
Use the ordinary application scheduler for the baseline. Laravel executes foreground scheduled commands in registration order, so slow unrelated commands can delay polling and inbox recovery even when the telemetry queue is empty. Include scheduler execution time and application startup in diagnosis; distributed command locks are acquired only after Laravel boots. Additional scheduler isolation or worker capacity is an optional operational decision that requires separate measurement, not a prerequisite imposed by this integration.
|
|
36
|
+
|
|
37
|
+
Rate-limit retries honor `Retry-After` (bounded to one hour). Other polling failures use 15/60/180/300-second backoff, with five queue attempts. Poll HTTP requests default to a 45-second total request timeout, configurable with `TELEMATICS_REQUEST_TIMEOUT_SECONDS`, and a connection limit of at most five seconds. Each request is capped by the remaining 90-second fetch budget; changing the request setting does not extend that sweep budget. Sweeps also stop after 100 pages and report incomplete coverage. Pending poll ingestion coalesces later polling ticks. Failed ingestion retries only failed units; the original encrypted body remains available. Five failed/interrupted attempts quarantine the delivery. Internally generated polling batches are normalized per unit: a unit without a position is reported as invalid without discarding valid neighbors. Such a sweep reports partial coverage. External webhook payloads still require the adapter's supported position envelope.
|
|
39
38
|
|
|
40
39
|
## Stage 2 registration
|
|
41
40
|
|
|
@@ -58,22 +57,77 @@ Existing provider interfaces and vehicle/trailer event names remain compatible.
|
|
|
58
57
|
|
|
59
58
|
The public receiver remains `POST webhooks/telematics/afaqy?telematic=...&key=...` under the application's API prefix. A new `device.telemetry_updated` broadcast identifies the device; an open device panel reloads its authorized resource and refetches after socket reconnection. Company broadcast identity comes from the persisted asset, not a worker session. Map movement uses the existing vehicle/trailer events, rejects older source timestamps, and reloads visible telemetry assets after socket reconnection with at most five concurrent requests.
|
|
60
59
|
|
|
61
|
-
Configure shared queues, retention, limits, and freshness under `telematics.telemetry` using `server/config/telemetry.php`. AFAQY's adapter switches and optional queue overrides remain under `telematics.afaqy` in `server/config/afaqy.php`. Defaults: two MiB per webhook, 10,000 pending deliveries per connection, processed payload retention 24 hours, quarantine retention seven days, stale-position thresholds 120 seconds with ignition on and 600 seconds with ignition off/unknown. Each device's metadata records the effective threshold. Connection status categories remain unchanged.
|
|
60
|
+
Configure shared queues, retention, limits, and freshness under `telematics.telemetry` using `server/config/telemetry.php`. `TELEMATICS_BATCH_SIZE` controls units per ingestion delivery (default 100); `TELEMATICS_REQUEST_TIMEOUT_SECONDS` controls the bounded polling request timeout (default 45 seconds). AFAQY's adapter switches and optional queue overrides remain under `telematics.afaqy` in `server/config/afaqy.php`. Defaults: two MiB per webhook, 10,000 pending deliveries per connection, processed payload retention 24 hours, quarantine retention seven days, stale-position thresholds 120 seconds with ignition on and 600 seconds with ignition off/unknown. Each device's metadata records the effective threshold. Connection status categories remain unchanged.
|
|
62
61
|
|
|
63
62
|
## Incident verification
|
|
64
63
|
|
|
65
|
-
|
|
64
|
+
### September 2026 Docker investigation
|
|
65
|
+
|
|
66
|
+
The affected Docker deployment was investigated using live AFAQY responses and its real database, queue workers, and scheduler. The initial implementation's isolated tests had not exercised that deployment. The observed June-to-September gap was not a timezone conversion issue.
|
|
67
|
+
|
|
68
|
+
The investigation identified several independent failures:
|
|
69
|
+
|
|
70
|
+
- **Encryption configuration differed between services.** An application-only Compose override made the API and queue workers load different effective encryption keys. The API's established key successfully decrypted existing ledger and inspection records; that key was preserved and shared across services. Cached-token recovery remains necessary, but repeatedly refreshing tokens cannot repair inconsistent service configuration.
|
|
71
|
+
- **Event lookups scanned a large legacy table.** The deployment had approximately 50,000 event rows occupying 1.2 GB, with no index on `device_events.uuid`. Activity logging refreshes saved models by UUID, producing measured 10–13-second table scans. The UUID-index migration was applied; the measured UUID lookup then took about 7 ms. A subsequent real single-unit ingestion took approximately 1.7 seconds across 25 SQL statements, with about 249 ms spent in SQL. These are different measurements—a prior lookup versus a complete later ingestion—not a controlled whole-fleet speedup ratio.
|
|
72
|
+
- **Manual sync progress was invisible during ingestion.** The job fetched 381 units but retained queued/zero-page metadata while doing database work. It now persists running phases and fetched totals before ingestion, then checkpoints completed counters every 25 records or five seconds and at page boundaries. The manual 381-unit inventory sync completed after the database correction.
|
|
73
|
+
- **Some inventory records had no position.** The live response contained 381 units, of which 379 had valid positions and two lacked `last_update`. The polling worker now processes valid records independently and reports the other two as invalid; it does not discard their whole batches or count them as applied telemetry.
|
|
74
|
+
- **The request deadline was too short for an observed response.** One live Units List response took about 23.6 seconds, exceeding the former 20-second limit. The configurable 45-second request timeout remains constrained by the overall fetch budget.
|
|
75
|
+
- **Automatic processing was not running.** The rollout polling switch was disabled and the general development scheduler was stopped. Enabling it dispatched polling and unrelated scheduled workload. Investigation temporarily used a dedicated telemetry scheduler, four generic workers, and batches of 25. That experiment changed the deployment's worker topology and does not validate the user's single-worker baseline. The temporary services, dedicated queue settings, batch override, and queue reservation changes have since been removed. Configuration is restored to the existing default worker and ordinary application scheduler, retaining the shared established encryption key, enabled polling, schema index, and code corrections.
|
|
76
|
+
- **Scheduler interruption left a 24-hour overlap lock.** Recreating the scheduler during execution left its default overlap lease in the shared cache, preventing later polling dispatches even with empty worker queues. The two telemetry scheduled commands now use two-minute overlap leases, and the sync command's own lock also expires after two minutes. During recovery, only the confirmed abandoned telemetry mutex was cleared after stopping the scheduler; unrelated cache entries and locks were preserved.
|
|
77
|
+
|
|
78
|
+
During the temporary four-worker experiment, the initial eleven automatic sweeps drained 379 valid position records per sweep and explicitly reported the two records without positions as partial coverage. Those sweeps ranged from 22 to 65 seconds with a median of 36 seconds. A later sweep during scheduler recreation and heavy unrelated scheduled workload took 152 seconds; 22–65 seconds is therefore not the full observed range. The subsequent run stalled with retained inbox batches while the host was under heavy load. Its Docker VM had eight CPUs and about 8 GB of memory; the host reported about 16 GB of swap in use and a load average near 48. Pausing a separate stack did not immediately restore normal startup. A bounded startup probe measured about 12 seconds for Composer autoload and about 40 seconds for provider registration/boot, excluding ingestion. Temporary dedicated cron startup wrappers were also removed with that experiment. Recovery and repeated-cycle cadence on the restored single-worker baseline remain open verification gates.
|
|
79
|
+
|
|
80
|
+
These observations establish that the inspected 381-unit fleet was fetched and ingested during the investigation. They do not establish single-worker performance, a consistent sub-minute sweep, the 5,000-unit target, webhook throughput, or end-to-end browser latency. Actual AFAQY webhook delivery remains unverified.
|
|
81
|
+
|
|
82
|
+
### Verification checklist
|
|
83
|
+
|
|
84
|
+
1. Compare the same provider unit identifier across the live Units List record, persisted device, and attached asset. Compare raw UTC `dtt`/`dts` with `device.meta.telemetry`, `last_online_at`, and the asset's source timestamp.
|
|
85
|
+
2. Verify effective API/worker/scheduler configuration, not only the `.env` file. Compare keys securely without printing them; confirm the established key decrypts existing records before aligning services. Rebuild cached configuration and restart long-running processes after changes.
|
|
86
|
+
3. Check applied migrations and the actual query plan for UUID-based event lookup. Confirm the lookup uses an index instead of scanning historical event payloads.
|
|
87
|
+
4. Verify polling is enabled, the scheduler invokes both telemetry commands every minute, and workers consume the effective polling, ingestion, and broadcast queues. Watch pending/retry/processing delivery age and depth. After interruption, inspect the specific scheduler mutex and queued/reserved poll jobs before clearing anything. New scheduler leases expire after two minutes, but an old 24-hour lease already stored in cache keeps its original expiry; remove only a confirmed abandoned telemetry lock. Do not flush the shared cache or clear all scheduled-task locks.
|
|
88
|
+
5. Observe several complete runs. Reconcile fetched, applied, and invalid/failed counts; inspect quarantined records safely. A run with units lacking positions must remain explicitly partial.
|
|
89
|
+
6. Confirm the stored device and asset advance with newer source timestamps and verify their rendered state separately. Database ingestion success does not by itself validate browser/socket delivery.
|
|
90
|
+
|
|
91
|
+
## Cached-token decryption failures
|
|
92
|
+
|
|
93
|
+
A manual sync runs `SyncTelematicDevicesJob`; scheduled telemetry polling uses
|
|
94
|
+
`PollTelematicTelemetry`. Both authenticate through the AFAQY provider. Previously,
|
|
95
|
+
an unreadable encrypted access token in the shared cache raised Laravel's
|
|
96
|
+
`DecryptException` before the provider could refresh the token. This is a cache
|
|
97
|
+
recovery defect; a MAC error alone does not establish that deployment keys differ.
|
|
98
|
+
|
|
99
|
+
The provider now catches decryption failure only for its disposable cached token,
|
|
100
|
+
evicts that one entry under the existing account lock, and authenticates again
|
|
101
|
+
using the already-resolved connection credentials. A replacement token is cached
|
|
102
|
+
encrypted. Login failures and rate limits still propagate. Persisted credentials,
|
|
103
|
+
webhook secrets, and inbox payloads are not reset or treated as plaintext.
|
|
104
|
+
A warning with reason `cached_token_decryption_failed` identifies this path without
|
|
105
|
+
logging secrets. Deploy the patch and restart long-running queue workers before
|
|
106
|
+
retrying. If the exception remains, capture the calling stack frames without
|
|
107
|
+
arguments to identify which encrypted value failed; do not rotate APP_KEY or flush
|
|
108
|
+
the entire shared cache as a diagnostic step.
|
|
109
|
+
|
|
110
|
+
Regression coverage now uses Laravel's real encrypter (`illuminate/encryption` is
|
|
111
|
+
a development dependency), persisted encrypted credentials, and both manual and
|
|
112
|
+
scheduled job handlers. Tampered MACs and foreign-key cache entries fail before
|
|
113
|
+
the fix and recover afterward. Provider HTTP responses remain simulated; these
|
|
114
|
+
checks do not prove successful authentication against a live AFAQY account.
|
|
66
115
|
|
|
67
116
|
## Validation and acceptance gates
|
|
68
117
|
|
|
69
118
|
Run the new provider contract and database ingestion suites with the package Pest runner, and the provider-neutral telemetry browser tests with Ember:
|
|
70
119
|
|
|
71
120
|
```sh
|
|
72
|
-
php scripts/pest-runner.php server/tests/Unit/Support/Telematics/Providers/AfaqyRealtimeContractTest.php
|
|
73
|
-
php scripts/pest-runner.php server/tests/Feature/Http/AfaqyRealtimeIngestionTest.php
|
|
121
|
+
php scripts/pest-runner.php server/tests/Unit/Support/Telematics/Providers/AfaqyRealtimeContractTest.php --debug
|
|
122
|
+
php scripts/pest-runner.php server/tests/Feature/Http/AfaqyRealtimeIngestionTest.php --debug
|
|
123
|
+
php scripts/pest-runner.php server/tests/Feature/Http/TelematicMixedFleetDeliveryTest.php --debug
|
|
124
|
+
php scripts/pest-runner.php server/tests/Feature/Http/Api/DeviceEventUuidIndexMigrationTest.php --debug
|
|
125
|
+
php scripts/pest-runner.php server/tests/SupportJobAndAiCoverageTest.php --filter='manual sync' --debug
|
|
74
126
|
node_modules/.bin/ember test --filter=telemetry
|
|
75
127
|
```
|
|
76
128
|
|
|
129
|
+
The incident patch passed 92 focused backend tests. The incident regressions exercise valid–invalid–valid polling batches through encrypted inbox processing and database ingestion, strict webhook quarantine, UUID-index migration behavior, and manual progress checkpoints/partial failures. The mixed-fleet SQLite fixture disables model listeners and external transports, and the manual-progress tests use provider/service doubles; those tests alone would not expose the deployed activity-log table scan. The live Docker investigation above supplies separate evidence for the actual MySQL schema and model path. Use `--debug` to retain PHPUnit test-pass events and its final exit status when the normal package formatter emits no summary.
|
|
130
|
+
|
|
77
131
|
The original implementation previously processed 5,000 signals plus 5,000 duplicate reconciliation observations in 45.9 seconds in the opt-in local SQLite benchmark (`XDEBUG_MODE=off AFAQY_RUN_LOAD_TESTS=1` with the ingestion suite above). During the provider-neutral refactor review, the final run took 66.5 seconds and failed the local 60-second budget; the original commit repeated in the same runtime took 83.4 seconds. Different machine/build load prevents a controlled speed comparison. The load gate remains open. It uses real model/database ingestion with indexed fixture tables, simulated spatial functions, and captured broadcasts; it excludes provider networking, broker latency, attached-asset load, and browser rendering.
|
|
78
132
|
|
|
79
133
|
The original implementation validation passed 103 selected backend tests, including database ingestion, sensor preservation, retention recovery, and provider contracts. A separate check against the installed Laravel dispatcher verified that broker failures release uniqueness leases. The standard browser launcher is blocked by the linked Testem/execa CommonJS/ES-module mismatch; the two focused browser tests passed all nine assertions in an isolated local harness with lazy engine assets and dummy host configuration. These cover live panel refresh, reconnect, freshness labels, map movement, and reversed broadcasts; they do not exercise a production socket server. PHPStan is not clean in the package-only runtime (unresolved Laravel helpers/model types and strict type findings); it is not counted as a passing gate.
|
package/docs/SAFEE.md
ADDED
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
# Safee / DSCO live polling
|
|
2
|
+
|
|
3
|
+
## Problem and API contract
|
|
4
|
+
|
|
5
|
+
The legacy sync fetched inventory and current state, then made three serial
|
|
6
|
+
requests per vehicle (`last-info`, `positions`, and `events`) before applying a
|
|
7
|
+
second ingestion pass. A 93-vehicle fleet required 281 data requests, excluding
|
|
8
|
+
authentication. Each enrichment request could wait up to 120 seconds. Historical
|
|
9
|
+
samples could overwrite the latest position, and failed connections were excluded
|
|
10
|
+
from subsequent scheduled discovery.
|
|
11
|
+
|
|
12
|
+
Safee's [Tracking REST Service V2.2.4.0](https://www.safee.com/_docs_/Safee_Tracking_REST_Service.pdf)
|
|
13
|
+
documents:
|
|
14
|
+
|
|
15
|
+
- `POST /api/v2/vehicle/list-info` with an empty JSON object for inventory.
|
|
16
|
+
- `POST /api/v2/vehicle/last-state` with `live: true`, `endDate: null`, and a
|
|
17
|
+
`vehicles` ID array to retrieve current state, position, speed, heading, and
|
|
18
|
+
available counters.
|
|
19
|
+
- Up to 1,000 vehicle IDs per request, and 50 requests per user per second.
|
|
20
|
+
- Unix timestamps with fractional seconds. The response envelope's `time` is not
|
|
21
|
+
a vehicle's source timestamp.
|
|
22
|
+
- OAuth access-token expiry and refresh-token grants.
|
|
23
|
+
|
|
24
|
+
There are no position webhooks for this integration. Temperature/door enrichment
|
|
25
|
+
and historical position/event retrieval remain outside routine live polling.
|
|
26
|
+
Existing enrichment APIs remain available for explicit use.
|
|
27
|
+
|
|
28
|
+
## Polling and ingestion
|
|
29
|
+
|
|
30
|
+
Safee opts into the existing provider-neutral telemetry pipeline. No Safee tables,
|
|
31
|
+
UI branches, dedicated queues, or additional workers are introduced.
|
|
32
|
+
|
|
33
|
+
1. The minute scheduler coalesces queued/running polling and pending ingestion.
|
|
34
|
+
Enabled connections remain eligible after transient errors. When
|
|
35
|
+
`SAFEE_POLLING_ENABLED=false`, Safee is paused; it does not fall back to the
|
|
36
|
+
legacy monolithic discovery job.
|
|
37
|
+
2. Inventory is cached briefly. Manual discovery refreshes it. Pagination uses
|
|
38
|
+
stable inventory slices; `list-info` itself is not treated as a paginated API.
|
|
39
|
+
3. Each slice retrieves current states in one batch of at most 1,000 IDs. Missing
|
|
40
|
+
states are reported per unit; malformed, duplicate, or unsuccessful responses
|
|
41
|
+
cannot masquerade as successful empty fleets.
|
|
42
|
+
4. Raw batches enter the shared encrypted durable inbox. Existing default-queue
|
|
43
|
+
jobs apply them transactionally with per-device serialization, deduplication,
|
|
44
|
+
and source-time ordering. Older samples cannot move current device or attached
|
|
45
|
+
asset positions backwards.
|
|
46
|
+
5. Units without a valid fix (for example source date `0`) keep their device link
|
|
47
|
+
and are counted in `invalid_count`, but a polled delivery is not quarantined
|
|
48
|
+
and its run is not marked partial for them. Pushed deliveries keep quarantine.
|
|
49
|
+
6. Partial messages preserve attachment identity and existing metadata, counters,
|
|
50
|
+
and sensors. A zero source date is treated as missing data. Source timestamps
|
|
51
|
+
are converted to UTC; receipt time is never substituted for a missing GPS time.
|
|
52
|
+
|
|
53
|
+
A fresh fleet sweep uses `1 + ceil(N / 1000)` data requests; subsequent sweeps use
|
|
54
|
+
`ceil(N / 1000)` while inventory is cached. These are request-count improvements,
|
|
55
|
+
not guarantees about provider latency or application throughput.
|
|
56
|
+
|
|
57
|
+
Authentication is cached using credential-sensitive keys and encrypted tokens.
|
|
58
|
+
Expiry is honored, a rejected token may be refreshed once, and authentication,
|
|
59
|
+
manual syncs, and retries share the per-user request budget. HTTP429 respects
|
|
60
|
+
`Retry-After`; connection/server failures use bounded queue retry delays. Scheduled
|
|
61
|
+
poll retries wait at most 60 seconds (15, 30, 60, 60) so a transient login or TLS
|
|
62
|
+
timeout cannot suppress minute polling for several minutes; manual requests keep
|
|
63
|
+
15, 60, 180, 300 seconds. Previously queued legacy discovery jobs exit quietly when
|
|
64
|
+
polling is paused or already queued instead of marking the connection as errored.
|
|
65
|
+
|
|
66
|
+
## Configuration and rollout
|
|
67
|
+
|
|
68
|
+
Safee polling is enabled by default because it replaces the previous scheduled
|
|
69
|
+
sync. `SAFEE_POLLING_ENABLED=false` pauses Safee polling/manual batch sync.
|
|
70
|
+
`meta.telemetry_sync_enabled=false` or a disabled connection also prevents work.
|
|
71
|
+
Webhooks remain unsupported.
|
|
72
|
+
|
|
73
|
+
- `SAFEE_INVENTORY_CACHE_SECONDS`: default300, capped at300seconds.
|
|
74
|
+
- `SAFEE_REQUEST_TIMEOUT_SECONDS`: default30seconds, capped by the remaining sweep
|
|
75
|
+
budget. Authentication and data requests share that budget.
|
|
76
|
+
- Existing `TELEMATICS_POLL_QUEUE` and `TELEMATICS_INGESTION_QUEUE` default to
|
|
77
|
+
`default`. One existing queue worker can process both types of jobs.
|
|
78
|
+
To run telematics on dedicated workers for one instance, see
|
|
79
|
+
[Dedicated telematics queue workers](TELEMATICS_QUEUES.md).
|
|
80
|
+
- Poll attempts use an80-second hard timeout and a60-second cooperative sweep
|
|
81
|
+
budget, below the baseline Redis `retry_after=90` seconds.
|
|
82
|
+
|
|
83
|
+
Apply the existing generic telemetry migrations and device-event lookup index,
|
|
84
|
+
then reload long-running workers so they use the new provider class. Clear/rebuild
|
|
85
|
+
cached configuration as part of the application's normal deployment process.
|
|
86
|
+
Do not retry an old failed monolithic job to recover a connection; start a new
|
|
87
|
+
manual sync or let scheduled polling recover it.
|
|
88
|
+
|
|
89
|
+
A completed scheduled sweep updates the connection's status and last sync time, so
|
|
90
|
+
a recovered connection no longer shows an old failure. Clicking "Sync Devices" while
|
|
91
|
+
a sweep is queued or running records the request for the next sweep instead of
|
|
92
|
+
returning an error. A partial sweep leaves the last reported state unchanged.
|
|
93
|
+
|
|
94
|
+
Inspect the shared telemetry diagnostics for fetched/applied/failed counts,
|
|
95
|
+
incomplete sweeps, pending deliveries, queue delay, and source age. Manual sync
|
|
96
|
+
completion is tied to its ingestion run; queueing or fetching alone is not
|
|
97
|
+
reported as a completed sync.
|
|
98
|
+
|
|
99
|
+
## Validation boundaries
|
|
100
|
+
|
|
101
|
+
The local DSCO connection had93 stored units. A read-only API probe on
|
|
102
|
+
2026-09-17 authenticated in2.466seconds, listed all93 units in4.136seconds,
|
|
103
|
+
and fetched all93 current states in3.932seconds. The returned access token
|
|
104
|
+
expired after300seconds. Some units returned source date0; these are invalid
|
|
105
|
+
positions and must remain visible as partial data rather than fabricated fixes.
|
|
106
|
+
|
|
107
|
+
The immediately preceding legacy worker failure was an SSL connection timeout
|
|
108
|
+
at the login endpoint. Subsequent authenticated requests succeeded. This shows
|
|
109
|
+
an intermittent provider/network failure as well as the independently verified
|
|
110
|
+
request-volume, token-refresh, and scheduler-recovery defects.
|
|
111
|
+
|
|
112
|
+
Contract and database tests use controlled fixtures; they do not prove production
|
|
113
|
+
throughput. Production freshness remains bounded by Safee/device reporting,
|
|
114
|
+
provider response time, the minute polling interval, and available capacity of
|
|
115
|
+
the shared worker. Monitor queue growth with the rest of the application's jobs.
|
|
116
|
+
|
|
117
|
+
### Local default-worker run (2026-09-17, 06:40-06:46 UTC)
|
|
118
|
+
|
|
119
|
+
After restarting the single existing `queue:work` worker on the local stack, five
|
|
120
|
+
scheduled sweeps of the 93-unit DSCO connection made 7 Safee requests in total:
|
|
121
|
+
1 token, 1 `list-info`, and 5 `last-state`. Every run completed with 93 units,
|
|
122
|
+
91 applied, 2 counted as invalid, one processed delivery, no Safee failed jobs, and
|
|
123
|
+
9-15 seconds from run creation to processed delivery. The legacy design would have
|
|
124
|
+
made about 281 data requests per sweep.
|
|
125
|
+
|
|
126
|
+
Only 42 of the 91 positioned units had a source fix under five minutes old (median
|
|
127
|
+
age 386 seconds). This reflects when devices last reported; faster polling cannot
|
|
128
|
+
improve it. AFAQY polling on the same worker hit 44-second `units/lists` timeouts
|
|
129
|
+
during this window, and those jobs share that worker's capacity. This run is local
|
|
130
|
+
evidence, not production acceptance.
|