Skip to content

Commit 712fef8

Browse files
jssturmcursoragent
andcommitted
feat(feedback): private BugDrop intake, docked control, a11y, and asset fingerprints
Add account-free BugDrop reporting to elevated-applicant-feedback with travel-data masking and GitHub fallback, move the report control into the sidebar footer, raise muted-text contrast to WCAG AA, and fingerprint CSS/JS so style updates stop requiring a hard refresh. Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent 529afc3 commit 712fef8

15 files changed

Lines changed: 460 additions & 74 deletions

.cursor/rules/zoo-anti-loop-shield.mdc

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
description: Anti-Loop & Anti-Hallucination Shield (Zoo mandate)
33
alwaysApply: true
44
source: .roo/rules/11-anti-loop-hallucination-shield.md
5-
syncedAt: 2026-07-27T17:34:09.037Z
5+
syncedAt: 2026-07-29T13:43:43.180Z
66
---
77

88
# Anti-Loop & Anti-Hallucination Shield

.cursor/rules/zoo-paperclip-munger.mdc

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
description: Paperclip + Munger Operating Model (Zoo mandate)
33
alwaysApply: true
44
source: .roo/rules/08-paperclip-munger-operating-model.md
5-
syncedAt: 2026-07-27T17:34:09.036Z
5+
syncedAt: 2026-07-29T13:43:43.179Z
66
---
77

88
# Paperclip + Munger Operating Model

.cursor/rules/zoo-path-safety.mdc

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
description: Path Safety (Zoo mandate)
33
alwaysApply: true
44
source: .roo/rules/00-paths.md
5-
syncedAt: 2026-07-27T17:34:09.031Z
5+
syncedAt: 2026-07-29T13:43:43.170Z
66
---
77

88
# Path Safety

.cursor/rules/zoo-pre-response-checklist.mdc

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
description: Pre-Response Self-Audit (Zoo mandate)
33
alwaysApply: true
44
source: .roo/rules/10-pre-response-checklist.md
5-
syncedAt: 2026-07-27T17:34:09.036Z
5+
syncedAt: 2026-07-29T13:43:43.180Z
66
---
77

88
# Pre-Response Self-Audit

.cursor/rules/zoo-three-failure-rule.mdc

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
description: Three-Failure Rule (Zoo mandate)
33
alwaysApply: true
44
source: .roo/rules/02-three-failure-rule.md
5-
syncedAt: 2026-07-27T17:34:09.034Z
5+
syncedAt: 2026-07-29T13:43:43.176Z
66
---
77

88
# Three-Failure Rule

app/main.py

Lines changed: 28 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,13 @@
33
import copy
44
import logging
55
import os
6+
import re
67
from contextlib import asynccontextmanager
78
from typing import Optional
89

910
from fastapi import Depends, FastAPI, HTTPException, Request
1011
from fastapi.middleware.cors import CORSMiddleware
11-
from fastapi.responses import FileResponse
12+
from fastapi.responses import FileResponse, HTMLResponse
1213
from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer
1314
from fastapi.staticfiles import StaticFiles
1415
from pydantic import BaseModel, Field
@@ -397,6 +398,30 @@ def start_day(request: Request, payload: TravelRequest) -> dict:
397398
# Static file serving (mounted after API routes so they take precedence)
398399
# ---------------------------------------------------------------------------
399400
_static_dir = os.path.join(os.path.dirname(__file__), "..", "static")
401+
402+
# Asset URLs in served HTML get a fingerprint query so a browser refetches CSS/JS
403+
# only when the file actually changes. Without it, cached styles survive updates.
404+
_ASSET_REF = re.compile(r'(?P<attr>href|src)="(?P<path>/(?:css|js)/[^"?]+)"')
405+
406+
407+
def _asset_fingerprint(url_path: str) -> str:
408+
try:
409+
return str(int(os.path.getmtime(os.path.join(_static_dir, url_path.lstrip("/")))))
410+
except OSError:
411+
return "0"
412+
413+
414+
def _html_with_versioned_assets(path: str) -> HTMLResponse:
415+
with open(path, encoding="utf-8") as handle:
416+
html = handle.read()
417+
html = _ASSET_REF.sub(
418+
lambda m: f'{m.group("attr")}="{m.group("path")}?v={_asset_fingerprint(m.group("path"))}"',
419+
html,
420+
)
421+
# The document must revalidate, otherwise the new fingerprints stay invisible.
422+
return HTMLResponse(html, headers={"Cache-Control": "no-cache"})
423+
424+
400425
if os.path.isdir(_static_dir):
401426
app.mount("/static", StaticFiles(directory=_static_dir), name="static")
402427

