feat(web,api): клиент/хуки расписания/каналов/истории + lastCheckStatus в domainResponse

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BwxdSt4reTm7Dj1oxRvpP3
This commit is contained in:
2026-07-04 14:24:02 +07:00
parent b31f886ae2
commit 45259b9720
5 changed files with 162 additions and 2 deletions
+68
View File
@@ -128,4 +128,72 @@ describe("api client", () => {
expect(url).toBe(`/api/v1/projects/${PROJECT_ID}/domains/d1`)
expect((opts as RequestInit).method).toBe("PATCH")
})
describe("schedule", () => {
it("getSchedule(projectId) GETs /schedule", async () => {
const spy = mockFetch({ intervalSeconds: 3600, enabled: false })
await api.getSchedule(PROJECT_ID)
expect(spy).toHaveBeenCalledWith(
`/api/v1/projects/${PROJECT_ID}/schedule`,
expect.objectContaining({ method: "GET", credentials: "include" }),
)
})
it("putSchedule(projectId, {intervalSeconds,enabled}) PUTs /schedule", async () => {
const spy = mockFetch({ intervalSeconds: 120, enabled: true })
await api.putSchedule(PROJECT_ID, { intervalSeconds: 120, enabled: true })
const [url, opts] = spy.mock.calls[0]
expect(url).toBe(`/api/v1/projects/${PROJECT_ID}/schedule`)
expect((opts as RequestInit).method).toBe("PUT")
expect((opts as RequestInit).credentials).toBe("include")
expect(String((opts as RequestInit).body)).toContain("intervalSeconds")
})
})
describe("channels", () => {
it("listChannels(projectId) GETs /channels", async () => {
const spy = mockFetch([])
await api.listChannels(PROJECT_ID)
expect(spy).toHaveBeenCalledWith(
`/api/v1/projects/${PROJECT_ID}/channels`,
expect.objectContaining({ method: "GET" }),
)
})
it("createChannel(projectId, {type,config,secret}) POSTs /channels with secret in body", async () => {
const spy = mockFetch({ id: "c1", type: "telegram", config: { chat_id: "1" }, enabled: true })
await api.createChannel(PROJECT_ID, { type: "telegram", config: { chat_id: "1" }, secret: "BOT_TOKEN" })
const [url, opts] = spy.mock.calls[0]
expect(url).toBe(`/api/v1/projects/${PROJECT_ID}/channels`)
expect((opts as RequestInit).method).toBe("POST")
expect(String((opts as RequestInit).body)).toContain("BOT_TOKEN")
})
it("deleteChannel(projectId, id) DELETEs /channels/{id}", async () => {
const spy = mockFetch(undefined, true, 204)
await api.deleteChannel(PROJECT_ID, "c1")
expect(spy).toHaveBeenCalledWith(
`/api/v1/projects/${PROJECT_ID}/channels/c1`,
expect.objectContaining({ method: "DELETE" }),
)
})
it("testChannel(projectId, id) POSTs /channels/{id}/test", async () => {
const spy = mockFetch({ status: "ok" })
await api.testChannel(PROJECT_ID, "c1")
expect(spy).toHaveBeenCalledWith(
`/api/v1/projects/${PROJECT_ID}/channels/c1/test`,
expect.objectContaining({ method: "POST" }),
)
})
})
it("domainHistory(projectId, domainId) GETs /domains/{did}/history", async () => {
const spy = mockFetch([])
await api.domainHistory(PROJECT_ID, "d1")
expect(spy).toHaveBeenCalledWith(
`/api/v1/projects/${PROJECT_ID}/domains/d1/history`,
expect.objectContaining({ method: "GET", credentials: "include" }),
)
})
})
+15
View File
@@ -3,6 +3,7 @@ import type {
AuthState,
Account, CreateAccountInput, Template, CreateTemplateInput,
Domain, CreateDomainInput, ChangesetResponse, ApplyRequest,
Schedule, Channel, CreateChannelInput, CheckRun,
} from "./types"
export class UnauthorizedError extends Error {
@@ -77,4 +78,18 @@ export const api = {
req<ChangesetResponse>(projectPath(projectId, `/domains/${id}/check`)),
applyDomain: (projectId: string, id: string, body: ApplyRequest) =>
req<ChangesetResponse>(projectPath(projectId, `/domains/${id}/apply`), { method: "POST", body: JSON.stringify(body) }),
domainHistory: (projectId: string, id: string) =>
req<CheckRun[]>(projectPath(projectId, `/domains/${id}/history`)),
getSchedule: (projectId: string) => req<Schedule>(projectPath(projectId, "/schedule")),
putSchedule: (projectId: string, input: Schedule) =>
req<Schedule>(projectPath(projectId, "/schedule"), { method: "PUT", body: JSON.stringify(input) }),
listChannels: (projectId: string) => req<Channel[]>(projectPath(projectId, "/channels")),
createChannel: (projectId: string, input: CreateChannelInput) =>
req<Channel>(projectPath(projectId, "/channels"), { method: "POST", body: JSON.stringify(input) }),
deleteChannel: (projectId: string, id: string) =>
req<void>(projectPath(projectId, `/channels/${id}`), { method: "DELETE" }),
testChannel: (projectId: string, id: string) =>
req<{ status: string }>(projectPath(projectId, `/channels/${id}/test`), { method: "POST" }),
}
+8
View File
@@ -15,6 +15,7 @@ export interface Domain {
zoneName: string
zoneId: string
templateId?: string | null // Go omitempty: поле может отсутствовать (undefined), а не null
lastCheckStatus?: string // "unknown" | "in_sync" | "drift" | "error"
}
export interface CreateDomainInput {
providerAccountId: string
@@ -23,6 +24,13 @@ export interface CreateDomainInput {
templateId?: string | null
}
export interface Schedule { intervalSeconds: number; enabled: boolean }
export interface Channel { id: string; type: string; config: object; enabled: boolean }
export interface CreateChannelInput { type: string; config: object; secret: string }
export interface CheckRun { id?: string; createdAt: string; result: object }
export interface RecordView {
kind: string // add | update | delete | in_sync
type: string