@@ -365,6 +365,11 @@ pub struct DomainFronter {
365365 /// strike state is per-deployment health bookkeeping, not the
366366 /// permanent ban list.
367367 script_timeouts : Arc < std:: sync:: Mutex < HashMap < String , ( Instant , u32 ) > > > ,
368+ /// Every call to `relay()` increments this — exit node AND Apps Script.
369+ /// Use this for UI "fetches today" display. Distinct from `relay_calls`
370+ /// (Apps-Script-direct only) and from the quota tracker's `requests_used`
371+ /// (also Apps-Script-only).
372+ total_relay_calls : AtomicU64 ,
368373 relay_calls : AtomicU64 ,
369374 relay_failures : AtomicU64 ,
370375 bytes_relayed : AtomicU64 ,
@@ -633,6 +638,7 @@ impl DomainFronter {
633638 coalesced : AtomicU64 :: new ( 0 ) ,
634639 blacklist : Arc :: new ( std:: sync:: Mutex :: new ( HashMap :: new ( ) ) ) ,
635640 script_timeouts : Arc :: new ( std:: sync:: Mutex :: new ( HashMap :: new ( ) ) ) ,
641+ total_relay_calls : AtomicU64 :: new ( 0 ) ,
636642 relay_calls : AtomicU64 :: new ( 0 ) ,
637643 relay_failures : AtomicU64 :: new ( 0 ) ,
638644 bytes_relayed : AtomicU64 :: new ( 0 ) ,
@@ -775,6 +781,7 @@ impl DomainFronter {
775781 guard. clone ( )
776782 } ;
777783 StatsSnapshot {
784+ total_relay_calls : self . total_relay_calls . load ( Ordering :: Relaxed ) ,
778785 relay_calls : self . relay_calls . load ( Ordering :: Relaxed ) ,
779786 relay_failures : self . relay_failures . load ( Ordering :: Relaxed ) ,
780787 coalesced : self . coalesced . load ( Ordering :: Relaxed ) ,
@@ -1784,6 +1791,23 @@ impl DomainFronter {
17841791 headers : & [ ( String , String ) ] ,
17851792 body : & [ u8 ] ,
17861793 ) -> Vec < u8 > {
1794+ self . total_relay_calls . fetch_add ( 1 , Ordering :: Relaxed ) ;
1795+
1796+ // Block ALL relay paths (exit node + Apps Script) when every account
1797+ // bucket is quota-exhausted. Checked here so the exit node short-circuit
1798+ // below can't bypass the global hard stop.
1799+ if self . quota_tracker . is_globally_hard_stopped ( ) {
1800+ self . relay_failures . fetch_add ( 1 , Ordering :: Relaxed ) ;
1801+ tracing:: error!(
1802+ "[quota] global hard stop active — all Apps Script account buckets exhausted"
1803+ ) ;
1804+ return error_response (
1805+ 502 ,
1806+ "All Apps Script accounts quota exhausted; hard stop active. \
1807+ Quota resets on a rolling 24-hour window per account.",
1808+ ) ;
1809+ }
1810+
17871811 // Optional URL rewrite for X/Twitter GraphQL (issue #16). Applied
17881812 // here, at the top of relay(), so it affects BOTH the cache key
17891813 // (so matching requests collapse into one entry) AND the URL that
@@ -4907,6 +4931,9 @@ fn decode_js_string_escapes(s: &str) -> Option<String> {
49074931
49084932#[ derive( Debug , Clone ) ]
49094933pub struct StatsSnapshot {
4934+ /// Total calls to `relay()` — all traffic through this fronter including
4935+ /// exit node and Apps Script. Use for "fetches today" display.
4936+ pub total_relay_calls : u64 ,
49104937 pub relay_calls : u64 ,
49114938 pub relay_failures : u64 ,
49124939 pub coalesced : u64 ,
0 commit comments