Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
85eb0039d1 |
@@ -9,15 +9,19 @@ class department_gate_config
|
|||||||
public string $type;
|
public string $type;
|
||||||
public ?string $phone_number = null;
|
public ?string $phone_number = null;
|
||||||
public ?int $call_duration_threshold = null;
|
public ?int $call_duration_threshold = null;
|
||||||
|
public ?string $relay_id = null;
|
||||||
|
public ?int $pulse_seconds = null;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param array $config
|
* @param array $config
|
||||||
*/
|
*/
|
||||||
public function __construct(array $config = [])
|
public function __construct(array $config = [])
|
||||||
{
|
{
|
||||||
$this->type = (string)($config['type'] ?? '');
|
$this->type = strtoupper(trim((string)($config['type'] ?? '')));
|
||||||
$this->phone_number = isset($config['phone_number']) ? (string)$config['phone_number'] : null;
|
$this->phone_number = isset($config['phone_number']) ? (string)$config['phone_number'] : null;
|
||||||
$this->call_duration_threshold = isset($config['call_duration_threshold']) ? (int)$config['call_duration_threshold'] : null;
|
$this->call_duration_threshold = isset($config['call_duration_threshold']) ? (int)$config['call_duration_threshold'] : null;
|
||||||
|
$this->relay_id = isset($config['relay_id']) ? trim((string)$config['relay_id']) : null;
|
||||||
|
$this->pulse_seconds = isset($config['pulse_seconds']) ? (int)$config['pulse_seconds'] : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -37,6 +41,14 @@ class department_gate_config
|
|||||||
$array['call_duration_threshold'] = $this->call_duration_threshold;
|
$array['call_duration_threshold'] = $this->call_duration_threshold;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ($this->relay_id !== null) {
|
||||||
|
$array['relay_id'] = $this->relay_id;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($this->pulse_seconds !== null) {
|
||||||
|
$array['pulse_seconds'] = $this->pulse_seconds;
|
||||||
|
}
|
||||||
|
|
||||||
return $array;
|
return $array;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -58,6 +70,19 @@ class department_gate_config
|
|||||||
if ($this->call_duration_threshold === null) {
|
if ($this->call_duration_threshold === null) {
|
||||||
throw new Exception('Call duration threshold is required for PHONE_CALL gate type');
|
throw new Exception('Call duration threshold is required for PHONE_CALL gate type');
|
||||||
}
|
}
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ($this->type === 'RELAY') {
|
||||||
|
if ($this->relay_id === null || $this->relay_id === '') {
|
||||||
|
throw new Exception('relay_id is required for RELAY gate type');
|
||||||
|
}
|
||||||
|
if ($this->pulse_seconds !== null && $this->pulse_seconds < 0) {
|
||||||
|
throw new Exception('pulse_seconds must be a positive integer for RELAY gate type');
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Exception('Unsupported gate config type: ' . $this->type);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+865
@@ -0,0 +1,865 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace classes;
|
||||||
|
|
||||||
|
use Exception;
|
||||||
|
use objects\department_gates_o;
|
||||||
|
use objects\department_lanes_o;
|
||||||
|
use objects\department_relays_o;
|
||||||
|
use objects\departments_o;
|
||||||
|
use objects\plate_scanners_o;
|
||||||
|
use objects\plate_scans_o;
|
||||||
|
|
||||||
|
class edge_gateway_department_workspace_service
|
||||||
|
{
|
||||||
|
public function __construct(private readonly ?edge_gateway_manager $manager = null)
|
||||||
|
{
|
||||||
|
edge_gateway_schema_bootstrap::ensureTables();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array<int,array<string,mixed>>
|
||||||
|
* @throws Exception
|
||||||
|
*/
|
||||||
|
public function listDepartmentSummaries(): array
|
||||||
|
{
|
||||||
|
$departments = (new departments_o())->list(true);
|
||||||
|
usort($departments, static function (array $left, array $right): int {
|
||||||
|
$leftPriority = (int)($left['order_priority'] ?? PHP_INT_MAX);
|
||||||
|
$rightPriority = (int)($right['order_priority'] ?? PHP_INT_MAX);
|
||||||
|
if ($leftPriority !== $rightPriority) {
|
||||||
|
return $leftPriority <=> $rightPriority;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (int)($left['id'] ?? 0) <=> (int)($right['id'] ?? 0);
|
||||||
|
});
|
||||||
|
|
||||||
|
$summaries = [];
|
||||||
|
foreach ($departments as $departmentRow) {
|
||||||
|
$departmentId = (int)($departmentRow['id'] ?? 0);
|
||||||
|
if ($departmentId <= 0) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$workspace = $this->buildDepartmentWorkspace($departmentId, false, $departmentRow);
|
||||||
|
$summaries[] = $workspace['summary'];
|
||||||
|
}
|
||||||
|
|
||||||
|
return $summaries;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array<string,mixed>
|
||||||
|
* @throws Exception
|
||||||
|
*/
|
||||||
|
public function getDepartmentWorkspace(int $departmentId): array
|
||||||
|
{
|
||||||
|
return $this->buildDepartmentWorkspace($departmentId, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array<string,mixed>|null $departmentRow
|
||||||
|
* @return array<string,mixed>
|
||||||
|
* @throws Exception
|
||||||
|
*/
|
||||||
|
private function buildDepartmentWorkspace(int $departmentId, bool $includeGateways, ?array $departmentRow = null): array
|
||||||
|
{
|
||||||
|
$department = (new departments_o())->select($departmentId);
|
||||||
|
if (!$department->exists()) {
|
||||||
|
throw new Exception('Department not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
$departmentPayload = [
|
||||||
|
'id' => $departmentId,
|
||||||
|
'name' => (string)$department->name->value(),
|
||||||
|
'description' => (string)$department->description->value(),
|
||||||
|
'order_priority' => (int)$department->order_priority->value(),
|
||||||
|
];
|
||||||
|
|
||||||
|
$transportMode = $this->manager()->getDepartmentTransportMode($departmentId);
|
||||||
|
$gateways = $this->manager()->listGateways($departmentId, true);
|
||||||
|
$bindingsByRelayId = $this->indexBindingsByRelayId($gateways);
|
||||||
|
$relayCatalog = $this->indexRelayCatalog($departmentId);
|
||||||
|
$consumersByRelayId = [];
|
||||||
|
|
||||||
|
$lanes = $this->buildLanePayloads($departmentId, $bindingsByRelayId, $relayCatalog, $consumersByRelayId);
|
||||||
|
$gates = $this->buildGatePayloads($departmentId, $bindingsByRelayId, $relayCatalog, $consumersByRelayId);
|
||||||
|
$gateways = $this->applyBindingConsumerContexts($gateways, $consumersByRelayId);
|
||||||
|
$selfServe = $this->buildSelfServePayload($department, $lanes);
|
||||||
|
$scanners = $this->buildScannerPayloads($departmentId, $lanes);
|
||||||
|
$issues = $this->buildIssues($transportMode, $gateways, $lanes, $gates, $scanners, $selfServe);
|
||||||
|
$actions = $this->buildActions($departmentId, $gateways, $lanes, $gates, $scanners, $selfServe);
|
||||||
|
$summary = $this->buildSummary(
|
||||||
|
$departmentPayload,
|
||||||
|
$departmentRow,
|
||||||
|
$transportMode,
|
||||||
|
$gateways,
|
||||||
|
$lanes,
|
||||||
|
$gates,
|
||||||
|
$scanners,
|
||||||
|
$selfServe,
|
||||||
|
$issues
|
||||||
|
);
|
||||||
|
|
||||||
|
return [
|
||||||
|
'department' => $departmentPayload,
|
||||||
|
'summary' => $summary,
|
||||||
|
'gateways' => $includeGateways ? $gateways : [],
|
||||||
|
'lanes' => $lanes,
|
||||||
|
'self_serve' => $selfServe,
|
||||||
|
'gates' => $gates,
|
||||||
|
'scanners' => $scanners,
|
||||||
|
'issues' => $issues,
|
||||||
|
'actions' => $actions,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array<int,array<string,mixed>> $gateways
|
||||||
|
* @return array<string,array<int,array<string,mixed>>>
|
||||||
|
*/
|
||||||
|
private function indexBindingsByRelayId(array $gateways): array
|
||||||
|
{
|
||||||
|
$bindingsByRelayId = [];
|
||||||
|
|
||||||
|
foreach ($gateways as $gateway) {
|
||||||
|
$bindings = isset($gateway['bindings']) && is_array($gateway['bindings'])
|
||||||
|
? (array)$gateway['bindings']
|
||||||
|
: [];
|
||||||
|
|
||||||
|
foreach ($bindings as $binding) {
|
||||||
|
if (!is_array($binding)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$relayId = trim((string)($binding['relay_id'] ?? ''));
|
||||||
|
if ($relayId === '') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$binding['gateway_label'] = (string)($gateway['label'] ?? ('Gateway ' . ($gateway['id'] ?? '')));
|
||||||
|
$binding['gateway_status'] = (string)($gateway['status'] ?? edge_gateway_manager::STATUS_OFFLINE);
|
||||||
|
$binding['is_primary_gateway'] = (bool)($gateway['is_primary'] ?? false);
|
||||||
|
$bindingsByRelayId[$relayId][] = $binding;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($bindingsByRelayId as $relayId => $bindings) {
|
||||||
|
usort($bindings, static function (array $left, array $right): int {
|
||||||
|
return ((int)($right['is_primary_gateway'] ?? 0) <=> (int)($left['is_primary_gateway'] ?? 0))
|
||||||
|
?: ((int)($left['gateway_id'] ?? 0) <=> (int)($right['gateway_id'] ?? 0));
|
||||||
|
});
|
||||||
|
$bindingsByRelayId[$relayId] = $bindings;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $bindingsByRelayId;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array<string,array<string,mixed>>
|
||||||
|
* @throws Exception
|
||||||
|
*/
|
||||||
|
private function indexRelayCatalog(int $departmentId): array
|
||||||
|
{
|
||||||
|
$catalog = [];
|
||||||
|
foreach ((new department_relays_o())->getDepartmentRelays($departmentId) as $relay) {
|
||||||
|
if (!$relay->exists()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$relayId = trim((string)$relay->relay_id->value());
|
||||||
|
if ($relayId === '') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$catalog[$relayId] = $relay->asArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
return $catalog;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array<string,array<int,array<string,mixed>>> $bindingsByRelayId
|
||||||
|
* @param array<string,array<string,mixed>> $relayCatalog
|
||||||
|
* @param array<string,array<int,array<string,mixed>>> $consumersByRelayId
|
||||||
|
* @return array<int,array<string,mixed>>
|
||||||
|
* @throws Exception
|
||||||
|
*/
|
||||||
|
private function buildLanePayloads(
|
||||||
|
int $departmentId,
|
||||||
|
array $bindingsByRelayId,
|
||||||
|
array $relayCatalog,
|
||||||
|
array &$consumersByRelayId
|
||||||
|
): array {
|
||||||
|
$lanes = [];
|
||||||
|
$slotMap = [
|
||||||
|
'relay_in_id' => 'ENTRY',
|
||||||
|
'relay_out_id' => 'EXIT',
|
||||||
|
'relay_machine_id' => 'MACHINE',
|
||||||
|
'relay_machine_program_picker_id' => 'PROGRAM_PICKER',
|
||||||
|
'relay_machine_cleaner_id' => 'CLEANER',
|
||||||
|
];
|
||||||
|
|
||||||
|
foreach ((new department_lanes_o())->getDepartmentLanes($departmentId) as $lane) {
|
||||||
|
if (!$lane->exists()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$relaySlots = [];
|
||||||
|
$boundRelayCount = 0;
|
||||||
|
foreach ($slotMap as $property => $slotName) {
|
||||||
|
$relayId = trim((string)$lane->{$property}->value());
|
||||||
|
if ($relayId === '') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$consumersByRelayId[$relayId][] = [
|
||||||
|
'type' => 'lane',
|
||||||
|
'id' => (int)$lane->id,
|
||||||
|
'slot' => $slotName,
|
||||||
|
'label' => (string)$lane->name->value(),
|
||||||
|
];
|
||||||
|
|
||||||
|
$coverage = $this->buildRelayCoverage($relayId, $bindingsByRelayId);
|
||||||
|
if ((bool)($coverage['covered'] ?? false)) {
|
||||||
|
$boundRelayCount += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
$relaySlots[] = [
|
||||||
|
'slot' => $slotName,
|
||||||
|
'relay_id' => $relayId,
|
||||||
|
'catalog' => $relayCatalog[$relayId] ?? null,
|
||||||
|
'coverage' => $coverage,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
$requiredRelayCount = count($relaySlots);
|
||||||
|
$laneStatus = 'UNKNOWN';
|
||||||
|
try {
|
||||||
|
$laneStatus = (string)$lane->getLaneStatus()->name;
|
||||||
|
} catch (\Throwable) {
|
||||||
|
}
|
||||||
|
|
||||||
|
$lanes[] = [
|
||||||
|
'id' => (int)$lane->id,
|
||||||
|
'department' => (int)$lane->department->value(),
|
||||||
|
'name' => (string)$lane->name->value(),
|
||||||
|
'relay_in_id' => $lane->relay_in_id->value() === null ? null : (string)$lane->relay_in_id->value(),
|
||||||
|
'relay_out_id' => $lane->relay_out_id->value() === null ? null : (string)$lane->relay_out_id->value(),
|
||||||
|
'relay_machine_id' => $lane->relay_machine_id->value() === null ? null : (string)$lane->relay_machine_id->value(),
|
||||||
|
'relay_machine_program_picker_id' => $lane->relay_machine_program_picker_id->value() === null ? null : (string)$lane->relay_machine_program_picker_id->value(),
|
||||||
|
'relay_machine_cleaner_id' => $lane->relay_machine_cleaner_id->value() === null ? null : (string)$lane->relay_machine_cleaner_id->value(),
|
||||||
|
'dynamic_image_id' => $lane->dynamic_image_id->value() === null ? null : (int)$lane->dynamic_image_id->value(),
|
||||||
|
'machine_type_id' => $lane->machine_type_id->value() === null ? null : (int)$lane->machine_type_id->value(),
|
||||||
|
'status' => $laneStatus,
|
||||||
|
'self_serve_products' => $lane->getSelfServeLaneProducts(),
|
||||||
|
'relay_slots' => $relaySlots,
|
||||||
|
'binding_coverage' => [
|
||||||
|
'required' => $requiredRelayCount,
|
||||||
|
'bound' => $boundRelayCount,
|
||||||
|
'missing' => max(0, $requiredRelayCount - $boundRelayCount),
|
||||||
|
'state' => $requiredRelayCount === 0
|
||||||
|
? 'NOT_REQUIRED'
|
||||||
|
: ($boundRelayCount === $requiredRelayCount ? 'READY' : 'MISSING'),
|
||||||
|
],
|
||||||
|
'links' => [
|
||||||
|
'legacy' => '/superuser/department/lanes/' . (int)$lane->id,
|
||||||
|
'self_serve_studio' => '/admin/' . $departmentId . '/modules/self-serve/studio',
|
||||||
|
],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
return $lanes;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array<string,array<int,array<string,mixed>>> $bindingsByRelayId
|
||||||
|
* @param array<string,array<string,mixed>> $relayCatalog
|
||||||
|
* @param array<string,array<int,array<string,mixed>>> $consumersByRelayId
|
||||||
|
* @return array<int,array<string,mixed>>
|
||||||
|
* @throws Exception
|
||||||
|
*/
|
||||||
|
private function buildGatePayloads(
|
||||||
|
int $departmentId,
|
||||||
|
array $bindingsByRelayId,
|
||||||
|
array $relayCatalog,
|
||||||
|
array &$consumersByRelayId
|
||||||
|
): array {
|
||||||
|
$gates = [];
|
||||||
|
|
||||||
|
foreach ((new department_gates_o())->getDepartmentGates($departmentId) as $gate) {
|
||||||
|
if (!$gate->exists()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$config = (array)$gate->config->value();
|
||||||
|
$gateType = strtoupper(trim((string)($config['type'] ?? 'UNKNOWN')));
|
||||||
|
$relayId = trim((string)($config['relay_id'] ?? ''));
|
||||||
|
|
||||||
|
if ($gateType === 'RELAY' && $relayId !== '') {
|
||||||
|
$consumersByRelayId[$relayId][] = [
|
||||||
|
'type' => 'gate',
|
||||||
|
'id' => (int)$gate->id,
|
||||||
|
'slot' => ((bool)$gate->is_entrance->value() ? 'ENTRANCE' : ((bool)$gate->is_exit->value() ? 'EXIT' : 'GENERAL')),
|
||||||
|
'label' => (string)$gate->name->value(),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
$coverage = $gateType === 'RELAY' && $relayId !== ''
|
||||||
|
? $this->buildRelayCoverage($relayId, $bindingsByRelayId)
|
||||||
|
: null;
|
||||||
|
|
||||||
|
$gates[] = [
|
||||||
|
'id' => (int)$gate->id,
|
||||||
|
'department' => (int)$gate->department->value(),
|
||||||
|
'name' => (string)$gate->name->value(),
|
||||||
|
'is_entrance' => (bool)$gate->is_entrance->value(),
|
||||||
|
'is_exit' => (bool)$gate->is_exit->value(),
|
||||||
|
'config' => $config,
|
||||||
|
'transport_type' => $gateType,
|
||||||
|
'config_complete' => $this->isGateConfigComplete($config),
|
||||||
|
'relay' => $relayId !== '' ? ($relayCatalog[$relayId] ?? ['relay_id' => $relayId]) : null,
|
||||||
|
'coverage' => $coverage,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
return $gates;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array<int,array<string,mixed>> $lanes
|
||||||
|
* @return array<string,mixed>
|
||||||
|
* @throws Exception
|
||||||
|
*/
|
||||||
|
private function buildSelfServePayload(departments_o $department, array $lanes): array
|
||||||
|
{
|
||||||
|
$enabled = false;
|
||||||
|
try {
|
||||||
|
$enabled = $department->getSelfServeEnabled();
|
||||||
|
} catch (\Throwable) {
|
||||||
|
}
|
||||||
|
|
||||||
|
$readyLanes = array_values(array_filter($lanes, static function (array $lane): bool {
|
||||||
|
return (string)($lane['binding_coverage']['state'] ?? 'UNKNOWN') === 'READY';
|
||||||
|
}));
|
||||||
|
|
||||||
|
$taskRows = (new \objects\department_selfserve_tasks_o())->getFieldsWhere([
|
||||||
|
'department' => (int)$department->id,
|
||||||
|
'deleted_at' => null,
|
||||||
|
], ['id', 'lane', 'product']);
|
||||||
|
|
||||||
|
$productIds = [];
|
||||||
|
foreach ($taskRows as $taskRow) {
|
||||||
|
if (isset($taskRow['product'])) {
|
||||||
|
$productIds[(int)$taskRow['product']] = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
'enabled' => $enabled,
|
||||||
|
'lane_count' => count($lanes),
|
||||||
|
'ready_lanes' => count($readyLanes),
|
||||||
|
'configured_task_count' => count($taskRows),
|
||||||
|
'configured_product_count' => count($productIds),
|
||||||
|
'readiness_state' => !$enabled
|
||||||
|
? 'DISABLED'
|
||||||
|
: (count($lanes) === 0 ? 'UNCONFIGURED' : (count($readyLanes) === count($lanes) ? 'READY' : 'PARTIAL')),
|
||||||
|
'links' => [
|
||||||
|
'studio' => '/admin/' . (int)$department->id . '/modules/self-serve/studio',
|
||||||
|
'legacy' => '/superuser/selfserve',
|
||||||
|
],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array<int,array<string,mixed>> $lanes
|
||||||
|
* @return array<int,array<string,mixed>>
|
||||||
|
* @throws Exception
|
||||||
|
*/
|
||||||
|
private function buildScannerPayloads(int $departmentId, array $lanes): array
|
||||||
|
{
|
||||||
|
$laneIndex = [];
|
||||||
|
foreach ($lanes as $lane) {
|
||||||
|
$laneIndex[(int)$lane['id']] = $lane;
|
||||||
|
}
|
||||||
|
|
||||||
|
$recentScansByScannerId = $this->groupRecentScansByScannerId($departmentId);
|
||||||
|
$scanners = [];
|
||||||
|
foreach ((new plate_scanners_o())->getDepartmentScanners($departmentId) as $scanner) {
|
||||||
|
if (!$scanner->exists()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$scannerPayload = $scanner->asArray();
|
||||||
|
$laneId = isset($scannerPayload['lane_id']) ? (int)($scannerPayload['lane_id'] ?? 0) : 0;
|
||||||
|
$assignedLane = $laneId > 0 ? ($laneIndex[$laneId] ?? null) : null;
|
||||||
|
$recentScans = $recentScansByScannerId[(int)$scanner->id] ?? [];
|
||||||
|
$recentScanAt = $recentScans !== [] ? ($recentScans[0]['created_at'] ?? null) : null;
|
||||||
|
|
||||||
|
$assignmentState = $laneId <= 0
|
||||||
|
? 'UNASSIGNED'
|
||||||
|
: ($assignedLane === null
|
||||||
|
? 'INVALID'
|
||||||
|
: (((int)($assignedLane['binding_coverage']['missing'] ?? 0) === 0) ? 'READY' : 'PARTIAL'));
|
||||||
|
|
||||||
|
$scanners[] = [
|
||||||
|
...$scannerPayload,
|
||||||
|
'assigned_lane' => $assignedLane,
|
||||||
|
'assignment_state' => $assignmentState,
|
||||||
|
'recent_scan_at' => $recentScanAt,
|
||||||
|
'recent_scans' => $recentScans,
|
||||||
|
'recent_scan_count' => count($recentScans),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
return $scanners;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array<string,array<int,array<string,mixed>>>
|
||||||
|
*/
|
||||||
|
private function groupRecentScansByScannerId(int $departmentId): array
|
||||||
|
{
|
||||||
|
$rows = (new plate_scans_o())->getFieldsWhere([
|
||||||
|
'department_id' => $departmentId,
|
||||||
|
], ['id', 'plate_scanner_id', 'plate', 'bay_id', 'created_at']);
|
||||||
|
|
||||||
|
usort($rows, static function (array $left, array $right): int {
|
||||||
|
$rightTimestamp = strtotime((string)($right['created_at'] ?? '')) ?: 0;
|
||||||
|
$leftTimestamp = strtotime((string)($left['created_at'] ?? '')) ?: 0;
|
||||||
|
return $rightTimestamp <=> $leftTimestamp ?: ((int)($right['id'] ?? 0) <=> (int)($left['id'] ?? 0));
|
||||||
|
});
|
||||||
|
|
||||||
|
$grouped = [];
|
||||||
|
foreach ($rows as $row) {
|
||||||
|
$scannerId = (int)($row['plate_scanner_id'] ?? 0);
|
||||||
|
if ($scannerId <= 0) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isset($grouped[$scannerId])) {
|
||||||
|
$grouped[$scannerId] = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (count($grouped[$scannerId]) >= 5) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$grouped[$scannerId][] = [
|
||||||
|
'id' => (int)($row['id'] ?? 0),
|
||||||
|
'plate' => (string)($row['plate'] ?? ''),
|
||||||
|
'bay_id' => isset($row['bay_id']) ? (string)$row['bay_id'] : null,
|
||||||
|
'created_at' => isset($row['created_at']) ? (string)$row['created_at'] : null,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
return $grouped;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array<int,array<string,mixed>> $gateways
|
||||||
|
* @param array<int,array<string,mixed>> $lanes
|
||||||
|
* @param array<int,array<string,mixed>> $gates
|
||||||
|
* @param array<int,array<string,mixed>> $scanners
|
||||||
|
* @return array<int,array<string,mixed>>
|
||||||
|
*/
|
||||||
|
private function buildIssues(
|
||||||
|
string $transportMode,
|
||||||
|
array $gateways,
|
||||||
|
array $lanes,
|
||||||
|
array $gates,
|
||||||
|
array $scanners,
|
||||||
|
array $selfServe
|
||||||
|
): array {
|
||||||
|
$issues = [];
|
||||||
|
|
||||||
|
if ($gateways === []) {
|
||||||
|
$issues[] = [
|
||||||
|
'severity' => 'danger',
|
||||||
|
'code' => 'NO_GATEWAY',
|
||||||
|
'message' => 'No edge gateway has been claimed for this department.',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
$onlineGateways = array_values(array_filter($gateways, static function (array $gateway): bool {
|
||||||
|
return strtoupper((string)($gateway['status'] ?? '')) === edge_gateway_manager::STATUS_ONLINE;
|
||||||
|
}));
|
||||||
|
|
||||||
|
if ($transportMode === edge_gateway_manager::TRANSPORT_MODE_GATEWAY && $onlineGateways === []) {
|
||||||
|
$issues[] = [
|
||||||
|
'severity' => 'danger',
|
||||||
|
'code' => 'NO_ONLINE_GATEWAY',
|
||||||
|
'message' => 'Gateway transport mode is enabled, but no department gateway is currently online.',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($lanes as $lane) {
|
||||||
|
if ((int)($lane['binding_coverage']['missing'] ?? 0) > 0) {
|
||||||
|
$issues[] = [
|
||||||
|
'severity' => 'warning',
|
||||||
|
'code' => 'LANE_BINDING_GAP',
|
||||||
|
'message' => 'Lane ' . (string)$lane['name'] . ' is missing relay bindings.',
|
||||||
|
'target_type' => 'lane',
|
||||||
|
'target_id' => (int)$lane['id'],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($gates as $gate) {
|
||||||
|
if (!($gate['config_complete'] ?? false)) {
|
||||||
|
$issues[] = [
|
||||||
|
'severity' => 'warning',
|
||||||
|
'code' => 'GATE_CONFIG_INCOMPLETE',
|
||||||
|
'message' => 'Gate ' . (string)$gate['name'] . ' has incomplete transport configuration.',
|
||||||
|
'target_type' => 'gate',
|
||||||
|
'target_id' => (int)$gate['id'],
|
||||||
|
];
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (($gate['transport_type'] ?? '') === 'RELAY' && !($gate['coverage']['covered'] ?? false)) {
|
||||||
|
$issues[] = [
|
||||||
|
'severity' => 'warning',
|
||||||
|
'code' => 'GATE_BINDING_MISSING',
|
||||||
|
'message' => 'Gate ' . (string)$gate['name'] . ' is assigned to an unbound relay.',
|
||||||
|
'target_type' => 'gate',
|
||||||
|
'target_id' => (int)$gate['id'],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($scanners as $scanner) {
|
||||||
|
if (($scanner['assignment_state'] ?? 'UNASSIGNED') === 'UNASSIGNED') {
|
||||||
|
$issues[] = [
|
||||||
|
'severity' => 'warning',
|
||||||
|
'code' => 'SCANNER_UNASSIGNED',
|
||||||
|
'message' => 'Scanner ' . (string)$scanner['name'] . ' is not assigned to a default lane.',
|
||||||
|
'target_type' => 'scanner',
|
||||||
|
'target_id' => (int)$scanner['id'],
|
||||||
|
];
|
||||||
|
} elseif (($scanner['assignment_state'] ?? '') === 'PARTIAL') {
|
||||||
|
$issues[] = [
|
||||||
|
'severity' => 'info',
|
||||||
|
'code' => 'SCANNER_LANE_PARTIAL',
|
||||||
|
'message' => 'Scanner ' . (string)$scanner['name'] . ' is assigned to a lane with missing relay coverage.',
|
||||||
|
'target_type' => 'scanner',
|
||||||
|
'target_id' => (int)$scanner['id'],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (($selfServe['enabled'] ?? false) && (int)($selfServe['ready_lanes'] ?? 0) < (int)($selfServe['lane_count'] ?? 0)) {
|
||||||
|
$issues[] = [
|
||||||
|
'severity' => 'warning',
|
||||||
|
'code' => 'SELFSERVE_PARTIAL_READY',
|
||||||
|
'message' => 'Self-serve is enabled, but one or more lanes are missing required relay coverage.',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
return $issues;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array<int,array<string,mixed>> $gateways
|
||||||
|
* @param array<int,array<string,mixed>> $lanes
|
||||||
|
* @param array<int,array<string,mixed>> $gates
|
||||||
|
* @param array<int,array<string,mixed>> $scanners
|
||||||
|
* @return array<int,array<string,mixed>>
|
||||||
|
*/
|
||||||
|
private function buildActions(
|
||||||
|
int $departmentId,
|
||||||
|
array $gateways,
|
||||||
|
array $lanes,
|
||||||
|
array $gates,
|
||||||
|
array $scanners,
|
||||||
|
array $selfServe
|
||||||
|
): array {
|
||||||
|
$actions = [
|
||||||
|
[
|
||||||
|
'code' => 'OPEN_GATEWAY_TAB',
|
||||||
|
'label' => 'Open gateway controls',
|
||||||
|
'path' => '/superuser/departments/' . $departmentId . '/gateways?tab=gateways',
|
||||||
|
],
|
||||||
|
];
|
||||||
|
|
||||||
|
if ($gateways === []) {
|
||||||
|
$actions[] = [
|
||||||
|
'code' => 'INSTALL_GATEWAY',
|
||||||
|
'label' => 'Install first edge gateway',
|
||||||
|
'path' => '/superuser/configuration/edgegateway',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($lanes as $lane) {
|
||||||
|
if ((int)($lane['binding_coverage']['missing'] ?? 0) > 0) {
|
||||||
|
$actions[] = [
|
||||||
|
'code' => 'REVIEW_LANE_BINDINGS',
|
||||||
|
'label' => 'Resolve lane bindings',
|
||||||
|
'path' => '/superuser/departments/' . $departmentId . '/gateways?tab=lanes',
|
||||||
|
];
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($gates as $gate) {
|
||||||
|
if (($gate['transport_type'] ?? '') === 'RELAY' && !($gate['coverage']['covered'] ?? false)) {
|
||||||
|
$actions[] = [
|
||||||
|
'code' => 'REVIEW_GATE_BINDINGS',
|
||||||
|
'label' => 'Resolve gate relay bindings',
|
||||||
|
'path' => '/superuser/departments/' . $departmentId . '/gateways?tab=gates',
|
||||||
|
];
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($scanners as $scanner) {
|
||||||
|
if (($scanner['assignment_state'] ?? 'UNASSIGNED') === 'UNASSIGNED') {
|
||||||
|
$actions[] = [
|
||||||
|
'code' => 'ASSIGN_SCANNERS',
|
||||||
|
'label' => 'Assign scanners to lanes',
|
||||||
|
'path' => '/superuser/departments/' . $departmentId . '/gateways?tab=scanners',
|
||||||
|
];
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (($selfServe['enabled'] ?? false) && (int)($selfServe['lane_count'] ?? 0) > 0) {
|
||||||
|
$actions[] = [
|
||||||
|
'code' => 'OPEN_SELFSERVE_STUDIO',
|
||||||
|
'label' => 'Open self-serve studio',
|
||||||
|
'path' => '/admin/' . $departmentId . '/modules/self-serve/studio',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
return $actions;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array<string,mixed> $departmentPayload
|
||||||
|
* @param array<string,mixed>|null $departmentRow
|
||||||
|
* @param array<int,array<string,mixed>> $gateways
|
||||||
|
* @param array<int,array<string,mixed>> $lanes
|
||||||
|
* @param array<int,array<string,mixed>> $gates
|
||||||
|
* @param array<int,array<string,mixed>> $scanners
|
||||||
|
* @param array<int,array<string,mixed>> $issues
|
||||||
|
* @return array<string,mixed>
|
||||||
|
*/
|
||||||
|
private function buildSummary(
|
||||||
|
array $departmentPayload,
|
||||||
|
?array $departmentRow,
|
||||||
|
string $transportMode,
|
||||||
|
array $gateways,
|
||||||
|
array $lanes,
|
||||||
|
array $gates,
|
||||||
|
array $scanners,
|
||||||
|
array $selfServe,
|
||||||
|
array $issues
|
||||||
|
): array {
|
||||||
|
$onlineGatewayCount = count(array_filter($gateways, static function (array $gateway): bool {
|
||||||
|
return strtoupper((string)($gateway['status'] ?? '')) === edge_gateway_manager::STATUS_ONLINE;
|
||||||
|
}));
|
||||||
|
$primaryGateway = null;
|
||||||
|
foreach ($gateways as $gateway) {
|
||||||
|
if (!empty($gateway['is_primary'])) {
|
||||||
|
$primaryGateway = [
|
||||||
|
'id' => (int)$gateway['id'],
|
||||||
|
'label' => (string)($gateway['label'] ?? ('Gateway ' . $gateway['id'])),
|
||||||
|
'status' => (string)($gateway['status'] ?? edge_gateway_manager::STATUS_OFFLINE),
|
||||||
|
];
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($primaryGateway === null && $gateways !== []) {
|
||||||
|
$gateway = $gateways[0];
|
||||||
|
$primaryGateway = [
|
||||||
|
'id' => (int)$gateway['id'],
|
||||||
|
'label' => (string)($gateway['label'] ?? ('Gateway ' . $gateway['id'])),
|
||||||
|
'status' => (string)($gateway['status'] ?? edge_gateway_manager::STATUS_OFFLINE),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
$requiredRelayIds = [];
|
||||||
|
$coveredRelayIds = [];
|
||||||
|
foreach ($lanes as $lane) {
|
||||||
|
foreach ((array)($lane['relay_slots'] ?? []) as $slot) {
|
||||||
|
$relayId = trim((string)($slot['relay_id'] ?? ''));
|
||||||
|
if ($relayId === '') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$requiredRelayIds[$relayId] = true;
|
||||||
|
if (!empty($slot['coverage']['covered'])) {
|
||||||
|
$coveredRelayIds[$relayId] = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
foreach ($gates as $gate) {
|
||||||
|
if (($gate['transport_type'] ?? '') !== 'RELAY') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$relayId = trim((string)($gate['relay']['relay_id'] ?? $gate['config']['relay_id'] ?? ''));
|
||||||
|
if ($relayId === '') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$requiredRelayIds[$relayId] = true;
|
||||||
|
if (!empty($gate['coverage']['covered'])) {
|
||||||
|
$coveredRelayIds[$relayId] = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$assignedScannerCount = count(array_filter($scanners, static function (array $scanner): bool {
|
||||||
|
return (int)($scanner['lane_id'] ?? 0) > 0;
|
||||||
|
}));
|
||||||
|
|
||||||
|
$recentScanAt = null;
|
||||||
|
foreach ($scanners as $scanner) {
|
||||||
|
$candidate = isset($scanner['recent_scan_at']) ? (string)$scanner['recent_scan_at'] : null;
|
||||||
|
if ($candidate === null || trim($candidate) === '') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if ($recentScanAt === null || strtotime($candidate) > strtotime($recentScanAt)) {
|
||||||
|
$recentScanAt = $candidate;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$relayGateCount = count(array_filter($gates, static function (array $gate): bool {
|
||||||
|
return ($gate['transport_type'] ?? '') === 'RELAY';
|
||||||
|
}));
|
||||||
|
$phoneGateCount = count(array_filter($gates, static function (array $gate): bool {
|
||||||
|
return ($gate['transport_type'] ?? '') === 'PHONE_CALL';
|
||||||
|
}));
|
||||||
|
|
||||||
|
return [
|
||||||
|
'department_id' => (int)$departmentPayload['id'],
|
||||||
|
'department_name' => (string)$departmentPayload['name'],
|
||||||
|
'order_priority' => (int)($departmentRow['order_priority'] ?? $departmentPayload['order_priority'] ?? PHP_INT_MAX),
|
||||||
|
'transport_mode' => $transportMode,
|
||||||
|
'gateway_count' => count($gateways),
|
||||||
|
'online_gateway_count' => $onlineGatewayCount,
|
||||||
|
'primary_gateway' => $primaryGateway,
|
||||||
|
'lane_count' => count($lanes),
|
||||||
|
'self_serve_enabled' => (bool)($selfServe['enabled'] ?? false),
|
||||||
|
'self_serve_ready_lanes' => (int)($selfServe['ready_lanes'] ?? 0),
|
||||||
|
'required_relay_count' => count($requiredRelayIds),
|
||||||
|
'bound_relay_count' => count($coveredRelayIds),
|
||||||
|
'missing_binding_count' => max(0, count($requiredRelayIds) - count($coveredRelayIds)),
|
||||||
|
'gate_count' => count($gates),
|
||||||
|
'gate_transport_mix' => [
|
||||||
|
'relay' => $relayGateCount,
|
||||||
|
'phone_call' => $phoneGateCount,
|
||||||
|
],
|
||||||
|
'scanner_count' => count($scanners),
|
||||||
|
'assigned_scanner_count' => $assignedScannerCount,
|
||||||
|
'recent_scan_at' => $recentScanAt,
|
||||||
|
'issue_count' => count($issues),
|
||||||
|
'health' => $this->deriveHealthState($transportMode, $gateways, $issues),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array<int,array<string,mixed>> $gateways
|
||||||
|
* @param array<int,array<string,mixed>> $issues
|
||||||
|
*/
|
||||||
|
private function deriveHealthState(string $transportMode, array $gateways, array $issues): string
|
||||||
|
{
|
||||||
|
foreach ($issues as $issue) {
|
||||||
|
if (($issue['severity'] ?? '') === 'danger') {
|
||||||
|
return 'AT_RISK';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($transportMode === edge_gateway_manager::TRANSPORT_MODE_GATEWAY) {
|
||||||
|
foreach ($gateways as $gateway) {
|
||||||
|
if (strtoupper((string)($gateway['status'] ?? '')) === edge_gateway_manager::STATUS_ONLINE) {
|
||||||
|
return $issues === [] ? 'READY' : 'PARTIAL';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 'AT_RISK';
|
||||||
|
}
|
||||||
|
|
||||||
|
return $issues === [] ? 'READY' : 'PARTIAL';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array<string,array<int,array<string,mixed>>> $bindingsByRelayId
|
||||||
|
* @return array<string,mixed>
|
||||||
|
*/
|
||||||
|
private function buildRelayCoverage(string $relayId, array $bindingsByRelayId): array
|
||||||
|
{
|
||||||
|
$bindings = $bindingsByRelayId[$relayId] ?? [];
|
||||||
|
$primaryBinding = $bindings[0] ?? null;
|
||||||
|
|
||||||
|
return [
|
||||||
|
'relay_id' => $relayId,
|
||||||
|
'covered' => $bindings !== [],
|
||||||
|
'status' => $bindings !== [] ? 'BOUND' : 'MISSING',
|
||||||
|
'binding_count' => count($bindings),
|
||||||
|
'primary_binding' => $primaryBinding,
|
||||||
|
'bindings' => $bindings,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array<int,array<string,mixed>> $gateways
|
||||||
|
* @param array<string,array<int,array<string,mixed>>> $consumersByRelayId
|
||||||
|
* @return array<int,array<string,mixed>>
|
||||||
|
*/
|
||||||
|
private function applyBindingConsumerContexts(array $gateways, array $consumersByRelayId): array
|
||||||
|
{
|
||||||
|
foreach ($gateways as $gatewayIndex => $gateway) {
|
||||||
|
$bindings = isset($gateway['bindings']) && is_array($gateway['bindings'])
|
||||||
|
? (array)$gateway['bindings']
|
||||||
|
: [];
|
||||||
|
|
||||||
|
foreach ($bindings as $bindingIndex => $binding) {
|
||||||
|
if (!is_array($binding)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$relayId = trim((string)($binding['relay_id'] ?? ''));
|
||||||
|
if ($relayId === '') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$metadata = isset($binding['metadata']) && is_array($binding['metadata'])
|
||||||
|
? (array)$binding['metadata']
|
||||||
|
: [];
|
||||||
|
$consumerContexts = $consumersByRelayId[$relayId] ?? [];
|
||||||
|
$metadata['consumer_contexts'] = $consumerContexts;
|
||||||
|
$metadata['consumers'] = $consumerContexts;
|
||||||
|
$bindings[$bindingIndex]['metadata'] = $metadata;
|
||||||
|
$bindings[$bindingIndex]['consumer_contexts'] = $consumerContexts;
|
||||||
|
}
|
||||||
|
|
||||||
|
$gateways[$gatewayIndex]['bindings'] = $bindings;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $gateways;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array<string,mixed> $config
|
||||||
|
*/
|
||||||
|
private function isGateConfigComplete(array $config): bool
|
||||||
|
{
|
||||||
|
$type = strtoupper(trim((string)($config['type'] ?? '')));
|
||||||
|
if ($type === 'PHONE_CALL') {
|
||||||
|
return trim((string)($config['phone_number'] ?? '')) !== ''
|
||||||
|
&& isset($config['call_duration_threshold']);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($type === 'RELAY') {
|
||||||
|
return trim((string)($config['relay_id'] ?? '')) !== '';
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function manager(): edge_gateway_manager
|
||||||
|
{
|
||||||
|
return $this->manager ?? new edge_gateway_manager();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2999,24 +2999,11 @@ BASH;
|
|||||||
}
|
}
|
||||||
|
|
||||||
$apiBaseUrl = $this->getApiBaseUrl();
|
$apiBaseUrl = $this->getApiBaseUrl();
|
||||||
$parsed = parse_url($apiBaseUrl);
|
if (trim($apiBaseUrl) === '') {
|
||||||
$host = $parsed['host'] ?? null;
|
|
||||||
if (!is_string($host) || trim($host) === '') {
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
$scheme = strtolower((string)($parsed['scheme'] ?? 'http')) === 'https' ? 'https' : 'http';
|
return rtrim($apiBaseUrl, '/') . '/edge-broker';
|
||||||
$port = (int)(getenv('EDGE_PUBLIC_BROKER_PORT') ?: 4300);
|
|
||||||
if ($port <= 0) {
|
|
||||||
$port = 4300;
|
|
||||||
}
|
|
||||||
|
|
||||||
$hostWithPort = $host;
|
|
||||||
if (!(($scheme === 'https' && $port === 443) || ($scheme === 'http' && $port === 80))) {
|
|
||||||
$hostWithPort .= ':' . $port;
|
|
||||||
}
|
|
||||||
|
|
||||||
return $scheme . '://' . $hostWithPort;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private function buildBrokerPublicWebSocketUrl(string $path = ''): ?string
|
private function buildBrokerPublicWebSocketUrl(string $path = ''): ?string
|
||||||
@@ -3034,9 +3021,11 @@ BASH;
|
|||||||
}
|
}
|
||||||
|
|
||||||
$port = isset($parsed['port']) ? ':' . (int)$parsed['port'] : '';
|
$port = isset($parsed['port']) ? ':' . (int)$parsed['port'] : '';
|
||||||
|
$basePath = rtrim((string)($parsed['path'] ?? ''), '/');
|
||||||
$normalizedPath = '/' . ltrim($path, '/');
|
$normalizedPath = '/' . ltrim($path, '/');
|
||||||
|
$fullPath = $basePath . ($normalizedPath === '/' ? '' : $normalizedPath);
|
||||||
|
|
||||||
return $scheme . '://' . $host . $port . ($normalizedPath === '/' ? '' : $normalizedPath);
|
return $scheme . '://' . $host . $port . ($fullPath === '' ? '' : $fullPath);
|
||||||
}
|
}
|
||||||
|
|
||||||
private function buildBrokerInternalUrl(): ?string
|
private function buildBrokerInternalUrl(): ?string
|
||||||
@@ -3133,6 +3122,29 @@ BASH;
|
|||||||
}
|
}
|
||||||
|
|
||||||
$metadata['fallback_mode'] = $fallbackMode;
|
$metadata['fallback_mode'] = $fallbackMode;
|
||||||
|
$consumerContexts = $binding['consumer_contexts'] ?? $binding['consumers'] ?? $metadata['consumer_contexts'] ?? $metadata['consumers'] ?? [];
|
||||||
|
if (!is_array($consumerContexts)) {
|
||||||
|
$consumerContexts = [];
|
||||||
|
}
|
||||||
|
$consumerContexts = array_values(array_filter(array_map(static function (mixed $consumer): ?array {
|
||||||
|
if (!is_array($consumer)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$consumerType = trim((string)($consumer['type'] ?? ''));
|
||||||
|
if ($consumerType === '') {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
'type' => $consumerType,
|
||||||
|
'id' => isset($consumer['id']) ? (int)$consumer['id'] : null,
|
||||||
|
'slot' => isset($consumer['slot']) ? (string)$consumer['slot'] : null,
|
||||||
|
'label' => isset($consumer['label']) ? (string)$consumer['label'] : null,
|
||||||
|
];
|
||||||
|
}, $consumerContexts)));
|
||||||
|
$metadata['consumer_contexts'] = $consumerContexts;
|
||||||
|
$metadata['consumers'] = $consumerContexts;
|
||||||
|
|
||||||
if (isset($binding['last_resolution']) && is_array($binding['last_resolution'])) {
|
if (isset($binding['last_resolution']) && is_array($binding['last_resolution'])) {
|
||||||
$metadata['last_resolution'] = (array)$binding['last_resolution'];
|
$metadata['last_resolution'] = (array)$binding['last_resolution'];
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ namespace routes;
|
|||||||
|
|
||||||
use classes\authentication;
|
use classes\authentication;
|
||||||
use classes\edge_gateway_manager;
|
use classes\edge_gateway_manager;
|
||||||
|
use classes\edge_gateway_department_workspace_service;
|
||||||
use classes\edge_gateway_operation_exception;
|
use classes\edge_gateway_operation_exception;
|
||||||
use classes\edge_gateway_operation_service;
|
use classes\edge_gateway_operation_service;
|
||||||
use classes\edge_gateway_registry_service;
|
use classes\edge_gateway_registry_service;
|
||||||
@@ -23,6 +24,12 @@ class moduleEdgeGatewayRoute
|
|||||||
$this->get('/modules/edge-gateways', fn() => $this->handleListGateways(), [
|
$this->get('/modules/edge-gateways', fn() => $this->handleListGateways(), [
|
||||||
'modules_shelly_config' => 'List edge gateway module fleet',
|
'modules_shelly_config' => 'List edge gateway module fleet',
|
||||||
]);
|
]);
|
||||||
|
$this->get('/modules/edge-gateways/workspace/departments', fn() => $this->handleDepartmentWorkspaceList(), [
|
||||||
|
'modules_shelly_config' => 'List department hardware workspaces',
|
||||||
|
]);
|
||||||
|
$this->get('/modules/edge-gateways/workspace/departments/{id}', fn() => $this->handleDepartmentWorkspaceDetail(), [
|
||||||
|
'modules_shelly_config' => 'View department hardware workspace detail',
|
||||||
|
]);
|
||||||
$this->get('/modules/edge-gateways/{id}', fn() => $this->handleGatewayDetail(), [
|
$this->get('/modules/edge-gateways/{id}', fn() => $this->handleGatewayDetail(), [
|
||||||
'modules_shelly_config' => 'View edge gateway module detail',
|
'modules_shelly_config' => 'View edge gateway module detail',
|
||||||
]);
|
]);
|
||||||
@@ -102,6 +109,26 @@ class moduleEdgeGatewayRoute
|
|||||||
$response->success($gateway);
|
$response->success($gateway);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private function handleDepartmentWorkspaceList(): void
|
||||||
|
{
|
||||||
|
global /** @var response $response */ $response;
|
||||||
|
$user = $this->requireModuleOperator();
|
||||||
|
$summaries = $this->workspaces()->listDepartmentSummaries();
|
||||||
|
(new logs_o())->add('modules_edgegateway', 'global', 1, $user->id, 'MODULES_EDGEGATEWAY_WORKSPACE_LIST', 'Listed department hardware workspace summaries');
|
||||||
|
$response->success($summaries);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function handleDepartmentWorkspaceDetail(): void
|
||||||
|
{
|
||||||
|
global /** @var response $response */ $response;
|
||||||
|
$user = $this->requireModuleOperator();
|
||||||
|
$departmentId = (int)$this->fromRoute('id');
|
||||||
|
self::requireParameterIntPositive($departmentId, 'id');
|
||||||
|
$this->requireDepartmentAccess((string)$departmentId);
|
||||||
|
(new logs_o())->add('modules_edgegateway', 'global', 1, $user->id, 'MODULES_EDGEGATEWAY_WORKSPACE_GET', 'Fetched department hardware workspace detail');
|
||||||
|
$response->success($this->workspaces()->getDepartmentWorkspace($departmentId));
|
||||||
|
}
|
||||||
|
|
||||||
private function handleGatewayTasksPage(): void
|
private function handleGatewayTasksPage(): void
|
||||||
{
|
{
|
||||||
global /** @var response $response */ $response;
|
global /** @var response $response */ $response;
|
||||||
@@ -395,4 +422,9 @@ class moduleEdgeGatewayRoute
|
|||||||
{
|
{
|
||||||
return new edge_gateway_manager();
|
return new edge_gateway_manager();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private function workspaces(): edge_gateway_department_workspace_service
|
||||||
|
{
|
||||||
|
return new edge_gateway_department_workspace_service();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
namespace objects;
|
namespace objects;
|
||||||
|
|
||||||
use classes\department_gate_config;
|
use classes\department_gate_config;
|
||||||
|
use classes\edge_gateway_manager;
|
||||||
use classes\bird;
|
use classes\bird;
|
||||||
use classes\db;
|
use classes\db;
|
||||||
use classes\object_property;
|
use classes\object_property;
|
||||||
@@ -262,6 +263,11 @@ class department_gates_o extends db
|
|||||||
return new slack();
|
return new slack();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected function resolveEdgeGatewayManager(): edge_gateway_manager
|
||||||
|
{
|
||||||
|
return new edge_gateway_manager();
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return array<int,array<string,mixed>>
|
* @return array<int,array<string,mixed>>
|
||||||
*/
|
*/
|
||||||
@@ -367,9 +373,25 @@ class department_gates_o extends db
|
|||||||
$this->requireSelected();
|
$this->requireSelected();
|
||||||
$config = (array)$this->config->value();
|
$config = (array)$this->config->value();
|
||||||
|
|
||||||
if (!$this->matchesPhoneCallGateConfig($config)) {
|
if ($this->matchesPhoneCallGateConfig($config)) {
|
||||||
throw new Exception('Unsupported gate type: ' . (string)($config['type'] ?? ''));
|
$this->openPhoneCallGate($config);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (strtoupper(trim((string)($config['type'] ?? ''))) === 'RELAY') {
|
||||||
|
$this->openRelayBackedGate($config);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Exception('Unsupported gate type: ' . (string)($config['type'] ?? ''));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array<string,mixed> $config
|
||||||
|
* @throws Exception
|
||||||
|
*/
|
||||||
|
protected function openPhoneCallGate(array $config): void
|
||||||
|
{
|
||||||
if (!isset($config['phone_number'])) {
|
if (!isset($config['phone_number'])) {
|
||||||
throw new Exception('Phone number is required for PHONE_CALL gate type');
|
throw new Exception('Phone number is required for PHONE_CALL gate type');
|
||||||
}
|
}
|
||||||
@@ -391,4 +413,30 @@ class department_gates_o extends db
|
|||||||
throw new Exception('Failed to open gate relay via phone call', 0, $e);
|
throw new Exception('Failed to open gate relay via phone call', 0, $e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array<string,mixed> $config
|
||||||
|
* @throws Exception
|
||||||
|
*/
|
||||||
|
protected function openRelayBackedGate(array $config): void
|
||||||
|
{
|
||||||
|
$relayId = trim((string)($config['relay_id'] ?? ''));
|
||||||
|
if ($relayId === '') {
|
||||||
|
throw new Exception('relay_id is required for RELAY gate type');
|
||||||
|
}
|
||||||
|
|
||||||
|
$departmentId = (int)$this->department->value();
|
||||||
|
if ($departmentId <= 0) {
|
||||||
|
throw new Exception('Gate department is invalid');
|
||||||
|
}
|
||||||
|
|
||||||
|
$pulseSeconds = isset($config['pulse_seconds']) ? max(0, (int)$config['pulse_seconds']) : 1;
|
||||||
|
$manager = $this->resolveEdgeGatewayManager();
|
||||||
|
$manager->dispatchRelaySwitch($departmentId, $relayId, true);
|
||||||
|
|
||||||
|
if ($pulseSeconds > 0) {
|
||||||
|
usleep($pulseSeconds * 1000000);
|
||||||
|
$manager->dispatchRelaySwitch($departmentId, $relayId, false);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ namespace objects;
|
|||||||
|
|
||||||
use classes\db;
|
use classes\db;
|
||||||
use classes\object_property;
|
use classes\object_property;
|
||||||
|
use Exception;
|
||||||
use traits\db_object_t;
|
use traits\db_object_t;
|
||||||
|
|
||||||
class plate_scanners_o extends db
|
class plate_scanners_o extends db
|
||||||
@@ -11,12 +12,16 @@ class plate_scanners_o extends db
|
|||||||
use db_object_t;
|
use db_object_t;
|
||||||
|
|
||||||
public object_property $department_id;
|
public object_property $department_id;
|
||||||
|
public object_property $lane_id;
|
||||||
public object_property $name;
|
public object_property $name;
|
||||||
public object_property $notes;
|
public object_property $notes;
|
||||||
public object_property $api_key;
|
public object_property $api_key;
|
||||||
|
|
||||||
|
private static bool $schemaInitialized = false;
|
||||||
|
|
||||||
public function structure(): void
|
public function structure(): void
|
||||||
{
|
{
|
||||||
|
self::ensureSchema();
|
||||||
$this->setTable('plate_scanners');
|
$this->setTable('plate_scanners');
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -41,22 +46,25 @@ class plate_scanners_o extends db
|
|||||||
public function getObjectProperties(): void
|
public function getObjectProperties(): void
|
||||||
{
|
{
|
||||||
$this->department_id = new object_property($this->table, $this->id, 'department_id', 'int');
|
$this->department_id = new object_property($this->table, $this->id, 'department_id', 'int');
|
||||||
|
$this->lane_id = new object_property($this->table, $this->id, 'lane_id', 'int');
|
||||||
$this->name = new object_property($this->table, $this->id, 'name', 'string');
|
$this->name = new object_property($this->table, $this->id, 'name', 'string');
|
||||||
$this->notes = new object_property($this->table, $this->id, 'notes', 'string');
|
$this->notes = new object_property($this->table, $this->id, 'notes', 'string');
|
||||||
$this->api_key = new object_property($this->table, $this->id, 'api_key', 'string');
|
$this->api_key = new object_property($this->table, $this->id, 'api_key', 'string');
|
||||||
}
|
}
|
||||||
|
|
||||||
public function add(int $department_id, string $name, string $notes): void
|
public function add(int $department_id, string $name, string $notes, ?int $lane_id = null): void
|
||||||
{
|
{
|
||||||
global $db, $response;
|
global $db, $response;
|
||||||
try {
|
try {
|
||||||
// Generate an API key
|
// Generate an API key
|
||||||
$api_key = bin2hex(random_bytes(32));
|
$api_key = bin2hex(random_bytes(32));
|
||||||
|
$lane_id = $this->normalizeLaneId($department_id, $lane_id);
|
||||||
// Avoid SQL injection
|
// Avoid SQL injection
|
||||||
$name = $db->escape_string($name);
|
$name = $db->escape_string($name);
|
||||||
$notes = $db->escape_string($notes);
|
$notes = $db->escape_string($notes);
|
||||||
|
$laneValue = $lane_id === null ? 'NULL' : (string)$lane_id;
|
||||||
// Create a new record in the database
|
// Create a new record in the database
|
||||||
$sql = "INSERT INTO $this->table (department_id, name, notes, api_key) VALUES ($department_id, '$name', '$notes', '$api_key')";
|
$sql = "INSERT INTO $this->table (department_id, lane_id, name, notes, api_key) VALUES ($department_id, $laneValue, '$name', '$notes', '$api_key')";
|
||||||
$db->query($sql);
|
$db->query($sql);
|
||||||
|
|
||||||
// Get the id of the new record
|
// Get the id of the new record
|
||||||
@@ -69,7 +77,14 @@ class plate_scanners_o extends db
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public function edit(int $id, int $department_id, string $name, string $notes): void
|
public function edit(
|
||||||
|
int $id,
|
||||||
|
int $department_id,
|
||||||
|
string $name,
|
||||||
|
string $notes,
|
||||||
|
?int $lane_id = null,
|
||||||
|
bool $laneIdProvided = false
|
||||||
|
): void
|
||||||
{
|
{
|
||||||
global $db, $response;
|
global $db, $response;
|
||||||
$this->id = $id;
|
$this->id = $id;
|
||||||
@@ -77,8 +92,17 @@ class plate_scanners_o extends db
|
|||||||
// Avoid SQL injection
|
// Avoid SQL injection
|
||||||
$name = $db->escape_string($name);
|
$name = $db->escape_string($name);
|
||||||
$notes = $db->escape_string($notes);
|
$notes = $db->escape_string($notes);
|
||||||
|
$setParts = [
|
||||||
|
"department_id = $department_id",
|
||||||
|
"name = '$name'",
|
||||||
|
"notes = '$notes'",
|
||||||
|
];
|
||||||
|
if ($laneIdProvided) {
|
||||||
|
$lane_id = $this->normalizeLaneId($department_id, $lane_id);
|
||||||
|
$setParts[] = 'lane_id = ' . ($lane_id === null ? 'NULL' : (string)$lane_id);
|
||||||
|
}
|
||||||
// Update the record in the database
|
// Update the record in the database
|
||||||
$sql = "UPDATE $this->table SET department_id = $department_id, name = '$name', notes = '$notes' WHERE id = $id";
|
$sql = "UPDATE $this->table SET " . implode(', ', $setParts) . " WHERE id = $id";
|
||||||
$db->query($sql);
|
$db->query($sql);
|
||||||
|
|
||||||
// Set the values of the object properties
|
// Set the values of the object properties
|
||||||
@@ -102,4 +126,116 @@ class plate_scanners_o extends db
|
|||||||
}
|
}
|
||||||
return $this;
|
return $this;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
/**
|
||||||
|
* @return array{id:int,department_id:int,lane_id:int|null,name:string,notes:string,api_key:string}
|
||||||
|
* @throws Exception
|
||||||
|
*/
|
||||||
|
public function asArray(): array
|
||||||
|
{
|
||||||
|
$this->requireSelected();
|
||||||
|
|
||||||
|
return [
|
||||||
|
'id' => (int)$this->id,
|
||||||
|
'department_id' => (int)$this->department_id->value(),
|
||||||
|
'lane_id' => $this->lane_id->value() === null ? null : (int)$this->lane_id->value(),
|
||||||
|
'name' => (string)$this->name->value(),
|
||||||
|
'notes' => (string)$this->notes->value(),
|
||||||
|
'api_key' => (string)$this->api_key->value(),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array<int,plate_scanners_o>
|
||||||
|
* @throws Exception
|
||||||
|
*/
|
||||||
|
public function getDepartmentScanners(int $departmentId): array
|
||||||
|
{
|
||||||
|
$scanners = [];
|
||||||
|
$rows = self::getFieldsWhere([
|
||||||
|
'department_id' => $departmentId,
|
||||||
|
], ['id']);
|
||||||
|
|
||||||
|
foreach ($rows as $row) {
|
||||||
|
$scanner = (new plate_scanners_o())->select((int)$row['id']);
|
||||||
|
if ($scanner->exists()) {
|
||||||
|
$scanners[] = $scanner;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $scanners;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @throws Exception
|
||||||
|
*/
|
||||||
|
public function rotateApiKey(int $id): array
|
||||||
|
{
|
||||||
|
$scanner = $this->select($id);
|
||||||
|
if (!$scanner->exists()) {
|
||||||
|
throw new Exception('Number plate scanner not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
$newApiKey = bin2hex(random_bytes(32));
|
||||||
|
$scanner->api_key->set($newApiKey);
|
||||||
|
|
||||||
|
return $scanner->asArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
private function normalizeLaneId(int $departmentId, ?int $laneId): ?int
|
||||||
|
{
|
||||||
|
if ($laneId === null || $laneId <= 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$lane = (new department_lanes_o())->select($laneId);
|
||||||
|
if (!$lane->exists()) {
|
||||||
|
throw new Exception('Department lane not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((int)$lane->department->value() !== $departmentId) {
|
||||||
|
throw new Exception('The lane does not belong to the number plate scanner department');
|
||||||
|
}
|
||||||
|
|
||||||
|
return (int)$lane->id;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static function ensureSchema(): void
|
||||||
|
{
|
||||||
|
if (self::$schemaInitialized) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
global $db;
|
||||||
|
|
||||||
|
if (!self::tableHasColumn('plate_scanners', 'lane_id')) {
|
||||||
|
$db->query("ALTER TABLE `plate_scanners` ADD COLUMN `lane_id` INT NULL AFTER `department_id`");
|
||||||
|
}
|
||||||
|
|
||||||
|
self::$schemaInitialized = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static function tableHasColumn(string $table, string $column): bool
|
||||||
|
{
|
||||||
|
global $db;
|
||||||
|
|
||||||
|
$table = $db->escape_string($table);
|
||||||
|
$column = $db->escape_string($column);
|
||||||
|
$database = $db->escape_string($db->getDatabase());
|
||||||
|
|
||||||
|
$result = $db->query(
|
||||||
|
"SELECT COUNT(*) AS c
|
||||||
|
FROM information_schema.COLUMNS
|
||||||
|
WHERE TABLE_SCHEMA = '$database'
|
||||||
|
AND TABLE_NAME = '$table'
|
||||||
|
AND COLUMN_NAME = '$column'"
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!$result) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$row = $result->fetch_assoc();
|
||||||
|
return ((int)($row['c'] ?? 0)) > 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -69,6 +69,17 @@ class machineButtonPressRoute
|
|||||||
return (int)$lane->id;
|
return (int)$lane->id;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ($plateScanner->lane_id->value() !== null) {
|
||||||
|
$lane = (new department_lanes_o())->select((int)$plateScanner->lane_id->value());
|
||||||
|
if (!$lane->exists()) {
|
||||||
|
$response->error('The default lane configured for the plate scanner no longer exists', 404);
|
||||||
|
}
|
||||||
|
if ((int)$lane->department->value() !== (int)$plateScanner->department_id->value()) {
|
||||||
|
$response->error('The default lane does not belong to the plate scanner department', 403);
|
||||||
|
}
|
||||||
|
return (int)$lane->id;
|
||||||
|
}
|
||||||
|
|
||||||
$lanes = (new department_lanes_o())->getDepartmentLanes((int)$plateScanner->department_id->value());
|
$lanes = (new department_lanes_o())->getDepartmentLanes((int)$plateScanner->department_id->value());
|
||||||
if (count($lanes) === 1) {
|
if (count($lanes) === 1) {
|
||||||
return (int)$lanes[0]->id;
|
return (int)$lanes[0]->id;
|
||||||
|
|||||||
@@ -34,7 +34,11 @@ class plateScannersRoute
|
|||||||
'name',
|
'name',
|
||||||
'notes'
|
'notes'
|
||||||
])
|
])
|
||||||
->listObjectsWithPaginationIfSet()
|
->listObjectsWithPaginationIfSet(
|
||||||
|
static function (array $scanner): array {
|
||||||
|
return (new plate_scanners_o())->select((int)$scanner['id'])->asArray();
|
||||||
|
}
|
||||||
|
)
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
// Log the incident
|
// Log the incident
|
||||||
@@ -68,12 +72,19 @@ class plateScannersRoute
|
|||||||
if (!isset($data['notes'])) {
|
if (!isset($data['notes'])) {
|
||||||
$response->error('Notes is required', 400);
|
$response->error('Notes is required', 400);
|
||||||
}
|
}
|
||||||
|
$laneId = array_key_exists('lane_id', $data) && $data['lane_id'] !== null
|
||||||
|
? (int)$data['lane_id']
|
||||||
|
: null;
|
||||||
// Add the number plate scanner
|
// Add the number plate scanner
|
||||||
(new plate_scanners_o())->add($data['department_id'], $data['name'], $data['notes']);
|
$scanner = new plate_scanners_o();
|
||||||
|
$scanner->add((int)$data['department_id'], (string)$data['name'], (string)$data['notes'], $laneId);
|
||||||
// Log the incident
|
// Log the incident
|
||||||
(new logs_o())->add('numberplatescanners', 'global', 1, $user->id, 'ADD_NUMBER_PLATE_SCANNER', 'Successfully added a number plate scanner');
|
(new logs_o())->add('numberplatescanners', 'global', 1, $user->id, 'ADD_NUMBER_PLATE_SCANNER', 'Successfully added a number plate scanner');
|
||||||
// Return a success message
|
// Return a success message
|
||||||
$response->success(['message' => 'Number plate scanner added']);
|
$response->success([
|
||||||
|
'message' => 'Number plate scanner added',
|
||||||
|
'scanner' => $scanner->asArray(),
|
||||||
|
]);
|
||||||
} else {
|
} else {
|
||||||
// Log the incident
|
// Log the incident
|
||||||
(new logs_o())->add('numberplatescanners', 'global', 1, 0, 'ADD_NUMBER_PLATE_SCANNER', 'No user found, or invalid session');
|
(new logs_o())->add('numberplatescanners', 'global', 1, 0, 'ADD_NUMBER_PLATE_SCANNER', 'No user found, or invalid session');
|
||||||
@@ -109,12 +120,25 @@ class plateScannersRoute
|
|||||||
if (!isset($data['notes'])) {
|
if (!isset($data['notes'])) {
|
||||||
$response->error('Notes is required', 400);
|
$response->error('Notes is required', 400);
|
||||||
}
|
}
|
||||||
|
$laneIdProvided = array_key_exists('lane_id', $data);
|
||||||
|
$laneId = $laneIdProvided && $data['lane_id'] !== null ? (int)$data['lane_id'] : null;
|
||||||
// Edit the number plate scanner
|
// Edit the number plate scanner
|
||||||
(new plate_scanners_o())->edit($data['id'], $data['department_id'], $data['name'], $data['notes']);
|
$scanner = new plate_scanners_o();
|
||||||
|
$scanner->edit(
|
||||||
|
(int)$data['id'],
|
||||||
|
(int)$data['department_id'],
|
||||||
|
(string)$data['name'],
|
||||||
|
(string)$data['notes'],
|
||||||
|
$laneId,
|
||||||
|
$laneIdProvided
|
||||||
|
);
|
||||||
// Log the incident
|
// Log the incident
|
||||||
(new logs_o())->add('numberplatescanners', 'global', 1, $user->id, 'EDIT_NUMBER_PLATE_SCANNER', 'Successfully edited a number plate scanner');
|
(new logs_o())->add('numberplatescanners', 'global', 1, $user->id, 'EDIT_NUMBER_PLATE_SCANNER', 'Successfully edited a number plate scanner');
|
||||||
// Return a success message
|
// Return a success message
|
||||||
$response->success(['message' => 'Number plate scanner edited']);
|
$response->success([
|
||||||
|
'message' => 'Number plate scanner edited',
|
||||||
|
'scanner' => $scanner->asArray(),
|
||||||
|
]);
|
||||||
} else {
|
} else {
|
||||||
// Log the incident
|
// Log the incident
|
||||||
(new logs_o())->add('numberplatescanners', 'global', 1, 0, 'EDIT_NUMBER_PLATE_SCANNER', 'No user found, or invalid session');
|
(new logs_o())->add('numberplatescanners', 'global', 1, 0, 'EDIT_NUMBER_PLATE_SCANNER', 'No user found, or invalid session');
|
||||||
@@ -127,6 +151,35 @@ class plateScannersRoute
|
|||||||
]
|
]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
$this->post('/numberplatescanners/{id}/rotate-key', function () {
|
||||||
|
global $response;
|
||||||
|
$this->requirePermission('edit_number_plate_scanner');
|
||||||
|
$user = (new authentication())->get_user();
|
||||||
|
|
||||||
|
if (!$user) {
|
||||||
|
(new logs_o())->add('numberplatescanners', 'global', 1, 0, 'ROTATE_NUMBER_PLATE_SCANNER_KEY', 'No user found, or invalid session');
|
||||||
|
$response->error('Invalid session', 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
$scannerId = (int)$this->fromRoute('id');
|
||||||
|
self::requireParameterIntPositive($scannerId, 'id');
|
||||||
|
$scanner = (new plate_scanners_o())->select($scannerId);
|
||||||
|
if (!$scanner->exists()) {
|
||||||
|
$response->error('Number plate scanner not found', 404);
|
||||||
|
}
|
||||||
|
|
||||||
|
self::requireDepartmentAccess((int)$scanner->department_id->value());
|
||||||
|
$rotatedScanner = (new plate_scanners_o())->rotateApiKey($scannerId);
|
||||||
|
(new logs_o())->add('numberplatescanners', 'global', 1, $user->id, 'ROTATE_NUMBER_PLATE_SCANNER_KEY', 'Successfully rotated a number plate scanner API key');
|
||||||
|
$response->success([
|
||||||
|
'message' => 'Number plate scanner API key rotated',
|
||||||
|
'scanner' => $rotatedScanner,
|
||||||
|
'api_key' => (string)$rotatedScanner['api_key'],
|
||||||
|
]);
|
||||||
|
}, [
|
||||||
|
'edit_number_plate_scanner' => 'Rotate a number plate scanner API key',
|
||||||
|
]);
|
||||||
|
|
||||||
self::get('/department/numberplatescanners', function () {
|
self::get('/department/numberplatescanners', function () {
|
||||||
// Require the user to be logged in
|
// Require the user to be logged in
|
||||||
global $response;
|
global $response;
|
||||||
@@ -152,12 +205,14 @@ class plateScannersRoute
|
|||||||
'department_id' => (int)self::getParameter('id')
|
'department_id' => (int)self::getParameter('id')
|
||||||
], [
|
], [
|
||||||
'id',
|
'id',
|
||||||
|
'lane_id',
|
||||||
'name',
|
'name',
|
||||||
'notes'
|
'notes'
|
||||||
]);
|
]);
|
||||||
// Parse the result
|
// Parse the result
|
||||||
foreach ( $result as $key => $value ) {
|
foreach ( $result as $key => $value ) {
|
||||||
$result[$key]['id'] = (int)$value['id'];
|
$result[$key]['id'] = (int)$value['id'];
|
||||||
|
$result[$key]['lane_id'] = $value['lane_id'] === null ? null : (int)$value['lane_id'];
|
||||||
}
|
}
|
||||||
// Return the list of plate scanners
|
// Return the list of plate scanners
|
||||||
$response->success(
|
$response->success(
|
||||||
@@ -176,4 +231,4 @@ class plateScannersRoute
|
|||||||
]
|
]
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,79 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
app_require('classes/edge_gateway_manager.php');
|
||||||
|
app_require('classes/object_property.php');
|
||||||
|
app_require('objects/department_gates_o.php');
|
||||||
|
|
||||||
|
use classes\edge_gateway_manager;
|
||||||
|
use classes\object_property;
|
||||||
|
use objects\department_gates_o;
|
||||||
|
|
||||||
|
final class DepartmentGatesRelayManagerFake extends edge_gateway_manager
|
||||||
|
{
|
||||||
|
public array $switchCalls = [];
|
||||||
|
|
||||||
|
public function __construct()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public function dispatchRelaySwitch(int $departmentId, string $logicalRelayId, bool $on): array
|
||||||
|
{
|
||||||
|
$this->switchCalls[] = [
|
||||||
|
'department_id' => $departmentId,
|
||||||
|
'relay_id' => $logicalRelayId,
|
||||||
|
'on' => $on,
|
||||||
|
];
|
||||||
|
|
||||||
|
return [
|
||||||
|
'relay_id' => $logicalRelayId,
|
||||||
|
'online' => true,
|
||||||
|
'on' => $on,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final class DepartmentGatesRelayOpenHarness extends department_gates_o
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
array $config,
|
||||||
|
int $departmentId,
|
||||||
|
private readonly DepartmentGatesRelayManagerFake $manager,
|
||||||
|
) {
|
||||||
|
$this->id = 1001;
|
||||||
|
$departmentProperty = new object_property('department_gates', -1, 'department', 'int');
|
||||||
|
$departmentProperty->set($departmentId);
|
||||||
|
$this->department = $departmentProperty;
|
||||||
|
|
||||||
|
$configProperty = new object_property('department_gates', -1, 'config', 'json');
|
||||||
|
$configProperty->set($config);
|
||||||
|
$this->config = $configProperty;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function requireSelected(): void
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function resolveEdgeGatewayManager(): edge_gateway_manager
|
||||||
|
{
|
||||||
|
return $this->manager;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
it('dispatches relay-backed gates through the edge gateway relay manager', function (): void {
|
||||||
|
$manager = new DepartmentGatesRelayManagerFake();
|
||||||
|
$gate = new DepartmentGatesRelayOpenHarness([
|
||||||
|
'type' => 'RELAY',
|
||||||
|
'relay_id' => 'ENTRY-GATE-1',
|
||||||
|
'pulse_seconds' => 0,
|
||||||
|
], 17, $manager);
|
||||||
|
|
||||||
|
$gate->openGate();
|
||||||
|
|
||||||
|
expect($manager->switchCalls)->toBe([
|
||||||
|
[
|
||||||
|
'department_id' => 17,
|
||||||
|
'relay_id' => 'ENTRY-GATE-1',
|
||||||
|
'on' => true,
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
});
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
app_require('classes/department_gate_config.php');
|
||||||
|
|
||||||
|
use classes\department_gate_config;
|
||||||
|
|
||||||
|
it('validates relay gate configs and keeps relay-specific fields in the payload', function (): void {
|
||||||
|
$config = new department_gate_config([
|
||||||
|
'type' => 'RELAY',
|
||||||
|
'relay_id' => 'ENTRY-GATE-1',
|
||||||
|
'pulse_seconds' => 0,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$config->validate();
|
||||||
|
|
||||||
|
expect($config->toArray())->toMatchArray([
|
||||||
|
'type' => 'RELAY',
|
||||||
|
'relay_id' => 'ENTRY-GATE-1',
|
||||||
|
'pulse_seconds' => 0,
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('rejects relay gate configs without a logical relay id', function (): void {
|
||||||
|
$config = new department_gate_config([
|
||||||
|
'type' => 'RELAY',
|
||||||
|
]);
|
||||||
|
|
||||||
|
expect(fn() => $config->validate())
|
||||||
|
->toThrow(Exception::class, 'relay_id is required for RELAY gate type');
|
||||||
|
});
|
||||||
+17
@@ -0,0 +1,17 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
it('defines the department hardware workspace service payload surface', function (): void {
|
||||||
|
$service = file_get_contents(app_path('modules/edgegateway/classes/edge_gateway_department_workspace_service.php'));
|
||||||
|
|
||||||
|
expect($service)->not->toBeFalse();
|
||||||
|
expect($service)->toContain('class edge_gateway_department_workspace_service');
|
||||||
|
expect($service)->toContain("'summary' => \$summary");
|
||||||
|
expect($service)->toContain("'gateways' => \$includeGateways ? \$gateways : []");
|
||||||
|
expect($service)->toContain("'lanes' => \$lanes");
|
||||||
|
expect($service)->toContain("'self_serve' => \$selfServe");
|
||||||
|
expect($service)->toContain("'gates' => \$gates");
|
||||||
|
expect($service)->toContain("'scanners' => \$scanners");
|
||||||
|
expect($service)->toContain("'issues' => \$issues");
|
||||||
|
expect($service)->toContain("'actions' => \$actions");
|
||||||
|
expect($service)->toContain("'consumer_contexts'");
|
||||||
|
});
|
||||||
@@ -5,6 +5,8 @@ it('registers module-scoped edge gateway operator routes', function (): void {
|
|||||||
|
|
||||||
expect($route)->not->toBeFalse();
|
expect($route)->not->toBeFalse();
|
||||||
expect($route)->toContain("'/modules/edge-gateways'");
|
expect($route)->toContain("'/modules/edge-gateways'");
|
||||||
|
expect($route)->toContain("'/modules/edge-gateways/workspace/departments'");
|
||||||
|
expect($route)->toContain("'/modules/edge-gateways/workspace/departments/{id}'");
|
||||||
expect($route)->toContain("'/modules/edge-gateways/{id}'");
|
expect($route)->toContain("'/modules/edge-gateways/{id}'");
|
||||||
expect($route)->toContain("'/modules/edge-gateways/install-token'");
|
expect($route)->toContain("'/modules/edge-gateways/install-token'");
|
||||||
expect($route)->toContain("'/modules/edge-gateways/install-token/{id}/status'");
|
expect($route)->toContain("'/modules/edge-gateways/install-token/{id}/status'");
|
||||||
|
|||||||
@@ -0,0 +1,23 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
it('adds lane-aware scanner management and a dedicated rotate-key action', function (): void {
|
||||||
|
$route = file_get_contents(app_path('routes/plateScannersRoute.php'));
|
||||||
|
$scannerObject = file_get_contents(app_path('objects/plate_scanners_o.php'));
|
||||||
|
|
||||||
|
expect($route)->not->toBeFalse();
|
||||||
|
expect($route)->toContain("'/numberplatescanners/{id}/rotate-key'");
|
||||||
|
expect($route)->toContain("'lane_id'");
|
||||||
|
|
||||||
|
expect($scannerObject)->not->toBeFalse();
|
||||||
|
expect($scannerObject)->toContain('public object_property $lane_id;');
|
||||||
|
expect($scannerObject)->toContain('public function rotateApiKey(int $id): array');
|
||||||
|
expect($scannerObject)->toContain('ADD COLUMN `lane_id` INT NULL AFTER `department_id`');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('uses the scanner default lane before requiring an explicit lane_id in machine button webhooks', function (): void {
|
||||||
|
$route = file_get_contents(app_path('routes/machineButtonPressRoute.php'));
|
||||||
|
|
||||||
|
expect($route)->not->toBeFalse();
|
||||||
|
expect($route)->toContain('$plateScanner->lane_id->value()');
|
||||||
|
expect($route)->toContain('default lane configured for the plate scanner');
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user