feat: add security headers + SSL Labs checks; compact layout
- checker.js: checkSecurityHeaders() graded A+-F, checkSSL() via SSL Labs API - Both run in parallel with sitespeed.io to minimise wait time - DB: auto-migrate headers_json + ssl_json columns - Layout: coach scores + CWV side-by-side, headers + SSL below - Scorecard + CWV made compact Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -8,10 +8,10 @@
|
||||
- return val <= t[0] ? 'bg-green-50 border-green-300' : val <= t[1] ? 'bg-yellow-50 border-yellow-300' : 'bg-red-50 border-red-300';
|
||||
- }
|
||||
|
||||
div(class='bg-white border border-gray-200 rounded-xl p-5')
|
||||
h2(class='text-base font-semibold mb-4') Core Web Vitals
|
||||
div(class='grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-5 gap-3')
|
||||
div(class='bg-white border border-gray-200 rounded-xl p-4 h-full')
|
||||
h2(class='text-sm font-semibold mb-3 text-gray-600 uppercase tracking-wide') Core Web Vitals
|
||||
div(class='grid grid-cols-3 sm:grid-cols-5 gap-2')
|
||||
each item in [['LCP', 'lcp', ms(job.lcp)], ['FCP', 'fcp', ms(job.fcp)], ['TBT', 'tbt', raw(job.tbt,'ms')], ['CLS', 'cls', raw(job.cls,'')], ['TTFB', 'ttfb', ms(job.ttfb)]]
|
||||
div(class=`metric-card border ${cwvClass(item[1], job[item[1]])} text-center`)
|
||||
div(class='text-2xl font-bold')= item[2]
|
||||
div(class='text-xs text-gray-500 mt-1')= item[0]
|
||||
div(class=`border rounded-lg px-2 py-2 ${cwvClass(item[1], job[item[1]])} text-center`)
|
||||
div(class='text-lg font-bold leading-tight')= item[2]
|
||||
div(class='text-xs text-gray-500 mt-0.5')= item[0]
|
||||
|
||||
21
views/partials/headers.pug
Normal file
21
views/partials/headers.pug
Normal file
@@ -0,0 +1,21 @@
|
||||
- const hd = job.headers_json ? JSON.parse(job.headers_json) : null
|
||||
- function hdColor(g) { return !g ? 'bg-gray-100 text-gray-400' : g === 'A+' ? 'bg-green-100 text-green-700' : g === 'A' ? 'bg-green-100 text-green-700' : g === 'B' ? 'bg-yellow-100 text-yellow-700' : g === 'C' ? 'bg-orange-100 text-orange-700' : 'bg-red-100 text-red-700'; }
|
||||
|
||||
div(class='bg-white border border-gray-200 rounded-xl p-5')
|
||||
h2(class='text-base font-semibold mb-3') Security Headers
|
||||
if hd
|
||||
div(class='flex items-start gap-5 flex-wrap')
|
||||
div(class='flex flex-col items-center gap-1')
|
||||
div(class=`score-circle w-14 h-14 text-lg ${hdColor(hd.grade)}`)= hd.grade
|
||||
span(class='text-xs text-gray-500') Grade
|
||||
span(class='text-xs text-gray-400 mt-0.5')= hd.headers.filter(h=>h.present).length + '/' + hd.headers.length
|
||||
div(class='grid grid-cols-1 sm:grid-cols-2 gap-1 flex-1')
|
||||
each h in hd.headers
|
||||
div(class=`flex items-center justify-between rounded px-3 py-1.5 text-xs ${h.present ? 'bg-green-50' : h.important ? 'bg-red-50' : 'bg-gray-50'}`)
|
||||
span(class='font-mono')= h.label
|
||||
if h.present
|
||||
span(class='text-green-600 font-bold ml-2') ✓
|
||||
else
|
||||
span(class=`font-bold ml-2 ${h.important ? 'text-red-500' : 'text-gray-400'}`) ✗
|
||||
else
|
||||
p(class='text-sm text-gray-400 italic') Not available
|
||||
@@ -1,12 +1,11 @@
|
||||
- function scoreColor(s) { return s == null ? '#9ca3af' : s >= 90 ? '#22c55e' : s >= 50 ? '#f59e0b' : '#ef4444'; }
|
||||
- function scoreBg(s) { return s == null ? 'bg-gray-100 text-gray-400' : s >= 90 ? 'bg-green-100 text-green-700' : s >= 50 ? 'bg-yellow-100 text-yellow-700' : 'bg-red-100 text-red-700'; }
|
||||
- const scores = [['Overall', job.score_overall], ['Performance', job.score_performance], ['Best Practice', job.score_bestpractice], ['Privacy', job.score_privacy]]
|
||||
- const scores = [['Overall', job.score_overall], ['Perf', job.score_performance], ['Best Practice', job.score_bestpractice], ['Privacy', job.score_privacy]]
|
||||
|
||||
div(class='bg-white border border-gray-200 rounded-xl p-5')
|
||||
h2(class='text-base font-semibold mb-4') Coach Scores
|
||||
div(class='flex flex-wrap gap-4 justify-start')
|
||||
div(class='bg-white border border-gray-200 rounded-xl p-4 h-full')
|
||||
h2(class='text-sm font-semibold mb-3 text-gray-600 uppercase tracking-wide') Coach Scores
|
||||
div(class='flex flex-wrap gap-3 justify-start')
|
||||
each item in scores
|
||||
div(class='flex flex-col items-center gap-1')
|
||||
div(class=`score-circle w-16 h-16 text-xl ${scoreBg(item[1])}`)
|
||||
div(class=`score-circle w-12 h-12 text-base ${scoreBg(item[1])}`)
|
||||
= item[1] != null ? Math.round(item[1]) : '?'
|
||||
span(class='text-xs text-gray-500 text-center')= item[0]
|
||||
|
||||
27
views/partials/ssl.pug
Normal file
27
views/partials/ssl.pug
Normal file
@@ -0,0 +1,27 @@
|
||||
- const ssl = job.ssl_json ? JSON.parse(job.ssl_json) : null
|
||||
- function sslColor(g) { return !g ? 'bg-gray-100 text-gray-400' : ['A+','A','A-'].includes(g) ? 'bg-green-100 text-green-700' : g === 'B' ? 'bg-yellow-100 text-yellow-700' : ['C','D'].includes(g) ? 'bg-orange-100 text-orange-700' : 'bg-red-100 text-red-700'; }
|
||||
|
||||
div(class='bg-white border border-gray-200 rounded-xl p-5')
|
||||
h2(class='text-base font-semibold mb-3') SSL / TLS
|
||||
if ssl
|
||||
div(class='flex items-start gap-5 flex-wrap')
|
||||
div(class='flex flex-col items-center gap-1')
|
||||
div(class=`score-circle w-14 h-14 text-lg ${sslColor(ssl.grade)}`)= ssl.grade || 'T'
|
||||
span(class='text-xs text-gray-500') Grade
|
||||
div(class='grid grid-cols-2 sm:grid-cols-3 gap-2 flex-1 text-sm')
|
||||
div(class='bg-gray-50 rounded-lg px-3 py-2')
|
||||
div(class='text-xs text-gray-400') IP Address
|
||||
div(class='font-medium')= ssl.ipAddress || '—'
|
||||
div(class='bg-gray-50 rounded-lg px-3 py-2')
|
||||
div(class='text-xs text-gray-400') Cert Expires
|
||||
div(class='font-medium')= ssl.certExpiry || '—'
|
||||
div(class='bg-gray-50 rounded-lg px-3 py-2')
|
||||
div(class='text-xs text-gray-400') Days Until Expiry
|
||||
- const d = ssl.daysUntilExpiry
|
||||
div(class=`font-medium ${d != null && d < 30 ? 'text-red-600' : d != null && d < 60 ? 'text-yellow-600' : 'text-green-700'}`)
|
||||
= d != null ? d + ' days' : '—'
|
||||
div(class='bg-gray-50 rounded-lg px-3 py-2 col-span-2 sm:col-span-3')
|
||||
div(class='text-xs text-gray-400 mb-1') Protocols
|
||||
div(class='font-medium')= ssl.protocols?.join(' · ') || '—'
|
||||
else
|
||||
p(class='text-sm text-gray-400 italic') Not available — HTTPS only or check timed out
|
||||
Reference in New Issue
Block a user