sitemend 1.2.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- sitemend/__init__.py +1 -0
- sitemend/assets/dashboard.html +466 -0
- sitemend/audit.py +902 -0
- sitemend/build_dashboard.py +61 -0
- sitemend/crawl.py +644 -0
- sitemend/report.py +342 -0
- sitemend/schema_check.py +347 -0
- sitemend/seo_repair.py +110 -0
- sitemend/seolib.py +359 -0
- sitemend/verify.py +96 -0
- sitemend/vitals.py +205 -0
- sitemend-1.2.1.dist-info/METADATA +286 -0
- sitemend-1.2.1.dist-info/RECORD +17 -0
- sitemend-1.2.1.dist-info/WHEEL +5 -0
- sitemend-1.2.1.dist-info/entry_points.txt +2 -0
- sitemend-1.2.1.dist-info/licenses/LICENSE +21 -0
- sitemend-1.2.1.dist-info/top_level.txt +1 -0
sitemend/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""sitemend (the seo-repair skill), packaged. The audit scripts are run by path (see seo_repair.py), so this file only makes the directory a package."""
|
|
@@ -0,0 +1,466 @@
|
|
|
1
|
+
<title>SEO Repair Dashboard</title>
|
|
2
|
+
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
3
|
+
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
4
|
+
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Archivo:wght@600;700&family=IBM+Plex+Mono:wght@400;500;600&family=IBM+Plex+Sans:wght@400;500;600&display=swap">
|
|
5
|
+
|
|
6
|
+
<style>
|
|
7
|
+
/* ---- Light palette: complete token set on bare :root ---- */
|
|
8
|
+
:root{
|
|
9
|
+
--ground:#f4f6f8;
|
|
10
|
+
--surface:#ffffff;
|
|
11
|
+
--surface-2:#eceff3;
|
|
12
|
+
--ink:#171d26;
|
|
13
|
+
--ink-2:#4a5666;
|
|
14
|
+
--ink-3:#78859a;
|
|
15
|
+
--line:#dbe1e9;
|
|
16
|
+
--line-2:#c3ccd8;
|
|
17
|
+
--accent:#2c3fa8;
|
|
18
|
+
--accent-soft:#e6e9f8;
|
|
19
|
+
--accent-ink:#ffffff;
|
|
20
|
+
|
|
21
|
+
--sev-critical:#a4193d;
|
|
22
|
+
--sev-high:#c2521a;
|
|
23
|
+
--sev-medium:#8a6512;
|
|
24
|
+
--sev-low:#5c6b7e;
|
|
25
|
+
--sev-critical-bg:#fae7ec;
|
|
26
|
+
--sev-high-bg:#fbebe1;
|
|
27
|
+
--sev-medium-bg:#f8f0dc;
|
|
28
|
+
--sev-low-bg:#e9edf2;
|
|
29
|
+
|
|
30
|
+
--ok:#1f6b4a;
|
|
31
|
+
--ok-bg:#e2f0e9;
|
|
32
|
+
--block:#a4193d;
|
|
33
|
+
--block-bg:#fae7ec;
|
|
34
|
+
|
|
35
|
+
--radius:3px;
|
|
36
|
+
--measure:68ch;
|
|
37
|
+
}
|
|
38
|
+
@media (prefers-color-scheme:dark){
|
|
39
|
+
:root:not([data-theme="light"]){
|
|
40
|
+
--ground:#10141a;
|
|
41
|
+
--surface:#171c24;
|
|
42
|
+
--surface-2:#1e242e;
|
|
43
|
+
--ink:#e7ecf3;
|
|
44
|
+
--ink-2:#a9b4c3;
|
|
45
|
+
--ink-3:#7a8698;
|
|
46
|
+
--line:#28303b;
|
|
47
|
+
--line-2:#39434f;
|
|
48
|
+
--accent:#8ea0f5;
|
|
49
|
+
--accent-soft:#1c2340;
|
|
50
|
+
--accent-ink:#0d1017;
|
|
51
|
+
|
|
52
|
+
--sev-critical:#f2809b;
|
|
53
|
+
--sev-high:#f0a06f;
|
|
54
|
+
--sev-medium:#ddbc63;
|
|
55
|
+
--sev-low:#96a4b6;
|
|
56
|
+
--sev-critical-bg:#331520;
|
|
57
|
+
--sev-high-bg:#332014;
|
|
58
|
+
--sev-medium-bg:#2e2717;
|
|
59
|
+
--sev-low-bg:#222932;
|
|
60
|
+
|
|
61
|
+
--ok:#6cc39a;
|
|
62
|
+
--ok-bg:#14291f;
|
|
63
|
+
--block:#f2809b;
|
|
64
|
+
--block-bg:#331520;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
:root[data-theme="dark"]{
|
|
68
|
+
--ground:#10141a;
|
|
69
|
+
--surface:#171c24;
|
|
70
|
+
--surface-2:#1e242e;
|
|
71
|
+
--ink:#e7ecf3;
|
|
72
|
+
--ink-2:#a9b4c3;
|
|
73
|
+
--ink-3:#7a8698;
|
|
74
|
+
--line:#28303b;
|
|
75
|
+
--line-2:#39434f;
|
|
76
|
+
--accent:#8ea0f5;
|
|
77
|
+
--accent-soft:#1c2340;
|
|
78
|
+
--accent-ink:#0d1017;
|
|
79
|
+
|
|
80
|
+
--sev-critical:#f2809b;
|
|
81
|
+
--sev-high:#f0a06f;
|
|
82
|
+
--sev-medium:#ddbc63;
|
|
83
|
+
--sev-low:#96a4b6;
|
|
84
|
+
--sev-critical-bg:#331520;
|
|
85
|
+
--sev-high-bg:#332014;
|
|
86
|
+
--sev-medium-bg:#2e2717;
|
|
87
|
+
--sev-low-bg:#222932;
|
|
88
|
+
|
|
89
|
+
--ok:#6cc39a;
|
|
90
|
+
--ok-bg:#14291f;
|
|
91
|
+
--block:#f2809b;
|
|
92
|
+
--block-bg:#331520;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
*{box-sizing:border-box}
|
|
96
|
+
body{
|
|
97
|
+
margin:0; background:var(--ground); color:var(--ink);
|
|
98
|
+
font-family:"IBM Plex Sans",system-ui,-apple-system,Segoe UI,sans-serif;
|
|
99
|
+
font-size:15px; line-height:1.55;
|
|
100
|
+
-webkit-font-smoothing:antialiased;
|
|
101
|
+
}
|
|
102
|
+
.wrap{max-width:1120px;margin:0 auto;padding:40px 24px 80px;display:flex;flex-direction:column;gap:38px}
|
|
103
|
+
h1,h2,h3{font-family:Archivo,"IBM Plex Sans",system-ui,sans-serif;text-wrap:balance;margin:0}
|
|
104
|
+
h1{font-size:clamp(28px,4.2vw,42px);font-weight:700;letter-spacing:-.022em;line-height:1.08}
|
|
105
|
+
h2{font-size:19px;font-weight:600;letter-spacing:-.012em}
|
|
106
|
+
h3{font-size:15px;font-weight:600}
|
|
107
|
+
a{color:var(--accent)}
|
|
108
|
+
code,.mono{font-family:"IBM Plex Mono",ui-monospace,SFMono-Regular,Menlo,monospace}
|
|
109
|
+
:focus-visible{outline:2px solid var(--accent);outline-offset:2px;border-radius:2px}
|
|
110
|
+
|
|
111
|
+
.eyebrow{
|
|
112
|
+
font-family:"IBM Plex Mono",monospace;font-size:11px;font-weight:500;
|
|
113
|
+
letter-spacing:.14em;text-transform:uppercase;color:var(--ink-3);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/* ---- masthead ---- */
|
|
117
|
+
.masthead{display:flex;flex-direction:column;gap:14px;
|
|
118
|
+
border-bottom:1px solid var(--line-2);padding-bottom:26px}
|
|
119
|
+
.meta-row{display:flex;flex-wrap:wrap;gap:8px 22px;font-size:13px;color:var(--ink-2)}
|
|
120
|
+
.meta-row .mono{color:var(--ink)}
|
|
121
|
+
|
|
122
|
+
/* ---- summary tiles ---- */
|
|
123
|
+
.tiles{display:grid;grid-template-columns:repeat(auto-fit,minmax(140px,1fr));gap:12px}
|
|
124
|
+
.tile{
|
|
125
|
+
background:var(--surface);border:1px solid var(--line);border-radius:var(--radius);
|
|
126
|
+
padding:16px 16px 14px;display:flex;flex-direction:column;gap:6px;
|
|
127
|
+
border-left:3px solid var(--stripe,var(--line-2));
|
|
128
|
+
}
|
|
129
|
+
.tile .n{font-family:Archivo,sans-serif;font-size:32px;font-weight:700;line-height:1;
|
|
130
|
+
font-variant-numeric:tabular-nums;color:var(--stripe,var(--ink))}
|
|
131
|
+
.tile.is-zero .n{color:var(--ink-3)}
|
|
132
|
+
.tile .lbl{font-family:"IBM Plex Mono",monospace;font-size:11px;letter-spacing:.12em;
|
|
133
|
+
text-transform:uppercase;color:var(--ink-3)}
|
|
134
|
+
|
|
135
|
+
/* ---- the stage rail: the pipeline is the thesis ---- */
|
|
136
|
+
.rail{background:var(--surface);border:1px solid var(--line);border-radius:var(--radius);padding:22px}
|
|
137
|
+
.rail-scroll{overflow-x:auto;padding-bottom:4px}
|
|
138
|
+
.stages{display:flex;align-items:stretch;gap:0;min-width:660px}
|
|
139
|
+
.stage{
|
|
140
|
+
flex:1;padding:12px 14px;border-top:3px solid var(--stripe,var(--line-2));
|
|
141
|
+
display:flex;flex-direction:column;gap:5px;min-width:0;
|
|
142
|
+
}
|
|
143
|
+
.stage .num{font-family:"IBM Plex Mono",monospace;font-size:11px;color:var(--ink-3);letter-spacing:.1em}
|
|
144
|
+
.stage .nm{font-size:13px;font-weight:600;line-height:1.25}
|
|
145
|
+
.stage .ct{font-family:"IBM Plex Mono",monospace;font-size:12px;color:var(--ink-2);
|
|
146
|
+
font-variant-numeric:tabular-nums}
|
|
147
|
+
.stage.clean .ct{color:var(--ok)}
|
|
148
|
+
.stage .ct.unmeasured{color:var(--ink-3);font-style:normal;opacity:.85}
|
|
149
|
+
.gatemark{
|
|
150
|
+
display:flex;align-items:center;padding:0 2px;flex:0 0 auto;
|
|
151
|
+
}
|
|
152
|
+
.gatemark span{
|
|
153
|
+
font-family:"IBM Plex Mono",monospace;font-size:10px;font-weight:600;letter-spacing:.08em;
|
|
154
|
+
writing-mode:vertical-rl;transform:rotate(180deg);
|
|
155
|
+
padding:6px 3px;border-radius:2px;
|
|
156
|
+
}
|
|
157
|
+
.gate-clear span{background:var(--ok-bg);color:var(--ok)}
|
|
158
|
+
.gate-blocked span{background:var(--block-bg);color:var(--block)}
|
|
159
|
+
/* A gate nobody could assess reads as neutral, never as a pass -- same rule as the
|
|
160
|
+
stage rail's "not measured" state. */
|
|
161
|
+
.gate-unassessed span{background:var(--surface-2);color:var(--ink-3)}
|
|
162
|
+
|
|
163
|
+
/* ---- gates ---- */
|
|
164
|
+
.gates{display:grid;grid-template-columns:repeat(auto-fit,minmax(268px,1fr));gap:12px}
|
|
165
|
+
.gate{
|
|
166
|
+
background:var(--surface);border:1px solid var(--line);border-radius:var(--radius);
|
|
167
|
+
padding:16px;display:flex;flex-direction:column;gap:8px;
|
|
168
|
+
}
|
|
169
|
+
.gate-head{display:flex;align-items:center;gap:10px}
|
|
170
|
+
.pill{
|
|
171
|
+
font-family:"IBM Plex Mono",monospace;font-size:10.5px;font-weight:600;
|
|
172
|
+
letter-spacing:.1em;text-transform:uppercase;padding:3px 8px;border-radius:2px;white-space:nowrap;
|
|
173
|
+
}
|
|
174
|
+
.pill.clear{background:var(--ok-bg);color:var(--ok)}
|
|
175
|
+
.pill.blocked{background:var(--block-bg);color:var(--block)}
|
|
176
|
+
.pill.unassessed{background:var(--surface-2);color:var(--ink-3)}
|
|
177
|
+
.gate h3{flex:1;min-width:0}
|
|
178
|
+
.gate p{margin:0;font-size:13.5px;color:var(--ink-2);max-width:var(--measure)}
|
|
179
|
+
|
|
180
|
+
/* ---- findings ---- */
|
|
181
|
+
.stage-block{display:flex;flex-direction:column;gap:12px}
|
|
182
|
+
.stage-hd{display:flex;align-items:baseline;gap:12px;flex-wrap:wrap;
|
|
183
|
+
border-bottom:1px solid var(--line-2);padding-bottom:8px}
|
|
184
|
+
.stage-hd .cnt{font-family:"IBM Plex Mono",monospace;font-size:12px;color:var(--ink-3);
|
|
185
|
+
font-variant-numeric:tabular-nums}
|
|
186
|
+
.f{
|
|
187
|
+
background:var(--surface);border:1px solid var(--line);border-left:3px solid var(--stripe);
|
|
188
|
+
border-radius:var(--radius);padding:13px 15px;display:flex;flex-direction:column;gap:7px;
|
|
189
|
+
}
|
|
190
|
+
.f-top{display:flex;align-items:center;gap:9px;flex-wrap:wrap}
|
|
191
|
+
.f-id{font-family:"IBM Plex Mono",monospace;font-size:12px;font-weight:500;color:var(--ink-2)}
|
|
192
|
+
.f-msg{font-size:14px;max-width:var(--measure)}
|
|
193
|
+
.f-pages{display:flex;flex-wrap:wrap;gap:5px}
|
|
194
|
+
.u{font-family:"IBM Plex Mono",monospace;font-size:11.5px;background:var(--surface-2);
|
|
195
|
+
color:var(--ink-2);padding:2px 6px;border-radius:2px;border:1px solid var(--line)}
|
|
196
|
+
.ev{font-family:"IBM Plex Mono",monospace;font-size:11.5px;color:var(--ink-3);
|
|
197
|
+
background:var(--surface-2);border:1px solid var(--line);border-radius:2px;
|
|
198
|
+
padding:7px 9px;overflow-x:auto;white-space:pre-wrap;word-break:break-word}
|
|
199
|
+
.ev b{color:var(--ink-2);font-weight:500}
|
|
200
|
+
|
|
201
|
+
/* ---- tables ---- */
|
|
202
|
+
.tbl-scroll{overflow-x:auto;border:1px solid var(--line);border-radius:var(--radius);background:var(--surface)}
|
|
203
|
+
table{border-collapse:collapse;width:100%;font-size:13.5px}
|
|
204
|
+
th,td{text-align:left;padding:9px 13px;border-bottom:1px solid var(--line);vertical-align:top}
|
|
205
|
+
th{font-family:"IBM Plex Mono",monospace;font-size:10.5px;letter-spacing:.1em;
|
|
206
|
+
text-transform:uppercase;color:var(--ink-3);font-weight:500;white-space:nowrap}
|
|
207
|
+
tr:last-child td{border-bottom:none}
|
|
208
|
+
td.num{font-family:"IBM Plex Mono",monospace;font-variant-numeric:tabular-nums;white-space:nowrap}
|
|
209
|
+
td .u{font-size:11.5px}
|
|
210
|
+
.dot{display:inline-block;width:7px;height:7px;border-radius:50%;margin-right:7px;vertical-align:1px}
|
|
211
|
+
|
|
212
|
+
/* ---- handoff ---- */
|
|
213
|
+
.handoffs{display:grid;grid-template-columns:repeat(auto-fit,minmax(300px,1fr));gap:12px}
|
|
214
|
+
.ho{background:var(--surface);border:1px dashed var(--line-2);border-radius:var(--radius);
|
|
215
|
+
padding:15px;display:flex;flex-direction:column;gap:6px}
|
|
216
|
+
.ho .where{font-family:"IBM Plex Mono",monospace;font-size:12px;color:var(--accent)}
|
|
217
|
+
.ho p{margin:0;font-size:13px;color:var(--ink-2);max-width:var(--measure)}
|
|
218
|
+
|
|
219
|
+
.note{font-size:13.5px;color:var(--ink-2);max-width:var(--measure)}
|
|
220
|
+
.foot{border-top:1px solid var(--line-2);padding-top:18px;font-size:12.5px;color:var(--ink-3);
|
|
221
|
+
max-width:var(--measure)}
|
|
222
|
+
section{display:flex;flex-direction:column;gap:14px}
|
|
223
|
+
[hidden]{display:none !important}
|
|
224
|
+
@media (max-width:640px){
|
|
225
|
+
.wrap{padding:26px 15px 60px;gap:30px}
|
|
226
|
+
.tile .n{font-size:26px}
|
|
227
|
+
}
|
|
228
|
+
</style>
|
|
229
|
+
|
|
230
|
+
<div class="wrap">
|
|
231
|
+
<header class="masthead">
|
|
232
|
+
<div class="eyebrow">SEO Repair · crawl-driven audit</div>
|
|
233
|
+
<h1 id="site"> </h1>
|
|
234
|
+
<div class="meta-row" id="meta"></div>
|
|
235
|
+
</header>
|
|
236
|
+
|
|
237
|
+
<section>
|
|
238
|
+
<div class="tiles" id="tiles"></div>
|
|
239
|
+
<p class="note" id="tiernote">
|
|
240
|
+
Every figure here was measured directly from the crawl. Search volume, keyword
|
|
241
|
+
difficulty, backlink data and field Core Web Vitals are absent by design — they
|
|
242
|
+
cannot be observed from a codebase, so they appear as handoff actions below rather
|
|
243
|
+
than as estimates.
|
|
244
|
+
</p>
|
|
245
|
+
</section>
|
|
246
|
+
|
|
247
|
+
<section>
|
|
248
|
+
<div class="eyebrow">Repair pipeline</div>
|
|
249
|
+
<div class="rail">
|
|
250
|
+
<div class="rail-scroll"><div class="stages" id="rail"></div></div>
|
|
251
|
+
</div>
|
|
252
|
+
<div class="gates" id="gates"></div>
|
|
253
|
+
</section>
|
|
254
|
+
|
|
255
|
+
<section id="vitals-sec" hidden>
|
|
256
|
+
<div class="eyebrow">Core Web Vitals · measured, lab</div>
|
|
257
|
+
<h2>Lighthouse</h2>
|
|
258
|
+
<p class="note" id="vitals-caveat"></p>
|
|
259
|
+
<div class="tbl-scroll"><table id="vitals-tbl"></table></div>
|
|
260
|
+
</section>
|
|
261
|
+
|
|
262
|
+
<section>
|
|
263
|
+
<div class="eyebrow">Findings</div>
|
|
264
|
+
<h2>By stage, in repair order</h2>
|
|
265
|
+
<div id="findings" style="display:flex;flex-direction:column;gap:30px"></div>
|
|
266
|
+
</section>
|
|
267
|
+
|
|
268
|
+
<section>
|
|
269
|
+
<div class="eyebrow">Pages</div>
|
|
270
|
+
<h2>Crawled</h2>
|
|
271
|
+
<div class="tbl-scroll"><table id="pages-tbl"></table></div>
|
|
272
|
+
<div id="intentional"></div>
|
|
273
|
+
</section>
|
|
274
|
+
|
|
275
|
+
<section>
|
|
276
|
+
<div class="eyebrow">Handoff</div>
|
|
277
|
+
<h2>Requires tools outside this environment</h2>
|
|
278
|
+
<div class="handoffs" id="handoffs"></div>
|
|
279
|
+
</section>
|
|
280
|
+
|
|
281
|
+
<footer class="foot" id="foot"></footer>
|
|
282
|
+
</div>
|
|
283
|
+
|
|
284
|
+
<script>
|
|
285
|
+
const DATA = /*__DATA__*/ null;
|
|
286
|
+
|
|
287
|
+
const SEV = ["critical","high","medium","low"];
|
|
288
|
+
const sevVar = s => `var(--sev-${s})`;
|
|
289
|
+
const el = (t,c,x) => { const n=document.createElement(t); if(c)n.className=c;
|
|
290
|
+
if(x!==undefined)n.textContent=x; return n; };
|
|
291
|
+
const STAGE_NAMES = {1:"Crawl & Indexing",2:"Architecture",3:"Content & On-Page",
|
|
292
|
+
4:"Performance",5:"Structured Data",6:"Authority",7:"QA & Monitoring"};
|
|
293
|
+
// Three states, not two. Anything that is not an explicit CLEAR must not be painted
|
|
294
|
+
// as a pass -- "NOT ASSESSED" used to fall through to the green pill.
|
|
295
|
+
const gateClass = s => s==="BLOCKED" ? "gate-blocked"
|
|
296
|
+
: s==="CLEAR" ? "gate-clear"
|
|
297
|
+
: "gate-unassessed";
|
|
298
|
+
|
|
299
|
+
function render(d){
|
|
300
|
+
document.getElementById("site").textContent = d.site;
|
|
301
|
+
document.title = d.site + " \u2014 SEO Repair";
|
|
302
|
+
|
|
303
|
+
const meta = document.getElementById("meta");
|
|
304
|
+
[["Generated",d.generated],["Pages",String(d.meta.page_count)],
|
|
305
|
+
["Mode",d.meta.mode],["Source",d.meta.source]].forEach(([k,v])=>{
|
|
306
|
+
const s=el("div"); s.append(el("span","",k+" "), el("span","mono",v)); meta.append(s);
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
// severity tiles
|
|
310
|
+
const tiles=document.getElementById("tiles");
|
|
311
|
+
const total=SEV.reduce((a,s)=>a+(d.counts[s]||0),0);
|
|
312
|
+
SEV.forEach(s=>{
|
|
313
|
+
const n=d.counts[s]||0, t=el("div","tile"+(n?"":" is-zero"));
|
|
314
|
+
t.style.setProperty("--stripe", n?sevVar(s):"var(--line-2)");
|
|
315
|
+
t.append(el("div","n",String(n)), el("div","lbl",s));
|
|
316
|
+
tiles.append(t);
|
|
317
|
+
});
|
|
318
|
+
const t=el("div","tile"); t.style.setProperty("--stripe","var(--accent)");
|
|
319
|
+
t.append(el("div","n",String(total)), el("div","lbl","total"));
|
|
320
|
+
tiles.append(t);
|
|
321
|
+
|
|
322
|
+
// stage rail with gates interleaved \u2014 the ordering is the point
|
|
323
|
+
const rail=document.getElementById("rail");
|
|
324
|
+
const gateAfter={1:d.gates[0],2:d.gates[1],5:d.gates[2]};
|
|
325
|
+
for(let i=1;i<=7;i++){
|
|
326
|
+
const items=d.findings.filter(f=>f.stage===i);
|
|
327
|
+
const worst=SEV.find(s=>items.some(f=>f.severity===s));
|
|
328
|
+
const st=el("div","stage");
|
|
329
|
+
st.style.setProperty("--stripe", worst?sevVar(worst):"var(--line-2)");
|
|
330
|
+
const a = (d.assessed && d.assessed[String(i)]) || {state:"assessed"};
|
|
331
|
+
let label, cls = "";
|
|
332
|
+
if (items.length) {
|
|
333
|
+
label = items.length + (items.length===1 ? " finding" : " findings");
|
|
334
|
+
} else if (a.state === "assessed") {
|
|
335
|
+
label = "clear"; cls = " clean";
|
|
336
|
+
} else if (a.state === "advisory") {
|
|
337
|
+
label = "advisory";
|
|
338
|
+
} else if (a.state === "not-run") {
|
|
339
|
+
label = "not run";
|
|
340
|
+
} else {
|
|
341
|
+
label = "not measured";
|
|
342
|
+
}
|
|
343
|
+
if (cls) st.className = "stage clean";
|
|
344
|
+
const ct = el("div","ct",label);
|
|
345
|
+
if (a.state !== "assessed") { ct.classList.add("unmeasured"); if (a.note) ct.title = a.note; }
|
|
346
|
+
st.append(el("div","num",String(i).padStart(2,"0")),
|
|
347
|
+
el("div","nm",STAGE_NAMES[i]), ct);
|
|
348
|
+
rail.append(st);
|
|
349
|
+
const g=gateAfter[i];
|
|
350
|
+
if(g){
|
|
351
|
+
const m=el("div","gatemark "+gateClass(g.status));
|
|
352
|
+
m.append(el("span","","GATE "+g.gate));
|
|
353
|
+
m.title=g.name+" \u2014 "+g.status;
|
|
354
|
+
rail.append(m);
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
// gates
|
|
359
|
+
const gw=document.getElementById("gates");
|
|
360
|
+
d.gates.forEach(g=>{
|
|
361
|
+
const c=el("div","gate"), h=el("div","gate-head");
|
|
362
|
+
h.append(el("h3","","Gate "+g.gate+" \u00b7 "+g.name),
|
|
363
|
+
el("span","pill "+gateClass(g.status).replace("gate-",""),g.status));
|
|
364
|
+
c.append(h, el("p","",g.rule), el("div","eyebrow",g.detail));
|
|
365
|
+
gw.append(c);
|
|
366
|
+
});
|
|
367
|
+
|
|
368
|
+
// vitals
|
|
369
|
+
if(d.vitals && d.vitals.runs && d.vitals.runs.length){
|
|
370
|
+
document.getElementById("vitals-sec").hidden=false;
|
|
371
|
+
document.getElementById("vitals-caveat").textContent=d.vitals.caveat;
|
|
372
|
+
const tb=document.getElementById("vitals-tbl");
|
|
373
|
+
tb.innerHTML="<thead><tr><th>Page</th><th>Profile</th><th>LCP</th><th>CLS</th>"+
|
|
374
|
+
"<th>TBT</th><th>Score</th></tr></thead>";
|
|
375
|
+
const bd=el("tbody");
|
|
376
|
+
d.vitals.runs.forEach(r=>{
|
|
377
|
+
const m=r.metrics, tr=el("tr");
|
|
378
|
+
tr.append(cellU(r.page), td(r.profile),
|
|
379
|
+
num(m["largest-contentful-paint"], v=>Math.round(v)+" ms", v=>v>4000?"critical":v>2500?"medium":null),
|
|
380
|
+
num(m["cumulative-layout-shift"], v=>v.toFixed(3), v=>v>0.25?"critical":v>0.1?"medium":null),
|
|
381
|
+
num(m["total-blocking-time"], v=>Math.round(v)+" ms", v=>v>600?"high":v>200?"medium":null),
|
|
382
|
+
num(m._score, v=>Math.round(v*100), v=>v<0.5?"high":v<0.9?"medium":null));
|
|
383
|
+
bd.append(tr);
|
|
384
|
+
});
|
|
385
|
+
tb.append(bd);
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
// findings by stage
|
|
389
|
+
const fw=document.getElementById("findings");
|
|
390
|
+
for(let i=1;i<=7;i++){
|
|
391
|
+
const items=d.findings.filter(f=>f.stage===i);
|
|
392
|
+
if(!items.length) continue;
|
|
393
|
+
items.sort((a,b)=>SEV.indexOf(a.severity)-SEV.indexOf(b.severity));
|
|
394
|
+
const blk=el("div","stage-block"), hd=el("div","stage-hd");
|
|
395
|
+
hd.append(el("h2","","Stage "+i+" \u00b7 "+STAGE_NAMES[i]),
|
|
396
|
+
el("span","cnt", items.length+" \u00b7 batch B"+i));
|
|
397
|
+
blk.append(hd);
|
|
398
|
+
items.forEach(f=>{
|
|
399
|
+
const c=el("div","f"); c.style.setProperty("--stripe",sevVar(f.severity));
|
|
400
|
+
const top=el("div","f-top");
|
|
401
|
+
const p=el("span","pill",f.severity);
|
|
402
|
+
p.style.background=`var(--sev-${f.severity}-bg)`; p.style.color=sevVar(f.severity);
|
|
403
|
+
top.append(p, el("span","f-id",f.id));
|
|
404
|
+
if(f.affects) top.append(el("span","eyebrow", f.affects+" pages"));
|
|
405
|
+
c.append(top, el("div","f-msg",f.message));
|
|
406
|
+
const pg=el("div","f-pages");
|
|
407
|
+
f.pages.slice(0,8).forEach(u=>pg.append(el("span","u",u)));
|
|
408
|
+
if(f.pages.length>8) pg.append(el("span","eyebrow","+"+(f.pages.length-8)+" more"));
|
|
409
|
+
c.append(pg);
|
|
410
|
+
if(f.evidence){ const e=el("div","ev"); e.append(el("b","","evidence "), document.createTextNode(f.evidence)); c.append(e); }
|
|
411
|
+
blk.append(c);
|
|
412
|
+
});
|
|
413
|
+
fw.append(blk);
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
// pages
|
|
417
|
+
const pt=document.getElementById("pages-tbl");
|
|
418
|
+
pt.innerHTML="<thead><tr><th>URL</th><th>Title</th><th>Words</th><th>Schema</th></tr></thead>";
|
|
419
|
+
const pb=el("tbody");
|
|
420
|
+
d.pages.forEach(p=>{
|
|
421
|
+
const tr=el("tr");
|
|
422
|
+
const c1=el("td");
|
|
423
|
+
const dot=el("span","dot"); dot.style.background=p.indexable?"var(--ok)":"var(--ink-3)";
|
|
424
|
+
dot.title=p.indexable?"indexable":"not indexed (deliberate)";
|
|
425
|
+
c1.append(dot, el("span","u",p.url));
|
|
426
|
+
tr.append(c1, td(p.title||"\u2014"), num(p.words, v=>v, v=>v<250?"medium":null),
|
|
427
|
+
td(p.jsonld && p.jsonld.length ? p.jsonld.join(", ") : "\u2014"));
|
|
428
|
+
pb.append(tr);
|
|
429
|
+
});
|
|
430
|
+
pt.append(pb);
|
|
431
|
+
|
|
432
|
+
if(d.intentional && d.intentional.length){
|
|
433
|
+
const w=document.getElementById("intentional");
|
|
434
|
+
w.append(el("div","eyebrow","Deliberate exclusions \u2014 not defects"));
|
|
435
|
+
const ul=el("div","f-pages"); ul.style.marginTop="8px";
|
|
436
|
+
d.intentional.forEach(i=>{
|
|
437
|
+
const s=el("span","u", i.url+" \u00b7 "+i.why.join("; ")); ul.append(s);
|
|
438
|
+
});
|
|
439
|
+
w.append(ul);
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
// handoffs
|
|
443
|
+
const hw=document.getElementById("handoffs");
|
|
444
|
+
d.handoffs.forEach(h=>{
|
|
445
|
+
const c=el("div","ho");
|
|
446
|
+
c.append(el("h3","",h.name), el("div","where",h.where), el("p","",h.why));
|
|
447
|
+
hw.append(c);
|
|
448
|
+
});
|
|
449
|
+
|
|
450
|
+
document.getElementById("foot").textContent =
|
|
451
|
+
"Generated by the seo-repair skill on "+d.generated+". Every finding cites evidence "+
|
|
452
|
+
"from the crawl. Nothing in this dashboard was inferred from data the tool could not observe.";
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
function td(x){ return el("td","",x==null?"\u2014":String(x)); }
|
|
456
|
+
function cellU(u){ const c=el("td"); c.append(el("span","u",u)); return c; }
|
|
457
|
+
function num(v,fmt,sev){
|
|
458
|
+
const c=el("td","num");
|
|
459
|
+
if(v==null){ c.textContent="\u2014"; return c; }
|
|
460
|
+
c.textContent=fmt(v);
|
|
461
|
+
const s=sev&&sev(v); if(s) c.style.color=sevVar(s);
|
|
462
|
+
return c;
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
if(DATA) render(DATA);
|
|
466
|
+
</script>
|