Convert UTC timestamps from SQLite to Mountain Time (America/Denver)
using a Jinja2 template filter backed by zoneinfo; add tzdata dependency
for IANA timezone data in the slim Docker image.
Replaces browser-cached Basic Auth credentials with proper server-side
session management. Logout now fully invalidates the session. Adds an
HTML login form at /admin/login, SECRET_KEY env var support, and updates
README with key generation instructions and role table.
Entrypoint now runs as root, chowns the data directory to appuser,
then drops privileges via gosu before starting Gunicorn. This prevents
sqlite3.OperationalError on mounted volumes owned by root.
- Add MIGRATIONS list — each entry is a list of SQL statements for
that schema version; append new lists to add future migrations,
never modify existing ones
- Add schema_version table to track applied migrations
- migrate_db() runs on startup and applies any pending versions
automatically; safe to run against existing DBs (v1 uses
CREATE IF NOT EXISTS so it no-ops on the existing table/indexes)
- entrypoint.sh: use GUNICORN_WORKERS to match example.env (#17)
- guestbook_export.py: read DATABASE_PATH from env instead of
hardcoded relative path (#18)
- Scrolling marquee: duplicate guest list for seamless loop,
animate translateX(0) to translateX(-50%), increase font to
1.25rem, fix JS speed calc to use half content width (#20)
Add a secondary normalized substring check: strips all non-alpha chars
then checks if any banned word appears as a substring. This catches:
- Spacing tricks: 'f u c k'
- Embedded forms: 'fucking'
Note: substring matching can produce false positives (e.g. 'classic'
contains 'ass'). Trade-off accepted for a museum kiosk context.
Swap hand-rolled regex for the email-validator library which handles
RFC 5322 edge cases correctly. check_deliverability=False skips DNS
lookups (not viable on an intranet). Blank email still passes — only
a non-empty, malformed address triggers the error.
Add Flask-Limiter and cap POST submissions to 5 per minute per IP.
GET requests are not limited. Uses in-memory storage (appropriate
for single-instance kiosk deployment).
Wrap all sqlite3 operations in try/except sqlite3.Error:
- SELECT on validation error path: falls back to empty guest list
- INSERT on form submit: shows user-friendly retry message
- SELECT on page load: falls back to empty guest list
- SELECT in /api/guests: returns 503 JSON response
Logging guest name and location is appropriate here: visitors knowingly
submit this info for a newsletter, and the log is useful for confirming
submissions and debugging on an intranet-only kiosk.
- Add idx_guests_id and idx_guests_email indexes in init_db()
- Cap all SELECT queries on the guests table to LIMIT 100 to prevent
unbounded memory growth as the guestbook accumulates entries
Create appuser with configurable UID/GID (default 1000, matching
example.env PID/GID vars) and switch to it before starting Gunicorn.
Override at build time with --build-arg UID=... --build-arg GID=...
Note: the /data volume mount must be owned by the matching UID on the
host for the DB to remain writable.
- Fixed scrolling marquee to use a fixed px/s speed via JS instead of
a fixed duration, preventing it from speeding up as entries are added
- Added inline TODO comments throughout codebase to track known issues
(rate limiting, CSRF, unbounded queries, deprecated Flask decorator,
PII logging, schema versioning, Docker non-root user, etc.)
- Added todo-to-issue GitHub Action to auto-create Issues from TODOs on push to main
- Added .claude/ to .gitignore
Remove guestbook.db from repository
- Deleted guestbook.db, a binary SQLite database file, from the repo.
- Added guestbook.db to .gitignore to prevent storing environment-specific binaries.
- This change enhances security and keeps the repository clean by not tracking generated files.