## BUG-112: No global Express error handler + unprotected async routes in recover & email-change
- **Date:** 2026-03-17
- **Severity:** MEDIUM
- **Issue:** The Express app has no global error-handling middleware (`(err, req, res, next)` 4-arg handler). Additionally, `src/routes/recover.ts` and `src/routes/email-change.ts` have async handlers with multiple `await` calls but zero try/catch blocks. If `queryWithRetry`, `verifyCode`, or `createPendingVerification` throws unexpectedly, the error propagates unhandled — Express may send its default HTML error page (leaking stack traces) or the request hangs.
- **Impact:** In DB failure scenarios, users see ugly error pages instead of clean JSON errors. Potential stack trace leakage.
- **Fix:** Add global Express error-handling middleware + wrap recover and email-change handlers in try/catch.
- **Status:** ✅ FIXED — pushed to main (a3bba8f), 788 tests passing, staging deployment pending CI

## BUG-111: CORS blocks all frontend modals on staging — hardcoded production origin
- **Date:** 2026-03-09
- **Severity:** MEDIUM
- **Issue:** CORS middleware in `src/index.ts` hardcodes `Access-Control-Allow-Origin: https://docfast.dev` for auth/billing routes (recover, email-change, billing, demo). When accessed from staging (`https://staging.docfast.dev`), browser blocks all XHR/fetch requests due to origin mismatch. All frontend modals on staging are broken.
- **Impact:** Cannot test any frontend modal flows on staging (recover, email-change, demo, billing checkout). Staging is supposed to be a full test environment.
- **Fix:** Make CORS origin dynamic — allow `https://staging.docfast.dev` when request comes from staging origin.
- **Status:** ✅ FIXED — commit da049b7. Dynamic CORS origin with allowlist. 13 TDD tests added. 579 tests total.

## BUG-110: POST /v1/recover initial request still uses in-memory cache only — recovery email not sent across pods
- **Date:** 2026-03-07
- **Severity:** MEDIUM
- **Issue:** `POST /v1/recover` uses `getAllKeys()` to find a user by email. BUG-107 added DB fallback to `/v1/recover/verify` but NOT to the initial `/v1/recover` route. If the key exists in DB but not in this pod's cache, the route returns "recovery_sent" but never actually sends the verification email (silently skips).
- **Impact:** Users may be unable to recover their API key if they hit a pod that doesn't have the key cached. Silent failure — same "recovery_sent" response regardless.
- **Fix:** Add DB fallback in `POST /v1/recover` — if `getAllKeys().find()` returns nothing, query DB by email before deciding to skip sending.
- **Status:** ✅ FIXED — commit d376d58. DB fallback sends recovery email when cache miss. 2 TDD tests added. 547 tests total.

## BUG-109: updateKeyEmail only checks in-memory cache — email change may silently fail across pods
- **Date:** 2026-03-07
- **Severity:** MEDIUM
- **Issue:** `updateKeyEmail()` in `src/services/keys.ts` only checks `keysCache` (in-memory). In 2-replica production, if the email-change verify route on one pod updates the DB directly but the billing webhook `customer.updated` calls `updateKeyEmail` on another pod where the key isn't cached, the function returns `false` without checking DB.
- **Impact:** Unlikely to trigger (email-change uses direct DB, not this function), but `updateKeyEmail` is exported and could be used elsewhere. Code quality issue — same pattern as BUG-106.
- **Fix:** Add DB fallback: query `api_keys` table by key when not found in cache.
- **Status:** ✅ FIXED — commit d376d58. DB fallback + cache hydration. 2 TDD tests added. 547 tests total.

## BUG-108: updateEmailByCustomer only checks in-memory cache — Stripe email sync silently fails across pods
- **Date:** 2026-03-07
- **Severity:** MEDIUM
- **Issue:** `updateEmailByCustomer()` in `src/services/keys.ts` only checks `keysCache` (in-memory). In 2-replica production, if a Stripe `customer.updated` webhook hits a pod that doesn't have the key cached (pod restart, key created on other pod), the function returns `false` without checking DB. Customer's email change from Stripe is silently lost.
- **Impact:** Email sync from Stripe billing silently fails. Customer's email in DocFast DB diverges from Stripe. Recovery emails go to wrong address.
- **Fix:** Add DB fallback: query `api_keys` table by `stripe_customer_id` when not found in cache, then update email in DB and hydrate local cache.
- **Status:** ✅ FIXED — commit d376d58. DB fallback + cache hydration. 2 TDD tests added. 547 tests total.

## BUG-107: Recover route uses in-memory cache only — recovery fails silently across pods
- **Date:** 2026-03-06
- **Severity:** MEDIUM
- **Issue:** `POST /v1/recover` and `/v1/recover/verify` use `getAllKeys()` (in-memory cache) to find a user's key by email. In a 2-replica setup, if the key was created or email changed on another pod, the cache is stale. Recovery silently returns "recovery_sent" without actually sending an email (because it doesn't find the key), or verify returns "No API key found" despite the key existing in DB.
- **Impact:** Users may be unable to recover their API key if they hit the "wrong" pod. Silent failure — no error shown.
- **Fix:** Fall back to DB query when in-memory lookup fails.
- **Status:** ✅ FIXED — commit b964b98. DB fallback in recover/verify endpoint. 2 TDD tests added (recover-db-fallback.test.ts). 520 tests total.

## BUG-106: downgradeByCustomer only checks in-memory cache — cancellations can silently fail
- **Date:** 2026-03-06
- **Severity:** HIGH
- **Issue:** `downgradeByCustomer()` in `src/services/keys.ts` only checks `keysCache` (in-memory). In 2-replica production, if a Stripe cancellation webhook hits a pod that doesn't have the key cached (pod restart, key created on other pod), the function returns `false` without checking DB. Customer keeps Pro access despite canceling their subscription.
- **Impact:** Revenue leakage — canceled customers retain Pro tier indefinitely. Silent failure with no error log.
- **Fix:** Add DB fallback: query `api_keys` table by `stripe_customer_id` when not found in cache, then update tier in DB and hydrate local cache.
- **Status:** ✅ FIXED — commit b964b98. DB fallback + cache hydration. 2 TDD tests added (keys-downgrade.test.ts). 520 tests total.

## BUG-105: Go and PHP examples show non-existent SDK code
- **Date:** 2026-03-05
- **Severity:** MEDIUM
- **Issue:** Go example used `github.com/docfast/docfast-go` (doesn't exist), PHP example used `DocFast\Client` (doesn't exist). Both said "SDK coming soon" but showed SDK code. Developers copying these would get import errors.
- **Fix:** Replaced with plain HTTP examples (Go: `net/http`, PHP: `file_get_contents`). Removed fake Laravel facade. 5 regression tests added.
- **Status:** ✅ FIXED — commit 4f6659c. 484 tests passing, 32 test files.

## BUG-104: Terms of Service still references discontinued "Free Tier"
- **Date:** 2026-03-05
- **Severity:** MEDIUM
- **Issue:** `public/src/terms.html` section 2.1 describes a "Free Tier" with "100 PDF conversions" and "10 requests per minute." Free accounts were discontinued — only a Demo endpoint exists (5 req/hour, no account needed). Section 5.1 also says "no SLA for free tier."
- **Impact:** Legal page contradicts actual product offering. Could confuse users or create contractual ambiguity.
- **Fix:** Replace Free Tier section with Demo tier description; update SLA reference.
- **Status:** ✅ FIXED — commit 503e651. Section 2.1 now describes Demo (Free), section 5.1 updated. 3 regression tests added. 487 tests total, all passing.

## BUG-103: Template render route bypasses PDF option validation entirely
- **Date:** 2026-03-05
- **Severity:** MEDIUM
- **Issue:** `/v1/templates/:id/render` accepts `_format` and `_margin` from user input and passes them directly to `renderPdf()` without going through `validatePdfOptions()`. All convert routes and demo routes were fixed in BUG-102 to use validated/sanitized options, but the template route was missed. Invalid format values (e.g., `"invalid"`) go straight to Puppeteer.
- **Impact:** Users could pass invalid page formats or margin values to template rendering. Puppeteer may reject or silently handle them. Inconsistent validation across API surface.
- **Fix:** Import and use `validatePdfOptions()` in template render route, same pattern as convert/demo routes.
- **Status:** ✅ FIXED — commit 47571c8. Added `validatePdfOptions()` to template render route. 6 TDD tests added (templates-render-validation.test.ts). 479 tests total, all passing. Pushed to main.

## BUG-102: Convert/demo routes ignore sanitized PDF options from validator
- **Date:** 2026-03-05
- **Severity:** MEDIUM
- **Issue:** `validatePdfOptions()` returns `{ valid: true, sanitized: {...} }` with normalized values (e.g., format `"a4"` → `"A4"`). But all convert routes (html, markdown, url) and demo routes pass raw `body.*` values to `renderPdf()` instead of `validation.sanitized.*`. Format normalization never takes effect.
- **Impact:** Users sending lowercase format like `"a4"` get it passed raw to Puppeteer. Puppeteer may reject or silently handle it, but the validator's normalization is wasted work.
- **Fix:** Use `validation.sanitized` for PDF options in all convert and demo routes.
- **Status:** ✅ FIXED — commit ba2e542. All 5 routes (3 convert + 2 demo) now use `validation.sanitized` from `validatePdfOptions()`. 5 TDD tests added (convert-sanitized.test.ts). 473 tests total, all passing. Pushed to main (staging auto-deploy pending CI runner).

