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> <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,
};
};
+1 -1
View File
@@ -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);
} }
} }
+18 -18
View File
@@ -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 -1
View File
@@ -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 -1
View File
@@ -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.
View File
+39 -22
View File
@@ -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 {
+40 -25
View File
@@ -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
} }