Compare commits

...

6 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
lukas c10b7a8013 Improve make dev-prod
Django CI/CD / test (push) Successful in 30s
Django CI/CD / build-and-push (push) Successful in 1m23s
2026-05-12 15:27:56 +02:00
lukas 103c29e234 Fix missing values for first and last game in stats view
Django CI/CD / test (push) Successful in 22s
Django CI/CD / build-and-push (push) Successful in 53s
2026-05-12 15:12:43 +02:00
17 changed files with 197 additions and 72 deletions
-1
View File
@@ -9,7 +9,6 @@ static
.drone.yml .drone.yml
.editorconfig .editorconfig
.gitignore .gitignore
Caddyfile
CHANGELOG.md CHANGELOG.md
db.sqlite3 db.sqlite3
docker-compose* 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 - name: Run Migrations
run: uv run python manage.py migrate run: uv run python manage.py migrate
# - name: Run Tests - name: Run Tests
# run: PROD=1 uv run pytest run: uv run --with pytest-django pytest
build-and-push: build-and-push:
needs: test needs: test
@@ -33,7 +33,7 @@ jobs:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- name: Set Version - 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 - name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3 uses: docker/setup-buildx-action@v3
+1
View File
@@ -5,6 +5,7 @@ __pycache__
node_modules node_modules
package-lock.json package-lock.json
db.sqlite3 db.sqlite3
data/
/static/ /static/
dist/ dist/
.DS_Store .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 ### 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 ## 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 * 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 * 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) * 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 ### Fixes
+10 -9
View File
@@ -1,14 +1,15 @@
{ {
auto_https off auto_https off
admin off
} }
:8000 { :8000 {
handle_path /static/* { handle_path /static/* {
root * /usr/share/caddy root * /home/timetracker/app/static
file_server file_server
} }
handle { handle /robots.txt {
reverse_proxy backend:8001 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 \ PYTHONUNBUFFERED=1 \
PATH="/home/timetracker/app/.venv/bin:$PATH" PATH="/home/timetracker/app/.venv/bin:$PATH"
RUN useradd -m --uid 1000 timetracker \ RUN apt-get update && apt-get install -y --no-install-recommends \
&& mkdir -p /var/www/django/static \ curl \
&& chown timetracker:timetracker /var/www/django/static 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 WORKDIR /home/timetracker/app
COPY --from=builder --chown=timetracker:timetracker /home/timetracker/app /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 / COPY --chown=timetracker:timetracker entrypoint.sh /
RUN chmod +x /entrypoint.sh RUN chmod +x /entrypoint.sh
USER timetracker ENV VERSION_NUMBER=1.7.0
ENV VERSION_NUMBER=1.6.1
EXPOSE 8000 EXPOSE 8000
CMD [ "/entrypoint.sh" ] ENTRYPOINT ["/entrypoint.sh"]
+5 -1
View File
@@ -40,7 +40,11 @@ caddy:
caddy run --watch caddy run --watch
dev-prod: migrate collectstatic dev-prod: migrate collectstatic
PROD=1 uv run python -m gunicorn --bind 0.0.0.0:8001 timetracker.asgi:application -k uvicorn.workers.UvicornWorker @npx concurrently \
--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: dumpgames:
uv run python manage.py dumpdata --format yaml games --output tracker_fixture.yaml uv run python manage.py dumpdata --format yaml games --output tracker_fixture.yaml
+12 -21
View File
@@ -1,30 +1,21 @@
--- ---
services: services:
backend: timetracker:
image: registry.kucharczyk.xyz/timetracker image: ${REGISTRY_URL:-registry.kucharczyk.xyz}/timetracker:1.7.0
build: build:
context: . context: .
dockerfile: Dockerfile dockerfile: Dockerfile
container_name: timetracker
environment: environment:
- TZ=Europe/Prague - TZ=${TZ:-Europe/Prague}
- CSRF_TRUSTED_ORIGINS="https://tracker.kucharczyk.xyz" - CSRF_TRUSTED_ORIGINS=https://tracker.kucharczyk.xyz
user: "1000" - PUID=${PUID:-1000}
- PGID=${PGID:-100}
- DATA_DIR=${DATA_DIR:-/home/timetracker/app/data}
ports:
- "${TIMETRACKER_EXTERNAL_PORT:-8000}:8000"
volumes: volumes:
- "static-files:/var/www/django/static" - "./data:/home/timetracker/app/data"
- "$PWD/db.sqlite3:/home/timetracker/app/db.sqlite3" - "${DOCKER_STORAGE_PATH:-/tmp}/timetracker/backups:/home/timetracker/app/games/fixtures/backups"
restart: unless-stopped 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 #!/bin/bash
# Apply database migrations
set -euo pipefail 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 python manage.py collectstatic --clear --no-input
_term() { exec /usr/bin/supervisord -c /etc/supervisor/conf.d/supervisor.conf
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"
+1 -1
View File
@@ -106,7 +106,7 @@
} }
}); });
</script> </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 id="global-modal-container" hx-swap-oob="true"></div>
<div x-data="toastStore()" <div x-data="toastStore()"
+2
View File
@@ -196,6 +196,8 @@ def stats_alltime(request: HttpRequest) -> HttpResponse:
first_play_date = "N/A" first_play_date = "N/A"
last_play_date = "N/A" last_play_date = "N/A"
first_play_game = None
last_play_game = None
if this_year_sessions: if this_year_sessions:
first_session = this_year_sessions.earliest() first_session = this_year_sessions.earliest()
first_play_game = first_session.game first_play_game = first_session.game
+1 -1
View File
@@ -1,6 +1,6 @@
[project] [project]
name = "timetracker" name = "timetracker"
version = "1.6.1" version = "1.7.0"
description = "A simple time tracker." description = "A simple time tracker."
authors = [{ name = "Lukáš Kucharczyk", email = "lukas@kucharczyk.xyz" }] authors = [{ name = "Lukáš Kucharczyk", email = "lukas@kucharczyk.xyz" }]
requires-python = ">=3.13,<4" 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. Generated by 'django-admin startproject' using Django 4.1.4.
For more information on this file, see 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 For the full list of settings and their values, see
https://docs.djangoproject.com/en/4.1/ref/settings/ https://docs.djangoproject.com/en/4.1/ref/settings/
@@ -110,7 +110,7 @@ WSGI_APPLICATION = "timetracker.wsgi.application"
DATABASES = { DATABASES = {
"default": { "default": {
"ENGINE": "django.db.backends.sqlite3", "ENGINE": "django.db.backends.sqlite3",
"NAME": BASE_DIR / "db.sqlite3", "NAME": Path(os.environ.get("DATA_DIR", str(BASE_DIR))) / "db.sqlite3",
"OPTIONS": { "OPTIONS": {
"timeout": 20, "timeout": 20,
"init_command": "PRAGMA synchronous=FULL; PRAGMA journal_mode=WAL;", "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/ # https://docs.djangoproject.com/en/4.1/howto/static-files/
STATIC_URL = "static/" 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 # Default primary key field type
# https://docs.djangoproject.com/en/4.1/ref/settings/#default-auto-field # https://docs.djangoproject.com/en/4.1/ref/settings/#default-auto-field
Generated
+1 -1
View File
@@ -822,7 +822,7 @@ wheels = [
[[package]] [[package]]
name = "timetracker" name = "timetracker"
version = "1.6.1" version = "1.7.0"
source = { editable = "." } source = { editable = "." }
dependencies = [ dependencies = [
{ name = "croniter" }, { name = "croniter" },