Files
LT_Janitorial_Quality_Control/CLAUDE.md
T
2026-05-02 13:23:59 -04:00

32 KiB
Raw Blame History

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: May 2026 (Phase B complete — iPad offline inspection app)


Table of Contents

  1. Project Overview
  2. Tech Stack
  3. Repository Layout
  4. Environment & Configuration
  5. Database Models
  6. Role & Permission Matrix
  7. Blueprint Prefixes & Route Inventory
  8. Utility Modules
  9. Mobile API (Phase 7 / Phase A / Phase B)
  10. iPad Native App
  11. Notification System
  12. SLA Engine
  13. Audit Trail
  14. PDF Export
  15. Scheduled Reports
  16. Rate Limiting
  17. Alembic Migration Chain
  18. Frontend Conventions
  19. Infrastructure
  20. Known Constraints & Hard Rules
  21. Change Philosophy

1. Project Overview

JQC (Janitorial Quality Control) is a production-grade, full-stack web application that manages:

  • Janitorial service contracts organised as Contracts (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 for the iPad native app
  • iPad native app — SwiftUI + SwiftData offline-first inspection tool (Phase A + B complete)

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)
Rate limiting Flask-Limiter (Redis-backed in production via REDIS_URL; falls back to in-process memory for dev)
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
iPad app SwiftUI + SwiftData, iOS 17+, Xcode 15+
iPad networking URLSession async/await + NWPathMonitor
iPad auth storage iOS Keychain (Security.framework)
Timezone All datetimes stored as US/Eastern (naive, via now_eastern())

3. Repository Layout

lt_janitorial_quality_control/
├── app/
│   ├── __init__.py              # Application factory — limiter, csrf, db, mail, login_manager
│   ├── api/                     # Mobile REST API
│   │   ├── __init__.py          # api_bp parent blueprint + register_api()
│   │   ├── auth.py              # /api/v1/auth/* and /api/v1/devices/*
│   │   ├── facilities.py        # /api/v1/facilities/* (Phase A)
│   │   ├── templates.py         # /api/v1/templates/* (Phase A)
│   │   ├── inspections.py       # /api/v1/inspections/* (Phase B)
│   │   ├── issues.py            # /api/v1/issues/* (Phase B)
│   │   ├── photos.py            # /api/v1/photos/upload (Phase B)
│   │   ├── decorators.py        # @jwt_required
│   │   ├── errors.py            # JSON error helpers + error handler registration
│   │   └── jwt_utils.py         # generate_access_token()
│   ├── models/
│   │   ├── inspection.py        # Inspection now has mobile_local_id column (Phase B)
│   │   ├── issue.py             # Issue now has mobile_local_id column (Phase B)
│   │   └── ...                  # (all other models unchanged)
│   ├── routes/                  # (unchanged from Phase 12)
│   ├── static/
│   ├── templates/
│   └── utils/
├── 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
│       ├── phase12_performance_indexes.py
│       └── phase_b_mobile_local_id.py   ← HEAD
├── JanitorialQC/                # Xcode iOS project root
│   ├── JanitorialQC.xcodeproj
│   └── JanitorialQC/
│       ├── JQCApp.swift             # @main — SwiftData container, env objects
│       ├── ContentView.swift        # Auth gate: LoginView ↔ DashboardView
│       ├── Auth/
│       │   ├── AuthManager.swift    # Login/logout/restore session, Keychain persistence
│       │   └── KeychainHelper.swift # Security.framework wrapper
│       ├── API/
│       │   ├── APIClient.swift      # URLSession + JWT inject + 401 retry + photo upload
│       │   └── APIModels.swift      # Codable response DTOs
│       ├── Sync/
│       │   └── SyncManager.swift    # NWPathMonitor + outbox queue processor
│       ├── Models/                  # SwiftData local models
│       │   ├── LocalFacility.swift
│       │   ├── LocalArea.swift
│       │   ├── LocalTemplate.swift
│       │   ├── LocalInspection.swift
│       │   ├── LocalIssue.swift
│       │   ├── PendingPhoto.swift
│       │   └── SyncQueueEntry.swift
│       ├── Views/
│       │   ├── Auth/
│       │   │   └── LoginView.swift
│       │   ├── Dashboard/
│       │   │   └── DashboardView.swift  # Sidebar + all detail views
│       │   └── Inspection/
│       │       ├── StartInspectionView.swift
│       │       ├── ExecuteInspectionView.swift
│       │       ├── FlagIssueView.swift
│       │       └── FormRenderer/
│       │           └── FormFieldView.swift  # All field type renderers
│       └── Utils/
│           └── Constants.swift      # baseURL, Keychain key strings
├── config.py
├── gunicorn_config.py
├── requirements.txt
├── run.py
└── wsgi.py

