Compare commits

..

50 Commits

Author SHA1 Message Date
Raj Nandan Sharma e0cf568aa9 feat: implement custom handle function to modify response headers 2025-03-28 21:50:07 +05:30
Raj Nandan Sharma c12d7f8345 feat: add null check for document before dispatching analytics event 2025-03-28 21:18:26 +05:30
Raj Nandan Sharma 9ba115d5ab Merge pull request #366 from rajnandan1/release/3.2.11
release 3.2.11
2025-03-28 20:56:59 +05:30
Raj Nandan Sharma 3756bbfd06 chore: bump version to 3.2.11 in package.json 2025-03-28 20:54:50 +05:30
Raj Nandan Sharma 9096bcc641 feat: import analytics event module for enhanced tracking in incident page 2025-03-28 20:53:44 +05:30
Raj Nandan Sharma c4d04854e5 feat: enhance monitor deletion confirmation UI and add analytics event for incident back button click 2025-03-28 20:52:59 +05:30
Raj Nandan Sharma d95fc0005b feat: indicate Markdown support in incident summary and comment sections 2025-03-28 20:40:24 +05:30
Raj Nandan Sharma 2b9093184a feat: add support for Markdown language in events and enhance SEO documentation as requested in #359 2025-03-28 20:34:19 +05:30
Raj Nandan Sharma 831376054b feat: add analytics capture snippets and enhance event tracking also added plausible and ms clarity #355 2025-03-28 15:07:45 +05:30
Raj Nandan Sharma 885ee0b6d6 feat: implement monitor deletion functionality with confirmation 2025-03-26 11:47:53 +05:30
Raj Nandan Sharma 511d0f26dd feat: add email validation in CreateUpdateTrigger function 2025-03-26 10:16:36 +05:30
github-actions 5263698625 Auto-generate README.md with release versions 2025-03-25 17:45:05 +00:00
Raj Nandan Sharma 7859f9564e fix: bump version to 3.2.10 in package.json 2025-03-25 22:58:04 +05:30
github-actions 149456682d Auto-generate README.md with release versions 2025-03-25 17:20:55 +00:00
Raj Nandan Sharma 8c040997da fix: refactor monitorTriggers initialization into a separate function 2025-03-25 22:47:05 +05:30
Raj Nandan Sharma 868fed0176 fix: update tzdata version in Dockerfile to use latest available 2025-03-25 22:38:13 +05:30
Raj Nandan Sharma c7f44a48da fix: bump version to 3.2.9 in package.json 2025-03-25 22:31:57 +05:30
Raj Nandan Sharma 6300bff4e7 Merge pull request #363 from rajnandan1/fix/357
fix: update RBAC documentation and improve monitor data retrieval log…
2025-03-25 22:27:04 +05:30
Raj Nandan Sharma 3e6b1ffdbb fix: streamline query execution in DbImpl by removing redundant return statement 2025-03-25 22:24:10 +05:30
Raj Nandan Sharma 408fd5318b fix: update RBAC documentation and improve monitor data retrieval logic, fixes #357 and #361 2025-03-25 22:20:45 +05:30
Raj Nandan Sharma cfe419f6df Merge pull request #362 from rajnandan1/fix/360
Fix/360
2025-03-25 20:39:07 +05:30
Raj Nandan Sharma 5403726099 fix: replace parseInt with Number for timeout conversion in monitorSheet.svelte and ping.js 2025-03-25 20:38:23 +05:30
Raj Nandan Sharma 057c14909f fix #360 2025-03-25 20:29:30 +05:30
Raj Nandan Sharma 37b18c06bb fix: validate timeout input and ensure it's a number in monitorSheet.svelte 2025-03-25 20:28:39 +05:30
github-actions 3ad1eb9a03 Auto-generate README.md with release versions 2025-03-24 15:18:34 +00:00
Raj Nandan Sharma a1064771f8 fix: increment package version from 3.2.7 to 3.2.8 in package.json 2025-03-24 20:34:24 +05:30
Raj Nandan Sharma 463ee286bf fix: increment package version from 3.2.6 to 3.2.7 in package.json 2025-03-24 20:30:05 +05:30
Raj Nandan Sharma a7ae6e72ef fix: downgrade package version from 3.2.8 to 3.2.6 in package.json 2025-03-24 20:28:01 +05:30
github-actions 36c67f60dd Auto-generate README.md with release versions 2025-03-24 14:56:20 +00:00
Raj Nandan Sharma bb718b04a6 fix: update Dockerfile to use ARG variables for Node.js version tags 2025-03-24 20:10:13 +05:30
Raj Nandan Sharma b6ea060054 fix: update publish-images workflow and increment package version to 3.2.8 2025-03-24 20:01:53 +05:30
Raj Nandan Sharma 5dba54c048 fix: update publish-images workflow to set GH_TOKEN environment variable correctly 2025-03-24 19:53:29 +05:30
Raj Nandan Sharma 415276163f Merge pull request #356 from rajnandan1/release/3.2.8-x
fix: update Node.js version to 23 and increment package version to 3.2.7
2025-03-24 19:45:44 +05:30
Raj Nandan Sharma 017b9e2e84 fix: update Node.js version to 23 and increment package version to 3.2.7 2025-03-24 19:45:15 +05:30
Raj Nandan Sharma 5740991ee1 fix: update GitHub Actions workflow to use GH_PAT instead of GITHUB_TOKEN for authentication 2025-03-24 15:12:40 +05:30
Raj Nandan Sharma 06008bd719 fix: streamline Dockerfile by removing unnecessary paths and using variable references for base images 2025-03-24 12:11:03 +05:30
Raj Nandan Sharma b53cd485e5 Merge pull request #263 from kaffolder7/feature/dependabot-version-updates
feat: automate dependency updates 🤖
2025-03-24 11:59:41 +05:30
Raj Nandan Sharma 7c1fd39158 Merge branch 'main' into feature/dependabot-version-updates 2025-03-24 11:58:58 +05:30
Raj Nandan Sharma f7888f1ba4 fix: update package versions in Dockerfile for consistency and stability 2025-03-24 11:32:05 +05:30
Kyle Affolder 8c95c94472 update: combine to avoid multiple processes
Combined to reduce spawning multiple `rm` processes.
2025-02-17 13:29:34 -05:00
Kyle Affolder 7dafb2eddc add: GHA job to confirm if Dependabot PRs exist
Adds job to check if any Dependabot PRs are open and if so, fail the Docker build (since we need to ensure OS packages exist and are in their correct versions when using pinned versions for security purposes).
2025-02-17 13:24:22 -05:00
Kyle Affolder 7c9f3eb87f add: add back pinned tzdata version
Necessary for Dependabot to track
2025-02-17 13:22:15 -05:00
Kyle ab34dd81f8 Merge branch 'rajnandan1:main' into feature/dependabot-version-updates 2025-02-17 13:04:23 -05:00
Kyle Affolder a8be878a87 update: commented out node.js deps. monitoring
Not sure if we are wanting Dependabot to track Node.js packages, so for the time being, commenting this block out, but leaving for now w/ “TODO” to come back to at a later point.
2025-02-11 17:54:37 -05:00
Kyle Affolder 2b1849f1a3 update: grammar updates 2025-02-11 17:47:45 -05:00
Kyle Affolder 25a6590324 Merge branch 'feature/dependabot-version-updates' of https://github.com/kaffolder7/kener into feature/dependabot-version-updates 2025-02-11 17:31:48 -05:00
Kyle Affolder fa251dc07f update: README generation workflow 2025-02-11 17:31:33 -05:00
Kyle 4de93e7e95 Merge branch 'main' into feature/dependabot-version-updates 2025-02-11 17:25:28 -05:00
Kyle Affolder a5e5f33dc8 Merge branch 'main' into feature/dependabot-version-updates
# Conflicts:
#	.github/workflows/publish-images.yml
#	Dockerfile
2025-02-11 17:17:45 -05:00
Kyle Affolder 4060094404 add(dependabot): to automate dependency updates
Integrating Dependabot into the workflow ensures automatic dependency updates, improving security, reducing technical debt, and keeping packages up to date with minimal manual effort. This helps prevent vulnerabilities and maintain code stability over time.

