@fleetbase/fleetops-engine 0.6.69 → 0.6.70

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.
Files changed (209) hide show
  1. package/RELEASE.md +41 -8
  2. package/addon/components/admin/telematics-settings.hbs +143 -0
  3. package/addon/components/admin/telematics-settings.js +145 -0
  4. package/addon/components/fuel-integration/hub.hbs +1 -1
  5. package/addon/components/layout/fleet-ops-sidebar.js +37 -17
  6. package/addon/components/map/leaflet-live-map.hbs +4 -242
  7. package/addon/components/map/leaflet-live-map.js +2 -1
  8. package/addon/components/map/marker-card/driver.hbs +57 -0
  9. package/addon/components/map/marker-card/driver.js +3 -0
  10. package/addon/components/map/marker-card/vehicle.hbs +78 -0
  11. package/addon/components/map/marker-card/vehicle.js +3 -0
  12. package/addon/components/telematic/hub.hbs +1 -1
  13. package/addon/components/widget/live-fleet.hbs +8 -40
  14. package/addon/controllers/analytics/reports/index.js +5 -0
  15. package/addon/controllers/connectivity/devices/index.js +4 -0
  16. package/addon/controllers/connectivity/fuel-providers/index.js +6 -4
  17. package/addon/controllers/connectivity/sensors/index.js +4 -0
  18. package/addon/controllers/connectivity/telematics/index.js +4 -0
  19. package/addon/controllers/maintenance/equipment/index.js +11 -4
  20. package/addon/controllers/maintenance/inspection-forms/index.js +2 -2
  21. package/addon/controllers/maintenance/inspection-submissions/index.js +8 -2
  22. package/addon/controllers/maintenance/maintenances/index.js +4 -0
  23. package/addon/controllers/maintenance/parts/index.js +11 -4
  24. package/addon/controllers/maintenance/schedules/index.js +17 -4
  25. package/addon/controllers/maintenance/work-orders/index.js +11 -4
  26. package/addon/controllers/management/contacts/index.js +4 -0
  27. package/addon/controllers/management/drivers/index.js +4 -0
  28. package/addon/controllers/management/fleets/index.js +4 -0
  29. package/addon/controllers/management/fuel-reports/index.js +4 -0
  30. package/addon/controllers/management/fuel-transactions/index.js +9 -7
  31. package/addon/controllers/management/issues/index.js +4 -0
  32. package/addon/controllers/management/places/index.js +4 -0
  33. package/addon/controllers/management/vehicles/index.js +4 -0
  34. package/addon/controllers/management/vendors/index.js +4 -0
  35. package/addon/controllers/operations/orders/index/details.js +30 -1
  36. package/addon/controllers/operations/orders/index.js +7 -0
  37. package/addon/controllers/operations/service-rates/index.js +3 -0
  38. package/addon/controllers/settings/index.js +13 -0
  39. package/addon/controllers/settings/telematics.js +113 -0
  40. package/addon/extension.js +38 -6
  41. package/addon/routes/analytics/index.js +14 -1
  42. package/addon/routes/analytics/reports/index/details.js +1 -1
  43. package/addon/routes/analytics/reports/index/edit.js +1 -1
  44. package/addon/routes/analytics/reports/index/new.js +14 -1
  45. package/addon/routes/analytics/reports.js +14 -1
  46. package/addon/routes/connectivity/devices/index/new.js +14 -1
  47. package/addon/routes/connectivity/devices.js +14 -1
  48. package/addon/routes/connectivity/events.js +14 -1
  49. package/addon/routes/connectivity/fuel-providers/details/transactions.js +12 -0
  50. package/addon/routes/connectivity/fuel-providers/details.js +9 -0
  51. package/addon/routes/connectivity/fuel-providers/edit.js +9 -0
  52. package/addon/routes/connectivity/fuel-providers/new.js +14 -1
  53. package/addon/routes/connectivity/fuel-providers.js +14 -1
  54. package/addon/routes/connectivity/sensors/index/new.js +14 -1
  55. package/addon/routes/connectivity/sensors.js +14 -1
  56. package/addon/routes/connectivity/telematics/new.js +14 -1
  57. package/addon/routes/connectivity/telematics.js +14 -1
  58. package/addon/routes/connectivity/tracking.js +14 -1
  59. package/addon/routes/maintenance/equipment/index/details.js +2 -2
  60. package/addon/routes/maintenance/equipment/index/edit.js +2 -2
  61. package/addon/routes/maintenance/equipment/index/new.js +14 -1
  62. package/addon/routes/maintenance/equipment.js +14 -1
  63. package/addon/routes/maintenance/index.js +14 -1
  64. package/addon/routes/maintenance/inspection-forms/index/details.js +10 -1
  65. package/addon/routes/maintenance/inspection-forms/index/edit.js +10 -1
  66. package/addon/routes/maintenance/inspection-forms/index/new.js +14 -1
  67. package/addon/routes/maintenance/inspection-forms.js +14 -1
  68. package/addon/routes/maintenance/inspection-submissions/index/details.js +10 -1
  69. package/addon/routes/maintenance/inspection-submissions/index/edit.js +10 -1
  70. package/addon/routes/maintenance/inspection-submissions/index/new.js +14 -1
  71. package/addon/routes/maintenance/inspection-submissions.js +14 -1
  72. package/addon/routes/maintenance/maintenances/index/details.js +2 -2
  73. package/addon/routes/maintenance/maintenances/index/edit.js +2 -2
  74. package/addon/routes/maintenance/maintenances/index/new.js +14 -1
  75. package/addon/routes/maintenance/maintenances.js +14 -1
  76. package/addon/routes/maintenance/parts/index/details.js +2 -2
  77. package/addon/routes/maintenance/parts/index/edit.js +2 -2
  78. package/addon/routes/maintenance/parts/index/new.js +14 -1
  79. package/addon/routes/maintenance/parts.js +14 -1
  80. package/addon/routes/maintenance/schedules/index/details.js +2 -2
  81. package/addon/routes/maintenance/schedules/index/edit.js +2 -2
  82. package/addon/routes/maintenance/schedules/index/new.js +15 -1
  83. package/addon/routes/maintenance/schedules.js +15 -1
  84. package/addon/routes/maintenance/work-orders/index/details.js +2 -2
  85. package/addon/routes/maintenance/work-orders/index/edit.js +2 -2
  86. package/addon/routes/maintenance/work-orders/index/new.js +14 -1
  87. package/addon/routes/maintenance/work-orders.js +14 -1
  88. package/addon/routes/management/fuel-transactions/index/details.js +9 -0
  89. package/addon/routes/management/fuel-transactions.js +14 -1
  90. package/addon/routes/management/index.js +13 -0
  91. package/addon/routes/management/trailers.js +16 -0
  92. package/addon/routes/management/vendors/integrated/details.js +14 -1
  93. package/addon/routes/management/vendors/integrated/edit.js +14 -1
  94. package/addon/routes/management/vendors/integrated/new.js +14 -1
  95. package/addon/routes/management/vendors/integrated.js +14 -1
  96. package/addon/routes/operations/orders.js +40 -1
  97. package/addon/routes/operations/routes/index/details.js +14 -1
  98. package/addon/routes/operations/routes/index/new.js +13 -0
  99. package/addon/routes/operations/routes.js +14 -1
  100. package/addon/routes/operations/service-rates/index/new.js +11 -0
  101. package/addon/routes/operations/service-rates.js +14 -1
  102. package/addon/routes/settings/avatars.js +14 -1
  103. package/addon/routes/settings/custom-fields.js +14 -1
  104. package/addon/routes/settings/map.js +14 -1
  105. package/addon/routes/settings/notifications.js +11 -0
  106. package/addon/routes/settings/orchestrator.js +1 -1
  107. package/addon/routes/settings/payments/index.js +1 -1
  108. package/addon/routes/settings/routing.js +14 -1
  109. package/addon/routes/settings/scheduling.js +14 -1
  110. package/addon/routes/settings/telematics.js +21 -0
  111. package/addon/routes/virtual.js +11 -0
  112. package/addon/routes.js +1 -0
  113. package/addon/templates/settings/telematics.hbs +70 -0
  114. package/app/components/admin/telematics-settings.js +1 -0
  115. package/app/components/map/marker-card/driver.js +1 -0
  116. package/app/components/map/marker-card/vehicle.js +1 -0
  117. package/app/controllers/settings/telematics.js +1 -0
  118. package/app/routes/settings/telematics.js +1 -0
  119. package/app/templates/settings/telematics.js +1 -0
  120. package/composer.json +2 -2
  121. package/docs/AFAQY.md +1 -1
  122. package/docs/TELEMATICS_QUEUES.md +1 -1
  123. package/docs/TELEMETRY_ARCHITECTURE.md +72 -1
  124. package/extension.json +1 -1
  125. package/package.json +1 -1
  126. package/server/config/telemetry.php +12 -0
  127. package/server/migrations/2026_09_23_000001_add_retention_index_to_device_events.php +40 -0
  128. package/server/migrations/2026_09_23_000002_add_retention_index_to_positions.php +40 -0
  129. package/server/migrations/2026_09_23_000003_add_retention_index_to_telematic_sync_runs.php +40 -0
  130. package/server/src/Auth/Schemas/FleetOps.php +104 -5
  131. package/server/src/Console/Commands/DrainTelematicInbox.php +3 -13
  132. package/server/src/Console/Commands/ProcessOperationalAlerts.php +3 -3
  133. package/server/src/Console/Commands/PruneTelematicsData.php +318 -0
  134. package/server/src/Http/Controllers/Api/v1/OrchestrationController.php +9 -0
  135. package/server/src/Http/Controllers/FleetOpsController.php +21 -0
  136. package/server/src/Http/Controllers/Internal/v1/AnalyticsController.php +11 -0
  137. package/server/src/Http/Controllers/Internal/v1/ContactController.php +11 -0
  138. package/server/src/Http/Controllers/Internal/v1/CustomerController.php +14 -0
  139. package/server/src/Http/Controllers/Internal/v1/DeviceController.php +13 -0
  140. package/server/src/Http/Controllers/Internal/v1/DeviceEventController.php +11 -0
  141. package/server/src/Http/Controllers/Internal/v1/DriverController.php +22 -0
  142. package/server/src/Http/Controllers/Internal/v1/FuelProviderConnectionController.php +13 -0
  143. package/server/src/Http/Controllers/Internal/v1/FuelProviderTransactionController.php +15 -0
  144. package/server/src/Http/Controllers/Internal/v1/FuelReportController.php +9 -3
  145. package/server/src/Http/Controllers/Internal/v1/InspectionFormController.php +15 -0
  146. package/server/src/Http/Controllers/Internal/v1/LiveController.php +16 -0
  147. package/server/src/Http/Controllers/Internal/v1/MaintenanceController.php +15 -0
  148. package/server/src/Http/Controllers/Internal/v1/MaintenanceScheduleController.php +15 -0
  149. package/server/src/Http/Controllers/Internal/v1/ManifestController.php +15 -0
  150. package/server/src/Http/Controllers/Internal/v1/MetricsController.php +11 -0
  151. package/server/src/Http/Controllers/Internal/v1/NavigatorController.php +42 -1
  152. package/server/src/Http/Controllers/Internal/v1/OrchestrationController.php +12 -0
  153. package/server/src/Http/Controllers/Internal/v1/OrderController.php +22 -0
  154. package/server/src/Http/Controllers/Internal/v1/PaymentController.php +13 -0
  155. package/server/src/Http/Controllers/Internal/v1/RadarController.php +19 -0
  156. package/server/src/Http/Controllers/Internal/v1/ServiceAreaController.php +9 -3
  157. package/server/src/Http/Controllers/Internal/v1/SettingController.php +465 -14
  158. package/server/src/Http/Controllers/Internal/v1/TelematicController.php +21 -0
  159. package/server/src/Http/Controllers/Internal/v1/TrailerController.php +21 -0
  160. package/server/src/Http/Controllers/Internal/v1/VehicleController.php +21 -0
  161. package/server/src/Http/Controllers/Internal/v1/VendorController.php +17 -0
  162. package/server/src/Http/Controllers/Internal/v1/WorkOrderController.php +11 -0
  163. package/server/src/Jobs/DispatchTelematicsRetentionJobs.php +70 -0
  164. package/server/src/Jobs/PruneTelematicsDataJob.php +74 -0
  165. package/server/src/Listeners/NotifyDriverOnShiftChange.php +3 -3
  166. package/server/src/Mail/CustomerCredentialsMail.php +1 -1
  167. package/server/src/Orchestration/Engines/VroomOrchestrationEngine.php +9 -1
  168. package/server/src/Providers/FleetOpsServiceProvider.php +3 -0
  169. package/server/src/Support/Analytics/LiveFleet.php +18 -2
  170. package/server/src/Support/Authorization.php +103 -0
  171. package/server/src/Support/Database/TableIndexes.php +66 -0
  172. package/server/src/Support/OrderTracker.php +2 -2
  173. package/server/src/Support/Reporting/FleetOpsReportSchema.php +476 -116
  174. package/server/src/Support/Telematics/Retention/RetentionPolicy.php +300 -0
  175. package/server/src/Support/Telematics/Retention/TelemetryActivity.php +40 -0
  176. package/server/src/Support/Telematics/TelematicService.php +12 -2
  177. package/server/src/Support/Telematics/Telemetry/Ingestor.php +4 -2
  178. package/server/src/Tracking/TrackingIntelligenceService.php +1 -1
  179. package/server/src/Tracking/TrackingOptions.php +2 -2
  180. package/server/src/Traits/AuthorizesMethods.php +37 -0
  181. package/server/src/routes.php +9 -1
  182. package/server/tests/AnalyticsRoutesTest.php +29 -2
  183. package/server/tests/Feature/Http/AfaqyRealtimeIngestionTest.php +16 -1
  184. package/server/tests/Feature/Http/Api/TelematicsRetentionIndexMigrationTest.php +115 -0
  185. package/server/tests/Feature/Http/Internal/SettingControllerHelpersTest.php +71 -0
  186. package/server/tests/Feature/Http/Internal/TelematicsStorageDeadlinesTest.php +119 -0
  187. package/server/tests/InternalImportExportControllerContractsTest.php +5 -8
  188. package/server/tests/NavigatorControllerContractsTest.php +36 -4
  189. package/server/tests/NotificationAndMailContractsTest.php +31 -0
  190. package/server/tests/ObserverContractsTest.php +1 -1
  191. package/server/tests/OrchestratorConsumableApiTest.php +42 -0
  192. package/server/tests/ProviderContractsTest.php +10 -0
  193. package/server/tests/ReportSchemaContractsTest.php +120 -6
  194. package/server/tests/SettingControllerContractsTest.php +429 -18
  195. package/server/tests/SmallControllerContractsTest.php +8 -5
  196. package/server/tests/Support/PruneTelematicsDataProbe.php +55 -0
  197. package/server/tests/Unit/Console/Commands/ProcessOperationalAlertsCommandTest.php +2 -2
  198. package/server/tests/Unit/Console/PruneTelematicsDataCommandTest.php +353 -0
  199. package/server/tests/Unit/Jobs/DispatchTelematicsRetentionJobsTest.php +114 -0
  200. package/server/tests/Unit/Jobs/PruneTelematicsDataJobTest.php +60 -0
  201. package/server/tests/Unit/Orchestration/VroomEngineTransportAndSettingsTest.php +20 -0
  202. package/server/tests/Unit/Support/Analytics/LiveFleetTest.php +6 -0
  203. package/server/tests/Unit/Support/AuthorizationTest.php +142 -0
  204. package/server/tests/Unit/Support/MaintainableAndOperationalAlertsTest.php +12 -1
  205. package/server/tests/Unit/Support/MoreQuerySeamsTest.php +11 -2
  206. package/server/tests/Unit/Support/Telematics/Retention/RetentionPolicyTest.php +190 -0
  207. package/server/tests/Unit/Support/Telematics/Retention/TelemetryActivityTest.php +68 -0
  208. package/server/tests/Unit/Tracking/TrackingOptionsCompanySettingsTest.php +109 -0
  209. package/translations/en-us.yaml +48 -0
