fix(frontend): satisfy phase 2 lint checks
All checks were successful
Mirror to GitHub / mirror (push) Successful in 5s
CI / frontend (pull_request) Successful in 7m18s
CI / backend (pull_request) Successful in 10m23s
CI / e2e (pull_request) Successful in 9m31s

Co-Authored-By: Codex <noreply@openai.com>
This commit is contained in:
2026-05-07 12:02:49 -04:00
parent 5e6541ab92
commit f85b90c95e
12 changed files with 74 additions and 99 deletions

View File

@@ -49,6 +49,7 @@ const PLAN_GATE_STAGES: ReadonlyArray<TrialBannerStage> = [
* Pure helper — picks the highest-priority incomplete item, or `null` when
* all relevant items are done. Exported for direct unit testing.
*/
// eslint-disable-next-line react-refresh/only-export-components -- pure helper exported for focused unit tests
export function pickNextStep(
status: OnboardingStatus,
trialStage: TrialBannerStage | null,

View File

@@ -29,6 +29,7 @@ const PLAN_GATE_STAGES: ReadonlyArray<TrialBannerStage> = [
'expired',
]
// eslint-disable-next-line react-refresh/only-export-components -- pure helper exported for focused unit tests
export function buildChecklistItems(
status: OnboardingStatus,
trialStage: TrialBannerStage | null,

View File

@@ -66,10 +66,7 @@ export function useAppConfig(): UseAppConfigResult {
const [config, setConfig] = useState<PublicConfig | null>(cached)
useEffect(() => {
if (cached) {
setConfig(cached)
return
}
if (cached) return
let active = true
const handler = (c: PublicConfig) => {
if (active) setConfig(c)

View File

@@ -1,4 +1,4 @@
import { useEffect, useRef, useState } from 'react'
import { useEffect, useState } from 'react'
import { useBillingStore } from '@/store/billingStore'
import { usageApi } from '@/api/usage'
@@ -53,61 +53,38 @@ function coerceLimit(raw: unknown): number | null {
export function useFeatureLimit(field: string): FeatureLimitResult {
const limit = useBillingStore((state) => coerceLimit(state.planLimits[field]))
// Initialize from cache on first mount only; subsequent `field` changes
// are handled inside the effect below so the render-phase result reflects
// the new field synchronously (no stale `used`/`isLoading` for one tick).
const initialCached = useRef<CacheEntry | undefined>(undefined)
if (initialCached.current === undefined) {
initialCached.current = cache.get(field)
}
const initialFresh =
initialCached.current && Date.now() - initialCached.current.timestamp < CACHE_TTL_MS
const [used, setUsed] = useState<number>(initialFresh ? initialCached.current!.used : 0)
const [isLoading, setIsLoading] = useState<boolean>(!initialFresh)
// Track the field that the current `used`/`isLoading` state describes.
// When `field` changes, we synchronously reset state in render so callers
// never see stale data for the previous field.
const stateField = useRef<string>(field)
if (stateField.current !== field) {
stateField.current = field
const [state, setState] = useState(() => {
const existing = cache.get(field)
const freshNow = existing && Date.now() - existing.timestamp < CACHE_TTL_MS
if (freshNow) {
setUsed(existing!.used)
setIsLoading(false)
} else {
setUsed(0)
setIsLoading(true)
const fresh = existing && Date.now() - existing.timestamp < CACHE_TTL_MS
return {
field,
used: fresh ? existing.used : 0,
isLoading: !fresh,
}
}
})
useEffect(() => {
const existing = cache.get(field)
if (existing && Date.now() - existing.timestamp < CACHE_TTL_MS) {
setUsed(existing.used)
setIsLoading(false)
// eslint-disable-next-line react-hooks/set-state-in-effect -- sync local hook state with fresh module cache on field change
setState({ field, used: existing.used, isLoading: false })
return
}
let cancelled = false
setIsLoading(true)
setState({ field, used: 0, isLoading: true })
usageApi
.getCount(field)
.then((result) => {
if (cancelled) return
cache.set(field, { used: result.used, timestamp: Date.now() })
setUsed(result.used)
setState({ field, used: result.used, isLoading: false })
})
.catch(() => {
// TODO: backend /usage/{field} endpoint not yet implemented (planned).
// 404s and other errors degrade to used=0 silently — no toast.
if (cancelled) return
setUsed(0)
})
.finally(() => {
if (cancelled) return
setIsLoading(false)
setState({ field, used: 0, isLoading: false })
})
return () => {
@@ -115,6 +92,8 @@ export function useFeatureLimit(field: string): FeatureLimitResult {
}
}, [field])
const used = state.field === field ? state.used : 0
const isLoading = state.field === field ? state.isLoading : true
const percentage =
limit === null || limit <= 0 ? null : Math.min(100, Math.round((used / limit) * 100))
const isAtLimit = limit !== null && used >= limit

View File

@@ -1,3 +1,4 @@
import { useState } from 'react'
import { useBillingStore } from '@/store/billingStore'
export type TrialBannerStage =
@@ -28,6 +29,7 @@ const MS_PER_DAY = 24 * 60 * 60 * 1000
*/
export function useTrialBanner(): TrialBannerResult {
const subscription = useBillingStore((state) => state.subscription)
const [now] = useState(() => Date.now())
if (!subscription) {
return { stage: null, daysRemaining: null }
@@ -51,7 +53,6 @@ export function useTrialBanner(): TrialBannerResult {
// upgrade prompt still surfaces rather than silently swallowing it.
return { stage: 'expired', daysRemaining: null }
}
const now = Date.now()
if (end <= now) {
return { stage: 'expired', daysRemaining: 0 }
}

View File

@@ -47,6 +47,7 @@ export function AcceptInvitePage() {
useEffect(() => {
if (!code) {
// eslint-disable-next-line react-hooks/set-state-in-effect -- route changes without a code should replace stale lookup state
setLookup({ status: 'missing-code' })
return
}

View File

@@ -58,6 +58,7 @@ export function OAuthCallbackPage() {
}
if (oauthError) {
// eslint-disable-next-line react-hooks/set-state-in-effect -- callback URL validation maps directly into local error state
setError(`OAuth error: ${oauthError}`)
return
}

View File

@@ -182,7 +182,6 @@ export function PricingPage() {
if (!appConfig.self_serve_enabled) return
let cancelled = false
setLoading(true)
plansApi
.getPublic()
.then((data) => {

View File

@@ -33,7 +33,8 @@ function randomState(): string {
return Array.from(buf, (b) => b.toString(16).padStart(2, '0')).join('')
}
/** Build provider authorize URL. Exported for tests. */
/** Build provider authorize URL. Exported for tests and invite OAuth handoff. */
// eslint-disable-next-line react-refresh/only-export-components -- pure helper shared with AcceptInvitePage and unit tests
export function buildOAuthAuthorizeUrl(
provider: 'google' | 'microsoft',
state: string,