Merge pull request #22 from operacle/develop
feat: release maintenance and incident management features; apply hotfixes
This commit is contained in:
@@ -22,3 +22,5 @@ dist-ssr
|
|||||||
*.njsproj
|
*.njsproj
|
||||||
*.sln
|
*.sln
|
||||||
*.sw?
|
*.sw?
|
||||||
|
application/src/lib/pocketbase.ts
|
||||||
|
Dockerfile
|
||||||
|
|||||||
-39
@@ -1,39 +0,0 @@
|
|||||||
# Stage 1: Build the frontend
|
|
||||||
FROM node:18-alpine AS frontend-builder
|
|
||||||
WORKDIR /app
|
|
||||||
COPY application/package*.json ./
|
|
||||||
RUN npm install
|
|
||||||
COPY application/ ./
|
|
||||||
RUN npm run build
|
|
||||||
|
|
||||||
# Stage 2: Download PocketBase binary
|
|
||||||
FROM alpine:3.17 AS pb-builder
|
|
||||||
WORKDIR /pb
|
|
||||||
ARG PB_VERSION=0.27.2
|
|
||||||
RUN apk add --no-cache wget unzip \
|
|
||||||
&& wget https://github.com/pocketbase/pocketbase/releases/download/v${PB_VERSION}/pocketbase_${PB_VERSION}_linux_amd64.zip \
|
|
||||||
&& unzip pocketbase_${PB_VERSION}_linux_amd64.zip \
|
|
||||||
&& chmod +x /pb/pocketbase
|
|
||||||
|
|
||||||
# Stage 3: Final runtime image
|
|
||||||
FROM alpine:3.17
|
|
||||||
WORKDIR /app
|
|
||||||
|
|
||||||
# Copy PocketBase binary
|
|
||||||
COPY --from=pb-builder /pb/pocketbase /app/pocketbase
|
|
||||||
|
|
||||||
# Copy frontend build to PocketBase public directory
|
|
||||||
COPY --from=frontend-builder /app/dist /app/pb_public
|
|
||||||
|
|
||||||
# Copy backend files
|
|
||||||
COPY server/pb_migrations /app/pb_migrations
|
|
||||||
COPY server/pb_data /app/pb_data
|
|
||||||
|
|
||||||
# Mark pb_data as a volume for runtime persistence
|
|
||||||
VOLUME /app/pb_data
|
|
||||||
|
|
||||||
# Expose default PocketBase port
|
|
||||||
EXPOSE 8090
|
|
||||||
|
|
||||||
# Launch PocketBase and bind to 0.0.0.0 for external access
|
|
||||||
CMD ["/app/pocketbase", "serve", "--http=0.0.0.0:8090", "--dir", "/app/pb_data"]
|
|
||||||
@@ -24,22 +24,23 @@ CheckCle is an Open Source solution for seamless, real-time monitoring of full-s
|
|||||||
|
|
||||||
## #️⃣ Getting Started
|
## #️⃣ Getting Started
|
||||||
|
|
||||||
### Installation with Docker Compose
|
### Installation with Docker Run and Compose
|
||||||
1. Copy ready docker run command
|
1. Copy ready docker run command
|
||||||
```bash
|
```bash
|
||||||
# Create Docker Volume for data persistence
|
docker run -d \
|
||||||
|
--name checkcle \
|
||||||
docker volume create pb_data
|
--restart unless-stopped \
|
||||||
|
-p 8090:8090 \
|
||||||
|
-v /opt/pb_data:/mnt/pb_data \
|
||||||
# Docker Run Command
|
--ulimit nofile=4096:8192 \
|
||||||
|
operacle/checkcle:latest
|
||||||
docker run --name checkcle --restart unless-stopped -p 8090:8090 -v pb_data:/app/pb_data --ulimit nofile=4096:8192 operacle/checkcle:latest
|
|
||||||
|
|
||||||
```
|
```
|
||||||
2. Docker Compose - Recommended
|
2. Docker Compose - Recommended
|
||||||
```bash
|
```bash
|
||||||
|
|
||||||
|
version: '3.9'
|
||||||
|
|
||||||
services:
|
services:
|
||||||
checkcle:
|
checkcle:
|
||||||
image: operacle/checkcle:latest
|
image: operacle/checkcle:latest
|
||||||
@@ -48,15 +49,12 @@ services:
|
|||||||
ports:
|
ports:
|
||||||
- "8090:8090" # Web Application
|
- "8090:8090" # Web Application
|
||||||
volumes:
|
volumes:
|
||||||
- pb_data:/app/pb_data # Ensure persistent data across rebuilds
|
- /opt/pb_data:/mnt/pb_data # Host directory mapped to container path
|
||||||
ulimits:
|
ulimits:
|
||||||
nofile:
|
nofile:
|
||||||
soft: 4096
|
soft: 4096
|
||||||
hard: 8192
|
hard: 8192
|
||||||
|
|
||||||
volumes:
|
|
||||||
pb_data: # Docker-managed volume for data persistence
|
|
||||||
|
|
||||||
```
|
```
|
||||||
3. Admin Web Management
|
3. Admin Web Management
|
||||||
|
|
||||||
@@ -64,12 +62,12 @@ volumes:
|
|||||||
User: admin@example.com
|
User: admin@example.com
|
||||||
Passwd: Admin123456
|
Passwd: Admin123456
|
||||||
|
|
||||||
4. Follow the Quick Start Guide at https://docs.checkcle.com (Coming Soon)
|
4. Follow the Quick Start Guide at https://docs.checkcle.io
|
||||||
|
|
||||||
###
|
###
|
||||||

