Refactor code structure for improved readability and maintainability

This commit is contained in:
Nawaz Dhandala
2026-03-19 08:15:35 +00:00
parent 139aa83fe4
commit dc3db1ec47
28 changed files with 4671 additions and 453 deletions

View File

@@ -0,0 +1,257 @@
import React, { FunctionComponent, ReactElement, useState } from "react";
import Card from "Common/UI/Components/Card/Card";
import DictionaryOfStringsViewer from "Common/UI/Components/Dictionary/DictionaryOfStingsViewer";
import {
KubernetesContainerSpec,
KubernetesContainerStatus,
} from "../../Pages/Kubernetes/Utils/KubernetesObjectParser";
export interface ComponentProps {
containers: Array<KubernetesContainerSpec>;
initContainers: Array<KubernetesContainerSpec>;
containerStatuses?: Array<KubernetesContainerStatus> | undefined;
initContainerStatuses?: Array<KubernetesContainerStatus> | undefined;
}
interface ContainerCardProps {
container: KubernetesContainerSpec;
status?: KubernetesContainerStatus | undefined;
isInit: boolean;
}
const ContainerCard: FunctionComponent<ContainerCardProps> = (
props: ContainerCardProps,
): ReactElement => {
const [showEnv, setShowEnv] = useState<boolean>(false);
const [showMounts, setShowMounts] = useState<boolean>(false);
const envRecord: Record<string, string> = {};
for (const env of props.container.env) {
envRecord[env.name] = env.value;
}
const hasResources: boolean =
Object.keys(props.container.resources.requests).length > 0 ||
Object.keys(props.container.resources.limits).length > 0;
return (
<Card
title={`${props.isInit ? "Init Container: " : "Container: "}${props.container.name}`}
description={props.container.image}
>
<div className="space-y-4">
{/* Status */}
{props.status && (
<div className="flex gap-4 text-sm">
<div>
<span className="text-gray-500">State:</span>{" "}
<span
className={`inline-flex px-2 py-0.5 text-xs font-medium rounded ${
props.status.state === "running"
? "bg-green-50 text-green-700"
: props.status.state === "waiting"
? "bg-yellow-50 text-yellow-700"
: "bg-red-50 text-red-700"
}`}
>
{props.status.state}
</span>
</div>
<div>
<span className="text-gray-500">Ready:</span>{" "}
<span
className={
props.status.ready ? "text-green-700" : "text-red-700"
}
>
{props.status.ready ? "Yes" : "No"}
</span>
</div>
<div>
<span className="text-gray-500">Restarts:</span>{" "}
<span
className={
props.status.restartCount > 0
? "text-yellow-700"
: "text-gray-700"
}
>
{props.status.restartCount}
</span>
</div>
</div>
)}
{/* Command & Args */}
{props.container.command.length > 0 && (
<div className="text-sm">
<span className="text-gray-500 font-medium">Command:</span>{" "}
<code className="text-xs bg-gray-100 px-1 py-0.5 rounded">
{props.container.command.join(" ")}
</code>
</div>
)}
{props.container.args.length > 0 && (
<div className="text-sm">
<span className="text-gray-500 font-medium">Args:</span>{" "}
<code className="text-xs bg-gray-100 px-1 py-0.5 rounded">
{props.container.args.join(" ")}
</code>
</div>
)}
{/* Ports */}
{props.container.ports.length > 0 && (
<div className="text-sm">
<span className="text-gray-500 font-medium">Ports:</span>{" "}
{props.container.ports.map((port, idx) => (
<span
key={idx}
className="inline-flex px-2 py-0.5 text-xs font-medium rounded bg-blue-50 text-blue-700 mr-1"
>
{port.name ? `${port.name}: ` : ""}
{port.containerPort}/{port.protocol}
</span>
))}
</div>
)}
{/* Resources */}
{hasResources && (
<div className="grid grid-cols-2 gap-4 text-sm">
{Object.keys(props.container.resources.requests).length > 0 && (
<div>
<span className="text-gray-500 font-medium block mb-1">
Requests:
</span>
<DictionaryOfStringsViewer
value={props.container.resources.requests}
/>
</div>
)}
{Object.keys(props.container.resources.limits).length > 0 && (
<div>
<span className="text-gray-500 font-medium block mb-1">
Limits:
</span>
<DictionaryOfStringsViewer
value={props.container.resources.limits}
/>
</div>
)}
</div>
)}
{/* Environment Variables (expandable) */}
{props.container.env.length > 0 && (
<div>
<button
onClick={() => {
setShowEnv(!showEnv);
}}
className="text-sm text-indigo-600 hover:text-indigo-900 font-medium"
>
{showEnv ? "Hide" : "Show"} Environment Variables (
{props.container.env.length})
</button>
{showEnv && (
<div className="mt-2">
<DictionaryOfStringsViewer value={envRecord} />
</div>
)}
</div>
)}
{/* Volume Mounts (expandable) */}
{props.container.volumeMounts.length > 0 && (
<div>
<button
onClick={() => {
setShowMounts(!showMounts);
}}
className="text-sm text-indigo-600 hover:text-indigo-900 font-medium"
>
{showMounts ? "Hide" : "Show"} Volume Mounts (
{props.container.volumeMounts.length})
</button>
{showMounts && (
<div className="mt-2 space-y-1">
{props.container.volumeMounts.map((mount, idx) => (
<div key={idx} className="text-sm flex gap-2">
<span className="font-medium text-gray-700">
{mount.name}
</span>
<span className="text-gray-500"></span>
<code className="text-xs bg-gray-100 px-1 py-0.5 rounded">
{mount.mountPath}
</code>
{mount.readOnly && (
<span className="text-xs text-gray-400">(read-only)</span>
)}
</div>
))}
</div>
)}
</div>
)}
</div>
</Card>
);
};
const KubernetesContainersTab: FunctionComponent<ComponentProps> = (
props: ComponentProps,
): ReactElement => {
if (
props.containers.length === 0 &&
props.initContainers.length === 0
) {
return (
<div className="text-gray-500 text-sm p-4">
No container information available.
</div>
);
}
const getStatus: (
name: string,
isInit: boolean,
) => KubernetesContainerStatus | undefined = (
name: string,
isInit: boolean,
): KubernetesContainerStatus | undefined => {
const statuses: Array<KubernetesContainerStatus> | undefined = isInit
? props.initContainerStatuses
: props.containerStatuses;
return statuses?.find(
(s: KubernetesContainerStatus) => s.name === name,
);
};
return (
<div className="space-y-4">
{props.initContainers.map(
(container: KubernetesContainerSpec, index: number) => (
<ContainerCard
key={`init-${index}`}
container={container}
status={getStatus(container.name, true)}
isInit={true}
/>
),
)}
{props.containers.map(
(container: KubernetesContainerSpec, index: number) => (
<ContainerCard
key={`container-${index}`}
container={container}
status={getStatus(container.name, false)}
isInit={false}
/>
),
)}
</div>
);
};
export default KubernetesContainersTab;

View File

@@ -0,0 +1,127 @@
import React, {
FunctionComponent,
ReactElement,
useEffect,
useState,
} from "react";
import {
fetchK8sEventsForResource,
KubernetesEvent,
} from "../../Pages/Kubernetes/Utils/KubernetesObjectFetcher";
import PageLoader from "Common/UI/Components/Loader/PageLoader";
import ErrorMessage from "Common/UI/Components/ErrorMessage/ErrorMessage";
export interface ComponentProps {
clusterIdentifier: string;
resourceKind: string; // "Pod", "Node", "Deployment", etc.
resourceName: string;
namespace?: string | undefined;
}
const KubernetesEventsTab: FunctionComponent<ComponentProps> = (
props: ComponentProps,
): ReactElement => {
const [events, setEvents] = useState<Array<KubernetesEvent>>([]);
const [isLoading, setIsLoading] = useState<boolean>(true);
const [error, setError] = useState<string>("");
useEffect(() => {
const fetchEvents: () => Promise<void> = async (): Promise<void> => {
setIsLoading(true);
try {
const result: Array<KubernetesEvent> =
await fetchK8sEventsForResource({
clusterIdentifier: props.clusterIdentifier,
resourceKind: props.resourceKind,
resourceName: props.resourceName,
namespace: props.namespace,
});
setEvents(result);
} catch (err) {
setError(
err instanceof Error ? err.message : "Failed to fetch events",
);
}
setIsLoading(false);
};
fetchEvents().catch(() => {});
}, [
props.clusterIdentifier,
props.resourceKind,
props.resourceName,
props.namespace,
]);
if (isLoading) {
return <PageLoader isVisible={true} />;
}
if (error) {
return <ErrorMessage message={error} />;
}
if (events.length === 0) {
return (
<div className="text-gray-500 text-sm p-4">
No events found for this {props.resourceKind.toLowerCase()} in the last
24 hours.
</div>
);
}
return (
<div className="overflow-x-auto">
<table className="min-w-full divide-y divide-gray-200">
<thead className="bg-gray-50">
<tr>
<th className="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase">
Time
</th>
<th className="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase">
Type
</th>
<th className="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase">
Reason
</th>
<th className="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase">
Message
</th>
</tr>
</thead>
<tbody className="bg-white divide-y divide-gray-200">
{events.map((event: KubernetesEvent, index: number) => {
const isWarning: boolean =
event.type.toLowerCase() === "warning";
return (
<tr key={index} className={isWarning ? "bg-yellow-50" : ""}>
<td className="px-4 py-3 whitespace-nowrap text-sm text-gray-500">
{event.timestamp}
</td>
<td className="px-4 py-3 whitespace-nowrap text-sm">
<span
className={`inline-flex px-2 py-1 text-xs font-semibold rounded-full ${
isWarning
? "bg-yellow-100 text-yellow-800"
: "bg-green-100 text-green-800"
}`}
>
{event.type}
</span>
</td>
<td className="px-4 py-3 whitespace-nowrap text-sm text-gray-900">
{event.reason}
</td>
<td className="px-4 py-3 text-sm text-gray-500 max-w-lg">
{event.message}
</td>
</tr>
);
})}
</tbody>
</table>
</div>
);
};
export default KubernetesEventsTab;

View File

@@ -0,0 +1,118 @@
import React, {
FunctionComponent,
ReactElement,
useEffect,
useState,
} from "react";
import {
fetchPodLogs,
KubernetesLogEntry,
} from "../../Pages/Kubernetes/Utils/KubernetesObjectFetcher";
import PageLoader from "Common/UI/Components/Loader/PageLoader";
import ErrorMessage from "Common/UI/Components/ErrorMessage/ErrorMessage";
export interface ComponentProps {
clusterIdentifier: string;
podName: string;
containerName?: string | undefined;
namespace?: string | undefined;
}
const KubernetesLogsTab: FunctionComponent<ComponentProps> = (
props: ComponentProps,
): ReactElement => {
const [logs, setLogs] = useState<Array<KubernetesLogEntry>>([]);
const [isLoading, setIsLoading] = useState<boolean>(true);
const [error, setError] = useState<string>("");
useEffect(() => {
const fetchLogs: () => Promise<void> = async (): Promise<void> => {
setIsLoading(true);
try {
const result: Array<KubernetesLogEntry> = await fetchPodLogs({
clusterIdentifier: props.clusterIdentifier,
podName: props.podName,
containerName: props.containerName,
namespace: props.namespace,
limit: 500,
});
setLogs(result);
} catch (err) {
setError(
err instanceof Error ? err.message : "Failed to fetch logs",
);
}
setIsLoading(false);
};
fetchLogs().catch(() => {});
}, [
props.clusterIdentifier,
props.podName,
props.containerName,
props.namespace,
]);
if (isLoading) {
return <PageLoader isVisible={true} />;
}
if (error) {
return <ErrorMessage message={error} />;
}
if (logs.length === 0) {
return (
<div className="text-gray-500 text-sm p-4">
No application logs found for this pod in the last 6 hours. Logs will
appear here once the kubernetes-agent&apos;s filelog receiver is collecting
data.
</div>
);
}
const getSeverityColor: (severity: string) => string = (
severity: string,
): string => {
const s: string = severity.toUpperCase();
if (s === "ERROR" || s === "FATAL" || s === "CRITICAL") {
return "text-red-600";
}
if (s === "WARN" || s === "WARNING") {
return "text-yellow-600";
}
if (s === "DEBUG" || s === "TRACE") {
return "text-gray-400";
}
return "text-gray-700";
};
return (
<div className="bg-gray-900 rounded-lg p-4 overflow-auto max-h-[600px] font-mono text-xs">
{logs.map((log: KubernetesLogEntry, index: number) => {
return (
<div key={index} className="flex gap-2 py-0.5 hover:bg-gray-800">
<span className="text-gray-500 whitespace-nowrap flex-shrink-0">
{log.timestamp}
</span>
{log.containerName && (
<span className="text-blue-400 whitespace-nowrap flex-shrink-0">
[{log.containerName}]
</span>
)}
<span
className={`whitespace-nowrap flex-shrink-0 w-12 ${getSeverityColor(log.severity)}`}
>
{log.severity}
</span>
<span className="text-gray-200 whitespace-pre-wrap break-all">
{log.body}
</span>
</div>
);
})}
</div>
);
};
export default KubernetesLogsTab;

View File

@@ -0,0 +1,47 @@
import React, {
FunctionComponent,
ReactElement,
useState,
} from "react";
import MetricView from "../../Components/Metrics/MetricView";
import MetricViewData from "Common/Types/Metrics/MetricViewData";
import MetricQueryConfigData from "Common/Types/Metrics/MetricQueryConfigData";
import OneUptimeDate from "Common/Types/Date";
import InBetween from "Common/Types/BaseDatabase/InBetween";
export interface ComponentProps {
queryConfigs: Array<MetricQueryConfigData>;
}
const KubernetesMetricsTab: FunctionComponent<ComponentProps> = (
props: ComponentProps,
): ReactElement => {
const endDate: Date = OneUptimeDate.getCurrentDate();
const startDate: Date = OneUptimeDate.addRemoveHours(endDate, -6);
const startAndEndDate: InBetween<Date> = new InBetween(startDate, endDate);
const [metricViewData, setMetricViewData] = useState<MetricViewData>({
startAndEndDate: startAndEndDate,
queryConfigs: [],
formulaConfigs: [],
});
return (
<MetricView
data={{
...metricViewData,
queryConfigs: props.queryConfigs,
}}
hideQueryElements={true}
onChange={(data: MetricViewData) => {
setMetricViewData({
...data,
queryConfigs: props.queryConfigs,
formulaConfigs: [],
});
}}
/>
);
};
export default KubernetesMetricsTab;

View File

@@ -0,0 +1,168 @@
import React, { FunctionComponent, ReactElement } from "react";
import Card from "Common/UI/Components/Card/Card";
import InfoCard from "Common/UI/Components/InfoCard/InfoCard";
import DictionaryOfStringsViewer from "Common/UI/Components/Dictionary/DictionaryOfStingsViewer";
import { KubernetesCondition } from "../../Pages/Kubernetes/Utils/KubernetesObjectParser";
import PageLoader from "Common/UI/Components/Loader/PageLoader";
export interface SummaryField {
title: string;
value: string | ReactElement;
}
export interface ComponentProps {
summaryFields: Array<SummaryField>;
labels: Record<string, string>;
annotations: Record<string, string>;
conditions?: Array<KubernetesCondition> | undefined;
ownerReferences?: Array<{ kind: string; name: string }> | undefined;
isLoading: boolean;
emptyMessage?: string | undefined;
}
const KubernetesOverviewTab: FunctionComponent<ComponentProps> = (
props: ComponentProps,
): ReactElement => {
if (props.isLoading) {
return <PageLoader isVisible={true} />;
}
if (
props.summaryFields.length === 0 &&
Object.keys(props.labels).length === 0
) {
return (
<div className="text-gray-500 text-sm p-4">
{props.emptyMessage ||
"Resource details not yet available. Ensure the kubernetes-agent Helm chart has resourceSpecs.enabled set to true and wait for the next data pull (up to 5 minutes)."}
</div>
);
}
return (
<div className="space-y-6">
{/* Summary Info Cards */}
{props.summaryFields.length > 0 && (
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4">
{props.summaryFields.map(
(field: SummaryField, index: number) => {
return (
<InfoCard
key={index}
title={field.title}
value={field.value}
/>
);
},
)}
</div>
)}
{/* Owner References */}
{props.ownerReferences && props.ownerReferences.length > 0 && (
<Card title="Owner References" description="Resources that own this object.">
<div className="space-y-1">
{props.ownerReferences.map(
(ref: { kind: string; name: string }, index: number) => {
return (
<div key={index} className="text-sm">
<span className="font-medium text-gray-700">
{ref.kind}:
</span>{" "}
<span className="text-gray-600">{ref.name}</span>
</div>
);
},
)}
</div>
</Card>
)}
{/* Conditions */}
{props.conditions && props.conditions.length > 0 && (
<Card
title="Conditions"
description="Current status conditions of this resource."
>
<div className="overflow-x-auto">
<table className="min-w-full divide-y divide-gray-200">
<thead className="bg-gray-50">
<tr>
<th className="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase">
Type
</th>
<th className="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase">
Status
</th>
<th className="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase">
Reason
</th>
<th className="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase">
Message
</th>
<th className="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase">
Last Transition
</th>
</tr>
</thead>
<tbody className="bg-white divide-y divide-gray-200">
{props.conditions.map(
(condition: KubernetesCondition, index: number) => {
return (
<tr key={index}>
<td className="px-4 py-3 whitespace-nowrap text-sm font-medium text-gray-900">
{condition.type}
</td>
<td className="px-4 py-3 whitespace-nowrap text-sm">
<span
className={`inline-flex px-2 py-0.5 text-xs font-medium rounded ${
condition.status === "True"
? "bg-green-50 text-green-700"
: condition.status === "False"
? "bg-red-50 text-red-700"
: "bg-gray-50 text-gray-700"
}`}
>
{condition.status}
</span>
</td>
<td className="px-4 py-3 whitespace-nowrap text-sm text-gray-600">
{condition.reason || "-"}
</td>
<td className="px-4 py-3 text-sm text-gray-600 max-w-md truncate">
{condition.message || "-"}
</td>
<td className="px-4 py-3 whitespace-nowrap text-sm text-gray-500">
{condition.lastTransitionTime || "-"}
</td>
</tr>
);
},
)}
</tbody>
</table>
</div>
</Card>
)}
{/* Labels */}
{Object.keys(props.labels).length > 0 && (
<Card title="Labels" description="Key-value labels attached to this resource.">
<DictionaryOfStringsViewer value={props.labels} />
</Card>
)}
{/* Annotations */}
{Object.keys(props.annotations).length > 0 && (
<Card
title="Annotations"
description="Metadata annotations on this resource."
>
<DictionaryOfStringsViewer value={props.annotations} />
</Card>
)}
</div>
);
};
export default KubernetesOverviewTab;

View File

