diff --git a/docs/audit/2026-05-12-deep-audit.md b/docs/audit/2026-05-12-deep-audit.md index b4972a44f..b23e4bf8a 100644 --- a/docs/audit/2026-05-12-deep-audit.md +++ b/docs/audit/2026-05-12-deep-audit.md @@ -45,7 +45,7 @@ Verified-fine on direct inspection (rejected from this audit): |---|------|---------|-----------|----| | 4 | HIGH | Marketing contact form: server action emits structured error codes (`rate_limit`, `invalid_email`, `1`) but UI maps them all to a single "Something went wrong" message. User cannot tell rate-limit from validation from server error. | `app/(marketing)/contact/ContactPageContentNew.tsx:681-692` vs `app/(marketing)/contact/actions.ts:29,53,58,85` | #50 | | 5 | MED | `app/app/settings/email-preferences/page.tsx:185` does `if (error) throw error;` inside a handler — re-scoped on inspection: the try/catch does swallow the throw; the real bug is the generic "Failed to save preferences" pill showing for every error mode. Now appends the actual `error.message` to the user-facing pill, truncated. | `app/app/settings/email-preferences/page.tsx:183-200` | #63 | -| 6 | MED | `app/admin/components/add-note-form.tsx:44` calls `window.location.reload()` after a successful note insert. Destroys all client state and flashes a full reload. Should optimistic-add + toast. | `app/admin/components/add-note-form.tsx:31-50` | — | +| 6 | MED | `app/admin/components/add-note-form.tsx:44` calls `window.location.reload()` after a successful note insert. Destroys all client state and flashes a full reload. Should optimistic-add + toast. Re-scoped to Option B per session guidance: `router.refresh()` + existing compliance-toast primitive extended with `duration: 0` opt-out so the error toast stays sticky until the admin manually dismisses it. | `app/admin/components/add-note-form.tsx:31-50` | #66 | | 7 | MED | MFA challenge form fallback error is "We could not verify that code. Please try again." for every failure mode (expired token, network, rate-limit). Server side has the codes; UI doesn't surface them. Switch now covers all six route codes (rate_limited, invalid_token, invalid_token_format, invalid_body, unauthorized, mfa_verify_failed). | `app/auth/mfa-challenge/MfaChallengeForm.tsx:34-95` | #64 | | 8 | LOW | `app/join/page.tsx:80-103` catches everything with "An unexpected error occurred." The `/api/auth/bootstrap` call returns useful error bodies that aren't surfaced. | `app/join/page.tsx:80-103` | — | diff --git a/docs/audit/2026-05-13-marketing-audit.md b/docs/audit/2026-05-13-marketing-audit.md index b5de2d725..c36f80dd4 100644 --- a/docs/audit/2026-05-13-marketing-audit.md +++ b/docs/audit/2026-05-13-marketing-audit.md @@ -106,9 +106,9 @@ Headline issues (by impact): | # | Sev | Where | Observation | Suggested fix hint | |---|------|-------|-------------|--------------------| -| 1 | CRIT | `sitemap.xml`, `robots.txt`, JSON-LD on every page | A `SITE_URL` (or equivalent) constant is built with a trailing newline. Raw bytes of `` are `https://www.formaos.com.au\n/about`. `robots.txt` has `Sitemap: https://www.formaos.com.au\n/sitemap.xml`. Home JSON-LD: `"url":"https://www.formaos.com.au\n","logo":"https://www.formaos.com.au\n/og-image.png"`. Confirmed via `xxd` and grep across all 85 pages. | Locate the constant (likely env var `NEXT_PUBLIC_SITE_URL` or a template literal in `lib/seo` / `app/sitemap.ts` / `app/robots.ts`); strip trailing whitespace at consumption or trim at the source. One fix corrects sitemap, robots, and all JSON-LD simultaneously. | -| 2 | CRIT | `/status` | "All systems operational" banner with `0%` 7-day uptime, `0` 24h uptime, `0` 7-day checks, "No recent check data available". Monitoring data isn't reaching the page. | Either gate the green badge on real data, or surface "monitoring data unavailable" honestly. Procurement teams screenshot this. | -| 3 | HIGH | Every page footer + every JSON-LD block | Two contradictory identities on the same 82 pages: footer mailto `Formaos.team@gmail.com` (Gmail) vs JSON-LD `contactPoint.email: support@formaos.com.au`. JSON-LD says `addressLocality: Adelaide, addressRegion: SA`; visible footer + contact page say "Sydney, Australia". | Pick one address + one contact email; mirror across visible copy and JSON-LD. The Gmail address on the visible side is the trust-side risk — a Gmail support address for a SOC 2 / ISO-claiming platform is a red flag in vendor-assurance reviews. | +| 1 | CRIT | `sitemap.xml`, `robots.txt`, JSON-LD on every page | A `SITE_URL` (or equivalent) constant is built with a trailing newline. Raw bytes of `` are `https://www.formaos.com.au\n/about`. `robots.txt` has `Sitemap: https://www.formaos.com.au\n/sitemap.xml`. Home JSON-LD: `"url":"https://www.formaos.com.au\n","logo":"https://www.formaos.com.au\n/og-image.png"`. Confirmed via `xxd` and grep across all 85 pages. **Shipped in #67** — `.trim()` at `config/brand.ts:siteUrl/appUrl`, one fix for sitemap+robots+JSON-LD. | Locate the constant (likely env var `NEXT_PUBLIC_SITE_URL` or a template literal in `lib/seo` / `app/sitemap.ts` / `app/robots.ts`); strip trailing whitespace at consumption or trim at the source. One fix corrects sitemap, robots, and all JSON-LD simultaneously. | +| 2 | CRIT | `/status` | "All systems operational" banner with `0%` 7-day uptime, `0` 24h uptime, `0` 7-day checks, "No recent check data available". Monitoring data isn't reaching the page. **Shipped in #68** — page deleted, sitemap entry removed, footer pulse pill removed, 307 redirect added, four trust-page references and the /legal index card all stripped. The cron + public-uptime data pipeline + DB tables were intentionally left in place per directive (used by `app/api/trust-packet/vendor`). | Either gate the green badge on real data, or surface "monitoring data unavailable" honestly. Procurement teams screenshot this. | +| 3 | HIGH | Every page footer + every JSON-LD block | Two contradictory identities on the same 82 pages: footer mailto `Formaos.team@gmail.com` (Gmail) vs JSON-LD `contactPoint.email: support@formaos.com.au`. JSON-LD says `addressLocality: Adelaide, addressRegion: SA`; visible footer + contact page say "Sydney, Australia". **Shipped in #70** — `config/brand.ts` now holds canonical `contactEmail` + `address`; Footer reads from there; 9 other marketing files had the Gmail literal replaced with `support@formaos.com.au`. | Pick one address + one contact email; mirror across visible copy and JSON-LD. The Gmail address on the visible side is the trust-side risk — a Gmail support address for a SOC 2 / ISO-claiming platform is a red flag in vendor-assurance reviews. | | 4 | HIGH | `/security`, `/trust`, all industry pages | 18+ frameworks listed (SOC 2, ISO 27001, GDPR, HIPAA, PCI-DSS, NIST CSF, CIS Controls, etc.) with no certification-status qualifier — no "certified", "in progress", "supported", or "ready" anywhere on the public surface. Enterprise buyers reading "SOC 2" assume Type II certification. | Add a single status taxonomy ("Certified" / "Audit-ready" / "Supported framework") and apply it everywhere a framework name appears in trust context. | | 5 | HIGH | 67 of 85 pages | Two `

