+
{
const lastLevel = escalation().levels[escalation().levels.length - 1];
- const newAfter = lastLevel ? lastLevel.after + 30 : 15;
+ const newAfter = typeof lastLevel?.after === 'number' ? lastLevel.after + 30 : 15;
setEscalation({
...escalation(),
levels: [...escalation().levels, { after: newAfter, notify: 'all' }]
diff --git a/frontend-modern/src/stores/websocket.ts b/frontend-modern/src/stores/websocket.ts
index 4bc157531..6b55825db 100644
--- a/frontend-modern/src/stores/websocket.ts
+++ b/frontend-modern/src/stores/websocket.ts
@@ -169,7 +169,7 @@ export function createWebSocketStore(url: string) {
// Transform tags from comma-separated strings to arrays
const transformedVMs = message.data.vms.map((vm: VM) => {
const originalTags = vm.tags;
- let transformedTags;
+ let transformedTags: string[];
if (originalTags && typeof originalTags === 'string' && originalTags.trim()) {
// String with content - split into array
@@ -195,7 +195,7 @@ export function createWebSocketStore(url: string) {
// Transform tags from comma-separated strings to arrays
const transformedContainers = message.data.containers.map((container: Container) => {
const originalTags = container.tags;
- let transformedTags;
+ let transformedTags: string[];
if (originalTags && typeof originalTags === 'string' && originalTags.trim()) {
// String with content - split into array
@@ -454,4 +454,4 @@ export function createWebSocketStore(url: string) {
}
}
};
-}
\ No newline at end of file
+}
diff --git a/frontend-modern/src/types/alerts.ts b/frontend-modern/src/types/alerts.ts
index 938235c55..471a321ba 100644
--- a/frontend-modern/src/types/alerts.ts
+++ b/frontend-modern/src/types/alerts.ts
@@ -25,6 +25,11 @@ export interface AlertThresholds {
[key: string]: HysteresisThreshold | number | undefined;
}
+export type RawOverrideConfig = AlertThresholds & {
+ disabled?: boolean;
+ disableConnectivity?: boolean;
+};
+
export interface CustomAlertRule {
id: string;
name: string;
@@ -53,7 +58,7 @@ export interface AlertConfig {
nodeDefaults: AlertThresholds;
storageDefault: HysteresisThreshold;
customRules?: CustomAlertRule[];
- overrides: Record; // key: resource ID
+ overrides: Record; // key: resource ID
minimumDelta?: number;
suppressionWindow?: number;
hysteresisMargin?: number;
@@ -127,4 +132,4 @@ export interface AlertConfig {
// 0: Global defaults
// 1-99: Reserved for system rules
// 100+: Custom user rules
-// 1000+: Guest-specific overrides
\ No newline at end of file
+// 1000+: Guest-specific overrides
diff --git a/frontend-modern/src/types/api.ts b/frontend-modern/src/types/api.ts
index 5b5c57d01..2e0f6ca8d 100644
--- a/frontend-modern/src/types/api.ts
+++ b/frontend-modern/src/types/api.ts
@@ -59,7 +59,7 @@ export interface VM {
uptime: number;
template: boolean;
lastBackup: string;
- tags: string[];
+ tags: string[] | string | null;
lock: string;
lastSeen: string;
}
@@ -83,7 +83,7 @@ export interface Container {
uptime: number;
template: boolean;
lastBackup: string;
- tags: string[];
+ tags: string[] | string | null;
lock: string;
lastSeen: string;
}
@@ -254,6 +254,7 @@ export interface PhysicalDisk {
node: string;
instance: string;
devPath: string;
+ device?: string;
model: string;
serial: string;
type: 'nvme' | 'sata' | 'sas' | string;
@@ -264,6 +265,7 @@ export interface PhysicalDisk {
rpm: number;
used: string;
lastChecked: string;
+ smart?: unknown;
}
export interface CPUInfo {
@@ -410,8 +412,20 @@ export type WSMessage =
}>;
errors?: string[];
timestamp?: number;
+ immediate?: boolean;
+ scanning?: boolean;
+ cached?: boolean;
+ }}
+ | { type: 'discovery_started'; data?: {
+ subnet?: string;
+ timestamp?: number;
+ scanning?: boolean;
+ }}
+ | { type: 'discovery_complete'; data?: {
+ timestamp?: number;
+ scanning?: boolean;
}};
// Utility types
export type Status = 'running' | 'stopped' | 'paused' | 'unknown';
-export type GuestType = 'qemu' | 'lxc';
\ No newline at end of file
+export type GuestType = 'qemu' | 'lxc';
diff --git a/frontend-modern/src/types/backups.ts b/frontend-modern/src/types/backups.ts
index 88bff334d..8afb25f23 100644
--- a/frontend-modern/src/types/backups.ts
+++ b/frontend-modern/src/types/backups.ts
@@ -1,95 +1,22 @@
-// Unified backup types for the backup view
+export type BackupType = 'snapshot' | 'local' | 'remote';
+export type GuestType = 'VM' | 'LXC' | 'Host' | 'Template' | 'ISO';
export interface UnifiedBackup {
- // Common fields
- backupType: 'backup' | 'snapshot' | 'pbs';
+ backupType: BackupType;
vmid: number;
name: string;
- type: 'VM' | 'LXC' | 'CT';
+ type: GuestType;
node: string;
- backupTime: number; // Unix timestamp in seconds
+ backupTime: number;
backupName: string;
description: string;
status: string;
size: number | null;
storage: string | null;
-
- // PBS specific
datastore: string | null;
namespace: string | null;
verified: boolean | null;
-
- // Common flags
protected: boolean;
encrypted?: boolean;
-
- // UI specific
- instance?: string;
- isPBS?: boolean;
-}
-
-// PBS-specific backup file info
-export interface PBSBackupFile {
- filename: string;
- size: number;
- crypt?: string;
-}
-
-// Extended PBS backup with file details
-export interface PBSBackupWithFiles {
- id: string;
- instance: string;
- datastore: string;
- namespace?: string;
- backupType: string;
- vmid: number;
- backupTime: string;
- size: number;
- protected: boolean;
- verified: boolean;
- comment?: string;
- files: PBSBackupFile[];
-}
-
-// PBS datastore with snapshots
-export interface PBSDatastoreSnapshot {
- id: string;
- backupTime: string;
- size: number;
owner?: string;
- verified?: boolean;
- protected?: boolean;
- files?: PBSBackupFile[];
}
-
-export interface PBSDatastore {
- name: string;
- total: number;
- used: number;
- free: number;
- snapshots: PBSDatastoreSnapshot[];
-}
-
-export interface PBSInstanceData {
- id: string;
- name: string;
- host: string;
- backups: PBSBackupWithFiles[];
- datastores: PBSDatastore[];
-}
-
-// Filter options for the backup view
-export interface BackupFilters {
- instance: string;
- type: 'all' | 'VM' | 'LXC';
- node: string;
- storage: string;
- protected: 'all' | 'protected' | 'unprotected';
- verified: 'all' | 'verified' | 'unverified';
-}
-
-// Sorting options
-export interface BackupSort {
- key: keyof UnifiedBackup;
- order: 'asc' | 'desc';
-}
\ No newline at end of file
diff --git a/frontend-modern/src/utils/searchQuery.ts b/frontend-modern/src/utils/searchQuery.ts
index 593136059..1c1dda881 100644
--- a/frontend-modern/src/utils/searchQuery.ts
+++ b/frontend-modern/src/utils/searchQuery.ts
@@ -1,4 +1,5 @@
import type { VM, Container, PBSBackup, StorageBackup, BackupTask } from '@/types/api';
+import type { UnifiedBackup } from '@/types/backups';
export type ComparisonOperator = '>' | '<' | '>=' | '<=' | '=' | '==';
export type LogicalOperator = 'AND' | 'OR';
@@ -186,7 +187,7 @@ export function parseSearchQuery(query: string): ParsedQuery {
};
}
-type FilterableItem = VM | Container | PBSBackup | StorageBackup | BackupTask;
+type FilterableItem = VM | Container | PBSBackup | StorageBackup | BackupTask | UnifiedBackup;
function evaluateMetricCondition(guest: FilterableItem, condition: MetricCondition): boolean {
let value: number;
@@ -208,9 +209,13 @@ function evaluateMetricCondition(guest: FilterableItem, condition: MetricConditi
break;
default:
// For backup-specific numeric fields like 'size'
- const fieldValue = (guest as Record)[condition.field];
- if (fieldValue !== undefined) {
- value = Number(fieldValue) || 0;
+ if (typeof guest === 'object' && guest !== null && condition.field in guest) {
+ const fieldValue = (guest as unknown as Record)[condition.field];
+ if (fieldValue !== undefined) {
+ value = Number(fieldValue) || 0;
+ } else {
+ return false;
+ }
} else {
return false;
}
@@ -245,25 +250,30 @@ function evaluateTextCondition(guest: FilterableItem, condition: TextCondition):
return 'vmid' in guest && guest.vmid ? guest.vmid.toString().includes(searchValue) : false;
case 'tags':
// Check if guest has any tags that match the search value
- if (!('tags' in guest) || !guest.tags || !Array.isArray(guest.tags) || guest.tags.length === 0) return false;
+ if (!('tags' in guest) || !guest.tags) return false;
+ const tagsArray = Array.isArray(guest.tags)
+ ? guest.tags.filter((tag): tag is string => typeof tag === 'string')
+ : typeof guest.tags === 'string'
+ ? guest.tags.split(',').map(tag => tag.trim()).filter(tag => tag.length > 0)
+ : [];
+ if (tagsArray.length === 0) return false;
// Support comma-separated tag searches (OR logic)
const searchTags = searchValue.split(',').map(t => t.trim()).filter(t => t.length > 0);
- return searchTags.some(searchTag =>
- guest.tags!.some((tag: string) => {
- if (typeof tag !== 'string') return false;
- return tag.toLowerCase().includes(searchTag.toLowerCase());
- })
+ return searchTags.some(searchTag =>
+ tagsArray.some(tag => tag.toLowerCase().includes(searchTag.toLowerCase()))
);
default:
// For backup-specific fields
- const fieldValue = (guest as Record)[condition.field];
- if (fieldValue) {
- if (typeof fieldValue === 'string') {
- return fieldValue.toLowerCase().includes(searchValue);
- } else if (typeof fieldValue === 'number') {
- return fieldValue.toString().includes(searchValue);
- } else if (typeof fieldValue === 'boolean') {
- return fieldValue.toString() === searchValue;
+ if (typeof guest === 'object' && guest !== null && condition.field in guest) {
+ const fieldValue = (guest as unknown as Record)[condition.field];
+ if (fieldValue) {
+ if (typeof fieldValue === 'string') {
+ return fieldValue.toLowerCase().includes(searchValue);
+ } else if (typeof fieldValue === 'number') {
+ return fieldValue.toString().includes(searchValue);
+ } else if (typeof fieldValue === 'boolean') {
+ return fieldValue.toString() === searchValue;
+ }
}
}
return false;
@@ -335,16 +345,17 @@ export function evaluateFilterStack(guest: FilterableItem, stack: FilterStack):
} else if (filter.type === 'raw' && filter.rawText) {
const term = filter.rawText.toLowerCase();
// Check name, vmid, node, status, and tags for raw text matches
- const basicMatch = ('name' in guest && guest.name && guest.name.toLowerCase().includes(term)) ||
- ('vmid' in guest && guest.vmid && guest.vmid.toString().includes(term)) ||
- ('node' in guest && guest.node && guest.node.toLowerCase().includes(term)) ||
- ('status' in guest && guest.status && guest.status.toLowerCase().includes(term));
-
+ const nameMatch = 'name' in guest && typeof guest.name === 'string' && guest.name.toLowerCase().includes(term);
+ const vmidMatch = 'vmid' in guest && !!guest.vmid && guest.vmid.toString().includes(term);
+ const nodeMatch = 'node' in guest && typeof guest.node === 'string' && guest.node.toLowerCase().includes(term);
+ const statusMatch = 'status' in guest && typeof guest.status === 'string' && guest.status.toLowerCase().includes(term);
+
// Also check if any tags contain the search term
- const tagMatch = 'tags' in guest && guest.tags && Array.isArray(guest.tags) &&
- guest.tags.some((tag: string) => tag.toLowerCase().includes(term));
-
- return basicMatch || tagMatch;
+ const tagMatch = 'tags' in guest && Array.isArray(guest.tags)
+ ? guest.tags.filter((tag): tag is string => typeof tag === 'string').some(tag => tag.toLowerCase().includes(term))
+ : false;
+
+ return nameMatch || vmidMatch || nodeMatch || statusMatch || tagMatch;
}
return true;
});
@@ -368,4 +379,4 @@ export function evaluateFilterStack(guest: FilterableItem, stack: FilterStack):
}
return result;
-}
\ No newline at end of file
+}