# Project export: PRGate

This document was generated by HackStack to give an AI agent context about a hackathon project. Sections are labeled with their provenance; content marked as truncated was cut to keep this document small.

## Project metadata

- Hackathon: OpenAI Build Week
- Tagline: PRGate audits a live website, fixes accessibility barriers in a safe branch, re-renders the result, verifies each change with real evidence, and opens a PR only when the fix truly works.
- Devpost: https://devpost.com/software/access-agent-j964ln
- GitHub: https://github.com/git791/Access-Agent
- Demo: https://access-agent.onrender.com/
- Video: https://www.youtube.com/embed/LcGk5zpC6Zk?enablejsapi=1&hl=en_US&rel=0&start=&version=3&wmode=transparent
- Team: 2 GitHub contributor(s) — git791 (37 commits), eipek-a (6 commits)

## Devpost submission (written by the team)

### Inspiration

Accessibility tools are excellent at identifying issues, but they often leave teams with a long backlog and no proof that a proposed fix actually works for a real user. We wanted to close that gap. PRGate was inspired by a simple question: what if an accessibility agent could inspect a rendered website, fix the underlying code safely, look at the deployed result again, and only claim success when it has evidence? The goal is not to replace accessibility experts, but to remove repetitive remediation work while keeping human review and proof at the center.

### What it does

PRGate audits a live public preview for accessibility barriers, including issues found by axe-core and visual issues that static analysis can miss. It then: Captures browser screenshots and accessibility-tree evidence. Prioritizes findings by user impact and WCAG relevance. Proposes source-level fixes inside an isolated GitHub branch. Runs the target repository’s tests. Waits for a Vercel Preview Deployment. Re-renders and re-audits the changed preview. Stores before and after evidence for every result. Opens a GitHub pull request only when all fixes in the batch verify. If a fix cannot be proven after the allowed retry attempts, PRGate marks it for human review instead of pretending it is complete.

### How we built it

We built the dashboard and API layer with Next.js, React, and TypeScript. Supabase provides GitHub OAuth, persistent run history, private screenshot evidence, and rate limiting. Inngest orchestrates the durable audit, patch, preview, verification, daily rescan, and retention workflows. The browser worker runs in a Render Docker service with Playwright and axe-core, allowing the system to render real pages, capture screenshots, inspect accessibility information, and perform static audits. For safe remediation, PRGate uses Vercel Sandbox to clone the target repository into an isolated environment, create a disposable accessagent/run-* branch, apply bounded edits, run tests, and push the branch. Vercel then creates a Preview Deployment that PRGate audits again before opening a pull request through the GitHub API. Codex, using GPT-5.6 Terra during development, helped us implement, inspect, test, debug, and iterate on the system. The runtime uses the OpenAI Responses API through a structured provider interface for visual inspection, patch planning, and verification. Playwright remains responsible for browser control and evidence capture.

### Challenges we ran into

The biggest challenge was making the system genuinely evidence-driven instead of producing a convincing but unverified demo. Browser automation on serverless infrastructure was difficult because full Chromium runtimes have storage, memory, and execution constraints. We separated the public Vercel dashboard from the Render-based browser worker to give Playwright a reliable Docker environment. We also encountered provider quotas, structured-output incompatibilities, sandbox patch failures, preview deployment timing, and Inngest endpoint synchronization conflicts. These failures shaped the product: patch edits are validated before application, tests run before preview verification, retries are bounded, and unsuccessful runs become human-review states. Another important challenge was avoiding false positives. For axe-originated issues, fresh axe results are authoritative after a re-render. For visual barriers, PRGate compares screenshots, accessibility context, and the original finding before calling a fix verified.

### Accomplishments we're proud of

We are proud that PRGate completes a real closed loop instead of stopping at a report or generated diff. The system successfully: Audits a rendered target through Playwright and axe-core. Captures stored before and after browser evidence. Detects real missing image alternatives, unlabeled form controls, and contrast issues. Creates isolated source changes in temporary GitHub branches. Runs repository tests before deployment. Waits for and audits a real Vercel Preview Deployment. Verifies fixes using fresh browser-derived evidence. Creates a GitHub pull request only after all fixes verify. Keeps failed or inconclusive fixes out of the verified path. Supports scheduled daily rescans and evidence retention cleanup. We are also proud that the dashboard is designed around the same accessibility principles it evaluates: high contrast, clear states, visible evidence, keyboard-accessible controls, and no fabricated findings before a real audit occurs.

