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.

add stats dashboard at /dashboard

- stats.zig: atomic counters for searches, errors, uptime
- dashboard.zig: HTML template with live uptime ticker
- shows session stats, db counts, top 20 tags

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

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

zzstoatzz 41b27dd5 5a36551a

+271 -3
+176
backend/src/dashboard.zig
··· 1 + const std = @import("std"); 2 + 3 + /// Generate dashboard HTML with live stats 4 + pub fn render( 5 + alloc: std.mem.Allocator, 6 + uptime_secs: i64, 7 + searches: u64, 8 + errors: u64, 9 + documents: i64, 10 + publications: i64, 11 + tags_json: []const u8, 12 + ) ![]const u8 { 13 + var buf: std.ArrayList(u8) = .{}; 14 + const w = buf.writer(alloc); 15 + 16 + try w.writeAll( 17 + \\<!DOCTYPE html> 18 + \\<html lang="en"> 19 + \\<head> 20 + \\ <meta charset="UTF-8"> 21 + \\ <meta name="viewport" content="width=device-width, initial-scale=1.0"> 22 + \\ <title>leaflet search stats</title> 23 + \\ <style> 24 + \\ * { box-sizing: border-box; margin: 0; padding: 0; } 25 + \\ body { 26 + \\ font-family: monospace; 27 + \\ background: #0a0a0a; 28 + \\ color: #ccc; 29 + \\ min-height: 100vh; 30 + \\ padding: 2rem; 31 + \\ line-height: 1.6; 32 + \\ } 33 + \\ .container { max-width: 800px; margin: 0 auto; } 34 + \\ h1 { font-size: 14px; color: #888; margin-bottom: 2rem; font-weight: normal; } 35 + \\ h1 a { color: #1B7340; text-decoration: none; } 36 + \\ h1 a:hover { color: #2a9d5c; } 37 + \\ h2 { font-size: 12px; color: #666; margin: 2rem 0 1rem; font-weight: normal; text-transform: uppercase; letter-spacing: 1px; } 38 + \\ .stats-grid { 39 + \\ display: grid; 40 + \\ grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); 41 + \\ gap: 1rem; 42 + \\ margin-bottom: 2rem; 43 + \\ } 44 + \\ .stat { 45 + \\ background: #111; 46 + \\ border: 1px solid #222; 47 + \\ padding: 1rem; 48 + \\ border-radius: 4px; 49 + \\ } 50 + \\ .stat-value { 51 + \\ font-size: 24px; 52 + \\ color: #fff; 53 + \\ margin-bottom: 0.25rem; 54 + \\ } 55 + \\ .stat-value.uptime { color: #2a9d5c; } 56 + \\ .stat-label { font-size: 11px; color: #666; } 57 + \\ .tags-grid { 58 + \\ display: flex; 59 + \\ flex-wrap: wrap; 60 + \\ gap: 0.5rem; 61 + \\ } 62 + \\ .tag { 63 + \\ background: #151515; 64 + \\ border: 1px solid #252525; 65 + \\ padding: 0.5rem 0.75rem; 66 + \\ border-radius: 4px; 67 + \\ font-size: 12px; 68 + \\ color: #888; 69 + \\ text-decoration: none; 70 + \\ } 71 + \\ .tag:hover { background: #1a1a1a; border-color: #333; color: #aaa; } 72 + \\ .tag .count { color: #555; margin-left: 0.5rem; } 73 + \\ .footer { margin-top: 3rem; font-size: 11px; color: #444; } 74 + \\ .footer a { color: #555; } 75 + \\ </style> 76 + \\</head> 77 + \\<body> 78 + \\ <div class="container"> 79 + \\ <h1><a href="https://leaflet-search.pages.dev">leaflet search</a> / stats</h1> 80 + \\ 81 + \\ <div class="stats-grid"> 82 + \\ <div class="stat"> 83 + \\ <div class="stat-value uptime" id="uptime">--</div> 84 + \\ <div class="stat-label">uptime</div> 85 + \\ </div> 86 + \\ <div class="stat"> 87 + \\ <div class="stat-value"> 88 + ); 89 + 90 + try w.print("{d}", .{searches}); 91 + try w.writeAll( 92 + \\</div> 93 + \\ <div class="stat-label">searches (this session)</div> 94 + \\ </div> 95 + \\ <div class="stat"> 96 + \\ <div class="stat-value"> 97 + ); 98 + 99 + try w.print("{d}", .{documents}); 100 + try w.writeAll( 101 + \\</div> 102 + \\ <div class="stat-label">documents indexed</div> 103 + \\ </div> 104 + \\ <div class="stat"> 105 + \\ <div class="stat-value"> 106 + ); 107 + 108 + try w.print("{d}", .{publications}); 109 + try w.writeAll( 110 + \\</div> 111 + \\ <div class="stat-label">publications indexed</div> 112 + \\ </div> 113 + \\ <div class="stat"> 114 + \\ <div class="stat-value"> 115 + ); 116 + 117 + try w.print("{d}", .{errors}); 118 + try w.writeAll( 119 + \\</div> 120 + \\ <div class="stat-label">errors</div> 121 + \\ </div> 122 + \\ </div> 123 + \\ 124 + \\ <h2>top tags</h2> 125 + \\ <div class="tags-grid" id="tags"></div> 126 + \\ 127 + \\ <div class="footer"> 128 + \\ <a href="https://tangled.sh/@zzstoatzz.io/leaflet-search" target="_blank">source</a> 129 + \\ </div> 130 + \\ </div> 131 + \\ 132 + \\ <script> 133 + \\ const startUptime = 134 + ); 135 + 136 + try w.print("{d}", .{uptime_secs}); 137 + try w.writeAll( 138 + \\; 139 + \\ const startTime = Date.now(); 140 + \\ 141 + \\ function formatUptime(secs) { 142 + \\ const d = Math.floor(secs / 86400); 143 + \\ const h = Math.floor((secs % 86400) / 3600); 144 + \\ const m = Math.floor((secs % 3600) / 60); 145 + \\ const s = secs % 60; 146 + \\ if (d > 0) return `${d}d ${h}h ${m}m`; 147 + \\ if (h > 0) return `${h}h ${m}m ${s}s`; 148 + \\ if (m > 0) return `${m}m ${s}s`; 149 + \\ return `${s}s`; 150 + \\ } 151 + \\ 152 + \\ function updateUptime() { 153 + \\ const elapsed = Math.floor((Date.now() - startTime) / 1000); 154 + \\ document.getElementById('uptime').textContent = formatUptime(startUptime + elapsed); 155 + \\ } 156 + \\ 157 + \\ updateUptime(); 158 + \\ setInterval(updateUptime, 1000); 159 + \\ 160 + \\ const tags = 161 + ); 162 + 163 + try w.writeAll(tags_json); 164 + try w.writeAll( 165 + \\; 166 + \\ const tagsHtml = tags.slice(0, 20).map(t => 167 + \\ `<a class="tag" href="https://leaflet-search.pages.dev/?tag=${encodeURIComponent(t.tag)}">${t.tag}<span class="count">${t.count}</span></a>` 168 + \\ ).join(''); 169 + \\ document.getElementById('tags').innerHTML = tagsHtml; 170 + \\ </script> 171 + \\</body> 172 + \\</html> 173 + ); 174 + 175 + return buf.toOwnedSlice(alloc); 176 + }
+45 -3
backend/src/http.zig
··· 3 3 const http = std.http; 4 4 const mem = std.mem; 5 5 const db = @import("db/mod.zig"); 6 + const stats = @import("stats.zig"); 7 + const dashboard = @import("dashboard.zig"); 6 8 7 9 const HTTP_BUF_SIZE = 8192; 8 10 const QUERY_PARAM_BUF_SIZE = 64; ··· 51 53 try handleStats(request); 52 54 } else if (mem.eql(u8, target, "/health")) { 53 55 try sendJson(request, "{\"status\":\"ok\"}"); 56 + } else if (mem.eql(u8, target, "/dashboard")) { 57 + try handleDashboard(request); 54 58 } else { 55 59 try sendNotFound(request); 56 60 } ··· 71 75 } 72 76 73 77 // perform FTS search - arena handles cleanup 74 - const results = try db.search(alloc, query, tag_filter); 78 + const results = db.search(alloc, query, tag_filter) catch |err| { 79 + stats.get().recordError(); 80 + return err; 81 + }; 82 + stats.get().recordSearch(); 75 83 try sendJson(request, results); 76 84 } 77 85 ··· 106 114 defer arena.deinit(); 107 115 const alloc = arena.allocator(); 108 116 109 - const stats = db.getStats(); 117 + const db_stats = db.getStats(); 110 118 111 119 var response: std.ArrayList(u8) = .{}; 112 120 defer response.deinit(alloc); 113 121 114 - try response.print(alloc, "{{\"documents\":{d},\"publications\":{d}}}", .{ stats.documents, stats.publications }); 122 + try response.print(alloc, "{{\"documents\":{d},\"publications\":{d}}}", .{ db_stats.documents, db_stats.publications }); 115 123 116 124 try sendJson(request, response.items); 117 125 } ··· 148 156 }, 149 157 }); 150 158 } 159 + 160 + fn handleDashboard(request: *http.Server.Request) !void { 161 + var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator); 162 + defer arena.deinit(); 163 + const alloc = arena.allocator(); 164 + 165 + const s = stats.get(); 166 + const db_stats = db.getStats(); 167 + const tags_json = db.getTags(alloc) catch "[]"; 168 + 169 + const html = dashboard.render( 170 + alloc, 171 + s.getUptime(), 172 + s.getSearches(), 173 + s.getErrors(), 174 + db_stats.documents, 175 + db_stats.publications, 176 + tags_json, 177 + ) catch { 178 + try sendNotFound(request); 179 + return; 180 + }; 181 + 182 + try sendHtml(request, html); 183 + } 184 + 185 + fn sendHtml(request: *http.Server.Request, body: []const u8) !void { 186 + try request.respond(body, .{ 187 + .status = .ok, 188 + .extra_headers = &.{ 189 + .{ .name = "content-type", .value = "text/html; charset=utf-8" }, 190 + }, 191 + }); 192 + }
+50
backend/src/stats.zig
··· 1 + const std = @import("std"); 2 + const Atomic = std.atomic.Value; 3 + 4 + /// Service stats - in-memory counters, Turso-backed totals 5 + pub const Stats = struct { 6 + started_at: i64, 7 + searches: Atomic(u64), 8 + errors: Atomic(u64), 9 + 10 + pub fn init() Stats { 11 + return .{ 12 + .started_at = std.time.timestamp(), 13 + .searches = Atomic(u64).init(0), 14 + .errors = Atomic(u64).init(0), 15 + }; 16 + } 17 + 18 + pub fn recordSearch(self: *Stats) void { 19 + _ = self.searches.fetchAdd(1, .monotonic); 20 + } 21 + 22 + pub fn recordError(self: *Stats) void { 23 + _ = self.errors.fetchAdd(1, .monotonic); 24 + } 25 + 26 + pub fn getUptime(self: *const Stats) i64 { 27 + return std.time.timestamp() - self.started_at; 28 + } 29 + 30 + pub fn getSearches(self: *const Stats) u64 { 31 + return self.searches.load(.monotonic); 32 + } 33 + 34 + pub fn getErrors(self: *const Stats) u64 { 35 + return self.errors.load(.monotonic); 36 + } 37 + }; 38 + 39 + var global_stats: Stats = undefined; 40 + var initialized: bool = false; 41 + 42 + pub fn init() void { 43 + global_stats = Stats.init(); 44 + initialized = true; 45 + } 46 + 47 + pub fn get() *Stats { 48 + if (!initialized) init(); 49 + return &global_stats; 50 + }