mirror of
https://github.com/rajnandan1/kener.git
synced 2026-04-22 18:20:29 -05:00
feat: i18n added french #179, removed github dependency, clean up old code, simpler i18n file
This commit is contained in:
+1
-1
@@ -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];
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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() {
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 <script> or <iframe> 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
@@ -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
@@ -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
@@ -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 <script> or <iframe> 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"
|
||||
}
|
||||
|
||||
@@ -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 <script> or <iframe> 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
@@ -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 <script> or <iframe> 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
@@ -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 <script> or <iframe> 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"
|
||||
}
|
||||
|
||||
@@ -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
@@ -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 <script> or <iframe> 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
@@ -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 <script> or <iframe> 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,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;
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
@@ -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
@@ -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 };
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user