4. Environment & Configuration

Required Environment Variables

Variable Notes
SECRET_KEY Flask secret — no fallback; startup fails if absent
DATABASE_URL 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 flags
APP_BASE_URL Full URL for email links
MAIL_DEFAULT_SENDER From address
DIGEST_SECRET Authenticates all cron endpoints
REDIS_URL Optional. When set, Flask-Limiter uses Redis for shared rate-limit counters across Gunicorn workers.

Email SSL Auto-Detection

MAIL_USE_SSL  = _mail_port == 465
MAIL_USE_TLS  = not MAIL_USE_SSL

Critical: Never set both to True — Flask-Mail breaks silently.

File Uploads

  • UPLOAD_FOLDER = app/static/uploads/
  • MAX_CONTENT_LENGTH = 50 MB
  • Allowed: png, jpg, jpeg, gif

5. Database Models

User

users: id, username (unique, indexed), full_name, email (unique, indexed),
       password_hash, role (ENUM), created_at, active,
       password_set, set_password_token (indexed), set_password_token_expires

Role ENUM: admin, director, inspector, project_manager, customer

Key property: display_namefull_name.strip() or falls back to username.

Facility / Area

facilities: id, name, address, contact_person, contact_phone, active, project_id (FK)
areas:      id, facility_id (FK), name, area_type

area_type choices: restroom, lobby, hallway, office, kitchen, storage, floor, outdoor, other

Project / CustomerAssignment

projects: id, name, description, project_manager_id, active, created_at
customer_assignments: id, user_id, project_id, facility_id (nullable)
    UniqueConstraint(user_id, project_id, facility_id)

Inspection

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,
             mobile_local_id VARCHAR(64) nullable indexed   ← Phase B

mobile_local_id: UUID string generated on the iPad. Used for idempotency — if a submission arrives twice (network retry), the server returns the existing record without creating a duplicate. Set NULL for all web-created inspections.

Score rule: Items with score = 0 mean "unanswered" — excluded from calculation entirely.

Issue

issues: id, inspection_id (nullable), area_id, severity (low/medium/high/critical),
        description, photo_path, status (open/in_progress/resolved/pending_verification),
        assigned_to, reported_at, resolved_at, result_notes, result_photos (JSON),
        verified_by, verified_at, verification_note, sla_notified,
        mobile_local_id VARCHAR(64) nullable indexed   ← Phase B

mobile_local_id: Same idempotency pattern as inspections.mobile_local_id.

Notification / NotificationPreference

notifications: id, user_id, title, body, link, is_read, created_at, issue_id, 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)
    UniqueConstraint(event_type, role_key)

AuditLog

audit_logs: id, user_id (nullable), 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)

6. Role & Permission Matrix

Area admin director project_manager inspector customer
Dashboard full full full limited scoped
Users
Notification Matrix only
Customers
Facilities read scoped
Contracts read scoped
Templates
Inspections (execute) read
Issues (create/assign) read
Issues (quick-assign)
Issue verification
Reports scoped
Scheduled Reports
Audit Trail only
Mobile API

Decorator Map

@admin_required            # role == 'admin' only
@supervisor_required       # role in ('admin', 'director') — name kept to avoid touching 30+ routes
@project_manager_required  # role in ('admin', 'director', 'project_manager')
@customer_required         # role == 'customer' only

7. Blueprint Prefixes & Route Inventory

