feat(q1): 前端类型安全治理完成 — any 83→40, P0 全部 done

- types/monitor.ts: 新增 6 个运行时记录类型(AlertEventRecord/AlertRuleRecord/
  AgentHostRecord/CronJobRecord/CertExpiryRecord/ApprovalWorkflowStepRecord)
- types/common.ts: 新增 OptionItem 通用下拉类型
- 组件级 any 清理 43 处:
  - AlertEventsTab/AlertRulesTab/AlertChannelDialog/alertShared (19处)
  - AuditOperationsTab normalizeLog + 筛选器 (8处)
  - CronJobManager 任务列表/编辑/操作 (6处)
  - AgentMonitor 主机列表/命令执行 (5处)
  - DashboardRecent 5 张卡片组件 (5处)
  - AlertManager 状态类型对齐 (联动修复)
  - Dashboard.tsx 调用端类型对齐 (联动修复)
- AlertChannel 接口新增 test_status 字段
- ESLint any: 83→40 (<50 目标达成)
- tsc -b / tsc --noEmit: 零错误
- npm run build: ✓ built in 17.11s
- 9/9 核心 API 回归通过
This commit is contained in:
attacker committed 2026-06-27 02:31:26 +08:00
1 parent 0535f30c87
commit bb2b91ebe0
15 files changed
+259 -93

No files matched your search