@@ -0,0 +1,410 @@
import Log from "Common/Models/AnalyticsModels/Log";
import AnalyticsModelAPI, {
ListResult,
} from "Common/UI/Utils/AnalyticsModelAPI/AnalyticsModelAPI";
import ProjectUtil from "Common/UI/Utils/Project";
import OneUptimeDate from "Common/Types/Date";
import SortOrder from "Common/Types/BaseDatabase/SortOrder";
import InBetween from "Common/Types/BaseDatabase/InBetween";
import { JSONObject } from "Common/Types/JSON";
import {
extractObjectFromLogBody,
getKvStringValue,
getKvValue,
KubernetesPodObject,
KubernetesNodeObject,
KubernetesDeploymentObject,
KubernetesStatefulSetObject,
KubernetesDaemonSetObject,
KubernetesJobObject,
KubernetesCronJobObject,
KubernetesNamespaceObject,
parsePodObject,
parseNodeObject,
parseDeploymentObject,
parseStatefulSetObject,
parseDaemonSetObject,
parseJobObject,
parseCronJobObject,
parseNamespaceObject,
} from "./KubernetesObjectParser";
export type KubernetesObjectType =
| KubernetesPodObject
| KubernetesNodeObject
| KubernetesDeploymentObject
| KubernetesStatefulSetObject
| KubernetesDaemonSetObject
| KubernetesJobObject
| KubernetesCronJobObject
| KubernetesNamespaceObject;
export interface FetchK8sObjectOptions {
clusterIdentifier: string;
resourceType: string; // "pods", "nodes", "deployments", etc.
resourceName: string;
namespace?: string | undefined; // Not needed for cluster-scoped resources (nodes, namespaces)
}
type ParserFunction = (kvList: JSONObject) => KubernetesObjectType | null;
function getParser(resourceType: string): ParserFunction | null {
const parsers: Record<string, ParserFunction> = {
pods: parsePodObject,
nodes: parseNodeObject,
deployments: parseDeploymentObject,
statefulsets: parseStatefulSetObject,
daemonsets: parseDaemonSetObject,
jobs: parseJobObject,
cronjobs: parseCronJobObject,
namespaces: parseNamespaceObject,
};
return parsers[resourceType] || null;
}
/**
* Fetch the latest K8s resource object from the Log table.
* The k8sobjects pull mode stores full K8s API objects as log entries.
*/
export async function fetchLatestK8sObject<T extends KubernetesObjectType>(
options: FetchK8sObjectOptions,
): Promise<T | null> {
const parser: ParserFunction | null = getParser(options.resourceType);
if (!parser) {
return null;
}
const projectId: string | undefined =
ProjectUtil.getCurrentProjectId()?.toString();
if (!projectId) {
return null;
}
const endDate: Date = OneUptimeDate.getCurrentDate();
const startDate: Date = OneUptimeDate.addRemoveHours(endDate, -24);
try {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const queryOptions: any = {
modelType: Log,
query: {
projectId: projectId,
time: new InBetween<Date>(startDate, endDate),
attributes: {
"logAttributes.event.domain": "k8s",
"logAttributes.k8s.resource.name": options.resourceType,
},
},
limit: 500, // Get enough logs to find the resource
skip: 0,
select: {
time: true,
body: true,
attributes: true,
},
sort: {
time: SortOrder.Descending,
},
requestOptions: {},
};
const listResult: ListResult<Log> =
await AnalyticsModelAPI.getList<Log>(queryOptions);
// Parse each log body and find the matching resource
for (const log of listResult.data) {
const attrs: JSONObject = log.attributes || {};
// Filter to this cluster
if (
attrs["resource.k8s.cluster.name"] !== options.clusterIdentifier &&
attrs["k8s.cluster.name"] !== options.clusterIdentifier
) {
continue;
}
if (typeof log.body !== "string") {
continue;
}
const objectKvList: JSONObject | null = extractObjectFromLogBody(
log.body,
);
if (!objectKvList) {
continue;
}
// Check if this is the resource we're looking for
const metadataKv: string | JSONObject | null = getKvValue(
objectKvList,
"metadata",
);
if (!metadataKv || typeof metadataKv === "string") {
continue;
}
const name: string = getKvStringValue(metadataKv, "name");
const namespace: string = getKvStringValue(metadataKv, "namespace");
if (name !== options.resourceName) {
continue;
}
// For namespaced resources, also match namespace
if (options.namespace && namespace && namespace !== options.namespace) {
continue;
}
const parsed: KubernetesObjectType | null = parser(objectKvList);
if (parsed) {
return parsed as T;
}
}
return null;
} catch {
return null;
}
}
/**
* Fetch K8s events related to a specific resource.
*/
export interface KubernetesEvent {
timestamp: string;
type: string;
reason: string;
objectKind: string;
objectName: string;
namespace: string;
message: string;
}
export async function fetchK8sEventsForResource(options: {
clusterIdentifier: string;
resourceKind: string; // "Pod", "Node", "Deployment", etc.
resourceName: string;
namespace?: string | undefined;
}): Promise<Array<KubernetesEvent>> {
const projectId: string | undefined =
ProjectUtil.getCurrentProjectId()?.toString();
if (!projectId) {
return [];
}
const endDate: Date = OneUptimeDate.getCurrentDate();
const startDate: Date = OneUptimeDate.addRemoveHours(endDate, -24);
try {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const eventsQueryOptions: any = {
modelType: Log,
query: {
projectId: projectId,
time: new InBetween<Date>(startDate, endDate),
attributes: {
"logAttributes.event.domain": "k8s",
"logAttributes.k8s.resource.name": "events",
},
},
limit: 500,
skip: 0,
select: {
time: true,
body: true,
attributes: true,
},
sort: {
time: SortOrder.Descending,
},
requestOptions: {},
};
const listResult: ListResult<Log> =
await AnalyticsModelAPI.getList<Log>(eventsQueryOptions);
const events: Array<KubernetesEvent> = [];
for (const log of listResult.data) {
const attrs: JSONObject = log.attributes || {};
// Filter to this cluster
if (
attrs["resource.k8s.cluster.name"] !== options.clusterIdentifier &&
attrs["k8s.cluster.name"] !== options.clusterIdentifier
) {
continue;
}
if (typeof log.body !== "string") {
continue;
}
let bodyObj: JSONObject | null = null;
try {
bodyObj = JSON.parse(log.body) as JSONObject;
} catch {
continue;
}
const topKvList: JSONObject | undefined = bodyObj["kvlistValue"] as
| JSONObject
| undefined;
if (!topKvList) {
continue;
}
// Get the "object" which is the actual k8s Event
const objectVal: string | JSONObject | null = getKvValue(
topKvList,
"object",
);
if (!objectVal || typeof objectVal === "string") {
continue;
}
const objectKvList: JSONObject = objectVal;
// Get event details
const eventType: string =
getKvStringValue(objectKvList, "type") || "";
const reason: string = getKvStringValue(objectKvList, "reason") || "";
const note: string = getKvStringValue(objectKvList, "note") || "";
// Get regarding object
const regardingKind: string =
getKvStringValue(
getKvValue(objectKvList, "regarding") as JSONObject | undefined,
"kind",
) || "";
const regardingName: string =
getKvStringValue(
getKvValue(objectKvList, "regarding") as JSONObject | undefined,
"name",
) || "";
const regardingNamespace: string =
getKvStringValue(
getKvValue(objectKvList, "regarding") as JSONObject | undefined,
"namespace",
) || "";
// Filter to events for this specific resource
if (
regardingKind.toLowerCase() !== options.resourceKind.toLowerCase() ||
regardingName !== options.resourceName
) {
continue;
}
if (
options.namespace &&
regardingNamespace &&
regardingNamespace !== options.namespace
) {
continue;
}
events.push({
timestamp: log.time
? OneUptimeDate.getDateAsLocalFormattedString(log.time)
: "",
type: eventType || "Unknown",
reason: reason || "Unknown",
objectKind: regardingKind || "Unknown",
objectName: regardingName || "Unknown",
namespace: regardingNamespace || "default",
message: note || "",
});
}
return events;
} catch {
return [];
}
}
/**
* Fetch application logs for a pod/container from the Log table.
* These come from the filelog receiver (not k8sobjects).
*/
export interface KubernetesLogEntry {
timestamp: string;
body: string;
severity: string;
containerName: string;
}
export async function fetchPodLogs(options: {
clusterIdentifier: string;
podName: string;
containerName?: string | undefined;
namespace?: string | undefined;
limit?: number | undefined;
}): Promise<Array<KubernetesLogEntry>> {
const projectId: string | undefined =
ProjectUtil.getCurrentProjectId()?.toString();
if (!projectId) {
return [];
}
const endDate: Date = OneUptimeDate.getCurrentDate();
const startDate: Date = OneUptimeDate.addRemoveHours(endDate, -6);
// Build attribute filters for filelog data
const attributeFilters: Record<string, string> = {
"resource.k8s.cluster.name": options.clusterIdentifier,
"resource.k8s.pod.name": options.podName,
};
if (options.containerName) {
attributeFilters["resource.k8s.container.name"] = options.containerName;
}
if (options.namespace) {
attributeFilters["resource.k8s.namespace.name"] = options.namespace;
}
try {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const logsQueryOptions: any = {
modelType: Log,
query: {
projectId: projectId,
time: new InBetween<Date>(startDate, endDate),
attributes: attributeFilters,
},
limit: options.limit || 200,
skip: 0,
select: {
time: true,
body: true,
severityText: true,
attributes: true,
},
sort: {
time: SortOrder.Descending,
},
requestOptions: {},
};
const listResult: ListResult<Log> =
await AnalyticsModelAPI.getList<Log>(logsQueryOptions);
return listResult.data
.filter((log: Log) => {
// Exclude k8s event logs — only application logs
const attrs: JSONObject = log.attributes || {};
return attrs["logAttributes.event.domain"] !== "k8s";
})
.map((log: Log) => {
const attrs: JSONObject = log.attributes || {};
return {
timestamp: log.time
? OneUptimeDate.getDateAsLocalFormattedString(log.time)
: "",
body: typeof log.body === "string" ? log.body : "",
severity: log.severityText || "INFO",
containerName:
(attrs["resource.k8s.container.name"] as string) || "",
};
});
} catch {
return [];
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -4,14 +4,10 @@ import Navigation from "Common/UI/Utils/Navigation";
import KubernetesCluster from "Common/Models/DatabaseModels/KubernetesCluster";
import Card from "Common/UI/Components/Card/Card";
import InfoCard from "Common/UI/Components/InfoCard/InfoCard";
import MetricView from "../../../Components/Metrics/MetricView";
import MetricViewData from "Common/Types/Metrics/MetricViewData";
import MetricQueryConfigData, {
ChartSeries,
} from "Common/Types/Metrics/MetricQueryConfigData";
import AggregationType from "Common/Types/BaseDatabase/AggregationType";
import OneUptimeDate from "Common/Types/Date";
import InBetween from "Common/Types/BaseDatabase/InBetween";
import React, {
Fragment,
FunctionComponent,
@@ -25,6 +21,10 @@ import PageLoader from "Common/UI/Components/Loader/PageLoader";
import ErrorMessage from "Common/UI/Components/ErrorMessage/ErrorMessage";
import { PromiseVoidFunction } from "Common/Types/FunctionTypes";
import AggregateModel from "Common/Types/BaseDatabase/AggregatedModel";
import Tabs from "Common/UI/Components/Tabs/Tabs";
import { Tab } from "Common/UI/Components/Tabs/Tab";
import KubernetesMetricsTab from "../../../Components/Kubernetes/KubernetesMetricsTab";
import KubernetesLogsTab from "../../../Components/Kubernetes/KubernetesLogsTab";
const KubernetesClusterContainerDetail: FunctionComponent<
PageComponentProps
@@ -36,16 +36,6 @@ const KubernetesClusterContainerDetail: FunctionComponent<
const [isLoading, setIsLoading] = useState<boolean>(true);
const [error, setError] = useState<string>("");
const endDate: Date = OneUptimeDate.getCurrentDate();
const startDate: Date = OneUptimeDate.addRemoveHours(endDate, -6);
const startAndEndDate: InBetween<Date> = new InBetween(startDate, endDate);
const [metricViewData, setMetricViewData] = useState<MetricViewData>({
startAndEndDate: startAndEndDate,
queryConfigs: [],
formulaConfigs: [],
});
const fetchCluster: PromiseVoidFunction = async (): Promise<void> => {
setIsLoading(true);
try {
@@ -89,7 +79,8 @@ const KubernetesClusterContainerDetail: FunctionComponent<
const attributes: Record<string, unknown> =
(data["attributes"] as Record<string, unknown>) || {};
const name: string =
(attributes["resource.k8s.container.name"] as string) || "Unknown Container";
(attributes["resource.k8s.container.name"] as string) ||
"Unknown Container";
return { title: name };
};
@@ -143,32 +134,55 @@ const KubernetesClusterContainerDetail: FunctionComponent<
getSeries: getSeries,
};
const tabs: Array<Tab> = [
{
name: "Overview",
children: (
<div className="p-4">
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
<InfoCard title="Container Name" value={containerName || "Unknown"} />
<InfoCard title="Cluster" value={clusterIdentifier} />
</div>
</div>
),
},
{
name: "Logs",
children: (
<Card title="Container Logs" description="Logs for this container from the last 6 hours.">
<KubernetesLogsTab
clusterIdentifier={clusterIdentifier}
podName=""
containerName={containerName}
/>
</Card>
),
},
{
name: "Metrics",
children: (
<Card
title={`Container Metrics: ${containerName}`}
description="CPU and memory usage for this container over the last 6 hours."
>
<KubernetesMetricsTab
queryConfigs={[cpuQuery, memoryQuery]}
/>
</Card>
),
},
];
return (
<Fragment>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4 mb-5">
<InfoCard title="Container" value={containerName || "Unknown"} />
<InfoCard title="Cluster" value={clusterIdentifier} />
<div className="mb-5">
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4 mb-5">
<InfoCard title="Container" value={containerName || "Unknown"} />
<InfoCard title="Cluster" value={clusterIdentifier} />
</div>
</div>
<Card
title={`Container Metrics: ${containerName}`}
description="CPU and memory usage for this container over the last 6 hours."
>
<MetricView
data={{
...metricViewData,
queryConfigs: [cpuQuery, memoryQuery],
}}
hideQueryElements={true}
onChange={(data: MetricViewData) => {
setMetricViewData({
...data,
queryConfigs: [cpuQuery, memoryQuery],
formulaConfigs: [],
});
}}
/>
</Card>
<Tabs tabs={tabs} onTabChange={() => {}} />
</Fragment>
);
};

View File

@@ -4,14 +4,10 @@ import Navigation from "Common/UI/Utils/Navigation";
import KubernetesCluster from "Common/Models/DatabaseModels/KubernetesCluster";
import Card from "Common/UI/Components/Card/Card";
import InfoCard from "Common/UI/Components/InfoCard/InfoCard";
import MetricView from "../../../Components/Metrics/MetricView";
import MetricViewData from "Common/Types/Metrics/MetricViewData";
import MetricQueryConfigData, {
ChartSeries,
} from "Common/Types/Metrics/MetricQueryConfigData";
import AggregationType from "Common/Types/BaseDatabase/AggregationType";
import OneUptimeDate from "Common/Types/Date";
import InBetween from "Common/Types/BaseDatabase/InBetween";
import React, {
Fragment,
FunctionComponent,
@@ -25,6 +21,13 @@ import PageLoader from "Common/UI/Components/Loader/PageLoader";
import ErrorMessage from "Common/UI/Components/ErrorMessage/ErrorMessage";
import { PromiseVoidFunction } from "Common/Types/FunctionTypes";
import AggregateModel from "Common/Types/BaseDatabase/AggregatedModel";
import Tabs from "Common/UI/Components/Tabs/Tabs";
import { Tab } from "Common/UI/Components/Tabs/Tab";
import KubernetesOverviewTab from "../../../Components/Kubernetes/KubernetesOverviewTab";
import KubernetesEventsTab from "../../../Components/Kubernetes/KubernetesEventsTab";
import KubernetesMetricsTab from "../../../Components/Kubernetes/KubernetesMetricsTab";
import { KubernetesCronJobObject } from "../Utils/KubernetesObjectParser";
import { fetchLatestK8sObject } from "../Utils/KubernetesObjectFetcher";
const KubernetesClusterCronJobDetail: FunctionComponent<
PageComponentProps
@@ -35,16 +38,9 @@ const KubernetesClusterCronJobDetail: FunctionComponent<
const [cluster, setCluster] = useState<KubernetesCluster | null>(null);
const [isLoading, setIsLoading] = useState<boolean>(true);
const [error, setError] = useState<string>("");
const endDate: Date = OneUptimeDate.getCurrentDate();
const startDate: Date = OneUptimeDate.addRemoveHours(endDate, -6);
const startAndEndDate: InBetween<Date> = new InBetween(startDate, endDate);
const [metricViewData, setMetricViewData] = useState<MetricViewData>({
startAndEndDate: startAndEndDate,
queryConfigs: [],
formulaConfigs: [],
});
const [cronJobObject, setCronJobObject] =
useState<KubernetesCronJobObject | null>(null);
const [isLoadingObject, setIsLoadingObject] = useState<boolean>(true);
const fetchCluster: PromiseVoidFunction = async (): Promise<void> => {
setIsLoading(true);
@@ -69,6 +65,32 @@ const KubernetesClusterCronJobDetail: FunctionComponent<
});
}, []);
// Fetch the K8s cronjob object for overview tab
useEffect(() => {
if (!cluster?.clusterIdentifier) {
return;
}
const fetchCronJobObject: () => Promise<void> =
async (): Promise<void> => {
setIsLoadingObject(true);
try {
const obj: KubernetesCronJobObject | null =
await fetchLatestK8sObject<KubernetesCronJobObject>({
clusterIdentifier: cluster.clusterIdentifier || "",
resourceType: "cronjobs",
resourceName: cronJobName,
});
setCronJobObject(obj);
} catch {
// Graceful degradation — overview tab shows empty state
}
setIsLoadingObject(false);
};
fetchCronJobObject().catch(() => {});
}, [cluster?.clusterIdentifier, cronJobName]);
if (isLoading) {
return <PageLoader isVisible={true} />;
}
@@ -143,32 +165,104 @@ const KubernetesClusterCronJobDetail: FunctionComponent<
getSeries: getSeries,
};
// Build overview summary fields from cronjob object
const summaryFields: Array<{ title: string; value: string | ReactElement }> =
[
{ title: "CronJob Name", value: cronJobName },
{ title: "Cluster", value: clusterIdentifier },
];
if (cronJobObject) {
summaryFields.push(
{
title: "Namespace",
value: cronJobObject.metadata.namespace || "default",
},
{
title: "Schedule",
value: cronJobObject.spec.schedule || "N/A",
},
{
title: "Suspend",
value: cronJobObject.spec.suspend ? "Yes" : "No",
},
{
title: "Concurrency Policy",
value: cronJobObject.spec.concurrencyPolicy || "N/A",
},
{
title: "Successful Jobs History Limit",
value: String(cronJobObject.spec.successfulJobsHistoryLimit ?? "N/A"),
},
{
title: "Failed Jobs History Limit",
value: String(cronJobObject.spec.failedJobsHistoryLimit ?? "N/A"),
},
{
title: "Last Schedule Time",
value: cronJobObject.status.lastScheduleTime || "N/A",
},
{
title: "Active Jobs",
value: String(cronJobObject.status.activeCount ?? 0),
},
{
title: "Created",
value: cronJobObject.metadata.creationTimestamp || "N/A",
},
);
}
const tabs: Array<Tab> = [
{
name: "Overview",
children: (
<KubernetesOverviewTab
summaryFields={summaryFields}
labels={cronJobObject?.metadata.labels || {}}
annotations={cronJobObject?.metadata.annotations || {}}
isLoading={isLoadingObject}
/>
),
},
{
name: "Events",
children: (
<Card title="CronJob Events" description="Kubernetes events for this cronjob in the last 24 hours.">
<KubernetesEventsTab
clusterIdentifier={clusterIdentifier}
resourceKind="CronJob"
resourceName={cronJobName}
namespace={cronJobObject?.metadata.namespace}
/>
</Card>
),
},
{
name: "Metrics",
children: (
<Card
title={`CronJob Metrics: ${cronJobName}`}
description="CPU and memory usage for pods in this cronjob over the last 6 hours."
>
<KubernetesMetricsTab
queryConfigs={[cpuQuery, memoryQuery]}
/>
</Card>
),
},
];
return (
<Fragment>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4 mb-5">
<InfoCard title="CronJob" value={cronJobName || "Unknown"} />
<InfoCard title="Cluster" value={clusterIdentifier} />
<div className="mb-5">
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4 mb-5">
<InfoCard title="CronJob Name" value={cronJobName || "Unknown"} />
<InfoCard title="Cluster" value={clusterIdentifier} />
</div>
</div>
<Card
title={`CronJob Metrics: ${cronJobName}`}
description="CPU and memory usage for pods in this cronjob over the last 6 hours."
>
<MetricView
data={{
...metricViewData,
queryConfigs: [cpuQuery, memoryQuery],
}}
hideQueryElements={true}
onChange={(data: MetricViewData) => {
setMetricViewData({
...data,
queryConfigs: [cpuQuery, memoryQuery],
formulaConfigs: [],
});
}}
/>
</Card>
<Tabs tabs={tabs} onTabChange={() => {}} />
</Fragment>
);
};

