Merge pull request #765 from rajnandan1/implement/665

Implement/665
This commit is contained in:
Raj Nandan Sharma
2026-06-19 22:39:38 +05:30
committed by GitHub
19 changed files with 349 additions and 150 deletions
+19
View File
@@ -0,0 +1,19 @@
import { resolve } from "$app/paths";
import clientResolver from "$lib/client/resolver.js";
import type { NotificationEvent } from "$lib/types/notifications.js";
interface NotificationsResponse {
notifications?: NotificationEvent[];
}
export async function requestNotifications(monitorTags: string[] = []): Promise<NotificationEvent[]> {
const query = monitorTags.length > 0 ? `?tags=${encodeURIComponent(monitorTags.join(","))}` : "";
const response = await fetch(clientResolver(resolve, "/dashboard-apis/notifications") + query);
if (!response.ok) {
throw new Error("Failed to fetch notifications");
}
const payload = (await response.json()) as NotificationsResponse;
return payload.notifications || [];
}
+118
View File
@@ -0,0 +1,118 @@
<script lang="ts">
import { resolve } from "$app/paths";
import { page } from "$app/state";
import { requestNotifications } from "$lib/client/notifications-client.js";
import clientResolver from "$lib/client/resolver.js";
import { Button } from "$lib/components/ui/button/index.js";
import { formatDate, formatDuration } from "$lib/stores/datetime";
import { t } from "$lib/stores/i18n";
import type { NotificationEvent } from "$lib/types/notifications.js";
import Calendar from "@lucide/svelte/icons/calendar-1";
import { format } from "date-fns";
import { onMount } from "svelte";
interface Props {
monitorTags?: string[];
eventsPath?: string;
notifications?: NotificationEvent[];
loading?: boolean;
fetchOnMount?: boolean;
}
let {
monitorTags = [],
eventsPath = "",
notifications = $bindable<NotificationEvent[]>([]),
loading = $bindable(false),
fetchOnMount = true
}: Props = $props();
const defaultEventsPath = $derived(`/events/${format(new Date(), "MMMM-yyyy")}`);
const resolvedEventsPath = $derived.by(() => {
const finalEventsPath = eventsPath || defaultEventsPath;
if (page.data?.globalPageVisibilitySettings?.forceExclusivity) {
const currentPagePath = page.params?.page_path?.trim();
return currentPagePath ? `/${currentPagePath}${finalEventsPath}` : finalEventsPath;
}
return finalEventsPath;
});
async function fetchNotifications() {
loading = true;
try {
notifications = await requestNotifications(monitorTags);
} catch {
// silently fail
} finally {
loading = false;
}
}
onMount(() => {
if (fetchOnMount) {
void fetchNotifications();
}
});
function getEventId(eventURL: string) {
return eventURL.split("/").filter(Boolean).at(-1) || "";
}
</script>
{#snippet notificationItem(item: NotificationEvent)}
<div class="my-0.5 flex items-center justify-between gap-2 text-xs">
<span class="text-muted-foreground text-[11px] uppercase">{$t(item.eventType)}</span>
<span class="text-{item.eventStatus.toLowerCase()}">{$t(item.eventStatus)}</span>
</div>
<div class="flex items-start justify-between gap-2">
<p class="line-clamp-2 text-sm">{item.eventTitle}</p>
</div>
<div class="text-muted-foreground mt-1 flex flex-wrap items-center gap-2 text-xs">
<span>{$formatDate(item.eventDate, page.data.dateAndTimeFormat.datePlusTime)}</span>
<span></span>
<span>{$formatDuration(item.eventStartDateTime, item.eventEndDateTime, $t("Ongoing"))}</span>
</div>
{/snippet}
<div class="flex items-center justify-between border-b px-4 py-3">
<h4 class="text-sm font-semibold">{$t("Events")}</h4>
<Button
variant="outline"
href={clientResolver(resolve, resolvedEventsPath)}
size="icon-sm"
class="rounded-btn"
aria-label={$t("Open events page")}
title={$t("Open events page")}
>
<Calendar class="size-4" />
</Button>
</div>
{#if notifications.length === 0}
<div class="text-muted-foreground px-4 py-6 text-center text-sm">
{$t("No events to show")}
</div>
{:else}
<div class="scrollbar-hidden max-h-96 overflow-y-auto">
{#each notifications as item, i (`${item.eventType}-${item.eventURL}-${item.eventDate}-${i}`)}
{@const eventId = getEventId(item.eventURL)}
{#if item.eventURL.startsWith("/incidents/")}
<a
href={resolve("/(kener)/incidents/[incident_id]", { incident_id: eventId })}
class="hover:bg-muted/60 block border-b px-4 py-3 last:border-b-0"
>
{@render notificationItem(item)}
</a>
{:else}
<a
href={resolve("/(kener)/maintenances/[maintenance_id]", { maintenance_id: eventId })}
class="hover:bg-muted/60 block border-b px-4 py-3 last:border-b-0"
>
{@render notificationItem(item)}
</a>
{/if}
{/each}
</div>
{/if}
+7 -61
View File
@@ -1,22 +1,18 @@
<script lang="ts">
import { resolve } from "$app/paths";
import { page } from "$app/state";
import NotificationsList from "$lib/components/NotificationsList.svelte";
import { Button } from "$lib/components/ui/button/index.js";
import * as Popover from "$lib/components/ui/popover/index.js";
import { Spinner } from "$lib/components/ui/spinner/index.js";
import { requestNotifications } from "$lib/client/notifications-client.js";
import ICONS from "$lib/icons";
import clientResolver from "$lib/client/resolver.js";
import { formatDate, formatDuration } from "$lib/stores/datetime";
import { t } from "$lib/stores/i18n";
import type { NotificationEvent } from "$lib/server/controllers/dashboardController.js";
import Calendar from "@lucide/svelte/icons/calendar-1";
import { format } from "date-fns";
import type { NotificationEvent } from "$lib/types/notifications.js";
import { onMount } from "svelte";
interface Props {
monitorTags?: string[];
compact?: boolean;
eventsPath: string;
eventsPath?: string;
}
let { monitorTags = [], compact = true, eventsPath = "" }: Props = $props();
@@ -24,26 +20,10 @@
let notifications = $state<NotificationEvent[]>([]);
let loading = $state(false);
const defaultEventsPath = $derived(`/events/${format(new Date(), "MMMM-yyyy")}`);
const resolvedEventsPath = $derived.by(() => {
const finalEventsPath = eventsPath || defaultEventsPath;
if (page.data?.globalPageVisibilitySettings?.forceExclusivity) {
const currentPagePath = page.params?.page_path?.trim();
return currentPagePath ? `/${currentPagePath}${finalEventsPath}` : finalEventsPath;
}
return finalEventsPath;
});
async function fetchNotifications() {
loading = true;
try {
const query = monitorTags.length > 0 ? `?tags=${encodeURIComponent(monitorTags.join(","))}` : "";
const response = await fetch(clientResolver(resolve, "/dashboard-apis/notifications") + query);
if (response.ok) {
const payload = await response.json();
notifications = payload.notifications || [];
}
notifications = await requestNotifications(monitorTags);
} catch {
// silently fail
} finally {
@@ -52,7 +32,7 @@
}
onMount(() => {
fetchNotifications();
void fetchNotifications();
});
</script>
@@ -87,40 +67,6 @@
class="bg-background/30 supports-backdrop-filter:bg-background/20 w-96 rounded-3xl border p-0 shadow-2xl backdrop-blur-2xl"
sideOffset={8}
>
<div class="flex items-center justify-between border-b px-4 py-3">
<h4 class="text-sm font-semibold">{$t("Events")}</h4>
<Button variant="outline" href={clientResolver(resolve, resolvedEventsPath)} size="icon-sm" class="rounded-btn">
<Calendar class="size-4" />
</Button>
</div>
{#if notifications.length === 0}
<div class="text-muted-foreground px-4 py-6 text-sm">
{$t("No events to show")}
</div>
{:else}
<div class="scrollbar-hidden max-h-96 overflow-y-auto">
{#each notifications as item, i (`${item.eventType}-${item.eventURL}-${item.eventDate}-${i}`)}
<a
href={clientResolver(resolve, item.eventURL)}
class="hover:bg-muted/60 block border-b px-4 py-3 last:border-b-0"
>
<div class="my-0.5 flex items-center justify-between gap-2 text-xs">
<span class="text-muted-foreground text-[11px] uppercase">{$t(item.eventType)}</span>
<span class="text-{item.eventStatus.toLowerCase()}">{$t(item.eventStatus)}</span>
</div>
<div class="flex items-start justify-between gap-2">
<p class="line-clamp-2 text-sm">{item.eventTitle}</p>
</div>
<div class="text-muted-foreground mt-1 flex flex-wrap items-center gap-2 text-xs">
<span>{$formatDate(item.eventDate, page.data.dateAndTimeFormat.datePlusTime)}</span>
<span></span>
<span>{$formatDuration(item.eventStartDateTime, item.eventEndDateTime, $t("Ongoing"))}</span>
</div>
</a>
{/each}
</div>
{/if}
<NotificationsList {monitorTags} {eventsPath} fetchOnMount={false} bind:notifications bind:loading />
</Popover.Content>
</Popover.Root>
+6 -2
View File
@@ -27,14 +27,16 @@
interface Props {
monitor_tags?: string[];
embedMonitorTag?: string;
hideNotificationsPopover?: boolean;
}
let { monitor_tags = [], embedMonitorTag = "" }: Props = $props();
let { monitor_tags = [], embedMonitorTag = "", hideNotificationsPopover = false }: Props = $props();
let protocol = $state("");
let domain = $state("");
let shareLink = $state("");
const eventsPath = $derived(`/events/${format(new Date(), "MMMM-yyyy")}`);
const showNotificationsPopover = $derived(!hideNotificationsPopover);
const rssHref = $derived.by(() => {
const params = page.params;
@@ -183,7 +185,9 @@
<TimezoneSelector />
{/if}
</ButtonGroup.Root>
<NotificationsPopover {eventsPath} monitorTags={monitor_tags} compact={true} />
{#if showNotificationsPopover}
<NotificationsPopover {eventsPath} monitorTags={monitor_tags} compact={true} />
{/if}
{#if loginDetails}
<Button
size="sm"
+1
View File
@@ -87,6 +87,7 @@
"Notifications": "Notifications",
"One-time": "One-time",
"Ongoing": "Ongoing",
"Open events page": "Open events page",
"Operational": "Operational",
"Partial Degraded Performance": "Partial Degraded Performance",
"Partial System Outage": "Partial System Outage",
@@ -17,6 +17,9 @@ import type {
import type { GroupMonitorTypeData } from "../types/monitor.js";
import GC from "../../global-constants.js";
import type { LayoutServerData } from "./layoutController.js";
import type { NotificationEvent } from "../../types/notifications.js";
export type { NotificationEvent };
// Default page settings
const defaultPageSettings: PageSettingsType = {
@@ -27,16 +30,6 @@ const defaultPageSettings: PageSettingsType = {
monitor_layout_style: GC.DEFAULT_MONITOR_LAYOUT_STYLE,
};
export interface NotificationEvent {
eventURL: string;
eventTitle: string;
eventDate: string;
eventType: string;
eventStartDateTime: number;
eventEndDateTime: number | null;
eventStatus: string;
}
export interface NotificationPayload {
notifications: NotificationEvent[];
}
@@ -377,17 +370,18 @@ export const GetPageDashboardData = async (
};
}
const eventSettings = layoutData.eventDisplaySettings;
const showInlineEvents = eventSettings.showInlineEvents === true;
// Fetch all dashboard data in parallel (respecting feature toggles)
const [latestData, parsedMonitors, ongoingIncidents, ongoingMaintenances, upcomingMaintenances] = await Promise.all([
GetLatestMonitoringDataAllActive(monitorTags),
GetMonitorsParsed({ tags: monitorTags, status: "ACTIVE", is_hidden: "NO" }),
eventSettings.incidents.enabled && eventSettings.incidents.ongoing.show
showInlineEvents && eventSettings.incidents.enabled && eventSettings.incidents.ongoing.show
? GetOngoingIncidentsForMonitorList(monitorTags)
: Promise.resolve([] as IncidentForMonitorListWithComments[]),
eventSettings.maintenances.enabled && eventSettings.maintenances.ongoing.show
showInlineEvents && eventSettings.maintenances.enabled && eventSettings.maintenances.ongoing.show
? GetOngoingMaintenances(monitorTags, nowTs)
: Promise.resolve([] as MaintenanceEventsMonitorList[]),
eventSettings.maintenances.enabled && eventSettings.maintenances.upcoming.show
showInlineEvents && eventSettings.maintenances.enabled && eventSettings.maintenances.upcoming.show
? GetUpcomingMaintenanceEventsForMonitorList(
monitorTags,
eventSettings.maintenances.upcoming.maxCount,
+38 -1
View File
@@ -76,6 +76,43 @@ export interface LayoutServerData {
metaSiteDescription?: string;
}
function NormalizeEventDisplaySettings(settings?: Partial<EventDisplaySettings>): EventDisplaySettings {
const defaults = structuredClone(seedSiteData.eventDisplaySettings);
return {
showInlineEvents:
typeof settings?.showInlineEvents === "boolean" ? settings.showInlineEvents : defaults.showInlineEvents,
incidents: {
...defaults.incidents,
...settings?.incidents,
ongoing: {
...defaults.incidents.ongoing,
...settings?.incidents?.ongoing,
},
resolved: {
...defaults.incidents.resolved,
...settings?.incidents?.resolved,
},
},
maintenances: {
...defaults.maintenances,
...settings?.maintenances,
ongoing: {
...defaults.maintenances.ongoing,
...settings?.maintenances?.ongoing,
},
past: {
...defaults.maintenances.past,
...settings?.maintenances?.past,
},
upcoming: {
...defaults.maintenances.upcoming,
...settings?.maintenances?.upcoming,
},
},
};
}
export async function GetLayoutServerData(cookies: Cookies, request: Request): Promise<LayoutServerData> {
const userAgent = request.headers.get("user-agent") ?? "";
const md = new MobileDetect(userAgent);
@@ -139,7 +176,7 @@ export async function GetLayoutServerData(cookies: Cookies, request: Request): P
font,
canSendEmail,
announcement: siteData.announcement,
eventDisplaySettings: siteData.eventDisplaySettings || seedSiteData.eventDisplaySettings,
eventDisplaySettings: NormalizeEventDisplaySettings(siteData.eventDisplaySettings),
socialPreviewImage: siteData.socialPreviewImage,
customCSS: siteData.customCSS,
globalPageVisibilitySettings: siteData.globalPageVisibilitySettings || seedSiteData.globalPageVisibilitySettings,
+1
View File
@@ -142,6 +142,7 @@ const seedSiteData = {
retentionDays: 90,
},
eventDisplaySettings: {
showInlineEvents: false,
incidents: {
enabled: true,
ongoing: { show: true },
+9
View File
@@ -0,0 +1,9 @@
export interface NotificationEvent {
eventURL: string;
eventTitle: string;
eventDate: string;
eventType: string;
eventStartDateTime: number;
eventEndDateTime: number | null;
eventStatus: string;
}
+1
View File
@@ -98,6 +98,7 @@ export interface DataRetentionPolicy {
}
export interface EventDisplaySettings {
showInlineEvents: boolean;
incidents: {
enabled: boolean;
ongoing: {
@@ -26,6 +26,7 @@ Use **Manage → Site Configurations** to control identity, navigation, monitor
| Monitor sub menu options | `subMenuOptions` | Gates monitor share actions (badges/embed) on public monitor pages |
| Global page visibility | `globalPageVisibilitySettings` | Controls page switcher visibility and page-scoped navigation/events |
| Data retention policy | `dataRetentionPolicy` | Controls daily cleanup of old `monitoring_data` |
| Event display settings | `eventDisplaySettings` | Controls event visibility and whether events render inline or in the notification surface |
| Maintenance notifications | `globalMaintenanceNotificationSettings` | Controls which maintenance lifecycle events notify subscribers and reminder buffer timing |
| Social preview & SEO | `metaSiteTitle`, `metaSiteDescription`, `socialPreviewImage` | Default `<title>`, `og:title`, `<meta description>`, `og:description`, `og:image` for all pages |
@@ -76,13 +77,14 @@ When enabled, cleanup runs daily at midnight UTC.
`eventDisplaySettings` controls which events are visible:
- `showInlineEvents`: when `true`, ongoing/upcoming events render inline on status pages; when `false` (default), they are shown through the notification UI.
- incidents: ongoing/resolved + resolved limits
- maintenances: ongoing/past/upcoming + limits
This affects:
- event sections on status pages
- notifications payload API used by the UI
- notification UI visibility and payloads used by the UI
## Maintenance notification settings {#maintenance-notification-settings}
@@ -126,7 +128,7 @@ These values are used as defaults for every page. Individual pages can override
- Enable `forceExclusivity` and verify:
- brand link stays within current page path,
- notifications calendar opens page-scoped events for the current month.
- Change event display settings and verify incident/maintenance visibility.
- Change event display settings and verify inline events vs notification UI behavior.
- Set retention policy and confirm scheduler logs in server output.
- Toggle maintenance notification event types and verify subscribers receive (or don't receive) emails at each lifecycle stage.
- Set meta title/description and social preview image, then check `<meta>` tags in page source.
+1 -1
View File
@@ -40,7 +40,7 @@
}
</style>`}
</svelte:head>
<main class="kener-public">
<main class="kener-public embed-app">
{@render children()}
</main>
+1 -1
View File
@@ -53,7 +53,7 @@
</style>`}
<script src={clientResolver(resolve, "/capture.js")}></script>
</svelte:head>
<main class="kener-public">
<main class="kener-public status-page-app">
<!-- Nav -->
<KenerNav />
<!-- Body -->
+63 -51
View File
@@ -6,9 +6,11 @@
import ThemePlus from "$lib/components/ThemePlus.svelte";
import IncidentItem from "$lib/components/IncidentItem.svelte";
import MaintenanceItem from "$lib/components/MaintenanceItem.svelte";
import NotificationsList from "$lib/components/NotificationsList.svelte";
import mdToHTML from "$lib/marked.js";
import clientResolver, { absoluteResolve } from "$lib/client/resolver.js";
import { resolve } from "$app/paths";
import { selectedTimezone } from "$lib/stores/timezone";
import { getEndOfDayAtTz } from "$lib/client/datetime";
import { requestMonitorBar } from "$lib/client/monitor-bar-client";
@@ -16,7 +18,6 @@
import { SveltePurify } from "@humanspeak/svelte-purify";
import type { PageMonitorLayoutStyle } from "$lib/types/api";
import GC from "$lib/global-constants.js";
let { data } = $props();
let pageSettings = $derived(data.pageDetails.page_settings);
let barCount = $derived.by(() =>
@@ -31,6 +32,7 @@
let requestVersion = 0;
let viewType = $derived<PageMonitorLayoutStyle | undefined>(pageSettings?.monitor_layout_style);
let isCompact = $derived(viewType === "compact-list" || viewType === "compact-grid");
let showInlineEvents = $derived(data.eventDisplaySettings?.showInlineEvents === true);
function getGridItemSpanClass(index: number, total: number, type: typeof viewType): string {
if (type === "default-grid") {
@@ -143,7 +145,7 @@
<!-- page title -->
<div class="flex flex-col gap-3 sm:gap-4">
<ThemePlus monitor_tags={data.monitorTags} />
<ThemePlus monitor_tags={data.monitorTags} hideNotificationsPopover={!!showInlineEvents} />
<div class="flex flex-col gap-2 px-3 py-2 sm:px-4">
{#if data.pageDetails?.page_logo}
<img
@@ -169,58 +171,68 @@
</Item.Content>
</Item.Root>
</div>
{#if !!data.monitorTags.length}
<EventsCard statusClass={data.pageStatus.statusClass} statusText={data.pageStatus.statusSummary} />
{#if data.ongoingIncidents && data.ongoingIncidents.length > 0}
<div class="flex flex-col gap-3">
{#each data.ongoingIncidents as incident, i (incident.id ?? i)}
<div class=" rounded-3xl border p-3 sm:p-4">
<IncidentItem {incident} />
<div class="grid grid-cols-1 gap-4 sm:grid-cols-16">
<div class="col-span-1 flex flex-col gap-3 sm:gap-4 {!showInlineEvents ? 'sm:col-span-16' : 'sm:col-span-11'}">
{#if !!data.monitorTags.length}
<EventsCard statusClass={data.pageStatus.statusClass} statusText={data.pageStatus.statusSummary} />
{#if showInlineEvents && data.ongoingIncidents && data.ongoingIncidents.length > 0}
<div class="flex flex-col gap-3">
{#each data.ongoingIncidents as incident, i (incident.id ?? i)}
<div class=" rounded-3xl border p-3 sm:p-4">
<IncidentItem {incident} />
</div>
{/each}
</div>
{/each}
</div>
{/if}
{#if data.ongoingMaintenances && data.ongoingMaintenances.length > 0}
<div class="flex flex-col gap-3">
{#each data.ongoingMaintenances as maintenance, i (maintenance.id ?? i)}
<div class="rounded-3xl border p-3 sm:p-4">
<MaintenanceItem {maintenance} />
{/if}
{#if showInlineEvents && data.ongoingMaintenances && data.ongoingMaintenances.length > 0}
<div class="flex flex-col gap-3">
{#each data.ongoingMaintenances as maintenance, i (maintenance.id ?? i)}
<div class="rounded-3xl border p-3 sm:p-4">
<MaintenanceItem {maintenance} />
</div>
{/each}
</div>
{/each}
</div>
{/if}
{#if data.upcomingMaintenances && data.upcomingMaintenances.length > 0}
<div class="flex flex-col gap-3">
{#each data.upcomingMaintenances as maintenance, i (maintenance.id ?? i)}
<div class="rounded-3xl border p-3 sm:p-4">
<MaintenanceItem {maintenance} />
{/if}
{#if showInlineEvents && data.upcomingMaintenances && data.upcomingMaintenances.length > 0}
<div class="flex flex-col gap-3">
{#each data.upcomingMaintenances as maintenance, i (maintenance.id ?? i)}
<div class="rounded-3xl border p-3 sm:p-4">
<MaintenanceItem {maintenance} />
</div>
{/each}
</div>
{/each}
</div>
{/if}
<div class="overflow-hidden rounded-3xl border">
<div class={`grid grid-cols-1 ${getGridContainerClass(viewType)}`}>
{#each data.monitorTags as tag, i (tag)}
<div
class="{viewType === 'compact-grid' || viewType === 'default-grid'
? `${getGridItemSpanClass(i, data.monitorTags.length, viewType)} bg-background`
: i < data.monitorTags.length - 1
? 'border-b'
: ''} px-2 py-2 sm:px-0"
>
<MonitorBar
{tag}
prefetchedData={monitorBarDataByTag[tag]}
prefetchedError={monitorBarErrorByTag[tag]}
days={barCount}
{endOfDayTodayAtTz}
groupChildTags={data.monitorGroupMembersByTag?.[tag] || []}
compact={isCompact}
grid={viewType === "compact-grid" || viewType === "default-grid"}
/>
{/if}
<div class="overflow-hidden rounded-3xl border">
<div class={`grid grid-cols-1 ${getGridContainerClass(viewType)}`}>
{#each data.monitorTags as tag, i (tag)}
<div
class="{viewType === 'compact-grid' || viewType === 'default-grid'
? `${getGridItemSpanClass(i, data.monitorTags.length, viewType)} bg-background`
: i < data.monitorTags.length - 1
? 'border-b'
: ''} px-2 py-2 sm:px-0"
>
<MonitorBar
{tag}
prefetchedData={monitorBarDataByTag[tag]}
prefetchedError={monitorBarErrorByTag[tag]}
days={barCount}
{endOfDayTodayAtTz}
groupChildTags={data.monitorGroupMembersByTag?.[tag] || []}
compact={isCompact}
grid={viewType === "compact-grid" || viewType === "default-grid"}
/>
</div>
{/each}
</div>
{/each}
</div>
</div>
{/if}
</div>
{/if}
{#if !!showInlineEvents}
<!-- Notification List -->
<div class="col-span-1 overflow-hidden rounded-3xl border sm:col-span-5">
<NotificationsList monitorTags={data.monitorTags} />
</div>
{/if}
</div>
</div>
+5 -4
View File
@@ -31,6 +31,7 @@
let requestVersion = 0;
let viewType = $derived<PageMonitorLayoutStyle | undefined>(pageSettings?.monitor_layout_style);
let isCompact = $derived(viewType === "compact-list" || viewType === "compact-grid");
let showInlineEvents = $derived(data.eventDisplaySettings?.showInlineEvents === true);
function getGridItemSpanClass(index: number, total: number, type: typeof viewType): string {
if (type === "default-grid") {
@@ -143,7 +144,7 @@
<!-- page title -->
<div class="flex flex-col gap-3 sm:gap-4">
<ThemePlus monitor_tags={data.monitorTags} />
<ThemePlus monitor_tags={data.monitorTags} hideNotificationsPopover={showInlineEvents} />
<div class="flex flex-col gap-2 px-3 py-2 sm:px-4">
{#if data.pageDetails?.page_logo}
<img
@@ -171,7 +172,7 @@
</div>
{#if !!data.monitorTags.length}
<EventsCard statusClass={data.pageStatus.statusClass} statusText={data.pageStatus.statusSummary} />
{#if data.ongoingIncidents && data.ongoingIncidents.length > 0}
{#if showInlineEvents && data.ongoingIncidents && data.ongoingIncidents.length > 0}
<div class="flex flex-col gap-3">
{#each data.ongoingIncidents as incident, i (incident.id ?? i)}
<div class=" rounded-3xl border p-3 sm:p-4">
@@ -180,7 +181,7 @@
{/each}
</div>
{/if}
{#if data.ongoingMaintenances && data.ongoingMaintenances.length > 0}
{#if showInlineEvents && data.ongoingMaintenances && data.ongoingMaintenances.length > 0}
<div class="flex flex-col gap-3">
{#each data.ongoingMaintenances as maintenance, i (maintenance.id ?? i)}
<div class="rounded-3xl border p-3 sm:p-4">
@@ -189,7 +190,7 @@
{/each}
</div>
{/if}
{#if data.upcomingMaintenances && data.upcomingMaintenances.length > 0}
{#if showInlineEvents && data.upcomingMaintenances && data.upcomingMaintenances.length > 0}
<div class="flex flex-col gap-3">
{#each data.upcomingMaintenances as maintenance, i (maintenance.id ?? i)}
<div class="rounded-3xl border p-3 sm:p-4">
@@ -36,10 +36,15 @@ export const load: PageServerLoad = async ({ params, parent }) => {
const monitorTags = [monitor_tag];
const eventSettings = parentData.eventDisplaySettings;
const showInlineEvents = eventSettings.showInlineEvents === true;
const [ongoingIncidents, ongoingMaintenances, upcomingMaintenances] = await Promise.all([
GetOngoingIncidentsForMonitorList(monitorTags),
GetOngoingMaintenanceEventsForMonitorList(monitorTags),
eventSettings.maintenances.enabled && eventSettings.maintenances.upcoming.show
showInlineEvents && eventSettings.incidents.enabled && eventSettings.incidents.ongoing.show
? GetOngoingIncidentsForMonitorList(monitorTags)
: Promise.resolve([]),
showInlineEvents && eventSettings.maintenances.enabled && eventSettings.maintenances.ongoing.show
? GetOngoingMaintenanceEventsForMonitorList(monitorTags)
: Promise.resolve([]),
showInlineEvents && eventSettings.maintenances.enabled && eventSettings.maintenances.upcoming.show
? GetUpcomingMaintenanceEventsForMonitorList(
monitorTags,
eventSettings.maintenances.upcoming.maxCount,
@@ -16,6 +16,7 @@
// State
let descriptionExpanded = $state(false);
let showInlineEvents = $derived(data.eventDisplaySettings?.showInlineEvents === true);
function toggleDescription(expanded: boolean) {
descriptionExpanded = expanded;
@@ -42,7 +43,11 @@
{/if}
</svelte:head>
<div class="flex flex-col gap-3">
<ThemePlus monitor_tags={[data.monitorTag]} embedMonitorTag={data.monitorTag} />
<ThemePlus
monitor_tags={[data.monitorTag]}
embedMonitorTag={data.monitorTag}
hideNotificationsPopover={showInlineEvents}
/>
<div class="flex flex-col gap-2 px-4 py-2">
{#if data.monitorImage}
<img
@@ -125,7 +130,7 @@
{/if}
</div>
</div>
{#if data.ongoingIncidents && data.ongoingIncidents.length > 0}
{#if showInlineEvents && data.ongoingIncidents && data.ongoingIncidents.length > 0}
<div class="flex flex-col gap-3">
{#each data.ongoingIncidents as incident, i (incident.id ?? i)}
<div class=" rounded-3xl border p-3 sm:p-4">
@@ -134,7 +139,7 @@
{/each}
</div>
{/if}
{#if data.ongoingMaintenances && data.ongoingMaintenances.length > 0}
{#if showInlineEvents && data.ongoingMaintenances && data.ongoingMaintenances.length > 0}
<div class="flex flex-col gap-3">
{#each data.ongoingMaintenances as maintenance, i (maintenance.id ?? i)}
<div class="rounded-3xl border p-3 sm:p-4">
@@ -143,7 +148,7 @@
{/each}
</div>
{/if}
{#if data.upcomingMaintenances && data.upcomingMaintenances.length > 0}
{#if showInlineEvents && data.upcomingMaintenances && data.upcomingMaintenances.length > 0}
<div class="flex flex-col gap-3">
{#each data.upcomingMaintenances as maintenance, i (maintenance.id ?? i)}
<div class="rounded-3xl border p-3 sm:p-4">
@@ -205,11 +205,7 @@
if (data.eventDisplaySettings) {
try {
const parsed =
typeof data.eventDisplaySettings === "string"
? JSON.parse(data.eventDisplaySettings)
: data.eventDisplaySettings;
eventDisplaySettings = { ...structuredClone(defaultEventDisplaySettings), ...parsed };
eventDisplaySettings = parseEventDisplaySettings(data.eventDisplaySettings);
} catch {
eventDisplaySettings = structuredClone(defaultEventDisplaySettings);
}
@@ -509,6 +505,44 @@
}
}
function parseEventDisplaySettings(value: unknown): EventDisplaySettings {
const parsed = (typeof value === "string" ? JSON.parse(value) : value) as Partial<EventDisplaySettings> | null;
const defaults = structuredClone(defaultEventDisplaySettings);
return {
showInlineEvents:
typeof parsed?.showInlineEvents === "boolean" ? parsed.showInlineEvents : defaults.showInlineEvents,
incidents: {
...defaults.incidents,
...parsed?.incidents,
ongoing: {
...defaults.incidents.ongoing,
...parsed?.incidents?.ongoing
},
resolved: {
...defaults.incidents.resolved,
...parsed?.incidents?.resolved
}
},
maintenances: {
...defaults.maintenances,
...parsed?.maintenances,
ongoing: {
...defaults.maintenances.ongoing,
...parsed?.maintenances?.ongoing
},
past: {
...defaults.maintenances.past,
...parsed?.maintenances?.past
},
upcoming: {
...defaults.maintenances.upcoming,
...parsed?.maintenances?.upcoming
}
}
};
}
function addSitemapUrl() {
sitemap.urls = [...sitemap.urls, { loc: "" }];
}
@@ -1249,6 +1283,16 @@
<Card.Description>Configure which incidents and maintenances are shown on the site</Card.Description>
</Card.Header>
<Card.Content class="space-y-6">
<div class="flex items-center justify-between rounded-lg border p-4">
<div class="space-y-0.5">
<Label for="events-display-inline">Display Events Inline</Label>
<p class="text-muted-foreground text-xs">
Turn on to show events inline on the status page. Off shows them in the notification list.
</p>
</div>
<Switch id="events-display-inline" bind:checked={eventDisplaySettings.showInlineEvents} />
</div>
<!-- Incidents -->
<div class="space-y-4">
<div class="flex items-center justify-between">
+2 -2
View File
@@ -329,7 +329,7 @@ body::-webkit-scrollbar {
stays transparent, so page content shows through both sections while scrolling. This paints
one blurred layer behind both: above page content, below the nav (z-10) and the bar (z-20).
Override --top-glass-h if the nav/bar wrap onto extra lines. */
.kener-public::before {
.kener-public.status-page-app::before {
content: "";
position: fixed;
inset: 0 0 auto 0;
@@ -340,6 +340,6 @@ body::-webkit-scrollbar {
backdrop-filter: blur(12px);
pointer-events: none;
}
:is(.dark) .kener-public::before {
:is(.dark) .kener-public.status-page-app::before {
background-color: color-mix(in oklab, var(--background) 70%, transparent);
}