Merge branch 'operacle:develop' into develop

This commit is contained in:
gnsworks
2025-07-15 21:23:16 +09:00
committed by GitHub
32 changed files with 382 additions and 173 deletions
-1
View File
@@ -20,7 +20,6 @@
<body>
<div id="root"></div>
<!-- IMPORTANT: DO NOT REMOVE THIS SCRIPT TAG OR THIS VERY COMMENT! -->
<script type="module" src="/src/main.tsx"></script>
</body>
</html>
@@ -3,7 +3,7 @@ import { getAuthHeaders, getBaseUrl, validateEmail } from '../utils';
import { SettingsApiResponse } from '../types';
const createEmailTemplate = (template: string, data: any): { subject: string; htmlBody: string } => {
let subject = 'Test Email from ReamStack';
let subject = 'Test Email from CheckCle';
let htmlBody = `
<html>
<body style="font-family: Arial, sans-serif; line-height: 1.6; color: #333;">
@@ -13,7 +13,7 @@ const createEmailTemplate = (template: string, data: any): { subject: string; ht
<p>If you received this email, your SMTP configuration is working correctly.</p>
<hr style="border: none; border-top: 1px solid #eee; margin: 20px 0;">
<p style="font-size: 12px; color: #666;">
Sent from ReamStack Monitoring System<br>
Sent from CheckCle Monitoring System<br>
Template: ${template}<br>
${data.collection ? `Collection: ${data.collection}` : ''}
</p>
@@ -24,7 +24,7 @@ const createEmailTemplate = (template: string, data: any): { subject: string; ht
switch (template) {
case 'verification':
subject = 'Email Verification Test - ReamStack';
subject = 'Email Verification Test - CheckCle';
htmlBody = `
<html>
<body style="font-family: Arial, sans-serif; line-height: 1.6; color: #333;">
@@ -37,14 +37,14 @@ const createEmailTemplate = (template: string, data: any): { subject: string; ht
<p><strong>Collection:</strong> ${data.collection || '_superusers'}</p>
</div>
<hr style="border: none; border-top: 1px solid #eee; margin: 20px 0;">
<p style="font-size: 12px; color: #666;">Sent from ReamStack Monitoring System</p>
<p style="font-size: 12px; color: #666;">Sent from CheckCle Monitoring System</p>
</div>
</body>
</html>
`;
break;
case 'password-reset':
subject = 'Password Reset Test - ReamStack';
subject = 'Password Reset Test - CheckCle';
htmlBody = `
<html>
<body style="font-family: Arial, sans-serif; line-height: 1.6; color: #333;">
@@ -57,14 +57,14 @@ const createEmailTemplate = (template: string, data: any): { subject: string; ht
<p><strong>Collection:</strong> ${data.collection || '_superusers'}</p>
</div>
<hr style="border: none; border-top: 1px solid #eee; margin: 20px 0;">
<p style="font-size: 12px; color: #666;">Sent from ReamStack Monitoring System</p>
<p style="font-size: 12px; color: #666;">Sent from CheckCle Monitoring System</p>
</div>
</body>
</html>
`;
break;
case 'email-change':
subject = 'Email Change Confirmation Test - ReamStack';
subject = 'Email Change Confirmation Test - CheckCle';
htmlBody = `
<html>
<body style="font-family: Arial, sans-serif; line-height: 1.6; color: #333;">
@@ -76,7 +76,7 @@ const createEmailTemplate = (template: string, data: any): { subject: string; ht
<p><strong>Template:</strong> Email Change Confirmation</p>
</div>
<hr style="border: none; border-top: 1px solid #eee; margin: 20px 0;">
<p style="font-size: 12px; color: #666;">Sent from ReamStack Monitoring System</p>
<p style="font-size: 12px; color: #666;">Sent from CheckCle Monitoring System</p>
</div>
</body>
</html>
@@ -178,10 +178,6 @@ export const sendTestEmail = async (data: any): Promise<SettingsApiResponse> =>
smtpPort: smtpSettings.port || 587
});
// For now, we'll simulate a successful email send
// In a real implementation, you would integrate with your email service here
// This could be nodemailer, SendGrid, or your PocketBase email system
// Simulate processing time
console.log('Simulating email send...');
await new Promise(resolve => setTimeout(resolve, 1000));
@@ -2,7 +2,7 @@ import { getAuthHeaders, getBaseUrl, validateEmail } from '../utils';
import { SettingsApiResponse } from '../types';
const createEmailTemplate = (template: string, data: any): { subject: string; htmlBody: string } => {
let subject = 'Test Email from ReamStack';
let subject = 'Test Email from CheckCle';
let htmlBody = `
<html>
<body style="font-family: Arial, sans-serif; line-height: 1.6; color: #333;">
@@ -12,7 +12,7 @@ const createEmailTemplate = (template: string, data: any): { subject: string; ht
<p>If you received this email, your SMTP configuration is working correctly.</p>
<hr style="border: none; border-top: 1px solid #eee; margin: 20px 0;">
<p style="font-size: 12px; color: #666;">
Sent from ReamStack Monitoring System<br>
Sent from CheckCle Monitoring System<br>
Template: ${template}<br>
${data.collection ? `Collection: ${data.collection}` : ''}
</p>
@@ -23,7 +23,7 @@ const createEmailTemplate = (template: string, data: any): { subject: string; ht
switch (template) {
case 'verification':
subject = 'Email Verification Test - ReamStack';
subject = 'Email Verification Test - CheckCle';
htmlBody = `
<html>
<body style="font-family: Arial, sans-serif; line-height: 1.6; color: #333;">
@@ -36,14 +36,14 @@ const createEmailTemplate = (template: string, data: any): { subject: string; ht
<p><strong>Collection:</strong> ${data.collection || '_superusers'}</p>
</div>
<hr style="border: none; border-top: 1px solid #eee; margin: 20px 0;">
<p style="font-size: 12px; color: #666;">Sent from ReamStack Monitoring System</p>
<p style="font-size: 12px; color: #666;">Sent from CheckCle Monitoring System</p>
</div>
</body>
</html>
`;
break;
case 'password-reset':
subject = 'Password Reset Test - ReamStack';
subject = 'Password Reset Test - CheckCle';
htmlBody = `
<html>
<body style="font-family: Arial, sans-serif; line-height: 1.6; color: #333;">
@@ -56,14 +56,14 @@ const createEmailTemplate = (template: string, data: any): { subject: string; ht
<p><strong>Collection:</strong> ${data.collection || '_superusers'}</p>
</div>
<hr style="border: none; border-top: 1px solid #eee; margin: 20px 0;">
<p style="font-size: 12px; color: #666;">Sent from ReamStack Monitoring System</p>
<p style="font-size: 12px; color: #666;">Sent from CheckCle Monitoring System</p>
</div>
</body>
</html>
`;
break;
case 'email-change':
subject = 'Email Change Confirmation Test - ReamStack';
subject = 'Email Change Confirmation Test - CheckCle';
htmlBody = `
<html>
<body style="font-family: Arial, sans-serif; line-height: 1.6; color: #333;">
@@ -75,7 +75,7 @@ const createEmailTemplate = (template: string, data: any): { subject: string; ht
<p><strong>Template:</strong> Email Change Confirmation</p>
</div>
<hr style="border: none; border-top: 1px solid #eee; margin: 20px 0;">
<p style="font-size: 12px; color: #666;">Sent from ReamStack Monitoring System</p>
<p style="font-size: 12px; color: #666;">Sent from CheckCle Monitoring System</p>
</div>
</body>
</html>
@@ -47,21 +47,21 @@ export const ServerTable = ({ servers, isLoading, onRefresh }: ServerTableProps)
newSet.delete(serverId);
return newSet;
});
console.log('Resume server monitoring:', serverId);
// console.log('Resume server monitoring:', serverId);
} else {
setPausedServers(prev => new Set(prev).add(serverId));
console.log('Pause server monitoring:', serverId);
// console.log('Pause server monitoring:', serverId);
}
};
const handleEdit = (serverId: string) => {
// TODO: Implement edit functionality
console.log('Edit server:', serverId);
// console.log('Edit server:', serverId);
};
const handleDelete = (serverId: string) => {
// TODO: Implement delete functionality
console.log('Delete server:', serverId);
// console.log('Delete server:', serverId);
};
if (isLoading) {
@@ -25,7 +25,7 @@ export function ServiceUptimeHistory({
const { data: uptimeHistory, isLoading, error } = useQuery({
queryKey: ['uptimeHistory', serviceId, serviceType, startDate?.toISOString(), endDate?.toISOString()],
queryFn: () => {
console.log(`ServiceUptimeHistory: Fetching for service ${serviceId} of type ${serviceType}`);
// console.log(`ServiceUptimeHistory: Fetching for service ${serviceId} of type ${serviceType}`);
return uptimeService.getUptimeHistory(serviceId, 200, startDate, endDate, serviceType);
},
enabled: !!serviceId && !!serviceType,
@@ -0,0 +1,131 @@
import {
Pagination,
PaginationContent,
PaginationItem,
PaginationLink,
PaginationNext,
PaginationPrevious
} from "@/components/ui/pagination";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue
} from "@/components/ui/select";
import { PageSize } from "@/hooks/useServicesPagination";
import { useLanguage } from "@/contexts/LanguageContext";
interface ServicesPaginationProps {
currentPage: number;
totalPages: number;
pageSize: PageSize;
totalItems: number;
onPageChange: (page: number) => void;
onPageSizeChange: (size: PageSize) => void;
}
export function ServicesPagination({
currentPage,
totalPages,
pageSize,
totalItems,
onPageChange,
onPageSizeChange,
}: ServicesPaginationProps) {
const { t } = useLanguage();
// Generate page numbers to display
const getPageNumbers = () => {
const pages = [];
const maxVisiblePages = 5;
const halfVisible = Math.floor(maxVisiblePages / 2);
let startPage = Math.max(1, currentPage - halfVisible);
let endPage = Math.min(totalPages, startPage + maxVisiblePages - 1);
// Adjust start page if we don't have enough pages at the end
if (endPage - startPage + 1 < maxVisiblePages) {
startPage = Math.max(1, endPage - maxVisiblePages + 1);
}
for (let i = startPage; i <= endPage; i++) {
pages.push(i);
}
return pages;
};
const startItem = Math.min((currentPage - 1) * pageSize + 1, totalItems);
const endItem = Math.min(currentPage * pageSize, totalItems);
return (
<div className="flex items-center justify-between py-4 px-4 border-t border-border">
<div className="flex items-center space-x-2">
<span className="text-sm text-muted-foreground">
{t("rowsPerPage") ?? "Rows per page"}:
</span>
<Select
value={pageSize.toString()}
onValueChange={(value) => onPageSizeChange(parseInt(value) as PageSize)}
>
<SelectTrigger className="h-8 w-[70px]">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="10">10</SelectItem>
<SelectItem value="30">30</SelectItem>
<SelectItem value="50">50</SelectItem>
</SelectContent>
</Select>
<span className="text-sm text-muted-foreground whitespace-nowrap">
{totalItems > 0
? `${startItem}-${endItem} of ${totalItems} ${t("services") || "services"}`
: `0 ${t("services") || "services"}`
}
</span>
</div>
{totalPages > 1 && (
<Pagination>
<PaginationContent>
<PaginationItem>
<PaginationPrevious
onClick={() => onPageChange(Math.max(1, currentPage - 1))}
className={
currentPage === 1
? "pointer-events-none opacity-50"
: "cursor-pointer"
}
/>
</PaginationItem>
{getPageNumbers().map((page) => (
<PaginationItem key={page}>
<PaginationLink
isActive={page === currentPage}
onClick={() => onPageChange(page)}
className="cursor-pointer"
>
{page}
</PaginationLink>
</PaginationItem>
))}
<PaginationItem>
<PaginationNext
onClick={() => onPageChange(Math.min(totalPages, currentPage + 1))}
className={
currentPage === totalPages
? "pointer-events-none opacity-50"
: "cursor-pointer"
}
/>
</PaginationItem>
</PaginationContent>
</Pagination>
)}
</div>
);
}
@@ -2,10 +2,12 @@
import { useEffect } from "react";
import { Service } from "@/types/service.types";
import { ServicesTableView } from "./ServicesTableView";
import { ServicesPagination } from "./ServicesPagination";
import { ServiceDeleteDialog } from "./ServiceDeleteDialog";
import { ServiceHistoryDialog } from "./ServiceHistoryDialog";
import { ServiceEditDialog } from "./ServiceEditDialog";
import { useServiceActions, useDialogState } from "./hooks";
import { useServicesPagination } from "@/hooks/useServicesPagination";
interface ServicesTableContainerProps {
services: Service[];
@@ -36,6 +38,16 @@ export const ServicesTableContainer = ({ services }: ServicesTableContainerProps
handleDeleteDialogChange
} = useDialogState();
const {
paginatedServices,
currentPage,
totalPages,
pageSize,
totalItems,
handlePageChange,
handlePageSizeChange,
} = useServicesPagination({ services: localServices });
// Update local services state when props change
useEffect(() => {
updateServices(services);
@@ -62,7 +74,7 @@ export const ServicesTableContainer = ({ services }: ServicesTableContainerProps
return (
<div className="flex-1 flex flex-col h-full">
<ServicesTableView
services={localServices}
services={paginatedServices}
onViewDetail={handleViewDetail}
onPauseResume={handlePauseResume}
onEdit={onEdit}
@@ -70,6 +82,15 @@ export const ServicesTableContainer = ({ services }: ServicesTableContainerProps
onMuteAlerts={handleMuteAlerts}
/>
<ServicesPagination
currentPage={currentPage}
totalPages={totalPages}
pageSize={pageSize}
totalItems={totalItems}
onPageChange={handlePageChange}
onPageSizeChange={handlePageSizeChange}
/>
<ServiceHistoryDialog
isOpen={isHistoryDialogOpen}
onOpenChange={setIsHistoryDialogOpen}
@@ -91,4 +112,4 @@ export const ServicesTableContainer = ({ services }: ServicesTableContainerProps
/>
</div>
);
}
}
@@ -26,8 +26,8 @@ export const ServicesTableView = ({
const { t } = useLanguage();
return (
<div className={`${theme === 'dark' ? 'bg-gray-900' : 'bg-white'} rounded-lg overflow-hidden border border-border flex-1 flex flex-col shadow-sm`}>
<div className="flex-1 overflow-auto">
<div className={`${theme === 'dark' ? 'bg-gray-900' : 'bg-white'} rounded-lg overflow-hidden border border-border shadow-sm`}>
<div className="overflow-auto">
<Table>
<TableHeader className={`${theme === 'dark' ? 'bg-gray-800' : 'bg-gray-50'} sticky top-0 z-10`}>
<TableRow className={`${theme === 'dark' ? 'border-gray-700 hover:bg-gray-800' : 'border-gray-200 hover:bg-gray-100'}`}>
@@ -65,4 +65,4 @@ export const ServicesTableView = ({
</div>
</div>
);
};
};
@@ -22,11 +22,11 @@ export function ServiceNotificationFields({ form }: ServiceNotificationFieldsPro
const notificationChannels = form.watch("notificationChannels") || [];
const alertTemplate = form.watch("alertTemplate");
console.log("Current notification values:", {
notificationStatus,
notificationChannels,
alertTemplate
});
// console.log("Current notification values:", {
// notificationStatus,
// notificationChannels,
// alertTemplate
// });
// Fetch alert configurations for notification channels
const { data: alertConfigsData } = useQuery({
@@ -42,16 +42,16 @@ export function ServiceNotificationFields({ form }: ServiceNotificationFieldsPro
setAlertConfigs(enabledChannels);
// Debug log to check what alert configs are loaded
console.log("Loaded alert configurations:", enabledChannels);
// console.log("Loaded alert configurations:", enabledChannels);
}
}, [alertConfigsData]);
// Log when form values change to debug
useEffect(() => {
console.log("Notification values changed:", {
notificationStatus: form.getValues("notificationStatus"),
notificationChannels: form.getValues("notificationChannels")
});
// console.log("Notification values changed:", {
// notificationStatus: form.getValues("notificationStatus"),
// notificationChannels: form.getValues("notificationChannels")
// });
}, [form.watch("notificationStatus"), form.watch("notificationChannels")]);
const handleChannelAdd = (channelId: string) => {
@@ -161,7 +161,7 @@ export function ServiceNotificationFields({ form }: ServiceNotificationFieldsPro
render={({ field }) => {
// Don't convert existing values to "default"
const displayValue = field.value || "default";
console.log("Rendering alert template field with value:", displayValue);
// console.log("Rendering alert template field with value:", displayValue);
return (
<FormItem>
@@ -39,7 +39,7 @@ export const ServiceRowActions = ({
try {
if (service.status === "paused") {
// Resume monitoring
console.log(`Resuming monitoring for service ${service.id} (${service.name}) from dropdown`);
// console.log(`Resuming monitoring for service ${service.id} (${service.name}) from dropdown`);
// First ensure we update the status
await serviceService.resumeMonitoring(service.id);
@@ -53,7 +53,7 @@ export const ServiceRowActions = ({
});
} else {
// Pause monitoring
console.log(`Pausing monitoring for service ${service.id} (${service.name}) from dropdown`);
// console.log(`Pausing monitoring for service ${service.id} (${service.name}) from dropdown`);
await serviceService.pauseMonitoring(service.id);
toast({
@@ -65,7 +65,7 @@ export const ServiceRowActions = ({
// Call the parent handler to refresh the UI
onPauseResume(service);
} catch (error) {
console.error("Error toggling monitoring:", error);
// console.error("Error toggling monitoring:", error);
toast({
variant: "destructive",
title: "Error",
@@ -83,10 +83,10 @@ export const ServiceRowActions = ({
if (onMuteAlerts) {
try {
console.log(`Attempting to ${alertsMuted ? 'unmute' : 'mute'} alerts for service ${service.id} (${service.name})`);
// console.log(`Attempting to ${alertsMuted ? 'unmute' : 'mute'} alerts for service ${service.id} (${service.name})`);
await onMuteAlerts(service);
} catch (error) {
console.error("Error toggling alerts:", error);
// console.error("Error toggling alerts:", error);
toast({
variant: "destructive",
title: "Error",
@@ -83,7 +83,7 @@ export const AboutSystem: React.FC = () => {
<span>{t('links')}</span>
</CardTitle>
<CardDescription className="font-medium text-base">
{systemName || 'ReamStack'} {t('resources').toLowerCase()}
{systemName || 'CheckCle'} {t('resources').toLowerCase()}
</CardDescription>
</CardHeader>
<CardContent className="space-y-4 pt-6">
@@ -0,0 +1,52 @@
import { useState, useMemo } from 'react';
import { Service } from '@/types/service.types';
export type PageSize = 10 | 30 | 50;
interface UseServicesPaginationProps {
services: Service[];
initialPageSize?: PageSize;
}
export const useServicesPagination = ({
services,
initialPageSize = 10
}: UseServicesPaginationProps) => {
const [currentPage, setCurrentPage] = useState(1);
const [pageSize, setPageSize] = useState<PageSize>(initialPageSize);
const { paginatedServices, totalPages } = useMemo(() => {
const totalItems = services.length;
const pages = Math.ceil(totalItems / pageSize);
const startIndex = (currentPage - 1) * pageSize;
const endIndex = startIndex + pageSize;
return {
paginatedServices: services.slice(startIndex, endIndex),
totalPages: Math.max(1, pages)
};
}, [services, currentPage, pageSize]);
// Reset to first page when page size changes or services change
const handlePageSizeChange = (newPageSize: PageSize) => {
setPageSize(newPageSize);
setCurrentPage(1);
};
// Reset to first page if current page exceeds total pages
const handlePageChange = (page: number) => {
const validPage = Math.min(Math.max(1, page), totalPages);
setCurrentPage(validPage);
};
return {
paginatedServices,
currentPage,
totalPages,
pageSize,
totalItems: services.length,
handlePageChange,
handlePageSizeChange,
};
};
+1 -1
View File
@@ -161,6 +161,6 @@ export function useSystemSettings() {
isUpdating: updateSettingsMutation.isPending,
testEmailConnection: testEmailConnectionMutation.mutate,
isTestingConnection: testEmailConnectionMutation.isPending,
systemName: settings?.system_name || settings?.meta?.appName || 'ReamStack',
systemName: settings?.system_name || settings?.meta?.appName || 'CheckCle',
};
}
@@ -38,8 +38,8 @@ export const generatePdf = async (incident: IncidentItem): Promise<string> => {
doc.setProperties({
title: title,
subject: 'Incident Report',
author: 'ReamStack System',
creator: 'ReamStack',
author: 'CheckCle System',
creator: 'CheckCle',
});
// Add header section
@@ -28,11 +28,11 @@ export const addHeader = (doc: jsPDF, incident: IncidentItem): number => {
doc.setFontSize(10);
doc.text(`Generated on: ${new Date().toLocaleDateString()}`, 105, yPos, { align: 'center' });
// Add ReamStack logo or text
// Add CheckCle logo or text
yPos += 8;
doc.setFontSize(12);
doc.setFont(fonts.italic);
doc.text('ReamStack Incident Management', 105, yPos, { align: 'center' });
doc.text('CheckCle Incident Management', 105, yPos, { align: 'center' });
// Add horizontal line
yPos += 5;
@@ -53,7 +53,7 @@ export const addFooter = (doc: jsPDF): void => {
doc.setFontSize(8);
doc.setTextColor(100, 100, 100);
doc.text(
`Page ${i} of ${pageCount} | Generated by ReamStack Incident Management System`,
`Page ${i} of ${pageCount} | Generated by CheckCle Incident Management System`,
105,
285,
{ align: 'center' }
@@ -31,8 +31,8 @@ export async function pauseMonitoring(serviceId: string): Promise<void> {
// We'll skip the notification here since it will be handled by the UI component
// This prevents duplicate notifications for the paused status
console.log(`Service ${service.name} paused at ${now}, skipping notification to prevent duplication`);
// console.log(`Service ${service.name} paused at ${now}, skipping notification to prevent duplication`);
} catch (error) {
console.error("Error pausing monitoring:", error);
// console.error("Error pausing monitoring:", error);
}
}
@@ -16,7 +16,7 @@ export async function resumeMonitoring(serviceId: string): Promise<void> {
// Fetch the current service to get its name for better logging
const service = await pb.collection('services').getOne(serviceId);
console.log(`Resuming service ${service.name} at ${now}`);
// console.log(`Resuming service ${service.name} at ${now}`);
// First, clear any existing interval just to be safe
const existingInterval = monitoringIntervals.get(serviceId);
@@ -57,7 +57,7 @@ export async function resumeMonitoring(serviceId: string): Promise<void> {
const alertsMuted = service.alerts === "muted" || serviceForNotification.alerts === "muted";
if (!alertsMuted) {
console.log(`Alerts NOT muted for service ${service.name}, sending resume notification`);
// console.log(`Alerts NOT muted for service ${service.name}, sending resume notification`);
// Send notification that service has been resumed
await notificationService.sendNotification({
service: serviceForNotification,
@@ -65,7 +65,7 @@ export async function resumeMonitoring(serviceId: string): Promise<void> {
timestamp: now
});
} else {
console.log(`Alerts muted for service ${service.name}, skipping resume notification`);
// console.log(`Alerts muted for service ${service.name}, skipping resume notification`);
}
// IMPORTANT: Wait a brief moment to ensure the status update is processed
@@ -76,6 +76,6 @@ export async function resumeMonitoring(serviceId: string): Promise<void> {
console.log(`Service ${service.name} resumed and ready for monitoring`);
} catch (error) {
console.error("Error resuming service:", error);
// console.error("Error resuming service:", error);
}
}
+18 -18
View File
@@ -31,20 +31,20 @@ export const notificationService = {
try {
const { service, status, responseTime } = data;
console.log(`Preparing to send notification for service: ${service.name}, status: ${status}`);
console.log(`Service alerts status: ${service.alerts}`);
// console.log(`Preparing to send notification for service: ${service.name}, status: ${status}`);
// console.log(`Service alerts status: ${service.alerts}`);
// First check if alerts are muted for this service
// STRICT equality check against "muted" string value
if (service.alerts === "muted") {
console.log(`NOTIFICATION BLOCKED: Alerts are muted for service: ${service.name}`);
// console.log(`NOTIFICATION BLOCKED: Alerts are muted for service: ${service.name}`);
return true; // Return true as this is expected behavior
}
// For paused status, check if this is a duplicate notification from another source
// This helps prevent the double-notification issue
if (status === "paused" && data._notificationSource === "duplicate_check") {
console.log("NOTIFICATION BLOCKED: Duplicate pause notification detected");
// console.log("NOTIFICATION BLOCKED: Duplicate pause notification detected");
return true; // Return true as this is expected behavior
}
@@ -62,20 +62,20 @@ export const notificationService = {
if (timeSinceLastNotif < NOTIFICATION_COOLDOWN) {
// Increment count only if we haven't reached max retries
if (lastNotif.count < maxRetries) {
console.log(`DOWN notification for ${service.name}: ${lastNotif.count + 1}/${maxRetries}`);
// console.log(`DOWN notification for ${service.name}: ${lastNotif.count + 1}/${maxRetries}`);
lastNotifications[serviceId].count += 1;
} else {
console.log(`DOWN notification for ${service.name} skipped: Max retries (${maxRetries}) reached. Next notification after cooldown.`);
// console.log(`DOWN notification for ${service.name} skipped: Max retries (${maxRetries}) reached. Next notification after cooldown.`);
return true; // Skip notification but return success
}
} else {
// Reset count after cooldown period
console.log(`Cooldown period elapsed for ${service.name}. Resetting notification count.`);
// console.log(`Cooldown period elapsed for ${service.name}. Resetting notification count.`);
lastNotifications[serviceId] = { timestamp: now, count: 1 };
}
} else {
// First notification for this service
console.log(`First DOWN notification for ${service.name}: 1/${maxRetries}`);
// console.log(`First DOWN notification for ${service.name}: 1/${maxRetries}`);
lastNotifications[serviceId] = { timestamp: now, count: 1 };
}
@@ -85,7 +85,7 @@ export const notificationService = {
// Check if notification channel is set
if (!service.notificationChannel) {
console.log(`No notification channel set for service: ${service.name}`);
// console.log(`No notification channel set for service: ${service.name}`);
return false;
}
@@ -93,12 +93,12 @@ export const notificationService = {
const alertConfigRecord = await pb.collection('alert_configurations').getOne(service.notificationChannel);
if (!alertConfigRecord) {
console.error(`Alert configuration not found for ID: ${service.notificationChannel}`);
// console.error(`Alert configuration not found for ID: ${service.notificationChannel}`);
return false;
}
if (!alertConfigRecord.enabled) {
console.log(`Alert configuration is disabled for service: ${service.name}`);
// console.log(`Alert configuration is disabled for service: ${service.name}`);
return false;
}
@@ -127,7 +127,7 @@ export const notificationService = {
try {
template = await templateService.getTemplate(service.alertTemplate);
} catch (error) {
console.error(`Error fetching template for ID: ${service.alertTemplate}`, error);
// console.error(`Error fetching template for ID: ${service.alertTemplate}`, error);
}
}
@@ -146,7 +146,7 @@ export const notificationService = {
message += `\n\nAlert ${retryInfo.count}/${maxRetries}`;
}
console.log(`Prepared notification message: ${message}`);
// console.log(`Prepared notification message: ${message}`);
// Send notification based on notification type
const notificationType = alertConfig.notification_type;
@@ -156,10 +156,10 @@ export const notificationService = {
}
// For other types like discord, slack, etc. (not implemented yet)
console.log(`Notification type ${notificationType} not implemented yet`);
// console.log(`Notification type ${notificationType} not implemented yet`);
return false;
} catch (error) {
console.error("Error sending notification:", error);
// console.error("Error sending notification:", error);
return false;
}
},
@@ -173,10 +173,10 @@ export const notificationService = {
? `Service ${serviceName} is UP${responseTime ? ` (Response time: ${responseTime}ms)` : ''}`
: `Service ${serviceName} is DOWN`;
console.log(`Test notification would have been sent: ${message}`);
// console.log(`Test notification would have been sent: ${message}`);
return true; // Just log, don't actually send
} catch (error) {
console.error("Error in test notification:", error);
// console.error("Error in test notification:", error);
return false;
}
},
@@ -187,7 +187,7 @@ export const notificationService = {
*/
resetNotificationCount(serviceId: string): void {
if (lastNotifications[serviceId]) {
console.log(`Resetting notification count for service ${serviceId}`);
// console.log(`Resetting notification count for service ${serviceId}`);
delete lastNotifications[serviceId];
}
}
+9 -1
View File
@@ -9,7 +9,15 @@ export type { Service, UptimeData, CreateServiceParams };
export const serviceService = {
async getServices(): Promise<Service[]> {
try {
const response = await pb.collection('services').getList(1, 50, {
// First get the total count of records
const countResponse = await pb.collection('services').getList(1, 1, {
sort: 'name',
});
const totalRecords = countResponse.totalItems;
// Then fetch all records using the total count as the limit
const response = await pb.collection('services').getList(1, totalRecords, {
sort: 'name',
});
@@ -14,7 +14,7 @@ export async function sendSSLNotification(
try {
// Check if notification channel is set
if (!certificate.notification_channel) {
console.log(`No notification channel set for certificate: ${certificate.domain}`);
// console.log(`No notification channel set for certificate: ${certificate.domain}`);
return false;
}
@@ -22,12 +22,12 @@ export async function sendSSLNotification(
const alertConfigRecord = await pb.collection('alert_configurations').getOne(certificate.notification_channel);
if (!alertConfigRecord) {
console.error(`Alert configuration not found for ID: ${certificate.notification_channel}`);
// console.error(`Alert configuration not found for ID: ${certificate.notification_channel}`);
return false;
}
if (!alertConfigRecord.enabled) {
console.log(`Alert configuration is disabled for certificate: ${certificate.domain}`);
// console.log(`Alert configuration is disabled for certificate: ${certificate.domain}`);
return false;
}
@@ -63,7 +63,7 @@ export async function sendSSLNotification(
return await sendNotificationByType(alertConfig, certificate, message, isCritical, sslNotification);
} catch (error) {
console.error("Error sending SSL notification:", error);
// console.error("Error sending SSL notification:", error);
return false;
}
}
@@ -87,7 +87,7 @@ async function sendNotificationByType(
// case 'slack':
// return await sendSlackNotification(alertConfig, certificate, message, isCritical);
default:
console.log(`Notification type ${alertConfig.notification_type} not implemented yet for SSL certificates`);
// console.log(`Notification type ${alertConfig.notification_type} not implemented yet for SSL certificates`);
return false;
}
}
@@ -102,7 +102,7 @@ async function sendTelegramNotification(
isCritical: boolean
): Promise<boolean> {
if (!alertConfig.bot_token || !alertConfig.telegram_chat_id) {
console.error("Missing Telegram bot token or chat ID");
// console.error("Missing Telegram bot token or chat ID");
return false;
}
@@ -112,10 +112,10 @@ export const deleteSSLCertificate = async (id: string): Promise<boolean> => {
*/
export const refreshAllCertificates = async (): Promise<{ success: number; failed: number }> => {
try {
const response = await pb.collection("ssl_certificates").getList(1, 100);
const response = await pb.collection("ssl_certificates").getList(1, 200);
const certificates = response.items as unknown as SSLCertificate[];
console.log(`Refreshing ${certificates.length} certificates...`);
// console.log(`Refreshing ${certificates.length} certificates...`);
let success = 0;
let failed = 0;
@@ -125,14 +125,14 @@ export const refreshAllCertificates = async (): Promise<{ success: number; faile
await checkCertificateAndNotify(cert);
success++;
} catch (error) {
console.error(`Failed to refresh certificate ${cert.domain}:`, error);
// console.error(`Failed to refresh certificate ${cert.domain}:`, error);
failed++;
}
}
return { success, failed };
} catch (error) {
console.error("Error refreshing certificates:", error);
// console.error("Error refreshing certificates:", error);
throw error;
}
};
@@ -1,7 +1,6 @@
import type { SSLCheckerResponse } from "./types";
import { toast } from "sonner";
import { checkWithFetch } from "./sslPrimaryChecker";
import { normalizeDomain, createErrorResponse } from "./sslCheckerUtils";
/**
@@ -10,7 +9,7 @@ import { normalizeDomain, createErrorResponse } from "./sslCheckerUtils";
*/
export const checkSSLCertificate = async (domain: string): Promise<SSLCheckerResponse> => {
try {
console.log(`Checking SSL certificate for domain: ${domain}`);
// console.log(`Checking SSL certificate for domain: ${domain}`);
// Normalize domain (remove protocol if present)
const normalizedDomain = normalizeDomain(domain);
@@ -20,11 +19,11 @@ export const checkSSLCertificate = async (domain: string): Promise<SSLCheckerRes
}
// Use the working CORS proxy approach
const result = await checkWithFetch(normalizedDomain);
console.log("SSL check result:", result);
return result;
// const result = await checkWithFetch(normalizedDomain);
// console.log("SSL check result:", result);
// return result;
} catch (error) {
console.error("SSL check failed completely:", error);
//console.error("SSL check failed completely:", error);
toast.error(`SSL check failed: ${error instanceof Error ? error.message : 'Unknown error'}`);
return createErrorResponse(domain, error);
}
@@ -14,7 +14,7 @@ export const fetchSSLCertificates = async (): Promise<SSLCertificate[]> => {
const endpoint = "/api/collections/ssl_certificates/records";
const params = {
page: 1,
perPage: 50,
perPage: 200,
sort: "-created",
cache: Date.now() // Prevent caching by adding a timestamp
};
@@ -1,29 +0,0 @@
import { createErrorResponse } from "./sslCheckerUtils";
import type { SSLCheckerResponse } from "./types";
/**
* Check SSL certificate using fetch with CORS proxy
* This is our primary method that's proven to work
*/
export const checkWithFetch = async (domain: string): Promise<SSLCheckerResponse> => {
console.log(`Checking SSL via CORS proxy for: ${domain}`);
try {
// Use the working CORS proxy
const corsProxyUrl = `https://api.allorigins.win/raw?url=${encodeURIComponent(`https://ssl-checker.io/api/v1/check/${domain}`)}`;
console.log("Using CORS proxy for SSL check:", corsProxyUrl);
const proxyResponse = await fetch(corsProxyUrl);
if (!proxyResponse.ok) {
throw new Error(`CORS proxy request failed with status: ${proxyResponse.status}`);
}
const proxyData = await proxyResponse.json();
console.log("CORS proxy returned SSL data:", proxyData);
return proxyData;
} catch (error) {
console.error("SSL check failed:", error);
return createErrorResponse(domain, error);
}
};
+1 -1
View File
@@ -1,6 +1,6 @@
export interface AboutTranslations {
aboutReamStack: string;
aboutCheckCle: string;
systemDescription: string;
systemVersion: string;
license: string;
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
View File
+39 -22
View File
@@ -62,30 +62,47 @@ func (c *PocketBaseClient) GetService(serviceID string) (*Service, error) {
}
func (c *PocketBaseClient) GetActiveServices() ([]Service, error) {
// Only fetch services that are not paused
req, err := http.NewRequest("GET",
fmt.Sprintf("%s/api/collections/services/records?filter=(status!='paused')", c.baseURL), nil)
if err != nil {
return nil, err
var allServices []Service
page := 1
perPage := 30 // Use default pagination size
for {
// Fetch services page by page with filter for non-paused services
req, err := http.NewRequest("GET",
fmt.Sprintf("%s/api/collections/services/records?page=%d&perPage=%d&filter=(status!='paused')",
c.baseURL, page, perPage), nil)
if err != nil {
return nil, err
}
// No authentication header needed for public access
resp, err := c.httpClient.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("failed to fetch active services, status: %d", resp.StatusCode)
}
var servicesResponse ServicesResponse
if err := json.NewDecoder(resp.Body).Decode(&servicesResponse); err != nil {
return nil, err
}
// Add current page items to the result
allServices = append(allServices, servicesResponse.Items...)
// Check if we've fetched all pages
if page >= servicesResponse.TotalPages || len(servicesResponse.Items) == 0 {
break
}
page++
}
// No authentication header needed for public access
resp, err := c.httpClient.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("failed to fetch active services, status: %d", resp.StatusCode)
}
var servicesResponse ServicesResponse
if err := json.NewDecoder(resp.Body).Decode(&servicesResponse); err != nil {
return nil, err
}
return servicesResponse.Items, nil
return allServices, nil
}
func (c *PocketBaseClient) UpdateServiceStatus(serviceID string, status string, responseTime int64, errorMessage string) error {
+40 -25
View File
@@ -1,4 +1,3 @@
package pocketbase
import (
@@ -11,37 +10,53 @@ import (
)
type SSLCertificatesResponse struct {
Page int `json:"page"`
PerPage int `json:"perPage"`
TotalItems int `json:"totalItems"`
TotalPages int `json:"totalPages"`
Items []types.SSLCertificate `json:"items"`
Page int `json:"page"`
PerPage int `json:"perPage"`
TotalItems int `json:"totalItems"`
TotalPages int `json:"totalPages"`
Items []types.SSLCertificate `json:"items"`
}
func (c *PocketBaseClient) GetSSLCertificates() ([]types.SSLCertificate, error) {
url := fmt.Sprintf("%s/api/collections/ssl_certificates/records", c.baseURL)
resp, err := c.httpClient.Get(url)
if err != nil {
return nil, fmt.Errorf("failed to fetch SSL certificates: %v", err)
}
defer resp.Body.Close()
var allCertificates []types.SSLCertificate
page := 1
perPage := 200 // You can increase up to 500 if needed
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("PocketBase returned status %d", resp.StatusCode)
for {
url := fmt.Sprintf(
"%s/api/collections/ssl_certificates/records?page=%d&perPage=%d",
c.baseURL, page, perPage,
)
resp, err := c.httpClient.Get(url)
if err != nil {
return nil, fmt.Errorf("failed to fetch SSL certificates: %v", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("PocketBase returned status %d", resp.StatusCode)
}
var response SSLCertificatesResponse
if err := json.NewDecoder(resp.Body).Decode(&response); err != nil {
return nil, fmt.Errorf("failed to decode SSL certificates response: %v", err)
}
allCertificates = append(allCertificates, response.Items...)
if page >= response.TotalPages || len(response.Items) == 0 {
break
}
page++
}
var response SSLCertificatesResponse
if err := json.NewDecoder(resp.Body).Decode(&response); err != nil {
return nil, fmt.Errorf("failed to decode SSL certificates response: %v", err)
}
return response.Items, nil
return allCertificates, nil
}
func (c *PocketBaseClient) UpdateSSLCertificate(id string, data map[string]interface{}) error {
url := fmt.Sprintf("%s/api/collections/ssl_certificates/records/%s", c.baseURL, id)
jsonData, err := json.Marshal(data)
if err != nil {
return fmt.Errorf("failed to marshal SSL certificate data: %v", err)
@@ -53,7 +68,7 @@ func (c *PocketBaseClient) UpdateSSLCertificate(id string, data map[string]inter
}
req.Header.Set("Content-Type", "application/json")
resp, err := c.httpClient.Do(req)
if err != nil {
return fmt.Errorf("failed to update SSL certificate: %v", err)
@@ -69,7 +84,7 @@ func (c *PocketBaseClient) UpdateSSLCertificate(id string, data map[string]inter
func (c *PocketBaseClient) GetSSLCertificateByID(id string) (*types.SSLCertificate, error) {
url := fmt.Sprintf("%s/api/collections/ssl_certificates/records/%s", c.baseURL, id)
resp, err := c.httpClient.Get(url)
if err != nil {
return nil, fmt.Errorf("failed to fetch SSL certificate: %v", err)
@@ -86,4 +101,4 @@ func (c *PocketBaseClient) GetSSLCertificateByID(id string) (*types.SSLCertifica
}
return &cert, nil
}
}