View File

@@ -4,14 +4,10 @@ import Navigation from "Common/UI/Utils/Navigation";
import KubernetesCluster from "Common/Models/DatabaseModels/KubernetesCluster";
import Card from "Common/UI/Components/Card/Card";
import InfoCard from "Common/UI/Components/InfoCard/InfoCard";
import MetricView from "../../../Components/Metrics/MetricView";
import MetricViewData from "Common/Types/Metrics/MetricViewData";
import MetricQueryConfigData, {
ChartSeries,
} from "Common/Types/Metrics/MetricQueryConfigData";
import AggregationType from "Common/Types/BaseDatabase/AggregationType";
import OneUptimeDate from "Common/Types/Date";
import InBetween from "Common/Types/BaseDatabase/InBetween";
import React, {
Fragment,
FunctionComponent,
@@ -25,6 +21,13 @@ import PageLoader from "Common/UI/Components/Loader/PageLoader";
import ErrorMessage from "Common/UI/Components/ErrorMessage/ErrorMessage";
import { PromiseVoidFunction } from "Common/Types/FunctionTypes";
import AggregateModel from "Common/Types/BaseDatabase/AggregatedModel";
import Tabs from "Common/UI/Components/Tabs/Tabs";
import { Tab } from "Common/UI/Components/Tabs/Tab";
import KubernetesOverviewTab from "../../../Components/Kubernetes/KubernetesOverviewTab";
import KubernetesEventsTab from "../../../Components/Kubernetes/KubernetesEventsTab";
import KubernetesMetricsTab from "../../../Components/Kubernetes/KubernetesMetricsTab";
import { KubernetesDaemonSetObject } from "../Utils/KubernetesObjectParser";
import { fetchLatestK8sObject } from "../Utils/KubernetesObjectFetcher";
const KubernetesClusterDaemonSetDetail: FunctionComponent<
PageComponentProps
@@ -35,16 +38,9 @@ const KubernetesClusterDaemonSetDetail: FunctionComponent<
const [cluster, setCluster] = useState<KubernetesCluster | null>(null);
const [isLoading, setIsLoading] = useState<boolean>(true);
const [error, setError] = useState<string>("");
const endDate: Date = OneUptimeDate.getCurrentDate();
const startDate: Date = OneUptimeDate.addRemoveHours(endDate, -6);
const startAndEndDate: InBetween<Date> = new InBetween(startDate, endDate);
const [metricViewData, setMetricViewData] = useState<MetricViewData>({
startAndEndDate: startAndEndDate,
queryConfigs: [],
formulaConfigs: [],
});
const [objectData, setObjectData] =
useState<KubernetesDaemonSetObject | null>(null);
const [isLoadingObject, setIsLoadingObject] = useState<boolean>(true);
const fetchCluster: PromiseVoidFunction = async (): Promise<void> => {
setIsLoading(true);
@@ -69,6 +65,31 @@ const KubernetesClusterDaemonSetDetail: FunctionComponent<
});
}, []);
// Fetch the K8s daemonset object for overview tab
useEffect(() => {
if (!cluster?.clusterIdentifier) {
return;
}
const fetchObject: () => Promise<void> = async (): Promise<void> => {
setIsLoadingObject(true);
try {
const obj: KubernetesDaemonSetObject | null =
await fetchLatestK8sObject<KubernetesDaemonSetObject>({
clusterIdentifier: cluster.clusterIdentifier || "",
resourceType: "daemonsets",
resourceName: daemonSetName,
});
setObjectData(obj);
} catch {
// Graceful degradation — overview tab shows empty state
}
setIsLoadingObject(false);
};
fetchObject().catch(() => {});
}, [cluster?.clusterIdentifier, daemonSetName]);
if (isLoading) {
return <PageLoader isVisible={true} />;
}
@@ -143,32 +164,99 @@ const KubernetesClusterDaemonSetDetail: FunctionComponent<
getSeries: getSeries,
};
// Build overview summary fields from daemonset object
const summaryFields: Array<{ title: string; value: string | ReactElement }> =
[
{ title: "Name", value: daemonSetName },
{ title: "Cluster", value: clusterIdentifier },
];
if (objectData) {
summaryFields.push(
{
title: "Namespace",
value: objectData.metadata.namespace || "default",
},
{
title: "Desired Scheduled",
value: String(objectData.status.desiredNumberScheduled ?? "N/A"),
},
{
title: "Current Scheduled",
value: String(objectData.status.currentNumberScheduled ?? "N/A"),
},
{
title: "Number Ready",
value: String(objectData.status.numberReady ?? "N/A"),
},
{
title: "Number Available",
value: String(objectData.status.numberAvailable ?? "N/A"),
},
{
title: "Update Strategy",
value: objectData.spec.updateStrategy || "N/A",
},
{
title: "Created",
value: objectData.metadata.creationTimestamp || "N/A",
},
);
}
const tabs: Array<Tab> = [
{
name: "Overview",
children: (
<KubernetesOverviewTab
summaryFields={summaryFields}
labels={objectData?.metadata.labels || {}}
annotations={objectData?.metadata.annotations || {}}
isLoading={isLoadingObject}
/>
),
},
{
name: "Events",
children: (
<Card
title="DaemonSet Events"
description="Kubernetes events for this daemonset in the last 24 hours."
>
<KubernetesEventsTab
clusterIdentifier={clusterIdentifier}
resourceKind="DaemonSet"
resourceName={daemonSetName}
namespace={objectData?.metadata.namespace}
/>
</Card>
),
},
{
name: "Metrics",
children: (
<Card
title={`DaemonSet Metrics: ${daemonSetName}`}
description="CPU and memory usage for pods in this daemonset over the last 6 hours."
>
<KubernetesMetricsTab
queryConfigs={[cpuQuery, memoryQuery]}
/>
</Card>
),
},
];
return (
<Fragment>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4 mb-5">
<InfoCard title="DaemonSet" value={daemonSetName || "Unknown"} />
<InfoCard title="Cluster" value={clusterIdentifier} />
<div className="mb-5">
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4 mb-5">
<InfoCard title="DaemonSet" value={daemonSetName || "Unknown"} />
<InfoCard title="Cluster" value={clusterIdentifier} />
</div>
</div>
<Card
title={`DaemonSet Metrics: ${daemonSetName}`}
description="CPU and memory usage for pods in this daemonset over the last 6 hours."
>
<MetricView
data={{
...metricViewData,
queryConfigs: [cpuQuery, memoryQuery],
}}
hideQueryElements={true}
onChange={(data: MetricViewData) => {
setMetricViewData({
...data,
queryConfigs: [cpuQuery, memoryQuery],
formulaConfigs: [],
});
}}
/>
</Card>
<Tabs tabs={tabs} onTabChange={() => {}} />
</Fragment>
);
};

View File

@@ -4,14 +4,10 @@ import Navigation from "Common/UI/Utils/Navigation";
import KubernetesCluster from "Common/Models/DatabaseModels/KubernetesCluster";
import Card from "Common/UI/Components/Card/Card";
import InfoCard from "Common/UI/Components/InfoCard/InfoCard";
import MetricView from "../../../Components/Metrics/MetricView";
import MetricViewData from "Common/Types/Metrics/MetricViewData";
import MetricQueryConfigData, {
ChartSeries,
} from "Common/Types/Metrics/MetricQueryConfigData";
import AggregationType from "Common/Types/BaseDatabase/AggregationType";
import OneUptimeDate from "Common/Types/Date";
import InBetween from "Common/Types/BaseDatabase/InBetween";
import React, {
Fragment,
FunctionComponent,
@@ -25,6 +21,13 @@ import PageLoader from "Common/UI/Components/Loader/PageLoader";
import ErrorMessage from "Common/UI/Components/ErrorMessage/ErrorMessage";
import { PromiseVoidFunction } from "Common/Types/FunctionTypes";
import AggregateModel from "Common/Types/BaseDatabase/AggregatedModel";
import Tabs from "Common/UI/Components/Tabs/Tabs";
import { Tab } from "Common/UI/Components/Tabs/Tab";
import KubernetesOverviewTab from "../../../Components/Kubernetes/KubernetesOverviewTab";
import KubernetesEventsTab from "../../../Components/Kubernetes/KubernetesEventsTab";
import KubernetesMetricsTab from "../../../Components/Kubernetes/KubernetesMetricsTab";
import { KubernetesDeploymentObject } from "../Utils/KubernetesObjectParser";
import { fetchLatestK8sObject } from "../Utils/KubernetesObjectFetcher";
const KubernetesClusterDeploymentDetail: FunctionComponent<
PageComponentProps
@@ -35,16 +38,9 @@ const KubernetesClusterDeploymentDetail: FunctionComponent<
const [cluster, setCluster] = useState<KubernetesCluster | null>(null);
const [isLoading, setIsLoading] = useState<boolean>(true);
const [error, setError] = useState<string>("");
const endDate: Date = OneUptimeDate.getCurrentDate();
const startDate: Date = OneUptimeDate.addRemoveHours(endDate, -6);
const startAndEndDate: InBetween<Date> = new InBetween(startDate, endDate);
const [metricViewData, setMetricViewData] = useState<MetricViewData>({
startAndEndDate: startAndEndDate,
queryConfigs: [],
formulaConfigs: [],
});
const [objectData, setObjectData] =
useState<KubernetesDeploymentObject | null>(null);
const [isLoadingObject, setIsLoadingObject] = useState<boolean>(true);
const fetchCluster: PromiseVoidFunction = async (): Promise<void> => {
setIsLoading(true);
@@ -69,6 +65,31 @@ const KubernetesClusterDeploymentDetail: FunctionComponent<
});
}, []);
// Fetch the K8s deployment object for overview tab
useEffect(() => {
if (!cluster?.clusterIdentifier) {
return;
}
const fetchObject: () => Promise<void> = async (): Promise<void> => {
setIsLoadingObject(true);
try {
const obj: KubernetesDeploymentObject | null =
await fetchLatestK8sObject<KubernetesDeploymentObject>({
clusterIdentifier: cluster.clusterIdentifier || "",
resourceType: "deployments",
resourceName: deploymentName,
});
setObjectData(obj);
} catch {
// Graceful degradation — overview tab shows empty state
}
setIsLoadingObject(false);
};
fetchObject().catch(() => {});
}, [cluster?.clusterIdentifier, deploymentName]);
if (isLoading) {
return <PageLoader isVisible={true} />;
}
@@ -143,32 +164,96 @@ const KubernetesClusterDeploymentDetail: FunctionComponent<
getSeries: getSeries,
};
// Build overview summary fields from deployment object
const summaryFields: Array<{ title: string; value: string | ReactElement }> =
[
{ title: "Name", value: deploymentName },
{ title: "Cluster", value: clusterIdentifier },
];
if (objectData) {
summaryFields.push(
{
title: "Namespace",
value: objectData.metadata.namespace || "default",
},
{
title: "Replicas",
value: String(objectData.spec.replicas ?? "N/A"),
},
{
title: "Ready Replicas",
value: String(objectData.status.readyReplicas ?? "N/A"),
},
{
title: "Available Replicas",
value: String(objectData.status.availableReplicas ?? "N/A"),
},
{
title: "Strategy",
value: objectData.spec.strategy || "N/A",
},
{
title: "Created",
value: objectData.metadata.creationTimestamp || "N/A",
},
);
}
const tabs: Array<Tab> = [
{
name: "Overview",
children: (
<KubernetesOverviewTab
summaryFields={summaryFields}
labels={objectData?.metadata.labels || {}}
annotations={objectData?.metadata.annotations || {}}
conditions={objectData?.status.conditions}
isLoading={isLoadingObject}
/>
),
},
{
name: "Events",
children: (
<Card
title="Deployment Events"
description="Kubernetes events for this deployment in the last 24 hours."
>
<KubernetesEventsTab
clusterIdentifier={clusterIdentifier}
resourceKind="Deployment"
resourceName={deploymentName}
namespace={objectData?.metadata.namespace}
/>
</Card>
),
},
{
name: "Metrics",
children: (
<Card
title={`Deployment Metrics: ${deploymentName}`}
description="CPU and memory usage for pods in this deployment over the last 6 hours."
>
<KubernetesMetricsTab
queryConfigs={[cpuQuery, memoryQuery]}
/>
</Card>
),
},
];
return (
<Fragment>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4 mb-5">
<InfoCard title="Deployment" value={deploymentName || "Unknown"} />
<InfoCard title="Cluster" value={clusterIdentifier} />
<div className="mb-5">
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4 mb-5">
<InfoCard title="Deployment" value={deploymentName || "Unknown"} />
<InfoCard title="Cluster" value={clusterIdentifier} />
</div>
</div>
<Card
title={`Deployment Metrics: ${deploymentName}`}
description="CPU and memory usage for pods in this deployment over the last 6 hours."
>
<MetricView
data={{
...metricViewData,
queryConfigs: [cpuQuery, memoryQuery],
}}
hideQueryElements={true}
onChange={(data: MetricViewData) => {
setMetricViewData({
...data,
queryConfigs: [cpuQuery, memoryQuery],
formulaConfigs: [],
});
}}
/>
</Card>
<Tabs tabs={tabs} onTabChange={() => {}} />
</Fragment>
);
};

View File

