Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions docs/guide.fa.md
Original file line number Diff line number Diff line change
Expand Up @@ -225,7 +225,9 @@ HTTP / HTTPS مثل قبل از Apps Script می‌رود (تغییری نمی
| ۶ | ۱۸۰ | توصیه‌شده برای استفادهٔ سنگین |
| ۱۲ | ۳۶۰ | چند حساب — حداکثر توان |

بیشتر Deployment = همزمانی بیشتر = تأخیر کمتر هر سشن. هر بَچ بین IDها چرخش می‌کند و بار به‌طور یکنواخت توزیع می‌شود، احتمال رسیدن به سقف سهمیهٔ یک Deployment کاهش می‌یابد.
بیشتر Deployment = همزمانی بیشتر = تأخیر کمتر هر سشن. انتخاب هر بَچ از بین IDهای تنظیم‌شده با یک ledger محلی rolling 24-hour انجام می‌شود؛ بار پخش می‌شود و کلاینت از Deploymentهایی که همین دستگاه نزدیک سقف request سهمیهٔ رایگان برده دوری می‌کند.

پنل **Script health** در UI دسکتاپ همین وضعیت محلی را فقط به‌صورت read-only نشان می‌دهد: Deployment ID ماسک‌شده، تعداد callهای مشاهده‌شده در پنجرهٔ rolling 24-hour، اینکه threshold محلی free-tier اشباع شده یا نه، cooldown باقی‌مانده، دلیل/کلاس خطایی که آن cooldown را ساخته، و تعداد timeout strikeهای فعلی. این فقط telemetry سمت کلاینت است؛ اگر دستگاه‌های دیگر هم از همان deployment استفاده کنند، Google ممکن است callهای بیشتری شمرده باشد.

**محافظ‌های منابع:**
- **حداکثر ۵۰ op** در هر بَچ — اگر سشن‌های فعال بیشتر باشند، مالتی‌پلکسر چند بَچ می‌فرستد
Expand Down Expand Up @@ -350,7 +352,7 @@ sni_hosts = ["www.google.com", "drive.google.com", "docs.google.com"]
| Connection pool | TTL ۴۵ ثانیه، حداکثر ۲۰ idle |
| رمزگشایی gzip | اتوماتیک |
| چند اسکریپت | چرخش round-robin |
| Blacklist خودکار | روی خطای 429 / quota، با cooldown ۱۰ دقیقه |
| قرنطینهٔ خودکار اسکریپت | خطاهای quota/account برای ۲۴ ساعت؛ خطاهای گذرای relay با cooldown کوتاه |
| کش پاسخ | ۵۰ مگابایت، FIFO + TTL، آگاه از `Cache-Control: max-age`، heuristic برای static asset |
| Coalescing | GETهای یکسان همزمان یک fetch upstream را به اشتراک می‌گذارند |
| تونل بازنویسی SNI | مستقیم به لبهٔ گوگل (بدون رله) برای `google.com`، `youtube.com`، `youtu.be`، `youtube-nocookie.com`، `fonts.googleapis.com` — دامنه‌های اضافی از فیلد `hosts` |
Expand Down
6 changes: 4 additions & 2 deletions docs/guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -225,7 +225,9 @@ max_concurrent = 30 × number_of_deployment_ids
| 6 | 180 | Recommended for heavy use |
| 12 | 360 | Multi-account power setup |

More deployments = more total concurrency = lower per-session latency. Each batch round-robins across your IDs, spreading load and reducing the chance of hitting any single deployment's quota ceiling.
More deployments = more total concurrency = lower per-session latency. Each batch is selected from the configured IDs with a local rolling 24-hour ledger, spreading load and steering away from deployments this client has already driven near the free-tier request budget.

The desktop **Script health** panel shows this local state without changing routing behavior: masked deployment ID, locally observed calls inside the rolling 24-hour window, whether the local free-tier steering threshold is saturated, any remaining cooldown, the failure class/reason that set that cooldown, and the current timeout-strike count. Treat it as client-side telemetry only; Google may also count requests from other devices using the same deployment.

**Resource guards:**
- **50 ops max** per batch — if more sessions are active, the mux splits into multiple batches
Expand Down Expand Up @@ -346,7 +348,7 @@ This port focuses on the **`apps_script` mode** — the only one that reliably w
- [x] Connection pooling (45 s TTL, max 20 idle)
- [x] Gzip response decoding
- [x] Multi-script round-robin
- [x] Auto-blacklist failing scripts on 429 / quota errors (10 min cooldown)
- [x] Auto-quarantine failing scripts: quota/account failures for 24 h, transient relay failures for a short cooldown
- [x] Response cache (50 MB, FIFO + TTL, `Cache-Control: max-age` aware, heuristics for static assets)
- [x] Request coalescing: concurrent identical GETs share one upstream fetch
- [x] SNI-rewrite tunnels for `google.com`, `youtube.com`, `youtu.be`, `youtube-nocookie.com`, `fonts.googleapis.com`, configurable via `hosts` map
Expand Down
90 changes: 89 additions & 1 deletion src/bin/ui.rs
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,7 @@ struct UiState {
running: bool,
started_at: Option<Instant>,
last_stats: Option<mhrv_rs::domain_fronter::StatsSnapshot>,
last_script_health: Vec<mhrv_rs::domain_fronter::ScriptHealthSnapshot>,
last_per_site: Vec<(String, mhrv_rs::domain_fronter::HostStat)>,
log: VecDeque<String>,
/// Result + timestamp for transient status banners (auto-hide after 10s).
Expand Down Expand Up @@ -1156,7 +1157,7 @@ impl eframe::App for App {
ui.add_space(8.0);

// ── Status + stats card ────────────────────────────────────────
let (running, started_at, stats, ca_trusted, last_test_msg, per_site) = {
let (running, started_at, stats, ca_trusted, last_test_msg, per_site, script_health) = {
let s = self.shared.state.lock().unwrap();
(
s.running,
Expand All @@ -1165,6 +1166,7 @@ impl eframe::App for App {
s.ca_trusted,
s.last_test_msg.clone(),
s.last_per_site.clone(),
s.last_script_health.clone(),
)
};

Expand Down Expand Up @@ -1318,6 +1320,66 @@ impl eframe::App for App {
});
}

if !script_health.is_empty() {
ui.add_space(2.0);
egui::CollapsingHeader::new(format!(
"Script health ({} deployments)",
script_health.len()
))
.default_open(false)
.show(ui, |ui| {
egui::ScrollArea::vertical()
.max_height(160.0)
.show(ui, |ui| {
egui::Grid::new("script_health")
.num_columns(5)
.spacing([8.0, 2.0])
.striped(true)
.show(ui, |ui| {
ui.label(egui::RichText::new("script").strong());
ui.label(egui::RichText::new("quota").strong());
ui.label(egui::RichText::new("cooldown").strong());
ui.label(egui::RichText::new("reason").strong());
ui.label(egui::RichText::new("timeouts").strong());
ui.end_row();

for st in &script_health {
let quota = format!(
"{} / {}{}",
st.quota_used,
st.quota_limit,
if st.quota_saturated { " saturated" } else { "" }
);
let cooldown = st
.cooldown_secs
.map(fmt_seconds_compact)
.unwrap_or_else(|| "-".to_string());
let reason = st
.cooldown_reason
.as_deref()
.unwrap_or("-")
.to_string();
ui.label(egui::RichText::new(&st.script_id).monospace());
ui.label(egui::RichText::new(quota).monospace());
ui.label(egui::RichText::new(cooldown).monospace());
ui.label(egui::RichText::new(reason).small());
ui.label(
egui::RichText::new(st.timeout_strikes.to_string())
.monospace(),
);
ui.end_row();
}
});
});
ui.small(
egui::RichText::new(
"Local view only: Google quota can also be consumed by other clients using the same deployment.",
)
.color(egui::Color32::from_gray(130)),
);
});
}

if !per_site.is_empty() {
ui.add_space(2.0);
egui::CollapsingHeader::new(format!("Per-site ({} hosts)", per_site.len()))
Expand Down Expand Up @@ -1949,6 +2011,16 @@ fn fmt_duration(d: Duration) -> String {
format!("{:02}:{:02}:{:02}", s / 3600, (s / 60) % 60, s % 60)
}

fn fmt_seconds_compact(seconds: u64) -> String {
if seconds >= 3600 {
format!("{}h {}m", seconds / 3600, (seconds / 60) % 60)
} else if seconds >= 60 {
format!("{}m {}s", seconds / 60, seconds % 60)
} else {
format!("{}s", seconds)
}
}

fn fmt_bytes(b: u64) -> String {
const K: u64 = 1024;
const M: u64 = K * K;
Expand Down Expand Up @@ -1986,9 +2058,11 @@ fn background_thread(shared: Arc<Shared>, rx: Receiver<Cmd>) {
if let Some(fronter) = f.as_ref() {
let s = fronter.snapshot_stats();
let per_site = fronter.snapshot_per_site();
let script_health = fronter.snapshot_script_health();
let mut st = shared.state.lock().unwrap();
st.last_stats = Some(s);
st.last_per_site = per_site;
st.last_script_health = script_health;
}
});
}
Expand Down Expand Up @@ -2064,6 +2138,7 @@ fn background_thread(shared: Arc<Shared>, rx: Receiver<Cmd>) {
// or normal shutdown without Cmd::Stop). The
// Stop handler clears this too — either is fine.
st.proxy_active = false;
st.last_script_health.clear();
}
push_log(&shared2, "[ui] proxy stopped");
});
Expand Down Expand Up @@ -2094,6 +2169,7 @@ fn background_thread(shared: Arc<Shared>, rx: Receiver<Cmd>) {
st.running = false;
st.started_at = None;
st.proxy_active = false;
st.last_script_health.clear();
}
}

Expand Down Expand Up @@ -2568,3 +2644,15 @@ fn push_log(shared: &Shared, msg: &str) {
s.log.pop_front();
}
}

#[cfg(test)]
mod tests {
use super::fmt_seconds_compact;

#[test]
fn compact_seconds_formatter_scales_units() {
assert_eq!(fmt_seconds_compact(9), "9s");
assert_eq!(fmt_seconds_compact(125), "2m 5s");
assert_eq!(fmt_seconds_compact(3660), "1h 1m");
}
}
Loading
Loading