Compare commits

..

22 Commits

Author SHA1 Message Date
Raj Nandan Sharma ee831f96bb Merge pull request #413 from rajnandan1/css-fix-1
chore: update version to 3.2.17 and adjust layout properties in monit…
2025-05-06 22:40:06 +05:30
Raj Nandan Sharma 5542790145 chore: update version to 3.2.17 and adjust layout properties in monitor component 2025-05-06 22:39:09 +05:30
github-actions 3b1091a34f Auto-generate README.md with release versions 2025-05-06 01:54:32 +00:00
Raj Nandan Sharma eebe6541c3 Merge pull request #408 from ToxykAuBleu/fix/gamedig-call
Fixing missing call of any gamedig monitors.
2025-05-06 07:05:59 +05:30
Raj Nandan Sharma e308089cfa refactor(triggerInfo): remove timeout for test loaders and improve image tag formatting 2025-05-06 07:05:04 +05:30
Raj Nandan Sharma 5132d6894e Merge pull request #407 from ToxykAuBleu/fix/compiler-warnings
Fixing compiler warnings.
2025-05-06 07:02:19 +05:30
Raj Nandan Sharma 006d1e1fa0 Merge pull request #411 from rajnandan1/fix/ui-cycle
fix(notifications): improve error handling and UI feedback
2025-05-06 07:01:05 +05:30
Raj Nandan Sharma 4c6dc24355 fix(notifications): improve error handling and UI feedback
Returns structured error messages from notification providers, updates API logic to surface errors to users, and enhances UI to display error or success states during trigger tests. Also adjusts monitor API request payload and corrects data range calculation.

Relates to improved reliability and user experience.
2025-05-06 06:57:12 +05:30
ToxykAuBleu acf227a947 Fixing missing call of any gamedig monitors. 2025-05-05 09:57:39 +00:00
ToxykAuBleu 87408d22c4 Merge branch 'main' into fix/compiler-warnings 2025-05-05 06:54:14 +00:00
Raj Nandan Sharma ea6754775d feat: update localization files with new duration options for multiple languages 2025-05-05 11:16:38 +05:30
Raj Nandan Sharma 3210f4d406 Merge pull request #406 from rajnandan1/cycle-changer
feat: add configurable home data range per device
2025-05-05 10:50:15 +05:30
Raj Nandan Sharma 16ac97a9c9 feat: document hidden categories and configurable data range in changelogs 2025-05-05 10:48:08 +05:30
Raj Nandan Sharma 8857a51dde chore: update version to 3.2.16 in package.json 2025-05-05 10:44:23 +05:30
Raj Nandan Sharma fc2abe7f16 refactor: remove commented code and clean up dropdown labels in homePage and monitor components 2025-05-05 10:31:15 +05:30
Raj Nandan Sharma f04a930e13 feat: add configurable home data range per device
Introduces per-device configuration for maximum data range and selectable days on the homepage, allowing separate settings for desktop and mobile. Updates UI, server logic, and data model to support these options, and uses user agent detection to apply the correct configuration. Improves flexibility for data display across devices.