@@ -24,16 +24,11 @@ import ErrorMessage from "Common/UI/Components/ErrorMessage/ErrorMessage";
import { PromiseVoidFunction } from "Common/Types/FunctionTypes";
import { JSONObject } from "Common/Types/JSON";
import InBetween from "Common/Types/BaseDatabase/InBetween";
interface KubernetesEvent {
timestamp: string;
type: string;
reason: string;
objectKind: string;
objectName: string;
namespace: string;
message: string;
}
import {
getKvValue,
getKvStringValue,
} from "../Utils/KubernetesObjectParser";
import { KubernetesEvent } from "../Utils/KubernetesObjectFetcher";
const KubernetesClusterEvents: FunctionComponent<
PageComponentProps
@@ -65,13 +60,18 @@ const KubernetesClusterEvents: FunctionComponent<
const endDate: Date = OneUptimeDate.getCurrentDate();
const startDate: Date = OneUptimeDate.addRemoveHours(endDate, -24);
const listResult: ListResult<Log> = await AnalyticsModelAPI.getList<Log>({
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const eventsQueryOptions: any = {
modelType: Log,
query: {
projectId: ProjectUtil.getCurrentProjectId()!.toString(),
time: new InBetween<Date>(startDate, endDate),
attributes: {
"logAttributes.event.domain": "k8s",
"logAttributes.k8s.resource.name": "events",
},
},
limit: 200,
limit: 500,
skip: 0,
select: {
time: true,
@@ -83,76 +83,9 @@ const KubernetesClusterEvents: FunctionComponent<
time: SortOrder.Descending,
},
requestOptions: {},
});
// Helper to extract a string value from OTLP kvlistValue
const getKvValue: (
kvList: JSONObject | undefined,
key: string,
) => string = (kvList: JSONObject | undefined, key: string): string => {
if (!kvList) {
return "";
}
const values: Array<JSONObject> | undefined = (kvList as JSONObject)[
"values"
] as Array<JSONObject> | undefined;
if (!values) {
return "";
}
for (const entry of values) {
if (entry["key"] === key) {
const val: JSONObject | undefined = entry["value"] as
| JSONObject
| undefined;
if (!val) {
return "";
}
if (val["stringValue"]) {
return val["stringValue"] as string;
}
if (val["intValue"]) {
return String(val["intValue"]);
}
// Nested kvlist (e.g., regarding, metadata)
if (val["kvlistValue"]) {
return val["kvlistValue"] as unknown as string;
}
}
}
return "";
};
// Helper to get nested kvlist value
const getNestedKvValue: (
kvList: JSONObject | undefined,
parentKey: string,
childKey: string,
) => string = (
kvList: JSONObject | undefined,
parentKey: string,
childKey: string,
): string => {
if (!kvList) {
return "";
}
const values: Array<JSONObject> | undefined = (kvList as JSONObject)[
"values"
] as Array<JSONObject> | undefined;
if (!values) {
return "";
}
for (const entry of values) {
if (entry["key"] === parentKey) {
const val: JSONObject | undefined = entry["value"] as
| JSONObject
| undefined;
if (val && val["kvlistValue"]) {
return getKvValue(val["kvlistValue"] as JSONObject, childKey);
}
}
}
return "";
};
const listResult: ListResult<Log> =
await AnalyticsModelAPI.getList<Log>(eventsQueryOptions);
const k8sEvents: Array<KubernetesEvent> = [];
@@ -167,11 +100,6 @@ const KubernetesClusterEvents: FunctionComponent<
continue;
}
// Only process k8s event logs (from k8sobjects receiver)
if (attrs["logAttributes.event.domain"] !== "k8s") {
continue;
}
// Parse the body which is OTLP kvlistValue JSON
let bodyObj: JSONObject | null = null;
try {
@@ -186,7 +114,6 @@ const KubernetesClusterEvents: FunctionComponent<
continue;
}
// The body has a top-level kvlistValue with "type" (ADDED/MODIFIED) and "object" keys
const topKvList: JSONObject | undefined = bodyObj["kvlistValue"] as
| JSONObject
| undefined;
@@ -195,25 +122,55 @@ const KubernetesClusterEvents: FunctionComponent<
}
// Get the "object" which is the actual k8s Event
const objectKvListRaw: string = getKvValue(topKvList, "object");
if (!objectKvListRaw || typeof objectKvListRaw === "string") {
const objectVal: string | JSONObject | null = getKvValue(
topKvList,
"object",
);
if (!objectVal || typeof objectVal === "string") {
continue;
}
const objectKvList: JSONObject =
objectKvListRaw as unknown as JSONObject;
const objectKvList: JSONObject = objectVal;
const eventType: string = getKvValue(objectKvList, "type") || "";
const reason: string = getKvValue(objectKvList, "reason") || "";
const note: string = getKvValue(objectKvList, "note") || "";
const eventType: string =
getKvStringValue(objectKvList, "type") || "";
const reason: string =
getKvStringValue(objectKvList, "reason") || "";
const note: string =
getKvStringValue(objectKvList, "note") || "";
// Get regarding object details using shared parser
const regardingKv: string | JSONObject | null = getKvValue(
objectKvList,
"regarding",
);
const regardingObj: JSONObject | undefined =
regardingKv && typeof regardingKv !== "string"
? regardingKv
: undefined;
const objectKind: string = regardingObj
? getKvStringValue(regardingObj, "kind")
: "";
const objectName: string = regardingObj
? getKvStringValue(regardingObj, "name")
: "";
const metadataKv: string | JSONObject | null = getKvValue(
objectKvList,
"metadata",
);
const metadataObj: JSONObject | undefined =
metadataKv && typeof metadataKv !== "string"
? metadataKv
: undefined;
// Get object details from "regarding" sub-object
const objectKind: string =
getNestedKvValue(objectKvList, "regarding", "kind") || "";
const objectName: string =
getNestedKvValue(objectKvList, "regarding", "name") || "";
const namespace: string =
getNestedKvValue(objectKvList, "regarding", "namespace") ||
getNestedKvValue(objectKvList, "metadata", "namespace") ||
(regardingObj
? getKvStringValue(regardingObj, "namespace")
: "") ||
(metadataObj
? getKvStringValue(metadataObj, "namespace")
: "") ||
"";
if (eventType || reason) {
@@ -297,7 +254,10 @@ const KubernetesClusterEvents: FunctionComponent<
const isWarning: boolean =
event.type.toLowerCase() === "warning";
return (
<tr key={index} className={isWarning ? "bg-yellow-50" : ""}>
<tr
key={index}
className={isWarning ? "bg-yellow-50" : ""}
>
<td className="px-4 py-3 whitespace-nowrap text-sm text-gray-500">
{event.timestamp}
</td>

View File

@@ -4,14 +4,10 @@ import Navigation from "Common/UI/Utils/Navigation";
import KubernetesCluster from "Common/Models/DatabaseModels/KubernetesCluster";
import Card from "Common/UI/Components/Card/Card";
import InfoCard from "Common/UI/Components/InfoCard/InfoCard";
import MetricView from "../../../Components/Metrics/MetricView";
import MetricViewData from "Common/Types/Metrics/MetricViewData";
import MetricQueryConfigData, {
ChartSeries,
} from "Common/Types/Metrics/MetricQueryConfigData";
import AggregationType from "Common/Types/BaseDatabase/AggregationType";
import OneUptimeDate from "Common/Types/Date";
import InBetween from "Common/Types/BaseDatabase/InBetween";
import React, {
Fragment,
FunctionComponent,
@@ -25,6 +21,13 @@ import PageLoader from "Common/UI/Components/Loader/PageLoader";
import ErrorMessage from "Common/UI/Components/ErrorMessage/ErrorMessage";
import { PromiseVoidFunction } from "Common/Types/FunctionTypes";
import AggregateModel from "Common/Types/BaseDatabase/AggregatedModel";
import Tabs from "Common/UI/Components/Tabs/Tabs";
import { Tab } from "Common/UI/Components/Tabs/Tab";
import KubernetesOverviewTab from "../../../Components/Kubernetes/KubernetesOverviewTab";
import KubernetesEventsTab from "../../../Components/Kubernetes/KubernetesEventsTab";
import KubernetesMetricsTab from "../../../Components/Kubernetes/KubernetesMetricsTab";
import { KubernetesJobObject } from "../Utils/KubernetesObjectParser";
import { fetchLatestK8sObject } from "../Utils/KubernetesObjectFetcher";
const KubernetesClusterJobDetail: FunctionComponent<
PageComponentProps
@@ -35,16 +38,8 @@ const KubernetesClusterJobDetail: FunctionComponent<
const [cluster, setCluster] = useState<KubernetesCluster | null>(null);
const [isLoading, setIsLoading] = useState<boolean>(true);
const [error, setError] = useState<string>("");
const endDate: Date = OneUptimeDate.getCurrentDate();
const startDate: Date = OneUptimeDate.addRemoveHours(endDate, -6);
const startAndEndDate: InBetween<Date> = new InBetween(startDate, endDate);
const [metricViewData, setMetricViewData] = useState<MetricViewData>({
startAndEndDate: startAndEndDate,
queryConfigs: [],
formulaConfigs: [],
});
const [jobObject, setJobObject] = useState<KubernetesJobObject | null>(null);
const [isLoadingObject, setIsLoadingObject] = useState<boolean>(true);
const fetchCluster: PromiseVoidFunction = async (): Promise<void> => {
setIsLoading(true);
@@ -69,6 +64,31 @@ const KubernetesClusterJobDetail: FunctionComponent<
});
}, []);
// Fetch the K8s job object for overview tab
useEffect(() => {
if (!cluster?.clusterIdentifier) {
return;
}
const fetchJobObject: () => Promise<void> = async (): Promise<void> => {
setIsLoadingObject(true);
try {
const obj: KubernetesJobObject | null =
await fetchLatestK8sObject<KubernetesJobObject>({
clusterIdentifier: cluster.clusterIdentifier || "",
resourceType: "jobs",
resourceName: jobName,
});
setJobObject(obj);
} catch {
// Graceful degradation — overview tab shows empty state
}
setIsLoadingObject(false);
};
fetchJobObject().catch(() => {});
}, [cluster?.clusterIdentifier, jobName]);
if (isLoading) {
return <PageLoader isVisible={true} />;
}
@@ -143,32 +163,109 @@ const KubernetesClusterJobDetail: FunctionComponent<
getSeries: getSeries,
};
// Build overview summary fields from job object
const summaryFields: Array<{ title: string; value: string | ReactElement }> =
[
{ title: "Job Name", value: jobName },
{ title: "Cluster", value: clusterIdentifier },
];
if (jobObject) {
summaryFields.push(
{
title: "Namespace",
value: jobObject.metadata.namespace || "default",
},
{
title: "Completions",
value: String(jobObject.spec.completions ?? "N/A"),
},
{
title: "Parallelism",
value: String(jobObject.spec.parallelism ?? "N/A"),
},
{
title: "Backoff Limit",
value: String(jobObject.spec.backoffLimit ?? "N/A"),
},
{
title: "Active",
value: String(jobObject.status.active ?? 0),
},
{
title: "Succeeded",
value: String(jobObject.status.succeeded ?? 0),
},
{
title: "Failed",
value: String(jobObject.status.failed ?? 0),
},
{
title: "Start Time",
value: jobObject.status.startTime || "N/A",
},
{
title: "Completion Time",
value: jobObject.status.completionTime || "N/A",
},
{
title: "Created",
value: jobObject.metadata.creationTimestamp || "N/A",
},
);
}
const tabs: Array<Tab> = [
{
name: "Overview",
children: (
<KubernetesOverviewTab
summaryFields={summaryFields}
labels={jobObject?.metadata.labels || {}}
annotations={jobObject?.metadata.annotations || {}}
conditions={jobObject?.status.conditions}
isLoading={isLoadingObject}
/>
),
},
{
name: "Events",
children: (
<Card title="Job Events" description="Kubernetes events for this job in the last 24 hours.">
<KubernetesEventsTab
clusterIdentifier={clusterIdentifier}
resourceKind="Job"
resourceName={jobName}
namespace={jobObject?.metadata.namespace}
/>
</Card>
),
},
{
name: "Metrics",
children: (
<Card
title={`Job Metrics: ${jobName}`}
description="CPU and memory usage for pods in this job over the last 6 hours."
>
<KubernetesMetricsTab
queryConfigs={[cpuQuery, memoryQuery]}
/>
</Card>
),
},
];
return (
<Fragment>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4 mb-5">
<InfoCard title="Job" value={jobName || "Unknown"} />
<InfoCard title="Cluster" value={clusterIdentifier} />
<div className="mb-5">
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4 mb-5">
<InfoCard title="Job Name" value={jobName || "Unknown"} />
<InfoCard title="Cluster" value={clusterIdentifier} />
</div>
</div>
<Card
title={`Job Metrics: ${jobName}`}
description="CPU and memory usage for pods in this job over the last 6 hours."
>
<MetricView
data={{
...metricViewData,
queryConfigs: [cpuQuery, memoryQuery],
}}
hideQueryElements={true}
onChange={(data: MetricViewData) => {
setMetricViewData({
...data,
queryConfigs: [cpuQuery, memoryQuery],
formulaConfigs: [],
});
}}
/>
</Card>
<Tabs tabs={tabs} onTabChange={() => {}} />
</Fragment>
);
};

View File

@@ -4,14 +4,10 @@ import Navigation from "Common/UI/Utils/Navigation";
import KubernetesCluster from "Common/Models/DatabaseModels/KubernetesCluster";
import Card from "Common/UI/Components/Card/Card";
import InfoCard from "Common/UI/Components/InfoCard/InfoCard";
import MetricView from "../../../Components/Metrics/MetricView";
import MetricViewData from "Common/Types/Metrics/MetricViewData";
import MetricQueryConfigData, {
ChartSeries,
} from "Common/Types/Metrics/MetricQueryConfigData";
import AggregationType from "Common/Types/BaseDatabase/AggregationType";
import OneUptimeDate from "Common/Types/Date";
import InBetween from "Common/Types/BaseDatabase/InBetween";
import React, {
Fragment,
FunctionComponent,
@@ -25,6 +21,13 @@ import PageLoader from "Common/UI/Components/Loader/PageLoader";
import ErrorMessage from "Common/UI/Components/ErrorMessage/ErrorMessage";
import { PromiseVoidFunction } from "Common/Types/FunctionTypes";
import AggregateModel from "Common/Types/BaseDatabase/AggregatedModel";
import Tabs from "Common/UI/Components/Tabs/Tabs";
import { Tab } from "Common/UI/Components/Tabs/Tab";
import KubernetesOverviewTab from "../../../Components/Kubernetes/KubernetesOverviewTab";
import KubernetesEventsTab from "../../../Components/Kubernetes/KubernetesEventsTab";
import KubernetesMetricsTab from "../../../Components/Kubernetes/KubernetesMetricsTab";
import { KubernetesNamespaceObject } from "../Utils/KubernetesObjectParser";
import { fetchLatestK8sObject } from "../Utils/KubernetesObjectFetcher";
const KubernetesClusterNamespaceDetail: FunctionComponent<
PageComponentProps
@@ -35,16 +38,9 @@ const KubernetesClusterNamespaceDetail: FunctionComponent<
const [cluster, setCluster] = useState<KubernetesCluster | null>(null);
const [isLoading, setIsLoading] = useState<boolean>(true);
const [error, setError] = useState<string>("");
const endDate: Date = OneUptimeDate.getCurrentDate();
const startDate: Date = OneUptimeDate.addRemoveHours(endDate, -6);
const startAndEndDate: InBetween<Date> = new InBetween(startDate, endDate);
const [metricViewData, setMetricViewData] = useState<MetricViewData>({
startAndEndDate: startAndEndDate,
queryConfigs: [],
formulaConfigs: [],
});
const [namespaceObject, setNamespaceObject] =
useState<KubernetesNamespaceObject | null>(null);
const [isLoadingObject, setIsLoadingObject] = useState<boolean>(true);
const fetchCluster: PromiseVoidFunction = async (): Promise<void> => {
setIsLoading(true);
@@ -69,6 +65,32 @@ const KubernetesClusterNamespaceDetail: FunctionComponent<
});
}, []);
// Fetch the K8s namespace object for overview tab
useEffect(() => {
if (!cluster?.clusterIdentifier) {
return;
}
const fetchNamespaceObject: () => Promise<void> =
async (): Promise<void> => {
setIsLoadingObject(true);
try {
const obj: KubernetesNamespaceObject | null =
await fetchLatestK8sObject<KubernetesNamespaceObject>({
clusterIdentifier: cluster.clusterIdentifier || "",
resourceType: "namespaces",
resourceName: namespaceName,
});
setNamespaceObject(obj);
} catch {
// Graceful degradation — overview tab shows empty state
}
setIsLoadingObject(false);
};
fetchNamespaceObject().catch(() => {});
}, [cluster?.clusterIdentifier, namespaceName]);
if (isLoading) {
return <PageLoader isVisible={true} />;
}
@@ -143,32 +165,75 @@ const KubernetesClusterNamespaceDetail: FunctionComponent<
getSeries: getSeries,
};
// Build overview summary fields from namespace object
const summaryFields: Array<{ title: string; value: string | ReactElement }> =
[
{ title: "Namespace Name", value: namespaceName },
{ title: "Cluster", value: clusterIdentifier },
];
if (namespaceObject) {
summaryFields.push(
{
title: "Status Phase",
value: namespaceObject.status.phase || "N/A",
},
{
title: "Created",
value: namespaceObject.metadata.creationTimestamp || "N/A",
},
);
}
const tabs: Array<Tab> = [
{
name: "Overview",
children: (
<KubernetesOverviewTab
summaryFields={summaryFields}
labels={namespaceObject?.metadata.labels || {}}
annotations={namespaceObject?.metadata.annotations || {}}
isLoading={isLoadingObject}
/>
),
},
{
name: "Events",
children: (
<Card title="Namespace Events" description="Kubernetes events for this namespace in the last 24 hours.">
<KubernetesEventsTab
clusterIdentifier={clusterIdentifier}
resourceKind="Namespace"
resourceName={namespaceName}
/>
</Card>
),
},
{
name: "Metrics",
children: (
<Card
title={`Namespace Metrics: ${namespaceName}`}
description="CPU and memory usage for pods in this namespace over the last 6 hours."
>
<KubernetesMetricsTab
queryConfigs={[cpuQuery, memoryQuery]}
/>
</Card>
),
},
];
return (
<Fragment>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4 mb-5">
<InfoCard title="Namespace" value={namespaceName || "Unknown"} />
<InfoCard title="Cluster" value={clusterIdentifier} />
<div className="mb-5">
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4 mb-5">
<InfoCard title="Namespace" value={namespaceName || "Unknown"} />
<InfoCard title="Cluster" value={clusterIdentifier} />
</div>
</div>
<Card
title={`Namespace Metrics: ${namespaceName}`}
description="CPU and memory usage for pods in this namespace over the last 6 hours."
>
<MetricView
data={{
...metricViewData,
queryConfigs: [cpuQuery, memoryQuery],
}}
hideQueryElements={true}
onChange={(data: MetricViewData) => {
setMetricViewData({
...data,
queryConfigs: [cpuQuery, memoryQuery],
formulaConfigs: [],
});
}}
/>
</Card>
<Tabs tabs={tabs} onTabChange={() => {}} />
</Fragment>
);
};

View File

@@ -4,12 +4,8 @@ import Navigation from "Common/UI/Utils/Navigation";
import KubernetesCluster from "Common/Models/DatabaseModels/KubernetesCluster";
import Card from "Common/UI/Components/Card/Card";
import InfoCard from "Common/UI/Components/InfoCard/InfoCard";
import MetricView from "../../../Components/Metrics/MetricView";
import MetricViewData from "Common/Types/Metrics/MetricViewData";
import MetricQueryConfigData from "Common/Types/Metrics/MetricQueryConfigData";
import AggregationType from "Common/Types/BaseDatabase/AggregationType";
import OneUptimeDate from "Common/Types/Date";
import InBetween from "Common/Types/BaseDatabase/InBetween";
import React, {
Fragment,
FunctionComponent,
@@ -22,6 +18,13 @@ import API from "Common/UI/Utils/API/API";
import PageLoader from "Common/UI/Components/Loader/PageLoader";
import ErrorMessage from "Common/UI/Components/ErrorMessage/ErrorMessage";
import { PromiseVoidFunction } from "Common/Types/FunctionTypes";
import Tabs from "Common/UI/Components/Tabs/Tabs";
import { Tab } from "Common/UI/Components/Tabs/Tab";
import KubernetesOverviewTab from "../../../Components/Kubernetes/KubernetesOverviewTab";
import KubernetesEventsTab from "../../../Components/Kubernetes/KubernetesEventsTab";
import KubernetesMetricsTab from "../../../Components/Kubernetes/KubernetesMetricsTab";
import { KubernetesNodeObject } from "../Utils/KubernetesObjectParser";
import { fetchLatestK8sObject } from "../Utils/KubernetesObjectFetcher";
const KubernetesClusterNodeDetail: FunctionComponent<
PageComponentProps
@@ -32,16 +35,10 @@ const KubernetesClusterNodeDetail: FunctionComponent<
const [cluster, setCluster] = useState<KubernetesCluster | null>(null);
const [isLoading, setIsLoading] = useState<boolean>(true);
const [error, setError] = useState<string>("");
const endDate: Date = OneUptimeDate.getCurrentDate();
const startDate: Date = OneUptimeDate.addRemoveHours(endDate, -6);
const startAndEndDate: InBetween<Date> = new InBetween(startDate, endDate);
const [metricViewData, setMetricViewData] = useState<MetricViewData>({
startAndEndDate: startAndEndDate,
queryConfigs: [],
formulaConfigs: [],
});
const [nodeObject, setNodeObject] = useState<KubernetesNodeObject | null>(
null,
);
const [isLoadingObject, setIsLoadingObject] = useState<boolean>(true);
const fetchCluster: PromiseVoidFunction = async (): Promise<void> => {
setIsLoading(true);
@@ -66,6 +63,31 @@ const KubernetesClusterNodeDetail: FunctionComponent<
});
}, []);
// Fetch the K8s node object for overview tab
useEffect(() => {
if (!cluster?.clusterIdentifier) {
return;
}
const fetchNodeObject: () => Promise<void> = async (): Promise<void> => {
setIsLoadingObject(true);
try {
const obj: KubernetesNodeObject | null =
await fetchLatestK8sObject<KubernetesNodeObject>({
clusterIdentifier: cluster.clusterIdentifier || "",
resourceType: "nodes",
resourceName: nodeName,
});
setNodeObject(obj);
} catch {
// Graceful degradation — overview tab shows empty state
}
setIsLoadingObject(false);
};
fetchNodeObject().catch(() => {});
}, [cluster?.clusterIdentifier, nodeName]);
if (isLoading) {
return <PageLoader isVisible={true} />;
}
@@ -200,44 +222,144 @@ const KubernetesClusterNodeDetail: FunctionComponent<
},
};
return (
<Fragment>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4 mb-5">
<InfoCard title="Node Name" value={nodeName || "Unknown"} />
<InfoCard title="Cluster" value={clusterIdentifier} />
</div>
// Determine node status from conditions
const getNodeStatus: () => { label: string; isReady: boolean } = (): {
label: string;
isReady: boolean;
} => {
if (!nodeObject) {
return { label: "Unknown", isReady: false };
}
const readyCondition = nodeObject.status.conditions.find(
(c) => {
return c.type === "Ready";
},
);
if (readyCondition && readyCondition.status === "True") {
return { label: "Ready", isReady: true };
}
return { label: "NotReady", isReady: false };
};
<Card
title={`Node Metrics: ${nodeName}`}
description="CPU, memory, filesystem, and network usage for this node over the last 6 hours."
>
<MetricView
data={{
...metricViewData,
queryConfigs: [
// Build overview summary fields from node object
const summaryFields: Array<{ title: string; value: string | ReactElement }> =
[
{ title: "Node Name", value: nodeName },
{ title: "Cluster", value: clusterIdentifier },
];
if (nodeObject) {
const nodeStatus = getNodeStatus();
summaryFields.push(
{
title: "Status",
value: (
<span
className={`inline-flex px-2 py-0.5 text-xs font-medium rounded ${
nodeStatus.isReady
? "bg-green-50 text-green-700"
: "bg-red-50 text-red-700"
}`}
>
{nodeStatus.label}
</span>
),
},
{
title: "OS Image",
value: nodeObject.status.nodeInfo.osImage || "N/A",
},
{
title: "Kernel",
value: nodeObject.status.nodeInfo.kernelVersion || "N/A",
},
{
title: "Container Runtime",
value: nodeObject.status.nodeInfo.containerRuntimeVersion || "N/A",
},
{
title: "Kubelet Version",
value: nodeObject.status.nodeInfo.kubeletVersion || "N/A",
},
{
title: "Architecture",
value: nodeObject.status.nodeInfo.architecture || "N/A",
},
{
title: "CPU Allocatable",
value: nodeObject.status.allocatable["cpu"] || "N/A",
},
{
title: "Memory Allocatable",
value: nodeObject.status.allocatable["memory"] || "N/A",
},
{
title: "Created",
value: nodeObject.metadata.creationTimestamp || "N/A",
},
);
}
const tabs: Array<Tab> = [
{
name: "Overview",
children: (
<KubernetesOverviewTab
summaryFields={summaryFields}
labels={nodeObject?.metadata.labels || {}}
annotations={nodeObject?.metadata.annotations || {}}
conditions={nodeObject?.status.conditions}
isLoading={isLoadingObject}
/>
),
},
{
name: "Events",
children: (
<Card
title="Node Events"
description="Kubernetes events for this node in the last 24 hours."
>
<KubernetesEventsTab
clusterIdentifier={clusterIdentifier}
resourceKind="Node"
resourceName={nodeName}
/>
</Card>
),
},
{
name: "Metrics",
children: (
<Card
title={`Node Metrics: ${nodeName}`}
description="CPU, memory, filesystem, and network usage for this node over the last 6 hours."
>
<KubernetesMetricsTab
queryConfigs={[
cpuQuery,
memoryQuery,
filesystemQuery,
networkRxQuery,
networkTxQuery,
],
}}
hideQueryElements={true}
onChange={(data: MetricViewData) => {
setMetricViewData({
...data,
queryConfigs: [
cpuQuery,
memoryQuery,
filesystemQuery,
networkRxQuery,
networkTxQuery,
],
formulaConfigs: [],
});
}}
/>
</Card>
]}
/>
</Card>
),
},
];
return (
<Fragment>
<div className="mb-5">
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4 mb-5">
<InfoCard title="Node Name" value={nodeName || "Unknown"} />
<InfoCard title="Cluster" value={clusterIdentifier} />
</div>
</div>
<Tabs tabs={tabs} onTabChange={() => {}} />
</Fragment>
);
};

