32 KiB
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
- Project Overview
- Tech Stack
- Repository Layout
- Environment & Configuration
- Database Models
- Role & Permission Matrix
- Blueprint Prefixes & Route Inventory
- Utility Modules
- Mobile API (Phase 7 / Phase A / Phase B)
- iPad Native App
- Notification System
- SLA Engine
- Audit Trail
- PDF Export
- Scheduled Reports
- Rate Limiting
- Alembic Migration Chain
- Frontend Conventions
- Infrastructure
- Known Constraints & Hard Rules
- 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 |
| 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_name → full_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
POST /api/v1/auth/login→ access token (60 min JWT) + refresh token (30 day opaque hex)- Bearer token on every request
POST /api/v1/auth/refresh→ token rotation (old revoked, new issued)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:
- iPad calls
POST /api/v1/photos/uploadwith multipart image - Server saves to
app/static/uploads/inspection_photos/orissue_photos/ - Returns
{ "server_path": "uploads/inspection_photos/uuid.jpg" } - iPad includes
server_pathin 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
float0–100 orNoneif 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 localFilePath → serverPath |
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 statesyncStatus: server submission state ("pending"|"synced"|"failed")
SyncManager Key Behaviours
- Fetch-then-filter pattern: All
processPhotoQueue,processInspectionQueue,processIssueQueuefetch all records and filter in Swift rather than using#Predicatewith string literals. This avoids a SwiftData#Predicatemacro type-inference bug with string comparisons across model boundaries. - Sequential reference data fetch:
pullReferenceData()uses sequentialawait(notasync let) to avoid Swift 6 actor-isolation warnings onDecodablestructs. - Photo-before-inspection ordering:
processPhotoQueueruns beforeprocessInspectionQueue. An inspection is only submitted after all itspendingPhotoshaveuploadStatus == "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.notAuthenticatedis thrown. - Keychain token storage:
kSecAttrAccessibleAfterFirstUnlock— tokens survive device reboot, accessible for background sync. - Photo upload: Multipart
form-databuilt 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 useINFORMATION_SCHEMA.STATISTICScheck first.batch_alter_table— SQLite-only workaround; do not use for MySQL migrations. Use directALTER TABLEstatements.- Migration deploy order: Always run
flask db upgradewith the oldapp/__init__.pystill in place if the new version imports models that reference columns the migration would add. Swap__init__.pyafter 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
RotatingFileHandler→logs/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
Recommended Cron Schedule
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
- Surgical, additive patches — smallest possible change to achieve the goal
- Preserve all routes, function names, variable names unless explicitly directed otherwise
- Never remove existing functionality unless explicitly directed
- Log all create/update/delete actions via
log_action() - Migration existence checks — all migrations safe to re-run
- Full file contents for 1–3 file changes; deployment map for larger changesets
- Explicit deploy instructions — migration steps separated from code steps
- Root cause analysis on errors — never apply temporary workarounds