Blueprint Prefix Notable routes
auth /auth /login, /logout, /profile, /users/*, /notification-matrix
dashboard / GET /, /facility-trend (AJAX)
facilities /facilities CRUD + area management
projects /projects CRUD + customer assignment management
customers /customers list, invite, set-password, manage, import CSV
inspections /inspections list, start, execute, view, PDF export, flag-issue, save-draft (AJAX), flag-followup, reinspect
templates /templates list, create, edit, delete, form editor, preview
issues /issues list, view, create, update, verify, comment, follow/unfollow, verification queue, delete, quick-assign
notifications /notifications list, mark-read, preferences, send-digest (cron), check-sla (cron), cleanup-tokens (cron)
audit /audit list (admin only), view, purge
reports /reports index, facility report, scorecard, CSV/PDF export
scheduled_reports /scheduled-reports CRUD + manual trigger
api /api/v1 parent blueprint
api_auth /api/v1 /auth/login, /auth/refresh, /auth/logout, /auth/me, /devices/register
api_facilities /api/v1 /facilities, /facilities/<id>/areas
api_templates /api/v1 /templates, /templates/<id>
api_inspections /api/v1 POST /inspections, PATCH /inspections/<id>
api_issues /api/v1 POST /issues
api_photos /api/v1 POST /photos/upload

8. Utility Modules

time_utils.py

now_eastern() — always use this, never datetime.utcnow().

audit.py

log_action(action, entity_type, entity_id, entity_label, details) — call after db.session.commit().

scope.py

get_customer_scope(user) — returns list[int] facility IDs for customers, None for staff.

forms.py

All WTForms classes. AreaForm.area_type includes floor. UserForm excludes customer role.

notifications.py

notify(), notify_by_matrix(), notify_customers_for_facility() — all email sent in background thread.

sla.py

sla_status(issue)'ok' | 'at_risk' | 'breached' | None (resolved).

pdf_export.py

ReportLab-based. 12-column grid must be preserved — never collapse in PDF views.


9. Mobile API (Phase 7 / Phase A / Phase B)

CSRF Exemption Pattern — Critical

csrf.exempt(api_bp) does NOT cascade to sub-blueprints. Flask-WTF's _is_exempt() checks the leaf blueprint object. Each child blueprint must be exempted individually in app/__init__.py:

from app.api import register_api, api_bp
from app.api.auth        import bp as _api_auth_bp
from app.api.facilities  import bp as _api_facilities_bp
from app.api.templates   import bp as _api_templates_bp
from app.api.inspections import bp as _api_inspections_bp
from app.api.issues      import bp as _api_issues_bp
from app.api.photos      import bp as _api_photos_bp
csrf.exempt(_api_auth_bp)
csrf.exempt(_api_facilities_bp)
csrf.exempt(_api_templates_bp)
csrf.exempt(_api_inspections_bp)
csrf.exempt(_api_issues_bp)
csrf.exempt(_api_photos_bp)
register_api(app)

Every new Phase C+ blueprint must add its own csrf.exempt() line here before register_api(app). Failing to do so produces a "The CSRF token is missing." error on all POST requests to that blueprint.

Auth Flow

  1. POST /api/v1/auth/login → access token (60 min JWT) + refresh token (30 day opaque hex)
  2. Bearer token on every request
  3. POST /api/v1/auth/refresh → token rotation (old revoked, new issued)
  4. POST /api/v1/auth/logout → revokes refresh token

Rate Limits

Endpoint Limit
POST /api/v1/auth/login 10/min, 3/sec
POST /api/v1/auth/refresh 30/min, 5/sec

Phase A Endpoints

Endpoint Auth Description
GET /api/v1/facilities jwt_required All active facilities scoped to user
GET /api/v1/facilities/<id>/areas jwt_required Areas for a facility
GET /api/v1/templates jwt_required Template list (summary, no form_schema)
GET /api/v1/templates/<id> jwt_required Full template with form_schema

Customer role is blocked from template endpoints (_ALLOWED_ROLES check). Facility endpoints honour get_customer_scope().

Phase B Endpoints

Endpoint Auth Description
POST /api/v1/inspections jwt_required Create inspection; idempotent via mobile_local_id
PATCH /api/v1/inspections/<id> jwt_required Update inspection (draft → completed)
POST /api/v1/issues jwt_required Create issue; idempotent via mobile_local_id
POST /api/v1/photos/upload jwt_required Multipart photo upload; returns server_path

Idempotency Pattern

All Phase B write endpoints accept mobile_local_id (UUID string from device). On receipt:

existing = Model.query.filter_by(mobile_local_id=mobile_local_id).first()
if existing:
    return api_ok({'id': existing.id, 'duplicate': True})

This protects against double-submission when the network fails after the server commits but before the device receives the response. Web-created records have mobile_local_id = NULL.

Photo Upload Flow

Photos are uploaded before the inspection or issue is submitted:

  1. iPad calls POST /api/v1/photos/upload with multipart image
  2. Server saves to app/static/uploads/inspection_photos/ or issue_photos/
  3. Returns { "server_path": "uploads/inspection_photos/uuid.jpg" }
  4. iPad includes server_path in the subsequent inspection/issue POST

Score Calculation (Server-Side)

app/api/inspections.py::_compute_score() mirrors routes/inspections.py::_compute_score_from_form() exactly:

  • Rating value 0 = unanswered → excluded from total
  • pass_fail accepted values: pass, yes, ok, good, acceptable, compliant
  • Returns float 0100 or None if no scoreable fields

10. iPad Native App

Platform

  • Language: Swift 5.10+
  • UI: SwiftUI (iPad-only, all four orientations)
  • Local DB: SwiftData (iOS 17+ required)
  • Networking: URLSession async/await
  • Connectivity: NWPathMonitor (Network.framework)
  • Token storage: iOS Keychain (Security.framework)
  • Xcode: 15+

Offline-First Architecture

The app follows the outbox pattern — every inspector action writes to SwiftData first; the server is a secondary destination.

Inspector action → SwiftData write (always succeeds) → SyncQueue entry
                                                              ↓
                                        NWPathMonitor detects reconnect
                                                              ↓
                                        SyncManager.triggerSync()
                                        1. Upload pending photos
                                        2. Submit completed inspections
                                        3. Submit pending issues
                                        4. Pull fresh reference data

SwiftData Models

Model Purpose
LocalFacility Cached facility reference data (read-only on device)
LocalArea Cached area reference data
LocalTemplate Cached template + formSchemaJSON (raw JSON string)
LocalInspection Inspector-created inspection records
LocalIssue Issues flagged during inspections
PendingPhoto Photos awaiting upload; tracks localFilePathserverPath
SyncQueueEntry Outbox queue (currently unused directly — filtering done in Swift)

LocalInspection Status Flow

"draft"  →  "completed"  →  "synced"
                         →  "failed" (after 5 retries)

syncStatus is separate from status:

  • status: inspector workflow state
  • syncStatus: server submission state ("pending" | "synced" | "failed")

SyncManager Key Behaviours

  • Fetch-then-filter pattern: All processPhotoQueue, processInspectionQueue, processIssueQueue fetch all records and filter in Swift rather than using #Predicate with string literals. This avoids a SwiftData #Predicate macro type-inference bug with string comparisons across model boundaries.
  • Sequential reference data fetch: pullReferenceData() uses sequential await (not async let) to avoid Swift 6 actor-isolation warnings on Decodable structs.
  • Photo-before-inspection ordering: processPhotoQueue runs before processInspectionQueue. An inspection is only submitted after all its pendingPhotos have uploadStatus == "uploaded" or "failed".
  • Retry limit: 5 retries per item before marking syncStatus = "failed".

APIClient Key Behaviours

  • 401 auto-retry: On a 401 response, refreshAccessToken() is called once and the original request is retried. If refresh fails, APIError.notAuthenticated is thrown.
  • Keychain token storage: kSecAttrAccessibleAfterFirstUnlock — tokens survive device reboot, accessible for background sync.
  • Photo upload: Multipart form-data built manually (no third-party library). Boundary is a UUID string.

FormFieldView — Supported Field Types

All types from the web app's INPUT_FIELD_TYPES set are rendered:

Type SwiftUI renderer
text, email TextField
textarea TextEditor
number TextField + .decimalPad
date DatePicker
checkbox Toggle
checkbox_group Custom multi-select buttons
radio Custom radio buttons
select Picker(.menu)
rating Custom star rating (tap same star to clear)
pass_fail Two-button Pass/Fail control
signature PKCanvasView (PencilKit)
image UIImagePickerController sheet → local file save
table Grid of TextField
section, label Display-only Text

Known iOS-Specific Constraints

# Constraint Rationale
1 import Combine required for @Published Swift 5.9+ does not auto-import Combine; ObservableObject without it causes build errors
2 NavigationSplitView — no selection: binding init(selection:content:) unavailable on iPadOS 17; use List with manual Button + @State var selectedTab
3 #Predicate — no string literal comparisons across model boundaries SwiftData macro type-inference bug; fetch all + filter in Swift instead
4 async let — Swift 6 actor-isolation warnings on Decodable Use sequential await calls for reference data fetches
5 PencilKit requires framework linkage Add PencilKit.framework under Target → Frameworks, Libraries, and Embedded Content
6 Free Apple ID provisioning expires every 7 days Rebuild with ⌘R while iPad is connected; SwiftData persists across reinstalls
7 kSecAttrAccessibleAfterFirstUnlock for background sync Tokens must be readable when the app is woken by BGTaskScheduler

11. Notification System

Event Constants (app/models/notification.py)

inspection_completed, issue_assigned, issue_reassigned, issue_unassigned,
issue_status, issue_comment, issue_follow_update, issue_flagged, issue_created,
issue_updated_customer, verification_requested, sla_alert,
customer_inspection_completed

Cron Endpoints (all require token=DIGEST_SECRET)

Endpoint Purpose Schedule
POST /notifications/send-digest Digest email delivery 0 7 * * *
POST /notifications/check-sla SLA breach/at-risk alerts */30 * * * *
POST /notifications/cleanup-tokens Purge expired API tokens 0 3 * * *

