Files
WebChecker/CLAUDE.md
T
2026-04-22 12:22:26 -04:00

314 lines
11 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/
│ ├── crypto.py Fernet credential encryption
│ ├── 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
├── 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], and [crypto] sections.
```ini
[database]
host=your-mysql-host
port=3306
database=website_checker
user=your-db-user
password=your-db-password
[email]
enabled=false
smtp_host=smtp.example.com
smtp_port=587
smtp_user=sender@example.com
smtp_password=secret
use_tls=true
recipients=admin@example.com
send_time=18:00
[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, 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.
### 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'
---
## 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() → config.ini read/write
- config_exists() → True if host, database, user are set
- reload_db_config() → re-reads config.ini, resets pool
### 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
- last_sent_date in-memory (resets on restart)
- start() / stop() called from app.py on login/logout (admin only)
### 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/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
### 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 |
| Credential encryption | Fernet (AES-128-CBC + HMAC) via cryptography library |
| 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 |
---
## 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 |
---
## 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 stores DB password and SMTP password in plaintext.
Website credential passwords are Fernet-encrypted.
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 in-memory — resets on restart.
A production deployment should persist it to the DB.
8. All log messages use ASCII only (hyphens not em-dashes/arrows) to prevent
cp1252 UnicodeEncodeError on Windows consoles.
---
## 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
```
stdlib used: tkinter, csv, smtplib, urllib.request, configparser, threading, calendar, datetime
---
## 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. Login screen shown
5. ThemeManager(root, initial="light") applied; shell built
---
## 12. 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