diff --git a/.claude/projects/-home-chriscarrollsmith-Documents-Software-Websites-fastapi-jinja2-postgres-webapp/memory/MEMORY.md b/.claude/projects/-home-chriscarrollsmith-Documents-Software-Websites-fastapi-jinja2-postgres-webapp/memory/MEMORY.md new file mode 100644 index 0000000..8268c36 --- /dev/null +++ b/.claude/projects/-home-chriscarrollsmith-Documents-Software-Websites-fastapi-jinja2-postgres-webapp/memory/MEMORY.md @@ -0,0 +1,3 @@ +# Memory Index + +- [feedback_red_green_testing.md](feedback_red_green_testing.md) — Always write a failing test before fixing a bug diff --git a/.claude/projects/-home-chriscarrollsmith-Documents-Software-Websites-fastapi-jinja2-postgres-webapp/memory/feedback_red_green_testing.md b/.claude/projects/-home-chriscarrollsmith-Documents-Software-Websites-fastapi-jinja2-postgres-webapp/memory/feedback_red_green_testing.md new file mode 100644 index 0000000..876a53f --- /dev/null +++ b/.claude/projects/-home-chriscarrollsmith-Documents-Software-Websites-fastapi-jinja2-postgres-webapp/memory/feedback_red_green_testing.md @@ -0,0 +1,11 @@ +--- +name: Red-green testing discipline +description: Always write a failing test (red phase) before applying a bug fix so the fix is validated +type: feedback +--- + +Always write a failing test that surfaces the bug before applying the fix. Running tests after a fix without a red-phase test doesn't prove anything. + +**Why:** The user expects disciplined red-green-refactor workflow. A test that only exists after the fix could be passing for the wrong reason. + +**How to apply:** When fixing a bug, first add or update a test that fails with the current code, confirm it fails, then apply the fix and confirm it passes. diff --git a/main.py b/main.py index 375d9a3..3741b53 100644 --- a/main.py +++ b/main.py @@ -13,7 +13,7 @@ require_unauthenticated_client ) from utils.core.auth import COOKIE_SECURE -from utils.core.htmx import is_htmx_request, toast_response +from utils.core.htmx import is_htmx_request, toast_response, get_flash_cookie, FLASH_COOKIE_NAME from exceptions.http_exceptions import ( AlreadyAuthenticatedError, AuthenticationError, @@ -47,6 +47,22 @@ async def lifespan(app: FastAPI): templates = Jinja2Templates(directory="templates") +# --- Flash cookie middleware --- +# Reads the flash cookie into request.state so templates can render it +# server-side, then clears the cookie on the response. + + +@app.middleware("http") +async def flash_cookie_middleware(request: Request, call_next): + flash = get_flash_cookie(request) + request.state.flash = flash + response = await call_next(request) + if flash: + response.delete_cookie(FLASH_COOKIE_NAME, path="/") + return response + + + # --- Include Routers --- diff --git a/routers/core/user.py b/routers/core/user.py index 6d6e77c..e47ec9e 100644 --- a/routers/core/user.py +++ b/routers/core/user.py @@ -116,6 +116,7 @@ async def update_profile( # Handle avatar update if avatar_changed: + assert avatar_file is not None avatar_data = await avatar_file.read() avatar_content_type = avatar_file.content_type @@ -141,17 +142,21 @@ async def update_profile( session.refresh(user) if is_htmx_request(request): - if avatar_changed: - # Avatar affects the navbar, which is outside the swap target. - # Tell HTMX to do a full page refresh so everything updates. - response = Response(status_code=200) - response.headers["HX-Refresh"] = "true" - return response response = templates.TemplateResponse( request, "users/partials/profile_display.html", {"user": user}, ) + if avatar_changed: + # Avatar also appears in the navbar — append an OOB swap for it. + navbar_html = bytes(templates.TemplateResponse( + request, + "base/partials/navbar_avatar_oob.html", + {"user": user}, + ).body).decode() + original = bytes(response.body).decode() + response.body = (original + navbar_html).encode() + response.headers["content-length"] = str(len(response.body)) return append_toast(response, request, templates, "Profile updated successfully.") return RedirectResponse(url=router.url_path_for("read_profile"), status_code=303) diff --git a/static/js/app.js b/static/js/app.js index 2b60d96..8d66d4d 100644 --- a/static/js/app.js +++ b/static/js/app.js @@ -3,58 +3,19 @@ // re-processed during htmx hx-boost body swaps, which means the event // listeners registered here persist across page navigations. -function showToast(message, level) { - level = level || 'success'; - var container = document.getElementById('toast-container'); - var wrapper = document.createElement('div'); - wrapper.className = 'toast align-items-center text-bg-' + level + ' border-0 show'; - wrapper.setAttribute('role', 'alert'); - wrapper.setAttribute('aria-atomic', 'true'); - wrapper.innerHTML = - '