From f04a930e137cf7314da4929c9a3d0eb0153d4b07 Mon Sep 17 00:00:00 2001 From: Raj Nandan Sharma Date: Mon, 5 May 2025 10:28:02 +0530 Subject: [PATCH] feat: add configurable home data range per device Introduces per-device configuration for maximum data range and selectable days on the homepage, allowing separate settings for desktop and mobile. Updates UI, server logic, and data model to support these options, and uses user agent detection to apply the correct configuration. Improves flexibility for data display across devices. Relates to #105 --- package-lock.json | 11 +- package.json | 1 + src/kener.css | 3 +- src/lib/components/manage/homePage.svelte | 203 ++++++++++++++++++ src/lib/components/monitor.svelte | 73 ++++--- src/lib/server/controllers/siteDataKeys.js | 5 + src/lib/server/db/seedSiteData.js | 10 + src/lib/server/page.js | 8 +- src/routes/(embed)/+layout.server.js | 6 +- .../embed/monitor-[tag]/+page.server.js | 59 ++--- src/routes/(kener)/+layout.server.js | 8 +- src/routes/(kener)/+page.server.js | 11 +- 12 files changed, 327 insertions(+), 71 deletions(-) diff --git a/package-lock.json b/package-lock.json index e450b06..6871cca 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "kener", - "version": "3.2.14", + "version": "3.2.15", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "kener", - "version": "3.2.14", + "version": "3.2.15", "license": "MIT", "dependencies": { "@babel/runtime": "^7.26.10", @@ -41,6 +41,7 @@ "knex": "^3.1.0", "lucide-svelte": "^0.483.0", "marked": "^11.1.1", + "mobile-detect": "^1.4.5", "mode-watcher": "^0.4.1", "moment": "^2.29.4", "moment-timezone": "^0.5.43", @@ -5604,6 +5605,12 @@ "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==", "license": "MIT" }, + "node_modules/mobile-detect": { + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/mobile-detect/-/mobile-detect-1.4.5.tgz", + "integrity": "sha512-yc0LhH6tItlvfLBugVUEtgawwFU2sIe+cSdmRJJCTMZ5GEJyLxNyC/NIOAOGk67Fa8GNpOttO3Xz/1bHpXFD/g==", + "license": "MIT" + }, "node_modules/mode-watcher": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/mode-watcher/-/mode-watcher-0.4.1.tgz", diff --git a/package.json b/package.json index aae3b23..748d466 100644 --- a/package.json +++ b/package.json @@ -99,6 +99,7 @@ "knex": "^3.1.0", "lucide-svelte": "^0.483.0", "marked": "^11.1.1", + "mobile-detect": "^1.4.5", "mode-watcher": "^0.4.1", "moment": "^2.29.4", "moment-timezone": "^0.5.43", diff --git a/src/kener.css b/src/kener.css index ebc5216..b4460b6 100644 --- a/src/kener.css +++ b/src/kener.css @@ -882,7 +882,8 @@ p code:not([class^="language-"]) { margin-bottom: 1.3em; } -input[type="checkbox"]:disabled + .peer { +input[type="checkbox"]:disabled + .peer, +label:has(input[type="checkbox"]:disabled) { cursor: not-allowed; opacity: 0.5; } diff --git a/src/lib/components/manage/homePage.svelte b/src/lib/components/manage/homePage.svelte index 4247216..ed98c9d 100644 --- a/src/lib/components/manage/homePage.svelte +++ b/src/lib/components/manage/homePage.svelte @@ -11,6 +11,8 @@ import * as DropdownMenu from "$lib/components/ui/dropdown-menu"; import Loader from "lucide-svelte/icons/loader"; + import LaptopMinimal from "lucide-svelte/icons/laptop-minimal"; + import SmartPhone from "lucide-svelte/icons/smartphone"; import X from "lucide-svelte/icons/x"; import Plus from "lucide-svelte/icons/plus"; import Info from "lucide-svelte/icons/info"; @@ -42,6 +44,18 @@ let homeIncidentStartTimeWithin = 30; let incidentGroupView = "EXPAND_FIRST"; let kenerTheme = "default"; + let homeDataMaxDays = { + desktop: { + maxDays: 90, + selectableDays: [1, 7, 14, 30, 60, 90] + }, + mobile: { + maxDays: 90, + selectableDays: [1, 7, 14, 30, 60, 90] + } + }; + + let selectableDaysList = [1, 7, 14, 30, 60, 90, 120, 150, 180]; let availableThemes = [ { name: "Default", value: "default" }, @@ -93,6 +107,9 @@ if (data.siteData.kenerTheme) { kenerTheme = data.siteData.kenerTheme; } + if (data.siteData.homeDataMaxDays) { + homeDataMaxDays = data.siteData.homeDataMaxDays; + } if (data.siteData.tzToggle) { tzToggle = data.siteData.tzToggle; } @@ -288,6 +305,23 @@ return; } } + + let dataErrorMessage = ""; + let formStateData = "idle"; + async function formSubmitData() { + dataErrorMessage = ""; + formStateData = "loading"; + let resp = await storeSiteData({ + homeDataMaxDays: JSON.stringify(homeDataMaxDays) + }); + //print data + let data = await resp.json(); + formStateData = "idle"; + if (data.error) { + dataErrorMessage = data.error; + return; + } + } @@ -380,6 +414,175 @@ + + + Data + + Configure the data shown on the homepage. This includes the time range for incidents. + + + +
+
+
+ +
+ + { + homeDataMaxDays.desktop.maxDays = e.value; + //uncheck all checkboxes greater than maxDays + homeDataMaxDays.desktop.selectableDays = homeDataMaxDays.desktop.selectableDays.filter( + (day) => day <= e.value + ); + //push all days less than maxDays + selectableDaysList.forEach((day) => { + if (day <= e.value && !homeDataMaxDays.desktop.selectableDays.includes(day)) { + homeDataMaxDays.desktop.selectableDays.push(day); + } + }); + }} + selected={{ + value: homeDataMaxDays.desktop.maxDays, + label: homeDataMaxDays.desktop.maxDays + " Days" + }} + > + + + + + + Max Days + {#each selectableDaysList as day} + + {day} Days + + {/each} + + + +
+
+ + +
+ {#each selectableDaysList as day} + + {/each} +
+
+
+ +
+ +
+ + { + homeDataMaxDays.mobile.maxDays = e.value; + //uncheck all checkboxes greater than maxDays + homeDataMaxDays.mobile.selectableDays = homeDataMaxDays.mobile.selectableDays.filter( + (day) => day <= e.value + ); + //push all days less than maxDays + selectableDaysList.forEach((day) => { + if (day <= e.value && !homeDataMaxDays.mobile.selectableDays.includes(day)) { + homeDataMaxDays.mobile.selectableDays.push(day); + } + }); + }} + selected={{ + value: homeDataMaxDays.mobile.maxDays, + label: homeDataMaxDays.mobile.maxDays + " Days" + }} + > + + + + + + Max Days + {#each selectableDaysList as day} + + {day} Days + + {/each} + + + +
+
+ + +
+ {#each selectableDaysList as day} + + {/each} +
+
+
+
+

+ + Select the maximum number of days to show data for. This will be used to filter the data shown on the + homepage. The dropdown will show all the days that are less than or equal to the selected max days. The max + value in the dropdown will be the entire value of data range it cannot be changed + +

+ {#if !!dataErrorMessage} +

{dataErrorMessage}

+ {/if} +
+ +
+
+
+
Incidents diff --git a/src/lib/components/monitor.svelte b/src/lib/components/monitor.svelte index a6bea04..140b8bc 100644 --- a/src/lib/components/monitor.svelte +++ b/src/lib/components/monitor.svelte @@ -39,6 +39,15 @@ let uptime90Day = monitor.pageData.uptime90Day; let incidents = {}; let dayIncidentsFull = []; + let homeDataMaxDays = monitor.pageData.homeDataMaxDays; + + let dimension = { + x1: 6, + x2: 4 + }; + + dimension.x1 = ($page.data.isMobile ? 346 : 546) / homeDataMaxDays.maxDays; + dimension.x2 = (4 / 6) * dimension.x1; function loadIncidents() { axios @@ -91,39 +100,37 @@ }); }, 1000 * 0.2); } - let uptimesRollers = [ - { - text: `${l(lang, "90 Days")}`, - startTs: monitor.pageData.startOfTheDay - 86400 * 89, - endTs: monitor.pageData.maxDateTodayTimestamp, - value: uptime90Day - }, - { - text: `${l(lang, "60 Days")}`, - startTs: monitor.pageData.startOfTheDay - 86400 * 59, - endTs: monitor.pageData.maxDateTodayTimestamp - }, - { - text: `${l(lang, "30 Days")}`, - startTs: monitor.pageData.startOfTheDay - 86400 * 29, - endTs: monitor.pageData.maxDateTodayTimestamp - }, - { - text: `${l(lang, "14 Days")}`, - startTs: monitor.pageData.startOfTheDay - 86400 * 13, - endTs: monitor.pageData.maxDateTodayTimestamp - }, - { - text: `${l(lang, "7 Days")}`, - startTs: monitor.pageData.startOfTheDay - 86400 * 6, - endTs: monitor.pageData.maxDateTodayTimestamp - }, - { - text: l(lang, "Today"), - startTs: monitor.pageData.startOfTheDay, - endTs: monitor.pageData.maxDateTodayTimestamp + + function returnUptimeRollers() { + let rollers = homeDataMaxDays.selectableDays; + //sort descending + rollers.sort((a, b) => b - a); + let ret = []; + for (let i = 0; i < rollers.length; i++) { + let roller = rollers[i]; + if (roller == 1) { + ret.push({ + text: `${l(lang, "Today")}`, + startTs: monitor.pageData.startOfTheDay, + endTs: monitor.pageData.maxDateTodayTimestamp + }); + } else { + ret.push({ + text: `${roller} ${l(lang, "Days")}`, + startTs: monitor.pageData.startOfTheDay - 86400 * (roller - 1), + endTs: monitor.pageData.maxDateTodayTimestamp + }); + } + //if last index + if (i == 0) { + ret[i].value = uptime90Day; + } } - ]; + + return ret; + } + + let uptimesRollers = returnUptimeRollers(); //start of the week moment let rolledAt = 0; @@ -316,12 +323,14 @@ href="#" class="oneline h-[34px] w-[6px] {bar.border ? 'opacity-100' : 'opacity-20'} pb-1" + style="width: {dimension.x1}px" >
{#if !!incidents[ts]}
typeof value === "string" && value.trim().length > 0, diff --git a/src/lib/server/db/seedSiteData.js b/src/lib/server/db/seedSiteData.js index 5ffbcb3..541a8c7 100644 --- a/src/lib/server/db/seedSiteData.js +++ b/src/lib/server/db/seedSiteData.js @@ -100,6 +100,16 @@ const seedSiteData = { categories: [{ name: "Home", description: "Monitors for Home Page", isHidden: false }], homeIncidentCount: 5, homeIncidentStartTimeWithin: 30, + homeDataMaxDays: { + desktop: { + maxDays: 90, + selectableDays: [1, 7, 14, 30, 60, 90], + }, + mobile: { + maxDays: 90, + selectableDays: [1, 7, 14, 30, 60, 90], + }, + }, kenerTheme: "default", showSiteStatus: "NO", }; diff --git a/src/lib/server/page.js b/src/lib/server/page.js index c2210f4..9ecf2fe 100644 --- a/src/lib/server/page.js +++ b/src/lib/server/page.js @@ -78,13 +78,16 @@ function getCountOfSimilarStatuesEnd(arr, statusType) { return count; } -const FetchData = async function (site, monitor, localTz, selectedLang, lang) { +const FetchData = async function (site, monitor, localTz, selectedLang, lang, isMobile) { const secondsInDay = 24 * 60 * 60; //get offset from utc in minutes const nowUTC = GetMinuteStartNowTimestampUTC(); + + let deviceType = isMobile ? "mobile" : "desktop"; + let homeDataMaxDays = site.homeDataMaxDays[deviceType]; const midnightUTC = GetDayStartTimestampUTC(nowUTC); const midnightTz = BeginningOfDay({ timeZone: localTz }); - const midnight90DaysAgoTz = midnightTz - 90 * 24 * 60 * 60; + const midnight90DaysAgoTz = midnightTz - homeDataMaxDays.maxDays * 24 * 60 * 60; const NO_DATA = "No Data"; let offsetInMinutes = parseInt((GetDayStartTimestampUTC(nowUTC) - midnightTz) / 60); const maxDateTodayTimestampTz = BeginningOfMinute({ timeZone: localTz }); @@ -198,6 +201,7 @@ const FetchData = async function (site, monitor, localTz, selectedLang, lang) { midnight90DaysAgo: midnight90DaysAgoTz, maxDateTodayTimestamp: maxDateTodayTimestampTz, startOfTheDay: midnightTz, + homeDataMaxDays, }; }; export { FetchData }; diff --git a/src/routes/(embed)/+layout.server.js b/src/routes/(embed)/+layout.server.js index fae077b..6e79e7b 100644 --- a/src/routes/(embed)/+layout.server.js +++ b/src/routes/(embed)/+layout.server.js @@ -2,9 +2,13 @@ import i18n from "$lib/i18n/server"; import { redirect } from "@sveltejs/kit"; import { base } from "$app/paths"; +import MobileDetect from "mobile-detect"; import { GetAllSiteData, IsSetupComplete, IsLoggedInSession } from "$lib/server/controllers/controller.js"; export async function load({ params, route, url, cookies, request }) { + const userAgent = request.headers.get("user-agent"); + const md = new MobileDetect(userAgent); + const isMobile = !!md.mobile(); let isSetupComplete = await IsSetupComplete(); if (!isSetupComplete) { throw redirect(302, base + "/manage/setup"); @@ -18,7 +22,6 @@ export async function load({ params, route, url, cookies, request }) { let site = await GetAllSiteData(); const headers = request.headers; - const userAgent = headers.get("user-agent"); let localTz = "UTC"; const localTzQuery = query.get("tz"); @@ -56,5 +59,6 @@ export async function load({ params, route, url, cookies, request }) { embed, bgc, isLoggedIn: !!isLoggedIn.user, + isMobile: isMobile, }; } diff --git a/src/routes/(embed)/embed/monitor-[tag]/+page.server.js b/src/routes/(embed)/embed/monitor-[tag]/+page.server.js index 0c184cb..8edddf9 100644 --- a/src/routes/(embed)/embed/monitor-[tag]/+page.server.js +++ b/src/routes/(embed)/embed/monitor-[tag]/+page.server.js @@ -3,33 +3,34 @@ import { FetchData } from "$lib/server/page"; import { GetMonitors } from "$lib/server/controllers/controller.js"; export async function load({ params, route, url, parent }) { - let monitors = await GetMonitors({ status: "ACTIVE" }); - const parentData = await parent(); - const siteData = parentData.site; - const monitorsActive = []; - const query = url.searchParams; - const theme = query.get("theme"); - for (let i = 0; i < monitors.length; i++) { - //only return monitors that have category as home or category is not present - if (monitors[i].tag !== params.tag) { - continue; - } - delete monitors[i].api; - delete monitors[i].default_status; - monitors[i].embed = true; - let data = await FetchData( - siteData, - monitors[i], - parentData.localTz, - parentData.selectedLang, - parentData.lang - ); - monitors[i].pageData = data; - monitorsActive.push(monitors[i]); - } - return { - monitors: monitorsActive, - theme, - openIncidents: [] - }; + let monitors = await GetMonitors({ status: "ACTIVE" }); + const parentData = await parent(); + const siteData = parentData.site; + const monitorsActive = []; + const query = url.searchParams; + const theme = query.get("theme"); + for (let i = 0; i < monitors.length; i++) { + //only return monitors that have category as home or category is not present + if (monitors[i].tag !== params.tag) { + continue; + } + delete monitors[i].api; + delete monitors[i].default_status; + monitors[i].embed = true; + let data = await FetchData( + siteData, + monitors[i], + parentData.localTz, + parentData.selectedLang, + parentData.lang, + parentData.isMobile, + ); + monitors[i].pageData = data; + monitorsActive.push(monitors[i]); + } + return { + monitors: monitorsActive, + theme, + openIncidents: [], + }; } diff --git a/src/routes/(kener)/+layout.server.js b/src/routes/(kener)/+layout.server.js index a36b773..7ff9766 100644 --- a/src/routes/(kener)/+layout.server.js +++ b/src/routes/(kener)/+layout.server.js @@ -1,6 +1,8 @@ // @ts-nocheck import i18n from "$lib/i18n/server"; import { redirect } from "@sveltejs/kit"; +import MobileDetect from "mobile-detect"; + import { base } from "$app/paths"; import { GetAllSiteData, @@ -11,6 +13,10 @@ import { } from "$lib/server/controllers/controller.js"; export async function load({ params, route, url, cookies, request }) { + const userAgent = request.headers.get("user-agent"); + const md = new MobileDetect(userAgent); + const isMobile = !!md.mobile(); + let isSetupComplete = await IsSetupComplete(); if (!isSetupComplete) { throw redirect(302, base + "/manage/setup"); @@ -24,7 +30,6 @@ export async function load({ params, route, url, cookies, request }) { let site = await GetAllSiteData(); const headers = request.headers; - const userAgent = headers.get("user-agent"); let localTz = "UTC"; const localTzCookie = cookies.get("localTz"); if (!!localTzCookie) { @@ -50,5 +55,6 @@ export async function load({ params, route, url, cookies, request }) { selectedLang: selectedLang, isLoggedIn: !!isLoggedIn.user, canSendEmail: IsEmailSetup(), + isMobile: isMobile, }; } diff --git a/src/routes/(kener)/+page.server.js b/src/routes/(kener)/+page.server.js index fc1ebf9..c7ac847 100644 --- a/src/routes/(kener)/+page.server.js +++ b/src/routes/(kener)/+page.server.js @@ -3,7 +3,6 @@ import { FetchData } from "$lib/server/page"; import { GetMonitors, GetIncidentsOpenHome, SystemDataMessage } from "$lib/server/controllers/controller.js"; import { SortMonitor } from "$lib/clientTools.js"; import moment from "moment"; -import { page } from "$app/stores"; import { redirect, error } from "@sveltejs/kit"; function removeTags(str) { @@ -108,7 +107,14 @@ export async function load({ parent, url }) { monitors = SortMonitor(siteData.monitorSort, monitors); const monitorsActive = []; for (let i = 0; i < monitors.length; i++) { - let data = await FetchData(siteData, monitors[i], parentData.localTz, parentData.selectedLang, parentData.lang); + let data = await FetchData( + siteData, + monitors[i], + parentData.localTz, + parentData.selectedLang, + parentData.lang, + parentData.isMobile, + ); monitors[i].pageData = data; monitors[i].activeIncidents = []; @@ -183,7 +189,6 @@ export async function load({ parent, url }) { return isPresent; }); } - allOpenIncidents = allOpenIncidents.map((incident) => { let incidentMonitors = incident.monitors; let monitorTags = incidentMonitors.map((monitor) => monitor.monitor_tag);