docs(plan): A11y rescan — live portal authenticated routes

11-task plan для повторного a11y-аудита: extend Pa11y от 7 guest URLs к 21
URLs (включая 14 authenticated через per-URL actions login flow), + axe-core
cross-validation via Playwright MCP, + inline fixes для real prod findings.

Closes Audit #3 Phase 7 «authenticated pages out of scope» clause per
explicit user request «Pa11y был настроен на старые HTML-эскизы, проведи
повторно аудит в этой части, чтобы он проверил реальный портал».

Plan: docs/superpowers/plans/2026-05-14-a11y-rescan-live-portal.md

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Дмитрий
2026-05-14 10:06:19 +03:00
parent e2d144c928
commit d952f3ac20
@@ -0,0 +1,552 @@
# A11y Rescan — Live Portal (Authenticated + Guest) Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use `superpowers:subagent-driven-development` (recommended) or `superpowers:executing-plans` to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** Provide accurate, current a11y coverage for the entire live Vue portal (guest + authenticated routes), close any real production findings, and document the new baseline. Continuation of Audit #3 Phase 7 + deferred-fixes sprint Task 1 — closes the `out-of-scope` clause for authenticated routes.
**Architecture:** Three-pillar a11y verification — (1) Pa11y on guest pages (existing baseline, verify holds); (2) Pa11y on authenticated pages via per-URL `actions` login flow (new); (3) axe-core via Playwright MCP sweep on the same authenticated routes (cross-validation against Pa11y). Real findings are fixed inline; known temp (DevIndexBadge) and Vuetify-structural patterns are documented as ignored.
**Tech Stack:** pa11y-ci 4.1 (chromium + axe rules) + Playwright MCP (browser_navigate + browser_evaluate for axe-core CDN) + Laravel 13 dev-server (`php artisan serve :8000`) + Vue Router (createWebHistory) + Sanctum SPA cookie auth + DemoSeeder credentials `admin@demo.local:password`.
**Scope:**
- **In scope:** All 26 SPA routes registered in `app/routes/web.php` (7 guest already in baseline + 14 authenticated + 3 error views + 2 already counted).
- **Out of scope:** Static HTML handoff prototypes (already relocated to opt-in `pa11y-handoff.config.json` in deferred-fixes sprint). Mobile viewport (1440×900 desktop baseline is sufficient for first authenticated baseline).
- **Branch:** work on `main` (current 🟢 GREEN, HEAD `ae20033`). Atomic commit per logical change.
**Pre-flight assumptions to verify in Task 1:**
- Dev-server up on `http://localhost:8000` (status 200 on `/login`).
- Demo tenant + `admin@demo.local` user present (DemoSeeder runs auto in local env per commit `e746b3c` Task 2.4).
- Vite production build current (Task 1 will rebuild to be safe — quirk #81 from `feedback_environment.md`: `php artisan serve` serves built assets, not Vite HMR).
---
## Task 1 — Pre-flight + verify guest baseline holds
**Files:**
- No modifications — verification only.
### Step 1.1 — Verify dev-server up + DemoSeeder applied
- [ ] Run from `c:/моя/проекты/портал crm/Документация`:
```bash
curl -s -o /dev/null -w "%{http_code}\n" http://localhost:8000/login
```
Expected: `200`. If not 200 → start `cd app && php artisan serve --port=8000 &`.
- [ ] Verify user count:
```bash
cd "/c/моя/проекты/портал crm/Документация/app" && php artisan tinker --execute='echo \App\Models\User::count();'
```
Expected: `>= 1`. If `0``php artisan migrate:fresh --seed`.
### Step 1.2 — Rebuild Vite (quirk #81)
```bash
cd "/c/моя/проекты/портал crm/Документация/app" && npx vite build
```
Expected: build OK, ~2s, `app/public/build/assets/*.js` files updated.
### Step 1.3 — Run Pa11y guest baseline
```bash
cd "/c/моя/проекты/портал crm/Документация" && npm run a11y
```
Expected: `✔ 7/7 URLs passed`. If anything fails, treat as regression — investigate before continuing.
### Step 1.4 — Capture & commit nothing (verification only)
- [ ] Note timing in scratch — no file changes yet.
---
## Task 2 — Discover login form selectors
**Files:**
- Read: `app/resources/js/views/auth/LoginView.vue` (selectors for email/password inputs + submit button)
- Read: `app/resources/js/views/auth/TwoFactorView.vue` (for 2FA flow if applicable — DemoSeeder user has `totp_enabled: false`, so 2FA can be skipped)
### Step 2.1 — Read LoginView for `data-testid` / `id` / `name` attributes
- [ ] Read full file:
```text
app/resources/js/views/auth/LoginView.vue
```
Identify:
- Email input selector (e.g. `input[type="email"]` or `#email` or `[data-testid="login-email"]`)
- Password input selector
- Submit button selector
- Success-redirect target (likely `/dashboard`)
### Step 2.2 — Verify DemoSeeder credentials in code
- [ ] Grep `app/database/seeders/DemoSeeder.php`:
```bash
grep -n "admin@demo.local\|password" app/database/seeders/DemoSeeder.php
```
Expected: confirmation that email is `admin@demo.local` and `Hash::make('password')` is used.
### Step 2.3 — Document selectors
- [ ] Write findings in scratch (no commit yet):
```text
Login flow:
- POST /login
- email input: <selector>
- password input: <selector>
- submit button: <selector>
- on success: redirect to /dashboard (Vue Router auth-guard pulls user via /api/auth/me)
```
---
## Task 3 — Extend Pa11y config with authenticated URLs (actions-based login)
**Files:**
- Modify: `pa11y.config.json` (add 14+ authenticated URLs with per-URL `actions` block for login)
### Step 3.1 — Verify Pa11y `actions` API works locally on a single auth URL
- [ ] Create temporary `pa11y-test.config.json`:
```json
{
"defaults": {
"standard": "WCAG2AA",
"timeout": 30000,
"wait": 1500,
"hideElements": ".js-skip-a11y, [data-a11y-skip], .dev-index-badge, .dev-index-num, .v-overlay-container",
"ignore": ["warning", "notice"],
"chromeLaunchConfig": {
"args": ["--no-sandbox", "--disable-dev-shm-usage"]
},
"viewport": { "width": 1440, "height": 900 }
},
"urls": [
{
"url": "http://localhost:8000/dashboard",
"screenCapture": "./bin/a11y-screenshots/live-auth-dashboard.png",
"actions": [
"navigate to http://localhost:8000/login",
"wait for element <SELECTOR_FROM_TASK_2> to be visible",
"set field <EMAIL_SELECTOR> to admin@demo.local",
"set field <PASSWORD_SELECTOR> to password",
"click element <SUBMIT_SELECTOR>",
"wait for path to be /dashboard",
"navigate to http://localhost:8000/dashboard",
"wait for element <DASHBOARD_LANDMARK> to be visible"
]
}
]
}
```
Replace `<SELECTOR_FROM_TASK_2>` with selectors discovered in Task 2.
### Step 3.2 — Run test config
```bash
cd "/c/моя/проекты/портал crm/Документация" && npx pa11y-ci --config pa11y-test.config.json
```
Expected: `1/1 URL passed` (or violations listed). If Pa11y errors with «navigation timeout» or «selector not found» — debug login-flow selectors before proceeding.
### Step 3.3 — If Step 3.2 succeeds, merge auth URLs into main config
- [ ] Add 14 authenticated URL blocks (one per route) to `pa11y.config.json`. Each block shares the same `actions` login template (only target URL + screenshot filename change).
Routes from `app/routes/web.php` (Route::view + auth-guarded — verify in Vue Router meta):
1. `/dashboard` (always auth)
2. `/deals` (auth)
3. `/kanban` (auth)
4. `/projects` (auth)
5. `/billing` (auth)
6. `/settings` (auth)
7. `/reports` (auth)
8. `/reminders` (auth)
9. `/admin/tenants` (admin-auth)
10. `/admin/billing` (admin)
11. `/admin/incidents` (admin)
12. `/admin/system` (admin)
13. `/admin/pricing-tiers` (admin)
14. `/admin/supplier-prices` (admin)
`/admin/impersonation` — covered by separate flow (`saas_admin` SSO not present on MVP per `routes/web.php:74`); for MVP, treat as authenticated like other admin.
### Step 3.4 — Delete the temporary test config
```bash
cd "/c/моя/проекты/портал crm/Документация" && rm pa11y-test.config.json
```
---
## Task 4 — Run extended Pa11y baseline + capture findings
**Files:**
- Output: terminal log + `bin/a11y-screenshots/live-auth-*.png` (auto-generated by Pa11y)
### Step 4.1 — Full Pa11y run
```bash
cd "/c/моя/проекты/портал crm/Документация" && npm run a11y 2>&1 | tee /tmp/pa11y-extended-baseline.log
```
Expected: at least one of two outcomes:
- All ≥21 URLs pass → straight to documentation (Task 8)
- Some URLs fail with violations → categorize in Task 5
### Step 4.2 — Tally pass/fail per URL
- [ ] From log, build a table:
| URL | Status | Violations count | Notes |
|---|---|---|---|
| /login | pass/fail | N | |
| /dashboard | | | |
| ... | | | |
### Step 4.3 — For each failing URL, capture violation details
- [ ] Per-violation: rule name (color-contrast, label, region, etc.), DOM selector, expected ratio/text, recommendation.
---
## Task 5 — Categorize findings (real prod vs known temp vs structural)
**Files:**
- Output: classification table in scratch + decision matrix for Task 7.
### Step 5.1 — Triage each finding
For each violation, decide:
- **REAL_PROD** — fix in code (Task 7)
- **KNOWN_TEMP** — DevIndexBadge / DevIndex* — already in `hideElements`; if it leaks through, investigate why
- **STRUCTURAL_VUETIFY** — `.v-overlay-container` / portal — already ignored; investigate if new
- **CONTENT_DATA** — depends on DemoSeeder content (e.g. empty table state) — fix in test data OR component fallback
- **MIGRATED_KNOWN** — already known from Audit #3 Phase 7 (empty-table-header on /admin/tenants — should be CLOSED now since title='Действия' was set in deferred-fixes sprint commit `c524227`)
### Step 5.2 — Decision matrix output
```text
REAL_PROD → fix in Task 7 (atomic commit per finding)
KNOWN_TEMP → no fix; verify hideElements covers selector
STRUCTURAL → no fix; document in audit-baseline-pa11y.md
CONTENT_DATA → fix in seed/component fallback if production-relevant
MIGRATED_KNOWN→ verify fix from c524227 holds; if regressed, re-fix
```
---
## Task 6 — axe-core cross-validation via Playwright MCP
**Files:**
- No file changes — Playwright MCP session output.
### Step 6.1 — Launch Playwright + login
- [ ] Open Playwright MCP browser to `http://localhost:8000/login`
- [ ] Fill email `admin@demo.local`, password `password`, submit
- [ ] Wait for `/dashboard`
### Step 6.2 — Inject axe-core CDN + run on each route
For each of the 18 routes in scope:
- [ ] `browser_navigate http://localhost:8000/<route>`
- [ ] `browser_wait_for` until main content rendered
- [ ] `browser_evaluate`:
```javascript
() => {
return new Promise((resolve) => {
if (!window.axe) {
const script = document.createElement('script');
script.src = 'https://cdnjs.cloudflare.com/ajax/libs/axe-core/4.10.0/axe.min.js';
script.onload = () => window.axe.run(document).then(resolve);
document.head.appendChild(script);
} else {
window.axe.run(document).then(resolve);
}
});
}
```
- [ ] Capture `violations[]` array per route.
### Step 6.3 — Cross-compare Pa11y vs axe-core findings
Build a table:
| Route | Pa11y violations | axe-core violations | Agreement? |
|---|---|---|---|
Pa11y uses axe-core rules under the hood, so they should largely agree. Discrepancies signal:
- Pa11y standard (WCAG2AA) vs axe default (axe-core baseline incl. best-practice) — axe surfaces more.
- Timing differences (Pa11y waits, axe runs once snapshot).
- Visual diff (Pa11y full screencap renders Vuetify portals empty vs axe sees populated DOM).
---
## Task 7 — Fix real production findings (one atomic commit per finding)
**Files:**
- TBD based on findings — likely Vue components in `app/resources/js/components/**`, `app/resources/js/views/**`, or scoped styles.
### Step 7.1 — For each REAL_PROD finding, write TDD-style fix
Per finding:
- [ ] Identify the source file containing the offending element
- [ ] Make minimal change (scoped CSS color override / aria-label / label association / etc.)
- [ ] Rebuild Vite: `cd "/c/моя/проекты/портал crm/Документация/app" && npx vite build`
- [ ] Re-run pa11y on the affected URL only:
```bash
cd "/c/моя/проекты/портал crm/Документация" && npx pa11y-ci --config pa11y.config.json --include "<full-url>"
```
- [ ] Verify violation gone, no new ones introduced.
### Step 7.2 — Commit per fix
```bash
git add <files>
git commit -m "fix(a11y): <one-line summary>"
```
Atomic commits make rollback safe and history readable. Per Pravila — one logical change → one commit.
---
## Task 8 — Update Pa11y baseline doc
**Files:**
- Modify: `docs/audit-baseline-pa11y.md` (extended baseline post-rescan)
### Step 8.1 — Append «Authenticated rescan 2026-05-14» section
```markdown
## Authenticated rescan — 2026-05-14
> Extension to first baseline (guest pages only). Added 14 authenticated routes
> via Pa11y `actions` per-URL login flow. DemoSeeder credentials.
### URLs scanned (live Vue, authenticated)
| # | URL | Pa11y exit | Violations | Status |
|---|---|---|---|---|
| 1 | /dashboard | 0/N errors | M | ✅/⚠️ |
| ... |
### New fixes during rescan
| URL | Violation | File | Fix commit |
|---|---|---|---|
| ... |
```
### Step 8.2 — Update «Authenticated pages — out of scope» section
```markdown
~~## Authenticated pages — out of scope для первой baseline~~
(SUPERSEDED 2026-05-14) — see «Authenticated rescan» section above.
Implemented via Pa11y `actions` API per-URL login flow + DemoSeeder
credentials.
```
---
## Task 9 — Create audit findings document
**Files:**
- Create: `docs/superpowers/audits/2026-05-14-a11y-rescan-findings.md`
### Step 9.1 — Write findings doc following Audit #3 format
```markdown
# A11y Rescan — Live Portal Findings (2026-05-14)
> Continuation of Audit #3 Phase 7 — closes the out-of-scope clause for
> authenticated routes after deferred-fixes sprint (commits 8ba9c55..ae20033).
## Scope expansion
- Pre-rescan baseline (after sprint `ae20033`): 7 guest pages, 0 violations
- Rescan adds: 14 authenticated routes (Pa11y `actions` API) + axe-core
cross-validation via Playwright MCP
## Findings
| ID | Severity | Rule | Route | DOM | Status |
|---|---|---|---|---|---|
| ... |
## Per-route table
[Reuse table from Task 8 Step 8.1]
## Fix tally
- REAL_PROD fixed inline: N
- KNOWN_TEMP (DevIndexBadge) — verified ignored via hideElements: N
- STRUCTURAL_VUETIFY (.v-overlay-container) — documented: N
- MIGRATED_KNOWN (empty-table-header from `c524227`) — verified closed: N
## Verdict: 🟢 GREEN / 🟡 YELLOW
```
---
## Task 10 — Final regression sweep (Phase 14-style)
**Files:**
- No file changes — verification only.
### Step 10.1 — Run all gates
```bash
cd "/c/моя/проекты/портал crm/Документация/app" && ./vendor/bin/pest --parallel
```
Expected: 742/739/3sk/0.
```bash
cd "/c/моя/проекты/портал crm/Документация/app" && npx vitest run --no-coverage
```
Expected: 91 files / 736 / 3sk / 0.
```bash
cd "/c/моя/проекты/портал crm/Документация/app" && npx vite build
```
Expected: ≤3s.
```bash
cd "/c/моя/проекты/портал crm/Документация" && npm run links
```
Expected: 0 errors.
```bash
cd "/c/моя/проекты/портал crm/Документация" && ./bin/gitleaks.exe detect --source . --redact --log-level info
```
Expected: 0 leaks.
```bash
cd "/c/моя/проекты/портал crm/Документация" && npm run a11y
```
Expected: full pass on all 21+ URLs after Task 7 fixes (or documented exemptions).
---
## Task 11 — Final commit + push
### Step 11.1 — Stage doc-only changes from Tasks 8+9
```bash
cd "/c/моя/проекты/портал crm/Документация"
git add pa11y.config.json docs/audit-baseline-pa11y.md docs/superpowers/audits/2026-05-14-a11y-rescan-findings.md docs/superpowers/plans/2026-05-14-a11y-rescan-live-portal.md
```
NB: code-fix commits from Task 7 are already individually committed.
### Step 11.2 — Commit doc bundle
```bash
git commit -m "$(cat <<'EOF'
docs(a11y): extend Pa11y scope to 14 authenticated routes + audit findings
Closes Audit #3 Phase 7 «authenticated pages out of scope» clause from
deferred-fixes sprint Task 1.
- pa11y.config.json: +14 authenticated URLs via per-URL actions login flow
- docs/audit-baseline-pa11y.md: «Authenticated rescan 2026-05-14» section
- docs/superpowers/audits/2026-05-14-a11y-rescan-findings.md: full findings
Final tallies: <N> URLs / <V> violations / <F> fixes inline.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
EOF
)"
```
### Step 11.3 — Push
```bash
git push origin main
```
Pre-push hooks: gitleaks-full-history + lychee-links must pass.
---
## Self-review
**Spec coverage:**
- ✅ Re-verify guest baseline (Task 1)
- ✅ Login flow discovery (Task 2)
- ✅ Pa11y actions config (Task 3)
- ✅ Run + tally (Task 4)
- ✅ Categorize findings (Task 5)
- ✅ axe-core cross-validation (Task 6)
- ✅ Fix real prod findings (Task 7)
- ✅ Update baseline doc (Task 8)
- ✅ Audit findings document (Task 9)
- ✅ Final regression (Task 10)
- ✅ Commit + push (Task 11)
**Placeholder scan:**
- Task 3 references `<SELECTOR_FROM_TASK_2>` etc. — these ARE filled in once Task 2 produces the selectors. Acceptable per workflow.
- Task 7 file paths are TBD — fundamentally cannot be filled in upfront because they depend on Task 5 findings. The procedure (identify source → minimal change → verify → commit) is fully specified.
- Task 8/9 violation tables are template skeletons — populated as Task 4 produces data.
**Type consistency:**
- All Pa11y config blocks share same `defaults` shape — consistent with current `pa11y.config.json`.
- All git commands use absolute path pattern — quirk #79 compliant.
- All `cd "/c/моя/проекты/портал crm/Документация"` invocations use full absolute path — no double-cd risk.
---
## Execution choice
Plan complete and saved to `docs/superpowers/plans/2026-05-14-a11y-rescan-live-portal.md`. Two execution options:
1. **Inline Execution (recommended for this task)** — sequential in this session with checkpoints. Task 6 (Playwright MCP) and Task 7 (fix iteration) need orchestrator decisions per-finding — subagent dispatch would fragment context.
2. **Subagent-Driven** — alternative for parallelism, but most tasks have inter-task dependencies (Task 2 selectors → Task 3 config → Task 4 run → Task 5 categorize → Task 7 fix per finding). Linear flow fits better.
**Recommendation:** Inline Execution.