Add loading screen for session confirmation and enhance vehicle management labels

This commit is contained in:
Jeppe Bundgaard
2026-07-13 22:11:54 +02:00
parent 233133365d
commit 5bac316e4b
9 changed files with 367 additions and 11 deletions
+37
View File
@@ -15409,6 +15409,35 @@ components:
type: string
enum: [disabled, not_configured, configured, ok, degraded, down]
SuperuserModuleUsage:
type: object
properties:
provider:
type: string
enum: [licenseplaterecognizer]
calls_used:
type: integer
minimum: 0
quota_calls:
type: integer
minimum: 1
calls_remaining:
type: integer
minimum: 0
usage_percent:
type: number
format: float
minimum: 0
version:
type: string
nullable: true
required:
- provider
- calls_used
- quota_calls
- calls_remaining
- usage_percent
SuperuserRuntimeMetric:
type: object
properties:
@@ -15518,6 +15547,14 @@ components:
status_reason:
type: string
nullable: true
status_reason_key:
type: string
nullable: true
status_reason_params:
type: object
additionalProperties: true
usage:
$ref: '#/components/schemas/SuperuserModuleUsage'
checked_at:
type: string
format: date-time
File diff suppressed because one or more lines are too long
@@ -244,6 +244,11 @@ class licenseplaterecognizer implements licenseplaterecognizer_i
return (string)$configured;
}
public static function configuredApiBaseUrl(): string
{
return self::normalizeApiUrl(self::configuredApiUrl());
}
private static function normalizeApiUrl(string $api_url): string
{
$api_url = trim($api_url);
@@ -575,6 +575,9 @@ class superuser_system_status_service
? $probeResult['status_reason_params']
: [];
$result['checked_at'] = (string)($probeResult['checked_at'] ?? $result['checked_at']);
if (isset($probeResult['usage']) && is_array($probeResult['usage'])) {
$result['usage'] = $probeResult['usage'];
}
$results[] = $result;
}
@@ -1163,12 +1166,16 @@ class superuser_system_status_service
$apiKey = trim((string)($config['api_key']['parsed'] ?? ''));
return $this->performHttpProbe(
$this->buildUrlWithQuery('https://vs4sws0kg4sog4ssw8kwowk4.coolify.truckwash.dk', '/info/'),
$this->buildUrlWithQuery(licenseplaterecognizer::configuredApiBaseUrl(), '/info/'),
[
'Authorization: Token ' . $apiKey,
'Accept: application/json',
],
'License Plate Recognizer'
'License Plate Recognizer',
null,
'GET',
null,
fn(array $httpResponse, string $label): array => $this->evaluateLicensePlateRecognizerProbeResponse($httpResponse, $label)
);
}
@@ -1486,6 +1493,99 @@ class superuser_system_status_service
];
}
protected function evaluateLicensePlateRecognizerProbeResponse(array $httpResponse, string $label): array
{
$classified = $this->classifyHttpProbeResult($httpResponse, $label);
if (($classified['status'] ?? 'down') !== 'ok') {
return $classified;
}
$decoded = json_decode((string)($httpResponse['body'] ?? ''), true);
if (!is_array($decoded)) {
return [
'status' => 'degraded',
'status_reason' => $label . ' returned an unreadable usage payload.',
'status_reason_key' => 'licenseplaterecognizer_usage_unreadable',
'status_reason_params' => ['label' => $label],
'checked_at' => $httpResponse['checked_at'] ?? date('c'),
'latency_ms' => $httpResponse['latency_ms'] ?? null,
'http_status' => $httpResponse['http_status'] ?? null,
];
}
$usage = is_array($decoded['usage'] ?? null) ? $decoded['usage'] : [];
$callsUsedRaw = $usage['calls'] ?? null;
$quotaCallsRaw = $decoded['total_calls'] ?? null;
if (!is_numeric($callsUsedRaw) || !is_numeric($quotaCallsRaw) || (int)$quotaCallsRaw <= 0) {
return [
'status' => 'degraded',
'status_reason' => $label . ' did not return usable quota values.',
'status_reason_key' => 'licenseplaterecognizer_usage_missing',
'status_reason_params' => ['label' => $label],
'checked_at' => $httpResponse['checked_at'] ?? date('c'),
'latency_ms' => $httpResponse['latency_ms'] ?? null,
'http_status' => $httpResponse['http_status'] ?? null,
];
}
$callsUsed = max(0, (int)$callsUsedRaw);
$quotaCalls = max(1, (int)$quotaCallsRaw);
$callsRemaining = max(0, $quotaCalls - $callsUsed);
$usagePercent = round(($callsUsed / $quotaCalls) * 100, 2);
$usagePayload = [
'provider' => 'licenseplaterecognizer',
'calls_used' => $callsUsed,
'quota_calls' => $quotaCalls,
'calls_remaining' => $callsRemaining,
'usage_percent' => $usagePercent,
'version' => trim((string)($decoded['version'] ?? '')),
];
$reasonParams = [
'label' => $label,
'used' => (string)$callsUsed,
'quota' => (string)$quotaCalls,
'remaining' => (string)$callsRemaining,
'percent' => number_format($usagePercent, 1, '.', ''),
];
if ($callsRemaining <= 0 || $usagePercent >= 100.0) {
return [
'status' => 'down',
'status_reason' => $label . ' quota is exhausted.',
'status_reason_key' => 'licenseplaterecognizer_quota_exhausted',
'status_reason_params' => $reasonParams,
'checked_at' => $httpResponse['checked_at'] ?? date('c'),
'latency_ms' => $httpResponse['latency_ms'] ?? null,
'http_status' => $httpResponse['http_status'] ?? null,
'usage' => $usagePayload,
];
}
if ($usagePercent >= 90.0) {
return [
'status' => 'degraded',
'status_reason' => $label . ' quota usage is near the limit.',
'status_reason_key' => 'licenseplaterecognizer_quota_near_limit',
'status_reason_params' => $reasonParams,
'checked_at' => $httpResponse['checked_at'] ?? date('c'),
'latency_ms' => $httpResponse['latency_ms'] ?? null,
'http_status' => $httpResponse['http_status'] ?? null,
'usage' => $usagePayload,
];
}
return [
'status' => 'ok',
'status_reason' => $label . ' usage and quota are available.',
'status_reason_key' => 'licenseplaterecognizer_usage_available',
'status_reason_params' => $reasonParams,
'checked_at' => $httpResponse['checked_at'] ?? date('c'),
'latency_ms' => $httpResponse['latency_ms'] ?? null,
'http_status' => $httpResponse['http_status'] ?? null,
'usage' => $usagePayload,
];
}
protected function evaluateRecaptchaProbeResponse(array $httpResponse, string $label): array
{
$classified = $this->classifyHttpProbeResult($httpResponse, $label);
@@ -22,12 +22,21 @@ class licenseplaterecognizer_info
public array $webhooks;
public int $total_calls;
public function __construct($data)
public function __construct(mixed $data)
{
$this->version = $data->version;
$this->usage = $data->usage ?? (object) ['calls' => 0];
$this->license_key = $data->license_key;
$this->webhooks = $data->webhooks;
$this->total_calls = $data->total_calls;
$values = is_array($data) ? $data : (is_object($data) ? get_object_vars($data) : []);
$usage = $values['usage'] ?? (object)['calls' => 0];
if (is_array($usage)) {
$usage = (object)$usage;
} elseif (!is_object($usage)) {
$usage = (object)['calls' => 0];
}
$usage->calls = is_numeric($usage->calls ?? null) ? (int)$usage->calls : 0;
$this->version = (string)($values['version'] ?? '');
$this->usage = $usage;
$this->license_key = (string)($values['license_key'] ?? '');
$this->webhooks = is_array($values['webhooks'] ?? null) ? $values['webhooks'] : [];
$this->total_calls = is_numeric($values['total_calls'] ?? null) ? (int)$values['total_calls'] : 0;
}
}
}
+37
View File
@@ -16507,6 +16507,35 @@ components:
type: string
enum: [disabled, not_configured, configured, ok, degraded, down]
SuperuserModuleUsage:
type: object
properties:
provider:
type: string
enum: [licenseplaterecognizer]
calls_used:
type: integer
minimum: 0
quota_calls:
type: integer
minimum: 1
calls_remaining:
type: integer
minimum: 0
usage_percent:
type: number
format: float
minimum: 0
version:
type: string
nullable: true
required:
- provider
- calls_used
- quota_calls
- calls_remaining
- usage_percent
SuperuserRuntimeMetric:
type: object
properties:
@@ -16616,6 +16645,14 @@ components:
status_reason:
type: string
nullable: true
status_reason_key:
type: string
nullable: true
status_reason_params:
type: object
additionalProperties: true
usage:
$ref: '#/components/schemas/SuperuserModuleUsage'
checked_at:
type: string
format: date-time
@@ -96,6 +96,32 @@ afterEach(function (): void {
reset_license_plate_recognizer_runtime_config_cache();
});
it('normalizes license plate recognizer info responses decoded as arrays or objects', function (): void {
$arrayInfo = new \licenseplaterecognizer\helpers\licenseplaterecognizer_info([
'version' => '1.54.0',
'usage' => ['calls' => '98'],
'license_key' => 'license-key',
'webhooks' => [['id' => 'webhook-1']],
'total_calls' => '2500',
]);
$objectInfo = new \licenseplaterecognizer\helpers\licenseplaterecognizer_info((object)[
'version' => '1.55.0',
'usage' => (object)['calls' => 7],
'license_key' => 'license-key-2',
'webhooks' => [],
'total_calls' => 100,
]);
expect($arrayInfo->version)->toBe('1.54.0');
expect($arrayInfo->usage->calls)->toBe(98);
expect($arrayInfo->license_key)->toBe('license-key');
expect($arrayInfo->webhooks)->toHaveCount(1);
expect($arrayInfo->total_calls)->toBe(2500);
expect($objectInfo->version)->toBe('1.55.0');
expect($objectInfo->usage->calls)->toBe(7);
expect($objectInfo->total_calls)->toBe(100);
});
it('sends data URI images to Plate Recognizer as multipart bytes with fast mode enabled', function (): void {
$reflection = new ReflectionClass(licenseplaterecognizer::class);
$recognizer = $reflection->newInstanceWithoutConstructor();
@@ -39,5 +39,9 @@ it('defines the reusable system status schemas and enums', function () use ($ope
expect($content)->toContain('enum: [ok, degraded, down]');
expect($content)->toContain('SuperuserModuleStatusEnum:');
expect($content)->toContain('enum: [disabled, not_configured, configured, ok, degraded, down]');
expect($content)->toContain('SuperuserModuleUsage:');
expect($content)->toContain('status_reason_key:');
expect($content)->toContain('status_reason_params:');
expect($content)->toContain('usage_percent:');
expect($content)->toContain('SuperuserSessionStatus:');
});
@@ -57,6 +57,11 @@ class SuperuserSystemStatusServiceProbeDouble extends superuser_system_status_se
return $this->evaluateRecaptchaProbeResponse($httpResponse, $label);
}
public function evaluateLicensePlateRecognizerProbeResponsePublic(array $httpResponse, string $label): array
{
return $this->evaluateLicensePlateRecognizerProbeResponse($httpResponse, $label);
}
public function normalizeWarningEntriesPublic(array $warnings): array
{
return $this->normalizeWarningEntries($warnings);
@@ -478,7 +483,7 @@ it('builds the expected http probe requests for newly supported modules', functi
null,
'GET',
null,
false,
true,
],
'bird' => [
'probeBirdModulePublic',
@@ -497,6 +502,139 @@ it('builds the expected http probe requests for newly supported modules', functi
],
]);
it('evaluates license plate recognizer usage and quota health from the info payload', function (
string $body,
string $expectedStatus,
string $expectedReasonKey,
?array $expectedUsage
): void {
$service = new SuperuserSystemStatusServiceProbeDouble();
$result = $service->evaluateLicensePlateRecognizerProbeResponsePublic([
'checked_at' => '2026-04-08T17:00:00+00:00',
'latency_ms' => 7.5,
'http_status' => 200,
'body' => $body,
'error' => '',
], 'License Plate Recognizer');
expect($result['status'])->toBe($expectedStatus);
expect($result['status_reason_key'])->toBe($expectedReasonKey);
expect(json_encode($result, JSON_THROW_ON_ERROR))->not->toContain('SECRET-LICENSE-KEY');
if ($expectedUsage === null) {
expect($result)->not->toHaveKey('usage');
return;
}
expect($result['usage'])->toMatchArray($expectedUsage);
})->with([
'normal usage' => [
json_encode([
'version' => '1.54.0',
'usage' => ['calls' => 98],
'license_key' => 'SECRET-LICENSE-KEY',
'webhooks' => [],
'total_calls' => 2500,
]),
'ok',
'licenseplaterecognizer_usage_available',
[
'provider' => 'licenseplaterecognizer',
'calls_used' => 98,
'quota_calls' => 2500,
'calls_remaining' => 2402,
'usage_percent' => 3.92,
'version' => '1.54.0',
],
],
'near quota' => [
json_encode([
'version' => '1.54.0',
'usage' => ['calls' => 2250],
'total_calls' => 2500,
]),
'degraded',
'licenseplaterecognizer_quota_near_limit',
[
'calls_used' => 2250,
'quota_calls' => 2500,
'calls_remaining' => 250,
'usage_percent' => 90.0,
],
],
'exhausted quota' => [
json_encode([
'version' => '1.54.0',
'usage' => ['calls' => 2500],
'total_calls' => 2500,
]),
'down',
'licenseplaterecognizer_quota_exhausted',
[
'calls_used' => 2500,
'quota_calls' => 2500,
'calls_remaining' => 0,
'usage_percent' => 100.0,
],
],
'unreadable payload' => [
'not-json',
'degraded',
'licenseplaterecognizer_usage_unreadable',
null,
],
'missing quota' => [
json_encode([
'version' => '1.54.0',
'usage' => ['calls' => 10],
]),
'degraded',
'licenseplaterecognizer_usage_missing',
null,
],
]);
it('carries license plate recognizer usage from probe results into module status rows', function (): void {
$service = new SuperuserSystemStatusServiceProbeDouble();
$service->nextHttpProbeResult = [
'status' => 'ok',
'status_reason' => 'License Plate Recognizer usage and quota are available.',
'status_reason_key' => 'licenseplaterecognizer_usage_available',
'status_reason_params' => ['used' => '98', 'quota' => '2500', 'remaining' => '2402', 'percent' => '3.9'],
'checked_at' => '2026-04-08T17:00:00+00:00',
'usage' => [
'provider' => 'licenseplaterecognizer',
'calls_used' => 98,
'quota_calls' => 2500,
'calls_remaining' => 2402,
'usage_percent' => 3.92,
'version' => '1.54.0',
],
];
$service->moduleConfigRowsOverride = [
'licenseplaterecognizer' => [
'enabled' => system_status_module_value(true, 'bool'),
'api_key' => system_status_module_value('lpr-key'),
],
];
$warnings = [];
$modules = $service->collectModulesPublic(false, $warnings);
$moduleMap = [];
foreach ($modules as $module) {
$moduleMap[$module['key']] = $module;
}
expect($moduleMap['licenseplaterecognizer']['usage'])->toMatchArray([
'provider' => 'licenseplaterecognizer',
'calls_used' => 98,
'quota_calls' => 2500,
'calls_remaining' => 2402,
'usage_percent' => 3.92,
]);
});
it('uses runtime economic credentials for the economic probe', function (): void {
$service = new SuperuserSystemStatusServiceProbeDouble();
$GLOBALS['ECONOMIC_API'] = [