Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
55 commits
Select commit Hold shift + click to select a range
ec129e3
Fix outdated links and add Prerequisites section
mayssagl Mar 23, 2026
eacbb22
feat(notifications): add Telegram notification channel (#2753)
LeC-D Mar 24, 2026
d1f1d31
fix(i18n): revert en.json to upstream format, keep only telegram keys
LeC-D Mar 24, 2026
fe242b1
fix: replace nested ternaries and remove type casts per review
LeC-D Mar 25, 2026
0ecc2e2
Implemement TimescaleDB class
ajhollid Mar 25, 2026
2422fcc
update gitignore
ajhollid Mar 25, 2026
ee74584
Add method to interface
ajhollid Mar 25, 2026
f43b758
add PG dependencies
ajhollid Mar 25, 2026
1defa52
update config
ajhollid Mar 25, 2026
f09f063
update error handler
ajhollid Mar 25, 2026
276b116
throw error on missing continent
ajhollid Mar 25, 2026
da6f03b
add teams repo
ajhollid Mar 25, 2026
9e686a2
add user repo
ajhollid Mar 25, 2026
f7db732
add status-pages repo
ajhollid Mar 25, 2026
51853cb
add settings repo
ajhollid Mar 25, 2026
729600a
recovery tokens repo
ajhollid Mar 25, 2026
3039fb9
nottificaitons repo
ajhollid Mar 25, 2026
dcdde84
monitors rep
ajhollid Mar 25, 2026
86acc0c
add monitor-stats repo
ajhollid Mar 25, 2026
0944dba
add maintenance-windows repo
ajhollid Mar 25, 2026
5ea86d8
add inivtes repo
ajhollid Mar 25, 2026
bc088c3
add incidents repo, rename old repo
ajhollid Mar 25, 2026
07957d3
geo-checks repo
ajhollid Mar 25, 2026
62e8643
checks repo, barrel export
ajhollid Mar 25, 2026
a864bc0
fix: run format and fix build errors
LeC-D Mar 25, 2026
40968c3
refactor(notifications): hoist default values, remove else block in u…
LeC-D Mar 25, 2026
4c94a7c
fix: resolve TypeScript build errors in notification form types
LeC-D Mar 25, 2026
e878799
fix TS errors
ajhollid Mar 26, 2026
be29cce
db types
ajhollid Mar 26, 2026
c558c4b
use ca tables
ajhollid Mar 26, 2026
2b2db2d
set default all for incident validation
ajhollid Mar 26, 2026
317d531
remove comments
ajhollid Mar 26, 2026
de3af64
migrations, gitignore
ajhollid Mar 26, 2026
13a7ae8
fix(notifications): remove type casts with exhaustive type narrowing …
LeC-D Mar 26, 2026
94ca989
feat(dlq): add DLQItem type definitions for dead letter queue
Br0wnHammer Mar 27, 2026
60b1d95
feat(dlq): add DLQItem Mongoose model with indexes for retry polling …
Br0wnHammer Mar 27, 2026
e6088fe
feat(dlq): add IDLQRepository interface for dead letter queue data ac…
Br0wnHammer Mar 27, 2026
214fc48
feat(dlq): add MongoDLQRepository with retry polling and grouped coun…
Br0wnHammer Mar 27, 2026
22d2626
Merge pull request #3441 from bluewave-labs/feat/timescaledb
ajhollid Mar 27, 2026
a895912
refactor(dlq): inline DLQItemDocumentBase into DLQItemDocument
Br0wnHammer Mar 27, 2026
15b0deb
Merge branch 'develop' into feat/dlq
Br0wnHammer Mar 27, 2026
04dfff4
fix(notifications): remove redundant defaultValue prop and fix incide…
LeC-D Mar 27, 2026
69b6955
Merge pull request #3439 from LeC-D/fix/telegram-notification-type
ajhollid Mar 27, 2026
8ab049a
Merge pull request #3447 from bluewave-labs/feat/dlq
ajhollid Mar 27, 2026
0d90083
Merge pull request #3455 from mayssagl/develop
ajhollid Apr 1, 2026
fb69e29
fix geocheck update logic
ajhollid Apr 1, 2026
4222d39
Merge pull request #3456 from bluewave-labs/fix/job-queue
ajhollid Apr 1, 2026
7272874
bump scheduler, consume scheduler events
ajhollid Apr 3, 2026
7636d2d
error -> warn on job fail, register listeners before starting scheduler
ajhollid Apr 3, 2026
42e82af
Merge pull request #3458 from bluewave-labs/feat/scheduler/1.9.1
ajhollid Apr 3, 2026
eb4a92c
fix: restructure and translate all i18n locale files
gorkem-bwl Apr 4, 2026
96f9360
style: format migration script with prettier
gorkem-bwl Apr 4, 2026
7c61ca4
Merge pull request #3461 from bluewave-labs/fix/i18n-locale-migration
ajhollid Apr 4, 2026
c53f4f4
Finished project
SaiBandarupalli001 Apr 10, 2026
9b2e6d2
edits
SaiBandarupalli001 Apr 10, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file added .DS_Store
Binary file not shown.
4 changes: 3 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,10 @@
.VSCodeCounter
*.sh
mongo
timescaledb
node_modules/
docs/architecture
docs/reviews
docs/todo
docs/frontend
docs/frontend
docs/timescale
8 changes: 6 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,10 @@ You can see the latest build of [Checkmate](https://checkmate-demo.bluewavelabs.

Usage instructions can be found [here](https://checkmate.so/docs).

## Prerequisites
- [Docker](https://www.docker.com/) installed
- [Git](https://git-scm.com/) installed

## Installation

See installation instructions in [Checkmate documentation portal](https://checkmate.so/docs).
Expand Down Expand Up @@ -80,7 +84,7 @@ You can see the memory footprint of MongoDB and Redis on the same server (398Mb
If you have any questions, suggestions or comments, you have several options:

- [Discord channel](https://discord.gg/NAb6H3UTjK) (preferred)
- [GitHub Discussions](https://github.com/bluewave-labs/bluewave-uptime/discussions) (we check here from time to time)
- [GitHub Discussions](https://github.com/bluewave-labs/Checkmate/discussions) (we check here from time to time)

Feel free to ask questions or share your ideas - we'd love to hear from you!

Expand Down Expand Up @@ -167,7 +171,7 @@ Here's how you can contribute:
<img src="https://contrib.rocks/image?repo=bluewave-labs/checkmate" />
</a>

[![Star History Chart](https://api.star-history.com/svg?repos=bluewave-labs/checkmate&type=Date)](https://star-history.com/#bluewave-labs/bluewave-uptime&Date)
[![Star History Chart](https://api.star-history.com/svg?repos=bluewave-labs/checkmate&type=Date)](https://star-history.com/#bluewave-labs/Checkmate&Date)

## Our sponsors

Expand Down
258 changes: 258 additions & 0 deletions client/scripts/migrate-locales.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,258 @@
/**
* Locale Migration Script
*
* Restructures all non-English locale files to match en.json's nested key structure.
* Recovers translations via safe key-matching strategies, uses English placeholders
* for unmatched keys.
*
* Usage: npx tsx client/scripts/migrate-locales.ts
*
* SAFETY: en.json is NEVER modified. Originals are backed up before overwriting.
*/

import fs from "fs";
import path from "path";

const LOCALES_DIR = path.resolve(import.meta.dirname, "../src/locales");
const BACKUP_DIR = path.join(LOCALES_DIR, "backup");
const REPORT_PATH = path.join(LOCALES_DIR, "migration-report.json");

// ── Helpers ──────────────────────────────────────────────────────────────────

/** Flatten a nested object into { "a.b.c": value } pairs */
function flattenKeys(obj: Record<string, unknown>, prefix = ""): Record<string, string> {
const result: Record<string, string> = {};
for (const [key, value] of Object.entries(obj)) {
const fullPath = prefix ? `${prefix}.${key}` : key;
if (typeof value === "object" && value !== null && !Array.isArray(value)) {
Object.assign(result, flattenKeys(value as Record<string, unknown>, fullPath));
} else {
result[fullPath] = String(value);
}
}
return result;
}

/** Set a value at a dot-separated path in a nested object */
function setNestedValue(
obj: Record<string, unknown>,
keyPath: string,
value: string
): void {
const parts = keyPath.split(".");
let current = obj;
for (let i = 0; i < parts.length - 1; i++) {
const part = parts[i];
if (!(part in current) || typeof current[part] !== "object") {
current[part] = {};
}
current = current[part] as Record<string, unknown>;
}
current[parts[parts.length - 1]] = value;
}

// ── Build mapping indexes from en.json ───────────────────────────────────────

function buildIndexes(enFlat: Record<string, string>) {
const enKeys = Object.keys(enFlat);

// last segment -> en key paths
const lastSegIndex: Record<string, string[]> = {};
// last 2 segments -> en key paths
const suffixIndex: Record<string, string[]> = {};

for (const keyPath of enKeys) {
const parts = keyPath.split(".");
const lastSeg = parts[parts.length - 1].toLowerCase();
if (!lastSegIndex[lastSeg]) lastSegIndex[lastSeg] = [];
lastSegIndex[lastSeg].push(keyPath);

if (parts.length >= 2) {
const suffix = parts.slice(-2).join(".").toLowerCase();
if (!suffixIndex[suffix]) suffixIndex[suffix] = [];
suffixIndex[suffix].push(keyPath);
}
}

return { lastSegIndex, suffixIndex };
}

// ── Mapping logic ────────────────────────────────────────────────────────────

type MatchStrategy = "exact" | "suffix-2" | "unique-name" | "english-placeholder";

interface KeyMatch {
enKey: string;
value: string;
strategy: MatchStrategy;
}

function buildMigratedLocale(
enFlat: Record<string, string>,
langFlat: Record<string, string>,
indexes: ReturnType<typeof buildIndexes>
): { matches: KeyMatch[]; result: Record<string, unknown> } {
const { lastSegIndex, suffixIndex } = indexes;
const matches: KeyMatch[] = [];
const result: Record<string, unknown> = {};

// Build reverse lookup: for each orphan key in lang, index by last segment and suffix
const langByLastSeg: Record<string, { key: string; value: string }[]> = {};
const langBySuffix: Record<string, { key: string; value: string }[]> = {};

for (const [langKey, langValue] of Object.entries(langFlat)) {
const parts = langKey.split(".");
const lastSeg = parts[parts.length - 1].toLowerCase();
if (!langByLastSeg[lastSeg]) langByLastSeg[lastSeg] = [];
langByLastSeg[lastSeg].push({ key: langKey, value: langValue });

if (parts.length >= 2) {
const suffix = parts.slice(-2).join(".").toLowerCase();
if (!langBySuffix[suffix]) langBySuffix[suffix] = [];
langBySuffix[suffix].push({ key: langKey, value: langValue });
}
}

for (const enKey of Object.keys(enFlat)) {
const enValue = enFlat[enKey];
const parts = enKey.split(".");
const lastSeg = parts[parts.length - 1].toLowerCase();
const suffix = parts.length >= 2 ? parts.slice(-2).join(".").toLowerCase() : null;

// Strategy 1: Exact path match
if (langFlat[enKey] !== undefined) {
const value = langFlat[enKey];
setNestedValue(result, enKey, value);
matches.push({ enKey, value, strategy: "exact" });
continue;
}

// Strategy 2: 2-segment suffix match (lang key suffix matches en key suffix, both unique)
if (suffix) {
const enCandidates = suffixIndex[suffix];
const langCandidates = langBySuffix[suffix];
if (
enCandidates &&
enCandidates.length === 1 &&
langCandidates &&
langCandidates.length === 1
) {
const value = langCandidates[0].value;
setNestedValue(result, enKey, value);
matches.push({ enKey, value, strategy: "suffix-2" });
continue;
}
}

// Strategy 3: Unique last-segment match
const enCandidates = lastSegIndex[lastSeg];
const langCandidates = langByLastSeg[lastSeg];
if (
enCandidates &&
enCandidates.length === 1 &&
langCandidates &&
langCandidates.length === 1
) {
const value = langCandidates[0].value;
setNestedValue(result, enKey, value);
matches.push({ enKey, value, strategy: "unique-name" });
continue;
}

// No match — use English placeholder
setNestedValue(result, enKey, enValue);
matches.push({ enKey, value: enValue, strategy: "english-placeholder" });
}

return { matches, result };
}

// ── Main ─────────────────────────────────────────────────────────────────────

function main() {
// Load en.json
const enPath = path.join(LOCALES_DIR, "en.json");
const en = JSON.parse(fs.readFileSync(enPath, "utf8"));
const enFlat = flattenKeys(en);
const enKeyCount = Object.keys(enFlat).length;
const indexes = buildIndexes(enFlat);

console.log(`English: ${enKeyCount} keys (source of truth — NOT modified)\n`);

// Find all non-English locale files
const localeFiles = fs
.readdirSync(LOCALES_DIR)
.filter(
(f: string) =>
f.endsWith(".json") && f !== "en.json" && f !== "migration-report.json"
);

if (localeFiles.length === 0) {
console.log("No locale files to migrate.");
return;
}

// Create backup directory
if (!fs.existsSync(BACKUP_DIR)) {
fs.mkdirSync(BACKUP_DIR, { recursive: true });
}

const report: Record<
string,
{
total: number;
exact: number;
suffix: number;
uniqueName: number;
placeholder: number;
}
> = {};

for (const file of localeFiles) {
const lang = file.replace(".json", "");
const filePath = path.join(LOCALES_DIR, file);

// Backup original
const backupPath = path.join(BACKUP_DIR, file);
fs.copyFileSync(filePath, backupPath);

// Load and flatten
const langData = JSON.parse(fs.readFileSync(filePath, "utf8"));
const langFlat = flattenKeys(langData);

// Migrate
const { matches, result } = buildMigratedLocale(enFlat, langFlat, indexes);

// Write migrated file
fs.writeFileSync(filePath, JSON.stringify(result, null, "\t") + "\n", "utf8");

// Compute stats
const stats = {
total: enKeyCount,
exact: matches.filter((m) => m.strategy === "exact").length,
suffix: matches.filter((m) => m.strategy === "suffix-2").length,
uniqueName: matches.filter((m) => m.strategy === "unique-name").length,
placeholder: matches.filter((m) => m.strategy === "english-placeholder").length,
};
report[lang] = stats;

const translated = stats.exact + stats.suffix + stats.uniqueName;
const pct = ((translated / enKeyCount) * 100).toFixed(1);
console.log(
`${lang.padEnd(8)} ${String(translated).padStart(4)} translated (${pct}%) | ` +
`exact: ${stats.exact} suffix: ${stats.suffix} name: ${stats.uniqueName} ` +
`placeholder: ${stats.placeholder}`
);
}

// Write report
fs.writeFileSync(REPORT_PATH, JSON.stringify(report, null, "\t") + "\n", "utf8");

const totalLangs = localeFiles.length;
console.log(
`\nMigrated ${totalLangs} locale files. Originals backed up to locales/backup/`
);
console.log(`Report written to ${REPORT_PATH}`);
}

main();
2 changes: 2 additions & 0 deletions client/src/Hooks/useMonitorForm.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ const getBaseDefaults = (data?: Monitor | null) => ({
description: data?.description || "",
interval: data?.interval || 60000,
notifications: data?.notifications || [],
escalationNotifications: data?.escalationNotifications || [],
escalateAfterMinutes: data?.escalateAfterMinutes ?? 5,
statusWindowSize: data?.statusWindowSize || 5,
statusWindowThreshold: data?.statusWindowThreshold || 60,
geoCheckEnabled: data?.geoCheckEnabled ?? false,
Expand Down
Loading