@@ -0,0 +1,142 @@
1
+ <?php
2
+
3
+ use Fleetbase\FleetOps\Support\Authorization;
4
+ use Fleetbase\FleetOps\Traits\AuthorizesMethods;
5
+ use Fleetbase\Models\User;
6
+ use Illuminate\Database\ConnectionResolver;
7
+ use Illuminate\Database\Eloquent\Model;
8
+ use Illuminate\Database\SQLiteConnection;
9
+ use Illuminate\Http\Exceptions\HttpResponseException;
10
+ use Illuminate\Http\Request;
11
+ use Illuminate\Routing\Controller;
12
+
13
+ if (!function_exists('Fleetbase\\Support\\auth')) {
14
+ eval('namespace Fleetbase\\Support; function auth() { return \\app("fleetops.authorization.test-auth"); }');
15
+ }
16
+
17
+ if (!function_exists('Fleetbase\\Support\\session')) {
18
+ eval('namespace Fleetbase\\Support; function session($key = null, $default = null) { return \\session($key, $default); }');
19
+ }
20
+
21
+ class FleetOpsAuthorizationUser extends User
22
+ {
23
+ public array $grantedPermissions = [];
24
+ public bool $permissionStoreUnavailable = false;
25
+
26
+ public function hasPermissionTo($permission, $guardName = null): bool
27
+ {
28
+ if ($this->permissionStoreUnavailable) {
29
+ throw new RuntimeException('Permission store unavailable');
30
+ }
31
+
32
+ return in_array($permission->name, $this->grantedPermissions, true);
33
+ }
34
+ }
35
+
36
+ beforeEach(function () {
37
+ $connection = new SQLiteConnection(new PDO('sqlite::memory:'));
38
+ $resolver = new ConnectionResolver(['mysql' => $connection]);
39
+ $resolver->setDefaultConnection('mysql');
40
+ Model::setConnectionResolver($resolver);
41
+ config()->set('auth.defaults.guard', 'sanctum');
42
+ config()->set('permission.table_names.permissions', 'permissions');
43
+ $connection->getSchemaBuilder()->create('permissions', function ($table) {
44
+ $table->string('id')->primary();
45
+ $table->string('name');
46
+ $table->string('guard_name')->default('sanctum');
47
+ });
48
+ $connection->table('permissions')->insert([
49
+ ['id' => 'optimize', 'name' => 'fleet-ops optimize order'],
50
+ ['id' => 'dispatch', 'name' => 'fleet-ops dispatch order'],
51
+ ['id' => 'resource', 'name' => 'fleet-ops * order'],
52
+ ['id' => 'service', 'name' => 'fleet-ops *'],
53
+ ]);
54
+ session(['user' => null]);
55
+ $this->user = new FleetOpsAuthorizationUser();
56
+ $this->user->setRawAttributes(['type' => 'user'], true);
57
+ $this->auth = new class($this->user) {
58
+ public bool $unavailable = false;
59
+
60
+ public function __construct(public ?User $currentUser)
61
+ {
62
+ }
63
+
64
+ public function user(): ?User
65
+ {
66
+ if ($this->unavailable) {
67
+ throw new RuntimeException('Session unavailable');
68
+ }
69
+
70
+ return $this->currentUser;
71
+ }
72
+ };
73
+ app()->instance('fleetops.authorization.test-auth', $this->auth);
74
+ });
75
+
76
+ test('operation authorization accepts exact and wildcard grants', function (string $grant) {
77
+ $this->user->grantedPermissions = [$grant];
78
+
79
+ expect(Authorization::canAny('optimize order'))->toBeTrue();
80
+ Authorization::authorize('optimize order');
81
+ })->with(['fleet-ops optimize order', 'fleet-ops * order', 'fleet-ops *']);
82
+
83
+ test('operation authorization accepts any requested permission and rejects missing grants', function () {
84
+ $this->user->grantedPermissions = ['fleet-ops dispatch order'];
85
+
86
+ expect(Authorization::canAny('optimize order'))->toBeFalse()
87
+ ->and(Authorization::canAny('optimize order', 'dispatch order'))->toBeTrue()
88
+ ->and(Authorization::canAny('list unseeded-resource'))->toBeTrue();
89
+ expect(fn () => Authorization::authorize('optimize order'))->toThrow(HttpResponseException::class);
90
+
91
+ $this->user->permissionStoreUnavailable = true;
92
+ expect(Authorization::canAny('dispatch order'))->toBeFalse();
93
+ });
94
+
95
+ test('platform administrators pass operations while system settings reject ordinary users', function () {
96
+ expect(fn () => Authorization::authorizeAdmin())->toThrow(HttpResponseException::class);
97
+ $this->user->setAttribute('type', 'admin');
98
+ expect(Authorization::canAny('optimize order'))->toBeTrue();
99
+ Authorization::authorizeAdmin();
100
+ });
101
+
102
+ test('authorization fails closed when the authenticated user is missing or cannot be read', function () {
103
+ $this->auth->currentUser = null;
104
+ expect(Authorization::canAny('optimize order'))->toBeFalse();
105
+ expect(fn () => Authorization::authorizeAdmin())->toThrow(HttpResponseException::class);
106
+ $this->auth->unavailable = true;
107
+ expect(Authorization::canAny('optimize order'))->toBeFalse();
108
+ });
109
+
110
+ test('controller middleware applies the mapped operation and administrator checks', function () {
111
+ $controller = new class extends Controller {
112
+ use AuthorizesMethods;
113
+
114
+ public function __construct()
115
+ {
116
+ $this->authorizeMethods(['optimize' => ['optimize order', 'dispatch order'], 'settings' => 'admin']);
117
+ }
118
+ };
119
+ $middleware = $controller->getMiddleware();
120
+ $request = Request::create('/');
121
+ $this->user->grantedPermissions = ['fleet-ops dispatch order'];
122
+
123
+ expect($middleware[0]['options']['only'])->toBe(['optimize'])
124
+ ->and($middleware[0]['middleware']($request, fn ($nextRequest) => $nextRequest))->toBe($request);
125
+ expect(fn () => $middleware[1]['middleware']($request, fn () => 'allowed'))->toThrow(HttpResponseException::class);
126
+ $this->user->setAttribute('type', 'admin');
127
+ expect($middleware[1]['middleware']($request, fn () => 'allowed'))->toBe('allowed');
128
+ });
129
+
130
+ test('analytics controllers enforce authorization before invoking their next middleware', function (string $class) {
131
+ $connection = Model::getConnectionResolver()->connection();
132
+ $connection->table('permissions')->insert(['id' => 'analytics', 'name' => 'fleet-ops view analytics']);
133
+ $controller = new $class();
134
+ $middleware = $controller->getMiddleware()[0]['middleware'];
135
+ $request = Request::create('/analytics');
136
+ expect(fn () => $middleware($request, fn () => 'private analytics'))->toThrow(HttpResponseException::class);
137
+ $this->user->grantedPermissions = ['fleet-ops view analytics'];
138
+ expect($middleware($request, fn ($forwarded) => $forwarded))->toBe($request);
139
+ })->with([
140
+ Fleetbase\FleetOps\Http\Controllers\Internal\v1\AnalyticsController::class,
141
+ Fleetbase\FleetOps\Http\Controllers\Internal\v1\MetricsController::class,
142
+ ]);
@@ -184,10 +184,21 @@ test('operational alert helpers query orders positions and settings', function (
184
184
  $order = Order::where('uuid', 'order-1')->withoutGlobalScopes()->first();
185
185
  expect($probe->callHelper('latestPositionForOrder', $order))->toBeInstanceOf(Position::class);
186
186
 
187
- $settings = $probe->callHelper('alertSettings');
187
+ $settings = $probe->callHelper('alertSettings', $order);
188
188
  expect($settings['late_departures']['enabled'])->toBeTrue()
189
189
  ->and($settings['route_deviations']['distance_threshold_meters'])->toBe(500)
190
190
  ->and($settings['prolonged_stoppages']['duration_threshold_minutes'])->toBe(30);
191
+
192
+ // Settings resolve from the order's company without a company session
193
+ $connection->table('settings')->insert([
194
+ 'key' => 'company.company-1.tracking',
195
+ 'value' => json_encode(['alerts' => ['late_departures' => ['enabled' => false], 'route_deviations' => ['distance_threshold_meters' => 750]]]),
196
+ ]);
197
+ session(['company' => null]);
198
+ $settings = $probe->callHelper('alertSettings', $order);
199
+ session(['company' => 'company-1']);
200
+ expect($settings['late_departures']['enabled'])->toBeFalse()
201
+ ->and($settings['route_deviations']['distance_threshold_meters'])->toBe(750);
191
202
  });
