Ctrl K
ComunidadGuías…

Makefile

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
.PHONY: network up up-public down-public logs-public config-public restore-test backup down clean reset dashboard analytics sync full-sync trigger-sync logs-dashboard logs-analytics logs-sync logs-all status migrate db gen-secrets setup add-host setup-user backfill-energy backfill-records tailwind-build test test-env-up test-env-down test-seed test-user test-e2e test-e2e-seeded test-all test-all-seeded test-coverage test-js test-js-coverage secure-env

DC := docker compose --env-file env/.env --env-file env/.env.app
# Public SaaS deployment (bundled Caddy + Let's Encrypt). The overlay decouples
# api from the home gateway's external `proxy` network. See docs/deployment-public.md.
DC_PUBLIC := docker compose --env-file env/.env --env-file env/.env.app -f docker-compose.yml -f deploy/docker-compose.public.yml

# E2E Test-Credentials (lokal, kein echter Account)
TEST_EMAIL  ?= e2e@pulsebase.test
TEST_PASSWORD ?= E2eLocalTest1!  # pragma: allowlist secret

network:
	docker network inspect proxy >/dev/null 2>&1 || docker network create proxy

up: network
	$(DC) up -d --build

# ── Public SaaS instance (bundled Caddy + Let's Encrypt on PUBLIC_DOMAIN) ──────
up-public: ## Öffentliche Instanz starten (eigenes Caddy + Let's Encrypt)
	@test -f env/.env.public || { echo "FEHLER: env/.env.public fehlt — cp env/.env.public.example env/.env.public und ausfüllen"; exit 1; }
	$(DC_PUBLIC) up -d --build

down-public:
	$(DC_PUBLIC) down

logs-public:
	$(DC_PUBLIC) logs -f caddy

config-public: ## Gemergte Public-Config rendern (Sanity-Check: api ohne proxy-Netz)
	$(DC_PUBLIC) config

down:
	$(DC) down

clean:
	$(DC) down -v --remove-orphans

reset:
	$(DC) down --remove-orphans
	docker volume rm garmin-dev_timescale-data garmin-dev_garmin-tokens 2>/dev/null || true
	@echo "=== Volumes geloescht — Datenbank wird neu aufgesetzt ==="
	$(DC) up flyway
	$(DC) up -d --build

dashboard: network
	$(DC) build api && $(DC) up -d api

analytics: network
	$(DC) build ml-service && $(DC) up -d ml-service

sync: network
	$(DC) build sync-service && $(DC) up -d --force-recreate sync-service

full-sync: ## Garmin-Backfill für alle aktiven User (DAYS=730)
	@days="$${DAYS:-730}"; \
	case "$$days" in *[!0-9]*|'') echo "FEHLER: DAYS muss eine positive Ganzzahl sein"; exit 2;; esac; \
	[ "$$days" -gt 0 ] || { echo "FEHLER: DAYS muss größer als 0 sein"; exit 2; }; \
	$(DC) pause sync-service || { echo "FEHLER: sync-service konnte nicht pausiert werden"; exit 1; }; \
	trap '$(DC) unpause sync-service >/dev/null 2>&1 || true' EXIT; \
	trap 'exit 130' INT; \
	trap 'exit 143' TERM; \
	printf '%s\n' \
	  'import asyncio' \
	  'from config import Settings' \
	  'from repositories.timescale import TimescaleRepository' \
	  'from sync_runner import sync_all_users' \
	  '' \
	  'async def run():' \
	  '    settings = Settings()' \
	  '    repo = TimescaleRepository(settings.db_url)' \
	  '    try:' \
	  '        await repo.init()' \
	  "        await sync_all_users(repo, days=$$days, settings=settings)" \
	  '    finally:' \
	  '        await repo.close()' \
	  '' \
	  'asyncio.run(run())' \
	| $(DC) run --rm --no-deps -T -e PYTHONPATH=/app/src sync-service python -

