feat(site-finder): deep-link tabs via ?tab= URL param #86
No reviewers
Labels
No labels
admin
analytics
auth
automation
bug
business
chore
ci
compliance
data
data-moat
docs
duplicate
dx
enhancement
Fable 5 ревью
feedback/max
generative
GG-форсайт
needs-discussion
needs-human
observability
pause-bots
performance
priority/p0
priority/p1
priority/p2
priority/p3
scope/backend
scope/db
scope/devops
scope/frontend
scope/qa
scrapers
security
site-finder
stage/1
stage/2
status/blocked
status/done
status/needs-analysis
status/needs-fix
status/qa
status/ready
status/review
status/wip
tech-debt
tradein
ux
week ревью 1
wontfix
вторичка
ИРД
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference: lekss361/gendesign#86
Loading…
Add table
Reference in a new issue
No description provided.
Delete branch "feat/site-finder-tab-urlparam"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Summary
Tab state на
/site-finderтеперь синхронизирован с URL — можно делиться deep-link на конкретный таб:/site-finder?tab=env— открыт таб "Окружение"/site-finder?tab=land— таб "Земля"/site-finder?tab=market— таб "Рынок"/site-finder(без param) — таб "Обзор" (default)Implementation
useRouter+useSearchParamsизnext/navigation(Next 15 app router, client-component)router.replace(неpush) — tab switches не засоряют browser historyuseEffectonsearchParams→ back/forward navigation работаютisTabId()type guard — только valid values из URL переключают state (invalid?tab=xxx→ default overview)Test plan
/site-finder?tab=market, ввести cad, нажать "Анализировать" — должен открыться таб "Рынок"?tab=env, история чистая/site-finder?tab=invalid_value→ fallback на overview без ошибкиnpx tsc --noEmit→ 0 errors (проверено)npm run lint→ 0 warnings (проверено)⚠️ Auto-review: minor (non-blocking) — 3 рекомендации по корректности и 1 опциональная.
Рекомендации (исправь до merge, но не блокирует):
router.replace("?", ...)fallback некорректен в Next 15 App Router — голый"?"оставляет dangling?в URL, не во всех браузерах чисто чистит query. ИспользоватьusePathname():Stale-closure guard в
useEffect—if (validTab !== tab)читаетtabиз stale closure (его нет в deps, eslint-disable это маскирует). На быстром back/forward UI может отстать на одну навигацию. Безопасный фикс: убрать guard вообще —setStateс тем же значением React сам бэйлит черезObject.is:Убрать
// eslint-disable-next-lineтоже.useSearchParams()в Next 15 App Router требует<Suspense>boundary в parent layout, иначе static rendering throw'нет. Проверь чтоfrontend/src/app/site-finder/layout.tsx(или родитель) обёрнут в<Suspense>. Если страница всегда dynamic — можно добавитьexport const dynamic = "force-dynamic"в page.tsx.Опционально:
initialTabIIFE считается на каждом render,useStateигнорирует после mount → wasted CPU. Перевести в lazy init:useState<TabId>(() => { ... }).Что хорошо:
isTabId(v)валидирует против closed enum — нет open-redirect / XSS surface.useEffectдля URL↔state sync — это не data-fetching, конвенция CLAUDE.md не нарушена.Все 3 рекомендации + опциональная применены в
6aa8222:router.replaceтеперь используетusePathname()— нет dangling?в Next 15.setTabState(...)без if, React сам бэйлит черезObject.is.eslint-disableудалён.<Suspense fallback={null}>—useSearchParams()теперь безопасен для static rendering. Сама логика вынесена вSiteFinderContent, верхний default export — Suspense wrapper.useState<TabId>(() => { ... })— lazy init, нет wasted CPU на re-render.npx tsc --noEmit+npm run lint --max-warnings 0— clean.PR в Auto-merge scope ✅ (
frontend/src/app/**UI-only). Если bot вернётverdict=approveс валидным SHA marker на6aa8222— заmerge'у автоматом per CLAUDE.md rule 6.✅ Auto-review passed (LGTM) — все 3 must-fix исправления выполнены корректно, optional lazy init тоже сделан.
Проверено:
router.replace(qs ? \${pathname}?${qs}` : pathname, { scroll: false })— dangling?` фикснут.useEffectбез guard и без eslint-disable; deps[searchParams], setState с тем же значением React сам бэйлит черезObject.is— no loop.<Suspense fallback={null}>обёртываетSiteFinderContent; split на content/shell чистый, page-level state весь в content.useState<TabId>(() => ...)— lazy init, считаем initialTab один раз на mount.isTabId— proper type guard,as readonly string[]— narrowest safe cast).Object.is), no flicker.Можно мержить (
gh pr merge --squash --delete-branch).