### What we learned

We learned that autonomous coding is most valuable when it is paired with strict verification, not when it simply produces more code. A source diff does not prove that an accessibility issue is resolved. The deployed page may behave differently, a style change may create a regression, or an AI-generated edit may not apply cleanly. Real browser evidence, tests, isolated branches, and preview deployments make the system more trustworthy. We also learned that deployment architecture matters as much as prompts. Separating the lightweight dashboard from the browser-capable worker made the system more reliable and made its security boundaries clearer. Finally, we learned to treat model quotas and provider failures as normal operational states that need explicit retries, limits, and human-review paths.

### What's next

for Access Agent Next, we want to expand keyboard-navigation and focus-management testing, improve source-file mapping for complex repositories, add team-level review workflows, and make the remediation policy more configurable. We also plan to add richer issue explanations for developers and accessibility consultants, stronger regression detection across multiple pages, configurable model tiers, cost controls, and deeper GitHub PR summaries. Long term, PRGate can become a continuous accessibility reliability layer: not just finding issues, but continuously proving that fixes remain valid as websites evolve.

## README (from the GitHub repository)

# Access-Agent

**A closed-loop accessibility remediation agent.** PRGate audits a rendered website, proposes source-level fixes in an isolated environment, re-renders the changed preview, and only opens a pull request when the recorded evidence verifies the fixes.

It is deliberately more than an accessibility scanner. A scanner can report a broken image alternative or unlabeled form field; AccessAgent carries that finding through a real browser audit, a disposable code branch, a test run, a deployment preview, and a second browser inspection before it makes a verification claim.

## Team

- Mohammed Ayaan Adil Ahmed
- Elif İpek Aktaş

## What is implemented

| Capability | What PRGate does |
| --- | --- |
| Rendered audit | Crawls an authorized same-origin site within page/depth limits, captures screenshots and an accessibility-tree snapshot, and runs axe-core. |
| Visual review | Sends screenshot, accessibility context, and static findings through a structured AI-provider contract to identify visual barriers such as insufficient contrast. |
| Safe source changes | Clones the configured repository into Vercel Sandbox, works only on a new `PRGate/run-*` branch, and never writes to `main`. |
| Test + preview gate | Runs the configured repository test command, pushes the temporary branch, then waits for its Vercel Preview Deployment. |
| Verification | Re-audits the preview, compares before/after screenshots, stores evidence, and labels each original finding as verified or needing review. |
| Pull request | Opens one GitHub PR only when every patch in the batch verifies; the branch includes the evidence files. |
| Durable workflows | Uses Inngest for retries, long-running orchestration, scheduled rescans, and retention cleanup. |
| Evidence dashboard | Shows a live trace, findings, diffs, screenshots, contrast chips, stored evidence, and scheduled-rescan state. |

The controlled `/demo-target` is intentionally inaccessible so that the full loop can be demonstrated safely. It is not an implementation example.

## How Codex and GPT-5.6 are used

### Codex + GPT-5.6 Terra: the development and verification environment

Codex, using GPT-5.6 Terra for this build, was used to turn the product specification into this working system: it inspected the repository, implemented the Next.js, Supabase, Inngest, browser-audit, sandbox, and GitHub integrations; ran type/unit checks; reviewed diffs; and iterated on deployment and runtime failures. That mirrors the core product thesis: make a source change, run real checks, and inspect the outcome rather than trusting an untested edit.

### GPT-5.6: the intended high-capability reasoning and visual-review model

PRGate's runtime calls the OpenAI Responses API for three bounded tasks: visual accessibility inspection, source-edit proposal, and post-preview verification. When an OpenAI project provides a GPT-5.6 model identifier, set `OPENAI_VISION_MODEL` and `OPENAI_PATCH_MODEL` to that identifier to use it for those roles. Playwright—not the model—controls the browser, captures screenshots, and runs axe-core; the model receives the resulting evidence and must return schema-validated structured output.

The committed default is currently `gpt-5-mini` to keep a hackathon proof affordable. This is intentional model tiering, not a claim that every run used GPT-5.6. The workflow, evidence gates, sandbox, and verification behavior stay the same when the configured OpenAI model changes.

