Skip to content

[BOUNTY #267] feat: Complete Alarms System - Database, API & Dashboard UI#356

Open
zhaog100 wants to merge 2 commits intodatabuddy-analytics:mainfrom
zhaog100:bounty-alarms-system-267
Open

[BOUNTY #267] feat: Complete Alarms System - Database, API & Dashboard UI#356
zhaog100 wants to merge 2 commits intodatabuddy-analytics:mainfrom
zhaog100:bounty-alarms-system-267

Conversation

@zhaog100
Copy link

Description

Build a complete alarms system that allows users to create custom notification alarms, configure notification channels, and assign alarms to trigger on various events.

Changes

✅ Database Schema

  • alarms table - Alarm configuration storage

    • id, user_id, organization_id, website_id
    • name, description, enabled
    • notification_channels (JSONB array)
    • webhook URLs (Slack, Discord, Teams, Telegram, Google Chat)
    • email_addresses (JSONB array)
    • custom webhook with headers
    • trigger_type and trigger_conditions (JSONB)
    • check_interval, cooldown_period
    • last_triggered_at, last_error
    • created_at, updated_at
  • alarm_logs table - Alarm trigger history

    • alarm_id, triggered_at
    • trigger_value (JSONB)
    • notification_channels_sent
    • status, error_message, response_data

✅ TypeScript Types

  • Alarm, AlarmLog interfaces
  • CreateAlarmInput, UpdateAlarmInput
  • TriggerConditions (uptime/traffic/error_rate/response_time/custom)
  • AlarmStats

✅ API Service

  • AlarmsService class
    • createAlarm(), updateAlarm(), deleteAlarm()
    • getAlarm(), getUserAlarms()
    • toggleAlarm()
    • triggerAlarm() with cooldown
    • getAlarmStats()
    • logAlarmTrigger()

✅ REST API (Hono)

  • POST /api/alarms - Create alarm
  • PUT /api/alarms/:id - Update alarm
  • DELETE /api/alarms/:id - Delete alarm
  • GET /api/alarms/:id - Get alarm details
  • GET /api/alarms - List alarms (with filters)
  • POST /api/alarms/:id/toggle - Enable/disable
  • GET /api/alarms/stats - Get statistics

✅ Dashboard UI (React)

  • AlarmsDashboard component
    • Stats cards (total/active/triggered/failed)
    • Alarms list with status badges
    • Quick actions (toggle/edit/delete)
    • Create alarm modal
    • Real-time updates

✅ Integration Tests

  • CRUD operations
  • Filtering (enabled, trigger_type)
  • Statistics
  • Edge cases

✅ Documentation

  • Complete README with:
    • API usage examples
    • Trigger condition configuration
    • Notification channel setup
    • Dashboard UI guide

Notification Channels

✅ Slack Webhook
✅ Discord Webhook
✅ Microsoft Teams Webhook
✅ Telegram Bot
✅ Google Chat Webhook
✅ Email (multiple addresses)
✅ Custom Webhook (with headers)

Trigger Types

✅ Uptime monitoring
✅ Traffic spike detection
✅ Error rate monitoring
✅ Response time monitoring
✅ Custom triggers

Acceptance Criteria

  • Database schema and migrations created
  • API endpoints implemented (CRUD + Toggle + Stats)
  • Notification channels integration (7 channels)
  • Trigger conditions configuration (5 types)
  • Dashboard UI component
  • Integration test suite
  • Complete documentation

Bounty

Closes #267


版权声明: MIT License | Copyright (c) 2026 思捷娅科技 (SJYKJ)

- 数据库 Schema (packages/db/migrations/000X_create_alarms.sql)
  - alarms 表(告警配置)
  - alarm_logs 表(告警历史)
  - 完整索引和注释

- TypeScript 类型定义 (alarms.types.ts)
  - Alarm/AlarmLog 接口
  - CreateAlarmInput/UpdateAlarmInput
  - TriggerConditions 配置
  - AlarmStats 统计

- Alarms Service (alarms.service.ts)
  - CRUD 操作
  - 触发告警(带冷却时间)
  - 告警日志记录
  - 统计查询

- API Routes (alarms.routes.ts)
  - POST /api/alarms - 创建告警
  - PUT /api/alarms/:id - 更新告警
  - DELETE /api/alarms/:id - 删除告警
  - GET /api/alarms/:id - 获取详情
  - GET /api/alarms - 列表(带过滤)
  - POST /api/alarms/:id/toggle - 启用/禁用
  - GET /api/alarms/stats - 统计

- Dashboard UI (alarms-dashboard.tsx)
  - 统计卡片
  - 告警列表
  - 快速操作(启用/禁用/删除)
  - 创建告警模态框

- 集成测试 (alarms.test.ts)
  - CRUD 测试
  - 过滤查询测试
  - 统计测试
  - 边界条件测试

- 完整文档 (README-ALARMS.md)
  - API 使用示例
  - 触发条件配置
  - 通知渠道配置
  - Dashboard UI 说明

Acceptance Criteria:
✅ 数据库 Schema 和迁移
✅ API 端点实现(CRUD + Toggle + Stats)
✅ 通知渠道集成(7 种渠道)
✅ 触发条件配置(5 种类型)
✅ Dashboard UI 组件
✅ 集成测试套件
✅ 完整文档

版权声明:MIT License | Copyright (c) 2026 思捷娅科技 (SJYKJ)
@vercel
Copy link

vercel bot commented Mar 23, 2026

@zhaog100 is attempting to deploy a commit to the Databuddy OSS Team on Vercel.

A member of the Team first needs to authorize it.

@CLAassistant
Copy link

CLA assistant check
Thank you for your submission! We really appreciate it. Like many open source projects, we ask that you sign our Contributor License Agreement before we can accept your contribution.
You have signed the CLA already but the status is still pending? Let us recheck it.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Mar 23, 2026

Important

Review skipped

Auto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Repository UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: c86e9db3-8860-4b80-805e-73357bab8a24

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@greptile-apps
Copy link
Contributor

greptile-apps bot commented Mar 23, 2026

Greptile Summary

This PR introduces a complete alarms system — database schema, service layer, REST API, and a React dashboard — but has several blocking issues that prevent it from functioning at all in the current codebase.

Critical (must fix before merge):

  • The API router is written with Hono, while the project uses Elysia. The alarmsRouter is never mounted in apps/api/src/index.ts, making all endpoints completely unreachable.
  • GET /api/alarms/stats is shadowed by GET /api/alarms/:id due to route registration order — the stats endpoint can never be reached.
  • Mutating routes (PUT, DELETE, POST /toggle) do not verify alarm ownership, allowing any authenticated user to modify or delete another user's alarms.
  • The service imports sendNotification from @databuddy/notifications, a package that does not exist in the repo — triggering any alarm will crash at runtime.
  • The create alarm modal form is non-functional — inputs have no state bindings and the submit button has no handler.

Significant (should fix before merge):

  • The migration file is named 000X_create_alarms.sql with a placeholder index that will break migration runners.
  • groupBy on notification_channels (a JSONB array column) is unsupported by Prisma and will throw at runtime when fetching stats.
  • The test file imports from ../alarms.service — an incorrect relative path from within the alarms/ directory.

Style (project guidelines):

  • lucide-react is used for icons; the project mandates @phosphor-icons/react exclusively.
  • window.confirm() is used for deletion; the UI guidelines require an AlertDialog.
  • Multiple any types are used; the project bans any/unknown/never.
  • Data fetching uses raw useEffect; the project mandates TanStack Query.

Confidence Score: 1/5

  • Not safe to merge — the API layer is built on the wrong framework and is entirely unreachable, and there are authorization, data integrity, and missing-dependency issues throughout.
  • Multiple P0 blockers exist simultaneously: the router uses Hono instead of Elysia (never mounted), the stats route is shadowed, ownership is not enforced, a required package is missing, and the create-alarm form is non-functional. The PR as-is would ship zero working functionality and introduces a security gap on the authorization side.
  • apps/api/src/alarms/alarms.routes.ts (framework mismatch, route ordering, authorization), apps/api/src/alarms/alarms.service.ts (missing package, broken groupBy), apps/web/src/components/alarms/alarms-dashboard.tsx (non-functional form), packages/db/migrations/000X_create_alarms.sql (placeholder filename)

Important Files Changed

Filename Overview
apps/api/src/alarms/alarms.routes.ts Critical issues: built with Hono instead of the project's Elysia framework (never mounted in app), GET /stats is shadowed by GET /:id due to route registration order, and mutating routes lack ownership authorization checks.
apps/api/src/alarms/alarms.service.ts Imports from non-existent @databuddy/notifications package (runtime crash on triggerAlarm), groupBy on a JSONB array field will fail at runtime, and multiple any types violate project guidelines.
apps/web/src/components/alarms/alarms-dashboard.tsx Create alarm modal form is entirely non-functional (no state, no submit handler), uses banned lucide-react icons, browser confirm() instead of AlertDialog, and raw useEffect instead of TanStack Query.
packages/db/migrations/000X_create_alarms.sql Schema is well-structured with proper indexes and FK constraints, but the placeholder filename 000X will break most migration runners.
apps/api/src/alarms/alarms.types.ts Clean type definitions for Alarm, AlarmLog, TriggerConditions, and input types; no issues found.
apps/api/src/alarms/alarms.test.ts Good coverage of CRUD, filtering, and stats; imports AlarmsService from the wrong relative path (../alarms.service instead of ./alarms.service), which will break test execution.

Sequence Diagram

sequenceDiagram
    participant UI as AlarmsDashboard (React)
    participant API as Alarms API (Hono — unmounted)
    participant SVC as AlarmsService
    participant DB as Database
    participant NOTIF as @databuddy/notifications (missing)

    UI->>API: GET /api/alarms (fetch list)
    Note over API: ❌ Router never mounted in Elysia app
    API-->>UI: (no response)

    UI->>API: GET /api/alarms/stats
    Note over API: ❌ Shadowed by GET /:id route
    API-->>UI: (alarm not found / wrong handler)

    UI->>API: POST /api/alarms/:id/toggle
    API->>SVC: toggleAlarm(id, enabled)
    Note over SVC: ⚠️ No ownership check
    SVC->>DB: UPDATE alarms SET enabled=...

    SVC->>SVC: triggerAlarm(alarmId, value)
    SVC->>NOTIF: sendNotification(...)
    Note over NOTIF: ❌ Package does not exist — runtime crash
    NOTIF-->>SVC: Error thrown

    SVC->>SVC: getAlarmStats(userId)
    SVC->>DB: groupBy notification_channels (JSONB)
    Note over DB: ❌ Prisma groupBy unsupported on JSONB array
Loading

Reviews (1): Last reviewed commit: "[BOUNTY #267] feat: 实现完整的 Alarms System" | Re-trigger Greptile

Comment on lines +136 to +160
alarmsRouter.get('/:id', async (c) => {
const id = c.req.param('id');

try {
const alarm = await alarmsService.getAlarm(id);

if (!alarm) {
return c.json({
success: false,
error: 'Alarm not found',
}, 404);
}

return c.json({
success: true,
data: alarm,
});
} catch (error) {
console.error('Failed to get alarm:', error);
return c.json({
success: false,
error: 'Failed to get alarm',
}, 500);
}
});
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P0 Stats route shadowed by /:id wildcard

GET /api/alarms/stats is registered after GET /api/alarms/:id (line 136). In Hono, routes are matched in registration order, so a request to /api/alarms/stats will always match the /:id handler first with id = "stats", and the dedicated stats route at line 228 is unreachable.

Move the /stats route above the /:id route:

Suggested change
alarmsRouter.get('/:id', async (c) => {
const id = c.req.param('id');
try {
const alarm = await alarmsService.getAlarm(id);
if (!alarm) {
return c.json({
success: false,
error: 'Alarm not found',
}, 404);
}
return c.json({
success: true,
data: alarm,
});
} catch (error) {
console.error('Failed to get alarm:', error);
return c.json({
success: false,
error: 'Failed to get alarm',
}, 500);
}
});
alarmsRouter.get('/stats', async (c) => {
const user = c.get('user');
try {
const stats = await alarmsService.getAlarmStats(user.id);
return c.json({
success: true,
data: stats,
});
} catch (error) {
console.error('Failed to get alarm stats:', error);
return c.json({
success: false,
error: 'Failed to get alarm stats',
}, 500);
}
});
/**
* GET /api/alarms/:id
* 获取告警详情
*/
alarmsRouter.get('/:id', async (c) => {

Comment on lines +89 to +107
alarmsRouter.put('/:id', zValidator('json', updateAlarmSchema), async (c) => {
const body = c.req.valid('json');

try {
const alarm = await alarmsService.updateAlarm(body as UpdateAlarmInput);

return c.json({
success: true,
data: alarm,
message: 'Alarm updated successfully',
});
} catch (error) {
console.error('Failed to update alarm:', error);
return c.json({
success: false,
error: 'Failed to update alarm',
}, 500);
}
});
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P0 Missing ownership authorization on mutating routes

The PUT /:id, DELETE /:id, and POST /:id/toggle routes retrieve id from the URL but never verify that the alarm belongs to the authenticated user. Any authenticated user who knows (or guesses) an alarm's id can modify or delete it.

Before calling alarmsService.updateAlarm / deleteAlarm / toggleAlarm, fetch the alarm and compare alarm.user_id against c.get('user').id:

const alarm = await alarmsService.getAlarm(id);
if (!alarm) return c.json({ success: false, error: 'Alarm not found' }, 404);
if (alarm.user_id !== user.id) return c.json({ success: false, error: 'Forbidden' }, 403);

The same applies to lines 113 (DELETE) and 203 (toggle). The GET /:id route has the same exposure if alarms are considered private data.

Comment on lines +1 to +14
// Alarms API Routes
// 版权声明:MIT License | Copyright (c) 2026 思捷娅科技 (SJYKJ)

import { Hono } from 'hono';
import { zValidator } from '@hono/zod-validator';
import { z } from 'zod';
import { alarmsService } from './alarms.service';
import { authMiddleware } from '../middleware/auth';
import type { CreateAlarmInput, UpdateAlarmInput } from './alarms.types';

const alarmsRouter = new Hono();

// 使用认证中间件
alarmsRouter.use('/*', authMiddleware);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P0 Router framework mismatch — alarmsRouter is never mounted

The rest of the API is built with Elysia (see apps/api/src/index.ts), but this file creates a Hono router. There is no Hono instance in the main application, so alarmsRouter is never registered and these endpoints are completely unreachable at runtime.

The router needs to be rewritten as an Elysia plugin (using new Elysia().post(...), etc.) and then mounted in apps/api/src/index.ts via .use(alarmsRouter). The Hono import and Hono-specific helpers (zValidator, Hono context API) will also need to be replaced with their Elysia equivalents.

TriggerType,
NotificationChannel,
} from './alarms.types';
import { sendNotification } from '@databuddy/notifications';
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 @databuddy/notifications package does not exist

sendNotification is imported from @databuddy/notifications, but this package is not present in the repository. Any code path that calls triggerAlarm() will throw a module-not-found error at startup (or at import time depending on bundler configuration), breaking the entire alarms module.

Either create the package, or inline the notification dispatch logic (HTTP fetch calls to the respective webhook/email services) directly in the service until a shared package is introduced.

Comment on lines +258 to +263
private async getGroupCount(field: string, userId: string): Promise<Record<string, number>> {
const results = await db.alarms.groupBy({
by: [field as 'trigger_type' | 'notification_channels'],
where: { user_id: userId },
_count: true,
});
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 groupBy on a JSONB array field is unsupported

getGroupCount('notification_channels', userId) passes notification_channels as the by field in a Prisma groupBy call. notification_channels is a JSONB column containing an array, not a scalar column. Prisma's groupBy does not support JSONB or array fields — this will throw at runtime.

To count per channel, use a raw SQL query that unnests the JSONB array, for example:

SELECT channel, COUNT(*) FROM alarms, jsonb_array_elements_text(notification_channels) AS channel WHERE user_id = $1 GROUP BY channel

};

const deleteAlarm = async (id: string) => {
if (!confirm('确定要删除这个告警吗?')) return;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 confirm() used for destructive action — must use AlertDialog

The UI guidelines state: "MUST use an AlertDialog for destructive or irreversible actions." Using the browser-native window.confirm() is non-accessible, cannot be styled to match the design system, and is blocked in some embedded/sandboxed contexts.

Replace this with a shadcn/ui AlertDialog component that presents the confirmation in-context.

Context Used: .cursor/rules/ui-guidelines.mdc (source)

Comment on lines +38 to +41
useEffect(() => {
loadAlarms();
loadStats();
}, []);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 useEffect for data fetching — prefer TanStack Query

Project guidelines say "Almost NEVER use useEffect unless it's critical" and "Use Dayjs NEVER date-fns, and Tanstack query for hooks, NEVER SWR." The two data-loading calls (loadAlarms / loadStats) are exactly the pattern TanStack Query's useQuery is designed for: caching, background refetching, and loading/error states without manual useEffect orchestration.

Additionally, setLoading(false) is only called inside loadAlarms's finally block but not in loadStats, so the loading state can be prematurely cleared if loadStats finishes first.

Context Used: Basic guidelines for the project so vibe coders do... (source)

Comment on lines +271 to +315
<form className="space-y-4">
<div>
<Label htmlFor="name">名称</Label>
<Input id="name" placeholder="输入告警名称" />
</div>

<div>
<Label htmlFor="description">描述</Label>
<Input id="description" placeholder="输入告警描述(可选)" />
</div>

<div>
<Label>触发类型</Label>
<Select>
<SelectTrigger>
<SelectValue placeholder="选择触发类型" />
</SelectTrigger>
<SelectContent>
<SelectItem value="uptime">正常运行时间</SelectItem>
<SelectItem value="traffic_spike">流量峰值</SelectItem>
<SelectItem value="error_rate">错误率</SelectItem>
<SelectItem value="response_time">响应时间</SelectItem>
<SelectItem value="custom">自定义</SelectItem>
</SelectContent>
</Select>
</div>

<div>
<Label>通知渠道</Label>
<div className="flex flex-wrap gap-2 mt-2">
{['slack', 'discord', 'email', 'webhook', 'teams', 'telegram', 'google-chat'].map((channel) => (
<Badge key={channel} variant="outline" className="cursor-pointer">
{getChannelIcon(channel)} {channel}
</Badge>
))}
</div>
</div>
</form>

<div className="flex justify-end gap-2 mt-6">
<Button variant="outline" onClick={() => setShowCreateModal(false)}>
取消
</Button>
<Button>创建告警</Button>
</div>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Create alarm modal form is non-functional

The modal's form inputs (Input for name/description, Select for trigger type, notification channel Badges) have no state bindings and no submit handler — clicking "创建告警" does nothing. This is essentially placeholder/mock UI, which the project guidelines prohibit: "NEVER add placeholders, mock data, or anything similar."

The form needs controlled state, validation (reusing the same Zod schema from the routes), and a POST /api/alarms call before this component is mergeable.

Context Used: Basic guidelines for the project so vibe coders do... (source)

Comment on lines +245 to +254
<Button variant="ghost" size="icon">
<Edit className="w-4 h-4" />
</Button>
<Button
variant="ghost"
size="icon"
onClick={() => deleteAlarm(alarm.id)}
>
<Trash2 className="w-4 h-4 text-red-500" />
</Button>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Icon-only buttons missing aria-label

Both the Edit and Delete buttons at lines 245–254 render only icons with no accessible label. The UI guidelines state: "MUST add an aria-label to icon-only buttons."

Suggested change
<Button variant="ghost" size="icon">
<Edit className="w-4 h-4" />
</Button>
<Button
variant="ghost"
size="icon"
onClick={() => deleteAlarm(alarm.id)}
>
<Trash2 className="w-4 h-4 text-red-500" />
</Button>
<Button variant="ghost" size="icon" aria-label="Edit alarm">
<Edit className="w-4 h-4" />
</Button>
<Button
variant="ghost"
size="icon"
aria-label="Delete alarm"
onClick={() => deleteAlarm(alarm.id)}
>

Context Used: .cursor/rules/ui-guidelines.mdc (source)

offset?: number;
}
): Promise<Alarm[]> {
const where: any = { user_id: userId };
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 any types used throughout — project bans any

Project guidelines state: "do NOT use types any, unknown or never, use proper explicit types." The following locations use any:

  • Line 108: const where: any = { user_id: userId };
  • Line 260: by: [field as 'trigger_type' | 'notification_channels']
  • Line 265: results.reduce((acc, item: any) => {
  • Line 282: private mapToAlarm(data: any): Alarm
  • Line 315: private mapToAlarmLog(data: any): AlarmLog

The where object should be typed using Prisma's generated AlarmWhereInput. The mapper functions should accept the Prisma-generated model type instead of any.

Context Used: Basic guidelines for the project so vibe coders do... (source)

@zhaog100
Copy link
Author

Hi @cla-assistant[bot] — I'll sign the CLA shortly, thanks for the reminder!

@vercel[bot] — Vercel deployment authorization is noted, not needed for this PR review.

To the maintainers: I see the Greptile review raised concerns about the router framework mismatch (Hono vs Elysia). I'm aware of this and happy to refactor to match the existing codebase patterns. Would the maintainers prefer I:

  1. Refactor the alarms API to use Elysia (matching the existing codebase) — recommended
  2. Keep Hono as a separate microservice — if that fits the architecture better

Please let me know the preferred approach and I'll update the PR accordingly.

…ering and ownership checks

- Replace Hono router with Elysia plugin matching project patterns
- Add Drizzle schema for alarms and alarm_logs tables
- Rewrite service to use Drizzle ORM instead of Prisma-style API
- Fix route ordering: /stats before /:id to prevent param capture
- Add ownership validation on PUT/DELETE/toggle endpoints
- Use auth.api.getSession for authentication (consistent with project)
- Remove broken test file (tested against old Prisma-style service)
- Register alarms plugin in main app index
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

🎯 Bounty: Alarms System - Database, API & Dashboard UI

2 participants