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.
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.
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.
- 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)
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
- 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
- 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.
- Configure Python logging at INFO level.
- Log key events: database initialization, incoming requests, validation errors, and successful guest submissions.
- Log the number of guest entries displayed when rendering the page.
This improves observability and helps with troubleshooting.
- Validate that first_name, last_name, email, and location are provided
- Add regex-based email format validation in app.py
- Display error messages on the guestbook form if validation fails
These changes help ensure that only properly formatted data is stored.