• 01 - Critter Models 02
  • 02 - VantaJS Shaders
  • 03 - App Bundle 02
  • 04 - Cathedrals
  • 05 - App Bundle 01
  • 06 - Retroscopic
  • 07 - Games
  • 08 - Voronoi
  • 09 - Terrain
  • 10 - Critter Models 01
  • 11 - Chromatophores
  • 12 - Conformal Maps
  • 13 - Echo Logix
  • 14 - Music
  • 15 - Colour Theory
MIKE'S NOTEBOOK
Notes
Number:
Project:
01Critter Models 02
└ Locust└ Butterfly└ Cockroach└ Spider└ Scorpion└ Dragon└ Brazilian Treehopper└ Tardigrade└ Diatoms└ Bumblebee└ Wasp└ Hammerhead Worm└ Giraffe Weevil└ Trilobite Beetle
02VantaJS Shaders
└ Clouds└ Waves└ Cells└ Rings└ Halo└ Globe└ Net└ Dots└ Birds└ Trunk└ Topology└ About VantaJS
03App Bundle 02
└ Gutindex└ Insect Builder└ OpenLayers
04Cathedrals
└ Lattice└ Lost└ Lucent└ Lumen└ Glass└ Disco└ Hyperion└ Latent
05App Bundle 01
└ Robohash└ PrismJS└ Horoscope└ Binary Jazz└ Morse Code└ URL Extractor└ Entity Escaper└ Phonetic Alphabet└ Toki Pona└ Semaphore└ Cut Up Machine└ Text Mixer└ Currency Freaks└ Host IO└ CrUX API└ Logo Dev└ Web Speech API└ GEO DB
06Retroscopic
└ Galaxy└ Ocean└ Slime└ 24-Cell└ Unicorn└ Polyp└ Oneiric 1└ Oneiric 2└ Node└ Torus Knot└ GLSL Grid└ Liquid Crystal└ Silky└ Butterfly└ Ocean 2└ Icosahedron└ Luma└ Swarm└ Broken Solid└ Hypersphere└ Constraints└ Psycho Killer
01 / 03

CRUX API - INTERACTIVE APP SOURCE CODE

