diff --git a/apps/api/src/services/dashboard-stats.ts b/apps/api/src/services/dashboard-stats.ts index 650f754..3ad4a68 100644 --- a/apps/api/src/services/dashboard-stats.ts +++ b/apps/api/src/services/dashboard-stats.ts @@ -96,7 +96,6 @@ export function computeDashboardStats(): DashboardStats { }).length let issuesCount = 0 - if (activeVps.some((v) => !(v.project || '').trim())) issuesCount++ if ( activeVps.some((v) => { const dr = Number(v.dailyRate || 0) diff --git a/apps/web/src/components/vps-filters-toolbar.tsx b/apps/web/src/components/vps-filters-toolbar.tsx index a7c0f5f..e358790 100644 --- a/apps/web/src/components/vps-filters-toolbar.tsx +++ b/apps/web/src/components/vps-filters-toolbar.tsx @@ -339,6 +339,9 @@ export function VpsFiltersToolbar({ value={filters.search} onChange={(e) => onChange({ ...filters, search: e.target.value })} className="pl-8" + autoComplete="off" + name="vps-inventory-search" + spellCheck={false} /> diff --git a/apps/web/src/lib/inventory-health.ts b/apps/web/src/lib/inventory-health.ts index fe3372f..6726814 100644 --- a/apps/web/src/lib/inventory-health.ts +++ b/apps/web/src/lib/inventory-health.ts @@ -87,11 +87,6 @@ export function computeInventoryHealth(input: InventoryHealthInput): InventoryIs const issues: InventoryIssue[] = [] - const noProject = vps.filter((v) => v.status === 'active' && !(v.project || '').trim()) - if (noProject.length) { - issues.push({ key: 'no-project', title: 'Активные VPS без проекта', count: noProject.length, to: '/vps?health=no-project' }) - } - const noRate = vps.filter((v) => { if (v.status !== 'active') return false const dr = Number(v.dailyRate || 0) diff --git a/apps/web/src/routes/_auth/dashboard.tsx b/apps/web/src/routes/_auth/dashboard.tsx index ae02118..d371018 100644 --- a/apps/web/src/routes/_auth/dashboard.tsx +++ b/apps/web/src/routes/_auth/dashboard.tsx @@ -27,6 +27,7 @@ import type { DataTableColumn } from '@/components/data-grid-types' import { dataGridCellStack } from '@/components/data-grid-cells' import { SectionCardsSkeleton, TableSkeleton } from '@/components/skeletons' import { Button } from '@cfdm/ui/components/button' +import { Badge } from '@cfdm/ui/components/badge' import { Alert, AlertDescription, AlertTitle } from '@cfdm/ui/components/alert' import { Tabs, TabsContent, TabsList, TabsTrigger } from '@cfdm/ui/components/tabs' import { StatusBadge } from '@/components/status-badge' @@ -306,25 +307,36 @@ function DashboardPage() { /> - - - Проблемы ({issues.length}) - Последние VPS - Аккаунты ({atRisk.length}) + + + + Проблемы + {issues.length > 0 ? {issues.length} : null} + + + Последние VPS + + + Аккаунты + {atRisk.length > 0 ? {atRisk.length} : null} + - + i.key} emptyTitle="Проблем не найдено" - emptyDescription="Все активные VPS имеют проект, ставку и актуальный синк" + emptyDescription="Критичных проблем в инвентаре не обнаружено" pagination={false} /> - + navigate({ to: '/vps', search: { edit: v.id } })} /> - + v.status === 'active' && !(v.project || '').trim()) - } else if (health === 'no-rate') { + if (health === 'no-rate') { rows = rows.filter((v) => { if (v.status !== 'active') return false const dr = Number(v.dailyRate || 0) @@ -224,16 +222,17 @@ function VpsPage() { return rows }, [snapshot, filters, health]) - const countryOptions = useMemo(() => { + const dbCountryNames = useMemo(() => { const names = new Set() - // Из существующих VPS — могут быть произвольные строки for (const v of snapshot?.vps ?? []) { const c = (v.country || '').trim() if (c) names.add(c) } - // Из стандартизированного справочника - for (const c of COUNTRIES) names.add(c.name) - return [...names].sort((a, b) => a.localeCompare(b, 'ru')).map((name) => { + return names + }, [snapshot?.vps]) + + const mapCountryOptions = (names: Iterable) => + [...names].sort((a, b) => a.localeCompare(b, 'ru')).map((name) => { const ref = COUNTRY_BY_NAME_RU[name.toLowerCase()] return { value: name, @@ -242,10 +241,20 @@ function VpsPage() { leading: , } }) - }, [snapshot?.vps]) - const cityOptions = useMemo( - () => buildCityOptions(snapshot?.vps, filters.country[0]), + const filterCountryOptions = useMemo( + () => mapCountryOptions(dbCountryNames), + [dbCountryNames], + ) + + const formCountryOptions = useMemo(() => { + const names = new Set(dbCountryNames) + for (const c of COUNTRIES) names.add(c.name) + return mapCountryOptions(names) + }, [dbCountryNames]) + + const filterCityOptions = useMemo( + () => buildCityOptions(snapshot?.vps, filters.country[0], { includeCatalog: false }), [snapshot?.vps, filters.country], ) @@ -425,8 +434,8 @@ function VpsPage() { providerAccounts={snap.providerAccounts} vps={snap.vps} projectNameOptions={projectNameOptions} - countryOptions={countryOptions} - cityOptions={cityOptions} + countryOptions={filterCountryOptions} + cityOptions={filterCityOptions} /> {tableSections.map((section) => ( diff --git a/packages/shared/src/geo/cities.ts b/packages/shared/src/geo/cities.ts index c8d570f..eeeea64 100644 --- a/packages/shared/src/geo/cities.ts +++ b/packages/shared/src/geo/cities.ts @@ -217,11 +217,13 @@ export function cityMatchesCountry( ) } -/** Опции городов: из VPS и справочника, опционально по стране. */ +/** Опции городов: из VPS и опционально справочника, опционально по стране. */ export function buildCityOptions( rows: readonly CityLocationRow[] | undefined, countryName?: string, + options?: { includeCatalog?: boolean }, ): { value: string; label: string }[] { + const includeCatalog = options?.includeCatalog ?? true const names = new Set() const countryCode = countryName?.trim() ? COUNTRY_BY_NAME_RU[countryName.trim().toLowerCase()]?.code @@ -245,8 +247,10 @@ export function buildCityOptions( } } - for (const { name } of listCities(countryCode)) { - names.add(name) + if (includeCatalog) { + for (const { name } of listCities(countryCode)) { + names.add(name) + } } return [...names]