## The idea in one minute

```text
Broken deployed page
       |
       v
Playwright + axe-core + visual inspection find real barriers
       |
       v
AI patch role proposes minimal source edits in an isolated sandbox branch
       |
       v
Configured tests pass -> branch gets a Vercel Preview Deployment
       |
       v
Playwright re-renders and re-audits that preview
       |
       +--> any unresolved/regressed issue: retry (maximum 3) or needs human review
       |
       `--> every issue verified: evidence is committed and a GitHub PR is opened
```

The central product rule is simple: **a diff is not proof**. A finding is only marked verified after fresh browser-derived evidence exists for the deployed patch preview.

## Architecture

```mermaid
flowchart LR
  U[Developer in PRGate dashboard] --> V[Vercel: Next.js dashboard and run API]
  V --> S[(Supabase: runs, findings, events, evidence)]
  V --> E[Inngest event API]
  E --> R[Render: Docker worker /api/inngest]
  R --> B[Playwright + axe-core]
  R --> A[AI provider: OpenAI production path]
  R --> X[Vercel Sandbox: isolated clone and patch]
  X --> G[GitHub temporary branch]
  G --> P[Vercel Preview Deployment]
  P --> B
  R --> S
  R --> G
```

### Deployment responsibilities

| Service | Responsibility | Important boundary |
| --- | --- | --- |
| Vercel | Public Next.js dashboard, GitHub sign-in, `POST /api/runs`, Vercel Preview Deployments, and Sandbox control plane. | It queues audit events; it does **not** run the durable browser/AI workflow. |
| Render | Docker worker with the Playwright-capable Chromium runtime and the Inngest function endpoint. | Holds AI, signing, patching, GitHub, and preview credentials. |
| Inngest | Durable workflow execution, retries, cron triggers, run timeline. | It must be synced to the **Render** endpoint, not the Vercel endpoint. |
| Supabase | GitHub OAuth session support, Postgres run history, Realtime dashboard data, private screenshot/evidence storage, and rate limiting. | Browser code uses only the publishable key; the secret key stays server-side. |
| GitHub | Repository source, short-lived patch branches, stored evidence, and final PR. | The fine-grained token needs Contents and Pull requests read/write access. |

### Critical Inngest deployment rule

The Inngest Vercel integration must be disconnected for this project. That integration can automatically resync `/api/inngest` back to a Vercel deployment whenever Vercel deploys, replacing the Render worker endpoint. The intended active URL is:

```text
https://<render-worker>.onrender.com/api/inngest
```

Vercel needs `INNGEST_EVENT_KEY` to send events. Render needs both `INNGEST_EVENT_KEY` and `INNGEST_SIGNING_KEY` because it serves and executes the signed workflows.

## How the system works, bottom-up

### 1. User interface and API ingress

The frontend is Next.js 15 and React 19. It renders the run trace, evidence, findings, patch attempts, before/after pairs, and rescan controls. The design uses a high-contrast ink/paper palette and ratio chips because the product interface should meet the standard it asks other sites to meet.

When a user starts an audit, `POST /api/runs` validates that the target is an authorized, safe public URL; applies the per-hour audit limit; creates a queued Supabase run; and sends `PRGate/audit.requested` to Inngest. If `ACCESSAGENT_REQUIRE_AUTH=true`, GitHub OAuth through Supabase is required before the run can start.

This Vercel ingress route intentionally checks only its own needs: Supabase and the Inngest event key. It does not need an AI key or an Inngest signing key.

### 2. Durable multi-stage orchestration

Inngest receives the event and calls the worker endpoint hosted on Render. `audit-patch-verify` retains state across browser work, AI calls, sandbox work, preview deployment waits, retries, and errors. The stages are represented in the UI as:

1. Crawl + audit
2. Visual inspection
3. Patch proposal
4. Re-render
5. Verification

The workflow retries provider-rate-limit errors and uses a hard maximum of three patch/preview/verification attempts. A run that cannot prove all fixes is never presented as successful and never opens a PR.

Two additional Inngest workflows exist:

- `scheduled-rescan` runs daily at `02:00` UTC and queues due rescan records.
- `retention-cleanup` runs daily at `02:30` UTC and removes stale runs/evidence according to `ACCESSAGENT_RETENTION_DAYS`.

### 3. Rendered browser audit

The worker launches Playwright Chromium agains

[README truncated for size]

## Detected evidence (automated analysis)

Indexed codebase: 46 recognized source files, 143 KB.
- CSS (language) — detected in the code
- Next.js (technology) — detected in the code
- OpenAI (technology) — detected in the code
- React (technology) — detected in the code
- SQL (language) — detected in the code
- Supabase (technology) — detected in the code
- TypeScript (language) — detected in the code
- Docker (technology) — claimed on Devpost, not found in the code
- Vercel (technology) — claimed on Devpost, not found in the code

## Codebase structure (from repository index)

### Files (53 of 53)

```
.dockerignore
.env.example
.github/workflows/ci.yml
.gitignore
app/api/health/route.ts
app/api/inngest/route.ts
app/api/runs/[runId]/events/route.ts
app/api/runs/[runId]/route.ts
app/api/runs/[runId]/schedule/route.ts
app/api/runs/route.ts
app/auth/callback/route.ts
app/auth/login/page.tsx
app/demo-target/catalog/page.tsx
app/demo-target/checkout/page.tsx
app/demo-target/demo-nav.tsx
app/demo-target/page.tsx
app/demo-target/product/page.tsx
app/demo-target/support/page.tsx
app/layout.tsx
app/page.tsx
app/styles.css
Dockerfile
docs/architecture.md
inngest/client.ts
inngest/functions.ts
lib/ai-provider.ts
lib/alerts.ts
lib/audit.ts
lib/auth.ts
lib/config.ts
lib/contracts.ts
lib/patch.ts
lib/pr.ts
lib/preview.ts
lib/prioritize.ts
lib/rate-limit.ts
lib/store.ts
lib/url-security.ts
lib/visual-audit.ts
middleware.ts
next-env.d.ts
next.config.ts
package.json
playwright.config.ts
README.md
render.yaml
supabase/migrations/001_initial.sql
tests/smoke.spec.ts
tests/unit/ai-provider.test.ts
tests/unit/prioritize.test.ts
tests/unit/url-security.test.ts
tsconfig.json
vercel.json
```

### Dependencies

- package.json: @axe-core/playwright@^4.10.2, @google/genai@^2.12.0, @playwright/test@^1.51.1, @sparticuz/chromium@^149.0.0, @supabase/ssr@^0.12.3, @supabase/supabase-js@^2.110.5, @types/node@^22.13.4, @types/react@^19.0.8, @types/react-dom@^19.0.3, @vercel/sandbox@^1.1.0, axe-core@^4.10.3, inngest@^3.38.0, next@^15.2.0, octokit@^4.1.2, openai@^4.86.1, playwright-core@^1.61.1, react@^19.0.0, react-dom@^19.0.0, tsx@^4.19.3, typescript@^5.7.3, zod@^3.24.2

### Recent commits (newest first)

- Merge pull request #6 from git791/codex/prgate-rename
- feat: rename frontend to PRGate
- Rename README
- Github CI fix
- Front end tweaks
- Merge pull request #2 from git791/frontend
- test: update frontend smoke expectations
- feat: refine accessibility workspace frontend
- README
- PR
- Remove inngest from vercel
- dou->sin
- Deterministic
- strictness
- Validation pass
- diff
- ()
- Patch
- model
- Patch

## Key source files (fetched from GitHub, selected and truncated for size)

### docs/architecture.md

```markdown
# AccessAgent runtime architecture

