From bd5efe41551ce0cf54674f08600939e25ae99cef Mon Sep 17 00:00:00 2001 From: Rodrigo Tornis Date: Wed, 7 Jan 2026 18:02:45 -0300 Subject: [PATCH 1/5] feat: add global auto-refresh for monitors - Created global refresh store to manage refresh state - Added refresh button in layout with animated icon - Monitors now respond to global refresh trigger - Updated getToday function to use current timestamp - Refresh interval configurable in seconds (min 5s) - Settings persisted in localStorage --- Dockerfile | 6 +- docker-compose.yml | 46 ++++----- src/lib/components/manage/homePage.svelte | 9 +- src/lib/components/monitor.svelte | 95 +++++++++++++----- src/lib/locales/en.json | 7 ++ src/lib/locales/pt-BR.json | 2 + src/lib/stores/refreshStore.js | 22 ++++ src/routes/(kener)/+layout.svelte | 117 +++++++++++++++++++++- 8 files changed, 244 insertions(+), 60 deletions(-) create mode 100644 src/lib/stores/refreshStore.js diff --git a/Dockerfile b/Dockerfile index f3e40890c..951c608e4 100644 --- a/Dockerfile +++ b/Dockerfile @@ -13,8 +13,8 @@ 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 \ - sqlite3=3.40.1-2+deb12u1 \ - libsqlite3-dev=3.40.1-2+deb12u1 \ + sqlite3 \ + libsqlite3-dev \ make=4.3-4.1 \ node-gyp=9.3.0-2 \ g++=4:12.2.0-3 \ @@ -77,7 +77,7 @@ 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=3:20221126-1+deb12u1 \ - sqlite3=3.40.1-2+deb12u1 \ + sqlite3 \ tzdata \ # 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 && \ diff --git a/docker-compose.yml b/docker-compose.yml index a19bfabb4..3ad6a9fc7 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -8,12 +8,12 @@ version: '3.8' services: kener: - image: rajnandan1/kener:latest # Change to 'rajnandan1/kener:alpine' for an even smaller image! 😁🚀 + build: . container_name: kener - # env_file: custom.env # Uncomment this if you are needing to export environment variables from a custom environment file. By default, Docker will import any variables that exist in `.env` - environment: - TZ: Etc/UTC - KENER_SECRET_KEY: replace_me_with_a_random_string # Keep private!! - best to define in `.env` file or through Docker Secret + env_file: .env # Uncomment this if you are needing to export environment variables from a custom environment file. By default, Docker will import any variables that exist in `.env` + #environment: + # TZ: America/Sao_Paulo + # KENER_SECRET_KEY: # Keep private!! - best to define in `.env` file or through Docker Secret # DATABASE_URL: custom_db_url # By default, a SQLite database is used - you may override the database url/type here # RESEND_API_KEY: # RESEND_SENDER_EMAIL: @@ -28,7 +28,7 @@ services: - '3000:3000/tcp' volumes: - data:/app/database # We suggest using a Docker named volume, which is more performant for databases - - $(pwd)/uploads:/app/uploads + - ./uploads:/app/uploads # read_only: true # Uncommenting this fortifies security by marking the container's filesystem as read-only (aka no data can be written to the container's filesystem except for explicitly defined writable volumes and bind mounts, an exception has already been defined for `/database` and `/uploads`) restart: unless-stopped # depends_on: # <-- Uncomment if you would like to use PostgreSQL or MySQL @@ -36,25 +36,25 @@ services: # - mysql # # Only use below section if you would like to utilize PostgreSQL instead of Kener's default SQLite database. (Don't forget to set `DATABASE_URL` in `kener` service to be: `DATABASE_URL=postgresql://db_user:db_password@localhost:5432/kener_db`) - postgres: - image: postgres:alpine - container_name: postgres - environment: - POSTGRES_USER: user - POSTGRES_PASSWORD: some_super_random_secure_password # Best to define this in `.env` or via Docker Secret!! - POSTGRES_DB: kener_db - restart: unless-stopped + # postgres: + # image: postgres:alpine + # container_name: postgres + # environment: + # POSTGRES_USER: user + # POSTGRES_PASSWORD: some_super_random_secure_password # Best to define this in `.env` or via Docker Secret!! + # POSTGRES_DB: kener_db + # restart: unless-stopped # Only use below section if you would like to utilize MySQL instead of Kener's default SQLite database. (Don't forget to set `DATABASE_URL` in `kener` service to be: `DATABASE_URL=mysql://db_user:db_password@localhost:3306/kener_db`) - mysql: - image: mariadb:11 - container_name: mysql - environment: - MYSQL_USER: user - MYSQL_PASSWORD: some_super_random_secure_password # Best to define this in `.env` or via Docker Secret!! - MYSQL_DATABASE: kener_db - MYSQL_RANDOM_ROOT_PASSWORD: true - restart: unless-stopped + #mysql: + # image: mariadb:11 + # container_name: mysql + # environment: + # MYSQL_USER: user + # MYSQL_PASSWORD: some_super_random_secure_password # Best to define this in `.env` or via Docker Secret!! + # MYSQL_DATABASE: kener_db + # MYSQL_RANDOM_ROOT_PASSWORD: true + # restart: unless-stopped volumes: data: diff --git a/src/lib/components/manage/homePage.svelte b/src/lib/components/manage/homePage.svelte index 66169dbb5..2e8f83948 100644 --- a/src/lib/components/manage/homePage.svelte +++ b/src/lib/components/manage/homePage.svelte @@ -1,4 +1,5 @@ @@ -288,7 +331,7 @@ {#if rollerLoading} {/if} - {#if !isNaN(uptimesRollers[rolledAt].value)} + {#if typeof uptimesRollers[rolledAt]?.value === 'number'} {#each dayIncidentsFull as incident, index}
- +
{/each} diff --git a/src/lib/locales/en.json b/src/lib/locales/en.json index 20a221636..fdc068d83 100644 --- a/src/lib/locales/en.json +++ b/src/lib/locales/en.json @@ -13,6 +13,13 @@ "All Systems are Down": "All Systems are Down", "All Systems are Operational": "All Systems are Operational", "All Systems are in Maintenance": "All Systems are in Maintenance", + "Not all incidents are shown. Click to see more": "Not all incidents are shown. Click to see more", + "Auto-Refresh": "Auto-Refresh", + "auto-refresh-description": "Enable to periodically refresh the page data.", + "Interval": "Interval", + "Seconds": "Seconds", + "Enable": "Enable", + "Disable": "Disable", "Back": "Back", "Badge Copied": "Badge Copied", "Badge": "Badge", diff --git a/src/lib/locales/pt-BR.json b/src/lib/locales/pt-BR.json index c340fd894..ea9992c74 100644 --- a/src/lib/locales/pt-BR.json +++ b/src/lib/locales/pt-BR.json @@ -13,6 +13,8 @@ "All Systems are Down": "Todos os Sistemas estão Indisponíveis", "All Systems are Operational": "Todos os Sistemas estão Operacionais", "All Systems are in Maintenance": "Todos os Sistemas estão em Manutenção", + "Auto-Refresh": "Atualização Automática", + "auto-refresh-description": "Ative para atualizar os dados da página periodicamente.", "Back": "Voltar", "Badge Copied": "Emblema Copiado", "Badge": "Emblema", diff --git a/src/lib/stores/refreshStore.js b/src/lib/stores/refreshStore.js new file mode 100644 index 000000000..bf49c3cd8 --- /dev/null +++ b/src/lib/stores/refreshStore.js @@ -0,0 +1,22 @@ +import { writable } from 'svelte/store'; + +// Store for global refresh configuration +function createRefreshStore() { + const { subscribe, set, update } = writable({ + enabled: false, + interval: 60, // seconds + lastRefresh: null + }); + + return { + subscribe, + enable: () => update(state => ({ ...state, enabled: true })), + disable: () => update(state => ({ ...state, enabled: false })), + toggle: () => update(state => ({ ...state, enabled: !state.enabled })), + setInterval: (interval) => update(state => ({ ...state, interval })), + updateLastRefresh: () => update(state => ({ ...state, lastRefresh: Date.now() })), + setState: (newState) => set(newState) + }; +} + +export const refreshStore = createRefreshStore(); diff --git a/src/routes/(kener)/+layout.svelte b/src/routes/(kener)/+layout.svelte index 73e09f3ce..7c5dd2447 100644 --- a/src/routes/(kener)/+layout.svelte +++ b/src/routes/(kener)/+layout.svelte @@ -3,7 +3,7 @@ import "../../kener.css"; import "../../theme.css"; import Nav from "$lib/components/nav.svelte"; - import { onMount } from "svelte"; + import { onMount, onDestroy } from "svelte"; import { Input } from "$lib/components/ui/input"; import { base } from "$app/paths"; import { Button } from "$lib/components/ui/button"; @@ -11,12 +11,15 @@ import Moon from "lucide-svelte/icons/moon"; import Languages from "lucide-svelte/icons/languages"; import Globe from "lucide-svelte/icons/globe"; + import Loader from "lucide-svelte/icons/loader"; + import RefreshCw from "lucide-svelte/icons/refresh-cw"; import * as Popover from "$lib/components/ui/popover"; import * as DropdownMenu from "$lib/components/ui/dropdown-menu"; import { analyticsEvent } from "$lib/boringOne"; import { setMode, mode, ModeWatcher } from "mode-watcher"; import { l } from "$lib/i18n/client"; - + import { refreshStore } from "$lib/stores/refreshStore.js"; + export let data; let defaultLocaleKey = data.selectedLang; let allTimezones = Intl.supportedValuesOf("timeZone"); @@ -67,7 +70,77 @@ } let myTimezone = data.localTz; + + // Global refresh configuration + let refreshInterval = 60; + let refreshIntervalId = null; + + // Load refresh settings from localStorage + function loadRefreshSettings() { + if (typeof window === 'undefined') return; + + const savedInterval = localStorage.getItem('kener-global-refresh-interval'); + if (savedInterval) { + refreshInterval = parseInt(savedInterval, 10); + refreshStore.setInterval(refreshInterval); + } + + const savedEnabled = localStorage.getItem('kener-global-refresh-enabled'); + if (savedEnabled === 'true') { + refreshStore.enable(); + startGlobalRefresh(); + } + } + + // Start global refresh + function startGlobalRefresh() { + if (refreshIntervalId) { + clearInterval(refreshIntervalId); + } + refreshIntervalId = setInterval(() => { + refreshStore.updateLastRefresh(); + }, refreshInterval * 1000); + } + + // Stop global refresh + function stopGlobalRefresh() { + if (refreshIntervalId) { + clearInterval(refreshIntervalId); + refreshIntervalId = null; + } + } + + // Toggle global refresh + function toggleGlobalRefresh() { + refreshStore.toggle(); + + if ($refreshStore.enabled) { + localStorage.setItem('kener-global-refresh-enabled', 'true'); + startGlobalRefresh(); + refreshStore.updateLastRefresh(); // Immediate refresh + } else { + localStorage.setItem('kener-global-refresh-enabled', 'false'); + stopGlobalRefresh(); + } + } + + // Save interval to localStorage and update store + $: if (typeof window !== 'undefined' && refreshInterval) { + localStorage.setItem('kener-global-refresh-interval', String(refreshInterval)); + refreshStore.setInterval(refreshInterval); + } + + // Restart interval if changed while active + $: if ($refreshStore.enabled && refreshIntervalId && refreshInterval) { + startGlobalRefresh(); + } + + onDestroy(() => { + stopGlobalRefresh(); + }); + onMount(async () => { + loadRefreshSettings(); myTimezone = Intl.DateTimeFormat().resolvedOptions().timeZone; if (data.localTz === "UTC") { if (data.isBot === false) { @@ -87,6 +160,7 @@ if (!!data.site.favicon && !data.site.favicon.startsWith("http")) data.site.favicon = `${base}${data.site.favicon}`; let kenerTheme = data.site.kenerTheme || "default"; + @@ -124,7 +198,42 @@ {@html data.site.footerHTML} {/if} -
+
+
+ + + + + +
+
+

{l(data.lang, 'Auto-Refresh')}

+

+ {l(data.lang, 'Configure automatic refresh for all monitors')} +

+
+
+
+ + +
+ +
+
+
+
+
{#if !!data.site.tzToggle && data.site.tzToggle === "YES"}
@@ -158,7 +267,7 @@ {#if tz.toLowerCase().includes(searchTzValue.toLowerCase())}