192
203
 
193
204
  test('route point collection normalizes pairs and measures distances', function () {
@@ -384,13 +384,22 @@ test('index driver resource counts assigned orders and names the current one', f
384
384
  });
385
385
 
386
386
  test('shift change listener reads company scheduling settings', function () {
387
- fleetopsMoreSeamBoot();
387
+ $connection = fleetopsMoreSeamBoot();
388
388
 
389
389
  $class = Fleetbase\FleetOps\Listeners\NotifyDriverOnShiftChange::class;
390
390
  $listener = (new ReflectionClass($class))->newInstanceWithoutConstructor();
391
391
 
392
392
  // With nothing stored the lookup falls back to the empty default
393
- expect(fleetopsMoreSeamInvoke($listener, $class, 'getSchedulingSettings'))->toBe([]);
393
+ expect(fleetopsMoreSeamInvoke($listener, $class, 'getSchedulingSettings', ['company-seam-2']))->toBe([]);
394
+
395
+ // Queued listeners have no company session, so the schedule's company is used
396
+ $connection->table('settings')->insert(['key' => 'company.company-seam-3.fleet-ops.scheduling-settings', 'value' => json_encode(['notify_drivers_on_shift_change' => true])]);
397
+ session(['company' => null]);
398
+ $settings = fleetopsMoreSeamInvoke($listener, $class, 'getSchedulingSettings', ['company-seam-3']);
399
+ session(['company' => 'company-seam-2']);
400
+
401
+ expect($settings)->toBe(['notify_drivers_on_shift_change' => true])
402
+ ->and(fleetopsMoreSeamInvoke($listener, $class, 'getSchedulingSettings', [null]))->toBe([]);
394
403
  });
395
404
 
396
405
  test('order insights capability builds its own company-scoped order query', function () {
@@ -0,0 +1,190 @@
1
+ <?php
2
+
3
+ use Fleetbase\FleetOps\Support\Telematics\Retention\RetentionPolicy;
4
+ use Illuminate\Support\Carbon;
5
+
6
+ /**
7
+ * RetentionPolicy combines administrator policy with allowed company history
8
+ * preferences, enforces maximums, and caches policies for long-lived workers.
9
+ */
10
+ function fleetopsRetentionResolver(array $system, array $company, ?array &$calls = null): void
11
+ {
12
+ $calls = [];
13
+ RetentionPolicy::$settingsResolver = function (string $scope, string $key, mixed $default, ?string $companyUuid) use ($system, $company, &$calls) {
14
+ $calls[] = [$scope, $key, $companyUuid];
15
+ expect($key)->toBe(RetentionPolicy::SETTING_KEY);
16
+
17
+ return $scope === 'system' ? $system : ($company[$companyUuid] ?? $default);
18
+ };
19
+ }
20
+
21
+ beforeEach(function () {
22
+ RetentionPolicy::flush();
23
+ RetentionPolicy::$settingsResolver = null;
24
+ config(['telematics.telemetry' => ['event_retention_days' => 45, 'processed_retention_hours' => 48, 'log_telemetry_activity' => true, 'poll_queue' => 'default']]);
25
+ });
26
+
27
+ afterEach(function () {
28
+ RetentionPolicy::flush();
29
+ RetentionPolicy::$settingsResolver = null;
30
+ Carbon::setTestNow();
31
+ config(['telematics.telemetry' => []]);
32
+ });
33
+
34
+ test('defaults come from package config, overridden by the admin setting, with everything clamped', function () {
35
+ fleetopsRetentionResolver([], []);
36
+ expect(RetentionPolicy::defaults())->toBe([
37
+ 'event_retention_days' => 45,
38
+ 'event_compact_after_days' => 7,
39
+ 'position_retention_days' => 90,
40
+ 'processed_retention_hours' => 48,
41
+ 'quarantine_retention_days' => 7,
42
+ 'sync_run_retention_days' => 7,
43
+ 'max_event_retention_days' => 0,
44
+ 'max_position_retention_days' => 0,
45
+ 'log_telemetry_activity' => true,
46
+ ]);
47
+
48
+ RetentionPolicy::flush();
49
+ fleetopsRetentionResolver([
50
+ 'event_retention_days' => 99999,
51
+ 'event_compact_after_days' => '',
52
+ 'position_retention_days' => 0,
53
+ 'quarantine_retention_days' => -5,
54
+ 'processed_retention_hours' => null,
55
+ 'log_telemetry_activity' => 'no',
56
+ 'ignored' => 'value',
57
+ ], []);
58
+ $defaults = RetentionPolicy::defaults();
59
+ expect($defaults['event_retention_days'])->toBe(3650)
60
+ ->and($defaults['event_compact_after_days'])->toBe(7)
61
+ ->and($defaults['position_retention_days'])->toBe(0)
62
+ ->and($defaults['quarantine_retention_days'])->toBe(1)
63
+ ->and($defaults['processed_retention_hours'])->toBe(48)
64
+ ->and($defaults['log_telemetry_activity'])->toBeFalse()
65
+ ->and($defaults)->not->toHaveKey('ignored')
66
+ ->and(RetentionPolicy::keys())->toBe(array_keys(RetentionPolicy::FALLBACKS));
67
+ });
68
+
69
+ test('company policies layer over the defaults and are cached until flushed', function () {
70
+ fleetopsRetentionResolver(['position_retention_days' => 30], ['company-1' => ['event_retention_days' => 10, 'log_telemetry_activity' => false]], $calls);
71
+
72
+ $policy = RetentionPolicy::forCompany('company-1');
73
+ expect($policy->get('event_retention_days'))->toBe(10)
74
+ ->and($policy->get('position_retention_days'))->toBe(30)
75
+ ->and($policy->get('processed_retention_hours'))->toBe(48)
76
+ ->and($policy->logsTelemetryActivity())->toBeTrue()
77
+ ->and($policy->toArray())->toHaveKeys(RetentionPolicy::keys())
78
+ ->and(RetentionPolicy::forCompany('company-1'))->toBe($policy)
79
+ ->and(RetentionPolicy::forCompany(null)->toArray())->toBe(RetentionPolicy::defaults())
80
+ ->and(RetentionPolicy::forCompany('company-2')->get('event_retention_days'))->toBe(45)
81
+ ->and(array_column($calls, 0))->toBe(['system', 'company', 'company']);
82
+
83
+ RetentionPolicy::flush('company-1');
84
+ RetentionPolicy::forCompany('company-1');
85
+ RetentionPolicy::forCompany('company-2');
86
+ expect(count($calls))->toBe(4);
87
+
88
+ RetentionPolicy::flush();
89
+ RetentionPolicy::forCompany('company-2');
90
+ expect(array_column($calls, 0))->toBe(['system', 'company', 'company', 'company', 'system', 'company']);
91
+ });
92
+
93
+ test('company history respects admin maximums and cannot override infrastructure or logging', function () {
94
+ fleetopsRetentionResolver([
95
+ 'event_retention_days' => 0,
96
+ 'position_retention_days' => 90,
97
+ 'max_event_retention_days' => 30,
98
+ 'max_position_retention_days' => 45,
99
+ 'log_telemetry_activity' => false,
100
+ ], [
101
+ 'company-1' => [
102
+ 'event_retention_days' => 0,
103
+ 'position_retention_days' => 120,
104
+ 'max_event_retention_days' => 0,
105
+ 'max_position_retention_days' => 0,
106
+ 'event_compact_after_days' => 0,
107
+ 'processed_retention_hours' => 0,
108
+ 'quarantine_retention_days' => 0,
109
+ 'sync_run_retention_days' => 0,
110
+ 'log_telemetry_activity' => true,
111
+ ],
112
+ 'company-2' => ['event_retention_days' => 5, 'position_retention_days' => 15],
113
+ ]);
114
+
115
+ $policy = RetentionPolicy::forCompany('company-1');
116
+ expect($policy->get('event_retention_days'))->toBe(30)
117
+ ->and($policy->get('position_retention_days'))->toBe(45)
118
+ ->and($policy->get('event_compact_after_days'))->toBe(7)
119
+ ->and($policy->get('processed_retention_hours'))->toBe(48)
120
+ ->and($policy->get('quarantine_retention_days'))->toBe(7)
121
+ ->and($policy->get('sync_run_retention_days'))->toBe(7)
122
+ ->and($policy->logsTelemetryActivity())->toBeFalse()
123
+ ->and(RetentionPolicy::forCompany('company-2')->get('event_retention_days'))->toBe(5)
124
+ ->and(RetentionPolicy::forCompany('company-2')->get('position_retention_days'))->toBe(15)
125
+ ->and(RetentionPolicy::forCompany('inherited')->get('event_retention_days'))->toBe(30)
126
+ ->and(RetentionPolicy::forCompany('inherited')->get('position_retention_days'))->toBe(45)
127
+ ->and(RetentionPolicy::forCompany(null)->get('event_retention_days'))->toBe(30);
128
+ });
129
+
130
+ test('unconfigured maximums preserve unlimited history and null preferences inherit defaults', function () {
131
+ fleetopsRetentionResolver([], ['company-1' => ['event_retention_days' => 0, 'position_retention_days' => null]]);
132
+
133
+ expect(RetentionPolicy::forCompany('company-1')->get('event_retention_days'))->toBe(0)
134
+ ->and(RetentionPolicy::forCompany('company-1')->get('position_retention_days'))->toBe(90)
135
+ ->and(RetentionPolicy::companyPreferences(['event_retention_days' => 'invalid', 'position_retention_days' => false]))->toBe([])
136
+ ->and(RetentionPolicy::companyPreferences(['event_retention_days' => null, 'position_retention_days' => '', 'log_telemetry_activity' => true]))->toBe([]);
137
+ });
138
+
139
+ test('constrained preferences keep zero explicit when allowed and replace it when an administrator imposes a maximum', function () {
140
+ $preferences = ['event_retention_days' => 0, 'position_retention_days' => null];
141
+ expect(RetentionPolicy::constrainCompanyPreferences($preferences, RetentionPolicy::FALLBACKS))->toBe(['event_retention_days' => 0]);
142
+
143
+ $defaults = array_replace(RetentionPolicy::FALLBACKS, ['max_event_retention_days' => 90, 'max_position_retention_days' => 360]);
144
+ $bounded = RetentionPolicy::constrainCompanyPreferences($preferences, $defaults);
145
+ expect($bounded)->toBe(['event_retention_days' => 90])
146
+ ->and(RetentionPolicy::constrainCompanyPreferences(['event_retention_days' => 30, 'position_retention_days' => 720], $defaults))->toBe(['event_retention_days' => 30, 'position_retention_days' => 360])
147
+ ->and(RetentionPolicy::constrainCompanyPreferences([], $defaults))->toBe([])
148
+ ->and(RetentionPolicy::constrainCompanyPreferences($bounded, RetentionPolicy::FALLBACKS))->toBe(['event_retention_days' => 90]);
149
+ });
150
+
151
+ test('cutoffs, compaction and deletion rules follow the clamped values', function () {
152
+ Carbon::setTestNow('2026-09-23 12:00:00 UTC');
153
+
154
+ $policy = RetentionPolicy::fromArray(['event_retention_days' => 10, 'event_compact_after_days' => 10, 'processed_retention_hours' => 6, 'position_retention_days' => 0]);
155
+ expect($policy->deletesEvents())->toBeTrue()
156
+ ->and($policy->compactsEvents())->toBeFalse()
157
+ ->and($policy->cutoff('processed_retention_hours')?->toDateTimeString())->toBe('2026-09-23 06:00:00')
158
+ ->and($policy->cutoff('event_retention_days')?->toDateTimeString())->toBe('2026-09-13 12:00:00')
159
+ ->and($policy->cutoff('position_retention_days'))->toBeNull()
160
+ ->and($policy->cutoff('unknown'))->toBeNull()
161
+ ->and($policy->get('unknown'))->toBeNull();
162
+
163
+ expect(RetentionPolicy::fromArray(['event_retention_days' => 10, 'event_compact_after_days' => 3])->compactsEvents())->toBeTrue()
164
+ ->and(RetentionPolicy::fromArray(['event_retention_days' => 0, 'event_compact_after_days' => 5])->compactsEvents())->toBeTrue()
165
+ ->and(RetentionPolicy::fromArray(['event_retention_days' => 0, 'event_compact_after_days' => 5])->deletesEvents())->toBeFalse()
166
+ ->and(RetentionPolicy::fromArray(['event_compact_after_days' => 0])->compactsEvents())->toBeFalse()
167
+ ->and(RetentionPolicy::fromArray([], ['event_retention_days' => 12])->get('event_retention_days'))->toBe(12)
168
+ ->and(RetentionPolicy::normalize(['log_telemetry_activity' => '1'], [])['log_telemetry_activity'])->toBeTrue();
169
+ });
170
+
171
+ test('a failing or malformed settings store falls back to the layer below', function () {
172
+ RetentionPolicy::$settingsResolver = fn () => throw new RuntimeException('settings unavailable');
173
+ expect(RetentionPolicy::forCompany('company-1')->get('event_retention_days'))->toBe(45);
174
+
175
+ RetentionPolicy::flush();
176
+ RetentionPolicy::$settingsResolver = fn () => 'not-an-array';
177
+ expect(RetentionPolicy::defaults()['processed_retention_hours'])->toBe(48);
178
+ });
179
+
180
+ test('company lookups scope the session for the setting store and restore it afterwards', function () {
181
+ // Without a resolver the policy reaches Setting, which has no database here and throws.
182
+ app('session')->put('company', 'previous-company');
183
+ expect(RetentionPolicy::forCompany('company-9')->get('event_retention_days'))->toBe(45)
184
+ ->and(app('session')->get('company'))->toBe('previous-company');
185
+
186
+ app('session')->forget('company');
187
+ RetentionPolicy::flush();
188
+ expect(RetentionPolicy::forCompany('company-9')->get('processed_retention_hours'))->toBe(48)
189
+ ->and(app('session')->has('company'))->toBeFalse();
190
+ });
@@ -0,0 +1,68 @@
1
+ <?php
2
+
3
+ use Fleetbase\FleetOps\Support\Telematics\Retention\RetentionPolicy;
4
+ use Fleetbase\FleetOps\Support\Telematics\Retention\TelemetryActivity;
5
+ use Spatie\Activitylog\ActivityLogger;
6
+
7
+ /**
8
+ * Telemetry writes bypass the activity log unless an administrator enabled it, and
9
+ * still run when no activity logger is available.
10
+ */
11
+ function fleetopsTelemetryActivityLogger(): ActivityLogger
12
+ {
13
+ $logger = new class extends ActivityLogger {
14
+ public int $suppressed = 0;
15
+
16
+ public function __construct()
17
+ {
18
+ }
19
+
20
+ public function withoutLogs(Closure $callback): mixed
21
+ {
22
+ $this->suppressed++;
23
+
24
+ return $callback();
25
+ }
26
+ };
27
+ app()->instance(ActivityLogger::class, $logger);
28
+
29
+ return $logger;
30
+ }
31
+
32
+ beforeEach(function () {
33
+ RetentionPolicy::flush();
34
+ config(['telematics.telemetry' => []]);
35
+ });
36
+
37
+ afterEach(function () {
38
+ RetentionPolicy::flush();
39
+ RetentionPolicy::$settingsResolver = null;
40
+ app()->forgetInstance(ActivityLogger::class);
41
+ });
42
+
43
+ test('telemetry activity follows the system policy and ignores legacy company overrides', function () {
44
+ RetentionPolicy::$settingsResolver = fn (string $scope, string $key, mixed $default, ?string $company) => $company === 'verbose' ? ['log_telemetry_activity' => true] : [];
45
+ $logger = fleetopsTelemetryActivityLogger();
46
+
47
+ expect(TelemetryActivity::run('quiet', fn () => 'saved'))->toBe('saved')
48
+ ->and($logger->suppressed)->toBe(1)
49
+ ->and(TelemetryActivity::run('verbose', fn () => 'saved'))->toBe('saved')
50
+ ->and($logger->suppressed)->toBe(2)
51
+ ->and(TelemetryActivity::run(null, fn () => 'system'))->toBe('system')
52
+ ->and($logger->suppressed)->toBe(3);
53
+
54
+ RetentionPolicy::flush();
55
+ RetentionPolicy::$settingsResolver = fn (string $scope) => ['log_telemetry_activity' => $scope === 'system'];
56
+
57
+ expect(TelemetryActivity::run('quiet', fn () => 'logged'))->toBe('logged')
58
+ ->and(TelemetryActivity::run('verbose', fn () => 'logged'))->toBe('logged')
59
+ ->and(TelemetryActivity::run(null, fn () => 'system'))->toBe('system')
60
+ ->and($logger->suppressed)->toBe(3);
61
+ });
62
+
63
+ test('telemetry writes still run when the activity logger cannot be resolved', function () {
64
+ RetentionPolicy::$settingsResolver = fn () => [];
65
+ app()->forgetInstance(ActivityLogger::class);
66
+
67
+ expect(TelemetryActivity::run('company-1', fn () => 'unlogged'))->toBe('unlogged');
68
+ });
@@ -0,0 +1,109 @@
1
+ <?php
2
+
3
+ if (!function_exists('Fleetbase\Models\session')) {
4
+ eval('namespace Fleetbase\Models; function session($key = null, $default = null) { if ($key === null) { return new class { public function has($k) { return \session($k) !== null; } public function get($k, $d = null) { return \session($k, $d); } public function missing($k) { return \session($k) === null; } }; } return \session($key, $default); }');
5
+ }
6
+
7
+ use Fleetbase\FleetOps\Models\Order;
8
+ use Fleetbase\FleetOps\Support\OrderTracker;
9
+ use Fleetbase\FleetOps\Tracking\TrackingIntelligenceService;
10
+ use Fleetbase\FleetOps\Tracking\TrackingOptions;
11
+ use Illuminate\Database\ConnectionResolver;
12
+ use Illuminate\Database\Eloquent\Model as EloquentModel;
13
+ use Illuminate\Database\SQLiteConnection;
14
+
15
+ /**
16
+ * Covers company tracking settings resolution for TrackingOptions: the
17
+ * order's company is used when there is no company session (queued jobs,
18
+ * console commands), and the session company remains the fallback.
19
+ */
20
+ function fleetopsTrackingOptionsBoot(): SQLiteConnection
21
+ {
22
+ $connection = new SQLiteConnection(new PDO('sqlite::memory:'));
23
+ $resolver = new ConnectionResolver(['default' => $connection, 'mysql' => $connection]);
24
+ $resolver->setDefaultConnection('mysql');
25
+ EloquentModel::setConnectionResolver($resolver);
26
+
27
+ $connection->getSchemaBuilder()->create('settings', function ($blueprint) {
28
+ $blueprint->increments('id');
29
+ $blueprint->string('key')->nullable();
30
+ $blueprint->text('value')->nullable();
31
+ $blueprint->timestamps();
32
+ });
33
+
34
+ $connection->table('settings')->insert([
35
+ ['key' => 'company.company-a.tracking', 'value' => json_encode(['provider' => 'osrm', 'cache_ttl_seconds' => 15])],
36
+ ['key' => 'company.company-b.tracking', 'value' => json_encode(['provider' => 'calculated', 'cache_ttl_seconds' => 45])],
37
+ ]);
38
+
39
+ session(['company' => null]);
40
+
41
+ return $connection;
42
+ }
43
+
44
+ test('tracking options read the given company settings regardless of session', function (?string $sessionCompany) {
45
+ fleetopsTrackingOptionsBoot();
46
+ session(['company' => $sessionCompany]);
47
+
48
+ $options = TrackingOptions::fromArray([], 'company-a');
49
+
50
+ expect($options->provider)->toBe('osrm')
51
+ ->and($options->cacheTtlSeconds)->toBe(15);
52
+
53
+ // Explicit options still win over company settings
54
+ expect(TrackingOptions::fromArray(['provider' => 'google'], 'company-a')->provider)->toBe('google');
55
+ })->with([null, 'company-b']);
56
+
57
+ test('tracking options fall back to the session company when no company is given', function () {
58
+ fleetopsTrackingOptionsBoot();
59
+ session(['company' => 'company-b']);
60
+
61
+ $options = TrackingOptions::fromArray([]);
62
+ session(['company' => null]);
63
+
64
+ expect($options->provider)->toBe('calculated')
65
+ ->and($options->cacheTtlSeconds)->toBe(45);
66
+ });
67
+
68
+ test('order tracker builds options from the order company', function () {
69
+ fleetopsTrackingOptionsBoot();
70
+
71
+ $service = new class extends TrackingIntelligenceService {
72
+ public ?TrackingOptions $received = null;
73
+
74
+ public function __construct()
75
+ {
76
+ }
77
+
78
+ public function track(Order $order, array|TrackingOptions $options = []): array
79
+ {
80
+ $this->received = $options;
81
+
82
+ return [];
83
+ }
84
+ };
85
+ app()->instance(TrackingIntelligenceService::class, $service);
86
+
87
+ $order = new Order();
88
+ $order->setRawAttributes(['uuid' => 'order-tracking-options', 'company_uuid' => 'company-a'], true);
89
+
90
+ (new OrderTracker($order))->toArray();
91
+ app()->forgetInstance(TrackingIntelligenceService::class);
92
+
93
+ expect($service->received)->toBeInstanceOf(TrackingOptions::class)
94
+ ->and($service->received->provider)->toBe('osrm');
95
+ });
96
+
97
+ test('tracking remains available with configured defaults when the settings database is unavailable', function () {
98
+ $connection = fleetopsTrackingOptionsBoot();
99
+ $connection->getSchemaBuilder()->drop('settings');
100
+ $original = config('fleetops.tracking', []);
101
+ try {
102
+ config(['fleetops.tracking' => ['provider' => 'calculated', 'cache_ttl_seconds' => 17]]);
103
+ $options = TrackingOptions::fromArray([], 'uncached-unavailable-company');
104
+ expect($options->provider)->toBe('calculated')->and($options->cacheTtlSeconds)->toBe(17);
105
+ expect(TrackingOptions::fromArray(['provider' => 'osrm'], 'uncached-unavailable-company')->provider)->toBe('osrm');
106
+ } finally {
107
+ config(['fleetops.tracking' => $original]);
108
+ }
109
+ });
@@ -114,6 +114,7 @@ menu:
114
114
  maintenances: Maintenances
115
115
  orchestrator: Orchestrator
116
116
  orchestrator-settings: Orchestrator Settings
117
+ telematics-settings: Telematics
117
118
 
118
119
  trailer:
119
120
  navigation-description: Manage towed assets, towing connections, equipment, and telematics.
@@ -2723,6 +2724,53 @@ route-list:
2723
2724
  more-waypoints: more waypoints
2724
2725
 
2725
2726
  settings:
2727
+ telematics:
2728
+ telematics-settings: Telematics Settings
2729
+ intro: Choose how long your organization keeps device events and position history within the limits set by your system administrator.
2730
+ use-system-default: Use system default
2731
+ history-policy-maximum: "Your administrator allows up to {days} days of history. Unlimited retention is not available."
2732
+ history-policy-unlimited: Your administrator allows unlimited history. Set the retention period to 0 to keep history forever.
2733
+ device-events: Device Events Retention
2734
+ event-retention-days: Keep device events for (days)
2735
+ event-retention-days-help: Events older than this are permanently deleted. Applies to telemetry updates and provider alerts alike.
2736
+ event-compact-after-days: Strip raw payloads after (days)
2737
+ event-compact-after-days-help: Events older than this keep their type, severity, location and normalized data, but the raw provider payload is removed. Raw payloads are the bulk of each event's size.
2738
+ compaction-warning: Raw payloads are only stripped from events that are kept. Set this lower than the event retention or set it to 0 to disable compaction.
2739
+ positions: Position History Retention
2740
+ position-retention-days: Keep positions for (days)
2741
+ position-retention-days-help: Vehicle and trailer position history older than this is permanently deleted. Route replay and history views only reach back this far.
2742
+ inbox: Delivery Inbox & Sync Run Retention
2743
+ processed-retention-hours: Keep processed deliveries for (hours)
2744
+ processed-retention-hours-help: Raw provider batches are stored encrypted until processed and kept briefly for replay and diagnostics. Each batch can be close to a megabyte.
2745
+ quarantine-retention-days: Keep quarantined deliveries for (days)
2746
+ quarantine-retention-days-help: Batches that could not be processed stay available for inspection and replay for this long.
2747
+ sync-run-retention-days: Keep sync run diagnostics for (days)
2748
+ sync-run-retention-days-help: One sync run row is recorded per connection per poll. In-flight runs are never removed.
2749
+ activity-logging: Telemetry Activity Logging
2750
+ log-telemetry-activity: Log telemetry activity
2751
+ log-telemetry-activity-help: Write telemetry-driven device, event and vehicle saves to the activity log. Off by default; every poll otherwise adds several activity rows per device.
2752
+ storage-usage: System Telematics Storage Usage
2753
+ table: Table
2754
+ rows: Rows
2755
+ estimated-rows: "{count} (estimated)"
2756
+ estimated-size: Estimated size
2757
+ oldest: Oldest row
2758
+ not-available: n/a
2759
+ raw-payload-rows: "{count} rows still carry raw payloads, {compactable} eligible for compaction now"
2760
+ refresh: Refresh
2761
+ run-cleanup: Run system cleanup
2762
+ run-cleanup-confirm: This queues telematics cleanup across all organizations, using the saved system policy and each organization's effective history preferences. Expired records are permanently deleted and cannot be recovered. Continue?
2763
+ cleanup-queued: Telematics cleanup has been queued across all organizations. Usage refreshes shortly; cleanup may still be running.
2764
+ cleanup-unavailable: System-wide telematics cleanup could not be confirmed. Please try again.
2765
+ usage-unavailable: Storage usage could not be loaded. Select Refresh to try again.
2766
+ space-reclaim-note: Usage includes all telematics data across the system. Row counts marked as estimated use database statistics. Sizes include table data and indexes. Deleting records frees space for reuse within the database; physical disk usage may not decrease immediately.
2767
+ settings-saved: Telematics settings saved.
2768
+ settings-unavailable: Telematics settings could not be loaded. Please try again.
2769
+ tables:
2770
+ device_events: Device events
2771
+ positions: Positions
2772
+ telematic_deliveries: Delivery inbox
2773
+ telematic_sync_runs: Sync runs
2726
2774
  avatar-management: Avatar Management
2727
2775
  custom-fields: Custom Fields
2728
2776
  notifications: