From dbd952d66ccbe706ee1dce47939e4fff462d7894 Mon Sep 17 00:00:00 2001 From: Vehbi Sinan Tunalioglu Date: Wed, 15 Jan 2025 17:37:06 +0800 Subject: [PATCH 1/5] chore(ci): fix release workflow --- .github/workflows/release.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 8b99332..b43bc92 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -25,13 +25,16 @@ jobs: fetch-depth: 0 - name: "Install Nix" + if: "${{ steps.release.outputs.release_created }}" uses: "DeterminateSystems/nix-installer-action@v16" - name: "Use Nix Cache" + if: "${{ steps.release.outputs.release_created }}" uses: "DeterminateSystems/magic-nix-cache-action@v8" ## TODO: This should not be necessary, but nixpkgs v24.11 requires it. - name: "Update Haskell Package List" + if: "${{ steps.release.outputs.release_created }}" run: | nix-shell --pure --run "cabal update --ignore-project" From a1de044aca22262575760de708d7ef90eaeacaaa Mon Sep 17 00:00:00 2001 From: Vehbi Sinan Tunalioglu Date: Wed, 15 Jan 2025 17:41:16 +0800 Subject: [PATCH 2/5] feat: add NTP and time sync information to host report --- src/HostPatrol/Remote.hs | 25 +++++++++++++++++++++++ src/HostPatrol/Types.hs | 25 +++++++++++++++++++++++ src/scripts/clock.sh | 43 ++++++++++++++++++++++++++++++++++++++++ 3 files changed, 93 insertions(+) create mode 100644 src/scripts/clock.sh diff --git a/src/HostPatrol/Remote.hs b/src/HostPatrol/Remote.hs index 1f1f60e..2a9d32a 100644 --- a/src/HostPatrol/Remote.hs +++ b/src/HostPatrol/Remote.hs @@ -85,6 +85,7 @@ compileHostReport ch = do _hostReportTimezone <- _toParseError _hostName $ _getParse pure "HOSTPATROL_GENERAL_TIMEZONE" kvs _hostReportCloud <- _mkCloud _hostName kvs _hostReportHardware <- _mkHardware _hostName kvs + _hostReportClock <- _mkClock _hostName =<< _fetchHostClockInfo h _hostReportKernel <- _mkKernel _hostName kvs _hostReportDistribution <- _mkDistribution _hostName kvs _hostReportDockerContainers <- _fetchHostDockerContainers h @@ -177,6 +178,17 @@ _fetchHostCloudInfo h@Types.Host {..} = parseKVs <$> _toSshError _hostName (Z.Ssh.runScript (getHostSshConfig h) $(embedStringFile "src/scripts/cloud.sh") ["bash"]) +-- | Attempts to retrieve remote host clock information and return it +-- as a list of key/value tuples. +_fetchHostClockInfo + :: MonadIO m + => MonadError HostPatrolError m + => Types.Host + -> m [(T.Text, T.Text)] +_fetchHostClockInfo h@Types.Host {..} = + parseKVs <$> _toSshError _hostName (Z.Ssh.runScript (getHostSshConfig h) $(embedStringFile "src/scripts/clock.sh") ["bash"]) + + -- | Attempts to retrieve remote host docker containers information and return it. -- -- Returns 'Nothing' if remote host is not identified as a Docker @@ -284,6 +296,19 @@ _mkHardware h kvs = pure Types.Hardware {..} +-- | Smart constructor for remote host clock information. +_mkClock + :: MonadError HostPatrolError m + => Z.Ssh.Destination + -> [(T.Text, T.Text)] + -> m Types.Clock +_mkClock h kvs = + _toParseError h $ do + _clockNtpAvailability <- _getParse pure "HOSTPATROL_CLOCK_NTP" kvs + _clockTimeSyncStatus <- _getParse pure "HOSTPATROL_CLOCK_NTP_SYNCHRONIZED" kvs + pure Types.Clock {..} + + -- | Smart constructor for remote host kernel information. _mkKernel :: MonadError HostPatrolError m diff --git a/src/HostPatrol/Types.hs b/src/HostPatrol/Types.hs index f680b0c..1b11016 100644 --- a/src/HostPatrol/Types.hs +++ b/src/HostPatrol/Types.hs @@ -136,6 +136,7 @@ data HostReport = HostReport , _hostReportTimezone :: !T.Text , _hostReportCloud :: !Cloud , _hostReportHardware :: !Hardware + , _hostReportClock :: !Clock , _hostReportKernel :: !Kernel , _hostReportDistribution :: !Distribution , _hostReportDockerContainers :: !(Maybe [DockerContainer]) @@ -160,6 +161,7 @@ instance ADC.HasCodec HostReport where <*> ADC.requiredField "timezone" "Timezone of the host." ADC..= _hostReportTimezone <*> ADC.requiredField "cloud" "Cloud information." ADC..= _hostReportCloud <*> ADC.requiredField "hardware" "Hardware information." ADC..= _hostReportHardware + <*> ADC.requiredField "clock" "Clock information." ADC..= _hostReportClock <*> ADC.requiredField "kernel" "Kernel information." ADC..= _hostReportKernel <*> ADC.requiredField "distribution" "Distribution information." ADC..= _hostReportDistribution <*> ADC.requiredField "dockerContainers" "List of Docker containers if the host is a Docker host." ADC..= _hostReportDockerContainers @@ -233,6 +235,29 @@ instance ADC.HasCodec Hardware where <*> ADC.requiredField "diskRoot" "Total disk space of root (`/`) filesystem (in GB)." ADC..= _hardwareDiskRoot +-- * Clock Information + + +-- | Data definition for host's clock information. +data Clock = Clock + { _clockNtpAvailability :: !T.Text + , _clockTimeSyncStatus :: !T.Text + } + deriving (Eq, Generic, Show) + deriving (Aeson.FromJSON, Aeson.ToJSON) via (ADC.Autodocodec Clock) + + +instance ADC.HasCodec Clock where + codec = + _codec ADC. "Clock Information" + where + _codec = + ADC.object "Clock" $ + Clock + <$> ADC.requiredField "ntp_availability" "Indicates NTP availability and enablement." ADC..= _clockNtpAvailability + <*> ADC.requiredField "time_sync_status" "Indicates time synchronisation status." ADC..= _clockTimeSyncStatus + + -- * Kernel Information diff --git a/src/scripts/clock.sh b/src/scripts/clock.sh new file mode 100644 index 0000000..7363202 --- /dev/null +++ b/src/scripts/clock.sh @@ -0,0 +1,43 @@ +#!/usr/bin/env sh + +################### +# SHELL BEHAVIOUR # +################### + +# Stop on errors: +set -e + +############### +# DEFINITIONS # +############### + +# Prints a key/value pair in SHELL variable format. Value is printed +# within double-quotes, and double-quotes in the variable are escaped. +_print_var() { + printf '%s="%s"\n' "${1}" "$(echo "${2}" | sed 's/"/\\"/g')" +} + +########## +# CHECKS # +########## + +## Check if timedatectl is available: +if ! command -v timedatectl >/dev/null; then + _print_var "HOSTPATROL_CLOCK_NTP" "timedatectl not available" + _print_var "HOSTPATROL_CLOCK_NTP_SYNCHRONIZED" "timedatectl not available" + exit 0 +fi + +## Check if we have a recent enough version of timedatectl: +if ! timedatectl show 2>/dev/null; then + _print_var "HOSTPATROL_CLOCK_NTP" "timedatectl failed (too old?)" + _print_var "HOSTPATROL_CLOCK_NTP_SYNCHRONIZED" "timedatectl failed (too old?)" + exit 0 +fi + +############# +# PROCEDURE # +############# + +_print_var "HOSTPATROL_CLOCK_NTP" "$(timedatectl show --property=NTP --value)" +_print_var "HOSTPATROL_CLOCK_NTP_SYNCHRONIZED" "$(timedatectl show --property=NTPSynchronized --value)" From fb2dc0534321b658b511e143735e131f393361fb Mon Sep 17 00:00:00 2001 From: Vehbi Sinan Tunalioglu Date: Wed, 15 Jan 2025 18:06:23 +0800 Subject: [PATCH 3/5] chore(website): update data definition as per latest report schema --- website/src/lib/data.ts | 379 +++++++++++++++++++++++++++++++++------- 1 file changed, 317 insertions(+), 62 deletions(-) diff --git a/website/src/lib/data.ts b/website/src/lib/data.ts index 6f46ce7..3353ef5 100644 --- a/website/src/lib/data.ts +++ b/website/src/lib/data.ts @@ -12,8 +12,14 @@ export const HOSTPATROL_REPORT_SCHEMA = { items: { $comment: 'Report Error\nReportError', properties: { - host: { $comment: 'Host of the error if applicable.', type: 'string' }, - message: { $comment: 'Error message.', type: 'string' }, + host: { + $comment: 'Host of the error if applicable.', + type: 'string', + }, + message: { + $comment: 'Error message.', + type: 'string', + }, }, required: ['message'], type: 'object', @@ -30,53 +36,155 @@ export const HOSTPATROL_REPORT_SCHEMA = { items: { $comment: 'SSH Public Key Information\nSshPublicKey', properties: { - comment: { $comment: 'Comment on the public key.', type: 'string' }, - data: { $comment: 'Original information.', type: 'string' }, - fingerprint: { $comment: 'Fingerprint of the public key.', type: 'string' }, + comment: { + $comment: 'Comment on the public key.', + type: 'string', + }, + data: { + $comment: 'Original information.', + type: 'string', + }, + fingerprint: { + $comment: 'Fingerprint of the public key.', + type: 'string', + }, length: { $comment: 'Length of the public key.', maximum: 2147483647, minimum: -2147483648, type: 'number', }, - type: { $comment: 'Type of the public key.', type: 'string' }, + type: { + $comment: 'Type of the public key.', + type: 'string', + }, }, required: ['fingerprint', 'comment', 'length', 'type', 'data'], type: 'object', }, type: 'array', }, + clock: { + $comment: 'Clock information.\nClock Information\nClock', + properties: { + ntp_availability: { + $comment: 'Indicates NTP availability and enablement.', + type: 'string', + }, + time_sync_status: { + $comment: 'Indicates time synchronisation status.', + type: 'string', + }, + }, + required: ['time_sync_status', 'ntp_availability'], + type: 'object', + }, cloud: { $comment: 'Cloud information.\nCloud Information\nCloud', properties: { hostAvailabilityZone: { $comment: 'Host availability zone.', - anyOf: [{ type: 'null' }, { type: 'string' }], + anyOf: [ + { + type: 'null', + }, + { + type: 'string', + }, + ], }, hostLocalAddress: { $comment: 'Local address of the host.', - anyOf: [{ type: 'null' }, { type: 'string' }], + anyOf: [ + { + type: 'null', + }, + { + type: 'string', + }, + ], }, hostLocalHostname: { $comment: 'Local hostname of the host.', - anyOf: [{ type: 'null' }, { type: 'string' }], + anyOf: [ + { + type: 'null', + }, + { + type: 'string', + }, + ], + }, + hostRegion: { + $comment: 'Host region.', + anyOf: [ + { + type: 'null', + }, + { + type: 'string', + }, + ], }, - hostRegion: { $comment: 'Host region.', anyOf: [{ type: 'null' }, { type: 'string' }] }, hostRemoteAddress: { $comment: 'Remote address of the host.', - anyOf: [{ type: 'null' }, { type: 'string' }], + anyOf: [ + { + type: 'null', + }, + { + type: 'string', + }, + ], }, hostRemoteHostname: { $comment: 'Remote hostname of the host.', - anyOf: [{ type: 'null' }, { type: 'string' }], + anyOf: [ + { + type: 'null', + }, + { + type: 'string', + }, + ], }, hostReservedAddress: { $comment: 'Reserved address of the host.', - anyOf: [{ type: 'null' }, { type: 'string' }], + anyOf: [ + { + type: 'null', + }, + { + type: 'string', + }, + ], + }, + hostType: { + $comment: 'Host type.', + anyOf: [ + { + type: 'null', + }, + { + type: 'string', + }, + ], + }, + id: { + $comment: 'Host identifier.', + anyOf: [ + { + type: 'null', + }, + { + type: 'string', + }, + ], + }, + name: { + $comment: 'Cloud name.', + type: 'string', }, - hostType: { $comment: 'Host type.', anyOf: [{ type: 'null' }, { type: 'string' }] }, - id: { $comment: 'Host identifier.', anyOf: [{ type: 'null' }, { type: 'string' }] }, - name: { $comment: 'Cloud name.', type: 'string' }, }, required: [ 'hostReservedAddress', @@ -97,16 +205,35 @@ export const HOSTPATROL_REPORT_SCHEMA = { properties: { codename: { $comment: "Distribution codename (cat /etc/os-release | grep 'VERSION_CODENAME=').", - anyOf: [{ type: 'null' }, { type: 'string' }], + anyOf: [ + { + type: 'null', + }, + { + type: 'string', + }, + ], }, description: { $comment: "Distribution description (cat /etc/os-release | grep 'PRETTY_NAME=').", type: 'string', }, - id: { $comment: "Distribution ID (cat /etc/os-release | grep 'ID=').", type: 'string' }, - name: { $comment: "Distribution name (cat /etc/os-release | grep 'NAME=')).", type: 'string' }, - release: { $comment: "Distribution release (cat /etc/os-release | grep 'VERSION_ID=').", type: 'string' }, - version: { $comment: "Distribution version (cat /etc/os-release | grep 'VERSION=').", type: 'string' }, + id: { + $comment: "Distribution ID (cat /etc/os-release | grep 'ID=').", + type: 'string', + }, + name: { + $comment: "Distribution name (cat /etc/os-release | grep 'NAME=')).", + type: 'string', + }, + release: { + $comment: "Distribution release (cat /etc/os-release | grep 'VERSION_ID=').", + type: 'string', + }, + version: { + $comment: "Distribution version (cat /etc/os-release | grep 'VERSION=').", + type: 'string', + }, }, required: ['description', 'codename', 'release', 'version', 'name', 'id'], type: 'object', @@ -114,7 +241,9 @@ export const HOSTPATROL_REPORT_SCHEMA = { dockerContainers: { $comment: 'List of Docker containers if the host is a Docker host.', anyOf: [ - { type: 'null' }, + { + type: 'null', + }, { items: { $comment: 'Docker Container Information\nDockerContainer', @@ -123,10 +252,22 @@ export const HOSTPATROL_REPORT_SCHEMA = { $comment: 'Date/time when the container is created at.\nDate/time in ISO8601 format.', type: 'string', }, - id: { $comment: 'ID of the container..', type: 'string' }, - image: { $comment: 'Image the container is created from.', type: 'string' }, - name: { $comment: 'Name of the container.', type: 'string' }, - running: { $comment: 'Indicates if the container is running.', type: 'boolean' }, + id: { + $comment: 'ID of the container..', + type: 'string', + }, + image: { + $comment: 'Image the container is created from.', + type: 'string', + }, + name: { + $comment: 'Name of the container.', + type: 'string', + }, + running: { + $comment: 'Indicates if the container is running.', + type: 'boolean', + }, }, required: ['running', 'created', 'image', 'name', 'id'], type: 'object', @@ -144,8 +285,14 @@ export const HOSTPATROL_REPORT_SCHEMA = { minimum: -2147483648, type: 'number', }, - diskRoot: { $comment: 'Total disk space of root (`/`) filesystem (in GB).', type: 'number' }, - ramTotal: { $comment: 'Total RAM (in GB).', type: 'number' }, + diskRoot: { + $comment: 'Total disk space of root (`/`) filesystem (in GB).', + type: 'number', + }, + ramTotal: { + $comment: 'Total RAM (in GB).', + type: 'number', + }, }, required: ['diskRoot', 'ramTotal', 'cpuCount'], type: 'object', @@ -153,55 +300,114 @@ export const HOSTPATROL_REPORT_SCHEMA = { host: { $comment: 'Host descriptor.\nHost Descriptor\nHost', properties: { - data: { $comment: 'Arbitrary data for the host.' }, - id: { $comment: 'External identifier of the host.', type: 'string' }, + data: { + $comment: 'Arbitrary data for the host.', + }, + id: { + $comment: 'External identifier of the host.', + type: 'string', + }, knownSshKeys: { $comment: 'Known SSH public keys for the host.', items: { $comment: 'SSH Public Key Information\nSshPublicKey', properties: { - comment: { $comment: 'Comment on the public key.', type: 'string' }, - data: { $comment: 'Original information.', type: 'string' }, - fingerprint: { $comment: 'Fingerprint of the public key.', type: 'string' }, + comment: { + $comment: 'Comment on the public key.', + type: 'string', + }, + data: { + $comment: 'Original information.', + type: 'string', + }, + fingerprint: { + $comment: 'Fingerprint of the public key.', + type: 'string', + }, length: { $comment: 'Length of the public key.', maximum: 2147483647, minimum: -2147483648, type: 'number', }, - type: { $comment: 'Type of the public key.', type: 'string' }, + type: { + $comment: 'Type of the public key.', + type: 'string', + }, }, required: ['fingerprint', 'comment', 'length', 'type', 'data'], type: 'object', }, type: 'array', }, - name: { $comment: 'Name of the host.', type: 'string' }, + name: { + $comment: 'Name of the host.', + type: 'string', + }, ssh: { $comment: 'SSH configuration.\nSSH Configuration\nSshConfig', properties: { - destination: { $comment: 'SSH destination.', type: 'string' }, - options: { $comment: 'SSH options.', items: { type: 'string' }, type: 'array' }, + destination: { + $comment: 'SSH destination.', + type: 'string', + }, + options: { + $comment: 'SSH options.', + items: { + type: 'string', + }, + type: 'array', + }, }, required: ['destination'], type: 'object', }, - tags: { $comment: 'Arbitrary tags for the host.', items: { type: 'string' }, type: 'array' }, - url: { $comment: 'URL to external host information.', type: 'string' }, + tags: { + $comment: 'Arbitrary tags for the host.', + items: { + type: 'string', + }, + type: 'array', + }, + url: { + $comment: 'URL to external host information.', + type: 'string', + }, }, required: ['name'], type: 'object', }, - hostname: { $comment: 'Hostname of the host.', type: 'string' }, + hostname: { + $comment: 'Hostname of the host.', + type: 'string', + }, kernel: { $comment: 'Kernel information.\nKernel Information\nKernel', properties: { - machine: { $comment: 'Architecture the kernel is running on (uname -m).', type: 'string' }, - name: { $comment: 'Kernel name (uname -s).', type: 'string' }, - node: { $comment: 'Name of the node kernel is running on (uname -n).', type: 'string' }, - os: { $comment: 'Operating system the kernel is driving (uname -o).', type: 'string' }, - release: { $comment: 'Kernel release (uname -r).', type: 'string' }, - version: { $comment: 'Kernel version (uname -v).', type: 'string' }, + machine: { + $comment: 'Architecture the kernel is running on (uname -m).', + type: 'string', + }, + name: { + $comment: 'Kernel name (uname -s).', + type: 'string', + }, + node: { + $comment: 'Name of the node kernel is running on (uname -n).', + type: 'string', + }, + os: { + $comment: 'Operating system the kernel is driving (uname -o).', + type: 'string', + }, + release: { + $comment: 'Kernel release (uname -r).', + type: 'string', + }, + version: { + $comment: 'Kernel version (uname -v).', + type: 'string', + }, }, required: ['os', 'machine', 'version', 'release', 'name', 'node'], type: 'object', @@ -211,16 +417,28 @@ export const HOSTPATROL_REPORT_SCHEMA = { items: { $comment: 'SSH Public Key Information\nSshPublicKey', properties: { - comment: { $comment: 'Comment on the public key.', type: 'string' }, - data: { $comment: 'Original information.', type: 'string' }, - fingerprint: { $comment: 'Fingerprint of the public key.', type: 'string' }, + comment: { + $comment: 'Comment on the public key.', + type: 'string', + }, + data: { + $comment: 'Original information.', + type: 'string', + }, + fingerprint: { + $comment: 'Fingerprint of the public key.', + type: 'string', + }, length: { $comment: 'Length of the public key.', maximum: 2147483647, minimum: -2147483648, type: 'number', }, - type: { $comment: 'Type of the public key.', type: 'string' }, + type: { + $comment: 'Type of the public key.', + type: 'string', + }, }, required: ['fingerprint', 'comment', 'length', 'type', 'data'], type: 'object', @@ -229,15 +447,22 @@ export const HOSTPATROL_REPORT_SCHEMA = { }, systemdServices: { $comment: 'List of systemd services found on host.', - items: { type: 'string' }, + items: { + type: 'string', + }, type: 'array', }, systemdTimers: { $comment: 'List of systemd timers found on host.', - items: { type: 'string' }, + items: { + type: 'string', + }, type: 'array', }, - timezone: { $comment: 'Timezone of the host.', type: 'string' }, + timezone: { + $comment: 'Timezone of the host.', + type: 'string', + }, }, required: [ 'systemdTimers', @@ -247,6 +472,7 @@ export const HOSTPATROL_REPORT_SCHEMA = { 'dockerContainers', 'distribution', 'kernel', + 'clock', 'hardware', 'cloud', 'timezone', @@ -262,11 +488,28 @@ export const HOSTPATROL_REPORT_SCHEMA = { items: { $comment: 'SSH Public Key Information\nSshPublicKey', properties: { - comment: { $comment: 'Comment on the public key.', type: 'string' }, - data: { $comment: 'Original information.', type: 'string' }, - fingerprint: { $comment: 'Fingerprint of the public key.', type: 'string' }, - length: { $comment: 'Length of the public key.', maximum: 2147483647, minimum: -2147483648, type: 'number' }, - type: { $comment: 'Type of the public key.', type: 'string' }, + comment: { + $comment: 'Comment on the public key.', + type: 'string', + }, + data: { + $comment: 'Original information.', + type: 'string', + }, + fingerprint: { + $comment: 'Fingerprint of the public key.', + type: 'string', + }, + length: { + $comment: 'Length of the public key.', + maximum: 2147483647, + minimum: -2147483648, + type: 'number', + }, + type: { + $comment: 'Type of the public key.', + type: 'string', + }, }, required: ['fingerprint', 'comment', 'length', 'type', 'data'], type: 'object', @@ -276,10 +519,22 @@ export const HOSTPATROL_REPORT_SCHEMA = { meta: { $comment: 'Meta information of the report.\nReport Meta Information\nReportMeta', properties: { - buildHash: { $comment: 'Build hash of the application.', type: 'string' }, - buildTag: { $comment: 'Build tag of the application.', type: 'string' }, - timestamp: { $comment: 'Timestamp of the report.\nDate/time in ISO8601 format.', type: 'string' }, - version: { $comment: 'Version of the application.', type: 'string' }, + buildHash: { + $comment: 'Build hash of the application.', + type: 'string', + }, + buildTag: { + $comment: 'Build tag of the application.', + type: 'string', + }, + timestamp: { + $comment: 'Timestamp of the report.\nDate/time in ISO8601 format.', + type: 'string', + }, + version: { + $comment: 'Version of the application.', + type: 'string', + }, }, required: ['timestamp', 'version'], type: 'object', From caefd22b81140b808fe946a7f9b26ea586076b2e Mon Sep 17 00:00:00 2001 From: Vehbi Sinan Tunalioglu Date: Wed, 15 Jan 2025 18:07:46 +0800 Subject: [PATCH 4/5] feat(website): add time-sync information to host data tabulation --- website/src/components/report/TabulateHosts.tsx | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/website/src/components/report/TabulateHosts.tsx b/website/src/components/report/TabulateHosts.tsx index 2e87839..df24b0d 100644 --- a/website/src/components/report/TabulateHosts.tsx +++ b/website/src/components/report/TabulateHosts.tsx @@ -1,6 +1,6 @@ import { HostReport } from '@/lib/data'; import { Chip } from '@nextui-org/chip'; -import { Radio, RadioGroup, Select, SelectItem, Selection, Slider } from '@nextui-org/react'; +import { Radio, RadioGroup, Select, SelectItem, Selection, Slider, Tooltip } from '@nextui-org/react'; import { Table, TableBody, TableCell, TableColumn, TableHeader, TableRow } from '@nextui-org/table'; import Image from 'next/image'; import Link from 'next/link'; @@ -346,6 +346,7 @@ export function TabulateHosts({ Systemd + Clock Sync Tags @@ -397,6 +398,15 @@ export function TabulateHosts({ {host.systemdServices.length} / {host.systemdTimers.length} + + + + {host.clock.time_sync_status.length > 10 + ? host.clock.time_sync_status.slice(0, 8) + '...' + : host.clock.time_sync_status} + + + {(host.host.tags || []).map((x) => ( From 5f3c0ab654de3021d46e7e6c89389fcb78ac6091 Mon Sep 17 00:00:00 2001 From: Vehbi Sinan Tunalioglu Date: Wed, 15 Jan 2025 18:13:47 +0800 Subject: [PATCH 5/5] feat(website): add time-sync information to host details --- .../src/components/report/ShowHostDetails.tsx | 28 +++++++++++++------ 1 file changed, 19 insertions(+), 9 deletions(-) diff --git a/website/src/components/report/ShowHostDetails.tsx b/website/src/components/report/ShowHostDetails.tsx index 4a8063a..7d1c920 100644 --- a/website/src/components/report/ShowHostDetails.tsx +++ b/website/src/components/report/ShowHostDetails.tsx @@ -56,15 +56,25 @@ export function ShowHostDetails({ host, data }: { host: HostReport; data: HostPa -
- +
+
+ + + +