## BUG-101: Body size limits on demo and convert routes are ineffective — global parser runs first
- **Date:** 2026-03-04
- **Severity:** MEDIUM
- **Issue:** In `src/index.ts`, the global `express.json({ limit: "2mb" })` middleware (line ~85) runs BEFORE route-specific parsers. Express only parses the body once — the first matching parser wins. So the route-specific limits never fire:
  - Demo route: `express.json({ limit: "50kb" })` — INEFFECTIVE, actually allows 2MB
  - Convert routes: `express.json({ limit: "500kb" })` — INEFFECTIVE, actually allows 2MB
- **Impact:** Demo users (unauthenticated) can send 2MB payloads instead of 50KB — free resource abuse vector. Authenticated users can send 2MB instead of 500KB.
- **Verified:** Sent 52KB payload to `/v1/demo/html` on production — got HTTP 200 instead of expected 413.
- **Fix:** Remove global JSON parser, apply route-specific parsers before each route group with correct limits.
- **Status:** ✅ FIXED — commit c03f217. Removed global `express.json()`, applied route-specific parsers per-route. 4 TDD tests added (body-limits.test.ts). 468 tests total, all passing. Pushed to main (staging auto-deploy).

## BUG-100: Usage flush transaction error handling broken — one bad key poisons entire batch
- **Date:** 2026-03-04
- **Severity:** MEDIUM
- **Issue:** `flushDirtyEntries()` in `src/middleware/usage.ts` wraps all usage writes in a single PostgreSQL transaction (`BEGIN`/`COMMIT`) with per-key `try/catch` inside the loop. In PostgreSQL, if any single INSERT fails inside a transaction, the transaction enters an "aborted" state and ALL subsequent queries fail with "current transaction is aborted, commands ignored until end of transaction block." The per-key error recovery is ineffective — one bad key causes all remaining keys in the batch to silently fail.
- **Impact:** If any single usage INSERT fails (e.g., constraint violation, type error), ALL remaining usage counts in that flush batch are lost. Could cause usage count divergence between in-memory and DB.
- **Fix:** Remove the transaction wrapper and flush each key independently, or use SAVEPOINTs.
- **Status:** ✅ FIXED — commit d2f819d. Removed transaction wrapper, each key flushes independently with its own client. 1 TDD test added. 464 tests total, all passing. Pushed to main (staging auto-deploy).

## BUG-099: provisionedSessions Set in billing.ts grows unbounded (memory leak)
- **Date:** 2026-03-03
- **Severity:** LOW
- **Issue:** `provisionedSessions` in `src/routes/billing.ts` is an unbounded `Set<string>` that stores every Stripe checkout session ID forever. Used to prevent duplicate key provisioning on success page refresh. DB-level dedup (`findKeyByCustomerId`) handles correctness across pod restarts, but the in-memory Set grows without bound.
- **Impact:** Over months/years, memory usage increases linearly with purchases. Low real-world impact given expected volume, but a code quality issue.
- **Fix:** Replace with a TTL Map that auto-expires entries after 24h.
- **Status:** ✅ FIXED — commit 5f776db. Replaced Set with TTL Map (24h expiry, hourly cleanup). 4 TDD tests added. 447 tests total, all passing. Pushed to main (staging auto-deploy).

## BUG-098: Request interceptor leaks across browser pool pages after URL-to-PDF conversion
- **Date:** 2026-03-02
- **Severity:** MEDIUM
- **Issue:** In `src/services/browser.ts`, `renderUrlPdf()` calls `page.setRequestInterception(true)` and adds a `page.on("request", ...)` listener for SSRF DNS pinning. `recyclePage()` never cleans these up. When the page is returned to the pool and reused, the stale interceptor blocks external resource requests (fonts, images, stylesheets) for subsequent HTML-to-PDF conversions. Successive URL-to-PDF calls on the same page also stack listeners.
- **Impact:** After a URL-to-PDF conversion, any HTML-to-PDF conversion reusing the same page may fail to load external resources. Low probability in practice (depends on pool cycling) but a real data corruption vector.
- **Fix:** Add `page.removeAllListeners("request")` and `page.setRequestInterception(false)` to `recyclePage()`.
- **Status:** ✅ FIXED — commit 024fa00. recyclePage now calls removeAllListeners("request") + setRequestInterception(false). 1 test added (TDD red→green verified). 443 tests total. Deployed to staging.

## BUG-097: Footer "Support" link missing on /examples, /privacy, and /status pages
- **Date:** 2026-03-02
- **Severity:** LOW
- **Issue:** The footer "Support" mailto link (`support@docfast.dev`) is present on /, /impressum, and /terms but MISSING from /examples, /privacy, and /status. Footer should be consistent across all pages.
- **Status:** ✅ FIXED — commit 6290c3e. Added Support link to shared footer partial. Verified on staging.
- **Status:** ✅ FIXED — commit 6290c3e. Added Support mailto link to `_footer.html` partial. 2 tests added. Verified on staging.

## BUG-096: Demo endpoint accepts invalid scale value (scale:99) without returning 400 error
- **Date:** 2026-03-02
- **Severity:** MEDIUM
- **Issue:** POST /v1/demo/html with `{"html":"<h1>test</h1>","options":{"scale":99}}` returns HTTP 200 and generates a PDF instead of returning a 400 validation error. Chromium's `page.pdf()` scale parameter accepts values between 0.1 and 2.0, so scale:99 is clearly invalid and should be rejected. The API silently accepts it, which could cause unexpected behavior (Chromium may clamp or error internally).
- **Steps to reproduce:** `curl -X POST https://staging.docfast.dev/v1/demo/html -H "Content-Type: application/json" -d '{"html":"<h1>test</h1>","options":{"scale":99}}'` → returns PDF (200)
- **Expected:** HTTP 400 with validation error about scale range
- **Actual:** HTTP 200 with generated PDF
- **Note:** format:"invalid" and margin with non-string values could not be retested due to 5/hour demo rate limit. These may also lack validation.
- **Status:** ✅ FIXED — BUG-102 fix (commit ba2e542) added `validatePdfOptions()` to all demo and convert routes. Scale validation (0.1–2.0) now enforced. Original test used nested `options` object (wrong API format); top-level `scale:99` is correctly rejected.

## BUG-095: /docs page footer missing most footer links (Home, Docs, Examples, API Status, Support, Change Email)
- **Date:** 2026-03-02
- **Severity:** LOW
- **Issue:** The /docs page (Swagger UI) footer only shows Impressum, Privacy Policy, and Terms of Service. Missing Home, Docs, Examples, API Status, Support, and Change Email links.
- **Status:** ✅ FIXED — commit 6290c3e. Expanded docs.html footer to include all 9 links. Verified on staging.
- **Status:** ✅ FIXED — commit 6290c3e. Expanded docs.html footer to include all 9 links. 2 tests added. Verified on staging.

## BUG-092: Footer missing "Change Email" link on landing page
- **Date:** 2026-03-01
- **Severity:** LOW
- **Issue:** The email change modal exists in the DOM but the footer has no link to trigger it. BUG-090 added the backend routes, but when the free tier was removed, the "Change Email" footer link was also removed (it was part of the old footer layout). Users have no way to reach the email change feature from the landing page.
- **Fix:** Add "Change Email" link back to footer with `class="open-email-change"`.
- **Status:** ✅ FIXED — commit added Change Email link to both `public/src/index.html` footer and `public/partials/_footer.html`. 2 tests added verifying footer link presence. 438 tests total, all passing.

## BUG-090: Email Change modal calls non-existent backend routes — feature broken
- **Date:** 2026-02-27
- **Severity:** HIGH
- **Issue:** The "Change Email" modal on the landing page (and footer links on all pages) calls `POST /v1/email-change` and `POST /v1/email-change/verify`. These backend routes DO NOT EXIST. The frontend was built but the backend was never implemented. Users who try to change their email get a 404 "Not Found" error.
- **Impact:** Complete user flow is broken. A feature advertised on every page doesn't work. Violates "every user flow must be complete" rule.
- **Fix:** Implement `src/routes/email-change.ts` with both endpoints, following existing verification code pattern.
- **Status:** ✅ FIXED — commit 480c794. Route + 9 tests added. 291 tests total, all passing. Awaiting CI build (runner appears down).

## BUG-089: Examples page not linked from main navigation or footer
- **Date:** 2026-02-27
- **Severity:** MEDIUM
- **Issue:** The `/examples` page exists and is in the sitemap, but is NOT linked from the main landing page nav or footer. The examples page itself includes "Examples" in its nav, but all other pages (/, /docs, /impressum, /privacy, /terms, /status) lack it. Developers can't discover code examples from the homepage.
- **Impact:** High-value developer conversion page is hidden. Only discoverable via sitemap or direct URL.
- **Fix:** Add "Examples" link to nav and footer partials so it appears on all pages.
- **Status:** ✅ FIXED — commit aa7fe55. Added to landing page inline nav, landing page footer, and shared footer partial. Nav partial already had the link.

