Skip to content

feat(proxies): show how many egress IPs actually served a proxy pool - #13581

Merged
diegosouzapw merged 6 commits into
diegosouzapw:release/v3.8.51from
maxmad64bis:feat/pool-egress-observation
Sep 15, 2026
Merged

diegosouzapw merged 6 commits into
diegosouzapw:release/v3.8.51from
maxmad64bis:feat/pool-egress-observation

Conversation

@maxmad64bis

@maxmad64bis maxmad64bis commented Sep 13, 2026 •

Copy link
Copy Markdown
Contributor

Summary

A pool of 12 members reads as 12 exits, but several members can share one real exit, so a single refused exit can stall the whole pool at once. The proxy log already records which entry served each connection and which exit it left by, so the pool editor now shows one line under the member list: distinct exits used, connections through them, and the busiest exit over the last 24 h. When the read fails or is turned off, the line stays hidden and the pool screen works exactly as before.

Related Issues

Validation

  • Change type: UI / DB
  • Focused tests and category gates from the golden path — node 19/19 (# pass 19 # fail 0), vitest 6/6 (2 files), file-size / api-docs-refs / route-validation / typechecks green on the commit
  • npm run lint — ESLint on the touched files is clean (exit 0); the full npm run lint was not replayed, so this stays unchecked
  • Reconciled with the current active release base; focused checks rerun afterward — rebased onto the release head (238cb1b07, which now includes feat(proxies): stop re-serving a proxy that just failed #13578 and feat(proxy-logs): keep the HTTP status the provider actually returned #13580), 0 behind, single commit; the only conflict was config/quality/file-size-baseline.json, resolved by keeping both rebaseline keys
  • Production-code changes include a new or updated automated test in this PR

Tests Added Or Updated

  • tests/unit/proxy-pool-egress-observation.test.ts (new, 7 tests: 12 connections through 3 members across 5 exits reading 12 / 5 / 4, out-of-pool rows ignored, NULL egress/connection ignored, rows older than 24 h ignored, empty pool returning zeros, key alias sharing one cache entry with account, injected SQL failures returning null)
  • tests/unit/proxy-pool-egress-observation-route.test.ts (new, 5 tests: shape of the JSON keys, zeros contract, 400s on missing params, failure answering null with 200; run together with the pool route suite for 19/19)
  • tests/unit/ui/PoolEgressObservation.test.tsx (new, 5 tests: rendered line with plurals, empty-traffic sentence, null rendering nothing, network error rendering nothing; run with the registry render test for 6/6)

Coverage Notes

The query, the service (cache, kill-switch, failure-to-null), the route (auth tier, 400s, null), and the component (line, empty sentence, hidden states) are covered directly. The OpenAPI gates only check the path is present, so the new path entry is deliberately minimal and a documenting follow-up (response schema, 400, 401) is still owed. i18n-vi-completeness reports # pass 1 # fail 4, identical to the base before this change.

Reviewer Notes

  • The read is one query in src/lib/db/proxyLogs.ts:85, joining log rows to registry members through current assignments and grouping by observed exit IP; only counts leave the function, never addresses.
  • The route (src/app/api/settings/proxies/pool/egress-observation/route.ts:12) sits behind the same management auth as the pool route, validates scope/scopeId itself (route.ts:18), and the pool route is untouched, so a broken observation can never break the pool editor.
  • The service (src/lib/proxyPoolEgressObservation.ts:29) normalizes the scope exactly like the pool read, caches a result for 30 seconds (200 entries max), caches only successes, and turns every failure into null, which hides the line (PoolEgressObservation.tsx:40).
  • Exception to the default-off rule: this read is on by default because it's read-only with no effect on routing; PROXY_POOL_EGRESS_OBSERVATION=false (or 0, no, off) turns it off (proxyPoolEgressObservation.ts:24).
  • The query plan stays on existing indexes, no new index or column: SEARCH l USING INDEX idx_pl_timestamp (timestamp>?), SEARCH r USING INDEX idx_proxy_registry_host (host=?), SEARCH proxy_assignments USING COVERING INDEX sqlite_autoindex_proxy_assignments_1 (scope=? AND scope_id=?), USE TEMP B-TREE FOR GROUP BY. Full EXPLAIN QUERY PLAN output:
  • [{"detail": "SEARCH l USING INDEX idx_pl_timestamp (timestamp>?)"}, {"detail": "SEARCH r USING INDEX idx_proxy_registry_host (host=?)"}, {"detail": "LIST SUBQUERY 1"}, {"detail": "SEARCH proxy_assignments USING COVERING INDEX sqlite_autoindex_proxy_assignments_1 (scope=? AND scope_id=?)"}, {"detail": "CREATE BLOOM FILTER"}, {"detail": "USE TEMP B-TREE FOR GROUP BY"}, {"detail": "USE TEMP B-TREE FOR count(DISTINCT)"}]
  • Messages were added in en and vi only (src/i18n/messages/en.json:10967); the 49 other locales don't have these keys, like 9 keys already missing at the base, and the UI sync script was deliberately not run to avoid rewriting unrelated keys.
  • ProxyRegistryManager.tsx grows by exactly 2 lines (import at :11, mount under the members label at :1242) with its file-size cap raised 1475 → 1477 and a rebaseline note; everything else lives in new files under the cap.
  • Inherited reds, unchanged by this PR and citing none of its files: env-doc sync (six OMNIROUTE_* vars), docs-counts sync (provider counts), i18n-vi-completeness (# pass 1 # fail 4, same as base).
  • The baseline conflict with feat(proxy-logs): keep the HTTP status the provider actually returned #13580 is already resolved in this rebase: both _rebaseline_… keys and both caps are kept (ProxyRegistryManager.tsx 1477 here, proxyFetch.ts 1275 from feat(proxy-logs): keep the HTTP status the provider actually returned #13580).
  • fix(proxies): keep the stored status when a write does not send one #13577 edits ProxyRegistryManager.tsx far from the import and the mount added here and adds no net line, so the 1477 cap stays right in any merge order. feat(proxies): stop re-serving a proxy that just failed #13578 adds its env var to .env.example and ENVIRONMENT.md at a different spot.

A pool of N members reads as N exits, but several members can leave through the
same egress IP, and the per-IP 429 lockout then pauses every connection behind
it at once. The proxy log already records the entry point used (proxy_host,
proxy_port) and the egress IP observed, so the pool editor can show what really
happened over the last 24 h: distinct egress IPs, connections, and the most
connections seen behind one IP. Only numbers are returned.

The read has its own route, GET /api/settings/proxies/pool/egress-observation,
behind the same management auth as the pool route, which stays unchanged. The
scope is normalized like the pool read, a result is cached for 30 seconds, any
failure answers null and hides the line, and PROXY_POOL_EGRESS_OBSERVATION=false
turns it off.

ProxyRegistryManager.tsx grows by two lines (import and mount); its file-size
cap is raised with a note, everything else lives in new files under the cap.
@maxmad64bis
maxmad64bis force-pushed the feat/pool-egress-observation branch from d29efb2 to 8b962d1 Compare September 15, 2026 21:39
maxmad64bis and others added 5 commits September 15, 2026 18:45
diegosouzapw#13602)

Behind `PROXY_SKIP_RECENTLY_FAILED` (from diegosouzapw#13578): a provider 429 received through a pool member sets that member aside and a 2xx clears it, for opencode providers.

Maintainer rework before merge (kept the idea, no default behavior change):
- `noteProxyOutcome` ran inside the fire-and-forget `safeLogEvents` after awaited dynamic imports, so a concurrent request could still pick the member; it now runs first, synchronously, before any `await`.
- The duplicate `177_proxy_logs_upstream_status.sql` the stack still carried alongside the renamed 179 was removed; the regression test the PR body named exists as `pool-ip-quota-429-path.test.ts`.

Validated first on the combined board of all 38 PRs of this batch (10 merged as-is, 28 after the maintainer rework) on top of release/v3.8.51 c0f92ec: typecheck:core, check:open-sse-typecheck and check:dashboard-typecheck clean; ESLint clean on every changed file; file-size (rebaselined for the combined growth), complexity, cognitive-complexity, changelog-integrity, docs-counts, docs-sync, migration-numbering and i18n new-key gates green; 735 focused node:test cases with the only batch-caused failure (a flag-count assertion) fixed. Then re-validated alone on the fresh release tip right before this merge: ESLint on the changed files, typecheck:core, check:open-sse-typecheck, the file-size/complexity/changelog gates and this PR's own tests.

Thanks @maxmad64bis!
…efault off)

Registers the switch for the read-only pool egress observation of diegosouzapw#13581 as a
regular feature flag (network category, defaultValue "false"), so the new
dashboard line stays hidden unless an operator opts in. The environment
variable of the same name keeps working through the normal flag resolution
(DB override > env > default).

Adds the catalog row in FEATURE_FLAGS.md, the description i18n key (vi and
pt-BR translated, other locales deferred with the __MISSING__ marker) and
bumps the flag-count assertions from 55 to 56.

(cherry picked from commit 09459b31a84f68dbcc9486b4137f29e161df6871)
…te the route query

Keeps the read-only panel of diegosouzapw#13581 and fixes the review findings:

- readPoolEgressObservation() reads the PROXY_POOL_EGRESS_OBSERVATION feature
  flag (default off, fail closed) instead of a default-on env switch, so the
  line under a proxy pool only appears once an operator opts in; the route
  answers null while it is off.
- The route validates its query with a Zod schema: an unknown scope (or an
  over-long scopeId) is a 400 instead of being normalized to the global pool,
  and every error goes through errorResponse()/buildErrorBody() so no raw
  error message can reach the body.
- docs/openapi.yaml now documents the security tier, both query parameters,
  the nullable response object and the 400/401/500 responses.
- The three dashboard strings reach every locale (pt-BR translated, vi kept,
  the rest deferred with the __MISSING__ marker the i18n gates accept).
- Re-apply the ProxyRegistryManager.tsx file-size note, document the flag in
  .env.example and ENVIRONMENT.md with its real default, and cover flag
  default/off/on, the DB override and the new 400 paths in the tests.

(cherry picked from commit 5e9716f63925aa1ce9c28198d9b6483e96530449)
# Conflicts:
#	src/i18n/messages/ar.json
#	src/i18n/messages/az.json
#	src/i18n/messages/bg.json
#	src/i18n/messages/bn.json
#	src/i18n/messages/cs.json
#	src/i18n/messages/da.json
#	src/i18n/messages/de.json
#	src/i18n/messages/el.json
#	src/i18n/messages/en.json
#	src/i18n/messages/es.json
#	src/i18n/messages/et.json
#	src/i18n/messages/fa.json
#	src/i18n/messages/fi.json
#	src/i18n/messages/fr.json
#	src/i18n/messages/ga.json
#	src/i18n/messages/gu.json
#	src/i18n/messages/he.json
#	src/i18n/messages/hi.json
#	src/i18n/messages/hr.json
#	src/i18n/messages/hu.json
#	src/i18n/messages/id.json
#	src/i18n/messages/it.json
#	src/i18n/messages/ja.json
#	src/i18n/messages/km.json
#	src/i18n/messages/kn.json
#	src/i18n/messages/ko.json
#	src/i18n/messages/lt.json
#	src/i18n/messages/lv.json
#	src/i18n/messages/ml.json
#	src/i18n/messages/mr.json
#	src/i18n/messages/ms.json
#	src/i18n/messages/mt.json
#	src/i18n/messages/my.json
#	src/i18n/messages/ne.json
#	src/i18n/messages/nl.json
#	src/i18n/messages/no.json
#	src/i18n/messages/or.json
#	src/i18n/messages/pa.json
#	src/i18n/messages/phi.json
#	src/i18n/messages/pl.json
#	src/i18n/messages/pt-BR.json
#	src/i18n/messages/pt.json
#	src/i18n/messages/ro.json
#	src/i18n/messages/ru.json
#	src/i18n/messages/si.json
#	src/i18n/messages/sk.json
#	src/i18n/messages/sl.json
#	src/i18n/messages/sr.json
#	src/i18n/messages/sv.json
#	src/i18n/messages/sw.json
#	src/i18n/messages/ta.json
#	src/i18n/messages/te.json
#	src/i18n/messages/th.json
#	src/i18n/messages/tr.json
#	src/i18n/messages/uk-UA.json
#	src/i18n/messages/ur.json
#	src/i18n/messages/vi.json
#	src/i18n/messages/zh-CN.json
#	src/i18n/messages/zh-TW.json
@diegosouzapw
diegosouzapw merged commit ca312d5 into diegosouzapw:release/v3.8.51 Sep 15, 2026
0 of 3 checks passed
@diegosouzapw diegosouzapw mentioned this pull request Sep 21, 2026
abhisheksharma2411 added a commit to abhisheksharma2411/OmniRoute that referenced this pull request Sep 22, 2026
`check:file-size` failed: ProxyRegistryManager.tsx is frozen at 1477 and this
branch had grown it to 1493.

Most of that was avoidable. The limit's state, its `/api/settings` read and the
default now live in `useProxyBulkImportLimit` in proxyRegistryData.ts, beside
loadProxyUsage/loadProxyHealth, with the resolver still in
shared/constants/proxyBulkImport.ts. The component keeps one import member and
one call site, which is the mount point and is irreducible — the same shape the
diegosouzapw#13581 rebaseline records for this file.

That leaves 1477 -> 1479, rebaselined in config/quality/file-size-baseline.json
with the usual justification entry. The value goes in the `frozen` map; my first
attempt added it at the top level, where the checker never reads it and the gate
kept failing against the old number.

The hook also adds a cancellation flag the inline version did not have, so a
settings response arriving after unmount no longer sets state.

Behaviour is unchanged: 9/9 tests pass, tsc is clean on both files, and the 3
ESLint errors in the component reproduce identically on the base.
@maxmad64bis
maxmad64bis deleted the feat/pool-egress-observation branch September 24, 2026 21:11
muhamadgalihsaputra pushed a commit to niyatna/NiyatnaRoute that referenced this pull request Sep 27, 2026
…iegosouzapw#13581)

Behind the new `PROXY_POOL_EGRESS_OBSERVATION` flag (default off): a line under each proxy pool showing how many distinct egress IPs actually served it over 24h, backed by `GET /api/settings/proxies/pool/egress-observation`.

Maintainer rework before merge (kept the idea, no default behavior change):
- The route validates its query with Zod (unknown `scope` → 400 instead of silently `global`), error bodies go through `errorResponse()`, the OpenAPI entry documents security, parameters and responses, and the three UI strings exist in every locale.

Validated first on the combined board of all 38 PRs of this batch (10 merged as-is, 28 after the maintainer rework) on top of release/v3.8.51 d61b804: typecheck:core, check:open-sse-typecheck and check:dashboard-typecheck clean; ESLint clean on every changed file; file-size (rebaselined for the combined growth), complexity, cognitive-complexity, changelog-integrity, docs-counts, docs-sync, migration-numbering and i18n new-key gates green; 735 focused node:test cases with the only batch-caused failure (a flag-count assertion) fixed. Then re-validated alone on the fresh release tip right before this merge: ESLint on the changed files, typecheck:core, check:open-sse-typecheck, the file-size/complexity/changelog gates and this PR's own tests.

Thanks @maxmad64bis!
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants