Homelable could only own the root of an origin. Behind an existing proxy
at `https://home.example/homelab/` the built `index.html` still asked for
`/assets/*`, which fell through to whatever owned the root — and when
that answered `text/html` for a `<script>` under `nosniff`, the browser
failed the load on an HTTP 200. The only workaround was patching the
checkout and rebuilding on every upgrade.
One build-time knob, `VITE_BASE_PATH`, becomes Vite's `base`. Vite
rewrites the asset URLs it emits; everything the app builds by hand goes
through the new `utils/basePath.ts` — the axios instances, the WebSocket
URL, the live-view route, the local brand icons, and the OIDC login href.
`resolveServerPath` covers what the *backend* hands back, which is always
root-absolute because it cannot know where the SPA is mounted: uploaded
floor-plan URLs already stored in a canvas are resolved at render time,
so plans predating the move keep loading. The OIDC callback used to
redirect to `/`, dropping subpath users at the origin root after login;
it now reads the prefix back out of `OIDC_REDIRECT_URI`.
The default is `/`, and stays a no-op there by construction: every helper
returns the string it returned before, the root build output is unchanged
and both nginx sites are byte-identical to what shipped — the Docker
image copies `docker/nginx.conf` verbatim and the installer keeps its
original heredoc. Only a non-default prefix takes the generated config.
Those generated configs use `root`, never `alias`, since `alias` plus
`try_files` mis-resolves `$uri` — the Docker build lands the bundle in
the matching subdirectory, and the installer symlinks it under
`/var/www/homelable`. Both accept either reverse-proxy style, prefix
forwarded intact or already stripped, with no redirect loop between them,
and `absolute_redirect off` stops the no-slash 301 from eating the port.
Closes#334.
ha-relevant: maybe
ShellCheck reads the installer but never runs it, so it cannot catch a
`set -euo pipefail` abort — the failure class that broke this script on a
fresh host. This job executes it for real in debian:12, which carries no
Node and gives the job no TTY, so both the nodesource path and the prompt
fallbacks are exercised on every run.
It asserts what the script promises: SECRET_KEY generated, the bcrypt hash
and the JSON values single-quoted (the systemd EnvironmentFile trap),
.env at mode 600, SQLITE_PATH under the install dir, the venv and the Vite
build present, the service user created, the unit's ExecStart and
EnvironmentFile correct. It then boots the backend by the unit's own
ExecStart and waits on /api/v1/health, which proves the generated .env
actually parses, and re-runs the installer to check the idempotency claim
leaves .env untouched.
systemd is out of reach in a container: systemctl is stubbed, so the unit
is written but never started, and the EnvironmentFile parse itself stays
untested. The job is gated on paths, since it costs roughly six minutes.
Also adds gnupg to the installer's apt list — the nodesource setup script
needs it and a minimal Debian does not have it.
Verified by running the same steps locally in debian:12: all assertions
pass, the backend answers /api/v1/health, and the second run keeps .env.
ha-relevant: no
Two ways install-baremetal.sh failed on exactly the hosts it targets, both
caused by `set -euo pipefail` turning an expected non-zero into an exit.
Node detection ran `node --version` in a command substitution. On a host
with no Node — the normal case, since the base apt list does not install
it — that exits 127, pipefail propagates it, and the script died at that
line, before the block that installs Node. `|| true` plus a numeric
fallback, and the version is stripped to digits so `-lt` cannot error on
unexpected output. The same abort applied to the `ip` and `hostname -I`
substitutions, which now tolerate failure too.
The prompts had no TTY guard, so the documented `curl … | sudo bash` form
hit EOF on the first `read` and aborted. Both prompts are now skipped when
stdin is not a terminal, falling back to their defaults with a warning.
Two `[[ … ]] && …` one-liners were the same trap in miniature — a false
test is a non-zero list — and are now if/fi.
Docs follow the behaviour: the piped install is shown with ADMIN_PASSWORD
and SCANNER_RANGES set, and the fallback (password `admin`, guessed range)
is stated rather than implied.
Verified by running the patched blocks under `env -i` with no node on PATH
and stdin closed: both reach the end, exit 0. Shellcheck and `bash -n`
clean.
ha-relevant: no
Adds scripts/install-baremetal.sh: installs Homelable natively on a
Debian/Ubuntu host — Python venv plus a homelable systemd unit for the
backend on 127.0.0.1:8000, the built frontend served by nginx on :3000.
Modeled on scripts/lxc-mcp-install.sh, with the install steps taken from
the community-scripts/ProxmoxVE recipe so the two stay recognisably the
same install.
The script clones into INSTALL_DIR when empty, creates the service user,
builds the venv and the frontend, generates backend/.env with a random
SECRET_KEY and a bcrypt hash, writes the systemd unit and the nginx site,
then waits on /api/v1/health. Re-running is safe and is the upgrade path:
an existing .env is kept, everything else is rebuilt. Every prompt has an
environment-variable override, so a non-interactive install is one line.
Two details worth calling out:
- JSON values in the generated .env are single-quoted. systemd's
EnvironmentFile parser strips bare double quotes, which would hand
pydantic [http://...] instead of ["http://..."] and fail startup.
- The admin password reaches Python through the environment rather than
argv, which is world-readable in ps.
The unit runs unprivileged, so nmap falls back to a TCP connect scan;
AmbientCapabilities=CAP_NET_RAW is shipped commented out with the
trade-off spelled out in the unit and in the docs.
INSTALLATION.md gains a "Bare metal — no Docker" section covering the
quick start, the upgrade, the option table, the non-root scan trade-off
and the host-nginx blocks for anyone bringing their own reverse proxy.
README links it from the install line.
No test suite: shell installers have none in this repo, and CI's
lint-scripts job shellchecks scripts/. Shellcheck is clean.
Closes#333
ha-relevant: no
- All prompted values overridable via env vars (MCP_API_KEY,
MCP_SERVICE_KEY, BACKEND_URL, INSTALL_DIR, etc.).
- Clone the repo into INSTALL_DIR if it isn't already present, so the
script can be fetched and run directly inside a fresh LXC created by
the community-scripts/ProxmoxVE helper (no manual git clone first).
- README: clarify the Proxmox flow (community-scripts creates the LXC,
user runs this script inside it).
Adds scripts/lxc-mcp-install.sh for Proxmox LXC and other non-Docker
hosts. Creates a homelable-mcp systemd service, prompts for
MCP_API_KEY / MCP_SERVICE_KEY (auto-generated on Enter), and skips
prompts when mcp/.env already exists so user edits are preserved.
Closes#132
- Prompt for container ID with auto-detected default (pvesh nextid)
- Prompt for root password with confirmation loop instead of auto-generating
- Both still overridable via CTID/ROOT_PASSWORD env vars for automation
- ping not available in base Debian 12 LXC — was silently hitting 20s timeout
- curl may also be missing — install it explicitly before running lxc-install.sh
- use ip route check for network readiness instead of ping
- install-proxmox.sh: runs on PVE host, creates Debian 12 LXC via pct,
then calls lxc-install.sh inside the container (tteck community-scripts pattern)
- lxc-install.sh: runs inside the container, fixed to use .env instead of
config.yml, correct bcrypt hash, proper CORS_ORIGINS with container IP
- README: clarify that install-proxmox.sh runs on the PVE host
All settings (auth credentials, scanner ranges, status_checker interval)
now live in a single .env file via pydantic-settings. config.yml and
config.yml.example are deleted.
- Settings: add auth_username, auth_password_hash, scanner_ranges,
status_checker_interval; add load_overrides()/save_overrides() for
persisting runtime changes to data/scan_config.json
- auth.py: read credentials directly from settings
- scan.py: read ranges/interval from settings; write-back via save_overrides()
- scheduler.py: read interval directly from settings
- main.py: call settings.load_overrides() at startup
- docker-compose.yml: remove config.yml volume mount, add new env vars
- conftest.py: set settings fields directly instead of writing a temp config.yml
- scanner.py: re-raise nmap exceptions so they reach ScanRun.error
(previously silently returned [] hiding the root cause)
- Sidebar: after triggering scan, auto-switch to History tab
- ScanHistoryPanel: auto-refresh every 3s while any run is 'running';
show error toast when a run transitions running→error; show spinner
on running runs; error message fully visible (no truncation)
- scripts/run_scan.py: standalone scan script to run with sudo for
nmap OS detection / SYN scans on macOS