diff --git a/lubelogger/README.md b/lubelogger/README.md new file mode 100644 index 0000000..c2d0b27 --- /dev/null +++ b/lubelogger/README.md @@ -0,0 +1,50 @@ +# LubeLogger — deployment source of truth + +Vehicle & small-engine maintenance tracker. **Host:** docker-node01 (.186). +Postgres 18 backend (tables in the `app` schema). In-app auth ENABLED (root user). + +## Routing (file provider) +Published on the host at `:8099`; Traefik on node01 routes `lubelogger.goattw.net` +→ `http://192.168.99.186:8099` via `dynamic_conf.yml` (see `traefik-router.dynamic_conf.yml`). +Edge chain: TLS (letsencrypt) → CrowdSec (global) → Authelia (`authelia-auth@file`, +`two_factor` off-LAN / bypass on-LAN). LubeLogger's own auth gates the LAN/`:8099` path. + +## Deploy (Ansible, idempotent) +Playbook `deploy-lubelogger.yml` (runs from `~/ansible` on ansible-control): +``` +ansible-playbook playbooks/deploy-lubelogger.yml +``` +Inline-compose house style: `copy:` the compose + `community.docker.docker_compose_v2`. +DB password is sourced from ansible-vault (`vault_lubelogger_db_password`) — never inline. +Second converge is `changed=0`. + +## Backup (PBS) +`lubelogger-backup.{sh,service,timer}` on node01 — daily 03:30, `pg_dump` → PBS +(`rust-usb`, host/lubelogger), prune `--keep-daily 14`. PBS creds live in +`/etc/lubelogger-backup.env` (root:600, NOT in git). Restore verified via full +PBS round-trip into a throwaway postgres:18 (live↔restored row-count parity). +NOTE: the `lubelogger-data`/`lubelogger-keys` volumes (uploads, config, the ROOT USER +credential, ASP.NET keyring) are a SEPARATE layer — covered by docker-volume-backup +(`stop-during-backup` labels already set). pg_dump captures Postgres data only. + +## Reminders → ntfy +`lubelogger-reminders-probe.{sh}` + units on ansible-control (.190) — daily 07:30. +Reads `/api/vehicle/info` (Basic auth, creds in `/etc/lubelogger-reminders.env`, +root:600, NOT in git), posts a digest to ntfy topic **garage** only when something is +due/overdue. Subscribe to the `garage` topic in your ntfy app to receive it. + +## Diun +App carries `diun.watch_repo=true` + semver `include_tags` so a new release tag pings +the existing Diun→HA-webhook pipeline. DB (`postgres:18`) is watched by default (digest). + +## Homepage +Tile + `lubelogger` widget under "AI & Productivity" in +`observability/homepage/services.yaml` (live) — widget password via +`HOMEPAGE_VAR_LUBELOGGER_PASSWORD` (observability/.env, not a git repo). See +`homepage-tile.yaml`. + +## Secrets (none in this repo) +- DB password: ansible-vault (`~/ansible`), and Postgres volume (already initialized). +- PBS token: `/etc/lubelogger-backup.env` (reuses vaultwarden token — a dedicated + `lubelogger-backup` PBS token would be cleaner; needs PBS root to create). +- Admin/basic-auth password: `/etc/lubelogger-reminders.env` + `observability/.env`. diff --git a/lubelogger/deploy-lubelogger.yml b/lubelogger/deploy-lubelogger.yml new file mode 100644 index 0000000..b71928a --- /dev/null +++ b/lubelogger/deploy-lubelogger.yml @@ -0,0 +1,104 @@ +--- +- name: Deploy LubeLogger on docker-node01 + hosts: docker-node01 + become: true + vars: + app_dir: /home/tommy/lubelogger + app_port: 8099 + + tasks: + - name: Create lubelogger directory + ansible.builtin.file: + path: "{{ app_dir }}" + state: directory + owner: tommy + group: tommy + mode: "0755" + + - name: Write docker-compose.yml + ansible.builtin.copy: + dest: "{{ app_dir }}/docker-compose.yml" + owner: tommy + group: tommy + mode: "0644" + content: | + # lubelogger - Vehicle & small-engine maintenance tracker + # Host: docker-node01 (.186), Port: {{ app_port }} + # Traefik: lubelogger.goattw.net (Authelia protected, @file router -> :{{ app_port }}) + # Backend: PostgreSQL 18 (tables live in the `app` schema). + # In-app auth is ENABLED (root user) - the published host port is safe. + # Do NOT lose the lubelogger-keys volume (ASP.NET DataProtection keyring). + + services: + lubelogger: + image: ghcr.io/hargata/lubelogger:v1.6.8 + container_name: lubelogger + restart: unless-stopped + depends_on: + - lubelogger-db + ports: + - "{{ app_port }}:8080" + environment: + TZ: America/Chicago + LUBELOGGER_LOCALE_OVERRIDE: "en_US" + POSTGRES_CONNECTION: "Host=lubelogger-db:5432;Username=lubelogger;Password={{ vault_lubelogger_db_password }};Database=lubelogger;" + volumes: + - lubelogger-data:/App/data + - lubelogger-keys:/root/.aspnet/DataProtection-Keys + networks: + - lubelogger-int + labels: + - "docker-volume-backup.stop-during-backup=true" + # Diun watches by default (digest); watch_repo + semver filter + # so a NEW release tag (e.g. v1.6.9) pings the existing Diun->HA pipeline. + - "diun.enable=true" + - "diun.watch_repo=true" + - 'diun.include_tags=^v[0-9]+\.[0-9]+\.[0-9]+$' + + lubelogger-db: + image: postgres:18 + container_name: lubelogger-db + restart: unless-stopped + environment: + POSTGRES_USER: lubelogger + POSTGRES_PASSWORD: "{{ vault_lubelogger_db_password }}" + POSTGRES_DB: lubelogger + TZ: America/Chicago + volumes: + - lubelogger-db:/var/lib/postgresql + - /etc/localtime:/etc/localtime:ro + networks: + - lubelogger-int + healthcheck: + test: ["CMD-SHELL", "pg_isready -U lubelogger -d lubelogger"] + interval: 10s + timeout: 5s + retries: 5 + start_period: 30s + labels: + - "docker-volume-backup.stop-during-backup=true" + + networks: + lubelogger-int: + driver: bridge + + volumes: + lubelogger-data: + lubelogger-keys: + lubelogger-db: + + - name: Remove stale .env (secret now sourced from ansible-vault) + ansible.builtin.file: + path: "{{ app_dir }}/.env" + state: absent + + - name: Deploy lubelogger with docker compose + community.docker.docker_compose_v2: + project_src: "{{ app_dir }}" + state: present + pull: missing + register: app_result + + - name: Show deployment result + ansible.builtin.debug: + msg: "lubelogger is {{ 'running (changed)' if app_result.changed else 'already up (no-op)' }} on port {{ app_port }}" diff --git a/lubelogger/homepage-tile.yaml b/lubelogger/homepage-tile.yaml new file mode 100644 index 0000000..ef0a2bd --- /dev/null +++ b/lubelogger/homepage-tile.yaml @@ -0,0 +1,10 @@ + - LubeLogger: + icon: lubelogger.png + href: https://lubelogger.goattw.net + description: Vehicle & small-engine maintenance + siteMonitor: https://lubelogger.goattw.net + widget: + type: lubelogger + url: http://192.168.99.186:8099 + username: tommy + password: {{HOMEPAGE_VAR_LUBELOGGER_PASSWORD}} diff --git a/lubelogger/lubelogger-backup.service b/lubelogger/lubelogger-backup.service new file mode 100644 index 0000000..99c254d --- /dev/null +++ b/lubelogger/lubelogger-backup.service @@ -0,0 +1,8 @@ +[Unit] +Description=LubeLogger Postgres dump -> PBS +After=docker.service +Requires=docker.service + +[Service] +Type=oneshot +ExecStart=/usr/local/bin/lubelogger-backup.sh diff --git a/lubelogger/lubelogger-backup.sh b/lubelogger/lubelogger-backup.sh new file mode 100644 index 0000000..25bd6ad --- /dev/null +++ b/lubelogger/lubelogger-backup.sh @@ -0,0 +1,24 @@ +#!/bin/bash +# LubeLogger Postgres logical backup -> Proxmox Backup Server. +# Installed at /usr/local/bin/lubelogger-backup.sh ; driven by lubelogger-backup.timer. +set -euo pipefail +source /etc/lubelogger-backup.env +export PBS_PASSWORD PBS_FINGERPRINT + +TMPDIR=$(mktemp -d) +trap 'rm -rf "$TMPDIR"' EXIT + +# Consistent logical dump (includes the `app` schema + data). App stays online. +docker exec lubelogger-db pg_dump -U lubelogger lubelogger > "$TMPDIR/lubelogger.sql" + +# Push to PBS +proxmox-backup-client backup \ + "lubelogger-db.pxar:$TMPDIR" \ + --repository "$PBS_REPOSITORY" \ + --backup-type host \ + --backup-id lubelogger + +# Retain ~14 daily snapshots +proxmox-backup-client prune "host/lubelogger" \ + --repository "$PBS_REPOSITORY" \ + --keep-daily 14 diff --git a/lubelogger/lubelogger-backup.timer b/lubelogger/lubelogger-backup.timer new file mode 100644 index 0000000..4a97dcf --- /dev/null +++ b/lubelogger/lubelogger-backup.timer @@ -0,0 +1,9 @@ +[Unit] +Description=Daily LubeLogger Postgres backup to PBS + +[Timer] +OnCalendar=*-*-* 03:30:00 +Persistent=true + +[Install] +WantedBy=timers.target diff --git a/lubelogger/lubelogger-reminders-probe.sh b/lubelogger/lubelogger-reminders-probe.sh new file mode 100644 index 0000000..4427a68 --- /dev/null +++ b/lubelogger/lubelogger-reminders-probe.sh @@ -0,0 +1,35 @@ +#!/usr/bin/env bash +set -euo pipefail +# Daily LubeLogger reminder digest -> ntfy 'garage' topic. +# Reads per-vehicle reminder urgency counts from LubeLogger's API and posts a +# summary ONLY when something is due/overdue (PastDue + VeryUrgent + Urgent). +# Runs on ansible-control (.190) via lubelogger-reminders.timer. +# Shape mirrors the gluetun vpn-tunnel-probe (oneshot + timer, set -euo pipefail). + +source /etc/lubelogger-reminders.env # LUBELOGGER_URL LUBELOGGER_USER LUBELOGGER_PASS NTFY_URL + +if ! resp=$(curl -sf --max-time 20 -u "${LUBELOGGER_USER}:${LUBELOGGER_PASS}" "${LUBELOGGER_URL}/api/vehicle/info"); then + logger -t lubelogger-reminders "ERROR: LubeLogger API query failed" + curl -sf --max-time 15 -H "Priority: high" -H "Tags: warning" \ + -H "Title: LubeLogger reminder probe failed" \ + -d "Could not query ${LUBELOGGER_URL}/api/vehicle/info" "$NTFY_URL" || true + exit 1 +fi + +total=$(printf '%s' "$resp" | jq '[.[] | (.PastDueReminderCount + .VeryUrgentReminderCount + .UrgentReminderCount)] | add // 0') + +if [ "${total:-0}" -gt 0 ]; then + body=$(printf '%s' "$resp" | jq -r ' + .[] + | (.PastDueReminderCount + .VeryUrgentReminderCount + .UrgentReminderCount) as $due + | select($due > 0) + | "• \(.VehicleData.Year) \(.VehicleData.Make) \(.VehicleData.Model): " + + "\(.PastDueReminderCount) past-due, \(.VeryUrgentReminderCount) very-urgent, \(.UrgentReminderCount) urgent"') + curl -sf --max-time 15 \ + -H "Title: Vehicle maintenance due (${total})" \ + -H "Tags: wrench" -H "Priority: default" \ + -d "$body" "$NTFY_URL" || true + logger -t lubelogger-reminders "posted ${total} due reminder(s) to ntfy garage" +else + logger -t lubelogger-reminders "no due/overdue reminders" +fi diff --git a/lubelogger/lubelogger-reminders.service b/lubelogger/lubelogger-reminders.service new file mode 100644 index 0000000..a082da3 --- /dev/null +++ b/lubelogger/lubelogger-reminders.service @@ -0,0 +1,8 @@ +[Unit] +Description=LubeLogger reminder digest -> ntfy garage +Wants=network-online.target +After=network-online.target + +[Service] +Type=oneshot +ExecStart=/home/tommy/ansible/scripts/lubelogger-reminders-probe.sh diff --git a/lubelogger/lubelogger-reminders.timer b/lubelogger/lubelogger-reminders.timer new file mode 100644 index 0000000..943e80b --- /dev/null +++ b/lubelogger/lubelogger-reminders.timer @@ -0,0 +1,9 @@ +[Unit] +Description=Daily LubeLogger reminder digest + +[Timer] +OnCalendar=*-*-* 07:30:00 +Persistent=true + +[Install] +WantedBy=timers.target diff --git a/lubelogger/traefik-router.dynamic_conf.yml b/lubelogger/traefik-router.dynamic_conf.yml new file mode 100644 index 0000000..0e62d7b --- /dev/null +++ b/lubelogger/traefik-router.dynamic_conf.yml @@ -0,0 +1,21 @@ +# File-provider router for LubeLogger — merged into node01 +# /home/tommy/traefik/dynamic_conf.yml (live) under http.routers and http.services. +# Traefik watch=true hot-reloads on change. (Reference copy; the live file is authoritative.) + +# --- under http.routers: --- + lubelogger: + rule: "Host(`lubelogger.goattw.net`)" + entryPoints: + - websecure + middlewares: + - authelia-auth@file # crowdsec-bouncer@file applies globally at the websecure entrypoint + service: lubelogger-service + tls: + certResolver: letsencrypt + +# --- under http.services: --- + lubelogger-service: + loadBalancer: + servers: + - url: "http://192.168.99.186:8099" + serversTransport: default-transport