From 345e79a9c35f064eceb9c6e8db0046da74c5f19d Mon Sep 17 00:00:00 2001 From: Nish <257724087+nish3451@users.noreply.github.com> Date: Fri, 14 Aug 2026 17:32:24 +0530 Subject: [PATCH 1/2] fix(public): net the live site against WCAG 2.2 tap-target drift without blocking PRs PR #22 (footer) and #25 (in-content) brought every link in public/styles.css up to the WCAG 2.2 SC 2.5.8 24px minimum, and scripts/test-public-link-targets.mjs guards the worktree sheet, but the backlog item keeps re-opening because the live site still serves the pre-fix stylesheet (June-20 bundle), so rendered in-content links are ~17px. PR #66 wired a live guard into the blocking npm test/ci chain, but PR #84 established that the blocking chains must stay green on repo state alone. Land the detector the same way #84 landed the soft-404 net: - scripts/check-public-live-tap-targets.mjs: fetches the deployed https://tinystudio.in/styles.css and re-asserts the five link rules the local suite requires (.top-nav a, .plain-list a, .product-links a, .rail-item strong a, .footer-links a: block-level box, min-height 24px, >= 4px vertical padding). A non-2xx response or a stylesheet that misses a rule fails loudly (the CodeRabbit P1 on #66); only a network-level failure skips, and SKIP_LIVE_CHECKS=1 is honored like the sibling live checks. - npm run site:check-live entry point now runs both live checks. - .github/workflows/live-site-check.yml gains a nightly + manual-dispatch live-tap-targets job. Verify: node --check clean; against the current live stylesheet the check reports 11 failures and exits 1 (the point of the net: public/ is compliant, the deployment is not - the red run is the signal to re-deploy from origin/main); npm test stays green (blocking chain untouched); git diff --check clean. Co-authored-by: CommandCodeBot --- .github/workflows/live-site-check.yml | 27 ++-- package.json | 2 +- scripts/check-public-live-tap-targets.mjs | 142 ++++++++++++++++++++++ 3 files changed, 162 insertions(+), 9 deletions(-) create mode 100644 scripts/check-public-live-tap-targets.mjs diff --git a/.github/workflows/live-site-check.yml b/.github/workflows/live-site-check.yml index 40c5d581..ac4b0178 100644 --- a/.github/workflows/live-site-check.yml +++ b/.github/workflows/live-site-check.yml @@ -1,11 +1,12 @@ name: Live Site Check -# Guards the deployed tinystudio.in against soft-404s and stale bundles -# without blocking pull requests. The deploy lane (deploy-public-site.yml) -# verifies the live site after every publish; this workflow is the nightly -# net that catches a stale or misconfigured deployment within a day even -# when nothing merges. It is deliberately NOT part of `npm run ci` - that -# blocking chain must stay green on repo state alone. +# Guards the deployed tinystudio.in against soft-404s, WCAG 2.2 tap-target +# drift (SC 2.5.8 24px minimum), and stale bundles without blocking pull +# requests. The deploy lane (deploy-public-site.yml) verifies the live site +# after every publish; this workflow is the nightly net that catches a stale +# or misconfigured deployment within a day even when nothing merges. It is +# deliberately NOT part of `npm run ci` - that blocking chain must stay green +# on repo state alone. # # This job is expected to FAIL while the deployment is stale (currently the # June-20 bundle); the red run is the loud signal to re-deploy from @@ -13,8 +14,8 @@ name: Live Site Check on: schedule: - # Nightly: catch stale or broken deployments (soft-404s, offline site) - # within a day even when no pull requests are open. + # Nightly: catch stale or broken deployments (soft-404s, sub-24px link + # targets, offline site) within a day even when no pull requests are open. - cron: "23 3 * * *" workflow_dispatch: @@ -46,3 +47,13 @@ jobs: cache: npm - run: npm ci --ignore-scripts - run: node scripts/test-public-live-contact-heading-hierarchy.mjs + live-tap-targets: + runs-on: [self-hosted, linux, x64, vps-verify] + steps: + - uses: actions/checkout@v7 + - uses: actions/setup-node@v7 + with: + node-version: 20 + cache: npm + - run: npm ci --ignore-scripts + - run: node scripts/check-public-live-tap-targets.mjs diff --git a/package.json b/package.json index c9b7c4d8..0dd31085 100644 --- a/package.json +++ b/package.json @@ -14,7 +14,7 @@ "client:proof-review": "node scripts/review-client-proof.mjs", "retention:automation-check": "node scripts/check-retention-automation.mjs", "site:prepare": "node scripts/prepare-static-site-bundle.mjs", - "site:check-live": "node scripts/check-public-live-soft-404.mjs", + "site:check-live": "node scripts/check-public-live-soft-404.mjs && node scripts/check-public-live-tap-targets.mjs", "site:check-live-contact-heading-hierarchy": "node scripts/test-public-live-contact-heading-hierarchy.mjs", "claims:check": "node scripts/check-outbound-claim-safety.mjs", "config:check": "node scripts/check-agency-defaults.mjs", diff --git a/scripts/check-public-live-tap-targets.mjs b/scripts/check-public-live-tap-targets.mjs new file mode 100644 index 00000000..cffa3008 --- /dev/null +++ b/scripts/check-public-live-tap-targets.mjs @@ -0,0 +1,142 @@ +// Guard the LIVE public site against WCAG 2.2 (SC 2.5.8) tap-target drift: +// the stylesheet tinystudio.in actually serves must keep every in-content +// and footer link rule at the 24px minimum. +// +// The static test (test-public-link-targets.mjs) only proves +// public/styles.css in the repo; it cannot catch a stale or misconfigured +// deployment. This check fetches the deployed stylesheet and re-asserts the +// same five link rules the local suite requires (.top-nav a, .plain-list a, +// .product-links a, .rail-item strong a, .footer-links a: block-level box, +// min-height 24px, >= 4px vertical padding), so a deployment that still +// serves pre-fix CSS (the June-20 bundle) fails loudly instead of silently +// re-opening the tap-target backlog item. It runs as part of +// `npm run site:check-live` and from the nightly live-site-check workflow. +// +// It is deliberately NOT part of `npm run test` / `npm run ci`: those +// blocking chains must stay green on repo state alone (see PR #84), while +// the live site is deployed by an external mechanism (Cloudflare Pages). +// Blocking CI on the live site would keep every pull request red whenever +// the deployment is stale. +// +// Failure semantics: a non-2xx HTTP response and a stylesheet that misses +// any required rule both FAIL this check (a deployment serving no rules is +// a failed deployment, not an unknown). Only a genuine network-level +// failure (site unreachable) skips with a notice, so offline machines do +// not go red; the nightly live-site-check workflow still catches the +// same outage via check-public-live-soft-404.mjs. +// +// Escape hatch for machines without network access: +// SKIP_LIVE_CHECKS=1 npm run site:check-live + +import { readFileSync } from "node:fs" +import { fileURLToPath } from "node:url" +import { dirname, join } from "node:path" + +if (process.env.SKIP_LIVE_CHECKS === "1") { + console.log("check-public-live-tap-targets: SKIP_LIVE_CHECKS=1, skipping live site checks") + process.exit(0) +} + +const ROOT = join(dirname(fileURLToPath(import.meta.url)), "..") +const read = (p) => readFileSync(join(ROOT, p), "utf8") + +const LIVE_CSS_URL = "https://tinystudio.in/styles.css" +const FETCH_TIMEOUT_MS = 10_000 +const FETCH_ATTEMPTS = 2 + +let failures = 0 +let checks = 0 +const ok = (cond, msg) => { + checks++ + if (cond) console.log(` ok ${msg}`) + else { + failures++ + console.error(` FAIL ${msg}`) + } +} + +// Same selectors test-public-link-targets.mjs requires of the local sheet. +const TAP_TARGET_SELECTORS = [ + ".top-nav a", + ".plain-list a", + ".product-links a", + ".rail-item strong a", + ".footer-links a" +] + +const targetRuleOf = (css, selector) => { + const esc = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&") + const pattern = selector + .split(",") + .map((s) => esc(s.trim())) + .join("\\s*,\\s*") + const match = css.match(new RegExp(`${pattern}\\s*\\{([^}]*)\\}`)) + return match ? match[1] : null +} + +const assert24pxRule = (selector, rule) => { + ok(rule !== null, `live styles.css has a ${selector} rule`) + if (rule) { + ok(/display:\s*(inline-block|inline-flex|block)/.test(rule), `live ${selector} links are block-level boxes (hit area covers the line box)`) + ok(!/display:\s*inline\s*;/.test(rule), `live ${selector} links are not plain inline boxes`) + ok(/min-height:\s*24px/.test(rule), `live ${selector} links declare min-height: 24px`) + const paddingMatch = rule.match(/padding:\s*([^;]+)/) + ok(paddingMatch !== null, `live ${selector} links declare vertical padding`) + if (paddingMatch) { + const vertical = parseFloat(paddingMatch[1].trim().split(/\s+/)[0]) + ok(vertical >= 4, `live ${selector} vertical padding is at least 4px (${vertical}px), so 16px text + padding >= 24px`) + } + } +} + +console.log("check-public-live-tap-targets: the deployed tinystudio.in stylesheet keeps every link rule at the WCAG 2.2 24px minimum") + +let css = null +try { + for (let attempt = 1; attempt <= FETCH_ATTEMPTS; attempt++) { + try { + const res = await fetch(LIVE_CSS_URL, { signal: AbortSignal.timeout(FETCH_TIMEOUT_MS) }) + if (!res.ok) { + failures++ + checks++ + console.error(` FAIL ${LIVE_CSS_URL} answered HTTP ${res.status}: the deployed stylesheet is missing or inaccessible, so none of the tap-target rules are being served`) + process.exit(1) + } + css = await res.text() + break + } catch (err) { + if (attempt === FETCH_ATTEMPTS) throw err + await new Promise((r) => setTimeout(r, 1000)) + } + } +} catch (err) { + console.log(` ok skipped: ${LIVE_CSS_URL} unreachable (${err?.cause?.code ?? err?.name ?? "network error"}) - no tap-target assertions run`) +} + +if (css !== null) { + console.log("A. every link rule in the deployed stylesheet enforces the 24px minimum") + for (const selector of TAP_TARGET_SELECTORS) { + assert24pxRule(selector, targetRuleOf(css, selector)) + } + if (failures === 0) { + console.log(" the deployed stylesheet carries every tap-target rule from public/styles.css") + } else { + console.log(" the deployed stylesheet is stale: it misses tap-target rules that public/styles.css already has (see FAIL lines above). Refresh the live deployment from origin/main.") + } +} + +console.log("B. wiring") +const pkg = JSON.parse(read("package.json")) +ok( + (pkg.scripts["site:check-live"] ?? "").includes("check-public-live-soft-404.mjs") && + (pkg.scripts["site:check-live"] ?? "").includes("check-public-live-tap-targets.mjs"), + "npm run site:check-live runs the soft-404 and tap-target live checks" +) +const workflow = read(".github/workflows/live-site-check.yml") +ok(workflow.includes("check-public-live-tap-targets.mjs"), "live-site-check.yml runs the live tap-target check (nightly + manual dispatch)") + +console.log(`\n${checks} checks, ${failures} failures`) +if (failures > 0) { + console.error("\nThe deployed stylesheet drifted below the WCAG 2.2 24px tap-target minimum. Re-deploy the public site from origin/main and re-run this check.") +} +process.exit(failures === 0 ? 0 : 1) From 272cd262915811d6c8d73c4cddd08f2d2e271159 Mon Sep 17 00:00:00 2001 From: Nish <257724087+nish3451@users.noreply.github.com> Date: Fri, 14 Aug 2026 17:32:54 +0530 Subject: [PATCH 2/2] =?UTF-8?q?docs(lane1):=20report=20the=20live=20tap-ta?= =?UTF-8?q?rget=20net=20=E2=80=94=20PR=20#152?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: CommandCodeBot --- .../fix/lane1-public-tap-targets-live-net.md | 41 +++++++++++++++++++ 1 file changed, 41 insertions(+) create mode 100644 .lane/reports/fix/lane1-public-tap-targets-live-net.md diff --git a/.lane/reports/fix/lane1-public-tap-targets-live-net.md b/.lane/reports/fix/lane1-public-tap-targets-live-net.md new file mode 100644 index 00000000..ce3164e8 --- /dev/null +++ b/.lane/reports/fix/lane1-public-tap-targets-live-net.md @@ -0,0 +1,41 @@ +# Lane report: fix/lane1-public-tap-targets-live-net + +## Item + +In-content links stay below the WCAG 2.2 24px tap-target minimum — merged PR #22 fixes only. + +## Root cause (verified live, 2026-08-14) + +The tap-target fix is correct **in source** and merged on main twice: + +- PR #22 (`5298652`) — footer links ≥24px +- PR #25 (`b0f5f06`) — in-content links ≥24px +- `scripts/test-public-link-targets.mjs` (static guard) → **77 checks, 0 failures** on origin/main + +The live site does not carry the fix. `https://tinystudio.in/styles.css` still serves the pre-fix June-20 bundle, which is missing all five tap-target rules — rendered in-content links are ~17px. This is the same stale-deployment gap documented in the soft-404 lane (no Cloudflare Pages token → release lane fails closed, nightly `Live Site Check` red by design). + +A previous lane attempt (PR #107, `fix/lane1-in-content-link-targets-live-net-20260812`) landed the same change shape but is **CONFLICTING** against current main (which now has a third live-check job) and never merged. This lane re-lands it fresh, rebased on origin/main. + +## What I changed + +| File | Change | +|---|---| +| `scripts/check-public-live-tap-targets.mjs` | New live guard: fetches deployed `styles.css`, re-asserts the five link rules the local suite requires (block-level box, `min-height: 24px`, ≥4px vertical padding). Non-2xx or missing rule = loud FAIL; network failure only = skip; `SKIP_LIVE_CHECKS=1` honored. | +| `package.json` | `site:check-live` now runs soft-404 + tap-target checks. Not added to `npm test`/`npm ci` (blocking chains stay green on repo state alone, per PR #84). | +| `.github/workflows/live-site-check.yml` | New nightly + manual-dispatch `live-tap-targets` job. | + +## Evidence + +- `node --check scripts/check-public-live-tap-targets.mjs` → clean +- `node scripts/check-public-live-tap-targets.mjs` against the live site → **19 checks, 11 failures, exit 1** (reproduces the item: deployed sheet is stale, sub-24px targets) +- `node scripts/test-public-link-targets.mjs` → 77 checks, 0 failures (repo correct) +- `node scripts/test-public-soft-404.mjs` → 19 checks, 0 failures +- `node scripts/test-deploy-public-site-workflow.mjs` → 10 checks, 0 failures +- `node scripts/test-pages-release.mjs` → 38 checks, 0 failures +- `git diff --check` → clean + +## Deliverable + +PR: https://github.com/nish3451/tinystudio-in/pull/152 (open) + +The red nightly run is the intended staleness alarm, not a reason to disable. Once the Cloudflare Pages token is provisioned (release-lane blocker, documented in `deploy-public-site.yml`), re-deploy from origin/main; this check must then report 0 failures and the item closes.