diff --git a/src/app/backups/page.tsx b/src/app/backups/page.tsx
index 27d8459..84d1ac4 100644
--- a/src/app/backups/page.tsx
+++ b/src/app/backups/page.tsx
@@ -4,6 +4,7 @@ import { getAuthUserSession, isAuthorizedForBackups } from "@/server/utils/actio
import PageTitle from "@/components/custom/page-title";
import backupService from "@/server/services/standalone-services/backup.service";
import BackupsTable from "./backups-table";
+import { CatchUtils } from "@/shared/utils/catch.utils";
import { AlertCircle, AlertTriangleIcon } from "lucide-react"
import {
Alert,
@@ -15,10 +16,11 @@ import {
export default async function BackupsPage() {
await isAuthorizedForBackups();
- const {
- backupInfoModels,
- backupsVolumesWithoutActualBackups
- } = await backupService.getBackupsForAllS3Targets();
+ const backupData = await CatchUtils.resultOrUndefined(() => backupService.getBackupsForAllS3Targets());
+
+ const backupInfoModels = backupData?.backupInfoModels ?? [];
+ const backupsVolumesWithoutActualBackups = backupData?.backupsVolumesWithoutActualBackups ?? [];
+ const failedS3Targets = backupData?.failedS3Targets ?? [];
const hasMissedBackups = backupInfoModels.some(x => x.missedBackup === true);
@@ -29,6 +31,20 @@ export default async function BackupsPage() {
subtitle={`View all backups wich are stored in all S3 Target destinations. If a backup exists from an app wich doesnt exist anymore, it will be shown as orphaned.`}>
+ {!backupData &&
+
+ Backup data could not be loaded
+
+ The configured backup storage could not be reached. Please verify your S3/B2 target settings and credentials.
+
+ }
+ {failedS3Targets.length > 0 &&
+
+ Some S3 targets could not be loaded
+
+ Backups from the following locations could not be fetched: {failedS3Targets.map((target) => `${target.name} (${target.endpoint}/${target.bucketName})`).join(', ')}
+
+ }
{backupsVolumesWithoutActualBackups.length > 0 &&
Apps without Backup
diff --git a/src/app/monitoring/disk-chart.tsx b/src/app/monitoring/disk-chart.tsx
index 3219e92..f8727ef 100644
--- a/src/app/monitoring/disk-chart.tsx
+++ b/src/app/monitoring/disk-chart.tsx
@@ -17,10 +17,17 @@ export default function ChartDiskRessources({
nodeRessource: NodeResourceModel;
}) {
+ const diskUsed = nodeRessource.diskUsageAbsolut ?? 0;
+ const diskReserved = nodeRessource.diskUsageReserved ?? 0;
+ const diskCapacity = nodeRessource.diskUsageCapacity ?? 0;
+ const diskSchedulable = nodeRessource.diskSpaceSchedulable ?? Math.max(0, diskCapacity - diskUsed - diskReserved);
+ const diskUsedAndReserved = diskUsed + diskReserved;
+ const diskUsagePercent = diskCapacity > 0 ? (diskUsedAndReserved / diskCapacity) * 100 : 0;
+
const chartData = [{
- diskUsed: nodeRessource.diskUsageAbsolut,
- diskReserved: nodeRessource.diskUsageReserved,
- diskSchedulable: nodeRessource.diskSpaceSchedulable
+ diskUsed,
+ diskReserved,
+ diskSchedulable
}];
const chartConfig = {
@@ -81,7 +88,7 @@ export default function ChartDiskRessources({
y={(viewBox.cy || 0) - 10}
className="fill-foreground text-4xl font-bold"
>
- {((nodeRessource.diskUsageAbsolut + nodeRessource.diskUsageReserved) / nodeRessource.diskUsageCapacity * 100).toFixed(0)}%
+ {diskUsagePercent.toFixed(0)}%
- {KubeSizeConverter.convertBytesToReadableSize(nodeRessource.diskUsageAbsolut + nodeRessource.diskUsageReserved, 1, true)} / {KubeSizeConverter.convertBytesToReadableSize(nodeRessource.diskUsageCapacity, 1)}
+ {KubeSizeConverter.convertBytesToReadableSize(diskUsedAndReserved, 1, true)} / {KubeSizeConverter.convertBytesToReadableSize(diskCapacity, 1)}
);
diff --git a/src/app/monitoring/monitoring-nodes.tsx b/src/app/monitoring/monitoring-nodes.tsx
index 19cdb78..33cf351 100644
--- a/src/app/monitoring/monitoring-nodes.tsx
+++ b/src/app/monitoring/monitoring-nodes.tsx
@@ -40,6 +40,11 @@ export default function ResourcesNodes({
resourcesNodes?: NodeResourceModel[];
}) {
+ const getDiskUsageAbsolut = (node: NodeResourceModel) => node.diskUsageAbsolut ?? 0;
+ const getDiskUsageReserved = (node: NodeResourceModel) => node.diskUsageReserved ?? 0;
+ const getDiskUsageCapacity = (node: NodeResourceModel) => node.diskUsageCapacity ?? 0;
+ const toPercent = (used: number, capacity: number) => (capacity > 0 ? (used / capacity) * 100 : 0);
+
const [updatedNodeRessources, setUpdatedResourcesNodes] = useState(resourcesNodes);
const fetchResourcesNodes = async () => {
@@ -76,9 +81,9 @@ export default function ResourcesNodes({
cpuCapacity: acc.cpuCapacity + node.cpuCapacity,
ramUsage: acc.ramUsage + node.ramUsage,
ramCapacity: acc.ramCapacity + node.ramCapacity,
- diskUsageAbsolut: acc.diskUsageAbsolut + node.diskUsageAbsolut,
- diskUsageReserved: acc.diskUsageReserved + node.diskUsageReserved,
- diskCapacity: acc.diskCapacity + node.diskUsageCapacity,
+ diskUsageAbsolut: acc.diskUsageAbsolut + getDiskUsageAbsolut(node),
+ diskUsageReserved: acc.diskUsageReserved + getDiskUsageReserved(node),
+ diskCapacity: acc.diskCapacity + getDiskUsageCapacity(node),
}), {
cpuUsage: 0, cpuCapacity: 0,
ramUsage: 0, ramCapacity: 0,
@@ -227,7 +232,7 @@ export default function ResourcesNodes({
return (
- {(((clusterStats.diskUsageAbsolut + clusterStats.diskUsageReserved) / clusterStats.diskCapacity) * 100).toFixed(0)}%
+ {toPercent(clusterStats.diskUsageAbsolut + clusterStats.diskUsageReserved, clusterStats.diskCapacity).toFixed(0)}%
Used
@@ -283,11 +288,22 @@ export default function ResourcesNodes({
-
- {(((node.diskUsageAbsolut + node.diskUsageReserved) / node.diskUsageCapacity) * 100).toFixed(0)}%
- {KubeSizeConverter.convertBytesToReadableSize(node.diskUsageAbsolut + node.diskUsageReserved)} / {KubeSizeConverter.convertBytesToReadableSize(node.diskUsageCapacity)}
-
-
+ {(() => {
+ const diskUsed = getDiskUsageAbsolut(node);
+ const diskReserved = getDiskUsageReserved(node);
+ const diskCapacity = getDiskUsageCapacity(node);
+ const diskUsedAndReserved = diskUsed + diskReserved;
+ const diskPercent = toPercent(diskUsedAndReserved, diskCapacity);
+ return (
+ <>
+
+ {diskPercent.toFixed(0)}%
+ {KubeSizeConverter.convertBytesToReadableSize(diskUsedAndReserved)} / {KubeSizeConverter.convertBytesToReadableSize(diskCapacity)}
+
+
+ >
+ );
+ })()}
diff --git a/src/app/monitoring/page.tsx b/src/app/monitoring/page.tsx
index 4958008..271cf74 100644
--- a/src/app/monitoring/page.tsx
+++ b/src/app/monitoring/page.tsx
@@ -11,22 +11,17 @@ import AppRessourceMonitoring from "./app-monitoring";
import AppVolumeMonitoring from "./app-volumes-monitoring";
import { AppMonitoringUsageModel } from "@/shared/model/app-monitoring-usage.model";
import { UserGroupUtils } from "@/shared/utils/role.utils";
+import { CatchUtils } from "@/shared/utils/catch.utils";
export default async function ResourceNodesInfoPage() {
const session = await getAuthUserSession();
- let resourcesNode: NodeResourceModel[] | undefined;
- let volumesUsage: AppVolumeMonitoringUsageModel[] | undefined;
- let updatedNodeRessources: AppMonitoringUsageModel[] | undefined;
- try {
- [resourcesNode, volumesUsage, updatedNodeRessources] = await Promise.all([
- clusterService.getNodeResourceUsage(),
- monitoringService.getAllAppVolumesUsage(),
- await monitoringService.getMonitoringForAllApps()
- ]);
- } catch (ex) {
- // do nothing --> if an error occurs, the ResourceNodes will show a loading spinner and error message
- }
+
+ let [resourcesNode, volumesUsage, updatedNodeRessources] = await Promise.all([
+ CatchUtils.resultOrUndefined(() => clusterService.getNodeResourceUsage()),
+ CatchUtils.resultOrUndefined(() => monitoringService.getAllAppVolumesUsage()),
+ CatchUtils.resultOrUndefined(() => monitoringService.getMonitoringForAllApps())
+ ]);
// filter by role
volumesUsage = volumesUsage?.filter((volume) => UserGroupUtils.sessionHasReadAccessForApp(session, volume.appId));
diff --git a/src/server/services/cluster.service.ts b/src/server/services/cluster.service.ts
index 0dbf487..416a009 100644
--- a/src/server/services/cluster.service.ts
+++ b/src/server/services/cluster.service.ts
@@ -6,6 +6,7 @@ import { Tags } from "../utils/cache-tag-generator.utils";
import { revalidateTag, unstable_cache } from "next/cache";
import longhornApiAdapter from "../adapter/longhorn-api.adapter";
import { KubeSizeConverter } from "../../shared/utils/kubernetes-size-converter.utils";
+import { CatchUtils } from "@/shared/utils/catch.utils";
class ClusterService {
@@ -88,7 +89,7 @@ class ClusterService {
nodeMetrics.sort((a, b) => b.timestamp.getTime() - a.timestamp.getTime());
const latestUsageItem = nodeMetrics[0];
- const diskInfo = await longhornApiAdapter.getNodeStorageInfo(node.Node.metadata?.name!);
+ const diskInfo = await CatchUtils.resultOrUndefined(() => longhornApiAdapter.getNodeStorageInfo(node.Node.metadata?.name!));
return {
name: node.Node.metadata?.name!,
@@ -96,10 +97,10 @@ class ClusterService {
cpuCapacity: Number(node.CPU?.Capacity!),
ramUsage: latestUsageItem.ramUsage,
ramCapacity: Number(node.Memory?.Capacity!),
- diskUsageAbsolut: diskInfo.totalStorageMaximum - diskInfo.totalStorageAvailable,
- diskUsageReserved: diskInfo.totalStorageReserved,
- diskUsageCapacity: diskInfo.totalStorageMaximum,
- diskSpaceSchedulable: diskInfo.totalSchedulableStorage
+ diskUsageAbsolut: diskInfo ? diskInfo.totalStorageMaximum - diskInfo.totalStorageAvailable : undefined,
+ diskUsageReserved: diskInfo?.totalStorageReserved,
+ diskUsageCapacity: diskInfo?.totalStorageMaximum,
+ diskSpaceSchedulable: diskInfo?.totalSchedulableStorage
}
}));
}
diff --git a/src/server/services/standalone-services/backup.service.ts b/src/server/services/standalone-services/backup.service.ts
index 0d8bc5d..1be0878 100644
--- a/src/server/services/standalone-services/backup.service.ts
+++ b/src/server/services/standalone-services/backup.service.ts
@@ -100,10 +100,39 @@ class BackupService {
async getBackupsForAllS3Targets() {
const s3Targets = await dataAccess.client.s3Target.findMany();
- const returnValFromAllS3Targets = await Promise.all(s3Targets.map(s3Target =>
- this.getBackupsFromS3Target(s3Target)));
+ const returnValFromAllS3Targets = await Promise.all(s3Targets.map(async (s3Target) => {
+ try {
+ const data = await this.getBackupsFromS3Target(s3Target);
+ return {
+ s3Target,
+ data,
+ error: undefined
+ };
+ } catch (error) {
+ const errorMessage = error instanceof Error
+ ? `${error.name}: ${error.message}`
+ : String(error);
+ console.error(`Failed to fetch backups for S3 target '${s3Target.name}' (${s3Target.endpoint}/${s3Target.bucketName})`, error);
+ return {
+ s3Target,
+ data: undefined,
+ error: errorMessage
+ };
+ }
+ }));
+
+ const successfulResults = returnValFromAllS3Targets.filter((result) => !!result.data);
+ const failedS3Targets = returnValFromAllS3Targets
+ .filter((result) => !!result.error)
+ .map((result) => ({
+ id: result.s3Target.id,
+ name: result.s3Target.name,
+ endpoint: result.s3Target.endpoint,
+ bucketName: result.s3Target.bucketName,
+ error: result.error ?? 'Unknown error'
+ }));
- const backupInfoModels = returnValFromAllS3Targets.map(x => x.backupInfoModels).flat();
+ const backupInfoModels = successfulResults.map(x => x.data!.backupInfoModels).flat();
backupInfoModels.sort((a, b) => {
if (a.projectName === b.projectName) {
return a.appName.localeCompare(b.appName);
@@ -111,10 +140,11 @@ class BackupService {
return a.projectName.localeCompare(b.projectName);
});
- const backupsVolumesWithoutActualBackups = returnValFromAllS3Targets.map(x => x.backupsVolumesWithoutActualBackups).flat();
+ const backupsVolumesWithoutActualBackups = successfulResults.map(x => x.data!.backupsVolumesWithoutActualBackups).flat();
return {
backupInfoModels,
- backupsVolumesWithoutActualBackups
+ backupsVolumesWithoutActualBackups,
+ failedS3Targets
};
}
diff --git a/src/shared/model/node-resource.model.ts b/src/shared/model/node-resource.model.ts
index 7aa449e..8c2e874 100644
--- a/src/shared/model/node-resource.model.ts
+++ b/src/shared/model/node-resource.model.ts
@@ -8,10 +8,10 @@ export const nodeResourceZodModel = z.object({
cpuCapacity: z.number(),
ramUsage: z.number(),
ramCapacity: z.number(),
- diskUsageAbsolut: z.number(),
- diskUsageCapacity: z.number(),
- diskUsageReserved: z.number(),
- diskSpaceSchedulable: z.number(),
+ diskUsageAbsolut: z.number().optional(),
+ diskUsageCapacity: z.number().optional(),
+ diskUsageReserved: z.number().optional(),
+ diskSpaceSchedulable: z.number().optional(),
})
export type NodeResourceModel = z.infer;
\ No newline at end of file
diff --git a/src/shared/utils/catch.utils.ts b/src/shared/utils/catch.utils.ts
new file mode 100644
index 0000000..ac2553a
--- /dev/null
+++ b/src/shared/utils/catch.utils.ts
@@ -0,0 +1,15 @@
+export class CatchUtils {
+ static async resultOrUndefined(promiseFunc: () => Promise, context?: string): Promise {
+ const promiseName = promiseFunc.name || 'anonymous-promise';
+ const contextInfo = context ? ` [${context}]` : '';
+ try {
+ return await promiseFunc();
+ } catch (error) {
+ const errorInfo = error instanceof Error
+ ? `${error.name}: ${error.message}`
+ : String(error);
+ console.error(`Error in resultOrUndefined${contextInfo} (${promiseName}): ${errorInfo}`, error);
+ return undefined;
+ }
+ }
+}
\ No newline at end of file