feat: i18n added french #179, removed github dependency, clean up old code, simpler i18n file

This commit is contained in:
Raj Nandan Sharma
2025-01-13 10:57:12 +05:30
parent f28c4e96c5
commit f827bd4aeb
23 changed files with 489 additions and 1623 deletions
+1 -1
View File
@@ -2,7 +2,7 @@
import dotenv from "dotenv";
dotenv.config();
const databaseURL = process.env.DATABASE_URL || "sqlite://./database/kener.s2qlite.db";
const databaseURL = process.env.DATABASE_URL || "sqlite://./database/kener.sqlite.db";
const databaseURLParts = databaseURL.split("://");
const databaseType = databaseURLParts[0];
+21 -7
View File
@@ -1,8 +1,10 @@
<script>
import moment from "moment";
import * as Accordion from "$lib/components/ui/accordion";
import { l } from "$lib/i18n/client";
export let incident;
export let index;
export let lang;
let startTime = incident.start_date_time;
// let startTime = moment(incident.incident_start_time * 1000).format("MMMM Do YYYY, h:mm:ss a");
let endTime = parseInt(new Date() / 1000);
@@ -28,7 +30,7 @@
<p
class="scroll-m-20 text-xs font-semibold leading-5 tracking-normal badge-{incident.state}"
>
{incident.state}
{l(lang, incident.state)}
</p>
<p class="scroll-m-20 text-lg font-medium tracking-tight">
{incident.title}
@@ -39,19 +41,29 @@
>
{#if !isFuture && incident.state != "RESOLVED"}
<span>
Started about {startedAt} ago, still ongoing
{l(lang, "Started about %startedAt ago, still ongoing", {
startedAt
})}
</span>
{:else if !isFuture && incident.state == "RESOLVED"}
<span>
Started about {startedAt} ago, lasted for about {lastedFor}
{l(
lang,
"Started about %startedAt ago, lasted for about %lastedFor",
{ startedAt, lastedFor }
)}
</span>
{:else if isFuture && incident.state != "RESOLVED"}
<span>
Starts in {startedAt}
{l(lang, "Starts in %startedAt", { startedAt })}
</span>
{:else if isFuture && incident.state == "RESOLVED"}
<span>
Starts in {startedAt}, will last for about {lastedFor}
{l(
lang,
"Starts in %startedAt, will last for about %lastedFor",
{ startedAt, lastedFor }
)}
</span>
{/if}
</p>
@@ -90,7 +102,7 @@
<div
class="absolute top-0 w-28 -translate-x-32 rounded border bg-secondary px-1.5 py-1 text-center text-xs font-semibold"
>
{comment.state}
{l(lang, comment.state)}
</div>
<time
class=" mb-1 text-sm font-medium leading-none text-muted-foreground"
@@ -107,7 +119,9 @@
{/each}
</ol>
{:else}
<p class="text-sm font-medium">No Updates Yet</p>
<p class="text-sm font-medium">
{l(lang, "No Updates Yet")}
</p>
{/if}
</div>
</Accordion.Content>
-224
View File
@@ -1,224 +0,0 @@
<script>
import * as Card from "$lib/components/ui/card";
import { Separator } from "$lib/components/ui/separator";
import moment from "moment";
import { Button } from "$lib/components/ui/button";
import { Badge } from "$lib/components/ui/badge";
import { ChevronDown, Info, CalendarCheck, Hammer } from "lucide-svelte";
import * as Collapsible from "$lib/components/ui/collapsible";
import { l } from "$lib/i18n/client";
import axios from "axios";
import { Skeleton } from "$lib/components/ui/skeleton";
import { base } from "$app/paths";
import { tooltipAction } from "svelte-legos";
export let incident;
export let variant = "title+body+comments+monitor";
export let state = "open";
export let monitor;
export let lang;
const StatusObj = {
UP: "api-up",
DEGRADED: "api-degraded",
DOWN: "api-down",
NO_DATA: "api-nodata"
};
let blinker = "bg-transparent";
let incidentPriority = "";
let incidentDuration = 0;
if (incident.labels.includes("incident-down")) {
blinker = "bg-red-500";
incidentPriority = "DOWN";
} else if (incident.labels.includes("incident-degraded")) {
blinker = "bg-yellow-500";
incidentPriority = "DEGRADED";
}
let incidentState = incident.state;
let incidentClosedAt = incident.incident_end_time;
let incidentCreatedAt = incident.incident_start_time;
let incidentMessage = "";
if (!!incidentClosedAt && !!incidentCreatedAt) {
//incidentDuration between closed_at and created_at
incidentDuration = moment(incidentClosedAt * 1000)
.add(1, "minutes")
.diff(moment(incidentCreatedAt * 1000), "minutes");
} else if (!!incidentCreatedAt) {
//incidentDuration between now and created_at
incidentDuration = moment().diff(moment(incidentCreatedAt * 1000), "minutes");
}
//find a replace /\[start_datetime:(\d+)\]/ empty in incident.body
//find a replace /\[end_datetime:(\d+)\]/ empty in incident.body
incident.body = incident.body.replace(/\[start_datetime:(\d+)\]/g, "");
incident.body = incident.body.replace(/\[end_datetime:(\d+)\]/g, "");
//fetch comments
incident.comments = [];
let commentsLoading = true;
function getComments() {
if (incident.comments.length > 0) return;
if (commentsLoading === false) return;
axios
.get(`${base}/incident/${incident.number}/comments`)
.then((response) => {
incident.comments = response.data;
commentsLoading = false;
})
.catch((error) => {
// console.log(error);
});
}
$: {
if (state == "open") {
getComments();
}
}
</script>
<div class="incident-div relative mb-8 grid w-full grid-cols-3 gap-4">
<div class="col-span-3">
<Card.Root>
<Card.Header class="pb-2 pt-3">
<Card.Title class="relative mb-0">
{#if incidentPriority != "" && incidentDuration > 0}
<p class="absolute -top-8 -translate-y-1 leading-10">
<Badge
class="-ml-4 bg-card text-xs font-semibold text-[rgba(0,0,0,.6)] text-{StatusObj[
incidentPriority
]} "
>
{incidentPriority} for {incidentDuration} Minute{incidentDuration >
1
? "s"
: ""}
</Badge>
</p>
{/if}
{#if monitor.image}
<img
src={monitor.image.startsWith("/")
? base + monitor.image
: monitor.image}
class="absolute left-0 top-1 inline h-5 w-5"
alt=""
srcset=""
/>
{/if}
<div class="">
<div class="scroll-m-20 text-xl font-medium tracking-tight">
{#if variant.includes("monitor")}
{monitor.name} -
{/if}
{#if variant.includes("title")}
{incident.title}
{/if}
</div>
</div>
{#if incidentState == "open"}
<span
class="absolute -left-[26px] -top-[16px] inline-flex h-[8px] w-[8px] animate-ping rounded-full {blinker} opacity-75"
></span>
{/if}
{#if variant.includes("body") || variant.includes("comments")}
<div class="toggle absolute right-4 {state}">
<Button
variant="outline"
class="h-6 w-6 rounded-full"
size="icon"
on:click={() => {
state = state == "open" ? "close" : "open";
}}
>
<ChevronDown class="text-muted-foreground" size={12} />
</Button>
</div>
{/if}
</Card.Title>
<Card.Description class="-mt-4 text-xs ">
{moment(incidentCreatedAt * 1000).format("MMMM Do YYYY, h:mm:ss a")}
<p class="mt-0 flex gap-2 leading-8">
{#if incident.labels.includes("identified")}
<span
class="tag-identified-text"
title={l(lang, "incident.identified")}
>
<Info class="inline h-4 w-4" />
</span>
{/if}
{#if incident.labels.includes("resolved")}
<span class="tag-resolved-text" title={l(lang, "incident.resolved")}>
<CalendarCheck class="inline h-4 w-4" />
</span>
{/if}
{#if incident.labels.includes("maintenance")}
<span
class="tag-maintenance-text"
title={l(lang, "incident.maintenance")}
>
<Hammer class="inline h-4 w-4" />
</span>
{/if}
</p>
</Card.Description>
</Card.Header>
{#if (variant.includes("body") || variant.includes("comments")) && state == "open"}
<Card.Content class="px-7">
{#if variant.includes("body")}
<div
class="prose prose-stone max-w-none text-sm dark:prose-invert prose-code:rounded prose-code:px-[0.3rem] prose-code:py-[0.2rem] prose-code:font-mono prose-code:text-sm"
>
{@html incident.body}
</div>
{/if}
{#if variant.includes("comments") && incident.comments?.length > 0}
<div class="ml-4 mt-8 px-4">
<ol class="relative border-s border-secondary">
{#each incident.comments as comment}
<li class="mb-10 ms-4">
<div
class="absolute -start-1.5 mt-1.5 h-3 w-3 rounded-full border border-secondary bg-secondary"
></div>
<time
class="mb-1 text-xs font-normal leading-none text-muted-foreground"
>
{moment(comment.created_at).format(
"MMMM Do YYYY, h:mm:ss a"
)}
</time>
<div
class="wysiwyg prose prose-stone mb-4 max-w-none text-base text-sm font-normal dark:prose-invert prose-code:rounded prose-code:px-[0.3rem] prose-code:py-[0.2rem] prose-code:font-mono prose-code:text-sm"
>
{@html comment.body}
</div>
</li>
{/each}
</ol>
</div>
{:else if commentsLoading}
<Skeleton class="h-[20px] w-[100px] rounded-full" />
{/if}
</Card.Content>
{/if}
</Card.Root>
</div>
</div>
<style>
.toggle {
display: none;
}
.toggle {
transition: all 0.15s ease-in-out;
}
.toggle.open {
transform: rotate(180deg);
}
.incident-div:hover .toggle {
display: block;
}
</style>
-194
View File
@@ -1,194 +0,0 @@
<script>
import * as Card from "$lib/components/ui/card";
import { Button } from "$lib/components/ui/button";
import { Input } from "$lib/components/ui/input";
import { Label } from "$lib/components/ui/label";
import { siteDataExtractFromDb, storeSiteData } from "$lib/clientTools.js";
import { base } from "$app/paths";
import { Loader, Info, Check, FileWarning } from "lucide-svelte";
import { Tooltip } from "bits-ui";
import * as Alert from "$lib/components/ui/alert";
export let data;
let githubInfo = {
repo: "",
owner: "",
incidentSince: "",
apiURL: "https://api.github.com"
};
githubInfo = siteDataExtractFromDb(data.siteData, { github: githubInfo }).github;
let formState = "idle";
async function formSubmit() {
formState = "loading";
let resp = await storeSiteData({
github: JSON.stringify(githubInfo)
});
//print data
let data = await resp.json();
formState = "idle";
if (data.error) {
alert(data.error);
return;
}
}
</script>
<Card.Root class="mt-4">
<Card.Header class="border-b">
<Card.Title>Github Repo Information</Card.Title>
<Card.Description>Set up a Github repository to store incident management.</Card.Description
>
</Card.Header>
<Card.Content>
{#if !!data.GH_TOKEN}
<Alert.Root class="mt-4">
<Check class="h-4 w-4" />
<Alert.Title>GH_TOKEN is Set</Alert.Title>
<Alert.Description>
<span class="text-green-500">{data.GH_TOKEN}</span> is set in your environment variables.
</Alert.Description>
</Alert.Root>
{:else}
<Alert.Root class="mt-4" variant="destructive">
<FileWarning class="h-4 w-4" />
<Alert.Title>GH_TOKEN not found.</Alert.Title>
<Alert.Description>
Please add GH_TOKEN in your environment variables. Visit <a
href="https://kener.ing/docs/gh-setup/"
class="text-blue-500"
target="_blank">here</a
> for more information.
</Alert.Description>
</Alert.Root>
{/if}
<form class="mx-auto mt-4 space-y-4" on:submit|preventDefault={formSubmit}>
<div class="flex w-full flex-row justify-evenly gap-2">
<div class="w-full">
<Label for="apiURL">
Github API URL
<span class="text-red-500">*</span>
<Tooltip.Root openDelay={100}>
<Tooltip.Trigger class="">
<Info class="inline-block h-4 w-4 text-muted-foreground" />
</Tooltip.Trigger>
<Tooltip.Content>
<div
class=" flex items-center justify-center rounded border bg-gray-800 p-1.5 text-xs font-medium text-gray-300 shadow-popover outline-none"
>
Change this only if you are using a custom Github API URL.
</div>
</Tooltip.Content>
</Tooltip.Root>
</Label>
<Input
bind:value={githubInfo.apiURL}
class="mt-2"
type="text"
id="apiURL"
placeholder="eg. https://api.github.com"
required
/>
</div>
</div>
<div class="flex w-full flex-row justify-evenly gap-2">
<div class="w-full">
<Label for="repo">
Github Repo
<span class="text-red-500">*</span>
<Tooltip.Root openDelay={100}>
<Tooltip.Trigger class="">
<Info class="inline-block h-4 w-4 text-muted-foreground" />
</Tooltip.Trigger>
<Tooltip.Content>
<div
class=" flex items-center justify-center rounded border bg-gray-800 p-1.5 text-xs font-medium text-gray-300 shadow-popover outline-none"
>
Repo name where incidents will be stored. More often then not it
is the same where kener is forked.
</div>
</Tooltip.Content>
</Tooltip.Root>
</Label>
<Input
bind:value={githubInfo.repo}
class="mt-2"
type="text"
id="repo"
placeholder="eg. kener"
required
/>
</div>
<div class="w-full">
<Label for="owner">
Github Username
<span class="text-red-500">*</span>
<Tooltip.Root openDelay={100}>
<Tooltip.Trigger class="">
<Info class="inline-block h-4 w-4 text-muted-foreground" />
</Tooltip.Trigger>
<Tooltip.Content>
<div
class=" flex items-center justify-center rounded border bg-gray-800 p-1.5 text-xs font-medium text-gray-300 shadow-popover outline-none"
>
Owner of the repo. More often then not it is the same where
kener is forked.
</div>
</Tooltip.Content>
</Tooltip.Root>
</Label>
<Input
bind:value={githubInfo.owner}
class="mt-2"
type="text"
id="owner"
placeholder="eg. rajnandan1"
required
/>
</div>
<div class="w-full">
<Label for="incidentSince">
Incident History
<span class="text-red-500">*</span>
<Tooltip.Root openDelay={100}>
<Tooltip.Trigger class="">
<Info class="inline-block h-4 w-4 text-muted-foreground" />
</Tooltip.Trigger>
<Tooltip.Content>
<div
class=" flex items-center justify-center rounded border bg-gray-800 px-2 py-1.5 text-xs font-medium text-gray-300 shadow-popover outline-none"
>
It is in hours. <br />It means if an issue is created before X
hours then kener would <br />not honor it. It would not show
then incident<br /> in active incident pages nor it will update<br
/> the uptime.
</div>
</Tooltip.Content>
</Tooltip.Root>
</Label>
<Input
bind:value={githubInfo.incidentSince}
class="mt-2"
type="number"
min="1"
id="incidentSince"
placeholder="eg. 24"
required
/>
</div>
</div>
<div class="flex w-full justify-end">
<Button type="submit" disabled={formState === "loading"}>
Save
{#if formState === "loading"}
<Loader class="ml-2 inline h-4 w-4 animate-spin" />
{/if}
</Button>
</div>
</form>
</Card.Content>
</Card.Root>
+18 -30
View File
@@ -6,6 +6,7 @@
import { siteDataExtractFromDb, storeSiteData } from "$lib/clientTools.js";
import { base } from "$app/paths";
import * as Select from "$lib/components/ui/select";
import locales from "$lib/locales/locales.json?raw";
import { Loader, X, Plus, Info, Play } from "lucide-svelte";
import { Tooltip } from "bits-ui";
@@ -30,38 +31,14 @@
let categories = [];
let i18n = {
defaultLocale: "en",
locales: [
{
code: "en",
name: "English",
locales: JSON.parse(locales).map((el) => {
return {
code: el.code,
name: el.name,
selected: true,
disabled: false
},
{
code: "hi",
name: "हिन्दी",
selected: true,
disabled: false
},
{
code: "zh_CN",
name: "中文",
selected: true,
disabled: false
},
{
code: "ja",
name: "日本語",
selected: true,
disabled: false
},
{
code: "vi",
name: "Tiếng Việt",
selected: true,
disabled: false
}
]
};
})
};
if (data.siteData?.nav) {
@@ -85,6 +62,17 @@
}
if (data.siteData.i18n) {
i18n = data.siteData.i18n;
//add locales that are not present in the data
JSON.parse(locales).forEach((el) => {
if (!i18n.locales.find((locale) => locale.code === el.code)) {
i18n.locales.push({
code: el.code,
name: el.name,
selected: false,
disabled: false
});
}
});
}
async function formSubmitHero() {
+26 -26
View File
@@ -91,29 +91,30 @@
let uptimesRollers = [
{
text: "90 Days",
text: `${l(lang, "90 Days")}`,
startTs: moment().subtract(90, "days").startOf("day").unix(),
value: uptime90Day
},
{
text: "Today",
startTs: moment().startOf("day").unix()
text: `${l(lang, "60 Days")}`,
startTs: moment().subtract(59, "days").startOf("day").unix(),
value: uptime90Day
},
{
text: "This Week",
startTs: moment().startOf("week").unix()
text: `${l(lang, "30 Days")}`,
startTs: moment().subtract(29, "days").startOf("day").unix()
},
{
text: "7 Days",
text: `${l(lang, "14 Days")}`,
startTs: moment().subtract(13, "days").startOf("day").unix()
},
{
text: `${l(lang, "7 Days")}`,
startTs: moment().subtract(6, "days").startOf("day").unix()
},
{
text: "This Month",
startTs: moment().startOf("month").unix()
},
{
text: "30 Days",
startTs: moment().subtract(29, "days").startOf("day").unix()
text: l(lang, "Today"),
startTs: moment().startOf("day").unix()
}
];
@@ -289,9 +290,11 @@
<div
class="text-api-up truncate text-xs font-semibold text-{monitor.pageData
.summaryColorClass}"
title={monitor.pageData.summaryText}
>
{summaryTime(lang, monitor.pageData.summaryText)}
{l(lang, summaryTime(monitor.pageData.summaryStatus), {
status: monitor.pageData.summaryStatus,
duration: monitor.pageData.summaryDuration
})}
</div>
</div>
</div>
@@ -343,7 +346,10 @@
{moment(new Date(bar.timestamp * 1000)).format(
"dddd, MMMM Do, YYYY"
)} -
{summaryTime(lang, bar.message)}
{l(lang, summaryTime(bar.summaryStatus), {
status: bar.summaryStatus,
duration: bar.summaryDuration
})}
</div>
</div>
{/if}
@@ -367,7 +373,7 @@
<div class="-mx-2 mb-4 grid grid-cols-1">
<div class="col-span-1 px-2">
<Badge variant="outline" class="border-0 pl-0">
{l(lang, "root.incidents")}
{l(lang, "Incident Updates")}
</Badge>
</div>
{#each dayIncidentsFull as incident, index}
@@ -393,19 +399,13 @@
>
<p>
<span class="text-{bar.cssClass}"></span>
{ampm(
lang,
n(
lang,
new Date(
bar.timestamp * 1000
).toLocaleTimeString()
)
)}
{new Date(
bar.timestamp * 1000
).toLocaleTimeString()}
</p>
{#if bar.status != "NO_DATA"}
<p class="pl-2">
{l(lang, "statuses." + bar.status)}
{l(lang, bar.status)}
</p>
{:else}
<p class="pl-2">-</p>
+30 -30
View File
@@ -139,10 +139,10 @@
</div>
<div class="px-4">
<h2 class="mb-1 text-sm font-semibold">
{l(lang, "monitor.share")}
{l(lang, "Share")}
</h2>
<p class="mb-2 text-xs text-muted-foreground">
{l(lang, "monitor.share_desc")}
{l(lang, "Share this monitor using a link with others")}
</p>
<Button
class="h-8 px-2 pr-4 text-xs font-semibold"
@@ -152,12 +152,12 @@
{#if !copiedLink}
<Link class="mr-2 inline" size={12} />
<span class="font-semibold">
{l(lang, "monitor.cp_link")}
{l(lang, "Copy Link")}
</span>
{:else}
<CopyCheck class="mr-2 inline" size={12} />
<span class="font-semibold">
{l(lang, "monitor.cpd_link")}
{l(lang, "Link Copied")}
</span>
{/if}
</Button>
@@ -165,32 +165,34 @@
<hr class="my-4" />
<div class="px-4">
<h2 class="mb-1 text-sm font-semibold">
{l(lang, "monitor.embed")}
{l(lang, "Embed")}
</h2>
<p class="mb-1 text-xs text-muted-foreground">
{l(lang, "monitor.embed_desc")}
{@html l(
lang,
"Embed this moni2tor using &#x3C;script&#x3E; or &#x3C;iframe&#x3E; in your app."
)}
</p>
<div class="mb-4 grid grid-cols-2 gap-2">
<div class="col-span-1">
<h3 class="mb-2 text-xs">
{l(lang, "monitor.theme")}
{l(lang, "Theme")}
</h3>
<RadioGroup.Root bind:value={theme} class=" flex">
<div class="flex items-center space-x-2">
<RadioGroup.Item value="light" id="light-theme" />
<Label class="text-xs" for="light-theme">{l(lang, "monitor.theme_light")}</Label
>
<Label class="text-xs" for="light-theme">{l(lang, "Light")}</Label>
</div>
<div class="flex items-center space-x-2">
<RadioGroup.Item value="dark" id="dark-theme" />
<Label class="text-xs" for="dark-theme">{l(lang, "monitor.theme_dark")}</Label>
<Label class="text-xs" for="dark-theme">{l(lang, "Dark")}</Label>
</div>
<RadioGroup.Input name="theme" />
</RadioGroup.Root>
</div>
<div class="col-span-1 pl-2">
<h3 class="mb-2 text-xs">
{l(lang, "monitor.mode")}
{l(lang, "Mode")}
</h3>
<RadioGroup.Root bind:value={embedType} class="flex">
<div class="flex items-center space-x-2">
@@ -209,12 +211,12 @@
{#if !copiedEmbed}
<Code class="mr-2 inline" size={12} />
<span class=" font-semibold">
{l(lang, "monitor.cp_code")}
{l(lang, "Copy Code")}
</span>
{:else}
<CopyCheck class="mr-2 inline" size={12} />
<span class="font-semibold">
{l(lang, "monitor.cpd_code")}
{l(lang, "Code Copied")}
</span>
{/if}
</Button>
@@ -222,23 +224,22 @@
<hr class="my-4" />
<div class="px-4">
<h2 class="mb-1 text-sm font-semibold">
{l(lang, "monitor.badge")}
{l(lang, "Badge")}
</h2>
<p class="mb-2 text-xs text-muted-foreground">
{l(lang, "monitor.badge_desc")}
{l(lang, "Get SVG badge for this monitor")}
</p>
<Button class="h-8 px-2 pr-4 text-xs" variant="secondary" on:click={copyStatusBadge}>
{#if !copiedBadgeStatus}
<TrendingUp class="mr-2 inline" size={12} />
<span class="font-semibold">
{l(lang, "monitor.status")}
{l(lang, "monitor.badge")}</span
>
{l(lang, "Status")}
{l(lang, "Badge")}
</span>
{:else}
<CopyCheck class="mr-2 inline" size={12} />
<span class="font-semibold">
{l(lang, "monitor.copied")}
{l(lang, "monitor.badge")}
{l(lang, "Badge Copied")}
</span>
{/if}
</Button>
@@ -246,14 +247,13 @@
{#if !copiedBadgeUptime}
<Percent class="mr-2 inline" size={12} />
<span class="font-semibold">
{l(lang, "monitor.uptime")}
{l(lang, "monitor.badge")}
{l(lang, "Uptime")}
{l(lang, "Badge")}
</span>
{:else}
<CopyCheck class="mr-2 inline" size={12} />
<span class="font-semibold">
{l(lang, "monitor.copied")}
{l(lang, "monitor.badge")}
{l(lang, "Badge Copied")}
</span>
{/if}
</Button>
@@ -261,21 +261,21 @@
<hr class="my-4" />
<div class="mb-4 px-4">
<h2 class="mb-1 text-sm font-semibold">
{l(lang, "monitor.status_svg")}
{l(lang, "LIVE Status")}
</h2>
<p class="mb-2 text-xs text-muted-foreground">
{l(lang, "monitor.status_svg_desc")}
{l(lang, "Get a LIVE Status for this monitor")}
</p>
<Button class="h-8 px-2 pr-4 text-xs" variant="secondary" on:click={copyDotStandard}>
{#if !copiedBadgeDotStandard}
<img src={pathMonitorBadgeDot} class="mr-1 inline h-5" alt="status" />
<span class="font-semibold">
{l(lang, "monitor.standard")}
{l(lang, "Standard")}
</span>
{:else}
<CopyCheck class="mr-2 inline h-5 w-5" />
<span class="font-semibold">
{l(lang, "monitor.standard")}
{l(lang, "Standard")}
</span>
{/if}
</Button>
@@ -283,12 +283,12 @@
{#if !copiedBadgeDotPing}
<img src={pathMonitorBadgeDotPing} class="mr-1 inline h-5" alt="status" />
<span class="font-semibold">
{l(lang, "monitor.pinging")}
{l(lang, "Pinging")}
</span>
{:else}
<CopyCheck class="mr-2 inline h-5 w-5" />
<span class="font-semibold">
{l(lang, "monitor.pinging")}
{l(lang, "Pinging")}
</span>
{/if}
</Button>
+21 -165
View File
@@ -1,178 +1,34 @@
const l = function (/** @type {any} */ sessionLangMap, /** @type {string} */ key) {
// @ts-nocheck
const l = function (sessionLangMap, key, args = {}) {
const keys = key.split(".");
let obj = sessionLangMap;
for (const key of keys) {
// @ts-ignore
obj = obj[key];
for (const keyPart of keys) {
obj = obj?.[keyPart];
if (!obj) {
break;
}
}
// Replace placeholders in the string using the args object
if (obj && typeof obj === "string") {
obj = obj.replace(/%\w+/g, (placeholder) => {
const argKey = placeholder.slice(1); // Remove the `%` to get the key
return args[argKey] !== undefined ? args[argKey] : placeholder;
});
}
return obj || key;
};
const summaryTime = function (/** @type {any} */ sessionLangMap, /** @type {string} */ message) {
if (message == "No Data") {
return sessionLangMap.monitor.status_no_data;
const summaryTime = function (summaryStatus) {
if (summaryStatus == "No Data") {
return "No Data";
}
if (message == "Status OK") {
return sessionLangMap.monitor.status_ok;
if (summaryStatus == "UP") {
return "Status OK";
}
const expectedStrings = [
{
regexes: [/^(DEGRADED|DOWN)/g, /\d+ minute$/g],
s: sessionLangMap.monitor.status_x_minute,
output: function (
/** @type {string} */ message,
/** @type {{ statuses: { [x: string]: any; }; numbers: { [x: string]: any; }; }} */ sessionLangMap
) {
let match = message.match(this.regexes[0]);
const status = match ? match[0] : null;
if (!status) {
return message;
}
match = message.match(this.regexes[1]);
const duration = match ? match[0] : null;
if (!duration) {
return message;
}
const digits = duration
.replace(" minute", "")
.split("")
.map((elem) => {
return sessionLangMap.numbers[String(elem)];
})
.join("");
return this.s
.replace(/%status/g, sessionLangMap.statuses[status])
.replace(/%minute/, digits);
}
},
{
regexes: [/^(DEGRADED|DOWN)/g, /\d+ minutes$/g],
s: sessionLangMap.monitor.status_x_minutes,
output: function (
/** @type {string} */ message,
/** @type {{ statuses: { [x: string]: any; }; numbers: { [x: string]: any; }; }} */ sessionLangMap
) {
let match = message.match(this.regexes[0]);
const status = match ? match[0] : null;
if (!status) {
return message;
}
match = message.match(this.regexes[1]);
const duration = match ? match[0] : null;
if (!duration) {
return message;
}
const digits = duration
.replace(" minutes", "")
.split("")
.map((elem) => {
return sessionLangMap.numbers[String(elem)];
})
.join("");
let res = this.s
.replace(/%status/g, sessionLangMap.statuses[status])
.replace(/%minutes/, digits);
return res;
}
},
{
regexes: [/^(DEGRADED|DOWN)/g, /\d+h/g, /\d+m$/g],
s: sessionLangMap.monitor.status_x_hour_y_minute,
output: function (
/** @type {string} */ message,
/** @type {{ statuses: { [x: string]: any; }; numbers: { [x: string]: any; }; }} */ sessionLangMap
) {
let match = message.match(this.regexes[0]);
const status = match ? match[0] : null;
if (!status) {
return message;
}
match = message.match(this.regexes[1]);
const hour = match ? match[0] : null;
if (!hour) {
return message;
}
match = message.match(this.regexes[2]);
const minute = match ? match[0] : null;
if (!minute) {
return message;
}
const digits = hour
.replace("h", "")
.split("")
.map((elem) => {
return sessionLangMap.numbers[String(elem)];
})
.join("");
const digits2 = minute
.replace("m", "")
.split("")
.map((elem) => {
return sessionLangMap.numbers[String(elem)];
})
.join("");
return this.s
.replace(/%status/g, sessionLangMap.statuses[status])
.replace(/%hours/g, digits)
.replace(/%minutes/, digits2);
}
},
{
regexes: [/^Last \d+ hours$/g],
s: sessionLangMap.root.last_x_hours,
output: function (
/** @type {string} */ message,
/** @type {{ statuses: { [x: string]: any; }; numbers: { [x: string]: any; }; }} */ sessionLangMap
) {
//extract the number out of message
let match = message.match(/\d+/g);
const hours = match ? match[0] : null;
if (!hours) {
return message;
}
const digits = hours
.split("")
.map((elem) => {
return sessionLangMap.numbers[String(elem)];
})
.join("");
return this.s.replace(/%hours/g, digits);
}
}
];
//loop through the expectedStrings array and find the matching string
let selectedIndex = -1;
for (let i = 0; i < expectedStrings.length; i++) {
let matchCount = 0;
for (let j = 0; j < expectedStrings[i].regexes.length; j++) {
if (message.match(expectedStrings[i].regexes[j])) {
matchCount++;
}
}
if (matchCount == expectedStrings[i].regexes.length) {
selectedIndex = i;
break;
}
}
if (selectedIndex < 0) {
return message;
}
const selectedReplace = expectedStrings[selectedIndex];
return selectedReplace.output(message, sessionLangMap);
return "%status for %duration";
};
const n = function (
@@ -180,7 +36,7 @@ const n = function (
/** @type {string} */ inputString
) {
const translations = sessionLangMap.numbers;
console.trace("translations", translations);
// @ts-ignore
return inputString.replace(
/\d/g,
+9 -13
View File
@@ -1,20 +1,16 @@
import en from "$lib/locales/en.json";
import hi from "$lib/locales/hi.json";
import ja from "$lib/locales/ja.json";
import vi from "$lib/locales/vi.json";
import zhCN from "$lib/locales/zh-CN.json";
//read $lib/locales/locales.json and dynamic import all the locales
/**
* Map of language codes to their corresponding values.
* @type {Record<string, any>}
*/
const langMap = {
en,
hi,
ja,
vi,
"zh-CN": zhCN
};
const langMap = {};
import localesStr from "$lib/locales/locales.json?raw";
const locales = JSON.parse(localesStr);
locales.forEach((/** @type {{ code: string; }} */ locale) => {
import(`$lib/locales/${locale.code}.json`).then((data) => {
langMap[locale.code] = data.default;
});
});
/**
* @param {{ [x: string]: any; }} language
+50 -59
View File
@@ -1,61 +1,52 @@
{
"root": {
"ongoing_incidents": "Ongoing Incidents",
"availability_per_component": "Availability per Component",
"other_monitors": "Other Monitors",
"no_monitors": "No monitors found",
"read_doc_monitor": "Read the documentation to add your first monitor",
"here": "here",
"category": "Category",
"incident": "Incident",
"incidents": "Incidents",
"no_recent_incident": "No recent incident",
"recent_incidents": "Recent Incidents",
"active_incidents": "Active Incidents",
"no_active_incident": "No Active Incident",
"last_x_hours": "Last %hours hours"
},
"statuses": {
"UP": "UP",
"DOWN": "DOWN",
"DEGRADED": "DEGRADED"
},
"incident": {
"identified": "Identified",
"resolved": "Resolved",
"maintenance": "Maintenance"
},
"monitor": {
"share": "Share",
"badge": "Badge",
"embed": "Embed",
"mode": "Mode",
"status": "Status",
"copied": "Copied",
"uptime": "Uptime",
"theme": "Theme",
"theme_light": "Light",
"theme_dark": "Dark",
"today": "Today",
"90_day": "90 Day",
"share_desc": "Share this monitor using a link with others",
"badge_desc": "Get SVG badge for this monitor",
"embed_desc": "Embed this monitor using <script> or <iframe> in your app.",
"cp_link": "Copy Link",
"cpd_link": "Link Copied",
"cp_code": "Copy Code",
"cpd_code": "Code Copied",
"status_x_minute": "%status for %minute minute",
"status_x_minutes": "%status for %minutes minutes",
"status_x_hour_y_minute": "%status for %hours h and %minutes m",
"status_no_data": "No Data",
"status_ok": "Status OK",
"am": "am",
"pm": "pm",
"standard": "Standard",
"pinging": "Pinging",
"status_svg": "Live Status",
"status_svg_desc": "Get a LIVE Status for this monitor"
},
"numbers": ["0", "1", "2", "3", "4", "5", "6", "7", "8", "9"]
"14 Days": "14 Days",
"30 Days": "30 Days",
"60 Days": "60 Days",
"7 Days": "7 Days",
"90 Days": "90 Days",
"Availability per Component": "Availability per Component",
"Badge": "Badge",
"Badge Copied": "Badge Copied",
"Code Copied": "Code Copied",
"Copy Code": "Copy Code",
"Copy Link": "Copy Link",
"Dark": "Dark",
"Days": "Days",
"DEGRADED": "DEGRADED",
"DOWN": "DOWN",
"Embed": "Embed",
"Embed this monitor using &#x3C;script&#x3E; or &#x3C;iframe&#x3E; in your app.": "Embed this monitor using <script> or <iframe> in your app.",
"Get SVG badge for this monitor": "Get SVG badge for this monitor",
"Get a LIVE Status for this monitor": "Get a LIVE Status for this monitor",
"IDENTIFIED": "IDENTIFIED",
"Incident Updates": "Incident Updates",
"INVESTIGATING": "INVESTIGATING",
"Lasted for about %lastedFor": "Lasted for about %lastedFor",
"Light": "Light",
"Link Copied": "Link Copied",
"LIVE Status": "LIVE Status",
"Mode": "Mode",
"MONITORING": "MONITORING",
"No Data": "No Data",
"No Monitor Found": "No Monitor Found",
"No Updates Yet": "No Updates Yet",
"Ongoing Incidents": "Ongoing Incidents",
"Pinging": "Pinging",
"Recent Incidents": "Recent Incidents",
"RESOLVED": "RESOLVED",
"Share": "Share",
"Share this monitor using a link with others": "Share this monitor using a link with others",
"Standard": "Standard",
"Started about %startedAt ago, lasted for about %lastedFor": "Started about %startedAt ago, lasted for about %lastedFor",
"Started about %startedAt ago, still ongoing": "Started about %startedAt ago, still ongoing",
"Starts in %startedAt": "Starts in %startedAt",
"Starts in %startedAt, will last for about %lastedFor": "Starts in %startedAt, will last for about %lastedFor",
"Status": "Status",
"Status OK": "Status OK",
"Theme": "Theme",
"Today": "Today",
"UP": "UP",
"Updates": "Updates",
"Uptime": "Uptime",
"%status for %duration": "%status for %duration"
}
+52
View File
@@ -0,0 +1,52 @@
{
"14 Days": "14 jours",
"30 Days": "30 jours",
"60 Days": "60 jours",
"7 Days": "7 jours",
"90 Days": "90 jours",
"Availability per Component": "Disponibilité par composant",
"Badge": "Badge",
"Badge Copied": "Badge copié",
"Code Copied": "Code copié",
"Copy Code": "Copier le code",
"Copy Link": "Copier le lien",
"Dark": "Sombre",
"Days": "Jours",
"DEGRADED": "Dégradé",
"DOWN": "Hors ligne",
"Embed": "Intégrer",
"Embed this monitor using &#x3C;script&#x3E; or &#x3C;iframe&#x3E; in your app.": "Intégrez ce moniteur dans votre application en utilisant <script> ou <iframe>.",
"Get SVG badge for this monitor": "Obtenez un badge SVG pour ce moniteur",
"Get a LIVE Status for this monitor": "Obtenez un statut LIVE pour ce moniteur",
"IDENTIFIED": "Identifié",
"Incident Updates": "Mises à jour des incidents",
"INVESTIGATING": "En cours d'investigation",
"Lasted for about %lastedFor": "A duré environ %lastedFor",
"Light": "Clair",
"Link Copied": "Lien copié",
"LIVE Status": "Statut LIVE",
"Mode": "Mode",
"MONITORING": "En surveillance",
"No Data": "Pas de données",
"No Monitor Found": "Aucun moniteur trouvé",
"No Updates Yet": "Pas encore de mises à jour",
"Ongoing Incidents": "Incidents en cours",
"Pinging": "Ping en cours",
"Recent Incidents": "Incidents récents",
"RESOLVED": "Résolu",
"Share": "Partager",
"Share this monitor using a link with others": "Partagez ce moniteur avec un lien à d'autres personnes",
"Standard": "Standard",
"Started about %startedAt ago, lasted for about %lastedFor": "Commencé il y a environ %startedAt, a duré environ %lastedFor",
"Started about %startedAt ago, still ongoing": "Commencé il y a environ %startedAt, toujours en cours",
"Starts in %startedAt": "Commence dans %startedAt",
"Starts in %startedAt, will last for about %lastedFor": "Commence dans %startedAt, durera environ %lastedFor",
"Status": "Statut",
"Status OK": "Statut OK",
"Theme": "Thème",
"Today": "Aujourd'hui",
"UP": "En ligne",
"Updates": "Mises à jour",
"Uptime": "Temps de fonctionnement",
"%status for %duration": "%status pendant %duration"
}
+50 -55
View File
@@ -1,57 +1,52 @@
{
"incident": {
"identified": "डेंटिफ़िएड",
"maintenance": "मेंटेनेंस",
"resolved": "रेसोल्वेड"
},
"monitor": {
"90_day": "90 दिन",
"am": "ऍम",
"badge": "बैज",
"badge_desc": "इस मॉनिटर के लिए SVG बैज प्राप्त करें",
"copied": "कोपीएड",
"cp_code": "कॉपी कोड",
"cp_link": "लिंक कॉपी करें",
"cpd_code": "कोड कॉपी किया गया",
"cpd_link": "लिंक कॉपी किया गया",
"embed": "एम्बेड",
"embed_desc": "इस मॉनटर को एम्बेड करें<script> या<iframe> अपने ऐप में.",
"mode": "मोड",
"pm": "पं",
"share": "शेयर",
"share_desc": "लिंक का उपयोग करके इस मॉनिटर को अन्य लोगों के साथ शेयर करें",
"status": "स्टेटस",
"status_no_data": "कोई डेटा नहीं",
"status_ok": "स्थिति ठीक है",
"status_x_hour_y_minute": "%hours घंटा %minutes मिनट के लिए %status",
"status_x_minute": "%minute मिनट के लिए %status",
"status_x_minutes": "%minutes मिनट के लिए %status",
"theme": "थीम",
"theme_dark": "डार्क",
"theme_light": "लाइट",
"today": "आज",
"uptime": "अपटाइम"
},
"numbers": ["0", "1", "2", "3", "4", "5", "6", "7", "8", "9"],
"root": {
"active_incidents": "सक्रिय घटनाएं",
"availability_per_component": "प्रति कॉम्पोनेन्ट उपलब्धता",
"category": "श्रेणी",
"here": "यहाँ",
"incident": "हादसा",
"incidents": "घटनाएं",
"last_x_hours": "अंतिम %hours घंटे",
"no_active_incident": "कोई सक्रिय घटना नहीं",
"no_monitors": "कोई मॉनिटर नहीं मिला",
"no_recent_incident": "कोई हालिया घटना नहीं",
"ongoing_incidents": "चल रही घटनाएँ",
"other_monitors": "अन्य मॉनिटर",
"read_doc_monitor": "अपना पहला मॉनिटर जोड़ने के लिए डॉक्यूमेंटेशन पढ़ें",
"recent_incidents": "हाल की घटनाएँ"
},
"statuses": {
"DEGRADED": "डेग्रेडेड",
"DOWN": "डाउन",
"UP": "उप"
}
"14 Days": "14 दिन",
"30 Days": "30 दिन",
"60 Days": "60 दिन",
"7 Days": "7 दिन",
"90 Days": "90 दिन",
"Availability per Component": "प्रत्येक घटक की उपलब्धता",
"Badge": "बैज",
"Badge Copied": "बैज कॉपी किया गया",
"Code Copied": "कोड कॉपी किया गया",
"Copy Code": "कोड कॉपी करें",
"Copy Link": "लिंक कॉपी करें",
"Dark": "डार्क",
"Days": "दिन",
"DEGRADED": "क्षीण",
"DOWN": "डाउन",
"Embed": "एम्बेड",
"Embed this monitor using &#x3C;script&#x3E; or &#x3C;iframe&#x3E; in your app.": "इस मॉनिटर को अपनी ऐप में <script> या <iframe> का उपयोग करके एम्बेड करें",
"Get SVG badge for this monitor": "इस मॉनिटर के लिए SVG बैज प्राप्त करें",
"Get a LIVE Status for this monitor": "इस मॉनिटर के लिए लाइव स्टेटस प्राप्त करें",
"IDENTIFIED": "पहचाना गया",
"Incident Updates": "घटना अपडेट",
"INVESTIGATING": "जांच चल रही है",
"Lasted for about %lastedFor": "लगभग %lastedFor तक चला",
"Light": "लाइट",
"Link Copied": "लिंक कॉपी किया गया",
"LIVE Status": "लाइव स्टेटस",
"Mode": "मोड",
"MONITORING": "निगरानी",
"No Data": "कोई डेटा नहीं",
"No Monitor Found": "कोई मॉनिटर नहीं मिला",
"No Updates Yet": "अभी तक कोई अपडेट नहीं",
"Ongoing Incidents": "चल रही घटनाएँ",
"Pinging": "पिंगिंग",
"Recent Incidents": "हाल की घटनाएँ",
"RESOLVED": "सुलझाया गया",
"Share": "साझा करें",
"Share this monitor using a link with others": "इस मॉनिटर को दूसरों के साथ लिंक के माध्यम से साझा करें",
"Standard": "मानक",
"Started about %startedAt ago, lasted for about %lastedFor": "लगभग %startedAt पहले शुरू हुआ, लगभग %lastedFor तक चला",
"Started about %startedAt ago, still ongoing": "लगभग %startedAt पहले शुरू हुआ, अभी भी जारी है",
"Starts in %startedAt": "%startedAt में शुरू होगा",
"Starts in %startedAt, will last for about %lastedFor": "%startedAt में शुरू होगा, लगभग %lastedFor तक चलेगा",
"Status": "स्थिति",
"Status OK": "स्थिति ठीक है",
"Theme": "थीम",
"Today": "आज",
"UP": "अप",
"Updates": "अपडेट्स",
"Uptime": "अपटाइम",
"%status for %duration": "%duration के लिए %status"
}
+50 -55
View File
@@ -1,57 +1,52 @@
{
"root": {
"ongoing_incidents": "進行中のインシデント",
"availability_per_component": "コンポーネントごとの可用性",
"other_monitors": "その他のモニター",
"no_monitors": "モニターはありません",
"read_doc_monitor": "最初のモニターを追加するには、ドキュメントをお読みください",
"here": "ここ",
"category": "カテゴリー",
"incident": "インシデント",
"incidents": "インシデント",
"no_recent_incident": "最近のインシデントはありません",
"recent_incidents": "最近のインシデント",
"active_incidents": "現在のインシデント",
"no_active_incident": "現在のインシデントはありません",
"last_x_hours": "最近%hours時間"
},
"statuses": {
"UP": "正常",
"DOWN": "ダウン",
"DEGRADED": "縮退"
},
"incident": {
"identified": "確認済み",
"resolved": "解決済み",
"maintenance": "メンテナンス"
},
"monitor": {
"share": "シェア",
"badge": "バッジ",
"embed": "埋め込み",
"mode": "モード",
"status": "ステータス",
"copied": "コピーしました",
"uptime": "稼働時間",
"theme": "テーマ",
"theme_light": "ライト",
"theme_dark": "ダーク",
"today": "今日",
"90_day": "90日間",
"share_desc": "このモニターをリンクで他の人にシェア",
"badge_desc": "このモニターのSVGバッジを取得",
"embed_desc": "このモニターを <script> や <iframe> で埋め込む",
"cp_link": "リンクをコピー",
"cpd_link": "リンクをコピーしました",
"cp_code": "コードをコピー",
"cpd_code": "コードをコピーしました",
"status_x_minute": "%minute分間の%status",
"status_x_minutes": "%minutes分間の%status",
"status_x_hour_y_minute": "%hours時間%minutes分間の%status",
"status_no_data": "データはありません",
"status_ok": "正常",
"am": "午前",
"pm": "午後"
},
"numbers": ["0", "1", "2", "3", "4", "5", "6", "7", "8", "9"]
"14 Days": "14日間",
"30 Days": "30日間",
"60 Days": "60日間",
"7 Days": "7日間",
"90 Days": "90日間",
"Availability per Component": "コンポーネントごとの可用性",
"Badge": "バッジ",
"Badge Copied": "バッジがコピーされました",
"Code Copied": "コードがコピーされました",
"Copy Code": "コードをコピー",
"Copy Link": "リンクをコピー",
"Dark": "ダーク",
"Days": "日間",
"DEGRADED": "劣化",
"DOWN": "ダウン",
"Embed": "埋め込み",
"Embed this monitor using &#x3C;script&#x3E; or &#x3C;iframe&#x3E; in your app.": "このモニターをアプリに <script> または <iframe> を使用して埋め込む。",
"Get SVG badge for this monitor": "このモニターのSVGバッジを取得",
"Get a LIVE Status for this monitor": "このモニターのライブステータスを取得",
"IDENTIFIED": "識別済み",
"Incident Updates": "インシデント更新",
"INVESTIGATING": "調査中",
"Lasted for about %lastedFor": "約 %lastedFor 続きました",
"Light": "ライト",
"Link Copied": "リンクがコピーされました",
"LIVE Status": "ライブステータス",
"Mode": "モード",
"MONITORING": "モニタリング中",
"No Data": "データなし",
"No Monitor Found": "モニターが見つかりません",
"No Updates Yet": "まだ更新はありません",
"Ongoing Incidents": "進行中のインシデント",
"Pinging": "ピング中",
"Recent Incidents": "最近のインシデント",
"RESOLVED": "解決済み",
"Share": "共有",
"Share this monitor using a link with others": "このモニターをリンクで他の人と共有",
"Standard": "標準",
"Started about %startedAt ago, lasted for about %lastedFor": "%startedAt 前に開始し、約 %lastedFor 続きました",
"Started about %startedAt ago, still ongoing": "%startedAt 前に開始し、まだ進行中です",
"Starts in %startedAt": "%startedAt 後に開始",
"Starts in %startedAt, will last for about %lastedFor": "%startedAt 後に開始し、約 %lastedFor 続きます",
"Status": "ステータス",
"Status OK": "ステータス正常",
"Theme": "テーマ",
"Today": "今日",
"UP": "稼働中",
"Updates": "更新",
"Uptime": "稼働時間",
"%status for %duration": "%duration の間 %status"
}
+26
View File
@@ -0,0 +1,26 @@
[
{
"code": "en",
"name": "English"
},
{
"code": "hi",
"name": "हिन्दी"
},
{
"code": "zh-CN",
"name": "中文"
},
{
"code": "ja",
"name": "日本語"
},
{
"code": "vi",
"name": "Tiếng Việt"
},
{
"code": "fr",
"name": "Français"
}
]
+50 -55
View File
@@ -1,57 +1,52 @@
{
"root": {
"ongoing_incidents": "Sự Cố Đang Xảy Ra",
"availability_per_component": "Độ Sẵn Sàng Của Từng Thành Phần",
"other_monitors": "Các Giám Sát Khác",
"no_monitors": "Không tìm thấy giám sát nào",
"read_doc_monitor": "Đọc tài liệu để thêm giám sát đầu tiên của bạn",
"here": "tại đây",
"category": "Danh Mục",
"incident": "Sự Cố",
"incidents": "Các Sự Cố",
"no_recent_incident": "Không có sự cố gần đây",
"recent_incidents": "Các Sự Cố Gần Đây",
"active_incidents": "Các Sự Cố Đang Hoạt Động",
"no_active_incident": "Không Có Sự Cố Đang Hoạt Động",
"last_x_hours": "Trong %hours giờ qua"
},
"statuses": {
"UP": "HOẠT ĐỘNG",
"DOWN": "NGỪNG HOẠT ĐỘNG",
"DEGRADED": "SUY GIẢM"
},
"incident": {
"identified": "Đã Xác Định",
"resolved": "Đã Giải Quyết",
"maintenance": "Bảo Trì"
},
"monitor": {
"share": "Chia Sẻ",
"badge": "Huy Hiệu",
"embed": "Nhúng",
"mode": "Chế Độ",
"status": "Trạng Thái",
"copied": ã Sao Chép",
"uptime": "Thời Gian Hoạt Động",
"theme": "Chủ Đề",
"theme_light": "Sáng",
"theme_dark": "Tối",
"today": "Hôm Nay",
"90_day": "90 Ngày",
"share_desc": "Chia sẻ giám sát này bằng một liên kết với người khác",
"badge_desc": "Nhận huy hiệu SVG cho giám sát này",
"embed_desc": "Nhúng giám sát này bằng <script> hoặc <iframe> vào ứng dụng của bạn.",
"cp_link": "Sao Chép Liên Kết",
"cpd_link": "Liên Kết Đã Được Sao Chép",
"cp_code": "Sao Chép Mã",
"cpd_code": "Mã Đã Được Sao Chép",
"status_x_minute": "%status trong %minute phút",
"status_x_minutes": "%status trong %minutes phút",
"status_x_hour_y_minute": "%status trong %hours giờ và %minutes phút",
"status_no_data": "Không Có Dữ Liệu",
"status_ok": "Trạng Thái OK",
"am": "sáng",
"pm": "chiều"
},
"numbers": ["0", "1", "2", "3", "4", "5", "6", "7", "8", "9"]
"14 Days": "14 ngày",
"30 Days": "30 ngày",
"60 Days": "60 ngày",
"7 Days": "7 ngày",
"90 Days": "90 ngày",
"Availability per Component": "Tính khả dụng theo thành phần",
"Badge": "Huy hiệu",
"Badge Copied": "Huy hiệu đã được sao chép",
"Code Copied": "Mã đã được sao chép",
"Copy Code": "Sao chép mã",
"Copy Link": "Sao chép liên kết",
"Dark": "Tối",
"Days": "Ngày",
"DEGRADED": "Suy giảm",
"DOWN": "Ngừng hoạt động",
"Embed": "Nhúng",
"Embed this monitor using &#x3C;script&#x3E; or &#x3C;iframe&#x3E; in your app.": "Nhúng trình theo dõi này vào ứng dụng của bạn bằng <script> hoặc <iframe>.",
"Get SVG badge for this monitor": "Lấy huy hiệu SVG cho trình theo dõi này",
"Get a LIVE Status for this monitor": "Lấy trạng thái TRỰC TIẾP cho trình theo dõi này",
"IDENTIFIED": "Đã xác định",
"Incident Updates": "Cập nhật sự cố",
"INVESTIGATING": "Đang điều tra",
"Lasted for about %lastedFor": "Kéo dài khoảng %lastedFor",
"Light": "Sáng",
"Link Copied": "Liên kết đã được sao chép",
"LIVE Status": "Trạng thái TRỰC TIẾP",
"Mode": "Chế độ",
"MONITORING": "Đang giám sát",
"No Data": "Không có dữ liệu",
"No Monitor Found": "Không tìm thấy trình theo dõi",
"No Updates Yet": "Chưa có cập nhật",
"Ongoing Incidents": "Sự cố đang diễn ra",
"Pinging": ang kiểm tra",
"Recent Incidents": "Sự cố gần đây",
"RESOLVED": "Đã giải quyết",
"Share": "Chia sẻ",
"Share this monitor using a link with others": "Chia sẻ trình theo dõi này với người khác bằng liên kết",
"Standard": "Tiêu chuẩn",
"Started about %startedAt ago, lasted for about %lastedFor": "Bắt đầu khoảng %startedAt trước, kéo dài khoảng %lastedFor",
"Started about %startedAt ago, still ongoing": "Bắt đầu khoảng %startedAt trước, vẫn đang diễn ra",
"Starts in %startedAt": "Bắt đầu trong %startedAt",
"Starts in %startedAt, will last for about %lastedFor": "Bắt đầu trong %startedAt, sẽ kéo dài khoảng %lastedFor",
"Status": "Trạng thái",
"Status OK": "Trạng thái OK",
"Theme": "Giao diện",
"Today": "Hôm nay",
"UP": "Hoạt động",
"Updates": "Cập nhật",
"Uptime": "Thời gian hoạt động",
"%status for %duration": "%status trong %duration"
}
+50 -55
View File
@@ -1,57 +1,52 @@
{
"root": {
"ongoing_incidents": "正在进行的事件",
"availability_per_component": "每个服务的可用性",
"other_monitors": "其他显示器",
"no_monitors": "未找到显示器",
"read_doc_monitor": "阅读文档以添加您的第一个显示器",
"here": "这里",
"category": "类别",
"incident": "事件",
"incidents": "事件",
"no_recent_incident": "最近没有发生事件",
"recent_incidents": "最近发生的事件",
"active_incidents": "活跃事件",
"no_active_incident": "没有活跃事件",
"last_x_hours": "最近%hours个小时"
},
"statuses": {
"UP": "正常",
"DOWN": "故障",
"DEGRADED": "异常"
},
"incident": {
"identified": "确认",
"resolved": "解决",
"maintenance": "维护"
},
"monitor": {
"share": "分享",
"badge": "徽章",
"embed": "嵌入",
"mode": "模式",
"status": "状态",
"copied": "已复制",
"uptime": "正常运行时间",
"theme": "主题",
"theme_light": "Light",
"theme_dark": "Dark",
"today": "今天",
"90_day": "90 天",
"share_desc": "使用链接与其他人共享此显示器",
"badge_desc": "获取此显示器的 SVG 徽章",
"embed_desc": "在您的应用程序中使用 <script> 或 <iframe> 嵌入此监视器。",
"cp_link": "复制链接",
"cpd_link": "链接已复制",
"cp_code": "复制代码",
"cpd_code": "代码已复制",
"status_x_minute": "%minute分钟的%status",
"status_x_minutes": "%minutes分钟的%status",
"status_x_hour_y_minute": "%hours小时%minutes分钟的%status",
"status_no_data": "没有数据",
"status_ok": "状态正常",
"am": "上午",
"pm": "下午"
},
"numbers": ["0", "1", "2", "3", "4", "5", "6", "7", "8", "9"]
"14 Days": "14天",
"30 Days": "30天",
"60 Days": "60天",
"7 Days": "7天",
"90 Days": "90天",
"Availability per Component": "每个组件的可用性",
"Badge": "徽章",
"Badge Copied": "徽章已复制",
"Code Copied": "代码已复制",
"Copy Code": "复制代码",
"Copy Link": "复制链接",
"Dark": "深色",
"Days": "",
"DEGRADED": "性能下降",
"DOWN": "不可用",
"Embed": "嵌入",
"Embed this monitor using &#x3C;script&#x3E; or &#x3C;iframe&#x3E; in your app.": "使用 <script> 或 <iframe> 将此监控嵌入您的应用。",
"Get SVG badge for this monitor": "获取此监控的SVG徽章",
"Get a LIVE Status for this monitor": "获取此监控的实时状态",
"IDENTIFIED": "已识别",
"Incident Updates": "事件更新",
"INVESTIGATING": "正在调查",
"Lasted for about %lastedFor": "持续了约 %lastedFor",
"Light": "浅色",
"Link Copied": "链接已复制",
"LIVE Status": "实时状态",
"Mode": "模式",
"MONITORING": "正在监控",
"No Data": "无数据",
"No Monitor Found": "未找到监控",
"No Updates Yet": "尚无更新",
"Ongoing Incidents": "正在处理的事件",
"Pinging": "正在测试",
"Recent Incidents": "最近的事件",
"RESOLVED": "已解决",
"Share": "分享",
"Share this monitor using a link with others": "通过链接与他人分享此监控",
"Standard": "标准",
"Started about %startedAt ago, lasted for about %lastedFor": "大约 %startedAt 前开始,持续了约 %lastedFor",
"Started about %startedAt ago, still ongoing": "大约 %startedAt 前开始,目前仍在进行中",
"Starts in %startedAt": "%startedAt 后开始",
"Starts in %startedAt, will last for about %lastedFor": "%startedAt 后开始,将持续约 %lastedFor",
"Status": "状态",
"Status OK": "状态正常",
"Theme": "主题",
"Today": "今天",
"UP": "可用",
"Updates": "更新",
"Uptime": "正常运行时间",
"%status for %duration": "%duration 内的 %status"
}
+1 -4
View File
@@ -1,11 +1,9 @@
// @ts-nocheck
import notification from "./notification/notif.js";
import { ParseIncidentPayload, GHIssueToKenerIncident } from "./webhook.js";
import moment from "moment";
import {
GetAllSiteData,
GetGithubData,
GetAllTriggers,
CreateIncident,
AddIncidentComment,
@@ -94,7 +92,6 @@ function createClosureComment(alert, commonJSON) {
async function alerting(m) {
let monitor = await db.getMonitorByTag(m.tag);
let siteData = await GetAllSiteData();
const githubData = await GetGithubData();
const triggers = await GetAllTriggers({
status: "ACTIVE"
});
@@ -115,7 +112,7 @@ async function alerting(m) {
const successThreshold = alertConfig.successThreshold;
const monitor_tag = monitor.tag;
const alertingChannels = alertConfig.triggers; //array of numbers of trigger ids
const createIncident = alertConfig.createIncident === "YES" && !!githubData;
const createIncident = alertConfig.createIncident === "YES";
const allMonitorClients = [];
const sendTrigger = alertConfig.active;
const severity = alertConfig.severity;
-13
View File
@@ -50,11 +50,6 @@ const siteDataKeys = [
isValid: (value) => typeof value === "string" && value.trim().length > 0,
data_type: "string"
},
{
key: "github",
isValid: IsValidGHObject,
data_type: "object"
},
{
key: "metaTags",
isValid: IsValidJSONString,
@@ -182,14 +177,6 @@ export async function GetAllSiteData() {
return transformedData;
}
export async function GetGithubData() {
let data = await db.getSiteData("github");
if (!data) {
return null;
}
return JSON.parse(data.value);
}
export const CreateUpdateMonitor = async (monitor) => {
let monitorData = { ...monitor };
if (monitorData.id) {
-447
View File
@@ -1,447 +0,0 @@
// @ts-nocheck
import axios from "axios";
import { GetMinuteStartNowTimestampUTC, GenerateRandomColor } from "./tool.js";
import { marked } from "marked";
import { fileURLToPath } from "url";
import { dirname } from "path";
import dotenv from "dotenv";
import { GetGithubData } from "./controllers/controller.js";
dotenv.config();
const GH_TOKEN = process.env.GH_TOKEN;
const GhNotConfiguredMsg =
"Github owner or repo or GH_TOKEN is undefined. Read the docs to configure github: https://kener.ing/docs/gh-setup/";
/**
* @param {any} url
*/
function getAxiosOptions(url) {
const options = {
url: url,
method: "GET",
headers: {
Accept: "application/vnd.github+json",
Authorization: "Bearer " + GH_TOKEN,
"X-GitHub-Api-Version": "2022-11-28"
}
};
return options;
}
function postAxiosOptions(url, data) {
const options = {
url: url,
method: "POST",
headers: {
Accept: "application/vnd.github+json",
Authorization: "Bearer " + GH_TOKEN,
"X-GitHub-Api-Version": "2022-11-28"
},
data: data
};
return options;
}
function patchAxiosOptions(url, data) {
const options = {
url: url,
method: "PATCH",
headers: {
Accept: "application/vnd.github+json",
Authorization: "Bearer " + GH_TOKEN,
"X-GitHub-Api-Version": "2022-11-28"
},
data: data
};
return options;
}
const GetAllGHLabels = async function () {
const githubData = await GetGithubData();
if (githubData === null || GH_TOKEN === undefined) {
console.warn(GhNotConfiguredMsg);
return [];
}
const options = getAxiosOptions(
`${githubData.apiURL}/repos/${githubData.owner}/${githubData.repo}/labels?per_page=1000`
);
let labels = [];
try {
const response = await axios.request(options);
labels = response.data.map((label) => label.name);
} catch (error) {
console.log(error.response?.data);
return [];
}
return labels;
};
const CreateGHLabel = async function (label, description, color) {
const githubData = await GetGithubData();
if (githubData === null || GH_TOKEN === undefined) {
console.warn(GhNotConfiguredMsg);
return null;
}
if (color === undefined) {
color = GenerateRandomColor();
}
const options = postAxiosOptions(
`${githubData.apiURL}/repos/${githubData.owner}/${githubData.repo}/labels`,
{
name: label,
color: color,
description: description
}
);
try {
const response = await axios.request(options);
return response.data;
} catch (error) {
console.log(error.response.data);
return null;
}
};
const GetStartTimeFromBody = function (text) {
const pattern = /\[start_datetime:(\d+)\]/;
const matches = pattern.exec(text);
if (matches) {
const timestamp = matches[1];
return parseInt(timestamp);
}
return null;
};
const GetEndTimeFromBody = function (text) {
const pattern = /\[end_datetime:(\d+)\]/;
const matches = pattern.exec(text);
if (matches) {
const timestamp = matches[1];
return parseInt(timestamp);
}
return null;
};
const GetIncidentByNumber = async function (incident_number) {
const githubData = await GetGithubData();
if (githubData === null || GH_TOKEN === undefined) {
console.warn(GhNotConfiguredMsg);
return null;
}
const url = `${githubData.apiURL}/repos/${githubData.owner}/${githubData.repo}/issues/${incident_number}`;
const options = getAxiosOptions(url);
try {
const response = await axios.request(options);
return response.data;
} catch (error) {
console.log(error.message, options, url);
return null;
}
};
const GetIncidents = async function (tagName, state = "all") {
const githubData = await GetGithubData();
if (githubData === null || GH_TOKEN === undefined) {
console.warn(GhNotConfiguredMsg);
return [];
}
if (tagName === undefined) {
return [];
}
const since = GetMinuteStartNowTimestampUTC() - githubData.incidentSince * 60 * 60;
const sinceISO = new Date(since * 1000).toISOString();
const url = `${githubData.apiURL}/repos/${githubData.owner}/${githubData.repo}/issues?state=${state}&labels=${tagName},incident&sort=created&direction=desc&since=${sinceISO}`;
const options = getAxiosOptions(url);
try {
const response = await axios.request(options);
let issues = response.data;
//issues.createAt should be after sinceISO
issues = issues.filter((issue) => {
return new Date(issue.created_at) >= new Date(sinceISO);
});
return issues;
} catch (error) {
console.log(error.response?.data);
return [];
}
};
const GetIncidentsManual = async function (tagName, state = "all") {
const githubData = await GetGithubData();
if (githubData === null || GH_TOKEN === undefined) {
console.warn(GhNotConfiguredMsg);
return [];
}
if (tagName === undefined) {
return [];
}
const since = GetMinuteStartNowTimestampUTC() - githubData.incidentSince * 60 * 60;
const sinceISO = new Date(since * 1000).toISOString();
const url = `${githubData.apiURL}/repos/${githubData.owner}/${githubData.repo}/issues?state=${state}&labels=${tagName},incident,manual&sort=created&direction=desc&since=${sinceISO}`;
const options = getAxiosOptions(url);
try {
const response = await axios.request(options);
let issues = response.data;
//issues.createAt should be after sinceISO
issues = issues.filter((issue) => {
return new Date(issue.created_at) >= new Date(sinceISO);
});
return issues;
} catch (error) {
console.log(error.response?.data);
return [];
}
};
const GetOpenIncidents = async function () {
const githubData = await GetGithubData();
if (githubData === null || GH_TOKEN === undefined) {
console.warn(GhNotConfiguredMsg);
return [];
}
const since = GetMinuteStartNowTimestampUTC() - githubData.incidentSince * 60 * 60;
const sinceISO = new Date(since * 1000).toISOString();
const url = `${githubData.apiURL}/repos/${githubData.owner}/${githubData.repo}/issues?state=open&labels=incident&sort=created&direction=desc&since=${sinceISO}`;
const options = getAxiosOptions(url);
try {
const response = await axios.request(options);
let issues = response.data;
//issues.createAt should be after sinceISO
issues = issues.filter((issue) => {
return new Date(issue.created_at) >= new Date(sinceISO);
});
return issues;
} catch (error) {
console.log(error.response?.data);
return [];
}
};
function FilterAndInsertMonitorInIncident(openIncidentsReduced, monitorsActive) {
let openIncidentExploded = [];
for (let i = 0; i < openIncidentsReduced.length; i++) {
for (let j = 0; j < monitorsActive.length; j++) {
if (openIncidentsReduced[i].labels.includes(monitorsActive[j].tag)) {
let incident = JSON.parse(JSON.stringify(openIncidentsReduced[i]));
incident.monitor = {
name: monitorsActive[j].name,
tag: monitorsActive[j].tag,
image: monitorsActive[j].image,
description: monitorsActive[j].description
};
openIncidentExploded.push(incident);
}
}
}
return openIncidentExploded;
}
function Mapper(issue) {
const html = marked.parse(issue.body);
//convert issue.created_at from iso to timestamp UTC minutes
const issueCreatedAt = new Date(issue.created_at);
const issueCreatedAtTimestamp = issueCreatedAt.getTime() / 1000;
//convert issue.closed_at from iso to timestamp UTC minutes
let issueClosedAtTimestamp = null;
if (issue.closed_at !== null) {
const issueClosedAt = new Date(issue.closed_at);
issueClosedAtTimestamp = issueClosedAt.getTime() / 1000;
}
let labels = issue.labels.map(function (label) {
return label.name;
});
//find and add monitors tag in labels
let res = {
title: issue.title,
incident_start_time: GetStartTimeFromBody(issue.body) || issueCreatedAtTimestamp,
incident_end_time: GetEndTimeFromBody(issue.body) || issueClosedAtTimestamp,
number: issue.number,
body: html,
created_at: issue.created_at,
updated_at: issue.updated_at,
collapsed: true,
// @ts-ignore
state: issue.state,
closed_at: issue.closed_at,
// @ts-ignore
labels: labels,
html_url: issue.html_url,
comments: []
};
return res;
}
async function GetCommentsForIssue(issueID) {
const githubData = await GetGithubData();
if (githubData === null || GH_TOKEN === undefined) {
console.warn(GhNotConfiguredMsg);
return [];
}
const url = `${githubData.apiURL}/repos/${githubData.owner}/${githubData.repo}/issues/${issueID}/comments`;
try {
const response = await axios.request(getAxiosOptions(url));
return response.data;
} catch (error) {
console.log(error.response.data);
return [];
}
}
async function CreateIssue(issueTitle, issueBody, issueLabels) {
const githubData = await GetGithubData();
if (githubData === null || GH_TOKEN === undefined) {
console.warn(GhNotConfiguredMsg);
return null;
}
const url = `${githubData.apiURL}/repos/${githubData.owner}/${githubData.repo}/issues`;
try {
const payload = {
title: issueTitle,
body: issueBody,
labels: issueLabels
};
const response = await axios.request(postAxiosOptions(url, payload));
return response.data;
} catch (error) {
console.log(error.response.data);
return null;
}
}
async function UpdateIssue(incident_number, issueTitle, issueBody, issueLabels, state = "open") {
const githubData = await GetGithubData();
if (githubData === null || GH_TOKEN === undefined) {
console.warn(GhNotConfiguredMsg);
return null;
}
const url = `${githubData.apiURL}/repos/${githubData.owner}/${githubData.repo}/issues/${incident_number}`;
try {
const payload = {
title: issueTitle,
body: issueBody,
labels: issueLabels,
state: state
};
const response = await axios.request(patchAxiosOptions(url, payload));
return response.data;
} catch (error) {
console.log(error.response.data);
return null;
}
}
async function CloseIssue(incident_number) {
const githubData = await GetGithubData();
if (githubData === null || GH_TOKEN === undefined) {
console.warn(GhNotConfiguredMsg);
return null;
}
const url = `${githubData.apiURL}/repos/${githubData.owner}/${githubData.repo}/issues/${incident_number}`;
try {
const payload = {
state: "closed"
};
const response = await axios.request(patchAxiosOptions(url, payload));
return response.data;
} catch (error) {
console.log(error.response.data);
return null;
}
}
async function AddComment(incident_number, commentBody) {
const githubData = await GetGithubData();
if (githubData === null || GH_TOKEN === undefined) {
console.warn(GhNotConfiguredMsg);
return null;
}
const url = `${githubData.apiURL}/repos/${githubData.owner}/${githubData.repo}/issues/${incident_number}/comments`;
try {
const payload = {
body: commentBody
};
const response = await axios.request(postAxiosOptions(url, payload));
return response.data;
} catch (error) {
console.log(error.response.data);
return null;
}
}
//update issue labels
async function UpdateIssueLabels(incident_number, issueLabels, body, state = "open") {
const githubData = await GetGithubData();
if (githubData === null || GH_TOKEN === undefined) {
console.warn(GhNotConfiguredMsg);
return null;
}
const url = `${githubData.apiURL}/repos/${githubData.owner}/${githubData.repo}/issues/${incident_number}`;
try {
const payload = {
labels: issueLabels,
body: body,
state: state
};
const response = await axios.request(patchAxiosOptions(url, payload));
return response.data;
} catch (error) {
console.log(error.response.data);
return null;
}
}
//search issue
async function SearchIssue(query, page, per_page) {
const githubData = await GetGithubData();
if (githubData === null || GH_TOKEN === undefined) {
console.warn(GhNotConfiguredMsg);
return null;
}
query.push("repo:" + githubData.owner + "/" + githubData.repo);
const searchQuery = query
.filter(function (q) {
if (q == "" || q === undefined || q === null) {
return false;
}
const qs = q.split(":");
if (qs.length < 2) {
return false;
}
if (qs[1] === "" || qs[1] === undefined || qs[1] === null) {
return false;
}
return true;
})
.join(" ");
const url = `${githubData.apiURL}/search/issues?q=${encodeURIComponent(
searchQuery
)}&per_page=${per_page}&page=${page}`;
try {
const response = await axios.request(getAxiosOptions(url));
return response.data;
} catch (error) {
console.log(error.response.data);
return [];
}
}
export {
GetAllGHLabels,
CreateGHLabel,
GetIncidents,
GetStartTimeFromBody,
GetEndTimeFromBody,
GetCommentsForIssue,
Mapper,
CreateIssue,
AddComment,
GetIncidentByNumber,
UpdateIssueLabels,
UpdateIssue,
CloseIssue,
GetOpenIncidents,
FilterAndInsertMonitorInIncident,
SearchIssue,
GetIncidentsManual
};
+24 -13
View File
@@ -11,13 +11,13 @@ import {
import { GetDataGroupByDayAlternative } from "$lib/server/controllers/controller.js";
function getDayMessage(type, numOfMinute) {
function getSummaryDuration(numOfMinute) {
if (numOfMinute > 59) {
let hour = Math.floor(numOfMinute / 60);
let minute = numOfMinute % 60;
return `${type} for ${hour}h:${minute}m`;
return `${hour} hours ${minute} minute${minute > 1 ? "s" : ""}`;
} else {
return `${type} for ${numOfMinute} minute${numOfMinute > 1 ? "s" : ""}`;
return `${numOfMinute} minute${numOfMinute > 1 ? "s" : ""}`;
}
}
function getTimezoneOffset(timeZone) {
@@ -92,6 +92,8 @@ const FetchData = async function (site, monitor, localTz) {
timestamp: i,
cssClass: StatusObj.NO_DATA,
textClass: StatusObj.NO_DATA,
summaryDuration: 0,
summaryStatus: NO_DATA,
message: NO_DATA,
border: true,
ij: ij
@@ -110,13 +112,17 @@ const FetchData = async function (site, monitor, localTz) {
let totalDownCount = 0;
let totalUpCount = 0;
let summaryText = NO_DATA;
let summaryDuration = 0;
let summaryStatus = "UP";
let summaryColorClass = "api-nodata";
for (let i = 0; i < dbData.length; i++) {
let dayData = dbData[i];
let ts = dayData.timestamp;
let cssClass = StatusObj.UP;
let message = "Status OK";
let summaryDuration = 0;
let summaryStatus = "UP";
totalDegradedCount += dayData.DEGRADED;
totalDownCount += dayData.DOWN;
@@ -124,17 +130,20 @@ const FetchData = async function (site, monitor, localTz) {
if (dayData.DEGRADED >= monitor.day_degraded_minimum_count) {
cssClass = returnStatusClass(dayData.DEGRADED, StatusObj.DEGRADED, site.barStyle);
message = getDayMessage("DEGRADED", dayData.DEGRADED);
summaryDuration = getSummaryDuration(dayData.DEGRADED);
summaryStatus = "DEGRADED";
}
if (dayData.DOWN >= monitor.day_down_minimum_count) {
cssClass = returnStatusClass(dayData.DOWN, StatusObj.DOWN, site.barStyle);
message = getDayMessage("DOWN", dayData.DOWN);
summaryDuration = getSummaryDuration(dayData.DOWN);
summaryStatus = "DOWN";
}
if (!!_90Day[ts]) {
_90Day[ts].timestamp = ts;
_90Day[ts].cssClass = cssClass;
_90Day[ts].message = message;
_90Day[ts].summaryDuration = summaryDuration;
_90Day[ts].summaryStatus = summaryStatus;
_90Day[ts].textClass = cssClass.replace(/-\d+$/, "");
}
}
@@ -154,31 +163,33 @@ const FetchData = async function (site, monitor, localTz) {
latestTimestamp + secondsInDay
);
summaryText = "Status OK";
summaryColorClass = "api-up";
let lastRow = todayDataDb[todayDataDb.length - 1];
if (!!lastRow && lastRow.status == "DEGRADED") {
summaryText = getDayMessage(
"DEGRADED",
summaryDuration = getSummaryDuration(
getCountOfSimilarStatuesEnd(todayDataDb, "DEGRADED")
);
summaryStatus = "DEGRADED";
summaryColorClass = "api-degraded";
}
if (!!lastRow && lastRow.status == "DOWN") {
summaryText = getDayMessage("DOWN", getCountOfSimilarStatuesEnd(todayDataDb, "DOWN"));
summaryDuration = getSummaryDuration(getCountOfSimilarStatuesEnd(todayDataDb, "DOWN"));
summaryStatus = "DOWN";
summaryColorClass = "api-down";
}
} else {
let lastData = _90Day[latestTimestamp];
summaryText = lastData.message;
summaryColorClass = lastData.cssClass.replace(/-\d+$/, "");
summaryDuration = lastData.summaryDuration;
summaryStatus = lastData.summaryStatus;
}
return {
_90Day: _90Day,
uptime90Day: uptime90Day,
summaryText: summaryText,
summaryDuration: summaryDuration,
summaryStatus: summaryStatus,
summaryColorClass: summaryColorClass,
barRoundness: site.barRoundness,
midnight90DaysAgo: midnight90DaysAgo,
+3 -160
View File
@@ -6,10 +6,6 @@ import {
ParseUptime,
GetDayStartTimestampUTC
} from "./tool.js";
import { GetStartTimeFromBody, GetEndTimeFromBody } from "./github.js";
const API_TOKEN = process.env.API_TOKEN;
const API_IP = process.env.API_IP;
const API_IP_REGEX = process.env.API_IP_REGEX;
import db from "./db/db.js";
import { GetMonitors, VerifyAPIKey } from "./controllers/controller.js";
@@ -45,35 +41,11 @@ const CheckIfValidTag = async function (tag) {
const auth = async function (request) {
const authHeader = request.headers.get("authorization");
const authToken = authHeader?.replace("Bearer ", "");
// let ip = "";
// try {
// //ip can be in x-forwarded-for or x-real-ip or remoteAddress
// if (request.headers.get("x-forwarded-for") !== null) {
// ip = request.headers.get("x-forwarded-for").split(",")[0];
// } else if (request.headers.get("x-real-ip") !== null) {
// ip = request.headers.get("x-real-ip");
// } else if (request.connection && request.connection.remoteAddress !== null) {
// ip = request.connection.remoteAddress;
// } else if (request.socket && request.socket.remoteAddress !== null) {
// ip = request.socket.remoteAddress;
// }
// } catch (err) {
// console.log("IP Not Found " + err.message);
// }
if ((await VerifyAPIKey(authToken)) === false) {
return new Error("invalid token");
}
// if (API_IP !== undefined && ip != "") {
// if (API_IP !== ip) {
// return new Error(`invalid ip: ${ip}`);
// }
// }
// if (API_IP_REGEX !== undefined && ip != "") {
// const regex = new RegExp(API_IP_REGEX);
// if (!regex.test(ip)) {
// return new Error(`invalid ip regex: ${ip}`);
// }
// }
return null;
};
const store = async function (data) {
@@ -130,128 +102,7 @@ const store = async function (data) {
});
return { status: 200, message: "success at " + data.timestampInSeconds };
};
const GHIssueToKenerIncident = async function (issue) {
if (!!!issue) {
return null;
}
let issueLabels = issue.labels.map((label) => {
return label.name;
});
let tagsAvailable = await GetAllTags();
//get common tags as array
let commonTags = tagsAvailable.filter((tag) => issueLabels.includes(tag));
let resp = {
created_at: Math.floor(new Date(issue.created_at).getTime() / 1000), //in seconds
closedAt: issue.closed_at ? Math.floor(new Date(issue.closed_at).getTime() / 1000) : null,
title: issue.title,
tags: commonTags,
incident_number: issue.number
};
resp.startDatetime = GetStartTimeFromBody(issue.body);
resp.endDatetime = GetEndTimeFromBody(issue.body);
let body = issue.body;
body = body.replace(/\[start_datetime:(\d+)\]/g, "");
body = body.replace(/\[end_datetime:(\d+)\]/g, "");
resp.body = body.trim();
resp.impact = null;
if (issueLabels.includes("incident-down")) {
resp.impact = "DOWN";
} else if (issueLabels.includes("incident-degraded")) {
resp.impact = "DEGRADED";
}
resp.isMaintenance = false;
if (issueLabels.includes("maintenance")) {
resp.isMaintenance = true;
}
resp.isIdentified = false;
resp.isResolved = false;
if (issueLabels.includes("identified")) {
resp.isIdentified = true;
}
if (issueLabels.includes("resolved")) {
resp.isResolved = true;
}
return resp;
};
const ParseIncidentPayload = async function (payload) {
let startDatetime = payload.startDatetime; //in utc seconds optional
let endDatetime = payload.endDatetime; //in utc seconds optional
let title = payload.title; //string required
let body = payload.body || ""; //string optional
let tags = payload.tags; //string and required
let impact = payload.impact; //string and optional
let isMaintenance = payload.isMaintenance; //boolean and optional
let isIdentified = payload.isIdentified; //string and optional and if present can be resolved or identified
let isResolved = payload.isResolved; //string and optional and if present can be resolved or identified
// Perform validations
if (startDatetime && typeof startDatetime !== "number") {
return { error: "Invalid startDatetime" };
}
if (endDatetime && (typeof endDatetime !== "number" || endDatetime <= startDatetime)) {
return { error: "Invalid endDatetime" };
}
if (!title || typeof title !== "string") {
return { error: "Invalid title" };
}
//tags should be an array of string with atleast one element
if (
!tags ||
!Array.isArray(tags) ||
tags.length === 0 ||
tags.some((tag) => typeof tag !== "string")
) {
return { error: "Invalid tags" };
}
// Optional validation for body and impact
if (body && typeof body !== "string") {
return { error: "Invalid body" };
}
if (impact && (typeof impact !== "string" || ["DOWN", "DEGRADED"].indexOf(impact) === -1)) {
return { error: "Invalid impact" };
}
//check if tags are valid
const allTags = await GetAllTags();
if (tags.some((tag) => allTags.indexOf(tag) === -1)) {
return { error: "Unknown tags" };
}
// Optional validation for isMaintenance
if (isMaintenance && typeof isMaintenance !== "boolean") {
return { error: "Invalid isMaintenance" };
}
let githubLabels = ["incident"];
tags.forEach((tag) => {
githubLabels.push(tag);
});
if (impact) {
githubLabels.push("incident-" + impact.toLowerCase());
}
if (isMaintenance) {
githubLabels.push("maintenance");
}
if (isResolved !== undefined && isResolved === true) {
githubLabels.push("resolved");
}
if (isIdentified !== undefined && isIdentified === true) {
githubLabels.push("identified");
}
if (startDatetime) body = body + " " + `[start_datetime:${startDatetime}]`;
if (endDatetime) body = body + " " + `[end_datetime:${endDatetime}]`;
return { title, body, githubLabels };
};
const GetMonitorStatusByTag = async function (tag, timestamp) {
let monitor = await db.getMonitorByTag(tag);
if (!!!monitor) {
@@ -300,12 +151,4 @@ const GetMonitorStatusByTag = async function (tag, timestamp) {
resp.status = lastData.status;
return { status: 200, ...resp };
};
export {
store,
auth,
CheckIfValidTag,
GHIssueToKenerIncident,
ParseIncidentPayload,
GetAllTags,
GetMonitorStatusByTag
};
export { store, auth, CheckIfValidTag, GetAllTags, GetMonitorStatusByTag };
+6 -6
View File
@@ -95,7 +95,7 @@
<div class="grid w-full grid-cols-2 gap-4">
<div class="col-span-2 text-center md:col-span-1 md:text-left">
<Badge variant="outline" class="border-0 pl-0">
{l(data.lang, "root.ongoing_incidents")}
{l(data.lang, "Ongoing Incidents")}
</Badge>
</div>
</div>
@@ -121,7 +121,7 @@
<div class="grid w-full grid-cols-2 gap-4">
<div class="col-span-2 text-center md:col-span-1 md:text-left">
<Badge class="border-0 md:pl-0" variant="outline">
{l(data.lang, "root.availability_per_component")}
{l(data.lang, "Availability per Component")}
</Badge>
</div>
<div class="col-span-2 text-center md:col-span-1 md:text-right">
@@ -129,21 +129,21 @@
<span class="bg-api-up mr-1 inline-flex h-[8px] w-[8px] rounded-full opacity-75"
></span>
<span class="mr-3">
{l(data.lang, "statuses.UP")}
{l(data.lang, "UP")}
</span>
<span
class="bg-api-degraded mr-1 inline-flex h-[8px] w-[8px] rounded-full opacity-75"
></span>
<span class="mr-3">
{l(data.lang, "statuses.DEGRADED")}
{l(data.lang, "DEGRADED")}
</span>
<span
class="bg-api-down mr-1 inline-flex h-[8px] w-[8px] rounded-full opacity-75"
></span>
<span class="">
{l(data.lang, "statuses.DOWN")}
{l(data.lang, "DOWN")}
</span>
</Badge>
</div>
@@ -210,7 +210,7 @@
class="bounce-right grid w-full cursor-pointer grid-cols-2 justify-between gap-4 rounded-md border bg-card px-4 py-2 text-sm font-medium hover:bg-secondary"
>
<div class="col-span-1 text-left">
{l(data.lang, "root.recent_incidents")}
{l(data.lang, "Recent Incidents")}
</div>
<div class="text-right">
<span class="arrow float-right mt-0.5">
+1 -11
View File
@@ -59,18 +59,8 @@
<h1
class="scroll-m-20 text-center text-2xl font-extrabold tracking-tight lg:text-2xl"
>
No Monitor Found.
{l(data.lang, "No Monitor Found")}
</h1>
<p class="mt-3 text-center">
{l(data.lang, "root.read_doc_monitor")}
<a
href="https://kener.ing/docs#h1add-monitors"
target="_blank"
class="underline"
>
{l(data.lang, "root.here")}
</a>
</p>
</Card.Content>
</Card.Root>
</section>