Compare commits

...
Author SHA1 Message Date
gorkem-bwl f85903adc9 chore(docs): trigger Vercel rebuild 2026-05-22 14:30:52 -04:00
gorkem-bwl a7895cce32 fix(docs): serve docs site at root of Vercel domain
Astro's base option was set to /docs to anticipate the reverse-proxy
URL at checkmate.so/docs. Vercel serves the dist folder at the root,
which made every page 404 because Astro had baked /docs/ into every
internal link and asset URL.

## Changes
- Remove base: "/docs" from astro.config.mjs — site now serves at root
- Update Releases nav link to /releases
- Update README's reverse-proxy snippet to strip the /docs prefix
  before forwarding to Vercel

## Final URL behaviour
- Direct: https://<vercel-domain>/                  → Introduction
- Direct: https://<vercel-domain>/releases/         → Releases
- Proxy:  https://checkmate.so/docs/                → strip /docs → Introduction
- Proxy:  https://checkmate.so/docs/releases/       → strip /docs → Releases
2026-05-22 14:26:51 -04:00
gorkem-bwl 8e4e9dd87c fix(docs): make the build self-contained for Vercel
Vercel's "Root Directory" setting limits the upload to docs/api/, so
the build cannot read sibling paths like ../../server/openapi.json or
../api-content/. A small prebuild script copies both into a vendored
folder inside the project tree (gitignored).

## Changes
- Add scripts/sync-sources.mjs to copy /docs/api-content/ and
  /server/openapi.json into docs/api/src/vendored/ before dev/build
- Wire predev/prebuild npm scripts so the sync runs automatically
- Update astro.config.mjs and src/content.config.ts to read from the
  vendored paths
- Gitignore src/vendored/ — contributors still edit the originals
2026-05-22 14:20:10 -04:00
gorkem-bwl 0d25973efb fix(docs): make the Starlight build pass
## Changes
- Bump @astrojs/starlight to ^0.32.0 so starlight-openapi 0.13's plugin
  contract (top-level setup hook) is supported
- Replace the no-arg docsLoader call with Astro's glob loader pointing
  at ../api-content so prose stays outside the Astro project tree
- Rename introduction.md → index.md and update the sidebar slug so the
  /docs/ root resolves instead of returning 404
- Commit package-lock.json so Vercel installs deterministically

## Verification
- npm run build produces 96 pages (7 narrative + 88 OpenAPI operations + 1
  releases timeline)
- /docs/, /docs/releases/, /docs/api-reference/operations/authlogin/ and
  every other route return 200 locally
- 14 release cards render with the brand-green "Latest" badge on v3.7.1
2026-05-22 14:04:52 -04:00
gorkem-bwl e69f40fdd0 docs(api): add releases timeline page
## Changes
- Add a Releases tab to the top navigation via a custom Starlight
  Header override (HeaderLinks.astro)
- Add ReleaseCard.astro — Vercel/Linear-style entry with sticky date
  + version on the left, headline and feature bullets on the right
- Curate 14 release entries spanning v1.0 (Sept 2024) through v3.7.1
  (April 2026), each focused on the most important user-facing
  features rather than every commit
- Newest release carries a "Latest" badge in the brand color
- Mobile layout collapses the two-column timeline to a single column

## Benefits
- Visitors can see at a glance what shipped recently and what
  Checkmate has matured into over time
- The data file (src/data/releases.ts) is the only thing that needs
  editing when a new version ships — no template or styling churn
- Built on the same Starlight site as the API docs, so look and feel
  stay consistent
2026-05-22 10:25:06 -04:00
gorkem-bwl 9aa2594df7 docs(api): add public API documentation site (Astro Starlight)
## Changes
- Add narrative API documentation in docs/api-content/ (introduction,
  quickstart, authentication, conventions, errors, rate limiting)
- Scaffold Astro Starlight site at docs/api/ that consumes prose from
  the sibling api-content folder and auto-generates the endpoint
  reference from server/openapi.json via starlight-openapi
