Skip to content

Commit 0901ede

Browse files
committed
More work on light House
1 parent 1cda994 commit 0901ede

10 files changed

Lines changed: 684 additions & 960 deletions

File tree

UI/src/App.jsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import {
77
Repeat,
88
FileText,
99
ShieldAlert,
10+
Gauge,
1011
PieChart,
1112
Share2,
1213
Search,
@@ -20,6 +21,7 @@ import Links from './views/Links';
2021
import Redirects from './views/Redirects';
2122
import Content from './views/Content';
2223
import Security from './views/Security';
24+
import Lighthouse from './views/Lighthouse';
2325
import Charts from './views/Charts';
2426
import Network from './views/Network';
2527

@@ -29,6 +31,7 @@ const VIEWS = [
2931
{ id: 'links', label: 'Link Explorer', component: Links, section: 'Deep Analysis', icon: LinkIcon },
3032
{ id: 'redirects', label: 'Redirects', component: Redirects, section: 'Deep Analysis', icon: Repeat },
3133
{ id: 'content', label: 'On-Page Content', component: Content, section: 'Deep Analysis', icon: FileText },
34+
{ id: 'lighthouse', label: 'Lighthouse', component: Lighthouse, section: 'Deep Analysis', icon: Gauge },
3235
{ id: 'security', label: 'Security & V', component: Security, section: 'Deep Analysis', icon: ShieldAlert },
3336
{ id: 'charts', label: 'Chart.js Analytics', component: Charts, section: 'Data Visualizations', icon: PieChart },
3437
{ id: 'network', label: 'Site Architecture', component: Network, section: 'Data Visualizations', icon: Share2 },

UI/src/views/Lighthouse.jsx

Lines changed: 299 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,299 @@
1+
import { useReport } from '../context/useReport';
2+
3+
// Lighthouse score color: 0-49 red, 50-89 orange, 90-100 green
4+
function scoreColor(score) {
5+
if (score == null) return 'rgb(71, 85, 105)'; // slate-500
6+
const s = Number(score);
7+
if (s >= 90) return 'rgb(34, 197, 94)'; // green-500
8+
if (s >= 50) return 'rgb(234, 179, 8)'; // yellow-500
9+
return 'rgb(239, 68, 68)'; // red-500
10+
}
11+
12+
function scoreRingColor(score) {
13+
if (score == null) return 'rgb(51, 65, 85)'; // slate-700
14+
return scoreColor(score);
15+
}
16+
17+
// Format metric for display: ms -> "0.3 s" or "120 ms", CLS -> "0" or "0.05"
18+
function formatMetric(key, value) {
19+
if (value == null || value === '') return '—';
20+
const v = Number(value);
21+
if (key === 'cls') return v === 0 ? '0' : v.toFixed(2);
22+
if (key === 'lcp_ms' || key === 'fcp_ms' || key === 'speed_index_ms') {
23+
if (v >= 1000) return `${(v / 1000).toFixed(1)} s`;
24+
return `${Math.round(v)} ms`;
25+
}
26+
if (key === 'tbt_ms') return `${Math.round(v)} ms`;
27+
return String(value);
28+
}
29+
30+
// Metric good/warning/poor for coloring (Lighthouse style)
31+
function metricStatus(key, value) {
32+
if (value == null) return 'neutral';
33+
const v = Number(value);
34+
if (key === 'lcp_ms') return v <= 2500 ? 'good' : v <= 4000 ? 'warn' : 'poor';
35+
if (key === 'fcp_ms') return v <= 1800 ? 'good' : v <= 3000 ? 'warn' : 'poor';
36+
if (key === 'tbt_ms') return v <= 200 ? 'good' : v <= 600 ? 'warn' : 'poor';
37+
if (key === 'cls') return v <= 0.1 ? 'good' : v <= 0.25 ? 'warn' : 'poor';
38+
if (key === 'speed_index_ms') return v <= 3400 ? 'good' : v <= 5800 ? 'warn' : 'poor';
39+
return 'neutral';
40+
}
41+
42+
function metricTextClass(status) {
43+
if (status === 'good') return 'text-green-400';
44+
if (status === 'warn') return 'text-yellow-400';
45+
if (status === 'poor') return 'text-red-400';
46+
return 'text-slate-400';
47+
}
48+
49+
function severityClass(s) {
50+
if (s === 'High') return 'bg-red-500/20 text-red-400 border border-red-500/30';
51+
if (s === 'Medium') return 'bg-yellow-500/20 text-yellow-400 border border-yellow-500/30';
52+
return 'bg-slate-500/20 text-slate-400 border border-slate-500/30';
53+
}
54+
55+
const CATEGORIES = [
56+
{ id: 'performance', label: 'Performance' },
57+
{ id: 'accessibility', label: 'Accessibility' },
58+
{ id: 'best-practices', label: 'Best Practices' },
59+
{ id: 'seo', label: 'SEO' },
60+
{ id: 'pwa', label: 'PWA' },
61+
];
62+
63+
const METRICS = [
64+
{ key: 'fcp_ms', label: 'First Contentful Paint' },
65+
{ key: 'lcp_ms', label: 'Largest Contentful Paint' },
66+
{ key: 'tbt_ms', label: 'Total Blocking Time' },
67+
{ key: 'cls', label: 'Cumulative Layout Shift' },
68+
{ key: 'speed_index_ms', label: 'Speed Index' },
69+
];
70+
71+
export default function Lighthouse() {
72+
const { data } = useReport();
73+
if (!data) return null;
74+
75+
const summary = data.lighthouse_summary || {};
76+
const diagnostics = data.lighthouse_diagnostics || data.lighthouse_summary?.diagnostics || [];
77+
const humanSummary = data.lighthouse_human_summary || summary.human_summary || '';
78+
const mm = summary.median_metrics || {};
79+
const cs = summary.category_scores || {};
80+
const topFailures = summary.top_failures || [];
81+
const strategy = summary.strategy || 'mobile';
82+
const device = summary.device || strategy;
83+
const mode = summary.mode || 'navigation';
84+
const categories = summary.categories || ['performance', 'accessibility', 'best-practices', 'seo', 'pwa'];
85+
const runTimestamp = summary.run_timestamp || '';
86+
const iterations = summary.iterations ?? 0;
87+
88+
const categoryLabels = {
89+
performance: 'Performance',
90+
accessibility: 'Accessibility',
91+
'best-practices': 'Best practices',
92+
seo: 'SEO',
93+
pwa: 'PWA',
94+
};
95+
96+
const hasData = summary.url || diagnostics.length > 0 || topFailures.length > 0;
97+
98+
if (!hasData) {
99+
return (
100+
<div className="p-6 lg:p-8">
101+
<div className="mb-8">
102+
<h1 className="text-3xl font-bold text-white mb-2">Lighthouse</h1>
103+
<p className="text-slate-400">
104+
Core Web Vitals and audit results from Lighthouse.
105+
</p>
106+
</div>
107+
<div className="bg-brand-800 border border-slate-700 rounded-xl p-8 text-center">
108+
<p className="text-slate-500">
109+
No Lighthouse data yet. Run <code className="bg-brand-900 px-2 py-1 rounded text-slate-300">python -m src lighthouse</code> and regenerate the report to see results here.
110+
</p>
111+
</div>
112+
</div>
113+
);
114+
}
115+
116+
return (
117+
<div className="p-6 lg:p-8 max-w-6xl mx-auto">
118+
{/* Header: URL + run info */}
119+
<div className="mb-8">
120+
<h1 className="text-3xl font-bold text-white mb-2">Lighthouse report</h1>
121+
{summary.url && (
122+
<p className="text-slate-400 text-sm mb-1">
123+
<a href={summary.url} target="_blank" rel="noreferrer" className="text-blue-400 hover:underline break-all">{summary.url}</a>
124+
</p>
125+
)}
126+
{/* Analysis settings: Mode, Device, Categories */}
127+
<div className="mt-4 p-4 bg-brand-800 border border-slate-700 rounded-xl">
128+
<h3 className="text-slate-400 text-xs font-bold uppercase tracking-wider mb-3">Analysis settings</h3>
129+
<div className="flex flex-wrap gap-6 text-sm">
130+
<div>
131+
<span className="text-slate-500 block text-xs mb-0.5">Mode</span>
132+
<span className="text-slate-200 font-medium capitalize">{mode}</span>
133+
<span className="text-slate-500 text-xs ml-1">(default: Navigation)</span>
134+
</div>
135+
<div>
136+
<span className="text-slate-500 block text-xs mb-0.5">Device</span>
137+
<span className="text-slate-200 font-medium capitalize">{device}</span>
138+
<span className="text-slate-500 text-xs ml-1">(Mobile / Desktop)</span>
139+
</div>
140+
<div className="min-w-0">
141+
<span className="text-slate-500 block text-xs mb-0.5">Categories</span>
142+
<span className="text-slate-200 font-medium">
143+
{Array.isArray(categories)
144+
? categories.map((c) => categoryLabels[c] || c).join(', ')
145+
: 'Performance, Accessibility, Best practices, SEO, PWA'}
146+
</span>
147+
</div>
148+
</div>
149+
{(runTimestamp || iterations) && (
150+
<p className="text-slate-500 text-xs mt-3 pt-3 border-t border-slate-700">
151+
{iterations > 0 && <span>Runs: {iterations} (medians shown)</span>}
152+
{runTimestamp && <span className="ml-3">Generated: {new Date(runTimestamp).toLocaleString()}</span>}
153+
</p>
154+
)}
155+
</div>
156+
</div>
157+
158+
{/* Category score circles - like official Lighthouse */}
159+
<div className="mb-10">
160+
<h2 className="text-slate-400 text-xs font-bold uppercase tracking-wider mb-4">Categories</h2>
161+
<div className="flex flex-wrap gap-6 justify-start items-center">
162+
{CATEGORIES.map(({ id, label }) => {
163+
const score = cs[id] != null ? Number(cs[id]) : null;
164+
const color = scoreRingColor(score);
165+
const displayScore = score != null ? score : '—';
166+
return (
167+
<div key={id} className="flex flex-col items-center">
168+
<div className="relative w-24 h-24">
169+
<svg viewBox="0 0 36 36" className="w-24 h-24 -rotate-90">
170+
<path
171+
d="M18 2.0845 a 15.9155 15.9155 0 0 1 0 31.831 a 15.9155 15.9155 0 0 1 0 -31.831"
172+
fill="none"
173+
stroke="rgb(51, 65, 85)"
174+
strokeWidth="3"
175+
/>
176+
<path
177+
d="M18 2.0845 a 15.9155 15.9155 0 0 1 0 31.831 a 15.9155 15.9155 0 0 1 0 -31.831"
178+
fill="none"
179+
stroke={color}
180+
strokeWidth="3"
181+
strokeDasharray={score != null ? `${score}, 100` : '0, 100'}
182+
strokeLinecap="round"
183+
/>
184+
</svg>
185+
<div className="absolute inset-0 flex items-center justify-center">
186+
<span className="text-xl font-bold text-white">{displayScore}</span>
187+
</div>
188+
</div>
189+
<span className="text-slate-400 text-xs font-medium mt-2 text-center">{label}</span>
190+
</div>
191+
);
192+
})}
193+
</div>
194+
{/* Score legend */}
195+
<div className="flex flex-wrap gap-6 mt-4 text-xs text-slate-500">
196+
<span><span className="inline-block w-2 h-2 rounded-full bg-red-500 mr-1" />0–49 Poor</span>
197+
<span><span className="inline-block w-2 h-2 rounded-full bg-yellow-500 mr-1" />50–89 Needs improvement</span>
198+
<span><span className="inline-block w-2 h-2 rounded-full bg-green-500 mr-1" />90–100 Good</span>
199+
</div>
200+
</div>
201+
202+
{/* Metrics section - like official Lighthouse */}
203+
<div className="mb-10">
204+
<h2 className="text-slate-400 text-xs font-bold uppercase tracking-wider mb-3">Metrics</h2>
205+
<p className="text-slate-500 text-sm mb-4">
206+
Values are estimated and may vary. The performance score is calculated from these metrics. Medians from {iterations || 1} run(s).
207+
</p>
208+
<div className="bg-brand-800 border border-slate-700 rounded-xl overflow-hidden">
209+
<div className="divide-y divide-slate-700">
210+
{METRICS.map(({ key, label }) => {
211+
const value = mm[key];
212+
const status = metricStatus(key, value);
213+
const textClass = metricTextClass(status);
214+
return (
215+
<div key={key} className="flex items-center justify-between px-5 py-4">
216+
<span className="text-slate-300 text-sm">{label}</span>
217+
<span className={`font-semibold text-sm ${textClass}`}>{formatMetric(key, value)}</span>
218+
</div>
219+
);
220+
})}
221+
</div>
222+
</div>
223+
</div>
224+
225+
{/* Human summary */}
226+
{humanSummary && (
227+
<div className="mb-10 bg-brand-800 border border-slate-700 rounded-xl p-5">
228+
<h2 className="text-slate-200 text-sm font-bold uppercase tracking-wider mb-3">Summary</h2>
229+
<pre className="text-slate-400 text-sm whitespace-pre-wrap font-sans">{humanSummary}</pre>
230+
</div>
231+
)}
232+
233+
{/* Opportunities / Diagnostics - like Lighthouse's actionable section */}
234+
<div className="mb-8">
235+
<h2 className="text-slate-400 text-xs font-bold uppercase tracking-wider mb-3">Diagnostics &amp; fixes</h2>
236+
<p className="text-slate-500 text-sm mb-4">
237+
Issues and recommendations with one-line fixes and evidence. Address these to improve scores.
238+
</p>
239+
{(diagnostics.length > 0 ? diagnostics : topFailures.map((f) => ({
240+
warning: f.helpText || f.id,
241+
lighthouse_audit_id: f.id,
242+
primary_impact: f.impact || 'UX',
243+
severity: 'High',
244+
one_line_fix: 'See Lighthouse report for fix.',
245+
evidence: f.evidence || [],
246+
}))).length === 0 ? (
247+
<div className="bg-brand-800 border border-slate-700 rounded-xl p-6 text-center text-slate-500 text-sm">
248+
No failing audits — all checks passed.
249+
</div>
250+
) : (
251+
<div className="space-y-4">
252+
{(diagnostics.length > 0 ? diagnostics : topFailures.map((f) => ({
253+
warning: f.helpText || f.id,
254+
lighthouse_audit_id: f.id,
255+
primary_impact: f.impact || 'UX',
256+
severity: 'High',
257+
one_line_fix: 'See Lighthouse report for fix.',
258+
evidence: f.evidence || [],
259+
}))).slice(0, 20).map((d, i) => (
260+
<div
261+
key={i}
262+
className="bg-brand-800 border border-slate-700 rounded-xl p-5 flex flex-col gap-3"
263+
>
264+
<div className="flex items-center gap-3 flex-wrap">
265+
<span className={`px-2 py-0.5 rounded text-[10px] font-bold uppercase ${severityClass(d.severity)}`}>
266+
{d.severity || 'Medium'}
267+
</span>
268+
<span className="text-xs font-semibold text-blue-400">{d.lighthouse_audit_id || d.id}</span>
269+
<span className="text-xs text-slate-500">{d.primary_impact || d.impact}</span>
270+
</div>
271+
<p className="text-slate-200 text-sm">{d.warning || d.helpText || '—'}</p>
272+
<div className="bg-brand-900 rounded p-3 border border-slate-800">
273+
<div className="text-[10px] text-blue-400 font-bold uppercase mb-1">How to fix</div>
274+
<p className="text-slate-300 text-sm">{d.one_line_fix || '—'}</p>
275+
{d.detailed_fix && (
276+
<p className="text-slate-500 text-xs mt-2">{d.detailed_fix}</p>
277+
)}
278+
</div>
279+
{Array.isArray(d.evidence) && d.evidence.length > 0 && (
280+
<div className="text-xs">
281+
<span className="text-slate-500 font-semibold">Evidence: </span>
282+
<ul className="list-disc list-inside text-slate-400 mt-1">
283+
{d.evidence.slice(0, 5).map((ev, j) => (
284+
<li key={j} className="truncate max-w-full" title={ev}>{ev}</li>
285+
))}
286+
</ul>
287+
</div>
288+
)}
289+
{d.estimated_impact && (
290+
<p className="text-slate-500 text-xs">Estimated impact: {d.estimated_impact}</p>
291+
)}
292+
</div>
293+
))}
294+
</div>
295+
)}
296+
</div>
297+
</div>
298+
);
299+
}

0 commit comments

Comments
 (0)