The Zero-Backend AI Triage Dashboard: One HTML File, Azure DevOps, and an LLM
How a single static HTML page turned our weekly bug review from an hour of scrolling into a five-minute, drill-down conversation — with no server, no deployment, and no stored secrets.
The problem every small support team knows
If you run support for an internal product, your weekly review probably looks like ours did. Bugs arrive all week. Someone logs them into Azure DevOps (on a good week). Then, before the management call, a team member opens the backlog, scrolls through everything created in the last seven days, reads the comments to figure out what actually happened, and tries to mentally bucket the items: was this a real defect? A configuration slip? Something the user just didn't know how to do?
That last question is the one management actually cares about. A week with fifteen bugs where twelve are knowledge gaps is a training problem. The same fifteen where twelve are system defects is an engineering problem. The raw backlog doesn't tell you that — a human has to synthesize it, every single week, from titles, descriptions, and comment threads.
We wanted that synthesis automated. But we didn't want another service to build, host, secure, and maintain. So we set an intentionally aggressive constraint:
The entire tool must be one HTML file that runs in the browser. No backend. No build step. No database. No stored credentials.
It turned out this constraint wasn't just achievable — it made the tool dramatically easier to adopt and share. Here's how it works and what we learned.
Why zero-backend is actually viable now
Two things make this pattern possible in 2026 that weren't obvious a few years ago:
1. Azure DevOps REST APIs support CORS with PAT authentication. Your browser can call dev.azure.com directly using a Personal Access Token over Basic auth. No proxy needed. That means a static page can run WIQL queries, fetch work items, and read comment threads — everything a triage tool needs — straight from client-side JavaScript.
2. LLM inference endpoints are just HTTPS. Azure OpenAI's chat completions API is a plain POST with an api-key header. If your endpoint allows browser origins (or you front it with API Management and a CORS policy — more on that in the gotchas), the browser can do the classification calls too.
Put those together and the "architecture" collapses into something refreshingly boring:
Browser (one HTML file)
├─ 1. WIQL query ──────────► Azure DevOps REST API
├─ 2. Work item details ───► Azure DevOps REST API
├─ 3. Comment threads ─────► Azure DevOps REST API
├─ 4. Batched classification ─► Azure OpenAI (chat completions)
├─ 5. Insight synthesis ──────► Azure OpenAI (chat completions)
└─ 6. Render dashboard ──► DOM (no framework, no build)
Credentials live in JavaScript variables for the lifetime of the tab. Close the tab, they're gone. Nothing is persisted, proxied, or logged by any middle tier — because there is no middle tier.
Step 1–3: Getting a week of bugs out of Azure DevOps
The data pipeline is three API shapes, all vanilla fetch:
The WIQL query finds everything created in the window:
SELECT [System.Id] FROM WorkItems
WHERE [System.TeamProject] = @project
AND [System.WorkItemType] = 'Bug'
AND [System.CreatedDate] >= @Today - 7
ORDER BY [System.CreatedDate] DESC
POST that to /{project}/_apis/wit/wiql?api-version=7.1 and you get back IDs. WIQL's @Today - 7 macro means you never do date math in JavaScript.
The batch details call (/_apis/wit/workitems?ids=...&fields=...) accepts up to 200 IDs at once, so even a heavy week is one or two requests. Ask only for the fields you need — title, state, description, repro steps, priority, assignee — and you keep payloads small.
The comments endpoint (/_apis/wit/workItems/{id}/comments) is per-item, so we fetch it in small parallel batches (six at a time) to stay polite. This endpoint is the secret ingredient: the resolution usually lives in the comments, not the description. The description tells you the symptom; the last few comments tell you what actually fixed it. Feeding both to the model is what makes the "solution" field in the output trustworthy.
One practical note: ADO descriptions and comments are HTML. Strip them before sending to the model — new DOMParser().parseFromString(html, "text/html").body.textContent does it in one line, and it meaningfully cuts token usage.
Step 4: The classification layer — and the emergent taxonomy trick
The obvious approach is to hard-code categories ("System Bug", "Knowledge Issue", "Data Issue") and ask the model to pick one. We deliberately didn't. Support taxonomies drift; the categories that matter this quarter aren't the ones that mattered last quarter. So we let the model propose categories freely.
The problem with free-form categories is consistency. Ask an LLM to label 21 bugs across three separate API calls and you'll get "Config Issue", "Configuration", and "Config/Setup" — three names for one bucket. The fix is simple and works well:
Thread the accumulated category list through every batch. Each classification call includes the instruction: "Reuse an existing category whenever it fits. Existing categories so far: Configuration, System Defect, Knowledge Gap…"
Batch one invents the vocabulary; every later batch is nudged to reuse it. In practice this converges on a clean 5–8 category taxonomy without any hard-coding, and the taxonomy adapts naturally as your bug mix changes over the months.
Each batch request asks for strict JSON — one object per bug:
{
"id": 12345,
"category": "Configuration",
"confidence": 90,
"issue": "Task not synced due to incorrect sprint mapping.",
"solution": "User story was mapped to the active sprint.",
"reasoning": "Comments show resolution required only a settings change."
}
Two hard-won rules for the JSON round-trip:
-
Never trust the model to return bare JSON, even when told to. Strip markdown fences, find the first
[or{, and slice to the last]or}before parsing. A ten-line "loose parser" eliminates 95% of parse failures. - Batch size matters more than prompt cleverness. Eight bugs per call (with truncated descriptions and the last ~6 comments each) keeps responses well under output-token limits, so you never get JSON cut off mid-array — the most common and most confusing failure mode.
Step 5: The insight layer — intelligence before detail
Classification alone gives you labels. The step that changed how our review call feels was adding one more model call after classification, which receives a digest of every category and its bugs and returns:
- a 3–4 sentence weekly brief (written for a management audience), and
- for each category, a one-sentence insight ("the pattern behind these bugs") and a short recommended action.
This is what powers the dashboard's information hierarchy. The landing view isn't a table of 21 rows — it's a handful of category cards, each carrying a count, a resolution progress bar, and that one-line insight. You read the week in ten seconds. Then you click a card and drill into just that category's bugs, each with its AI-summarized issue, solution, confidence score, and a link back to the real work item.
We call this "intelligence first, detail on demand." The tool leads with the synthesized read of the week and makes the raw data one click away — instead of leading with raw data and making the human do the synthesis. It's the difference between a report and an analyst.
Gotchas we hit (so you don't have to)
Users will paste the full ADO URL into the "organization" field. Of course they will — that's what's in their address bar. Normalize it: extract the org from dev.azure.com/{org}/... or legacy {org}.visualstudio.com patterns instead of failing with a cryptic fetch error. This was literally our first bug report.
Azure OpenAI CORS depends on your setup. Azure DevOps CORS is reliable; direct browser calls to an Azure OpenAI endpoint may be blocked depending on configuration. The clean fix if you already run API Management: expose the deployment through APIM with a CORS policy and point the tool at the APIM URL. Your key handling and rate limiting improve as a side effect.
Plan for 429s. LLM rate limits will interrupt a run eventually. Because the ADO fetch (steps 1–3) is cheap and the AI calls are the expensive part, cache the fetched bugs in memory and offer a "Re-run AI only" button. Nothing is more annoying than re-downloading a week of comments because one classification batch got throttled.
Show the pipeline. A five-step visible progress tracker (query → details → comments → classify → insights), with live counts and a per-step error message that names the fix ("check the PAT scope", "check the deployment name"), turns debugging from a console-tab exercise into something any team member can self-serve.
Escape everything you render. Bug titles and comments are user-generated content going into your DOM. One escapeHtml() helper, used everywhere, keeps a triage tool from becoming an XSS vector.
Honest limitations
This pattern is deliberately scoped. A PAT in a browser tab is fine for a trusted internal team of three; it is not an enterprise auth story — for wider rollout you'd want OAuth/Entra ID, which does require registering an app. Classification quality depends on comment hygiene: bugs closed silently with no comments classify on symptoms alone. And there's no persistence, which is a feature for security but means no week-over-week trends — the natural next step is exporting each run (the tool does CSV) and letting trend analysis live wherever your reporting already lives.
The takeaway
The interesting part of this project wasn't the AI prompt — it was the shape of the solution. A lot of internal tooling doesn't need to be a service. If your data source speaks CORS and your model endpoint speaks HTTPS, a single static file can be the whole product: zero infrastructure to maintain, trivially shareable (email the file), auditable in one read, and modifiable by anyone on the team with a text editor.
Weekly triage was our use case. The same skeleton — fetch from a work-tracking API, classify in threaded batches with an emergent taxonomy, synthesize insights, render dashboard-then-drill-down — applies just as well to incident retros, PR review summaries, customer feedback clustering, or sprint retrospectives.
One file. Two APIs. No servers. Your weekly review will thank you.
Stack: vanilla HTML/CSS/JS, Azure DevOps REST API 7.1 (WIQL, work items, comments), Azure OpenAI chat completions. No frameworks, no build tools, no dependencies.
Top comments (0)