Compare commits
6
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7d0373989f | ||
|
|
ac51dfeab7 | ||
|
|
99a5aa1722 | ||
|
|
5791bdd5e7 | ||
|
|
f7514bc938 | ||
|
|
2bbaf7d1eb |
No files matched your search
@@ -1,258 +0,0 @@
|
||||
/**
|
||||
* 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();
|
||||
@@ -1,12 +1,13 @@
|
||||
import type { Monitor } from "@/Types/Monitor";
|
||||
import Stack from "@mui/material/Stack";
|
||||
import Typography from "@mui/material/Typography";
|
||||
import { PulseDot, Dot } from "@/Components/design-elements";
|
||||
import { PulseDot, Dot, StrategyBadge } from "@/Components/design-elements";
|
||||
import { getStatusColor, formatUrl } from "@/Utils/MonitorUtils";
|
||||
import { useTheme } from "@mui/material/styles";
|
||||
import prettyMilliseconds from "pretty-ms";
|
||||
import { typographyLevels } from "@/Utils/Theme/Palette";
|
||||
import useMediaQuery from "@mui/material/useMediaQuery";
|
||||
import { LAYOUT } from "@/Utils/Theme/constants";
|
||||
export const MonitorStatus = ({ monitor }: { monitor: Monitor }) => {
|
||||
const theme = useTheme();
|
||||
const isSmall = useMediaQuery(theme.breakpoints.down("md"));
|
||||
@@ -28,7 +29,7 @@ export const MonitorStatus = ({ monitor }: { monitor: Monitor }) => {
|
||||
<Stack
|
||||
direction="row"
|
||||
alignItems={"center"}
|
||||
gap={theme.spacing(4)}
|
||||
gap={theme.spacing(LAYOUT.XS)}
|
||||
>
|
||||
<PulseDot color={getStatusColor(monitor.status, theme)} />
|
||||
<Typography
|
||||
@@ -41,6 +42,12 @@ export const MonitorStatus = ({ monitor }: { monitor: Monitor }) => {
|
||||
>
|
||||
{formatUrl(monitor?.url)}
|
||||
</Typography>
|
||||
{monitor.type === "pagespeed" && monitor.strategy && (
|
||||
<>
|
||||
<Dot />
|
||||
<StrategyBadge strategy={monitor.strategy} />
|
||||
</>
|
||||
)}
|
||||
{!isSmall && (
|
||||
<>
|
||||
<Dot />
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
import Typography from "@mui/material/Typography";
|
||||
import { useTheme } from "@mui/material/styles";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import type { PageSpeedStrategy } from "@/Types/Monitor";
|
||||
import { SPACING } from "@/Utils/Theme/constants";
|
||||
|
||||
interface StrategyBadgeProps {
|
||||
strategy: PageSpeedStrategy | undefined;
|
||||
variant?: "caption" | "body2";
|
||||
}
|
||||
|
||||
export const StrategyBadge = ({ strategy, variant = "body2" }: StrategyBadgeProps) => {
|
||||
const theme = useTheme();
|
||||
const { t } = useTranslation();
|
||||
|
||||
if (!strategy) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Typography
|
||||
variant={variant}
|
||||
px={SPACING.XXL}
|
||||
py={SPACING.SM}
|
||||
borderRadius={theme.shape.borderRadius}
|
||||
bgcolor={theme.palette.secondary.light}
|
||||
color={theme.palette.text.primary}
|
||||
>
|
||||
{t(`pages.pageSpeed.strategy.${strategy}`)}
|
||||
</Typography>
|
||||
);
|
||||
};
|
||||
@@ -21,3 +21,4 @@ export * from "./SplitBox";
|
||||
export * from "./TextLink";
|
||||
export * from "./OfflineBanner";
|
||||
export * from "./Avatar";
|
||||
export * from "./StrategyBadge";
|
||||
@@ -1,5 +1,6 @@
|
||||
import { useMemo } from "react";
|
||||
import { monitorSchema, type MonitorFormData } from "@/Validation/monitor";
|
||||
import { DefaultPageSpeedStrategy } from "@/Types/Monitor";
|
||||
import type { Monitor, MonitorType } from "@/Types/Monitor";
|
||||
|
||||
interface UseMonitorFormOptions {
|
||||
@@ -88,6 +89,7 @@ export const useMonitorForm = ({
|
||||
...base,
|
||||
type: "pagespeed",
|
||||
url: data?.url || "",
|
||||
strategy: data?.strategy ?? DefaultPageSpeedStrategy,
|
||||
};
|
||||
break;
|
||||
case "hardware":
|
||||
|
||||
@@ -37,6 +37,7 @@ import {
|
||||
type MonitorType,
|
||||
type GamesMap,
|
||||
supportsGeoCheck,
|
||||
DefaultPageSpeedStrategy,
|
||||
} from "@/Types/Monitor";
|
||||
import type { Notification } from "@/Types/Notification";
|
||||
import type { MonitorFormData } from "@/Validation/monitor";
|
||||
@@ -51,6 +52,7 @@ interface GeneralSettingsConfig {
|
||||
showSecret: boolean;
|
||||
showGrpcServiceName: boolean;
|
||||
showIgnoreTls: boolean;
|
||||
showStrategy: boolean;
|
||||
}
|
||||
|
||||
const getGeneralSettingsConfig = (
|
||||
@@ -68,6 +70,7 @@ const getGeneralSettingsConfig = (
|
||||
showSecret: false,
|
||||
showGrpcServiceName: false,
|
||||
showIgnoreTls: false,
|
||||
showStrategy: false,
|
||||
},
|
||||
ping: {
|
||||
urlLabel: t("pages.createMonitor.form.general.option.host.label"),
|
||||
@@ -79,6 +82,7 @@ const getGeneralSettingsConfig = (
|
||||
showSecret: false,
|
||||
showGrpcServiceName: false,
|
||||
showIgnoreTls: false,
|
||||
showStrategy: false,
|
||||
},
|
||||
docker: {
|
||||
urlLabel: t("pages.createMonitor.form.general.option.container.label"),
|
||||
@@ -90,6 +94,7 @@ const getGeneralSettingsConfig = (
|
||||
showSecret: false,
|
||||
showGrpcServiceName: false,
|
||||
showIgnoreTls: false,
|
||||
showStrategy: false,
|
||||
},
|
||||
port: {
|
||||
urlLabel: t("pages.createMonitor.form.general.option.url.label"),
|
||||
@@ -101,6 +106,7 @@ const getGeneralSettingsConfig = (
|
||||
showSecret: false,
|
||||
showGrpcServiceName: false,
|
||||
showIgnoreTls: false,
|
||||
showStrategy: false,
|
||||
},
|
||||
game: {
|
||||
urlLabel: t("pages.createMonitor.form.general.option.url.label"),
|
||||
@@ -112,6 +118,7 @@ const getGeneralSettingsConfig = (
|
||||
showSecret: false,
|
||||
showGrpcServiceName: false,
|
||||
showIgnoreTls: false,
|
||||
showStrategy: false,
|
||||
},
|
||||
grpc: {
|
||||
urlLabel: t("pages.createMonitor.form.general.option.host.label"),
|
||||
@@ -123,6 +130,7 @@ const getGeneralSettingsConfig = (
|
||||
showSecret: false,
|
||||
showGrpcServiceName: true,
|
||||
showIgnoreTls: true,
|
||||
showStrategy: false,
|
||||
},
|
||||
pagespeed: {
|
||||
urlLabel: t("pages.createMonitor.form.general.option.url.label"),
|
||||
@@ -134,6 +142,7 @@ const getGeneralSettingsConfig = (
|
||||
showSecret: false,
|
||||
showGrpcServiceName: false,
|
||||
showIgnoreTls: false,
|
||||
showStrategy: true,
|
||||
},
|
||||
hardware: {
|
||||
urlLabel: t("pages.createMonitor.form.general.option.url.label"),
|
||||
@@ -145,6 +154,7 @@ const getGeneralSettingsConfig = (
|
||||
showSecret: true,
|
||||
showGrpcServiceName: false,
|
||||
showIgnoreTls: false,
|
||||
showStrategy: false,
|
||||
},
|
||||
websocket: {
|
||||
urlLabel: t("pages.createMonitor.form.general.option.wsUrl.label"),
|
||||
@@ -156,6 +166,7 @@ const getGeneralSettingsConfig = (
|
||||
showSecret: false,
|
||||
showGrpcServiceName: false,
|
||||
showIgnoreTls: true,
|
||||
showStrategy: false,
|
||||
},
|
||||
};
|
||||
return configs[type] || configs.http;
|
||||
@@ -382,6 +393,31 @@ const CreateMonitorPage = () => {
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Strategy field - only for pagespeed type */}
|
||||
{generalSettingsConfig.showStrategy && (
|
||||
<Controller
|
||||
name="strategy"
|
||||
control={control}
|
||||
render={({ field, fieldState }) => (
|
||||
<Select
|
||||
{...field}
|
||||
value={field.value ?? DefaultPageSpeedStrategy}
|
||||
fieldLabel={t(
|
||||
"pages.createMonitor.form.general.option.strategy.label"
|
||||
)}
|
||||
error={!!fieldState.error}
|
||||
>
|
||||
<MenuItem value="desktop">
|
||||
{t("pages.createMonitor.form.general.option.strategy.desktop")}
|
||||
</MenuItem>
|
||||
<MenuItem value="mobile">
|
||||
{t("pages.createMonitor.form.general.option.strategy.mobile")}
|
||||
</MenuItem>
|
||||
</Select>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Port field - only for port and game types */}
|
||||
{generalSettingsConfig.showPort && (
|
||||
<Controller
|
||||
|
||||
@@ -1,7 +1,13 @@
|
||||
import Stack from "@mui/material/Stack";
|
||||
import Box from "@mui/material/Box";
|
||||
import Typography from "@mui/material/Typography";
|
||||
import { Table, Pagination, StatusLabel } from "@/Components/design-elements";
|
||||
import {
|
||||
Table,
|
||||
Pagination,
|
||||
StatusLabel,
|
||||
StrategyBadge,
|
||||
} from "@/Components/design-elements";
|
||||
import { LAYOUT } from "@/Utils/Theme/constants";
|
||||
import { HistogramPageSpeed } from "@/Components/monitors";
|
||||
import { ArrowDown, ArrowUp } from "lucide-react";
|
||||
import type { Header } from "@/Components/design-elements/Table";
|
||||
@@ -179,7 +185,19 @@ export const PageSpeedMonitorsTable = ({
|
||||
</Stack>
|
||||
),
|
||||
render: (row) => {
|
||||
return row.name;
|
||||
return (
|
||||
<Stack
|
||||
direction="row"
|
||||
alignItems="center"
|
||||
gap={LAYOUT.XS}
|
||||
>
|
||||
<Typography>{row.name}</Typography>
|
||||
<StrategyBadge
|
||||
strategy={row.strategy}
|
||||
variant="caption"
|
||||
/>
|
||||
</Stack>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
|
||||
@@ -38,6 +38,10 @@ export type MonitorStatus = (typeof MonitorStatuses)[number];
|
||||
|
||||
export type MonitorMatchMethod = "equal" | "include" | "regex" | "";
|
||||
|
||||
export const PageSpeedStrategies = ["desktop", "mobile"] as const;
|
||||
export type PageSpeedStrategy = (typeof PageSpeedStrategies)[number];
|
||||
export const DefaultPageSpeedStrategy: PageSpeedStrategy = "desktop";
|
||||
|
||||
export interface Monitor {
|
||||
id: string;
|
||||
userId: string;
|
||||
@@ -72,6 +76,7 @@ export interface Monitor {
|
||||
selectedDisks: string[];
|
||||
gameId?: string;
|
||||
grpcServiceName?: string;
|
||||
strategy?: PageSpeedStrategy;
|
||||
group: string | null;
|
||||
geoCheckEnabled?: boolean;
|
||||
geoCheckLocations?: GeoContinent[];
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { z } from "zod";
|
||||
import { GeoContinents } from "@/Types/GeoCheck";
|
||||
import { PageSpeedStrategies } from "@/Types/Monitor";
|
||||
|
||||
// URL schema with custom error message
|
||||
const urlSchema = z.url({ message: "Please enter a valid URL" });
|
||||
@@ -89,6 +90,7 @@ const grpcSchema = baseSchema.extend({
|
||||
const pagespeedSchema = baseSchema.extend({
|
||||
type: z.literal("pagespeed"),
|
||||
url: urlSchema,
|
||||
strategy: z.enum(PageSpeedStrategies),
|
||||
});
|
||||
|
||||
// Hardware/Infrastructure monitor schema
|
||||
|
||||
+1085
-1282
File diff suppressed because it is too large.
Load diff
+1085
-1282
File diff suppressed because it is too large.
Load diff
+1107
-1272
File diff suppressed because it is too large.
Load diff
@@ -503,6 +503,11 @@
|
||||
"wsUrl": {
|
||||
"label": "WebSocket URL",
|
||||
"placeholder": "wss://example.com/socket"
|
||||
},
|
||||
"strategy": {
|
||||
"label": "Strategy",
|
||||
"desktop": "Desktop",
|
||||
"mobile": "Mobile"
|
||||
}
|
||||
},
|
||||
"title": "General settings",
|
||||
@@ -968,6 +973,10 @@
|
||||
}
|
||||
},
|
||||
"pageSpeed": {
|
||||
"strategy": {
|
||||
"desktop": "Desktop",
|
||||
"mobile": "Mobile"
|
||||
},
|
||||
"charts": {
|
||||
"common": {
|
||||
"cls": "Cumulative Layout Shift (CLS)",
|
||||
|
||||
+1085
-1282
File diff suppressed because it is too large.
Load diff
+1085
-1282
File diff suppressed because it is too large.
Load diff
+1107
-1272
File diff suppressed because it is too large.
Load diff
+1086
-1272
File diff suppressed because it is too large.
Load diff
+1107
-1272
File diff suppressed because it is too large.
Load diff
+1107
-1272
File diff suppressed because it is too large.
Load diff
+1107
-1272
File diff suppressed because it is too large.
Load diff
+1085
-1282
File diff suppressed because it is too large.
Load diff
+792
-1259
File diff suppressed because it is too large.
Load diff
+792
-1259
File diff suppressed because it is too large.
Load diff
+1099
-1272
File diff suppressed because it is too large.
Load diff
+1089
-1300
File diff suppressed because it is too large.
Load diff
@@ -3631,6 +3631,12 @@
|
||||
"type": "string",
|
||||
"default": ""
|
||||
},
|
||||
"strategy": {
|
||||
"type": "string",
|
||||
"enum": ["desktop", "mobile"],
|
||||
"default": "desktop",
|
||||
"description": "PageSpeed analysis strategy"
|
||||
},
|
||||
"selectedDisks": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
@@ -3737,6 +3743,11 @@
|
||||
"grpcServiceName": {
|
||||
"type": "string"
|
||||
},
|
||||
"strategy": {
|
||||
"type": "string",
|
||||
"enum": ["desktop", "mobile"],
|
||||
"description": "PageSpeed analysis strategy"
|
||||
},
|
||||
"selectedDisks": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
|
||||
Generated
+117
-102
@@ -41,7 +41,7 @@
|
||||
"ping": "0.4.4",
|
||||
"sharp": "0.33.5",
|
||||
"ssl-checker": "2.0.10",
|
||||
"super-simple-scheduler": "1.9.1",
|
||||
"super-simple-scheduler": "1.4.5",
|
||||
"swagger-ui-express": "5.0.1",
|
||||
"winston": "^3.13.0",
|
||||
"ws": "^8.19.0",
|
||||
@@ -801,20 +801,40 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@aws-sdk/xml-builder": {
|
||||
"version": "3.972.16",
|
||||
"resolved": "https://registry.npmjs.org/@aws-sdk/xml-builder/-/xml-builder-3.972.16.tgz",
|
||||
"integrity": "sha512-iu2pyvaqmeatIJLURLqx9D+4jKAdTH20ntzB6BFwjyN7V960r4jK32mx0Zf7YbtOYAbmbtQfDNuL60ONinyw7A==",
|
||||
"version": "3.972.10",
|
||||
"resolved": "https://registry.npmjs.org/@aws-sdk/xml-builder/-/xml-builder-3.972.10.tgz",
|
||||
"integrity": "sha512-OnejAIVD+CxzyAUrVic7lG+3QRltyja9LoNqCE/1YVs8ichoTbJlVSaZ9iSMcnHLyzrSNtvaOGjSDRP+d/ouFA==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@smithy/types": "^4.13.1",
|
||||
"fast-xml-parser": "5.5.8",
|
||||
"@smithy/types": "^4.13.0",
|
||||
"fast-xml-parser": "5.4.1",
|
||||
"tslib": "^2.6.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@aws-sdk/xml-builder/node_modules/fast-xml-parser": {
|
||||
"version": "5.4.1",
|
||||
"resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-5.4.1.tgz",
|
||||
"integrity": "sha512-BQ30U1mKkvXQXXkAGcuyUA/GA26oEB7NzOtsxCDtyu62sjGw5QraKFhx2Em3WQNjPw9PG6MQ9yuIIgkSDfGu5A==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/NaturalIntelligence"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"fast-xml-builder": "^1.0.0",
|
||||
"strnum": "^2.1.2"
|
||||
},
|
||||
"bin": {
|
||||
"fxparser": "src/cli/cli.js"
|
||||
}
|
||||
},
|
||||
"node_modules/@aws/lambda-invoke-store": {
|
||||
"version": "0.2.3",
|
||||
"resolved": "https://registry.npmjs.org/@aws/lambda-invoke-store/-/lambda-invoke-store-0.2.3.tgz",
|
||||
@@ -3835,9 +3855,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@smithy/types": {
|
||||
"version": "4.13.1",
|
||||
"resolved": "https://registry.npmjs.org/@smithy/types/-/types-4.13.1.tgz",
|
||||
"integrity": "sha512-787F3yzE2UiJIQ+wYW1CVg2odHjmaWLGksnKQHUrK/lYZSEcy1msuLVvxaR/sI2/aDe9U+TBuLsXnr3vod1g0g==",
|
||||
"version": "4.13.0",
|
||||
"resolved": "https://registry.npmjs.org/@smithy/types/-/types-4.13.0.tgz",
|
||||
"integrity": "sha512-COuLsZILbbQsdrwKQpkkpyep7lCsByxwj7m0Mg5v66/ZTyenlfBc40/QFQ5chO0YN/PNEH1Bi3fGtfXPnYNeDw==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
@@ -4942,9 +4962,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": {
|
||||
"version": "5.0.5",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.5.tgz",
|
||||
"integrity": "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==",
|
||||
"version": "5.0.4",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.4.tgz",
|
||||
"integrity": "sha512-h+DEnpVvxmfVefa4jFbCf5HdH5YMDXRsmKflpf1pILZWRFlTbJpxeU55nJl4Smt5HQaGzg1o6RHFPJaOqnmBDg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@@ -5885,9 +5905,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/brace-expansion": {
|
||||
"version": "1.1.13",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.13.tgz",
|
||||
"integrity": "sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w==",
|
||||
"version": "1.1.12",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
|
||||
"integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@@ -8132,9 +8152,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/fast-xml-builder": {
|
||||
"version": "1.1.4",
|
||||
"resolved": "https://registry.npmjs.org/fast-xml-builder/-/fast-xml-builder-1.1.4.tgz",
|
||||
"integrity": "sha512-f2jhpN4Eccy0/Uz9csxh3Nu6q4ErKxf0XIsasomfOihuSUa3/xw6w8dnOtCDgEItQFJG8KyXPzQXzcODDrrbOg==",
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/fast-xml-builder/-/fast-xml-builder-1.0.0.tgz",
|
||||
"integrity": "sha512-fpZuDogrAgnyt9oDDz+5DBz0zgPdPZz6D4IR7iESxRXElrlGTRkHJ9eEt+SACRJwT0FNFrt71DFQIUFBJfX/uQ==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
@@ -8142,31 +8162,7 @@
|
||||
"url": "https://github.com/sponsors/NaturalIntelligence"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"path-expression-matcher": "^1.1.3"
|
||||
}
|
||||
},
|
||||
"node_modules/fast-xml-parser": {
|
||||
"version": "5.5.8",
|
||||
"resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-5.5.8.tgz",
|
||||
"integrity": "sha512-Z7Fh2nVQSb2d+poDViM063ix2ZGt9jmY1nWhPfHBOK2Hgnb/OW3P4Et3P/81SEej0J7QbWtJqxO05h8QYfK7LQ==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/NaturalIntelligence"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"fast-xml-builder": "^1.1.4",
|
||||
"path-expression-matcher": "^1.2.0",
|
||||
"strnum": "^2.2.0"
|
||||
},
|
||||
"bin": {
|
||||
"fxparser": "src/cli/cli.js"
|
||||
}
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/fastq": {
|
||||
"version": "1.20.1",
|
||||
@@ -8307,9 +8303,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/flatted": {
|
||||
"version": "3.4.2",
|
||||
"resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.2.tgz",
|
||||
"integrity": "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==",
|
||||
"version": "3.3.3",
|
||||
"resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz",
|
||||
"integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==",
|
||||
"dev": true,
|
||||
"license": "ISC"
|
||||
},
|
||||
@@ -8756,9 +8752,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/glob/node_modules/brace-expansion": {
|
||||
"version": "2.0.3",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.3.tgz",
|
||||
"integrity": "sha512-MCV/fYJEbqx68aE58kv2cA/kiky1G8vux3OR6/jbS+jIMe/6fJWa0DTzJU7dqijOWYwHi1t29FlfYI9uytqlpA==",
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz",
|
||||
"integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"balanced-match": "^1.0.0"
|
||||
@@ -8858,9 +8854,9 @@
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/handlebars": {
|
||||
"version": "4.7.9",
|
||||
"resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.9.tgz",
|
||||
"integrity": "sha512-4E71E0rpOaQuJR2A3xDZ+GM1HyWYv1clR58tC8emQNeQe3RH7MAzSbat+V0wG78LQBo6m6bzSG/L4pBuCsgnUQ==",
|
||||
"version": "4.7.8",
|
||||
"resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.8.tgz",
|
||||
"integrity": "sha512-vafaFqs8MZkRrSX7sFVUdo3ap/eNiLnb4IakshzvP56X5Nr1iGKAIqdX6tMlm6HcNRIkr6AxO5jFEoJzzpT8aQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"minimist": "^1.2.5",
|
||||
@@ -10160,9 +10156,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/jest-util/node_modules/picomatch": {
|
||||
"version": "4.0.4",
|
||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz",
|
||||
"integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==",
|
||||
"version": "4.0.3",
|
||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
|
||||
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
@@ -10697,9 +10693,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/lodash": {
|
||||
"version": "4.18.1",
|
||||
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz",
|
||||
"integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==",
|
||||
"version": "4.17.23",
|
||||
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.23.tgz",
|
||||
"integrity": "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/lodash.camelcase": {
|
||||
@@ -11241,9 +11237,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/mjml-cli/node_modules/brace-expansion": {
|
||||
"version": "2.0.3",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.3.tgz",
|
||||
"integrity": "sha512-MCV/fYJEbqx68aE58kv2cA/kiky1G8vux3OR6/jbS+jIMe/6fJWa0DTzJU7dqijOWYwHi1t29FlfYI9uytqlpA==",
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz",
|
||||
"integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"balanced-match": "^1.0.0"
|
||||
@@ -12337,22 +12333,6 @@
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/path-expression-matcher": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/path-expression-matcher/-/path-expression-matcher-1.2.1.tgz",
|
||||
"integrity": "sha512-d7gQQmLvAKXKXE2GeP9apIGbMYKz88zWdsn/BN2HRWVQsDFdUY36WSLTY0Jvd4HWi7Fb30gQ62oAOzdgJA6fZw==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/NaturalIntelligence"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=14.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/path-is-absolute": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
|
||||
@@ -12389,9 +12369,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/path-to-regexp": {
|
||||
"version": "0.1.13",
|
||||
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.13.tgz",
|
||||
"integrity": "sha512-A/AGNMFN3c8bOlvV9RreMdrv7jsmF9XIfDeCd87+I8RNg6s78BhJxMu69NEMHBSJFxKidViTEdruRwEk/WIKqA==",
|
||||
"version": "0.1.12",
|
||||
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz",
|
||||
"integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/path-type": {
|
||||
@@ -12501,9 +12481,9 @@
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/picomatch": {
|
||||
"version": "2.3.2",
|
||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz",
|
||||
"integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==",
|
||||
"version": "2.3.1",
|
||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
|
||||
"integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8.6"
|
||||
@@ -14529,9 +14509,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/strnum": {
|
||||
"version": "2.2.2",
|
||||
"resolved": "https://registry.npmjs.org/strnum/-/strnum-2.2.2.tgz",
|
||||
"integrity": "sha512-DnR90I+jtXNSTXWdwrEy9FakW7UX+qUZg28gj5fk2vxxl7uS/3bpI4fjFYVmdK9etptYBPNkpahuQnEwhwECqA==",
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/strnum/-/strnum-2.2.0.tgz",
|
||||
"integrity": "sha512-Y7Bj8XyJxnPAORMZj/xltsfo55uOiyHcU2tnAVzHUnSJR/KsEX+9RoDeXEnsXtl/CX4fAcrt64gZ13aGaWPeBg==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
@@ -14558,12 +14538,25 @@
|
||||
}
|
||||
},
|
||||
"node_modules/super-simple-scheduler": {
|
||||
"version": "1.9.1",
|
||||
"resolved": "https://registry.npmjs.org/super-simple-scheduler/-/super-simple-scheduler-1.9.1.tgz",
|
||||
"integrity": "sha512-JSiEQLk7MsnlSkJ87uoyFKIseG2N27ikB0FSxlh25nnQHWC7R5Lrd0VcoLpwSBanyLzlAslhK4W2rQ0mCU8jCQ==",
|
||||
"version": "1.4.5",
|
||||
"resolved": "https://registry.npmjs.org/super-simple-scheduler/-/super-simple-scheduler-1.4.5.tgz",
|
||||
"integrity": "sha512-xHR3a+pLywY7HtwjWj/+OaANdEwAieQhckC/i0s6OoHtVJQ4+VLO1VfkKUbxp+oSROGXcaZet8PTGONVdKNUUw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"uuid": "11.1.0"
|
||||
"uuid": "11.1.0",
|
||||
"winston": "3.17.0"
|
||||
}
|
||||
},
|
||||
"node_modules/super-simple-scheduler/node_modules/is-stream": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz",
|
||||
"integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/super-simple-scheduler/node_modules/uuid": {
|
||||
@@ -14579,6 +14572,28 @@
|
||||
"uuid": "dist/esm/bin/uuid"
|
||||
}
|
||||
},
|
||||
"node_modules/super-simple-scheduler/node_modules/winston": {
|
||||
"version": "3.17.0",
|
||||
"resolved": "https://registry.npmjs.org/winston/-/winston-3.17.0.tgz",
|
||||
"integrity": "sha512-DLiFIXYC5fMPxaRg832S6F5mJYvePtmO5G9v9IgUFPhXm9/GkXarH/TUrBAVzhTCzAj9anE/+GjrgXp/54nOgw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@colors/colors": "^1.6.0",
|
||||
"@dabh/diagnostics": "^2.0.2",
|
||||
"async": "^3.2.3",
|
||||
"is-stream": "^2.0.0",
|
||||
"logform": "^2.7.0",
|
||||
"one-time": "^1.0.0",
|
||||
"readable-stream": "^3.4.0",
|
||||
"safe-stable-stringify": "^2.3.1",
|
||||
"stack-trace": "0.0.x",
|
||||
"triple-beam": "^1.3.0",
|
||||
"winston-transport": "^4.9.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 12.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/supports-color": {
|
||||
"version": "7.2.0",
|
||||
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
|
||||
@@ -14682,9 +14697,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/test-exclude/node_modules/brace-expansion": {
|
||||
"version": "2.0.3",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.3.tgz",
|
||||
"integrity": "sha512-MCV/fYJEbqx68aE58kv2cA/kiky1G8vux3OR6/jbS+jIMe/6fJWa0DTzJU7dqijOWYwHi1t29FlfYI9uytqlpA==",
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz",
|
||||
"integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@@ -14749,9 +14764,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/tinyglobby/node_modules/picomatch": {
|
||||
"version": "4.0.4",
|
||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz",
|
||||
"integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==",
|
||||
"version": "4.0.3",
|
||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
|
||||
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
@@ -15140,9 +15155,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/undici": {
|
||||
"version": "6.24.1",
|
||||
"resolved": "https://registry.npmjs.org/undici/-/undici-6.24.1.tgz",
|
||||
"integrity": "sha512-sC+b0tB1whOCzbtlx20fx3WgCXwkW627p4EA9uM+/tNNPkSS+eSEld6pAs9nDv7WbY1UUljBMYPtu9BCOrCWKA==",
|
||||
"version": "6.23.0",
|
||||
"resolved": "https://registry.npmjs.org/undici/-/undici-6.23.0.tgz",
|
||||
"integrity": "sha512-VfQPToRA5FZs/qJxLIinmU59u0r7LXqoJkCzinq3ckNJp3vKEh7jTWN589YQ5+aoAC/TGRLyJLCPKcLQbM8r9g==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=18.17"
|
||||
@@ -15810,9 +15825,9 @@
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/yaml": {
|
||||
"version": "2.8.3",
|
||||
"resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.3.tgz",
|
||||
"integrity": "sha512-AvbaCLOO2Otw/lW5bmh9d/WEdcDFdQp2Z2ZUH3pX9U2ihyUY0nvLv7J6TrWowklRGPYbB/IuIMfYgxaCPg5Bpg==",
|
||||
"version": "2.8.2",
|
||||
"resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.2.tgz",
|
||||
"integrity": "sha512-mplynKqc1C2hTVYxd0PU2xQAc22TI1vShAYGksCCfxbn/dFwnHTNi1bvYsBTkhdUNtGIf5xNOg938rrSSYvS9A==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"bin": {
|
||||
|
||||
+1
-1
@@ -56,7 +56,7 @@
|
||||
"ping": "0.4.4",
|
||||
"sharp": "0.33.5",
|
||||
"ssl-checker": "2.0.10",
|
||||
"super-simple-scheduler": "1.9.1",
|
||||
"super-simple-scheduler": "1.4.5",
|
||||
"swagger-ui-express": "5.0.1",
|
||||
"winston": "^3.13.0",
|
||||
"ws": "^8.19.0",
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
import type { Pool } from "pg";
|
||||
|
||||
export const addStrategyToMonitors = async (pool: Pool) => {
|
||||
await pool.query(`
|
||||
ALTER TABLE monitors
|
||||
ADD COLUMN IF NOT EXISTS strategy TEXT DEFAULT NULL;
|
||||
`);
|
||||
};
|
||||
|
||||
export const dropStrategyFromMonitors = async (pool: Pool) => {
|
||||
await pool.query(`
|
||||
ALTER TABLE monitors
|
||||
DROP COLUMN IF EXISTS strategy;
|
||||
`);
|
||||
};
|
||||
@@ -21,6 +21,7 @@ import { createStatusPages, dropStatusPages } from "./0018_create_status_pages.j
|
||||
import { createAppSettings, dropAppSettings } from "./0019_create_app_settings.js";
|
||||
import { createContinuousAggregates, dropContinuousAggregates } from "./0020_create_continuous_aggregates.js";
|
||||
import { createRetentionCompression, dropRetentionCompression } from "./0021_create_retention_compression.js";
|
||||
import { addStrategyToMonitors, dropStrategyFromMonitors } from "./0022_add_strategy_to_monitors.js";
|
||||
|
||||
const SERVICE_NAME = "TimescaleDB Migrations";
|
||||
|
||||
@@ -52,6 +53,7 @@ const migrations: MigrationEntry[] = [
|
||||
{ name: "0019_create_app_settings", up: createAppSettings, down: dropAppSettings },
|
||||
{ name: "0020_create_continuous_aggregates", up: createContinuousAggregates, down: dropContinuousAggregates },
|
||||
{ name: "0021_create_retention_compression", up: createRetentionCompression, down: dropRetentionCompression },
|
||||
{ name: "0022_add_strategy_to_monitors", up: addStrategyToMonitors, down: dropStrategyFromMonitors },
|
||||
];
|
||||
|
||||
const ensureMigrationsTable = async (pool: Pool) => {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Schema, model, Types } from "mongoose";
|
||||
import type { Monitor, MonitorMatchMethod, CheckSnapshot } from "@/types/monitor.js";
|
||||
import { MonitorTypes, MonitorStatuses } from "@/types/monitor.js";
|
||||
import { MonitorTypes, MonitorStatuses, PageSpeedStrategies } from "@/types/monitor.js";
|
||||
import type {
|
||||
CheckAudits,
|
||||
CheckCaptureInfo,
|
||||
@@ -330,6 +330,10 @@ const MonitorSchema = new Schema<MonitorDocument>(
|
||||
type: String,
|
||||
default: "",
|
||||
},
|
||||
strategy: {
|
||||
type: String,
|
||||
enum: PageSpeedStrategies,
|
||||
},
|
||||
group: {
|
||||
type: String,
|
||||
trim: true,
|
||||
|
||||
@@ -386,6 +386,7 @@ class MongoMonitorsRepository implements IMonitorsRepository {
|
||||
selectedDisks: doc.selectedDisks ?? [],
|
||||
gameId: doc.gameId ?? undefined,
|
||||
grpcServiceName: doc.grpcServiceName ?? undefined,
|
||||
strategy: doc.strategy ?? undefined,
|
||||
group: doc.group ?? null,
|
||||
recentChecks: (doc.recentChecks ?? []).map((check: CheckSnapshotDocument) => this.toCheckSnapshot(check)),
|
||||
geoCheckEnabled: doc.geoCheckEnabled ?? false,
|
||||
@@ -445,6 +446,7 @@ class MongoMonitorsRepository implements IMonitorsRepository {
|
||||
selectedDisks: doc.selectedDisks ?? [],
|
||||
gameId: doc.gameId ?? undefined,
|
||||
grpcServiceName: doc.grpcServiceName ?? undefined,
|
||||
strategy: doc.strategy ?? undefined,
|
||||
group: doc.group ?? null,
|
||||
recentChecks: (doc.recentChecks ?? []).map((check: CheckSnapshotDocument) => this.toCheckSnapshot(check)),
|
||||
geoCheckEnabled: doc.geoCheckEnabled ?? false,
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import type { Pool } from "pg";
|
||||
import type { Monitor, MonitorsSummary, MonitorStatus, MonitorType, MonitorMatchMethod, GeoContinent } from "@/types/monitor.js";
|
||||
import type { Monitor, MonitorsSummary, MonitorStatus, MonitorType, MonitorMatchMethod, GeoContinent, PageSpeedStrategy } from "@/types/monitor.js";
|
||||
|
||||
import type { IMonitorsRepository, TeamQueryConfig, SummaryConfig } from "./IMonitorsRepository.js";
|
||||
import { AppError } from "@/utils/AppError.js";
|
||||
|
||||
@@ -36,6 +37,7 @@ interface MonitorRow {
|
||||
selected_disks: string[] | null;
|
||||
game_id: string | null;
|
||||
grpc_service_name: string | null;
|
||||
strategy: PageSpeedStrategy | null;
|
||||
monitor_group: string | null;
|
||||
geo_check_enabled: boolean;
|
||||
geo_check_locations: GeoContinent[] | null;
|
||||
@@ -49,7 +51,7 @@ const MONITOR_COLUMNS = `id, user_id, team_id, name, description, type, status,
|
||||
interval_ms, is_active, status_window, status_window_size, status_window_threshold, uptime_percentage,
|
||||
cpu_alert_threshold, cpu_alert_counter, memory_alert_threshold, memory_alert_counter,
|
||||
disk_alert_threshold, disk_alert_counter, temp_alert_threshold, temp_alert_counter, selected_disks,
|
||||
game_id, grpc_service_name, monitor_group, geo_check_enabled, geo_check_locations, geo_check_interval_ms,
|
||||
game_id, grpc_service_name, strategy, monitor_group, geo_check_enabled, geo_check_locations, geo_check_interval_ms,
|
||||
created_at, updated_at`;
|
||||
|
||||
export class TimescaleMonitorsRepository implements IMonitorsRepository {
|
||||
@@ -62,8 +64,8 @@ export class TimescaleMonitorsRepository implements IMonitorsRepository {
|
||||
interval_ms, is_active, status_window, status_window_size, status_window_threshold,
|
||||
cpu_alert_threshold, cpu_alert_counter, memory_alert_threshold, memory_alert_counter,
|
||||
disk_alert_threshold, disk_alert_counter, temp_alert_threshold, temp_alert_counter, selected_disks,
|
||||
game_id, grpc_service_name, monitor_group, geo_check_enabled, geo_check_locations, geo_check_interval_ms)
|
||||
VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14,$15,$16,$17,$18,$19,$20,$21,$22,$23,$24,$25,$26,$27,$28,$29,$30,$31,$32,$33,$34)
|
||||
game_id, grpc_service_name, strategy, monitor_group, geo_check_enabled, geo_check_locations, geo_check_interval_ms)
|
||||
VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14,$15,$16,$17,$18,$19,$20,$21,$22,$23,$24,$25,$26,$27,$28,$29,$30,$31,$32,$33,$34,$35)
|
||||
RETURNING ${MONITOR_COLUMNS}`,
|
||||
[
|
||||
userId,
|
||||
@@ -96,6 +98,7 @@ export class TimescaleMonitorsRepository implements IMonitorsRepository {
|
||||
monitor.selectedDisks ?? [],
|
||||
monitor.gameId ?? null,
|
||||
monitor.grpcServiceName ?? null,
|
||||
monitor.strategy ?? null,
|
||||
monitor.group ?? null,
|
||||
monitor.geoCheckEnabled ?? false,
|
||||
monitor.geoCheckLocations ?? [],
|
||||
@@ -594,6 +597,7 @@ export class TimescaleMonitorsRepository implements IMonitorsRepository {
|
||||
["selectedDisks", "selected_disks"],
|
||||
["gameId", "game_id"],
|
||||
["grpcServiceName", "grpc_service_name"],
|
||||
["strategy", "strategy"],
|
||||
["group", "monitor_group"],
|
||||
["geoCheckEnabled", "geo_check_enabled"],
|
||||
["geoCheckLocations", "geo_check_locations"],
|
||||
@@ -1033,6 +1037,7 @@ export class TimescaleMonitorsRepository implements IMonitorsRepository {
|
||||
selectedDisks: row.selected_disks ?? [],
|
||||
gameId: row.game_id ?? undefined,
|
||||
grpcServiceName: row.grpc_service_name ?? undefined,
|
||||
strategy: row.strategy ?? undefined,
|
||||
group: row.monitor_group,
|
||||
geoCheckEnabled: row.geo_check_enabled,
|
||||
geoCheckLocations: row.geo_check_locations ?? [],
|
||||
|
||||
@@ -73,91 +73,22 @@ export class SuperSimpleQueue implements ISuperSimpleQueue {
|
||||
return SuperSimpleQueue.SERVICE_NAME;
|
||||
}
|
||||
|
||||
private registerListeners = () => {
|
||||
this.scheduler.on("scheduler:start", () => {
|
||||
this.logger.info({
|
||||
message: "Scheduler started",
|
||||
service: SERVICE_NAME,
|
||||
});
|
||||
});
|
||||
|
||||
this.scheduler.on("scheduler:stop", () => {
|
||||
this.logger.info({
|
||||
message: "Scheduler stopped",
|
||||
service: SERVICE_NAME,
|
||||
});
|
||||
});
|
||||
|
||||
this.scheduler.on("scheduler:error", (error) => {
|
||||
this.logger.error({
|
||||
message: `Scheduler error: ${error instanceof Error ? error.message : String(error)}`,
|
||||
service: SERVICE_NAME,
|
||||
stack: error instanceof Error ? error.stack : undefined,
|
||||
});
|
||||
});
|
||||
|
||||
this.scheduler.on("job:abort", (job, reason) => {
|
||||
this.logger.warn({
|
||||
message: `${job.id} aborted: ${reason}`,
|
||||
service: SERVICE_NAME,
|
||||
});
|
||||
});
|
||||
|
||||
this.scheduler.on("job:attempt", (job, attempt) => {
|
||||
this.logger.debug({
|
||||
message: `${job.id} attempt ${attempt}`,
|
||||
service: SERVICE_NAME,
|
||||
});
|
||||
});
|
||||
|
||||
this.scheduler.on("job:complete", (job) => {
|
||||
this.logger.debug({
|
||||
message: `${job.id} completed successfully`,
|
||||
service: SERVICE_NAME,
|
||||
});
|
||||
});
|
||||
|
||||
this.scheduler.on("job:exhausted", (job, error) => {
|
||||
this.logger.error({
|
||||
message: `${job.id} exhausted all retries: ${error instanceof Error ? error.message : String(error)}`,
|
||||
service: SERVICE_NAME,
|
||||
stack: error instanceof Error ? error.stack : undefined,
|
||||
});
|
||||
});
|
||||
|
||||
this.scheduler.on("job:fail", (job, error, attempt) => {
|
||||
this.logger.warn({
|
||||
message: `${job.id} failed on attempt ${attempt}: ${error instanceof Error ? error.message : String(error)}`,
|
||||
service: SERVICE_NAME,
|
||||
stack: error instanceof Error ? error.stack : undefined,
|
||||
});
|
||||
});
|
||||
|
||||
this.scheduler.on("job:start", (job) => {
|
||||
this.logger.debug({
|
||||
message: `${job.id} started`,
|
||||
service: SERVICE_NAME,
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
static async create(logger: ILogger, helper: ISuperSimpleQueueHelper, monitorsRepository: IMonitorsRepository) {
|
||||
const scheduler = new Scheduler({
|
||||
// storeType: "mongo",
|
||||
// storeType: "redis",
|
||||
logLevel: "debug",
|
||||
// dbUri: envSettings.dbConnectionString,
|
||||
});
|
||||
const instance = new SuperSimpleQueue(logger, helper, monitorsRepository, scheduler);
|
||||
await instance.init();
|
||||
|
||||
return instance;
|
||||
}
|
||||
|
||||
init = async () => {
|
||||
try {
|
||||
this.registerListeners();
|
||||
|
||||
this.scheduler.start();
|
||||
|
||||
this.scheduler.addTemplate("monitor-job", this.helper.getHeartbeatJob());
|
||||
this.scheduler.addTemplate("geo-check-job", this.helper.getHeartbeatGeoJob());
|
||||
this.scheduler.addTemplate("cleanup-orphaned", this.helper.getCleanupOrphanedJob());
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { IStatusProvider } from "@/service/infrastructure/network/IStatusProvider.js";
|
||||
import { MonitorStatusResponse, PageSpeedStatusPayload } from "@/types/network.js";
|
||||
import { Monitor, MonitorType } from "@/types/monitor.js";
|
||||
import { Monitor, MonitorType, DefaultPageSpeedStrategy } from "@/types/monitor.js";
|
||||
import { HttpProvider } from "@/service/infrastructure/network/HttpProvider.js";
|
||||
import { ISettingsService } from "@/service/system/settingsService.js";
|
||||
import { ILogger } from "@/utils/logger.js";
|
||||
@@ -19,14 +19,15 @@ export class PageSpeedProvider implements IStatusProvider<PageSpeedStatusPayload
|
||||
}
|
||||
|
||||
async handle(monitor: Monitor): Promise<MonitorStatusResponse<PageSpeedStatusPayload>> {
|
||||
const { url } = monitor;
|
||||
const { url, strategy } = monitor;
|
||||
try {
|
||||
if (!url) throw new Error("URL is required for PageSpeed monitor");
|
||||
const dbSettings = await this.settingsService.getDBSettings();
|
||||
const apiKey = dbSettings?.pagespeedApiKey;
|
||||
const resolvedStrategy = strategy ?? DefaultPageSpeedStrategy;
|
||||
let pageSpeedUrl = `https://pagespeedonline.googleapis.com/pagespeedonline/v5/runPagespeed?url=${encodeURIComponent(
|
||||
url
|
||||
)}&category=seo&category=accessibility&category=best-practices&category=performance`;
|
||||
)}&strategy=${encodeURIComponent(resolvedStrategy)}&category=seo&category=accessibility&category=best-practices&category=performance`;
|
||||
|
||||
if (apiKey) {
|
||||
pageSpeedUrl += `&key=${apiKey}`;
|
||||
|
||||
@@ -249,6 +249,9 @@ export class StatusService implements IStatusService {
|
||||
};
|
||||
}
|
||||
|
||||
// With a full window, a single raw check must not change monitor.status; only the sliding-window threshold can trigger a transition.
|
||||
newStatus = monitor.status;
|
||||
|
||||
// Check if threshold has been met
|
||||
const failures = monitor.statusWindow.filter((s) => s === false).length;
|
||||
const failureRate = (failures / monitor.statusWindow.length) * 100;
|
||||
|
||||
@@ -6,6 +6,10 @@ export type { GeoContinent } from "@/types/geoCheck.js";
|
||||
export const MonitorTypes = ["http", "ping", "pagespeed", "hardware", "docker", "port", "game", "grpc", "websocket", "unknown"] as const;
|
||||
export type MonitorType = (typeof MonitorTypes)[number];
|
||||
|
||||
export const PageSpeedStrategies = ["desktop", "mobile"] as const;
|
||||
export type PageSpeedStrategy = (typeof PageSpeedStrategies)[number];
|
||||
export const DefaultPageSpeedStrategy: PageSpeedStrategy = "desktop";
|
||||
|
||||
export const GeoCheckSupportedTypes: readonly MonitorType[] = ["http", "ping"] as const;
|
||||
export const supportsGeoCheck = (type: MonitorType): boolean => GeoCheckSupportedTypes.includes(type);
|
||||
|
||||
@@ -49,6 +53,7 @@ export interface Monitor {
|
||||
selectedDisks: string[];
|
||||
gameId?: string;
|
||||
grpcServiceName?: string;
|
||||
strategy?: PageSpeedStrategy;
|
||||
group: string | null;
|
||||
geoCheckEnabled?: boolean;
|
||||
geoCheckLocations?: GeoContinent[];
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { z } from "zod";
|
||||
import { booleanCoercion } from "./shared.js";
|
||||
import { GeoContinents } from "@/types/geoCheck.js";
|
||||
import { MonitorMatchMethods, MonitorTypes } from "@/types/monitor.js";
|
||||
import { MonitorMatchMethods, MonitorTypes, PageSpeedStrategies, DefaultPageSpeedStrategy } from "@/types/monitor.js";
|
||||
|
||||
export const getMonitorByIdParamValidation = z.object({
|
||||
monitorId: z.string().min(1, "Monitor ID is required"),
|
||||
@@ -73,6 +73,7 @@ export const createMonitorBodyValidation = z.object({
|
||||
matchMethod: z.union([z.enum(MonitorMatchMethods), z.literal("")]).optional(),
|
||||
gameId: z.union([z.string(), z.literal("")]).optional(),
|
||||
grpcServiceName: z.union([z.string(), z.literal("")]).default(""),
|
||||
strategy: z.enum(PageSpeedStrategies).optional(),
|
||||
selectedDisks: z.array(z.string()).optional(),
|
||||
group: z.union([z.string().max(50).trim(), z.null(), z.literal("")]).optional(),
|
||||
geoCheckEnabled: z.boolean().optional(),
|
||||
@@ -102,6 +103,7 @@ export const editMonitorBodyValidation = z.object({
|
||||
tempAlertThreshold: z.number().optional(),
|
||||
gameId: z.union([z.string(), z.literal("")]).optional(),
|
||||
grpcServiceName: z.union([z.string(), z.literal("")]).optional(),
|
||||
strategy: z.enum(PageSpeedStrategies).optional(),
|
||||
selectedDisks: z.array(z.string()).optional(),
|
||||
group: z.union([z.string().max(50).trim(), z.null(), z.literal("")]).optional(),
|
||||
geoCheckEnabled: z.boolean().optional(),
|
||||
@@ -156,6 +158,7 @@ const importedMonitorSchema = z.object({
|
||||
selectedDisks: z.array(z.string()).default([]),
|
||||
gameId: z.union([z.string(), z.literal("")]).optional(),
|
||||
grpcServiceName: z.union([z.string(), z.literal("")]).default(""),
|
||||
strategy: z.enum(PageSpeedStrategies).default(DefaultPageSpeedStrategy),
|
||||
group: z.union([z.string().max(50).trim(), z.null()]).default(null),
|
||||
geoCheckEnabled: z.boolean().default(false),
|
||||
geoCheckLocations: z.array(z.enum(GeoContinents)).default([]),
|
||||
|
||||
Reference in new issue
Block a user