feat: support for postgres using knex

This commit is contained in:
Raj Nandan Sharma
2025-01-12 13:41:17 +05:30
parent bc9faf9456
commit 17dc752902
26 changed files with 1183 additions and 795 deletions
+3 -20
View File
@@ -5,24 +5,7 @@ let interval = 1000;
let waitTime = 0;
async function allFilesExist() {
let tablesCreated = (await db.checkTables()).map((table) => table.name);
let tablesRequired = [
"monitoring_data",
"monitor_alerts",
"site_data",
"monitors",
"triggers",
"users",
"api_keys"
];
for (let table of tablesRequired) {
let tableExists = tablesCreated.includes(table);
if (!tableExists) {
return false;
}
}
return true;
return await db.checkTables();
}
//use setTimeout to create a delay promise
@@ -37,13 +20,13 @@ let requiredFilesExist = false;
while (!requiredFilesExist && waitTime < maxWait) {
await delay(1000);
requiredFilesExist = await allFilesExist();
waitTime += interval;
}
if (!requiredFilesExist) {
console.error("Error loading site data");
process.exit(1);
} else {
console.log("✅ All files exist. Starting Frontend server...");
console.log("✅ All files exist. Starting server...");
process.exit(0);
}
})();
+26
View File
@@ -0,0 +1,26 @@
// @ts-nocheck
import dotenv from "dotenv";
dotenv.config();
const dbType = process.env.DATABASE_TYPE || "sqlite";
const knexOb = {
client: "better-sqlite3",
connection: {
filename: process.env.SQLITE_FILEPATH || "./database/kener.local7.db"
},
useNullAsDefault: true,
migrations: {
directory: "./migrations"
},
seeds: {
directory: "./seeds"
}
};
if (dbType === "postgres") {
knexOb.client = "pg";
knexOb.connection = process.env.POSTGRES_DATABASE_URL;
}
export default knexOb;
+135
View File
@@ -0,0 +1,135 @@
// migrations/YYYYMMDDHHMMSS_create_monitoring_tables.js
export function up(knex) {
return (
knex.schema
// Create monitoring_data table
.createTable("monitoring_data", (table) => {
table.text("monitor_tag").notNullable();
table.integer("timestamp").notNullable();
table.text("status");
table.float("latency");
table.text("type");
table.primary(["monitor_tag", "timestamp"]);
})
// Create monitor_alerts table
.createTable("monitor_alerts", (table) => {
table.increments("id").primary();
table.text("monitor_tag").notNullable();
table.text("monitor_status").notNullable();
table.text("alert_status").notNullable();
table.integer("health_checks").notNullable();
table.integer("incident_number").defaultTo(0);
table.timestamp("created_at").defaultTo(knex.fn.now());
table.timestamp("updated_at").defaultTo(knex.fn.now());
})
// Add index to monitor_alerts table
.raw(
"CREATE INDEX idx_monitor_tag_created_at ON monitor_alerts (monitor_tag, created_at)"
)
.createTable("site_data", (table) => {
table.increments("id").primary();
table.text("key").notNullable().unique();
table.text("value").notNullable();
table.text("data_type").notNullable();
table.timestamp("created_at").defaultTo(knex.fn.now());
table.timestamp("updated_at").defaultTo(knex.fn.now());
})
.createTable("monitors", (table) => {
table.increments("id").primary();
table.text("tag").notNullable().unique();
table.text("name").notNullable().unique();
table.text("description");
table.text("image");
table.text("cron");
table.text("default_status");
table.text("status");
table.text("category_name");
table.text("monitor_type");
table.text("down_trigger");
table.text("degraded_trigger");
table.text("type_data");
table.integer("day_degraded_minimum_count");
table.integer("day_down_minimum_count");
table.text("include_degraded_in_downtime").defaultTo("NO");
table.timestamp("created_at").defaultTo(knex.fn.now());
table.timestamp("updated_at").defaultTo(knex.fn.now());
})
.createTable("triggers", (table) => {
table.increments("id").primary();
table.text("name").notNullable().unique();
table.text("trigger_type");
table.text("trigger_desc");
table.text("trigger_status");
table.text("trigger_meta");
table.timestamp("created_at").defaultTo(knex.fn.now());
table.timestamp("updated_at").defaultTo(knex.fn.now());
})
.createTable("users", (table) => {
table.increments("id").primary();
table.text("email").notNullable().unique();
table.text("name").notNullable();
table.text("password_hash").notNullable();
table.integer("is_active").defaultTo(1);
table.integer("is_verified").defaultTo(0);
table.text("role").defaultTo("user");
table.timestamp("created_at").defaultTo(knex.fn.now());
table.timestamp("updated_at").defaultTo(knex.fn.now());
})
.createTable("api_keys", (table) => {
table.increments("id").primary();
table.text("name").notNullable().unique();
table.text("hashed_key").notNullable().unique();
table.text("masked_key").notNullable();
table.text("status").defaultTo("ACTIVE");
table.timestamp("created_at").defaultTo(knex.fn.now());
table.timestamp("updated_at").defaultTo(knex.fn.now());
})
.createTable("incidents", (table) => {
table.increments("id").primary();
table.text("title").notNullable();
table.integer("start_date_time").notNullable();
table.integer("end_date_time");
table.timestamp("created_at").defaultTo(knex.fn.now());
table.timestamp("updated_at").defaultTo(knex.fn.now());
table.text("status").defaultTo("ACTIVE");
table.text("state").defaultTo("INVESTIGATING");
})
.createTable("incident_monitors", (table) => {
table.increments("id").primary();
table.text("monitor_tag").notNullable();
table.text("monitor_impact");
table.timestamp("created_at").defaultTo(knex.fn.now());
table.timestamp("updated_at").defaultTo(knex.fn.now());
table.integer("incident_id").notNullable();
table.unique(["monitor_tag", "incident_id"]);
})
.createTable("incident_comments", (table) => {
table.increments("id").primary();
table.text("comment").notNullable();
table.integer("incident_id").notNullable();
table.integer("commented_at").notNullable();
table.timestamp("created_at").defaultTo(knex.fn.now());
table.timestamp("updated_at").defaultTo(knex.fn.now());
table.text("status").defaultTo("ACTIVE");
table.text("state").defaultTo("INVESTIGATING");
})
);
}
export function down(knex) {
return (
knex.schema
// Drop tables in reverse order
.dropTableIfExists("monitor_alerts")
.dropTableIfExists("monitoring_data")
.dropTableIfExists("site_data")
.dropTableIfExists("monitors")
.dropTableIfExists("triggers")
.dropTableIfExists("users")
.dropTableIfExists("api_keys")
.dropTableIfExists("incidents")
.dropTableIfExists("incident_monitors")
.dropTableIfExists("incident_comments")
);
}
+169 -3
View File
@@ -27,6 +27,7 @@
"fs-extra": "^11.1.1",
"js-yaml": "^4.1.0",
"jsonwebtoken": "^9.0.2",
"knex": "^3.1.0",
"lucide-svelte": "^0.292.0",
"marked": "^11.1.1",
"mode-watcher": "^0.4.1",
@@ -1872,6 +1873,12 @@
"dev": true,
"license": "MIT"
},
"node_modules/colorette": {
"version": "2.0.19",
"resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.19.tgz",
"integrity": "sha512-3tlv/dIP7FWvj3BsbHrGLJ6l/oKh1O3TcgBqMn+yyCagOxc23fyzDS6HypQbgxWbkpDnf52p1LuR4eWDQ/K9WQ==",
"license": "MIT"
},
"node_modules/combined-stream": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
@@ -2531,7 +2538,6 @@
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz",
"integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==",
"dev": true,
"engines": {
"node": ">=6"
}
@@ -2551,6 +2557,15 @@
"node": ">=0.8.0"
}
},
"node_modules/esm": {
"version": "3.2.25",
"resolved": "https://registry.npmjs.org/esm/-/esm-3.2.25.tgz",
"integrity": "sha512-U1suiZ2oDVWv4zPO56S0NcR5QriEahGtdN2OR6FiOG4WJvcjBVFB0qI4+eKoWFH483PKGuLuu6V8Z4T5g63UVA==",
"license": "MIT",
"engines": {
"node": ">=6"
}
},
"node_modules/esm-env": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/esm-env/-/esm-env-1.2.0.tgz",
@@ -2955,6 +2970,15 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/get-package-type": {
"version": "0.1.0",
"resolved": "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz",
"integrity": "sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==",
"license": "MIT",
"engines": {
"node": ">=8.0.0"
}
},
"node_modules/get-symbol-description": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.0.2.tgz",
@@ -2972,6 +2996,12 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/getopts": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/getopts/-/getopts-2.3.0.tgz",
"integrity": "sha512-5eDf9fuSXwxBL6q5HX+dhDj+dslFGWzU5thZ9kNKUkcPtaPdatmUFKwHFrLb/uf/WpA4BHET+AX3Scl56cAjpA==",
"license": "MIT"
},
"node_modules/github-from-package": {
"version": "0.0.0",
"resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz",
@@ -3273,6 +3303,15 @@
"node": ">= 0.4"
}
},
"node_modules/interpret": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/interpret/-/interpret-2.2.0.tgz",
"integrity": "sha512-Ju0Bz/cEia55xDwUWEa8+olFpCiQoypjnQySseKtmjNrnps3P+xfpUmGr90T7yjlVJmOtybRvPXhKMbHr+fWnw==",
"license": "MIT",
"engines": {
"node": ">= 0.10"
}
},
"node_modules/ipaddr.js": {
"version": "1.9.1",
"resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
@@ -3687,6 +3726,104 @@
"node": ">=6"
}
},
"node_modules/knex": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/knex/-/knex-3.1.0.tgz",
"integrity": "sha512-GLoII6hR0c4ti243gMs5/1Rb3B+AjwMOfjYm97pu0FOQa7JH56hgBxYf5WK2525ceSbBY1cjeZ9yk99GPMB6Kw==",
"license": "MIT",
"dependencies": {
"colorette": "2.0.19",
"commander": "^10.0.0",
"debug": "4.3.4",
"escalade": "^3.1.1",
"esm": "^3.2.25",
"get-package-type": "^0.1.0",
"getopts": "2.3.0",
"interpret": "^2.2.0",
"lodash": "^4.17.21",
"pg-connection-string": "2.6.2",
"rechoir": "^0.8.0",
"resolve-from": "^5.0.0",
"tarn": "^3.0.2",
"tildify": "2.0.0"
},
"bin": {
"knex": "bin/cli.js"
},
"engines": {
"node": ">=16"
},
"peerDependenciesMeta": {
"better-sqlite3": {
"optional": true
},
"mysql": {
"optional": true
},
"mysql2": {
"optional": true
},
"pg": {
"optional": true
},
"pg-native": {
"optional": true
},
"sqlite3": {
"optional": true
},
"tedious": {
"optional": true
}
}
},
"node_modules/knex/node_modules/commander": {
"version": "10.0.1",
"resolved": "https://registry.npmjs.org/commander/-/commander-10.0.1.tgz",
"integrity": "sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==",
"license": "MIT",
"engines": {
"node": ">=14"
}
},
"node_modules/knex/node_modules/debug": {
"version": "4.3.4",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz",
"integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==",
"license": "MIT",
"dependencies": {
"ms": "2.1.2"
},
"engines": {
"node": ">=6.0"
},
"peerDependenciesMeta": {
"supports-color": {
"optional": true
}
}
},
"node_modules/knex/node_modules/ms": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==",
"license": "MIT"
},
"node_modules/knex/node_modules/pg-connection-string": {
"version": "2.6.2",
"resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.6.2.tgz",
"integrity": "sha512-ch6OwaeaPYcova4kKZ15sbJ2hKb/VP48ZD2gE7i1J+L4MspCtBMAx8nMgz7bksc7IojCIIWuEhHibSMFH8m8oA==",
"license": "MIT"
},
"node_modules/knex/node_modules/resolve-from": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz",
"integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==",
"license": "MIT",
"engines": {
"node": ">=8"
}
},
"node_modules/lilconfig": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.0.0.tgz",
@@ -3732,8 +3869,7 @@
"node_modules/lodash": {
"version": "4.17.21",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
"dev": true
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="
},
"node_modules/lodash.castarray": {
"version": "4.4.0",
@@ -5258,6 +5394,18 @@
"node": ">=8.10.0"
}
},
"node_modules/rechoir": {
"version": "0.8.0",
"resolved": "https://registry.npmjs.org/rechoir/-/rechoir-0.8.0.tgz",
"integrity": "sha512-/vxpCXddiX8NGfGO/mTafwjq4aFa/71pvamip0++IQk3zG8cbCj0fifNPrjjF1XMXUne91jL9OoxmdykoEtifQ==",
"license": "MIT",
"dependencies": {
"resolve": "^1.20.0"
},
"engines": {
"node": ">= 10.13.0"
}
},
"node_modules/regenerator-runtime": {
"version": "0.14.0",
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.0.tgz",
@@ -6389,6 +6537,15 @@
"node": ">=10"
}
},
"node_modules/tarn": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/tarn/-/tarn-3.0.2.tgz",
"integrity": "sha512-51LAVKUSZSVfI05vjPESNc5vwqqZpbXCsU+/+wxlOrUjk2SnFTt97v9ZgQrD4YmxYW1Px6w2KjaDitCfkvgxMQ==",
"license": "MIT",
"engines": {
"node": ">=8.0.0"
}
},
"node_modules/thenify": {
"version": "3.3.1",
"resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz",
@@ -6408,6 +6565,15 @@
"node": ">=0.8"
}
},
"node_modules/tildify": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/tildify/-/tildify-2.0.0.tgz",
"integrity": "sha512-Cc+OraorugtXNfs50hU9KS369rFXCfgGLpfCfvlc+Ud5u6VWmUQsOAa9HbTvheQdYnrdJqqv1e5oIqXppMYnSw==",
"license": "MIT",
"engines": {
"node": ">=8"
}
},
"node_modules/tiny-glob": {
"version": "0.2.9",
"resolved": "https://registry.npmjs.org/tiny-glob/-/tiny-glob-0.2.9.tgz",
+4
View File
@@ -30,6 +30,9 @@
"build": "vite build",
"preview": "vite preview",
"configure": "node build.js",
"preseed": "npx knex migrate:latest",
"seed": "npx knex seed:run",
"predev": "npm run seed",
"devschedule": "node src/lib/server/startup.js",
"schedule": "node src/lib/server/startup.js",
"predevelopment": "node delay.js",
@@ -80,6 +83,7 @@
"fs-extra": "^11.1.1",
"js-yaml": "^4.1.0",
"jsonwebtoken": "^9.0.2",
"knex": "^3.1.0",
"lucide-svelte": "^0.292.0",
"marked": "^11.1.1",
"mode-watcher": "^0.4.1",
+32
View File
@@ -0,0 +1,32 @@
import monitorSeed from "../src/lib/server/db/seedMonitorData.js";
/**
* @param { import("knex").Knex } knex
* @returns { Promise<void> }
*/
export async function seed(knex) {
// Check if the table is empty
const count = await knex("monitors").count("id as CNT").first();
if (count.CNT == 0) {
// Deletes ALL existing entries
for (const monitor of monitorSeed) {
await knex("monitors").insert({
tag: monitor.tag,
name: monitor.name,
description: monitor.description,
image: monitor.image,
cron: monitor.cron,
default_status: monitor.default_status,
status: monitor.status,
category_name: monitor.category_name,
monitor_type: monitor.monitor_type,
type_data: monitor.type_data,
day_degraded_minimum_count: monitor.day_degraded_minimum_count,
day_down_minimum_count: monitor.day_down_minimum_count,
include_degraded_in_downtime: monitor.include_degraded_in_downtime,
created_at: knex.fn.now(),
updated_at: knex.fn.now()
});
}
}
}
+24
View File
@@ -0,0 +1,24 @@
import seedSiteData from "../src/lib/server/db/seedSiteData.js";
/**
* @param { import("knex").Knex } knex
* @returns { Promise<void> }
*/
export async function seed(knex) {
// Check if the table is empty
const count = await knex("site_data").count("id as CNT").first();
for (const key in seedSiteData) {
if (Object.prototype.hasOwnProperty.call(seedSiteData, key)) {
let value = seedSiteData[key];
let data_type = typeof value;
if (data_type === "object") {
value = JSON.stringify(value);
}
const existingEntry = await knex("site_data").where({ key: key }).first();
if (!existingEntry) {
await knex("site_data").insert([{ key: key, value: value, data_type: data_type }]);
}
}
}
}
+100
View File
@@ -727,3 +727,103 @@ textarea::placeholder {
),
linear-gradient(90deg, #ffffff, #ffffff);
}
/* CSS */
.button-77 {
align-items: center;
appearance: none;
background-clip: padding-box;
background-color: initial;
background-image: none;
border-style: none;
box-sizing: border-box;
cursor: pointer;
display: inline-block;
flex-direction: row;
flex-shrink: 0;
font-size: 16px;
font-weight: 600;
justify-content: center;
line-height: 24px;
margin: 0;
min-height: 64px;
outline: none;
overflow: visible;
padding: 19px 26px;
pointer-events: auto;
text-align: center;
text-decoration: none;
text-transform: none;
user-select: none;
-webkit-user-select: none;
touch-action: manipulation;
vertical-align: middle;
width: auto;
word-break: keep-all;
z-index: 0;
}
@media (min-width: 768px) {
.button-77 {
padding: 19px 32px;
}
}
.button-77:before,
.button-77:after {
border-radius: 80px;
}
.button-77:before {
background-color: rgba(249, 58, 19, 0.32);
content: "";
display: block;
height: 100%;
left: 0;
overflow: hidden;
position: absolute;
top: 0;
width: 100%;
z-index: -2;
}
.button-77:after {
background-color: initial;
background-image: linear-gradient(92.83deg, #ff7426 0, #f93a13 100%);
bottom: 4px;
content: "";
display: block;
left: 4px;
overflow: hidden;
position: absolute;
right: 4px;
top: 4px;
transition: all 100ms ease-out;
z-index: -1;
}
.button-77:hover:not(:disabled):after {
bottom: 0;
left: 0;
right: 0;
top: 0;
transition-timing-function: ease-in;
}
.button-77:active:not(:disabled) {
color: #ccc;
}
.button-77:active:not(:disabled):after {
background-image: linear-gradient(0deg, rgba(0, 0, 0, 0.2), rgba(0, 0, 0, 0.2)),
linear-gradient(92.83deg, #ff7426 0, #f93a13 100%);
bottom: 4px;
left: 4px;
right: 4px;
top: 4px;
}
.button-77:disabled {
cursor: default;
opacity: 0.24;
}
+11 -22
View File
@@ -148,14 +148,10 @@
<tbody class="divide-y divide-gray-200 dark:divide-neutral-700">
{#each alerts as alert}
<tr>
<td
class="whitespace-nowrap px-6 py-4 text-sm font-medium text-gray-800 dark:text-neutral-200"
>
<td class="whitespace-nowrap px-6 py-4 text-sm font-medium">
{alert.id}
</td>
<td
class="whitespace-nowrap px-6 py-4 text-xs font-semibold text-gray-800 dark:text-neutral-200"
>
<td class="whitespace-nowrap px-6 py-4 text-xs font-semibold">
{#if alert.monitor_status === "DOWN"}
<span class="text-red-500">
{alert.monitor_status}
@@ -166,14 +162,10 @@
</span>
{/if}
</td>
<td
class="whitespace-nowrap px-6 py-4 text-sm text-gray-800 dark:text-neutral-200"
>
<td class="whitespace-nowrap px-6 py-4 text-sm">
{alert.monitor_tag}
</td>
<td
class="whitespace-nowrap px-6 py-4 text-xs font-semibold text-gray-800 dark:text-neutral-200"
>
<td class="whitespace-nowrap px-6 py-4 text-xs font-semibold">
{#if alert.alert_status === "RESOLVED"}
<span class="text-blue-500">
{alert.alert_status}
@@ -184,14 +176,10 @@
</span>
{/if}
</td>
<td
class="whitespace-nowrap px-6 py-4 text-sm text-gray-800 dark:text-neutral-200"
>
<td class="whitespace-nowrap px-6 py-4 text-sm">
{alert.health_checks}
</td>
<td
class="whitespace-nowrap px-6 py-4 text-xs font-semibold text-gray-800 dark:text-neutral-200"
>
<td class="whitespace-nowrap px-6 py-4 text-xs font-semibold">
{#if !!alert.incident_number}
<a
target="_blank"
@@ -204,10 +192,11 @@
-
{/if}
</td>
<td
class="whitespace-nowrap px-6 py-4 text-sm text-gray-800 dark:text-neutral-200"
>
{moment(alert.created_at).format("YYYY-MM-DD HH:mm:ss")}
<td class="whitespace-nowrap px-6 py-4 text-sm">
{moment
.utc(alert.created_at, "YYYY-MM-DD HH:mm:ss")
.local()
.format("YYYY-MM-DD HH:mm:ss")}
</td>
</tr>
{/each}
+7 -2
View File
@@ -139,7 +139,9 @@
</Button>
</p>
<p class="text-xs text-muted-foreground">
Your new API key has been created. It will be not shown again, so make sure to save it.
Your new API key has been created. It will be <b class="uppercase underline"
>not be shown again</b
>, so make sure to save it.
</p>
</div>
{/if}
@@ -187,7 +189,10 @@
<td
class="whitespace-nowrap px-6 py-4 text-sm text-gray-800 dark:text-neutral-200"
>
{moment(apiKey.created_at).format("YYYY-MM-DD HH:mm:ss")}
{moment
.utc(apiKey.created_at, "YYYY-MM-DD HH:mm:ss")
.local()
.format("YYYY-MM-DD HH:mm:ss")}
</td>
<td
class="whitespace-nowrap px-6 py-4 text-xs font-semibold text-gray-800 dark:text-neutral-200"
+1 -1
View File
@@ -51,7 +51,7 @@
}).metaTags;
let analytics = siteDataExtractFromDb(data.siteData, {
analytics: "[]"
analytics: []
}).analytics;
//merge analyticsSupported with analytics
+2 -2
View File
@@ -17,7 +17,7 @@
let formState = "idle";
let themeData = {
pattern: "dots",
pattern: "none",
theme: "light",
themeToggle: "YES",
barStyle: "PARTIAL",
@@ -37,7 +37,7 @@
};
themeData = siteDataExtractFromDb(data.siteData, themeData);
themeData.colorsJ = data.siteData.colors;
if (data.siteData.colors) {
themeData.colorsJ = data.siteData.colors;
}
+63 -42
View File
@@ -4,7 +4,17 @@
import { Badge } from "$lib/components/ui/badge";
import { Button } from "$lib/components/ui/button";
import { base } from "$app/paths";
import { Share2, Link, CopyCheck, Code, TrendingUp, Percent, Loader } from "lucide-svelte";
import {
Share2,
Link,
CopyCheck,
Code,
TrendingUp,
Percent,
Loader,
ChevronLeft,
ChevronRight
} from "lucide-svelte";
import { buttonVariants } from "$lib/components/ui/button";
import { createEventDispatcher } from "svelte";
import { afterUpdate } from "svelte";
@@ -111,7 +121,7 @@
let rolledAt = 0;
let rollerLoading = false;
async function rollSummary(r) {
let newRolledAt = (rolledAt + 1) % uptimesRollers.length;
let newRolledAt = (rolledAt + r) % uptimesRollers.length;
if (uptimesRollers[newRolledAt].value === undefined) {
rollerLoading = true;
@@ -218,56 +228,67 @@
: 'overflow-hidden'} min-h-[94px] pt-2"
>
<div class="col-span-12">
<div class="grid grid-cols-12">
<div class="gap- flex justify-between">
<div
class="{monitor.embed === undefined
class=" {monitor.embed === undefined
? 'col-span-12'
: 'col-span-8'} md:col-span-8"
>
<Button
class="h-8 justify-start text-xs font-semibold transition-all"
variant="secondary"
disabled={rollerLoading}
style="transition: width 2s ease-in;"
on:click={() => {
scrollToRight();
rollSummary();
}}
>
<span class="text-left">
{uptimesRollers[rolledAt].text}
</span>
<div class="flex gap-x-1">
{#if rolledAt > 0}
<Button
variant="ghost"
class="h-5 w-5 p-0"
on:click={() => rollSummary(-1)}
>
<ChevronLeft class="h-4 w-4" />
</Button>
{/if}
<div class="flex text-xs font-semibold">
<span>
{uptimesRollers[rolledAt].text}
</span>
<span class="block w-full pl-2 text-right">
{#if rollerLoading}
<Loader class="mx-1 inline h-4 w-4 animate-spin" />
{:else}
<TrendingUp class="ml-1 mr-1 inline h-3 w-3" />
{/if}
{#if isNaN(uptimesRollers[rolledAt].value)}
<span class="text-muted-foreground">-</span>
{:else}
<NumberFlow
value={uptimesRollers[rolledAt].value}
format={{
notation: "standard",
minimumFractionDigits: 4,
maximumFractionDigits: 4
}}
suffix="%"
/>
{/if}
</span>
</Button>
<span class="">
{#if rollerLoading}
<Loader class="mx-1 -mt-0.5 inline h-4 w-4 animate-spin" />
{:else}
<TrendingUp class="mx-1 -mt-0.5 inline h-3 w-3" />
{/if}
{#if isNaN(uptimesRollers[rolledAt].value)}
<span class="text-muted-foreground">-</span>
{:else}
<NumberFlow
value={uptimesRollers[rolledAt].value}
format={{
notation: "standard",
minimumFractionDigits: 4,
maximumFractionDigits: 4
}}
suffix="%"
/>
{/if}
</span>
</div>
{#if rolledAt < uptimesRollers.length - 1}
<Button
variant="ghost"
class="h-5 w-5 p-0"
on:click={() => rollSummary(1)}
>
<ChevronRight class="h-4 w-4" />
</Button>
{/if}
</div>
</div>
<div
class="{monitor.embed === undefined
class="pt-0.5 {monitor.embed === undefined
? 'col-span-12'
: 'col-span-4'} text-right md:col-span-4"
: 'col-span-4'} text-right md:col-span-4"
>
<div
class="text-api-up mt-3 truncate text-xs font-semibold text-{monitor
.pageData.summaryColorClass}"
class="text-api-up truncate text-xs font-semibold text-{monitor.pageData
.summaryColorClass}"
title={monitor.pageData.summaryText}
>
{summaryTime(lang, monitor.pageData.summaryText)}
+3 -2
View File
@@ -9,7 +9,8 @@ import {
GetAllTriggers,
CreateIncident,
AddIncidentComment,
AddIncidentMonitor
AddIncidentMonitor,
InsertNewAlert
} from "./controllers/controller.js";
import db from "./db/db.js";
@@ -151,7 +152,7 @@ async function alerting(m) {
activeAlert = await db.getActiveAlert(monitor_tag, monitor_status, TRIGGERED);
}
if (isAffected && !alertExists) {
activeAlert = await db.insertAlert({
activeAlert = await InsertNewAlert({
monitor_tag: monitor_tag,
monitor_status: monitor_status,
alert_status: TRIGGERED,
+44 -1
View File
@@ -376,7 +376,6 @@ export const GetDataGroupByDayAlternative = async (
const offsetSeconds = offsetMinutes * 60;
const rawData = await db.getDataGroupByDayAlternative(monitor_tag, start, end);
const groupedData = rawData.reduce((acc, row) => {
// Calculate day group considering timezone offset
const dayGroup = Math.floor((row.timestamp + offsetSeconds) / 86400);
@@ -673,3 +672,47 @@ export const GetIncidentsByIDS = async (ids) => {
return incidents;
};
export const InsertNewAlert = async (data) => {
if (await db.alertExists(data.monitor_tag, data.monitor_status, data.alert_status)) {
return;
}
await db.insertAlert(data);
return await db.getActiveAlert(data.monitor_tag, data.monitor_status, data.alert_status);
};
export const IsLoggedInSession = async (cookies) => {
let tokenData = cookies.get("kener-user");
if (!!!tokenData) {
//redirect to signin page if user is not authenticated
//throw redirect(302, base + "/signin");
return {
error: "User not authenticated",
action: "redirect",
location: "/signin"
};
}
let tokenUser = await VerifyToken(tokenData);
if (!!!tokenUser) {
//redirect to signin page if user is not authenticated
// throw redirect(302, base + "/signin/logout");
return {
error: "User not authenticated",
action: "redirect",
location: "/signin/logout"
};
}
let userDB = await db.getUserByEmail(tokenUser.email);
if (!!!userDB) {
//redirect to signin page if user is not authenticated
// throw redirect(302, base + "/signin");
return {
error: "User not authenticated",
action: "redirect",
location: "/signin"
};
}
return {
user: userDB
};
};
+2 -31
View File
@@ -1,35 +1,6 @@
// @ts-nocheck
import Sqlite from "./sqlite.js";
import Postgres from "./postgres.js";
let instance = null;
let database = {
sqlite: {
dbName: "kener.local5.db"
}
};
const supportedDatabases = ["sqlite"];
const dbType = Object.keys(database)[0] || "sqlite";
const dbConfig = database[dbType];
if (!supportedDatabases.includes(dbType)) {
console.error(`Database type ${dbType} is not supported`);
process.exit(1);
}
if (dbType === "sqlite") {
if (dbConfig.dbName === undefined) {
console.error("dbName name is required for sqlite database");
process.exit(1);
}
instance = new Sqlite({
dbName: `./database/${dbConfig.dbName}`
});
}
//migration2(instance, "./database");
//create anonymous function to call the init function
import knexOb from "../../../../knexfile.js";
let instance = new Sqlite(knexOb);
export default instance;
+4 -9
View File
@@ -5,12 +5,6 @@ const seedSiteData = {
home: "/",
logo: "https://kener.ing/logo.png",
favicon: "https://kener.ing/logo96.png",
github: {
apiURL: "https://api.github.com",
owner: "rajnandan1",
repo: "kener",
incidentSince: 720
},
metaTags: [
{
key: "description",
@@ -45,7 +39,6 @@ const seedSiteData = {
}
],
nav: [
{ name: "Manage", url: "/manage", iconURL: "" },
{ name: "Documentation", url: "/docs/home", iconURL: "" },
{ name: "Github", iconURL: "", url: "https://github.com/rajnandan1/kener" },
{ name: "Buy me a coffee", iconURL: "", url: "https://buymeacoffee.com/rajnandan1" }
@@ -66,7 +59,7 @@ const seedSiteData = {
{ code: "vi", name: "Tiếng Việt", selected: true, disabled: false }
]
},
pattern: "squares",
pattern: "none",
analytics: [
{
id: "G-Q3MLRXCBFT",
@@ -101,7 +94,9 @@ const seedSiteData = {
cssSrc: "https://fonts.googleapis.com/css2?family=Albert+Sans:ital,wght@0,100..900;1,100..900&display=swap",
family: "Albert Sans"
},
categories: [{ name: "Home", description: "Monitors for Home Page" }]
categories: [{ name: "Home", description: "Monitors for Home Page" }],
homeIncidentCount: 5,
homeIncidentStartTimeWithin: 30
};
export default seedSiteData;
File diff suppressed because it is too large Load Diff
+1 -19
View File
@@ -2,8 +2,6 @@
import { json, redirect } from "@sveltejs/kit";
import { base } from "$app/paths";
import db from "$lib/server/db/db.js";
import seedSiteData from "$lib/server/db/seedSiteData.js";
import seedMonitorData from "$lib/server/db/seedMonitorData.js";
import { HashPassword, GenerateSalt } from "$lib/server/controllers/controller.js";
//function to validate a strong password
@@ -52,21 +50,5 @@ export async function POST({ request }) {
}
await db.insertUser(user);
for (const key in seedSiteData) {
if (Object.prototype.hasOwnProperty.call(seedSiteData, key)) {
let value = seedSiteData[key];
let data_type = typeof value;
if (data_type === "object") {
value = JSON.stringify(value);
}
await db.insertOrUpdateSiteData(key, value, data_type);
}
}
//loop through array seedMonitorData
for (const monitor of seedMonitorData) {
await db.insertMonitor(monitor);
}
throw redirect(302, base + "");
throw redirect(302, base + "/");
}
+9 -2
View File
@@ -2,7 +2,11 @@
import i18n from "$lib/i18n/server";
import { redirect } from "@sveltejs/kit";
import { base } from "$app/paths";
import { GetAllSiteData, IsSetupComplete } from "$lib/server/controllers/controller.js";
import {
GetAllSiteData,
IsSetupComplete,
IsLoggedInSession
} from "$lib/server/controllers/controller.js";
export async function load({ params, route, url, cookies, request }) {
let isSetupComplete = await IsSetupComplete();
@@ -14,6 +18,8 @@ export async function load({ params, route, url, cookies, request }) {
throw redirect(302, base + "/setup");
}
let isLoggedIn = await IsLoggedInSession(cookies);
let site = await GetAllSiteData();
const headers = request.headers;
const userAgent = headers.get("user-agent");
@@ -56,6 +62,7 @@ export async function load({ params, route, url, cookies, request }) {
lang: i18n(String(selectedLang)),
selectedLang: selectedLang,
embed,
bgc
bgc,
isLoggedIn: !!isLoggedIn.user
};
}
+5
View File
@@ -202,6 +202,11 @@
{/if}
</div>
{/if}
{#if data.isLoggedIn}
<a href="{base}/manage/site" class="button-77 fixed bottom-8 left-8" role="button">
Manage
</a>
{/if}
</main>
<style>
@@ -15,9 +15,9 @@ export async function POST({ request }) {
const start = payload.startTs;
let end = GetMinuteStartNowTimestampUTC();
let aggregatedData = await db.getAggregatedMonitoringData(monitor.tag, start, end);
let ups = Number(aggregatedData.UP);
let downs = Number(aggregatedData.DOWN);
let degradeds = Number(aggregatedData.DEGRADED);
let ups = Number(aggregatedData.UP || aggregatedData.up);
let downs = Number(aggregatedData.DOWN || aggregatedData.down);
let degradeds = Number(aggregatedData.DEGRADED || aggregatedData.degraded);
let total = ups + downs + degradeds;
let uptime = ParseUptime(ups + degradeds, total);
@@ -38,9 +38,9 @@ export async function GET({ params, url }) {
}
let dbData = await db.getAggregatedMonitoringData(tag, since, now);
dbData.UP = Number(dbData.UP);
dbData.DOWN = Number(dbData.DOWN);
dbData.DEGRADED = Number(dbData.DEGRADED);
dbData.UP = Number(dbData.UP || dbData.up);
dbData.DOWN = Number(dbData.DOWN || dbData.down);
dbData.DEGRADED = Number(dbData.DEGRADED || dbData.degraded);
let numerator = dbData.UP + dbData.DEGRADED;
let denominator = dbData.UP + dbData.DEGRADED + dbData.DOWN;
if (include_degraded_in_downtime === "YES") {
@@ -89,10 +89,7 @@
class="mx-auto mb-2 mt-4 flex w-full flex-1 flex-col items-start justify-center bg-transparent md:w-[655px]"
>
{#if sortedIncidentSmartDates.length == 0}
<div
class="mx-auto w-full rounded-md bg-clip-text p-12 text-center text-transparent"
style="background-image: linear-gradient( 135deg, #2AFADF 10%, #4C83FF 100%);"
>
<div class="mx-auto w-full rounded-md bg-clip-text p-12 text-center">
<div class="mx-auto mb-4 h-[32px] w-[32px] text-primary">
<picture>
<source
@@ -366,7 +366,7 @@
</script>
<div class="min-h-[70vh]">
<div class="mt-4 flex justify-between">
<div class="mt-4 flex justify-end">
<div class="flex w-40">
<Select.Root
portal={null}
@@ -375,7 +375,7 @@
fetchData();
}}
>
<Select.Trigger id="statusmonitor">
<Select.Trigger class="hidden" id="statusmonitor">
<Select.Value bind:value={status} placeholder={status} />
</Select.Trigger>
<Select.Content>
@@ -385,10 +385,10 @@
ALL
</Select.Item>
<Select.Item value="OPEN" label="OPEN" class="text-sm font-medium">
OPEN
ACTIVE
</Select.Item>
<Select.Item value="CLOSED" label="CLOSED" class="text-sm font-medium">
CLOSED
DELETED
</Select.Item>
</Select.Group>
</Select.Content>
@@ -8,7 +8,7 @@
<div class="min-h-[70vh]">
<MonitorsAdd
categories={data.siteData?.categories}
colorDown={data.siteData?.colors.DOWN}
colorDegraded={data.siteData?.colors.DEGRADED}
colorDown={data.siteData?.colors?.DOWN || "#ca3038"}
colorDegraded={data.siteData?.colors?.DEGRADED || "#e6ca61"}
/>
</div>