Back to DevLog

Adding an admin users panel and AI call logging to a Flask app

3 min read

I needed visibility into who was signing up and whether the AI feature was working. The admin panel had analytics but no way to see registered users or debug silent AI failures.

Building the users panel

I added a new /admin/users route that shows email, plan, signup date, last login, AI call count, last AI call, and signup source. The table is sortable by email, signup date, last login, and AI call count using an ORDER BY whitelist. Four metric tiles show total accounts, paid accounts, AI calls by logged-in users, and AI calls by anonymous users.

I added a last_login column that updates once per UTC day in the user loader to avoid write overhead.

Capturing signup source

I wanted to know where signups were coming from. I wrote first-touch referrer and landing path to sessionStorage on page load and posted them as hidden fields at registration. A sibling signup_source table stores the data instead of adding columns to users, because the User constructor expects a fixed signature and auth.db is shared with production. Altering the users table would have broken prod immediately.

I used client-side sessionStorage instead of Flask session because writing the session on anonymous GET requests would set a cookie sitewide and make every HTML page uncacheable at nginx and Cloudflare. That was the exact failure mode from an earlier 502 crawler saturation incident.

Logging AI failures

For five months, production AI failures left no trace. The three exception handlers in the answer stream swallowed exceptions without logging. I added a new ai_call_log table and updated all three handlers to log warnings and write rows with the error message.

I added an AI model health strip to /admin/analytics showing the answer model, extract model, calls today, errors in the last 24 hours, last error message, and whether ASK_ENABLED is set. The strip has a red left border when errors are above zero.

I verified the error path by temporarily pointing ASK_MODEL_ANSWER at a nonexistent model, exercising the handler, and reverting. A synthetic INSERT would not have proven the branch fires.

Adding a sitewide Ask AI button

I added an Ask AI button to the header search bar in base.html. It carries the query to /ai-analyst?q=... as a prefill but never auto-submits. I visually separated it from the search form with a small muted "or" so it stops reading as a second submit button.

The AI badge in the nav reuses the exact livePulse keyframes from the LIVE badge for in-sync pulsing. When the input is prefilled via ?q=, the AI send button gets a hot-send accent glow using the same timing. The glow stops on first user interaction and never fires on empty or manually-typed input.

Problems

Two templates showed massive line counts in git diff --stat but small counts with -w. The patch script I wrote read files in Python text mode, so universal-newline translation stripped CRLF before backups were even written. I restored CRLF wholesale, then put back one 3-line LF-only block that was already in HEAD.

When I tried to extract ADMIN_SECRET from /proc/<pid>/environ, I got "No such file or directory" because pgrep-selected worker PIDs had rotated. Only the gunicorn master environ is stable.

The ai_call_log table was empty after the first AI call because the Playwright harness closed the page mid-SSE, killing the generator before post-stream code ran. I fixed it by polling until the rendered answer text stops growing.

Share this post