Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions src/lib/client/types/refresh.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export type RefreshState = {
enabled: boolean;
interval: number;
lastRefresh: number | null;
};
78 changes: 42 additions & 36 deletions src/lib/components/KenerNav.svelte
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
<script lang="ts">
import type { Snippet } from "svelte";
import { page } from "$app/state";
import * as NavigationMenu from "$lib/components/ui/navigation-menu/index.js";
import * as Dropdown from "$lib/components/ui/dropdown-menu/index.js";
Expand All @@ -9,6 +10,7 @@
import trackEvent from "$lib/beacon";
import MenuIcon from "@lucide/svelte/icons/menu";

let { controls }: { controls?: Snippet } = $props();
let { data } = page;
const navItems: { name: string; url: string; iconURL: string }[] = data.navItems || [];
const { siteName, logo, globalPageVisibilitySettings } = data;
Expand Down Expand Up @@ -80,42 +82,46 @@
</NavigationMenu.List>
</NavigationMenu.Root>

<!-- Mobile Nav -->
{#if navItems.length > 0}
<Dropdown.Root>
<Dropdown.Trigger>
{#snippet child({ props })}
<button
{...props}
type="button"
class="{navigationMenuTriggerStyle()} hover:border-border border border-transparent bg-transparent text-xs hover:bg-transparent sm:hidden"
style="border-radius: var(--radius-3xl)"
aria-label="Open navigation menu"
>
<MenuIcon class="h-4 w-4" />
</button>
{/snippet}
</Dropdown.Trigger>
<Dropdown.Content align="end" class="w-56 rounded-3xl p-2">
{#each navItems as item (item.url)}
<Button
variant="ghost"
size="sm"
href={clientResolver(resolve, item.url)}
class="w-full justify-start rounded-full text-xs"
target={item.url.startsWith("http") ? "_blank" : undefined}
rel={item.url.startsWith("http") ? "noopener noreferrer" : undefined}
onclick={() => trackNavClick(item)}
>
{#if item.iconURL}
<img src={clientResolver(resolve, item.iconURL)} alt={item.name} class="mr-2 h-4 w-4 object-cover" />
{/if}
{item.name}
</Button>
{/each}
</Dropdown.Content>
</Dropdown.Root>
{/if}
<div class="flex items-center gap-1">
{@render controls?.()}

<!-- Mobile Nav -->
{#if navItems.length > 0}
<Dropdown.Root>
<Dropdown.Trigger>
{#snippet child({ props })}
<button
{...props}
type="button"
class="{navigationMenuTriggerStyle()} hover:border-border border border-transparent bg-transparent text-xs hover:bg-transparent sm:hidden"
style="border-radius: var(--radius-3xl)"
aria-label="Open navigation menu"
>
<MenuIcon class="h-4 w-4" />
</button>
{/snippet}
</Dropdown.Trigger>
<Dropdown.Content align="end" class="w-56 rounded-3xl p-2">
{#each navItems as item (item.url)}
<Button
variant="ghost"
size="sm"
href={clientResolver(resolve, item.url)}
class="w-full justify-start rounded-full text-xs"
target={item.url.startsWith("http") ? "_blank" : undefined}
rel={item.url.startsWith("http") ? "noopener noreferrer" : undefined}
onclick={() => trackNavClick(item)}
>
{#if item.iconURL}
<img src={clientResolver(resolve, item.iconURL)} alt={item.name} class="mr-2 h-4 w-4 object-cover" />
{/if}
{item.name}
</Button>
{/each}
</Dropdown.Content>
</Dropdown.Root>
{/if}
</div>
</div>
</div>
</div>
6 changes: 6 additions & 0 deletions src/lib/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,12 @@
"%latency %metric latency": "%latency %metric latency",
"Affected Monitors (%count)": "Affected Monitors (%count)",
"All Systems Operational": "All Systems Operational",
"Auto-Refresh": "Auto-Refresh",
"auto-refresh-description": "Enable periodic page data refresh.",
"Interval": "Interval",
"Seconds": "Seconds",
"Enable": "Enable",
"Disable": "Disable",
"Average Latency": "Average Latency",
"Avg Latency": "Avg Latency",
"Back": "Back",
Expand Down
6 changes: 6 additions & 0 deletions src/lib/locales/pt-BR.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,12 @@
"%latency %metric latency": "%latency %metric latência",
"Affected Monitors (%count)": "Monitores afetados (%count)",
"All Systems Operational": "Todos os sistemas operando",
"Auto-Refresh": "Atualização Automática",
"auto-refresh-description": "Ative para atualizar os dados da página periodicamente.",
"Interval": "Intervalo",
"Seconds": "Segundos",
"Enable": "Ativar",
"Disable": "Desativar",
"Average Latency": "Latência Média",
"Avg Latency": "Latência Média",
"Back": "Voltar",
Expand Down
44 changes: 44 additions & 0 deletions src/lib/stores/refreshStore.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { writable } from "svelte/store";
import type { RefreshState } from "$lib/client/types/refresh";

export const DEFAULT_REFRESH_INTERVAL = 60;
export const MIN_REFRESH_INTERVAL = 5;
export const MAX_REFRESH_INTERVAL = 86400;

const defaultState: RefreshState = {
enabled: false,
interval: DEFAULT_REFRESH_INTERVAL,
lastRefresh: null
Comment thread
coderabbitai[bot] marked this conversation as resolved.
};

function createRefreshStore() {
const { subscribe, set, update } = writable<RefreshState>(defaultState);

return {
subscribe,

toggle: () =>
update((state) => ({
...state,
enabled: !state.enabled
})),

setInterval: (interval: number) =>
update((state) => ({
...state,
interval
})),

updateLastRefresh: () =>
update((state) => ({
...state,
lastRefresh: Date.now()
})),

setState: (newState: RefreshState) => set(newState),

reset: () => set(defaultState)
};
}

export const refreshStore = createRefreshStore();
152 changes: 151 additions & 1 deletion src/routes/(kener)/+layout.svelte
Original file line number Diff line number Diff line change
@@ -1,14 +1,28 @@
<script lang="ts">
import "../layout.css";
import "../kener.css";
import { onMount } from "svelte";
import { ModeWatcher } from "mode-watcher";
import KenerNav from "$lib/components/KenerNav.svelte";
import KenerFooter from "$lib/components/KenerFooter.svelte";
import { Button } from "$lib/components/ui/button/index.js";
import { Input } from "$lib/components/ui/input/index.js";
import * as Popover from "$lib/components/ui/popover/index.js";
import * as Tooltip from "$lib/components/ui/tooltip/index.js";
import { Toaster } from "$lib/components/ui/sonner/index.js";
import { invalidateAll } from "$app/navigation";
import { resolve } from "$app/paths";
import { page } from "$app/state";
import clientResolver from "$lib/client/resolver.js";
import RefreshCw from "@lucide/svelte/icons/refresh-cw";
import { toast } from "svelte-sonner";
import { t } from "$lib/stores/i18n";
import {
refreshStore,
DEFAULT_REFRESH_INTERVAL,
MIN_REFRESH_INTERVAL,
MAX_REFRESH_INTERVAL
} from "$lib/stores/refreshStore";

let { children, data } = $props();

Expand All @@ -18,6 +32,96 @@
if (params.page_path) return clientResolver(resolve, `/${params.page_path}/rss.xml`);
return clientResolver(resolve, "/rss.xml");
});

let refreshInterval = $state(DEFAULT_REFRESH_INTERVAL);
let refreshIntervalId: number | undefined;
let refreshInProgress = $state(false);

async function refreshPageData(): Promise<void> {
if (refreshInProgress) return;

refreshInProgress = true;

try {
await invalidateAll();
refreshStore.updateLastRefresh();
} catch (error) {
console.error("Failed to refresh page data:", error);
toast.error($t("Failed to refresh page data"));
} finally {
refreshInProgress = false;
}
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

function startGlobalRefresh() {
stopGlobalRefresh();
refreshIntervalId = window.setInterval(() => {
void refreshPageData();
}, $refreshStore.interval * 1000);
}

function stopGlobalRefresh() {
if (refreshIntervalId !== undefined) {
clearInterval(refreshIntervalId);
refreshIntervalId = undefined;
}
}

function normalizeRefreshInterval(value: unknown): number {
const interval = Number(value);

if (!Number.isFinite(interval)) {
return DEFAULT_REFRESH_INTERVAL;
}

return Math.min(
MAX_REFRESH_INTERVAL,
Math.max(MIN_REFRESH_INTERVAL, Math.floor(interval))
);
}

function saveRefreshInterval() {
refreshInterval = normalizeRefreshInterval(refreshInterval);

localStorage.setItem(
"kener-global-refresh-interval",
String(refreshInterval)
);

refreshStore.setInterval(refreshInterval);

if ($refreshStore.enabled) {
startGlobalRefresh();
}
}

function toggleGlobalRefresh() {
refreshStore.toggle();
localStorage.setItem("kener-global-refresh-enabled", String($refreshStore.enabled));

if ($refreshStore.enabled) {
startGlobalRefresh();
void refreshPageData();
} else {
stopGlobalRefresh();
}
}

onMount(() => {
const savedInterval = localStorage.getItem(
"kener-global-refresh-interval"
);

refreshInterval = normalizeRefreshInterval(savedInterval);
refreshStore.setInterval(refreshInterval);

if (localStorage.getItem("kener-global-refresh-enabled") === "true") {
refreshStore.setState({ ...$refreshStore, enabled: true });
startGlobalRefresh();
}

return stopGlobalRefresh;
});
</script>

<ModeWatcher defaultMode={data.defaultSiteTheme as 'light' | 'dark' | 'system'} />
Expand Down Expand Up @@ -56,9 +160,55 @@
</style>`}
<script src={clientResolver(resolve, "/capture.js")}></script>
</svelte:head>
{#snippet refreshControls()}
<Popover.Root>
<Popover.Trigger>
{#snippet child({ props })}
<Button
{...props}
variant="ghost"
size="icon"
aria-label={$t("Auto-Refresh")}
>
<RefreshCw
class={`h-[1.2rem] w-[1.2rem] ${
refreshInProgress ? "animate-spin" : ""
}`}
/>
</Button>
{/snippet}
</Popover.Trigger>
<Popover.Content class="w-60">
<div class="grid gap-4">
<div class="space-y-2">
<h4 class="font-medium leading-none">{$t("Auto-Refresh")}</h4>
<p class="text-sm text-muted-foreground">{$t("auto-refresh-description")}</p>
</div>
<div class="grid gap-2">
<div class="grid grid-cols-3 items-center gap-4">
<label for="global-interval" class="text-xs">{$t("Interval")}</label>
<Input
id="global-interval"
type="number"
bind:value={refreshInterval}
min={MIN_REFRESH_INTERVAL}
max={MAX_REFRESH_INTERVAL}
class="col-span-2 h-8"
placeholder={$t("Seconds")}
onchange={saveRefreshInterval}
/>
</div>
<Button onclick={toggleGlobalRefresh} size="sm">
{$refreshStore.enabled ? $t("Disable") : $t("Enable")}
</Button>
</div>
</div>
</Popover.Content>
</Popover.Root>
{/snippet}
<main class="kener-public status-page-app">
<!-- Nav -->
<KenerNav />
<KenerNav controls={refreshControls} />
<!-- Body -->
<div class="mx-auto max-w-5xl px-4 pt-18">
<Tooltip.Provider>
Expand Down
Loading