fix: reduce db query
This commit is contained in:
1 parent
596626565c
commit
4d6c53f0f8
20 files changed
+638
-599
No files matched your search
@@ -55,6 +55,21 @@ CREATE TABLE IF NOT EXISTS monitor_status_history (
|
||||
FOREIGN KEY (monitor_id) REFERENCES monitors(id)
|
||||
);
|
||||
|
||||
-- 监控每日统计表
|
||||
CREATE TABLE IF NOT EXISTS monitor_daily_stats (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
monitor_id INTEGER NOT NULL,
|
||||
date TEXT NOT NULL,
|
||||
total_checks INTEGER NOT NULL DEFAULT 0,
|
||||
up_checks INTEGER NOT NULL DEFAULT 0,
|
||||
down_checks INTEGER NOT NULL DEFAULT 0,
|
||||
avg_response_time INTEGER DEFAULT 0,
|
||||
min_response_time INTEGER DEFAULT 0,
|
||||
max_response_time INTEGER DEFAULT 0,
|
||||
availability REAL DEFAULT 0,
|
||||
FOREIGN KEY (monitor_id) REFERENCES monitors(id)
|
||||
);
|
||||
|
||||
-- 客户端表
|
||||
CREATE TABLE IF NOT EXISTS agents (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
|
||||
+61
-47
@@ -9,43 +9,27 @@ monitors.get("/", async (c) => {
|
||||
const payload = c.get("jwtPayload");
|
||||
|
||||
// 调用服务层获取监控列表
|
||||
const result = await MonitorService.getAllMonitors(
|
||||
c.env.DB,
|
||||
payload.id,
|
||||
payload.role
|
||||
);
|
||||
const result = await MonitorService.getAllMonitors(c.env.DB);
|
||||
|
||||
return c.json(
|
||||
{
|
||||
success: result.success,
|
||||
monitors: result.monitors,
|
||||
message: result.message,
|
||||
},
|
||||
result.status as any
|
||||
);
|
||||
});
|
||||
|
||||
// 获取单个监控
|
||||
monitors.get("/:id", async (c) => {
|
||||
const id = parseInt(c.req.param("id"));
|
||||
const payload = c.get("jwtPayload");
|
||||
// 获取所有监控的每日统计数据
|
||||
monitors.get("/daily", async (c) => {
|
||||
// 调用服务层获取所有监控的每日统计数据
|
||||
const result = await MonitorService.getAllMonitorDailyStats(c.env.DB);
|
||||
|
||||
// 调用服务层获取监控详情
|
||||
const result = await MonitorService.getMonitorById(
|
||||
c.env.DB,
|
||||
id,
|
||||
payload.id,
|
||||
payload.role
|
||||
);
|
||||
|
||||
return c.json(
|
||||
{
|
||||
success: result.success,
|
||||
monitor: result.monitor,
|
||||
message: result.message,
|
||||
},
|
||||
result.status as any
|
||||
);
|
||||
return c.json({
|
||||
success: result.success,
|
||||
dailyStats: result.dailyStats,
|
||||
message: result.message,
|
||||
});
|
||||
});
|
||||
|
||||
// 创建监控
|
||||
@@ -66,6 +50,38 @@ monitors.post("/", async (c) => {
|
||||
);
|
||||
});
|
||||
|
||||
// 获取所有监控状态历史
|
||||
monitors.get("/history", async (c) => {
|
||||
// 调用服务层获取监控历史
|
||||
const result = await MonitorService.getAllMonitorStatusHistory(c.env.DB);
|
||||
|
||||
return c.json(
|
||||
{
|
||||
success: result.success,
|
||||
history: result.history,
|
||||
},
|
||||
result.status as any
|
||||
);
|
||||
});
|
||||
|
||||
// 获取单个监控
|
||||
monitors.get("/:id", async (c) => {
|
||||
const id = parseInt(c.req.param("id"));
|
||||
const payload = c.get("jwtPayload");
|
||||
|
||||
// 调用服务层获取监控详情
|
||||
const result = await MonitorService.getMonitorById(c.env.DB, id);
|
||||
|
||||
return c.json(
|
||||
{
|
||||
success: result.success,
|
||||
monitor: result.monitor,
|
||||
message: result.message,
|
||||
},
|
||||
result.status as any
|
||||
);
|
||||
});
|
||||
|
||||
// 更新监控
|
||||
monitors.put("/:id", async (c) => {
|
||||
const id = parseInt(c.req.param("id"));
|
||||
@@ -73,13 +89,7 @@ monitors.put("/:id", async (c) => {
|
||||
const data = await c.req.json();
|
||||
|
||||
// 调用服务层更新监控
|
||||
const result = await MonitorService.updateMonitor(
|
||||
c.env.DB,
|
||||
id,
|
||||
data,
|
||||
payload.id,
|
||||
payload.role
|
||||
);
|
||||
const result = await MonitorService.updateMonitor(c.env.DB, id, data);
|
||||
|
||||
return c.json(
|
||||
{
|
||||
@@ -97,12 +107,7 @@ monitors.delete("/:id", async (c) => {
|
||||
const payload = c.get("jwtPayload");
|
||||
|
||||
// 调用服务层删除监控
|
||||
const result = await MonitorService.deleteMonitor(
|
||||
c.env.DB,
|
||||
id,
|
||||
payload.id,
|
||||
payload.role
|
||||
);
|
||||
const result = await MonitorService.deleteMonitor(c.env.DB, id);
|
||||
|
||||
return c.json(
|
||||
{
|
||||
@@ -113,7 +118,7 @@ monitors.delete("/:id", async (c) => {
|
||||
);
|
||||
});
|
||||
|
||||
// 获取监控状态历史
|
||||
// 获取单个监控状态历史
|
||||
monitors.get("/:id/history", async (c) => {
|
||||
const id = parseInt(c.req.param("id"));
|
||||
const payload = c.get("jwtPayload");
|
||||
@@ -135,19 +140,28 @@ monitors.get("/:id/history", async (c) => {
|
||||
result.status as any
|
||||
);
|
||||
});
|
||||
|
||||
// 获取单个监控的每日统计数据
|
||||
monitors.get("/:id/daily", async (c) => {
|
||||
const id = parseInt(c.req.param("id"));
|
||||
|
||||
// 调用服务层获取每日统计数据
|
||||
const result = await MonitorService.getMonitorDailyStats(c.env.DB, id);
|
||||
|
||||
return c.json({
|
||||
success: result.success,
|
||||
dailyStats: result.dailyStats,
|
||||
message: result.message,
|
||||
});
|
||||
});
|
||||
|
||||
// 手动检查单个监控
|
||||
monitors.post("/:id/check", async (c) => {
|
||||
const id = parseInt(c.req.param("id"));
|
||||
const payload = c.get("jwtPayload");
|
||||
|
||||
// 调用服务层手动检查监控
|
||||
const result = await MonitorService.manualCheckMonitor(
|
||||
c.env.DB,
|
||||
id,
|
||||
payload.id,
|
||||
payload.role,
|
||||
c.env
|
||||
);
|
||||
const result = await MonitorService.manualCheckMonitor(c.env.DB, id, c.env);
|
||||
|
||||
return c.json(
|
||||
{
|
||||
|
||||
@@ -24,6 +24,11 @@ export async function createTables(env: Bindings): Promise<void> {
|
||||
"CREATE TABLE IF NOT EXISTS monitor_status_history (id INTEGER PRIMARY KEY AUTOINCREMENT, monitor_id INTEGER NOT NULL, status TEXT NOT NULL, timestamp TEXT DEFAULT CURRENT_TIMESTAMP, response_time INTEGER, status_code INTEGER, error TEXT, FOREIGN KEY (monitor_id) REFERENCES monitors(id))"
|
||||
);
|
||||
|
||||
console.log("创建监控每日统计表...");
|
||||
await env.DB.exec(
|
||||
"CREATE TABLE IF NOT EXISTS monitor_daily_stats (id INTEGER PRIMARY KEY AUTOINCREMENT, monitor_id INTEGER NOT NULL, date TEXT NOT NULL, total_checks INTEGER NOT NULL DEFAULT 0, up_checks INTEGER NOT NULL DEFAULT 0, down_checks INTEGER NOT NULL DEFAULT 0, avg_response_time INTEGER DEFAULT 0, min_response_time INTEGER DEFAULT 0, max_response_time INTEGER DEFAULT 0, availability REAL DEFAULT 0, created_at TEXT NOT NULL, FOREIGN KEY (monitor_id) REFERENCES monitors(id))"
|
||||
);
|
||||
|
||||
console.log("创建客户端表...");
|
||||
await env.DB.exec(
|
||||
"CREATE TABLE IF NOT EXISTS agents (id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL, token TEXT NOT NULL UNIQUE, created_by INTEGER NOT NULL, status TEXT DEFAULT 'inactive', created_at TEXT NOT NULL, updated_at TEXT NOT NULL, hostname TEXT, ip_addresses TEXT, os TEXT, version TEXT, cpu_usage REAL, memory_total INTEGER, memory_used INTEGER, disk_total INTEGER, disk_used INTEGER, network_rx INTEGER, network_tx INTEGER, FOREIGN KEY (created_by) REFERENCES users(id))"
|
||||
|
||||
@@ -49,6 +49,7 @@ export async function checkAndInitializeDatabase(
|
||||
"notification_templates",
|
||||
"notification_settings",
|
||||
"notification_history",
|
||||
"monitor_daily_stats",
|
||||
];
|
||||
|
||||
// 检查每个表是否存在
|
||||
@@ -90,70 +91,6 @@ export async function checkAndInitializeDatabase(
|
||||
initialized = true;
|
||||
}
|
||||
|
||||
// 检查用户表是否存在并且为空,如果为空则创建管理员用户
|
||||
if (await tableExists(env, "users")) {
|
||||
const userCount = await env.DB.prepare(
|
||||
"SELECT COUNT(*) as count FROM users"
|
||||
).first<{ count: number }>();
|
||||
if (!userCount || userCount.count === 0) {
|
||||
console.log("用户表为空,创建管理员用户...");
|
||||
await createAdminUser(env);
|
||||
initialized = true;
|
||||
} else {
|
||||
console.log("用户表已有数据,跳过创建管理员用户...");
|
||||
}
|
||||
} else {
|
||||
console.log("用户表不存在,跳过检查用户数据...");
|
||||
}
|
||||
|
||||
// 检查状态页配置表是否存在并且为空,如果为空则创建默认状态页
|
||||
if (await tableExists(env, "status_page_config")) {
|
||||
const statusPageCount = await env.DB.prepare(
|
||||
"SELECT COUNT(*) as count FROM status_page_config"
|
||||
).first<{ count: number }>();
|
||||
if (!statusPageCount || statusPageCount.count === 0) {
|
||||
console.log("状态页配置表为空,创建默认状态页...");
|
||||
await createDefaultStatusPage(env);
|
||||
initialized = true;
|
||||
} else {
|
||||
console.log("状态页配置表已有数据,跳过创建默认状态页...");
|
||||
}
|
||||
} else {
|
||||
console.log("状态页配置表不存在,跳过检查状态页数据...");
|
||||
}
|
||||
|
||||
// 检查通知模板表是否存在并且为空,如果为空则创建默认通知模板
|
||||
if (await tableExists(env, "notification_templates")) {
|
||||
const templateCount = await env.DB.prepare(
|
||||
"SELECT COUNT(*) as count FROM notification_templates"
|
||||
).first<{ count: number }>();
|
||||
if (!templateCount || templateCount.count === 0) {
|
||||
console.log("通知模板表为空,创建默认通知模板...");
|
||||
await createNotificationTemplates(env);
|
||||
initialized = true;
|
||||
} else {
|
||||
console.log("通知模板表已有数据,跳过创建默认通知模板...");
|
||||
}
|
||||
} else {
|
||||
console.log("通知模板表不存在,跳过检查通知模板数据...");
|
||||
}
|
||||
|
||||
// 检查通知渠道表是否存在并且为空,如果为空则创建默认通知渠道和设置
|
||||
if (await tableExists(env, "notification_channels")) {
|
||||
const channelCount = await env.DB.prepare(
|
||||
"SELECT COUNT(*) as count FROM notification_channels"
|
||||
).first<{ count: number }>();
|
||||
if (!channelCount || channelCount.count === 0) {
|
||||
console.log("通知渠道表为空,创建默认通知渠道和设置...");
|
||||
await createNotificationChannelsAndSettings(env);
|
||||
initialized = true;
|
||||
} else {
|
||||
console.log("通知渠道表已有数据,跳过创建默认通知渠道和设置...");
|
||||
}
|
||||
} else {
|
||||
console.log("通知渠道表不存在,跳过检查通知渠道数据...");
|
||||
}
|
||||
|
||||
// 执行迁移
|
||||
await runMigrations(env);
|
||||
|
||||
|
||||
@@ -159,11 +159,220 @@ async function handleMonitorNotification(
|
||||
}
|
||||
}
|
||||
|
||||
// 生成每日监控统计数据的函数
|
||||
async function generateDailyStats(c: any) {
|
||||
try {
|
||||
console.log("开始生成每日监控统计数据...");
|
||||
|
||||
// 获取前一天的日期 (YYYY-MM-DD 格式)
|
||||
const yesterday = new Date();
|
||||
yesterday.setDate(yesterday.getDate() - 1);
|
||||
const dateStr = yesterday.toISOString().split('T')[0];
|
||||
|
||||
console.log(`正在处理日期 ${dateStr} 的数据`);
|
||||
|
||||
// 时间范围
|
||||
const startTime = `${dateStr}T00:00:00.000Z`;
|
||||
const endTime = `${dateStr}T23:59:59.999Z`;
|
||||
|
||||
// 一次性获取所有监控
|
||||
const monitorsResult = await c.env.DB.prepare(
|
||||
"SELECT id, name FROM monitors"
|
||||
).all();
|
||||
|
||||
if (!monitorsResult.results || monitorsResult.results.length === 0) {
|
||||
console.log("没有找到监控");
|
||||
return { success: true, message: "没有监控", processed: 0 };
|
||||
}
|
||||
|
||||
const monitors = monitorsResult.results;
|
||||
console.log(`找到 ${monitors.length} 个监控`);
|
||||
|
||||
// 创建监控ID列表
|
||||
const monitorIds = monitors.map((m: any) => m.id);
|
||||
|
||||
// 一次性获取所有监控历史记录(指定日期范围内)
|
||||
console.log(`一次性查询所有监控在 ${startTime} 至 ${endTime} 的历史记录`);
|
||||
|
||||
const historyResult = await c.env.DB.prepare(`
|
||||
SELECT
|
||||
monitor_id,
|
||||
status,
|
||||
response_time
|
||||
FROM
|
||||
monitor_status_history
|
||||
WHERE
|
||||
timestamp >= ? AND
|
||||
timestamp <= ?
|
||||
`)
|
||||
.bind(startTime, endTime)
|
||||
.all();
|
||||
|
||||
if (!historyResult.results || historyResult.results.length === 0) {
|
||||
console.log(`在 ${dateStr} 没有找到任何监控历史记录`);
|
||||
return { success: true, message: "没有历史记录", processed: 0 };
|
||||
}
|
||||
|
||||
console.log(`找到 ${historyResult.results.length} 条历史记录`);
|
||||
|
||||
// 按监控ID分组处理数据
|
||||
const statsMap = new Map();
|
||||
|
||||
// 初始化每个监控的统计数据结构
|
||||
for (const monitorId of monitorIds) {
|
||||
statsMap.set(monitorId, {
|
||||
monitorId,
|
||||
totalChecks: 0,
|
||||
upChecks: 0,
|
||||
downChecks: 0,
|
||||
responseTimes: [],
|
||||
avgResponseTime: 0,
|
||||
minResponseTime: 0,
|
||||
maxResponseTime: 0,
|
||||
availability: 0
|
||||
});
|
||||
}
|
||||
|
||||
// 处理所有历史记录
|
||||
for (const record of historyResult.results) {
|
||||
const monitorId = record.monitor_id;
|
||||
|
||||
// 如果这个监控ID不在我们的列表中,跳过
|
||||
if (!statsMap.has(monitorId)) continue;
|
||||
|
||||
const stats = statsMap.get(monitorId);
|
||||
stats.totalChecks++;
|
||||
|
||||
// 统计状态
|
||||
if (record.status === 'up') {
|
||||
stats.upChecks++;
|
||||
} else if (record.status === 'down') {
|
||||
stats.downChecks++;
|
||||
}
|
||||
|
||||
// 收集响应时间数据
|
||||
if (record.response_time != null && record.response_time > 0) {
|
||||
stats.responseTimes.push(record.response_time);
|
||||
}
|
||||
}
|
||||
|
||||
// 处理每个监控的响应时间统计和可用率计算
|
||||
for (const [monitorId, stats] of statsMap.entries()) {
|
||||
// 只处理有记录的监控
|
||||
if (stats.totalChecks === 0) continue;
|
||||
|
||||
// 计算响应时间统计数据
|
||||
if (stats.responseTimes.length > 0) {
|
||||
stats.avgResponseTime = stats.responseTimes.reduce((sum: number, time: number) => sum + time, 0) / stats.responseTimes.length;
|
||||
stats.minResponseTime = Math.min(...stats.responseTimes);
|
||||
stats.maxResponseTime = Math.max(...stats.responseTimes);
|
||||
}
|
||||
|
||||
// 计算可用率
|
||||
stats.availability = stats.totalChecks > 0 ? (stats.upChecks / stats.totalChecks) * 100 : 0;
|
||||
|
||||
// 删除临时数据
|
||||
delete stats.responseTimes;
|
||||
}
|
||||
|
||||
// 将数据写入数据库
|
||||
const now = new Date().toISOString();
|
||||
let processed = 0;
|
||||
|
||||
// 批量构建 INSERT 语句
|
||||
for (const [monitorId, stats] of statsMap.entries()) {
|
||||
// 跳过没有记录的监控
|
||||
if (stats.totalChecks === 0) continue;
|
||||
|
||||
const monitor = monitors.find((m: any) => m.id === monitorId);
|
||||
const monitorName = monitor ? monitor.name : `ID: ${monitorId}`;
|
||||
|
||||
try {
|
||||
console.log(`监控 ${monitorName} (ID: ${monitorId}) 数据: 总检查=${stats.totalChecks}, 正常=${stats.upChecks}, 故障=${stats.downChecks}, 可用率=${stats.availability.toFixed(2)}%`);
|
||||
|
||||
// 使用 UPSERT 操作
|
||||
await c.env.DB.prepare(`
|
||||
INSERT INTO monitor_daily_stats (
|
||||
monitor_id,
|
||||
date,
|
||||
total_checks,
|
||||
up_checks,
|
||||
down_checks,
|
||||
avg_response_time,
|
||||
min_response_time,
|
||||
max_response_time,
|
||||
availability,
|
||||
created_at
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
ON CONFLICT(monitor_id, date) DO UPDATE SET
|
||||
total_checks = excluded.total_checks,
|
||||
up_checks = excluded.up_checks,
|
||||
down_checks = excluded.down_checks,
|
||||
avg_response_time = excluded.avg_response_time,
|
||||
min_response_time = excluded.min_response_time,
|
||||
max_response_time = excluded.max_response_time,
|
||||
availability = excluded.availability
|
||||
`).bind(
|
||||
monitorId,
|
||||
dateStr,
|
||||
stats.totalChecks,
|
||||
stats.upChecks,
|
||||
stats.downChecks,
|
||||
stats.avgResponseTime,
|
||||
stats.minResponseTime,
|
||||
stats.maxResponseTime,
|
||||
stats.availability,
|
||||
now
|
||||
).run();
|
||||
|
||||
processed++;
|
||||
console.log(`成功更新监控 ID ${monitorId} 的每日统计数据`);
|
||||
} catch (error) {
|
||||
console.error(`更新监控 ID ${monitorId} 的每日统计数据时出错:`, error);
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`每日统计数据生成完成,成功处理了 ${processed} 个监控`);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: "每日统计数据生成完成",
|
||||
processed: processed,
|
||||
date: dateStr
|
||||
};
|
||||
} catch (error) {
|
||||
console.error("生成每日统计数据时出错:", error);
|
||||
return {
|
||||
success: false,
|
||||
message: "生成每日统计数据时出错",
|
||||
error: String(error)
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// 在 Cloudflare Workers 中设置定时触发器
|
||||
export default {
|
||||
async scheduled(event: any, env: any, ctx: any) {
|
||||
const c = { env };
|
||||
await checkMonitors(c);
|
||||
|
||||
// 根据CRON表达式的触发时间决定执行哪个任务
|
||||
const hour = new Date().getUTCHours();
|
||||
const minute = new Date().getUTCMinutes();
|
||||
|
||||
console.log(`定时任务触发,当前UTC时间: ${hour}:${minute}`);
|
||||
|
||||
// 默认执行监控检查任务
|
||||
let result: any = await checkMonitors(c);
|
||||
|
||||
// 在每天凌晨00:05左右额外执行每日统计生成任务
|
||||
// 由于定时任务精度的问题,检查0点左右的所有触发
|
||||
if (hour === 0 && minute >= 0 && minute <= 10) {
|
||||
console.log("执行每日统计数据生成任务");
|
||||
const statsResult = await generateDailyStats(c);
|
||||
result = { monitorCheck: result, dailyStats: statsResult };
|
||||
}
|
||||
|
||||
return result;
|
||||
},
|
||||
fetch: monitorTask.fetch,
|
||||
};
|
||||
@@ -29,3 +29,17 @@ export interface MonitorStatusHistory {
|
||||
status_code: number;
|
||||
error: string | null;
|
||||
}
|
||||
|
||||
// 监控每日统计类型
|
||||
export interface MonitorDailyStats {
|
||||
date: string;
|
||||
total_checks: number;
|
||||
up_checks: number;
|
||||
down_checks: number;
|
||||
avg_response_time: number;
|
||||
min_response_time: number;
|
||||
max_response_time: number;
|
||||
availability: number;
|
||||
monitor_id: number;
|
||||
created_at: string;
|
||||
}
|
||||
@@ -3,6 +3,7 @@ import {
|
||||
Monitor,
|
||||
MonitorStatusHistory,
|
||||
DbResultMeta,
|
||||
MonitorDailyStats,
|
||||
} from "../models";
|
||||
|
||||
/**
|
||||
@@ -94,6 +95,32 @@ export async function getMonitorById(db: Bindings["DB"], id: number) {
|
||||
return monitor;
|
||||
}
|
||||
|
||||
// 获取所有监控
|
||||
export async function getAllMonitors(db: Bindings["DB"]) {
|
||||
const result = await db
|
||||
.prepare("SELECT * FROM monitors ORDER BY created_at DESC")
|
||||
.all<Monitor>();
|
||||
|
||||
// 解析所有监控的 headers 字段
|
||||
if (result.results) {
|
||||
result.results.forEach((monitor) => {
|
||||
if (typeof monitor.headers === "string") {
|
||||
try {
|
||||
monitor.headers = JSON.parse(monitor.headers);
|
||||
} catch (e) {
|
||||
monitor.headers = {};
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
monitors: result.results || [],
|
||||
status: 200,
|
||||
};
|
||||
}
|
||||
|
||||
// 批量获取监控项详情
|
||||
export async function getMonitorsByIds(
|
||||
db: Bindings["DB"],
|
||||
@@ -110,7 +137,7 @@ export async function getMonitorsByIds(
|
||||
.all<Monitor>();
|
||||
}
|
||||
|
||||
// 获取监控状态历史
|
||||
// 获取单个监控状态历史 24小时内
|
||||
export async function getMonitorStatusHistory(
|
||||
db: Bindings["DB"],
|
||||
monitorId: number
|
||||
@@ -119,12 +146,23 @@ export async function getMonitorStatusHistory(
|
||||
.prepare(
|
||||
`SELECT * FROM monitor_status_history
|
||||
WHERE monitor_id = ?
|
||||
AND timestamp > datetime('now', '-24 hours')
|
||||
ORDER BY timestamp ASC`
|
||||
)
|
||||
.bind(monitorId)
|
||||
.all<MonitorStatusHistory>();
|
||||
}
|
||||
|
||||
// 获取所有监控状态历史 24小时内
|
||||
export async function getAllMonitorStatusHistory(db: Bindings["DB"]) {
|
||||
return await db
|
||||
.prepare(
|
||||
`SELECT * FROM monitor_status_history
|
||||
WHERE timestamp > datetime('now', '-24 hours')
|
||||
ORDER BY timestamp ASC`
|
||||
)
|
||||
.all<MonitorStatusHistory>();
|
||||
}
|
||||
// 记录监控状态历史
|
||||
export async function insertMonitorStatusHistory(
|
||||
db: Bindings["DB"],
|
||||
@@ -175,46 +213,6 @@ export async function updateMonitorStatus(
|
||||
.run();
|
||||
}
|
||||
|
||||
// 记录监控出错状态
|
||||
export async function recordMonitorError(
|
||||
db: Bindings["DB"],
|
||||
monitorId: number,
|
||||
response_time: number,
|
||||
errorMessage: string
|
||||
) {
|
||||
// 记录错误状态
|
||||
await insertMonitorStatusHistory(
|
||||
db,
|
||||
monitorId,
|
||||
"down",
|
||||
response_time,
|
||||
0,
|
||||
errorMessage
|
||||
);
|
||||
|
||||
// 使用ISO格式的时间戳
|
||||
const now = new Date().toISOString();
|
||||
|
||||
// 更新监控状态
|
||||
return await db
|
||||
.prepare(
|
||||
`UPDATE monitors
|
||||
SET status = 'down',
|
||||
last_checked = ?,
|
||||
response_time = 0,
|
||||
uptime = (
|
||||
SELECT ROUND((COUNT(CASE WHEN status = 'up' THEN 1 ELSE NULL END) * 100.0 / COUNT(*)), 2)
|
||||
FROM monitor_status_history
|
||||
WHERE monitor_id = ?
|
||||
ORDER BY timestamp DESC
|
||||
LIMIT 100
|
||||
)
|
||||
WHERE id = ?`
|
||||
)
|
||||
.bind(now, monitorId, monitorId)
|
||||
.run();
|
||||
}
|
||||
|
||||
// 创建新监控
|
||||
export async function createMonitor(
|
||||
db: Bindings["DB"],
|
||||
@@ -383,70 +381,41 @@ export async function deleteMonitor(db: Bindings["DB"], id: number) {
|
||||
return await db.prepare("DELETE FROM monitors WHERE id = ?").bind(id).run();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有监控
|
||||
* @param db 数据库连接
|
||||
* @returns 监控列表和操作结果
|
||||
*/
|
||||
export async function getAllMonitors(db: Bindings["DB"]) {
|
||||
// 根据用户角色过滤监控
|
||||
let result;
|
||||
result = await db
|
||||
.prepare("SELECT * FROM monitors ORDER BY created_at DESC")
|
||||
.all<Monitor>();
|
||||
export async function getMonitorDailyStatsById(db: Bindings["DB"], id: number) {
|
||||
// 查询每日统计数据
|
||||
const result = await db
|
||||
.prepare(
|
||||
`
|
||||
SELECT
|
||||
date,
|
||||
total_checks,
|
||||
up_checks,
|
||||
down_checks,
|
||||
avg_response_time,
|
||||
min_response_time,
|
||||
max_response_time,
|
||||
availability
|
||||
FROM
|
||||
monitor_daily_stats
|
||||
WHERE
|
||||
monitor_id = ?
|
||||
ORDER BY
|
||||
date ASC
|
||||
`
|
||||
)
|
||||
.bind(id)
|
||||
.all<MonitorDailyStats>();
|
||||
|
||||
// 解析所有监控的 headers 字段
|
||||
if (result.results && result.results.length > 0) {
|
||||
result.results.forEach((monitor) => {
|
||||
if (typeof monitor.headers === "string") {
|
||||
try {
|
||||
monitor.headers = JSON.parse(monitor.headers);
|
||||
} catch (e) {
|
||||
monitor.headers = {};
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// 获取所有监控的历史状态数据
|
||||
const monitorsWithHistory = await Promise.all(
|
||||
result.results.map(async (monitor) => {
|
||||
const historyResult = await db
|
||||
.prepare(
|
||||
`SELECT * FROM monitor_status_history
|
||||
WHERE monitor_id = ?
|
||||
ORDER BY timestamp ASC`
|
||||
)
|
||||
.bind(monitor.id)
|
||||
.all<MonitorStatusHistory>();
|
||||
|
||||
return {
|
||||
...monitor,
|
||||
history: historyResult.results || [],
|
||||
};
|
||||
})
|
||||
);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
monitors: monitorsWithHistory,
|
||||
status: 200,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
monitors: result.results || [],
|
||||
status: 200,
|
||||
};
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有监控(不包含历史状态数据)
|
||||
* 获取所有监控的每日统计数据
|
||||
* @param db 数据库连接
|
||||
* @returns 监控列表和操作结果
|
||||
* @returns 所有监控的每日统计数据和操作结果
|
||||
*/
|
||||
export async function getAllMonitorsWithoutHistory(db: Bindings["DB"]) {
|
||||
export async function getAllMonitorDailyStats(db: Bindings["DB"]) {
|
||||
return await db
|
||||
.prepare("SELECT * FROM monitors ORDER BY created_at DESC")
|
||||
.all<Monitor>();
|
||||
.prepare("SELECT * FROM monitor_daily_stats ORDER BY date ASC")
|
||||
.all<MonitorDailyStats>();
|
||||
}
|
||||
@@ -2,11 +2,11 @@ import { Bindings } from "../models";
|
||||
import * as repositories from "../repositories";
|
||||
|
||||
export async function getDashboardData(db: Bindings["DB"]) {
|
||||
const monitors = await repositories.getAllMonitorsWithoutHistory(db);
|
||||
const monitors = await repositories.getAllMonitors(db);
|
||||
const agents = await repositories.getAllAgents(db);
|
||||
|
||||
if (monitors.results && monitors.results.length > 0) {
|
||||
monitors.results.forEach((monitor) => {
|
||||
if (monitors.monitors && monitors.monitors.length > 0) {
|
||||
monitors.monitors.forEach((monitor) => {
|
||||
if (typeof monitor.headers === "string") {
|
||||
try {
|
||||
monitor.headers = JSON.parse(monitor.headers);
|
||||
@@ -17,7 +17,7 @@ export async function getDashboardData(db: Bindings["DB"]) {
|
||||
});
|
||||
}
|
||||
return {
|
||||
monitors: monitors.results,
|
||||
monitors: monitors.monitors,
|
||||
agents: agents.results,
|
||||
};
|
||||
}
|
||||
@@ -66,9 +66,6 @@ export async function checkMonitor(
|
||||
error = e instanceof Error ? e.message : String(e);
|
||||
console.error(`监控 ${monitor.name} (${monitor.url}) 请求失败: ${error}`);
|
||||
|
||||
// 记录错误状态
|
||||
await repositories.recordMonitorError(db, monitor.id, 0, error);
|
||||
|
||||
return {
|
||||
success: false,
|
||||
status: "down",
|
||||
@@ -175,99 +172,31 @@ export async function checkSingleMonitor(
|
||||
/**
|
||||
* 获取所有监控(根据用户角色过滤)
|
||||
* @param db 数据库连接
|
||||
* @param userId 用户ID
|
||||
* @param userRole 用户角色
|
||||
* @returns 监控列表和操作结果
|
||||
*/
|
||||
export async function getAllMonitors(
|
||||
db: Bindings["DB"],
|
||||
userId: number,
|
||||
userRole: string
|
||||
) {
|
||||
try {
|
||||
// 根据用户角色过滤监控
|
||||
let result;
|
||||
if (userRole === "admin") {
|
||||
result = await db
|
||||
.prepare("SELECT * FROM monitors ORDER BY created_at DESC")
|
||||
.all<models.Monitor>();
|
||||
} else {
|
||||
result = await db
|
||||
.prepare(
|
||||
"SELECT * FROM monitors WHERE created_by = ? ORDER BY created_at DESC"
|
||||
)
|
||||
.bind(userId)
|
||||
.all<models.Monitor>();
|
||||
}
|
||||
export async function getAllMonitors(db: Bindings["DB"]) {
|
||||
const result = await repositories.getAllMonitors(db);
|
||||
|
||||
// 获取所有监控的历史状态数据
|
||||
if (result.results && result.results.length > 0) {
|
||||
const monitorsWithHistory = await Promise.all(
|
||||
result.results.map(async (monitor) => {
|
||||
const historyResult = await db
|
||||
.prepare(
|
||||
`SELECT * FROM monitor_status_history
|
||||
WHERE monitor_id = ?
|
||||
ORDER BY timestamp ASC`
|
||||
)
|
||||
.bind(monitor.id)
|
||||
.all<models.MonitorStatusHistory>();
|
||||
|
||||
return {
|
||||
...monitor,
|
||||
history: historyResult.results || [],
|
||||
};
|
||||
})
|
||||
);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
monitors: monitorsWithHistory,
|
||||
status: 200,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
monitors: result.results || [],
|
||||
status: 200,
|
||||
};
|
||||
} catch (error) {
|
||||
console.error("获取监控列表错误:", error);
|
||||
return {
|
||||
success: false,
|
||||
message: "获取监控列表失败",
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
status: 500,
|
||||
};
|
||||
}
|
||||
return {
|
||||
success: true,
|
||||
monitors: result.monitors || [],
|
||||
status: 200,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取单个监控详情
|
||||
* @param db 数据库连接
|
||||
* @param id 监控ID
|
||||
* @param userId 用户ID
|
||||
* @param userRole 用户角色
|
||||
* @returns 监控详情和操作结果
|
||||
*/
|
||||
export async function getMonitorById(
|
||||
db: Bindings["DB"],
|
||||
id: number,
|
||||
userId: number,
|
||||
userRole: string
|
||||
) {
|
||||
export async function getMonitorById(db: Bindings["DB"], id: number) {
|
||||
const monitor = await repositories.getMonitorById(db, id);
|
||||
|
||||
if (!monitor) {
|
||||
return { success: false, message: "监控不存在", status: 404 };
|
||||
}
|
||||
|
||||
// 检查权限
|
||||
if (userRole !== "admin" && monitor.created_by !== userId) {
|
||||
return { success: false, message: "无权访问此监控", status: 403 };
|
||||
}
|
||||
|
||||
// 获取历史状态数据
|
||||
const historyResult = await repositories.getMonitorStatusHistory(db, id);
|
||||
|
||||
@@ -334,17 +263,9 @@ export async function createMonitor(
|
||||
* @param db 数据库连接
|
||||
* @param id 监控ID
|
||||
* @param data 更新数据
|
||||
* @param userId 用户ID
|
||||
* @param userRole 用户角色
|
||||
* @returns 更新结果
|
||||
*/
|
||||
export async function updateMonitor(
|
||||
db: Bindings["DB"],
|
||||
id: number,
|
||||
data: any,
|
||||
userId: number,
|
||||
userRole: string
|
||||
) {
|
||||
export async function updateMonitor(db: Bindings["DB"], id: number, data: any) {
|
||||
try {
|
||||
// 检查监控是否存在
|
||||
const monitor = await repositories.getMonitorById(db, id);
|
||||
@@ -353,11 +274,6 @@ export async function updateMonitor(
|
||||
return { success: false, message: "监控不存在", status: 404 };
|
||||
}
|
||||
|
||||
// 检查权限
|
||||
if (userRole !== "admin" && monitor.created_by !== userId) {
|
||||
return { success: false, message: "无权修改此监控", status: 403 };
|
||||
}
|
||||
|
||||
// 准备更新数据
|
||||
const updateData: Partial<models.Monitor> = {};
|
||||
|
||||
@@ -414,16 +330,9 @@ export async function updateMonitor(
|
||||
* 删除监控
|
||||
* @param db 数据库连接
|
||||
* @param id 监控ID
|
||||
* @param userId 用户ID
|
||||
* @param userRole 用户角色
|
||||
* @returns 删除结果
|
||||
*/
|
||||
export async function deleteMonitor(
|
||||
db: Bindings["DB"],
|
||||
id: number,
|
||||
userId: number,
|
||||
userRole: string
|
||||
) {
|
||||
export async function deleteMonitor(db: Bindings["DB"], id: number) {
|
||||
try {
|
||||
// 检查监控是否存在
|
||||
const monitor = await repositories.getMonitorById(db, id);
|
||||
@@ -432,11 +341,6 @@ export async function deleteMonitor(
|
||||
return { success: false, message: "监控不存在", status: 404 };
|
||||
}
|
||||
|
||||
// 检查权限
|
||||
if (userRole !== "admin" && monitor.created_by !== userId) {
|
||||
return { success: false, message: "无权删除此监控", status: 403 };
|
||||
}
|
||||
|
||||
// 执行删除
|
||||
const result = await repositories.deleteMonitor(db, id);
|
||||
|
||||
@@ -505,20 +409,32 @@ export async function getMonitorStatusHistoryById(
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* 获取所有监控状态历史
|
||||
* @param db 数据库连接
|
||||
* @returns 所有监控状态历史
|
||||
*/
|
||||
export async function getAllMonitorStatusHistory(db: Bindings["DB"]) {
|
||||
const result = await repositories.getAllMonitorStatusHistory(db);
|
||||
return {
|
||||
success: true,
|
||||
history: result.results || [],
|
||||
status: 200,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 手动检查监控并处理通知
|
||||
* @param db 数据库连接
|
||||
* @param id 监控ID
|
||||
* @param userId 用户ID
|
||||
* @param userRole 用户角色
|
||||
* @param env 环境变量
|
||||
* @returns 检查结果
|
||||
*/
|
||||
export async function manualCheckMonitor(
|
||||
db: Bindings["DB"],
|
||||
id: number,
|
||||
userId: number,
|
||||
userRole: string,
|
||||
env: any
|
||||
) {
|
||||
try {
|
||||
@@ -529,11 +445,6 @@ export async function manualCheckMonitor(
|
||||
return { success: false, message: "监控不存在", status: 404 };
|
||||
}
|
||||
|
||||
// 检查权限
|
||||
if (userRole !== "admin" && monitor.created_by !== userId) {
|
||||
return { success: false, message: "无权访问此监控", status: 403 };
|
||||
}
|
||||
|
||||
// 使用抽象出来的通用检查监控函数进行检查
|
||||
const result = await checkSingleMonitor(db, monitor);
|
||||
|
||||
@@ -656,3 +567,36 @@ function getExpectedStatusDisplay(expectedStatus: number): string {
|
||||
}
|
||||
return String(expectedStatus);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取单个监控的每日统计数据
|
||||
* @param db 数据库连接
|
||||
* @param id 监控ID
|
||||
* @returns 监控的每日统计数据和操作结果
|
||||
*/
|
||||
export async function getMonitorDailyStats(db: Bindings["DB"], id: number) {
|
||||
const result = await repositories.getMonitorDailyStatsById(db, id);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
dailyStats: result.results,
|
||||
message: "获取监控每日统计数据成功",
|
||||
status: 200,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有监控的每日统计数据
|
||||
* @param db 数据库连接
|
||||
* @returns 所有监控的每日统计数据和操作结果
|
||||
*/
|
||||
export async function getAllMonitorDailyStats(db: Bindings["DB"]) {
|
||||
// 获取所有监控
|
||||
const result = await repositories.getAllMonitorDailyStats(db);
|
||||
return {
|
||||
success: true,
|
||||
dailyStats: result.results,
|
||||
message: "获取所有监控的每日统计数据成功",
|
||||
status: 200,
|
||||
};
|
||||
}
|
||||
@@ -223,13 +223,18 @@ export async function getStatusPagePublicData(env: { DB: Bindings["DB"] }) {
|
||||
if (monitorIds.length > 0) {
|
||||
for (const monitorId of monitorIds) {
|
||||
const monitor = await repositories.getMonitorById(env.DB, monitorId);
|
||||
const historyResult = await repositories.getMonitorStatusHistory(
|
||||
const monitorDailyStats = await repositories.getMonitorDailyStatsById(
|
||||
env.DB,
|
||||
monitor.id
|
||||
monitorId
|
||||
);
|
||||
const monitorHistory = await repositories.getMonitorStatusHistory(
|
||||
env.DB,
|
||||
monitorId
|
||||
);
|
||||
monitors.push({
|
||||
...monitor,
|
||||
history: historyResult.results,
|
||||
dailyStats: monitorDailyStats.results,
|
||||
history: monitorHistory.results,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -249,8 +254,6 @@ export async function getStatusPagePublicData(env: { DB: Bindings["DB"] }) {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
return {
|
||||
title: config.title,
|
||||
description: config.description,
|
||||
|
||||
@@ -4,18 +4,14 @@ import {
|
||||
CrossCircledIcon,
|
||||
QuestionMarkCircledIcon,
|
||||
} from "@radix-ui/react-icons";
|
||||
import { Monitor } from "../types/monitors";
|
||||
import { MonitorWithDailyStatsAndStatusHistory } from "../types/monitors";
|
||||
import "../styles/components.css";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import StatusBar from "./StatusBar";
|
||||
import ResponseTimeChart from "./ResponseTimeChart";
|
||||
|
||||
interface MonitorCardProps {
|
||||
monitor: Monitor & {
|
||||
response_time?: number;
|
||||
uptime?: number;
|
||||
history?: any[];
|
||||
};
|
||||
monitor: MonitorWithDailyStatsAndStatusHistory;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -78,7 +74,7 @@ const MonitorCard = ({ monitor }: MonitorCardProps) => {
|
||||
|
||||
{/* 状态条显示 */}
|
||||
<Box pt="2" style={{ width: "100%" }}>
|
||||
<StatusBar status={currentStatus} history={monitor.history} />
|
||||
<StatusBar dailyStats={monitor.dailyStats} />
|
||||
</Box>
|
||||
|
||||
{/* 响应时间图表 */}
|
||||
|
||||
@@ -52,6 +52,8 @@ const ResponseTimeChart: React.FC<ResponseTimeChartProps> = ({
|
||||
const { t } = useTranslation();
|
||||
const chartRef = useRef<ChartJS<"line">>(null);
|
||||
|
||||
console.log("ResponseTimeChart组件的history: ", history);
|
||||
|
||||
// 格式化时间的函数使用 useCallback 缓存
|
||||
const formatTime = useCallback((date: Date) => {
|
||||
try {
|
||||
@@ -274,6 +276,8 @@ const ResponseTimeChart: React.FC<ResponseTimeChartProps> = ({
|
||||
};
|
||||
});
|
||||
|
||||
console.log("processedHistory: ", processedHistory);
|
||||
|
||||
// 计算24小时前的时间戳
|
||||
const twentyFourHoursAgo = Date.now() - 24 * 60 * 60 * 1000;
|
||||
|
||||
@@ -327,6 +331,8 @@ const ResponseTimeChart: React.FC<ResponseTimeChartProps> = ({
|
||||
.filter((point) => point.x >= twentyFourHoursAgo)
|
||||
.sort((a, b) => a.x - b.x);
|
||||
|
||||
console.log("responseTimeData: ", responseTimeData);
|
||||
|
||||
// 确保相邻点至少间隔1分钟
|
||||
const oneMinute = 60 * 1000;
|
||||
let filteredData: DataPoint[] = [];
|
||||
|
||||
@@ -1,30 +1,20 @@
|
||||
import React, { useState, useMemo } from "react";
|
||||
import {
|
||||
Box,
|
||||
Flex,
|
||||
Tooltip,
|
||||
Dialog,
|
||||
ScrollArea,
|
||||
Text,
|
||||
Button,
|
||||
} from "@radix-ui/themes";
|
||||
import React, { useMemo } from "react";
|
||||
import { Box, Tooltip, Text } from "@radix-ui/themes";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { MonitorStatusHistory } from "../types/monitors";
|
||||
import { DailyStats } from "../types/monitors";
|
||||
|
||||
interface StatusBarProps {
|
||||
status: string;
|
||||
history?: MonitorStatusHistory[];
|
||||
dailyStats?: DailyStats[]; // 新增每日统计数据参数
|
||||
}
|
||||
|
||||
/**
|
||||
* 状态条组件 - 展示监控状态历史的时间轴格子
|
||||
* 每个格子代表一天的数据,最多展示最近30天
|
||||
*/
|
||||
const StatusBar: React.FC<StatusBarProps> = ({ status, history = [] }) => {
|
||||
const StatusBar: React.FC<StatusBarProps> = ({ dailyStats = [] }) => {
|
||||
const { t } = useTranslation();
|
||||
const [dialogOpen, setDialogOpen] = useState(false);
|
||||
const [dialogTitle, setDialogTitle] = useState("");
|
||||
const [dialogContent, setDialogContent] = useState<React.ReactNode>(<div />);
|
||||
|
||||
console.log("StatusBar组件的dailyStats: ", dailyStats);
|
||||
|
||||
// 根据状态或百分比确定颜色
|
||||
const getColor = (value: string | number, isHover = false) => {
|
||||
@@ -60,177 +50,48 @@ const StatusBar: React.FC<StatusBarProps> = ({ status, history = [] }) => {
|
||||
}
|
||||
};
|
||||
|
||||
// 按天聚合数据
|
||||
// 按天聚合数据 - 优先使用每日统计数据,如果没有则使用历史记录
|
||||
const dailyHistory = useMemo(() => {
|
||||
// 确保历史记录有数据
|
||||
if (history.length === 0) {
|
||||
return [
|
||||
{
|
||||
date: new Date().toISOString().split("T")[0],
|
||||
status: status as "up" | "down",
|
||||
availability: 100,
|
||||
records: [
|
||||
{
|
||||
id: 0,
|
||||
monitor_id: 0,
|
||||
status: status as "up" | "down",
|
||||
response_time: 0,
|
||||
timestamp: new Date().toISOString(),
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
// 按日期分组
|
||||
const grouped: Record<string, MonitorStatusHistory[]> = {};
|
||||
|
||||
// 确保每条记录都有时间戳
|
||||
history.forEach((item) => {
|
||||
if (!item.timestamp) return;
|
||||
|
||||
// 提取日期部分 (YYYY-MM-DD)
|
||||
const dateStr = new Date(item.timestamp).toISOString().split("T")[0];
|
||||
|
||||
if (!grouped[dateStr]) {
|
||||
grouped[dateStr] = [];
|
||||
}
|
||||
|
||||
grouped[dateStr].push(item);
|
||||
});
|
||||
|
||||
// 转换为按日期排序的数组
|
||||
const result = Object.entries(grouped)
|
||||
.map(([date, records]) => {
|
||||
// 按时间戳排序,确保最新的在前面
|
||||
const sortedRecords = [...records].sort((a, b) => {
|
||||
const timeA = a.timestamp ? new Date(a.timestamp).getTime() : 0;
|
||||
const timeB = b.timestamp ? new Date(b.timestamp).getTime() : 0;
|
||||
return timeB - timeA; // 降序,最新的在前
|
||||
});
|
||||
|
||||
// 计算当天的主要状态
|
||||
const downCount = records.filter((r) => r.status === "down").length;
|
||||
const upCount = records.filter((r) => r.status === "up").length;
|
||||
const primaryStatus = upCount > downCount ? "up" : "down";
|
||||
|
||||
// 计算可用率
|
||||
const availability =
|
||||
records.length > 0 ? (upCount / records.length) * 100 : 0;
|
||||
// 如果有每日统计数据,优先使用
|
||||
if (dailyStats && dailyStats.length > 0) {
|
||||
return dailyStats.map((stat) => {
|
||||
// 确定每天的主要状态
|
||||
const dailyStatus =
|
||||
stat.up_checks > stat.down_checks
|
||||
? ("up" as const)
|
||||
: ("down" as const);
|
||||
|
||||
return {
|
||||
date,
|
||||
status: primaryStatus,
|
||||
availability,
|
||||
records: sortedRecords,
|
||||
date: stat.date,
|
||||
status: dailyStatus,
|
||||
availability: stat.availability,
|
||||
total_checks: stat.total_checks,
|
||||
up_checks: stat.up_checks,
|
||||
down_checks: stat.down_checks,
|
||||
avg_response_time: stat.avg_response_time,
|
||||
min_response_time: stat.min_response_time,
|
||||
max_response_time: stat.max_response_time,
|
||||
monitor_id: stat.monitor_id,
|
||||
};
|
||||
})
|
||||
.sort((a, b) => new Date(a.date).getTime() - new Date(b.date).getTime());
|
||||
|
||||
// 最多返回最近30天
|
||||
const maxDays = 30;
|
||||
return result.slice(-maxDays);
|
||||
}, [history, status]);
|
||||
|
||||
// 显示详细信息对话框
|
||||
const showDialog = (dayData: {
|
||||
date: string;
|
||||
status: string;
|
||||
records: MonitorStatusHistory[];
|
||||
availability: number;
|
||||
}) => {
|
||||
const date = new Date(dayData.date).toLocaleDateString();
|
||||
const statusText =
|
||||
dayData.status === "up"
|
||||
? t("monitor.status.normal")
|
||||
: t("monitor.status.failure");
|
||||
|
||||
setDialogTitle(`📅 ${date} - ${statusText}`);
|
||||
|
||||
// 获取该天最新的10条记录
|
||||
const latestRecords = dayData.records.slice(0, 10);
|
||||
|
||||
setDialogContent(
|
||||
<Box p="4">
|
||||
<p>
|
||||
{t("common.date")}: {date}
|
||||
</p>
|
||||
<p>
|
||||
{t("common.status")}: {statusText}
|
||||
</p>
|
||||
<p>
|
||||
{t("monitor.history.records")}: {dayData.records.length}
|
||||
</p>
|
||||
<p>
|
||||
{t("monitor.history.availability")}: {dayData.availability.toFixed(2)}
|
||||
%
|
||||
</p>
|
||||
|
||||
<Box mt="4">
|
||||
<p>{t("monitor.history.latest")}:</p>
|
||||
<ScrollArea style={{ height: "200px", marginTop: "10px" }}>
|
||||
{latestRecords.map((record, idx) => {
|
||||
const recordTime = record.timestamp
|
||||
? new Date(record.timestamp).toLocaleString()
|
||||
: "";
|
||||
|
||||
return (
|
||||
<Box
|
||||
key={record.id || `record-${idx}`}
|
||||
style={{
|
||||
padding: "8px",
|
||||
marginBottom: "8px",
|
||||
borderRadius: "4px",
|
||||
backgroundColor:
|
||||
record.status === "up"
|
||||
? "var(--green-2)"
|
||||
: "var(--red-2)",
|
||||
}}
|
||||
>
|
||||
<p>
|
||||
{t("common.status")}:{" "}
|
||||
{record.status === "up"
|
||||
? t("monitor.status.normal")
|
||||
: t("monitor.status.failure")}
|
||||
</p>
|
||||
<p>
|
||||
{t("monitor.history.time")}: {recordTime}
|
||||
</p>
|
||||
<p>
|
||||
{t("monitor.history.responseTime")}: {record.response_time}
|
||||
ms
|
||||
</p>
|
||||
{record.error && (
|
||||
<p>
|
||||
{t("monitor.history.error")}: {record.error}
|
||||
</p>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
})}
|
||||
</ScrollArea>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
|
||||
setDialogOpen(true);
|
||||
};
|
||||
});
|
||||
}
|
||||
}, [dailyStats]);
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* 状态历史条 - 使用Grid布局代替Flex */}
|
||||
<Box
|
||||
style={{
|
||||
display: 'grid',
|
||||
gridTemplateColumns: `repeat(${dailyHistory.length}, 1fr)`,
|
||||
gap: '4px',
|
||||
width: '100%'
|
||||
<Box
|
||||
style={{
|
||||
display: "grid",
|
||||
gridTemplateColumns: `repeat(${dailyHistory?.length}, 1fr)`,
|
||||
gap: "4px",
|
||||
width: "100%",
|
||||
}}
|
||||
>
|
||||
{dailyHistory.map((dayData, index) => {
|
||||
{dailyHistory?.map((dayData) => {
|
||||
return (
|
||||
<Tooltip
|
||||
key={dayData.date || `empty-${index}`}
|
||||
key={`${dayData.monitor_id}-${Math.random()}`}
|
||||
content={
|
||||
<>
|
||||
<Text as="span" size="1" mb="1">
|
||||
@@ -247,10 +108,6 @@ const StatusBar: React.FC<StatusBarProps> = ({ status, history = [] }) => {
|
||||
: t("monitor.status.pending")}
|
||||
</Text>
|
||||
<br></br>
|
||||
<Text as="span" size="1" mb="1">
|
||||
{t("monitor.history.records")}: {dayData.records.length}
|
||||
</Text>
|
||||
<br></br>
|
||||
<Text as="span" size="1" mb="1">
|
||||
{t("monitor.history.availability")}:{" "}
|
||||
{dayData.availability.toFixed(2)}%
|
||||
@@ -258,37 +115,21 @@ const StatusBar: React.FC<StatusBarProps> = ({ status, history = [] }) => {
|
||||
</>
|
||||
}
|
||||
>
|
||||
<Button
|
||||
<Box
|
||||
style={{
|
||||
width: '100%',
|
||||
width: "100%",
|
||||
height: "50px",
|
||||
backgroundColor: getColor(dayData.status),
|
||||
borderRadius: "2px",
|
||||
transition: "background-color 0.2s",
|
||||
cursor: "pointer",
|
||||
padding: '0'
|
||||
padding: "0",
|
||||
}}
|
||||
onClick={() => showDialog(dayData)}
|
||||
/>
|
||||
</Tooltip>
|
||||
);
|
||||
})}
|
||||
</Box>
|
||||
|
||||
{/* 详细信息对话框 */}
|
||||
<Dialog.Root open={dialogOpen} onOpenChange={setDialogOpen}>
|
||||
<Dialog.Content style={{ maxWidth: "500px" }}>
|
||||
<Dialog.Title>{dialogTitle}</Dialog.Title>
|
||||
<Dialog.Description>{dialogContent}</Dialog.Description>
|
||||
<Flex gap="3" mt="2" justify="end">
|
||||
<Dialog.Close>
|
||||
<button style={{ cursor: "pointer", padding: "6px 12px" }}>
|
||||
{t("common.close")}
|
||||
</button>
|
||||
</Dialog.Close>
|
||||
</Flex>
|
||||
</Dialog.Content>
|
||||
</Dialog.Root>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -145,7 +145,7 @@ const zhCN = {
|
||||
"monitor.uptime": "正常运行时间",
|
||||
"monitor.responseTime": "响应时间",
|
||||
"monitor.oneDayHistory": "24小时记录",
|
||||
"monitor.threeMonthsHistory": "90天记录",
|
||||
"monitor.MonthsHistory": "30天记录",
|
||||
"monitor.noResponseTimeData": "暂无响应时间数据",
|
||||
"monitor.manualCheck": "手动检查",
|
||||
"monitor.checkCompleted": "监控检查已完成",
|
||||
|
||||
@@ -22,8 +22,10 @@ import {
|
||||
getMonitor,
|
||||
deleteMonitor,
|
||||
checkMonitor,
|
||||
getMonitorStatusHistoryById,
|
||||
getMonitorDailyStats,
|
||||
} from "../../services/api/monitors";
|
||||
import { Monitor } from "../../types/monitors";
|
||||
import { MonitorWithDailyStatsAndStatusHistory } from "../../types/monitors";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import ResponseTimeChart from "../../components/ResponseTimeChart";
|
||||
import StatusBar from "../../components/StatusBar";
|
||||
@@ -43,7 +45,8 @@ const formatStatusCode = (code: number | undefined): string => {
|
||||
const MonitorDetail = () => {
|
||||
const { id } = useParams<{ id: string }>();
|
||||
const navigate = useNavigate();
|
||||
const [monitor, setMonitor] = useState<Monitor | null>(null);
|
||||
const [monitor, setMonitor] =
|
||||
useState<MonitorWithDailyStatsAndStatusHistory | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [toastOpen, setToastOpen] = useState(false);
|
||||
@@ -51,20 +54,6 @@ const MonitorDetail = () => {
|
||||
const [toastType, setToastType] = useState<"success" | "error">("success");
|
||||
const { t } = useTranslation();
|
||||
|
||||
// 获取监控详情数据
|
||||
const fetchMonitorData = async () => {
|
||||
if (!id) return;
|
||||
setLoading(true);
|
||||
const response = await getMonitor(parseInt(id));
|
||||
|
||||
if (response.success && response.monitor) {
|
||||
setMonitor(response.monitor);
|
||||
} else {
|
||||
setError(t("common.error.fetch"));
|
||||
}
|
||||
setLoading(false);
|
||||
};
|
||||
|
||||
// 组件加载时获取数据
|
||||
useEffect(() => {
|
||||
fetchMonitorData();
|
||||
@@ -79,6 +68,34 @@ const MonitorDetail = () => {
|
||||
return () => clearInterval(intervalId);
|
||||
}, [id]);
|
||||
|
||||
// 获取监控详情数据
|
||||
const fetchMonitorData = async () => {
|
||||
if (!id) return;
|
||||
setLoading(true);
|
||||
let monitorData: MonitorWithDailyStatsAndStatusHistory | null = null;
|
||||
const monitor = await getMonitor(parseInt(id));
|
||||
const history = await getMonitorStatusHistoryById(parseInt(id));
|
||||
const dailyStats = await getMonitorDailyStats(parseInt(id));
|
||||
|
||||
console.log("dailyStats: ", dailyStats);
|
||||
console.log("history: ", history);
|
||||
console.log("monitor: ", monitor);
|
||||
|
||||
if (monitor.success && monitor.monitor) {
|
||||
monitorData = {
|
||||
...monitor.monitor,
|
||||
history: history.history || [],
|
||||
dailyStats: dailyStats.dailyStats || [],
|
||||
};
|
||||
}
|
||||
if (monitorData) {
|
||||
setMonitor(monitorData);
|
||||
} else {
|
||||
setError(t("common.error.fetch"));
|
||||
}
|
||||
setLoading(false);
|
||||
};
|
||||
|
||||
// 手动检查监控状态
|
||||
const handleCheck = async () => {
|
||||
if (!id) return;
|
||||
@@ -234,12 +251,9 @@ const MonitorDetail = () => {
|
||||
</Card>
|
||||
<Card mt="4">
|
||||
<Flex direction="column" gap="3">
|
||||
<Heading size="4">{t("monitor.threeMonthsHistory")}</Heading>
|
||||
<Heading size="4">{t("monitor.MonthsHistory")}</Heading>
|
||||
<Box>
|
||||
<StatusBar
|
||||
status={monitor.status}
|
||||
history={monitor.history || []}
|
||||
/>
|
||||
<StatusBar dailyStats={monitor.dailyStats || []} />
|
||||
</Box>
|
||||
</Flex>
|
||||
</Card>
|
||||
|
||||
@@ -26,14 +26,21 @@ import {
|
||||
ReloadIcon,
|
||||
InfoCircledIcon,
|
||||
} from "@radix-ui/react-icons";
|
||||
import { getAllMonitors, deleteMonitor } from "../../services/api/monitors";
|
||||
import { Monitor } from "../../types/monitors";
|
||||
import {
|
||||
getAllMonitors,
|
||||
deleteMonitor,
|
||||
getAllDailyStats,
|
||||
getAllMonitorHistory,
|
||||
} from "../../services/api/monitors";
|
||||
import { MonitorWithDailyStatsAndStatusHistory } from "../../types/monitors";
|
||||
import MonitorCard from "../../components/MonitorCard";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
const MonitorsList = () => {
|
||||
const navigate = useNavigate();
|
||||
const [monitors, setMonitors] = useState<Monitor[]>([]);
|
||||
const [monitors, setMonitors] = useState<
|
||||
MonitorWithDailyStatsAndStatusHistory[]
|
||||
>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [view, setView] = useState<"list" | "grid">("grid");
|
||||
@@ -43,25 +50,6 @@ const MonitorsList = () => {
|
||||
);
|
||||
const { t } = useTranslation();
|
||||
|
||||
// 获取监控数据
|
||||
const fetchData = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
const response = await getAllMonitors();
|
||||
|
||||
if (response.success && response.monitors) {
|
||||
setMonitors(response.monitors);
|
||||
} else {
|
||||
setError(response.message || t("monitors.loadingError"));
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(t("monitors.loadingError"), err);
|
||||
setError(t("monitors.loadingError"));
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
fetchData();
|
||||
|
||||
@@ -75,6 +63,44 @@ const MonitorsList = () => {
|
||||
return () => clearInterval(intervalId);
|
||||
}, []);
|
||||
|
||||
// 获取监控数据
|
||||
const fetchData = async () => {
|
||||
setLoading(true);
|
||||
const response = await getAllMonitors();
|
||||
const responseDailyStats = await getAllDailyStats();
|
||||
const responseMonitorHistory = await getAllMonitorHistory();
|
||||
|
||||
console.log("responseDailyStats: ", responseDailyStats);
|
||||
|
||||
if (
|
||||
response.success &&
|
||||
responseDailyStats.success &&
|
||||
responseMonitorHistory.success
|
||||
) {
|
||||
const monitorsWithData = response.monitors?.map((monitor) => {
|
||||
// 将与当前监控相关的 dailyStats 数据附加到 monitor 上
|
||||
const dailyStats = (responseDailyStats.dailyStats || []).filter(
|
||||
(stat) => stat.monitor_id === monitor.id
|
||||
);
|
||||
// 将与当前监控相关的 monitorHistory 数据附加到 monitor 上
|
||||
const history = (responseMonitorHistory.history || []).filter(
|
||||
(item) => item.monitor_id === monitor.id
|
||||
);
|
||||
// 返回附加了相关数据的 monitor
|
||||
return {
|
||||
...monitor,
|
||||
dailyStats: dailyStats,
|
||||
history: history,
|
||||
};
|
||||
});
|
||||
console.log("monitorsWithData: ", monitorsWithData);
|
||||
setMonitors(monitorsWithData || []);
|
||||
} else {
|
||||
setError(response.message || t("monitors.loadingError"));
|
||||
}
|
||||
setLoading(false);
|
||||
};
|
||||
|
||||
// 处理刷新
|
||||
const handleRefresh = () => {
|
||||
fetchData();
|
||||
@@ -230,7 +256,7 @@ const MonitorsList = () => {
|
||||
|
||||
<Table.Body>
|
||||
{monitors.map((monitor) => (
|
||||
<Table.Row key={monitor.id}>
|
||||
<Table.Row key={`${monitor.id}-${Math.random()}`}>
|
||||
<Table.Cell>
|
||||
<Text weight="medium">{monitor.name}</Text>
|
||||
</Table.Cell>
|
||||
@@ -303,7 +329,10 @@ const MonitorsList = () => {
|
||||
// 网格视图 - 使用 MonitorCard 组件
|
||||
<Grid columns={{ initial: "1" }} gap="4">
|
||||
{monitors.map((monitor) => (
|
||||
<Box key={monitor.id} style={{ position: "relative" }}>
|
||||
<Box
|
||||
key={`${monitor.id}-${Math.random()}`}
|
||||
style={{ position: "relative" }}
|
||||
>
|
||||
<MonitorCard monitor={monitor} />
|
||||
<Flex
|
||||
style={{
|
||||
|
||||
@@ -4,12 +4,12 @@ import { getStatusPageData } from "../../services/api/status";
|
||||
import AgentCard from "../../components/AgentCard";
|
||||
import MonitorCard from "../../components/MonitorCard";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Agent, Monitor } from "../../types";
|
||||
import { Agent, MonitorWithDailyStatsAndStatusHistory } from "../../types";
|
||||
|
||||
const StatusPage = () => {
|
||||
const { t } = useTranslation();
|
||||
const [data, setData] = useState<{
|
||||
monitors: Monitor[];
|
||||
monitors: MonitorWithDailyStatsAndStatusHistory[];
|
||||
agents: Agent[];
|
||||
}>({
|
||||
monitors: [],
|
||||
@@ -22,7 +22,6 @@ const StatusPage = () => {
|
||||
);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
|
||||
// 从API获取数据
|
||||
useEffect(() => {
|
||||
fetchData();
|
||||
@@ -41,6 +40,7 @@ const StatusPage = () => {
|
||||
const fetchData = async () => {
|
||||
setLoading(true);
|
||||
const response = await getStatusPageData();
|
||||
|
||||
if (response) {
|
||||
// 设置页面标题和描述
|
||||
setPageTitle(response.title || t("statusPage.title"));
|
||||
@@ -58,7 +58,6 @@ const StatusPage = () => {
|
||||
setLoading(false);
|
||||
};
|
||||
|
||||
|
||||
// 错误显示
|
||||
if (error) {
|
||||
return (
|
||||
@@ -123,7 +122,7 @@ const StatusPage = () => {
|
||||
</Heading>
|
||||
<Grid columns={{ initial: "1" }} gap="4">
|
||||
{data.monitors.map((monitor) => (
|
||||
<MonitorCard key={monitor.id} monitor={monitor} />
|
||||
<MonitorCard monitor={monitor} />
|
||||
))}
|
||||
</Grid>
|
||||
</Box>
|
||||
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
CreateMonitorRequest,
|
||||
UpdateMonitorRequest,
|
||||
MonitorStatusHistoryResponse,
|
||||
DailyStatsResponse,
|
||||
} from "../../types/monitors";
|
||||
|
||||
// 获取所有监控
|
||||
@@ -13,6 +14,18 @@ export const getAllMonitors = async (): Promise<MonitorsResponse> => {
|
||||
return response.data;
|
||||
};
|
||||
|
||||
// 获取所有每日统计
|
||||
export const getAllDailyStats = async (): Promise<DailyStatsResponse> => {
|
||||
const response = await api.get<DailyStatsResponse>("/api/monitors/daily");
|
||||
return response.data;
|
||||
};
|
||||
|
||||
// 获取单个监控每日统计
|
||||
export const getMonitorDailyStats = async (id: number): Promise<DailyStatsResponse> => {
|
||||
const response = await api.get<DailyStatsResponse>(`/api/monitors/${id}/daily`);
|
||||
return response.data;
|
||||
};
|
||||
|
||||
// 获取单个监控
|
||||
export const getMonitor = async (id: number): Promise<MonitorResponse> => {
|
||||
const response = await api.get<MonitorResponse>(`/api/monitors/${id}`);
|
||||
@@ -42,8 +55,8 @@ export const deleteMonitor = async (id: number): Promise<MonitorResponse> => {
|
||||
return response.data;
|
||||
};
|
||||
|
||||
// 获取监控历史
|
||||
export const getMonitorHistory = async (
|
||||
// 获取单个监控历史 24小时内
|
||||
export const getMonitorStatusHistoryById = async (
|
||||
id: number
|
||||
): Promise<MonitorStatusHistoryResponse> => {
|
||||
const response = await api.get<MonitorStatusHistoryResponse>(
|
||||
@@ -52,6 +65,15 @@ export const getMonitorHistory = async (
|
||||
return response.data;
|
||||
};
|
||||
|
||||
// 获取所有监控历史 24小时内
|
||||
export const getAllMonitorHistory =
|
||||
async (): Promise<MonitorStatusHistoryResponse> => {
|
||||
const response = await api.get<MonitorStatusHistoryResponse>(
|
||||
`/api/monitors/history`
|
||||
);
|
||||
return response.data;
|
||||
};
|
||||
|
||||
// 手动检查监控
|
||||
export const checkMonitor = async (
|
||||
id: number
|
||||
|
||||
@@ -22,7 +22,11 @@ export interface Monitor {
|
||||
active?: number;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
history?: MonitorStatusHistory[];
|
||||
}
|
||||
|
||||
export interface MonitorWithDailyStatsAndStatusHistory extends Monitor {
|
||||
dailyStats: DailyStats[];
|
||||
history: MonitorStatusHistory[];
|
||||
}
|
||||
|
||||
export interface MonitorStatusHistory {
|
||||
@@ -53,6 +57,11 @@ export interface MonitorsResponse {
|
||||
monitors?: Monitor[];
|
||||
}
|
||||
|
||||
export interface DailyStatsResponse {
|
||||
success: boolean;
|
||||
message: string;
|
||||
dailyStats?: DailyStats[];
|
||||
}
|
||||
|
||||
export interface CreateMonitorRequest {
|
||||
name: string;
|
||||
@@ -75,3 +84,17 @@ export interface UpdateMonitorRequest {
|
||||
interval?: number;
|
||||
timeout?: number;
|
||||
}
|
||||
|
||||
// 新增每日统计数据类型
|
||||
export interface DailyStats {
|
||||
date: string;
|
||||
total_checks: number;
|
||||
up_checks: number;
|
||||
down_checks: number;
|
||||
avg_response_time: number;
|
||||
min_response_time: number;
|
||||
max_response_time: number;
|
||||
availability: number;
|
||||
monitor_id: number;
|
||||
created_at: string;
|
||||
}
|
||||
@@ -3,7 +3,7 @@
|
||||
*/
|
||||
|
||||
import { Agent } from "./agents";
|
||||
import { Monitor } from "./monitors";
|
||||
import { MonitorWithDailyStatsAndStatusHistory } from "./monitors";
|
||||
|
||||
export interface ConfigMonitor {
|
||||
id: number;
|
||||
@@ -40,7 +40,6 @@ export interface StatusPageData {
|
||||
description: string;
|
||||
logoUrl: string;
|
||||
customCss: string;
|
||||
monitors: Monitor[];
|
||||
monitors: MonitorWithDailyStatsAndStatusHistory[];
|
||||
agents: Agent[];
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user