QA Report — PR #1431 (Tailwind build-step + favicon.ico)
- Date: 2026-05-09 (UTC)
- Mode: thorough
- PR: #1431 —
fix(console): replace Tailwind CDN with build-step CSS, add favicon.ico - Branch:
worktree-agent-aa8ed6eaf973c90d5->main - Closes: #1426 (Tailwind CDN warning), #1427 (favicon 404)
- Verdict:
qa:partial-pending-deploy— code/build verifications pass; live console.raxx.app probes need post-merge re-run (CF Access gates the surface; no preview deploy for console)
Summary
PR replaces the dev cdn.tailwindcss.com script tag with a build-time-compiled tailwind.css shipped from the slug, and adds a /favicon.ico Flask redirect plus a generated multi-size .ico asset. Build pipeline now runs Tailwind CLI before the git-subtree split. All artifacts ship via the standard Heroku slug — no new infra or secrets.
Pre-merge verification on the PR branch passes every check available without a deployed instance: - Tailwind build is deterministic (local rebuild matches manifest SHA exactly) - Asset manifest entries match committed file SHAs byte-for-byte - Flask test_client routes return correct status codes (301/200/200) - Existing console test suite (62 tests) passes with no regressions - All 12 CI checks on the PR are SUCCESS or SKIPPED (none failing) - Layer A asset-manifest checksum guard passed (validates committed entries match disk)
The two ACs that strictly require a deployed instance — the absence of the CDN warning in a real browser console and the live 200 on console.raxx.app/favicon.ico — must be re-checked post-merge once the operator dispatches the manual deploy-console.yml workflow per ADR-0020.
Per-AC results
Issue #1426 — replace Tailwind CDN (4 ACs)
AC1: Replace CDN script tag with compiled output.css shipped from build pipeline — PASS
Evidence:
- console/app/templates/base.html line 24 (PR diff): <link rel="stylesheet" href="{{ url_for('static', filename='tailwind.css') }}?v=2">
- CDN <script> tag removed (was line 19 in the prior version)
- Compiled file shipped at console/app/static/tailwind.css (49,802 bytes, SHA dadbe0cf...)
- Repo-wide grep for cdn.tailwindcss in deployable code: only match is the comment in base.html warning future maintainers not to restore it. (Other matches are docs/design/*/mockups/*.html design artifacts, not deployed.)
AC2: Either Tailwind CLI build step in CI, or PostCSS plugin — PASS
Evidence:
- .github/workflows/deploy-console.yml lines 310-340 (PR diff) adds two new steps before the subtree-split commit:
- Setup Node (Tailwind build) — actions/setup-node@v4, Node 20, npm cache keyed on console/package-lock.json
- Build Tailwind CSS — runs npm ci && npm run build:css and aborts deploy if app/static/tailwind.css is missing or empty
- The post-build Stage deploy-time generated files for subtree split step now also git adds tailwind.css so the subtree split bundles the freshly-compiled file even if the committed copy drifted
- Local build reproduction confirms determinism:
cd console && npm ci && npm run build:css
-> Done in 385ms.
-> 49802 bytes
-> SHA256: dadbe0cffdaad65ebde136b4e1c0eab876ec6127b3bd44b3f610f0e8b133ff1b
Matches docs/asset-manifest.json exactly.
AC3: Warning gone from console on production pages — PARTIAL (pending deploy)
Evidence (pre-merge, code-level):
- console/app/templates/base.html no longer emits <script src="https://cdn.tailwindcss.com">. With the CDN script gone, the runtime warning cannot fire.
- Local Flask test_client probe of /static/tailwind.css returns HTTP 200, 49802 bytes — the replacement asset serves correctly.
Evidence (post-deploy, blocked): console.raxx.app is gated by Cloudflare Access (302 → CF login). Operator-assisted browser auth was offered for this run, but the PR is not yet merged and the console has no preview surface (Antlers preview build doesn't cover console — confirmed via PR Preview / Antlers check SKIPPED). Manual deploy + browser console re-check required post-merge.
→ Labeled qa:partial-pending-deploy for this AC.
AC4: No visible style regression (audit dashboard, secrets, flags, billing) — PARTIAL (pending deploy)
Evidence (pre-merge):
- Tailwind config (console/tailwind.config.js) scans ./app/templates/**/*.html and ./app/static/js/**/*.js for class names, so JIT emits all classes used in current templates
- 49 KB minified output is consistent with a production-pruned bundle
- Test_client probes for /static/tailwind.css return 200 and the file content begins with the standard Tailwind variable preamble (*,:after,:before{--tw-border-spacing-x:0;...) — confirming the file is a real compiled bundle
Evidence (post-deploy, blocked): Visual regression audit on dashboard/secrets/flags/billing pages requires the deployed surface and cannot be done from build artifacts alone. Same qa:partial-pending-deploy reason as AC3.
→ Labeled qa:partial-pending-deploy for this AC.
Issue #1427 — favicon.ico 404 (3 ACs)
AC1: favicon.ico (or proper favicon family) deployed as static asset, served at /favicon.ico — PASS
Evidence:
- New file console/app/static/favicon.ico (2,301 bytes) — verified ICO format (magic 00 00 01 00) with two embedded PNG entries (16x16 + 32x32), generated from favicon-32.png per the PR body
- New route in console/app/blueprints/root.py:
python
@bp.route("/favicon.ico")
def favicon_ico():
return redirect(url_for("static", filename="favicon.ico"), code=301)
- Local Flask test_client probe:
GET /favicon.ico -> 301 Location: /static/favicon.ico
GET /static/favicon.ico -> 200 Content-Length: 2301 Content-Type: image/x-icon
AC2: No 404 in console / Sentry for favicon — PARTIAL (pending deploy)
Evidence (pre-merge): Local route returns 301 + 200 chain; cannot 404. Slug-bundling verified by git ls-tree -r pr-1431 -- console/app/static/favicon.ico (file present, blob 75617e08...).
Evidence (post-deploy, blocked): Live probe of console.raxx.app/favicon.ico requires CF Access auth + post-merge deploy. Pre-merge prod still 404s (current state matches the issue).
→ Labeled qa:partial-pending-deploy for this AC.
AC3: in base.html references the asset — PASS
Evidence: console/app/templates/base.html line 13 (PR diff):
<link rel="icon" href="{{ url_for('static', filename='favicon.ico') }}?v=2">
Inserted as the first <link rel="icon"> in <head>, ordered before the existing svg/png/apple-touch entries so older browsers prefer it.
Asset-manifest delta (Layer A guard, ADR-0051 / #1359)
generated_at: 2026-05-07T11:47:47Z -> 2026-05-09T04:32:42Z
total assets: 19 -> 21
+favicon.ico sha256=21a7170f39563ca180ded4b4520b259c770f3a91cf98ea41a63734041928d7ef
+tailwind.css sha256=dadbe0cffdaad65ebde136b4e1c0eab876ec6127b3bd44b3f610f0e8b133ff1b
Verified locally — shasum -a 256 of the committed blobs matches both manifest entries exactly. CI's Asset-manifest checksum guard (Layer A) job passed.
CI checks
All 12 PR check runs reported via gh pr view --json statusCheckRollup:
| Check | Conclusion |
|---|---|
| Detect changed paths | SUCCESS |
| Detect changed surfaces | SUCCESS |
| Antlers (raxx-app) | SKIPPED (no Antlers diff) |
| PR base-branch cleanliness (hard gate) | SUCCESS |
| Mockups (raxx-mockups) | SKIPPED (no mockups diff) |
| Stale-branch guard (hard gate) | SUCCESS |
| Commit message lint (advisory) | SUCCESS |
| Sticky PR comment | SUCCESS |
| Sprint readiness gate | SUCCESS |
| Flag-promotion enforcement (B1) | SKIPPED (no flag YAML diff) |
| Asset-manifest checksum guard (Layer A) | SUCCESS |
| PR report | SUCCESS |
No FAILURE conclusions. No CANCELLED runs that would indicate hidden duplicates.
Test coverage
- Console unit/integration tests:
python3 -m pytest console/tests/test_auth.py console/tests/test_api_status.py -q-> 62 passed in 2.06s on the PR branch - Build reproducibility:
cd console && npm ci && npm run build:css-> 49,802 bytes, SHA matches manifest - Flask routes via test_client: 301/200/200 chain confirmed for favicon + tailwind
Gap (non-blocking): No new test added for the /favicon.ico redirect route or for the presence of the Tailwind link tag in base.html. Recommend a follow-up to add a small test_root_favicon.py so the route doesn't silently regress. Filed as advisory note in the PR comment, not a blocker.
Findings (minor, non-blocking)
Minor doc inaccuracy in console/app/blueprints/root.py docstring. The new docstring at line 8 claims:
Cloudflare Access exempts /static/* and /favicon.ico via the cloudflare_origin_guard.py bypass list.
But console/app/middleware/cloudflare_origin_guard.py line 47 declares:
_ALLOWLISTED_PATHS = frozenset([
"/health",
"/",
"/poller-status",
])
Neither /static/* nor /favicon.ico is in the bypass list. In practice this doesn't break anything because:
1. The CF origin guard is gated on FLAG_ENFORCE_CF_ORIGIN (default off)
2. CF-proxied requests inject CF-Connecting-IP regardless of path, so any genuine request reaching origin will pass
But the comment is misleading future readers. Not a blocker — recommend a one-line fixup (either correct the comment or add the paths to the allowlist) in a follow-up. Noted in PR comment.
Recommended next step
- Merge PR #1431.
- Operator dispatches
deploy-console.ymlworkflow_dispatch->production(per ADR-0020). - Re-run QA on prod (operator-assisted CF Access auth via Playwright):
- Load
https://console.raxx.app/dashboard- Open browser devtools console - Confirm absence ofcdn.tailwindcss.com should not be used in productionwarning - Probehttps://console.raxx.app/favicon.icodirectly -> expect 301 -> 200 - Visual smoke: dashboard, secrets, flags, billing pages render with no missing styles - Re-label PR (or open a follow-up note) with
qa:passedonce the post-deploy checks come back clean.
Issues filed
None — all ACs either PASS or are PARTIAL-pending-deploy. No failing AC.