trigger-sync: ## Garmin-Sync für alle aktiven User anfordern (sync-service verarbeitet binnen 1 Minute)
	$(DC) exec -T db psql -U garmin_app -d garmin \
	  -c "WITH requested AS (UPDATE users SET sync_requested = true WHERE garmin_linked = true AND is_active = true RETURNING id) INSERT INTO service_events (event_type, user_id, payload) SELECT 'sync_requested', id, jsonb_build_object('schema_version', 1, 'correlation_id', gen_random_uuid()::text, 'cause', 'operator') FROM requested ON CONFLICT (event_type, user_id) WHERE status IN ('pending', 'processing') DO UPDATE SET generation = service_events.generation + 1, payload = EXCLUDED.payload, available_at = CASE WHEN service_events.status = 'pending' THEN NOW() ELSE service_events.available_at END, attempts = CASE WHEN service_events.status = 'pending' THEN 0 ELSE service_events.attempts END, last_error = CASE WHEN service_events.status = 'pending' THEN NULL ELSE service_events.last_error END;"
	@echo "Sync angefordert — Event-Consumer startet sofort. Fortschritt: make logs-sync"

logs-dashboard:
	$(DC) logs -f api

logs-analytics:
	$(DC) logs -f ml-service

logs-sync:
	$(DC) logs -f sync-service

logs-all:
	$(DC) logs -f

migrate: network
	$(DC) up flyway

backfill-energy:
	$(DC) exec ml-service python /app/src/backfill_energy.py

backfill-records: ## Fetch GPS/HR records from Garmin for all activities without records
	$(DC) exec sync-service python /app/src/backfill_records.py

backfill-battery: ## Force-recompute body_battery_custom with new model (deletes old predictions first)
	$(DC) exec db psql -U garmin_app garmin \
	  -c "DELETE FROM ml_predictions WHERE model = 'body_battery_custom';"
	$(DC) exec ml-service python /app/src/backfill_energy.py

status:
	$(DC) ps

db: network
	if [ -n "$(SQL)" ]; then \
		$(DC) exec -T db psql -U garmin_app -d garmin -c "$(SQL)"; \
	elif [ -t 0 ]; then \
		$(DC) exec db psql -U garmin_app -d garmin; \
	else \
		$(DC) exec -T db psql -U garmin_app -d garmin; \
	fi

gen-secrets:
	@echo "In env/.env.api eintragen:"
	@echo "SESSION_SECRET=$$(openssl rand -hex 32)"
	@echo ""
	@echo "In env/.env.app eintragen (shared — wird von api + sync gelesen):"
	@echo "FERNET_KEY=$$(python3 -c 'import os,base64;print(base64.urlsafe_b64encode(os.urandom(32)).decode())')"
	@echo ""
	@echo "Per-Service-DB-Rollen (V24) — ebenfalls in env/.env.app:"
	@echo "DB_APP_PASSWORD=$$(openssl rand -hex 24)"
	@echo "DB_SYNC_PASSWORD=$$(openssl rand -hex 24)"
	@echo "DB_ML_PASSWORD=$$(openssl rand -hex 24)"
	@echo ""
	@echo "Backup-Verschlüsselung — age-Keypair auf einer VERTRAUENSWÜRDIGEN"
	@echo "Offsite-Maschine erzeugen (NICHT auf dem Server):"
	@echo "  age-keygen -o pulsebase-backup.key"
	@echo "  → 'Public key: age1…' als AGE_RECIPIENT in env/.env.backup"
	@echo "  → Datei pulsebase-backup.key NUR offsite/Passwortmanager (für Restore)"

secure-env:
	chmod 600 env/.env env/.env.app env/.env.api env/.env.sync env/.env.ml
	@test -f env/.env.public && chmod 600 env/.env.public || true
	@test -f env/.env.backup && chmod 600 env/.env.backup || true

# ── Backup sofort ausführen (One-Off; sonst täglich via Backup-Container-Loop) ─
backup:
	$(DC) run --rm backup /scripts/backup.sh

