Update CLAUDE.md with cache-busting, email fallback, and deploy context
Documents three things that came out of today's Phase 2 deploy session and weren't previously written down: 1. Static Assets & Cache-Busting (new section): explains that production traffic goes through Cloudflare with 4h edge cache; the `deployment_timestamp` template variable is what breaks stale caches; and why `request.timestamp` must never be used (the silent-default-to-1.0 bug that ate a couple of hours). 2. Environment Variables: inline notes for each var. Most important new fact is that DEFAULT_FROM_EMAIL is now optional — falls back to EMAIL_HOST_USER if unset (prevents the "Invalid address ''" failure mode on outbound mail). Also documents that .env lives at BASE_DIR.parent on Flatlogic and can only be edited via Gemini/shell. 3. Flatlogic Deployment: collectstatic isn't auto-run, django-dev.service runs manage.py runserver (dev server in prod — known but works at this scale), Cloudflare sits in front, VM has two git remotes (github + gitea) that must stay in sync, VM-local safety branches for rollback, and the "pick one write path" workflow rule to avoid divergence. No code changes — documentation only. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
5d6446ae75
commit
a8ef7bb341
114
CLAUDE.md
114
CLAUDE.md
@ -194,6 +194,52 @@ python manage.py check # System check
|
||||
pair of body vars for dark mode).
|
||||
- **Email/PDF payslips**: Worker name dominant — do NOT add prominent FoxFitt branding
|
||||
|
||||
## Static Assets & Cache-Busting (Cloudflare is in front)
|
||||
|
||||
Production traffic reaches the Flatlogic VM through **Cloudflare** (response headers
|
||||
include `cf-ray`, `cf-cache-status`, and a `cache-control: max-age=14400`). Static
|
||||
assets — including `custom.css` — are cached at Cloudflare's edge for up to 4 hours
|
||||
per unique URL. This is great for performance and bad for deploys if the URL doesn't
|
||||
change when the file does.
|
||||
|
||||
### How cache-busting works now
|
||||
`base.html` loads CSS as:
|
||||
|
||||
```html
|
||||
<link rel="stylesheet" href="{% static 'css/custom.css' %}?v={{ deployment_timestamp|default:'1' }}">
|
||||
```
|
||||
|
||||
`deployment_timestamp` comes from `core/context_processors.py::project_context` as
|
||||
`int(time.time())` — meaning every Django request generates a new query-string value.
|
||||
Cloudflare treats each new `?v=...` value as a new URL → `cf-cache-status: MISS` →
|
||||
fresh fetch from the VM. Users always see the latest CSS as soon as the Django
|
||||
process restarts.
|
||||
|
||||
**Trade-off**: because the timestamp changes every second, CDN cache-hit rate on
|
||||
CSS is effectively zero. For a low-traffic app this is fine. If traffic grows,
|
||||
consider switching to a file-mtime-based token so the URL only changes when the
|
||||
CSS actually changes.
|
||||
|
||||
### The pitfall this replaced
|
||||
Pre-Apr 2026, the template used `{{ request.timestamp|default:'1.0' }}`. But
|
||||
`request.timestamp` is **not** a Django request attribute — the variable always
|
||||
fell back to the literal `'1.0'`. Every deploy's CSS URL resolved to the same
|
||||
`custom.css?v=1.0`, so Cloudflare held onto a pre-redesign copy for hours while
|
||||
the VM served the new one. Symptom was "the deploy worked but the page looks wrong"
|
||||
that only a hard refresh in incognito temporarily fixed. Never use `request.timestamp`
|
||||
in templates — it doesn't exist.
|
||||
|
||||
### When CSS changes don't appear on production
|
||||
1. Confirm Django is rendering the new URL: `curl -s https://foxlog.flatlogic.app/ | grep -oE 'custom\.css\?v=[^"]+'` — the `v=` number should change per request (or at least per restart)
|
||||
2. Confirm the CDN honours it: `curl -sI "https://foxlog.flatlogic.app/static/css/custom.css?cb=test$(date +%s)" | grep -i cache` — expect `cf-cache-status: MISS` then `HIT` on repeat
|
||||
3. If the Django URL still looks like `?v=1.0` (constant), `deployment_timestamp` isn't being injected — check that `core.context_processors.project_context` is listed in `TEMPLATES[0]['OPTIONS']['context_processors']` in `config/settings.py`
|
||||
|
||||
### `collectstatic` is required after CSS/JS changes on production
|
||||
Flatlogic's rebuild does NOT automatically run `collectstatic`. If new CSS is on
|
||||
disk but the VM's `staticfiles/` hasn't been refreshed, Apache serves the old
|
||||
collected copy. Have Gemini run `python3 manage.py collectstatic --noinput`
|
||||
after any PR that touches `static/`.
|
||||
|
||||
## PDF Generation (WeasyPrint)
|
||||
Migrated from xhtml2pdf in Nov 2026. WeasyPrint is a browser-grade HTML→PDF renderer — it supports real CSS (flexbox, grid, `@font-face`, shadows, `border-radius`, proper cascade) that xhtml2pdf could not handle.
|
||||
|
||||
@ -503,23 +549,83 @@ python manage.py restore_data backup.json
|
||||
|
||||
## Environment Variables
|
||||
```
|
||||
DJANGO_SECRET_KEY, DJANGO_DEBUG, HOST_FQDN, CSRF_TRUSTED_ORIGIN
|
||||
DJANGO_SECRET_KEY # required in prod — startup fails without it
|
||||
DJANGO_DEBUG # "true"/"false"; defaults to false; keep false in prod
|
||||
HOST_FQDN, CSRF_TRUSTED_ORIGIN # trusted hostnames (scheme-less ok, auto-prefixed https://)
|
||||
DB_NAME, DB_USER, DB_PASS, DB_HOST (default: 127.0.0.1), DB_PORT (default: 3306)
|
||||
USE_SQLITE # "true" → use SQLite instead of MySQL
|
||||
EMAIL_HOST_USER, EMAIL_HOST_PASSWORD (Gmail App Password — 16 chars)
|
||||
DEFAULT_FROM_EMAIL, SPARK_RECEIPT_EMAIL
|
||||
USE_SQLITE # "true" → use SQLite instead of MySQL (local dev only)
|
||||
EMAIL_HOST_USER # Gmail address — required for any outbound email
|
||||
EMAIL_HOST_PASSWORD # Gmail App Password (16 chars, no spaces/non-breaking-space)
|
||||
DEFAULT_FROM_EMAIL # Optional — falls back to EMAIL_HOST_USER if unset
|
||||
SPARK_RECEIPT_EMAIL # Optional — defaults to FoxFitt's Spark Receipt address
|
||||
PROJECT_DESCRIPTION, PROJECT_IMAGE_URL # Flatlogic branding
|
||||
```
|
||||
|
||||
### Email fallback behaviour
|
||||
`DEFAULT_FROM_EMAIL` is not strictly required — `config/settings.py` sets it as:
|
||||
|
||||
```python
|
||||
DEFAULT_FROM_EMAIL = os.getenv("DEFAULT_FROM_EMAIL", "") or EMAIL_HOST_USER
|
||||
```
|
||||
|
||||
…so if the env var is unset or empty, the "From" address on every outbound email
|
||||
falls back to the authenticated Gmail address (which is always valid since we
|
||||
send AS that account). Without this fallback, receipt and payslip emails fail
|
||||
with `Invalid address ""`. If you want to send FROM a different display address
|
||||
than the authenticated one (e.g. "FoxFitt Payroll <payroll@foxfitt.co.za>"),
|
||||
set `DEFAULT_FROM_EMAIL` explicitly — but Gmail will likely rewrite it to the
|
||||
authenticated user anyway unless you've configured a "Send mail as" alias.
|
||||
|
||||
### Where env vars live on Flatlogic
|
||||
Flatlogic's platform has no env-var UI. Values are set in a `.env` file at
|
||||
`BASE_DIR.parent / ".env"` on the VM (one level up from the repo). Edit via
|
||||
Gemini/shell — the user cannot modify via Flatlogic's web editor because
|
||||
`.env` is outside the project tree. The file is loaded by
|
||||
`python-dotenv` in `config/settings.py` before any `os.getenv()` calls.
|
||||
|
||||
## Flatlogic/AppWizzy Deployment
|
||||
- **Branches**: `ai-dev` = development (Flatlogic AI + Claude Code). `master` = deploy target.
|
||||
- **Workflow**: Push to `ai-dev` → Flatlogic auto-detects → "Pull Latest" → app rebuilds (~5 min)
|
||||
- **Deploy from Git** (Settings): Full rebuild from `master` — use for production
|
||||
- **Migrations**: Sometimes run automatically during rebuild, but NOT always reliable. If you get "Unknown column" errors after pulling latest, visit `/run-migrate/` in the browser to apply pending migrations manually. This endpoint runs `python manage.py migrate` on the production MySQL database.
|
||||
- **Static files**: Flatlogic's rebuild does NOT auto-run `collectstatic`. After CSS/JS changes have Gemini run `python3 manage.py collectstatic --noinput` + restart the service, otherwise Apache keeps serving the previously-collected copy.
|
||||
- **Service**: The Django app runs as `django-dev.service` (systemd). Gemini restarts it via `sudo systemctl restart django-dev.service`. It runs `python manage.py runserver 0.0.0.0:8000` — a **development server**, not gunicorn/uwsgi (Flatlogic default, works fine at this scale).
|
||||
- **CDN**: All production traffic goes through Cloudflare. Response headers show `cf-ray`/`cf-cache-status`. Static assets are cached at the edge for 4h — see "Static Assets & Cache-Busting" section for how the `deployment_timestamp` token breaks stale caches.
|
||||
- **Never edit `ai-dev` directly on GitHub** — Flatlogic pushes overwrite it
|
||||
- **Gemini gotcha**: Flatlogic's Gemini AI reads `__pycache__/*.pyc` and gets confused. Tell it: "Do NOT read .pyc files. Only work with .py source files."
|
||||
- **Sequential workflow**: Don't edit in Flatlogic and Claude Code at the same time
|
||||
|
||||
### Git remotes on the VM
|
||||
The Flatlogic VM has TWO git remotes, both kept in sync:
|
||||
- `github` → `https://github.com/Konradzar/LabourPay_v5.git` (our canonical source)
|
||||
- `gitea` → `https://gitea.flatlogic.app/admin/<id>-vm.git` (Flatlogic's internal mirror — the one the platform UI watches)
|
||||
|
||||
Any push the VM makes must go to BOTH: `git push github ai-dev && git push gitea ai-dev`.
|
||||
If the two diverge, Flatlogic's dashboard can show a different commit than GitHub,
|
||||
which silently confuses deploys. Flatlogic's UI occasionally commits as
|
||||
`Flatlogic Bot <support@flatlogic.com>` (autosaves from the in-browser file editor) —
|
||||
those commits land on gitea but don't propagate to GitHub unless someone pushes.
|
||||
|
||||
### VM-local safety branches
|
||||
When doing risky deploys (model migrations, branch resets, history rewrites), we
|
||||
create a safety branch on the VM at the pre-deploy HEAD so Gemini can
|
||||
`git reset --hard <safety-branch>` + service-restart to roll back in ~60 seconds:
|
||||
|
||||
```
|
||||
git branch pre-<purpose>-YYYYMMDD HEAD
|
||||
git branch --list "pre-*"
|
||||
```
|
||||
|
||||
Safety branches are VM-local — not pushed to GitHub by default. They're
|
||||
single-use rollback anchors; delete after 7 days of confirmed stability via
|
||||
`git branch -D pre-<purpose>-YYYYMMDD`.
|
||||
|
||||
### Workflow options going forward
|
||||
Either works — pick one and stick to it per change to avoid divergence:
|
||||
1. **Claude → GitHub → Flatlogic pulls**: Claude pushes to origin/ai-dev; you click "Pull Latest" in the Flatlogic UI (or ask Gemini to `git pull + push gitea + restart`).
|
||||
2. **Flatlogic UI → GitHub**: edit in Flatlogic's file editor; click "Push to GitHub" in their UI; Claude pulls locally with `git pull origin ai-dev`.
|
||||
**Don't mix** paths in the same change — that's how divergence (and the "Ver XX.YY screeeewup" commits) happen.
|
||||
|
||||
## Security Notes
|
||||
- Production: `SESSION_COOKIE_SECURE=True`, `CSRF_COOKIE_SECURE=True`, `SameSite=None` (cross-origin for Flatlogic iframe)
|
||||
- Local dev: Secure cookies disabled when `USE_SQLITE=true`
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user