mirror of
https://github.com/DRYTRIX/TimeTracker.git
synced 2025-12-30 15:49:44 -06:00
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:
@@ -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.
|
||||
|
||||
|
||||
23
README.md
23
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
|
||||
|
||||
140
app/__init__.py
140
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:
|
||||
|
||||
@@ -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
|
||||
|
||||
76
docker-compose.example.yml
Normal file
76
docker-compose.example.yml
Normal 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
|
||||
|
||||
|
||||
167
docs/DOCKER_COMPOSE_SETUP.md
Normal file
167
docs/DOCKER_COMPOSE_SETUP.md
Normal 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.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]].
|
||||
|
||||
|
||||
@@ -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"}
|
||||
|
||||
Reference in New Issue
Block a user