+11 -9
View File
@@ -5,6 +5,7 @@ import { agentApi } from '@/api';
import { AgentWizard } from './AgentWizard';
import { HostDetailDialog } from './HostDetailDialog';
import { isOnline, timeAgo, asList } from './agentUtils';
import type { AgentHostRecord } from '@/types';
import { Card, CardContent } from '@/components/ui/card';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
@@ -40,10 +41,10 @@ import {
// ============ 组件 ============
export function AgentMonitor() {
const [hosts, setHosts] = useState<any[]>([]);
const [hosts, setHosts] = useState<AgentHostRecord[]>([]);
const [loading, setLoading] = useState(true);
const [refreshing, setRefreshing] = useState(false);
const [selectedHost, setSelectedHost] = useState<any>(null);
const [selectedHost, setSelectedHost] = useState<AgentHostRecord | null>(null);
const [wizardOpen, setWizardOpen] = useState(false);
const timerRef = useRef<ReturnType<typeof setInterval> | null>(null);
@@ -53,13 +54,13 @@ export function AgentMonitor() {
const [cmdCommand, setCmdCommand] = useState('');
const [cmdTimeout, setCmdTimeout] = useState(30);
const [cmdSubmitting, setCmdSubmitting] = useState(false);
const [cmdResult, setCmdResult] = useState<any>(null);
const [cmdResult, setCmdResult] = useState<{ status: string; result?: string; output?: string; error?: string } | null>(null);
const loadLatest = useCallback(async (silent = false) => {
if (!silent) setRefreshing(true);
try {
const data = await agentApi.getLatest();
const list = asList(data);
const list = asList(data) as AgentHostRecord[];
setHosts(list);
// 同步选中主机的最新数据
if (selectedHost) {
@@ -102,15 +103,16 @@ export function AgentMonitor() {
setCmdSubmitting(true);
setCmdResult(null);
try {
const res: Record<string, any> = await agentApi.execCommand({
const res = await agentApi.execCommand({
hostname: cmdHost,
command: cmdCommand.trim(),
timeout: Number(cmdTimeout) || 30,
});
}) as Record<string, unknown>;
const resData = (res?.data ?? {}) as Record<string, unknown>;
setCmdResult({
status: res?.code === 200 ? 'completed' : 'failed',
result: res?.data?.output || res?.message || '无输出',
error: res?.data?.error,
result: (resData.output as string) || (res?.message as string) || '无输出',
error: resData.error as string | undefined,
});
} catch (err) {
setCmdResult({ status: 'failed', result: err?.message || '执行失败' });
@@ -209,7 +211,7 @@ export function AgentMonitor() {
const cpu = Number(host.cpu_percent ?? host.cpu ?? 0);
const mem = Number(host.mem_percent ?? host.mem ?? 0);
const disk = Number(host.disk_percent ?? host.disk ?? 0);
const ports: Record<string, any>[] = Array.isArray(host.ports) ? host.ports : [];
const ports: Record<string, unknown>[] = Array.isArray(host.ports) ? (host.ports as Record<string, unknown>[]) : [];
return (
<Card
key={host.hostname}
@@ -26,17 +26,18 @@ import { useConfirm } from '@/components/ui/confirm-dialog';
import { getStatusBadge, parseCron } from './cronJobHelpers';
import { CronJobFormDialog } from './CronJobFormDialog';
import { ExecutionHistoryDialog } from './ExecutionHistoryDialog';
import type { CronJobRecord } from '@/types';
export function CronJobManager() {
const [searchQuery, setSearchQuery] = useState('');
const [jobs, setJobs] = useState<any[]>([]);
const [jobs, setJobs] = useState<CronJobRecord[]>([]);
const [initialLoading, setInitialLoading] = useState(true);
const [refreshing, setRefreshing] = useState(false);
const [isFormDialogOpen, setIsFormDialogOpen] = useState(false);
const [isLogDialogOpen, setIsLogDialogOpen] = useState(false);
const [executingId, setExecutingId] = useState<number | null>(null);
// editJob=null 表示新建模式;非 null 表示编辑已有任务 // Logan.Li
const [editJob, setEditJob] = useState<any | null>(null);
const [editJob, setEditJob] = useState<CronJobRecord | null>(null);
// 批量选择(批量删除) // Logan.Li
const [selectedIds, setSelectedIds] = useState<number[]>([]);
@@ -47,7 +48,7 @@ export function CronJobManager() {
setIsFormDialogOpen(true);
};
const openEdit = ( job: any) => {
const openEdit = ( job: CronJobRecord) => {
setEditJob(job);
setIsFormDialogOpen(true);
};
@@ -56,7 +57,7 @@ export function CronJobManager() {
const refreshData = useCallback(async () => {
setRefreshing(true);
try {
const data = await cronJobApi.getJobs();
const data = await cronJobApi.getJobs() as { list?: CronJobRecord[] };
setJobs(data.list || []);
} catch (error) {
console.error('Failed to load cron jobs:', error);
@@ -77,7 +78,7 @@ export function CronJobManager() {
(job.description || '').toLowerCase().includes(searchQuery.toLowerCase())
);
const handleToggleJob = async ( id: any) => {
const handleToggleJob = async ( id: number) => {
try {
await cronJobApi.toggleJob(id);
toast.success('状态已切换');
@@ -88,7 +89,7 @@ export function CronJobManager() {
}
};
const handleExecuteJob = async ( id: any) => {
const handleExecuteJob = async ( id: number) => {
setExecutingId(id);
try {
const result = await cronJobApi.executeJob(id);
@@ -108,7 +109,7 @@ export function CronJobManager() {
};
// 删除任务(带确认框) // Logan.Li
const handleDeleteJob = async ( job: any) => {
const handleDeleteJob = async ( job: CronJobRecord) => {
const ok = await _confirm({
title: '删除定时任务',
description: `确定删除「${job.name}」吗?此操作不可恢复。`,
@@ -13,8 +13,14 @@ import {
ServerStatusCard,
KubernetesClusterCard,
} from './DashboardRecent';
import type {
DashboardRelease,
DashboardServer,
DashboardCluster,
} from './DashboardRecent';
import { SkeletonDashboard } from '@/components/layout/SkeletonCard';
import type { JsonData } from '@/types/common';
import type { AlertEventRecord, CertExpiryRecord } from '@/types';
/**
* Dashboard - 优化后的仪表盘页面
@@ -32,11 +38,11 @@ export function Dashboard() {
const [initialLoading, setInitialLoading] = useState(true);
const [refreshing, setRefreshing] = useState(false);
const [stats, setStats] = useState<JsonData>(null);
const [servers, setServers] = useState<JsonData[]>([]);
const [clusters, setClusters] = useState<JsonData[]>([]);
const [alerts, setAlerts] = useState<JsonData[]>([]);
const [releases, setReleases] = useState<JsonData[]>([]);
const [expCerts, setExpCerts] = useState<JsonData[]>([]);
const [servers, setServers] = useState<DashboardServer[]>([]);
const [clusters, setClusters] = useState<DashboardCluster[]>([]);
const [alerts, setAlerts] = useState<AlertEventRecord[]>([]);
const [releases, setReleases] = useState<DashboardRelease[]>([]);
const [expCerts, setExpCerts] = useState<CertExpiryRecord[]>([]);
const [metrics, setMetrics] = useState<JsonData[]>([]);
const refreshData = useCallback(async () => {
@@ -13,6 +13,38 @@ import {
Circle,
} from 'lucide-react';
import { cn } from '@/lib/utils';
import type { AlertEventRecord, CertExpiryRecord } from '@/types';
/** 仪表盘卡片数据类型 — 与 Dashboard API 响应对齐 */
export interface DashboardRelease {
id: number | string;
status: string;
app_name: string;
version: string;
environment?: string;
commit_id?: string;
[key: string]: unknown;
}
export interface DashboardServer {
id: number | string;
status: string;
hostname?: string;
ip?: string;
[key: string]: unknown;
}
export interface DashboardCluster {
id: number | string;
code?: string;
name: string;
status: string;
cpu_usage?: number;
memory_usage?: number;
node_count?: number;
pod_count?: number;
[key: string]: unknown;
}
/**
* DashboardRecent - 优化后的仪表盘底部卡片组件
@@ -56,7 +88,7 @@ function SeverityBadge({ severity }: { severity: string }) {
);
}
export function AlertMessagesCard({ alerts }: { alerts: any[] }) {
export function AlertMessagesCard({ alerts }: { alerts: AlertEventRecord[] }) {
const navigate = useNavigate();
const recentAlerts = alerts.filter((a) => a.status === 'firing').slice(0, 8);
@@ -119,7 +151,7 @@ export function AlertMessagesCard({ alerts }: { alerts: any[] }) {
);
}
export function CertificateExpiryCard({ expCerts }: { expCerts: any[] }) {
export function CertificateExpiryCard({ expCerts }: { expCerts: CertExpiryRecord[] }) {
const expiringDomains = (expCerts ?? [])
.map((d) => ({
...d,
@@ -209,7 +241,7 @@ export function CertificateExpiryCard({ expCerts }: { expCerts: any[] }) {
);
}
export function ReleaseHistoryCard({ releases }: { releases: any[] }) {
export function ReleaseHistoryCard({ releases }: { releases: DashboardRelease[] }) {
const navigate = useNavigate();
const releaseList = releases?.slice(0, 5) ?? [];
@@ -299,7 +331,7 @@ export function ReleaseHistoryCard({ releases }: { releases: any[] }) {
);
}
export function ServerStatusCard({ servers }: { servers: any[] }) {
export function ServerStatusCard({ servers }: { servers: DashboardServer[] }) {
const navigate = useNavigate();
const runningServers = servers.filter((s) => s.status === 'running').length;
const stoppedServers = servers.filter((s) => s.status === 'stopped').length;
@@ -382,7 +414,7 @@ export function ServerStatusCard({ servers }: { servers: any[] }) {
);
}
export function KubernetesClusterCard({ clusters }: { clusters: any[] }) {
export function KubernetesClusterCard({ clusters }: { clusters: DashboardCluster[] }) {
const navigate = useNavigate();
return (
@@ -17,6 +17,7 @@ import {
import { Plus, Trash2, Loader2 } from 'lucide-react';
import { alertApi } from '@/api';
import { CHANNEL_TYPES, getChannelTypeLabel } from './alertShared';
import type { AlertChannel } from '@/types';
const defaultChannelForm = { name: '', type: 'dingtalk', config: '', enabled: true };
@@ -28,11 +29,11 @@ export function AlertChannelDialog({
}: {
open: boolean;
onOpenChange: (open: boolean) => void;
notifyChannels: any[];
notifyChannels: AlertChannel[];
onChannelsChanged: () => void;
}) {
const _confirm = useConfirm();
const [editingChannel, setEditingChannel] = useState<any>(null);
const [editingChannel, setEditingChannel] = useState<AlertChannel | null>(null);
const [channelSaving, setChannelSaving] = useState(false);
const [channelTesting, setChannelTesting] = useState<number | null>(null);
const [channelForm, setChannelForm] = useState({ ...defaultChannelForm });
@@ -45,7 +46,7 @@ export function AlertChannelDialog({
}
}, [open]);
const openEditChannel = ( ch: any) => {
const openEditChannel = ( ch: AlertChannel) => {
setEditingChannel(ch);
setChannelForm({
name: ch.name || '',
@@ -83,7 +84,7 @@ export function AlertChannelDialog({
}
};
const handleDeleteChannel = async ( id: any) => {
const handleDeleteChannel = async ( id: number) => {
{ const ok = await _confirm({ title: '删除通知渠道', description: '确定删除此通知渠道?', confirmText: '删除', variant: 'destructive' }); if (!ok) return; }
try {
await alertApi.deleteNotifyChannel(id);
@@ -94,7 +95,7 @@ export function AlertChannelDialog({
}
};
const handleToggleChannel = async ( ch: any) => {
const handleToggleChannel = async ( ch: AlertChannel) => {
try {
await alertApi.updateNotifyChannel(ch.id, { enabled: !ch.enabled });
onChannelsChanged();
@@ -103,7 +104,7 @@ export function AlertChannelDialog({
}
};
const handleTestChannel = async ( id: any) => {
const handleTestChannel = async ( id: number) => {
setChannelTesting(id);
try {
await alertApi.testNotifyChannel(id);
@@ -14,13 +14,14 @@ import {
} from '@/components/ui/table';
import { Search, AlertTriangle, Bell, CheckCircle, Loader2 } from 'lucide-react';
import { alertApi } from '@/api';
import type { AlertEventRecord } from '@/types';
export function AlertEventsTab({
alerts,
loading,
onReload,
}: {
alerts: any[];
alerts: AlertEventRecord[];
loading: boolean;
onReload: () => void;
}) {
@@ -38,7 +39,7 @@ export function AlertEventsTab({
return matchesSearch && matchesSeverity && matchesStatus;
});
const getSeverityBadge = ( severity: any) => {
const getSeverityBadge = ( severity: string) => {
switch (severity) {
case 'critical':
return (
@@ -66,7 +67,7 @@ export function AlertEventsTab({
}
};
const getStatusBadge = ( status: any) => {
const getStatusBadge = ( status: string) => {
switch (status) {
case 'firing':
return (
@@ -85,7 +86,7 @@ export function AlertEventsTab({
}
};
const handleAcknowledge = async ( id: any) => {
const handleAcknowledge = async ( id: number) => {
try {
await alertApi.acknowledge(id);
onReload();
@@ -18,16 +18,17 @@ import { AlertRulesTab } from './AlertRulesTab';
import { AlertRuleDialog } from './AlertRuleDialog';
import { AlertChannelDialog } from './AlertChannelDialog';
import type { JsonData } from '@/types/common';
import type { AlertEventRecord, AlertRuleRecord, AlertChannel } from '@/types';
export function AlertManager() {
const [activeTab, setActiveTab] = useState('events');
const [alerts, setAlerts] = useState<JsonData[]>([]);
const [rules, setRules] = useState<JsonData[]>([]);
const [alerts, setAlerts] = useState<AlertEventRecord[]>([]);
const [rules, setRules] = useState<AlertRuleRecord[]>([]);
const [loading, setLoading] = useState(true);
const [ruleDialogOpen, setRuleDialogOpen] = useState(false);
const [editingRule, setEditingRule] = useState<JsonData>(null);
const [editingRule, setEditingRule] = useState<AlertRuleRecord | null>(null);
const [channelDialogOpen, setChannelDialogOpen] = useState(false);
const [notifyChannels, setNotifyChannels] = useState<JsonData[]>([]);
const [notifyChannels, setNotifyChannels] = useState<AlertChannel[]>([]);
const loadData = async () => {
try {
@@ -56,7 +57,7 @@ export function AlertManager() {
setRuleDialogOpen(true);
};
const openEditRule = (rule: JsonData) => {
const openEditRule = (rule: AlertRuleRecord) => {
setEditingRule(rule);
setRuleDialogOpen(true);
};
@@ -17,6 +17,7 @@ import { Settings, Trash2 } from 'lucide-react';
import { toast } from 'sonner';
import { alertApi } from '@/api';
import { renderRuleChannels } from './alertShared';
import type { AlertChannel, AlertRuleRecord } from '@/types';
const COL_SPAN = 9; // 复选框 + 7 数据列 + 操作列 // Logan.Li
@@ -26,10 +27,10 @@ export function AlertRulesTab({
onReload,
onEditRule,
}: {
rules: any[];
notifyChannels: any[];
rules: AlertRuleRecord[];
notifyChannels: AlertChannel[];
onReload: () => void;
onEditRule: ( rule: any) => void;
onEditRule: ( rule: AlertRuleRecord) => void;
}) {
const confirm = useConfirm();
const [selectedIds, setSelectedIds] = useState<number[]>([]);
@@ -50,7 +51,7 @@ export function AlertRulesTab({
);
};
const handleDeleteRule = async ( id: any) => {
const handleDeleteRule = async ( id: number) => {
const ok = await confirm({
title: '删除告警规则',
description: '确定删除此告警规则?',
@@ -89,7 +90,7 @@ export function AlertRulesTab({
}
};
const handleToggleRule = async ( id: any) => {
const handleToggleRule = async ( id: number) => {
try {
await alertApi.toggleRule(id);
onReload();
@@ -1,7 +1,8 @@
import { Mail, Phone, MessageSquare, Bell } from 'lucide-react';
import type { AlertChannel, AlertRuleRecord } from '@/types';
// 将 notify_channels (可能是数组/JSON字符串/逗号分隔) 解析为数组
export function parseChannels( notifyChannels: any): string[] {
export function parseChannels( notifyChannels: unknown): string[] {
if (!notifyChannels) return [];
if (Array.isArray(notifyChannels)) return notifyChannels;
if (typeof notifyChannels === 'string') {
@@ -30,7 +31,7 @@ export const CHANNEL_TYPE_LABELS: Record<string, string> = {
// 解析单个 notify_channels 引用值 — 返回 {id?, type, name, isLegacy}
// 新格式存渠道 ID (string),旧格式存类型字符串 (email/dingtalk 等)
export function resolveChannelRef(value: string, channels: any[]): { type: string; name: string; isLegacy: boolean } | null {
export function resolveChannelRef(value: string, channels: AlertChannel[]): { type: string; name: string; isLegacy: boolean } | null {
if (!value) return null;
// 尝试按 ID 匹配已配置渠道
const ch = channels.find((c) => String(c.id) === String(value));
@@ -71,7 +72,7 @@ export function getChannelIcon(type: string) {
}
// 渲染规则的通知渠道引用列表(兼容新格式=渠道ID 和 旧格式=类型字符串)
export function renderRuleChannels( rule: any, notifyChannels: any[]) {
export function renderRuleChannels( rule: AlertRuleRecord, notifyChannels: AlertChannel[]) {
const refs = parseChannels(rule.notify_channels);
if (refs.length === 0) return <span className="text-xs text-slate-600">--</span>;
return (
+19 -17
View File
@@ -22,22 +22,22 @@ import {
import { auditApi } from '@/api';
import { useAuditLogs } from '@/hooks/api';
import { useConfirm } from '@/components/ui/confirm-dialog';
import type { OperationLog } from '@/types';
import type { OperationLog, OptionItem } from '@/types';
// Normalize backend log to frontend type
function normalizeLog(raw: any): OperationLog {
function normalizeLog(raw: Record<string, unknown>): OperationLog {
return {
id: String(raw.id ?? ''),
userId: String(raw.user_id ?? raw.userId ?? ''),
username: raw.username ?? '',
module: raw.module ?? raw.resource ?? '',
action: raw.action ?? '',
resource: raw.resource ?? '',
ip: raw.ip ?? '',
userAgent: raw.user_agent ?? raw.userAgent ?? '',
username: String(raw.username ?? ''),
module: String(raw.module ?? raw.resource ?? ''),
action: String(raw.action ?? ''),
resource: String(raw.resource ?? ''),
ip: String(raw.ip ?? ''),
userAgent: String(raw.user_agent ?? raw.userAgent ?? ''),
result: raw.result === 'failure' ? 'failure' : 'success',
message: raw.message ?? raw.detail ?? '',
createdAt: raw.createdAt ?? raw.created_at ?? '',
message: String(raw.message ?? raw.detail ?? ''),
createdAt: String(raw.createdAt ?? raw.created_at ?? ''),
};
}
@@ -70,8 +70,8 @@ export function AuditOperationsTab() {
() => (auditData?.list || []).map((raw) => normalizeLog(raw)),
[auditData],
);
const [modules, setModules] = useState<string[]>([]);
const [actions, setActions] = useState<string[]>([]);
const [modules, setModules] = useState<OptionItem[]>([]);
const [actions, setActions] = useState<OptionItem[]>([]);
// Detail modal
const [detailLog, setDetailLog] = useState<OperationLog | null>(null);
@@ -84,8 +84,10 @@ export function AuditOperationsTab() {
const fetchOptions = useCallback(async () => {
try {
const [modRes, actRes] = await Promise.all([auditApi.getModules(), auditApi.getActions()]);
setModules((modRes as any)?.list ?? (modRes as any) ?? []);
setActions((actRes as any)?.list ?? (actRes as any) ?? []);
const modData = modRes as { list?: OptionItem[] } | OptionItem[];
const actData = actRes as { list?: OptionItem[] } | OptionItem[];
setModules(Array.isArray(modData) ? modData : (modData?.list ?? []));
setActions(Array.isArray(actData) ? actData : (actData?.list ?? []));
} catch {
/* noop */
}
@@ -120,7 +122,7 @@ export function AuditOperationsTab() {
const handleExport = async () => {
try {
const params: any = {};
const params: Record<string, string> = {};
if (moduleFilter) params.module = moduleFilter;
if (actionFilter) params.action = actionFilter;
await auditApi.exportLogs(params);
@@ -167,7 +169,7 @@ export function AuditOperationsTab() {
className="h-9 px-3 text-sm rounded-md bg-slate-800/50 border border-slate-700 text-slate-300 focus:outline-none focus:border-blue-500"
>
<option value="">全部模块</option>
{modules.map((m: any) => (
{modules.map((m) => (
<option key={m.value} value={m.value}>
{m.label}
</option>
@@ -181,7 +183,7 @@ export function AuditOperationsTab() {
className="h-9 px-3 text-sm rounded-md bg-slate-800/50 border border-slate-700 text-slate-300 focus:outline-none focus:border-blue-500"
>
<option value="">全部操作</option>
{actions.map((a: any) => (
{actions.map((a) => (
<option key={a.value} value={a.value}>
{a.label}
</option>
+6
View File
@@ -37,6 +37,12 @@ export const ErrorCode = {
INTERNAL_ERROR: 500,
} as const;
// 下拉选项通用类型(模块/操作类型等筛选器)
export interface OptionItem {
value: string;
label: string;
}
// WebSocket 消息
export interface WsMessage<T = unknown> {
type: string;
+111
View File
@@ -175,6 +175,7 @@ export interface AlertChannel {
config: string; // JSON string
enabled: boolean;
description: string;
test_status?: string;
}
// ===== 健康探针 =====
@@ -371,3 +372,113 @@ export interface GrafanaDashboard {
createdBy: string;
isDefault: boolean;
}
// ===== 运行时记录类型(与后端 API 响应 JSON 对齐,snake_case)=====
/** 告警事件运行时记录 — GET /alerts 返回,字段高度动态 */
export interface AlertEventRecord {
id: number;
rule?: { id: number; name: string };
rule_id?: number;
rule_name?: string;
severity: string;
status: string;
message: string;
target: string;
value?: number;
threshold?: number;
started_at?: string;
resolved_at?: string;
created_at?: string;
acked?: boolean;
}
/** 告警规则运行时记录 — GET /alerts/rules 返回 */
export interface AlertRuleRecord {
id: number;
name: string;
metric?: string;
operator?: string;
threshold?: number;
duration?: number;
duration_sec?: number;
severity: string;
enabled: boolean;
description?: string;
notify_channels?: string | (number | string)[];
server_id?: number;
server_hostname?: string;
type?: string;
source?: string;
expression?: string;
created_at?: string;
updated_at?: string;
}
/** Agent 主机运行时记录 — GET /agent/latest 返回 */
export interface AgentHostRecord {
id?: number;
hostname: string;
ip?: string;
agent_version?: string;
os?: string;
kernel?: string;
status?: string;
agent_status?: string;
last_seen?: string;
last_report?: string;
last_reported_at?: string;
updated_at?: string;
cpu_percent?: number;
cpu?: number;
mem_percent?: number;
mem?: number;
disk_percent?: number;
disk?: number;
ports?: unknown[];
[key: string]: unknown;
}
/** 定时任务运行时记录 — GET /cron-jobs 返回 */
export interface CronJobRecord {
id: number;
name: string;
description?: string;
enabled: boolean;
cron?: string;
cron_expr?: string;
command?: string;
content?: string;
script?: { name: string; [key: string]: unknown } | null;
type?: string;
target_type?: string;
targets?: string[];
success_count?: number;
fail_count?: number;
run_count?: number;
created_at?: string;
updated_at?: string;
last_run_at?: string;
last_status?: string;
[key: string]: unknown;
}
/** 域名证书到期记录 */
export interface CertExpiryRecord {
id?: number | string;
domain: string;
name?: string;
expire_date: string;
days_until_expiry?: number;
[key: string]: unknown;
}
/** 审批工作流步骤运行时记录 */
export interface ApprovalWorkflowStepRecord {
id?: number;
step_order: number;
step_type: 'submit' | 'audit' | 'execute' | string;
name: string;
approver_ids?: number[];
description?: string;
}