lightweight com.atproto.sync.listReposByCollection
45
fork

Configure Feed

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

admin overview page

phil d2854cb3 319ac2de

+202 -3
+3 -2
src/http.rs
··· 66 66 67 67 async fn limit(&self, host: &str, path: &str) { 68 68 let (id, limiting) = self.get_or_create_limiter(host); 69 + let req_id = (id, path.to_string()); 69 70 let mut throttled = false; 70 71 while let Err(not_until) = limiting.limiter.check() { 71 72 if !throttled { 72 73 throttled = true; 73 74 metrics::gauge!("lightrail_http_host_throttling").increment(1); 74 - limiting.limiting.insert((id, path.to_string())); 75 + limiting.limiting.insert(req_id.clone()); 75 76 } 76 77 let min_wait = not_until.wait_time_from(QuantaClock::default().now()); 77 78 let jitter = Duration::from_millis((fastrand::f32() * THROTTLE_JITTER_MS) as u64); ··· 82 83 } 83 84 if throttled { 84 85 metrics::gauge!("lightrail_http_host_throttling").decrement(1); 85 - limiting.limiting.remove(&(id, path.to_string())); 86 + limiting.limiting.remove(&req_id); 86 87 } 87 88 } 88 89 }
+1 -1
src/server/admin.rs
··· 136 136 /// 137 137 /// Accepts any username. Uses constant-time byte comparison to avoid leaking 138 138 /// whether a guessed password shares a prefix with the real one. 139 - fn check_basic_auth(headers: &HeaderMap, expected_password: &str) -> bool { 139 + pub(super) fn check_basic_auth(headers: &HeaderMap, expected_password: &str) -> bool { 140 140 let Some(auth) = headers.get(header::AUTHORIZATION) else { 141 141 return false; 142 142 };
+195
src/server/admin_page.rs
··· 1 + use axum::{Extension, http::HeaderMap, response::Html}; 2 + 3 + use super::AdminConfig; 4 + use super::admin::{AdminStatusError, check_basic_auth}; 5 + 6 + /// `GET /admin` — HTML dashboard that polls `/admin/status` for live stats. 7 + /// 8 + /// Protected by the same HTTP Basic Auth as the JSON endpoint. 9 + pub async fn admin_page( 10 + Extension(config): Extension<AdminConfig>, 11 + headers: HeaderMap, 12 + ) -> Result<Html<&'static str>, AdminStatusError> { 13 + if !check_basic_auth(&headers, &config.admin_password) { 14 + return Err(AdminStatusError::Unauthorized); 15 + } 16 + Ok(Html(PAGE)) 17 + } 18 + 19 + const PAGE: &str = r##"<!DOCTYPE html> 20 + <html lang="en"> 21 + <head> 22 + <meta charset="utf-8"> 23 + <meta name="viewport" content="width=device-width, initial-scale=1"> 24 + <title>lightrail admin</title> 25 + <style> 26 + *{box-sizing:border-box;margin:0;padding:0} 27 + body{font-family:system-ui,-apple-system,sans-serif;background:#f8f8f8;color:#222;padding:1.5rem;max-width:1100px;margin:0 auto} 28 + header{margin-bottom:1.5rem;display:flex;justify-content:space-between;align-items:baseline;flex-wrap:wrap;gap:0.5rem} 29 + h1{font-size:1.4rem;font-weight:600;letter-spacing:-0.02em} 30 + .sub{color:#888;font-size:0.82rem} 31 + .grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(320px,1fr));gap:1rem} 32 + .card{background:#fff;border-radius:6px;padding:1rem 1.25rem;border:1px solid #e8e8e8} 33 + .card h2{font-size:0.78rem;text-transform:uppercase;letter-spacing:0.06em;color:#999;margin-bottom:0.6rem;font-weight:600} 34 + .row{display:flex;justify-content:space-between;align-items:center;padding:0.25rem 0} 35 + .row .k{color:#666;font-size:0.88rem} 36 + .row .v{font-weight:600;font-variant-numeric:tabular-nums;font-size:0.88rem} 37 + .bar-row{margin:0.35rem 0} 38 + .bar-head{display:flex;justify-content:space-between;font-size:0.82rem;margin-bottom:3px} 39 + .bar-head .k{color:#666} 40 + .bar-head .v{font-weight:600;font-variant-numeric:tabular-nums} 41 + .track{height:5px;background:#eee;border-radius:3px;overflow:hidden} 42 + .fill{height:100%;border-radius:3px;transition:width 0.6s ease} 43 + table{width:100%;border-collapse:collapse;font-size:0.82rem;margin-top:0.25rem} 44 + th{text-align:left;font-weight:500;color:#999;padding:0.2rem 0.5rem 0.2rem 0;border-bottom:1px solid #eee;font-size:0.78rem} 45 + td{padding:0.2rem 0.5rem 0.2rem 0;border-bottom:1px solid #f5f5f5;font-family:ui-monospace,'SF Mono',monospace;font-size:0.8rem} 46 + td.num{text-align:right;font-family:system-ui,-apple-system,sans-serif;font-variant-numeric:tabular-nums} 47 + .badge{display:inline-block;padding:0.1rem 0.45rem;border-radius:3px;font-size:0.75rem;font-weight:600} 48 + .bg{background:#e8f5e9;color:#2e7d32} 49 + .by{background:#fff8e1;color:#f57f17} 50 + .dot{display:inline-block;width:7px;height:7px;border-radius:50%;background:#ccc;margin-right:0.4rem;vertical-align:middle;transition:background 0.3s} 51 + .dot.on{background:#4caf50} 52 + .empty{color:#bbb;font-style:italic} 53 + .sec-head{margin-top:0.6rem;font-size:0.75rem;color:#999;text-transform:uppercase;letter-spacing:0.04em;padding-bottom:0.2rem;border-bottom:1px solid #eee} 54 + </style> 55 + </head> 56 + <body> 57 + <header> 58 + <div><h1>lightrail</h1><div class="sub" id="info"></div></div> 59 + <div class="sub"><span class="dot" id="dot"></span><span id="ts">loading&hellip;</span></div> 60 + </header> 61 + <div class="grid" id="g"></div> 62 + <script> 63 + var $=function(s){return document.getElementById(s)}; 64 + var N=function(n){return typeof n==='number'?n.toLocaleString():'—'}; 65 + 66 + function dur(ms){ 67 + var s=Math.floor(ms/1000),d=Math.floor(s/86400),h=Math.floor(s%86400/3600),m=Math.floor(s%3600/60); 68 + return d>0?d+'d '+h+'h':h>0?h+'h '+m+'m':m+'m'; 69 + } 70 + 71 + function esc(s){var e=document.createElement('span');e.textContent=s;return e.innerHTML} 72 + 73 + function card(title,body){return '<div class="card"><h2>'+title+'</h2>'+body+'</div>'} 74 + 75 + function row(k,v){return '<div class="row"><span class="k">'+k+'</span><span class="v">'+v+'</span></div>'} 76 + 77 + function barRow(k,v,pct,color){ 78 + return '<div class="bar-row"><div class="bar-head"><span class="k">'+k+'</span><span class="v">'+N(v)+'</span></div>' 79 + +'<div class="track"><div class="fill" style="width:'+pct.toFixed(1)+'%;background:'+color+'"></div></div></div>'; 80 + } 81 + 82 + function render(d){ 83 + var now=Date.now(); 84 + $('info').textContent=d.first_startup_ms 85 + ?'up '+dur(now-d.first_startup_ms)+' \u00b7 '+d.startup_count+' start'+(d.startup_count!==1?'s':'') 86 + :''; 87 + 88 + document.title='lightrail \u00b7 Q:'+N(d.resync_queue_depth); 89 + 90 + var h=''; 91 + 92 + /* Backfill */ 93 + var bfBadge=d.upstream_backfill_complete 94 + ?'<span class="badge bg">complete</span>' 95 + :'<span class="badge by">in progress</span>'; 96 + var bf=row('Status',bfBadge)+row('Repos queued',N(d.repos_queued_total)); 97 + if(d.upstream_backfill_completed_at)bf+=row('Completed',new Date(d.upstream_backfill_completed_at).toLocaleString()); 98 + h+=card('Backfill',bf); 99 + 100 + /* Pipeline */ 101 + h+=card('Pipeline', 102 + row('Resyncs completed',N(d.resyncs_completed_total)) 103 + +row('Collection births',N(d.collection_births_total)) 104 + +row('Collection deaths',N(d.collection_deaths_total)) 105 + +row('Queue depth',N(d.resync_queue_depth)) 106 + +row('Buffer count',N(d.resync_buffer_count)) 107 + ); 108 + 109 + /* Cardinality */ 110 + var aMax=Math.max(d.distinct_accounts_all,1); 111 + var pct=function(v){return Math.min(100,v/aMax*100)}; 112 + h+=card('Cardinality Estimates', 113 + row('Collections',N(d.distinct_collections)) 114 + +row('PDS hosts',N(d.distinct_pds_hosts)) 115 + +'<div style="margin-top:0.5rem">' 116 + +barRow('All accounts',d.distinct_accounts_all,pct(d.distinct_accounts_all),'#64b5f6') 117 + +barRow('Resynced',d.distinct_accounts_resynced,pct(d.distinct_accounts_resynced),'#81c784') 118 + +barRow('Commit (strict)',d.distinct_accounts_commit_strict,pct(d.distinct_accounts_commit_strict),'#4a90d9') 119 + +barRow('Commit (lenient)',d.distinct_accounts_commit_lenient,pct(d.distinct_accounts_commit_lenient),'#ffb74d') 120 + +barRow('Desynced',d.distinct_accounts_desynced,pct(d.distinct_accounts_desynced),'#e57373') 121 + +'</div>' 122 + ); 123 + 124 + /* Dispatcher */ 125 + if(d.dispatcher){ 126 + var dp=d.dispatcher; 127 + var body=row('Workers',N(dp.worker_count)) 128 + +row('Busy',N(dp.busy.length)) 129 + +row('Cooling',N(dp.cooling.length)); 130 + 131 + if(dp.hosts.length>0){ 132 + body+='<div class="sec-head">Active hosts</div>'; 133 + body+='<table><tr><th>Host</th><th style="text-align:right">Workers</th></tr>'; 134 + dp.hosts.slice(0,20).forEach(function(e){ 135 + body+='<tr><td>'+esc(e.host)+'</td><td class="num">'+e.workers+'</td></tr>'; 136 + }); 137 + if(dp.hosts.length>20)body+='<tr><td colspan="2" class="empty">+' +(dp.hosts.length-20)+' more</td></tr>'; 138 + body+='</table>'; 139 + } 140 + 141 + if(dp.cooling.length>0){ 142 + body+='<div class="sec-head" style="margin-top:0.5rem">Cooling down</div>'; 143 + body+='<table><tr><th>Host</th><th style="text-align:right">Remaining</th></tr>'; 144 + dp.cooling.forEach(function(c){ 145 + body+='<tr><td>'+esc(c.host)+'</td><td class="num">'+c.remaining_secs.toFixed(0)+'s</td></tr>'; 146 + }); 147 + body+='</table>'; 148 + } 149 + 150 + h+=card('Dispatcher',body); 151 + } 152 + 153 + /* Throttled hosts */ 154 + if(d.throttled_hosts){ 155 + var hosts=Object.entries(d.throttled_hosts); 156 + if(hosts.length>0){ 157 + hosts.sort(function(a,b){ 158 + var ta=Object.values(a[1]).reduce(function(s,n){return s+n},0); 159 + var tb=Object.values(b[1]).reduce(function(s,n){return s+n},0); 160 + return tb-ta; 161 + }); 162 + var body='<table><tr><th>Host</th><th style="text-align:right">Blocked reqs</th></tr>'; 163 + hosts.slice(0,20).forEach(function(pair){ 164 + var total=Object.values(pair[1]).reduce(function(s,n){return s+n},0); 165 + body+='<tr><td>'+esc(pair[0])+'</td><td class="num">'+N(total)+'</td></tr>'; 166 + }); 167 + if(hosts.length>20)body+='<tr><td colspan="2" class="empty">+'+(hosts.length-20)+' more</td></tr>'; 168 + body+='</table>'; 169 + h+=card('Throttled Hosts',body); 170 + } 171 + } 172 + 173 + $('g').innerHTML=h; 174 + } 175 + 176 + function poll(){ 177 + fetch('/admin/status',{credentials:'same-origin'}) 178 + .then(function(r){if(!r.ok)throw new Error(r.status+' '+r.statusText);return r.json()}) 179 + .then(function(d){ 180 + render(d); 181 + $('dot').classList.add('on'); 182 + $('ts').textContent=new Date().toLocaleTimeString(); 183 + }) 184 + .catch(function(e){ 185 + $('dot').classList.remove('on'); 186 + $('ts').textContent='error: '+e.message; 187 + }); 188 + } 189 + 190 + poll(); 191 + setInterval(poll,5000); 192 + </script> 193 + </body> 194 + </html> 195 + "##;
+3
src/server/mod.rs
··· 4 4 //! `IntoRouter` helper. 5 5 6 6 mod admin; 7 + mod admin_page; 7 8 mod get_repo_status; 8 9 mod hello; 9 10 mod list_repos; 10 11 mod list_repos_by_collection; 11 12 12 13 use admin::admin_status; 14 + use admin_page::admin_page; 13 15 use get_repo_status::get_repo_status; 14 16 use list_repos::list_repos; 15 17 use list_repos_by_collection::list_repos_by_collection; ··· 64 66 65 67 let app = if let Some(config) = admin_config { 66 68 let mut app = base 69 + .route("/admin", axum::routing::get(admin_page)) 67 70 .route("/admin/status", axum::routing::get(admin_status)) 68 71 .route("/", axum::routing::get(hello::hello)) 69 72 .with_state(db)