## Chosen stack

| Concern | Choice | Responsibility |
| --- | --- | --- |
| Isolated patch execution | Vercel Sandbox | Runs agent-generated edits and commands in an isolated Firecracker microVM. |
| Durable orchestration | Inngest | Coordinates audit → patch → verify → retry with per-step history. |
| Agent contracts | OpenAI Structured Outputs + Zod | Validates each role handoff before the next role acts. |
| Live dashboard | Server-Sent Events + Supabase Realtime | Streams trace events and persists reconnectable state. |
| Data and evidence | Supabase Postgres + Storage | Stores runs, findings, verdicts, and screenshot pairs. |
| Browser baseline | Playwright + axe-core | Captures the rendered page and static accessibility signal. |
| Pull request | Octokit | Creates a typed GitHub PR only after verified verdicts. |
| Deployment | Vercel | Hosts the dashboard, Inngest handlers, and Sandbox control plane. |

## Non-negotiable safety boundary

The Next.js application never runs patch commands against a developer machine or production repository. It creates a Vercel Sandbox from a disposable repository reference. Credentials remain server-side; browser screenshots and state are stored as evidence.

## Event flow

1. `audit/requested` starts an Inngest run.
2. The audit role writes a validated `AuditHandoff`.
3. The patch role receives one issue at a time and returns a validated `PatchHandoff` from a Vercel Sandbox.
4. The verify role returns a `Verdict`; failed verdicts loop to patch for at most three attempts.
5. Verified findings are persisted and streamed to the dashboard. The PR role is allowed to run only when all included verdicts are verified.

