11 KiB
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.
[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" = Mon–Fri. 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
- Check if user has active shifts today (DAYOFWEEK match)
- If yes: union of shift websites filtered by check_type + visibility
- 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
-
bind_all("") is NEVER used at module level. Always Enter/Leave scoped. Every view's destroy() calls unbind_all("") and _unbind_shortcuts().
-
COLOURS is a live proxy — delegates to ThemeManager.get() on every access. Never snapshot it into a local variable at class-creation time.
-
bcrypt is slow by design (~200-400ms at rounds=12). Expected behaviour.
-
config.ini stores DB password and SMTP password in plaintext. Website credential passwords are Fernet-encrypted.
-
get_today_checks GROUP BY includes sc.id to prevent row collisions when a user belongs to multiple shifts sharing the same website.
-
Weekly site logic uses YEARWEEK(..., 1) (ISO week, Monday start).
-
The email scheduler last_sent_date is in-memory — resets on restart. A production deployment should persist it to the DB.
-
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
- _boot() → config_exists() → False → SettingsView (locked modal)
- User enters DB creds → Test Connection → Save & Connect
- reload_db_config() → initialize_database() → seeds admin/admin123 if users empty
- Login screen shown
- 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