Dependabot will automatically monitor the project’s dependencies and open pull requests (PRs) to update them when new versions are released. Here’s how it works:

1. Scans for Outdated Dependencies – It checks project dependency files (e.g., package.json, Dockerfile, .env.build, etc.) for outdated versions.
2. Fetches Latest Versions – When a newer version of a dependency is available, Dependabot retrieves it and updates the dependency files accordingly.
3. Opens a Pull Request – It then creates a PR with the updated dependency, detailing the changes and linking to release notes, changelogs, or security advisories.
4. Runs CI/CD Tests – If we end up setting up continuous integration (CI) tests, the PR will trigger the tests to check for breaking changes.
5. Security Updates – Dependabot also detects vulnerable dependencies and creates PRs to update them to a secure version.
6. Auto-Merging (Optional) – We might consider this at a later point, but if configured, it can automatically merge PRs when updates pass all tests and meet the requirements.
2025-02-09 22:08:06 -05:00
40 changed files with 1079 additions and 297 deletions
+47
View File
@@ -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"
+2 -2
View File
@@ -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
+1
View File
@@ -9,6 +9,7 @@ config/server.yaml
/src/lib/.kener
/package
.env
.vscode
.env.*
!.env.example
vite.config.js.timestamp-*
+19 -18
View File
@@ -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
+9 -9
View File
@@ -79,7 +79,7 @@ npm run dev
Official Docker images for **Kener** are available on [Docker Hub](https://hub.docker.com/r/rajnandan1/kener). Multiple versions are maintained to support different use cases.
<a href="https://hub.docker.com/r/rajnandan1/kener/tags?page=1&ordering=last_updated&name=3.2.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 &nbsp;<strong><em>(smallest image size)</em></strong></td>
</tr>
<tr>
<td><a href="https://hub.docker.com/r/rajnandan1/kener/tags?page=1&ordering=last_updated&name=alpine" target="_blank"><code>alpine</code></td>
<td>Latest stable release (aka 3.2.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
View File
@@ -119,7 +119,7 @@ Admins can manage user accounts through the user settings page:
## Role Limitations
- The Admin role can only be assigned during initial setup or by existing Admins
- The Admin role can only be assigned during initial setup
- Members cannot create or modify content
- Users cannot modify their own role (only an Admin can change roles)
+11 -1
View File
@@ -21,7 +21,17 @@ Add API key of your Amplitude account
### Mixpanel
Add token of your Mixpanel account.
Add project token of your Mixpanel account.
### Plausible
- Add domain of your Plausible account. Example: `example.com`
- Add API key of your Plausible account. Default is `https://plausible.io/api/event`
- Add Script Source of your Plausible account. Default is `https://plausible.io/js/script.pageview-props.tagged-events.js`
### Microsoft Clarity
Add Project ID of your Microsoft Clarity account.
## Search Engine Optimization
+80 -2
View File
@@ -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
View File
@@ -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",
+6
View File
@@ -0,0 +1,6 @@
/** @type {import('@sveltejs/kit').Handle} */
export async function handle({ event, resolve }) {
const response = await resolve(event);
response.headers.delete("Link");
return response;
}
+7
View File
@@ -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
View File
@@ -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,
},
}),
);
}
};
+11 -2
View File
@@ -2,6 +2,7 @@
import { formatDistanceToNow, formatDistance } from "date-fns";
import Settings from "lucide-svelte/icons/settings";
import ArrowRight from "lucide-svelte/icons/arrow-right";
import { analyticsEvent } from "$lib/boringOne";
import * as Accordion from "$lib/components/ui/accordion";
import { l, f, fd, fdn } from "$lib/i18n/client";
import { base } from "$app/paths";
@@ -9,6 +10,7 @@
import { Tooltip } from "bits-ui";
import GMI from "$lib/components/gmi.svelte";
import { page } from "$app/stores";
import { marked } from "marked";
export let incident;
export let index;
export let lang;
@@ -79,7 +81,10 @@
<div class="col-span-12">
<Accordion.Root bind:value={index} class="accor">
<Accordion.Item value={accordionValue}>
<Accordion.Trigger class="rounded-md px-4 hover:bg-muted hover:no-underline">
<Accordion.Trigger
class="rounded-md px-4 hover:bg-muted hover:no-underline"
on:click={() => analyticsEvent("incident_open", { incident_title: incident.title })}
>
<div class="w-full text-left hover:no-underline">
<p class="flex gap-x-2 text-xs font-semibold">
{#if incidentType == "INCIDENT"}
@@ -185,7 +190,11 @@
</time>
<div class="mb-4 text-sm font-normal">
{@html comment.comment}
<div
class="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}
+69 -1
View File
@@ -188,6 +188,12 @@
invalidFormMessage = `Host ${hosts[i].host} is not of type ${hosts[i].type}`;
return;
}
//validating timeout for ping
if (!!hosts[i].timeout && isNaN(hosts[i].timeout)) {
invalidFormMessage = "Timeout should be a number";
return;
}
hosts[i].timeout = Number(hosts[i].timeout);
//validating timeout
if (hosts[i].timeout < 1) {
invalidFormMessage = "Timeout should be greater than 0";
@@ -222,6 +228,12 @@
invalidFormMessage = `Host ${hosts[i].host} is not of type ${hosts[i].type}`;
return;
}
//validating timeout for tcp
if (!!hosts[i].timeout && isNaN(hosts[i].timeout)) {
invalidFormMessage = "Timeout should be a number";
return;
}
hosts[i].timeout = Number(hosts[i].timeout);
//validating timeout
if (hosts[i].timeout < 1) {
invalidFormMessage = "Timeout should be greater than 0";
@@ -419,6 +431,35 @@
pg: "PostgreSQL",
mysql2: "MySQL"
};
let deleteMonitorConfirmText = "";
let deletingMonitor = false;
async function deleteMonitor() {
if (deleteMonitorConfirmText != `delete ${newMonitor.tag}`) {
invalidFormMessage = "Please type the correct text to delete the monitor";
return;
}
deletingMonitor = true;
try {
let data = await fetch(base + "/manage/app/api/", {
method: "POST",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify({ action: "deleteMonitor", data: { tag: newMonitor.tag } })
});
let resp = await data.json();
if (resp.error) {
invalidFormMessage = resp.error;
} else {
dispatch("closeModal", {});
}
} catch (error) {
invalidFormMessage = "Error while deleting monitor";
} finally {
deletingMonitor = false;
}
}
</script>
<div class="fixed left-0 top-0 z-50 h-screen w-screen bg-card bg-opacity-20 backdrop-blur-sm">
@@ -943,7 +984,7 @@
];
}}
>
<Plus class="h-5 w-5" /> Add New
<Plus class="mr-1 h-5 w-5" /> Add New
</Button>
</div>
</div>
@@ -1264,6 +1305,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">
+29 -23
View File
@@ -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
+201 -74
View File
@@ -5,9 +5,11 @@
import { Label } from "$lib/components/ui/label";
import autoAnimate from "@formkit/auto-animate";
import { siteDataExtractFromDb, storeSiteData } from "$lib/clientTools.js";
import * as DropdownMenu from "$lib/components/ui/dropdown-menu";
import { base } from "$app/paths";
import Plus from "lucide-svelte/icons/plus";
import X from "lucide-svelte/icons/x";
import Check from "lucide-svelte/icons/check";
import Loader from "lucide-svelte/icons/loader";
import Info from "lucide-svelte/icons/info";
import { Tooltip } from "bits-ui";
@@ -24,26 +26,7 @@
}
];
}
let analyticsSupported = [
{
id: "",
type: "GA",
name: "Google Analytics",
script: "https://unpkg.com/@analytics/google-analytics@1.0.7/dist/@analytics/google-analytics.min.js"
},
{
id: "",
type: "AMPLITUDE",
name: "Amplitude",
script: "https://unpkg.com/@analytics/amplitude@0.1.3/dist/@analytics/amplitude.min.js"
},
{
id: "",
type: "MIXPANEL",
name: "MixPanel",
script: "https://unpkg.com/@analytics/mixpanel@0.4.0/dist/@analytics/mixpanel.min.js"
}
];
function removeRow(i) {
metaTags = metaTags.filter((_, index) => index !== i);
}
@@ -52,19 +35,6 @@
metaTags: JSON.stringify(metaTags)
}).metaTags;
let analytics = siteDataExtractFromDb(data.siteData, {
analytics: []
}).analytics;
//merge analyticsSupported with analytics
analytics = analyticsSupported.map((supported) => {
let found = analytics.find((a) => a.type === supported.type);
if (found) {
return found;
}
return supported;
});
let formState = "idle";
let formStateAnalytics = "idle";
@@ -87,17 +57,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}
+11 -1
View File
@@ -8,7 +8,7 @@
import { sub, startOfDay, getUnixTime } from "date-fns";
import GMI from "$lib/components/gmi.svelte";
import { page } from "$app/stores";
import { analyticsEvent } from "$lib/boringOne";
import Share2 from "lucide-svelte/icons/share-2";
import ArrowRight from "lucide-svelte/icons/arrow-right";
import Settings from "lucide-svelte/icons/settings";
@@ -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);
+4 -1
View File
@@ -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} />
+57 -2
View File
@@ -20,7 +20,7 @@ import { Resend } from "resend";
import crypto from "crypto";
import { format, subMonths, addMonths, startOfMonth } from "date-fns";
import { UP, DOWN, DEGRADED, NO_DATA, REALTIME, SIGNAL } from "../constants.js";
import { GetMinuteStartNowTimestampUTC, GetNowTimestampUTC, ReplaceAllOccurrences } from "../tool.js";
import { GetMinuteStartNowTimestampUTC, GetNowTimestampUTC, ReplaceAllOccurrences, ValidateEmail } from "../tool.js";
import getSMTPTransport from "../notification/smtps.js";
const saltRounds = 10;
@@ -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");
+33 -4
View File
@@ -138,9 +138,9 @@ class DbImpl {
return data;
}
async getLastStatusBeforeCombined(monitor_tags_arr, timestamp) {
async getLastStatusBeforeCombined(monitor_tags_arr, timestamp, minTimestamp) {
//monitor_tag_arr is an array of string
return await this.knex("monitoring_data")
let query = this.knex("monitoring_data")
.select(
"timestamp",
this.knex.raw("COUNT(*) as total_entries"),
@@ -153,8 +153,15 @@ class DbImpl {
END as status
`),
)
.whereIn("monitor_tag", monitor_tags_arr)
.where("timestamp", "=", timestamp)
.whereIn("monitor_tag", monitor_tags_arr);
if (!!minTimestamp) {
query = query.whereBetween("timestamp", [minTimestamp, timestamp]);
} else {
query = query.where("timestamp", "=", timestamp);
}
return await query
.groupBy("timestamp")
.havingRaw("COUNT(*) = ?", [monitor_tags_arr.length])
.orderBy("timestamp", "desc")
@@ -317,6 +324,11 @@ class DbImpl {
return await this.knex("site_data").where("key", key).first();
}
//get site data that starts with analytics.
async getAllSiteDataAnalytics() {
return await this.knex("site_data").where("key", "like", "analytics.%");
}
//insert into monitors
async insertMonitor(data) {
return await this.knex("monitors").insert({
@@ -911,6 +923,23 @@ class DbImpl {
.andWhere("invitation_expiry", ">", this.knex.fn.now())
.first();
}
//delete from monitor_data using tag
async deleteMonitorDataByTag(tag) {
return await this.knex("monitoring_data").where("monitor_tag", tag).del();
}
//delete from incident monitors using tag
async deleteIncidentMonitorsByTag(tag) {
return await this.knex("incident_monitors").where("monitor_tag", tag).del();
}
//delete from monitor alerts using tag
async deleteMonitorAlertsByTag(tag) {
return await this.knex("monitor_alerts").where("monitor_tag", tag).del();
}
//delete from monitors using tag
async deleteMonitorsByTag(tag) {
return await this.knex("monitors").where("tag", tag).del();
}
}
export default DbImpl;
+64 -63
View File
@@ -3,85 +3,86 @@ import net from "net"; // Use import instead of require
import ping from "ping";
const TCP = function (type, host, port, timeout) {
port = parseInt(port, 10);
return new Promise((resolve) => {
const socket = new net.Socket();
const start = process.hrtime.bigint(); // High-precision timestamp
let resolved = false;
timeout = Number(timeout);
port = parseInt(port, 10);
return new Promise((resolve) => {
const socket = new net.Socket();
const start = process.hrtime.bigint(); // High-precision timestamp
let resolved = false;
const onFinish = (status) => {
if (!resolved) {
resolved = true;
const end = process.hrtime.bigint();
const latency = Number(end - start) / 1e6; // Convert nanoseconds to milliseconds
socket.destroy();
resolve({ status, latency, host, port, type });
}
};
const onFinish = (status) => {
if (!resolved) {
resolved = true;
const end = process.hrtime.bigint();
const latency = Number(end - start) / 1e6; // Convert nanoseconds to milliseconds
socket.destroy();
resolve({ status, latency, host, port, type });
}
};
socket.setTimeout(timeout);
socket.once("connect", () => onFinish("open"));
socket.once("timeout", () => onFinish("timeout"));
socket.once("error", () => onFinish("error"));
socket.setTimeout(timeout);
socket.once("connect", () => onFinish("open"));
socket.once("timeout", () => onFinish("timeout"));
socket.once("error", () => onFinish("error"));
// Check if it's an IPv6 address (contains ':')
const options = {
host,
port,
family: type === "IP6" ? 6 : 4
}; //host.includes(":") ? { host, port, family: 6 } : { host, port };
socket.connect(options);
});
// Check if it's an IPv6 address (contains ':')
const options = {
host,
port,
family: type === "IP6" ? 6 : 4,
}; //host.includes(":") ? { host, port, family: 6 } : { host, port };
socket.connect(options);
});
};
const Ping = async function (type, host, timeout, count) {
let output = {
alive: false,
min: null,
max: null,
avg: null,
latencies: [],
latency: null,
host: host,
type: type
};
let output = {
alive: false,
min: null,
max: null,
avg: null,
latencies: [],
latency: null,
host: host,
type: type,
};
try {
let res = await ping.promise.probe(host, {
v6: type === "IP6",
timeout: type === "IP6" ? false : Math.floor(timeout / 1000),
min_reply: count
});
output.alive = res.alive;
output.min = res.min;
output.max = res.max;
output.avg = res.avg;
output.latencies = res.times;
output.latency = res.time;
} catch (error) {
console.log(`Error in pingCall IP4 for ${host}`, error);
}
return output;
try {
let res = await ping.promise.probe(host, {
v6: type === "IP6",
timeout: type === "IP6" ? false : Math.floor(timeout / 1000),
min_reply: count,
});
output.alive = res.alive;
output.min = res.min;
output.max = res.max;
output.avg = res.avg;
output.latencies = res.times;
output.latency = res.time;
} catch (error) {
console.log(`Error in pingCall IP4 for ${host}`, error);
}
return output;
};
/**
* @param {string} input
*/
function ExtractIPv6HostAndPort(input) {
const parts = input.split(":"); // Split by colons
const parts = input.split(":"); // Split by colons
// If there's a valid port at the end, extract it
const lastPart = parts[parts.length - 1];
const port = /^\d+$/.test(lastPart) ? parseInt(parts.pop(), 10) : null; // Check if last part is a number
// If there's a valid port at the end, extract it
const lastPart = parts[parts.length - 1];
const port = /^\d+$/.test(lastPart) ? parseInt(parts.pop(), 10) : null; // Check if last part is a number
// Reconstruct the IPv6 address
const host = parts.join(":");
// Reconstruct the IPv6 address
const host = parts.join(":");
// Ensure it's a valid IPv6 format
if (host.includes(":")) {
return { host, port }; // Port may be null if not present
}
return null; // Return null if the format is incorrect
// Ensure it's a valid IPv6 format
if (host.includes(":")) {
return { host, port }; // Port may be null if not present
}
return null; // Return null if the format is incorrect
}
export { TCP, ExtractIPv6HostAndPort, Ping };
+5
View File
@@ -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,
+8
View File
@@ -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,
};
+42
View File
@@ -0,0 +1,42 @@
/**
* ***************************************************
* * *
* * Amplitude Analytics Capture Snippet *
* * *
* ***************************************************
**/
// @ts-nocheck
(function () {
let loadJS = function (url, implementationCode, location) {
//url is URL of external file, implementationCode is the code
//to be called from the file, location is the location to
//insert the <script> element
let scriptTag = document.createElement("script");
scriptTag.src = url;
scriptTag.onload = implementationCode;
scriptTag.onreadystatechange = implementationCode;
location.appendChild(scriptTag);
};
let initJS = function () {
window.amplitude.add(window.sessionReplay.plugin({ sampleRate: 1 }));
window.amplitude.init("{{api_key}}", { fetchRemoteConfig: true, autocapture: true });
window.addEventListener("analyticsEvent", function (e) {
// Extract event name and data from the custom event
const eventName = e.detail.event;
const eventData = e.detail.data || {};
if (eventName) {
window.amplitude.track(eventName, eventData);
}
});
};
//on dom ready
document.addEventListener("DOMContentLoaded", function () {
loadJS("https://cdn.amplitude.com/script/{{api_key}}.js", initJS, document.getElementsByTagName("head")[0]);
});
})();
+20
View File
@@ -0,0 +1,20 @@
/**
* ***************************************************
* * *
* * Clarity Analytics Capture Snippet *
* * *
* ***************************************************
**/
// @ts-nocheck
(function (c, l, a, r, i, t, y) {
c[a] =
c[a] ||
function () {
(c[a].q = c[a].q || []).push(arguments);
};
t = l.createElement(r);
t.async = 1;
t.src = "https://www.clarity.ms/tag/" + i;
y = l.getElementsByTagName(r)[0];
y.parentNode.insertBefore(t, y);
})(window, document, "clarity", "script", "{{project_id}}");
@@ -0,0 +1,48 @@
/**
* ***************************************************
* * *
* * Google Analytics Capture Snippet *
* * *
* ***************************************************
**/
// @ts-nocheck
(function () {
let loadJS = function (url, implementationCode, location) {
//url is URL of external file, implementationCode is the code
//to be called from the file, location is the location to
//insert the <script> element
let scriptTag = document.createElement("script");
scriptTag.src = url;
scriptTag.onload = implementationCode;
scriptTag.onreadystatechange = implementationCode;
location.appendChild(scriptTag);
};
let initJS = function () {
window.dataLayer = window.dataLayer || [];
function gtag() {
window.dataLayer.push(arguments);
}
gtag("js", new Date());
gtag("config", "{{id}}");
window.addEventListener("analyticsEvent", function (e) {
// Extract event name and data from the custom event
const eventName = e.detail.event;
const eventData = e.detail.data || {};
// Send the event to Google Tag Manager
if (window.dataLayer && eventName) {
gtag("event", eventName, eventData);
}
});
};
//on dom ready
document.addEventListener("DOMContentLoaded", function () {
loadJS("https://www.googletagmanager.com/gtag/js?id={{id}}", initJS, document.getElementsByTagName("head")[0]);
});
})();
+78
View File
@@ -0,0 +1,78 @@
/**
* ***************************************************
* * *
* * Mixpanel Analytics Capture Snippet *
* * *
* ***************************************************
**/
// @ts-nocheck
(function (f, b) {
if (!b.__SV) {
var e, g, i, h;
window.mixpanel = b;
b._i = [];
b.init = function (e, f, c) {
function g(a, d) {
var b = d.split(".");
2 == b.length && ((a = a[b[0]]), (d = b[1]));
a[d] = function () {
a.push([d].concat(Array.prototype.slice.call(arguments, 0)));
};
}
var a = b;
"undefined" !== typeof c ? (a = b[c] = []) : (c = "mixpanel");
a.people = a.people || [];
a.toString = function (a) {
var d = "mixpanel";
"mixpanel" !== c && (d += "." + c);
a || (d += " (stub)");
return d;
};
a.people.toString = function () {
return a.toString(1) + ".people (stub)";
};
i =
"disable time_event track track_pageview track_links track_forms track_with_groups add_group set_group remove_group register register_once alias unregister identify name_tag set_config reset opt_in_tracking opt_out_tracking has_opted_in_tracking has_opted_out_tracking clear_opt_in_out_tracking start_batch_senders people.set people.set_once people.unset people.increment people.append people.union people.track_charge people.clear_charges people.delete_user people.remove".split(
" ",
);
for (h = 0; h < i.length; h++) g(a, i[h]);
var j = "set set_once union unset remove delete".split(" ");
a.get_group = function () {
function b(c) {
d[c] = function () {
call2_args = arguments;
call2 = [c].concat(Array.prototype.slice.call(call2_args, 0));
a.push([e, call2]);
};
}
for (var d = {}, e = ["get_group"].concat(Array.prototype.slice.call(arguments, 0)), c = 0; c < j.length; c++)
b(j[c]);
return d;
};
b._i.push([e, f, c]);
};
b.__SV = 1.2;
e = f.createElement("script");
e.type = "text/javascript";
e.async = !0;
e.src =
"undefined" !== typeof MIXPANEL_CUSTOM_LIB_URL
? MIXPANEL_CUSTOM_LIB_URL
: "file:" === f.location.protocol && "//cdn.mxpnl.com/libs/mixpanel-2-latest.min.js".match(/^\/\//)
? "https://cdn.mxpnl.com/libs/mixpanel-2-latest.min.js"
: "//cdn.mxpnl.com/libs/mixpanel-2-latest.min.js";
g = f.getElementsByTagName("script")[0];
g.parentNode.insertBefore(e, g);
}
})(document, window.mixpanel || []);
window.mixpanel.init("{{YOUR_PROJECT_TOKEN}}", { track_pageview: true, autocapture: true });
window.addEventListener("analyticsEvent", function (e) {
const eventName = e.detail.event;
const eventData = e.detail.data || {};
if (eventName) {
window.mixpanel.track(eventName, eventData);
}
});
+47
View File
@@ -0,0 +1,47 @@
/**
* ***************************************************
* * *
* * Plausible Analytics Capture Snippet *
* * *
* ***************************************************
**/
// @ts-nocheck
(function () {
let loadJS = function (url, implementationCode, location) {
//url is URL of external file, implementationCode is the code
//to be called from the file, location is the location to
//insert the <script> element
let scriptTag = document.createElement("script");
scriptTag.src = url;
scriptTag.defer = true;
scriptTag.dataset.domain = "{{domain}}";
scriptTag.dataset.api = "{{api}}";
scriptTag.onload = implementationCode;
scriptTag.onreadystatechange = implementationCode;
location.appendChild(scriptTag);
};
let initJS = function () {
window.plausible =
window.plausible ||
function () {
(window.plausible.q = window.plausible.q || []).push(arguments);
};
window.addEventListener("analyticsEvent", function (e) {
// Extract event name and data from the custom event
const eventName = e.detail.event;
const eventData = e.detail.data || {};
if (eventName) {
window.plausible(eventName, { props: eventData });
}
});
};
//on dom ready
document.addEventListener("DOMContentLoaded", function () {
loadJS("{{script_src}}", initJS, document.getElementsByTagName("head")[0]);
});
})();
+8 -66
View File
@@ -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}
+17 -3
View File
@@ -12,6 +12,7 @@
import { hotKeyAction, clickOutsideAction } from "svelte-legos";
import { onMount } from "svelte";
import ShareMenu from "$lib/components/shareMenu.svelte";
import { analyticsEvent } from "$lib/boringOne";
import { scale } from "svelte/transition";
import { format } from "date-fns";
import GMI from "$lib/components/gmi.svelte";
@@ -39,12 +40,22 @@
});
let kindFilter = "INCIDENT";
function kindOfIncidents(kind) {
analyticsEvent("monitor_incident_kind_filter", {
kind: kind
});
kindFilter = kind;
}
if (data.allRecentIncidents.length == 0) {
kindOfIncidents("MAINTENANCE");
}
function closeShareMenu() {
shareMenusToggle = false;
analyticsEvent("monitor_share_menu_close", {
tag: activeMonitor.tag
});
}
</script>
<svelte:head>
@@ -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

BIN
View File
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
View File
@@ -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: [