mirror of
https://github.com/d0zingcat/alert-message-center.git
synced 2026-05-22 23:16:49 +00:00
@@ -1,174 +1,208 @@
|
||||
import { Hono } from 'hono';
|
||||
import { eq, and, desc, sql, gt, sum, count } from 'drizzle-orm';
|
||||
import { z } from 'zod';
|
||||
import { zValidator } from '@hono/zod-validator';
|
||||
import { db } from './db';
|
||||
import { topics, users, subscriptions, alertTasks, topicGroupChats, knownGroupChats } from './db/schema';
|
||||
import { requireAuth, requireAdmin, AuthSession } from './middleware';
|
||||
import { zValidator } from "@hono/zod-validator";
|
||||
import { and, count, desc, eq, gt, sql, sum } from "drizzle-orm";
|
||||
import { Hono } from "hono";
|
||||
import { z } from "zod";
|
||||
import { db } from "./db";
|
||||
import {
|
||||
alertTasks,
|
||||
knownGroupChats,
|
||||
subscriptions,
|
||||
topicGroupChats,
|
||||
topics,
|
||||
users,
|
||||
} from "./db/schema";
|
||||
import { type AuthSession, requireAdmin, requireAuth } from "./middleware";
|
||||
|
||||
const api = new Hono<{ Variables: { session: AuthSession } }>();
|
||||
|
||||
const topicSchema = z.object({
|
||||
name: z.string().min(1),
|
||||
slug: z.string().min(1),
|
||||
description: z.string().optional(),
|
||||
name: z.string().min(1),
|
||||
slug: z.string().min(1),
|
||||
description: z.string().optional(),
|
||||
});
|
||||
|
||||
const groupBindingSchema = z.object({
|
||||
chatId: z.string().min(1),
|
||||
name: z.string().min(1),
|
||||
chatId: z.string().min(1),
|
||||
name: z.string().min(1),
|
||||
});
|
||||
|
||||
const userSchema = z.object({
|
||||
name: z.string().min(1),
|
||||
feishuUserId: z.string().min(1),
|
||||
email: z.string().email().optional().or(z.literal('')),
|
||||
name: z.string().min(1),
|
||||
feishuUserId: z.string().min(1),
|
||||
email: z.string().email().optional().or(z.literal("")),
|
||||
});
|
||||
|
||||
// --- Topics ---
|
||||
|
||||
// --- Topics ---
|
||||
|
||||
api.get('/topics', requireAuth, async (c) => {
|
||||
const session = c.get('session');
|
||||
const isAdmin = session.isAdmin;
|
||||
const currentUserId = session.id;
|
||||
api.get("/topics", requireAuth, async (c) => {
|
||||
const session = c.get("session");
|
||||
const isAdmin = session.isAdmin;
|
||||
const currentUserId = session.id;
|
||||
|
||||
const allTopics = await db.query.topics.findMany({
|
||||
where: eq(topics.status, 'approved'),
|
||||
with: {
|
||||
creator: true,
|
||||
approver: true,
|
||||
subscriptions: {
|
||||
where: (subscriptions, { eq }) =>
|
||||
isAdmin ? undefined : (currentUserId ? eq(subscriptions.userId, currentUserId) : undefined),
|
||||
with: {
|
||||
user: true
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
const allTopics = await db.query.topics.findMany({
|
||||
where: eq(topics.status, "approved"),
|
||||
with: {
|
||||
creator: true,
|
||||
approver: true,
|
||||
subscriptions: {
|
||||
where: (subscriptions, { eq }) =>
|
||||
isAdmin
|
||||
? undefined
|
||||
: currentUserId
|
||||
? eq(subscriptions.userId, currentUserId)
|
||||
: undefined,
|
||||
with: {
|
||||
user: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
return c.json(allTopics);
|
||||
return c.json(allTopics);
|
||||
});
|
||||
|
||||
api.get('/topics/requests', requireAdmin, async (c) => {
|
||||
const requests = await db.query.topics.findMany({
|
||||
where: eq(topics.status, 'pending'),
|
||||
with: {
|
||||
creator: true
|
||||
}
|
||||
});
|
||||
return c.json(requests);
|
||||
api.get("/topics/requests", requireAdmin, async (c) => {
|
||||
const requests = await db.query.topics.findMany({
|
||||
where: eq(topics.status, "pending"),
|
||||
with: {
|
||||
creator: true,
|
||||
},
|
||||
});
|
||||
return c.json(requests);
|
||||
});
|
||||
|
||||
api.get('/topics/all', requireAdmin, async (c) => {
|
||||
const allTopics = await db.query.topics.findMany({
|
||||
with: {
|
||||
creator: true,
|
||||
approver: true,
|
||||
subscriptions: true
|
||||
},
|
||||
orderBy: [desc(topics.createdAt)]
|
||||
});
|
||||
return c.json(allTopics);
|
||||
api.get("/topics/all", requireAdmin, async (c) => {
|
||||
const allTopics = await db.query.topics.findMany({
|
||||
with: {
|
||||
creator: true,
|
||||
approver: true,
|
||||
subscriptions: true,
|
||||
},
|
||||
orderBy: [desc(topics.createdAt)],
|
||||
});
|
||||
return c.json(allTopics);
|
||||
});
|
||||
|
||||
api.get('/topics/my-requests', requireAuth, async (c) => {
|
||||
const session = c.get('session');
|
||||
const requests = await db.query.topics.findMany({
|
||||
where: eq(topics.createdBy, session.id),
|
||||
orderBy: [desc(topics.createdAt)],
|
||||
with: {
|
||||
approver: true,
|
||||
}
|
||||
});
|
||||
return c.json(requests);
|
||||
api.get("/topics/my-requests", requireAuth, async (c) => {
|
||||
const session = c.get("session");
|
||||
const requests = await db.query.topics.findMany({
|
||||
where: eq(topics.createdBy, session.id),
|
||||
orderBy: [desc(topics.createdAt)],
|
||||
with: {
|
||||
approver: true,
|
||||
},
|
||||
});
|
||||
return c.json(requests);
|
||||
});
|
||||
|
||||
api.post('/topics/:id/approve', requireAdmin, async (c) => {
|
||||
const id = c.req.param('id');
|
||||
const session = c.get('session');
|
||||
const result = await db.update(topics)
|
||||
.set({ status: 'approved', approvedBy: session.id })
|
||||
.where(eq(topics.id, id))
|
||||
.returning();
|
||||
return c.json(result[0]);
|
||||
api.post("/topics/:id/approve", requireAdmin, async (c) => {
|
||||
const id = c.req.param("id");
|
||||
const session = c.get("session");
|
||||
const result = await db
|
||||
.update(topics)
|
||||
.set({ status: "approved", approvedBy: session.id })
|
||||
.where(eq(topics.id, id))
|
||||
.returning();
|
||||
return c.json(result[0]);
|
||||
});
|
||||
|
||||
api.post('/topics/:id/reject', requireAdmin, async (c) => {
|
||||
const id = c.req.param('id');
|
||||
const result = await db.update(topics)
|
||||
.set({ status: 'rejected' })
|
||||
.where(eq(topics.id, id))
|
||||
.returning();
|
||||
return c.json(result[0]);
|
||||
api.post("/topics/:id/reject", requireAdmin, async (c) => {
|
||||
const id = c.req.param("id");
|
||||
const result = await db
|
||||
.update(topics)
|
||||
.set({ status: "rejected" })
|
||||
.where(eq(topics.id, id))
|
||||
.returning();
|
||||
return c.json(result[0]);
|
||||
});
|
||||
|
||||
// Only admins can create topics
|
||||
// Authenticated users can create topics (requests)
|
||||
api.post('/topics', requireAuth, zValidator('json', topicSchema), async (c) => {
|
||||
const body = c.req.valid('json');
|
||||
const session = c.get('session');
|
||||
api.post("/topics", requireAuth, zValidator("json", topicSchema), async (c) => {
|
||||
const body = c.req.valid("json");
|
||||
const session = c.get("session");
|
||||
|
||||
const status = session.isAdmin ? 'approved' : 'pending';
|
||||
const status = session.isAdmin ? "approved" : "pending";
|
||||
|
||||
const result = await db.insert(topics).values({
|
||||
...body,
|
||||
status,
|
||||
createdBy: session.id,
|
||||
approvedBy: session.isAdmin ? session.id : null,
|
||||
}).returning();
|
||||
return c.json(result[0]);
|
||||
const result = await db
|
||||
.insert(topics)
|
||||
.values({
|
||||
...body,
|
||||
status,
|
||||
createdBy: session.id,
|
||||
approvedBy: session.isAdmin ? session.id : null,
|
||||
})
|
||||
.returning();
|
||||
return c.json(result[0]);
|
||||
});
|
||||
|
||||
// Only admins can update topics
|
||||
api.put('/topics/:id', requireAdmin, zValidator('json', topicSchema.partial()), async (c) => {
|
||||
const id = c.req.param('id');
|
||||
const body = c.req.valid('json');
|
||||
const result = await db.update(topics).set(body).where(eq(topics.id, id)).returning();
|
||||
return c.json(result[0]);
|
||||
});
|
||||
api.put(
|
||||
"/topics/:id",
|
||||
requireAdmin,
|
||||
zValidator("json", topicSchema.partial()),
|
||||
async (c) => {
|
||||
const id = c.req.param("id");
|
||||
const body = c.req.valid("json");
|
||||
const result = await db
|
||||
.update(topics)
|
||||
.set(body)
|
||||
.where(eq(topics.id, id))
|
||||
.returning();
|
||||
return c.json(result[0]);
|
||||
},
|
||||
);
|
||||
|
||||
// Only admins can delete topics
|
||||
api.delete('/topics/:id', requireAdmin, async (c) => {
|
||||
const id = c.req.param('id');
|
||||
await db.delete(topics).where(eq(topics.id, id));
|
||||
return c.json({ success: true });
|
||||
api.delete("/topics/:id", requireAdmin, async (c) => {
|
||||
const id = c.req.param("id");
|
||||
await db.delete(topics).where(eq(topics.id, id));
|
||||
return c.json({ success: true });
|
||||
});
|
||||
|
||||
// --- Users ---
|
||||
|
||||
api.get('/users', requireAdmin, async (c) => {
|
||||
const allUsers = await db.query.users.findMany({
|
||||
with: {
|
||||
subscriptions: {
|
||||
with: {
|
||||
topic: true
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
return c.json(allUsers);
|
||||
api.get("/users", requireAdmin, async (c) => {
|
||||
const allUsers = await db.query.users.findMany({
|
||||
with: {
|
||||
subscriptions: {
|
||||
with: {
|
||||
topic: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
return c.json(allUsers);
|
||||
});
|
||||
|
||||
api.post('/users', requireAdmin, zValidator('json', userSchema), async (c) => {
|
||||
const body = c.req.valid('json');
|
||||
const result = await db.insert(users).values(body).returning();
|
||||
return c.json(result[0]);
|
||||
api.post("/users", requireAdmin, zValidator("json", userSchema), async (c) => {
|
||||
const body = c.req.valid("json");
|
||||
const result = await db.insert(users).values(body).returning();
|
||||
return c.json(result[0]);
|
||||
});
|
||||
|
||||
api.put('/users/:id', requireAdmin, zValidator('json', userSchema.partial()), async (c) => {
|
||||
const id = c.req.param('id');
|
||||
const body = c.req.valid('json');
|
||||
const result = await db.update(users).set(body).where(eq(users.id, id)).returning();
|
||||
return c.json(result[0]);
|
||||
});
|
||||
api.put(
|
||||
"/users/:id",
|
||||
requireAdmin,
|
||||
zValidator("json", userSchema.partial()),
|
||||
async (c) => {
|
||||
const id = c.req.param("id");
|
||||
const body = c.req.valid("json");
|
||||
const result = await db
|
||||
.update(users)
|
||||
.set(body)
|
||||
.where(eq(users.id, id))
|
||||
.returning();
|
||||
return c.json(result[0]);
|
||||
},
|
||||
);
|
||||
|
||||
api.delete('/users/:id', requireAdmin, async (c) => {
|
||||
const id = c.req.param('id');
|
||||
await db.delete(users).where(eq(users.id, id));
|
||||
return c.json({ success: true });
|
||||
api.delete("/users/:id", requireAdmin, async (c) => {
|
||||
const id = c.req.param("id");
|
||||
await db.delete(users).where(eq(users.id, id));
|
||||
return c.json({ success: true });
|
||||
});
|
||||
|
||||
// --- Subscriptions ---
|
||||
@@ -176,159 +210,184 @@ api.delete('/users/:id', requireAdmin, async (c) => {
|
||||
// --- Subscriptions ---
|
||||
|
||||
// Users can subscribe themselves or admins can subscribe anyone
|
||||
api.post('/topics/:topicId/subscribe/:userId', requireAuth, async (c) => {
|
||||
const { topicId, userId } = c.req.param();
|
||||
const session = c.get('session');
|
||||
api.post("/topics/:topicId/subscribe/:userId", requireAuth, async (c) => {
|
||||
const { topicId, userId } = c.req.param();
|
||||
const session = c.get("session");
|
||||
|
||||
// Check if user is subscribing themselves or is an admin
|
||||
if (session.id !== userId && !session.isAdmin) {
|
||||
return c.json({ error: 'You can only subscribe yourself' }, 403);
|
||||
}
|
||||
// Check if user is subscribing themselves or is an admin
|
||||
if (session.id !== userId && !session.isAdmin) {
|
||||
return c.json({ error: "You can only subscribe yourself" }, 403);
|
||||
}
|
||||
|
||||
const result = await db.insert(subscriptions).values({ topicId, userId }).returning();
|
||||
return c.json(result[0]);
|
||||
const result = await db
|
||||
.insert(subscriptions)
|
||||
.values({ topicId, userId })
|
||||
.returning();
|
||||
return c.json(result[0]);
|
||||
});
|
||||
|
||||
// Users can unsubscribe themselves or admins can unsubscribe anyone
|
||||
api.delete('/topics/:topicId/subscribe/:userId', requireAuth, async (c) => {
|
||||
const { topicId, userId } = c.req.param();
|
||||
const session = c.get('session');
|
||||
api.delete("/topics/:topicId/subscribe/:userId", requireAuth, async (c) => {
|
||||
const { topicId, userId } = c.req.param();
|
||||
const session = c.get("session");
|
||||
|
||||
// Check if user is unsubscribing themselves or is an admin
|
||||
if (session.id !== userId && !session.isAdmin) {
|
||||
return c.json({ error: 'You can only unsubscribe yourself' }, 403);
|
||||
}
|
||||
// Check if user is unsubscribing themselves or is an admin
|
||||
if (session.id !== userId && !session.isAdmin) {
|
||||
return c.json({ error: "You can only unsubscribe yourself" }, 403);
|
||||
}
|
||||
|
||||
await db.delete(subscriptions)
|
||||
.where(and(
|
||||
eq(subscriptions.topicId, topicId),
|
||||
eq(subscriptions.userId, userId)
|
||||
));
|
||||
return c.json({ success: true });
|
||||
await db
|
||||
.delete(subscriptions)
|
||||
.where(
|
||||
and(eq(subscriptions.topicId, topicId), eq(subscriptions.userId, userId)),
|
||||
);
|
||||
return c.json({ success: true });
|
||||
});
|
||||
|
||||
// --- Group Bindings (App Bot) ---
|
||||
|
||||
// Get list of known groups (for selection)
|
||||
api.get('/groups', requireAuth, async (c) => {
|
||||
// Return recent active groups
|
||||
const groups = await db.select().from(knownGroupChats)
|
||||
.orderBy(desc(knownGroupChats.lastActiveAt))
|
||||
.limit(50);
|
||||
return c.json(groups);
|
||||
api.get("/groups", requireAuth, async (c) => {
|
||||
// Return recent active groups
|
||||
const groups = await db
|
||||
.select()
|
||||
.from(knownGroupChats)
|
||||
.orderBy(desc(knownGroupChats.lastActiveAt))
|
||||
.limit(50);
|
||||
return c.json(groups);
|
||||
});
|
||||
|
||||
// Get bindings for a topic
|
||||
api.get('/topics/:id/groups', requireAuth, async (c) => {
|
||||
const topicId = c.req.param('id');
|
||||
const groups = await db.select().from(topicGroupChats)
|
||||
.where(eq(topicGroupChats.topicId, topicId))
|
||||
.orderBy(desc(topicGroupChats.createdAt));
|
||||
return c.json(groups);
|
||||
api.get("/topics/:id/groups", requireAuth, async (c) => {
|
||||
const topicId = c.req.param("id");
|
||||
const groups = await db
|
||||
.select()
|
||||
.from(topicGroupChats)
|
||||
.where(eq(topicGroupChats.topicId, topicId))
|
||||
.orderBy(desc(topicGroupChats.createdAt));
|
||||
return c.json(groups);
|
||||
});
|
||||
|
||||
// Bind a group to a topic
|
||||
api.post('/topics/:id/groups', requireAuth, zValidator('json', groupBindingSchema), async (c) => {
|
||||
const topicId = c.req.param('id');
|
||||
const body = c.req.valid('json');
|
||||
const session = c.get('session');
|
||||
api.post(
|
||||
"/topics/:id/groups",
|
||||
requireAuth,
|
||||
zValidator("json", groupBindingSchema),
|
||||
async (c) => {
|
||||
const topicId = c.req.param("id");
|
||||
const body = c.req.valid("json");
|
||||
const session = c.get("session");
|
||||
|
||||
const result = await db.insert(topicGroupChats).values({
|
||||
topicId,
|
||||
chatId: body.chatId,
|
||||
name: body.name,
|
||||
createdBy: session.id,
|
||||
}).returning();
|
||||
const result = await db
|
||||
.insert(topicGroupChats)
|
||||
.values({
|
||||
topicId,
|
||||
chatId: body.chatId,
|
||||
name: body.name,
|
||||
createdBy: session.id,
|
||||
})
|
||||
.returning();
|
||||
|
||||
return c.json(result[0]);
|
||||
});
|
||||
return c.json(result[0]);
|
||||
},
|
||||
);
|
||||
|
||||
// Unbind a group
|
||||
api.delete('/topics/:id/groups/:bindingId', requireAuth, async (c) => {
|
||||
const { id: topicId, bindingId } = c.req.param();
|
||||
api.delete("/topics/:id/groups/:bindingId", requireAuth, async (c) => {
|
||||
const { id: topicId, bindingId } = c.req.param();
|
||||
|
||||
await db.delete(topicGroupChats)
|
||||
.where(and(
|
||||
eq(topicGroupChats.id, bindingId),
|
||||
eq(topicGroupChats.topicId, topicId)
|
||||
));
|
||||
await db
|
||||
.delete(topicGroupChats)
|
||||
.where(
|
||||
and(
|
||||
eq(topicGroupChats.id, bindingId),
|
||||
eq(topicGroupChats.topicId, topicId),
|
||||
),
|
||||
);
|
||||
|
||||
return c.json({ success: true });
|
||||
return c.json({ success: true });
|
||||
});
|
||||
|
||||
// --- Alert Tasks ---
|
||||
|
||||
api.get('/alerts/tasks', requireAdmin, async (c) => {
|
||||
const limit = Math.min(Number(c.req.query('limit') || 50), 100);
|
||||
const tasks = await db.query.alertTasks.findMany({
|
||||
orderBy: [desc(alertTasks.createdAt)],
|
||||
limit,
|
||||
with: {
|
||||
sender: true,
|
||||
logs: {
|
||||
limit: 10, // Only show first 10 logs inline
|
||||
}
|
||||
}
|
||||
});
|
||||
return c.json(tasks);
|
||||
api.get("/alerts/tasks", requireAdmin, async (c) => {
|
||||
const limit = Math.min(Number(c.req.query("limit") || 50), 100);
|
||||
const tasks = await db.query.alertTasks.findMany({
|
||||
orderBy: [desc(alertTasks.createdAt)],
|
||||
limit,
|
||||
with: {
|
||||
sender: true,
|
||||
logs: {
|
||||
limit: 10, // Only show first 10 logs inline
|
||||
},
|
||||
},
|
||||
});
|
||||
return c.json(tasks);
|
||||
});
|
||||
|
||||
api.get('/alerts/tasks/:id', requireAuth, async (c) => {
|
||||
const id = c.req.param('id');
|
||||
const task = await db.query.alertTasks.findFirst({
|
||||
where: eq(alertTasks.id, id),
|
||||
with: {
|
||||
sender: true,
|
||||
logs: true // Show all logs for detail view
|
||||
}
|
||||
});
|
||||
api.get("/alerts/tasks/:id", requireAuth, async (c) => {
|
||||
const id = c.req.param("id");
|
||||
const task = await db.query.alertTasks.findFirst({
|
||||
where: eq(alertTasks.id, id),
|
||||
with: {
|
||||
sender: true,
|
||||
logs: true, // Show all logs for detail view
|
||||
},
|
||||
});
|
||||
|
||||
if (!task) {
|
||||
return c.json({ error: 'Task not found' }, 404);
|
||||
}
|
||||
if (!task) {
|
||||
return c.json({ error: "Task not found" }, 404);
|
||||
}
|
||||
|
||||
return c.json(task);
|
||||
return c.json(task);
|
||||
});
|
||||
|
||||
// --- Stats ---
|
||||
|
||||
api.get('/stats', requireAdmin, async (c) => {
|
||||
// 1. Message count per topic
|
||||
const topicStats = await db.select({
|
||||
topicSlug: alertTasks.topicSlug,
|
||||
totalTasks: count(),
|
||||
totalRecipients: sql<number>`cast(${sum(alertTasks.recipientCount)} as int)`,
|
||||
totalSuccess: sql<number>`cast(${sum(alertTasks.successCount)} as int)`,
|
||||
})
|
||||
.from(alertTasks)
|
||||
.groupBy(alertTasks.topicSlug);
|
||||
api.get("/stats", requireAdmin, async (c) => {
|
||||
// 1. Message count per topic
|
||||
const topicStats = await db
|
||||
.select({
|
||||
topicSlug: alertTasks.topicSlug,
|
||||
totalTasks: count(),
|
||||
totalRecipients: sql<number>`cast(${sum(alertTasks.recipientCount)} as int)`,
|
||||
totalSuccess: sql<number>`cast(${sum(alertTasks.successCount)} as int)`,
|
||||
})
|
||||
.from(alertTasks)
|
||||
.groupBy(alertTasks.topicSlug);
|
||||
|
||||
// 2. Recent metrics (last 24h)
|
||||
const last24h = new Date(Date.now() - 24 * 60 * 60 * 1000);
|
||||
const recentStats = await db.select({
|
||||
totalRecipients: sql<number>`cast(${sum(alertTasks.recipientCount)} as int)`,
|
||||
totalSuccess: sql<number>`cast(${sum(alertTasks.successCount)} as int)`,
|
||||
taskCount: count(),
|
||||
})
|
||||
.from(alertTasks)
|
||||
.where(gt(alertTasks.createdAt, last24h));
|
||||
// 2. Recent metrics (last 24h)
|
||||
const last24h = new Date(Date.now() - 24 * 60 * 60 * 1000);
|
||||
const recentStats = await db
|
||||
.select({
|
||||
totalRecipients: sql<number>`cast(${sum(alertTasks.recipientCount)} as int)`,
|
||||
totalSuccess: sql<number>`cast(${sum(alertTasks.successCount)} as int)`,
|
||||
taskCount: count(),
|
||||
})
|
||||
.from(alertTasks)
|
||||
.where(gt(alertTasks.createdAt, last24h));
|
||||
|
||||
const recent = recentStats[0] || { totalRecipients: 0, totalSuccess: 0, taskCount: 0 };
|
||||
const totalRecipients = Number(recent.totalRecipients || 0);
|
||||
const totalSuccess = Number(recent.totalSuccess || 0);
|
||||
const failedCount = totalRecipients - totalSuccess;
|
||||
const successRate = totalRecipients > 0 ? (totalSuccess / totalRecipients) * 100 : 100;
|
||||
const recent = recentStats[0] || {
|
||||
totalRecipients: 0,
|
||||
totalSuccess: 0,
|
||||
taskCount: 0,
|
||||
};
|
||||
const totalRecipients = Number(recent.totalRecipients || 0);
|
||||
const totalSuccess = Number(recent.totalSuccess || 0);
|
||||
const failedCount = totalRecipients - totalSuccess;
|
||||
const successRate =
|
||||
totalRecipients > 0 ? (totalSuccess / totalRecipients) * 100 : 100;
|
||||
|
||||
return c.json({
|
||||
topics: topicStats,
|
||||
recent: {
|
||||
alertsReceived: Number(recent.taskCount || 0),
|
||||
plannedMessages: totalRecipients,
|
||||
successCount: totalSuccess,
|
||||
failedCount: failedCount,
|
||||
successRate: successRate,
|
||||
}
|
||||
});
|
||||
return c.json({
|
||||
topics: topicStats,
|
||||
recent: {
|
||||
alertsReceived: Number(recent.taskCount || 0),
|
||||
plannedMessages: totalRecipients,
|
||||
successCount: totalSuccess,
|
||||
failedCount: failedCount,
|
||||
successRate: successRate,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
export default api;
|
||||
|
||||
Reference in New Issue
Block a user