View File

@@ -4,14 +4,10 @@ import Navigation from "Common/UI/Utils/Navigation";
import KubernetesCluster from "Common/Models/DatabaseModels/KubernetesCluster";
import Card from "Common/UI/Components/Card/Card";
import InfoCard from "Common/UI/Components/InfoCard/InfoCard";
import MetricView from "../../../Components/Metrics/MetricView";
import MetricViewData from "Common/Types/Metrics/MetricViewData";
import MetricQueryConfigData, {
ChartSeries,
} from "Common/Types/Metrics/MetricQueryConfigData";
import AggregationType from "Common/Types/BaseDatabase/AggregationType";
import OneUptimeDate from "Common/Types/Date";
import InBetween from "Common/Types/BaseDatabase/InBetween";
import React, {
Fragment,
FunctionComponent,
@@ -25,6 +21,15 @@ import PageLoader from "Common/UI/Components/Loader/PageLoader";
import ErrorMessage from "Common/UI/Components/ErrorMessage/ErrorMessage";
import { PromiseVoidFunction } from "Common/Types/FunctionTypes";
import AggregateModel from "Common/Types/BaseDatabase/AggregatedModel";
import Tabs from "Common/UI/Components/Tabs/Tabs";
import { Tab } from "Common/UI/Components/Tabs/Tab";
import KubernetesOverviewTab from "../../../Components/Kubernetes/KubernetesOverviewTab";
import KubernetesContainersTab from "../../../Components/Kubernetes/KubernetesContainersTab";
import KubernetesEventsTab from "../../../Components/Kubernetes/KubernetesEventsTab";
import KubernetesLogsTab from "../../../Components/Kubernetes/KubernetesLogsTab";
import KubernetesMetricsTab from "../../../Components/Kubernetes/KubernetesMetricsTab";
import { KubernetesPodObject } from "../Utils/KubernetesObjectParser";
import { fetchLatestK8sObject } from "../Utils/KubernetesObjectFetcher";
const KubernetesClusterPodDetail: FunctionComponent<
PageComponentProps
@@ -35,16 +40,8 @@ const KubernetesClusterPodDetail: FunctionComponent<
const [cluster, setCluster] = useState<KubernetesCluster | null>(null);
const [isLoading, setIsLoading] = useState<boolean>(true);
const [error, setError] = useState<string>("");
const endDate: Date = OneUptimeDate.getCurrentDate();
const startDate: Date = OneUptimeDate.addRemoveHours(endDate, -6);
const startAndEndDate: InBetween<Date> = new InBetween(startDate, endDate);
const [metricViewData, setMetricViewData] = useState<MetricViewData>({
startAndEndDate: startAndEndDate,
queryConfigs: [],
formulaConfigs: [],
});
const [podObject, setPodObject] = useState<KubernetesPodObject | null>(null);
const [isLoadingObject, setIsLoadingObject] = useState<boolean>(true);
const fetchCluster: PromiseVoidFunction = async (): Promise<void> => {
setIsLoading(true);
@@ -69,6 +66,31 @@ const KubernetesClusterPodDetail: FunctionComponent<
});
}, []);
// Fetch the K8s pod object for overview/containers tabs
useEffect(() => {
if (!cluster?.clusterIdentifier) {
return;
}
const fetchPodObject: () => Promise<void> = async (): Promise<void> => {
setIsLoadingObject(true);
try {
const obj: KubernetesPodObject | null =
await fetchLatestK8sObject<KubernetesPodObject>({
clusterIdentifier: cluster.clusterIdentifier || "",
resourceType: "pods",
resourceName: podName,
});
setPodObject(obj);
} catch {
// Graceful degradation — overview tab shows empty state
}
setIsLoadingObject(false);
};
fetchPodObject().catch(() => {});
}, [cluster?.clusterIdentifier, podName]);
if (isLoading) {
return <PageLoader isVisible={true} />;
}
@@ -89,7 +111,8 @@ const KubernetesClusterPodDetail: FunctionComponent<
const attributes: Record<string, unknown> =
(data["attributes"] as Record<string, unknown>) || {};
const containerName: string =
(attributes["resource.k8s.container.name"] as string) || "Unknown Container";
(attributes["resource.k8s.container.name"] as string) ||
"Unknown Container";
return { title: containerName };
};
@@ -191,42 +214,133 @@ const KubernetesClusterPodDetail: FunctionComponent<
},
};
// Build overview summary fields from pod object
const summaryFields: Array<{ title: string; value: string | ReactElement }> =
[
{ title: "Pod Name", value: podName },
{ title: "Cluster", value: clusterIdentifier },
];
if (podObject) {
summaryFields.push(
{
title: "Namespace",
value: podObject.metadata.namespace || "default",
},
{
title: "Status",
value: (
<span
className={`inline-flex px-2 py-0.5 text-xs font-medium rounded ${
podObject.status.phase === "Running"
? "bg-green-50 text-green-700"
: podObject.status.phase === "Succeeded"
? "bg-blue-50 text-blue-700"
: podObject.status.phase === "Failed"
? "bg-red-50 text-red-700"
: "bg-yellow-50 text-yellow-700"
}`}
>
{podObject.status.phase || "Unknown"}
</span>
),
},
{ title: "Node", value: podObject.spec.nodeName || "N/A" },
{ title: "Pod IP", value: podObject.status.podIP || "N/A" },
{ title: "Host IP", value: podObject.status.hostIP || "N/A" },
{
title: "Service Account",
value: podObject.spec.serviceAccountName || "default",
},
{
title: "Created",
value: podObject.metadata.creationTimestamp || "N/A",
},
);
}
const tabs: Array<Tab> = [
{
name: "Overview",
children: (
<KubernetesOverviewTab
summaryFields={summaryFields}
labels={podObject?.metadata.labels || {}}
annotations={podObject?.metadata.annotations || {}}
conditions={podObject?.status.conditions}
ownerReferences={podObject?.metadata.ownerReferences}
isLoading={isLoadingObject}
/>
),
},
{
name: "Containers",
children: podObject ? (
<KubernetesContainersTab
containers={podObject.spec.containers}
initContainers={podObject.spec.initContainers}
containerStatuses={podObject.status.containerStatuses}
initContainerStatuses={podObject.status.initContainerStatuses}
/>
) : isLoadingObject ? (
<PageLoader isVisible={true} />
) : (
<div className="text-gray-500 text-sm p-4">
Container details not yet available. Ensure the kubernetes-agent Helm
chart has resourceSpecs.enabled set to true.
</div>
),
},
{
name: "Events",
children: (
<Card title="Pod Events" description="Kubernetes events for this pod in the last 24 hours.">
<KubernetesEventsTab
clusterIdentifier={clusterIdentifier}
resourceKind="Pod"
resourceName={podName}
namespace={podObject?.metadata.namespace}
/>
</Card>
),
},
{
name: "Logs",
children: (
<Card title="Application Logs" description="Container logs for this pod from the last 6 hours.">
<KubernetesLogsTab
clusterIdentifier={clusterIdentifier}
podName={podName}
namespace={podObject?.metadata.namespace}
/>
</Card>
),
},
{
name: "Metrics",
children: (
<Card
title={`Pod Metrics: ${podName}`}
description="CPU, memory, and container-level resource usage for this pod over the last 6 hours."
>
<KubernetesMetricsTab
queryConfigs={[podCpuQuery, podMemoryQuery, cpuQuery, memoryQuery]}
/>
</Card>
),
},
];
return (
<Fragment>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4 mb-5">
<InfoCard title="Pod Name" value={podName || "Unknown"} />
<InfoCard title="Cluster" value={clusterIdentifier} />
<div className="mb-5">
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4 mb-5">
<InfoCard title="Pod Name" value={podName || "Unknown"} />
<InfoCard title="Cluster" value={clusterIdentifier} />
</div>
</div>
<Card
title={`Pod Metrics: ${podName}`}
description="CPU, memory, and container-level resource usage for this pod over the last 6 hours."
>
<MetricView
data={{
...metricViewData,
queryConfigs: [
podCpuQuery,
podMemoryQuery,
cpuQuery,
memoryQuery,
],
}}
hideQueryElements={true}
onChange={(data: MetricViewData) => {
setMetricViewData({
...data,
queryConfigs: [
podCpuQuery,
podMemoryQuery,
cpuQuery,
memoryQuery,
],
formulaConfigs: [],
});
}}
/>
</Card>
<Tabs tabs={tabs} onTabChange={() => {}} />
</Fragment>
);
};

View File

@@ -4,14 +4,10 @@ import Navigation from "Common/UI/Utils/Navigation";
import KubernetesCluster from "Common/Models/DatabaseModels/KubernetesCluster";
import Card from "Common/UI/Components/Card/Card";
import InfoCard from "Common/UI/Components/InfoCard/InfoCard";
import MetricView from "../../../Components/Metrics/MetricView";
import MetricViewData from "Common/Types/Metrics/MetricViewData";
import MetricQueryConfigData, {
ChartSeries,
} from "Common/Types/Metrics/MetricQueryConfigData";
import AggregationType from "Common/Types/BaseDatabase/AggregationType";
import OneUptimeDate from "Common/Types/Date";
import InBetween from "Common/Types/BaseDatabase/InBetween";
import React, {
Fragment,
FunctionComponent,
@@ -25,6 +21,13 @@ import PageLoader from "Common/UI/Components/Loader/PageLoader";
import ErrorMessage from "Common/UI/Components/ErrorMessage/ErrorMessage";
import { PromiseVoidFunction } from "Common/Types/FunctionTypes";
import AggregateModel from "Common/Types/BaseDatabase/AggregatedModel";
import Tabs from "Common/UI/Components/Tabs/Tabs";
import { Tab } from "Common/UI/Components/Tabs/Tab";
import KubernetesOverviewTab from "../../../Components/Kubernetes/KubernetesOverviewTab";
import KubernetesEventsTab from "../../../Components/Kubernetes/KubernetesEventsTab";
import KubernetesMetricsTab from "../../../Components/Kubernetes/KubernetesMetricsTab";
import { KubernetesStatefulSetObject } from "../Utils/KubernetesObjectParser";
import { fetchLatestK8sObject } from "../Utils/KubernetesObjectFetcher";
const KubernetesClusterStatefulSetDetail: FunctionComponent<
PageComponentProps
@@ -35,16 +38,9 @@ const KubernetesClusterStatefulSetDetail: FunctionComponent<
const [cluster, setCluster] = useState<KubernetesCluster | null>(null);
const [isLoading, setIsLoading] = useState<boolean>(true);
const [error, setError] = useState<string>("");
const endDate: Date = OneUptimeDate.getCurrentDate();
const startDate: Date = OneUptimeDate.addRemoveHours(endDate, -6);
const startAndEndDate: InBetween<Date> = new InBetween(startDate, endDate);
const [metricViewData, setMetricViewData] = useState<MetricViewData>({
startAndEndDate: startAndEndDate,
queryConfigs: [],
formulaConfigs: [],
});
const [objectData, setObjectData] =
useState<KubernetesStatefulSetObject | null>(null);
const [isLoadingObject, setIsLoadingObject] = useState<boolean>(true);
const fetchCluster: PromiseVoidFunction = async (): Promise<void> => {
setIsLoading(true);
@@ -69,6 +65,31 @@ const KubernetesClusterStatefulSetDetail: FunctionComponent<
});
}, []);
// Fetch the K8s statefulset object for overview tab
useEffect(() => {
if (!cluster?.clusterIdentifier) {
return;
}
const fetchObject: () => Promise<void> = async (): Promise<void> => {
setIsLoadingObject(true);
try {
const obj: KubernetesStatefulSetObject | null =
await fetchLatestK8sObject<KubernetesStatefulSetObject>({
clusterIdentifier: cluster.clusterIdentifier || "",
resourceType: "statefulsets",
resourceName: statefulSetName,
});
setObjectData(obj);
} catch {
// Graceful degradation — overview tab shows empty state
}
setIsLoadingObject(false);
};
fetchObject().catch(() => {});
}, [cluster?.clusterIdentifier, statefulSetName]);
if (isLoading) {
return <PageLoader isVisible={true} />;
}
@@ -143,32 +164,102 @@ const KubernetesClusterStatefulSetDetail: FunctionComponent<
getSeries: getSeries,
};
// Build overview summary fields from statefulset object
const summaryFields: Array<{ title: string; value: string | ReactElement }> =
[
{ title: "Name", value: statefulSetName },
{ title: "Cluster", value: clusterIdentifier },
];
if (objectData) {
summaryFields.push(
{
title: "Namespace",
value: objectData.metadata.namespace || "default",
},
{
title: "Replicas",
value: String(objectData.spec.replicas ?? "N/A"),
},
{
title: "Ready Replicas",
value: String(objectData.status.readyReplicas ?? "N/A"),
},
{
title: "Service Name",
value: objectData.spec.serviceName || "N/A",
},
{
title: "Pod Management Policy",
value: objectData.spec.podManagementPolicy || "N/A",
},
{
title: "Update Strategy",
value: objectData.spec.updateStrategy || "N/A",
},
{
title: "Created",
value: objectData.metadata.creationTimestamp || "N/A",
},
);
}
const tabs: Array<Tab> = [
{
name: "Overview",
children: (
<KubernetesOverviewTab
summaryFields={summaryFields}
labels={objectData?.metadata.labels || {}}
annotations={objectData?.metadata.annotations || {}}
isLoading={isLoadingObject}
/>
),
},
{
name: "Events",
children: (
<Card
title="StatefulSet Events"
description="Kubernetes events for this statefulset in the last 24 hours."
>
<KubernetesEventsTab
clusterIdentifier={clusterIdentifier}
resourceKind="StatefulSet"
resourceName={statefulSetName}
namespace={objectData?.metadata.namespace}
/>
</Card>
),
},
{
name: "Metrics",
children: (
<Card
title={`StatefulSet Metrics: ${statefulSetName}`}
description="CPU and memory usage for pods in this statefulset over the last 6 hours."
>
<KubernetesMetricsTab
queryConfigs={[cpuQuery, memoryQuery]}
/>
</Card>
),
},
];
return (
<Fragment>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4 mb-5">
<InfoCard title="StatefulSet" value={statefulSetName || "Unknown"} />
<InfoCard title="Cluster" value={clusterIdentifier} />
<div className="mb-5">
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4 mb-5">
<InfoCard
title="StatefulSet"
value={statefulSetName || "Unknown"}
/>
<InfoCard title="Cluster" value={clusterIdentifier} />
</div>
</div>
<Card
title={`StatefulSet Metrics: ${statefulSetName}`}
description="CPU and memory usage for pods in this statefulset over the last 6 hours."
>
<MetricView
data={{
...metricViewData,
queryConfigs: [cpuQuery, memoryQuery],
}}
hideQueryElements={true}
onChange={(data: MetricViewData) => {
setMetricViewData({
...data,
queryConfigs: [cpuQuery, memoryQuery],
formulaConfigs: [],
});
}}
/>
</Card>
<Tabs tabs={tabs} onTabChange={() => {}} />
</Fragment>
);
};

View File

@@ -32,6 +32,38 @@ data:
- name: events
mode: watch
group: events.k8s.io
{{- if .Values.resourceSpecs.enabled }}
# Pull full resource specs for dashboard detail views
- name: pods
mode: pull
interval: {{ .Values.resourceSpecs.interval }}
- name: nodes
mode: pull
interval: {{ .Values.resourceSpecs.interval }}
- name: namespaces
mode: pull
interval: {{ .Values.resourceSpecs.interval }}
- name: deployments
mode: pull
interval: {{ .Values.resourceSpecs.interval }}
group: apps
- name: statefulsets
mode: pull
interval: {{ .Values.resourceSpecs.interval }}
group: apps
- name: daemonsets
mode: pull
interval: {{ .Values.resourceSpecs.interval }}
group: apps
- name: jobs
mode: pull
interval: {{ .Values.resourceSpecs.interval }}
group: batch
- name: cronjobs
mode: pull
interval: {{ .Values.resourceSpecs.interval }}
group: batch
{{- end }}
{{- if .Values.controlPlane.enabled }}
# Scrape control plane metrics via Prometheus endpoints

View File

@@ -70,6 +70,13 @@ logs:
cpu: 200m
memory: 256Mi
# Resource spec collection — pulls full K8s resource objects (pods, nodes,
# deployments, etc.) for displaying labels, annotations, env vars, status,
# and other details in the dashboard.
resourceSpecs:
enabled: true
interval: 300s # How often to pull full resource specs (default: 5 minutes)
# Collection intervals
collectionInterval: 30s

View File

