diff --git a/apps/dbagent/.env.example b/apps/dbagent/.env.example
index 7a206fb9..0610255e 100644
--- a/apps/dbagent/.env.example
+++ b/apps/dbagent/.env.example
@@ -1,4 +1,5 @@
DATABASE_URL='postgres://dbagent:changeme@localhost:5432/dbagent'
OPENAI_API_KEY=
DEEPSEEK_API_KEY=
-ANTHROPIC_API_KEY=
\ No newline at end of file
+ANTHROPIC_API_KEY=
+PUBLIC_URL='http://localhost:4001'
\ No newline at end of file
diff --git a/apps/dbagent/package.json b/apps/dbagent/package.json
index d9c58075..df97de79 100644
--- a/apps/dbagent/package.json
+++ b/apps/dbagent/package.json
@@ -28,6 +28,7 @@
"@vercel/functions": "^2.0.0",
"ai": "^4.1.45",
"bytes": "^3.1.2",
+ "canvas-confetti": "^1.9.3",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"cron-parser": "^5.0.4",
@@ -52,6 +53,7 @@
"@internal/eslint-config": "workspace:*",
"@internal/tsconfig": "workspace:*",
"@types/bytes": "^3.1.5",
+ "@types/canvas-confetti": "^1.9.0",
"@types/pg": "^8.11.11",
"@types/react": "^19.0.10",
"@types/react-dom": "^19.0.4",
diff --git a/apps/dbagent/server.ts b/apps/dbagent/server.ts
new file mode 100644
index 00000000..04db8b5e
--- /dev/null
+++ b/apps/dbagent/server.ts
@@ -0,0 +1,16 @@
+/* eslint-disable no-process-env */
+import { createServer } from 'http';
+import next from 'next';
+
+const port = process.env.PORT || 4001;
+const dev = process.env.NODE_ENV !== 'production';
+const app = next({ dev });
+const handle = app.getRequestHandler();
+
+void app.prepare().then(() => {
+ createServer((req, res) => {
+ void handle(req, res);
+ }).listen(port, () => {
+ console.log(`> Ready on http://localhost:${port}`);
+ });
+});
diff --git a/apps/dbagent/src/app/(main)/layout.tsx b/apps/dbagent/src/app/(main)/layout.tsx
index 045bc5b6..31f8f812 100644
--- a/apps/dbagent/src/app/(main)/layout.tsx
+++ b/apps/dbagent/src/app/(main)/layout.tsx
@@ -1,7 +1,10 @@
+'use server';
import { BelowHeaderBar, HeaderBar } from '~/app/components/ui/header-bar';
+import { getCompletedTaskPercentage } from '~/components/onboarding/actions';
import { SideNav } from '../components/ui/side-nav';
export default async function Layout({ children }: { children: React.ReactNode }) {
+ const onboardingComplete = await getCompletedTaskPercentage();
return (
<>
@@ -10,7 +13,7 @@ export default async function Layout({ children }: { children: React.ReactNode }
-
+
{children}
diff --git a/apps/dbagent/src/app/(main)/monitoring/schedule/[id]/page.tsx b/apps/dbagent/src/app/(main)/monitoring/schedule/[id]/page.tsx
index 2b4a1fbe..a6f52af2 100644
--- a/apps/dbagent/src/app/(main)/monitoring/schedule/[id]/page.tsx
+++ b/apps/dbagent/src/app/(main)/monitoring/schedule/[id]/page.tsx
@@ -2,17 +2,18 @@ import { actionListConnections } from '~/components/connections/actions';
import { actionListPlaybooks } from '~/components/monitoring/actions';
import { ScheduleForm } from '~/components/monitoring/schedule-form';
-export default async function Page({ params }: { params: { id: string; connection?: string } }) {
+interface PageParams {
+ id: string;
+ connection?: string;
+}
+
+export default async function Page({ params }: { params: Promise
}) {
const playbooks = await actionListPlaybooks();
const connections = await actionListConnections();
+ const { id, connection } = await params;
return (
-
+
);
}
diff --git a/apps/dbagent/src/app/(main)/start/connect/edit/[id]/page.tsx b/apps/dbagent/src/app/(main)/start/connect/edit/[id]/page.tsx
index dc38bfd7..5bffa4b9 100644
--- a/apps/dbagent/src/app/(main)/start/connect/edit/[id]/page.tsx
+++ b/apps/dbagent/src/app/(main)/start/connect/edit/[id]/page.tsx
@@ -1,10 +1,11 @@
import { ConnectionForm } from '~/components/connections/connection-form';
-export default async function EditConnection({ params }: { params: { id: number } }) {
+export default async function EditConnection({ params }: { params: Promise<{ id: number }> }) {
+ const { id } = await params;
return (
Edit Connection
-
+
);
}
diff --git a/apps/dbagent/src/app/(main)/start/notifications/page.tsx b/apps/dbagent/src/app/(main)/start/notifications/page.tsx
new file mode 100644
index 00000000..52118d7b
--- /dev/null
+++ b/apps/dbagent/src/app/(main)/start/notifications/page.tsx
@@ -0,0 +1,12 @@
+'use server';
+
+import { SlackIntegration } from '~/components/slack-integration/slack-integration';
+
+export default async function Page() {
+ return (
+
+
Collect info about your database
+
+
+ );
+}
diff --git a/apps/dbagent/src/app/api/chat/route.ts b/apps/dbagent/src/app/api/chat/route.ts
index d768310f..d03455fa 100644
--- a/apps/dbagent/src/app/api/chat/route.ts
+++ b/apps/dbagent/src/app/api/chat/route.ts
@@ -6,7 +6,7 @@ import { getTargetDbConnection } from '~/lib/targetdb/db';
export const runtime = 'nodejs';
export const maxDuration = 30;
-export function errorHandler(error: unknown) {
+function errorHandler(error: unknown) {
if (error == null) {
return 'unknown error';
}
diff --git a/apps/dbagent/src/app/components/ui/side-nav.tsx b/apps/dbagent/src/app/components/ui/side-nav.tsx
index 7fac726c..667d8ce5 100644
--- a/apps/dbagent/src/app/components/ui/side-nav.tsx
+++ b/apps/dbagent/src/app/components/ui/side-nav.tsx
@@ -3,28 +3,46 @@
import { cn, Input } from '@internal/components';
import {
ActivityIcon,
+ AlarmClock,
BotMessageSquare,
CloudIcon,
DatabaseIcon,
- GitBranch,
PlugIcon,
Server,
ZapIcon
} from 'lucide-react';
import Link from 'next/link';
import { usePathname } from 'next/navigation';
+import { useEffect, useState } from 'react';
interface SideNavProps {
className?: string;
+ onboardingComplete: number;
}
-export function SideNav({ className }: SideNavProps) {
+export function SideNav({ className, onboardingComplete }: SideNavProps) {
const pathname = usePathname();
+ const [onboardingCompleteState, setOnboardingComplete] = useState(onboardingComplete);
const isActive = (path: string) => {
return pathname === path ? 'bg-primary text-primary-foreground' : 'text-muted-foreground hover:bg-accent';
};
+ useEffect(() => {
+ const handleOnboardingStatus = (e: CustomEvent) => {
+ // Update your onboarding complete state here
+ setOnboardingComplete(e.detail.completed);
+ };
+
+ window.addEventListener('onboardingStatusChanged', handleOnboardingStatus as EventListener);
+
+ return () => {
+ window.removeEventListener('onboardingStatusChanged', handleOnboardingStatus as EventListener);
+ };
+ }, []);
+
+ console.log('onboardingComplete', onboardingCompleteState);
+
return (
@@ -35,7 +53,7 @@ export function SideNav({ className }: SideNavProps) {
className={cn('flex items-center gap-2 rounded-md px-3 py-2 text-sm', isActive(`/start`))}
>
- Starter guide
+ Starter guide {onboardingCompleteState ? `(${onboardingCompleteState}%)` : ''}
@@ -68,11 +86,11 @@ export function SideNav({ className }: SideNavProps) {
-
- Setup environments
+
+ Setup notifications
diff --git a/apps/dbagent/src/components/chats/chats-ui.tsx b/apps/dbagent/src/components/chats/chats-ui.tsx
index 294df250..eea8b6c3 100644
--- a/apps/dbagent/src/components/chats/chats-ui.tsx
+++ b/apps/dbagent/src/components/chats/chats-ui.tsx
@@ -15,7 +15,7 @@ import {
import { useChat } from 'ai/react';
import { Bot, Clock, Lightbulb, Send, User, Wrench } from 'lucide-react';
import { useSearchParams } from 'next/navigation';
-import { useEffect, useRef, useState } from 'react';
+import { Suspense, useEffect, useRef, useState } from 'react';
import ReactMarkdown from 'react-markdown';
import { DbConnection } from '~/lib/db/connections';
import { ChatSidebar } from './chat-sidebar';
@@ -24,6 +24,14 @@ import { mockChats } from './mock-data';
import { ModelSelector } from './model-selector';
export function ChatsUI({ connections }: { connections: DbConnection[] }) {
+ return (
+ Loading... }>
+
+
+ );
+}
+
+function ChatsUIContent({ connections }: { connections: DbConnection[] }) {
const searchParams = useSearchParams();
const [selectedChatId, setSelectedChatId] = useState
(null);
const [chats, setChats] = useState(mockChats);
diff --git a/apps/dbagent/src/components/monitoring/schedules-table.tsx b/apps/dbagent/src/components/monitoring/schedules-table.tsx
index 6304eab0..2b71f5bc 100644
--- a/apps/dbagent/src/components/monitoring/schedules-table.tsx
+++ b/apps/dbagent/src/components/monitoring/schedules-table.tsx
@@ -100,8 +100,8 @@ export function MonitoringScheduleTable({ connections }: { connections: DbConnec
- Playbook
Database
+ Playbook
Schedule
Status
Last Run
diff --git a/apps/dbagent/src/components/onboarding/actions.ts b/apps/dbagent/src/components/onboarding/actions.ts
index f6c1ff22..54b2f754 100644
--- a/apps/dbagent/src/components/onboarding/actions.ts
+++ b/apps/dbagent/src/components/onboarding/actions.ts
@@ -3,6 +3,7 @@
import { getClusters } from '~/lib/db/clusters';
import { getDefaultConnection } from '~/lib/db/connections';
import { getDbInfo } from '~/lib/db/dbinfo';
+import { getIntegration } from '~/lib/db/integrations';
// Server action to get completed tasks
export async function getCompletedTasks(): Promise {
@@ -24,5 +25,15 @@ export async function getCompletedTasks(): Promise {
completedTasks.push('cloud');
}
+ const slack = await getIntegration('slack');
+ if (slack) {
+ completedTasks.push('notifications');
+ }
+
return completedTasks;
}
+
+export async function getCompletedTaskPercentage(): Promise {
+ const completedTasks = await getCompletedTasks();
+ return Math.round((completedTasks.length / 4) * 100);
+}
diff --git a/apps/dbagent/src/components/onboarding/onboarding.tsx b/apps/dbagent/src/components/onboarding/onboarding.tsx
index a4cfc1f4..dce39dd0 100644
--- a/apps/dbagent/src/components/onboarding/onboarding.tsx
+++ b/apps/dbagent/src/components/onboarding/onboarding.tsx
@@ -1,12 +1,45 @@
'use client';
-import { Activity, Database, GitBranch, Server, TowerControlIcon } from 'lucide-react';
+import { Button } from '@internal/components';
+import confetti from 'canvas-confetti';
+import { Activity, Check, Database, GitBranch, Server } from 'lucide-react';
import { useRouter } from 'next/navigation';
import { useEffect, useState } from 'react';
import { getCompletedTasks } from './actions';
import { OnboardingProgress } from './onboarding-progress';
import { OnboardingTask } from './onboarding-task';
+export const onboardingTasks = [
+ {
+ id: 'connect',
+ title: 'Connect to Database',
+ description: `Add at least a database connection. You'd normally configure your production database connection here. Don't worry, I won't run any destructive queries.`,
+ icon: ,
+ navigateTo: '/start/connect'
+ },
+ {
+ id: 'collect',
+ title: 'Collect Database Info',
+ description: `Let's check that I have proper access and that I can collect some basic information about your database.`,
+ icon: ,
+ navigateTo: '/start/collect'
+ },
+ {
+ id: 'cloud',
+ title: 'Connect cloud management',
+ description: `Use an integration to allow me to read your relevant instance and observability data.`,
+ icon: ,
+ navigateTo: '/start/cloud'
+ },
+ {
+ id: 'notifications',
+ title: 'Setup Slack notifications',
+ description: 'Configure a Slack integration so I can notify you if I find any issues with your database.',
+ icon: ,
+ navigateTo: '/start/notifications'
+ }
+];
+
export function Onboarding() {
const router = useRouter();
const [completedTasks, setCompletedTasks] = useState([]);
@@ -14,54 +47,26 @@ export function Onboarding() {
useEffect(() => {
getCompletedTasks()
.then((tasks) => {
+ if (tasks.length === onboardingTasks.length && completedTasks.length < onboardingTasks.length) {
+ void confetti({
+ particleCount: 200,
+ spread: 2000,
+ origin: { y: 0.3 }
+ });
+ }
setCompletedTasks(tasks);
+ // Dispatch a custom event when tasks are loaded
+ window.dispatchEvent(
+ new CustomEvent('onboardingStatusChanged', {
+ detail: { completed: Math.round((tasks.length / onboardingTasks.length) * 100) }
+ })
+ );
})
.catch((error) => {
console.error('Failed to load completed tasks:', error);
});
}, []);
- const tasks = [
- {
- id: 'connect',
- title: 'Connect to Database',
- description: `Add at least a database connection. You'd normally configure your production database connection here. Don't worry, I won't run any destructive queries.`,
- icon: ,
- navigateTo: '/start/connect'
- },
- {
- id: 'collect',
- title: 'Collect Database Info',
- description: `Let's check that I have proper access and that I can collect some basic information about your database.`,
- icon: ,
- navigateTo: '/start/collect'
- },
- {
- id: 'cloud',
- title: 'Connect cloud management',
- description: `Use an integration to allow me to read your relevant instance and observability data.`,
- icon: ,
- navigateTo: '/start/cloud'
- },
- {
- id: 'report',
- title: 'Get an initial assessment',
- description:
- 'I will get an initial assessment of your database, instance/cluster type, main settings and activity and provide you with an initial report.',
- icon: ,
- buttonText: 'Get initial assessment',
- navigateTo: '/chats?start=report'
- },
- {
- id: 'environments',
- title: 'Setup staging and dev environments',
- description:
- 'If I can use a staging environment that supports copy-on-write branches, I will be able to test changes in a safe environment on my own.',
- icon: ,
- navigateTo: '/start/environments'
- }
- ];
-
const handleTaskAction = async (navigateTo: string) => {
router.push(navigateTo);
};
@@ -78,21 +83,45 @@ export function Onboarding() {
-
+
- {tasks.map((task) => (
+ {onboardingTasks.map((task) => (
handleTaskAction(task.navigateTo)}
/>
))}
+ {completedTasks.length === onboardingTasks.length && (
+
+
+
+
+
+
+
+
Congratulations! All tasks completed
+
+ Great job! You've completed all the setup tasks. I'm now ready to help you monitor and
+ optimize your database.
+
+
+
+ router.push('/chats?start=report')}>Get Initial Assessment
+ router.push('/monitoring')} variant="outline">
+ Setup Periodic Monitoring
+
+
+
+
+
+ )}
);
diff --git a/apps/dbagent/src/components/slack-integration/actions.ts b/apps/dbagent/src/components/slack-integration/actions.ts
new file mode 100644
index 00000000..70b77458
--- /dev/null
+++ b/apps/dbagent/src/components/slack-integration/actions.ts
@@ -0,0 +1,28 @@
+'use server';
+
+import { getIntegration, saveIntegration, SlackIntegration } from '~/lib/db/integrations';
+
+export async function saveWebhookUrl(webhookUrl: string): Promise<{ success: boolean; message: string }> {
+ // TODO: Implement actual saving logic here
+ // This is a placeholder for demonstration purposes
+ const success = webhookUrl && webhookUrl.startsWith('https://hooks.slack.com/');
+
+ if (!success) {
+ // In a real implementation, you would save the webhook URL to your database or configuration
+ return { success: false, message: 'Invalid webhook URL' };
+ }
+
+ try {
+ await saveIntegration('slack', { webhookUrl });
+ } catch (error) {
+ console.error('Failed to save webhook URL:', error);
+ return { success: false, message: `Failed to save webhook URL.` };
+ }
+
+ console.log('Webhook URL saved:', webhookUrl);
+ return { success: true, message: 'Webhook URL saved successfully' };
+}
+
+export async function getWebhookUrl(): Promise {
+ return await getIntegration('slack');
+}
diff --git a/apps/dbagent/src/components/slack-integration/slack-integration.tsx b/apps/dbagent/src/components/slack-integration/slack-integration.tsx
new file mode 100644
index 00000000..74a7e006
--- /dev/null
+++ b/apps/dbagent/src/components/slack-integration/slack-integration.tsx
@@ -0,0 +1,118 @@
+'use client';
+
+import {
+ Alert,
+ AlertDescription,
+ AlertTitle,
+ Button,
+ Card,
+ CardContent,
+ CardDescription,
+ CardHeader,
+ CardTitle,
+ Input,
+ Label,
+ toast
+} from '@internal/components';
+import { AlertCircle } from 'lucide-react';
+import Link from 'next/link';
+import { useEffect, useState } from 'react';
+import { useForm } from 'react-hook-form';
+import { getWebhookUrl, saveWebhookUrl } from './actions';
+
+export function SlackIntegration() {
+ const [isLoading, setIsLoading] = useState(true);
+
+ const {
+ register,
+ handleSubmit,
+ formState: { errors },
+ setValue
+ } = useForm({
+ defaultValues: {
+ webhookUrl: ''
+ }
+ });
+
+ useEffect(() => {
+ const fetchWebhookUrl = async () => {
+ try {
+ const data = await getWebhookUrl();
+ if (data) {
+ setValue('webhookUrl', data.webhookUrl);
+ }
+ } catch (error) {
+ toast('Failed to load existing webhook configuration');
+ } finally {
+ setIsLoading(false);
+ }
+ };
+
+ void fetchWebhookUrl();
+ }, [setValue]);
+
+ const onSubmit = async (data: { webhookUrl: string }) => {
+ try {
+ const response = await saveWebhookUrl(data.webhookUrl);
+ if (response.success) {
+ toast('Slack webhook URL saved successfully');
+ } else {
+ toast(`Error saving Slack webhook URL. ${response.message}`);
+ }
+ } catch (error) {
+ toast(`Failed to save Slack webhook URL.`);
+ }
+ };
+
+ return (
+
+
+
+ Notifications
+ Configure slack notifications
+
+
+
+
+
+ Create a Slack Webhook
+
+ To create an incoming webhook for posting to your Slack workspace,{' '}
+
+ follow this guide
+
+ . It only takes a few minutes to set up.
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/apps/dbagent/src/lib/db/integrations.ts b/apps/dbagent/src/lib/db/integrations.ts
index 75a6606b..24f7db00 100644
--- a/apps/dbagent/src/lib/db/integrations.ts
+++ b/apps/dbagent/src/lib/db/integrations.ts
@@ -6,8 +6,13 @@ export type AwsIntegration = {
region: string;
};
+export type SlackIntegration = {
+ webhookUrl: string;
+};
+
type IntegrationModules = {
aws: AwsIntegration;
+ slack: SlackIntegration;
};
export async function saveIntegration(name: T, data: IntegrationModules[T]) {
@@ -23,7 +28,7 @@ export async function saveIntegration(name:
}
export async function getIntegration(
- name: string
+ name: T
): Promise {
const client = await pool.connect();
try {
diff --git a/apps/dbagent/src/lib/env/general.ts b/apps/dbagent/src/lib/env/general.ts
new file mode 100644
index 00000000..ac858963
--- /dev/null
+++ b/apps/dbagent/src/lib/env/general.ts
@@ -0,0 +1,11 @@
+/* eslint-disable no-process-env */
+
+import { z } from 'zod';
+
+const schema = z.object({
+ PUBLIC_URL: z.string().default('http://localhost:4001')
+});
+
+export const env = schema.parse({
+ PUBLIC_URL: process.env.PUBLIC_URL
+});
diff --git a/apps/dbagent/src/lib/monitoring/runner.ts b/apps/dbagent/src/lib/monitoring/runner.ts
index 6a7b916c..f076bb7a 100644
--- a/apps/dbagent/src/lib/monitoring/runner.ts
+++ b/apps/dbagent/src/lib/monitoring/runner.ts
@@ -1,7 +1,9 @@
-import { generateText } from 'ai';
+import { CoreMessage, generateObject, generateText } from 'ai';
+import { z } from 'zod';
import { Schedule } from '~/lib/db/schedules';
import { getModelInstance, getTools, monitoringSystemPrompt } from '../ai/aidba';
import { getConnection } from '../db/connections';
+import { sendScheduleNotification } from '../notifications/slack-webhook';
import { getTargetDbConnection } from '../targetdb/db';
export async function runSchedule(schedule: Schedule) {
@@ -16,23 +18,56 @@ export async function runSchedule(schedule: Schedule) {
const modelInstance = getModelInstance(schedule.model);
+ const messages = [
+ {
+ role: 'user',
+ content: `Run this playbook: ${schedule.playbook}`
+ }
+ ] as CoreMessage[];
+
+ if (schedule.additionalInstructions) {
+ messages.push({
+ role: 'user',
+ content: schedule.additionalInstructions
+ });
+ }
+
const result = await generateText({
model: modelInstance,
system: monitoringSystemPrompt,
- messages: [
- {
- role: 'user',
- content: `Run this playbook: ${schedule.playbook}`
- },
- {
- role: 'user',
- content: schedule.additionalInstructions ?? ''
- }
- ],
+ messages: messages,
tools: await getTools(connection, targetClient),
maxSteps: 20
});
- console.log(result);
console.log(result.text);
+
+ const notificationResult = await generateObject({
+ model: modelInstance,
+ schema: z.object({
+ summary: z.string(),
+ notificationLevel: z.enum(['info', 'warning', 'alert'])
+ }),
+ prompt: `Decide a level of notification for the following
+ result of a playbook run. Choose one of these levels:
+
+ info: Everything is fine, no action is needed.
+ warning: Some issues were found, but nothing that requires immediate attention.
+ alert: We need immediate action.
+
+ Also provide a one sentence summary of the result. It can be something like "No issues found" or "Some issues were found".
+
+ Playbook: ${schedule.playbook}
+ Result: ${result.text}`
+ });
+
+ console.log(JSON.stringify(notificationResult.object, null, 2));
+
+ await sendScheduleNotification(
+ schedule,
+ connection,
+ notificationResult.object.notificationLevel,
+ notificationResult.object.summary,
+ result.text
+ );
}
diff --git a/apps/dbagent/src/lib/notifications/slack-webhook.ts b/apps/dbagent/src/lib/notifications/slack-webhook.ts
new file mode 100644
index 00000000..de4807ef
--- /dev/null
+++ b/apps/dbagent/src/lib/notifications/slack-webhook.ts
@@ -0,0 +1,111 @@
+import { DbConnection } from '../db/connections';
+import { getIntegration } from '../db/integrations';
+import { Schedule } from '../db/schedules';
+import { env } from '../env/general';
+
+export type NotificationLevel = 'info' | 'warning' | 'alert';
+
+export async function sendScheduleNotification(
+ schedule: Schedule,
+ connection: DbConnection,
+ level: NotificationLevel,
+ title: string,
+ message: string
+) {
+ const slack = await getIntegration('slack');
+ if (!slack) {
+ console.error('No Slack integration configured.');
+ return;
+ }
+
+ // Format message for Slack "markdown"
+ message = message
+ .replace(/^### (.*$)/gm, '*$1*') // h3 headers to bold
+ .replace(/^## (.*$)/gm, '*$1*') // h2 headers to bold
+ .replace(/^# (.*$)/gm, '*$1*') // h1 headers to bold
+ .replace(/\*\*(.*?)\*\*/g, '*$1*') // bold
+ .replace(/__(.*?)__/g, '_$1_') // underline/italics
+ .replace(/\[(.*?)\]\((.*?)\)/g, '<$2|$1>') // links
+ .replace(/`{3}([\s\S]*?)`{3}/g, '```$1```') // code blocks
+ .replace(/`([^`]+)`/g, '`$1`'); // inline code
+
+ const slackEmoji = level === 'info' ? ':white_check_mark:' : level === 'warning' ? ':warning:' : ':alert:';
+
+ const slackBlocks = {
+ blocks: [
+ {
+ type: 'section',
+ text: {
+ type: 'plain_text',
+ emoji: true,
+ text: slackEmoji + ' ' + title
+ }
+ },
+ {
+ type: 'section',
+ fields: [
+ {
+ type: 'mrkdwn',
+ text: '*Database:*\n' + connection.name
+ },
+ {
+ type: 'mrkdwn',
+ text: '*Playbook:*\n' + schedule.playbook
+ },
+ {
+ type: 'mrkdwn',
+ text: '*Model:*\n' + schedule.model
+ },
+ {
+ type: 'mrkdwn',
+ text: '*Schedule:*\n' + (schedule.scheduleType === 'cron' ? schedule.cronExpression : 'Automatic')
+ }
+ ]
+ },
+ {
+ type: 'divider'
+ },
+ {
+ type: 'section',
+ text: {
+ type: 'mrkdwn',
+ text: message
+ }
+ },
+ {
+ type: 'divider'
+ },
+ {
+ type: 'section',
+ text: {
+ type: 'mrkdwn',
+ text: `I'll do the next check at *${schedule.nextRun}*`
+ }
+ },
+ {
+ type: 'actions',
+ elements: [
+ {
+ type: 'button',
+ text: {
+ type: 'plain_text',
+ text: 'View Schedule Settings'
+ },
+ url: `${env.PUBLIC_URL}/monitoring/schedule/${schedule.id}`
+ }
+ ]
+ }
+ ]
+ };
+
+ console.log(JSON.stringify(slackBlocks, null, 2));
+
+ const response = await fetch(slack.webhookUrl, {
+ method: 'POST',
+ body: JSON.stringify(slackBlocks)
+ });
+
+ if (!response.ok) {
+ console.error('Failed to send Slack notification:', await response.text());
+ }
+}
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 50a8fd57..6b246d2d 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -89,6 +89,9 @@ importers:
bytes:
specifier: ^3.1.2
version: 3.1.2
+ canvas-confetti:
+ specifier: ^1.9.3
+ version: 1.9.3
class-variance-authority:
specifier: ^0.7.1
version: 0.7.1
@@ -156,6 +159,9 @@ importers:
'@types/bytes':
specifier: ^3.1.5
version: 3.1.5
+ '@types/canvas-confetti':
+ specifier: ^1.9.0
+ version: 1.9.0
'@types/pg':
specifier: ^8.11.11
version: 8.11.11
@@ -2093,6 +2099,9 @@ packages:
'@types/bytes@3.1.5':
resolution: {integrity: sha512-VgZkrJckypj85YxEsEavcMmmSOIzkUHqWmM4CCyia5dc54YwsXzJ5uT4fYxBQNEXx+oF1krlhgCbvfubXqZYsQ==}
+ '@types/canvas-confetti@1.9.0':
+ resolution: {integrity: sha512-aBGj/dULrimR1XDZLtG9JwxX1b4HPRF6CX9Yfwh3NvstZEm1ZL7RBnel4keCPSqs1ANRu1u2Aoz9R+VmtjYuTg==}
+
'@types/chroma-js@3.1.1':
resolution: {integrity: sha512-SFCr4edNkZ1bGaLzGz7rgR1bRzVX4MmMxwsIa3/Bh6ose8v+hRpneoizHv0KChdjxaXyjRtaMq7sCuZSzPomQA==}
@@ -2532,6 +2541,9 @@ packages:
caniuse-lite@1.0.30001700:
resolution: {integrity: sha512-2S6XIXwaE7K7erT8dY+kLQcpa5ms63XlRkMkReXjle+kf6c5g38vyMl+Z5y8dSxOFDhcFe+nxnn261PLxBSQsQ==}
+ canvas-confetti@1.9.3:
+ resolution: {integrity: sha512-rFfTURMvmVEX1gyXFgn5QMn81bYk70qa0HLzcIOSVEyl57n6o9ItHeBtUSWdvKAPY0xlvBHno4/v3QPrT83q9g==}
+
ccount@2.0.1:
resolution: {integrity: sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==}
@@ -6917,6 +6929,8 @@ snapshots:
'@types/bytes@3.1.5': {}
+ '@types/canvas-confetti@1.9.0': {}
+
'@types/chroma-js@3.1.1': {}
'@types/d3-array@3.2.1': {}
@@ -7438,6 +7452,8 @@ snapshots:
caniuse-lite@1.0.30001700: {}
+ canvas-confetti@1.9.3: {}
+
ccount@2.0.1: {}
chai@5.2.0: