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/package-lock.json b/client/package-lock.json
index e917619ae..7f5397b7f 100644
--- a/client/package-lock.json
+++ b/client/package-lock.json
@@ -12,7 +12,6 @@
"@emotion/styled": "^11.13.0",
"@hello-pangea/dnd": "^18.0.0",
"@hookform/resolvers": "5.2.2",
- "@mui/icons-material": "6.4.11",
"@mui/lab": "6.0.0-dev.240424162023-9968b4889d",
"@mui/material": "6.4.11",
"@mui/x-charts": "^7.5.1",
@@ -25,6 +24,7 @@
"human-interval": "2.0.1",
"i18next": "25.4.2",
"joi": "17.13.3",
+ "lucide-react": "^0.562.0",
"mui-color-input": "^6.0.0",
"pretty-ms": "9.3.0",
"react": "18.3.1",
@@ -1189,32 +1189,6 @@
"url": "https://opencollective.com/mui-org"
}
},
- "node_modules/@mui/icons-material": {
- "version": "6.4.11",
- "resolved": "https://registry.npmjs.org/@mui/icons-material/-/icons-material-6.4.11.tgz",
- "integrity": "sha512-+jjJGIrB1awNbMv4ZVPPdN/p7O1UKFZ+xqRvNIQ8B1KnlID5hPMPBLM6UUbRF4bu3UDCbu79rn9Nye5LGNzmeA==",
- "license": "MIT",
- "dependencies": {
- "@babel/runtime": "^7.26.0"
- },
- "engines": {
- "node": ">=14.0.0"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/mui-org"
- },
- "peerDependencies": {
- "@mui/material": "^6.4.11",
- "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0",
- "react": "^17.0.0 || ^18.0.0 || ^19.0.0"
- },
- "peerDependenciesMeta": {
- "@types/react": {
- "optional": true
- }
- }
- },
"node_modules/@mui/lab": {
"version": "6.0.0-dev.240424162023-9968b4889d",
"resolved": "https://registry.npmjs.org/@mui/lab/-/lab-6.0.0-dev.240424162023-9968b4889d.tgz",
@@ -2050,9 +2024,9 @@
}
},
"node_modules/@remix-run/router": {
- "version": "1.23.0",
- "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.23.0.tgz",
- "integrity": "sha512-O3rHJzAQKamUz1fvE0Qaw0xSFqsA/yafi2iqeE0pvdFtCO1viYx8QL6f3Ln/aCCTLxs68SLf0KPM9eSeM8yBnA==",
+ "version": "1.23.2",
+ "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.23.2.tgz",
+ "integrity": "sha512-Ic6m2U/rMjTkhERIa/0ZtXJP17QUi2CbWE7cqx4J58M8aA3QTfW+2UlQ4psvTX9IO1RfNVhK3pcpdjej7L+t2w==",
"license": "MIT",
"engines": {
"node": ">=14.0.0"
@@ -5297,9 +5271,9 @@
"license": "MIT"
},
"node_modules/js-yaml": {
- "version": "4.1.0",
- "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz",
- "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==",
+ "version": "4.1.1",
+ "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz",
+ "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==",
"license": "MIT",
"dependencies": {
"argparse": "^2.0.1"
@@ -5464,6 +5438,15 @@
"yallist": "^3.0.2"
}
},
+ "node_modules/lucide-react": {
+ "version": "0.562.0",
+ "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.562.0.tgz",
+ "integrity": "sha512-82hOAu7y0dbVuFfmO4bYF1XEwYk/mEbM5E+b1jgci/udUBEE/R7LF5Ip0CCEmXe8AybRM8L+04eP+LGZeDvkiw==",
+ "license": "ISC",
+ "peerDependencies": {
+ "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0"
+ }
+ },
"node_modules/math-intrinsics": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
@@ -6129,12 +6112,12 @@
}
},
"node_modules/react-router": {
- "version": "6.30.1",
- "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.30.1.tgz",
- "integrity": "sha512-X1m21aEmxGXqENEPG3T6u0Th7g0aS4ZmoNynhbs+Cn+q+QGTLt+d5IQ2bHAXKzKcxGJjxACpVbnYQSCRcfxHlQ==",
+ "version": "6.30.3",
+ "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.30.3.tgz",
+ "integrity": "sha512-XRnlbKMTmktBkjCLE8/XcZFlnHvr2Ltdr1eJX4idL55/9BbORzyZEaIkBFDhFGCEWBBItsVrDxwx3gnisMitdw==",
"license": "MIT",
"dependencies": {
- "@remix-run/router": "1.23.0"
+ "@remix-run/router": "1.23.2"
},
"engines": {
"node": ">=14.0.0"
@@ -6144,13 +6127,13 @@
}
},
"node_modules/react-router-dom": {
- "version": "6.30.1",
- "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.30.1.tgz",
- "integrity": "sha512-llKsgOkZdbPU1Eg3zK8lCn+sjD9wMRZZPuzmdWWX5SUs8OFkN5HnFVC0u5KMeMaC9aoancFI/KoLuKPqN+hxHw==",
+ "version": "6.30.3",
+ "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.30.3.tgz",
+ "integrity": "sha512-pxPcv1AczD4vso7G4Z3TKcvlxK7g7TNt3/FNGMhfqyntocvYKj+GCatfigGDjbLozC4baguJ0ReCigoDJXb0ag==",
"license": "MIT",
"dependencies": {
- "@remix-run/router": "1.23.0",
- "react-router": "6.30.1"
+ "@remix-run/router": "1.23.2",
+ "react-router": "6.30.3"
},
"engines": {
"node": ">=14.0.0"
diff --git a/client/package.json b/client/package.json
index 376671942..50fafb35c 100644
--- a/client/package.json
+++ b/client/package.json
@@ -17,7 +17,6 @@
"@emotion/styled": "^11.13.0",
"@hello-pangea/dnd": "^18.0.0",
"@hookform/resolvers": "5.2.2",
- "@mui/icons-material": "6.4.11",
"@mui/lab": "6.0.0-dev.240424162023-9968b4889d",
"@mui/material": "6.4.11",
"@mui/x-charts": "^7.5.1",
@@ -30,6 +29,7 @@
"human-interval": "2.0.1",
"i18next": "25.4.2",
"joi": "17.13.3",
+ "lucide-react": "^0.562.0",
"mui-color-input": "^6.0.0",
"pretty-ms": "9.3.0",
"react": "18.3.1",
diff --git a/client/src/Components/LanguageSelector.jsx b/client/src/Components/LanguageSelector.jsx
index 9b46204e3..fdda73f4a 100644
--- a/client/src/Components/LanguageSelector.jsx
+++ b/client/src/Components/LanguageSelector.jsx
@@ -16,7 +16,7 @@ const langMap = {
const LanguageSelector = () => {
const { i18n } = useTranslation();
const theme = useTheme();
- const { language } = useSelector((state) => state.ui);
+ const { language = "en" } = useSelector((state) => state.ui);
const dispatch = useDispatch();
const handleChange = (event) => {
const newLang = event.target.value;
diff --git a/client/src/Components/v1/ActionsMenu/index.jsx b/client/src/Components/v1/ActionsMenu/index.jsx
index 5cd0dfa5b..ea0d029b8 100644
--- a/client/src/Components/v1/ActionsMenu/index.jsx
+++ b/client/src/Components/v1/ActionsMenu/index.jsx
@@ -2,7 +2,7 @@
import IconButton from "@mui/material/IconButton";
import Menu from "@mui/material/Menu";
import MenuItem from "@mui/material/MenuItem";
-import Settings from "../../../assets/icons/settings-bold.svg?react";
+import Icon from "../Icon";
import Dialog from "../Dialog/index.jsx";
// Utils
@@ -32,7 +32,6 @@ const ActionsMenu = ({
event.preventDefault();
event.stopPropagation();
let monitor = { id: actions.id };
- console.log(monitor);
await deleteMonitor({ monitor });
updateRowCallback();
};
@@ -84,7 +83,10 @@ const ActionsMenu = ({
},
}}
>
-
+
,
- error: ,
- warning: ,
+ info: (
+
+ ),
+ error: (
+
+ ),
+ warning: (
+
+ ),
};
/**
@@ -97,10 +109,9 @@ const Alert = ({ variant, title, body, isToast, hasIcon = true, onClick }) => {
},
}}
>
-
)}
diff --git a/client/src/Components/v1/ArrowLeft/index.jsx b/client/src/Components/v1/ArrowLeft/index.jsx
index a327d6bb7..e18a7f2d5 100644
--- a/client/src/Components/v1/ArrowLeft/index.jsx
+++ b/client/src/Components/v1/ArrowLeft/index.jsx
@@ -1,27 +1,28 @@
-import LeftArrow from "../../../assets/icons/left-arrow.svg?react";
-import LeftArrowDouble from "../../../assets/icons/left-arrow-double.svg?react";
-import LeftArrowLong from "../../../assets/icons/left-arrow-long.svg?react";
+import Icon from "../Icon";
import PropTypes from "prop-types";
const ArrowLeft = ({ type, color = "#667085", ...props }) => {
if (type === "double") {
return (
-
);
} else if (type === "long") {
return (
-
);
} else {
return (
-
);
diff --git a/client/src/Components/v1/ArrowRight/index.jsx b/client/src/Components/v1/ArrowRight/index.jsx
index 08a2639b0..3ae64a580 100644
--- a/client/src/Components/v1/ArrowRight/index.jsx
+++ b/client/src/Components/v1/ArrowRight/index.jsx
@@ -1,19 +1,20 @@
-import RightArrow from "../../../assets/icons/right-arrow.svg?react";
-import RightArrowDouble from "../../../assets/icons/right-arrow-double.svg?react";
+import Icon from "../Icon";
import PropTypes from "prop-types";
const ArrowRight = ({ type, color = "#667085", ...props }) => {
if (type === "double") {
return (
-
);
} else {
return (
-
);
diff --git a/client/src/Components/v1/Check/Check.jsx b/client/src/Components/v1/Check/Check.jsx
index 53971316b..cc3749fda 100644
--- a/client/src/Components/v1/Check/Check.jsx
+++ b/client/src/Components/v1/Check/Check.jsx
@@ -1,7 +1,6 @@
import "./check.css";
import PropTypes from "prop-types";
-import CheckGrey from "../../../assets/icons/check.svg?react";
-import CheckOutlined from "../../../assets/icons/check-outlined.svg?react";
+import Icon from "../Icon";
import { Box, Stack, Typography } from "@mui/material";
import { useTheme } from "@emotion/react";
@@ -35,16 +34,16 @@ const Check = ({ text, noHighlightText, variant = "info", outlined = false }) =>
alignItems="center"
>
{outlined ? (
-
+
) : (
- path": { fill: colors[variant] },
- }}
- >
-
-
+
)}
{
aria-label="Menu button"
onClick={toggleDrawer(true)}
>
-
+
{
}}
>
-
+
diff --git a/client/src/Components/v1/FilterHeader/index.jsx b/client/src/Components/v1/FilterHeader/index.jsx
index 36dd04081..de2c7f0e9 100644
--- a/client/src/Components/v1/FilterHeader/index.jsx
+++ b/client/src/Components/v1/FilterHeader/index.jsx
@@ -1,7 +1,7 @@
import { Checkbox, FormControl, ListItemText, MenuItem, Select } from "@mui/material";
import { useTheme } from "@emotion/react";
import PropTypes from "prop-types";
-import AddCircleOutlineIcon from "@mui/icons-material/AddCircleOutline";
+import Icon from "../Icon";
/**
* A reusable filter header component that displays a dropdown menu with selectable options.
@@ -29,9 +29,10 @@ const FilterHeader = ({ header, options, value, onChange, multiple = true }) =>
);
diff --git a/client/src/Pages/Infrastructure/Monitors/Components/Filters/index.jsx b/client/src/Pages/Infrastructure/Monitors/Components/Filters/index.jsx
index 70bf502af..01e56c68a 100644
--- a/client/src/Pages/Infrastructure/Monitors/Components/Filters/index.jsx
+++ b/client/src/Pages/Infrastructure/Monitors/Components/Filters/index.jsx
@@ -3,7 +3,7 @@ import PropTypes from "prop-types";
import FilterHeader from "@/Components/v1/FilterHeader/index.jsx";
import { useMemo } from "react";
import { Box, Button } from "@mui/material";
-import ClearIcon from "@mui/icons-material/Clear";
+import Icon from "@/Components/v1/Icon";
import { useTranslation } from "react-i18next";
/**
@@ -68,7 +68,12 @@ const Filter = ({
color={theme.palette.primary.contrastText}
onClick={handleReset}
variant="contained"
- endIcon={}
+ endIcon={
+
+ }
sx={{
visibility: isFilterActive ? "visible" : "hidden",
}}
diff --git a/client/src/Pages/Infrastructure/Monitors/Components/MonitorsTable/index.jsx b/client/src/Pages/Infrastructure/Monitors/Components/MonitorsTable/index.jsx
index 5f0360312..292eb9714 100644
--- a/client/src/Pages/Infrastructure/Monitors/Components/MonitorsTable/index.jsx
+++ b/client/src/Pages/Infrastructure/Monitors/Components/MonitorsTable/index.jsx
@@ -7,7 +7,7 @@ import { Stack } from "@mui/material";
import { InfrastructureMenu } from "../MonitorsTableMenu/index.jsx";
import LoadingSpinner from "../../../../Uptime/Monitors/Components/LoadingSpinner/index.jsx";
// Assets
-import CPUChipIcon from "@/assets/icons/cpu-chip.svg?react";
+import Icon from "@/Components/v1/Icon";
import CustomGauge from "@/Components/v1/Charts/CustomGauge/index.jsx";
// Utils
@@ -67,9 +67,9 @@ const MonitorsTable = ({
alignItems={"center"}
gap=".25rem"
>
-
{row.processor}
diff --git a/client/src/Pages/Infrastructure/Monitors/Components/MonitorsTableMenu/index.jsx b/client/src/Pages/Infrastructure/Monitors/Components/MonitorsTableMenu/index.jsx
index 39aa84e90..50af0aaa2 100644
--- a/client/src/Pages/Infrastructure/Monitors/Components/MonitorsTableMenu/index.jsx
+++ b/client/src/Pages/Infrastructure/Monitors/Components/MonitorsTableMenu/index.jsx
@@ -5,7 +5,7 @@ import { useTheme } from "@emotion/react";
import { useNavigate } from "react-router-dom";
import { createToast } from "@/Utils/toastUtils.jsx";
import { IconButton, Menu, MenuItem } from "@mui/material";
-import Settings from "@/assets/icons/settings-bold.svg?react";
+import Icon from "@/Components/v1/Icon";
import PropTypes from "prop-types";
import Dialog from "@/Components/v1/Dialog/index.jsx";
import { networkService } from "@/Utils/NetworkService.js";
@@ -100,7 +100,10 @@ const InfrastructureMenu = ({ monitor, isAdmin, updateCallback }) => {
}}
ref={anchor}
>
-
+