@@ -1222,6 +1222,27 @@ const HomeFeatureSet: FeatureSet = {
},
);
app.get(
"/product/scheduled-maintenance",
(_req: ExpressRequest, res: ExpressResponse) => {
const seo: PageSEOData & { fullCanonicalUrl: string } = getSEOForPath(
"/product/scheduled-maintenance",
res.locals["homeUrl"] as string,
);
res.render(`${ViewsPath}/scheduled-maintenance`, {
enableGoogleTagManager: IsBillingEnabled,
seo,
});
},
);
app.get(
"/scheduled-maintenance",
(_req: ExpressRequest, res: ExpressResponse) => {
res.redirect("/product/scheduled-maintenance");
},
);
app.get("/product/traces", (_req: ExpressRequest, res: ExpressResponse) => {
const seo: PageSEOData & { fullCanonicalUrl: string } = getSEOForPath(
"/product/traces",

View File

@@ -438,6 +438,43 @@ export const PageSEOConfig: Record<string, PageSEOData> = {
},
},
"/product/scheduled-maintenance": {
title:
"Scheduled Maintenance | Plan & Communicate Downtime | OneUptime",
description:
"Plan, schedule, and communicate maintenance windows to your users. Notify subscribers automatically, update status pages in real-time. Open source maintenance management.",
canonicalPath: "/product/scheduled-maintenance",
twitterCard: "summary_large_image",
pageType: "product",
breadcrumbs: [
{ name: "Home", url: "/" },
{ name: "Products", url: "/#products" },
{
name: "Scheduled Maintenance",
url: "/product/scheduled-maintenance",
},
],
softwareApplication: {
name: "OneUptime Scheduled Maintenance",
applicationCategory: "DeveloperApplication",
operatingSystem: "Web, Cloud",
description:
"Schedule maintenance windows, notify subscribers via email, SMS, and webhooks, and keep your status page updated in real-time.",
features: [
"Maintenance scheduling",
"Subscriber notifications",
"Status page integration",
"Custom maintenance states",
"Email and SMS alerts",
"Webhook integrations",
"Slack and Teams notifications",
"Maintenance timeline",
"Affected monitors tracking",
"Automatic state transitions",
],
},
},
"/product/traces": {
title: "Distributed Tracing | End-to-End Request Tracing | OneUptime",
description:

View File

@@ -37,6 +37,7 @@ const PAGE_CONFIG: Record<string, SitemapPageConfig> = {
"/product/workflows": { priority: 0.9, changefreq: "weekly" },
"/product/dashboards": { priority: 0.9, changefreq: "weekly" },
"/product/ai-agent": { priority: 0.9, changefreq: "weekly" },
"/product/scheduled-maintenance": { priority: 0.9, changefreq: "weekly" },
// Teams (Solutions) pages
"/solutions/devops": { priority: 0.8, changefreq: "weekly" },

View File

@@ -0,0 +1,10 @@
<!-- Scheduled Maintenance - Teal -->
<div class="hero-card-wrapper hero-glow-teal-wrapper h-full">
<a href="/product/scheduled-maintenance" class="hero-card hero-glow-teal group flex flex-col items-center justify-center text-center rounded-2xl bg-white px-4 py-5 ring-1 ring-inset ring-gray-200 transition-all hover:ring-teal-300 h-full">
<div class="flex h-11 w-11 items-center justify-center rounded-xl bg-teal-50 ring-1 ring-teal-200">
<%- include('../icons/scheduled-maintenance') %>
</div>
<div class="mt-3 text-sm font-medium text-gray-900">Maintenance</div>
<div class="mt-1 text-xs text-gray-500">Plan &amp; communicate downtime</div>
</a>
</div>

View File

@@ -1,5 +1,2 @@
<!-- Kubernetes Icon - Helm wheel / K8s logo style -->
<svg class="<%= typeof iconClass !== 'undefined' ? iconClass : 'h-5 w-5' %> text-cyan-600" fill="none" stroke="currentColor" stroke-width="1.5" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" d="M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z"></path>
<circle cx="12" cy="12" r="3" stroke-linecap="round" stroke-linejoin="round"></circle>
</svg>
<!-- Kubernetes Icon - Official K8s logo -->
<img src="/img/kubernetes.svg" alt="Kubernetes" class="<%= typeof iconClass !== 'undefined' ? iconClass : 'h-5 w-5' %>">

Before

Width:  |  Height:  |  Size: 476 B

After

Width:  |  Height:  |  Size: 166 B

View File

@@ -0,0 +1,4 @@
<!-- Scheduled Maintenance Icon - Calendar with wrench -->
<svg class="<%= typeof iconClass !== 'undefined' ? iconClass : 'h-5 w-5' %> text-teal-600" fill="none" stroke="currentColor" stroke-width="1.5" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" d="M6.75 3v2.25M17.25 3v2.25M3 18.75V7.5a2.25 2.25 0 012.25-2.25h13.5A2.25 2.25 0 0121 7.5v11.25m-18 0A2.25 2.25 0 005.25 21h13.5A2.25 2.25 0 0021 18.75m-18 0v-7.5A2.25 2.25 0 015.25 9h13.5A2.25 2.25 0 0121 11.25v7.5m-9-6h.008v.008H12v-.008zM12 15h.008v.008H12V15zm0 2.25h.008v.008H12v-.008zM9.75 15h.008v.008H9.75V15zm0 2.25h.008v.008H9.75v-.008zM7.5 15h.008v.008H7.5V15zm0 2.25h.008v.008H7.5v-.008zm6.75-4.5h.008v.008h-.008v-.008zm0 2.25h.008v.008h-.008V15zm0 2.25h.008v.008h-.008v-.008zm2.25-4.5h.008v.008H16.5v-.008zm0 2.25h.008v.008H16.5V15z"></path>
</svg>

After

Width:  |  Height:  |  Size: 842 B

View File

@@ -1062,6 +1062,193 @@
</div>
</div>
<!-- Team Notifications Section -->
<div class="relative overflow-hidden bg-white py-24 sm:py-32">
<div class="mx-auto max-w-7xl px-6 lg:px-8">
<div class="lg:grid lg:grid-cols-2 lg:gap-16 lg:items-center">
<!-- Content -->
<div class="relative">
<p class="text-sm font-medium text-cyan-600 uppercase tracking-wide mb-3">Team Notifications</p>
<h2 class="text-3xl font-semibold tracking-tight text-gray-900 sm:text-4xl">
Kubernetes alerts where your team already works
</h2>
<p class="mt-4 text-lg text-gray-600 leading-relaxed">
Get instant notifications for pod crashes, node issues, and deployment failures in Slack and Microsoft Teams. Acknowledge and investigate without leaving your chat app.
</p>
<!-- Integration cards -->
<div class="mt-10 space-y-3">
<!-- Slack -->
<div class="flex items-center gap-4 p-4 rounded-xl bg-gray-50 border border-gray-100">
<div class="flex h-10 w-10 items-center justify-center rounded-lg bg-white shadow-sm ring-1 ring-gray-200">
<svg class="h-5 w-5" viewBox="0 0 24 24" fill="none">
<path d="M5.042 15.165a2.528 2.528 0 0 1-2.52 2.523A2.528 2.528 0 0 1 0 15.165a2.527 2.527 0 0 1 2.522-2.52h2.52v2.52zM6.313 15.165a2.527 2.527 0 0 1 2.521-2.52 2.527 2.527 0 0 1 2.521 2.52v6.313A2.528 2.528 0 0 1 8.834 24a2.528 2.528 0 0 1-2.521-2.522v-6.313z" fill="#E01E5A"/>
<path d="M8.834 5.042a2.528 2.528 0 0 1-2.521-2.52A2.528 2.528 0 0 1 8.834 0a2.528 2.528 0 0 1 2.521 2.522v2.52H8.834zM8.834 6.313a2.528 2.528 0 0 1 2.521 2.521 2.528 2.528 0 0 1-2.521 2.521H2.522A2.528 2.528 0 0 1 0 8.834a2.528 2.528 0 0 1 2.522-2.521h6.312z" fill="#36C5F0"/>
<path d="M18.956 8.834a2.528 2.528 0 0 1 2.522-2.521A2.528 2.528 0 0 1 24 8.834a2.528 2.528 0 0 1-2.522 2.521h-2.522V8.834zM17.688 8.834a2.528 2.528 0 0 1-2.523 2.521 2.527 2.527 0 0 1-2.52-2.521V2.522A2.527 2.527 0 0 1 15.165 0a2.528 2.528 0 0 1 2.523 2.522v6.312z" fill="#2EB67D"/>
<path d="M15.165 18.956a2.528 2.528 0 0 1 2.523 2.522A2.528 2.528 0 0 1 15.165 24a2.527 2.527 0 0 1-2.52-2.522v-2.522h2.52zM15.165 17.688a2.527 2.527 0 0 1-2.52-2.523 2.526 2.526 0 0 1 2.52-2.52h6.313A2.527 2.527 0 0 1 24 15.165a2.528 2.528 0 0 1-2.522 2.523h-6.313z" fill="#ECB22E"/>
</svg>
</div>
<div class="flex-1">
<h3 class="text-sm font-semibold text-gray-900">Slack</h3>
<p class="text-sm text-gray-500">Interactive Kubernetes alert actions</p>
</div>
</div>
<!-- Microsoft Teams -->
<div class="flex items-center gap-4 p-4 rounded-xl bg-gray-50 border border-gray-100">
<div class="flex h-10 w-10 items-center justify-center rounded-lg bg-white shadow-sm ring-1 ring-gray-200">
<svg class="h-5 w-5" viewBox="0 0 24 24" fill="none">
<path d="M20.16 7.8h-4.736c.103.317.159.654.159 1.005v7.029a3.588 3.588 0 01-3.583 3.583h-4.8v.777c0 .998.81 1.806 1.806 1.806h8.339l2.869 1.912a.452.452 0 00.703-.376V21.2h.646c.997 0 1.806-.809 1.806-1.806v-9.788A1.806 1.806 0 0020.16 7.8z" fill="#5059C9"/>
<circle cx="18.5" cy="4.5" r="2.5" fill="#5059C9"/>
<path d="M13.5 6H3.806A1.806 1.806 0 002 7.806v9.388c0 .997.81 1.806 1.806 1.806h2.611v3.336a.452.452 0 00.703.376L10.5 20h3a3.5 3.5 0 003.5-3.5v-7A3.5 3.5 0 0013.5 6z" fill="#7B83EB"/>
<circle cx="10" cy="3" r="3" fill="#7B83EB"/>
<path d="M6 11h6M6 14h4" stroke="white" stroke-width="1.5" stroke-linecap="round"/>
</svg>
</div>
<div class="flex-1">
<h3 class="text-sm font-semibold text-gray-900">Microsoft Teams</h3>
<p class="text-sm text-gray-500">Native adaptive cards integration</p>
</div>
</div>
</div>
<!-- Features list -->
<div class="mt-8 grid grid-cols-2 gap-4">
<div class="flex items-center gap-2 text-sm text-gray-600">
<svg class="h-4 w-4 text-green-500" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7" />
</svg>
Real-time delivery
</div>
<div class="flex items-center gap-2 text-sm text-gray-600">
<svg class="h-4 w-4 text-green-500" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7" />
</svg>
Pod crash alerts
</div>
<div class="flex items-center gap-2 text-sm text-gray-600">
<svg class="h-4 w-4 text-green-500" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7" />
</svg>
Rich formatting
</div>
<div class="flex items-center gap-2 text-sm text-gray-600">
<svg class="h-4 w-4 text-green-500" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7" />
</svg>
Action buttons
</div>
</div>
</div>
<!-- Slack Demo -->
<div class="mt-12 lg:mt-0 relative">
<!-- Decorative gradient background -->
<div class="absolute -inset-4 bg-gray-100/50 rounded-3xl blur-2xl"></div>
<div class="relative rounded-2xl bg-[#1a1d21] shadow-2xl ring-1 ring-white/10 overflow-hidden">
<!-- Slack Window Header -->
<div class="bg-[#0f1114] px-4 py-3 flex items-center justify-between border-b border-white/5">
<div class="flex items-center gap-2">
<div class="flex gap-1.5">
<div class="w-3 h-3 rounded-full bg-[#ff5f57]"></div>
<div class="w-3 h-3 rounded-full bg-[#febc2e]"></div>
<div class="w-3 h-3 rounded-full bg-[#28c840]"></div>
</div>
</div>
<div class="flex items-center gap-2">
<svg class="h-4 w-4 text-white/60" viewBox="0 0 24 24" fill="currentColor">
<path d="M5.042 15.165a2.528 2.528 0 0 1-2.52 2.523A2.528 2.528 0 0 1 0 15.165a2.527 2.527 0 0 1 2.522-2.52h2.52v2.52zM6.313 15.165a2.527 2.527 0 0 1 2.521-2.52 2.527 2.527 0 0 1 2.521 2.52v6.313A2.528 2.528 0 0 1 8.834 24a2.528 2.528 0 0 1-2.521-2.522v-6.313zM8.834 5.042a2.528 2.528 0 0 1-2.521-2.52A2.528 2.528 0 0 1 8.834 0a2.528 2.528 0 0 1 2.521 2.522v2.52H8.834zM8.834 6.313a2.528 2.528 0 0 1 2.521 2.521 2.528 2.528 0 0 1-2.521 2.521H2.522A2.528 2.528 0 0 1 0 8.834a2.528 2.528 0 0 1 2.522-2.521h6.312zM18.956 8.834a2.528 2.528 0 0 1 2.522-2.521A2.528 2.528 0 0 1 24 8.834a2.528 2.528 0 0 1-2.522 2.521h-2.522V8.834zM17.688 8.834a2.528 2.528 0 0 1-2.523 2.521 2.527 2.527 0 0 1-2.52-2.521V2.522A2.527 2.527 0 0 1 15.165 0a2.528 2.528 0 0 1 2.523 2.522v6.312zM15.165 18.956a2.528 2.528 0 0 1 2.523 2.522A2.528 2.528 0 0 1 15.165 24a2.527 2.527 0 0 1-2.52-2.522v-2.522h2.52zM15.165 17.688a2.527 2.527 0 0 1-2.52-2.523 2.526 2.526 0 0 1 2.52-2.52h6.313A2.527 2.527 0 0 1 24 15.165a2.528 2.528 0 0 1-2.522 2.523h-6.313z"/>
</svg>
<span class="text-white/60 text-sm font-medium">#kubernetes-alerts</span>
</div>
<div class="w-16"></div>
</div>
<!-- Slack Messages Area -->
<div class="p-4 space-y-4 min-h-[320px]" id="kubernetes-slack-messages">
<!-- Main Alert Message -->
<div class="flex gap-3" style="animation: fadeSlideIn 0.5s ease-out forwards;">
<div class="h-10 w-10 rounded-lg bg-gradient-to-br from-cyan-500 to-blue-600 flex items-center justify-center flex-shrink-0 shadow-lg">
<svg class="h-6 w-6 text-white" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path stroke-linecap="round" stroke-linejoin="round" d="M20 7l-8-4-8 4m16 0l-8 4m8-4v10l-8 4m0-10L4 7m8 4v10M4 7v10l8 4" />
</svg>
</div>
<div class="flex-1 min-w-0">
<div class="flex items-baseline gap-2">
<span class="font-bold text-white text-sm">OneUptime Kubernetes</span>
<span class="text-xs text-white/40">2:47 PM</span>
</div>
<!-- Alert Card -->
<div class="mt-2 rounded-lg bg-[#0f1114] border border-white/10 overflow-hidden">
<div class="p-3 border-l-4 border-red-500">
<div class="flex items-center gap-2 mb-2">
<span class="inline-flex items-center px-2 py-0.5 rounded text-xs font-bold bg-red-500/20 text-red-400 ring-1 ring-red-500/30">CRITICAL</span>
<span class="text-white/40 text-xs">Pod CrashLoopBackOff</span>
</div>
<p class="text-white font-semibold">payment-service-6d8f9 restarting</p>
<div class="mt-2 flex items-center gap-4 text-xs text-white/50">
<span class="flex items-center gap-1">
<svg class="h-3.5 w-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
<path stroke-linecap="round" stroke-linejoin="round" d="M20 7l-8-4-8 4m16 0l-8 4m8-4v10l-8 4m0-10L4 7m8 4v10M4 7v10l8 4" />
</svg>
ns: production
</span>
<span class="flex items-center gap-1">
<svg class="h-3.5 w-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
<path stroke-linecap="round" stroke-linejoin="round" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
</svg>
Restarts: 5
</span>
</div>
<!-- Action Buttons -->
<div class="mt-3 flex gap-2">
<button class="px-3 py-1.5 rounded text-xs font-semibold bg-white/10 text-white hover:bg-white/20 transition-colors border border-white/10">
Acknowledge
</button>
<button class="px-3 py-1.5 rounded text-xs font-semibold bg-cyan-600 text-white hover:bg-cyan-700 transition-colors">
View Pod Logs
</button>
<button class="px-3 py-1.5 rounded text-xs font-semibold bg-white/10 text-white hover:bg-white/20 transition-colors border border-white/10">
Resolve
</button>
</div>
</div>
</div>
<!-- Reactions -->
<div class="mt-2 flex items-center gap-1">
<span class="inline-flex items-center gap-1 px-2 py-0.5 rounded-full bg-white/5 text-xs border border-white/10 hover:bg-white/10 cursor-pointer transition-colors">
<span>👀</span>
<span class="text-white/60">3</span>
</span>
<span class="inline-flex items-center gap-1 px-2 py-0.5 rounded-full bg-white/5 text-xs border border-white/10 hover:bg-white/10 cursor-pointer transition-colors">
<span>🚨</span>
<span class="text-white/60">2</span>
</span>
</div>
</div>
</div>
<!-- Thread Replies Container -->
<div id="kubernetes-thread-container" class="ml-12 space-y-3 hidden">
</div>
</div>
<!-- Message Input Area -->
<div class="px-4 py-3 border-t border-white/5">
<div class="flex items-center gap-2 px-3 py-2 rounded-lg bg-[#0f1114] border border-white/10">
<svg class="h-5 w-5 text-white/30" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
<path stroke-linecap="round" stroke-linejoin="round" d="M12 6v6m0 0v6m0-6h6m-6 0H6" />
</svg>
<span class="text-white/30 text-sm">Message #kubernetes-alerts</span>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<%- include('./Partials/enterprise-ready') -%>
<%- include('features-table') -%>
<%- include('cta') -%>
@@ -1070,6 +1257,19 @@
<%- include('footer') -%>
<%- include('./Partials/video-script') -%>
<style>
@keyframes fadeSlideIn {
from {
opacity: 0;
transform: translateY(10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
</style>
<!-- Grid glow cursor effect -->
<script>
(function() {

View File

@@ -109,6 +109,17 @@
<p class="text-xs text-gray-500">Smart routing & escalations</p>
</div>
</a>
<!-- Scheduled Maintenance - Teal -->
<a href="/product/scheduled-maintenance" class="group flex items-center gap-3 rounded-lg p-2.5 transition-colors hover:bg-teal-50">
<div class="flex h-9 w-9 flex-shrink-0 items-center justify-center rounded-lg bg-teal-50 ring-1 ring-teal-200 group-hover:bg-teal-100 group-hover:ring-teal-300">
<%- include('./Partials/icons/scheduled-maintenance', {iconClass: 'h-4 w-4'}) %>
</div>
<div>
<p class="text-sm font-medium text-gray-900">Scheduled Maintenance</p>
<p class="text-xs text-gray-500">Plan & communicate downtime</p>
</div>
</a>
</div>
</div>
@@ -775,6 +786,14 @@
<span class="text-sm font-medium text-gray-900">On-Call</span>
</a>
<!-- Scheduled Maintenance - Teal -->
<a href="/product/scheduled-maintenance" class="flex items-center gap-3 rounded-xl p-3 hover:bg-teal-50 ring-1 ring-transparent hover:ring-teal-200">
<div class="flex h-9 w-9 flex-shrink-0 items-center justify-center rounded-lg bg-teal-50 ring-1 ring-teal-200">
<%- include('./Partials/icons/scheduled-maintenance', {iconClass: 'h-4 w-4'}) %>
</div>
<span class="text-sm font-medium text-gray-900">Maintenance</span>
</a>
<!-- Logs - Amber -->
<a href="/product/logs-management" class="flex items-center gap-3 rounded-xl p-3 hover:bg-amber-50 ring-1 ring-transparent hover:ring-amber-200">
<div class="flex h-9 w-9 flex-shrink-0 items-center justify-center rounded-lg bg-amber-50 ring-1 ring-amber-200">

View File

@@ -0,0 +1,704 @@
<!DOCTYPE html>
<html lang="en" id="home">
<meta http-equiv="content-type" content="text/html;charset=utf-8" />
<head>
<title>OneUptime | Scheduled Maintenance - Plan & Communicate Downtime</title>
<meta name="description"
content="Plan, schedule, and communicate maintenance windows to your users. Notify subscribers automatically, update status pages in real-time, and keep stakeholders informed. Open source.">
<%- include('head', {
enableGoogleTagManager: typeof enableGoogleTagManager !== 'undefined' ? enableGoogleTagManager : false
}) -%>
</head>
<body>
<%- include('nav') -%>
<main id="main-content">
<!-- Hero Section -->
<div class="relative isolate overflow-hidden bg-white" id="maintenance-hero-section">
<!-- Subtle grid pattern background -->
<div class="absolute inset-0 -z-10 h-full w-full bg-white bg-[linear-gradient(to_right,#f0f0f0_1px,transparent_1px),linear-gradient(to_bottom,#f0f0f0_1px,transparent_1px)] bg-[size:4rem_4rem] [mask-image:radial-gradient(ellipse_60%_50%_at_50%_0%,#000_70%,transparent_110%)]"></div>
<!-- Grid glow effect that follows cursor - teal color -->
<div id="maintenance-grid-glow" class="absolute inset-0 -z-9 pointer-events-none" style="opacity: 0; transition: opacity 0.3s ease-out; background: linear-gradient(to right, rgba(20, 184, 166, 0.3) 1px, transparent 1px), linear-gradient(to bottom, rgba(20, 184, 166, 0.3) 1px, transparent 1px); background-size: 4rem 4rem; -webkit-mask-image: radial-gradient(circle 250px at var(--mouse-x, 50%) var(--mouse-y, 50%), black 0%, transparent 100%); mask-image: radial-gradient(circle 250px at var(--mouse-x, 50%) var(--mouse-y, 50%), black 0%, transparent 100%);"></div>
<div class="py-20 sm:py-28 lg:py-32">
<div class="mx-auto max-w-7xl px-6 lg:px-8">
<div class="mx-auto max-w-3xl text-center">
<!-- Minimal badge -->
<p class="text-sm font-medium text-teal-600 mb-4">Keep users informed during planned downtime</p>
<h1 class="text-4xl font-semibold tracking-tight text-gray-900 sm:text-5xl lg:text-6xl">
Scheduled maintenance, done right
</h1>
<p class="mt-6 text-lg leading-8 text-gray-600 max-w-2xl mx-auto">
Plan maintenance windows, notify subscribers automatically, and update your status page in real-time. Keep your users informed every step of the way.
</p>
<div class="mt-10 flex items-center justify-center gap-x-6">
<a href="/accounts/register"
class="rounded-lg bg-gray-900 px-5 py-2.5 text-sm font-semibold text-white shadow-sm hover:bg-gray-800 transition-colors">
Get started
</a>
<a href="/enterprise/demo" class="text-sm font-semibold text-gray-900 hover:text-gray-600 transition-colors">
Request demo <span aria-hidden="true">&rarr;</span>
</a>
</div>
<!-- Subtle feature list -->
<div class="mt-12 flex flex-wrap items-center justify-center gap-x-8 gap-y-3 text-sm text-gray-500">
<span>Advance scheduling</span>
<span class="hidden sm:inline text-gray-300">|</span>
<span>Subscriber notifications</span>
<span class="hidden sm:inline text-gray-300">|</span>
<span>Status page integration</span>
<span class="hidden sm:inline text-gray-300">|</span>
<span>Custom states</span>
</div>
</div>
<!-- Hero visual - Scheduled Maintenance dashboard mockup -->
<div class="mt-16 sm:mt-20">
<div class="relative mx-auto max-w-5xl">
<div class="rounded-xl bg-gray-900/5 p-1.5 ring-1 ring-gray-900/10">
<!-- Maintenance Dashboard Mockup -->
<div class="rounded-lg bg-white shadow-lg overflow-hidden">
<!-- Header bar -->
<div class="bg-gray-50 px-6 py-3 flex items-center justify-between border-b border-gray-200">
<div class="flex items-center gap-3">
<div class="flex gap-1.5">
<div class="w-3 h-3 rounded-full bg-red-400"></div>
<div class="w-3 h-3 rounded-full bg-yellow-400"></div>
<div class="w-3 h-3 rounded-full bg-green-400"></div>
</div>
<div class="flex items-center gap-2">
<svg class="h-4 w-4 text-teal-600" fill="none" stroke="currentColor" stroke-width="1.5" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" d="M6.75 3v2.25M17.25 3v2.25M3 18.75V7.5a2.25 2.25 0 012.25-2.25h13.5A2.25 2.25 0 0121 7.5v11.25m-18 0A2.25 2.25 0 005.25 21h13.5A2.25 2.25 0 0021 18.75m-18 0v-7.5A2.25 2.25 0 015.25 9h13.5A2.25 2.25 0 0121 11.25v7.5"></path>
</svg>
<span class="text-sm text-gray-700 font-semibold">Scheduled Maintenance</span>
</div>
</div>
<div class="flex items-center gap-3">
<button class="flex items-center gap-1.5 px-3 py-1.5 bg-teal-600 rounded-md text-xs font-medium text-white hover:bg-teal-700 transition-colors">
<svg class="h-3.5 w-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
<path stroke-linecap="round" stroke-linejoin="round" d="M12 4.5v15m7.5-7.5h-15" />
</svg>
Schedule Maintenance
</button>
</div>
</div>
<!-- Dashboard content -->
<div class="p-5">
<!-- Top stats row -->
<div class="grid grid-cols-4 gap-3 mb-5">
<div class="bg-white rounded-lg p-3.5 border border-gray-200 shadow-sm">
<div class="flex items-center justify-between mb-2">
<div class="text-[10px] font-medium text-gray-400 uppercase tracking-wider">Scheduled</div>
<div class="w-6 h-6 rounded-md bg-blue-50 flex items-center justify-center">
<svg class="w-3.5 h-3.5 text-blue-600" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
<path stroke-linecap="round" stroke-linejoin="round" d="M12 6v6h4.5m4.5 0a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
</div>
</div>
<div class="text-xl font-bold text-gray-900">3</div>
<div class="flex items-center gap-1 mt-1">
<span class="text-[10px] text-blue-600 font-medium">Upcoming this week</span>
</div>
</div>
<div class="bg-white rounded-lg p-3.5 border border-gray-200 shadow-sm">
<div class="flex items-center justify-between mb-2">
<div class="text-[10px] font-medium text-gray-400 uppercase tracking-wider">Ongoing</div>
<div class="w-6 h-6 rounded-md bg-teal-50 flex items-center justify-center">
<svg class="w-3.5 h-3.5 text-teal-600" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
<path stroke-linecap="round" stroke-linejoin="round" d="M11.42 15.17l-5.384-3.08A1.59 1.59 0 015 10.68V5.83c0-1.16 1.057-2 2.163-1.78l7.603 1.52A1.59 1.59 0 0116 7.16v5.06c0 .63-.37 1.2-.95 1.46l-3.63 1.5z" />
</svg>
</div>
</div>
<div class="text-xl font-bold text-gray-900">1</div>
<div class="flex items-center gap-1 mt-1">
<span class="w-1.5 h-1.5 rounded-full bg-teal-500 animate-pulse"></span>
<span class="text-[10px] text-teal-600 font-medium">In progress</span>
</div>
</div>
<div class="bg-white rounded-lg p-3.5 border border-gray-200 shadow-sm">
<div class="flex items-center justify-between mb-2">
<div class="text-[10px] font-medium text-gray-400 uppercase tracking-wider">Completed</div>
<div class="w-6 h-6 rounded-md bg-emerald-50 flex items-center justify-center">
<svg class="w-3.5 h-3.5 text-emerald-600" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
<path stroke-linecap="round" stroke-linejoin="round" d="M9 12.75L11.25 15 15 9.75M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
</div>
</div>
<div class="text-xl font-bold text-gray-900">12</div>
<div class="flex items-center gap-1 mt-1">
<span class="text-[10px] text-emerald-600 font-medium">This month</span>
</div>
</div>
<div class="bg-white rounded-lg p-3.5 border border-gray-200 shadow-sm">
<div class="flex items-center justify-between mb-2">
<div class="text-[10px] font-medium text-gray-400 uppercase tracking-wider">Subscribers</div>
<div class="w-6 h-6 rounded-md bg-violet-50 flex items-center justify-center">
<svg class="w-3.5 h-3.5 text-violet-600" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
<path stroke-linecap="round" stroke-linejoin="round" d="M15 19.128a9.38 9.38 0 002.625.372 9.337 9.337 0 004.121-.952 4.125 4.125 0 00-7.533-2.493M15 19.128v-.003c0-1.113-.285-2.16-.786-3.07M15 19.128v.106A12.318 12.318 0 018.624 21c-2.331 0-4.512-.645-6.374-1.766l-.001-.109a6.375 6.375 0 0111.964-3.07M12 6.375a3.375 3.375 0 11-6.75 0 3.375 3.375 0 016.75 0zm8.25 2.25a2.625 2.625 0 11-5.25 0 2.625 2.625 0 015.25 0z" />
</svg>
</div>
</div>
<div class="text-xl font-bold text-gray-900">847</div>
<div class="flex items-center gap-1 mt-1">
<span class="text-[10px] text-violet-600 font-medium">Auto-notified</span>
</div>
</div>
</div>
<!-- Maintenance events list -->
<div class="bg-white rounded-lg border border-gray-200 shadow-sm overflow-hidden">
<div class="px-4 py-2.5 bg-gray-50 border-b border-gray-200 flex items-center justify-between">
<span class="text-xs font-semibold text-gray-700">Upcoming & Ongoing Events</span>
<span class="text-[10px] text-gray-400">4 events</span>
</div>
<div class="divide-y divide-gray-100">
<!-- Ongoing event -->
<div class="px-4 py-3 flex items-center justify-between">
<div class="flex items-center gap-3">
<div class="w-2 h-8 rounded-full bg-teal-500"></div>
<div>
<div class="text-xs font-medium text-gray-900">Database Migration - Production</div>
<div class="text-[10px] text-gray-500 mt-0.5">Started 45 min ago &middot; Est. 2 hours remaining</div>
</div>
</div>
<div class="flex items-center gap-2">
<span class="flex items-center gap-1 px-2 py-1 bg-teal-50 rounded text-[10px] text-teal-700 border border-teal-200 font-medium">
<span class="w-1.5 h-1.5 rounded-full bg-teal-500 animate-pulse"></span>
Ongoing
</span>
</div>
</div>
<!-- Scheduled event 1 -->
<div class="px-4 py-3 flex items-center justify-between">
<div class="flex items-center gap-3">
<div class="w-2 h-8 rounded-full bg-blue-400"></div>
<div>
<div class="text-xs font-medium text-gray-900">SSL Certificate Renewal</div>
<div class="text-[10px] text-gray-500 mt-0.5">Tomorrow, 2:00 AM &ndash; 2:30 AM UTC</div>
</div>
</div>
<div class="flex items-center gap-2">
<span class="px-2 py-1 bg-blue-50 rounded text-[10px] text-blue-700 border border-blue-200 font-medium">Scheduled</span>
</div>
</div>
<!-- Scheduled event 2 -->
<div class="px-4 py-3 flex items-center justify-between">
<div class="flex items-center gap-3">
<div class="w-2 h-8 rounded-full bg-blue-400"></div>
<div>
<div class="text-xs font-medium text-gray-900">Infrastructure Upgrade - Load Balancers</div>
<div class="text-[10px] text-gray-500 mt-0.5">Sat, Mar 22 &middot; 1:00 AM &ndash; 5:00 AM UTC</div>
</div>
</div>
<div class="flex items-center gap-2">
<span class="px-2 py-1 bg-blue-50 rounded text-[10px] text-blue-700 border border-blue-200 font-medium">Scheduled</span>
</div>
</div>
<!-- Scheduled event 3 -->
<div class="px-4 py-3 flex items-center justify-between">
<div class="flex items-center gap-3">
<div class="w-2 h-8 rounded-full bg-blue-400"></div>
<div>
<div class="text-xs font-medium text-gray-900">API Gateway Version Upgrade</div>
<div class="text-[10px] text-gray-500 mt-0.5">Mon, Mar 24 &middot; 3:00 AM &ndash; 4:00 AM UTC</div>
</div>
</div>
<div class="flex items-center gap-2">
<span class="px-2 py-1 bg-blue-50 rounded text-[10px] text-blue-700 border border-blue-200 font-medium">Scheduled</span>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<%- include('logo-roll') -%>
<!-- How It Works Section -->
<div class="relative bg-gray-50 py-24 sm:py-32">
<div class="mx-auto max-w-7xl px-6 lg:px-8">
<div class="mx-auto max-w-2xl text-center mb-16">
<p class="text-sm font-medium text-teal-600 uppercase tracking-wide mb-3">How It Works</p>
<h2 class="text-3xl font-semibold tracking-tight text-gray-900 sm:text-4xl">
From planning to completion in four steps
</h2>
<p class="mt-4 text-lg text-gray-600">
Schedule maintenance windows, keep users informed, and complete your work without surprises.
</p>
</div>
<div class="mx-auto max-w-5xl">
<!-- Connecting line for desktop -->
<div class="hidden lg:block relative">
<div class="absolute top-8 left-[calc(12.5%+24px)] right-[calc(12.5%+24px)] h-px bg-teal-200"></div>
</div>
<div class="grid grid-cols-1 gap-10 sm:grid-cols-2 lg:grid-cols-4">
<!-- Step 1 -->
<div class="text-center">
<div class="relative inline-flex items-center justify-center h-16 w-16 rounded-full bg-teal-600 text-white mb-6 shadow-sm">
<svg class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M6.75 3v2.25M17.25 3v2.25M3 18.75V7.5a2.25 2.25 0 012.25-2.25h13.5A2.25 2.25 0 0121 7.5v11.25m-18 0A2.25 2.25 0 005.25 21h13.5A2.25 2.25 0 0021 18.75m-18 0v-7.5A2.25 2.25 0 015.25 9h13.5A2.25 2.25 0 0121 11.25v7.5" />
</svg>
<span class="absolute -bottom-1 -right-1 flex h-6 w-6 items-center justify-center rounded-full bg-white text-xs font-semibold text-teal-600 ring-2 ring-teal-600">1</span>
</div>
<h3 class="text-base font-semibold text-gray-900 mb-2">Schedule Event</h3>
<p class="text-sm text-gray-600 leading-relaxed">Create a maintenance event with start time, duration, and affected services.</p>
</div>
<!-- Step 2 -->
<div class="text-center">
<div class="relative inline-flex items-center justify-center h-16 w-16 rounded-full bg-teal-600 text-white mb-6 shadow-sm">
<svg class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M14.857 17.082a23.848 23.848 0 005.454-1.31A8.967 8.967 0 0118 9.75v-.7V9A6 6 0 006 9v.75a8.967 8.967 0 01-2.312 6.022c1.733.64 3.56 1.085 5.455 1.31m5.714 0a24.255 24.255 0 01-5.714 0m5.714 0a3 3 0 11-5.714 0" />
</svg>
<span class="absolute -bottom-1 -right-1 flex h-6 w-6 items-center justify-center rounded-full bg-white text-xs font-semibold text-teal-600 ring-2 ring-teal-600">2</span>
</div>
<h3 class="text-base font-semibold text-gray-900 mb-2">Notify Subscribers</h3>
<p class="text-sm text-gray-600 leading-relaxed">Automatically notify subscribers via email, SMS, and webhooks before maintenance begins.</p>
</div>
<!-- Step 3 -->
<div class="text-center">
<div class="relative inline-flex items-center justify-center h-16 w-16 rounded-full bg-teal-600 text-white mb-6 shadow-sm">
<svg class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M11.42 15.17l-5.384-3.08A1.59 1.59 0 015 10.68V5.83c0-1.16 1.057-2 2.163-1.78l7.603 1.52A1.59 1.59 0 0116 7.16v5.06c0 .63-.37 1.2-.95 1.46l-3.63 1.5z" />
</svg>
<span class="absolute -bottom-1 -right-1 flex h-6 w-6 items-center justify-center rounded-full bg-white text-xs font-semibold text-teal-600 ring-2 ring-teal-600">3</span>
</div>
<h3 class="text-base font-semibold text-gray-900 mb-2">Post Updates</h3>
<p class="text-sm text-gray-600 leading-relaxed">Share real-time progress updates on your status page as work proceeds.</p>
</div>
<!-- Step 4 -->
<div class="text-center">
<div class="relative inline-flex items-center justify-center h-16 w-16 rounded-full bg-teal-600 text-white mb-6 shadow-sm">
<svg class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M9 12.75L11.25 15 15 9.75M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
<span class="absolute -bottom-1 -right-1 flex h-6 w-6 items-center justify-center rounded-full bg-white text-xs font-semibold text-teal-600 ring-2 ring-teal-600">4</span>
</div>
<h3 class="text-base font-semibold text-gray-900 mb-2">Complete & Resolve</h3>
<p class="text-sm text-gray-600 leading-relaxed">Mark maintenance complete and automatically notify subscribers that services are restored.</p>
</div>
</div>
</div>
</div>
</div>
<!-- Why OneUptime Section -->
<div class="relative bg-white py-24 sm:py-32 overflow-hidden">
<div class="mx-auto max-w-7xl px-6 lg:px-8">
<!-- Section header -->
<div class="mx-auto max-w-2xl text-center mb-20">
<div class="inline-flex items-center gap-2 rounded-full bg-teal-50 px-4 py-1.5 text-sm font-medium text-teal-700 ring-1 ring-inset ring-teal-600/20 mb-6">
<svg class="h-4 w-4 text-teal-600" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11.48 3.499a.562.562 0 011.04 0l2.125 5.111a.563.563 0 00.475.345l5.518.442c.499.04.701.663.321.988l-4.204 3.602a.563.563 0 00-.182.557l1.285 5.385a.562.562 0 01-.84.61l-4.725-2.885a.563.563 0 00-.586 0L6.982 20.54a.562.562 0 01-.84-.61l1.285-5.386a.562.562 0 00-.182-.557l-4.204-3.602a.563.563 0 01.321-.988l5.518-.442a.563.563 0 00.475-.345L11.48 3.5z" />
</svg>
<span class="text-sm font-medium text-teal-700">Why OneUptime for Scheduled Maintenance</span>
</div>
<h2 class="text-3xl font-bold tracking-tight text-gray-900 sm:text-4xl lg:text-5xl">
Maintenance communication your users will appreciate
</h2>
<p class="mt-6 text-lg leading-8 text-gray-600">
Proactive communication builds trust. Keep your users informed about planned downtime so they can plan accordingly.
</p>
</div>
<!-- Feature blocks -->
<div class="space-y-24">
<!-- Feature 1: Schedule & Plan -->
<div class="lg:grid lg:grid-cols-2 lg:gap-16 lg:items-center">
<div class="relative">
<div class="flex items-center gap-4 mb-6">
<div class="flex h-10 w-10 items-center justify-center rounded-xl bg-teal-50 ring-1 ring-teal-200">
<svg class="h-5 w-5 text-teal-600" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M6.75 3v2.25M17.25 3v2.25M3 18.75V7.5a2.25 2.25 0 012.25-2.25h13.5A2.25 2.25 0 0121 7.5v11.25m-18 0A2.25 2.25 0 005.25 21h13.5A2.25 2.25 0 0021 18.75m-18 0v-7.5A2.25 2.25 0 015.25 9h13.5A2.25 2.25 0 0121 11.25v7.5" />
</svg>
</div>
<span class="text-sm font-semibold text-teal-600 uppercase tracking-wide">Planning</span>
</div>
<h3 class="text-2xl font-bold tracking-tight text-gray-900 sm:text-3xl">Schedule maintenance with precision</h3>
<p class="mt-4 text-lg text-gray-600">Define start and end times, select affected monitors and status pages, and set custom maintenance states to communicate exactly what is happening.</p>
<ul class="mt-6 space-y-3">
<li class="flex items-center gap-3 text-gray-600">
<svg class="h-5 w-5 text-teal-500 flex-shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7" />
</svg>
Set precise start and end times
</li>
<li class="flex items-center gap-3 text-gray-600">
<svg class="h-5 w-5 text-teal-500 flex-shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7" />
</svg>
Assign affected monitors and services
</li>
<li class="flex items-center gap-3 text-gray-600">
<svg class="h-5 w-5 text-teal-500 flex-shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7" />
</svg>
Custom maintenance states and labels
</li>
</ul>
<a href="/accounts/register" class="inline-flex items-center gap-1.5 px-4 py-2 bg-gray-900 text-white text-sm font-medium rounded-lg hover:bg-gray-800 transition-colors mt-8">
Get started
<svg class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 7l5 5m0 0l-5 5m5-5H6" />
</svg>
</a>
</div>
<div class="mt-12 lg:mt-0">
<div class="relative">
<div class="absolute -inset-4 bg-gray-100/50 rounded-3xl blur-2xl"></div>
<!-- Schedule Event Mockup -->
<div class="relative max-w-md mx-auto">
<div class="bg-white rounded-xl shadow-lg overflow-hidden border border-gray-200">
<div class="bg-gray-50 px-4 py-2.5 flex items-center gap-3 border-b border-gray-100">
<div class="flex gap-1.5">
<div class="w-2.5 h-2.5 rounded-full bg-gray-300"></div>
<div class="w-2.5 h-2.5 rounded-full bg-gray-300"></div>
<div class="w-2.5 h-2.5 rounded-full bg-gray-300"></div>
</div>
<div class="flex-1 text-center">
<span class="text-xs text-gray-400">Create Maintenance Event</span>
</div>
</div>
<div class="p-4 space-y-3">
<div>
<label class="text-[10px] font-medium text-gray-500 uppercase tracking-wider">Event Title</label>
<div class="mt-1 px-3 py-2 border border-gray-200 rounded-lg text-sm text-gray-900 bg-white">Database Migration - Production</div>
</div>
<div class="grid grid-cols-2 gap-3">
<div>
<label class="text-[10px] font-medium text-gray-500 uppercase tracking-wider">Start Time</label>
<div class="mt-1 px-3 py-2 border border-gray-200 rounded-lg text-sm text-gray-900 bg-white">Mar 20, 2:00 AM</div>
</div>
<div>
<label class="text-[10px] font-medium text-gray-500 uppercase tracking-wider">End Time</label>
<div class="mt-1 px-3 py-2 border border-gray-200 rounded-lg text-sm text-gray-900 bg-white">Mar 20, 6:00 AM</div>
</div>
</div>
<div>
<label class="text-[10px] font-medium text-gray-500 uppercase tracking-wider">Affected Monitors</label>
<div class="mt-1 flex flex-wrap gap-1.5">
<span class="px-2 py-1 bg-teal-50 rounded text-[10px] text-teal-700 border border-teal-200">API Server</span>
<span class="px-2 py-1 bg-teal-50 rounded text-[10px] text-teal-700 border border-teal-200">Database</span>
<span class="px-2 py-1 bg-teal-50 rounded text-[10px] text-teal-700 border border-teal-200">Web App</span>
</div>
</div>
<div>
<label class="text-[10px] font-medium text-gray-500 uppercase tracking-wider">Status Pages</label>
<div class="mt-1 flex flex-wrap gap-1.5">
<span class="px-2 py-1 bg-blue-50 rounded text-[10px] text-blue-700 border border-blue-200">Public Status Page</span>
<span class="px-2 py-1 bg-blue-50 rounded text-[10px] text-blue-700 border border-blue-200">Internal Status</span>
</div>
</div>
<button class="w-full mt-2 py-2 bg-teal-600 text-white text-sm font-medium rounded-lg hover:bg-teal-700 transition-colors">Create Event</button>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Feature 2: Automatic Notifications -->
<div class="lg:grid lg:grid-cols-2 lg:gap-16 lg:items-center">
<div class="order-2 lg:order-1 mt-12 lg:mt-0">
<div class="relative">
<div class="absolute -inset-4 bg-gray-100/50 rounded-3xl blur-2xl"></div>
<!-- Notification Mockup -->
<div class="relative bg-white rounded-xl shadow-lg overflow-hidden max-w-md mx-auto border border-gray-200">
<div class="bg-gray-50 px-4 py-2.5 flex items-center gap-3 border-b border-gray-100">
<div class="flex gap-1.5">
<div class="w-2.5 h-2.5 rounded-full bg-gray-300"></div>
<div class="w-2.5 h-2.5 rounded-full bg-gray-300"></div>
<div class="w-2.5 h-2.5 rounded-full bg-gray-300"></div>
</div>
<div class="flex-1 text-center">
<span class="text-xs text-gray-400">Subscriber Notifications</span>
</div>
</div>
<div class="p-4 space-y-3">
<!-- Email notification -->
<div class="p-3 bg-gray-50 rounded-lg border border-gray-200">
<div class="flex items-center gap-2 mb-2">
<div class="w-6 h-6 rounded bg-blue-100 flex items-center justify-center">
<svg class="w-3.5 h-3.5 text-blue-600" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
<path stroke-linecap="round" stroke-linejoin="round" d="M21.75 6.75v10.5a2.25 2.25 0 01-2.25 2.25h-15a2.25 2.25 0 01-2.25-2.25V6.75m19.5 0A2.25 2.25 0 0019.5 4.5h-15a2.25 2.25 0 00-2.25 2.25m19.5 0v.243a2.25 2.25 0 01-1.07 1.916l-7.5 4.615a2.25 2.25 0 01-2.36 0L3.32 8.91a2.25 2.25 0 01-1.07-1.916V6.75" />
</svg>
</div>
<div class="text-xs font-medium text-gray-700">Email</div>
<span class="ml-auto text-[10px] text-emerald-600 font-medium">847 sent</span>
</div>
<div class="text-[10px] text-gray-500">Scheduled maintenance notification sent to all subscribers</div>
</div>
<!-- SMS notification -->
<div class="p-3 bg-gray-50 rounded-lg border border-gray-200">
<div class="flex items-center gap-2 mb-2">
<div class="w-6 h-6 rounded bg-emerald-100 flex items-center justify-center">
<svg class="w-3.5 h-3.5 text-emerald-600" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
<path stroke-linecap="round" stroke-linejoin="round" d="M10.5 1.5H8.25A2.25 2.25 0 006 3.75v16.5a2.25 2.25 0 002.25 2.25h7.5A2.25 2.25 0 0018 20.25V3.75a2.25 2.25 0 00-2.25-2.25H13.5m-3 0V3h3V1.5m-3 0h3m-3 18.75h3" />
</svg>
</div>
<div class="text-xs font-medium text-gray-700">SMS</div>
<span class="ml-auto text-[10px] text-emerald-600 font-medium">123 sent</span>
</div>
<div class="text-[10px] text-gray-500">SMS alerts sent to phone subscribers</div>
</div>
<!-- Webhook notification -->
<div class="p-3 bg-gray-50 rounded-lg border border-gray-200">
<div class="flex items-center gap-2 mb-2">
<div class="w-6 h-6 rounded bg-violet-100 flex items-center justify-center">
<svg class="w-3.5 h-3.5 text-violet-600" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
<path stroke-linecap="round" stroke-linejoin="round" d="M13.19 8.688a4.5 4.5 0 011.242 7.244l-4.5 4.5a4.5 4.5 0 01-6.364-6.364l1.757-1.757m13.35-.622l1.757-1.757a4.5 4.5 0 00-6.364-6.364l-4.5 4.5a4.5 4.5 0 001.242 7.244" />
</svg>
</div>
<div class="text-xs font-medium text-gray-700">Webhooks</div>
<span class="ml-auto text-[10px] text-emerald-600 font-medium">5 triggered</span>
</div>
<div class="text-[10px] text-gray-500">Webhook payloads sent to integrations</div>
</div>
</div>
</div>
</div>
</div>
<div class="order-1 lg:order-2">
<div class="flex items-center gap-4 mb-6">
<div class="flex h-10 w-10 items-center justify-center rounded-xl bg-teal-50 ring-1 ring-teal-200">
<svg class="h-5 w-5 text-teal-600" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M14.857 17.082a23.848 23.848 0 005.454-1.31A8.967 8.967 0 0118 9.75v-.7V9A6 6 0 006 9v.75a8.967 8.967 0 01-2.312 6.022c1.733.64 3.56 1.085 5.455 1.31m5.714 0a24.255 24.255 0 01-5.714 0m5.714 0a3 3 0 11-5.714 0" />
</svg>
</div>
<span class="text-sm font-semibold text-teal-600 uppercase tracking-wide">Notifications</span>
</div>
<h3 class="text-2xl font-bold tracking-tight text-gray-900 sm:text-3xl">Automatic subscriber notifications</h3>
<p class="mt-4 text-lg text-gray-600">Notify your users before maintenance begins via email, SMS, and webhooks. Send updates during maintenance and a final notification when services are restored.</p>
<ul class="mt-6 space-y-3">
<li class="flex items-center gap-3 text-gray-600">
<svg class="h-5 w-5 text-teal-500 flex-shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7" />
</svg>
Email, SMS, and webhook notifications
</li>
<li class="flex items-center gap-3 text-gray-600">
<svg class="h-5 w-5 text-teal-500 flex-shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7" />
</svg>
Pre-maintenance advance notices
</li>
<li class="flex items-center gap-3 text-gray-600">
<svg class="h-5 w-5 text-teal-500 flex-shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7" />
</svg>
Completion notifications when resolved
</li>
</ul>
<a href="/accounts/register" class="inline-flex items-center gap-1.5 px-4 py-2 bg-gray-900 text-white text-sm font-medium rounded-lg hover:bg-gray-800 transition-colors mt-8">
Get started
<svg class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 7l5 5m0 0l-5 5m5-5H6" />
</svg>
</a>
</div>
</div>
<!-- Feature 3: Status Page Integration -->
<div class="lg:grid lg:grid-cols-2 lg:gap-16 lg:items-center">
<div class="relative">
<div class="flex items-center gap-4 mb-6">
<div class="flex h-10 w-10 items-center justify-center rounded-xl bg-teal-50 ring-1 ring-teal-200">
<svg class="h-5 w-5 text-teal-600" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M9 12h3.75M9 15h3.75M9 18h3.75m3 .75H18a2.25 2.25 0 002.25-2.25V6.108c0-1.135-.845-2.098-1.976-2.192a48.424 48.424 0 00-1.123-.08m-5.801 0c-.065.21-.1.433-.1.664 0 .414.336.75.75.75h4.5a.75.75 0 00.75-.75 2.25 2.25 0 00-.1-.664m-5.8 0A2.251 2.251 0 0113.5 2.25H15c1.012 0 1.867.668 2.15 1.586m-5.8 0c-.376.023-.75.05-1.124.08C9.095 4.01 8.25 4.973 8.25 6.108V8.25m0 0H4.875c-.621 0-1.125.504-1.125 1.125v11.25c0 .621.504 1.125 1.125 1.125h9.75c.621 0 1.125-.504 1.125-1.125V9.375c0-.621-.504-1.125-1.125-1.125H8.25zM6.75 12h.008v.008H6.75V12zm0 3h.008v.008H6.75V15zm0 3h.008v.008H6.75V18z" />
</svg>
</div>
<span class="text-sm font-semibold text-teal-600 uppercase tracking-wide">Status Page</span>
</div>
<h3 class="text-2xl font-bold tracking-tight text-gray-900 sm:text-3xl">Seamless status page integration</h3>
<p class="mt-4 text-lg text-gray-600">Scheduled maintenance events automatically appear on your status page, keeping your users informed with a timeline of upcoming and ongoing maintenance.</p>
<ul class="mt-6 space-y-3">
<li class="flex items-center gap-3 text-gray-600">
<svg class="h-5 w-5 text-teal-500 flex-shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7" />
</svg>
Automatic status page updates
</li>
<li class="flex items-center gap-3 text-gray-600">
<svg class="h-5 w-5 text-teal-500 flex-shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7" />
</svg>
Public maintenance timeline
</li>
<li class="flex items-center gap-3 text-gray-600">
<svg class="h-5 w-5 text-teal-500 flex-shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7" />
</svg>
Real-time progress notes
</li>
</ul>
<a href="/accounts/register" class="inline-flex items-center gap-1.5 px-4 py-2 bg-gray-900 text-white text-sm font-medium rounded-lg hover:bg-gray-800 transition-colors mt-8">
Get started
<svg class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 7l5 5m0 0l-5 5m5-5H6" />
</svg>
</a>
</div>
<div class="mt-12 lg:mt-0">
<div class="relative">
<div class="absolute -inset-4 bg-gray-100/50 rounded-3xl blur-2xl"></div>
<!-- Status Page Mockup -->
<div class="relative max-w-md mx-auto">
<div class="bg-white rounded-xl shadow-lg overflow-hidden border border-gray-200">
<div class="bg-gray-50 px-4 py-2.5 flex items-center gap-3 border-b border-gray-100">
<div class="flex gap-1.5">
<div class="w-2.5 h-2.5 rounded-full bg-gray-300"></div>
<div class="w-2.5 h-2.5 rounded-full bg-gray-300"></div>
<div class="w-2.5 h-2.5 rounded-full bg-gray-300"></div>
</div>
<div class="flex-1 text-center">
<span class="text-xs text-gray-400">Status Page - Maintenance</span>
</div>
</div>
<div class="p-4">
<div class="flex items-center gap-2 mb-4">
<svg class="h-5 w-5 text-teal-600" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.5">
<path stroke-linecap="round" stroke-linejoin="round" d="M11.42 15.17l-5.384-3.08A1.59 1.59 0 015 10.68V5.83c0-1.16 1.057-2 2.163-1.78l7.603 1.52A1.59 1.59 0 0116 7.16v5.06c0 .63-.37 1.2-.95 1.46l-3.63 1.5z" />
</svg>
<span class="text-sm font-semibold text-gray-900">Scheduled Maintenance</span>
</div>
<!-- Timeline -->
<div class="space-y-4">
<div class="relative pl-6 border-l-2 border-teal-200">
<div class="absolute -left-[5px] top-1 w-2 h-2 rounded-full bg-teal-500"></div>
<div class="text-[10px] text-teal-600 font-medium">In Progress</div>
<div class="text-xs font-medium text-gray-900 mt-1">Database Migration - Production</div>
<div class="text-[10px] text-gray-500 mt-0.5">Migration is 75% complete. No user impact detected.</div>
<div class="text-[10px] text-gray-400 mt-1">Updated 15 minutes ago</div>
</div>
<div class="relative pl-6 border-l-2 border-teal-200">
<div class="absolute -left-[5px] top-1 w-2 h-2 rounded-full bg-teal-400"></div>
<div class="text-[10px] text-teal-600 font-medium">Started</div>
<div class="text-xs font-medium text-gray-900 mt-1">Maintenance window opened</div>
<div class="text-[10px] text-gray-500 mt-0.5">Beginning database migration. Expected duration: 4 hours.</div>
<div class="text-[10px] text-gray-400 mt-1">2:00 AM UTC</div>
</div>
<div class="relative pl-6 border-l-2 border-gray-200">
<div class="absolute -left-[5px] top-1 w-2 h-2 rounded-full bg-gray-300"></div>
<div class="text-[10px] text-gray-500 font-medium">Scheduled</div>
<div class="text-xs font-medium text-gray-900 mt-1">Maintenance announced</div>
<div class="text-[10px] text-gray-500 mt-0.5">Subscribers notified about upcoming maintenance.</div>
<div class="text-[10px] text-gray-400 mt-1">Mar 18, 10:00 AM UTC</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Workspace Integration Section -->
<div class="bg-gray-50 py-24 sm:py-32">
<div class="mx-auto max-w-7xl px-6 lg:px-8">
<div class="mx-auto max-w-2xl text-center mb-16">
<p class="text-sm font-medium text-teal-600 uppercase tracking-wide mb-3">Integrations</p>
<h2 class="text-3xl font-semibold tracking-tight text-gray-900 sm:text-4xl">
Works with your existing tools
</h2>
<p class="mt-4 text-lg text-gray-600">
Connect scheduled maintenance events to Slack, Microsoft Teams, and more. Keep your entire team in the loop.
</p>
</div>
<div class="mx-auto max-w-4xl grid grid-cols-1 gap-8 sm:grid-cols-3">
<!-- Slack -->
<div class="bg-white rounded-xl p-6 shadow-sm border border-gray-200 text-center">
<div class="inline-flex items-center justify-center w-12 h-12 bg-gray-50 rounded-xl mb-4 ring-1 ring-gray-200">
<svg class="w-6 h-6" viewBox="0 0 24 24" fill="none">
<path d="M5.042 15.165a2.528 2.528 0 0 1-2.52 2.523A2.528 2.528 0 0 1 0 15.165a2.527 2.527 0 0 1 2.522-2.52h2.52v2.52zM6.313 15.165a2.527 2.527 0 0 1 2.521-2.52 2.527 2.527 0 0 1 2.521 2.52v6.313A2.528 2.528 0 0 1 8.834 24a2.528 2.528 0 0 1-2.521-2.522v-6.313zM8.834 5.042a2.528 2.528 0 0 1-2.521-2.52A2.528 2.528 0 0 1 8.834 0a2.528 2.528 0 0 1 2.521 2.522v2.52H8.834zM8.834 6.313a2.528 2.528 0 0 1 2.521 2.521 2.528 2.528 0 0 1-2.521 2.521H2.522A2.528 2.528 0 0 1 0 8.834a2.528 2.528 0 0 1 2.522-2.521h6.312zM18.956 8.834a2.528 2.528 0 0 1 2.522-2.521A2.528 2.528 0 0 1 24 8.834a2.528 2.528 0 0 1-2.522 2.521h-2.522V8.834zM17.688 8.834a2.528 2.528 0 0 1-2.523 2.521 2.527 2.527 0 0 1-2.52-2.521V2.522A2.527 2.527 0 0 1 15.165 0a2.528 2.528 0 0 1 2.523 2.522v6.312zM15.165 18.956a2.528 2.528 0 0 1 2.523 2.522A2.528 2.528 0 0 1 15.165 24a2.527 2.527 0 0 1-2.52-2.522v-2.522h2.52zM15.165 17.688a2.527 2.527 0 0 1-2.52-2.523 2.526 2.526 0 0 1 2.52-2.52h6.313A2.527 2.527 0 0 1 24 15.165a2.528 2.528 0 0 1-2.522 2.523h-6.313z" fill="#E01E5A"/>
</svg>
</div>
<h3 class="text-sm font-semibold text-gray-900 mb-1">Slack</h3>
<p class="text-xs text-gray-500">Post maintenance updates directly to Slack channels</p>
</div>
<!-- Microsoft Teams -->
<div class="bg-white rounded-xl p-6 shadow-sm border border-gray-200 text-center">
<div class="inline-flex items-center justify-center w-12 h-12 bg-gray-50 rounded-xl mb-4 ring-1 ring-gray-200">
<svg class="w-6 h-6" viewBox="0 0 24 24" fill="none">
<path d="M20.625 7.5h-3.75v-1.875A1.875 1.875 0 0015 3.75h-1.875A1.875 1.875 0 0011.25 5.625V7.5H7.5a1.875 1.875 0 00-1.875 1.875v9.375A1.875 1.875 0 007.5 20.625h13.125A1.875 1.875 0 0022.5 18.75V9.375A1.875 1.875 0 0020.625 7.5z" fill="#5059C9"/>
<circle cx="16.875" cy="5.625" r="2.625" fill="#5059C9"/>
<path d="M13.125 9.375H1.875A1.875 1.875 0 000 11.25v6.375a1.875 1.875 0 001.875 1.875h11.25A1.875 1.875 0 0015 17.625V11.25a1.875 1.875 0 00-1.875-1.875z" fill="#7B83EB"/>
<circle cx="7.5" cy="6.375" r="3.375" fill="#7B83EB"/>
</svg>
</div>
<h3 class="text-sm font-semibold text-gray-900 mb-1">Microsoft Teams</h3>
<p class="text-xs text-gray-500">Send maintenance alerts to Teams channels</p>
</div>
<!-- Webhooks -->
<div class="bg-white rounded-xl p-6 shadow-sm border border-gray-200 text-center">
<div class="inline-flex items-center justify-center w-12 h-12 bg-gray-50 rounded-xl mb-4 ring-1 ring-gray-200">
<svg class="w-6 h-6 text-gray-700" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.5">
<path stroke-linecap="round" stroke-linejoin="round" d="M13.19 8.688a4.5 4.5 0 011.242 7.244l-4.5 4.5a4.5 4.5 0 01-6.364-6.364l1.757-1.757m13.35-.622l1.757-1.757a4.5 4.5 0 00-6.364-6.364l-4.5 4.5a4.5 4.5 0 001.242 7.244" />
</svg>
</div>
<h3 class="text-sm font-semibold text-gray-900 mb-1">Webhooks</h3>
<p class="text-xs text-gray-500">Trigger custom workflows with webhook events</p>
</div>
</div>
</div>
</div>
<%- include('./Partials/enterprise-ready') -%>
<%- include('features-table') -%>
<%- include('cta') -%>
</main>
<%- include('footer') -%>
<!-- Grid glow effect script -->
<script>
(function() {
const heroSection = document.getElementById('maintenance-hero-section');
const gridGlow = document.getElementById('maintenance-grid-glow');
if (heroSection && gridGlow) {
heroSection.addEventListener('mousemove', function(e) {
const rect = heroSection.getBoundingClientRect();
const x = e.clientX - rect.left;
const y = e.clientY - rect.top;
gridGlow.style.setProperty('--mouse-x', x + 'px');
gridGlow.style.setProperty('--mouse-y', y + 'px');
gridGlow.style.opacity = '1';
});
heroSection.addEventListener('mouseleave', function() {
gridGlow.style.opacity = '0';
});
}
})();
</script>
</body>
</html>