## BUG-088: Landing page and FAQ schema falsely claim SDKs exist
- **Date:** 2026-02-26
- **Severity:** MEDIUM
- **Issue:** FAQ structured data and landing page text both claim "Official SDKs for Node.js, Python, Go, PHP, and Laravel" exist. SDKs have NOT been published — only code examples exist. The /examples page was fixed (BUG-086) but landing page and FAQ schema were missed.
- **Impact:** Misleading structured data could hurt SEO trust. Users expecting `npm install docfast` would be confused.
- **Fix:** Updated FAQ answer and landing page subtitle to say "Code examples for Node.js, Python, Go, PHP, and cURL. Official SDKs coming soon."
- **Status:** ✅ FIXED — commit 9dcc473

## BUG-087: OpenAPI spec empty on staging — swagger-jsdoc 7.0.0-rc.6 regression
- **Date:** 2026-02-25
- **Severity:** HIGH
- **Issue:** Session 93 upgraded swagger-jsdoc from 6.2.8 to 7.0.0-rc.6 to fix a minimatch ReDoS vuln. The RC is broken — `swaggerJsdoc()` returns `{}` (empty object, 0 paths). The `/docs` page on staging shows no API endpoints. `/openapi.json` returns `{}`.
- **Root cause:** swagger-jsdoc 7.0.0-rc.6 does not parse `@openapi` JSDoc annotations from glob-matched files.
- **Impact:** Staging has no API documentation. Production (v0.4.5) is unaffected (still on 6.2.8 build).
- **Fix:** Reverted to swagger-jsdoc 6.2.8. Added 2 regression tests verifying OpenAPI spec has paths and includes key endpoints. npm audit still shows 0 vulnerabilities.
- **Status:** ✅ FIXED — commit 288d6c7, deploying to staging

## BUG-080: Landing page still shows Free tier after v0.4.0 "removal"
- **Date:** 2026-02-20
- **Severity:** HIGH
- **Issue:** CEO reported free tier removed and landing page updated in v0.4.0, but both staging and production still show the old Free tier pricing card, "Get Free API Key" CTA, and signup modal. The backend may have demo endpoints but the static HTML was never actually changed.
- **Affected:** staging.docfast.dev AND docfast.dev landing pages
- **Root cause:** CEO bumped version and reported completion without verifying user-facing output
- **Fix needed:** Actually update all landing page HTML — remove Free tier card, add playground/demo, update CTAs to "Try Demo" + "Get Pro API Key"
- **Status:** ✅ FIXED (v0.4.1) — Free tier removed, playground added, CTAs updated, structured data fixed

---

# DocFast QA Full Audit — 2026-02-20

**Tester:** QA Bot (subagent)
**Version:** 0.3.2
**URL:** https://docfast.dev
**Date:** 2026-02-20 07:00 UTC

---

## New Bugs Found

