/ca/ca.crt` and `ca.key`.
+- Installed into the user's OS trust store via the `cert_installer` module.
+- On Windows: user-trust store via `certutil -addstore`.
+- On macOS: login keychain via `security`.
+- On Linux: distro-specific (NSS for Firefox, system bundle for Chrome/curl).
+- **On Android**: only the **user trust store**, not system. Most apps (YouTube, Gmail, Telegram, Instagram, banking) only trust the system store, so they don't see mhrv-rs. Chrome/Firefox/Edge browsers explicitly opt in to user trust and DO use mhrv-rs. This is the Android user-trust-store gotcha that drives much of the Android UX confusion. Workaround for power users: root + Magisk + MagiskTrustUserCerts module migrates user CA to system.
+
+The `--remove-cert` CLI flag tears down the CA cleanly (uninstall from trust store + delete files). PR #121 from `dazzling-no-more` added this; lives in `src/main.rs` `remove_cert` flow.
+
+## SNI rewriting + google_ip rotation
+
+The TLS handshake between mhrv-rs and Apps Script does:
+
+- **TCP connect** to `google_ip` (default `216.239.38.120` — a Google edge IP)
+- **TLS SNI** = `www.google.com` (rewritten — this is what the ISP sees in cleartext)
+- **HTTP Host header** = `script.google.com` (the real destination, hidden inside the encrypted tunnel)
+
+Iran ISPs occasionally filter specific Google IPs (#313 pattern). When this happens, the user can rotate `google_ip` to another IP from `DEFAULT_GOOGLE_SNI_POOL` (the 12-entry list in `src/domain_fronter.rs`). `mhrv-rs scan-ips` is a diagnostic command that probes Google IPs from the user's network and reports which ones complete TLS handshakes.
+
+`scan_config.json` (separate from main `config.json`) is the input for `mhrv-rs scan-ips` — users sometimes confuse the two and put the scan config where the main config should be. See `issue-patterns.md`.
+
+## v1.8.0 anti-fingerprinting features
+
+- **Random padding** (`_pad` field, 0-1024 bytes uniform random, base64) — defeats DPI length-distribution fingerprinting. Users on heavily-throttled ISPs can disable with `disable_padding: true` (~25% bandwidth savings) — landed in v1.8.1.
+- **Auto-blacklist deployments** that timeout repeatedly (#319) — round-robin pool actively excludes failing deployments for a cooldown period. Tunable strike threshold queued for v1.8.x.
+- **Decoy responses** for bad-auth requests — see `DIAGNOSTIC_MODE` above.
+- **Active-probing defense** — random benign body on `doGet` requests so a probe to the deployment URL doesn't reveal that it's a relay.
+
+## v1.8.3 features (just shipped)
+
+- **DoH bypass** — DNS-over-HTTPS to Cloudflare/Google/Quad9/AdGuard/etc. routes around the Apps Script tunnel via plain TCP/443. Saves ~2s per DNS lookup. Default on; opt out with `tunnel_doh: true`.
+- **H1 container keepalive** — 240s ping to prevent Apps Script V8 cold-start stalls. Visible win for YouTube playback after pause.
+- **64 KB header cap with HTTP 431** — replaces silent socket drops that caused browser retry loops on oversized headers.
+- **Spreadsheet-backed response cache** in Code.gs (opt-in via `CACHE_SPREADSHEET_ID`) — TTL-aware, Vary-aware, circular-buffer for O(1) writes. Reduces UrlFetchApp quota consumption.
+
+## Key files in the repo
+
+- `src/main.rs` — CLI binary entry point. `init_logging()` reads `config.log_level`. `Cmd::Test`, `Cmd::ScanIps`, etc. as subcommands.
+- `src/bin/ui.rs` — UI binary entry (Windows + Android via JNI). Shares lib code via `mhrv_rs::*`. The `install_ui_tracing` function (post-v1.8.2) reads `RUST_LOG > config.log_level > info,hyper=warn`.
+- `src/lib.rs` — re-exports for the lib + Android JNI shim.
+- `src/domain_fronter.rs` — the SNI-rewrite TLS dialer + the `DomainFronter` orchestrator. `DEFAULT_GOOGLE_SNI_POOL` lives here.
+- `src/proxy_server.rs` — HTTP/SOCKS5 listeners, dispatch logic, DoH bypass, MITM mode entry.
+- `src/tunnel_client.rs` — Full mode batch client. Decoy detection + script_id-in-logs added v1.8.1; softer 6-cause message v1.8.3.
+- `src/mitm/` — MITM cert manager.
+- `src/cert_installer/` — per-OS trust store installation logic.
+- `src/config.rs` — `Config` struct + JSON serde. Default values, validation.
+- `assets/apps_script/Code.gs` and `CodeFull.gs` — server-side scripts. Edit these and tell users to redeploy as new version in Apps Script.
+- `tunnel-node/` — separate Rust crate for the Full-mode VPS container. README + README.fa.md (Persian translation).
+- `android/app/src/main/java/com/therealaleph/mhrv/` — Android Kotlin glue. `MhrvVpnService.kt` is the VPNService that calls into Rust via JNI. `ConfigStore.kt` is the form/preferences round-trip.
+- `docs/changelog/` — versioned changelog files. Format: Persian, then `---`, then English.
+- `.github/workflows/release.yml` — release CI: builds for all platforms, attaches to GitHub release.
+- `.github/workflows/telegram-publish-files.yml` — fires on `workflow_run` of release.yml; posts each file individually to the Telegram channel `-1003966234444` with Persian captions, SHA-256 in caption, and a cross-link from the main channel.
+- `.github/scripts/telegram_publish_files.py` — stdlib-only Python script that does the actual Telegram posting (no `requests` dep so it works in minimal CI runners).
diff --git a/docs/maintainer/references/contributors.md b/docs/maintainer/references/contributors.md
new file mode 100644
index 0000000..1330fd5
--- /dev/null
+++ b/docs/maintainer/references/contributors.md
@@ -0,0 +1,126 @@
+# Contributor ecosystem
+
+The project's substantive contributors fall into a few specialty domains. Knowing who-does-what lets you tag the right reviewer, weight feedback appropriately, and route new design decisions to the people most likely to have informed opinions.
+
+## Project owner
+
+### @therealaleph
+
+Maintainer. Final authority on architectural decisions, release timing, what merges. Persian/English bilingual. Replies that go through Claude carry the marker `[reply via Anthropic Claude | reviewed by @therealaleph]` and are reviewed before posting.
+
+## Core community contributors
+
+These are the contributors whose substantive PRs and reports have shaped the project's roadmap. When designing features that touch their domain, tag them for review.
+
+### @w0l4i
+
+**Domain**: deep diagnostic feedback, architectural insight, persistence on hard bugs.
+
+**Notable contributions**:
+- Drove the v1.8.1 → v1.8.2 → v1.8.3 evolution of decoy detection. Reported the false-positive in v1.8.1 that led to the 4-cause taxonomy (then 5-cause, then 6-cause).
+- Reported the Persian-localized quota body case (#404) after multiple iterations through wrong hypotheses (third-party relay → Iranian VPS appliance → Hetzner DE → Apps Script account locale).
+- Suggested the v1.8.x "per-deployment auto-throttle" feature (AIMD style) with detailed rationale.
+- Suggested the v1.9.0 xmux roadmap items: byte-range slipstreaming across deployments, MTU/packet-size optimization, per-deployment burst limits.
+- Drove the v1.8.x DNS architecture redesign by pointing out that Iranian DNS providers (Shecan, 403) perform DNS hijacking and poisoning — they cannot be trusted as privacy-preserving alternatives (see #449).
+
+**How to engage**:
+- Reports are detailed and self-correct fast as data comes in
+- Setups tend to be advanced (multiple deployments, Hetzner VPS, Full mode)
+- Tag as a core reviewer for v1.9.0 xmux design issue when filed
+- Communication: English
+
+### @2bemoji
+
+**Domain**: roadmap design discussions, particularly for QUIC blocking and DNS optimization.
+
+**Notable contributions**:
+- Drove the design of `block_quic` 3-state UI toggle (off / drop / reject with ICMP unreachable for instant Happy Eyeballs failover) in #361 / #377
+- Surfaced the mobile-accessibility framing for `block_quic` (config-only is "Linux desktop only" for users who can't easily edit Android's `/data/data/...` config)
+
+**How to engage**:
+- Tag for Android UI batch decisions, especially anything touching QUIC / DNS / network-layer toggles
+- Tag for v1.9.0 xmux design as a core reviewer
+- Communication: English
+
+### @ipvsami / Sam Ashouri
+
+**Domain**: advanced Full mode setups, dual-VPS topologies, account suspension reports.
+
+**Notable contributions**:
+- Reported the Iranian-VPS xray entry topology in #420 (Iranian VPS as xray entry, German VPS as tunnel-node exit) — drove the dual-routing-xray design discussion
+- Reported the Google account flag pattern in #421 (phone-less new accounts, "action required" notifications, Workspace landing HTML on flagged deployments) — drove the v1.8.x detection for the 6th cause in the diagnostic taxonomy
+
+**How to engage**:
+- Comfortable with VPS / xray / network routing; explanations can assume that level
+- Tag for v1.9.0 xmux design as a core reviewer
+- Communication: English
+
+### @dazzling-no-more
+
+**Domain**: code contributor — substantive Rust PRs.
+
+**Notable contributions**:
+- PR #121 (`--remove-cert` flag for clean CA teardown)
+- PR #359 (Google Drive queue tunnel mode — community-testing, awaiting cleanup confirmation)
+- PR #438 (H1 container keepalive + 431 oversized headers + clearer port-collision message — merged in v1.8.3)
+- PR #439 (DoH bypass for Cloudflare/Google/Quad9/etc. on TCP/443 — merged in v1.8.3)
+- PR #446 (tunnel-node long-poll raised to 15s, adaptive straggler settle — merged in v1.8.4)
+
+**How to engage**:
+- PRs tend to be self-contained with tests and clean diffs
+- Address review feedback substantively — they iterate based on reviewer comments
+- Tag for v1.9.0 xmux design as a core reviewer (could potentially contribute the implementation)
+- Communication: English
+
+### @euvel
+
+**Domain**: code contributor — Apps Script (Code.gs) features.
+
+**Notable contributions**:
+- Designed the spreadsheet-backed response cache (#400 design discussion → PR #443 implementation)
+- All 5 review suggestions from the design discussion implemented in PR #443: TTL-aware caching, 35 KB body-size gate, header rewriting on hit, circular buffer for O(1) writes, Vary-aware compound cache keys
+
+**How to engage**:
+- Apps Script JavaScript expertise; consider tagging for any future Code.gs changes
+- Communication: English
+
+## Adjacent projects
+
+### @masterking32
+
+Original Python project (`masterking32/MasterHttpRelayVPN`). mhrv-rs is the Rust port; the project periodically cherry-picks stability/feature commits from masterking32. PR #438 in v1.8.3 was a batch of three such cherry-picks. Not a direct contributor here, but the project's design parent.
+
+### @denuitt1
+
+Maintainer of `denuitt1/mhr-cfw` — Cloudflare Workers backend that aims to be Apps Script-compatible. Independent project, not officially endorsed. Tracked in #380 / #393 for compatibility audit. Not a direct contributor here.
+
+### @g3ntrix, @mehrad-mz
+
+Authors of forks/branches on the Python project that occasionally have valuable commits to cherry-pick (see #430 for the audit list).
+
+## Tagging conventions
+
+When tagging in a comment:
+
+- Reviewer requests: "@dazzling-no-more — would you mind reviewing this approach?"
+- Cross-references: "see [#404](https://github.com/therealaleph/MasterHttpRelayVPN-RUST/issues/404) where @w0l4i described this"
+- Recognition: "this drove the design — thanks @euvel for the detailed initial proposal"
+- For v1.9.0 xmux design issue specifically (when it's filed): tag @w0l4i, @2bemoji, @ipvsami, @dazzling-no-more, @euvel as core reviewers
+
+Don't ping people gratuitously; each ping should have a clear ask or recognition.
+
+## Project history context
+
+The project predates this repo as `masterking32/MasterHttpRelayVPN` (Python). The Rust port was started for performance + cross-platform binary distribution. Apps Script protocol stayed compatible across both, and we periodically cherry-pick from upstream Python. v1.7.x represented the initial port stabilization; v1.8.x is the "DPI evasion + diagnostics + community-contribution batch"; v1.9.0 will be the xmux flagship.
+
+Canonical "long" issues for context:
+- #313 — Iran ISP throttle, primary tracking issue
+- #300 — SABR cliff, primary tracking for video streaming limit
+- #310 — VPS setup help, primary tracking for setup questions
+- #333 — VPS / Full mode / Iranian-network workarounds
+- #420 — dual-VPS topology, primary tracking for advanced Full mode
+- #382 — Cloudflare error patterns
+- #325 — community-shared deployment workflow
+- #361 / #377 — Android UI batch + QUIC blocking design
+- #369 — v1.9.0 xmux design (RFC, not yet filed as the formal design issue)
+- #449 — DNS architecture redesign (post-Shecan correction)
diff --git a/docs/maintainer/references/diagnostic-taxonomy.md b/docs/maintainer/references/diagnostic-taxonomy.md
new file mode 100644
index 0000000..35b8b6f
--- /dev/null
+++ b/docs/maintainer/references/diagnostic-taxonomy.md
@@ -0,0 +1,161 @@
+# Diagnostic taxonomy: the placeholder body
+
+## What this is
+
+Multiple distinct conditions cause Apps Script (or our own scripts on Apps Script) to return an HTML body that mhrv-rs's batch parser sees as `bad response: no json in batch response: `. Through user reports and iteration we've narrowed the body strings to **6 candidate causes**. Distinguishing them requires both client-side detection (string-match on body content) and server-side disambiguation (`DIAGNOSTIC_MODE` flag in Code.gs).
+
+This taxonomy is the post-mortem evolution of v1.8.0 → v1.8.1 → v1.8.2 → v1.8.3 detection. v1.8.1 falsely asserted "AUTH_KEY mismatch" on body match; v1.8.2 softened to enumerate 4 candidates; v1.8.3 added the Persian-localized cause and the Workspace landing HTML cause for account-flagged deployments — bringing the count to 6.
+
+## The 6 candidate causes
+
+### 1. AUTH_KEY mismatch (intentional decoy)
+
+**Body**:
+```html
+
+
+Web App
+The script completed but did not return anything.
+
+```
+
+**Source**: Our `Code.gs` / `CodeFull.gs` returns this when `request.k !== AUTH_KEY` and `DIAGNOSTIC_MODE = false`. It mimics Apps Script's stock placeholder for empty-return scripts.
+
+**Trigger**: User edited AUTH_KEY in Apps Script editor but didn't redeploy as new version, OR user has different AUTH_KEY in `config.json` than in `Code.gs`, OR user is using Code.gs deployment ID with `mode: full` (which expects CodeFull.gs).
+
+**Disambiguator**: Set `DIAGNOSTIC_MODE = true` in Code.gs / CodeFull.gs + redeploy as new version. Then this case returns `{"e":"unauthorized"}` (explicit JSON) instead of the HTML. The other 5 cases are independent of DIAGNOSTIC_MODE and still return their natural body.
+
+**Fix**: Align AUTH_KEY values + redeploy as new version.
+
+### 2. Apps Script execution timeout
+
+**Body**: same `"The script completed but did not return anything"` HTML, but emitted by Apps Script itself (not our script) when the execution exceeded the per-invocation cap.
+
+**Source**: Apps Script's runtime kills the script after 6-min hard cap or 30s soft cap on Web App responses, then serves the placeholder body.
+
+**Trigger**: Slow upstream destination, large response payload, network stall mid-fetch.
+
+**Disambiguator**: With `DIAGNOSTIC_MODE = true`, AUTH_KEY mismatch (cause 1) goes away; if the placeholder body still appears for some batches, it's likely cause 2/3/4/5/6.
+
+**Fix**: Lower `parallel_concurrency` in `config.json`, retry, accept some intermittent failures.
+
+### 3. Apps Script soft-quota tear
+
+**Body**: same placeholder HTML. Sometimes a different short HTML page mentioning Apps Script's quota system.
+
+**Source**: Apps Script's per-100s rolling soft quota or per-account daily quota hit. Apps Script kills the request mid-execution.
+
+**Trigger**: Account-aggregate UrlFetchApp throughput exceeded per-100s threshold (~30 concurrent or so). Common with multi-device single-deployment users during page load events (browsers fire 50+ requests in a burst).
+
+**Disambiguator**: Same as 2 — DIAGNOSTIC_MODE rules out AUTH_KEY but doesn't distinguish 2 from 3 from 4. Check the per-script_id error rate over a few minutes — if a deployment has 30%+ failure rate during peak browser activity but works fine when idle, it's quota-related (3 or possibly 5).
+
+**Fix**: Lower `parallel_concurrency`, add more deployments to `script_ids` rotation, distribute deployments across multiple Google accounts.
+
+### 4. Iran ISP-side response truncation
+
+**Body**: typically truncated mid-stream — the body that arrives at mhrv-rs is missing the trailing JSON envelope. The early bytes look like a valid Apps Script response prefix but the request was cut by an ISP-side TCP RST mid-flight.
+
+**Source**: Iran's ISP infrastructure (especially TCI/مخابرات) actively RST-injects on TLS connections to specific Google IPs (the #313 pattern).
+
+**Trigger**: Network-conditional. Active throttle periods (sometimes hours, sometimes days). Worse on certain Google IPs. Worse on certain Iranian ISPs.
+
+**Disambiguator**: Direct curl test from the user's network (see `issue-patterns.md` Pattern 3). If curl-to-Apps-Script also gets timeouts/RST, confirmed ISP-side. The HTML body in this case is partial/truncated — sometimes just `
+
+
+ ...
+```
+
+May also include phrases like `از سهمیه پهنای باند مجاز فراتر رفتهاید` ("you exceeded the allowed bandwidth quota") and `مقدار انتقال داده را کمتر کنید` ("reduce data transfer volume").
+
+**Source**: Apps Script itself. Apps Script localizes its system error pages based on the deploying Google account's locale (fa-IR for Persian) and/or the request-origin IP.
+
+**Trigger**: Account is Persian-locale (common for Iranian users) AND hit a quota threshold (cause 3) OR an internal Google-side hiccup.
+
+**Disambiguator**: With `DIAGNOSTIC_MODE = true`, cause 1 returns explicit JSON; if Persian HTML still appears, it's not our script — it's Apps Script's own response.
+
+**Important**: w0l4i's case in #404 traced through several wrong hypotheses before landing here:
+- Initially diagnosed as AUTH_KEY mismatch → no, mixed success/failure on same `script_id`
+- Then diagnosed as third-party relay (`g.workstream.ir` looks Iranian) → no, w0l4i clarified it's his own tunnel
+- Then diagnosed as Iranian VPS provider appliance → no, Hetzner Nuremberg
+- Final landing: Apps Script's own Persian-localized quota response based on Google account locale
+
+This iteration is documented because the false starts are instructive — don't lock in on the first hypothesis.
+
+**Fix**: Same as cause 3 (it's a quota issue presenting as Persian HTML).
+
+### 6. Workspace landing HTML for account-flagged deployments
+
+**Body**:
+```html
+
+
+ ...
+```
+
+The body is Google Workspace's landing page (the description "Word web processing, presentations, and spreadsheets" is the standard tagline for Google Docs/Sheets/Slides). It's served by Apps Script when the deployment owner's Google account is in a flagged state (post-warning, pre-suspension).
+
+**Source**: Apps Script refuses to execute the deployed script when the owning account is restricted, and serves the Workspace landing page as a "log in" prompt instead.
+
+**Trigger**: Account is in stage 1b or stage 2 of the suspension progression (see `issue-patterns.md` Pattern 8). Often correlates with phone-less new accounts that ignored the "action required" prompt.
+
+**Disambiguator**: Owner of the deployment can log in to Google → see if there are pending warnings or restrictions. If yes → fix the account (add phone) or rotate the deployment to a healthier account.
+
+**Fix**: Account-side, not config-side. Add phone verification, OR move to a different deployment owner via #325 workflow.
+
+## v1.8.3 detection logic
+
+```rust
+// In src/tunnel_client.rs around line 893+
+if err_msg.contains("The script completed but did not return anything") {
+ tracing::error!(
+ "batch failed (script {}): got the v1.8.0 decoy/placeholder body — \
+ could be (1) AUTH_KEY mismatch (run a direct curl probe against \
+ the deployment to verify), (2) Apps Script execution timeout or \
+ per-100s quota tear (try lowering parallel_concurrency), \
+ (3) Apps Script internal hiccup (transient, retry next batch), \
+ or (4) ISP-side response truncation (#313 pattern, try a \
+ different google_ip). To distinguish (1) from the rest: set \
+ DIAGNOSTIC_MODE=true at the top of Code.gs + redeploy as new \
+ version — only AUTH_KEY mismatch returns this body in diagnostic \
+ mode.",
+ sid_short
+ );
+}
+```
+
+This is the v1.8.2 string. v1.8.3 adds detection for the Persian quota body and the Workspace landing HTML as separate paths.
+
+## When responding to users showing this log
+
+The right response shape is:
+
+1. **Acknowledge** the log line they pasted
+2. **Enumerate** the 6 (or 4-5 in older versions) candidate causes briefly
+3. **Identify the most likely** for their specific case using context clues:
+ - Single-deployment user, fresh setup → likely cause 1 (AUTH_KEY)
+ - Mixed success/failure on same script_id → NOT cause 1 (AUTH_KEY would fail 100%)
+ - "Worked yesterday, broken today" → likely cause 4 (ISP throttle) or cause 8 (account flag in progression)
+ - High concurrency / many devices on one deployment → likely cause 3 (quota) or cause 5 (Persian quota variant)
+ - Persian HTML body → cause 5 or 6
+ - Hetzner/Iranian VPS Full-mode user → check if VPS is actually Iranian (provider appliance is real for Iranian VPS only)
+4. **Give the disambiguator**: DIAGNOSTIC_MODE flip + redeploy
+5. **Give the immediate workaround** appropriate to the most-likely cause
+
+Don't claim certainty before disambiguator data. v1.8.1 over-asserted; v1.8.3 explicitly enumerates because we learned to.
+
+## What v1.8.x roadmap is doing about this
+
+- **Per-script_id error-category counter** — surface in CLI/UI: "deployment AKfycbz1: 95% success, 4% timeout, 1% quota, 0% auth_mismatch over last 5 min". Lets users diagnose without flipping DIAGNOSTIC_MODE.
+- **Distinct error categories in client logs** — separate AUTH_KEY mismatch / timeout / quota / ISP truncation / Persian quota / Workspace landing into 6 distinct error log lines. Currently merged.
+- **AIMD per-deployment auto-throttle** — automatically lower `parallel_concurrency` for deployments that hit quota too often. Find the sustainable rate per deployment without manual tuning.
+
+These are queued for v1.8.x batch (~2-4 weeks).
diff --git a/docs/maintainer/references/issue-patterns.md b/docs/maintainer/references/issue-patterns.md
new file mode 100644
index 0000000..87dd77b
--- /dev/null
+++ b/docs/maintainer/references/issue-patterns.md
@@ -0,0 +1,327 @@
+# Issue patterns
+
+The repo gets the same ~15 issues over and over with different wrappers. Recognizing the pattern fast is most of the maintenance job. Each section below covers: the symptoms users describe, what's actually happening, how to diagnose, and the canonical reply structure.
+
+## Pattern 1: AUTH_KEY mismatch (the v1.8.0 decoy body)
+
+**Symptoms**:
+- `502 Relay error: bad response: no json in: ...The script completed but did not return anything`
+- v1.8.1+ logs say `got the v1.8.0 bad-auth decoy` (now soft-language in v1.8.3)
+- Issue title often "502 error", "خطای 502", "ارور relay", or "no json in batch response"
+- Often combined with: "MITM mode works but Full mode doesn't" (CodeFull.gs has different AUTH_KEY than Code.gs)
+
+**Root cause**: The `AUTH_KEY` constant in `Code.gs` (or `CodeFull.gs`) on Apps Script doesn't match the `auth_key` field in mhrv-rs `config.json`. Apps Script returns the v1.8.0 decoy HTML.
+
+**The hidden killer**: Apps Script does NOT auto-pickup edits to deployed scripts. Editing `const AUTH_KEY = "..."` in the Apps Script editor and clicking Save does nothing for the deployed version. The user must:
+
+1. Apps Script web editor → **Deploy → Manage Deployments**
+2. Click the deployment → pencil/Edit
+3. Version dropdown → **New version**
+4. Click Deploy
+
+This redeploys with the new AUTH_KEY. Most users skip this and stay on the old version.
+
+**Diagnostic procedure**:
+
+Tell the user to flip `DIAGNOSTIC_MODE = true` at the top of `Code.gs` / `CodeFull.gs`, redeploy as new version, and re-test:
+
+- If they still see the same decoy body → it's NOT AUTH_KEY mismatch (one of the other 5 candidate causes — see `diagnostic-taxonomy.md`)
+- If they see explicit JSON `{"e":"unauthorized"}` → confirmed AUTH_KEY mismatch; align values + redeploy as new version
+
+**Canonical reply structure** (from #414 thread):
+
+1. Confirm the symptom matches the v1.8.x decoy detection
+2. Walk through the 6 candidate causes and explain why AUTH_KEY mismatch is most likely for their case
+3. Detail the redeploy-as-new-version steps with exact UI clicks
+4. Suggest the DIAGNOSTIC_MODE flip as the disambiguator
+5. Close with link to `diagnostic-taxonomy.md`-equivalent context
+
+## Pattern 2: TUNNEL_AUTH_KEY env var name confusion (Full mode)
+
+**Symptoms**:
+- User on Full mode, Docker container set up
+- `docker logs mhrv-tunnel` shows `tunnel_auth_key not set, using defaults`
+- Or: AUTH_KEY mismatch errors in mhrv-rs that the user "definitely" set correctly
+- Often Persian-language issue (matches Iranian VPS user demographic)
+
+**Root cause**: User typed `MHRV_AUTH_KEY` (wrong, this is what some old docs said), `Tunnel` (wrong, partial match), `tunnel_auth_key` (wrong, lowercase), `TUNNEL-AUTH-KEY` (wrong, dash instead of underscore), or skipped the env var entirely.
+
+The literal env var name is **`TUNNEL_AUTH_KEY`** — uppercase, three underscored words.
+
+**Diagnostic command**:
+```bash
+docker exec mhrv-tunnel env | grep TUNNEL_AUTH_KEY
+```
+
+Should print: `TUNNEL_AUTH_KEY=`. If empty, the env var wasn't set during `docker run`.
+
+**Canonical fix**:
+```bash
+docker stop mhrv-tunnel
+docker rm mhrv-tunnel
+
+docker run -d --name mhrv-tunnel \
+ --restart unless-stopped \
+ -p 8443:8443 \
+ -e TUNNEL_AUTH_KEY="" \
+ ghcr.io/therealaleph/mhrv-tunnel-node:latest
+```
+
+Then in `CodeFull.gs`, `const TUNNEL_AUTH_KEY = ""` must match. Redeploy as new version.
+
+**Related: port mismatch**. If `docker run` used `-p 8443:8080` or similar mapping, the curl test must use the external port. Check with `docker port mhrv-tunnel`.
+
+## Pattern 3: Iran ISP throttle (#313)
+
+**Symptoms**:
+- 504 timeouts, intermittent connection drops
+- "Worked yesterday, broken today"
+- "Mobile data works but home Wi-Fi doesn't" (or vice versa)
+- TLS handshake timeouts during SNI rotation pool tests
+- All sites slow, not specific to one destination
+
+**Root cause**: Iran's ISP infrastructure (especially TCI/مخابرات, less so MCI/همراه) actively RST-injects mid-stream into TLS connections destined for specific Google IPs. This is targeted at Apps Script outbound, not generic Google access. The throttle has plus-and-minus periods — sometimes off for hours, sometimes on for days. Was particularly aggressive starting late April 2026.
+
+**Direct curl test** (the gold-standard diagnostic):
+```bash
+curl -L -X POST 'https://script.google.com/macros/s//exec' \
+ -H 'Content-Type: application/json' \
+ -d '{"k":"","u":"https://httpbin.org/get","m":"GET"}' \
+ --max-time 30 -w "\ntime: %{time_total}s\n"
+```
+
+Run 5-10 times. If majority timeout/RST → ISP throttle confirmed. If majority succeed → it's mhrv-rs path or config.
+
+**Workarounds** (in roughly the order to try):
+1. Upgrade to latest version (each release tends to add diagnostics + small mitigations)
+2. `disable_padding: true` in config (~25% bandwidth savings, helps under throttle)
+3. Rotate `google_ip` to a different IP from the SNI pool (some IPs filtered, others not, varies by ISP and week)
+4. Switch network (mobile data often less throttled than home Wi-Fi)
+5. Multiple `script_ids` in config — rotation helps when individual deployments are mid-throttle
+6. Full mode + non-Iranian VPS (Hetzner/Contabo/OVH or Iranian-VPS-broker like Parspack selling German VPS)
+
+**Don't promise a fix**. The ISP throttle is upstream of anything we can ship. Acknowledge it, list workarounds, point at #313 as the canonical thread.
+
+## Pattern 4: Apps Script self-loop restriction (Google services blocked)
+
+**Symptoms**:
+- "cloud.google.com gives 403"
+- "Can't access Gmail / Meet / Drive / Colab / Gemini"
+- "google.com loads but mail.google.com doesn't"
+- "YouTube video player shows error" (different — this is SABR cliff #300)
+
+**Root cause**: Google explicitly blocks `UrlFetchApp.fetch()` calls to `*.google.com`, `*.googleapis.com`, `*.gstatic.com`, `*.googleusercontent.com`. This is hardcoded into Google's API to prevent Apps Script from being abused as an internal Google proxy. **No HTTP-relay-on-Apps-Script architecture can fix this.**
+
+**No workaround in apps_script mode**. This is permanent.
+
+**Workaround for users with VPS in Full mode**: dual-routing in xray. Their xray client (or v2ray, etc.) routes Google domains direct from their VPS, everything else through mhrv-rs. See #420 for the canonical thread with config snippets.
+
+**Canonical reply**: explain the architectural limit, list the affected sites, point at #420 for the dual-VPS workaround. Close as duplicate of #420 if it's a clean duplicate.
+
+## Pattern 5: SABR cliff (#300) — YouTube video doesn't play
+
+**Symptoms**:
+- "YouTube loads but video doesn't play"
+- "This content isn't available"
+- "Playback error" / "An error occurred"
+- "Short videos work, long ones don't"
+
+**Root cause**: Apps Script's 30-second response cap. YouTube's SABR streaming protocol expects long-lived response streams. After ~30s the stream gets cut by Apps Script and the video player errors out. Page HTML/JS loads fine (small, fits in window). Video stream doesn't.
+
+**Workarounds**:
+- Short videos (<1 min) often work
+- Lowest quality (144p/240p) sometimes squeaks past
+- YouTube web in Chrome/Firefox (browsers use user trust store on Android, YouTube app doesn't) > YouTube app
+- NewPipe (Android, F-Droid) sometimes works better than official app
+- Full mode + VPS (definitive — bytes flow through TCP tunnel, not Apps Script's response window)
+
+v1.9.0 xmux roadmap aims to mitigate by splitting streams across multiple deployments. Won't fully resolve.
+
+**Canonical reply**: explain SABR cliff, list workarounds, close as duplicate of #300 if pure duplicate.
+
+## Pattern 6: Android user trust store
+
+**Symptoms**:
+- "Browser works but YouTube/Telegram/Instagram apps don't"
+- "VPN is on but apps don't go through mhrv-rs"
+- "How do I make Gmail app work?"
+
+**Root cause**: Android has two CA trust stores — system (factory-installed CAs) and user (user-installed CAs via Settings → Security → Install certificate). Since Android 7.0 (2016), most apps default to system-only. The mhrv-rs MITM CA installs to user trust store; system trust requires root.
+
+**Apps that work via mhrv-rs on Android**: Chrome, Firefox, Edge, Brave (browsers explicitly opt in to user trust). Most desktop-class apps that delegate to system browser.
+
+**Apps that don't work**: YouTube app, Gmail app, Maps, Instagram, Twitter/X, banking apps, any app shipped with strict TLS pinning. They use system trust + don't see mhrv-rs.
+
+**Workarounds**:
+- Use web versions (`youtube.com` in Chrome instead of YouTube app)
+- Root + Magisk + MagiskTrustUserCerts module migrates user CA to system
+- Full mode + VPS (bytes don't flow through MITM, so trust isn't needed for arbitrary apps; v2ray/xray on VPS handles routing)
+
+**Canonical reply**: explain user/system trust store distinction, list which apps work, give the three workarounds. This is FAQ-tier — should eventually be in `docs/faq/android.md`.
+
+## Pattern 7: Cloudflare CAPTCHA / 403
+
+**Symptoms**:
+- "Most CF-protected sites block me"
+- "ChatGPT shows captcha I can't solve"
+- "Cloudflare checking your browser..." stuck
+
+**Root cause**: All mhrv-rs traffic exits via Google data center IPs (Apps Script's outbound). Cloudflare's bot detection flags traffic from Google IPs to consumer-facing sites as suspicious — looks like a scraper/bot, not a person. Result: aggressive CAPTCHA, sometimes outright 403.
+
+**Workarounds** (limited):
+- Solve interactive CAPTCHA when shown — the resulting token works for hours
+- Different browser fingerprints sometimes pass (Brave, Tor)
+- Full mode + VPS — VPS exits with its own (residential-adjacent) IP, often not flagged
+- Cloudflare WARP integration is on the v1.9.x roadmap (#309) but feasibility uncertain
+
+**Canonical reply**: explain why (Google IP exit), list workarounds, point at #382 (canonical Cloudflare thread) and #309 (WARP roadmap).
+
+## Pattern 8: Apps Script account suspension / phone-required
+
+**Symptoms**:
+- "Action required" notifications on Google account
+- "Phone number must be added"
+- Deployment intermittently returns Persian Workspace landing HTML (`پردازش کلمه وب...`)
+- Sometimes resolves on its own; sometimes escalates to suspension
+
+**Root cause**: Google's anti-abuse system flags new Google accounts (especially phone-less ones) within hours of deploying automation-pattern code. The progression is: warning → soft restriction (Workspace landing HTML on UrlFetchApp calls) → full suspension.
+
+**Workarounds**:
+1. Add a phone number to the account (most reliable). Iranian phones often filtered by Google's verification; user might need a friend's foreign number, TextNow, paid SMS-receive service, or shared phone
+2. Use established phone-verified accounts (own main Gmail, family/friends' main accounts) — multi-year-old accounts with normal usage history are very rarely flagged
+3. Workflow #325 — community shared deployments (one user with stable account hosts the deployment, others use the deployment ID + shared AUTH_KEY)
+
+**Risk levels** (approximate, from observed reports):
+- Phone-verified personal Gmail, single deployment, light use → low risk
+- Phone-verified, multiple deployments under same account → medium risk
+- New no-phone account, any usage → high risk
+- Old established account, single deployment → very low risk
+
+No confirmed cases of full Google account ban (Gmail deletion, Drive loss). Suspensions are scoped to Apps Script + UrlFetchApp.
+
+## Pattern 9: Telegram / VoIP / "app doesn't work in Full mode"
+
+**Symptoms**:
+- "Can I add Telegram support?"
+- "WhatsApp/Skype voice calls don't work"
+- "Need a port for Telegram"
+
+**Root cause**: Telegram uses MTProto (custom UDP-ish protocol). WhatsApp/Skype/FaceTime voice/video use WebRTC (UDP STUN/TURN). Apps Script's `UrlFetchApp` is HTTP/HTTPS only — **cannot carry UDP or non-HTTP protocols by design.**
+
+**Workarounds**:
+- **Telegram messaging**: web.telegram.org through mhrv-rs Chrome (HTTPS, works)
+- **Telegram MTProto proxy**: use a public MTProto proxy from Telegram channels (free, unreliable) or self-host on VPS
+- **Voice/video calls**: only via Full mode + VPS + xray UDP-enabled routing — bytes route direct from VPS to upstream, not through Apps Script
+
+Architectural ceiling — can't be fixed in mhrv-rs core.
+
+## Pattern 10: Config file confusion (config.json vs scan_config.json)
+
+**Symptoms**:
+- "I followed instructions but it doesn't import the config"
+- User pastes a config that has `google_ips`, `max_ips_to_scan`, `scan_batch_size`, `google_ip_validation` fields
+- Says "the program doesn't pick up my config"
+
+**Root cause**: User confused `config.json` (main runtime config — `script_ids`, `auth_key`, `google_ip`, `mode`, etc.) with `scan_config.json` (input for `mhrv-rs scan-ips` diagnostic command — Google IP discovery).
+
+**Fix**: explain the two files, point at `config.example.json` in repo root for the right template.
+
+Common related typos:
+- `script_id` (singular) instead of `script_ids` (plural array) — mhrv-rs parses as 0 deployments and falls back
+- `mode: "fullmode"` or `"full_mode"` instead of `"full"` (or `"apps_script"`)
+
+## Pattern 11: Windows OpenGL renderer fail
+
+**Symptoms**:
+- `Error: Glutin(Error { ... NotSupported("extension to create ES context with wgl is not present") })`
+- `Error: Wgpu(NoSuitableAdapterFound)`
+- run.bat fails twice (Glow then wgpu fallback) and exits
+
+**Root cause**: User's Windows lacks OpenGL 2.0+ AND lacks DX12/Vulkan-compatible GPU. Causes: old GPU (Intel HD 2500/3000-era), running in VM without GPU acceleration, RDP session, missing/corrupt graphics drivers.
+
+**Workaround**: use the CLI binary `mhrv-rs.exe` directly. Put `config.json` in the same folder, double-click `mhrv-rs.exe`, set browser proxy to `127.0.0.1:8086`. Same functionality, no UI.
+
+v1.8.x roadmap: improve `run.bat` to auto-fallback to CLI when both UI renderers fail.
+
+## Pattern 12: VPS / Full mode setup questions
+
+**Symptoms**:
+- "How do I set up VPS?"
+- "Does the VPS need to be reachable from Iran?"
+- "Which provider should I buy?"
+- "Step-by-step please"
+
+**Canonical answer**: VPS does NOT need to be reachable from Iran (Apps Script proxies the path). Recommended providers:
+
+- **Direct purchase from Iran**: difficult — Hetzner needs VAT ID
+- **Iranian reseller**: Parspack ([parspack.com/vps](https://parspack.com/vps)), Iranserver, Hostiran sell German VPS via Iranian payment with mark-up (~20-40% over direct)
+- **Outside Iran**: Hetzner Falkenstein DE, Contabo DE, OVH SYS — direct euro/dollar payment
+
+Specs: 1 vCPU, 1 GB RAM, 25 GB SSD, 50+ Mbps unmetered → ~$3-5/month direct or ~250-500k toman/month via reseller for personal use. For 5+ devices + Instagram smooth: 2-4 GB RAM, 100 Mbps unmetered.
+
+Setup walkthrough: see `tunnel-node/README.md` and `tunnel-node/README.fa.md` (Persian).
+
+## Pattern 13: Iranian VPS provider bandwidth-cap appliance
+
+**Symptoms** (rare but observed):
+- Persian "exceeded bandwidth quota" HTML response from user's own tunnel-node URL
+- Mixed success/failure on same `script_id`
+
+**Root cause** (provisional — confirmed only when VPS is on Iranian provider): Iranian VPS providers enforce monthly bandwidth quotas at the upstream router/load-balancer layer. When tripped, they intercept traffic and serve a Persian quota landing page **upstream** of the user's Docker container. Container itself never sees the request during quota events.
+
+**Note**: Several users have reported this where the VPS turned out to be at Hetzner DE (not Iranian) — in which case the Persian body is actually Apps Script's own localized soft-quota response (cause #5 in the diagnostic taxonomy). Always confirm the VPS provider before assuming.
+
+**Workarounds**:
+1. Upgrade plan if provider has a higher tier
+2. Move to non-Iranian VPS (Hetzner/Contabo/OVH unmetered)
+3. Client-side bandwidth optimizations: `disable_padding`, lower `parallel_concurrency`, DNS bypass (v1.8.3+)
+
+## Pattern 14: Account locale → Persian Apps Script error pages
+
+**Symptoms**:
+- Apps Script's response body comes back as Persian HTML (Workspace landing page or quota page)
+- User on Hetzner/non-Iranian VPS
+- Their Google account is set to fa-IR locale OR request originates from Iranian IP through some leg
+
+**Root cause**: Apps Script localizes its system error/placeholder pages based on the deploying account's locale and (sometimes) request-origin IP. Persian-locale account → Persian error pages. This is independent of the user's geographic location running mhrv-rs.
+
+**Disambiguator**: `DIAGNOSTIC_MODE = true` in Code.gs. If still see Persian body → it's NOT AUTH_KEY mismatch (which gets replaced with explicit JSON in diagnostic mode). It's Apps Script's own quota/state response.
+
+This is the "5th candidate cause" in the diagnostic taxonomy and the "6th candidate cause" if you separate "Workspace landing HTML for account-flagged deployments" from "Persian quota body for healthy deployments under quota tear".
+
+## Pattern 15: Download large files / IDM workaround
+
+**Symptoms**:
+- "Downloads stick at 1-10 MB"
+- "Need to download a 1 GB file, IDM gets partial only"
+
+**Root cause**: 30s response cliff again. For 10 MB files at typical Apps Script throughput, 30s is enough. For 1 GB, would need 200+ seconds — hopeless.
+
+**Workarounds**:
+- IDM's multi-segment download with 5 MB segments — each segment fits inside 30s window
+- Full mode + VPS — bytes flow through TCP tunnel, not constrained
+- v1.8.x roadmap: range-aware splicing in Code.gs to natively support `Range:` requests
+
+## Quick triage table
+
+When a new issue lands, scan for these keywords to map fast:
+
+| Keywords | Pattern |
+|----------|---------|
+| `502`, `decoy`, `no json in batch`, `script completed but did not return` | 1 (AUTH_KEY mismatch) |
+| `tunnel_auth_key not set`, `MHRV_AUTH_KEY`, `Tunnel_Auth_Key`, `docker logs mhrv-tunnel` | 2 (TUNNEL_AUTH_KEY confusion) |
+| `504`, `timeout`, `Apps Script unresponsive`, `Connection reset`, `RST`, "yesterday worked" | 3 (Iran ISP throttle #313) |
+| `cloud.google.com`, `colab`, `gmail`, `meet`, `gemini`, `drive` not loading | 4 (self-loop restriction → #420) |
+| `YouTube video doesn't play`, `This content isn't available`, `playback error` | 5 (SABR cliff → #300) |
+| Android, `Gmail app`, `YouTube app`, `Telegram`, "browser works but apps don't" | 6 (user trust store) |
+| `Cloudflare`, `captcha`, `403 Forbidden`, "checking your browser" | 7 (CF bot detection → #382) |
+| `Google account`, `phone required`, `action required`, `suspension`, `Workspace landing` | 8 (account flag) |
+| `Telegram support`, `WhatsApp call`, `Skype`, `voice call`, `video call` | 9 (UDP/MTProto architectural) |
+| Config has `google_ips`, `scan_batch_size`, `max_ips_to_scan` | 10 (scan_config confusion) |
+| `egui_glow`, `OpenGL`, `wgl`, `Wgpu(NoSuitableAdapterFound)`, `run.bat` | 11 (Windows OpenGL → CLI) |
+| `VPS`, `Hetzner`, `Parspack`, `setup help`, "step by step VPS" | 12 (Full mode setup) |
+| `سهمیه پهنای باند`, `bandwidth quota`, Iranian VPS provider | 13 (provider appliance) |
+| Persian HTML body in error log + non-Iranian VPS | 14 (account locale) |
+| `IDM`, `download stuck`, `large file`, `1 GB download` | 15 (range/cliff) |
+
+If the issue doesn't fit any pattern, it's worth reading carefully — these are the genuine new bugs.
diff --git a/docs/maintainer/references/persian-templates.md b/docs/maintainer/references/persian-templates.md
new file mode 100644
index 0000000..932dcd7
--- /dev/null
+++ b/docs/maintainer/references/persian-templates.md
@@ -0,0 +1,439 @@
+# Persian reply templates
+
+These are starting templates for the highest-frequency Persian-language replies. Don't use them verbatim — adapt to the specific user's log lines, config, and report. They exist to prevent re-deriving common phrasings each time and to keep the project's Persian voice consistent across replies.
+
+The conventions throughout assume:
+- Polite professional register (`میفرمایید` over `میگی`, full pronouns)
+- Half-spaces (ZWNJ, ``) in compound words
+- Latin-script for technical terms inline with Persian particles
+- Persian numerals optional in prose (`۲۰،۰۰۰` or `20,000` both fine — match the user)
+- Code blocks always in Latin
+- Reply marker (Latin) at end
+
+## Template 1: AUTH_KEY mismatch (with redeploy-as-new-version walkthrough)
+
+For users showing the v1.8.x decoy detection log line:
+
+```markdown
+این `502` با body `The script completed but did not return anything` دقیقاً همان pattern decoy detection است که در v1.8.x اضافه شد. شش علت ممکن (per v1.8.3 taxonomy) داره ولی محتملترین برای case شما **AUTH_KEY mismatch** است.
+
+**نکته بسیار مهم که اکثر کاربران نمیدونند:**
+
+اگر AUTH_KEY رو در Code.gs ویرایش کردهاید **بعد از deployment اولیه**، Apps Script اتومات edit رو در deployment موجود pick-up نمیکنه. لازمه که **redeploy as new version** کنید:
+
+1. در Apps Script web editor بازش کنید
+2. Deploy → **Manage Deployments** (نه Deploy → New deployment)
+3. روی **deployment موجود** کلیک کنید → پنسیل (Edit)
+4. در dropdown **Version** → **New version** انتخاب کنید (نه "Head")
+5. Description بنویسید (مثلاً "AUTH_KEY update")
+6. **Deploy** کلیک کنید
+
+URL deployment همون میمونه ولی الان Apps Script کد جدید با AUTH_KEY جدید رو serve میکنه.
+
+**Diagnostic سریع برای تأیید AUTH_KEY mismatch:**
+
+در بالای Code.gs این خط رو پیدا کنید:
+
+`const DIAGNOSTIC_MODE = false;`
+
+تغییر دهید به:
+
+`const DIAGNOSTIC_MODE = true;`
+
+سپس **redeploy as new version** کنید (مثل بالا). سپس test:
+
+- اگر **هنوز decoy body همون** برمیگرده → علت **NOT** AUTH_KEY mismatch است (یکی از سایر ۵ علت)
+- اگر **JSON `{"e":"unauthorized"}` صریح** برمیگرده → بله، AUTH_KEY mismatch — fix رو با aligning AUTH_KEY در config.json با Code.gs انجام دهید + redeploy as new version
+
+بعد از debug کامل، DIAGNOSTIC_MODE رو به `false` برگردونید + redeploy. در production این flag رو false نگه میداریم چون decoy body anti-fingerprinting protection محسوب میشه.
+
+نتیجه DIAGNOSTIC_MODE flip + پیغام دقیق error بعد از redeploy رو share کنید + میتونیم narrow کنیم.
+
+---
+[reply via Anthropic Claude | reviewed by @therealaleph]
+```
+
+## Template 2: TUNNEL_AUTH_KEY exact spelling
+
+For users showing `tunnel_auth_key not set, using defaults` in `docker logs mhrv-tunnel`:
+
+```markdown
+مشکلت یادم نرفته! `tunnel_auth_key not set, using defaults` در logها یعنی **اسم env variable هنوز اشتباه است**. میخوام دقیقتر توضیح بدم چون اسم env vars خیلی sensitive هست:
+
+**اسم env variable باید دقیقاً این باشد** (نه چیز دیگهای، نه شبیه به این):
+
+```
+TUNNEL_AUTH_KEY
+```
+
+- **همهش حروف بزرگ**
+- **با underscore (`_`) بین کلمات** — نه فاصله، نه dash
+- **سه قسمت**: `TUNNEL` + `_` + `AUTH` + `_` + `KEY`
+
+**اشتباهات رایج که `tunnel_auth_key not set` میده:**
+
+| اشتباه | چرا کار نمیکنه |
+|--------|-----------------|
+| `Tunnel` یا `tunnel` (تنها) | اسم کامل نیست، tunnel-node این رو نمیخونه |
+| `Tunnel_Auth_Key` یا `tunnel_auth_key` (lowercase/mixed) | env vars در Linux/Docker case-sensitive هستن |
+| `TUNNEL-AUTH-KEY` (با dash) | باید underscore باشه نه dash |
+| `MHRV_AUTH_KEY` | اشتباه قدیمی، tunnel-node این رو نمیخونه |
+
+**دستور docker run درست — کپی exact:**
+
+```bash
+ssh root@your-vps-ip
+docker stop mhrv-tunnel
+docker rm mhrv-tunnel
+
+docker run -d --name mhrv-tunnel \
+ --restart unless-stopped \
+ -p 8443:8443 \
+ -e TUNNEL_AUTH_KEY="your-secret-here" \
+ ghcr.io/therealaleph/mhrv-tunnel-node:latest
+```
+
+بهجای `your-secret-here` همون مقداری که در CodeFull.gs گذاشتید بنویسید.
+
+**verify بعد از start:**
+
+```bash
+docker exec mhrv-tunnel env | grep TUNNEL_AUTH_KEY
+```
+
+اگر خروجی این باشه:
+```
+TUNNEL_AUTH_KEY=your-secret-here
+```
+درسته. اگر هیچ خروجی نداد یا خروجی متفاوت بود، دستور `docker run` با اسم اشتباه اجرا شده.
+
+نتیجه + خروجی `docker exec` رو share کنید + اگر همچنان مشکل بود narrow میکنیم.
+
+---
+[reply via Anthropic Claude | reviewed by @therealaleph]
+```
+
+## Template 3: #313 ISP throttle (for "504 timeout" reports)
+
+For users with intermittent timeouts that look like ISP throttle:
+
+```markdown
+این الگو با [#313](https://github.com/therealaleph/MasterHttpRelayVPN-RUST/issues/313) (Iran ISP throttle Apps Script outbound) match میکنه. throttle این هفته در حال پلاسی بوده — گاهی off میشه ساعتی، گاهی روزی.
+
+**Diagnostic سریع — direct curl test:**
+
+```bash
+curl -L -X POST 'https://script.google.com/macros/s/YOUR_DEPLOYMENT_ID/exec' \
+ -H 'Content-Type: application/json' \
+ -d '{"k":"YOUR_AUTH_KEY","u":"https://httpbin.org/get","m":"GET"}' \
+ --max-time 30 -w "\ntime: %{time_total}s\n"
+```
+
+اجرا کنید ۵-۱۰ بار. اگر:
+
+- اکثرشون timeout/RST میگیرن = #313 ISP throttle (شبکه شما Apps Script رو filter میکنه)
+- اکثرشون JSON برمیگردونن = مشکل از path mhrv-rs است (config، auth_key، یا غیره)
+
+**Workaround احتمالی برای ISP throttle:**
+
+۱. **به نسخه v1.8.3 (الان موجود) ارتقا دهید:**
+ - دانلود از یا
+ - شامل DoH bypass، H1 keepalive، 6-cause error detection
+
+۲. **`disable_padding: true` در config:**
+
+```json
+{
+ "disable_padding": true,
+ ...
+}
+```
+
+~۲۵٪ bandwidth کمتر، در شبکههای throttle شده compounds رو کم میکنه.
+
+۳. **`google_ip` متفاوت تست کنید** — default `216.239.38.120` ممکنه روی شبکه شما filter شده + یکی دیگه از pool reachable است. لیست pool در `src/domain_fronter.rs` `DEFAULT_GOOGLE_SNI_POOL`.
+
+۴. **شبکه عوض کنید** — همراه/MCI کمترین throttle داره معمولاً. اگر روی Wi-Fi مخابرات هستید، با موبایل دیتا تست کنید.
+
+۵. **چند `script_ids` داشته باشید** — اگر یک deployment quota tear گرفته یا throttle شده، rotation کار میکنه. حداقل ۳-۵ deployment.
+
+۶. **اگر VPS دارید** — Full mode رو امتحان کنید (راهنما [tunnel-node README فارسی](https://github.com/therealaleph/MasterHttpRelayVPN-RUST/blob/main/tunnel-node/README.fa.md)). ISP throttle Apps Script outbound روی Full mode اعمال نمیشه.
+
+نتیجه v1.8.3 + curl test + log رو share کنید + میتونیم narrow کنیم.
+
+---
+[reply via Anthropic Claude | reviewed by @therealaleph]
+```
+
+## Template 4: VPS setup (Full mode) walkthrough
+
+For "how do I set up VPS?" questions:
+
+```markdown
+**Q: آیا VPS باید مستقیم از Iran قابل دسترسی باشه؟**
+
+**کوتاه: نه.** VPS لازم نیست از Iran direct reachable باشه. این مزیت architectural mhrv-rs Full mode است.
+
+مسیر traffic:
+
+```
+Phone (Iran) → mhrv-rs client (Iran) → Apps Script (via Google IP fronting) →
+ Apps Script's UrlFetchApp →
+ VPS tunnel-node container →
+ upstream internet
+```
+
+دقت کنید: **مسیر از Iran به VPS از طریق Apps Script میگذره**. پس:
+
+- Iran ISP فقط TLS traffic به Google IPها میبینه (`216.239.38.120` و سایر) — مثل HTTPS عادی به Google
+- Apps Script (در Google data center، US/EU) به VPS شما call میکنه
+- VPS شما فقط traffic از Google IP میگیره (Apps Script's outbound)
+
+پس حتی اگر VPS IP از Iran ISP filter شده باشه، **مهم نیست** چون هیچ Iran connection direct به VPS نمیره.
+
+**Setup گامبهگام:**
+
+**۱. خرید VPS:**
+
+- اگر میتوانید Hetzner direct: ~€۴.۵۰/ماه از Falkenstein DE — [hetzner.com/cloud](https://www.hetzner.com/cloud)
+- اگر VAT ID نیست: Parspack ([parspack.com/vps](https://parspack.com/vps)) واسطهی آلمانی فروش میکنه با ~۲۵۰-۵۰۰ هزار تومان/ماه
+
+specs توصیه شده:
+- شخصی: 1 vCPU، 1 GB RAM، 25 GB SSD، 50+ Mbps unmetered
+- خانوادگی (۵+ device + Instagram smooth): 2-4 GB RAM، 100 Mbps unmetered
+
+**۲. Docker install:**
+
+```bash
+ssh root@your-vps-ip
+apt update && apt upgrade -y
+apt install -y docker.io
+systemctl enable --now docker
+docker --version # verify
+```
+
+**۳. tunnel-node container run:**
+
+```bash
+docker run -d --name mhrv-tunnel \
+ --restart unless-stopped \
+ -p 8443:8443 \
+ -e TUNNEL_AUTH_KEY="your-secret-here" \
+ ghcr.io/therealaleph/mhrv-tunnel-node:latest
+```
+
+**اسم env var دقیقاً `TUNNEL_AUTH_KEY` ست** — uppercase، با underscore. هر deviation در default `changeme` میافته + بعداً mismatch میسازه.
+
+برای ساخت secret تصادفی:
+```bash
+openssl rand -hex 32
+```
+
+**۴. firewall:**
+
+```bash
+sudo ufw allow 8443/tcp
+sudo ufw allow ssh
+sudo ufw enable
+```
+
+**۵. verify direct از خود VPS:**
+
+```bash
+curl -X POST 'http://localhost:8443/tunnel' \
+ -H 'Content-Type: application/json' \
+ -d '{"k":"YOUR_TUNNEL_SECRET","op":"connect","host":"www.google.com","port":443}' \
+ --max-time 10
+```
+
+باید JSON success برگرده. اگر نه، tunnel-node container start نشده.
+
+**۶. CodeFull.gs setup:**
+
+در [`assets/apps_script/CodeFull.gs`](https://github.com/therealaleph/MasterHttpRelayVPN-RUST/blob/main/assets/apps_script/CodeFull.gs) محتوا رو copy کنید + در script.google.com یک پروژه جدید ایجاد کنید + paste کنید.
+
+بالای فایل تنظیم کنید:
+
+```js
+const AUTH_KEY = "your-mhrv-auth-key";
+const TUNNEL_URL = "http://YOUR_VPS_IP:8443/tunnel";
+const TUNNEL_AUTH_KEY = "your-tunnel-secret-here"; // match با docker run -e
+```
+
+سپس **Deploy → New deployment → Web App → Execute as: Me + Who has access: Anyone → Deploy**. URL deployment رو copy کنید + ID بخشش رو بردارید.
+
+**۷. mhrv-rs config:**
+
+```json
+{
+ "mode": "full",
+ "auth_key": "your-mhrv-auth-key",
+ "script_ids": ["YOUR_DEPLOYMENT_ID"]
+}
+```
+
+**`script_ids` plural با s** — این یک typo رایجه که config رو 0-deployment میکنه.
+
+**۸. Connect + verify:**
+
+mhrv-rs رو start کنید + log باید نشون بده:
+
+```
+INFO batch: 1 ops → AKfyc..., rtt=Xs ← good
+INFO tunnel session abc1234... opened for ...:443 ← good
+```
+
+اگر `ERROR batch failed: got the v1.8.0 bad-auth decoy` میگیرید، AUTH_KEY mismatch است (gam ۶ check کنید).
+
+اگر `Connection refused` به VPS، firewall بسته است (gam ۴ بررسی کنید).
+
+برای فارسی-language راهنما با تصاویر [tunnel-node README فارسی](https://github.com/therealaleph/MasterHttpRelayVPN-RUST/blob/main/tunnel-node/README.fa.md) رو ببینید.
+
+اگر در گامی fail کرد، error log + خروجی command رو share کنید + میتونیم narrow کنیم.
+
+---
+[reply via Anthropic Claude | reviewed by @therealaleph]
+```
+
+## Template 5: Account suspension / phone-required (for "action required" reports)
+
+For users reporting Google account flag or "action required" notifications:
+
+```markdown
+این الگو شناختهشدهست + در اساس Google's anti-abuse system فلاگ میکنه new accounts که immediately Apps Script deployment میسازن (مخصوصاً بدون phone verification).
+
+**Stage تشخیص account flag:**
+
+```
+Stage 1: "Action required - add phone number"
+ ↓ (phone اضافه میشه) → account stable
+ ↓ (phone اضافه نمیشه + automation activity ادامه میده)
+ ↓
+Stage 2: "Account temporarily restricted"
+ ↓ (Apps Script deployments شروع میکنن Workspace landing HTML برگردونن
+ ↓ بهجای execute کردن — see #421 + cause #6 در v1.8.3 detection)
+ ↓
+Stage 3: "Account suspended" — full lockout، deployments fail
+```
+
+شما الان در Stage 1. اگر زود phone verify کنید، account stable میمونه + deployments بدون مشکل ادامه میدن.
+
+**برای فکر شما درباره ban Google account کلی:**
+
+در history reports این پروژه (~۵۰+ کاربر در طول سال گذشته)، **هیچ confirmed case full account ban** ندیدم. consequences scope-شده به Apps Script + UrlFetchApp quota — نه Gmail یا Drive یا سایر Google services. accounts با history regular usage (Gmail, Drive files، etc.) و age چند سال + در low-risk قرار دارند برای personal CodeFull.gs deployment.
+
+**workarounds:**
+
+**۱. بهترین: phone اضافه کنید.**
+
+Iranian phone گاهی filter میشه، ولی میتوانید:
+
+- phone یک friend/family member outside Iran استفاده کنید (SMS code رو forward کنند)
+- TextNow / Google Voice (US) / paid SMS-receive services
+- بعضی موارد Google یک phone رو روی چند account قبول میکنه (~۵ account per phone limit)
+
+**۲. اگر phone نمیتوانید:**
+
+accounts احتمالاً به Stage 2-3 progress میکنن طی روزها-تا-هفته. برای حفظ service:
+
+- deployments جدید زیر accounts متفاوت بسازید قبل از اینکه old fail کنه
+- از **community shared deployment** workflow ([#325](https://github.com/therealaleph/MasterHttpRelayVPN-RUST/issues/325)) استفاده کنید — friend با account stable deployment میسازه + ID share میکنه + AUTH_KEY مشترک
+
+**۳. برای access به script.google.com وقتی شبکه slow:**
+
+میتوانید از **mhrv-rs خود** برای access به script.google.com استفاده کنید. mhrv-rs's HTTP proxy به browser → CONNECT tunneling به Google عمل میکنه (نه UrlFetchApp.fetch — که Google block میکنه). browser رو با proxy `127.0.0.1:8086` تنظیم کنید + بروید script.google.com.
+
+**Action item:**
+
+اگر Stage 1a هستید (notification ولی deployments هنوز کار میکنن): فوراً phone verify کنید.
+
+اگر Stage 1b هستید (deployments شروع به Workspace HTML برمیگردونن): همان، plus rotation deploymentها به accounts سالم.
+
+---
+[reply via Anthropic Claude | reviewed by @therealaleph]
+```
+
+## Template 6: Architectural limit (Google services + UrlFetchApp self-loop)
+
+For users asking why `cloud.google.com` / `colab` / `gmail` / `meet` / `gemini` doesn't work:
+
+```markdown
+این محدودیت **architectural** است + ربطی به config یا setup شما نداره.
+
+**Apps Script's UrlFetchApp self-loop restriction:**
+
+`UrlFetchApp.fetch()` Google در API hardcoded ساخته که نمیتونه به دامنههای `*.google.com` / `*.googleapis.com` / `*.gstatic.com` request بفرسته. Apps Script یا empty response میده یا 4xx/5xx error.
+
+این محدودیت **Google ست** (نه implementation ما) + در Apps Script API documentation هم ذکر شده. هیچ HTTP-relay مبتنی بر Apps Script نمیتونه به Google services از Apps Script→Google path برسه.
+
+**سایتهای متأثر:**
+
+- `cloud.google.com` — Console
+- `colab.research.google.com` — Colab
+- `gemini.google.com` — Gemini chat
+- `drive.google.com` — Drive
+- `docs.google.com` / `sheets.google.com` / `slides.google.com` — Workspace
+- `meet.google.com` — Meet (Web)
+- `mail.google.com` — Gmail
+- `script.google.com/home/usage` — Apps Script dashboard
+- `*.google.com` بهطور کلی
+
+**راهحلها:**
+
+**۱. سایتهای alternative:**
+
+- بهجای Drive: WebDAV / Mega / Cloudflare R2
+- بهجای Colab: Kaggle Notebooks / Jupyter Lab روی VPS
+- بهجای Gemini: ChatGPT (openai.com) / Claude (claude.ai) — اگر CF block نشدن، کار میکنن
+- بهجای Cloud Console: SSH مستقیم یا cloud provider's CLI
+
+**۲. Full mode + VPS:**
+
+VPS از طرف خود به Google direct وصل میشه. در Full mode، traffic Google رو میتوانید با xray dual-routing از mhrv-rs bypass کنید. detail در [#420](https://github.com/therealaleph/MasterHttpRelayVPN-RUST/issues/420). با این setup همهی Google services از طریق VPS direct کار میکنن.
+
+**۳. temp VPN موقت:**
+
+برای access گاهگاهی به Google services (مثلاً برای download فایل از Drive یا setup OAuth)، یک VPN موقت ۱۰ دقیقهای استفاده کنید + سپس به mhrv-rs برمیگردید.
+
+**نتیجه:**
+
+اگر میخواهید سایتهای Google کار کنن با همان setup mhrv-rs که الان دارید، نیاز به Full mode + VPS + xray routing است. تا وقتی فقط apps_script mode دارید، Google services unreachable میمونن.
+
+---
+[reply via Anthropic Claude | reviewed by @therealaleph]
+```
+
+## Common Persian phrases for inline use
+
+When writing custom replies, these phrases come up frequently and should be standardized:
+
+| Concept | Persian phrasing |
+|---------|------------------|
+| "redeploy as new version" | `redeploy as new version کنید (نه head)` |
+| "exact match" | `دقیقاً match کنه` / `exact match` |
+| "case-sensitive" | `case-sensitive است` |
+| "ISP throttle" | `ISP throttle` (English term, transliterate not used) |
+| "narrow down" | `narrow کنیم` |
+| "share the log" | `log رو share کنید` |
+| "thanks for the report" | `ممنون از گزارش` / `تشکر از گزارش` |
+| "I owe you" / "apologies" | `معذرت میخوام بابت` |
+| "for your specific case" | `برای case خاص شما` |
+| "unfortunately" | `متأسفانه` |
+| "the workaround is" | `workaround این هست که...` |
+| "this is a known issue" | `این مشکل شناخته شده است` |
+| "feature is queued" | `feature در roadmap است` |
+| "we'll ship in v1.x.y" | `در v1.x.y ship میشه` |
+| "configuration file" | `فایل config` |
+| "command line" | `command line` / `terminal` / `ترمینال` |
+| "deployment" (Apps Script) | `deployment` (transliterated `دپلوی` is not used in this project) |
+| "tunnel" (Full mode) | `tunnel` |
+| "browser" | `browser` / `مرورگر` |
+| "system proxy" | `system proxy` |
+| "page loads but X doesn't work" | `page بالا میاد ولی X کار نمیکنه` |
+| "I tested with curl" | `با curl تست کردم` |
+| "the bug is fixed in vX.Y.Z" | `bug در vX.Y.Z حل شده` |
+| "thanks for catching this" | `ممنون از catch کردن این` |
+| "let me know if it works" | `اگر کار کرد گزارش بدید` |
+| "if it fails again, share the log" | `اگر دوباره fail کرد، log رو share کنید` |
+
+These let Persian replies use English technical terms naturally without forced transliteration, which matches how Iranian developers actually talk.
diff --git a/docs/maintainer/references/release-workflow.md b/docs/maintainer/references/release-workflow.md
new file mode 100644
index 0000000..7e5b069
--- /dev/null
+++ b/docs/maintainer/references/release-workflow.md
@@ -0,0 +1,211 @@
+# Release workflow
+
+Cutting a release is fast and low-ceremony for this project. Most releases are patch bumps that go from "decision to ship" to "Telegram channel posting" in under 30 minutes of human work + ~30 minutes of CI.
+
+## When to cut a release
+
+Cut a release whenever **anything user-visible** has landed since the last tag. User-visible includes:
+
+- Bug fixes that affect runtime behavior
+- New config options
+- New CLI subcommands or flags
+- Diagnostic improvements (better log messages, error categories)
+- Apps Script script changes (Code.gs / CodeFull.gs)
+- Documentation that users will read (README updates, troubleshooting docs — though these can also batch into the next release)
+
+Don't cut for:
+- Internal refactors with no behavior change
+- CI/workflow file edits
+- Markdown formatting fixes
+- Test-only changes
+
+When in doubt, cut. Patch releases are cheap and Iranian users actively check the Telegram channel for updates.
+
+## The release workflow
+
+### Step 1: Decide the version
+
+Read the latest tag:
+
+```bash
+git describe --tags --abbrev=0
+```
+
+Then bump:
+- **Patch (Z+1)** — for ~95% of releases. v1.8.2 → v1.8.3
+- **Minor (Y+1)** — for a coherent feature batch shipped together. v1.7.x → v1.8.0 represented "DPI evasion + active-probing defense + full-mode usage counters" together
+- **Major (X+1)** — never done in this project's history. Reserved for true protocol-incompatible changes with the Apps Script side. Don't bump major without explicit go-ahead.
+
+### Step 2: Bump `Cargo.toml`
+
+Edit `Cargo.toml` line 3 (`version = "X.Y.Z"`). Keep package name `mhrv-rs` unchanged. The `tunnel-node` subcrate has its own version that's independent — don't bump it unless you're shipping a tunnel-node change.
+
+### Step 3: Build to refresh `Cargo.lock`
+
+```bash
+cargo build --release 2>&1 | tail -3
+```
+
+`Cargo.lock` will pick up the new version string. Verify with:
+
+```bash
+git diff Cargo.lock | head -20
+```
+
+Should show only the `name = "mhrv-rs"` block's `version = "X.Y.Z"` change.
+
+### Step 4: Write the changelog
+
+Create `docs/changelog/vX.Y.Z.md` using the format in `assets/changelog-template.md`. Persian first, then `---`, then English. See `workflow-conventions.md` for format details.
+
+When the release is shipping multiple PRs from contributors, credit each by name + handle in both halves of the changelog.
+
+### Step 5: Run tests + final build
+
+```bash
+cargo test --lib 2>&1 | tail -5
+cargo build --release 2>&1 | tail -3
+cargo build --bin mhrv-rs-ui --release --features ui 2>&1 | tail -3
+```
+
+All three must succeed. Test count varies by version. All passing is the gate.
+
+If any contributor PRs were merged in this release, also verify by re-running tests after the merge — sometimes integration with main reveals issues that didn't show in the PR's CI.
+
+### Step 6: Commit + tag + push
+
+```bash
+git add Cargo.toml Cargo.lock docs/changelog/vX.Y.Z.md
+git status # sanity check
+git commit -m "$(cat <<'EOF'
+chore: vX.Y.Z —
+
+
+EOF
+)"
+
+git push origin main
+git tag vX.Y.Z
+git push origin vX.Y.Z
+```
+
+The `git push origin vX.Y.Z` is the trigger — release CI auto-fires on tag push.
+
+If `git push origin main` fails with `non-fast-forward`, someone (often the auto-binary-refresh CI from a prior release) pushed in the meantime:
+
+```bash
+git pull --rebase origin main
+git push origin main
+git tag vX.Y.Z # if you didn't tag yet
+git push origin vX.Y.Z
+```
+
+If you already tagged before the push race, the tag still works — it's pinned to your commit, and the rebase shouldn't change your commit's SHA unless there were conflicts.
+
+### Step 7: Watch CI
+
+```bash
+gh run list --limit 3
+```
+
+Two workflows fire on tag push:
+1. **`release-drafter`** — quick (~15s), updates the GitHub release draft. Always succeeds.
+2. **`release`** — slow (~25-35 minutes), builds binaries for all platforms, attaches to release.
+
+Once `release` succeeds, a third workflow auto-fires:
+3. **`Telegram publish release files`** — posts each binary individually to the Telegram channel `mhrv_rs` with Persian captions, SHA-256 hashes, and a cross-link from the main channel. Takes ~1-2 minutes.
+
+If `release` fails, common causes:
+
+- **Cross-compile failure** — particularly on i686 / mipsel. i686 was dropped from the matrix in v1.7.11 because of MSRV churn (see #411 thread). If a new architecture starts failing, it's usually a transitive dep bumping MSRV past what the toolchain pinned for that target supports. Triage: check which architecture's job failed, look at the cargo error, decide whether to pin a dep with `cargo update --precise` or drop the architecture.
+- **`actions/download-artifact@v4` flakiness** — replaced with `gh run download` + 3-attempt retry in v1.7.11. Should be stable now; if it flakes again, increase retry count.
+
+After CI succeeds, optionally check the binary refresh:
+
+```bash
+git pull origin main
+git log --oneline -3
+```
+
+You should see an auto-generated commit `chore(releases): refresh prebuilt binaries for vX.Y.Z` from the release CI bot.
+
+### Step 8: Verify Telegram channel
+
+The Telegram publish workflow posts to channel `mhrv_rs` (public link `https://t.me/mhrv_rs`). The channel should show:
+
+1. An announcement post: `📦 mhrv-rs vX.Y.Z منتشر شد...` referencing the changelog file
+2. ~16 individual file posts (Android APKs split by ABI, Windows ZIP, macOS arm64/amd64 dmg+tar, Linux x86_64/arm64 incl. musl, Raspbian, OpenWRT)
+3. Each file caption includes Persian description (e.g., "نسخه ویندوز x86") + SHA-256 hash
+4. A "main channel" post (different channel) cross-linking to the files channel post
+
+Files larger than 50 MB get chunked into `.part_aa`, `.part_ab`, etc. via the `split` pattern in `.github/scripts/telegram_publish_files.py`.
+
+If something didn't post, check the workflow run logs:
+
+```bash
+gh run view --log
+```
+
+Common cause: the auto-fire dispatch on `workflow_run` requires the parent workflow to succeed; if release.yml had a flaky download retry, the dispatch might still succeed but partial.
+
+## Manual re-publish (rare)
+
+If you need to re-trigger Telegram publishing for an already-released version (e.g., the workflow failed and you fixed it), use `workflow_dispatch`:
+
+```bash
+gh workflow run "Telegram publish release files" -f version=vX.Y.Z
+```
+
+The script downloads artifacts via `gh release download` (not the workflow's artifacts) so it works retroactively.
+
+## Re-cutting a release (very rare)
+
+If a release was tagged and pushed but turns out to be broken (e.g., bug in a merged PR you wanted to revert):
+
+1. **Don't** delete the tag if the release is already public. Iranian users may have already pulled the binaries; a deleted tag confuses them and they think the project is gone.
+2. Cut a fix immediately as the next patch (vX.Y.Z+1).
+3. Optionally edit the GitHub release notes for the broken version to say "known issue, upgrade to vX.Y.Z+1".
+
+If you tagged but didn't push yet, just delete the tag locally and re-tag after fixing:
+
+```bash
+git tag -d vX.Y.Z # local only; safe
+# fix the issue, commit
+git tag vX.Y.Z
+git push origin vX.Y.Z
+```
+
+## Pre-release rollback
+
+If `cargo test --lib` fails after merging PRs but before tagging:
+
+1. Don't tag.
+2. Either revert the merge commit (`git revert `) or fix forward (commit a new fix on main).
+3. Re-run tests until green.
+4. Tag.
+
+The release CI doesn't run tests before building, so untagged-but-broken main is fine — you have time to fix before tagging.
+
+## Coordinating with multiple PRs in flight
+
+If two PRs are both ready to merge, the order matters:
+
+- Merge them one at a time (not both into a single tag) **only** if they're independent
+- If they touch the same files, merge them sequentially with `gh pr checkout N1 && cargo test && merge`, then `gh pr checkout N2` (which now bases on the new main; CI on the PR may show the old base, but the local checkout sees latest main) `&& cargo test && merge`
+- If a merge introduces conflicts, GitHub's UI flags the PR as conflicting; resolve via `gh pr checkout N` + manual rebase + push to the PR branch
+
+After all PRs are merged, **then** bump version, write changelog (covering all merged PRs), tag, push.
+
+## Versioning the tunnel-node subcrate
+
+`tunnel-node/Cargo.toml` has its own version field separate from the main crate. Bump it when:
+
+- Changing the tunnel-node HTTP API (`/tunnel`, `/batch` endpoints)
+- Changing the auth flow (`TUNNEL_AUTH_KEY` semantics)
+- Changing the env var contract
+- Bumping the Docker image label
+
+For pure internal refactors of tunnel-node that don't change the surface, leave it alone — the Docker image at `ghcr.io/therealaleph/mhrv-tunnel-node:latest` continues to be the latest tag and users don't need to know an internal version bumped.
+
+When tunnel-node version bumps, the Docker image gets re-tagged in the registry by the CI. Users running `docker pull ghcr.io/therealaleph/mhrv-tunnel-node:latest` get the new version automatically; users pinned to a specific version stay pinned.
diff --git a/docs/maintainer/references/roadmap.md b/docs/maintainer/references/roadmap.md
new file mode 100644
index 0000000..5a1482b
--- /dev/null
+++ b/docs/maintainer/references/roadmap.md
@@ -0,0 +1,118 @@
+# Roadmap
+
+This is the project's queued work, organized by release batch. Use it when:
+- Categorizing a new feature request from a user (which batch does this fit?)
+- Cross-referencing roadmap items in your replies ("queued for v1.8.x")
+- Deciding what to ship in the current batch vs deferred
+
+Update this file when items ship (move to "shipped") or when new items are added (cluster with similar items in the right batch).
+
+## v1.8.x (current batch — small fixes + diagnostics + Android UI)
+
+The v1.8.x line is a **small-and-frequent** release pattern. Each release ships one or two completed items rather than batching many. The theme is "diagnostic improvements + Android UX + Apps Script script enhancements".
+
+### Shipped
+
+- ✅ **v1.8.0**: Random padding (DPI evasion), auto-blacklist deployments, decoy responses, full-mode usage counters, active-probing defense, DIAGNOSTIC_MODE flag in Code.gs
+- ✅ **v1.8.1**: Decoy detection client-side (with v1.8.2/v1.8.3 corrections), `script_id` in error logs, `disable_padding` config flag
+- ✅ **v1.8.2**: UI binary tracing reads `config.log_level` (with reload handle for live changes), softer 4-cause decoy detection error message
+- ✅ **v1.8.3**: Spreadsheet-backed response cache (Code.gs, opt-in), DoH bypass for known DoH endpoints, H1 container keepalive (240s), 64 KB header cap with HTTP 431, clearer port-collision error message
+
+### Queued (small, can ship in next 1-3 patch releases)
+
+- **v1.8.4 candidate items**:
+ - Soften decoy detection further with the 6-cause taxonomy (Persian quota body + Workspace landing HTML detection)
+ - Per-`script_id` rolling-window error-category counter visible in CLI/UI
+ - run.bat auto-fallback to CLI when both UI renderers fail (#417 / #426)
+ - TUNNEL_AUTH_KEY startup warning when `MHRV_AUTH_KEY` is set without `TUNNEL_AUTH_KEY` (catches the recurring #391-style env var typo)
+ - Range-aware splicing in Code.gs (lets large downloads work via HTTP Range requests, partial fix for #441)
+
+### Queued (medium-effort, batch into focused release)
+
+- **`googlevideo_ip` config field** (#300) — separate `google_ip` for googlevideo.com vs other Google domains. Some users have one IP that works for the latter but not the former. Approx 1-2 days of work.
+- **DNS ad-blocking via StevenBlack/hosts** (#377) — opt-in DNS-level filtering during SOCKS5/MITM dispatch. Reduces upstream calls for ad-domains.
+- **DNS caching + parallel dispatch via hickory-resolver** (#377) — replace blocking DNS with cached + parallel resolver. Substantial latency win for browser pageloads.
+- **Tunable strike-counter threshold for auto-blacklist** (#391) — single-deployment users currently hit the auto-blacklist after a few transient errors and end up with no working deployment. Make threshold configurable.
+- **`block_quic` 3-state UI toggle** (#361 / #377): off / drop / reject (default reject = ICMP unreachable, instant Happy Eyeballs failover). 2bemoji's design.
+- **Android UI batch** (#285 / #361 / #261 / #295 / #254 / #313 / #375):
+ - block_quic toggle
+ - youtube_via_relay toggle
+ - listen_host editor
+ - passthrough_hosts editor
+ - Active deployment indicator
+ - Per-deployment quota counters
+ - Android disconnect crash fix (#418)
+- **System proxy toggle** (#432) — Windows/macOS/Linux desktop UI: on Connect set system HTTP proxy to mhrv-rs, on Disconnect clear. With crash-rollback so a hung mhrv-rs doesn't leave system proxy stuck.
+- **`script_ids_url` dynamic config** (#433) — config field pointing at an HTTPS URL that returns a JSON list of deployment IDs. mhrv-rs fetches at startup + every TTL. Lets distributors update deployment lists for many users without each editing config manually.
+- **In-app updater via mhrv-rs's own proxy** (#366) — let mhrv-rs check for updates + download new binaries through its own relay (avoiding the chicken-and-egg of "I can't reach github.com to update mhrv-rs"). Defense in depth.
+- **Temporal jitter** (#369 §2) — randomize timing of batch dispatches to defeat timing-correlation DPI.
+- **`tls_verify` config** (#430 / masterking32 PR #26) — opt-in to skip upstream TLS verification for self-signed certs. Trade-off: opens MITM-of-MITM risk; needs careful design.
+- **`request_timeout` configurable** (#430 / masterking32 PR #25) — currently hard-coded `BATCH_TIMEOUT = 30s`. Make configurable for users on slow networks who want longer timeouts.
+- **CF Workers backend audit** (#380 / #393) — test mhr-cfw compatibility. If it works, document as alternative backend.
+
+### Documentation queued
+
+- **`docs/full-mode-google-bypass.md`** (#420) — dual-routing in xray for users with Iranian VPS xray entry topology
+- **`docs/full-mode-iran-vps-setup.md`** (#420) — full step-by-step for the dual-VPS topology (Iranian xray entry + non-Iranian tunnel-node exit)
+- **`docs/iran-mirrors.md`** (#422) — community-maintained Iranian CDN mirrors for users who can't reach github.com. Pending SHA-256 verification of @amintoorchi's xdevteam.liara.space mirrors.
+- **`docs/win7-build.md`** (#411) — manual Cargo.lock downgrade + cargo update --precise chain for community Win7 32-bit builds. Officially unsupported since v1.7.11 but the build path works for technical users.
+- **`docs/faq/android.md`** — user trust store explanation, which apps work, why Gmail/YouTube don't, root + Magisk option
+- **Updates to README** — short explanation of dual-routing for Google services + xray config snippet
+
+## v1.9.0 (headline release — xmux)
+
+The v1.9.0 release is the **xmux** feature: stream splitting across multiple Apps Script deployments at byte-range level. Currently in design / RFC stage (#369).
+
+### Design goals
+
+- **Survivability under ISP RST** — when one deployment's TCP connection gets RST-injected mid-stream, other deployments continue to carry remaining byte ranges
+- **Latency reduction** — small responses can hit any of N deployments first; mhrv-rs takes the first to respond
+- **Bandwidth aggregation** — large downloads chunk across deployments concurrently. 5 deployments × 10 MB/s each ≈ 50 MB/s aggregate (subject to per-deployment caps)
+- **SABR cliff mitigation** — long YouTube streams chunk into <30s windows across deployments; each window finishes within Apps Script's response cap, then mhrv-rs reassembles
+
+### Open design questions
+
+- **Reordering buffer size** — bigger = more memory; smaller = more retries on out-of-order
+- **Failure recovery** — if a deployment fails mid-chunk, who picks up the half-served range?
+- **Idempotency** — POST requests are tricky; current design only handles GET safely
+- **State consistency** — if some chunks come from cache and some don't, ETag/Last-Modified handling needs care
+- **Configurability** — when does a user want xmux on (latency-sensitive) vs off (quota-sensitive)?
+
+### Implementation timeline
+
+- 4-6 weeks of design + implementation
+- Tag @w0l4i, @2bemoji, @ipvsami, @dazzling-no-more, @euvel as core reviewers when design issue is filed
+
+The design issue should be filed after the v1.8.x batch settles (so the queue isn't too long).
+
+## v1.9.x and beyond (longer-horizon)
+
+These are committed to the project's roadmap but not actively in design. Listed for traceability when users ask "are you planning X?".
+
+- **Cloudflare WARP integration** (#309) — outbound traffic exits via Cloudflare WARP after Apps Script. Lets sites that flag Google IPs (most CF-protected) see traffic as Cloudflare-residential. Feasibility uncertain — needs CF account + WARP wireguard interface integration.
+- **TLS fingerprint randomization** (#369 §2) — randomize JA3/JA4 across deployments. Defeats CF / commercial bot detection.
+- **tunnel-node UPSTREAM_SOCKS5 chain** (#333 kanan-droid) — let tunnel-node forward through a SOCKS5 upstream (e.g., another VPN). Defense in depth + IP variety.
+- **Tier-3 i686-win7-windows-msvc target** (#411) — Windows 7 32-bit support via tier-3 target with `-Z build-std`. Needs nightly Rust. Roadmap v1.9.x or v2.x.
+- **Web frontend / dashboard** (#384) — landing page for the project. Low priority but recurring request.
+- **In-app changelog viewer** — show changelog for new version inside mhrv-rs UI when an update is available (small UX polish).
+
+## How to use this when triaging issues
+
+When a feature request comes in:
+
+1. Match the request to an existing item in this list. If found, reply: "Queued for v1.8.x [or whichever batch]. ETA ~X weeks. See [#NNN](#) for the canonical thread."
+2. If it's a duplicate of an existing roadmap item, close as duplicate of the canonical issue.
+3. If it's a new request not on this list:
+ - Substantive feature: add to v1.8.x or v1.9.x list as appropriate, note the issue number, reply with the planned bucket
+ - Long-horizon / uncertain: add to v1.9.x and beyond, reply that it's noted but no timeline
+ - Architectural impossibility (UrlFetchApp self-loop, MTProto, etc.): close with explanation, link to architectural reference
+
+## Roadmap velocity
+
+The project ships v1.x.y patches frequently — typically 1-3 per week during active development. Minor (1.x) bumps happen every few months. v1.0 → v1.8 took ~12 months. So:
+
+- "v1.8.x ETA" usually means "next 1-2 weeks" for small items, "next 1-2 months" for big items
+- "v1.9.0 ETA" usually means "next 2-3 months"
+- "v1.9.x" or "v2.x" means "no specific timeline, but committed to consider"
+
+Be honest with users about timelines. Iranian users especially appreciate knowing whether to wait or pursue alternatives.
diff --git a/docs/maintainer/references/workflow-conventions.md b/docs/maintainer/references/workflow-conventions.md
new file mode 100644
index 0000000..f3c3ba8
--- /dev/null
+++ b/docs/maintainer/references/workflow-conventions.md
@@ -0,0 +1,174 @@
+# Workflow conventions
+
+These are the writing conventions, formatting rules, and tone guidelines for everything that goes into the public repo or out to users. Internalize these — they're applied to every issue reply, every commit message, every changelog, every PR description.
+
+## The reply marker
+
+Every substantive issue or PR comment ends with this exact footer:
+
+```
+---
+[reply via Anthropic Claude | reviewed by @therealaleph]
+```
+
+That's a literal Markdown horizontal rule, then the `...` line. The `[reply via Anthropic Claude | reviewed by @therealaleph]` text is verbatim — same brackets, same pipe, same case, same `@therealaleph` mention.
+
+**Why this exists**: replies are drafted by Claude and reviewed by the maintainer before posting. The marker signals this to the user. Users in this community know this convention and rely on it.
+
+**Don't omit it**, don't translate "reviewed by" into Persian, don't paraphrase the format. The marker stays the same regardless of whether the rest of the reply is in Persian or English.
+
+**Where it doesn't go**: very short comments like "Dup of #423." or "Closing as resolved." or close-comments via `gh issue close --comment "..."`. The marker is for substantive replies. Trivial close comments don't need it.
+
+## Persian or English: match the user
+
+The repo's userbase is majority Persian-speaking. Writing in their language matters — both for clarity (technical context lands better) and for respect (assuming everyone wants English is wrong).
+
+**Match what the user wrote**:
+- User wrote in Persian → reply in Persian
+- User wrote in English → reply in English
+- User wrote a mix → match the dominant language; if it's roughly even, prefer Persian since most mixed-language Iranian users default to Persian for nuance and English for technical terms
+
+**Things that always stay in original Latin form**, regardless of reply language:
+- Code blocks (Rust, JSON, bash, JS — all stay as-is)
+- Command-line examples (`gh issue close N`, `cargo build`, `docker run ...`)
+- Technical identifiers: `AUTH_KEY`, `TUNNEL_AUTH_KEY`, `script_id`, `parallel_concurrency`, `disable_padding`, `tunnel_doh`, `bypass_doh_hosts`, `DIAGNOSTIC_MODE`, `passthrough_hosts`, `google_ip`, `mode: "full"` / `mode: "apps_script"`
+- Filename references: `Code.gs`, `CodeFull.gs`, `config.json`, `tunnel-node`, `mhrv-rs.exe`, `MhrvVpnService.kt`, `domain_fronter.rs`
+- URLs and links
+- The reply marker
+- Issue references like `#404`, `#313`
+- HTTP status codes (`502`, `504`, `403`)
+
+**Don't**:
+- Translate command names or function names
+- Mix Persian text into code blocks (unless user did so in their own paste)
+- Use machine-translation for the Persian — write it natively
+
+**Persian register**: write at "polite professional" level — `میفرمایید` over `میگی`, `لطفاً` (please), full pronouns when needed. Iranian Github users tend to write fairly formally; match that. Use Persian punctuation conventions: `،` (Persian comma), `؛` (Persian semicolon), `؟` (Persian question mark) — though comma in lists is acceptable as `،` or `,` per style preference.
+
+## Public artifact tone
+
+Anything that goes into the public repo — issue replies, PR comments, commit messages, PR descriptions, changelogs — is full prose, written warmly and clearly. Iranian users especially read carefully and brevity reads as cold or dismissive in this context. Use full sentences. Explain reasoning. Be patient.
+
+## Changelog format
+
+Every release has a file at `docs/changelog/vX.Y.Z.md`. The format is strict:
+
+```markdown
+
+• [bullet 1 in Persian, with markdown links to issue numbers]
+• [bullet 2 in Persian]
+• [bullet 3 in Persian]
+---
+• [same bullet 1 in English, written natively, not machine-translated]
+• [same bullet 2 in English]
+• [same bullet 3 in English]
+```
+
+Conventions:
+
+- **Use `•` (U+2022 bullet)**, not `-` or `*`. The Persian half uses bullets because Markdown unordered lists don't render naturally with Persian RTL text in the GitHub Releases page.
+- **Issue/PR links**: full GitHub URLs in markdown form: `[#404](https://github.com/therealaleph/MasterHttpRelayVPN-RUST/issues/404)`. Don't use bare `#404` in changelogs — they don't auto-link in the Persian section.
+- **Same content both halves** — they cover the same bullets, in the same order. Not necessarily verbatim translation; the Persian is written for Persian readers and may use slightly different framing.
+- **Length**: each bullet should describe what changed AND why it matters. "Added DoH bypass" is too thin; "DoH lookups now route around the Apps Script tunnel via plain TCP, saving the ~2s UrlFetchApp roundtrip per name without losing privacy (DoH is already encrypted)" is the right depth.
+- **Credit contributors**: if a PR landed from a community contributor, say so by name + handle. Persian: `از @euvel`. English: `by @euvel`.
+- **Backwards-incompatible changes**: rare for this project, but flag prominently if any. Add `**شکستگی سازگار**` / `**Breaking change**` prefix.
+
+The starter template is at `assets/changelog-template.md`.
+
+## Commit messages
+
+Format:
+
+```
+: vX.Y.Z —
+
+
+
+[optional: bullet list of specific changes]
+```
+
+Types in regular use:
+- `feat:` — new feature, user-visible (most common)
+- `fix:` — bug fix
+- `chore(releases):` — auto-fired CI commit refreshing prebuilt binaries
+- `chore:` — version bump, dep update, etc.
+- `docs:` — documentation-only changes
+- `ci(workflow-name):` — workflow file changes
+- `feat(area):` — feature scoped to a specific subsystem (e.g., `feat(code.gs):`, `feat(drive):`)
+
+Example commit message:
+
+```
+feat: v1.8.3 — sheet cache + DoH bypass + H1 keepalive + 431 + clearer errors
+
+Three substantive PRs from contributors landed for this release:
+
+- #443 by @euvel: optional spreadsheet-backed response cache in Code.gs.
+ Implements all 5 review suggestions from the design discussion (#400):
+ TTL-aware caching, 35 KB body-size gate, header rewriting on hit,
+ circular buffer for O(1) writes, Vary-aware compound keys.
+
+- #439 by @dazzling-no-more: bypass Apps Script tunnel for known DoH
+ endpoints on TCP/443. Cloudflare/Google/Quad9/AdGuard/NextDNS/OpenDNS/
+ ...
+```
+
+Conventions:
+- **Subject line under 75 chars** (GitHub truncates longer)
+- **Body wrapped at ~75-80 chars** for terminal-readability
+- **PR-merge commits**: when merging PRs via `gh pr merge --merge`, use `--subject` and `--body` to write the merge commit. Format is the same — type prefix, short summary, body explaining what shipped and credit.
+
+## Issue close reasons
+
+Always pass `--reason`:
+
+- `--reason completed` — the user's problem was resolved (their fix worked, or our fix shipped + they confirmed). For close comments, brief acknowledgement is fine; full marker not required.
+- `--reason "not planned"` — duplicate, architectural limit, won't-fix, or stale and unrecoverable. Always link to the canonical thread when closing as duplicate.
+
+For close comments, always include the destination issue if duplicate:
+
+```
+gh issue close N --reason "not planned" --comment "Closing as duplicate of #420 — full discussion + workarounds there."
+```
+
+## File names for reply markdown
+
+Convention: write reply markdown to a temp file (e.g., `/tmp/r--.md`) before posting via `gh issue comment N --body-file `.
+
+Examples:
+- `/tmp/r-404-quota.md` — reply to #404 about a quota observation
+- `/tmp/r-414-decoy.md` — reply to #414 about the decoy body
+- `/tmp/r-pr-merged.md` — generic "merged + included in vX.Y.Z" PR thank-you reply
+
+**Why use files instead of inline `--body`**: the inline `--body` argument runs through the shell, which interprets backticks (\`code\`) and `$()` substitutions. Issue replies frequently contain bash command examples with these patterns. The file approach sidesteps the quoting hell entirely. Use it by default.
+
+The exception is very short replies like `Dup of #423.` — those can use `--body "Dup of #423."` directly.
+
+## Tone
+
+- **Warm but technical**. Iranian users in particular often write apologetically ("sorry for using AI for the translation", "sorry to bother") — answer them as you'd want to be answered: with care, with technical depth, with explicit acknowledgment that their report is valuable.
+- **Don't promise fixes you can't deliver**. The Iran ISP throttle is not something the project can fix; saying "we're working on it" is OK, "we'll fix it next release" is not.
+- **Don't pretend certainty**. v1.8.1's over-confident "AUTH_KEY mismatch" message in the decoy detection cost trust with reporters who turned out to be hitting one of the other candidate causes. v1.8.2 + v1.8.3 are explicitly less assertive ("could be one of the following four/six causes...") because being honest about uncertainty is the better long-term move.
+- **Acknowledge community contributions liberally**. When a contributor's report shaped a roadmap item, say so by name. When a PR lands, thank them in the merge commit + PR comment + changelog. The project runs on goodwill.
+- **Don't apologize excessively** but do correct yourself when wrong. Iterating publicly through wrong hypotheses to a correct one is fine; doubling down on a wrong assertion is not.
+
+## Persian translation specifics
+
+When writing Persian replies:
+
+- **Half-spaces (ZWNJ — ``)** in compound words: `میخواهم` (not `میخواهم` or `می خواهم`), `نمیتوانم` (not `نمیتوانم`)
+- **Persian numerals**: optional but common in formal writing — `۲۰،۰۰۰` instead of `20,000`. Code/commands always Latin numerals.
+- **English technical terms in Persian text**: leave them in Latin script with surrounding Persian particles. Example: `از طریق Apps Script روی Google` (not transliterated)
+- **Quotation marks**: Persian uses `«...»` rather than `"..."` for prose. Code/commands use `"..."` regardless.
+- **The reply marker stays in English** as established. Don't translate `reviewed by` to Persian.
+
+## DOPR cycle structure
+
+When triaging a batch of issues/PRs, work through them in this order:
+
+1. **Read everything first** — list PRs, list recently-updated issues, scan headlines. Don't reply to issue 1 before knowing what issues 2-15 contain. Often there are clusters that should be addressed together (e.g., five users all hit the v1.8.0 decoy on the same day).
+2. **Triage by pattern** — match each issue to a pattern from `issue-patterns.md`. Issues that match a pattern get pattern-canonical replies (with specifics drawn from the user's actual log lines). Issues that don't match a pattern get individual attention.
+3. **Substantive PRs first** — if a PR has tests passing and looks mergeable, merge it. Then your subsequent issue replies can reference "shipped in vX.Y.Z" instead of "queued for next release".
+4. **Reply in batches but not as templates** — write each reply to address that user's specific log lines, config quirks, or terminology. Templated replies are easy to spot and erode trust.
+5. **Close cleanly** — if an issue was a duplicate, close at the end of your reply with the close-comment pointing to canonical thread. If it's awaiting user verification, leave open with last comment from you.
+6. **Cut releases when work lands** — don't accumulate fixes across multiple work sessions. Each session that lands user-visible code → one tag → one release.