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>