+
+
+ } onClick={() => navigate('/home')}>
+
+
+
+ {object?.object_name ?? ` #${objectId}`}
+
+
+
+ {object?.allowed ? : }
+ {object?.health_score != null && (
+ = 80 ? 'green' : object.health_score >= 60 ? 'orange' : 'red'}>
+ Health {object.health_score}
+
+ )}
+
+
+
+ {object && (
+
+ {reasonLabel(object.inclusion_reason)}
+
+ )}
+
+ {availability?.insufficient_data && (
+
+ )}
+
+
+
+
+
+ {summary?.totals.devices ?? 0}
+
+
+
+
+
+ {summary?.totals.problem_devices ?? 0}
+
+
+
+
+ ({window})
+
+ {availability?.overall_availability_pct != null ? `${availability.overall_availability_pct}%` : ''}
+
+
+
+
+
+
setWindow(v as DashboardAvailabilityWindow)}
+ options={[
+ { value: '24h', label: '24' },
+ { value: '3d', label: '3' },
+ { value: '7d', label: '7' },
+ ]}
+ style={{ width: 100 }}
+ />
+ }
+ style={{ marginBottom: 16 }}
+ >
+
+
+
+
+ (
+
+ {row.ip}
+ {row.hostname ?? ''}
+
+ ),
+ },
+ { title: '', dataIndex: 'category', key: 'category' },
+ {
+ title: 'Ping',
+ key: 'ping',
+ render: (_: unknown, row: any) => (
+
+
+ {row.ping_status}
+
+
+ {row.ping_checked_at ? dayjs(row.ping_checked_at).format('DD.MM HH:mm:ss') : ''}
+ {row.last_ping_ms != null ? ` ${row.last_ping_ms}ms` : ''}
+
+
+ ),
+ },
+ {
+ title: 'Disk',
+ key: 'disk',
+ render: (_: unknown, row: any) =>
+ row.disk_status ? (
+
+ {row.disk_status}{row.disk_max_pct != null ? ` (${row.disk_max_pct}%)` : ''}
+
+ ) : '',
+ },
+ {
+ title: '',
+ key: 'problem',
+ render: (_: unknown, row: any) => row.has_problem ? : ,
+ },
+ ]}
+ />
+
+
+
+ {v} },
+ { title: 'Hostname', dataIndex: 'hostname', key: 'hostname', render: (v: string | null) => v ?? '' },
+ {
+ title: 'Availability',
+ key: 'availability_pct',
+ render: (_: unknown, row: any) => row.availability_pct != null ? `${row.availability_pct}%` : '',
+ },
+ { title: 'Samples', dataIndex: 'samples', key: 'samples' },
+ {
+ title: ' ',
+ key: 'last_status',
+ render: (_: unknown, row: any) => row.last_status ? {row.last_status} : '',
+ },
+ ]}
+ />
+
+
+ )
+}
diff --git a/frontend/src/pages/HomeSettings.tsx b/frontend/src/pages/HomeSettings.tsx
new file mode 100644
index 0000000..b94b38d
--- /dev/null
+++ b/frontend/src/pages/HomeSettings.tsx
@@ -0,0 +1,381 @@
+import { ArrowLeftOutlined } from '@ant-design/icons'
+import { Alert, Button, Card, Col, Divider, InputNumber, Row, Select, Space, Switch, Table, Tag, Typography, message } from 'antd'
+import dayjs from 'dayjs'
+import { useEffect, useMemo, useState } from 'react'
+import { useNavigate } from 'react-router-dom'
+import { useObjects } from '../api/objects'
+import {
+ useDashboardChecks,
+ useDashboardMonitorStatus,
+ useDashboardPolicy,
+ useDashboardRunNow,
+ usePatchDashboardChecks,
+ usePatchDashboardObjectOverrides,
+ usePatchDashboardObjectPolicy,
+ usePatchDashboardTagPolicy,
+ type DashboardObjectOverride,
+} from '../api/dashboard'
+import { useTags } from '../api/tags'
+
+const CATEGORY_OPTIONS = [
+ 'main_server',
+ 'vm',
+ 'router',
+ 'embedded',
+ 'camera',
+ 'io_board',
+ 'bank_terminal',
+ 'cash_register',
+ 'other',
+]
+
+type OverrideEnabledMode = 'inherit' | 'enabled' | 'disabled'
+type OverrideCategoriesMode = 'inherit' | 'custom'
+
+export function HomeSettingsPage() {
+ const navigate = useNavigate()
+
+ const { data: objects } = useObjects()
+ const { data: tags } = useTags()
+ const { data: policy } = useDashboardPolicy()
+ const { data: checksData } = useDashboardChecks()
+ const { data: monitorStatus } = useDashboardMonitorStatus()
+
+ const runNow = useDashboardRunNow()
+ const patchTagPolicy = usePatchDashboardTagPolicy()
+ const patchObjectPolicy = usePatchDashboardObjectPolicy()
+ const patchChecks = usePatchDashboardChecks()
+ const patchOverrides = usePatchDashboardObjectOverrides()
+
+ const [overrideObjectId, setOverrideObjectId] = useState()
+ const [overrideCheckKey, setOverrideCheckKey] = useState()
+ const [overrideInterval, setOverrideInterval] = useState()
+ const [overrideEnabledMode, setOverrideEnabledMode] = useState('inherit')
+ const [overrideCategoriesMode, setOverrideCategoriesMode] = useState('inherit')
+ const [overrideCategories, setOverrideCategories] = useState([])
+
+ const tagPolicyMap = useMemo(() => {
+ const map = new Map()
+ for (const row of policy?.tag_policies ?? []) map.set(row.tag_id, row.enabled)
+ return map
+ }, [policy])
+
+ const objectPolicyMap = useMemo(() => {
+ const map = new Map()
+ for (const row of policy?.object_policies ?? []) map.set(row.object_id, row.mode)
+ return map
+ }, [policy])
+
+ const overrideMap = useMemo(() => {
+ const map = new Map()
+ for (const row of checksData?.object_overrides ?? []) {
+ map.set(`${row.object_id}:${row.check_key}`, row)
+ }
+ return map
+ }, [checksData])
+
+ useEffect(() => {
+ if (!overrideObjectId || !overrideCheckKey) {
+ setOverrideEnabledMode('inherit')
+ setOverrideInterval(undefined)
+ setOverrideCategoriesMode('inherit')
+ setOverrideCategories([])
+ return
+ }
+
+ const current = overrideMap.get(`${overrideObjectId}:${overrideCheckKey}`)
+ if (!current) {
+ setOverrideEnabledMode('inherit')
+ setOverrideInterval(undefined)
+ setOverrideCategoriesMode('inherit')
+ setOverrideCategories([])
+ return
+ }
+
+ if (current.enabled_override === true) setOverrideEnabledMode('enabled')
+ else if (current.enabled_override === false) setOverrideEnabledMode('disabled')
+ else setOverrideEnabledMode('inherit')
+
+ setOverrideInterval(current.interval_sec_override ?? undefined)
+
+ if (current.categories_override && current.categories_override.length > 0) {
+ setOverrideCategoriesMode('custom')
+ setOverrideCategories(current.categories_override)
+ } else {
+ setOverrideCategoriesMode('inherit')
+ setOverrideCategories([])
+ }
+ }, [overrideObjectId, overrideCheckKey, overrideMap])
+
+ const handleRunNow = async () => {
+ try {
+ await runNow.mutateAsync({ force: true })
+ message.success(' ')
+ } catch {
+ message.error(' ')
+ }
+ }
+
+ return (
+
+
+
+ } onClick={() => navigate('/home')}>
+
+
+
+
+
+
+
+
+
+
+ : {monitorStatus?.status ?? '-'}
+ {monitorStatus?.last_success_at
+ ? ` ${dayjs(monitorStatus.last_success_at).format('DD.MM HH:mm:ss')}`
+ : ''}
+
+
+ {monitorStatus?.last_error && (
+
+ )}
+
+
+
+ {(checksData?.checks ?? []).map((check) => (
+
+ {check.check_key}
+
+ {
+ await patchChecks.mutateAsync([{ check_key: check.check_key, enabled }])
+ }}
+ />
+
+
+ {
+ const val = Number((e.target as HTMLInputElement).value)
+ if (!Number.isFinite(val) || val <= 0) return
+ await patchChecks.mutateAsync([{ check_key: check.check_key, interval_sec_default: val }])
+ }}
+ />
+
+
+
+ ))}
+
+
+
+
+
+ {(tags ?? []).map((tag) => (
+
+
+ {tag.name}
+ {
+ await patchTagPolicy.mutateAsync([{ tag_id: tag.id, enabled }])
+ }}
+ />
+
+
+ ))}
+
+
+
+
+ (
+