Files
WebChecker/CLAUDE.md
T

469 lines
20 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# CLAUDE.md — Website Checker: Complete Technical Knowledge Base
This document captures the full architecture, design decisions, database schema, module inventory,
feature set, and operational notes for the **Website Checker** desktop application.
---
## 1. Application Overview
Website Checker is a Python/Tkinter desktop application for shift-based website monitoring.
Regular users log in, view websites assigned to their shift, open each site, and mark it checked.
Administrators manage users, websites, shifts, credentials, and run reports.
- **UI:** Python `tkinter` + `ttk`
- **Database:** Remote MySQL 5.7+ / MariaDB 10.3+ via `mysql-connector-python`
- **Entry point:** `app.py``class App(tk.Tk)`
- **Default window:** 1100×700, minimum 960×620
- **Default theme:** Light (toggleable to Dark)
---
## 2. Project File Structure
```
website_checker/
├── app.py Entry point, shell, navigation, session management
├── config.py DB config, connection pool, schema DDL, migrations
├── models.py All database access (data layer)
├── requirements.txt
├── CLAUDE.md This file
├── utils/
│ ├── config_crypto.py Windows DPAPI encryption for config.ini credentials
│ ├── crypto.py Fernet credential encryption (website passwords)
│ ├── export.py CSV + Excel export
│ ├── scheduler.py Daily email report background daemon
│ └── ui_helpers.py ThemeManager, COLOURS proxy, DateEntry, widgets
└── views/
├── admin_dashboard_view.py Admin home — KPI cards + per-user progress table
├── admin_log_view.py Activity log treeview
├── admin_shifts_view.py Shift CRUD + PDF export
├── admin_users_view.py User CRUD
├── admin_websites_view.py Website CRUD + visibility + credentials (collapsible)
├── ai_summary_view.py AI document analysis via Groq API (NEW)
├── change_password_view.py Self-service password change (all roles)
├── email_settings_view.py SMTP / scheduled report configuration
├── login_view.py Login form with rate-limiting countdown
├── reports_view.py Shift Detail / Unchecked / Summary / Chart tabs
├── settings_view.py DB connection settings dialog
└── user_dashboard_view.py User checklist — search, bulk check, notifications
```
---
## 3. Runtime Files
### `config.ini`
Created on first launch. Contains [database], [email], [crypto], and [groq] sections.
**Sensitive fields are DPAPI-encrypted** (see §6 Security). Encrypted values have a `dpapi:` prefix.
```ini
[database]
host=your-mysql-host
port=3306
database=website_checker
user=dpapi:<base64-blob> ; encrypted
password=dpapi:<base64-blob> ; encrypted
[email]
enabled=false
smtp_host=smtp.example.com
smtp_port=587
smtp_user=sender@example.com
smtp_password=dpapi:<base64-blob> ; encrypted
use_tls=true
recipients=admin@example.com
send_time=18:00
[groq]
api_key=dpapi:<base64-blob> ; encrypted
model=llama-3.3-70b-versatile
[crypto]
salt=<base64 32-byte salt — auto-generated>
```
### `app.log`
UTF-8 log file in working directory. All INFO/WARNING/ERROR from every module.
---
## 4. Database Schema
All tables use InnoDB/utf8mb4. Created by `initialize_database()` on first launch.
Safe ALTER TABLE migrations run automatically for columns added post-deployment.
### users
| Column | Type | Notes |
|---|---|---|
| id | INT PK AUTO | |
| username | VARCHAR(100) UNIQUE | |
| password | VARCHAR(255) | bcrypt hash. Legacy SHA-256 (64 hex) auto-migrated on login |
| role | ENUM('admin','user') | |
| full_name | VARCHAR(200) | |
| is_active | TINYINT(1) | |
| failed_attempts | TINYINT UNSIGNED | Incremented on bad login; reset on success |
| locked_until | DATETIME NULL | Set when failed_attempts >= MAX_FAILED_ATTEMPTS (5) |
| created_at / updated_at | DATETIME | |
### websites
| Column | Type | Notes |
|---|---|---|
| id | INT PK AUTO | |
| name | VARCHAR(200) | |
| url | TEXT | |
| check_type | ENUM('daily','weekly') | weekly = once per ISO week |
| visibility | ENUM('all','assigned') | assigned = only users in website_users table |
| note | TEXT | Shown on user dashboard card |
| is_active | TINYINT(1) | Soft-delete |
| created_by | INT FK users SET NULL | |
### website_credentials
| Column | Notes |
|---|---|
| website_id FK | |
| username | Plaintext |
| password | Fernet-encrypted with `enc:` prefix |
| label | e.g. "Admin", "Read-only" |
### shift_checks
One row per user per site per day (upserted). `user_note` optional.
### activity_log
`action` codes: LOGIN, LOGOUT, SESSION_TIMEOUT, ACCOUNT_LOCKED, CREATE/UPDATE/DELETE_USER,
CHANGE_PASSWORD, CHANGE_PASSWORD_FAIL, RESET_PASSWORD, CREATE/UPDATE/DELETE_WEBSITE,
ADD/REMOVE_CREDENTIAL, CREATE/UPDATE/DELETE_SHIFT, CHECK_WEBSITE, UPDATE_NOTE,
EXPORT_CSV, EXPORT_EXCEL, EXPORT_SHIFT_PDF, UPDATE_EMAIL_SETTINGS,
CREATE/UPDATE/DELETE_AI_CRITERION.
### shifts
`days_of_week` is a digit string using MySQL DAYOFWEEK: 1=Sun 2=Mon 3=Tue 4=Wed 5=Thu 6=Fri 7=Sat.
E.g. "23456" = MonFri. Queried with `LOCATE(DAYOFWEEK(CURDATE()), days_of_week) > 0`.
### shift_users (junction): (shift_id, user_id) PK, both CASCADE
### shift_websites (junction): (shift_id, website_id) PK + sort_order
### login_attempts: username, attempted_at, ip_address (indexed)
### website_users (junction): (website_id, user_id) PK — for visibility='assigned'
### ai_criteria
Stores evaluation criteria used by the AI to assess opportunity alignment.
| Column | Type | Notes |
|---|---|---|
| id | INT PK AUTO | |
| title | VARCHAR(200) | Short label |
| description | TEXT | Full criterion text sent to the AI |
| is_active | TINYINT(1) | Inactive criteria excluded from AI prompt |
| sort_order | INT | Controls display and prompt ordering |
| created_by | INT FK users SET NULL | |
### ai_analysis_log
Persists every AI analysis result for history and audit purposes.
| Column | Type | Notes |
|---|---|---|
| id | INT PK AUTO | |
| user_id | INT FK users SET NULL | |
| file_names | TEXT | Comma-separated filenames analyzed |
| model | VARCHAR(100) | Groq model used |
| verdict | ENUM('PURSUE','PASS','UNCLEAR') NULL | NULL when no criteria active |
| criteria_snapshot | TEXT NULL | Active criteria text at time of analysis |
| summary_text | MEDIUMTEXT | Full AI response |
| analyzed_at | DATETIME | |
---
## 5. Module Details
### config.py
- DB_CONFIG loaded from config.ini at import; placeholders if file missing
- get_connection() → pool (pool_size=5)
- initialize_database() → idempotent DDL + ALTER TABLE migrations
- load_config() / save_config() → decrypt/encrypt DPAPI fields transparently
- migrate_plaintext_config() → one-time migration; encrypts any plain-text creds on startup
- config_exists() → True if host, database, user are set
- reload_db_config() → re-reads config.ini, resets pool
### utils/config_crypto.py *(NEW)*
Windows DPAPI-based encryption for config.ini sensitive values.
- encrypt_value(plaintext) → "dpapi:<base64>" string
- decrypt_value(stored) → plaintext; plain-text pass-through for legacy values
- is_encrypted(value) → True if value starts with "dpapi:"
- Tied to the current Windows user account — blob is unreadable on any other machine/account
- Graceful fallback: if pywin32 not available, values stored/returned as plain text
### models.py
Each function opens+closes its own connection. All writes call log_action().
Password helpers:
- _hash_password(pw) → bcrypt rounds=12
- _verify_password(pw, stored) → handles bcrypt + legacy SHA-256
- _needs_rehash(stored) → True for 64-char hex (SHA-256)
Rate-limit constants: MAX_FAILED_ATTEMPTS=5, LOCKOUT_MINUTES=15
Password strength: PW_MIN_LENGTH=8, requires upper + digit + special char
### utils/crypto.py
- PBKDF2-HMAC-SHA256, 100,000 iterations, 32-byte salt from config.ini [crypto]
- encrypt(plaintext) → "enc:" + base64(Fernet token)
- decrypt(ciphertext) → plaintext; legacy plaintext (no enc: prefix) passes through unchanged
- reset_fernet() → force key reload after config.ini replacement
### utils/ui_helpers.py
- ThemeManager singleton — initial="light". toggle(rebuild_callback) flips theme.
- COLOURS = _ColourProxy(dict) — always delegates to ThemeManager.get(). Import once, always current.
- THEMES["dark"] and THEMES["light"] each have 18 colour keys + 4 cal_* keys
- DateEntry — Frame subclass; .get() → "YYYY-MM-DD", .set(str). Calendar popup with prev/next month+year.
- make_scrollable_frame() — mousewheel scoped to Enter/Leave to prevent stale-widget crashes
- scrolled_text(parent, height, width) → (frame, tk.Text)
### utils/scheduler.py
- Daemon thread; polls every 60 seconds
- Sends HTML email once per day when now >= send_time
- smtp_password is DPAPI-encrypted on save (encrypt_value) / decrypted on load (decrypt_value)
- last_sent_date in-memory (resets on restart)
- start() / stop() called from app.py on login/logout (admin only)
### views/ai_summary_view.py
AI-powered document analysis panel. Accessible from the sidebar for both admin and user roles.
Key internals:
- Supported file types: .txt, .md, .csv, .pdf, .docx, .doc, .xlsx, .xls
- Groq API key and model selection are **visible only to admins** (user role sees neither)
- Regular users use the stored API key transparently
- Background thread for AI calls (prevents UI freeze)
- Extraction prompt focused on procurement/solicitation fields:
Solicitation Number/Type, Set-Aside, Description, Work Site, Pre-Proposal Conference,
POC, Square Footage, Driving Distance from office (2815 Hartland Rd, Falls Church VA),
Last Day for Questions, Due Date
- Overall summary covers: Scope of Work, Contract Period, Proposal Submission Requirements,
Key Deadlines & Action Items
- Groq API key stored DPAPI-encrypted in config.ini [groq] section
- max_tokens=4096; temperature=0.2; MAX_CHARS_PER_FILE=14,000
- **Evaluation Criteria panel** (collapsible): admin can add/edit/delete criteria;
users see read-only list. Active criteria are appended to the AI prompt as an
alignment evaluation section. The AI outputs RECOMMENDATION: PURSUE/PASS/UNCLEAR
which is parsed and displayed as a colour-coded verdict banner.
- **History tab**: every analysis is saved to ai_analysis_log (with verdict + criteria
snapshot). History treeview is filterable by verdict. Double-click restores full
output in the Analyze tab. Admins see all users; regular users see only their own.
- CriterionDialog has a live character counter with 500-char soft limit guidance.
- URL open and health-check probe both guarded by _validate_and_normalise_url.
.doc reading (legacy binary Word format) — 3-tier fallback:
1. win32com (Word COM automation — requires MS Word installed)
2. docx2txt (pure Python)
3. Raw ASCII scrape from binary
### views/admin_websites_view.py
- Website CRUD dialog now has a **collapsible credentials section** (hidden by default)
- "🔑 Show Credentials" / "🔒 Hide Credentials" toggle button
- Auto-expands when editing a site that already has saved credentials
- " Add Credential" auto-expands the section if collapsed
- **Bug fixed:** `_on_visibility_change` previously used `before=self.creds_container`
which crosses a widget parent boundary causing a TclError; corrected to `before=self._cred_body`
### views/user_dashboard_view.py
Key internals:
- _health_cache: {website_id: ("ok"|"slow"|"down", ms)} — daemon threads, HEAD request, 6s timeout
- Dot colours: green (<=3000ms), amber (>3000ms), red (error)
- Mousewheel: Enter/Leave scoped; unbind_all in destroy()
- Keyboard shortcuts: self.bind() stored in _shortcut_ids; unbound in destroy()
- Notifications: after(60_000) loop; plyer first, fallback to borderless Toplevel toast
- **Credentials popup no longer opens automatically on link click**
- "🔑 Credentials" button appears on each site card only if the site has saved credentials
- CredentialsPopup: 📋 copy button for username (2s flash); 📋 copy button for password
(2s flash + clipboard auto-cleared after 15s for security); 👁 toggle to reveal password
### views/login_view.py
- check_login_allowed(username) called before authenticate()
- Locked: form disabled; 1-second countdown; re-enables when timer reaches 0
- Not-yet-locked failed attempt: shows "N attempt(s) remaining before lockout"
### views/admin_shifts_view.py
- _export_pdf(): reportlab A4 document; one section per active shift; user+website tables
### views/reports_view.py
- All date fields use DateEntry (no manual text entry)
- Chart: matplotlib Agg + FigureCanvasTkAgg; bars green>=100% / amber>=50% / red<50%
---
## 6. Authentication & Security
| Concern | Implementation |
|---|---|
| Password hashing | bcrypt rounds=12; SHA-256 auto-rehashed on next login |
| Website credential encryption | Fernet (AES-128-CBC + HMAC) via cryptography library |
| Config credential protection | Windows DPAPI (CryptProtectData) — user-account-scoped |
| Encrypted fields | DB user, DB password, SMTP password, Groq API key |
| Login rate limiting | 5 attempts → 15-min lockout in DB |
| Session timeout | 30-min idle; 1-min warning; any mouse/key event resets |
| Password policy | 8+ chars, uppercase, digit, special character |
| Self-service change | Requires current password; strength meter; same-as-current guard |
| Clipboard security | Password copy auto-clears clipboard after 15 seconds |
### DPAPI Migration
`migrate_plaintext_config()` is called automatically on every startup (in `_init_db`).
It is a no-op if all sensitive fields are already encrypted (dpapi: prefix present).
On first run after this feature was added, it encrypts any existing plain-text values in-place.
---
## 7. Key Business Logic
### check_type
- daily: shown every shift day
- weekly: hidden once checked this week (YEARWEEK ISO); reappears Monday
### visibility
- all: every user in shift sees it
- assigned: only users in website_users table see it
### get_today_checks resolution
1. Check if user has active shifts today (DAYOFWEEK match)
2. If yes: union of shift websites filtered by check_type + visibility
3. If no: all active websites filtered by visibility + check_type (legacy fallback)
### Soft deletes
Websites and Shifts: is_active=0. Users: hard-delete (admin can deactivate first).
---
## 8. Configuration Constants (edit in source)
| File | Constant | Default |
|---|---|---|
| app.py | IDLE_TIMEOUT_MS | 1,800,000 (30 min) |
| app.py | IDLE_WARNING_MS | 60,000 (1 min) |
| app.py | APP_VERSION | "1.0.0" |
| app.py | VERSION_CHECK_ENABLED | False |
| models.py | MAX_FAILED_ATTEMPTS | 5 |
| models.py | LOCKOUT_MINUTES | 15 |
| models.py | PW_MIN_LENGTH | 8 |
| user_dashboard_view.py | NOTIFY_MINUTES_BEFORE | 15 |
| admin_dashboard_view.py | REFRESH_INTERVAL_MS | 60,000 |
| utils/crypto.py | _ITERATIONS | 100,000 |
| ai_summary_view.py | MAX_CHARS_PER_FILE | 14,000 |
| ai_summary_view.py | _OFFICE_ADDRESS | "2815 Hartland Road, Falls Church, VA 22043, USA" |
| ai_summary_view.py | CriterionDialog._DESC_SOFT_LIMIT | 500 (chars, soft guidance only) |
| app.py | App._IDLE_THROTTLE_S | 5.0 (seconds between Motion-event timer resets) |
| admin_users_view.py | _PasswordResetDialog._AUTO_CLOSE_S | 120 (seconds before auto-close) |
| admin_users_view.py | _PasswordResetDialog._CLIP_CLEAR_S | 30 (seconds before clipboard wipe) |
---
## 9. Critical Gotchas
1. bind_all("<MouseWheel>") is NEVER used at module level. Always Enter/Leave scoped.
Every view's destroy() calls unbind_all("<MouseWheel>") and _unbind_shortcuts().
2. COLOURS is a live proxy — delegates to ThemeManager.get() on every access.
Never snapshot it into a local variable at class-creation time.
3. bcrypt is slow by design (~200-400ms at rounds=12). Expected behaviour.
4. config.ini sensitive fields are DPAPI-encrypted (dpapi: prefix).
Website credential passwords are Fernet-encrypted (enc: prefix).
Non-sensitive fields (host, port, db name, smtp_host, recipients) remain plain text.
5. get_today_checks GROUP BY includes sc.id to prevent row collisions when a user
belongs to multiple shifts sharing the same website.
6. Weekly site logic uses YEARWEEK(..., 1) (ISO week, Monday start).
7. The email scheduler `last_sent_date` is now persisted to `config.ini`
`[email] last_sent_date` (ISO format). A restart after the configured
`send_time` will not re-send the report on the same day.
8. All log messages use ASCII only (hyphens not em-dashes/arrows) to prevent
cp1252 UnicodeEncodeError on Windows consoles.
9. DPAPI encrypted blobs are tied to the Windows user account that created them.
If config.ini is copied to a different machine or user account, credentials
cannot be decrypted. Users must re-enter credentials via the Settings dialog.
10. The AI summary view fetches credentials from the DB at card render time (not lazily).
Avoid having hundreds of sites with credentials as this adds DB round-trips per render.
---
## 10. Dependencies
```
mysql-connector-python>=8.0.0
bcrypt>=4.0.0
openpyxl>=3.1.0
cryptography>=41.0.0
matplotlib>=3.7.0
plyer>=2.1.0
reportlab>=4.0.0
groq>=1.0.0
pypdf>=3.0.0
python-docx>=1.0.0
pywin32>=306
docx2txt>=0.8
```
stdlib used: tkinter, csv, smtplib, urllib.request, configparser, threading, calendar,
datetime, base64, re, tempfile
---
## 11. First-Run Flow
1. _boot() → config_exists() → False → SettingsView (locked modal)
2. User enters DB creds → Test Connection → Save & Connect
3. reload_db_config() → initialize_database() → seeds admin/admin123 if users empty
4. migrate_plaintext_config() → encrypts any plain-text credentials in config.ini
5. Login screen shown
6. ThemeManager(root, initial="light") applied; shell built
---
## 12. Sidebar Navigation
### Admin role
Dashboard · Websites · Users · Shifts · Reports · Activity Log · 🤖 AI Summary · Settings · Sign Out
### User role
My Shift · 🤖 AI Summary · Change Password · Sign Out
---
## 13. Deployment Notes
- Python 3.9+ required. 3.12 tested on Windows.
- tkinter bundled on Windows/macOS; Linux: apt install python3-tk
- Create DB first: CREATE DATABASE website_checker CHARACTER SET utf8mb4;
- MySQL port 3306 must be reachable from client
- Default admin: username=admin / password=admin123 — change immediately
- pywin32 required for DPAPI encryption (Windows only). Install: pip install pywin32
- Microsoft Word recommended for .doc file support in AI Summary (falls back to docx2txt)
- Groq API key required for AI Summary feature — free at https://console.groq.com
---
## 14. AI Summary Feature — Setup & Prompt Details
### Setup (Admin)
1. Obtain a free Groq API key at https://console.groq.com
2. Navigate to **🤖 AI Summary** in the sidebar
3. Enter the API key and select a model
4. Click **💾 Save Settings** — key is stored DPAPI-encrypted in config.ini
### Supported Models
- llama-3.3-70b-versatile (default, recommended)
- llama-3.1-8b-instant
- gemma2-9b-it
- mixtral-8x7b-32768
### Prompt Strategy
The prompt instructs the AI to extract 12 labeled fields per document:
Solicitation Number, Type, Set-Aside, Description/Scope, Work Site, Pre-Proposal Conference,
POC, Square Footage, **Driving Distance from 2815 Hartland Rd Falls Church VA** (calculated by AI),
Last Day for Questions, Due Date, Other requirements.
Then produces an Overall Summary with 4 sections:
A. Scope of Work · B. Contract Period · C. Proposal Submission Requirements · D. Key Deadlines
The driving distance/time is an AI estimate using major highways. Actual times may vary with traffic.