Fix ingress logging and dashboard cache navigation
This commit is contained in:
2
.gitignore
vendored
2
.gitignore
vendored
@@ -11,3 +11,5 @@ __pycache__/
|
|||||||
.env
|
.env
|
||||||
.env.local
|
.env.local
|
||||||
.env.*
|
.env.*
|
||||||
|
/.actuator_store/
|
||||||
|
/MagicMock/
|
||||||
|
|||||||
@@ -1,5 +1,14 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## 1.5.4 - 2026-06-18
|
||||||
|
- Add-on-Start vertraut Ingress-Proxy-Headern nicht mehr blind. Uvicorn loggt
|
||||||
|
damit den direkten Docker-/Ingress-Peer statt LAN-IPs aus `X-Forwarded-For`.
|
||||||
|
- Dashboard behält bereits geladene System-, Lern- und Discovery-Daten beim
|
||||||
|
Wechseln der Ansichten und aktualisiert sie nur im Hintergrund.
|
||||||
|
- Details sind kein eigener Menüpunkt mehr, sondern gehören zum ausgewählten
|
||||||
|
Aktor aus der Lernübersicht. Bereits geöffnete Details bleiben sichtbar und
|
||||||
|
laden nur bei expliziter Aktualisierung neu.
|
||||||
|
|
||||||
## 1.2.0 - 2026-06-17
|
## 1.2.0 - 2026-06-17
|
||||||
- Automatische Sensor-Gewichtungsanpassung aus Nutzerfeedback:
|
- Automatische Sensor-Gewichtungsanpassung aus Nutzerfeedback:
|
||||||
korrektes Feedback staerkt aktuelle Kontextsignale leicht, falsches Feedback
|
korrektes Feedback staerkt aktuelle Kontextsignale leicht, falsches Feedback
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
name: SillyHome Next
|
name: SillyHome Next
|
||||||
version: "1.5.3"
|
version: "1.5.4"
|
||||||
slug: sillyhome_next
|
slug: sillyhome_next
|
||||||
description: Lernt automatisch aus deinem Verhalten und steuert freigegebene Aktoren
|
description: Lernt automatisch aus deinem Verhalten und steuert freigegebene Aktoren
|
||||||
url: http://192.168.6.31:3000/pino/sillyhome-next
|
url: http://192.168.6.31:3000/pino/sillyhome-next
|
||||||
|
|||||||
@@ -21,5 +21,4 @@ if [ -f /data/options.json ]; then
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
mkdir -p "$SILLYHOME_MODEL_STORE" "$SILLYHOME_AUTOMATION_STORE" "$SILLYHOME_ACTUATOR_STORE"
|
mkdir -p "$SILLYHOME_MODEL_STORE" "$SILLYHOME_AUTOMATION_STORE" "$SILLYHOME_ACTUATOR_STORE"
|
||||||
exec uvicorn app.main:app --app-dir /app --host 0.0.0.0 --port 8000 \
|
exec uvicorn app.main:app --app-dir /app --host 0.0.0.0 --port 8000
|
||||||
--proxy-headers --forwarded-allow-ips='*'
|
|
||||||
|
|||||||
@@ -117,7 +117,7 @@ async def lifespan(app: FastAPI) -> AsyncIterator[None]:
|
|||||||
app = FastAPI(
|
app = FastAPI(
|
||||||
title="SillyHome Next API",
|
title="SillyHome Next API",
|
||||||
description="Lokales Smart-Home-Intelligenzsystem für Home Assistant.",
|
description="Lokales Smart-Home-Intelligenzsystem für Home Assistant.",
|
||||||
version="1.5.3",
|
version="1.5.4",
|
||||||
lifespan=lifespan,
|
lifespan=lifespan,
|
||||||
)
|
)
|
||||||
app.state.settings = load_settings()
|
app.state.settings = load_settings()
|
||||||
|
|||||||
@@ -154,7 +154,6 @@
|
|||||||
<select id="section-jump" onchange="showView(this.value)">
|
<select id="section-jump" onchange="showView(this.value)">
|
||||||
<option value="status-section">Startseite / System</option>
|
<option value="status-section">Startseite / System</option>
|
||||||
<option value="observed">Lernen</option>
|
<option value="observed">Lernen</option>
|
||||||
<option value="detail">Details</option>
|
|
||||||
<option value="choose">Discovery & Einrichtung</option>
|
<option value="choose">Discovery & Einrichtung</option>
|
||||||
<option value="settings">Einstellungen</option>
|
<option value="settings">Einstellungen</option>
|
||||||
<option value="guide">Ablauf</option>
|
<option value="guide">Ablauf</option>
|
||||||
@@ -309,7 +308,12 @@ let manualContextState = {options: [], selected: new Set()};
|
|||||||
let cachedActuators = null;
|
let cachedActuators = null;
|
||||||
let cachedEntities = null;
|
let cachedEntities = null;
|
||||||
let cachedDiscovery = null;
|
let cachedDiscovery = null;
|
||||||
|
let cachedSystemOverview = null;
|
||||||
|
let cachedDashboardOverview = null;
|
||||||
|
let cachedDetailHtml = new Map();
|
||||||
let discoveryLoadPromise = null;
|
let discoveryLoadPromise = null;
|
||||||
|
let overviewLoadPromise = null;
|
||||||
|
let systemLoadPromise = null;
|
||||||
let currentSensorWeightGroups = [];
|
let currentSensorWeightGroups = [];
|
||||||
let visibleActuatorLimit = 24;
|
let visibleActuatorLimit = 24;
|
||||||
const ACTUATOR_RESULT_LIMIT = 50;
|
const ACTUATOR_RESULT_LIMIT = 50;
|
||||||
@@ -465,14 +469,29 @@ function jumpToSection(target) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function showView(viewId) {
|
function showView(viewId) {
|
||||||
|
if (viewId === "detail" && !currentActuatorId) {
|
||||||
|
viewId = "observed";
|
||||||
|
}
|
||||||
for (const section of document.querySelectorAll(".app-view")) {
|
for (const section of document.querySelectorAll(".app-view")) {
|
||||||
section.classList.toggle("active", section.id === viewId);
|
section.classList.toggle("active", section.id === viewId);
|
||||||
}
|
}
|
||||||
localStorage.setItem("sillyhome.ui.view", viewId);
|
localStorage.setItem("sillyhome.ui.view", viewId);
|
||||||
if (viewId === "status-section") {
|
if (viewId === "status-section") {
|
||||||
void loadSystemOverview();
|
if (cachedSystemOverview) {
|
||||||
|
renderDashboardStatus(cachedSystemOverview);
|
||||||
|
refreshSystemOverviewInBackground();
|
||||||
|
} else {
|
||||||
|
void loadSystemOverview();
|
||||||
|
}
|
||||||
} else if (viewId === "observed") {
|
} else if (viewId === "observed") {
|
||||||
void loadOverview();
|
if (cachedActuators) {
|
||||||
|
renderConfiguredActuators();
|
||||||
|
refreshOverviewInBackground();
|
||||||
|
} else {
|
||||||
|
void loadOverview();
|
||||||
|
}
|
||||||
|
} else if (viewId === "choose") {
|
||||||
|
if (cachedDiscovery) renderActuatorDiscovery();
|
||||||
} else if (viewId === "settings") {
|
} else if (viewId === "settings") {
|
||||||
syncSettingsView();
|
syncSettingsView();
|
||||||
}
|
}
|
||||||
@@ -484,8 +503,10 @@ function setLanguage(language) {
|
|||||||
localStorage.setItem("sillyhome.ui.language", uiLang);
|
localStorage.setItem("sillyhome.ui.language", uiLang);
|
||||||
syncSettingsView();
|
syncSettingsView();
|
||||||
if (cachedActuators) renderConfiguredActuators();
|
if (cachedActuators) renderConfiguredActuators();
|
||||||
void loadSystemOverview();
|
if (cachedSystemOverview) renderDashboardStatus(cachedSystemOverview);
|
||||||
if (currentActuatorId) void showActuator(currentActuatorId);
|
if (currentActuatorId && cachedDetailHtml.has(currentActuatorId)) {
|
||||||
|
document.getElementById("actuator-detail").innerHTML = cachedDetailHtml.get(currentActuatorId);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function syncSettingsView() {
|
function syncSettingsView() {
|
||||||
@@ -514,6 +535,9 @@ function invalidateDashboardCache() {
|
|||||||
cachedActuators = null;
|
cachedActuators = null;
|
||||||
cachedEntities = null;
|
cachedEntities = null;
|
||||||
cachedDiscovery = null;
|
cachedDiscovery = null;
|
||||||
|
cachedDashboardOverview = null;
|
||||||
|
cachedSystemOverview = null;
|
||||||
|
cachedDetailHtml.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
async function api(path, options = {}) {
|
async function api(path, options = {}) {
|
||||||
@@ -654,13 +678,24 @@ function optionGroups(entities, selectedIds = new Set()) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function loadOverview() {
|
async function loadOverview() {
|
||||||
|
if (overviewLoadPromise) return overviewLoadPromise;
|
||||||
|
overviewLoadPromise = doLoadOverview().finally(() => {
|
||||||
|
overviewLoadPromise = null;
|
||||||
|
});
|
||||||
|
return overviewLoadPromise;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function doLoadOverview() {
|
||||||
const startedAt = performance.now();
|
const startedAt = performance.now();
|
||||||
const budget = document.getElementById("load-budget");
|
const budget = document.getElementById("load-budget");
|
||||||
if (budget) budget.textContent = "Startdaten laden ...";
|
if (budget) budget.textContent = "Startdaten laden ...";
|
||||||
document.getElementById("configured-actuators").innerHTML = "<p class='muted'>Beobachtete Geräte werden geladen ...</p>";
|
if (!cachedActuators) {
|
||||||
|
document.getElementById("configured-actuators").innerHTML = "<p class='muted'>Beobachtete Geräte werden geladen ...</p>";
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
const dashboard = await api("v1/actuators/dashboard/start");
|
const dashboard = await api("v1/actuators/dashboard/start");
|
||||||
dashboard._load_elapsed_ms = Math.round(performance.now() - startedAt);
|
dashboard._load_elapsed_ms = Math.round(performance.now() - startedAt);
|
||||||
|
cachedDashboardOverview = dashboard;
|
||||||
cachedActuators = dashboard.actuators || [];
|
cachedActuators = dashboard.actuators || [];
|
||||||
cachedEntities = [];
|
cachedEntities = [];
|
||||||
renderDashboardStatus(dashboard);
|
renderDashboardStatus(dashboard);
|
||||||
@@ -685,12 +720,21 @@ async function loadOverview() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function loadSystemOverview() {
|
async function loadSystemOverview() {
|
||||||
|
if (systemLoadPromise) return systemLoadPromise;
|
||||||
|
systemLoadPromise = doLoadSystemOverview().finally(() => {
|
||||||
|
systemLoadPromise = null;
|
||||||
|
});
|
||||||
|
return systemLoadPromise;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function doLoadSystemOverview() {
|
||||||
const startedAt = performance.now();
|
const startedAt = performance.now();
|
||||||
const budget = document.getElementById("load-budget");
|
const budget = document.getElementById("load-budget");
|
||||||
if (budget) budget.textContent = "Systemübersicht lädt ...";
|
if (budget) budget.textContent = "Systemübersicht lädt ...";
|
||||||
try {
|
try {
|
||||||
const dashboard = await api("v1/actuators/dashboard/system");
|
const dashboard = await api("v1/actuators/dashboard/system");
|
||||||
dashboard._load_elapsed_ms = Math.round(performance.now() - startedAt);
|
dashboard._load_elapsed_ms = Math.round(performance.now() - startedAt);
|
||||||
|
cachedSystemOverview = dashboard;
|
||||||
cachedActuators = dashboard.actuators || cachedActuators;
|
cachedActuators = dashboard.actuators || cachedActuators;
|
||||||
renderDashboardStatus(dashboard);
|
renderDashboardStatus(dashboard);
|
||||||
if (budget) {
|
if (budget) {
|
||||||
@@ -706,6 +750,22 @@ async function loadSystemOverview() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function refreshOverviewInBackground() {
|
||||||
|
if (!overviewLoadPromise) {
|
||||||
|
overviewLoadPromise = doLoadOverview().finally(() => {
|
||||||
|
overviewLoadPromise = null;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function refreshSystemOverviewInBackground() {
|
||||||
|
if (!systemLoadPromise) {
|
||||||
|
systemLoadPromise = doLoadSystemOverview().finally(() => {
|
||||||
|
systemLoadPromise = null;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function scheduleDashboardExtras() {
|
function scheduleDashboardExtras() {
|
||||||
const run = () => {
|
const run = () => {
|
||||||
void loadDashboardExtras();
|
void loadDashboardExtras();
|
||||||
@@ -1131,10 +1191,20 @@ async function showActuator(actuatorId, evaluationMessage = "") {
|
|||||||
for (const section of document.querySelectorAll(".app-view")) {
|
for (const section of document.querySelectorAll(".app-view")) {
|
||||||
section.classList.toggle("active", section.id === "detail");
|
section.classList.toggle("active", section.id === "detail");
|
||||||
}
|
}
|
||||||
document.getElementById("section-jump").value = "detail";
|
document.getElementById("section-jump").value = "observed";
|
||||||
localStorage.setItem("sillyhome.ui.view", "detail");
|
localStorage.setItem("sillyhome.ui.view", "detail");
|
||||||
const box = document.getElementById("actuator-detail");
|
const box = document.getElementById("actuator-detail");
|
||||||
renderActuatorDetailShell(actuatorId);
|
if (!evaluationMessage && cachedDetailHtml.has(actuatorId)) {
|
||||||
|
box.innerHTML = cachedDetailHtml.get(actuatorId);
|
||||||
|
document.getElementById("detail").scrollIntoView({behavior: "smooth", block: "start"});
|
||||||
|
renderConfiguredActuators();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (cachedDetailHtml.has(actuatorId)) {
|
||||||
|
box.innerHTML = cachedDetailHtml.get(actuatorId);
|
||||||
|
} else {
|
||||||
|
renderActuatorDetailShell(actuatorId);
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
const record = await api(`v1/actuators/${encodeURIComponent(actuatorId)}/detail`);
|
const record = await api(`v1/actuators/${encodeURIComponent(actuatorId)}/detail`);
|
||||||
contextOptions = [];
|
contextOptions = [];
|
||||||
@@ -1403,13 +1473,13 @@ async function showActuator(actuatorId, evaluationMessage = "") {
|
|||||||
<button class="secondary compact" onclick="setRelatedAutomation('${escapeHtml(record.actuator_entity_id)}', '${escapeHtml(automation.entity_id)}', ${automation.enabled ? "false" : "true"})">${automation.enabled ? "Pausieren" : "Fortsetzen"}</button>
|
<button class="secondary compact" onclick="setRelatedAutomation('${escapeHtml(record.actuator_entity_id)}', '${escapeHtml(automation.entity_id)}', ${automation.enabled ? "false" : "true"})">${automation.enabled ? "Pausieren" : "Fortsetzen"}</button>
|
||||||
</li>`).join("")}</ul>`
|
</li>`).join("")}</ul>`
|
||||||
: "<p class='muted'>Keine eindeutig passende HA-Automation gefunden.</p>";
|
: "<p class='muted'>Keine eindeutig passende HA-Automation gefunden.</p>";
|
||||||
box.innerHTML = `
|
const detailHtml = `
|
||||||
<div class="detail-header">
|
<div class="detail-header">
|
||||||
<div>
|
<div>
|
||||||
<h3>${escapeHtml(record.actuator_entity_id)}</h3>
|
<h3>${escapeHtml(record.actuator_entity_id)}</h3>
|
||||||
<p class="muted">Alle wichtigen Aktionen für dieses Gerät.</p>
|
<p class="muted">Alle wichtigen Aktionen für dieses Gerät.</p>
|
||||||
</div>
|
</div>
|
||||||
<button class="secondary compact" onclick="loadOverview()">Alles aktualisieren</button>
|
<button class="secondary compact" onclick="showActuator('${escapeHtml(record.actuator_entity_id)}', 'Aktualisiert.')">Details aktualisieren</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="grid-two">
|
<div class="grid-two">
|
||||||
<div>
|
<div>
|
||||||
@@ -1461,7 +1531,10 @@ async function showActuator(actuatorId, evaluationMessage = "") {
|
|||||||
${currentContextControls}
|
${currentContextControls}
|
||||||
${manualAssignment}
|
${manualAssignment}
|
||||||
`;
|
`;
|
||||||
|
box.innerHTML = detailHtml;
|
||||||
|
cachedDetailHtml.set(actuatorId, detailHtml);
|
||||||
document.getElementById("detail").scrollIntoView({behavior: "smooth", block: "start"});
|
document.getElementById("detail").scrollIntoView({behavior: "smooth", block: "start"});
|
||||||
|
renderConfiguredActuators();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
box.textContent = error.message;
|
box.textContent = error.message;
|
||||||
}
|
}
|
||||||
@@ -1768,8 +1841,11 @@ async function startDashboard() {
|
|||||||
document.getElementById("configured-actuators").innerHTML = "<div class='empty-state'>Öffne „Lernen“, um Geräte zu laden.</div>";
|
document.getElementById("configured-actuators").innerHTML = "<div class='empty-state'>Öffne „Lernen“, um Geräte zu laden.</div>";
|
||||||
document.getElementById("actuator-detail").innerHTML = "<div class='empty-state'>Wähle später ein Gerät aus der Übersicht.</div>";
|
document.getElementById("actuator-detail").innerHTML = "<div class='empty-state'>Wähle später ein Gerät aus der Übersicht.</div>";
|
||||||
syncSettingsView();
|
syncSettingsView();
|
||||||
document.getElementById("section-jump").value = "status-section";
|
const initialView = localStorage.getItem("sillyhome.ui.view") === "detail"
|
||||||
showView("status-section");
|
? "observed"
|
||||||
|
: (localStorage.getItem("sillyhome.ui.view") || "status-section");
|
||||||
|
document.getElementById("section-jump").value = initialView;
|
||||||
|
showView(initialView);
|
||||||
await new Promise(resolve => requestAnimationFrame(resolve));
|
await new Promise(resolve => requestAnimationFrame(resolve));
|
||||||
setTimeout(() => void loadStatus(), 100);
|
setTimeout(() => void loadStatus(), 100);
|
||||||
}
|
}
|
||||||
|
|||||||
35
docs/V1_5_4_OPERATING_GUIDE.md
Normal file
35
docs/V1_5_4_OPERATING_GUIDE.md
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
# SillyHome Next v1.5.4 Operating Guide
|
||||||
|
|
||||||
|
Diese Version korrigiert Ingress-Logging und Dashboard-Navigation.
|
||||||
|
|
||||||
|
## Ingress-/Access-Logs
|
||||||
|
|
||||||
|
- Das Add-on startet Uvicorn ohne `--proxy-headers` und ohne
|
||||||
|
`--forwarded-allow-ips='*'`.
|
||||||
|
- Vorher konnte Uvicorn LAN-Adressen aus `X-Forwarded-For` anzeigen. Diese
|
||||||
|
Adresse war dann der urspruengliche Client oder Home-Assistant-Proxy, nicht
|
||||||
|
der direkte Container-Peer.
|
||||||
|
- Nach dem Update sollten Access-Logs den direkten Docker-/Ingress-Peer zeigen.
|
||||||
|
`GET ... HTTP/1.1` bleibt normal und ist kein Hinweis auf fehlendes Streaming.
|
||||||
|
|
||||||
|
## Dashboard-Verhalten
|
||||||
|
|
||||||
|
- Die Startseite nutzt weiter `/v1/actuators/dashboard/system`.
|
||||||
|
- Die Lernuebersicht nutzt weiter `/v1/actuators/dashboard/start`.
|
||||||
|
- Bereits geladene System-, Lern- und Discovery-Daten bleiben beim Wechseln der
|
||||||
|
Ansichten im Browser erhalten und werden nur im Hintergrund aufgefrischt.
|
||||||
|
- Details sind kein eigener Menuepunkt mehr. Sie werden nur ueber ein
|
||||||
|
ausgewaehltes beobachtetes Geraet geoeffnet.
|
||||||
|
- Ein bereits geoeffneter Aktor zeigt seine Detaildaten sofort aus dem
|
||||||
|
Browser-Cache. Neue Detaildaten werden erst ueber `Details aktualisieren`
|
||||||
|
oder nach einer Speichern-/Schaltaktion geladen.
|
||||||
|
|
||||||
|
## Pruefung
|
||||||
|
|
||||||
|
1. Add-on aktualisieren und neu starten.
|
||||||
|
2. Ingress hart neu laden.
|
||||||
|
3. Zwischen Startseite, Lernen und Discovery wechseln.
|
||||||
|
4. Erwartung: Bereits geladene Inhalte bleiben sichtbar; keine volle
|
||||||
|
Neuladung bei jedem Ansichtswechsel.
|
||||||
|
5. Details eines Aktors oeffnen, wegwechseln und wieder Details oeffnen.
|
||||||
|
Erwartung: Die zuletzt geladene Detailansicht steht sofort wieder da.
|
||||||
@@ -16,3 +16,10 @@ def test_addon_version_invalidates_application_build_layer() -> None:
|
|||||||
config_copy = dockerfile.index("COPY config.yaml /tmp/addon-config.yaml")
|
config_copy = dockerfile.index("COPY config.yaml /tmp/addon-config.yaml")
|
||||||
repository_clone = dockerfile.index("git clone --depth 1 --branch main")
|
repository_clone = dockerfile.index("git clone --depth 1 --branch main")
|
||||||
assert config_copy < repository_clone
|
assert config_copy < repository_clone
|
||||||
|
|
||||||
|
|
||||||
|
def test_addon_does_not_trust_forwarded_lan_ips() -> None:
|
||||||
|
run_script = Path("addon/run.sh").read_text(encoding="utf-8")
|
||||||
|
|
||||||
|
assert "--proxy-headers" not in run_script
|
||||||
|
assert "--forwarded-allow-ips" not in run_script
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ def test_dashboard_is_served_at_root() -> None:
|
|||||||
assert "Liste durchsuchen" in response.text
|
assert "Liste durchsuchen" in response.text
|
||||||
assert "Geräteliste bei Bedarf laden" in response.text
|
assert "Geräteliste bei Bedarf laden" in response.text
|
||||||
assert "Vorschläge können Home Assistant stark abfragen" in response.text
|
assert "Vorschläge können Home Assistant stark abfragen" in response.text
|
||||||
|
assert '<option value="detail">Details</option>' not in response.text
|
||||||
assert "Wie gewohnt bedienen" in response.text
|
assert "Wie gewohnt bedienen" in response.text
|
||||||
assert "Ohne deine spätere Freigabe wird nichts geschaltet" in response.text
|
assert "Ohne deine spätere Freigabe wird nichts geschaltet" in response.text
|
||||||
assert "Du wählst keine Sensoren und erstellst keine Regeln" in response.text
|
assert "Du wählst keine Sensoren und erstellst keine Regeln" in response.text
|
||||||
@@ -42,5 +43,7 @@ def test_dashboard_is_served_at_root() -> None:
|
|||||||
assert 'api("v1/entities")' not in response.text
|
assert 'api("v1/entities")' not in response.text
|
||||||
assert 'details class="collapsible"' in response.text
|
assert 'details class="collapsible"' in response.text
|
||||||
assert 'class="group-panel"' in response.text
|
assert 'class="group-panel"' in response.text
|
||||||
|
assert "cachedDetailHtml" in response.text
|
||||||
|
assert "refreshOverviewInBackground" in response.text
|
||||||
assert "Automation-Entwurf" not in response.text
|
assert "Automation-Entwurf" not in response.text
|
||||||
assert "Manuelle Overrides" not in response.text
|
assert "Manuelle Overrides" not in response.text
|
||||||
|
|||||||
Reference in New Issue
Block a user