Compare commits

..

4 Commits

Author SHA1 Message Date
lukas 2f433c92da Update uv.lock
Django CI/CD / test (push) Successful in 44s
Django CI/CD / build-and-push (push) Successful in 1m26s
2026-05-12 18:57:13 +02:00
lukas 5b2b79f553 Fix comment not being a comment 2026-05-12 18:56:58 +02:00
lukas 36411c99a7 Version 1.7.0
Django CI/CD / test (push) Successful in 38s
Django CI/CD / build-and-push (push) Has been skipped
## 1.7.0 / 2026-05-12

### New
* Add toast notification system with HTMX middleware integration
* Add component system (Cotton-based): button, modal, table_row,
search_field, gamelink
* Add needs_price_update field to Purchase model for reliable price
change detection
* Add confirmation dialog before deleting a game
* Add game status information documentation (STATUSES.md)
* Allow directly updating device in session list via inline selector
* Migrate from Poetry to uv for Python dependency management
* Scope URLs to the games namespace
* Start session template shared between add and edit views

### Improved
* Major style overhaul: CSS variables, improved dark mode, Flowbite 4.x
upgrade
* Improve game status evaluation and add abandon prompt on refund
* Robustify Docker container and fix default database location
* Make component rendering deterministic for improved caching
* Component caching: deterministic randomid generation
* Component test suite with 1000+ lines of tests
* Make tests more robust with django-pytest
* Update NameWithIcon component: testable, fixed platform extraction bug
* Pin Caddy version and improve make dev-prod
* Add .env.example documenting environment variables
* Unify A() component with explicit url_name vs href parameters

### Fixed
* Fix refund confirmation not working
* Fix stats view missing first and last game values
* Fix A() component silent fallback on URL typos
* Fix secondary submit buttons not working
* Fix button not passing attributes
* Fix default mutable arguments in component functions
* Fix extra submit button when adding purchase
* Fix pointer cursor on search field button

### Removed
* Remove GraphQL API

