From a56204a5f2726a7446a9b2e6d4533c046ed38738 Mon Sep 17 00:00:00 2001 From: Pouzor Date: Mon, 9 Mar 2026 11:26:49 +0100 Subject: [PATCH] refactor: replace config.yml with .env for all configuration 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 --- .gitignore | 3 -- README.md | 78 ++++++++++----------------------- backend/.env.example | 12 ++++- backend/app/api/routes/auth.py | 13 +----- backend/app/api/routes/scan.py | 34 ++++---------- backend/app/core/config.py | 38 +++++++++++++++- backend/app/core/scheduler.py | 15 +------ backend/app/main.py | 1 + backend/config.yml.example | 9 ---- backend/tests/conftest.py | 18 ++------ backend/tests/test_auth.py | 15 ++----- backend/tests/test_canvas.py | 6 +-- backend/tests/test_edges.py | 7 +-- backend/tests/test_nodes.py | 7 +-- backend/tests/test_scan.py | 3 +- backend/tests/test_scheduler.py | 60 ++++++------------------- docker-compose.yml | 6 ++- scripts/lxc-install.sh | 4 +- 18 files changed, 117 insertions(+), 212 deletions(-) delete mode 100644 backend/config.yml.example diff --git a/.gitignore b/.gitignore index 4b69641..3856de0 100644 --- a/.gitignore +++ b/.gitignore @@ -41,9 +41,6 @@ build/ htmlcov/ .coverage -# App config — contains credentials, never commit -backend/config.yml - # SQLite *.db *.db-shm diff --git a/README.md b/README.md index c493306..8753b68 100644 --- a/README.md +++ b/README.md @@ -1,32 +1,18 @@ # Homelable -A self-hosted, open-source tool to visually map, document and monitor your homelab infrastructure. +Homelable is a self-hosted infrastructure visualization solution. It provides a network scanning feature to accelerate the identification of machines and services deployed on your local infrastructure. -Interactive network canvas where each node is a physical machine, VM, LXC container, switch, or device. Nodes show live status, IPs, hostnames, and running services. Edges represent network links. +Homelable also offers a healthcheck system (WIP) through multiple methods (ping/TCP, /health API, etc.) to get a global overview of online/offline services. ---- - -## Features - -- **Interactive canvas** — drag, zoom, pan, snap-to-grid (React Flow) -- **11 node types** — ISP, router, switch, server, Proxmox, VM, LXC, NAS, IoT, AP, generic -- **Proxmox nested nodes** — VM/LXC rendered inside a resizable Proxmox group container -- **5 edge types** — ethernet, Wi-Fi, IoT, VLAN (color-coded), virtual -- **Live status** — per-node checks via ping / HTTP / HTTPS / SSH / TCP / Prometheus -- **Network scanner** — nmap-based discovery, approve/hide/ignore new devices -- **Auto-layout** — one-click Dagre hierarchical arrangement -- **Export** — download canvas as PNG -- **Dark theme** — neon accent colors, JetBrains Mono for technical values -- **Self-contained** — SQLite database, single config file, no cloud dependency --- ## Quick Start — Docker ```bash -git clone https://github.com/you/homelable.git +git clone https://github.com/Pouzor/homelable.git cd homelable - +cp .env.example .env docker compose up -d ``` @@ -38,34 +24,12 @@ Open **http://localhost:3000** — login with `admin` / `admin`. --- -## Quick Start — Development - -**Backend (Python 3.13):** -```bash -cd backend -python3.13 -m venv .venv && source .venv/bin/activate -pip install -r requirements.txt -cp .env.example .env # edit SECRET_KEY -uvicorn app.main:app --reload --port 8000 -``` - -**Frontend:** -```bash -cd frontend -npm install -npm run dev # http://localhost:5173 -``` - -Default login: `admin` / `admin` - ---- - ## Proxmox LXC Install Run inside a Debian/Ubuntu LXC container: ```bash -bash <(curl -fsSL https://raw.githubusercontent.com/you/homelable/main/scripts/lxc-install.sh) +bash <(curl -fsSL https://raw.githubusercontent.com/Pouzor/homelable/main/scripts/lxc-install.sh) ``` This installs the backend as a systemd service and serves the frontend via nginx. @@ -158,19 +122,25 @@ Proxmox nodes render as a resizable group container. VM and LXC nodes can be pla --- -## Development +## Development Mode + +**Backend (Python 3.13):** ```bash -# Backend tests -cd backend && source .venv/bin/activate -pytest # 40 tests - -# Backend lint -ruff check . - -# Frontend tests -cd frontend && npm test - -# Frontend lint + typecheck -npm run lint && npm run typecheck +cd backend +python3.13 -m venv .venv && source .venv/bin/activate +pip install -r requirements.txt +cp .env.example .env # edit SECRET_KEY +uvicorn app.main:app --reload --port 8000 ``` + +**Frontend:** +```bash +cd frontend +npm install +npm run dev # http://localhost:5173 +``` + +Default login: `admin` / `admin` + +--- diff --git a/backend/.env.example b/backend/.env.example index 14c8692..d84e650 100644 --- a/backend/.env.example +++ b/backend/.env.example @@ -1,4 +1,14 @@ SECRET_KEY= SQLITE_PATH=./data/homelab.db -CONFIG_PATH=./config.yml CORS_ORIGINS=["http://localhost:5173","http://localhost:3000"] + +# Auth — set a strong password hash (generate with passlib): +# python3 -c "from passlib.context import CryptContext; print(CryptContext(schemes=['bcrypt']).hash('yourpassword'))" +AUTH_USERNAME=admin +AUTH_PASSWORD_HASH= + +# Scanner — JSON array of CIDR ranges to scan +SCANNER_RANGES=["192.168.1.0/24"] + +# Status checker interval in seconds +STATUS_CHECKER_INTERVAL=60 diff --git a/backend/app/api/routes/auth.py b/backend/app/api/routes/auth.py index b815847..adead57 100644 --- a/backend/app/api/routes/auth.py +++ b/backend/app/api/routes/auth.py @@ -1,6 +1,5 @@ import hmac -import yaml from fastapi import APIRouter, HTTPException, status from pydantic import BaseModel @@ -20,20 +19,12 @@ class TokenResponse(BaseModel): token_type: str = "bearer" -def _load_credentials() -> tuple[str, str]: - with open(settings.config_path) as f: - cfg = yaml.safe_load(f) - auth = cfg.get("auth", {}) - return auth.get("username", "admin"), auth.get("password_hash", "") - - @router.post("/login", response_model=TokenResponse) async def login(body: LoginRequest) -> TokenResponse: - username, password_hash = _load_credentials() # Always run both checks to prevent timing-based username enumeration. # hmac.compare_digest is constant-time; verify_password (bcrypt) always runs. - username_ok = hmac.compare_digest(body.username, username) - password_ok = verify_password(body.password, password_hash) + username_ok = hmac.compare_digest(body.username, settings.auth_username) + password_ok = verify_password(body.password, settings.auth_password_hash) if not username_ok or not password_ok: raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid credentials") token = create_access_token(body.username) diff --git a/backend/app/api/routes/scan.py b/backend/app/api/routes/scan.py index dd1aec4..4118999 100644 --- a/backend/app/api/routes/scan.py +++ b/backend/app/api/routes/scan.py @@ -1,7 +1,6 @@ import logging from typing import Any -import yaml from fastapi import APIRouter, BackgroundTasks, Depends, HTTPException from pydantic import BaseModel from sqlalchemy import select @@ -20,19 +19,11 @@ class ScanConfig(BaseModel): ranges: list[str] interval_seconds: int + logger = logging.getLogger(__name__) router = APIRouter() -def _load_ranges() -> list[str]: - try: - with open(settings.config_path) as f: - cfg: dict[str, Any] = yaml.safe_load(f) or {} - return list(cfg.get("scanner", {}).get("ranges", [])) - except Exception: - return [] - - async def _background_scan(run_id: str, ranges: list[str]) -> None: async with AsyncSessionLocal() as db: await run_scan(ranges, db, run_id) @@ -44,7 +35,7 @@ async def trigger_scan( db: AsyncSession = Depends(get_db), _: str = Depends(get_current_user), ) -> ScanRun: - ranges = _load_ranges() + ranges = settings.scanner_ranges run = ScanRun(status="running", ranges=ranges) db.add(run) await db.commit() @@ -112,25 +103,18 @@ async def list_runs(db: AsyncSession = Depends(get_db), _: str = Depends(get_cur @router.get("/config", response_model=ScanConfig) async def get_scan_config(_: str = Depends(get_current_user)) -> ScanConfig: - try: - with open(settings.config_path) as f: - cfg = yaml.safe_load(f) - ranges = cfg.get("scanner", {}).get("ranges", []) - interval = int(cfg.get("status_checker", {}).get("interval_seconds", 60)) - return ScanConfig(ranges=ranges, interval_seconds=interval) - except Exception as exc: - raise HTTPException(status_code=500, detail=str(exc)) from exc + return ScanConfig( + ranges=settings.scanner_ranges, + interval_seconds=settings.status_checker_interval, + ) @router.post("/config", response_model=ScanConfig) async def update_scan_config(payload: ScanConfig, _: str = Depends(get_current_user)) -> ScanConfig: try: - with open(settings.config_path) as f: - cfg = yaml.safe_load(f) or {} - cfg.setdefault("scanner", {})["ranges"] = payload.ranges - cfg.setdefault("status_checker", {})["interval_seconds"] = payload.interval_seconds - with open(settings.config_path, "w") as f: - yaml.dump(cfg, f, default_flow_style=False, allow_unicode=True) + settings.scanner_ranges = payload.ranges + settings.status_checker_interval = payload.interval_seconds + settings.save_overrides() return payload except Exception as exc: raise HTTPException(status_code=500, detail=str(exc)) from exc diff --git a/backend/app/core/config.py b/backend/app/core/config.py index 34af9a2..cf8cf0c 100644 --- a/backend/app/core/config.py +++ b/backend/app/core/config.py @@ -1,3 +1,6 @@ +import json +from pathlib import Path + from pydantic_settings import BaseSettings, SettingsConfigDict @@ -6,12 +9,43 @@ class Settings(BaseSettings): secret_key: str # Required — set SECRET_KEY in .env sqlite_path: str = "./data/homelab.db" - config_path: str = "./config.yml" cors_origins: list[str] = ["http://localhost:5173", "http://localhost:3000"] # JWT algorithm: str = "HS256" access_token_expire_minutes: int = 1440 # 24h + # Auth — set AUTH_USERNAME and AUTH_PASSWORD_HASH in .env + auth_username: str = "admin" + auth_password_hash: str = "" -settings = Settings() # type: ignore[call-arg] # pydantic-settings loads secret_key from env + # Scanner + scanner_ranges: list[str] = ["192.168.1.0/24"] + + # Status checker + status_checker_interval: int = 60 + + def _override_path(self) -> Path: + return Path(self.sqlite_path).parent / "scan_config.json" + + def load_overrides(self) -> None: + """Load runtime scan config overrides written by the API.""" + try: + data = json.loads(self._override_path().read_text()) + if "scanner_ranges" in data: + self.scanner_ranges = data["scanner_ranges"] + if "status_checker_interval" in data: + self.status_checker_interval = int(data["status_checker_interval"]) + except Exception: + pass + + def save_overrides(self) -> None: + """Persist scan config so it survives container restarts.""" + self._override_path().parent.mkdir(parents=True, exist_ok=True) + self._override_path().write_text(json.dumps({ + "scanner_ranges": self.scanner_ranges, + "status_checker_interval": self.status_checker_interval, + })) + + +settings = Settings() # type: ignore[call-arg] diff --git a/backend/app/core/scheduler.py b/backend/app/core/scheduler.py index 630c12b..40b41c4 100644 --- a/backend/app/core/scheduler.py +++ b/backend/app/core/scheduler.py @@ -2,7 +2,6 @@ import logging from datetime import UTC, datetime -import yaml from apscheduler.schedulers.asyncio import AsyncIOScheduler from sqlalchemy import select @@ -46,22 +45,12 @@ async def _run_status_checks() -> None: logger.error("Status check failed for node %s: %s", node.id, exc) -def _load_interval() -> int: - try: - with open(settings.config_path) as f: - cfg = yaml.safe_load(f) - return int(cfg.get("status_checker", {}).get("interval_seconds", 60)) - except Exception: - return 60 - - def start_scheduler() -> None: global scheduler scheduler = AsyncIOScheduler() - interval = _load_interval() - scheduler.add_job(_run_status_checks, "interval", seconds=interval, id="status_checks") + scheduler.add_job(_run_status_checks, "interval", seconds=settings.status_checker_interval, id="status_checks") scheduler.start() - logger.info("Scheduler started — status checks every %ds", interval) + logger.info("Scheduler started — status checks every %ds", settings.status_checker_interval) def stop_scheduler() -> None: diff --git a/backend/app/main.py b/backend/app/main.py index f01819e..8b02287 100644 --- a/backend/app/main.py +++ b/backend/app/main.py @@ -14,6 +14,7 @@ from app.db.database import init_db @asynccontextmanager async def lifespan(app: FastAPI) -> AsyncGenerator[None, None]: await init_db() + settings.load_overrides() start_scheduler() yield stop_scheduler() diff --git a/backend/config.yml.example b/backend/config.yml.example deleted file mode 100644 index 25594a5..0000000 --- a/backend/config.yml.example +++ /dev/null @@ -1,9 +0,0 @@ -auth: - username: admin - password_hash: "" # Generate with: python -c "from passlib.context import CryptContext; print(CryptContext(schemes=['bcrypt']).hash('yourpassword'))" -scanner: - interval: null - ranges: - - 192.168.1.0/24 -status_checker: - interval_seconds: 60 diff --git a/backend/tests/conftest.py b/backend/tests/conftest.py index ecfa01a..de3d6dd 100644 --- a/backend/tests/conftest.py +++ b/backend/tests/conftest.py @@ -4,7 +4,6 @@ import os os.environ.setdefault("SECRET_KEY", "test-only-secret-key-not-for-production") import pytest -import yaml from httpx import ASGITransport, AsyncClient from passlib.context import CryptContext from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker, create_async_engine @@ -18,20 +17,11 @@ _pwd_ctx = CryptContext(schemes=["bcrypt"], deprecated="auto") @pytest.fixture(autouse=True, scope="session") -def test_config_file(tmp_path_factory): - """Write a minimal config.yml for the test session and point settings at it.""" - cfg = { - "auth": { - "username": "admin", - "password_hash": _pwd_ctx.hash("admin"), - }, - "scanner": {"ranges": []}, - "status_checker": {"interval_seconds": 3600}, - } - cfg_path = tmp_path_factory.mktemp("cfg") / "config.yml" - cfg_path.write_text(yaml.dump(cfg)) +def test_credentials(): + """Configure test auth credentials directly on settings.""" from app.core.config import settings - settings.config_path = str(cfg_path) + settings.auth_username = "admin" + settings.auth_password_hash = _pwd_ctx.hash("admin") @pytest.fixture diff --git a/backend/tests/test_auth.py b/backend/tests/test_auth.py index 4dca511..95422eb 100644 --- a/backend/tests/test_auth.py +++ b/backend/tests/test_auth.py @@ -1,16 +1,7 @@ -from unittest.mock import patch - -import pytest from httpx import AsyncClient -@pytest.fixture -def mock_credentials(): - with patch("app.api.routes.auth._load_credentials", return_value=("admin", "$2b$12$o/LWyvmBc978CNpSsHxcveXN0WqjAGW/gBR0.U.HURWbaYD3GCDqS")): - yield - - -async def test_login_success(client: AsyncClient, mock_credentials): +async def test_login_success(client: AsyncClient): res = await client.post("/api/v1/auth/login", json={"username": "admin", "password": "admin"}) assert res.status_code == 200 data = res.json() @@ -18,12 +9,12 @@ async def test_login_success(client: AsyncClient, mock_credentials): assert data["token_type"] == "bearer" -async def test_login_wrong_password(client: AsyncClient, mock_credentials): +async def test_login_wrong_password(client: AsyncClient): res = await client.post("/api/v1/auth/login", json={"username": "admin", "password": "wrong"}) assert res.status_code == 401 -async def test_login_wrong_username(client: AsyncClient, mock_credentials): +async def test_login_wrong_username(client: AsyncClient): res = await client.post("/api/v1/auth/login", json={"username": "notadmin", "password": "admin"}) assert res.status_code == 401 diff --git a/backend/tests/test_canvas.py b/backend/tests/test_canvas.py index d943ee4..a14bc78 100644 --- a/backend/tests/test_canvas.py +++ b/backend/tests/test_canvas.py @@ -1,16 +1,12 @@ import uuid -from unittest.mock import patch import pytest from httpx import AsyncClient -TOKEN_HASH = "$2b$12$o/LWyvmBc978CNpSsHxcveXN0WqjAGW/gBR0.U.HURWbaYD3GCDqS" - @pytest.fixture async def headers(client: AsyncClient): - with patch("app.api.routes.auth._load_credentials", return_value=("admin", TOKEN_HASH)): - res = await client.post("/api/v1/auth/login", json={"username": "admin", "password": "admin"}) + res = await client.post("/api/v1/auth/login", json={"username": "admin", "password": "admin"}) return {"Authorization": f"Bearer {res.json()['access_token']}"} diff --git a/backend/tests/test_edges.py b/backend/tests/test_edges.py index 78b7eb8..3665650 100644 --- a/backend/tests/test_edges.py +++ b/backend/tests/test_edges.py @@ -1,15 +1,10 @@ -from unittest.mock import patch - import pytest from httpx import AsyncClient -TOKEN_HASH = "$2b$12$o/LWyvmBc978CNpSsHxcveXN0WqjAGW/gBR0.U.HURWbaYD3GCDqS" - @pytest.fixture async def headers(client: AsyncClient): - with patch("app.api.routes.auth._load_credentials", return_value=("admin", TOKEN_HASH)): - res = await client.post("/api/v1/auth/login", json={"username": "admin", "password": "admin"}) + res = await client.post("/api/v1/auth/login", json={"username": "admin", "password": "admin"}) token = res.json()["access_token"] return {"Authorization": f"Bearer {token}"} diff --git a/backend/tests/test_nodes.py b/backend/tests/test_nodes.py index e0b81da..eafff2c 100644 --- a/backend/tests/test_nodes.py +++ b/backend/tests/test_nodes.py @@ -1,15 +1,10 @@ -from unittest.mock import patch - import pytest from httpx import AsyncClient -TOKEN_HASH = "$2b$12$o/LWyvmBc978CNpSsHxcveXN0WqjAGW/gBR0.U.HURWbaYD3GCDqS" - @pytest.fixture async def headers(client: AsyncClient): - with patch("app.api.routes.auth._load_credentials", return_value=("admin", TOKEN_HASH)): - res = await client.post("/api/v1/auth/login", json={"username": "admin", "password": "admin"}) + res = await client.post("/api/v1/auth/login", json={"username": "admin", "password": "admin"}) token = res.json()["access_token"] return {"Authorization": f"Bearer {token}"} diff --git a/backend/tests/test_scan.py b/backend/tests/test_scan.py index 37c8708..f355f2a 100644 --- a/backend/tests/test_scan.py +++ b/backend/tests/test_scan.py @@ -49,8 +49,9 @@ async def test_trigger_scan_requires_auth(client: AsyncClient): async def test_trigger_scan_creates_run(client: AsyncClient, headers): with ( patch("app.api.routes.scan._background_scan", new_callable=AsyncMock), - patch("app.api.routes.scan._load_ranges", return_value=["192.168.1.0/24"]), + patch("app.api.routes.scan.settings") as mock_settings, ): + mock_settings.scanner_ranges = ["192.168.1.0/24"] res = await client.post("/api/v1/scan/trigger", headers=headers) assert res.status_code == 200 data = res.json() diff --git a/backend/tests/test_scheduler.py b/backend/tests/test_scheduler.py index fb4457a..e688fd6 100644 --- a/backend/tests/test_scheduler.py +++ b/backend/tests/test_scheduler.py @@ -1,11 +1,11 @@ -"""Tests for background scheduler: _load_interval, _run_status_checks, lifecycle.""" +"""Tests for background scheduler: _run_status_checks, lifecycle.""" import uuid from unittest.mock import AsyncMock, MagicMock, patch import pytest from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker, create_async_engine -from app.core.scheduler import _load_interval, _run_status_checks, start_scheduler, stop_scheduler +from app.core.scheduler import _run_status_checks, start_scheduler, stop_scheduler from app.db.database import Base from app.db.models import Node @@ -25,35 +25,6 @@ def _make_node(**kwargs) -> Node: return Node(**{**defaults, **kwargs}) -# --------------------------------------------------------------------------- -# _load_interval -# --------------------------------------------------------------------------- - -def test_load_interval_reads_from_config(tmp_path): - """_load_interval returns the value set in config.yml.""" - cfg = tmp_path / "config.yml" - cfg.write_text("status_checker:\n interval_seconds: 30\n") - with patch("app.core.scheduler.settings") as mock_settings: - mock_settings.config_path = str(cfg) - assert _load_interval() == 30 - - -def test_load_interval_defaults_to_60_when_key_missing(tmp_path): - """_load_interval returns 60 when status_checker section is absent.""" - cfg = tmp_path / "config.yml" - cfg.write_text("auth:\n username: admin\n") - with patch("app.core.scheduler.settings") as mock_settings: - mock_settings.config_path = str(cfg) - assert _load_interval() == 60 - - -def test_load_interval_defaults_to_60_on_missing_file(): - """_load_interval returns 60 when config file does not exist.""" - with patch("app.core.scheduler.settings") as mock_settings: - mock_settings.config_path = "/nonexistent/path/config.yml" - assert _load_interval() == 60 - - # --------------------------------------------------------------------------- # _run_status_checks # --------------------------------------------------------------------------- @@ -164,26 +135,23 @@ async def test_run_status_checks_handles_check_error_gracefully(mem_db): # start_scheduler / stop_scheduler # --------------------------------------------------------------------------- +def test_scheduler_uses_settings_interval(): + """Scheduler registers the job with the interval from settings.""" + mock_sched = MagicMock() + with patch("app.core.scheduler.settings") as mock_settings, \ + patch("app.core.scheduler.AsyncIOScheduler", return_value=mock_sched): + mock_settings.status_checker_interval = 45 + start_scheduler() + _, kwargs = mock_sched.add_job.call_args + assert kwargs["seconds"] == 45 + + def test_start_and_stop_scheduler(): """Scheduler can be started and stopped without errors.""" mock_sched = MagicMock() - with patch("app.core.scheduler._load_interval", return_value=3600), \ - patch("app.core.scheduler.AsyncIOScheduler", return_value=mock_sched): + with patch("app.core.scheduler.AsyncIOScheduler", return_value=mock_sched): start_scheduler() stop_scheduler() mock_sched.add_job.assert_called_once() mock_sched.start.assert_called_once() mock_sched.shutdown.assert_called_once() - - -def test_start_scheduler_uses_configured_interval(): - """Scheduler registers the status_checks job with the correct interval.""" - mock_sched = MagicMock() - with patch("app.core.scheduler._load_interval", return_value=120) as mock_interval, \ - patch("app.core.scheduler.AsyncIOScheduler", return_value=mock_sched): - start_scheduler() - mock_interval.assert_called_once() - mock_sched.add_job.assert_called_once() - _, kwargs = mock_sched.add_job.call_args - assert kwargs.get("seconds") == 120 - mock_sched.start.assert_called_once() diff --git a/docker-compose.yml b/docker-compose.yml index 22598c8..ba9efce 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -7,11 +7,13 @@ services: environment: SECRET_KEY: ${SECRET_KEY:?SECRET_KEY must be set in the environment or a .env file} SQLITE_PATH: /app/data/homelab.db - CONFIG_PATH: /app/config.yml CORS_ORIGINS: '["http://localhost:3000"]' + AUTH_USERNAME: ${AUTH_USERNAME:-admin} + AUTH_PASSWORD_HASH: ${AUTH_PASSWORD_HASH:?AUTH_PASSWORD_HASH must be set} + SCANNER_RANGES: ${SCANNER_RANGES:-["192.168.1.0/24"]} + STATUS_CHECKER_INTERVAL: ${STATUS_CHECKER_INTERVAL:-60} volumes: - backend_data:/app/data - - ./backend/config.yml:/app/config.yml networks: - homelable # Required for ping-based status checks diff --git a/scripts/lxc-install.sh b/scripts/lxc-install.sh index 39277d2..eb33a1e 100755 --- a/scripts/lxc-install.sh +++ b/scripts/lxc-install.sh @@ -1,7 +1,7 @@ #!/usr/bin/env bash # Homelable — LXC/VM bootstrap installer # Compatible with Proxmox VE (Debian/Ubuntu LXC containers) -# Usage: bash <(curl -fsSL https://raw.githubusercontent.com/you/homelable/main/scripts/lxc-install.sh) +# Usage: bash <(curl -fsSL https://raw.githubusercontent.com/Pouzor/homelable/main/scripts/lxc-install.sh) set -euo pipefail @@ -48,7 +48,7 @@ if ! id "$SERVICE_USER" &>/dev/null; then fi # ── Clone / update repo ─────────────────────────────────────────────────────── -REPO_URL="https://github.com/you/homelable.git" # ← update before publishing +REPO_URL="https://github.com/Pouzor/homelable.git" if [[ -d "$INSTALL_DIR/.git" ]]; then info "Updating existing installation..." git -C "$INSTALL_DIR" pull