We provide all the information about MCP servers via our MCP API.
curl -X GET 'https://glama.ai/api/mcp/v1/servers/tbrennem-source/sf-permits-mcp'
If you have feedback or need assistance with the MCP directory API, please join our Discord server
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Stuck Permit Analyzer — sfpermits.ai</title>
{% include "fragments/head_obsidian.html" %}
<script nonce="{{ csp_nonce }}" src="/static/htmx.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js" nonce="{{ csp_nonce }}"></script>
<link rel="stylesheet" href="{{ url_for('static', filename='css/share.css') }}">
<style nonce="{{ csp_nonce }}">
body {
background: var(--obsidian);
color: var(--text-primary);
font-family: var(--sans);
margin: 0;
min-height: 100vh;
}
.page-header {
padding: var(--space-12) 0 var(--space-8);
}
.page-header h1 {
font-family: var(--sans);
font-size: var(--text-2xl);
font-weight: 300;
color: var(--text-primary);
margin: 0 0 var(--space-3) 0;
}
.page-header .subtitle {
font-family: var(--sans);
font-size: var(--text-base);
color: var(--text-secondary);
margin: 0;
max-width: 560px;
}
/* Input section */
.input-section {
margin-bottom: var(--space-6);
}
.form-card {
background: var(--obsidian-mid);
border: 1px solid var(--glass-border);
border-radius: var(--radius-md);
padding: var(--space-8);
transition: border-color 0.3s;
}
.form-card:hover {
border-color: var(--glass-hover);
}
.form-label {
display: block;
font-family: var(--mono);
font-size: var(--text-xs);
font-weight: 400;
color: var(--text-secondary);
text-transform: uppercase;
letter-spacing: 0.08em;
margin-bottom: var(--space-3);
}
.input-row {
display: flex;
gap: var(--space-3);
align-items: stretch;
}
.permit-input {
flex: 1;
padding: 14px 20px;
font-family: var(--mono);
font-size: var(--text-base);
font-weight: 300;
color: var(--text-primary);
background: var(--glass);
border: 1px solid var(--glass-border);
border-radius: var(--radius-md);
outline: none;
transition: border-color 0.4s, background 0.4s, box-shadow 0.4s;
min-width: 0;
}
.permit-input::placeholder {
color: var(--text-tertiary);
font-weight: 300;
}
.permit-input:focus {
border-color: var(--accent-ring);
background: rgba(255, 255, 255, 0.06);
box-shadow: 0 0 40px var(--accent-glow);
}
.diagnose-btn {
font-family: var(--mono);
font-size: var(--text-sm);
font-weight: 400;
color: var(--text-secondary);
background: var(--glass);
border: 1px solid var(--glass-border);
border-radius: var(--radius-sm);
padding: 0 var(--space-6);
cursor: pointer;
white-space: nowrap;
transition: border-color 0.3s, color 0.3s, background 0.3s;
display: inline-flex;
align-items: center;
gap: var(--space-2);
}
.diagnose-btn:hover {
border-color: var(--glass-hover);
color: var(--text-primary);
}
.diagnose-btn:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.diagnose-btn.loading {
color: var(--signal-amber);
border-color: rgba(251, 191, 36, 0.30);
}
/* Loading dot */
.loading-dot {
width: 8px;
height: 8px;
border-radius: var(--radius-full);
background: var(--signal-amber);
animation: pulse-dot 1s ease-in-out infinite alternate;
display: none;
}
.diagnose-btn.loading .loading-dot {
display: inline-block;
}
@keyframes pulse-dot {
from { opacity: 0.4; }
to { opacity: 1; }
}
/* Skeleton loading */
.skeleton-screen {
display: none;
}
.skeleton-screen.is-visible {
display: block;
}
.skeleton-severity-row {
display: flex;
gap: var(--space-4);
align-items: center;
margin-bottom: var(--space-6);
}
.skeleton-badge {
width: 80px;
height: 28px;
background: var(--glass);
border-radius: var(--radius-sm);
animation: skeleton-pulse 1.5s ease-in-out infinite;
}
.skeleton-bar {
height: 10px;
background: var(--glass);
border-radius: var(--radius-full);
margin-bottom: var(--space-3);
animation: skeleton-pulse 1.5s ease-in-out infinite;
}
.skeleton-card {
background: var(--glass);
border-radius: var(--radius-sm);
height: 80px;
margin-bottom: var(--space-3);
animation: skeleton-pulse 1.5s ease-in-out infinite;
}
@keyframes skeleton-pulse {
0%, 100% { opacity: 0.4; }
50% { opacity: 0.8; }
}
/* Empty state */
.empty-state {
text-align: center;
padding: var(--space-10) var(--space-6);
}
.empty-state-hint {
font-family: var(--sans);
font-size: var(--text-sm);
color: var(--text-tertiary);
margin: 0 0 var(--space-6) 0;
}
.demo-permits {
display: flex;
gap: var(--space-2);
flex-wrap: wrap;
justify-content: center;
}
.demo-permit-chip {
font-family: var(--mono);
font-size: var(--text-xs);
color: var(--text-secondary);
background: var(--glass);
border: 1px solid var(--glass-border);
border-radius: var(--radius-full);
padding: var(--space-1) var(--space-3);
cursor: pointer;
transition: color 0.2s, border-color 0.2s;
display: inline-block;
text-decoration: none;
}
.demo-permit-chip:hover {
color: var(--accent);
border-color: var(--accent-ring);
}
/* Results area */
.results-area {
background: var(--obsidian-mid);
border: 1px solid var(--glass-border);
border-radius: var(--radius-md);
padding: var(--space-8);
transition: border-color 0.3s;
min-height: 120px;
}
.results-area:hover {
border-color: var(--glass-hover);
}
/* Severity dashboard */
.severity-dashboard {
margin-bottom: var(--space-8);
padding-bottom: var(--space-6);
border-bottom: 1px solid var(--glass-border);
}
.severity-dashboard-row {
display: flex;
align-items: center;
gap: var(--space-4);
flex-wrap: wrap;
}
.severity-badge {
font-family: var(--mono);
font-size: var(--text-sm);
font-weight: 400;
text-transform: uppercase;
letter-spacing: 0.06em;
padding: var(--space-2) var(--space-4);
border-radius: var(--radius-sm);
border: 1px solid;
}
.severity-badge-green {
color: var(--signal-green);
background: rgba(52, 211, 153, 0.08);
border-color: rgba(52, 211, 153, 0.25);
}
.severity-badge-amber {
color: var(--signal-amber);
background: rgba(251, 191, 36, 0.08);
border-color: rgba(251, 191, 36, 0.25);
}
.severity-badge-red {
color: var(--signal-red);
background: rgba(248, 113, 113, 0.08);
border-color: rgba(248, 113, 113, 0.25);
}
.severity-meta {
font-family: var(--mono);
font-size: var(--text-sm);
color: var(--text-secondary);
}
.severity-permit-number {
font-family: var(--mono);
font-size: var(--text-sm);
color: var(--accent);
margin-left: auto;
}
/* Section label */
.section-label {
font-family: var(--mono);
font-size: var(--text-xs);
color: var(--text-secondary);
text-transform: uppercase;
letter-spacing: 0.08em;
margin: 0 0 var(--space-4) 0;
}
/* Block cards */
.block-cards {
display: flex;
flex-direction: column;
gap: var(--space-3);
margin-bottom: var(--space-8);
}
.block-card {
padding: var(--space-4) var(--space-5);
border-radius: var(--radius-sm);
border-left: 3px solid;
}
.block-card-critical {
background: rgba(248, 113, 113, 0.06);
border-left-color: var(--signal-red);
}
.block-card-stalled {
background: rgba(251, 191, 36, 0.06);
border-left-color: var(--signal-amber);
}
.block-card-normal {
background: var(--glass);
border-left-color: var(--glass-border);
}
.block-card-header {
display: flex;
align-items: center;
gap: var(--space-3);
margin-bottom: var(--space-2);
flex-wrap: wrap;
}
.block-card-station {
font-family: var(--mono);
font-size: var(--text-sm);
font-weight: 400;
color: var(--text-primary);
}
.block-card-status-chip {
font-family: var(--mono);
font-size: var(--text-xs);
font-weight: 400;
text-transform: uppercase;
letter-spacing: 0.04em;
padding: 1px 6px;
border-radius: 3px;
white-space: nowrap;
}
.chip-critical { color: var(--signal-red); background: rgba(248, 113, 113, 0.12); }
.chip-stalled { color: var(--signal-amber); background: rgba(251, 191, 36, 0.12); }
.chip-normal { color: var(--text-secondary); background: var(--glass); }
.block-card-meta {
display: flex;
gap: var(--space-4);
flex-wrap: wrap;
}
.block-card-meta-item {
font-family: var(--mono);
font-size: var(--text-xs);
color: var(--text-tertiary);
}
.block-card-flags {
margin-top: var(--space-2);
display: flex;
flex-direction: column;
gap: 4px;
}
.block-card-flag {
font-family: var(--sans);
font-size: var(--text-sm);
color: var(--text-secondary);
}
/* Intervention playbook */
.playbook-section {
margin-bottom: var(--space-8);
}
.playbook-steps {
display: flex;
flex-direction: column;
gap: var(--space-3);
}
.playbook-step {
display: flex;
gap: var(--space-4);
padding: var(--space-4) var(--space-5);
background: var(--glass);
border: 1px solid var(--glass-border);
border-radius: var(--radius-sm);
transition: border-color 0.2s;
}
.playbook-step:hover {
border-color: var(--glass-hover);
}
.playbook-step-number {
font-family: var(--mono);
font-size: var(--text-sm);
font-weight: 400;
color: var(--text-tertiary);
min-width: 20px;
flex-shrink: 0;
}
.playbook-step-body {
flex: 1;
}
.playbook-step-urgency {
font-family: var(--mono);
font-size: var(--text-xs);
font-weight: 400;
text-transform: uppercase;
letter-spacing: 0.04em;
padding: 1px 6px;
border-radius: 3px;
margin-bottom: var(--space-2);
display: inline-block;
}
.urgency-immediate { color: var(--signal-red); background: rgba(248,113,113,0.12); }
.urgency-high { color: var(--signal-amber); background: rgba(251,191,36,0.12); }
.urgency-medium { color: var(--signal-blue); background: rgba(96,165,250,0.12); }
.urgency-low { color: var(--text-secondary); background: var(--glass); }
.playbook-step-action {
font-family: var(--sans);
font-size: var(--text-sm);
color: var(--text-primary);
margin-bottom: var(--space-2);
}
.playbook-step-contact {
font-family: var(--mono);
font-size: var(--text-xs);
color: var(--text-secondary);
}
.playbook-step-contact a {
color: var(--accent);
text-decoration: none;
}
.playbook-step-contact a:hover {
opacity: 0.8;
}
/* Timeline impact note */
.timeline-impact {
background: rgba(251, 191, 36, 0.04);
border: 1px solid rgba(251, 191, 36, 0.15);
border-radius: var(--radius-sm);
padding: var(--space-4) var(--space-5);
margin-bottom: var(--space-8);
}
.timeline-impact-label {
font-family: var(--mono);
font-size: var(--text-xs);
color: var(--signal-amber);
text-transform: uppercase;
letter-spacing: 0.08em;
margin-bottom: var(--space-2);
}
.timeline-impact-text {
font-family: var(--sans);
font-size: var(--text-sm);
color: var(--text-secondary);
margin: 0;
}
/* Markdown fallback for full playbook */
.playbook-content {
font-family: var(--sans);
font-size: var(--text-base);
color: var(--text-secondary);
line-height: 1.7;
}
.playbook-content h1,
.playbook-content h2 {
font-family: var(--sans);
font-weight: 400;
color: var(--text-primary);
margin-top: var(--space-8);
margin-bottom: var(--space-3);
}
.playbook-content h3,
.playbook-content h4 {
font-family: var(--sans);
font-weight: 400;
color: var(--text-primary);
margin-top: var(--space-6);
margin-bottom: var(--space-2);
}
.playbook-content h1 { font-size: var(--text-xl); }
.playbook-content h2 { font-size: var(--text-lg); }
.playbook-content h3 { font-size: var(--text-base); }
.playbook-content p {
margin: 0 0 var(--space-4) 0;
}
.playbook-content ul,
.playbook-content ol {
padding-left: var(--space-6);
margin: 0 0 var(--space-4) 0;
}
.playbook-content li {
margin-bottom: var(--space-2);
}
.playbook-content code {
font-family: var(--mono);
font-size: var(--text-sm);
color: var(--text-primary);
background: var(--glass);
padding: 2px 6px;
border-radius: 3px;
}
.playbook-content strong {
color: var(--text-primary);
font-weight: 500;
}
.playbook-content hr {
border: none;
border-top: 1px solid var(--glass-border);
margin: var(--space-6) 0;
}
/* Error state */
.error-message {
font-family: var(--mono);
font-size: var(--text-sm);
color: var(--signal-red);
padding: var(--space-4);
background: rgba(248, 113, 113, 0.06);
border: 1px solid rgba(248, 113, 113, 0.15);
border-radius: var(--radius-sm);
}
.auth-prompt {
text-align: center;
padding: var(--space-8) 0;
}
.auth-prompt p {
font-family: var(--sans);
font-size: var(--text-base);
color: var(--text-secondary);
margin: 0 0 var(--space-4) 0;
}
.auth-prompt a {
font-family: var(--mono);
font-size: var(--text-sm);
font-weight: 300;
color: var(--accent);
text-decoration: none;
transition: opacity 0.3s;
}
.auth-prompt a:hover {
opacity: 0.8;
}
/* Mobile */
@media (max-width: 768px) {
.page-header {
padding: var(--space-8) 0 var(--space-6);
}
.input-row {
flex-direction: column;
}
.diagnose-btn {
width: 100%;
justify-content: center;
padding: var(--space-4) var(--space-6);
}
.severity-dashboard-row {
flex-direction: column;
align-items: flex-start;
}
.severity-permit-number {
margin-left: 0;
}
.block-card-meta {
flex-direction: column;
gap: var(--space-1);
}
}
@media (max-width: 375px) {
.permit-input {
font-size: var(--text-sm);
}
}
/* Anonymous user soft CTA */
.anon-cta {
display: flex;
align-items: center;
gap: var(--space-2);
padding: var(--space-3) var(--space-4);
margin-top: var(--space-6);
background: var(--obsidian-mid);
border: 1px solid var(--glass-border);
border-radius: var(--radius-sm);
}
.anon-cta-text {
font-family: var(--sans);
font-size: var(--text-sm);
color: var(--text-tertiary);
}
</style>
</head>
<body>
{% include "fragments/nav.html" %}
<main>
<div class="obs-container">
<header class="page-header">
<h1>Stuck Permit Analyzer</h1>
<p class="subtitle">Diagnose delays and get a ranked intervention playbook for any SF permit.</p>
</header>
<div class="input-section">
<div class="form-card">
<label class="form-label" for="permit-number-input">Permit Number</label>
<div class="input-row">
<input
type="text"
id="permit-number-input"
class="permit-input"
placeholder="Enter permit number (e.g. 202301015555)"
autocomplete="off"
maxlength="20"
aria-label="Permit number"
>
<button
class="diagnose-btn"
id="diagnose-btn"
onclick="diagnosedPermit()"
type="button"
aria-label="Diagnose permit"
>
<span class="loading-dot" aria-hidden="true"></span>
<span id="btn-text">Diagnose →</span>
</button>
</div>
</div>
</div>
<!-- Skeleton loading screen -->
<div id="skeleton-screen" class="skeleton-screen" aria-hidden="true">
<div class="results-area">
<div class="severity-dashboard">
<div class="skeleton-severity-row">
<div class="skeleton-badge"></div>
<div class="skeleton-bar" style="width:120px;margin:0;"></div>
</div>
<div class="skeleton-bar" style="width:80%;"></div>
</div>
<div class="skeleton-card"></div>
<div class="skeleton-card" style="height:60px;"></div>
</div>
</div>
<!-- Empty state -->
<div id="empty-state" class="results-area">
<div class="empty-state">
<p class="empty-state-hint">Enter an SF permit number to diagnose delays, identify blocked stations, and get a ranked intervention playbook with agency contact info.</p>
<div class="demo-permits" aria-label="Demo permit numbers">
<button class="demo-permit-chip" onclick="fillDemo('202509155257')" type="button">202509155257</button>
<button class="demo-permit-chip" onclick="fillDemo('202401019876')" type="button">202401019876</button>
<button class="demo-permit-chip" onclick="fillDemo('202303014433')" type="button">202303014433</button>
</div>
</div>
</div>
<!-- Results area — hidden until populated -->
<div id="results" class="results-area glass-card" style="display:none;"></div>
{% if not g.user %}
<div class="anon-cta" id="anon-cta">
<span class="anon-cta-text">Analyze your own permits —</span>
<a href="/beta/join" class="ghost-cta">Sign up free →</a>
</div>
{% endif %}
{% include "components/share_button.html" %}
</div>
</main>
<script nonce="{{ csp_nonce }}" src="{{ url_for('static', filename='js/share.js') }}" defer></script>
<script nonce="{{ csp_nonce }}">
(function() {
// Configure marked.js
if (typeof marked !== 'undefined') {
marked.setOptions({ breaks: true, gfm: true });
}
var input = document.getElementById('permit-number-input');
var btn = document.getElementById('diagnose-btn');
var btnText = document.getElementById('btn-text');
var resultsDiv = document.getElementById('results');
var emptyState = document.getElementById('empty-state');
var skeletonScreen = document.getElementById('skeleton-screen');
// ── Auto-fill from ?permit= query param ──────────────────────────────
document.addEventListener('DOMContentLoaded', function () {
var params = new URLSearchParams(window.location.search);
var permitParam = params.get('permit');
if (permitParam && input) {
input.value = permitParam.trim();
diagnosedPermit();
}
});
// Enter key support
if (input) {
input.addEventListener('keydown', function(e) {
if (e.key === 'Enter') {
window.diagnosedPermit();
}
});
}
// Demo chip fill
window.fillDemo = function (permit) {
if (input) input.value = permit;
diagnosedPermit();
};
// ── Utility ───────────────────────────────────────────────────────────
function getCSRFToken() {
var meta = document.querySelector('meta[name="csrf-token"]');
return meta ? meta.getAttribute('content') : '';
}
function escHtml(str) {
return String(str || '')
.replace(/&/g, '&')
.replace(/</g, '<')
.replace(/>/g, '>')
.replace(/"/g, '"');
}
// ── Loading state ─────────────────────────────────────────────────────
function setLoading(isLoading) {
if (btn) btn.disabled = isLoading;
if (isLoading) {
btn.classList.add('loading');
if (btnText) btnText.textContent = 'Analyzing\u2026';
if (skeletonScreen) skeletonScreen.classList.add('is-visible');
if (emptyState) emptyState.style.display = 'none';
if (resultsDiv) resultsDiv.style.display = 'none';
} else {
btn.classList.remove('loading');
if (btnText) btnText.textContent = 'Diagnose \u2192';
if (skeletonScreen) skeletonScreen.classList.remove('is-visible');
}
}
// ── Parse severity from markdown ──────────────────────────────────────
function parseSeverity(markdown) {
if (/CRITICAL/i.test(markdown)) return 'critical';
if (/STALLED/i.test(markdown)) return 'stalled';
return 'normal';
}
function parseBlockCount(markdown) {
var match = markdown.match(/(\d+)\s*station/i);
return match ? parseInt(match[1], 10) : 0;
}
// ── Parse intervention steps from markdown ────────────────────────────
/**
* Extracts numbered intervention steps from the "## Intervention Steps"
* section of the playbook markdown.
* Returns array of { urgency, action, contactLines }.
*/
function parseInterventionSteps(markdown) {
var steps = [];
var inSteps = false;
var lines = markdown.split('\n');
var currentStep = null;
for (var i = 0; i < lines.length; i++) {
var line = lines[i];
if (/^##\s+Intervention Steps/i.test(line)) {
inSteps = true;
continue;
}
if (inSteps && /^##\s/.test(line)) {
// Entered next section
if (currentStep) steps.push(currentStep);
break;
}
if (!inSteps) continue;
// Numbered step: "1. [IMMEDIATE] Some action text"
var stepMatch = line.match(/^(\d+)\.\s+\[([A-Z]+)\]\s+(.*)/);
if (stepMatch) {
if (currentStep) steps.push(currentStep);
currentStep = {
number: parseInt(stepMatch[1], 10),
urgency: stepMatch[2].toLowerCase(),
action: stepMatch[3].trim(),
contactLines: [],
};
continue;
}
// Contact lines indented with " - "
var contactMatch = line.match(/^\s{3}-\s+(.*)/);
if (contactMatch && currentStep) {
currentStep.contactLines.push(contactMatch[1].trim());
}
}
if (currentStep) steps.push(currentStep);
return steps;
}
// ── Parse station blocks from markdown ───────────────────────────────
/**
* Extracts station diagnosis entries from "## Station Diagnosis" section.
* Returns array of { station, status, dwell, flags }.
*/
function parseStationBlocks(markdown) {
var blocks = [];
var inDiagnosis = false;
var lines = markdown.split('\n');
var current = null;
for (var i = 0; i < lines.length; i++) {
var line = lines[i].trim();
if (/^##\s+Station Diagnosis/i.test(line)) {
inDiagnosis = true;
continue;
}
if (inDiagnosis && /^##\s/.test(line)) {
if (current) blocks.push(current);
break;
}
if (!inDiagnosis) continue;
// Section headings: "### **BLDG** — [STALLED] — 45d"
var headMatch = line.match(/^###\s+\*\*(.+?)\*\*.*\[(CRITICAL|STALLED|NORMAL)\].*?(\d+)d/i);
if (headMatch) {
if (current) blocks.push(current);
current = {
station: headMatch[1].trim(),
status: headMatch[2].toLowerCase(),
dwell: parseInt(headMatch[3], 10),
flags: [],
};
continue;
}
// Flag lines: "- dwell 45d > p75 (30d baseline)"
var flagMatch = line.match(/^-\s+(.*)/);
if (flagMatch && current) {
current.flags.push(flagMatch[1].trim());
}
}
if (current) blocks.push(current);
return blocks;
}
// ── Build severity dashboard HTML ─────────────────────────────────────
function buildSeverityDashboard(severity, permitNumber, stationCount) {
var badgeClass = {
critical: 'severity-badge-red',
stalled: 'severity-badge-amber',
normal: 'severity-badge-green',
}[severity] || 'severity-badge-green';
var label = {
critical: 'CRITICAL',
stalled: 'STALLED',
normal: 'ON TRACK',
}[severity] || 'UNKNOWN';
var meta = stationCount > 0 ? stationCount + ' station' + (stationCount !== 1 ? 's' : '') + ' blocked' : '';
return '<div class="severity-dashboard">'
+ '<div class="severity-dashboard-row">'
+ '<div class="severity-badge ' + badgeClass + '">' + label + '</div>'
+ (meta ? '<div class="severity-meta">' + escHtml(meta) + '</div>' : '')
+ '<div class="severity-permit-number">' + escHtml(permitNumber) + '</div>'
+ '</div>'
+ '</div>';
}
// ── Build block cards HTML ─────────────────────────────────────────────
function buildBlockCards(stationBlocks) {
if (!stationBlocks.length) return '';
var html = '<p class="section-label">Station Diagnosis</p>';
html += '<div class="block-cards">';
stationBlocks.forEach(function (block) {
var cardClass = 'block-card ';
var chipClass = 'block-card-status-chip ';
var chipLabel = '';
if (block.status === 'critically_stalled' || block.status === 'critical') {
cardClass += 'block-card-critical';
chipClass += 'chip-critical';
chipLabel = 'CRITICAL';
} else if (block.status === 'stalled') {
cardClass += 'block-card-stalled';
chipClass += 'chip-stalled';
chipLabel = 'STALLED';
} else {
cardClass += 'block-card-normal';
chipClass += 'chip-normal';
chipLabel = 'NORMAL';
}
html += '<div class="' + cardClass + '">';
html += '<div class="block-card-header">';
html += '<span class="block-card-station">' + escHtml(block.station) + '</span>';
html += '<span class="' + chipClass + '">' + chipLabel + '</span>';
html += '</div>';
if (block.dwell) {
html += '<div class="block-card-meta">';
html += '<span class="block-card-meta-item">' + block.dwell + ' days at station</span>';
html += '</div>';
}
if (block.flags && block.flags.length) {
html += '<div class="block-card-flags">';
block.flags.forEach(function (f) {
html += '<div class="block-card-flag">— ' + escHtml(f) + '</div>';
});
html += '</div>';
}
html += '</div>'; // block-card
});
html += '</div>'; // block-cards
return html;
}
// ── Build playbook steps HTML ──────────────────────────────────────────
function buildPlaybookSteps(steps) {
if (!steps.length) return '';
var html = '<div class="playbook-section">';
html += '<p class="section-label">Intervention Playbook</p>';
html += '<div class="playbook-steps">';
steps.forEach(function (step) {
var urgencyClass = {
immediate: 'urgency-immediate',
high: 'urgency-high',
medium: 'urgency-medium',
low: 'urgency-low',
}[step.urgency] || 'urgency-low';
html += '<div class="playbook-step">';
html += '<div class="playbook-step-number">' + step.number + '.</div>';
html += '<div class="playbook-step-body">';
html += '<div class="playbook-step-urgency ' + urgencyClass + '">' + step.urgency.toUpperCase() + '</div>';
html += '<div class="playbook-step-action">' + escHtml(step.action) + '</div>';
if (step.contactLines.length) {
html += '<div class="playbook-step-contact">';
step.contactLines.forEach(function (c) {
// Linkify phone numbers and URLs
var linked = escHtml(c)
.replace(/\(415\)\s*\d{3}-\d{4}/g, function (m) {
return '<a href="tel:+1' + m.replace(/\D/g, '') + '" style="color:var(--accent);font-weight:500;">' + m + '</a>';
})
.replace(/https?:\/\/[\w./\-?=&#%]+/g, function (m) {
return '<a href="' + m + '" target="_blank" rel="noopener" style="color:var(--accent);">' + m + '</a>';
});
html += linked + '<br>';
});
html += '</div>';
}
html += '</div>'; // playbook-step-body
html += '</div>'; // playbook-step
});
html += '</div>'; // playbook-steps
html += '</div>'; // playbook-section
return html;
}
// ── Show results ───────────────────────────────────────────────────────
function showResults(html) {
if (emptyState) emptyState.style.display = 'none';
if (resultsDiv) {
resultsDiv.style.display = '';
resultsDiv.innerHTML = html;
}
}
function showEmpty() {
if (emptyState) emptyState.style.display = '';
if (resultsDiv) {
resultsDiv.style.display = 'none';
resultsDiv.innerHTML = '';
}
}
function renderResult(data, permitNumber) {
var markdown = data.result || '';
var severity = parseSeverity(markdown);
var stationBlocks = parseStationBlocks(markdown);
var steps = parseInterventionSteps(markdown);
var hasStructured = stationBlocks.length > 0 || steps.length > 0;
var html = '';
// Severity dashboard
html += buildSeverityDashboard(severity, data.permit_number || permitNumber, stationBlocks.length);
// Block cards (if structured data available)
if (stationBlocks.length > 0) {
html += buildBlockCards(stationBlocks);
}
// Intervention playbook (structured steps)
if (steps.length > 0) {
html += buildPlaybookSteps(steps);
}
// Timeline impact note (only when structured content is present)
if (hasStructured) {
html += '<div class="timeline-impact">'
+ '<div class="timeline-impact-label">Timeline impact</div>'
+ '<p class="timeline-impact-text">Each comment-response cycle adds 6–8 weeks to the total review timeline. Resubmitting promptly and completely is the single highest-leverage action.</p>'
+ '</div>';
}
// Full markdown: always show; use collapsible when structured content exists
var parsedMd = '';
try {
parsedMd = (typeof marked !== 'undefined') ? marked.parse(markdown) : '<pre>' + escHtml(markdown) + '</pre>';
} catch (e) {
parsedMd = '<pre>' + escHtml(markdown) + '</pre>';
}
if (hasStructured) {
// Collapsible full detail section
html += '<div style="margin-top:var(--space-6);border-top:1px solid var(--glass-border);padding-top:var(--space-4);">'
+ '<button type="button" onclick="this.nextElementSibling.style.display=this.nextElementSibling.style.display===\'none\'?\'block\':\'none\';this.textContent=this.nextElementSibling.style.display===\'none\'?\'Full diagnostic report \u25BA\':\'Full diagnostic report \u25C4\';" '
+ 'style="background:none;border:none;cursor:pointer;font-family:var(--mono);font-size:var(--text-xs);color:var(--text-secondary);text-transform:uppercase;letter-spacing:0.08em;padding:0;">Full diagnostic report \u25BA</button>'
+ '<div style="display:none;">'
+ '<div class="playbook-content">' + parsedMd + '</div>'
+ '</div>'
+ '</div>';
} else {
html += '<div class="playbook-content">' + parsedMd + '</div>';
}
showResults(html);
}
// ── Main handler ──────────────────────────────────────────────────────
window.diagnosedPermit = function() {
if (!input) return;
var permitNumber = input.value.trim();
if (!permitNumber) {
input.focus();
return;
}
setLoading(true);
fetch('/api/stuck-permit/' + encodeURIComponent(permitNumber), {
method: 'GET',
headers: {
'Accept': 'application/json',
'X-CSRFToken': getCSRFToken()
},
credentials: 'same-origin'
})
.then(function(response) {
if (response.status === 401) {
setLoading(false);
showResults(
'<div class="auth-prompt">'
+ '<p>This tool requires a free account.</p>'
+ '<a href="/auth/login">Log in or create account →</a>'
+ '</div>'
);
return null;
}
if (!response.ok) {
return response.json().then(function(data) {
throw new Error(data.error || 'Analysis failed. Please try again.');
});
}
return response.json();
})
.then(function(data) {
if (!data) return;
setLoading(false);
renderResult(data, permitNumber);
})
.catch(function(err) {
setLoading(false);
showResults('<div class="error-message">' + escHtml(err.message || 'An unexpected error occurred. Please try again.') + '</div>');
});
};
})();
</script>
{% include 'fragments/feedback_widget.html' %}
<script nonce="{{ csp_nonce }}" src="/static/admin-feedback.js" defer></script>
<script nonce="{{ csp_nonce }}" src="/static/admin-tour.js" defer></script>
</body>
</html>