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>feat/phase-1-bootstrap
parent
ede174e50b
commit
beac04ec51
|
|
@ -0,0 +1,18 @@
|
||||||
|
root = true
|
||||||
|
|
||||||
|
[*]
|
||||||
|
charset = utf-8
|
||||||
|
end_of_line = lf
|
||||||
|
indent_size = 4
|
||||||
|
indent_style = space
|
||||||
|
insert_final_newline = true
|
||||||
|
trim_trailing_whitespace = true
|
||||||
|
|
||||||
|
[*.md]
|
||||||
|
trim_trailing_whitespace = false
|
||||||
|
|
||||||
|
[*.{yml,yaml}]
|
||||||
|
indent_size = 2
|
||||||
|
|
||||||
|
[{compose,docker-compose}.{yml,yaml}]
|
||||||
|
indent_size = 4
|
||||||
|
|
@ -0,0 +1,74 @@
|
||||||
|
APP_NAME=HomeOS
|
||||||
|
APP_ENV=local
|
||||||
|
APP_KEY=
|
||||||
|
APP_DEBUG=true
|
||||||
|
APP_URL=http://localhost
|
||||||
|
|
||||||
|
APP_LOCALE=de
|
||||||
|
APP_FALLBACK_LOCALE=en
|
||||||
|
APP_FAKER_LOCALE=de_DE
|
||||||
|
APP_TIMEZONE=UTC
|
||||||
|
|
||||||
|
APP_MAINTENANCE_DRIVER=file
|
||||||
|
|
||||||
|
BCRYPT_ROUNDS=12
|
||||||
|
|
||||||
|
LOG_CHANNEL=stack
|
||||||
|
LOG_STACK=single
|
||||||
|
LOG_DEPRECATIONS_CHANNEL=null
|
||||||
|
LOG_LEVEL=debug
|
||||||
|
|
||||||
|
DB_CONNECTION=pgsql
|
||||||
|
DB_HOST=db
|
||||||
|
DB_PORT=5432
|
||||||
|
DB_DATABASE=homeos
|
||||||
|
DB_USERNAME=homeos
|
||||||
|
DB_PASSWORD=
|
||||||
|
|
||||||
|
SESSION_DRIVER=database
|
||||||
|
SESSION_LIFETIME=120
|
||||||
|
SESSION_ENCRYPT=false
|
||||||
|
SESSION_PATH=/
|
||||||
|
SESSION_DOMAIN=null
|
||||||
|
|
||||||
|
BROADCAST_CONNECTION=reverb
|
||||||
|
FILESYSTEM_DISK=local
|
||||||
|
QUEUE_CONNECTION=redis
|
||||||
|
|
||||||
|
CACHE_STORE=redis
|
||||||
|
|
||||||
|
REDIS_CLIENT=phpredis
|
||||||
|
REDIS_HOST=redis
|
||||||
|
REDIS_PASSWORD=null
|
||||||
|
REDIS_PORT=6379
|
||||||
|
|
||||||
|
MAIL_MAILER=log
|
||||||
|
MAIL_FROM_ADDRESS="homeos@localhost"
|
||||||
|
MAIL_FROM_NAME="${APP_NAME}"
|
||||||
|
|
||||||
|
# --- Reverb (private channels), proxied same-origin via nginx (/app, /apps) ---
|
||||||
|
REVERB_APP_ID=
|
||||||
|
REVERB_APP_KEY=
|
||||||
|
REVERB_APP_SECRET=
|
||||||
|
REVERB_HOST=reverb
|
||||||
|
REVERB_PORT=8080
|
||||||
|
REVERB_SCHEME=http
|
||||||
|
REVERB_SERVER_HOST=0.0.0.0
|
||||||
|
REVERB_SERVER_PORT=8080
|
||||||
|
|
||||||
|
# client (Vite/Echo) — Echo connects same-origin, so only the key is needed here
|
||||||
|
VITE_APP_NAME="${APP_NAME}"
|
||||||
|
VITE_REVERB_APP_KEY="${REVERB_APP_KEY}"
|
||||||
|
|
||||||
|
# ================= docker compose (deployment) =================
|
||||||
|
HOST_UID=1000
|
||||||
|
HOST_GID=1000
|
||||||
|
APP_PORT=80
|
||||||
|
VITE_PORT=5173
|
||||||
|
REVERB_HOST_PORT=6001
|
||||||
|
DB_HOST_PORT=5432
|
||||||
|
|
||||||
|
# --- HomeOS admin (dev seed; set a strong value, change for production) ---
|
||||||
|
HOMEOS_ADMIN_EMAIL=admin@homeos.local
|
||||||
|
HOMEOS_ADMIN_NAME=Admin
|
||||||
|
HOMEOS_ADMIN_PASSWORD=
|
||||||
|
|
@ -0,0 +1,11 @@
|
||||||
|
* text=auto eol=lf
|
||||||
|
|
||||||
|
*.blade.php diff=html
|
||||||
|
*.css diff=css
|
||||||
|
*.html diff=html
|
||||||
|
*.md diff=markdown
|
||||||
|
*.php diff=php
|
||||||
|
|
||||||
|
/.github export-ignore
|
||||||
|
CHANGELOG.md export-ignore
|
||||||
|
.styleci.yml export-ignore
|
||||||
|
|
@ -0,0 +1,34 @@
|
||||||
|
*.log
|
||||||
|
.DS_Store
|
||||||
|
.env
|
||||||
|
.env.backup
|
||||||
|
.env.production
|
||||||
|
.phpactor.json
|
||||||
|
.phpunit.result.cache
|
||||||
|
/.codex
|
||||||
|
/.cursor/
|
||||||
|
/.idea
|
||||||
|
/.nova
|
||||||
|
/.phpunit.cache
|
||||||
|
/.vscode
|
||||||
|
/.zed
|
||||||
|
/auth.json
|
||||||
|
/node_modules
|
||||||
|
/public/build
|
||||||
|
/public/fonts-manifest.dev.json
|
||||||
|
/public/hot
|
||||||
|
/public/storage
|
||||||
|
/storage/*.key
|
||||||
|
/storage/pail
|
||||||
|
/vendor
|
||||||
|
_ide_helper.php
|
||||||
|
Homestead.json
|
||||||
|
Homestead.yaml
|
||||||
|
Thumbs.db
|
||||||
|
|
||||||
|
# HomeOS
|
||||||
|
/docker-compose.override.yml
|
||||||
|
/_bootstrap
|
||||||
|
/_verify.mjs
|
||||||
|
/sidecar/__pycache__
|
||||||
|
*.pyc
|
||||||
|
|
@ -0,0 +1,61 @@
|
||||||
|
# CLAUDE.md — HomeOS
|
||||||
|
|
||||||
|
Self-built smart-home control plane. Single household, self-hosted, LAN-first.
|
||||||
|
**Not** Home Assistant / not a wrapper: own data model, own integrations, own UI.
|
||||||
|
|
||||||
|
Authoritative build spec: **`handoff.md`**. Non-negotiable conventions: **`rules.md`**.
|
||||||
|
Approved design direction: **`design-mockup.html`** (open in a browser).
|
||||||
|
Read all three before non-trivial work.
|
||||||
|
|
||||||
|
## Locked decisions (see handoff §12)
|
||||||
|
|
||||||
|
- **Framework:** Laravel 13 · **Livewire v3** class-based (NOT Volt) · **Tailwind v4** (CSS-first `@theme`, no `tailwind.config.js`) · Vite
|
||||||
|
- **DB:** PostgreSQL 17 + TimescaleDB (telemetry hypertables + compression/retention)
|
||||||
|
- **Realtime:** Laravel Reverb + Echo — **every channel PRIVATE**
|
||||||
|
- **Queue/Cache:** Redis (+ Horizon)
|
||||||
|
- **MQTT:** Eclipse Mosquitto 2 · client `php-mqtt/laravel-client` · Python discovery sidecar
|
||||||
|
- **Git:** local repo, no remote yet · feature branches, never commit to `main`
|
||||||
|
- **Name:** "HomeOS" is a working title
|
||||||
|
|
||||||
|
## Everything runs in containers (R8 — host has only Docker)
|
||||||
|
|
||||||
|
The host has **no PHP / Composer / Node** for project work. Always exec into the stack:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker compose up -d # start stack
|
||||||
|
docker compose exec app php artisan … # artisan
|
||||||
|
docker compose exec app composer … # composer
|
||||||
|
docker compose exec app npm … # node/vite (or: docker compose exec app npm run build)
|
||||||
|
docker compose logs -f app # tail
|
||||||
|
```
|
||||||
|
|
||||||
|
The `app`, `horizon`, `scheduler`, `reverb` (and later `mqtt-listener`) services share one image
|
||||||
|
(`docker/app/Dockerfile`). Ports are env-driven in `.env` (`APP_PORT`, `VITE_PORT`,
|
||||||
|
`REVERB_HOST_PORT`, `DB_HOST_PORT`). `HOST_UID`/`HOST_GID` come from `id nexxo` — never hardcode.
|
||||||
|
|
||||||
|
## Architecture (handoff §3)
|
||||||
|
|
||||||
|
MQTT-first hybrid. Mosquitto is the bus. `mqtt-listener` (artisan daemon) parses + dispatches only
|
||||||
|
(H2) → Horizon jobs upsert `device_states` → Reverb private channels → Livewire (Echo).
|
||||||
|
UI action → `DeviceCommandService` → driver (`ShellyMqttDriver` first) → device, every command
|
||||||
|
audited in `commands` (H1). Vendor specifics only in `app/Support/Mqtt` + `app/Support/Drivers` (H3).
|
||||||
|
|
||||||
|
## Design system (handoff §8)
|
||||||
|
|
||||||
|
"Enterprise Ops, dark OLED" — UniFi/Grafana-class console, not a consumer app. Tokens live in
|
||||||
|
`resources/css/app.css` `@theme` (ported verbatim from the mockup). Status via colored dot/pill +
|
||||||
|
text (never color alone, never emoji, R9). Every numeric value / MAC / IP / timestamp in
|
||||||
|
`font-mono` with `tabular-nums`. Hairline borders, no shadows. Accent (cyan) sparingly.
|
||||||
|
|
||||||
|
## Non-negotiable rules
|
||||||
|
|
||||||
|
See **`rules.md`** (R1–R17 + H1–H5). Highlights: class-based Livewire pages (R1/R2), tokens-only
|
||||||
|
styling (R3/R10), wire-elements/modal for confirms (R5), UUID route keys (R11), browser-verify every
|
||||||
|
page (R12), English routes / German labels (R13), self-hosted fonts (R14), Codex review gate (R15),
|
||||||
|
full localization DE+EN (R16). **On any conflict: STOP and ask.**
|
||||||
|
|
||||||
|
## Build order (handoff §13)
|
||||||
|
|
||||||
|
1. Bootstrap → 2. Domain core → 3. MQTT ingest → 4. Discovery → 5. Presence → 6. Automations →
|
||||||
|
7. Polish + PWA. Each phase ends **browser-verified (R12) + Codex-clean (R15) + committed on a
|
||||||
|
feature branch**.
|
||||||
|
|
@ -0,0 +1,58 @@
|
||||||
|
<p align="center"><a href="https://laravel.com" target="_blank"><img src="https://raw.githubusercontent.com/laravel/art/master/logo-lockup/5%20SVG/2%20CMYK/1%20Full%20Color/laravel-logolockup-cmyk-red.svg" width="400" alt="Laravel Logo"></a></p>
|
||||||
|
|
||||||
|
<p align="center">
|
||||||
|
<a href="https://github.com/laravel/framework/actions"><img src="https://github.com/laravel/framework/workflows/tests/badge.svg" alt="Build Status"></a>
|
||||||
|
<a href="https://packagist.org/packages/laravel/framework"><img src="https://img.shields.io/packagist/dt/laravel/framework" alt="Total Downloads"></a>
|
||||||
|
<a href="https://packagist.org/packages/laravel/framework"><img src="https://img.shields.io/packagist/v/laravel/framework" alt="Latest Stable Version"></a>
|
||||||
|
<a href="https://packagist.org/packages/laravel/framework"><img src="https://img.shields.io/packagist/l/laravel/framework" alt="License"></a>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
## About Laravel
|
||||||
|
|
||||||
|
Laravel is a web application framework with expressive, elegant syntax. We believe development must be an enjoyable and creative experience to be truly fulfilling. Laravel takes the pain out of development by easing common tasks used in many web projects, such as:
|
||||||
|
|
||||||
|
- [Simple, fast routing engine](https://laravel.com/docs/routing).
|
||||||
|
- [Powerful dependency injection container](https://laravel.com/docs/container).
|
||||||
|
- Multiple back-ends for [session](https://laravel.com/docs/session) and [cache](https://laravel.com/docs/cache) storage.
|
||||||
|
- Expressive, intuitive [database ORM](https://laravel.com/docs/eloquent).
|
||||||
|
- Database agnostic [schema migrations](https://laravel.com/docs/migrations).
|
||||||
|
- [Robust background job processing](https://laravel.com/docs/queues).
|
||||||
|
- [Real-time event broadcasting](https://laravel.com/docs/broadcasting).
|
||||||
|
|
||||||
|
Laravel is accessible, powerful, and provides tools required for large, robust applications.
|
||||||
|
|
||||||
|
## Learning Laravel
|
||||||
|
|
||||||
|
Laravel has the most extensive and thorough [documentation](https://laravel.com/docs) and video tutorial library of all modern web application frameworks, making it a breeze to get started with the framework.
|
||||||
|
|
||||||
|
In addition, [Laracasts](https://laracasts.com) contains thousands of video tutorials on a range of topics including Laravel, modern PHP, unit testing, and JavaScript. Boost your skills by digging into our comprehensive video library.
|
||||||
|
|
||||||
|
You can also watch bite-sized lessons with real-world projects on [Laravel Learn](https://laravel.com/learn), where you will be guided through building a Laravel application from scratch while learning PHP fundamentals.
|
||||||
|
|
||||||
|
## Agentic Development
|
||||||
|
|
||||||
|
Laravel's predictable structure and conventions make it ideal for AI coding agents like Claude Code, Cursor, and GitHub Copilot. Install [Laravel Boost](https://laravel.com/docs/ai) to supercharge your AI workflow:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
composer require laravel/boost --dev
|
||||||
|
|
||||||
|
php artisan boost:install
|
||||||
|
```
|
||||||
|
|
||||||
|
Boost provides your agent 15+ tools and skills that help agents build Laravel applications while following best practices.
|
||||||
|
|
||||||
|
## Contributing
|
||||||
|
|
||||||
|
Thank you for considering contributing to the Laravel framework! The contribution guide can be found in the [Laravel documentation](https://laravel.com/docs/contributions).
|
||||||
|
|
||||||
|
## Code of Conduct
|
||||||
|
|
||||||
|
In order to ensure that the Laravel community is welcoming to all, please review and abide by the [Code of Conduct](https://laravel.com/docs/contributions#code-of-conduct).
|
||||||
|
|
||||||
|
## Security Vulnerabilities
|
||||||
|
|
||||||
|
If you discover a security vulnerability within Laravel, please send an e-mail to Taylor Otwell via [taylor@laravel.com](mailto:taylor@laravel.com). All security vulnerabilities will be promptly addressed.
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
The Laravel framework is open-sourced software licensed under the [MIT license](https://opensource.org/licenses/MIT).
|
||||||
|
|
@ -0,0 +1,8 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers;
|
||||||
|
|
||||||
|
abstract class Controller
|
||||||
|
{
|
||||||
|
//
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,64 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Livewire\Auth;
|
||||||
|
|
||||||
|
use Illuminate\Support\Facades\Auth;
|
||||||
|
use Illuminate\Support\Facades\RateLimiter;
|
||||||
|
use Illuminate\Support\Str;
|
||||||
|
use Illuminate\Validation\ValidationException;
|
||||||
|
use Livewire\Attributes\Layout;
|
||||||
|
use Livewire\Attributes\Validate;
|
||||||
|
use Livewire\Component;
|
||||||
|
|
||||||
|
#[Layout('layouts.guest')]
|
||||||
|
class Login extends Component
|
||||||
|
{
|
||||||
|
#[Validate('required|string|email')]
|
||||||
|
public string $email = '';
|
||||||
|
|
||||||
|
#[Validate('required|string')]
|
||||||
|
public string $password = '';
|
||||||
|
|
||||||
|
public bool $remember = false;
|
||||||
|
|
||||||
|
public function login()
|
||||||
|
{
|
||||||
|
$this->validate();
|
||||||
|
|
||||||
|
$this->ensureIsNotRateLimited();
|
||||||
|
|
||||||
|
if (! Auth::attempt(['email' => $this->email, 'password' => $this->password], $this->remember)) {
|
||||||
|
RateLimiter::hit($this->throttleKey());
|
||||||
|
|
||||||
|
throw ValidationException::withMessages([
|
||||||
|
'email' => __('auth.failed'),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
RateLimiter::clear($this->throttleKey());
|
||||||
|
session()->regenerate();
|
||||||
|
|
||||||
|
return redirect()->intended(route('dashboard'));
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function ensureIsNotRateLimited(): void
|
||||||
|
{
|
||||||
|
if (! RateLimiter::tooManyAttempts($this->throttleKey(), 5)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw ValidationException::withMessages([
|
||||||
|
'email' => __('auth.throttle', ['seconds' => RateLimiter::availableIn($this->throttleKey())]),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function throttleKey(): string
|
||||||
|
{
|
||||||
|
return Str::transliterate(Str::lower($this->email).'|'.request()->ip());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function render()
|
||||||
|
{
|
||||||
|
return view('livewire.auth.login');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,23 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Livewire;
|
||||||
|
|
||||||
|
use Livewire\Attributes\Layout;
|
||||||
|
use Livewire\Component;
|
||||||
|
|
||||||
|
#[Layout('layouts.app')]
|
||||||
|
class Dashboard extends Component
|
||||||
|
{
|
||||||
|
/** Interactive demo toggle — verifies Livewire reactivity in Phase 1. */
|
||||||
|
public bool $demoOn = true;
|
||||||
|
|
||||||
|
public function toggleDemo(): void
|
||||||
|
{
|
||||||
|
$this->demoOn = ! $this->demoOn;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function render()
|
||||||
|
{
|
||||||
|
return view('livewire.dashboard');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,32 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
// use Illuminate\Contracts\Auth\MustVerifyEmail;
|
||||||
|
use Database\Factories\UserFactory;
|
||||||
|
use Illuminate\Database\Eloquent\Attributes\Fillable;
|
||||||
|
use Illuminate\Database\Eloquent\Attributes\Hidden;
|
||||||
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
|
use Illuminate\Foundation\Auth\User as Authenticatable;
|
||||||
|
use Illuminate\Notifications\Notifiable;
|
||||||
|
|
||||||
|
#[Fillable(['name', 'email', 'password'])]
|
||||||
|
#[Hidden(['password', 'remember_token'])]
|
||||||
|
class User extends Authenticatable
|
||||||
|
{
|
||||||
|
/** @use HasFactory<UserFactory> */
|
||||||
|
use HasFactory, Notifiable;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the attributes that should be cast.
|
||||||
|
*
|
||||||
|
* @return array<string, string>
|
||||||
|
*/
|
||||||
|
protected function casts(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'email_verified_at' => 'datetime',
|
||||||
|
'password' => 'hashed',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,24 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Providers;
|
||||||
|
|
||||||
|
use Illuminate\Support\ServiceProvider;
|
||||||
|
|
||||||
|
class AppServiceProvider extends ServiceProvider
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Register any application services.
|
||||||
|
*/
|
||||||
|
public function register(): void
|
||||||
|
{
|
||||||
|
//
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Bootstrap any application services.
|
||||||
|
*/
|
||||||
|
public function boot(): void
|
||||||
|
{
|
||||||
|
//
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,36 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Providers;
|
||||||
|
|
||||||
|
use Illuminate\Support\Facades\Gate;
|
||||||
|
use Laravel\Horizon\Horizon;
|
||||||
|
use Laravel\Horizon\HorizonApplicationServiceProvider;
|
||||||
|
|
||||||
|
class HorizonServiceProvider extends HorizonApplicationServiceProvider
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Bootstrap any application services.
|
||||||
|
*/
|
||||||
|
public function boot(): void
|
||||||
|
{
|
||||||
|
parent::boot();
|
||||||
|
|
||||||
|
// Horizon::routeSmsNotificationsTo('15556667777');
|
||||||
|
// Horizon::routeMailNotificationsTo('example@example.com');
|
||||||
|
// Horizon::routeSlackNotificationsTo('slack-webhook-url', '#channel');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Register the Horizon gate.
|
||||||
|
*
|
||||||
|
* This gate determines who can access Horizon in non-local environments.
|
||||||
|
*/
|
||||||
|
protected function gate(): void
|
||||||
|
{
|
||||||
|
Gate::define('viewHorizon', function ($user = null) {
|
||||||
|
return in_array(optional($user)->email, [
|
||||||
|
//
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,18 @@
|
||||||
|
#!/usr/bin/env php
|
||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Foundation\Application;
|
||||||
|
use Symfony\Component\Console\Input\ArgvInput;
|
||||||
|
|
||||||
|
define('LARAVEL_START', microtime(true));
|
||||||
|
|
||||||
|
// Register the Composer autoloader...
|
||||||
|
require __DIR__.'/vendor/autoload.php';
|
||||||
|
|
||||||
|
// Bootstrap Laravel and handle the command...
|
||||||
|
/** @var Application $app */
|
||||||
|
$app = require_once __DIR__.'/bootstrap/app.php';
|
||||||
|
|
||||||
|
$status = $app->handleCommand(new ArgvInput);
|
||||||
|
|
||||||
|
exit($status);
|
||||||
|
|
@ -0,0 +1,22 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Foundation\Application;
|
||||||
|
use Illuminate\Foundation\Configuration\Exceptions;
|
||||||
|
use Illuminate\Foundation\Configuration\Middleware;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
|
||||||
|
return Application::configure(basePath: dirname(__DIR__))
|
||||||
|
->withRouting(
|
||||||
|
web: __DIR__.'/../routes/web.php',
|
||||||
|
commands: __DIR__.'/../routes/console.php',
|
||||||
|
channels: __DIR__.'/../routes/channels.php',
|
||||||
|
health: '/up',
|
||||||
|
)
|
||||||
|
->withMiddleware(function (Middleware $middleware): void {
|
||||||
|
//
|
||||||
|
})
|
||||||
|
->withExceptions(function (Exceptions $exceptions): void {
|
||||||
|
$exceptions->shouldRenderJsonWhen(
|
||||||
|
fn (Request $request) => $request->is('api/*'),
|
||||||
|
);
|
||||||
|
})->create();
|
||||||
|
|
@ -0,0 +1,2 @@
|
||||||
|
*
|
||||||
|
!.gitignore
|
||||||
|
|
@ -0,0 +1,6 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
return [
|
||||||
|
App\Providers\AppServiceProvider::class,
|
||||||
|
App\Providers\HorizonServiceProvider::class,
|
||||||
|
];
|
||||||
|
|
@ -0,0 +1,90 @@
|
||||||
|
{
|
||||||
|
"$schema": "https://getcomposer.org/schema.json",
|
||||||
|
"name": "laravel/laravel",
|
||||||
|
"type": "project",
|
||||||
|
"description": "The skeleton application for the Laravel framework.",
|
||||||
|
"keywords": ["laravel", "framework"],
|
||||||
|
"license": "MIT",
|
||||||
|
"require": {
|
||||||
|
"php": "^8.3",
|
||||||
|
"laravel/framework": "^13.8",
|
||||||
|
"laravel/horizon": "^5.47",
|
||||||
|
"laravel/reverb": "^1.10",
|
||||||
|
"laravel/tinker": "^3.0",
|
||||||
|
"livewire/livewire": "^4.3",
|
||||||
|
"wire-elements/modal": "^3.0"
|
||||||
|
},
|
||||||
|
"require-dev": {
|
||||||
|
"fakerphp/faker": "^1.23",
|
||||||
|
"laravel/pail": "^1.2.5",
|
||||||
|
"laravel/pao": "^1.0.6",
|
||||||
|
"laravel/pint": "^1.27",
|
||||||
|
"mockery/mockery": "^1.6",
|
||||||
|
"nunomaduro/collision": "^8.6",
|
||||||
|
"phpunit/phpunit": "^12.5.12"
|
||||||
|
},
|
||||||
|
"autoload": {
|
||||||
|
"psr-4": {
|
||||||
|
"App\\": "app/",
|
||||||
|
"Database\\Factories\\": "database/factories/",
|
||||||
|
"Database\\Seeders\\": "database/seeders/"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"autoload-dev": {
|
||||||
|
"psr-4": {
|
||||||
|
"Tests\\": "tests/"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"setup": [
|
||||||
|
"composer install",
|
||||||
|
"@php -r \"file_exists('.env') || copy('.env.example', '.env');\"",
|
||||||
|
"@php artisan key:generate",
|
||||||
|
"@php artisan migrate --force",
|
||||||
|
"npm install --ignore-scripts",
|
||||||
|
"npm run build"
|
||||||
|
],
|
||||||
|
"dev": [
|
||||||
|
"Composer\\Config::disableProcessTimeout",
|
||||||
|
"npx concurrently -c \"#93c5fd,#c4b5fd,#fb7185,#fdba74\" \"php artisan serve\" \"php artisan queue:listen --tries=1 --timeout=0\" \"php artisan pail --timeout=0\" \"npm run dev\" --names=server,queue,logs,vite --kill-others"
|
||||||
|
],
|
||||||
|
"test": [
|
||||||
|
"@php artisan config:clear --ansi @no_additional_args",
|
||||||
|
"@php artisan test"
|
||||||
|
],
|
||||||
|
"post-autoload-dump": [
|
||||||
|
"Illuminate\\Foundation\\ComposerScripts::postAutoloadDump",
|
||||||
|
"@php artisan package:discover --ansi"
|
||||||
|
],
|
||||||
|
"post-update-cmd": [
|
||||||
|
"@php artisan vendor:publish --tag=laravel-assets --ansi --force"
|
||||||
|
],
|
||||||
|
"post-root-package-install": [
|
||||||
|
"@php -r \"file_exists('.env') || copy('.env.example', '.env');\""
|
||||||
|
],
|
||||||
|
"post-create-project-cmd": [
|
||||||
|
"@php artisan key:generate --ansi",
|
||||||
|
"@php -r \"file_exists('database/database.sqlite') || touch('database/database.sqlite');\"",
|
||||||
|
"@php artisan migrate --graceful --ansi"
|
||||||
|
],
|
||||||
|
"pre-package-uninstall": [
|
||||||
|
"Illuminate\\Foundation\\ComposerScripts::prePackageUninstall"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"extra": {
|
||||||
|
"laravel": {
|
||||||
|
"dont-discover": []
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"config": {
|
||||||
|
"optimize-autoloader": true,
|
||||||
|
"preferred-install": "dist",
|
||||||
|
"sort-packages": true,
|
||||||
|
"allow-plugins": {
|
||||||
|
"pestphp/pest-plugin": true,
|
||||||
|
"php-http/discovery": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"minimum-stability": "stable",
|
||||||
|
"prefer-stable": true
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,126 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
return [
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Application Name
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| This value is the name of your application, which will be used when the
|
||||||
|
| framework needs to place the application's name in a notification or
|
||||||
|
| other UI elements where an application name needs to be displayed.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'name' => env('APP_NAME', 'Laravel'),
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Application Environment
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| This value determines the "environment" your application is currently
|
||||||
|
| running in. This may determine how you prefer to configure various
|
||||||
|
| services the application utilizes. Set this in your ".env" file.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'env' => env('APP_ENV', 'production'),
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Application Debug Mode
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| When your application is in debug mode, detailed error messages with
|
||||||
|
| stack traces will be shown on every error that occurs within your
|
||||||
|
| application. If disabled, a simple generic error page is shown.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'debug' => (bool) env('APP_DEBUG', false),
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Application URL
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| This URL is used by the console to properly generate URLs when using
|
||||||
|
| the Artisan command line tool. You should set this to the root of
|
||||||
|
| the application so that it's available within Artisan commands.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'url' => env('APP_URL', 'http://localhost'),
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Application Timezone
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| Here you may specify the default timezone for your application, which
|
||||||
|
| will be used by the PHP date and date-time functions. The timezone
|
||||||
|
| is set to "UTC" by default as it is suitable for most use cases.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'timezone' => 'UTC',
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Application Locale Configuration
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| The application locale determines the default locale that will be used
|
||||||
|
| by Laravel's translation / localization methods. This option can be
|
||||||
|
| set to any locale for which you plan to have translation strings.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'locale' => env('APP_LOCALE', 'en'),
|
||||||
|
|
||||||
|
'fallback_locale' => env('APP_FALLBACK_LOCALE', 'en'),
|
||||||
|
|
||||||
|
'faker_locale' => env('APP_FAKER_LOCALE', 'en_US'),
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Encryption Key
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| This key is utilized by Laravel's encryption services and should be set
|
||||||
|
| to a random, 32 character string to ensure that all encrypted values
|
||||||
|
| are secure. You should do this prior to deploying the application.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'cipher' => 'AES-256-CBC',
|
||||||
|
|
||||||
|
'key' => env('APP_KEY'),
|
||||||
|
|
||||||
|
'previous_keys' => [
|
||||||
|
...array_filter(
|
||||||
|
explode(',', (string) env('APP_PREVIOUS_KEYS', ''))
|
||||||
|
),
|
||||||
|
],
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Maintenance Mode Driver
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| These configuration options determine the driver used to determine and
|
||||||
|
| manage Laravel's "maintenance mode" status. The "cache" driver will
|
||||||
|
| allow maintenance mode to be controlled across multiple machines.
|
||||||
|
|
|
||||||
|
| Supported drivers: "file", "cache"
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'maintenance' => [
|
||||||
|
'driver' => env('APP_MAINTENANCE_DRIVER', 'file'),
|
||||||
|
'store' => env('APP_MAINTENANCE_STORE', 'database'),
|
||||||
|
],
|
||||||
|
|
||||||
|
];
|
||||||
|
|
@ -0,0 +1,117 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
use App\Models\User;
|
||||||
|
|
||||||
|
return [
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Authentication Defaults
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| This option defines the default authentication "guard" and password
|
||||||
|
| reset "broker" for your application. You may change these values
|
||||||
|
| as required, but they're a perfect start for most applications.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'defaults' => [
|
||||||
|
'guard' => env('AUTH_GUARD', 'web'),
|
||||||
|
'passwords' => env('AUTH_PASSWORD_BROKER', 'users'),
|
||||||
|
],
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Authentication Guards
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| Next, you may define every authentication guard for your application.
|
||||||
|
| Of course, a great default configuration has been defined for you
|
||||||
|
| which utilizes session storage plus the Eloquent user provider.
|
||||||
|
|
|
||||||
|
| All authentication guards have a user provider, which defines how the
|
||||||
|
| users are actually retrieved out of your database or other storage
|
||||||
|
| system used by the application. Typically, Eloquent is utilized.
|
||||||
|
|
|
||||||
|
| Supported: "session"
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'guards' => [
|
||||||
|
'web' => [
|
||||||
|
'driver' => 'session',
|
||||||
|
'provider' => 'users',
|
||||||
|
],
|
||||||
|
],
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| User Providers
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| All authentication guards have a user provider, which defines how the
|
||||||
|
| users are actually retrieved out of your database or other storage
|
||||||
|
| system used by the application. Typically, Eloquent is utilized.
|
||||||
|
|
|
||||||
|
| If you have multiple user tables or models you may configure multiple
|
||||||
|
| providers to represent the model / table. These providers may then
|
||||||
|
| be assigned to any extra authentication guards you have defined.
|
||||||
|
|
|
||||||
|
| Supported: "database", "eloquent"
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'providers' => [
|
||||||
|
'users' => [
|
||||||
|
'driver' => 'eloquent',
|
||||||
|
'model' => env('AUTH_MODEL', User::class),
|
||||||
|
],
|
||||||
|
|
||||||
|
// 'users' => [
|
||||||
|
// 'driver' => 'database',
|
||||||
|
// 'table' => 'users',
|
||||||
|
// ],
|
||||||
|
],
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Resetting Passwords
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| These configuration options specify the behavior of Laravel's password
|
||||||
|
| reset functionality, including the table utilized for token storage
|
||||||
|
| and the user provider that is invoked to actually retrieve users.
|
||||||
|
|
|
||||||
|
| The expiry time is the number of minutes that each reset token will be
|
||||||
|
| considered valid. This security feature keeps tokens short-lived so
|
||||||
|
| they have less time to be guessed. You may change this as needed.
|
||||||
|
|
|
||||||
|
| The throttle setting is the number of seconds a user must wait before
|
||||||
|
| generating more password reset tokens. This prevents the user from
|
||||||
|
| quickly generating a very large amount of password reset tokens.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'passwords' => [
|
||||||
|
'users' => [
|
||||||
|
'provider' => 'users',
|
||||||
|
'table' => env('AUTH_PASSWORD_RESET_TOKEN_TABLE', 'password_reset_tokens'),
|
||||||
|
'expire' => 60,
|
||||||
|
'throttle' => 60,
|
||||||
|
],
|
||||||
|
],
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Password Confirmation Timeout
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| Here you may define the number of seconds before a password confirmation
|
||||||
|
| window expires and users are asked to re-enter their password via the
|
||||||
|
| confirmation screen. By default, the timeout lasts for three hours.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'password_timeout' => env('AUTH_PASSWORD_TIMEOUT', 10800),
|
||||||
|
|
||||||
|
];
|
||||||
|
|
@ -0,0 +1,82 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
return [
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Default Broadcaster
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| This option controls the default broadcaster that will be used by the
|
||||||
|
| framework when an event needs to be broadcast. You may set this to
|
||||||
|
| any of the connections defined in the "connections" array below.
|
||||||
|
|
|
||||||
|
| Supported: "reverb", "pusher", "ably", "redis", "log", "null"
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'default' => env('BROADCAST_CONNECTION', 'null'),
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Broadcast Connections
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| Here you may define all of the broadcast connections that will be used
|
||||||
|
| to broadcast events to other systems or over WebSockets. Samples of
|
||||||
|
| each available type of connection are provided inside this array.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'connections' => [
|
||||||
|
|
||||||
|
'reverb' => [
|
||||||
|
'driver' => 'reverb',
|
||||||
|
'key' => env('REVERB_APP_KEY'),
|
||||||
|
'secret' => env('REVERB_APP_SECRET'),
|
||||||
|
'app_id' => env('REVERB_APP_ID'),
|
||||||
|
'options' => [
|
||||||
|
'host' => env('REVERB_HOST'),
|
||||||
|
'port' => env('REVERB_PORT', 443),
|
||||||
|
'scheme' => env('REVERB_SCHEME', 'https'),
|
||||||
|
'useTLS' => env('REVERB_SCHEME', 'https') === 'https',
|
||||||
|
],
|
||||||
|
'client_options' => [
|
||||||
|
// Guzzle client options: https://docs.guzzlephp.org/en/stable/request-options.html
|
||||||
|
],
|
||||||
|
],
|
||||||
|
|
||||||
|
'pusher' => [
|
||||||
|
'driver' => 'pusher',
|
||||||
|
'key' => env('PUSHER_APP_KEY'),
|
||||||
|
'secret' => env('PUSHER_APP_SECRET'),
|
||||||
|
'app_id' => env('PUSHER_APP_ID'),
|
||||||
|
'options' => [
|
||||||
|
'cluster' => env('PUSHER_APP_CLUSTER'),
|
||||||
|
'host' => env('PUSHER_HOST') ?: 'api-'.env('PUSHER_APP_CLUSTER', 'mt1').'.pusher.com',
|
||||||
|
'port' => env('PUSHER_PORT', 443),
|
||||||
|
'scheme' => env('PUSHER_SCHEME', 'https'),
|
||||||
|
'encrypted' => true,
|
||||||
|
'useTLS' => env('PUSHER_SCHEME', 'https') === 'https',
|
||||||
|
],
|
||||||
|
'client_options' => [
|
||||||
|
// Guzzle client options: https://docs.guzzlephp.org/en/stable/request-options.html
|
||||||
|
],
|
||||||
|
],
|
||||||
|
|
||||||
|
'ably' => [
|
||||||
|
'driver' => 'ably',
|
||||||
|
'key' => env('ABLY_KEY'),
|
||||||
|
],
|
||||||
|
|
||||||
|
'log' => [
|
||||||
|
'driver' => 'log',
|
||||||
|
],
|
||||||
|
|
||||||
|
'null' => [
|
||||||
|
'driver' => 'null',
|
||||||
|
],
|
||||||
|
|
||||||
|
],
|
||||||
|
|
||||||
|
];
|
||||||
|
|
@ -0,0 +1,136 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Support\Str;
|
||||||
|
|
||||||
|
return [
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Default Cache Store
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| This option controls the default cache store that will be used by the
|
||||||
|
| framework. This connection is utilized if another isn't explicitly
|
||||||
|
| specified when running a cache operation inside the application.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'default' => env('CACHE_STORE', 'database'),
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Cache Stores
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| Here you may define all of the cache "stores" for your application as
|
||||||
|
| well as their drivers. You may even define multiple stores for the
|
||||||
|
| same cache driver to group types of items stored in your caches.
|
||||||
|
|
|
||||||
|
| Supported drivers: "array", "database", "file", "memcached",
|
||||||
|
| "redis", "dynamodb", "storage", "octane",
|
||||||
|
| "session", "failover", "null"
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'stores' => [
|
||||||
|
|
||||||
|
'array' => [
|
||||||
|
'driver' => 'array',
|
||||||
|
'serialize' => false,
|
||||||
|
],
|
||||||
|
|
||||||
|
'database' => [
|
||||||
|
'driver' => 'database',
|
||||||
|
'connection' => env('DB_CACHE_CONNECTION'),
|
||||||
|
'table' => env('DB_CACHE_TABLE', 'cache'),
|
||||||
|
'lock_connection' => env('DB_CACHE_LOCK_CONNECTION'),
|
||||||
|
'lock_table' => env('DB_CACHE_LOCK_TABLE'),
|
||||||
|
],
|
||||||
|
|
||||||
|
'file' => [
|
||||||
|
'driver' => 'file',
|
||||||
|
'path' => storage_path('framework/cache/data'),
|
||||||
|
'lock_path' => storage_path('framework/cache/data'),
|
||||||
|
],
|
||||||
|
|
||||||
|
'storage' => [
|
||||||
|
'driver' => 'storage',
|
||||||
|
'disk' => env('CACHE_STORAGE_DISK'),
|
||||||
|
'path' => env('CACHE_STORAGE_PATH', 'framework/cache/data'),
|
||||||
|
],
|
||||||
|
|
||||||
|
'memcached' => [
|
||||||
|
'driver' => 'memcached',
|
||||||
|
'persistent_id' => env('MEMCACHED_PERSISTENT_ID'),
|
||||||
|
'sasl' => [
|
||||||
|
env('MEMCACHED_USERNAME'),
|
||||||
|
env('MEMCACHED_PASSWORD'),
|
||||||
|
],
|
||||||
|
'options' => [
|
||||||
|
// Memcached::OPT_CONNECT_TIMEOUT => 2000,
|
||||||
|
],
|
||||||
|
'servers' => [
|
||||||
|
[
|
||||||
|
'host' => env('MEMCACHED_HOST', '127.0.0.1'),
|
||||||
|
'port' => env('MEMCACHED_PORT', 11211),
|
||||||
|
'weight' => 100,
|
||||||
|
],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
|
||||||
|
'redis' => [
|
||||||
|
'driver' => 'redis',
|
||||||
|
'connection' => env('REDIS_CACHE_CONNECTION', 'cache'),
|
||||||
|
'lock_connection' => env('REDIS_CACHE_LOCK_CONNECTION', 'default'),
|
||||||
|
],
|
||||||
|
|
||||||
|
'dynamodb' => [
|
||||||
|
'driver' => 'dynamodb',
|
||||||
|
'key' => env('AWS_ACCESS_KEY_ID'),
|
||||||
|
'secret' => env('AWS_SECRET_ACCESS_KEY'),
|
||||||
|
'region' => env('AWS_DEFAULT_REGION', 'us-east-1'),
|
||||||
|
'table' => env('DYNAMODB_CACHE_TABLE', 'cache'),
|
||||||
|
'endpoint' => env('DYNAMODB_ENDPOINT'),
|
||||||
|
],
|
||||||
|
|
||||||
|
'octane' => [
|
||||||
|
'driver' => 'octane',
|
||||||
|
],
|
||||||
|
|
||||||
|
'failover' => [
|
||||||
|
'driver' => 'failover',
|
||||||
|
'stores' => [
|
||||||
|
'database',
|
||||||
|
'array',
|
||||||
|
],
|
||||||
|
],
|
||||||
|
|
||||||
|
],
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Cache Key Prefix
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| When utilizing the APC, database, memcached, Redis, and DynamoDB cache
|
||||||
|
| stores, there might be other applications using the same cache. For
|
||||||
|
| that reason, you may prefix every cache key to avoid collisions.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'prefix' => env('CACHE_PREFIX', Str::slug((string) env('APP_NAME', 'laravel')).'-cache-'),
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Serializable Classes
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| This value determines the classes that can be unserialized from cache
|
||||||
|
| storage. By default, no PHP classes will be unserialized from your
|
||||||
|
| cache to prevent gadget chain attacks if your APP_KEY is leaked.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'serializable_classes' => false,
|
||||||
|
|
||||||
|
];
|
||||||
|
|
@ -0,0 +1,184 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Support\Str;
|
||||||
|
use Pdo\Mysql;
|
||||||
|
|
||||||
|
return [
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Default Database Connection Name
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| Here you may specify which of the database connections below you wish
|
||||||
|
| to use as your default connection for database operations. This is
|
||||||
|
| the connection which will be utilized unless another connection
|
||||||
|
| is explicitly specified when you execute a query / statement.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'default' => env('DB_CONNECTION', 'sqlite'),
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Database Connections
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| Below are all of the database connections defined for your application.
|
||||||
|
| An example configuration is provided for each database system which
|
||||||
|
| is supported by Laravel. You're free to add / remove connections.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'connections' => [
|
||||||
|
|
||||||
|
'sqlite' => [
|
||||||
|
'driver' => 'sqlite',
|
||||||
|
'url' => env('DB_URL'),
|
||||||
|
'database' => env('DB_DATABASE', database_path('database.sqlite')),
|
||||||
|
'prefix' => '',
|
||||||
|
'foreign_key_constraints' => env('DB_FOREIGN_KEYS', true),
|
||||||
|
'busy_timeout' => null,
|
||||||
|
'journal_mode' => null,
|
||||||
|
'synchronous' => null,
|
||||||
|
'transaction_mode' => 'DEFERRED',
|
||||||
|
],
|
||||||
|
|
||||||
|
'mysql' => [
|
||||||
|
'driver' => 'mysql',
|
||||||
|
'url' => env('DB_URL'),
|
||||||
|
'host' => env('DB_HOST', '127.0.0.1'),
|
||||||
|
'port' => env('DB_PORT', '3306'),
|
||||||
|
'database' => env('DB_DATABASE', 'laravel'),
|
||||||
|
'username' => env('DB_USERNAME', 'root'),
|
||||||
|
'password' => env('DB_PASSWORD', ''),
|
||||||
|
'unix_socket' => env('DB_SOCKET', ''),
|
||||||
|
'charset' => env('DB_CHARSET', 'utf8mb4'),
|
||||||
|
'collation' => env('DB_COLLATION', 'utf8mb4_unicode_ci'),
|
||||||
|
'prefix' => '',
|
||||||
|
'prefix_indexes' => true,
|
||||||
|
'strict' => true,
|
||||||
|
'engine' => null,
|
||||||
|
'options' => extension_loaded('pdo_mysql') ? array_filter([
|
||||||
|
Mysql::ATTR_SSL_CA => env('MYSQL_ATTR_SSL_CA'),
|
||||||
|
]) : [],
|
||||||
|
],
|
||||||
|
|
||||||
|
'mariadb' => [
|
||||||
|
'driver' => 'mariadb',
|
||||||
|
'url' => env('DB_URL'),
|
||||||
|
'host' => env('DB_HOST', '127.0.0.1'),
|
||||||
|
'port' => env('DB_PORT', '3306'),
|
||||||
|
'database' => env('DB_DATABASE', 'laravel'),
|
||||||
|
'username' => env('DB_USERNAME', 'root'),
|
||||||
|
'password' => env('DB_PASSWORD', ''),
|
||||||
|
'unix_socket' => env('DB_SOCKET', ''),
|
||||||
|
'charset' => env('DB_CHARSET', 'utf8mb4'),
|
||||||
|
'collation' => env('DB_COLLATION', 'utf8mb4_unicode_ci'),
|
||||||
|
'prefix' => '',
|
||||||
|
'prefix_indexes' => true,
|
||||||
|
'strict' => true,
|
||||||
|
'engine' => null,
|
||||||
|
'options' => extension_loaded('pdo_mysql') ? array_filter([
|
||||||
|
Mysql::ATTR_SSL_CA => env('MYSQL_ATTR_SSL_CA'),
|
||||||
|
]) : [],
|
||||||
|
],
|
||||||
|
|
||||||
|
'pgsql' => [
|
||||||
|
'driver' => 'pgsql',
|
||||||
|
'url' => env('DB_URL'),
|
||||||
|
'host' => env('DB_HOST', '127.0.0.1'),
|
||||||
|
'port' => env('DB_PORT', '5432'),
|
||||||
|
'database' => env('DB_DATABASE', 'laravel'),
|
||||||
|
'username' => env('DB_USERNAME', 'root'),
|
||||||
|
'password' => env('DB_PASSWORD', ''),
|
||||||
|
'charset' => env('DB_CHARSET', 'utf8'),
|
||||||
|
'prefix' => '',
|
||||||
|
'prefix_indexes' => true,
|
||||||
|
'search_path' => 'public',
|
||||||
|
'sslmode' => env('DB_SSLMODE', 'prefer'),
|
||||||
|
],
|
||||||
|
|
||||||
|
'sqlsrv' => [
|
||||||
|
'driver' => 'sqlsrv',
|
||||||
|
'url' => env('DB_URL'),
|
||||||
|
'host' => env('DB_HOST', 'localhost'),
|
||||||
|
'port' => env('DB_PORT', '1433'),
|
||||||
|
'database' => env('DB_DATABASE', 'laravel'),
|
||||||
|
'username' => env('DB_USERNAME', 'root'),
|
||||||
|
'password' => env('DB_PASSWORD', ''),
|
||||||
|
'charset' => env('DB_CHARSET', 'utf8'),
|
||||||
|
'prefix' => '',
|
||||||
|
'prefix_indexes' => true,
|
||||||
|
// 'encrypt' => env('DB_ENCRYPT', 'yes'),
|
||||||
|
// 'trust_server_certificate' => env('DB_TRUST_SERVER_CERTIFICATE', 'false'),
|
||||||
|
],
|
||||||
|
|
||||||
|
],
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Migration Repository Table
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| This table keeps track of all the migrations that have already run for
|
||||||
|
| your application. Using this information, we can determine which of
|
||||||
|
| the migrations on disk haven't actually been run on the database.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'migrations' => [
|
||||||
|
'table' => 'migrations',
|
||||||
|
'update_date_on_publish' => true,
|
||||||
|
],
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Redis Databases
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| Redis is an open source, fast, and advanced key-value store that also
|
||||||
|
| provides a richer body of commands than a typical key-value system
|
||||||
|
| such as Memcached. You may define your connection settings here.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'redis' => [
|
||||||
|
|
||||||
|
'client' => env('REDIS_CLIENT', 'phpredis'),
|
||||||
|
|
||||||
|
'options' => [
|
||||||
|
'cluster' => env('REDIS_CLUSTER', 'redis'),
|
||||||
|
'prefix' => env('REDIS_PREFIX', Str::slug((string) env('APP_NAME', 'laravel')).'-database-'),
|
||||||
|
'persistent' => env('REDIS_PERSISTENT', false),
|
||||||
|
],
|
||||||
|
|
||||||
|
'default' => [
|
||||||
|
'url' => env('REDIS_URL'),
|
||||||
|
'host' => env('REDIS_HOST', '127.0.0.1'),
|
||||||
|
'username' => env('REDIS_USERNAME'),
|
||||||
|
'password' => env('REDIS_PASSWORD'),
|
||||||
|
'port' => env('REDIS_PORT', '6379'),
|
||||||
|
'database' => env('REDIS_DB', '0'),
|
||||||
|
'max_retries' => env('REDIS_MAX_RETRIES', 3),
|
||||||
|
'backoff_algorithm' => env('REDIS_BACKOFF_ALGORITHM', 'decorrelated_jitter'),
|
||||||
|
'backoff_base' => env('REDIS_BACKOFF_BASE', 100),
|
||||||
|
'backoff_cap' => env('REDIS_BACKOFF_CAP', 1000),
|
||||||
|
],
|
||||||
|
|
||||||
|
'cache' => [
|
||||||
|
'url' => env('REDIS_URL'),
|
||||||
|
'host' => env('REDIS_HOST', '127.0.0.1'),
|
||||||
|
'username' => env('REDIS_USERNAME'),
|
||||||
|
'password' => env('REDIS_PASSWORD'),
|
||||||
|
'port' => env('REDIS_PORT', '6379'),
|
||||||
|
'database' => env('REDIS_CACHE_DB', '1'),
|
||||||
|
'max_retries' => env('REDIS_MAX_RETRIES', 3),
|
||||||
|
'backoff_algorithm' => env('REDIS_BACKOFF_ALGORITHM', 'decorrelated_jitter'),
|
||||||
|
'backoff_base' => env('REDIS_BACKOFF_BASE', 100),
|
||||||
|
'backoff_cap' => env('REDIS_BACKOFF_CAP', 1000),
|
||||||
|
],
|
||||||
|
|
||||||
|
],
|
||||||
|
|
||||||
|
];
|
||||||
|
|
@ -0,0 +1,80 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
return [
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Default Filesystem Disk
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| Here you may specify the default filesystem disk that should be used
|
||||||
|
| by the framework. The "local" disk, as well as a variety of cloud
|
||||||
|
| based disks are available to your application for file storage.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'default' => env('FILESYSTEM_DISK', 'local'),
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Filesystem Disks
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| Below you may configure as many filesystem disks as necessary, and you
|
||||||
|
| may even configure multiple disks for the same driver. Examples for
|
||||||
|
| most supported storage drivers are configured here for reference.
|
||||||
|
|
|
||||||
|
| Supported drivers: "local", "ftp", "sftp", "s3"
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'disks' => [
|
||||||
|
|
||||||
|
'local' => [
|
||||||
|
'driver' => 'local',
|
||||||
|
'root' => storage_path('app/private'),
|
||||||
|
'serve' => true,
|
||||||
|
'throw' => false,
|
||||||
|
'report' => false,
|
||||||
|
],
|
||||||
|
|
||||||
|
'public' => [
|
||||||
|
'driver' => 'local',
|
||||||
|
'root' => storage_path('app/public'),
|
||||||
|
'url' => rtrim(env('APP_URL', 'http://localhost'), '/').'/storage',
|
||||||
|
'visibility' => 'public',
|
||||||
|
'throw' => false,
|
||||||
|
'report' => false,
|
||||||
|
],
|
||||||
|
|
||||||
|
's3' => [
|
||||||
|
'driver' => 's3',
|
||||||
|
'key' => env('AWS_ACCESS_KEY_ID'),
|
||||||
|
'secret' => env('AWS_SECRET_ACCESS_KEY'),
|
||||||
|
'region' => env('AWS_DEFAULT_REGION'),
|
||||||
|
'bucket' => env('AWS_BUCKET'),
|
||||||
|
'url' => env('AWS_URL'),
|
||||||
|
'endpoint' => env('AWS_ENDPOINT'),
|
||||||
|
'use_path_style_endpoint' => env('AWS_USE_PATH_STYLE_ENDPOINT', false),
|
||||||
|
'throw' => false,
|
||||||
|
'report' => false,
|
||||||
|
],
|
||||||
|
|
||||||
|
],
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Symbolic Links
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| Here you may configure the symbolic links that will be created when the
|
||||||
|
| `storage:link` Artisan command is executed. The array keys should be
|
||||||
|
| the locations of the links and the values should be their targets.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'links' => [
|
||||||
|
public_path('storage') => storage_path('app/public'),
|
||||||
|
],
|
||||||
|
|
||||||
|
];
|
||||||
|
|
@ -0,0 +1,254 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Support\Str;
|
||||||
|
|
||||||
|
return [
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Horizon Name
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| This name appears in notifications and in the Horizon UI. Unique names
|
||||||
|
| can be useful while running multiple instances of Horizon within an
|
||||||
|
| application, allowing you to identify the Horizon you're viewing.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'name' => env('HORIZON_NAME'),
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Horizon Domain
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| This is the subdomain where Horizon will be accessible from. If this
|
||||||
|
| setting is null, Horizon will reside under the same domain as the
|
||||||
|
| application. Otherwise, this value will serve as the subdomain.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'domain' => env('HORIZON_DOMAIN'),
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Horizon Path
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| This is the URI path where Horizon will be accessible from. Feel free
|
||||||
|
| to change this path to anything you like. Note that the URI will not
|
||||||
|
| affect the paths of its internal API that aren't exposed to users.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'path' => env('HORIZON_PATH', 'horizon'),
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Horizon Redis Connection
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| This is the name of the Redis connection where Horizon will store the
|
||||||
|
| meta information required for it to function. It includes the list
|
||||||
|
| of supervisors, failed jobs, job metrics, and other information.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'use' => 'default',
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Horizon Redis Prefix
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| This prefix will be used when storing all Horizon data in Redis. You
|
||||||
|
| may modify the prefix when you are running multiple installations
|
||||||
|
| of Horizon on the same server so that they don't have problems.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'prefix' => env(
|
||||||
|
'HORIZON_PREFIX',
|
||||||
|
Str::slug(env('APP_NAME', 'laravel'), '_').'_horizon:'
|
||||||
|
),
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Horizon Route Middleware
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| These middleware will get attached onto each Horizon route, giving you
|
||||||
|
| the chance to add your own middleware to this list or change any of
|
||||||
|
| the existing middleware. Or, you can simply stick with this list.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'middleware' => ['web'],
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Queue Wait Time Thresholds
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| This option allows you to configure when the LongWaitDetected event
|
||||||
|
| will be fired. Every connection / queue combination may have its
|
||||||
|
| own, unique threshold (in seconds) before this event is fired.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'waits' => [
|
||||||
|
'redis:default' => 60,
|
||||||
|
],
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Job Trimming Times
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| Here you can configure for how long (in minutes) you desire Horizon to
|
||||||
|
| persist the recent and failed jobs. Typically, recent jobs are kept
|
||||||
|
| for one hour while all failed jobs are stored for an entire week.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'trim' => [
|
||||||
|
'recent' => 60,
|
||||||
|
'pending' => 60,
|
||||||
|
'completed' => 60,
|
||||||
|
'recent_failed' => 10080,
|
||||||
|
'failed' => 10080,
|
||||||
|
'monitored' => 10080,
|
||||||
|
],
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Silenced Jobs
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| Silencing a job will instruct Horizon to not place the job in the list
|
||||||
|
| of completed jobs within the Horizon dashboard. This setting may be
|
||||||
|
| used to fully remove any noisy jobs from the completed jobs list.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'silenced' => [
|
||||||
|
// App\Jobs\ExampleJob::class,
|
||||||
|
],
|
||||||
|
|
||||||
|
'silenced_tags' => [
|
||||||
|
// 'notifications',
|
||||||
|
],
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Metrics
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| Here you can configure how many snapshots should be kept to display in
|
||||||
|
| the metrics graph. This will get used in combination with Horizon's
|
||||||
|
| `horizon:snapshot` schedule to define how long to retain metrics.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'metrics' => [
|
||||||
|
'trim_snapshots' => [
|
||||||
|
'job' => 24,
|
||||||
|
'queue' => 24,
|
||||||
|
],
|
||||||
|
],
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Fast Termination
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| When this option is enabled, Horizon's "terminate" command will not
|
||||||
|
| wait on all of the workers to terminate unless the --wait option
|
||||||
|
| is provided. Fast termination can shorten deployment delay by
|
||||||
|
| allowing a new instance of Horizon to start while the last
|
||||||
|
| instance will continue to terminate each of its workers.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'fast_termination' => false,
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Memory Limit (MB)
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| This value describes the maximum amount of memory the Horizon master
|
||||||
|
| supervisor may consume before it is terminated and restarted. For
|
||||||
|
| configuring these limits on your workers, see the next section.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'memory_limit' => 64,
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Queue Worker Configuration
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| Here you may define the queue worker settings used by your application
|
||||||
|
| in all environments. These supervisors and settings handle all your
|
||||||
|
| queued jobs and will be provisioned by Horizon during deployment.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'defaults' => [
|
||||||
|
'supervisor-1' => [
|
||||||
|
'connection' => 'redis',
|
||||||
|
'queue' => ['default'],
|
||||||
|
'balance' => 'auto',
|
||||||
|
'autoScalingStrategy' => 'time',
|
||||||
|
'maxProcesses' => 1,
|
||||||
|
'maxTime' => 0,
|
||||||
|
'maxJobs' => 0,
|
||||||
|
'memory' => 128,
|
||||||
|
'tries' => 1,
|
||||||
|
'timeout' => 60,
|
||||||
|
'nice' => 0,
|
||||||
|
],
|
||||||
|
],
|
||||||
|
|
||||||
|
'environments' => [
|
||||||
|
'production' => [
|
||||||
|
'supervisor-1' => [
|
||||||
|
'maxProcesses' => 10,
|
||||||
|
'balanceMaxShift' => 1,
|
||||||
|
'balanceCooldown' => 3,
|
||||||
|
],
|
||||||
|
],
|
||||||
|
|
||||||
|
'local' => [
|
||||||
|
'supervisor-1' => [
|
||||||
|
'maxProcesses' => 3,
|
||||||
|
],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| File Watcher Configuration
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| The following list of directories and files will be watched when using
|
||||||
|
| the `horizon:listen` command. Whenever any directories or files are
|
||||||
|
| changed, Horizon will automatically restart to apply all changes.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'watch' => [
|
||||||
|
'app',
|
||||||
|
'bootstrap',
|
||||||
|
'config/**/*.php',
|
||||||
|
'database/**/*.php',
|
||||||
|
'public/**/*.php',
|
||||||
|
'resources/**/*.php',
|
||||||
|
'routes',
|
||||||
|
'composer.lock',
|
||||||
|
'composer.json',
|
||||||
|
'.env',
|
||||||
|
],
|
||||||
|
];
|
||||||
|
|
@ -0,0 +1,132 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
use Monolog\Handler\NullHandler;
|
||||||
|
use Monolog\Handler\StreamHandler;
|
||||||
|
use Monolog\Handler\SyslogUdpHandler;
|
||||||
|
use Monolog\Processor\PsrLogMessageProcessor;
|
||||||
|
|
||||||
|
return [
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Default Log Channel
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| This option defines the default log channel that is utilized to write
|
||||||
|
| messages to your logs. The value provided here should match one of
|
||||||
|
| the channels present in the list of "channels" configured below.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'default' => env('LOG_CHANNEL', 'stack'),
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Deprecations Log Channel
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| This option controls the log channel that should be used to log warnings
|
||||||
|
| regarding deprecated PHP and library features. This allows you to get
|
||||||
|
| your application ready for upcoming major versions of dependencies.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'deprecations' => [
|
||||||
|
'channel' => env('LOG_DEPRECATIONS_CHANNEL', 'null'),
|
||||||
|
'trace' => env('LOG_DEPRECATIONS_TRACE', false),
|
||||||
|
],
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Log Channels
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| Here you may configure the log channels for your application. Laravel
|
||||||
|
| utilizes the Monolog PHP logging library, which includes a variety
|
||||||
|
| of powerful log handlers and formatters that you're free to use.
|
||||||
|
|
|
||||||
|
| Available drivers: "single", "daily", "slack", "syslog",
|
||||||
|
| "errorlog", "monolog", "custom", "stack"
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'channels' => [
|
||||||
|
|
||||||
|
'stack' => [
|
||||||
|
'driver' => 'stack',
|
||||||
|
'channels' => explode(',', (string) env('LOG_STACK', 'single')),
|
||||||
|
'ignore_exceptions' => false,
|
||||||
|
],
|
||||||
|
|
||||||
|
'single' => [
|
||||||
|
'driver' => 'single',
|
||||||
|
'path' => storage_path('logs/laravel.log'),
|
||||||
|
'level' => env('LOG_LEVEL', 'debug'),
|
||||||
|
'replace_placeholders' => true,
|
||||||
|
],
|
||||||
|
|
||||||
|
'daily' => [
|
||||||
|
'driver' => 'daily',
|
||||||
|
'path' => storage_path('logs/laravel.log'),
|
||||||
|
'level' => env('LOG_LEVEL', 'debug'),
|
||||||
|
'days' => env('LOG_DAILY_DAYS', 14),
|
||||||
|
'replace_placeholders' => true,
|
||||||
|
],
|
||||||
|
|
||||||
|
'slack' => [
|
||||||
|
'driver' => 'slack',
|
||||||
|
'url' => env('LOG_SLACK_WEBHOOK_URL'),
|
||||||
|
'username' => env('LOG_SLACK_USERNAME', env('APP_NAME', 'Laravel')),
|
||||||
|
'emoji' => env('LOG_SLACK_EMOJI', ':boom:'),
|
||||||
|
'level' => env('LOG_LEVEL', 'critical'),
|
||||||
|
'replace_placeholders' => true,
|
||||||
|
],
|
||||||
|
|
||||||
|
'papertrail' => [
|
||||||
|
'driver' => 'monolog',
|
||||||
|
'level' => env('LOG_LEVEL', 'debug'),
|
||||||
|
'handler' => env('LOG_PAPERTRAIL_HANDLER', SyslogUdpHandler::class),
|
||||||
|
'handler_with' => [
|
||||||
|
'host' => env('PAPERTRAIL_URL'),
|
||||||
|
'port' => env('PAPERTRAIL_PORT'),
|
||||||
|
'connectionString' => 'tls://'.env('PAPERTRAIL_URL').':'.env('PAPERTRAIL_PORT'),
|
||||||
|
],
|
||||||
|
'processors' => [PsrLogMessageProcessor::class],
|
||||||
|
],
|
||||||
|
|
||||||
|
'stderr' => [
|
||||||
|
'driver' => 'monolog',
|
||||||
|
'level' => env('LOG_LEVEL', 'debug'),
|
||||||
|
'handler' => StreamHandler::class,
|
||||||
|
'handler_with' => [
|
||||||
|
'stream' => 'php://stderr',
|
||||||
|
],
|
||||||
|
'formatter' => env('LOG_STDERR_FORMATTER'),
|
||||||
|
'processors' => [PsrLogMessageProcessor::class],
|
||||||
|
],
|
||||||
|
|
||||||
|
'syslog' => [
|
||||||
|
'driver' => 'syslog',
|
||||||
|
'level' => env('LOG_LEVEL', 'debug'),
|
||||||
|
'facility' => env('LOG_SYSLOG_FACILITY', LOG_USER),
|
||||||
|
'replace_placeholders' => true,
|
||||||
|
],
|
||||||
|
|
||||||
|
'errorlog' => [
|
||||||
|
'driver' => 'errorlog',
|
||||||
|
'level' => env('LOG_LEVEL', 'debug'),
|
||||||
|
'replace_placeholders' => true,
|
||||||
|
],
|
||||||
|
|
||||||
|
'null' => [
|
||||||
|
'driver' => 'monolog',
|
||||||
|
'handler' => NullHandler::class,
|
||||||
|
],
|
||||||
|
|
||||||
|
'emergency' => [
|
||||||
|
'path' => storage_path('logs/laravel.log'),
|
||||||
|
],
|
||||||
|
|
||||||
|
],
|
||||||
|
|
||||||
|
];
|
||||||
|
|
@ -0,0 +1,118 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
return [
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Default Mailer
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| This option controls the default mailer that is used to send all email
|
||||||
|
| messages unless another mailer is explicitly specified when sending
|
||||||
|
| the message. All additional mailers can be configured within the
|
||||||
|
| "mailers" array. Examples of each type of mailer are provided.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'default' => env('MAIL_MAILER', 'log'),
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Mailer Configurations
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| Here you may configure all of the mailers used by your application plus
|
||||||
|
| their respective settings. Several examples have been configured for
|
||||||
|
| you and you are free to add your own as your application requires.
|
||||||
|
|
|
||||||
|
| Laravel supports a variety of mail "transport" drivers that can be used
|
||||||
|
| when delivering an email. You may specify which one you're using for
|
||||||
|
| your mailers below. You may also add additional mailers if needed.
|
||||||
|
|
|
||||||
|
| Supported: "smtp", "sendmail", "mailgun", "ses", "ses-v2",
|
||||||
|
| "postmark", "resend", "log", "array",
|
||||||
|
| "failover", "roundrobin"
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'mailers' => [
|
||||||
|
|
||||||
|
'smtp' => [
|
||||||
|
'transport' => 'smtp',
|
||||||
|
'scheme' => env('MAIL_SCHEME'),
|
||||||
|
'url' => env('MAIL_URL'),
|
||||||
|
'host' => env('MAIL_HOST', '127.0.0.1'),
|
||||||
|
'port' => env('MAIL_PORT', 2525),
|
||||||
|
'username' => env('MAIL_USERNAME'),
|
||||||
|
'password' => env('MAIL_PASSWORD'),
|
||||||
|
'timeout' => null,
|
||||||
|
'local_domain' => env('MAIL_EHLO_DOMAIN', parse_url((string) env('APP_URL', 'http://localhost'), PHP_URL_HOST)),
|
||||||
|
],
|
||||||
|
|
||||||
|
'ses' => [
|
||||||
|
'transport' => 'ses',
|
||||||
|
],
|
||||||
|
|
||||||
|
'postmark' => [
|
||||||
|
'transport' => 'postmark',
|
||||||
|
// 'message_stream_id' => env('POSTMARK_MESSAGE_STREAM_ID'),
|
||||||
|
// 'client' => [
|
||||||
|
// 'timeout' => 5,
|
||||||
|
// ],
|
||||||
|
],
|
||||||
|
|
||||||
|
'resend' => [
|
||||||
|
'transport' => 'resend',
|
||||||
|
],
|
||||||
|
|
||||||
|
'sendmail' => [
|
||||||
|
'transport' => 'sendmail',
|
||||||
|
'path' => env('MAIL_SENDMAIL_PATH', '/usr/sbin/sendmail -bs -i'),
|
||||||
|
],
|
||||||
|
|
||||||
|
'log' => [
|
||||||
|
'transport' => 'log',
|
||||||
|
'channel' => env('MAIL_LOG_CHANNEL'),
|
||||||
|
],
|
||||||
|
|
||||||
|
'array' => [
|
||||||
|
'transport' => 'array',
|
||||||
|
],
|
||||||
|
|
||||||
|
'failover' => [
|
||||||
|
'transport' => 'failover',
|
||||||
|
'mailers' => [
|
||||||
|
'smtp',
|
||||||
|
'log',
|
||||||
|
],
|
||||||
|
'retry_after' => 60,
|
||||||
|
],
|
||||||
|
|
||||||
|
'roundrobin' => [
|
||||||
|
'transport' => 'roundrobin',
|
||||||
|
'mailers' => [
|
||||||
|
'ses',
|
||||||
|
'postmark',
|
||||||
|
],
|
||||||
|
'retry_after' => 60,
|
||||||
|
],
|
||||||
|
|
||||||
|
],
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Global "From" Address
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| You may wish for all emails sent by your application to be sent from
|
||||||
|
| the same address. Here you may specify a name and address that is
|
||||||
|
| used globally for all emails that are sent by your application.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'from' => [
|
||||||
|
'address' => env('MAIL_FROM_ADDRESS', 'hello@example.com'),
|
||||||
|
'name' => env('MAIL_FROM_NAME', env('APP_NAME', 'Laravel')),
|
||||||
|
],
|
||||||
|
|
||||||
|
];
|
||||||
|
|
@ -0,0 +1,129 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
return [
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Default Queue Connection Name
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| Laravel's queue supports a variety of backends via a single, unified
|
||||||
|
| API, giving you convenient access to each backend using identical
|
||||||
|
| syntax for each. The default queue connection is defined below.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'default' => env('QUEUE_CONNECTION', 'database'),
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Queue Connections
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| Here you may configure the connection options for every queue backend
|
||||||
|
| used by your application. An example configuration is provided for
|
||||||
|
| each backend supported by Laravel. You're also free to add more.
|
||||||
|
|
|
||||||
|
| Drivers: "sync", "database", "beanstalkd", "sqs", "redis",
|
||||||
|
| "deferred", "background", "failover", "null"
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'connections' => [
|
||||||
|
|
||||||
|
'sync' => [
|
||||||
|
'driver' => 'sync',
|
||||||
|
],
|
||||||
|
|
||||||
|
'database' => [
|
||||||
|
'driver' => 'database',
|
||||||
|
'connection' => env('DB_QUEUE_CONNECTION'),
|
||||||
|
'table' => env('DB_QUEUE_TABLE', 'jobs'),
|
||||||
|
'queue' => env('DB_QUEUE', 'default'),
|
||||||
|
'retry_after' => (int) env('DB_QUEUE_RETRY_AFTER', 90),
|
||||||
|
'after_commit' => false,
|
||||||
|
],
|
||||||
|
|
||||||
|
'beanstalkd' => [
|
||||||
|
'driver' => 'beanstalkd',
|
||||||
|
'host' => env('BEANSTALKD_QUEUE_HOST', 'localhost'),
|
||||||
|
'queue' => env('BEANSTALKD_QUEUE', 'default'),
|
||||||
|
'retry_after' => (int) env('BEANSTALKD_QUEUE_RETRY_AFTER', 90),
|
||||||
|
'block_for' => 0,
|
||||||
|
'after_commit' => false,
|
||||||
|
],
|
||||||
|
|
||||||
|
'sqs' => [
|
||||||
|
'driver' => 'sqs',
|
||||||
|
'key' => env('AWS_ACCESS_KEY_ID'),
|
||||||
|
'secret' => env('AWS_SECRET_ACCESS_KEY'),
|
||||||
|
'prefix' => env('SQS_PREFIX', 'https://sqs.us-east-1.amazonaws.com/your-account-id'),
|
||||||
|
'queue' => env('SQS_QUEUE', 'default'),
|
||||||
|
'suffix' => env('SQS_SUFFIX'),
|
||||||
|
'region' => env('AWS_DEFAULT_REGION', 'us-east-1'),
|
||||||
|
'after_commit' => false,
|
||||||
|
],
|
||||||
|
|
||||||
|
'redis' => [
|
||||||
|
'driver' => 'redis',
|
||||||
|
'connection' => env('REDIS_QUEUE_CONNECTION', 'default'),
|
||||||
|
'queue' => env('REDIS_QUEUE', 'default'),
|
||||||
|
'retry_after' => (int) env('REDIS_QUEUE_RETRY_AFTER', 90),
|
||||||
|
'block_for' => null,
|
||||||
|
'after_commit' => false,
|
||||||
|
],
|
||||||
|
|
||||||
|
'deferred' => [
|
||||||
|
'driver' => 'deferred',
|
||||||
|
],
|
||||||
|
|
||||||
|
'background' => [
|
||||||
|
'driver' => 'background',
|
||||||
|
],
|
||||||
|
|
||||||
|
'failover' => [
|
||||||
|
'driver' => 'failover',
|
||||||
|
'connections' => [
|
||||||
|
'database',
|
||||||
|
'deferred',
|
||||||
|
],
|
||||||
|
],
|
||||||
|
|
||||||
|
],
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Job Batching
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| The following options configure the database and table that store job
|
||||||
|
| batching information. These options can be updated to any database
|
||||||
|
| connection and table which has been defined by your application.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'batching' => [
|
||||||
|
'database' => env('DB_CONNECTION', 'sqlite'),
|
||||||
|
'table' => 'job_batches',
|
||||||
|
],
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Failed Queue Jobs
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| These options configure the behavior of failed queue job logging so you
|
||||||
|
| can control how and where failed jobs are stored. Laravel ships with
|
||||||
|
| support for storing failed jobs in a simple file or in a database.
|
||||||
|
|
|
||||||
|
| Supported drivers: "database-uuids", "dynamodb", "file", "null"
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'failed' => [
|
||||||
|
'driver' => env('QUEUE_FAILED_DRIVER', 'database-uuids'),
|
||||||
|
'database' => env('DB_CONNECTION', 'sqlite'),
|
||||||
|
'table' => 'failed_jobs',
|
||||||
|
],
|
||||||
|
|
||||||
|
];
|
||||||
|
|
@ -0,0 +1,102 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
return [
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Default Reverb Server
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| This option controls the default server used by Reverb to handle
|
||||||
|
| incoming messages as well as broadcasting message to all your
|
||||||
|
| connected clients. At this time only "reverb" is supported.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'default' => env('REVERB_SERVER', 'reverb'),
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Reverb Servers
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| Here you may define details for each of the supported Reverb servers.
|
||||||
|
| Each server has its own configuration options that are defined in
|
||||||
|
| the array below. You should ensure all the options are present.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'servers' => [
|
||||||
|
|
||||||
|
'reverb' => [
|
||||||
|
'host' => env('REVERB_SERVER_HOST', '0.0.0.0'),
|
||||||
|
'port' => env('REVERB_SERVER_PORT', 8080),
|
||||||
|
'path' => env('REVERB_SERVER_PATH', ''),
|
||||||
|
'hostname' => env('REVERB_HOST'),
|
||||||
|
'options' => [
|
||||||
|
'tls' => [],
|
||||||
|
],
|
||||||
|
'max_request_size' => env('REVERB_MAX_REQUEST_SIZE', 10_000),
|
||||||
|
'scaling' => [
|
||||||
|
'enabled' => env('REVERB_SCALING_ENABLED', false),
|
||||||
|
'channel' => env('REVERB_SCALING_CHANNEL', 'reverb'),
|
||||||
|
'server' => [
|
||||||
|
'url' => env('REDIS_URL'),
|
||||||
|
'host' => env('REDIS_HOST', '127.0.0.1'),
|
||||||
|
'port' => env('REDIS_PORT', '6379'),
|
||||||
|
'username' => env('REDIS_USERNAME'),
|
||||||
|
'password' => env('REDIS_PASSWORD'),
|
||||||
|
'database' => env('REDIS_DB', '0'),
|
||||||
|
'timeout' => env('REDIS_TIMEOUT', 60),
|
||||||
|
],
|
||||||
|
],
|
||||||
|
'pulse_ingest_interval' => env('REVERB_PULSE_INGEST_INTERVAL', 15),
|
||||||
|
'telescope_ingest_interval' => env('REVERB_TELESCOPE_INGEST_INTERVAL', 15),
|
||||||
|
],
|
||||||
|
|
||||||
|
],
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Reverb Applications
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| Here you may define how Reverb applications are managed. If you choose
|
||||||
|
| to use the "config" provider, you may define an array of apps which
|
||||||
|
| your server will support, including their connection credentials.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'apps' => [
|
||||||
|
|
||||||
|
'provider' => 'config',
|
||||||
|
|
||||||
|
'apps' => [
|
||||||
|
[
|
||||||
|
'key' => env('REVERB_APP_KEY'),
|
||||||
|
'secret' => env('REVERB_APP_SECRET'),
|
||||||
|
'app_id' => env('REVERB_APP_ID'),
|
||||||
|
'options' => [
|
||||||
|
'host' => env('REVERB_HOST'),
|
||||||
|
'port' => env('REVERB_PORT', 443),
|
||||||
|
'scheme' => env('REVERB_SCHEME', 'https'),
|
||||||
|
'useTLS' => env('REVERB_SCHEME', 'https') === 'https',
|
||||||
|
],
|
||||||
|
'allowed_origins' => ['*'],
|
||||||
|
'ping_interval' => env('REVERB_APP_PING_INTERVAL', 60),
|
||||||
|
'activity_timeout' => env('REVERB_APP_ACTIVITY_TIMEOUT', 30),
|
||||||
|
'max_connections' => env('REVERB_APP_MAX_CONNECTIONS'),
|
||||||
|
'max_message_size' => env('REVERB_APP_MAX_MESSAGE_SIZE', 10_000),
|
||||||
|
'accept_client_events_from' => env('REVERB_APP_ACCEPT_CLIENT_EVENTS_FROM', 'members'),
|
||||||
|
'rate_limiting' => [
|
||||||
|
'enabled' => env('REVERB_APP_RATE_LIMITING_ENABLED', false),
|
||||||
|
'max_attempts' => env('REVERB_APP_RATE_LIMIT_MAX_ATTEMPTS', 60),
|
||||||
|
'decay_seconds' => env('REVERB_APP_RATE_LIMIT_DECAY_SECONDS', 60),
|
||||||
|
'terminate_on_limit' => env('REVERB_APP_RATE_LIMIT_TERMINATE', false),
|
||||||
|
],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
|
||||||
|
],
|
||||||
|
|
||||||
|
];
|
||||||
|
|
@ -0,0 +1,38 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
return [
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Third Party Services
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| This file is for storing the credentials for third party services such
|
||||||
|
| as Mailgun, Postmark, AWS and more. This file provides the de facto
|
||||||
|
| location for this type of information, allowing packages to have
|
||||||
|
| a conventional file to locate the various service credentials.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'postmark' => [
|
||||||
|
'key' => env('POSTMARK_API_KEY'),
|
||||||
|
],
|
||||||
|
|
||||||
|
'resend' => [
|
||||||
|
'key' => env('RESEND_API_KEY'),
|
||||||
|
],
|
||||||
|
|
||||||
|
'ses' => [
|
||||||
|
'key' => env('AWS_ACCESS_KEY_ID'),
|
||||||
|
'secret' => env('AWS_SECRET_ACCESS_KEY'),
|
||||||
|
'region' => env('AWS_DEFAULT_REGION', 'us-east-1'),
|
||||||
|
],
|
||||||
|
|
||||||
|
'slack' => [
|
||||||
|
'notifications' => [
|
||||||
|
'bot_user_oauth_token' => env('SLACK_BOT_USER_OAUTH_TOKEN'),
|
||||||
|
'channel' => env('SLACK_BOT_USER_DEFAULT_CHANNEL'),
|
||||||
|
],
|
||||||
|
],
|
||||||
|
|
||||||
|
];
|
||||||
|
|
@ -0,0 +1,233 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Support\Str;
|
||||||
|
|
||||||
|
return [
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Default Session Driver
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| This option determines the default session driver that is utilized for
|
||||||
|
| incoming requests. Laravel supports a variety of storage options to
|
||||||
|
| persist session data. Database storage is a great default choice.
|
||||||
|
|
|
||||||
|
| Supported: "file", "cookie", "database", "memcached",
|
||||||
|
| "redis", "dynamodb", "array"
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'driver' => env('SESSION_DRIVER', 'database'),
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Session Lifetime
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| Here you may specify the number of minutes that you wish the session
|
||||||
|
| to be allowed to remain idle before it expires. If you want them
|
||||||
|
| to expire immediately when the browser is closed then you may
|
||||||
|
| indicate that via the expire_on_close configuration option.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'lifetime' => (int) env('SESSION_LIFETIME', 120),
|
||||||
|
|
||||||
|
'expire_on_close' => env('SESSION_EXPIRE_ON_CLOSE', false),
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Session Encryption
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| This option allows you to easily specify that all of your session data
|
||||||
|
| should be encrypted before it's stored. All encryption is performed
|
||||||
|
| automatically by Laravel and you may use the session like normal.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'encrypt' => env('SESSION_ENCRYPT', false),
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Session File Location
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| When utilizing the "file" session driver, the session files are placed
|
||||||
|
| on disk. The default storage location is defined here; however, you
|
||||||
|
| are free to provide another location where they should be stored.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'files' => storage_path('framework/sessions'),
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Session Database Connection
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| When using the "database" or "redis" session drivers, you may specify a
|
||||||
|
| connection that should be used to manage these sessions. This should
|
||||||
|
| correspond to a connection in your database configuration options.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'connection' => env('SESSION_CONNECTION'),
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Session Database Table
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| When using the "database" session driver, you may specify the table to
|
||||||
|
| be used to store sessions. Of course, a sensible default is defined
|
||||||
|
| for you; however, you're welcome to change this to another table.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'table' => env('SESSION_TABLE', 'sessions'),
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Session Cache Store
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| When using one of the framework's cache driven session backends, you may
|
||||||
|
| define the cache store which should be used to store the session data
|
||||||
|
| between requests. This must match one of your defined cache stores.
|
||||||
|
|
|
||||||
|
| Affects: "dynamodb", "memcached", "redis"
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'store' => env('SESSION_STORE'),
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Session Sweeping Lottery
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| Some session drivers must manually sweep their storage location to get
|
||||||
|
| rid of old sessions from storage. Here are the chances that it will
|
||||||
|
| happen on a given request. By default, the odds are 2 out of 100.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'lottery' => [2, 100],
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Session Cookie Name
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| Here you may change the name of the session cookie that is created by
|
||||||
|
| the framework. Typically, you should not need to change this value
|
||||||
|
| since doing so does not grant a meaningful security improvement.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'cookie' => env(
|
||||||
|
'SESSION_COOKIE',
|
||||||
|
Str::slug((string) env('APP_NAME', 'laravel')).'-session'
|
||||||
|
),
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Session Cookie Path
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| The session cookie path determines the path for which the cookie will
|
||||||
|
| be regarded as available. Typically, this will be the root path of
|
||||||
|
| your application, but you're free to change this when necessary.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'path' => env('SESSION_PATH', '/'),
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Session Cookie Domain
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| This value determines the domain and subdomains the session cookie is
|
||||||
|
| available to. By default, the cookie will be available to the root
|
||||||
|
| domain without subdomains. Typically, this shouldn't be changed.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'domain' => env('SESSION_DOMAIN'),
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| HTTPS Only Cookies
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| By setting this option to true, session cookies will only be sent back
|
||||||
|
| to the server if the browser has a HTTPS connection. This will keep
|
||||||
|
| the cookie from being sent to you when it can't be done securely.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'secure' => env('SESSION_SECURE_COOKIE'),
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| HTTP Access Only
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| Setting this value to true will prevent JavaScript from accessing the
|
||||||
|
| value of the cookie and the cookie will only be accessible through
|
||||||
|
| the HTTP protocol. It's unlikely you should disable this option.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'http_only' => env('SESSION_HTTP_ONLY', true),
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Same-Site Cookies
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| This option determines how your cookies behave when cross-site requests
|
||||||
|
| take place, and can be used to mitigate CSRF attacks. By default, we
|
||||||
|
| will set this value to "lax" to permit secure cross-site requests.
|
||||||
|
|
|
||||||
|
| See: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie#samesitesamesite-value
|
||||||
|
|
|
||||||
|
| Supported: "lax", "strict", "none", null
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'same_site' => env('SESSION_SAME_SITE', 'lax'),
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Partitioned Cookies
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| Setting this value to true will tie the cookie to the top-level site for
|
||||||
|
| a cross-site context. Partitioned cookies are accepted by the browser
|
||||||
|
| when flagged "secure" and the Same-Site attribute is set to "none".
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'partitioned' => env('SESSION_PARTITIONED_COOKIE', false),
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Session Serialization
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| This value controls the serialization strategy for session data, which
|
||||||
|
| is JSON by default. Setting this to "php" allows the storage of PHP
|
||||||
|
| objects in the session but can make an application vulnerable to
|
||||||
|
| "gadget chain" serialization attacks if the APP_KEY is leaked.
|
||||||
|
|
|
||||||
|
| Supported: "json", "php"
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'serialization' => 'json',
|
||||||
|
|
||||||
|
];
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
*.sqlite*
|
||||||
|
|
@ -0,0 +1,45 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Database\Factories;
|
||||||
|
|
||||||
|
use App\Models\User;
|
||||||
|
use Illuminate\Database\Eloquent\Factories\Factory;
|
||||||
|
use Illuminate\Support\Facades\Hash;
|
||||||
|
use Illuminate\Support\Str;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @extends Factory<User>
|
||||||
|
*/
|
||||||
|
class UserFactory extends Factory
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* The current password being used by the factory.
|
||||||
|
*/
|
||||||
|
protected static ?string $password;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Define the model's default state.
|
||||||
|
*
|
||||||
|
* @return array<string, mixed>
|
||||||
|
*/
|
||||||
|
public function definition(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'name' => fake()->name(),
|
||||||
|
'email' => fake()->unique()->safeEmail(),
|
||||||
|
'email_verified_at' => now(),
|
||||||
|
'password' => static::$password ??= Hash::make('password'),
|
||||||
|
'remember_token' => Str::random(10),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Indicate that the model's email address should be unverified.
|
||||||
|
*/
|
||||||
|
public function unverified(): static
|
||||||
|
{
|
||||||
|
return $this->state(fn (array $attributes) => [
|
||||||
|
'email_verified_at' => null,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,49 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Run the migrations.
|
||||||
|
*/
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
Schema::create('users', function (Blueprint $table) {
|
||||||
|
$table->id();
|
||||||
|
$table->string('name');
|
||||||
|
$table->string('email')->unique();
|
||||||
|
$table->timestamp('email_verified_at')->nullable();
|
||||||
|
$table->string('password');
|
||||||
|
$table->rememberToken();
|
||||||
|
$table->timestamps();
|
||||||
|
});
|
||||||
|
|
||||||
|
Schema::create('password_reset_tokens', function (Blueprint $table) {
|
||||||
|
$table->string('email')->primary();
|
||||||
|
$table->string('token');
|
||||||
|
$table->timestamp('created_at')->nullable();
|
||||||
|
});
|
||||||
|
|
||||||
|
Schema::create('sessions', function (Blueprint $table) {
|
||||||
|
$table->string('id')->primary();
|
||||||
|
$table->foreignId('user_id')->nullable()->index();
|
||||||
|
$table->string('ip_address', 45)->nullable();
|
||||||
|
$table->text('user_agent')->nullable();
|
||||||
|
$table->longText('payload');
|
||||||
|
$table->integer('last_activity')->index();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reverse the migrations.
|
||||||
|
*/
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::dropIfExists('users');
|
||||||
|
Schema::dropIfExists('password_reset_tokens');
|
||||||
|
Schema::dropIfExists('sessions');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,35 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Run the migrations.
|
||||||
|
*/
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
Schema::create('cache', function (Blueprint $table) {
|
||||||
|
$table->string('key')->primary();
|
||||||
|
$table->mediumText('value');
|
||||||
|
$table->bigInteger('expiration')->index();
|
||||||
|
});
|
||||||
|
|
||||||
|
Schema::create('cache_locks', function (Blueprint $table) {
|
||||||
|
$table->string('key')->primary();
|
||||||
|
$table->string('owner');
|
||||||
|
$table->bigInteger('expiration')->index();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reverse the migrations.
|
||||||
|
*/
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::dropIfExists('cache');
|
||||||
|
Schema::dropIfExists('cache_locks');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,59 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Run the migrations.
|
||||||
|
*/
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
Schema::create('jobs', function (Blueprint $table) {
|
||||||
|
$table->id();
|
||||||
|
$table->string('queue')->index();
|
||||||
|
$table->longText('payload');
|
||||||
|
$table->unsignedSmallInteger('attempts');
|
||||||
|
$table->unsignedInteger('reserved_at')->nullable();
|
||||||
|
$table->unsignedInteger('available_at');
|
||||||
|
$table->unsignedInteger('created_at');
|
||||||
|
});
|
||||||
|
|
||||||
|
Schema::create('job_batches', function (Blueprint $table) {
|
||||||
|
$table->string('id')->primary();
|
||||||
|
$table->string('name');
|
||||||
|
$table->integer('total_jobs');
|
||||||
|
$table->integer('pending_jobs');
|
||||||
|
$table->integer('failed_jobs');
|
||||||
|
$table->longText('failed_job_ids');
|
||||||
|
$table->mediumText('options')->nullable();
|
||||||
|
$table->integer('cancelled_at')->nullable();
|
||||||
|
$table->integer('created_at');
|
||||||
|
$table->integer('finished_at')->nullable();
|
||||||
|
});
|
||||||
|
|
||||||
|
Schema::create('failed_jobs', function (Blueprint $table) {
|
||||||
|
$table->id();
|
||||||
|
$table->string('uuid')->unique();
|
||||||
|
$table->string('connection');
|
||||||
|
$table->string('queue');
|
||||||
|
$table->longText('payload');
|
||||||
|
$table->longText('exception');
|
||||||
|
$table->timestamp('failed_at')->useCurrent();
|
||||||
|
|
||||||
|
$table->index(['connection', 'queue', 'failed_at']);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reverse the migrations.
|
||||||
|
*/
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::dropIfExists('jobs');
|
||||||
|
Schema::dropIfExists('job_batches');
|
||||||
|
Schema::dropIfExists('failed_jobs');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,27 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Database\Seeders;
|
||||||
|
|
||||||
|
use App\Models\User;
|
||||||
|
use Illuminate\Database\Seeder;
|
||||||
|
use Illuminate\Support\Facades\Hash;
|
||||||
|
|
||||||
|
class DatabaseSeeder extends Seeder
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Seed the application's database.
|
||||||
|
*
|
||||||
|
* Admin credentials come from the (gitignored) .env so no secret is committed.
|
||||||
|
*/
|
||||||
|
public function run(): void
|
||||||
|
{
|
||||||
|
User::updateOrCreate(
|
||||||
|
['email' => env('HOMEOS_ADMIN_EMAIL', 'admin@homeos.local')],
|
||||||
|
[
|
||||||
|
'name' => env('HOMEOS_ADMIN_NAME', 'Admin'),
|
||||||
|
'password' => Hash::make(env('HOMEOS_ADMIN_PASSWORD', 'homeos-dev')),
|
||||||
|
'email_verified_at' => now(),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
File diff suppressed because one or more lines are too long
|
|
@ -0,0 +1,73 @@
|
||||||
|
# HomeOS — dev stack (dev = prod parity, R8). All app-family services share one image.
|
||||||
|
# mosquitto / mqtt-listener (Phase 3) and discovery sidecar (Phase 4) are added in their phases.
|
||||||
|
|
||||||
|
x-app-image: &app-image
|
||||||
|
image: homeos-app:dev
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
dockerfile: docker/app/Dockerfile
|
||||||
|
args:
|
||||||
|
HOST_UID: ${HOST_UID:-1000}
|
||||||
|
HOST_GID: ${HOST_GID:-1000}
|
||||||
|
# Laravel reads its own .env from the mounted volume (correct phpdotenv interpolation).
|
||||||
|
# Compose still reads root .env for ${...} substitution in this file (ports, uid, db creds).
|
||||||
|
volumes:
|
||||||
|
- .:/var/www/html
|
||||||
|
- ./docker/nginx/default.conf:/etc/nginx/sites-available/default:ro
|
||||||
|
depends_on:
|
||||||
|
db:
|
||||||
|
condition: service_healthy
|
||||||
|
redis:
|
||||||
|
condition: service_started
|
||||||
|
restart: unless-stopped
|
||||||
|
|
||||||
|
services:
|
||||||
|
app:
|
||||||
|
<<: *app-image
|
||||||
|
ports:
|
||||||
|
- "${APP_PORT:-80}:80"
|
||||||
|
- "${VITE_PORT:-5173}:5173"
|
||||||
|
|
||||||
|
horizon:
|
||||||
|
<<: *app-image
|
||||||
|
command: ["php", "artisan", "horizon"]
|
||||||
|
ports: []
|
||||||
|
|
||||||
|
scheduler:
|
||||||
|
<<: *app-image
|
||||||
|
command: ["php", "artisan", "schedule:work"]
|
||||||
|
ports: []
|
||||||
|
|
||||||
|
reverb:
|
||||||
|
<<: *app-image
|
||||||
|
command: ["php", "artisan", "reverb:start", "--host=0.0.0.0", "--port=8080"]
|
||||||
|
ports:
|
||||||
|
- "${REVERB_HOST_PORT:-6001}:8080"
|
||||||
|
|
||||||
|
db:
|
||||||
|
image: timescale/timescaledb:latest-pg17
|
||||||
|
environment:
|
||||||
|
POSTGRES_DB: ${DB_DATABASE:-homeos}
|
||||||
|
POSTGRES_USER: ${DB_USERNAME:-homeos}
|
||||||
|
POSTGRES_PASSWORD: ${DB_PASSWORD:-homeos}
|
||||||
|
volumes:
|
||||||
|
- db-data:/var/lib/postgresql/data
|
||||||
|
ports:
|
||||||
|
- "127.0.0.1:${DB_HOST_PORT:-5432}:5432"
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD-SHELL", "pg_isready -U ${DB_USERNAME:-homeos} -d ${DB_DATABASE:-homeos}"]
|
||||||
|
interval: 5s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 10
|
||||||
|
restart: unless-stopped
|
||||||
|
|
||||||
|
redis:
|
||||||
|
image: redis:7-alpine
|
||||||
|
command: ["redis-server", "--appendonly", "yes"]
|
||||||
|
volumes:
|
||||||
|
- redis-data:/data
|
||||||
|
restart: unless-stopped
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
db-data:
|
||||||
|
redis-data:
|
||||||
|
|
@ -0,0 +1,52 @@
|
||||||
|
# HomeOS application image — php-fpm 8.4 + nginx + node/vite + supervisor.
|
||||||
|
# Reused by: app, horizon, scheduler, reverb, mqtt-listener (each overrides `command`).
|
||||||
|
FROM php:8.4-fpm-bookworm
|
||||||
|
|
||||||
|
# --- system packages -------------------------------------------------------
|
||||||
|
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||||
|
git curl unzip zip ca-certificates gnupg procps \
|
||||||
|
libpq-dev libzip-dev libicu-dev libonig-dev \
|
||||||
|
nginx supervisor \
|
||||||
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
# --- PHP extensions --------------------------------------------------------
|
||||||
|
RUN docker-php-ext-configure intl \
|
||||||
|
&& docker-php-ext-install -j"$(nproc)" \
|
||||||
|
pdo_pgsql pgsql bcmath zip intl opcache pcntl sockets \
|
||||||
|
&& pecl install redis \
|
||||||
|
&& docker-php-ext-enable redis
|
||||||
|
|
||||||
|
# --- Composer --------------------------------------------------------------
|
||||||
|
COPY --from=composer:2 /usr/bin/composer /usr/bin/composer
|
||||||
|
|
||||||
|
# --- Node 20 (Vite build + dev) -------------------------------------------
|
||||||
|
RUN curl -fsSL https://deb.nodesource.com/setup_20.x | bash - \
|
||||||
|
&& apt-get install -y --no-install-recommends nodejs \
|
||||||
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
# --- non-root user matching the host uid/gid (clean volume permissions) ----
|
||||||
|
ARG HOST_UID=1000
|
||||||
|
ARG HOST_GID=1000
|
||||||
|
RUN if getent group "${HOST_GID}" >/dev/null; then \
|
||||||
|
groupmod -n app "$(getent group ${HOST_GID} | cut -d: -f1)"; \
|
||||||
|
else groupadd -g "${HOST_GID}" app; fi \
|
||||||
|
&& if getent passwd "${HOST_UID}" >/dev/null; then \
|
||||||
|
usermod -l app -g "${HOST_GID}" -d /home/app -m "$(getent passwd ${HOST_UID} | cut -d: -f1)"; \
|
||||||
|
else useradd -u "${HOST_UID}" -g "${HOST_GID}" -m -s /bin/bash app; fi
|
||||||
|
|
||||||
|
# --- config ----------------------------------------------------------------
|
||||||
|
COPY docker/app/php.ini /usr/local/etc/php/conf.d/zz-homeos.ini
|
||||||
|
COPY docker/app/php-fpm-pool.conf /usr/local/etc/php-fpm.d/zz-homeos.conf
|
||||||
|
COPY docker/nginx/default.conf /etc/nginx/sites-available/default
|
||||||
|
COPY docker/app/supervisord.conf /etc/supervisor/conf.d/homeos.conf
|
||||||
|
|
||||||
|
# nginx needs writable runtime dirs owned by app
|
||||||
|
RUN mkdir -p /var/lib/nginx/body /var/log/nginx /run \
|
||||||
|
&& chown -R app:app /var/lib/nginx /var/log/nginx
|
||||||
|
|
||||||
|
WORKDIR /var/www/html
|
||||||
|
|
||||||
|
EXPOSE 80 5173 8080
|
||||||
|
|
||||||
|
# Default: app service (php-fpm + nginx via supervisor). Other services override command.
|
||||||
|
CMD ["/usr/bin/supervisord", "-c", "/etc/supervisor/conf.d/homeos.conf"]
|
||||||
|
|
@ -0,0 +1,12 @@
|
||||||
|
[www]
|
||||||
|
user = app
|
||||||
|
group = app
|
||||||
|
listen = 127.0.0.1:9000
|
||||||
|
pm = dynamic
|
||||||
|
pm.max_children = 20
|
||||||
|
pm.start_servers = 3
|
||||||
|
pm.min_spare_servers = 2
|
||||||
|
pm.max_spare_servers = 6
|
||||||
|
clear_env = no
|
||||||
|
catch_workers_output = yes
|
||||||
|
decorate_workers_output = no
|
||||||
|
|
@ -0,0 +1,12 @@
|
||||||
|
; HomeOS PHP overrides
|
||||||
|
memory_limit = 512M
|
||||||
|
upload_max_filesize = 32M
|
||||||
|
post_max_size = 32M
|
||||||
|
max_execution_time = 120
|
||||||
|
date.timezone = UTC
|
||||||
|
|
||||||
|
; opcache (dev-friendly: validate timestamps so edits take effect)
|
||||||
|
opcache.enable = 1
|
||||||
|
opcache.enable_cli = 0
|
||||||
|
opcache.validate_timestamps = 1
|
||||||
|
opcache.revalidate_freq = 0
|
||||||
|
|
@ -0,0 +1,40 @@
|
||||||
|
[supervisord]
|
||||||
|
nodaemon=true
|
||||||
|
user=root
|
||||||
|
logfile=/dev/null
|
||||||
|
logfile_maxbytes=0
|
||||||
|
pidfile=/run/supervisord.pid
|
||||||
|
|
||||||
|
[program:php-fpm]
|
||||||
|
command=php-fpm -F
|
||||||
|
autostart=true
|
||||||
|
autorestart=true
|
||||||
|
stdout_logfile=/dev/stdout
|
||||||
|
stdout_logfile_maxbytes=0
|
||||||
|
stderr_logfile=/dev/stderr
|
||||||
|
stderr_logfile_maxbytes=0
|
||||||
|
priority=10
|
||||||
|
|
||||||
|
[program:nginx]
|
||||||
|
command=nginx -g "daemon off;"
|
||||||
|
autostart=true
|
||||||
|
autorestart=true
|
||||||
|
stdout_logfile=/dev/stdout
|
||||||
|
stdout_logfile_maxbytes=0
|
||||||
|
stderr_logfile=/dev/stderr
|
||||||
|
stderr_logfile_maxbytes=0
|
||||||
|
priority=20
|
||||||
|
|
||||||
|
; Vite dev server (HMR). Off by default — Phase 1 verification uses built assets
|
||||||
|
; (npm run build) for LAN reliability. Start manually: supervisorctl start vite
|
||||||
|
[program:vite]
|
||||||
|
command=npm run dev -- --host 0.0.0.0
|
||||||
|
directory=/var/www/html
|
||||||
|
user=app
|
||||||
|
autostart=false
|
||||||
|
autorestart=true
|
||||||
|
stdout_logfile=/dev/stdout
|
||||||
|
stdout_logfile_maxbytes=0
|
||||||
|
stderr_logfile=/dev/stderr
|
||||||
|
stderr_logfile_maxbytes=0
|
||||||
|
priority=30
|
||||||
|
|
@ -0,0 +1,55 @@
|
||||||
|
# WebSocket upgrade mapping (http context)
|
||||||
|
map $http_upgrade $connection_upgrade {
|
||||||
|
default upgrade;
|
||||||
|
'' close;
|
||||||
|
}
|
||||||
|
|
||||||
|
server {
|
||||||
|
listen 80 default_server;
|
||||||
|
listen [::]:80 default_server;
|
||||||
|
server_name _;
|
||||||
|
root /var/www/html/public;
|
||||||
|
index index.php;
|
||||||
|
|
||||||
|
charset utf-8;
|
||||||
|
client_max_body_size 32M;
|
||||||
|
|
||||||
|
# Reverb (Pusher protocol) proxied same-origin: browser connects to /app/<key>,
|
||||||
|
# Laravel publishes to /apps/<id>/events. Keeps WS on the page's own host/port.
|
||||||
|
# Docker embedded DNS + variable proxy_pass → runtime resolution (reverb may start
|
||||||
|
# after nginx, or change IP on restart, without breaking the config).
|
||||||
|
location ~ ^/(app|apps)(/|$) {
|
||||||
|
resolver 127.0.0.11 valid=30s ipv6=off;
|
||||||
|
set $reverb_upstream reverb;
|
||||||
|
proxy_pass http://$reverb_upstream:8080$request_uri;
|
||||||
|
proxy_http_version 1.1;
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_set_header Upgrade $http_upgrade;
|
||||||
|
proxy_set_header Connection $connection_upgrade;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
|
proxy_read_timeout 600s;
|
||||||
|
proxy_send_timeout 600s;
|
||||||
|
}
|
||||||
|
|
||||||
|
location / {
|
||||||
|
try_files $uri $uri/ /index.php?$query_string;
|
||||||
|
}
|
||||||
|
|
||||||
|
location = /favicon.ico { access_log off; log_not_found off; }
|
||||||
|
location = /robots.txt { access_log off; log_not_found off; }
|
||||||
|
|
||||||
|
error_page 404 /index.php;
|
||||||
|
|
||||||
|
location ~ \.php$ {
|
||||||
|
fastcgi_pass 127.0.0.1:9000;
|
||||||
|
fastcgi_index index.php;
|
||||||
|
fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name;
|
||||||
|
include fastcgi_params;
|
||||||
|
fastcgi_read_timeout 120s;
|
||||||
|
}
|
||||||
|
|
||||||
|
location ~ /\.(?!well-known).* {
|
||||||
|
deny all;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,424 @@
|
||||||
|
# handoff.md — HomeOS
|
||||||
|
|
||||||
|
> **Status: FINAL (2026-07-17).** This is the authoritative handoff — supersedes any earlier
|
||||||
|
> draft or chat summary.
|
||||||
|
>
|
||||||
|
> **Handoff for the Claude Code session on the server.** This document is the source of truth
|
||||||
|
> to build the project from scratch on the target VM. Read fully before the first command.
|
||||||
|
> Starting state of the VM: see §6 (user `nexxo` + sudo, Docker installed, otherwise empty).
|
||||||
|
> Companion file: `design-mockup.html` (interactive design reference — open in a browser;
|
||||||
|
> contains all three views: Dashboard, access gate, mobile/PWA layout).
|
||||||
|
>
|
||||||
|
> Meta-docs are English; **UI copy is German** (localized DE+EN, see rules). Conventions and
|
||||||
|
> STRICT RULES are carried over from the user's Clusev project and are **non-negotiable**.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Product
|
||||||
|
|
||||||
|
**HomeOS** *(working title — rename is an open decision, see §12)* — a **self-built smart-home
|
||||||
|
control plane**. Explicitly NOT Home Assistant and not a wrapper around it: own data model, own
|
||||||
|
integrations, own UI. Single household, self-hosted, LAN-first.
|
||||||
|
|
||||||
|
**Core capabilities (full vision):**
|
||||||
|
- Automatic **device discovery** on the network (mDNS/SSDP/ARP) with an "assign new device" flow
|
||||||
|
- **Device control** (switches, lights, plugs, power metering; later TVs, covers, …)
|
||||||
|
- **Rooms** and **persons**: every device/entity assignable to a room and optionally a person
|
||||||
|
- **Presence detection** (home/away per person) via UniFi + later geofencing, score-based fusion
|
||||||
|
- **Window/door contact sensors** with status + automations
|
||||||
|
- **Automations** (trigger → condition → action)
|
||||||
|
- Live dashboard (Reverb) + later **PWA** on the phone
|
||||||
|
- Later: settings area protected by **WebAuthn/passkeys** (deferred, see §11)
|
||||||
|
|
||||||
|
**v1 scope (build this first — do NOT boil the ocean):**
|
||||||
|
1. Foundation: Docker stack, auth, design system, device/entity/room model, MQTT ingest,
|
||||||
|
Reverb live updates, audit/command log
|
||||||
|
2. **Shelly integration** (the user's actual hardware today — see §4)
|
||||||
|
3. Discovery sidecar + "Neue Geräte" assignment flow
|
||||||
|
4. Presence via **UniFi API** (the user's actual network — see §5)
|
||||||
|
5. Automations engine (minimal TCA: state-change + time triggers, few actions)
|
||||||
|
|
||||||
|
**Deferred (keep OUT of v1):** face recognition entirely (later, maybe), WebAuthn gate,
|
||||||
|
TV/Chromecast control, Matter, Zigbee (until hardware decision), Ring bridge, native mobile app
|
||||||
|
(PWA later covers it), energy history charts beyond a simple sparkline.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Tech stack (hard requirements)
|
||||||
|
|
||||||
|
| Layer | Choice |
|
||||||
|
|---|---|
|
||||||
|
| Framework | **Laravel 13** (current major; user's newer projects run 13 — confirm once, see §12) |
|
||||||
|
| UI | **Livewire v3** — class-based, **NOT Volt** |
|
||||||
|
| CSS | **Tailwind v4** (CSS-first, `@theme` in `app.css`, no `tailwind.config.js`) |
|
||||||
|
| Build | **Vite** |
|
||||||
|
| Modals | **wire-elements/modal** |
|
||||||
|
| Realtime | **Laravel Reverb** + Echo — **every channel PRIVATE** |
|
||||||
|
| Queue/Cache | **Redis** (+ Horizon) |
|
||||||
|
| DB | **PostgreSQL 17 + TimescaleDB** recommended (telemetry hypertables, compression, retention). Fallback if user prefers stack consistency with Clusev: MariaDB + monthly partitioning + prune job. **Open decision §12 — ask before migrating.** |
|
||||||
|
| MQTT | **Eclipse Mosquitto 2** (auth + per-client ACLs from day one) |
|
||||||
|
| MQTT client | **php-mqtt/laravel-client** (^1.8) — subscriber runs as its own Artisan daemon container |
|
||||||
|
| Discovery | **Python sidecar** container (`network_mode: host`): python-zeroconf (mDNS), async-upnp-client (SSDP), arp-scan — reports via MQTT. PHP cannot do multicast listening from the bridge network. |
|
||||||
|
| Charts | JS lib as Alpine island (uPlot or ApexCharts) — v1 needs only sparklines |
|
||||||
|
| Icons | **Lucide**, inline SVG via `x-icon` Blade component — no emoji anywhere |
|
||||||
|
| Fonts | **Plus Jakarta Sans** (UI) · **IBM Plex Mono** (values, MACs, IPs, timestamps) — **self-hosted woff2 only** (R14) |
|
||||||
|
|
||||||
|
Composer: `livewire/livewire wire-elements/modal laravel/reverb laravel/horizon php-mqtt/laravel-client`
|
||||||
|
NPM: `tailwindcss @tailwindcss/vite laravel-echo pusher-js` (+ chart lib)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Architecture
|
||||||
|
|
||||||
|
**MQTT-first hybrid.** Mosquitto is the central bus. Everything that speaks MQTT natively
|
||||||
|
(Shelly Gen2+, later Zigbee2MQTT, Tasmota, WLED, ring-mqtt) connects directly. Devices without
|
||||||
|
MQTT get adapters later (HTTP drivers in PHP, or sidecars for TV/Cast/Matter). The UI never
|
||||||
|
touches protocol topics directly — **always through the DB mapping** (protocol-agnostic).
|
||||||
|
|
||||||
|
```
|
||||||
|
devices ──MQTT──► mosquitto ──► mqtt-listener (artisan daemon)
|
||||||
|
│ parse + dispatch only (NEVER heavy work in the loop)
|
||||||
|
▼
|
||||||
|
Events / Horizon jobs ──► DB upsert (device_states)
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
Reverb (private channels) ──► Livewire UI (Echo)
|
||||||
|
|
||||||
|
UI action ──► Livewire ──► DeviceCommand job ──► driver (MqttDriver | HttpDriver) ──► device
|
||||||
|
└── every command written to command log (audit)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Data model (core tables):**
|
||||||
|
- `users`, `rooms`, `persons` (person ↔ user optional link)
|
||||||
|
- `devices` — identity: uuid (route key, R11), name, vendor, model, protocol, connection config
|
||||||
|
(JSON: topic prefix / IP / auth ref), `room_id`, `person_id` nullable, first_seen/last_seen,
|
||||||
|
approval state (`discovered` | `active` | `ignored`)
|
||||||
|
- `entities` — one capability per row: type (`switch`, `light`, `power`, `contact`, `temperature`,
|
||||||
|
`humidity`, `motion`, …), capabilities JSON, `device_id`
|
||||||
|
- `device_states` — CURRENT state only: `entity_id` unique, state JSON, updated_at (pure upserts,
|
||||||
|
stays small and fast)
|
||||||
|
- `telemetry` — history (TimescaleDB hypertable; numeric value column + state JSON). Compression
|
||||||
|
+ retention policy **from day one** (a 30 s sensor ≈ 1 M rows/year)
|
||||||
|
- `commands` — audit: who/what switched which entity when (user | automation | physical),
|
||||||
|
payload, result. Unverzichtbar for debugging "warum ging das Licht an?"
|
||||||
|
- `automations` — name, enabled, trigger_type + trigger_config JSON, conditions JSON,
|
||||||
|
actions JSON, `cooldown_seconds`, `last_triggered_at` (cooldown from day one — motion sensors
|
||||||
|
without debounce flood the queue)
|
||||||
|
- `presence_signals` + computed person state (see §5)
|
||||||
|
- `discovery_findings` — raw sidecar reports before a device is approved
|
||||||
|
|
||||||
|
**Driver contract:** `app/Support/Drivers/DeviceDriver` interface (`turnOn`, `turnOff`,
|
||||||
|
`setState`, `capabilities`). v1 implements `ShellyMqttDriver`. Everything else is a later driver
|
||||||
|
behind the same contract — this is what keeps the system "für alle Produkte brauchbar".
|
||||||
|
|
||||||
|
**Broadcasting:** private channels only — `rooms.{uuid}`, `devices.{uuid}`, `presence`,
|
||||||
|
`discovery`. Authorize in `routes/channels.php`. State changes broadcast with
|
||||||
|
`ShouldBroadcastNow` (or a dedicated high-priority queue) — a slow job in the default queue must
|
||||||
|
never delay a light toggle in the UI.
|
||||||
|
|
||||||
|
**Long-runners:** `mqtt-listener`, `reverb`, `horizon`, `scheduler` are separate containers off
|
||||||
|
the same PHP image. MQTT loop rule: parse + dispatch only; reconnect with exponential backoff;
|
||||||
|
graceful SIGTERM shutdown via pcntl.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Hardware reality (drives the build order)
|
||||||
|
|
||||||
|
**Today the user owns: Shelly devices only, plus one Ring product. No Zigbee, no Hue.**
|
||||||
|
|
||||||
|
**Shelly (v1 target):**
|
||||||
|
- Gen2/Gen3 speak JSON-RPC over **MQTT** (enable MQTT in device settings → point at Mosquitto),
|
||||||
|
plus HTTP `POST /rpc` and WebSocket. Status arrives on `<device>/status/...`, notifications via
|
||||||
|
`rpc_ntf`; commands via RPC (`Switch.Set`, `Light.Set`).
|
||||||
|
- Discovery: Shellys announce via mDNS (`_shelly._tcp`) and expose `GET /shelly` (identity JSON:
|
||||||
|
model, gen, mac) — the sidecar uses both to classify.
|
||||||
|
- Power metering (PM devices) → `power` entity → dashboard energy tile.
|
||||||
|
- Docs: shelly-api-docs.shelly.cloud
|
||||||
|
|
||||||
|
**Window/door contacts (near future, still Shelly-native):** **Shelly BLU Door/Window**
|
||||||
|
(Bluetooth) received by any Shelly Gen3 device or a Shelly BLU Gateway acting as BT gateway —
|
||||||
|
events surface over the same MQTT path. This avoids buying into Zigbee just for contacts.
|
||||||
|
Zigbee (Zigbee2MQTT + LAN coordinator SLZB-06, never USB passthrough into the Proxmox VM)
|
||||||
|
remains the documented expansion path if/when the user buys Zigbee hardware.
|
||||||
|
|
||||||
|
**Ring (deferred, optional):** no official local API. Established bridge: **ring-mqtt**
|
||||||
|
(tsightler/ring-mqtt, Docker) — cloud-based, token auth, surfaces motion/ding/contact/camera
|
||||||
|
events on MQTT. Fits the bus as-is. Cloud dependency must be labeled in the UI (device badge
|
||||||
|
"Cloud"). Do not build a custom Ring client.
|
||||||
|
|
||||||
|
**Later expansion paths (documented, not built):** Hue via local CLIP v2 (HTTPS, HTTP driver in
|
||||||
|
PHP — existing PHP Hue packages are all deprecated v1 API, don't use them), TVs (LG SSAP /
|
||||||
|
Samsung Tizen websockets) + Chromecast + **Matter** (matter.js + @matter/mqtt) via a Node
|
||||||
|
sidecar, Tasmota/WLED native MQTT. The entity/driver model above must never assume Shelly.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. Network topology & presence
|
||||||
|
|
||||||
|
**Actual network:** Fritz!Box (internet, NAT) → **UniFi Dream Machine** (own NAT — double NAT,
|
||||||
|
works fine, accepted) → LAN/WLAN with all devices and phones.
|
||||||
|
|
||||||
|
Consequences:
|
||||||
|
- **Presence source of truth = UniFi**, NOT Fritz!Box (phones associate with UniFi WLAN).
|
||||||
|
Package: **art-of-wifi/unifi-api-client** (`list_clients()` → MAC, IP, AP, last_seen). UDM =
|
||||||
|
UniFi OS console (port 443); create a dedicated read-only local user or API key for HomeOS.
|
||||||
|
Fritz!Box TR-064 is irrelevant here (it only sees the UDM as one client).
|
||||||
|
- Presence engine: "home" immediately on association; "away" only after **5–10 min debounce**
|
||||||
|
(`last_seen_at` + scheduler sweep). iPhone WLAN sleep causes false-aways otherwise. Never hang
|
||||||
|
a security automation on a single raw signal.
|
||||||
|
- MAC randomization: private MAC is stable per SSID — onboarding flow assigns "this client =
|
||||||
|
person X" from the live UniFi client list instead of asking for hardware MACs.
|
||||||
|
- Later additions: OwnTracks geofencing (MQTT, fastest clean "left home" signal), score-based
|
||||||
|
fusion of UniFi + GPS (+ BLE if ever). v1 = UniFi only.
|
||||||
|
- **Remote access:** double NAT makes port forwarding annoying (two chained forwards) — use
|
||||||
|
**Tailscale** (or WireGuard on the UDM) instead. No open ports. This is also the interim
|
||||||
|
answer for "PWA von unterwegs" until a public domain + TLS is set up.
|
||||||
|
- **WebAuthn (deferred)** will require HTTPS + a real domain (RP ID cannot be an IP; domain
|
||||||
|
change invalidates all passkeys) — plan `home.<domain>` + Let's Encrypt DNS-01 via Caddy when
|
||||||
|
that phase starts.
|
||||||
|
- Proxmox: VM NIC on vmbr0, **same L2/VLAN as the smart-home devices**, otherwise
|
||||||
|
mDNS/SSDP discovery is blind. If IoT ever moves to its own VLAN → mDNS reflector required.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. Docker topology (dev = prod parity, R8 — host has only Docker)
|
||||||
|
|
||||||
|
**Starting point on the target VM (already prepared — this is where you begin):**
|
||||||
|
- User **`nexxo`** exists and has **sudo**. Work as this user.
|
||||||
|
- **Docker is already installed.** Verify with `docker compose version` as one of the first
|
||||||
|
commands; if the compose plugin is missing, report it.
|
||||||
|
- The VM is otherwise **empty**: no PHP, no Composer, no Node on the host — hence R8, every
|
||||||
|
project command runs in containers (bootstrap Laravel via a throwaway container).
|
||||||
|
- Project root: **`/home/nexxo/homeos`** (own git repo — does not exist yet, create it during
|
||||||
|
bootstrap; feature branches, not `main`). Read `HOST_UID`/`HOST_GID` from `id nexxo` into
|
||||||
|
`.env` — do not hardcode a UID.
|
||||||
|
|
||||||
|
Services in `docker-compose.yml`:
|
||||||
|
|
||||||
|
| Service | Role |
|
||||||
|
|---|---|
|
||||||
|
| `app` | php-fpm + nginx + Vite (dev) via supervisor — Laravel + Livewire |
|
||||||
|
| `horizon` | queue workers (same image, `php artisan horizon`) |
|
||||||
|
| `scheduler` | `php artisan schedule:work` (same image) — presence sweeps, retention, polling fallbacks |
|
||||||
|
| `reverb` | `php artisan reverb:start` (same image) |
|
||||||
|
| `mqtt-listener` | `php artisan mqtt:listen` (same image, `restart: always`) |
|
||||||
|
| `mosquitto` | eclipse-mosquitto:2 — password auth + ACL per client (laravel, sidecar, shelly, later z2m/ring) |
|
||||||
|
| `db` | timescale/timescaledb:latest-pg17 (or MariaDB — §12) |
|
||||||
|
| `redis` | redis:7-alpine |
|
||||||
|
| `discovery` | Python sidecar, **`network_mode: host`** + `cap_add: [NET_RAW]` |
|
||||||
|
|
||||||
|
Port planning (host-mode container shares the VM's ports — plan collisions explicitly):
|
||||||
|
app **:80**, Vite **:5173**, Reverb internal 8080 → host **:6001**, Mosquitto **:1883**
|
||||||
|
(LAN only), DB bound to 127.0.0.1. All ports env-driven, nothing hardcoded.
|
||||||
|
|
||||||
|
Ops non-negotiables:
|
||||||
|
- **Backups:** Proxmox vzdump for the VM + DB dump job; later `coordinator_backup.json` when
|
||||||
|
Zigbee exists. Mosquitto persistence volume.
|
||||||
|
- **Timezone:** app logic Europe/Berlin, storage UTC; VM NTP-synced (DST double/zero-fire bug
|
||||||
|
in time triggers otherwise).
|
||||||
|
- **Reverb behind proxy:** websocket upgrade headers + long `proxy_read_timeout`, or sockets
|
||||||
|
drop every 60 s.
|
||||||
|
- **Graceful degradation:** server down must never make the home unusable — physical switches
|
||||||
|
keep working because Shellys switch locally (detached/relay mode kept functional).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. Folder map (R6 — follow exactly)
|
||||||
|
|
||||||
|
```
|
||||||
|
app/
|
||||||
|
Livewire/
|
||||||
|
Dashboard.php
|
||||||
|
Rooms/{Index.php, Show.php}
|
||||||
|
Devices/{Index.php, Show.php}
|
||||||
|
Discovery/Index.php
|
||||||
|
Persons/Index.php
|
||||||
|
Automations/{Index.php, Edit.php}
|
||||||
|
Settings/Index.php
|
||||||
|
Modals/ # wire-elements/modal (ConfirmDelete, AssignDevice, …)
|
||||||
|
Concerns/
|
||||||
|
Models/ # Device, Entity, DeviceState, Room, Person, Automation, Command, DiscoveryFinding
|
||||||
|
Support/
|
||||||
|
Mqtt/ # topic mapping, payload normalizers per vendor
|
||||||
|
Drivers/ # DeviceDriver contract + ShellyMqttDriver (+ later drivers)
|
||||||
|
Services/ # DeviceCommandService, PresenceEngine, AutomationEngine, DiscoveryService, UnifiClient
|
||||||
|
Events/ # DeviceStateChanged, DeviceDiscovered, PresenceChanged (ShouldBroadcastNow)
|
||||||
|
Jobs/
|
||||||
|
Console/Commands/MqttListenCommand.php
|
||||||
|
resources/
|
||||||
|
views/livewire/… # mirrors app/Livewire, kebab-case
|
||||||
|
views/components/ # x-panel, x-kpi, x-status-pill, x-status-dot, x-badge, x-icon,
|
||||||
|
# x-room-card, x-device-chip, x-toggle, sidebar, topbar
|
||||||
|
views/layouts/app.blade.php
|
||||||
|
css/app.css # @import "tailwindcss"; @theme { tokens §8 }
|
||||||
|
fonts/ # plus-jakarta-sans*.woff2, ibm-plex-mono*.woff2 (Vite-bundled)
|
||||||
|
js/app.js # Echo bootstrap + Alpine chart islands
|
||||||
|
lang/{de,en}/ # common, dashboard, rooms, devices, discovery, persons, automations, settings, modals
|
||||||
|
routes/web.php # full-page Livewire routes, English paths (R13)
|
||||||
|
routes/channels.php # private-channel auth
|
||||||
|
sidecar/ # Python discovery sidecar (own Dockerfile)
|
||||||
|
docker/ docker-compose.yml docker-compose.prod.yml
|
||||||
|
handoff.md rules.md CLAUDE.md design-mockup.html
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. Design system — "Enterprise Ops, dark OLED"
|
||||||
|
|
||||||
|
Reference: **`design-mockup.html`** (open it — it is the approved direction: UniFi/Grafana-class
|
||||||
|
ops console, NOT a consumer app). Port these tokens verbatim into `@theme` (R3); markup uses
|
||||||
|
token utilities only.
|
||||||
|
|
||||||
|
```css
|
||||||
|
@theme {
|
||||||
|
/* surfaces */
|
||||||
|
--color-base: #080D18; /* page bg, blue-biased OLED black */
|
||||||
|
--color-surface: #0D1424; /* cards */
|
||||||
|
--color-raised: #121C31; /* nested surfaces, chips */
|
||||||
|
--color-inset: #18233D; /* icon wells, kbd */
|
||||||
|
/* text ramp */
|
||||||
|
--color-ink: #EAF0FB;
|
||||||
|
--color-ink-2: #93A1BD;
|
||||||
|
--color-ink-3: #5E6C8A;
|
||||||
|
/* brand */
|
||||||
|
--color-accent: #4FC1FF; /* cyan — interaction, active nav, toggles; tints via /10 /25 */
|
||||||
|
/* status triad — reserved, never decorative */
|
||||||
|
--color-online: #34D399;
|
||||||
|
--color-warning: #FBBF24;
|
||||||
|
--color-offline: #F87171;
|
||||||
|
/* hairlines */
|
||||||
|
--color-line: #1C2846;
|
||||||
|
--color-line-soft: #141E36;
|
||||||
|
/* fonts */
|
||||||
|
--font-sans: "Plus Jakarta Sans", ui-sans-serif, system-ui;
|
||||||
|
--font-mono: "IBM Plex Mono", ui-monospace, monospace;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Rules of the look: status via colored dot/pill + text (never color alone, never emoji);
|
||||||
|
**every numeric value, MAC, IP, timestamp in `font-mono` with `tabular-nums`**; hairline borders
|
||||||
|
instead of shadows; accent used sparingly (one primary action per view); subtle pulse on the
|
||||||
|
live indicator; `prefers-reduced-motion` respected. Responsive 375/768/1280 (R7): sidebar →
|
||||||
|
drawer, KPI grid 5→3→2→1, right rail stacks below.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 9. STRICT RULES (carried over — compact form)
|
||||||
|
|
||||||
|
> The user's Clusev `rules.md` applies 1:1 to this project (full examples live there).
|
||||||
|
> Copy this list into `rules.md` in the repo at bootstrap. On any conflict: **STOP and ask.**
|
||||||
|
|
||||||
|
- **R1** Pages = full-page class-based Livewire components mapped directly in `routes/web.php`. No page controllers.
|
||||||
|
- **R2** Livewire class + separate Blade view. **Volt forbidden** (never `make:volt`).
|
||||||
|
- **R3** All colors/design tokens in `@theme` (`app.css`); markup uses token utilities only — no raw hex/rgb.
|
||||||
|
- **R4** No inline `style=` — single exception: progress-bar `width`.
|
||||||
|
- **R5** Destructive/confirm actions via **wire-elements/modal** — never `confirm()`/Alpine popups.
|
||||||
|
- **R6** Files exactly per folder map (§7).
|
||||||
|
- **R7** Responsive verified at 375/768/1280; touch targets ≥ 44 px.
|
||||||
|
- **R8** Every PHP/Composer/Node/artisan command **inside the container** — host has only Docker.
|
||||||
|
- **R9** UI copy German, terse/operational, **no emoji**; status via color/dots/pills; technical tokens stay native.
|
||||||
|
- **R10** Reuse `@theme` tokens + Blade component kit — no ad-hoc colors/widgets.
|
||||||
|
- **R11** URL-exposed records addressed by **UUID**, never integer PK (`getRouteKeyName(): 'uuid'`).
|
||||||
|
- **R12** Every touched page verified in a real browser: **HTTP 200, zero console errors, zero failed requests**; loaded state of lazy pages, not the skeleton; inspect rendered DOM for leaked `@`/`{{ }}`/`group.key` literals. Green `Livewire::test` ≠ done.
|
||||||
|
- **R13** Route paths + names **English** (`/devices`, not `/geraete`); German only in visible labels.
|
||||||
|
- **R14** Fonts **self-hosted** (`resources/fonts/*.woff2`, relative `url('../fonts/…')` + `@font-face`) — no Google Fonts/CDN link or import.
|
||||||
|
- **R15** **Codex review** (`/codex:review`) after every change — no errors, no security findings, else fix and re-run. Task not done until clean.
|
||||||
|
- **R16** Every UI string localized `lang/{de,en}/<group>.php`, identical keys both languages, `__('group.key')` — no hard-coded copy anywhere.
|
||||||
|
- **R17** Blade: block `@php … @endphp` only (no inline `@php(...)`); never the literal directive tokens in comments/text.
|
||||||
|
- **Secrets:** `.env*`, tokens, keys never staged; check `git status` before every commit.
|
||||||
|
Git repo does not exist yet — create it at bootstrap (feature branches, not `main`).
|
||||||
|
|
||||||
|
**HomeOS-specific additions:**
|
||||||
|
- **H1** UI/Livewire never publishes to protocol topics directly — every command goes through
|
||||||
|
`DeviceCommandService` → driver, and every command is written to the `commands` audit table.
|
||||||
|
- **H2** MQTT subscriber callbacks: parse + dispatch only. No DB aggregation, no HTTP calls in
|
||||||
|
the loop.
|
||||||
|
- **H3** Vendor specifics (topics, payload shapes) live only in `Support/Mqtt` + `Support/Drivers`.
|
||||||
|
Models, Livewire and views stay protocol-agnostic.
|
||||||
|
- **H4** Telemetry writes go through one ingest path that enforces retention/compression policies.
|
||||||
|
- **H5** Automations always carry `cooldown_seconds`; the engine has a **dry-run mode** (log
|
||||||
|
instead of switch) for development — never develop automations against the live home.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 10. Claude Code plugin setup (server session — do this FIRST, before Phase 1)
|
||||||
|
|
||||||
|
The server session must mirror the user's local Claude Code plugin set and **use** these
|
||||||
|
plugins during the build. At session start:
|
||||||
|
|
||||||
|
1. **Add the external marketplaces**, then install:
|
||||||
|
|
||||||
|
| Plugin | Marketplace / source | Install |
|
||||||
|
|---|---|---|
|
||||||
|
| superpowers | `claude-plugins-official` (built-in) | `/plugin install superpowers@claude-plugins-official` |
|
||||||
|
| frontend-design | `claude-plugins-official` | `/plugin install frontend-design@claude-plugins-official` |
|
||||||
|
| code-review | `claude-plugins-official` | `/plugin install code-review@claude-plugins-official` |
|
||||||
|
| context7 | `claude-plugins-official` | `/plugin install context7@claude-plugins-official` |
|
||||||
|
| codex | `/plugin marketplace add openai/codex-plugin-cc` | `/plugin install codex@openai-codex` |
|
||||||
|
| claude-mem | `/plugin marketplace add thedotmack/claude-mem` | `/plugin install claude-mem@thedotmack` |
|
||||||
|
| ui-ux-pro-max | `/plugin marketplace add nextlevelbuilder/ui-ux-pro-max-skill` | `/plugin install ui-ux-pro-max@ui-ux-pro-max-skill` |
|
||||||
|
| caveman | `/plugin marketplace add JuliusBrussee/caveman` | `/plugin install caveman@caveman` |
|
||||||
|
|
||||||
|
2. **Use them, don't just install them:** superpowers workflows (brainstorming before creative
|
||||||
|
work, TDD, systematic debugging, writing/executing plans), frontend-design + ui-ux-pro-max
|
||||||
|
for every UI task (§8 design system), code-review + **codex for R15** (mandatory review
|
||||||
|
gate), claude-mem for cross-session memory, context7 for current library docs
|
||||||
|
(Laravel/Livewire/Tailwind APIs — don't answer from training data), caveman optional.
|
||||||
|
3. **Anything missing → install it.** If an install fails, a plugin needs a runtime it can't
|
||||||
|
find, or **any tool requires a login/authentication** (Codex CLI login, API keys, tokens,
|
||||||
|
Gitea credentials, UniFi access) → **stop and report to the user**; never skip silently or
|
||||||
|
work around it. Because R15 depends on Codex, verify `/codex:setup` during bootstrap, not
|
||||||
|
when the first review is due.
|
||||||
|
4. Scope note: plugin runtimes (node/python for claude-mem, ui-ux-pro-max) belong to the Claude
|
||||||
|
Code session on the host — R8 (everything in containers) applies to **project** tooling, not
|
||||||
|
to session tooling.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 11. Deferred decisions already researched (do not re-litigate, just don't build yet)
|
||||||
|
|
||||||
|
- **Face recognition:** postponed entirely. When it returns: **WebAuthn/passkeys**
|
||||||
|
(spatie/laravel-passkeys or asbiin/laravel-webauthn) is the security gate — browser-based
|
||||||
|
DIY liveness is not securable (virtual cameras, replay, deepfakes). Camera face recognition
|
||||||
|
only ever as comfort/personalization layer, never as the lock.
|
||||||
|
- **Zigbee:** if bought, Zigbee2MQTT + **SLZB-06 LAN coordinator** (PoE), never USB passthrough
|
||||||
|
into the Proxmox VM. Zigbee channel chosen against WLAN channels BEFORE first pairing.
|
||||||
|
- **Matter/TVs/Cast:** Node/Python sidecars speaking MQTT — never native PHP implementations.
|
||||||
|
- **Mobile:** PWA first (manifest + service worker + Web Push via
|
||||||
|
laravel-notification-channels/webpush; iOS supports Web Push for installed PWAs). Expo/React
|
||||||
|
Native only if a concrete PWA limit hurts. NativePHP rejected (runs a second Laravel on the
|
||||||
|
phone — wrong model).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 12. Open decisions — ask the user before committing
|
||||||
|
|
||||||
|
1. **DB:** PostgreSQL+TimescaleDB (recommended for telemetry) vs MariaDB (Clusev consistency)?
|
||||||
|
2. **Laravel version:** 13 assumed (current, Clusev precedent) — original statement said 12.
|
||||||
|
3. **Project name:** "HomeOS" is a working title.
|
||||||
|
4. **Git remote:** Gitea like Clusev (`git.bave.dev`)? Repo must be created at bootstrap.
|
||||||
|
5. **UniFi auth:** local read-only user vs API key on the UDM — needs the user to create it.
|
||||||
|
6. **Which Ring product** the user owns (relevant only when the ring-mqtt phase starts).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 13. Build order (each phase ends browser-verified, R12 + R15; §10 plugin setup comes first)
|
||||||
|
|
||||||
|
1. **Bootstrap** — compose stack up, Laravel installed in-container, git repo + `rules.md` +
|
||||||
|
`CLAUDE.md` created, tokens + fonts + layout shell (sidebar/topbar per mockup), auth + login.
|
||||||
|
2. **Domain core** — migrations for §3 tables, models, seeders with fake devices; dashboard
|
||||||
|
renders mock state (rooms, KPI row, live feed) — mock first, real hardware after.
|
||||||
|
3. **MQTT ingest** — mosquitto + mqtt-listener daemon; Shelly state flows into
|
||||||
|
`device_states`; Reverb pushes to the dashboard; toggle switches a real Shelly via
|
||||||
|
`ShellyMqttDriver`; commands audited.
|
||||||
|
4. **Discovery** — Python sidecar (mDNS/SSDP/ARP → MQTT), `discovery_findings`, "Neue Geräte"
|
||||||
|
panel with assign/ignore flow (per mockup).
|
||||||
|
5. **Presence** — UnifiClient poller (scheduler), person↔client mapping UI, debounce sweep,
|
||||||
|
presence panel + avatars live.
|
||||||
|
6. **Automations** — TCA engine (state_change + time triggers; actions: switch entity, notify),
|
||||||
|
cooldown, dry-run mode, audit integration.
|
||||||
|
7. **Polish + PWA prep** — empty states, `wire:init` loading states, breakpoints pass,
|
||||||
|
manifest groundwork.
|
||||||
|
|
||||||
|
Definition of done per phase: R12 browser proof (200, console clean, all 3 breakpoints) +
|
||||||
|
R15 Codex clean + commit on a feature branch.
|
||||||
|
|
@ -0,0 +1,14 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
return [
|
||||||
|
'title' => 'Zugang',
|
||||||
|
'subtitle' => 'Melde dich an, um die Steuerzentrale zu öffnen.',
|
||||||
|
'email' => 'E-Mail',
|
||||||
|
'password' => 'Passwort',
|
||||||
|
'remember' => 'Angemeldet bleiben',
|
||||||
|
'submit' => 'Anmelden',
|
||||||
|
'signing_in' => 'Anmeldung läuft…',
|
||||||
|
'failed' => 'Diese Zugangsdaten stimmen nicht mit unseren Daten überein.',
|
||||||
|
'throttle' => 'Zu viele Versuche. Bitte in :seconds Sekunden erneut versuchen.',
|
||||||
|
'secured' => 'Lokal gesichert · LAN-first',
|
||||||
|
];
|
||||||
|
|
@ -0,0 +1,17 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
return [
|
||||||
|
'app_name' => 'HomeOS',
|
||||||
|
'app_tagline' => 'Steuerzentrale',
|
||||||
|
|
||||||
|
'status_online' => 'Online',
|
||||||
|
'status_offline' => 'Offline',
|
||||||
|
'status_warning' => 'Warnung',
|
||||||
|
|
||||||
|
'system_ok' => 'System betriebsbereit',
|
||||||
|
'live' => 'Live',
|
||||||
|
'signed_in_as' => 'Angemeldet als',
|
||||||
|
'logout' => 'Abmelden',
|
||||||
|
'menu' => 'Menü',
|
||||||
|
'close' => 'Schließen',
|
||||||
|
];
|
||||||
|
|
@ -0,0 +1,23 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
return [
|
||||||
|
'title' => 'Dashboard',
|
||||||
|
'subtitle' => 'Überblick über Haus, Geräte und Anwesenheit.',
|
||||||
|
|
||||||
|
'welcome_title' => 'Willkommen bei HomeOS',
|
||||||
|
'welcome_body' => 'Das Grundgerüst steht. Räume, Geräte und Live-Zustände folgen in der nächsten Phase.',
|
||||||
|
|
||||||
|
'phase_label' => 'Aufbau',
|
||||||
|
'phase_value' => 'Phase 1',
|
||||||
|
'phase_meta' => 'Fundament · Auth · Design-System',
|
||||||
|
|
||||||
|
'stack_framework' => 'Framework',
|
||||||
|
'stack_database' => 'Datenbank',
|
||||||
|
'stack_realtime' => 'Echtzeit',
|
||||||
|
'stack_queue' => 'Queue',
|
||||||
|
|
||||||
|
'legend_title' => 'Design-System',
|
||||||
|
'legend_status' => 'Statusfarben',
|
||||||
|
'demo_toggle' => 'Beispiel-Schalter',
|
||||||
|
'demo_chips' => 'Geräte-Chips',
|
||||||
|
];
|
||||||
|
|
@ -0,0 +1,22 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
return [
|
||||||
|
// sections
|
||||||
|
'section_overview' => 'Übersicht',
|
||||||
|
'section_persons' => 'Personen',
|
||||||
|
'section_security' => 'Sicherheit',
|
||||||
|
'section_system' => 'System',
|
||||||
|
|
||||||
|
// items
|
||||||
|
'dashboard' => 'Dashboard',
|
||||||
|
'rooms' => 'Räume',
|
||||||
|
'devices' => 'Geräte',
|
||||||
|
'persons' => 'Personen & Anwesenheit',
|
||||||
|
'windows' => 'Fenster & Türen',
|
||||||
|
'access' => 'Zugang & Face-ID',
|
||||||
|
'network' => 'Netzwerk & Discovery',
|
||||||
|
'automations' => 'Automationen',
|
||||||
|
'settings' => 'Einstellungen',
|
||||||
|
|
||||||
|
'soon' => 'In Kürze',
|
||||||
|
];
|
||||||
|
|
@ -0,0 +1,14 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
return [
|
||||||
|
'title' => 'Access',
|
||||||
|
'subtitle' => 'Sign in to open the control plane.',
|
||||||
|
'email' => 'Email',
|
||||||
|
'password' => 'Password',
|
||||||
|
'remember' => 'Stay signed in',
|
||||||
|
'submit' => 'Sign in',
|
||||||
|
'signing_in' => 'Signing in…',
|
||||||
|
'failed' => 'These credentials do not match our records.',
|
||||||
|
'throttle' => 'Too many attempts. Please try again in :seconds seconds.',
|
||||||
|
'secured' => 'Locally secured · LAN-first',
|
||||||
|
];
|
||||||
|
|
@ -0,0 +1,17 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
return [
|
||||||
|
'app_name' => 'HomeOS',
|
||||||
|
'app_tagline' => 'Control Plane',
|
||||||
|
|
||||||
|
'status_online' => 'Online',
|
||||||
|
'status_offline' => 'Offline',
|
||||||
|
'status_warning' => 'Warning',
|
||||||
|
|
||||||
|
'system_ok' => 'System operational',
|
||||||
|
'live' => 'Live',
|
||||||
|
'signed_in_as' => 'Signed in as',
|
||||||
|
'logout' => 'Sign out',
|
||||||
|
'menu' => 'Menu',
|
||||||
|
'close' => 'Close',
|
||||||
|
];
|
||||||
|
|
@ -0,0 +1,23 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
return [
|
||||||
|
'title' => 'Dashboard',
|
||||||
|
'subtitle' => 'Overview of home, devices and presence.',
|
||||||
|
|
||||||
|
'welcome_title' => 'Welcome to HomeOS',
|
||||||
|
'welcome_body' => 'The foundation is in place. Rooms, devices and live state arrive in the next phase.',
|
||||||
|
|
||||||
|
'phase_label' => 'Build',
|
||||||
|
'phase_value' => 'Phase 1',
|
||||||
|
'phase_meta' => 'Foundation · Auth · Design system',
|
||||||
|
|
||||||
|
'stack_framework' => 'Framework',
|
||||||
|
'stack_database' => 'Database',
|
||||||
|
'stack_realtime' => 'Realtime',
|
||||||
|
'stack_queue' => 'Queue',
|
||||||
|
|
||||||
|
'legend_title' => 'Design system',
|
||||||
|
'legend_status' => 'Status colors',
|
||||||
|
'demo_toggle' => 'Example switch',
|
||||||
|
'demo_chips' => 'Device chips',
|
||||||
|
];
|
||||||
|
|
@ -0,0 +1,22 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
return [
|
||||||
|
// sections
|
||||||
|
'section_overview' => 'Overview',
|
||||||
|
'section_persons' => 'People',
|
||||||
|
'section_security' => 'Security',
|
||||||
|
'section_system' => 'System',
|
||||||
|
|
||||||
|
// items
|
||||||
|
'dashboard' => 'Dashboard',
|
||||||
|
'rooms' => 'Rooms',
|
||||||
|
'devices' => 'Devices',
|
||||||
|
'persons' => 'People & Presence',
|
||||||
|
'windows' => 'Windows & Doors',
|
||||||
|
'access' => 'Access & Face ID',
|
||||||
|
'network' => 'Network & Discovery',
|
||||||
|
'automations' => 'Automations',
|
||||||
|
'settings' => 'Settings',
|
||||||
|
|
||||||
|
'soon' => 'Coming soon',
|
||||||
|
];
|
||||||
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,20 @@
|
||||||
|
{
|
||||||
|
"$schema": "https://www.schemastore.org/package.json",
|
||||||
|
"private": true,
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"build": "vite build",
|
||||||
|
"dev": "vite"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@tailwindcss/vite": "^4.0.0",
|
||||||
|
"concurrently": "^9.0.1",
|
||||||
|
"laravel-vite-plugin": "^3.1",
|
||||||
|
"tailwindcss": "^4.0.0",
|
||||||
|
"vite": "^8.0.0"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"laravel-echo": "^2.4.0",
|
||||||
|
"pusher-js": "^8.5.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,36 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||||
|
xsi:noNamespaceSchemaLocation="vendor/phpunit/phpunit/phpunit.xsd"
|
||||||
|
bootstrap="vendor/autoload.php"
|
||||||
|
colors="true"
|
||||||
|
>
|
||||||
|
<testsuites>
|
||||||
|
<testsuite name="Unit">
|
||||||
|
<directory>tests/Unit</directory>
|
||||||
|
</testsuite>
|
||||||
|
<testsuite name="Feature">
|
||||||
|
<directory>tests/Feature</directory>
|
||||||
|
</testsuite>
|
||||||
|
</testsuites>
|
||||||
|
<source>
|
||||||
|
<include>
|
||||||
|
<directory>app</directory>
|
||||||
|
</include>
|
||||||
|
</source>
|
||||||
|
<php>
|
||||||
|
<env name="APP_ENV" value="testing"/>
|
||||||
|
<env name="APP_MAINTENANCE_DRIVER" value="file"/>
|
||||||
|
<env name="BCRYPT_ROUNDS" value="4"/>
|
||||||
|
<env name="BROADCAST_CONNECTION" value="null"/>
|
||||||
|
<env name="CACHE_STORE" value="array"/>
|
||||||
|
<env name="DB_CONNECTION" value="sqlite"/>
|
||||||
|
<env name="DB_DATABASE" value=":memory:"/>
|
||||||
|
<env name="DB_URL" value=""/>
|
||||||
|
<env name="MAIL_MAILER" value="array"/>
|
||||||
|
<env name="QUEUE_CONNECTION" value="sync"/>
|
||||||
|
<env name="SESSION_DRIVER" value="array"/>
|
||||||
|
<env name="PULSE_ENABLED" value="false"/>
|
||||||
|
<env name="TELESCOPE_ENABLED" value="false"/>
|
||||||
|
<env name="NIGHTWATCH_ENABLED" value="false"/>
|
||||||
|
</php>
|
||||||
|
</phpunit>
|
||||||
|
|
@ -0,0 +1,25 @@
|
||||||
|
<IfModule mod_rewrite.c>
|
||||||
|
<IfModule mod_negotiation.c>
|
||||||
|
Options -MultiViews -Indexes
|
||||||
|
</IfModule>
|
||||||
|
|
||||||
|
RewriteEngine On
|
||||||
|
|
||||||
|
# Handle Authorization Header
|
||||||
|
RewriteCond %{HTTP:Authorization} .
|
||||||
|
RewriteRule .* - [E=HTTP_AUTHORIZATION:%{HTTP:Authorization}]
|
||||||
|
|
||||||
|
# Handle X-XSRF-Token Header
|
||||||
|
RewriteCond %{HTTP:x-xsrf-token} .
|
||||||
|
RewriteRule .* - [E=HTTP_X_XSRF_TOKEN:%{HTTP:X-XSRF-Token}]
|
||||||
|
|
||||||
|
# Redirect Trailing Slashes If Not A Folder...
|
||||||
|
RewriteCond %{REQUEST_FILENAME} !-d
|
||||||
|
RewriteCond %{REQUEST_URI} (.+)/$
|
||||||
|
RewriteRule ^ %1 [L,R=301]
|
||||||
|
|
||||||
|
# Send Requests To Front Controller...
|
||||||
|
RewriteCond %{REQUEST_FILENAME} !-d
|
||||||
|
RewriteCond %{REQUEST_FILENAME} !-f
|
||||||
|
RewriteRule ^ index.php [L]
|
||||||
|
</IfModule>
|
||||||
|
|
@ -0,0 +1,7 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
|
||||||
|
<rect width="32" height="32" rx="8" fill="#0D1424"/>
|
||||||
|
<rect x="6" y="6" width="9" height="12" rx="2" fill="#4FC1FF"/>
|
||||||
|
<rect x="18" y="6" width="8" height="6" rx="2" fill="#4FC1FF" opacity="0.75"/>
|
||||||
|
<rect x="18" y="15" width="8" height="11" rx="2" fill="#4FC1FF"/>
|
||||||
|
<rect x="6" y="21" width="9" height="5" rx="2" fill="#4FC1FF" opacity="0.75"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 419 B |
|
|
@ -0,0 +1,20 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Foundation\Application;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
|
||||||
|
define('LARAVEL_START', microtime(true));
|
||||||
|
|
||||||
|
// Determine if the application is in maintenance mode...
|
||||||
|
if (file_exists($maintenance = __DIR__.'/../storage/framework/maintenance.php')) {
|
||||||
|
require $maintenance;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Register the Composer autoloader...
|
||||||
|
require __DIR__.'/../vendor/autoload.php';
|
||||||
|
|
||||||
|
// Bootstrap Laravel and handle the request...
|
||||||
|
/** @var Application $app */
|
||||||
|
$app = require_once __DIR__.'/../bootstrap/app.php';
|
||||||
|
|
||||||
|
$app->handleRequest(Request::capture());
|
||||||
|
|
@ -0,0 +1,2 @@
|
||||||
|
User-agent: *
|
||||||
|
Disallow:
|
||||||
|
|
@ -0,0 +1,117 @@
|
||||||
|
@import 'tailwindcss';
|
||||||
|
|
||||||
|
/* Scan Blade sources for utility classes */
|
||||||
|
@source '../views';
|
||||||
|
@source '../../app/Livewire';
|
||||||
|
@source '../../vendor/laravel/framework/src/Illuminate/Pagination/resources/views/*.blade.php';
|
||||||
|
@source '../../storage/framework/views/*.php';
|
||||||
|
|
||||||
|
/* ── Design tokens — "Enterprise Ops, dark OLED" (handoff §8, ported verbatim) ── */
|
||||||
|
@theme {
|
||||||
|
/* surfaces */
|
||||||
|
--color-base: #080D18; /* page bg, blue-biased OLED black */
|
||||||
|
--color-surface: #0D1424; /* cards */
|
||||||
|
--color-raised: #121C31; /* nested surfaces, chips */
|
||||||
|
--color-inset: #18233D; /* icon wells, kbd */
|
||||||
|
|
||||||
|
/* text ramp */
|
||||||
|
--color-ink: #EAF0FB;
|
||||||
|
--color-ink-2: #93A1BD;
|
||||||
|
--color-ink-3: #5E6C8A;
|
||||||
|
|
||||||
|
/* brand */
|
||||||
|
--color-accent: #4FC1FF; /* cyan — interaction, active nav, toggles */
|
||||||
|
|
||||||
|
/* status triad — reserved, never decorative */
|
||||||
|
--color-online: #34D399;
|
||||||
|
--color-warning: #FBBF24;
|
||||||
|
--color-offline: #F87171;
|
||||||
|
|
||||||
|
/* hairlines */
|
||||||
|
--color-line: #1C2846;
|
||||||
|
--color-line-soft: #141E36;
|
||||||
|
|
||||||
|
/* radius */
|
||||||
|
--radius-card: 10px;
|
||||||
|
|
||||||
|
/* fonts (self-hosted, R14) */
|
||||||
|
--font-sans: 'Plus Jakarta Sans', ui-sans-serif, system-ui, sans-serif;
|
||||||
|
--font-mono: 'IBM Plex Mono', ui-monospace, monospace;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Self-hosted fonts (R14 — no CDN/Google Fonts) ── */
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Plus Jakarta Sans'; font-style: normal; font-weight: 400;
|
||||||
|
font-display: swap; src: url('../fonts/plus-jakarta-sans-latin-400-normal.woff2') format('woff2');
|
||||||
|
}
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Plus Jakarta Sans'; font-style: normal; font-weight: 500;
|
||||||
|
font-display: swap; src: url('../fonts/plus-jakarta-sans-latin-500-normal.woff2') format('woff2');
|
||||||
|
}
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Plus Jakarta Sans'; font-style: normal; font-weight: 600;
|
||||||
|
font-display: swap; src: url('../fonts/plus-jakarta-sans-latin-600-normal.woff2') format('woff2');
|
||||||
|
}
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Plus Jakarta Sans'; font-style: normal; font-weight: 700;
|
||||||
|
font-display: swap; src: url('../fonts/plus-jakarta-sans-latin-700-normal.woff2') format('woff2');
|
||||||
|
}
|
||||||
|
@font-face {
|
||||||
|
font-family: 'IBM Plex Mono'; font-style: normal; font-weight: 400;
|
||||||
|
font-display: swap; src: url('../fonts/ibm-plex-mono-latin-400-normal.woff2') format('woff2');
|
||||||
|
}
|
||||||
|
@font-face {
|
||||||
|
font-family: 'IBM Plex Mono'; font-style: normal; font-weight: 500;
|
||||||
|
font-display: swap; src: url('../fonts/ibm-plex-mono-latin-500-normal.woff2') format('woff2');
|
||||||
|
}
|
||||||
|
@font-face {
|
||||||
|
font-family: 'IBM Plex Mono'; font-style: normal; font-weight: 600;
|
||||||
|
font-display: swap; src: url('../fonts/ibm-plex-mono-latin-600-normal.woff2') format('woff2');
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Base ── */
|
||||||
|
@layer base {
|
||||||
|
[x-cloak] { display: none !important; }
|
||||||
|
html { scrollbar-color: var(--color-line) transparent; }
|
||||||
|
body {
|
||||||
|
background-color: var(--color-base);
|
||||||
|
color: var(--color-ink);
|
||||||
|
font-family: var(--font-sans);
|
||||||
|
-webkit-font-smoothing: antialiased;
|
||||||
|
text-rendering: optimizeLegibility;
|
||||||
|
}
|
||||||
|
::selection { background: color-mix(in srgb, var(--color-accent) 30%, transparent); }
|
||||||
|
::-webkit-scrollbar { width: 10px; height: 10px; }
|
||||||
|
::-webkit-scrollbar-thumb { background: var(--color-line); border-radius: 20px; border: 3px solid transparent; background-clip: content-box; }
|
||||||
|
::-webkit-scrollbar-track { background: transparent; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Component helpers that must reference tokens (keeps raw color out of markup, R3) ── */
|
||||||
|
@layer components {
|
||||||
|
.dot-glow-online { box-shadow: 0 0 8px color-mix(in srgb, var(--color-online) 60%, transparent); }
|
||||||
|
.dot-glow-warning { box-shadow: 0 0 8px color-mix(in srgb, var(--color-warning) 55%, transparent); }
|
||||||
|
.dot-glow-offline { box-shadow: 0 0 8px color-mix(in srgb, var(--color-offline) 55%, transparent); }
|
||||||
|
|
||||||
|
.logo-mark {
|
||||||
|
background: linear-gradient(140deg, #123049, #0C1B33);
|
||||||
|
border: 1px solid color-mix(in srgb, var(--color-accent) 35%, transparent);
|
||||||
|
box-shadow: 0 0 18px color-mix(in srgb, var(--color-accent) 18%, transparent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.topbar-blur {
|
||||||
|
background: color-mix(in srgb, var(--color-base) 85%, transparent);
|
||||||
|
backdrop-filter: blur(14px);
|
||||||
|
}
|
||||||
|
.sidebar-tint { background: color-mix(in srgb, #0A101E 72%, transparent); }
|
||||||
|
|
||||||
|
.pulse-live { animation: pulse 2.2s ease-in-out infinite; }
|
||||||
|
.reveal { opacity: 0; transform: translateY(10px); animation: reveal .5s cubic-bezier(.2,.7,.3,1) forwards; }
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes pulse { 0%, 100% { opacity: 1; } 50% { opacity: .35; } }
|
||||||
|
@keyframes reveal { to { opacity: 1; transform: none; } }
|
||||||
|
|
||||||
|
@media (prefers-reduced-motion: reduce) {
|
||||||
|
.pulse-live { animation: none; }
|
||||||
|
.reveal { opacity: 1; transform: none; animation: none; }
|
||||||
|
}
|
||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
|
@ -0,0 +1,20 @@
|
||||||
|
import Echo from 'laravel-echo';
|
||||||
|
import Pusher from 'pusher-js';
|
||||||
|
|
||||||
|
// Reverb (Pusher protocol), proxied same-origin through nginx (/app, /apps).
|
||||||
|
// The browser connects to the page's own host/port — works on localhost and over the LAN
|
||||||
|
// with no separate WS port and no host mismatch. Every channel is PRIVATE (/broadcasting/auth).
|
||||||
|
const loc = window.location;
|
||||||
|
const isHttps = loc.protocol === 'https:';
|
||||||
|
const port = loc.port ? Number(loc.port) : (isHttps ? 443 : 80);
|
||||||
|
|
||||||
|
window.Pusher = Pusher;
|
||||||
|
window.Echo = new Echo({
|
||||||
|
broadcaster: 'reverb',
|
||||||
|
key: import.meta.env.VITE_REVERB_APP_KEY,
|
||||||
|
wsHost: loc.hostname,
|
||||||
|
wsPort: port,
|
||||||
|
wssPort: port,
|
||||||
|
forceTLS: isHttps,
|
||||||
|
enabledTransports: ['ws', 'wss'],
|
||||||
|
});
|
||||||
|
|
@ -0,0 +1,14 @@
|
||||||
|
@props(['variant' => 'default'])
|
||||||
|
|
||||||
|
@php
|
||||||
|
$map = [
|
||||||
|
'default' => 'bg-inset text-ink-2',
|
||||||
|
'warning' => 'bg-warning/10 text-warning',
|
||||||
|
'accent' => 'bg-accent/10 text-accent',
|
||||||
|
];
|
||||||
|
$classes = $map[$variant] ?? $map['default'];
|
||||||
|
@endphp
|
||||||
|
|
||||||
|
<span {{ $attributes->merge(['class' => 'font-mono text-[10.5px] font-semibold leading-none rounded-full px-[7px] py-[3px] tabular-nums '.$classes]) }}>
|
||||||
|
{{ $slot }}
|
||||||
|
</span>
|
||||||
|
|
@ -0,0 +1,6 @@
|
||||||
|
@props(['state' => 'idle', 'off' => false])
|
||||||
|
|
||||||
|
<span {{ $attributes->merge(['class' => 'inline-flex items-center gap-1.5 rounded-md border border-line-soft bg-raised px-2 py-1 text-[11px] font-semibold '.($off ? 'text-ink-3' : 'text-ink-2')]) }}>
|
||||||
|
<x-status-dot :state="$state" class="!w-1.5 !h-1.5" />
|
||||||
|
{{ $slot }}
|
||||||
|
</span>
|
||||||
|
|
@ -0,0 +1,36 @@
|
||||||
|
@props(['name' => 'dot', 'size' => 18])
|
||||||
|
|
||||||
|
@php
|
||||||
|
// Lucide-derived inline SVGs (stroke, 24 grid). No emoji anywhere (R9).
|
||||||
|
$paths = [
|
||||||
|
'dashboard' => '<rect x="3" y="3" width="7" height="9" rx="1.5"/><rect x="14" y="3" width="7" height="5" rx="1.5"/><rect x="14" y="12" width="7" height="9" rx="1.5"/><rect x="3" y="16" width="7" height="5" rx="1.5"/>',
|
||||||
|
'rooms' => '<path d="M3 21h18"/><path d="M5 21V5a2 2 0 0 1 2-2h10a2 2 0 0 1 2 2v16"/><path d="M14 12h.01"/>',
|
||||||
|
'devices' => '<rect x="5" y="5" width="14" height="14" rx="2"/><path d="M9 2v3M15 2v3M9 19v3M15 19v3M2 9h3M2 15h3M19 9h3M19 15h3"/>',
|
||||||
|
'persons' => '<circle cx="9" cy="8" r="3.5"/><path d="M2.5 20c.8-3.2 3.4-5 6.5-5s5.7 1.8 6.5 5"/><circle cx="17.5" cy="9" r="2.5"/><path d="M16.5 14.5c2.6.2 4.4 1.7 5 4.5"/>',
|
||||||
|
'window' => '<rect x="4" y="3" width="16" height="18" rx="2"/><path d="M12 3v18M4 12h16"/>',
|
||||||
|
'shield' => '<path d="M12 3 4.5 6v5.5c0 4.6 3.2 8 7.5 9.5 4.3-1.5 7.5-4.9 7.5-9.5V6L12 3z"/><path d="m9 12 2 2 4-4"/>',
|
||||||
|
'network' => '<circle cx="12" cy="12" r="2"/><path d="M16.2 7.8a6 6 0 0 1 0 8.4M7.8 16.2a6 6 0 0 1 0-8.4M19 5a10 10 0 0 1 0 14M5 19A10 10 0 0 1 5 5"/>',
|
||||||
|
'automation'=> '<path d="M13 3 5 13h6l-1 8 8-10h-6l1-8z"/>',
|
||||||
|
'settings' => '<circle cx="12" cy="12" r="2.6"/><path d="M19.4 15a1.7 1.7 0 0 0 .34 1.87l.06.06a2 2 0 1 1-2.83 2.83l-.06-.06a1.7 1.7 0 0 0-1.87-.34 1.7 1.7 0 0 0-1 1.55V21a2 2 0 1 1-4 0v-.09a1.7 1.7 0 0 0-1-1.55 1.7 1.7 0 0 0-1.87.34l-.06.06a2 2 0 1 1-2.83-2.83l.06-.06a1.7 1.7 0 0 0 .34-1.87 1.7 1.7 0 0 0-1.55-1H3a2 2 0 1 1 0-4h.09a1.7 1.7 0 0 0 1.55-1 1.7 1.7 0 0 0-.34-1.87l-.06-.06a2 2 0 1 1 2.83-2.83l.06.06a1.7 1.7 0 0 0 1.87.34h.01a1.7 1.7 0 0 0 1-1.55V3a2 2 0 1 1 4 0v.09a1.7 1.7 0 0 0 1 1.55h.01a1.7 1.7 0 0 0 1.87-.34l.06-.06a2 2 0 1 1 2.83 2.83l-.06.06a1.7 1.7 0 0 0-.34 1.87v.01a1.7 1.7 0 0 0 1.55 1H21a2 2 0 1 1 0 4h-.09a1.7 1.7 0 0 0-1.55 1z"/>',
|
||||||
|
'lock' => '<rect x="5" y="11" width="14" height="10" rx="2"/><path d="M8 11V7a4 4 0 0 1 8 0v4"/>',
|
||||||
|
'menu' => '<path d="M4 6h16M4 12h16M4 18h16"/>',
|
||||||
|
'close' => '<path d="M18 6 6 18M6 6l12 12"/>',
|
||||||
|
'logout' => '<path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4"/><path d="m16 17 5-5-5-5"/><path d="M21 12H9"/>',
|
||||||
|
'activity' => '<path d="M22 12h-4l-3 9L9 3l-3 9H2"/>',
|
||||||
|
'wifi' => '<path d="M5 12.55a11 11 0 0 1 14 0"/><path d="M8.5 16.1a6 6 0 0 1 7 0"/><path d="M12 20h.01"/><path d="M2 8.82a15 15 0 0 1 20 0"/>',
|
||||||
|
'plus' => '<path d="M12 5v14M5 12h14"/>',
|
||||||
|
'check' => '<path d="M20 6 9 17l-5-5"/>',
|
||||||
|
'alert' => '<path d="M10.29 3.86 1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"/><path d="M12 9v4M12 17h.01"/>',
|
||||||
|
'user' => '<circle cx="12" cy="8" r="4"/><path d="M4 21c0-4 3.6-6 8-6s8 2 8 6"/>',
|
||||||
|
'chevron' => '<path d="m9 18 6-6-6-6"/>',
|
||||||
|
'bolt' => '<path d="M13 3 5 13h6l-1 8 8-10h-6l1-8z"/>',
|
||||||
|
'dot' => '<circle cx="12" cy="12" r="4"/>',
|
||||||
|
];
|
||||||
|
$inner = $paths[$name] ?? $paths['dot'];
|
||||||
|
@endphp
|
||||||
|
|
||||||
|
<svg width="{{ $size }}" height="{{ $size }}" viewBox="0 0 24 24" fill="none"
|
||||||
|
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
|
||||||
|
aria-hidden="true" {{ $attributes->merge(['class' => 'shrink-0']) }}>
|
||||||
|
{!! $inner !!}
|
||||||
|
</svg>
|
||||||
|
|
@ -0,0 +1,22 @@
|
||||||
|
@props(['label', 'value', 'unit' => null, 'meta' => null, 'icon' => null, 'warnEdge' => false])
|
||||||
|
|
||||||
|
<div {{ $attributes->merge(['class' => 'relative overflow-hidden bg-surface border border-line-soft rounded-card px-4 py-[14px] flex flex-col gap-1.5 transition-colors hover:border-line']) }}>
|
||||||
|
@if ($warnEdge)
|
||||||
|
<span class="absolute inset-y-0 left-0 w-[3px] bg-warning"></span>
|
||||||
|
@endif
|
||||||
|
|
||||||
|
<div class="flex items-center gap-[7px] text-[11px] font-semibold uppercase tracking-[0.08em] text-ink-3">
|
||||||
|
@if ($icon)
|
||||||
|
<x-icon :name="$icon" :size="13" />
|
||||||
|
@endif
|
||||||
|
<span>{{ $label }}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="font-mono text-2xl font-extrabold tracking-[-0.02em] tabular-nums text-ink leading-none">
|
||||||
|
{{ $value }}@if ($unit)<small class="ml-1 text-[13px] font-semibold text-ink-3">{{ $unit }}</small>@endif
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@if ($meta)
|
||||||
|
<div class="text-[11.5px] text-ink-3">{{ $meta }}</div>
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
|
|
@ -0,0 +1,18 @@
|
||||||
|
@props(['title' => null, 'padded' => true])
|
||||||
|
|
||||||
|
<section {{ $attributes->merge(['class' => 'bg-surface border border-line-soft rounded-card']) }}>
|
||||||
|
@if ($title || isset($actions))
|
||||||
|
<header class="flex items-center gap-3 px-4 py-3 border-b border-line-soft">
|
||||||
|
@if ($title)
|
||||||
|
<h3 class="text-[13px] font-semibold text-ink">{{ $title }}</h3>
|
||||||
|
@endif
|
||||||
|
@isset($actions)
|
||||||
|
<div class="ml-auto flex items-center gap-2">{{ $actions }}</div>
|
||||||
|
@endisset
|
||||||
|
</header>
|
||||||
|
@endif
|
||||||
|
|
||||||
|
<div class="{{ $padded ? 'p-4' : '' }}">
|
||||||
|
{{ $slot }}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
@ -0,0 +1,24 @@
|
||||||
|
@props(['name', 'meta' => null, 'icon' => 'rooms'])
|
||||||
|
|
||||||
|
<div {{ $attributes->merge(['class' => 'bg-surface border border-line-soft rounded-card p-4 flex flex-col gap-3 transition-[border-color,transform] duration-200 hover:border-line hover:-translate-y-px']) }}>
|
||||||
|
<div class="flex items-center gap-2.5">
|
||||||
|
<span class="grid place-items-center w-8 h-8 rounded-lg bg-inset text-ink-2">
|
||||||
|
<x-icon :name="$icon" :size="16" />
|
||||||
|
</span>
|
||||||
|
<div class="min-w-0">
|
||||||
|
<div class="text-[13px] font-semibold text-ink truncate">{{ $name }}</div>
|
||||||
|
@if ($meta)
|
||||||
|
<div class="text-[11px] text-ink-3">{{ $meta }}</div>
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
|
@isset($action)
|
||||||
|
<div class="ml-auto">{{ $action }}</div>
|
||||||
|
@endisset
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@if (trim($slot) !== '')
|
||||||
|
<div class="flex flex-wrap gap-1.5">
|
||||||
|
{{ $slot }}
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
|
|
@ -0,0 +1,78 @@
|
||||||
|
@php
|
||||||
|
$groups = [
|
||||||
|
'nav.section_overview' => [
|
||||||
|
['key' => 'dashboard', 'icon' => 'dashboard', 'route' => 'dashboard', 'lock' => false],
|
||||||
|
['key' => 'rooms', 'icon' => 'rooms', 'route' => null, 'lock' => false],
|
||||||
|
['key' => 'devices', 'icon' => 'devices', 'route' => null, 'lock' => false],
|
||||||
|
],
|
||||||
|
'nav.section_persons' => [
|
||||||
|
['key' => 'persons', 'icon' => 'persons', 'route' => null, 'lock' => false],
|
||||||
|
],
|
||||||
|
'nav.section_security' => [
|
||||||
|
['key' => 'windows', 'icon' => 'window', 'route' => null, 'lock' => false],
|
||||||
|
['key' => 'access', 'icon' => 'shield', 'route' => null, 'lock' => true],
|
||||||
|
],
|
||||||
|
'nav.section_system' => [
|
||||||
|
['key' => 'network', 'icon' => 'network', 'route' => null, 'lock' => false],
|
||||||
|
['key' => 'automations', 'icon' => 'automation', 'route' => null, 'lock' => false],
|
||||||
|
['key' => 'settings', 'icon' => 'settings', 'route' => null, 'lock' => true],
|
||||||
|
],
|
||||||
|
];
|
||||||
|
@endphp
|
||||||
|
|
||||||
|
<div class="flex flex-col gap-6 h-full">
|
||||||
|
{{-- brand --}}
|
||||||
|
<a href="{{ route('dashboard') }}" class="flex items-center gap-2.5 px-2 py-0.5" wire:navigate>
|
||||||
|
<span class="logo-mark grid place-items-center w-8 h-8 rounded-[9px] text-accent">
|
||||||
|
<x-icon name="dashboard" :size="17" />
|
||||||
|
</span>
|
||||||
|
<span class="leading-tight">
|
||||||
|
<span class="block font-extrabold text-[15px] tracking-[0.02em] text-ink">{{ __('common.app_name') }}</span>
|
||||||
|
<span class="block text-[10px] font-semibold uppercase tracking-[0.14em] text-ink-3">{{ __('common.app_tagline') }}</span>
|
||||||
|
</span>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
{{-- nav --}}
|
||||||
|
<nav class="flex flex-col gap-5">
|
||||||
|
@foreach ($groups as $label => $items)
|
||||||
|
<div class="flex flex-col gap-0.5" aria-label="{{ __($label) }}">
|
||||||
|
<div class="px-2.5 mb-1.5 text-[10px] font-bold uppercase tracking-[0.14em] text-ink-3">{{ __($label) }}</div>
|
||||||
|
@foreach ($items as $item)
|
||||||
|
@php $active = $item['route'] && request()->routeIs($item['route']); @endphp
|
||||||
|
@if ($item['route'])
|
||||||
|
<a href="{{ route($item['route']) }}" wire:navigate
|
||||||
|
@class([
|
||||||
|
'flex items-center gap-2.5 w-full px-2.5 py-2 rounded-lg text-[13px] font-semibold transition-colors',
|
||||||
|
'bg-accent/10 text-accent' => $active,
|
||||||
|
'text-ink-2 hover:bg-raised hover:text-ink' => ! $active,
|
||||||
|
])
|
||||||
|
@if ($active) aria-current="page" @endif>
|
||||||
|
<x-icon :name="$item['icon']" :size="16" />
|
||||||
|
<span>{{ __('nav.'.$item['key']) }}</span>
|
||||||
|
</a>
|
||||||
|
@else
|
||||||
|
<button type="button" title="{{ __('nav.soon') }}"
|
||||||
|
class="flex items-center gap-2.5 w-full px-2.5 py-2 rounded-lg text-[13px] font-semibold text-ink-2 hover:bg-raised hover:text-ink transition-colors cursor-default">
|
||||||
|
<x-icon :name="$item['icon']" :size="16" />
|
||||||
|
<span>{{ __('nav.'.$item['key']) }}</span>
|
||||||
|
@if ($item['lock'])
|
||||||
|
<span class="ml-auto text-ink-3"><x-icon name="lock" :size="13" /></span>
|
||||||
|
@else
|
||||||
|
<x-badge class="ml-auto">{{ __('nav.soon') }}</x-badge>
|
||||||
|
@endif
|
||||||
|
</button>
|
||||||
|
@endif
|
||||||
|
@endforeach
|
||||||
|
</div>
|
||||||
|
@endforeach
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
{{-- system status --}}
|
||||||
|
<div class="mt-auto border border-line-soft rounded-card bg-surface p-3 flex flex-col gap-2">
|
||||||
|
<h4 class="text-[10px] font-semibold uppercase tracking-[0.14em] text-ink-3">{{ __('common.status_online') }}</h4>
|
||||||
|
<div class="flex items-center gap-2 text-[12px] text-ink-2">
|
||||||
|
<x-status-dot state="online" :pulse="true" />
|
||||||
|
<span>{{ __('common.system_ok') }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
@ -0,0 +1,13 @@
|
||||||
|
@props(['state' => 'idle', 'pulse' => false])
|
||||||
|
|
||||||
|
@php
|
||||||
|
$map = [
|
||||||
|
'online' => 'bg-online dot-glow-online',
|
||||||
|
'warning' => 'bg-warning dot-glow-warning',
|
||||||
|
'offline' => 'bg-offline dot-glow-offline',
|
||||||
|
'idle' => 'bg-ink-3',
|
||||||
|
];
|
||||||
|
$classes = $map[$state] ?? $map['idle'];
|
||||||
|
@endphp
|
||||||
|
|
||||||
|
<span {{ $attributes->merge(['class' => 'inline-block w-[7px] h-[7px] rounded-full shrink-0 '.$classes.($pulse ? ' pulse-live' : '')]) }}></span>
|
||||||
|
|
@ -0,0 +1,15 @@
|
||||||
|
@props(['state' => 'neutral'])
|
||||||
|
|
||||||
|
@php
|
||||||
|
$map = [
|
||||||
|
'online' => 'bg-online/10 text-online',
|
||||||
|
'warning' => 'bg-warning/10 text-warning',
|
||||||
|
'offline' => 'bg-offline/10 text-offline',
|
||||||
|
'neutral' => 'bg-inset text-ink-2',
|
||||||
|
];
|
||||||
|
$classes = $map[$state] ?? $map['neutral'];
|
||||||
|
@endphp
|
||||||
|
|
||||||
|
<span {{ $attributes->merge(['class' => 'inline-flex items-center gap-1.5 rounded-full px-2.5 py-[3px] text-[10.5px] font-bold leading-none '.$classes]) }}>
|
||||||
|
{{ $slot }}
|
||||||
|
</span>
|
||||||
|
|
@ -0,0 +1,7 @@
|
||||||
|
@props(['on' => false, 'label' => null])
|
||||||
|
|
||||||
|
<button type="button" role="switch" aria-checked="{{ $on ? 'true' : 'false' }}"
|
||||||
|
@isset($label) aria-label="{{ $label }}" @endisset
|
||||||
|
{{ $attributes->merge(['class' => 'relative w-9 h-5 rounded-full shrink-0 transition-colors duration-200 '.($on ? 'bg-accent' : 'bg-inset')]) }}>
|
||||||
|
<span class="absolute top-[2px] h-4 w-4 rounded-full transition-all duration-200 {{ $on ? 'left-[18px] bg-base' : 'left-[2px] bg-ink-3' }}"></span>
|
||||||
|
</button>
|
||||||
|
|
@ -0,0 +1,44 @@
|
||||||
|
@props(['title' => null, 'subtitle' => null])
|
||||||
|
|
||||||
|
<header class="topbar-blur sticky top-0 z-40 flex items-center gap-3 px-5 lg:px-[26px] py-3.5 border-b border-line-soft">
|
||||||
|
{{-- mobile nav trigger --}}
|
||||||
|
<button type="button" @click="nav = true"
|
||||||
|
class="lg:hidden grid place-items-center w-9 h-9 rounded-lg text-ink-2 hover:bg-raised hover:text-ink transition-colors"
|
||||||
|
aria-label="{{ __('common.menu') }}">
|
||||||
|
<x-icon name="menu" :size="20" />
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<div class="min-w-0">
|
||||||
|
@if ($title)
|
||||||
|
<h1 class="text-[15px] font-bold text-ink leading-tight truncate">{{ $title }}</h1>
|
||||||
|
@endif
|
||||||
|
@if ($subtitle)
|
||||||
|
<p class="text-[12px] text-ink-3 truncate">{{ $subtitle }}</p>
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="ml-auto flex items-center gap-3">
|
||||||
|
<span class="hidden sm:inline-flex items-center gap-1.5 rounded-full bg-raised border border-line-soft px-2.5 py-1 text-[11px] font-semibold text-ink-2">
|
||||||
|
<span class="w-[7px] h-[7px] rounded-full bg-accent pulse-live"></span>
|
||||||
|
{{ __('common.live') }}
|
||||||
|
</span>
|
||||||
|
|
||||||
|
@auth
|
||||||
|
<span class="hidden sm:flex items-center gap-2 text-[12px] text-ink-2">
|
||||||
|
<span class="grid place-items-center w-7 h-7 rounded-full bg-inset text-ink-2">
|
||||||
|
<x-icon name="user" :size="15" />
|
||||||
|
</span>
|
||||||
|
<span class="font-mono">{{ auth()->user()->name }}</span>
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<form method="POST" action="{{ route('logout') }}">
|
||||||
|
@csrf
|
||||||
|
<button type="submit"
|
||||||
|
class="grid place-items-center w-9 h-9 rounded-lg text-ink-2 hover:bg-raised hover:text-ink transition-colors"
|
||||||
|
aria-label="{{ __('common.logout') }}" title="{{ __('common.logout') }}">
|
||||||
|
<x-icon name="logout" :size="18" />
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
@endauth
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
@ -0,0 +1,48 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}" class="scroll-smooth">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<meta name="csrf-token" content="{{ csrf_token() }}">
|
||||||
|
<link rel="icon" href="{{ asset('favicon.svg') }}" type="image/svg+xml">
|
||||||
|
<title>{{ $title ?? __('common.app_name') }} · {{ __('common.app_name') }}</title>
|
||||||
|
@vite(['resources/css/app.css', 'resources/js/app.js'])
|
||||||
|
@livewireStyles
|
||||||
|
</head>
|
||||||
|
<body class="min-h-dvh antialiased">
|
||||||
|
<div x-data="{ nav: false }" @keydown.escape.window="nav = false">
|
||||||
|
<div class="lg:grid lg:grid-cols-[236px_1fr]">
|
||||||
|
{{-- desktop sidebar --}}
|
||||||
|
<aside class="hidden lg:flex flex-col sidebar-tint border-r border-line-soft sticky top-0 h-dvh overflow-y-auto px-3.5 pt-5 pb-4">
|
||||||
|
<x-sidebar />
|
||||||
|
</aside>
|
||||||
|
|
||||||
|
{{-- main column --}}
|
||||||
|
<div class="min-w-0">
|
||||||
|
{{ $slot }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{{-- mobile drawer --}}
|
||||||
|
<div x-show="nav" x-cloak class="lg:hidden fixed inset-0 z-50" role="dialog" aria-modal="true">
|
||||||
|
<div x-show="nav" x-transition.opacity @click="nav = false"
|
||||||
|
class="absolute inset-0 bg-black/60"></div>
|
||||||
|
<aside x-show="nav"
|
||||||
|
x-transition:enter="transition ease-out duration-200" x-transition:enter-start="-translate-x-full" x-transition:enter-end="translate-x-0"
|
||||||
|
x-transition:leave="transition ease-in duration-150" x-transition:leave-start="translate-x-0" x-transition:leave-end="-translate-x-full"
|
||||||
|
class="absolute inset-y-0 left-0 w-[280px] max-w-[85%] sidebar-tint border-r border-line-soft overflow-y-auto px-3.5 pt-5 pb-4">
|
||||||
|
<div class="flex justify-end -mt-1 mb-1">
|
||||||
|
<button type="button" @click="nav = false"
|
||||||
|
class="grid place-items-center w-9 h-9 rounded-lg text-ink-2 hover:bg-raised hover:text-ink transition-colors"
|
||||||
|
aria-label="{{ __('common.close') }}">
|
||||||
|
<x-icon name="close" :size="20" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<x-sidebar />
|
||||||
|
</aside>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@livewireScripts
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|
@ -0,0 +1,37 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}" class="scroll-smooth">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<meta name="csrf-token" content="{{ csrf_token() }}">
|
||||||
|
<link rel="icon" href="{{ asset('favicon.svg') }}" type="image/svg+xml">
|
||||||
|
<title>{{ __('auth.title') }} · {{ __('common.app_name') }}</title>
|
||||||
|
@vite(['resources/css/app.css', 'resources/js/app.js'])
|
||||||
|
@livewireStyles
|
||||||
|
</head>
|
||||||
|
<body class="min-h-dvh antialiased">
|
||||||
|
<div class="min-h-dvh flex items-center justify-center px-5 py-10">
|
||||||
|
<div class="w-full max-w-[400px] flex flex-col gap-6">
|
||||||
|
{{-- brand --}}
|
||||||
|
<div class="flex items-center gap-3">
|
||||||
|
<span class="logo-mark grid place-items-center w-10 h-10 rounded-[11px] text-accent">
|
||||||
|
<x-icon name="dashboard" :size="20" />
|
||||||
|
</span>
|
||||||
|
<span class="leading-tight">
|
||||||
|
<span class="block font-extrabold text-lg tracking-[0.02em] text-ink">{{ __('common.app_name') }}</span>
|
||||||
|
<span class="block text-[10px] font-semibold uppercase tracking-[0.14em] text-ink-3">{{ __('common.app_tagline') }}</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{{ $slot }}
|
||||||
|
|
||||||
|
<p class="flex items-center justify-center gap-1.5 text-[11px] text-ink-3">
|
||||||
|
<x-icon name="lock" :size="12" />
|
||||||
|
{{ __('auth.secured') }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@livewireScripts
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|
@ -0,0 +1,48 @@
|
||||||
|
<div class="bg-surface border border-line-soft rounded-card p-6 flex flex-col gap-5">
|
||||||
|
<div class="flex flex-col gap-1">
|
||||||
|
<h1 class="text-lg font-bold text-ink">{{ __('auth.title') }}</h1>
|
||||||
|
<p class="text-[13px] text-ink-3">{{ __('auth.subtitle') }}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<form wire:submit="login" class="flex flex-col gap-4">
|
||||||
|
{{-- email --}}
|
||||||
|
<div class="flex flex-col gap-1.5">
|
||||||
|
<label for="email" class="text-[12px] font-semibold text-ink-2">{{ __('auth.email') }}</label>
|
||||||
|
<input wire:model="email" id="email" type="email" name="email"
|
||||||
|
autocomplete="username" required autofocus
|
||||||
|
class="w-full rounded-lg bg-raised border border-line px-3 py-2.5 text-sm text-ink placeholder:text-ink-3 outline-none transition-colors focus:border-accent focus:ring-1 focus:ring-accent">
|
||||||
|
@error('email')
|
||||||
|
<p class="flex items-center gap-1.5 text-[12px] text-offline">
|
||||||
|
<x-icon name="alert" :size="13" /> {{ $message }}
|
||||||
|
</p>
|
||||||
|
@enderror
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{{-- password --}}
|
||||||
|
<div class="flex flex-col gap-1.5">
|
||||||
|
<label for="password" class="text-[12px] font-semibold text-ink-2">{{ __('auth.password') }}</label>
|
||||||
|
<input wire:model="password" id="password" type="password" name="password"
|
||||||
|
autocomplete="current-password" required
|
||||||
|
class="w-full rounded-lg bg-raised border border-line px-3 py-2.5 text-sm text-ink placeholder:text-ink-3 outline-none transition-colors focus:border-accent focus:ring-1 focus:ring-accent">
|
||||||
|
@error('password')
|
||||||
|
<p class="flex items-center gap-1.5 text-[12px] text-offline">
|
||||||
|
<x-icon name="alert" :size="13" /> {{ $message }}
|
||||||
|
</p>
|
||||||
|
@enderror
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{{-- remember --}}
|
||||||
|
<label class="flex items-center gap-2 text-[13px] text-ink-2 select-none cursor-pointer">
|
||||||
|
<input wire:model="remember" type="checkbox"
|
||||||
|
class="rounded border-line bg-raised text-accent focus:ring-accent focus:ring-offset-0">
|
||||||
|
{{ __('auth.remember') }}
|
||||||
|
</label>
|
||||||
|
|
||||||
|
{{-- submit --}}
|
||||||
|
<button type="submit" wire:loading.attr="disabled"
|
||||||
|
class="mt-1 inline-flex items-center justify-center gap-2 rounded-lg bg-accent px-4 py-2.5 text-sm font-bold text-base transition-[filter] hover:brightness-110 disabled:opacity-60 disabled:cursor-not-allowed">
|
||||||
|
<span wire:loading.remove wire:target="login">{{ __('auth.submit') }}</span>
|
||||||
|
<span wire:loading wire:target="login">{{ __('auth.signing_in') }}</span>
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
@ -0,0 +1,68 @@
|
||||||
|
<div>
|
||||||
|
<x-topbar :title="__('dashboard.title')" :subtitle="__('dashboard.subtitle')" />
|
||||||
|
|
||||||
|
<div class="px-5 lg:px-[26px] py-6 flex flex-col gap-5 max-w-[1560px] mx-auto w-full">
|
||||||
|
{{-- stack facts --}}
|
||||||
|
<div class="grid grid-cols-1 sm:grid-cols-2 xl:grid-cols-4 gap-3 reveal">
|
||||||
|
<x-kpi :label="__('dashboard.stack_framework')" value="Laravel 13" icon="bolt" />
|
||||||
|
<x-kpi :label="__('dashboard.stack_database')" value="PG 17" :meta="'TimescaleDB'" icon="network" />
|
||||||
|
<x-kpi :label="__('dashboard.stack_realtime')" value="Reverb" :meta="'WebSocket'" icon="activity" />
|
||||||
|
<x-kpi :label="__('dashboard.stack_queue')" value="Redis" :meta="'Horizon'" icon="devices" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="grid grid-cols-1 lg:grid-cols-[1fr_340px] gap-5 items-start">
|
||||||
|
{{-- welcome --}}
|
||||||
|
<x-panel class="reveal">
|
||||||
|
<div class="flex flex-col gap-4">
|
||||||
|
<div class="flex items-start gap-3">
|
||||||
|
<span class="logo-mark grid place-items-center w-11 h-11 rounded-[12px] text-accent shrink-0">
|
||||||
|
<x-icon name="dashboard" :size="22" />
|
||||||
|
</span>
|
||||||
|
<div class="flex flex-col gap-1">
|
||||||
|
<h2 class="text-base font-bold text-ink">{{ __('dashboard.welcome_title') }}</h2>
|
||||||
|
<p class="text-[13px] text-ink-2 leading-relaxed">{{ __('dashboard.welcome_body') }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-wrap items-center gap-2 pt-1">
|
||||||
|
<x-status-pill state="online">
|
||||||
|
<x-status-dot state="online" class="!w-1.5 !h-1.5" /> {{ __('common.system_ok') }}
|
||||||
|
</x-status-pill>
|
||||||
|
<x-status-pill state="neutral">{{ __('dashboard.phase_label') }} · {{ __('dashboard.phase_value') }}</x-status-pill>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</x-panel>
|
||||||
|
|
||||||
|
{{-- design-system legend --}}
|
||||||
|
<x-panel :title="__('dashboard.legend_title')" class="reveal">
|
||||||
|
<div class="flex flex-col gap-4">
|
||||||
|
{{-- status colors --}}
|
||||||
|
<div class="flex flex-col gap-2">
|
||||||
|
<span class="text-[11px] font-semibold uppercase tracking-[0.08em] text-ink-3">{{ __('dashboard.legend_status') }}</span>
|
||||||
|
<div class="flex flex-col gap-1.5 text-[13px] text-ink-2">
|
||||||
|
<span class="flex items-center gap-2"><x-status-dot state="online" :pulse="true" /> {{ __('common.status_online') }}</span>
|
||||||
|
<span class="flex items-center gap-2"><x-status-dot state="warning" /> {{ __('common.status_warning') }}</span>
|
||||||
|
<span class="flex items-center gap-2"><x-status-dot state="offline" /> {{ __('common.status_offline') }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{{-- interactive toggle (verifies Livewire) --}}
|
||||||
|
<div class="flex items-center gap-3 border-t border-line-soft pt-3">
|
||||||
|
<span class="text-[13px] text-ink-2">{{ __('dashboard.demo_toggle') }}</span>
|
||||||
|
<x-toggle :on="$demoOn" wire:click="toggleDemo" :label="__('dashboard.demo_toggle')" class="ml-auto" />
|
||||||
|
<x-status-pill :state="$demoOn ? 'online' : 'neutral'">{{ $demoOn ? __('common.status_online') : __('common.status_offline') }}</x-status-pill>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{{-- device chips --}}
|
||||||
|
<div class="flex flex-col gap-2 border-t border-line-soft pt-3">
|
||||||
|
<span class="text-[11px] font-semibold uppercase tracking-[0.08em] text-ink-3">{{ __('dashboard.demo_chips') }}</span>
|
||||||
|
<div class="flex flex-wrap gap-1.5">
|
||||||
|
<x-device-chip state="online">Shelly Plus 1</x-device-chip>
|
||||||
|
<x-device-chip state="warning">BLU Door</x-device-chip>
|
||||||
|
<x-device-chip state="offline" :off="true">Plug S</x-device-chip>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</x-panel>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
@ -0,0 +1,7 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Support\Facades\Broadcast;
|
||||||
|
|
||||||
|
Broadcast::channel('App.Models.User.{id}', function ($user, $id) {
|
||||||
|
return (int) $user->id === (int) $id;
|
||||||
|
});
|
||||||
|
|
@ -0,0 +1,8 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Foundation\Inspiring;
|
||||||
|
use Illuminate\Support\Facades\Artisan;
|
||||||
|
|
||||||
|
Artisan::command('inspire', function () {
|
||||||
|
$this->comment(Inspiring::quote());
|
||||||
|
})->purpose('Display an inspiring quote');
|
||||||
|
|
@ -0,0 +1,24 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
use App\Livewire\Auth\Login;
|
||||||
|
use App\Livewire\Dashboard;
|
||||||
|
use Illuminate\Support\Facades\Auth;
|
||||||
|
use Illuminate\Support\Facades\Route;
|
||||||
|
|
||||||
|
Route::get('/', fn () => redirect()->route('dashboard'));
|
||||||
|
|
||||||
|
Route::middleware('guest')->group(function () {
|
||||||
|
Route::get('/login', Login::class)->name('login');
|
||||||
|
});
|
||||||
|
|
||||||
|
Route::middleware('auth')->group(function () {
|
||||||
|
Route::get('/dashboard', Dashboard::class)->name('dashboard');
|
||||||
|
|
||||||
|
Route::post('/logout', function () {
|
||||||
|
Auth::guard('web')->logout();
|
||||||
|
request()->session()->invalidate();
|
||||||
|
request()->session()->regenerateToken();
|
||||||
|
|
||||||
|
return redirect()->route('login');
|
||||||
|
})->name('logout');
|
||||||
|
});
|
||||||
|
|
@ -0,0 +1,34 @@
|
||||||
|
# rules.md — HomeOS STRICT RULES
|
||||||
|
|
||||||
|
> Carried over 1:1 from the user's Clusev project. **Non-negotiable.**
|
||||||
|
> On any conflict between a rule and a request: **STOP and ask.**
|
||||||
|
> Full examples live in the Clusev `rules.md`; this is the compact, authoritative list.
|
||||||
|
|
||||||
|
## Core rules
|
||||||
|
|
||||||
|
- **R1** Pages = full-page class-based Livewire components mapped directly in `routes/web.php`. No page controllers.
|
||||||
|
- **R2** Livewire class + separate Blade view. **Volt forbidden** (never `make:volt`).
|
||||||
|
- **R3** All colors/design tokens in `@theme` (`resources/css/app.css`); markup uses token utilities only — no raw hex/rgb.
|
||||||
|
- **R4** No inline `style=` — single exception: progress-bar `width`.
|
||||||
|
- **R5** Destructive/confirm actions via **wire-elements/modal** — never `confirm()` / Alpine popups.
|
||||||
|
- **R6** Files exactly per the folder map (handoff §7).
|
||||||
|
- **R7** Responsive verified at 375 / 768 / 1280; touch targets ≥ 44 px.
|
||||||
|
- **R8** Every PHP / Composer / Node / artisan command **inside the container** — the host has only Docker.
|
||||||
|
- **R9** UI copy German, terse/operational, **no emoji**; status via color/dots/pills; technical tokens (MAC, IP, model) stay native.
|
||||||
|
- **R10** Reuse `@theme` tokens + the Blade component kit — no ad-hoc colors/widgets.
|
||||||
|
- **R11** URL-exposed records addressed by **UUID**, never integer PK (`getRouteKeyName(): 'uuid'`).
|
||||||
|
- **R12** Every touched page verified in a real browser: **HTTP 200, zero console errors, zero failed requests**; the loaded state of lazy pages (not the skeleton); inspect the rendered DOM for leaked `@` / `{{ }}` / `group.key` literals. A green `Livewire::test` ≠ done.
|
||||||
|
- **R13** Route paths + names **English** (`/devices`, not `/geraete`); German only in visible labels.
|
||||||
|
- **R14** Fonts **self-hosted** (`resources/fonts/*.woff2`, relative `url('../fonts/…')` + `@font-face`) — no Google Fonts / CDN link or import.
|
||||||
|
- **R15** **Codex review** (`/codex:review`) after every change — no errors, no security findings, else fix and re-run. Task not done until clean.
|
||||||
|
- **R16** Every UI string localized in `lang/{de,en}/<group>.php`, identical keys in both languages, accessed via `__('group.key')` — no hard-coded copy anywhere.
|
||||||
|
- **R17** Blade: block `@php … @endphp` only (no inline `@php(...)`); never write the literal directive tokens in comments/text.
|
||||||
|
- **Secrets:** `.env*`, tokens, keys are never staged; check `git status` before every commit. Feature branches, never commit directly to `main`.
|
||||||
|
|
||||||
|
## HomeOS-specific additions
|
||||||
|
|
||||||
|
- **H1** UI / Livewire never publishes to protocol topics directly — every command goes through `DeviceCommandService` → driver, and every command is written to the `commands` audit table.
|
||||||
|
- **H2** MQTT subscriber callbacks: **parse + dispatch only**. No DB aggregation, no HTTP calls in the loop.
|
||||||
|
- **H3** Vendor specifics (topics, payload shapes) live only in `app/Support/Mqtt` + `app/Support/Drivers`. Models, Livewire and views stay protocol-agnostic.
|
||||||
|
- **H4** Telemetry writes go through one ingest path that enforces retention/compression policies.
|
||||||
|
- **H5** Automations always carry `cooldown_seconds`; the engine has a **dry-run mode** (log instead of switch) for development — never develop automations against the live home.
|
||||||
|
|
@ -0,0 +1,4 @@
|
||||||
|
*
|
||||||
|
!private/
|
||||||
|
!public/
|
||||||
|
!.gitignore
|
||||||
|
|
@ -0,0 +1,2 @@
|
||||||
|
*
|
||||||
|
!.gitignore
|
||||||
|
|
@ -0,0 +1,2 @@
|
||||||
|
*
|
||||||
|
!.gitignore
|
||||||
|
|
@ -0,0 +1,9 @@
|
||||||
|
compiled.php
|
||||||
|
config.php
|
||||||
|
down
|
||||||
|
events.scanned.php
|
||||||
|
maintenance.php
|
||||||
|
routes.php
|
||||||
|
routes.scanned.php
|
||||||
|
schedule-*
|
||||||
|
services.json
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
*
|
||||||
|
!data/
|
||||||
|
!.gitignore
|
||||||
|
|
@ -0,0 +1,2 @@
|
||||||
|
*
|
||||||
|
!.gitignore
|
||||||
|
|
@ -0,0 +1,2 @@
|
||||||
|
*
|
||||||
|
!.gitignore
|
||||||
|
|
@ -0,0 +1,2 @@
|
||||||
|
*
|
||||||
|
!.gitignore
|
||||||
|
|
@ -0,0 +1,2 @@
|
||||||
|
*
|
||||||
|
!.gitignore
|
||||||
|
|
@ -0,0 +1,2 @@
|
||||||
|
*
|
||||||
|
!.gitignore
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue