04/25 updated Claude.md, Readme.md, and .gitignore

This commit is contained in:
2026-04-25 10:25:49 -04:00
parent 1726be726d
commit 9d9880e20b
3 changed files with 1067 additions and 3 deletions
+293 -1
View File
@@ -1 +1,293 @@
# janitorial_qc
# 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 |
| Email | Flask-Mail (SMTP) |
| PDF | 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
```bash
git clone <repo-url> jqc
cd jqc
python -m venv venv
source venv/bin/activate # Linux / macOS
# venv\Scripts\activate # Windows
```
### 2. Install dependencies
```bash
pip install -r requirements.txt
```
### 3. Create the database
```sql
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):
```dotenv
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 on `MAIL_PORT` — never set both flags to True.
### 5. Run database migrations
```bash
flask db upgrade
```
### 6. Start the development server
```bash
python run.py
```
The application will be available at `http://localhost:5000`.
---
## Production Deployment
### Gunicorn
```bash
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)
```nginx
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)
```ini
[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:
```bash
# 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
```bash
# 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
```bash
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
1. Admin navigates to **Customers → Invite Customer**
2. Enter the customer's name and email address
3. The system auto-generates a username and sends an invitation email with a 72-hour setup link
4. Customer clicks the link, sets their password, and gains access to their scoped portal
5. 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
```bash
# 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.md` for 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.