Files
LT_Janitorial_Quality_Control/CLAUDE.md
T

30 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: April 2026 (Phase 11 complete)


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)
  10. Notification System
  11. SLA Engine
  12. Audit Trail
  13. PDF Export
  14. Scheduled Reports
  15. Alembic Migration Chain
  16. Frontend Conventions
  17. Infrastructure
  18. Known Constraints & Hard Rules
  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

# 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_namefull_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: openin_progresspending_verificationresolved

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

@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/<id>/edit, /users/<id>/delete, /users/<id>/toggle-active, /notification-matrix
dashboard / GET /
facilities /facilities CRUD + /facilities/<id>/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:

# 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:

csrf.exempt(api_bp)
register_api(app)

8. Utility Modules

app/utils/time_utils.py

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

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

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

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.

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 <access_token>
  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

# 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=<DIGEST_SECRET> — triggered by server cron, authenticates via DIGEST_SECRET.

Email Delivery

Emails are dispatched in a background thread:

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/<id>
  • 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=<DIGEST_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:

-- 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

flask db migrate -m "description"
flask db upgrade

All migration scripts include existence checks for safe re-runs.

Deprecated SQLAlchemy Patterns

# 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 <input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
  • Never nest <form> 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)

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():

  • RotatingFileHandlerlogs/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)

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 <form> 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 13 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