Fix R15: require prod hardening in docs; drop N+1 in warnings

- Document that production must set APP_ENV=production + APP_DEBUG=false (else
  exceptions leak and the demo household seeds); note it in .env.example too.
- HomeStatus::warnings now iterates devices → entities using the already-loaded
  device, instead of $entity->device, removing an N+1 on dashboard render.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
feat/phase-1-bootstrap
HomeOS Bootstrap 2026-07-17 22:03:20 +02:00
parent 52af82115d
commit 2a39f1cc4e
3 changed files with 33 additions and 20 deletions

View File

@ -1,4 +1,6 @@
APP_NAME=HomeOS
# Development defaults. For production set APP_ENV=production and APP_DEBUG=false
# (otherwise exceptions are exposed and the demo household is seeded).
APP_ENV=local
APP_KEY=
APP_DEBUG=true

View File

@ -22,8 +22,17 @@ Then open **http://localhost** and sign in with the seeded dev admin:
- **admin@homeos.local** / **homeos-dev** ← change this for anything but local dev.
For production, set strong values in `.env` for `DB_PASSWORD`, the `REVERB_APP_*`
keys and `HOMEOS_ADMIN_PASSWORD`, and run `php artisan key:generate`.
### Production
`.env.example` ships **development** defaults. For a real deployment you **must**:
- set `APP_ENV=production` and `APP_DEBUG=false` (otherwise exceptions are exposed
and the demo household is seeded into your real database);
- set strong values for `DB_PASSWORD`, the `REVERB_APP_*` keys and
`HOMEOS_ADMIN_PASSWORD` (seeding aborts if the admin password is unset outside
local/testing), and run `php artisan key:generate`.
The demo seeder (`DemoHomeSeeder`) only runs in local/testing.
## Everyday commands (in-container, R8)

View File

@ -40,33 +40,35 @@ class HomeStatus
/** @return array<int, array{level:string,icon:string,title:string,meta:?string,link:?string}> */
public function warnings(Collection $devices): array
{
$entities = $devices->flatMap->entities;
$warnings = [];
foreach ($devices->reject->isOnline() as $device) {
$warnings[] = [
'level' => 'offline', 'icon' => 'devices',
'title' => $device->name, 'meta' => __('devices.offline'), 'link' => null,
];
}
foreach ($entities as $entity) {
if ($entity->type === 'contact' && data_get($entity->state, 'state.open') === true) {
// Iterate devices (already loaded) → entities so we never lazy-load $entity->device (no N+1).
foreach ($devices as $device) {
if (! $device->isOnline()) {
$warnings[] = [
'level' => 'warning', 'icon' => 'window',
'title' => $entity->device->name, 'meta' => __('devices.open'), 'link' => null,
'level' => 'offline', 'icon' => 'devices',
'title' => $device->name, 'meta' => __('devices.offline'), 'link' => null,
];
}
if ($entity->type === 'battery') {
$percent = (int) data_get($entity->state, 'state.percent', 100);
if ($percent < self::LOW_BATTERY) {
foreach ($device->entities as $entity) {
if ($entity->type === 'contact' && data_get($entity->state, 'state.open') === true) {
$warnings[] = [
'level' => 'warning', 'icon' => 'alert',
'title' => $entity->device->name,
'meta' => __('devices.battery').': '.$percent.'%', 'link' => null,
'level' => 'warning', 'icon' => 'window',
'title' => $device->name, 'meta' => __('devices.open'), 'link' => null,
];
}
if ($entity->type === 'battery') {
$percent = (int) data_get($entity->state, 'state.percent', 100);
if ($percent < self::LOW_BATTERY) {
$warnings[] = [
'level' => 'warning', 'icon' => 'alert',
'title' => $device->name,
'meta' => __('devices.battery').': '.$percent.'%', 'link' => null,
];
}
}
}
}