Files
LT_Janitorial_Quality_Control/CLAUDE.md
T
2026-04-25 13:14:00 -04:00

618 lines
24 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 12 complete)
---
## Table of Contents
1. [Project Overview](#1-project-overview)
2. [Tech Stack](#2-tech-stack)
3. [Repository Layout](#3-repository-layout)
4. [Environment & Configuration](#4-environment--configuration)
5. [Database Models](#5-database-models)
6. [Role & Permission Matrix](#6-role--permission-matrix)
7. [Blueprint Prefixes & Route Inventory](#7-blueprint-prefixes--route-inventory)
8. [Utility Modules](#8-utility-modules)
9. [Mobile API (Phase 7)](#9-mobile-api-phase-7)
10. [Notification System](#10-notification-system)
11. [SLA Engine](#11-sla-engine)
12. [Audit Trail](#12-audit-trail)
13. [PDF Export](#13-pdf-export)
14. [Scheduled Reports](#14-scheduled-reports)
15. [Rate Limiting](#15-rate-limiting)
16. [Alembic Migration Chain](#16-alembic-migration-chain)
17. [Frontend Conventions](#17-frontend-conventions)
18. [Infrastructure](#18-infrastructure)
19. [Known Constraints & Hard Rules](#19-known-constraints--hard-rules)
20. [Change Philosophy](#20-change-philosophy)
---
## 1. Project Overview
**JQC (Janitorial Quality Control)** is a production-grade, full-stack web application that manages:
- Janitorial service contracts organised as **Contracts (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) |
| Rate limiting | Flask-Limiter (in-memory storage; swap for Redis in multi-worker) |
| 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 — limiter, csrf, db, mail, login_manager
│ ├── 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/*; rate-limited login/refresh
│ │ ├── decorators.py # @jwt_required
│ │ ├── errors.py # JSON error helpers + error handler registration
│ │ └── jwt_utils.py # generate_access_token()
│ ├── models/
│ │ ├── __init__.py
│ │ ├── 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
│ │ ├── project.py # Project, CustomerAssignment
│ │ ├── scheduled_report.py # ScheduledReport
│ │ └── user.py # User (password-setup workflow, display_name)
│ ├── routes/
│ │ ├── auth.py # /auth/* — login rate-limited; _safe_next() open-redirect guard
│ │ ├── customers.py # /customers/* — _safe_referrer() helper; expired invitation banner
│ │ ├── dashboard.py # / — single open_issues query (len() not .count())
│ │ ├── facilities.py # /facilities/*
│ │ ├── inspections.py # /inspections/* — passes now=now_eastern() for stale badge
│ │ ├── issues.py # /issues/* — quick-assign AJAX; facility filter; SLA column
│ │ ├── notifications.py # /notifications/* — send-digest, check-sla, cleanup-tokens
│ │ ├── projects.py # /projects/*
│ │ ├── reports.py # /reports/*
│ │ ├── scheduled_reports.py # /scheduled-reports/*
│ │ ├── templates.py # /templates/*
│ │ └── audit.py # /audit/*
│ ├── static/
│ │ ├── css/ipad_responsive.css
│ │ └── uploads/
│ ├── templates/
│ │ ├── base.html # Active nav tab styling; request.endpoint-based active detection
│ │ ├── _sla_badge.html
│ │ └── ...
│ └── utils/
│ ├── audit.py # log_action() + ACTION_* constants
│ ├── decorators.py # @admin_required, @supervisor_required, @project_manager_required, @customer_required
│ ├── forms.py # All WTForms (AreaForm includes 'floor' type)
│ ├── notifications.py # notify(), notify_by_matrix(), send_pending_digests()
│ ├── pdf_export.py # ReportLab PDF generation
│ ├── scope.py # get_customer_scope()
│ ├── 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
│ └── phase12_performance_indexes.py ← HEAD
├── config.py
├── gunicorn_config.py
├── requirements.txt # Includes Flask-Limiter
├── run.py
└── wsgi.py
```
---
## 4. Environment & Configuration
### Required Environment Variables
| Variable | Notes |
|---|---|
| `SECRET_KEY` | Flask secret — no fallback; startup fails if absent |
| `DATABASE_URL` | 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 flags |
| `APP_BASE_URL` | Full URL for email links |
| `MAIL_DEFAULT_SENDER` | From address |
| `DIGEST_SECRET` | Authenticates all cron endpoints: `send-digest`, `check-sla`, `cleanup-tokens`, `scheduled-reports/run` |
### Email SSL Auto-Detection
```python
MAIL_USE_SSL = _mail_port == 465
MAIL_USE_TLS = not MAIL_USE_SSL
```
**Critical:** Never set both to `True` — Flask-Mail breaks silently.
### File Uploads
- `UPLOAD_FOLDER` = `app/static/uploads/`
- `MAX_CONTENT_LENGTH` = 50 MB
- Allowed: `png`, `jpg`, `jpeg`, `gif`
---
## 5. Database Models
### User
```
users: id, username (unique, indexed), full_name, email (unique, indexed),
password_hash, role (ENUM), created_at, active,
password_set, set_password_token (indexed), set_password_token_expires
```
**Role ENUM:** `admin`, `director`, `inspector`, `project_manager`, `customer`
**Key property:** `display_name``full_name.strip()` or falls back to `username`. Use this everywhere in templates — never use `.username` for display.
**Expired invitations:** Customer accounts with `password_set=False` and `set_password_token_expires < now_eastern()` are flagged on the `/customers` index with an inline Resend button.
### Facility / Area
```
facilities: id, name, address, contact_person, contact_phone, active, project_id (FK)
areas: id, facility_id (FK), name, area_type
```
**`area_type` choices:** `restroom`, `lobby`, `hallway`, `office`, `kitchen`, `storage`, `floor`, `outdoor`, `other`
### Project / CustomerAssignment
```
projects: id, name, description, project_manager_id, active, created_at
customer_assignments: id, user_id, project_id, facility_id (nullable)
UniqueConstraint(user_id, project_id, facility_id)
```
**UI terminology:** "Project" is displayed as **"Contract"** throughout the UI. All backend identifiers (`project_id`, class `Project`, routes `projects.*`) are unchanged.
`facility_id = NULL` → access to all active facilities in that project.
### Inspection
```
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
```
**Indexed columns (Phase 12):** `status`, `facility_id`, `inspector_id`, `inspection_date`
**Score rule:** Items with `score = 0` mean "unanswered" — excluded from calculation entirely.
**Status display:** `completed` renders as **"Submitted"** in the UI. DB value unchanged.
**Stale badge:** In-progress inspections older than 24 hours show a `⚠ Stale` badge. Route passes `now=now_eastern()` to the list template.
### Issue
```
issues: id, inspection_id (nullable), area_id, severity (low/medium/high/critical),
description, photo_path, status (open/in_progress/resolved/pending_verification),
assigned_to, reported_at, resolved_at, result_notes, result_photos (JSON),
verified_by, verified_at, verification_note, sla_notified
```
**Indexed columns (Phase 12):** `status`, `severity`, `assigned_to`, `reported_at`
**Issue status flow:** `open``in_progress``pending_verification``resolved`
**Quick-assign:** `POST /issues/<id>/quick-assign` — AJAX endpoint for admin/director. Returns JSON. Sends assignment notification. Logs to audit trail.
### Notification / NotificationPreference
```
notifications: id, user_id, title, body, link, is_read, created_at, issue_id, event_type
notification_preferences: id, user_id, event_type, email_enabled, digest_mode, digest_frequency
```
**Pause all emails:** Preferences page has a "Pause All Emails" toggle that bulk-disables all email toggles client-side.
### NotificationMatrix
```
notification_matrix: id, event_type, role_key, enabled, custom_emails (JSON)
UniqueConstraint(event_type, role_key)
```
### AuditLog
```
audit_logs: id, user_id (nullable, SET NULL on delete), 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)
```
**Token cleanup:**
1. **Passive** — on every successful API login, expired/revoked tokens for that user are deleted
2. **Cron**`POST /notifications/cleanup-tokens?token=<DIGEST_SECRET>` deletes all globally
---
## 6. Role & Permission Matrix
| Area | admin | director | project_manager | inspector | customer |
|---|---|---|---|---|---|
| Dashboard | ✅ full | ✅ full | ✅ full | ✅ limited | ✅ scoped |
| Users | ✅ | ✅ | ❌ | ❌ | ❌ |
| Notification Matrix | ✅ only | ❌ | ❌ | ❌ | ❌ |
| Customers | ✅ | ✅ | ❌ | ❌ | ❌ |
| Facilities | ✅ | ✅ | ✅ | read | scoped |
| Contracts | ✅ | ✅ | ✅ | read | scoped |
| Templates | ✅ | ✅ | ❌ | ❌ | ❌ |
| Inspections (execute) | ✅ | ✅ | ✅ | ✅ | read |
| Issues (create/assign) | ✅ | ✅ | ✅ | ✅ | read |
| Issues (quick-assign) | ✅ | ✅ | ❌ | ❌ | ❌ |
| Issue verification | ✅ | ✅ | ❌ | ❌ | ❌ |
| Reports | ✅ | ✅ | ✅ | ✅ | scoped |
| Scheduled Reports | ✅ | ✅ | ✅ | ❌ | ❌ |
| Audit Trail | ✅ only | ❌ | ❌ | ❌ | ❌ |
| Mobile API | ✅ | ✅ | ✅ | ✅ | ❌ |
### Decorator Map
```python
@admin_required # role == 'admin' only
@supervisor_required # role in ('admin', 'director') — name kept to avoid touching 30+ routes
@project_manager_required # role in ('admin', 'director', 'project_manager')
@customer_required # role == 'customer' only
```
---
## 7. Blueprint Prefixes & Route Inventory
| Blueprint | Prefix | Notable routes |
|---|---|---|
| `auth` | `/auth` | `/login` (rate-limited 20/min), `/logout`, `/profile`, `/users/*`, `/notification-matrix` |
| `dashboard` | `/` | `GET /`, `/facility-trend` (AJAX) |
| `facilities` | `/facilities` | CRUD + area management |
| `projects` | `/projects` | CRUD + customer assignment management |
| `customers` | `/customers` | list (expired invitation banner), invite, set-password, manage, import CSV |
| `inspections` | `/inspections` | list (stale badge, `now` passed from route), start, execute, view, PDF export, flag-issue, save-draft (AJAX), flag-followup, reinspect |
| `templates` | `/templates` | list, create, edit, delete, form editor, preview |
| `issues` | `/issues` | list (SLA column, facility filter, quick-assign dropdown), view, create, update, verify, comment, follow/unfollow, verification queue, delete |
| `issues` | `/issues` | `POST /<id>/quick-assign` — JSON AJAX, admin/director only |
| `notifications` | `/notifications` | list, mark-read, mark-all-read, preferences (pause-all toggle), `send-digest` (cron), `check-sla` (cron), `cleanup-tokens` (cron) |
| `audit` | `/audit` | list (admin only), view, purge |
| `reports` | `/reports` | index, facility report, scorecard, CSV/PDF export |
| `scheduled_reports` | `/scheduled-reports` | CRUD + manual trigger |
| `api` | `/api/v1` | parent blueprint |
| `api_auth` | `/api/v1` | `/auth/login` (10/min), `/auth/refresh` (30/min), `/auth/logout`, `/auth/me`, `/devices/register` |
---
## 8. Utility Modules
### `time_utils.py`
`now_eastern()` — always use this, never `datetime.utcnow()`.
### `audit.py`
`log_action(action, entity_type, entity_id, entity_label, details)` — call **after** `db.session.commit()`.
### `scope.py`
`get_customer_scope(user)` — returns `list[int]` facility IDs for customers, `None` for staff.
### `forms.py`
All WTForms classes. `AreaForm.area_type` includes `floor`. `UserForm` excludes `customer` role.
### `notifications.py`
`notify()`, `notify_by_matrix()`, `notify_customers_for_facility()` — all email sent in background thread.
### `sla.py`
`sla_status(issue)``'ok'` | `'at_risk'` | `'breached'` | `None` (resolved).
`send_sla_alerts()` — called by cron, deduplicates via `issue.sla_notified`.
### `pdf_export.py`
ReportLab-based. 12-column grid must be preserved — never collapse in PDF views.
---
## 9. Mobile API (Phase 7)
### Auth Flow
1. `POST /api/v1/auth/login` → access token (60 min JWT) + refresh token (30 day opaque hex)
2. Bearer token on every request
3. `POST /api/v1/auth/refresh` → token rotation (old revoked, new issued)
4. `POST /api/v1/auth/logout` → revokes refresh token
### Rate Limits
| Endpoint | Limit |
|---|---|
| `POST /api/v1/auth/login` | 10/min, 3/sec |
| `POST /api/v1/auth/refresh` | 30/min, 5/sec |
| `POST /auth/login` (web) | 20/min, 5/sec |
### Refresh Token Cleanup
- Raw token never stored — SHA-256 hash only
- Passive: expired/revoked tokens purged per-user on each login
- Cron: `POST /notifications/cleanup-tokens`
### CSRF Exemption Pattern
```python
csrf.exempt(api_bp) # BEFORE register_api(app) — order matters
register_api(app)
```
### Phase 2 (planned)
`api_facilities`, `api_inspections`, `api_issues`, `api_notifications`, `api_photos` — all stubbed in `app/api/__init__.py`.
---
## 10. Notification System
### Event Constants (`app/models/notification.py`)
```
inspection_completed, issue_assigned, issue_reassigned, issue_unassigned,
issue_status, issue_comment, issue_follow_update, issue_flagged, issue_created,
issue_updated_customer, verification_requested, sla_alert,
customer_inspection_completed
```
### Cron Endpoints (all require `token=DIGEST_SECRET`)
| Endpoint | Purpose | Schedule |
|---|---|---|
| `POST /notifications/send-digest` | Digest email delivery | `0 7 * * *` |
| `POST /notifications/check-sla` | SLA breach/at-risk alerts | `*/30 * * * *` |
| `POST /notifications/cleanup-tokens` | Purge expired API tokens | `0 3 * * *` |
### Implicit Notifications (always sent, not matrix-controlled)
- Issue **assignee** on assignment/status/comment
- Issue **followers** on any update
- SLA **assignee + followers** on SLA events
---
## 11. SLA Engine
| Severity | Window | At-Risk |
|---|---|---|
| critical | 4h | 3h |
| high | 24h | 18h |
| medium | 72h | 54h |
| low | 168h | 126h |
SLA column shown on issues list. `issue.sla_notified` prevents duplicate cron notifications.
---
## 12. Audit Trail
- Admin-only at `/audit/` — director is excluded
- Actions: `CREATE`, `UPDATE`, `DELETE`, `LOGIN`, `LOGOUT`, `EXPORT`
- Immutable — never updated or deleted through the application (purge UI exists for old records)
- IP from `X-Forwarded-For` or `request.remote_addr`
---
## 13. PDF Export
ReportLab — `app/utils/pdf_export.py`. **12-column grid must be preserved** — do not collapse in print/PDF.
---
## 14. Scheduled Reports
Types: `summary`, `facility`, `issues`. Frequencies: `daily`, `weekly`, `monthly`.
Cron: `POST /scheduled-reports/run?secret=<DIGEST_SECRET>`
---
## 15. Rate Limiting
Initialised in `app/__init__.py` as a module-level extension:
```python
limiter = Limiter(
key_func = get_remote_address,
default_limits = [],
storage_uri = 'memory://', # ← swap for 'redis://...' in multi-worker
)
limiter.init_app(app)
```
Import in routes: `from app import limiter`, then `@limiter.limit('N per period')`.
> **Multi-worker caveat:** `memory://` is per-process. With multiple Gunicorn workers the effective limit is `N × workers`. Use Redis for shared-state enforcement.
---
## 16. 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
→ phase12_performance_indexes ← HEAD
```
### MySQL ENUM Change Protocol (3 steps — always follow)
```sql
-- 1. Expand to include both values
ALTER TABLE users MODIFY COLUMN role ENUM('admin','supervisor','director',...) NOT NULL;
-- 2. Migrate data
UPDATE users SET role = 'director' WHERE role = 'supervisor';
-- 3. Contract to remove old value
ALTER TABLE users MODIFY COLUMN role ENUM('admin','director',...) NOT NULL;
```
### MySQL Compatibility
`CREATE INDEX IF NOT EXISTS` requires MySQL ≥ 8.0.1. For 5.7 compatibility use `information_schema.statistics` existence checks — see `phase12_performance_indexes.py` for the reusable `_index_exists()` pattern.
### Deprecated SQLAlchemy Patterns
```python
# WRONG
Model.query.get(id)
Model.query.get_or_404(id)
# CORRECT
obj = db.session.get(Model, id)
if obj is None: abort(404)
```
---
## 17. Frontend Conventions
### Active Nav Tab
```css
/* base.html <style> block */
.navbar-dark .navbar-nav .nav-link.active {
background-color: rgba(255,255,255,0.18);
color: #fff !important;
border-radius: 6px;
font-weight: 600;
box-shadow: inset 0 -2px 0 rgba(255,255,255,0.6);
}
```
Active state detected via `request.endpoint.startswith('<blueprint>.')` in each nav `<a>` tag.
### Display Names
Always use `user.display_name` in templates — never `.username` for display purposes.
### Status Label Map
| DB value | Displayed as |
|---|---|
| `completed` | **Submitted** |
| `in_progress` | In Progress |
| `flagged` | Flagged |
| `open` | Open |
| `resolved` | Resolved |
| `pending_verification` | Pending Verification |
### Forms
- Flask-WTF CSRF auto-applied to all web forms
- Manual forms: `<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">`
- **Never nest `<form>` tags** — browsers silently discard inner forms
### Real-Time
**SSE banned.** All "live" updates use polling.
---
## 18. Infrastructure
### Gunicorn
```python
bind = "127.0.0.1:8000"
workers = multiprocessing.cpu_count() * 2 + 1
worker_class = "sync"
timeout = 30
```
Logs: `/home/jqc/logs/gunicorn-error.log`, `/home/jqc/logs/gunicorn-access.log`
### Application Logging
- `RotatingFileHandler``logs/jqc.log` (5 × 5 MB)
- `StreamHandler` → stdout (journalctl)
- Format: `[YYYY-MM-DD HH:MM:SS] LEVEL in module: message`
### Nginx
- `client_max_body_size 50M`
- Passes `X-Forwarded-For`
### Recommended Cron Schedule
```bash
0 7 * * * curl -s -X POST https://your-domain.com/notifications/send-digest \
-d "token=SECRET&frequency=daily"
*/30 * * * * curl -s -X POST https://your-domain.com/notifications/check-sla \
-d "token=SECRET"
0 3 * * * curl -s -X POST https://your-domain.com/notifications/cleanup-tokens \
-d "token=SECRET"
0 8 * * * curl -s -X POST https://your-domain.com/scheduled-reports/run \
-d "secret=SECRET"
```
---
## 19. Known Constraints & Hard Rules
| # | Rule | Rationale |
|---|---|---|
| 1 | **No SSE** | Exhausted Gunicorn sync worker pool |
| 2 | **`now_eastern()` always** | `utcnow()` caused incorrect SLA cutoffs |
| 3 | **3-step MySQL ENUM changes** | Skipping causes data loss |
| 4 | **Port 465 → SSL; 587 → STARTTLS** | Both True breaks Flask-Mail |
| 5 | **`CSRFProtect` as extension instance** | Flask-WTF 1.0+ removed per-route `@csrf_exempt` |
| 6 | **`supervisor_required` name preserved** | Renaming would touch 30+ route decorators |
| 7 | **Score 0 = unanswered** | Excluded from calculation — not the same as scoring zero |
| 8 | **12-column grid in PDF** | Must not collapse in print/PDF |
| 9 | **No nested `<form>` tags** | Browsers silently discard inner forms |
| 10 | **`log_action()` after `db.session.commit()`** | Entity ID must exist 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 loops cause N+1 |
| 14 | **Email in background thread** | Never block HTTP response |
| 15 | **Open-redirect guards** | `_safe_next()` in auth.py; `_safe_referrer()` in customers.py |
| 16 | **`CREATE INDEX IF NOT EXISTS` not on MySQL < 8.0.1** | Use `information_schema` — see phase12 |
| 17 | **Flask-Limiter `memory://` is per-process** | Multi-worker needs Redis for shared counters |
| 18 | **"Project" → "Contract" is UI-only** | Backend identifiers unchanged |
| 19 | **`display_name` not `username` in templates** | Respects full_name; username is login identity only |
| 20 | **`open_issues` count derived from `len()` not `.count()`** | Avoids a second DB round-trip on the dashboard |
---
## 20. Change Philosophy
1. **Surgical, additive patches** — smallest possible change to achieve the goal
2. **Preserve all routes, function names, variable names** unless explicitly directed otherwise
3. **Never remove existing functionality** unless explicitly directed
4. **Log all create/update/delete actions** via `log_action()`
5. **Migration existence checks** — all migrations safe to re-run
6. **Full file contents for 13 file changes**; deployment map for larger changesets
7. **Explicit deploy instructions** — migration steps separated from code steps
8. **Root cause analysis** on errors — never apply temporary workarounds