@@ -405,7 +430,7 @@ async def serve_app():
405430
"""Serve the main app UI."""
406431
app_path = os.path.join(_static_dir, "app.html")
407432
if os.path.isfile(app_path):
408-
return FileResponse(app_path)
433+
return _html_with_versioned_assets(app_path)
409434
return {"status": "ok", "note": "Plan-It — app UI not found"}
410435

411436
@app.get("/{full_path:path}", dependencies=[], include_in_schema=False)
@@ -419,7 +444,7 @@ async def spa_fallback(full_path: str):
419444
# SPA fallback — serve index.html for client-side routes
420445
index_path = os.path.join(_static_dir, "index.html")
421446
if os.path.isfile(index_path):
422-
return FileResponse(index_path)
447+
return _html_with_versioned_assets(index_path)
423448
return {"status": "ok", "note": "Plan-It API — static UI not found"}
424449
else:
425450
@app.get("/", include_in_schema=False)

static/app.html

Lines changed: 32 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -19,14 +19,6 @@
1919
<!-- Sidebar overlay for mobile -->
2020
<div id="sidebar-overlay" class="sidebar-overlay"></div>
2121

22-
<!-- Bug Report FAB (floating action button) -->
23-
<div id="bug-report-fab" class="bug-report-fab">
24-
<span class="bug-report-fab-badge">· alpha</span>
25-
<button id="btn-bug-report" class="bug-report-fab-btn" data-i18n-html="bug.fab">
26-
🐛 Report a bug
27-
</button>
28-
</div>
29-
3022
<!-- =======================================================================
3123
App Shell
3224
======================================================================= -->
@@ -58,7 +50,7 @@
5850
</button>
5951

