From 01e0ffcbc05c54752955ef7974a6bac1c8032378 Mon Sep 17 00:00:00 2001 From: Eli Bosley Date: Tue, 2 May 2023 11:39:35 -0400 Subject: [PATCH] fix: app can be linted (#639) --- api/codegen.yml | 70 ++ api/dev/states/myservers.cfg | 4 +- .../common/dashboard/generate-data.test.ts | 6 +- api/src/cli/commands/start.ts | 2 +- api/src/common/allowed-origins.ts | 93 +++ api/src/common/dashboard/boot-timestamp.ts | 4 + api/src/common/dashboard/generate-data.ts | 41 +- api/src/core/utils/shares/process-share.ts | 1 + api/src/graphql/generate/validators.ts | 214 +++++ api/src/graphql/generated/api/types.ts | 2 +- api/src/graphql/generated/client/graphql.ts | 770 ++++++++++++++++++ api/src/graphql/resolvers/query/info.ts | 464 +++++++++++ api/src/store/modules/docker.ts | 1 - api/src/store/watch/docker-watch.ts | 25 +- 14 files changed, 1651 insertions(+), 46 deletions(-) create mode 100644 api/codegen.yml create mode 100644 api/src/common/allowed-origins.ts create mode 100644 api/src/common/dashboard/boot-timestamp.ts create mode 100644 api/src/graphql/generate/validators.ts create mode 100644 api/src/graphql/generated/client/graphql.ts create mode 100644 api/src/graphql/resolvers/query/info.ts diff --git a/api/codegen.yml b/api/codegen.yml new file mode 100644 index 000000000..539b8cb1d --- /dev/null +++ b/api/codegen.yml @@ -0,0 +1,70 @@ +overwrite: true +emitLegacyCommonJSImports: false +verbose: true +require: + - ts-node/register +config: + namingConvention: + typeNames: './fix-array-type.cjs' + enumValues: 'change-case#upperCase' + useTypeImports: true + scalars: + DateTime: string + Long: number + JSON: "{ [key: string]: any }" + URL: URL + Port: number + UUID: string + +generates: + src/graphql/generated/client/: + documents: './src/graphql/mothership/*.ts' + schema: + '${MOTHERSHIP_GRAPHQL_LINK}': + headers: + origin: 'https://forums.unraid.net' + preset: client + presetConfig: + gqlTagName: graphql + config: + useTypeImports: true + withObjectType: true + plugins: + - add: { content: '/* eslint-disable */' } + src/graphql/generate/validators.ts: + schema: + '${MOTHERSHIP_GRAPHQL_LINK}': + headers: + origin: 'https://forums.unraid.net' + plugins: + - typescript-validation-schema + - add: { content: '/* eslint-disable */'} + config: + importFrom: '@app/graphql/generated/client/graphql' + strictScalars: false + schema: 'zod' +# Generate Types for the API Server + src/graphql/generated/api/types.ts: + schema: + - './src/graphql/types.ts' + - './src/graphql/schema/types/**/*.graphql' + plugins: + - typescript + - typescript-resolvers + - add: { content: '/* eslint-disable */' } + config: + contextType: '@app/graphql/schema/utils#Context' + useIndexSignature: true +# Generate Operations for any built in API Server Operations (ie report.ts) + src/graphql/generated/api/operations.ts: + documents: './src/graphql/client/api/*.ts' + schema: + - './src/graphql/types.ts' + - './src/graphql/schema/types/**/*.graphql' + preset: import-types + presetConfig: + typesPath: '@app/graphql/generated/api/types' + plugins: + - typescript-operations + - typed-document-node + - add: { content: '/* eslint-disable */' } diff --git a/api/dev/states/myservers.cfg b/api/dev/states/myservers.cfg index 6536b47c5..dabdfaf77 100644 --- a/api/dev/states/myservers.cfg +++ b/api/dev/states/myservers.cfg @@ -1,5 +1,5 @@ [api] -version="3.1.0+948d5ecf" +version="3.1.0+0baf1385" [local] [notifier] apikey="unnotify_30994bfaccf839c65bae75f7fa12dd5ee16e69389f754c3b98ed7d5" @@ -19,4 +19,4 @@ allowedOrigins="/var/run/unraid-notifications.sock, /var/run/unraid-php.sock, /v [upc] apikey="unupc_fab6ff6ffe51040595c6d9ffb63a353ba16cc2ad7d93f813a2e80a5810" [connectionStatus] -minigraph="PRE_INIT" +minigraph="CONNECTED" diff --git a/api/src/__test__/common/dashboard/generate-data.test.ts b/api/src/__test__/common/dashboard/generate-data.test.ts index 1a73d1178..460dffe91 100644 --- a/api/src/__test__/common/dashboard/generate-data.test.ts +++ b/api/src/__test__/common/dashboard/generate-data.test.ts @@ -83,14 +83,14 @@ test('Returns generated data', async () => { }, "os": { "hostname": "Tower", - "uptime": 2022-06-10T04:35:58.276Z, + "uptime": "2022-06-10T04:35:58.276Z", }, "services": [ { "name": "unraid-api", "online": true, "uptime": { - "timestamp": 2022-06-10T04:35:58.276Z, + "timestamp": "2022-06-10T04:35:58.276Z", }, "version": "THIS_WILL_BE_REPLACED_WHEN_BUILT", }, @@ -98,7 +98,7 @@ test('Returns generated data', async () => { "name": "dynamic-remote-access", "online": false, "uptime": { - "timestamp": 2022-06-10T04:35:58.276Z, + "timestamp": "2022-06-10T04:35:58.276Z", }, "version": "DISABLED", }, diff --git a/api/src/cli/commands/start.ts b/api/src/cli/commands/start.ts index 0d7939b26..b1de06f2b 100644 --- a/api/src/cli/commands/start.ts +++ b/api/src/cli/commands/start.ts @@ -63,7 +63,7 @@ export const start = async () => { // Spawn child // First arg is path (inside PKG), second arg is restart, stop, etc, rest is args to main argument - const [path, _, ...rest] = process.argv.slice(1); + const [path, , ...rest] = process.argv.slice(1); const replacedCommand = [path, 'start', ...rest]; const child = spawn(process.execPath, replacedCommand, { // In the parent set the tracking environment variable diff --git a/api/src/common/allowed-origins.ts b/api/src/common/allowed-origins.ts new file mode 100644 index 000000000..a2b0f9a01 --- /dev/null +++ b/api/src/common/allowed-origins.ts @@ -0,0 +1,93 @@ +import { getters, type RootState, store } from '@app/store'; +import { uniq } from 'lodash'; +import { getServerIps, getUrlForField } from '@app/graphql/resolvers/subscription/network'; +import { FileLoadStatus } from '@app/store/types'; +import { logger } from '../core'; +import { ENVIRONMENT, INTROSPECTION } from '@app/environment'; + +const getAllowedSocks = (): string[] => [ + // Notifier bridge + '/var/run/unraid-notifications.sock', + + // Unraid PHP scripts + '/var/run/unraid-php.sock', + + // CLI + '/var/run/unraid-cli.sock', +]; + +const getLocalAccessUrlsForServer = (state: RootState = store.getState()): string[] => { + const { emhttp } = state; + if (emhttp.status !== FileLoadStatus.LOADED) { + return []; + } + + const { nginx } = emhttp; + try { + return [ + getUrlForField({ url: 'localhost', port: nginx.httpPort }).toString(), + getUrlForField({ url: 'localhost', portSsl: nginx.httpsPort }).toString(), + ]; + } catch (error: unknown) { + logger.debug('Caught error in getLocalAccessUrlsForServer: \n%o', error); + return []; + } +}; + +const getRemoteAccessUrlsForAllowedOrigins = (state: RootState = store.getState()): string[] => { + const { urls } = getServerIps(state); + + if (urls) { + return urls.reduce((acc, curr) => { + if (curr.ipv4 && curr.ipv6) { + acc.push(curr.ipv4.toString()); + } else if (curr.ipv4) { + acc.push(curr.ipv4.toString()); + } else if (curr.ipv6) { + acc.push(curr.ipv6.toString()); + } + + return acc; + }, []); + } + + return []; +}; + +const getExtraOrigins = (): string[] => { + const { extraOrigins } = getters.config().api; + if (extraOrigins) { + return extraOrigins.split(', ').filter(origin => origin.startsWith('http://') || origin.startsWith('https://')); + } + + return []; +}; + +const getConnectOrigins = () : string[] => { + const connectMain = 'https://connect.myunraid.net'; + const connectStaging = 'https://staging.connect.myunraid.net'; + const connectDev = 'https://dev-my.myunraid.net:4000'; + + return [ + connectMain, + connectStaging, + connectDev + ] +} + +const getApolloSandbox = (): string[] => { + if (INTROSPECTION || ENVIRONMENT === 'development') { + return ['https://studio.apollographql.com']; + } + return []; +} + +export const getAllowedOrigins = (state: RootState = store.getState()): string[] => uniq([ + ...getAllowedSocks(), + ...getLocalAccessUrlsForServer(), + ...getRemoteAccessUrlsForAllowedOrigins(state), + ...getExtraOrigins(), + ...getConnectOrigins(), + ...getApolloSandbox() + +]).map(url => url.endsWith('/') ? url.slice(0, -1) : url); diff --git a/api/src/common/dashboard/boot-timestamp.ts b/api/src/common/dashboard/boot-timestamp.ts new file mode 100644 index 000000000..5ca4cdbe5 --- /dev/null +++ b/api/src/common/dashboard/boot-timestamp.ts @@ -0,0 +1,4 @@ +import { uptime } from 'os'; + +// Get uptime on boot and convert to date +export const bootTimestamp = new Date(new Date().getTime() - (uptime() * 1_000)); \ No newline at end of file diff --git a/api/src/common/dashboard/generate-data.ts b/api/src/common/dashboard/generate-data.ts index af21d469c..92c58e213 100644 --- a/api/src/common/dashboard/generate-data.ts +++ b/api/src/common/dashboard/generate-data.ts @@ -1,7 +1,6 @@ import { ConnectListAllDomainsFlags } from '@vmngr/libvirt'; import { getHypervisor } from '@app/core/utils/vms/get-hypervisor'; import display from '@app/graphql/resolvers/query/display'; -import { docker } from '@app/core/utils/clients/docker'; import { getUnraidVersion } from '@app/common/dashboard/get-unraid-version'; import { getArray } from '@app/common/dashboard/get-array'; import { bootTimestamp } from '@app/common/dashboard/boot-timestamp'; @@ -37,22 +36,7 @@ const getVmSummary = async (): Promise => { } }; -/* -const twoFactor = (): Dashboard['twoFactor'] => { - const { isRemoteEnabled, isLocalEnabled } = checkTwoFactorEnabled(); - return { - remote: { - enabled: isRemoteEnabled, - }, - local: { - enabled: isLocalEnabled, - }, - }; -}; */ - const getDynamicRemoteAccessService = (): DashboardServiceInput | null => { - const uptimeTimestamp = bootTimestamp.toISOString(); - const { config, dynamicRemoteAccess } = store.getState(); const enabledStatus = config.remote.dynamicRemoteAccessType; @@ -61,23 +45,24 @@ const getDynamicRemoteAccessService = (): DashboardServiceInput | null => { online: enabledStatus !== DynamicRemoteAccessType.DISABLED, version: dynamicRemoteAccess.runningType, uptime: { - timestamp: new Date(uptimeTimestamp), + timestamp: bootTimestamp.toISOString(), }, }; }; const services = (): DashboardInput['services'] => { - const uptimeTimestamp = bootTimestamp.toISOString(); const dynamicRemoteAccess = getDynamicRemoteAccessService(); - return [{ - name: 'unraid-api', - online: true, - uptime: { - timestamp: new Date(uptimeTimestamp), - }, - version: API_VERSION, - }, - ...(dynamicRemoteAccess ? [dynamicRemoteAccess] : [])]; + return [ + { + name: 'unraid-api', + online: true, + uptime: { + timestamp: bootTimestamp.toISOString(), + }, + version: API_VERSION, + }, + ...(dynamicRemoteAccess ? [dynamicRemoteAccess] : []), + ]; }; const getData = async (): Promise => { @@ -99,7 +84,7 @@ const getData = async (): Promise => { }, os: { hostname: emhttp.var.name, - uptime: new Date(bootTimestamp.toISOString()), + uptime: bootTimestamp.toISOString() }, vms: await getVmSummary(), array: getArray(), diff --git a/api/src/core/utils/shares/process-share.ts b/api/src/core/utils/shares/process-share.ts index 6bcfec15f..0dd4782ec 100644 --- a/api/src/core/utils/shares/process-share.ts +++ b/api/src/core/utils/shares/process-share.ts @@ -1,3 +1,4 @@ +/* eslint-disable @typescript-eslint/no-unused-vars */ import { getters } from '@app/store'; import type { DiskShare, Share, UserShare } from '@app/core/types/states/share'; import { type ArrayDisk } from '@app/graphql/generated/api/types'; diff --git a/api/src/graphql/generate/validators.ts b/api/src/graphql/generate/validators.ts new file mode 100644 index 000000000..068f335e4 --- /dev/null +++ b/api/src/graphql/generate/validators.ts @@ -0,0 +1,214 @@ +/* eslint-disable */ +import { z } from 'zod' +import { AccessUrlInput, ArrayCapacityBytesInput, ArrayCapacityInput, ClientType, ConfigErrorState, DashboardAppsInput, DashboardArrayInput, DashboardCaseInput, DashboardConfigInput, DashboardDisplayInput, DashboardInput, DashboardOsInput, DashboardServiceInput, DashboardServiceUptimeInput, DashboardTwoFactorInput, DashboardTwoFactorLocalInput, DashboardTwoFactorRemoteInput, DashboardVarsInput, DashboardVersionsInput, DashboardVmsInput, EventType, Importance, KeyType, NetworkInput, NotificationInput, NotificationStatus, PingEventSource, RegistrationState, RemoteAccessEventActionType, RemoteAccessInput, RemoteGraphQLClientInput, RemoteGraphQLEventType, RemoteGraphQLServerInput, ServerStatus, URL_TYPE, UpdateType } from '@app/graphql/generated/client/graphql' + +type Properties = Required<{ + [K in keyof T]: z.ZodType; +}>; + +type definedNonNullAny = {}; + +export const isDefinedNonNullAny = (v: any): v is definedNonNullAny => v !== undefined && v !== null; + +export const definedNonNullAnySchema = z.any().refine((v) => isDefinedNonNullAny(v)); + +export function AccessUrlInputSchema(): z.ZodObject> { + return z.object>({ + ipv4: definedNonNullAnySchema.nullish(), + ipv6: definedNonNullAnySchema.nullish(), + name: z.string().nullish(), + type: definedNonNullAnySchema + }) +} + +export function ArrayCapacityBytesInputSchema(): z.ZodObject> { + return z.object>({ + free: z.number().nullish(), + total: z.number().nullish(), + used: z.number().nullish() + }) +} + +export function ArrayCapacityInputSchema(): z.ZodObject> { + return z.object>({ + bytes: z.lazy(() => definedNonNullAnySchema.nullish()) + }) +} + +export const ClientTypeSchema = z.nativeEnum(ClientType); + +export const ConfigErrorStateSchema = z.nativeEnum(ConfigErrorState); + +export function DashboardAppsInputSchema(): z.ZodObject> { + return z.object>({ + installed: z.number(), + started: z.number() + }) +} + +export function DashboardArrayInputSchema(): z.ZodObject> { + return z.object>({ + capacity: z.lazy(() => definedNonNullAnySchema), + state: z.string() + }) +} + +export function DashboardCaseInputSchema(): z.ZodObject> { + return z.object>({ + base64: z.string(), + error: z.string().nullish(), + icon: z.string(), + url: z.string() + }) +} + +export function DashboardConfigInputSchema(): z.ZodObject> { + return z.object>({ + error: z.string().nullish(), + valid: z.boolean() + }) +} + +export function DashboardDisplayInputSchema(): z.ZodObject> { + return z.object>({ + case: z.lazy(() => definedNonNullAnySchema) + }) +} + +export function DashboardInputSchema(): z.ZodObject> { + return z.object>({ + apps: z.lazy(() => definedNonNullAnySchema), + array: z.lazy(() => definedNonNullAnySchema), + config: z.lazy(() => definedNonNullAnySchema), + display: z.lazy(() => definedNonNullAnySchema), + os: z.lazy(() => definedNonNullAnySchema), + services: z.array(z.lazy(() => definedNonNullAnySchema)), + twoFactor: z.lazy(() => definedNonNullAnySchema.nullish()), + vars: z.lazy(() => definedNonNullAnySchema), + versions: z.lazy(() => definedNonNullAnySchema), + vms: z.lazy(() => definedNonNullAnySchema) + }) +} + +export function DashboardOsInputSchema(): z.ZodObject> { + return z.object>({ + hostname: z.string(), + uptime: z.string() + }) +} + +export function DashboardServiceInputSchema(): z.ZodObject> { + return z.object>({ + name: z.string(), + online: z.boolean(), + uptime: z.lazy(() => definedNonNullAnySchema.nullish()), + version: z.string() + }) +} + +export function DashboardServiceUptimeInputSchema(): z.ZodObject> { + return z.object>({ + timestamp: z.string() + }) +} + +export function DashboardTwoFactorInputSchema(): z.ZodObject> { + return z.object>({ + local: z.lazy(() => definedNonNullAnySchema), + remote: z.lazy(() => definedNonNullAnySchema) + }) +} + +export function DashboardTwoFactorLocalInputSchema(): z.ZodObject> { + return z.object>({ + enabled: z.boolean() + }) +} + +export function DashboardTwoFactorRemoteInputSchema(): z.ZodObject> { + return z.object>({ + enabled: z.boolean() + }) +} + +export function DashboardVarsInputSchema(): z.ZodObject> { + return z.object>({ + flashGuid: z.string(), + regState: z.string(), + regTy: z.string() + }) +} + +export function DashboardVersionsInputSchema(): z.ZodObject> { + return z.object>({ + unraid: z.string() + }) +} + +export function DashboardVmsInputSchema(): z.ZodObject> { + return z.object>({ + installed: z.number(), + started: z.number() + }) +} + +export const EventTypeSchema = z.nativeEnum(EventType); + +export const ImportanceSchema = z.nativeEnum(Importance); + +export const KeyTypeSchema = z.nativeEnum(KeyType); + +export function NetworkInputSchema(): z.ZodObject> { + return z.object>({ + accessUrls: z.array(z.lazy(() => definedNonNullAnySchema)) + }) +} + +export function NotificationInputSchema(): z.ZodObject> { + return z.object>({ + description: z.string().nullish(), + importance: definedNonNullAnySchema, + link: z.string().nullish(), + subject: z.string().nullish(), + title: z.string().nullish() + }) +} + +export const NotificationStatusSchema = z.nativeEnum(NotificationStatus); + +export const PingEventSourceSchema = z.nativeEnum(PingEventSource); + +export const RegistrationStateSchema = z.nativeEnum(RegistrationState); + +export const RemoteAccessEventActionTypeSchema = z.nativeEnum(RemoteAccessEventActionType); + +export function RemoteAccessInputSchema(): z.ZodObject> { + return z.object>({ + apiKey: z.string(), + type: definedNonNullAnySchema, + url: z.lazy(() => definedNonNullAnySchema.nullish()) + }) +} + +export function RemoteGraphQLClientInputSchema(): z.ZodObject> { + return z.object>({ + apiKey: z.string(), + body: z.string() + }) +} + +export const RemoteGraphQLEventTypeSchema = z.nativeEnum(RemoteGraphQLEventType); + +export function RemoteGraphQLServerInputSchema(): z.ZodObject> { + return z.object>({ + body: z.string(), + sha256: z.string(), + type: definedNonNullAnySchema + }) +} + +export const ServerStatusSchema = z.nativeEnum(ServerStatus); + +export const URL_TYPESchema = z.nativeEnum(URL_TYPE); + +export const UpdateTypeSchema = z.nativeEnum(UpdateType); diff --git a/api/src/graphql/generated/api/types.ts b/api/src/graphql/generated/api/types.ts index fda00b6ed..00ab587a6 100644 --- a/api/src/graphql/generated/api/types.ts +++ b/api/src/graphql/generated/api/types.ts @@ -14,7 +14,7 @@ export type Scalars = { Boolean: boolean; Int: number; Float: number; - DateTime: Date; + DateTime: string; JSON: { [key: string]: any }; Long: number; UUID: string; diff --git a/api/src/graphql/generated/client/graphql.ts b/api/src/graphql/generated/client/graphql.ts new file mode 100644 index 000000000..de77dbfc8 --- /dev/null +++ b/api/src/graphql/generated/client/graphql.ts @@ -0,0 +1,770 @@ +/* eslint-disable */ +import type { TypedDocumentNode as DocumentNode } from '@graphql-typed-document-node/core'; +export type Maybe = T | null; +export type InputMaybe = Maybe; +export type Exact = { [K in keyof T]: T[K] }; +export type MakeOptional = Omit & { [SubKey in K]?: Maybe }; +export type MakeMaybe = Omit & { [SubKey in K]: Maybe }; +/** All built-in and custom scalars, mapped to their actual values */ +export type Scalars = { + ID: string; + String: string; + Boolean: boolean; + Int: number; + Float: number; + /** A date-time string at UTC, such as 2007-12-03T10:15:30Z, compliant with the `date-time` format outlined in section 5.6 of the RFC 3339 profile of the ISO 8601 standard for representation of dates and times using the Gregorian calendar. */ + DateTime: string; + /** A field whose value is a IPv4 address: https://en.wikipedia.org/wiki/IPv4. */ + IPv4: any; + /** A field whose value is a IPv6 address: https://en.wikipedia.org/wiki/IPv6. */ + IPv6: any; + /** The `JSON` scalar type represents JSON values as specified by [ECMA-404](http://www.ecma-international.org/publications/files/ECMA-ST/ECMA-404.pdf). */ + JSON: { [key: string]: any }; + /** The `Long` scalar type represents 52-bit integers */ + Long: number; + /** A field whose value is a valid TCP port within the range of 0 to 65535: https://en.wikipedia.org/wiki/Transmission_Control_Protocol#TCP_ports */ + Port: number; + /** A field whose value conforms to the standard URL format as specified in RFC3986: https://www.ietf.org/rfc/rfc3986.txt. */ + URL: URL; +}; + +export type AccessUrl = { + __typename?: 'AccessUrl'; + ipv4?: Maybe; + ipv6?: Maybe; + name?: Maybe; + type: URL_TYPE; +}; + +export type AccessUrlInput = { + ipv4?: InputMaybe; + ipv6?: InputMaybe; + name?: InputMaybe; + type: URL_TYPE; +}; + +export type ArrayCapacity = { + __typename?: 'ArrayCapacity'; + bytes?: Maybe; +}; + +export type ArrayCapacityBytes = { + __typename?: 'ArrayCapacityBytes'; + free?: Maybe; + total?: Maybe; + used?: Maybe; +}; + +export type ArrayCapacityBytesInput = { + free?: InputMaybe; + total?: InputMaybe; + used?: InputMaybe; +}; + +export type ArrayCapacityInput = { + bytes?: InputMaybe; +}; + +export type ClientConnectedEvent = { + __typename?: 'ClientConnectedEvent'; + data: ClientConnectionEventData; + type: EventType; +}; + +export type ClientConnectionEventData = { + __typename?: 'ClientConnectionEventData'; + apiKey: Scalars['String']; + type: ClientType; + version: Scalars['String']; +}; + +export type ClientDisconnectedEvent = { + __typename?: 'ClientDisconnectedEvent'; + data: ClientConnectionEventData; + type: EventType; +}; + +export type ClientPingEvent = { + __typename?: 'ClientPingEvent'; + data: PingEventData; + type: EventType; +}; + +export enum ClientType { + API = 'API', + DASHBOARD = 'DASHBOARD' +} + +export type Config = { + __typename?: 'Config'; + error?: Maybe; + valid?: Maybe; +}; + +export enum ConfigErrorState { + INVALID = 'INVALID', + NO_KEY_SERVER = 'NO_KEY_SERVER', + UNKNOWN_ERROR = 'UNKNOWN_ERROR', + WITHDRAWN = 'WITHDRAWN' +} + +export type Dashboard = { + __typename?: 'Dashboard'; + apps?: Maybe; + array?: Maybe; + config?: Maybe; + display?: Maybe; + id: Scalars['ID']; + lastPublish?: Maybe; + network?: Maybe; + online?: Maybe; + os?: Maybe; + services?: Maybe>>; + twoFactor?: Maybe; + vars?: Maybe; + versions?: Maybe; + vms?: Maybe; +}; + +export type DashboardApps = { + __typename?: 'DashboardApps'; + installed?: Maybe; + started?: Maybe; +}; + +export type DashboardAppsInput = { + installed: Scalars['Int']; + started: Scalars['Int']; +}; + +export type DashboardArray = { + __typename?: 'DashboardArray'; + /** Current array capacity */ + capacity?: Maybe; + /** Current array state */ + state?: Maybe; +}; + +export type DashboardArrayInput = { + /** Current array capacity */ + capacity: ArrayCapacityInput; + /** Current array state */ + state: Scalars['String']; +}; + +export type DashboardCase = { + __typename?: 'DashboardCase'; + base64?: Maybe; + error?: Maybe; + icon?: Maybe; + url?: Maybe; +}; + +export type DashboardCaseInput = { + base64: Scalars['String']; + error?: InputMaybe; + icon: Scalars['String']; + url: Scalars['String']; +}; + +export type DashboardConfig = { + __typename?: 'DashboardConfig'; + error?: Maybe; + valid?: Maybe; +}; + +export type DashboardConfigInput = { + error?: InputMaybe; + valid: Scalars['Boolean']; +}; + +export type DashboardDisplay = { + __typename?: 'DashboardDisplay'; + case?: Maybe; +}; + +export type DashboardDisplayInput = { + case: DashboardCaseInput; +}; + +export type DashboardInput = { + apps: DashboardAppsInput; + array: DashboardArrayInput; + config: DashboardConfigInput; + display: DashboardDisplayInput; + os: DashboardOsInput; + services: Array; + twoFactor?: InputMaybe; + vars: DashboardVarsInput; + versions: DashboardVersionsInput; + vms: DashboardVmsInput; +}; + +export type DashboardOs = { + __typename?: 'DashboardOs'; + hostname?: Maybe; + uptime?: Maybe; +}; + +export type DashboardOsInput = { + hostname: Scalars['String']; + uptime: Scalars['DateTime']; +}; + +export type DashboardService = { + __typename?: 'DashboardService'; + name?: Maybe; + online?: Maybe; + uptime?: Maybe; + version?: Maybe; +}; + +export type DashboardServiceInput = { + name: Scalars['String']; + online: Scalars['Boolean']; + uptime?: InputMaybe; + version: Scalars['String']; +}; + +export type DashboardServiceUptime = { + __typename?: 'DashboardServiceUptime'; + timestamp?: Maybe; +}; + +export type DashboardServiceUptimeInput = { + timestamp: Scalars['DateTime']; +}; + +export type DashboardTwoFactor = { + __typename?: 'DashboardTwoFactor'; + local?: Maybe; + remote?: Maybe; +}; + +export type DashboardTwoFactorInput = { + local: DashboardTwoFactorLocalInput; + remote: DashboardTwoFactorRemoteInput; +}; + +export type DashboardTwoFactorLocal = { + __typename?: 'DashboardTwoFactorLocal'; + enabled?: Maybe; +}; + +export type DashboardTwoFactorLocalInput = { + enabled: Scalars['Boolean']; +}; + +export type DashboardTwoFactorRemote = { + __typename?: 'DashboardTwoFactorRemote'; + enabled?: Maybe; +}; + +export type DashboardTwoFactorRemoteInput = { + enabled: Scalars['Boolean']; +}; + +export type DashboardVars = { + __typename?: 'DashboardVars'; + flashGuid?: Maybe; + regState?: Maybe; + regTy?: Maybe; +}; + +export type DashboardVarsInput = { + flashGuid: Scalars['String']; + regState: Scalars['String']; + regTy: Scalars['String']; +}; + +export type DashboardVersions = { + __typename?: 'DashboardVersions'; + unraid?: Maybe; +}; + +export type DashboardVersionsInput = { + unraid: Scalars['String']; +}; + +export type DashboardVms = { + __typename?: 'DashboardVms'; + installed?: Maybe; + started?: Maybe; +}; + +export type DashboardVmsInput = { + installed: Scalars['Int']; + started: Scalars['Int']; +}; + +export type Event = ClientConnectedEvent | ClientDisconnectedEvent | ClientPingEvent | RemoteAccessEvent | RemoteGraphQLEvent | UpdateEvent; + +export enum EventType { + CLIENT_CONNECTED_EVENT = 'CLIENT_CONNECTED_EVENT', + CLIENT_DISCONNECTED_EVENT = 'CLIENT_DISCONNECTED_EVENT', + CLIENT_PING_EVENT = 'CLIENT_PING_EVENT', + REMOTE_ACCESS_EVENT = 'REMOTE_ACCESS_EVENT', + REMOTE_GRAPHQL_EVENT = 'REMOTE_GRAPHQL_EVENT', + UPDATE_EVENT = 'UPDATE_EVENT' +} + +export type FullServerDetails = { + __typename?: 'FullServerDetails'; + apiConnectedCount?: Maybe; + apiVersion?: Maybe; + connectionTimestamp?: Maybe; + dashboard?: Maybe; + lastPublish?: Maybe; + network?: Maybe; + online?: Maybe; +}; + +export enum Importance { + ALERT = 'ALERT', + INFO = 'INFO', + WARNING = 'WARNING' +} + +export enum KeyType { + BASIC = 'BASIC', + PLUS = 'PLUS', + PRO = 'PRO', + TRIAL = 'TRIAL' +} + +export type KsServerDetails = { + __typename?: 'KsServerDetails'; + accessLabel: Scalars['String']; + accessUrl: Scalars['String']; + apiKey?: Maybe; + description: Scalars['String']; + dnsHash: Scalars['String']; + flashBackupDate?: Maybe; + flashBackupUrl: Scalars['String']; + flashProduct: Scalars['String']; + flashVendor: Scalars['String']; + guid: Scalars['String']; + ipsId: Scalars['String']; + keyType: KeyType; + licenseKey: Scalars['String']; + name: Scalars['String']; + plgVersion?: Maybe; + signedIn: Scalars['Boolean']; +}; + +export type LegacyService = { + __typename?: 'LegacyService'; + name?: Maybe; + online?: Maybe; + uptime?: Maybe; + version?: Maybe; +}; + +export type Mutation = { + __typename?: 'Mutation'; + remoteGraphQLResponse: Scalars['Boolean']; + remoteMutation: Scalars['String']; + remoteSession?: Maybe; + sendNotification?: Maybe; + sendPing?: Maybe; + updateDashboard: Dashboard; + updateNetwork: Network; +}; + + +export type MutationremoteGraphQLResponseArgs = { + input: RemoteGraphQLServerInput; +}; + + +export type MutationremoteMutationArgs = { + input: RemoteGraphQLClientInput; +}; + + +export type MutationremoteSessionArgs = { + remoteAccess: RemoteAccessInput; +}; + + +export type MutationsendNotificationArgs = { + notification: NotificationInput; +}; + + +export type MutationupdateDashboardArgs = { + data: DashboardInput; +}; + + +export type MutationupdateNetworkArgs = { + data: NetworkInput; +}; + +export type Network = { + __typename?: 'Network'; + accessUrls?: Maybe>; +}; + +export type NetworkInput = { + accessUrls: Array; +}; + +export type Notification = { + __typename?: 'Notification'; + description?: Maybe; + importance?: Maybe; + link?: Maybe; + status: NotificationStatus; + subject?: Maybe; + title?: Maybe; +}; + +export type NotificationInput = { + description?: InputMaybe; + importance: Importance; + link?: InputMaybe; + subject?: InputMaybe; + title?: InputMaybe; +}; + +export enum NotificationStatus { + FAILED_TO_SEND = 'FAILED_TO_SEND', + NOT_FOUND = 'NOT_FOUND', + PENDING = 'PENDING', + SENT = 'SENT' +} + +export type PingEvent = { + __typename?: 'PingEvent'; + data?: Maybe; + type: EventType; +}; + +export type PingEventData = { + __typename?: 'PingEventData'; + source: PingEventSource; +}; + +export enum PingEventSource { + API = 'API', + MOTHERSHIP = 'MOTHERSHIP' +} + +export type ProfileModel = { + __typename?: 'ProfileModel'; + avatar?: Maybe; + url?: Maybe; + userId?: Maybe; + username?: Maybe; +}; + +export type Query = { + __typename?: 'Query'; + apiVersion?: Maybe; + dashboard?: Maybe; + ksServers: Array; + online?: Maybe; + remoteQuery: Scalars['String']; + servers: Array>; + status?: Maybe; +}; + + +export type QuerydashboardArgs = { + id: Scalars['String']; +}; + + +export type QueryremoteQueryArgs = { + input: RemoteGraphQLClientInput; +}; + +export enum RegistrationState { + /** Basic */ + BASIC = 'BASIC', + /** BLACKLISTED */ + EBLACKLISTED = 'EBLACKLISTED', + /** BLACKLISTED */ + EBLACKLISTED1 = 'EBLACKLISTED1', + /** BLACKLISTED */ + EBLACKLISTED2 = 'EBLACKLISTED2', + /** Trial Expired */ + EEXPIRED = 'EEXPIRED', + /** GUID Error */ + EGUID = 'EGUID', + /** Multiple License Keys Present */ + EGUID1 = 'EGUID1', + /** Trial Requires Internet Connection */ + ENOCONN = 'ENOCONN', + /** No Flash */ + ENOFLASH = 'ENOFLASH', + ENOFLASH1 = 'ENOFLASH1', + ENOFLASH2 = 'ENOFLASH2', + ENOFLASH3 = 'ENOFLASH3', + ENOFLASH4 = 'ENOFLASH4', + ENOFLASH5 = 'ENOFLASH5', + ENOFLASH6 = 'ENOFLASH6', + ENOFLASH7 = 'ENOFLASH7', + /** No Keyfile */ + ENOKEYFILE = 'ENOKEYFILE', + /** No Keyfile */ + ENOKEYFILE1 = 'ENOKEYFILE1', + /** Missing key file */ + ENOKEYFILE2 = 'ENOKEYFILE2', + /** Invalid installation */ + ETRIAL = 'ETRIAL', + /** Plus */ + PLUS = 'PLUS', + /** Pro */ + PRO = 'PRO', + /** Trial */ + TRIAL = 'TRIAL' +} + +export type RemoteAccessEvent = { + __typename?: 'RemoteAccessEvent'; + data: RemoteAccessEventData; + type: EventType; +}; + +/** Defines whether remote access event is the initiation (from connect) or the response (from the server) */ +export enum RemoteAccessEventActionType { + ACK = 'ACK', + END = 'END', + INIT = 'INIT', + PING = 'PING' +} + +export type RemoteAccessEventData = { + __typename?: 'RemoteAccessEventData'; + apiKey: Scalars['String']; + type: RemoteAccessEventActionType; + url?: Maybe; +}; + +export type RemoteAccessInput = { + apiKey: Scalars['String']; + type: RemoteAccessEventActionType; + url?: InputMaybe; +}; + +export type RemoteGraphQLClientInput = { + apiKey: Scalars['String']; + body: Scalars['String']; +}; + +export type RemoteGraphQLEvent = { + __typename?: 'RemoteGraphQLEvent'; + data: RemoteGraphQLEventData; + type: EventType; +}; + +export type RemoteGraphQLEventData = { + __typename?: 'RemoteGraphQLEventData'; + /** Contains mutation / subscription / query data in the form of body: JSON, variables: JSON */ + body: Scalars['String']; + /** sha256 hash of the body */ + sha256: Scalars['String']; + type: RemoteGraphQLEventType; +}; + +export enum RemoteGraphQLEventType { + REMOTE_MUTATION_EVENT = 'REMOTE_MUTATION_EVENT', + REMOTE_QUERY_EVENT = 'REMOTE_QUERY_EVENT', + REMOTE_SUBSCRIPTION_EVENT = 'REMOTE_SUBSCRIPTION_EVENT', + REMOTE_SUBSCRIPTION_EVENT_PING = 'REMOTE_SUBSCRIPTION_EVENT_PING' +} + +export type RemoteGraphQLServerInput = { + /** Body - contains an object containing data: (GQL response data) or errors: (GQL Errors) */ + body: Scalars['String']; + /** sha256 hash of the body */ + sha256: Scalars['String']; + type: RemoteGraphQLEventType; +}; + +export type Server = { + __typename?: 'Server'; + apikey?: Maybe; + guid?: Maybe; + lanip?: Maybe; + localurl?: Maybe; + name?: Maybe; + owner?: Maybe; + remoteurl?: Maybe; + status?: Maybe; + wanip?: Maybe; +}; + +/** Defines server fields that have a TTL on them, for example last ping */ +export type ServerFieldsWithTtl = { + __typename?: 'ServerFieldsWithTtl'; + lastPing?: Maybe; +}; + +export type ServerModel = { + apikey: Scalars['String']; + guid: Scalars['String']; + lanip: Scalars['String']; + localurl: Scalars['String']; + name: Scalars['String']; + remoteurl: Scalars['String']; + wanip: Scalars['String']; +}; + +export enum ServerStatus { + NEVER_CONNECTED = 'never_connected', + OFFLINE = 'offline', + ONLINE = 'online' +} + +export type Service = { + __typename?: 'Service'; + name?: Maybe; + online?: Maybe; + uptime?: Maybe; + version?: Maybe; +}; + +export type Subscription = { + __typename?: 'Subscription'; + events?: Maybe>; + remoteSubscription: Scalars['String']; + servers: Array; +}; + + +export type SubscriptionremoteSubscriptionArgs = { + input: RemoteGraphQLClientInput; +}; + +export type TwoFactorLocal = { + __typename?: 'TwoFactorLocal'; + enabled?: Maybe; +}; + +export type TwoFactorRemote = { + __typename?: 'TwoFactorRemote'; + enabled?: Maybe; +}; + +export type TwoFactorWithToken = { + __typename?: 'TwoFactorWithToken'; + local?: Maybe; + remote?: Maybe; + token?: Maybe; +}; + +export type TwoFactorWithoutToken = { + __typename?: 'TwoFactorWithoutToken'; + local?: Maybe; + remote?: Maybe; +}; + +export enum URL_TYPE { + DEFAULT = 'DEFAULT', + LAN = 'LAN', + MDNS = 'MDNS', + WAN = 'WAN', + WIREGUARD = 'WIREGUARD' +} + +export type UpdateEvent = { + __typename?: 'UpdateEvent'; + data: UpdateEventData; + type: EventType; +}; + +export type UpdateEventData = { + __typename?: 'UpdateEventData'; + apiKey: Scalars['String']; + type: UpdateType; +}; + +export enum UpdateType { + DASHBOARD = 'DASHBOARD', + NETWORK = 'NETWORK' +} + +export type Uptime = { + __typename?: 'Uptime'; + timestamp?: Maybe; +}; + +export type UserProfileModelWithServers = { + __typename?: 'UserProfileModelWithServers'; + profile: ProfileModel; + servers: Array; +}; + +export type Vars = { + __typename?: 'Vars'; + expireTime?: Maybe; + flashGuid?: Maybe; + regState?: Maybe; + regTm2?: Maybe; + regTy?: Maybe; +}; + +export type updateDashboardMutationVariables = Exact<{ + data: DashboardInput; + apiKey: Scalars['String']; +}>; + + +export type updateDashboardMutation = { __typename?: 'Mutation', updateDashboard: { __typename?: 'Dashboard', apps?: { __typename?: 'DashboardApps', installed?: number | null } | null } }; + +export type sendNotificationMutationVariables = Exact<{ + notification: NotificationInput; + apiKey: Scalars['String']; +}>; + + +export type sendNotificationMutation = { __typename?: 'Mutation', sendNotification?: { __typename?: 'Notification', title?: string | null, subject?: string | null, description?: string | null, importance?: Importance | null, link?: string | null, status: NotificationStatus } | null }; + +export type updateNetworkMutationVariables = Exact<{ + data: NetworkInput; + apiKey: Scalars['String']; +}>; + + +export type updateNetworkMutation = { __typename?: 'Mutation', updateNetwork: { __typename?: 'Network', accessUrls?: Array<{ __typename?: 'AccessUrl', name?: string | null, type: URL_TYPE, ipv4?: URL | null, ipv6?: URL | null }> | null } }; + +export type sendRemoteAccessMutationMutationVariables = Exact<{ + remoteAccess: RemoteAccessInput; +}>; + + +export type sendRemoteAccessMutationMutation = { __typename?: 'Mutation', remoteSession?: boolean | null }; + +export type sendRemoteGraphQLResponseMutationVariables = Exact<{ + input: RemoteGraphQLServerInput; +}>; + + +export type sendRemoteGraphQLResponseMutation = { __typename?: 'Mutation', remoteGraphQLResponse: boolean }; + +export type RemoteGraphQLEventFragmentFragment = { __typename?: 'RemoteGraphQLEvent', remoteGraphQLEventData: { __typename?: 'RemoteGraphQLEventData', type: RemoteGraphQLEventType, body: string, sha256: string } } & { ' $fragmentName'?: 'RemoteGraphQLEventFragmentFragment' }; + +export type RemoteAccessEventFragmentFragment = { __typename?: 'RemoteAccessEvent', type: EventType, data: { __typename?: 'RemoteAccessEventData', type: RemoteAccessEventActionType, apiKey: string, url?: { __typename?: 'AccessUrl', type: URL_TYPE, name?: string | null, ipv4?: URL | null, ipv6?: URL | null } | null } } & { ' $fragmentName'?: 'RemoteAccessEventFragmentFragment' }; + +export type eventsSubscriptionVariables = Exact<{ [key: string]: never; }>; + + +export type eventsSubscription = { __typename?: 'Subscription', events?: Array<{ __typename: 'ClientConnectedEvent', connectedEvent: EventType, connectedData: { __typename?: 'ClientConnectionEventData', type: ClientType, version: string, apiKey: string } } | { __typename: 'ClientDisconnectedEvent', disconnectedEvent: EventType, disconnectedData: { __typename?: 'ClientConnectionEventData', type: ClientType, version: string, apiKey: string } } | { __typename: 'ClientPingEvent' } | ( + { __typename: 'RemoteAccessEvent' } + & { ' $fragmentRefs'?: { 'RemoteAccessEventFragmentFragment': RemoteAccessEventFragmentFragment } } + ) | ( + { __typename: 'RemoteGraphQLEvent' } + & { ' $fragmentRefs'?: { 'RemoteGraphQLEventFragmentFragment': RemoteGraphQLEventFragmentFragment } } + ) | { __typename: 'UpdateEvent' }> | null }; + +export const RemoteGraphQLEventFragmentFragmentDoc = {"kind":"Document","definitions":[{"kind":"FragmentDefinition","name":{"kind":"Name","value":"RemoteGraphQLEventFragment"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"RemoteGraphQLEvent"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","alias":{"kind":"Name","value":"remoteGraphQLEventData"},"name":{"kind":"Name","value":"data"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"type"}},{"kind":"Field","name":{"kind":"Name","value":"body"}},{"kind":"Field","name":{"kind":"Name","value":"sha256"}}]}}]}}]} as unknown as DocumentNode; +export const RemoteAccessEventFragmentFragmentDoc = {"kind":"Document","definitions":[{"kind":"FragmentDefinition","name":{"kind":"Name","value":"RemoteAccessEventFragment"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"RemoteAccessEvent"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"type"}},{"kind":"Field","name":{"kind":"Name","value":"data"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"type"}},{"kind":"Field","name":{"kind":"Name","value":"url"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"type"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"ipv4"}},{"kind":"Field","name":{"kind":"Name","value":"ipv6"}}]}},{"kind":"Field","name":{"kind":"Name","value":"apiKey"}}]}}]}}]} as unknown as DocumentNode; +export const updateDashboardDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"updateDashboard"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"data"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"DashboardInput"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"apiKey"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"updateDashboard"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"data"},"value":{"kind":"Variable","name":{"kind":"Name","value":"data"}}}],"directives":[{"kind":"Directive","name":{"kind":"Name","value":"auth"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"apiKey"},"value":{"kind":"Variable","name":{"kind":"Name","value":"apiKey"}}}]}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"apps"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"installed"}}]}}]}}]}}]} as unknown as DocumentNode; +export const sendNotificationDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"sendNotification"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"notification"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"NotificationInput"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"apiKey"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"sendNotification"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"notification"},"value":{"kind":"Variable","name":{"kind":"Name","value":"notification"}}}],"directives":[{"kind":"Directive","name":{"kind":"Name","value":"auth"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"apiKey"},"value":{"kind":"Variable","name":{"kind":"Name","value":"apiKey"}}}]}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"subject"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"importance"}},{"kind":"Field","name":{"kind":"Name","value":"link"}},{"kind":"Field","name":{"kind":"Name","value":"status"}}]}}]}}]} as unknown as DocumentNode; +export const updateNetworkDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"updateNetwork"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"data"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"NetworkInput"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"apiKey"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"updateNetwork"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"data"},"value":{"kind":"Variable","name":{"kind":"Name","value":"data"}}}],"directives":[{"kind":"Directive","name":{"kind":"Name","value":"auth"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"apiKey"},"value":{"kind":"Variable","name":{"kind":"Name","value":"apiKey"}}}]}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"accessUrls"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"type"}},{"kind":"Field","name":{"kind":"Name","value":"ipv4"}},{"kind":"Field","name":{"kind":"Name","value":"ipv6"}}]}}]}}]}}]} as unknown as DocumentNode; +export const sendRemoteAccessMutationDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"sendRemoteAccessMutation"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"remoteAccess"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"RemoteAccessInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"remoteSession"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"remoteAccess"},"value":{"kind":"Variable","name":{"kind":"Name","value":"remoteAccess"}}}]}]}}]} as unknown as DocumentNode; +export const sendRemoteGraphQLResponseDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"sendRemoteGraphQLResponse"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"RemoteGraphQLServerInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"remoteGraphQLResponse"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}]}]}}]} as unknown as DocumentNode; +export const eventsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"subscription","name":{"kind":"Name","value":"events"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"events"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"__typename"}},{"kind":"InlineFragment","typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"ClientConnectedEvent"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","alias":{"kind":"Name","value":"connectedData"},"name":{"kind":"Name","value":"data"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"type"}},{"kind":"Field","name":{"kind":"Name","value":"version"}},{"kind":"Field","name":{"kind":"Name","value":"apiKey"}}]}},{"kind":"Field","alias":{"kind":"Name","value":"connectedEvent"},"name":{"kind":"Name","value":"type"}}]}},{"kind":"InlineFragment","typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"ClientDisconnectedEvent"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","alias":{"kind":"Name","value":"disconnectedData"},"name":{"kind":"Name","value":"data"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"type"}},{"kind":"Field","name":{"kind":"Name","value":"version"}},{"kind":"Field","name":{"kind":"Name","value":"apiKey"}}]}},{"kind":"Field","alias":{"kind":"Name","value":"disconnectedEvent"},"name":{"kind":"Name","value":"type"}}]}},{"kind":"FragmentSpread","name":{"kind":"Name","value":"RemoteAccessEventFragment"}},{"kind":"FragmentSpread","name":{"kind":"Name","value":"RemoteGraphQLEventFragment"}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"RemoteAccessEventFragment"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"RemoteAccessEvent"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"type"}},{"kind":"Field","name":{"kind":"Name","value":"data"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"type"}},{"kind":"Field","name":{"kind":"Name","value":"url"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"type"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"ipv4"}},{"kind":"Field","name":{"kind":"Name","value":"ipv6"}}]}},{"kind":"Field","name":{"kind":"Name","value":"apiKey"}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"RemoteGraphQLEventFragment"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"RemoteGraphQLEvent"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","alias":{"kind":"Name","value":"remoteGraphQLEventData"},"name":{"kind":"Name","value":"data"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"type"}},{"kind":"Field","name":{"kind":"Name","value":"body"}},{"kind":"Field","name":{"kind":"Name","value":"sha256"}}]}}]}}]} as unknown as DocumentNode; \ No newline at end of file diff --git a/api/src/graphql/resolvers/query/info.ts b/api/src/graphql/resolvers/query/info.ts new file mode 100644 index 000000000..48de8afda --- /dev/null +++ b/api/src/graphql/resolvers/query/info.ts @@ -0,0 +1,464 @@ +import { + baseboard, + cpu, + cpuFlags, + mem, + memLayout, + osInfo, + system, + versions, +} from 'systeminformation'; +import { docker } from '@app/core/utils/clients/docker'; +import { + type InfoApps, + type Os as InfoOs, + type InfoCpu, + type Display, + type Theme, + type Temperature, + type Baseboard, + type Versions, + type InfoMemory, + type MemoryLayout, + type System, + type Devices, + type InfoResolvers, + type Gpu, +} from '@app/graphql/generated/api/types'; +import { getters } from '@app/store'; +import { loadState } from '@app/core/utils/misc/load-state'; +import { type DynamixConfig } from '@app/core/types/ini'; +import { toBoolean } from '@app/core/utils/casting'; +import toBytes from 'bytes'; +import { getUnraidVersion } from '@app/common/dashboard/get-unraid-version'; +import { AppError } from '@app/core/errors/app-error'; +import { cleanStdout } from '@app/core/utils/misc/clean-stdout'; +import { getMachineId } from '@app/core/utils/misc/get-machine-id'; +import { execaCommandSync, execa } from 'execa'; +import { pathExists } from 'path-exists'; +import { filter as asyncFilter } from 'p-iteration'; +import { isSymlink } from 'path-type'; +import type { PciDevice } from '@app/core/types'; +import { vmRegExps } from '@app/core/utils/vms/domain/vm-regexps'; +import { getPciDevices } from '@app/core/utils/vms/get-pci-devices'; +import { filterDevices } from '@app/core/utils/vms/filter-devices'; +import { sanitizeVendor } from '@app/core/utils/vms/domain/sanitize-vendor'; +import { sanitizeProduct } from '@app/core/utils/vms/domain/sanitize-product'; +import { bootTimestamp } from '@app/common/dashboard/boot-timestamp'; + +export const generateApps = async (): Promise => { + const installed = await docker + .listContainers({ all: true }) + .catch(() => []) + .then((containers) => containers.length); + const started = await docker + .listContainers() + .catch(() => []) + .then((containers) => containers.length); + return { installed, started }; +}; + +const generateOs = async (): Promise => { + const os = await osInfo(); + + return { + ...os, + uptime: bootTimestamp.toISOString(), + }; +}; + +const generateCpu = async (): Promise => { + const { cores, physicalCores, speedMin, speedMax, stepping, ...rest } = + await cpu(); + const flags = await cpuFlags().then((flags) => flags.split(' ')); + + return { + ...rest, + cores: physicalCores, + threads: cores, + flags, + stepping: Number(stepping), + // @TODO Find out what these should be if they're not defined + speedmin: speedMin || -1, + speedmax: speedMax || -1, + }; +}; + +const generateDisplay = async (): Promise => { + const filePath = getters.paths()['dynamix-config']; + const state = loadState(filePath); + if (!state) { + return {}; + } + const { theme, unit, ...display } = state.display; + return { + ...display, + theme: theme as Theme, + unit: unit as Temperature, + scale: toBoolean(display.scale), + tabs: toBoolean(display.tabs), + resize: toBoolean(display.resize), + wwn: toBoolean(display.wwn), + total: toBoolean(display.total), + usage: toBoolean(display.usage), + text: toBoolean(display.text), + warning: Number.parseInt(display.warning, 10), + critical: Number.parseInt(display.critical, 10), + hot: Number.parseInt(display.hot, 10), + max: Number.parseInt(display.max, 10), + locale: display.locale || 'en_US', + }; +}; + +const generateBaseboard = async (): Promise => baseboard(); + +const generateVersions = async (): Promise => { + const unraid = await getUnraidVersion(); + const softwareVersions = await versions(); + + return { + unraid, + ...softwareVersions, + }; +}; + +const generateMemory = async (): Promise => { + const layout = await memLayout().then((dims) => + dims.map((dim) => dim as MemoryLayout) + ); + const info = await mem(); + let max = info.total; + + // Max memory + try { + const memoryInfo = await execa('dmidecode', ['-t', 'memory']) + .then(cleanStdout) + .catch((error: NodeJS.ErrnoException) => { + if (error.code === 'ENOENT') { + throw new AppError('The dmidecode cli utility is missing.'); + } + + throw error; + }); + const lines = memoryInfo.split('\n'); + const header = lines.find((line) => + line.startsWith('Physical Memory Array') + ); + if (header) { + const start = lines.indexOf(header); + const nextHeaders = lines + .slice(start, -1) + .find((line) => line.startsWith('Handle ')); + + if (nextHeaders) { + const end = lines.indexOf(nextHeaders); + const fields = lines.slice(start, end); + + max = toBytes( + fields + ?.find((line) => + line.trim().startsWith('Maximum Capacity') + ) + ?.trim() + ?.split(': ')[1] ?? '0' + ); + } + } + } catch { + // Ignore errors here + } + + return { + layout, + max, + ...info, + }; +}; + +const generateDevices = async (): Promise => { + /** + * Set device class to device. + * @param device The device to modify. + * @returns The same device passed in but with the class modified. + */ + const addDeviceClass = (device: Readonly): PciDevice => { + const modifiedDevice: PciDevice = { + ...device, + class: 'other', + }; + + // GPU + if (vmRegExps.allowedGpuClassId.test(device.typeid)) { + modifiedDevice.class = 'vga'; + // Specialized product name cleanup for GPU + // GF116 [GeForce GTX 550 Ti] --> GeForce GTX 550 Ti + const regex = new RegExp(/.+\[(?.+)]/); + const productName = regex.exec(device.productname)?.groups?.gpuName; + + if (productName) { + modifiedDevice.productname = productName; + } + + return modifiedDevice; + // Audio + } + + if (vmRegExps.allowedAudioClassId.test(device.typeid)) { + modifiedDevice.class = 'audio'; + + return modifiedDevice; + } + + return modifiedDevice; + }; + + /** + * System PCI devices. + */ + const systemPciDevices = async (): Promise => { + const devices = await getPciDevices(); + const basePath = '/sys/bus/pci/devices/0000:'; + + // Remove devices with no IOMMU support + const filteredDevices = await asyncFilter( + devices, + async (device: Readonly) => + pathExists(`${basePath}${device.id}/iommu_group/`) + ); + + /** + * Run device cleanup + * + * Tasks: + * - Mark disallowed devices + * - Add class + * - Add whether kernel-bound driver exists + * - Cleanup device vendor/product names + */ + const processedDevices = await filterDevices(filteredDevices).then( + async (devices) => + Promise.all( + devices + // @ts-expect-error - Device is not PciDevice + .map((device) => addDeviceClass(device)) + .map(async (device) => { + // Attempt to get the current kernel-bound driver for this pci device + await isSymlink( + `${basePath}${device.id}/driver` + ).then((symlink) => { + if (symlink) { + // $strLink = @readlink('/sys/bus/pci/devices/0000:'.$arrMatch['id']. '/driver'); + // if (!empty($strLink)) { + // $strDriver = basename($strLink); + // } + } + }); + + // Clean up the vendor and product name + device.vendorname = sanitizeVendor( + device.vendorname + ); + device.productname = sanitizeProduct( + device.productname + ); + + return device; + }) + ) + ); + + return processedDevices; + }; + + /** + * System GPU Devices + * + * @name systemGPUDevices + * @ignore + * @private + */ + const systemGPUDevices: Promise = systemPciDevices().then( + (devices) => { + return devices.filter( + (device) => device.class === 'vga' && !device.allowed + ).map(entry => { + const gpu: Gpu = { + blacklisted: entry.allowed, + class: entry.class, + id: entry.id, + productid: entry.product, + typeid: entry.typeid, + type: entry.manufacturer, + vendorname: entry.vendorname + } + return gpu; + }); + } + ).catch(() => []); + + /** + * System usb devices. + * @returns Array of USB devices. + */ + const getSystemUSBDevices = async () => { + try { + // Get a list of all usb hubs so we can filter the allowed/disallowed + const usbHubs = await execa( + 'cat /sys/bus/usb/drivers/hub/*/modalias', + { shell: true } + ) + .then(({ stdout }) => + stdout.split('\n').map((line) => { + // eslint-disable-next-line @typescript-eslint/prefer-regexp-exec + const [, id] = line.match(/usb:v(\w{9})/) ?? []; + return id.replace('p', ':'); + }) + ) + .catch(() => [] as string[]); + + const emhttp = getters.emhttp(); + + // Remove boot drive + const filterBootDrive = (device: Readonly): boolean => + emhttp.var.flashGuid !== device.guid; + + // Remove usb hubs + const filterUsbHubs = (device: Readonly): boolean => + !usbHubs.includes(device.id); + + // Clean up the name + const sanitizeVendorName = (device: Readonly) => { + const vendorname = sanitizeVendor(device.vendorname || ''); + return { + ...device, + vendorname, + }; + }; + + const parseDeviceLine = ( + line: Readonly + ): { value: string; string: string } => { + const emptyLine = { value: '', string: '' }; + + // If the line is blank return nothing + if (!line) { + return emptyLine; + } + + // Parse the line + const [, _] = line.split(/[ \t]{2,}/).filter(Boolean); + // eslint-disable-next-line @typescript-eslint/prefer-regexp-exec + const match = _.match(/^(\S+)\s(.*)/)?.slice(1); + + // If there's no match return nothing + if (!match) { + return emptyLine; + } + + return { + value: match[0], + string: match[1], + }; + }; + + // Add extra fields to device + const parseDevice = (device: Readonly) => { + const modifiedDevice: PciDevice = { + ...device, + }; + const info = execaCommandSync( + `lsusb -d ${device.id} -v` + ).stdout.split('\n'); + const deviceName = device.name.trim(); + const iSerial = parseDeviceLine( + info.filter((line) => line.includes('iSerial'))[0] + ); + const iProduct = parseDeviceLine( + info.filter((line) => line.includes('iProduct'))[0] + ); + const iManufacturer = parseDeviceLine( + info.filter((line) => line.includes('iManufacturer'))[0] + ); + const idProduct = parseDeviceLine( + info.filter((line) => line.includes('idProduct'))[0] + ); + const idVendor = parseDeviceLine( + info.filter((line) => line.includes('idVendor'))[0] + ); + const serial = `${iSerial.string + .slice(8) + .slice(0, 4)}-${iSerial.string.slice(8).slice(4)}`; + const guid = `${idVendor.value.slice( + 2 + )}-${idProduct.value.slice(2)}-${serial}`; + + modifiedDevice.serial = iSerial.string; + modifiedDevice.product = iProduct.string; + modifiedDevice.manufacturer = iManufacturer.string; + modifiedDevice.guid = guid; + + // Set name if missing + if (deviceName === '') { + modifiedDevice.name = + `${iProduct.string} ${iManufacturer.string}`.trim(); + } + + // Name still blank? Replace using fallback default + if (deviceName === '') { + modifiedDevice.name = '[unnamed device]'; + } + + // Ensure name is trimmed + modifiedDevice.name = device.name.trim(); + + return modifiedDevice; + }; + + const parseUsbDevices = (stdout: string) => + stdout.split('\n').map((line) => { + const regex = new RegExp(/^.+: ID (?\S+)(?.*)$/); + const result = regex.exec(line); + return result?.groups as unknown as PciDevice; + }) ?? []; + + // Get all usb devices + const usbDevices = await execa('lsusb').then(async ({ stdout }) => + parseUsbDevices(stdout) + .map(parseDevice) + .filter(filterBootDrive) + .filter(filterUsbHubs) + .map(sanitizeVendorName) + ); + + return usbDevices; + } catch (error: unknown) { + return []; + } + }; + + return { + // Scsi: await scsiDevices, + gpu: await systemGPUDevices, + // Move this to interfaces + // network: await si.networkInterfaces(), + pci: await systemPciDevices(), + usb: await getSystemUSBDevices(), + }; +}; + +const generateMachineId = async (): Promise => getMachineId(); + +const generateSystem = async (): Promise => system(); + +export const infoSubResolvers: InfoResolvers = { + apps: async () => generateApps(), + baseboard: async () => generateBaseboard(), + cpu: async () => generateCpu(), + devices: async () => generateDevices(), + display: async () => generateDisplay(), + machineId: async () => generateMachineId(), + memory: async () => generateMemory(), + os: async () => generateOs(), + system: async () => generateSystem(), + versions: async () => generateVersions(), +}; diff --git a/api/src/store/modules/docker.ts b/api/src/store/modules/docker.ts index 32abc987c..0bdf3f3e4 100644 --- a/api/src/store/modules/docker.ts +++ b/api/src/store/modules/docker.ts @@ -1,5 +1,4 @@ import { createSlice, type PayloadAction } from '@reduxjs/toolkit'; -import merge from 'lodash/merge'; import { DaemonConnectionStatus } from '@app/store/types'; import { type DockerContainer } from '@app/graphql/generated/api/types'; diff --git a/api/src/store/watch/docker-watch.ts b/api/src/store/watch/docker-watch.ts index 0bd94b2a0..5721e4e1e 100644 --- a/api/src/store/watch/docker-watch.ts +++ b/api/src/store/watch/docker-watch.ts @@ -2,18 +2,23 @@ import { store } from '@app/store'; import { dockerLogger } from '@app/core/log'; import { updateDockerState } from '@app/store/modules/docker'; import { getDockerContainers } from '@app/core/modules/index'; -import { ContainerState } from '@app/graphql/generated/api/types'; import { docker } from '@app/core/utils/index'; import DockerEE from 'docker-event-emitter'; import { debounce } from 'lodash'; const updateContainerCache = async () => { - try { - await getDockerContainers({ useCache: false }); - } catch (err) { - dockerLogger.warn('Caught error getting containers %o', err) - store.dispatch(updateDockerState({ installed: null, running: null, containers: [] })) - } + try { + await getDockerContainers({ useCache: false }); + } catch (err) { + dockerLogger.warn('Caught error getting containers %o', err); + store.dispatch( + updateDockerState({ + installed: null, + running: null, + containers: [], + }) + ); + } }; const debouncedContainerCacheUpdate = debounce(updateContainerCache, 500); @@ -50,12 +55,12 @@ export const setupDockerWatch = async (): Promise => { dockerLogger.addContext('data', data); dockerLogger.debug(`[${data.from}] ${data.Type}->${data.Action}`); dockerLogger.removeContext('data'); - await debouncedContainerCacheUpdate() + await debouncedContainerCacheUpdate(); } ); - // Get docker container count on first start + // Get docker container count on first start await debouncedContainerCacheUpdate(); await dee.start(); dockerLogger.debug('Binding to docker events'); - return dee; + return dee; };