Second half of the Ring integration (handoff §12):
- RingTopics + RingNormalizer (H3): parse ring/<loc>/<cat>/<id>/<entity>/state,
map ding/motion/contact/lock/battery; bridge status topic drives the addon
connection state. Defensive — unknown topics are ignored, never junk devices.
- IngestRingMessage (H4, single ingest path): auto-creates cloud-flagged Ring
devices on first sight, monotonic race-safe state upsert, broadcasts live.
Gated on the addon being installed (cached) so a still-running bridge can't
recreate devices after uninstall.
- Shared AppliesDeviceState trait: the monotonic upsert now lives once, used by
both Shelly and Ring ingest (MqttTest guards the Shelly path).
- Listener subscribes ring/#. ring-mqtt container (opt-in `addons` compose
profile) as least-privileged `ring` MQTT user (ACL: ring/# only); gen-passwd
seeds the account. "Cloud" badge on Ring devices in list + detail.
11 Ring tests (topic parse, normalizer, bridge status, auto-create, install
gate, unknown-topic guard, out-of-order). Suite 40 green; 12/12 tabs clean.
Live-verified: real MQTT ring/.../ding + info publish → cloud device created
with ding + battery state. (Real Ring OAuth login runs in the ring-mqtt sidecar.)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Makes HomeOS installable on the tablet/phone (the tablet-control use case):
- manifest.webmanifest: standalone display, brand colors, 192/512 "any" +
maskable icons, /panel + /rooms shortcuts, start_url /dashboard.
- sw.js: conservative for an authenticated Livewire app — never intercepts
non-GET (login/Livewire/broadcasting stay online), cache-first only for
immutable /build/ + /icons/, network-first navigations with an offline
fallback so authenticated HTML is never served stale.
- offline.html: self-contained branded offline page (no Vite dependency).
- Rendered PNG icons (any + maskable, glyph inside the safe zone) +
apple-touch-icon; manifest/theme-color/apple meta in both layouts; SW
registered from app.js.
- nginx: application/manifest+json MIME, no-cache + Service-Worker-Allowed
for sw.js (compose mount now active on the app container).
Verified: manifest 200/valid, SW registers+activates (scope /), 11/11 tabs
clean, zero console errors.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Assigning a Shelly now issues a real broker credential so the physical device can
connect (onboarding was incomplete before):
- MqttCredentialProvisioner writes a mosquitto-compatible PBKDF2-SHA512 ($7$)
password line (in PHP) for username = the device's topic prefix, and touches a
reload trigger. A small wrapper in the mosquitto container (docker/mosquitto/
config/entrypoint.sh) SIGHUPs mosquitto so it re-reads the passwd live — no
restart. Verified: a provisioned device authenticates and publishes to its own
prefix (bound by the pattern %u ACL).
- The credential is shown once on the device page after assignment (enter it into
the Shelly). passwd is app-owned + world-readable so the web request can write it.
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>
gen-passwd.sh embedded passwords in `sh -c "..."`, which breaks or injects for
strong passwords containing quotes/$/;. Export them and forward with bare
`-e NAME`, reading them as env vars inside a single-quoted container script — any
character is now handled safely.
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>
- 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>