12. SLA Engine

Severity Window At-Risk
critical 4h 3h
high 24h 18h
medium 72h 54h
low 168h 126h

issue.sla_notified prevents duplicate cron notifications.


13. Audit Trail

  • Admin-only at /audit/ — director is excluded
  • Actions: CREATE, UPDATE, DELETE, LOGIN, LOGOUT, EXPORT
  • Mobile API routes call log_action() for all create/update operations
  • Immutable — never updated or deleted through the application

14. PDF Export

ReportLab — app/utils/pdf_export.py. 12-column grid must be preserved — do not collapse in print/PDF.


15. Scheduled Reports

Types: summary, facility, issues. Frequencies: daily, weekly, monthly. Cron: POST /scheduled-reports/run?secret=<DIGEST_SECRET>


16. Rate Limiting

limiter = Limiter(
    key_func       = get_remote_address,
    default_limits = [],
    storage_uri    = os.environ.get('REDIS_URL', 'memory://'),
)

Production: Set REDIS_URL=redis://127.0.0.1:6379/0. Counters shared across all Gunicorn workers.


17. 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
        → phase12_performance_indexes → phase_b_mobile_local_id   ← HEAD

phase_b_mobile_local_id

Adds mobile_local_id VARCHAR(64) NULL + index to both inspections and issues. Uses INFORMATION_SCHEMA.COLUMNS and INFORMATION_SCHEMA.STATISTICS existence checks — safe to re-run.