<!DOCTYPE html>
<html lang="en" data-theme="dark">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>CrUX Dashboard | Core Web Vitals</title>
    <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
    <style>
        :root {
            --bg-primary: #0a0a0a;
            --bg-secondary: #141414;
            --bg-tertiary: #1e1e1e;
            --bg-hover: #2a2a2a;
            --text-primary: #ffffff;
            --text-secondary: #cccccc;
            --text-muted: #888888;
            --border-color: #333333;
            --accent-primary: #4285f4;
            --accent-success: #34a853;
            --accent-warning: #fbbc05;
            --accent-danger: #ea4335;
            --accent-info: #8b5cf6;
            --shadow-sm: 0 1px 3px rgba(0, 0, 0, 0.4);
            --shadow-md: 0 4px 12px rgba(0, 0, 0, 0.5);
            --radius-sm: 4px;
            --radius-md: 6px;
            --radius-lg: 8px;
            --transition: all 0.15s ease;
            --spacing-xs: 0.25rem;
            --spacing-sm: 0.5rem;
            --spacing-md: 0.75rem;
            --spacing-lg: 1rem;
            --spacing-xl: 1.5rem;
        }

        * {
            margin: 0;
            padding: 0;
            box-sizing: border-box;
        }

        html, body {
            height: 100%;
            overflow-x: hidden;
        }

        body {
            font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, Roboto, 'Helvetica Neue', Arial, sans-serif;
            background-color: var(--bg-primary);
            color: var(--text-primary);
            line-height: 1.4;
            font-size: 14px;
            display: flex;
            flex-direction: column;
            min-height: 100vh;
            max-width: 100vw;
        }

        /* Compact Header */
        .header {
            background-color: var(--bg-secondary);
            border-bottom: 1px solid var(--border-color);
            padding: 0.75rem 0;
            position: sticky;
            top: 0;
            z-index: 1000;
            box-shadow: var(--shadow-sm);
        }

        .container {
            max-width: 1400px;
            margin: 0 auto;
            padding: 0 var(--spacing-lg);
            width: 100%;
        }

        .header-content {
            display: flex;
            justify-content: space-between;
            align-items: center;
        }

        .logo {
            display: flex;
            align-items: center;
            gap: 0.75rem;
        }

        .logo-icon {
            color: var(--accent-primary);
            font-size: 1.25rem;
        }

        .logo-text {
            font-size: 1.1rem;
            font-weight: 600;
            letter-spacing: -0.01em;
        }

        .logo-subtext {
            font-size: 0.75rem;
            color: var(--text-muted);
            margin-top: 0.125rem;
            font-weight: 400;
        }

        /* Main Layout - More Compact */
        .main-layout {
            display: grid;
            grid-template-columns: 1fr;
            gap: var(--spacing-xl);
            padding: var(--spacing-xl) 0;
            flex: 1;
            overflow-y: auto;
        }

        /* Input Section - Compact */
        .input-section {
            background-color: var(--bg-secondary);
            border-radius: var(--radius-lg);
            padding: var(--spacing-xl);
            border: 1px solid var(--border-color);
            box-shadow: var(--shadow-sm);
        }

        .section-title {
            font-size: 1rem;
            font-weight: 600;
            margin-bottom: var(--spacing-lg);
            color: var(--text-primary);
            display: flex;
            align-items: center;
            gap: 0.5rem;
        }

        .section-title i {
            color: var(--accent-primary);
            font-size: 0.9rem;
        }

        .input-group {
            display: flex;
            gap: 0.75rem;
            margin-bottom: var(--spacing-lg);
        }

        .url-input {
            flex: 1;
            padding: 0.625rem 0.875rem;
            background-color: var(--bg-tertiary);
            border: 1px solid var(--border-color);
            border-radius: var(--radius-md);
            color: var(--text-primary);
            font-size: 0.875rem;
            transition: var(--transition);
            font-family: inherit;
        }

        .url-input:focus {
            outline: none;
            border-color: var(--accent-primary);
            box-shadow: 0 0 0 2px rgba(66, 133, 244, 0.15);
        }

        .form-factor-select {
            padding: 0.625rem 0.875rem;
            background-color: var(--bg-tertiary);
            border: 1px solid var(--border-color);
            border-radius: var(--radius-md);
            color: var(--text-primary);
            font-size: 0.875rem;
            min-width: 120px;
            cursor: pointer;
            font-family: inherit;
        }

        .btn {
            padding: 0.625rem 1.25rem;
            background-color: var(--accent-primary);
            color: white;
            border: none;
            border-radius: var(--radius-md);
            font-size: 0.875rem;
            font-weight: 500;
            cursor: pointer;
            display: flex;
            align-items: center;
            gap: 0.5rem;
            transition: var(--transition);
            white-space: nowrap;
            font-family: inherit;
        }

        .btn:hover {
            background-color: #3b78e7;
        }

        .btn:active {
            transform: translateY(0);
        }

        .btn:disabled {
            opacity: 0.5;
            cursor: not-allowed;
        }

        .btn i {
            font-size: 0.875rem;
        }

        .input-hint {
            color: var(--text-muted);
            font-size: 0.75rem;
            margin-top: 0.5rem;
        }

        /* Info Section - More Compact */
        .info-section {
            background-color: var(--bg-secondary);
            border-radius: var(--radius-lg);
            padding: var(--spacing-xl);
            border: 1px solid var(--border-color);
            box-shadow: var(--shadow-sm);
            margin-bottom: var(--spacing-xl);
        }

        .info-content {
            margin-top: var(--spacing-md);
        }

        .info-item {
            margin-bottom: var(--spacing-xl);
        }

        .info-item:last-child {
            margin-bottom: 0;
        }

        .info-item h3 {
            font-size: 0.9rem;
            font-weight: 600;
            margin-bottom: 0.75rem;
            color: var(--text-primary);
            text-transform: uppercase;
            letter-spacing: 0.05em;
        }

        .info-item p {
            color: var(--text-secondary);
            font-size: 0.85rem;
            line-height: 1.5;
        }

        .metric-explanation {
            display: grid;
            grid-template-columns: 1fr;
            gap: 0.75rem;
            margin-top: 0.75rem;
        }

        .metric-card {
            background-color: var(--bg-tertiary);
            border-radius: var(--radius-md);
            padding: 0.875rem;
            border-left: 3px solid var(--accent-primary);
        }

        .metric-card.lcp {
            border-left-color: var(--accent-success);
        }

        .metric-card.inp {
            border-left-color: var(--accent-info);
        }

        .metric-card.cls {
            border-left-color: var(--accent-warning);
        }

        .metric-card h4 {
            font-size: 0.85rem;
            font-weight: 600;
            margin-bottom: 0.375rem;
            display: flex;
            align-items: center;
            gap: 0.5rem;
        }

        .metric-card p {
            font-size: 0.8rem;
            color: var(--text-secondary);
            line-height: 1.4;
        }

        /* Results Section - Compact */
        .results-section {
            background-color: var(--bg-secondary);
            border-radius: var(--radius-lg);
            padding: var(--spacing-xl);
            margin-top:20px;
            border: 1px solid var(--border-color);
            box-shadow: var(--shadow-sm);
            display: none;
        }

        .results-header {
            display: flex;
            justify-content: space-between;
            align-items: center;
            margin-bottom: var(--spacing-lg);
            flex-wrap: wrap;
            gap: 0.75rem;
        }

        .results-title {
            font-size: 1rem;
            font-weight: 600;
            color: var(--text-primary);
        }

        .query-info {
            display: flex;
            flex-wrap: wrap;
            gap: 1rem;
            margin-bottom: var(--spacing-lg);
            padding: 0.875rem;
            background-color: var(--bg-tertiary);
            border-radius: var(--radius-md);
            font-size: 0.85rem;
        }

        .query-info-item {
            display: flex;
            flex-direction: column;
            min-width: 120px;
        }

        .query-info-label {
            font-size: 0.75rem;
            color: var(--text-muted);
            margin-bottom: 0.25rem;
            text-transform: uppercase;
            letter-spacing: 0.05em;
        }

        .query-info-value {
            font-size: 0.9rem;
            font-weight: 500;
            color: var(--text-primary);
            word-break: break-all;
        }

        .loading-container {
            display: flex;
            flex-direction: column;
            align-items: center;
            justify-content: center;
            padding: var(--spacing-xl);
            text-align: center;
        }

        .spinner {
            width: 2rem;
            height: 2rem;
            border: 2px solid var(--border-color);
            border-top: 2px solid var(--accent-primary);
            border-radius: 50%;
            animation: spin 0.8s linear infinite;
            margin-bottom: 1rem;
        }

        @keyframes spin {
            0% { transform: rotate(0deg); }
            100% { transform: rotate(360deg); }
        }

        .error-container {
            padding: 1rem;
            background-color: rgba(234, 67, 53, 0.08);
            border: 1px solid var(--accent-danger);
            border-radius: var(--radius-md);
            margin-bottom: var(--spacing-lg);
        }

        .error-title {
            color: var(--accent-danger);
            font-weight: 600;
            margin-bottom: 0.375rem;
            display: flex;
            align-items: center;
            gap: 0.5rem;
            font-size: 0.9rem;
        }

        .error-message {
            color: var(--text-secondary);
            font-size: 0.85rem;
            line-height: 1.4;
        }

        /* Metrics Grid - Compact */
        .metrics-grid {
            display: grid;
            grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
            gap: 1rem;
            margin-top: var(--spacing-lg);
        }

        .metric-container {
            background-color: var(--bg-tertiary);
            border-radius: var(--radius-md);
            padding: 1rem;
            border: 1px solid var(--border-color);
            transition: var(--transition);
        }

        .metric-container:hover {
            border-color: #444;
        }

        .metric-header {
            display: flex;
            justify-content: space-between;
            align-items: flex-start;
            margin-bottom: 0.75rem;
        }

        .metric-name {
            font-size: 0.9rem;
            font-weight: 600;
            display: flex;
            align-items: center;
            gap: 0.5rem;
        }

        .metric-name i {
            font-size: 0.85rem;
        }

        .metric-value {
            font-size: 1.5rem;
            font-weight: 700;
            margin: 0.5rem 0;
            letter-spacing: -0.02em;
        }

        .metric-status {
            padding: 0.25rem 0.625rem;
            border-radius: var(--radius-sm);
            font-size: 0.75rem;
            font-weight: 600;
            text-transform: uppercase;
            letter-spacing: 0.05em;
        }

        .status-good {
            background-color: rgba(52, 168, 83, 0.15);
            color: var(--accent-success);
        }

        .status-needs-improvement {
            background-color: rgba(251, 188, 5, 0.15);
            color: var(--accent-warning);
        }

        .status-poor {
            background-color: rgba(234, 67, 53, 0.15);
            color: var(--accent-danger);
        }

        .histogram {
            margin-top: 0.75rem;
        }

        .histogram-bars {
            display: flex;
            height: 1.5rem;
            border-radius: var(--radius-sm);
            overflow: hidden;
            margin-bottom: 0.375rem;
        }

        .histogram-bar {
            height: 100%;
            display: flex;
            align-items: center;
            justify-content: center;
            color: var(--bg-primary);
            font-weight: 600;
            font-size: 0.7rem;
            transition: var(--transition);
        }

        .histogram-bar:hover {
            filter: brightness(1.1);
        }

        .histogram-labels {
            display: flex;
            justify-content: space-between;
            font-size: 0.7rem;
            color: var(--text-muted);
        }

        .percentile {
            margin-top: 0.75rem;
            padding-top: 0.75rem;
            border-top: 1px solid var(--border-color);
            font-size: 0.8rem;
            color: var(--text-secondary);
        }

        .percentile strong {
            color: var(--text-primary);
            font-weight: 600;
        }

        .collection-period {
            margin-top: var(--spacing-lg);
            padding: 0.875rem;
            background-color: var(--bg-tertiary);
            border-radius: var(--radius-md);
            font-size: 0.8rem;
            color: var(--text-secondary);
            text-align: center;
        }

        .collection-period strong {
            color: var(--text-primary);
        }

        /* Debug Log - Compact */
        .debug-log {
            background-color: var(--bg-tertiary);
            border: 1px solid var(--border-color);
            border-radius: var(--radius-md);
            padding: 0.875rem;
            margin-top: var(--spacing-lg);
            font-family: 'SF Mono', 'Monaco', 'Inconsolata', 'Fira Mono', monospace;
            font-size: 0.75rem;
            color: var(--text-secondary);
            max-height: 200px;
            overflow-y: auto;
            display: none;
        }

        .debug-header {
            display: flex;
            justify-content: space-between;
            align-items: center;
            margin-bottom: 0.5rem;
        }

        .debug-toggle {
            background: none;
            border: none;
            color: var(--accent-primary);
            cursor: pointer;
            font-size: 0.75rem;
            padding: 0.25rem 0.5rem;
            border-radius: var(--radius-sm);
            transition: var(--transition);
        }

        .debug-toggle:hover {
            background-color: rgba(66, 133, 244, 0.1);
        }

        /* Footer - Compact */
        .footer {
            background-color: var(--bg-secondary);
            border-top: 1px solid var(--border-color);
            padding: 0.875rem 0;
            margin-top: auto;
        }

        .footer-content {
            display: flex;
            justify-content: space-between;
            align-items: center;
            flex-wrap: wrap;
            gap: 0.75rem;
        }

        .footer-text {
            color: var(--text-muted);
            font-size: 0.75rem;
        }

        .footer-links {
            display: flex;
            gap: 1rem;
        }

        .footer-link {
            color: var(--text-secondary);
            text-decoration: none;
            font-size: 0.75rem;
            transition: var(--transition);
        }

        .footer-link:hover {
            color: var(--accent-primary);
        }

        /* Responsive Design */
        @media (max-width: 768px) {
            .container {
                padding: 0 0.875rem;
            }

            .header-content {
                flex-direction: column;
                align-items: flex-start;
                gap: 0.75rem;
            }

            .input-group {
                flex-direction: column;
            }

            .form-factor-select {
                width: 100%;
            }

            .url-input, .btn {
                width: 100%;
            }

            .metrics-grid {
                grid-template-columns: 1fr;
            }

            .query-info {
                flex-direction: column;
                gap: 0.75rem;
            }

            .query-info-item {
                min-width: 100%;
            }

            .footer-content {
                flex-direction: column;
                align-items: flex-start;
            }

            .footer-links {
                margin-top: 0.25rem;
            }

            .main-layout {
                padding: 1rem 0;
                gap: 1rem;
            }

            .input-section, .info-section, .results-section {
                padding: 1rem;
            }
        }

        @media (min-width: 769px) and (max-width: 1024px) {
            .main-layout {
                grid-template-columns: 1fr;
            }
        }

        @media (min-width: 1025px) {
            .main-layout {
                grid-template-columns: 1fr 320px;
            }
        }

        /* Utility Classes */
        .hidden {
            display: none !important;
        }

        .text-center {
            text-align: center;
        }

        .mt-1 { margin-top: var(--spacing-xs); }
        .mt-2 { margin-top: var(--spacing-sm); }
        .mt-3 { margin-top: var(--spacing-md); }
        .mt-4 { margin-top: var(--spacing-lg); }
        .mt-5 { margin-top: var(--spacing-xl); }

        .mb-1 { margin-bottom: var(--spacing-xs); }
        .mb-2 { margin-bottom: var(--spacing-sm); }
        .mb-3 { margin-bottom: var(--spacing-md); }
        .mb-4 { margin-bottom: var(--spacing-lg); }
        .mb-5 { margin-bottom: var(--spacing-xl); }

        /* Scrollbar Styling */
        ::-webkit-scrollbar {
            width: 8px;
            height: 8px;
        }

        ::-webkit-scrollbar-track {
            background: var(--bg-secondary);
            border-radius: 4px;
        }

        ::-webkit-scrollbar-thumb {
            background: var(--border-color);
            border-radius: 4px;
        }

        ::-webkit-scrollbar-thumb:hover {
            background: #444;
        }
    </style>
