evilstudio / homelab-assistant-tool
Homelab operations tool: CLI and web UI for devices, UPS units, schedules, and action logs.
Package info
github.com/evilprophet/homelab-assistant-tool
Type:project
pkg:composer/evilstudio/homelab-assistant-tool
Requires
- php: ^8.3
- ext-ctype: *
- ext-iconv: *
- ext-json: *
- ext-mbstring: *
- ext-openssl: *
- ext-pdo: *
- ext-pdo_sqlite: *
- diegonz/php-wake-on-lan: ^2.1
- doctrine/doctrine-bundle: ^2.13
- doctrine/doctrine-migrations-bundle: ^3.4
- doctrine/orm: ^3.6
- dragonmantank/cron-expression: ^3.4
- geerlingguy/ping: ^1.2
- phpseclib/phpseclib: ^3.0
- symfony/asset: 7.4.*
- symfony/dotenv: ^7.4
- symfony/flex: ^2
- symfony/framework-bundle: ^7.4
- symfony/monolog-bundle: ^4.0
- symfony/process: ^7.4
- symfony/runtime: ^7.4
- symfony/security-bundle: ^7.4
- symfony/twig-bundle: ^7.4
- symfony/yaml: ^7.4
Requires (Dev)
- phpunit/phpunit: ^12.0
- squizlabs/php_codesniffer: ^4.0
Conflicts
This package is auto-updated.
Last update: 2026-08-17 16:35:04 UTC
README
Introduction
Homelab Assistant Tool is an application for homelab operations and automation. It combines a CLI interface with a web UI for managing devices, UPS units, schedules, and operational logs.
For technical details, see Tech Stack.
โจ Key Features
- ๐ Device operations: Start, stop, check status, and open SSH sessions for managed devices
- ๐ UPS-aware behavior: UPS monitoring with battery-aware automation, including shutdown on low runtime
- โฐ Scheduling: Cron-based automation with schedule-to-device assignment
- ๐งพ Action logs: Unified history for
CLI,CRON, andWEBoperations, with filtering and cleanup - ๐ Web UI: Management views for devices, UPS, schedules, and logs, plus manual operational actions
- ๐ Authentication:
simplemode with local accounts, oroidcmode delegating to an identity provider
๐ Project Structure
.
โโโ assets/ # Tailwind sources for the web UI
โโโ bin/ # CLI entrypoints and helper scripts
โโโ config/ # Symfony and app configuration
โโโ docker/ # Entrypoint and scheduler crontab shipped in the image
โโโ docs/ # Technical documentation
โโโ migrations/ # Doctrine migrations
โโโ public/ # Web entrypoint and built assets
โโโ src/ # Application source code
โ โโโ Command/ # CLI commands (setup, CRUD, runtime, users)
โ โโโ Contract/ # Runtime contracts and enums
โ โโโ Controller/ # Web controllers
โ โโโ Entity/ # Doctrine entities
โ โโโ EventSubscriber/ # Request, auth, log, and security-header subscribers
โ โโโ Exception/ # Domain exceptions
โ โโโ Factory/ # Runtime factories and adapters
โ โโโ Helper/ # Configuration and helpers
โ โโโ Repository/ # Doctrine repositories
โ โโโ Runtime/ # Runtime models
โ โโโ Security/ # Authenticators
โ โโโ Service/ # Application, runtime, auth, and infrastructure services
โโโ templates/ # Twig templates
โโโ tests/ # Unit, integration, and functional test suites
โโโ var/ # Runtime data, cache, logs, SQLite files
๐ ๏ธ Requirements
Always required:
- PHP
8.3+withctype,iconv,json,mbstring,openssl,pdo,pdo_sqlite,sockets, andxml - Composer
- An SSH private key reachable by the application, for shutting down Linux devices
- A scheduler for
hat:cron:execute- the Docker setup ships one, otherwise host cron
Required per feature:
- NUT client (
upsc) on the host running HAT, for UPS status and battery-mode automation - Docker and Docker Compose, only to serve the web UI
- Node.js, only to rebuild the stylesheet during development
๐ Quick Start
HAT runs in two setups, described in Run Modes. Pick one and follow the matching steps.
Option 1: Docker only
Runs HAT without a working copy of the repository.
-
Download
docker-compose.yml,.env, andconfig/parameters.yaml:mkdir -p config var/data curl -L -o docker-compose.yml https://raw.githubusercontent.com/evilprophet/homelab-assistant-tool/2.x/docker-compose.yml curl -L -o .env https://raw.githubusercontent.com/evilprophet/homelab-assistant-tool/2.x/.env.example curl -L -o config/parameters.yaml https://raw.githubusercontent.com/evilprophet/homelab-assistant-tool/2.x/config/parameters.yaml.template
config/parameters.yamlis not tracked in the repository, so the template stands in for it. It ships working defaults, which matters because the application refuses to boot without that file. -
Update
.envandconfig/parameters.yamlfor your environment:.envfor app and auth settings,config/parameters.yamlfor runtime settings.APP_SECRETmust be replaced - see Configuration.Edit both files on the host. Do not run
hat:setup:configurein this setup: compose bind-mountsconfig/parameters.yamlas a single file, and the command rewrites it through an atomic rename, which a container cannot perform over a bind-mounted file. -
Pull the GHCR image:
docker compose pull
-
Run the setup commands:
docker compose run --rm hat-app php bin/console hat:setup:db --init docker compose run --rm hat-app php bin/console hat:user:create # only for simple auth mode -
Start the container and open
http://localhost:8080:docker compose up -d
-
Confirm the scheduler is running:
docker compose logs -f hat-cron
docker compose up -dalso starts thehat-cronservice, which runshat:cron:executeevery five minutes. Nothing else has to be scheduled on the host. Set schedule minutes on five-minute boundaries - see Scheduling.
Option 2: Cloned project
Used for development, or to run the CLI without a container.
- Clone the repository:
git clone https://github.com/evilprophet/homelab-assistant-tool.git cd homelab-assistant-tool composer install cp .env.example .env - Update
.envwith application and auth values for your environment.APP_SECRETmust be replaced - see Configuration. - Run the setup commands:
php bin/console hat:setup:configure php bin/console hat:setup:db --init php bin/console hat:user:create # only for simple auth mode - Serve the web UI, from the published image or from a local build with Xdebug:
# Published image docker compose pull && docker compose up -d # Local build with Xdebug docker compose -f docker-compose.dev.yml up -d --build
Wake-on-LAN and UPS status do not work in the development compose, because it cannot use host networking. The file itself documents why. - Schedule
hat:cron:executeon the host - a cloned project schedules nothing on its own. See Scheduling.
๐ Commands
| Command | When | Description |
|---|---|---|
hat:setup:configure |
install | Configure app technical settings. |
hat:setup:db |
install, update | Initialize or migrate the SQLite database. |
hat:setup:init |
install | Run the full setup flow. |
hat:device:create/update/remove/list |
day-to-day | Manage devices. |
hat:ups:create/update/remove/list |
day-to-day | Manage UPS entries. |
hat:schedule:create/update/remove/list |
day-to-day | Manage schedules. |
hat:device:check-status |
day-to-day | Check runtime status for a device. |
hat:device:ssh |
day-to-day | Open an SSH session to a device. |
hat:device:start |
day-to-day | Send a wake-on-LAN packet to a device. |
hat:device:stop |
day-to-day | Shut down a device over SSH. |
hat:cron:execute |
scheduler, every tick | Execute scheduled tasks and maintenance. |
hat:logs:list |
day-to-day | List action logs stored in the database. |
hat:logs:cleanup |
day-to-day | Remove old or all action logs. |
hat:user:create |
web, simple auth mode | Create a local user. |
hat:user:remove |
web, simple auth mode | Remove a local user. |
hat:user:reset-password |
web, simple auth mode | Reset a local user password. |
hat:cron:execute is the only command meant to run unattended; everything else is operator-driven. The web rows matter only when the web UI runs with HAT_AUTH_MODE=simple.
Scripted use
- Removal commands require
--forceunder--no-interaction, and fail instead of prompting when it is missing hat:device:sshreturns the exit code of the underlyingsshprocesshat:device:startreports that the wake-on-LAN packet was sent; it cannot confirm that the device booted- Passwords must not contain whitespace, and are rejected at creation and reset rather than silently trimmed
Pass passwords through --password-stdin rather than --password, which stays visible in shell history and in the process list:
printf '%s' "$NEW_PASSWORD" | bin/console hat:user:create admin --password-stdin --no-interaction
๐งฉ Run Modes
HAT is a CLI application first. The web UI is an optional layer on top of it, and every operational action is available from the command line.
| Requirement | CLI only | CLI + web |
|---|---|---|
config/parameters.yaml |
yes | yes |
| SQLite database | yes | yes |
| SSH key for Linux shutdown | yes | yes |
Scheduler for hat:cron:execute |
host cron | hat-cron service |
| Migrations after an update | manual | automatic |
Container and .env auth settings |
no | yes |
Both modes share one SQLite database. Initial setup is always a CLI step; the container takes over scheduling and migrations only once it is running.
Mixing the two - serving the web UI from the image while running commands from a cloned project - works, but both installations must then stay on the same version, because they run different code against one schema.
โฐ Scheduling
hat:cron:execute evaluates due schedules, applies UPS battery-mode rules, and prunes old action logs. It is the only command meant to run unattended.
In the Docker setup the hat-cron service runs it every five minutes. To change the interval:
- Write your own crontab file:
# ./hat-crontab */10 * * * * cd /app && php bin/console hat:cron:execute >> /proc/1/fd/1 2>&1
- Mount it over the one baked into the image:
hat-cron: volumes: - ./hat-crontab:/etc/crontabs/hat:ro
Without the container, schedule it on the host:
*/5 * * * * cd /path/to/homelab-assistant-tool && php bin/console hat:cron:execute
Two rules apply to any interval:
- A schedule fires when its due minute falls within one minute of a tick, so schedule minutes must sit on the interval's boundary. With a five-minute interval use
0,5,10, and so on - Overlapping runs are prevented by a lock file in
var/data/, so a host crontab and thehat-cronservice cannot execute the same tick twice as long as both use the same data directory
โ๏ธ Configuration
Runtime behaviour lives in config/parameters.yaml, written by hat:setup:configure. Authentication and framework settings live in .env.
APP_SECRET must be replaced with a real value, because .env.example ships the literal placeholder {app_secret}. The container refuses to start while it is unset or still the placeholder:
openssl rand -hex 64
If the SSH key is passphrase-protected, put the passphrase in ssh_key_passphrase and leave it empty for an unencrypted key. hat:setup:configure does not ask for it, so add it by hand.
The container reads these environment variables:
| Variable | Default | Effect |
|---|---|---|
HAT_AUTO_MIGRATE |
1 |
Back up the database and apply pending migrations before the web server starts. 0 disables it. |
PHP_CLI_SERVER_WORKERS |
4 |
Requests the web server handles in parallel. HAT blocks on SSH, upsc, and ICMP inside requests, so a value of 1 lets one unreachable host stall the whole application. |
TRUSTED_PROXIES |
empty | Comma-separated reverse proxy addresses allowed to set X-Forwarded-*. Use 127.0.0.1 for a proxy on the same host. |
HAT_AUTH_MODE |
- | simple or oidc, lowercase. Any other value stops the application at startup with a single clear error rather than failing per request. |
Set HAT_AUTO_MIGRATE=0 when a cloned project shares the same database, so migrations stay under your control rather than being applied by whichever installation starts first. The automatic step never runs for docker compose run commands, only when the container starts the web server.
๐ Reverse Proxy
The container serves plain HTTP on port 8080 and terminates no TLS of its own. Running it behind a reverse proxy is the supported way to reach HAT over HTTPS, and .env.example already assumes it through DEFAULT_URI and OIDC_REDIRECT_URI.
The proxy is responsible for:
- Terminating TLS and redirecting
http://tohttps:// - Sending
Strict-Transport-Security: max-age=31536000- HAT deliberately does not set this header itself, because a direct HTTP deployment on the LAN is equally valid and would be broken by it - Forwarding
X-Forwarded-For,X-Forwarded-Proto,X-Forwarded-Host, andX-Forwarded-Port
Set TRUSTED_PROXIES to the proxy address. Until it is set, Symfony ignores every X-Forwarded-* header, so generated URLs use http and the client IP in the action log is the proxy's. Never list whole private ranges there.
Serving HAT under a path prefix (for example /hat/) additionally requires the proxy to send X-Forwarded-Prefix and that header to be added to Symfony's trusted headers, which is not part of its default set.
โฌ๏ธ Updating
In the Docker setup the container migrates itself: on start it backs up the database, applies pending migrations, and refuses to serve traffic if a migration fails. A cloned project migrates nothing on its own.
# Docker docker compose pull docker compose up -d # Cloned project git pull composer install --no-dev php bin/console hat:setup:db --migrate --backup
The backup is written next to the database as hat.sqlite.<timestamp>.bak, and only when migrations are actually pending, so restarts do not accumulate copies. Old backups are never pruned - remove them yourself.
With HAT_AUTO_MIGRATE=0, or when both installations share one database, run the migration explicitly before starting the new version:
docker compose run --rm hat-app php bin/console hat:setup:db --migrate --backup
Upgrading to a non-root container
The container used to run as root and now runs as UID/GID 1000. Two things must be adjusted once, before the first start of the new image:
- Hand the mounted directories to the new user:
sudo chown -R 1000:1000 var/data var/log
- Move the SSH key into the mounted data directory, because a key under
/root/.ssh/is no longer readable:cp ~/.ssh/id_ed25519 var/data/id_ed25519 chmod 600 var/data/id_ed25519 sudo chown 1000:1000 var/data/id_ed25519Then pointssh_key_pathinconfig/parameters.yamlat the new location.
Skipping either step leaves the application unable to write its database or to shut down Linux devices.
๐งช Development
Quick Start:
# Run all tests (Xdebug off) bin/phpunit # Run with coverage report bin/phpunit-coverage # Check the coding standard, and fix what can be fixed automatically composer cs composer cs-fix # Rebuild the stylesheet npm run build:css
๐ Documentation:
- ๐ Test Implementation Summary - Overview and current scope
- ๐ CI/CD Guide - GitLab quality pipeline and GHCR image publication
- ๐งฑ Tech Stack - Frameworks, libraries, and tooling assumptions
๐งญ Notes
- Business data is stored in SQLite, and integration/functional tests use a separate database (
var/data/hat_test.sqlite) - Runtime settings are configured in
config/parameters.yaml - Web request logs are written to
var/log/, rotated daily and kept for 28 days (web-<date>.login production,web.login development); no host-side logrotate rule is needed. Application actions are recorded in the database and readable throughhat:logs:listor the web UI - Every account has the same privileges. There is no admin/user split: anyone who can log in can edit devices (which hold SSH credentials), schedules, and UPS entries, and can purge action logs. In
oidcmode any user the identity provider lets through is provisioned automatically, so the provider's application-access policy is the only authorization gate - Sessions and the Symfony cache live in
var/cache, which is not a mounted volume. Updating the image or recreating the container logs everyone out - by design, nothing there needs backing up