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.
- Fixed port variable interpolation to use ${PORT:-8000} for a default value.
- Updated volume configuration to use a named volume (guestbook_data) mounted at /data.
- Improved YAML formatting for clarity.
- Hide comment field by default.
- Add JavaScript to reveal comment field when first name, last name, and location have at least 3 characters.
- Update form instructions to inform users about the comment field.
- Display brief instructions above the guestbook form.
- Update validation: require first name, last name, and location; make email optional.
- Remove the 'required' attribute from the email input field.
- Provide context in the UI so users understand why email is optional.