Compare commits
4 Commits
c10b7a8013
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
2f433c92da
|
|||
|
5b2b79f553
|
|||
|
36411c99a7
|
|||
| 360e8f9eaf |
@@ -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*
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
@@ -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
@@ -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"]
|
||||||
|
|||||||
@@ -41,9 +41,10 @@ caddy:
|
|||||||
|
|
||||||
dev-prod: migrate collectstatic
|
dev-prod: migrate collectstatic
|
||||||
@npx concurrently \
|
@npx concurrently \
|
||||||
--names "Django,Django-Q" \
|
--names "Caddy,Django,Django-Q" \
|
||||||
"PROD=1 uv run python -m gunicorn --bind 0.0.0.0:8001 timetracker.asgi:application -k uvicorn.workers.UvicornWorker"
|
"caddy run --config Caddyfile.dev" \
|
||||||
"uv run manage.py qcluster"
|
"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
@@ -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
@@ -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"
|
|
||||||
|
|||||||
@@ -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()"
|
||||||
|
|||||||
+1
-1
@@ -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"
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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" },
|
||||||
|
|||||||
Reference in New Issue
Block a user