Jev-Powered GEO Citability Dashboard
Audit public web pages with crawl-aware discovery and source-linked AI evidence.
Introduction
30 Second Summary
Reviewing hundreds of pages can leave you with a spreadsheet full of numbers but no clear place to start. Slow feedback also makes each content improvement harder to prove.
In this project, you will build a local GEO citability dashboard that discovers permitted public pages from robots.txt and XML sitemaps. The dashboard extracts server-rendered HTML, scores it with Jev, and scales from a five-page pilot to Full Discovery across up to 600 pages.
What You'll Build
You enter an authorised website URL, watch its permitted pages appear, and see each finished source page update a live review queue.
By the end of this project, you'll have:
- A real-site discovery path that reads crawl policy and same-host sitemaps before listing permitted public URLs.
- A secure five-page pilot that proves Jev can score real page text without exposing the OpenRouter key in browser code.
- A Full Discovery command center with truthful progress, live worker lights, an automatic 50-percent success guard, a ranked review queue, and a safely pushed GitHub project.
- Secret Mission: Add a floating audit guide that explains the product, discusses current results, and uses NVIDIA Nemotron 3 Ultra through OpenRouter for free by default.
Are there any prerequisites?
You need a supported Windows, macOS, or Linux workstation with Python 3.10+.
Git, a modern browser, Claude Code access, a GitHub account, funded OpenRouter access, and internet access are also required.
Before We Start
This first checkpoint locks in your commitment to audit page citability signals. Each score serves as rubric-based editorial evidence for review, with no guarantee that a page will be cited.
Prepare Your Workstation
A live audit depends on a local toolchain that can run the server and track each change. Checking that foundation now prevents setup problems from interrupting your first dashboard run.
You will verify Python 3.10+, Git, and Claude Code with shared commands. You will check account access for Claude Code and the GitHub CLI, then create the empty repository for your project.
In this step, get ready to:
- Verify Python, Git, and Claude Code.
- Confirm access to Claude Code and GitHub.
- Create and verify an empty project repository.
Verify your required tools
Python runs the local server in later steps. Git records your changes, while Claude Code helps you review and edit the supplied project.
- Check all three tools by running these commands:
python3 --version
git --version
claude --version
- Choose the tab that matches your result.
✔️ I see all three versions
Your workstation can run the server, track changes, and open Claude Code. That is the toolchain ready.
ⓧ Python is missing or below 3.10
Python 3.10 or higher is required for this project. Use the terminal commands for your operating system to install a current release.
macOS
The Python package is about 75 MB, so the download may take a minute on a slower connection. The second command asks for your macOS password because it installs Python for all users.
- Download Python 3.14.7 from Python.org and install it by running these commands:
curl -O https://www.python.org/ftp/python/3.14.7/python-3.14.7-macos11.pkg
sudo installer -pkg ./python-3.14.7-macos11.pkg -target /
python3 --version
You should see Python 3.14.7. That version exceeds the project's Python 3.10 minimum.
Windows
WinGet downloads Python and may display a permission prompt. Allow the installer to make the requested change.
- Install Python 3.14 and check the installed version by running these commands in PowerShell:
winget install Python.Python.3.14
python --version
You should see Python 3.14. On Windows, use python anywhere a later command shows python3.
Linux
Your distribution chooses the supported Python 3 release. Ubuntu 22.04 and newer provide a version that meets this project's minimum.
- Install Python 3 and check the installed version by running these commands:
sudo apt update
sudo apt install -y python3
python3 --version
You should see Python 3.10 or higher.
Still seeing the old version?
Restart your terminal after installation, then retry the version command for your operating system. If the version is still below 3.10, help me diagnose the Python command.
ⓧ Git or Claude Code is missing
Install only the command that failed. The platform tabs keep every setup action in the terminal.
macOS
The Git command opens Apple's built-in installer. Confirm the installation when macOS prompts you.
- Install Git only when the git command is missing by running:
xcode-select --install
- Install Claude Code only when the claude command is missing by running:
curl -fsSL https://claude.ai/install.sh | bash
The Claude Code installer prints Claude Code successfully installed! when it finishes.
- Verify both tools by running:
git --version
claude --version
You should see a version for both Git and Claude Code.
Windows
WinGet may display an installer permission prompt. Allow the requested change for the tool you install.
- Install Git only when the git command is missing by running this command in PowerShell:
winget install --id Git.Git -e --source winget
- Install Claude Code only when the claude command is missing by running this command in PowerShell:
winget install Anthropic.ClaudeCode
- Restart PowerShell after either installation.
- Verify both tools by running:
git --version
claude --version
You should see a version for both Git and Claude Code.
Linux
The package update may take a minute while Linux refreshes its repository list. The Claude Code installer then downloads its current native binary.
- Install Git only when the git command is missing by running:
sudo apt-get update
sudo apt-get install -y git
- Install Claude Code only when the claude command is missing by running:
curl -fsSL https://claude.ai/install.sh | bash
The Claude Code installer prints Claude Code successfully installed! when it finishes.
- Restart your terminal after either installation.
- Verify both tools by running:
git --version
claude --version
You should see a version for both Git and Claude Code.
Command still unavailable?
A terminal opened before installation may still have the old command path. Restart it first, then help me check the missing command.
Check account access
Claude Code and GitHub CLI store their login state on your computer. A status check lets returning learners skip authentication while keeping first-time setup close by.
- Check both account connections by running these commands:
claude auth status --text
gh auth status
You should see a signed-in Claude account and an active GitHub account. Open a support tab only when its status check fails.
✔️ Both accounts are ready
Both tools can access the accounts needed for this project. You can move straight to repository creation.
ⓧ Claude Code needs authentication
Authentication opens a browser and stores the resulting login on your computer. Your password stays out of the terminal.
- Start Claude Code authentication by running this command:
claude auth login
- Follow the browser prompts to sign in.
- Return to your terminal after the browser confirms authentication.
- Confirm the saved login by running:
claude auth status --text
You should see a human-readable status confirming that Claude Code is logged in.
Browser login did not finish?
Press c when Claude Code offers to copy the login URL, then open that URL in your browser. If authentication still fails, help me diagnose Claude Code login.
ⓧ GitHub CLI needs setup or login
The repository command uses GitHub CLI. Install it only when gh auth status reports that the command is unavailable.
- Open the official GitHub CLI installation guide when the gh command is missing.
- Complete the installation instructions for your operating system.
- Restart your terminal after the installation finishes.
- Start GitHub authentication by running:
gh auth login
- Select GitHub.com when the interactive setup asks where to authenticate.
- Select HTTPS when the setup asks for a Git protocol.
- Complete the browser authentication flow.
- Confirm the active account by running:
gh auth status
You should see the GitHub account that can create your project repository.
GitHub CLI still not ready?
Confirm that you restarted the terminal after installing GitHub CLI. If the status still fails, help me diagnose GitHub CLI setup.
Create your empty project repository
An empty repository keeps the project self-contained. The next step supplies every file, so you do not depend on an external starter template.
- Move to your Desktop by running this command:
- Create the public repository and clone it by running:
cd ~/Desktop
gh repo create jev-geo-audit --public --clone
GitHub CLI should confirm that it created the public repository and cloned jev-geo-audit to your Desktop.
- Enter the cloned repository by running:
cd jev-geo-audit
Before you check the repository, do you expect Git to report project files or a clean empty starting point?
- Confirm that no supplied project files are tracked by running:
git ls-files
You should see no file names. That empty result proves the project does not rely on supplied starter code.
- Confirm that your public copy is the origin remote by running:
git remote -v
You should see matching fetch and push entries for your jev-geo-audit repository. Later commits now have the correct destination.
Repository did not clone?
Confirm that gh auth status still shows an active account. An existing repository with the same name can also block creation.
If creation or cloning still fails, help me diagnose the repository flow.
Your workstation and empty repository are ready. Next, you will add every complete project file and open the working mock dashboard.
Build the Visible Mock Dashboard
Your workstation and empty repository are ready. The project now needs every file required to produce a visible result.
You will open jev-geo-audit in VS Code or Cursor. Claude Code will work beside your files while the browser begins with safe mock evidence and the live API route stays disabled.
In this step, get ready to:
- Open the project in VS Code or Cursor and connect Claude Code.
- Create the project guidance and dashboard files with Claude Code.
- Run the smoke test and preview the mock dashboard.
Create the project boundary files
Claude Code can read the folder open in your IDE and show its edits beside your files. This keeps each prompt, generated file, and diff in one workspace.
Choose VS Code or Cursor below. Keep the Claude Code panel open after you connect it because each file group builds on the same project context.
VS Code
- Open your VS Code
- Press Cmd+Space on macOS or the Windows key on Windows to open system search. On Linux, open the application menu.
- Type Visual Studio Code, Enter
- Click Open Folder.
- Select the jev-geo-audit folder on your Desktop.
- Click Open.
You should see the empty repository in the Explorer sidebar. The official Claude Code extension requires VS Code 1.94 or higher.
- Press Cmd+Shift+X to open Extensions.
- Enter Claude Code in the extension search field.Install
The extension adds Claude Code as a panel inside the IDE. Your saved account from Step 1 should carry across.
- Press Cmd+Shift+P to open the Command Palette.
- Enter Claude Code.
- Select Claude Code: Open in New Tab.
- Complete the browser sign-in if Claude Code requests it.
You should see the Claude Code chat beside the jev-geo-audit files.
Cursor
- Open your Cursor
- Press Cmd+Space on macOS or the Windows key on Windows to open system search. On Linux, open the application menu.
- Type Cursor, Enter.
- Click File in the top menu.
- Click Open Folder.
- Select the jev-geo-audit folder on your Desktop.
- Click Open.
You should see the empty repository in the Explorer sidebar. Cursor supports the same official Claude Code extension.
- Press Cmd+Shift+X to open Extensions.
- Enter Claude Code in the extension search field, Install
The extension adds Claude Code as a panel inside Cursor. Your saved account from Step 1 should carry across.
- Press Cmd+Shift+P to open the Command Palette.
- Enter Claude Code.
- Select Claude Code: Open in New Tab.
- Complete the browser sign-in if Claude Code requests it.
You should see the Claude Code chat beside the jev-geo-audit files.
Claude Code panel missing?
The extension sometimes needs an IDE reload before its panel appears. Your project folder stays open during the reload.
- Open the Command Palette.
- Run Developer: Reload Window.
If the panel is still missing, help me open Claude Code in my IDE.
- Ask Claude Code in the IDE panel to create the four boundary files by pasting this prompt:
Create README.md, CLAUDE.md, .gitignore, and .env.example in this repository.
Use the complete file references from my project instructions as the source of truth.
README.md must explain the mock-first workflow and local-only server.
CLAUDE.md must forbid browser-side credentials and external dependencies.
.gitignore must exclude .env, Python caches, and .DS_Store.
.env.example must contain only OPENROUTER_API_KEY=your-api-key-here.
Do not create .env and do not add a real credential.
After writing the files, list the filenames you created.
What does this file group do?
- The README.md file explains how to run the project.
- The CLAUDE.md file constrains future AI-assisted edits.
- The .gitignore file keeps local secrets and generated files out of Git.
- The .env.example file documents the required environment variable without storing a credential.
Use these complete references to replace any differences in the four files.
# Jev GEO Citability Audit
This project builds a local dashboard that discovers public pages from an authorised website and measures how easily their content can support concise, verifiable answers.
## Project flow
1. Start with twelve safe mock results.
2. Keep the OpenRouter key in a local `.env` file.
3. Discover public URLs through `robots.txt` and sitemap files.
4. Run a five-page real-site pilot before starting Full Discovery across up to 600 pages.
5. Stop Full Discovery automatically after at least 20 attempts when fewer than 50 percent succeed.
6. Draft an improvement for the weakest successful page and export the evidence.
## Requirements
- Python 3.10 or later.
- A modern browser.
- Git, GitHub CLI, and Claude Code.
- A public website you own or have permission to audit.
## Local boundary
The server binds to `127.0.0.1:8000`. Python's `http.server` is used for learning and local testing only, not production hosting.
The crawler accepts public HTTP and HTTPS pages, respects `robots.txt`, and reads server-rendered HTML. It does not render JavaScript-only content.
Never place a real OpenRouter key in `index.html`, `styles.css`, or `app.js`.
# Project instructions
- Keep the project dependency-free. Use browser APIs and the Python standard library.
- Keep every real credential in the local `.env` file.
- Never place a real credential or server environment name in browser-delivered files.
- Keep `.env` ignored by Git.
- Bind the learning server to `127.0.0.1`.
- Use `ThreadingHTTPServer` for concurrent local requests.
- Use `POST https://openrouter.ai/api/alpha/decisions` for live Jev audits.
- Use model `typesafe/jev-1.13`.
- Preserve the `/api/discover` and `/api/audit` browser contracts.
- Preserve the `readiness`, `weakest_signal`, and `verifiable_support` question keys.
- Discover URLs from `robots.txt`, sitemap URL sets, and sitemap indexes.
- Remove leading whitespace from sitemap bytes after optional gzip expansion and before XML parsing.
- Respect `robots.txt`, `Crawl-delay`, and the 600-page project cap.
- Fetch only public HTTP or HTTPS URLs from the authorised website.
- Reject credentials in URLs, unsafe ports, private IP addresses, and cross-site redirects.
- Limit downloaded sitemap and HTML response sizes.
- Extract text from server-rendered HTML without executing JavaScript.
- Classify pages with fewer than 80 extracted characters as skipped before any Jev call.
- Track attempted, succeeded, skipped, and failed pages separately.
- Stop the pilot after five successful audits or 25 candidate attempts.
- Call the larger run `Full Discovery` in buttons, dialogs, status text, and learner-facing copy.
- Keep the 1, 8, and 24 analysis controls available for Full Discovery.
- Keep Sequential selected by default. Treat Fast x8 and Turbo x24 as optional accelerators.
- Build the browser as a Jev-only command center. Never invent other model or token-stream activity.
- Fit the dashboard inside one `100dvh` desktop viewport and keep body scrolling disabled at common desktop sizes.
- Restore normal page scrolling below 1100px wide or 680px high.
- Keep long feeds, queues, and editor content inside independently scrolling panels.
- Preserve the element IDs `appShell`, `activityPanel`, `workerRail`, `runStatus`, `activeWorkers`, `progressBar`, `liveSpeed`, and `fullscreenBtn`.
- Use the Fullscreen API only after a direct user action. Detect unsupported or rejected requests without breaking the dashboard.
- Treat website discovery as a real busy operation that is separate from a Jev audit run.
- While `/api/discover` is pending, set `aria-busy` on `appShell` and add `is-discovering` to `activityPanel`.
- During discovery, show the authorised URL and the truthful status `Reading crawl rules and sitemap files` in the worker rail.
- Use an indeterminate progress sweep during discovery. Never display a fabricated completion percentage or simulated page count.
- Update discovery elapsed time with `requestAnimationFrame`, then cancel the frame when discovery succeeds or fails.
- Change the discovery button label to `Discovering...` while the request is pending and restore it when the request settles.
- Disable conflicting discovery and audit controls while discovery is pending.
- Remove the discovery worker card, busy state, and animation after success or failure.
- Track real in-flight audit requests in `activePages`. Never add placeholders to `activePages` or simulate live worker cards.
- Render one worker card per in-flight URL with a moving perimeter light.
- Drive worker-card perimeter lights from a stable animation clock on `workerRail` so metrics updates cannot reset their orbit.
- Never recreate worker cards inside the `requestAnimationFrame` metrics loop.
- Remove a worker card when its request settles, then place its real outcome in the completion feed.
- Render each result when its own request settles.
- Drive the linear progress bar from attempted candidates divided by the run target.
- Add a moving highlight inside the truthful filled portion while an audit is running.
- Show a successful audit rate arc based on succeeded candidates divided by attempted candidates.
- Keep candidate-processing speed, successful-page speed, and successful-response cost as separate metrics.
- After at least 20 settled Full Discovery attempts, automatically stop before launching another batch whenever succeeded candidates divided by attempted candidates is below 50 percent.
- Let every request already in flight settle before evaluating the automatic-stop guard.
- Count skipped and failed candidates as unsuccessful attempts when calculating the guard.
- Do not offer a continue action after the guard triggers.
- Show the attempted, succeeded, skipped, failed, and successful audit rate values with the automatic-stop reason.
- Set `aria-busy` on `appShell` and add `is-running` to `activityPanel` only during an audit run.
- Stop processing animation after completion, failure, a manual stop, or an automatic threshold stop.
- Respect `prefers-reduced-motion` with static active borders, progress styling, and worker-card borders.
- Track every audit `AbortController` and abort all in-flight requests when Stop is selected.
- Show an in-page paid confirmation before Full Discovery with candidate count, maximum cost, remaining $6 budget, and selected speed.
- Show an in-page completion summary after a settled run.
- Export source URL, outcome, fetch time, HTTP status, methodology, model, request ID, latency, confidence, support, and cost.
- Treat the readiness score as a project-defined editorial heuristic, not an official GEO standard.
- Treat editor changes as local drafts. Never claim the dashboard modifies the live website.
- Escape server-controlled and URL-derived text before inserting it into the page.
- Do not use Python's `http.server` as production hosting.
.env
__pycache__/
*.pyc
.DS_Store
OPENROUTER_API_KEY=your-api-key-here
- Review Claude Code's completion summary.
- Confirm that it names README.md, CLAUDE.md, .gitignore, and .env.example.
Create the complete dashboard files
The next three files form the browser application. They include a mock preview, real-site discovery controls, bounded analysis workers, live results, local draft re-scoring, and evidence exports.
- Ask Claude Code in the same IDE panel to create the frontend files by pasting this prompt:
Create index.html, styles.css, and app.js for the GEO Citability Audit dashboard.
Match the complete file references from my project instructions.
Preserve the existing /api/discover and /api/audit server contracts.
Build a compact Jev command center without inventing other model activity or token streaming.
Fit the desktop interface inside one 100dvh viewport with no body scrolling.
Use a compact header, audit toolbar, metrics row, live worker rail, completion feed, review queue, and local editor.
Give long dashboard panels their own internal scrolling.
Restore normal page scrolling and stack panels below 1100px wide or 680px high.
Call the larger run Full Discovery everywhere in the interface.
Include Sequential, Fast x8, and optional Turbo x24 controls for Full Discovery.
Keep Sequential selected by default.
Add an Enter full screen button using requestFullscreen, exitFullscreen, and fullscreenchange with a graceful unsupported fallback.
Track actual active audit requests in an activePages Map and render one worker card per in-flight URL.
Give each live worker card a moving green perimeter light until its request settles.
Drive every worker-card perimeter light from a stable animation clock on workerRail so frequent metric renders cannot reset the orbit.
Never recreate worker cards inside the requestAnimationFrame metrics loop.
Move settled pages into the completion feed and never leave completed pages in the worker rail.
Show real completed pages, active workers, elapsed time, candidate-processing speed, successful-page speed, average latency, successful-response cost, and successful audit rate.
Render the successful audit rate as a semicircular arc inspired by a compact command-center gauge.
Render each result as its request settles instead of waiting for a complete batch.
Treat website discovery as a separate busy state while /api/discover is pending.
Set aria-busy=true on appShell and add is-discovering to activityPanel during discovery.
Change the Discover pages button label to Discovering... until the request settles.
Show the authorised URL and the status Reading crawl rules and sitemap files in the worker rail during discovery.
Animate a green light around activityPanel and an indeterminate progress beam while discovery is pending.
Update the elapsed metric with requestAnimationFrame during discovery, then cancel it on success or failure.
Never show a fabricated discovery percentage, simulated page count, or invented backend event.
Disable conflicting discovery and audit controls while discovery is pending.
Remove the discovery card, busy state, and animation after success or failure.
Set aria-busy=true on appShell and add is-running to activityPanel only while an audit runs.
Drive the audit progress width from attempted candidates divided by the target.
Animate a moving highlight only inside the truthful filled portion of the progress bar.
After at least 20 settled Full Discovery attempts, calculate succeeded divided by attempted after each settled batch.
Automatically stop before launching another batch whenever that successful audit rate is below 50 percent.
Count skipped and failed candidates as unsuccessful attempts.
Let every request already in flight settle before evaluating the automatic-stop guard.
Do not offer a Continue Full Discovery action after the guard triggers.
Show the automatic-stop reason with attempted, succeeded, skipped, failed, and successful audit rate values in the completion dialog.
Stop every processing animation when the run settles, is stopped manually, or is stopped by the threshold.
Disable motion under prefers-reduced-motion while keeping static active borders and progress styling.
Keep website discovery, the five-success pilot, the 600-page cap, thin-content skips, and local draft re-scoring.
Add a Stop button that aborts every in-flight audit fetch through AbortController.
Show an in-page paid confirmation dialog before Full Discovery with the candidate count, maximum Jev estimate at $0.01 per 1,000 calls, remaining $6 budget, and selected speed.
Show a completion dialog after a run settles with attempted, succeeded, skipped, failed, elapsed, cost, speed, successful audit rate, and stop reason.
Keep CSV and JSON evidence exports.
Start with twelve static mock results and never simulate live requests with timers or invented URLs.
Never include a real credential or server environment name in browser files.
Escape or create text nodes for every server-controlled and URL-derived value.
After writing the files, run node --check app.js and summarize the discovery, Full Discovery, automatic 50-percent stop, progress, gauge, worker, manual stop, fullscreen, and reduced-motion behavior.
Why build the complete browser now?
The same interface supports the mock, pilot, Full Discovery, improvement, and export stages. Later steps only activate the secure server route and use the features already present.
✔️ My index file matches
Your index.html contains the required dashboard regions. Save the file before moving on.
ⓧ I'd like to double check the full code
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Jev GEO Citability Audit</title>
<link rel="stylesheet" href="styles.css">
</head>
<body>
<div id="appShell" class="app-shell" aria-busy="false">
<!-- Keep identity, live status, and full-screen access visible. -->
<header class="topbar">
<div class="brand-lockup">
<span class="brand-mark" aria-hidden="true">J</span>
<div>
<p class="eyebrow">Jev GEO Citability Audit | NextWork</p>
<h1>Live editorial command center</h1>
</div>
</div>
<div class="topbar-telemetry" aria-label="Current audit status">
<span class="status-dot" aria-hidden="true"></span>
<strong id="runStatus" role="status">Mock preview ready</strong>
<span id="activeWorkers">0 / 1 active</span>
</div>
<button id="fullscreenBtn" class="quiet-button" type="button" aria-pressed="false">Enter full screen</button>
</header>
<main class="dashboard">
<!-- Keep audit inputs compact so the evidence workspace stays visible. -->
<section class="panel control-bar" aria-labelledby="controls-title">
<div class="control-heading">
<p class="section-label">Audit controls</p>
<h2 id="controls-title">Choose a site and speed</h2>
</div>
<div class="url-control">
<label for="websiteUrl">Authorised website URL</label>
<div class="input-row">
<input id="websiteUrl" type="url" placeholder="https://example.com" autocomplete="url">
<button id="discoverSite" type="button">Discover pages</button>
</div>
<p id="discoveryStatus" class="status"></p>
</div>
<fieldset class="speed-control">
<legend>Analysis speed</legend>
<div class="speed-grid">
<button class="mode-button active" type="button" data-concurrency="1" aria-pressed="true">Sequential</button>
<button class="mode-button" type="button" data-concurrency="8" aria-pressed="false">Fast x8</button>
<button class="mode-button" type="button" data-concurrency="24" aria-pressed="false">Turbo x24 <small>Advanced</small></button>
</div>
</fieldset>
<div class="prediction-control">
<label for="prediction">Predicted weakest signal</label>
<select id="prediction">
<option value="answer_clarity">Answer clarity</option>
<option value="evidence_specificity">Evidence specificity</option>
<option value="source_transparency">Source transparency</option>
<option value="extractable_structure">Extractable structure</option>
</select>
<p id="predictionResult" class="status"></p>
</div>
<div class="run-actions">
<button id="runPilot" type="button" disabled>Run five-page pilot</button>
<button id="runFull" class="primary" type="button" disabled>Run Full Discovery</button>
<button id="stopRun" class="danger-button" type="button" disabled>Stop</button>
</div>
</section>
<!-- Report measured values without inventing activity. -->
<section class="metrics" aria-label="Audit metrics">
<article><span>Progress</span><strong id="progress">12 mock</strong></article>
<article><span>Elapsed</span><strong id="elapsed">0.0 s</strong></article>
<article><span>Candidate speed</span><strong id="liveSpeed">0.00 pages/s</strong></article>
<article><span>Average latency</span><strong id="latency">0 ms</strong></article>
<article><span>Successful cost</span><strong id="cost">$0.000000</strong></article>
<article class="rate-card">
<span>Successful audit rate</span>
<div class="gauge-wrap">
<svg class="rate-gauge" viewBox="0 0 120 66" aria-hidden="true">
<path class="gauge-track" pathLength="100" d="M10 58 A50 50 0 0 1 110 58"></path>
<path id="successGauge" class="gauge-value" pathLength="100" d="M10 58 A50 50 0 0 1 110 58"></path>
</svg>
<strong id="successRate">100%</strong>
</div>
</article>
</section>
<!-- Surface only genuine in-flight URLs and truthful progress. -->
<section id="activityPanel" class="panel activity-panel" aria-labelledby="activity-title">
<div class="activity-heading">
<div>
<p class="section-label">Jev live processing</p>
<h2 id="activity-title">Worker activity</h2>
</div>
<span id="runState" class="pill">Mock data</span>
</div>
<div class="progress-track" aria-label="Audit progress">
<span id="progressBar" role="progressbar" aria-valuemin="0" aria-valuemax="100" aria-valuenow="0"></span>
</div>
<div id="workerRail" class="worker-rail" aria-live="polite">
<p class="empty-state">Worker cards appear here when real audit requests begin.</p>
</div>
</section>
<!-- Keep feed, queue, and editor visible in the remaining viewport. -->
<section class="workspace">
<article class="panel workspace-panel">
<div class="panel-heading">
<div>
<p class="section-label">Live evidence</p>
<h2>Completion feed</h2>
</div>
<span id="throughput" class="pill">0.00 successful/s</span>
</div>
<div id="feed" class="scroll-region feed" aria-live="polite"></div>
</article>
<article class="panel workspace-panel">
<div class="panel-heading">
<div>
<p class="section-label">Priorities</p>
<h2>Review queue</h2>
</div>
<div class="compact-actions">
<button id="exportCsv" type="button">CSV</button>
<button id="exportJson" type="button">JSON</button>
</div>
</div>
<div id="reviewQueue" class="scroll-region queue"></div>
</article>
<aside class="panel workspace-panel editor" aria-labelledby="editor-title">
<div class="panel-heading">
<div>
<p class="section-label">Local improvement draft</p>
<h2 id="editor-title">Select a weak page</h2>
<p id="editorUrl" class="status">No page selected.</p>
</div>
<span id="editorScore" class="score-badge">No score</span>
</div>
<label for="pageContent">Proposed page content</label>
<textarea id="pageContent" placeholder="Select a page to review its content."></textarea>
<div class="compact-actions editor-actions">
<button id="saveText" type="button">Save local draft</button>
<button id="rescore" class="primary" type="button">Re-score draft</button>
</div>
<p id="editorStatus" class="status">Changes here stay in this browser session.</p>
<details class="method-note">
<summary>Method and limitations</summary>
<p>The readiness score is a project-defined editorial heuristic. Jev evaluates extracted server-rendered text, while skipped pages are excluded from successful throughput and latency.</p>
</details>
</aside>
</section>
</main>
</div>
<!-- Confirm maximum paid exposure before Full Discovery. -->
<dialog id="fullRunDialog">
<form method="dialog" class="dialog-card">
<p class="section-label">Paid API confirmation</p>
<h2>Review Full Discovery</h2>
<dl class="dialog-stats">
<div><dt>Candidate pages</dt><dd id="dialogCandidates">0</dd></div>
<div><dt>Maximum Jev estimate</dt><dd id="dialogCost">$0.000000</dd></div>
<div><dt>Remaining project budget</dt><dd id="dialogBudget">$6.000000</dd></div>
<div><dt>Selected speed</dt><dd id="dialogSpeed">Sequential</dd></div>
</dl>
<p class="status">Only eligible pages reach the paid API, so the final charge can be lower than this maximum.</p>
<div class="dialog-actions">
<button id="cancelFull" value="cancel" type="submit">Cancel</button>
<button id="confirmFull" class="primary" value="default" type="button">Start Full Discovery</button>
</div>
</form>
</dialog>
<!-- Report normal, manual, and automatic stops in one completion summary. -->
<dialog id="completionDialog">
<form method="dialog" class="dialog-card">
<p class="section-label">Audit settled</p>
<h2>Jev run summary</h2>
<dl id="completionSummary" class="dialog-stats"></dl>
<div class="dialog-actions">
<button class="primary" value="close" type="submit">Return to dashboard</button>
</div>
</form>
</dialog>
<script src="app.js"></script>
</body>
</html>
✔️ My stylesheet matches
Your dashboard now has a truthful moving progress highlight, a successful audit rate arc, and live worker cards with circulating perimeter lights. Reduced-motion mode keeps the same states visible with static borders.
ⓧ I'd like to double check the full code
/* Define the compact command-center palette and viewport rhythm. */
:root {
color-scheme: dark;
--bg: #070a12;
--panel: #101620;
--panel-soft: #161f2b;
--panel-raised: #1b2635;
--line: #2b3949;
--text: #f4f7fb;
--muted: #94a2b5;
--accent: #79f2b4;
--accent-strong: #b0ffd4;
--good: #79f2b4;
--warn: #f4c96b;
--bad: #ff7f91;
--radius: 14px;
--topbar-height: 56px;
}
/* Keep the worker light on a stable parent clock so card refreshes cannot reset it. */
@property --worker-orbit-angle {
syntax: "<angle>";
inherits: true;
initial-value: 0deg;
}
* {
box-sizing: border-box;
}
html,
body {
height: 100%;
overflow: hidden;
}
body {
margin: 0;
background: radial-gradient(circle at 50% -20%, #18322b 0, var(--bg) 42rem);
color: var(--text);
font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
}
button,
input,
select,
textarea {
font: inherit;
}
button,
input,
select {
min-height: 38px;
}
button {
border: 1px solid var(--line);
border-radius: 9px;
background: var(--panel-raised);
color: var(--text);
cursor: pointer;
padding: 0.55rem 0.8rem;
}
button:hover,
button:focus-visible,
input:focus-visible,
select:focus-visible,
textarea:focus-visible,
summary:focus-visible {
border-color: var(--accent);
outline: 3px solid rgb(121 242 180 / 16%);
outline-offset: 1px;
}
button:disabled {
cursor: not-allowed;
opacity: 0.45;
}
button.primary,
.mode-button.active {
border-color: var(--accent);
background: var(--accent);
color: #07120d;
font-weight: 800;
}
button.danger-button:not(:disabled) {
border-color: rgb(255 127 145 / 55%);
color: #ffc2ca;
}
.quiet-button {
white-space: nowrap;
}
.app-shell {
height: 100dvh;
min-height: 0;
overflow: hidden;
}
.topbar {
display: grid;
grid-template-columns: minmax(280px, 1fr) auto auto;
align-items: center;
gap: 1rem;
height: var(--topbar-height);
border-bottom: 1px solid var(--line);
background: rgb(7 10 18 / 88%);
padding: 0 0.8rem;
backdrop-filter: blur(18px);
}
.brand-lockup,
.topbar-telemetry,
.input-row,
.speed-grid,
.compact-actions,
.dialog-actions {
display: flex;
align-items: center;
gap: 0.55rem;
}
.brand-mark {
display: grid;
width: 34px;
height: 34px;
place-items: center;
border: 1px solid rgb(121 242 180 / 55%);
border-radius: 10px;
background: rgb(121 242 180 / 12%);
color: var(--accent);
font-weight: 900;
}
.topbar h1,
h2,
p {
margin-top: 0;
}
.topbar h1 {
margin-bottom: 0;
font-size: 0.98rem;
}
.eyebrow,
.section-label {
margin-bottom: 0.12rem;
color: var(--accent-strong);
font-size: 0.68rem;
font-weight: 850;
letter-spacing: 0.11em;
text-transform: uppercase;
}
.topbar-telemetry {
color: var(--muted);
font-size: 0.78rem;
}
.topbar-telemetry strong {
color: var(--text);
}
.status-dot {
width: 8px;
height: 8px;
border-radius: 50%;
background: var(--muted);
}
.app-shell[aria-busy="true"] .status-dot {
background: var(--accent);
box-shadow: 0 0 14px var(--accent);
}
.dashboard {
display: grid;
grid-template-rows: auto 76px 132px minmax(0, 1fr);
gap: 8px;
height: calc(100dvh - var(--topbar-height));
min-height: 0;
overflow: hidden;
padding: 8px;
}
.panel {
min-width: 0;
min-height: 0;
border: 1px solid var(--line);
border-radius: var(--radius);
background: rgb(16 22 32 / 94%);
box-shadow: 0 16px 44px rgb(0 0 0 / 24%);
}
.control-bar {
display: grid;
grid-template-columns: 150px minmax(240px, 1.35fr) minmax(270px, 1fr) minmax(180px, 0.72fr) minmax(300px, auto);
align-items: end;
gap: 0.65rem;
padding: 0.65rem;
}
.control-heading {
align-self: center;
}
.control-heading h2,
.activity-heading h2,
.panel-heading h2,
dialog h2 {
margin-bottom: 0;
font-size: 1rem;
}
label,
legend {
display: block;
margin-bottom: 0.3rem;
color: var(--muted);
font-size: 0.72rem;
font-weight: 750;
}
fieldset {
min-width: 0;
margin: 0;
border: 0;
padding: 0;
}
input,
select,
textarea {
width: 100%;
border: 1px solid var(--line);
border-radius: 9px;
background: #0a1018;
color: var(--text);
padding: 0.55rem 0.65rem;
}
.url-control input {
min-width: 0;
flex: 1;
}
.url-control button {
white-space: nowrap;
}
.speed-grid {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 0.35rem;
}
.speed-grid button {
padding-inline: 0.45rem;
font-size: 0.74rem;
white-space: nowrap;
}
.speed-grid small {
display: block;
font-size: 0.57rem;
font-weight: 700;
}
.run-actions {
display: grid;
grid-template-columns: repeat(3, minmax(92px, 1fr));
gap: 0.4rem;
}
.run-actions button {
font-size: 0.72rem;
}
.status,
.dialog-copy {
margin: 0.28rem 0 0;
color: var(--muted);
font-size: 0.7rem;
line-height: 1.35;
overflow-wrap: anywhere;
}
.status.success {
color: var(--good);
}
.status.error {
color: var(--bad);
}
.metrics {
display: grid;
grid-template-columns: repeat(6, minmax(0, 1fr));
gap: 8px;
}
.metrics article {
display: grid;
align-content: center;
min-width: 0;
border: 1px solid var(--line);
border-radius: 12px;
background: var(--panel-soft);
padding: 0.5rem 0.7rem;
}
.metrics span {
color: var(--muted);
font-size: 0.65rem;
}
.metrics strong {
margin-top: 0.12rem;
font-size: 0.94rem;
}
.rate-card {
grid-template-columns: minmax(0, 1fr) auto;
align-items: center;
gap: 0.4rem;
}
.gauge-wrap {
position: relative;
width: 76px;
height: 46px;
}
.rate-gauge {
display: block;
width: 76px;
height: 42px;
overflow: visible;
}
.gauge-track,
.gauge-value {
fill: none;
stroke-linecap: round;
stroke-width: 9;
}
.gauge-track {
stroke: #303b49;
}
.gauge-value {
stroke: var(--accent);
stroke-dasharray: 0 100;
transition: stroke-dasharray 220ms ease;
}
.gauge-wrap #successRate {
position: absolute;
right: 0;
bottom: 0;
left: 0;
margin: 0;
text-align: center;
font-size: 0.72rem;
}
.activity-panel {
position: relative;
isolation: isolate;
overflow: hidden;
padding: 0.65rem;
}
.activity-panel::before {
position: absolute;
z-index: 3;
top: -1px;
left: -32%;
width: 32%;
height: 2px;
background: linear-gradient(90deg, transparent, var(--accent), transparent);
content: "";
opacity: 0;
pointer-events: none;
}
.activity-panel.is-running,
.activity-panel.is-discovering {
border-color: rgb(121 242 180 / 55%);
box-shadow: 0 0 0 1px rgb(121 242 180 / 10%), 0 0 28px rgb(121 242 180 / 10%), 0 16px 44px rgb(0 0 0 / 28%);
}
.activity-panel.is-running::before {
opacity: 1;
animation: border-sweep 1.15s linear infinite;
}
.activity-panel.is-discovering::before {
opacity: 1;
animation: discovery-border-sweep 0.58s linear infinite;
}
.activity-heading,
.panel-heading {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 0.65rem;
}
.activity-heading {
margin-bottom: 0.45rem;
}
.pill,
.score-badge {
border: 1px solid var(--line);
border-radius: 999px;
background: #0a1018;
color: var(--muted);
font-size: 0.67rem;
padding: 0.28rem 0.5rem;
white-space: nowrap;
}
.progress-track {
height: 7px;
margin-bottom: 0.55rem;
overflow: hidden;
border-radius: 999px;
background: #070b11;
}
#progressBar {
position: relative;
display: block;
width: 0;
height: 100%;
overflow: hidden;
border-radius: inherit;
background: linear-gradient(90deg, #35bd7d, var(--accent));
transition: width 180ms ease;
}
#progressBar::after {
position: absolute;
inset: 0 auto 0 -42%;
width: 42%;
background: linear-gradient(90deg, transparent, rgb(255 255 255 / 78%), transparent);
content: "";
opacity: 0;
}
.activity-panel.is-running #progressBar::after {
opacity: 1;
animation: progress-light 0.92s linear infinite;
}
.activity-panel.is-discovering .progress-track {
box-shadow: 0 0 14px rgb(121 242 180 / 18%);
}
.activity-panel.is-discovering #progressBar {
width: 38% !important;
background: linear-gradient(90deg, transparent, var(--accent), var(--accent-strong), transparent);
animation: discovery-progress 0.72s ease-in-out infinite;
}
.worker-rail {
--worker-orbit-angle: 0deg;
display: flex;
gap: 0.45rem;
min-height: 58px;
overflow-x: auto;
overflow-y: hidden;
padding-bottom: 0.15rem;
scrollbar-width: thin;
animation: worker-orbit-clock 1.05s linear infinite;
}
.worker-card {
position: relative;
isolation: isolate;
flex: 0 0 min(260px, 38vw);
min-width: 0;
overflow: hidden;
border: 1px solid rgb(121 242 180 / 48%);
border-radius: 10px;
background: var(--panel-raised);
padding: 0.5rem 0.6rem;
box-shadow: 0 0 16px rgb(121 242 180 / 10%);
}
.worker-card::before {
position: absolute;
z-index: -2;
inset: -180%;
background: conic-gradient(transparent 0deg 292deg, var(--accent) 326deg, var(--accent-strong) 344deg, transparent 360deg);
content: "";
transform: rotate(var(--worker-orbit-angle));
will-change: transform;
}
.worker-card::after {
position: absolute;
z-index: -1;
inset: 1px;
border-radius: 8px;
background: linear-gradient(135deg, rgb(121 242 180 / 11%), var(--panel-raised));
content: "";
}
.worker-card.discovery-card {
flex-basis: min(420px, 72vw);
}
.worker-card.discovery-card::after {
background: linear-gradient(110deg, rgb(121 242 180 / 18%), var(--panel-raised) 62%);
}
.worker-card strong,
.worker-card small {
display: block;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.worker-card strong {
font-size: 0.72rem;
}
.worker-card small {
margin-top: 0.2rem;
color: var(--accent-strong);
font-size: 0.64rem;
}
.empty-state {
align-self: center;
margin: 0;
color: var(--muted);
font-size: 0.72rem;
}
.workspace {
display: grid;
grid-template-columns: minmax(0, 1.15fr) minmax(230px, 0.8fr) minmax(300px, 1fr);
gap: 8px;
min-height: 0;
overflow: hidden;
}
.workspace-panel {
display: flex;
min-height: 0;
flex-direction: column;
overflow: hidden;
padding: 0.7rem;
}
.panel-heading {
flex: 0 0 auto;
margin-bottom: 0.55rem;
}
.compact-actions {
align-items: stretch;
}
.compact-actions button {
min-height: 32px;
padding: 0.35rem 0.55rem;
font-size: 0.67rem;
}
.scroll-region {
display: grid;
min-height: 0;
gap: 0.42rem;
overflow: auto;
padding-right: 0.15rem;
scrollbar-width: thin;
}
.result-row,
.queue-row {
display: grid;
grid-template-columns: minmax(0, 1fr) auto;
align-items: center;
gap: 0.55rem;
width: 100%;
min-height: 52px;
text-align: left;
}
.result-row small,
.queue-row small {
display: block;
margin-top: 0.14rem;
color: var(--muted);
font-size: 0.62rem;
overflow-wrap: anywhere;
}
.result-row.success {
border-left: 3px solid var(--good);
}
.result-row.skipped {
border-left: 3px solid var(--warn);
}
.result-row.failed {
border-left: 3px solid var(--bad);
}
.score.good {
color: var(--good);
}
.score.warn {
color: var(--warn);
}
.score.bad {
color: var(--bad);
}
.editor textarea {
min-height: 0;
flex: 1 1 auto;
resize: none;
}
.editor > label {
flex: 0 0 auto;
}
.editor-actions {
flex: 0 0 auto;
margin-top: 0.55rem;
}
.method-note {
flex: 0 0 auto;
margin-top: 0.45rem;
color: var(--muted);
font-size: 0.68rem;
}
.method-note summary {
cursor: pointer;
color: var(--text);
font-weight: 750;
}
.method-note p {
margin: 0.35rem 0 0;
line-height: 1.4;
}
dialog {
width: min(520px, calc(100vw - 2rem));
border: 1px solid var(--line);
border-radius: 16px;
background: var(--panel);
color: var(--text);
padding: 0;
box-shadow: 0 32px 90px rgb(0 0 0 / 55%);
}
dialog::backdrop {
background: rgb(0 0 0 / 68%);
backdrop-filter: blur(4px);
}
.dialog-card {
padding: 1.2rem;
}
.dialog-stats {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 0.55rem;
margin: 1rem 0;
}
.dialog-stats div {
border: 1px solid var(--line);
border-radius: 10px;
background: var(--panel-soft);
padding: 0.65rem;
}
.dialog-stats dt {
color: var(--muted);
font-size: 0.68rem;
}
.dialog-stats dd {
margin: 0.18rem 0 0;
font-weight: 800;
}
.dialog-actions {
justify-content: flex-end;
margin-top: 1rem;
}
@keyframes border-sweep {
to { transform: translateX(420%); }
}
@keyframes discovery-border-sweep {
to { transform: translateX(420%); }
}
@keyframes discovery-progress {
from { transform: translateX(-115%); }
to { transform: translateX(305%); }
}
@keyframes progress-light {
to { transform: translateX(340%); }
}
@keyframes worker-orbit-clock {
to { --worker-orbit-angle: 1turn; }
}
@media (max-width: 1100px), (max-height: 680px) {
html,
body {
height: auto;
overflow: auto;
}
.app-shell {
height: auto;
min-height: 100dvh;
overflow: visible;
}
.topbar {
grid-template-columns: 1fr auto;
height: auto;
min-height: var(--topbar-height);
padding-block: 0.55rem;
}
.topbar-telemetry {
grid-column: 1 / -1;
grid-row: 2;
}
.dashboard {
height: auto;
overflow: visible;
}
.control-bar {
grid-template-columns: repeat(2, minmax(0, 1fr));
align-items: start;
}
.control-heading,
.run-actions {
grid-column: 1 / -1;
}
.metrics {
grid-template-columns: repeat(3, minmax(0, 1fr));
}
.workspace {
grid-template-columns: 1fr;
overflow: visible;
}
.workspace-panel {
min-height: 360px;
}
.scroll-region {
max-height: 420px;
}
}
@media (max-width: 620px) {
.topbar,
.control-bar,
.metrics,
.dialog-stats {
grid-template-columns: 1fr;
}
.topbar-telemetry,
.control-heading,
.run-actions {
grid-column: auto;
}
.speed-grid,
.run-actions {
grid-template-columns: 1fr;
}
.worker-card {
flex-basis: 78vw;
}
}
@media (prefers-reduced-motion: reduce) {
*,
*::before,
*::after {
scroll-behavior: auto !important;
animation-duration: 0.001ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.001ms !important;
}
.activity-panel.is-running::before,
.activity-panel.is-discovering::before {
left: 0;
width: 100%;
opacity: 0.75;
transform: none;
}
.activity-panel.is-discovering #progressBar {
width: 100% !important;
opacity: 0.75;
transform: none;
}
.worker-rail {
--worker-orbit-angle: 0deg;
animation: none;
}
.activity-panel.is-running #progressBar::after,
.worker-card::before {
display: none;
}
.worker-card {
border-color: var(--accent);
box-shadow: 0 0 0 1px rgb(121 242 180 / 18%);
}
}
✔️ My browser logic matches
Your browser now makes discovery and Full Discovery visible with truthful progress, a successful audit rate, and one illuminated card per real in-flight page. After at least 20 settled attempts, it automatically stops before launching another batch whenever fewer than half of all attempts succeeded.
ⓧ I'd like to double check the full code
// Provide static sample evidence before the learner connects a real website.
const MOCK_PAGES = [
["Shipping policy", "Orders usually arrive quickly. Delivery timing depends on location and carrier availability."],
["Returns guide", "Customers can request a return within 30 days. Items must be unused and include the order number."],
["Security overview", "Account sessions use encrypted transport. The security team reviews access logs every weekday."],
["Pricing FAQ", "The standard plan costs $24 per month and includes five team members. Annual billing reduces the total by 15%."],
["Support policy", "Support is available by email. Priority customers receive a response within four business hours."],
["Product summary", "The platform helps teams publish useful pages. It offers flexible tools and modern workflows."],
["Data retention", "Deleted workspace data enters a 14-day recovery window. Backups expire after 35 days."],
["Accessibility", "Keyboard navigation covers the primary workflow. The latest internal review was completed in March 2026."],
["Integration guide", "Connect the service with an API token. Send JSON requests to your workspace endpoint."],
["Service status", "The service performed well last quarter. Reliability remained strong for most customers."],
["Onboarding", "Create a workspace and invite colleagues. Most teams finish the checklist in about 20 minutes."],
["Privacy FAQ", "Workspace owners control member access. Export requests are processed within seven days."]
].map(([title, content], index) => ({
id: `mock-${index + 1}`,
title,
content,
url: `https://example.com/mock/${index + 1}`
}));
// Cache every element that changes while discovery or an audit runs.
const ui = {
appShell: document.querySelector("#appShell"),
websiteUrl: document.querySelector("#websiteUrl"),
discoverSite: document.querySelector("#discoverSite"),
discoveryStatus: document.querySelector("#discoveryStatus"),
modeButtons: [...document.querySelectorAll("[data-concurrency]")],
prediction: document.querySelector("#prediction"),
predictionResult: document.querySelector("#predictionResult"),
runPilot: document.querySelector("#runPilot"),
runFull: document.querySelector("#runFull"),
stopRun: document.querySelector("#stopRun"),
fullscreenBtn: document.querySelector("#fullscreenBtn"),
runStatus: document.querySelector("#runStatus"),
activeWorkers: document.querySelector("#activeWorkers"),
progress: document.querySelector("#progress"),
elapsed: document.querySelector("#elapsed"),
liveSpeed: document.querySelector("#liveSpeed"),
throughput: document.querySelector("#throughput"),
latency: document.querySelector("#latency"),
cost: document.querySelector("#cost"),
successGauge: document.querySelector("#successGauge"),
successRate: document.querySelector("#successRate"),
runState: document.querySelector("#runState"),
progressBar: document.querySelector("#progressBar"),
activityPanel: document.querySelector("#activityPanel"),
workerRail: document.querySelector("#workerRail"),
feed: document.querySelector("#feed"),
reviewQueue: document.querySelector("#reviewQueue"),
editorTitle: document.querySelector("#editor-title"),
editorUrl: document.querySelector("#editorUrl"),
editorScore: document.querySelector("#editorScore"),
pageContent: document.querySelector("#pageContent"),
saveText: document.querySelector("#saveText"),
rescore: document.querySelector("#rescore"),
editorStatus: document.querySelector("#editorStatus"),
exportCsv: document.querySelector("#exportCsv"),
exportJson: document.querySelector("#exportJson"),
fullRunDialog: document.querySelector("#fullRunDialog"),
dialogCandidates: document.querySelector("#dialogCandidates"),
dialogCost: document.querySelector("#dialogCost"),
dialogBudget: document.querySelector("#dialogBudget"),
dialogSpeed: document.querySelector("#dialogSpeed"),
confirmFull: document.querySelector("#confirmFull"),
completionDialog: document.querySelector("#completionDialog"),
completionSummary: document.querySelector("#completionSummary")
};
// Hold discovered pages, evidence, and genuine in-flight work in browser memory.
let pages = [...MOCK_PAGES];
let site = null;
let concurrency = 1;
let results = [];
let events = [];
let selectedPageId = null;
let targetCount = 12;
let startedAt = 0;
let finishedElapsed = 0;
let running = false;
let stopping = false;
let discovering = false;
let mockMode = true;
let runMode = "mock";
let statusMessage = "Mock preview ready";
let stopReason = "Completed";
let sessionSpend = 0;
let animationFrame = null;
let stats = { attempted: 12, succeeded: 12, skipped: 0, failed: 0 };
const activePages = new Map();
const controllers = new Set();
function mockResult(page, index) {
const weaknesses = ["answer_clarity", "evidence_specificity", "source_transparency", "extractable_structure"];
const score = 38 + ((index * 13) % 51);
return {
pageId: page.id,
title: page.title,
url: page.url,
content: page.content,
outcome: "succeeded",
score,
readiness: Math.min(4, Math.max(0, Math.round(score / 25))),
confidence: 0.62 + ((index % 4) * 0.07),
weakest: weaknesses[index % weaknesses.length],
weakestConfidence: 0.67 + ((index % 3) * 0.08),
support: Math.min(0.95, score / 100),
cost: 0,
latencyMs: 120 + (index * 17),
model: "mock-data",
requestId: `mock-${index + 1}`,
fetchedAt: null,
httpStatus: 200,
source: "mock"
};
}
function formatMoney(value) {
return `$${Number(value || 0).toFixed(6)}`;
}
function scoreClass(score) {
if (score >= 75) return "good";
if (score >= 50) return "warn";
return "bad";
}
function speedLabel() {
if (concurrency === 24) return "Turbo x24";
if (concurrency === 8) return "Fast x8";
return "Sequential";
}
function currentElapsed() {
return running || discovering ? (performance.now() - startedAt) / 1000 : finishedElapsed;
}
function successfulAuditRate() {
return stats.attempted > 0 ? stats.succeeded / stats.attempted : 0;
}
function setBusyState(kind) {
discovering = kind === "discovering";
const busy = kind === "discovering" || kind === "running";
ui.appShell.setAttribute("aria-busy", String(busy));
ui.activityPanel.classList.toggle("is-discovering", kind === "discovering");
ui.activityPanel.classList.toggle("is-running", kind === "running");
ui.discoverSite.disabled = busy;
ui.websiteUrl.disabled = busy;
ui.runPilot.disabled = busy || !site;
ui.runFull.disabled = busy || !site;
ui.stopRun.disabled = kind !== "running";
ui.modeButtons.forEach((button) => {
button.disabled = busy;
});
ui.discoverSite.textContent = kind === "discovering" ? "Discovering..." : "Discover pages";
}
function updateClock() {
renderMetrics();
if (running || discovering) animationFrame = requestAnimationFrame(updateClock);
}
function startClock() {
if (animationFrame !== null) cancelAnimationFrame(animationFrame);
startedAt = performance.now();
animationFrame = requestAnimationFrame(updateClock);
}
function stopClock() {
finishedElapsed = currentElapsed();
if (animationFrame !== null) cancelAnimationFrame(animationFrame);
animationFrame = null;
}
function createResultButton(event, queue = false) {
const button = document.createElement("button");
const outcome = event.outcome || "succeeded";
button.className = `${queue ? "queue-row" : "result-row"} ${outcome}`;
button.dataset.pageId = event.pageId;
const details = document.createElement("span");
const title = document.createElement("strong");
const meta = document.createElement("small");
const score = document.createElement("span");
title.textContent = event.title || "Untitled page";
meta.textContent = outcome === "succeeded"
? `${event.url} · ${event.weakest} · ${Math.round(event.latencyMs || 0)} ms`
: `${event.url} · ${event.error || outcome}`;
score.textContent = outcome === "succeeded" ? Number(event.score).toFixed(1) : outcome[0].toUpperCase() + outcome.slice(1);
score.className = `score ${outcome === "succeeded" ? scoreClass(event.score) : outcome === "skipped" ? "warn" : "bad"}`;
details.append(title, meta);
button.append(details, score);
return button;
}
function renderWorkers() {
ui.workerRail.replaceChildren();
if (discovering) {
const card = document.createElement("article");
const url = document.createElement("strong");
const state = document.createElement("small");
card.className = "worker-card discovery-card";
url.textContent = ui.websiteUrl.value.trim();
state.textContent = "Reading crawl rules and sitemap files";
card.append(url, state);
ui.workerRail.append(card);
ui.activeWorkers.textContent = "Discovery request active";
return;
}
if (!activePages.size) {
const empty = document.createElement("p");
empty.className = "empty-state";
empty.textContent = running ? "Waiting for the next eligible page." : "Worker cards appear here when real audit requests begin.";
ui.workerRail.append(empty);
} else {
activePages.forEach((page) => {
const card = document.createElement("article");
const url = document.createElement("strong");
const state = document.createElement("small");
card.className = "worker-card";
url.textContent = page.url;
state.textContent = "Jev is analyzing this page";
card.append(url, state);
ui.workerRail.append(card);
});
}
ui.activeWorkers.textContent = `${activePages.size} / ${concurrency} active`;
}
function renderMetrics() {
const elapsed = currentElapsed();
const successful = results.filter((result) => result.outcome === "succeeded");
const averageLatency = successful.length
? successful.reduce((sum, result) => sum + Number(result.latencyMs || 0), 0) / successful.length
: 0;
const totalCost = successful.reduce((sum, result) => sum + Number(result.cost || 0), 0);
const candidateSpeed = !mockMode && !discovering && elapsed > 0 ? stats.attempted / elapsed : 0;
const successfulSpeed = !mockMode && !discovering && elapsed > 0 ? stats.succeeded / elapsed : 0;
const rate = mockMode ? 100 : successfulAuditRate() * 100;
const progressValue = !mockMode && !discovering && targetCount > 0 ? Math.min(100, stats.attempted / targetCount * 100) : 0;
ui.progress.textContent = mockMode
? `${results.length} mock`
: discovering
? "Mapping site"
: `${stats.attempted}/${targetCount} · ${stats.succeeded} succeeded · ${stats.skipped} skipped · ${stats.failed} failed`;
ui.elapsed.textContent = `${elapsed.toFixed(1)} s`;
ui.liveSpeed.textContent = `${candidateSpeed.toFixed(2)} pages/s`;
ui.throughput.textContent = `${successfulSpeed.toFixed(2)} successful/s`;
ui.latency.textContent = `${Math.round(averageLatency)} ms`;
ui.cost.textContent = formatMoney(totalCost);
ui.successRate.textContent = `${Math.round(rate)}%`;
ui.successGauge.style.strokeDasharray = `${rate} 100`;
ui.runState.textContent = discovering ? "Discovering pages" : running ? `${speedLabel()} running` : mockMode ? "Mock data" : "Ready";
ui.runStatus.textContent = discovering ? "Discovering permitted pages" : running ? stopping ? "Stopping active requests" : "Jev is analyzing" : statusMessage;
if (!discovering) {
ui.progressBar.style.width = `${progressValue}%`;
ui.progressBar.setAttribute("aria-valuenow", String(Math.round(progressValue)));
}
renderWorkers();
}
function renderFeed() {
ui.feed.replaceChildren(...[...events].reverse().slice(0, 80).map((event) => createResultButton(event)));
}
function renderQueue() {
const ranked = results
.filter((result) => result.outcome === "succeeded")
.sort((a, b) => a.score - b.score)
.slice(0, 30);
ui.reviewQueue.replaceChildren(...ranked.map((result) => createResultButton(result, true)));
}
function renderPrediction() {
const successful = results.filter((result) => result.outcome === "succeeded");
if (mockMode || !successful.length) return;
const counts = successful.reduce((totals, result) => {
totals[result.weakest] = (totals[result.weakest] || 0) + 1;
return totals;
}, {});
const mostCommon = Object.entries(counts).sort((a, b) => b[1] - a[1])[0]?.[0] || "none";
const matched = mostCommon === ui.prediction.value;
ui.predictionResult.textContent = matched
? `Prediction matched: ${mostCommon} was most common.`
: `Predicted ${ui.prediction.value}. Most common was ${mostCommon}.`;
ui.predictionResult.className = `status ${matched ? "success" : ""}`;
}
function renderAll() {
renderMetrics();
renderFeed();
renderQueue();
renderPrediction();
}
function selectPage(pageId) {
const page = pages.find((item) => item.id === pageId);
const result = results.find((item) => item.pageId === pageId && item.outcome === "succeeded");
if (!page || !result) return;
selectedPageId = pageId;
ui.editorTitle.textContent = result.title;
ui.editorUrl.textContent = result.url;
ui.editorScore.textContent = `${Number(result.score).toFixed(1)} / 100`;
ui.pageContent.value = result.content || page.content || "";
ui.editorStatus.textContent = "Changes here stay in this browser session.";
}
async function readJson(response) {
const body = await response.json().catch(() => ({}));
if (!response.ok) {
const error = new Error(body.error?.message || `Request failed with HTTP ${response.status}.`);
error.status = response.status;
error.details = body.error?.details || {};
throw error;
}
return body;
}
async function discoverSite() {
const websiteUrl = ui.websiteUrl.value.trim();
if (!websiteUrl) {
ui.discoveryStatus.textContent = "Enter a public website URL first.";
ui.discoveryStatus.className = "status error";
return;
}
setBusyState("discovering");
finishedElapsed = 0;
startClock();
ui.progressBar.removeAttribute("aria-valuenow");
ui.progressBar.setAttribute("aria-valuetext", "Website discovery in progress");
ui.discoveryStatus.textContent = "Reading crawl rules and sitemap files.";
try {
const response = await fetch("/api/discover", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ websiteUrl })
});
const body = await readJson(response);
site = body;
pages = body.pages.slice(0, 600);
targetCount = pages.length;
mockMode = false;
statusMessage = `${pages.length} permitted pages ready`;
ui.discoveryStatus.textContent = body.notice || `Discovered ${pages.length} permitted pages.`;
ui.discoveryStatus.className = "status success";
} catch (error) {
site = null;
statusMessage = "Discovery failed";
ui.discoveryStatus.textContent = error.message;
ui.discoveryStatus.className = "status error";
} finally {
stopClock();
ui.progressBar.removeAttribute("aria-valuetext");
ui.progressBar.setAttribute("aria-valuenow", "0");
setBusyState("idle");
renderAll();
}
}
async function auditPage(page) {
const controller = new AbortController();
const requestKey = `${page.id}-${stats.attempted + activePages.size + 1}`;
controllers.add(controller);
activePages.set(requestKey, page);
renderWorkers();
let event;
try {
const response = await fetch("/api/audit", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ page }),
signal: controller.signal
});
const body = await readJson(response);
event = { ...body, pageId: page.id, title: body.title || page.title, url: body.url || page.url, outcome: "succeeded" };
stats.succeeded += 1;
sessionSpend += Number(event.cost || 0);
} catch (error) {
const skipped = error.status === 422 && error.details?.reason === "thin_content";
event = {
pageId: page.id,
title: page.title,
url: error.details?.url || page.url,
content: "",
outcome: skipped ? "skipped" : "failed",
error: skipped ? "Thin content did not reach Jev" : error.name === "AbortError" ? "Request stopped" : error.message,
httpStatus: error.status || null,
extractedChars: error.details?.extractedChars ?? null,
cost: 0,
latencyMs: 0,
source: "live_html"
};
if (skipped) stats.skipped += 1;
else stats.failed += 1;
} finally {
stats.attempted += 1;
activePages.delete(requestKey);
controllers.delete(controller);
}
results.push(event);
events.push(event);
renderAll();
}
function shouldAutoStop() {
return runMode === "full" && stats.attempted >= 20 && successfulAuditRate() < 0.5;
}
async function runAudit(mode) {
if (!site || running) return;
runMode = mode;
running = true;
stopping = false;
mockMode = false;
stopReason = "Completed";
results = [];
events = [];
stats = { attempted: 0, succeeded: 0, skipped: 0, failed: 0 };
targetCount = mode === "pilot" ? Math.min(25, pages.length) : pages.length;
statusMessage = mode === "pilot" ? "Pilot running" : "Full Discovery running";
finishedElapsed = 0;
setBusyState("running");
startClock();
renderAll();
let cursor = 0;
while (cursor < pages.length && !stopping) {
if (mode === "pilot" && (stats.succeeded >= 5 || stats.attempted >= 25)) break;
let batchSize = Math.min(concurrency, pages.length - cursor);
if (mode === "pilot") {
batchSize = Math.min(batchSize, 25 - stats.attempted, 5 - stats.succeeded);
} else if (stats.attempted < 20) {
batchSize = Math.min(batchSize, 20 - stats.attempted);
}
if (batchSize <= 0) break;
const batch = pages.slice(cursor, cursor + batchSize);
cursor += batchSize;
await Promise.all(batch.map((page) => auditPage(page)));
// Evaluate the guard only after the active batch settles, then stop before scheduling more work.
if (shouldAutoStop()) {
stopping = true;
stopReason = "Stopped automatically: successful audit rate below 50%";
statusMessage = stopReason;
}
}
stopClock();
running = false;
stopping = false;
if (stopReason === "Completed") {
statusMessage = mode === "pilot"
? stats.succeeded >= 5 ? "Five-page pilot complete" : "Pilot settled below five successes"
: "Full Discovery complete";
}
setBusyState("idle");
renderAll();
showCompletion();
}
function stopActiveRun() {
if (!running) return;
stopping = true;
stopReason = "Stopped manually";
statusMessage = "Stopping active requests";
controllers.forEach((controller) => controller.abort());
renderMetrics();
}
function addSummaryValue(label, value) {
const wrapper = document.createElement("div");
const term = document.createElement("dt");
const detail = document.createElement("dd");
term.textContent = label;
detail.textContent = value;
wrapper.append(term, detail);
ui.completionSummary.append(wrapper);
}
function showCompletion() {
ui.completionSummary.replaceChildren();
addSummaryValue("Result", stopReason);
addSummaryValue("Attempted", String(stats.attempted));
addSummaryValue("Succeeded", String(stats.succeeded));
addSummaryValue("Skipped", String(stats.skipped));
addSummaryValue("Failed", String(stats.failed));
addSummaryValue("Successful audit rate", `${Math.round(successfulAuditRate() * 100)}%`);
addSummaryValue("Elapsed", `${finishedElapsed.toFixed(1)} s`);
addSummaryValue("Cost", formatMoney(results.reduce((sum, result) => sum + Number(result.cost || 0), 0)));
addSummaryValue("Speed", speedLabel());
ui.completionDialog.showModal();
}
async function rescoreDraft() {
const page = pages.find((item) => item.id === selectedPageId);
if (!page) return;
ui.rescore.disabled = true;
ui.editorStatus.textContent = "Jev is re-scoring the local draft.";
try {
const response = await fetch("/api/audit", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ page, draftContent: ui.pageContent.value })
});
const updated = await readJson(response);
const index = results.findIndex((result) => result.pageId === selectedPageId && result.outcome === "succeeded");
if (index >= 0) results[index] = { ...updated, outcome: "succeeded" };
ui.editorScore.textContent = `${Number(updated.score).toFixed(1)} / 100`;
ui.editorStatus.textContent = "The local draft has a new Jev score.";
renderQueue();
} catch (error) {
ui.editorStatus.textContent = error.message;
} finally {
ui.rescore.disabled = false;
}
}
function exportEvidence(format) {
const data = results.map((result) => ({
source_url: result.url,
outcome: result.outcome,
fetched_at: result.fetchedAt || "",
http_status: result.httpStatus ?? "",
methodology: result.source || "",
model: result.model || "",
request_id: result.requestId || "",
latency_ms: result.latencyMs || 0,
confidence: result.confidence ?? "",
support: result.support ?? "",
cost: result.cost || 0,
score: result.score ?? "",
weakest_signal: result.weakest || "",
error: result.error || ""
}));
let content;
let type;
let extension;
if (format === "json") {
content = JSON.stringify(data, null, 2);
type = "application/json";
extension = "json";
} else {
const headers = Object.keys(data[0] || { source_url: "" });
const escapeCell = (value) => `"${String(value).replaceAll('"', '""')}"`;
content = [headers.join(","), ...data.map((row) => headers.map((key) => escapeCell(row[key])).join(","))].join("\n");
type = "text/csv";
extension = "csv";
}
const link = document.createElement("a");
link.href = URL.createObjectURL(new Blob([content], { type }));
link.download = `jev-geo-audit.${extension}`;
link.click();
URL.revokeObjectURL(link.href);
}
ui.modeButtons.forEach((button) => {
button.addEventListener("click", () => {
concurrency = Number(button.dataset.concurrency);
ui.modeButtons.forEach((item) => {
const active = item === button;
item.classList.toggle("active", active);
item.setAttribute("aria-pressed", String(active));
});
renderMetrics();
});
});
ui.discoverSite.addEventListener("click", discoverSite);
ui.runPilot.addEventListener("click", () => runAudit("pilot"));
ui.runFull.addEventListener("click", () => {
ui.dialogCandidates.textContent = String(pages.length);
ui.dialogCost.textContent = formatMoney(pages.length / 1000 * 0.01);
ui.dialogBudget.textContent = formatMoney(Math.max(0, 6 - sessionSpend));
ui.dialogSpeed.textContent = speedLabel();
ui.fullRunDialog.showModal();
});
ui.confirmFull.addEventListener("click", () => {
ui.fullRunDialog.close();
runAudit("full");
});
ui.stopRun.addEventListener("click", stopActiveRun);
ui.feed.addEventListener("click", (event) => selectPage(event.target.closest("[data-page-id]")?.dataset.pageId));
ui.reviewQueue.addEventListener("click", (event) => selectPage(event.target.closest("[data-page-id]")?.dataset.pageId));
ui.saveText.addEventListener("click", () => {
ui.editorStatus.textContent = "Local draft saved for this browser session.";
});
ui.rescore.addEventListener("click", rescoreDraft);
ui.exportCsv.addEventListener("click", () => exportEvidence("csv"));
ui.exportJson.addEventListener("click", () => exportEvidence("json"));
ui.fullscreenBtn.addEventListener("click", async () => {
try {
if (document.fullscreenElement) await document.exitFullscreen();
else if (document.documentElement.requestFullscreen) await document.documentElement.requestFullscreen();
else throw new Error("Full screen is not supported in this browser.");
} catch (error) {
statusMessage = error.message;
renderMetrics();
}
});
document.addEventListener("fullscreenchange", () => {
const active = Boolean(document.fullscreenElement);
ui.fullscreenBtn.textContent = active ? "Exit full screen" : "Enter full screen";
ui.fullscreenBtn.setAttribute("aria-pressed", String(active));
});
results = MOCK_PAGES.map(mockResult);
events = [...results];
renderAll();
- Review Claude Code's completion summary.
- Confirm that it names index.html, styles.css, and app.js.
Add and run the local server
The final two files make the bundle runnable. The mock server serves local assets and deliberately rejects live audit requests until the secure proxy is added in the next step.
- Ask Claude Code in the same IDE panel to create the server and test files by pasting this prompt:
Create server.py and smoke_test.py in this repository.
Match the complete file references from my project instructions.
server.py must bind ThreadingHTTPServer to 127.0.0.1:8000.
It must serve local files and return status 501 for POST /api/discover and POST /api/audit.
smoke_test.py must verify all nine required files.
It must verify the HTML links styles.css and app.js.
It must verify the discovery, pilot, Full Discovery, manual stop, fullscreen, activity, worker, progress, successful-rate gauge, feed, queue, editor, paid confirmation, and completion elements.
It must verify that the HTML does not offer Continue Full Discovery after a threshold stop.
It must verify the browser logic contains activePages, AbortController, requestFullscreen, exitFullscreen, fullscreenchange, and an automatic stop after at least 20 attempts when succeeded divided by attempted is below 50 percent.
It must verify that skipped and failed outcomes lower the successful audit rate.
It must verify that in-flight requests settle before the automatic-stop guard is evaluated and no new batch starts after the guard triggers.
It must verify the discovery logic contains is-discovering, Discovering..., requestAnimationFrame, cancelAnimationFrame, aria-valuetext, and Reading crawl rules and sitemap files.
It must verify the stylesheet contains the 100dvh desktop shell, is-running and is-discovering activity states, moving progress and worker lights, the rate gauge, internal scrolling, responsive fallback, and prefers-reduced-motion handling.
It must reject real credential patterns and server-only environment names in browser files.
It must verify .env is ignored and .env.example uses the placeholder only.
After writing both files, report the test count.
✔️ My mock server matches
Your local server serves the complete dashboard while keeping live audit requests disabled.
ⓧ I'd like to double check the full code
import json
from http.server import SimpleHTTPRequestHandler, ThreadingHTTPServer
# Keep the learning server local to this computer.
HOST = "127.0.0.1"
PORT = 8000
class MockHandler(SimpleHTTPRequestHandler):
"""Serve the mock dashboard without fetching websites or forwarding audits."""
def do_POST(self):
# Leave discovery and live auditing disabled until the secure proxy is added.
if self.path in {"/api/discover", "/api/audit"}:
payload = json.dumps({
"error": "Complete the secure website proxy before using this live action."
}).encode("utf-8")
self.send_response(501)
self.send_header("Content-Type", "application/json; charset=utf-8")
self.send_header("Content-Length", str(len(payload)))
self.end_headers()
self.wfile.write(payload)
return
self.send_error(404, "Not found")
if __name__ == "__main__":
server = ThreadingHTTPServer((HOST, PORT), MockHandler)
print(f"Jev GEO Audit mock running at http://{HOST}:{PORT}")
try:
server.serve_forever()
except KeyboardInterrupt:
print("\nStopping server.")
finally:
server.server_close()
✔️ My smoke test matches
Your nine tests now check the complete bundle, the browser credential boundary, Full Discovery naming, the automatic 50-percent stop, the successful audit rate arc, and the live processing states before the server runs.
ⓧ I'd like to double check the full code
import unittest
from pathlib import Path
# Verify the repository boundary and command-center contract before a credential exists.
ROOT = Path(__file__).parent
REQUIRED_FILES = {
"README.md", "CLAUDE.md", ".gitignore", ".env.example",
"index.html", "styles.css", "app.js", "server.py", "smoke_test.py"
}
BROWSER_FILES = ("index.html", "styles.css", "app.js")
class ProjectRepositoryTests(unittest.TestCase):
def test_required_files_exist(self):
missing = sorted(name for name in REQUIRED_FILES if not (ROOT / name).is_file())
self.assertEqual(missing, [], f"Missing project files: {missing}")
def test_browser_assets_are_linked(self):
html = (ROOT / "index.html").read_text(encoding="utf-8")
self.assertIn('href="styles.css"', html)
self.assertIn('src="app.js"', html)
def test_command_center_elements_exist(self):
html = (ROOT / "index.html").read_text(encoding="utf-8")
required_ids = (
"appShell", "websiteUrl", "discoverSite", "runPilot", "runFull", "stopRun",
"fullscreenBtn", "activityPanel", "workerRail", "runStatus", "activeWorkers",
"progressBar", "liveSpeed", "throughput", "successGauge", "successRate",
"feed", "reviewQueue", "pageContent", "fullRunDialog", "completionDialog",
"completionSummary"
)
for element_id in required_ids:
self.assertIn(f'id="{element_id}"', html)
self.assertIn("Run Full Discovery", html)
self.assertNotIn("Continue Full Discovery", html)
self.assertNotIn('id="skipWarningDialog"', html)
def test_live_worker_fullscreen_and_automatic_stop_logic_exist(self):
source = (ROOT / "app.js").read_text(encoding="utf-8")
for required_text in (
"activePages", "AbortController", "requestFullscreen", "exitFullscreen",
"fullscreenchange", "aria-busy", "await Promise.all(batch.map",
"function shouldAutoStop()", "stats.attempted >= 20",
"successfulAuditRate() < 0.5",
"Stopped automatically: successful audit rate below 50%"
):
self.assertIn(required_text, source)
self.assertNotIn("Continue Full Discovery", source)
def test_discovery_processing_contract_exists(self):
source = (ROOT / "app.js").read_text(encoding="utf-8")
styles = (ROOT / "styles.css").read_text(encoding="utf-8")
for required_text in (
"is-discovering", "Discovering...", "requestAnimationFrame", "cancelAnimationFrame",
"aria-valuetext", "Reading crawl rules and sitemap files"
):
self.assertIn(required_text, source)
for required_text in (
".activity-panel.is-discovering", "discovery-border-sweep", "discovery-progress"
):
self.assertIn(required_text, styles)
def test_viewport_motion_gauge_and_scroll_contract_exist(self):
styles = (ROOT / "styles.css").read_text(encoding="utf-8")
for required_text in (
"100dvh", ".activity-panel.is-running", ".activity-panel.is-discovering",
".scroll-region", "overflow: hidden", "max-width: 1100px", "prefers-reduced-motion",
"progress-light", "worker-orbit", ".gauge-value"
):
self.assertIn(required_text, styles)
def test_browser_files_contain_no_real_key_reference(self):
source = "\n".join((ROOT / name).read_text(encoding="utf-8") for name in BROWSER_FILES)
self.assertNotIn("OPENROUTER_API_KEY", source)
self.assertNotIn("sk-or-v1-", source)
def test_local_secret_is_ignored(self):
ignored = (ROOT / ".gitignore").read_text(encoding="utf-8").splitlines()
self.assertIn(".env", ignored)
def test_example_uses_only_the_placeholder(self):
example = (ROOT / ".env.example").read_text(encoding="utf-8").strip()
self.assertEqual(example, "OPENROUTER_API_KEY=your-api-key-here")
if __name__ == "__main__":
unittest.main()
- Keep the Claude Code panel open for later edits.
- Click Terminal in the IDE's top menu. Click New Terminal.
The integrated terminal should open inside jev-geo-audit. Run the smoke test for your operating system before opening the dashboard.
macOS
- Run the complete project checks on macOS with this command:
python3 smoke_test.py
You should see nine tests finish with OK.
Windows
- Run the complete project checks on Windows with this command:
py smoke_test.py
You should see nine tests finish with OK.
Linux
- Run the complete project checks on Linux with this command:
python3 smoke_test.py
You should see nine tests finish with OK.
Smoke test failed?
The failing test names the file or safety rule that differs. Replace that file with its complete reference above, then run the test again.
If a check still fails, help me diagnose the complete project bundle.
The mock server occupies its terminal while it runs. Keep that terminal open during the browser checks.
macOS
- Start the complete mock dashboard on macOS by running this command:
python3 server.py
You should see Jev GEO Audit mock running at http://127.0.0.1:8000.
Windows
- Start the complete mock dashboard on Windows by running this command:
py server.py
You should see Jev GEO Audit mock running at http://127.0.0.1:8000.
Linux
- Start the complete mock dashboard on Linux by running this command:
python3 server.py
You should see Jev GEO Audit mock running at http://127.0.0.1:8000.
- Ctrl + shift + click on http://127.0.0.1:8000 to open browser.
You should see the GEO Citability Audit with a Website URL field plus Sequential, Fast x8, and Turbo x24 controls. Sequential should be selected, the progress card should show 12 mock, and the successful audit rate arc should show 100% while the live run buttons remain disabled.
You now have every project file and a working mock dashboard with real-site controls. Next, you will secure the credential, activate public-page discovery, and audit five pages from an authorised website.
Secure the Key and Run the Pilot
Your mock dashboard exposed the fake key because every browser-delivered value is visible. A real OpenRouter key belongs behind a local server boundary.
In this step, you will activate a threaded Python proxy that discovers pages through robots.txt and XML sitemaps. You will then run a five-page Jev pilot against public HTML from a website you are authorised to test.
In this step, get ready to:
- Store the OpenRouter key in an ignored local file.
- Activate safe sitemap discovery and public HTML extraction.
- Audit five real pages from an authorised website.
Protect the key behind a local boundary
The ignored .env file gives the local server access to your credential. The browser files never receive its value.
Keep the credential local
This action handles a live credential. Enter the key only in your local .env file.
Keep the key out of Claude Code prompts. Keep it out of screenshots.
- Return to Claude Code session from earlier.
- Paste this prompt to create the protected local file:
- Approve changes to .env or .gitignore if Claude Code asks for permission.
Inside the jev-geo-audit folder, check that .gitignore contains .env on its own first line. If it is missing, add it without removing the existing entries.
Copy .env.example to a new file named .env in the same folder. Do not ask for or add my OpenRouter key. Do not display the contents of .env after I add the key. Make no other changes.
When finished, tell me the paths of the files you created or checked.
- Confirm .env appears beside .env.example in the /jev-geo-audit file tree.
Your protected local file is ready. Before you paste a credential into it, choose the path that matches your OpenRouter setup.
✔️ I already have an OpenRouter key
Keep your existing key off-screen. You will paste it directly into .env in the next instruction.
ⓧ I need an account or API key
An OpenRouter account is free to create. Jev uses paid credits, so you may need to fund the account before running the pilot.
Check the cost before adding credits
OpenRouter gives new users a small testing allowance, but it does not state the amount. If you need more, the minimum credit purchase is $5, with a minimum $0.80 non-crypto purchase fee before tax.
A $1 limit on this project key stops the audit from using the rest of your account balance.
- Open the OpenRouter API Keys page.
- Select Sign up below the sign-in form if you do not have an account.
Return to the OpenRouter API Keys page.
- Start creating a new API key.
- Enter jev-geo-audit as the key name.
- Set the key's credit limit to $1.
- Finish creating the key.
- Copy the displayed key once, then keep it out of screenshots and Claude Code prompts.
OpenRouter shows the plaintext key only when it is created. Continue directly to the local .env instructions below so the credential does not sit in notes or chat history.
- Select .env in your local editor's project file tree.
- Replace your-api-key-here with your real project-specific OpenRouter key.
- Save .env.
- Close the .env tab to move the credential off-screen.
- Open .gitignore from the same project file tree.
- Confirm its first line is .env.
Is the key file missing?
Make sure Claude Code created .env inside jev-geo-audit. A file in another folder cannot be loaded by this project.
If the file still does not appear, help me locate my local key file.
Activate safe website discovery
The server finds pages the website permits and extracts their text. It sends that text to Jev while your API key stays private.
What makes the crawler safe?
- Only public HTTP and HTTPS URLs on standard web ports are allowed.
- Redirects must stay on the authorised website.
- The crawler follows robots.txt, crawl delays, and response-size limits.
- Choose a public website you own or have permission to audit.
- Use the web-scraping.dev test site if you do not have an authorised website.
- Paste the prompt below into Claude Code.
- Approve the change to server.py if Claude Code asks for permission.
Inside the jev-geo-audit folder, replace server.py with the supplied server.py reference. Do not edit any other file.
Review index.html, app.js, and CLAUDE.md before editing. If the supplied reference is unavailable, stop and ask me for it.
Requirements:
- Serve the dashboard on 127.0.0.1:8000 with ThreadingHTTPServer.
- Load OPENROUTER_API_KEY from .env. Never print, return, modify, or request the key.
- Make POST /api/discover return websiteUrl, robotsUrl, sitemapUrls, crawlDelay, pages, and notice.
- Discover XML sitemap indexes, URL sets, and compressed sitemaps, capped at 600 pages.
- After optional gzip expansion, remove leading whitespace from sitemap bytes before XML parsing.
- Keep public-host validation, same-host redirects, robots.txt rules, crawl delays, and response-size limits.
- Make POST /api/audit accept page with id, title, and url plus draftContent for local re-scoring.
- Extract visible HTML, meta descriptions, and application/ld+json data.
- Return status 422 for fewer than 80 extracted characters.
- Include reason thin_content, url, and extractedChars in that error's details.
- Send eligible text to POST https://openrouter.ai/api/alpha/decisions with model typesafe/jev-1.13.
- Copy the exact QUESTIONS object and response fields from the supplied reference.
- Retry temporary OpenRouter failures for no more than three total attempts.
- Use only the Python standard library.
After editing, check server.py for syntax errors. Summarize the changes without reading .env. Do not start the server, make a network request, or call the paid API.
- Compare the generated server.py with the supplied reference under I'd like to double check the full code.
- Replace any differences with the supplied server.py reference before continuing.
✔️ Awesome, I've got everything!
Your proxy matches the supplied implementation. Save server.py before starting the pilot.
ⓧ I'd like to double check the full code
import gzip
import hashlib
import io
import ipaddress
import json
import os
import socket
import threading
import time
import xml.etree.ElementTree as ET
from datetime import datetime, timezone
from html.parser import HTMLParser
from http.server import SimpleHTTPRequestHandler, ThreadingHTTPServer
from urllib.error import HTTPError, URLError
from urllib.parse import unquote, urlsplit, urlunsplit
from urllib.request import HTTPRedirectHandler, ProxyHandler, Request, build_opener
from urllib.robotparser import RobotFileParser
HOST, PORT = "127.0.0.1", 8000
API_URL = "https://openrouter.ai/api/alpha/decisions"
MODEL_ID = "typesafe/jev-1.13"
USER_AGENT = "NextWork-GEO-Audit/1.0"
MAX_ATTEMPTS = 3
MAX_PAGES = 600
MAX_SITEMAPS = 50
MAX_SITEMAP_BYTES = 2_000_000
MAX_HTML_BYTES = 1_000_000
MAX_TEXT_CHARS = 20_000
SITE_POLICIES = {}
FETCH_SLOTS = {}
FETCH_LOCK = threading.Lock()
QUESTIONS = {
"readiness": {
"type": "score",
"instructions": "How ready is `page.content` to be quoted as a concise answer? Judge only the supplied text.",
"criteria": [
"Vague or promotional; no direct answer.",
"A topic is present, but the answer is hard to isolate.",
"A usable answer exists but lacks support or structure.",
"A direct answer is supported and easy to extract.",
"A self-contained answer has specific support, transparent sourcing, and clear structure."
]
},
"weakest_signal": {
"type": "choice",
"instructions": "Which single citability signal is weakest in `page.content`?",
"criteria": {
"answer_clarity": "The answer is indirect, buried, or ambiguous.",
"evidence_specificity": "Claims lack concrete, checkable support.",
"source_transparency": "The basis of important claims is unclear.",
"extractable_structure": "The answer is difficult to quote by itself.",
"none": "No meaningful weakness is present."
}
},
"verifiable_support": {
"type": "noul",
"instructions": "Does `page.content` contain at least one concrete supporting detail a reviewer could verify?",
"criteria": {
"true": "A source, measurement, dated fact, example, or checkable detail supports the answer.",
"false": "The text relies on broad or unsupported claims."
}
}
}
class ApiProblem(Exception):
def __init__(self, status, message, details=None):
super().__init__(message)
self.status, self.message, self.details = status, message, details
class SafeRedirectHandler(HTTPRedirectHandler):
"""Reject redirects that leave the authorised public host."""
def __init__(self, expected_host):
super().__init__()
self.expected_host = expected_host
def redirect_request(self, req, fp, code, msg, headers, newurl):
validate_public_url(newurl, self.expected_host)
return super().redirect_request(req, fp, code, msg, headers, newurl)
class ReadableHTMLParser(HTMLParser):
"""Collect visible text plus readable metadata from server HTML."""
SKIP_TAGS = {"style", "noscript", "svg", "template"}
BREAK_TAGS = {"article", "br", "div", "footer", "h1", "h2", "h3", "h4", "header", "li", "main", "p", "section"}
META_DESCRIPTION_KEYS = {"description", "og:description", "twitter:description"}
JSON_TEXT_KEYS = {"articlebody", "description", "headline", "name", "text"}
def __init__(self):
super().__init__(convert_charrefs=True)
self.skip_stack = []
self.in_title = False
self.in_json_ld = False
self.current_json = []
self.json_documents = []
self.title_parts = []
self.text_parts = []
self.meta_parts = []
def handle_starttag(self, tag, attrs):
tag = tag.lower()
attributes = {str(key).lower(): value for key, value in attrs if key and value is not None}
if self.skip_stack:
if tag == "script" or tag in self.SKIP_TAGS:
self.skip_stack.append(tag)
return
if tag == "script":
script_type = attributes.get("type", "").split(";", 1)[0].strip().lower()
if script_type == "application/ld+json":
self.in_json_ld = True
self.current_json = []
else:
self.skip_stack.append(tag)
return
if tag in self.SKIP_TAGS:
self.skip_stack.append(tag)
return
if tag == "meta":
key = (attributes.get("name") or attributes.get("property") or "").strip().lower()
content = attributes.get("content", "").strip()
if key in self.META_DESCRIPTION_KEYS and content:
self.meta_parts.append(content)
elif tag == "title":
self.in_title = True
elif tag in self.BREAK_TAGS:
self.text_parts.append("\n")
def handle_endtag(self, tag):
tag = tag.lower()
if self.in_json_ld and tag == "script":
self.json_documents.append("".join(self.current_json))
self.current_json = []
self.in_json_ld = False
return
if self.skip_stack:
if tag == self.skip_stack[-1]:
self.skip_stack.pop()
return
if tag == "title":
self.in_title = False
elif tag in self.BREAK_TAGS:
self.text_parts.append("\n")
def handle_data(self, data):
if self.in_json_ld:
self.current_json.append(data)
return
if self.skip_stack:
return
cleaned = " ".join(data.split())
if not cleaned:
return
if self.in_title:
self.title_parts.append(cleaned)
self.text_parts.append(cleaned)
def append_json_value(self, value, output):
if isinstance(value, str):
output.append(value)
elif isinstance(value, list):
for item in value:
if isinstance(item, str):
output.append(item)
elif isinstance(item, (dict, list)):
self.collect_json_text(item, output)
elif isinstance(value, dict):
self.collect_json_text(value, output)
def collect_json_text(self, value, output):
if isinstance(value, dict):
for key, item in value.items():
if str(key).lower() in self.JSON_TEXT_KEYS:
self.append_json_value(item, output)
elif isinstance(item, (dict, list)):
self.collect_json_text(item, output)
elif isinstance(value, list):
for item in value:
self.collect_json_text(item, output)
def result(self):
title = " ".join(self.title_parts).strip()
pieces = [line for line in " ".join(self.text_parts).splitlines() if line.strip()]
pieces.extend(self.meta_parts)
for document in self.json_documents:
try:
structured = json.loads(document)
except json.JSONDecodeError:
continue
self.collect_json_text(structured, pieces)
lines = []
seen = set()
for piece in pieces:
cleaned = " ".join(str(piece).split())
if cleaned and cleaned not in seen:
seen.add(cleaned)
lines.append(cleaned)
return title, "\n".join(lines)[:MAX_TEXT_CHARS]
def load_env(path=".env"):
if not os.path.isfile(path):
return
with open(path, encoding="utf-8") as env_file:
for raw_line in env_file:
line = raw_line.strip()
if not line or line.startswith("#") or "=" not in line:
continue
key, value = line.split("=", 1)
os.environ.setdefault(key.strip(), value.strip().strip('"').strip("'"))
def utc_now():
return datetime.now(timezone.utc).isoformat()
def canonical_host(host):
host = (host or "").lower().rstrip(".")
return host[4:] if host.startswith("www.") else host
def validate_public_url(url, expected_host=None):
parts = urlsplit(url.strip())
if parts.scheme not in {"http", "https"} or not parts.hostname:
raise ApiProblem(400, "Use a complete public HTTP or HTTPS URL.")
if parts.username or parts.password:
raise ApiProblem(400, "Credentials are not allowed in website URLs.")
try:
port = parts.port or (443 if parts.scheme == "https" else 80)
except ValueError as error:
raise ApiProblem(400, "The website URL contains an invalid port.") from error
if port not in {80, 443}:
raise ApiProblem(400, "Only standard public web ports 80 and 443 are allowed.")
host = canonical_host(parts.hostname)
if expected_host and host != canonical_host(expected_host):
raise ApiProblem(400, "The website redirected outside the authorised host.")
try:
answers = socket.getaddrinfo(parts.hostname, port, type=socket.SOCK_STREAM)
except socket.gaierror as error:
raise ApiProblem(400, "The website hostname could not be resolved.") from error
for answer in answers:
address = ipaddress.ip_address(answer[4][0].split("%")[0])
if not address.is_global:
raise ApiProblem(400, "Private, local, reserved, and link-local addresses are blocked.")
return urlunsplit((parts.scheme, parts.netloc, parts.path or "/", parts.query, ""))
def fetch_resource(url, expected_host, max_bytes, allowed_types=None):
safe_url = validate_public_url(url, expected_host)
opener = build_opener(ProxyHandler({}), SafeRedirectHandler(expected_host))
request = Request(safe_url, headers={"User-Agent": USER_AGENT, "Accept": "text/html,application/xml,text/xml,text/plain;q=0.9,*/*;q=0.1"})
try:
with opener.open(request, timeout=20) as response:
final_url = validate_public_url(response.geturl(), expected_host)
content_type = response.headers.get_content_type().lower()
if allowed_types and content_type not in allowed_types:
raise ApiProblem(415, f"Unsupported response type: {content_type}.")
data = response.read(max_bytes + 1)
if len(data) > max_bytes:
raise ApiProblem(413, "The remote response exceeded the project safety limit.")
charset = response.headers.get_content_charset() or "utf-8"
return data, final_url, response.status, content_type, charset
except HTTPError as error:
raise ApiProblem(error.code, f"The website returned HTTP {error.code} for {safe_url}.") from error
except URLError as error:
raise ApiProblem(502, f"The website request failed: {error.reason}.") from error
def load_policy(website_url):
value = website_url.strip()
if "://" not in value:
value = f"https://{value}"
root_url = validate_public_url(value)
parts = urlsplit(root_url)
origin = f"{parts.scheme}://{parts.netloc}"
host = canonical_host(parts.hostname)
robots_url = f"{origin}/robots.txt"
parser = RobotFileParser()
parser.set_url(robots_url)
try:
raw, robots_url, _, _, charset = fetch_resource(robots_url, host, 512_000)
parser.parse(raw.decode(charset, errors="replace").splitlines())
except ApiProblem as error:
if error.status != 404:
raise ApiProblem(502, "The crawler could not safely read robots.txt.") from error
parser.parse([])
delay = parser.crawl_delay(USER_AGENT) or parser.crawl_delay("*") or 0.5
delay = max(0.5, min(float(delay), 10.0))
policy = {"host": host, "origin": origin, "robotsUrl": robots_url, "parser": parser, "delay": delay}
SITE_POLICIES[host] = policy
return root_url, policy
def read_gzip_limited(data, limit):
with gzip.GzipFile(fileobj=io.BytesIO(data)) as stream:
expanded = stream.read(limit + 1)
if len(expanded) > limit:
raise ApiProblem(413, "The expanded sitemap exceeded the project safety limit.")
return expanded
def element_name(element):
return element.tag.rsplit("}", 1)[-1].lower()
def child_locations(root):
return [child.text.strip() for child in root.iter() if element_name(child) == "loc" and child.text and child.text.strip()]
def page_label(url):
path = unquote(urlsplit(url).path).strip("/")
leaf = path.rsplit("/", 1)[-1] if path else "Homepage"
return leaf.replace("-", " ").replace("_", " ").strip().title() or "Homepage"
def page_id(url):
return hashlib.sha256(url.encode("utf-8")).hexdigest()[:16]
def discover_pages(root_url, policy):
parser = policy["parser"]
sitemap_queue = parser.site_maps() or [f"{policy['origin']}/sitemap.xml"]
sitemap_queue = [validate_public_url(url, policy["host"]) for url in sitemap_queue]
seen_sitemaps = set()
seen_pages = set()
pages = []
used_sitemaps = []
sitemap_errors = 0
while sitemap_queue and len(seen_sitemaps) < MAX_SITEMAPS and len(pages) < MAX_PAGES:
sitemap_url = sitemap_queue.pop(0)
if sitemap_url in seen_sitemaps:
continue
seen_sitemaps.add(sitemap_url)
try:
raw, final_url, _, _, _ = fetch_resource(sitemap_url, policy["host"], MAX_SITEMAP_BYTES)
if final_url.endswith(".gz") or raw.startswith(b"\x1f\x8b"):
raw = read_gzip_limited(raw, MAX_SITEMAP_BYTES)
# Some valid sitemap responses place whitespace before the XML declaration.
root = ET.fromstring(raw.lstrip())
except (ApiProblem, ET.ParseError):
sitemap_errors += 1
continue
used_sitemaps.append(final_url)
locations = child_locations(root)
if element_name(root) == "sitemapindex":
for location in locations:
if len(seen_sitemaps) + len(sitemap_queue) >= MAX_SITEMAPS:
break
try:
sitemap_queue.append(validate_public_url(location, policy["host"]))
except ApiProblem:
continue
continue
if element_name(root) != "urlset":
continue
for location in locations:
if len(pages) >= MAX_PAGES:
break
try:
page_url = validate_public_url(location, policy["host"])
except ApiProblem:
continue
if page_url in seen_pages or not parser.can_fetch(USER_AGENT, page_url):
continue
seen_pages.add(page_url)
pages.append({"id": page_id(page_url), "title": page_label(page_url), "url": page_url})
if not pages and parser.can_fetch(USER_AGENT, root_url):
pages.append({"id": page_id(root_url), "title": page_label(root_url), "url": root_url})
notice = "No readable sitemap was found, so the audit contains the authorised homepage only."
elif not pages:
raise ApiProblem(403, "robots.txt does not permit this crawler to fetch the supplied website.")
else:
notice = f"Discovered {len(pages)} permitted public pages from {len(used_sitemaps)} sitemap file(s)."
if sitemap_errors:
notice += f" Skipped {sitemap_errors} unreadable sitemap file(s)."
return pages, used_sitemaps, notice
def wait_for_fetch_slot(policy):
host = policy["host"]
with FETCH_LOCK:
now = time.monotonic()
reserved = max(now, FETCH_SLOTS.get(host, now))
FETCH_SLOTS[host] = reserved + policy["delay"]
wait = reserved - now
if wait > 0:
time.sleep(wait)
def extract_page(page, policy):
if not policy["parser"].can_fetch(USER_AGENT, page["url"]):
raise ApiProblem(403, "robots.txt does not permit this page to be fetched.")
wait_for_fetch_slot(policy)
raw, final_url, status, _, charset = fetch_resource(
page["url"], policy["host"], MAX_HTML_BYTES, {"text/html", "application/xhtml+xml"}
)
parser = ReadableHTMLParser()
parser.feed(raw.decode(charset, errors="replace"))
title, content = parser.result()
if len(content) < 80:
raise ApiProblem(
422,
f"{final_url} exposed only {len(content)} extracted characters. At least 80 are required for an audit.",
{"reason": "thin_content", "url": final_url, "extractedChars": len(content)}
)
return {
"id": page["id"],
"title": title or page["title"],
"url": final_url,
"content": content,
"fetchedAt": utc_now(),
"httpStatus": status,
"source": "live_html"
}
def decode_json(raw):
try:
return json.loads(raw)
except json.JSONDecodeError:
return {"error": {"message": raw or "Unreadable API response"}}
def retry_delay(headers, attempt):
value = headers.get("Retry-After") if headers else None
try:
return min(float(value), 10) if value and float(value) > 0 else min(2 ** attempt, 4)
except ValueError:
return min(2 ** attempt, 4)
def call_jev(page):
key = os.getenv("OPENROUTER_API_KEY")
if not key or key == "your-api-key-here":
raise ApiProblem(500, "Add OPENROUTER_API_KEY to .env before starting the server.")
data = json.dumps({"model": MODEL_ID, "state": {"page": page}, "questions": QUESTIONS}).encode("utf-8")
request = Request(API_URL, data=data, headers={
"Authorization": f"Bearer {key}", "Content-Type": "application/json"
}, method="POST")
for attempt in range(MAX_ATTEMPTS):
try:
with build_opener(ProxyHandler({})).open(request, timeout=60) as response:
body = decode_json(response.read().decode("utf-8"))
if body.get("error"):
raise ApiProblem(502, body["error"].get("message", "OpenRouter returned an error."), body)
return body
except HTTPError as error:
body = decode_json(error.read().decode("utf-8", errors="replace"))
source = body.get("error", {}).get("metadata", {}).get("limit_source")
transient = error.code == 429 or error.code >= 500 or (error.code == 402 and source == "openrouter_in_flight_budget")
if transient and attempt < MAX_ATTEMPTS - 1:
time.sleep(retry_delay(error.headers, attempt))
continue
raise ApiProblem(error.code, body.get("error", {}).get("message", str(error)), body) from error
except URLError as error:
if attempt < MAX_ATTEMPTS - 1:
time.sleep(2 ** attempt)
continue
raise ApiProblem(502, f"Network error: {error.reason}") from error
raise ApiProblem(502, "The Jev request did not complete.")
def normalize(page, raw, elapsed):
answers, usage = raw.get("answers", {}), raw.get("usage", {})
ready = answers.get("readiness", {})
weak = answers.get("weakest_signal", {})
support = answers.get("verifiable_support", {})
checks = [
ready.get("type") == "score" and isinstance(ready.get("score"), (int, float)),
weak.get("type") == "choice" and isinstance(weak.get("choice"), str),
support.get("type") == "noul" and isinstance(support.get("noul"), (int, float)),
isinstance(ready.get("confidence"), (int, float)),
isinstance(weak.get("confidence"), (int, float)),
isinstance(usage.get("cost"), (int, float))
]
if not all(checks):
raise ApiProblem(502, "Jev returned an unexpected answer shape.", raw)
score = round(ready["score"] / 4 * 70 + support["noul"] * 30, 1)
return {
"pageId": page["id"], "title": page["title"], "url": page["url"], "content": page["content"],
"fetchedAt": page["fetchedAt"], "httpStatus": page["httpStatus"], "source": page["source"],
"score": score, "readiness": ready["score"], "confidence": ready["confidence"],
"weakest": weak["choice"], "weakestConfidence": weak["confidence"],
"support": support["noul"], "cost": usage["cost"], "latencyMs": round(elapsed),
"model": raw.get("model", MODEL_ID), "requestId": raw.get("id", "unavailable")
}
def read_request(handler):
length = int(handler.headers.get("Content-Length", "0"))
if length <= 0 or length > 200_000:
raise ApiProblem(400, "Request body must be between 1 and 200000 bytes.")
try:
return json.loads(handler.rfile.read(length))
except json.JSONDecodeError as error:
raise ApiProblem(400, "Request body must be valid JSON.") from error
class AuditHandler(SimpleHTTPRequestHandler):
def send_json(self, status, payload):
encoded = json.dumps(payload).encode("utf-8")
self.send_response(status)
self.send_header("Content-Type", "application/json; charset=utf-8")
self.send_header("Content-Length", str(len(encoded)))
self.end_headers()
self.wfile.write(encoded)
def do_POST(self):
try:
body = read_request(self)
if self.path == "/api/discover":
website_url = body.get("websiteUrl")
if not isinstance(website_url, str) or not website_url.strip():
raise ApiProblem(400, "Enter a public website URL.")
root_url, policy = load_policy(website_url)
pages, sitemaps, notice = discover_pages(root_url, policy)
self.send_json(200, {
"websiteUrl": root_url, "robotsUrl": policy["robotsUrl"], "sitemapUrls": sitemaps,
"crawlDelay": policy["delay"], "pages": pages, "notice": notice
})
return
if self.path == "/api/audit":
page = body.get("page")
if not isinstance(page, dict) or any(not isinstance(page.get(field), str) or not page[field].strip() for field in ("id", "title", "url")):
raise ApiProblem(400, "Request must contain page.id, page.title, and page.url.")
draft = body.get("draftContent")
started = time.perf_counter()
if draft is None:
safe_url = validate_public_url(page["url"])
host = canonical_host(urlsplit(safe_url).hostname)
policy = SITE_POLICIES.get(host)
if not policy:
raise ApiProblem(400, "Discover this website before auditing its pages.")
audit_page = extract_page({**page, "url": safe_url}, policy)
else:
if not isinstance(draft, str) or not 80 <= len(draft.strip()) <= MAX_TEXT_CHARS:
raise ApiProblem(400, "Draft content must contain between 80 and 20000 characters.")
audit_page = {
**page, "content": draft.strip(), "fetchedAt": utc_now(),
"httpStatus": None, "source": "local_draft"
}
result = normalize(audit_page, call_jev(audit_page), (time.perf_counter() - started) * 1000)
self.send_json(200, result)
return
raise ApiProblem(404, "Not found")
except ApiProblem as error:
self.send_json(error.status, {"error": {"message": error.message, "details": error.details or {}}})
except Exception as error:
self.send_json(500, {"error": {"message": f"Unexpected server error: {error}", "details": {}}})
if __name__ == "__main__":
load_env()
server = ThreadingHTTPServer((HOST, PORT), AuditHandler)
print(f"Jev GEO Audit running at http://{HOST}:{PORT}")
try:
server.serve_forever()
except KeyboardInterrupt:
print("\nStopping server.")
finally:
server.server_close()
What does this server do?
- The URL checks allow only public HTTP and HTTPS destinations on standard web ports.
- The robots parser discovers sitemap locations and rejects disallowed pages.
- The sitemap reader supports URL sets, sitemap indexes, and compressed sitemap files.
- The HTML parser combines visible body text with meta descriptions and readable JSON-LD fields.
- The parser excludes non-JSON scripts and layout code before deduplicating the extracted text.
- The server spaces website fetches before concurrent Jev analysis begins.
- The QUESTIONS contract still measures readiness, weakest signal, and verifiable support.
- Local draft re-scoring reuses copied content without changing the live website.
- Save server.py.
- Confirm server.py appears inside jev-geo-audit.
Does your server code differ?
Check the constants against the reference. Check the function names next.
If the files still differ, help me compare my server with the supplied proxy.
Run and inspect the five-page pilot
The browser sends the website URL to /api/discover. Python checks the site's crawl rules, finds sitemap URLs, and returns up to 600 permitted pages before any paid audit begins.
- Switch back to the terminal running the mock server.
- Press Ctrl+C to stop the mock process.
You will see the terminal prompt return. Port 8000 is now available for the threaded proxy.
macOS
- Start the threaded proxy from jev-geo-audit by running this command:
python3 server.py
What does this command do?
This starts server.py with Python. The process serves the dashboard while listening for local audit requests.
You will see Jev GEO Audit running at http://127.0.0.1:8000 in the terminal.
- Leave this terminal process running during the pilot.
Did the server fail to start?
Confirm the terminal is inside jev-geo-audit. Check that the mock server has stopped.
If the proxy still stops, help me debug the macOS startup.
Windows
- Start the threaded proxy from jev-geo-audit by running this command:
py server.py
What does this command do?
This starts server.py with the Windows Python launcher. The process serves the dashboard while listening for local audit requests.
You will see Jev GEO Audit running at http://127.0.0.1:8000 in the terminal.
- Leave this terminal process running during the pilot.
Did the server fail to start?
Confirm the terminal is inside jev-geo-audit. Check that the mock server has stopped.
If the proxy still stops, help me debug the Windows startup.
Linux
- Start the threaded proxy from jev-geo-audit by running this command:
python3 server.py
What does this command do?
This starts server.py with Python. The process serves the dashboard while listening for local audit requests.
You will see Jev GEO Audit running at http://127.0.0.1:8000 in the terminal.
Did the server fail to start?
Confirm the terminal is inside jev-geo-audit. Check that the mock server has stopped.
If the proxy still stops, help me debug the Linux startup.
That startup message proves the security boundary is running. Your dashboard now has a threaded route for live audits.
How do the audit controls work?
- Authorised website URL holds the public site that you own or have permission to audit.
- Discover pages reads the site's crawl rules and sitemap files. This discovery does not send paid Jev requests.
- Analysis speed controls simultaneous Jev requests. Sequential uses one request at a time. Fast x8 and Turbo x24 are optional accelerators. Website fetches still follow the crawl delay.
- Predicted weakest signal records your expectation before the run. After completion, the dashboard compares it with the most common result.
- Run five-page pilot inspects up to 25 permitted candidates until five audits succeed.
- Run Full Discovery processes the eligible discovered set up to the 600-page cap. After 20 attempts, it stops automatically if the successful audit rate falls below 40%.
- Worker activity shows one illuminated card for each real in-flight URL. A settled page leaves the worker rail and enters the completion feed.
- Successful audit rate shows succeeded candidates as a percentage of all attempts. Skipped and failed candidates lower this rate.
- Stop becomes available during an active run so you can halt it manually.
- Pilot summary: After the pilot, this panel confirms how many audits succeeded. It also shows elapsed time, speed, latency, and API cost.
- Completion feed: During the pilot, this panel adds each completed page with its citability score and weakest signal. It lets you compare your prediction with the most common result.
- Review queue: This panel sorts Jev's scores from lowest to highest, so the pages needing the most work appear first.
- Local improvement draft: This panel lets you edit a local copy of a weak page and ask Jev to score it again before changing the live website.
- Reload the dashboard.
- Enter your authorised website URL in the Website URL field.
- Keep the Worker activity panel in view.
- Click Discover pages.
Discovery can take a few seconds while the proxy reads crawl rules and sitemap files. You should immediately see Discovering..., a fast green light around the processing panel, a moving progress beam, the authorised URL, and a live elapsed timer.
Before you run the pilot, which signal do you expect Jev to identify most often?
- Select Sequential in the analysis controls.
- Choose your expected weakest signal in the prediction selector.
- Click Run 5-page pilot.
Five successful pilot audits
The pilot inspects up to 25 permitted candidates to collect five successful audits. Pages with fewer than 80 extracted characters are classified as skipped before Jev receives any content.
Concurrent workers stop launching candidates once five successes are possible from the active batch. The server still follows the site's crawl delay before every fetch.
With the fallback site, you should see five successful product-page rows in the live feed. The progress card also reports every attempted, skipped, and failed candidate.
✔️ My pilot has five results
Your pilot returned five successful Jev audits. Continue to the panel summary below.
ⓧ My pilot ended below five results
A pilot below five successes can come from an older sitemap parser or browser worker. The fallback sitemap begins with whitespace before its XML declaration, so the parser must trim the raw bytes first.
- Stop the local proxy with Ctrl+C.
- Paste this repair prompt into Claude Code:
Update only server.py and app.js in jev-geo-audit.
In server.py:
- After optional gzip expansion, parse sitemap XML with ET.fromstring(raw.lstrip()).
- Keep public-host validation, robots.txt rules, crawl delay, size limits, and the 600-page cap.
- Return status 422 for content below 80 characters.
- Put reason thin_content, url, and extractedChars inside the error details object.
- Return API errors as error.message plus error.details.
In app.js:
- Preserve error details from non-successful API responses.
- Classify thin_content as skipped.
- Track attempted, succeeded, skipped, and failed separately.
- Let the pilot inspect at most 25 candidates until five audits succeed.
- Guard concurrent batches so they do not continue after the success target is met.
- Include outcome counters, skipped pages, and failures in exports.
Run local syntax checks only. Do not read .env, start the server, fetch a website, or call Jev.
- Restart the proxy with the command for your operating system from earlier.
- Reload the dashboard.
- Discover the test site again.
- Run the five-page pilot again.
Still below five results?
If the pilot still ends below five successes, help me inspect the outcome counters.
Your secure five-page pilot now proves that the dashboard can turn real website text into Jev evidence. Next, you will start Full Discovery in Sequential mode, watch truthful live progress, and choose when faster workers are useful.
Improve, Export, and Publish
Run Full Discovery
Your secure five-page pilot proved that the dashboard can discover, fetch, and score real pages. Now you will start Full Discovery with Sequential processing and watch the larger queue move through the same evidence workflow.
Full Discovery processes up to 600 permitted candidates. It stops automatically after at least 20 attempts if fewer than half produce successful audits.
In this step, get ready to:
- Start Full Discovery with Sequential processing.
- Confirm that the run stops automatically if fewer than half of the attempts succeed after the first 20.
- Review the settled evidence and push the project to GitHub.
Start Full Discovery
Check the budget before Full Discovery
Full Discovery is the project’s largest spend-sensitive action. The run can make one paid Jev request for each eligible discovered page, up to the 600-page cap.
Confirm the candidate count and maximum estimate before starting. Keep a low credit limit on the project-specific OpenRouter key.
- Return to the dashboard in your browser.
- Confirm Sequential is selected in the analysis controls.
- Click Run Full Discovery.
- Review the candidate count, maximum estimate, remaining budget, and selected speed.
Before you start, do you expect at least half of the first 20 candidates to produce successful Jev audits? After those attempts settle, the dashboard stops automatically if the successful audit rate is below 40%.
- Click Start Full Discovery.
- Keep the dashboard tab open until the completion dialog appears.
You will see real URLs appear as illuminated cards while they are in flight. If fewer than half of the attempts have succeeded after the first 20, the current batch settles and the completion dialog reports an automatic threshold stop.
Review the completed audit
- Read the Result value in the completion dialog.
- Review the attempted, succeeded, skipped, and failed counts.
- Read the Successful audit rate value to confirm whether the guard triggered.
- Click Return to dashboard.
- Select the first successful page in the Review queue if one is available.
- Read its source URL, score, weakest signal, and fetched page text.
You should see successful Jev results ranked in the review queue when any pages succeeded. A rate below 40% after at least 20 attempts leaves the run settled with its evidence intact instead of continuing through mostly skipped pages.
Run stopped below 40%?
An automatic stop means fewer than half of the settled candidates produced successful audits. Thin-content skips never reached Jev, while fetch and API failures also count as unsuccessful attempts.
Check the completion counts before running the site again. If the cause is unclear, help me interpret the automatic stop.
Push the existing project
The dashboard is complete, so no more code or documentation changes are needed on the main path. Your final task is to inspect and push the files you already built.
- Open the New or Split Terminal menu in your editor.
- Stage the existing project files by running:
git add .
- Inspect the staged files by running:
git status
You should see the dashboard project files ready to commit. You should not see .env because .gitignore excludes it.
Before you commit, do you expect the OpenRouter credential to appear in the staged files?
- Commit and push the existing project by running these commands:
git commit -m "Complete Jev GEO audit dashboard"
git push --set-upstream origin HEAD
Commit or push failed?
Confirm that .env is absent from git status before retrying the commit. Confirm that the repository’s origin remote belongs to your GitHub account.
If the push still fails, help me diagnose the project push.
- Return to the GitHub repository page. Refresh.
- Confirm that the dashboard project files are visible.
You have completed the main project without adding another edit cycle. The repository now preserves a dashboard with truthful progress, measured outcomes, and an automatic stop when the successful audit rate falls below 40% after at least 20 attempts.
Secret mission
Add a Floating Audit Guide
Add a larger, resizable chatbot that shows its processing state, explains the current audit, and renders context-backed mini visualizations when they make an answer clearer.
Clean Up Your Resources
Clean Up Your Resources
Successful Jev calls have already used paid OpenRouter credits, while the optional Nemotron chatbot uses a free model variant. Decide whether to keep the project active, pause local access, or remove its resources.
Cost warning
OpenRouter charges your funded account for successful Jev calls. The Nemotron chatbot uses a $0 model variant, while the optional Anthropic path can incur separate API charges.
Stopping the local server prevents the dashboard from sending more requests. Revoking each project key closes its provider access completely.
Resources you used:
- The running local ThreadingHTTPServer process from server.py.
- The cloned local jev-geo-audit folder with the dashboard source files.
- The ignored .env file containing the OpenRouter API key and any optional Anthropic key you added.
- The downloaded JSON audit evidence file.
- The downloaded CSV audit evidence file.
- The public GitHub repository named jev-geo-audit.
Keep everything running
No action is needed while you are actively demonstrating the dashboard. Each new dashboard run can add paid calls.
- Leave the server.py process running while you use the dashboard.
- Retain the local jev-geo-audit folder for future edits.
- Keep the downloaded JSON file as structured evidence from the audit.
- Keep the downloaded CSV file for spreadsheet review.
- Leave the public jev-geo-audit repository available on GitHub.
Pause - I'll come back to this later
Shut down the running process to prevent more local requests. Your files remain ready for another session.
- Switch back to the terminal running server.py.
- Press Ctrl+C to stop the local server.
You'll see Stopping server. in the terminal. That is the safe stopping point for paid requests.
- Keep the jev-geo-audit folder in its current cloned location.
- Leave the OpenRouter key active for later use.
- Leave any optional Anthropic key active only if you plan to test that provider again.
- Retain both downloaded audit evidence files.
- Leave the public GitHub repository available for your next session.
Delete - I don't want to use this again
Permanent deletion can feel drastic. These steps remove this project's local files, public repository, and key access.
Measured charges from completed OpenRouter calls remain on your account.
- Switch back to the terminal running server.py.
- Press Ctrl+C to stop the local server.
- Open your OpenRouter account's key settings.
- Revoke the project-specific OpenRouter key stored in .env.
- Open API keys in the Claude Console if you added the optional Anthropic key.
- Deactivate the project-specific Anthropic key if you created one for this mission.
You have now closed the API paths for this project. The revoked OpenRouter key and any deactivated Anthropic key can no longer authenticate new requests.
- Open the public jev-geo-audit repository on GitHub.
- Use the repository's deletion setting to delete it permanently.
GitHub should stop showing the repository at its former page. The remaining project copies are stored on your computer.
- Use Finder on macOS, File Explorer on Windows, or your Linux file manager to locate the cloned jev-geo-audit folder.
- Move the jev-geo-audit folder to your operating system's trash.
- Open your browser's download folder.
- Move the JSON audit export to your operating system's trash.
- Move the CSV audit export to your operating system's trash.
- Empty your operating system's trash to remove the local resources permanently.
- Use your operating system's search to confirm that the jev-geo-audit folder and both audit exports are absent.
That completes the teardown. The local server, credential file, exports, public repository, and project key are gone.
Nice Work!
Nice Work!
Strong finish! You built a live GEO citability dashboard that discovers permitted public pages, extracts their server-rendered text, and turns real URLs into rubric-based evidence. Your local proxy keeps the API key out of browser code.
You've learned how to:
- Discover permitted pages through robots.txt and XML sitemaps while preserving each public source URL.
- Verify a secure five-page Jev pilot with real page text and protected credentials.
- Run Full Discovery with truthful progress, illuminated live workers, and an automatic stop below a 40% successful audit rate. Push the completed dashboard to GitHub without exposing .env.
- Complete the optional Secret Mission by using Claude Code to add a floating product guide powered by the free NVIDIA Nemotron 3 Ultra model, with an optional Anthropic provider path.
Ready to quiz yourself?