MySQL ENUM Change Protocol (3 steps — always follow)

-- 1. Expand
ALTER TABLE users MODIFY COLUMN role ENUM('admin','supervisor','director',...) NOT NULL;
-- 2. Migrate
UPDATE users SET role = 'director' WHERE role = 'supervisor';
-- 3. Contract
ALTER TABLE users MODIFY COLUMN role ENUM('admin','director',...) NOT NULL;

MySQL Compatibility Rules

  • CREATE INDEX IF NOT EXISTS — not supported on MySQL < 8.0.12. Always use INFORMATION_SCHEMA.STATISTICS check first.
  • batch_alter_table — SQLite-only workaround; do not use for MySQL migrations. Use direct ALTER TABLE statements.
  • Migration deploy order: Always run flask db upgrade with the old app/__init__.py still in place if the new version imports models that reference columns the migration would add. Swap __init__.py after the migration succeeds.

Deprecated SQLAlchemy Patterns

# WRONG
Model.query.get(id)

# CORRECT
obj = db.session.get(Model, id)
if obj is None: abort(404)

18. Frontend Conventions

Active Nav Tab

Detected via request.endpoint.startswith('<blueprint>.') in each nav <a> tag.

Display Names

Always use user.display_name in templates — never .username for display purposes.

Status Label Map

