## File: docs/hosting/checkmate-so/README.md

# checkmate.so hosting (demo + staging)

Config for BlueWave's hosted instances at `demo.checkmate.so` and
`staging.checkmate.so`: one host, one nginx gateway terminating TLS, two
independent Checkmate stacks running the mono image
(`ghcr.io/bluewave-labs/checkmate`), plus a shared
[Capture](https://github.com/bluewave-labs/capture) agent. It replaces the old
`docker/unified/` setup (nginx client images + separate backend images + a
republished Mongo image) and doubles as a worked example of running the mono
image behind a TLS reverse proxy.

## How deploys work

`.github/workflows/build-image.yml` builds and pushes the image, then SSHes in
and runs `docker compose pull <service> && docker compose up -d <service>` in
this directory:

- merge to `develop` → image tagged `:develop` → `checkmate-staging` recreated
- merge to `master` → image tagged `:master` → `checkmate-demo` (prod) recreated

The host keeps a git checkout of this repo (on `develop`); the deploy does
`git pull` first so compose changes land with the code. Note the deploy only
recreates the checkmate service — changes to `gateway/nginx.conf` or the mongo
services need a manual `docker compose up -d` / gateway restart on the host.

## Host-local files (not committed)

Kept next to this compose file on the host:

- `server-prod.env` / `server-staging.env` — Checkmate server env
  (`DB_CONNECTION_STRING`, `JWT_SECRET`, `CLIENT_HOST=https://demo.checkmate.so`
  etc.). The old `UPTIME_APP_*` client vars are no-ops now and can be removed;
  if a client override is ever needed, use the `CLIENT_CONFIG_*` vars (see the
  Configuration section of the root README).
- `mongo-prod.env` / `mongo-staging.env` — Mongo credentials.
- `mongo/data-prod` / `mongo/data-staging` — Mongo data directories.

TLS certs live in `/etc/letsencrypt` (see `certbot-compose.yaml`; certbot
answers ACME challenges on port 8080 behind the gateway).

## One-time migration from docker/unified

1. `cd ~/checkmate && git pull` (must be on a branch containing this directory).
2. Stop the old stack: `cd docker/unified && docker compose down`.
3. Move host-local files here:
   `mv docker/unified/{server-prod.env,server-staging.env,mongo-prod.env,mongo-staging.env} docs/hosting/checkmate-so/`
   `mv docker/unified/mongo docs/hosting/checkmate-so/mongo`
4. **Check the Mongo data version before first start.** The old
   `checkmate:mongo-*` images were built `FROM mongo` (floating latest), so the
   data files follow whatever version was current at their last image build. Run
   `db.adminCommand({ getParameter: 1, featureCompatibilityVersion: 1 })`
   against the old container first and make the compose's `mongo:` tag match
   (checked 2026-07-09: both stacks report FCV 8.2, hence the `mongo:8.2` pin
   here). A lower pin than the data's FCV will refuse to start; a higher one
   should step up one release at a time.
5. `cd docs/hosting/checkmate-so && docker compose up -d`.

---

## File: docs/coding-conventions.md

# Coding conventions

Style and structural rules the codebase follows. Frontend rules are extracted from review feedback on PR series #3591–#3596; backend rules are extracted from the provider/repository/service layering used across `server/src/service/infrastructure/network/*` and the patterns enforced in code review. These are non-negotiable for new code. They exist to keep the codebase greppable, maintainable, and consistent.

> If you're touching a file in `client/src` the **frontend** rules apply.
> If you're touching a file in `server/src` the **backend** rules apply.
> Rule 0 applies everywhere.

---

## 0. Look at existing implementations and follow the established pattern

Before writing new code, find one or two files that already do something similar — a peer provider for a new monitor type, a peer page for a new admin screen, a peer repository method for a new query — and follow the same shape. Match imports, helper-function placement, naming, error handling, mock-construction style, and assertion idioms. If you're tempted to do it differently, the burden is on you to justify the divergence.

**Why:** consistency is more valuable than locally-optimal cleverness. A reader who has internalized the pattern of one peer file should be able to read the new one with no extra mental load. New shapes also make refactors more expensive — when someone wants to change the convention later, every variant has to be visited and reasoned about separately.

**How to apply:** before writing a new component, page, provider, repository, or test file:

1. Grep for the closest existing peers (e.g. `WebSocketProvider.ts` if you're writing a new monitor provider).
2. Read at least two of them end-to-end.
3. Mirror their structure: same constants at the top (`SERVICE_NAME`, `TIMEOUT_MS`), same import order, same DI shape, same try/catch envelope, same test-file location and helper imports.
4. If your new code needs a shape no peer has, name it explicitly in the PR description and link to the peer you diverged from. Don't sneak novelty in.

**Concrete check before opening a PR:** can a reader open a peer file side-by-side with yours and trace each section in the same order? If not, restructure.

---

# Part 1 — Frontend (`client/src`)

## 1. Prefer MUI native (system) props over `sx`

MUI components accept layout, spacing, color, border, typography and sizing values as top-level props. Use those. Reach for `sx` only when no native prop exists for the value you need (e.g. `textDecoration`, pseudo-selectors, complex selectors, conditional CSS).

**Why:** native props are easier to read in JSX, easier to grep, easier to refactor, and don't force the reader to mentally parse a nested object literal for things that are conceptually one attribute. A `sx` block also can't be split-line-edited cleanly when most of its keys are simple primitives.

**How to apply:** any time you write `sx={{ … }}`, ask whether each key has a native equivalent. If yes, hoist it.

### Canonical mappings

| `sx` key | Native prop |
|---|---|
| `color` | `color` |
| `backgroundColor` | `bgcolor` |
| `borderRadius` | `borderRadius` |
| `border`, `borderTop`, … | `border`, `borderTop`, … |
| `width`, `height`, `minHeight`, `maxWidth` | same names as native |
| `p`, `pt`, `pb`, `px`, `py` | same names as native |
| `m`, `mt`, `mb`, `mx`, `my` | same names as native |
| `display` | `display` |
| `textAlign` | `textAlign` |
| `fontWeight`, `fontSize`, `lineHeight` | same names as native |
| `gap` (on Stack) | `gap` |

### Examples

```tsx
// BAD — everything inside sx
<Box
  sx={{
    width: "100%",
    p: theme.spacing(LAYOUT.MD),
    borderRadius: 1,
    border: `1px solid ${alpha(tone, 0.45)}`,
    backgroundColor: alpha(tone, 0.08),
    textAlign: "left",
  }}
/>

// GOOD — every key that has a native prop is hoisted
<Box
  width="100%"
  p={theme.spacing(LAYOUT.MD)}
  borderRadius={theme.shape.borderRadius}
  border={`1px solid ${alpha(tone, 0.45)}`}
  bgcolor={alpha(tone, 0.08)}
  textAlign="left"
/>
```

```tsx
// BAD
<Typography sx={{ color: theme.palette.text.primary, lineHeight: 1.55, fontSize: 13 }}>

// GOOD — hoist what you can; drop fontSize:13 because it's the body default
<Typography
  color={theme.palette.text.primary}
  lineHeight={1.55}
>
```

### When `sx` is still correct

Keep `sx` for things that don't have a native prop:

```tsx
<Link
  component={RouterLink}
  to="/settings"
  color={theme.palette.primary.main}
  fontWeight={600}
  sx={{ textDecoration: "underline" }}   // OK — no native prop for textDecoration
/>
```

Pseudo-selectors (`"&:hover"`), `transition`, `cursor`, `outline`, `overflow` (when not on Box/Stack), media queries, and conditional spread objects all stay in `sx`.

---

## 2. Use the full theme path for colors, never the shorthand string

Prop forms like `color="text.secondary"`, `color="primary.main"`, `bgcolor="background.paper"` work, but they are **forbidden in this codebase**.

**Why:** the shorthand is invisible to grep. If a future change needs to find every place that consumes `theme.palette.text.secondary` — to retheme, to swap, to audit — the shorthand strings are missed. That is a maintenance trap. It's worth a few extra characters at every callsite to keep the codebase searchable.

**How to apply:** always destructure or reference the theme value explicitly.

```tsx
// BAD
<Typography color="text.secondary">…</Typography>
<Typography color="primary.main">…</Typography>

// GOOD
const theme = useTheme();
<Typography color={theme.palette.text.secondary}>…</Typography>
<Typography color={theme.palette.primary.main}>…</Typography>
```

This applies to every theme-aware string prop (`color`, `bgcolor`, `borderColor`, etc.), and inside `sx` if you really need it there.

---

## 3. No hardcoded numeric or string literals — use tokens

The theme exposes tokens for every kind of magic value. Use them. Reserve raw numbers for values that are genuinely one-off (and even then, push back on yourself first).

**Why:** tokens are the single source of truth. A literal `2` in a `mt` prop is invisible — it could mean `LAYOUT.XXS`, a rounding correction, or a thinko. The reader can't tell. With a token, intent is in the name.

**How to apply:** before writing a literal, check whether one of these tokens covers it. If it nearly does but not exactly, extend the token rather than hardcoding (Alex did exactly this — he added `LAYOUT.XXS = 2` rather than letting `mt: "2px"` stand).

### Token sources

- **Spacing / layout** — `client/src/Utils/Theme/constants.ts`
  - `LAYOUT.XXS = 2` · `LAYOUT.XS = 4` · `LAYOUT.SM = 6` · `LAYOUT.MD = 8` · `LAYOUT.LG = 10` · `LAYOUT.XL = 12` · `LAYOUT.XXL = 16`
  - `SPACING` for `theme.spacing(...)` multipliers
- **Typography sizes** — `client/src/Utils/Theme/Palette.ts` → `typographyLevels`
  - `xs / s / m / l / xl / xxl` (rem values)
- **Hover / interaction** — `HOVER.DARKEN` etc.
- **Shape** — `theme.shape.borderRadius` (don't write `borderRadius: 1` or `borderRadius: 4`)
- **Colors** — `theme.palette.*` (rule 2 above)

### Examples

```tsx
// BAD
<Box sx={{ mt: "2px" }} />
<Stack spacing={theme.spacing(2)} />
<Box sx={{ borderRadius: 1 }} />
<Typography fontSize={18} />
<Typography fontSize={13} />   // 13 is the body default — drop the prop entirely

// GOOD
<Box mt={LAYOUT.XXS} />
<Stack spacing={theme.spacing(LAYOUT.XXS)} />
<Box borderRadius={theme.shape.borderRadius} />
<Typography fontSize={typographyLevels.xl} />
<Typography />   // inherits 13px from theme
```

### Token naming gotcha

`typographyLevels.xl` was historically used for both 18px and 23px callsites. Don't reuse a token for two different purposes — split it (`xl` = 18, `xxl` = 23) and update every callsite. If you find an ambiguous token while editing, fix it the same way.

---

## 4. Use `useTheme()` inside components, don't import the theme module

The `theme` object exported from `@/Utils/Theme/Theme` is the *factory output*, not a live, mode-aware instance. Importing it directly couples a component to the wrong reference and breaks dark-mode reactivity. It also breaks the build at import time in some module-graph configurations (this happened in PR #3596 and required a follow-up commit).

**Why:** themes can change at runtime (mode toggle), and the hook is the only correct way to read the active theme. Imports also create circular-graph risk because `Theme.ts` itself depends on `Palette.ts`.

**How to apply:** inside any component or component-helper that needs theme values:

```tsx
// BAD
import { theme } from "@/Utils/Theme/Theme";
const SectionHeading = ({ children }) => (
  <Typography color={theme.palette.text.secondary}>{children}</Typography>
);

// GOOD
import { useTheme } from "@mui/material/styles";
const SectionHeading = ({ children }) => {
  const theme = useTheme();
  return <Typography color={theme.palette.text.secondary}>{children}</Typography>;
};
```

Module-level utility files that don't render React (pure helpers) should accept `theme` as a parameter rather than importing it.

---

## 5. Discriminated unions: pair the runtime list with the type

When you have a closed set of string literals that's used both as a type and as runtime data (severities, statuses, kinds), declare the runtime tuple first and derive the type from it. Don't declare them as two parallel literals that can drift.

**Why:** if the list and the type aren't tied to each other, adding a new value to one and forgetting the other compiles fine and breaks at runtime. Deriving the type from the tuple makes them inseparable.

```tsx
// BAD
type Severity = "info" | "warning" | "error";

// GOOD
export const Severities = ["info", "warning", "error"] as const;
export type Severity = (typeof Severities)[number];
```

Export both when callers need to iterate (e.g. for a Storybook stories matrix or a select dropdown).

---

# Part 2 — Backend (`server/src`)

## 6. Layering: Controller → Service → Repository → Mongoose

The backend enforces a strict three-layer separation between HTTP handling, business logic, and data access. Every new feature follows this path, no shortcuts.

```
Request → Controller → Service → Repository → MongoDB (Mongoose)
```

**Why:** this is what makes each layer independently testable. Mongoose calls in services force services to mock the database in tests; raw DB queries in controllers couple HTTP shape to schema shape and turn every refactor into a multi-file rewrite.

**How to apply:** when adding a new feature, the pattern is — **add a repository method** for any new DB query, **call it from a service**, **expose it via a controller**. If a service constructs a `MonitorModel.findOne({...})`, you've broken the rule; the query belongs in the repository.

- **Controllers** (`server/src/api/controllers/`) handle HTTP concerns only. Each method is annotated `Handler` (from `controllerUtils.ts`), parses the request with a Zod schema, calls the service with the parse result, and writes `res.json({ success, msg, data })`. `Handler` types the response body as the envelope, so any other shape is a compile error. No wrapper: Express 5 forwards a rejected promise to `handleErrors`. No business logic.
- **Services** (`server/src/service/business/`) contain all business logic. They throw `AppError` on validation/auth failures; they do not write directly to MongoDB.
- **Repositories** (`server/src/repositories/`) are the sole layer that talks to MongoDB. They expose clean query methods (`findByMonitorId`, `createCheck`, `updateStatusWindowAndChecks`) and return entities — never Mongoose documents — via a `toEntity` private helper.

---

## 7. Provider conventions for `service/infrastructure/network/*`

Every monitor-type provider implements `IStatusProvider<TPayload>` and follows the same skeleton.

**Why:** the registry in `services.ts` builds providers identically (`new XxxProvider(deps)`) and `NetworkService` invokes them identically (`provider.handle(monitor)`). If your provider's shape diverges, the registry/dispatch becomes asymmetric and `NetworkService` grows special cases.

**How to apply:** copy the structure of a recent peer (`WebSocketProvider`, `GrpcProvider`, `PortProvider`). The skeleton:

```ts
const SERVICE_NAME = "XxxProvider";
const TIMEOUT_MS = 10000;

type ClientCtor = typeof SomeNodeStdlibClass;

export class XxxProvider implements IStatusProvider<XxxStatusPayload> {
  readonly type = "xxx";

  constructor(private Client: ClientCtor) {}

  supports(type: MonitorType): boolean {
    return type === "xxx";
  }

  async handle(monitor: Monitor): Promise<MonitorStatusResponse<XxxStatusPayload>> {
    try {
      // 1. validate inputs → throw AppError(400) on bad input
      // 2. set up the client (use this.Client, never imported singleton)
      // 3. perform the call inside timeRequest(...), with a setTimeout race
      //    in the Promise body to enforce TIMEOUT_MS
      // 4. return MonitorStatusResponse with payload
    } catch (err: unknown) {
      if (err instanceof AppError) throw err;
      const originalMessage = err instanceof Error ? err.message : String(err);
      throw new AppError(internalError, {
        message: originalMessage || "Error performing xxx check",
        service: SERVICE_NAME,
        details: { /* monitor-shaped context */ },
      });
    }
  }
}
```

### Hard rules

- **DI for all external dependencies.** `typeof WebSocket`, `typeof net` — pass the constructor in via the provider's constructor. Never `import { Foo } from "some/library"` and call it inside `handle()`. This is the single thing that makes the provider unit-testable without `jest.unstable_mockModule`.
- **`SERVICE_NAME` and `TIMEOUT_MS`** as module-level constants near the top, not magic strings/numbers in the body.
- **Use the shared `timeRequest()` helper** from `service/infrastructure/network/utils.ts` to measure latency. Don't roll your own `process.hrtime()`.
- **Enforce a hard timeout.** Every peer provider inlines a `setTimeout` race in the Promise body around the underlying client call (see `WebSocketProvider.ts`); sockets, DNS resolvers, and external clients can hang indefinitely without it. There is no shared `withTimeout` helper today — match the inline pattern.
- **Outer try/catch** that converts unknown errors to `new AppError(internalError, { service, details })` while letting your own `AppError` pass through.

---

## 8. Mongoose schema fields with closed value sets

If a field's TypeScript type is a closed union (status, type, record type), the Mongoose schema must declare the same set as `enum`.

**Why:** the validator and the schema are independent gates. A service-layer Zod check can be bypassed (imports, migrations, direct repo writes). A schema `enum` is the last line of defense and surfaces invalid data as a Mongoose validation error rather than silently accepting `"badtype"` and breaking downstream consumers.

**How to apply:** when you add a string field whose values are constrained by a const tuple in `types/`, reuse the same const for the schema enum:

```ts
// types/monitor.ts (existing)
export const MonitorMatchMethods = ["equal", "include", "regex"] as const;

// db/models/Monitor.ts
{
  matchMethod: {
    type: String,
    enum: MonitorMatchMethods,
  },
}
```

The same applies to `MonitorTypes`, `MonitorStatuses`, and any new closed-set field.

---

## 9. Test conventions for `server/test/unit/providers/network/*`

Each provider gets a unit test file at `server/test/unit/providers/network/.test.ts`. Follow the structure used by `webSocketProvider.test.ts`, `grpcProvider.test.ts`, `portProvider.test.ts`.

**Why:** consistency lets reviewers diff a new test file against any peer and only see the actual logic differences, not framework/import noise. It also lets the `testStatusProviderContract` helper guarantee every provider satisfies the `IStatusProvider` contract.

**How to apply:**

- **Imports**: `from "@jest/globals"` (not the global `jest`), and `.ts` extensions on every relative import.
- **Contract test**: call `testStatusProviderContract("XxxProvider", { create, supportedType, unsupportedType, makeMonitor })` before the per-test `describe` block.
- **Mock construction**: build the constructor mock as `jest.fn().mockImplementation(() => mock) as unknown as typeof Client` (typed cast, not `as any`).
- **`makeMonitor` factory**: takes `Partial<Monitor>` overrides, spreads onto a baseline, returns `as Monitor`.
- **Inline `setup()` per test** rather than `beforeEach` shared state. Each test should construct its own mock + provider, so a single test can be read in isolation:

  ```ts
  const setup = () => {
    const mock = createMockClient();
    const Ctor = jest.fn().mockImplementation(() => mock) as unknown as typeof Client;
    const provider = new XxxProvider(Ctor);
    return { mock, provider };
  };

  it("does X", async () => {
    const { mock, provider } = setup();
    // ...
  });
  ```

- **Assertion style**: individual `expect(result.field).toBe(...)` lines or `expect(result).toEqual(expect.objectContaining({...}))`. Both are used; pick what reads cleanest for the case.
- **Test naming**: lowercase imperative ("returns success when connection opens", "uses the configured timeout", "throws AppError when host is missing").

---

## 10. Validation: centralize, don't inline

Whenever the same string-set or shape constraint appears in two or more validators, lift it into `types/` as a const tuple and `import` it into both validators. Don't inline `z.enum(["A", "AAAA", ...])` in three places.

**Why:** drift. Three inline copies will eventually disagree — someone adds a new value to one and forgets the other two. The validator silently accepts it on `POST /monitors` and rejects it on `PATCH`, leading to bug reports the author can't reproduce.

**How to apply:** keep validation source-of-truth in `server/src/types/*.ts` as paired const tuples + derived types (rule 5). The validators in `server/src/validation/*.ts` and the Mongoose schemas in `server/src/db/models/*.ts` import from `types/` and reuse the same tuple.

---

## 11. OpenAPI: entity schemas live in the domain

Each entity's shape is one Zod schema in `domain/<entity>/<entity>.schema.ts`. The TS type is `z.infer` of it in `<entity>.type.ts`, the route table references it as the `response`, and the OpenAPI spec is generated from it. Entities with a secret field (`User`, `Proxy`, `Monitor`) also export a `*ResponseSchema` that is `.omit()` of the entity schema and carries the component id. Shapes a route composes (lists, pages, envelopes) live in `api/validation/<entity>Validation.ts`.

**Why:** the spec is auto-generated. If the schema doesn't list a field, SDK consumers don't see it, even though the server returns it. One definition means the type, the mapper and the spec cannot drift from each other.

**How to apply:** when you add an entity field, update two files in the same PR:

1. `domain/<entity>/<entity>.schema.ts` — add the field. Required or optional follows what the repository's `toEntity` writes. If the field is a secret, add it to the response schema's `.omit()`.
2. `domain/<entity>/<entity>.model.ts` — add the Mongoose schema entry (with `enum` if the value set is closed; rule 8).

The repository's `toEntity` must write the new field; the build fails until it does. Zod outside `api/validation/` is for response shapes only: request parsing stays in the validators, and services never call `.parse` on a response schema.

## 12. Every `AppError` is built from a definition

Client errors are `{ status, description }` literals in `domain/<entity>/<entity>.errors.ts` (middleware errors in `api/middleware/middleware.errors.ts`, email errors in `service/email.errors.ts`), thrown as `new AppError(<entity>Errors.<name>, { service, message?, details? })` and listed by reference in the route table's `errors`. Server errors throw `internalError` from `utils/AppError.ts`. The OpenAPI spec is generated from the definitions, and `npm run test:route-errors` fails when a route omits a definition its handler can throw, or lists one it cannot.

**Why:** the status and the spec description come from the same value, so they cannot drift. A reviewer sees every error a route can return by reading its table entry.

**How to apply:** when you add a throw:

1. Add the definition to the entity's catalog if none fits. Reuse an existing one when the meaning is the same; `monitorErrors.notFound` is thrown from several repositories.
2. Throw it with `service` and `method`. Pass `message` only when it adds something the description cannot, such as an id. Otherwise the description is the message.
3. Add it to the `errors` array of every route entry whose handler can reach it, including entries in other domains' tables.
4. Run `npm run test:route-errors`.

Guard errors (validation 400, auth 401, role 403, upload 413 and 415, rate limit 429, and 500) are derived from the entry's fields by `registerRoutes` and are never listed.

---

# Pre-PR checklist

Before opening a PR, grep your diff for the patterns below. Each item describes the **anti-pattern** to find and fix; if your diff has none, that line passes. Frontend section applies to `client/src`; backend to `server/src`.

## Universal
- [ ] I can name the peer file I used as a template, and my file's structure mirrors it (rule 0).
- [ ] `npm run build` and `npm run format-check` are clean in both `client/` and `server/`.

## Frontend (`client/src`)
- [ ] No `sx={{ … }}` block containing keys with a native-prop equivalent (rule 1).
- [ ] No `color="text.X"`, `bgcolor="background.X"`, or other short-string palette paths (rule 2).
- [ ] No raw number or px-string in spacing / margin / padding / fontSize / borderRadius props that should be a token (rule 3).
- [ ] No `import { theme } from "@/Utils/Theme/Theme"` inside a component (rule 4).
- [ ] No `type X = "a" | "b" | "c"` for a closed set used at runtime (rule 5).

## Backend (`server/src`)
- [ ] No `MonitorModel.findOne` (or similar Mongoose call) outside `repositories/` (rule 6).
- [ ] No provider that imports its underlying client at module top instead of receiving it via constructor DI (rule 7).
- [ ] No provider missing `SERVICE_NAME` / `TIMEOUT_MS` / `AppError` outer catch / `timeRequest()` / inline timeout (rule 7).
- [ ] No closed-set string field on a Mongoose schema without an `enum` (rule 8).
- [ ] No provider test that uses `beforeEach` instead of inline `setup()` (rule 9).
- [ ] No duplicated `z.enum([...])` literal that should reference a `types/*` const tuple (rule 10).
- [ ] No new entity field missing from `domain/<entity>/<entity>.schema.ts` (rule 11).
- [ ] No `new AppError({ ... })` with an object as the first argument; every throw names a definition, `internalError` for 500s (rule 12).

A clean PR on these axes is a PR that ships without a normalize follow-up.

---

## File: docs/custom-ca-quick-reference.md

# Custom CA Trust - Quick Reference

## Fast Setup (Node-level)

1. **Export your CA certificate:**
   ```bash
   # For Smallstep
   step certificate inspect --format pem step-ca/root_ca.crt > docker/dev/certs/custom-ca.pem
   ```

2. **Use the provided example override:**
   ```bash
   docker-compose -f docker-compose.yaml -f docker-compose.custom-ca-example.yaml up -d
   ```

3. **Or manually update docker-compose.yaml:**
   ```yaml
   services:
     server:
       environment:
         NODE_EXTRA_CA_CERTS: /certs/custom-ca.pem
       volumes:
         - ./certs:/certs:ro
   ```

## Alternative Setup (OS-level)

1. **Use custom Dockerfile:**
   ```bash
   docker-compose -f docker-compose.yaml -f docker-compose.custom-ca.yaml up
   ```

## File Structure
```
docker/dev/
├── docker-compose.yaml
├── docker-compose.custom-ca-example.yaml  # Example config
├── certs/
│   ├── README.md
│   └── custom-ca.pem                     # Your CA certificate
└── export-smallstep-ca.sh                # Helper script
```

## Environment Variables
- `NODE_EXTRA_CA_CERTS=/certs/custom-ca.pem` - Node.js trust
- Mount `./certs:/certs:ro` - Volume mount

## Security
- ✅ Only trust CAs you control
- ✅ Use read-only volume mounts
- ✅ Keep certificates out of version control
- ❌ Never trust untrusted CAs

## Troubleshooting
- Check container logs: `docker-compose logs server`
- Verify certificate: `docker exec -it <container> ls -la /certs/`
- Test connection: `docker exec -it <container> wget --ca-certificate=/certs/custom-ca.pem https://your-internal-site.com`

## Full Documentation
See [Custom CA Trust Guide](./custom-ca-trust.md) for detailed instructions.

---

## File: docs/custom-ca-trust.md

# Custom Certificate Authority Trust

This guide explains how to configure Checkmate to trust custom Certificate Authorities (CAs) when running in Docker, particularly useful for internal/private CAs like Smallstep.

## Understanding Certificate Authorities

Certificate Authorities (CAs) are entities that issue and manage digital certificates. While public CAs (like Let's Encrypt, DigiCert) are trusted by default in most systems, private or internal CAs (like those issued by Smallstep, internal PKI systems) require explicit trust configuration.

When Checkmate monitors HTTPS endpoints with certificates from private CAs, it may show them as "DOWN" due to certificate validation failures, even if the service is actually accessible.

## Node-level Trust Approach

The simplest approach is to mount your custom CA certificate and configure Node.js to trust it using the `NODE_EXTRA_CA_CERTS` environment variable.

### Docker Compose Configuration

Add a volume mount for your CA certificate and set the environment variable:

```yaml
services:
  checkmate:
    image: ghcr.io/bluewave-labs/checkmate:latest
    restart: always
    ports:
      - "52345:52345"
    env_file:
      - server.env
    environment:
      NODE_EXTRA_CA_CERTS: /certs/custom-ca.pem
    volumes:
      - ./certs:/certs:ro
    depends_on:
      - mongodb
```

### Directory Structure

Create a `certs` directory in your Docker Compose project root:

```
docker/dev/
├── docker-compose.yaml
├── certs/
│   └── custom-ca.pem
└── ...
```

## OS-level Trust Approach (Debian-based)

For more comprehensive trust configuration, you can create a derived Dockerfile that installs your CA at the OS level.

### Custom Dockerfile

Create `checkmate-custom-ca.Dockerfile`:

```dockerfile
FROM ghcr.io/bluewave-labs/checkmate:latest

USER root

RUN apt-get update \
    && apt-get install -y ca-certificates \
    && rm -rf /var/lib/apt/lists/*

# Copy your custom CA certificate (must have a .crt extension)
COPY ./certs/custom-ca.crt /usr/local/share/ca-certificates/

# Update CA certificates
RUN update-ca-certificates

USER node

# Node uses its bundled CA store by default; make it read the system store
ENV NODE_OPTIONS="--use-system-ca"
```

### Docker Compose Override

Create `docker-compose.custom-ca.yaml`:

```yaml
services:
  checkmate:
    build:
      context: .
      dockerfile: checkmate-custom-ca.Dockerfile
    restart: always
    ports:
      - "52345:52345"
    env_file:
      - server.env
    depends_on:
      - mongodb
```

Run with: `docker compose -f docker-compose.yaml -f docker-compose.custom-ca.yaml up`

## Base Image Considerations

The Checkmate image is Debian-based (`node:22-slim`), which does not ship the `ca-certificates` package — install it before running `update-ca-certificates` (see the Dockerfile above). Certificates dropped into `/usr/local/share/ca-certificates/` must have a `.crt` extension, and Node only consults the system store when started with `--use-system-ca`. For most setups the `NODE_EXTRA_CA_CERTS` compose approach is simpler.

## Smallstep CA Configuration

If you're using Smallstep as your internal CA, you can export the root CA certificate:

### Export Smallstep Root CA

```bash
# Export the root CA certificate
step certificate inspect --format pem step-ca/root_ca.crt > custom-ca.pem

# Or if you have the CA URL configured
step certificate inspect --format pem $(step path)/certs/root_ca.crt > custom-ca.pem
```

### Using the Exported Certificate

1. Copy the exported `custom-ca.pem` to your `docker/dev/certs/` directory
2. Use either the Node-level or OS-level approach above
3. Restart your Checkmate server container

## Security Considerations

⚠️ **Important Security Warning**: Only trust CAs that you control or explicitly trust. Adding untrusted CAs can compromise the security of your monitoring system.

- **Private CAs**: Only trust CAs from your organization's PKI infrastructure
- **Self-signed certificates**: Consider using proper CA infrastructure instead
- **Certificate validation**: Ensure your CA certificates are valid and not expired
- **Access control**: Limit access to the CA certificate files in production

## Troubleshooting

### Verify CA Trust

Test if your CA is trusted by the container:

```bash
# Enter the running container
docker exec -it <container_name> sh

# Check if the CA is in the trust store
ls -la /usr/local/share/ca-certificates/
cat /etc/ssl/certs/ca-certificates.crt | grep -A 5 -B 5 "YOUR_CA_NAME"
```

### Common Issues

1. **Permission denied**: Ensure the CA certificate file has proper read permissions
2. **Certificate format**: Use PEM format (.pem, .crt) for best compatibility
3. **Container restart**: Always restart the container after adding new CA certificates
4. **Path issues**: Verify the certificate path in your volume mounts

## Example: Complete Working Setup

Here's a complete example for a Smallstep CA setup. This demonstrates both the baseline failure (expected) and the custom CA success:

### Baseline Test (Should Fail)
1. **Start Checkmate without custom CA trust:**
   ```bash
   cd docker/dev
   docker-compose up -d
   ```

2. **Test connection to internal HTTPS endpoint:**
   ```bash
   # This should fail with TLS error (unknown CA)
   docker-compose exec server node -e "
     const https = require('https');
     https.get('https://your-internal-site.com', res => {
       console.log('STATUS:', res.statusCode);
     }).on('error', e => {
       console.error('ERR:', e.message);
     });
   "
   ```
   **Expected result**: TLS error due to unknown CA

### Custom CA Test (Should Succeed)
1. **Export Smallstep Root CA:**
   ```bash
   step certificate inspect --format pem step-ca/root_ca.crt > docker/dev/certs/smallstep-root-ca.pem
   ```

2. **Update docker-compose.yaml with custom CA trust:**
   ```yaml
   services:
     server:
       environment:
         NODE_EXTRA_CA_CERTS: /certs/smallstep-root-ca.pem
       volumes:
         - ./certs:/certs:ro
   ```

3. **Restart with custom CA:**
   ```bash
   docker-compose down
   docker-compose -f docker-compose.yaml -f docker-compose.custom-ca-example.yaml up -d
   ```

4. **Test the same connection:**
   ```bash
   # This should now succeed
   docker-compose exec server node -e "
     const https = require('https');
     https.get('https://your-internal-site.com', res => {
       console.log('STATUS:', res.statusCode);
     }).on('error', e => {
       console.error('ERR:', e.message);
     });
   "
   ```
   **Expected result**: HTTP 200 OK

### Verification
- **Baseline**: TLS failure proves default behavior (unknown CA)
- **Custom CA**: TLS success proves custom CA trust is working
- **Both tests must pass** to confirm the feature works correctly

Your Checkmate server should now trust certificates issued by your Smallstep CA, allowing you to monitor internal HTTPS endpoints without disabling SSL validation.

---

## File: docs/i18n-contribution.md

# Contributing Translations

Checkmate supports multiple languages. This guide explains how to contribute translations.

## How to Add a New Language

1. Create a new JSON file in `client/src/locales/` named with the language code (e.g., `it.json` for Italian)

2. Copy the structure from `en.json` and translate all values

3. Add the language to the language selector in the UI

## Translation Guidelines

- en.json is the reference localization file, all keys should be declared here and mirrored in other localization files
- Translate only the values
- Use formal/informal tone consistently
- Test your translations in the UI

## File Structure
client/src/locales/
├── en.json      # Source language
├── es.json      # Spanish
├── de.json      # German
└── [lang].json  # Your language
## Testing Translations

1. Start the development server: `npm run dev`
2. Go to Settings → Language
3. Select your language
4. Verify all strings display correctly

---

## File: docs/infrastructure-disk-selection.md

# Infrastructure Monitoring - Disk Selection

This guide explains how to use the selective disk monitoring feature in Checkmate's infrastructure monitoring.

## Overview

By default, Checkmate monitors all detected disks on your server. The disk selection feature allows you to choose specific disks/mountpoints to monitor, giving you more control over what gets tracked and displayed.

## Prerequisites

- Checkmate server running with [Capture agent](https://github.com/bluewave-labs/capture) installed on target server
- An existing infrastructure monitor already created and configured
- At least one disk detected by the Capture agent

## Using Disk Selection

### Accessing the Feature

1. Navigate to your Infrastructure monitors page
2. Click on an existing infrastructure monitor to view details
3. Click the "Configure" or "Edit" button
4. Scroll down to the **Disk Selection** section

> **Note:** The disk selection feature is only available when editing existing monitors that have already detected disks.

### Selecting Disks to Monitor

1. **View Available Disks**: The system automatically detects all disks/mountpoints from your server
2. **Select Disks**: Check the boxes next to the disks you want to monitor
3. **Apply Changes**: Click "Save" to update your monitor configuration

### Understanding Disk Identifiers

Disks are identified by their mountpoint or device name:
- **Mountpoint**: `/` (root), `/home`, `/var`, etc.
- **Device**: `/dev/sda1`, `/dev/nvme0n1p1`, etc.

### Behavior

- **All disks selected**: Shows all detected disks in gauges and charts
- **Specific disks selected**: Only shows selected disks in the monitoring interface
- **No disks detected**: Displays "No disk detected for the moment" message

## What Gets Filtered

When you select specific disks, the following elements are filtered:

- **Disk Usage Gauges**: Only selected disks appear in the dashboard
- **Disk Usage Charts**: Time-series charts show only selected disks
- **Monitor Table**: Summary view reflects only selected disk usage

## Example Use Cases

- **Server with multiple drives**: Monitor only critical system disks, ignore temporary storage
- **Database servers**: Focus monitoring on database partition while ignoring logs partition
- **Web servers**: Monitor web content disk separately from system disk
- **Development environments**: Track only project-specific mountpoints

## Troubleshooting

### No Disks Appearing
- Verify the Capture agent is running on the target server
- Check that the agent has proper permissions to read disk information
- Ensure the monitor has been running long enough to collect at least one check

### Missing Disk Information
- Some disks may not be accessible to the Capture agent due to permissions
- Virtual or network-mounted disks may not appear in the list
- Check the Capture agent logs for any disk detection errors

## Related Documentation

- [Infrastructure Monitoring Setup](https://checkmate.so/docs)
- [Capture Agent Installation](https://github.com/bluewave-labs/capture)

---

## File: docs/README.md

# Checkmate Documentation

Welcome to the Checkmate documentation. This directory contains guides and references for various aspects of Checkmate deployment and configuration.

## Available Documentation

### Deployment & Configuration
- **[Custom CA Trust Guide](./custom-ca-trust.md)** - Configure Checkmate to trust custom Certificate Authorities
- **[Custom CA Quick Reference](./custom-ca-quick-reference.md)** - Quick setup guide for custom CA trust

## Docker Configuration

### Custom CA Trust
If you need to monitor internal HTTPS endpoints with certificates from private Certificate Authorities (like Smallstep), see our custom CA trust documentation:

- **Full Guide**: [Custom CA Trust Guide](./custom-ca-trust.md)
- **Quick Reference**: [Custom CA Quick Reference](./custom-ca-quick-reference.md)

### Example Configurations
The `docker/dev/` directory contains example configurations:
- `docker-compose.custom-ca-example.yaml` - Example Docker Compose with custom CA

## Contributing to Documentation

If you find issues or want to improve the documentation:

1. Check existing issues and discussions
2. Submit a pull request with your changes
3. Follow the same markdown formatting style
4. Include practical examples and code snippets

## Getting Help

- [GitHub Discussions](https://github.com/bluewave-labs/checkmate/discussions)
- [Discord Channel](https://discord.gg/NAb6H3UTjK)
- [Documentation Portal](https://checkmate.so/docs)

---

## File: server/README.md

<h1 align="center">[Checkmate backend](https://bluewavelabs.ca)</h1>



This directory contains the **backend** of Checkmate, which handles data processing, storage, and API services for the Checkmate monitoring tool. The backend is responsible for managing uptime checks, handling real-time alerts, and storing historical monitoring data.

Checkmate's backend is designed to be lightweight and scalable, ensuring reliable performance even with a high number of active monitors.

## Installation & documentation

For setup instructions, configuration details, and deployment guidelines, please visit our official documentation at [docs.checkmate.so](https://checkmate.so/docs).

## Related repositories

- [Capture Agent](https://github.com/bluewave-labs/capture) (Optional, provides additional server insights)