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 terminal dashboard script with rich + plotext

- index stats, usage metrics, latency percentiles
- timeline chart for docs indexed per day
- p50 latency comparison chart
- clear labeling (e.g., "similar cache hit" not just "cache")

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

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

zzstoatzz ed2fedd5 d69d5a7e

+164
+164
scripts/dashboard.py
··· 1 + #!/usr/bin/env python3 2 + # /// script 3 + # requires-python = ">=3.11" 4 + # dependencies = ["httpx", "rich", "plotext", "typer"] 5 + # /// 6 + """pub-search terminal dashboard 7 + 8 + usage: 9 + uv run scripts/dashboard.py # default view 10 + uv run scripts/dashboard.py --days 14 # longer timeline 11 + """ 12 + 13 + import httpx 14 + import plotext as plt 15 + import typer 16 + from rich.console import Console 17 + from rich.panel import Panel 18 + from rich.table import Table 19 + 20 + API_BASE = "https://leaflet-search-backend.fly.dev" 21 + console = Console() 22 + app = typer.Typer(add_completion=False) 23 + 24 + 25 + def fetch_stats() -> dict: 26 + """fetch /stats endpoint""" 27 + resp = httpx.get(f"{API_BASE}/stats", timeout=30) 28 + resp.raise_for_status() 29 + return resp.json() 30 + 31 + 32 + def fetch_dashboard() -> dict: 33 + """fetch /api/dashboard endpoint""" 34 + resp = httpx.get(f"{API_BASE}/api/dashboard", timeout=30) 35 + resp.raise_for_status() 36 + return resp.json() 37 + 38 + 39 + def display_overview(stats: dict) -> None: 40 + """show document/publication counts""" 41 + table = Table(show_header=False, box=None, padding=(0, 2), expand=True) 42 + table.add_column(style="dim") 43 + table.add_column(style="bold green", justify="right") 44 + 45 + table.add_row("documents", f"{stats['documents']:,}") 46 + table.add_row("publications", f"{stats['publications']:,}") 47 + table.add_row("embeddings", f"{stats['embeddings']:,}") 48 + 49 + embed_pct = (stats['embeddings'] / stats['documents'] * 100) if stats['documents'] > 0 else 0 50 + table.add_row("embedded", f"{embed_pct:.0f}%") 51 + 52 + console.print(Panel(table, title="[bold]index[/]", border_style="blue", expand=False)) 53 + 54 + 55 + def display_usage(stats: dict) -> None: 56 + """show usage and similarity cache stats""" 57 + hits = stats.get('cache_hits', 0) 58 + misses = stats.get('cache_misses', 0) 59 + total = hits + misses 60 + hit_rate = (hits / total * 100) if total > 0 else 0 61 + 62 + table = Table(show_header=False, box=None, padding=(0, 2), expand=True) 63 + table.add_column(style="dim") 64 + table.add_column(style="bold cyan", justify="right") 65 + 66 + table.add_row("searches", f"{stats.get('searches', 0):,}") 67 + table.add_row("errors", f"{stats.get('errors', 0):,}") 68 + table.add_row("similar cache hit", f"{hit_rate:.0f}% ({hits}/{total})") 69 + 70 + console.print(Panel(table, title="[bold]usage[/]", border_style="cyan", expand=False)) 71 + 72 + 73 + def display_latency(stats: dict) -> None: 74 + """show latency percentiles""" 75 + timing = stats.get('timing', {}) 76 + if not timing: 77 + return 78 + 79 + table = Table(box=None, padding=(0, 1), expand=True) 80 + table.add_column("endpoint", style="dim") 81 + table.add_column("p50", justify="right", style="green") 82 + table.add_column("p95", justify="right", style="yellow") 83 + table.add_column("p99", justify="right", style="red") 84 + table.add_column("count", justify="right", style="dim") 85 + 86 + for endpoint in ['search', 'similar', 'tags', 'popular']: 87 + if endpoint in timing: 88 + t = timing[endpoint] 89 + table.add_row( 90 + endpoint, 91 + f"{t['p50_ms']:.0f}ms", 92 + f"{t['p95_ms']:.0f}ms", 93 + f"{t['p99_ms']:.0f}ms", 94 + f"{t['count']:,}", 95 + ) 96 + 97 + console.print(Panel(table, title="[bold]latency[/]", border_style="magenta", expand=False)) 98 + 99 + 100 + def display_timeline(dashboard: dict, days: int) -> None: 101 + """show indexing activity chart""" 102 + timeline = dashboard.get('timeline', [])[:days] 103 + if not timeline: 104 + return 105 + 106 + timeline = list(reversed(timeline)) # oldest first 107 + dates = [d['date'][-5:] for d in timeline] # MM-DD 108 + counts = [d['count'] for d in timeline] 109 + 110 + plt.clear_figure() 111 + plt.theme("dark") 112 + plt.title("documents indexed per day") 113 + plt.bar(dates, counts, color="cyan") 114 + plt.plotsize(70, 12) 115 + plt.show() 116 + print() 117 + 118 + 119 + def display_latency_chart(stats: dict) -> None: 120 + """bar chart of p50 latencies by endpoint""" 121 + timing = stats.get('timing', {}) 122 + if not timing: 123 + return 124 + 125 + endpoints = [] 126 + p50s = [] 127 + for endpoint in ['search', 'similar', 'tags', 'popular']: 128 + if endpoint in timing: 129 + endpoints.append(endpoint) 130 + p50s.append(timing[endpoint]['p50_ms']) 131 + 132 + plt.clear_figure() 133 + plt.theme("dark") 134 + plt.title("p50 latency by endpoint (ms)") 135 + plt.bar(endpoints, p50s, color="cyan") 136 + plt.plotsize(50, 10) 137 + plt.show() 138 + print() 139 + 140 + 141 + @app.command() 142 + def main( 143 + days: int = typer.Option(7, "-d", "--days", help="days of timeline to show"), 144 + ) -> None: 145 + """pub-search terminal dashboard""" 146 + console.print("\n[bold cyan]pub-search[/] dashboard\n") 147 + 148 + try: 149 + stats = fetch_stats() 150 + dashboard = fetch_dashboard() 151 + except httpx.HTTPError as e: 152 + console.print(f"[red]error fetching data:[/] {e}") 153 + raise typer.Exit(1) 154 + 155 + display_overview(stats) 156 + display_usage(stats) 157 + display_latency(stats) 158 + print() 159 + display_timeline(dashboard, days) 160 + display_latency_chart(stats) 161 + 162 + 163 + if __name__ == "__main__": 164 + app()