fix(tradein/v2): audit #2081 residual — 404 short-circuit, disabled-tab tooltips, stale hex token, ДКП decode #2392

Merged
lekss361 merged 1 commit from fix/tradein-2081-audit-residual into main 2026-07-04 07:47:57 +00:00
3 changed files with 50 additions and 11 deletions

View file

@ -439,9 +439,24 @@ export default function TradeInV2Page() {
// Skip the restore fetch once we already hold a fresh result.
const restored = useEstimate(freshResult === null ? urlId : null);
// L3 — once the restore-by-id fetch has confirmed the estimate does not
// exist (404), `currentEstimateId` below drops to null so the sibling
// dashboard hooks (analytics/location-coef/placement-history/sell-time,
// all `enabled: estimate_id !== null`) don't each fire their own doomed
// request against the same dead id. Hoisted above the sub-hooks (was
// computed further down, after they'd already fired on the stale id).
const restoreActive = urlId !== null && freshResult === null;
const restoreError = restoreActive && restored.isError;
const restoreNotFound =
restoreError &&
restored.error instanceof HTTPError &&
restored.error.status === 404;
const estimate: AggregatedEstimate | null =
freshResult ?? restored.data ?? null;
const currentEstimateId = freshResult?.estimate_id ?? urlId;
const currentEstimateId = restoreNotFound
? null
: (freshResult?.estimate_id ?? urlId);
// Per-user side data, independent of the current estimate. Both fail-soft
// (retry:false): history feeds the «Предыдущие оценки» overlay (04), quota
@ -486,13 +501,7 @@ export default function TradeInV2Page() {
const sellTimeData = sellTime.data ?? null;
// ── Derived state flags ───────────────────────────────────────────────────
const restoreActive = urlId !== null && freshResult === null;
const restoreLoading = restoreActive && restored.isPending;
const restoreError = restoreActive && restored.isError;
const restoreNotFound =
restoreError &&
restored.error instanceof HTTPError &&
restored.error.status === 404;
const loading = mutation.isPending || restoreLoading;
const insufficient =

View file

@ -175,6 +175,19 @@ export default function HistoryView({
>
{data.dkpHeader.title}
</div>
{/* M12 расшифровка «ДКП» один раз на секцию, тем же lowercase
hint-стилем что ObjectSummary.rowHint() (fontSize 8/letterSpacing
0.2/muted3), а не uppercase ResultPanel §M8 стилем. */}
<div
style={{
fontSize: "8px",
letterSpacing: 0.2,
color: tokens.muted3,
marginTop: 2,
}}
>
ДКП · договор купли-продажи
</div>
</div>
<div
style={{

View file

@ -156,7 +156,9 @@ export default function TopNav({
fontSize="27"
fontWeight="400"
letterSpacing="11"
fill="#6f8195"
// L7 — stale literal from before the C1 contrast sweep darkened
// the muted scale; token keeps this logo subtitle in sync.
fill={tokens.muted}
>
ОЦЕНКА НЕДВИЖИМОСТИ
</text>
@ -370,7 +372,14 @@ export default function TopNav({
</div>
</div>
<div style={menuItemDisabledStyle} aria-disabled="true">
{/* L5 `title` gives the dimmed/non-interactive item a hover
tooltip explaining WHY it's disabled, instead of a silent dead
row (mirrored on Настройки/Помощь below). */}
<div
style={menuItemDisabledStyle}
aria-disabled="true"
title="Раздел «Профиль» скоро появится"
>
<svg width="15" height="15" viewBox="0 0 15 15" fill="none" aria-hidden="true">
<circle
cx="7.5"
@ -412,7 +421,11 @@ export default function TopNav({
</span>
</div>
<div style={menuItemDisabledStyle} aria-disabled="true">
<div
style={menuItemDisabledStyle}
aria-disabled="true"
title="Раздел «Настройки» скоро появится"
>
<svg width="15" height="15" viewBox="0 0 15 15" fill="none" aria-hidden="true">
<circle
cx="7.5"
@ -430,7 +443,11 @@ export default function TopNav({
Настройки
</div>
<div style={menuItemDisabledStyle} aria-disabled="true">
<div
style={menuItemDisabledStyle}
aria-disabled="true"
title="Раздел «Помощь» скоро появится"
>
<svg width="15" height="15" viewBox="0 0 15 15" fill="none" aria-hidden="true">
<circle
cx="7.5"