The canonical TypeScript contracts live in `lib/contracts.ts`; no agent exchanges freeform prose as executable state.

```

### Dockerfile

```
# The Playwright image ships the matching Chromium binary and all Linux browser
# dependencies, which a regular Node/Vercel serverless image does not provide.
FROM mcr.microsoft.com/playwright:v1.61.1-noble

WORKDIR /app
ENV NODE_ENV=production

COPY package.json package-lock.json ./
RUN npm ci --legacy-peer-deps

COPY . ./
RUN npm run build

EXPOSE 10000
CMD ["sh", "-c", "npm run start -- -p ${PORT:-10000}"]

```

### package.json

```
{
  "name": "access-agent",
  "version": "0.1.0",
  "private": true,
  "scripts": {
    "dev": "next dev",
    "build": "next build",
    "start": "next start",
    "lint": "tsc --noEmit",
    "test": "tsc --noEmit && tsx --test tests/unit/*.test.ts",
    "test:e2e": "playwright test"
  },
  "dependencies": {
    "@axe-core/playwright": "^4.10.2",
    "@google/genai": "^2.12.0",
    "@playwright/test": "^1.51.1",
    "@sparticuz/chromium": "^149.0.0",
    "@supabase/ssr": "^0.12.3",
    "@supabase/supabase-js": "^2.110.5",
    "@vercel/sandbox": "^1.1.0",
    "axe-core": "^4.10.3",
    "inngest": "^3.38.0",
    "next": "^15.2.0",
    "octokit": "^4.1.2",
    "openai": "^4.86.1",
    "playwright-core": "^1.61.1",
    "react": "^19.0.0",
    "react-dom": "^19.0.0",
    "zod": "^3.24.2"
  },
  "devDependencies": {
    "@types/node": "^22.13.4",
    "@types/react": "^19.0.8",
    "@types/react-dom": "^19.0.3",
    "tsx": "^4.19.3",
    "typescript": "^5.7.3"
  },
  "overrides": {
    "postcss": "^8.5.10"
  }
}

```

### app/layout.tsx

```typescript
import type { Metadata } from "next";
import "./styles.css";

const logoIcon = "data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 64 64'%3E%3Crect width='64' height='64' rx='12' fill='%23183A5A'/%3E%3Ctext x='32' y='40' text-anchor='middle' fill='%23fff1dd' font-family='Arial,sans-serif' font-size='20' font-weight='700'%3EPR%3C/text%3E%3C/svg%3E";

export const metadata: Metadata = {
  title: "PRGate — Accessibility audit workspace",
  description: "Audit public previews, prioritize accessibility barriers, and retain the evidence behind every verified result.",
  applicationName: "PRGate",
  icons: { icon: logoIcon },
};

export default function RootLayout({ children }: Readonly<{ children: React.ReactNode }>) {
  return <html lang="en"><body>{children}</body></html>;
}

```

### app/demo-target/page.tsx

```typescript
"use client";

import { useState } from "react";
import { DemoNav } from "./demo-nav";

/** Controlled target for the live demo. The defects are deliberate and isolated to this route. */
export default function DemoTarget() {
  const [open, setOpen] = useState(false);
  return <main style={{ maxWidth: 720, margin: "48px auto", fontFamily: "Arial, sans-serif" }}>
    <DemoNav />
    <p style={{ color: "#999", background: "#fff" }}>New collection: designed for everyone.</p>
    <h1 id="products">Acme Store</h1>
    <img src="/demo-product.svg" />
    <p id="support">A controlled preview used to demonstrate real accessibility findings across five linked pages.</p>
    <input type="email" placeholder="Email address" />
    <button style={{ background: "#9f8c73", color: "#fff", border: 0, padding: 12 }} onClick={() => setOpen(true)}>Checkout</button>
    {open && <div role="dialog" aria-modal="true" style={{ border: "2px solid #222", padding: 24, marginTop: 24 }}><h2>Checkout</h2><p>This dialog intentionally lacks focus management for the demo audit.</p><button onClick={() => setOpen(false)}>Close</button></div>}
  </main>;
}

