Add loading screen for session confirmation and enhance vehicle management labels
This commit is contained in:
@@ -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);
|
||||
|
||||
+16
-7
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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'] = [
|
||||
|
||||
Reference in New Issue
Block a user