From a8ef7bb34196beec00ae9ec0a3d6a68909cb6c4d Mon Sep 17 00:00:00 2001 From: Konrad du Plessis Date: Wed, 22 Apr 2026 04:42:32 +0200 Subject: [PATCH] Update CLAUDE.md with cache-busting, email fallback, and deploy context MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- CLAUDE.md | 114 ++++++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 110 insertions(+), 4 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 9b5f2cd..5a503ce 100644 --- a/CLAUDE.md +++ b/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 + +``` + +`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 "), +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/-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 ` (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 ` + service-restart to roll back in ~60 seconds: + +``` +git branch pre--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--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`