Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| ca253d1e6c |
@@ -1,5 +1,13 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## 1.0.5 - 2026-06-17
|
||||||
|
- Lange Friendly Names, Entity-IDs, Chips, Tabellenwerte und Metriken brechen
|
||||||
|
im Dashboard responsiv um und laufen nicht mehr aus Karten oder Boxen.
|
||||||
|
- Automatisierter Performance-Budget-Test fuer Root-HTML und
|
||||||
|
`/v1/actuators/dashboard` gegen das 5-Sekunden-Limit ergaenzt.
|
||||||
|
- HA-/Ingress-Verifikation mit Supervisor-Status, Backup, Watchdog,
|
||||||
|
Hard-Reload und Rollback im Operating Guide dokumentiert.
|
||||||
|
|
||||||
## 1.0.4 - 2026-06-17
|
## 1.0.4 - 2026-06-17
|
||||||
- Sensor-Relevanz ist in der Aktor-Detailansicht sichtbar: automatische
|
- Sensor-Relevanz ist in der Aktor-Detailansicht sichtbar: automatische
|
||||||
Relevanz, aktive Gewichtung und Score werden pro verwendetem Sensor/Zustand
|
Relevanz, aktive Gewichtung und Score werden pro verwendetem Sensor/Zustand
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
name: SillyHome Next
|
name: SillyHome Next
|
||||||
version: "1.0.4"
|
version: "1.0.5"
|
||||||
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
|
||||||
|
|||||||
@@ -105,7 +105,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.0.4",
|
version="1.0.5",
|
||||||
lifespan=lifespan,
|
lifespan=lifespan,
|
||||||
)
|
)
|
||||||
app.state.settings = load_settings()
|
app.state.settings = load_settings()
|
||||||
|
|||||||
@@ -25,6 +25,7 @@
|
|||||||
--bad:#8fb8ff;
|
--bad:#8fb8ff;
|
||||||
}
|
}
|
||||||
* { box-sizing:border-box; }
|
* { box-sizing:border-box; }
|
||||||
|
html, body { max-width:100%; overflow-x:hidden; }
|
||||||
body { margin:0; font-size:15px; background:var(--panel-quiet); }
|
body { margin:0; font-size:15px; background:var(--panel-quiet); }
|
||||||
h1,h2,h3 { margin:0 0 10px; letter-spacing:0; }
|
h1,h2,h3 { margin:0 0 10px; letter-spacing:0; }
|
||||||
p { margin:6px 0; }
|
p { margin:6px 0; }
|
||||||
@@ -76,26 +77,29 @@
|
|||||||
button.secondary { background:var(--complement-soft); color:#dff6ff; border:1px solid #22607c; }
|
button.secondary { background:var(--complement-soft); color:#dff6ff; border:1px solid #22607c; }
|
||||||
button.danger { background:#2a3441; color:#f2f6fb; border:1px solid #536273; }
|
button.danger { background:#2a3441; color:#f2f6fb; border:1px solid #536273; }
|
||||||
button.compact { width:auto; min-width:112px; margin-right:8px; padding:8px 10px; min-height:36px; }
|
button.compact { width:auto; min-width:112px; margin-right:8px; padding:8px 10px; min-height:36px; }
|
||||||
table { width: 100%; border-collapse: collapse; font-size: .92rem; }
|
table { width: 100%; border-collapse: collapse; table-layout:fixed; font-size: .92rem; }
|
||||||
td,th { padding: 8px; border-bottom: 1px solid #2d3a47; text-align: left; vertical-align: top; }
|
td,th { padding: 8px; border-bottom: 1px solid #2d3a47; text-align: left; vertical-align: top; overflow-wrap:anywhere; word-break:break-word; }
|
||||||
ul { margin: 8px 0; padding-left: 18px; }
|
ul { margin: 8px 0; padding-left: 18px; }
|
||||||
.notice { border-left:4px solid var(--complement); padding-left:10px; }
|
.notice { border-left:4px solid var(--complement); padding-left:10px; }
|
||||||
.grid-two { display:grid; grid-template-columns:repeat(auto-fit,minmax(240px,1fr)); gap:12px; }
|
.grid-two { display:grid; grid-template-columns:repeat(auto-fit,minmax(240px,1fr)); gap:12px; }
|
||||||
.chips { display:flex; flex-wrap:wrap; gap:6px; margin-top:8px; }
|
.chips { display:flex; flex-wrap:wrap; gap:6px; margin-top:8px; }
|
||||||
.chip { padding:4px 8px; border-radius:8px; background:#222b36; border:1px solid var(--border); font-size:.85rem; }
|
.chip { padding:4px 8px; border-radius:8px; background:#222b36; border:1px solid var(--border); font-size:.85rem; max-width:100%; overflow-wrap:anywhere; word-break:break-word; }
|
||||||
.muted { color:var(--text-soft); }
|
.muted { color:var(--text-soft); }
|
||||||
.card-list { display:grid; grid-template-columns:repeat(auto-fit,minmax(250px,1fr)); gap:10px; }
|
.card-list { display:grid; grid-template-columns:repeat(auto-fit,minmax(250px,1fr)); gap:10px; }
|
||||||
.actuator-card { background:#121922; border:1px solid var(--border); border-radius:8px; padding:10px; min-width:0; }
|
.actuator-card { background:#121922; border:1px solid var(--border); border-radius:8px; padding:10px; min-width:0; }
|
||||||
.actuator-card.selected { border-color:var(--complement); box-shadow:0 0 0 1px rgba(28,199,255,.35); }
|
.actuator-card.selected { border-color:var(--complement); box-shadow:0 0 0 1px rgba(28,199,255,.35); }
|
||||||
.card-title { display:flex; justify-content:space-between; gap:10px; align-items:flex-start; margin-bottom:8px; }
|
.card-title { display:flex; flex-wrap:wrap; justify-content:space-between; gap:10px; align-items:flex-start; margin-bottom:8px; min-width:0; }
|
||||||
.entity-id { overflow-wrap:anywhere; font-weight:800; }
|
.card-title > div { min-width:0; flex:1 1 160px; overflow-wrap:anywhere; word-break:break-word; }
|
||||||
|
.card-title .chip { flex:0 1 auto; white-space:normal; text-align:center; }
|
||||||
|
.actuator-card strong { overflow-wrap:anywhere; word-break:break-word; }
|
||||||
|
.entity-id { overflow-wrap:anywhere; word-break:break-word; font-weight:800; }
|
||||||
.metric-grid { display:grid; grid-template-columns:repeat(auto-fit,minmax(120px,1fr)); gap:6px; margin:8px 0; }
|
.metric-grid { display:grid; grid-template-columns:repeat(auto-fit,minmax(120px,1fr)); gap:6px; margin:8px 0; }
|
||||||
.metric { background:var(--panel-soft); border:1px solid var(--border); border-radius:8px; padding:8px; min-width:0; }
|
.metric { background:var(--panel-soft); border:1px solid var(--border); border-radius:8px; padding:8px; min-width:0; overflow-wrap:anywhere; word-break:break-word; }
|
||||||
.metric strong { display:block; margin-bottom:4px; color:#cfe0ec; font-size:.84rem; }
|
.metric strong { display:block; margin-bottom:4px; color:#cfe0ec; font-size:.84rem; }
|
||||||
.actions { display:flex; flex-wrap:wrap; gap:8px; margin-top:10px; }
|
.actions { display:flex; flex-wrap:wrap; gap:8px; margin-top:10px; }
|
||||||
.actions button { flex:1 1 180px; margin-top:0; }
|
.actions button { flex:1 1 180px; margin-top:0; }
|
||||||
.detail-header { display:flex; justify-content:space-between; gap:12px; align-items:flex-start; flex-wrap:wrap; }
|
.detail-header { display:flex; justify-content:space-between; gap:12px; align-items:flex-start; flex-wrap:wrap; }
|
||||||
.manual-context { margin-top:12px; background:#121922; border:1px solid var(--border); border-radius:8px; padding:10px; }
|
.manual-context { margin-top:12px; background:#121922; border:1px solid var(--border); border-radius:8px; padding:10px; min-width:0; overflow-wrap:anywhere; word-break:break-word; }
|
||||||
.inline-controls { display:grid; grid-template-columns:repeat(auto-fit,minmax(160px,1fr)); gap:8px; margin:8px 0; }
|
.inline-controls { display:grid; grid-template-columns:repeat(auto-fit,minmax(160px,1fr)); gap:8px; margin:8px 0; }
|
||||||
.manual-entry { min-height:80px; resize:vertical; }
|
.manual-entry { min-height:80px; resize:vertical; }
|
||||||
textarea { width:100%; border-radius:8px; border:1px solid #3b4b5b; padding:12px; background:#101820; color:#fff; font:inherit; }
|
textarea { width:100%; border-radius:8px; border:1px solid #3b4b5b; padding:12px; background:#101820; color:#fff; font:inherit; }
|
||||||
|
|||||||
@@ -101,6 +101,21 @@ wget -qO /tmp/summary.json http://58adbe1e-sillyhome-next:8000/v1/actuators/summ
|
|||||||
wget -qO /tmp/dashboard.json http://58adbe1e-sillyhome-next:8000/v1/actuators/dashboard
|
wget -qO /tmp/dashboard.json http://58adbe1e-sillyhome-next:8000/v1/actuators/dashboard
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Wenn der Add-on-Container aus dem Agent-Host nicht direkt routbar ist, gilt der
|
||||||
|
Home-Assistant-Supervisor als Verifikationsquelle:
|
||||||
|
|
||||||
|
- Add-on-Info pruefen: Version, `version_latest`, `update_available`, `state`,
|
||||||
|
`boot` und `watchdog`.
|
||||||
|
- Vor Updates eine Home-Assistant-Teil-Sicherung fuer **SillyHome Next**
|
||||||
|
erstellen.
|
||||||
|
- Nach einem Store-Reload und Update muss `version == version_latest`,
|
||||||
|
`update_available == false`, `state == started`, `boot == auto` und
|
||||||
|
`watchdog == true` gelten.
|
||||||
|
- Den HA-/Ingress-Tab nach jedem Update hart neu laden, weil Home Assistant
|
||||||
|
sonst alte HTML-/JavaScript-Ressourcen aus dem bestehenden Tab verwenden kann.
|
||||||
|
- Rollback erfolgt ueber die vorherige Add-on-Teil-Sicherung oder den letzten
|
||||||
|
Git-Tag; beide Referenzen im Release-/Abnahmeprotokoll notieren.
|
||||||
|
|
||||||
## Rollback
|
## Rollback
|
||||||
|
|
||||||
Der stabile Vor-1.0-Stand ist `v0.7.21`. Vor dem 1.0.0-Umbau wurde ein
|
Der stabile Vor-1.0-Stand ist `v0.7.21`. Vor dem 1.0.0-Umbau wurde ein
|
||||||
|
|||||||
@@ -44,6 +44,12 @@ expliziter Freigabe.
|
|||||||
- `ruff check .`
|
- `ruff check .`
|
||||||
- `mypy app backend tests`
|
- `mypy app backend tests`
|
||||||
- `git diff --check`
|
- `git diff --check`
|
||||||
|
- Performance-Budget:
|
||||||
|
- Automatisierter Test prueft Root-HTML und `/v1/actuators/dashboard` gegen
|
||||||
|
das 5-Sekunden-Budget mit kontrollierten Fake-HA-/Cache-Daten.
|
||||||
|
- HA-/Ingress-Verifikation:
|
||||||
|
- Supervisor-Update, Add-on-Status, Watchdog, Backup, Ingress-Hard-Reload
|
||||||
|
und Rollback sind im Operating Guide dokumentiert.
|
||||||
|
|
||||||
## Teilweise Erfuellt
|
## Teilweise Erfuellt
|
||||||
|
|
||||||
@@ -62,14 +68,10 @@ expliziter Freigabe.
|
|||||||
|
|
||||||
## Offen Fuer v1.0.x
|
## Offen Fuer v1.0.x
|
||||||
|
|
||||||
- Echte Dashboard-Performance-Budget-Tests, die Start-HTML und
|
|
||||||
`/v1/actuators/dashboard` gegen ein 5-Sekunden-Limit messen.
|
|
||||||
- Dashboard-Jobstatus fuer Reconciliation, Training, Discovery und
|
- Dashboard-Jobstatus fuer Reconciliation, Training, Discovery und
|
||||||
Automation-Refresh.
|
Automation-Refresh.
|
||||||
- Mehr Entscheidungsstatistik pro Aktor: welche Sensoren wie stark
|
- Mehr Entscheidungsstatistik pro Aktor: welche Sensoren wie stark
|
||||||
beigetragen haben, wie sich Confidence und Sample Count entwickeln.
|
beigetragen haben, wie sich Confidence und Sample Count entwickeln.
|
||||||
- Dokumentierte HA-Installationspruefung mit Supervisor-/Ingress-Hinweisen,
|
|
||||||
weil direkte Container-HTTP-Pruefung ausserhalb HA nicht immer routbar ist.
|
|
||||||
|
|
||||||
## Rollback
|
## Rollback
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|||||||
|
|
||||||
[project]
|
[project]
|
||||||
name = "sillyhome-next"
|
name = "sillyhome-next"
|
||||||
version = "1.0.4"
|
version = "1.0.5"
|
||||||
description = "Lokales Smart-Home-Intelligenzsystem für Home Assistant"
|
description = "Lokales Smart-Home-Intelligenzsystem für Home Assistant"
|
||||||
requires-python = ">=3.11"
|
requires-python = ">=3.11"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from time import perf_counter
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
@@ -299,6 +300,26 @@ def test_dashboard_overview_uses_cache_without_ha_roundtrip(tmp_path: Path) -> N
|
|||||||
assert payload["discovery_groups"]
|
assert payload["discovery_groups"]
|
||||||
|
|
||||||
|
|
||||||
|
def test_dashboard_start_path_stays_within_five_second_budget(tmp_path: Path) -> None:
|
||||||
|
with TestClient(app) as client:
|
||||||
|
_install_service(tmp_path)
|
||||||
|
client.get("/v1/actuators/discovery")
|
||||||
|
client.post("/v1/actuators", json={"actuator_entity_id": "light.abstellkammer"})
|
||||||
|
|
||||||
|
root_started_at = perf_counter()
|
||||||
|
root_response = client.get("/")
|
||||||
|
root_elapsed = perf_counter() - root_started_at
|
||||||
|
|
||||||
|
dashboard_started_at = perf_counter()
|
||||||
|
dashboard_response = client.get("/v1/actuators/dashboard")
|
||||||
|
dashboard_elapsed = perf_counter() - dashboard_started_at
|
||||||
|
|
||||||
|
assert root_response.status_code == 200
|
||||||
|
assert dashboard_response.status_code == 200
|
||||||
|
assert root_elapsed < 5.0
|
||||||
|
assert dashboard_elapsed < 5.0
|
||||||
|
|
||||||
|
|
||||||
def test_discovery_reads_entities_once_and_reuses_them(tmp_path: Path) -> None:
|
def test_discovery_reads_entities_once_and_reuses_them(tmp_path: Path) -> None:
|
||||||
with TestClient(app) as client:
|
with TestClient(app) as client:
|
||||||
_install_service(tmp_path)
|
_install_service(tmp_path)
|
||||||
|
|||||||
Reference in New Issue
Block a user