|
|
||||||

|

|
||||||

|

|
||||||
|

|
||||||
|
|
||||||
## 📝 Development Roadmap
|
## 📝 Development Roadmap
|
||||||
|
|
||||||
@@ -80,15 +78,16 @@ volumes:
|
|||||||
- [x] Docker containerization
|
- [x] Docker containerization
|
||||||
- [x] CheckCle Website
|
- [x] CheckCle Website
|
||||||
- [x] CheckCle Demo Server
|
- [x] CheckCle Demo Server
|
||||||
- [x] SSL & Domain Monitoring - added in release of https://github.com/operacle/checkcle/releases/tag/v1.1.0
|
- [x] SSL & Domain Monitoring
|
||||||
|
- [x] Schedule Maintenance
|
||||||
|
- [x] Incident Management
|
||||||
- [ ] Uptime monitoring (PING - Inprogress)
|
- [ ] Uptime monitoring (PING - Inprogress)
|
||||||
- [ ] Infrastructure Server Monitoring
|
- [ ] Infrastructure Server Monitoring
|
||||||
- [ ] Schedule Maintenance & Incident Management
|
|
||||||
- [ ] Operational Status / Public Status Pages
|
- [ ] Operational Status / Public Status Pages
|
||||||
- [ ] Uptime monitoring (TCP, PING, DNS)
|
- [ ] Uptime monitoring (TCP, PING, DNS)
|
||||||
- [ ] User Permission Roles & Service Group
|
- [ ] User Permission Roles & Service Group
|
||||||
- [ ] Notifications (Email/Slack/Discord/Signal)
|
- [ ] Notifications (Email/Slack/Discord/Signal)
|
||||||
- [ ] Open-source release with full documentation
|
- [x] Open-source release with full documentation
|
||||||
|
|
||||||
## 🌟 CheckCle for Communities?
|
## 🌟 CheckCle for Communities?
|
||||||
- **Built with Passion**: Created by an open-source enthusiast for the community
|
- **Built with Passion**: Created by an open-source enthusiast for the community
|
||||||
@@ -111,6 +110,7 @@ Here are some ways you can help improve CheckCle:
|
|||||||
|
|
||||||
## 🌍 Stay Connected
|
## 🌍 Stay Connected
|
||||||
- Website: [checkcle.io](https://checkcle.io)
|
- Website: [checkcle.io](https://checkcle.io)
|
||||||
|
- Documentation: [docs.checkcle.io](https://docs.checkcle.io)
|
||||||
- GitHub Repository: ⭐ [CheckCle](https://github.com/operacle/checkcle.git)
|
- GitHub Repository: ⭐ [CheckCle](https://github.com/operacle/checkcle.git)
|
||||||
- Community Channels: Engage via discussions and issues!
|
- Community Channels: Engage via discussions and issues!
|
||||||
- Discord: Join our community [@discord](https://discord.gg/xs9gbubGwX)
|
- Discord: Join our community [@discord](https://discord.gg/xs9gbubGwX)
|
||||||
|
|||||||
@@ -43,22 +43,22 @@ export const ServiceRow = ({
|
|||||||
className={`border-b ${theme === 'dark' ? 'border-gray-800 hover:bg-gray-900/60' : 'border-gray-200 hover:bg-gray-50'} cursor-pointer`}
|
className={`border-b ${theme === 'dark' ? 'border-gray-800 hover:bg-gray-900/60' : 'border-gray-200 hover:bg-gray-50'} cursor-pointer`}
|
||||||
onClick={handleRowClick}
|
onClick={handleRowClick}
|
||||||
>
|
>
|
||||||
<TableCell className="font-medium py-4" onClick={(e) => e.stopPropagation()}>
|
<TableCell className="font-medium py-4">
|
||||||
<ServiceRowHeader service={service} />
|
<ServiceRowHeader service={service} />
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell onClick={(e) => e.stopPropagation()} className={`text-base py-4 ${theme === 'dark' ? 'text-gray-300' : 'text-gray-700'}`}>
|
<TableCell className={`text-base py-4 ${theme === 'dark' ? 'text-gray-300' : 'text-gray-700'}`}>
|
||||||
{service.type}
|
{service.type}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell onClick={(e) => e.stopPropagation()} className="py-4">
|
<TableCell className="py-4">
|
||||||
<StatusBadge status={service.status} size="md" />
|
<StatusBadge status={service.status} size="md" />
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell onClick={(e) => e.stopPropagation()} className="py-4">
|
<TableCell className="py-4">
|
||||||
<ServiceRowResponseTime responseTime={service.responseTime} />
|
<ServiceRowResponseTime responseTime={service.responseTime} />
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell className="w-52 py-4" onClick={(e) => e.stopPropagation()}>
|
<TableCell className="w-52 py-4">
|
||||||
<UptimeBar uptime={service.uptime} status={service.status} serviceId={service.id} />
|
<UptimeBar uptime={service.uptime} status={service.status} serviceId={service.id} />
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell onClick={(e) => e.stopPropagation()} className="py-4">
|
<TableCell className="py-4">
|
||||||
<LastCheckedTime
|
<LastCheckedTime
|
||||||
lastCheckedTime={displayTimestamp}
|
lastCheckedTime={displayTimestamp}
|
||||||
status={service.status}
|
status={service.status}
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
|
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { useToast } from "@/hooks/use-toast";
|
import { useToast } from "@/hooks/use-toast";
|
||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate } from "react-router-dom";
|
||||||
@@ -24,6 +23,7 @@ export function useServiceActions(initialServices: Service[]) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleViewDetail = (service: Service) => {
|
const handleViewDetail = (service: Service) => {
|
||||||
|
console.log(`Navigating to service detail for service ID: ${service.id}`);
|
||||||
navigate(`/service/${service.id}`);
|
navigate(`/service/${service.id}`);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
|
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { Card } from "@/components/ui/card";
|
|
||||||
import { SSLCertificate } from "@/types/ssl.types";
|
import { SSLCertificate } from "@/types/ssl.types";
|
||||||
import { useLanguage } from "@/contexts/LanguageContext";
|
import { useLanguage } from "@/contexts/LanguageContext";
|
||||||
|
import { OverviewCard } from "@/components/schedule-incident/common/OverviewCard";
|
||||||
|
import { Shield, ShieldAlert, ShieldX } from "lucide-react";
|
||||||
|
|
||||||
interface SSLCertificateStatusCardsProps {
|
interface SSLCertificateStatusCardsProps {
|
||||||
certificates: SSLCertificate[];
|
certificates: SSLCertificate[];
|
||||||
@@ -16,48 +18,30 @@ export const SSLCertificateStatusCards = ({ certificates }: SSLCertificateStatus
|
|||||||
const expiredCount = certificates.filter(cert => cert.status === 'expired').length;
|
const expiredCount = certificates.filter(cert => cert.status === 'expired').length;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
|
||||||
<Card className="p-6 flex items-center space-x-4">
|
<OverviewCard
|
||||||
<div className="bg-green-100 dark:bg-green-900/20 p-3 rounded-full">
|
title={t('validCertificates')}
|
||||||
<div className="h-12 w-12 rounded-full bg-green-500/20 flex items-center justify-center">
|
value={validCount}
|
||||||
<div className="h-8 w-8 rounded-full bg-green-500 flex items-center justify-center text-white">
|
icon={<Shield className="h-6 w-6 text-white" />}
|
||||||
✓
|
color="green"
|
||||||
</div>
|
className="hover:scale-105 transition-transform duration-200"
|
||||||
</div>
|
/>
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<p className="text-sm font-medium text-muted-foreground">{t('validCertificates')}</p>
|
|
||||||
<p className="text-3xl font-bold">{validCount}</p>
|
|
||||||
</div>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
<Card className="p-6 flex items-center space-x-4">
|
<OverviewCard
|
||||||
<div className="bg-yellow-100 dark:bg-yellow-900/20 p-3 rounded-full">
|
title={t('expiringSoon')}
|
||||||
<div className="h-12 w-12 rounded-full bg-yellow-500/20 flex items-center justify-center">
|
value={expiringCount}
|
||||||
<div className="h-8 w-8 rounded-full bg-yellow-500 flex items-center justify-center text-white">
|
icon={<ShieldAlert className="h-6 w-6 text-white" />}
|
||||||
!
|
color="amber"
|
||||||
</div>
|
className="hover:scale-105 transition-transform duration-200"
|
||||||
</div>
|
/>
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<p className="text-sm font-medium text-muted-foreground">{t('expiringSoon')}</p>
|
|
||||||
<p className="text-3xl font-bold">{expiringCount}</p>
|
|
||||||
</div>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
<Card className="p-6 flex items-center space-x-4">
|
<OverviewCard
|
||||||
<div className="bg-red-100 dark:bg-red-900/20 p-3 rounded-full">
|
title={t('expired')}
|
||||||
<div className="h-12 w-12 rounded-full bg-red-500/20 flex items-center justify-center">
|
value={expiredCount}
|
||||||
<div className="h-8 w-8 rounded-full bg-red-500 flex items-center justify-center text-white">
|
icon={<ShieldX className="h-6 w-6 text-white" />}
|
||||||
✗
|
color="red"
|
||||||
</div>
|
className="hover:scale-105 transition-transform duration-200"
|
||||||
</div>
|
/>
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<p className="text-sm font-medium text-muted-foreground">{t('expired')}</p>
|
|
||||||
<p className="text-3xl font-bold">{expiredCount}</p>
|
|
||||||
</div>
|
|
||||||
</Card>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@@ -37,7 +37,7 @@ export const LanguageProvider = ({ children }: { children: ReactNode }) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return key;
|
return key;
|
||||||
git fetch origin };
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<LanguageContext.Provider value={{ language, setLanguage, t }}>
|
<LanguageContext.Provider value={{ language, setLanguage, t }}>
|
||||||
|
|||||||
@@ -1,16 +1,17 @@
|
|||||||
import PocketBase from 'pocketbase';
|
import PocketBase from 'pocketbase';
|
||||||
|
|
||||||
// Auto-detect base URL from browser location
|
// Dynamically detect API base URL from current host (for use in browser)
|
||||||
const dynamicBaseUrl =
|
const dynamicBaseUrl =
|
||||||
typeof window !== 'undefined'
|
typeof window !== 'undefined'
|
||||||
? window.location.origin
|
? `${window.location.protocol}//${window.location.hostname}:8090`
|
||||||
: 'http://localhost:8090';
|
: 'http://localhost:8090';
|
||||||
|
|
||||||
// Define API endpoints
|
// Define available API endpoints
|
||||||
export const API_ENDPOINTS = {
|
export const API_ENDPOINTS = {
|
||||||
REMOTE: dynamicBaseUrl
|
REMOTE: dynamicBaseUrl
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Get the current endpoint from localStorage or use remote as default
|
||||||
export const getCurrentEndpoint = (): string => {
|
export const getCurrentEndpoint = (): string => {
|
||||||
if (typeof window !== 'undefined') {
|
if (typeof window !== 'undefined') {
|
||||||
const savedEndpoint = localStorage.getItem('pocketbase_endpoint');
|
const savedEndpoint = localStorage.getItem('pocketbase_endpoint');
|
||||||
@@ -19,19 +20,26 @@ export const getCurrentEndpoint = (): string => {
|
|||||||
return API_ENDPOINTS.REMOTE;
|
return API_ENDPOINTS.REMOTE;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Set the API endpoint and reinitialize PocketBase
|
||||||
export const setApiEndpoint = (endpoint: string): void => {
|
export const setApiEndpoint = (endpoint: string): void => {
|
||||||
if (typeof window !== 'undefined') {
|
if (typeof window !== 'undefined') {
|
||||||
localStorage.setItem('pocketbase_endpoint', endpoint);
|
localStorage.setItem('pocketbase_endpoint', endpoint);
|
||||||
window.location.reload();
|
window.location.reload(); // Reload to reinitialize PocketBase with new endpoint
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Initialize the PocketBase client with the current API URL
|
||||||
export const pb = new PocketBase(getCurrentEndpoint());
|
export const pb = new PocketBase(getCurrentEndpoint());
|
||||||
|
|
||||||
export const isAuthenticated = () => pb.authStore.isValid;
|
// Helper to check if user is authenticated
|
||||||
|
export const isAuthenticated = () => {
|
||||||
|
return pb.authStore.isValid;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Export the auth store for use in components
|
||||||
export const authStore = pb.authStore;
|
export const authStore = pb.authStore;
|
||||||
|
|
||||||
|
// Configure PocketBase to persist authentication between page reloads
|
||||||
if (typeof window !== 'undefined') {
|
if (typeof window !== 'undefined') {
|
||||||
const storedAuthData = localStorage.getItem('pocketbase_auth');
|
const storedAuthData = localStorage.getItem('pocketbase_auth');
|
||||||
if (storedAuthData) {
|
if (storedAuthData) {
|
||||||
@@ -44,15 +52,13 @@ if (typeof window !== 'undefined') {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Subscribe to authStore changes to persist authentication
|
||||||
pb.authStore.onChange(() => {
|
pb.authStore.onChange(() => {
|
||||||
if (pb.authStore.isValid) {
|
if (pb.authStore.isValid) {
|
||||||
localStorage.setItem(
|
localStorage.setItem('pocketbase_auth', JSON.stringify({
|
||||||
'pocketbase_auth',
|
|
||||||
JSON.stringify({
|
|
||||||
token: pb.authStore.token,
|
token: pb.authStore.token,
|
||||||
model: pb.authStore.model
|
model: pb.authStore.model
|
||||||
})
|
}));
|
||||||
);
|
|
||||||
} else {
|
} else {
|
||||||
localStorage.removeItem('pocketbase_auth');
|
localStorage.removeItem('pocketbase_auth');
|
||||||
}
|
}
|
||||||
|
|||||||
+2
-4
@@ -1,3 +1,4 @@
|
|||||||
|
version: '3.9'
|
||||||
services:
|
services:
|
||||||
checkcle:
|
checkcle:
|
||||||
image: operacle/checkcle:latest
|
image: operacle/checkcle:latest
|
||||||
@@ -6,11 +7,8 @@ services:
|
|||||||
ports:
|
ports:
|
||||||
- "8090:8090" # Web Application
|
- "8090:8090" # Web Application
|
||||||
volumes:
|
volumes:
|
||||||
- pb_data:/app/pb_data # Ensure persistent data across rebuilds
|
- /opt/pb_data:/mnt/pb_data # Host directory mapped to container path
|
||||||
ulimits:
|
ulimits:
|
||||||
nofile:
|
nofile:
|
||||||
soft: 4096
|
soft: 4096
|
||||||
hard: 8192
|
hard: 8192
|
||||||
|
|
||||||
volumes:
|
|
||||||
pb_data: # Docker-managed volume for data persistence
|
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
version: '3.9'
|
||||||
|
|
||||||
services:
|
services:
|
||||||
checkcle:
|
checkcle:
|
||||||
build:
|
build:
|
||||||
@@ -6,13 +8,10 @@ services:
|
|||||||
container_name: checkcle
|
container_name: checkcle
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
ports:
|
ports:
|
||||||
- "8090:8090" # Allow access to frontend + PocketBase
|
- "8090:8090"
|
||||||
volumes:
|
volumes:
|
||||||
- pb_data:/app/pb_data # Ensure persistent data across rebuilds
|
- /var/pb_data:/mnt/pb_data # Updated mount target to match CMD in Dockerfile
|
||||||
ulimits:
|
ulimits:
|
||||||
nofile:
|
nofile:
|
||||||
soft: 4096
|
soft: 4096
|
||||||
hard: 8192
|
hard: 8192
|
||||||
|
|
||||||
volumes:
|
|
||||||
pb_data: # Docker-managed volume for persistence
|
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
version: '3.9'
|
||||||
|
|
||||||
services:
|
services:
|
||||||
checkcle:
|
checkcle:
|
||||||
image: operacle/checkcle:latest
|
image: operacle/checkcle:latest
|
||||||
@@ -6,11 +8,8 @@ services:
|
|||||||
ports:
|
ports:
|
||||||
- "8090:8090" # Web Application
|
- "8090:8090" # Web Application
|
||||||
volumes:
|
volumes:
|
||||||
- pb_data:/app/pb_data # Ensure persistent data across rebuilds
|
- /opt/pb_data:/mnt/pb_data # Host directory mapped to container path
|
||||||
ulimits:
|
ulimits:
|
||||||
nofile:
|
nofile:
|
||||||
soft: 4096
|
soft: 4096
|
||||||
hard: 8192
|
hard: 8192
|
||||||
|
|
||||||
volumes:
|
|
||||||
pb_data: # Docker-managed volume for data persistence
|
|
||||||
|
|||||||
Reference in New Issue
Block a user