diff --git a/.cursor/rules/backend-api-ui.mdc b/.cursor/rules/backend-api-ui.mdc index 0f91e77..b9bd34f 100644 --- a/.cursor/rules/backend-api-ui.mdc +++ b/.cursor/rules/backend-api-ui.mdc @@ -20,13 +20,14 @@ Fastify backend: `apps/api/`. Контракты — `@cfdm/shared` (Zod). Front - Дублирование Zod schemas в `apps/web` — только re-export из `@cfdm/shared` - Самописные формы без `Field` + RHF + Zod -## shadcn-паттерны +## UI-паттерны (ReUI Frame) | API-данные | UI | |------------|-----| -| Список | `Table` / `DataTableCard` | -| Создание | `Card` + `FieldGroup` + RHF | -| Статус | `Badge` variants | +| Список | `ResourcePage` (Frame + DataGrid + Filters) | +| Создание / редактирование | `FormSheet` + `FieldGroup` + RHF | +| Settings | `SettingsShell` + Frame + `SettingRow` | +| Статус | ReUI `Badge` / `StatusBadge` | | Ошибка | `sonner` `toast.error` | ## Согласованность diff --git a/.cursor/rules/frontend-ui-patterns.mdc b/.cursor/rules/frontend-ui-patterns.mdc index 0db863d..038f679 100644 --- a/.cursor/rules/frontend-ui-patterns.mdc +++ b/.cursor/rules/frontend-ui-patterns.mdc @@ -36,7 +36,7 @@ alwaysApply: false | Forms | [form-7](https://reui.io/preview/base/form-7) | | Shell | [app-shell-12](https://reui.io/preview/base/app-shell-12) | -**SettingRow:** `FieldSeparator` только между соседними rows; не между toggle-row и nested fields (Health-check). +**SettingRow:** `FieldSeparator` opt-in (`separated`); не между toggle-row и nested fields (Health-check). Settings-секции — отдельные Frame + `gap`, без hairline под PageHeader. ## Иерархия компонентов diff --git a/apps/api/src/routes/health.ts b/apps/api/src/routes/health.ts index a16d287..43df357 100644 --- a/apps/api/src/routes/health.ts +++ b/apps/api/src/routes/health.ts @@ -1,7 +1,6 @@ import type { FastifyInstance } from "fastify"; import { z } from "zod"; import { healthCheck } from "../plugins/db.js"; -import { getAppSwitcher } from "@cfdm/db"; import * as authService from "../services/auth.js"; export async function healthRoutes(app: FastifyInstance) { @@ -40,10 +39,6 @@ export async function authRoutes(app: FastifyInstance) { }; }); - app.get("/settings/app-switcher", async (request) => { - return getAppSwitcher(request.server.db); - }); - const loginSchema = z.object({ username: z.string(), password: z.string(), diff --git a/apps/api/src/services/health-check-service.ts b/apps/api/src/services/health-check-service.ts index 527e7da..b8a876a 100644 --- a/apps/api/src/services/health-check-service.ts +++ b/apps/api/src/services/health-check-service.ts @@ -85,9 +85,10 @@ function createIpPinnedAgent( sniHost: string, useTls: boolean, timeoutMs: number, + verifyTls: boolean, ): Agent { const connector = buildConnector({ - rejectUnauthorized: false, + rejectUnauthorized: verifyTls, timeout: timeoutMs, }); return new Agent({ @@ -126,14 +127,15 @@ async function httpProbe( const connectAddr = String(ip || "").trim(); const headerHost = (target.hostname || "").trim() || connectAddr; const url = buildHttpProbeUrl(headerHost, port, pathWithSlash, useTls); + const verifyTls = target.verify_tls === true; const family = isIP(connectAddr); const pinToIp = family === 4 || family === 6; const dispatcher = pinToIp - ? createIpPinnedAgent(connectAddr, headerHost, useTls, timeoutMs) + ? createIpPinnedAgent(connectAddr, headerHost, useTls, timeoutMs, verifyTls) : useTls - ? createIpPinnedAgent(connectAddr, headerHost, useTls, timeoutMs) + ? createIpPinnedAgent(connectAddr, headerHost, useTls, timeoutMs, verifyTls) : undefined; try { @@ -373,6 +375,7 @@ export async function runDomainMonitors( path: monitor.path, expected_status: monitor.expected_status, timeout_ms: monitor.timeout_ms, + verify_tls: false, }; let result: ProbeResult; if (monitor.type === "http") { diff --git a/apps/api/src/services/service-config-service.ts b/apps/api/src/services/service-config-service.ts index 9e55111..d1c8179 100644 --- a/apps/api/src/services/service-config-service.ts +++ b/apps/api/src/services/service-config-service.ts @@ -40,6 +40,7 @@ export interface ServiceDomainInput { health_check_expected_status?: number | null; health_check_interval_sec?: number; health_check_timeout_ms?: number; + health_check_verify_tls?: boolean; } export interface ToggleRequest { @@ -59,6 +60,7 @@ export interface ServiceGroupBody { health_check_expected_status?: number | null; health_check_interval_sec?: number; health_check_timeout_ms?: number; + health_check_verify_tls?: boolean; } export interface UpdateServiceGroupBody { @@ -74,6 +76,7 @@ export interface UpdateServiceGroupBody { health_check_expected_status?: number | null; health_check_interval_sec?: number; health_check_timeout_ms?: number; + health_check_verify_tls?: boolean; } export interface UpdateServiceConfigRequest { @@ -322,6 +325,7 @@ async function buildView(db: Db, serviceId: number): Promise { health_check_expected_status: binding.health_check_expected_status, health_check_interval_sec: binding.health_check_interval_sec, health_check_timeout_ms: binding.health_check_timeout_ms, + health_check_verify_tls: binding.health_check_verify_tls, sync_status: aggregateSyncStatus(statuses), }; }); @@ -1209,7 +1213,8 @@ export async function updateConfig( input.health_check_path !== undefined || input.health_check_expected_status !== undefined || input.health_check_interval_sec !== undefined || - input.health_check_timeout_ms !== undefined + input.health_check_timeout_ms !== undefined || + input.health_check_verify_tls !== undefined ) { repos.updateBindingLbConfig(db, binding.id, { lb_mode: input.lb_mode, @@ -1220,6 +1225,7 @@ export async function updateConfig( health_check_expected_status: input.health_check_expected_status, health_check_interval_sec: input.health_check_interval_sec, health_check_timeout_ms: input.health_check_timeout_ms, + health_check_verify_tls: input.health_check_verify_tls, }); } @@ -1314,6 +1320,7 @@ export async function createGroup( health_check_expected_status: body.health_check_expected_status, health_check_interval_sec: body.health_check_interval_sec, health_check_timeout_ms: body.health_check_timeout_ms, + health_check_verify_tls: body.health_check_verify_tls, }, ); } @@ -1349,6 +1356,7 @@ export async function updateGroup( health_check_expected_status: body.health_check_expected_status, health_check_interval_sec: body.health_check_interval_sec, health_check_timeout_ms: body.health_check_timeout_ms, + health_check_verify_tls: body.health_check_verify_tls, }, ); if (!domain && group.enabled) { diff --git a/apps/api/test/health-check.test.ts b/apps/api/test/health-check.test.ts index 891760e..ec5763e 100644 --- a/apps/api/test/health-check.test.ts +++ b/apps/api/test/health-check.test.ts @@ -55,6 +55,7 @@ describe("health-check probeTarget", () => { path: null, expected_status: null, timeout_ms: 1000, + verify_tls: false, }; const result = await healthCheckService.probeTarget(target); expect(result.ok).toBe(true); @@ -73,6 +74,7 @@ describe("health-check probeTarget", () => { path: null, expected_status: null, timeout_ms: 500, + verify_tls: false, }; const result = await healthCheckService.probeTarget(target); expect(result.ok).toBe(false); @@ -104,6 +106,7 @@ describe("health-check probeTarget", () => { path: "/", expected_status: 200, timeout_ms: 1000, + verify_tls: false, }; const bindingTarget: HealthCheckTarget = { ...groupTarget, @@ -138,6 +141,7 @@ describe("health-check probeTarget", () => { path: null, expected_status: null, timeout_ms: 3000, + verify_tls: false, }; const binding: HealthCheckTarget = { ...group, @@ -204,4 +208,27 @@ describe("health-check state derivation via runAllChecks", () => { ); expect(status?.status).toBe("down"); }); + + it("listHealthCheckTargets includes verify_tls from binding config", async () => { + const { createMemoryDb, repos, runMigrations } = await import("@cfdm/db"); + const { db, sqlite } = createMemoryDb(); + runMigrations(sqlite); + + const domain = repos.createDomain(db, null, "example.com", "zone-id"); + const service = repos.createService(db, "Svc", "svc"); + const binding = repos.insertBinding(db, domain.id, service.id, "@", null); + repos.updateBindingLbConfig(db, binding.id, { + health_check_enabled: true, + health_check_type: "http", + health_check_port: 443, + health_check_verify_tls: true, + }); + repos.replaceBindingIpsWithMeta(db, binding.id, [ + { ip: "10.0.0.1", weight: 1, priority: 1 }, + ]); + + const targets = repos.listHealthCheckTargets(db); + expect(targets).toHaveLength(1); + expect(targets[0]?.verify_tls).toBe(true); + }); }); diff --git a/apps/web/src/components/blocks/app-shell-12/components/app-shell.tsx b/apps/web/src/components/blocks/app-shell-12/components/app-shell.tsx deleted file mode 100644 index 67a2151..0000000 --- a/apps/web/src/components/blocks/app-shell-12/components/app-shell.tsx +++ /dev/null @@ -1,46 +0,0 @@ -import { type CSSProperties } from "react" - -import { cn } from "@cfdm/ui/lib/utils" -import { SidebarInset, SidebarProvider } from "@cfdm/ui/components/sidebar" - -import { AppSidebar } from "./app-sidebar" -import { PageToolbar } from "./page-toolbar" -import { SiteFooter } from "./site-footer" -import { SiteHeader } from "./site-header" - -export function AppShell() { - return ( - - {/* Sidebar */} - - - {/* Header */} - - {/* Content */} -
- -
-
-
-
-
-
-
- {/* Footer */} - -
-
- ) -} \ No newline at end of file diff --git a/apps/web/src/components/blocks/app-shell-12/components/app-sidebar.tsx b/apps/web/src/components/blocks/app-shell-12/components/app-sidebar.tsx deleted file mode 100644 index a9a5f97..0000000 --- a/apps/web/src/components/blocks/app-shell-12/components/app-sidebar.tsx +++ /dev/null @@ -1,92 +0,0 @@ -import { cn } from "@cfdm/ui/lib/utils" -import { Separator } from "@cfdm/ui/components/separator" -import { - SidebarContent, - SidebarFooter, - SidebarHeader, - Sidebar as SidebarRoot, - useSidebar, -} from "@cfdm/ui/components/sidebar" - -import { Brand } from "./brand" -import { NavMain } from "./nav-main" -import { NavProjects } from "./nav-projects" -import { NavSecondary } from "./nav-secondary" - -function SidebarRailToggle() { - const { state, toggleSidebar } = useSidebar() - const isExpanded = state === "expanded" - - return ( - - ) -} - -export function AppSidebar() { - return ( - - - - - - - - - - - -
- -
- -
- - -
- ) -} \ No newline at end of file diff --git a/apps/web/src/components/blocks/app-shell-12/components/apps-menu.tsx b/apps/web/src/components/blocks/app-shell-12/components/apps-menu.tsx deleted file mode 100644 index c9b3211..0000000 --- a/apps/web/src/components/blocks/app-shell-12/components/apps-menu.tsx +++ /dev/null @@ -1,54 +0,0 @@ -import { Button } from "@cfdm/ui/components/button" -import { - DropdownMenu, - DropdownMenuContent, - DropdownMenuGroup, - DropdownMenuItem, - DropdownMenuLabel, - DropdownMenuSeparator, - DropdownMenuTrigger, -} from "@cfdm/ui/components/dropdown-menu" -import { APPS } from "./data" -import { LayoutGridIcon } from "lucide-react" - -export function AppsMenu() { - return ( - - } - > - - - - - Apps -
- {APPS.map((app) => ( - } - className="h-auto flex-col gap-1.5 py-3 text-center [&_svg]:size-5" - > - {app.icon} - {app.label} - - ))} -
- - } - className="justify-center text-sm font-medium" - > - Browse All Apps - -
-
-
- ) -} \ No newline at end of file diff --git a/apps/web/src/components/blocks/app-shell-12/components/brand.tsx b/apps/web/src/components/blocks/app-shell-12/components/brand.tsx deleted file mode 100644 index ef03007..0000000 --- a/apps/web/src/components/blocks/app-shell-12/components/brand.tsx +++ /dev/null @@ -1,72 +0,0 @@ -import { cn } from "@cfdm/ui/lib/utils" -import { Item, ItemMedia } from "@cfdm/ui/components/item" -import { - SidebarMenu, - SidebarMenuButton, - SidebarMenuItem, -} from "@cfdm/ui/components/sidebar" - -import { BRAND } from "./data" - -function AuthLogo({ className }: { className?: string }) { - return ( - - ) -} - -function BrandLogo() { - return ( - - - - ) -} - -export function Brand() { - return ( - - {/* Sidebar */} - - } - className="gap-2.5" - > - -
- {BRAND.name} - - {BRAND.tier} plan - -
-
-
-
- ) -} \ No newline at end of file diff --git a/apps/web/src/components/blocks/app-shell-12/components/data.tsx b/apps/web/src/components/blocks/app-shell-12/components/data.tsx deleted file mode 100644 index 5d1f521..0000000 --- a/apps/web/src/components/blocks/app-shell-12/components/data.tsx +++ /dev/null @@ -1,604 +0,0 @@ -import { type ReactNode } from "react" -import { SettingsIcon, UsersIcon, BookOpenIcon, HouseIcon, BarChart3Icon, GlobeIcon, ExternalLinkIcon, UserPlusIcon, FlagIcon, CopyIcon, ArchiveIcon, InboxIcon, CalendarIcon, FileTextIcon, ListChecksIcon } from "lucide-react" - -// ── Types ── - -export type NavChild = { - id: string - label: string - isActive?: boolean - disabled?: boolean -} - -export type NavItem = { - id: string - label: string - icon: ReactNode - badge?: string | number - isActive?: boolean - disabled?: boolean - children?: NavChild[] -} - -export type NavGroup = { - id: string - label?: string - items: NavItem[] -} - -export type SecondaryItem = { - id: string - label: string - icon: ReactNode -} - -export type Workspace = { - id: string - name: string - tier: string - description: string -} - -export type Project = { - id: string - name: string - progress: number - color: string -} - -export type ItemAction = { - id: string - label: string - icon: ReactNode - destructive: boolean -} - -export type AppShortcut = { - id: string - label: string - icon: ReactNode -} - -export type NotificationType = - | "mention" - | "comment" - | "share" - | "invite" - | "billing" - | "security" - | "feature" - | "deployment" - | "usage" - | "system" - | "task" - | "approval" - | "integration" - | "achievement" - | "feedback" - | "team_join" - | "reaction" - | "review" - | "event" - -export type NotificationVariant = "info" | "success" | "warning" | "destructive" - -export type NotificationAction = { - label: string - variant?: "default" | "outline" | "destructive" -} - -export type NotificationAttachment = { - name: string - size: string -} - -export type NotificationAvatar = { - src: string - fallback: string -} - -export type NotificationGroupMember = { - src: string - fallback: string - online?: boolean -} - -export type NotificationMeta = { - label: string - value: string - color?: string -} - -export type Notification = { - id: string - type: NotificationType - variant?: NotificationVariant - title: string - body?: string - time: string - unread?: boolean - avatar?: NotificationAvatar - username?: string - link?: string - badge?: string - actions?: NotificationAction[] - attachment?: NotificationAttachment - meta?: NotificationMeta - progress?: number - progressVariant?: "default" | "success" - avatarGroup?: NotificationGroupMember[] - avatarGroupCount?: number - rating?: number - eventDate?: string - eventTime?: string -} - -export type FooterLink = { id: string; label: string } - -// ── Brand + account ── - -export const BRAND = { - name: "ReUI", - tier: "Pro", -} as const - -export const USER = { - name: "Theo Park", - email: "theo@reui.io", - image: - "https://images.unsplash.com/photo-1472099645785-5658abf4ff4e?w=96&h=96&dpr=2&q=80", - initials: "TP", -} as const - -export const WORKSPACES: Workspace[] = [ - { - id: "reui", - name: "ReUI Labs", - tier: "Enterprise", - description: "Design systems and block releases", - }, - { - id: "studio", - name: "ReUI Studio", - tier: "Pro", - description: "Customer workspace builds", - }, - { - id: "ops", - name: "ReUI Ops", - tier: "Team", - description: "Billing, support, and reliability", - }, -] - -// ── Nav secondary ── - -export const NAV_SECONDARY: SecondaryItem[] = [ - { - id: "settings", - label: "Settings", - icon: ( -