Files
HealthLog/.env.example
T
Marc-André BombeckandClaude Opus 4.7 9b01c86e30 feat(charts): medication chart matches other charts (7d trend + indicator + target-range)
Marc: "Bei Medikamenten zum Beispiel wird jetzt nicht 7-Tage-Schnitt,
Trend und Zielbereiche angezeigt. Das möchte ich natürlich für alle
Charts."

The medication-compliance chart shipped in v1.4.15 A4 (replacing the
static placeholder) was missing the trend chip + target-range
visualization the BP/weight/mood charts already paint. Bringing it to
parity:

- 7-day trend chip in the header: signed delta in percentage points
  with a metric-aware sentiment colour (rising compliance → green,
  falling → orange) and an arrow icon. Mirrors the bg-muted/40 chip
  pattern HealthChart uses for its weekly/monthly bucket label.
- New `computeMedicationTrend7d()` helper computes the delta as the
  mean of the most-recent 7 daily rates minus the mean of the prior 7
  (capped at a 14-day window). Mean-difference rather than slope so a
  single outlier day doesn't wag the indicator.
- Target-range visualization: added a 100 % goal ReferenceLine (green,
  solid-ish) ALONGSIDE the existing 80 % minimum-acceptable threshold
  (yellow, paler dash). Two lines together visualise the target range
  the way HealthChart's targetZone band does for BP/weight tiles.

Tests:
- 6 new unit tests pin computeMedicationTrend7d() including direction,
  delta, the 14-day cap, short windows, stable noise rejection.
- The existing chart-render snapshot is extended with an explicit
  assertion that the trend chip is suppressed in the empty state
  (so the empty-state header doesn't paint a misleading "0 pp"
  indicator).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-09 23:32:01 +02:00

163 lines
7.9 KiB
Bash

