Compare commits
33 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 6c7534b9b6 | |||
| ae6c2c985f | |||
| 940331b87f | |||
| e6b5600a47 | |||
| ce2a6ab756 | |||
| 6af91af639 | |||
| e1ff156b91 | |||
| b56d20ac35 | |||
| 9ee7a7b861 | |||
| f73ffce3f0 | |||
| 0997967787 | |||
| 3476ec2b29 | |||
| e0cf568aa9 | |||
| c12d7f8345 | |||
| 9ba115d5ab | |||
| 3756bbfd06 | |||
| 9096bcc641 | |||
| c4d04854e5 | |||
| d95fc0005b | |||
| 2b9093184a | |||
| 831376054b | |||
| 885ee0b6d6 | |||
| 511d0f26dd | |||
| 5263698625 | |||
| 7859f9564e | |||
| 149456682d | |||
| 8c040997da | |||
| 868fed0176 | |||
| c7f44a48da | |||
| 6300bff4e7 | |||
| 3e6b1ffdbb | |||
| 408fd5318b | |||
| cfe419f6df |
@@ -9,6 +9,7 @@ config/server.yaml
|
||||
/src/lib/.kener
|
||||
/package
|
||||
.env
|
||||
.vscode
|
||||
.env.*
|
||||
!.env.example
|
||||
vite.config.js.timestamp-*
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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.8"><img src="https://img.shields.io/badge/Latest_Stable_Release-3.2.8-blue" alt="Kener latest stable version: 3.2.8" /></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.8)</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.8" target="_blank"><code>3.2.8</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.8) 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.8) 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 <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.8)</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.8-alpine" target="_blank"><code>3.2.8-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.8) 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.8) 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>
|
||||
|
||||
|
||||
@@ -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 <style> 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;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -66,7 +66,7 @@ export const WebhookJSONTemplate = JSON.stringify(
|
||||
actions: [
|
||||
{
|
||||
text: "{{action_text}}",
|
||||
url: "{{action_url}}",
|
||||
url: "{{{action_url}}}",
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
}),
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -431,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">
|
||||
@@ -1276,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">
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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{color: red;}"
|
||||
></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}
|
||||
|
||||
@@ -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}"
|
||||
|
||||
@@ -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} />
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -197,6 +197,7 @@ const FetchData = async function (site, monitor, localTz, selectedLang, lang) {
|
||||
barRoundness: site.barRoundness,
|
||||
midnight90DaysAgo: midnight90DaysAgoTz,
|
||||
maxDateTodayTimestamp: maxDateTodayTimestampTz,
|
||||
startOfTheDay: midnightTz,
|
||||
};
|
||||
};
|
||||
export { FetchData };
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
@@ -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]);
|
||||
});
|
||||
})();
|
||||
@@ -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]);
|
||||
});
|
||||
})();
|
||||
@@ -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);
|
||||
}
|
||||
});
|
||||
@@ -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]);
|
||||
});
|
||||
})();
|
||||
@@ -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]);
|
||||
});
|
||||
})();
|
||||
@@ -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>
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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",
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
@@ -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 */
|
||||
}
|
||||
}
|
||||
|
After Width: | Height: | Size: 74 KiB |
|
After Width: | Height: | Size: 21 KiB |
|
After Width: | Height: | Size: 37 KiB |
|
After Width: | Height: | Size: 2.4 KiB |
|
After Width: | Height: | Size: 4.7 KiB |
|
After Width: | Height: | Size: 10 KiB |
|
After Width: | Height: | Size: 2.8 KiB |
|
After Width: | Height: | Size: 3.0 KiB |
|
After Width: | Height: | Size: 6.3 KiB |
|
After Width: | Height: | Size: 6.1 KiB |
|
After Width: | Height: | Size: 103 KiB |
|
After Width: | Height: | Size: 56 KiB |
|
After Width: | Height: | Size: 32 KiB |
|
After Width: | Height: | Size: 5.6 KiB |
|
After Width: | Height: | Size: 2.6 KiB |
|
After Width: | Height: | Size: 16 KiB |
|
After Width: | Height: | Size: 2.5 KiB |
|
After Width: | Height: | Size: 9.5 KiB |
@@ -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: [
|
||||
|
||||