Merge branch 'operacle:develop' into develop
This commit is contained in:
@@ -20,7 +20,6 @@
|
|||||||
|
|
||||||
<body>
|
<body>
|
||||||
<div id="root"></div>
|
<div id="root"></div>
|
||||||
<!-- IMPORTANT: DO NOT REMOVE THIS SCRIPT TAG OR THIS VERY COMMENT! -->
|
|
||||||
<script type="module" src="/src/main.tsx"></script>
|
<script type="module" src="/src/main.tsx"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { getAuthHeaders, getBaseUrl, validateEmail } from '../utils';
|
|||||||
import { SettingsApiResponse } from '../types';
|
import { SettingsApiResponse } from '../types';
|
||||||
|
|
||||||
const createEmailTemplate = (template: string, data: any): { subject: string; htmlBody: string } => {
|
const createEmailTemplate = (template: string, data: any): { subject: string; htmlBody: string } => {
|
||||||
let subject = 'Test Email from ReamStack';
|
let subject = 'Test Email from CheckCle';
|
||||||
let htmlBody = `
|
let htmlBody = `
|
||||||
<html>
|
<html>
|
||||||
<body style="font-family: Arial, sans-serif; line-height: 1.6; color: #333;">
|
<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>
|
<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;">
|
<hr style="border: none; border-top: 1px solid #eee; margin: 20px 0;">
|
||||||
<p style="font-size: 12px; color: #666;">
|
<p style="font-size: 12px; color: #666;">
|
||||||
Sent from ReamStack Monitoring System<br>
|
Sent from CheckCle Monitoring System<br>
|
||||||
Template: ${template}<br>
|
Template: ${template}<br>
|
||||||
${data.collection ? `Collection: ${data.collection}` : ''}
|
${data.collection ? `Collection: ${data.collection}` : ''}
|
||||||
</p>
|
</p>
|
||||||
@@ -24,7 +24,7 @@ const createEmailTemplate = (template: string, data: any): { subject: string; ht
|
|||||||
|
|
||||||
switch (template) {
|
switch (template) {
|
||||||
case 'verification':
|
case 'verification':
|
||||||
subject = 'Email Verification Test - ReamStack';
|
subject = 'Email Verification Test - CheckCle';
|
||||||
htmlBody = `
|
htmlBody = `
|
||||||
<html>
|
<html>
|
||||||
<body style="font-family: Arial, sans-serif; line-height: 1.6; color: #333;">
|
<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>
|
<p><strong>Collection:</strong> ${data.collection || '_superusers'}</p>
|
||||||
</div>
|
</div>
|
||||||
<hr style="border: none; border-top: 1px solid #eee; margin: 20px 0;">
|
<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>
|
</div>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
`;
|
`;
|
||||||
break;
|
break;
|
||||||
case 'password-reset':
|
case 'password-reset':
|
||||||
subject = 'Password Reset Test - ReamStack';
|
subject = 'Password Reset Test - CheckCle';
|
||||||
htmlBody = `
|
htmlBody = `
|
||||||
<html>
|
<html>
|
||||||
<body style="font-family: Arial, sans-serif; line-height: 1.6; color: #333;">
|
<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>
|
<p><strong>Collection:</strong> ${data.collection || '_superusers'}</p>
|
||||||
</div>
|
</div>
|
||||||
<hr style="border: none; border-top: 1px solid #eee; margin: 20px 0;">
|
<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>
|
</div>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
`;
|
`;
|
||||||
break;
|
break;
|
||||||
case 'email-change':
|
case 'email-change':
|
||||||
subject = 'Email Change Confirmation Test - ReamStack';
|
subject = 'Email Change Confirmation Test - CheckCle';
|
||||||
htmlBody = `
|
htmlBody = `
|
||||||
<html>
|
<html>
|
||||||
<body style="font-family: Arial, sans-serif; line-height: 1.6; color: #333;">
|
<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>
|
<p><strong>Template:</strong> Email Change Confirmation</p>
|
||||||
</div>
|
</div>
|
||||||
<hr style="border: none; border-top: 1px solid #eee; margin: 20px 0;">
|
<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>
|
</div>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
@@ -178,10 +178,6 @@ export const sendTestEmail = async (data: any): Promise<SettingsApiResponse> =>
|
|||||||
smtpPort: smtpSettings.port || 587
|
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
|
// Simulate processing time
|
||||||
console.log('Simulating email send...');
|
console.log('Simulating email send...');
|
||||||
await new Promise(resolve => setTimeout(resolve, 1000));
|
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { getAuthHeaders, getBaseUrl, validateEmail } from '../utils';
|
|||||||
import { SettingsApiResponse } from '../types';
|
import { SettingsApiResponse } from '../types';
|
||||||
|
|
||||||
const createEmailTemplate = (template: string, data: any): { subject: string; htmlBody: string } => {
|
const createEmailTemplate = (template: string, data: any): { subject: string; htmlBody: string } => {
|
||||||
let subject = 'Test Email from ReamStack';
|
let subject = 'Test Email from CheckCle';
|
||||||
let htmlBody = `
|
let htmlBody = `
|
||||||
<html>
|
<html>
|
||||||
<body style="font-family: Arial, sans-serif; line-height: 1.6; color: #333;">
|
<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>
|
<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;">
|
<hr style="border: none; border-top: 1px solid #eee; margin: 20px 0;">
|
||||||
<p style="font-size: 12px; color: #666;">
|
<p style="font-size: 12px; color: #666;">
|
||||||
Sent from ReamStack Monitoring System<br>
|
Sent from CheckCle Monitoring System<br>
|
||||||
Template: ${template}<br>
|
Template: ${template}<br>
|
||||||
${data.collection ? `Collection: ${data.collection}` : ''}
|
${data.collection ? `Collection: ${data.collection}` : ''}
|
||||||
</p>
|
</p>
|
||||||
@@ -23,7 +23,7 @@ const createEmailTemplate = (template: string, data: any): { subject: string; ht
|
|||||||
|
|
||||||
switch (template) {
|
switch (template) {
|
||||||
case 'verification':
|
case 'verification':
|
||||||
subject = 'Email Verification Test - ReamStack';
|
subject = 'Email Verification Test - CheckCle';
|
||||||
htmlBody = `
|
htmlBody = `
|
||||||
<html>
|
<html>
|
||||||
<body style="font-family: Arial, sans-serif; line-height: 1.6; color: #333;">
|
<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>
|
<p><strong>Collection:</strong> ${data.collection || '_superusers'}</p>
|
||||||
</div>
|
</div>
|
||||||
<hr style="border: none; border-top: 1px solid #eee; margin: 20px 0;">
|
<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>
|
</div>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
`;
|
`;
|
||||||
break;
|
break;
|
||||||
case 'password-reset':
|
case 'password-reset':
|
||||||
subject = 'Password Reset Test - ReamStack';
|
subject = 'Password Reset Test - CheckCle';
|
||||||
htmlBody = `
|
htmlBody = `
|
||||||
<html>
|
<html>
|
||||||
<body style="font-family: Arial, sans-serif; line-height: 1.6; color: #333;">
|
<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>
|
<p><strong>Collection:</strong> ${data.collection || '_superusers'}</p>
|
||||||
</div>
|
</div>
|
||||||
<hr style="border: none; border-top: 1px solid #eee; margin: 20px 0;">
|
<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>
|
</div>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
`;
|
`;
|
||||||
break;
|
break;
|
||||||
case 'email-change':
|
case 'email-change':
|
||||||
subject = 'Email Change Confirmation Test - ReamStack';
|
subject = 'Email Change Confirmation Test - CheckCle';
|
||||||
htmlBody = `
|
htmlBody = `
|
||||||
<html>
|
<html>
|
||||||
<body style="font-family: Arial, sans-serif; line-height: 1.6; color: #333;">
|
<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>
|
<p><strong>Template:</strong> Email Change Confirmation</p>
|
||||||
</div>
|
</div>
|
||||||
<hr style="border: none; border-top: 1px solid #eee; margin: 20px 0;">
|
<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>
|
</div>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -47,21 +47,21 @@ export const ServerTable = ({ servers, isLoading, onRefresh }: ServerTableProps)
|
|||||||
newSet.delete(serverId);
|
newSet.delete(serverId);
|
||||||
return newSet;
|
return newSet;
|
||||||
});
|
});
|
||||||
console.log('Resume server monitoring:', serverId);
|
// console.log('Resume server monitoring:', serverId);
|
||||||
} else {
|
} else {
|
||||||
setPausedServers(prev => new Set(prev).add(serverId));
|
setPausedServers(prev => new Set(prev).add(serverId));
|
||||||
console.log('Pause server monitoring:', serverId);
|
// console.log('Pause server monitoring:', serverId);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleEdit = (serverId: string) => {
|
const handleEdit = (serverId: string) => {
|
||||||
// TODO: Implement edit functionality
|
// TODO: Implement edit functionality
|
||||||
console.log('Edit server:', serverId);
|
// console.log('Edit server:', serverId);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleDelete = (serverId: string) => {
|
const handleDelete = (serverId: string) => {
|
||||||
// TODO: Implement delete functionality
|
// TODO: Implement delete functionality
|
||||||
console.log('Delete server:', serverId);
|
// console.log('Delete server:', serverId);
|
||||||
};
|
};
|
||||||
|
|
||||||
if (isLoading) {
|
if (isLoading) {
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ export function ServiceUptimeHistory({
|
|||||||
const { data: uptimeHistory, isLoading, error } = useQuery({
|
const { data: uptimeHistory, isLoading, error } = useQuery({
|
||||||
queryKey: ['uptimeHistory', serviceId, serviceType, startDate?.toISOString(), endDate?.toISOString()],
|
queryKey: ['uptimeHistory', serviceId, serviceType, startDate?.toISOString(), endDate?.toISOString()],
|
||||||
queryFn: () => {
|
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);
|
return uptimeService.getUptimeHistory(serviceId, 200, startDate, endDate, serviceType);
|
||||||
},
|
},
|
||||||
enabled: !!serviceId && !!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 { useEffect } from "react";
|
||||||
import { Service } from "@/types/service.types";
|
import { Service } from "@/types/service.types";
|
||||||
import { ServicesTableView } from "./ServicesTableView";
|
import { ServicesTableView } from "./ServicesTableView";
|
||||||
|
import { ServicesPagination } from "./ServicesPagination";
|
||||||
import { ServiceDeleteDialog } from "./ServiceDeleteDialog";
|
import { ServiceDeleteDialog } from "./ServiceDeleteDialog";
|
||||||
import { ServiceHistoryDialog } from "./ServiceHistoryDialog";
|
import { ServiceHistoryDialog } from "./ServiceHistoryDialog";
|
||||||
import { ServiceEditDialog } from "./ServiceEditDialog";
|
import { ServiceEditDialog } from "./ServiceEditDialog";
|
||||||
import { useServiceActions, useDialogState } from "./hooks";
|
import { useServiceActions, useDialogState } from "./hooks";
|
||||||
|
import { useServicesPagination } from "@/hooks/useServicesPagination";
|
||||||
|
|
||||||
interface ServicesTableContainerProps {
|
interface ServicesTableContainerProps {
|
||||||
services: Service[];
|
services: Service[];
|
||||||
@@ -36,6 +38,16 @@ export const ServicesTableContainer = ({ services }: ServicesTableContainerProps
|
|||||||
handleDeleteDialogChange
|
handleDeleteDialogChange
|
||||||
} = useDialogState();
|
} = useDialogState();
|
||||||
|
|
||||||
|
const {
|
||||||
|
paginatedServices,
|
||||||
|
currentPage,
|
||||||
|
totalPages,
|
||||||
|
pageSize,
|
||||||
|
totalItems,
|
||||||
|
handlePageChange,
|
||||||
|
handlePageSizeChange,
|
||||||
|
} = useServicesPagination({ services: localServices });
|
||||||
|
|
||||||
// Update local services state when props change
|
// Update local services state when props change
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
updateServices(services);
|
updateServices(services);
|
||||||
@@ -62,7 +74,7 @@ export const ServicesTableContainer = ({ services }: ServicesTableContainerProps
|
|||||||
return (
|
return (
|
||||||
<div className="flex-1 flex flex-col h-full">
|
<div className="flex-1 flex flex-col h-full">
|
||||||
<ServicesTableView
|
<ServicesTableView
|
||||||
services={localServices}
|
services={paginatedServices}
|
||||||
onViewDetail={handleViewDetail}
|
onViewDetail={handleViewDetail}
|
||||||
onPauseResume={handlePauseResume}
|
onPauseResume={handlePauseResume}
|
||||||
onEdit={onEdit}
|
onEdit={onEdit}
|
||||||
@@ -70,6 +82,15 @@ export const ServicesTableContainer = ({ services }: ServicesTableContainerProps
|
|||||||
onMuteAlerts={handleMuteAlerts}
|
onMuteAlerts={handleMuteAlerts}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<ServicesPagination
|
||||||
|
currentPage={currentPage}
|
||||||
|
totalPages={totalPages}
|
||||||
|
pageSize={pageSize}
|
||||||
|
totalItems={totalItems}
|
||||||
|
onPageChange={handlePageChange}
|
||||||
|
onPageSizeChange={handlePageSizeChange}
|
||||||
|
/>
|
||||||
|
|
||||||
<ServiceHistoryDialog
|
<ServiceHistoryDialog
|
||||||
isOpen={isHistoryDialogOpen}
|
isOpen={isHistoryDialogOpen}
|
||||||
onOpenChange={setIsHistoryDialogOpen}
|
onOpenChange={setIsHistoryDialogOpen}
|
||||||
@@ -91,4 +112,4 @@ export const ServicesTableContainer = ({ services }: ServicesTableContainerProps
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -26,8 +26,8 @@ export const ServicesTableView = ({
|
|||||||
const { t } = useLanguage();
|
const { t } = useLanguage();
|
||||||
|
|
||||||
return (
|
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={`${theme === 'dark' ? 'bg-gray-900' : 'bg-white'} rounded-lg overflow-hidden border border-border shadow-sm`}>
|
||||||
<div className="flex-1 overflow-auto">
|
<div className="overflow-auto">
|
||||||
<Table>
|
<Table>
|
||||||
<TableHeader className={`${theme === 'dark' ? 'bg-gray-800' : 'bg-gray-50'} sticky top-0 z-10`}>
|
<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'}`}>
|
<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>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@@ -22,11 +22,11 @@ export function ServiceNotificationFields({ form }: ServiceNotificationFieldsPro
|
|||||||
const notificationChannels = form.watch("notificationChannels") || [];
|
const notificationChannels = form.watch("notificationChannels") || [];
|
||||||
const alertTemplate = form.watch("alertTemplate");
|
const alertTemplate = form.watch("alertTemplate");
|
||||||
|
|
||||||
console.log("Current notification values:", {
|
// console.log("Current notification values:", {
|
||||||
notificationStatus,
|
// notificationStatus,
|
||||||
notificationChannels,
|
// notificationChannels,
|
||||||
alertTemplate
|
// alertTemplate
|
||||||
});
|
// });
|
||||||
|
|
||||||
// Fetch alert configurations for notification channels
|
// Fetch alert configurations for notification channels
|
||||||
const { data: alertConfigsData } = useQuery({
|
const { data: alertConfigsData } = useQuery({
|
||||||
@@ -42,16 +42,16 @@ export function ServiceNotificationFields({ form }: ServiceNotificationFieldsPro
|
|||||||
setAlertConfigs(enabledChannels);
|
setAlertConfigs(enabledChannels);
|
||||||
|
|
||||||
// Debug log to check what alert configs are loaded
|
// Debug log to check what alert configs are loaded
|
||||||
console.log("Loaded alert configurations:", enabledChannels);
|
// console.log("Loaded alert configurations:", enabledChannels);
|
||||||
}
|
}
|
||||||
}, [alertConfigsData]);
|
}, [alertConfigsData]);
|
||||||
|
|
||||||
// Log when form values change to debug
|
// Log when form values change to debug
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
console.log("Notification values changed:", {
|
// console.log("Notification values changed:", {
|
||||||
notificationStatus: form.getValues("notificationStatus"),
|
// notificationStatus: form.getValues("notificationStatus"),
|
||||||
notificationChannels: form.getValues("notificationChannels")
|
// notificationChannels: form.getValues("notificationChannels")
|
||||||
});
|
// });
|
||||||
}, [form.watch("notificationStatus"), form.watch("notificationChannels")]);
|
}, [form.watch("notificationStatus"), form.watch("notificationChannels")]);
|
||||||
|
|
||||||
const handleChannelAdd = (channelId: string) => {
|
const handleChannelAdd = (channelId: string) => {
|
||||||
@@ -161,7 +161,7 @@ export function ServiceNotificationFields({ form }: ServiceNotificationFieldsPro
|
|||||||
render={({ field }) => {
|
render={({ field }) => {
|
||||||
// Don't convert existing values to "default"
|
// Don't convert existing values to "default"
|
||||||
const displayValue = field.value || "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 (
|
return (
|
||||||
<FormItem>
|
<FormItem>
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ export const ServiceRowActions = ({
|
|||||||
try {
|
try {
|
||||||
if (service.status === "paused") {
|
if (service.status === "paused") {
|
||||||
// Resume monitoring
|
// 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
|
// First ensure we update the status
|
||||||
await serviceService.resumeMonitoring(service.id);
|
await serviceService.resumeMonitoring(service.id);
|
||||||
@@ -53,7 +53,7 @@ export const ServiceRowActions = ({
|
|||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
// Pause monitoring
|
// 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);
|
await serviceService.pauseMonitoring(service.id);
|
||||||
|
|
||||||
toast({
|
toast({
|
||||||
@@ -65,7 +65,7 @@ export const ServiceRowActions = ({
|
|||||||
// Call the parent handler to refresh the UI
|
// Call the parent handler to refresh the UI
|
||||||
onPauseResume(service);
|
onPauseResume(service);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error toggling monitoring:", error);
|
// console.error("Error toggling monitoring:", error);
|
||||||
toast({
|
toast({
|
||||||
variant: "destructive",
|
variant: "destructive",
|
||||||
title: "Error",
|
title: "Error",
|
||||||
@@ -83,10 +83,10 @@ export const ServiceRowActions = ({
|
|||||||
|
|
||||||
if (onMuteAlerts) {
|
if (onMuteAlerts) {
|
||||||
try {
|
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);
|
await onMuteAlerts(service);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error toggling alerts:", error);
|
// console.error("Error toggling alerts:", error);
|
||||||
toast({
|
toast({
|
||||||
variant: "destructive",
|
variant: "destructive",
|
||||||
title: "Error",
|
title: "Error",
|
||||||
|
|||||||
@@ -83,7 +83,7 @@ export const AboutSystem: React.FC = () => {
|
|||||||
<span>{t('links')}</span>
|
<span>{t('links')}</span>
|
||||||
</CardTitle>
|
</CardTitle>
|
||||||
<CardDescription className="font-medium text-base">
|
<CardDescription className="font-medium text-base">
|
||||||
{systemName || 'ReamStack'} {t('resources').toLowerCase()}
|
{systemName || 'CheckCle'} {t('resources').toLowerCase()}
|
||||||
</CardDescription>
|
</CardDescription>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="space-y-4 pt-6">
|
<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,
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -161,6 +161,6 @@ export function useSystemSettings() {
|
|||||||
isUpdating: updateSettingsMutation.isPending,
|
isUpdating: updateSettingsMutation.isPending,
|
||||||
testEmailConnection: testEmailConnectionMutation.mutate,
|
testEmailConnection: testEmailConnectionMutation.mutate,
|
||||||
isTestingConnection: testEmailConnectionMutation.isPending,
|
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({
|
doc.setProperties({
|
||||||
title: title,
|
title: title,
|
||||||
subject: 'Incident Report',
|
subject: 'Incident Report',
|
||||||
author: 'ReamStack System',
|
author: 'CheckCle System',
|
||||||
creator: 'ReamStack',
|
creator: 'CheckCle',
|
||||||
});
|
});
|
||||||
|
|
||||||
// Add header section
|
// Add header section
|
||||||
|
|||||||
@@ -28,11 +28,11 @@ export const addHeader = (doc: jsPDF, incident: IncidentItem): number => {
|
|||||||
doc.setFontSize(10);
|
doc.setFontSize(10);
|
||||||
doc.text(`Generated on: ${new Date().toLocaleDateString()}`, 105, yPos, { align: 'center' });
|
doc.text(`Generated on: ${new Date().toLocaleDateString()}`, 105, yPos, { align: 'center' });
|
||||||
|
|
||||||
// Add ReamStack logo or text
|
// Add CheckCle logo or text
|
||||||
yPos += 8;
|
yPos += 8;
|
||||||
doc.setFontSize(12);
|
doc.setFontSize(12);
|
||||||
doc.setFont(fonts.italic);
|
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
|
// Add horizontal line
|
||||||
yPos += 5;
|
yPos += 5;
|
||||||
@@ -53,7 +53,7 @@ export const addFooter = (doc: jsPDF): void => {
|
|||||||
doc.setFontSize(8);
|
doc.setFontSize(8);
|
||||||
doc.setTextColor(100, 100, 100);
|
doc.setTextColor(100, 100, 100);
|
||||||
doc.text(
|
doc.text(
|
||||||
`Page ${i} of ${pageCount} | Generated by ReamStack Incident Management System`,
|
`Page ${i} of ${pageCount} | Generated by CheckCle Incident Management System`,
|
||||||
105,
|
105,
|
||||||
285,
|
285,
|
||||||
{ align: 'center' }
|
{ 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
|
// We'll skip the notification here since it will be handled by the UI component
|
||||||
// This prevents duplicate notifications for the paused status
|
// 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) {
|
} 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
|
// Fetch the current service to get its name for better logging
|
||||||
const service = await pb.collection('services').getOne(serviceId);
|
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
|
// First, clear any existing interval just to be safe
|
||||||
const existingInterval = monitoringIntervals.get(serviceId);
|
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";
|
const alertsMuted = service.alerts === "muted" || serviceForNotification.alerts === "muted";
|
||||||
|
|
||||||
if (!alertsMuted) {
|
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
|
// Send notification that service has been resumed
|
||||||
await notificationService.sendNotification({
|
await notificationService.sendNotification({
|
||||||
service: serviceForNotification,
|
service: serviceForNotification,
|
||||||
@@ -65,7 +65,7 @@ export async function resumeMonitoring(serviceId: string): Promise<void> {
|
|||||||
timestamp: now
|
timestamp: now
|
||||||
});
|
});
|
||||||
} else {
|
} 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
|
// 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`);
|
console.log(`Service ${service.name} resumed and ready for monitoring`);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error resuming service:", error);
|
// console.error("Error resuming service:", error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -31,20 +31,20 @@ export const notificationService = {
|
|||||||
try {
|
try {
|
||||||
const { service, status, responseTime } = data;
|
const { service, status, responseTime } = data;
|
||||||
|
|
||||||
console.log(`Preparing to send notification for service: ${service.name}, status: ${status}`);
|
// console.log(`Preparing to send notification for service: ${service.name}, status: ${status}`);
|
||||||
console.log(`Service alerts status: ${service.alerts}`);
|
// console.log(`Service alerts status: ${service.alerts}`);
|
||||||
|
|
||||||
// First check if alerts are muted for this service
|
// First check if alerts are muted for this service
|
||||||
// STRICT equality check against "muted" string value
|
// STRICT equality check against "muted" string value
|
||||||
if (service.alerts === "muted") {
|
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
|
return true; // Return true as this is expected behavior
|
||||||
}
|
}
|
||||||
|
|
||||||
// For paused status, check if this is a duplicate notification from another source
|
// For paused status, check if this is a duplicate notification from another source
|
||||||
// This helps prevent the double-notification issue
|
// This helps prevent the double-notification issue
|
||||||
if (status === "paused" && data._notificationSource === "duplicate_check") {
|
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
|
return true; // Return true as this is expected behavior
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -62,20 +62,20 @@ export const notificationService = {
|
|||||||
if (timeSinceLastNotif < NOTIFICATION_COOLDOWN) {
|
if (timeSinceLastNotif < NOTIFICATION_COOLDOWN) {
|
||||||
// Increment count only if we haven't reached max retries
|
// Increment count only if we haven't reached max retries
|
||||||
if (lastNotif.count < maxRetries) {
|
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;
|
lastNotifications[serviceId].count += 1;
|
||||||
} else {
|
} 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
|
return true; // Skip notification but return success
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Reset count after cooldown period
|
// 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 };
|
lastNotifications[serviceId] = { timestamp: now, count: 1 };
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// First notification for this service
|
// 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 };
|
lastNotifications[serviceId] = { timestamp: now, count: 1 };
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -85,7 +85,7 @@ export const notificationService = {
|
|||||||
|
|
||||||
// Check if notification channel is set
|
// Check if notification channel is set
|
||||||
if (!service.notificationChannel) {
|
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;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -93,12 +93,12 @@ export const notificationService = {
|
|||||||
const alertConfigRecord = await pb.collection('alert_configurations').getOne(service.notificationChannel);
|
const alertConfigRecord = await pb.collection('alert_configurations').getOne(service.notificationChannel);
|
||||||
|
|
||||||
if (!alertConfigRecord) {
|
if (!alertConfigRecord) {
|
||||||
console.error(`Alert configuration not found for ID: ${service.notificationChannel}`);
|
// console.error(`Alert configuration not found for ID: ${service.notificationChannel}`);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!alertConfigRecord.enabled) {
|
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;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -127,7 +127,7 @@ export const notificationService = {
|
|||||||
try {
|
try {
|
||||||
template = await templateService.getTemplate(service.alertTemplate);
|
template = await templateService.getTemplate(service.alertTemplate);
|
||||||
} catch (error) {
|
} 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}`;
|
message += `\n\nAlert ${retryInfo.count}/${maxRetries}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(`Prepared notification message: ${message}`);
|
// console.log(`Prepared notification message: ${message}`);
|
||||||
|
|
||||||
// Send notification based on notification type
|
// Send notification based on notification type
|
||||||
const notificationType = alertConfig.notification_type;
|
const notificationType = alertConfig.notification_type;
|
||||||
@@ -156,10 +156,10 @@ export const notificationService = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// For other types like discord, slack, etc. (not implemented yet)
|
// 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;
|
return false;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error sending notification:", error);
|
// console.error("Error sending notification:", error);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -173,10 +173,10 @@ export const notificationService = {
|
|||||||
? `Service ${serviceName} is UP${responseTime ? ` (Response time: ${responseTime}ms)` : ''}`
|
? `Service ${serviceName} is UP${responseTime ? ` (Response time: ${responseTime}ms)` : ''}`
|
||||||
: `Service ${serviceName} is DOWN`;
|
: `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
|
return true; // Just log, don't actually send
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error in test notification:", error);
|
// console.error("Error in test notification:", error);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -187,7 +187,7 @@ export const notificationService = {
|
|||||||
*/
|
*/
|
||||||
resetNotificationCount(serviceId: string): void {
|
resetNotificationCount(serviceId: string): void {
|
||||||
if (lastNotifications[serviceId]) {
|
if (lastNotifications[serviceId]) {
|
||||||
console.log(`Resetting notification count for service ${serviceId}`);
|
// console.log(`Resetting notification count for service ${serviceId}`);
|
||||||
delete lastNotifications[serviceId];
|
delete lastNotifications[serviceId];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,7 +9,15 @@ export type { Service, UptimeData, CreateServiceParams };
|
|||||||
export const serviceService = {
|
export const serviceService = {
|
||||||
async getServices(): Promise<Service[]> {
|
async getServices(): Promise<Service[]> {
|
||||||
try {
|
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',
|
sort: 'name',
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ export async function sendSSLNotification(
|
|||||||
try {
|
try {
|
||||||
// Check if notification channel is set
|
// Check if notification channel is set
|
||||||
if (!certificate.notification_channel) {
|
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;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -22,12 +22,12 @@ export async function sendSSLNotification(
|
|||||||
const alertConfigRecord = await pb.collection('alert_configurations').getOne(certificate.notification_channel);
|
const alertConfigRecord = await pb.collection('alert_configurations').getOne(certificate.notification_channel);
|
||||||
|
|
||||||
if (!alertConfigRecord) {
|
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;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!alertConfigRecord.enabled) {
|
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;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -63,7 +63,7 @@ export async function sendSSLNotification(
|
|||||||
return await sendNotificationByType(alertConfig, certificate, message, isCritical, sslNotification);
|
return await sendNotificationByType(alertConfig, certificate, message, isCritical, sslNotification);
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error sending SSL notification:", error);
|
// console.error("Error sending SSL notification:", error);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -87,7 +87,7 @@ async function sendNotificationByType(
|
|||||||
// case 'slack':
|
// case 'slack':
|
||||||
// return await sendSlackNotification(alertConfig, certificate, message, isCritical);
|
// return await sendSlackNotification(alertConfig, certificate, message, isCritical);
|
||||||
default:
|
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;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -102,7 +102,7 @@ async function sendTelegramNotification(
|
|||||||
isCritical: boolean
|
isCritical: boolean
|
||||||
): Promise<boolean> {
|
): Promise<boolean> {
|
||||||
if (!alertConfig.bot_token || !alertConfig.telegram_chat_id) {
|
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;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -112,10 +112,10 @@ export const deleteSSLCertificate = async (id: string): Promise<boolean> => {
|
|||||||
*/
|
*/
|
||||||
export const refreshAllCertificates = async (): Promise<{ success: number; failed: number }> => {
|
export const refreshAllCertificates = async (): Promise<{ success: number; failed: number }> => {
|
||||||
try {
|
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[];
|
const certificates = response.items as unknown as SSLCertificate[];
|
||||||
|
|
||||||
console.log(`Refreshing ${certificates.length} certificates...`);
|
// console.log(`Refreshing ${certificates.length} certificates...`);
|
||||||
|
|
||||||
let success = 0;
|
let success = 0;
|
||||||
let failed = 0;
|
let failed = 0;
|
||||||
@@ -125,14 +125,14 @@ export const refreshAllCertificates = async (): Promise<{ success: number; faile
|
|||||||
await checkCertificateAndNotify(cert);
|
await checkCertificateAndNotify(cert);
|
||||||
success++;
|
success++;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`Failed to refresh certificate ${cert.domain}:`, error);
|
// console.error(`Failed to refresh certificate ${cert.domain}:`, error);
|
||||||
failed++;
|
failed++;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return { success, failed };
|
return { success, failed };
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error refreshing certificates:", error);
|
// console.error("Error refreshing certificates:", error);
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -1,7 +1,6 @@
|
|||||||
|
|
||||||
import type { SSLCheckerResponse } from "./types";
|
import type { SSLCheckerResponse } from "./types";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { checkWithFetch } from "./sslPrimaryChecker";
|
|
||||||
import { normalizeDomain, createErrorResponse } from "./sslCheckerUtils";
|
import { normalizeDomain, createErrorResponse } from "./sslCheckerUtils";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -10,7 +9,7 @@ import { normalizeDomain, createErrorResponse } from "./sslCheckerUtils";
|
|||||||
*/
|
*/
|
||||||
export const checkSSLCertificate = async (domain: string): Promise<SSLCheckerResponse> => {
|
export const checkSSLCertificate = async (domain: string): Promise<SSLCheckerResponse> => {
|
||||||
try {
|
try {
|
||||||
console.log(`Checking SSL certificate for domain: ${domain}`);
|
// console.log(`Checking SSL certificate for domain: ${domain}`);
|
||||||
|
|
||||||
// Normalize domain (remove protocol if present)
|
// Normalize domain (remove protocol if present)
|
||||||
const normalizedDomain = normalizeDomain(domain);
|
const normalizedDomain = normalizeDomain(domain);
|
||||||
@@ -20,11 +19,11 @@ export const checkSSLCertificate = async (domain: string): Promise<SSLCheckerRes
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Use the working CORS proxy approach
|
// Use the working CORS proxy approach
|
||||||
const result = await checkWithFetch(normalizedDomain);
|
// const result = await checkWithFetch(normalizedDomain);
|
||||||
console.log("SSL check result:", result);
|
// console.log("SSL check result:", result);
|
||||||
return result;
|
// return result;
|
||||||
} catch (error) {
|
} 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'}`);
|
toast.error(`SSL check failed: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
||||||
return createErrorResponse(domain, error);
|
return createErrorResponse(domain, error);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ export const fetchSSLCertificates = async (): Promise<SSLCertificate[]> => {
|
|||||||
const endpoint = "/api/collections/ssl_certificates/records";
|
const endpoint = "/api/collections/ssl_certificates/records";
|
||||||
const params = {
|
const params = {
|
||||||
page: 1,
|
page: 1,
|
||||||
perPage: 50,
|
perPage: 200,
|
||||||
sort: "-created",
|
sort: "-created",
|
||||||
cache: Date.now() // Prevent caching by adding a timestamp
|
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,6 +1,6 @@
|
|||||||
|
|
||||||
export interface AboutTranslations {
|
export interface AboutTranslations {
|
||||||
aboutReamStack: string;
|
aboutCheckCle: string;
|
||||||
systemDescription: string;
|
systemDescription: string;
|
||||||
systemVersion: string;
|
systemVersion: string;
|
||||||
license: string;
|
license: string;
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -62,30 +62,47 @@ func (c *PocketBaseClient) GetService(serviceID string) (*Service, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (c *PocketBaseClient) GetActiveServices() ([]Service, error) {
|
func (c *PocketBaseClient) GetActiveServices() ([]Service, error) {
|
||||||
// Only fetch services that are not paused
|
var allServices []Service
|
||||||
req, err := http.NewRequest("GET",
|
page := 1
|
||||||
fmt.Sprintf("%s/api/collections/services/records?filter=(status!='paused')", c.baseURL), nil)
|
perPage := 30 // Use default pagination size
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
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
|
return allServices, nil
|
||||||
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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *PocketBaseClient) UpdateServiceStatus(serviceID string, status string, responseTime int64, errorMessage string) error {
|
func (c *PocketBaseClient) UpdateServiceStatus(serviceID string, status string, responseTime int64, errorMessage string) error {
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
|
|
||||||
package pocketbase
|
package pocketbase
|
||||||
|
|
||||||
import (
|
import (
|
||||||
@@ -11,37 +10,53 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type SSLCertificatesResponse struct {
|
type SSLCertificatesResponse struct {
|
||||||
Page int `json:"page"`
|
Page int `json:"page"`
|
||||||
PerPage int `json:"perPage"`
|
PerPage int `json:"perPage"`
|
||||||
TotalItems int `json:"totalItems"`
|
TotalItems int `json:"totalItems"`
|
||||||
TotalPages int `json:"totalPages"`
|
TotalPages int `json:"totalPages"`
|
||||||
Items []types.SSLCertificate `json:"items"`
|
Items []types.SSLCertificate `json:"items"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *PocketBaseClient) GetSSLCertificates() ([]types.SSLCertificate, error) {
|
func (c *PocketBaseClient) GetSSLCertificates() ([]types.SSLCertificate, error) {
|
||||||
url := fmt.Sprintf("%s/api/collections/ssl_certificates/records", c.baseURL)
|
var allCertificates []types.SSLCertificate
|
||||||
|
page := 1
|
||||||
resp, err := c.httpClient.Get(url)
|
perPage := 200 // You can increase up to 500 if needed
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to fetch SSL certificates: %v", err)
|
|
||||||
}
|
|
||||||
defer resp.Body.Close()
|
|
||||||
|
|
||||||
if resp.StatusCode != http.StatusOK {
|
for {
|
||||||
return nil, fmt.Errorf("PocketBase returned status %d", resp.StatusCode)
|
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
|
return allCertificates, nil
|
||||||
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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *PocketBaseClient) UpdateSSLCertificate(id string, data map[string]interface{}) error {
|
func (c *PocketBaseClient) UpdateSSLCertificate(id string, data map[string]interface{}) error {
|
||||||
url := fmt.Sprintf("%s/api/collections/ssl_certificates/records/%s", c.baseURL, id)
|
url := fmt.Sprintf("%s/api/collections/ssl_certificates/records/%s", c.baseURL, id)
|
||||||
|
|
||||||
jsonData, err := json.Marshal(data)
|
jsonData, err := json.Marshal(data)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to marshal SSL certificate data: %v", err)
|
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")
|
req.Header.Set("Content-Type", "application/json")
|
||||||
|
|
||||||
resp, err := c.httpClient.Do(req)
|
resp, err := c.httpClient.Do(req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to update SSL certificate: %v", err)
|
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) {
|
func (c *PocketBaseClient) GetSSLCertificateByID(id string) (*types.SSLCertificate, error) {
|
||||||
url := fmt.Sprintf("%s/api/collections/ssl_certificates/records/%s", c.baseURL, id)
|
url := fmt.Sprintf("%s/api/collections/ssl_certificates/records/%s", c.baseURL, id)
|
||||||
|
|
||||||
resp, err := c.httpClient.Get(url)
|
resp, err := c.httpClient.Get(url)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to fetch SSL certificate: %v", err)
|
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
|
return &cert, nil
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user