```

### app/page.tsx

```typescript
"use client";

import { FormEvent, useEffect, useState } from "react";
import { createBrowserClient } from "@supabase/ssr";

type Finding = {
  id: string;
  title: string;
  wcag: string;
  impact: "Critical" | "Serious" | "Moderate";
  helps: string;
  status: "Verified" | "Review" | "Found";
};

type Patch = { branch: string; commitSha?: string; attempt: number; filesChanged: string[]; diff: string };
type Run = {
  id: string;
  status: string;
  message: string;
  findings: Finding[];
  patches?: Patch[];
  evidence?: { before?: string; after?: string };
  targetUrl?: string;
};
type Schedule = { id: string; target_url: string; enabled: boolean; next_run_at: string };

const terminalStatuses = ["completed", "needs_review", "failed"];
const trace = [
  { title: "Baseline audit", detail: "Capture the rendered page, accessibility tree, and deterministic findings." },
  { title: "Visual review", detail: "Check the interface a person encounters for barriers automated rules may miss." },
  { title: "Patch proposal", detail: "Prepare the smallest source-level change in an isolated branch." },
  { title: "Fresh render", detail: "Render the changed preview after tests and deployment checks pass." },
  { title: "Verification", detail: "Keep a success claim only when fresh evidence supports the fix." }
] as const;
const emptyMessage = "Start with a public preview URL, or try the live example for a guided walkthrough.";
const liveDemoUrl = "https://access-agent-sable.vercel.app/demo-target";

function isHttpUrl(value: string) {
  try {
    const parsed = new URL(value);
    return /^https?:$/.test(parsed.protocol) && !parsed.username && !parsed.password;
  } catch { return false; }
}

function explainAuditError(error?: string) {
  const raw = error?.trim() || "";
  const value = raw.toLowerCase();
  if (value.includes("live audits are unavailable") || value.includes("configuration is complete")) return "Auditing is not configured for this workspace yet. Ask a workspace administrator to finish setup.";
  if (value.includes("valid http") || value.includes("embedded credentials")) return "Use a complete public http:// or https:// URL without a username or password.";
  if (value.includes("private") || value.includes("reserved") || value.includes("localhost")) return "The audit worker cannot reach local or private URLs. Deploy a public preview, then try again.";
  if (value.includes("401") || value.includes("403") || value.includes("unauthorized") || value.includes("forbidden")) return "The preview refused access. Use an authorized public preview or check its access settings.";
  if (value.includes("timeout") || value.includes("network") || value.includes("econnrefused") || value.includes("enotfound")) return "The audit worker could not reach the preview. Check that it is online, public, and has a valid HTTPS certificate.";
  return raw || "The audit could not start. Check the preview URL and try again.";
}