## BUG-079: Billing Checkout Endpoint Has No Authentication — CRITICAL
- **Date:** 2026-02-20
- **Severity:** CRITICAL
- **Endpoint:** `POST /v1/billing/checkout`
- **Issue:** The billing checkout endpoint creates Stripe checkout sessions without ANY authentication. No API key required, no validation of provided keys. Tested with: no auth header, fake key "completely_invalid", random string — all return a valid Stripe checkout URL.
- **Impact:** (1) Anyone can spam-create Stripe checkout sessions (Stripe rate limits apply but still abuse vector), (2) If someone pays through an unauthenticated session, the subscription cannot be linked to a user account — they pay but may never get a Pro key, (3) Potential for Stripe fee abuse
- **Fix needed:** Add authentication middleware to the checkout route. Require a valid API key so the Stripe session is linked to the correct user account. Return 401/403 for unauthenticated requests.
- **Status:** ✅ FIXED (v0.3.4) — Rate limited to 3/IP/hour, body size check (1KB max), IP logging. Auth not added (users don't have keys before checkout), but abuse vector mitigated.

---

## Test Results

| # | Test | Result | Notes |
|---|------|--------|-------|
| 1 | Landing page loads | ✅ PASS | All sections render, version 0.3.2 |
| 2 | Console errors (homepage) | ✅ PASS | Zero errors |
| 3 | Mobile 375×812 (iPhone) | ✅ PASS | Layout stacks properly, no overflow |
| 4 | Mobile 768×1024 (iPad) | ✅ PASS | Responsive layout good |
| 5 | Nav links (Features, Pricing, Docs) | ✅ PASS | All work |
| 6 | Footer links (Home, Docs, Status, Impressum, Privacy, Terms) | ✅ PASS | All 6 links work |
| 7 | Signup flow — modal opens | ✅ PASS | "Get Free API Key" opens dialog |
| 8 | Signup — invalid email client-side | ✅ PASS | Shows "Please enter a valid email address" |
| 9 | Signup — invalid email API | ✅ PASS | Returns `{"error":"A valid email address is required."}` |
| 10 | Recovery flow — modal opens | ✅ PASS | "Lost your API key?" opens recovery dialog |
| 11 | /docs page (Swagger UI) | ✅ PASS | Full API docs, all endpoint groups |
| 12 | /docs console errors | ✅ PASS | Zero errors |
| 13 | /docs SEO meta tags | ✅ PASS | description, og:, twitter: all present (BUG-059 fixed) |
| 14 | /docs footer legal links | ✅ PASS | Impressum, Privacy, Terms present (BUG-069 fixed) |
| 15 | /status page | ✅ PASS | Shows real data: DB connected, PDF engine ready, 15/15 pool |
| 16 | /status console errors | ✅ PASS | Zero errors |
| 17 | /impressum | ✅ PASS | Full legal info, correct company data |
| 18 | /privacy | ✅ PASS | GDPR-compliant, 11 sections |
| 19 | /terms | ✅ PASS | Renders properly |
| 20 | Console errors (all pages) | ✅ PASS | ZERO errors across all 6 pages |
| 21 | Skip-to-content link | ✅ PASS | Present on all pages (BUG-067 fixed) |
| 22 | Heading hierarchy | ✅ PASS | h1 → h2 → h3, no skips (BUG-063 fixed) |
| 23 | Modal aria-modal | ✅ PASS | Present (BUG-066 fixed) |
| 24 | Modal labels | ✅ PASS | 4 labels found (BUG-064 fixed) |
| 25 | Close button aria-label | ✅ PASS | aria-label="Close" (BUG-065 fixed) |
| 26 | Sitemap | ✅ PASS | Correct namespace, all 6 pages (BUG-056, BUG-061 fixed) |
| 27 | HTTP headers | ✅ PASS | No duplicates (BUG-051/052 fixed) |
| 28 | Sub-pages og: tags | ✅ PASS | Present on impressum (BUG-060 fixed) |
| 29 | Health endpoint | ✅ PASS | DB status included, PostgreSQL 17.4 |
| 30 | Billing checkout auth | 🔴 FAIL | No authentication — BUG-079 |
| 31 | Performance | ✅ PASS | Pages load fast, no layout shifts |
| 32 | Keyboard navigation | ✅ PASS | Tab order sensible, skip link works |

---

## Previously Reported Bugs — Status Check

| Bug | Status | Notes |
|-----|--------|-------|
| BUG-051 (duplicate headers) | ✅ FIXED | Single headers now |
| BUG-052 (duplicate cache-control) | ✅ FIXED | Single header |
| BUG-053 (JS not minified) | ⚠️ STILL OPEN | 520 lines / 17KB, low priority |
| BUG-056 (sitemap namespace) | ✅ FIXED | Correct sitemaps.org |
| BUG-059 (/docs SEO) | ✅ FIXED | Full meta tags |
| BUG-060 (sub-page og tags) | ✅ FIXED | og: tags present |
| BUG-061 (/status in sitemap) | ✅ FIXED | Added |
| BUG-062 (main wraps only hero) | ✅ FIXED | main has id="main-content" |
| BUG-063 (heading skip) | ✅ FIXED | h2 for "Hosted in EU" |
| BUG-064 (modal labels) | ✅ FIXED | Labels present |
| BUG-065 (close button aria) | ✅ FIXED | aria-label="Close" |
| BUG-066 (aria-modal) | ✅ FIXED | aria-modal present |
| BUG-067 (skip-to-content) | ✅ FIXED | Skip link on all pages |
| BUG-069 (/docs footer) | ✅ FIXED | Legal links added |

---

## Summary

- **Total tests:** 32
- **Passed:** 31
- **Failed:** 1 (BUG-079 — CRITICAL: unauthenticated billing checkout)
- **New bugs:** 1 (CRITICAL severity)
- **Previously open bugs now fixed:** 14
- **Still open from before:** BUG-053 (JS minification, LOW)

**Overall Quality Assessment:** The site is in excellent shape. Massive improvement since the Session 48 audit — 14 of 19 previously reported issues have been fixed. The one new CRITICAL finding (BUG-079: unauthenticated billing) needs immediate attention as it's a security/business logic flaw. Otherwise, the landing page, docs, status, legal pages, responsive design, accessibility, and SEO are all solid.

---

## BUG-078: SMTP Config Pointed to Old Server
- **Date:** 2026-02-19
- **Severity:** HIGH
- **Issue:** K8s secret SMTP_HOST was set to 167.235.156.214 (old decommissioned server) instead of mail.cloonar.com
- **Root cause:** Legacy config from Docker era, never updated during K3s migration
- **Fix:** Updated K8s secret: SMTP_HOST=mail.cloonar.com, SMTP_PORT=587, SMTP_USER/SMTP_PASS from docfast.env
- **Status:** ✅ RESOLVED — Email verified working (full signup flow tested with support@docfast.dev)
- **NOTE:** SMTP is managed by Cloonar (mail.cloonar.com). There is NO Postfix on K3s. Do NOT deploy mail infrastructure.

## BUG-077: Cannot Push Code — Wrong SSH Port
- **Date:** 2026-02-19
- **Severity:** HIGH
- **Issue:** Git push failing — was using SSH port 2222 instead of 22
- **Fix:** Corrected SSH config. Git push works from openclaw-vm via deploy key (forgejo-docfast SSH alias)
- **Status:** ✅ RESOLVED
- **Impact:** All code changes blocked. 4 fixes prepared but can't be deployed.
- **Workaround:** Changes saved locally in /tmp/docfast-push (also on k3s-mgr:/tmp/docfast). Can be pushed once access is restored.
- **Fix needed:** (1) Restart Forgejo SSH service or container, (2) Create new API token with write:repository scope
- **Status:** OPEN — escalated to investor

## BUG-075: Database Connection Pool Does Not Handle PgBouncer Failover
- **Date:** 2026-02-18 14:05 UTC
- **Severity:** CRITICAL
- **Issue:** When a PgBouncer pooler instance goes down (K8s node failure), pods keep stale TCP connections and get "no available server" errors. App never reconnects — all PDF conversions fail until manual pod restart.
- **Root cause:** `pg.Pool` in `src/services/db.ts` has no keepAlive, no connectionTimeoutMillis, no retry logic. No mechanism to detect or recover from dead connections.
- **Impact:** Complete service outage for pods connected to failed pooler. Violates HA requirements.
- **Fix needed:** (1) Enable TCP keepalive + connection timeout, (2) Add retry with backoff for transient DB errors, (3) Health check must fail when DB is unreachable (already partially done but needs timeout), (4) Verify recovery without pod restart
- **Status:** ✅ FIXED (PROPERLY) — v0.2.6 fix was insufficient (retried on same dead pool connections). Commit 95ca101 fixes the real issue: `queryWithRetry` now uses explicit client checkout and calls `client.release(true)` to DESTROY dead connections on transient errors, forcing fresh TCP connections on retry. `connectWithRetry` validates with `SELECT 1` before returning. `idleTimeoutMillis` reduced to 10s. Health check destroys bad connections. **Verified on staging:** killed a pooler pod, 15/15 health checks passed, zero app restarts.

## BUG-074: Email Broken on K3s Production — SMTP Misconfigured
- **Date:** 2026-02-18 13:00 UTC
- **Severity:** CRITICAL
- **Issue:** After K3s migration, production pods used old Docker code connecting to `host.docker.internal:25` (non-existent in K3s). The SMTP fix (commit 0902e1e) was on staging but not deployed to production.
- **Root cause:** Three issues: (1) Production image at old commit without SMTP env var support, (2) K3s secrets pointed to mail.cloonar.com which rejected K3s worker IPs, (3) Old server Postfix only listened on localhost + Docker bridge, not public IP
- **Fix applied:**
  1. Updated K8s SMTP secrets to use old server (167.235.156.214) as relay (has DKIM for docfast.dev)
  2. Added K3s worker IPs to old server's Postfix `mynetworks` and UFW rules (port 25)
  3. Made Postfix listen on public IP (`inet_interfaces`)
  4. Tagged v0.2.3 to deploy SMTP fix to production
  5. Restarted all pods to pick up new secrets
- **Verification:** `Verification email sent` confirmed in production logs
- **Status:** ✅ FIXED

## BUG-073: Staging Landing Page Shows Wrong Pro Plan Quota (2,500 vs 5,000)
- **Date:** 2026-02-18 13:05 UTC
- **Severity:** MEDIUM
- **Issue:** Landing page showed "2,500" but Stripe said "5,000". Mismatch.
- **Fix:** Landing page + JSON-LD updated to 5,000. Tagged v0.2.4.
- **Status:** ✅ FIXED (Session 53)

## BUG-076: k3s-w1 Node Down — Complete Network Unreachability
- **Date:** 2026-02-18 16:00 UTC
- **Severity:** HIGH (degraded HA, not outage)
- **Issue:** k3s-w1 (159.69.23.121) completely unreachable — 100% packet loss from both external and private network (k3s-mgr). Node shows NotReady in K8s. CNPG failover triggered: primary moved to main-db-2 on w2. Production running on single node (w2 only).
- **Impact:** HA is degraded — running on 1 worker. If w2 also fails, full outage. No data loss (DB failover worked).
- **Requires:** Investor to reboot k3s-w1 via Hetzner Console (CEO's API token doesn't have access to K3s project).
- **Status:** OPEN — escalated to investor

---

## BUG-072: Production Outage — UFW+Docker Conflict + Dual Deployment
- **Date:** 2026-02-18 ~08:00 UTC
- **Severity:** CRITICAL
- **Duration:** Unknown start time → fixed 08:04 UTC
- **Symptoms:** App container stuck in restart loop, unable to reach PostgreSQL. `ENETUNREACH 172.17.0.1:5432`
- **Root causes:**
  1. UFW injected DROP rules into Docker's DOCKER iptables chain, blocking container-to-host networking
  2. A systemd service (`docfast.service`) running a separate Node.js process from `/opt/docfast` was binding port 3100, preventing Docker container from starting after fix
- **Fix applied:**
  - Removed iptables DROP rules from DOCKER chain
  - Stopped and disabled `docfast.service` systemd unit
  - Changed DATABASE_HOST from `172.17.0.1` to `host.docker.internal` in docker-compose.yml
  - Full `docker compose down && up -d` — container healthy
- **Permanent fix:** DevOps agent dispatched to properly resolve ufw+Docker conflict and remove dual deployment
- **Status:** FIXED (immediate), permanent fix in progress

---

# DocFast QA Report — 2026-02-15

**Tester:** QA Bot (automated)
**Version:** 0.2.1
**URL:** https://docfast.dev

---

## Bug Fix Verification

### ✅ BUG-032: Mobile Terminal Gap — FIXED
- Tested at 375×812 viewport
- `.code-section` uses `display: flex; flex-direction: column`
- Gap between `.code-header` and `.code-block`: **0px** ✅
- Screenshot confirms no visible gap

### ✅ BUG-035: STRIPE_WEBHOOK_SECRET Deployed — VERIFIED
- Pro "Get Started →" button redirects to Stripe checkout
- Stripe page shows "Subscribe to DocFast Pro" at $9.00/mo
- Merchant: Cloonar Technologies GmbH
- Stripe checkout fully functional

### ⚠️ BUG-037: Webhook product_id Filter — CANNOT VERIFY
- Cannot test webhook handler directly (requires Stripe event)
- Stripe checkout page loads correctly, suggesting integration is wired up
- **Needs manual verification** with a test Stripe webhook event

---

## Test Results

### 1. Console Errors — ✅ PASS
- Zero JS errors in browser console
- Zero warnings

### 2. Mobile Terminal Gap — ✅ PASS
- 375×812 viewport, zero gap between terminal header and body
- Flexbox layout confirmed via computed styles

### 3. Signup Flow — ✅ PASS
- "Get Free API Key" button opens modal
- Email input works, "Generate API Key →" submits
- Verification code screen appears with correct email displayed
- API: `POST /v1/signup/free` returns `{"status":"verification_required"}`
- API: `POST /v1/signup/verify` with wrong code returns `{"error":"Invalid verification code."}`

### 4. Pro Checkout — ✅ PASS
- "Get Started →" redirects to Stripe checkout
- Correct product: DocFast Pro, $9.00/month
- Full Stripe payment form (card, billing address, etc.)

### 5. /docs Page — ✅ PASS
- Swagger UI loads with full API documentation
- All endpoint groups visible: Conversion, Templates, Account, Billing, System
- OpenAPI spec accessible at `/openapi.json`

### 6. Health Endpoint — ⚠️ PARTIAL PASS
- `GET /health` returns `{"status":"ok","version":"0.2.1",...}`
- Pool stats included (size, active, available, queue depth)
- **Issue: No PostgreSQL connection info in health response**
  - No `database` or `postgres` field
  - No DB version reported
  - Health check only covers the browser pool, not the database
  - **Severity: LOW** — DB issues would surface as auth/signup failures, but health endpoint should ideally confirm DB connectivity

### 7. HTML→PDF Generation — ⏭️ SKIPPED
- Cannot complete without a valid API key (email verification requires receiving actual email)
- Signup works, but test environment can't receive verification emails at `@test.docfast.dev`

### 8. Error Handling — ✅ PASS
- Bad API key → `403 {"error":"Invalid API key"}` ✅
- No API key → `401 {"error":"Missing API key. Use: Authorization: Bearer <key> or X-API-Key: <key>"}` ✅
- Missing params with bad key → `403` (auth checked first, correct behavior) ✅
- **Cannot test missing params with valid key** (see #7)

---

## New Issues Found

### BUG-038: Health Endpoint Missing Database Status
- **Severity:** LOW
- **Endpoint:** `GET /health`
- **Expected:** Health response should include PostgreSQL connection status and version
- **Actual:** Only returns browser pool stats, no database info
- **Impact:** Monitoring blind spot — DB could be down but /health reports "ok"

### BUG-039: API Signup Endpoint Mismatch in Docs
- **Severity:** INFO
- **Details:** The docs page references `POST /v1/signup/free` but the original test spec listed `POST /v1/auth/signup` — this is just a documentation/spec mismatch in the test plan, not a bug in the app itself

---

## Summary

| Test | Result |
|------|--------|
| Console errors | ✅ PASS (0 errors) |
| Mobile terminal gap | ✅ PASS (0px gap) |
| Signup flow | ✅ PASS |
| Pro checkout → Stripe | ✅ PASS |
| /docs page | ✅ PASS |
| Health endpoint | ⚠️ PARTIAL (no DB status) |
| PDF generation | ⏭️ SKIPPED (no valid key) |
| Error handling | ✅ PASS |

**Overall: 5 PASS, 1 PARTIAL, 1 SKIPPED, 1 N/A**

The three reported bugs (BUG-032, BUG-035, BUG-037) are verified fixed (032, 035) or plausibly fixed (037 — needs webhook test). One new low-severity issue found (health endpoint missing DB status).

---

# DocFast QA Full Regression — 2026-02-16

**Tester:** QA Bot (harsh mode)
**Trigger:** Container was found DOWN this morning, restarted
**URL:** https://docfast.dev
**Browser:** Chrome (OpenClaw profile)
**Tests:** Full regression suite

---

## BUG-050: Broken MX Record Causes Email Delivery Failures — CRITICAL
- **Severity:** CRITICAL
- **Issue:** MX record for docfast.dev resolves to `mail.cloonar.com.docfast.dev` (non-existent) instead of a valid mail server. This is a relative hostname in DNS that got appended to the zone.
- **Impact:** Any mail server doing sender address verification (like cloonar.com) rejects our emails. Customer #370 cannot receive verification codes. This likely affects other recipients too.
- **Root cause:** MX DNS record was entered as `mail.cloonar.com` without trailing dot, so Hetzner DNS appended `.docfast.dev`
- **Fix needed:** Investor must fix MX record in Hetzner DNS console:
  - **Option A (recommended):** Set MX to `docfast.dev.` (point to own server, since Postfix runs there)
  - **Option B:** Delete the broken MX record entirely (servers will fall back to A record)
- **Workaround applied:** Postfix now accepts local mail for noreply@docfast.dev (mydestination + virtual alias), but this only helps if the remote server can reach us — which it can't due to broken MX.
- **Status:** OPEN — requires investor DNS action
- **Discovered:** 2026-02-17 Session 48

---

## Test Results Summary

| Test Category | Status | Details |
|--------------|--------|---------|
| Site Load + Console | ✅ PASS | ZERO JS errors (requirement met) |
| Signup Flow | ✅ PASS | Email → verification screen works |
| Pro → Stripe | ✅ PASS | Redirect + checkout form working |
| /docs Swagger UI | ✅ PASS | Full API documentation loads |
| Mobile Responsive | ✅ PASS | 375×812 layout perfect |
| /health endpoint | ✅ PASS | Database status included |
| API Tests | ✅ PASS | All endpoints working |
| Error Handling | ✅ PASS | 401/403 responses correct |

**Overall Result: ALL TESTS PASS ✅**

---

## Detailed Test Results

### 1. Site Load & Console Errors — ✅ PASS
- **Requirement:** ZERO JS errors
- **Result:** Console completely clean, no errors/warnings
- **URL:** https://docfast.dev
- **Screenshots:** Homepage visual verification passed

### 2. Full Signup Flow — ✅ PASS
- **Test:** Email → verification code screen appears
- **Steps:**
  1. Clicked "Get Free API Key →" button
  2. Modal appeared with email input
  3. Entered "qa-test@example.com"
  4. Clicked "Generate API Key →"
  5. **✅ SUCCESS:** Verification screen appeared with:
     - "Enter verification code" heading
     - Email address displayed: qa-test@example.com
     - 6-digit code input field
     - "Verify →" button
     - "Code expires in 15 minutes" text

### 3. Pro → Stripe Checkout — ✅ PASS
- **Test:** Pro plan redirects to Stripe properly
- **Steps:**
  1. Clicked "Get Started →" on Pro plan ($9/mo)
  2. **✅ SUCCESS:** Redirected to Stripe checkout page with:
     - "Subscribe to DocFast Pro" heading
     - $9.00 per month pricing
     - Full payment form (card, expiry, CVC, billing)
     - "Pay and subscribe" button
     - Powered by Stripe footer

### 4. /docs Page with Swagger UI — ✅ PASS
- **Test:** Swagger UI loads completely
- **Result:** Full API documentation loaded with:
  - DocFast API 1.0.0 header
  - Authentication & rate limits info
  - All endpoint categories:
    - **Conversion:** HTML, Markdown, URL to PDF
    - **Templates:** List & render templates
    - **Account:** Signup, verify, recovery, email change
    - **Billing:** Stripe checkout
    - **System:** Usage stats, health check
  - Interactive "Try it out" buttons
  - OpenAPI JSON link working
  - Schemas section

### 5. Mobile Test — ✅ PASS
- **Test:** browser resize to 375×812 (iPhone X)
- **Result:** Perfect responsive layout
  - All content visible and readable
  - Proper scaling and text sizes
  - Swagger UI adapts well to mobile
  - No horizontal scrolling issues

### 6. Health Endpoint — ✅ PASS
- **Browser test:** https://docfast.dev/health
- **Result:** Clean JSON response with database status:
```json
{
  "status": "ok",
  "version": "0.1.0",
  "database": {
    "status": "ok",
    "version": "PostgreSQL 16.11"
  },
  "pool": {
    "size": 15,
    "active": 0,
    "available": 15,
    "queueDepth": 0,
    "pdfCount": 0,
    "restarting": false,
    "uptimeSeconds": 125
  }
}
```

### 7. API Tests via curl — ✅ PASS

#### Health Check API
```bash
curl -s https://docfast.dev/health
# ✅ SUCCESS: Returns OK with database status
```

#### Free Signup API
```bash
curl -s -X POST https://docfast.dev/v1/signup/free \
  -H "Content-Type: application/json" \
  -d '{"email":"api-test@example.com"}'
# ✅ SUCCESS: {"status":"verification_required","message":"Check your email for the verification code."}
```

#### Error Handling Tests

**Bad API Key (403):**
```bash
curl -s -X POST https://docfast.dev/v1/convert/html \
  -H "Authorization: Bearer invalid-key-123" \
  -H "Content-Type: application/json" \
  -d '{"html":"<h1>Test</h1>"}'
# ✅ SUCCESS: {"error":"Invalid API key"} HTTP 403
```

**Missing API Key (401):**
```bash
curl -s -X POST https://docfast.dev/v1/convert/html \
  -H "Content-Type: application/json" \
  -d '{"html":"<h1>Test</h1>"}'
# ✅ SUCCESS: {"error":"Missing API key. Use: Authorization: Bearer <key> or X-API-Key: <key>"} HTTP 401
```

---

## Issues Found

**ZERO ISSUES FOUND** 🎉

All systems operational after container restart. The site is working perfectly across all test scenarios.

---

## Test Environment

- **Date:** 2026-02-16 08:30 UTC
- **Browser:** Chrome (OpenClaw headless)
- **Resolution:** 1280×720 (desktop), 375×812 (mobile)
- **Network:** Direct sandbox connection
- **API Client:** curl 8.5.0

---

## Post-Container-Restart Status: ✅ FULLY OPERATIONAL

Container restart appears to have been clean. All services came back online properly:
- Web frontend: ✅
- API backend: ✅
- Database connections: ✅
- Stripe integration: ✅
- Email verification system: ✅ (API endpoints working)

**Recommendation:** Continue monitoring, but no urgent issues detected.

---

# CEO Code Audit — 2026-02-16

## BUG-046: Usage Endpoint Leaks Other Users' Data
- **Severity:** CRITICAL
- **Endpoint:** `GET /v1/usage`
- **Issue:** `getUsageStats()` returned ALL users' usage data to any authenticated user. GDPR violation.
- **Fix:** Scoped `getUsageStats(apiKey)` to only return the authenticated user's data. Route passes `req.apiKeyInfo.key`.
- **Status:** ✅ FIXED (Session 41)

## BUG-047: No Copy Button on Pro Key Success Page
- **Severity:** HIGH
- **Page:** `/v1/billing/success`
- **Issue:** Pro key displayed but no visible copy button
- **Fix:** Added visible "Copy" button that changes to "Copied!" on click
- **Status:** ✅ FIXED (Session 41)

## BUG-048: Change Email Modal Never Opens
- **Severity:** HIGH
- **Issue:** Footer "Change Email" links used `href="/#change-email"` but lacked `class="open-email-change"` that JS targets
- **Fix:** Added `class="open-email-change"` to all Change Email links across all 4 HTML pages
- **Status:** ✅ FIXED (Session 41)

---

## BUG-040: SSRF Vulnerability in URL→PDF Endpoint
- **Severity:** HIGH
- **Endpoint:** `POST /v1/convert/url`
- **Issue:** URL validation only checks protocol (http/https) but does NOT block private/internal IP addresses. Attacker can request internal URLs like `http://169.254.169.254/latest/meta-data/` (cloud metadata), `http://127.0.0.1:3100/health`, or any RFC1918 address.
- **Fix:** Resolve hostname via DNS before passing to Puppeteer, block private IP ranges.
- **Status:** FIXED (verified in Session 38)

## BUG-041: Docker Healthcheck Broken — Container Permanently "Unhealthy"
- **Severity:** MEDIUM
- **Status:** ✅ FIXED (Session 39 → verified Session 40, container shows "healthy")

## BUG-042: Pricing in USD Instead of EUR
- **Severity:** MEDIUM
- **Status:** ✅ FIXED (Session 39 → QA verified Session 40, Stripe shows €9.00/mo)

## BUG-043: No Legal Pages (Impressum, Privacy, Terms)
- **Severity:** HIGH
- **Status:** ✅ FIXED (Session 39, verified Session 40 — all 3 pages live with correct data)

## BUG-044: EU Hosting Not Marketed
- **Severity:** LOW
- **Status:** ✅ FIXED (Session 39, QA verified EU hosting badge on landing page)

---

# DocFast QA — Currency & Feature Test — 2026-02-16 16:03 UTC

**Tester:** QA Bot (harsh mode)
**URL:** https://docfast.dev

---

## CRITICAL TEST: Stripe Checkout Currency

### ✅ BUG-042 VERIFIED FIXED: EUR Currency in Stripe Checkout
- **Clicked:** Pro "Get Started →" button
- **Result:** Redirected to `checkout.stripe.com`
- **Price shown:** **€9.00 per month** ✅
- **Currency:** EUR (€ symbol confirmed)
- **Merchant:** Cloonar Technologies GmbH
- **Description:** "Unlimited PDF conversions via API. HTML, Markdown, and URL to PDF."
- **Screenshot:** Confirmed visually — €9.00, not $9.00

### ✅ BUG-045: Stripe Checkout Says "Unlimited" But Landing Page Says "10,000"
- **Severity:** MEDIUM
- **Status:** ✅ FIXED (Session 40) — Landing page updated to "Unlimited PDF conversions" to match Stripe and actual code behavior (Pro has no limit in code). Commit d7b0a0e deployed.

---

## Feature Tests

### ✅ "Change Email" Footer Link — PASS
- Clicked "Change Email" in footer
- Opens modal dialog with:
  - "Change your email" heading
  - API key input field (placeholder: "df_free_... or df_pro_...")
  - New email input field
  - "Send Verification Code →" button
  - Helper text: "A verification code will be sent to your new email"
- **Working correctly**

### ✅ "Lost your API key? Recover it →" Link — PASS
- Clicked the link in hero section
- Opens modal dialog with:
  - "Recover your API key" heading
  - Email input field
  - "Send Verification Code →" button
  - Security note: "Your key will be shown here after verification — never sent via email"
- **Working correctly**

### ✅ Mobile Responsive (375×812) — PASS
- Full-page screenshot at iPhone X dimensions
- Layout stacks correctly: nav → hero → stats → features → pricing → footer
- Text readable, no overflow, no horizontal scroll
- Pricing cards stack vertically
- Terminal code block fits properly

### ✅ Console Errors — PASS
- **ZERO errors** in browser console
- **ZERO warnings**

---

## Landing Page Observations
- Pricing shows €0/mo (Free) and €9/mo (Pro) — EUR on landing page ✅
- EU hosting section present with 🇪🇺 flag ✅
- Footer has: Docs, API Status, Change Email, Impressum, Privacy Policy, Terms of Service ✅

---

## Summary

| Test | Status |
|------|--------|
| Stripe EUR pricing (€9.00/mo) | ✅ PASS |
| Change Email link | ✅ PASS |
| Recover API Key link | ✅ PASS |
| Mobile responsive 375×812 | ✅ PASS |
| Console errors | ✅ PASS (zero) |

**New issue:** BUG-045 — Stripe/landing page copy mismatch ("Unlimited" vs "10,000") — **FIXED by CEO (Session 40)**: Updated Stripe product description to "10,000 PDF conversions per month"

**Overall: 5/5 PASS, 1 new medium-severity bug found and fixed**

---

# QA Audit — Session 48

**Tester:** QA Subagent (deep audit)
**Date:** 2026-02-17 08:00–08:30 UTC
**URL:** https://docfast.dev
**Version:** 0.2.1
**Scope:** Performance · SEO · Accessibility · Cross-page Consistency

---

## 🔴 PERFORMANCE AUDIT

### BUG-051: Duplicate HTTP Response Headers — Nginx Misconfiguration
- **Severity:** MEDIUM
- **Affected:** All responses (HTML, JS, PNG)
- **Issue:** Two headers appear twice in every HTTP response:
  - `Vary: Accept-Encoding` — appears twice in HTML response
  - `X-Content-Type-Options: nosniff` — appears twice in all responses
- **Root cause:** Likely set in both the nginx base config and the application (Express helmet)
- **Impact:** RFC violation; some proxies may mishandle duplicate headers; wasted bytes
- **Fix:** Remove duplicate from one layer (either nginx or helmet)

### BUG-052: Duplicate `Cache-Control` Headers on Static Assets
- **Severity:** MEDIUM
- **Affected:** `app.js`, `status.js`, `og-image.png`
- **Issue:** Two conflicting Cache-Control headers are returned simultaneously:
  - `Cache-Control: max-age=604800` (without `public`)
  - `Cache-Control: public, max-age=604800, immutable`
- **Impact:** Proxy/CDN behavior is undefined when two Cache-Control headers conflict; should be a single directive
- **Fix:** Consolidate to one header: `Cache-Control: public, max-age=604800, immutable`

### BUG-053: JavaScript Files Not Minified
- **Severity:** LOW
- **Files:** `/app.js` (515 lines, 16,838 bytes), `/status.js` (48 lines, 3,285 bytes)
- **Issue:** Both JS files are served unminified with readable variable names, full whitespace, and comments
- **Impact:** ~30–40% larger payload than necessary; slower parse time (minor for these sizes)
- **Fix:** Add a build step: `terser app.js -o app.min.js` and update HTML references

### BUG-054: No Brotli Compression — Gzip Only
- **Severity:** LOW
- **Issue:** Server only supports gzip compression; does not serve Brotli (`Content-Encoding: br`) even when `Accept-Encoding: br` is sent
- **Impact:** Brotli compresses ~20–26% better than gzip for text assets; Chrome and all modern browsers support it
- **Fix:** Enable `brotli_static` in nginx or add `compression` middleware with Brotli support

### BUG-055: Duplicate `<link rel="preconnect">` Tags in Homepage HTML
- **Severity:** LOW
- **Issue:** The homepage HTML includes the Google Fonts preconnect hint twice:
  - Line 17: `<link rel="preconnect" href="https://fonts.googleapis.com">` (in `<head>`, no actual font loaded nearby)
  - Line 266: `<link rel="preconnect" href="https://fonts.googleapis.com">` (again, right before the stylesheet)
  - Same duplication for `https://fonts.gstatic.com`
- **Impact:** Wasted browser hint; negligible but signals copy-paste error in template
- **Fix:** Remove the first set (lines ~17–18); keep only the preconnect immediately before the font `<link>`

---

## 🟡 SEO AUDIT

### BUG-056: Sitemap Namespace URL Typo — Invalid Sitemap
- **Severity:** HIGH
- **File:** `https://docfast.dev/sitemap.xml`
- **Issue:** Sitemap uses wrong namespace: `http://www.sitemapns.org/schemas/sitemap/0.9`
  - Correct URL: `http://www.sitemaps.org/schemas/sitemap/0.9`
  - Difference: `sitemapns.org` vs `sitemaps.org` (extra "n")
- **Impact:** Google Search Console will reject this sitemap as invalid XML schema. Crawlers that validate the namespace may ignore it entirely.
- **Fix:** Change `sitemapns.org` → `sitemaps.org` in the sitemap XML

### BUG-057: JSON-LD Structured Data Has Stale Pro Plan Description
- **Severity:** MEDIUM
- **Location:** `<script type="application/ld+json">` in homepage `<head>`
- **Issue:** JSON-LD Pro offer says `"description": "5,000 PDFs per month"` but:
  - The landing page shows 10,000 PDFs per month for Pro
  - BUG-045 (previously fixed) updated Stripe to say "10,000 PDFs per month"
  - The JSON-LD was never updated — still shows the old 5,000 figure
- **Impact:** Google may display incorrect pricing/quota info in rich results
- **Fix:** Update JSON-LD Pro offer description to `"10,000 PDFs per month"`

### BUG-058: twitter:image Meta Tag Missing on Homepage
- **Severity:** LOW
- **Location:** `<head>` of `https://docfast.dev/`
- **Issue:** Homepage has `og:image` set to `/og-image.png` ✅ but the corresponding `<meta name="twitter:image">` tag is absent
- **Impact:** Twitter/X link previews won't show the card image even though the image exists and og:image is set (Twitter prefers its own tags when present)
- **Fix:** Add `<meta name="twitter:image" content="https://docfast.dev/og-image.png">`

### BUG-059: `/docs` Page Missing All SEO Meta Tags
- **Severity:** MEDIUM
- **URL:** `https://docfast.dev/docs`
- **Issue:** The docs page (Swagger UI) has:
  - No `<meta name="description">` ❌
  - No `<link rel="canonical">` ❌
  - No og: tags ❌
  - No twitter: tags ❌
  - Title: "DocFast API Documentation" (inconsistent format — others use `Page — DocFast` pattern)
- **Impact:** Poor search appearance for a high-value SEO target (developers searching for PDF API docs)
- **Fix:** Add standard meta tags to the `/docs` HTML template

### BUG-060: Sub-pages Missing Open Graph / Twitter Meta Tags
- **Severity:** LOW
- **Affected:** `/impressum`, `/privacy`, `/terms`, `/status`
- **Issue:** These pages have `<title>`, `<meta description>`, and `<canonical>` but no og: or twitter: tags
- **Impact:** Unfriendly link previews when shared on social media
- **Fix:** Add at minimum `og:title`, `og:description`, `og:url` to each sub-page template

### BUG-061: `/status` Page Not Included in Sitemap
- **Severity:** INFO
- **URL:** `https://docfast.dev/sitemap.xml`
- **Issue:** The sitemap includes `/`, `/docs`, `/impressum`, `/privacy`, `/terms` but omits `/status`
- **Impact:** Minor — status pages are generally low SEO priority, but consistency is good
- **Fix:** Add `/status` to sitemap with `changefreq=always` and low priority (0.2)

---

## 🟠 ACCESSIBILITY AUDIT

### BUG-062: `<main>` Element Only Wraps Hero Section
- **Severity:** HIGH
- **Location:** `https://docfast.dev/` (lines 283–309)
- **Issue:** The `<main>` landmark element only wraps the hero (headline + CTA + code block). The trust stats, EU hosting badge, features grid, and pricing section are all **outside** `<main>` — they're bare `<section>` elements after `</main>`
- **Impact:** Screen reader users using "jump to main content" will skip to the hero and miss the features/pricing. WCAG 1.3.6 landmark best practice violation.
- **Fix:** Extend `<main>` to wrap all page content between nav and footer

### BUG-063: Heading Hierarchy Skip — h3 Without Parent h2
- **Severity:** MEDIUM
- **Location:** "Hosted in the EU" section (homepage)
- **Issue:** Page structure: `<h1>` (hero) → `<h3>` "Hosted in the EU" → `<h2>` "Everything you need". The h3 appears before the first h2, violating sequential heading order (WCAG 1.3.1, Success Criterion: Info and Relationships)
- **Impact:** Screen readers announce heading levels; skipping from h1 to h3 confuses navigation
- **Fix:** Change the "Hosted in the EU" heading from `<h3>` to `<h2>`, or restructure so it follows an h2

### BUG-064: Modal Form Inputs Have No `<label>` Elements
- **Severity:** HIGH
- **Location:** All three modals (Sign Up, Recover API Key, Change Email)
- **Issue:** Every `<input>` in the modals uses only `placeholder` text for labeling. There are no `<label for="...">` elements and no `aria-label` or `aria-labelledby` attributes on the inputs
- **Impact:** WCAG 1.3.1, 3.3.2 violations. Screen readers will announce inputs with only their placeholder (which vanishes on focus/input), making the form inaccessible for blind users
- **Fix:** Add `<label>` elements for each input, or add `aria-label` attributes:
  ```html
  <label for="signupEmail" class="sr-only">Email address</label>
  <input type="email" id="signupEmail" ...>
  ```

### BUG-065: Modal Close Buttons Have No Accessible Label
- **Severity:** MEDIUM
- **Location:** All three modal close buttons (`id="btn-close-signup"`, `btn-close-recover`, `btn-close-email-change`)
- **Issue:** Buttons render `×` (HTML entity `&times;`) with no `aria-label` attribute
- **Impact:** Screen readers will announce "times" or "multiplication sign" instead of "Close". WCAG 4.1.2 violation.
- **Fix:** Add `aria-label="Close dialog"` to each close button

### BUG-066: Modals Missing `aria-modal="true"`
- **Severity:** MEDIUM
- **Location:** `#signupModal`, `#recoverModal`, `#emailChangeModal`
- **Issue:** Modal divs have `role="dialog"` but lack `aria-modal="true"`. Without `aria-modal`, screen readers (especially NVDA/JAWS in browsing mode) may continue reading background content
- **Impact:** Background page content is announced while modal is open
- **Fix:** Add `aria-modal="true"` to all three modal overlay divs

### BUG-067: No Skip-to-Content Link
- **Severity:** LOW
- **Issue:** There is no "Skip to main content" link at the top of any page. Keyboard users must tab through the entire navigation on every page load to reach the main content
- **Impact:** WCAG 2.4.1 (Bypass Blocks) — Level A violation. Particularly problematic since there is a 3-item nav on every page.
- **Fix:** Add as first element in `<body>`:
  ```html
  <a href="#main-content" class="skip-link">Skip to main content</a>
  ```

---

## 🔵 CROSS-PAGE CONSISTENCY AUDIT

### BUG-068: "Change Email" Footer Link Broken on Sub-pages
- **Severity:** MEDIUM
- **Affected:** `/impressum`, `/privacy`, `/terms`, `/status`
- **Issue:** The footer "Change Email" link (`href="/#change-email"` + `class="open-email-change"`) works on the **homepage** because `app.js` intercepts the click and opens the modal. On sub-pages, `app.js` is **not loaded**, so clicking navigates to `https://docfast.dev/#change-email`. The homepage `app.js` DOMContentLoaded handler does not check `window.location.hash` on load, so the modal never auto-opens.
- **Note:** BUG-048 (previous session) added the `class="open-email-change"` to these links, but the incomplete fix only works on the homepage itself. Sub-page navigation to `/#change-email` falls through silently.
- **Impact:** Users on sub-pages who want to change their email get taken to the homepage with no modal — confusing dead end
- **Fix:** Either (a) add hash detection in app.js DOMContentLoaded: `if (window.location.hash === '#change-email') openEmailChange();`, or (b) add a standalone mini-script to sub-pages that handles the click and redirects with a proper action param

### BUG-069: `/docs` Page Uses Different Navigation Pattern
- **Severity:** INFO
- **URL:** `https://docfast.dev/docs`
- **Issue:** The docs page uses a custom Swagger UI top bar with "← Back to docfast.dev" instead of the main site nav (Features, Pricing, Docs links). Footer is also absent.
- **Note:** This is intentional for SwaggerUI but means the docs page is an isolated island with no footer navigation and no way to reach /status, /impressum, /privacy, or /terms from docs
- **Impact:** Low — power users know to use the browser back button; but /impressum and /privacy should be reachable from all pages (legal requirement in some jurisdictions)
- **Fix:** Add a minimal footer to the /docs page with legal links (Impressum, Privacy, Terms)

---

## ✅ ITEMS CONFIRMED WORKING

| Check | Status |
|-------|--------|
| HTTPS enforced (HSTS header) | ✅ OK |
| Gzip compression active | ✅ OK |
| Cache-Control for HTML (24h) | ✅ OK |
| ETag + Last-Modified on HTML | ✅ OK |
| og:image exists (40KB PNG) | ✅ OK |
| robots.txt present and correct | ✅ OK |
| Canonical URLs on all pages | ✅ OK |
| Security headers (CSP, CORP, etc.) | ✅ OK |
| Nav consistent across all pages | ✅ OK |
| Footer consistent across all pages | ✅ OK |
| External links use target=_blank + rel=noopener | ✅ OK |
| Mobile layout at 375px (iPhone X) | ✅ OK |
| Focus-visible styles defined | ✅ OK |
| ARIA labels on nav, footer, modals | ✅ OK |
| Decorative icons have aria-hidden | ✅ OK |
| Modal focus trapping (JS) | ✅ OK |
| Modal Escape-key-to-close (JS) | ✅ OK |
| /status page working with live data | ✅ OK |
| /health returns JSON with DB status | ✅ OK |
| All 6 pages load HTTP 200 | ✅ OK |

---

## Summary Table

| Bug ID | Severity | Area | Title |
|--------|----------|------|-------|
| BUG-051 | MEDIUM | Performance | Duplicate HTTP headers (Vary, nosniff) |
| BUG-052 | MEDIUM | Performance | Duplicate Cache-Control on static assets |
| BUG-053 | LOW | Performance | JS files not minified |
| BUG-054 | LOW | Performance | No Brotli compression |
| BUG-055 | LOW | Performance | Duplicate preconnect link tags in HTML |
| BUG-056 | HIGH | SEO | Sitemap namespace URL typo (sitemapns.org) |
| BUG-057 | MEDIUM | SEO | JSON-LD Pro plan stale (5,000 vs 10,000) |
| BUG-058 | LOW | SEO | twitter:image missing on homepage |
| BUG-059 | MEDIUM | SEO | /docs page missing all SEO meta tags |
| BUG-060 | LOW | SEO | Sub-pages missing og:/twitter: tags |
| BUG-061 | INFO | SEO | /status not in sitemap |
| BUG-062 | HIGH | Accessibility | `<main>` wraps only hero section |
| BUG-063 | MEDIUM | Accessibility | h3 before h2 (heading skip) |
| BUG-064 | HIGH | Accessibility | Modal inputs have no `<label>` elements |
| BUG-065 | MEDIUM | Accessibility | Close buttons have no aria-label |
| BUG-066 | MEDIUM | Accessibility | Modals missing aria-modal="true" |
| BUG-067 | LOW | Accessibility | No skip-to-content link |
| BUG-068 | MEDIUM | Consistency | Change Email footer broken on sub-pages |
| BUG-069 | INFO | Consistency | /docs has no footer with legal links |

**Total: 19 new findings** — 0 CRITICAL, 1 HIGH (SEO), 2 HIGH (A11y), 6 MEDIUM, 5 LOW, 2 INFO


## BUG-071: Support Agent Leaked API Key via Social Engineering — CRITICAL SECURITY INCIDENT
- **Date:** 2026-02-17
- **Severity:** CRITICAL
- **What happened:** Support agent (Franz Hubert) retrieved API key `df_free_87aa...100d` from database and sent it in plaintext to office@cloonar.com (ticket #370). The requester claimed to be dominik.polakovics@cloonar.com but was emailing from a DIFFERENT address. Classic social engineering attack.
- **Impact:** Third party obtained a user's API key
- **Response:**
  1. Compromised key rotated immediately — old key invalidated, new key generated
  2. Container restarted to reload key cache
  3. Support agent prompt hardened with explicit security rules (boxed, emphasized, real-world warning)
  4. Removed ALL database access guidance from support agent prompt
  5. Added escalation-only flow for key recovery issues
- **Status:** RESOLVED (key rotated, prompt hardened)
- **Prevention:** Support agent now has zero ability to retrieve keys; can only direct to website recovery or escalate to human

## BUG-081: Stripe webhook can create duplicate Pro keys across pods
- **Date:** 2026-02-20
- **Severity:** MEDIUM
- **Issue:** `api_keys.stripe_customer_id` has no UNIQUE constraint. With 2 replicas and in-memory cache, a webhook retry hitting a different pod can INSERT a duplicate key for the same Stripe customer.
- **Root cause:** `keysCache` is in-memory per-pod, never synced. `createProKey` checks cache before INSERT, but pod B doesn't have pod A's newly created key in cache.
- **Impact:** Customer could end up with 2 Pro API keys (both work). Low probability but real data integrity issue.
- **Fix:** Add partial UNIQUE index on `stripe_customer_id WHERE NOT NULL`, use ON CONFLICT UPSERT in createProKey.
- **Status:** ✅ FIXED — UNIQUE index on stripe_customer_id, UPSERT in createProKey, DB lookup for success page dedup

## BUG-082: CI promote workflow uses stale `latest` image
- **Date:** 2026-02-20
- **Severity:** MEDIUM
- **Issue:** `promote.yml` pulls `latest` and retags for production. If tag event fires before staging build completes, `latest` is stale. Causes version mismatches.
- **Root cause:** Race between staging build (push to main) and production promote (tag event)
- **Impact:** Production deploys with old code despite new tag
- **Fix:** Use commit SHA image from staging build instead of `latest`, with retry if not yet available
- **Status:** ✅ FIXED — promote.yml now uses commit SHA with 10-min retry loop

## BUG-083: Copy button on API key recovery broken — CSP blocks inline onclick
- **Date:** 2026-02-21
- **Severity:** HIGH
- **Issue:** The Copy button in the API key recovery success modal uses `onclick="copyRecoveredKey()"` inline handler, but CSP header has `script-src 'self'` which blocks all inline scripts/handlers.
- **Impact:** Users who recover their API key cannot copy it using the Copy button. They must manually select and copy the key text.
- **Fix:** Remove inline onclick, add addEventListener in app.js instead.
- **Status:** ✅ FIXED — commit 4aeac95. All inline onclick handlers replaced with external JS (copy-helper.js for server-rendered pages, addEventListener in app.js for templates).

## BUG-084: Dual HTML build system — templates/ changes don't deploy
- **Date:** 2026-02-21
- **Severity:** HIGH (process issue)
- **Issue:** Project has two build systems:
  1. `npm run build:pages` — builds from `templates/pages/` → `public/`
  2. `scripts/build-html.cjs` (Dockerfile) — builds from `public/src/` + `public/partials/` → `public/`
  The Docker build runs last and overwrites. Changes to `templates/pages/` never reach production.
- **Impact:** Session 73 fixes (FAQ schema, h2 heading) were applied to wrong source files and never deployed despite being committed.
- **Root cause:** Build system confusion — two different templating approaches coexist
- **Fix needed (immediate):** Apply fixes to `public/src/index.html` and `public/partials/` instead
- **Fix needed (long-term):** Consolidate to ONE build system. Either remove `templates/` or remove `public/src/`.
- **Status:** ✅ FIXED (FULLY) — commit b476b0b (immediate fix) + commit ca72f04 (long-term: deleted templates/pages/ and build-pages.js, consolidated to single build-html.cjs system).

## BUG-085: Examples page uses non-existent api.docfast.dev subdomain
- **Date:** 2026-02-23
- **Severity:** HIGH
- **Issue:** Node.js, Python, Go, PHP examples on /examples page reference `https://api.docfast.dev/v1/convert/html` and `https://api.docfast.dev/v1/convert/markdown`. The `api.docfast.dev` subdomain does not exist — correct URL is `https://docfast.dev/v1/...`. Developers following examples will get DNS errors.
- **Affected:** staging.docfast.dev/examples (and production once deployed)
- **Fix:** Replace all `api.docfast.dev` with `docfast.dev` in `public/src/examples.html`
- **Status:** ✅ FIXED (commit 1c0c8a3) — all api.docfast.dev → docfast.dev

## BUG-086: Examples page references non-existent SDK packages
- **Date:** 2026-02-23
- **Severity:** MEDIUM
- **Issue:** Go example says `go get github.com/docfast/docfast-go` (404 on GitHub). PHP example says `composer require docfast/docfast-php` (doesn't exist on Packagist). SDKs haven't been published yet — blocked on npm/PyPI/Go/Packagist tokens.
- **Fix:** Either publish SDKs or change examples to use plain HTTP (curl/fetch) without SDK install instructions until packages are published.
- **Status:** ✅ FIXED (commit 1c0c8a3) — replaced with "SDK coming soon" notes, kept HTTP examples

## BUG-091: Demo endpoint accepts invalid scale value (scale:99) without error
- **Date:** 2026-03-01
- **Severity:** MEDIUM
- **Issue:** `POST /v1/demo/html` with `{"html":"<h1>test</h1>","options":{"scale":99}}` returns HTTP 200 and generates a PDF instead of returning 400 validation error. Chromium's valid scale range is 0.1–2.0. Scale of 99 should be rejected.
- **Steps to reproduce:** `curl -X POST https://staging.docfast.dev/v1/demo/html -H 'Content-Type: application/json' -d '{"html":"<h1>test</h1>","options":{"scale":99}}'`
- **Expected:** 400 error with validation message about invalid scale
- **Actual:** 200 with a PDF generated (Chromium may silently clamp or ignore)
- **Status:** ✅ FALSE POSITIVE — API uses top-level params, not nested `options` object. `scale:99` at top level IS correctly validated. Confirmed session 117/118.

## BUG-092: Footer missing "Change Email" link
- **Date:** 2026-03-01
- **Severity:** LOW
- **Issue:** The task spec expects a "Change Email" link in the footer, but it's not present. The change email modal (`emailChangeModal`) exists in the DOM but there's no footer link to trigger it. Users have no discoverable way to access the email change feature from the landing page.
- **Steps to reproduce:** Visit https://staging.docfast.dev/ → scroll to footer → no "Change Email" link
- **Expected:** Footer should include a "Change Email" link
- **Actual:** Footer has: Home, Docs, Examples, API Status, Support, Impressum, Privacy Policy, Terms of Service — no Change Email
- **Status:** OPEN

## BUG-093: Signup modal removed but "Get Pro API Key" button text is misleading
- **Date:** 2026-03-01
- **Severity:** LOW
- **Issue:** Free signup was discontinued (API returns 410). The signup modal (`signupModal`) has been removed from the DOM. However, the "Get Pro API Key — €9/mo" button now redirects directly to Stripe checkout, which is correct behavior. Not a bug per se, but the old "signup modal" test case is no longer applicable — documenting for awareness.
- **Status:** NOT A BUG — by design after free tier removal

## BUG-094: email-change endpoint returns unhelpful error without auth
- **Date:** 2026-03-01
- **Severity:** LOW
- **Issue:** `POST /v1/email-change` without authentication returns `{"error":"apiKey is required."}` with HTTP 400. Should return 401 for consistency with other auth-required endpoints (`/v1/convert/html` and `/v1/usage` return 401).
- **Steps to reproduce:** `curl -X POST https://staging.docfast.dev/v1/email-change -H 'Content-Type: application/json' -d '{"current_email":"a@b.com","new_email":"c@d.com"}'`
- **Expected:** 401 with consistent "Missing API key" error message
- **Actual:** 400 with "apiKey is required."
- **Status:** ✅ WON'T FIX — email-change uses body `apiKey` param (not header auth), so 400 for missing body field is correct. Different auth pattern from header-auth endpoints.
