Compare commits

..

37 Commits

Author SHA1 Message Date
Raj Nandan Sharma 6c7534b9b6 Merge pull request #374 from rajnandan1/release/3.2.13
Release/3.2.13
2025-04-08 10:46:07 +05:30
Raj Nandan Sharma ae6c2c985f feat: enhance incident details formatting and add active status filter for triggers #370 2025-04-08 10:44:17 +05:30
Raj Nandan Sharma 940331b87f feat: update package description and enhance incident date formatting #371 2025-04-08 09:23:12 +05:30
Raj Nandan Sharma e6b5600a47 feat: add Umami analytics support and enhance capture functionality 2025-04-02 21:16:34 +05:30
Raj Nandan Sharma ce2a6ab756 feat: update version to 3.2.13 and refactor monitoring data calculations 2025-04-01 22:51:55 +05:30
Raj Nandan Sharma 6af91af639 chore: bump version to 3.2.12 in package.json 2025-04-01 21:22:43 +05:30
github-actions e1ff156b91 Auto-generate README.md with release versions 2025-03-31 12:16:53 +00:00
Raj Nandan Sharma b56d20ac35 Merge pull request #367 from rajnandan1/release/3.2.12
feat: add kenerTheme support and enhance footer HTML structure
2025-03-31 17:17:30 +05:30
Raj Nandan Sharma 9ee7a7b861 feat: add kenerTheme support and enhance footer HTML structure 2025-03-31 17:12:40 +05:30
Raj Nandan Sharma f73ffce3f0 feat: enhance documentation and styling for custom JS/CSS guide and incident comments 2025-03-30 12:58:19 +05:30
Raj Nandan Sharma 0997967787 docs: update environment variables documentation for CORS and reverse proxy setup
feat: modify analytics event naming convention for consistency

fix: adjust delete monitor confirmation form layout for better UX

style: refine positioning of monitor action buttons for improved alignment

refactor: streamline password validation imports in setup and forgot password routes

