search for standard sites pub-search.waow.tech
search zig blog atproto
11
fork

Configure Feed

Select the types of activity you want to include in your feed.

fix: separate line chart for latency history

- restored original timing table (no inline charts)
- added new "latency history (24h)" section with area chart
- shows search and similar endpoints as colored lines
- grafana-style visualization

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

zzstoatzz bf476017 19b24a29

+116 -30
+25 -15
site/dashboard.css
··· 109 109 justify-content: space-between; 110 110 font-size: 12px; 111 111 padding: 0.25rem 0; 112 + border-bottom: 1px solid #1a1a1a; 112 113 } 114 + .timing-row:last-child { border-bottom: none; } 113 115 .timing-name { color: #888; } 114 116 .timing-value { color: #ccc; } 115 117 .timing-value .dim { color: #555; } 116 118 117 - .timing-chart { 119 + .latency-chart { 120 + position: relative; 121 + height: 80px; 122 + margin-top: 1rem; 123 + } 124 + .latency-chart canvas { 125 + width: 100%; 126 + height: 100%; 127 + } 128 + .latency-legend { 118 129 display: flex; 119 - align-items: flex-end; 120 - gap: 1px; 121 - height: 24px; 122 - margin-bottom: 0.75rem; 123 - padding-bottom: 0.5rem; 124 - border-bottom: 1px solid #1a1a1a; 130 + gap: 1rem; 131 + font-size: 10px; 132 + margin-top: 0.5rem; 125 133 } 126 - .timing-chart:last-child { border-bottom: none; margin-bottom: 0; } 127 - 128 - .timing-bar { 129 - flex: 1; 130 - background: #1B7340; 131 - min-width: 2px; 132 - opacity: 0.6; 134 + .latency-legend span { 135 + display: flex; 136 + align-items: center; 137 + gap: 4px; 138 + color: #666; 133 139 } 134 - .timing-bar:hover { opacity: 1; } 140 + .latency-legend .dot { 141 + width: 8px; 142 + height: 8px; 143 + border-radius: 50%; 144 + } 135 145 136 146 .tags { 137 147 display: flex;
+7
site/dashboard.html
··· 44 44 </section> 45 45 46 46 <section> 47 + <div class="section-title">latency history (24h)</div> 48 + <div class="chart-box"> 49 + <div id="latency-history"></div> 50 + </div> 51 + </section> 52 + 53 + <section> 47 54 <div class="section-title">documents indexed (last 30 days)</div> 48 55 <div class="chart-box"> 49 56 <div class="timeline" id="timeline"></div>
+84 -15
site/dashboard.js
··· 104 104 formatMs(t.p95_ms) + ' <span class="dim">p95</span></span>'; 105 105 } 106 106 el.appendChild(row); 107 + }); 107 108 108 - // add 24h mini chart if history available 109 - if (t.history && t.history.length > 0) { 110 - const chart = document.createElement('div'); 111 - chart.className = 'timing-chart'; 112 - const maxCount = Math.max(...t.history.map(h => h.count), 1); 113 - t.history.forEach(h => { 114 - const bar = document.createElement('div'); 115 - bar.className = 'timing-bar'; 116 - const height = h.count > 0 ? Math.max((h.count / maxCount) * 100, 5) : 0; 117 - bar.style.height = height + '%'; 118 - const hourStr = new Date(h.hour * 1000).toLocaleTimeString([], {hour: '2-digit', minute: '2-digit'}); 119 - bar.title = hourStr + ': ' + h.count + ' req, ' + formatMs(h.avg_ms) + ' avg'; 120 - chart.appendChild(bar); 121 - }); 122 - el.appendChild(chart); 109 + // render line chart for history 110 + renderLatencyChart(timing); 111 + } 112 + 113 + function renderLatencyChart(timing) { 114 + const container = document.getElementById('latency-history'); 115 + if (!container) return; 116 + 117 + const endpoints = ['search', 'similar']; 118 + const colors = { search: '#8b5cf6', similar: '#06b6d4' }; 119 + 120 + // check if any endpoint has history data 121 + const hasData = endpoints.some(name => timing[name]?.history?.some(h => h.count > 0)); 122 + if (!hasData) { 123 + container.innerHTML = '<div style="color:#444;font-size:11px;text-align:center;padding:2rem">no data yet</div>'; 124 + return; 125 + } 126 + 127 + const canvas = document.createElement('canvas'); 128 + const chartDiv = document.createElement('div'); 129 + chartDiv.className = 'latency-chart'; 130 + chartDiv.appendChild(canvas); 131 + container.appendChild(chartDiv); 132 + 133 + const ctx = canvas.getContext('2d'); 134 + const dpr = window.devicePixelRatio || 1; 135 + const rect = chartDiv.getBoundingClientRect(); 136 + canvas.width = rect.width * dpr; 137 + canvas.height = rect.height * dpr; 138 + ctx.scale(dpr, dpr); 139 + 140 + const w = rect.width; 141 + const h = rect.height; 142 + const padding = { top: 10, right: 10, bottom: 20, left: 10 }; 143 + const chartW = w - padding.left - padding.right; 144 + const chartH = h - padding.top - padding.bottom; 145 + 146 + // find max value across all endpoints 147 + let maxVal = 0; 148 + endpoints.forEach(name => { 149 + const history = timing[name]?.history || []; 150 + history.forEach(p => { if (p.avg_ms > maxVal) maxVal = p.avg_ms; }); 151 + }); 152 + if (maxVal === 0) maxVal = 100; 153 + 154 + // draw each endpoint as an area chart 155 + endpoints.forEach(name => { 156 + const history = timing[name]?.history || []; 157 + if (history.length === 0) return; 158 + 159 + const color = colors[name]; 160 + const points = history.map((p, i) => ({ 161 + x: padding.left + (i / (history.length - 1)) * chartW, 162 + y: padding.top + chartH - (p.avg_ms / maxVal) * chartH 163 + })); 164 + 165 + // draw filled area 166 + ctx.beginPath(); 167 + ctx.moveTo(points[0].x, padding.top + chartH); 168 + points.forEach(p => ctx.lineTo(p.x, p.y)); 169 + ctx.lineTo(points[points.length - 1].x, padding.top + chartH); 170 + ctx.closePath(); 171 + ctx.fillStyle = color + '20'; 172 + ctx.fill(); 173 + 174 + // draw line 175 + ctx.beginPath(); 176 + ctx.moveTo(points[0].x, points[0].y); 177 + for (let i = 1; i < points.length; i++) { 178 + ctx.lineTo(points[i].x, points[i].y); 123 179 } 180 + ctx.strokeStyle = color; 181 + ctx.lineWidth = 1.5; 182 + ctx.stroke(); 124 183 }); 184 + 185 + // legend 186 + const legend = document.createElement('div'); 187 + legend.className = 'latency-legend'; 188 + endpoints.forEach(name => { 189 + const span = document.createElement('span'); 190 + span.innerHTML = '<span class="dot" style="background:' + colors[name] + '"></span>' + name; 191 + legend.appendChild(span); 192 + }); 193 + container.appendChild(legend); 125 194 } 126 195 127 196 function escapeHtml(str) {