- Wire Checkmate brand color (#13715B) into Starlight via custom CSS
- Add vercel.json so the docs site deploys cleanly as an Astro project
  with root directory docs/api/

## Benefits
- Public API docs ready to publish at checkmate.so/docs
- Endpoint reference stays in sync with the OpenAPI spec automatically
- Zero third-party SaaS dependency: site is fully self-hosted on Vercel
- Contributors edit plain markdown; no Astro knowledge needed for prose
2026-05-22 10:21:10 -04:00
ashu130698 9027707c92 chore: clean up deprecated FloatingActionBar component 2026-05-14 16:24:51 +05:30
ashu130698 9e69d863df fix(ui): remove unused SPACING import in monitors index 2026-05-14 15:54:55 +05:30
ashu130698 c740fb1aca fix(ui): resolve PR feedback regarding type casts and hardcoded spacing 2026-05-14 15:50:56 +05:30
ashu130698 3c05b9df98 fix(i18n): migrate and insert missing translation keys for bulk edit notifications 2026-05-14 15:49:16 +05:30
ashu130698 d5d79a1eda fix(ux): replace buggy margin-based centering with reliable flexbox wrapper to permanently center floating action bars 2026-05-14 15:49:16 +05:30
ashu130698 2097bc477a fix(ux): replace overwritten transform on floating action bars to fix overflow centering on mobile devices 2026-05-14 15:49:16 +05:30
ashu130698 744d8c8dcf style: fix code format issues 2026-05-14 15:49:16 +05:30
ashu130698 7a5ee6ba05 fix(monitors): address PR feedback on mobile card views and modal selection state 2026-05-14 15:48:55 +05:30
ashu130698 eeee6db94e fix(monitors): add missing page prop to MonitorTable 2026-05-14 15:46:55 +05:30
ashu130698 656c04d0e4 fix(monitors): PR feedback -layout rhythm, pagination memory leak, error alerts, and mobile cards 2026-05-14 15:46:33 +05:30
ashu130698 010ef9661f fix: corrected TypeScript notification property name 2026-05-14 15:45:30 +05:30
ashu130698 ff0ef928a6 feat: added bulk edit notification UI to uptime monitors 2026-05-14 15:45:11 +05:30
ashu130698 eb29a725c2 feat: add table row selection and floating action bar MVP 2026-05-14 15:41:50 +05:30
28 changed files with 9672 additions and 10 deletions

No files matched your search

@@ -142,7 +142,7 @@ export function DataTable<
>
<Typography
component="div"
color={theme.palette.text.primary}
color={theme.palette.text.secondary}
>
{header.mobileLabel ?? header.content}
</Typography>
@@ -0,0 +1,138 @@
import React, { useState, useEffect } from "react";
import { useTranslation } from "react-i18next";
import { useTheme } from "@mui/material/styles";
import { Dialog, Select, RadioWithDescription } from "@/Components/inputs";
import { SPACING, LAYOUT } from "@/Utils/Theme/constants";
import Stack from "@mui/material/Stack";
import RadioGroup from "@mui/material/RadioGroup";
import MenuItem from "@mui/material/MenuItem";
import Typography from "@mui/material/Typography";
import Alert from "@mui/material/Alert";
import { useGet, usePatch } from "@/Hooks/UseApi";
import type { Notification } from "@/Types/Notification";
interface BulkEditNotificationsModalProps {
open: boolean;
onClose: () => void;
selectedMonitors: string[];
onComplete: (success: boolean) => void;
}
export const BulkEditNotificationsModal: React.FC<BulkEditNotificationsModalProps> = ({
open,
onClose,
selectedMonitors,
onComplete,
}) => {
const { t } = useTranslation();
const theme = useTheme();
const [action, setAction] = useState<"add" | "remove" | "set">("add");
const [notificationIds, setNotificationIds] = useState<string[]>([]);
// Fetch available notifications
const { data: notifications } = useGet<Notification[]>(
"/notifications/team",
undefined,
{
keepPreviousData: true,
}
);
const { patch, loading: isPatching, error } = usePatch();
// Reset state when modal opens
useEffect(() => {
if (open) {
setAction("add");
setNotificationIds([]);
}
}, [open]);
const handleConfirm = async () => {
const res = await patch("/monitors/notifications", {
monitorIds: selectedMonitors,
notificationIds,
action,
});
onComplete(!!res);
};
// Prevent submitting empty arrays unless the action is 'set'
const isMissingSelection = action !== "set" && notificationIds.length === 0;
return (
<Dialog
open={open}
title={t("pages.common.monitors.bulkEdit.title")}
onCancel={onClose}
onConfirm={isMissingSelection ? undefined : handleConfirm}
loading={isPatching}
maxWidth="sm"
fullWidth
>
<Stack
spacing={theme.spacing(LAYOUT.XS)}
mt={theme.spacing(SPACING.LG)}
>
{error && <Alert severity="error">{error}</Alert>}
<Typography variant="body1">
{t("pages.common.monitors.bulkEdit.selectedText", {
count: selectedMonitors.length,
})}
</Typography>
<RadioGroup
value={action}
onChange={(e) => {
const val = e.target.value;
if (val === "add" || val === "remove" || val === "set") {
setAction(val);
}
}}
>
<RadioWithDescription
value="add"
label={t("pages.common.monitors.bulkEdit.actionAdd")}
description={t("pages.common.monitors.bulkEdit.actionAddDesc")}
/>
<RadioWithDescription
value="remove"
label={t("pages.common.monitors.bulkEdit.actionRemove")}
description={t("pages.common.monitors.bulkEdit.actionRemoveDesc")}
/>
<RadioWithDescription
value="set"
label={t("pages.common.monitors.bulkEdit.actionSet")}
description={t("pages.common.monitors.bulkEdit.actionSetDesc")}
/>
</RadioGroup>
<Select
multiple
value={notificationIds}
onChange={(e) => {
const val = e.target.value;
if (typeof val === "string") {
setNotificationIds(val.split(","));
} else if (Array.isArray(val)) {
setNotificationIds(val.filter((v) => typeof v === "string"));
}
}}
fieldLabel={t("pages.common.monitors.bulkEdit.selectLabel")}
placeholder={t("pages.common.monitors.bulkEdit.selectPlaceholder")}
>
{notifications?.map((n) => (
<MenuItem
key={n.id}
value={n.id}
>
{n.notificationName} ({n.type.toUpperCase()})
</MenuItem>
))}
</Select>
</Stack>
</Dialog>
);
};
+1
View File
@@ -15,3 +15,4 @@ export * from "./charts/HistogramPageSpeedDetailsTooltip";
export * from "./charts/HistogramInfrastructure";
export * from "./HeaderMonitorsSummary";
export * from "./BulkActionsBar";
export * from "./BulkEditNotificationsModal";
@@ -13,6 +13,7 @@ import { ArrowDown, ArrowUp } from "lucide-react";
import { useTranslation } from "react-i18next";
import { useTheme } from "@mui/material/styles";
import useMediaQuery from "@mui/material/useMediaQuery";
import { useNavigate } from "react-router-dom";
import { usePost } from "@/Hooks/UseApi";
import { useSelector } from "react-redux";
@@ -21,6 +22,7 @@ import type { Monitor } from "@/Types/Monitor";
import type { ActionMenuItem } from "@/Components/actions-menu";
import type { RootState } from "@/Types/state";
import { Checkbox } from "@/Components/inputs";
import { SPACING, LAYOUT } from "@/Utils/Theme/constants";
interface MonitorTableProps {
monitors: Monitor[];
@@ -57,6 +59,7 @@ export const MonitorTable = ({
}: MonitorTableProps) => {
const { t } = useTranslation();
const theme = useTheme();
const isSmall = useMediaQuery(theme.breakpoints.down("md"));
const navigate = useNavigate();
const chartType = useSelector((state: RootState) => state.ui?.chartType ?? "histogram");
const { post } = usePost<Record<string, never>, Monitor>();
@@ -205,7 +208,7 @@ export const MonitorTable = ({
mobileLabel: t("common.table.headers.name"),
content: (
<Stack
gap={theme.spacing(4)}
gap={theme.spacing(LAYOUT.XS)}
direction={"row"}
alignItems={"center"}
onClick={(e) => handleSort(e, "name")}
@@ -225,14 +228,14 @@ export const MonitorTable = ({
mobileLabel: t("common.table.headers.status"),
content: (
<Stack
gap={theme.spacing(4)}
gap={theme.spacing(LAYOUT.XS)}
direction={"row"}
justifyContent={"center"}
alignItems={"center"}
onClick={(e) => handleSort(e, "status")}
sx={{ cursor: "pointer" }}
>
<Box width={theme.spacing(8)} />
<Box width={theme.spacing(LAYOUT.MD)} />
{t("common.table.headers.status")}
{renderSortIcon(sortField === "status")}
</Stack>
@@ -257,14 +260,14 @@ export const MonitorTable = ({
mobileLabel: t("common.table.headers.type"),
content: (
<Stack
gap={theme.spacing(4)}
gap={theme.spacing(LAYOUT.XS)}
direction={"row"}
justifyContent={"center"}
alignItems={"center"}
onClick={(e) => handleSort(e, "type")}
sx={{ cursor: "pointer" }}
>
<Box width={theme.spacing(8)} />
<Box width={theme.spacing(LAYOUT.MD)} />
{t("common.table.headers.type")}
{renderSortIcon(sortField === "type")}
</Stack>
@@ -288,6 +291,30 @@ export const MonitorTable = ({
return (
<Box>
{isSmall && (
<Box
px={SPACING.SM}
pb={SPACING.LG}
>
<Stack
direction="row"
alignItems="center"
spacing={SPACING.SM}
>
<Checkbox
checked={isAllSelected}
indeterminate={isSomeSelected}
onChange={(e) => handleSelectAll(e.target.checked)}
/>
<Typography
variant="body2"
color="text.secondary"
>
{t("pages.common.monitors.bulkEdit.selectAll", "Select All")}
</Typography>
</Stack>
</Box>
)}
<Table
headers={headers}
data={monitors}
+32 -4
View File
@@ -9,6 +9,7 @@ import Stack from "@mui/material/Stack";
import { MonitorTable } from "@/Pages/Uptime/Monitors/Components/UptimeMonitorsTable";
import { HeaderCreate } from "@/Components/common";
import { Play, Pause } from "lucide-react";
import { BulkEditNotificationsModal } from "@/Components/monitors";
import { useTranslation } from "react-i18next";
import useMediaQuery from "@mui/material/useMediaQuery";
@@ -22,6 +23,7 @@ import type { RootState } from "@/Types/state";
import { useTheme } from "@mui/material";
import useDebounce from "@/Hooks/useDebounce";
import { useBulkMonitorActions } from "@/Hooks/useBulkMonitorActions";
import { LAYOUT } from "@/Utils/Theme/constants";
const UptimeMonitorsPage = () => {
const { t } = useTranslation();
@@ -43,7 +45,7 @@ const UptimeMonitorsPage = () => {
const [sortField, setSortField] = useState<string>("");
const [sortOrder, setSortOrder] = useState<"asc" | "desc">("asc");
const [selectedMonitor, setSelectedMonitor] = useState<Monitor | null>(null);
const [isBulkEditModalOpen, setIsBulkEditModalOpen] = useState(false);
const isDialogOpen = Boolean(selectedMonitor);
const debouncedSearch = useDebounce<string>(search, 300);
@@ -136,6 +138,14 @@ const UptimeMonitorsPage = () => {
refetch();
};
const handleBulkEditComplete = (success: boolean) => {
setIsBulkEditModalOpen(false);
if (success) {
setSelectedRows([]);
refetch();
}
};
const handleCancel = () => {
setSelectedMonitor(null);
};
@@ -174,7 +184,7 @@ const UptimeMonitorsPage = () => {
<Stack
direction={isSmall ? "column" : "row"}
justifyContent={isSmall ? "flex-start" : "space-between"}
gap={theme.spacing(4)}
gap={theme.spacing(LAYOUT.XS)}
>
<ControlsFilter
selectedTypes={selectedTypes}
@@ -213,16 +223,27 @@ const UptimeMonitorsPage = () => {
>
{t("common.buttons.pause")}
</Button>
<Button
size="small"
onClick={() => setIsBulkEditModalOpen(true)}
>
{t("pages.common.monitors.bulkEdit.editButton", {
defaultValue: "Edit Notifications",
})}
</Button>
</BulkActionsBar>
)}
<MonitorTable
page={page}
monitors={monitorsWithChecks || []}
refetch={refetch}
setSelectedMonitor={setSelectedMonitor}
count={count || 0}
page={page}
setPage={setPage}
setPage={(newPage) => {
setPage(newPage);
setSelectedRows([]);
}}
rowsPerPage={rowsPerPage}
sortField={sortField}
setSortField={setSortField}
@@ -236,6 +257,7 @@ const UptimeMonitorsPage = () => {
})
);
setPage(0);
setSelectedRows([]);
}}
selectedRows={selectedRows}
onSelectionChange={setSelectedRows}
@@ -248,6 +270,12 @@ const UptimeMonitorsPage = () => {
onCancel={handleCancel}
loading={isDeleting}
/>
<BulkEditNotificationsModal
open={isBulkEditModalOpen}
onClose={() => setIsBulkEditModalOpen(false)}
selectedMonitors={selectedRows}
onComplete={handleBulkEditComplete}
/>
</MonitorBasePageWithStates>
);
};
+15
View File
@@ -30,6 +30,7 @@
"home": "Home"
},
"buttons": {
"selected": "selected",
"addMember": "Add member",
"cancel": "Cancel",
"close": "Close",
@@ -425,6 +426,20 @@
},
"common": {
"monitors": {
"bulkEdit": {
"selectAll": "Select All",
"editButton": "Edit Notifications",
"title": "Edit Notifications",
"selectedText": "You are applying changes to {{count}} monitor(s).",
"actionAdd": "Add",
"actionAddDesc": "Adds the selected notifications to these monitors without removing existing ones.",
"actionRemove": "Remove",
"actionRemoveDesc": "Removes exclusively the selected notifications from these monitors.",
"actionSet": "Replace all",
"actionSetDesc": "Replaces all existing notifications. Leave empty to clear all notifications.",
"selectLabel": "Notification Channels",
"selectPlaceholder": "Select channels..."
},
"actions": {
"configure": "Configure",
"delete": "Delete",
+114
View File
@@ -0,0 +1,114 @@
# Checkmate API documentation
Public-facing API documentation that powers **https://checkmate.so/docs**.
The site is built with [Astro Starlight](https://starlight.astro.build/) — a static-site generator that produces a fast, modern docs site with zero JavaScript by default. Starlight is open source and self-hostable; there is no third-party account required to publish.
## Repository layout
```
docs/
├── api-content/ ← markdown prose (this folder — edit here)
│ ├── introduction.md
│ ├── quickstart.md
│ ├── authentication.md
│ ├── conventions.md
│ ├── errors.md
│ ├── rate-limiting.md
│ └── api-reference/
│ └── overview.md
└── api/ ← Astro Starlight project (build tool — leave alone unless changing the site)
├── package.json
├── astro.config.mjs
├── src/content.config.ts
└── src/styles/checkmate.css
```
**Contributors editing prose only need to touch `docs/api-content/`**. Astro's content loader picks up the markdown automatically.
The endpoint reference (every URL + schema) is **auto-generated from [`server/openapi.json`](../../server/openapi.json)** via the `starlight-openapi` plugin. Adding or changing an endpoint means updating that JSON; the public docs pick it up on the next Vercel build.
## Local preview
```bash
cd docs/api
npm install
npm run dev
```
Visit http://localhost:4321/docs. Hot-reload works for markdown changes in `docs/api-content/`.
To produce the static site:
```bash
npm run build # outputs docs/api/dist/
npm run preview # serve the built output locally
```
## Deploying to checkmate.so/docs
Astro Starlight builds a static site. Deploy on Vercel and reverse-proxy `checkmate.so/docs/*` to it.
### One-time Vercel setup
1. Go to https://vercel.com → New Project → import the `bluewave-labs/Checkmate` GitHub repo.
2. **Root Directory**: set to `docs/api`.
3. **Framework Preset**: Vercel auto-detects Astro from `vercel.json`.
4. **Build & Output**: leave defaults (`npm run build` → `dist/`).
5. **Production Branch**: `develop`.
6. Deploy. Vercel returns a URL like `checkmate-docs.vercel.app`.
7. Verify the site renders correctly.
The deployment auto-rebuilds on every push to `develop` and on every PR (as a preview deploy).
### Reverse-proxy /docs to Vercel
In whichever edge proxy fronts `checkmate.so`, route `/docs/*` to the Vercel deployment.
**Cloudflare Workers:**
```javascript
addEventListener("fetch", (event) => {
const url = new URL(event.request.url);
if (url.pathname === "/docs" || url.pathname.startsWith("/docs/")) {
const target = new URL(url.pathname, "https://checkmate-docs.vercel.app");
target.search = url.search;
return event.respondWith(fetch(target.toString(), event.request));
}
});
```
**nginx:**
```nginx
location /docs/ {
proxy_pass https://checkmate-docs.vercel.app/docs/;
proxy_set_header Host checkmate-docs.vercel.app;
proxy_ssl_server_name on;
}
```
The `base: "/docs"` setting in `astro.config.mjs` ensures every internal link and asset URL is prefixed correctly so the site works at `checkmate.so/docs/...` rather than the root.
## Updating the docs
| Task | What to edit |
|---|---|
| Fix a typo, clarify wording | `docs/api-content/*.md` |
| Add a new narrative page | Add the file to `docs/api-content/`, then add a `slug` entry to `astro.config.mjs` → `sidebar` |
| Fix something in the API reference | Edit `server/openapi.json` (or the route spec it's built from) — never edit the generated reference pages |
| Change branding / colors | `docs/api/src/styles/checkmate.css` |
| Change site config (title, logo, social) | `docs/api/astro.config.mjs` |
Two rules:
1. **Never hand-edit the API reference.** It's regenerated on every build. Fix the spec.
2. **Test code samples against a real instance before merging.** Wrong code in docs is worse than missing code.
## Why Astro Starlight (and not Mintlify, Docusaurus, etc.)
- **No third-party account required.** Self-hostable on any static host.
- **Best looking** of the modern docs frameworks per multiple 2026 reviews — Distr, among others, migrated away from Docusaurus specifically for Starlight's design.
- **Zero JavaScript by default.** Pages are pure HTML — fast load, perfect Lighthouse scores.
- **OpenAPI integration is first-class** via `starlight-openapi`.
- **Markdown is the source of truth.** No proprietary formats. Easy to migrate later if needed.
@@ -0,0 +1,47 @@
---
title: API reference overview
description: All endpoints, grouped by resource. Auto-generated from the OpenAPI spec.
---
# API reference
The full machine-readable specification is at [`server/openapi.json`](https://github.com/bluewave-labs/Checkmate/blob/develop/server/openapi.json). Mintlify renders an interactive reference from it — each endpoint below is fully documented with request/response schemas, examples, and a built-in "Try it" form.
## Endpoint groups
| Group | Description |
|---|---|
| **auth** | Register, log in, password recovery, manage users |
| **invite** | Send and redeem team invites |
| **monitors** | Create, update, pause, and delete monitors |
| **checks** | Read individual check results and team-wide summaries |
| **incidents** | List, filter, and resolve downtime incidents |
| **maintenance-window** | Schedule maintenance windows to silence checks |
| **notifications** | Configure email, Slack, Discord, and webhook channels |
| **status-page** | Manage public status pages |
| **geo-checks** | Multi-region check results |
| **logs** | Audit and operational logs |
| **settings** | Global Checkmate settings |
| **queue** | Background-job queue health (operational) |
| **diagnostic** | System health and version info |
## Authentication
Every endpoint except the unauthenticated auth/recovery routes requires:
```
Authorization: Bearer <jwt>
```
See [Authentication](/api/authentication) for how to obtain a token.
## Conventions recap
- Base URL: `https://your-checkmate-host/api/v1`
- Response envelope: `{ success, msg, data }`
- IDs: 24-character MongoDB ObjectIds
- Timestamps: ISO 8601 UTC
- Durations: integers in milliseconds
- Most resources are team-scoped — pass `teamId` where the path includes `/team/...`
See [Conventions](/api/conventions) for full details.
+137
View File
@@ -0,0 +1,137 @@
---
title: Authentication
description: Obtain a JWT from /auth/login and send it as a Bearer token on every subsequent request.
---
# Authentication
Checkmate uses **JWT bearer tokens**. There are no API keys, no OAuth, and no client secrets. Every API user is a regular Checkmate user — the credentials you log in to the web UI with are the credentials you use to obtain a token.
## The flow
1. **Get a token** by calling `POST /auth/login` with your email and password.
2. **Send the token** as `Authorization: Bearer <token>` on every subsequent request.
3. **Refresh** by calling `POST /auth/login` again before the token expires.
## Obtaining a token
```bash
curl -X POST https://your-checkmate-host/api/v1/auth/login \
-H "Content-Type: application/json" \
-d '{
"email": "you@example.com",
"password": "YourPass1!"
}'
```
A successful response returns the token plus the authenticated user record:
```json
{
"success": true,
"msg": "OK",
"data": {
"user": {
"_id": "65f1c2a4d8b9e0123456789a",
"firstName": "Ada",
"lastName": "Lovelace",
"email": "you@example.com",
"role": ["admin"],
"teamId": "65f1c2a4d8b9e01234567890",
"createdAt": "2026-04-01T10:00:00.000Z",
"updatedAt": "2026-04-15T14:30:00.000Z"
},
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
}
}
```
Store both `token` (for the `Authorization` header) and `user.teamId` (most resource endpoints are scoped by team).
## Using a token
Every authenticated request must include:
```
Authorization: Bearer <token>
```
```bash
curl https://your-checkmate-host/api/v1/monitors/team/<teamId> \
-H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
```
```javascript
const res = await fetch("https://your-checkmate-host/api/v1/monitors/team/<teamId>", {
headers: { Authorization: `Bearer ${token}` },
});
```
## Token lifetime
Tokens expire after the duration configured by the operator via the `TOKEN_TTL` environment variable. The default is **99 days**.
There is no separate refresh-token endpoint. When a token is close to expiry — or after a 401 response — call `POST /auth/login` again to obtain a new one.
## Roles
The `user.role` claim is an array. Available roles:
| Role | Capability |
|---|---|
| `superadmin` | Full access to all teams and global settings. Can promote other users. |
| `admin` | Full access within their team. |
| `user` | Read access within their team. |
| `demo` | Limited demo-mode access. |
A user can hold multiple roles simultaneously.
## Registering the first user
A fresh Checkmate instance has no users. The first call to `POST /auth/register` creates the first **superadmin** and provisions a team for them:
```bash
curl -X POST https://your-checkmate-host/api/v1/auth/register \
-H "Content-Type: application/json" \
-d '{
"firstName": "Ada",
"lastName": "Lovelace",
"email": "ada@example.com",
"password": "Hunter2!",
"role": ["superadmin"],
"teamId": ""
}'
```
After the first user exists, additional users are usually created via the invite flow rather than `POST /auth/register`:
```
POST /invite/send # superadmin/admin sends an invite
POST /auth/register # invitee redeems the invite token
```
You can check whether a superadmin already exists with:
```bash
curl https://your-checkmate-host/api/v1/auth/users/superadmin
```
```json
{ "success": true, "data": true }
```
## Password recovery
The password-recovery flow is three calls, all unauthenticated:
```
POST /auth/recovery/request # user requests a recovery email
POST /auth/recovery/validate # client validates the recovery token
POST /auth/recovery/reset # user sets a new password
```
## Logging out
JWTs are stateless — there is no server-side logout. Drop the token from your client storage. If you need to invalidate a token before its expiry (e.g. a compromised device), the operator must rotate the `JWT_SECRET` environment variable, which invalidates every outstanding token.
Next: [Conventions](/api/conventions).
+122
View File
@@ -0,0 +1,122 @@
---
title: Conventions
description: Response envelope, request format, identifiers, pagination, and team scoping.
---
# Conventions
A small number of conventions apply to every endpoint. Once you've internalized them, the per-resource reference is mostly self-explanatory.
## Request format
- **Content-Type**: `application/json` for all POST/PUT/PATCH bodies, except multipart uploads (logos, profile images), which use `multipart/form-data`.
- **Charset**: UTF-8.
- **Method semantics**:
- `GET` — read, never mutates state.
- `POST` — create, or action that doesn't fit GET/PATCH/DELETE.
- `PATCH` — partial update.
- `PUT` — full replace.
- `DELETE` — delete.
## Response envelope
Every JSON response uses the same shape:
```json
{
"success": true,
"msg": "OK",
"data": { /* endpoint-specific payload */ }
}
```
| Field | Type | Meaning |
|---|---|---|
| `success` | boolean | `true` on 2xx, `false` on 4xx/5xx |
| `msg` | string | Human-readable summary — safe to surface in UIs |
| `data` | object \| array \| null | Endpoint-specific payload. Absent or `null` for `204 No Content`. |
Error responses use the same envelope with `success: false` and `data` typically omitted. See [Errors](/api/errors).
## Identifiers
- All resource IDs are **MongoDB ObjectIds** (24-character hex strings):
```
65f1c2a4d8b9e0123456789a
```
- Treat IDs as opaque strings. Do not attempt to parse the timestamp or counter components.
## Team scoping
Most resources belong to a **team**. After login, the user record contains a `teamId`; pass it as a path parameter wherever the endpoint name includes `/team/...`:
```
GET /monitors/team/65f1c2a4d8b9e01234567890
GET /incidents/team
GET /checks/team
```
Users only see resources belonging to their own team, except for `superadmin`, who can read across teams.
## Date and time
- All timestamps in JSON are **ISO 8601** strings in UTC:
```
"createdAt": "2026-04-01T10:00:00.000Z"
```
- Durations are typically integers in **milliseconds** (intervals, response times, time windows).
## Pagination
Endpoints that return collections accept these optional query parameters:
| Parameter | Type | Default | Notes |
|---|---|---|---|
| `limit` | integer | endpoint-dependent | Maximum rows to return. |
| `page` | integer | `0` | Zero-indexed page number. |
| `sortBy` | string | endpoint-dependent | Field to sort by. |
| `sortOrder` | `asc` \| `desc` | `desc` | Sort direction. |
| `filter` | string | — | Endpoint-specific filter (e.g. status). |
When a collection supports pagination, the response `data` includes the rows plus the total count:
```json
{
"success": true,
"msg": "OK",
"data": {
"monitors": [ /* … */ ],
"filteredMonitors": [ /* … */ ],
"summary": { /* counters */ }
}
}
```
Exact shapes vary per endpoint. See the [API reference](/api/reference) for each endpoint's response schema.
## File uploads
Two endpoints accept files:
- Status page logo: `POST /status-page` (multipart, field `logo`)
- User profile picture: `POST /auth/register`, `PATCH /auth/user/:id` (multipart, field `profileImage`)
Use `multipart/form-data` and include the JSON fields as separate form parts.
## CORS
The server's CORS policy is configured via the `CLIENT_HOST` environment variable. Only requests whose `Origin` matches `CLIENT_HOST` are accepted, and credentials (cookies) are allowed.
If you're calling the API from a browser on a different origin, the operator must add that origin to `CLIENT_HOST`. Server-to-server calls (curl, your backend) are not subject to CORS.
## Versioning
The base path includes the API version:
```
/api/v1/...
```
Within `v1`, additive changes (new endpoints, new optional fields) ship without notice. Breaking changes (removed endpoints, changed field types) only ship under a new major version.
Next: [Errors](/api/errors).
+124
View File
@@ -0,0 +1,124 @@
---
title: Errors
description: HTTP status codes Checkmate returns, what they mean, and how to handle them.
---
# Errors
Failure responses share the same envelope as successful ones, with `success: false` and `data` omitted (or `null`):
```json
{
"success": false,
"msg": "Validation failed: email must be a valid email"
}
```
Always treat the **HTTP status code** as the source of truth for handling logic, and surface `msg` to humans.
## Status codes
| Status | Meaning | When you'll see it |
|---|---|---|
| `200 OK` | Request succeeded with a body | Most GETs, successful mutations that return the new resource |
| `204 No Content` | Request succeeded with no body | DELETEs, successful actions like `/status-page/:url/unlock` |
| `400 Bad Request` | Malformed request | Missing required path param, malformed JSON, invalid file upload |
| `401 Unauthorized` | Missing or invalid JWT | Token expired, token missing, token signed with a different secret |
| `403 Forbidden` | Authenticated, but not allowed | Accessing a resource that belongs to a different team, or insufficient role |
| `404 Not Found` | Resource doesn't exist | Wrong ID, deleted resource, mistyped path |
| `409 Conflict` | Operation would violate a constraint | Duplicate email on register, slug already taken on status page |
| `413 Payload Too Large` | File upload exceeded limit | Logo or profile image too big |
| `422 Unprocessable Entity` | Request was well-formed but failed validation | Zod / Joi schema rejected the body |
| `429 Too Many Requests` | Rate limit exceeded | See [Rate limiting](/api/rate-limiting) |
| `500 Internal Server Error` | Unexpected server error | A bug — report it with the request ID |
## Common shapes
### 401 — missing or invalid token
```json
{
"success": false,
"msg": "No auth token provided"
}
```
Re-authenticate by calling `POST /auth/login`.
### 422 — validation error
```json
{
"success": false,
"msg": "\"email\" must be a valid email"
}
```
The `msg` describes the first failing field. Re-send the request with the fix.
### 403 — forbidden
```json
{
"success": false,
"msg": "Forbidden"
}
```
You're authenticated but the resource isn't visible to you. Common cause: passing a `teamId` that isn't your team's.
### 429 — rate limited
```json
{
"success": false,
"msg": "Too many requests"
}
```
Inspect `RateLimit-*` response headers and back off. See [Rate limiting](/api/rate-limiting).
## Error handling pattern
Client code should branch on status code, not on `msg`:
```javascript
const res = await fetch(url, opts);
const body = await res.json().catch(() => ({}));
switch (res.status) {
case 200:
case 204:
return body.data;
case 401:
// Token expired or missing — re-authenticate, then retry once.
await reauthenticate();
return retry();
case 422:
// Surface the validation error to the user.
throw new ValidationError(body.msg);
case 429:
// Back off using Retry-After if present.
const retryAfter = Number(res.headers.get("Retry-After") ?? 60);
await sleep(retryAfter * 1000);
return retry();
default:
throw new Error(`Checkmate API ${res.status}: ${body.msg ?? res.statusText}`);
}
```
## Reporting bugs
If you see a `500`, capture:
- The exact request (method, path, body — **redact secrets**)
- The full response body and headers
- A timestamp in UTC
Open an issue at https://github.com/bluewave-labs/Checkmate/issues with that information.
Next: [Rate limiting](/api/rate-limiting).
+84
View File
@@ -0,0 +1,84 @@
---
title: Introduction
description: Use the Checkmate REST API to manage monitors, incidents, status pages, notifications, and users programmatically.
---
# Checkmate API
Checkmate exposes a REST API that mirrors everything the web UI can do. Use it to provision monitors from infrastructure-as-code, build internal dashboards, automate incident workflows, or integrate Checkmate into your existing alerting stack.
The web UI you log in to is itself just a client of this same API.
## Base URL
```
https://<your-checkmate-host>/api/v1
```
Replace `<your-checkmate-host>` with the domain where you deployed Checkmate. For local development:
```
http://localhost:52345/api/v1
```
All endpoints are relative to this base. Every path on this page is shown without the `/api/v1` prefix — prepend it when making requests.
## API stability
The API is at version **`v1`** and is considered stable. Breaking changes will only ship under a new major version (`/api/v2`).
## What you can do
| Resource | What it covers |
|---|---|
| `monitors` | Create, update, pause, and delete monitors of any type (HTTP, Ping, Port, DNS, Docker, PageSpeed, gRPC, WebSocket, Game, Hardware) |
| `checks` | Read individual check results and aggregated stats |
| `incidents` | List, filter, and resolve downtime incidents |
| `notifications` | Manage notification channels (email, Slack, Discord, webhooks) |
| `status-page` | Configure public status pages |
| `maintenance-window` | Schedule maintenance windows so checks don't fire incidents |
| `geo-checks` | Multi-region check results |
| `auth` | Register, log in, manage users |
| `invite` | Invite teammates |
| `logs` | Audit and operational logs |
| `settings` | Tune global Checkmate settings |
| `queue` | Inspect the background-job queue (operational) |
| `diagnostic` | Health and system info |
## What you'll need
1. A running Checkmate instance you administer.
2. A user account on that instance.
3. A JWT obtained by calling `POST /auth/login` — see [Authentication](/api/authentication).
There is no separate "API key" concept in Checkmate. The same JWT that authenticates the web UI authenticates every API call.
## Response shape
Every endpoint returns the same envelope:
```json
{
"success": true,
"msg": "OK",
"data": { /* endpoint-specific payload */ }
}
```
See [Conventions](/api/conventions) for the full contract and [Errors](/api/errors) for failure shapes.
## Quick example
```bash
# 1. Log in to get a token
TOKEN=$(curl -s -X POST https://your-checkmate-host/api/v1/auth/login \
-H "Content-Type: application/json" \
-d '{"email":"you@example.com","password":"YourPass1!"}' \
| jq -r '.data.token')
# 2. List your team's monitors
curl https://your-checkmate-host/api/v1/monitors/team/<teamId> \
-H "Authorization: Bearer $TOKEN"
```
Next: [Authentication](/api/authentication).
+117
View File
@@ -0,0 +1,117 @@
---
title: Quickstart
description: From zero to a working monitor in under five minutes.
---
# Quickstart
This walkthrough creates a Checkmate user, logs in, provisions an HTTP monitor, and reads its checks — all via the API.
It assumes you have a Checkmate instance running and reachable. Substitute `https://your-checkmate-host` with your actual host throughout.
## 1. Register the first user
If your instance is fresh, the first user becomes a superadmin:
```bash
curl -X POST https://your-checkmate-host/api/v1/auth/register \
-H "Content-Type: application/json" \
-d '{
"firstName": "Ada",
"lastName": "Lovelace",
"email": "ada@example.com",
"password": "Hunter2!Hunter2!",
"role": ["superadmin"],
"teamId": ""
}'
```
The response includes a JWT and a freshly-created `teamId`. Save both.
If a superadmin already exists, skip to step 2 and use existing credentials.
## 2. Log in
If you already have an account:
```bash
TOKEN=$(curl -s -X POST https://your-checkmate-host/api/v1/auth/login \
-H "Content-Type: application/json" \
-d '{"email":"ada@example.com","password":"Hunter2!Hunter2!"}' \
| jq -r '.data.token')
TEAM_ID=$(curl -s -X POST https://your-checkmate-host/api/v1/auth/login \
-H "Content-Type: application/json" \
-d '{"email":"ada@example.com","password":"Hunter2!Hunter2!"}' \
| jq -r '.data.user.teamId')
echo "Token: $TOKEN"
echo "Team: $TEAM_ID"
```
## 3. Create a monitor
This creates an HTTP monitor that probes example.com every 60 seconds:
```bash
curl -X POST https://your-checkmate-host/api/v1/monitors \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d "{
\"name\": \"example.com\",
\"type\": \"http\",
\"url\": \"https://example.com\",
\"interval\": 60000,
\"teamId\": \"$TEAM_ID\",
\"isActive\": true
}"
```
A successful response returns the new monitor including its `_id`.
## 4. List your team's monitors
```bash
curl https://your-checkmate-host/api/v1/monitors/team/$TEAM_ID \
-H "Authorization: Bearer $TOKEN"
```
You should see the monitor you just created. Within a minute, the background worker will have run a few checks against it.
## 5. Read recent checks
Replace `<MONITOR_ID>` with the `_id` from step 3:
```bash
curl "https://your-checkmate-host/api/v1/checks/<MONITOR_ID>?limit=10" \
-H "Authorization: Bearer $TOKEN"
```
You'll see an array of check results — status code, response time, timestamp, region (if applicable).
## 6. Pause and delete the monitor
Pause:
```bash
curl -X PATCH https://your-checkmate-host/api/v1/monitors/<MONITOR_ID> \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"isActive": false}'
```
Delete:
```bash
curl -X DELETE https://your-checkmate-host/api/v1/monitors/<MONITOR_ID> \
-H "Authorization: Bearer $TOKEN"
```
## What's next
- **Set up notifications** — `POST /notifications` to wire email / Slack / Discord / webhook channels, then attach them to monitors.
- **Build a status page** — `POST /status-page` to publish your monitors publicly.
- **Schedule maintenance** — `POST /maintenance-window` to silence checks during planned downtime.
- **Browse the full API reference** — see every endpoint with request/response schemas.
See the [reference](/api/reference) for the complete list of endpoints.
+83
View File
@@ -0,0 +1,83 @@
---
title: Rate limiting
description: Per-IP request limits, headers, and how to back off.
---
# Rate limiting
Checkmate applies two per-IP rate limiters, both with a **60-second window**:
| Limiter | Applies to | Limit |
|---|---|---|
| General | All requests under `/api/v1/*` | **600 requests / 60 seconds** |
| Auth-sensitive | `/auth/login`, `/auth/register`, `/auth/recovery/*`, `/status-page/:url/unlock` | **15 requests / 60 seconds** |
The two limiters are **independent**. Auth-sensitive endpoints count against both buckets — you can hammer `/auth/login` 15 times in a minute before being rate-limited there, but a single client can never exceed 600 total requests per minute regardless of which endpoints are called.
## Headers
Every response includes the standard `RateLimit-*` headers so you can pace yourself proactively:
```
RateLimit-Limit: 600
RateLimit-Remaining: 587
RateLimit-Reset: 42
```
| Header | Meaning |
|---|---|
| `RateLimit-Limit` | The cap for the current window. |
| `RateLimit-Remaining` | Requests left before you hit `429`. |
| `RateLimit-Reset` | Seconds until the window resets. |
## When you exceed the limit
You receive `HTTP 429 Too Many Requests`:
```json
{
"success": false,
"msg": "Too many requests"
}
```
The response also includes `Retry-After`:
```
Retry-After: 42
```
Wait that many seconds before retrying. If `Retry-After` is missing, fall back to the `RateLimit-Reset` header.
## Recommended backoff
For one-off scripts, simple sleep-then-retry is fine:
```javascript
async function callWithRetry(url, opts, attempts = 3) {
for (let i = 0; i < attempts; i++) {
const res = await fetch(url, opts);
if (res.status !== 429) return res;
const retry = Number(res.headers.get("Retry-After") ?? 60);
await new Promise(r => setTimeout(r, retry * 1000));
}
throw new Error("Rate limit retry exhausted");
}
```
For higher-throughput integrations, watch `RateLimit-Remaining` and pace your sender to stay below the limit rather than waiting for `429`.
## Limits and proxies
Rate-limiting is per **client IP**, derived from the connection — or, when Checkmate is behind a reverse proxy with `trust proxy` configured, from the leftmost public IP in `X-Forwarded-For`.
If many of your clients share an egress IP (NAT, corporate proxy), they share the bucket. In that case, an operator should either:
- Increase the limits (server code change required), or
- Make sure each client connects from a distinct IP.
## Limits are not contractual
The numbers above reflect the current defaults. An operator can change them at any time (they're hardcoded in `server/src/middleware/rateLimiter.ts`). If you're building an integration that depends on a higher limit, coordinate with the operator first.
Next: [API reference](/api/reference) — auto-generated from the OpenAPI spec.
+21
View File
@@ -0,0 +1,21 @@
# Astro build
dist/
.astro/
# Sources copied in at build time by scripts/sync-sources.mjs.
# Edit /docs/api-content/ and /server/openapi.json instead.
src/vendored/
# Dependencies
node_modules/
# Logs
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
# Env
.env
.env.production
.vercel
+83
View File
@@ -0,0 +1,83 @@
import { defineConfig } from "astro/config";
import starlight from "@astrojs/starlight";
import starlightOpenAPI, { openAPISidebarGroups } from "starlight-openapi";
// The OpenAPI spec is vendored from server/openapi.json by
// scripts/sync-sources.mjs (runs as predev/prebuild). We point at the
// vendored copy so the build is self-contained — Vercel only uploads
// the docs/api/ subtree, so paths outside this directory are not
// available at build time.
const OPENAPI_PATH = "./src/vendored/openapi.json";
export default defineConfig({
// The deployed site serves at the root of its Vercel domain. The
// reverse proxy that maps checkmate.so/docs/* → Vercel must strip
// the /docs/ prefix before forwarding (the Cloudflare Worker snippet
// in docs/api-content/README.md does this).
site: "https://checkmate.so",
integrations: [
starlight({
title: "Checkmate API",
description: "Use the Checkmate REST API to manage monitors, incidents, status pages, notifications, and users programmatically.",
// Prose markdown is loaded from /docs/api-content via the custom
// docsLoader in src/content.config.ts. Keeping content outside the
// Astro project tree means contributors can edit it without dealing
// with Astro / Starlight tooling.
logo: {
src: "./src/assets/logo.svg",
replacesTitle: true,
},
favicon: "/favicon.svg",
customCss: ["./src/styles/checkmate.css"],
social: {
github: "https://github.com/bluewave-labs/Checkmate",
},
// Inject a "Releases" link into the site header.
components: {
SocialIcons: "./src/components/HeaderLinks.astro",
},
sidebar: [
{
label: "Getting started",
items: [
{ label: "Introduction", slug: "" },
{ label: "Quickstart", slug: "quickstart" },
{ label: "Authentication", slug: "authentication" },
],
},
{
label: "Core concepts",
items: [
{ label: "Conventions", slug: "conventions" },
{ label: "Errors", slug: "errors" },
{ label: "Rate limiting", slug: "rate-limiting" },
],
},
{
label: "API reference",
items: [
{ label: "Overview", slug: "api-reference/overview" },
// The OpenAPI plugin appends every endpoint under this group.
...openAPISidebarGroups,
],
},
],
plugins: [
starlightOpenAPI([
{
base: "api-reference",
label: "REST API",
schema: OPENAPI_PATH,
},
]),
],
}),
],
});
+7983
View File
File diff suppressed because it is too large. Load diff
+22
View File
@@ -0,0 +1,22 @@
{
"name": "checkmate-docs-api",
"type": "module",
"version": "0.1.0",
"private": true,
"description": "Public API documentation for Checkmate. Built with Astro Starlight.",
"scripts": {
"prebuild": "node ./scripts/sync-sources.mjs",
"predev": "node ./scripts/sync-sources.mjs",
"dev": "astro dev",
"start": "astro dev",
"build": "astro build",
"preview": "astro preview",
"astro": "astro"
},
"dependencies": {
"@astrojs/starlight": "^0.32.0",
"astro": "^5.0.0",
"sharp": "^0.33.5",
"starlight-openapi": "^0.13.0"
}
}
+55
View File
@@ -0,0 +1,55 @@
// Copies external sources into the Astro project tree so the build is
// self-contained. Required because Vercel's "Root Directory" setting
// only uploads the docs/api/ subtree, leaving sibling/parent paths
// (server/openapi.json, docs/api-content/) outside the build context.
//
// Run automatically by predev / prebuild via package.json. If the
// external paths exist, they win; otherwise we assume the files were
// already vendored (e.g. by a previous run) and continue.
import { existsSync, mkdirSync, cpSync, copyFileSync, statSync } from "node:fs";
import { dirname, join } from "node:path";
import { fileURLToPath } from "node:url";
const __dirname = dirname(fileURLToPath(import.meta.url));
const PROJECT_ROOT = join(__dirname, "..");
const REPO_ROOT = join(PROJECT_ROOT, "..", "..");
const SOURCES = [
{
// Markdown prose (the public docs content).
from: join(REPO_ROOT, "docs", "api-content"),
to: join(PROJECT_ROOT, "src", "vendored", "api-content"),
kind: "dir",
},
{
// OpenAPI spec — read at build time by starlight-openapi.
from: join(REPO_ROOT, "server", "openapi.json"),
to: join(PROJECT_ROOT, "src", "vendored", "openapi.json"),
kind: "file",
},
];
let synced = 0;
for (const src of SOURCES) {
if (!existsSync(src.from)) {
if (existsSync(src.to)) {
console.log(`[sync-sources] ${src.from} not present; using existing vendored copy.`);
continue;
}
console.error(`[sync-sources] ERROR: source missing and no vendored copy: ${src.from}`);
process.exit(1);
}
mkdirSync(dirname(src.to), { recursive: true });
if (src.kind === "dir") {
cpSync(src.from, src.to, { recursive: true });
} else {
copyFileSync(src.from, src.to);
}
const stat = statSync(src.from);
console.log(`[sync-sources] ${src.from} → ${src.to} (${stat.isDirectory() ? "dir" : `${stat.size} bytes`})`);
synced++;
}
console.log(`[sync-sources] OK (${synced} sources copied)`);
+5
View File
@@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 200 32" fill="none">
<rect x="2" y="6" width="20" height="20" rx="4" fill="#13715B"/>
<path d="M7 16l3.5 3.5L17 12" stroke="white" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round"/>
<text x="30" y="22" font-family="-apple-system, BlinkMacSystemFont, 'Inter', sans-serif" font-size="18" font-weight="600" fill="currentColor">Checkmate</text>
</svg>

After

Width:  |  Height:  |  Size: 424 B

+29
View File
@@ -0,0 +1,29 @@
---
// Renders next to the search/theme controls in Starlight's header.
// Adds a "Releases" link plus delegates to the default SocialIcons.
import Default from "@astrojs/starlight/components/SocialIcons.astro";
---
<a class="header-link" href="/releases">Releases</a>
<Default><slot /></Default>
<style>
.header-link {
color: var(--sl-color-white);
font-size: var(--sl-text-sm);
font-weight: 500;
text-decoration: none;
padding: 0.4rem 0.75rem;
border-radius: 0.375rem;
transition: background-color 0.15s ease;
}
.header-link:hover {
background-color: var(--sl-color-gray-5);
}
:global([data-theme="light"]) .header-link {
color: var(--sl-color-gray-6);
}
:global([data-theme="light"]) .header-link:hover {
background-color: var(--sl-color-gray-1);
}
</style>
+139
View File
@@ -0,0 +1,139 @@
---
interface Props {
version: string;
date: string;
headline: string;
highlights: string[];
isLatest?: boolean;
}
const { version, date, headline, highlights, isLatest = false } = Astro.props;
const formattedDate = new Date(date).toLocaleDateString("en-US", {
month: "long",
day: "numeric",
year: "numeric",
});
---
<article class="release-card">
<aside class="release-meta">
<time datetime={date}>{formattedDate}</time>
<span class="release-version">
{version}
{isLatest && <span class="latest-badge">Latest</span>}
</span>
</aside>
<div class="release-content">
<h2 class="release-headline">{headline}</h2>
<ul class="release-highlights">
{highlights.map((item) => <li>{item}</li>)}
</ul>
</div>
</article>
<style>
.release-card {
display: grid;
grid-template-columns: 200px 1fr;
gap: 2.5rem;
padding: 2rem 0;
border-bottom: 1px solid var(--sl-color-gray-5);
align-items: flex-start;
}
.release-card:last-child {
border-bottom: none;
}
.release-meta {
display: flex;
flex-direction: column;
gap: 0.4rem;
position: sticky;
top: 6rem;
}
.release-meta time {
color: var(--sl-color-gray-3);
font-size: var(--sl-text-sm);
font-feature-settings: "tnum";
}
.release-version {
display: inline-flex;
align-items: center;
gap: 0.5rem;
color: var(--sl-color-white);
font-weight: 600;
font-size: var(--sl-text-base);
font-variant-numeric: tabular-nums;
}
.latest-badge {
background: var(--sl-color-accent);
color: var(--sl-color-white);
font-size: 0.6875rem;
font-weight: 600;
letter-spacing: 0.04em;
text-transform: uppercase;
padding: 0.15rem 0.5rem;
border-radius: 0.25rem;
line-height: 1.2;
}
.release-content {
min-width: 0;
}
.release-headline {
margin: 0 0 1rem 0;
font-size: var(--sl-text-xl);
font-weight: 600;
color: var(--sl-color-white);
line-height: 1.3;
}
.release-highlights {
margin: 0;
padding-left: 1.25rem;
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.release-highlights li {
color: var(--sl-color-gray-2);
line-height: 1.55;
font-size: var(--sl-text-base);
}
.release-highlights li::marker {
color: var(--sl-color-accent);
}
:global([data-theme="light"]) .release-card {
border-bottom-color: var(--sl-color-gray-2);
}
:global([data-theme="light"]) .release-version,
:global([data-theme="light"]) .release-headline {
color: var(--sl-color-gray-6);
}
:global([data-theme="light"]) .release-highlights li {
color: var(--sl-color-gray-5);
}
@media (max-width: 768px) {
.release-card {
grid-template-columns: 1fr;
gap: 0.75rem;
padding: 1.5rem 0;
}
.release-meta {
position: static;
flex-direction: row;
align-items: center;
gap: 1rem;
}
}
</style>
+26
View File
@@ -0,0 +1,26 @@
import { defineCollection } from "astro:content";
import { glob } from "astro/loaders";
import { docsSchema } from "@astrojs/starlight/schema";
// Load markdown from a sibling folder so prose lives outside the Astro
// project tree. Contributors edit /docs/api-content/*.md without touching
// the build tool.
//
// Starlight's built-in docsLoader is hardcoded to src/content/docs/, so we
// use Astro's plain glob loader with a custom base and apply Starlight's
// schema on top. The collection name must remain "docs" — Starlight looks
// it up by that exact name.
export const collections = {
docs: defineCollection({
loader: glob({
// Vendored from /docs/api-content/ by scripts/sync-sources.mjs.
// This indirection keeps the build self-contained for Vercel
// (whose Root Directory setting limits the upload to docs/api/).
base: "./src/vendored/api-content",
// README is contributor-facing docs (how to build the site),
// not part of the published content.
pattern: ["**/[^_]*.{md,mdx}", "!README.md"],
}),
schema: docsSchema(),
}),
};
+168
View File
@@ -0,0 +1,168 @@
// Source of truth for the Releases timeline. Newest first.
// Each entry should focus on user-facing features — leave routine
// bumps, lint fixes, and dependency updates out.
export interface Release {
version: string;
date: string; // ISO 8601 yyyy-mm-dd
headline: string;
highlights: string[];
}
export const releases: Release[] = [
{
version: "v3.7.1",
date: "2026-04-29",
headline: "Status page themes and a polished incidents page",
highlights: [
"Pick a theme for your public status page: Refined, Modern, Bold, or Editorial",
"Choose between light, dark, or auto theme modes",
"Polished incidents page with better filtering and grouping",
"New empty state for the infrastructure network view",
"Remove-monitors confirmation modal in settings",
],
},
{
version: "v3.6.1",
date: "2026-04-23",
headline: "Twilio SMS notifications and incident comments",
highlights: [
"Send incident alerts via SMS using Twilio",
"Resolve incidents with a comment so context is preserved",
"Login page no longer shows the register tip when registration is closed",
"Removed TimescaleDB dependency for a leaner deployment",
"New monitor-lifecycle documentation",
],
},
{
version: "v3.5.1",
date: "2026-03-06",
headline: "Uptime over WebSockets, Globalping ping, and team member removal",
highlights: [
"Real-time uptime updates over WebSockets — no more polling delays",
"Use Globalping as the engine for distributed ping monitors",
"Admins can remove team members from the team settings page",
"Fixed dark-mode background and several theme contrast issues",
],
},
{
version: "v3.5.0",
date: "2026-03-04",
headline: "SWR data layer and tightened validation",
highlights: [
"Global SWR-based data fetching for snappier navigation and automatic revalidation",
"Stricter server-side validation across all monitor types",
"Env-var validation surfaces misconfiguration at startup, not at first failure",
"Logs page improvements for easier debugging",
],
},
{
version: "v3.4.0",
date: "2026-02-20",
headline: "Settings refactor and status page access fixes",
highlights: [
"Settings UI rebuilt for clarity and consistency",
"Fixed access rules so unpublished status pages remain admin-only",
"Several caching fixes for fresher data after configuration changes",
],
},
{
version: "v3.3",
date: "2026-01-27",
headline: "PageSpeed v2 details and a faster uptime details page",
highlights: [
"Rebuilt PageSpeed details view with deeper Core Web Vitals breakdown",
"v2 uptime details page with denser charts and faster loading",
"Recent-checks endpoint redesigned for lower latency",
"PageSpeed tooltips clarify what each metric means",
],
},
{
version: "v3.2.0",
date: "2025-09-18",
headline: "Notification bodies and uptime % charts",
highlights: [
"Customise the body of email, Slack, Discord, and webhook notifications",
"Toggle between absolute uptime and uptime % on monitor charts",
"Sliding-window threshold evaluation for more stable status decisions",
"Skip-maintenance notifications no longer fire during maintenance windows",
],
},
{
version: "v3.1",
date: "2025-08-20",
headline: "N-of-M alert thresholds and PagerDuty",
highlights: [
"Alert only after N consecutive failures out of M checks — fewer flaky alerts",
"PagerDuty notification channel",
"Streamlined maintenance window creation flow",
"Network service refactor for more reliable cross-region checks",
],
},
{
version: "v2.3",
date: "2025-07-08",
headline: "Resolvable incidents and a cleaner incidents UI",
highlights: [
"Mark incidents as resolved from the UI with a single click",
"Redesigned incidents page with better grouping",
"Move the status-page delete action out of harm's way",
"Configurable behavior when no notifications are attached to a monitor",
],
},
{
version: "v2.2",
date: "2025-07-07",
headline: "Notification management and i18n polish",
highlights: [
"Delete notification channels with confirmation",
"Sorted translation keys for easier maintenance",
"Many small i18n fixes across pages",
],
},
{
version: "v2.1",
date: "2025-05-13",
headline: "Maintenance window improvements and form fixes",
highlights: [
"Settings update flow rebuilt",
"Fixed monitor resume / pause behavior",
"Better PageSpeed reliability",
"Infrastructure monitor filtering improvements",
"Conditional alert rendering during maintenance windows",
],
},
{
version: "v2.0",
date: "2024-12-04",
headline: "Major UI refresh and password security",
highlights: [
"Major frontend redesign with a refreshed theme",
"Improved password validation and error messages",
"Many fixes to incident detail views, colors, and responsiveness",
],
},
{
version: "v1.1.0",
date: "2024-10-11",
headline: "Maintenance windows and JWT-expiry handling",
highlights: [
"Schedule maintenance windows so checks don't fire false incidents",
"Auto-logout users when their JWT expires",
"Centralised backend error handling",
"Initial maintenance-page feature",
],
},
{
version: "v1.0",
date: "2024-09-23",
headline: "First public release",
highlights: [
"HTTP, ping, port, and PageSpeed monitors",
"Incident detection with email and Discord notifications",
"Public status pages",
"Team management with role-based access",
"Open source under the AGPL — self-host the entire stack",
],
},
];
+59
View File
@@ -0,0 +1,59 @@
---
import StarlightPage from "@astrojs/starlight/components/StarlightPage.astro";
import ReleaseCard from "../components/ReleaseCard.astro";
import { releases } from "../data/releases";
---
<StarlightPage
frontmatter={{
title: "Releases",
description: "What's new in Checkmate — every release with the most important features.",
template: "splash",
pagefind: false,
}}
>
<div class="releases-intro">
<p>
Every meaningful Checkmate release, newest first. For the
complete commit history, see the
<a href="https://github.com/bluewave-labs/Checkmate/releases" rel="noopener">GitHub releases page</a>.
</p>
</div>
<div class="releases-timeline">
{releases.map((release, index) => (
<ReleaseCard
version={release.version}
date={release.date}
headline={release.headline}
highlights={release.highlights}
isLatest={index === 0}
/>
))}
</div>
</StarlightPage>
<style>
.releases-intro {
max-width: 60ch;
margin: 0 0 2rem 0;
color: var(--sl-color-gray-3);
font-size: var(--sl-text-base);
line-height: 1.55;
}
.releases-intro a {
color: var(--sl-color-accent-high);
text-decoration: underline;
text-underline-offset: 2px;
}
.releases-timeline {
max-width: 56rem;
margin: 0;
}
:global([data-theme="light"]) .releases-intro {
color: var(--sl-color-gray-5);
}
</style>
+23
View File
@@ -0,0 +1,23 @@
/* Checkmate brand colors layered on top of Starlight defaults. */
:root {
--sl-color-accent-low: #0e5a48;
--sl-color-accent: #13715b;
--sl-color-accent-high: #1a8a6f;
--sl-color-white: #ffffff;
--sl-color-gray-1: #eef2f0;
--sl-color-gray-2: #c9d3cf;
--sl-color-gray-3: #95a39d;
--sl-color-gray-4: #5c6d66;
--sl-color-gray-5: #3a4844;
--sl-color-gray-6: #2a3531;
--sl-color-black: #1a201e;
}
/* Light mode tweaks so the green accent reads against white. */
:root[data-theme="light"] {
--sl-color-accent-low: #d0e4dc;
--sl-color-accent: #13715b;
--sl-color-accent-high: #0e5a48;
--sl-color-text-accent: #0e5a48;
}
+5
View File
@@ -0,0 +1,5 @@
{
"extends": "astro/tsconfigs/strict",
"include": [".astro/types.d.ts", "**/*"],
"exclude": ["dist"]
}
+7
View File
@@ -0,0 +1,7 @@
{
"$schema": "https://openapi.vercel.sh/vercel.json",
"buildCommand": "npm run build",
"outputDirectory": "dist",
"framework": "astro",
"installCommand": "npm install"
}