chore(devops): streamline Docker Compose and refresh docs/config

- Simplify docker-compose setup and align environment defaults
- Update README and Quick Start to reflect the new compose flow
- Refine app initialization and configuration for clearer env handling
- Minor consistency and cleanup in config modules

No breaking changes expected.
This commit is contained in:
Dries Peeters
2025-10-20 20:44:11 +02:00
parent e99036fb78
commit f390a13474
7 changed files with 382 additions and 213 deletions

View File

@@ -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.

View File

@@ -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

View File

@@ -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:

View File

@@ -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

View File

@@ -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

View File

@@ -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.01.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]].

View File

@@ -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"}