From 4b29f7cbed0c92f953f6b250424e878a4e9f1f21 Mon Sep 17 00:00:00 2001 From: headlessdev Date: Thu, 17 Apr 2025 13:07:19 +0200 Subject: [PATCH 01/37] Prisma notification model --- .../migration.sql | 18 ++++++++++++++++++ prisma/schema.prisma | 16 ++++++++++++++++ 2 files changed, 34 insertions(+) create mode 100644 prisma/migrations/20250417110704_notification_model/migration.sql diff --git a/prisma/migrations/20250417110704_notification_model/migration.sql b/prisma/migrations/20250417110704_notification_model/migration.sql new file mode 100644 index 0000000..b8c5876 --- /dev/null +++ b/prisma/migrations/20250417110704_notification_model/migration.sql @@ -0,0 +1,18 @@ +-- CreateTable +CREATE TABLE "notification" ( + "id" SERIAL NOT NULL, + "enabled" BOOLEAN NOT NULL DEFAULT true, + "type" TEXT NOT NULL, + "smtpHost" TEXT, + "smtpPort" INTEGER, + "smtpFrom" TEXT, + "smtpUser" TEXT, + "smtpPass" TEXT, + "smtpSecure" BOOLEAN, + "smtpTo" TEXT, + "telegramChatId" TEXT, + "telegramToken" TEXT, + "discordWebhook" TEXT, + + CONSTRAINT "notification_pkey" PRIMARY KEY ("id") +); diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 55fe57d..d963a67 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -53,4 +53,20 @@ model user { id String @id @default(uuid()) email String @unique password String +} + +model notification { + id Int @id @default(autoincrement()) + enabled Boolean @default(true) + type String + smtpHost String? + smtpPort Int? + smtpFrom String? + smtpUser String? + smtpPass String? + smtpSecure Boolean? + smtpTo String? + telegramChatId String? + telegramToken String? + discordWebhook String? } \ No newline at end of file From 2325f9b042456f60c6763803b84039e5b0ea917f Mon Sep 17 00:00:00 2001 From: headlessdev Date: Thu, 17 Apr 2025 13:08:20 +0200 Subject: [PATCH 02/37] Version to 0.0.5 --- package-lock.json | 15 +++++++++++++++ package.json | 2 +- 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/package-lock.json b/package-lock.json index f295e81..b4859da 100644 --- a/package-lock.json +++ b/package-lock.json @@ -4545,6 +4545,21 @@ "optional": true } } + }, + "node_modules/@next/swc-win32-x64-msvc": { + "version": "15.3.0", + "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-15.3.0.tgz", + "integrity": "sha512-vHUQS4YVGJPmpjn7r5lEZuMhK5UQBNBRSB+iGDvJjaNk649pTIcRluDWNb9siunyLLiu/LDPHfvxBtNamyuLTw==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } } } } diff --git a/package.json b/package.json index 72dbb10..42dd644 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "corecontrol", - "version": "0.0.4", + "version": "0.0.5", "private": true, "scripts": { "dev": "next dev --turbopack", From cecc5e0bab82ef90a764319edf96c9259d4693a2 Mon Sep 17 00:00:00 2001 From: headlessdev Date: Thu, 17 Apr 2025 13:17:22 +0200 Subject: [PATCH 03/37] Add notification settings with alert dialog in Settings component --- app/dashboard/settings/Settings.tsx | 56 ++++++++++++++++++++++++++++- 1 file changed, 55 insertions(+), 1 deletion(-) diff --git a/app/dashboard/settings/Settings.tsx b/app/dashboard/settings/Settings.tsx index 171935f..3d4d4f5 100644 --- a/app/dashboard/settings/Settings.tsx +++ b/app/dashboard/settings/Settings.tsx @@ -33,7 +33,19 @@ import axios from "axios"; import Cookies from "js-cookie"; import { Button } from "@/components/ui/button"; import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert" -import { AlertCircle, Check, Palette, User } from "lucide-react"; +import { AlertCircle, Check, Palette, User, Bell } from "lucide-react"; + +import { + AlertDialog, + AlertDialogAction, + AlertDialogCancel, + AlertDialogContent, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogTitle, + AlertDialogTrigger, +} from "@/components/ui/alert-dialog" export default function Settings() { const { theme, setTheme } = useTheme(); @@ -289,6 +301,48 @@ export default function Settings() { + + + + +
+ +

Notifications

+
+
+ +
+ Set up Notifications to get notified when an application goes offline or online. +
+ + + + + + + Add Notification + + + + + Cancel + Continue + + + +
+
From 2fd8e50f7f3afad25f0b0663ef5e0b21e34b9aaa Mon Sep 17 00:00:00 2001 From: headlessdev Date: Thu, 17 Apr 2025 14:02:54 +0200 Subject: [PATCH 04/37] Add notification settings with SMTP, Telegram, and Discord options in Settings component --- app/dashboard/settings/Settings.tsx | 64 ++++++++++++++++++++++++++++- components/ui/checkbox.tsx | 32 +++++++++++++++ package-lock.json | 50 ++++++++++++++-------- package.json | 1 + 4 files changed, 128 insertions(+), 19 deletions(-) create mode 100644 components/ui/checkbox.tsx diff --git a/app/dashboard/settings/Settings.tsx b/app/dashboard/settings/Settings.tsx index 3d4d4f5..f726ce7 100644 --- a/app/dashboard/settings/Settings.tsx +++ b/app/dashboard/settings/Settings.tsx @@ -46,6 +46,8 @@ import { AlertDialogTitle, AlertDialogTrigger, } from "@/components/ui/alert-dialog" +import { Label } from "@/components/ui/label"; +import { Checkbox } from "@/components/ui/checkbox"; export default function Settings() { const { theme, setTheme } = useTheme(); @@ -63,6 +65,8 @@ export default function Settings() { const [passwordSuccess, setPasswordSuccess] = useState(false) const [emailSuccess, setEmailSuccess] = useState(false) + const [notificationType, setNotificationType] = useState("") + const changeEmail = async () => { setEmailErrorVisible(false); setEmailSuccess(false); @@ -324,7 +328,7 @@ export default function Settings() { Add Notification - setNotificationType(value)}> @@ -333,11 +337,67 @@ export default function Settings() { Telegram Discord + + {notificationType === "smtp" && ( +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ )} + + {notificationType === "telegram" && ( +
+
+ + +
+
+ + +
+
+ )} + + {notificationType === "discord" && ( +
+
+ + +
+
+ )} +
Cancel - Continue + Add
diff --git a/components/ui/checkbox.tsx b/components/ui/checkbox.tsx new file mode 100644 index 0000000..fa0e4b5 --- /dev/null +++ b/components/ui/checkbox.tsx @@ -0,0 +1,32 @@ +"use client" + +import * as React from "react" +import * as CheckboxPrimitive from "@radix-ui/react-checkbox" +import { CheckIcon } from "lucide-react" + +import { cn } from "@/lib/utils" + +function Checkbox({ + className, + ...props +}: React.ComponentProps) { + return ( + + + + + + ) +} + +export { Checkbox } diff --git a/package-lock.json b/package-lock.json index b4859da..c36d905 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,17 +1,18 @@ { "name": "corecontrol", - "version": "0.0.4", + "version": "0.0.5", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "corecontrol", - "version": "0.0.4", + "version": "0.0.5", "dependencies": { "@prisma/client": "^6.6.0", "@prisma/extension-accelerate": "^1.3.0", "@radix-ui/react-accordion": "^1.2.4", "@radix-ui/react-alert-dialog": "^1.1.7", + "@radix-ui/react-checkbox": "^1.1.5", "@radix-ui/react-dialog": "^1.1.7", "@radix-ui/react-dropdown-menu": "^2.1.7", "@radix-ui/react-label": "^2.1.3", @@ -1240,6 +1241,36 @@ } } }, + "node_modules/@radix-ui/react-checkbox": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@radix-ui/react-checkbox/-/react-checkbox-1.1.5.tgz", + "integrity": "sha512-B0gYIVxl77KYDR25AY9EGe/G//ef85RVBIxQvK+m5pxAC7XihAc/8leMHhDvjvhDu02SBSb6BuytlWr/G7F3+g==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.2", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-presence": "1.1.3", + "@radix-ui/react-primitive": "2.0.3", + "@radix-ui/react-use-controllable-state": "1.1.1", + "@radix-ui/react-use-previous": "1.1.1", + "@radix-ui/react-use-size": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-collapsible": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/@radix-ui/react-collapsible/-/react-collapsible-1.1.4.tgz", @@ -4545,21 +4576,6 @@ "optional": true } } - }, - "node_modules/@next/swc-win32-x64-msvc": { - "version": "15.3.0", - "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-15.3.0.tgz", - "integrity": "sha512-vHUQS4YVGJPmpjn7r5lEZuMhK5UQBNBRSB+iGDvJjaNk649pTIcRluDWNb9siunyLLiu/LDPHfvxBtNamyuLTw==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 10" - } } } } diff --git a/package.json b/package.json index 42dd644..8b15dd0 100644 --- a/package.json +++ b/package.json @@ -13,6 +13,7 @@ "@prisma/extension-accelerate": "^1.3.0", "@radix-ui/react-accordion": "^1.2.4", "@radix-ui/react-alert-dialog": "^1.1.7", + "@radix-ui/react-checkbox": "^1.1.5", "@radix-ui/react-dialog": "^1.1.7", "@radix-ui/react-dropdown-menu": "^2.1.7", "@radix-ui/react-label": "^2.1.3", From 346b79ca224e8cff8927f828a3a21f4ed7e7ab1e Mon Sep 17 00:00:00 2001 From: headlessdev Date: Thu, 17 Apr 2025 14:13:21 +0200 Subject: [PATCH 05/37] Implement notification creation and deletion endpoints --- app/api/notifications/add/route.ts | 43 +++++++++++++++++++++++++++ app/api/notifications/delete/route.ts | 21 +++++++++++++ 2 files changed, 64 insertions(+) create mode 100644 app/api/notifications/add/route.ts create mode 100644 app/api/notifications/delete/route.ts diff --git a/app/api/notifications/add/route.ts b/app/api/notifications/add/route.ts new file mode 100644 index 0000000..721bf5a --- /dev/null +++ b/app/api/notifications/add/route.ts @@ -0,0 +1,43 @@ +import { NextResponse, NextRequest } from "next/server"; +import { prisma } from "@/lib/prisma"; + +interface AddRequest { + type: string; + smtpHost?: string; + smtpPort?: number; + smtpSecure?: boolean; + smtpUsername?: string; + smtpPassword?: string; + smtpFrom?: string; + smtpTo?: string; + telegramToken?: string; + telegramChatId?: string; + discordWebhook?: string; +} + +export async function POST(request: NextRequest) { + try { + const body: AddRequest = await request.json(); + const { type, smtpHost, smtpPort, smtpSecure, smtpUsername, smtpPassword, smtpFrom, smtpTo, telegramToken, telegramChatId, discordWebhook } = body; + + const notification = await prisma.notification.create({ + data: { + type: type, + smtpHost: smtpHost, + smtpPort: smtpPort, + smtpFrom: smtpFrom, + smtpUser: smtpUsername, + smtpPass: smtpPassword, + smtpSecure: smtpSecure, + smtpTo: smtpTo, + telegramChatId: telegramChatId, + telegramToken: telegramToken, + discordWebhook: discordWebhook, + } + }); + + return NextResponse.json({ message: "Success", notification }); + } catch (error: any) { + return NextResponse.json({ error: error.message }, { status: 500 }); + } +} diff --git a/app/api/notifications/delete/route.ts b/app/api/notifications/delete/route.ts new file mode 100644 index 0000000..67966a6 --- /dev/null +++ b/app/api/notifications/delete/route.ts @@ -0,0 +1,21 @@ +import { NextResponse, NextRequest } from "next/server"; +import { prisma } from "@/lib/prisma"; + +export async function POST(request: NextRequest) { + try { + const body = await request.json(); + const id = Number(body.id); + + if (!id) { + return NextResponse.json({ error: "Missing ID" }, { status: 400 }); + } + + await prisma.notification.delete({ + where: { id: id } + }); + + return NextResponse.json({ success: true }); + } catch (error: any) { + return NextResponse.json({ error: error.message }, { status: 500 }); + } +} \ No newline at end of file From a51f8c2a3c73bb359af4b458b1d2fc69709da28d Mon Sep 17 00:00:00 2001 From: headlessdev Date: Thu, 17 Apr 2025 14:17:51 +0200 Subject: [PATCH 06/37] Add @next/swc-win32-x64-msvc package to package-lock.json --- package-lock.json | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/package-lock.json b/package-lock.json index c36d905..4e5a164 100644 --- a/package-lock.json +++ b/package-lock.json @@ -4576,6 +4576,21 @@ "optional": true } } + }, + "node_modules/@next/swc-win32-x64-msvc": { + "version": "15.3.0", + "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-15.3.0.tgz", + "integrity": "sha512-vHUQS4YVGJPmpjn7r5lEZuMhK5UQBNBRSB+iGDvJjaNk649pTIcRluDWNb9siunyLLiu/LDPHfvxBtNamyuLTw==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } } } } From 631c5b0c3bd5be714a559732a02211bf6c8e33a4 Mon Sep 17 00:00:00 2001 From: headlessdev Date: Thu, 17 Apr 2025 15:14:27 +0200 Subject: [PATCH 07/37] Add Notifications System --- app/dashboard/settings/Settings.tsx | 72 ++++++++++++++++++++++++----- 1 file changed, 60 insertions(+), 12 deletions(-) diff --git a/app/dashboard/settings/Settings.tsx b/app/dashboard/settings/Settings.tsx index f726ce7..72781ff 100644 --- a/app/dashboard/settings/Settings.tsx +++ b/app/dashboard/settings/Settings.tsx @@ -66,6 +66,16 @@ export default function Settings() { const [emailSuccess, setEmailSuccess] = useState(false) const [notificationType, setNotificationType] = useState("") + const [smtpHost, setSmtpHost] = useState("") + const [smtpPort, setSmtpPort] = useState(0) + const [smtpSecure, setSmtpSecure] = useState(false) + const [smtpUsername, setSmtpUsername] = useState("") + const [smtpPassword, setSmtpPassword] = useState("") + const [smtpFrom, setSmtpFrom] = useState("") + const [smtpTo, setSmtpTo] = useState("") + const [telegramToken, setTelegramToken] = useState("") + const [telegramChatId, setTelegramChatId] = useState("") + const [discordWebhook, setDiscordWebhook] = useState("") const changeEmail = async () => { setEmailErrorVisible(false); @@ -147,6 +157,42 @@ export default function Settings() { }, 3000); } } + + const addNotification = async () => { + try { + const response = await axios.post('/api/notifications/add', { + type: notificationType, + smtpHost: smtpHost, + smtpPort: smtpPort, + smtpSecure: smtpSecure, + smtpUsername: smtpUsername, + smtpPassword: smtpPassword, + smtpFrom: smtpFrom, + smtpTo: smtpTo, + telegramToken: telegramToken, + telegramChatId: telegramChatId, + discordWebhook: discordWebhook + }); + } + catch (error: any) { + alert(error.response.data.error); + } + } + + const deleteNotification = async (id: number) => { + try { + const response = await axios.post('/api/notifications/delete', { + id: id + }); + if (response.status === 200) { + alert("Notification deleted successfully"); + } + } catch (error: any) { + alert(error.response.data.error); + } + } + + return ( @@ -342,31 +388,31 @@ export default function Settings() {
- + setSmtpHost(e.target.value)} />
- + setSmtpPort(Number(e.target.value))} />
- + setSmtpSecure(checked)} />
- + setSmtpUsername(e.target.value)} />
- + setSmtpPassword(e.target.value)} />
- + setSmtpFrom(e.target.value)} />
- + setSmtpTo(e.target.value)} />
)} @@ -375,11 +421,11 @@ export default function Settings() {
- + setTelegramToken(e.target.value)} />
- + setTelegramChatId(e.target.value)} />
)} @@ -388,7 +434,7 @@ export default function Settings() {
- + setDiscordWebhook(e.target.value)} />
)} @@ -397,10 +443,12 @@ export default function Settings() { Cancel - Add + + Add + - + From e7e873c75c5d26b0bd9e2233483eb9d3cd01eadd Mon Sep 17 00:00:00 2001 From: headlessdev Date: Thu, 17 Apr 2025 15:16:38 +0200 Subject: [PATCH 08/37] Implement endpoint to retrieve notifications --- app/api/notifications/get/route.ts | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) create mode 100644 app/api/notifications/get/route.ts diff --git a/app/api/notifications/get/route.ts b/app/api/notifications/get/route.ts new file mode 100644 index 0000000..dff6e7f --- /dev/null +++ b/app/api/notifications/get/route.ts @@ -0,0 +1,16 @@ +import { NextResponse, NextRequest } from "next/server"; +import { prisma } from "@/lib/prisma"; + + +export async function POST(request: NextRequest) { + try { + + const notifications = await prisma.notification.findMany(); + + return NextResponse.json({ + notifications + }); + } catch (error: any) { + return NextResponse.json({ error: error.message }, { status: 500 }); + } +} \ No newline at end of file From d00ec9313369ec093eac606022cc95629a363f83 Mon Sep 17 00:00:00 2001 From: headlessdev Date: Thu, 17 Apr 2025 15:17:03 +0200 Subject: [PATCH 09/37] Fix type annotation for smtpSecure checkbox onChange handler --- app/dashboard/settings/Settings.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/dashboard/settings/Settings.tsx b/app/dashboard/settings/Settings.tsx index 72781ff..4b2d68b 100644 --- a/app/dashboard/settings/Settings.tsx +++ b/app/dashboard/settings/Settings.tsx @@ -395,7 +395,7 @@ export default function Settings() { setSmtpPort(Number(e.target.value))} />
- setSmtpSecure(checked)} /> + setSmtpSecure(checked)} />
From 4a8759f627893e613ce8714eb95895f1655aa24f Mon Sep 17 00:00:00 2001 From: headlessdev Date: Thu, 17 Apr 2025 15:25:43 +0200 Subject: [PATCH 10/37] Notifications Display & Delete Notifications --- app/dashboard/settings/Settings.tsx | 49 +++++++++++++++++++++++++++-- 1 file changed, 47 insertions(+), 2 deletions(-) diff --git a/app/dashboard/settings/Settings.tsx b/app/dashboard/settings/Settings.tsx index 4b2d68b..95ccb21 100644 --- a/app/dashboard/settings/Settings.tsx +++ b/app/dashboard/settings/Settings.tsx @@ -28,7 +28,7 @@ import { AccordionTrigger, } from "@/components/ui/accordion" import { Input } from "@/components/ui/input" -import { useState } from "react"; +import { useEffect, useState } from "react"; import axios from "axios"; import Cookies from "js-cookie"; import { Button } from "@/components/ui/button"; @@ -77,6 +77,8 @@ export default function Settings() { const [telegramChatId, setTelegramChatId] = useState("") const [discordWebhook, setDiscordWebhook] = useState("") + const [notifications, setNotifications] = useState([]) + const changeEmail = async () => { setEmailErrorVisible(false); setEmailSuccess(false); @@ -185,13 +187,30 @@ export default function Settings() { id: id }); if (response.status === 200) { - alert("Notification deleted successfully"); + getNotifications() } } catch (error: any) { alert(error.response.data.error); } } + const getNotifications = async () => { + try { + const response = await axios.post('/api/notifications/get', {}); + if (response.status === 200 && response.data) { + console.log(response.data.notifications) + setNotifications(response.data.notifications); + } + } + catch (error: any) { + alert(error.response.data.error); + } + } + + useEffect(() => { + getNotifications() + }, []) + return ( @@ -449,6 +468,32 @@ export default function Settings() { + +
+ {notifications.length > 0 ? ( + notifications.map((notification) => ( +
+
+

{notification.type}

+
+ +
+ )) + ) : ( +
+ No notifications configured +
+ )} +
From e9aba02d5faee7ea1e3be419a0074d4b02519eea Mon Sep 17 00:00:00 2001 From: headlessdev Date: Thu, 17 Apr 2025 15:29:43 +0200 Subject: [PATCH 11/37] Add Notification SMTP Layout Fix --- app/dashboard/settings/Settings.tsx | 96 +++++++++++++++++++++-------- 1 file changed, 71 insertions(+), 25 deletions(-) diff --git a/app/dashboard/settings/Settings.tsx b/app/dashboard/settings/Settings.tsx index 95ccb21..76cbdfd 100644 --- a/app/dashboard/settings/Settings.tsx +++ b/app/dashboard/settings/Settings.tsx @@ -404,36 +404,82 @@ export default function Settings() { {notificationType === "smtp" && ( -
-
+
+
+
- setSmtpHost(e.target.value)} /> + setSmtpHost(e.target.value)} + />
-
+
- setSmtpPort(Number(e.target.value))} /> -
-
- setSmtpSecure(checked)} /> - -
-
- - setSmtpUsername(e.target.value)} /> -
-
- - setSmtpPassword(e.target.value)} /> -
-
- - setSmtpFrom(e.target.value)} /> -
-
- - setSmtpTo(e.target.value)} /> + setSmtpPort(Number(e.target.value))} + />
+ +
+ setSmtpSecure(checked)} + /> + +
+ +
+
+ + setSmtpUsername(e.target.value)} + /> +
+ +
+ + setSmtpPassword(e.target.value)} + /> +
+ +
+
+ + setSmtpFrom(e.target.value)} + /> +
+ +
+ + setSmtpTo(e.target.value)} + /> +
+
+
+
)} {notificationType === "telegram" && ( From 6fd360b5943331ea2b8550505afd36cb83900c2b Mon Sep 17 00:00:00 2001 From: headlessdev Date: Thu, 17 Apr 2025 15:34:45 +0200 Subject: [PATCH 12/37] Change HTTP request method from HEAD to GET in checkAndUpdateStatus function --- agent/main.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/agent/main.go b/agent/main.go index e03d91b..ed08058 100644 --- a/agent/main.go +++ b/agent/main.go @@ -107,7 +107,7 @@ func checkAndUpdateStatus(db *sql.DB, client *http.Client, apps []Application) { httpCtx, httpCancel := context.WithTimeout(context.Background(), 4*time.Second) defer httpCancel() - req, err := http.NewRequestWithContext(httpCtx, "HEAD", app.PublicURL, nil) + req, err := http.NewRequestWithContext(httpCtx, "GET", app.PublicURL, nil) if err != nil { fmt.Printf("Error creating request: %v\n", err) continue From 155a0af8839f49b1aff61111bbc0a7ebbba626fa Mon Sep 17 00:00:00 2001 From: headlessdev Date: Thu, 17 Apr 2025 15:39:21 +0200 Subject: [PATCH 13/37] Type Error Fix getNotifications --- app/dashboard/settings/Settings.tsx | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/app/dashboard/settings/Settings.tsx b/app/dashboard/settings/Settings.tsx index 76cbdfd..4fe2634 100644 --- a/app/dashboard/settings/Settings.tsx +++ b/app/dashboard/settings/Settings.tsx @@ -49,6 +49,10 @@ import { import { Label } from "@/components/ui/label"; import { Checkbox } from "@/components/ui/checkbox"; +interface NotificationsResponse { + notifications: any[]; +} + export default function Settings() { const { theme, setTheme } = useTheme(); @@ -196,9 +200,8 @@ export default function Settings() { const getNotifications = async () => { try { - const response = await axios.post('/api/notifications/get', {}); + const response = await axios.post('/api/notifications/get', {}); if (response.status === 200 && response.data) { - console.log(response.data.notifications) setNotifications(response.data.notifications); } } From e925f37b194d5b1419ce55a1e31f7c599796969b Mon Sep 17 00:00:00 2001 From: headlessdev Date: Thu, 17 Apr 2025 16:12:10 +0200 Subject: [PATCH 14/37] Notification Agent System --- agent/go.mod | 2 + agent/go.sum | 4 ++ agent/main.go | 168 ++++++++++++++++++++++++++++++++++++++++++++------ 3 files changed, 155 insertions(+), 19 deletions(-) diff --git a/agent/go.mod b/agent/go.mod index 0693b03..551dcaa 100644 --- a/agent/go.mod +++ b/agent/go.mod @@ -17,4 +17,6 @@ require ( github.com/jackc/pgtype v1.14.0 // indirect golang.org/x/crypto v0.20.0 // indirect golang.org/x/text v0.14.0 // indirect + gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect + gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df // indirect ) diff --git a/agent/go.sum b/agent/go.sum index e7903f5..b2a3b40 100644 --- a/agent/go.sum +++ b/agent/go.sum @@ -171,9 +171,13 @@ golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522/go.mod h1:I/5z698sn9Ka8T golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc h1:2gGKlE2+asNV9m7xrywl36YYNnBG5ZQ0r/BOOxqPpmk= +gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc/go.mod h1:m7x9LTH6d71AHyAX77c9yqWCCa3UKHcVEj9y7hAtKDk= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= +gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df h1:n7WqCuqOuCbNr617RXOY0AWRXxgwEyPp2z+p0+hgMuE= +gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df/go.mod h1:LRQQ+SO6ZHR7tOkpBDuZnXENFzX8qRjMDMyPD6BRkCw= gopkg.in/inconshreveable/log15.v2 v2.0.0-20180818164646-67afb5ed74ec/go.mod h1:aPpfJ7XW+gOuirDoZ8gHhLh3kZ1B08FtV2bbmy7Jv3s= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/agent/main.go b/agent/main.go index ed08058..0557c2f 100644 --- a/agent/main.go +++ b/agent/main.go @@ -6,10 +6,12 @@ import ( "fmt" "net/http" "os" + "strings" "time" _ "github.com/jackc/pgx/v4/stdlib" "github.com/joho/godotenv" + "gopkg.in/gomail.v2" ) type Application struct { @@ -18,6 +20,24 @@ type Application struct { Online bool } +type Notification struct { + ID int + Enabled bool + Type string + SMTPHost sql.NullString + SMTPPort sql.NullInt64 + SMTPFrom sql.NullString + SMTPUser sql.NullString + SMTPPass sql.NullString + SMTPSecure sql.NullBool + SMTPTo sql.NullString + TelegramChatID sql.NullString + TelegramToken sql.NullString + DiscordWebhook sql.NullString +} + +var notifications []Notification + func main() { if err := godotenv.Load(); err != nil { fmt.Println("No env vars found") @@ -34,8 +54,14 @@ func main() { } defer db.Close() + // Load notification configs + notifications, err = loadNotifications(db) + if err != nil { + panic(fmt.Sprintf("Failed to load notifications: %v", err)) + } + go func() { - deletionTicker := time.NewTicker(1 * time.Hour) + deletionTicker := time.NewTicker(time.Hour) defer deletionTicker.Stop() for range deletionTicker.C { @@ -45,7 +71,7 @@ func main() { } }() - ticker := time.NewTicker(1 * time.Second) + ticker := time.NewTicker(time.Second) defer ticker.Stop() client := &http.Client{ @@ -62,12 +88,41 @@ func main() { } } +func loadNotifications(db *sql.DB) ([]Notification, error) { + rows, err := db.Query( + `SELECT id, enabled, type, "smtpHost", "smtpPort", "smtpFrom", "smtpUser", "smtpPass", "smtpSecure", "smtpTo", + "telegramChatId", "telegramToken", "discordWebhook" + FROM notification + WHERE enabled = true`, + ) + if err != nil { + return nil, err + } + defer rows.Close() + + var configs []Notification + for rows.Next() { + var n Notification + if err := rows.Scan( + &n.ID, &n.Enabled, &n.Type, + &n.SMTPHost, &n.SMTPPort, &n.SMTPFrom, &n.SMTPUser, &n.SMTPPass, &n.SMTPSecure, &n.SMTPTo, + &n.TelegramChatID, &n.TelegramToken, &n.DiscordWebhook, + ); err != nil { + fmt.Printf("Error scanning notification: %v\n", err) + continue + } + configs = append(configs, n) + } + return configs, nil +} + func deleteOldEntries(db *sql.DB) error { ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) defer cancel() res, err := db.ExecContext(ctx, - `DELETE FROM uptime_history WHERE "createdAt" < now() - interval '30 days'`) + `DELETE FROM uptime_history WHERE "createdAt" < now() - interval '30 days'`, + ) if err != nil { return err } @@ -77,11 +132,9 @@ func deleteOldEntries(db *sql.DB) error { } func getApplications(db *sql.DB) []Application { - rows, err := db.Query(` - SELECT id, "publicURL", online - FROM application - WHERE "publicURL" IS NOT NULL - `) + rows, err := db.Query( + `SELECT id, "publicURL", online FROM application WHERE "publicURL" IS NOT NULL`, + ) if err != nil { fmt.Printf("Error fetching applications: %v\n", err) return nil @@ -91,8 +144,7 @@ func getApplications(db *sql.DB) []Application { var apps []Application for rows.Next() { var app Application - err := rows.Scan(&app.ID, &app.PublicURL, &app.Online) - if err != nil { + if err := rows.Scan(&app.ID, &app.PublicURL, &app.Online); err != nil { fmt.Printf("Error scanning row: %v\n", err) continue } @@ -103,7 +155,7 @@ func getApplications(db *sql.DB) []Application { func checkAndUpdateStatus(db *sql.DB, client *http.Client, apps []Application) { for _, app := range apps { - // Context for HTTP request + // HTTP request context httpCtx, httpCancel := context.WithTimeout(context.Background(), 4*time.Second) defer httpCancel() @@ -114,20 +166,26 @@ func checkAndUpdateStatus(db *sql.DB, client *http.Client, apps []Application) { } resp, err := client.Do(req) - isOnline := false - if err == nil && resp.StatusCode >= 200 && resp.StatusCode < 300 { - isOnline = true + isOnline := err == nil && resp.StatusCode >= 200 && resp.StatusCode < 300 || resp.StatusCode == 405 + + // Notify on status change + if isOnline != app.Online { + status := "offline" + if isOnline { + status = "online" + } + message := fmt.Sprintf("Application %d (%s) is now %s", app.ID, app.PublicURL, status) + sendNotifications(message) } - // Create a new context for database operations with a separate timeout + // DB context dbCtx, dbCancel := context.WithTimeout(context.Background(), 5*time.Second) defer dbCancel() // Update application status _, err = db.ExecContext(dbCtx, `UPDATE application SET online = $1 WHERE id = $2`, - isOnline, - app.ID, + isOnline, app.ID, ) if err != nil { fmt.Printf("Update failed for app %d: %v\n", app.ID, err) @@ -136,11 +194,83 @@ func checkAndUpdateStatus(db *sql.DB, client *http.Client, apps []Application) { // Insert into uptime_history _, err = db.ExecContext(dbCtx, `INSERT INTO uptime_history ("applicationId", online, "createdAt") VALUES ($1, $2, now())`, - app.ID, - isOnline, + app.ID, isOnline, ) if err != nil { fmt.Printf("Insert into uptime_history failed for app %d: %v\n", app.ID, err) } } } + +func sendNotifications(message string) { + for _, n := range notifications { + switch n.Type { + case "email": + if n.SMTPHost.Valid && n.SMTPTo.Valid { + sendEmail(n, message) + } + case "telegram": + if n.TelegramToken.Valid && n.TelegramChatID.Valid { + sendTelegram(n, message) + } + case "discord": + if n.DiscordWebhook.Valid { + sendDiscord(n, message) + } + } + } +} + +func sendEmail(n Notification, body string) { + // Initialize SMTP dialer with host, port, user, pass + d := gomail.NewDialer( + n.SMTPHost.String, + int(n.SMTPPort.Int64), + n.SMTPUser.String, + n.SMTPPass.String, + ) + if n.SMTPSecure.Valid && n.SMTPSecure.Bool { + d.SSL = true + } + + m := gomail.NewMessage() + m.SetHeader("From", n.SMTPFrom.String) + m.SetHeader("To", n.SMTPTo.String) + m.SetHeader("Subject", "Uptime Notification") + m.SetBody("text/plain", body) + + if err := d.DialAndSend(m); err != nil { + fmt.Printf("Email send failed: %v\n", err) + } +} + +func sendTelegram(n Notification, message string) { + url := fmt.Sprintf("https://api.telegram.org/bot%s/sendMessage?chat_id=%s&text=%s", + n.TelegramToken.String, + n.TelegramChatID.String, + message, + ) + resp, err := http.Get(url) + if err != nil { + fmt.Printf("Telegram send failed: %v\n", err) + return + } + resp.Body.Close() +} + +func sendDiscord(n Notification, message string) { + payload := fmt.Sprintf(`{"content": "%s"}`, message) + req, err := http.NewRequest("POST", n.DiscordWebhook.String, strings.NewReader(payload)) + if err != nil { + fmt.Printf("Discord request creation failed: %v\n", err) + return + } + req.Header.Set("Content-Type", "application/json") + client := &http.Client{Timeout: 5 * time.Second} + resp, err := client.Do(req) + if err != nil { + fmt.Printf("Discord send failed: %v\n", err) + return + } + resp.Body.Close() +} From 8f647d34895d030e2cfee2c954edf0afd3c64c77 Mon Sep 17 00:00:00 2001 From: headlessdev Date: Thu, 17 Apr 2025 16:13:29 +0200 Subject: [PATCH 15/37] Implement notification reload mechanism and improve thread safety with mutex --- agent/main.go | 47 ++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 42 insertions(+), 5 deletions(-) diff --git a/agent/main.go b/agent/main.go index 0557c2f..b64109e 100644 --- a/agent/main.go +++ b/agent/main.go @@ -7,6 +7,7 @@ import ( "net/http" "os" "strings" + "sync" "time" _ "github.com/jackc/pgx/v4/stdlib" @@ -36,7 +37,10 @@ type Notification struct { DiscordWebhook sql.NullString } -var notifications []Notification +var ( + notifications []Notification + notifMutex sync.RWMutex +) func main() { if err := godotenv.Load(); err != nil { @@ -54,12 +58,34 @@ func main() { } defer db.Close() - // Load notification configs - notifications, err = loadNotifications(db) + // initial load + notifs, err := loadNotifications(db) if err != nil { panic(fmt.Sprintf("Failed to load notifications: %v", err)) } + notifMutex.Lock() + notifications = notifMutexCopy(notifs) + notifMutex.Unlock() + // reload notification configs every minute + go func() { + reloadTicker := time.NewTicker(time.Minute) + defer reloadTicker.Stop() + + for range reloadTicker.C { + newNotifs, err := loadNotifications(db) + if err != nil { + fmt.Printf("Failed to reload notifications: %v\n", err) + continue + } + notifMutex.Lock() + notifications = notifMutexCopy(newNotifs) + notifMutex.Unlock() + fmt.Println("Reloaded notification configurations") + } + }() + + // clean up old entries hourly go func() { deletionTicker := time.NewTicker(time.Hour) defer deletionTicker.Stop() @@ -88,6 +114,13 @@ func main() { } } +// helper to safely copy slice +func notifMutexCopy(src []Notification) []Notification { + copyDst := make([]Notification, len(src)) + copy(copyDst, src) + return copyDst +} + func loadNotifications(db *sql.DB) ([]Notification, error) { rows, err := db.Query( `SELECT id, enabled, type, "smtpHost", "smtpPort", "smtpFrom", "smtpUser", "smtpPass", "smtpSecure", "smtpTo", @@ -166,7 +199,7 @@ func checkAndUpdateStatus(db *sql.DB, client *http.Client, apps []Application) { } resp, err := client.Do(req) - isOnline := err == nil && resp.StatusCode >= 200 && resp.StatusCode < 300 || resp.StatusCode == 405 + isOnline := (err == nil && resp.StatusCode >= 200 && resp.StatusCode < 300) || resp.StatusCode == 405 // Notify on status change if isOnline != app.Online { @@ -203,7 +236,11 @@ func checkAndUpdateStatus(db *sql.DB, client *http.Client, apps []Application) { } func sendNotifications(message string) { - for _, n := range notifications { + notifMutex.RLock() + notifs := notifMutexCopy(notifications) + notifMutex.RUnlock() + + for _, n := range notifs { switch n.Type { case "email": if n.SMTPHost.Valid && n.SMTPTo.Valid { From dacde7153fbfe43924cf7548d7164f8feeee8db4 Mon Sep 17 00:00:00 2001 From: headlessdev Date: Thu, 17 Apr 2025 16:14:52 +0200 Subject: [PATCH 16/37] Update Notifications List after adding Notification --- app/dashboard/settings/Settings.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/app/dashboard/settings/Settings.tsx b/app/dashboard/settings/Settings.tsx index 4fe2634..391a99e 100644 --- a/app/dashboard/settings/Settings.tsx +++ b/app/dashboard/settings/Settings.tsx @@ -179,6 +179,7 @@ export default function Settings() { telegramChatId: telegramChatId, discordWebhook: discordWebhook }); + getNotifications(); } catch (error: any) { alert(error.response.data.error); From b9fac8ddb691eb66f1f372faca2a3559f49a4959 Mon Sep 17 00:00:00 2001 From: headlessdev Date: Thu, 17 Apr 2025 16:20:24 +0200 Subject: [PATCH 17/37] Enhance Application struct and update getApplications to include Name; improve HTTP request handling in checkAndUpdateStatus --- agent/main.go | 39 +++++++++++++++++++++++---------------- 1 file changed, 23 insertions(+), 16 deletions(-) diff --git a/agent/main.go b/agent/main.go index b64109e..5bb33b6 100644 --- a/agent/main.go +++ b/agent/main.go @@ -17,6 +17,7 @@ import ( type Application struct { ID int + Name string PublicURL string Online bool } @@ -166,7 +167,7 @@ func deleteOldEntries(db *sql.DB) error { func getApplications(db *sql.DB) []Application { rows, err := db.Query( - `SELECT id, "publicURL", online FROM application WHERE "publicURL" IS NOT NULL`, + `SELECT id, name, "publicURL", online FROM application WHERE "publicURL" IS NOT NULL`, ) if err != nil { fmt.Printf("Error fetching applications: %v\n", err) @@ -177,7 +178,7 @@ func getApplications(db *sql.DB) []Application { var apps []Application for rows.Next() { var app Application - if err := rows.Scan(&app.ID, &app.PublicURL, &app.Online); err != nil { + if err := rows.Scan(&app.ID, &app.Name, &app.PublicURL, &app.Online); err != nil { fmt.Printf("Error scanning row: %v\n", err) continue } @@ -188,47 +189,53 @@ func getApplications(db *sql.DB) []Application { func checkAndUpdateStatus(db *sql.DB, client *http.Client, apps []Application) { for _, app := range apps { - // HTTP request context + // — HTTP check with proper nil‑guard — httpCtx, httpCancel := context.WithTimeout(context.Background(), 4*time.Second) - defer httpCancel() - req, err := http.NewRequestWithContext(httpCtx, "GET", app.PublicURL, nil) if err != nil { + httpCancel() fmt.Printf("Error creating request: %v\n", err) continue } resp, err := client.Do(req) - isOnline := (err == nil && resp.StatusCode >= 200 && resp.StatusCode < 300) || resp.StatusCode == 405 + httpCancel() - // Notify on status change + var isOnline bool + if err == nil { + isOnline = resp.StatusCode >= 200 && resp.StatusCode < 300 || + resp.StatusCode == 405 + resp.Body.Close() + } + + // — Notify on change — if isOnline != app.Online { status := "offline" if isOnline { status = "online" } - message := fmt.Sprintf("Application %d (%s) is now %s", app.ID, app.PublicURL, status) - sendNotifications(message) + sendNotifications( + fmt.Sprintf("The application '%s' (%s) went %s!", app.Name, app.PublicURL, status), + ) } - // DB context + // — Update DB with its own context — dbCtx, dbCancel := context.WithTimeout(context.Background(), 5*time.Second) - defer dbCancel() - - // Update application status _, err = db.ExecContext(dbCtx, `UPDATE application SET online = $1 WHERE id = $2`, isOnline, app.ID, ) + dbCancel() if err != nil { fmt.Printf("Update failed for app %d: %v\n", app.ID, err) } - // Insert into uptime_history - _, err = db.ExecContext(dbCtx, - `INSERT INTO uptime_history ("applicationId", online, "createdAt") VALUES ($1, $2, now())`, + dbCtx2, dbCancel2 := context.WithTimeout(context.Background(), 5*time.Second) + _, err = db.ExecContext(dbCtx2, + `INSERT INTO uptime_history("applicationId", online, "createdAt") VALUES ($1, $2, now())`, app.ID, isOnline, ) + dbCancel2() if err != nil { fmt.Printf("Insert into uptime_history failed for app %d: %v\n", app.ID, err) } From 88f7f6a9d15828845bdc463442b61a8790907025 Mon Sep 17 00:00:00 2001 From: headlessdev Date: Thu, 17 Apr 2025 16:51:15 +0200 Subject: [PATCH 18/37] Add notification_text column to settings model in Prisma schema and migration --- .../20250417145101_settings_notification_text/migration.sql | 2 ++ prisma/schema.prisma | 1 + 2 files changed, 3 insertions(+) create mode 100644 prisma/migrations/20250417145101_settings_notification_text/migration.sql diff --git a/prisma/migrations/20250417145101_settings_notification_text/migration.sql b/prisma/migrations/20250417145101_settings_notification_text/migration.sql new file mode 100644 index 0000000..1999d54 --- /dev/null +++ b/prisma/migrations/20250417145101_settings_notification_text/migration.sql @@ -0,0 +1,2 @@ +-- AlterTable +ALTER TABLE "settings" ADD COLUMN "notification_text" TEXT; diff --git a/prisma/schema.prisma b/prisma/schema.prisma index d963a67..a701dff 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -47,6 +47,7 @@ model server { model settings { id Int @id @default(autoincrement()) uptime_checks Boolean @default(true) + notification_text String? } model user { From b0c7b813e69b036782bb0bd8f7f0f863324f7de9 Mon Sep 17 00:00:00 2001 From: headlessdev Date: Thu, 17 Apr 2025 16:51:27 +0200 Subject: [PATCH 19/37] Bump version to 0.0.6 in package.json --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 8b15dd0..4472eff 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "corecontrol", - "version": "0.0.5", + "version": "0.0.6", "private": true, "scripts": { "dev": "next dev --turbopack", From 88d99cee43f557092f7efa4ac987d705e1df6a8c Mon Sep 17 00:00:00 2001 From: headlessdev Date: Thu, 17 Apr 2025 17:03:06 +0200 Subject: [PATCH 20/37] Add endpoint for managing notification text in settings --- app/api/settings/notification_text/route.ts | 34 +++++++++++++++++++++ 1 file changed, 34 insertions(+) create mode 100644 app/api/settings/notification_text/route.ts diff --git a/app/api/settings/notification_text/route.ts b/app/api/settings/notification_text/route.ts new file mode 100644 index 0000000..0a37722 --- /dev/null +++ b/app/api/settings/notification_text/route.ts @@ -0,0 +1,34 @@ +import { NextResponse, NextRequest } from "next/server"; +import { prisma } from "@/lib/prisma"; + +interface AddRequest { + text: string; +} + +export async function POST(request: NextRequest) { + try { + const body: AddRequest = await request.json(); + const { text } = body; + + // Check if there is already a settings entry + const existingSettings = await prisma.settings.findFirst(); + if (existingSettings) { + // Update the existing settings entry + const updatedSettings = await prisma.settings.update({ + where: { id: existingSettings.id }, + data: { notification_text: text }, + }); + return NextResponse.json({ message: "Success", updatedSettings }); + } + // If no settings entry exists, create a new one + const settings = await prisma.settings.create({ + data: { + notification_text: text, + } + }); + + return NextResponse.json({ message: "Success", settings }); + } catch (error: any) { + return NextResponse.json({ error: error.message }, { status: 500 }); + } +} From c690a1cb377d26b2e9f1a100d9c39818e6e64535 Mon Sep 17 00:00:00 2001 From: headlessdev Date: Thu, 17 Apr 2025 17:15:39 +0200 Subject: [PATCH 21/37] Add customizable notification text feature in settings --- app/dashboard/settings/Settings.tsx | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/app/dashboard/settings/Settings.tsx b/app/dashboard/settings/Settings.tsx index 391a99e..98ed016 100644 --- a/app/dashboard/settings/Settings.tsx +++ b/app/dashboard/settings/Settings.tsx @@ -48,6 +48,7 @@ import { } from "@/components/ui/alert-dialog" import { Label } from "@/components/ui/label"; import { Checkbox } from "@/components/ui/checkbox"; +import { Textarea } from "@/components/ui/textarea"; interface NotificationsResponse { notifications: any[]; @@ -519,6 +520,33 @@ export default function Settings() { + + +
+ +
+
+ + Customize Notification Text + +
+
+ +