DB value Displayed as
completed Submitted
in_progress In Progress
flagged Flagged
open Open
resolved Resolved
pending_verification Pending Verification

Forms

  • Flask-WTF CSRF auto-applied to all web forms
  • Never nest <form> tags — browsers silently discard inner forms

Real-Time

SSE banned. All "live" updates use polling.


19. Infrastructure

Gunicorn

bind = "127.0.0.1:8000"
workers = multiprocessing.cpu_count() * 2 + 1
worker_class = "sync"
timeout = 30

Application Logging

  • RotatingFileHandlerlogs/jqc.log (5 × 5 MB)
  • StreamHandler → stdout (journalctl)
  • Format: [YYYY-MM-DD HH:MM:SS] LEVEL in module: message

Nginx

  • client_max_body_size 50M
  • Passes X-Forwarded-For
0 7    * * * curl -s -X POST https://your-domain.com/notifications/send-digest \
                   -d "token=SECRET&frequency=daily"
*/30 * * * * curl -s -X POST https://your-domain.com/notifications/check-sla \
                   -d "token=SECRET"
0 3    * * * curl -s -X POST https://your-domain.com/notifications/cleanup-tokens \
                   -d "token=SECRET"
0 8    * * * curl -s -X POST https://your-domain.com/scheduled-reports/run \
                   -d "secret=SECRET"

20. Known Constraints & Hard Rules

# Rule Rationale
1 No SSE Exhausted Gunicorn sync worker pool
2 now_eastern() always utcnow() caused incorrect SLA cutoffs
3 3-step MySQL ENUM changes Skipping causes data loss
4 Port 465 → SSL; 587 → STARTTLS Both True breaks Flask-Mail
5 csrf.exempt() on each child blueprint individually csrf.exempt(api_bp) does NOT cascade; Flask-WTF checks leaf blueprint object only
6 supervisor_required name preserved Renaming would touch 30+ route decorators
7 Score 0 = unanswered Excluded from calculation — not the same as scoring zero
8 12-column grid in PDF Must not collapse in print/PDF
9 No nested <form> tags Browsers silently discard inner forms
10 log_action() after db.session.commit() Entity ID must exist 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 loops cause N+1
14 Email in background thread Never block HTTP response
15 Open-redirect guards _safe_next() in auth.py; _safe_referrer() in customers.py
16 CREATE INDEX IF NOT EXISTS not on MySQL < 8.0.12 Use INFORMATION_SCHEMA.STATISTICS check
17 batch_alter_table is SQLite-only Use direct ALTER TABLE for MySQL migrations
18 Set REDIS_URL in production memory:// is per-process; Gunicorn needs Redis for accurate shared counters
19 "Project" → "Contract" is UI-only Backend identifiers unchanged
20 display_name not username in templates Respects full_name; username is login identity only
21 mobile_local_id idempotency on all mobile write endpoints Network retries must not create duplicate records
22 Photo upload before inspection/issue submission Server path must be known before the parent record is created
23 Migration deploy before new app/__init__.py New init imports models referencing new columns; columns must exist first
24 import Combine required in iOS files using @Published Swift 5.9+ does not auto-import Combine
25 No selection: binding on NavigationSplitView init(selection:content:) unavailable on iPadOS 17
26 SwiftData #Predicate — fetch all + filter in Swift for string comparisons Macro type-inference bug with string literals across model type boundaries
27 Sequential await for reference data fetches in SyncManager async let causes Swift 6 actor-isolation warnings on Decodable structs
28 hmac.compare_digest() for token comparison Prevents timing oracle attacks
29 get_customer_scope() uses bulk project query Replaces per-assignment loop
30 CSV exports always call log_action(ACTION_EXPORT, ...) Data exports are compliance-relevant audit events

21. Change Philosophy

  1. Surgical, additive patches — smallest possible change to achieve the goal
  2. Preserve all routes, function names, variable names unless explicitly directed otherwise
  3. Never remove existing functionality unless explicitly directed
  4. Log all create/update/delete actions via log_action()
  5. Migration existence checks — all migrations safe to re-run
  6. Full file contents for 13 file changes; deployment map for larger changesets
  7. Explicit deploy instructions — migration steps separated from code steps
  8. Root cause analysis on errors — never apply temporary workarounds