# ── Backup-Restore-Test (Regel "test backups") ───────────────────────────────
# Entschlüsselt den neuesten Backup (age) + restored ihn TimescaleDB-korrekt
# (pre/post_restore, kein -j) in eine Wegwerf-DB und prüft die User-Zahl.
# Key-Pfad kommt aus env/.env.backup (AGE_IDENTITY=); überschreibbar via Variable.
restore-test:
	@key="$${AGE_IDENTITY:-$$(sed -n 's/^AGE_IDENTITY=//p' env/.env.backup 2>/dev/null)}"; \
	test -n "$$key" || { echo "FEHLER: AGE_IDENTITY in env/.env.backup setzen (Pfad zum privaten age-Key)"; exit 1; }; \
	$(DC) run --rm -v "$$key:/age.key:ro" -e AGE_IDENTITY=/age.key backup /scripts/restore.sh latest restore_check; \
	echo "→ Restore-Test OK"

setup:
	@echo "=== Erstmalige Einrichtung ==="
	@echo ""
	@echo "Schritt 1 — Session Secret generieren:"
	@echo "  make gen-secrets"
	@echo "  → Wert in env/.env.api eintragen"
	@echo ""
	@echo "Schritt 2 — Domain konfigurieren:"
	@echo "  env/.env:     HOST_IP=<deine-domain>"
	@echo "  env/.env.api: APP_BASE_URL=https://<deine-domain>"
	@echo "  Standalone/lokal: HOST_IP=pulsebase.local + Hosts-Eintrag via make add-host"
	@echo ""
	@echo "Schritt 3 — Datenbank migrieren:"
	@echo "  make migrate"
	@echo ""
	@echo "Schritt 4 — Starten:"
	@echo "  make up         (Heim: hinter homelab-gateway / Caddy-Tailscale)"
	@echo "  make up-public  (öffentliche Instanz: eigenes Caddy + Let's Encrypt — siehe docs/deployment-public.md)"
	@echo ""
	@echo "Schritt 5 — Registrieren und Garmin verknüpfen:"
	@echo "  https://<deine-domain>/register"
	@echo "  https://<deine-domain>/garmin/link"

add-host:
	@grep -q "pulsebase.local" /etc/hosts && echo "pulsebase.local ist bereits eingetragen." || (echo "127.0.0.1 pulsebase.local" | sudo tee -a /etc/hosts && echo "pulsebase.local hinzugefügt.")

setup-user:
	@echo "Neuen User anlegen:"
	@echo "  https://<deine-domain>/register"
	@echo "Garmin verknüpfen:"
	@echo "  https://<deine-domain>/garmin/link"

tailwind-build:
	@echo "=== Tailwind CLI Build ==="
	@OS=$$(uname -s | tr '[:upper:]' '[:lower:]' | sed 's/darwin/macos/'); \
	ARCH=$$(uname -m | sed 's/x86_64/x64/;s/aarch64/arm64/'); \
	curl -fsSL "https://github.com/tailwindlabs/tailwindcss/releases/download/v3.4.19/tailwindcss-$$OS-$$ARCH" \
	  -o /tmp/tailwindcss-cli && chmod +x /tmp/tailwindcss-cli && \
	cd api && /tmp/tailwindcss-cli -c tailwind.config.js \
	  -i src/static/input.css \
	  -o src/static/tailwind.min.css \
	  --minify
	@echo "Done: api/src/static/tailwind.min.css"

test: ## Unit + Integration aller 3 Services (kein Docker nötig)
	cd api && .venv/bin/pytest tests/ -v --ignore=tests/e2e
	cd sync-service && .venv/bin/pytest tests/ -v
	cd ml-service && .venv/bin/pytest tests/ -v

test-build: ## api-Test-Image bauen (ohne Stack zu starten)
	$(DC) -f deploy/docker-compose.test.yml build api-test

test-env-up: ## Test-Stack auf Port 8001 starten (Image muss bereits gebaut sein — make test-build)
	$(DC) -f deploy/docker-compose.test.yml up -d --wait --wait-timeout 120

test-env-down: ## Test-Stack stoppen
	docker compose -f deploy/docker-compose.test.yml down 2>/dev/null || true
	docker rm -f pulsebase-db-test pulsebase-flyway-test pulsebase-api-test 2>/dev/null || true

