A self-hosted Docker web app for tracking your work hours when your employer doesn't.
Hours are stored in /data/hours.json in the backend container. Data persists across rebuilds via a host
bind mount from ./data on the host. No action necessary.
Copy the config template and fill it in:
cp .env.default .envOpen .env and set your values:
| Variable | Default | Description |
|---|---|---|
DATA_FILE |
/data/hours.json |
Path to the JSON data file inside the backend container |
WORK_HOURS_PER_WEEK |
40 |
Total contracted hours per week |
WORK_DAYS_PER_WEEK |
5 |
Working days per week (Mon onwards — 5=Mon–Fri, 4=Mon–Thu, etc.) |
SECRET_KEY |
— | Secret key for signing session cookies |
APP_PASSWORD_HASH |
— | Hash of your login password |
DISABLE_AUTH |
false |
Set to true to bypass authentication entirely (e.g. on a trusted local network) |
Generate the two required security values and paste them into .env, wrapped in single quotes (prevents Docker Compose from misinterpreting the $ characters in the hash):
# Generate a secret key
python3 -c "import secrets; print(secrets.token_hex(32))"
# Generate a password hash (replace 'yourpassword' with your chosen password)
python3 -c "from werkzeug.security import generate_password_hash; print(generate_password_hash('yourpassword'))"Example entries in .env:
SECRET_KEY='your-generated-key-here'
APP_PASSWORD_HASH='scrypt:32768:8:1$...'
.envis git-ignored. Never commit it.
docker compose up --buildOpen http://localhost:925
- Monthly calendar view — Click any day to add entries, click entries to edit
- Easy in/out times — Dedicated 24-hour HH:MM input fields; use ↑ / ↓ arrow keys to increment or decrement hours and minutes, or type directly
- Stats page — Weekly breakdown, daily average, and pace indicators showing whether you are ahead or behind for the current week and current month
- Off days — Mark individual days as no-work (public holidays, leave). Off days are excluded from the expected hours target; any hours actually logged on those days still count toward your total
- Configurable schedule — Set your contracted hours and working days per week via environment variables
- Persistent storage — JSON file on a Docker volume
| Service | Tech | Port |
|---|---|---|
| Backend | Flask/Python | 5000 |
| Frontend | React/Vite | 3000 |
| Proxy | Nginx | 8080 |
GET /api/entries?year=2025&month=3— List entries for a monthPOST /api/entries— Create entry (clock_outoptional for open/punched-in entries)PUT /api/entries/:id— Update entryDELETE /api/entries/:id— Delete entry
GET /api/off-days— List all off daysGET /api/off-days?year=2025&month=3— List off days for a monthPOST /api/off-days— Mark a day as off ({"date": "YYYY-MM-DD"}). Idempotent.DELETE /api/off-days/2025-03-17— Remove an off day
GET /api/stats?year=2025&month=3— Monthly statistics
Key response fields:
| Field | Description |
|---|---|
total_hours |
Hours logged with both clock-in and clock-out |
target_hours |
Contracted hours for the month (adjusted for off days) |
expected_so_far |
Target pro-rated to the last completed workday |
difference |
total_hours − expected_so_far (positive = ahead) |
on_track |
true when difference >= 0 |
week_difference |
Ahead/behind for the current week |
week_target_so_far |
Week target pro-rated to the last completed workday |
elapsed_workdays |
Workdays counted so far (off days excluded) |
off_days |
List of YYYY-MM-DD strings for the queried month |
hours_per_day |
Derived from schedule (hours_per_week / work_days_per_week) |
weekly |
Per-week breakdown with week, hours, target, difference |
clock_inandclock_outmust be 24-hourHH:MM(zero-padded)- Examples:
08:30,17:45 - Invalid:
8:30,04:30 PM
After any code change, run the relevant test suite(s) and only consider the work complete when all tests pass.
cd backend
uv sync
uv run pytest tests/ -vcd frontend
npm install
npm test