Commit Graph

12 Commits (42ae76227b6cdaf4d5786f0129ace9cbbafafc6b)

Author SHA1 Message Date
HomeOS Bootstrap 42ae76227b fix(ring): seed bridge config so it actually starts (auto, reproducible)
ring-mqtt v5 needs /data/config.json or it exits immediately — so "always on"
crashed. gen-passwd.sh now generates docker/ring-mqtt/config.json (the broker
mqtt_url for the least-privileged `ring` user), bind-mounted read-only into the
bridge; the token/state stays in the ring-data volume. config.json is gitignored
(has the password); a .example is committed.

Verified live: bridge starts on `docker compose up -d`, web UI answers 200 on
:55123, MQTT URL set — it only waits for the Ring login. Fully automatic, no
manual command.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-18 11:00:32 +02:00
HomeOS Bootstrap ecfc49e665 feat(devices): auto-detect host IP, discovery dedup, delete + rescan
Addresses the device-management feedback:
- The MQTT server address is no longer hardcoded (was 10.10.90.110). It's
  auto-detected from the address you open HomeOS at (App\Support\HostAddress —
  request host + port), so it follows a DHCP/changed IP. MQTT_DEVICE_HOST is now
  an optional override only. Settings + device page use it.
- Discovery dedup: a finding whose prefix already belongs to a device (assigned
  OR auto-onboarded) no longer shows under "Neue Geräte" — fixes the device
  appearing both in discovery and in Geräte.
- Delete device: confirm-guarded button on the device page; deleting frees the
  discovery finding so the device can be re-added.
- "Neu scannen" button on the network page → publishes homeos/sidecar/rescan;
  the sidecar (now a subscriber) re-queries mDNS. ACL grants it read on that
  topic. paho-mqtt pinned to the v2 callback API.

+4 tests (host override/fallback, dedup, delete frees finding). Suite 62 green,
12/12 clean. Live-verified: rescan reaches sidecar; duplicate Shelly gone.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-18 09:34:07 +02:00
HomeOS Bootstrap 71b3028a8e fix: harden MQTT sharing + input roles + dedup (adversarial review)
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>
2026-07-18 08:59:12 +02:00
HomeOS Bootstrap fef393954c feat(mqtt): Home-Assistant-style onboarding — shared account + auto-create
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>
2026-07-18 08:20:57 +02:00
HomeOS Bootstrap a4489ac7d3 feat(addons): Ring backend bridge — ring-mqtt ingest, cloud devices, badge
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>
2026-07-18 01:58:40 +02:00
HomeOS Bootstrap 8aa3257fa2 feat(pwa): installable app — manifest, service worker, offline shell, icons
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>
2026-07-18 01:25:24 +02:00
HomeOS Bootstrap a4edbbf801 Fix R15: provision a per-device MQTT credential when assigning a device
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>
2026-07-18 00:50:53 +02:00
HomeOS Bootstrap 23ef993cb2 Fix R15: per-device MQTT ACLs, monotonic presence, real-device online semantics
- 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>
2026-07-17 23:08:29 +02:00
HomeOS Bootstrap 1eec714d4a Fix R15: pass MQTT passwords to the broker without shell interpolation
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>
2026-07-17 22:54:03 +02:00
HomeOS Bootstrap ea9e35883b Phase 3: MQTT ingest — Mosquitto bus, Shelly driver, live device state
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>
2026-07-17 22:51:14 +02:00
HomeOS Bootstrap 1b69c39a93 Fix R15 findings: fresh-clone bootstrap + never seed a blank admin password
- 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>
2026-07-17 21:24:10 +02:00
HomeOS Bootstrap beac04ec51 Phase 1: bootstrap — Docker stack, Laravel 13, design system, auth
Foundation for HomeOS (handoff §13.1). Verified in a real browser (R12):
HTTP 200, zero console errors, zero failed requests, breakpoints 1280/768/375,
no leaked template/lang literals. Feature test suite green (7 passing).

Stack (locked, handoff §12): Laravel 13, Livewire v3 (class-based, no Volt),
Tailwind v4 (@theme, no config), PostgreSQL 17 + TimescaleDB, Redis + Horizon,
Reverb (private channels, proxied same-origin via nginx), Vite. Everything runs
in containers (R8); one app image shared by app/horizon/scheduler/reverb.

- docker: app (php-fpm 8.4 + nginx + supervisor), db (timescale pg17), redis,
  reverb, horizon, scheduler; env-driven ports; HOST_UID/GID from id nexxo
- design system ported verbatim from design-mockup.html into @theme (§8);
  self-hosted Plus Jakarta Sans + IBM Plex Mono woff2 (R14)
- Blade component kit: icon, panel, kpi, status-dot/pill, badge, toggle,
  room-card, device-chip, sidebar, topbar; app + guest layouts (mobile drawer)
- auth: class-based Livewire Login (throttled) + Dashboard; English routes,
  German UI; full DE/EN localization (R16)
- rules.md + CLAUDE.md; admin user seeded from gitignored .env

Deferred by design: php-mqtt/laravel-client (Phase 3), mosquitto/mqtt-listener
(Phase 3), discovery sidecar (Phase 4).

R15 (Codex review) pending user `codex login`.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-17 21:06:20 +02:00