diff --git a/CLAUDE.md b/CLAUDE.md
new file mode 100644
index 000000000..1535e84cf
--- /dev/null
+++ b/CLAUDE.md
@@ -0,0 +1,145 @@
+# CLAUDE.md
+
+This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
+
+## Project Overview
+
+Checkmate is an open-source uptime and infrastructure monitoring application. It monitors server hardware, uptime, response times, and incidents with real-time alerts. The companion agent [Capture](https://github.com/bluewave-labs/capture) provides infrastructure metrics (CPU, RAM, disk, temperature).
+
+## Development Commands
+
+### Client (React/Vite)
+```bash
+cd client
+npm install
+npm run dev # Start dev server at http://localhost:5173
+npm run build # TypeScript check + production build
+npm run lint # ESLint (strict, max-warnings 0)
+npm run format # Prettier formatting
+npm run format-check # Check formatting
+```
+
+### Server (Node.js/Express)
+```bash
+cd server
+npm install
+npm run dev # Start with hot-reload (nodemon + tsx) at http://localhost:52345
+npm run build # TypeScript compile + path alias resolution
+npm run test # Run Mocha tests with c8 coverage
+npm run lint # ESLint v9
+npm run lint-fix # Auto-fix lint issues
+npm run format # Prettier formatting
+```
+
+### Docker Development
+```bash
+cd docker/dev
+./build_images.sh
+docker run -d -p 27017:27017 -v uptime_mongo_data:/data/db --name uptime_database_mongo mongo:6.0
+```
+
+## Environment Setup
+
+### Server `.env` (minimum required)
+```env
+CLIENT_HOST="http://localhost:5173"
+JWT_SECRET="my_secret_key_change_this"
+DB_CONNECTION_STRING="mongodb://localhost:27017/uptime_db"
+TOKEN_TTL="99d"
+ORIGIN="localhost"
+LOG_LEVEL="debug"
+```
+
+### Client `.env`
+```env
+VITE_APP_API_BASE_URL="http://localhost:52345/api/v1"
+VITE_APP_LOG_LEVEL="debug"
+```
+
+## Architecture
+
+### Monorepo Structure
+- `/client` - React 18 + TypeScript + Vite + MUI frontend
+- `/server` - Node.js 20+ + Express + TypeScript backend
+- `/docker` - Multi-environment Docker configs (dev, staging, prod, arm, mono)
+
+### Backend Layers
+```
+server/src/
+├── controllers/ # Route handlers (authController, monitorController, etc.)
+├── service/ # Business logic
+│ ├── business/ # Core monitoring logic
+│ ├── infrastructure/ # Server/system utilities
+│ └── system/ # App-level settings
+├── db/
+│ ├── models/ # Mongoose schemas (Monitor, Check, Incident, User, etc.)
+│ ├── migration/ # Database migrations (run on startup)
+│ └── modules/ # Database-specific modules
+├── middleware/v1/ # verifyJWT, rateLimiter, sanitization, responseHandler
+├── routes/v1/ # API route definitions
+├── validation/ # Joi input validation schemas
+└── repositories/ # Data access layer
+```
+
+### Frontend Structure
+```
+client/src/
+├── Components/ # Reusable UI components
+├── Pages/ # Page components (Auth, Uptime, Infrastructure, Incidents, etc.)
+├── Features/ # Redux slices (Auth, UI)
+├── Hooks/ # Custom React hooks
+├── Utils/ # Utilities (NetworkService.js is main API client)
+├── Validation/ # Input validation
+└── locales/ # i18n translations
+```
+
+### API
+- Base URL: `/api/v1`
+- Documentation: `http://localhost:52345/api-docs` (Swagger UI)
+- OpenAPI spec: `/server/openapi.json`
+
+### Key Technologies
+- **State Management**: Redux Toolkit + Redux-Persist
+- **Data Fetching**: SWR + Axios
+- **Database**: MongoDB with Mongoose ODM
+- **Queue/Cache**: Redis + BullMQ + Pulse (cron scheduling)
+- **i18n**: i18next + react-i18next (translations via PoEditor)
+
+## Code Conventions
+
+### Internationalization
+All user-facing strings must use the translation function:
+```javascript
+t('your.key') // Never hardcode UI strings
+```
+
+### Branching
+- Always branch from `develop` (not master)
+- Use descriptive names: `feat/add-alerts`, `fix/login-error`
+- PRs target `develop` branch
+
+### Formatting
+- **Client**: Prettier with `printWidth: 90`, tabs, double quotes
+- **Server**: Prettier with `printWidth: 150`, tabs, double quotes
+- Both use ESLint with strict settings
+
+### Testing
+Server tests use Mocha + Chai + Sinon:
+```bash
+npm test # Run all tests with coverage
+npm test -- --grep "pattern" # Run specific tests
+```
+Test files: `server/tests/**/*.test.js`
+
+## Database Models
+
+Key Mongoose models in `/server/src/db/models/`:
+- **Monitor** - Monitoring configuration (website, infrastructure, port, etc.)
+- **Check** - Individual monitoring check results
+- **Incident** - Downtime incidents
+- **User** - User accounts
+- **Team** - Team/workspace management
+- **StatusPage** - Public status pages
+- **Notification** - Alert configuration (email, Discord, Slack, webhooks)
+- **MaintenanceWindow** - Scheduled maintenance periods
+- **AppSettings** - Global application settings
diff --git a/client/src/Components/v1/Host/index.jsx b/client/src/Components/v1/Host/index.jsx
index 935224994..6670722c1 100644
--- a/client/src/Components/v1/Host/index.jsx
+++ b/client/src/Components/v1/Host/index.jsx
@@ -23,12 +23,12 @@ const Host = ({ url, title, percentageColor, percentage, showURL, status }) => {
direction="row"
position="relative"
alignItems="center"
- gap={theme.spacing(5)}
+ gap={theme.spacing(4)}
>
{title}
@@ -38,9 +38,9 @@ const Host = ({ url, title, percentageColor, percentage, showURL, status }) => {
<>
diff --git a/client/src/Pages/Incidents2/Components/ActiveIncidentsPanel/index.jsx b/client/src/Pages/Incidents2/Components/ActiveIncidentsPanel/index.jsx
index e98c2a143..fda257695 100644
--- a/client/src/Pages/Incidents2/Components/ActiveIncidentsPanel/index.jsx
+++ b/client/src/Pages/Incidents2/Components/ActiveIncidentsPanel/index.jsx
@@ -42,25 +42,24 @@ const ActiveIncidentsPanel = ({ totalCount = 0, isLoading = false, error = null
if (!totalCount || totalCount === 0) {
return (
-
+
{t("incidentsPage.allSystemsAreOperational")}
@@ -88,21 +85,20 @@ const ActiveIncidentsPanel = ({ totalCount = 0, isLoading = false, error = null
}
return (
-
+
{totalCount}
-
- {t("incidentsPage.incidentsActivePanelTitle")}
-
);
diff --git a/client/src/Pages/Incidents2/Components/IncidentsSummaryPanel/index.jsx b/client/src/Pages/Incidents2/Components/IncidentsSummaryPanel/index.jsx
index 43071fa03..33ba21cd1 100644
--- a/client/src/Pages/Incidents2/Components/IncidentsSummaryPanel/index.jsx
+++ b/client/src/Pages/Incidents2/Components/IncidentsSummaryPanel/index.jsx
@@ -53,7 +53,7 @@ const IncidentsSummaryPanel = ({ updateTrigger }) => {
<>
{
const duration = calculateDuration();
const iconWrapperStyle = {
- px: theme.spacing(2),
display: "flex",
justifyContent: "center",
+ mx: theme.spacing(2),
color: theme.palette.primary.contrastTextTertiary,
+ "& svg": {
+ width: 18,
+ height: 18,
+ },
"& svg path": {
stroke: "currentColor",
+ strokeWidth: 1.5,
},
};
return (
-
+
+
+
-
-
-
-
-
-
- {t("incidentsPage.incidentItemMonitor")}:
-
-
- {incident.monitorName || t("incidentsPage.unknownMonitor")}
-
-
-
-
-
-
-
-
-
-
-
- {t("incidentsPage.incidentItemStatus")}:
-
-
-
-
-
-
-
-
-
- {duration}
-
-
+ {incident.monitorName || t("incidentsPage.unknownMonitor")}
+
+
+
+
+
+
+
+ {duration}
+
);
};
diff --git a/client/src/Pages/Incidents2/Components/LatestIncidentsPanel/index.jsx b/client/src/Pages/Incidents2/Components/LatestIncidentsPanel/index.jsx
index 21d2330c4..d24bcd0ef 100644
--- a/client/src/Pages/Incidents2/Components/LatestIncidentsPanel/index.jsx
+++ b/client/src/Pages/Incidents2/Components/LatestIncidentsPanel/index.jsx
@@ -5,6 +5,7 @@ import { useTranslation } from "react-i18next";
import PanelSkeleton from "../IncidentsSummaryPanel/skeleton.jsx";
import IncidentItem from "./IncidentItem.jsx";
import SummaryCard from "../SummaryCard/index.jsx";
+import Icon from "@/Components/v1/Icon";
/**
* LatestIncidentsPanel Component
@@ -51,21 +52,35 @@ const LatestIncidentsPanel = ({ incidents = [], isLoading = false, error = null
return (
{!incidents || incidents.length === 0 ? (
-
+
+
+
+
{t("incidentsPage.incidentsLatestPanelEmpty")}
-
+
) : (
{incidents.map((incident, index) => (
diff --git a/client/src/Pages/Incidents2/Components/StatisticsPanel/index.jsx b/client/src/Pages/Incidents2/Components/StatisticsPanel/index.jsx
index 4aeffb80f..7f07f688f 100644
--- a/client/src/Pages/Incidents2/Components/StatisticsPanel/index.jsx
+++ b/client/src/Pages/Incidents2/Components/StatisticsPanel/index.jsx
@@ -1,9 +1,8 @@
import PropTypes from "prop-types";
-import { Box, Stack, Typography } from "@mui/material";
+import { Box, Stack, Typography, Divider } from "@mui/material";
import { useTheme } from "@emotion/react";
import PanelSkeleton from "../IncidentsSummaryPanel/skeleton.jsx";
import { useTranslation } from "react-i18next";
-import { Divider } from "@mui/material";
import Icon from "@/Components/v1/Icon";
import SummaryCard from "../SummaryCard/index.jsx";
@@ -32,18 +31,19 @@ const StatisticsPanel = ({ isLoading = false, error = null, summary = {} }) => {
const { t } = useTranslation();
const totalResolutions =
(summary?.totalManualResolutions || 0) + (summary?.totalAutomaticResolutions || 0);
- const automaticPercentage =
- totalResolutions > 0
- ? (summary?.totalAutomaticResolutions / totalResolutions) * 100
- : 0;
const iconWrapperStyle = {
display: "flex",
justifyContent: "center",
- mx: theme.spacing(3),
+ mx: theme.spacing(2),
color: theme.palette.primary.contrastTextTertiary,
+ "& svg": {
+ width: 18,
+ height: 18,
+ },
"& svg path": {
stroke: "currentColor",
+ strokeWidth: 1.5,
},
};
if (isLoading) {
@@ -73,162 +73,87 @@ const StatisticsPanel = ({ isLoading = false, error = null, summary = {} }) => {
);
}
+ const getMostAffectedMonitor = () => {
+ if (!summary.total || summary.total === 0) {
+ return t("incidentsPage.none");
+ }
+ return summary.topMonitor?.monitorName || t("incidentsPage.none");
+ };
+
+ const rowStyle = {
+ py: theme.spacing(0.5),
+ minHeight: 32,
+ };
+
return (
-
-
-
-
-
-
- {t("incidentsPage.totalIncidents")} : {summary.total || 0}
-
-
-
-
-
-
-
-
-
-
- {t("incidentsPage.mostAffectedMonitor")} :{" "}
- {summary.topMonitor?.monitorName || t("incidentsPage.unknownMonitor")}
-
-
-
-
-
-
-
-
-
-
- {t("incidentsPage.avgResolutionTime")} :{" "}
- {summary.avgResolutionTimeHours || 0} {t("incidentsPage.hours")}
-
-
-
-
-
-
+
-
-
-
- {t("incidentsPage.resolutions")}: {totalResolutions}
-
-
+
+ {t("incidentsPage.totalIncidents")}: {summary.total || 0}
+
-
-
+
+
+
-
+
+
+
+ {t("incidentsPage.mostAffectedMonitor")}: {getMostAffectedMonitor()}
+
+
+
+
+
+
+
-
- {summary.totalAutomaticResolutions > 0 && (
-
- {t("incidentsPage.automatic")} ({summary.totalAutomaticResolutions})
-
- )}
- {summary.totalManualResolutions > 0 && (
-
- {t("incidentsPage.manual")} ({summary.totalManualResolutions})
-
- )}
-
-
+ {t("incidentsPage.avgResolutionTime")}:{" "}
+ {summary.total > 0
+ ? `${summary.avgResolutionTimeHours || 0} ${t("incidentsPage.hours")}`
+ : "N/A"}
+
+
);
diff --git a/client/src/Pages/Incidents2/Components/SummaryCard/index.jsx b/client/src/Pages/Incidents2/Components/SummaryCard/index.jsx
index cb895dc50..3a8589dc1 100644
--- a/client/src/Pages/Incidents2/Components/SummaryCard/index.jsx
+++ b/client/src/Pages/Incidents2/Components/SummaryCard/index.jsx
@@ -8,17 +8,14 @@ const SummaryCard = ({ children, isHighPriority = false, sx = {}, title = null }
{title}
@@ -49,9 +45,9 @@ const SummaryCard = ({ children, isHighPriority = false, sx = {}, title = null }
)}
{children}
diff --git a/client/src/Pages/Logs/Diagnostics/components/gauges/index.jsx b/client/src/Pages/Logs/Diagnostics/components/gauges/index.jsx
index b38837cf2..be6dc99aa 100644
--- a/client/src/Pages/Logs/Diagnostics/components/gauges/index.jsx
+++ b/client/src/Pages/Logs/Diagnostics/components/gauges/index.jsx
@@ -12,13 +12,16 @@ import { Box } from "@mui/material";
const BaseContainer = ({children}) => {
const theme = useTheme()
return(
-
{children}
@@ -99,7 +102,7 @@ const Gauges = ({ diagnostics, isLoading }) => {
return (
{
+const LevelBadge = ({ level }) => {
+ const theme = useTheme();
+
const levelColors = {
info: theme.palette.success.main,
warn: theme.palette.warning.main,
@@ -17,31 +20,38 @@ const formatLog = (theme, log, idx) => {
debug: theme.palette.accent.main,
};
- const color = levelColors[log.level] || theme.palette.primary.contrastText;
+ const color = levelColors[level] || theme.palette.primary.contrastText;
return (
-
- [{log.timestamp}]{" "}
- {log.level.toUpperCase()}
- {": "}
- {`(${log.service})`}
- {`(${log.method})`}
- {": "}
- {log.message}
-
-
+
+ {level}
+
);
};
-const Logs = () => {
- // Local state
- const [logLevel, setLogLevel] = useState("all");
+const formatTimestamp = (timestamp) => {
+ if (!timestamp) return "-";
+ const date = new Date(timestamp);
+ return date.toLocaleString();
+};
+
+const Logs = () => {
+ const [logLevel, setLogLevel] = useState("all");
+ const [page, setPage] = useState(0);
+ const [rowsPerPage, setRowsPerPage] = useState(15);
- // Hooks
const theme = useTheme();
const { t } = useTranslation();
const [logs, isLoading, error] = useFetchLogs();
- // Setup
+
const LOG_LEVELS = [
{ _id: "all", name: t("logsPage.logLevelSelect.values.all") },
{ _id: "info", name: t("logsPage.logLevelSelect.values.info") },
@@ -49,52 +59,116 @@ const Logs = () => {
{ _id: "error", name: t("logsPage.logLevelSelect.values.error") },
{ _id: "debug", name: t("logsPage.logLevelSelect.values.debug") },
];
+
+ const headers = [
+ {
+ id: "timestamp",
+ content: t("logsPage.table.timestamp"),
+ render: (row) => (
+
+ {formatTimestamp(row.timestamp)}
+
+ ),
+ },
+ {
+ id: "level",
+ content: t("logsPage.table.level"),
+ render: (row) => ,
+ },
+ {
+ id: "service",
+ content: t("logsPage.table.service"),
+ render: (row) => (
+ {row.service || "-"}
+ ),
+ },
+ {
+ id: "method",
+ content: t("logsPage.table.method"),
+ render: (row) => (
+ {row.method || "-"}
+ ),
+ },
+ {
+ id: "message",
+ content: t("logsPage.table.message"),
+ render: (row) => (
+
+ {row.message || "-"}
+
+ ),
+ },
+ ];
+
+ const filteredLogs = logs
+ ?.filter((log) => {
+ if (logLevel === "all") return true;
+ return log.level === logLevel;
+ })
+ .reverse()
+ .map((log, idx) => ({ ...log, id: idx }));
+
+ const paginatedLogs = filteredLogs?.slice(
+ page * rowsPerPage,
+ page * rowsPerPage + rowsPerPage
+ );
+
+ const handleChangePage = (event, newPage) => {
+ setPage(newPage);
+ };
+
+ const handleChangeRowsPerPage = (event) => {
+ setRowsPerPage(parseInt(event.target.value, 10));
+ setPage(0);
+ };
+
+ const handleLogLevelChange = (e) => {
+ setLogLevel(e.target.value);
+ setPage(0);
+ };
+
return (
{t("logsPage.logLevelSelect.title")}
-
-
- {logs
- ?.filter((log) => {
- if (logLevel === "all") return true;
- return log.level === logLevel;
- })
- .reverse()
- .map((log, idx) => formatLog(theme, log, idx))}
-
-
+ />
+
+ {filteredLogs?.length > 0 && (
+
+ )}
);
};
diff --git a/client/src/locales/en.json b/client/src/locales/en.json
index a658d6d66..e59bf1615 100644
--- a/client/src/locales/en.json
+++ b/client/src/locales/en.json
@@ -791,7 +791,16 @@
"error": "Error",
"debug": "Debug"
}
- }
+ },
+ "table": {
+ "timestamp": "Timestamp",
+ "level": "Level",
+ "service": "Service",
+ "method": "Method",
+ "message": "Message",
+ "logs": "logs"
+ },
+ "noLogs": "No logs found"
},
"queuePage": {
"title": "Queue",
@@ -1195,6 +1204,7 @@
"incidentsOptionsHeaderFilterResolved": "Resolved",
"incidentsTableResolved": "Closed",
"incidentsTableActionResolveManually": "Resolve Manually",
- "hours": "hours"
+ "hours": "hours",
+ "none": "None"
}
}