JQC — Janitorial Quality Control System
A production-grade web application for managing janitorial service contracts, facility inspections, issue tracking, and client reporting.
Features
- Inspection Management — Execute structured inspections against configurable templates with a drag-and-drop form builder supporting ratings, pass/fail, photos, signatures, and free-form fields
- Issue Tracking — Full lifecycle management (open → in-progress → pending verification → resolved) with SLA enforcement, follower subscriptions, and resolution photo uploads
- Customer Portal — Scoped facility visibility for client accounts with invitation-based onboarding (email link, 72-hour token)
- Notification System — In-app + email notifications driven by an admin-controlled routing matrix; per-user preferences and digest mode
- Reports — On-demand PDF/CSV scorecards and scheduled recurring email reports (daily/weekly/monthly)
- Audit Trail — Immutable log of every create, update, and delete action with actor and IP capture
- Mobile API — JWT-authenticated REST API for the companion React Native / Expo mobile application
- Project Hierarchy — Facilities grouped into Projects with optional Project Manager assignment and per-project customer access control
Tech Stack
| Layer | Technology |
|---|---|
| Backend | Python / Flask |
| Database | MySQL |
| ORM / Migrations | SQLAlchemy + Alembic |
| Auth (web) | Flask-Login + Flask-WTF (CSRF) |
| Auth (API) | JWT + opaque refresh tokens |
| Flask-Mail (SMTP) | |
| ReportLab | |
| Frontend | Bootstrap 5, Chart.js, Jinja2 |
| Server | Gunicorn + Nginx |
| Mobile | React Native + Expo |
Prerequisites
- Python 3.11+
- MySQL 8.x
- A configured SMTP server (port 465 or 587)
Installation
1. Clone and create a virtual environment
git clone <repo-url> jqc
cd jqc
python -m venv venv
source venv/bin/activate # Linux / macOS
# venv\Scripts\activate # Windows
2. Install dependencies
pip install -r requirements.txt
3. Create the database
CREATE DATABASE jqc CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
CREATE USER 'jqc'@'localhost' IDENTIFIED BY 'your_password';
GRANT ALL PRIVILEGES ON jqc.* TO 'jqc'@'localhost';
FLUSH PRIVILEGES;
4. Configure environment variables
Create a .env file in the project root (never commit this file):
SECRET_KEY=<long-random-string>
DATABASE_URL=mysql+pymysql://jqc:your_password@localhost/jqc
# Email
MAIL_SERVER=smtp.example.com
MAIL_PORT=587
MAIL_USERNAME=noreply@example.com
MAIL_PASSWORD=smtp_password
MAIL_DEFAULT_SENDER=noreply@example.com
# Application
APP_BASE_URL=https://your-domain.com
DIGEST_SECRET=<random-secret-for-cron-auth>
Email SSL: Port 465 uses implicit SSL (
MAIL_USE_SSL=True). Port 587 uses STARTTLS (MAIL_USE_TLS=True). The application auto-detects based onMAIL_PORT— never set both flags to True.
5. Run database migrations
flask db upgrade
6. Start the development server
python run.py
The application will be available at http://localhost:5000.
Production Deployment
Gunicorn
gunicorn -c gunicorn_config.py wsgi:app
The included gunicorn_config.py binds to 127.0.0.1:8000 with sync workers. Log files are written to /home/jqc/logs/.
Nginx (recommended configuration)
server {
listen 443 ssl;
server_name your-domain.com;
client_max_body_size 50M; # must match MAX_CONTENT_LENGTH in config.py
location / {
proxy_pass http://127.0.0.1:8000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
location /static/ {
alias /path/to/jqc/app/static/;
expires 30d;
}
}
Systemd service (example)
[Unit]
Description=JQC Gunicorn
After=network.target mysql.service
[Service]
User=jqc
WorkingDirectory=/home/jqc/lt_janitorial_quality_control
EnvironmentFile=/home/jqc/.env
ExecStart=/home/jqc/venv/bin/gunicorn -c gunicorn_config.py wsgi:app
Restart=on-failure
[Install]
WantedBy=multi-user.target
Cron Jobs
Two background tasks require scheduled execution:
SLA Alerts
Checks all open issues against their SLA window and dispatches notifications:
# Run every 30 minutes
*/30 * * * * /home/jqc/venv/bin/python -c "
from app import create_app
from app.utils.sla import send_sla_alerts
app = create_app('production')
with app.app_context():
send_sla_alerts()
"
Digest Emails and Scheduled Reports
# Daily digest — run at 7:00 AM
0 7 * * * curl -s -X POST "https://your-domain.com/notifications/send-digest?frequency=daily&secret=YOUR_DIGEST_SECRET"
# Scheduled reports
0 8 * * * curl -s -X POST "https://your-domain.com/scheduled-reports/run?secret=YOUR_DIGEST_SECRET"
User Roles
| Role | Description |
|---|---|
| admin | Full access to all features including Audit Trail and Notification Matrix |
| director | Broad access equivalent to admin, excluding Audit Trail and Notification Matrix |
| project_manager | Manages projects, facilities, and reports; cannot manage users or system settings |
| inspector | Executes inspections and manages assigned issues |
| customer | Read-only portal scoped to assigned facilities; receives notifications on their facilities |
Creating the First Admin Account
flask shell
>>> from app import db
>>> from app.models.user import User
>>> u = User(username='admin', email='admin@example.com', role='admin')
>>> u.set_password('your-secure-password')
>>> db.session.add(u)
>>> db.session.commit()
Customer Onboarding
- Admin navigates to Customers → Invite Customer
- Enter the customer's name and email address
- The system auto-generates a username and sends an invitation email with a 72-hour setup link
- Customer clicks the link, sets their password, and gains access to their scoped portal
- Admin assigns the customer to one or more Projects/Facilities via Customers → Manage Assignments
Mobile API
The REST API is available at /api/v1/ and uses JWT Bearer token authentication.
Authentication Endpoints
| Method | Endpoint | Description |
|---|---|---|
| POST | /api/v1/auth/login |
Login; returns access + refresh tokens |
| POST | /api/v1/auth/refresh |
Rotate refresh token; returns new access token |
| POST | /api/v1/auth/logout |
Revoke refresh token |
| GET | /api/v1/auth/me |
Return current user profile |
| POST | /api/v1/devices/register |
Register APNs device token for push notifications |
Access tokens expire after 60 minutes. Refresh tokens are valid for 30 days and rotate on every use.
Database Migrations
# Apply all pending migrations
flask db upgrade
# Create a new migration after model changes
flask db migrate -m "description of change"
# Roll back one migration
flask db downgrade
MySQL ENUM changes require three steps: expand the ENUM to include both values, migrate existing data, then contract the ENUM. See
Claude.mdfor the full protocol.
Application Logs
| Log File | Contents |
|---|---|
logs/jqc.log |
Application log (rotating, 5 × 5 MB) |
/home/jqc/logs/gunicorn-error.log |
Gunicorn worker errors |
/home/jqc/logs/gunicorn-access.log |
HTTP access log |
Configuration Reference
| Variable | Default | Description |
|---|---|---|
SECRET_KEY |
— required | Flask session signing key |
DATABASE_URL |
— required | SQLAlchemy connection URI |
MAIL_SERVER |
— | SMTP hostname |
MAIL_PORT |
587 |
SMTP port (465 = SSL, 587 = STARTTLS) |
MAIL_USERNAME |
— | SMTP username |
MAIL_PASSWORD |
— | SMTP password |
MAIL_DEFAULT_SENDER |
noreply@janitorialqc.local |
From address |
APP_BASE_URL |
"" |
Base URL for links in emails |
DIGEST_SECRET |
— | Token for authenticating cron requests |
MAX_CONTENT_LENGTH |
50MB |
Maximum upload size per request |
License
See LICENSE for terms.