# 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= ``` ### `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 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("") is NEVER used at module level. Always Enter/Leave scoped. Every view's destroy() calls unbind_all("") 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