6052
<div class="sidebar-nav-label mt-4" data-i18n="nav.savedPlan">Saved Plan</div>
61-
<div id="sidebar-saved-plans">
53+
<div id="sidebar-saved-plans" data-bugdrop-mask>
6254
<div class="text-xs text-muted" style="padding: var(--space-2) var(--space-3);" data-i18n="nav.noSavedPlans">
6355
No saved plans yet
6456
</div>
@@ -80,6 +72,13 @@
8072
<span class="theme-toggle-label" id="theme-toggle-label">Dark</span>
8173
</button>
8274
</div>
75+
<div id="bug-report-fab" class="bug-report-fab">
76+
<span class="bug-report-fab-badge" data-i18n="bug.sectionLabel">Feedback · alpha</span>
77+
<button id="btn-bug-report" class="bug-report-fab-btn" data-i18n-html="bug.fab">
78+
🐛 Report a bug
79+
</button>
80+
</div>
81+
8382
<div class="health-indicator">
8483
<span id="health-dot" class="health-dot"></span>
8584
<span id="health-label" data-i18n="health.checking">API checking...</span>
@@ -113,7 +112,7 @@ <h2 data-i18n="newTrip.title">Plan Your Trip</h2>
113112
<div class="card" style="max-width:720px;">
114113
<div class="form-group">
115114
<label class="form-label" for="trip-input" data-i18n="newTrip.whatsYourTrip">What's your trip?</label>
116-
<textarea id="trip-input" class="form-textarea" rows="8"
115+
<textarea id="trip-input" class="form-textarea" rows="8" data-bugdrop-mask
117116
data-i18n-placeholder="newTrip.tripPlaceholder"
118117
placeholder="e.g. Drive to Kennedy Space Center from Orlando tomorrow — stop for lunch, stay overnight at a hotel near Cocoa Beach, and drive back the next morning. Need long-term parking at KSC."></textarea>
119118
<div class="form-hint" data-i18n="newTrip.tripHint">Include as many details as possible — venue, date, mode of travel (drive/fly), meal stops, hotel stays, long-term parking, return trip, and any special interests. The more you tell us, the better your itinerary.</div>
@@ -122,13 +121,13 @@ <h2 data-i18n="newTrip.title">Plan Your Trip</h2>
122121
<div class="grid-2">
123122
<div class="form-group">
124123
<label class="form-label" for="trip-departure" data-i18n="newTrip.departureTime">Departure Time</label>
125-
<div class="time-input-group">
124+
<div class="time-input-group" data-bugdrop-mask>
126125
<input id="trip-departure-hh" class="form-input time-input-hh" type="text"
127126
placeholder="07" maxlength="2" autocomplete="off" />
128127
<span class="time-colon">:</span>
129128
<input id="trip-departure-mm" class="form-input time-input-mm" type="text"
130129
placeholder="00" maxlength="2" autocomplete="off" />
131-
<div class="ampm-toggle" id="trip-departure-ampm">
130+
<div class="ampm-toggle" id="trip-departure-ampm" data-bugdrop-mask>
132131
<button type="button" class="ampm-btn active" data-period="AM">AM</button>
133132
<button type="button" class="ampm-btn" data-period="PM">PM</button>
134133
</div>
@@ -137,15 +136,15 @@ <h2 data-i18n="newTrip.title">Plan Your Trip</h2>
137136
</div>
138137
<div class="form-group">
139138
<label class="form-label" for="trip-start" data-i18n="newTrip.startingLocation">Starting Location (Street Address, City, State, Zip Code)</label>
140-
<input id="trip-start" class="form-input" type="text"
139+
<input id="trip-start" class="form-input" type="text" data-bugdrop-mask
141140
data-i18n-placeholder="newTrip.startPlaceholder"
142141
placeholder="e.g. 9801 International Dr, Orlando, Florida 32819" />
143142
<div class="form-hint" data-i18n="newTrip.startHint">Required if provided — must include street address, city, state, and zip code, separated by commas.</div>
144143
</div>
145144
</div>
146145
<div class="form-group">
147146
<label class="form-label" for="trip-restaurants" data-i18n="newTrip.restaurantPrefs">Restaurant Preferences</label>
148-
<input id="trip-restaurants" class="form-input" type="text"
147+
<input id="trip-restaurants" class="form-input" type="text" data-bugdrop-mask
149148
data-i18n-placeholder="newTrip.restaurantPlaceholder"
150149
placeholder="e.g. vegetarian, Italian, $$-$$$ range" />
151150
<div class="form-hint" data-i18n="newTrip.restaurantHint">Diet, cuisine, or price preferences for meal stops.</div>
@@ -165,7 +164,7 @@ <h2 data-i18n="newTrip.title">Plan Your Trip</h2>
165164
</div>
166165

167166
<!-- Result area (hidden until plan is generated) -->
168-
<div id="result-new-trip" class="hidden" style="margin-top: var(--space-8);"></div>
167+
<div id="result-new-trip" class="hidden" style="margin-top: var(--space-8);" data-bugdrop-mask></div>
169168
</section>
170169

