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.local
|
||||
.env.*
|
||||
/.actuator_store/
|
||||
/MagicMock/
|
||||
|
||||
@@ -1,5 +1,14 @@
|
||||
# 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
|
||||
- Automatische Sensor-Gewichtungsanpassung aus Nutzerfeedback:
|
||||
korrektes Feedback staerkt aktuelle Kontextsignale leicht, falsches Feedback
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
name: SillyHome Next
|
||||
version: "1.5.3"
|
||||
version: "1.5.4"
|
||||
slug: sillyhome_next
|
||||
description: Lernt automatisch aus deinem Verhalten und steuert freigegebene Aktoren
|
||||
url: http://192.168.6.31:3000/pino/sillyhome-next
|
||||
|
||||
@@ -21,5 +21,4 @@ if [ -f /data/options.json ]; then
|
||||
fi
|
||||
|
||||
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 \
|
||||
--proxy-headers --forwarded-allow-ips='*'
|
||||
exec uvicorn app.main:app --app-dir /app --host 0.0.0.0 --port 8000
|
||||
|
||||
@@ -117,7 +117,7 @@ async def lifespan(app: FastAPI) -> AsyncIterator[None]:
|
||||
app = FastAPI(
|
||||
title="SillyHome Next API",
|
||||
description="Lokales Smart-Home-Intelligenzsystem für Home Assistant.",
|
||||
version="1.5.3",
|
||||
version="1.5.4",
|
||||
lifespan=lifespan,
|
||||
)
|
||||
app.state.settings = load_settings()
|
||||
|
||||
@@ -154,7 +154,6 @@
|
||||
<select id="section-jump" onchange="showView(this.value)">
|
||||
<option value="status-section">Startseite / System</option>
|
||||
<option value="observed">Lernen</option>
|
||||
<option value="detail">Details</option>
|
||||
<option value="choose">Discovery & Einrichtung</option>
|
||||
<option value="settings">Einstellungen</option>
|
||||
<option value="guide">Ablauf</option>
|
||||
@@ -309,7 +308,12 @@ let manualContextState = {options: [], selected: new Set()};
|
||||
let cachedActuators = null;
|
||||
let cachedEntities = null;
|
||||
let cachedDiscovery = null;
|
||||
let cachedSystemOverview = null;
|
||||
let cachedDashboardOverview = null;
|
||||
let cachedDetailHtml = new Map();
|
||||
let discoveryLoadPromise = null;
|
||||
let overviewLoadPromise = null;
|
||||
let systemLoadPromise = null;
|
||||
let currentSensorWeightGroups = [];
|
||||
let visibleActuatorLimit = 24;
|
||||
const ACTUATOR_RESULT_LIMIT = 50;
|
||||
@@ -465,14 +469,29 @@ function jumpToSection(target) {
|
||||
}
|
||||
|
||||
function showView(viewId) {
|
||||
if (viewId === "detail" && !currentActuatorId) {
|
||||
viewId = "observed";
|
||||
}
|
||||
for (const section of document.querySelectorAll(".app-view")) {
|
||||
section.classList.toggle("active", section.id === viewId);
|
||||
}
|
||||
localStorage.setItem("sillyhome.ui.view", viewId);
|
||||
if (viewId === "status-section") {
|
||||
if (cachedSystemOverview) {
|
||||
renderDashboardStatus(cachedSystemOverview);
|
||||
refreshSystemOverviewInBackground();
|
||||
} else {
|
||||
void loadSystemOverview();
|
||||
}
|
||||
} else if (viewId === "observed") {
|
||||
if (cachedActuators) {
|
||||
renderConfiguredActuators();
|
||||
refreshOverviewInBackground();
|
||||
} else {
|
||||
void loadOverview();
|
||||
}
|
||||
} else if (viewId === "choose") {
|
||||
if (cachedDiscovery) renderActuatorDiscovery();
|
||||
} else if (viewId === "settings") {
|
||||
syncSettingsView();
|
||||
}
|
||||
@@ -484,8 +503,10 @@ function setLanguage(language) {
|
||||
localStorage.setItem("sillyhome.ui.language", uiLang);
|
||||
syncSettingsView();
|
||||
if (cachedActuators) renderConfiguredActuators();
|
||||
void loadSystemOverview();
|
||||
if (currentActuatorId) void showActuator(currentActuatorId);
|
||||
if (cachedSystemOverview) renderDashboardStatus(cachedSystemOverview);
|
||||
if (currentActuatorId && cachedDetailHtml.has(currentActuatorId)) {
|
||||
document.getElementById("actuator-detail").innerHTML = cachedDetailHtml.get(currentActuatorId);
|
||||
}
|
||||
}
|
||||
|
||||
function syncSettingsView() {
|
||||
@@ -514,6 +535,9 @@ function invalidateDashboardCache() {
|
||||
cachedActuators = null;
|
||||
cachedEntities = null;
|
||||
cachedDiscovery = null;
|
||||
cachedDashboardOverview = null;
|
||||
cachedSystemOverview = null;
|
||||
cachedDetailHtml.clear();
|
||||
}
|
||||
|
||||
async function api(path, options = {}) {
|
||||
@@ -654,13 +678,24 @@ function optionGroups(entities, selectedIds = new Set()) {
|
||||
}
|
||||
|
||||
async function loadOverview() {
|
||||
if (overviewLoadPromise) return overviewLoadPromise;
|
||||
overviewLoadPromise = doLoadOverview().finally(() => {
|
||||
overviewLoadPromise = null;
|
||||
});
|
||||
return overviewLoadPromise;
|
||||
}
|
||||
|
||||
async function doLoadOverview() {
|
||||
const startedAt = performance.now();
|
||||
const budget = document.getElementById("load-budget");
|
||||
if (budget) budget.textContent = "Startdaten laden ...";
|
||||
if (!cachedActuators) {
|
||||
document.getElementById("configured-actuators").innerHTML = "<p class='muted'>Beobachtete Geräte werden geladen ...</p>";
|
||||
}
|
||||
try {
|
||||
const dashboard = await api("v1/actuators/dashboard/start");
|
||||
dashboard._load_elapsed_ms = Math.round(performance.now() - startedAt);
|
||||
cachedDashboardOverview = dashboard;
|
||||
cachedActuators = dashboard.actuators || [];
|
||||
cachedEntities = [];
|
||||
renderDashboardStatus(dashboard);
|
||||
@@ -685,12 +720,21 @@ async function loadOverview() {
|
||||
}
|
||||
|
||||
async function loadSystemOverview() {
|
||||
if (systemLoadPromise) return systemLoadPromise;
|
||||
systemLoadPromise = doLoadSystemOverview().finally(() => {
|
||||
systemLoadPromise = null;
|
||||
});
|
||||
return systemLoadPromise;
|
||||
}
|
||||
|
||||
async function doLoadSystemOverview() {
|
||||
const startedAt = performance.now();
|
||||
const budget = document.getElementById("load-budget");
|
||||
if (budget) budget.textContent = "Systemübersicht lädt ...";
|
||||
try {
|
||||
const dashboard = await api("v1/actuators/dashboard/system");
|
||||
dashboard._load_elapsed_ms = Math.round(performance.now() - startedAt);
|
||||
cachedSystemOverview = dashboard;
|
||||
cachedActuators = dashboard.actuators || cachedActuators;
|
||||
renderDashboardStatus(dashboard);
|
||||
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() {
|
||||
const run = () => {
|
||||
void loadDashboardExtras();
|
||||
@@ -1131,10 +1191,20 @@ async function showActuator(actuatorId, evaluationMessage = "") {
|
||||
for (const section of document.querySelectorAll(".app-view")) {
|
||||
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");
|
||||
const box = document.getElementById("actuator-detail");
|
||||
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 {
|
||||
const record = await api(`v1/actuators/${encodeURIComponent(actuatorId)}/detail`);
|
||||
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>
|
||||
</li>`).join("")}</ul>`
|
||||
: "<p class='muted'>Keine eindeutig passende HA-Automation gefunden.</p>";
|
||||
box.innerHTML = `
|
||||
const detailHtml = `
|
||||
<div class="detail-header">
|
||||
<div>
|
||||
<h3>${escapeHtml(record.actuator_entity_id)}</h3>
|
||||
<p class="muted">Alle wichtigen Aktionen für dieses Gerät.</p>
|
||||
</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 class="grid-two">
|
||||
<div>
|
||||
@@ -1461,7 +1531,10 @@ async function showActuator(actuatorId, evaluationMessage = "") {
|
||||
${currentContextControls}
|
||||
${manualAssignment}
|
||||
`;
|
||||
box.innerHTML = detailHtml;
|
||||
cachedDetailHtml.set(actuatorId, detailHtml);
|
||||
document.getElementById("detail").scrollIntoView({behavior: "smooth", block: "start"});
|
||||
renderConfiguredActuators();
|
||||
} catch (error) {
|
||||
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("actuator-detail").innerHTML = "<div class='empty-state'>Wähle später ein Gerät aus der Übersicht.</div>";
|
||||
syncSettingsView();
|
||||
document.getElementById("section-jump").value = "status-section";
|
||||
showView("status-section");
|
||||
const initialView = localStorage.getItem("sillyhome.ui.view") === "detail"
|
||||
? "observed"
|
||||
: (localStorage.getItem("sillyhome.ui.view") || "status-section");
|
||||
document.getElementById("section-jump").value = initialView;
|
||||
showView(initialView);
|
||||
await new Promise(resolve => requestAnimationFrame(resolve));
|
||||
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")
|
||||
repository_clone = dockerfile.index("git clone --depth 1 --branch main")
|
||||
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 "Geräteliste bei Bedarf laden" 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 "Ohne deine spätere Freigabe wird nichts geschaltet" 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 'details class="collapsible"' 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 "Manuelle Overrides" not in response.text
|
||||
|
||||
Reference in New Issue
Block a user