04/25 updated Claude.md, Readme.md, and .gitignore
This commit is contained in:
@@ -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.
|
||||
Reference in New Issue
Block a user