mirror of
https://github.com/rajnandan1/kener.git
synced 2026-08-07 15:27:13 +00:00
Compare commits
50 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| e0cf568aa9 | |||
| c12d7f8345 | |||
| 9ba115d5ab | |||
| 3756bbfd06 | |||
| 9096bcc641 | |||
| c4d04854e5 | |||
| d95fc0005b | |||
| 2b9093184a | |||
| 831376054b | |||
| 885ee0b6d6 | |||
| 511d0f26dd | |||
| 5263698625 | |||
| 7859f9564e | |||
| 149456682d | |||
| 8c040997da | |||
| 868fed0176 | |||
| c7f44a48da | |||
| 6300bff4e7 | |||
| 3e6b1ffdbb | |||
| 408fd5318b | |||
| cfe419f6df | |||
| 5403726099 | |||
| 057c14909f | |||
| 37b18c06bb | |||
| 3ad1eb9a03 | |||
| a1064771f8 | |||
| 463ee286bf | |||
| a7ae6e72ef | |||
| 36c67f60dd | |||
| bb718b04a6 | |||
| b6ea060054 | |||
| 5dba54c048 | |||
| 415276163f | |||
| 017b9e2e84 | |||
| 5740991ee1 | |||
| 06008bd719 | |||
| b53cd485e5 | |||
| 7c1fd39158 | |||
| f7888f1ba4 | |||
| 8c95c94472 | |||
| 7dafb2eddc | |||
| 7c9f3eb87f | |||
| ab34dd81f8 | |||
| a8be878a87 | |||
| 2b1849f1a3 | |||
| 25a6590324 | |||
| fa251dc07f | |||
| 4de93e7e95 | |||
| a5e5f33dc8 | |||
| 4060094404 |
@@ -0,0 +1,47 @@
|
||||
version: 2
|
||||
updates:
|
||||
# Track base image versions via .env.build
|
||||
- package-ecosystem: "docker"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
file-patterns:
|
||||
- ".env.build"
|
||||
- "node:*" # Ensures Node.js images are correctly detected
|
||||
|
||||
# Monitor OS package versions in Dockerfile (Debian/Alpine)
|
||||
- package-ecosystem: "gitsubmodule" # Alternative method to track OS packages in Dockerfile
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
labels:
|
||||
- "dependencies"
|
||||
- "os-packages"
|
||||
commit-message:
|
||||
prefix: "os"
|
||||
include: "scope"
|
||||
|
||||
# Monitor Node.js dependencies from package.json
|
||||
# TODO: Uncomment below if we want to begin letting Dependabot monitor & open PRs for Node.js project dependencies
|
||||
# - package-ecosystem: "npm"
|
||||
# directory: "/"
|
||||
# schedule:
|
||||
# interval: "weekly"
|
||||
# labels:
|
||||
# - "dependencies"
|
||||
# - "npm"
|
||||
# commit-message:
|
||||
# prefix: "npm"
|
||||
# include: "scope"
|
||||
|
||||
# Monitor GitHub Actions dependencies
|
||||
- package-ecosystem: "github-actions"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
labels:
|
||||
- "dependencies"
|
||||
- "github-actions"
|
||||
commit-message:
|
||||
prefix: "actions"
|
||||
include: "scope"
|
||||
@@ -27,7 +27,7 @@ jobs:
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '20' # Adjust as needed
|
||||
node-version: '23' # Adjust as needed
|
||||
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
@@ -153,4 +153,4 @@ jobs:
|
||||
echo "Setting BUILD_VERSION to $VERSION"
|
||||
gh variable set BUILD_VERSION --body "$VERSION"
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GH_PAT }} # Needs to be PAT w/ Read access to metadata and secrets & Read and Write access to actions, actions variables, and code
|
||||
GH_TOKEN: ${{ secrets.GH_PAT }} # Needs to be PAT w/ Read access to metadata and secrets & Read and Write access to actions, actions variables, and code
|
||||
@@ -9,6 +9,7 @@ config/server.yaml
|
||||
/src/lib/.kener
|
||||
/package
|
||||
.env
|
||||
.vscode
|
||||
.env.*
|
||||
!.env.example
|
||||
vite.config.js.timestamp-*
|
||||
|
||||
+19
-18
@@ -1,15 +1,15 @@
|
||||
# syntax=docker/dockerfile:1
|
||||
|
||||
# Global build arguments
|
||||
ARG ALPINE_VERSION=23.7.0-alpine3.21
|
||||
ARG DEBIAN_VERSION=23.7.0-bookworm-slim
|
||||
# Global build arguments (defined default values in case `.env.build` isn't loaded)
|
||||
ARG ALPINE_VERSION_TAG=23.7.0-alpine3.21
|
||||
ARG DEBIAN_VERSION_TAG=23.7.0-bookworm-slim
|
||||
ARG VARIANT=debian
|
||||
|
||||
#==========================================================#
|
||||
# STAGE 1: BUILD STAGE #
|
||||
#==========================================================#
|
||||
|
||||
FROM node:${DEBIAN_VERSION} AS builder-debian
|
||||
FROM node:${DEBIAN_VERSION_TAG} AS builder-debian
|
||||
RUN apt-get update && apt-get install -y \
|
||||
build-essential=12.9 \
|
||||
python3=3.11.2-1+b1 \
|
||||
@@ -18,11 +18,11 @@ RUN apt-get update && apt-get install -y \
|
||||
make=4.3-4.1 \
|
||||
node-gyp=9.3.0-2 \
|
||||
g++=4:12.2.0-3 \
|
||||
tzdata=2024b-0+deb12u1 \
|
||||
tzdata \
|
||||
iputils-ping=3:20221126-1+deb12u1 && \
|
||||
rm -rf /var/lib/apt/lists/*
|
||||
|
||||
FROM node:${ALPINE_VERSION} AS builder-alpine
|
||||
FROM node:${ALPINE_VERSION_TAG} AS builder-alpine
|
||||
RUN apk add --no-cache --update \
|
||||
build-base=0.5-r3 \
|
||||
python3=3.12.9-r0 \
|
||||
@@ -58,10 +58,9 @@ COPY . .
|
||||
# TODO: Reevaluate permissions (possibly reduce?)...
|
||||
# Remove docs directory and ensure required directories exist
|
||||
RUN rm -rf src/routes/\(docs\) \
|
||||
static/documentation \
|
||||
static/fonts/lato/full && \
|
||||
mkdir -p uploads database && \
|
||||
# TODO: Consider changing below to `chmod -R u-rwX,g=rX,o= uploads database`
|
||||
static/documentation \
|
||||
static/fonts/lato/full && \
|
||||
mkdir -p uploads database && \
|
||||
chmod -R 750 uploads database
|
||||
|
||||
# Build the application and remove `devDependencies`
|
||||
@@ -72,19 +71,21 @@ RUN npm run build && \
|
||||
# STAGE 2: PRODUCTION/FINAL STAGE #
|
||||
#==========================================================#
|
||||
|
||||
FROM node:${DEBIAN_VERSION} AS final-debian
|
||||
FROM node:${DEBIAN_VERSION_TAG} AS final-debian
|
||||
# TODO: Consider adding `--no-install-recommends`, but will need testing (may further help reduce final build size)
|
||||
RUN apt-get update && apt-get install -y \
|
||||
iputils-ping \
|
||||
sqlite3 \
|
||||
iputils-ping=3:20221126-1+deb12u1 \
|
||||
sqlite3=3.40.1-2+deb12u1 \
|
||||
tzdata \
|
||||
curl && \
|
||||
# TODO: Is it ok to change to `curl` here so that we don't have to maintain `wget` version mismatch between Debian architectures? (`curl` is only used for the container healthcheck and because there is an Alpine variant (best!) we probably don't care if the Debian image ends up building bigger due to `curl`.)
|
||||
curl && \
|
||||
rm -rf /var/lib/apt/lists/*
|
||||
|
||||
FROM node:${ALPINE_VERSION} AS final-alpine
|
||||
FROM node:${ALPINE_VERSION_TAG} AS final-alpine
|
||||
RUN apk add --no-cache --update \
|
||||
iputils \
|
||||
sqlite \
|
||||
tzdata
|
||||
iputils=20240905-r0 \
|
||||
sqlite=3.48.0-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.4"><img src="https://img.shields.io/badge/Latest_Stable_Release-3.2.4-blue" alt="Kener latest stable version: 3.2.4" /></a>
|
||||
<a href="https://hub.docker.com/r/rajnandan1/kener/tags?page=1&ordering=last_updated&name=3.2.10"><img src="https://img.shields.io/badge/Latest_Stable_Release-3.2.10-blue" alt="Kener latest stable version: 3.2.10" /></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.4)</td>
|
||||
<td>Latest stable release (aka 3.2.10)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><a href="https://hub.docker.com/r/rajnandan1/kener/tags?page=1&ordering=last_updated&name=3.2.4" target="_blank"><code>3.2.4</code></a></td>
|
||||
<td><a href="https://hub.docker.com/r/rajnandan1/kener/tags?page=1&ordering=last_updated&name=3.2.10" target="_blank"><code>3.2.10</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.4) release within that minor version (3.2.x)</td>
|
||||
<td>Major-minor version tag pointing to the latest patch (3.2.10) 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.4) release within that major version (3.x.x)</td>
|
||||
<td>Major version tag pointing to the latest stable (3.2.10) 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.4)</td>
|
||||
<td>Latest stable release (aka 3.2.10)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><a href="https://hub.docker.com/r/rajnandan1/kener/tags?page=1&ordering=last_updated&name=3.2.4-alpine" target="_blank"><code>3.2.4-alpine</code></a></td>
|
||||
<td><a href="https://hub.docker.com/r/rajnandan1/kener/tags?page=1&ordering=last_updated&name=3.2.10-alpine" target="_blank"><code>3.2.10-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.4) release within that minor version (3.2.x)</td>
|
||||
<td>Major-minor version tag pointing to the latest patch (3.2.10) 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.4) release within that major version (3.x.x)</td>
|
||||
<td>Major version tag pointing to the latest stable (3.2.10) release within that major version (3.x.x)</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
|
||||
+1
-1
@@ -119,7 +119,7 @@ Admins can manage user accounts through the user settings page:
|
||||
|
||||
## Role Limitations
|
||||
|
||||
- The Admin role can only be assigned during initial setup or by existing Admins
|
||||
- The Admin role can only be assigned during initial setup
|
||||
- Members cannot create or modify content
|
||||
- Users cannot modify their own role (only an Admin can change roles)
|
||||
|
||||
|
||||
+11
-1
@@ -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
|
||||
|
||||
|
||||
Generated
+80
-2
@@ -1,17 +1,18 @@
|
||||
{
|
||||
"name": "kener",
|
||||
"version": "3.2.5",
|
||||
"version": "3.2.10",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "kener",
|
||||
"version": "3.2.5",
|
||||
"version": "3.2.10",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.26.10",
|
||||
"@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 +219,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 +274,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 +898,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 +918,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 +960,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",
|
||||
|
||||
+2
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "kener",
|
||||
"version": "3.2.6",
|
||||
"version": "3.2.11",
|
||||
"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.",
|
||||
@@ -70,6 +70,7 @@
|
||||
"@babel/runtime": "^7.26.10",
|
||||
"@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",
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -939,3 +939,10 @@ 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;
|
||||
}
|
||||
|
||||
+13
-10
@@ -1,13 +1,16 @@
|
||||
// @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
|
||||
event = "kener:" + event;
|
||||
if (typeof document !== "undefined") {
|
||||
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="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}
|
||||
|
||||
@@ -188,6 +188,12 @@
|
||||
invalidFormMessage = `Host ${hosts[i].host} is not of type ${hosts[i].type}`;
|
||||
return;
|
||||
}
|
||||
//validating timeout for ping
|
||||
if (!!hosts[i].timeout && isNaN(hosts[i].timeout)) {
|
||||
invalidFormMessage = "Timeout should be a number";
|
||||
return;
|
||||
}
|
||||
hosts[i].timeout = Number(hosts[i].timeout);
|
||||
//validating timeout
|
||||
if (hosts[i].timeout < 1) {
|
||||
invalidFormMessage = "Timeout should be greater than 0";
|
||||
@@ -222,6 +228,12 @@
|
||||
invalidFormMessage = `Host ${hosts[i].host} is not of type ${hosts[i].type}`;
|
||||
return;
|
||||
}
|
||||
//validating timeout for tcp
|
||||
if (!!hosts[i].timeout && isNaN(hosts[i].timeout)) {
|
||||
invalidFormMessage = "Timeout should be a number";
|
||||
return;
|
||||
}
|
||||
hosts[i].timeout = Number(hosts[i].timeout);
|
||||
//validating timeout
|
||||
if (hosts[i].timeout < 1) {
|
||||
invalidFormMessage = "Timeout should be greater than 0";
|
||||
@@ -419,6 +431,35 @@
|
||||
pg: "PostgreSQL",
|
||||
mysql2: "MySQL"
|
||||
};
|
||||
let deleteMonitorConfirmText = "";
|
||||
let deletingMonitor = false;
|
||||
|
||||
async function deleteMonitor() {
|
||||
if (deleteMonitorConfirmText != `delete ${newMonitor.tag}`) {
|
||||
invalidFormMessage = "Please type the correct text to delete the monitor";
|
||||
return;
|
||||
}
|
||||
deletingMonitor = true;
|
||||
try {
|
||||
let data = await fetch(base + "/manage/app/api/", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json"
|
||||
},
|
||||
body: JSON.stringify({ action: "deleteMonitor", data: { tag: newMonitor.tag } })
|
||||
});
|
||||
let resp = await data.json();
|
||||
if (resp.error) {
|
||||
invalidFormMessage = resp.error;
|
||||
} else {
|
||||
dispatch("closeModal", {});
|
||||
}
|
||||
} catch (error) {
|
||||
invalidFormMessage = "Error while deleting monitor";
|
||||
} finally {
|
||||
deletingMonitor = false;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="fixed left-0 top-0 z-50 h-screen w-screen bg-card bg-opacity-20 backdrop-blur-sm">
|
||||
@@ -943,7 +984,7 @@
|
||||
];
|
||||
}}
|
||||
>
|
||||
<Plus class="h-5 w-5" /> Add New
|
||||
<Plus class="mr-1 h-5 w-5" /> Add New
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1264,6 +1305,33 @@
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
<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>
|
||||
</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">
|
||||
|
||||
@@ -229,28 +229,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 +291,7 @@
|
||||
}
|
||||
let currentAlertMonitor;
|
||||
function openAlertMenu(m) {
|
||||
initMonitorTriggers();
|
||||
currentAlertMonitor = m;
|
||||
if (m.down_trigger) {
|
||||
monitorTriggers.down_trigger = m.down_trigger;
|
||||
@@ -503,7 +509,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,135 @@
|
||||
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: ""
|
||||
}
|
||||
]
|
||||
}
|
||||
];
|
||||
|
||||
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 +194,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 +329,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}
|
||||
|
||||
@@ -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";
|
||||
@@ -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);
|
||||
|
||||
@@ -32,7 +32,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} />
|
||||
|
||||
@@ -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;
|
||||
@@ -169,6 +169,31 @@ 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",
|
||||
},
|
||||
];
|
||||
|
||||
export function InsertKeyValue(key, value) {
|
||||
@@ -203,6 +228,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 +281,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 +431,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 +1224,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;
|
||||
|
||||
+64
-63
@@ -3,85 +3,86 @@ import net from "net"; // Use import instead of require
|
||||
import ping from "ping";
|
||||
|
||||
const TCP = function (type, host, port, timeout) {
|
||||
port = parseInt(port, 10);
|
||||
return new Promise((resolve) => {
|
||||
const socket = new net.Socket();
|
||||
const start = process.hrtime.bigint(); // High-precision timestamp
|
||||
let resolved = false;
|
||||
timeout = Number(timeout);
|
||||
port = parseInt(port, 10);
|
||||
return new Promise((resolve) => {
|
||||
const socket = new net.Socket();
|
||||
const start = process.hrtime.bigint(); // High-precision timestamp
|
||||
let resolved = false;
|
||||
|
||||
const onFinish = (status) => {
|
||||
if (!resolved) {
|
||||
resolved = true;
|
||||
const end = process.hrtime.bigint();
|
||||
const latency = Number(end - start) / 1e6; // Convert nanoseconds to milliseconds
|
||||
socket.destroy();
|
||||
resolve({ status, latency, host, port, type });
|
||||
}
|
||||
};
|
||||
const onFinish = (status) => {
|
||||
if (!resolved) {
|
||||
resolved = true;
|
||||
const end = process.hrtime.bigint();
|
||||
const latency = Number(end - start) / 1e6; // Convert nanoseconds to milliseconds
|
||||
socket.destroy();
|
||||
resolve({ status, latency, host, port, type });
|
||||
}
|
||||
};
|
||||
|
||||
socket.setTimeout(timeout);
|
||||
socket.once("connect", () => onFinish("open"));
|
||||
socket.once("timeout", () => onFinish("timeout"));
|
||||
socket.once("error", () => onFinish("error"));
|
||||
socket.setTimeout(timeout);
|
||||
socket.once("connect", () => onFinish("open"));
|
||||
socket.once("timeout", () => onFinish("timeout"));
|
||||
socket.once("error", () => onFinish("error"));
|
||||
|
||||
// Check if it's an IPv6 address (contains ':')
|
||||
const options = {
|
||||
host,
|
||||
port,
|
||||
family: type === "IP6" ? 6 : 4
|
||||
}; //host.includes(":") ? { host, port, family: 6 } : { host, port };
|
||||
socket.connect(options);
|
||||
});
|
||||
// Check if it's an IPv6 address (contains ':')
|
||||
const options = {
|
||||
host,
|
||||
port,
|
||||
family: type === "IP6" ? 6 : 4,
|
||||
}; //host.includes(":") ? { host, port, family: 6 } : { host, port };
|
||||
socket.connect(options);
|
||||
});
|
||||
};
|
||||
|
||||
const Ping = async function (type, host, timeout, count) {
|
||||
let output = {
|
||||
alive: false,
|
||||
min: null,
|
||||
max: null,
|
||||
avg: null,
|
||||
latencies: [],
|
||||
latency: null,
|
||||
host: host,
|
||||
type: type
|
||||
};
|
||||
let output = {
|
||||
alive: false,
|
||||
min: null,
|
||||
max: null,
|
||||
avg: null,
|
||||
latencies: [],
|
||||
latency: null,
|
||||
host: host,
|
||||
type: type,
|
||||
};
|
||||
|
||||
try {
|
||||
let res = await ping.promise.probe(host, {
|
||||
v6: type === "IP6",
|
||||
timeout: type === "IP6" ? false : Math.floor(timeout / 1000),
|
||||
min_reply: count
|
||||
});
|
||||
output.alive = res.alive;
|
||||
output.min = res.min;
|
||||
output.max = res.max;
|
||||
output.avg = res.avg;
|
||||
output.latencies = res.times;
|
||||
output.latency = res.time;
|
||||
} catch (error) {
|
||||
console.log(`Error in pingCall IP4 for ${host}`, error);
|
||||
}
|
||||
return output;
|
||||
try {
|
||||
let res = await ping.promise.probe(host, {
|
||||
v6: type === "IP6",
|
||||
timeout: type === "IP6" ? false : Math.floor(timeout / 1000),
|
||||
min_reply: count,
|
||||
});
|
||||
output.alive = res.alive;
|
||||
output.min = res.min;
|
||||
output.max = res.max;
|
||||
output.avg = res.avg;
|
||||
output.latencies = res.times;
|
||||
output.latency = res.time;
|
||||
} catch (error) {
|
||||
console.log(`Error in pingCall IP4 for ${host}`, error);
|
||||
}
|
||||
return output;
|
||||
};
|
||||
|
||||
/**
|
||||
* @param {string} input
|
||||
*/
|
||||
function ExtractIPv6HostAndPort(input) {
|
||||
const parts = input.split(":"); // Split by colons
|
||||
const parts = input.split(":"); // Split by colons
|
||||
|
||||
// If there's a valid port at the end, extract it
|
||||
const lastPart = parts[parts.length - 1];
|
||||
const port = /^\d+$/.test(lastPart) ? parseInt(parts.pop(), 10) : null; // Check if last part is a number
|
||||
// If there's a valid port at the end, extract it
|
||||
const lastPart = parts[parts.length - 1];
|
||||
const port = /^\d+$/.test(lastPart) ? parseInt(parts.pop(), 10) : null; // Check if last part is a number
|
||||
|
||||
// Reconstruct the IPv6 address
|
||||
const host = parts.join(":");
|
||||
// Reconstruct the IPv6 address
|
||||
const host = parts.join(":");
|
||||
|
||||
// Ensure it's a valid IPv6 format
|
||||
if (host.includes(":")) {
|
||||
return { host, port }; // Port may be null if not present
|
||||
}
|
||||
return null; // Return null if the format is incorrect
|
||||
// Ensure it's a valid IPv6 format
|
||||
if (host.includes(":")) {
|
||||
return { host, port }; // Port may be null if not present
|
||||
}
|
||||
return null; // Return null if the format is incorrect
|
||||
}
|
||||
|
||||
export { TCP, ExtractIPv6HostAndPort, Ping };
|
||||
|
||||
@@ -6,11 +6,16 @@ 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) {
|
||||
return data;
|
||||
}
|
||||
return {
|
||||
status: DOWN,
|
||||
latency: 0,
|
||||
|
||||
@@ -293,6 +293,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 +329,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]);
|
||||
});
|
||||
})();
|
||||
@@ -22,8 +22,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 +59,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 +76,18 @@
|
||||
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}`;
|
||||
</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 +95,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}
|
||||
@@ -222,7 +162,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>
|
||||
@@ -94,10 +105,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}/`);
|
||||
}
|
||||
}}
|
||||
@@ -256,21 +270,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,60 @@
|
||||
// @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 { 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);
|
||||
}
|
||||
|
||||
return new Response(captureScript, {
|
||||
headers: {
|
||||
"Content-Type": "application/javascript",
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -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}/`);
|
||||
}}
|
||||
>
|
||||
|
||||
@@ -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=" 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"}
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 74 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 37 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 6.3 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 103 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 32 KiB |
+2
-1
@@ -18,7 +18,8 @@ export default defineConfig(({ mode }) => ({
|
||||
"codemirror",
|
||||
"@codemirror/language-javascript",
|
||||
"@uiw/codemirror-theme-github",
|
||||
"@codemirror/language-json" /* ... */,
|
||||
"@codemirror/language-json",
|
||||
"@codemirror/language-markdown",
|
||||
],
|
||||
},
|
||||
plugins: [
|
||||
|
||||
Reference in New Issue
Block a user