# Claude.md — JQC Developer Reference > **Audience:** AI assistants and developers working on this codebase. > **Purpose:** Authoritative reference for architecture, conventions, gotchas, and decisions. > **Last reviewed:** April 2026 (Phase 11 complete) --- ## Table of Contents 1. [Project Overview](#1-project-overview) 2. [Tech Stack](#2-tech-stack) 3. [Repository Layout](#3-repository-layout) 4. [Environment & Configuration](#4-environment--configuration) 5. [Database Models](#5-database-models) 6. [Role & Permission Matrix](#6-role--permission-matrix) 7. [Blueprint Prefixes & Route Inventory](#7-blueprint-prefixes--route-inventory) 8. [Utility Modules](#8-utility-modules) 9. [Mobile API (Phase 7)](#9-mobile-api-phase-7) 10. [Notification System](#10-notification-system) 11. [SLA Engine](#11-sla-engine) 12. [Audit Trail](#12-audit-trail) 13. [PDF Export](#13-pdf-export) 14. [Scheduled Reports](#14-scheduled-reports) 15. [Alembic Migration Chain](#15-alembic-migration-chain) 16. [Frontend Conventions](#16-frontend-conventions) 17. [Infrastructure](#17-infrastructure) 18. [Known Constraints & Hard Rules](#18-known-constraints--hard-rules) 19. [Change Philosophy](#19-change-philosophy) --- ## 1. Project Overview **JQC (Janitorial Quality Control)** is a production-grade, full-stack web application that manages: - Janitorial service contracts organised as **Projects → Facilities → Areas** - **Inspection** execution against configurable templates with dynamic form builder - **Issue** tracking with SLA enforcement, follower subscriptions, and verification workflow - **Customer portal** with scoped facility visibility and invitation-based onboarding - **Notification** system (in-app + email) driven by an admin-controlled matrix - **Reports** — on-demand PDF/CSV scorecards and scheduled email digests - **Audit trail** — immutable log of every create/update/delete action - **Mobile API** — JWT-authenticated REST layer (Phase 7) for a React Native / Expo mobile app The application is actively deployed in production and maintained by a single developer/administrator. --- ## 2. Tech Stack | Layer | Technology | |---|---| | Language | Python 3.11+ | | Web framework | Flask (application factory pattern) | | ORM | Flask-SQLAlchemy (SQLAlchemy 2.x) | | Database | MySQL (via PyMySQL driver) | | Auth (web) | Flask-Login + Flask-WTF CSRF | | Auth (API) | JWT access tokens + opaque refresh tokens (PyJWT) | | Migrations | Flask-Migrate / Alembic | | Email | Flask-Mail (SMTP, background threading) | | PDF generation | ReportLab | | Forms | WTForms + Flask-WTF | | Templating | Jinja2 | | Frontend | Bootstrap 5, Chart.js, vanilla JS | | Server | Gunicorn (sync workers) behind Nginx | | OS | Ubuntu Linux | | Mobile client | React Native + Expo (Expo Go for dev; cloud Mac for iOS builds) | | Timezone | All datetimes stored as US/Eastern (naive, via `now_eastern()`) | --- ## 3. Repository Layout ``` lt_janitorial_quality_control/ ├── app/ │ ├── __init__.py # Application factory — create_app() │ ├── add_form_schema.py # One-off migration helper (safe re-run) │ ├── api/ # Mobile REST API (Phase 7) │ │ ├── __init__.py # api_bp parent blueprint + register_api() │ │ ├── auth.py # /api/v1/auth/* and /api/v1/devices/* │ │ ├── decorators.py # @jwt_required │ │ ├── errors.py # JSON error helpers + error handler registration │ │ └── jwt_utils.py # generate_access_token() │ ├── models/ │ │ ├── __init__.py # Re-exports all models │ │ ├── api_token.py # RefreshToken, DeviceToken │ │ ├── audit.py # AuditLog │ │ ├── facility.py # Facility, Area │ │ ├── inspection.py # InspectionTemplate, ChecklistItem, Inspection, InspectionResult │ │ ├── issue.py # Issue, IssueComment, IssueFollower │ │ ├── notification.py # Notification, NotificationPreference + event constants │ │ ├── notification_matrix.py # NotificationMatrix + MATRIX_DEFAULTS, MATRIX_EVENTS │ │ ├── project.py # Project, CustomerAssignment │ │ ├── scheduled_report.py # ScheduledReport │ │ └── user.py # User (with password-setup workflow) │ ├── routes/ │ │ ├── __init__.py │ │ ├── audit.py # /audit/* │ │ ├── auth.py # /auth/* (users, login, notification matrix) │ │ ├── customers.py # /customers/* │ │ ├── dashboard.py # / │ │ ├── facilities.py # /facilities/* │ │ ├── inspections.py # /inspections/* │ │ ├── issues.py # /issues/* │ │ ├── notifications.py # /notifications/* │ │ ├── projects.py # /projects/* │ │ ├── reports.py # /reports/* │ │ ├── scheduled_reports.py # /scheduled-reports/* │ │ └── templates.py # /templates/* │ ├── static/ │ │ ├── css/ipad_responsive.css │ │ └── uploads/ # User-uploaded photos (gitignored) │ ├── templates/ # Jinja2 templates (mirrors routes/ structure) │ │ ├── base.html # Master layout (navbar, sidebar, flash messages) │ │ ├── _sla_badge.html # Reusable SLA status badge partial │ │ └── ... │ └── utils/ │ ├── __init__.py │ ├── audit.py # log_action() + ACTION_* constants │ ├── decorators.py # @admin_required, @supervisor_required, @project_manager_required, @customer_required │ ├── forms.py # All WTForms form classes │ ├── notifications.py # notify(), notify_by_matrix(), send_pending_digests() │ ├── pdf_export.py # generate_inspection_pdf(), scorecard PDF │ ├── scope.py # get_customer_scope() — facility access for customers │ ├── sla.py # sla_status(), sla_deadline(), sla_hours_remaining(), send_sla_alerts() │ └── time_utils.py # now_eastern() ├── migrations/ │ └── versions/ │ ├── phase1_projects_roles.py │ ├── phase6_features.py │ ├── phase7_mobile_api.py │ ├── phase8_notification_matrix.py │ ├── phase9_user_full_name.py │ ├── phase10_customer_password_setup.py │ └── phase11_director_role.py # HEAD ├── config.py # Config, DevelopmentConfig, ProductionConfig ├── gunicorn_config.py # bind, workers, log paths ├── requirements.txt ├── run.py # Development entry point └── wsgi.py # Gunicorn entry point ``` --- ## 4. Environment & Configuration ### Required Environment Variables | Variable | Notes | |---|---| | `SECRET_KEY` | Flask secret — no fallback; startup fails if absent | | `DATABASE_URL` | Full SQLAlchemy URI, e.g. `mysql+pymysql://user:pass@localhost/jqc` | | `MAIL_SERVER` | SMTP hostname | | `MAIL_USERNAME` | SMTP login | | `MAIL_PASSWORD` | SMTP password | | `MAIL_PORT` | 465 (SSL) or 587 (STARTTLS) — auto-selects `MAIL_USE_SSL`/`MAIL_USE_TLS` | | `APP_BASE_URL` | Full URL prefix for email links, e.g. `https://jqc.example.com` | | `MAIL_DEFAULT_SENDER` | From address | | `DIGEST_SECRET` | Token used by cron to authenticate `/notifications/send-digest` | ### Email SSL Auto-Detection ```python # config.py — port 465 → implicit SSL; port 587 → STARTTLS MAIL_USE_SSL = _mail_port == 465 MAIL_USE_TLS = not MAIL_USE_SSL ``` > **Critical:** Never set both flags to `True` — Flask-Mail breaks silently. ### File Uploads - `UPLOAD_FOLDER` = `app/static/uploads/` - `MAX_CONTENT_LENGTH` = 50 MB (Flask-side limit; Nginx must also be configured) - Allowed extensions: `png`, `jpg`, `jpeg`, `gif` - Subfolders: `inspection_photos/`, `issue_photos/` ### Session Cookies - `SESSION_COOKIE_SECURE = True` in production (HTTP only for dev) - `SESSION_COOKIE_HTTPONLY = True` - `SESSION_COOKIE_SAMESITE = 'Lax'` - Session lifetime: 24 hours --- ## 5. Database Models ### User ``` users ├── id, username (unique), full_name, email (unique) ├── password_hash, role (ENUM), created_at, active ├── password_set (Bool) — False until customer completes setup flow ├── set_password_token (str/64) — time-limited invitation token └── set_password_token_expires — Eastern datetime ``` **Role ENUM (current):** `admin`, `director`, `inspector`, `project_manager`, `customer` > `supervisor` was renamed to `director` in Phase 11. The ENUM no longer contains `supervisor`. **Key methods:** - `display_name` → `full_name.strip()` or falls back to `username` - `generate_set_password_token(expires_hours=72)` → creates 64-hex token + expiry - `verify_set_password_token(token)` → returns User or None ### Facility / Area ``` facilities: id, name, address, contact_person, contact_phone, active, project_id (FK→projects) areas: id, facility_id (FK), name, area_type ``` ### Project / CustomerAssignment ``` projects: id, name, description, project_manager_id (FK→users), active, created_at customer_assignments: id, user_id, project_id, facility_id (nullable) └── UniqueConstraint(user_id, project_id, facility_id) ``` `facility_id = NULL` means the customer has access to all active facilities in the project. ### InspectionTemplate / ChecklistItem / Inspection / InspectionResult ``` inspection_templates: id, name, description, frequency, created_by, created_at, form_schema (JSON) checklist_items: id, template_id, category, item_description, scoring_type, weight, requires_photo, display_order inspections: id, template_id, facility_id, area_id, inspector_id, inspection_date, overall_score, status (in_progress/completed/flagged), notes, form_data (JSON), completed_at, parent_inspection_id (self-FK), follow_up_required, follow_up_note inspection_results: id, inspection_id, checklist_item_id, score, passed, comments, photo_path ``` **Scoring rule:** Items with `score = 0` mean "unanswered" and are excluded from score calculation entirely. **Follow-up workflow:** `parent_inspection_id` links re-inspections back to the original. ### Issue / IssueComment / IssueFollower ``` issues: id, inspection_id (nullable), area_id, severity (low/medium/high/critical), description, photo_path, status (open/in_progress/resolved/pending_verification), assigned_to (FK→users), reported_at, resolved_at, result_notes, result_photos (JSON), verified_by, verified_at, verification_note, sla_notified (None/'at_risk'/'breached') issue_comments: id, issue_id, user_id, status_at_time, body, created_at issue_followers: id, issue_id, user_id, created_at — UniqueConstraint(issue_id, user_id) ``` **Issue status flow:** `open` → `in_progress` → `pending_verification` → `resolved` ### Notification / NotificationPreference ``` notifications: id, user_id, title, body, link, is_read, created_at, issue_id (nullable), event_type notification_preferences: id, user_id, event_type, email_enabled, digest_mode, digest_frequency ``` ### NotificationMatrix ``` notification_matrix: id, event_type, role_key, enabled, custom_emails (JSON text) └── UniqueConstraint(event_type, role_key) ``` ### AuditLog ``` audit_logs: id, user_id (FK nullable on delete SET NULL), username (snapshot), user_role (snapshot), action, entity_type, entity_id, entity_label, details, created_at (indexed), ip_address ``` ### RefreshToken / DeviceToken ``` api_refresh_tokens: id, user_id, token_hash (SHA-256, unique), device_id, device_name, created_at, expires_at, revoked api_device_tokens: id, user_id, device_id, apns_token, device_name, app_version, registered_at └── UniqueConstraint(user_id, device_id) ``` ### ScheduledReport ``` scheduled_reports: id, name, report_type (summary/facility/issues), frequency (daily/weekly/monthly), facility_id (nullable), recipients (JSON), include_pdf, include_csv, active, created_by, created_at, last_sent_at, next_send_at ``` --- ## 6. Role & Permission Matrix | Route Area | admin | director | project_manager | inspector | customer | |---|---|---|---|---|---| | Dashboard | ✅ full | ✅ full | ✅ full | ✅ limited | ✅ scoped | | Users (`/auth/users`) | ✅ | ✅ | ❌ | ❌ | ❌ | | Notification Matrix | ✅ only | ❌ | ❌ | ❌ | ❌ | | Customers (`/customers`) | ✅ | ✅ | ❌ | ❌ | ❌ | | Facilities | ✅ | ✅ | ✅ | ✅ read | ✅ scoped | | Projects | ✅ | ✅ | ✅ | ✅ read | ✅ scoped | | Templates (create/edit) | ✅ | ✅ | ❌ | ❌ | ❌ | | Inspections (execute) | ✅ | ✅ | ✅ | ✅ | ✅ read | | Issues (create/assign) | ✅ | ✅ | ✅ | ✅ | ✅ read | | Issue verification | ✅ | ✅ | ❌ | ❌ | ❌ | | Reports (export) | ✅ | ✅ | ✅ | ✅ | ✅ scoped | | Scheduled Reports | ✅ | ✅ | ✅ | ❌ | ❌ | | Audit Trail | ✅ only | ❌ | ❌ | ❌ | ❌ | | Mobile API | ✅ | ✅ | ✅ | ✅ | ❌ (TBD) | ### Decorator Map ```python @admin_required # role == 'admin' only @supervisor_required # role in ('admin', 'director') — intentionally kept as-is (Phase 11) @project_manager_required # role in ('admin', 'director', 'project_manager') @customer_required # role == 'customer' only ``` > **Important:** `@supervisor_required` is **not** renamed to `director_required` — that would require touching 30+ route decorators. The function name is a legacy artefact; the access list is correct. ### Customer Scoping Customer visibility is derived from `CustomerAssignment` rows via `get_customer_scope(user)`: - Returns `list[int]` of facility IDs, or `None` for non-customer roles - `facility_id = NULL` assignment → access to all active facilities in that project - All customer-facing queries must filter by `facility_id.in_(scope)` or `area.facility_id.in_(scope)` --- ## 7. Blueprint Prefixes & Route Inventory | Blueprint | Prefix | Key Routes | |---|---|---| | `auth` | `/auth` | `/login`, `/logout`, `/profile`, `/users`, `/users/new`, `/users//edit`, `/users//delete`, `/users//toggle-active`, `/notification-matrix` | | `dashboard` | `/` | `GET /` | | `facilities` | `/facilities` | CRUD + `/facilities//areas` | | `projects` | `/projects` | CRUD + assignment management | | `customers` | `/customers` | list, create, edit, delete, manage assignments, invite, set-password | | `inspections` | `/inspections` | list, start, execute, view, PDF export, flag issue | | `templates` | `/templates` | list, create, edit, delete, form editor, preview | | `issues` | `/issues` | list, view, create, update, assign, verify, comment, follow/unfollow, verification queue | | `notifications` | `/notifications` | list, mark-read, preferences, send-digest (cron) | | `audit` | `/audit` | list (admin-only), view | | `reports` | `/reports` | index, facility report, scorecard, CSV/PDF export | | `scheduled_reports` | `/scheduled-reports` | CRUD + manual trigger | | `api` (parent) | `/api/v1` | — | | `api_auth` | `/api/v1` | `/auth/login`, `/auth/refresh`, `/auth/logout`, `/auth/me`, `/devices/register` | ### API Blueprint Registration The mobile API uses a **parent + child** pattern: ```python # app/api/__init__.py api_bp = Blueprint('api', __name__, url_prefix='/api/v1') def register_api(app): from app.api.auth import bp as auth_bp api_bp.register_blueprint(auth_bp) app.register_blueprint(api_bp) ``` CSRF is exempted **before** registration: ```python csrf.exempt(api_bp) register_api(app) ``` --- ## 8. Utility Modules ### `app/utils/time_utils.py` ```python from app.utils.time_utils import now_eastern ``` Returns a **naive datetime** representing the current US/Eastern wall-clock time (adjusts automatically for EDT/EST). All `db.Column(db.DateTime, default=now_eastern)` calls use this. Never use `datetime.utcnow()` — it caused incorrect SLA purge cutoffs. ### `app/utils/audit.py` ```python from app.utils.audit import log_action, ACTION_CREATE, ACTION_UPDATE, ACTION_DELETE, ACTION_LOGIN, ACTION_LOGOUT, ACTION_EXPORT log_action( action = ACTION_CREATE, entity_type = 'Facility', entity_id = facility.id, entity_label = facility.name, details = f'address={facility.address}', ) ``` - Must be called **after** `db.session.commit()` to capture the entity's ID - Never raises — failures are logged but never bubble up to break the primary request - Snapshots `username` and `user_role` so records survive user deletion ### `app/utils/decorators.py` Four decorators: `admin_required`, `supervisor_required`, `project_manager_required`, `customer_required`. All redirect to `dashboard.index` with a flash message on unauthorized access. ### `app/utils/scope.py` ```python from app.utils.scope import get_customer_scope facility_ids = get_customer_scope(current_user) # None for non-customers if facility_ids is not None: q = q.filter(Inspection.facility_id.in_(facility_ids)) ``` ### `app/utils/forms.py` All WTForms classes live here: - `LoginForm`, `ProfileForm`, `UserForm` (excludes `customer` role — use `/customers`) - `FacilityForm`, `AreaForm` - `InspectionTemplateForm`, `ChecklistItemForm`, `StartInspectionForm` - `IssueForm`, `IssueUpdateForm` - `ProjectForm`, `ProjectAssignmentForm` - `CustomerUserForm`, `CustomerAssignmentForm`, `CustomerInviteForm`, `SetPasswordForm` - `ScheduledReportForm` ### `app/utils/notifications.py` ```python from app.utils.notifications import notify, notify_by_matrix, notify_customers_for_facility notify( recipient = user_obj, title = 'Issue Updated', body = 'Status changed to resolved.', link = '/issues/42', issue_id = 42, event_type = EVENT_ISSUE_STATUS, send_email = True, ) notify_by_matrix( event_type = 'inspection_completed', title = '...', body = '...', link = '...', issue_id = None, exclude_user_ids = {assignee.id}, # deduplicate ) ``` Email is sent in a background thread so it never blocks the HTTP response. ### `app/utils/pdf_export.py` - `generate_inspection_pdf(inspection)` — full ReportLab inspection report with 12-column CSS grid preserved in print - Scorecard generation for facility/project reports - **Critical:** The 12-column grid layout must not be collapsed to single-column in print/PDF views ### `app/utils/sla.py` SLA thresholds: `critical=4h`, `high=24h`, `medium=72h`, `low=168h` At-risk threshold: 75% of window elapsed. ```python from app.utils.sla import sla_status, sla_deadline, sla_hours_remaining, send_sla_alerts status = sla_status(issue) # 'ok' | 'at_risk' | 'breached' | None (resolved) ``` `send_sla_alerts()` is called by cron. It deduplicates using `issue.sla_notified` — once `breached` is recorded, no further alerts fire for that issue. --- ## 9. Mobile API (Phase 7) ### Authentication Flow 1. `POST /api/v1/auth/login` → returns `access_token` (JWT, 60 min) + `refresh_token` (opaque hex, 30 days) 2. App stores both in iOS Keychain 3. Every authenticated request sends `Authorization: Bearer ` 4. When access token expires → `POST /api/v1/auth/refresh` → token rotation (old revoked, new issued) 5. `POST /api/v1/auth/logout` → revokes refresh token ### JWT Details - Library: PyJWT - Access token lifetime: 60 minutes (configurable in `jwt_utils.py`) - Payload: `sub` (user ID), `exp`, `iat`, `role` - `@jwt_required` decorator extracts and validates the token; sets `g.api_user` ### Refresh Token Storage - Raw token is **never stored** — only its SHA-256 hex digest (`token_hash`) - `RefreshToken.verify(raw_token)` hashes and looks up; returns None if expired/revoked - Token rotation: old row's `revoked = True`, new row inserted ### CSRF Exemption Pattern ```python # app/__init__.py — order matters csrf = CSRFProtect() # module-level instance csrf.init_app(app) # in create_app() csrf.exempt(api_bp) # BEFORE register_api(app) register_api(app) ``` > **Never** use per-route decorators (`@csrf.exempt`) — Flask-WTF 1.0+ removed that API. ### Phase 2 Extensions (planned) Commented stubs in `app/api/__init__.py`: - `api_facilities`, `api_inspections`, `api_issues`, `api_notifications`, `api_photos` --- ## 10. Notification System ### Event Types (constants in `app/models/notification.py`) ``` EVENT_INSPECTION_DONE = 'inspection_completed' EVENT_ISSUE_ASSIGNED = 'issue_assigned' EVENT_ISSUE_REASSIGNED = 'issue_reassigned' EVENT_ISSUE_UNASSIGNED = 'issue_unassigned' EVENT_ISSUE_STATUS = 'issue_status' EVENT_ISSUE_COMMENT = 'issue_comment' EVENT_ISSUE_FOLLOW = 'issue_follow_update' EVENT_ISSUE_FLAGGED = 'issue_flagged' EVENT_ISSUE_CREATED = 'issue_created' EVENT_CUSTOMER_ISSUE_UPDATED = 'issue_updated_customer' EVENT_VERIFICATION_REQUESTED = 'verification_requested' EVENT_SLA_ALERT = 'sla_alert' EVENT_CUSTOMER_INSPECTION_DONE = 'customer_inspection_completed' ``` ### Notification Matrix The `notification_matrix` table controls which roles receive each event type. Admin UI at `/auth/notification-matrix` (admin-only). - `role_key` values: `admin`, `director`, `inspector`, `project_manager`, `customer`, `custom` - `custom` role_key stores a JSON list of arbitrary email addresses - Matrix rows are auto-created from `MATRIX_DEFAULTS` on first access (`get_matrix_row()`) ### Implicit Notifications (always sent, not matrix-controlled) - Issue **assignee** — always notified on assignment/status changes - Issue **followers** — always notified on any update - SLA **assignee + followers** — always notified on SLA events ### Notification Preferences Users can configure per-event preferences at `/notifications/preferences`: - `email_enabled` — receive email at all - `digest_mode` — batch into digest rather than immediate send - `digest_frequency` — daily/weekly/monthly ### Digest Cron `POST /notifications/send-digest?frequency=daily&secret=` — triggered by server cron, authenticates via `DIGEST_SECRET`. ### Email Delivery Emails are dispatched in a background thread: ```python t = threading.Thread(target=_send_email_thread, args=(...)) t.daemon = True t.start() ``` Never blocks the HTTP response. Failures are logged but not raised. --- ## 11. SLA Engine | Severity | Window | At-Risk Trigger | |---|---|---| | critical | 4 hours | 3 hours | | high | 24 hours | 18 hours | | medium | 72 hours | 54 hours | | low | 168 hours (7 days) | 126 hours | - Cron calls `send_sla_alerts()` from within Flask app context - `Issue.sla_notified` prevents duplicate notifications — tracked per-issue at level (`at_risk` / `breached`) - `breached` supersedes `at_risk` — user receives two notifications total (once at-risk, once breached) - SLA does not apply once `issue.status == 'resolved'` --- ## 12. Audit Trail Covered entities: `User`, `Facility`, `Area`, `Template`, `ChecklistItem`, `Inspection`, `Issue`, `IssueComment`, `Project`, `CustomerAssignment`, `ScheduledReport`, `NotificationMatrix` Actions: `CREATE`, `UPDATE`, `DELETE`, `LOGIN`, `LOGOUT`, `EXPORT` - Admin-only access: `/audit/` and `/audit/` - Director role: **excluded** from viewing the Audit Trail - IP captured from `X-Forwarded-For` (Nginx) or `request.remote_addr` - `username` and `user_role` are snapshotted at write time — survives user deletion (`ondelete='SET NULL'`) --- ## 13. PDF Export Uses **ReportLab** directly (not weasyprint or wkhtmltopdf). Located in `app/utils/pdf_export.py`. - Inspection PDF: full checklist results, scores, photos, notes, follow-up status - Scorecard PDF: facility/project performance summary with charts - **12-column grid must be preserved** — do not collapse to single-column in PDF views Reports also support CSV export (Python `csv` module, streamed as `Response`). --- ## 14. Scheduled Reports `ScheduledReport` records drive recurring email reports: - Types: `summary` (KPI digest), `facility` (single facility scorecard), `issues` (open issues list) - Frequencies: `daily`, `weekly`, `monthly` - Attachments: optional PDF and/or CSV via `include_pdf` / `include_csv` - `next_send_at` and `last_sent_at` track execution state - Cron trigger: `POST /scheduled-reports/run?secret=` --- ## 15. Alembic Migration Chain ``` phase1_projects_roles └── phase6_features └── phase7_mobile_api └── phase8_notification_matrix └── phase9_user_full_name └── phase10_customer_password_setup └── phase11_director_role ← HEAD ``` ### MySQL ENUM Change Protocol (3 Steps) **Always** follow this sequence when modifying an ENUM column — skipping steps causes data loss or migration failures: ```sql -- Step 1: Expand ENUM to include BOTH old and new values ALTER TABLE users MODIFY COLUMN role ENUM('admin','supervisor','director',...) NOT NULL; -- Step 2: Migrate existing data UPDATE users SET role = 'director' WHERE role = 'supervisor'; -- Step 3: Contract ENUM to remove old value ALTER TABLE users MODIFY COLUMN role ENUM('admin','director',...) NOT NULL; ``` ### Adding New Migrations ```bash flask db migrate -m "description" flask db upgrade ``` All migration scripts include existence checks for safe re-runs. ### Deprecated SQLAlchemy Patterns ```python # WRONG — deprecated in SQLAlchemy 2.x User.query.get(user_id) # CORRECT db.session.get(User, user_id) ``` Also: `filter()` must precede `limit()`. Avoid N+1 query patterns — use eager loading or bulk queries. --- ## 16. Frontend Conventions ### Base Template (`base.html`) - Bootstrap 5 (CDN) - Chart.js (CDN, for dashboard charts) - Navbar includes unread notification badge (injected via `@app.context_processor`) - `pending_verification_count` badge for admin/director roles - Jinja2 globals available in all templates: `csrf_token`, `enumerate`, `sla_status`, `sla_deadline`, `sla_hours_remaining`, `SLA_HOURS` ### Forms - All web forms use Flask-WTF CSRF protection automatically - Manual forms (without a WTForms object) must include `` - **Never nest `
` tags** — browsers silently discard inner forms. Toggle/action forms must be siblings, not children, of edit forms. ### File Uploads in Templates Photos are served from `app/static/uploads/` via `url_for('static', filename=path)`. ### Real-Time Updates **SSE is banned.** Server-Sent Events exhausted the Gunicorn sync worker pool in production. All "live" updates use polling (JS `setInterval` + `fetch`). --- ## 17. Infrastructure ### Gunicorn (`gunicorn_config.py`) ```python bind = "127.0.0.1:8000" workers = multiprocessing.cpu_count() * 2 + 1 worker_class = "sync" # SSE would require 'gevent' or 'eventlet' — banned timeout = 30 ``` Log paths: `/home/jqc/logs/gunicorn-error.log`, `/home/jqc/logs/gunicorn-access.log` ### Application Logging Configured in `create_app()`: - `RotatingFileHandler` → `logs/jqc.log` (5 × 5 MB) - `StreamHandler` → stdout (captured by journalctl) - Format: `[YYYY-MM-DD HH:MM:SS] LEVEL in module: message` - Applied to both `app.logger` and the root logger ### Nginx - Reverse proxies to `127.0.0.1:8000` - Must set `client_max_body_size 50M` to match `MAX_CONTENT_LENGTH` - Passes `X-Forwarded-For` for IP capture in audit logs ### Deployment Sequence (typical change) ```bash git pull pip install -r requirements.txt # if dependencies changed flask db upgrade # if migrations added sudo systemctl restart jqc # or equivalent gunicorn restart ``` --- ## 18. Known Constraints & Hard Rules | # | Rule | Rationale | |---|---|---| | 1 | **No SSE** | Exhausted Gunicorn sync worker pool in production | | 2 | **`now_eastern()` always** | `datetime.utcnow()` caused incorrect SLA purge cutoffs | | 3 | **3-step MySQL ENUM changes** | Skipping steps causes data loss | | 4 | **Port 465 → SSL, port 587 → STARTTLS** | Both flags True breaks Flask-Mail | | 5 | **`CSRFProtect` as extension instance, not per-route** | Flask-WTF 1.0+ removed `@csrf_exempt` | | 6 | **`supervisor_required` decorator name preserved** | Changing it would touch 30+ route decorators | | 7 | **Score 0 = unanswered, exclude from calculation** | Not the same as a score of zero | | 8 | **12-column grid in PDF** | Must not collapse in print/PDF views | | 9 | **No nested `` tags** | Browsers silently discard inner forms | | 10 | **`log_action()` after `db.session.commit()`** | Entity ID must be committed before audit capture | | 11 | **`db.session.get(Model, id)` not `Model.query.get(id)`** | SQLAlchemy 2.x deprecation | | 12 | **`filter()` before `limit()`** | SQLAlchemy ordering requirement | | 13 | **Bulk queries in customer list** | Per-customer query loop caused N+1 in `/customers` | | 14 | **Email sent in background thread** | Never block HTTP response | | 15 | **Open-redirect guard on login** | `_safe_next()` validates relative URLs only | --- ## 19. Change Philosophy 1. **Surgical, additive patches** — smallest possible change to achieve the goal 2. **Preserve all routes, function names, and variable names** unless explicitly asked to rename 3. **Never remove existing functionality** unless explicitly directed 4. **Log all create/update/delete actions** via `log_action()` 5. **Test migrations with existence checks** so they are safe to re-run 6. **Full file contents for 1–3 file changes**; zip package with placement map for larger changesets 7. **Explicit deploy instructions** always included (migration steps separated from code changes) 8. **Root cause analysis** on errors — never apply temporary workarounds