feat(manager): integrate forecast controls and merge V4 development
Tests / test (push) Failing after 49s

Merge the V4 development history with current manager, SDL and setpoint fixes. Retain guarded trial behavior and consolidate controls in Prognose / Forecast. Exclude the unverified accounting-evidence change; no runtime deployment or new dispatch permission.

Validated: 357 PHPUnit tests / 1758 assertions, 150 PHP syntax checks, 310 backend tests, 16 portal tests and isolated UI/receiver checks. develop and beta publication explicitly approved by Daniel Haefliger.
This commit is contained in:
dh
2026-10-06 14:22:16 +00:00
207 changed files with 20340 additions and 11 deletions
+116
View File
@@ -0,0 +1,116 @@
<?php
declare(strict_types=1);
require_once __DIR__ . '/../../libs/ManagerPrognoseFormular.php';
use Belevo\EnelixEMS\ManagerPrognoseFormular;
$count = 0;
function check(bool $ok, string $name): void
{
global $count;
if (!$ok) {
throw new RuntimeException('FAIL ' . $name);
}
$count++;
echo 'PASS ' . $name . PHP_EOL;
}
function fields(array $form): array
{
foreach ($form['elements'] as $element) {
if (($element['name'] ?? '') === 'PrognoseForecast') {
$fields = [];
foreach ($element['items'] as $field) {
if (isset($field['name'])) {
if (isset($fields[$field['name']])) {
throw new RuntimeException('Duplicate field: ' . $field['name']);
}
$fields[$field['name']] = $field;
}
}
return $fields;
}
}
throw new RuntimeException('Forecast panel missing');
}
$base = json_decode(file_get_contents(__DIR__ . '/../../Manager/form.json'), true, 512, JSON_THROW_ON_ERROR);
$state = [
'prognoseAktiv' => true, 'lizenziert' => true, 'senderAktiv' => true, 'empfangAktiv' => true,
'messdatenAktiv' => true, 'legacyAktiv' => false, 'managerAktiv' => true, 'testErlaubt' => true,
'aktivtestErlaubt' => true, 'watchdogVerzichtErlaubt' => true, 'aktivtestAktiv' => false,
'ende' => 1791387487, 'jetzt' => 1791300000,
];
$form = ManagerPrognoseFormular::integrieren($base, $state);
$f = fields($form);
check(count($form['elements']) === count($base['elements']), 'no additional panel');
check(!str_contains(json_encode($form), 'V4 Planempfang'), 'separate V4 tab removed');
foreach (['PrognoseAktiv', 'NetzfahrplanAktiv', 'PrognoseSendeintervall', 'NetzfahrplanV4EmpfangAktiv',
'PrognoseNetzfahrplanStart', 'PrognoseNetzfahrplanStop', 'PrognoseNetzfahrplanPruefen',
'PrognoseLizenzVerwalten', 'PrognoseNetzfahrplanStatus'] as $name) {
check(isset($f[$name]), 'one forecast panel contains ' . $name);
}
foreach ($base['elements'] as $index => $element) {
if (($element['name'] ?? '') !== 'PrognoseForecast') {
check($form['elements'][$index] === $element, 'unrelated panel unchanged: ' . $element['caption']);
}
}
foreach (['PrognoseAktiv', 'PrognoseSendeintervall', 'PrognoseLizenzInformation'] as $name) {
check($f[$name] === fields($base)[$name], 'existing forecast field unchanged: ' . $name);
}
check($f['NetzfahrplanAktiv']['visible'] === false, 'inactive legacy control hidden for configured V4');
check($f['PrognoseNetzfahrplanStart']['enabled'], 'licensed existing consent enables explicit start');
check(str_contains($f['PrognoseNetzfahrplanStart']['onClick'], "'FormNetzfahrplanSchalten', true"), 'start uses guarded manager action');
check(str_contains($f['PrognoseNetzfahrplanStop']['onClick'], "'FormNetzfahrplanSchalten', false"), 'stop uses existing stop path');
check(str_contains($f['PrognoseNetzfahrplanPruefen']['onClick'], "'NetzfahrplanV4Empfangen'"), 'plan check uses existing throttled receiver');
check(str_contains($f['PrognoseLizenzVerwalten']['onClick'], 'https://license.enelix.ch'), 'existing license portal retained');
check(str_contains($f['PrognoseNetzfahrplanStatus']['caption'], '07.10.2026 15:38:07 UTC'), 'original expiry displayed without extension');
check(str_contains($f['PrognoseNetzfahrplanStatus']['caption'], 'Kein freigegebener Dauerbetrieb'), 'test limitation remains explicit');
foreach (['prognoseAktiv', 'lizenziert', 'senderAktiv', 'empfangAktiv', 'managerAktiv',
'testErlaubt', 'aktivtestErlaubt', 'watchdogVerzichtErlaubt'] as $key) {
$blocked = array_replace($state, [$key => false]);
$b = fields(ManagerPrognoseFormular::integrieren($base, $blocked));
check(!$b['PrognoseNetzfahrplanStart']['enabled'], 'start blocked without ' . $key);
check($b['PrognoseNetzfahrplanStop']['enabled'], 'stop remains possible without ' . $key);
}
foreach ([['legacyAktiv' => true], ['aktivtestAktiv' => true], ['ende' => 1791300000], ['ende' => -1]] as $change) {
check(!ManagerPrognoseFormular::startErlaubt(array_replace($state, $change)), 'conflict, replay or expiry blocks start: ' . json_encode($change));
}
check(!ManagerPrognoseFormular::startErlaubt([]), 'missing state fails closed');
check(ManagerPrognoseFormular::startErlaubt(array_replace($state, ['ende' => 0])), 'first start still uses existing backend consent procedure');
$b = fields(ManagerPrognoseFormular::integrieren($base, array_replace($state, ['legacyAktiv' => true, 'lizenziert' => false])));
check($b['NetzfahrplanAktiv']['visible'] && $b['NetzfahrplanAktiv']['enabled'], 'active legacy controller can be disabled despite lost license');
check(str_contains($b['PrognoseNetzfahrplanStatus']['caption'], 'konkurrier') === false
&& str_contains($b['PrognoseNetzfahrplanStatus']['caption'], 'gesperrt'), 'controller conflict is visible');
$b = fields(ManagerPrognoseFormular::integrieren($base, array_replace($state, ['lizenziert' => false])));
check($b['NetzfahrplanV4EmpfangAktiv']['enabled'], 'active receiver remains switchable off without license');
$b = fields(ManagerPrognoseFormular::integrieren($base, array_replace($state, ['lizenziert' => false, 'empfangAktiv' => false])));
check(!$b['NetzfahrplanV4EmpfangAktiv']['enabled'], 'unlicensed receiver cannot be enabled through form');
$b = fields(ManagerPrognoseFormular::integrieren($base, array_replace($state, [
'senderAktiv' => false, 'empfangAktiv' => false, 'messdatenAktiv' => false,
])));
check($b['NetzfahrplanAktiv'] === fields($base)['NetzfahrplanAktiv'], 'non-migrated installation retains legacy control');
$missing = $base;
$missing['elements'] = array_values(array_filter($missing['elements'], fn($e) => ($e['name'] ?? '') !== 'PrognoseForecast'));
try {
ManagerPrognoseFormular::integrieren($missing, $state);
check(false, 'missing forecast anchor rejected');
} catch (LogicException $e) {
check(true, 'missing forecast anchor rejected');
}
$duplicate = $base;
foreach ($base['elements'] as $element) {
if (($element['name'] ?? '') === 'PrognoseForecast') {
$duplicate['elements'][] = $element;
}
}
try {
ManagerPrognoseFormular::integrieren($duplicate, $state);
check(false, 'duplicate forecast anchor rejected');
} catch (LogicException $e) {
check(true, 'duplicate forecast anchor rejected');
}
check($form === ManagerPrognoseFormular::integrieren($base, $state), 'repeated rendering is deterministic and does not modify the base form');
echo "Total: $count checks. Form generation only; no IPS or device calls.\n";