30 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: April 2026 (Phase 11 complete)
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)
- Notification System
- SLA Engine
- Audit Trail
- PDF Export
- Scheduled Reports
- 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 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 |
| 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 = Truein production (HTTP only for dev)SESSION_COOKIE_HTTPONLY = TrueSESSION_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
supervisorwas renamed todirectorin Phase 11. The ENUM no longer containssupervisor.
Key methods:
display_name→full_name.strip()or falls back tousernamegenerate_set_password_token(expires_hours=72)→ creates 64-hex token + expiryverify_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
@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_requiredis not renamed todirector_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, orNonefor non-customer roles facility_id = NULLassignment → access to all active facilities in that project- All customer-facing queries must filter by
facility_id.in_(scope)orarea.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
usernameanduser_roleso 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(excludescustomerrole — use/customers)FacilityForm,AreaFormInspectionTemplateForm,ChecklistItemForm,StartInspectionFormIssueForm,IssueUpdateFormProjectForm,ProjectAssignmentFormCustomerUserForm,CustomerAssignmentForm,CustomerInviteForm,SetPasswordFormScheduledReportForm
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
POST /api/v1/auth/login→ returnsaccess_token(JWT, 60 min) +refresh_token(opaque hex, 30 days)- App stores both in iOS Keychain
- Every authenticated request sends
Authorization: Bearer <access_token> - When access token expires →
POST /api/v1/auth/refresh→ token rotation (old revoked, new issued) 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_requireddecorator extracts and validates the token; setsg.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_keyvalues:admin,director,inspector,project_manager,customer,customcustomrole_key stores a JSON list of arbitrary email addresses- Matrix rows are auto-created from
MATRIX_DEFAULTSon 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 alldigest_mode— batch into digest rather than immediate senddigest_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_notifiedprevents duplicate notifications — tracked per-issue at level (at_risk/breached)breachedsupersedesat_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) orrequest.remote_addr usernameanduser_roleare 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_atandlast_sent_attrack 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_countbadge 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():
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.loggerand the root logger
Nginx
- Reverse proxies to
127.0.0.1:8000 - Must set
client_max_body_size 50Mto matchMAX_CONTENT_LENGTH - Passes
X-Forwarded-Forfor 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
- Surgical, additive patches — smallest possible change to achieve the goal
- Preserve all routes, function names, and variable names unless explicitly asked to rename
- Never remove existing functionality unless explicitly directed
- Log all create/update/delete actions via
log_action() - Test migrations with existence checks so they are safe to re-run
- Full file contents for 1–3 file changes; zip package with placement map for larger changesets
- Explicit deploy instructions always included (migration steps separated from code changes)
- Root cause analysis on errors — never apply temporary workarounds