diff --git a/web/src/hooks/useApi.ts b/web/src/hooks/useApi.ts
index e150ed7..a7bccf0 100644
--- a/web/src/hooks/useApi.ts
+++ b/web/src/hooks/useApi.ts
@@ -100,6 +100,41 @@ export function useImportZones() {
onSuccess: () => qc.invalidateQueries({ queryKey: ["domains", project?.id] }),
})
}
+const CHECK_ALL_CONCURRENCY = 4
+
+// Ручной check по всем переданным доменам. Бэкенд (handleCheck) сам пишет
+// статус, поэтому после прогона достаточно перечитать список доменов.
+// Сбой одной зоны не прерывает остальные — считаем успехи и ошибки.
+export function useCheckAllDomains() {
+ const { project } = useAuth()
+ const qc = useQueryClient()
+ return useMutation({
+ mutationFn: async (ids: string[]) => {
+ const pid = requireProjectId(project)
+ let ok = 0
+ let failed = 0
+ const queue = [...ids]
+ const worker = async () => {
+ for (let id = queue.shift(); id !== undefined; id = queue.shift()) {
+ try {
+ await api.checkDomain(pid, id)
+ ok++
+ } catch {
+ failed++
+ }
+ }
+ }
+ await Promise.all(
+ Array.from({ length: Math.min(CHECK_ALL_CONCURRENCY, ids.length) }, worker),
+ )
+ return { ok, failed }
+ },
+ onSettled: () => {
+ qc.invalidateQueries({ queryKey: ["domains", project?.id] })
+ qc.invalidateQueries({ queryKey: ["check", project?.id] })
+ },
+ })
+}
export function useSetDomainTemplate() {
const { project } = useAuth()
const qc = useQueryClient()
diff --git a/web/src/pages/DomainsPage.test.tsx b/web/src/pages/DomainsPage.test.tsx
index 7b9c7dd..959f3f2 100644
--- a/web/src/pages/DomainsPage.test.tsx
+++ b/web/src/pages/DomainsPage.test.tsx
@@ -234,3 +234,16 @@ test("счётчик показывает, сколько доменов отф
expect(screen.getByText(/показано 1 из 2/i)).toBeInTheDocument()
})
+
+test("«Проверить все зоны» проверяет только домены с шаблоном и обновляет список", async () => {
+ const check = vi.spyOn(api, "checkDomain").mockResolvedValue({ diffs: [] } as never)
+ const user = userEvent.setup()
+ renderPage()
+ await screen.findByText("example.com.")
+
+ await user.click(screen.getByRole("button", { name: "Проверить все зоны" }))
+
+ await waitFor(() => expect(check).toHaveBeenCalledTimes(1))
+ expect(check).toHaveBeenCalledWith(PROJECT_ID, "d2")
+ expect(await screen.findByText("Проверено 1")).toBeInTheDocument()
+})
diff --git a/web/src/pages/DomainsPage.tsx b/web/src/pages/DomainsPage.tsx
index fc2aefb..9338ca4 100644
--- a/web/src/pages/DomainsPage.tsx
+++ b/web/src/pages/DomainsPage.tsx
@@ -1,6 +1,6 @@
import { useState } from "react"
import { Link } from "react-router-dom"
-import { Inbox, Loader2, Upload } from "lucide-react"
+import { GitCompareArrows, Inbox, Loader2, Upload } from "lucide-react"
import { Button } from "@/components/ui/button"
import { StatusBadge } from "@/components/StatusBadge"
import {
@@ -20,6 +20,7 @@ import {
} from "@/components/ui/table"
import {
useAccounts,
+ useCheckAllDomains,
useDomains,
useImportZones,
useSetDomainTemplate,
@@ -56,6 +57,7 @@ export function DomainsPage() {
const accounts = useAccounts()
const templates = useTemplates()
const importZones = useImportZones()
+ const checkAll = useCheckAllDomains()
const setTemplate = useSetDomainTemplate()
const accountList = accounts.data ?? []
@@ -104,6 +106,13 @@ export function DomainsPage() {
importZones.mutate(selectedImportAccount)
}
+ // Диф есть только у доменов с шаблоном; фильтры списка на набор не влияют.
+ const checkableIds = domainList.filter((d) => d.templateId).map((d) => d.id)
+
+ function onCheckAll() {
+ checkAll.mutate(checkableIds)
+ }
+
function onTemplateChange(domainId: string, value: unknown) {
const templateId = value === NO_TEMPLATE ? null : (value as string)
setTemplate.mutate({ id: domainId, templateId })
@@ -146,9 +155,27 @@ export function DomainsPage() {
)}
Импортировать зоны
+
{importZones.isError && (
{importZones.error.message}
)}
+ {checkAll.isSuccess && (
+
+ Проверено {checkAll.data.ok}
+ {checkAll.data.failed > 0 && `, ошибок ${checkAll.data.failed}`}
+
+ )}
{importZones.isSuccess && (
Создано {importZones.data.created.length}, удалено {importZones.data.removed.length}