diff --git a/src/Lhp/Remote.hs b/src/Lhp/Remote.hs index 9643e49..501dde6 100644 --- a/src/Lhp/Remote.hs +++ b/src/Lhp/Remote.hs @@ -35,12 +35,13 @@ compileReport -> m Types.Report compileReport h@Types.Host {..} = do kvs <- (++) <$> _fetchHostInfo _hostName <*> _fetchHostCloudInfo _hostName - Types.Report h - <$> _mkCloud _hostName kvs - <*> _mkHardware _hostName kvs - <*> _mkKernel _hostName kvs - <*> _mkDistribution _hostName kvs - <*> _fetchHostDockerContainers _hostName + let _reportHost = h + _reportCloud <- _mkCloud _hostName kvs + _reportHardware <- _mkHardware _hostName kvs + _reportKernel <- _mkKernel _hostName kvs + _reportDistribution <- _mkDistribution _hostName kvs + _reportDockerContainers <- _fetchHostDockerContainers _hostName + pure Types.Report {..} -- * Errors @@ -110,18 +111,18 @@ _mkCloud -> [(T.Text, T.Text)] -> m Types.Cloud _mkCloud h kvs = - _toParseError h $ - Types.Cloud - <$> (fromMaybe "UNKNOWN" <$> _findParse pure "LHP_CLOUD_NAME" kvs) - <*> _findParse pure "LHP_CLOUD_ID" kvs - <*> _findParse pure "LHP_CLOUD_TYPE" kvs - <*> _findParse pure "LHP_CLOUD_REGION" kvs - <*> _findParse pure "LHP_CLOUD_AVAILABILITY_ZONE" kvs - <*> _findParse pure "LHP_CLOUD_LOCAL_HOSTNAME" kvs - <*> _findParse pure "LHP_CLOUD_LOCAL_ADDRESS" kvs - <*> _findParse pure "LHP_CLOUD_PUBLIC_HOSTNAME" kvs - <*> _findParse pure "LHP_CLOUD_PUBLIC_ADDRESS" kvs - <*> _findParse pure "LHP_CLOUD_RESERVED_ADDRESS" kvs + _toParseError h $ do + _cloudName <- fromMaybe "UNKNOWN" <$> _findParse pure "LHP_CLOUD_NAME" kvs + _cloudHostId <- _findParse pure "LHP_CLOUD_ID" kvs + _cloudHostType <- _findParse pure "LHP_CLOUD_TYPE" kvs + _cloudHostRegion <- _findParse pure "LHP_CLOUD_REGION" kvs + _cloudHostAvailabilityZone <- _findParse pure "LHP_CLOUD_AVAILABILITY_ZONE" kvs + _cloudHostLocalHostname <- _findParse pure "LHP_CLOUD_LOCAL_HOSTNAME" kvs + _cloudHostLocalAddress <- _findParse pure "LHP_CLOUD_LOCAL_ADDRESS" kvs + _cloudHostRemoteHostname <- _findParse pure "LHP_CLOUD_PUBLIC_HOSTNAME" kvs + _cloudHostRemoteAddress <- _findParse pure "LHP_CLOUD_PUBLIC_ADDRESS" kvs + _cloudHostReservedAddress <- _findParse pure "LHP_CLOUD_RESERVED_ADDRESS" kvs + pure Types.Cloud {..} -- | Smart constructor for remote host rudimentary hardware @@ -132,11 +133,11 @@ _mkHardware -> [(T.Text, T.Text)] -> m Types.Hardware _mkHardware h kvs = - _toParseError h $ - Types.Hardware - <$> _getParse _parseRead "LHP_HW_CPU" kvs - <*> _getParse (fmap (_roundS 2 . _toGB) . _parseRead) "LHP_HW_RAM" kvs - <*> _getParse (fmap (_roundS 2 . _toGB) . _parseRead) "LHP_HW_DISK" kvs + _toParseError h $ do + _hardwareCpuCount <- _getParse _parseRead "LHP_HW_CPU" kvs + _hardwareRamTotal <- _getParse (fmap (_roundS 2 . _toGB) . _parseRead) "LHP_HW_RAM" kvs + _hardwareDiskRoot <- _getParse (fmap (_roundS 2 . _toGB) . _parseRead) "LHP_HW_DISK" kvs + pure Types.Hardware {..} -- | Smart constructor for remote host kernel information. @@ -146,14 +147,14 @@ _mkKernel -> [(T.Text, T.Text)] -> m Types.Kernel _mkKernel h kvs = - _toParseError h $ - Types.Kernel - <$> _getParse pure "LHP_KERNEL_NAME" kvs - <*> _getParse pure "LHP_KERNEL_NODE" kvs - <*> _getParse pure "LHP_KERNEL_RELEASE" kvs - <*> _getParse pure "LHP_KERNEL_VERSION" kvs - <*> _getParse pure "LHP_KERNEL_MACHINE" kvs - <*> _getParse pure "LHP_KERNEL_OS" kvs + _toParseError h $ do + _kernelNode <- _getParse pure "LHP_KERNEL_NODE" kvs + _kernelName <- _getParse pure "LHP_KERNEL_NAME" kvs + _kernelRelease <- _getParse pure "LHP_KERNEL_RELEASE" kvs + _kernelVersion <- _getParse pure "LHP_KERNEL_VERSION" kvs + _kernelMachine <- _getParse pure "LHP_KERNEL_MACHINE" kvs + _kernelOs <- _getParse pure "LHP_KERNEL_OS" kvs + pure Types.Kernel {..} -- | Smart constructor for remote host distribution information. @@ -163,14 +164,14 @@ _mkDistribution -> [(T.Text, T.Text)] -> m Types.Distribution _mkDistribution h kvs = - _toParseError h $ - Types.Distribution - <$> _getParse pure "LHP_DISTRO_ID" kvs - <*> _getParse pure "LHP_DISTRO_NAME" kvs - <*> _getParse pure "LHP_DISTRO_VERSION" kvs - <*> _getParse pure "LHP_DISTRO_VERSION_ID" kvs - <*> _findParse pure "LHP_DISTRO_VERSION_CODENAME" kvs - <*> _getParse pure "LHP_DISTRO_PRETTY_NAME" kvs + _toParseError h $ do + _distributionId <- _getParse pure "LHP_DISTRO_ID" kvs + _distributionName <- _getParse pure "LHP_DISTRO_NAME" kvs + _distributionVersion <- _getParse pure "LHP_DISTRO_VERSION" kvs + _distributionRelease <- _getParse pure "LHP_DISTRO_VERSION_ID" kvs + _distributionCodename <- _findParse pure "LHP_DISTRO_VERSION_CODENAME" kvs + _distributionDescription <- _getParse pure "LHP_DISTRO_PRETTY_NAME" kvs + pure Types.Distribution {..} -- | Attempts to parse a list of key/value tuples formatted in the diff --git a/website/package-lock.json b/website/package-lock.json index 1fb90c9..87c6b4e 100644 --- a/website/package-lock.json +++ b/website/package-lock.json @@ -1,15 +1,17 @@ { - "name": "lhp-ui", + "name": "lhpui", "version": "0.0.0", "lockfileVersion": 3, "requires": true, "packages": { "": { - "name": "lhp-ui", + "name": "lhpui", "version": "0.0.0", "dependencies": { "@nextui-org/react": "^2.2.10", + "ajv": "^8.12.0", "framer-motion": "^11.0.8", + "json-schema-to-ts": "^3.0.1", "next": "14.1.3", "purify-ts": "^2.0.3", "react": "^18", @@ -122,6 +124,28 @@ "url": "https://opencollective.com/eslint" } }, + "node_modules/@eslint/eslintrc/node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/@eslint/eslintrc/node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true + }, "node_modules/@eslint/js": { "version": "8.57.0", "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.57.0.tgz", @@ -3027,14 +3051,13 @@ } }, "node_modules/ajv": { - "version": "6.12.6", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", - "dev": true, + "version": "8.12.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.12.0.tgz", + "integrity": "sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==", "dependencies": { "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2", "uri-js": "^4.2.2" }, "funding": { @@ -4326,6 +4349,28 @@ "url": "https://opencollective.com/eslint" } }, + "node_modules/eslint/node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/eslint/node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true + }, "node_modules/espree": { "version": "9.6.1", "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", @@ -4388,8 +4433,7 @@ "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", - "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", - "dev": true + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" }, "node_modules/fast-glob": { "version": "3.3.2", @@ -5375,11 +5419,22 @@ "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", "dev": true }, + "node_modules/json-schema-to-ts": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-schema-to-ts/-/json-schema-to-ts-3.0.1.tgz", + "integrity": "sha512-ANphQxnKbzLWPeYDmdoci8C9g9ttpfMx8etTlJJ8UCEmNXH9jxGkn3AAbMe+lR4N5OG/01nYxPrDyugLdsRt+A==", + "dependencies": { + "@babel/runtime": "^7.18.3", + "ts-algebra": "^1.2.2" + }, + "engines": { + "node": ">=16" + } + }, "node_modules/json-schema-traverse": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", - "dev": true + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==" }, "node_modules/json-stable-stringify-without-jsonify": { "version": "1.0.1", @@ -6288,7 +6343,6 @@ "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", - "dev": true, "engines": { "node": ">=6" } @@ -6495,6 +6549,14 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/resolve": { "version": "1.22.8", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz", @@ -7131,6 +7193,11 @@ "node": ">=8.0" } }, + "node_modules/ts-algebra": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/ts-algebra/-/ts-algebra-1.2.2.tgz", + "integrity": "sha512-kloPhf1hq3JbCPOTYoOWDKxebWjNb2o/LKnNfkWhxVVisFFmMJPPdJeGoGmM+iRLyoXAR61e08Pb+vUXINg8aA==" + }, "node_modules/ts-api-utils": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.2.1.tgz", @@ -7330,7 +7397,6 @@ "version": "4.4.1", "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", - "dev": true, "dependencies": { "punycode": "^2.1.0" } diff --git a/website/package.json b/website/package.json index afbd5b7..4c46145 100644 --- a/website/package.json +++ b/website/package.json @@ -11,7 +11,9 @@ }, "dependencies": { "@nextui-org/react": "^2.2.10", + "ajv": "^8.12.0", "framer-motion": "^11.0.8", + "json-schema-to-ts": "^3.0.1", "next": "14.1.3", "purify-ts": "^2.0.3", "react": "^18", diff --git a/website/src/components/app/-app.tsx b/website/src/components/app/-app.tsx index be237fe..953df62 100644 --- a/website/src/components/app/-app.tsx +++ b/website/src/components/app/-app.tsx @@ -110,7 +110,7 @@ export function TabulateHosts({ hosts, onHostSelect }: { hosts: LhpData[]; onHos > Hostname - Cloud + Cloud Distribution Arch @@ -122,7 +122,7 @@ export function TabulateHosts({ hosts, onHostSelect }: { hosts: LhpData[]; onHos Disk - + Docker Tags @@ -172,7 +172,7 @@ export function TabulateHosts({ hosts, onHostSelect }: { hosts: LhpData[]; onHos {(host.host.tags || []).map((x) => ( - + {x} ))} @@ -200,7 +200,7 @@ export function HostDetails({ host }: { host: LhpData }) {
{(host.host.tags || []).map((x) => ( - + {x} ))} diff --git a/website/src/components/app/-data.tsx b/website/src/components/app/-data.tsx index 7404f90..fb286ed 100644 --- a/website/src/components/app/-data.tsx +++ b/website/src/components/app/-data.tsx @@ -1,66 +1,135 @@ import { Card, CardBody, CardFooter, CardHeader } from '@nextui-org/card'; import { Divider } from '@nextui-org/divider'; +import Ajv from 'ajv'; +import type { JSONSchema } from 'json-schema-to-ts'; +import { FromSchema } from 'json-schema-to-ts'; import { Either, Left, Right } from 'purify-ts/Either'; import { Just, Maybe, Nothing } from 'purify-ts/Maybe'; import { ChangeEvent, useState } from 'react'; import { Centered } from './-ui'; -export interface LhpData { - host: { - name: string; - tags?: string[]; - url?: string; - [k: string]: unknown; - }; - hardware: { - cpuCount: number; - diskRoot: number; - ramTotal: number; - [k: string]: unknown; - }; - kernel: { - machine: string; - name: string; - node: string; - os: string; - release: string; - version: string; - [k: string]: unknown; - }; - distribution: { - description: string; - codename: null | string; - id: string; - name: string; - release: string; - version: string; - [k: string]: unknown; - }; - cloud: { - hostAvailabilityZone: null | string; - hostLocalAddress: null | string; - hostLocalHostname: null | string; - hostRegion: null | string; - hostRemoteAddress: null | string; - hostRemoteHostname: null | string; - hostReservedAddress: null | string; - hostType: null | string; - id: null | string; - name: string; - [k: string]: unknown; - }; - dockerContainers: - | null - | { - created: string; - id: string; - image: string; - name: string; - running: boolean; - [k: string]: unknown; - }[]; - [k: string]: unknown; -} +export const LHP_PATROL_REPORT_SCHEMA = { + $comment: 'Host Patrol Report\nReport', + properties: { + cloud: { + $comment: 'Cloud information.\nCloud Information\nCloud', + properties: { + hostAvailabilityZone: { $comment: 'Host availability zone.', anyOf: [{ type: 'null' }, { type: 'string' }] }, + hostLocalAddress: { $comment: 'Local address of the host.', anyOf: [{ type: 'null' }, { type: 'string' }] }, + hostLocalHostname: { $comment: 'Local hostname of the host.', 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' }] }, + hostRemoteHostname: { $comment: 'Remote hostname of the host.', anyOf: [{ type: 'null' }, { type: 'string' }] }, + hostReservedAddress: { + $comment: 'Reserved address of the host.', + 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' }, + }, + required: [ + 'hostReservedAddress', + 'hostRemoteAddress', + 'hostRemoteHostname', + 'hostLocalAddress', + 'hostLocalHostname', + 'hostAvailabilityZone', + 'hostRegion', + 'hostType', + 'id', + 'name', + ], + type: 'object', + }, + distribution: { + $comment: 'Distribution information.\nDistribution Information\nDistribution', + properties: { + codename: { + $comment: "Distribution codename (cat /etc/os-release | grep 'VERSION_CODENAME=').", + 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' }, + }, + required: ['description', 'codename', 'release', 'version', 'name', 'id'], + type: 'object', + }, + dockerContainers: { + $comment: 'List of Docker containers if the host is a Docker host.', + anyOf: [ + { type: 'null' }, + { + items: { + $comment: 'Docker Container Information\nDockerContainer', + properties: { + created: { $comment: 'Date/time when the container is created at.\nLocalTime', 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' }, + }, + required: ['running', 'created', 'image', 'name', 'id'], + type: 'object', + }, + type: 'array', + }, + ], + }, + hardware: { + $comment: 'Hardware information.\nRudimentary Hardware Information\nHardware', + properties: { + cpuCount: { + $comment: 'Number of (v)CPU cores.', + // maximum: 9223372036854775807, + // minimum: -9223372036854775808, + 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', + }, + host: { + $comment: 'Host descriptor.\nHost Descriptor\nHost', + properties: { + name: { $comment: 'Name of the host.', 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', + }, + 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' }, + }, + required: ['os', 'machine', 'version', 'release', 'name', 'node'], + type: 'object', + }, + }, + required: ['dockerContainers', 'distribution', 'kernel', 'hardware', 'cloud', 'host'], + type: 'object', +} as const satisfies JSONSchema; + +export type LhpData = FromSchema; + +const AJV = new Ajv(); + +const LHP_PATROL_REPORT_VALIDATOR = AJV.compile(LHP_PATROL_REPORT_SCHEMA); const _LOCAL_STORAGE_KEY_DATA = 'LHP_DATA'; @@ -84,7 +153,22 @@ export function deleteData(): void { export function parseData(raw: string): Either { try { - return Right(JSON.parse(raw) as LhpData[]); // TODO: Parse (or validate) + const parsed = JSON.parse(raw); + + if (!Array.isArray(parsed)) { + return Left('Data is expected to be an array of host information objects.'); + } + + for (const elem of parsed) { + const result = LHP_PATROL_REPORT_VALIDATOR(elem); + + if (!result) { + console.error(elem, LHP_PATROL_REPORT_VALIDATOR.errors); + return Left('Invalid host information object.'); + } + } + + return Right(parsed); } catch (err) { return Left(`Data can not be parsed into JSON. ${err}`); } diff --git a/website/src/components/header.tsx b/website/src/components/header.tsx index ea8b613..3363a2b 100644 --- a/website/src/components/header.tsx +++ b/website/src/components/header.tsx @@ -16,7 +16,13 @@ export default function Header() { const [isMenuOpen, setIsMenuOpen] = useState(false); return ( - +

lhp