diff --git a/QUICK_START_LOCAL_DEVELOPMENT.md b/QUICK_START_LOCAL_DEVELOPMENT.md index 4e59dff..07ddc27 100644 --- a/QUICK_START_LOCAL_DEVELOPMENT.md +++ b/QUICK_START_LOCAL_DEVELOPMENT.md @@ -1,168 +1,39 @@ -# Quick Start: Local Development with PostHog +# Quick Start: Local Development with Docker Compose -## TL;DR - Fastest Way to Test PostHog Locally - -Since analytics keys are embedded (not overridable via env vars), here's the quickest way to test PostHog locally: - -### Step 1: Get Your Dev Key - -1. Go to https://posthog.com -2. Create account / Sign in -3. Create project: "TimeTracker Dev" -4. Copy your **Project API Key** (starts with `phc_`) - -### Step 2: Run Setup Script (Windows) +## TL;DR - Fastest Local Start ```powershell -# Run the setup script -.\scripts\setup-dev-analytics.bat +git clone https://github.com/drytrix/TimeTracker.git +cd TimeTracker + +cp env.example .env +# Edit .env and set a strong SECRET_KEY + +docker-compose -f docker-compose.example.yml up -d + +# Open http://localhost:8080 ``` -**Or manually:** +See the full Docker Compose setup guide: `docs/DOCKER_COMPOSE_SETUP.md`. -1. **Create local config** (gitignored): - ```powershell - # Copy template - cp app\config\analytics_defaults.py app\config\analytics_defaults_local.py - ``` +## Local Development (Python) Alternative -2. **Edit `app\config\analytics_defaults_local.py`**: - ```python - # Replace placeholders with your dev keys - POSTHOG_API_KEY_DEFAULT = "phc_YOUR_DEV_KEY_HERE" - POSTHOG_HOST_DEFAULT = "https://app.posthog.com" - - SENTRY_DSN_DEFAULT = "" # Optional - SENTRY_TRACES_RATE_DEFAULT = "1.0" - ``` - -3. **Update `app\config\__init__.py`**: - ```python - """Configuration module for TimeTracker.""" - - # Try local dev config first - try: - from app.config.analytics_defaults_local import get_analytics_config, has_analytics_configured - except ImportError: - from app.config.analytics_defaults import get_analytics_config, has_analytics_configured - - __all__ = ['get_analytics_config', 'has_analytics_configured'] - ``` - -4. **Add to `.gitignore`** (if not already): - ``` - app/config/analytics_defaults_local.py - app/config/__init__.py.backup - ``` - -### Step 3: Run the Application +If you prefer to run locally with Python: ```powershell -# With Docker -docker-compose up -d - -# Or locally (if you have Python setup) +python -m venv venv +venv\Scripts\activate +pip install -r requirements.txt python app.py ``` -### Step 4: Enable Telemetry +## Analytics & Telemetry (Optional) -1. Open http://localhost:5000 -2. Complete setup → **Check "Enable telemetry"** -3. Or later: Admin → Telemetry Dashboard → Enable +To test PostHog or Sentry in development, set the respective variables in `.env` and restart the app. For advanced local analytics configuration, see `docs/analytics.md` and `assets/README.md`. -### Step 5: Test! +## Troubleshooting -Perform actions: -- Login/Logout -- Start/Stop timer -- Create project -- Create task - -Check PostHog dashboard - events should appear within seconds! - -## Verification - -### Check if PostHog is initialized - -```powershell -# Docker -docker-compose logs app | Select-String "PostHog" - -# Should see: "PostHog product analytics initialized" -``` - -### Check events locally - -```powershell -# View events in local logs -Get-Content logs\app.jsonl -Tail 50 | Select-String "event_type" -``` - -### Check PostHog Dashboard - -1. Go to PostHog dashboard -2. Click "Live Events" or "Events" -3. You should see events streaming in real-time! - -## Common Issues - -### "No events in PostHog" - -**Check 1:** Is telemetry enabled? -```powershell -Get-Content data\installation.json | Select-String "telemetry_enabled" -# Should show: "telemetry_enabled": true -``` - -**Check 2:** Is PostHog initialized? -```powershell -docker-compose logs app | Select-String "PostHog" -``` - -**Check 3:** Is the API key correct? -- Verify in PostHog dashboard: Settings → Project API Key - -### "Import error" when running app - -Make sure you created `analytics_defaults_local.py` and updated `__init__.py` - -### Keys visible in git - -```powershell -# Check what would be committed -git status -git diff app\config\analytics_defaults.py - -# Should NOT show your dev keys -# If it does, revert: -git checkout app\config\analytics_defaults.py -``` - -## Clean Up - -Before committing: - -```powershell -# Verify no keys in the main file -git diff app\config\analytics_defaults.py - -# Remove local config if needed -rm app\config\analytics_defaults_local.py - -# Restore original __init__.py -mv app\config\__init__.py.backup app\config\__init__.py -``` - -## Full Documentation - -See `docs/LOCAL_DEVELOPMENT_WITH_ANALYTICS.md` for: -- Multiple setup options -- Detailed troubleshooting -- Docker build approach -- Best practices - ---- - -**That's it!** You should now see events in your PostHog dashboard. 🎉 +- CSRF token errors: For HTTP (localhost), set `WTF_CSRF_SSL_STRICT=false` and ensure `SESSION_COOKIE_SECURE=false`/`CSRF_COOKIE_SECURE=false`. +- Database not ready: The app waits for Postgres healthcheck; check `docker-compose logs db`. +- Timezone issues: Set `TZ` to your locale. diff --git a/README.md b/README.md index 51df010..e8d4625 100644 --- a/README.md +++ b/README.md @@ -145,12 +145,18 @@ Get TimeTracker running in under 2 minutes: git clone https://github.com/drytrix/TimeTracker.git cd TimeTracker +# Create your .env from the template and set SECRET_KEY and TZ +cp env.example .env +# Edit .env and set a strong SECRET_KEY (python -c "import secrets; print(secrets.token_hex(32))") + # Start with Docker Compose -docker-compose up -d +docker-compose -f docker-compose.example.yml up -d # Access at http://localhost:8080 ``` +See the full Docker Compose setup guide: [`docs/DOCKER_COMPOSE_SETUP.md`](docs/DOCKER_COMPOSE_SETUP.md) + **First login creates the admin account** — just enter your username! ### Quick Test with SQLite @@ -239,11 +245,9 @@ docker-compose up -d ```bash # Configure your .env file cp env.example .env -# Edit .env with production settings -# IMPORTANT: Set a secure SECRET_KEY for CSRF tokens and sessions -# Generate one with: python -c "import secrets; print(secrets.token_hex(32))" +# Edit .env with production settings (set SECRET_KEY, TZ, DB credentials) -# Start with production compose +# Start with production compose (published image) docker-compose -f docker-compose.remote.yml up -d ``` @@ -262,7 +266,12 @@ docker-compose up -d ## 🔧 Configuration -TimeTracker is highly configurable through environment variables: +TimeTracker is highly configurable through environment variables. For a comprehensive list and recommended values, see: + +- [`docs/DOCKER_COMPOSE_SETUP.md`](docs/DOCKER_COMPOSE_SETUP.md) +- [`env.example`](env.example) + +Common settings: ```bash # Timezone and locale @@ -283,8 +292,6 @@ SECRET_KEY=your-secure-random-key SESSION_COOKIE_SECURE=true ``` -**📖 See [Configuration Guide](docs/REQUIREMENTS.md) for all options** - --- ## 📊 Analytics & Telemetry diff --git a/app/__init__.py b/app/__init__.py index 50e818c..74e4a88 100644 --- a/app/__init__.py +++ b/app/__init__.py @@ -195,7 +195,10 @@ def create_app(config=None): login_manager.init_app(app) socketio.init_app(app, cors_allowed_origins="*") oauth.init_app(app) - csrf.init_app(app) + + # Only initialize CSRF protection if enabled + if app.config.get('WTF_CSRF_ENABLED'): + csrf.init_app(app) try: # Configure limiter defaults from config if provided default_limits = [] @@ -473,47 +476,68 @@ def create_app(config=None): except Exception: pass - # Ensure CSRF cookie is present for HTML GET responses (helps login page) - try: - # Only for safe, HTML page responses - if request.method == "GET": - content_type = response.headers.get("Content-Type", "") - if isinstance(content_type, str) and content_type.startswith("text/html"): - cookie_name = app.config.get("CSRF_COOKIE_NAME", "XSRF-TOKEN") - has_cookie = bool(request.cookies.get(cookie_name)) - if not has_cookie: - # Generate a CSRF token and set cookie using same settings as /auth/csrf-token - try: - from flask_wtf.csrf import generate_csrf - token = generate_csrf() - except Exception: - token = "" - cookie_secure = bool( - app.config.get( - "CSRF_COOKIE_SECURE", - app.config.get("SESSION_COOKIE_SECURE", False), + # CSRF cookie/token handling + # If CSRF is enabled, ensure CSRF cookie exists for HTML GET responses + # If CSRF is disabled, explicitly clear any existing CSRF cookie to avoid confusion + if app.config.get('WTF_CSRF_ENABLED'): + try: + # Only for safe, HTML page responses + if request.method == "GET": + content_type = response.headers.get("Content-Type", "") + if isinstance(content_type, str) and content_type.startswith("text/html"): + cookie_name = app.config.get("CSRF_COOKIE_NAME", "XSRF-TOKEN") + has_cookie = bool(request.cookies.get(cookie_name)) + if not has_cookie: + # Generate a CSRF token and set cookie using same settings as /auth/csrf-token + try: + from flask_wtf.csrf import generate_csrf + token = generate_csrf() + except Exception: + token = "" + cookie_secure = bool( + app.config.get( + "CSRF_COOKIE_SECURE", + app.config.get("SESSION_COOKIE_SECURE", False), + ) ) - ) - cookie_httponly = bool(app.config.get("CSRF_COOKIE_HTTPONLY", False)) - cookie_samesite = app.config.get("CSRF_COOKIE_SAMESITE", "Lax") - cookie_domain = app.config.get("CSRF_COOKIE_DOMAIN") or None - cookie_path = app.config.get("CSRF_COOKIE_PATH", "/") - try: - max_age = int(app.config.get("WTF_CSRF_TIME_LIMIT", 3600)) - except Exception: - max_age = 3600 - response.set_cookie( - cookie_name, - token or "", - max_age=max_age, - secure=cookie_secure, - httponly=cookie_httponly, - samesite=cookie_samesite, - domain=cookie_domain, - path=cookie_path, - ) - except Exception: - pass + cookie_httponly = bool(app.config.get("CSRF_COOKIE_HTTPONLY", False)) + cookie_samesite = app.config.get("CSRF_COOKIE_SAMESITE", "Lax") + cookie_domain = app.config.get("CSRF_COOKIE_DOMAIN") or None + cookie_path = app.config.get("CSRF_COOKIE_PATH", "/") + try: + max_age = int(app.config.get("WTF_CSRF_TIME_LIMIT", 3600)) + except Exception: + max_age = 3600 + response.set_cookie( + cookie_name, + token or "", + max_age=max_age, + secure=cookie_secure, + httponly=cookie_httponly, + samesite=cookie_samesite, + domain=cookie_domain, + path=cookie_path, + ) + except Exception: + pass + else: + try: + cookie_name = app.config.get("CSRF_COOKIE_NAME", "XSRF-TOKEN") + if request.cookies.get(cookie_name): + # Clear the cookie by setting it expired + response.set_cookie( + cookie_name, + "", + max_age=0, + expires=0, + path=app.config.get("CSRF_COOKIE_PATH", "/"), + domain=app.config.get("CSRF_COOKIE_DOMAIN") or None, + secure=bool(app.config.get("CSRF_COOKIE_SECURE", app.config.get("SESSION_COOKIE_SECURE", False))), + httponly=bool(app.config.get("CSRF_COOKIE_HTTPONLY", False)), + samesite=app.config.get("CSRF_COOKIE_SAMESITE", "Lax"), + ) + except Exception: + pass return response # CSRF error handler with HTML-friendly fallback @@ -597,26 +621,36 @@ def create_app(config=None): return redirect(dest) # Expose csrf_token() in Jinja templates even without FlaskForm - try: - from flask_wtf.csrf import generate_csrf - - @app.context_processor - def inject_csrf_token(): - return dict(csrf_token=lambda: generate_csrf()) - - except Exception: - pass + # Always inject the function, but return empty string when CSRF is disabled + @app.context_processor + def inject_csrf_token(): + def get_csrf_token(): + # Return empty string if CSRF is disabled + if not app.config.get('WTF_CSRF_ENABLED'): + return "" + try: + from flask_wtf.csrf import generate_csrf + return generate_csrf() + except Exception: + return "" + return dict(csrf_token=get_csrf_token) # CSRF token refresh endpoint (GET) @app.route("/auth/csrf-token", methods=["GET"]) def get_csrf_token(): + # If CSRF is disabled, return empty token + if not app.config.get('WTF_CSRF_ENABLED'): + resp = jsonify(csrf_token="", csrf_enabled=False) + resp.headers["Cache-Control"] = "no-store, no-cache, must-revalidate, max-age=0" + return resp + try: from flask_wtf.csrf import generate_csrf token = generate_csrf() except Exception: token = "" - resp = jsonify(csrf_token=token) + resp = jsonify(csrf_token=token, csrf_enabled=True) try: resp.headers["Cache-Control"] = "no-store, no-cache, must-revalidate, max-age=0" except Exception: @@ -685,7 +719,9 @@ def create_app(config=None): app.register_blueprint(setup_bp) # Exempt API blueprint from CSRF protection (JSON API uses authentication, not CSRF tokens) - csrf.exempt(api_bp) + # Only if CSRF is enabled + if app.config.get('WTF_CSRF_ENABLED'): + csrf.exempt(api_bp) # Register OAuth OIDC client if enabled try: diff --git a/app/config.py b/app/config.py index cd006d9..bf37bcf 100644 --- a/app/config.py +++ b/app/config.py @@ -159,7 +159,8 @@ class ProductionConfig(Config): SESSION_COOKIE_SECURE = True SESSION_COOKIE_HTTPONLY = True REMEMBER_COOKIE_SECURE = True - WTF_CSRF_ENABLED = True + # Honor environment configuration; default to enabled in production + WTF_CSRF_ENABLED = os.getenv('WTF_CSRF_ENABLED', 'true').lower() == 'true' WTF_CSRF_SSL_STRICT = True # Configuration mapping diff --git a/docker-compose.example.yml b/docker-compose.example.yml new file mode 100644 index 0000000..19285af --- /dev/null +++ b/docker-compose.example.yml @@ -0,0 +1,76 @@ +services: + app: + image: ghcr.io/drytrix/timetracker:latest + container_name: timetracker-app + environment: + - TZ=${TZ:-Europe/Brussels} + - CURRENCY=${CURRENCY:-EUR} + - ROUNDING_MINUTES=${ROUNDING_MINUTES:-1} + - SINGLE_ACTIVE_TIMER=${SINGLE_ACTIVE_TIMER:-true} + - ALLOW_SELF_REGISTER=${ALLOW_SELF_REGISTER:-true} + - IDLE_TIMEOUT_MINUTES=${IDLE_TIMEOUT_MINUTES:-30} + - ADMIN_USERNAMES=${ADMIN_USERNAMES:-admin} + # Security (required in production) + - SECRET_KEY=${SECRET_KEY} + # Database (bundled Postgres) + - DATABASE_URL=postgresql+psycopg2://timetracker:timetracker@db:5432/timetracker + # CSRF & cookies (safe for HTTP local; tighten for HTTPS) + - WTF_CSRF_ENABLED=${WTF_CSRF_ENABLED:-true} + - WTF_CSRF_TIME_LIMIT=${WTF_CSRF_TIME_LIMIT:-3600} + - WTF_CSRF_SSL_STRICT=${WTF_CSRF_SSL_STRICT:-false} + - SESSION_COOKIE_SECURE=${SESSION_COOKIE_SECURE:-false} + - REMEMBER_COOKIE_SECURE=${REMEMBER_COOKIE_SECURE:-false} + - CSRF_COOKIE_SECURE=${CSRF_COOKIE_SECURE:-false} + - CSRF_COOKIE_HTTPONLY=${CSRF_COOKIE_HTTPONLY:-false} + - CSRF_COOKIE_SAMESITE=${CSRF_COOKIE_SAMESITE:-Lax} + - CSRF_COOKIE_NAME=${CSRF_COOKIE_NAME:-XSRF-TOKEN} + - SESSION_COOKIE_SAMESITE=${SESSION_COOKIE_SAMESITE:-Lax} + - PREFERRED_URL_SCHEME=${PREFERRED_URL_SCHEME:-http} + # Analytics (optional) + - SENTRY_DSN=${SENTRY_DSN:-} + - SENTRY_TRACES_RATE=${SENTRY_TRACES_RATE:-0.0} + - POSTHOG_API_KEY=${POSTHOG_API_KEY:-} + - POSTHOG_HOST=${POSTHOG_HOST:-https://app.posthog.com} + - ENABLE_TELEMETRY=${ENABLE_TELEMETRY:-false} + - TELE_SALT=${TELE_SALT:-} + ports: + - "8080:8080" + volumes: + - app_data:/data + - ./logs:/app/logs + depends_on: + db: + condition: service_healthy + restart: unless-stopped + healthcheck: + test: ["CMD", "curl", "-f", "http://localhost:8080/_health"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 40s + + db: + image: postgres:16-alpine + container_name: timetracker-db + environment: + - POSTGRES_DB=${POSTGRES_DB:-timetracker} + - POSTGRES_USER=${POSTGRES_USER:-timetracker} + - POSTGRES_PASSWORD=${POSTGRES_PASSWORD:-timetracker} + - TZ=${TZ:-Europe/Brussels} + volumes: + - db_data:/var/lib/postgresql/data + healthcheck: + test: ["CMD-SHELL", "pg_isready -U $$POSTGRES_USER -d $$POSTGRES_DB"] + interval: 10s + timeout: 5s + retries: 5 + start_period: 30s + restart: unless-stopped + +volumes: + app_data: + driver: local + db_data: + driver: local + + diff --git a/docs/DOCKER_COMPOSE_SETUP.md b/docs/DOCKER_COMPOSE_SETUP.md new file mode 100644 index 0000000..87f7442 --- /dev/null +++ b/docs/DOCKER_COMPOSE_SETUP.md @@ -0,0 +1,167 @@ +## Docker Compose Setup Guide + +This guide shows how to configure TimeTracker with Docker Compose, including all environment variables, a production-friendly example compose file, and quick-start commands. + +### Prerequisites +- Docker and Docker Compose installed +- A `.env` file in the project root + +### 1) Create and configure your .env file + +Start from the example and edit values: + +```bash +cp env.example .env +``` + +Required for production: +- SECRET_KEY: Generate a strong key: `python -c "import secrets; print(secrets.token_hex(32))"` +- TZ: Set your local timezone (preferred over UTC) to ensure correct timestamps based on your locale [[memory:7499916]]. + +Recommended defaults (safe to keep initially): +- POSTGRES_DB=timetracker +- POSTGRES_USER=timetracker +- POSTGRES_PASSWORD=timetracker + +If you use the bundled PostgreSQL container, leave `DATABASE_URL` as: +`postgresql+psycopg2://timetracker:timetracker@db:5432/timetracker` + +### 2) Use the example compose file + +We provide `docker-compose.example.yml` with sane defaults using the published image `ghcr.io/drytrix/timetracker:latest` [[memory:7499921]]. Copy it as your working compose file or run it directly: + +```bash +# Option A: Use example directly +docker-compose -f docker-compose.example.yml up -d + +# Option B: Make it your default compose +cp docker-compose.example.yml docker-compose.yml +docker-compose up -d +``` + +Access the app at `http://localhost:8080`. + +For a full stack with HTTPS reverse proxy and monitoring, see the root `docker-compose.yml` and the Monitoring section below. + +### 3) Verify +```bash +docker-compose ps +docker-compose logs app --tail=100 +``` + +### 4) Optional services +- Reverse proxy (HTTPS): See `docker-compose.yml` (services `certgen` and `nginx`). +- Monitoring stack: Prometheus, Grafana, Loki, Promtail are available in `docker-compose.yml`. + +--- + +## Environment Variables Reference + +All environment variables can be provided via `.env` and are consumed by the `app` container unless otherwise noted. Defaults shown are the effective values if not overridden. + +### Core +- SECRET_KEY: Secret used for sessions/CSRF. Required in production. No default. +- FLASK_ENV: Flask environment. Default: `production`. +- FLASK_DEBUG: Enable debug. Default: `false`. +- TZ: Local timezone (e.g., `Europe/Brussels`). Default: `Europe/Rome` in env.example; compose defaults may override. + +### Database +- DATABASE_URL: SQLAlchemy URL. Default: `postgresql+psycopg2://timetracker:timetracker@db:5432/timetracker`. +- POSTGRES_DB: Database name (db service). Default: `timetracker`. +- POSTGRES_USER: Database user (db service). Default: `timetracker`. +- POSTGRES_PASSWORD: Database password (db service). Default: `timetracker`. +- POSTGRES_HOST: Hostname for external DB (not needed with bundled db). Default: `db`. + +### Application behavior +- CURRENCY: ISO currency code. Default: `EUR`. +- ROUNDING_MINUTES: Rounding step for entries. Default: `1`. +- SINGLE_ACTIVE_TIMER: Allow only one active timer per user. Default: `true`. +- IDLE_TIMEOUT_MINUTES: Auto-pause after idle. Default: `30`. +- ALLOW_SELF_REGISTER: Allow new users to self-register. Default: `true`. +- ADMIN_USERNAMES: Comma-separated admin usernames. Default: `admin`. + +### Authentication +- AUTH_METHOD: `local` | `oidc` | `both`. Default: `local`. +- OIDC_ISSUER: OIDC provider issuer URL. +- OIDC_CLIENT_ID: OIDC client id. +- OIDC_CLIENT_SECRET: OIDC client secret. +- OIDC_REDIRECT_URI: App redirect URI for OIDC callback. +- OIDC_SCOPES: Space-separated scopes. Default: `openid profile email`. +- OIDC_USERNAME_CLAIM: Default: `preferred_username`. +- OIDC_FULL_NAME_CLAIM: Default: `name`. +- OIDC_EMAIL_CLAIM: Default: `email`. +- OIDC_GROUPS_CLAIM: Default: `groups`. +- OIDC_ADMIN_GROUP: Optional admin group name. +- OIDC_ADMIN_EMAILS: Optional comma-separated admin emails. +- OIDC_POST_LOGOUT_REDIRECT_URI: Optional RP-initiated logout return URI. + +### CSRF and Cookies +- WTF_CSRF_ENABLED: Enable CSRF protection. Default: `true` (example) or `false` in dev. +- WTF_CSRF_TIME_LIMIT: Token lifetime (seconds). Default: `3600`. +- WTF_CSRF_SSL_STRICT: Require HTTPS for CSRF referer checks. Default: `true` for production via compose; set `false` for HTTP. +- WTF_CSRF_TRUSTED_ORIGINS: Comma-separated allowed origins (scheme://host). Default: `https://localhost`. +- PREFERRED_URL_SCHEME: `http` or `https`. Default: `https` in production setups; set `http` for local. +- SESSION_COOKIE_SECURE: Send cookies only over HTTPS. Default: `true` (prod) / `false` (local test). +- SESSION_COOKIE_HTTPONLY: Default: `true`. +- SESSION_COOKIE_SAMESITE: `Lax` | `Strict` | `None`. Default: `Lax`. +- REMEMBER_COOKIE_SECURE: Default: `true` (prod) / `false` (local test). +- CSRF_COOKIE_SECURE: Default: `true` (prod) / `false` (local test). +- CSRF_COOKIE_HTTPONLY: Default: `false`. +- CSRF_COOKIE_SAMESITE: Default: `Lax`. +- CSRF_COOKIE_NAME: Default: `XSRF-TOKEN`. +- CSRF_COOKIE_DOMAIN: Optional cookie domain for subdomains (unset by default). +- PERMANENT_SESSION_LIFETIME: Session lifetime seconds. Default: `86400`. + +### File uploads and backups +- MAX_CONTENT_LENGTH: Max upload size in bytes. Default: `16777216` (16MB). +- UPLOAD_FOLDER: Upload path inside container. Default: `/data/uploads`. +- BACKUP_RETENTION_DAYS: Retain DB backups (if enabled). Default: `30`. +- BACKUP_TIME: Backup time (HH:MM). Default: `02:00`. + +### Logging +- LOG_LEVEL: Default: `INFO`. +- LOG_FILE: Default: `/data/logs/timetracker.log` or `/app/logs/timetracker.log` based on compose. + +### Analytics & Telemetry (optional) +- SENTRY_DSN: Sentry DSN (empty by default). +- SENTRY_TRACES_RATE: 0.0–1.0 sampling rate. Default: `0.0`. +- POSTHOG_API_KEY: PostHog API key (empty by default). +- POSTHOG_HOST: PostHog host. Default: `https://app.posthog.com`. +- ENABLE_TELEMETRY: Anonymous install telemetry toggle. Default: `false`. +- TELE_SALT: Unique salt for anonymous fingerprinting (optional). +- APP_VERSION: Optional override; usually auto-detected. + +### Reverse proxy & monitoring (compose-only variables) +- HOST_IP: Used by `certgen` (in `docker-compose.remote.yml`) to embed SANs in self-signed certs. Default: `192.168.1.100`. +- Grafana variables (service `grafana` in `docker-compose.yml`): + - GF_SECURITY_ADMIN_PASSWORD: Default: `admin` (set your own in prod). + - GF_USERS_ALLOW_SIGN_UP: Default: `false`. + - GF_SERVER_ROOT_URL: Default: `http://localhost:3000`. + +--- + +## Monitoring stack (optional) + +The root `docker-compose.yml` includes Prometheus, Grafana, Loki and Promtail. Start them together with the app: + +```bash +docker-compose up -d # uses the root compose with monitoring +``` + +Open: +- App: `http://localhost` (or `https://localhost` if certificates are present) +- Grafana: `http://localhost:3000` +- Prometheus: `http://localhost:9090` +- Loki: `http://localhost:3100` + +For CSRF and cookie issues behind proxies, see `docs/CSRF_CONFIGURATION.md`. + +--- + +## Troubleshooting + +- CSRF token errors: Ensure `SECRET_KEY` is stable and set correct CSRF/cookie flags for HTTP vs HTTPS. +- Database connection: Confirm `db` service is healthy and `DATABASE_URL` points to it. +- Timezone issues: Set `TZ` to your local timezone [[memory:7499916]]. + + diff --git a/logs/app.jsonl b/logs/app.jsonl index f792534..1d1fc58 100644 --- a/logs/app.jsonl +++ b/logs/app.jsonl @@ -12,3 +12,14 @@ {"asctime": "2025-10-20 14:30:09,546", "levelname": "INFO", "name": "timetracker", "message": "auth.logout", "request_id": "f80073a2-aee6-4928-b9cf-44d6ace690b0", "event": "auth.logout", "user_id": 1} {"asctime": "2025-10-20 14:34:19,473", "levelname": "INFO", "name": "timetracker", "message": "setup.completed", "request_id": "86ac6b57-806a-45a5-abf1-781ea6b4ca4b", "event": "setup.completed", "telemetry_enabled": true} {"asctime": "2025-10-20 14:34:22,253", "levelname": "INFO", "name": "timetracker", "message": "auth.login", "request_id": "0dcfc3dd-1efa-4c6d-b403-26c187656674", "event": "auth.login", "user_id": 1, "auth_method": "local"} +{"asctime": "2025-10-20 20:08:21,420", "levelname": "INFO", "name": "timetracker", "message": "setup.completed", "request_id": "bea1cf53-11ed-4851-bce4-e3528c47d42b", "event": "setup.completed", "telemetry_enabled": false} +{"asctime": "2025-10-20 20:08:23,876", "levelname": "INFO", "name": "timetracker", "message": "auth.login", "request_id": "f0167eaa-0f9d-4c6e-af3b-f35e0cd6f63b", "event": "auth.login", "user_id": 1, "auth_method": "local"} +{"asctime": "2025-10-20 20:09:56,566", "levelname": "INFO", "name": "timetracker", "message": "setup.completed", "request_id": "7fc7c326-29db-49a7-a80b-0515f427fe3c", "event": "setup.completed", "telemetry_enabled": false} +{"asctime": "2025-10-20 20:09:59,301", "levelname": "INFO", "name": "timetracker", "message": "auth.login", "request_id": "78bf6b25-412f-4bee-8d67-ded5b4fee86a", "event": "auth.login", "user_id": 1, "auth_method": "local"} +{"asctime": "2025-10-20 20:15:47,262", "levelname": "INFO", "name": "timetracker", "message": "setup.completed", "request_id": "3828246e-f2fa-47cd-84fc-f322da1cc216", "event": "setup.completed", "telemetry_enabled": false} +{"asctime": "2025-10-20 20:15:49,953", "levelname": "INFO", "name": "timetracker", "message": "auth.login", "request_id": "9b9b16ff-5e6c-4cd7-bbde-54162d1a929b", "event": "auth.login", "user_id": 1, "auth_method": "local"} +{"asctime": "2025-10-20 20:40:12,247", "levelname": "INFO", "name": "timetracker", "message": "setup.completed", "request_id": "13c49d26-2c81-4644-9f2d-f6a117bcea7f", "event": "setup.completed", "telemetry_enabled": false} +{"asctime": "2025-10-20 20:40:19,162", "levelname": "INFO", "name": "timetracker", "message": "auth.login", "request_id": "cdd831f4-40fe-430c-af5e-123affba5069", "event": "auth.login", "user_id": 1, "auth_method": "local"} +{"asctime": "2025-10-20 20:40:42,782", "levelname": "INFO", "name": "timetracker", "message": "project.created", "request_id": "86e686d1-750c-4599-8009-1ae8284b9576", "event": "project.created", "user_id": 1, "project_id": 8, "project_name": "fezfjsvvjkldfjl", "has_client": true} +{"asctime": "2025-10-20 20:43:44,701", "levelname": "INFO", "name": "timetracker", "message": "setup.completed", "request_id": "17a2f9be-8851-4caf-9129-43643cda15ce", "event": "setup.completed", "telemetry_enabled": true} +{"asctime": "2025-10-20 20:43:50,049", "levelname": "INFO", "name": "timetracker", "message": "auth.login", "request_id": "b2e3a7e8-828a-4aec-8efa-f27d5728f164", "event": "auth.login", "user_id": 1, "auth_method": "local"}