` elements rendered per page with different Tailwind class signatures (`text-[2.35rem] ...` and `text-[2.5rem] ...`). Most visible on `/contact` and `/blog` where both `

`s carry the same text. Looks like the marketing layout's hero heading and the page-level heading both render as `h1`. A11y violation and SEO confusion. | Demote one to `h2` (probably the layout-level hero subtitle, or wrap it in a `role="banner"` block with `h1` only for the page-specific heading). | | 6 | HIGH | `/features` | Hero claim says "18 features across compliance, automation, security, and collaboration." Category counts immediately below sum to 5+6+4+7+3 = **25**, not 18. | Fix the hero number or the category counts; they should match. | @@ -116,7 +116,7 @@ Headline issues (by impact): | 8 | HIGH | `/security`, `/trust`, every industry page | "Trusted by" claims and feature claims appear without any third-party verification anchor. `/customer-stories` is honest ("Anonymized scenarios"), but the home page implies real trust. | Either name a small number of pilot customers (with permission) or remove ambiguous "trusted by" framing. | | 9 | MED | Whole site | SEO URL cannibalization: 3 healthcare pages (`/healthcare-compliance`, `/healthcare-compliance-platform`, `/use-cases/healthcare`), 3 NDIS pages (`/ndis-providers`, `/ndis-compliance-system`, `/use-cases/ndis-aged-care`), 2 financial pages (`/financial-services-compliance`, `/use-cases/financial-services`). All targeting overlapping search intents. Google will pick one and ignore the others, or split rank between them. | Consolidate to one canonical per industry; the others should `301` or carry a `rel="canonical"` to the primary. | | 10 | MED | Whole site | ~42 of 85 pages have **no `og:image`** (verified by parsing every static HTML response). Pages missing OG image include `/features`, `/enterprise`, `/integrations`, `/faq`, `/contact`, `/customer-stories`, `/documentation`, `/changelog`, `/roadmap`, all six `/use-cases/*`, all six trust subpages with the exception of `/trust/dpa`. Social previews on LinkedIn/Twitter fall back to text. | Add `opengraph-image.tsx` (or `opengraph-image.png`) to each page directory; Next.js convention will pick it up. The home, about, and blog-post directories already have one — same pattern. | -| 11 | MED | `/contact`, footer (every page) | Visible support email is `Formaos.team@gmail.com`. For the audience FormaOS is selling to (NDIS providers, AHPRA-regulated practices, AFS licensees, SOC 2 buyers), a Gmail address on the support surface materially weakens vendor-assurance reviews. Note this is consistent with JSON-LD finding #3 but worth its own line — the visible-side change is the user-facing fix. | Replace with `support@formaos.com.au` site-wide. DNS already supports it (Resend is in the subprocessor list, and JSON-LD already uses the address). | +| 11 | MED | `/contact`, footer (every page) | Visible support email is `Formaos.team@gmail.com`. For the audience FormaOS is selling to (NDIS providers, AHPRA-regulated practices, AFS licensees, SOC 2 buyers), a Gmail address on the support surface materially weakens vendor-assurance reviews. Note this is consistent with JSON-LD finding #3 but worth its own line — the visible-side change is the user-facing fix. **Shipped in #70** (rolled into the #3 fix). | Replace with `support@formaos.com.au` site-wide. DNS already supports it (Resend is in the subprocessor list, and JSON-LD already uses the address). | | 12 | MED | Many pages | Top-level page titles run **>60 characters** on at least 22 pages, including `/`, `/pricing`, `/ndis-providers`, `/healthcare-compliance`, `/healthcare-compliance-platform`, `/childcare-compliance`, `/construction-compliance`, `/audit-evidence-management`, all four `/compare/*`. Google truncates around 60. Most of these include "\| FormaOS" + a tagline that's already in the meta description. | Tighten title to ` \| FormaOS` shape; move the long-tail to the description. | | 13 | MED | Many pages | Meta description >170 characters on ~16 pages (`/`, `/pricing`, `/features`, `/compare`, `/enterprise`, `/childcare-compliance`, `/construction-compliance`, `/financial-services-compliance`, `/healthcare-compliance-platform`, `/ndis-compliance-system`, `/ndis-providers`, `/integrations`, `/roadmap`, `/what-is-a-compliance-operating-system`, plus two blog posts). Google truncates around 155–160. | Trim to ≤160 chars. | | 14 | MED | Many pages | Meta description **<100 characters** on `/status` (44), `/terms` (39), `/trust/data-handling` (74), `/trust/subprocessors` (71), `/trust/sla` (89), `/trust/dpa` (95), `/trust/incident-response` (84), `/trust/procurement` (82), `/evaluate` (84), `/legal` (99), `/our-story` (93), `/prove` (97). Short descriptions waste SERP real estate. | Expand to ~140–155 chars; trust pages especially deserve a real summary. | @@ -458,7 +458,10 @@ Pages exercised (10): Brief asked for `/compliance` and `/care`; neither exists on the live site (both return `404`). Substituted `/trust` (compliance hub) and `/healthcare-compliance` (the closest "care" surface — see §4.15). -Pricing was excluded per Phase D directive. +Pricing was excluded per Phase D directive. **Shipped in #71** — +307 redirects `/compliance` → `/trust` and `/care` → `/healthcare-compliance`. +Investigation confirmed neither route was ever built (no git log, +zero inbound refs); the 404s are a URL-gap, not a regression. Time per dimension (approx, single operator, sequential where Chrome contention required it): @@ -626,7 +629,7 @@ intent — the visible CTA label does not change to match. | # | Sev | Test | Observation | Fix hint | |---|-----|------|-------------|----------| -| 85 | HIGH | Empty submit | Clicking "Talk to Sales" with all required fields blank produces **no visible error UI**. No `[role=alert]`, no `.text-red-500`, no inline `data-error`. The browser's native `checkValidity()` returns `false` and emits a tooltip on the first invalid field (`name`), but the form does not surface a custom error state. A user who Tabs past the tooltip, or whose browser doesn't render it (assistive tech in some screen-reader configs), sees no feedback. | Add visible inline error messages tied to `aria-describedby` and `aria-invalid` on each required field. The form already declares `required` but doesn't paint the failure state. | +| 85 | HIGH | Empty submit | Clicking "Talk to Sales" with all required fields blank produces **no visible error UI**. No `[role=alert]`, no `.text-red-500`, no inline `data-error`. The browser's native `checkValidity()` returns `false` and emits a tooltip on the first invalid field (`name`), but the form does not surface a custom error state. A user who Tabs past the tooltip, or whose browser doesn't render it (assistive tech in some screen-reader configs), sees no feedback. **Shipped in #73** — `onSubmit` interception, `noValidate`, per-field `aria-invalid` + `aria-describedby` + clear-on-edit, inline `

` per field, focus-first-invalid on submit. | Add visible inline error messages tied to `aria-describedby` and `aria-invalid` on each required field. The form already declares `required` but doesn't paint the failure state. | | 86 | MED | Bad email (`not-an-email`) | Same behaviour as #85 — browser tooltip says "Please include an '@' in the email address. 'not-an-email' is missing an '@'." No custom inline error. No POST is made (validity blocks submit). | Same fix as #85; the validation already runs, the UI just doesn't render the failure state. | | 87 | LOW | Boundary (5000-char unicode message, valid email shape, emoji in name) | Form accepts the input client-side, no character-count warning, no length cap on `