Returns the current session CSRF token as JSON so the offline queue
replay can obtain a fresh token without parsing HTML. Rate-limited to
30 requests/minute. Passes offline flag through to the thank-you
template.
Displays total entries, this week, this month, and newsletter opt-in
count and percentage at the top of the admin view. Week/month
boundaries are computed in America/Denver time and converted to UTC
for SQLite comparison, handling DST correctly.
Redirects to /thank-you?name=<first_name> on successful submission
instead of back to the blank form. Shows a 4-second countdown with
meta-refresh fallback before returning to the form. Includes the
scrolling guest marquee so the page feels consistent with the kiosk.
Serves /manifest.webmanifest dynamically from Flask so SITE_TITLE and
LOGO_URL env vars flow into the manifest at runtime. Serves /sw.js from
/static/sw.js with Service-Worker-Allowed: / header to allow root scope.
Service worker caches static assets and passes all app routes to network.
Runs aquasecurity/trivy-action after the build step and fails the
workflow if any CRITICAL or HIGH severity vulnerabilities are found,
blocking the push to Docker Hub.
set -e ensures the script aborts on any error (e.g. failed chown)
rather than silently continuing. --timeout 30 kills hung workers to
prevent slow-client attacks from exhausting the worker pool.
Adds FIELD_MAX constants and server-side length checks in the index
route. Adds matching maxlength attributes on all form inputs so the
browser enforces limits before submission.
Installs Flask-WTF and enables CSRFProtect globally. Adds csrf_token
hidden fields to all four POST forms (login, delete entry, add user,
delete user, and the public guestbook form). Exempts the API endpoint
which uses header-based key auth instead.
Sets X-Content-Type-Options, X-Frame-Options, and Referrer-Policy on
all responses. Prevents browsers from caching admin pages. Configures
session cookies as HttpOnly and SameSite=Lax with an 8-hour lifetime.
Limits POST to /admin/login to 10 requests/minute to block brute-force
attacks. Limits GET /api/guests to 100 requests/hour to prevent bulk
data exfiltration.
Raises RuntimeError at startup instead of silently falling back to a
hardcoded default, preventing misconfigured deployments from running
with a publicly-known session key.
Add weekly Dependabot updates for pip, Docker, and GitHub Actions.
Add issue templates for bug reports, feature requests, documentation,
and general feedback.
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