</head>
<body>
    <main class="container">
        <div class="main-layout">
            <div>
                <section class="input-section">
                    <h2 class="section-title"><i class="fas fa-search"></i> Analyse URL Performance</h2>
                    <div class="input-group">
                        <input type="url" class="url-input" id="urlInput"
                               placeholder="https://example.com"
                               value="https://web.dev">
                        <select class="form-factor-select" id="formFactorSelect">
                            <option value="">All Devices</option>
                            <option value="PHONE">Phone</option>
                            <option value="DESKTOP">Desktop</option>
                            <option value="TABLET">Tablet</option>
                        </select>
                        <button class="btn" id="analyzeBtn">
                            <i class="fas fa-chart-bar"></i>
                            Analyse
                        </button>
                    </div>
                    <p class="input-hint">Enter any public URL to view its Core Web Vitals performance data.</p>
                </section>

                <section class="results-section" id="resultsSection">
                    <div class="results-header">
                        <h2 class="results-title">Performance Results</h2>
                        <div class="query-info">
                            <div class="query-info-item">
                                <span class="query-info-label">URL</span>
                                <span class="query-info-value" id="resultUrl"></span>
                            </div>
                            <div class="query-info-item">
                                <span class="query-info-label">Device</span>
                                <span class="query-info-value" id="resultFormFactor"></span>
                            </div>
                            <div class="query-info-item">
                                <span class="query-info-label">Status</span>
                                <span class="query-info-value" id="resultStatus"></span>
                            </div>
                        </div>
                    </div>

                    <div id="loadingIndicator" class="loading-container hidden">
                        <div class="spinner"></div>
                        <p>Querying CrUX API for performance data...</p>
                    </div>

                    <div id="errorContainer" class="error-container hidden">
                        <h3 class="error-title"><i class="fas fa-exclamation-triangle"></i> Error</h3>
                        <p class="error-message" id="errorMessage"></p>
                    </div>

                    <div id="metricsContainer"></div>

                    <div id="collectionPeriod" class="collection-period hidden">
                        Data collected from <strong id="collectionStart"></strong> to <strong id="collectionEnd"></strong>
                    </div>

                    <div class="debug-log" id="debugLog">
                        <div class="debug-header">
                            <span>Debug Log</span>
                            <button class="debug-toggle" id="debugToggle">Show</button>
                        </div>
                        <pre id="debugContent"></pre>
                    </div>
                </section>
            </div>

            <aside class="info-section">
                <h2 class="section-title"><i class="fas fa-info-circle"></i> How This Works</h2>
                <div class="info-content">
                    <div class="info-item">
                        <h3>About This Tool</h3>
                        <p>This dashboard queries Google's Chrome UX Report (CrUX) API to retrieve real-user performance data for any URL. The data represents actual user experiences from Chrome browsers over a 28-day collection period.</p>
                    </div>

                    <div class="info-item">
                        <h3>Core Web Vitals</h3>
                        <p>Core Web Vitals are a set of metrics that measure real-world user experience for loading performance, interactivity, and visual stability.</p>

                        <div class="metric-explanation">
                            <div class="metric-card lcp">
                                <h4><i class="fas fa-tachometer-alt"></i> LCP (Largest Contentful Paint)</h4>
                                <p>Measures loading performance. To provide a good user experience, LCP should occur within 2.5 seconds.</p>
                            </div>

                            <div class="metric-card inp">
                                <h4><i class="fas fa-mouse-pointer"></i> INP (Interaction to Next Paint)</h4>
                                <p>Measures responsiveness. For a good experience, INP should be less than 200 milliseconds.</p>
                            </div>

                            <div class="metric-card cls">
                                <h4><i class="fas fa-ruler-combined"></i> CLS (Cumulative Layout Shift)</h4>
                                <p>Measures visual stability. A good CLS score is less than 0.1.</p>
                            </div>
                        </div>
                    </div>

                    <div class="info-item">
                        <h3>Data Source</h3>
                        <p>The data comes from Google's CrUX dataset, which aggregates anonymous performance data from real Chrome users visiting millions of websites. This is the same data used in Google Search Console and PageSpeed Insights.</p>
                    </div>
                </div>
            </aside>
        </div>
    </main>

    <footer class="footer">
        <div class="container">
            <div class="footer-content">
                <div class="footer-text">
                    CrUX Dashboard • Uses Google Chrome UX Report API • Data updates monthly
                </div>
                <div class="footer-links">
                    <a href="https://developer.chrome.com/docs/crux/api" class="footer-link" target="_blank">CrUX API Docs</a>
                    <a href="https://developers.google.com/search/docs/appearance/core-web-vitals" class="footer-link" target="_blank">Core Web Vitals</a>
                </div>
            </div>
        </div>
    </footer>

    <script>
        // DOM Elements
        const urlInput = document.getElementById('urlInput');
        const formFactorSelect = document.getElementById('formFactorSelect');
        const analyzeBtn = document.getElementById('analyzeBtn');
        const resultsSection = document.getElementById('resultsSection');
        const resultUrl = document.getElementById('resultUrl');
        const resultFormFactor = document.getElementById('resultFormFactor');
        const resultStatus = document.getElementById('resultStatus');
        const loadingIndicator = document.getElementById('loadingIndicator');
        const errorContainer = document.getElementById('errorContainer');
        const errorMessage = document.getElementById('errorMessage');
        const metricsContainer = document.getElementById('metricsContainer');
        const collectionPeriod = document.getElementById('collectionPeriod');
        const collectionStart = document.getElementById('collectionStart');
        const collectionEnd = document.getElementById('collectionEnd');
        const debugLog = document.getElementById('debugLog');
        const debugContent = document.getElementById('debugContent');
        const debugToggle = document.getElementById('debugToggle');

        // Debug log management
        let debugLogEntries = [];

        function addDebugLog(entry) {
            const timestamp = new Date().toISOString().split('T')[1].split('.')[0];
            debugLogEntries.push(`[${timestamp}] ${entry}`);
            debugContent.textContent = debugLogEntries.join('\n');
            debugLog.scrollTop = debugLog.scrollHeight;
        }

        function clearDebugLog() {
            debugLogEntries = [];
            debugContent.textContent = '';
        }

        debugToggle.addEventListener('click', () => {
            if (debugLog.style.display === 'none' || debugLog.style.display === '') {
                debugLog.style.display = 'block';
                debugToggle.textContent = 'Hide';
            } else {
                debugLog.style.display = 'none';
                debugToggle.textContent = 'Show';
            }
        });

        // Core Web Vitals thresholds
        const THRESHOLDS = {
            lcp: { good: 2500, poor: 4000 },
            inp: { good: 200, poor: 500 },
            cls: { good: 0.1, poor: 0.25 }
        };

        // Metric metadata
        const METRICS = {
            largest_contentful_paint: {
                name: "Largest Contentful Paint (LCP)",
                unit: "ms",
                description: "Loading performance",
                icon: "fa-tachometer-alt",
                goodThreshold: THRESHOLDS.lcp.good,
                poorThreshold: THRESHOLDS.lcp.poor
            },
            interaction_to_next_paint: {
                name: "Interaction to Next Paint (INP)",
                unit: "ms",
                description: "Responsiveness",
                icon: "fa-mouse-pointer",
                goodThreshold: THRESHOLDS.inp.good,
                poorThreshold: THRESHOLDS.inp.poor
            },
            cumulative_layout_shift: {
                name: "Cumulative Layout Shift (CLS)",
                unit: "",
                description: "Visual stability",
                icon: "fa-ruler-combined",
                goodThreshold: THRESHOLDS.cls.good,
                poorThreshold: THRESHOLDS.cls.poor
            }
        };

        // Format date from API response
        function formatDate(dateObj) {
            if (!dateObj) return 'N/A';
            const { year, month, day } = dateObj;
            return `${year}-${String(month).padStart(2, '0')}-${String(day).padStart(2, '0')}`;
        }

        // Determine status for a metric value
        function getStatus(metricKey, value) {
            const metric = METRICS[metricKey];
            if (!metric) return 'unknown';

            if (value <= metric.goodThreshold) return 'good';
            if (value <= metric.poorThreshold) return 'needs-improvement';
            return 'poor';
        }

        // Get status display text
        function getStatusText(status) {
            switch(status) {
                case 'good': return 'Good';
                case 'needs-improvement': return 'Bad';
                case 'poor': return 'Poor';
                default: return 'Unknown';
            }
        }

        // Get status class for styling
        function getStatusClass(status) {
            switch(status) {
                case 'good': return 'status-good';
                case 'needs-improvement': return 'status-needs-improvement';
                case 'poor': return 'status-poor';
                default: return '';
            }
        }

        // Create histogram visualization
        function createHistogram(histogramData, metricKey) {
            const container = document.createElement('div');
            container.className = 'histogram';

            const barsContainer = document.createElement('div');
            barsContainer.className = 'histogram-bars';

            const labelsContainer = document.createElement('div');
            labelsContainer.className = 'histogram-labels';

            histogramData.forEach((bin, index) => {
                const density = bin.density || 0;
                const widthPercent = density * 100;

                const bar = document.createElement('div');
                bar.className = 'histogram-bar';
                bar.style.width = `${widthPercent}%`;
                bar.style.backgroundColor = getBarColor(metricKey, bin);
                bar.textContent = widthPercent > 10 ? `${(density * 100).toFixed(1)}%` : '';
                barsContainer.appendChild(bar);

                if (index === 0 || index === histogramData.length - 1) {
                    const label = document.createElement('span');
                    if (index === 0) {
                        label.textContent = bin.end ? `< ${formatValue(bin.end, metricKey)}` : 'All';
                    } else {
                        label.textContent = bin.start ? `> ${formatValue(bin.start, metricKey)}` : 'All';
                    }
                    labelsContainer.appendChild(label);
                }
            });

            container.appendChild(barsContainer);
            container.appendChild(labelsContainer);

            return container;
        }

        // Get color for histogram bar based on metric and bin
        function getBarColor(metricKey, bin) {
            const metric = METRICS[metricKey];
            if (!metric) return '#6b7280';

            if (metricKey === 'cumulative_layout_shift') {
                const value = bin.end || bin.start || 0;
                if (value <= metric.goodThreshold) return '#34a853'; // Green
                if (value <= metric.poorThreshold) return '#fbbc05'; // Orange
                return '#ea4335'; // Red
            }

            if (bin.end && bin.end <= metric.goodThreshold) return '#34a853';
            if (bin.start && bin.start >= metric.poorThreshold) return '#ea4335';
            return '#fbbc05';
        }

        // Format value with appropriate unit
        function formatValue(value, metricKey) {
            const metric = METRICS[metricKey];
            if (!metric) return value;

            if (metricKey === 'cumulative_layout_shift') {
                return parseFloat(value).toFixed(3);
            }

            return `${value}${metric.unit}`;
        }

        // Create metric card
        function createMetricCard(metricKey, metricData) {
            const metricInfo = METRICS[metricKey];
            if (!metricInfo || !metricData) return null;

            const p75 = metricData.percentiles?.p75;
            const status = getStatus(metricKey, p75);

            const card = document.createElement('div');
            card.className = 'metric-container';

            card.innerHTML = `
                <div class="metric-header">
                    <div class="metric-name">
                        <i class="fas ${metricInfo.icon}"></i>
                        ${metricInfo.name}
                    </div>
                    <div class="metric-status ${getStatusClass(status)}">
                        ${getStatusText(status)}
                    </div>
                </div>

                <div class="metric-value">
                    ${formatValue(p75, metricKey)}
                </div>

                <div class="histogram" id="histogram-${metricKey}"></div>

                <div class="percentile">
                    75% of users experience ${metricInfo.name.split('(')[0].trim()} ≤ <strong>${formatValue(p75, metricKey)}</strong>
                </div>
            `;

            if (metricData.histogram) {
                const histogramContainer = card.querySelector(`#histogram-${metricKey}`);
                histogramContainer.appendChild(createHistogram(metricData.histogram, metricKey));
            }

            return card;
        }

        // Display results
        function displayResults(data, url, formFactor) {
            resultsSection.style.display = 'block';
            resultUrl.textContent = url;
            resultFormFactor.textContent = formFactor || 'All Devices';

            if (data.error) {
                errorContainer.style.display = 'block';
                errorMessage.textContent = data.error.message || 'No data available for this URL';
                metricsContainer.innerHTML = '';
                collectionPeriod.style.display = 'none';
                resultStatus.textContent = 'No Data';
                return;
            }

            errorContainer.style.display = 'none';

            if (data.record?.collectionPeriod) {
                collectionPeriod.style.display = 'block';
                collectionStart.textContent = formatDate(data.record.collectionPeriod.firstDate);
                collectionEnd.textContent = formatDate(data.record.collectionPeriod.lastDate);
            }

            metricsContainer.innerHTML = '';

            const grid = document.createElement('div');
            grid.className = 'metrics-grid';

            const metrics = data.record?.metrics || {};
            let hasData = false;

            Object.keys(METRICS).forEach(metricKey => {
                if (metrics[metricKey]) {
                    const card = createMetricCard(metricKey, metrics[metricKey]);
                    if (card) {
                        grid.appendChild(card);
                        hasData = true;
                    }
                }
            });

            metricsContainer.appendChild(grid);

            if (hasData) {
                resultStatus.textContent = 'Data Available';
                resultStatus.style.color = 'var(--accent-success)';
            } else {
                resultStatus.textContent = 'Partial Data';
                resultStatus.style.color = 'var(--accent-warning)';
                metricsContainer.innerHTML = '<p style="color: var(--text-muted); text-align: center; padding: 2rem;">Limited data available for this URL</p>';
            }

            // Scroll to results
            resultsSection.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
        }

        // Query CrUX API via proxy with detailed debugging
        async function queryCruxAPI(url, formFactor) {
            addDebugLog(`Starting API query for URL: ${url}`);
            addDebugLog(`Form factor: ${formFactor || 'All'}`);

            const requestBody = {
                url: url,
                metrics: [
                    'largest_contentful_paint',
                    'interaction_to_next_paint',
                    'cumulative_layout_shift'
                ]
            };

            if (formFactor) {
                requestBody.formFactor = formFactor;
            }

            addDebugLog(`Request body: ${JSON.stringify(requestBody)}`);

            try {
                addDebugLog('Calling window.proxyFetch...');

                // Check if proxyFetch is available
                if (typeof window.proxyFetch !== 'function') {
                    const errorMsg = 'window.proxyFetch is not a function. Make sure your proxy is properly loaded.';
                    addDebugLog(`ERROR: ${errorMsg}`);
                    throw new Error(errorMsg);
                }

                const response = await window.proxyFetch('https://chromeuxreport.googleapis.com/v1/records:queryRecord', {
                    method: 'POST',
                    headers: {
                        'Content-Type': 'application/json',
                        'Authorization': 'Bearer {{CRUX_API}}'
                    },
                    body: JSON.stringify(requestBody)
                });

                addDebugLog(`Response status: ${response.status} ${response.statusText}`);

                if (!response.ok) {
                    const errorText = await response.text();
                    addDebugLog(`Error response: ${errorText}`);
                    throw new Error(`HTTP ${response.status}: ${response.statusText}`);
                }

                const data = await response.json();
                addDebugLog('Successfully received API response');
                addDebugLog(`Response keys: ${Object.keys(data).join(', ')}`);

                if (data.record) {
                    addDebugLog(`Record contains metrics: ${Object.keys(data.record.metrics || {}).join(', ')}`);
                }

                return data;
            } catch (error) {
                addDebugLog(`ERROR in API call: ${error.name} - ${error.message}`);

                // Provide more helpful error messages
                let userErrorMessage = error.message;

                if (error.message.includes('Proxy Error')) {
                    userErrorMessage = 'Proxy server error. Check that your proxy server is running and properly configured for CrUX API requests.';
                } else if (error.message.includes('Failed to fetch') || error.message.includes('NetworkError')) {
                    userErrorMessage = 'Network error. Check your internet connection and that the proxy server is accessible.';
                } else if (error.message.includes('CORS')) {
                    userErrorMessage = 'CORS error. The proxy server may not be sending proper CORS headers.';
                }

                addDebugLog(`User error message: ${userErrorMessage}`);

                return {
                    error: {
                        message: userErrorMessage
                    }
                };
            }
        }

        // Handle analyze button click
        analyzeBtn.addEventListener('click', async () => {
            const url = urlInput.value.trim();
            const formFactor = formFactorSelect.value;

            if (!url) {
                alert('Please enter a URL');
                urlInput.focus();
                return;
            }

            try {
                new URL(url);
            } catch (e) {
                alert('Please enter a valid URL (include https://)');
                urlInput.focus();
                return;
            }

            clearDebugLog();
            debugLog.style.display = 'none';
            debugToggle.textContent = 'Show';

            analyzeBtn.disabled = true;
            analyzeBtn.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Analyzing...';
            loadingIndicator.style.display = 'flex';
            errorContainer.style.display = 'none';
            resultsSection.style.display = 'block';

            try {
                addDebugLog('=== Starting Analysis ===');
                const data = await queryCruxAPI(url, formFactor);
                displayResults(data, url, formFactor);
                addDebugLog('=== Analysis Complete ===');
            } catch (error) {
                console.error('Analysis process failed:', error);
                errorContainer.style.display = 'block';
                errorMessage.textContent = `Analysis error: ${error.message}`;
                addDebugLog(`Analysis process error: ${error.message}`);
            } finally {
                analyzeBtn.disabled = false;
                analyzeBtn.innerHTML = '<i class="fas fa-chart-bar"></i> Analyze';
                loadingIndicator.style.display = 'none';
            }
        });

        // Handle Enter key in URL input
        urlInput.addEventListener('keypress', (e) => {
            if (e.key === 'Enter') {
                analyzeBtn.click();
            }
        });

        // Initialize
        document.addEventListener('DOMContentLoaded', () => {
            urlInput.focus();
            addDebugLog('CrUX Dashboard initialized and ready.');

            // Check if proxyFetch is available
            if (typeof window.proxyFetch !== 'function') {
                addDebugLog('WARNING: window.proxyFetch is not available yet.');
                console.warn('window.proxyFetch is not defined. The app requires a proxy function to make API calls.');
            } else {
                addDebugLog('window.proxyFetch is available.');
            }

            // Auto-hide debug log on load
            debugLog.style.display = 'none';
        });
    </script>
</body>
</html>
MikeHiggins.me.uk LinkedIn GitHub Bluesky Contra Upwork
© 2026 Mike Higgins. All rights reserved.