test-seed: ## Live-DB (garmin) → Test-DB (garmin_test) kopieren — Test-Stack muss laufen (make test-env-up)
	@docker ps --format '{{.Names}}' | grep -qx pulsebase-db-test || { \
	  echo "FEHLER: Test-DB 'pulsebase-db-test' läuft nicht — zuerst 'make test-build && make test-env-up' (oder nutze 'make test-e2e-seeded')"; exit 1; }
	docker exec pulsebase-db pg_dump \
	  -U $$(grep ^DB_USER env/.env | cut -d= -f2) garmin \
	  | docker exec -i pulsebase-db-test psql \
	  -U $$(grep ^DB_USER env/.env | cut -d= -f2) garmin_test

test-user: ## Test-User in garmin_test anlegen — reicht für E2E
	cd api && TEST_EMAIL=$(TEST_EMAIL) TEST_PASSWORD=$(TEST_PASSWORD) DB_PORT=5434 \
	  DB_USER=$$(grep ^DB_USER ../env/.env | cut -d= -f2) \
	  DB_PASSWORD=$$(grep ^DB_PASSWORD ../env/.env | cut -d= -f2) \
	  .venv/bin/python tests/e2e/create_ci_user.py

test-e2e: ## Playwright E2E — alles automatisch, Stack wird auch bei Fehler gestoppt
	$(MAKE) test-build
	$(MAKE) test-env-up
	$(MAKE) test-user && \
	  api/.venv/bin/playwright install chromium --with-deps && \
	  ( cd api && TEST_EMAIL=$(TEST_EMAIL) TEST_PASSWORD=$(TEST_PASSWORD) .venv/bin/pytest tests/e2e/ -v ); \
	  EXIT=$$?; $(MAKE) test-env-down; exit $$EXIT

test-e2e-seeded: ## Wie test-e2e, aber mit echten Garmin-Daten (Prod→Test) + CI_HAS_DATA=true (inkl. @requires_data)
	# Hinweis: @requires_data-Tests brauchen einen User MIT Daten — ggf.
	#   make test-e2e-seeded TEST_EMAIL=<dein-Konto> TEST_PASSWORD=<...>
	# Der Default-Test-User (e2e@pulsebase.test) hat keine Aktivitäten.
	$(MAKE) test-build
	$(MAKE) test-env-up
	$(MAKE) test-seed
	$(MAKE) test-user && \
	  api/.venv/bin/playwright install chromium --with-deps && \
	  ( cd api && CI_HAS_DATA=true TEST_EMAIL=$(TEST_EMAIL) TEST_PASSWORD=$(TEST_PASSWORD) .venv/bin/pytest tests/e2e/ -v ); \
	  EXIT=$$?; $(MAKE) test-env-down; exit $$EXIT

test-all: ## Voll-Lauf (ohne Seed): Unit + E2E + JS-Coverage + Coverage — ein Kommando
	$(MAKE) test
	$(MAKE) test-e2e
	$(MAKE) test-js-coverage
	$(MAKE) test-coverage

test-all-seeded: ## Voll-Lauf MIT Garmin-Daten: Unit + seeded-E2E + JS-Coverage + Coverage
	$(MAKE) test
	$(MAKE) test-e2e-seeded
	$(MAKE) test-js-coverage
	$(MAKE) test-coverage

test-coverage: ## Coverage-Report aller 3 Services (Terminal + HTML unter */htmlcov/index.html)
	@echo "── api ─────────────────────────────────────────────────────────"
	cd api && .venv/bin/pytest tests/ --ignore=tests/e2e \
	  --cov=src --cov-report=term-missing --cov-report=html:htmlcov
	@echo "── sync-service ─────────────────────────────────────────────────"
	cd sync-service && .venv/bin/pytest tests/ \
	  --cov=src --cov-report=term-missing --cov-report=html:htmlcov
	@echo "── ml-service ───────────────────────────────────────────────────"
	cd ml-service && .venv/bin/pytest tests/ \
	  --cov=src --cov-report=term-missing --cov-report=html:htmlcov
	@echo ""
	@echo "HTML-Reports:"
	@echo "  → open api/htmlcov/index.html"
	@echo "  → open sync-service/htmlcov/index.html"
	@echo "  → open ml-service/htmlcov/index.html"

test-js: ## JS Unit Tests (Vitest)
	cd api && npm test

test-js-coverage: ## JS Coverage-Report (api/coverage/index.html)
	cd api && npm run test:coverage
	@echo "→ open api/coverage/index.html"