171170
<!-- Page: My Plans -->
@@ -174,7 +173,7 @@ <h2 data-i18n="newTrip.title">Plan Your Trip</h2>
174173
<h2 data-i18n="myPlans.title">My Plans</h2>
175174
<p data-i18n="myPlans.subtitle">Previously generated itineraries stored in this session.</p>
176175
</div>
177-
<div id="plans-list"></div>
176+
<div id="plans-list" data-bugdrop-mask></div>
178177
<div id="plans-empty" class="empty-state hidden">
179178
<div class="empty-state-icon">&#128203;</div>
180179
<div class="empty-state-title" data-i18n="myPlans.emptyTitle">No saved plans</div>
@@ -186,10 +185,10 @@ <h2 data-i18n="myPlans.title">My Plans</h2>
186185
<section id="page-plan-detail" class="page hidden">
187186
<div class="page-header">
188187
<button class="btn btn-sm btn-ghost mb-2" id="btn-back-plans">&#8592; <span data-i18n="myPlans.title">Back to Plans</span></button>
189-
<h2 id="plan-detail-title" data-i18n="topbar.itinerary">Itinerary</h2>
190-
<p id="plan-detail-subtitle"></p>
188+
<h2 id="plan-detail-title" data-i18n="topbar.itinerary" data-bugdrop-mask>Itinerary</h2>
189+
<p id="plan-detail-subtitle" data-bugdrop-mask></p>
191190
</div>
192-
<div id="plan-detail-content"></div>
191+
<div id="plan-detail-content" data-bugdrop-mask></div>
193192
</section>
194193
</main>
195194
</div>
@@ -198,13 +197,26 @@ <h2 id="plan-detail-title" data-i18n="topbar.itinerary">Itinerary</h2>
198197
<div id="toast-container" class="toast-container"></div>
199198

200199
<!-- Modal Container (rendered dynamically) -->
201-
<div id="modal-container"></div>
200+
<div id="modal-container" data-bugdrop-mask></div>
202201

203202
<!-- =======================================================================
204203
Application JavaScript
205204
======================================================================= -->
206205
<script src="/js/i18n.js"></script>
207206
<script src="/js/app.js"></script>
207+
<script
208+
id="planit-bugdrop"
209+
src="https://bugdrop.neonwatty.workers.dev/widget.v1.js"
210+
data-repo="jssturm/elevated-applicant-feedback"
211+
data-button="false"
212+
data-theme="auto"
213+
data-screenshot="optional"
214+
data-send-console-logs="false"
215+
data-show-name="false"
216+
data-show-email="false"
217+
data-show-issue-link="never"
218+
data-welcome="Describe the Plan-It problem. Screenshots are optional and are sent privately to our feedback intake through BugDrop."
219+
></script>
208220

209221
<script>
210222
// Only load Vercel Analytics in production (skip local/dev 404 noise)

static/css/app.css

Lines changed: 30 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,8 @@
2020

2121
--color-text-primary: #111827;
2222
--color-text-secondary: #5f6675;
23-
--color-text-muted: #9098a8;
23+
/* WCAG 2.1 AA: 4.5:1 against the darkest light surface (--color-bg-chip). */
24+
--color-text-muted: #656c7a;
2425
--color-text-inverse: #ffffff;
2526

