04/22 Upgraded code commit
This commit is contained in:
@@ -0,0 +1,313 @@
|
||||
# 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" = 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("<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
|
||||
Reference in New Issue
Block a user