Relates to #105
2025-05-05 10:28:02 +05:30
Raj Nandan Sharma 1de1e2d6ba Merge pull request #405 from rajnandan1/hide-cate
refactor: rename visibility property to isHidden and update related l…
2025-05-04 12:47:32 +05:30
Raj Nandan Sharma 0208e4baf9 refactor: rename visibility property to isHidden and update related logic #321 2025-05-04 12:46:11 +05:30
Raj Nandan Sharma 21af2a6d02 Merge pull request #399 from rohith1222004/feature/monitorVisibility
feat: implement visibility control for monitors
2025-05-04 12:11:37 +05:30
github-actions 2fbe301a51 Auto-generate README.md with release versions 2025-04-30 17:53:33 +00:00
Rohith Kumar 9462a0331a feat: implement visibility control for monitors
- Private monitors are visible only to logged-in users
- Public monitors are accessible to all
2025-04-30 15:46:08 +05:30
ToxykAuBleu 4e3d1838c0 Fixing compiler warnings. 2025-04-30 06:43:21 +00:00
41 changed files with 499 additions and 125 deletions
+9 -9
View File
@@ -79,7 +79,7 @@ npm run dev
Official Docker images for **Kener** are available on [Docker Hub](https://hub.docker.com/r/rajnandan1/kener). Multiple versions are maintained to support different use cases.
<a href="https://hub.docker.com/r/rajnandan1/kener/tags?page=1&ordering=last_updated&name=3.2.14"><img src="https://img.shields.io/badge/Latest_Stable_Release-3.2.14-blue" alt="Kener latest stable version: 3.2.14" /></a>
<a href="https://hub.docker.com/r/rajnandan1/kener/tags?page=1&ordering=last_updated&name=3.2.16"><img src="https://img.shields.io/badge/Latest_Stable_Release-3.2.16-blue" alt="Kener latest stable version: 3.2.16" /></a>
#### Available Tags
@@ -93,38 +93,38 @@ Official Docker images for **Kener** are available on [Docker Hub](https://hub.d
</tr>
<tr>
<td><a href="https://hub.docker.com/r/rajnandan1/kener/tags?page=1&ordering=last_updated&name=latest" target="_blank"><code>latest</code></td>
<td>Latest stable release (aka 3.2.14)</td>
<td>Latest stable release (aka 3.2.16)</td>
</tr>
<tr>
<td><a href="https://hub.docker.com/r/rajnandan1/kener/tags?page=1&ordering=last_updated&name=3.2.14" target="_blank"><code>3.2.14</code></a></td>
<td><a href="https://hub.docker.com/r/rajnandan1/kener/tags?page=1&ordering=last_updated&name=3.2.16" target="_blank"><code>3.2.16</code></a></td>
<td>Specific release version</td>
</tr>
<tr>
<td><a href="https://hub.docker.com/r/rajnandan1/kener/tags?page=1&ordering=last_updated&name=3.2" target="_blank"><code>3.2</code></a></td>
<td>Major-minor version tag pointing to the latest patch (3.2.14) release within that minor version (3.2.x)</td>
<td>Major-minor version tag pointing to the latest patch (3.2.16) release within that minor version (3.2.x)</td>
</tr>
<tr>
<td><a href="https://hub.docker.com/r/rajnandan1/kener/tags?page=1&ordering=last_updated&name=3" target="_blank"><code>3</code></a></td>
<td>Major version tag pointing to the latest stable (3.2.14) release within that major version (3.x.x)</td>
<td>Major version tag pointing to the latest stable (3.2.16) release within that major version (3.x.x)</td>
</tr>
<tr>
<td align="left" colspan="2" style="color:#0D597F;text-align:left;">Alpine Linux 3.21 w/ Node.js v23.7.0 &nbsp;<strong><em>(smallest image size)</em></strong></td>
</tr>
<tr>
<td><a href="https://hub.docker.com/r/rajnandan1/kener/tags?page=1&ordering=last_updated&name=alpine" target="_blank"><code>alpine</code></td>
<td>Latest stable release (aka 3.2.14)</td>
<td>Latest stable release (aka 3.2.16)</td>
</tr>
<tr>
<td><a href="https://hub.docker.com/r/rajnandan1/kener/tags?page=1&ordering=last_updated&name=3.2.14-alpine" target="_blank"><code>3.2.14-alpine</code></a></td>
<td><a href="https://hub.docker.com/r/rajnandan1/kener/tags?page=1&ordering=last_updated&name=3.2.16-alpine" target="_blank"><code>3.2.16-alpine</code></a></td>
<td>Specific release version</td>
</tr>
<tr>
<td><a href="https://hub.docker.com/r/rajnandan1/kener/tags?page=1&ordering=last_updated&name=3.2-alpine" target="_blank"><code>3.2-alpine</code></a></td>
<td>Major-minor version tag pointing to the latest patch (3.2.14) release within that minor version (3.2.x)</td>
<td>Major-minor version tag pointing to the latest patch (3.2.16) release within that minor version (3.2.x)</td>
</tr>
<tr>
<td><a href="https://hub.docker.com/r/rajnandan1/kener/tags?page=1&ordering=last_updated&name=3-alpine" target="_blank"><code>3-alpine</code></a></td>
<td>Major version tag pointing to the latest stable (3.2.14) release within that major version (3.x.x)</td>
<td>Major version tag pointing to the latest stable (3.2.16) release within that major version (3.x.x)</td>
</tr>
</table>
+7
View File
@@ -7,6 +7,13 @@ description: Changelogs for Kener
Here are the changelogs for Kener. Changelogs are only published when there are new features or breaking changes.
## v3.2.16
### ✨ Features
- **Hidden Categories:** You can now create hidden categories that will not show up for users that are not logged in. All the monitors will also not show up. Some one however can directly get to see them if they visit the monitor link directly
- **Configure data rage:** Now you can configure the data range in home page between 1 and 180 days. You can set different ranges for mobile and desktop devices.
## v3.2.14
### ✨ Features
+27 -1
View File
@@ -29,17 +29,25 @@ Copy and update the translation file in the `locales` folder. The translation fi
```json
{
"%status for %duration": "%status for %duration",
"120 Days": "120 Days",
"14 Days": "14 Days",
"150 Days": "150 Days",
"180 Days": "180 Days",
"30 Days": "30 Days",
"60 Days": "60 Days",
"7 Days": "7 Days",
"90 Days": "90 Days",
"Availability per Component": "Availability per Component",
"All Systems are Degraded": "All Systems are Degraded",
"All Systems are Down": "All Systems are Down",
"All Systems are Operational": "All Systems are Operational",
"Back": "Back",
"Badge Copied": "Badge Copied",
"Badge": "Badge",
"Browse Events": "Browse Events",
"Change Email": "Change Email",
"Code Copied": "Code Copied",
"Confirm Login": "Confirm Login",
"Copy Code": "Copy Code",
"Copy Link": "Copy Link",
"DEGRADED": "DEGRADED",
@@ -48,6 +56,9 @@ Copy and update the translation file in the `locales` folder. The translation fi
"Days": "Days",
"Embed this monitor using &#x3C;script&#x3E; or &#x3C;iframe&#x3E; in your app.": "Embed this monitor using <script> or <iframe> in your app.",
"Embed": "Embed",
"Enter the code": "Enter the code",
"Enter your email": "Enter your email",
"Enter your email to log in. If you are not subscribed, you will be prompted to subscribe.": "Enter your email to log in. If you are not subscribed, you will be prompted to subscribe.",
"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",
@@ -57,10 +68,12 @@ Copy and update the translation file in the `locales` folder. The translation fi
"Lasted for about %lastedFor": "Lasted for about %lastedFor",
"Light": "Light",
"Link Copied": "Link Copied",
"Login": "Login",
"MAINTENANCE": "MAINTENANCE",
"MONITORING": "MONITORING",
"Maintenance Completed": "Maintenance Completed",
"Maintenance in Progress": "Maintenance in Progress",
"Manage Subscription": "Manage Subscription",
"Mode": "Mode",
"No Data": "No Data",
"No Incident in %date": "No Incident in %date",
@@ -69,11 +82,15 @@ Copy and update the translation file in the `locales` folder. The translation fi
"No Updates Yet": "No Updates Yet",
"Ongoing Incidents": "Ongoing Incidents",
"Pinging": "Pinging",
"Please select specific monitors to receive updates from.": "Please select specific monitors to receive updates from.",
"RESOLVED": "RESOLVED",
"Recent Incidents": "Recent Incidents",
"Recent Maintenances": "Recent Maintenances",
"Share this monitor using a link with others": "Share this monitor using a link with others",
"Share": "Share",
"Some Systems are not working as expected": "Some Systems are not working as expected",
"Some Systems Degraded": "Some Systems Degraded",
"Some Systems Down": "Some Systems Down",
"Standard": "Standard",
"Started %startedAt, lasted for %lastedFor": "Started %startedAt, lasted for %lastedFor",
"Started %startedAt, still ongoing": "Started %startedAt, still ongoing",
@@ -82,14 +99,23 @@ Copy and update the translation file in the `locales` folder. The translation fi
"Starts %startedAt": "Starts %startedAt",
"Status OK": "Status OK",
"Status": "Status",
"Subscribe": "Subscribe",
"Subscribe to Updates": "Subscribe to Updates",
"Subscribe to all monitors": "Subscribe to all monitors",
"Switch to your timezone": "Switch to your timezone",
"Theme": "Theme",
"There are no upcoming maintenances": "There are no upcoming maintenances",
"Timezone set to %tz": "Timezone set to %tz",
"Today": "Today",
"UP": "UP",
"Update Subscription": "Update Subscription",
"Upcoming Maintenance": "Upcoming Maintenance",
"Upcoming Maintenances": "Upcoming Maintenances",
"Updates": "Updates",
"Uptime": "Uptime"
"Uptime": "Uptime",
"View in detail": "View in detail",
"We have sent a code to your email. Please enter it below to confirm your login": "We have sent a code to your email. Please enter it below to confirm your login",
"You are logged in as %email": "You are logged in as %email"
}
```
+9 -2
View File
@@ -1,12 +1,12 @@
{
"name": "kener",
"version": "3.2.14",
"version": "3.2.15",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "kener",
"version": "3.2.14",
"version": "3.2.15",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.26.10",
@@ -41,6 +41,7 @@
"knex": "^3.1.0",
"lucide-svelte": "^0.483.0",
"marked": "^11.1.1",
"mobile-detect": "^1.4.5",
"mode-watcher": "^0.4.1",
"moment": "^2.29.4",
"moment-timezone": "^0.5.43",
@@ -5604,6 +5605,12 @@
"integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==",
"license": "MIT"
},
"node_modules/mobile-detect": {
"version": "1.4.5",
"resolved": "https://registry.npmjs.org/mobile-detect/-/mobile-detect-1.4.5.tgz",
"integrity": "sha512-yc0LhH6tItlvfLBugVUEtgawwFU2sIe+cSdmRJJCTMZ5GEJyLxNyC/NIOAOGk67Fa8GNpOttO3Xz/1bHpXFD/g==",
"license": "MIT"
},
"node_modules/mode-watcher": {
"version": "0.4.1",
"resolved": "https://registry.npmjs.org/mode-watcher/-/mode-watcher-0.4.1.tgz",
+2 -1
View File
@@ -1,6 +1,6 @@
{
"name": "kener",
"version": "3.2.15",
"version": "3.2.17",
"private": false,
"license": "MIT",
"description": "Kener is a modern, open-source status page application built with Node.js. It provides real-time monitoring, uptime tracking, incident management, and beautiful dashboards. Perfect for DevOps teams, SaaS providers, and businesses needing reliable service status communication with minimal setup.",
@@ -99,6 +99,7 @@
"knex": "^3.1.0",
"lucide-svelte": "^0.483.0",
"marked": "^11.1.1",
"mobile-detect": "^1.4.5",
"mode-watcher": "^0.4.1",
"moment": "^2.29.4",
"moment-timezone": "^0.5.43",
+2 -1
View File
@@ -882,7 +882,8 @@ p code:not([class^="language-"]) {
margin-bottom: 1.3em;
}
input[type="checkbox"]:disabled + .peer {
input[type="checkbox"]:disabled + .peer,
label:has(input[type="checkbox"]:disabled) {
cursor: not-allowed;
opacity: 0.5;
}
+225 -6
View File
@@ -11,6 +11,8 @@
import * as DropdownMenu from "$lib/components/ui/dropdown-menu";
import Loader from "lucide-svelte/icons/loader";
import LaptopMinimal from "lucide-svelte/icons/laptop-minimal";
import SmartPhone from "lucide-svelte/icons/smartphone";
import X from "lucide-svelte/icons/x";
import Plus from "lucide-svelte/icons/plus";
import Info from "lucide-svelte/icons/info";
@@ -42,6 +44,18 @@
let homeIncidentStartTimeWithin = 30;
let incidentGroupView = "EXPAND_FIRST";
let kenerTheme = "default";
let homeDataMaxDays = {
desktop: {
maxDays: 90,
selectableDays: [1, 7, 14, 30, 60, 90]
},
mobile: {
maxDays: 90,
selectableDays: [1, 7, 14, 30, 60, 90]
}
};
let selectableDaysList = [1, 7, 14, 30, 60, 90, 120, 150, 180];
let availableThemes = [
{ name: "Default", value: "default" },
@@ -93,6 +107,9 @@
if (data.siteData.kenerTheme) {
kenerTheme = data.siteData.kenerTheme;
}
if (data.siteData.homeDataMaxDays) {
homeDataMaxDays = data.siteData.homeDataMaxDays;
}
if (data.siteData.tzToggle) {
tzToggle = data.siteData.tzToggle;
}
@@ -121,7 +138,6 @@
let resp = await storeSiteData({
hero: JSON.stringify(hero)
});
//print data
let data = await resp.json();
formStateHero = "idle";
if (data.error) {
@@ -259,7 +275,8 @@
...categories,
{
name: "",
description: ""
description: "",
isHidden: false
}
];
}
@@ -287,6 +304,23 @@
return;
}
}
let dataErrorMessage = "";
let formStateData = "idle";
async function formSubmitData() {
dataErrorMessage = "";
formStateData = "loading";
let resp = await storeSiteData({
homeDataMaxDays: JSON.stringify(homeDataMaxDays)
});
//print data
let data = await resp.json();
formStateData = "idle";
if (data.error) {
dataErrorMessage = data.error;
return;
}
}
</script>
<Card.Root class="mt-4">
@@ -379,6 +413,173 @@
</form>
</Card.Content>
</Card.Root>
<Card.Root class="mt-4">
<Card.Header class="border-b">
<Card.Title>Data</Card.Title>
<Card.Description>
Configure the data shown on the homepage. This includes the time range for incidents.
</Card.Description>
</Card.Header>
<Card.Content>
<form class="mx-auto mt-4 flex flex-col space-y-4" on:submit|preventDefault={formSubmitData}>
<div class="grid grid-cols-2 gap-4">
<div class="flex flex-col gap-y-2">
<Label class="text-lg">
<LaptopMinimal class="mr-2 inline h-4 w-4" />
Desktop
</Label>
<div class="border-b pb-4">
<Label for="desktopmaxdata" class="text-sm font-medium">Select Data Range</Label>
<Select.Root
portal={null}
onSelectedChange={(e) => {
homeDataMaxDays.desktop.maxDays = e.value;
//uncheck all checkboxes greater than maxDays
homeDataMaxDays.desktop.selectableDays = homeDataMaxDays.desktop.selectableDays.filter(
(day) => day <= e.value
);
//push all days less than maxDays
selectableDaysList.forEach((day) => {
if (day <= e.value && !homeDataMaxDays.desktop.selectableDays.includes(day)) {
homeDataMaxDays.desktop.selectableDays.push(day);
}
});
}}
selected={{
value: homeDataMaxDays.desktop.maxDays,
label: homeDataMaxDays.desktop.maxDays + " Days"
}}
>
<Select.Trigger class=" mt-2 w-[200px]" id="desktopmaxdata">
<Select.Value bind:value={homeDataMaxDays.desktop.maxDays} placeholder="Select Range" />
</Select.Trigger>
<Select.Content>
<Select.Group>
<Select.Label>Max Days</Select.Label>
{#each selectableDaysList as day}
<Select.Item value={day} label={day + " Days"} class="text-sm font-medium">
{day} Days
</Select.Item>
{/each}
</Select.Group>
</Select.Content>
</Select.Root>
</div>
<div class="border-b pb-4">
<Label for="desktopdropdown" class="text-sm font-medium">Select Dropdown Range</Label>
<div class="mt-2 flex flex-row flex-wrap gap-x-2">
{#each selectableDaysList as day}
<label>
<input
on:change={(e) => {
if (e.target.checked) {
homeDataMaxDays.desktop.selectableDays.push(day);
} else {
homeDataMaxDays.desktop.selectableDays = homeDataMaxDays.desktop.selectableDays.filter(
(d) => d !== day
);
}
}}
type="checkbox"
disabled={day >= homeDataMaxDays.desktop.maxDays}
checked={homeDataMaxDays.desktop.selectableDays.includes(day)}
/>
{day} Days
</label>
{/each}
</div>
</div>
</div>
<div class="flex flex-col gap-y-2">
<Label class="text-lg">
<SmartPhone class="mr-2 inline h-4 w-4" />
Mobile
</Label>
<div class="border-b pb-4">
<Label for="mobilemaxdata" class="text-sm font-medium">Select Data Range</Label>
<Select.Root
portal={null}
onSelectedChange={(e) => {
homeDataMaxDays.mobile.maxDays = e.value;
//uncheck all checkboxes greater than maxDays
homeDataMaxDays.mobile.selectableDays = homeDataMaxDays.mobile.selectableDays.filter(
(day) => day <= e.value
);
//push all days less than maxDays
selectableDaysList.forEach((day) => {
if (day <= e.value && !homeDataMaxDays.mobile.selectableDays.includes(day)) {
homeDataMaxDays.mobile.selectableDays.push(day);
}
});
}}
selected={{
value: homeDataMaxDays.mobile.maxDays,
label: homeDataMaxDays.mobile.maxDays + " Days"
}}
>
<Select.Trigger class=" mt-2 w-[200px]" id="mobilemaxdata">
<Select.Value bind:value={homeDataMaxDays.mobile.maxDays} placeholder="Select Range" />
</Select.Trigger>
<Select.Content>
<Select.Group>
<Select.Label>Max Days</Select.Label>
{#each selectableDaysList as day}
<Select.Item value={day} label={day + " Days"} class="text-sm font-medium">
{day} Days
</Select.Item>
{/each}
</Select.Group>
</Select.Content>
</Select.Root>
</div>
<div class="border-b pb-4">
<Label for="mobiledropdown" class="text-sm font-medium">Select Dropdown Range</Label>
<div class="mt-2 flex flex-row flex-wrap gap-x-2">
{#each selectableDaysList as day}
<label>
<input
on:change={(e) => {
if (e.target.checked) {
homeDataMaxDays.mobile.selectableDays.push(day);
} else {
homeDataMaxDays.mobile.selectableDays = homeDataMaxDays.mobile.selectableDays.filter(
(d) => d !== day
);
}
}}
type="checkbox"
disabled={day >= homeDataMaxDays.mobile.maxDays}
checked={homeDataMaxDays.mobile.selectableDays.includes(day)}
/>
{day} Days
</label>
{/each}
</div>
</div>
</div>
</div>
<p>
<span class="text-xs font-medium text-muted-foreground">
Select the maximum number of days to show data for. This will be used to filter the data shown on the
homepage. The dropdown will show all the days that are less than or equal to the selected max days. The max
value in the dropdown will be the entire value of data range it cannot be changed
</span>
</p>
{#if !!dataErrorMessage}
<p class="text-sm font-medium text-destructive">{dataErrorMessage}</p>
{/if}
<div class="flex w-full justify-end gap-x-2">
<Button type="submit" disabled={formStateData === "loading"}>
Save Data Settings
{#if formStateData === "loading"}
<Loader class="ml-2 inline h-4 w-4 animate-spin" />
{/if}
</Button>
</div>
</form>
</Card.Content>
</Card.Root>
<Card.Root class="mt-4">
<Card.Header class="border-b">
<Card.Title>Incidents</Card.Title>
@@ -652,8 +853,8 @@
<Card.Content>
<form class="mx-auto mt-4 space-y-4" on:submit|preventDefault={formSubmitCategories}>
{#each categories as cat, i}
<div class="grid grid-cols-4 gap-2">
<div class="col-span-1">
<div class="flex items-center gap-4">
<div class="flex-1">
<Label for="{i}catkey" class="block w-full">Name</Label>
<Input
bind:value={cat.name}
@@ -664,7 +865,7 @@
disabled={i === 0}
/>
</div>
<div class="relative col-span-3 pr-8">
<div class="flex-1">
<Label for="{i}catdsc" class="block w-full">Description</Label>
<Input
bind:value={cat.description}
@@ -674,9 +875,27 @@
placeholder="Category Description"
disabled={i === 0}
/>
</div>
<div class="relative flex flex-col items-center gap-y-2">
<Label for="{i}vis" class="block w-full">Visibility</Label>
<DropdownMenu.Root>
<DropdownMenu.Trigger disabled={i === 0} id="{i}vis">
<Button variant="outline" class="mr-2" disabled={cat.name === "Home"}>
{cat.isHidden ? "Private" : "Public"}
</Button>
</DropdownMenu.Trigger>
<DropdownMenu.Content>
<DropdownMenu.Group>
<DropdownMenu.Item on:click={() => (cat.isHidden = true)}>Private</DropdownMenu.Item>
<DropdownMenu.Item on:click={() => (cat.isHidden = false)}>Public</DropdownMenu.Item>
</DropdownMenu.Group>
</DropdownMenu.Content>
</DropdownMenu.Root>
</div>
<div class="flex items-center">
<Button
variant="ghost"
class="absolute right-0 top-8 ml-2 h-6 w-6 p-1 "
class="mt-4 h-6 w-6 p-1"
disabled={i === 0}
on:click={() => (categories = categories.filter((_, index) => index !== i))}
>
+14 -5
View File
@@ -275,10 +275,16 @@
body: JSON.stringify({ action: "testTrigger", data: { trigger_id, status } })
});
let resp = await data.json();
//check status code, if not 200 then show error
if (resp.error) {
triggers[i].testLoaders = "error";
alert(resp.error);
} else {
triggers[i].testLoaders = "success";
}
} catch (error) {
alert("Error: " + error);
} finally {
triggers[i].testLoaders = "success";
setTimeout(() => {
triggers[i].testLoaders = "idle";
}, 3000);
@@ -426,6 +432,9 @@
{#if trigger.testLoaders == "success"}
<Check class="mr-1 inline h-3 w-3 text-green-500 " />
{/if}
{#if trigger.testLoaders == "error"}
<X class="mr-1 inline h-3 w-3 text-red-500 " />
{/if}
<span class="h-4 text-xs font-medium"> Test Trigger </span>
</Button>
{#if $page.data.user.role != "member"}
@@ -493,7 +502,7 @@
newTrigger.trigger_type = "webhook";
}}
>
<img src="{base}/webhooks.svg" title="webhook" class="mr-4 w-6" />
<img src="{base}/webhooks.svg" title="webhook" class="mr-4 w-6" alt="Icon that represents webhooks" />
Webhooks
</Button>
<Button
@@ -503,7 +512,7 @@
newTrigger.trigger_type = "discord";
}}
>
<img src="{base}/discord.svg" title="discord" class="mr-4 w-6" />
<img src="{base}/discord.svg" title="discord" class="mr-4 w-6" alt="Discord logo" />
Discord
</Button>
<Button
@@ -513,7 +522,7 @@
newTrigger.trigger_type = "slack";
}}
>
<img src="{base}/slack.svg" title="slack" class="mr-4 w-6" />
<img src="{base}/slack.svg" title="slack" class="mr-4 w-6" alt="Slack logo" />
Slack
</Button>
@@ -524,7 +533,7 @@
newTrigger.trigger_type = "email";
}}
>
<img src="{base}/email.png" title="email" class="mr-4 w-6" />
<img src="{base}/email.png" title="email" class="mr-4 w-6" alt="Icon that represents emails" />
Email
</Button>
</div>
+54 -41
View File
@@ -39,6 +39,15 @@
let uptime90Day = monitor.pageData.uptime90Day;
let incidents = {};
let dayIncidentsFull = [];
let homeDataMaxDays = monitor.pageData.homeDataMaxDays;
let dimension = {
x1: 6,
x2: 4
};
dimension.x1 = ($page.data.isMobile ? 346 : 546) / homeDataMaxDays.maxDays;
dimension.x2 = (4 / 6) * dimension.x1;
function loadIncidents() {
axios
@@ -91,39 +100,37 @@
});
}, 1000 * 0.2);
}
let uptimesRollers = [
{
text: `${l(lang, "90 Days")}`,
startTs: monitor.pageData.startOfTheDay - 86400 * 89,
endTs: monitor.pageData.maxDateTodayTimestamp,
value: uptime90Day
},
{
text: `${l(lang, "60 Days")}`,
startTs: monitor.pageData.startOfTheDay - 86400 * 59,
endTs: monitor.pageData.maxDateTodayTimestamp
},
{
text: `${l(lang, "30 Days")}`,
startTs: monitor.pageData.startOfTheDay - 86400 * 29,
endTs: monitor.pageData.maxDateTodayTimestamp
},
{
text: `${l(lang, "14 Days")}`,
startTs: monitor.pageData.startOfTheDay - 86400 * 13,
endTs: monitor.pageData.maxDateTodayTimestamp
},
{
text: `${l(lang, "7 Days")}`,
startTs: monitor.pageData.startOfTheDay - 86400 * 6,
endTs: monitor.pageData.maxDateTodayTimestamp
},
{
text: l(lang, "Today"),
startTs: monitor.pageData.startOfTheDay,
endTs: monitor.pageData.maxDateTodayTimestamp
function returnUptimeRollers() {
let rollers = homeDataMaxDays.selectableDays;
//sort descending
rollers.sort((a, b) => b - a);
let ret = [];
for (let i = 0; i < rollers.length; i++) {
let roller = rollers[i];
if (roller == 1) {
ret.push({
text: `${l(lang, "Today")}`,
startTs: monitor.pageData.startOfTheDay,
endTs: monitor.pageData.maxDateTodayTimestamp
});
} else {
ret.push({
text: `${roller} ${l(lang, "Days")}`,
startTs: monitor.pageData.startOfTheDay - 86400 * (roller - 1),
endTs: monitor.pageData.maxDateTodayTimestamp
});
}
//if last index
if (i == 0) {
ret[i].value = uptime90Day;
}
}
];
return ret;
}
let uptimesRollers = returnUptimeRollers();
//start of the week moment
let rolledAt = 0;
@@ -140,7 +147,10 @@
uptimesRollers[newRolledAt].loading = true;
let resp = await axios.post(`${base}/api/today/aggregated`, {
monitor: monitor,
monitor: {
tag: monitor.tag,
include_degraded_in_downtime: monitor.include_degraded_in_downtime
},
startTs: uptimesRollers[newRolledAt].startTs,
endTs: uptimesRollers[newRolledAt].endTs
});
@@ -201,7 +211,7 @@
}
</script>
<div class="monitor relative grid w-full grid-cols-12 gap-2 pb-2 pt-0 md:w-[655px]">
<div class="monitor relative grid w-full max-w-[655px] grid-cols-12 gap-2 pb-2 pt-0">
{#if !!!embed}
<div class="col-span-12 md:w-[546px]">
<div class="pt-0">
@@ -302,9 +312,9 @@
showDailyDataModal = false;
}}
>
<div class="daygrid90 flex min-h-[60px] justify-start overflow-x-auto overflow-y-hidden py-1">
<div class="daygrid90 flex min-h-[60px] justify-between overflow-x-auto overflow-y-hidden py-1">
{#each Object.entries(_90Day) as [ts, bar]}
<a
<button
data-ts={ts}
use:hoverAction
on:hover={(e) => {
@@ -313,24 +323,27 @@
on:click={(e) => {
dailyDataGetter(e, bar, incidents[ts]);
}}
href="#"
class="oneline h-[34px] w-[6px]
class="oneline h-[34px]
{bar.border ? 'opacity-100' : 'opacity-20'} pb-1"
style="width: {dimension.x1}px"
>
<div
class="oneline-in h-[30px] bg-{bar.cssClass} mx-auto w-[4px] rounded-{monitor.pageData.barRoundness.toUpperCase() ==
class="oneline-in h-[30px] bg-{bar.cssClass} mx-auto rounded-{monitor.pageData.barRoundness.toUpperCase() ==
'SHARP'
? 'none'
: 'sm'}"
style="width: {dimension.x2}px"
></div>
<!-- incident dot -->
{#if !!incidents[ts]}
<div
class="bg-api-{incidents[
ts
].monitor_impact.toLowerCase()} comein absolute -bottom-[3px] left-[1px] h-[4px] w-[4px] rounded-full"
].monitor_impact.toLowerCase()} comein absolute -bottom-[3px] h-[4px] w-[4px] rounded-full"
style="left: {dimension.x1 / 2 - 2}px"
></div>
{/if}
</a>
</button>
{#if bar.showDetails}
<div class="show-hover absolute text-sm">
<div class="text-{bar.textClass} text-xs font-semibold">
-1
View File
@@ -18,7 +18,6 @@
export let monitor;
export let lang;
export let selectedLang = "en";
let theme = "light";
let copiedLink = false;
let embedType = "js";
+3
View File
@@ -1,6 +1,9 @@
{
"%status for %duration": "%status für %duration",
"120 Days": "120 Tage",
"14 Days": "14 Tage",
"150 Days": "150 Tage",
"180 Days": "180 Tage",
"30 Days": "30 Tage",
"60 Days": "60 Tage",
"7 Days": "7 Tage",
+3
View File
@@ -5,6 +5,9 @@
"60 Days": "60 Dage",
"7 Days": "7 Dage",
"90 Days": "90 Dage",
"120 Days": "120 Dage",
"150 Days": "150 Dage",
"180 Days": "180 Dage",
"Availability per Component": "Tilgængelighed pr. Komponent",
"All Systems are Degraded": "Alle Systemer er Forringet",
"All Systems are Down": "Alle Systemer er Nede",
+3
View File
@@ -1,6 +1,9 @@
{
"%status for %duration": "%status for %duration",
"120 Days": "120 Days",
"14 Days": "14 Days",
"150 Days": "150 Days",
"180 Days": "180 Days",
"30 Days": "30 Days",
"60 Days": "60 Days",
"7 Days": "7 Days",
+3
View File
@@ -5,6 +5,9 @@
"60 Days": "60 jours",
"7 Days": "7 jours",
"90 Days": "90 jours",
"120 Days": "120 jours",
"150 Days": "150 jours",
"180 Days": "180 jours",
"Availability per Component": "Disponibilité par composant",
"All Systems are Degraded": "Tous les systèmes sont dégradés",
"All Systems are Down": "Tous les systèmes sont hors ligne",
+3
View File
@@ -5,6 +5,9 @@
"60 Days": "60 दिन",
"7 Days": "7 दिन",
"90 Days": "90 दिन",
"120 Days": "120 दिन",
"150 Days": "150 दिन",
"180 Days": "180 दिन",
"Availability per Component": "प्रत्येक घटक की उपलब्धता",
"All Systems are Degraded": "सभी सिस्टम क्षीण हैं",
"All Systems are Down": "सभी सिस्टम डाउन हैं",
+3
View File
@@ -1,6 +1,9 @@
{
"%status for %duration": "%duration の間 %status",
"120 Days": "120日間",
"14 Days": "14日間",
"150 Days": "150日間",
"180 Days": "180日間",
"30 Days": "30日間",
"60 Days": "60日間",
"7 Days": "7日間",
+4 -1
View File
@@ -1,7 +1,10 @@
{
"%status for %duration": "%duration 동안 %status 상태였음",
"120 Days": "120 일",
"14 Days": "14 일",
"30 Days": "30 ",
"150 Days": "150 ",
"180 Days": "180 일",
"30 Days": "30 일",
"60 Days": "60 일",
"7 Days": "7 일",
"90 Days": "90 일",
+3
View File
@@ -5,6 +5,9 @@
"60 Days": "60 dager",
"7 Days": "7 dager",
"90 Days": "90 dager",
"120 Days": "120 dager",
"150 Days": "150 dager",
"180 Days": "180 dager",
"Availability per Component": "Tilgjengelighet per komponent",
"All Systems are Degraded": "Alle systemer har nedsatt ytelse",
"All Systems are Down": "Alle systemer er nede",
+3
View File
@@ -1,6 +1,9 @@
{
"%status for %duration": "%status voor %duration",
"120 Days": "120 dagen",
"14 Days": "14 dagen",
"150 Days": "150 dagen",
"180 Days": "180 dagen",
"30 Days": "30 dagen",
"60 Days": "60 dagen",
"7 Days": "7 dagen",
+3
View File
@@ -1,6 +1,9 @@
{
"%status for %duration": "%status przez %duration",
"120 Days": "120 dni",
"14 Days": "14 dni",
"150 Days": "150 dni",
"180 Days": "180 dni",
"30 Days": "30 dni",
"60 Days": "60 dni",
"7 Days": "7 dni",
+3
View File
@@ -1,6 +1,9 @@
{
"%status for %duration": "%status por %duration",
"120 Days": "120 Dias",
"14 Days": "14 Dias",
"150 Days": "150 Dias",
"180 Days": "180 Dias",
"30 Days": "30 Dias",
"60 Days": "60 Dias",
"7 Days": "7 Dias",
+3
View File
@@ -5,6 +5,9 @@
"60 Days": "60 Дней",
"7 Days": "7 Дней",
"90 Days": "90 Дней",
"120 Days": "120 Дней",
"150 Days": "150 Дней",
"180 Days": "180 Дней",
"Availability per Component": "Доступность по компонентам",
"All Systems are Degraded": "Все системы работают с ухудшенной производительностью",
"All Systems are Down": "Все системы недоступны",
+3
View File
@@ -5,6 +5,9 @@
"60 Days": "60 Gün",
"7 Days": "7 Gün",
"90 Days": "90 Gün",
"120 Days": "120 Gün",
"150 Days": "150 Gün",
"180 Days": "180 Gün",
"Availability per Component": "Bileşen Kullanılabilirliği",
"All Systems are Degraded": "Tüm Sistemler Bozuk Durumda",
"All Systems are Down": "Tüm Sistemler Çalışmıyor",
+3
View File
@@ -5,6 +5,9 @@
"60 Days": "60 ngày",
"7 Days": "7 ngày",
"90 Days": "90 ngày",
"120 Days": "120 ngày",
"150 Days": "150 ngày",
"180 Days": "180 ngày",
"Availability per Component": "Tính khả dụng theo thành phần",
"All Systems are Degraded": "Tất cả hệ thống đang suy giảm hiệu suất",
"All Systems are Down": "Tất cả hệ thống đang ngừng hoạt động",
+3
View File
@@ -1,6 +1,9 @@
{
"%status for %duration": "%duration 内的 %status",
"120 Days": "120天",
"14 Days": "14天",
"150 Days": "150天",
"180 Days": "180天",
"30 Days": "30天",
"60 Days": "60天",
"7 Days": "7天",
@@ -26,6 +26,11 @@ export const siteDataKeys = [
isValid: IsValidURL,
data_type: "string",
},
{
key: "homeDataMaxDays",
isValid: IsValidJSONString,
data_type: "object",
},
{
key: "home",
isValid: (value) => typeof value === "string" && value.trim().length > 0,
+2
View File
@@ -110,6 +110,8 @@ const Minuter = async (monitor) => {
realTimeData[startOfMinute] = await serviceClient.execute();
} else if (monitor.monitor_type === "HEARTBEAT") {
realTimeData[startOfMinute] = await serviceClient.execute();
} else if (monitor.monitor_type === "GAMEDIG") {
realTimeData[startOfMinute] = await serviceClient.execute();
}
manualData = await manualIncident(monitor);
+11 -1
View File
@@ -97,9 +97,19 @@ const seedSiteData = {
cssSrc: "",
family: "",
},
categories: [{ name: "Home", description: "Monitors for Home Page" }],
categories: [{ name: "Home", description: "Monitors for Home Page", isHidden: false }],
homeIncidentCount: 5,
homeIncidentStartTimeWithin: 30,
homeDataMaxDays: {
desktop: {
maxDays: 90,
selectableDays: [1, 7, 14, 30, 60, 90],
},
mobile: {
maxDays: 90,
selectableDays: [1, 7, 14, 30, 60, 90],
},
},
kenerTheme: "default",
showSiteStatus: "NO",
};
+1 -1
View File
@@ -64,7 +64,7 @@ class Discord {
return response;
} catch (error) {
console.error("Error sending webhook", error);
return error;
return { error: error.message };
}
}
}
+1 -1
View File
@@ -221,7 +221,7 @@ class Email {
return await transporter.sendMail(mailOptions);
} catch (error) {
console.error("Error sending email via SMTP", error);
return error;
return { error: error.message };
}
}
}
+1 -1
View File
@@ -58,7 +58,7 @@ class Slack {
return response;
} catch (error) {
console.error("Error sending webhook", error);
return error;
return { error: error.message };
}
}
}
+2 -2
View File
@@ -93,12 +93,12 @@ class Webhook {
body: this.transformData(data),
});
if (!response.ok) {
throw new Error(`Error from webhook`);
throw new Error(`Error from webhook: ${response.status} ${response.statusText}`);
}
return response;
} catch (error) {
console.error("Error sending webhook", error);
return error;
return { error: error.message };
}
}
}
+7 -3
View File
@@ -78,13 +78,16 @@ function getCountOfSimilarStatuesEnd(arr, statusType) {
return count;
}
const FetchData = async function (site, monitor, localTz, selectedLang, lang) {
const FetchData = async function (site, monitor, localTz, selectedLang, lang, isMobile) {
const secondsInDay = 24 * 60 * 60;
//get offset from utc in minutes
const nowUTC = GetMinuteStartNowTimestampUTC();
const midnightUTC = GetDayStartTimestampUTC(nowUTC);
let deviceType = isMobile ? "mobile" : "desktop";
let homeDataMaxDays = site.homeDataMaxDays[deviceType];
const midnightTz = BeginningOfDay({ timeZone: localTz });
const midnight90DaysAgoTz = midnightTz - 90 * 24 * 60 * 60;
const midnight90DaysAgoTz = midnightTz - (homeDataMaxDays.maxDays - 1) * 24 * 60 * 60;
const NO_DATA = "No Data";
let offsetInMinutes = parseInt((GetDayStartTimestampUTC(nowUTC) - midnightTz) / 60);
const maxDateTodayTimestampTz = BeginningOfMinute({ timeZone: localTz });
@@ -198,6 +201,7 @@ const FetchData = async function (site, monitor, localTz, selectedLang, lang) {
midnight90DaysAgo: midnight90DaysAgoTz,
maxDateTodayTimestamp: maxDateTodayTimestampTz,
startOfTheDay: midnightTz,
homeDataMaxDays,
};
};
export { FetchData };
+5 -1
View File
@@ -2,9 +2,13 @@
import i18n from "$lib/i18n/server";
import { redirect } from "@sveltejs/kit";
import { base } from "$app/paths";
import MobileDetect from "mobile-detect";
import { GetAllSiteData, IsSetupComplete, IsLoggedInSession } from "$lib/server/controllers/controller.js";
export async function load({ params, route, url, cookies, request }) {
const userAgent = request.headers.get("user-agent");
const md = new MobileDetect(userAgent);
const isMobile = !!md.mobile();
let isSetupComplete = await IsSetupComplete();
if (!isSetupComplete) {
throw redirect(302, base + "/manage/setup");
@@ -18,7 +22,6 @@ export async function load({ params, route, url, cookies, request }) {
let site = await GetAllSiteData();
const headers = request.headers;
const userAgent = headers.get("user-agent");
let localTz = "UTC";
const localTzQuery = query.get("tz");
@@ -56,5 +59,6 @@ export async function load({ params, route, url, cookies, request }) {
embed,
bgc,
isLoggedIn: !!isLoggedIn.user,
isMobile: isMobile,
};
}
@@ -3,33 +3,34 @@ import { FetchData } from "$lib/server/page";
import { GetMonitors } from "$lib/server/controllers/controller.js";
export async function load({ params, route, url, parent }) {
let monitors = await GetMonitors({ status: "ACTIVE" });
const parentData = await parent();
const siteData = parentData.site;
const monitorsActive = [];
const query = url.searchParams;
const theme = query.get("theme");
for (let i = 0; i < monitors.length; i++) {
//only return monitors that have category as home or category is not present
if (monitors[i].tag !== params.tag) {
continue;
}
delete monitors[i].api;
delete monitors[i].default_status;
monitors[i].embed = true;
let data = await FetchData(
siteData,
monitors[i],
parentData.localTz,
parentData.selectedLang,
parentData.lang
);
monitors[i].pageData = data;
monitorsActive.push(monitors[i]);
}
return {
monitors: monitorsActive,
theme,
openIncidents: []
};
let monitors = await GetMonitors({ status: "ACTIVE" });
const parentData = await parent();
const siteData = parentData.site;
const monitorsActive = [];
const query = url.searchParams;
const theme = query.get("theme");
for (let i = 0; i < monitors.length; i++) {
//only return monitors that have category as home or category is not present
if (monitors[i].tag !== params.tag) {
continue;
}
delete monitors[i].api;
delete monitors[i].default_status;
monitors[i].embed = true;
let data = await FetchData(
siteData,
monitors[i],
parentData.localTz,
parentData.selectedLang,
parentData.lang,
parentData.isMobile,
);
monitors[i].pageData = data;
monitorsActive.push(monitors[i]);
}
return {
monitors: monitorsActive,
theme,
openIncidents: [],
};
}
+7 -1
View File
@@ -1,6 +1,8 @@
// @ts-nocheck
import i18n from "$lib/i18n/server";
import { redirect } from "@sveltejs/kit";
import MobileDetect from "mobile-detect";
import { base } from "$app/paths";
import {
GetAllSiteData,
@@ -11,6 +13,10 @@ import {
} from "$lib/server/controllers/controller.js";
export async function load({ params, route, url, cookies, request }) {
const userAgent = request.headers.get("user-agent");
const md = new MobileDetect(userAgent);
const isMobile = !!md.mobile();
let isSetupComplete = await IsSetupComplete();
if (!isSetupComplete) {
throw redirect(302, base + "/manage/setup");
@@ -24,7 +30,6 @@ export async function load({ params, route, url, cookies, request }) {
let site = await GetAllSiteData();
const headers = request.headers;
const userAgent = headers.get("user-agent");
let localTz = "UTC";
const localTzCookie = cookies.get("localTz");
if (!!localTzCookie) {
@@ -50,5 +55,6 @@ export async function load({ params, route, url, cookies, request }) {
selectedLang: selectedLang,
isLoggedIn: !!isLoggedIn.user,
canSendEmail: IsEmailSetup(),
isMobile: isMobile,
};
}
+14 -2
View File
@@ -3,6 +3,7 @@ import { FetchData } from "$lib/server/page";
import { GetMonitors, GetIncidentsOpenHome, SystemDataMessage } from "$lib/server/controllers/controller.js";
import { SortMonitor } from "$lib/clientTools.js";
import moment from "moment";
import { redirect, error } from "@sveltejs/kit";
function removeTags(str) {
if (str === null || str === "") return false;
@@ -106,7 +107,14 @@ export async function load({ parent, url }) {
monitors = SortMonitor(siteData.monitorSort, monitors);
const monitorsActive = [];
for (let i = 0; i < monitors.length; i++) {
let data = await FetchData(siteData, monitors[i], parentData.localTz, parentData.selectedLang, parentData.lang);
let data = await FetchData(
siteData,
monitors[i],
parentData.localTz,
parentData.selectedLang,
parentData.lang,
parentData.isMobile,
);
monitors[i].pageData = data;
monitors[i].activeIncidents = [];
@@ -150,6 +158,9 @@ export async function load({ parent, url }) {
let allCategories = siteData.categories;
let selectedCategory = allCategories.find((category) => category.name === query.get("category"));
if (selectedCategory) {
if (!!selectedCategory.isHidden && !parentData.isLoggedIn) {
throw error(404, "Category not found");
}
pageTitle = selectedCategory.name + " - " + pageTitle;
pageDescription = selectedCategory.description;
canonical = canonical + "?category=" + query.get("category");
@@ -159,6 +170,8 @@ export async function load({ parent, url }) {
subtitle: selectedCategory.description,
image: selectedCategory.image,
};
} else {
throw error(404, "Category not found");
}
}
if (isCategoryPage || isMonitorPage || isGroupPage) {
@@ -176,7 +189,6 @@ export async function load({ parent, url }) {
return isPresent;
});
}
allOpenIncidents = allOpenIncidents.map((incident) => {
let incidentMonitors = incident.monitors;
let monitorTags = incidentMonitors.map((monitor) => monitor.monitor_tag);
+6 -5
View File
@@ -17,6 +17,7 @@
import { scale } from "svelte/transition";
import { format } from "date-fns";
import GMI from "$lib/components/gmi.svelte";
import { page } from "$app/stores";
export let data;
let shareMenusToggle = false;
@@ -242,10 +243,10 @@
</div>
</section>
<section
class="section-monitors z-20 mx-auto mb-8 flex w-full flex-1 flex-col items-start justify-center backdrop-blur-[2px] md:w-[655px]"
class="section-monitors z-20 mx-auto mb-8 flex w-full max-w-[655px] flex-1 flex-col items-center justify-center"
>
<Card.Root class="monitor-root">
<Card.Content class="monitors-card p-0">
<Card.Root class="monitor-root w-full">
<Card.Content class="monitors-card p-0 ">
{#each data.monitors as monitor}
<Monitor
on:show_shareMenu={showShareMenu}
@@ -261,9 +262,9 @@
{/if}
{#if data.site.categories && data.pageType == "home"}
<section
class="section-categories relative z-10 mx-auto mb-8 w-full max-w-[890px] flex-1 flex-col items-start backdrop-blur-[2px] md:w-[655px]"
class="section-categories relative z-10 mx-auto mb-8 w-full max-w-[655px] flex-1 flex-col items-start backdrop-blur-[2px] md:w-[655px]"
>
{#each data.site.categories.filter((e) => e.name != "Home") as category}
{#each data.site.categories.filter((category) => category.name != "Home" && (!!!category.isHidden || $page.data.isLoggedIn)) as category}
<a href={`?category=${category.name}`} rel="external">
<Card.Root class="mb-4 hover:bg-secondary">
<Card.Header class="bounce-right relative w-full cursor-pointer px-4 ">
@@ -3,8 +3,6 @@
import { onMount } from "svelte";
import { Button } from "$lib/components/ui/button";
import ExternalLink from "lucide-svelte/icons/external-link";
export let data;
</script>
<div class="min-h-[70vh]">
@@ -233,6 +233,10 @@ export async function POST({ request, cookies }) {
],
};
resp = await notificationClient.send(testObj);
//check if resp is error
if (resp.error) {
throw new Error(resp.error);
}
} else if (action == "testMonitor") {
let monitorID = data.monitor_id;
let monitors = await GetMonitorsParsed({ id: monitorID });
@@ -845,38 +845,38 @@
<div
class="bg-hover state-{newComment.state} mt-2 grid grid-cols-4 overflow-hidden rounded-md border text-xs font-medium"
>
<div
<button
class="col-span-1 cursor-pointer px-2 py-2 text-center hover:underline"
on:click={() => {
setCommentState("INVESTIGATING");
}}
>
INVESTIGATING
</div>
<div
</button>
<button
class="col-span-1 cursor-pointer px-2 py-2 text-center hover:underline"
on:click={() => {
setCommentState("IDENTIFIED");
}}
>
IDENTIFIED
</div>
<div
</button>
<button
class="col-span-1 cursor-pointer px-2 py-2 text-center hover:underline"
on:click={() => {
setCommentState("MONITORING");
}}
>
MONITORING
</div>
<div
</button>
<button
class="col-span-1 cursor-pointer px-2 py-2 text-center hover:underline"
on:click={() => {
setCommentState("RESOLVED");
}}
>
RESOLVED
</div>
</button>
</div>
{/if}
<div class="mt-4 flex w-full gap-4">