export default function Home() {
  const [url, setUrl] = useState(liveDemoUrl);
  const [run, setRun] = useState<Run | null>(null);
  const [running, setRunning] = useState(false);
  const [message, setMessage] = useState(emptyMessage);
  const [events, setEvents] = useState<string[]>([]);
  const [queueFilter, setQueueFilter] = useState<"All" | "Critical" | "Review" | "Verified">("All");
  const [selectedFinding, setSelectedFinding] = useState<Finding | null>(null);
  const [schedules, setSchedules] = useState<Schedule[]>([]);
  const [scheduleMessage, setScheduleMessage] = useState("");
  const [accessToken, setAccessToken] = useState<string | null>(null);

  useEffect(() => {
    if (!process.env.NEXT_PUBLIC_SUPABASE_URL || !process.env.NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY) return;
    const client = createBrowserClient(process.env.NEXT_PUBLIC_SUPABASE_URL, process.env.NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY);
    client.auth.getSession().then(({ data }) => setAccessToken(data.session?.access_token ?? null));
    const { data: listener } = client.auth.onAuthStateChange((_, session) => setAccessToken(session?.access_token ?? null));
    return () => listener.subscription.unsubscribe();
  }, []);

  async function signOut() {
    if (!process.env.NEXT_PUBLIC_SUPABASE_URL || !process.env.NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY) return;
    const client = createBrowserClient(process.env.NEXT_PUBLIC_SUPABASE_URL, process.env.NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY);
    await client.auth.signOut();
  }

  useEffect(() => {
    if (!run || terminalStatuses.includes(run.status)) return;
    let cancelled = false;
    let failures = 0;
    const refresh = async () => {
      try {
        const response = await fetch(`/api/runs/${run.id}`, { cache: "no-store" });
        const updated = await response.json().catch(() => null) as Run | null;
        if (!response.ok || !updated?.status) {
          failures += 1;
          if (failures >= 3 && !cancelled) { setRunning(false); setMessage("We stopped receiving audit updates. Your current results are still available; refresh or try again later."); }
          return;
        }
        if (cancelled) return;
        failures = 0;
        setRun(updated);
        setMessage(updated.status === "failed" ? explainAuditError(updated.message) : updated.message);
        if (terminalStatuses.includes(updated.status)) setRunning(false);
      } catch { failures += 1; }
    };
    const timer = window.setInterval(refresh, 2500);
    return () => { cancelled = true; window.clearInterval(timer); };
  }, [run?.id, run?.status]);

  useEffect(() => {
    if (!run || terminalStatuses.includes(run.status)) return;
    const stream = new EventSource(`/api/runs/${run.id}/events`);
    stream.onmessage = (event) => {
      const item = JSON.parse(event.data) as { message: string };
      setEvents((current) => [...current.slice(-4), item.message]);
    };
    stream.onerror = () => stream.close();
    return () => stream.close();
  }, [run?.id, run?.status]);

  useEffect(() => {

[truncated — 17518 more characters]
```

### app/api/health/route.ts

```typescript
import { NextResponse } from "next/server";

/** Lightweight liveness endpoint for the Render worker deployment. */
export function GET() {
  return NextResponse.json({ ok: true, service: "access-agent-worker" });
}

```

### app/api/inngest/route.ts

```typescript
import { serve } from "inngest/next";
import { inngest } from "../../../inngest/client";
import { auditWorkflow, retentionCleanupWorkflow, scheduledRescanWorkflow } from "../../../inngest/functions";

export const maxDuration = 60;
export const { GET, POST, PUT } = serve({ client: inngest, functions: [auditWorkflow, scheduledRescanWorkflow, retentionCleanupWorkflow] });

```

### app/demo-target/support/page.tsx

```typescript
import { DemoNav } from "../demo-nav";

/** Deliberate, isolated accessibility defects for the controlled crawl demo. */
export default function DemoSupport() {
  return <main style={{ maxWidth: 720, margin: "48px auto", fontFamily: "Arial, sans-serif" }}>
    <DemoNav />
    <h1>Support</h1>
    <img src="/demo-product.svg" />
    <p style={{ color: "#999", background: "#fff" }}>We normally answer within one business day.</p>
    <input placeholder="Order number" />
    <textarea placeholder="How can we help?" />
    <button style={{ background: "#9f8c73", color: "#fff", border: 0, padding: 12 }}>Send message</button>
  </main>;
}

```

### app/demo-target/checkout/page.tsx

```typescript
import { DemoNav } from "../demo-nav";

/** Deliberate, isolated accessibility defects for the controlled crawl demo. */
export default function DemoCheckout() {
  return <main style={{ maxWidth: 720, margin: "48px auto", fontFamily: "Arial, sans-serif" }}>
    <DemoNav />
    <h1>Checkout</h1>
    <p style={{ color: "#999", background: "#fff" }}>Secure payment · your details stay private.</p>
    <input autoComplete="cc-name" placeholder="Name on card" />
    <input inputMode="numeric" autoComplete="cc-number" placeholder="Card number" />
    <input autoComplete="postal-code" placeholder="Postal code" />
    <iframe srcDoc="<p>Secure payment provider</p>" style={{ border: 0, display: "block", margin: "16px 0" }} />
    <button>Place order</button>
  </main>;
}

```

[37 more indexed source files omitted to keep this export small. The full file list is in the Codebase structure section above.]