### Dependencies
* Update django-ninja to 1.6.2
2026-05-12 18:36:46 +02:00
lukas 360e8f9eaf Make container more robust (#95)
Django CI/CD / build-and-push (push) Has been cancelled
Django CI/CD / test (push) Has been cancelled
Reviewed-on: #95

12 files changed (+149, -66)
Key changes:
1. Monolithic container — Replaced the two-service compose setup (backend + frontend/caddy) with a single timetracker container. Caddy is now built into the image rather than running as a separate container.
2. Supervisord process manager — Added supervisor.conf and installed supervisor in the Dockerfile. entrypoint.sh now delegates to supervisord to manage three processes: Caddy, Gunicorn, and Qcluster — replacing manual trap/signaling logic.
3. Bundled Caddy — The Dockerfile now downloads and installs Caddy v2.9.1 directly into the image (/usr/local/bin/caddy). The Caddyfile was updated to use reverse_proxy localhost:8001 and serves static files from /home/timetracker/app/static.
4. Configurable deployment — Added .env.example with configurable environment variables: TZ, PUID/PGID, TIMETRACKER_EXTERNAL_PORT, DATA_DIR, CSRF_TRUSTED_ORIGINS. docker-compose.yml now references these with sensible defaults.
5. UID/GID flexibility — entrypoint.sh uses usermod/groupmod at startup to remap the timetracker user to the host-specified PUID/PGID, avoiding permission issues with mounted volumes.
6. Database & static files — settings.py now respects DATA_DIR env var for the SQLite database path. STATIC_ROOT changed to BASE_DIR / "static".
7. Dev improvements — New Caddyfile.dev (with browse enabled for static files) and updated Makefile dev-prod target runs Caddy alongside Django in development.
8. Tests — Re-enabled the test step in the Docker build GitHub Actions workflow.
2026-05-12 16:29:34 +00:00
16 changed files with 194 additions and 74 deletions
-1
View File
@@ -9,7 +9,6 @@ static
.drone.yml
.editorconfig
.gitignore
Caddyfile
CHANGELOG.md
db.sqlite3
docker-compose*
+21
View File
@@ -0,0 +1,21 @@
# Docker registry URL (used in docker-compose.yml)
REGISTRY_URL=registry.kucharczyk.xyz
# Container timezone
TZ=Europe/Prague
# User/group IDs for container (used in entrypoint.sh)
PUID=1000
PGID=100
# External port mapping
TIMETRACKER_EXTERNAL_PORT=8000
# Django production mode (set to "1" for production)
PROD=1
# Database directory (defaults to project root)
DATA_DIR=/home/timetracker/app/data
# CSRF trusted origins
CSRF_TRUSTED_ORIGINS=https://tracker.kucharczyk.xyz
+3 -3
View File
@@ -22,8 +22,8 @@ jobs:
- name: Run Migrations
run: uv run python manage.py migrate
# - name: Run Tests
# run: PROD=1 uv run pytest
- name: Run Tests
run: uv run --with pytest-django pytest
build-and-push:
needs: test
@@ -33,7 +33,7 @@ jobs:
- uses: actions/checkout@v4
- name: Set Version
run: echo "VERSION_NUMBER=1.6.1" >> $GITHUB_ENV
run: echo "VERSION_NUMBER=1.7.0" >> $GITHUB_ENV
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
+1
View File
@@ -5,6 +5,7 @@ __pycache__
node_modules
package-lock.json
db.sqlite3
data/
/static/
dist/
.DS_Store
+40 -3
View File
@@ -1,7 +1,44 @@
## Unreleased
## 1.7.0 / 2026-05-12
### New
* Add toast notification system with HTMX middleware integration
* Add component system (Cotton-based): button, modal, table_row, search_field, gamelink
* Add needs_price_update field to Purchase model for reliable price change detection
* Add confirmation dialog before deleting a game
* Add game status information documentation (STATUSES.md)
* Allow directly updating device in session list via inline selector
* Migrate from Poetry to uv for Python dependency management
* Scope URLs to the games namespace
* Start session template shared between add and edit views
### Improved
* Add a prompt to set game to Abandoned upon refund
* Major style overhaul: CSS variables, improved dark mode, Flowbite 4.x upgrade
* Improve game status evaluation and add abandon prompt on refund
* Robustify Docker container and fix default database location
* Make component rendering deterministic for improved caching
* Component caching: deterministic randomid generation
* Component test suite with 1000+ lines of tests
* Make tests more robust with django-pytest
* Update NameWithIcon component: testable, fixed platform extraction bug
* Pin Caddy version and improve make dev-prod
* Add .env.example documenting environment variables
* Unify A() component with explicit url_name vs href parameters
### Fixed
* Fix refund confirmation not working
* Fix stats view missing first and last game values
* Fix A() component silent fallback on URL typos
* Fix secondary submit buttons not working
* Fix button not passing attributes
* Fix default mutable arguments in component functions
* Fix extra submit button when adding purchase
* Fix pointer cursor on search field button
### Removed
* Remove GraphQL API
### Dependencies
* Update django-ninja to 1.6.2
## 1.6.1 / 2026-01-30 11:48+01:00
@@ -161,7 +198,7 @@
* Use the same form when editing a session as when adding a session
* Change recent session view to current year instead of last 30 days
* Add a hacky way not to reload a page when starting or ending a session (https://git.kucharczyk.xyz/lukas/timetracker/issues/52)
* Improve session list (https://git.kucharczyk.xyz/lukas/timetracker/issues/53)
* Improve session listing (https://git.kucharczyk.xyz/lukas/timetracker/issues/53)
### Fixes
+5 -4
View File
@@ -1,14 +1,15 @@
{
auto_https off
admin off
}
:8000 {
handle_path /static/* {
root * /usr/share/caddy
root * /home/timetracker/app/static
file_server
}
handle {
reverse_proxy backend:8001
handle /robots.txt {
root * /home/timetracker/app/games/static
file_server
}
reverse_proxy localhost:8001
}
+15
View File
@@ -0,0 +1,15 @@
{
auto_https off
}
:8000 {
handle_path /static/* {
root * static
file_server browse
}
handle /robots.txt {
root * games/static
file_server browse
}
reverse_proxy :8001
}
+21 -7
View File
@@ -22,20 +22,34 @@ ENV PROD=1 \
PYTHONUNBUFFERED=1 \
PATH="/home/timetracker/app/.venv/bin:$PATH"
RUN useradd -m --uid 1000 timetracker \
&& mkdir -p /var/www/django/static \
&& chown timetracker:timetracker /var/www/django/static
RUN apt-get update && apt-get install -y --no-install-recommends \
curl \
ca-certificates \
libcap2-bin \
supervisor \
&& rm -rf /var/lib/apt/lists/* \
&& useradd -m --uid 1000 timetracker \
&& mkdir -p /var/log/supervisor /etc/supervisor/conf.d /home/timetracker/data \
&& chown timetracker:timetracker /var/log/supervisor /home/timetracker/data
ARG CADDY_VERSION=2.9.1
RUN curl -sL "https://github.com/caddyserver/caddy/releases/download/v${CADDY_VERSION}/caddy_${CADDY_VERSION}_linux_amd64.tar.gz" \
-o /tmp/caddy.tar.gz && \
tar -xzf /tmp/caddy.tar.gz -C /tmp && \
mv /tmp/caddy /usr/local/bin/caddy && \
rm /tmp/caddy.tar.gz && \
chmod +x /usr/local/bin/caddy
WORKDIR /home/timetracker/app
COPY --from=builder --chown=timetracker:timetracker /home/timetracker/app /home/timetracker/app
COPY --chown=timetracker:timetracker Caddyfile /etc/caddy/Caddyfile
COPY --chown=timetracker:timetracker supervisor.conf /etc/supervisor/conf.d/supervisor.conf
COPY --chown=timetracker:timetracker entrypoint.sh /
RUN chmod +x /entrypoint.sh
USER timetracker
ENV VERSION_NUMBER=1.6.1
ENV VERSION_NUMBER=1.7.0
EXPOSE 8000
CMD [ "/entrypoint.sh" ]
ENTRYPOINT ["/entrypoint.sh"]
+4 -3
View File
@@ -41,9 +41,10 @@ caddy:
dev-prod: migrate collectstatic
@npx concurrently \
--names "Django,Django-Q" \
"PROD=1 uv run python -m gunicorn --bind 0.0.0.0:8001 timetracker.asgi:application -k uvicorn.workers.UvicornWorker"
"uv run manage.py qcluster"
--names "Caddy,Django,Django-Q" \
"caddy run --config Caddyfile.dev" \
"PROD=1 uv run python -m gunicorn --bind 0.0.0.0:8001 timetracker.asgi:application -k uvicorn.workers.UvicornWorker" \
"PROD=1 uv run manage.py qcluster"
dumpgames:
uv run python manage.py dumpdata --format yaml games --output tracker_fixture.yaml
+12 -21
View File
@@ -1,30 +1,21 @@
---
services:
backend:
image: registry.kucharczyk.xyz/timetracker
timetracker:
image: ${REGISTRY_URL:-registry.kucharczyk.xyz}/timetracker:1.7.0
build:
context: .
dockerfile: Dockerfile
container_name: timetracker
environment:
- TZ=Europe/Prague
- CSRF_TRUSTED_ORIGINS="https://tracker.kucharczyk.xyz"
user: "1000"
- TZ=${TZ:-Europe/Prague}
- CSRF_TRUSTED_ORIGINS=https://tracker.kucharczyk.xyz
- PUID=${PUID:-1000}
- PGID=${PGID:-100}
- DATA_DIR=${DATA_DIR:-/home/timetracker/app/data}
ports:
- "${TIMETRACKER_EXTERNAL_PORT:-8000}:8000"
volumes:
- "static-files:/var/www/django/static"
- "$PWD/db.sqlite3:/home/timetracker/app/db.sqlite3"
- "./data:/home/timetracker/app/data"
- "${DOCKER_STORAGE_PATH:-/tmp}/timetracker/backups:/home/timetracker/app/games/fixtures/backups"
restart: unless-stopped
frontend:
image: caddy
volumes:
- "static-files:/usr/share/caddy:ro"
- "$PWD/Caddyfile:/etc/caddy/Caddyfile"
ports:
- "8000:8000"
depends_on:
- backend
volumes:
static-files:
+18 -18
View File
@@ -1,23 +1,23 @@
#!/bin/bash
# Apply database migrations
set -euo pipefail
echo "Apply database migrations"
python manage.py migrate
echo "Collect static files"
PUID=${PUID:-1000}
PGID=${PGID:-100}
USERHOME=$(grep timetracker /etc/passwd | cut -d ":" -f6)
usermod -d "/root" timetracker
groupmod -o -g "$PGID" timetracker
usermod -o -u "$PUID" timetracker
usermod -d "${USERHOME}" timetracker
mkdir -p /home/timetracker/app/data /var/log/supervisor
chmod 755 /home/timetracker/app
chmod 755 /home/timetracker/app/.venv
chown "$PUID:$PGID" /home/timetracker/app/data
chown "$PUID:$PGID" /var/log/supervisor
python manage.py migrate
python manage.py collectstatic --clear --no-input
_term() {
echo "Caught SIGTERM signal!"
kill -SIGTERM "$gunicorn_pid"
kill -SIGTERM "$django_q_pid"
}
trap _term SIGTERM
echo "Starting Django-Q cluster"
python manage.py qcluster & django_q_pid=$!
echo "Starting app"
python -m gunicorn --bind 0.0.0.0:8001 timetracker.asgi:application -k uvicorn.workers.UvicornWorker --access-logfile - --error-logfile - & gunicorn_pid=$!
wait "$gunicorn_pid" "$django_q_pid"
exec /usr/bin/supervisord -c /etc/supervisor/conf.d/supervisor.conf
+1 -1
View File
@@ -106,7 +106,7 @@
}
});
</script>
// hx-swap-oob makes sure the modal gets removed upon any HTMX response
<!-- hx-swap-oob makes sure the modal gets removed upon any HTMX response -->
<div id="global-modal-container" hx-swap-oob="true"></div>
<div x-data="toastStore()"
+1 -1
View File
@@ -1,6 +1,6 @@
[project]
name = "timetracker"
version = "1.6.1"
version = "1.7.0"
description = "A simple time tracker."
authors = [{ name = "Lukáš Kucharczyk", email = "lukas@kucharczyk.xyz" }]
requires-python = ">=3.13,<4"
+40
View File
@@ -0,0 +1,40 @@
[supervisord]
nodaemon=true
user=root
logfile=/dev/stdout
logfile_maxbytes=0
[program:caddy]
command=/usr/local/bin/caddy run --config /etc/caddy/Caddyfile
directory=/home/timetracker/app
autostart=true
autorestart=true
stderr_logfile=/dev/stderr
stdout_logfile=/dev/stdout
stderr_logfile_maxbytes=0
stdout_logfile_maxbytes=0
user=timetracker
[program:gunicorn]
command=python -m gunicorn --bind 0.0.0.0:8001 timetracker.asgi:application -k uvicorn.workers.UvicornWorker --access-logfile - --error-logfile -
directory=/home/timetracker/app
autostart=true
autorestart=true
stderr_logfile=/dev/stderr
stdout_logfile=/dev/stdout
stderr_logfile_maxbytes=0
stdout_logfile_maxbytes=0
process_name=%(program_name)s
user=timetracker
[program:qcluster]
command=python manage.py qcluster
directory=/home/timetracker/app
autostart=true
autorestart=true
stderr_logfile=/dev/stderr
stdout_logfile=/dev/stdout
stderr_logfile_maxbytes=0
stdout_logfile_maxbytes=0
process_name=%(program_name)s
user=timetracker
+3 -3
View File
@@ -4,7 +4,7 @@ Django settings for timetracker project.
Generated by 'django-admin startproject' using Django 4.1.4.
For more information on this file, see
https://docs.djangoproject.com/en/4.1/topics/settings/
https://docs.djangoproject.com/en/4.1/topics/deployment/checklist/
For the full list of settings and their values, see
https://docs.djangoproject.com/en/4.1/ref/settings/
@@ -110,7 +110,7 @@ WSGI_APPLICATION = "timetracker.wsgi.application"
DATABASES = {
"default": {
"ENGINE": "django.db.backends.sqlite3",
"NAME": BASE_DIR / "db.sqlite3",
"NAME": Path(os.environ.get("DATA_DIR", str(BASE_DIR))) / "db.sqlite3",
"OPTIONS": {
"timeout": 20,
"init_command": "PRAGMA synchronous=FULL; PRAGMA journal_mode=WAL;",
@@ -154,7 +154,7 @@ USE_TZ = True
# https://docs.djangoproject.com/en/4.1/howto/static-files/
STATIC_URL = "static/"
STATIC_ROOT = BASE_DIR / "static" if DEBUG else "/var/www/django/static"
STATIC_ROOT = BASE_DIR / "static"
# Default primary key field type
# https://docs.djangoproject.com/en/4.1/ref/settings/#default-auto-field
Generated
+1 -1
View File
@@ -822,7 +822,7 @@ wheels = [
[[package]]
name = "timetracker"
version = "1.6.1"
version = "1.7.0"
source = { editable = "." }
dependencies = [
{ name = "croniter" },