Issue 1 — presence was half-built:
- Away detection: the debounce was 8 min (you disconnect, still "home"). Now
3 min and configurable (homeos.presence.away_debounce_minutes /
PRESENCE_AWAY_DEBOUNCE).
- Person management: ManagePerson modal handles BOTH add and edit (name, UniFi
client, avatar). Delete from the person card (confirm). Replaces AddPerson.
- Avatar upload: image stored on the public disk (storage:link), shown on cards
+ the who's-home strip; initials fallback (green when home).
- Who's home: a summary strip ("N Zuhause · M abwesend" with avatars) + clear
per-person home/away, so you can see at a glance who is in.
5 tests (debounce, create+avatar, edit, delete+avatar removal). Suite 78 green,
12/12 tabs clean.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Multi-agent review (5 confirmed) + Codex found real issues in the rework:
- [P1 security] Shared `shelly` ACL had readwrite +/rpc, so a compromised
device could inject Switch.Set into ANY other device's <prefix>/rpc. Now
read-only on +/rpc (receive own commands) + write only homeos/rpc (reply).
Broker-verified: a shelly publish to victim/rpc is denied; laravel's is not.
- [P2 security] `+` wildcard reaches reserved homeos/ring namespaces → bogus
device. Ingest now rejects RESERVED_PREFIXES (homeos/ring/$SYS).
- [P2 security] Unbounded auto-onboarding = DB-exhaustion DoS. Added a device
cap (homeos.mqtt.max_devices, default 250).
- [P2 correctness] Every Shelly `input` became a phantom window contact (wall
switches shown as windows, possibly inverted). `input` is now a generic
sensor; the user PROMOTES specific inputs to window/door contacts on the
device page (invert-aware), stored in config->input_roles and applied in the
ingest — this is the "assign contacts" flow the user asked for.
- [P3 ux] Motion pill read `active`; producer writes `on`. Now reads both.
- [P1 migrations] Dedup computed survivor keys once; 3+ duplicates could
collide on unique(device_id,key). Re-query per duplicate (ring + mqtt_prefix).
+9 tests (reserved prefix, cap, input generic/promoted/inverted, demo echo).
Live-verified: input published → onboarded as input → assigned window contact
via UI → appears on Fenster page, persists across messages. Suite 57 green,
12/12 tabs clean.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Addresses "I have to configure MQTT per device; HA just works":
- Shared device account `shelly` (one credential every Shelly uses) with a
SCOPED ACL (status/events/online/rpc for any prefix; cannot touch homeos/,
ring/ or $SYS). gen-passwd seeds it from MQTT_SHELLY_PASSWORD.
- Auto-onboarding: IngestShellyMessage creates the device on the first
recognizable component (sys/wifi/cloud noise ignored), so pointing a Shelly
at the broker is all it takes — no manual "Zuweisen". Partial unique index on
config->>'mqtt_prefix' + race-safe create (merge-dedup migration).
- Settings → Geräte-MQTT card: server, username, reveal/copy password + steps
(config/homeos.php, MQTT_DEVICE_HOST). Discovery "Zuweisen" now upserts by
prefix (names/rooms an already-onboarded device, no duplicate) and no longer
forces per-device creds.
- Per-device credentials kept as opt-in hardening: a "generate" button on the
device page (pattern %u ACL retained).
Live-verified: publishing as `shelly` to a new prefix auto-creates the device
with switch+power state; a sys topic creates nothing. Suite 46 green, 12/12
tabs clean.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Codex R15: duplicate Ring rows from the pre-index race can own different
entities (one got battery, another motion); deleting the extras cascaded away
already-ingested state. The migration now MERGES — reparents each duplicate's
entities and their state onto the earliest row, keeping the survivor's copy on
a (device_id,key) collision — then removes the emptied duplicates.
Verified on Postgres: survivor keeps battery=90 and gains the reparented
motion entity (disjoint data preserved); suite 43 green on SQLite.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Codex R15: on an upgrade where the pre-index race already produced duplicate
config.ring_id rows, CREATE UNIQUE INDEX would abort the migration. Now the
migration first collapses duplicates (keep the earliest id per ring_id; the
rest cascade-delete their entities/state) using portable subquery SQL that
runs on Postgres (prod) and SQLite (tests) alike.
Verified: 3 duplicates → 1 on Postgres, index then created; suite 43 green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Codex R15 on the Ring bridge flagged two issues:
- [P1] Concurrent first-sight messages (retained info + motion on bridge
startup) could create duplicate devices — no uniqueness on ring_id. Added a
partial unique index on (config->>'ring_id'); resolveDevice now creates and,
on the unique violation, re-fetches the winner (race-safe).
- [P2] The install-gate cache was never invalidated, so ingest was dropped for
up to 15s after install and devices kept being created for up to 15s after
uninstall. AddonService now forgets the shared cache key on install/uninstall.
+3 tests (unique-index guard, repeated-messages-reuse-device, cache
invalidation). Suite 43 green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
First half of the Ring integration (handoff §12: ring-mqtt, cloud token auth):
- Addons registry (static catalogue) + Addon model with encrypted config, so
the Ring refresh token / credentials are never stored in plaintext.
- AddonService: install / uninstall (wipes secrets) / saveConnection (blank
password keeps the stored one) / markStatus (from the bridge over MQTT).
- Addons page (new sidebar tab) with per-addon install, Cloud badge, live
status pill, and a Ring connect modal capturing email/password/2FA — we
never call Ring ourselves; creds are handed to the ring-mqtt sidecar.
- Uninstall behind a wire-elements confirm (R5). Fixed the confirm target:
Livewire names App\Livewire\Addons\Index as "addons" (Index suffix dropped),
and only $wire.$dispatch (not Alpine $dispatch) reaches the modal listener.
- DE/EN localization (R16); doorbell/package/cloud icons.
7 AddonTest cases (registry, install, encryption-at-rest, blank-password
keep, uninstall wipe, status guard). Suite 33 green; 12/12 tabs clean; full
install→connect→connecting→uninstall UI flow browser-verified, 0 console errors.
Backend bridge (ring-mqtt container) + Ring MQTT normalizer land next.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
New "Steuerung" page for tablet control:
- Large touch tiles for every light/switch — tap toggles (through
DeviceCommandService), highlighted when on, live via Echo.
- Drag-to-reorder via SortableJS (handle per tile); order persisted to
entities.panel_sort.
- Lights get a colour/brightness modal → Light.Set (brightness + rgb) on the
Shelly, with preset swatches + a custom colour picker. Driver contract gains
setLight(); command service audits it (H1).
Nav check 11/11 tabs clean.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- metrics table + Metric model; metrics:sample command (scheduled every minute,
pruned to 24h) records MQTT throughput (a Redis counter the ingest jobs bump)
and host CPU load + memory% from /proc.
- Chart.js line-chart Alpine island (themed from CSS tokens, dual axis) on the
Host page shows MQTT/min, CPU load and memory %. wire:ignore so the 10s health
poll doesn't re-init it. Nav check 10/10 clean (0 console errors).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- UnifiClient service (art-of-wifi/unifi-api-client) talks to the local UDM with a
read-only account (self-signed cert, verify_ssl off). Verified live: logged in
and read 32 active clients.
- presence:poll command (scheduled every minute, withoutOverlapping): "home"
immediately on association, "away" only after an 8-min debounce so iPhone WLAN
sleep can't cause false-aways. Broadcasts PresenceChanged on the presence channel.
- Persons page: "Person hinzufügen" modal picks the representing device straight
from the live UniFi client list (falls back to manual MAC entry if UniFi is down);
the sweep then tracks that person's presence. mac + last_seen_home_at on persons.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Persons & Anwesenheit, Netzwerk & Discovery, Automationen, Einstellungen and
Zugang & Face-ID are now working Livewire pages (were dead "In Kürze" buttons).
Adds persons / discovery_findings / automations tables + models; authorizes the
presence + discovery broadcast channels; silences MQTT debug-log spam. Nav check
10/10 tabs clean (200, 0 console errors, 0 failed requests).
Deeper behaviour (presence polling, discovery sidecar, automation engine) lands
in the respective phase; the pages + schema are in place and render live.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- ACL: drop the shared `shelly` account + `+` wildcard (allowed cross-device
spoofing/control). Devices now authenticate with per-device credentials
(username = topic prefix, provisioned at onboarding) bound to their own prefix
via `pattern %u`. gen-passwd.sh creates only laravel + sidecar.
- last_seen_at is set from the message receive time (observed_at), monotonically,
so a delayed/retried/stale ingest job can't mark a device online incorrectly.
- Device::isOnline: a real device with no last_seen is OFFLINE (never connected);
only demo devices are assumed reachable. Added `demo` flag + presence tests.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
With multiple Horizon workers, ingest jobs for one entity can finish out of
order and overwrite newer state with older. The listener now stamps each message
with a µs receive time (observed_at); IngestShellyMessage applies state only when
the incoming message is newer (race-safe via a conditional update + unique guard),
and broadcasts only when applied. Added a feature test for the ordering guard.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Real bidirectional MQTT so devices are live, not mock (handoff §13.3):
- Mosquitto 2 broker (auth + per-client ACLs for laravel/shelly/sidecar from day
one); passwd generated by docker/mosquitto/gen-passwd.sh (gitignored).
- mqtt-listener daemon: subscribes `+/status/#`, parse + dispatch only (H2),
exponential reconnect backoff, graceful SIGTERM. php-mqtt/laravel-client.
- Ingest path (H4): IngestShellyMessage resolves device by mqtt_prefix, upserts
device_states, refreshes last_seen, broadcasts DeviceStateChanged
(ShouldBroadcastNow) on the private `home` channel.
- Control path (H1): DeviceDriver contract + ShellyMqttDriver (command topic +
Shelly.Reboot RPC) behind DeviceCommandService, which audits every command to
the new `commands` table. Device detail toggles + restart route through it;
flash reflects the real result.
- Live UI: dashboard + device pages listen via Echo (#[On('echo-private:home,
.DeviceStateChanged')]) and re-render instantly.
- Vendor specifics isolated in Support/Mqtt + Support/Drivers (H3).
Verified end-to-end in a real browser: publishing an MQTT status turned a light
"An" on the dashboard in 3.0s with no reload, 0 console errors. R12 30/30;
15 feature tests green (incl. ingest + command audit). README/bootstrap document
the broker passwd step.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
`migrate --seed` runs in the documented bootstrap; without a guard a production
deploy would get the mock household (stale/open/low-battery devices) in real
tables. DemoHomeSeeder now only runs in local/testing.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- DatabaseSeeder throws outside local/testing when HOMEOS_ADMIN_PASSWORD is
unset, instead of silently seeding the documented `homeos-dev` password
(bootstrap always runs migrate --seed, so a prod misconfig must fail loudly).
- Devices\Show::saveRoom validates roomId as nullable|exists:rooms,id, so a
crafted request can no longer trigger a foreign-key 500. Added a feature test.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Warnings no longer clutter the dashboard: a compact "Warnungen (n)" button in
the topbar opens a themed wire-elements/modal (R5) listing all messages. Dark
modal wrapper overridden; HomeStatus service shared by dashboard + modal.
- Device detail page (/devices/{uuid}, UUID route key): edit name, room and
active state; view info + live capabilities; Neustart (with confirm modal) and
"Update prüfen" as mock commands (Phase 3 routes them through the real driver).
Devices index (/devices) added; "Geräte" nav activated; dashboard device rows
and index link to the detail. Generic Confirm modal + x-detail component.
- Mock devices no longer rot: online is now "active and (no last_seen or seen
<10min)", so the demo stays healthy; the one offline device keeps a stale
timestamp. Full DE/EN i18n for devices + modal copy.
Verified: R12 30/30 in headless Chromium (0 console errors, 0 failed requests);
10 feature tests green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Document the first-run path (deps/assets/key/migrate aren't auto-installed
because vendor/node_modules/public-build are gitignored): add
docker/app/bootstrap.sh and a README with the exact sequence to run before
`docker compose up`.
- Seeder: fall back with `?:` so a present-but-empty HOMEOS_ADMIN_PASSWORD can
never create a blank-password admin; .env.example ships a non-empty dev value.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Reworks the dashboard into a home-control view per user feedback (it is not a
server dashboard):
- Dashboard now shows home data — rooms with live device state (lights on/off,
power draw, window/door contacts, battery %), a home summary (devices online,
lights on, open contacts, low batteries), and a Warnings panel that only lists
what needs attention (offline device, open window, low battery, degraded host
service). No server internals on the dashboard.
- Server/service health (DB, Redis, Reverb, Horizon) + version info moved to a
dedicated "Host & Dienste" page (new nav item under System); dashboard surfaces
a host problem only as a warning that links there.
- New domain slice (handoff §3, mock-first §13.2): rooms/devices/entities/
device_states migrations + models (UUID route keys, R11) + DemoHomeSeeder with
Shelly-like devices incl. deliberate faults. Extracted SystemHealth service.
- Sidebar decluttered further; new x-entity-state component; full DE/EN i18n.
- Fixed R15 findings: .env.example now ships matching non-empty dev defaults for
DB_PASSWORD and Reverb keys so a fresh `cp .env.example .env` boots cleanly.
Verified: R12 21/21 in headless Chromium (0 console errors, 0 failed requests,
breakpoints 375/768/1280); 10 feature tests green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>