mirror of
https://github.com/Dvorinka/beszel.git
synced 2026-06-03 21:02:56 +00:00
fe5c7eaa95
Build Docker images / Hub (push) Failing after 1m35s
Implement comprehensive domain data collection including provider detection (DNS, hosting, email, CA), HTTP headers, TLS certificate chains, and SEO metadata. Added PageSpeed Insights integration for monitors to track Core Web Vitals. - **hub**: - Add provider detection logic for DNS, email, and hosting. - Expand `Domain` entity to include SEO, headers, certificates, and enhanced registration details. - Implement automated collection of TLD, WHOIS raw data, and host country codes. - Update scheduler to track changes in providers and security settings (privacy/transfer lock). - Add PageSpeed check endpoint to monitor API. - **site**: - Update domain table and detail views to display new intelligence (providers, headers, SEO). - Implement PageSpeed metrics visualization with Core Web Vitals status indicators. - Add display options for provider information in the domain list. - **db**: - Add migration for new domain collection fields.
261 lines
7.0 KiB
Go
261 lines
7.0 KiB
Go
package pagespeed
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"net/url"
|
|
"time"
|
|
)
|
|
|
|
// PageSpeedResponse represents the PageSpeed API response
|
|
type PageSpeedResponse struct {
|
|
LighthouseResult struct {
|
|
Categories struct {
|
|
Performance struct {
|
|
Score float64 `json:"score"`
|
|
} `json:"performance"`
|
|
Accessibility struct {
|
|
Score float64 `json:"score"`
|
|
} `json:"accessibility"`
|
|
BestPractices struct {
|
|
Score float64 `json:"best-practices"`
|
|
} `json:"best-practices"`
|
|
SEO struct {
|
|
Score float64 `json:"score"`
|
|
} `json:"seo"`
|
|
PWA struct {
|
|
Score float64 `json:"score"`
|
|
} `json:"pwa"`
|
|
} `json:"categories"`
|
|
Audits struct {
|
|
Fcp struct {
|
|
DisplayValue string `json:"displayValue"`
|
|
NumericValue float64 `json:"numericValue"`
|
|
} `json:"first-contentful-paint"`
|
|
Lcp struct {
|
|
DisplayValue string `json:"displayValue"`
|
|
NumericValue float64 `json:"numericValue"`
|
|
} `json:"largest-contentful-paint"`
|
|
Ttfb struct {
|
|
DisplayValue string `json:"displayValue"`
|
|
NumericValue float64 `json:"numericValue"`
|
|
} `json:"server-response-time"`
|
|
Cls struct {
|
|
DisplayValue string `json:"displayValue"`
|
|
NumericValue float64 `json:"numericValue"`
|
|
} `json:"cumulative-layout-shift"`
|
|
Tbt struct {
|
|
DisplayValue string `json:"displayValue"`
|
|
NumericValue float64 `json:"numericValue"`
|
|
} `json:"total-blocking-time"`
|
|
SpeedIndex struct {
|
|
DisplayValue string `json:"displayValue"`
|
|
NumericValue float64 `json:"numericValue"`
|
|
} `json:"speed-index"`
|
|
Interactive struct {
|
|
DisplayValue string `json:"displayValue"`
|
|
NumericValue float64 `json:"numericValue"`
|
|
} `json:"interactive"`
|
|
} `json:"audits"`
|
|
} `json:"lighthouseResult"`
|
|
AnalysisUTCTimestamp string `json:"analysisUTCTimestamp"`
|
|
Id string `json:"id"`
|
|
}
|
|
|
|
// Metrics represents the extracted performance metrics
|
|
type Metrics struct {
|
|
Performance float64 `json:"performance"`
|
|
Accessibility float64 `json:"accessibility"`
|
|
BestPractices float64 `json:"bestPractices"`
|
|
SEO float64 `json:"seo"`
|
|
PWA float64 `json:"pwa"`
|
|
FCP float64 `json:"fcp"` // First Contentful Paint (ms)
|
|
LCP float64 `json:"lcp"` // Largest Contentful Paint (ms)
|
|
TTFB float64 `json:"ttfb"` // Time to First Byte (ms)
|
|
CLS float64 `json:"cls"` // Cumulative Layout Shift
|
|
TBT float64 `json:"tbt"` // Total Blocking Time (ms)
|
|
SpeedIndex float64 `json:"speedIndex"` // Speed Index (ms)
|
|
TTI float64 `json:"tti"` // Time to Interactive (ms)
|
|
CheckedAt time.Time `json:"checkedAt"`
|
|
URL string `json:"url"`
|
|
Strategy string `json:"strategy"` // mobile or desktop
|
|
}
|
|
|
|
// Checker handles PageSpeed checks
|
|
type Checker struct {
|
|
apiKey string
|
|
client *http.Client
|
|
}
|
|
|
|
// NewChecker creates a new PageSpeed checker
|
|
func NewChecker(apiKey string) *Checker {
|
|
return &Checker{
|
|
apiKey: apiKey,
|
|
client: &http.Client{
|
|
Timeout: 60 * time.Second,
|
|
},
|
|
}
|
|
}
|
|
|
|
// CheckURL runs a PageSpeed check on a URL
|
|
func (c *Checker) CheckURL(pageURL string, strategy string) (*Metrics, error) {
|
|
if strategy == "" {
|
|
strategy = "mobile"
|
|
}
|
|
|
|
// Build PageSpeed API URL
|
|
apiURL := fmt.Sprintf(
|
|
"https://www.googleapis.com/pagespeedonline/v5/runPagespeed?url=%s&strategy=%s&category=PERFORMANCE&category=ACCESSIBILITY&category=BEST_PRACTICES&category=SEO&category=PWA",
|
|
url.QueryEscape(pageURL),
|
|
strategy,
|
|
)
|
|
|
|
if c.apiKey != "" {
|
|
apiURL += "&key=" + c.apiKey
|
|
}
|
|
|
|
resp, err := c.client.Get(apiURL)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("pagespeed API request failed: %w", err)
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
if resp.StatusCode != http.StatusOK {
|
|
body, _ := io.ReadAll(resp.Body)
|
|
return nil, fmt.Errorf("pagespeed API returned status %d: %s", resp.StatusCode, string(body))
|
|
}
|
|
|
|
var result PageSpeedResponse
|
|
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
|
|
return nil, fmt.Errorf("failed to decode pagespeed response: %w", err)
|
|
}
|
|
|
|
metrics := &Metrics{
|
|
URL: pageURL,
|
|
Strategy: strategy,
|
|
CheckedAt: time.Now(),
|
|
Performance: result.LighthouseResult.Categories.Performance.Score * 100,
|
|
Accessibility: result.LighthouseResult.Categories.Accessibility.Score * 100,
|
|
BestPractices: result.LighthouseResult.Categories.BestPractices.Score * 100,
|
|
SEO: result.LighthouseResult.Categories.SEO.Score * 100,
|
|
PWA: result.LighthouseResult.Categories.PWA.Score * 100,
|
|
FCP: result.LighthouseResult.Audits.Fcp.NumericValue,
|
|
LCP: result.LighthouseResult.Audits.Lcp.NumericValue,
|
|
TTFB: result.LighthouseResult.Audits.Ttfb.NumericValue,
|
|
CLS: result.LighthouseResult.Audits.Cls.NumericValue,
|
|
TBT: result.LighthouseResult.Audits.Tbt.NumericValue,
|
|
SpeedIndex: result.LighthouseResult.Audits.SpeedIndex.NumericValue,
|
|
TTI: result.LighthouseResult.Audits.Interactive.NumericValue,
|
|
}
|
|
|
|
return metrics, nil
|
|
}
|
|
|
|
// Grade returns a letter grade based on score
|
|
func Grade(score float64) string {
|
|
switch {
|
|
case score >= 90:
|
|
return "A"
|
|
case score >= 80:
|
|
return "B"
|
|
case score >= 70:
|
|
return "C"
|
|
case score >= 60:
|
|
return "D"
|
|
default:
|
|
return "F"
|
|
}
|
|
}
|
|
|
|
// GradeColor returns a color for the grade
|
|
func GradeColor(score float64) string {
|
|
switch {
|
|
case score >= 90:
|
|
return "brightgreen"
|
|
case score >= 80:
|
|
return "green"
|
|
case score >= 70:
|
|
return "yellow"
|
|
case score >= 60:
|
|
return "orange"
|
|
default:
|
|
return "red"
|
|
}
|
|
}
|
|
|
|
// GetCoreWebVitalsStatus returns the status for Core Web Vitals
|
|
func GetCoreWebVitalsStatus(metrics *Metrics) map[string]string {
|
|
return map[string]string{
|
|
"lcp": getLCPStatus(metrics.LCP),
|
|
"fid": getFIDStatus(metrics.TBT), // Using TBT as proxy for FID
|
|
"cls": getCLSStatus(metrics.CLS),
|
|
"fcp": getFCPStatus(metrics.FCP),
|
|
"ttfb": getTTFBStatus(metrics.TTFB),
|
|
"tti": getTTIStatus(metrics.TTI),
|
|
}
|
|
}
|
|
|
|
func getLCPStatus(value float64) string {
|
|
if value <= 2500 {
|
|
return "good"
|
|
} else if value <= 4000 {
|
|
return "needs-improvement"
|
|
}
|
|
return "poor"
|
|
}
|
|
|
|
func getFIDStatus(value float64) string {
|
|
if value <= 100 {
|
|
return "good"
|
|
} else if value <= 300 {
|
|
return "needs-improvement"
|
|
}
|
|
return "poor"
|
|
}
|
|
|
|
func getCLSStatus(value float64) string {
|
|
if value <= 0.1 {
|
|
return "good"
|
|
} else if value <= 0.25 {
|
|
return "needs-improvement"
|
|
}
|
|
return "poor"
|
|
}
|
|
|
|
func getFCPStatus(value float64) string {
|
|
if value <= 1800 {
|
|
return "good"
|
|
} else if value <= 3000 {
|
|
return "needs-improvement"
|
|
}
|
|
return "poor"
|
|
}
|
|
|
|
func getTTFBStatus(value float64) string {
|
|
if value <= 800 {
|
|
return "good"
|
|
} else if value <= 1800 {
|
|
return "needs-improvement"
|
|
}
|
|
return "poor"
|
|
}
|
|
|
|
func getTTIStatus(value float64) string {
|
|
if value <= 3800 {
|
|
return "good"
|
|
} else if value <= 7300 {
|
|
return "needs-improvement"
|
|
}
|
|
return "poor"
|
|
}
|
|
|
|
// FormatDuration formats milliseconds to readable string
|
|
func FormatDuration(ms float64) string {
|
|
if ms < 1000 {
|
|
return fmt.Sprintf("%.0fms", ms)
|
|
}
|
|
return fmt.Sprintf("%.1fs", ms/1000)
|
|
}
|