2627
--color-accent: #4f5ef5;
@@ -110,7 +111,8 @@ html.dark {
110111

111112
--color-text-primary: #e8eaed;
112113
--color-text-secondary: #9aa0a8;
113-
--color-text-muted: #6b7280;
114+
/* WCAG 2.1 AA: 4.5:1 against the lightest dark surface (--color-bg-chip). */
115+
--color-text-muted: #8b92a0;
114116
--color-text-inverse: #0f1117;
115117

116118
--color-accent: #6b78f7;
@@ -182,8 +184,6 @@ html.dark .ea-tooltip.flip::after {
182184
border-bottom-color: #334155;
183185
}
184186

185-
html.dark .bug-report-fab { background: #1e293b; border-color: #334155; }
186-
187187
/* --------------------------------------------------------------------------
188188
Reset & Base
189189
-------------------------------------------------------------------------- */
@@ -1984,50 +1984,48 @@ ul, ol {
19841984
}
19851985

19861986
/* --------------------------------------------------------------------------
1987-
Bug Report FAB (floating action button)
1987+
Bug Report control (docked in the sidebar footer)
19881988
-------------------------------------------------------------------------- */
19891989
.bug-report-fab {
1990-
position: fixed;
1991-
bottom: var(--space-4);
1992-
left: var(--space-4);
1993-
z-index: 70;
1994-
display: flex;
1995-
align-items: center;
1996-
gap: var(--space-2);
1997-
background: var(--color-bg-card);
1998-
border: 1px solid var(--color-border);
1999-
border-radius: 9999px;
2000-
padding: 0.35rem 0.35rem 0.35rem 0.75rem;
2001-
box-shadow: var(--shadow-elevated);
2002-
backdrop-filter: blur(8px);
1990+
margin-bottom: var(--space-3);
20031991
}
20041992

20051993
.bug-report-fab-badge {
1994+
display: block;
20061995
font-size: var(--text-xs);
20071996
font-weight: 600;
1997+
text-transform: uppercase;
1998+
letter-spacing: 0.05em;
20081999
color: var(--color-text-muted);
2009-
white-space: nowrap;
2000+
margin-bottom: var(--space-1);
20102001
}
20112002

20122003
.bug-report-fab-btn {
2013-
display: inline-flex;
2004+
display: flex;
20142005
align-items: center;
2015-
gap: 0.25rem;
2016-
background: var(--color-accent-muted);
2017-
color: var(--color-accent);
2018-
border: none;
2019-
border-radius: 9999px;
2020-
padding: 0.25rem 0.75rem;
2021-
font-size: var(--text-xs);
2022-
font-weight: 600;
2006+
gap: var(--space-2);
2007+
width: 100%;
2008+
padding: var(--space-2) var(--space-3);
2009+
background: var(--color-bg-input);
2010+
border: 1px solid var(--color-border);
2011+
border-radius: var(--radius-sm);
2012+
font-size: var(--text-sm);
2013+
font-family: var(--font-sans);
2014+
font-weight: 500;
2015+
color: var(--color-text-primary);
20232016
cursor: pointer;
2024-
transition: background var(--transition-fast), color var(--transition-fast);
2025-
white-space: nowrap;
2017+
transition: border-color var(--transition-fast), background var(--transition-fast);
20262018
}
20272019

20282020
.bug-report-fab-btn:hover {
2029-
background: var(--color-accent);
2030-
color: var(--color-text-inverse);
2021+
border-color: var(--color-text-muted);
2022+
background: var(--color-bg-chip);
2023+
}
2024+
2025+
.bug-report-fab-btn:focus-visible {
2026+
outline: none;
2027+
border-color: var(--color-border-focus);
2028+
box-shadow: 0 0 0 3px var(--color-accent-muted);
20312029
}
20322030

20332031
/* Bug report modal — review payload before sending */
@@ -2062,12 +2060,6 @@ ul, ol {
20622060
font-size: 0.85rem;
20632061
}
20642062

2065-
/* Responsive — hide badge on very small screens */
2066-
@media (max-width: 480px) {
2067-
.bug-report-fab-badge {
2068-
display: none;
2069-
}
2070-
}
20712063

20722064
/* --------------------------------------------------------------------------
20732065
Crowd Prediction Banner

static/index.html

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -320,9 +320,6 @@ <h2>From a Sentence to a Complete Itinerary</h2>
320320

321321
<footer>
322322
<p>
323-
<strong>Plan-It</strong> — Open source under MIT License.
324-
</p>
325-
<p style="margin-top:0.75rem">
326323
<a href="https://elevated-applicant.vercel.app/career-portfolio" style="font-weight:600">👤 Jeff Sturm — Career Portfolio</a>
327324
&middot;
328325
<a href="https://www.linkedin.com/in/jeff-sturm-8830aa27/" target="_blank" rel="noopener">LinkedIn Profile ↗</a>

0 commit comments

Comments
 (0)