# 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 `