Fix (herkes, flag'siz): evrensel post-auth segment kapısı (segment-gate.tsx).
Kayıtların ~%41'i segmentsizdi çünkü login.tsx Google OAuth segment adımını
atlıyordu. Yeni kullanıcıya zorunlu (açığı kapatır), mevcut segmentsize
7g-cooldown'lu yumuşak prompt (~538 backfill). Segment localStorage +
PostHog person prop (b2b_segment); backend persist faz 2.
Kova B (funnel-bucket flag, b2b_qualified): trial-value-upsell'e segmente-özel
Meta-kanıtlı kopya (iade / yanlış-parça / sınırsız-şase). Flag SADECE banner
görünürken okunur → deney maruziyeti = gerçekten gören aktif trial'lar.
vehicle_owner / bilinmeyen segment → nötr control kopya (ürün kararı).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Per-dakika rate cap'i (pcat 90/dk) burst'ü sınırlıyor ama günlük TOPLAM'ı değil —
bir kaynak saatlerce tavanda çalışıp proxy bütçesini boşaltabiliyor (2026-07-09:
backlog drain pcat'i ~74k çağrı / ~10 GB'a çıkardı). SOURCE_DAILY_MAX eklendi:
UTC-gün fixed-window Redis sayacı; kaynak günlük bütçeyi aşınca backfill job'ları
pencere dönene kadar ertelenir. Kullanıcı decode'ları etkilenmez (worker'dan
geçmiyor). Dakikalık gate'ten SONRA sayılır → rate-limitli retry'lar şişirmez.
Default (env-tunable): pcat 30k, emex 20k, pl24 15k/gün. Backlog drain'i
hızlandırmak için yükselt, bütçeyi daha çok korumak için düşür.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Floxy kalıcı olarak devre dışı (bakiye/tünel ölü — pcat+emex loglarında sürekli
"Floxy unhealthy → DataImpulse failover"). Floxy-primary her çağrıda önce bir
başarısız deneme yakıp sonra failover ediyordu. DataImpulse zaten kanıtlanmış
çalışıyor (call-leg %97,6).
PCAT_PROXY_PROVIDER ve EMEX_PROXY_PROVIDER default'ları floxy→dataimpulse
(pcat auth + emex browser + emex HTTP ayağı). Floxy hâlâ env ile seçilebilir
(PCAT_PROXY_PROVIDER=floxy / EMEX_PROXY_PROVIDER=floxy); failover makinesi
dormant kalıyor.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Her JWT capture yeni context (boş cache) açıp aynı widget JS/CSS'ini residential
proxy'den yeniden indiriyordu — ölçüldü ~2,2 MB/capture, %93'ü static (en büyüğü
tüm sitelerde ORTAK `.../v3/bundle_<hash>.js`, 603 KB). ~3-4k capture/gün ile bu
en büyük kaçınılabilir proxy maliyeti (~20 GB/15g), sıfır ban riski.
URL-anahtarlı, disk-destekli (hot in-memory + best-effort disk) cache eklendi.
Sadece versiyonlu static JS/CSS cache'lenir (hash/`?_=` → invalidation otomatik);
token XHR (/v3/api/proxy/*) ve HTML document HER ZAMAN canlı geçer. HIT'te sıfır
proxy byte'ı; MISS'te bir kez route.fetch (context proxy'sinden) + sakla + servis.
PCAT_ASSET_CACHE=false ile redeploysuz kapatılabilir (kill switch).
Doğrulama (autotrade.md + e-trak.ru, gerçek yükleme, 2 ardışık capture):
warm capture canlı trafiği %89-91 düştü VE token XHR ikisinde de atıldı — yani
cache'ten JS servisi widget'ı bozmuyor (geçen capture-blocking olayının tersi).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
A blank header region means the Dialogys vehicle result never rendered (transient/
slow), NOT a genuine miss — a decodable Renault (Megane) read "" here and was wrongly
returned not_found (made definitive by the Rpartstore-down gate). Return ambiguous so
the caller's cheap in-session reset retries; a real empty-form miss just exhausts the
bounded retries. A NON-empty unparseable header (old R19's garbled parts screen) stays
a fast definitive not_found.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
So the flag reaches the container (Coolify only injects compose-referenced ${VAR}).
Default true = unchanged; set false during a Rpartstore outage.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Rpartstore is DOWN upstream. Every Renault decode still tried to OPEN it
first → launch-error, stuck "Loading application..." app (dirty-resume for
the next decode), ~30-50s burned, and the in-memory down-cooldown resets on
every worker restart so the first decode after each restart repaid the full
cost. That overhead pushed decodes over VINPIN_DECODE_BUDGET_MS → budget
abort → sessionPoisoned → cascade.
Add a persistent kill-switch: VINPIN_RPARTSTORE_ENABLED=false makes BOTH
Renault paths (warm warmRenaultDecode + cold runRenaultFlow) skip opening
Rpartstore entirely and route straight to Dialogys — mirroring the existing
rpartstoreInCooldown() skip but surviving worker restarts. Flag-disabled is
treated as "Rpartstore unavailable" exactly like cooldown, so primaryRan
stays false and a clean Dialogys not_found is definitive (no retry thrash).
Also gated the _warmUp Rpartstore-open so a future warm session with the
flag off pays no launch cost / leaves no stray app.
DEFAULT true (only the exact string "false" disables) → behaviour with the
flag unset is completely unchanged. tsc clean; vinpin unit tests green
(+3 flag tests: warm/cold skip + Dialogys-definitive, and default-true still
attempts Rpartstore).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The failing test expected normalizeName("ANNA-MARIA")=="Anna-Maria" (dotted i),
but that is internally inconsistent with the suite's own Turkish tests that REQUIRE
the deliberate tr-TR locale: "ALİ YILMAZ"→"Ali Yılmaz" (dotless ı) and "ÇAĞRI"→
"Çağrı". Uppercase Latin "I" (U+0049) is the SAME codepoint as Turkish dotless-I,
so tr-TR lowercases it to "ı" — correct for a TR product (invariant casing would
break every Turkish name: Yilmaz/Çağri, and mangle İ→i̇ with a combining dot). Not
a code bug; expectation corrected to "Anna-Marıa" with an explanatory comment.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
FIX A made a CLEAN Dialogys not_found definitive on its own while Rpartstore is
down (no retry). But runDialogysSearch inferred not_found from a SINGLE OCR
sample taken after a blind fixed wait — a slow render or a transient OCR glitch
on a DECODABLE Renault read empty at t=afterDialogysSubmit and was mislabelled
not_found, and under FIX A that miss is now terminal (decodeRenaultLocked
returns null, no cheap retry).
Replace the blind wait + single sample with a poll (pollForState) over the
header region for a decodable render, capped at afterDialogysSubmit — mirrors
runRpartstore's poll-then-decide shape. Multi-samples across the same window and
returns early on a hit, so a slow render/OCR glitch no longer produces a false
not_found. Cap unchanged, so a genuine miss consumes no more time than before:
FIX A's no-budget-burn / no-seat-poison guarantee and the definitive-not_found
semantics both hold, and the full-frame fallback is preserved. Working Renault
decodes only get faster (early return). tsc clean; vinpin (119) + extractModelYear
(12) green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
FIX A (runRenaultFlow): when Rpartstore is UNAVAILABLE (down-cooldown or it
never loaded, so `primary` is only a placeholder ambiguous), a CLEAN Dialogys
not_found is now DEFINITIVE. The old gate required BOTH catalogs to say
not_found, so every undecodable Renault while Rpartstore was down got
downgraded to ambiguous → 3x retry → 180s budget → sessionPoisoned, which
then degraded later decodes. A genuinely-ambiguous Dialogys (unreachable)
still retries. When Rpartstore actually ran, both-must-agree is preserved.
runDialogysSearch now logs its outcome + truncated OCR header so this class
is diagnosable from prod logs.
FIX B (vinpin.constants): add old R-number + TR-badge Renault model tokens
(R5/R9/R11/R12/R19/R21/R25, Europa/Broadway/Toros/Flash). R-prefixed form
only — no bare numerics that could false-match year/engine digits.
FIX C (vin-validator extractModelYear): the position-10 year code repeats every
30 years ("T" = 1996 or 2026) with no clean VIN-only rule. New optional
{modelResolved:false} signal: for a brand-only decode of an old-shaped Renault
VIN (Renault WMI + numeric-led VDS type code) whose code pins to the current
cycle's leading edge, roll back one 30-year cycle so a ~1996 R19 isn't labelled
2026. Narrow: model-resolved or modern-shaped VINs are unchanged. Corgi's
WMI-only decoder wired to pass modelResolved:false.
Keeps never-throw / VINPIN_DECODE_BUDGET_MS / sessionPoisoned semantics and the
Fiat + working Renault paths intact. tsc clean; vinpin + corgi + extractModelYear
tests green (new tests cover A and C).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The last gap: when ensureWarmWindow('fiat') can't bring the Fiat ePER to its VIN
panel (partial Rpartstore-down session leaves it off-panel + re-nav can't recover),
warmDecode threw SessionDropped → re-warm → null, turning a DECODABLE Fiat VIN into
a not_found. Fall back to the proven cold Fiat path instead, so a warm-window fault
never loses a real decode. Completes the FIX2 cold-fallback (previously only the
unusable-parse branch had it; now the can't-reach-panel branch does too).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The full 3-window gate never warmed during a prolonged Rpartstore outage, so the
daemon re-attempted warm-up every backoff cycle — each ~2min attempt holds the
single-seat mutex and STARVES real decodes. Anchor warm on Fiat ePER + Dialogys
(the two windows that serve both brands; the Rpartstore-down cooldown routes
Renault to Dialogys anyway, so Rpartstore is an optional bonus). Warm is then
claimed once and HELD (no re-warm loop → no starvation); the Поиск-token foreground
fix makes warm Fiat raise the correct window. Rpartstore rejoins on recovery.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
A partial warm session (e.g. Rpartstore DOWN → only Fiat+Dialogys) is proven
unstable: re-warm cycles + the missing catalog's launch-error keep knocking the
Fiat window off its VIN panel, so warm Fiat decodes thrash to the 180s budget →
not_found. Require all three windows before this.warm=true; otherwise stay on the
reliable cold path (which decodes Fiat/Renault + cross-brand cleanly). The daemon
backs off + retries, so warm auto-resumes once Rpartstore recovers.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Review finding: warmDecode's genuine-not-found decision used VINPIN_OCR.notFound
(/…|Catalogue/i) against the FULL frame, where the 'Spare Parts Catalogue' header
always matches → every on-panel garble was flagged a genuine miss and null'd out
instead of falling back to the cold retry. Add notFoundStrict (no Catalogue token)
for the full-frame check so a transient on-panel garble (VIN exists) recovers via
the cold path; keep notFound for the runVinFlow modal-region settle poll.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
When the warm daemon is up and Rpartstore is DOWN, a partial-warm session
(Fiat opened, Rpartstore launch-error, Dialogys opened last → foreground)
made a Fiat warm decode fail silently: the Fiat foreground regex shared the
`Поиск` token with the Dialogys "ПОИСК" button, so raiseWarmWindow reported
success without raising Fiat and the VIN was typed into Dialogys → garbage;
warmDecode's Fiat branch then did a bare `return null` (no log, no fallback).
- FIX 1: drop the ambiguous `Поиск` from VINPIN_WINDOW_FOREGROUND.fiat; keep
Fiat-only chrome (Dealer/ePER) + VIN-panel model tokens.
- FIX 2: warmDecode Fiat unusable-parse no longer returns a silent null —
OCR the frame; on-panel + genuine not-found → null (real miss), otherwise
warn (cold-path parity) and fall back to the proven cold decodeFiatLocked.
- FIX 3: ensureWarmWindow panel-verifies a raised Fiat window (catalogueReady);
if up but off the VIN panel, re-navigate via establishSession (bounded/never-throw).
- FIX 4: _warmUp records per-window availability (warmWindows) so a Fiat VIN
routes straight to cold when no Fiat window opened; and dismisses a leftover
Rpartstore launch-error modal before opening Dialogys so it can't dirty the
desktop / drive the wrong-window state.
Adds ocrFrame() test seam + 4 unit tests. tsc clean; 114 vinpin tests green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
An AMBIGUOUS Renault outcome is transient/state-dependent, but decodeRenaultLocked
retried it with a full browser teardown (this.close()). The next attempt then
re-launched chromium + re-did the web login + re-established from scratch
(~60-90s each), and each re-establish re-hit the seat's dirty-resume ("catalog
window resumed open" -> closeStrayRunningApps), so 3 attempts blew the 180s budget
-> not_found. Proven live: VF1RFE00653633190 decoded cleanly to KADJAR earlier
when the desktop state was favorable, then thrashed to not_found on relaunch.
Fix: on the ambiguous path, reset to a clean VinPower brand grid on the SAME live
session via ensureBrandGrid (closeStrayRunningApps DOM recovery first, canvas
tab-X fallback) instead of tearing the browser down. Keep the browser + authed so
the next iteration's ensureAuthenticated is a no-op (no relaunch, no web login),
and re-run runRenaultFlow from the clean grid (~30-40s). Graduated safety: if the
cheap reset can't confirm a clean grid or the session is broken (page
closed/disconnected), fall back to the old close() + cold re-establish. The reset
runs under the wall-clock deadline so an overrun still routes to the existing
VinpinBudgetError teardown+poison path. never-throw + budget/poison paths
unchanged; maxAttempts semantics unchanged.
Tests: +3 (ambiguous -> in-session ensureBrandGrid reset re-runs runRenaultFlow
with NO close(); graduated fallback close()s when the reset can't reach a grid;
broken session skips straight to close()). 110 vinpin tests green; tsc clean.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Review finding: closeStrayRunningApps classified an unreadable/empty VinPower
running-app row as a stray and closed it (`/VinPower/i.test('')` is false),
needlessly killing+relaunching a healthy VinPower on a transient name-read miss.
Only close rows POSITIVELY identified as non-VinPower (non-empty name that fails
the VinPower match); treat unreadable names as keep.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
On login the RDS seat resumes DIRTY (e.g. a Renault Rpartstore launch-error
modal + its taskbar window over the brand grid). The old recovery failed: the
canvas tab-✕ (93,45) only hits a TABBED catalog window's ✕, which a resumed
stray doesn't have, so 6 tries did nothing and escalated to logout+relogin —
counterproductive, since the seat publishes apps-only and the RDS session ends
only on server-side idle timeout, so a Connection-Server logout+relogin PROVABLY
resumes the same dirty window.
New state-agnostic recovery `closeStrayRunningApps`: reveal the Horizon sidebar
(#sidebar-toggler), enumerate ul.running-app rows, and terminate every app whose
name != VinPower via its per-app ✕ (li.icon-close-app-image) — real DOM outside
the Blast canvas, so it closes a window regardless of its modal/spinner/loading
state. Collapse the sidebar, OCR-confirm the brand grid; relaunch VinPower via
#available-VINPIN (or the vinpinApp canvas coord) if the app itself was gone.
Wired as the PRIMARY recovery in ensureBrandGrid — both the resumed-catalog
branch (before the canvas tab-✕ fallback) and the end-of-loop escalation, which
NO LONGER calls logout+relogin (method retired). Every DOM op is guarded
(try/catch + presence check) so a missing selector / canvas-only render degrades
gracefully to the existing dismissBlockingModal + tab-✕ / OCR path instead of
throwing. Bounded loop; the launch-error modal dismissal (OK 868,530 / Escape)
is kept as a fast pre-step and fallback.
Preserves the never-throw contract, 180s budget/poison, spinner-guard, acquire
cap, launch-error cooldown, ensureRpartstore fast-bail, and the Fiat ePER path.
tsc clean; 107 vinpin tests green (adds closeStrayRunningApps close/degrade tests
and the ensureBrandGrid-uses-closeStrayRunningApps escalation tests).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
When Rpartstore is DOWN, its hard launch-error modal ("Ошибка запуска каталога",
title "Renault Rpartstore") renders OVER the brand grid. The grid tiles stay
OCR-visible behind the small centered modal, so the full-frame read matches both
`brandGrid` and `renaultSubmenu` — making ensureRpartstore's flyout branch fire on
EVERY iteration, re-clicking renaultRpartstore(584,779) + langOk + a 12s full-frame
poll for all ~6 iterations (~72s) before finally returning false. That wastes ~60s on
the first cold DOWN decode AND repeatedly actuates coordinates on a wedged desktop.
Detect the launch-error modal (upscaled crop via the existing
`rpartstoreLaunchErrorPresent`) at the top of the per-iteration loop, BEFORE the
flyout branch: if present, return false on iteration 1 so the acquire loop's
`!present` path dismisses it, sets the down-cooldown, and routes straight to Dialogys.
Depends on the crop OCR being legible (ffmpeg upscale, added in 281c54a); when
illegible it's false and behaviour is exactly as before.
Adds two robustness tests: (1) modal-over-grid → false on the first iteration with no
flyout re-clicks; (2) illegible crop → flyout path still runs (unchanged).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Two review follow-ups to 281c54a:
1. born-stuck cooldown regression: acquireLoadedRpartstoreInner set the 10m
down-cooldown when maxOpens was exhausted by a born-stuck-spinner streak — a
TRANSIENT, reopen-recoverable blip, not a server outage. Because the cooldown
can only self-clear from INSIDE the acquire loop (skipped while cooling down),
one spinner streak suppressed the richer Rpartstore catalog for every Renault
decode for 10m. Reserve the cooldown for the confirmed launch-error DOWN signal
(unchanged at the two launch-error sites); the born-stuck give-up now just falls
back to Dialogys for that one VIN and retries Rpartstore fresh next VIN.
2. clean-logout false disconnect-recovery: cleanTeardown's disconnect recovery
gated on VINPIN_OCR.sessionDropped, whose broad "HTML Access" token also matches
the clean-logout Horizon HTML-Access launcher. A clean log-off could then click
disconnectedClose + RELAUNCH VinPower right before close(), leaving the exact
dirty resumed session the teardown prevents (+~17s wasted). Veto the recovery
with !VINPIN_OCR.launcher so it fires only on a real Disconnected drop.
Keeps never-throw, budget, spinner-guard, Fiat/Dialogys fallbacks intact. Adds a
born-stuck-no-cooldown test and a clean-logout-launcher-no-relaunch test.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Root cause: ffmpeg is absent in the prod worker, so vinpin.ocr cropScale silently
returns the full 1600x900 frame — the tiny centered Rpartstore launch-error modal
is illegible, so a DOWN Rpartstore is misread as a spinner and thrashes the seat.
- Dockerfile: install ffmpeg so cropScale actually crops+3x-upscales (restores all
clipped OCR: modal-crop error detect, Fiat modal, Renault header).
- ensureRpartstore: gate the "already open" short-circuit on a CONTENT token
(rpartstoreLoaded), not rpartstoreOpen which false-matches the flyout/title word
"Rpartstore"; click the flyout entry when the submenu is up over the grid; keep a
late open-window branch so a spinner/vehicle-page window still counts as present.
- closeRpartstoreTab: never click windowClose(1298,14) (it hits the language
selector and wedges the grid); gate the retry on a content token; Escape after.
- cleanTeardown: dismiss any blocking modal BEFORE "Çıkış yap" so logout is a clean
RDS log-off (not a dirty channel disconnect); recover a Disconnected dialog via
disconnectedClose(953,505) + relaunch VINPIN app for a fresh grid.
- Rpartstore-down cooldown (10m): a launch-error / repeated load-failure routes
Renault decodes straight to Dialogys (skip reopening a down catalog); a confirmed
load clears it.
- ensureBrandGrid: dismiss a wedging launch-error modal + short-retry instead of
burning the 84s dead-wait.
- parseRenaultHeader: ignore the status-bar license-expiry date when reading the
model year; sessionAlive matches RDST01/RDST02 (seat load-balances).
- Keeps never-throw, VINPIN_DECODE_BUDGET_MS, sessionPoisoned, the acquire cap,
spinner-guard, relogin-cap and the Fiat/Dialogys fallbacks intact.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The Renault decode fell to not_found when Rpartstore is DOWN (hard
launch-error modal) even though Dialogys should take over. Root cause:
- acquireLoadedRpartstore's budget-exhausted / never-loaded return-false
paths left the centered launch-error modal on screen (only the flaky
OCR error-detect branch dismissed it). The modal then blocked the
Dialogys grid-return.
- returnToBrandGrid's tab-✕ can't close a centered dialog, so every
"not on brand grid (try N/4)" wedged and re-entered ensureBrandGrid,
which escalated to logoutAndRelogin → close()+launch() ("browser
disconnected — will relaunch") on EVERY iteration, thrashing on a stale
page ref until the 180s budget → not_found.
Fixes (conservative, all safety nets intact):
1. Wrap acquireLoadedRpartstore so EVERY false return runs a best-effort
defensive dismiss (Escape → click launch-error OK 868,530 → Escape),
unconditional of the OCR read. Harmless when no modal is up.
2. returnToBrandGrid + ensureBrandGrid dismiss a possible centered modal
before the tab-✕ close so a leftover dialog can't wedge the loop.
3. Cap the logout+relogin escalation to ONE attempt per decode/warm-up
(reloginUsedThisDecode) — a capped exhaustion poisons the seat for a
clean cold restart instead of looping close()+launch() until budget.
Keeps the OCR fast-path branch, maxOpens/acquireBudgetMs=14s, fcc0298,
61b5769, Fiat path, never-throw/budget/poison all intact.
Tests: +3 (budget-exhausted defensive dismiss; grid-return modal-clear
before tab-✕; relogin capped to one attempt) — 88 vinpin tests green,
tsc + biome clean. Needs prod validation.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Rpartstore's launch-error modal OCR-detection is unreliable across renderings,
so a DOWN Rpartstore was grinding all 3 reopens (~54s) + dirtying the seat →
Dialogys fallback couldn't finish inside the decode budget → not_found.
Lower acquireBudgetMs 90s→14s so the loop bails after the first open+poll (~22s)
straight to Dialogys on a still-clean seat. Healthy Rpartstore loads on the first
open and is used as before; maxOpens kept so tests still exercise the reopen path.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The prod Rpartstore→Dialogys bail regressed: a DOWN Rpartstore was still
misclassified as a "born-stuck spinner", burning 3 reopens (~54s) and pushing
the decode past the 180s budget → not_found. The rpartstoreLaunchError regex
never matched because acquireLoadedRpartstore's load-poll OCR'd the FULL
1600x900 frame at 1x — at which the small centered Cyrillic modal ("Ошибка
запуска каталога") is illegible to tesseract (it returns the surrounding
brand-grid tiles and drops the modal text). The regex text was actually fine;
the modal was never fed to it.
Root cause (verified live 2026-07-15, seat trvinpin47828): wrong OCR
resolution/region, not wrong regex.
Fix:
- New VINPIN_RPARTSTORE_ERROR_REGION (centered modal crop); OCR it UPSCALED (3x)
so "Ошибка запуска каталога" reads as "Owwu6ka 3anycka KaTanora" and matches.
- pollRpartstoreState(): each poll reads LOADED off the full frame (large Latin
text, unchanged) AND the launch-error off the upscaled modal crop → bail on the
FIRST open, no wasted reopens. Genuine-spinner reopen path preserved.
- Also catch the launch error when ensureRpartstore/raiseWarmWindow can't confirm
a window (modal is over the grid, no catalog chrome) → dismiss + bail.
- rpartstoreLaunchError regex: add the verbatim live transliterations
(Owwu6ka/OwwbKa); 3anycka+KaTanora remain the stable anchors.
Live verification (seat trvinpin47828, exclusive night access):
- Rpartstore is DOWN server-side (hard launch error, NOT a spinner).
- New detection returns launchError on open 1 → bail, no reopens.
- Dialogys fallback decoded VF1RFE00653633190 → RENAULT Kadjar (HFE) in 52.2s
(well under the 180s budget).
Budget/poison/livelock/Fiat paths untouched. 85 vinpin unit tests green;
typecheck + biome clean.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Rpartstore currently throws an immediate hard launch-error modal ("Ошибка
запуска каталога" / title "Renault Rpartstore") within ~8-10s of every open —
a server-side/entitlement failure that reopening never fixes. The old code
misclassified it: the error dialog's title matched rpartstoreOpen so
ensureRpartstore returned true, but it lacked the rpartstoreLoaded content
markers, so acquireLoadedRpartstore judged it a born-stuck spinner and burned
all 3 reopens (~54s) before falling back to Dialogys — which then ran out of
the 150s decode budget → not_found. Dialogys itself decodes correctly (~25s).
Fix (detect-and-bail on the FIRST open, no wasted reopens):
- constants: add VINPIN_COORDS.rpartstoreLaunchErrorOk (868,530) + the
VINPIN_OCR.rpartstoreLaunchError pattern (matches the real Cyrillic AND its
stable eng-OCR transliteration "Owwnbka 3anycka KaTanora").
- acquireLoadedRpartstore step 2: poll now stops on loaded OR launch-error and
classifies via the returned OCR text; a launch error dismisses the modal and
returns false immediately → straight to Dialogys (reopen loop untouched for
genuine spinners).
- ensureRpartstore: submenu-open poll also stops fast on the launch error
instead of dead-waiting afterRenaultCatalogOpen.
- bump VINPIN_DECODE_BUDGET_MS 150s→180s (cheap safety margin).
- tests: launch-error → false after ONE open (no reopens, dismiss clicked);
genuine spinner still reopens; OCR-pattern matches Cyrillic + transliteration.
Never-throw contract, sessionPoisoned, budget teardown, cold/Dialogys
fallbacks, the fcc0298 spinner-guard, the 61b5769 establish/teardown fix and
the Fiat ePER path are all intact. Skipped the warm-path Dialogys field-clear
tweak — runDialogysSearch is shared with the cold path and switching its clear
step would change cold behavior.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>