Website Issues — Per-Page Issue Detection & AI Code Fixer
Status: [Live May 2026]
Issue detection runs automatically after every website-insights job. Issues are stored per page per type. Dashboard shows an Issues tab on the channel detail and an Issues card on each page detail. The “Fix with AI” button triggers the
website-code-fixeragent which opens a GitHub PR with targeted source-code fixes.
Concept
After every crawl + insight cycle, a deterministic rule engine scans the WebPage records and writes one WebsiteIssue row per page per detected problem. This gives tenants a structured, actionable list distinct from the Claude-generated narrative in WebsiteInsight.
When a WebsiteGitHubSync config is present, users can ask the code-fixer agent to fix a specific issue type (or all open issues for a page) directly in their source repository. The agent reads the repo file tree, detects the framework, finds the responsible source files, applies surgical fixes, and opens a pull request.
Data Models
WebsiteIssue
One row per page per detected issue type. Regenerated on every insight run (open issues replaced; fixed/ignored rows preserved).
model WebsiteIssue {
id String @id @default(cuid())
tenantId String
connectedChannelId String
webPageId String
webCrawlJobId String
issueType String // see Issue Types table below
severity String // "critical" | "warning" | "info"
pageUrl String // denormalized — display without join
details Json? // { wordCount, httpStatus, duplicateCount, imageUrl, ... }
status String @default("open") // "open" | "fixed" | "ignored"
fixPrUrl String?
fixPrNumber Int?
fixedAt DateTime?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([connectedChannelId, issueType])
@@index([connectedChannelId, status])
@@index([webPageId])
@@map("website_issue")
}WebsiteGitHubSync
Maps a Website channel to a GitHub repo for code-fix PRs. One record per website channel (unique).
model WebsiteGitHubSync {
id String @id @default(cuid())
tenantId String
connectedChannelId String @unique // Website ConnectedChannel
githubChannelId String // GitHub OAuth ConnectedChannel
repoOwner String
repoName String
branch String @default("main")
lastFixRunAt DateTime?
lastPrUrl String?
lastPrNumber Int?
lastFixStatus String? // "idle" | "running" | "success" | "failed"
lastFixError String? @db.Text
@@map("website_github_sync")
}Issue Types
issueType | Severity | Detection rule | details fields |
|---|---|---|---|
missing_title | critical | WebPage.title IS NULL | — |
missing_meta_description | critical | WebPage.description IS NULL | — |
broken_page | critical | WebPage.httpStatus >= 400 | { httpStatus } |
thin_content | warning | extractedText word count < 200 | { wordCount } |
duplicate_title | warning | Multiple pages share same lowercase title | { title, duplicateCount } |
duplicate_meta | warning | Multiple pages share same lowercase description | { descriptionSnippet, duplicateCount } |
missing_alt_text | warning | WebMedia.altText IS NULL on IMAGE type | { imageUrl } |
Issue Generation
Where: packages/agents/src/workers/insights/website-insights.worker.ts — exported generateWebsiteIssues() function.
When: Immediately after buildMetrics() in the existing website-insights worker (which runs after every completed crawl). Non-fatal — a generation failure is logged but does not block the Claude insight.
Algorithm:
generateWebsiteIssues(tenantId, connectedChannelId, webCrawlJobId)
├─ Load all WebPage rows for this crawl
├─ Load IMAGE WebMedia rows with no altText (via pageMedias join)
│
├─ For each page:
│ ├─ title IS NULL → missing_title (critical)
│ ├─ description IS NULL → missing_meta_description (critical)
│ ├─ httpStatus >= 400 → broken_page (critical)
│ └─ wordCount < 200 > 0 → thin_content (warning)
│
├─ Across all pages (duplicate detection):
│ ├─ Group pages by lowercase title → titles shared by 2+ pages → duplicate_title (warning) per page
│ └─ Group pages by lowercase desc → descs shared by 2+ pages → duplicate_meta (warning) per page
│
├─ For each image with no altText → missing_alt_text (warning) on its first associated page
│
├─ DELETE existing open issues for channel (fixed/ignored preserved)
└─ createMany(issues)API
Prefix: /tenant/v1/website-issues
| Method | Path | Description |
|---|---|---|
GET | /channel/:id | Cursor-paginated list. Query: status, issueType, severity, cursor, limit |
GET | /channel/:id/summary | Counts by type (open only), by severity (open only), by status (all) |
GET | /page/:webPageId | Issues for a single page |
PATCH | /:issueId | Update status to "open" or "ignored" |
POST | /github-sync | Upsert WebsiteGitHubSync config (body: connectedChannelId, githubChannelId, repoOwner, repoName, branch) |
GET | /github-sync/:connectedChannelId | Get sync config for a channel |
POST | /fix | Trigger code-fixer agent (body: connectedChannelId, optional issueIds[]) |
Router file: apps/api/src/routers/website-issues.ts
Registered in both app.ts and index.ts.
Queue
// packages/queue/src/types.ts
interface WebsiteCodeFixerJobData {
tenantId: string;
tenantName: string;
connectedChannelId: string;
websiteGitHubSyncId: string;
issueIds: string[]; // empty = fix all open issues for channel
}// packages/queue/src/queues.ts
enqueueWebsiteCodeFixer(data: WebsiteCodeFixerJobData): Promise<string>
// Queue: agent__website-code-fixer
// JobId: website-code-fixer__{tenantId}__{connectedChannelId}__{Date.now()}Code-Fixer Agent Worker
File: packages/agents/src/workers/website-code-fixer.worker.ts
Queue: agent__website-code-fixer
Concurrency: 1 (prevents race conditions on the same repo)
Flow
processJob(job)
├─ 1. Load WebsiteGitHubSync → decrypt GitHub OAuth token
├─ 2. Load open WebsiteIssue rows (issueIds filter or all open)
│ Cap at 50. Filter to code-fixable types only (excludes broken_page).
├─ 3. publishAgentEvent(agent:started)
├─ 4. GitHubService.getFileTree() → detect framework from file names
│ Frameworks: nextjs / nuxt / gatsby / wordpress / astro / node-generic / html
├─ 5. Claude tool-use loop (max 20 iterations):
│ Tools:
│ read_file(filePath) → GitHubService.getFileContent()
│ write_file(filePath, content, commitMessage) → GitHubService.putFile()
│ (creates branch on first write: "leadmetrics/fix-website-issues-{timestamp}")
│ Prompt: framework + file tree (first 300 paths) + issue list
│ Model: from AgentConfig.role="website-code-fixer", default claude-sonnet-4-6
├─ 6. GitHubService.createPR() — if any files were changed
│ Title: "fix: website SEO issues detected by Leadmetrics (N issues)"
│ Body: issue list + files changed + merge instructions
├─ 7. WebsiteIssue.updateMany → status="fixed", fixPrUrl, fixPrNumber, fixedAt
├─ 8. WebsiteGitHubSync.update → lastFixStatus, lastPrUrl, lastPrNumber
└─ 9. publishAgentEvent(agent:completed)Claude Prompt Structure
# Website Code Fix Task
## Framework
{detected framework}
## Repository File Tree (first 300 lines)
{file paths, one per line}
## Issues to Fix
1. [missing_title] https://example.com/about
2. [missing_meta_description] https://example.com/services — ...
## Instructions
- read_file before editing, write_file after
- Make ONLY targeted changes, no refactoring
- For missing_title/meta: add/update HTML meta tags in template/layout file
- For missing_alt_text: add descriptive alt to <img> tags
- For thin_content: create a details.json note (do NOT fabricate content)
- For duplicate_title/meta: make each page's tag unique by appending page name
- Skip issues where the responsible file cannot be determined
- Output a JSON summary block at the end: { fixed[], skipped[], filesChanged[] }Framework Detection
| Marker file | Detected as |
|---|---|
next.config.js/ts/mjs | nextjs |
nuxt.config.ts/js | nuxt |
gatsby-config.js/ts | gatsby |
wp-config.php | wordpress |
any *.astro file | astro |
package.json (no above) | node-generic |
| none of the above | html |
Code-Fixable Issues
The agent only attempts to fix: missing_title, missing_meta_description, missing_h1, thin_content, duplicate_title, duplicate_meta, missing_alt_text.
broken_page (HTTP 4xx/5xx) is excluded — these require infrastructure or routing changes that the agent cannot safely make.
UI
Channel Detail — Issues Tab
Location: apps/dashboard/src/app/(dashboard)/channels/[id]/WebsiteChannelDetail.tsx
Added as a fifth tab alongside Overview / Pages / Media / Insights. Tab label shows live count: Issues (12 critical, 8 warnings).
Summary row: 3 stat cards — Critical count (red / XCircle), Warning count (amber / AlertCircle), Fixed count (emerald / CheckCircle2).
Filter pills: Open / Fixed / Ignored
Issue groups: Issues grouped by issueType. Each group renders as a rounded-2xl card:
- Header: icon + human-readable label + count badge + “Fix with AI” button (shown only for
status=open) - Rows:
pageUrl+ details snippet + status badge - IntersectionObserver infinite scroll on the flat list
Banners:
- Success (emerald): “Code fixer agent queued — a PR will be opened in your GitHub repo shortly.”
- Warning (amber): “Connect a GitHub repo first in the GitHub Sync tab.” (when
NO_GITHUB_SYNCerror) - Error (red): generic API error message
Page Detail — Issues Card
Location: apps/dashboard/src/app/(dashboard)/channels/[id]/webpages/[webPageId]/WebPageDetailClient.tsx
An Issues card rendered between the metadata section and the content section. Shows:
- Card header: “Issues” + count badge
- Empty state: “No issues detected for this page.”
- Each issue row: severity badge (critical=red, warning=amber) + human-readable label + details snippet + status badge + optional “View PR #N” link
- “Fix with AI” button — POSTs all open issue IDs for the page to
/tenant/v1/website-issues/fix
Files Changed (May 2026)
| File | Change |
|---|---|
packages/db/prisma/schema.prisma | Add WebsiteIssue + WebsiteGitHubSync models + back-relations |
packages/queue/src/types.ts | Add WebsiteCodeFixerJobData + "website-code-fixer" to AgentRole |
packages/queue/src/queues.ts | Add enqueueWebsiteCodeFixer() |
packages/queue/src/index.ts | Export new type + function |
packages/agents/src/workers/insights/website-insights.worker.ts | Add generateWebsiteIssues() + call in processJob() |
packages/agents/src/workers/website-code-fixer.worker.ts | New — code-fixer agent worker |
apps/servers/agents/src/index.ts | Register startWebsiteCodeFixerWorker + stop |
apps/api/src/routers/website-issues.ts | New — 7 endpoints |
apps/api/src/app.ts | Register websiteIssuesRouter |
apps/api/src/index.ts | Register websiteIssuesRouter |
apps/dashboard/src/app/(dashboard)/channels/[id]/WebsiteChannelDetail.tsx | Add Issues tab (state, fetch, render) |
apps/dashboard/src/app/(dashboard)/channels/[id]/webpages/[webPageId]/WebPageDetailClient.tsx | Add Issues card + fix trigger |
apps/dashboard/src/app/(dashboard)/channels/[id]/webpages/[webPageId]/page.tsx | Pass apiUrl + token to client |