# =============================================================================
# HealthLog — Environment Configuration
# =============================================================================
# Copy this file to .env and fill in the values.
# $ cp .env.example .env
#
# Required variables: DATABASE_URL or POSTGRES_PASSWORD, SESSION_SECRET,
# ENCRYPTION_KEY, API_TOKEN_HMAC_KEY
#
# Generate the three secrets with:
# $ openssl rand -hex 32 # any UNIX shell
# $ node -e 'console.log(require("crypto").randomBytes(32).toString("hex"))'
#
# All secrets must be 64-character hex strings (= 32 bytes).
# =============================================================================
# -----------------------------------------------------------------------------
# Database
# -----------------------------------------------------------------------------
# When using docker-compose.yml: the app container reaches Postgres via the
# service name "db", so the hostname is "db" — NOT "localhost".
# When developing without Docker (`pnpm dev` against a host-installed Postgres):
# change the hostname to "localhost" and adjust the credentials.
DATABASE_URL="postgresql://healthlog:CHANGE-ME@db:5432/healthlog?schema=public"
# Postgres password — used both by the db service AND by DATABASE_URL above.
# Pick a strong random password, e.g. `openssl rand -base64 24`.
POSTGRES_PASSWORD="CHANGE-ME"
# -----------------------------------------------------------------------------
# App URL
# -----------------------------------------------------------------------------
# The public URL where users reach the app. Behind a reverse proxy with TLS,
# this looks like https://healthlog.example.com. For local dev, leave default.
NEXT_PUBLIC_APP_URL="http://localhost:3000"
APP_URL="http://localhost:3000"
# Node environment: "development" for `pnpm dev`, "production" for Docker.
NODE_ENV="development"
# -----------------------------------------------------------------------------
# Required Secrets — generate with `openssl rand -hex 32`
# -----------------------------------------------------------------------------
# Signs session cookies. Rotating it logs out everyone.
SESSION_SECRET="REPLACE-WITH-OUTPUT-OF-openssl-rand-hex-32"
# AES-256-GCM key for encrypting Withings tokens, GitHub PATs, AI provider
# keys, and VAPID private keys at rest. Rotate carefully — old ciphertexts
# need re-encryption.
ENCRYPTION_KEY="REPLACE-WITH-OUTPUT-OF-openssl-rand-hex-32"
# HMAC-SHA256 key for hashing external-API Bearer tokens before storing them
# in the database. Rotating it invalidates all issued tokens.
API_TOKEN_HMAC_KEY="REPLACE-WITH-OUTPUT-OF-openssl-rand-hex-32"
# -----------------------------------------------------------------------------
# Withings OAuth (optional — for automatic scale/BP/activity sync)
# -----------------------------------------------------------------------------
# Register an app at https://developer.withings.com/ and paste the credentials
# here. Per-user OAuth credentials can also be configured in Settings.
# WITHINGS_CLIENT_ID=""
# WITHINGS_CLIENT_SECRET=""
# WITHINGS_REDIRECT_URI="https://healthlog.example.com/api/withings/callback"
# WITHINGS_WEBHOOK_SECRET="" # any random string — used to harden the webhook URL
# -----------------------------------------------------------------------------
# Telegram (optional — webhook hardening)
# -----------------------------------------------------------------------------
# Set to verify incoming Telegram updates. The bot token itself is configured
# per-user in Settings (encrypted at rest).
# TELEGRAM_WEBHOOK_SECRET=""
# -----------------------------------------------------------------------------
# AI Provider (optional)
# -----------------------------------------------------------------------------
# Users bring their own API keys via Settings. No server-side key needed by
# default. The admin can configure a fallback key in the Admin Panel.
# Maximum number of comprehensive-insight generations a single user can
# trigger per hour (POST /api/insights/generate). Default 10. The 24h
# DB cache short-circuits this for read traffic; only force=true and
# expired-cache regenerations consume tokens. Lower this when running
# on a tight LLM budget.
# INSIGHTS_RATE_LIMIT_PER_HOUR="10"
# -----------------------------------------------------------------------------
# Structured Logging — Wide Events (optional, sensible defaults)
# -----------------------------------------------------------------------------
# Levels: debug | info | warn | error
# LOG_LEVEL="info"
# Sample rate for successful requests (0.0 = sample nothing, 1.0 = log all).
# Errors and slow requests are ALWAYS logged regardless of this setting.
# LOG_SAMPLE_RATE="1.0"
# Slow-request threshold in milliseconds — anything slower is always kept.
# LOG_SLOW_THRESHOLD_MS="3000"
# Include stack traces on error events. Disable in environments where logs
# leave the host unencrypted.
# LOG_INCLUDE_STACK="true"
# Optional: ship Wide Events to Loki. Leave blank for stdout-only logging.
# LOKI_ENDPOINT="http://loki:3100"
# LOKI_USERNAME=""
# LOKI_PASSWORD=""
# -----------------------------------------------------------------------------
# Process split (optional, single-container deploys ignore this)
# -----------------------------------------------------------------------------
# `all` — single container runs HTTP + reminder worker (default, legacy).
# `web` — HTTP only; pair with a separate `app-worker` service.
# `worker`— pg-boss only; rejects HTTP traffic with a 503.
# See docs/self-hosting/scaling.md for the split-stack guide.
# HEALTHLOG_PROCESS_TYPE="all"
# -----------------------------------------------------------------------------
# Encryption-key versioning (optional — for at-rest key rotation)
# -----------------------------------------------------------------------------
# JSON map of key id → 64-hex-char or base64 32-byte key. When set, it
# supersedes ENCRYPTION_KEY for new writes; old rows continue to decrypt under
# their original id. ENCRYPTION_ACTIVE_KEY_ID picks which entry encrypt() uses.
# Run `pnpm tsx scripts/rotate-encryption-key.ts <new-id>` to re-encrypt every
# existing row under the new key. See docs/ops/encryption-key-rotation.md.
# ENCRYPTION_KEYS='{"v1":"<old-key>","v2":"<new-key>"}'
# ENCRYPTION_ACTIVE_KEY_ID="v2"
# -----------------------------------------------------------------------------
# Off-host backup (optional — daily encrypted dumps to an S3-compatible bucket)
# -----------------------------------------------------------------------------
# Required ALL-OR-NONE: setting any of these without setting the rest disables
# the backup silently. The backup runs at 02:30 Europe/Berlin via pg-boss cron.
# Restore with `pnpm tsx scripts/restore-backup.ts <userId> <YYYY-MM-DD>`.
# See docs/ops/backup-restore.md for the format spec + step-by-step.
# BACKUP_S3_ENDPOINT="https://<account>.r2.cloudflarestorage.com"
# BACKUP_S3_BUCKET="healthlog-backups"
# BACKUP_S3_ACCESS_KEY=""
# BACKUP_S3_SECRET_KEY=""
# BACKUP_S3_REGION="auto" # "auto" for Cloudflare R2; real region for AWS S3
# BACKUP_RETENTION_DAYS="30" # objects older than this are pruned (best-effort; prefer a bucket lifecycle rule)
# BACKUP_ENCRYPTION_KEY="" # 64-hex-char or base64 32-byte; SEPARATE from ENCRYPTION_KEY
# -----------------------------------------------------------------------------
# Auto-deploy webhook (v1.4.15 — phase C2)
# -----------------------------------------------------------------------------
# Coolify (or any CI) pings POST /api/internal/deploy-webhook after a deploy
# attempt with success/failure. We verify the request via this shared secret
# carried in the `X-Deploy-Webhook-Secret` header (timing-safe compare).
# Generate with `openssl rand -hex 32`. Configure the SAME value in Coolify's
# Notifications → Webhook custom-headers panel. See docs/audit/v1415-auto-deploy.md.
# DEPLOY_WEBHOOK_SECRET=""