fix: remove unnecessary line breaks in layout server file
2025-03-29 22:33:17 +05:30
github-actions 3476ec2b29 Auto-generate README.md with release versions 2025-03-28 16:39:18 +00:00
Raj Nandan Sharma e0cf568aa9 feat: implement custom handle function to modify response headers 2025-03-28 21:50:07 +05:30
Raj Nandan Sharma c12d7f8345 feat: add null check for document before dispatching analytics event 2025-03-28 21:18:26 +05:30
Raj Nandan Sharma 9ba115d5ab Merge pull request #366 from rajnandan1/release/3.2.11
release 3.2.11
2025-03-28 20:56:59 +05:30
Raj Nandan Sharma 3756bbfd06 chore: bump version to 3.2.11 in package.json 2025-03-28 20:54:50 +05:30
Raj Nandan Sharma 9096bcc641 feat: import analytics event module for enhanced tracking in incident page 2025-03-28 20:53:44 +05:30
Raj Nandan Sharma c4d04854e5 feat: enhance monitor deletion confirmation UI and add analytics event for incident back button click 2025-03-28 20:52:59 +05:30
Raj Nandan Sharma d95fc0005b feat: indicate Markdown support in incident summary and comment sections 2025-03-28 20:40:24 +05:30
Raj Nandan Sharma 2b9093184a feat: add support for Markdown language in events and enhance SEO documentation as requested in #359 2025-03-28 20:34:19 +05:30
Raj Nandan Sharma 831376054b feat: add analytics capture snippets and enhance event tracking also added plausible and ms clarity #355 2025-03-28 15:07:45 +05:30
Raj Nandan Sharma 885ee0b6d6 feat: implement monitor deletion functionality with confirmation 2025-03-26 11:47:53 +05:30
Raj Nandan Sharma 511d0f26dd feat: add email validation in CreateUpdateTrigger function 2025-03-26 10:16:36 +05:30
github-actions 5263698625 Auto-generate README.md with release versions 2025-03-25 17:45:05 +00:00
Raj Nandan Sharma 7859f9564e fix: bump version to 3.2.10 in package.json 2025-03-25 22:58:04 +05:30
github-actions 149456682d Auto-generate README.md with release versions 2025-03-25 17:20:55 +00:00
Raj Nandan Sharma 8c040997da fix: refactor monitorTriggers initialization into a separate function 2025-03-25 22:47:05 +05:30
Raj Nandan Sharma 868fed0176 fix: update tzdata version in Dockerfile to use latest available 2025-03-25 22:38:13 +05:30
Raj Nandan Sharma c7f44a48da fix: bump version to 3.2.9 in package.json 2025-03-25 22:31:57 +05:30
Raj Nandan Sharma 6300bff4e7 Merge pull request #363 from rajnandan1/fix/357
fix: update RBAC documentation and improve monitor data retrieval log…
2025-03-25 22:27:04 +05:30
Raj Nandan Sharma 3e6b1ffdbb fix: streamline query execution in DbImpl by removing redundant return statement 2025-03-25 22:24:10 +05:30
Raj Nandan Sharma 408fd5318b fix: update RBAC documentation and improve monitor data retrieval logic, fixes #357 and #361 2025-03-25 22:20:45 +05:30
Raj Nandan Sharma cfe419f6df Merge pull request #362 from rajnandan1/fix/360
Fix/360
2025-03-25 20:39:07 +05:30
Raj Nandan Sharma 5403726099 fix: replace parseInt with Number for timeout conversion in monitorSheet.svelte and ping.js 2025-03-25 20:38:23 +05:30
Raj Nandan Sharma 057c14909f fix #360 2025-03-25 20:29:30 +05:30
Raj Nandan Sharma 37b18c06bb fix: validate timeout input and ensure it's a number in monitorSheet.svelte 2025-03-25 20:28:39 +05:30
github-actions 3ad1eb9a03 Auto-generate README.md with release versions 2025-03-24 15:18:34 +00:00
68 changed files with 2506 additions and 588 deletions
+1
View File
@@ -9,6 +9,7 @@ config/server.yaml
/src/lib/.kener
/package
.env
.vscode
.env.*
!.env.example
vite.config.js.timestamp-*
+2 -2
View File
@@ -31,7 +31,7 @@ RUN apk add --no-cache --update \
g++=14.2.0-r4 \
sqlite=3.48.0-r0 \
sqlite-dev=3.48.0-r0 \
tzdata=2025a-r0 \
tzdata \
iputils=20240905-r0
FROM builder-${VARIANT} AS builder
@@ -85,7 +85,7 @@ FROM node:${ALPINE_VERSION_TAG} AS final-alpine
RUN apk add --no-cache --update \
iputils=20240905-r0 \
sqlite=3.48.0-r0 \
tzdata=2025a-r0
tzdata
FROM final-${VARIANT} AS final
+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.6"><img src="https://img.shields.io/badge/Latest_Stable_Release-3.2.6-blue" alt="Kener latest stable version: 3.2.6" /></a>
<a href="https://hub.docker.com/r/rajnandan1/kener/tags?page=1&ordering=last_updated&name=3.2.12"><img src="https://img.shields.io/badge/Latest_Stable_Release-3.2.12-blue" alt="Kener latest stable version: 3.2.12" /></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.6)</td>
<td>Latest stable release (aka 3.2.12)</td>
</tr>
<tr>
<td><a href="https://hub.docker.com/r/rajnandan1/kener/tags?page=1&ordering=last_updated&name=3.2.6" target="_blank"><code>3.2.6</code></a></td>
<td><a href="https://hub.docker.com/r/rajnandan1/kener/tags?page=1&ordering=last_updated&name=3.2.12" target="_blank"><code>3.2.12</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.6) release within that minor version (3.2.x)</td>
<td>Major-minor version tag pointing to the latest patch (3.2.12) 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.6) release within that major version (3.x.x)</td>
<td>Major version tag pointing to the latest stable (3.2.12) 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.6)</td>
<td>Latest stable release (aka 3.2.12)</td>
</tr>
<tr>
<td><a href="https://hub.docker.com/r/rajnandan1/kener/tags?page=1&ordering=last_updated&name=3.2.6-alpine" target="_blank"><code>3.2.6-alpine</code></a></td>
<td><a href="https://hub.docker.com/r/rajnandan1/kener/tags?page=1&ordering=last_updated&name=3.2.12-alpine" target="_blank"><code>3.2.12-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.6) release within that minor version (3.2.x)</td>
<td>Major-minor version tag pointing to the latest patch (3.2.12) 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.6) release within that major version (3.x.x)</td>
<td>Major version tag pointing to the latest stable (3.2.12) release within that major version (3.x.x)</td>
</tr>
</table>
+44 -1
View File
@@ -33,10 +33,53 @@ To add inline css go to Manage kener -> Theme -> Custom CSS and add your CSS the
```css
.my-class {
color: red;
color: red;
}
```
<div class="note danger">
Do not include &#x3C;style&#x3E; tags.
</div>
## Grid Layout
To change from a column layout to a grid layout in your Kener instance, you can use the following CSS:
```css
@media (min-width: 1330px) {
.section-monitors,
.section-categories,
.section-hero,
.section-back,
.section-events,
.section-legend,
.section-categories,
.section-browser-events {
width: 1330px !important;
max-width: 1330px !important;
}
.section-monitors .monitor-root {
background-color: transparent;
border: none !important;
box-shadow: none;
}
.section-monitors .monitor-root .monitors-card {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 0.5rem;
}
.section-monitors .monitor-root .monitors-card .monitor {
grid-column: span 1;
border-radius: 0.375rem;
background-color: hsl(var(--card) / var(--tw-bg-opacity, 1));
border-width: 1px;
}
.section-monitors .monitor-root .monitors-card .monitor:last-child {
border-bottom: inherit;
border-bottom-width: 1px !important;
}
}
```
+9 -2
View File
@@ -25,6 +25,13 @@ Set this to the origin of your website(protocl + hostname + port if there). This
export ORIGIN=http://localhost:3000
```
If your host is at let us say `https://example.com`, then set the ORIGIN to `https://example.com`. This is required for CORS.
If you are using a reverse proxy like nginx, make sure to set the `ORIGIN` to the domain name of your website.
```bash
export ORIGIN=https://example.com
```
## PORT
Defaults to 3000 if not specified
@@ -37,8 +44,8 @@ export PORT=4242
By default kener runs on `/` but you can change it to `/status` or any other path.
- Important: The base path should _**NOT**_ have a trailing slash and should start with `/`
- Important: This env variable should be present during both build and run time
- Important: The base path should _**NOT**_ have a trailing slash and should start with `/`
- Important: This env variable should be present during both build and run time
```bash
export KENER_BASE_PATH=/status
+1 -1
View File
@@ -119,7 +119,7 @@ Admins can manage user accounts through the user settings page:
## Role Limitations
- The Admin role can only be assigned during initial setup or by existing Admins
- The Admin role can only be assigned during initial setup
- Members cannot create or modify content
- Users cannot modify their own role (only an Admin can change roles)
+11 -1
View File
@@ -21,7 +21,17 @@ Add API key of your Amplitude account
### Mixpanel
Add token of your Mixpanel account.
Add project token of your Mixpanel account.
### Plausible
- Add domain of your Plausible account. Example: `example.com`
- Add API key of your Plausible account. Default is `https://plausible.io/api/event`
- Add Script Source of your Plausible account. Default is `https://plausible.io/js/script.pageview-props.tagged-events.js`
### Microsoft Clarity
Add Project ID of your Microsoft Clarity account.
## Search Engine Optimization
+82 -2
View File
@@ -1,17 +1,20 @@
{
"name": "kener",
"version": "3.2.5",
"version": "3.2.11",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "kener",
"version": "3.2.5",
"version": "3.2.11",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.26.10",
"@codemirror/lang-css": "^6.3.1",
"@codemirror/lang-html": "^6.4.9",
"@codemirror/lang-javascript": "^6.2.3",
"@codemirror/lang-json": "^6.0.1",
"@codemirror/lang-markdown": "^6.3.2",
"@formkit/auto-animate": "^0.8.2",
"@number-flow/svelte": "^0.2.1",
"@scalar/express-api-reference": "^0.4.167",
@@ -218,6 +221,36 @@
"@lezer/common": "^1.1.0"
}
},
"node_modules/@codemirror/lang-css": {
"version": "6.3.1",
"resolved": "https://registry.npmjs.org/@codemirror/lang-css/-/lang-css-6.3.1.tgz",
"integrity": "sha512-kr5fwBGiGtmz6l0LSJIbno9QrifNMUusivHbnA1H6Dmqy4HZFte3UAICix1VuKo0lMPKQr2rqB+0BkKi/S3Ejg==",
"license": "MIT",
"dependencies": {
"@codemirror/autocomplete": "^6.0.0",
"@codemirror/language": "^6.0.0",
"@codemirror/state": "^6.0.0",
"@lezer/common": "^1.0.2",
"@lezer/css": "^1.1.7"
}
},
"node_modules/@codemirror/lang-html": {
"version": "6.4.9",
"resolved": "https://registry.npmjs.org/@codemirror/lang-html/-/lang-html-6.4.9.tgz",
"integrity": "sha512-aQv37pIMSlueybId/2PVSP6NPnmurFDVmZwzc7jszd2KAF8qd4VBbvNYPXWQq90WIARjsdVkPbw29pszmHws3Q==",
"license": "MIT",
"dependencies": {
"@codemirror/autocomplete": "^6.0.0",
"@codemirror/lang-css": "^6.0.0",
"@codemirror/lang-javascript": "^6.0.0",
"@codemirror/language": "^6.4.0",
"@codemirror/state": "^6.0.0",
"@codemirror/view": "^6.17.0",
"@lezer/common": "^1.0.0",
"@lezer/css": "^1.1.0",
"@lezer/html": "^1.3.0"
}
},
"node_modules/@codemirror/lang-javascript": {
"version": "6.2.3",
"resolved": "https://registry.npmjs.org/@codemirror/lang-javascript/-/lang-javascript-6.2.3.tgz",
@@ -243,6 +276,21 @@
"@lezer/json": "^1.0.0"
}
},
"node_modules/@codemirror/lang-markdown": {
"version": "6.3.2",
"resolved": "https://registry.npmjs.org/@codemirror/lang-markdown/-/lang-markdown-6.3.2.tgz",
"integrity": "sha512-c/5MYinGbFxYl4itE9q/rgN/sMTjOr8XL5OWnC+EaRMLfCbVUmmubTJfdgpfcSS2SCaT7b+Q+xi3l6CgoE+BsA==",
"license": "MIT",
"dependencies": {
"@codemirror/autocomplete": "^6.7.1",
"@codemirror/lang-html": "^6.0.0",
"@codemirror/language": "^6.3.0",
"@codemirror/state": "^6.0.0",
"@codemirror/view": "^6.0.0",
"@lezer/common": "^1.2.1",
"@lezer/markdown": "^1.0.0"
}
},
"node_modules/@codemirror/language": {
"version": "6.11.0",
"resolved": "https://registry.npmjs.org/@codemirror/language/-/language-6.11.0.tgz",
@@ -852,6 +900,17 @@
"integrity": "sha512-w7ojc8ejBqr2REPsWxJjrMFsA/ysDCFICn8zEOR9mrqzOu2amhITYuLD8ag6XZf0CFXDrhKqw7+tW8cX66NaDA==",
"license": "MIT"
},
"node_modules/@lezer/css": {
"version": "1.1.11",
"resolved": "https://registry.npmjs.org/@lezer/css/-/css-1.1.11.tgz",
"integrity": "sha512-FuAnusbLBl1SEAtfN8NdShxYJiESKw9LAFysfea1T96jD3ydBn12oYjaSG1a04BQRIUd93/0D8e5CV1cUMkmQg==",
"license": "MIT",
"dependencies": {
"@lezer/common": "^1.2.0",
"@lezer/highlight": "^1.0.0",
"@lezer/lr": "^1.0.0"
}
},
"node_modules/@lezer/highlight": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/@lezer/highlight/-/highlight-1.2.1.tgz",
@@ -861,6 +920,17 @@
"@lezer/common": "^1.0.0"
}
},
"node_modules/@lezer/html": {
"version": "1.3.10",
"resolved": "https://registry.npmjs.org/@lezer/html/-/html-1.3.10.tgz",
"integrity": "sha512-dqpT8nISx/p9Do3AchvYGV3qYc4/rKr3IBZxlHmpIKam56P47RSHkSF5f13Vu9hebS1jM0HmtJIwLbWz1VIY6w==",
"license": "MIT",
"dependencies": {
"@lezer/common": "^1.2.0",
"@lezer/highlight": "^1.0.0",
"@lezer/lr": "^1.0.0"
}
},
"node_modules/@lezer/javascript": {
"version": "1.4.21",
"resolved": "https://registry.npmjs.org/@lezer/javascript/-/javascript-1.4.21.tgz",
@@ -892,6 +962,16 @@
"@lezer/common": "^1.0.0"
}
},
"node_modules/@lezer/markdown": {
"version": "1.4.2",
"resolved": "https://registry.npmjs.org/@lezer/markdown/-/markdown-1.4.2.tgz",
"integrity": "sha512-iYewCigG/517D0xJPQd7RGaCjZAFwROiH8T9h7OTtz0bRVtkxzFhGBFJ9JGKgBBs4uuo1cvxzyQ5iKhDLMcLUQ==",
"license": "MIT",
"dependencies": {
"@lezer/common": "^1.0.0",
"@lezer/highlight": "^1.0.0"
}
},
"node_modules/@mapbox/node-pre-gyp": {
"version": "1.0.11",
"resolved": "https://registry.npmjs.org/@mapbox/node-pre-gyp/-/node-pre-gyp-1.0.11.tgz",
+20 -17
View File
@@ -1,26 +1,26 @@
{
"name": "kener",
"version": "3.2.8",
"version": "3.2.13",
"private": false,
"license": "MIT",
"description": "Kener: An open-source Node.js status page application for real-time service monitoring, incident management, and customizable reporting. Simplify service outage tracking, enhance incident communication, and ensure a seamless user experience.",
"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.",
"author": "Raj Nandan Sharma <rajnandan1@gmail.com>",
"keywords": [
"Node.js application",
"Open-source status page",
"Service monitoring tool",
"Real-time incident management",
"Customizable reporting",
"Service outage tracker",
"User-friendly dashboard",
"Incident communication platform",
"Scalable monitoring solution",
"Community-driven software",
"Website status tracker",
"Incident response tool",
"System status monitoring",
"Service reliability management",
"Incident alert system"
"status page",
"uptime monitoring",
"incident management",
"DevOps tools",
"service reliability",
"open source",
"Node.js",
"dashboard",
"system monitoring",
"status alerts",
"outage communication",
"API monitoring",
"SaaS status",
"performance metrics",
"status reporting"
],
"repository": {
"type": "git",
@@ -68,8 +68,11 @@
"type": "module",
"dependencies": {
"@babel/runtime": "^7.26.10",
"@codemirror/lang-css": "^6.3.1",
"@codemirror/lang-html": "^6.4.9",
"@codemirror/lang-javascript": "^6.2.3",
"@codemirror/lang-json": "^6.0.1",
"@codemirror/lang-markdown": "^6.3.2",
"@formkit/auto-animate": "^0.8.2",
"@number-flow/svelte": "^0.2.1",
"@scalar/express-api-reference": "^0.4.167",
-4
View File
@@ -1,7 +1,3 @@
code:not([class^="language-"]) {
@apply rounded bg-gray-100 px-1.5 py-0.5 font-mono text-xs dark:bg-gray-800;
}
.sidebar-item.active,
.sidebar-item:hover {
color: #ed702d;
+6
View File
@@ -0,0 +1,6 @@
/** @type {import('@sveltejs/kit').Handle} */
export async function handle({ event, resolve }) {
const response = await resolve(event);
response.headers.delete("Link");
return response;
}
+40 -98
View File
@@ -1,108 +1,10 @@
/*one is the class for dotted background*/
.dots-pattern {
position: absolute;
top: 0px;
left: 0;
width: 100%;
z-index: 0;
background-repeat: no-repeat;
background-size: 100%;
height: 100svh;
background: linear-gradient(177deg, rgba(255, 137, 131, 0.5) 0%, rgba(35, 136, 224, 0.05) 60%);
clip-path: polygon(0 0, 100% 0, 100% 54%, 0% 100%);
}
.dots-pattern::after {
content: "";
position: absolute;
background-image: radial-gradient(rgba(0, 0, 0, 0) 1.5px, var(--background-kener) 1px);
background-size: 14px 14px;
width: 100%;
height: 100vh;
top: 0;
transform: blur(3px);
left: 0;
}
.squares-pattern {
position: absolute;
top: 0;
left: 0;
width: 100%;
z-index: 0;
background-repeat: no-repeat;
background-size: 100%;
height: 100svh;
/* clip-path: polygon(0 0, 100% 0, 100% 54%, 0% 100%); */
}
.squares-pattern::after {
content: "";
position: absolute;
background-image:
linear-gradient(#444cf7 1px, transparent 1px),
linear-gradient(to right, #444cf7 1px, var(--background-kener) 1px);
-webkit-mask-image: linear-gradient(
180deg,
rgba(0, 0, 0, 1) 0%,
rgba(0, 0, 0, 0.8) 20%,
rgba(0, 0, 0, 0.6) 40%,
rgba(0, 0, 0, 0.4) 60%,
rgba(0, 0, 0, 0.2) 80%,
rgba(0, 0, 0, 0) 100%
);
mask-image: linear-gradient(
180deg,
rgba(0, 0, 0, 0.2) 0%,
rgba(0, 0, 0, 0.1) 20%,
rgba(0, 0, 0, 0.05) 40%,
rgba(0, 0, 0, 0.025) 60%,
rgba(0, 0, 0, 0.0125) 80%,
rgba(0, 0, 0, 0) 100%
);
background-size: 32px 32px;
width: 100%;
height: 100vh;
top: 0;
left: 0;
}
.dark .squares-pattern::after {
-webkit-mask-image: linear-gradient(
to bottom,
rgba(0, 0, 0, 0.5) 0%,
rgba(0, 0, 0, 0.25) 20%,
rgba(0, 0, 0, 0.125) 40%,
rgba(0, 0, 0, 0.0625) 60%,
rgba(0, 0, 0, 0.03125) 80%,
rgba(0, 0, 0, 0) 100%
);
mask-image: linear-gradient(
180deg,
rgba(0, 0, 0, 0.5) 0%,
rgba(0, 0, 0, 0.25) 20%,
rgba(0, 0, 0, 0.125) 40%,
rgba(0, 0, 0, 0.0625) 60%,
rgba(0, 0, 0, 0.03125) 80%,
rgba(0, 0, 0, 0) 100%
);
background-image:
linear-gradient(#616fbf 1px, transparent 1px),
linear-gradient(to right, #616fbf 1px, var(--background-kener) 1px);
}
/*Needed to overlay content on top of dotted bg*/
section {
position: relative;
z-index: 1;
}
/*Needed overlay content on top of dotted bg*/
.blurry-bg {
/* background-color: var(--background-kener-rgba);
box-shadow: 0 0 64px 64px var(--background-kener-rgba); */
}
:root {
--up-color: #4ead94;
@@ -939,3 +841,43 @@ textarea::placeholder {
.dark .like-input {
background-color: rgba(0, 0, 0, 0.1) !important;
}
.ͼo .cm-gutters {
border-top-left-radius: var(--radius);
border-bottom-left-radius: var(--radius);
}
.no-gutter .ͼo .cm-gutters {
display: none;
}
.kener-md {
@apply text-sm;
}
p code:not([class^="language-"]) {
@apply rounded-sm bg-yellow-100 px-1 py-0.5 font-mono text-xs dark:bg-neutral-700 dark:text-neutral-100;
}
.kener-theme-mono .section-hero .h1,
.kener-theme-mono-hero-h1 {
@apply text-foreground;
}
.kener-theme-default .section-hero .h1,
.kener-theme-default-hero-h1 {
@apply bg-gradient-to-r from-green-300 via-blue-500 to-purple-600 bg-clip-text text-transparent;
}
.kener-theme-sunset .section-hero .h1,
.kener-theme-sunset-hero-h1 {
@apply bg-gradient-to-r from-yellow-500 via-red-500 to-pink-500 bg-clip-text text-transparent;
}
.kener-theme-forest .section-hero .h1,
.kener-theme-forest-hero-h1 {
@apply bg-gradient-to-l from-blue-500 via-teal-500 to-green-500 bg-clip-text text-transparent;
}
.kener-theme-ocean .section-hero .h1,
.kener-theme-ocean-hero-h1 {
@apply bg-gradient-to-l from-cyan-500 via-blue-500 to-indigo-500 bg-clip-text text-transparent;
}
.prose :where(hr):not(:where([class~="not-prose"], [class~="not-prose"] *)) {
margin-top: 1.3em;
margin-bottom: 1.3em;
}
+1 -1
View File
@@ -66,7 +66,7 @@ export const WebhookJSONTemplate = JSON.stringify(
actions: [
{
text: "{{action_text}}",
url: "{{action_url}}",
url: "{{{action_url}}}",
},
],
},
+14 -10
View File
@@ -1,13 +1,17 @@
// @ts-ignore
export const analyticsEvent = (event, data) => {
// Do something with the event and data
document.dispatchEvent(
new CustomEvent("analyticsEvent", {
bubbles: true,
detail: {
event,
data
}
})
);
// Do something with the event and data
if (typeof document !== "undefined") {
event = "kener_" + event;
document.dispatchEvent(
new CustomEvent("analyticsEvent", {
bubbles: true,
detail: {
event,
data,
},
}),
);
}
};
+18 -5
View File
@@ -2,6 +2,7 @@
import { formatDistanceToNow, formatDistance } from "date-fns";
import Settings from "lucide-svelte/icons/settings";
import ArrowRight from "lucide-svelte/icons/arrow-right";
import { analyticsEvent } from "$lib/boringOne";
import * as Accordion from "$lib/components/ui/accordion";
import { l, f, fd, fdn } from "$lib/i18n/client";
import { base } from "$app/paths";
@@ -9,6 +10,7 @@
import { Tooltip } from "bits-ui";
import GMI from "$lib/components/gmi.svelte";
import { page } from "$app/stores";
import { marked } from "marked";
export let incident;
export let index;
export let lang;
@@ -79,7 +81,10 @@
<div class="col-span-12">
<Accordion.Root bind:value={index} class="accor">
<Accordion.Item value={accordionValue}>
<Accordion.Trigger class="rounded-md px-4 hover:bg-muted hover:no-underline">
<Accordion.Trigger
class="rounded-md px-4 hover:bg-muted hover:no-underline"
on:click={() => analyticsEvent("incident_open", { incident_title: incident.title })}
>
<div class="w-full text-left hover:no-underline">
<p class="flex gap-x-2 text-xs font-semibold">
{#if incidentType == "INCIDENT"}
@@ -185,7 +190,11 @@
</time>
<div class="mb-4 text-sm font-normal">
{@html comment.comment}
<div
class="kener-md prose prose-stone max-w-none dark:prose-invert prose-code:rounded prose-code:py-[0.2rem] prose-code:font-mono prose-code:text-sm prose-code:font-normal prose-pre:bg-opacity-0 dark:prose-pre:bg-neutral-900"
>
{@html marked.parse(comment.comment)}
</div>
</div>
</li>
{/each}
@@ -203,9 +212,13 @@
)}
</time>
<p class="mb-2 text-sm font-normal">
{comment.comment}
</p>
<div class="mb-2 text-sm font-normal">
<div
class="kener-md prose prose-stone max-w-none dark:prose-invert prose-code:rounded prose-code:py-[0.2rem] prose-code:font-mono prose-code:text-sm prose-code:font-normal prose-pre:bg-opacity-0 dark:prose-pre:bg-neutral-900"
>
{@html marked.parse(comment.comment)}
</div>
</div>
</li>
{/each}
</ol>
+108 -50
View File
@@ -8,6 +8,7 @@
import * as Select from "$lib/components/ui/select";
import locales from "$lib/locales/locales.json?raw";
import GMI from "$lib/components/gmi.svelte";
import * as DropdownMenu from "$lib/components/ui/dropdown-menu";
import Loader from "lucide-svelte/icons/loader";
import X from "lucide-svelte/icons/x";
@@ -16,6 +17,11 @@
import Play from "lucide-svelte/icons/play";
import { Tooltip } from "bits-ui";
import CodeMirror from "svelte-codemirror-editor";
import { html } from "@codemirror/lang-html";
import { githubLight, githubDark } from "@uiw/codemirror-theme-github";
import { mode } from "mode-watcher";
export let data;
let formStateHero = "idle";
@@ -34,6 +40,15 @@
let homeIncidentCount = 10;
let homeIncidentStartTimeWithin = 30;
let incidentGroupView = "EXPAND_FIRST";
let kenerTheme = "default";
let availableThemes = [
{ name: "Default", value: "default" },
{ name: "Mono", value: "mono" },
{ name: "Sunset", value: "sunset" },
{ name: "Forest", value: "forest" },
{ name: "Ocean", value: "ocean" }
];
let nav = [];
let categories = [];
@@ -74,6 +89,9 @@
if (data.siteData.footerHTML) {
footerHTML = data.siteData.footerHTML;
}
if (data.siteData.kenerTheme) {
kenerTheme = data.siteData.kenerTheme;
}
if (data.siteData.tzToggle) {
tzToggle = data.siteData.tzToggle;
}
@@ -105,6 +123,19 @@
heroError = data.error;
return;
}
saveTheme();
}
async function saveTheme() {
let resp = await storeSiteData({
kenerTheme: kenerTheme
});
//print data
let data = await resp.json();
if (data.error) {
alert(data.error);
return;
}
}
let navErrorMessage = "";
@@ -258,44 +289,16 @@
<Card.Description>Configure the hero section of your site.</Card.Description>
</Card.Header>
<Card.Content>
<form class="mx-auto mt-4 space-y-4" on:submit|preventDefault={formSubmitHero}>
<form class="mx-auto mt-4 flex flex-col gap-y-4" on:submit|preventDefault={formSubmitHero}>
<div class="flex w-full flex-row justify-evenly gap-2">
<div class="w-full">
<Label for="hero_title">
Title
<Tooltip.Root openDelay={100}>
<Tooltip.Trigger class="">
<Info class="inline-block h-4 w-4 text-muted-foreground" />
</Tooltip.Trigger>
<Tooltip.Content class="max-w-md">
<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"
>
You can set the title of the hero section here. This will be displayed on the top of the page.
</div>
</Tooltip.Content>
</Tooltip.Root>
</Label>
<Label for="hero_title">Title</Label>
<Input bind:value={hero.title} class="mt-2" type="text" id="hero_title" placeholder="Status Page for Kener" />
</div>
</div>
<div class="flex w-full flex-row justify-evenly gap-2">
<div class="w-full">
<Label for="hero_subtitle">
Subtitle
<Tooltip.Root openDelay={100}>
<Tooltip.Trigger class="">
<Info class="inline-block h-4 w-4 text-muted-foreground" />
</Tooltip.Trigger>
<Tooltip.Content class="max-w-md">
<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"
>
You can set the subtitle of the hero section here. This will be displayed below the title.
</div>
</Tooltip.Content>
</Tooltip.Root>
</Label>
<Label for="hero_subtitle">Subtitle</Label>
<Input
bind:value={hero.subtitle}
class="mt-2"
@@ -305,12 +308,49 @@
/>
</div>
</div>
<div class="flex flex-col gap-y-2">
<div>
<Label>Color</Label>
</div>
<div class="flex flex-row gap-x-2 rounded-md border p-2">
<div class="w-3/4 pt-2">
<span class="kener-theme-{kenerTheme}-hero-h1 font-medium">{hero.title}</span>
</div>
<div class="flex w-1/4 justify-end">
<DropdownMenu.Root>
<DropdownMenu.Trigger>
<Button variant="outline">
{availableThemes.find((el) => el.value === kenerTheme)?.name || "Mono"}
</Button>
</DropdownMenu.Trigger>
<DropdownMenu.Content>
{#each availableThemes as theme}
<DropdownMenu.Group>
<DropdownMenu.Item>
<Button
variant="ghost"
class="h-6 w-full justify-start text-xs"
on:click={() => {
kenerTheme = theme.value;
}}
>
{theme.name}
</Button>
</DropdownMenu.Item>
</DropdownMenu.Group>
{/each}
</DropdownMenu.Content>
</DropdownMenu.Root>
</div>
</div>
</div>
<div class="flex w-full justify-end gap-x-2">
{#if !!heroError}
<div class="py-2 text-sm font-medium text-destructive">{heroError}</div>
{/if}
<Button type="submit" disabled={formStateHero === "loading"}>
Save
Save Hero Section
{#if formStateHero === "loading"}
<Loader class="ml-2 inline h-4 w-4 animate-spin" />
{/if}
@@ -370,7 +410,7 @@
</div>
<div class="pt-8">
<Button type="submit" disabled={formStateIncident === "loading"}>
Save
Save Incidents Settings
{#if formStateIncident === "loading"}
<Loader class="ml-2 inline h-4 w-4 animate-spin" />
{/if}
@@ -495,7 +535,7 @@
<span class="text-sm font-medium text-destructive"> {navErrorMessage} </span>
</p>
<Button type="submit" disabled={formStateHero === "loading"}>
Save
Save Navigation
{#if formStateHero === "loading"}
<Loader class="ml-2 inline h-4 w-4 animate-spin" />
{/if}
@@ -574,7 +614,7 @@
<div class="py-2 text-sm font-medium text-destructive">{i18nErrorMessage}</div>
{/if}
<Button type="submit" disabled={formStatei18n === "loading"}>
Save
Save Languages
{#if formStatei18n === "loading"}
<Loader class="ml-2 inline h-4 w-4 animate-spin" />
{/if}
@@ -637,7 +677,7 @@
<div class="py-2 text-sm font-medium text-destructive">{categoriesErrorMessage}</div>
{/if}
<Button type="submit" disabled={formStateCategories === "loading"}>
Save
Save Categories
{#if formStateCategories === "loading"}
<Loader class="ml-2 inline h-4 w-4 animate-spin" />
{/if}
@@ -656,24 +696,42 @@
<form class="mx-auto mt-4 space-y-4" on:submit|preventDefault={formSubmitFooter}>
<div class="flex w-full flex-row justify-evenly gap-2">
<div class="w-full">
<textarea
bind:value={footerHTML}
class="mt-2 h-48 w-full rounded-sm border p-2"
placeholder="eg. <p>Powered by Kener</p>"
></textarea>
<div class="overflow-hidden rounded-md">
<CodeMirror
bind:value={footerHTML}
lang={html()}
theme={$mode == "dark" ? githubDark : githubLight}
styles={{
"&": {
width: "100%",
maxWidth: "100%",
height: "320px",
border: "1px solid hsl(var(--border) / var(--tw-border-opacity))",
borderRadius: "0.375rem"
}
}}
/>
</div>
</div>
</div>
<div class="flex w-full justify-end gap-x-2">
{#if !!footerErrorMessage}
<div class="py-2 text-sm font-medium text-destructive">{footerErrorMessage}</div>
{/if}
<Button type="submit" disabled={formStateFooter === "loading"}>
Save
{#if formStateFooter === "loading"}
<Loader class="ml-2 inline h-4 w-4 animate-spin" />
<div class="flex w-full justify-between gap-x-2">
<div>
<Button variant="outline" class="mt-2" on:click={() => (footerHTML = data.defaultFooterHTML)}>
<span class="ml-2">Reset to default</span>
</Button>
</div>
<div class="flex flex-row justify-end gap-x-2">
{#if !!footerErrorMessage}
<div class="py-2 text-sm font-medium text-destructive">{footerErrorMessage}</div>
{/if}
</Button>
<Button type="submit" disabled={formStateFooter === "loading"}>
Save Custom Footer
{#if formStateFooter === "loading"}
<Loader class="ml-2 inline h-4 w-4 animate-spin" />
{/if}
</Button>
</div>
</div>
</form>
</Card.Content>
+73 -1
View File
@@ -188,6 +188,12 @@
invalidFormMessage = `Host ${hosts[i].host} is not of type ${hosts[i].type}`;
return;
}
//validating timeout for ping
if (!!hosts[i].timeout && isNaN(hosts[i].timeout)) {
invalidFormMessage = "Timeout should be a number";
return;
}
hosts[i].timeout = Number(hosts[i].timeout);
//validating timeout
if (hosts[i].timeout < 1) {
invalidFormMessage = "Timeout should be greater than 0";
@@ -222,6 +228,12 @@
invalidFormMessage = `Host ${hosts[i].host} is not of type ${hosts[i].type}`;
return;
}
//validating timeout for tcp
if (!!hosts[i].timeout && isNaN(hosts[i].timeout)) {
invalidFormMessage = "Timeout should be a number";
return;
}
hosts[i].timeout = Number(hosts[i].timeout);
//validating timeout
if (hosts[i].timeout < 1) {
invalidFormMessage = "Timeout should be greater than 0";
@@ -419,6 +431,35 @@
pg: "PostgreSQL",
mysql2: "MySQL"
};
let deleteMonitorConfirmText = "";
let deletingMonitor = false;
async function deleteMonitor() {
if (deleteMonitorConfirmText != `delete ${newMonitor.tag}`) {
invalidFormMessage = "Please type the correct text to delete the monitor";
return;
}
deletingMonitor = true;
try {
let data = await fetch(base + "/manage/app/api/", {
method: "POST",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify({ action: "deleteMonitor", data: { tag: newMonitor.tag } })
});
let resp = await data.json();
if (resp.error) {
invalidFormMessage = resp.error;
} else {
dispatch("closeModal", {});
}
} catch (error) {
invalidFormMessage = "Error while deleting monitor";
} finally {
deletingMonitor = false;
}
}
</script>
<div class="fixed left-0 top-0 z-50 h-screen w-screen bg-card bg-opacity-20 backdrop-blur-sm">
@@ -943,7 +984,7 @@
];
}}
>
<Plus class="h-5 w-5" /> Add New
<Plus class="mr-1 h-5 w-5" /> Add New
</Button>
</div>
</div>
@@ -1264,6 +1305,37 @@
</div>
</div>
{/if}
{#if !!newMonitor.id}
<div class="mt-4 flex">
<div class="flex w-full flex-col rounded-md border border-destructive p-2">
<div class="w-full">
<h2 class="flex justify-between text-lg">
<span class="text-destructive">Danger Zone</span>
</h2>
<p class="text-sm">Deleting a monitor is irreversible. Please be sure before deleting.</p>
</div>
<form class="flex gap-x-2" on:submit|preventDefault={deleteMonitor}>
<div class="mt-1 flex flex-col gap-y-2">
<Label for="deleteMonitor">
Type <i class="text-destructive">delete {newMonitor.tag}</i> to confirm
</Label>
<Input bind:value={deleteMonitorConfirmText} id="deleteMonitor" required />
</div>
<Button
variant="destructive"
type="submit"
class="mt-7"
disabled={deleteMonitorConfirmText != `delete ${newMonitor.tag}`}
>
Delete
{#if deletingMonitor}
<Loader class="ml-2 inline h-4 w-4 animate-spin" />
{/if}
</Button>
</form>
</div>
</div>
{/if}
</div>
<div class="absolute bottom-0 grid h-16 w-full grid-cols-6 justify-end gap-2 border-t p-3 pr-6">
<div class="col-span-5 py-2.5">
+35 -24
View File
@@ -196,7 +196,12 @@
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify({ action: "getTriggers", data: {} })
body: JSON.stringify({
action: "getTriggers",
data: {
status: "ACTIVE"
}
})
});
triggers = await apiResp.json();
} catch (error) {
@@ -229,28 +234,33 @@
});
let shareMenusToggle = false;
let monitorTriggers = {
down_trigger: {
failureThreshold: 1,
trigger_type: "DOWN",
successThreshold: 1,
description: "The monitor is down",
createIncident: "NO",
active: false,
triggers: [],
severity: "critical"
},
degraded_trigger: {
failureThreshold: 1,
trigger_type: "DEGRADED",
successThreshold: 1,
active: false,
description: "The monitor is degraded",
createIncident: "NO",
triggers: [],
severity: "warning"
}
};
function initMonitorTriggers() {
monitorTriggers = {
down_trigger: {
failureThreshold: 1,
trigger_type: "DOWN",
successThreshold: 1,
description: "The monitor is down",
createIncident: "NO",
active: false,
triggers: [],
severity: "critical"
},
degraded_trigger: {
failureThreshold: 1,
trigger_type: "DEGRADED",
successThreshold: 1,
active: false,
description: "The monitor is degraded",
createIncident: "NO",
triggers: [],
severity: "warning"
}
};
}
let monitorTriggers;
initMonitorTriggers();
let saveTriggerError = "";
async function saveTriggers() {
@@ -286,6 +296,7 @@
}
let currentAlertMonitor;
function openAlertMenu(m) {
initMonitorTriggers();
currentAlertMonitor = m;
if (m.down_trigger) {
monitorTriggers.down_trigger = m.down_trigger;
@@ -503,7 +514,7 @@
<div class="text-red-500">
{monitor.testResult.error}
</div>
{:else if !!monitor.testResult.status && !!monitor.testResult.latency}
{:else if monitor.testResult.status != undefined && monitor.testResult.latency != undefined}
<p class="text-muted-foreground">
Status: <span class="text-api-{monitor.testResult.status.toLowerCase()}"
>{monitor.testResult.status}</span
+216 -74
View File
@@ -5,9 +5,11 @@
import { Label } from "$lib/components/ui/label";
import autoAnimate from "@formkit/auto-animate";
import { siteDataExtractFromDb, storeSiteData } from "$lib/clientTools.js";
import * as DropdownMenu from "$lib/components/ui/dropdown-menu";
import { base } from "$app/paths";
import Plus from "lucide-svelte/icons/plus";
import X from "lucide-svelte/icons/x";
import Check from "lucide-svelte/icons/check";
import Loader from "lucide-svelte/icons/loader";
import Info from "lucide-svelte/icons/info";
import { Tooltip } from "bits-ui";
@@ -24,26 +26,7 @@
}
];
}
let analyticsSupported = [
{
id: "",
type: "GA",
name: "Google Analytics",
script: "https://unpkg.com/@analytics/google-analytics@1.0.7/dist/@analytics/google-analytics.min.js"
},
{
id: "",
type: "AMPLITUDE",
name: "Amplitude",
script: "https://unpkg.com/@analytics/amplitude@0.1.3/dist/@analytics/amplitude.min.js"
},
{
id: "",
type: "MIXPANEL",
name: "MixPanel",
script: "https://unpkg.com/@analytics/mixpanel@0.4.0/dist/@analytics/mixpanel.min.js"
}
];
function removeRow(i) {
metaTags = metaTags.filter((_, index) => index !== i);
}
@@ -52,19 +35,6 @@
metaTags: JSON.stringify(metaTags)
}).metaTags;
let analytics = siteDataExtractFromDb(data.siteData, {
analytics: []
}).analytics;
//merge analyticsSupported with analytics
analytics = analyticsSupported.map((supported) => {
let found = analytics.find((a) => a.type === supported.type);
if (found) {
return found;
}
return supported;
});
let formState = "idle";
let formStateAnalytics = "idle";
@@ -87,17 +57,150 @@
async function formSubmitAnalytics() {
seoError = "";
formStateAnalytics = "loading";
let resp = await storeSiteData({
analytics: JSON.stringify(analytics)
let dataToSave = {};
selectedAnalytics.requirements.forEach((req) => {
dataToSave[req.label] = req.value;
});
let finalData = {};
finalData[selectedAnalytics.key] = JSON.stringify({
requirements: dataToSave,
isEnabled: selectedAnalytics.isEnabled
});
let resp = await storeSiteData(finalData);
//print data
let data = await resp.json();
formStateAnalytics = "idle";
if (data.error) {
seoError = data.error;
return;
} else {
selectedAnalytics.activeInSite = selectedAnalytics.isEnabled;
//update activeInSite in analyticsDropdownItems also
let index = analyticsDropdownItems.findIndex((item) => item.label === selectedAnalytics.label);
analyticsDropdownItems[index].activeInSite = true;
}
}
let selectedAnalytics;
let analyticsDropdownItems = [
{
label: "Google Analytics",
logo: `/ga.png`,
key: "analytics.googleTagManager",
isEnabled: false,
requirements: [
{
label: "Measurement ID",
type: "text",
placeholder: "G-S05E5E5E5E5",
required: true,
value: ""
}
]
},
{
label: "Plausible",
logo: `/plausible.png`,
key: "analytics.plausible",
isEnabled: false,
requirements: [
{
label: "Domain",
type: "text",
placeholder: "kener.ing",
required: true,
value: new URL(data.siteData.siteURL).hostname
},
{
label: "API",
type: "text",
placeholder: "https://plausible.io/api/event",
required: true,
value: "https://plausible.io/api/event"
},
{
label: "Script Source",
type: "text",
placeholder: "https://plausible.io/js/script.pageview-props.tagged-events.js",
required: true,
value: "https://plausible.io/js/script.pageview-props.tagged-events.js"
}
]
},
{
label: "MixPanel",
logo: `/mixpanel.png`,
key: "analytics.mixpanel",
isEnabled: false,
requirements: [
{
label: "Project Token",
type: "text",
placeholder: "YOUR_PROJECT_TOKEN",
required: true,
value: ""
}
]
},
{
label: "Amplitude",
logo: `/amplitude.png`,
key: "analytics.amplitude",
isEnabled: false,
requirements: [
{
label: "Amplitude API Key",
type: "text",
placeholder: "API key for your Amplitude project",
required: true,
value: ""
}
]
},
{
label: "Microsoft Clarity",
logo: `/clarity.png`,
key: "analytics.clarity",
isEnabled: false,
requirements: [
{
label: "Project ID",
type: "text",
placeholder: "Project ID for your Microsoft Clarity project",
required: true,
value: ""
}
]
},
{
label: "Umami",
logo: `/umami.png`,
key: "analytics.umami",
isEnabled: false,
requirements: [
{
label: "Website ID",
type: "text",
placeholder: "Website ID for your umami script and domain",
required: true,
value: ""
}
]
}
];
analyticsDropdownItems = analyticsDropdownItems.map((item) => {
let dbData = data.siteData[item.key];
if (!!dbData) {
item.isEnabled = dbData.isEnabled;
item.requirements.forEach((req) => {
req.value = dbData.requirements[req.label];
});
item.activeInSite = true;
}
return item;
});
selectedAnalytics = analyticsDropdownItems[0];
</script>
<Card.Root class="mt-4">
@@ -106,48 +209,87 @@
<Card.Description>Add your analytics ID/Key here. You can add multiple analytics providers.</Card.Description>
</Card.Header>
<Card.Content>
<form class="mx-auto mt-4 space-y-4" on:submit|preventDefault={formSubmitAnalytics}>
<div class="grid grid-cols-2 gap-2">
{#each analytics as analytic}
<div class="col-span-1">
<Label for={analytic.type}>
{analytic.name}
<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"
>
Add your {analytic.name} ID/Key here.
</div>
</Tooltip.Content>
</Tooltip.Root>
</Label>
<Input
bind:value={analytic.id}
class="mt-2"
type="text"
id={analytic.type}
placeholder="{analytic.type} ID/Key"
/>
</div>
<div class="mt-4 grid grid-cols-4 overflow-hidden rounded-md border">
<div class="col-span-1 flex flex-col border-r">
{#each analyticsDropdownItems as item}
<Button
variant={!!selectedAnalytics && selectedAnalytics.label === item.label ? "primary" : "ghost"}
class="flex items-center justify-between gap-x-2 rounded-none border-b text-sm last:border-none"
on:click={() => (selectedAnalytics = item)}
>
<div class="flex flex-row gap-x-3">
<img src={base + item.logo} class="w-5" alt={item.label} />
<span>{item.label}</span>
</div>
{#if item.activeInSite && item.isEnabled}
<div class="rounded-sm">
<span class="relative flex size-2">
<span class="absolute inline-flex h-full w-full animate-ping rounded-full bg-green-400 opacity-75"
></span>
<span class="relative inline-flex size-2 rounded-full bg-green-500"></span>
</span>
</div>
{/if}
</Button>
{/each}
</div>
<div class="flex w-full justify-end gap-x-2">
{#if !!seoError}
<div class="py-2 text-sm font-medium text-destructive">{seoError}</div>
{/if}
<Button type="submit" disabled={formStateAnalytics === "loading"}>
Save
{#if formStateAnalytics === "loading"}
<Loader class="ml-2 inline h-4 w-4 animate-spin" />
<form on:submit|preventDefault={formSubmitAnalytics} class="col-span-3 flex flex-col justify-between p-3 py-2">
{#if !!selectedAnalytics}
<div>
<div class="mb-2 text-sm font-medium text-muted-foreground">
Add details for your {selectedAnalytics.label} account
</div>
</div>
<div class="grid grid-cols-4 gap-4">
{#each selectedAnalytics.requirements as req}
<div class="col-span-2 flex flex-col gap-y-2">
<Label for={req.label}>{req.label}</Label>
<Input
bind:value={req.value}
class=""
type={req.type}
placeholder={req.placeholder}
bind:required={req.required}
/>
</div>
{/each}
</div>
<div class="mt-2 flex flex-row justify-between">
<div class="flex flex-row gap-x-2">
<label class="inline-flex cursor-pointer items-center">
<input
type="checkbox"
class="peer sr-only"
checked={selectedAnalytics.isEnabled}
on:change={() => {
selectedAnalytics.isEnabled = !selectedAnalytics.isEnabled;
}}
/>
<div
class="peer relative h-6 w-11 rounded-full bg-gray-200 after:absolute after:start-[2px] after:top-[2px] after:h-5 after:w-5 after:rounded-full after:border after:border-gray-300 after:bg-white after:transition-all after:content-[''] peer-checked:bg-blue-600 peer-checked:after:translate-x-full peer-checked:after:border-white peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-blue-300 dark:border-gray-600 dark:bg-gray-700 dark:peer-focus:ring-blue-800 rtl:peer-checked:after:-translate-x-full"
></div>
</label>
<div class="pt-2.5 text-sm font-medium text-muted-foreground">
{selectedAnalytics.label} is
<i class="underline">{!selectedAnalytics.isEnabled ? "disabled" : "enabled"}</i> for your site
</div>
</div>
<div>
<Button type="submit" disabled={formStateAnalytics === "loading"}>
Save Changes for {selectedAnalytics.label}
{#if formStateAnalytics === "loading"}
<Loader class="ml-2 inline h-4 w-4 animate-spin" />
{/if}
</Button>
</div>
</div>
{#if !!seoError}
<div class="py-2 text-sm font-medium text-destructive">{seoError}</div>
{/if}
</Button>
</div>
</form>
{/if}
</form>
</div>
</Card.Content>
</Card.Root>
<Card.Root class="mt-4">
@@ -202,7 +344,7 @@
<div class="py-2 text-sm font-medium text-destructive">{metaError}</div>
{/if}
<Button type="submit" disabled={formState === "loading"}>
Save
Save Meta Tags
{#if formState === "loading"}
<Loader class="ml-2 inline h-4 w-4 animate-spin" />
{/if}
+103 -37
View File
@@ -14,6 +14,11 @@
import { Tooltip } from "bits-ui";
import ColorPicker from "svelte-awesome-color-picker";
import CodeMirror from "svelte-codemirror-editor";
import { css } from "@codemirror/lang-css";
import { githubLight, githubDark } from "@uiw/codemirror-theme-github";
import { mode } from "mode-watcher";
export let data;
let formState = "idle";
@@ -26,6 +31,7 @@
barStyle: "PARTIAL",
barRoundness: "ROUNDED",
summaryStyle: "CURRENT",
kenerTheme: "default",
colorsJ: {
UP: "#4ead94",
DOWN: "#ca3038",
@@ -95,16 +101,64 @@
</p>
<div class="flex">
<RadioGroup.Root bind:value={themeData.pattern} onValueChange={(e) => (themeData.pattern = e)}>
<div class="flex items-center space-x-2">
<RadioGroup.Root
class="flex flex-1 flex-wrap justify-start gap-4"
bind:value={themeData.pattern}
onValueChange={(e) => (themeData.pattern = e)}
>
<div class="flex w-1/6 items-center space-x-2">
<RadioGroup.Item value="squares" id="rsq" />
<Label for="rsq">Squares</Label>
</div>
<div class="flex items-center space-x-2">
<div class="flex w-1/6 items-center space-x-2">
<RadioGroup.Item value="dots" id="rdot" />
<Label for="rdot">Dots</Label>
</div>
<div class="flex items-center space-x-2">
<div class="flex w-1/6 items-center space-x-2">
<RadioGroup.Item value="tiles" id="tiles" />
<Label for="tiles">Tiles</Label>
</div>
<div class="flex w-1/6 items-center space-x-2">
<RadioGroup.Item value="radial-blue" id="radial-blue" />
<Label for="radial-blue">Radial Blue</Label>
</div>
<div class="flex w-1/6 items-center space-x-2">
<RadioGroup.Item value="radial-mono" id="radial-mono" />
<Label for="radial-mono">Radial Mono</Label>
</div>
<div class="flex w-1/6 items-center space-x-2">
<RadioGroup.Item value="radial-midnight" id="radial-midnight" />
<Label for="radial-midnight">Radial Midnight</Label>
</div>
<div class="flex w-1/6 items-center space-x-2">
<RadioGroup.Item value="circle-mono" id="circle-mono" />
<Label for="circle-mono">Circle Mono</Label>
</div>
<div class="flex w-1/6 items-center space-x-2">
<RadioGroup.Item value="carbon-fibre" id="carbon-fibre" />
<Label for="carbon-fibre">Carbon Fibre</Label>
</div>
<div class="flex w-1/6 items-center space-x-2">
<RadioGroup.Item value="texture-sky" id="texture-sky" />
<Label for="texture-sky">Texture Sky</Label>
</div>
<div class="flex w-1/6 items-center space-x-2">
<RadioGroup.Item value="angular-mono" id="angular-mono" />
<Label for="angular-mono">Angular Mono</Label>
</div>
<div class="flex w-1/6 items-center space-x-2">
<RadioGroup.Item value="angular-spring" id="angular-spring" />
<Label for="angular-spring">Angular Spring</Label>
</div>
<div class="flex w-1/6 items-center space-x-2">
<RadioGroup.Item value="angular-bloom" id="angular-bloom" />
<Label for="angular-bloom">Angular Bloom</Label>
</div>
<div class="flex w-1/6 items-center space-x-2">
<RadioGroup.Item value="pets" id="pets" />
<Label for="pets">Pets</Label>
</div>
<div class="flex w-1/6 items-center space-x-2">
<RadioGroup.Item value="none" id="rnone" />
<Label for="rnone">None</Label>
</div>
@@ -239,53 +293,51 @@
</RadioGroup.Root>
</div>
<hr />
<p class="font-medium">
Colors
<Tooltip.Root openDelay={100}>
<Tooltip.Trigger class="">
<Info class="inline-block h-4 w-4 text-muted-foreground" />
</Tooltip.Trigger>
<Tooltip.Content class="max-w-md">
<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"
>
Choose the colors for the monitor. You can choose between UP, DEGRADED, and DOWN.
</div>
</Tooltip.Content>
</Tooltip.Root>
</p>
<div class="flex w-full flex-row justify-evenly gap-2">
<div class="relative w-full">
<div>
<p class="font-medium">Status Colors</p>
<p class=" text-xs font-semibold text-muted-foreground">
Choose the colors for the monitor. You can choose between UP, DEGRADED, and DOWN.
</p>
</div>
<div class="flex w-full flex-row justify-start gap-x-8">
<div class="relative">
<ColorPicker
bind:hex={themeData.colorsJ.UP}
position="responsive"
isAlpha={false}
isDark={$mode == "dark"}
--input-size="16px"
isTextInput={true}
label="UP"
--cp-input-color="#777"
--cp-button-hover-color="#767676"
on:input={(event) => {
themeData.colorsJ.UP = event.detail.hex;
}}
/>
</div>
<div class="relative w-full">
<div class="relative">
<ColorPicker
bind:hex={themeData.colorsJ.DEGRADED}
position="responsive"
isAlpha={false}
isDark={$mode == "dark"}
--input-size="16px"
isTextInput={true}
label="DEGRADED"
--cp-input-color="#777"
--cp-button-hover-color="#767676"
on:input={(event) => {
themeData.colorsJ.DEGRADED = event.detail.hex;
}}
/>
</div>
<div class="relative w-full">
<div class="relative">
<ColorPicker
bind:hex={themeData.colorsJ.DOWN}
position="responsive"
isAlpha={false}
isDark={$mode == "dark"}
--input-size="16px"
isTextInput={true}
label="DOWN"
--cp-input-color="#777"
--cp-button-hover-color="#767676"
on:input={(event) => {
themeData.colorsJ.DOWN = event.detail.hex;
}}
@@ -338,14 +390,28 @@
<div class="flex w-full flex-row justify-start gap-2">
<div class="w-full">
<Label for="customCSS" class="text-sm text-muted-foreground">
You can add custom CSS to style your site. Do not include style tags.
You can add custom CSS to style your site. Do not include style tags. Go to <a
href="https://kener.ing/docs/custom-js-css-guide#grid-layout"
class="text-card-foreground"
target="_blank">this link</a
> to learn more about it.
</Label>
<textarea
bind:value={themeData.customCSS}
id="customCSS"
class="mt-1 h-48 w-full rounded-sm border p-2"
placeholder=".className&#123;color: red;&#125;"
></textarea>
<div class="overflow-hidden rounded-md">
<CodeMirror
bind:value={themeData.customCSS}
lang={css()}
theme={$mode == "dark" ? githubDark : githubLight}
styles={{
"&": {
width: "100%",
maxWidth: "100%",
height: "320px",
border: "1px solid hsl(var(--border) / var(--tw-border-opacity))",
borderRadius: "0.375rem"
}
}}
/>
</div>
</div>
</div>
<hr />
@@ -354,7 +420,7 @@
<div class="py-2 text-sm font-medium text-destructive">{themeError}</div>
{/if}
<Button type="submit" disabled={formState === "loading"}>
Save
Save Theme Settings
{#if formState === "loading"}
<Loader class="ml-2 inline h-4 w-4 animate-spin" />
{/if}
+18 -8
View File
@@ -8,7 +8,7 @@
import { sub, startOfDay, getUnixTime } from "date-fns";
import GMI from "$lib/components/gmi.svelte";
import { page } from "$app/stores";
import { analyticsEvent } from "$lib/boringOne";
import Share2 from "lucide-svelte/icons/share-2";
import ArrowRight from "lucide-svelte/icons/arrow-right";
import Settings from "lucide-svelte/icons/settings";
@@ -94,33 +94,33 @@
let uptimesRollers = [
{
text: `${l(lang, "90 Days")}`,
startTs: getUnixTime(startOfDay(sub(new Date(), { days: 90 }))),
startTs: monitor.pageData.startOfTheDay - 86400 * 89,
endTs: monitor.pageData.maxDateTodayTimestamp,
value: uptime90Day
},
{
text: `${l(lang, "60 Days")}`,
startTs: getUnixTime(startOfDay(sub(new Date(), { days: 59 }))),
startTs: monitor.pageData.startOfTheDay - 86400 * 59,
endTs: monitor.pageData.maxDateTodayTimestamp
},
{
text: `${l(lang, "30 Days")}`,
startTs: getUnixTime(startOfDay(sub(new Date(), { days: 29 }))),
startTs: monitor.pageData.startOfTheDay - 86400 * 29,
endTs: monitor.pageData.maxDateTodayTimestamp
},
{
text: `${l(lang, "14 Days")}`,
startTs: getUnixTime(startOfDay(sub(new Date(), { days: 13 }))),
startTs: monitor.pageData.startOfTheDay - 86400 * 13,
endTs: monitor.pageData.maxDateTodayTimestamp
},
{
text: `${l(lang, "7 Days")}`,
startTs: getUnixTime(startOfDay(sub(new Date(), { days: 6 }))),
startTs: monitor.pageData.startOfTheDay - 86400 * 6,
endTs: monitor.pageData.maxDateTodayTimestamp
},
{
text: l(lang, "Today"),
startTs: getUnixTime(startOfDay(new Date())),
startTs: monitor.pageData.startOfTheDay,
endTs: monitor.pageData.maxDateTodayTimestamp
}
];
@@ -130,6 +130,10 @@
let rollerLoading = false;
async function rollSummary(r) {
let newRolledAt = r;
analyticsEvent("monitor_interval_switch", {
tag: monitor.tag,
interval: uptimesRollers[newRolledAt].text
});
if (uptimesRollers[newRolledAt].value === undefined) {
rollerLoading = true;
@@ -179,12 +183,18 @@
if (embed) {
return;
}
let incidentIDs = incidentObj?.ids || [];
dayUptime = "NA";
dateFetchedFor = f(new Date(bar.timestamp * 1000), "EEEE, MMMM do, yyyy", selectedLang, $page.data.localTz);
showDailyDataModal = true;
loadingDayData = true;
dayIncidentsFull = [];
analyticsEvent("monitor_day_data", {
tag: monitor.tag,
data_date: dateFetchedFor
});
setTimeout(() => {
getToday(bar.timestamp, incidentIDs);
}, 50);
@@ -213,7 +223,7 @@
{@html monitor.description}
{/if}
</p>
<div class="absolute right-14 top-5 flex gap-x-2">
<div class="absolute right-4 top-5 flex gap-x-2 md:right-14">
{#if $page.data.isLoggedIn}
<Button
href="{base}/manage/app/monitors#{monitor.tag}"
+57 -3
View File
@@ -8,11 +8,62 @@
import GMI from "$lib/components/gmi.svelte";
export let data;
let defaultPattern = data.site?.pattern || "squares";
let allPets = [
{
url: base + "/chicken.gif",
bottom: "-5"
},
{
url: base + "/dog.gif",
bottom: "-17"
},
{
url: base + "/cockatiel.gif",
bottom: "-10"
},
{
url: base + "/crab.gif",
bottom: "-20"
},
{
url: base + "/fox.gif",
bottom: "-9"
},
{
url: base + "/horse.gif",
bottom: "-11"
},
{
url: base + "/panda.gif",
bottom: "0"
},
{
url: base + "/totoro.gif",
bottom: "-27"
},
{
url: base + "/rabbit.gif",
bottom: "0"
},
{
url: base + "/duck.gif",
bottom: "-5"
},
{
url: base + "/snake.gif",
bottom: "0"
}
];
let randomPet = allPets[Math.floor(Math.random() * allPets.length)];
</script>
<div class="{defaultPattern}-pattern"></div>
{#if defaultPattern == "pets" && !!randomPet}
<div class="pets-pattern" style="background-image: url({randomPet.url});bottom: {randomPet.bottom}px"></div>
{:else}
<div class="{defaultPattern}-pattern"></div>
{/if}
<header class="blurry-bg sticky top-0 z-50 mx-auto md:mt-2">
<header class="sticky top-0 z-50 mx-auto md:mt-2">
<div class="container flex h-14 max-w-[820px] items-center border bg-card px-3 md:rounded-md">
<a rel="external" href={data.site.home ? data.site.home : base} class="mr-6 flex items-center space-x-2">
{#if data.site.logo}
@@ -32,7 +83,10 @@
rel="external"
href={navItem.url}
class="flex rounded-md px-3 py-2 text-card-foreground transition-all ease-linear hover:bg-background"
on:click={() => analyticsEvent("nav", navItem.name)}
on:click={() =>
analyticsEvent("navigation", {
name: navItem.name
})}
>
{#if navItem.iconURL}
<GMI src={navItem.iconURL} classList="mr-1.5 mt-0.5 inline h-4" alt={navItem.name} />
+28 -12
View File
@@ -8,6 +8,7 @@ import {
CreateIncident,
AddIncidentComment,
AddIncidentMonitor,
GetIncidentByIDDashboard,
InsertNewAlert,
} from "./controllers/controller.js";
@@ -15,6 +16,7 @@ import db from "./db/db.js";
const TRIGGERED = "TRIGGERED";
const RESOLVED = "RESOLVED";
const CLOSED = "CLOSED";
async function createJSONCommonAlert(monitor, config, alert, severity) {
let siteData = await GetAllSiteData();
@@ -58,17 +60,22 @@ async function createNewIncident(monitor, alert, commonData) {
};
let update = commonData.description;
update =
update +
`<div style="line-height: 1.5;">
<p><strong>Monitor Name:</strong> ${monitor.name}</p>
<p><strong>Incident Status:</strong> ${commonData.status}</p>
<p><strong>Severity:</strong> ${commonData.severity}</p>
<p><strong>Monitor Status:</strong> ${alert.monitor_status}</p>
<p><strong>Monitor Health Checks:</strong> ${alert.health_checks}</p>
<p><strong>Monitor Failure Threshold:</strong> ${commonData.details.threshold}</p>
<p><strong>Visit:</strong> <a href="${commonData.actions[0].url}">${commonData.actions[0].url}</a></p>
</div>`;
update = `#### ${update}
---
**Monitor Name:** ${monitor.name}
**Incident Status:** ${commonData.status}
**Severity:** ${commonData.severity}
**Monitor Status:** ${alert.monitor_status}
**Monitor Health Checks:** ${alert.health_checks}
**Monitor Failure Threshold:** ${commonData.details.threshold}
**Visit:** [${commonData.actions[0].url}](${commonData.actions[0].url})
`;
let newIncident = await CreateIncident(payload);
@@ -82,8 +89,17 @@ async function createNewIncident(monitor, alert, commonData) {
}
async function closeIncident(alert, comment) {
//check if incident is already resolved
let incident = await GetIncidentByIDDashboard({
incident_id: alert.incident_number,
});
if (incident.status === CLOSED || incident.state === RESOLVED) {
console.log("Incident is already resolved");
return;
}
let incident_id = alert.incident_number;
return await AddIncidentComment(incident_id, comment, "RESOLVED", moment(alert.updated_at).unix());
return await AddIncidentComment(incident_id, comment, RESOLVED, moment(alert.updated_at).unix());
}
function createClosureComment(alert, commonJSON) {
+86 -4
View File
@@ -20,7 +20,7 @@ import { Resend } from "resend";
import crypto from "crypto";
import { format, subMonths, addMonths, startOfMonth } from "date-fns";
import { UP, DOWN, DEGRADED, NO_DATA, REALTIME, SIGNAL } from "../constants.js";
import { GetMinuteStartNowTimestampUTC, GetNowTimestampUTC, ReplaceAllOccurrences } from "../tool.js";
import { GetMinuteStartNowTimestampUTC, GetNowTimestampUTC, ReplaceAllOccurrences, ValidateEmail } from "../tool.js";
import getSMTPTransport from "../notification/smtps.js";
const saltRounds = 10;
@@ -77,6 +77,11 @@ const siteDataKeys = [
isValid: (value) => typeof value === "string",
data_type: "string",
},
{
key: "kenerTheme",
isValid: (value) => typeof value === "string",
data_type: "string",
},
{
key: "customCSS",
isValid: (value) => typeof value === "string",
@@ -89,8 +94,25 @@ const siteDataKeys = [
},
{
key: "pattern",
//string dots or sqaures or circ
isValid: (value) => typeof value === "string" && ["dots", "squares", "none"].includes(value),
//string dots or squares or circle
isValid: (value) =>
typeof value === "string" &&
[
"dots",
"squares",
"tiles",
"none",
"radial-blue",
"radial-mono",
"radial-midnight",
"circle-mono",
"carbon-fibre",
"texture-sky",
"angular-mono",
"angular-spring",
"angular-bloom",
"pets",
].includes(value),
data_type: "string",
},
{
@@ -169,6 +191,36 @@ const siteDataKeys = [
isValid: (value) => typeof value === "string" && value.trim().length > 0,
data_type: "string",
},
{
key: "analytics.googleTagManager",
isValid: IsValidJSONString,
data_type: "object",
},
{
key: "analytics.plausible",
isValid: IsValidJSONString,
data_type: "object",
},
{
key: "analytics.mixpanel",
isValid: IsValidJSONString,
data_type: "object",
},
{
key: "analytics.amplitude",
isValid: IsValidJSONString,
data_type: "object",
},
{
key: "analytics.clarity",
isValid: IsValidJSONString,
data_type: "object",
},
{
key: "analytics.umami",
isValid: IsValidJSONString,
data_type: "object",
},
];
export function InsertKeyValue(key, value) {
@@ -203,6 +255,20 @@ export async function GetAllSiteData() {
return transformedData;
}
//get all analytics data as json
export async function GetAllAnalyticsData() {
let data = await db.getAllSiteDataAnalytics();
//return all data as key value pairs, transform using data_type
let transformedData = [];
for (const d of data) {
transformedData.push({
key: d.key,
value: JSON.parse(d.value),
});
}
return transformedData;
}
export const CreateUpdateMonitor = async (monitor) => {
let monitorData = { ...monitor };
if (monitorData.id) {
@@ -242,6 +308,16 @@ export const GetMonitorsParsed = async (query) => {
export const CreateUpdateTrigger = async (alert) => {
let alertData = { ...alert };
let alertMetaJSON = JSON.parse(alertData.trigger_meta);
if (alertData.trigger_type === "email") {
let emailsArray = alertMetaJSON.to.split(",").map((email) => email.trim());
for (let i = 0; i < emailsArray.length; i++) {
if (!ValidateEmail(emailsArray[i])) {
throw new Error(`Invalid email: ${emailsArray[i]}`);
}
}
}
if (alertData.id) {
return await db.updateTrigger(alertData);
} else {
@@ -382,7 +458,6 @@ export const VerifyToken = async (token) => {
const decoded = jwt.verify(token, process.env.KENER_SECRET_KEY || DUMMY_SECRET);
return decoded; // Returns the decoded payload if the token is valid
} catch (err) {
console.error("Error verifying token:", err);
return undefined; // Returns null if the token is invalid
}
};
@@ -1176,6 +1251,13 @@ export const CreateNewUser = async (currentUser, data) => {
return await db.insertUser(user);
};
export const DeleteMonitorCompletelyUsingTag = async (tag) => {
await db.deleteMonitorDataByTag(tag);
await db.deleteIncidentMonitorsByTag(tag);
await db.deleteMonitorAlertsByTag(tag);
return await db.deleteMonitorsByTag(tag);
};
export const GetSiteMap = async (cookies) => {
let siteMapData = [];
let siteURLData = await GetSiteDataByKey("siteURL");
+33 -4
View File
@@ -138,9 +138,9 @@ class DbImpl {
return data;
}
async getLastStatusBeforeCombined(monitor_tags_arr, timestamp) {
async getLastStatusBeforeCombined(monitor_tags_arr, timestamp, minTimestamp) {
//monitor_tag_arr is an array of string
return await this.knex("monitoring_data")
let query = this.knex("monitoring_data")
.select(
"timestamp",
this.knex.raw("COUNT(*) as total_entries"),
@@ -153,8 +153,15 @@ class DbImpl {
END as status
`),
)
.whereIn("monitor_tag", monitor_tags_arr)
.where("timestamp", "=", timestamp)
.whereIn("monitor_tag", monitor_tags_arr);
if (!!minTimestamp) {
query = query.whereBetween("timestamp", [minTimestamp, timestamp]);
} else {
query = query.where("timestamp", "=", timestamp);
}
return await query
.groupBy("timestamp")
.havingRaw("COUNT(*) = ?", [monitor_tags_arr.length])
.orderBy("timestamp", "desc")
@@ -317,6 +324,11 @@ class DbImpl {
return await this.knex("site_data").where("key", key).first();
}
//get site data that starts with analytics.
async getAllSiteDataAnalytics() {
return await this.knex("site_data").where("key", "like", "analytics.%");
}
//insert into monitors
async insertMonitor(data) {
return await this.knex("monitors").insert({
@@ -911,6 +923,23 @@ class DbImpl {
.andWhere("invitation_expiry", ">", this.knex.fn.now())
.first();
}
//delete from monitor_data using tag
async deleteMonitorDataByTag(tag) {
return await this.knex("monitoring_data").where("monitor_tag", tag).del();
}
//delete from incident monitors using tag
async deleteIncidentMonitorsByTag(tag) {
return await this.knex("incident_monitors").where("monitor_tag", tag).del();
}
//delete from monitor alerts using tag
async deleteMonitorAlertsByTag(tag) {
return await this.knex("monitor_alerts").where("monitor_tag", tag).del();
}
//delete from monitors using tag
async deleteMonitorsByTag(tag) {
return await this.knex("monitors").where("tag", tag).del();
}
}
export default DbImpl;
+15 -2
View File
@@ -43,8 +43,20 @@ const seedSiteData = {
title: "Build stunning status pages in minutes",
subtitle: "Let your users know what's going on.",
},
footerHTML:
'<p class="text-center">\nMade using \n<a href="https://github.com/rajnandan1/kener" target="_blank" rel="noreferrer" class="font-medium underline underline-offset-4">\n Kener\n</a>\nan open source status page system built with Svelte and TailwindCSS.<br/>\nCreated with ❤️ by <a href="https://rajnandan.com" target="_blank" rel="noreferrer" class="font-medium underline underline-offset-4">Raj Nandan Sharma</a>.\n</p>\n',
footerHTML: `<div
class="container relative mt-4 max-w-[655px]"
>
<div class="flex flex-col items-center gap-4 px-8 md:flex-row md:gap-2 md:px-0">
<p class="text-center text-sm leading-loose text-muted-foreground ">
Made using
<a href="https://github.com/rajnandan1/kener" target="_blank" class="font-medium underline underline-offset-4">
Kener
</a>
an open source status page system built with Svelte and TailwindCSS.<br/>
Created with by <a href="https://rajnandan.com" target="_blank" class="font-medium underline underline-offset-4">Raj Nandan Sharma</a>.
</p>
</div>
</div>`,
i18n: {
defaultLocale: "en",
locales: [{ code: "en", name: "English", selected: true, disabled: false }],
@@ -88,6 +100,7 @@ const seedSiteData = {
categories: [{ name: "Home", description: "Monitors for Home Page" }],
homeIncidentCount: 5,
homeIncidentStartTimeWithin: 30,
kenerTheme: "default",
};
export default seedSiteData;
+1
View File
@@ -197,6 +197,7 @@ const FetchData = async function (site, monitor, localTz, selectedLang, lang) {
barRoundness: site.barRoundness,
midnight90DaysAgo: midnight90DaysAgoTz,
maxDateTodayTimestamp: maxDateTodayTimestampTz,
startOfTheDay: midnightTz,
};
};
export { FetchData };
+64 -63
View File
@@ -3,85 +3,86 @@ import net from "net"; // Use import instead of require
import ping from "ping";
const TCP = function (type, host, port, timeout) {
port = parseInt(port, 10);
return new Promise((resolve) => {
const socket = new net.Socket();
const start = process.hrtime.bigint(); // High-precision timestamp
let resolved = false;
timeout = Number(timeout);
port = parseInt(port, 10);
return new Promise((resolve) => {
const socket = new net.Socket();
const start = process.hrtime.bigint(); // High-precision timestamp
let resolved = false;
const onFinish = (status) => {
if (!resolved) {
resolved = true;
const end = process.hrtime.bigint();
const latency = Number(end - start) / 1e6; // Convert nanoseconds to milliseconds
socket.destroy();
resolve({ status, latency, host, port, type });
}
};
const onFinish = (status) => {
if (!resolved) {
resolved = true;
const end = process.hrtime.bigint();
const latency = Number(end - start) / 1e6; // Convert nanoseconds to milliseconds
socket.destroy();
resolve({ status, latency, host, port, type });
}
};
socket.setTimeout(timeout);
socket.once("connect", () => onFinish("open"));
socket.once("timeout", () => onFinish("timeout"));
socket.once("error", () => onFinish("error"));
socket.setTimeout(timeout);
socket.once("connect", () => onFinish("open"));
socket.once("timeout", () => onFinish("timeout"));
socket.once("error", () => onFinish("error"));
// Check if it's an IPv6 address (contains ':')
const options = {
host,
port,
family: type === "IP6" ? 6 : 4
}; //host.includes(":") ? { host, port, family: 6 } : { host, port };
socket.connect(options);
});
// Check if it's an IPv6 address (contains ':')
const options = {
host,
port,
family: type === "IP6" ? 6 : 4,
}; //host.includes(":") ? { host, port, family: 6 } : { host, port };
socket.connect(options);
});
};
const Ping = async function (type, host, timeout, count) {
let output = {
alive: false,
min: null,
max: null,
avg: null,
latencies: [],
latency: null,
host: host,
type: type
};
let output = {
alive: false,
min: null,
max: null,
avg: null,
latencies: [],
latency: null,
host: host,
type: type,
};
try {
let res = await ping.promise.probe(host, {
v6: type === "IP6",
timeout: type === "IP6" ? false : Math.floor(timeout / 1000),
min_reply: count
});
output.alive = res.alive;
output.min = res.min;
output.max = res.max;
output.avg = res.avg;
output.latencies = res.times;
output.latency = res.time;
} catch (error) {
console.log(`Error in pingCall IP4 for ${host}`, error);
}
return output;
try {
let res = await ping.promise.probe(host, {
v6: type === "IP6",
timeout: type === "IP6" ? false : Math.floor(timeout / 1000),
min_reply: count,
});
output.alive = res.alive;
output.min = res.min;
output.max = res.max;
output.avg = res.avg;
output.latencies = res.times;
output.latency = res.time;
} catch (error) {
console.log(`Error in pingCall IP4 for ${host}`, error);
}
return output;
};
/**
* @param {string} input
*/
function ExtractIPv6HostAndPort(input) {
const parts = input.split(":"); // Split by colons
const parts = input.split(":"); // Split by colons
// If there's a valid port at the end, extract it
const lastPart = parts[parts.length - 1];
const port = /^\d+$/.test(lastPart) ? parseInt(parts.pop(), 10) : null; // Check if last part is a number
// If there's a valid port at the end, extract it
const lastPart = parts[parts.length - 1];
const port = /^\d+$/.test(lastPart) ? parseInt(parts.pop(), 10) : null; // Check if last part is a number
// Reconstruct the IPv6 address
const host = parts.join(":");
// Reconstruct the IPv6 address
const host = parts.join(":");
// Ensure it's a valid IPv6 format
if (host.includes(":")) {
return { host, port }; // Port may be null if not present
}
return null; // Return null if the format is incorrect
// Ensure it's a valid IPv6 format
if (host.includes(":")) {
return { host, port }; // Port may be null if not present
}
return null; // Return null if the format is incorrect
}
export { TCP, ExtractIPv6HostAndPort, Ping };
+7 -1
View File
@@ -6,18 +6,24 @@ import db from "../db/db.js";
async function waitForDataAndReturn(arr, ts, d, maxTime) {
await Wait(d);
let data = await db.getLastStatusBeforeCombined(arr, ts);
if (data) {
data.type = REALTIME;
return data;
} else if (d > maxTime) {
data = await db.getLastStatusBeforeCombined(arr, ts, ts - 86400);
if (data) {
data.type = REALTIME;
return data;
}
return {
status: DOWN,
latency: 0,
type: TIMEOUT,
};
} else {
return await waitForDataAndReturn(arr, ts, d * 2, maxTime);
return await waitForDataAndReturn(arr, ts, d + 500, maxTime);
}
}
+11
View File
@@ -280,6 +280,9 @@ function Wait(ms) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
function MaskString(str) {
if (str.length <= 4) {
return "*".repeat(str.length);
}
return "*".repeat(str.length - 4) + str.slice(-4);
}
@@ -293,6 +296,13 @@ function HashString(str) {
return hash.digest("hex");
}
//function to validate email address
function ValidateEmail(email) {
const regex =
/^(?:(?:[a-zA-Z0-9!#$%&'*+\-/=?^_`{|}~]+\.)*[a-zA-Z0-9!#$%&'*+\-/=?^_`{|}~]+)@(?:(?:[a-zA-Z0-9](?:[a-zA-Z0-9-]*[a-zA-Z0-9])?\.)+[a-zA-Z]{2,}|(?:\[(?:(?:25[0-5]|2[0-4]\d|[01]?\d\d?)(?:\.(?!$)|$)){4}\]))$/;
return regex.test(email);
}
export {
IsValidURL,
IsValidHTTPMethod,
@@ -322,4 +332,5 @@ export {
MaskString,
GetDbType,
HashString,
ValidateEmail,
};
+42
View File
@@ -0,0 +1,42 @@
/**
* ***************************************************
* * *
* * Amplitude Analytics Capture Snippet *
* * *
* ***************************************************
**/
// @ts-nocheck
(function () {
let loadJS = function (url, implementationCode, location) {
//url is URL of external file, implementationCode is the code
//to be called from the file, location is the location to
//insert the <script> element
let scriptTag = document.createElement("script");
scriptTag.src = url;
scriptTag.onload = implementationCode;
scriptTag.onreadystatechange = implementationCode;
location.appendChild(scriptTag);
};
let initJS = function () {
window.amplitude.add(window.sessionReplay.plugin({ sampleRate: 1 }));
window.amplitude.init("{{api_key}}", { fetchRemoteConfig: true, autocapture: true });
window.addEventListener("analyticsEvent", function (e) {
// Extract event name and data from the custom event
const eventName = e.detail.event;
const eventData = e.detail.data || {};
if (eventName) {
window.amplitude.track(eventName, eventData);
}
});
};
//on dom ready
document.addEventListener("DOMContentLoaded", function () {
loadJS("https://cdn.amplitude.com/script/{{api_key}}.js", initJS, document.getElementsByTagName("head")[0]);
});
})();
+20
View File
@@ -0,0 +1,20 @@
/**
* ***************************************************
* * *
* * Clarity Analytics Capture Snippet *
* * *
* ***************************************************
**/
// @ts-nocheck
(function (c, l, a, r, i, t, y) {
c[a] =
c[a] ||
function () {
(c[a].q = c[a].q || []).push(arguments);
};
t = l.createElement(r);
t.async = 1;
t.src = "https://www.clarity.ms/tag/" + i;
y = l.getElementsByTagName(r)[0];
y.parentNode.insertBefore(t, y);
})(window, document, "clarity", "script", "{{project_id}}");
@@ -0,0 +1,48 @@
/**
* ***************************************************
* * *
* * Google Analytics Capture Snippet *
* * *
* ***************************************************
**/
// @ts-nocheck
(function () {
let loadJS = function (url, implementationCode, location) {
//url is URL of external file, implementationCode is the code
//to be called from the file, location is the location to
//insert the <script> element
let scriptTag = document.createElement("script");
scriptTag.src = url;
scriptTag.onload = implementationCode;
scriptTag.onreadystatechange = implementationCode;
location.appendChild(scriptTag);
};
let initJS = function () {
window.dataLayer = window.dataLayer || [];
function gtag() {
window.dataLayer.push(arguments);
}
gtag("js", new Date());
gtag("config", "{{id}}");
window.addEventListener("analyticsEvent", function (e) {
// Extract event name and data from the custom event
const eventName = e.detail.event;
const eventData = e.detail.data || {};
// Send the event to Google Tag Manager
if (window.dataLayer && eventName) {
gtag("event", eventName, eventData);
}
});
};
//on dom ready
document.addEventListener("DOMContentLoaded", function () {
loadJS("https://www.googletagmanager.com/gtag/js?id={{id}}", initJS, document.getElementsByTagName("head")[0]);
});
})();
+78
View File
@@ -0,0 +1,78 @@
/**
* ***************************************************
* * *
* * Mixpanel Analytics Capture Snippet *
* * *
* ***************************************************
**/
// @ts-nocheck
(function (f, b) {
if (!b.__SV) {
var e, g, i, h;
window.mixpanel = b;
b._i = [];
b.init = function (e, f, c) {
function g(a, d) {
var b = d.split(".");
2 == b.length && ((a = a[b[0]]), (d = b[1]));
a[d] = function () {
a.push([d].concat(Array.prototype.slice.call(arguments, 0)));
};
}
var a = b;
"undefined" !== typeof c ? (a = b[c] = []) : (c = "mixpanel");
a.people = a.people || [];
a.toString = function (a) {
var d = "mixpanel";
"mixpanel" !== c && (d += "." + c);
a || (d += " (stub)");
return d;
};
a.people.toString = function () {
return a.toString(1) + ".people (stub)";
};
i =
"disable time_event track track_pageview track_links track_forms track_with_groups add_group set_group remove_group register register_once alias unregister identify name_tag set_config reset opt_in_tracking opt_out_tracking has_opted_in_tracking has_opted_out_tracking clear_opt_in_out_tracking start_batch_senders people.set people.set_once people.unset people.increment people.append people.union people.track_charge people.clear_charges people.delete_user people.remove".split(
" ",
);
for (h = 0; h < i.length; h++) g(a, i[h]);
var j = "set set_once union unset remove delete".split(" ");
a.get_group = function () {
function b(c) {
d[c] = function () {
call2_args = arguments;
call2 = [c].concat(Array.prototype.slice.call(call2_args, 0));
a.push([e, call2]);
};
}
for (var d = {}, e = ["get_group"].concat(Array.prototype.slice.call(arguments, 0)), c = 0; c < j.length; c++)
b(j[c]);
return d;
};
b._i.push([e, f, c]);
};
b.__SV = 1.2;
e = f.createElement("script");
e.type = "text/javascript";
e.async = !0;
e.src =
"undefined" !== typeof MIXPANEL_CUSTOM_LIB_URL
? MIXPANEL_CUSTOM_LIB_URL
: "file:" === f.location.protocol && "//cdn.mxpnl.com/libs/mixpanel-2-latest.min.js".match(/^\/\//)
? "https://cdn.mxpnl.com/libs/mixpanel-2-latest.min.js"
: "//cdn.mxpnl.com/libs/mixpanel-2-latest.min.js";
g = f.getElementsByTagName("script")[0];
g.parentNode.insertBefore(e, g);
}
})(document, window.mixpanel || []);
window.mixpanel.init("{{YOUR_PROJECT_TOKEN}}", { track_pageview: true, autocapture: true });
window.addEventListener("analyticsEvent", function (e) {
const eventName = e.detail.event;
const eventData = e.detail.data || {};
if (eventName) {
window.mixpanel.track(eventName, eventData);
}
});
+47
View File
@@ -0,0 +1,47 @@
/**
* ***************************************************
* * *
* * Plausible Analytics Capture Snippet *
* * *
* ***************************************************
**/
// @ts-nocheck
(function () {
let loadJS = function (url, implementationCode, location) {
//url is URL of external file, implementationCode is the code
//to be called from the file, location is the location to
//insert the <script> element
let scriptTag = document.createElement("script");
scriptTag.src = url;
scriptTag.defer = true;
scriptTag.dataset.domain = "{{domain}}";
scriptTag.dataset.api = "{{api}}";
scriptTag.onload = implementationCode;
scriptTag.onreadystatechange = implementationCode;
location.appendChild(scriptTag);
};
let initJS = function () {
window.plausible =
window.plausible ||
function () {
(window.plausible.q = window.plausible.q || []).push(arguments);
};
window.addEventListener("analyticsEvent", function (e) {
// Extract event name and data from the custom event
const eventName = e.detail.event;
const eventData = e.detail.data || {};
if (eventName) {
window.plausible(eventName, { props: eventData });
}
});
};
//on dom ready
document.addEventListener("DOMContentLoaded", function () {
loadJS("{{script_src}}", initJS, document.getElementsByTagName("head")[0]);
});
})();
+38
View File
@@ -0,0 +1,38 @@
/**
* ***************************************************
* * *
* * Umami Analytics Capture Snippet *
* * *
* ***************************************************
**/
// @ts-nocheck
(function () {
let loadJS = function (url, implementationCode, location) {
//url is URL of external file, implementationCode is the code
//to be called from the file, location is the location to
//insert the <script> element
let scriptTag = document.createElement("script");
scriptTag.src = url;
scriptTag.onload = implementationCode;
scriptTag.dataset.websiteId = "{{website_id}}";
scriptTag.onreadystatechange = implementationCode;
location.appendChild(scriptTag);
};
let initJS = function () {
window.addEventListener("analyticsEvent", function (e) {
// Extract event name and data from the custom event
const eventName = e.detail.event;
const eventData = e.detail.data || {};
window.umami.track(eventName, eventData);
});
};
//on dom ready
document.addEventListener("DOMContentLoaded", function () {
loadJS("https://cloud.umami.is/script.js", initJS, document.getElementsByTagName("head")[0]);
});
})();
+1 -1
View File
@@ -217,7 +217,7 @@
</div>
</div>
<div
class="prose prose-stone max-w-none dark:prose-invert prose-code:rounded prose-code:py-[0.2rem] prose-code:font-mono prose-code:text-sm prose-code:font-normal prose-pre:bg-opacity-0 dark:prose-pre:bg-neutral-900"
class="kener-md prose prose-stone max-w-none dark:prose-invert prose-code:rounded prose-code:py-[0.2rem] prose-code:font-mono prose-code:text-sm prose-code:font-normal prose-pre:bg-opacity-0 dark:prose-pre:bg-neutral-900"
>
<slot />
</div>
+14 -76
View File
@@ -1,6 +1,7 @@
<script>
import "../../app.postcss";
import "../../kener.css";
import "../../theme.css";
import Nav from "$lib/components/nav.svelte";
import { onMount } from "svelte";
import { Input } from "$lib/components/ui/input";
@@ -22,8 +23,6 @@
let searchTzValue = "";
let defaultTheme = data.site.theme;
const allLocales = data.site.i18n?.locales.filter((locale) => locale.selected === true);
let hasConfiguredAnalytics =
!!data.site.analytics && data.site.analytics.filter((provider) => !!provider.id).length > 0;
function toggleMode() {
if ($mode === "light") {
@@ -61,10 +60,12 @@
//set timezone
function setTz(tz) {
document.cookie = `localTz=${tz};max-age=${60 * 60 * 24 * 365 * 30};path=${base ? "base" : "/"};`;
analyticsEvent("timezone_changed", {
time_zone: tz
});
location.reload();
}
let Analytics;
let myTimezone = data.localTz;
onMount(async () => {
myTimezone = Intl.DateTimeFormat().resolvedOptions().timeZone;
@@ -76,70 +77,20 @@
if (defaultTheme != "none") {
setMode(defaultTheme);
}
//
const providers = data.site.analytics;
const analyticsPlugins = [];
if (hasConfiguredAnalytics) {
//loop object
for (let i = 0; i < providers.length; i++) {
const provider = providers[i];
if (!!!provider.id) {
continue;
}
if (provider.type == "GA") {
analyticsPlugins.push(
analyticsGa.default({
measurementIds: provider.id
})
);
} else if (provider.type == "AMPLITUDE") {
analyticsPlugins.push(
analyticsAmplitude({
apiKey: provider.measurementIds[0],
options: {
trackingOptions: {
ip_address: false
}
}
})
);
} else if (provider.type == "MIXPANEL") {
analyticsPlugins.push(
analyticsMixpanel({
token: provider.measurementIds[0]
})
);
}
}
}
if (hasConfiguredAnalytics) {
Analytics = _analytics.init({
app: "kener",
debug: true,
version: 100,
plugins: analyticsPlugins
});
Analytics.page();
}
if (!!data.bgc && data.bgc[0] == "#") {
document.body.style.backgroundColor = data.bgc;
}
});
function captureAnalytics(e) {
if (hasConfiguredAnalytics) {
const { event, data } = e.detail;
Analytics.track(event, data);
}
}
let customCSS = `<style>${data.site.customCSS}</style>`;
if (!!data.site.favicon && !data.site.favicon.startsWith("http")) data.site.favicon = `${base}${data.site.favicon}`;
let kenerTheme = data.site.kenerTheme || "default";
</script>
<svelte:window on:analyticsEvent={captureAnalytics} />
<svelte:head>
<script src="{base}/api/js/capture.js"></script>
{#if data.site.favicon}
<link rel="icon" id="kener-app-favicon" href={data.site.favicon} />
{/if}
@@ -147,14 +98,6 @@
<link href={data.site.font.cssSrc} rel="stylesheet" />
{/if}
{#if hasConfiguredAnalytics}
<script src="https://unpkg.com/analytics/dist/analytics.min.js"></script>
{#each data.site.analytics as { id, type, script }}
{#if !!id}
<script data-type={type} src={script}></script>
{/if}
{/each}
{/if}
{#if !!data.site.customCSS}
{@html customCSS}
{/if}
@@ -168,6 +111,7 @@
--down-color: {data.site.colors.DOWN};
--degraded-color: {data.site.colors.DEGRADED}
"
class="kener-theme-{kenerTheme}"
>
<Nav {data} />
<div class="main-content min-h-[70vh]">
@@ -175,16 +119,8 @@
</div>
{#if !!data.site.footerHTML}
<footer class="z-10 py-6 md:px-8 md:py-0">
<div
class="container relative flex max-w-[655px] flex-col items-center justify-center gap-4 pl-0 md:h-24 md:flex-row"
>
<div class="flex flex-col items-center gap-4 px-8 md:flex-row md:gap-2 md:px-0">
<p class="text-center text-sm leading-loose text-muted-foreground md:text-left">
{@html data.site.footerHTML}
</p>
</div>
</div>
<footer class="kener-footer z-10 py-6 md:px-8 md:py-0">
{@html data.site.footerHTML}
</footer>
{/if}
<div class="fixed bottom-4 right-4 z-20 flex flex-col rounded-md bg-background">
@@ -222,7 +158,9 @@
<div class="text-xs font-semibold">
<Button
variant={tz === data.localTz ? "primary" : "ghost"}
class="h-8 w-full justify-start rounded-none text-xs text-muted-foreground"
class="h-8 w-full justify-start rounded-none text-xs {tz === data.localTz
? 'text-background'
: 'text-muted-foreground'}"
on:click={() => setTz(tz)}
>
{tz}
+32 -22
View File
@@ -12,6 +12,7 @@
import { hotKeyAction, clickOutsideAction } from "svelte-legos";
import { onMount } from "svelte";
import ShareMenu from "$lib/components/shareMenu.svelte";
import { analyticsEvent } from "$lib/boringOne";
import { scale } from "svelte/transition";
import { format } from "date-fns";
import GMI from "$lib/components/gmi.svelte";
@@ -39,12 +40,22 @@
});
let kindFilter = "INCIDENT";
function kindOfIncidents(kind) {
analyticsEvent("monitor_incident_kind_filter", {
kind: kind
});
kindFilter = kind;
}
if (data.allRecentIncidents.length == 0) {
kindOfIncidents("MAINTENANCE");
}
function closeShareMenu() {
shareMenusToggle = false;
analyticsEvent("monitor_share_menu_close", {
tag: activeMonitor.tag
});
}
</script>
<svelte:head>
@@ -66,24 +77,20 @@
<div class="mt-12"></div>
{#if data.hero}
<section class="section-hero mx-auto mb-8 flex w-full max-w-[655px] flex-1 flex-col items-start justify-center">
<div class="mx-auto max-w-screen-xl px-4 lg:flex lg:items-center">
<div class="blurry-bg mx-auto max-w-3xl text-center">
{#if data.hero.image}
<GMI src={data.hero.image} classList="m-auto mb-2 h-14 w-14" alt="" srcset="" />
{/if}
{#if data.hero.title}
<h1
class="bg-gradient-to-r from-green-300 via-blue-500 to-purple-600 bg-clip-text text-5xl font-extrabold leading-tight text-transparent"
>
{@html data.hero.title}
</h1>
{/if}
{#if data.hero.subtitle}
<h2 class="mx-auto mt-4 max-w-xl sm:text-xl">
{@html data.hero.subtitle}
</h2>
{/if}
</div>
<div class="mx-auto max-w-3xl text-center">
{#if data.hero.image}
<GMI src={data.hero.image} classList="m-auto mb-2 h-14 w-14" alt="" srcset="" />
{/if}
{#if data.hero.title}
<h1 class="h1 text-5xl font-extrabold leading-tight">
{@html data.hero.title}
</h1>
{/if}
{#if data.hero.subtitle}
<h2 class="mx-auto mt-4 max-w-xl sm:text-xl">
{@html data.hero.subtitle}
</h2>
{/if}
</div>
</section>
{/if}
@@ -94,10 +101,13 @@
class="bounce-left h-8 justify-start pl-1.5"
on:click={() => {
if (data.pageType == "category") {
analyticsEvent("category_back_button_click");
return window.history.back();
} else if (data.pageType == "monitor") {
analyticsEvent("monitor_back_button_click");
return (window.location.href = `${base}/`);
} else if (data.pageType == "group") {
analyticsEvent("group_back_button_click");
return (window.location.href = `${base}/`);
}
}}
@@ -109,7 +119,7 @@
{/if}
{#if data.allRecentIncidents.length + data.allRecentMaintenances.length > 0}
<section
class="section-events mx-auto mb-8 flex w-full max-w-[655px] flex-1 flex-col items-start justify-center backdrop-blur-[2px]"
class="section-events mx-auto mb-8 flex w-full max-w-[655px] flex-1 flex-col items-start justify-center"
id=""
>
<div class="mb-2 grid w-full grid-cols-2 gap-4">
@@ -256,21 +266,21 @@
transition:scale={{ duration: 100 }}
use:hotKeyAction={{
code: "Escape",
cb: () => (shareMenusToggle = false)
cb: () => closeShareMenu()
}}
class="moldal-container fixed left-0 top-0 z-30 h-screen w-full bg-card bg-opacity-30 backdrop-blur-sm"
>
<div
use:clickOutsideAction
on:clickoutside={() => {
shareMenusToggle = false;
closeShareMenu();
}}
class="absolute left-1/2 top-1/2 h-fit w-full max-w-md -translate-x-1/2 -translate-y-1/2 rounded-md border bg-background shadow-lg backdrop-blur-lg md:w-[568px]"
>
<Button
variant="ghost"
on:click={() => {
shareMenusToggle = false;
closeShareMenu();
}}
class="absolute right-2 top-2 z-40 h-6 w-6 rounded-full border bg-background p-1"
>
@@ -0,0 +1,65 @@
// @ts-nocheck
import gtm from "$lib/snippets/capture/google-tag-manager.js?raw";
import mx from "$lib/snippets/capture/mixpanel.js?raw";
import am from "$lib/snippets/capture/amplitude.js?raw";
import pl from "$lib/snippets/capture/plausible.js?raw";
import mscl from "$lib/snippets/capture/clarity.js?raw";
import um from "$lib/snippets/capture/umami.js?raw";
import { GetAllAnalyticsData } from "$lib/server/controllers/controller.js";
export async function GET({ params, setHeaders, url }) {
let analyticsData = await GetAllAnalyticsData();
analyticsData = analyticsData.filter((item) => item.value.isEnabled == true);
if (analyticsData.length == 0) {
return new Response("//no data", {
headers: {
"Content-Type": "application/javascript",
},
});
}
//reduce to key value
analyticsData = analyticsData.reduce((acc, item) => {
acc[item.key] = item.value;
return acc;
}, {});
let captureScript = "";
if (!!analyticsData["analytics.googleTagManager"]) {
let id = analyticsData["analytics.googleTagManager"].requirements["Measurement ID"];
captureScript = captureScript + ";\n" + gtm.replaceAll("{{id}}", id);
}
if (!!analyticsData["analytics.mixpanel"]) {
let projectToken = analyticsData["analytics.mixpanel"].requirements["Project Token"];
captureScript = captureScript + ";\n" + mx.replaceAll("{{YOUR_PROJECT_TOKEN}}", projectToken);
}
if (!!analyticsData["analytics.amplitude"]) {
let api_key = analyticsData["analytics.amplitude"].requirements["Amplitude API Key"];
captureScript = captureScript + ";\n" + am.replaceAll("{{api_key}}", api_key);
}
if (!!analyticsData["analytics.plausible"]) {
let domain = analyticsData["analytics.plausible"].requirements["Domain"];
let api = analyticsData["analytics.plausible"].requirements["API"];
let script_src = analyticsData["analytics.plausible"].requirements["Script Source"];
captureScript =
captureScript +
";\n" +
pl.replaceAll("{{domain}}", domain).replaceAll("{{api}}", api).replaceAll("{{script_src}}", script_src);
}
if (!!analyticsData["analytics.clarity"]) {
let project_id = analyticsData["analytics.clarity"].requirements["Project ID"];
captureScript = captureScript + ";\n" + mscl.replaceAll("{{project_id}}", project_id);
}
if (!!analyticsData["analytics.umami"]) {
let website_id = analyticsData["analytics.umami"].requirements["Website ID"];
captureScript = captureScript + ";\n" + um.replaceAll("{{website_id}}", website_id);
}
return new Response(captureScript, {
headers: {
"Content-Type": "application/javascript",
},
});
}
+20 -14
View File
@@ -29,14 +29,28 @@ export async function POST({ request }) {
};
}
let dayData = await db.getMonitoringData(monitor.tag, payload.startTs, payload.endTs);
let anchorStatus = await GetLastStatusBefore(monitor.tag, start);
dayData = InterpolateData(dayData, payload.startTs, anchorStatus, payload.endTs);
let siteData = await GetAllSiteData();
let ups = 0;
let downs = 0;
let degradeds = 0;
let dayData = await db.getMonitoringData(monitor.tag, start, end);
let anchorStatus = await GetLastStatusBefore(monitor.tag, start);
dayData = InterpolateData(dayData, start, anchorStatus, end);
let aggregatedData = dayData.reduce(
(acc, row) => {
acc[row.status] = (acc[row.status] || 0) + 1;
return acc;
},
{
UP: 0,
DOWN: 0,
DEGRADED: 0,
},
);
let ups = Number(aggregatedData.UP);
let downs = Number(aggregatedData.DOWN);
let degradeds = Number(aggregatedData.DEGRADED);
for (let i = 0; i < dayData.length; i++) {
let row = dayData[i];
@@ -54,14 +68,6 @@ export async function POST({ request }) {
_0Day[timestamp].status = status;
_0Day[timestamp].cssClass = StatusObj[status];
}
if (status == "UP") {
ups++;
} else if (status == "DOWN") {
downs++;
} else if (status == "DEGRADED") {
degradeds++;
}
}
let total = ups + downs + degradeds;
@@ -2,6 +2,7 @@
import { Badge } from "$lib/components/ui/badge";
import Incident from "$lib/components/IncidentNew.svelte";
import { Button } from "$lib/components/ui/button";
import { analyticsEvent } from "$lib/boringOne";
import ArrowRight from "lucide-svelte/icons/arrow-right";
import ArrowLeft from "lucide-svelte/icons/arrow-left";
import ChevronLeft from "lucide-svelte/icons/chevron-left";
@@ -76,6 +77,7 @@
variant="outline"
class="bounce-left h-8 justify-start pl-1.5"
on:click={() => {
analyticsEvent("incident_back_button_click");
return (window.location.href = `${base}/`);
}}
>
@@ -109,7 +111,12 @@
<h1 class=" text-xl font-semibold leading-tight">
{l(data.lang, "No Incident in %date", {
date: f(parse(data.thisMonthName, "MMMM-yyyy", new Date()), "MMMM, yyyy", selectedLang, $page.data.localTz)
date: f(
new Date(data.midnightMonthStartUTCTimestamp * 1000),
"MMMM, yyyy",
selectedLang,
$page.data.localTz
)
})}
</h1>
</div>
@@ -5,13 +5,7 @@ import db from "$lib/server/db/db.js";
import { SendEmailWithTemplate, GetSiteLogoURL } from "$lib/server/controllers/controller.js";
import forgotPasswordTemplate from "$lib/server/templates/forgot_pass.html?raw";
import {
HashPassword,
GenerateSalt,
GenerateToken,
VerifyToken,
GetSMTPFromENV,
} from "$lib/server/controllers/controller.js";
import { HashPassword, GenerateToken, VerifyToken, GetSMTPFromENV } from "$lib/server/controllers/controller.js";
export async function POST({ request, cookies }) {
//read form post data email and password
@@ -2,23 +2,7 @@
import { json, redirect } from "@sveltejs/kit";
import { base } from "$app/paths";
import db from "$lib/server/db/db.js";
import {
HashPassword,
GenerateSalt,
GenerateToken,
CookieConfig,
ValidatePassword,
} from "$lib/server/controllers/controller.js";
//function to validate a strong password
/**
* Validates a password to ensure it meets the following criteria:
* - Contains at least one digit.
* - Contains at least one lowercase letter.
* - Contains at least one uppercase letter.
* - Contains at least one letter (either lowercase or uppercase).
* - Has a minimum length of 8 characters.
*/
import { HashPassword, GenerateToken, CookieConfig, ValidatePassword } from "$lib/server/controllers/controller.js";
export async function POST({ request, cookies }) {
//read form post data email and password
@@ -21,7 +21,6 @@ export async function load({ params, route, url, cookies, request }) {
}
userDB = isLoggedIn.user;
return {
siteData,
KENER_SECRET_KEY: !!process.env.KENER_SECRET_KEY ? MaskString(process.env.KENER_SECRET_KEY) : "",
@@ -43,6 +43,7 @@ import {
GetUserByID,
GetUserByEmail,
ManualUpdateUserData,
DeleteMonitorCompletelyUsingTag,
} from "$lib/server/controllers/controller.js";
import { INVITE_VERIFY_EMAIL } from "$lib/server/constants.js";
@@ -148,6 +149,9 @@ export async function POST({ request, cookies }) {
resp = await CreateUpdateMonitor(data);
} else if (action == "getMonitors") {
resp = await GetMonitors(data);
} else if (action == "deleteMonitor") {
AdminEditorCan(userDB.role);
resp = await DeleteMonitorCompletelyUsingTag(data.tag);
} else if (action == "createUpdateTrigger") {
AdminEditorCan(userDB.role);
resp = await CreateUpdateTrigger(data);
@@ -27,6 +27,12 @@
import MessageSquarePlus from "lucide-svelte/icons/message-square-plus";
import ChevronRight from "lucide-svelte/icons/chevron-right";
import Trash from "lucide-svelte/icons/trash";
import CodeMirror from "svelte-codemirror-editor";
import { markdown } from "@codemirror/lang-markdown";
import { marked } from "marked";
import { githubLight, githubDark } from "@uiw/codemirror-theme-github";
import { mode } from "mode-watcher";
import * as Select from "$lib/components/ui/select";
export let data;
let status = "OPEN";
@@ -414,6 +420,7 @@
function setCommentEdit(c) {
newComment = { ...c };
newComment.commented_at = new Date(Number(c.commented_at) * 1000);
scrollToElementInsideContainer("incident-modal", "edit-comment-box");
}
$: {
//broadcast a custom event named blockScroll
@@ -429,6 +436,17 @@
showModal = false;
window.location.hash = "";
}
function scrollToElementInsideContainer(containerId, elementId) {
const container = document.getElementById(containerId);
const element = document.getElementById(elementId);
if (container && element) {
container.scrollTo({
top: element.offsetTop - container.offsetTop,
behavior: "smooth"
});
}
}
</script>
<div class="min-h-[70vh]">
@@ -674,7 +692,7 @@
on:clickoutside={closeModal}
class="absolute right-0 top-0 h-screen w-[800px] border-l bg-background p-4 shadow-xl"
>
<div class="mt-0 w-full overflow-y-auto p-3" style="height: 100vh;">
<div class="mt-0 w-full overflow-y-auto p-3" style="height: 100vh;" id="incident-modal">
<div class="rounded-md border p-4">
<div>
{#if newIncident.id}
@@ -735,12 +753,24 @@
<span class="capitalize">{newIncident.incident_type}</span>
Summary
<span class="text-red-500">*</span>
(supports Markdown)
</Label>
<Input
class="mt-2"
bind:value={newIncident.firstComment}
placeholder="We are facing degraded service in all servers"
/>
<div class="overflow-hidden rounded-md">
<CodeMirror
bind:value={newIncident.firstComment}
lang={markdown()}
theme={$mode == "dark" ? githubDark : githubLight}
styles={{
"&": {
width: "100%",
maxWidth: "100%",
height: "320px",
border: "1px solid hsl(var(--border) / var(--tw-border-opacity))",
borderRadius: "var(--radius)"
}
}}
/>
</div>
</div>
</div>
{/if}
@@ -860,11 +890,23 @@
/>
</div>
<div class="mt-4">
<textarea
bind:value={newComment.comment}
class="h-24 w-full rounded-sm border p-2 text-sm"
placeholder="There is an outage in all server. Our best minds are on it. We will update you soon."
></textarea>
<p class="mb-1 text-right text-xs font-medium">Supports Markdown</p>
<div class="overflow-hidden rounded-md" id="edit-comment-box">
<CodeMirror
bind:value={newComment.comment}
lang={markdown()}
theme={$mode == "dark" ? githubDark : githubLight}
styles={{
"&": {
width: "100%",
maxWidth: "100%",
height: "220px",
border: "1px solid hsl(var(--border) / var(--tw-border-opacity))",
borderRadius: "var(--radius)"
}
}}
/>
</div>
</div>
<div class="flex justify-end gap-x-2">
@@ -913,7 +955,14 @@
{#each comments as comment}
<div class="flex items-center justify-between gap-2 border-b py-2">
<div class="w-full rounded px-2 py-2 {newComment.id == comment.id ? 'bg-input' : ''}">
<p class="mb-2 text-xs font-medium">{@html comment.comment}</p>
<div class="mb-2 max-h-[400px] overflow-y-auto rounded-md border bg-card p-2">
<div
class="kener-md prose prose-stone max-w-none dark:prose-invert prose-code:rounded prose-code:py-[0.2rem] prose-code:font-mono prose-code:text-sm prose-code:font-normal prose-pre:bg-opacity-0 dark:prose-pre:bg-neutral-900"
>
{@html marked.parse(comment.comment)}
</div>
</div>
<div class="flex w-full justify-between gap-x-2">
<div class="text-xs font-semibold text-muted-foreground">
{#if currentIncident.incident_type == "INCIDENT"}
@@ -0,0 +1,7 @@
import seedSiteData from "$lib/server/db/seedSiteData.js";
export async function load({ parent }) {
return {
defaultFooterHTML: seedSiteData.footerHTML,
};
}
+902
View File
@@ -0,0 +1,902 @@
.dots-pattern {
position: absolute;
top: 0px;
left: 0;
width: 100%;
z-index: 0;
background-repeat: no-repeat;
background-size: 100%;
height: 100svh;
background: linear-gradient(177deg, rgba(255, 137, 131, 0.5) 0%, rgba(35, 136, 224, 0.05) 60%);
clip-path: polygon(0 0, 100% 0, 100% 54%, 0% 100%);
}
.dots-pattern::after {
content: "";
position: absolute;
background-image: radial-gradient(rgba(0, 0, 0, 0) 1.5px, var(--background-kener) 1px);
background-size: 14px 14px;
width: 100%;
height: 100vh;
top: 0;
transform: blur(3px);
left: 0;
}
.squares-pattern {
position: absolute;
top: 0;
left: 0;
width: 100%;
z-index: 0;
background-repeat: no-repeat;
background-size: 100%;
height: 100svh;
/* clip-path: polygon(0 0, 100% 0, 100% 54%, 0% 100%); */
}
.squares-pattern::after {
content: "";
position: absolute;
background-image:
linear-gradient(#444cf7 1px, transparent 1px),
linear-gradient(to right, #444cf7 1px, var(--background-kener) 1px);
-webkit-mask-image: linear-gradient(
180deg,
rgba(0, 0, 0, 1) 0%,
rgba(0, 0, 0, 0.8) 20%,
rgba(0, 0, 0, 0.6) 40%,
rgba(0, 0, 0, 0.4) 60%,
rgba(0, 0, 0, 0.2) 80%,
rgba(0, 0, 0, 0) 100%
);
mask-image: linear-gradient(
180deg,
rgba(0, 0, 0, 0.2) 0%,
rgba(0, 0, 0, 0.1) 20%,
rgba(0, 0, 0, 0.05) 40%,
rgba(0, 0, 0, 0.025) 60%,
rgba(0, 0, 0, 0.0125) 80%,
rgba(0, 0, 0, 0) 100%
);
background-size: 32px 32px;
width: 100%;
height: 100vh;
top: 0;
left: 0;
}
.dark .squares-pattern::after {
-webkit-mask-image: linear-gradient(
to bottom,
rgba(0, 0, 0, 0.5) 0%,
rgba(0, 0, 0, 0.25) 20%,
rgba(0, 0, 0, 0.125) 40%,
rgba(0, 0, 0, 0.0625) 60%,
rgba(0, 0, 0, 0.03125) 80%,
rgba(0, 0, 0, 0) 100%
);
mask-image: linear-gradient(
180deg,
rgba(0, 0, 0, 0.5) 0%,
rgba(0, 0, 0, 0.25) 20%,
rgba(0, 0, 0, 0.125) 40%,
rgba(0, 0, 0, 0.0625) 60%,
rgba(0, 0, 0, 0.03125) 80%,
rgba(0, 0, 0, 0) 100%
);
background-image:
linear-gradient(#616fbf 1px, transparent 1px),
linear-gradient(to right, #616fbf 1px, var(--background-kener) 1px);
}
.radial-blue-pattern {
position: absolute;
top: 0;
left: 0;
width: 100%;
z-index: 0;
background-repeat: no-repeat;
background-size: 100%;
height: 100svh;
background-image: radial-gradient(
circle at top right,
rgb(49, 157, 235) 0%,
rgb(49, 157, 235) 13%,
rgb(76, 166, 234) 13%,
rgb(76, 166, 234) 23%,
rgb(103, 176, 232) 23%,
rgb(103, 176, 232) 33%,
rgb(130, 185, 231) 33%,
rgb(130, 185, 231) 46%,
rgb(156, 194, 230) 46%,
rgb(156, 194, 230) 48%,
rgb(183, 203, 229) 48%,
rgb(183, 203, 229) 63%,
rgb(210, 213, 227) 63%,
rgb(210, 213, 227) 83%,
rgb(237, 222, 226) 83%,
rgb(237, 222, 226) 100%
);
}
.dark .radial-blue-pattern {
position: absolute;
top: 0;
left: 0;
width: 100%;
z-index: 0;
background-repeat: no-repeat;
background-size: 100%;
height: 100svh;
background-image: radial-gradient(
circle at bottom right,
rgb(42, 23, 131) 0%,
rgb(42, 23, 131) 20%,
rgb(42, 40, 158) 20%,
rgb(42, 40, 158) 40%,
rgb(29, 41, 133) 40%,
rgb(21, 34, 134) 60%,
rgb(29, 52, 145) 60%,
rgb(24, 47, 141) 80%,
rgb(25, 54, 140) 80%,
rgb(26, 58, 154) 100%
);
}
.radial-mono-pattern {
position: absolute;
top: 0;
left: 0;
width: 100%;
z-index: 0;
background-repeat: no-repeat;
background-size: 100%;
height: 100svh;
background-image: radial-gradient(
circle at top left,
rgb(195, 195, 195) 0%,
rgb(195, 195, 195) 2%,
rgb(202, 202, 202) 2%,
rgb(202, 202, 202) 23%,
rgb(209, 209, 209) 23%,
rgb(209, 209, 209) 55%,
rgb(217, 217, 217) 55%,
rgb(217, 217, 217) 80%,
rgb(224, 224, 224) 80%,
rgb(224, 224, 224) 86%,
rgb(231, 231, 231) 86%,
rgb(231, 231, 231) 96%,
rgb(238, 238, 238) 96%,
rgb(238, 238, 238) 100%
);
}
.dark .radial-mono-pattern {
position: absolute;
top: 0;
left: 0;
width: 100%;
z-index: 0;
background-repeat: no-repeat;
background-size: 100%;
height: 100svh;
background-image: radial-gradient(
circle at top left,
rgb(48, 48, 48) 0%,
rgb(48, 48, 48) 8%,
rgb(46, 46, 46) 8%,
rgb(46, 46, 46) 57%,
rgb(43, 43, 43) 57%,
rgb(43, 43, 43) 65%,
rgb(41, 41, 41) 65%,
rgb(41, 41, 41) 67%,
rgb(39, 39, 39) 67%,
rgb(39, 39, 39) 72%,
rgb(36, 36, 36) 72%,
rgb(36, 36, 36) 80%,
rgb(34, 34, 34) 80%,
rgb(34, 34, 34) 100%
);
}
.radial-midnight-pattern {
position: absolute;
top: 0;
left: 0;
width: 100%;
z-index: 0;
background-repeat: no-repeat;
background-size: 100%;
height: 100svh;
background-image:
radial-gradient(
circle at 11% 62%,
rgba(205, 205, 205, 0.04) 0%,
rgba(205, 205, 205, 0.04) 50%,
rgba(149, 149, 149, 0.04) 50%,
rgba(149, 149, 149, 0.04) 100%
),
radial-gradient(
circle at 78% 10%,
rgba(49, 49, 49, 0.04) 0%,
rgba(49, 49, 49, 0.04) 50%,
rgba(254, 254, 254, 0.04) 50%,
rgba(254, 254, 254, 0.04) 100%
),
radial-gradient(
circle at 92% 43%,
rgba(34, 34, 34, 0.04) 0%,
rgba(34, 34, 34, 0.04) 50%,
rgba(206, 206, 206, 0.04) 50%,
rgba(206, 206, 206, 0.04) 100%
),
linear-gradient(329deg, rgb(241, 77, 33), rgb(218, 227, 61));
}
.dark .radial-midnight-pattern {
position: absolute;
top: 0;
left: 0;
width: 100%;
z-index: 0;
background-repeat: no-repeat;
background-size: 100%;
height: 100svh;
background-image:
radial-gradient(
circle at 90% 24%,
rgba(209, 209, 209, 0.04) 0%,
rgba(209, 209, 209, 0.04) 50%,
rgba(160, 160, 160, 0.04) 50%,
rgba(160, 160, 160, 0.04) 100%
),
radial-gradient(
circle at 91% 63%,
rgba(45, 45, 45, 0.04) 0%,
rgba(45, 45, 45, 0.04) 50%,
rgba(87, 87, 87, 0.04) 50%,
rgba(87, 87, 87, 0.04) 100%
),
radial-gradient(
circle at 17% 2%,
rgba(124, 124, 124, 0.04) 0%,
rgba(124, 124, 124, 0.04) 50%,
rgba(117, 117, 117, 0.04) 50%,
rgba(117, 117, 117, 0.04) 100%
),
linear-gradient(88deg, rgb(33, 20, 105), rgb(1, 15, 13));
}
.circle-mono-pattern {
position: absolute;
top: 0;
left: 0;
width: 100%;
z-index: 0;
background-repeat: no-repeat;
background-size: 100%;
height: 100svh;
background-image: radial-gradient(
circle at center center,
rgb(252, 252, 252),
rgb(129, 129, 129)
);
}
.dark .circle-mono-pattern {
position: absolute;
top: 0;
left: 0;
width: 100%;
z-index: 0;
background-repeat: no-repeat;
background-size: 100%;
height: 100svh;
background-image: radial-gradient(circle at center center, rgb(71, 71, 71), rgb(8, 8, 8));
}
.carbon-fibre-pattern {
position: absolute;
top: 0;
left: 0;
width: 100%;
z-index: 0;
background-repeat: no-repeat;
background-size: 100%;
height: 100svh;
background-image:
radial-gradient(circle at center center, transparent, rgb(255, 255, 255)),
repeating-linear-gradient(
135deg,
rgb(255, 255, 255) 0px,
rgb(255, 255, 255) 12px,
transparent 12px,
transparent 19px,
rgb(255, 255, 255) 19px,
rgb(255, 255, 255) 24px,
transparent 24px,
transparent 28px
),
repeating-linear-gradient(
45deg,
rgb(245, 245, 245) 0px,
rgb(245, 245, 245) 7px,
transparent 7px,
transparent 14px
),
linear-gradient(90deg, rgb(255, 255, 255), rgb(255, 255, 255));
}
.dark .carbon-fibre-pattern {
position: absolute;
top: 0;
left: 0;
width: 100%;
z-index: 0;
background-repeat: no-repeat;
background-size: 100%;
height: 100svh;
background-image:
radial-gradient(circle at center center, rgba(33, 33, 33, 0), rgb(33, 33, 33)),
repeating-linear-gradient(
135deg,
rgb(33, 33, 33) 0px,
rgb(33, 33, 33) 1px,
transparent 1px,
transparent 4px
),
repeating-linear-gradient(
45deg,
rgb(56, 56, 56) 0px,
rgb(56, 56, 56) 5px,
transparent 5px,
transparent 6px
),
linear-gradient(90deg, rgb(33, 33, 33), rgb(33, 33, 33));
}
.texture-sky-pattern {
position: absolute;
top: 0;
left: 0;
width: 100%;
z-index: 0;
background-repeat: no-repeat;
background-size: 100%;
height: 100svh;
background-image:
repeating-linear-gradient(
45deg,
rgba(0, 0, 0, 0.04),
rgba(0, 0, 0, 0.08),
rgba(0, 0, 0, 0.05),
rgba(0, 0, 0, 0.04),
rgba(0, 0, 0, 0.01),
rgba(0, 0, 0, 0.1),
rgba(0, 0, 0, 0.09),
rgba(0, 0, 0, 0.1),
rgba(0, 0, 0, 0.03),
transparent,
rgba(0, 0, 0, 0.07),
rgba(0, 0, 0, 0.03),
rgba(0, 0, 0, 0.06) 4px
),
linear-gradient(90deg, rgb(193, 255, 253), rgb(59, 104, 247));
}
.dark .texture-sky-pattern {
position: absolute;
top: 0;
left: 0;
width: 100%;
z-index: 0;
background-repeat: no-repeat;
background-size: 100%;
height: 100svh;
background-image:
repeating-linear-gradient(
90deg,
transparent,
rgba(0, 0, 0, 0.1),
rgba(0, 0, 0, 0.08),
rgba(0, 0, 0, 0.01),
rgba(0, 0, 0, 0.1),
rgba(0, 0, 0, 0.05),
rgba(0, 0, 0, 0.08),
rgba(0, 0, 0, 0.1),
rgba(0, 0, 0, 0.04),
rgba(0, 0, 0, 0.02),
rgba(0, 0, 0, 0.02),
rgba(0, 0, 0, 0.09),
rgba(0, 0, 0, 0.04),
rgba(0, 0, 0, 0.06),
rgba(0, 0, 0, 0.01) 3px
),
linear-gradient(0deg, rgb(28, 117, 231), rgb(72, 64, 87));
}
.tiles-pattern {
position: absolute;
top: 0;
left: 0;
width: 100%;
z-index: 0;
background-repeat: no-repeat;
background-size: 100%;
height: 100svh;
background-image:
linear-gradient(45deg, rgb(255, 255, 255) 0%, transparent 36%),
repeating-linear-gradient(
135deg,
rgba(197, 197, 197, 0.1) 0px,
rgba(197, 197, 197, 0.1) 1px,
transparent 1px,
transparent 11px
),
repeating-linear-gradient(
45deg,
rgba(197, 197, 197, 0.1) 0px,
rgba(197, 197, 197, 0.1) 1px,
transparent 1px,
transparent 11px
),
linear-gradient(0deg, rgb(255, 255, 255), rgb(255, 255, 255));
}
.dark .tiles-pattern {
position: absolute;
top: 0;
left: 0;
width: 100%;
z-index: 0;
background-repeat: no-repeat;
background-size: 100%;
height: 100svh;
background-image:
linear-gradient(90deg, rgb(33, 33, 33) 0%, transparent 59%),
repeating-linear-gradient(
45deg,
rgba(168, 168, 168, 0.1) 0px,
rgba(168, 168, 168, 0.1) 1px,
transparent 1px,
transparent 13px
),
repeating-linear-gradient(
135deg,
rgba(168, 168, 168, 0.1) 0px,
rgba(168, 168, 168, 0.1) 1px,
transparent 1px,
transparent 13px
),
linear-gradient(90deg, rgb(33, 33, 33), rgb(33, 33, 33));
}
.angular-mono-pattern {
position: absolute;
top: 0;
left: 0;
width: 100%;
z-index: 0;
background-repeat: no-repeat;
background-size: 100%;
height: 100svh;
background-image:
linear-gradient(
67.5deg,
rgb(215, 215, 215) 0%,
rgb(215, 215, 215) 46%,
rgb(198, 198, 198) 46%,
rgb(198, 198, 198) 49%,
rgb(181, 181, 181) 49%,
rgb(181, 181, 181) 56%,
rgb(164, 164, 164) 56%,
rgb(164, 164, 164) 61%,
rgb(146, 146, 146) 61%,
rgb(146, 146, 146) 75%,
rgb(129, 129, 129) 75%,
rgb(129, 129, 129) 84%,
rgb(112, 112, 112) 84%,
rgb(112, 112, 112) 100%
),
linear-gradient(
22.5deg,
rgb(215, 215, 215) 0%,
rgb(215, 215, 215) 46%,
rgb(198, 198, 198) 46%,
rgb(198, 198, 198) 49%,
rgb(181, 181, 181) 49%,
rgb(181, 181, 181) 56%,
rgb(164, 164, 164) 56%,
rgb(164, 164, 164) 61%,
rgb(146, 146, 146) 61%,
rgb(146, 146, 146) 75%,
rgb(129, 129, 129) 75%,
rgb(129, 129, 129) 84%,
rgb(112, 112, 112) 84%,
rgb(112, 112, 112) 100%
),
linear-gradient(
112.5deg,
rgb(215, 215, 215) 0%,
rgb(215, 215, 215) 46%,
rgb(198, 198, 198) 46%,
rgb(198, 198, 198) 49%,
rgb(181, 181, 181) 49%,
rgb(181, 181, 181) 56%,
rgb(164, 164, 164) 56%,
rgb(164, 164, 164) 61%,
rgb(146, 146, 146) 61%,
rgb(146, 146, 146) 75%,
rgb(129, 129, 129) 75%,
rgb(129, 129, 129) 84%,
rgb(112, 112, 112) 84%,
rgb(112, 112, 112) 100%
),
linear-gradient(90deg, rgb(231, 231, 231), rgb(195, 195, 195));
background-blend-mode: overlay, overlay, overlay, normal;
}
.dark .angular-mono-pattern {
position: absolute;
top: 0;
left: 0;
width: 100%;
z-index: 0;
background-repeat: no-repeat;
background-size: 100%;
height: 100svh;
background-image:
linear-gradient(
45deg,
rgb(46, 46, 46) 0%,
rgb(46, 46, 46) 22%,
rgb(81, 81, 81) 22%,
rgb(81, 81, 81) 75%,
rgb(115, 115, 115) 75%,
rgb(115, 115, 115) 77%,
rgb(150, 150, 150) 77%,
rgb(150, 150, 150) 82%,
rgb(184, 184, 184) 82%,
rgb(184, 184, 184) 93%,
rgb(219, 219, 219) 93%,
rgb(219, 219, 219) 98%,
rgb(253, 253, 253) 98%,
rgb(253, 253, 253) 100%
),
linear-gradient(
135deg,
rgb(46, 46, 46) 0%,
rgb(46, 46, 46) 22%,
rgb(81, 81, 81) 22%,
rgb(81, 81, 81) 75%,
rgb(115, 115, 115) 75%,
rgb(115, 115, 115) 77%,
rgb(150, 150, 150) 77%,
rgb(150, 150, 150) 82%,
rgb(184, 184, 184) 82%,
rgb(184, 184, 184) 93%,
rgb(219, 219, 219) 93%,
rgb(219, 219, 219) 98%,
rgb(253, 253, 253) 98%,
rgb(253, 253, 253) 100%
),
linear-gradient(
22.5deg,
rgb(46, 46, 46) 0%,
rgb(46, 46, 46) 22%,
rgb(81, 81, 81) 22%,
rgb(81, 81, 81) 75%,
rgb(115, 115, 115) 75%,
rgb(115, 115, 115) 77%,
rgb(150, 150, 150) 77%,
rgb(150, 150, 150) 82%,
rgb(184, 184, 184) 82%,
rgb(184, 184, 184) 93%,
rgb(219, 219, 219) 93%,
rgb(219, 219, 219) 98%,
rgb(253, 253, 253) 98%,
rgb(253, 253, 253) 100%
),
linear-gradient(
157.5deg,
rgb(46, 46, 46) 0%,
rgb(46, 46, 46) 22%,
rgb(81, 81, 81) 22%,
rgb(81, 81, 81) 75%,
rgb(115, 115, 115) 75%,
rgb(115, 115, 115) 77%,
rgb(150, 150, 150) 77%,
rgb(150, 150, 150) 82%,
rgb(184, 184, 184) 82%,
rgb(184, 184, 184) 93%,
rgb(219, 219, 219) 93%,
rgb(219, 219, 219) 98%,
rgb(253, 253, 253) 98%,
rgb(253, 253, 253) 100%
),
linear-gradient(90deg, rgb(58, 58, 58), rgb(76, 76, 76));
background-blend-mode: overlay, overlay, overlay, overlay, normal;
}
.angular-spring-pattern {
position: absolute;
top: 0;
left: 0;
width: 100%;
z-index: 0;
background-repeat: no-repeat;
background-size: 100%;
height: 100svh;
background-image:
linear-gradient(90deg, rgb(188, 188, 188), rgb(172, 172, 172)),
linear-gradient(
45deg,
rgb(164, 46, 188) 0%,
rgb(164, 46, 188) 14%,
rgb(165, 82, 189) 14%,
rgb(165, 82, 189) 20%,
rgb(165, 117, 190) 20%,
rgb(165, 117, 190) 23%,
rgb(166, 153, 191) 23%,
rgb(166, 153, 191) 30%,
rgb(166, 188, 192) 30%,
rgb(166, 188, 192) 37%,
rgb(167, 224, 193) 37%,
rgb(167, 224, 193) 100%
),
linear-gradient(
135deg,
rgb(164, 46, 188) 0%,
rgb(164, 46, 188) 14%,
rgb(165, 82, 189) 14%,
rgb(165, 82, 189) 20%,
rgb(165, 117, 190) 20%,
rgb(165, 117, 190) 23%,
rgb(166, 153, 191) 23%,
rgb(166, 153, 191) 30%,
rgb(166, 188, 192) 30%,
rgb(166, 188, 192) 37%,
rgb(167, 224, 193) 37%,
rgb(167, 224, 193) 100%
);
background-blend-mode: overlay, overlay, normal;
}
.dark .angular-spring-pattern {
position: absolute;
top: 0;
left: 0;
width: 100%;
z-index: 0;
background-repeat: no-repeat;
background-size: 100%;
height: 100svh;
background-image:
linear-gradient(0deg, rgb(59, 59, 59), rgb(197, 197, 197)),
linear-gradient(
45deg,
rgb(159, 116, 214) 0%,
rgb(159, 116, 214) 25%,
rgb(130, 95, 173) 25%,
rgb(130, 95, 173) 45%,
rgb(101, 74, 132) 45%,
rgb(101, 74, 132) 50%,
rgb(72, 54, 90) 50%,
rgb(72, 54, 90) 52%,
rgb(43, 33, 49) 52%,
rgb(43, 33, 49) 61%,
rgb(14, 12, 8) 61%,
rgb(14, 12, 8) 100%
),
linear-gradient(
135deg,
rgb(159, 116, 214) 0%,
rgb(159, 116, 214) 25%,
rgb(130, 95, 173) 25%,
rgb(130, 95, 173) 45%,
rgb(101, 74, 132) 45%,
rgb(101, 74, 132) 50%,
rgb(72, 54, 90) 50%,
rgb(72, 54, 90) 52%,
rgb(43, 33, 49) 52%,
rgb(43, 33, 49) 61%,
rgb(14, 12, 8) 61%,
rgb(14, 12, 8) 100%
);
background-blend-mode: overlay, overlay, normal;
}
.angular-bloom-pattern {
position: absolute;
top: 0;
left: 0;
width: 100%;
z-index: 0;
background-repeat: no-repeat;
background-size: 100%;
height: 100svh;
background-image:
linear-gradient(
157.5deg,
rgb(95, 97, 179) 0%,
rgb(95, 97, 179) 16%,
rgb(115, 120, 184) 16%,
rgb(115, 120, 184) 20%,
rgb(135, 142, 188) 20%,
rgb(135, 142, 188) 32%,
rgb(155, 165, 193) 32%,
rgb(155, 165, 193) 38%,
rgb(175, 187, 198) 38%,
rgb(175, 187, 198) 48%,
rgb(195, 210, 203) 48%,
rgb(195, 210, 203) 51%,
rgb(215, 232, 207) 51%,
rgb(215, 232, 207) 59%,
rgb(235, 255, 212) 59%,
rgb(235, 255, 212) 100%
),
linear-gradient(
135deg,
rgb(95, 97, 179) 0%,
rgb(95, 97, 179) 16%,
rgb(115, 120, 184) 16%,
rgb(115, 120, 184) 20%,
rgb(135, 142, 188) 20%,
rgb(135, 142, 188) 32%,
rgb(155, 165, 193) 32%,
rgb(155, 165, 193) 38%,
rgb(175, 187, 198) 38%,
rgb(175, 187, 198) 48%,
rgb(195, 210, 203) 48%,
rgb(195, 210, 203) 51%,
rgb(215, 232, 207) 51%,
rgb(215, 232, 207) 59%,
rgb(235, 255, 212) 59%,
rgb(235, 255, 212) 100%
),
linear-gradient(
112.5deg,
rgb(95, 97, 179) 0%,
rgb(95, 97, 179) 16%,
rgb(115, 120, 184) 16%,
rgb(115, 120, 184) 20%,
rgb(135, 142, 188) 20%,
rgb(135, 142, 188) 32%,
rgb(155, 165, 193) 32%,
rgb(155, 165, 193) 38%,
rgb(175, 187, 198) 38%,
rgb(175, 187, 198) 48%,
rgb(195, 210, 203) 48%,
rgb(195, 210, 203) 51%,
rgb(215, 232, 207) 51%,
rgb(215, 232, 207) 59%,
rgb(235, 255, 212) 59%,
rgb(235, 255, 212) 100%
),
linear-gradient(90deg, rgb(236, 151, 145), rgb(201, 243, 43));
background-blend-mode: overlay, overlay, overlay, normal;
}
.dark .angular-bloom-pattern {
position: absolute;
top: 0;
left: 0;
width: 100%;
z-index: 0;
background-repeat: no-repeat;
background-size: 100%;
height: 100svh;
background-image:
linear-gradient(
90deg,
rgb(168, 86, 201) 0%,
rgb(168, 86, 201) 3%,
rgb(140, 74, 173) 3%,
rgb(140, 74, 173) 11%,
rgb(112, 61, 145) 11%,
rgb(112, 61, 145) 20%,
rgb(85, 49, 118) 20%,
rgb(85, 49, 118) 49%,
rgb(57, 37, 90) 49%,
rgb(57, 37, 90) 71%,
rgb(29, 24, 62) 71%,
rgb(29, 24, 62) 72%,
rgb(1, 12, 34) 72%,
rgb(1, 12, 34) 100%
),
linear-gradient(
67.5deg,
rgb(168, 86, 201) 0%,
rgb(168, 86, 201) 3%,
rgb(140, 74, 173) 3%,
rgb(140, 74, 173) 11%,
rgb(112, 61, 145) 11%,
rgb(112, 61, 145) 20%,
rgb(85, 49, 118) 20%,
rgb(85, 49, 118) 49%,
rgb(57, 37, 90) 49%,
rgb(57, 37, 90) 71%,
rgb(29, 24, 62) 71%,
rgb(29, 24, 62) 72%,
rgb(1, 12, 34) 72%,
rgb(1, 12, 34) 100%
),
linear-gradient(
112.5deg,
rgb(168, 86, 201) 0%,
rgb(168, 86, 201) 3%,
rgb(140, 74, 173) 3%,
rgb(140, 74, 173) 11%,
rgb(112, 61, 145) 11%,
rgb(112, 61, 145) 20%,
rgb(85, 49, 118) 20%,
rgb(85, 49, 118) 49%,
rgb(57, 37, 90) 49%,
rgb(57, 37, 90) 71%,
rgb(29, 24, 62) 71%,
rgb(29, 24, 62) 72%,
rgb(1, 12, 34) 72%,
rgb(1, 12, 34) 100%
),
linear-gradient(
45deg,
rgb(168, 86, 201) 0%,
rgb(168, 86, 201) 3%,
rgb(140, 74, 173) 3%,
rgb(140, 74, 173) 11%,
rgb(112, 61, 145) 11%,
rgb(112, 61, 145) 20%,
rgb(85, 49, 118) 20%,
rgb(85, 49, 118) 49%,
rgb(57, 37, 90) 49%,
rgb(57, 37, 90) 71%,
rgb(29, 24, 62) 71%,
rgb(29, 24, 62) 72%,
rgb(1, 12, 34) 72%,
rgb(1, 12, 34) 100%
),
linear-gradient(90deg, rgb(95, 3, 74), rgb(35, 65, 96));
background-blend-mode: overlay, overlay, overlay, overlay, normal;
}
.tiles-pattern::after,
.radial-mono-pattern::after,
.angular-mono-pattern::after,
.angular-spring-pattern::after,
.angular-bloom-pattern::after,
.texture-sky-pattern::after,
.carbon-fibre-pattern::after,
.circle-mono-pattern::after,
.radial-midnight-pattern::after,
.radial-blue-pattern::after {
content: "";
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-image: linear-gradient(
to bottom,
var(--background-kener-rgba) 0%,
var(--background-kener) 100%
);
}
.pets-pattern {
position: fixed;
left: -50px; /* Start offscreen */
width: 50px;
height: 50px;
z-index: 100;
background-repeat: no-repeat;
background-size: 100%;
animation: petWalking 45s linear infinite;
}
@keyframes petWalking {
0% {
left: -50px;
transform: scaleX(1); /* Dog facing right */
}
45% {
left: calc(100vw + 25px);
transform: scaleX(1); /* Still facing right */
}
50% {
left: calc(100vw + 50px);
transform: scaleX(-1); /* Flip to face left */
}
95% {
left: -25px;
transform: scaleX(-1); /* Still facing left */
}
100% {
left: -50px;
transform: scaleX(1); /* Flip back to face right */
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 74 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 6.3 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 6.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 103 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 56 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 5.6 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 9.5 KiB

+4 -1
View File
@@ -18,7 +18,10 @@ export default defineConfig(({ mode }) => ({
"codemirror",
"@codemirror/language-javascript",
"@uiw/codemirror-theme-github",
"@codemirror/language-json" /* ... */,
"@codemirror/language-json",
"@codemirror/language-markdown",
"@codemirror/language-css",
"@codemirror/language-html",
],
},
plugins: [