# CLAUDE.md — AI Developer Context This file gives Claude (or any AI assistant) the context needed to continue development on this project without re-reading the entire codebase from scratch. --- ## What This Project Is **Bid Checker Web** is a Flask web application that is a direct port of a Tkinter desktop application. Both apps share the **same MySQL database** — this is the single most important constraint. Any schema change, query, or encryption logic must remain compatible with what the desktop app writes and reads. The app helps a small team at LT Services Inc. (Falls Church, VA) track government procurement websites across scheduled shifts, manage bid/opportunity follow-up, and run AI-powered solicitation document analysis. --- ## Architecture at a Glance ``` Browser → Nginx (reverse proxy) → Gunicorn (4 workers) → Flask app │ MySQL (shared with desktop) ``` - **Entry point:** `wsgi.py` → `app.py::create_app()` — **only `wsgi.py` calls `create_app()`**; the bottom of `app.py` no longer has a module-level call (that caused double initialisation) - **No ORM** — all DB access is raw SQL via `mysql-connector-python` in `models.py` - **No frontend framework** — vanilla JS, no build step, no npm - **Blueprints:** one file per feature area in `routes/` - **Templates:** Jinja2; most extend `base.html` — **`login.html` is a standalone exception** (see CSRF section) - **Static assets:** single `style.css` + `app.js` — no preprocessor --- ## Critical Constraints ### 1. Shared Database Never rename columns, drop tables, or change column types without verifying the desktop app still works. Key schema facts: - `activity_log` timestamp column is **`logged_at`** (DDL and queries both use `logged_at`; a migration in `config.py` renames `created_at` → `logged_at` for old web-only installs) - `app_log` timestamp column is **`logged_at`** - All other tables generally use `created_at` - The desktop app writes `bid_tracker`, `bid_updates`, `ai_analysis_log`, `ai_criteria`, `app_settings`, `users`, `websites`, `website_credentials`, `shifts`, `shift_users`, `shift_websites`, `shift_checks`, `login_attempts` ### 2. Credential Encryption `utils/crypto.py` must stay byte-for-byte compatible with the desktop's `utils/crypto.py`: - `_APP_SECRET = b"WebsiteChecker-v1-CredentialKey"` — never change - `_ITERATIONS = 100_000` — never change - Salt stored as **base64** in `app_settings` under key `"crypto.salt"` - Ciphertext has **`enc:`** prefix; values without this prefix are legacy plaintext and returned as-is - Calling `reset_fernet()` forces key reload if the salt changes ### 3. No Inline JS with Dynamic Jinja Values All button `onclick` handlers that need dynamic data (site ID, site name, note text) **must use `data-*` attributes** on the HTML element and read them in a delegated event listener. Direct `onclick="fn({{ value }})"` breaks when the value contains quotes, apostrophes, or backslashes. Example of the correct pattern: ```html ``` ```js document.getElementById('list').addEventListener('click', function(e) { const btn = e.target.closest('.js-check'); if (btn) openCheckModal(btn.dataset.id, btn.dataset.name); }); ``` ### 4. Jinja Macros and `{% extends %}` Jinja macros defined in the same file as `{% extends "base.html" %}` cannot be called with `{{ macro_name(...) }}` before the macro definition is reached. **Do not use macros in child templates.** Inline the HTML directly or use JS to build dynamic content. ### 5. CSS Specificity — Local `