Blog

All posts
John Damask · 2026-03-08
devlogfeaturessecurityfeedbackfrontend

Standard Footer on Output Pages

Quick fix: the footer on generated output pages had inconsistent branding text. Updated generator.py to inject a standard footer with a hyperlink back to nowigetit.us on every generated page. Small change, but it means every page that's ever shared links back to the app.


Feedback Thumbs Widget

The Feature

Added a "Was this page helpful?" feedback widget to every generated page. Thumbs up records a positive vote immediately. Thumbs down opens a modal with five category checkboxes (Not accurate, Missing key information, Poorly structured, Page errors, Don't like design) and a free-text textarea, then submits. Votes are stored in a new DynamoDB table via a dedicated Lambda behind API Gateway.

The widget is a standalone feedback-widget.js file hosted on S3 and injected into generated HTML during post-processing (same pattern as the KaTeX injection). It dynamically loads /config.js to get the API base URL, which means it works on both test and prod without code changes. LocalStorage prevents double-voting -- the key uses the full URL path (rating:/NOWIGETIT/abc123.html) so it's self-documenting in browser dev tools.

Security: Sanitize but Don't Forget

The free-text field needed input sanitization before going into DynamoDB. The initial approach stripped HTML tags, control characters, and truncated to 1000 characters -- standard XSS prevention. But that raised a question: if you silently strip malicious input, you're stopping the attack but blind to the attacker.

The solution follows a well-established security pattern: sanitize for safety, log for visibility. The _sanitize_text() function now returns a was_modified flag alongside the cleaned text. If sanitization changed anything -- or if someone submitted categories not in the allowed whitelist -- the record gets flagged: true in DynamoDB, the raw unsanitized input is preserved in a raw_feedback_text field, and a [SECURITY] log line goes to CloudWatch with the IP address, raw text, and what was stripped. Clean submissions get flagged: false with no raw fields, so there's zero storage overhead for legitimate users.

Tested on both environments by submitting <script bad_stuff.js/> in the feedback text. CloudWatch caught it immediately:

[SECURITY] Flagged feedback from ip=203.0.113.42 job_id=868a1cef-...
  text_sanitized=True rejected_categories=[]
  raw_text='Purty gud but not gr8. <script bad_stuff.js/>'

The textarea also enforces the 1000-character limit client-side with a live counter that turns red at 950 characters, matching the server-side MAX_FEEDBACK_LENGTH. The modal was restyled to match the site's dark theme (dark card background, gold accent buttons, DM Sans font) rather than the generic white modal it started as.

Infrastructure

CloudFormation adds: FeedbackData DynamoDB table (partition key: feedback_id, GSIs on job_id and vote+timestamp), FeedbackFunction Lambda, API Gateway route, and IAM permissions. Both deploy scripts updated to package the Lambda and upload the widget JS. The DynamoDB logical resource is named FeedbackData rather than FeedbackTable due to a CloudFormation limitation -- changing a custom-named table's key schema requires replacement, and the two-step rename dance is easier with a distinct logical name.

Deployed to test, verified, then deployed to prod.