diff --git a/.gitignore b/.gitignore index 8449fad731..96417168a3 100644 --- a/.gitignore +++ b/.gitignore @@ -122,7 +122,10 @@ api/dev/Unraid.net/myservers.cfg # local Mise settings .mise.toml +mise.toml # Compiled test pages (generated from Nunjucks templates) web/public/test-pages/*.html +# local scripts for testing and development +.dev-scripts/ diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000000..17020360bf --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,6 @@ +{ + "files.associations": { + "*.page": "php" + }, + "intelephense.format.enable": false, +} diff --git a/@tailwind-shared/base-utilities.css b/@tailwind-shared/base-utilities.css index f6ed2a36da..b9312a54a1 100644 --- a/@tailwind-shared/base-utilities.css +++ b/@tailwind-shared/base-utilities.css @@ -86,4 +86,4 @@ unraid-sso-button.unapi { --text-7xl: 4.5rem; --text-8xl: 6rem; --text-9xl: 8rem; -} +} \ No newline at end of file diff --git a/@tailwind-shared/index.css b/@tailwind-shared/index.css index ccd3a47e1a..29bd37368a 100644 --- a/@tailwind-shared/index.css +++ b/@tailwind-shared/index.css @@ -2,4 +2,4 @@ @import './css-variables.css'; @import './unraid-theme.css'; @import './theme-variants.css'; -@import './base-utilities.css'; +@import './base-utilities.css'; \ No newline at end of file diff --git a/@tailwind-shared/theme-variants.css b/@tailwind-shared/theme-variants.css index a1780be4b8..9b2a6f15f1 100644 --- a/@tailwind-shared/theme-variants.css +++ b/@tailwind-shared/theme-variants.css @@ -65,4 +65,4 @@ /* Dark Mode Overrides */ .dark { --color-border: #383735; -} +} \ No newline at end of file diff --git a/api/dev/configs/api.json b/api/dev/configs/api.json index f4f76b8f55..0b9e096d2f 100644 --- a/api/dev/configs/api.json +++ b/api/dev/configs/api.json @@ -3,7 +3,5 @@ "extraOrigins": [], "sandbox": true, "ssoSubIds": [], - "plugins": [ - "unraid-api-plugin-connect" - ] + "plugins": ["unraid-api-plugin-connect"] } \ No newline at end of file diff --git a/api/generated-schema.graphql b/api/generated-schema.graphql index 126a982ad6..59b55f3bba 100644 --- a/api/generated-schema.graphql +++ b/api/generated-schema.graphql @@ -1388,18 +1388,6 @@ type FlatOrganizerEntry { meta: DockerContainer } -type NotificationCounts { - info: Int! - warning: Int! - alert: Int! - total: Int! -} - -type NotificationOverview { - unread: NotificationCounts! - archive: NotificationCounts! -} - type Notification implements Node { id: PrefixedID! @@ -1427,6 +1415,37 @@ enum NotificationType { ARCHIVE } +type NotificationEvent { + type: NotificationEventType! + notification: Notification +} + +enum NotificationEventType { + ADDED + UPDATED + DELETED + CLEARED +} + +type NotificationCounts { + info: Int! + warning: Int! + alert: Int! + total: Int! +} + +type NotificationSettings { + position: String! + expand: Boolean! + duration: Int! + max: Int! +} + +type NotificationOverview { + unread: NotificationCounts! + archive: NotificationCounts! +} + type Notifications implements Node { id: PrefixedID! @@ -1438,6 +1457,7 @@ type Notifications implements Node { Deduplicated list of unread warning and alert notifications, sorted latest first. """ warningsAndAlerts: [Notification!]! + settings: NotificationSettings! } input NotificationFilter { @@ -2893,6 +2913,7 @@ type Subscription { notificationAdded: Notification! notificationsOverview: NotificationOverview! notificationsWarningsAndAlerts: [Notification!]! + notificationEvent: NotificationEvent! ownerSubscription: Owner! serversSubscription: Server! parityHistorySubscription: ParityCheck! diff --git a/api/package.json b/api/package.json index a8c0006d7b..129919109f 100644 --- a/api/package.json +++ b/api/package.json @@ -39,6 +39,7 @@ "// Testing": "", "test": "NODE_ENV=test vitest run", "test:watch": "NODE_ENV=test vitest --ui", + "test:modifications:update": "rm -rf src/unraid-api/unraid-file-modifier/modifications/__test__/__fixtures__/downloaded src/unraid-api/unraid-file-modifier/modifications/patches /Users/ajitmehrotra/Projects/Unraid/api/api/src/unraid-api/unraid-file-modifier/modifications/__test__/snapshots && NODE_ENV=test vitest run src/unraid-api/unraid-file-modifier/modifications/__test__/generic-modification.spec.ts -u", "coverage": "NODE_ENV=test vitest run --coverage", "// Docker": "", "container:build": "./scripts/dc.sh build dev", @@ -46,6 +47,7 @@ "container:stop": "./scripts/dc.sh stop dev", "container:test": "./scripts/dc.sh run --rm builder pnpm run test", "container:enter": "./scripts/dc.sh exec dev /bin/bash", + "docker:build-and-run": "pnpm --filter @unraid/connect-plugin docker:build-and-run", "// Migration Scripts": "", "migration:codefirst": "tsx ./src/unraid-api/graph/migration-script.ts" }, diff --git a/api/src/__test__/utils.test.ts b/api/src/__test__/utils.test.ts index f3a28fb8d3..8c54fed31b 100644 --- a/api/src/__test__/utils.test.ts +++ b/api/src/__test__/utils.test.ts @@ -68,6 +68,29 @@ describe('formatDatetime', () => { } ); }); + + describe('Unraid PHP-style date formats (conversion)', () => { + const phpFormats = [ + ['d-m-Y', '14-02-2024'], + ['m-d-Y', '02-14-2024'], + ['Y-m-d', '2024-02-14'], + ]; + + const phpTimeFormats = [ + ['h:i A', '12:34 PM'], + ['H:i', '12:34'], + ]; + + it.each(phpFormats)('converts and formats date with %s', (format, expected) => { + const result = formatDatetime(testDate, { dateFormat: format }); + expect(result).toContain(expected); + }); + + it.each(phpTimeFormats)('converts and formats time with %s', (format, expected) => { + const result = formatDatetime(testDate, { timeFormat: format, dateFormat: 'Y-m-d' }); + expect(result).toContain(expected); + }); + }); }); describe('csvStringToArray', () => { diff --git a/api/src/core/types/ini.ts b/api/src/core/types/ini.ts index 7165b0322f..e34c2b9cb1 100644 --- a/api/src/core/types/ini.ts +++ b/api/src/core/types/ini.ts @@ -73,10 +73,10 @@ interface Notify { plugin: string; docker_notify: string; report: string; - /** @deprecated (will remove in future release). Date format: DD-MM-YYYY, MM-DD-YYY, or YYYY-MM-DD */ + /** Date format: DD-MM-YYYY, MM-DD-YYY, or YYYY-MM-DD */ date: 'd-m-Y' | 'm-d-Y' | 'Y-m-d'; /** - * @deprecated (will remove in future release). Time format: + * Time format: * - `hi: A` => 12 hr * - `H:i` => 24 hr (default) */ @@ -93,6 +93,9 @@ interface Notify { system: string; version: string; docker_update: string; + expand?: string | boolean; + duration?: string | number; + max?: string | number; } interface Ssmtp { diff --git a/api/src/unraid-api/cli/generated/graphql.ts b/api/src/unraid-api/cli/generated/graphql.ts index 27032c24b9..6851629cd5 100644 --- a/api/src/unraid-api/cli/generated/graphql.ts +++ b/api/src/unraid-api/cli/generated/graphql.ts @@ -560,6 +560,17 @@ export type CpuLoad = { percentUser: Scalars['Float']['output']; }; +export type CpuPackages = Node & { + __typename?: 'CpuPackages'; + id: Scalars['PrefixedID']['output']; + /** Power draw per package (W) */ + power: Array; + /** Temperature per package (°C) */ + temp: Array; + /** Total CPU package power draw (W) */ + totalPower: Scalars['Float']['output']; +}; + export type CpuUtilization = Node & { __typename?: 'CpuUtilization'; /** CPU load for each core */ @@ -591,6 +602,19 @@ export type Customization = { theme: Theme; }; +/** Customization related mutations */ +export type CustomizationMutations = { + __typename?: 'CustomizationMutations'; + /** Update the UI theme (writes dynamix.cfg) */ + setTheme: Theme; +}; + + +/** Customization related mutations */ +export type CustomizationMutationsSetThemeArgs = { + theme: ThemeName; +}; + export type DeleteApiKeyInput = { ids: Array; }; @@ -1065,6 +1089,7 @@ export type InfoCpu = Node & { manufacturer?: Maybe; /** CPU model */ model?: Maybe; + packages: CpuPackages; /** Number of physical processors */ processors?: Maybe; /** CPU revision */ @@ -1081,6 +1106,8 @@ export type InfoCpu = Node & { stepping?: Maybe; /** Number of CPU threads */ threads?: Maybe; + /** Per-package array of core/thread pairs, e.g. [[[0,1],[2,3]], [[4,5],[6,7]]] */ + topology: Array>>; /** CPU vendor */ vendor?: Maybe; /** CPU voltage */ @@ -1422,6 +1449,7 @@ export type Mutation = { createDockerFolderWithItems: ResolvedOrganizerV1; /** Creates a new notification record */ createNotification: Notification; + customization: CustomizationMutations; /** Deletes all archived notifications on server. */ deleteArchivedNotifications: NotificationOverview; deleteDockerEntries: ResolvedOrganizerV1; @@ -1640,6 +1668,19 @@ export type NotificationData = { title: Scalars['String']['input']; }; +export type NotificationEvent = { + __typename?: 'NotificationEvent'; + notification?: Maybe; + type: NotificationEventType; +}; + +export enum NotificationEventType { + ADDED = 'ADDED', + CLEARED = 'CLEARED', + DELETED = 'DELETED', + UPDATED = 'UPDATED' +} + export type NotificationFilter = { importance?: InputMaybe; limit: Scalars['Int']['input']; @@ -1659,6 +1700,14 @@ export type NotificationOverview = { unread: NotificationCounts; }; +export type NotificationSettings = { + __typename?: 'NotificationSettings'; + duration: Scalars['Int']['output']; + expand: Scalars['Boolean']['output']; + max: Scalars['Int']['output']; + position: Scalars['String']['output']; +}; + export enum NotificationType { ARCHIVE = 'ARCHIVE', UNREAD = 'UNREAD' @@ -1670,6 +1719,7 @@ export type Notifications = Node & { list: Array; /** A cached overview of the notifications in the system & their severity. */ overview: NotificationOverview; + settings: NotificationSettings; /** Deduplicated list of unread warning and alert notifications, sorted latest first. */ warningsAndAlerts: Array; }; @@ -2263,12 +2313,14 @@ export type Subscription = { dockerContainerStats: DockerContainerStats; logFile: LogFileContent; notificationAdded: Notification; + notificationEvent: NotificationEvent; notificationsOverview: NotificationOverview; notificationsWarningsAndAlerts: Array; ownerSubscription: Owner; parityHistorySubscription: ParityCheck; serversSubscription: Server; systemMetricsCpu: CpuUtilization; + systemMetricsCpuTelemetry: CpuPackages; systemMetricsMemory: MemoryUtilization; upsUpdates: UpsDevice; }; diff --git a/api/src/unraid-api/config/api-config.module.ts b/api/src/unraid-api/config/api-config.module.ts index a3daf5f88a..773cab5d86 100644 --- a/api/src/unraid-api/config/api-config.module.ts +++ b/api/src/unraid-api/config/api-config.module.ts @@ -29,21 +29,25 @@ export const loadApiConfig = async () => { const defaultConfig = createDefaultConfig(); const apiHandler = new ApiConfigPersistence(new ConfigService()).getFileHandler(); - const diskConfig: Partial = await apiHandler.loadConfig(); - // Hack: cleanup stale connect plugin entry if necessary - if (!isConnectPluginInstalled()) { - diskConfig.plugins = diskConfig.plugins?.filter( - (plugin) => plugin !== 'unraid-api-plugin-connect' - ); - await apiHandler.writeConfigFile(diskConfig as ApiConfig); - } + try { + const diskConfig: Partial = await apiHandler.loadConfig(); + // Hack: cleanup stale connect plugin entry if necessary + if (!isConnectPluginInstalled() && diskConfig.plugins) { + diskConfig.plugins = diskConfig.plugins?.filter( + (plugin) => plugin !== 'unraid-api-plugin-connect' + ); + await apiHandler.writeConfigFile(diskConfig as ApiConfig); + } - return { - ...defaultConfig, - ...diskConfig, - // diskConfig's version may be older, but we still want to use the correct version - version: API_VERSION, - }; + return { + ...defaultConfig, + ...diskConfig, + // diskConfig's version may be older, but we still want to use the correct version + version: API_VERSION, + }; + } catch (e) { + return defaultConfig; + } }; /** diff --git a/api/src/unraid-api/config/api-config.test.ts b/api/src/unraid-api/config/api-config.test.ts index ede0b15149..9b7ac34963 100644 --- a/api/src/unraid-api/config/api-config.test.ts +++ b/api/src/unraid-api/config/api-config.test.ts @@ -194,6 +194,12 @@ describe('ApiConfigPersistence', () => { describe('loadApiConfig', () => { beforeEach(async () => { vi.clearAllMocks(); + vi.spyOn(ApiConfigPersistence.prototype, 'getFileHandler').mockReturnValue({ + loadConfig: vi.fn().mockResolvedValue({}), + readConfigFile: vi.fn().mockResolvedValue({}), + writeConfigFile: vi.fn().mockResolvedValue(true), + updateConfig: vi.fn().mockResolvedValue(true), + } as any); }); it('should return default config with current API_VERSION', async () => { @@ -209,6 +215,13 @@ describe('loadApiConfig', () => { }); it('should handle errors gracefully and return defaults', async () => { + vi.spyOn(ApiConfigPersistence.prototype, 'getFileHandler').mockReturnValue({ + loadConfig: vi.fn().mockRejectedValue(new Error('Config load failed')), + readConfigFile: vi.fn().mockResolvedValue({}), + writeConfigFile: vi.fn(), + updateConfig: vi.fn(), + } as any); + const result = await loadApiConfig(); expect(result).toEqual({ diff --git a/api/src/unraid-api/graph/resolvers/docker/docker.constants.ts b/api/src/unraid-api/graph/resolvers/docker/docker.constants.ts new file mode 100644 index 0000000000..17ce9df925 --- /dev/null +++ b/api/src/unraid-api/graph/resolvers/docker/docker.constants.ts @@ -0,0 +1 @@ +export const DOCKER_SERVICE_TOKEN = Symbol('DOCKER_SERVICE'); diff --git a/api/src/unraid-api/graph/resolvers/notifications/notifications.model.ts b/api/src/unraid-api/graph/resolvers/notifications/notifications.model.ts index 64dd0893a1..6068395040 100644 --- a/api/src/unraid-api/graph/resolvers/notifications/notifications.model.ts +++ b/api/src/unraid-api/graph/resolvers/notifications/notifications.model.ts @@ -1,7 +1,7 @@ import { Field, InputType, Int, ObjectType, registerEnumType } from '@nestjs/graphql'; import { Node } from '@unraid/shared/graphql.model.js'; -import { IsEnum, IsInt, IsNotEmpty, IsOptional, IsString, Min } from 'class-validator'; +import { IsBoolean, IsEnum, IsInt, IsNotEmpty, IsOptional, IsString, Min } from 'class-validator'; export enum NotificationType { UNREAD = 'UNREAD', @@ -23,6 +23,69 @@ registerEnumType(NotificationImportance, { name: 'NotificationImportance', }); +export enum NotificationEventType { + ADDED = 'ADDED', + UPDATED = 'UPDATED', + DELETED = 'DELETED', + CLEARED = 'CLEARED', +} + +registerEnumType(NotificationEventType, { + name: 'NotificationEventType', +}); + +@ObjectType({ implements: () => Node }) +export class Notification extends Node { + @Field({ description: "Also known as 'event'" }) + @IsString() + @IsNotEmpty() + title!: string; + + @Field() + @IsString() + @IsNotEmpty() + subject!: string; + + @Field() + @IsString() + @IsNotEmpty() + description!: string; + + @Field(() => NotificationImportance) + @IsEnum(NotificationImportance) + @IsNotEmpty() + importance!: NotificationImportance; + + @Field({ nullable: true }) + @IsString() + @IsOptional() + link?: string; + + @Field(() => NotificationType) + @IsEnum(NotificationType) + @IsNotEmpty() + type!: NotificationType; + + @Field({ nullable: true, description: 'ISO Timestamp for when the notification occurred' }) + @IsString() + @IsOptional() + timestamp?: string; + + @Field({ nullable: true }) + @IsString() + @IsOptional() + formattedTimestamp?: string; +} + +@ObjectType('NotificationEvent') +export class NotificationEvent { + @Field(() => NotificationEventType) + type!: NotificationEventType; + + @Field(() => Notification, { nullable: true }) + notification?: Notification; +} + @InputType('NotificationFilter') export class NotificationFilter { @Field(() => NotificationImportance, { nullable: true }) @@ -99,58 +162,40 @@ export class NotificationCounts { total!: number; } -@ObjectType('NotificationOverview') -export class NotificationOverview { - @Field(() => NotificationCounts) - @IsNotEmpty() - unread!: NotificationCounts; - - @Field(() => NotificationCounts) - @IsNotEmpty() - archive!: NotificationCounts; -} - -@ObjectType({ implements: () => Node }) -export class Notification extends Node { - @Field({ description: "Also known as 'event'" }) - @IsString() - @IsNotEmpty() - title!: string; - +@ObjectType('NotificationSettings') +export class NotificationSettings { @Field() @IsString() @IsNotEmpty() - subject!: string; + position!: string; - @Field() - @IsString() + @Field(() => Boolean) + @IsBoolean() @IsNotEmpty() - description!: string; + expand!: boolean; - @Field(() => NotificationImportance) - @IsEnum(NotificationImportance) + @Field(() => Int) + @IsInt() + @Min(1) @IsNotEmpty() - importance!: NotificationImportance; - - @Field({ nullable: true }) - @IsString() - @IsOptional() - link?: string; + duration!: number; - @Field(() => NotificationType) - @IsEnum(NotificationType) + @Field(() => Int) + @IsInt() + @Min(1) @IsNotEmpty() - type!: NotificationType; + max!: number; +} - @Field({ nullable: true, description: 'ISO Timestamp for when the notification occurred' }) - @IsString() - @IsOptional() - timestamp?: string; +@ObjectType('NotificationOverview') +export class NotificationOverview { + @Field(() => NotificationCounts) + @IsNotEmpty() + unread!: NotificationCounts; - @Field({ nullable: true }) - @IsString() - @IsOptional() - formattedTimestamp?: string; + @Field(() => NotificationCounts) + @IsNotEmpty() + archive!: NotificationCounts; } @ObjectType({ implements: () => Node }) @@ -170,4 +215,8 @@ export class Notifications extends Node { }) @IsNotEmpty() warningsAndAlerts!: Notification[]; + + @Field(() => NotificationSettings) + @IsNotEmpty() + settings!: NotificationSettings; } diff --git a/api/src/unraid-api/graph/resolvers/notifications/notifications.resolver.ts b/api/src/unraid-api/graph/resolvers/notifications/notifications.resolver.ts index d3e0c6797b..0ee5311aa4 100644 --- a/api/src/unraid-api/graph/resolvers/notifications/notifications.resolver.ts +++ b/api/src/unraid-api/graph/resolvers/notifications/notifications.resolver.ts @@ -9,10 +9,12 @@ import { createSubscription, PUBSUB_CHANNEL } from '@app/core/pubsub.js'; import { Notification, NotificationData, + NotificationEvent, NotificationFilter, NotificationImportance, NotificationOverview, Notifications, + NotificationSettings, NotificationType, } from '@app/unraid-api/graph/resolvers/notifications/notifications.model.js'; import { NotificationsService } from '@app/unraid-api/graph/resolvers/notifications/notifications.service.js'; @@ -41,6 +43,11 @@ export class NotificationsResolver { return this.notificationsService.getOverview(); } + @ResolveField(() => NotificationSettings) + public settings(): NotificationSettings { + return this.notificationsService.getSettings(); + } + @ResolveField(() => [Notification]) public async list( @Args('filter', { type: () => NotificationFilter }) @@ -191,4 +198,13 @@ export class NotificationsResolver { async notificationsWarningsAndAlerts() { return createSubscription(PUBSUB_CHANNEL.NOTIFICATION_WARNINGS_AND_ALERTS); } + + @Subscription(() => NotificationEvent) + @UsePermissions({ + action: AuthAction.READ_ANY, + resource: Resource.NOTIFICATIONS, + }) + async notificationEvent() { + return createSubscription(PUBSUB_CHANNEL.NOTIFICATION_EVENT); + } } diff --git a/api/src/unraid-api/graph/resolvers/notifications/notifications.service.ts b/api/src/unraid-api/graph/resolvers/notifications/notifications.service.ts index 8daa13a98a..11e8bfa9b0 100644 --- a/api/src/unraid-api/graph/resolvers/notifications/notifications.service.ts +++ b/api/src/unraid-api/graph/resolvers/notifications/notifications.service.ts @@ -22,9 +22,11 @@ import { Notification, NotificationCounts, NotificationData, + NotificationEventType, NotificationFilter, NotificationImportance, NotificationOverview, + NotificationSettings, NotificationType, } from '@app/unraid-api/graph/resolvers/notifications/notifications.model.js'; import { validateObject } from '@app/unraid-api/graph/resolvers/validation.utils.js'; @@ -55,6 +57,13 @@ export class NotificationsService { }, }; + /** + * Tracks archive file paths that are currently being processed by batch operations. + * This allows us to suppress the file watcher's 'add' event for these files + * to prevent double counting stats and flooding pubsub events. + */ + private processingArchives = new Set(); + constructor() { this.path = getters.dynamix().notify!.path; void this.getNotificationsWatcher(this.path); @@ -98,22 +107,67 @@ export class NotificationsService { } await NotificationsService.watcher?.close().catch((e) => this.logger.error(e)); - NotificationsService.watcher = watch(basePath, { usePolling: CHOKIDAR_USEPOLLING }).on( - 'add', - (path) => { - void this.handleNotificationAdd(path).catch((e) => this.logger.error(e)); - } - ); + NotificationsService.watcher = watch(basePath, { + usePolling: CHOKIDAR_USEPOLLING, + ignoreInitial: true, // Only watch for new files + }).on('add', (path) => { + void this.handleNotificationAdd(path).catch((e) => this.logger.error(e)); + }); return NotificationsService.watcher; } private async handleNotificationAdd(path: string) { + if (this.processingArchives.has(path)) { + this.processingArchives.delete(path); + // this.logger.debug(`[handleNotificationAdd] Ignoring batch-processed file: ${path}`); + return; + } + // The path looks like /{notification base path}/{type}/{notification id} const type = path.includes('/unread/') ? NotificationType.UNREAD : NotificationType.ARCHIVE; - // this.logger.debug(`Adding ${type} Notification: ${path}`); + this.logger.log(`[handleNotificationAdd] Adding ${type} Notification: ${path}`); + + // If adding to archive, check if it exists in unread. + // If so, we do not want to count it towards the archive stats yet. + if (type === NotificationType.ARCHIVE) { + const filename = basename(path); + const unreadPath = join(this.paths().UNREAD, filename); + if (await fileExists(unreadPath)) { + this.logger.debug( + `[handleNotificationAdd] Ignoring archive notification shadowed by unread: ${filename}` + ); + return; + } + } + + let notification: Notification | undefined; + let lastError: unknown; + + for (let i = 0; i < 5; i++) { + try { + notification = await this.loadNotificationFile(path, NotificationType[type]); + this.logger.debug( + `[handleNotificationAdd] Successfully loaded ${path} on attempt ${i + 1}` + ); + break; + } catch (error) { + lastError = error; + this.logger.warn( + `[handleNotificationAdd] Attempt ${i + 1} failed for ${path}: ${error}` + ); + // wait 100ms before retrying + await new Promise((resolve) => setTimeout(resolve, 100)); + } + } - const notification = await this.loadNotificationFile(path, NotificationType[type]); + if (!notification) { + this.logger.error( + `[handleNotificationAdd] Failed to load notification after 5 retries: ${path}`, + lastError + ); + return; + } this.increment(notification.importance, NotificationsService.overview[type.toLowerCase()]); if (type === NotificationType.UNREAD) { @@ -121,8 +175,24 @@ export class NotificationsService { pubsub.publish(PUBSUB_CHANNEL.NOTIFICATION_ADDED, { notificationAdded: notification, }); + pubsub.publish(PUBSUB_CHANNEL.NOTIFICATION_EVENT, { + notificationEvent: { + type: NotificationEventType.ADDED, + notification, + }, + }); void this.publishWarningsAndAlerts(); } + // Also publish overview updates for archive adds, so counts stay in sync + if (type === NotificationType.ARCHIVE) { + pubsub.publish(PUBSUB_CHANNEL.NOTIFICATION_EVENT, { + notificationEvent: { + type: NotificationEventType.ADDED, + notification, + }, + }); + this.publishOverview(); + } } /** @@ -137,6 +207,26 @@ export class NotificationsService { return structuredClone(NotificationsService.overview); } + public getSettings(): NotificationSettings { + const { notify } = getters.dynamix(); + const parseBoolean = (value: unknown, defaultValue: boolean) => { + if (value === undefined || value === null || value === '') return defaultValue; + const s = String(value).toLowerCase(); + return s === 'true' || s === '1' || s === 'yes'; + }; + const parsePositiveInt = (value: unknown, defaultValue: number) => { + const n = Number(value); + return !isNaN(n) && n > 0 ? n : defaultValue; + }; + + return { + position: notify?.position ?? 'top-right', + expand: parseBoolean(notify?.expand, true), + duration: parsePositiveInt(notify?.duration, 5000), + max: parsePositiveInt(notify?.max, 3), + }; + } + private publishOverview(overview = NotificationsService.overview) { return pubsub.publish(PUBSUB_CHANNEL.NOTIFICATION_OVERVIEW, { notificationsOverview: overview, @@ -188,7 +278,16 @@ export class NotificationsService { // // recalculates stats for a particular notification type const recalculate = async (type: NotificationType) => { - const ids = await this.listFilesInFolder(this.paths()[type]); + let narrowContent = (contents: string[]) => contents; + + // If listing archives, filter out any that are also in unread + // to avoid double counting in stats + if (type === NotificationType.ARCHIVE) { + const unreadIds = new Set(await readdir(this.paths().UNREAD)); + narrowContent = (contents) => contents.filter((file) => !unreadIds.has(file)); + } + + const ids = await this.listFilesInFolder(this.paths()[type], narrowContent); const [notifications] = await this.loadNotificationsFromPaths(ids, {}); notifications.forEach((n) => this.increment(n.importance, overview[type.toLowerCase()])); }; @@ -216,11 +315,12 @@ export class NotificationsService { const fileData = this.makeNotificationFileData(data); try { - const [command, args] = this.getLegacyScriptArgs(fileData); + const [command, args] = this.getLegacyScriptArgs(fileData, id); + this.logger.log(`[createNotification] Executing: ${command} ${args.join(' ')}`); await execa(command, args); } catch (error) { - // manually write the file if the script fails - this.logger.debug(`[createNotification] legacy notifier failed: ${error}`); + // manually write the file if the script fails entirely + this.logger.warn(`[createNotification] legacy notifier failed: ${error}`); this.logger.verbose(`[createNotification] Writing: ${JSON.stringify(fileData, null, 4)}`); const path = join(this.paths().UNREAD, id); @@ -243,7 +343,7 @@ export class NotificationsService { * @param notification The notification to be converted to command line arguments. * @returns A 2-element tuple containing the legacy notifier command and arguments. */ - public getLegacyScriptArgs(notification: NotificationIni): [string, string[]] { + public getLegacyScriptArgs(notification: NotificationIni, id?: string): [string, string[]] { const { event, subject, description, link, importance } = notification; const args = [ ['-i', importance], @@ -254,6 +354,9 @@ export class NotificationsService { if (link) { args.push(['-l', link]); } + if (id) { + args.push(['-u', id]); + } return ['/usr/local/emhttp/webGui/scripts/notify', args.flat()]; } @@ -317,6 +420,14 @@ export class NotificationsService { this.decrement(notification.importance, NotificationsService.overview[type.toLowerCase()]); await this.publishOverview(); + + pubsub.publish(PUBSUB_CHANNEL.NOTIFICATION_EVENT, { + notificationEvent: { + type: NotificationEventType.DELETED, + notification, + }, + }); + if (type === NotificationType.UNREAD) { void this.publishWarningsAndAlerts(); } @@ -344,6 +455,14 @@ export class NotificationsService { if (type === NotificationType.UNREAD) { void this.publishWarningsAndAlerts(); } + + pubsub.publish(PUBSUB_CHANNEL.NOTIFICATION_EVENT, { + notificationEvent: { + type: NotificationEventType.CLEARED, + notification: null, + }, + }); + return this.getOverview(); } @@ -431,6 +550,7 @@ export class NotificationsService { public async archiveNotification({ id }: Pick): Promise { const unreadPath = join(this.paths().UNREAD, id); + const archivePath = join(this.paths().ARCHIVE, id); // We expect to only archive 'unread' notifications, but it's possible that the notification // has already been archived or deleted (e.g. retry logic, spike in network latency). @@ -450,19 +570,63 @@ export class NotificationsService { *------------------------**/ const snapshot = this.getOverview(); const notification = await this.loadNotificationFile(unreadPath, NotificationType.UNREAD); - const moveToArchive = this.moveNotification({ - from: NotificationType.UNREAD, - to: NotificationType.ARCHIVE, - snapshot, - }); - await moveToArchive(notification); + // Update stats + this.decrement(notification.importance, NotificationsService.overview.unread); + + if (snapshot) { + this.decrement(notification.importance, snapshot.unread); + } + + if (await fileExists(archivePath)) { + // File already in archive, just delete the unread one + await unlink(unreadPath); + + // Since we previously ignored this file in the archive definition (because it was in unread), + // we must now increment the archive stats because it has been "revealed" as an archived notification. + this.increment(notification.importance, NotificationsService.overview.archive); + if (snapshot) { + this.increment(notification.importance, snapshot.archive); + } + } else { + // File not in archive, move it there + try { + await rename(unreadPath, archivePath); + } catch (err) { + // revert our earlier decrement + // we do it this way (decrement -> try rename -> revert if error) to avoid + // a race condition between `rename` and `decrement` + this.increment(notification.importance, NotificationsService.overview.unread); + if (snapshot) { + this.increment(notification.importance, snapshot?.unread); + } + throw err; + } + + // We moved a file to archive that wasn't there. + // We DO need to increment the stats. + this.increment(notification.importance, NotificationsService.overview.archive); + if (snapshot) { + this.increment(notification.importance, snapshot.archive); + } + } + + void this.publishOverview(); void this.publishWarningsAndAlerts(); - return { + const updated = { ...notification, type: NotificationType.ARCHIVE, }; + + pubsub.publish(PUBSUB_CHANNEL.NOTIFICATION_EVENT, { + notificationEvent: { + type: NotificationEventType.UPDATED, + notification: updated, + }, + }); + + return updated; } public async markAsUnread({ id }: Pick): Promise { @@ -485,32 +649,84 @@ export class NotificationsService { await moveToUnread(notification); void this.publishWarningsAndAlerts(); - return { + + const updated = { ...notification, type: NotificationType.UNREAD, }; + + pubsub.publish(PUBSUB_CHANNEL.NOTIFICATION_EVENT, { + notificationEvent: { + type: NotificationEventType.UPDATED, + notification: updated, + }, + }); + + return updated; } public async archiveAll(importance?: NotificationImportance) { - const { UNREAD } = this.paths(); + const { UNREAD, ARCHIVE } = this.paths(); - if (!importance) { - await readdir(UNREAD).then((ids) => this.archiveIds(ids)); - return { overview: NotificationsService.overview }; - } - - const overviewSnapshot = this.getOverview(); + // Get notifications to process + // This implicitly handles the importance filter if provided const unreads = await this.listFilesInFolder(UNREAD); - const [notifications] = await this.loadNotificationsFromPaths(unreads, { importance }); - const archive = this.moveNotification({ - from: NotificationType.UNREAD, - to: NotificationType.ARCHIVE, - snapshot: overviewSnapshot, + const [notifications] = await this.loadNotificationsFromPaths(unreads, { + importance, + type: NotificationType.UNREAD, }); - const stats = await batchProcess(notifications, archive); + if (notifications.length === 0) { + return { overview: this.getOverview() }; + } + + const batchCreateArchive = async (notification: Notification) => { + const unreadPath = join(UNREAD, notification.id); + const archivePath = join(ARCHIVE, notification.id); + + try { + // If file already exists in archive, delete the unread copy + if (await fileExists(archivePath)) { + await unlink(unreadPath); + } else { + // Otherwise move it to archive + // We must register this path to be ignored by the file watcher's 'add' event + // to avoid double counting stats and flooding pubsub events. + this.processingArchives.add(archivePath); + await rename(unreadPath, archivePath); + } + + // Update in-memory stats + this.decrement(notification.importance, NotificationsService.overview.unread); + this.increment(notification.importance, NotificationsService.overview.archive); + } catch (error) { + this.logger.error( + `[archiveAll] Failed to archive ${notification.id}: ${error instanceof Error ? error.message : error}` + ); + } + }; + + const stats = await batchProcess(notifications, batchCreateArchive); + + if (stats.errorOccurred) { + this.logger.warn(`[archiveAll] Finished with errors: ${stats.errors.length} failed.`); + } + + // Publish updates once + void this.publishOverview(); void this.publishWarningsAndAlerts(); - return { ...stats, overview: overviewSnapshot }; + + // Send a single event to trigger frontend refresh + // We use CLEARED because from the perspective of the unread list, they are cleared. + // The frontend handles CLEARED by refetching both lists. + pubsub.publish(PUBSUB_CHANNEL.NOTIFICATION_EVENT, { + notificationEvent: { + type: NotificationEventType.CLEARED, + notification: null, + }, + }); + + return { overview: this.getOverview() }; } public async unarchiveAll(importance?: NotificationImportance) { @@ -682,6 +898,29 @@ export class NotificationsService { .map(({ path }) => path); } + private async *getNotificationsGenerator( + files: string[], + type: NotificationType + ): AsyncGenerator<{ success: true; value: Notification } | { success: false; reason: unknown }> { + const BATCH_SIZE = 10; + for (let i = 0; i < files.length; i += BATCH_SIZE) { + const batch = files.slice(i, i + BATCH_SIZE); + const promises = batch.map(async (file) => { + try { + const value = await this.loadNotificationFile(file, type); + return { success: true, value } as const; + } catch (reason) { + return { success: false, reason } as const; + } + }); + + const results = await Promise.all(promises); + for (const res of results) { + yield res; + } + } + } + /** * Given a an array of files, reads and filters all the files in the directory, * and attempts to parse each file as a Notification. @@ -699,27 +938,39 @@ export class NotificationsService { filters: Partial ): Promise<[Notification[], unknown[]]> { const { importance, type, offset = 0, limit = files.length } = filters; - - const fileReads = files - .slice(offset, limit + offset) - .map((file) => this.loadNotificationFile(file, type ?? NotificationType.UNREAD)); - const results = await Promise.allSettled(fileReads); + const notifications: Notification[] = []; + const errors: unknown[] = []; + let skipped = 0; // if the filter is defined & truthy, tests if the actual value matches the filter const passesFilter = (actual: T, filter?: unknown) => !filter || actual === filter; + const matches = (n: Notification) => + passesFilter(n.importance, importance) && + passesFilter(n.type, type ?? NotificationType.UNREAD); - return [ - results - .filter(isFulfilled) - .map((result) => result.value) - .filter( - (notification) => - passesFilter(notification.importance, importance) && - passesFilter(notification.type, type) - ) - .sort(this.sortLatestFirst), - results.filter(isRejected).map((result) => result.reason), - ]; + const generator = this.getNotificationsGenerator(files, type ?? NotificationType.UNREAD); + + for await (const result of generator) { + if (!result.success) { + errors.push(result.reason); + continue; + } + + const notification = result.value; + + if (matches(notification)) { + if (skipped < offset) { + skipped++; + } else { + notifications.push(notification); + if (notifications.length >= limit) { + break; + } + } + } + } + + return [notifications.sort(this.sortLatestFirst), errors]; } /** @@ -856,16 +1107,27 @@ export class NotificationsService { } private formatDatetime(date: Date) { - const { display: settings } = getters.dynamix(); - if (!settings) { + const { display, notify } = getters.dynamix(); + + if (!display && !notify) { this.logger.warn( - '[formatTimestamp] Dynamix display settings not found. Cannot apply user settings.' + '[formatTimestamp] Dynamix display/notify settings not found. Cannot apply user settings.' ); return date.toISOString(); } + + // Prioritize notification-specific settings, fall back to global display settings + const dateFormat = notify?.date || display?.date || null; + const timeFormat = notify?.time || display?.time || null; + + if (!dateFormat || !timeFormat) { + // If we still don't have a format (e.g. neither config implies one), fallback to ISO + return date.toISOString(); + } + return formatDatetime(date, { - dateFormat: settings.date, - timeFormat: settings.time, + dateFormat, + timeFormat, omitTimezone: true, }); } diff --git a/api/src/unraid-api/graph/resolvers/vms/vms.service.spec.ts b/api/src/unraid-api/graph/resolvers/vms/vms.service.spec.ts index aebc4b7036..f5964a4f93 100644 --- a/api/src/unraid-api/graph/resolvers/vms/vms.service.spec.ts +++ b/api/src/unraid-api/graph/resolvers/vms/vms.service.spec.ts @@ -158,7 +158,7 @@ const isQemuAvailable = () => { } }; -describe('VmsService', () => { +describe.skipIf(!isQemuAvailable())('VmsService', () => { let service: VmsService; let hypervisor: Hypervisor; let testVm: VmDomain | null = null; @@ -184,14 +184,6 @@ describe('VmsService', () => { `; - beforeAll(() => { - if (!isQemuAvailable()) { - throw new Error( - 'QEMU not available - skipping VM integration tests. Please install QEMU to run these tests.' - ); - } - }); - beforeAll(async () => { // Override the LIBVIRT_URI environment variable for testing process.env.LIBVIRT_URI = LIBVIRT_URI; diff --git a/api/src/unraid-api/unraid-file-modifier/modifications/__fixtures__/downloaded/font-awesome.css b/api/src/unraid-api/unraid-file-modifier/modifications/__fixtures__/downloaded/font-awesome.css new file mode 100644 index 0000000000..05b7d122c3 --- /dev/null +++ b/api/src/unraid-api/unraid-file-modifier/modifications/__fixtures__/downloaded/font-awesome.css @@ -0,0 +1,6 @@ +/* Font Awesome 4.7.0 by @davegandy - http://fontawesome.io - @fontawesome + * License - http://fontawesome.io/license (Font: SIL OFL 1.1, CSS: MIT License) + */ + +@font-face{font-family:'FontAwesome';font-weight:normal;font-style:normal;src:url('/webGui/styles/font-awesome.woff?v=220508') format('woff')} +.fa{display:inline-block;font:normal normal normal 14px/1 FontAwesome;font-size:inherit;text-rendering:auto;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.fa-lg{font-size:1.33333333em;line-height:.75em;vertical-align:-15%}.fa-2x{font-size:2em}.fa-3x{font-size:3em}.fa-4x{font-size:4em}.fa-5x{font-size:5em}.fa-fw{width:1.28571429em;text-align:center}.fa-ul{padding-left:0;margin-left:2.14285714em;list-style-type:none}.fa-ul>li{position:relative}.fa-li{position:absolute;left:-2.14285714em;width:2.14285714em;top:.14285714em;text-align:center}.fa-li.fa-lg{left:-1.85714286em}.fa-border{padding:.2em .25em .15em;border:solid .08em #eee;border-radius:.1em}.fa-pull-left{float:left}.fa-pull-right{float:right}.fa.fa-pull-left{margin-right:.3em}.fa.fa-pull-right{margin-left:.3em}.pull-right{float:right}.pull-left{float:left}.fa.pull-left{margin-right:.3em}.fa.pull-right{margin-left:.3em}.fa-spin{-webkit-animation:fa-spin 2s infinite linear;animation:fa-spin 2s infinite linear}.fa-pulse{-webkit-animation:fa-spin 1s infinite steps(8);animation:fa-spin 1s infinite steps(8)}@-webkit-keyframes fa-spin{0%{-webkit-transform:rotate(0deg);transform:rotate(0deg)}100%{-webkit-transform:rotate(359deg);transform:rotate(359deg)}}@keyframes fa-spin{0%{-webkit-transform:rotate(0deg);transform:rotate(0deg)}100%{-webkit-transform:rotate(359deg);transform:rotate(359deg)}}.fa-rotate-90{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=1)";-webkit-transform:rotate(90deg);-ms-transform:rotate(90deg);transform:rotate(90deg)}.fa-rotate-180{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=2)";-webkit-transform:rotate(180deg);-ms-transform:rotate(180deg);transform:rotate(180deg)}.fa-rotate-270{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=3)";-webkit-transform:rotate(270deg);-ms-transform:rotate(270deg);transform:rotate(270deg)}.fa-flip-horizontal{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=0, mirror=1)";-webkit-transform:scale(-1, 1);-ms-transform:scale(-1, 1);transform:scale(-1, 1)}.fa-flip-vertical{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=2, mirror=1)";-webkit-transform:scale(1, -1);-ms-transform:scale(1, -1);transform:scale(1, -1)}:root .fa-rotate-90,:root .fa-rotate-180,:root .fa-rotate-270,:root .fa-flip-horizontal,:root .fa-flip-vertical{filter:none}.fa-stack{position:relative;display:inline-block;width:2em;height:2em;line-height:2em;vertical-align:middle}.fa-stack-1x,.fa-stack-2x{position:absolute;left:0;width:100%;text-align:center}.fa-stack-1x{line-height:inherit}.fa-stack-2x{font-size:2em}.fa-inverse{color:#fff}.fa-glass:before{content:"\f000"}.fa-music:before{content:"\f001"}.fa-search:before{content:"\f002"}.fa-envelope-o:before{content:"\f003"}.fa-heart:before{content:"\f004"}.fa-star:before{content:"\f005"}.fa-star-o:before{content:"\f006"}.fa-user:before{content:"\f007"}.fa-film:before{content:"\f008"}.fa-th-large:before{content:"\f009"}.fa-th:before{content:"\f00a"}.fa-th-list:before{content:"\f00b"}.fa-check:before{content:"\f00c"}.fa-remove:before,.fa-close:before,.fa-times:before{content:"\f00d"}.fa-search-plus:before{content:"\f00e"}.fa-search-minus:before{content:"\f010"}.fa-power-off:before{content:"\f011"}.fa-signal:before{content:"\f012"}.fa-gear:before,.fa-cog:before{content:"\f013"}.fa-trash-o:before{content:"\f014"}.fa-home:before{content:"\f015"}.fa-file-o:before{content:"\f016"}.fa-clock-o:before{content:"\f017"}.fa-road:before{content:"\f018"}.fa-download:before{content:"\f019"}.fa-arrow-circle-o-down:before{content:"\f01a"}.fa-arrow-circle-o-up:before{content:"\f01b"}.fa-inbox:before{content:"\f01c"}.fa-play-circle-o:before{content:"\f01d"}.fa-rotate-right:before,.fa-repeat:before{content:"\f01e"}.fa-refresh:before{content:"\f021"}.fa-list-alt:before{content:"\f022"}.fa-lock:before{content:"\f023"}.fa-flag:before{content:"\f024"}.fa-headphones:before{content:"\f025"}.fa-volume-off:before{content:"\f026"}.fa-volume-down:before{content:"\f027"}.fa-volume-up:before{content:"\f028"}.fa-qrcode:before{content:"\f029"}.fa-barcode:before{content:"\f02a"}.fa-tag:before{content:"\f02b"}.fa-tags:before{content:"\f02c"}.fa-book:before{content:"\f02d"}.fa-bookmark:before{content:"\f02e"}.fa-print:before{content:"\f02f"}.fa-camera:before{content:"\f030"}.fa-font:before{content:"\f031"}.fa-bold:before{content:"\f032"}.fa-italic:before{content:"\f033"}.fa-text-height:before{content:"\f034"}.fa-text-width:before{content:"\f035"}.fa-align-left:before{content:"\f036"}.fa-align-center:before{content:"\f037"}.fa-align-right:before{content:"\f038"}.fa-align-justify:before{content:"\f039"}.fa-list:before{content:"\f03a"}.fa-dedent:before,.fa-outdent:before{content:"\f03b"}.fa-indent:before{content:"\f03c"}.fa-video-camera:before{content:"\f03d"}.fa-photo:before,.fa-image:before,.fa-picture-o:before{content:"\f03e"}.fa-pencil:before{content:"\f040"}.fa-map-marker:before{content:"\f041"}.fa-adjust:before{content:"\f042"}.fa-tint:before{content:"\f043"}.fa-edit:before,.fa-pencil-square-o:before{content:"\f044"}.fa-share-square-o:before{content:"\f045"}.fa-check-square-o:before{content:"\f046"}.fa-arrows:before{content:"\f047"}.fa-step-backward:before{content:"\f048"}.fa-fast-backward:before{content:"\f049"}.fa-backward:before{content:"\f04a"}.fa-play:before{content:"\f04b"}.fa-pause:before{content:"\f04c"}.fa-stop:before{content:"\f04d"}.fa-forward:before{content:"\f04e"}.fa-fast-forward:before{content:"\f050"}.fa-step-forward:before{content:"\f051"}.fa-eject:before{content:"\f052"}.fa-chevron-left:before{content:"\f053"}.fa-chevron-right:before{content:"\f054"}.fa-plus-circle:before{content:"\f055"}.fa-minus-circle:before{content:"\f056"}.fa-times-circle:before{content:"\f057"}.fa-check-circle:before{content:"\f058"}.fa-question-circle:before{content:"\f059"}.fa-info-circle:before{content:"\f05a"}.fa-crosshairs:before{content:"\f05b"}.fa-times-circle-o:before{content:"\f05c"}.fa-check-circle-o:before{content:"\f05d"}.fa-ban:before{content:"\f05e"}.fa-arrow-left:before{content:"\f060"}.fa-arrow-right:before{content:"\f061"}.fa-arrow-up:before{content:"\f062"}.fa-arrow-down:before{content:"\f063"}.fa-mail-forward:before,.fa-share:before{content:"\f064"}.fa-expand:before{content:"\f065"}.fa-compress:before{content:"\f066"}.fa-plus:before{content:"\f067"}.fa-minus:before{content:"\f068"}.fa-asterisk:before{content:"\f069"}.fa-exclamation-circle:before{content:"\f06a"}.fa-gift:before{content:"\f06b"}.fa-leaf:before{content:"\f06c"}.fa-fire:before{content:"\f06d"}.fa-eye:before{content:"\f06e"}.fa-eye-slash:before{content:"\f070"}.fa-warning:before,.fa-exclamation-triangle:before{content:"\f071"}.fa-plane:before{content:"\f072"}.fa-calendar:before{content:"\f073"}.fa-random:before{content:"\f074"}.fa-comment:before{content:"\f075"}.fa-magnet:before{content:"\f076"}.fa-chevron-up:before{content:"\f077"}.fa-chevron-down:before{content:"\f078"}.fa-retweet:before{content:"\f079"}.fa-shopping-cart:before{content:"\f07a"}.fa-folder:before{content:"\f07b"}.fa-folder-open:before{content:"\f07c"}.fa-arrows-v:before{content:"\f07d"}.fa-arrows-h:before{content:"\f07e"}.fa-bar-chart-o:before,.fa-bar-chart:before{content:"\f080"}.fa-twitter-square:before{content:"\f081"}.fa-facebook-square:before{content:"\f082"}.fa-camera-retro:before{content:"\f083"}.fa-key:before{content:"\f084"}.fa-gears:before,.fa-cogs:before{content:"\f085"}.fa-comments:before{content:"\f086"}.fa-thumbs-o-up:before{content:"\f087"}.fa-thumbs-o-down:before{content:"\f088"}.fa-star-half:before{content:"\f089"}.fa-heart-o:before{content:"\f08a"}.fa-sign-out:before{content:"\f08b"}.fa-linkedin-square:before{content:"\f08c"}.fa-thumb-tack:before{content:"\f08d"}.fa-external-link:before{content:"\f08e"}.fa-sign-in:before{content:"\f090"}.fa-trophy:before{content:"\f091"}.fa-github-square:before{content:"\f092"}.fa-upload:before{content:"\f093"}.fa-lemon-o:before{content:"\f094"}.fa-phone:before{content:"\f095"}.fa-square-o:before{content:"\f096"}.fa-bookmark-o:before{content:"\f097"}.fa-phone-square:before{content:"\f098"}.fa-twitter:before{content:"\f099"}.fa-facebook-f:before,.fa-facebook:before{content:"\f09a"}.fa-github:before{content:"\f09b"}.fa-unlock:before{content:"\f09c"}.fa-credit-card:before{content:"\f09d"}.fa-feed:before,.fa-rss:before{content:"\f09e"}.fa-hdd-o:before{content:"\f0a0"}.fa-bullhorn:before{content:"\f0a1"}.fa-bell:before{content:"\f0f3"}.fa-certificate:before{content:"\f0a3"}.fa-hand-o-right:before{content:"\f0a4"}.fa-hand-o-left:before{content:"\f0a5"}.fa-hand-o-up:before{content:"\f0a6"}.fa-hand-o-down:before{content:"\f0a7"}.fa-arrow-circle-left:before{content:"\f0a8"}.fa-arrow-circle-right:before{content:"\f0a9"}.fa-arrow-circle-up:before{content:"\f0aa"}.fa-arrow-circle-down:before{content:"\f0ab"}.fa-globe:before{content:"\f0ac"}.fa-wrench:before{content:"\f0ad"}.fa-tasks:before{content:"\f0ae"}.fa-filter:before{content:"\f0b0"}.fa-briefcase:before{content:"\f0b1"}.fa-arrows-alt:before{content:"\f0b2"}.fa-group:before,.fa-users:before{content:"\f0c0"}.fa-chain:before,.fa-link:before{content:"\f0c1"}.fa-cloud:before{content:"\f0c2"}.fa-flask:before{content:"\f0c3"}.fa-cut:before,.fa-scissors:before{content:"\f0c4"}.fa-copy:before,.fa-files-o:before{content:"\f0c5"}.fa-paperclip:before{content:"\f0c6"}.fa-save:before,.fa-floppy-o:before{content:"\f0c7"}.fa-square:before{content:"\f0c8"}.fa-navicon:before,.fa-reorder:before,.fa-bars:before{content:"\f0c9"}.fa-list-ul:before{content:"\f0ca"}.fa-list-ol:before{content:"\f0cb"}.fa-strikethrough:before{content:"\f0cc"}.fa-underline:before{content:"\f0cd"}.fa-table:before{content:"\f0ce"}.fa-magic:before{content:"\f0d0"}.fa-truck:before{content:"\f0d1"}.fa-pinterest:before{content:"\f0d2"}.fa-pinterest-square:before{content:"\f0d3"}.fa-google-plus-square:before{content:"\f0d4"}.fa-google-plus:before{content:"\f0d5"}.fa-money:before{content:"\f0d6"}.fa-caret-down:before{content:"\f0d7"}.fa-caret-up:before{content:"\f0d8"}.fa-caret-left:before{content:"\f0d9"}.fa-caret-right:before{content:"\f0da"}.fa-columns:before{content:"\f0db"}.fa-unsorted:before,.fa-sort:before{content:"\f0dc"}.fa-sort-down:before,.fa-sort-desc:before{content:"\f0dd"}.fa-sort-up:before,.fa-sort-asc:before{content:"\f0de"}.fa-envelope:before{content:"\f0e0"}.fa-linkedin:before{content:"\f0e1"}.fa-rotate-left:before,.fa-undo:before{content:"\f0e2"}.fa-legal:before,.fa-gavel:before{content:"\f0e3"}.fa-dashboard:before,.fa-tachometer:before{content:"\f0e4"}.fa-comment-o:before{content:"\f0e5"}.fa-comments-o:before{content:"\f0e6"}.fa-flash:before,.fa-bolt:before{content:"\f0e7"}.fa-sitemap:before{content:"\f0e8"}.fa-umbrella:before{content:"\f0e9"}.fa-paste:before,.fa-clipboard:before{content:"\f0ea"}.fa-lightbulb-o:before{content:"\f0eb"}.fa-exchange:before{content:"\f0ec"}.fa-cloud-download:before{content:"\f0ed"}.fa-cloud-upload:before{content:"\f0ee"}.fa-user-md:before{content:"\f0f0"}.fa-stethoscope:before{content:"\f0f1"}.fa-suitcase:before{content:"\f0f2"}.fa-bell-o:before{content:"\f0a2"}.fa-coffee:before{content:"\f0f4"}.fa-cutlery:before{content:"\f0f5"}.fa-file-text-o:before{content:"\f0f6"}.fa-building-o:before{content:"\f0f7"}.fa-hospital-o:before{content:"\f0f8"}.fa-ambulance:before{content:"\f0f9"}.fa-medkit:before{content:"\f0fa"}.fa-fighter-jet:before{content:"\f0fb"}.fa-beer:before{content:"\f0fc"}.fa-h-square:before{content:"\f0fd"}.fa-plus-square:before{content:"\f0fe"}.fa-angle-double-left:before{content:"\f100"}.fa-angle-double-right:before{content:"\f101"}.fa-angle-double-up:before{content:"\f102"}.fa-angle-double-down:before{content:"\f103"}.fa-angle-left:before{content:"\f104"}.fa-angle-right:before{content:"\f105"}.fa-angle-up:before{content:"\f106"}.fa-angle-down:before{content:"\f107"}.fa-desktop:before{content:"\f108"}.fa-laptop:before{content:"\f109"}.fa-tablet:before{content:"\f10a"}.fa-mobile-phone:before,.fa-mobile:before{content:"\f10b"}.fa-circle-o:before{content:"\f10c"}.fa-quote-left:before{content:"\f10d"}.fa-quote-right:before{content:"\f10e"}.fa-spinner:before{content:"\f110"}.fa-circle:before{content:"\f111"}.fa-mail-reply:before,.fa-reply:before{content:"\f112"}.fa-github-alt:before{content:"\f113"}.fa-folder-o:before{content:"\f114"}.fa-folder-open-o:before{content:"\f115"}.fa-smile-o:before{content:"\f118"}.fa-frown-o:before{content:"\f119"}.fa-meh-o:before{content:"\f11a"}.fa-gamepad:before{content:"\f11b"}.fa-keyboard-o:before{content:"\f11c"}.fa-flag-o:before{content:"\f11d"}.fa-flag-checkered:before{content:"\f11e"}.fa-terminal:before{content:"\f120"}.fa-code:before{content:"\f121"}.fa-mail-reply-all:before,.fa-reply-all:before{content:"\f122"}.fa-star-half-empty:before,.fa-star-half-full:before,.fa-star-half-o:before{content:"\f123"}.fa-location-arrow:before{content:"\f124"}.fa-crop:before{content:"\f125"}.fa-code-fork:before{content:"\f126"}.fa-unlink:before,.fa-chain-broken:before{content:"\f127"}.fa-question:before{content:"\f128"}.fa-info:before{content:"\f129"}.fa-exclamation:before{content:"\f12a"}.fa-superscript:before{content:"\f12b"}.fa-subscript:before{content:"\f12c"}.fa-eraser:before{content:"\f12d"}.fa-puzzle-piece:before{content:"\f12e"}.fa-microphone:before{content:"\f130"}.fa-microphone-slash:before{content:"\f131"}.fa-shield:before{content:"\f132"}.fa-calendar-o:before{content:"\f133"}.fa-fire-extinguisher:before{content:"\f134"}.fa-rocket:before{content:"\f135"}.fa-maxcdn:before{content:"\f136"}.fa-chevron-circle-left:before{content:"\f137"}.fa-chevron-circle-right:before{content:"\f138"}.fa-chevron-circle-up:before{content:"\f139"}.fa-chevron-circle-down:before{content:"\f13a"}.fa-html5:before{content:"\f13b"}.fa-css3:before{content:"\f13c"}.fa-anchor:before{content:"\f13d"}.fa-unlock-alt:before{content:"\f13e"}.fa-bullseye:before{content:"\f140"}.fa-ellipsis-h:before{content:"\f141"}.fa-ellipsis-v:before{content:"\f142"}.fa-rss-square:before{content:"\f143"}.fa-play-circle:before{content:"\f144"}.fa-ticket:before{content:"\f145"}.fa-minus-square:before{content:"\f146"}.fa-minus-square-o:before{content:"\f147"}.fa-level-up:before{content:"\f148"}.fa-level-down:before{content:"\f149"}.fa-check-square:before{content:"\f14a"}.fa-pencil-square:before{content:"\f14b"}.fa-external-link-square:before{content:"\f14c"}.fa-share-square:before{content:"\f14d"}.fa-compass:before{content:"\f14e"}.fa-toggle-down:before,.fa-caret-square-o-down:before{content:"\f150"}.fa-toggle-up:before,.fa-caret-square-o-up:before{content:"\f151"}.fa-toggle-right:before,.fa-caret-square-o-right:before{content:"\f152"}.fa-euro:before,.fa-eur:before{content:"\f153"}.fa-gbp:before{content:"\f154"}.fa-dollar:before,.fa-usd:before{content:"\f155"}.fa-rupee:before,.fa-inr:before{content:"\f156"}.fa-cny:before,.fa-rmb:before,.fa-yen:before,.fa-jpy:before{content:"\f157"}.fa-ruble:before,.fa-rouble:before,.fa-rub:before{content:"\f158"}.fa-won:before,.fa-krw:before{content:"\f159"}.fa-bitcoin:before,.fa-btc:before{content:"\f15a"}.fa-file:before{content:"\f15b"}.fa-file-text:before{content:"\f15c"}.fa-sort-alpha-asc:before{content:"\f15d"}.fa-sort-alpha-desc:before{content:"\f15e"}.fa-sort-amount-asc:before{content:"\f160"}.fa-sort-amount-desc:before{content:"\f161"}.fa-sort-numeric-asc:before{content:"\f162"}.fa-sort-numeric-desc:before{content:"\f163"}.fa-thumbs-up:before{content:"\f164"}.fa-thumbs-down:before{content:"\f165"}.fa-youtube-square:before{content:"\f166"}.fa-youtube:before{content:"\f167"}.fa-xing:before{content:"\f168"}.fa-xing-square:before{content:"\f169"}.fa-youtube-play:before{content:"\f16a"}.fa-dropbox:before{content:"\f16b"}.fa-stack-overflow:before{content:"\f16c"}.fa-instagram:before{content:"\f16d"}.fa-flickr:before{content:"\f16e"}.fa-adn:before{content:"\f170"}.fa-bitbucket:before{content:"\f171"}.fa-bitbucket-square:before{content:"\f172"}.fa-tumblr:before{content:"\f173"}.fa-tumblr-square:before{content:"\f174"}.fa-long-arrow-down:before{content:"\f175"}.fa-long-arrow-up:before{content:"\f176"}.fa-long-arrow-left:before{content:"\f177"}.fa-long-arrow-right:before{content:"\f178"}.fa-apple:before{content:"\f179"}.fa-windows:before{content:"\f17a"}.fa-android:before{content:"\f17b"}.fa-linux:before{content:"\f17c"}.fa-dribbble:before{content:"\f17d"}.fa-skype:before{content:"\f17e"}.fa-foursquare:before{content:"\f180"}.fa-trello:before{content:"\f181"}.fa-female:before{content:"\f182"}.fa-male:before{content:"\f183"}.fa-gittip:before,.fa-gratipay:before{content:"\f184"}.fa-sun-o:before{content:"\f185"}.fa-moon-o:before{content:"\f186"}.fa-archive:before{content:"\f187"}.fa-bug:before{content:"\f188"}.fa-vk:before{content:"\f189"}.fa-weibo:before{content:"\f18a"}.fa-renren:before{content:"\f18b"}.fa-pagelines:before{content:"\f18c"}.fa-stack-exchange:before{content:"\f18d"}.fa-arrow-circle-o-right:before{content:"\f18e"}.fa-arrow-circle-o-left:before{content:"\f190"}.fa-toggle-left:before,.fa-caret-square-o-left:before{content:"\f191"}.fa-dot-circle-o:before{content:"\f192"}.fa-wheelchair:before{content:"\f193"}.fa-vimeo-square:before{content:"\f194"}.fa-turkish-lira:before,.fa-try:before{content:"\f195"}.fa-plus-square-o:before{content:"\f196"}.fa-space-shuttle:before{content:"\f197"}.fa-slack:before{content:"\f198"}.fa-envelope-square:before{content:"\f199"}.fa-wordpress:before{content:"\f19a"}.fa-openid:before{content:"\f19b"}.fa-institution:before,.fa-bank:before,.fa-university:before{content:"\f19c"}.fa-mortar-board:before,.fa-graduation-cap:before{content:"\f19d"}.fa-yahoo:before{content:"\f19e"}.fa-google:before{content:"\f1a0"}.fa-reddit:before{content:"\f1a1"}.fa-reddit-square:before{content:"\f1a2"}.fa-stumbleupon-circle:before{content:"\f1a3"}.fa-stumbleupon:before{content:"\f1a4"}.fa-delicious:before{content:"\f1a5"}.fa-digg:before{content:"\f1a6"}.fa-pied-piper-pp:before{content:"\f1a7"}.fa-pied-piper-alt:before{content:"\f1a8"}.fa-drupal:before{content:"\f1a9"}.fa-joomla:before{content:"\f1aa"}.fa-language:before{content:"\f1ab"}.fa-fax:before{content:"\f1ac"}.fa-building:before{content:"\f1ad"}.fa-child:before{content:"\f1ae"}.fa-paw:before{content:"\f1b0"}.fa-spoon:before{content:"\f1b1"}.fa-cube:before{content:"\f1b2"}.fa-cubes:before{content:"\f1b3"}.fa-behance:before{content:"\f1b4"}.fa-behance-square:before{content:"\f1b5"}.fa-steam:before{content:"\f1b6"}.fa-steam-square:before{content:"\f1b7"}.fa-recycle:before{content:"\f1b8"}.fa-automobile:before,.fa-car:before{content:"\f1b9"}.fa-cab:before,.fa-taxi:before{content:"\f1ba"}.fa-tree:before{content:"\f1bb"}.fa-spotify:before{content:"\f1bc"}.fa-deviantart:before{content:"\f1bd"}.fa-soundcloud:before{content:"\f1be"}.fa-database:before{content:"\f1c0"}.fa-file-pdf-o:before{content:"\f1c1"}.fa-file-word-o:before{content:"\f1c2"}.fa-file-excel-o:before{content:"\f1c3"}.fa-file-powerpoint-o:before{content:"\f1c4"}.fa-file-photo-o:before,.fa-file-picture-o:before,.fa-file-image-o:before{content:"\f1c5"}.fa-file-zip-o:before,.fa-file-archive-o:before{content:"\f1c6"}.fa-file-sound-o:before,.fa-file-audio-o:before{content:"\f1c7"}.fa-file-movie-o:before,.fa-file-video-o:before{content:"\f1c8"}.fa-file-code-o:before{content:"\f1c9"}.fa-vine:before{content:"\f1ca"}.fa-codepen:before{content:"\f1cb"}.fa-jsfiddle:before{content:"\f1cc"}.fa-life-bouy:before,.fa-life-buoy:before,.fa-life-saver:before,.fa-support:before,.fa-life-ring:before{content:"\f1cd"}.fa-circle-o-notch:before{content:"\f1ce"}.fa-ra:before,.fa-resistance:before,.fa-rebel:before{content:"\f1d0"}.fa-ge:before,.fa-empire:before{content:"\f1d1"}.fa-git-square:before{content:"\f1d2"}.fa-git:before{content:"\f1d3"}.fa-y-combinator-square:before,.fa-yc-square:before,.fa-hacker-news:before{content:"\f1d4"}.fa-tencent-weibo:before{content:"\f1d5"}.fa-qq:before{content:"\f1d6"}.fa-wechat:before,.fa-weixin:before{content:"\f1d7"}.fa-send:before,.fa-paper-plane:before{content:"\f1d8"}.fa-send-o:before,.fa-paper-plane-o:before{content:"\f1d9"}.fa-history:before{content:"\f1da"}.fa-circle-thin:before{content:"\f1db"}.fa-header:before{content:"\f1dc"}.fa-paragraph:before{content:"\f1dd"}.fa-sliders:before{content:"\f1de"}.fa-share-alt:before{content:"\f1e0"}.fa-share-alt-square:before{content:"\f1e1"}.fa-bomb:before{content:"\f1e2"}.fa-soccer-ball-o:before,.fa-futbol-o:before{content:"\f1e3"}.fa-tty:before{content:"\f1e4"}.fa-binoculars:before{content:"\f1e5"}.fa-plug:before{content:"\f1e6"}.fa-slideshare:before{content:"\f1e7"}.fa-twitch:before{content:"\f1e8"}.fa-yelp:before{content:"\f1e9"}.fa-newspaper-o:before{content:"\f1ea"}.fa-wifi:before{content:"\f1eb"}.fa-calculator:before{content:"\f1ec"}.fa-paypal:before{content:"\f1ed"}.fa-google-wallet:before{content:"\f1ee"}.fa-cc-visa:before{content:"\f1f0"}.fa-cc-mastercard:before{content:"\f1f1"}.fa-cc-discover:before{content:"\f1f2"}.fa-cc-amex:before{content:"\f1f3"}.fa-cc-paypal:before{content:"\f1f4"}.fa-cc-stripe:before{content:"\f1f5"}.fa-bell-slash:before{content:"\f1f6"}.fa-bell-slash-o:before{content:"\f1f7"}.fa-trash:before{content:"\f1f8"}.fa-copyright:before{content:"\f1f9"}.fa-at:before{content:"\f1fa"}.fa-eyedropper:before{content:"\f1fb"}.fa-paint-brush:before{content:"\f1fc"}.fa-birthday-cake:before{content:"\f1fd"}.fa-area-chart:before{content:"\f1fe"}.fa-pie-chart:before{content:"\f200"}.fa-line-chart:before{content:"\f201"}.fa-lastfm:before{content:"\f202"}.fa-lastfm-square:before{content:"\f203"}.fa-toggle-off:before{content:"\f204"}.fa-toggle-on:before{content:"\f205"}.fa-bicycle:before{content:"\f206"}.fa-bus:before{content:"\f207"}.fa-ioxhost:before{content:"\f208"}.fa-angellist:before{content:"\f209"}.fa-cc:before{content:"\f20a"}.fa-shekel:before,.fa-sheqel:before,.fa-ils:before{content:"\f20b"}.fa-meanpath:before{content:"\f20c"}.fa-buysellads:before{content:"\f20d"}.fa-connectdevelop:before{content:"\f20e"}.fa-dashcube:before{content:"\f210"}.fa-forumbee:before{content:"\f211"}.fa-leanpub:before{content:"\f212"}.fa-sellsy:before{content:"\f213"}.fa-shirtsinbulk:before{content:"\f214"}.fa-simplybuilt:before{content:"\f215"}.fa-skyatlas:before{content:"\f216"}.fa-cart-plus:before{content:"\f217"}.fa-cart-arrow-down:before{content:"\f218"}.fa-diamond:before{content:"\f219"}.fa-ship:before{content:"\f21a"}.fa-user-secret:before{content:"\f21b"}.fa-motorcycle:before{content:"\f21c"}.fa-street-view:before{content:"\f21d"}.fa-heartbeat:before{content:"\f21e"}.fa-venus:before{content:"\f221"}.fa-mars:before{content:"\f222"}.fa-mercury:before{content:"\f223"}.fa-intersex:before,.fa-transgender:before{content:"\f224"}.fa-transgender-alt:before{content:"\f225"}.fa-venus-double:before{content:"\f226"}.fa-mars-double:before{content:"\f227"}.fa-venus-mars:before{content:"\f228"}.fa-mars-stroke:before{content:"\f229"}.fa-mars-stroke-v:before{content:"\f22a"}.fa-mars-stroke-h:before{content:"\f22b"}.fa-neuter:before{content:"\f22c"}.fa-genderless:before{content:"\f22d"}.fa-facebook-official:before{content:"\f230"}.fa-pinterest-p:before{content:"\f231"}.fa-whatsapp:before{content:"\f232"}.fa-server:before{content:"\f233"}.fa-user-plus:before{content:"\f234"}.fa-user-times:before{content:"\f235"}.fa-hotel:before,.fa-bed:before{content:"\f236"}.fa-viacoin:before{content:"\f237"}.fa-train:before{content:"\f238"}.fa-subway:before{content:"\f239"}.fa-medium:before{content:"\f23a"}.fa-yc:before,.fa-y-combinator:before{content:"\f23b"}.fa-optin-monster:before{content:"\f23c"}.fa-opencart:before{content:"\f23d"}.fa-expeditedssl:before{content:"\f23e"}.fa-battery-4:before,.fa-battery:before,.fa-battery-full:before{content:"\f240"}.fa-battery-3:before,.fa-battery-three-quarters:before{content:"\f241"}.fa-battery-2:before,.fa-battery-half:before{content:"\f242"}.fa-battery-1:before,.fa-battery-quarter:before{content:"\f243"}.fa-battery-0:before,.fa-battery-empty:before{content:"\f244"}.fa-mouse-pointer:before{content:"\f245"}.fa-i-cursor:before{content:"\f246"}.fa-object-group:before{content:"\f247"}.fa-object-ungroup:before{content:"\f248"}.fa-sticky-note:before{content:"\f249"}.fa-sticky-note-o:before{content:"\f24a"}.fa-cc-jcb:before{content:"\f24b"}.fa-cc-diners-club:before{content:"\f24c"}.fa-clone:before{content:"\f24d"}.fa-balance-scale:before{content:"\f24e"}.fa-hourglass-o:before{content:"\f250"}.fa-hourglass-1:before,.fa-hourglass-start:before{content:"\f251"}.fa-hourglass-2:before,.fa-hourglass-half:before{content:"\f252"}.fa-hourglass-3:before,.fa-hourglass-end:before{content:"\f253"}.fa-hourglass:before{content:"\f254"}.fa-hand-grab-o:before,.fa-hand-rock-o:before{content:"\f255"}.fa-hand-stop-o:before,.fa-hand-paper-o:before{content:"\f256"}.fa-hand-scissors-o:before{content:"\f257"}.fa-hand-lizard-o:before{content:"\f258"}.fa-hand-spock-o:before{content:"\f259"}.fa-hand-pointer-o:before{content:"\f25a"}.fa-hand-peace-o:before{content:"\f25b"}.fa-trademark:before{content:"\f25c"}.fa-registered:before{content:"\f25d"}.fa-creative-commons:before{content:"\f25e"}.fa-gg:before{content:"\f260"}.fa-gg-circle:before{content:"\f261"}.fa-tripadvisor:before{content:"\f262"}.fa-odnoklassniki:before{content:"\f263"}.fa-odnoklassniki-square:before{content:"\f264"}.fa-get-pocket:before{content:"\f265"}.fa-wikipedia-w:before{content:"\f266"}.fa-safari:before{content:"\f267"}.fa-chrome:before{content:"\f268"}.fa-firefox:before{content:"\f269"}.fa-opera:before{content:"\f26a"}.fa-internet-explorer:before{content:"\f26b"}.fa-tv:before,.fa-television:before{content:"\f26c"}.fa-contao:before{content:"\f26d"}.fa-500px:before{content:"\f26e"}.fa-amazon:before{content:"\f270"}.fa-calendar-plus-o:before{content:"\f271"}.fa-calendar-minus-o:before{content:"\f272"}.fa-calendar-times-o:before{content:"\f273"}.fa-calendar-check-o:before{content:"\f274"}.fa-industry:before{content:"\f275"}.fa-map-pin:before{content:"\f276"}.fa-map-signs:before{content:"\f277"}.fa-map-o:before{content:"\f278"}.fa-map:before{content:"\f279"}.fa-commenting:before{content:"\f27a"}.fa-commenting-o:before{content:"\f27b"}.fa-houzz:before{content:"\f27c"}.fa-vimeo:before{content:"\f27d"}.fa-black-tie:before{content:"\f27e"}.fa-fonticons:before{content:"\f280"}.fa-reddit-alien:before{content:"\f281"}.fa-edge:before{content:"\f282"}.fa-credit-card-alt:before{content:"\f283"}.fa-codiepie:before{content:"\f284"}.fa-modx:before{content:"\f285"}.fa-fort-awesome:before{content:"\f286"}.fa-usb:before{content:"\f287"}.fa-product-hunt:before{content:"\f288"}.fa-mixcloud:before{content:"\f289"}.fa-scribd:before{content:"\f28a"}.fa-pause-circle:before{content:"\f28b"}.fa-pause-circle-o:before{content:"\f28c"}.fa-stop-circle:before{content:"\f28d"}.fa-stop-circle-o:before{content:"\f28e"}.fa-shopping-bag:before{content:"\f290"}.fa-shopping-basket:before{content:"\f291"}.fa-hashtag:before{content:"\f292"}.fa-bluetooth:before{content:"\f293"}.fa-bluetooth-b:before{content:"\f294"}.fa-percent:before{content:"\f295"}.fa-gitlab:before{content:"\f296"}.fa-wpbeginner:before{content:"\f297"}.fa-wpforms:before{content:"\f298"}.fa-envira:before{content:"\f299"}.fa-universal-access:before{content:"\f29a"}.fa-wheelchair-alt:before{content:"\f29b"}.fa-question-circle-o:before{content:"\f29c"}.fa-blind:before{content:"\f29d"}.fa-audio-description:before{content:"\f29e"}.fa-volume-control-phone:before{content:"\f2a0"}.fa-braille:before{content:"\f2a1"}.fa-assistive-listening-systems:before{content:"\f2a2"}.fa-asl-interpreting:before,.fa-american-sign-language-interpreting:before{content:"\f2a3"}.fa-deafness:before,.fa-hard-of-hearing:before,.fa-deaf:before{content:"\f2a4"}.fa-glide:before{content:"\f2a5"}.fa-glide-g:before{content:"\f2a6"}.fa-signing:before,.fa-sign-language:before{content:"\f2a7"}.fa-low-vision:before{content:"\f2a8"}.fa-viadeo:before{content:"\f2a9"}.fa-viadeo-square:before{content:"\f2aa"}.fa-snapchat:before{content:"\f2ab"}.fa-snapchat-ghost:before{content:"\f2ac"}.fa-snapchat-square:before{content:"\f2ad"}.fa-pied-piper:before{content:"\f2ae"}.fa-first-order:before{content:"\f2b0"}.fa-yoast:before{content:"\f2b1"}.fa-themeisle:before{content:"\f2b2"}.fa-google-plus-circle:before,.fa-google-plus-official:before{content:"\f2b3"}.fa-fa:before,.fa-font-awesome:before{content:"\f2b4"}.fa-handshake-o:before{content:"\f2b5"}.fa-envelope-open:before{content:"\f2b6"}.fa-envelope-open-o:before{content:"\f2b7"}.fa-linode:before{content:"\f2b8"}.fa-address-book:before{content:"\f2b9"}.fa-address-book-o:before{content:"\f2ba"}.fa-vcard:before,.fa-address-card:before{content:"\f2bb"}.fa-vcard-o:before,.fa-address-card-o:before{content:"\f2bc"}.fa-user-circle:before{content:"\f2bd"}.fa-user-circle-o:before{content:"\f2be"}.fa-user-o:before{content:"\f2c0"}.fa-id-badge:before{content:"\f2c1"}.fa-drivers-license:before,.fa-id-card:before{content:"\f2c2"}.fa-drivers-license-o:before,.fa-id-card-o:before{content:"\f2c3"}.fa-quora:before{content:"\f2c4"}.fa-free-code-camp:before{content:"\f2c5"}.fa-telegram:before{content:"\f2c6"}.fa-thermometer-4:before,.fa-thermometer:before,.fa-thermometer-full:before{content:"\f2c7"}.fa-thermometer-3:before,.fa-thermometer-three-quarters:before{content:"\f2c8"}.fa-thermometer-2:before,.fa-thermometer-half:before{content:"\f2c9"}.fa-thermometer-1:before,.fa-thermometer-quarter:before{content:"\f2ca"}.fa-thermometer-0:before,.fa-thermometer-empty:before{content:"\f2cb"}.fa-shower:before{content:"\f2cc"}.fa-bathtub:before,.fa-s15:before,.fa-bath:before{content:"\f2cd"}.fa-podcast:before{content:"\f2ce"}.fa-window-maximize:before{content:"\f2d0"}.fa-window-minimize:before{content:"\f2d1"}.fa-window-restore:before{content:"\f2d2"}.fa-times-rectangle:before,.fa-window-close:before{content:"\f2d3"}.fa-times-rectangle-o:before,.fa-window-close-o:before{content:"\f2d4"}.fa-bandcamp:before{content:"\f2d5"}.fa-grav:before{content:"\f2d6"}.fa-etsy:before{content:"\f2d7"}.fa-imdb:before{content:"\f2d8"}.fa-ravelry:before{content:"\f2d9"}.fa-eercast:before{content:"\f2da"}.fa-microchip:before{content:"\f2db"}.fa-snowflake-o:before{content:"\f2dc"}.fa-superpowers:before{content:"\f2dd"}.fa-wpexplorer:before{content:"\f2de"}.fa-meetup:before{content:"\f2e0"}.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0, 0, 0, 0);border:0}.sr-only-focusable:active,.sr-only-focusable:focus{position:static;width:auto;height:auto;margin:0;overflow:visible;clip:auto} diff --git a/api/src/unraid-api/unraid-file-modifier/modifications/__test__/__fixtures__/downloaded/.set-password.php b/api/src/unraid-api/unraid-file-modifier/modifications/__test__/__fixtures__/downloaded/.set-password.php new file mode 100644 index 0000000000..091f31be3e --- /dev/null +++ b/api/src/unraid-api/unraid-file-modifier/modifications/__test__/__fixtures__/downloaded/.set-password.php @@ -0,0 +1,416 @@ + _('root requires a password'), + 'mismatch' => _('Password confirmation does not match'), + 'maxLength' => _('Max password length is 128 characters'), + 'saveError' => _('Unable to set password'), +]; +$POST_ERROR = ''; + +/** + * POST handler + */ +if (!empty($_POST['password']) && !empty($_POST['confirmPassword'])) { + if ($_POST['password'] !== $_POST['confirmPassword']) return $POST_ERROR = $VALIDATION_MESSAGES['mismatch']; + if (strlen($_POST['password']) > $MAX_PASS_LENGTH) return $POST_ERROR = $VALIDATION_MESSAGES['maxLength']; + + $userName = 'root'; + $userPassword = base64_encode($_POST['password']); + + exec("/usr/local/sbin/emcmd 'cmdUserEdit=Change&userName=$userName&userPassword=$userPassword'", $output, $result); + if ($result == 0) { + // PAM service will log to syslog: "password changed for root" + if (session_status()==PHP_SESSION_NONE) session_start(); + $_SESSION['unraid_login'] = time(); + $_SESSION['unraid_user'] = 'root'; + session_regenerate_id(true); + session_write_close(); + + // Redirect the user to the start page + header("Location: /".$start_page); + exit; + } + + // Error when attempting to set password + my_logger("{$VALIDATION_MESSAGES['saveError']} [REMOTE_ADDR]: {$REMOTE_ADDR}"); + return $POST_ERROR = $VALIDATION_MESSAGES['saveError']; +} + +$THEME_DARK = in_array($display['theme'],['black','gray']); +?> + + + + + + + + + + + + + + <?=$var['NAME']?>/SetPassword + + + + + + +
+
+ +
+
+
+

+

+

.

+

.

+
+ +
+ + + +
+ + +
+ + + + +

+
+ +
+
+
+
+ + + diff --git a/api/src/unraid-api/unraid-file-modifier/modifications/__test__/__fixtures__/downloaded/DockerContainers.page b/api/src/unraid-api/unraid-file-modifier/modifications/__test__/__fixtures__/downloaded/DockerContainers.page new file mode 100644 index 0000000000..1412b71ba5 --- /dev/null +++ b/api/src/unraid-api/unraid-file-modifier/modifications/__test__/__fixtures__/downloaded/DockerContainers.page @@ -0,0 +1,196 @@ +Menu="Docker:1" +Title="Docker Containers" +Tag="cubes" +Cond="is_file('/var/run/dockerd.pid')" +Markdown="false" +Nchan="docker_load:stop" +--- + + "._('Please wait')."... "._('starting up containers'); +$cpus = cpu_list(); +?> + + + + + +
_(Application)__(Version)__(Network)__(Container IP)__(Container Port)__(LAN IP:Port)__(Volume Mappings)_ (_(App to Host)_)_(CPU & Memory load)__(Autostart)__(Uptime)_
+ + + + + + + + + + + + + diff --git a/api/src/unraid-api/unraid-file-modifier/modifications/__test__/__fixtures__/downloaded/Notify.php b/api/src/unraid-api/unraid-file-modifier/modifications/__test__/__fixtures__/downloaded/Notify.php new file mode 100644 index 0000000000..e692b41b03 --- /dev/null +++ b/api/src/unraid-api/unraid-file-modifier/modifications/__test__/__fixtures__/downloaded/Notify.php @@ -0,0 +1,59 @@ + + $value) { + switch ($option) { + case 'e': + case 's': + case 'd': + case 'i': + case 'm': + $notify .= " -{$option} ".escapeshellarg($value); + break; + case 'x': + case 't': + $notify .= " -{$option}"; + break; + } + } + shell_exec("$notify add"); + break; +case 'get': + echo shell_exec("$notify get"); + break; +case 'hide': + $file = $_POST['file']??''; + if (file_exists($file) && $file==realpath($file) && pathinfo($file,PATHINFO_EXTENSION)=='notify') chmod($file,0400); + break; +case 'archive': + $file = $_POST['file']??''; + if ($file && strpos($file,'/')===false) shell_exec("$notify archive ".escapeshellarg($file)); + break; +} +?> diff --git a/api/src/unraid-api/unraid-file-modifier/modifications/__test__/__fixtures__/downloaded/Translations.php b/api/src/unraid-api/unraid-file-modifier/modifications/__test__/__fixtures__/downloaded/Translations.php new file mode 100644 index 0000000000..c3bd47382d --- /dev/null +++ b/api/src/unraid-api/unraid-file-modifier/modifications/__test__/__fixtures__/downloaded/Translations.php @@ -0,0 +1,177 @@ + + $that) if (strpos($text,$word)!==false) {$text = str_replace($word,$that,$text); break;} + foreach ($days as $word => $that) if (strpos($text,$word)!==false) {$text = str_replace($word,$that,$text); break;} + return $text; + case 1: // number translation + parse_array($language['Numbers_array'] ?? '',$numbers); + return $numbers[$text] ?? $text; + case 2: // time translation + $keys = ['days','hours','minutes','seconds','day','hour','minute','second']; + foreach ($keys as $key) if (isset($language[$key])) $text = preg_replace("/\b$key\b/",$language[$key],$text); + return $text; + case 3: // device translation + [$p1,$p2] = array_pad(preg_split('/(?<=[a-z])(?= ?[0-9]+)/i',$text),2,''); + return _($p1).$p2; + default: // regular translation + $text = $language[preg_replace(['/\&|[\?\{\}\|\&\~\!\[\]\(\)\/\\:\*^\.\"\']|<.+?\/?>/','/^(null|yes|no|true|false|on|off|none)$/i','/ +/'],['','$1.',' '],$text)] ?? $text; + return preg_replace(['/\*\*(.+?)\*\*/','/\*(.+?)\*/',"/'/"],['$1','$1','''],$text); + } +} +function parse_lang_file($file) { + // parser for translation files, includes some trickery to handle PHP quirks. + return array_safe((array)parse_ini_string(preg_replace(['/^\s*?(null|yes|no|true|false|on|off|none)\s*?=/mi','/^\s*?([^>].*?)\s*?=\s*?(.*)\s*?$/m','/^:(.+_(help|plug)):$/m','/^:end$/m'],['$1.=','$1="$2"','_$1="','"'],escapeQuotes(file_get_contents($file))))); +} +function parse_help_file($file) { + // parser for help text files, includes some trickery to handle PHP quirks. + $text = array_tags((array)parse_ini_string(preg_replace(['/^$/m','/^([^:;].+)$/m','/^:(.+_help(_\d{8})?):$/m','/^:end$/m'],['>','>$1','_$1="','"'],escapeQuotes(file_get_contents($file))))); + return array_help($text); +} +function parse_text($text) { + // inline text parser + return preg_replace_callback('/_\((.+?)\)_/m',function($m){return _($m[1]);},preg_replace(["/^:(.+_help):$/m","/^:(.+_plug):$/m","/^:end$/m"],["","",""],$text)); +} +function parse_file($file,$markdown=true) { + // replacement of PHP include function + return $markdown ? Markdown(parse_text(file_get_contents($file))) : parse_text(file_get_contents($file)); +} +function parse_plugin($plugin) { + // parse and add plugin related translations + global $docroot,$language,$locale; + $plugin = strtolower($plugin); + $text = "$docroot/languages/$locale/$plugin.txt"; + if (file_exists($text)) { + $store = "$docroot/languages/$locale/$plugin.dot"; + if (!file_exists($store)) file_put_contents($store,serialize(parse_lang_file($text))); + $language = array_merge($language,unserialize(file_get_contents($store))); + } +} + +// internal helper functions +function parse_array($text,&$array) { + // multi keyword parser + parse_str(str_replace([' ',':'],['&','='],$text),$array); +} +function array_safe($array) { + // remove potential dangerous tags + return array_filter($array,function($v,$k){ + return strlen($v) && !preg_match('#<(script|iframe)(.*?)>(.+?)|<(link|meta)\s(.+?)/?>#is',html_entity_decode($v)); + },ARRAY_FILTER_USE_BOTH); +} +function array_tags($array) { + // filter outdated help tags + return array_filter($array,function($v,$k){ + $tag = explode('_',$k); + $tag = end($tag); + return ($tag=='help' ? true : $tag <= $_SESSION['buildDate']) && strlen($v); + },ARRAY_FILTER_USE_BOTH); +} +function array_help(&$array) { + // select latest applicable help + foreach ($array as $key => $val) { + $tag = explode('_',$key); + if (end($tag)=='help') continue; + $array[implode('_',array_slice($tag,0,-1))] = $array[$key]; + unset($array[$key]); + } + return $array; +} +function escapeQuotes($text) { + // escape double quotes + return str_replace(["\"\n",'"'],["\" \n",'\"'],$text); +} +function translate($key) { + // replaces multi-line sections + global $language,$netpool,$netpool6,$netport,$nginx; + if ($plug = isset($language[$key])) eval('?>'.Markdown($language[$key])); + return !$plug; +} + +// main +$language = []; +$locale = $_SESSION['locale'] ?? $login_locale ?? ''; +$return = "function _(t){return t;}"; +$jscript = "$docroot/webGui/javascript/translate.en_US.js"; +$root = "$docroot/languages/en_US/helptext.txt"; +$help = "$docroot/languages/en_US/helptext.dot"; + +if ($locale) { + $text = "$docroot/languages/$locale/translations.txt"; + if (file_exists($text)) { + $store = "$docroot/languages/$locale/translations.dot"; + // global translations + if (!file_exists($store)) file_put_contents($store,serialize(parse_lang_file($text))); + $language = unserialize(file_get_contents($store)); + } + if (file_exists("$docroot/languages/$locale/helptext.txt")) { + $root = "$docroot/languages/$locale/helptext.txt"; + $help = "$docroot/languages/$locale/helptext.dot"; + } + $jscript = "$docroot/webGui/javascript/translate.$locale.js"; + if (!file_exists($jscript)) { + // create javascript file with translations + $source = []; + $files = glob("$docroot/languages/$locale/javascript*.txt",GLOB_NOSORT); + foreach ($files as $js) $source = array_merge($source,parse_lang_file($js)); + if (count($source)) { + $script = ['function _(t){var l=[];']; + foreach ($source as $key => $value) $script[] = "l[\"$key\"]=\"$value\";"; + $script[] = "return l[t.replace(/\&|[\?\{\}\|\&\~\!\[\]\(\)\/\\:\*^\.\"']|<.+?\/?>/g,'').replace(/ +/g,' ')]||t;}"; + file_put_contents($jscript,implode($script)); + } else { + file_put_contents($jscript,$return); + } + } +} +// split URI into translation levels +$uri = array_filter(explode('/',strtolower(strtok($_SERVER['REQUEST_URI']??'','?')))); +foreach($uri as $more) { + $text = "$docroot/languages/$locale/$more.txt"; + if (file_exists($text)) { + // additional translations + $store = "$docroot/languages/$locale/$more.dot"; + if (!file_exists($store)) file_put_contents($store,serialize(parse_lang_file($text))); + $language = array_merge($language,unserialize(file_get_contents($store))); + } +} +// help text +if (($_SERVER['REQUEST_URI'][0]??'')=='/') { + if (!file_exists($help)) file_put_contents($help,serialize(parse_help_file($root))); + $language = array_merge($language,unserialize(file_get_contents($help))); +} +// remove unused variables +unset($return,$jscript,$root,$help,$store,$uri,$more,$text); +?> diff --git a/api/src/unraid-api/unraid-file-modifier/modifications/__test__/__fixtures__/downloaded/default-azure.css b/api/src/unraid-api/unraid-file-modifier/modifications/__test__/__fixtures__/downloaded/default-azure.css new file mode 100644 index 0000000000..eb4914fe30 --- /dev/null +++ b/api/src/unraid-api/unraid-file-modifier/modifications/__test__/__fixtures__/downloaded/default-azure.css @@ -0,0 +1,274 @@ +html{font-family:clear-sans,sans-serif;font-size:62.5%;height:100%} +body{font-size:1.3rem;color:#606e7f;background-color:#e4e2e4;padding:0;margin:0;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale} +img{border:none;text-decoration:none;vertical-align:middle} +p{text-align:left} +p.centered{text-align:left} +p:empty{display:none} +a:hover{text-decoration:underline} +a{color:#486dba;text-decoration:none} +a.none{color:#606e7f} +a.img{text-decoration:none;border:none} +a.info{position:relative} +a.info span{display:none;white-space:nowrap;font-variant:small-caps;position:absolute;top:16px;left:12px;color:#4f4f4f;line-height:2rem;padding:5px 8px;border:1px solid #42453e;border-radius:3px;background-color:#edeaef} +a.info:hover span{display:block;z-index:1} +a.nohand{cursor:default} +a.hand{cursor:pointer;text-decoration:none} +a.static{cursor:default;color:#909090;text-decoration:none} +a.view{display:inline-block;width:20px} +i.spacing{margin-left:0;margin-right:10px} +i.icon{font-size:1.6rem;margin-right:4px;vertical-align:middle} +i.title{display:none} +i.control{cursor:pointer;color:#909090;font-size:1.8rem} +i.favo{display:none;font-size:1.8rem;position:absolute} +pre ul{margin:0;padding-top:0;padding-bottom:0;padding-left:28px} +pre li{margin:0;padding-top:0;padding-bottom:0;padding-left:18px} +big{font-size:1.4rem;font-weight:bold;text-transform:uppercase} +hr{border:none;height:1px!important;color:#606e7f;background-color:#606e7f} +input[type=text],input[type=password],input[type=number],input[type=url],input[type=email],input[type=date],input[type=file],textarea,.textarea{font-family:clear-sans;font-size:1.3rem;background-color:transparent;border:1px solid #606e7f;padding:5px 6px;min-height:2rem;line-height:2rem;outline:none;width:300px;margin:0 20px 0 0;box-shadow:none;border-radius:0;color:#606e7f} +input[type=button],input[type=reset],input[type=submit],button,button[type=button],a.button,.sweet-alert button{font-family:clear-sans;font-size:1.2rem;border:1px solid #9f9180;border-radius:5px;min-width:76px;margin:10px 12px 10px 0;padding:8px;text-align:center;cursor:pointer;outline:none;color:#9f9180;background-color:#edeaef} +input[type=checkbox]{vertical-align:middle;margin-right:6px} +input[type=number]::-webkit-outer-spin-button,input[type=number]::-webkit-inner-spin-button{-webkit-appearance:none} +input[type=number]{-moz-appearance:textfield} +input:focus[type=text],input:focus[type=password],input:focus[type=number],input:focus[type=url],input:focus[type=email],input:focus[type=file],textarea:focus,.sweet-alert button:focus{background-color:#edeaef;border-color:#0072c6} +input:hover[type=button],input:hover[type=reset],input:hover[type=submit],button:hover,button:hover[type=button],a.button:hover,.sweet-alert button:hover{border-color:#0072c6;color:#4f4f4f;background-color:#edeaef!important} +input:active[type=button],input:active[type=reset],input:active[type=submit],button:active,button:active[type=button],a.button:active,.sweet-alert button:active{border-color:#0072c6;box-shadow:none} +input[disabled],button[disabled],input:hover[type=button][disabled],input:hover[type=reset][disabled],input:hover[type=submit][disabled],button:hover[disabled],button:hover[type=button][disabled],input:active[type=button][disabled],input:active[type=reset][disabled],input:active[type=submit][disabled],button:active[disabled],button:active[type=button][disabled],textarea[disabled],.sweet-alert button[disabled]{color:#808080;border-color:#808080;background-color:#c7c5cb;opacity:0.5;cursor:default} +input::-webkit-input-placeholder{color:#00529b} +select{-webkit-appearance:none;font-family:clear-sans;font-size:1.3rem;min-width:188px;max-width:314px;padding:6px 14px 6px 6px;margin:0 10px 0 0;border:1px solid #606e7f;box-shadow:none;border-radius:0;color:#606e7f;background-color:transparent;background-image:linear-gradient(66.6deg, transparent 60%, #606e7f 40%),linear-gradient(113.4deg, #606e7f 40%, transparent 60%);background-position:calc(100% - 8px),calc(100% - 4px);background-size:4px 6px,4px 6px;background-repeat:no-repeat;outline:none;display:inline-block;cursor:pointer} +select option{color:#606e7f;background-color:#edeaef} +select:focus{border-color:#0072c6} +select[disabled]{color:#808080;border-color:#808080;background-color:#c7c5cb;opacity:0.5;cursor:default} +select[name=enter_view]{font-size:1.2rem;margin:0;padding:0 12px 0 0;border:none;min-width:auto} +select[name=enter_share]{font-size:1.1rem;color:#9794a0;padding:0;border:none;min-width:40px;float:right;margin-top:18px;margin-right:20px} +select[name=port_select]{border:none;min-width:54px;padding-top:0;padding-bottom:0} +select.narrow{min-width:87px} +select.auto{min-width:auto} +select.slot{min-width:44rem;max-width:44rem} +input.narrow{width:174px} +input.trim{width:74px;min-width:74px} +textarea{resize:none} +#header{position:fixed;top:0;left:0;width:100%;height:90px;z-index:100;margin:0;background-color:#edeaef;background-size:100% 90px;background-repeat:no-repeat;border-bottom:1px solid #9794a0} +#header .logo{float:left;margin-left:75px;color:#e22828;text-align:center} +#header .logo svg{width:160px;display:block;margin:25px 0 8px 0} +#header .block{margin:0;float:right;text-align:right;background-color:rgba(237,234,239,0.2);padding:10px 12px} +#header .text-left{float:left;text-align:right;padding-right:5px;border-right:solid medium #f15a2c} +#header .text-right{float:right;text-align:left;padding-left:5px} +#header .text-right a{color:#606e7f} +#header .text-right #licensetype{font-weight:bold;font-style:italic;margin-right:4px} +#menu{position:fixed;top:0;left:0;bottom:12px;width:65px;padding:0;margin:0;background-color:#383a34;z-index:2000;box-shadow:inset -1px 0 2px #121510} +#nav-block{position:absolute;top:0;bottom:12px;color:#ffdfb9;white-space:nowrap;float:left;overflow-y:scroll;direction:rtl;letter-spacing:1.8px;scrollbar-width:none} +#nav-block::-webkit-scrollbar{display:none} +#nav-block{-ms-overflow-style:none;overflow:-moz-scrollbars-none} +#nav-block>div{direction:ltr} +.nav-item{width:40px;text-align:left;padding:14px 24px 14px 0;border-bottom:1px solid #42453e;font-size:18px!important;overflow:hidden;transition:.2s background-color ease} +.nav-item:hover{width:auto;padding-right:0;color:#ffdfb9;background:-webkit-gradient(linear,left top,right top,from(#e22828),to(#ff8c2f));background:linear-gradient(90deg,#e22828 0,#ff8c2f);-webkit-transition:all 0.2s ease-in-out;transition:all 0.2s ease-in-out;border-bottom-color:#e22828} +.nav-item:hover a{color:#ffdfb9;background:-webkit-gradient(linear,left top,right top,from(#e22828),to(#ff8c2f));background:linear-gradient(90deg,#e22828 0,#ff8c2f);border-bottom-color:#e22828;font-size:18px} +.nav-item img{display:none} +.nav-item a{color:#a6a7a7;text-decoration:none;padding:20px 80px 13px 16px} +.nav-item.util a{padding-left:24px} +.nav-item a:before{font-family:docker-icon,fontawesome,unraid;font-size:26px;margin-right:25px} +.nav-item.util a:before{font-size:16px} +.nav-item.active,.nav-item.active a{color:#ffdfb9;background:-webkit-gradient(linear,left top,right top,from(#e22828),to(#ff8c2f));background:linear-gradient(90deg,#e22828 0,#ff8c2f)} +.nav-item.HelpButton.active:hover,.nav-item.HelpButton.active a:hover{background:-webkit-gradient(linear,left top,right top,from(#e22828),to(#ff8c2f));background:linear-gradient(90deg,#e22828 0,#ff8c2f);font-size:18px} +.nav-item.HelpButton.active,.nav-item.HelpButton.active a{font-size:18px} +.nav-item a b{display:none} +.nav-user{position:fixed;top:102px;right:10px} +.nav-user a{color:#606e7f;background-color:transparent} +.LanguageButton{font-size:12px!important} /* Fix Switch Language Being Cut-Off */ +div.title{color:#39587f;margin:20px 0 10px 0;padding:10px 0;clear:both;background-color:#e4e2e4;border-bottom:1px solid #606e7f;letter-spacing:1.8px} +div.title span.left{font-size:1.6rem;text-transform:uppercase} +div.title span.right{font-size:1.6rem;padding-right:10px;float:right} +div.title span img,.title p{display:none} +div.title:first-child{margin-top:0} +div.title.shift{margin-top:-12px} +#clear{clear:both} +#footer{position:fixed;bottom:0;left:0;color:#808080;background-color:#121510;padding:5px 0;width:100%;height:1.6rem;line-height:1.6rem;text-align:center;z-index:10000} +#statusraid{float:left;padding-left:10px} +#countdown{margin:0 auto} +#copyright{font-family:bitstream;font-size:1.1rem;float:right;padding-right:10px} +.green{color:#4f8a10;padding-left:5px;padding-right:5px} +.red{color:#f0000c;padding-left:5px;padding-right:5px} +.orange{color:#e68a00;padding-left:5px;padding-right:5px} +.blue{color:#486dba;padding-left:5px;padding-right:5px} +.green-text,.passed{color:#4f8a10} +.red-text,.failed{color:#f0000c} +.orange-text,.warning{color:#e68a00} +.blue-text{color:#486dba} +.grey-text{color:#606060} +.green-orb{color:#33cc33} +.grey-orb{color:#c0c0c0} +.blue-orb{color:#0099ff} +.yellow-orb{color:#ff9900} +.red-orb{color:#ff3300} +.usage-bar{position:fixed;top:64px;left:300px;height:2.2rem;line-height:2.2rem;width:11rem;background-color:#606060} +.usage-bar>span{display:block;height:3px;color:#ffffff;background-color:#606e7f} +.usage-disk{position:relative;height:2.2rem;line-height:2.2rem;background-color:#eceaec;margin:0} +.usage-disk>span:first-child{position:absolute;left:0;margin:0!important;height:3px;background-color:#606e7f} +.usage-disk>span:last-child{position:relative;padding-right:4px;z-index:1000} +.usage-disk.sys{line-height:normal;background-color:transparent;margin:-15px 20px 0 44px} +.usage-disk.sys>span{line-height:normal;height:12px;padding:0} +.usage-disk.mm{height:3px;line-height:normal;background-color:transparent;margin:5px 20px 0 0} +.usage-disk.mm>span:first-child{height:3px;line-height:normal} +.notice{background:url(../images/notice.png) no-repeat 30px 50%;font-size:1.5rem;text-align:left;vertical-align:middle;padding-left:100px;height:6rem;line-height:6rem} +.greenbar{background:-webkit-radial-gradient(#127a05,#17bf0b);background:linear-gradient(#127a05,#17bf0b)} +.orangebar{background:-webkit-radial-gradient(#ce7c10,#f0b400);background:linear-gradient(#ce7c10,#f0b400)} +.redbar{background:-webkit-radial-gradient(#941c00,#de1100);background:linear-gradient(#941c00,#de1100)} +.graybar{background:-webkit-radial-gradient(#949494,#d9d9d9);background:linear-gradient(#949494,#d9d9d9)} +table{border-collapse:collapse;border-spacing:0;border-style:hidden;margin:0;width:100%} +table thead td{line-height:3rem;height:3rem;white-space:nowrap} +table tbody td{line-height:3rem;height:3rem;white-space:nowrap} +table tbody tr.tr_last{border-bottom:1px solid #606e7f} +table.unraid thead tr:first-child>td{font-size:1.2rem;text-transform:uppercase;letter-spacing:1px;color:#9794a0;border-bottom:1px solid #606e7f} +table.unraid tbody tr:not(.tr_last):hover>td{background-color:rgba(0,0,0,0.05)} +table.unraid tr>td{overflow:hidden;text-overflow:ellipsis;padding-left:8px} +table.unraid tr>td:hover{overflow:visible} +table.legacy{table-layout:auto!important} +table.legacy thead td{line-height:normal;height:auto;padding:7px 0} +table.legacy tbody td{line-height:normal;height:auto;padding:5px 0} +table.disk_status{table-layout:fixed} +table.disk_status tr>td:last-child{padding-right:8px} +table.disk_status tr>td:nth-child(1){width:13%} +table.disk_status tr>td:nth-child(2){width:30%} +table.disk_status tr>td:nth-child(3){width:8%;text-align:right} +table.disk_status tr>td:nth-child(n+4){width:7%;text-align:right} +table.disk_status tr.offline>td:nth-child(2){width:43%} +table.disk_status tr.offline>td:nth-child(n+3){width:5.5%} +table.disk_status tbody tr{border-bottom:1px solid #f3f0f4} +table.array_status{table-layout:fixed} +table.array_status tr>td{padding-left:8px;white-space:normal} +table.array_status tr>td:nth-child(1){width:33%} +table.array_status tr>td:nth-child(2){width:22%} +able.array_status.noshift{margin-top:0} +table.array_status td.line{border-top:1px solid #f3f0f4} +table.share_status{table-layout:fixed;margin-top:12px} +table.share_status tr>td{padding-left:8px} +table.share_status tr>td:nth-child(1){width:15%} +table.share_status tr>td:nth-child(2){width:30%} +table.share_status tr>td:nth-child(n+3){width:10%} +table.share_status tr>td:nth-child(5){width:15%} +table.dashboard{margin:0;border:none;background-color:#d7dbdd} +table.dashboard tbody{border:1px solid #cacfd2} +table.dashboard tr:first-child>td{height:3.6rem;padding-top:12px;font-size:1.6rem;font-weight:bold;letter-spacing:1.8px;text-transform:none;vertical-align:top} +table.dashboard tr:last-child>td{padding-bottom:20px} +table.dashboard tr.last>td{padding-bottom:20px} +table.dashboard tr.header>td{padding-bottom:10px;color:#9794a0} +table.dashboard tr{border:none} +table.dashboard td{line-height:normal;height:auto;padding:3px 10px;border:none!important} +table.dashboard td.stopgap{height:20px!important;line-height:20px!important;padding:0!important;background-color:#e4e2e4} +table.dashboard td.vpn{font-size:1.1rem;font-weight:bold;text-transform:uppercase;letter-spacing:1px} +table.dashboard td div.section{display:inline-block;vertical-align:top;margin-left:4px;font-size:1.2rem;font-weight:bold;text-transform:uppercase;letter-spacing:1px} +table.dashboard td div.section span{font-weight:normal;text-transform:none;letter-spacing:0;white-space:normal} +table.dashboard td span.info{float:right;margin-right:20px;font-size:1.2rem;font-weight:normal;text-transform:none;letter-spacing:0} +table.dashboard td span.info.title{font-weight:bold} +table.dashboard td span.load{display:inline-block;width:38px;text-align:right} +table.dashboard td span.finish{float:right;margin-right:24px} +table.dashboard i.control{float:right;font-size:1.4rem!important;margin:0 3px 0 0;cursor:pointer;color:#d7dbdd;background-color:rgba(0,0,0,0.3);padding:2px;border-radius:5px} +tr.alert{color:#f0000c;background-color:#ff9e9e} +tr.warn{color:#e68a00;background-color:#feefb3} +tr.past{color:#d63301;background-color:#ffddd1} +[name=arrayOps]{margin-top:12px} +span.error{color:#f0000c;background-color:#ff9e9e;display:block;width:100%} +span.warn{color:#e68a00;background-color:#feefb3;display:block;width:100%} +span.system{color:#00529b;background-color:#bde5f8;display:block;width:100%} +span.array{color:#4f8a10;background-color:#dff2bf;display:block;width:100%} +span.login{color:#d63301;background-color:#ffddd1;display:block;width:100%} +span.lite{background-color:#edeaef} +span.label{font-size:1.1rem;padding:2px 0 2px 6px;margin-right:6px;border-radius:4px;display:inline;width:auto;vertical-align:middle} +span.cpu-speed{display:block;color:#3b5998} +span.status{float:right;font-size:1.4rem;letter-spacing:1.8px} +span.status.vhshift{margin-top:0;margin-right:8px} +span.status.vshift{margin-top:-16px} +span.status.hshift{margin-right:-20px} +span.diskinfo{float:left;clear:both;margin-top:5px;padding-left:10px} +span.bitstream{font-family:bitstream;font-size:1.1rem} +span.p0{padding-left:0} +span.ucfirst{text-transform:capitalize} +span.strong{font-weight:bold} +span.big{font-size:1.4rem} +span.small{font-size:1.1rem} +span#dropbox{background:none;line-height:6rem;margin-right:20px} +span.outer{margin-bottom:20px;margin-right:0} +span.outer.solid{background-color:#d7dbdd} +span.hand{cursor:pointer} +span.outer.started>img,span.outer.started>i.img{opacity:1.0} +span.outer.stopped>img,span.outer.stopped>i.img{opacity:0.3} +span.outer.paused>img,span.outer.paused>i.img{opacity:0.6} +span.inner{display:inline-block;vertical-align:top} +span.state{font-size:1.1rem;margin-left:7px} +span.slots{display:inline-block;width:44rem;margin:0!important} +span.slots-left{float:left;margin:0!important} +input.subpool{float:right;margin:2px 0 0 0} +i.padlock{margin-right:8px;cursor:default;vertical-align:middle} +i.nolock{visibility:hidden;margin-right:8px;vertical-align:middle} +i.lock{margin-left:8px;cursor:default;vertical-align:middle} +i.orb{font-size:1.1rem;margin:0 8px 0 3px} +img.img,i.img{width:32px;height:32px;margin-right:10px} +img.icon{margin:-3px 4px 0 0} +img.list{width:auto;max-width:32px;height:32px} +i.list{font-size:32px} +a.list{text-decoration:none;color:inherit} +div.content{position:absolute;top:0;left:0;width:100%;padding-bottom:30px;z-index:-1;clear:both} +div.content.shift{margin-top:1px} +label+.content{margin-top:64px} +div.tabs{position:relative;margin:110px 20px 30px 90px;background-color:#e4e2e4} +div.tab{float:left;margin-top:23px} +div.tab input[id^='tab']{display:none} +div.tab [type=radio]+label:hover{cursor:pointer;border-color:#004e86;opacity:1} +div.tab [type=radio]:checked+label{cursor:default;background-color:transparent;color:#606e7f;border-color:#004e86;opacity:1} +div.tab [type=radio]+label~.content{display:none} +div.tab [type=radio]:checked+label~.content{display:inline} +div.tab [type=radio]+label{position:relative;letter-spacing:1.8px;padding:10px 10px;margin-right:2px;border-top-left-radius:12px;border-top-right-radius:12px;background-color:#606e7f;color:#b0b0b0;border:#8b98a7 1px solid;border-bottom:none;opacity:0.5} +div.tab [type=radio]+label img{display:none} +div.Panel{width:25%;height:auto;float:left;margin:0;padding:5px;border-right:#f3f0f4 1px solid;border-bottom:1px solid #f3f0f4;box-sizing:border-box} +div.Panel a{text-decoration:none} +div.Panel:hover{background-color:#edeaef} +div.Panel:hover .PanelText{text-decoration:underline} +div.Panel br,.vmtemplate br{display:none} +div.Panel img.PanelImg{float:left;width:auto;max-width:32px;height:32px;margin:10px} +div.Panel i.PanelIcon{float:left;font-size:32px;color:#606e7f;margin:10px} +div.Panel .PanelText{font-size:1.4rem;padding-top:16px;text-align:center} +div.user-list{float:left;padding:10px;margin-right:10px;margin-bottom:24px;border:1px solid #f3f0f4;border-radius:5px;line-height:2rem;height:10rem;width:10rem} +div.user-list img{width:auto;max-width:48px;height:48px;margin-bottom:16px} +div.user-list:hover{background-color:#edeaef} +div.vmheader{display:block;clear:both} +div.vmtemplate:hover{background-color:#edeaef} +div.vmtemplate{height:12rem;width:12rem;border:1px solid #f3f0f4} +div.vmtemplate img{margin-top:20px} +div.up{margin-top:-20px;border:1px solid #f3f0f4;padding:4px 6px;overflow:auto} +div.spinner{text-align:center;cursor:wait} +div.spinner.fixed{display:none;position:fixed;top:0;left:0;z-index:99999;bottom:0;right:0;margin:0} +div.spinner .unraid_mark{height:64px; position:fixed;top:50%;left:50%;margin-top:-16px;margin-left:-64px} +div.spinner .unraid_mark_2,div .unraid_mark_4{animation:mark_2 1.5s ease infinite} +div.spinner .unraid_mark_3{animation:mark_3 1.5s ease infinite} +div.spinner .unraid_mark_6,div .unraid_mark_8{animation:mark_6 1.5s ease infinite} +div.spinner .unraid_mark_7{animation:mark_7 1.5s ease infinite} +@keyframes mark_2{50% {transform:translateY(-40px)} 100% {transform:translateY(0px)}} +@keyframes mark_3{50% {transform:translateY(-62px)} 100% {transform:translateY(0px)}} +@keyframes mark_6{50% {transform:translateY(40px)} 100% {transform:translateY(0px)}} +@keyframes mark_7{50% {transform:translateY(62px)} 100% {transform: translateY(0px)}} +pre.up{margin-top:0} +pre{border:1px solid #f3f0f4;font-family:bitstream;font-size:1.3rem;line-height:1.8rem;padding:0;overflow:auto;margin-bottom:10px;padding:10px} +iframe#progressFrame{position:fixed;bottom:32px;left:60px;margin:0;padding:8px 8px 0 8px;width:100%;height:1.2rem;line-height:1.2rem;border-style:none;overflow:hidden;font-family:bitstream;font-size:1.1rem;color:#808080;white-space:nowrap;z-index:-2} +dl{margin-top:0;padding-left:12px;line-height:2.6rem} +dt{width:35%;clear:left;float:left;text-align:right;margin-right:4rem} +dd{margin-bottom:12px;white-space:nowrap} +dd p{margin:0 0 4px 0} +dd blockquote{padding-left:0} +blockquote{width:90%;margin:10px auto;text-align:left;padding:4px 20px;border:1px solid #bce8f1;color:#222222;background-color:#d9edf7;box-sizing:border-box} +blockquote.ontop{margin-top:0;margin-bottom:46px} +blockquote a{color:#ff8c2f;font-weight:600} +blockquote a:hover,blockquote a:focus{color:#f15a2c} +label.checkbox{display:block;position:relative;padding-left:28px;margin:3px 0;cursor:pointer;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none} +label.checkbox input{position:absolute;opacity:0;cursor:pointer} +span.checkmark{position:absolute;top:0;left:6px;height:14px;width:14px;background-color:#d4d2d4;border-radius:100%} +label.checkbox:hover input ~ .checkmark{background-color:#a4a2a4} +label.checkbox input:checked ~ .checkmark{background-color:#ff8c2f} +label.checkbox input:disabled ~ .checkmark{opacity:0.5} +a.bannerDismiss {float:right;cursor:pointer;text-decoration:none;margin-right:1rem} +.bannerDismiss::before {content:"\e92f";font-family:Unraid;color:#e68a00} +a.bannerInfo {cursor:pointer;text-decoration:none} +.bannerInfo::before {content:"\f05a";font-family:fontAwesome;color:#e68a00} +::-webkit-scrollbar{width:8px;height:8px;background:transparent} +::-webkit-scrollbar-thumb{background:lightgray;border-radius:10px} +::-webkit-scrollbar-corner{background:lightgray;border-radius:10px} +::-webkit-scrollbar-thumb:hover{background:gray} diff --git a/api/src/unraid-api/unraid-file-modifier/modifications/__test__/__fixtures__/downloaded/default-base.css b/api/src/unraid-api/unraid-file-modifier/modifications/__test__/__fixtures__/downloaded/default-base.css new file mode 100644 index 0000000000..f60aaffe15 --- /dev/null +++ b/api/src/unraid-api/unraid-file-modifier/modifications/__test__/__fixtures__/downloaded/default-base.css @@ -0,0 +1,2220 @@ +html { + font-family: clear-sans, sans-serif; + font-size: 62.5%; + height: 100%; +} +body { + font-size: 1.3rem; + color: var(--text-color); + background-color: var(--background-color); + padding: 0; + margin: 0; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} +@media (max-width: 1280px) { + #template { + min-width: 1260px; + max-width: 1260px; + margin: 0; + } +} +@media (min-width: 1281px) { + #template { + min-width: 1260px; + margin: 0 10px; + } +} +@media (min-width: 1921px) { + #template { + min-width: 1260px; + max-width: 1920px; + margin: 0 auto; + } +} +img { + border: none; + text-decoration: none; + vertical-align: middle; +} +p { + text-align: justify; +} +p.centered { + text-align: left; +} +p:empty { + display: none; +} +a:hover { + text-decoration: underline; +} +a { + color: var(--blue-800); + text-decoration: none; +} +a.none { + color: var(--text-color); +} +a.img { + text-decoration: none; + border: none; +} +a.info { + position: relative; +} +a.info span { + display: none; + white-space: nowrap; + font-variant: small-caps; + position: absolute; + top: 16px; + left: 12px; + line-height: 2rem; + color: var(--text-color); + padding: 5px 8px; + border: 1px solid var(--inverse-border-color); + border-radius: 3px; + background-color: var(--background-color); + box-shadow: var(--small-shadow); +} +a.info:hover span { + display: block; + z-index: 1; +} +a.nohand { + cursor: default; +} +a.hand { + cursor: pointer; + text-decoration: none; +} +a.static { + cursor: default; + color: var(--alt-text-color); + text-decoration: none; +} +a.view { + display: inline-block; + width: 20px; +} +i.spacing { + margin-left: -6px; +} +i.icon { + font-size: 1.6rem; + margin-right: 4px; + vertical-align: middle; +} +i.title { + margin-right: 8px; +} +i.control { + cursor: pointer; + color: var(--alt-text-color); + font-size: 1.8rem; +} +i.favo { + display: none; + font-size: 1.8rem; + position: absolute; + margin-left: 12px; +} +hr { + border: none; + height: 1px !important; + color: var(--hr-color); + background-color: var(--hr-color); +} +input[type="text"], +input[type="password"], +input[type="number"], +input[type="url"], +input[type="email"], +input[type="date"], +input[type="file"], +textarea, +.textarea { + color: var(--text-color); + font-family: clear-sans; + font-size: 1.3rem; + background-color: transparent; + border-width: 0; + border-style: solid; + border-color: var(--input-border-color); + border-bottom-width: 1px; + padding: 4px 0; + text-indent: 0; + min-height: 2rem; + line-height: 2rem; + outline: none; + width: 300px; + margin: 0 20px 0 0; + box-shadow: none; + border-radius: 0; +} +input[type="button"], +input[type="reset"], +input[type="submit"], +button, +button[type="button"], +a.button, +.sweet-alert button { + font-family: clear-sans; + font-size: 1.1rem; + font-weight: bold; + letter-spacing: 1.8px; + text-transform: uppercase; + min-width: 86px; + margin: 10px 12px 10px 0; + padding: 8px; + text-align: center; + text-decoration: none; + white-space: nowrap; + cursor: pointer; + outline: none; + border-radius: 4px; + border: var(--button-border); + color: var(--button-text-color); + background: var(--button-background); + background-size: var(--button-background-size); +} +input[type="checkbox"] { + vertical-align: middle; + margin-right: 6px; +} +input[type="number"]::-webkit-outer-spin-button, +input[type="number"]::-webkit-inner-spin-button { + -webkit-appearance: none; +} +input[type="number"] { + -moz-appearance: textfield; +} +input:focus[type="text"], +input:focus[type="password"], +input:focus[type="number"], +input:focus[type="url"], +input:focus[type="email"], +input:focus[type="file"], +textarea:focus, +.sweet-alert button:focus { + background-color: var(--focus-input-bg-color); + outline: 0; +} +input:hover[type="button"], +input:hover[type="reset"], +input:hover[type="submit"], +button:hover, +button:hover[type="button"], +a.button:hover, +.sweet-alert button:hover { + color: var(--hover-button-text-color); + background: var(--hover-button-background); +} +input[disabled], +textarea[disabled] { + color: var(--text-color); + border-bottom-color: var(--disabled-input-border-color); + opacity: 0.5; + cursor: default; +} +input[type="button"][disabled], +input[type="reset"][disabled], +input[type="submit"][disabled], +button[disabled], +button[type="button"][disabled], +a.button[disabled] input:hover[type="button"][disabled], +input:hover[type="reset"][disabled], +input:hover[type="submit"][disabled], +button:hover[disabled], +button:hover[type="button"][disabled], +a.button:hover[disabled] input:active[type="button"][disabled], +input:active[type="reset"][disabled], +input:active[type="submit"][disabled], +button:active[disabled], +button:active[type="button"][disabled], +a.button:active[disabled], +.sweet-alert button[disabled] { + opacity: 0.5; + cursor: default; + color: var(--disabled-text-color); + background: -webkit-gradient( + linear, + left top, + right top, + from(var(--gray-600)), + to(var(--gray-500)) + ) + 0 0 no-repeat, + -webkit-gradient( + linear, + left top, + right top, + from(var(--gray-600)), + to(var(--gray-500)) + ) 0 100% no-repeat, + -webkit-gradient( + linear, + left bottom, + left top, + from(var(--gray-600)), + to(var(--gray-600)) + ) 0 100% no-repeat, + -webkit-gradient( + linear, + left bottom, + left top, + from(var(--gray-500)), + to(var(--gray-500)) + ) 100% 100% no-repeat; + background: linear-gradient(90deg, var(--gray-600) 0, var(--gray-500)) 0 0 no-repeat, + linear-gradient(90deg, var(--gray-600) 0, var(--gray-500)) 0 100% no-repeat, + linear-gradient(0deg, var(--gray-600) 0, var(--gray-600)) 0 100% no-repeat, + linear-gradient(0deg, var(--gray-500) 0, var(--gray-500)) 100% 100% no-repeat; + background-size: 100% 2px, 100% 2px, 2px 100%, 2px 100%; +} +input::-webkit-input-placeholder { + color: var(--link-text-color); +} +select { + -webkit-appearance: none; + font-family: clear-sans; + font-size: 1.3rem; + min-width: 166px; + max-width: 300px; + padding: 5px 8px 5px 0; + text-indent: 0; + margin: 0 10px 0 0; + border: none; + border-bottom: 1px solid var(--input-border-color); + box-shadow: none; + border-radius: 0; + color: var(--text-color); + background-color: transparent; + background-image: linear-gradient(66.6deg, transparent 60%, var(--input-border-color) 40%), + linear-gradient(113.4deg, var(--input-border-color) 40%, transparent 60%); + background-position: calc(100% - 4px), 100%; + background-size: 4px 6px, 4px 6px; + background-repeat: no-repeat; + outline: none; + display: inline-block; + cursor: pointer; +} +select option { + color: var(--text-color); + background-color: var(--mild-background-color); +} +select:focus { + outline: 0; +} +select[disabled] { + color: var(--text-color); + border-bottom-color: var(--disabled-border-color); + opacity: 0.5; + cursor: default; +} +select[name="enter_view"] { + margin: 0; + padding: 0 12px 0 0; + border: none; + min-width: auto; +} +select[name="enter_share"] { + font-size: 1.1rem; + padding: 0; + border: none; + min-width: 40px; + float: right; + margin-top: 13px; + margin-right: 20px; +} +select[name="port_select"] { + border: none; + min-width: 54px; + padding-top: 0; + padding-bottom: 0; +} +select.narrow { + min-width: 76px; +} +select.auto { + min-width: auto; +} +select.slot { + min-width: 44rem !important; + max-width: 44rem !important; +} +input.narrow { + width: 166px; +} +input.trim { + width: 76px; + min-width: 76px; +} +textarea { + resize: none; + padding: 6px; + border: 1px solid var(--textarea-border-color); + border-bottom: 1px solid var(--input-border-color); +} +#header { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 91px; + z-index: 102; + margin: 0; + color: var(--inverse-text-color); + background-color: var(--header-color); + background-size: 100% 90px; + background-repeat: no-repeat; +} +#header .logo { + float: left; + margin-left: 10px; + color: var(--brand-red); + text-align: center; +} +#header .logo svg { + width: 160px; + display: block; + margin: 25px 0 8px 0; +} +#header .block { + margin: 0; + float: right; + text-align: right; + background-color: var(--gray-120); + padding: 10px 12px; +} +#header .text-left { + float: left; + text-align: right; + padding-right: 5px; + border-right: solid medium var(--orange-800); +} +#header .text-right { + float: right; + text-align: left; + padding-left: 5px; +} +#header .text-right a { + color: var(--inverse-text-color); +} +#header .text-right #licensetype { + font-weight: bold; + font-style: italic; + margin-right: 4px; +} +div.title { + margin: 20px 0 32px 0; + padding: 8px 10px; + clear: both; + border-bottom: 1px solid var(--table-border-color); + background-color: var(--title-header-background-color); + letter-spacing: 1.8px; +} +div.title span.left { + font-size: 1.4rem; +} +div.title span.right { + font-size: 1.4rem; + padding-top: 2px; + padding-right: 10px; + float: right; +} +div.title span img { + padding-right: 4px; +} +div.title.shift { + margin-top: -30px; +} +#menu { + position: absolute; + top: 90px; + left: 0; + right: 0; + display: grid; + grid-template-columns: auto max-content; + z-index: 101; +} +.nav-tile { + height: 4rem; + line-height: 4rem; + display: block; + padding: 0; + margin: 0; + font-size: 1.2rem; + letter-spacing: 1.8px; + background-color: var(--header-color); + white-space: nowrap; + overflow-x: auto; + overflow-y: hidden; + scrollbar-width: thin; +} +.nav-tile::-webkit-scrollbar { + height: 5px; +} +.nav-tile.right { + text-align: right; +} +.nav-item, +.nav-user { + position: relative; + display: inline-block; + text-align: center; + margin: 0; +} +.nav-item a { + min-width: 0; +} +.nav-item a span { + display: none; +} +.nav-item .system { + vertical-align: middle; + padding-bottom: 2px; +} +.nav-item a { + color: var(--inverse-text-color); + background-color: transparent; + text-transform: uppercase; + font-weight: bold; + display: block; + padding: 0 10px; +} +.nav-item a { + text-decoration: none; + text-decoration-skip-ink: auto; + -webkit-text-decoration-skip: objects; + -webkit-transition: all 0.25s ease-out; + transition: all 0.25s ease-out; +} +.nav-item:after, +.nav-user.show:after { + border-radius: 4px; + display: block; + background-color: transparent; + content: ""; + width: 32px; + height: 2px; + bottom: 8px; + position: absolute; + left: 50%; + margin-left: -16px; + -webkit-transition: all 0.25s ease-in-out; + transition: all 0.25s ease-in-out; + pointer-events: none; +} +.nav-item:focus:after, +.nav-item:hover:after, +.nav-user.show:hover:after { + background-color: var(--orange-800); +} +.nav-item.active:after { + background-color: var(--background-color); +} +.nav-user a { + color: var(--inverse-text-color); + background-color: transparent; + display: block; + padding: 0 10px; +} +.nav-user .system { + vertical-align: middle; + padding-bottom: 2px; +} +#clear { + clear: both; +} +#footer { + position: fixed; + bottom: 0; + left: 0; + color: var(--footer-text); + background-color: var(--footer-background-color); + padding: 5px 0; + width: 100%; + height: 1.6rem; + line-height: 1.6rem; + text-align: center; + z-index: 10000; +} +#statusraid { + float: left; + padding-left: 10px; +} +#countdown { + margin: 0 auto; +} +#copyright { + font-family: bitstream; + font-size: 1.1rem; + float: right; + padding-right: 10px; +} +.green { + color: var(--green-800); + padding-left: 5px; + padding-right: 5px; +} +.red { + color: var(--red-600); + padding-left: 5px; + padding-right: 5px; +} +.orange { + color: var(--orange-300); + padding-left: 5px; + padding-right: 5px; +} +.blue { + color: var(--blue-800); + padding-left: 5px; + padding-right: 5px; +} +.green-text, +.passed { + color: var(--green-800); +} +.red-text, +.failed { + color: var(--red-600); +} +.orange-text, +.warning { + color: var(--orange-300); +} +.blue-text { + color: var(--blue-800); +} +.grey-text { + color: var(--alt-text-color); +} +.green-orb { + color: var(--green-200); +} +.grey-orb { + color: var(--gray-300); +} +.blue-orb { + color: var(--blue-700); +} +.yellow-orb { + color: var(--orange-200); +} +.red-orb { + color: var(--red-500); +} +.usage-bar { + float: left; + height: 2rem; + line-height: 2rem; + width: 14rem; + padding: 1px 1px 1px 2px; + margin: 8px 12px; + border-radius: 3px; + background-color: var(--usage-bar-background-color); + box-shadow: 0 1px 0 var(--gray-400), inset 0 1px 0 var(--gray-700); +} +.usage-bar > span { + display: block; + height: 100%; + text-align: right; + border-radius: 2px; + color: var(--gray-100); + background-color: var(--usage-bar-used-background-color); + box-shadow: inset 0 1px 0 var(--white-opacity-50); +} +.usage-disk { + position: relative; + height: 1.8rem; + background-color: var(--usage-disk-background-color); + margin: 0; +} +.usage-disk > span:first-child { + position: absolute; + left: 0; + margin: 0 !important; + height: 1.8rem; + background-color: var(--gray-400); +} +.usage-disk > span:last-child { + position: relative; + top: -0.4rem; + right: 0; + padding-right: 6px; + z-index: 1; +} +.usage-disk.sys { + height: 12px; + margin: -1.4rem 20px 0 44px; +} +.usage-disk.sys > span { + height: 12px; + padding: 0; +} +.usage-disk.sys.none { + background-color: transparent; +} +.usage-disk.mm { + height: 3px; + margin: 5px 20px 0 0; +} +.usage-disk.mm > span:first-child { + height: 3px; +} +.notice { + background: url(../images/notice.png) no-repeat 30px 50%; + font-size: 1.5rem; + text-align: left; + vertical-align: middle; + padding-left: 100px; + height: 6rem; + line-height: 6rem; +} +.notice.shift { + margin-top: 160px; +} +.greenbar { + background: -webkit-gradient( + linear, + left top, + right top, + from(var(--green-900)), + to(var(--green-500)) + ); + background: linear-gradient(90deg, var(--green-900) 0, var(--green-500)); +} +.orangebar { + background: -webkit-gradient( + linear, + left top, + right top, + from(var(--orange-400)), + to(var(--orange-400)) + ); + background: linear-gradient(90deg, var(--orange-400) 0, var(--orange-400)); +} +.redbar { + background: -webkit-gradient( + linear, + left top, + right top, + from(var(--red-900)), + to(var(--red-700)) + ); + background: linear-gradient(90deg, var(--red-900) 0, var(--red-700)); +} +.graybar { + background: -webkit-gradient( + linear, + left top, + right top, + from(var(--gray-400)), + to(var(--gray-200)) + ); + background: linear-gradient(90deg, var(--gray-400) 0, var(--gray-200)); +} +table { + border-collapse: collapse; + border-spacing: 0; + border-style: hidden; + margin: -30px 0 0 0; + width: 100%; + background-color: var(--background-color); +} +table thead td { + line-height: 2.8rem; + height: 2.8rem; + white-space: nowrap; +} +table tbody td { + line-height: 2.6rem; + height: 2.6rem; + white-space: nowrap; +} +table tbody tr.alert { + color: var(--red-600); +} +table tbody tr.warn { + color: var(--orange-300); +} +table.unraid thead tr:first-child > td { + font-size: 1.1rem; + text-transform: uppercase; + letter-spacing: 1px; + background-color: var(--table-header-background-color); +} +table.unraid thead tr:last-child { + border-bottom: 1px solid var(--table-border-color); +} +table.unraid tbody tr:nth-child(even) { + background-color: var(--table-background-color); +} +table.unraid tbody tr:not(.tr_last):hover > td { + background-color: var(--hover-table-row-background-color); +} +table.unraid tr > td { + overflow: hidden; + text-overflow: ellipsis; + padding-left: 8px; +} +table.unraid tr > td:hover { + overflow: visible; +} +table.legacy { + table-layout: auto !important; +} +table.legacy thead td { + line-height: normal; + height: auto; + padding: 7px 0; +} +table.legacy tbody td { + line-height: normal; + height: auto; + padding: 5px 0; +} +table.disk_status { + table-layout: fixed; +} +table.disk_status tr > td:last-child { + padding-right: 8px; +} +table.disk_status tr > td:nth-child(1) { + width: 13%; +} +table.disk_status tr > td:nth-child(2) { + width: 30%; +} +table.disk_status tr > td:nth-child(3) { + width: 8%; + text-align: right; +} +table.disk_status tr > td:nth-child(n + 4) { + width: 7%; + text-align: right; +} +table.disk_status tr.offline > td:nth-child(2) { + width: 43%; +} +table.disk_status tr.offline > td:nth-child(n + 3) { + width: 5.5%; +} +table.disk_status tbody tr.tr_last { + line-height: 3rem; + height: 3rem; + background-color: var(--table-background-color); + border-top: 1px solid var(--table-border-color); +} +table.array_status { + table-layout: fixed; +} +table.array_status tr > td { + padding-left: 8px; + white-space: normal; +} +table.array_status tr > td:nth-child(1) { + width: 33%; +} +table.array_status tr > td:nth-child(2) { + width: 22%; +} +table.array_status.noshift { + margin-top: 0; +} +table.array_status td.line { + border-top: 1px solid var(--table-border-color); +} +table.share_status { + table-layout: fixed; +} +table.share_status tr > td { + padding-left: 8px; +} +table.share_status tr > td:nth-child(1) { + width: 15%; +} +table.share_status tr > td:nth-child(2) { + width: 30%; +} +table.share_status tr > td:nth-child(n + 3) { + width: 10%; +} +table.share_status tr > td:nth-child(5) { + width: 15%; +} +table.dashboard { + margin: 0; + border: none; + background-color: var(--mild-background-color); +} +table.dashboard tbody { + border: 1px solid var(--table-border-color); +} +table.dashboard tbody td { + line-height: normal; + height: auto; + padding: 3px 10px; +} +table.dashboard tr:first-child > td { + height: 3.6rem; + padding-top: 12px; + font-size: 1.6rem; + font-weight: bold; + letter-spacing: 1.8px; + text-transform: none; + vertical-align: top; +} +table.dashboard tr:nth-child(even) { + background-color: transparent; +} +table.dashboard tr:last-child > td { + padding-bottom: 20px; +} +table.dashboard tr.last > td { + padding-bottom: 20px; +} +table.dashboard tr.header > td { + padding-bottom: 10px; +} +table.dashboard td { + line-height: 2.4rem; + height: 2.4rem; +} +table.dashboard td.stopgap { + height: 20px !important; + line-height: 20px !important; + padding: 0 !important; + background-color: var(--background-color); +} +table.dashboard td.vpn { + font-size: 1.1rem; + font-weight: bold; + text-transform: uppercase; + letter-spacing: 1px; +} +table.dashboard td div.section { + display: inline-block; + vertical-align: top; + margin-left: 4px; + font-size: 1.2rem; + font-weight: bold; + text-transform: uppercase; + letter-spacing: 1px; +} +table.dashboard td div.section span { + font-weight: normal; + text-transform: none; + letter-spacing: 0; + white-space: normal; +} +table.dashboard td span.info { + float: right; + margin-right: 20px; + font-size: 1.2rem; + font-weight: normal; + text-transform: none; + letter-spacing: 0; +} +table.dashboard td span.info.title { + font-weight: bold; +} +table.dashboard td span.load { + display: inline-block; + width: 38px; + text-align: right; +} +table.dashboard td span.finish { + float: right; + margin-right: 24px; +} +table.dashboard i.control { + float: right; + font-size: 1.4rem !important; + margin: 0 3px 0 0; + cursor: pointer; + color: var(--dashboard-icon-color); + background-color: var(--dashboard-title-action-color); + padding: 2px; + border-radius: 5px; +} +[name="arrayOps"] { + margin-top: 12px; +} +span.error { + color: var(--red-600); + background-color: var(--red-300); + display: block; + width: 100%; +} +span.warn { + color: var(--orange-300); + background-color: var(--yellow-200); + display: block; + width: 100%; +} +span.system { + color: var(--blue-700); + background-color: var(--blue-300); + display: block; + width: 100%; +} +span.array { + color: var(--green-800); + background-color: var(--green-100); + display: block; + width: 100%; +} +span.login { + color: var(--orange-900); + background-color: var(--red-100); + display: block; + width: 100%; +} +span.lite { + background-color: var(--background-color); +} +span.label { + font-size: 1.2rem; + padding: 2px 0 2px 6px; + margin-right: 6px; + border-radius: 4px; + display: inline; + width: auto; + vertical-align: middle; +} +span.cpu-speed { + display: block; + color: var(--blue-900); +} +span.status { + float: right; + font-size: 1.4rem; + margin-top: 30px; + padding-right: 8px; + letter-spacing: 1.8px; +} +span.status.vhshift { + margin-top: 0; + margin-right: -9px; +} +span.status.vshift { + margin-top: -16px; +} +span.status.hshift { + margin-right: -20px; +} +span.diskinfo { + float: left; + clear: both; + margin-top: 5px; + padding-left: 10px; +} +span.bitstream { + font-family: bitstream; + font-size: 1.1rem; +} +span.ucfirst { + text-transform: capitalize; +} +span.strong { + font-weight: bold; +} +span.big { + font-size: 1.4rem; +} +span.small { + font-size: 1.2rem; +} +span.outer { + margin-bottom: 20px; + margin-right: 0; +} +span.outer.solid { + background-color: var(--mild-background-color); +} +span.hand { + cursor: pointer; +} +span.outer.started > img, +span.outer.started > i.img { + opacity: 1; +} +span.outer.stopped > img, +span.outer.stopped > i.img { + opacity: 0.3; +} +span.outer.paused > img, +span.outer.paused > i.img { + opacity: 0.6; +} +span.inner { + display: inline-block; + vertical-align: top; +} +span.state { + font-size: 1.1rem; + margin-left: 7px; +} +span.slots { + display: inline-block; + width: 44rem; + margin: 0 !important; +} +span.slots-left { + float: left; + margin: 0 !important; +} +input.subpool { + float: right; + margin: 2px 0 0 0; +} +i.padlock { + margin-right: 8px; + cursor: default; + vertical-align: middle; +} +i.nolock { + visibility: hidden; + margin-right: 8px; + vertical-align: middle; +} +i.lock { + margin-left: 8px; + cursor: default; + vertical-align: middle; +} +i.orb { + font-size: 1.1rem; + margin: 0 8px 0 3px; +} +img.img, +i.img { + width: 32px; + height: 32px; + margin-right: 10px; +} +img.icon { + margin: -3px 4px 0 0; +} +img.list { + width: auto; + max-width: 32px; + height: 32px; +} +i.list { + font-size: 32px; +} +a.list { + text-decoration: none; + color: inherit; +} +div.content { + position: absolute; + top: 0; + left: 0; + width: 100%; + padding-bottom: 30px; + z-index: -1; + clear: both; +} +div.content.shift { + margin-top: 1px; +} +label + .content { + margin-top: 86px; +} +div.tabs { + position: relative; + margin: 120px 0 0 0; +} +div.tab { + float: left; + margin-top: 30px; +} +div.tab input[id^="tab"] { + display: none; +} +div.tab [type="radio"] + label:hover { + background-color: transparent; + border: 1px solid var(--brand-orange); + border-bottom: none; + cursor: pointer; + opacity: 1; +} +div.tab [type="radio"]:checked + label { + cursor: default; + background-color: transparent; + border: 1px solid var(--brand-orange); + border-bottom: none; + opacity: 1; +} +div.tab [type="radio"] + label ~ .content { + display: none; +} +div.tab [type="radio"]:checked + label ~ .content { + display: inline; +} +div.tab [type="radio"] + label { + position: relative; + font-size: 1.4rem; + letter-spacing: 1.8px; + padding: 4px 10px; + margin-right: 2px; + border-top-left-radius: 6px; + border-top-right-radius: 6px; + border: 1px solid var(--disabled-input-border-color); + border-bottom: none; + background-color: var(--radio-background-color); + opacity: 0.5; +} +div.tab [type="radio"] + label img { + padding-right: 4px; +} +div.Panel { + text-align: center; + float: left; + margin: 0 0 30px 10px; + padding-right: 50px; + height: 8rem; +} +div.Panel a { + text-decoration: none; +} +div.Panel span { + height: 42px; + display: block; +} +div.Panel:hover .PanelText { + text-decoration: underline; +} +div.Panel img.PanelImg { + width: auto; + max-width: 32px; + height: 32px; +} +div.Panel i.PanelIcon { + font-size: 32px; + color: var(--text-color); +} +div.user-list { + float: left; + padding: 10px; + margin-right: 10px; + margin-bottom: 24px; + border: 1px solid var(--border-color); + border-radius: 5px; + line-height: 2rem; + height: 10rem; + width: 10rem; + background-color: var(--mild-background-color); +} +div.user-list img { + width: auto; + max-width: 48px; + height: 48px; + margin-bottom: 16px; +} +div.up { + margin-top: -30px; + border: 1px solid var(--border-color); + padding: 4px 6px; + overflow: auto; +} +div.spinner { + text-align: center; + cursor: wait; +} +div.spinner.fixed { + display: none; + position: fixed; + top: 0; + left: 0; + z-index: 99999; + bottom: 0; + right: 0; + margin: 0; +} +div.spinner .unraid_mark { + height: 64px; + position: fixed; + top: 50%; + left: 50%; + margin-top: -16px; + margin-left: -64px; +} +div.spinner .unraid_mark_2, +div .unraid_mark_4 { + animation: mark_2 1.5s ease infinite; +} +div.spinner .unraid_mark_3 { + animation: mark_3 1.5s ease infinite; +} +div.spinner .unraid_mark_6, +div .unraid_mark_8 { + animation: mark_6 1.5s ease infinite; +} +div.spinner .unraid_mark_7 { + animation: mark_7 1.5s ease infinite; +} +@keyframes mark_2 { + 50% { + transform: translateY(-40px); + } + 100% { + transform: translateY(0px); + } +} +@keyframes mark_3 { + 50% { + transform: translateY(-62px); + } + 100% { + transform: translateY(0px); + } +} +@keyframes mark_6 { + 50% { + transform: translateY(40px); + } + 100% { + transform: translateY(0px); + } +} +@keyframes mark_7 { + 50% { + transform: translateY(62px); + } + 100% { + transform: translateY(0px); + } +} +pre.up { + margin-top: -30px; +} +pre { + border: 1px solid var(--border-color); + font-family: bitstream; + font-size: 1.3rem; + line-height: 1.8rem; + padding: 4px 6px; + overflow: auto; +} +iframe#progressFrame { + position: fixed; + bottom: 32px; + left: 0; + margin: 0; + padding: 8px 8px 0 8px; + width: 100%; + height: 1.2rem; + line-height: 1.2rem; + border-style: none; + overflow: hidden; + font-family: bitstream; + font-size: 1.1rem; + color: var(--alt-text-color); + white-space: nowrap; + z-index: -10; +} +dl { + margin: 0; + padding-left: 12px; + line-height: 2.6rem; +} +dt { + width: 35%; + clear: left; + float: left; + font-weight: normal; + text-align: right; + margin-right: 4rem; +} +dd { + margin-bottom: 12px; + white-space: nowrap; +} +dd p { + margin: 0 0 4px 0; +} +dd blockquote { + padding-left: 0; +} +blockquote { + width: 90%; + margin: 10px auto; + text-align: left; + padding: 4px 20px; + border-top: 2px solid var(--blue-200); + border-bottom: 2px solid var(--blue-200); + color: var(--blockquote-text-color); + background-color: var(--blue-100); +} +blockquote.ontop { + margin-top: -20px; + margin-bottom: 46px; +} +blockquote a { + color: var(--brand-orange); + font-weight: 600; +} +blockquote a:hover, +blockquote a:focus { + color: var(--orange-800); +} +label.checkbox { + display: block; + position: relative; + padding-left: 28px; + margin: 3px 0; + cursor: pointer; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; +} +label.checkbox input { + position: absolute; + opacity: 0; + cursor: pointer; +} +span.checkmark { + position: absolute; + top: 0; + left: 6px; + height: 14px; + width: 14px; + background-color: var(--checkbox-color); + border-radius: 100%; +} +label.checkbox:hover input ~ .checkmark { + background-color: var(--checkbox-hover-color); +} +label.checkbox input:checked ~ .checkmark { + background-color: var(--brand-orange); +} +label.checkbox input:disabled ~ .checkmark { + opacity: 0.5; +} +a.bannerDismiss { + float: right; + cursor: pointer; + text-decoration: none; + margin-right: 1rem; +} +.bannerDismiss::before { + content: "\e92f"; + font-family: Unraid; + color: var(--orange-300); +} +a.bannerInfo { + cursor: pointer; + text-decoration: none; +} +.bannerInfo::before { + content: "\f05a"; + font-family: fontAwesome; + color: var(--orange-300); +} +::-webkit-scrollbar { + width: 8px; + height: 8px; + background: transparent; +} +::-webkit-scrollbar-thumb { + background: var(--scrollbar-color); + border-radius: 10px; +} +::-webkit-scrollbar-corner { + background: var(--scrollbar-color); + border-radius: 10px; +} +::-webkit-scrollbar-thumb:hover { + background: var(--scrollbar-hover-color); +} + +.inline_help { + display: none; +} +.upgrade_notice { + position: fixed; + top: 24px; + left: 50%; + margin-left: -480px; + width: 900px; + height: 38px; + line-height: 38px; + color: var(--orange-300); + background-color: var(--yellow-200); + border: 1px solid var(--orange-300); + border-radius: 38px; + text-align: center; + font-size: 1.4rem; + z-index: 999; +} +.upgrade_notice.done { + color: var(--green-800); + background-color: var(--green-100); + border-color: var(--green-800); +} +.upgrade_notice.alert { + color: var(--red-600); + background-color: var(--red-300); + border-color: var(--red-600); +} +.upgrade_notice i { + float: right; + cursor: pointer; +} +.back_to_top, +.move_to_end { + display: none; + position: fixed; + bottom: 24px; + color: var(--red-800); + font-size: 2.5rem; + z-index: 999; +} +.back_to_top { + right: 40px; +} +.move_to_end { + right:12px; +} +span.big.blue-text { + cursor: pointer; +} +span.strong.tour { + margin-left: 5px; + padding-left: 0; +} +i.abortOps { + font-size: 2rem; + float: right; + margin-right: 20px; + margin-top: 8px; + cursor: pointer; +} +pre#swalbody p { + margin-block-end: 1em; +} +span#wlan0 { + float: right; + margin-right: 10px; + cursor: pointer; +} + +.shade { + margin-top: 1rem; + padding: 1rem 0; + background-color: var(--shade-bg-color); +} + +/* + * Using CSS Nesting, to narrow down the scope of the styles to the .Theme--sidebar class. + * This allows us to have default-azure & default-gray set css variables + * + * @todo check version of included Firefox in Unraid OS gui + * + * @see https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_nesting + * @see https://developer.mozilla.org/en-US/docs/Web/CSS/Nesting_selector + * @see https://caniuse.com/?search=nesting + */ +.Theme--sidebar { + p { + text-align: left; + } + + i.spacing { + margin-left: 0; + margin-right: 10px; + } + + i.title { + display: none; + } + + i.favo { + margin-left: 0; + } + pre ul { + margin: 0; + padding-top: 0; + padding-bottom: 0; + padding-left: 28px; + } + pre li { + margin: 0; + padding-top: 0; + padding-bottom: 0; + padding-left: 18px; + } + big { + font-size: 1.4rem; + font-weight: bold; + text-transform: uppercase; + } + + input[type="text"], + input[type="password"], + input[type="number"], + input[type="url"], + input[type="email"], + input[type="date"], + input[type="file"], + textarea, + .textarea { + padding: 5px 6px; + border-width: 1px; + } + + input[type="button"], + input[type="reset"], + input[type="submit"], + button, + button[type="button"], + a.button, + .sweet-alert button { + font-family: clear-sans; + font-size: 1.2rem; + font-weight: normal; + text-transform: none; + letter-spacing: normal; + border: 1px solid var(--button-border); + color: var(--button-text-color); + background: none; + background-color: var(--button-background); + } + + input:focus[type="text"], + input:focus[type="password"], + input:focus[type="number"], + input:focus[type="url"], + input:focus[type="email"], + input:focus[type="file"], + textarea:focus, + .sweet-alert button:focus { + background: none; + background-color: var(--focus-input-background-color); + border-color: var(--focus-input-border-color); + } + + input:hover[type="button"], + input:hover[type="reset"], + input:hover[type="submit"], + button:hover, + button:hover[type="button"], + a.button:hover, + .sweet-alert button:hover { + border-color: var(--hover-button-border); + color: var(--hover-button-text-color); + background: none; + background-color: var(--hover-button-background) !important; + } + + input:active[type="button"], + input:active[type="reset"], + input:active[type="submit"], + button:active, + button:active[type="button"], + a.button:active, + .sweet-alert button:active { + border-color: var(--hover-button-border); + box-shadow: none; + } + + input[disabled], + button[disabled], + input:hover[type="button"][disabled], + input:hover[type="reset"][disabled], + input:hover[type="submit"][disabled], + button:hover[disabled], + button:hover[type="button"][disabled], + input:active[type="button"][disabled], + input:active[type="reset"][disabled], + input:active[type="submit"][disabled], + button:active[disabled], + button:active[type="button"][disabled], + textarea[disabled], + .sweet-alert button[disabled] { + color: var(--disabled-text-color) !important; + border-color: var(--disabled-input-border-color) !important; + background: none !important; + background-size: 0 !important; + background-color: var(--disabled-input-background-color) !important; + } + + input::-webkit-input-placeholder { + color: var(--blue-700); + } + + select { + min-width: 188px; + max-width: 314px; + padding: 6px 14px 6px 6px; + border: 1px solid var(--border-color); + color: var(--text-color); + background-image: linear-gradient(66.6deg, transparent 60%, var(--border-color) 40%), + linear-gradient(113.4deg, var(--border-color) 40%, transparent 60%); + background-position: calc(100% - 8px), calc(100% - 4px); + background-size: + 4px 6px, + 4px 6px; + } + + select option { + color: var(--text-color); + background-color: var(--opac-background-color); + } + + select[disabled] { + color: var(--disabled-text-color); + border-color: var(--disabled-input-border-color); + background-color: var(--disabled-input-background-color); + } + + select[name="enter_view"] { + font-size: 1.2rem; + } + + select[name="enter_share"] { + color: var(--gray-500); + min-width: 40px; + margin-top: 0; + padding: 0; + border: none; + } + + select.narrow { + min-width: 87px; + } + + input.narrow { + width: 174px; + } + input.trim { + width: 74px; + min-width: 74px; + } + + #header { + position: fixed; + height: 90px; + z-index: 100; + background-color: var(--mild-background-color); + border-bottom: 1px solid var(--gray-600); + box-sizing: border-box; + padding-left: 80px; + } + + #header .logo { + margin-left: 0; + color: var(--brand-red); + } + + #header .block { + background-color: var(--gray-120); + } + + #header .text-left { + border-right: solid medium var(--orange-800); + } + + #header .text-right a { + color: var(--text-color); + } + + #menu { + position: fixed; + top: 0; + left: 0; + bottom: 12px; + width: 64px; + padding: 0; + margin: 0; + background-color: var(--alt-background-color); + z-index: 2000; + box-shadow: inset -1px 0 2px var(--gray-900); + } + #nav-block { + position: absolute; + top: 0; + bottom: 12px; + color: var(--orange-100); + white-space: nowrap; + float: left; + overflow-y: scroll; + direction: rtl; + scrollbar-width: none; + -ms-overflow-style: none; + overflow: -moz-scrollbars-none; + } + + #nav-block::-webkit-scrollbar { + display: none; + } + #nav-block > div { + direction: ltr; + } + .nav-tile { + height: auto; + line-height: 1; + display: block; + padding: 0; + margin: 0; + font-size: 1.2rem; + letter-spacing: 1.8px; + background-color: transparent; + white-space: nowrap; + overflow-x: auto; + overflow-y: hidden; + scrollbar-width: thin; + } + .nav-item { + display: block; + width: 64px; + text-align: left; + padding: 0; + border-bottom: 1px solid var(--gray-600); + font-size: 18px !important; + overflow: hidden; + transition: 0.2s background-color ease; + } + + .nav-item::after, + .nav-user.show::after { + width: 0; + height: 0; + } + + .nav-item:hover { + width: auto; + padding-right: 0; + color: var(--orange-100); + background: -webkit-gradient( + linear, + left top, + right top, + from(var(--red-800)), + to(var(--brand-orange)) + ); + background: linear-gradient(90deg, var(--red-800) 0, var(--brand-orange)); + -webkit-transition: all 0.2s ease-in-out; + transition: all 0.2s ease-in-out; + border-bottom-color: var(--red-800); + } + .nav-item:hover a { + color: var(--orange-100); + background: -webkit-gradient( + linear, + left top, + right top, + from(var(--red-800)), + to(var(--brand-orange)) + ); + background: linear-gradient(90deg, var(--red-800) 0, var(--brand-orange)); + border-bottom-color: var(--red-800); + font-size: 18px; + } + .nav-item img { + display: none; + } + .nav-item a { + display: inline-flex; + color: var(--gray-400); + text-decoration: none; + padding: 16px 18px; + gap: 25px; + justify-content: start; + align-items: center; + text-transform: none; + font-weight: normal; + } + .nav-item.util a { + padding-left: 24px; + } + .nav-item a:before { + font-family: docker-icon, fontawesome, unraid; + font-size: 26px; + /* margin-right: 25px; */ + } + .nav-item.util a:before { + font-size: 16px; + } + .nav-item.active, + .nav-item.active a { + color: var(--orange-100); + background: -webkit-gradient( + linear, + left top, + right top, + from(var(--red-800)), + to(var(--brand-orange)) + ); + background: linear-gradient(90deg, var(--red-800) 0, var(--brand-orange)); + } + .nav-item.HelpButton.active:hover, + .nav-item.HelpButton.active a:hover { + background: -webkit-gradient( + linear, + left top, + right top, + from(var(--red-800)), + to(var(--brand-orange)) + ); + background: linear-gradient(90deg, var(--red-800) 0, var(--brand-orange)); + font-size: 18px; + } + .nav-item.HelpButton.active, + .nav-item.HelpButton.active a { + font-size: 18px; + } + + .nav-item a span { + display: inline; + } + + .nav-item a b { + display: none; + } + + .nav-user { + position: fixed; + top: 102px; + right: 10px; + } + + .nav-user a { + color: var(--text-color); + padding: 0; + } + + .LanguageButton { + font-size: 12px !important; + } /* Fix Switch Language Being Cut-Off */ + + div.title { + color: var(--text-color); + margin: 20px 0 10px 0; + padding: 10px 0; + clear: both; + background-color: var(--background-color); + border-bottom: 1px solid var(--border-color); + letter-spacing: 1.8px; + } + div.title span.left { + font-size: 1.6rem; + text-transform: uppercase; + } + div.title span.right { + font-size: 1.6rem; + padding-right: 10px; + float: right; + } + div.title span img, + .title p { + display: none; + } + div.title:first-child { + margin-top: 0; + } + div.title.shift { + margin-top: -12px; + } + + .usage-bar { + float: unset; + position: fixed; + top: 64px; + left: 300px; + height: 2.2rem; + line-height: 2.2rem; + width: 11rem; + margin: 0; + padding: 0; + } + + .usage-disk { + height: 2.2rem; + line-height: 2.2rem; + } + + .usage-disk > span:first-child { + height: 3px; + background-color: var(--border-color); + } + + .usage-disk > span:last-child { + padding-right: 4px; + top: 0; + z-index: 1000; + } + + .usage-disk.sys { + line-height: normal; + background-color: transparent; + margin: -15px 20px 0 44px; + } + + .usage-disk.sys > span { + line-height: normal; + } + + .usage-disk.mm { + line-height: normal; + background-color: transparent; + } + + .usage-disk.mm > span:first-child { + line-height: normal; + } + + table { + margin: 0; + background-color: transparent; + } + + table tbody tr.tr_last { + border-bottom: 1px solid var(--border-color); + } + + table.unraid tbody tr:nth-child(even) { + background-color: var(--table-background-color); + } + + table.unraid thead tr:first-child > td { + font-size: 1.2rem; + color: var(--gray-500); + border-bottom: 1px solid var(--border-color); + background-color: transparent; + } + + table.unraid tbody tr:not(.tr_last):hover > td { + /* background-color: var(--black-opacity-05); */ + background-color: var(--hover-table-row-background-color); + } + + table.disk_status tbody tr { + border-bottom: 1px solid var(--table-alt-border-color); + } + + table.array_status td.line { + border-top: 1px solid var(--table-alt-border-color); + } + + table.share_status { + margin-top: 12px; + } + + table.dashboard { + background-color: var(--dashboard-background-color); + } + + table.dashboard tbody { + border: 1px solid var(--dashboard-border-color); + } + + table.dashboard tr { + border: none; + } + + table.dashboard td { + line-height: normal; + height: auto; + padding: 3px 10px; + border: none !important; + } + + tr.alert { + background-color: var(--red-300); + } + tr.warn { + background-color: var(--yellow-200); + } + tr.past { + color: var(--orange-900); + background-color: var(--red-100); + } + + span.label { + font-size: 1.1rem; + } + + span.status { + margin-top: 0; + padding-right: 0; + } + + span.status.vhshift { + margin-right: 8px; + } + + span.p0 { + padding-left: 0; + } + + span.small { + font-size: 1.1rem; + } + + span.outer.solid { + background-color: var(--dashboard-background-color); + } + + label + .content { + margin-top: 64px; + } + + div.tabs { + margin: 110px 20px 30px 90px; + background-color: var(--background-color); + } + + div.tab { + margin-top: 23px; + } + + div.tab [type="radio"] + label { + padding: 10px; + border-top-left-radius: 12px; + border-top-right-radius: 12px; + background-color: var(--border-color); + color: var(--gray-300); + border: 1px solid var(--gray-500); + border-bottom: none; + } + + div.tab [type="radio"] + label:hover { + border-color: var(--alt-border-color); + } + + div.tab [type="radio"]:checked + label { + cursor: default; + background-color: transparent; + border-bottom: none; + border-color: var(--alt-border-color); + opacity: 1; + color: var(--text-color); + } + + div.tab [type="radio"] + label img { + display: none; + } + + div.Panel { + width: 25%; + height: auto; + margin: 0; + padding: 5px; + border-right: var(--table-alt-border-color) 1px solid; + border-bottom: 1px solid var(--table-alt-border-color); + box-sizing: border-box; + } + + div.Panel:hover { + background-color: var(--mild-background-color); + } + + div.Panel a { + display: flex; + justify-content: start; + align-items: center; + gap: 32px; + } + + div.Panel span { + height: auto; + } + + div.Panel br, + .vmtemplate br { + display: none; + } + + div.Panel img.PanelImg { + float: left; + margin: 10px; + } + + div.Panel i.PanelIcon { + float: left; + color: var(--text-color); + margin: 10px; + } + + div.Panel .PanelText { + font-size: 1.4rem; + padding-top: 0; + text-align: center; + } + + div.user-list { + background-color: transparent; + border: 1px solid var(--table-alt-border-color); + } + + div.user-list:hover { + background-color: var(--opac-background-color); + } + div.vmheader { + display: block; + clear: both; + } + div.vmtemplate { + height: 12rem; + width: 12rem; + border: 1px solid var(--table-alt-border-color); + } + div.vmtemplate:hover { + background-color: var(--opac-background-color); + } + div.vmtemplate img { + margin-top: 20px; + } + + div.up { + margin-top: -20px; + border: 1px solid var(--table-alt-border-color); + } + + pre.up { + margin-top: 0; + } + + pre { + border: 1px solid var(--table-alt-border-color); + margin-bottom: 10px; + padding: 10px; + } + + iframe#progressFrame { + left: 60px; + color: var(--gray-500); + z-index: -2; + } + + blockquote { + border: 1px solid var(--blue-200); + color: var(--blockquote-text-color); + background-color: var(--blue-100); + box-sizing: border-box; + } + + blockquote.ontop { + margin-top: 0; + } + + span.checkmark { + background-color: var(--gray-200); + } + + label.checkbox:hover input ~ .checkmark { + background-color: var(--gray-400); + } + label.checkbox input:checked ~ .checkmark { + background-color: var(--brand-orange); + } +} diff --git a/api/src/unraid-api/unraid-file-modifier/modifications/__test__/__fixtures__/downloaded/default-black.css b/api/src/unraid-api/unraid-file-modifier/modifications/__test__/__fixtures__/downloaded/default-black.css new file mode 100644 index 0000000000..fbf0f17062 --- /dev/null +++ b/api/src/unraid-api/unraid-file-modifier/modifications/__test__/__fixtures__/downloaded/default-black.css @@ -0,0 +1,262 @@ +html{font-family:clear-sans,sans-serif;font-size:62.5%;height:100%} +body{font-size:1.3rem;color:#f2f2f2;background-color:#1c1b1b;padding:0;margin:0;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale} +img{border:none;text-decoration:none;vertical-align:middle} +p{text-align:justify} +p.centered{text-align:left} +p:empty{display:none} +a:hover{text-decoration:underline} +a{color:#486dba;text-decoration:none} +a.none{color:#f2f2f2} +a.img{text-decoration:none;border:none} +a.info{position:relative} +a.info span{display:none;white-space:nowrap;font-variant:small-caps;position:absolute;top:16px;left:12px;line-height:2rem;color:#f2f2f2;padding:5px 8px;border:1px solid rgba(255,255,255,0.25);border-radius:3px;background-color:rgba(25,25,25,0.95);box-shadow:0 0 3px #303030} +a.info:hover span{display:block;z-index:1} +a.nohand{cursor:default} +a.hand{cursor:pointer;text-decoration:none} +a.static{cursor:default;color:#606060;text-decoration:none} +a.view{display:inline-block;width:20px} +i.spacing{margin-left:-6px} +i.icon{font-size:1.6rem;margin-right:4px;vertical-align:middle} +i.title{margin-right:8px} +i.control{cursor:pointer;color:#606060;font-size:1.8rem} +i.favo{display:none;font-size:1.8rem;position:absolute;margin-left:12px} +hr{border:none;height:1px!important;color:#2b2b2b;background-color:#2b2b2b} +input[type=text],input[type=password],input[type=number],input[type=url],input[type=email],input[type=date],input[type=file],textarea,.textarea{font-family:clear-sans;font-size:1.3rem;background-color:transparent;border:none;border-bottom:1px solid #e5e5e5;padding:4px 0;text-indent:0;min-height:2rem;line-height:2rem;outline:none;width:300px;margin:0 20px 0 0;box-shadow:none;border-radius:0;color:#f2f2f2} +input[type=button],input[type=reset],input[type=submit],button,button[type=button],a.button,.sweet-alert button{font-family:clear-sans;font-size:1.1rem;font-weight:bold;letter-spacing:1.8px;text-transform:uppercase;min-width:86px;margin:10px 12px 10px 0;padding:8px;text-align:center;text-decoration:none;white-space:nowrap;cursor:pointer;outline:none;border-radius:4px;border:none;color:#ff8c2f;background:-webkit-gradient(linear,left top,right top,from(#e22828),to(#ff8c2f)) 0 0 no-repeat,-webkit-gradient(linear,left top,right top,from(#e22828),to(#ff8c2f)) 0 100% no-repeat,-webkit-gradient(linear,left bottom,left top,from(#e22828),to(#e22828)) 0 100% no-repeat,-webkit-gradient(linear,left bottom,left top,from(#ff8c2f),to(#ff8c2f)) 100% 100% no-repeat;background:linear-gradient(90deg,#e22828 0,#ff8c2f) 0 0 no-repeat,linear-gradient(90deg,#e22828 0,#ff8c2f) 0 100% no-repeat,linear-gradient(0deg,#e22828 0,#e22828) 0 100% no-repeat,linear-gradient(0deg,#ff8c2f 0,#ff8c2f) 100% 100% no-repeat;background-size:100% 2px,100% 2px,2px 100%,2px 100%} +input[type=checkbox]{vertical-align:middle;margin-right:6px} +input[type=number]::-webkit-outer-spin-button,input[type=number]::-webkit-inner-spin-button{-webkit-appearance: none} +input[type=number]{-moz-appearance:textfield} +input:focus[type=text],input:focus[type=password],input:focus[type=number],input:focus[type=url],input:focus[type=email],input:focus[type=file],textarea:focus,.sweet-alert button:focus{background-color:#262626;outline:0} +input:hover[type=button],input:hover[type=reset],input:hover[type=submit],button:hover,button:hover[type=button],a.button:hover,.sweet-alert button:hover{color:#f2f2f2;background:-webkit-gradient(linear,left top,right top,from(#e22828),to(#ff8c2f));background:linear-gradient(90deg,#e22828 0,#ff8c2f)} +input[disabled],textarea[disabled]{color:#f2f2f2;border-bottom-color:#6c6c6c;opacity:0.5;cursor:default} +input[type=button][disabled],input[type=reset][disabled],input[type=submit][disabled],button[disabled],button[type=button][disabled],a.button[disabled] +input:hover[type=button][disabled],input:hover[type=reset][disabled],input:hover[type=submit][disabled],button:hover[disabled],button:hover[type=button][disabled],a.button:hover[disabled] +input:active[type=button][disabled],input:active[type=reset][disabled],input:active[type=submit][disabled],button:active[disabled],button:active[type=button][disabled],a.button:active[disabled],.sweet-alert button[disabled]{opacity:0.5;cursor:default;color:#808080;background:-webkit-gradient(linear,left top,right top,from(#404040),to(#808080)) 0 0 no-repeat,-webkit-gradient(linear,left top,right top,from(#404040),to(#808080)) 0 100% no-repeat,-webkit-gradient(linear,left bottom,left top,from(#404040),to(#404040)) 0 100% no-repeat,-webkit-gradient(linear,left bottom,left top,from(#808080),to(#808080)) 100% 100% no-repeat;background:linear-gradient(90deg,#404040 0,#808080) 0 0 no-repeat,linear-gradient(90deg,#404040 0,#808080) 0 100% no-repeat,linear-gradient(0deg,#404040 0,#404040) 0 100% no-repeat,linear-gradient(0deg,#808080 0,#808080) 100% 100% no-repeat;background-size:100% 2px,100% 2px,2px 100%,2px 100%} +input::-webkit-input-placeholder{color:#486dba} +select{-webkit-appearance:none;font-family:clear-sans;font-size:1.3rem;min-width:166px;max-width:300px;padding:5px 8px 5px 0;text-indent:0;margin:0 10px 0 0;border:none;border-bottom:1px solid #e5e5e5;box-shadow:none;border-radius:0;color:#f2f2f2;background-color:transparent;background-image:linear-gradient(66.6deg, transparent 60%, #f2f2f2 40%),linear-gradient(113.4deg, #f2f2f2 40%, transparent 60%);background-position:calc(100% - 4px),100%;background-size:4px 6px,4px 6px;background-repeat:no-repeat;outline:none;display:inline-block;cursor:pointer} +select option{color:#f2f2f2;background-color:#262626} +select:focus{outline:0} +select[disabled]{color:#f2f2f2;border-bottom-color:#6c6c6c;opacity:0.5;cursor:default} +select[name=enter_view]{margin:0;padding:0 12px 0 0;border:none;min-width:auto} +select[name=enter_share]{font-size:1.1rem;padding:0;border:none;min-width:40px;float:right;margin-top:13px;margin-right:20px} +select[name=port_select]{border:none;min-width:54px;padding-top:0;padding-bottom:0} +select.narrow{min-width:76px} +select.auto{min-width:auto} +select.slot{min-width:44rem;max-width:44rem} +input.narrow{width:166px} +input.trim{width:76px;min-width:76px} +textarea{resize:none} +#header{position:absolute;top:0;left:0;width:100%;height:91px;z-index:102;margin:0;color:#1c1b1b;background-color:#f2f2f2;background-size:100% 90px;background-repeat:no-repeat} +#header .logo{float:left;margin-left:10px;color:#e22828;text-align:center} +#header .logo svg{width:160px;display:block;margin:25px 0 8px 0} +#header .block{margin:0;float:right;text-align:right;background-color:rgba(242,242,242,0.2);padding:10px 12px} +#header .text-left{float:left;text-align:right;padding-right:5px;border-right:solid medium #f15a2c} +#header .text-right{float:right;text-align:left;padding-left:5px} +#header .text-right a{color:#1c1b1b} +#header .text-right #licensetype{font-weight:bold;font-style:italic;margin-right:4px} +div.title{margin:20px 0 32px 0;padding:8px 10px;clear:both;border-bottom:1px solid #2b2b2b;background-color:#262626;letter-spacing:1.8px} +div.title span.left{font-size:1.4rem} +div.title span.right{font-size:1.4rem;padding-top:2px;padding-right:10px;float:right} +div.title span img{padding-right:4px} +div.title.shift{margin-top:-30px} +#menu{position:absolute;top:90px;left:0;right:0;display:grid;grid-template-columns:auto max-content;z-index:101} +.nav-tile{height:4rem;line-height:4rem;padding:0;margin:0;font-size:1.2rem;letter-spacing:1.8px;background-color:#f2f2f2;white-space:nowrap;overflow-x:auto;overflow-y:hidden;scrollbar-width:thin} +.nav-tile::-webkit-scrollbar{height:5px} +.nav-tile.right{text-align:right} +.nav-item,.nav-user{position:relative;display:inline-block;text-align:center;margin:0} +.nav-item a{min-width:0} +.nav-item a span{display:none} +.nav-item .system{vertical-align:middle;padding-bottom:2px} +.nav-item a{color:#1c1b1b;background-color:transparent;text-transform:uppercase;font-weight:bold;display:block;padding:0 10px} +.nav-item a{text-decoration:none;text-decoration-skip-ink:auto;-webkit-text-decoration-skip:objects;-webkit-transition:all .25s ease-out;transition:all .25s ease-out} +.nav-item:after,.nav-user.show:after{border-radius:4px;display:block;background-color:transparent;content:"";width:32px;height:2px;bottom:8px;position:absolute;left:50%;margin-left:-16px;-webkit-transition:all .25s ease-in-out;transition:all .25s ease-in-out;pointer-events:none} +.nav-item:focus:after,.nav-item:hover:after,.nav-user.show:hover:after{background-color:#f15a2c} +.nav-item.active:after{background-color:#1c1b1b} +.nav-user a{color:#1c1b1b;background-color:transparent;display:block;padding:0 10px} +.nav-user .system{vertical-align:middle;padding-bottom:2px} +#clear{clear:both} +#footer{position:fixed;bottom:0;left:0;color:#d4d5d6;background-color:#2b2a29;padding:5px 0;width:100%;height:1.6rem;line-height:1.6rem;text-align:center;z-index:10000} +#statusraid{float:left;padding-left:10px} +#countdown{margin:0 auto} +#copyright{font-family:bitstream;font-size:1.1rem;float:right;padding-right:10px} +.green{color:#4f8a10;padding-left:5px;padding-right:5px} +.red{color:#f0000c;padding-left:5px;padding-right:5px} +.orange{color:#e68a00;padding-left:5px;padding-right:5px} +.blue{color:#486dba;padding-left:5px;padding-right:5px} +.green-text,.passed{color:#4f8a10} +.red-text,.failed{color:#f0000c} +.orange-text,.warning{color:#e68a00} +.blue-text{color:#486dba} +.grey-text{color:#606060} +.green-orb{color:#33cc33} +.grey-orb{color:#c0c0c0} +.blue-orb{color:#0099ff} +.yellow-orb{color:#ff9900} +.red-orb{color:#ff3300} +.usage-bar{float:left;height:2rem;line-height:2rem;width:14rem;padding:1px 1px 1px 2px;margin:8px 12px;border-radius:3px;background-color:#585858;box-shadow:0 1px 0 #989898,inset 0 1px 0 #202020} +.usage-bar>span{display:block;height:100%;text-align:right;border-radius:2px;color:#f2f2f2;background-color:#808080;box-shadow:inset 0 1px 0 rgba(255,255,255,.5)} +.usage-disk{position:relative;height:1.8rem;background-color:#444444;margin:0} +.usage-disk>span:first-child{position:absolute;left:0;margin:0!important;height:1.8rem;background-color:#787878} +.usage-disk>span:last-child{position:relative;top:-0.4rem;right:0;padding-right:6px;z-index:1} +.usage-disk.sys{height:12px;margin:-1.4rem 20px 0 44px} +.usage-disk.sys>span{height:12px;padding:0} +.usage-disk.sys.none{background-color:transparent} +.usage-disk.mm{height:3px;margin:5px 20px 0 0} +.usage-disk.mm>span:first-child{height:3px} +.notice{background:url(../images/notice.png) no-repeat 30px 50%;font-size:1.5rem;text-align:left;vertical-align:middle;padding-left:100px;height:6rem;line-height:6rem} +.notice.shift{margin-top:160px} +.greenbar{background:-webkit-gradient(linear,left top,right top,from(#127a05),to(#17bf0b));background:linear-gradient(90deg,#127a05 0,#17bf0b)} +.orangebar{background:-webkit-gradient(linear,left top,right top,from(#ce7c10),to(#ce7c10));background:linear-gradient(90deg,#ce7c10 0,#ce7c10)} +.redbar{background:-webkit-gradient(linear,left top,right top,from(#941c00),to(#de1100));background:linear-gradient(90deg,#941c00 0,#de1100)} +.graybar{background:-webkit-gradient(linear,left top,right top,from(#949494),to(#d9d9d9));background:linear-gradient(90deg,#949494 0,#d9d9d9)} +table{border-collapse:collapse;border-spacing:0;border-style:hidden;margin:-30px 0 0 0;width:100%;background-color:#191818} +table thead td{line-height:2.8rem;height:2.8rem;white-space:nowrap} +table tbody td{line-height:2.6rem;height:2.6rem;white-space:nowrap} +table tbody tr.alert{color:#f0000c} +table tbody tr.warn{color:#e68a00} +table.unraid thead tr:first-child>td{font-size:1.1rem;text-transform:uppercase;letter-spacing:1px;background-color:#262626} +table.unraid thead tr:last-child{border-bottom:1px solid #2b2b2b} +table.unraid tbody tr:nth-child(even){background-color:#212121} +table.unraid tbody tr:not(.tr_last):hover>td{background-color:rgba(255,255,255,0.1)} +table.unraid tr>td{overflow:hidden;text-overflow:ellipsis;padding-left:8px} +table.unraid tr>td:hover{overflow:visible} +table.legacy{table-layout:auto!important} +table.legacy thead td{line-height:normal;height:auto;padding:7px 0} +table.legacy tbody td{line-height:normal;height:auto;padding:5px 0} +table.disk_status{table-layout:fixed} +table.disk_status tr>td:last-child{padding-right:8px} +table.disk_status tr>td:nth-child(1){width:13%} +table.disk_status tr>td:nth-child(2){width:30%} +table.disk_status tr>td:nth-child(3){width:8%;text-align:right} +table.disk_status tr>td:nth-child(n+4){width:7%;text-align:right} +table.disk_status tr.offline>td:nth-child(2){width:43%} +table.disk_status tr.offline>td:nth-child(n+3){width:5.5%} +table.disk_status tbody tr.tr_last{line-height:3rem;height:3rem;background-color:#212121;border-top:1px solid #2b2b2b} +table.array_status{table-layout:fixed} +table.array_status tr>td{padding-left:8px;white-space:normal} +table.array_status tr>td:nth-child(1){width:33%} +table.array_status tr>td:nth-child(2){width:22%} +table.array_status.noshift{margin-top:0} +table.array_status td.line{border-top:1px solid #2b2b2b} +table.share_status{table-layout:fixed} +table.share_status tr>td{padding-left:8px} +table.share_status tr>td:nth-child(1){width:15%} +table.share_status tr>td:nth-child(2){width:30%} +table.share_status tr>td:nth-child(n+3){width:10%} +table.share_status tr>td:nth-child(5){width:15%} +table.dashboard{margin:0;border:none;background-color:#262626} +table.dashboard tbody{border:1px solid #333333} +table.dashboard tbody td{line-height:normal;height:auto;padding:3px 10px} +table.dashboard tr:first-child>td{height:3.6rem;padding-top:12px;font-size:1.6rem;font-weight:bold;letter-spacing:1.8px;text-transform:none;vertical-align:top} +table.dashboard tr:nth-child(even){background-color:transparent} +table.dashboard tr:last-child>td{padding-bottom:20px} +table.dashboard tr.last>td{padding-bottom:20px} +table.dashboard tr.header>td{padding-bottom:10px} +table.dashboard td{line-height:2.4rem;height:2.4rem} +table.dashboard td.stopgap{height:20px!important;line-height:20px!important;padding:0!important;background-color:#1c1b1b} +table.dashboard td.vpn{font-size:1.1rem;font-weight:bold;text-transform:uppercase;letter-spacing:1px} +table.dashboard td div.section{display:inline-block;vertical-align:top;margin-left:4px;font-size:1.2rem;font-weight:bold;text-transform:uppercase;letter-spacing:1px} +table.dashboard td div.section span{font-weight:normal;text-transform:none;letter-spacing:0;white-space:normal} +table.dashboard td span.info{float:right;margin-right:20px;font-size:1.2rem;font-weight:normal;text-transform:none;letter-spacing:0} +table.dashboard td span.info.title{font-weight:bold} +table.dashboard td span.load{display:inline-block;width:38px;text-align:right} +table.dashboard td span.finish{float:right;margin-right:24px} +table.dashboard i.control{float:right;font-size:1.4rem!important;margin:0 3px 0 0;cursor:pointer;color:#262626;background-color:rgba(255,255,255,0.3);padding:2px;border-radius:5px} +[name=arrayOps]{margin-top:12px} +span.error{color:#f0000c;background-color:#ff9e9e;display:block;width:100%} +span.warn{color:#e68a00;background-color:#feefb3;display:block;width:100%} +span.system{color:#0099ff;background-color:#bde5f8;display:block;width:100%} +span.array{color:#4f8a10;background-color:#dff2bf;display:block;width:100%} +span.login{color:#d63301;background-color:#ffddd1;display:block;width:100%} +span.lite{background-color:#212121} +span.label{font-size:1.2rem;padding:2px 0 2px 6px;margin-right:6px;border-radius:4px;display:inline;width:auto;vertical-align:middle} +span.cpu-speed{display:block;color:#3b5998} +span.status{float:right;font-size:1.4rem;margin-top:30px;padding-right:8px;letter-spacing:1.8px} +span.status.vhshift{margin-top:0;margin-right:-9px} +span.status.vshift{margin-top:-16px} +span.status.hshift{margin-right:-20px} +span.diskinfo{float:left;clear:both;margin-top:5px;padding-left:10px} +span.bitstream{font-family:bitstream;font-size:1.1rem} +span.ucfirst{text-transform:capitalize} +span.strong{font-weight:bold} +span.big{font-size:1.4rem} +span.small{font-size:1.2rem} +span.outer{margin-bottom:20px;margin-right:0} +span.outer.solid{background-color:#262626} +span.hand{cursor:pointer} +span.outer.started>img,span.outer.started>i.img{opacity:1.0} +span.outer.stopped>img,span.outer.stopped>i.img{opacity:0.3} +span.outer.paused>img,span.outer.paused>i.img{opacity:0.6} +span.inner{display:inline-block;vertical-align:top} +span.state{font-size:1.1rem;margin-left:7px} +span.slots{display:inline-block;width:44rem;margin:0!important} +span.slots-left{float:left;margin:0!important} +input.subpool{float:right;margin:2px 0 0 0} +i.padlock{margin-right:8px;cursor:default;vertical-align:middle} +i.nolock{visibility:hidden;margin-right:8px;vertical-align:middle} +i.lock{margin-left:8px;cursor:default;vertical-align:middle} +i.orb{font-size:1.1rem;margin:0 8px 0 3px} +img.img,i.img{width:32px;height:32px;margin-right:10px} +img.icon{margin:-3px 4px 0 0} +img.list{width:auto;max-width:32px;height:32px} +i.list{font-size:32px} +a.list{text-decoration:none;color:inherit} +div.content{position:absolute;top:0;left:0;width:100%;padding-bottom:30px;z-index:-1;clear:both} +div.content.shift{margin-top:1px} +label+.content{margin-top:86px} +div.tabs{position:relative;margin:130px 0 0 0} +div.tab{float:left;margin-top:30px} +div.tab input[id^="tab"]{display:none} +div.tab [type=radio]+label:hover{background-color:transparent;border:1px solid #ff8c2f;border-bottom:none;cursor:pointer;opacity:1} +div.tab [type=radio]:checked+label{cursor:default;background-color:transparent;border:1px solid #ff8c2f;border-bottom:none;opacity:1} +div.tab [type=radio]+label~.content{display:none} +div.tab [type=radio]:checked+label~.content{display:inline} +div.tab [type=radio]+label{position:relative;font-size:1.4rem;letter-spacing:1.8px;padding:4px 10px;margin-right:2px;border-top-left-radius:6px;border-top-right-radius:6px;border:1px solid #6c6c6c;border-bottom:none;background-color:#3c3c3c;opacity:0.5} +div.tab [type=radio]+label img{padding-right:4px} +div.Panel{text-align:center;float:left;margin:0 0 30px 10px;padding-right:50px;height:8rem} +div.Panel a{text-decoration:none} +div.Panel span{height:42px;display:block} +div.Panel:hover .PanelText{text-decoration:underline} +div.Panel img.PanelImg{width:auto;max-width:32px;height:32px} +div.Panel i.PanelIcon{font-size:32px;color:#f2f2f2} +div.user-list{float:left;padding:10px;margin-right:10px;margin-bottom:24px;border:1px solid #2f2f2f;border-radius:5px;line-height:2rem;height:10rem;width:10rem;background-color:#262626} +div.user-list img{width:auto;max-width:48px;height:48px;margin-bottom:16px} +div.up{margin-top:-30px;border:1px solid #2b2b2b;padding:4px 6px;overflow:auto} +div.spinner{text-align:center;cursor:wait} +div.spinner.fixed{display:none;position:fixed;top:0;left:0;z-index:99999;bottom:0;right:0;margin:0} +div.spinner .unraid_mark{height:64px; position:fixed;top:50%;left:50%;margin-top:-16px;margin-left:-64px} +div.spinner .unraid_mark_2,div .unraid_mark_4{animation:mark_2 1.5s ease infinite} +div.spinner .unraid_mark_3{animation:mark_3 1.5s ease infinite} +div.spinner .unraid_mark_6,div .unraid_mark_8{animation:mark_6 1.5s ease infinite} +div.spinner .unraid_mark_7{animation:mark_7 1.5s ease infinite} +div.domain{margin-top:-20px} +@keyframes mark_2{50% {transform:translateY(-40px)} 100% {transform:translateY(0px)}} +@keyframes mark_3{50% {transform:translateY(-62px)} 100% {transform:translateY(0px)}} +@keyframes mark_6{50% {transform:translateY(40px)} 100% {transform:translateY(0px)}} +@keyframes mark_7{50% {transform:translateY(62px)} 100% {transform: translateY(0px)}} +pre.up{margin-top:-30px} +pre{border:1px solid #2b2b2b;font-family:bitstream;font-size:1.3rem;line-height:1.8rem;padding:4px 6px;overflow:auto} +iframe#progressFrame{position:fixed;bottom:32px;left:0;margin:0;padding:8px 8px 0 8px;width:100%;height:1.2rem;line-height:1.2rem;border-style:none;overflow:hidden;font-family:bitstream;font-size:1.1rem;color:#808080;white-space:nowrap;z-index:-10} +dl{margin:0;padding-left:12px;line-height:2.6rem} +dt{width:35%;clear:left;float:left;font-weight:normal;text-align:right;margin-right:4rem} +dd{margin-bottom:12px;white-space:nowrap} +dd p{margin:0 0 4px 0} +dd blockquote{padding-left:0} +blockquote{width:90%;margin:10px auto;text-align:left;padding:4px 20px;border-top:2px solid #bce8f1;border-bottom:2px solid #bce8f1;color:#222222;background-color:#d9edf7} +blockquote.ontop{margin-top:-20px;margin-bottom:46px} +blockquote a{color:#ff8c2f;font-weight:600} +blockquote a:hover,blockquote a:focus{color:#f15a2c} +label.checkbox{display:block;position:relative;padding-left:28px;margin:3px 0;cursor:pointer;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none} +label.checkbox input{position:absolute;opacity:0;cursor:pointer} +span.checkmark{position:absolute;top:0;left:6px;height:14px;width:14px;background-color:#2b2b2b;border-radius:100%} +label.checkbox:hover input ~ .checkmark{background-color:#5b5b5b} +label.checkbox input:checked ~ .checkmark{background-color:#ff8c2f} +label.checkbox input:disabled ~ .checkmark{opacity:0.5} +a.bannerDismiss {float:right;cursor:pointer;text-decoration:none;margin-right:1rem} +.bannerDismiss::before {content:"\e92f";font-family:Unraid;color:#e68a00} +a.bannerInfo {cursor:pointer;text-decoration:none} +.bannerInfo::before {content:"\f05a";font-family:fontAwesome;color:#e68a00} +::-webkit-scrollbar{width:8px;height:8px;background:transparent} +::-webkit-scrollbar-thumb{background:gray;border-radius:10px} +::-webkit-scrollbar-corner{background:gray;border-radius:10px} +::-webkit-scrollbar-thumb:hover{background:lightgray} diff --git a/api/src/unraid-api/unraid-file-modifier/modifications/__test__/__fixtures__/downloaded/default-gray.css b/api/src/unraid-api/unraid-file-modifier/modifications/__test__/__fixtures__/downloaded/default-gray.css new file mode 100644 index 0000000000..576b77506d --- /dev/null +++ b/api/src/unraid-api/unraid-file-modifier/modifications/__test__/__fixtures__/downloaded/default-gray.css @@ -0,0 +1,274 @@ +html{font-family:clear-sans,sans-serif;font-size:62.5%;height:100%} +body{font-size:1.3rem;color:#606e7f;background-color:#1b1d1b;padding:0;margin:0;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale} +img{border:none;text-decoration:none;vertical-align:middle} +p{text-align:left} +p.centered{text-align:left} +p:empty{display:none} +a:hover{text-decoration:underline} +a{color:#486dba;text-decoration:none} +a.none{color:#606e7f} +a.img{text-decoration:none;border:none} +a.info{position:relative} +a.info span{display:none;white-space:nowrap;font-variant:small-caps;position:absolute;top:16px;left:12px;color:#b0b0b0;line-height:2rem;padding:5px 8px;border:1px solid #82857e;border-radius:3px;background-color:#121510} +a.info:hover span{display:block;z-index:1} +a.nohand{cursor:default} +a.hand{cursor:pointer;text-decoration:none} +a.static{cursor:default;color:#606060;text-decoration:none} +a.view{display:inline-block;width:20px} +i.spacing{margin-left:0;margin-right:10px} +i.icon{font-size:1.6rem;margin-right:4px;vertical-align:middle} +i.title{display:none} +i.control{cursor:pointer;color:#606060;font-size:1.8rem} +i.favo{display:none;font-size:1.8rem;position:absolute} +pre ul{margin:0;padding-top:0;padding-bottom:0;padding-left:28px} +pre li{margin:0;padding-top:0;padding-bottom:0;padding-left:18px} +big{font-size:1.4rem;font-weight:bold;text-transform:uppercase} +hr{border:none;height:1px!important;color:#606e7f;background-color:#606e7f} +input[type=text],input[type=password],input[type=number],input[type=url],input[type=email],input[type=date],input[type=file],textarea,.textarea{font-family:clear-sans;font-size:1.3rem;background-color:transparent;border:1px solid #606e7f;padding:5px 6px;min-height:2rem;line-height:2rem;outline:none;width:300px;margin:0 20px 0 0;box-shadow:none;border-radius:0;color:#606e7f} +input[type=button],input[type=reset],input[type=submit],button,button[type=button],a.button,.sweet-alert button{font-family:clear-sans;font-size:1.2rem;border:1px solid #606e7f;border-radius:5px;min-width:76px;margin:10px 12px 10px 0;padding:8px;text-align:center;cursor:pointer;outline:none;color:#606e7f;background-color:#121510} +input[type=checkbox]{vertical-align:middle;margin-right:6px} +input[type=number]::-webkit-outer-spin-button,input[type=number]::-webkit-inner-spin-button{-webkit-appearance:none} +input[type=number]{-moz-appearance:textfield} +input:focus[type=text],input:focus[type=password],input:focus[type=number],input:focus[type=url],input:focus[type=email],input:focus[type=file],textarea:focus,.sweet-alert button:focus{background-color:#121510;border-color:#0072c6} +input:hover[type=button],input:hover[type=reset],input:hover[type=submit],button:hover,button:hover[type=button],a.button:hover,.sweet-alert button:hover{border-color:#0072c6;color:#b0b0b0;background-color:#121510!important} +input:active[type=button],input:active[type=reset],input:active[type=submit],button:active,button:active[type=button],a.button:active,.sweet-alert button:active{border-color:#0072c6;box-shadow:none} +input[disabled],button[disabled],input:hover[type=button][disabled],input:hover[type=reset][disabled],input:hover[type=submit][disabled],button:hover[disabled],button:hover[type=button][disabled],input:active[type=button][disabled],input:active[type=reset][disabled],input:active[type=submit][disabled],button:active[disabled],button:active[type=button][disabled],textarea[disabled],.sweet-alert button[disabled]{color:#808080;border-color:#808080;background-color:#383a34;opacity:0.5;cursor:default} +input::-webkit-input-placeholder{color:#00529b} +select{-webkit-appearance:none;font-family:clear-sans;font-size:1.3rem;min-width:188px;max-width:314px;padding:6px 14px 6px 6px;margin:0 10px 0 0;border:1px solid #606e7f;box-shadow:none;border-radius:0;color:#606e7f;background-color:transparent;background-image:linear-gradient(66.6deg, transparent 60%, #606e7f 40%),linear-gradient(113.4deg, #606e7f 40%, transparent 60%);background-position:calc(100% - 8px),calc(100% - 4px);background-size:4px 6px,4px 6px;background-repeat:no-repeat;outline:none;display:inline-block;cursor:pointer} +select option{color:#606e7f;background-color:#121510} +select:focus{border-color:#0072c6} +select[disabled]{color:#808080;border-color:#808080;background-color:#383a34;opacity:0.3;cursor:default} +select[name=enter_view]{font-size:1.2rem;margin:0;padding:0 12px 0 0;border:none;min-width:auto} +select[name=enter_share]{font-size:1.1rem;color:#82857e;padding:0;border:none;min-width:40px;float:right;margin-top:18px;margin-right:20px} +select[name=port_select]{border:none;min-width:54px;padding-top:0;padding-bottom:0} +select.narrow{min-width:87px} +select.auto{min-width:auto} +select.slot{min-width:44rem;max-width:44rem} +input.narrow{width:174px} +input.trim{width:74px;min-width:74px} +textarea{resize:none} +#header{position:fixed;top:0;left:0;width:100%;height:90px;z-index:100;margin:0;background-color:#121510;background-size:100% 90px;background-repeat:no-repeat;border-bottom:1px solid #42453e} +#header .logo{float:left;margin-left:75px;color:#e22828;text-align:center} +#header .logo svg{width:160px;display:block;margin:25px 0 8px 0} +#header .block{margin:0;float:right;text-align:right;background-color:rgba(18,21,16,0.2);padding:10px 12px} +#header .text-left{float:left;text-align:right;padding-right:5px;border-right:solid medium #f15a2c} +#header .text-right{float:right;text-align:left;padding-left:5px} +#header .text-right a{color:#606e7f} +#header .text-right #licensetype{font-weight:bold;font-style:italic;margin-right:4px} +#menu{position:fixed;top:0;left:0;bottom:12px;width:65px;padding:0;margin:0;background-color:#383a34;z-index:2000;box-shadow:inset -1px 0 2px #121510} +#nav-block{position:absolute;top:0;bottom:12px;color:#ffdfb9;white-space:nowrap;float:left;overflow-y:scroll;direction:rtl;letter-spacing:1.8px;scrollbar-width:none} +#nav-block::-webkit-scrollbar{display:none} +#nav-block{-ms-overflow-style:none;overflow:-moz-scrollbars-none} +#nav-block>div{direction:ltr} +.nav-item{width:40px;text-align:left;padding:14px 24px 14px 0;border-bottom:1px solid #42453e;font-size:18px!important;overflow:hidden;transition:.2s background-color ease} +.nav-item:hover{width:auto;padding-right:0;color:#ffdfb9;background:-webkit-gradient(linear,left top,right top,from(#e22828),to(#ff8c2f));background:linear-gradient(90deg,#e22828 0,#ff8c2f);-webkit-transition:all 0.2s ease-in-out;transition:all 0.2s ease-in-out;border-bottom-color:#e22828} +.nav-item:hover a{color:#ffdfb9;background:-webkit-gradient(linear,left top,right top,from(#e22828),to(#ff8c2f));background:linear-gradient(90deg,#e22828 0,#ff8c2f);border-bottom-color:#e22828;font-size:18px} +.nav-item img{display:none} +.nav-item a{color:#a6a7a7;text-decoration:none;padding:20px 80px 13px 16px} +.nav-item.util a{padding-left:24px} +.nav-item a:before{font-family:docker-icon,fontawesome,unraid;font-size:26px;margin-right:25px} +.nav-item.util a:before{font-size:16px} +.nav-item.active,.nav-item.active a{color:#ffdfb9;background:-webkit-gradient(linear,left top,right top,from(#e22828),to(#ff8c2f));background:linear-gradient(90deg,#e22828 0,#ff8c2f)} +.nav-item.HelpButton.active:hover,.nav-item.HelpButton.active a:hover{background:-webkit-gradient(linear,left top,right top,from(#e22828),to(#ff8c2f));background:linear-gradient(90deg,#e22828 0,#ff8c2f);font-size:18px} +.nav-item.HelpButton.active,.nav-item.HelpButton.active a{font-size:18px} +.nav-item a b{display:none} +.nav-user{position:fixed;top:102px;right:10px} +.nav-user a{color:#606e7f;background-color:transparent} +.LanguageButton{font-size:12px!important} /* Fix Switch Language Being Cut-Off */ +div.title{color:#39587f;margin:20px 0 10px 0;padding:10px 0;clear:both;background-color:#1b1d1b;border-bottom:1px solid #606e7f;letter-spacing:1.8px} +div.title span.left{font-size:1.6rem;text-transform:uppercase} +div.title span.right{font-size:1.6rem;padding-right:10px;float:right} +div.title span img,.title p{display:none} +div.title:first-child{margin-top:0} +div.title.shift{margin-top:-12px} +#clear{clear:both} +#footer{position:fixed;bottom:0;left:0;color:#808080;background-color:#121510;padding:5px 0;width:100%;height:1.6rem;line-height:1.6rem;text-align:center;z-index:10000} +#statusraid{float:left;padding-left:10px} +#countdown{margin:0 auto} +#copyright{font-family:bitstream;font-size:1.1rem;float:right;padding-right:10px} +.green{color:#4f8a10;padding-left:5px;padding-right:5px} +.red{color:#f0000c;padding-left:5px;padding-right:5px} +.orange{color:#e68a00;padding-left:5px;padding-right:5px} +.blue{color:#486dba;padding-left:5px;padding-right:5px} +.green-text,.passed{color:#4f8a10} +.red-text,.failed{color:#f0000c} +.orange-text,.warning{color:#e68a00} +.blue-text{color:#486dba} +.grey-text{color:#606060} +.green-orb{color:#33cc33} +.grey-orb{color:#c0c0c0} +.blue-orb{color:#0099ff} +.yellow-orb{color:#ff9900} +.red-orb{color:#ff3300} +.usage-bar{position:fixed;top:64px;left:300px;height:2.2rem;line-height:2.2rem;width:11rem;background-color:#606060} +.usage-bar>span{display:block;height:3px;color:#ffffff;background-color:#606e7f} +.usage-disk{position:relative;height:2.2rem;line-height:2.2rem;background-color:#232523;margin:0} +.usage-disk>span:first-child{position:absolute;left:0;margin:0!important;height:3px;background-color:#606e7f} +.usage-disk>span:last-child{position:relative;padding-right:4px;z-index:1} +.usage-disk.sys{line-height:normal;background-color:transparent;margin:-15px 20px 0 44px} +.usage-disk.sys>span{line-height:normal;height:12px;padding:0} +.usage-disk.mm{height:3px;line-height:normal;background-color:transparent;margin:5px 20px 0 0} +.usage-disk.mm>span:first-child{height:3px;line-height:normal} +.notice{background:url(../images/notice.png) no-repeat 30px 50%;font-size:1.5rem;text-align:left;vertical-align:middle;padding-left:100px;height:6rem;line-height:6rem} +.greenbar{background:-webkit-radial-gradient(#127a05,#17bf0b);background:linear-gradient(#127a05,#17bf0b)} +.orangebar{background:-webkit-radial-gradient(#ce7c10,#f0b400);background:linear-gradient(#ce7c10,#f0b400)} +.redbar{background:-webkit-radial-gradient(#941c00,#de1100);background:linear-gradient(#941c00,#de1100)} +.graybar{background:-webkit-radial-gradient(#949494,#d9d9d9);background:linear-gradient(#949494,#d9d9d9)} +table{border-collapse:collapse;border-spacing:0;border-style:hidden;margin:0;width:100%} +table thead td{line-height:3rem;height:3rem;white-space:nowrap} +table tbody td{line-height:3rem;height:3rem;white-space:nowrap} +table tbody tr.tr_last{border-bottom:1px solid #606e7f} +table.unraid thead tr:first-child>td{font-size:1.2rem;text-transform:uppercase;letter-spacing:1px;color:#82857e;border-bottom:1px solid #606e7f} +table.unraid tbody tr:not(.tr_last):hover>td{background-color:rgba(255,255,255,0.05)} +table.unraid tr>td{overflow:hidden;text-overflow:ellipsis;padding-left:8px} +table.unraid tr>td:hover{overflow:visible} +table.legacy{table-layout:auto!important} +table.legacy thead td{line-height:normal;height:auto;padding:7px 0} +table.legacy tbody td{line-height:normal;height:auto;padding:5px 0} +table.disk_status{table-layout:fixed} +table.disk_status tr>td:last-child{padding-right:8px} +table.disk_status tr>td:nth-child(1){width:13%} +table.disk_status tr>td:nth-child(2){width:30%} +table.disk_status tr>td:nth-child(3){width:8%;text-align:right} +table.disk_status tr>td:nth-child(n+4){width:7%;text-align:right} +table.disk_status tr.offline>td:nth-child(2){width:43%} +table.disk_status tr.offline>td:nth-child(n+3){width:5.5%} +table.disk_status tbody tr{border-bottom:1px solid #0c0f0b} +table.array_status{table-layout:fixed} +table.array_status tr>td{padding-left:8px;white-space:normal} +table.array_status tr>td:nth-child(1){width:33%} +table.array_status tr>td:nth-child(2){width:22%} +table.array_status.noshift{margin-top:0} +table.array_status td.line{border-top:1px solid #0c0f0b} +table.share_status{table-layout:fixed;margin-top:12px} +table.share_status tr>td{padding-left:8px} +table.share_status tr>td:nth-child(1){width:15%} +table.share_status tr>td:nth-child(2){width:30%} +table.share_status tr>td:nth-child(n+3){width:10%} +table.share_status tr>td:nth-child(5){width:15%} +table.dashboard{margin:0;border:none;background-color:#212f3d} +table.dashboard tbody{border:1px solid #566573} +table.dashboard tr:first-child>td{height:3.6rem;padding-top:12px;font-size:1.6rem;font-weight:bold;letter-spacing:1.8px;text-transform:none;vertical-align:top} +table.dashboard tr:last-child>td{padding-bottom:20px} +table.dashboard tr.last>td{padding-bottom:20px} +table.dashboard tr.header>td{padding-bottom:10px;color:#82857e} +table.dashboard tr{border:none} +table.dashboard td{line-height:normal;height:auto;padding:3px 10px;border:none!important} +table.dashboard td.stopgap{height:20px!important;line-height:20px!important;padding:0!important;background-color:#1b1d1b} +table.dashboard td.vpn{font-size:1.1rem;font-weight:bold;text-transform:uppercase;letter-spacing:1px} +table.dashboard td div.section{display:inline-block;vertical-align:top;margin-left:4px;font-size:1.2rem;font-weight:bold;text-transform:uppercase;letter-spacing:1px} +table.dashboard td div.section span{font-weight:normal;text-transform:none;letter-spacing:0;white-space:normal} +table.dashboard td span.info{float:right;margin-right:20px;font-size:1.2rem;font-weight:normal;text-transform:none;letter-spacing:0} +table.dashboard td span.info.title{font-weight:bold} +table.dashboard td span.load{display:inline-block;width:38px;text-align:right} +table.dashboard td span.finish{float:right;margin-right:24px} +table.dashboard i.control{float:right;font-size:1.4rem!important;margin:0 3px 0 0;cursor:pointer;color:#212f3d;background-color:rgba(255,255,255,0.3);padding:2px;border-radius:5px} +tr.alert{color:#f0000c;background-color:#ff9e9e} +tr.warn{color:#e68a00;background-color:#feefb3} +tr.past{color:#d63301;background-color:#ffddd1} +[name=arrayOps]{margin-top:12px} +span.error{color:#f0000c;background-color:#ff9e9e;display:block;width:100%} +span.warn{color:#e68a00;background-color:#feefb3;display:block;width:100%} +span.system{color:#00529b;background-color:#bde5f8;display:block;width:100%} +span.array{color:#4f8a10;background-color:#dff2bf;display:block;width:100%} +span.login{color:#d63301;background-color:#ffddd1;display:block;width:100%} +span.lite{background-color:#121510} +span.label{font-size:1.1rem;padding:2px 0 2px 6px;margin-right:6px;border-radius:4px;display:inline;width:auto;vertical-align:middle} +span.cpu-speed{display:block;color:#3b5998} +span.status{float:right;font-size:1.4rem;letter-spacing:1.8px} +span.status.vhshift{margin-top:0;margin-right:8px} +span.status.vshift{margin-top:-16px} +span.status.hshift{margin-right:-20px} +span.diskinfo{float:left;clear:both;margin-top:5px;padding-left:10px} +span.bitstream{font-family:bitstream;font-size:1.1rem} +span.p0{padding-left:0} +span.ucfirst{text-transform:capitalize} +span.strong{font-weight:bold} +span.big{font-size:1.4rem} +span.small{font-size:1.1rem} +span#dropbox{background:none;line-height:6rem;margin-right:20px} +span.outer{margin-bottom:20px;margin-right:0} +span.outer.solid{background-color:#212f3d} +span.hand{cursor:pointer} +span.outer.started>img,span.outer.started>i.img{opacity:1.0} +span.outer.stopped>img,span.outer.stopped>i.img{opacity:0.3} +span.outer.paused>img,span.outer.paused>i.img{opacity:0.6} +span.inner{display:inline-block;vertical-align:top} +span.state{font-size:1.1rem;margin-left:7px} +span.slots{display:inline-block;width:44rem;margin:0!important} +span.slots-left{float:left;margin:0!important} +input.subpool{float:right;margin:2px 0 0 0} +i.padlock{margin-right:8px;cursor:default;vertical-align:middle} +i.nolock{visibility:hidden;margin-right:8px;vertical-align:middle} +i.lock{margin-left:8px;cursor:default;vertical-align:middle} +i.orb{font-size:1.1rem;margin:0 8px 0 3px} +img.img,i.img{width:32px;height:32px;margin-right:10px} +img.icon{margin:-3px 4px 0 0} +img.list{width:auto;max-width:32px;height:32px} +i.list{font-size:32px} +a.list{text-decoration:none;color:inherit} +div.content{position:absolute;top:0;left:0;width:100%;padding-bottom:30px;z-index:-1;clear:both} +div.content.shift{margin-top:1px} +label+.content{margin-top:64px} +div.tabs{position:relative;margin:110px 20px 30px 90px;background-color:#1b1d1b} +div.tab{float:left;margin-top:23px} +div.tab input[id^='tab']{display:none} +div.tab [type=radio]+label:hover{cursor:pointer;border-color:#0072c6;opacity:1} +div.tab [type=radio]:checked+label{cursor:default;background-color:transparent;color:#606e7f;border-color:#004e86;opacity:1} +div.tab [type=radio]+label~.content{display:none} +div.tab [type=radio]:checked+label~.content{display:inline} +div.tab [type=radio]+label{position:relative;letter-spacing:1.8px;padding:10px 10px;margin-right:2px;border-top-left-radius:12px;border-top-right-radius:12px;background-color:#606e7f;color:#b0b0b0;border:1px solid #8b98a7;border-bottom:none;opacity:0.5} +div.tab [type=radio]+label img{display:none} +div.Panel{width:25%;height:auto;float:left;margin:0;padding:5px;border-right:#0c0f0b 1px solid;border-bottom:1px solid #0c0f0b;box-sizing:border-box} +div.Panel a{text-decoration:none} +div.Panel:hover{background-color:#121510} +div.Panel:hover .PanelText{text-decoration:underline} +div.Panel br,.vmtemplate br{display:none} +div.Panel img.PanelImg{float:left;width:auto;max-width:32px;height:32px;margin:10px} +div.Panel i.PanelIcon{float:left;font-size:32px;color:#606e7f;margin:10px} +div.Panel .PanelText{font-size:1.4rem;padding-top:16px;text-align:center} +div.user-list{float:left;padding:10px;margin-right:10px;margin-bottom:24px;border:1px solid #0c0f0b;border-radius:5px;line-height:2rem;height:10rem;width:10rem} +div.user-list img{width:auto;max-width:48px;height:48px;margin-bottom:16px} +div.user-list:hover{background-color:#121510} +div.vmheader{display:block;clear:both} +div.vmtemplate:hover{background-color:#121510} +div.vmtemplate{height:12rem;width:12rem;border:1px solid #0c0f0b} +div.vmtemplate img{margin-top:20px} +div.up{margin-top:-20px;border:1px solid #0c0f0b;padding:4px 6px;overflow:auto} +div.spinner{text-align:center;cursor:wait} +div.spinner.fixed{display:none;position:fixed;top:0;left:0;z-index:99999;bottom:0;right:0;margin:0} +div.spinner .unraid_mark{height:64px; position:fixed;top:50%;left:50%;margin-top:-16px;margin-left:-64px} +div.spinner .unraid_mark_2,div .unraid_mark_4{animation:mark_2 1.5s ease infinite} +div.spinner .unraid_mark_3{animation:mark_3 1.5s ease infinite} +div.spinner .unraid_mark_6,div .unraid_mark_8{animation:mark_6 1.5s ease infinite} +div.spinner .unraid_mark_7{animation:mark_7 1.5s ease infinite} +@keyframes mark_2{50% {transform:translateY(-40px)} 100% {transform:translateY(0px)}} +@keyframes mark_3{50% {transform:translateY(-62px)} 100% {transform:translateY(0px)}} +@keyframes mark_6{50% {transform:translateY(40px)} 100% {transform:translateY(0px)}} +@keyframes mark_7{50% {transform:translateY(62px)} 100% {transform: translateY(0px)}} +pre.up{margin-top:0} +pre{border:1px solid #0c0f0b;font-family:bitstream;font-size:1.3rem;line-height:1.8rem;padding:0;overflow:auto;margin-bottom:10px;padding:10px} +iframe#progressFrame{position:fixed;bottom:32px;left:60px;margin:0;padding:8px 8px 0 8px;width:100%;height:1.2rem;line-height:1.2rem;border-style:none;overflow:hidden;font-family:bitstream;font-size:1.1rem;color:#808080;white-space:nowrap;z-index:-2} +dl{margin-top:0;padding-left:12px;line-height:2.6rem} +dt{width:35%;clear:left;float:left;text-align:right;margin-right:4rem} +dd{margin-bottom:12px;white-space:nowrap} +dd p{margin:0 0 4px 0} +dd blockquote{padding-left:0} +blockquote{width:90%;margin:10px auto;text-align:left;padding:4px 20px;border:1px solid #bce8f1;color:#222222;background-color:#d9edf7;box-sizing:border-box} +blockquote.ontop{margin-top:0;margin-bottom:46px} +blockquote a{color:#ff8c2f;font-weight:600} +blockquote a:hover,blockquote a:focus{color:#f15a2c} +label.checkbox{display:block;position:relative;padding-left:28px;margin:3px 0;cursor:pointer;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none} +label.checkbox input{position:absolute;opacity:0;cursor:pointer} +span.checkmark{position:absolute;top:0;left:6px;height:14px;width:14px;background-color:#2b2d2b;border-radius:100%} +label.checkbox:hover input ~ .checkmark{background-color:#5b5d5b} +label.checkbox input:checked ~ .checkmark{background-color:#ff8c2f} +label.checkbox input:disabled ~ .checkmark{opacity:0.5} +a.bannerDismiss {float:right;cursor:pointer;text-decoration:none;margin-right:1rem} +.bannerDismiss::before {content:"\e92f";font-family:Unraid;color:#e68a00} +a.bannerInfo {cursor:pointer;text-decoration:none} +.bannerInfo::before {content:"\f05a";font-family:fontAwesome;color:#e68a00} +::-webkit-scrollbar{width:8px;height:8px;background:transparent} +::-webkit-scrollbar-thumb{background:gray;border-radius:10px} +::-webkit-scrollbar-corner{background:gray;border-radius:10px} +::-webkit-scrollbar-thumb:hover{background:lightgray} diff --git a/api/src/unraid-api/unraid-file-modifier/modifications/__test__/__fixtures__/downloaded/default-white.css b/api/src/unraid-api/unraid-file-modifier/modifications/__test__/__fixtures__/downloaded/default-white.css new file mode 100644 index 0000000000..3c75099761 --- /dev/null +++ b/api/src/unraid-api/unraid-file-modifier/modifications/__test__/__fixtures__/downloaded/default-white.css @@ -0,0 +1,262 @@ +html{font-family:clear-sans,sans-serif;font-size:62.5%;height:100%} +body{font-size:1.3rem;color:#1c1b1b;background-color:#f2f2f2;padding:0;margin:0;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale} +img{border:none;text-decoration:none;vertical-align:middle} +p{text-align:justify} +p.centered{text-align:left} +p:empty{display:none} +a:hover{text-decoration:underline} +a{color:#486dba;text-decoration:none} +a.none{color:#1c1b1b} +a.img{text-decoration:none;border:none} +a.info{position:relative} +a.info span{display:none;white-space:nowrap;font-variant:small-caps;position:absolute;top:16px;left:12px;line-height:2rem;color:#f2f2f2;padding:5px 8px;border:1px solid rgba(255,255,255,0.25);border-radius:3px;background-color:rgba(25,25,25,0.95);box-shadow:0 0 3px #303030} +a.info:hover span{display:block;z-index:1} +a.nohand{cursor:default} +a.hand{cursor:pointer;text-decoration:none} +a.static{cursor:default;color:#909090;text-decoration:none} +a.view{display:inline-block;width:20px} +i.spacing{margin-left:-6px} +i.icon{font-size:1.6rem;margin-right:4px;vertical-align:middle} +i.title{margin-right:8px} +i.control{cursor:pointer;color:#909090;font-size:1.8rem} +i.favo{display:none;font-size:1.8rem;position:absolute;margin-left:12px} +hr{border:none;height:1px!important;color:#e3e3e3;background-color:#e3e3e3} +input[type=text],input[type=password],input[type=number],input[type=url],input[type=email],input[type=date],input[type=file],textarea,.textarea{font-family:clear-sans;font-size:1.3rem;background-color:transparent;border:none;border-bottom:1px solid #1c1b1b;padding:4px 0;text-indent:0;min-height:2rem;line-height:2rem;outline:none;width:300px;margin:0 20px 0 0;box-shadow:none;border-radius:0;color:#1c1b1b} +input[type=button],input[type=reset],input[type=submit],button,button[type=button],a.button,.sweet-alert button{font-family:clear-sans;font-size:1.1rem;font-weight:bold;letter-spacing:1.8px;text-transform:uppercase;min-width:86px;margin:10px 12px 10px 0;padding:8px;text-align:center;text-decoration:none;white-space:nowrap;cursor:pointer;outline:none;border-radius:4px;border:none;color:#ff8c2f;background:-webkit-gradient(linear,left top,right top,from(#e22828),to(#ff8c2f)) 0 0 no-repeat,-webkit-gradient(linear,left top,right top,from(#e22828),to(#ff8c2f)) 0 100% no-repeat,-webkit-gradient(linear,left bottom,left top,from(#e22828),to(#e22828)) 0 100% no-repeat,-webkit-gradient(linear,left bottom,left top,from(#ff8c2f),to(#ff8c2f)) 100% 100% no-repeat;background:linear-gradient(90deg,#e22828 0,#ff8c2f) 0 0 no-repeat,linear-gradient(90deg,#e22828 0,#ff8c2f) 0 100% no-repeat,linear-gradient(0deg,#e22828 0,#e22828) 0 100% no-repeat,linear-gradient(0deg,#ff8c2f 0,#ff8c2f) 100% 100% no-repeat;background-size:100% 2px,100% 2px,2px 100%,2px 100%} +input[type=checkbox]{vertical-align:middle;margin-right:6px} +input[type=number]::-webkit-outer-spin-button,input[type=number]::-webkit-inner-spin-button{-webkit-appearance: none} +input[type=number]{-moz-appearance:textfield} +input:focus[type=text],input:focus[type=password],input:focus[type=number],input:focus[type=url],input:focus[type=email],input:focus[type=file],textarea:focus,.sweet-alert button:focus{background-color:#e8e8e8;outline:0} +input:hover[type=button],input:hover[type=reset],input:hover[type=submit],button:hover,button:hover[type=button],a.button:hover,.sweet-alert button:hover{color:#f2f2f2;background:-webkit-gradient(linear,left top,right top,from(#e22828),to(#ff8c2f));background:linear-gradient(90deg,#e22828 0,#ff8c2f)} +input[disabled],textarea[disabled]{color:#1c1b1b;border-bottom-color:#a2a2a2;opacity:0.5;cursor:default} +input[type=button][disabled],input[type=reset][disabled],input[type=submit][disabled],button[disabled],button[type=button][disabled],a.button[disabled] +input:hover[type=button][disabled],input:hover[type=reset][disabled],input:hover[type=submit][disabled],button:hover[disabled],button:hover[type=button][disabled],a.button:hover[disabled] +input:active[type=button][disabled],input:active[type=reset][disabled],input:active[type=submit][disabled],button:active[disabled],button:active[type=button][disabled],a.button:active[disabled],.sweet-alert button[disabled]{opacity:0.5;cursor:default;color:#808080;background:-webkit-gradient(linear,left top,right top,from(#404040),to(#808080)) 0 0 no-repeat,-webkit-gradient(linear,left top,right top,from(#404040),to(#808080)) 0 100% no-repeat,-webkit-gradient(linear,left bottom,left top,from(#404040),to(#404040)) 0 100% no-repeat,-webkit-gradient(linear,left bottom,left top,from(#808080),to(#808080)) 100% 100% no-repeat;background:linear-gradient(90deg,#404040 0,#808080) 0 0 no-repeat,linear-gradient(90deg,#404040 0,#808080) 0 100% no-repeat,linear-gradient(0deg,#404040 0,#404040) 0 100% no-repeat,linear-gradient(0deg,#808080 0,#808080) 100% 100% no-repeat;background-size:100% 2px,100% 2px,2px 100%,2px 100%} +input::-webkit-input-placeholder{color:#486dba} +select{-webkit-appearance:none;font-family:clear-sans;font-size:1.3rem;min-width:166px;max-width:300px;padding:5px 8px 5px 0;text-indent:0;margin:0 10px 0 0;border:none;border-bottom:1px solid #1c1b1b;box-shadow:none;border-radius:0;color:#1c1b1b;background-color:transparent;background-image:linear-gradient(66.6deg, transparent 60%, #1c1b1b 40%),linear-gradient(113.4deg, #1c1b1b 40%, transparent 60%);background-position:calc(100% - 4px),100%;background-size:4px 6px,4px 6px;background-repeat:no-repeat;outline:none;display:inline-block;cursor:pointer} +select option{color:#1c1b1b;background-color:#e8e8e8} +select:focus{outline:0} +select[disabled]{color:#1c1b1b;border-bottom-color:#a2a2a2;opacity:0.5;cursor:default} +select[name=enter_view]{margin:0;padding:0 12px 0 0;border:none;min-width:auto} +select[name=enter_share]{font-size:1.1rem;padding:0;border:none;min-width:40px;float:right;margin-top:13px;margin-right:20px} +select[name=port_select]{border:none;min-width:54px;padding-top:0;padding-bottom:0} +select.narrow{min-width:76px} +select.auto{min-width:auto} +select.slot{min-width:44rem;max-width:44rem} +input.narrow{width:166px} +input.trim{width:76px;min-width:76px} +textarea{resize:none} +#header{position:absolute;top:0;left:0;width:100%;height:91px;z-index:102;margin:0;color:#f2f2f2;background-color:#1c1b1b;background-size:100% 90px;background-repeat:no-repeat} +#header .logo{float:left;margin-left:10px;color:#e22828;text-align:center} +#header .logo svg{width:160px;display:block;margin:25px 0 8px 0} +#header .block{margin:0;float:right;text-align:right;background-color:rgba(28,27,27,0.2);padding:10px 12px} +#header .text-left{float:left;text-align:right;padding-right:5px;border-right:solid medium #f15a2c} +#header .text-right{float:right;text-align:left;padding-left:5px} +#header .text-right a{color:#f2f2f2} +#header .text-right #licensetype{font-weight:bold;font-style:italic;margin-right:4px} +div.title{margin:20px 0 32px 0;padding:8px 10px;clear:both;border-bottom:1px solid #e3e3e3;background-color:#e8e8e8;letter-spacing:1.8px} +div.title span.left{font-size:1.4rem} +div.title span.right{font-size:1.4rem;padding-top:2px;padding-right:10px;float:right} +div.title span img{padding-right:4px} +div.title.shift{margin-top:-30px} +#menu{position:absolute;top:90px;left:0;right:0;display:grid;grid-template-columns:auto max-content;z-index:101} +.nav-tile{height:4rem;line-height:4rem;padding:0;margin:0;font-size:1.2rem;letter-spacing:1.8px;background-color:#1c1b1b;white-space:nowrap;overflow-x:auto;overflow-y:hidden;scrollbar-width:thin} +.nav-tile::-webkit-scrollbar{height:5px} +.nav-tile.right{text-align:right} +.nav-item,.nav-user{position:relative;display:inline-block;text-align:center;margin:0} +.nav-item a{min-width:0} +.nav-item a span{display:none} +.nav-item .system{vertical-align:middle;padding-bottom:2px} +.nav-item a{color:#f2f2f2;background-color:transparent;text-transform:uppercase;font-weight:bold;display:block;padding:0 10px} +.nav-item a{text-decoration:none;text-decoration-skip-ink:auto;-webkit-text-decoration-skip:objects;-webkit-transition:all .25s ease-out;transition:all .25s ease-out} +.nav-item:after,.nav-user.show:after{border-radius:4px;display:block;background-color:transparent;content:"";width:32px;height:2px;bottom:8px;position:absolute;left:50%;margin-left:-16px;-webkit-transition:all .25s ease-in-out;transition:all .25s ease-in-out;pointer-events:none} +.nav-item:focus:after,.nav-item:hover:after,.nav-user.show:hover:after{background-color:#f15a2c} +.nav-item.active:after{background-color:#f2f2f2} +.nav-user a{color:#f2f2f2;background-color:transparent;display:block;padding:0 10px} +.nav-user .system{vertical-align:middle;padding-bottom:2px} +#clear{clear:both} +#footer{position:fixed;bottom:0;left:0;color:#2b2a29;background-color:#d4d5d6;padding:5px 0;width:100%;height:1.6rem;line-height:1.6rem;text-align:center;z-index:10000} +#statusraid{float:left;padding-left:10px} +#countdown{margin:0 auto} +#copyright{font-family:bitstream;font-size:1.1rem;float:right;padding-right:10px} +.green{color:#4f8a10;padding-left:5px;padding-right:5px} +.red{color:#f0000c;padding-left:5px;padding-right:5px} +.orange{color:#e68a00;padding-left:5px;padding-right:5px} +.blue{color:#486dba;padding-left:5px;padding-right:5px} +.green-text,.passed{color:#4f8a10} +.red-text,.failed{color:#f0000c} +.orange-text,.warning{color:#e68a00} +.blue-text{color:#486dba} +.grey-text{color:#606060} +.green-orb{color:#33cc33} +.grey-orb{color:#c0c0c0} +.blue-orb{color:#0099ff} +.yellow-orb{color:#ff9900} +.red-orb{color:#ff3300} +.usage-bar{float:left;height:2rem;line-height:2rem;width:14rem;padding:1px 1px 1px 2px;margin:8px 12px;border-radius:3px;background-color:#585858;box-shadow:0 1px 0 #989898,inset 0 1px 0 #202020} +.usage-bar>span{display:block;height:100%;text-align:right;border-radius:2px;color:#f2f2f2;background-color:#808080;box-shadow:inset 0 1px 0 rgba(255,255,255,.5)} +.usage-disk{position:relative;height:1.8rem;background-color:#dcdcdc;margin:0} +.usage-disk>span:first-child{position:absolute;left:0;margin:0!important;height:1.8rem;background-color:#a8a8a8} +.usage-disk>span:last-child{position:relative;top:-0.4rem;right:0;padding-right:6px;z-index:1} +.usage-disk.sys{height:12px;margin:-1.4rem 20px 0 44px} +.usage-disk.sys>span{height:12px;padding:0} +.usage-disk.sys.none{background-color:transparent} +.usage-disk.mm{height:3px;margin:5px 20px 0 0} +.usage-disk.mm>span:first-child{height:3px} +.notice{background:url(../images/notice.png) no-repeat 30px 50%;font-size:1.5rem;text-align:left;vertical-align:middle;padding-left:100px;height:6rem;line-height:6rem} +.notice.shift{margin-top:160px} +.greenbar{background:-webkit-gradient(linear,left top,right top,from(#127a05),to(#17bf0b));background:linear-gradient(90deg,#127a05 0,#17bf0b)} +.orangebar{background:-webkit-gradient(linear,left top,right top,from(#ce7c10),to(#ce7c10));background:linear-gradient(90deg,#ce7c10 0,#ce7c10)} +.redbar{background:-webkit-gradient(linear,left top,right top,from(#941c00),to(#de1100));background:linear-gradient(90deg,#941c00 0,#de1100)} +.graybar{background:-webkit-gradient(linear,left top,right top,from(#949494),to(#d9d9d9));background:linear-gradient(90deg,#949494 0,#d9d9d9)} +table{border-collapse:collapse;border-spacing:0;border-style:hidden;margin:-30px 0 0 0;width:100%;background-color:#f5f5f5} +table thead td{line-height:2.8rem;height:2.8rem;white-space:nowrap} +table tbody td{line-height:2.6rem;height:2.6rem;white-space:nowrap} +table tbody tr.alert{color:#f0000c} +table tbody tr.warn{color:#e68a00} +table.unraid thead tr:first-child>td{font-size:1.1rem;text-transform:uppercase;letter-spacing:1px;background-color:#e8e8e8} +table.unraid thead tr:last-child{border-bottom:1px solid #e3e3e3} +table.unraid tbody tr:nth-child(even){background-color:#ededed} +table.unraid tbody tr:not(.tr_last):hover>td{background-color:rgba(0,0,0,0.1)} +table.unraid tr>td{overflow:hidden;text-overflow:ellipsis;padding-left:8px} +table.unraid tr>td:hover{overflow:visible} +table.legacy{table-layout:auto!important} +table.legacy thead td{line-height:normal;height:auto;padding:7px 0} +table.legacy tbody td{line-height:normal;height:auto;padding:5px 0} +table.disk_status{table-layout:fixed} +table.disk_status tr>td:last-child{padding-right:8px} +table.disk_status tr>td:nth-child(1){width:13%} +table.disk_status tr>td:nth-child(2){width:30%} +table.disk_status tr>td:nth-child(3){width:8%;text-align:right} +table.disk_status tr>td:nth-child(n+4){width:7%;text-align:right} +table.disk_status tr.offline>td:nth-child(2){width:43%} +table.disk_status tr.offline>td:nth-child(n+3){width:5.5%} +table.disk_status tbody tr.tr_last{line-height:3rem;height:3rem;background-color:#ededed;border-top:1px solid #e3e3e3} +table.array_status{table-layout:fixed} +table.array_status tr>td{padding-left:8px;white-space:normal} +table.array_status tr>td:nth-child(1){width:33%} +table.array_status tr>td:nth-child(2){width:22%} +table.array_status.noshift{margin-top:0} +table.array_status td.line{border-top:1px solid #e3e3e3} +table.share_status{table-layout:fixed} +table.share_status tr>td{padding-left:8px} +table.share_status tr>td:nth-child(1){width:15%} +table.share_status tr>td:nth-child(2){width:30%} +table.share_status tr>td:nth-child(n+3){width:10%} +table.share_status tr>td:nth-child(5){width:15%} +table.dashboard{margin:0;border:none;background-color:#f7f9f9} +table.dashboard tbody{border:1px solid #dfdfdf} +table.dashboard tbody td{line-height:normal;height:auto;padding:3px 10px} +table.dashboard tr:first-child>td{height:3.6rem;padding-top:12px;font-size:1.6rem;font-weight:bold;letter-spacing:1.8px;text-transform:none;vertical-align:top} +table.dashboard tr:nth-child(even){background-color:transparent} +table.dashboard tr:last-child>td{padding-bottom:20px} +table.dashboard tr.last>td{padding-bottom:20px} +table.dashboard tr.header>td{padding-bottom:10px} +table.dashboard td{line-height:2.4rem;height:2.4rem} +table.dashboard td.stopgap{height:20px!important;line-height:20px!important;padding:0!important;background-color:#f2f2f2} +table.dashboard td.vpn{font-size:1.1rem;font-weight:bold;text-transform:uppercase;letter-spacing:1px} +table.dashboard td div.section{display:inline-block;vertical-align:top;margin-left:4px;font-size:1.2rem;font-weight:bold;text-transform:uppercase;letter-spacing:1px} +table.dashboard td div.section span{font-weight:normal;text-transform:none;letter-spacing:0;white-space:normal} +table.dashboard td span.info{float:right;margin-right:20px;font-size:1.2rem;font-weight:normal;text-transform:none;letter-spacing:0} +table.dashboard td span.info.title{font-weight:bold} +table.dashboard td span.load{display:inline-block;width:38px;text-align:right} +table.dashboard td span.finish{float:right;margin-right:24px} +table.dashboard i.control{float:right;font-size:1.4rem!important;margin:0 3px 0 0;cursor:pointer;color:#f7f9f9;background-color:rgba(0,0,0,0.3);padding:2px;border-radius:5px} +[name=arrayOps]{margin-top:12px} +span.error{color:#f0000c;background-color:#ff9e9e;display:block;width:100%} +span.warn{color:#e68a00;background-color:#feefb3;display:block;width:100%} +span.system{color:#0099ff;background-color:#bde5f8;display:block;width:100%} +span.array{color:#4f8a10;background-color:#dff2bf;display:block;width:100%} +span.login{color:#d63301;background-color:#ffddd1;display:block;width:100%} +span.lite{background-color:#ededed} +span.label{font-size:1.2rem;padding:2px 0 2px 6px;margin-right:6px;border-radius:4px;display:inline;width:auto;vertical-align:middle} +span.cpu-speed{display:block;color:#3b5998} +span.status{float:right;font-size:1.4rem;margin-top:30px;padding-right:8px;letter-spacing:1.8px} +span.status.vhshift{margin-top:0;margin-right:-9px} +span.status.vshift{margin-top:-16px} +span.status.hshift{margin-right:-20px} +span.diskinfo{float:left;clear:both;margin-top:5px;padding-left:10px} +span.bitstream{font-family:bitstream;font-size:1.1rem} +span.ucfirst{text-transform:capitalize} +span.strong{font-weight:bold} +span.big{font-size:1.4rem} +span.small{font-size:1.2rem} +span.outer{margin-bottom:20px;margin-right:0} +span.outer.solid{background-color:#F7F9F9} +span.hand{cursor:pointer} +span.outer.started>img,span.outer.started>i.img{opacity:1.0} +span.outer.stopped>img,span.outer.stopped>i.img{opacity:0.3} +span.outer.paused>img,span.outer.paused>i.img{opacity:0.6} +span.inner{display:inline-block;vertical-align:top} +span.state{font-size:1.1rem;margin-left:7px} +span.slots{display:inline-block;width:44rem;margin:0!important} +span.slots-left{float:left;margin:0!important} +input.subpool{float:right;margin:2px 0 0 0} +i.padlock{margin-right:8px;cursor:default;vertical-align:middle} +i.nolock{visibility:hidden;margin-right:8px;vertical-align:middle} +i.lock{margin-left:8px;cursor:default;vertical-align:middle} +i.orb{font-size:1.1rem;margin:0 8px 0 3px} +img.img,i.img{width:32px;height:32px;margin-right:10px} +img.icon{margin:-3px 4px 0 0} +img.list{width:auto;max-width:32px;height:32px} +i.list{font-size:32px} +a.list{text-decoration:none;color:inherit} +div.content{position:absolute;top:0;left:0;width:100%;padding-bottom:30px;z-index:-1;clear:both} +div.content.shift{margin-top:1px} +label+.content{margin-top:86px} +div.tabs{position:relative;margin:130px 0 0 0} +div.tab{float:left;margin-top:30px} +div.tab input[id^="tab"]{display:none} +div.tab [type=radio]+label:hover{background-color:transparent;border:1px solid #ff8c2f;border-bottom:none;cursor:pointer;opacity:1} +div.tab [type=radio]:checked+label{cursor:default;background-color:transparent;border:1px solid #ff8c2f;border-bottom:none;opacity:1} +div.tab [type=radio]+label~.content{display:none} +div.tab [type=radio]:checked+label~.content{display:inline} +div.tab [type=radio]+label{position:relative;font-size:1.4rem;letter-spacing:1.8px;padding:4px 10px;margin-right:2px;border-top-left-radius:6px;border-top-right-radius:6px;border:1px solid #b2b2b2;border-bottom:none;background-color:#e2e2e2;opacity:0.5} +div.tab [type=radio]+label img{padding-right:4px} +div.Panel{text-align:center;float:left;margin:0 0 30px 10px;padding-right:50px;height:8rem} +div.Panel a{text-decoration:none} +div.Panel span{height:42px;display:block} +div.Panel:hover .PanelText{text-decoration:underline} +div.Panel img.PanelImg{width:auto;max-width:32px;height:32px} +div.Panel i.PanelIcon{font-size:32px;color:#1c1b1b} +div.user-list{float:left;padding:10px;margin-right:10px;margin-bottom:24px;border:1px solid #dedede;border-radius:5px;line-height:2rem;height:10rem;width:10rem;background-color:#e8e8e8} +div.user-list img{width:auto;max-width:48px;height:48px;margin-bottom:16px} +div.up{margin-top:-30px;border:1px solid #e3e3e3;padding:4px 6px;overflow:auto} +div.spinner{text-align:center;cursor:wait} +div.spinner.fixed{display:none;position:fixed;top:0;left:0;z-index:99999;bottom:0;right:0;margin:0} +div.spinner .unraid_mark{height:64px; position:fixed;top:50%;left:50%;margin-top:-16px;margin-left:-64px} +div.spinner .unraid_mark_2,div .unraid_mark_4{animation:mark_2 1.5s ease infinite} +div.spinner .unraid_mark_3{animation:mark_3 1.5s ease infinite} +div.spinner .unraid_mark_6,div .unraid_mark_8{animation:mark_6 1.5s ease infinite} +div.spinner .unraid_mark_7{animation:mark_7 1.5s ease infinite} +div.domain{margin-top:-20px} +@keyframes mark_2{50% {transform:translateY(-40px)} 100% {transform:translateY(0px)}} +@keyframes mark_3{50% {transform:translateY(-62px)} 100% {transform:translateY(0px)}} +@keyframes mark_6{50% {transform:translateY(40px)} 100% {transform:translateY(0px)}} +@keyframes mark_7{50% {transform:translateY(62px)} 100% {transform: translateY(0px)}} +pre.up{margin-top:-30px} +pre{border:1px solid #e3e3e3;font-family:bitstream;font-size:1.3rem;line-height:1.8rem;padding:4px 6px;overflow:auto} +iframe#progressFrame{position:fixed;bottom:32px;left:0;margin:0;padding:8px 8px 0 8px;width:100%;height:1.2rem;line-height:1.2rem;border-style:none;overflow:hidden;font-family:bitstream;font-size:1.1rem;color:#808080;white-space:nowrap;z-index:-10} +dl{margin:0;padding-left:12px;line-height:2.6rem} +dt{width:35%;clear:left;float:left;font-weight:normal;text-align:right;margin-right:4rem} +dd{margin-bottom:12px;white-space:nowrap} +dd p{margin:0 0 4px 0} +dd blockquote{padding-left:0} +blockquote{width:90%;margin:10px auto;text-align:left;padding:4px 20px;border-top:2px solid #bce8f1;border-bottom:2px solid #bce8f1;color:#222222;background-color:#d9edf7} +blockquote.ontop{margin-top:-20px;margin-bottom:46px} +blockquote a{color:#ff8c2f;font-weight:600} +blockquote a:hover,blockquote a:focus{color:#f15a2c} +label.checkbox{display:block;position:relative;padding-left:28px;margin:3px 0;cursor:pointer;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none} +label.checkbox input{position:absolute;opacity:0;cursor:pointer} +span.checkmark{position:absolute;top:0;left:6px;height:14px;width:14px;background-color:#e3e3e3;border-radius:100%} +label.checkbox:hover input ~ .checkmark{background-color:#b3b3b3} +label.checkbox input:checked ~ .checkmark{background-color:#ff8c2f} +label.checkbox input:disabled ~ .checkmark{opacity:0.5} +a.bannerDismiss {float:right;cursor:pointer;text-decoration:none;margin-right:1rem} +.bannerDismiss::before {content:"\e92f";font-family:Unraid;color:#e68a00} +a.bannerInfo {cursor:pointer;text-decoration:none} +.bannerInfo::before {content:"\f05a";font-family:fontAwesome;color:#e68a00} +::-webkit-scrollbar{width:8px;height:8px;background:transparent} +::-webkit-scrollbar-thumb{background:lightgray;border-radius:10px} +::-webkit-scrollbar-corner{background:lightgray;border-radius:10px} +::-webkit-scrollbar-thumb:hover{background:gray} diff --git a/api/src/unraid-api/unraid-file-modifier/modifications/__test__/__fixtures__/downloaded/default.cfg b/api/src/unraid-api/unraid-file-modifier/modifications/__test__/__fixtures__/downloaded/default.cfg new file mode 100644 index 0000000000..d43fbc22b8 --- /dev/null +++ b/api/src/unraid-api/unraid-file-modifier/modifications/__test__/__fixtures__/downloaded/default.cfg @@ -0,0 +1,82 @@ +[confirm] +down="1" +stop="1" +[display] +width="" +font="" +tty="15" +date="%c" +time="%R" +number=".," +unit="C" +scale="-1" +resize="0" +wwn="0" +total="1" +banner="" +header="" +background="" +tabs="1" +users="Tasks:3" +usage="0" +text="1" +warning="70" +critical="90" +hot="45" +max="55" +hotssd="60" +maxssd="70" +power="" +theme="white" +locale="" +raw="" +rtl="" +headermetacolor="" +headerdescription="yes" +showBannerGradient="yes" +favorites="yes" +liveUpdate="yes" +[parity] +mode="0" +hour="0 0" +dotm="1" +month="1" +day="0" +cron="" +write="NOCORRECT" +[notify] +display="0" +life="5" +date="d-m-Y" +time="H:i" +position="top-right" +path="/tmp/notifications" +system="*/1 * * * *" +entity="1" +normal="1" +warning="1" +alert="1" +unraid="1" +plugin="1" +docker_notify="1" +language_notify="1" +report="1" +unraidos="" +version="" +docker_update="" +language_update="" +status="" +[ssmtp] +root="" +RcptTo="" +SetEmailPriority="True" +Subject="Unraid Status: " +server="smtp.gmail.com" +port="465" +UseTLS="YES" +UseSTARTTLS="NO" +UseTLSCert="NO" +TLSCert="" +AuthMethod="login" +AuthUser="" +AuthPass="" diff --git a/api/src/unraid-api/unraid-file-modifier/modifications/__test__/__fixtures__/downloaded/font-awesome.css b/api/src/unraid-api/unraid-file-modifier/modifications/__test__/__fixtures__/downloaded/font-awesome.css new file mode 100644 index 0000000000..e0852f1cc9 --- /dev/null +++ b/api/src/unraid-api/unraid-file-modifier/modifications/__test__/__fixtures__/downloaded/font-awesome.css @@ -0,0 +1,5 @@ +/* Font Awesome 4.7.0 by @davegandy - http://fontawesome.io - @fontawesome + * License - http://fontawesome.io/license (Font: SIL OFL 1.1, CSS: MIT License) + */ +@font-face{font-family:'FontAwesome';font-weight:normal;font-style:normal;src:url('/webGui/styles/font-awesome.woff?v=220508') format('woff')} +.fa{display:inline-block;font:normal normal normal 14px/1 FontAwesome;font-size:inherit;text-rendering:auto;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.fa-lg{font-size:1.33333333em;line-height:.75em;vertical-align:-15%}.fa-2x{font-size:2em}.fa-3x{font-size:3em}.fa-4x{font-size:4em}.fa-5x{font-size:5em}.fa-fw{width:1.28571429em;text-align:center}.fa-ul{padding-left:0;margin-left:2.14285714em;list-style-type:none}.fa-ul>li{position:relative}.fa-li{position:absolute;left:-2.14285714em;width:2.14285714em;top:.14285714em;text-align:center}.fa-li.fa-lg{left:-1.85714286em}.fa-border{padding:.2em .25em .15em;border:solid .08em #eee;border-radius:.1em}.fa-pull-left{float:left}.fa-pull-right{float:right}.fa.fa-pull-left{margin-right:.3em}.fa.fa-pull-right{margin-left:.3em}.pull-right{float:right}.pull-left{float:left}.fa.pull-left{margin-right:.3em}.fa.pull-right{margin-left:.3em}.fa-spin{-webkit-animation:fa-spin 2s infinite linear;animation:fa-spin 2s infinite linear}.fa-pulse{-webkit-animation:fa-spin 1s infinite steps(8);animation:fa-spin 1s infinite steps(8)}@-webkit-keyframes fa-spin{0%{-webkit-transform:rotate(0deg);transform:rotate(0deg)}100%{-webkit-transform:rotate(359deg);transform:rotate(359deg)}}@keyframes fa-spin{0%{-webkit-transform:rotate(0deg);transform:rotate(0deg)}100%{-webkit-transform:rotate(359deg);transform:rotate(359deg)}}.fa-rotate-90{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=1)";-webkit-transform:rotate(90deg);-ms-transform:rotate(90deg);transform:rotate(90deg)}.fa-rotate-180{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=2)";-webkit-transform:rotate(180deg);-ms-transform:rotate(180deg);transform:rotate(180deg)}.fa-rotate-270{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=3)";-webkit-transform:rotate(270deg);-ms-transform:rotate(270deg);transform:rotate(270deg)}.fa-flip-horizontal{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=0, mirror=1)";-webkit-transform:scale(-1, 1);-ms-transform:scale(-1, 1);transform:scale(-1, 1)}.fa-flip-vertical{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=2, mirror=1)";-webkit-transform:scale(1, -1);-ms-transform:scale(1, -1);transform:scale(1, -1)}:root .fa-rotate-90,:root .fa-rotate-180,:root .fa-rotate-270,:root .fa-flip-horizontal,:root .fa-flip-vertical{filter:none}.fa-stack{position:relative;display:inline-block;width:2em;height:2em;line-height:2em;vertical-align:middle}.fa-stack-1x,.fa-stack-2x{position:absolute;left:0;width:100%;text-align:center}.fa-stack-1x{line-height:inherit}.fa-stack-2x{font-size:2em}.fa-inverse{color:#fff}.fa-glass:before{content:"\f000"}.fa-music:before{content:"\f001"}.fa-search:before{content:"\f002"}.fa-envelope-o:before{content:"\f003"}.fa-heart:before{content:"\f004"}.fa-star:before{content:"\f005"}.fa-star-o:before{content:"\f006"}.fa-user:before{content:"\f007"}.fa-film:before{content:"\f008"}.fa-th-large:before{content:"\f009"}.fa-th:before{content:"\f00a"}.fa-th-list:before{content:"\f00b"}.fa-check:before{content:"\f00c"}.fa-remove:before,.fa-close:before,.fa-times:before{content:"\f00d"}.fa-search-plus:before{content:"\f00e"}.fa-search-minus:before{content:"\f010"}.fa-power-off:before{content:"\f011"}.fa-signal:before{content:"\f012"}.fa-gear:before,.fa-cog:before{content:"\f013"}.fa-trash-o:before{content:"\f014"}.fa-home:before{content:"\f015"}.fa-file-o:before{content:"\f016"}.fa-clock-o:before{content:"\f017"}.fa-road:before{content:"\f018"}.fa-download:before{content:"\f019"}.fa-arrow-circle-o-down:before{content:"\f01a"}.fa-arrow-circle-o-up:before{content:"\f01b"}.fa-inbox:before{content:"\f01c"}.fa-play-circle-o:before{content:"\f01d"}.fa-rotate-right:before,.fa-repeat:before{content:"\f01e"}.fa-refresh:before{content:"\f021"}.fa-list-alt:before{content:"\f022"}.fa-lock:before{content:"\f023"}.fa-flag:before{content:"\f024"}.fa-headphones:before{content:"\f025"}.fa-volume-off:before{content:"\f026"}.fa-volume-down:before{content:"\f027"}.fa-volume-up:before{content:"\f028"}.fa-qrcode:before{content:"\f029"}.fa-barcode:before{content:"\f02a"}.fa-tag:before{content:"\f02b"}.fa-tags:before{content:"\f02c"}.fa-book:before{content:"\f02d"}.fa-bookmark:before{content:"\f02e"}.fa-print:before{content:"\f02f"}.fa-camera:before{content:"\f030"}.fa-font:before{content:"\f031"}.fa-bold:before{content:"\f032"}.fa-italic:before{content:"\f033"}.fa-text-height:before{content:"\f034"}.fa-text-width:before{content:"\f035"}.fa-align-left:before{content:"\f036"}.fa-align-center:before{content:"\f037"}.fa-align-right:before{content:"\f038"}.fa-align-justify:before{content:"\f039"}.fa-list:before{content:"\f03a"}.fa-dedent:before,.fa-outdent:before{content:"\f03b"}.fa-indent:before{content:"\f03c"}.fa-video-camera:before{content:"\f03d"}.fa-photo:before,.fa-image:before,.fa-picture-o:before{content:"\f03e"}.fa-pencil:before{content:"\f040"}.fa-map-marker:before{content:"\f041"}.fa-adjust:before{content:"\f042"}.fa-tint:before{content:"\f043"}.fa-edit:before,.fa-pencil-square-o:before{content:"\f044"}.fa-share-square-o:before{content:"\f045"}.fa-check-square-o:before{content:"\f046"}.fa-arrows:before{content:"\f047"}.fa-step-backward:before{content:"\f048"}.fa-fast-backward:before{content:"\f049"}.fa-backward:before{content:"\f04a"}.fa-play:before{content:"\f04b"}.fa-pause:before{content:"\f04c"}.fa-stop:before{content:"\f04d"}.fa-forward:before{content:"\f04e"}.fa-fast-forward:before{content:"\f050"}.fa-step-forward:before{content:"\f051"}.fa-eject:before{content:"\f052"}.fa-chevron-left:before{content:"\f053"}.fa-chevron-right:before{content:"\f054"}.fa-plus-circle:before{content:"\f055"}.fa-minus-circle:before{content:"\f056"}.fa-times-circle:before{content:"\f057"}.fa-check-circle:before{content:"\f058"}.fa-question-circle:before{content:"\f059"}.fa-info-circle:before{content:"\f05a"}.fa-crosshairs:before{content:"\f05b"}.fa-times-circle-o:before{content:"\f05c"}.fa-check-circle-o:before{content:"\f05d"}.fa-ban:before{content:"\f05e"}.fa-arrow-left:before{content:"\f060"}.fa-arrow-right:before{content:"\f061"}.fa-arrow-up:before{content:"\f062"}.fa-arrow-down:before{content:"\f063"}.fa-mail-forward:before,.fa-share:before{content:"\f064"}.fa-expand:before{content:"\f065"}.fa-compress:before{content:"\f066"}.fa-plus:before{content:"\f067"}.fa-minus:before{content:"\f068"}.fa-asterisk:before{content:"\f069"}.fa-exclamation-circle:before{content:"\f06a"}.fa-gift:before{content:"\f06b"}.fa-leaf:before{content:"\f06c"}.fa-fire:before{content:"\f06d"}.fa-eye:before{content:"\f06e"}.fa-eye-slash:before{content:"\f070"}.fa-warning:before,.fa-exclamation-triangle:before{content:"\f071"}.fa-plane:before{content:"\f072"}.fa-calendar:before{content:"\f073"}.fa-random:before{content:"\f074"}.fa-comment:before{content:"\f075"}.fa-magnet:before{content:"\f076"}.fa-chevron-up:before{content:"\f077"}.fa-chevron-down:before{content:"\f078"}.fa-retweet:before{content:"\f079"}.fa-shopping-cart:before{content:"\f07a"}.fa-folder:before{content:"\f07b"}.fa-folder-open:before{content:"\f07c"}.fa-arrows-v:before{content:"\f07d"}.fa-arrows-h:before{content:"\f07e"}.fa-bar-chart-o:before,.fa-bar-chart:before{content:"\f080"}.fa-twitter-square:before{content:"\f081"}.fa-facebook-square:before{content:"\f082"}.fa-camera-retro:before{content:"\f083"}.fa-key:before{content:"\f084"}.fa-gears:before,.fa-cogs:before{content:"\f085"}.fa-comments:before{content:"\f086"}.fa-thumbs-o-up:before{content:"\f087"}.fa-thumbs-o-down:before{content:"\f088"}.fa-star-half:before{content:"\f089"}.fa-heart-o:before{content:"\f08a"}.fa-sign-out:before{content:"\f08b"}.fa-linkedin-square:before{content:"\f08c"}.fa-thumb-tack:before{content:"\f08d"}.fa-external-link:before{content:"\f08e"}.fa-sign-in:before{content:"\f090"}.fa-trophy:before{content:"\f091"}.fa-github-square:before{content:"\f092"}.fa-upload:before{content:"\f093"}.fa-lemon-o:before{content:"\f094"}.fa-phone:before{content:"\f095"}.fa-square-o:before{content:"\f096"}.fa-bookmark-o:before{content:"\f097"}.fa-phone-square:before{content:"\f098"}.fa-twitter:before{content:"\f099"}.fa-facebook-f:before,.fa-facebook:before{content:"\f09a"}.fa-github:before{content:"\f09b"}.fa-unlock:before{content:"\f09c"}.fa-credit-card:before{content:"\f09d"}.fa-feed:before,.fa-rss:before{content:"\f09e"}.fa-hdd-o:before{content:"\f0a0"}.fa-bullhorn:before{content:"\f0a1"}.fa-bell:before{content:"\f0f3"}.fa-certificate:before{content:"\f0a3"}.fa-hand-o-right:before{content:"\f0a4"}.fa-hand-o-left:before{content:"\f0a5"}.fa-hand-o-up:before{content:"\f0a6"}.fa-hand-o-down:before{content:"\f0a7"}.fa-arrow-circle-left:before{content:"\f0a8"}.fa-arrow-circle-right:before{content:"\f0a9"}.fa-arrow-circle-up:before{content:"\f0aa"}.fa-arrow-circle-down:before{content:"\f0ab"}.fa-globe:before{content:"\f0ac"}.fa-wrench:before{content:"\f0ad"}.fa-tasks:before{content:"\f0ae"}.fa-filter:before{content:"\f0b0"}.fa-briefcase:before{content:"\f0b1"}.fa-arrows-alt:before{content:"\f0b2"}.fa-group:before,.fa-users:before{content:"\f0c0"}.fa-chain:before,.fa-link:before{content:"\f0c1"}.fa-cloud:before{content:"\f0c2"}.fa-flask:before{content:"\f0c3"}.fa-cut:before,.fa-scissors:before{content:"\f0c4"}.fa-copy:before,.fa-files-o:before{content:"\f0c5"}.fa-paperclip:before{content:"\f0c6"}.fa-save:before,.fa-floppy-o:before{content:"\f0c7"}.fa-square:before{content:"\f0c8"}.fa-navicon:before,.fa-reorder:before,.fa-bars:before{content:"\f0c9"}.fa-list-ul:before{content:"\f0ca"}.fa-list-ol:before{content:"\f0cb"}.fa-strikethrough:before{content:"\f0cc"}.fa-underline:before{content:"\f0cd"}.fa-table:before{content:"\f0ce"}.fa-magic:before{content:"\f0d0"}.fa-truck:before{content:"\f0d1"}.fa-pinterest:before{content:"\f0d2"}.fa-pinterest-square:before{content:"\f0d3"}.fa-google-plus-square:before{content:"\f0d4"}.fa-google-plus:before{content:"\f0d5"}.fa-money:before{content:"\f0d6"}.fa-caret-down:before{content:"\f0d7"}.fa-caret-up:before{content:"\f0d8"}.fa-caret-left:before{content:"\f0d9"}.fa-caret-right:before{content:"\f0da"}.fa-columns:before{content:"\f0db"}.fa-unsorted:before,.fa-sort:before{content:"\f0dc"}.fa-sort-down:before,.fa-sort-desc:before{content:"\f0dd"}.fa-sort-up:before,.fa-sort-asc:before{content:"\f0de"}.fa-envelope:before{content:"\f0e0"}.fa-linkedin:before{content:"\f0e1"}.fa-rotate-left:before,.fa-undo:before{content:"\f0e2"}.fa-legal:before,.fa-gavel:before{content:"\f0e3"}.fa-dashboard:before,.fa-tachometer:before{content:"\f0e4"}.fa-comment-o:before{content:"\f0e5"}.fa-comments-o:before{content:"\f0e6"}.fa-flash:before,.fa-bolt:before{content:"\f0e7"}.fa-sitemap:before{content:"\f0e8"}.fa-umbrella:before{content:"\f0e9"}.fa-paste:before,.fa-clipboard:before{content:"\f0ea"}.fa-lightbulb-o:before{content:"\f0eb"}.fa-exchange:before{content:"\f0ec"}.fa-cloud-download:before{content:"\f0ed"}.fa-cloud-upload:before{content:"\f0ee"}.fa-user-md:before{content:"\f0f0"}.fa-stethoscope:before{content:"\f0f1"}.fa-suitcase:before{content:"\f0f2"}.fa-bell-o:before{content:"\f0a2"}.fa-coffee:before{content:"\f0f4"}.fa-cutlery:before{content:"\f0f5"}.fa-file-text-o:before{content:"\f0f6"}.fa-building-o:before{content:"\f0f7"}.fa-hospital-o:before{content:"\f0f8"}.fa-ambulance:before{content:"\f0f9"}.fa-medkit:before{content:"\f0fa"}.fa-fighter-jet:before{content:"\f0fb"}.fa-beer:before{content:"\f0fc"}.fa-h-square:before{content:"\f0fd"}.fa-plus-square:before{content:"\f0fe"}.fa-angle-double-left:before{content:"\f100"}.fa-angle-double-right:before{content:"\f101"}.fa-angle-double-up:before{content:"\f102"}.fa-angle-double-down:before{content:"\f103"}.fa-angle-left:before{content:"\f104"}.fa-angle-right:before{content:"\f105"}.fa-angle-up:before{content:"\f106"}.fa-angle-down:before{content:"\f107"}.fa-desktop:before{content:"\f108"}.fa-laptop:before{content:"\f109"}.fa-tablet:before{content:"\f10a"}.fa-mobile-phone:before,.fa-mobile:before{content:"\f10b"}.fa-circle-o:before{content:"\f10c"}.fa-quote-left:before{content:"\f10d"}.fa-quote-right:before{content:"\f10e"}.fa-spinner:before{content:"\f110"}.fa-circle:before{content:"\f111"}.fa-mail-reply:before,.fa-reply:before{content:"\f112"}.fa-github-alt:before{content:"\f113"}.fa-folder-o:before{content:"\f114"}.fa-folder-open-o:before{content:"\f115"}.fa-smile-o:before{content:"\f118"}.fa-frown-o:before{content:"\f119"}.fa-meh-o:before{content:"\f11a"}.fa-gamepad:before{content:"\f11b"}.fa-keyboard-o:before{content:"\f11c"}.fa-flag-o:before{content:"\f11d"}.fa-flag-checkered:before{content:"\f11e"}.fa-terminal:before{content:"\f120"}.fa-code:before{content:"\f121"}.fa-mail-reply-all:before,.fa-reply-all:before{content:"\f122"}.fa-star-half-empty:before,.fa-star-half-full:before,.fa-star-half-o:before{content:"\f123"}.fa-location-arrow:before{content:"\f124"}.fa-crop:before{content:"\f125"}.fa-code-fork:before{content:"\f126"}.fa-unlink:before,.fa-chain-broken:before{content:"\f127"}.fa-question:before{content:"\f128"}.fa-info:before{content:"\f129"}.fa-exclamation:before{content:"\f12a"}.fa-superscript:before{content:"\f12b"}.fa-subscript:before{content:"\f12c"}.fa-eraser:before{content:"\f12d"}.fa-puzzle-piece:before{content:"\f12e"}.fa-microphone:before{content:"\f130"}.fa-microphone-slash:before{content:"\f131"}.fa-shield:before{content:"\f132"}.fa-calendar-o:before{content:"\f133"}.fa-fire-extinguisher:before{content:"\f134"}.fa-rocket:before{content:"\f135"}.fa-maxcdn:before{content:"\f136"}.fa-chevron-circle-left:before{content:"\f137"}.fa-chevron-circle-right:before{content:"\f138"}.fa-chevron-circle-up:before{content:"\f139"}.fa-chevron-circle-down:before{content:"\f13a"}.fa-html5:before{content:"\f13b"}.fa-css3:before{content:"\f13c"}.fa-anchor:before{content:"\f13d"}.fa-unlock-alt:before{content:"\f13e"}.fa-bullseye:before{content:"\f140"}.fa-ellipsis-h:before{content:"\f141"}.fa-ellipsis-v:before{content:"\f142"}.fa-rss-square:before{content:"\f143"}.fa-play-circle:before{content:"\f144"}.fa-ticket:before{content:"\f145"}.fa-minus-square:before{content:"\f146"}.fa-minus-square-o:before{content:"\f147"}.fa-level-up:before{content:"\f148"}.fa-level-down:before{content:"\f149"}.fa-check-square:before{content:"\f14a"}.fa-pencil-square:before{content:"\f14b"}.fa-external-link-square:before{content:"\f14c"}.fa-share-square:before{content:"\f14d"}.fa-compass:before{content:"\f14e"}.fa-toggle-down:before,.fa-caret-square-o-down:before{content:"\f150"}.fa-toggle-up:before,.fa-caret-square-o-up:before{content:"\f151"}.fa-toggle-right:before,.fa-caret-square-o-right:before{content:"\f152"}.fa-euro:before,.fa-eur:before{content:"\f153"}.fa-gbp:before{content:"\f154"}.fa-dollar:before,.fa-usd:before{content:"\f155"}.fa-rupee:before,.fa-inr:before{content:"\f156"}.fa-cny:before,.fa-rmb:before,.fa-yen:before,.fa-jpy:before{content:"\f157"}.fa-ruble:before,.fa-rouble:before,.fa-rub:before{content:"\f158"}.fa-won:before,.fa-krw:before{content:"\f159"}.fa-bitcoin:before,.fa-btc:before{content:"\f15a"}.fa-file:before{content:"\f15b"}.fa-file-text:before{content:"\f15c"}.fa-sort-alpha-asc:before{content:"\f15d"}.fa-sort-alpha-desc:before{content:"\f15e"}.fa-sort-amount-asc:before{content:"\f160"}.fa-sort-amount-desc:before{content:"\f161"}.fa-sort-numeric-asc:before{content:"\f162"}.fa-sort-numeric-desc:before{content:"\f163"}.fa-thumbs-up:before{content:"\f164"}.fa-thumbs-down:before{content:"\f165"}.fa-youtube-square:before{content:"\f166"}.fa-youtube:before{content:"\f167"}.fa-xing:before{content:"\f168"}.fa-xing-square:before{content:"\f169"}.fa-youtube-play:before{content:"\f16a"}.fa-dropbox:before{content:"\f16b"}.fa-stack-overflow:before{content:"\f16c"}.fa-instagram:before{content:"\f16d"}.fa-flickr:before{content:"\f16e"}.fa-adn:before{content:"\f170"}.fa-bitbucket:before{content:"\f171"}.fa-bitbucket-square:before{content:"\f172"}.fa-tumblr:before{content:"\f173"}.fa-tumblr-square:before{content:"\f174"}.fa-long-arrow-down:before{content:"\f175"}.fa-long-arrow-up:before{content:"\f176"}.fa-long-arrow-left:before{content:"\f177"}.fa-long-arrow-right:before{content:"\f178"}.fa-apple:before{content:"\f179"}.fa-windows:before{content:"\f17a"}.fa-android:before{content:"\f17b"}.fa-linux:before{content:"\f17c"}.fa-dribbble:before{content:"\f17d"}.fa-skype:before{content:"\f17e"}.fa-foursquare:before{content:"\f180"}.fa-trello:before{content:"\f181"}.fa-female:before{content:"\f182"}.fa-male:before{content:"\f183"}.fa-gittip:before,.fa-gratipay:before{content:"\f184"}.fa-sun-o:before{content:"\f185"}.fa-moon-o:before{content:"\f186"}.fa-archive:before{content:"\f187"}.fa-bug:before{content:"\f188"}.fa-vk:before{content:"\f189"}.fa-weibo:before{content:"\f18a"}.fa-renren:before{content:"\f18b"}.fa-pagelines:before{content:"\f18c"}.fa-stack-exchange:before{content:"\f18d"}.fa-arrow-circle-o-right:before{content:"\f18e"}.fa-arrow-circle-o-left:before{content:"\f190"}.fa-toggle-left:before,.fa-caret-square-o-left:before{content:"\f191"}.fa-dot-circle-o:before{content:"\f192"}.fa-wheelchair:before{content:"\f193"}.fa-vimeo-square:before{content:"\f194"}.fa-turkish-lira:before,.fa-try:before{content:"\f195"}.fa-plus-square-o:before{content:"\f196"}.fa-space-shuttle:before{content:"\f197"}.fa-slack:before{content:"\f198"}.fa-envelope-square:before{content:"\f199"}.fa-wordpress:before{content:"\f19a"}.fa-openid:before{content:"\f19b"}.fa-institution:before,.fa-bank:before,.fa-university:before{content:"\f19c"}.fa-mortar-board:before,.fa-graduation-cap:before{content:"\f19d"}.fa-yahoo:before{content:"\f19e"}.fa-google:before{content:"\f1a0"}.fa-reddit:before{content:"\f1a1"}.fa-reddit-square:before{content:"\f1a2"}.fa-stumbleupon-circle:before{content:"\f1a3"}.fa-stumbleupon:before{content:"\f1a4"}.fa-delicious:before{content:"\f1a5"}.fa-digg:before{content:"\f1a6"}.fa-pied-piper-pp:before{content:"\f1a7"}.fa-pied-piper-alt:before{content:"\f1a8"}.fa-drupal:before{content:"\f1a9"}.fa-joomla:before{content:"\f1aa"}.fa-language:before{content:"\f1ab"}.fa-fax:before{content:"\f1ac"}.fa-building:before{content:"\f1ad"}.fa-child:before{content:"\f1ae"}.fa-paw:before{content:"\f1b0"}.fa-spoon:before{content:"\f1b1"}.fa-cube:before{content:"\f1b2"}.fa-cubes:before{content:"\f1b3"}.fa-behance:before{content:"\f1b4"}.fa-behance-square:before{content:"\f1b5"}.fa-steam:before{content:"\f1b6"}.fa-steam-square:before{content:"\f1b7"}.fa-recycle:before{content:"\f1b8"}.fa-automobile:before,.fa-car:before{content:"\f1b9"}.fa-cab:before,.fa-taxi:before{content:"\f1ba"}.fa-tree:before{content:"\f1bb"}.fa-spotify:before{content:"\f1bc"}.fa-deviantart:before{content:"\f1bd"}.fa-soundcloud:before{content:"\f1be"}.fa-database:before{content:"\f1c0"}.fa-file-pdf-o:before{content:"\f1c1"}.fa-file-word-o:before{content:"\f1c2"}.fa-file-excel-o:before{content:"\f1c3"}.fa-file-powerpoint-o:before{content:"\f1c4"}.fa-file-photo-o:before,.fa-file-picture-o:before,.fa-file-image-o:before{content:"\f1c5"}.fa-file-zip-o:before,.fa-file-archive-o:before{content:"\f1c6"}.fa-file-sound-o:before,.fa-file-audio-o:before{content:"\f1c7"}.fa-file-movie-o:before,.fa-file-video-o:before{content:"\f1c8"}.fa-file-code-o:before{content:"\f1c9"}.fa-vine:before{content:"\f1ca"}.fa-codepen:before{content:"\f1cb"}.fa-jsfiddle:before{content:"\f1cc"}.fa-life-bouy:before,.fa-life-buoy:before,.fa-life-saver:before,.fa-support:before,.fa-life-ring:before{content:"\f1cd"}.fa-circle-o-notch:before{content:"\f1ce"}.fa-ra:before,.fa-resistance:before,.fa-rebel:before{content:"\f1d0"}.fa-ge:before,.fa-empire:before{content:"\f1d1"}.fa-git-square:before{content:"\f1d2"}.fa-git:before{content:"\f1d3"}.fa-y-combinator-square:before,.fa-yc-square:before,.fa-hacker-news:before{content:"\f1d4"}.fa-tencent-weibo:before{content:"\f1d5"}.fa-qq:before{content:"\f1d6"}.fa-wechat:before,.fa-weixin:before{content:"\f1d7"}.fa-send:before,.fa-paper-plane:before{content:"\f1d8"}.fa-send-o:before,.fa-paper-plane-o:before{content:"\f1d9"}.fa-history:before{content:"\f1da"}.fa-circle-thin:before{content:"\f1db"}.fa-header:before{content:"\f1dc"}.fa-paragraph:before{content:"\f1dd"}.fa-sliders:before{content:"\f1de"}.fa-share-alt:before{content:"\f1e0"}.fa-share-alt-square:before{content:"\f1e1"}.fa-bomb:before{content:"\f1e2"}.fa-soccer-ball-o:before,.fa-futbol-o:before{content:"\f1e3"}.fa-tty:before{content:"\f1e4"}.fa-binoculars:before{content:"\f1e5"}.fa-plug:before{content:"\f1e6"}.fa-slideshare:before{content:"\f1e7"}.fa-twitch:before{content:"\f1e8"}.fa-yelp:before{content:"\f1e9"}.fa-newspaper-o:before{content:"\f1ea"}.fa-wifi:before{content:"\f1eb"}.fa-calculator:before{content:"\f1ec"}.fa-paypal:before{content:"\f1ed"}.fa-google-wallet:before{content:"\f1ee"}.fa-cc-visa:before{content:"\f1f0"}.fa-cc-mastercard:before{content:"\f1f1"}.fa-cc-discover:before{content:"\f1f2"}.fa-cc-amex:before{content:"\f1f3"}.fa-cc-paypal:before{content:"\f1f4"}.fa-cc-stripe:before{content:"\f1f5"}.fa-bell-slash:before{content:"\f1f6"}.fa-bell-slash-o:before{content:"\f1f7"}.fa-trash:before{content:"\f1f8"}.fa-copyright:before{content:"\f1f9"}.fa-at:before{content:"\f1fa"}.fa-eyedropper:before{content:"\f1fb"}.fa-paint-brush:before{content:"\f1fc"}.fa-birthday-cake:before{content:"\f1fd"}.fa-area-chart:before{content:"\f1fe"}.fa-pie-chart:before{content:"\f200"}.fa-line-chart:before{content:"\f201"}.fa-lastfm:before{content:"\f202"}.fa-lastfm-square:before{content:"\f203"}.fa-toggle-off:before{content:"\f204"}.fa-toggle-on:before{content:"\f205"}.fa-bicycle:before{content:"\f206"}.fa-bus:before{content:"\f207"}.fa-ioxhost:before{content:"\f208"}.fa-angellist:before{content:"\f209"}.fa-cc:before{content:"\f20a"}.fa-shekel:before,.fa-sheqel:before,.fa-ils:before{content:"\f20b"}.fa-meanpath:before{content:"\f20c"}.fa-buysellads:before{content:"\f20d"}.fa-connectdevelop:before{content:"\f20e"}.fa-dashcube:before{content:"\f210"}.fa-forumbee:before{content:"\f211"}.fa-leanpub:before{content:"\f212"}.fa-sellsy:before{content:"\f213"}.fa-shirtsinbulk:before{content:"\f214"}.fa-simplybuilt:before{content:"\f215"}.fa-skyatlas:before{content:"\f216"}.fa-cart-plus:before{content:"\f217"}.fa-cart-arrow-down:before{content:"\f218"}.fa-diamond:before{content:"\f219"}.fa-ship:before{content:"\f21a"}.fa-user-secret:before{content:"\f21b"}.fa-motorcycle:before{content:"\f21c"}.fa-street-view:before{content:"\f21d"}.fa-heartbeat:before{content:"\f21e"}.fa-venus:before{content:"\f221"}.fa-mars:before{content:"\f222"}.fa-mercury:before{content:"\f223"}.fa-intersex:before,.fa-transgender:before{content:"\f224"}.fa-transgender-alt:before{content:"\f225"}.fa-venus-double:before{content:"\f226"}.fa-mars-double:before{content:"\f227"}.fa-venus-mars:before{content:"\f228"}.fa-mars-stroke:before{content:"\f229"}.fa-mars-stroke-v:before{content:"\f22a"}.fa-mars-stroke-h:before{content:"\f22b"}.fa-neuter:before{content:"\f22c"}.fa-genderless:before{content:"\f22d"}.fa-facebook-official:before{content:"\f230"}.fa-pinterest-p:before{content:"\f231"}.fa-whatsapp:before{content:"\f232"}.fa-server:before{content:"\f233"}.fa-user-plus:before{content:"\f234"}.fa-user-times:before{content:"\f235"}.fa-hotel:before,.fa-bed:before{content:"\f236"}.fa-viacoin:before{content:"\f237"}.fa-train:before{content:"\f238"}.fa-subway:before{content:"\f239"}.fa-medium:before{content:"\f23a"}.fa-yc:before,.fa-y-combinator:before{content:"\f23b"}.fa-optin-monster:before{content:"\f23c"}.fa-opencart:before{content:"\f23d"}.fa-expeditedssl:before{content:"\f23e"}.fa-battery-4:before,.fa-battery:before,.fa-battery-full:before{content:"\f240"}.fa-battery-3:before,.fa-battery-three-quarters:before{content:"\f241"}.fa-battery-2:before,.fa-battery-half:before{content:"\f242"}.fa-battery-1:before,.fa-battery-quarter:before{content:"\f243"}.fa-battery-0:before,.fa-battery-empty:before{content:"\f244"}.fa-mouse-pointer:before{content:"\f245"}.fa-i-cursor:before{content:"\f246"}.fa-object-group:before{content:"\f247"}.fa-object-ungroup:before{content:"\f248"}.fa-sticky-note:before{content:"\f249"}.fa-sticky-note-o:before{content:"\f24a"}.fa-cc-jcb:before{content:"\f24b"}.fa-cc-diners-club:before{content:"\f24c"}.fa-clone:before{content:"\f24d"}.fa-balance-scale:before{content:"\f24e"}.fa-hourglass-o:before{content:"\f250"}.fa-hourglass-1:before,.fa-hourglass-start:before{content:"\f251"}.fa-hourglass-2:before,.fa-hourglass-half:before{content:"\f252"}.fa-hourglass-3:before,.fa-hourglass-end:before{content:"\f253"}.fa-hourglass:before{content:"\f254"}.fa-hand-grab-o:before,.fa-hand-rock-o:before{content:"\f255"}.fa-hand-stop-o:before,.fa-hand-paper-o:before{content:"\f256"}.fa-hand-scissors-o:before{content:"\f257"}.fa-hand-lizard-o:before{content:"\f258"}.fa-hand-spock-o:before{content:"\f259"}.fa-hand-pointer-o:before{content:"\f25a"}.fa-hand-peace-o:before{content:"\f25b"}.fa-trademark:before{content:"\f25c"}.fa-registered:before{content:"\f25d"}.fa-creative-commons:before{content:"\f25e"}.fa-gg:before{content:"\f260"}.fa-gg-circle:before{content:"\f261"}.fa-tripadvisor:before{content:"\f262"}.fa-odnoklassniki:before{content:"\f263"}.fa-odnoklassniki-square:before{content:"\f264"}.fa-get-pocket:before{content:"\f265"}.fa-wikipedia-w:before{content:"\f266"}.fa-safari:before{content:"\f267"}.fa-chrome:before{content:"\f268"}.fa-firefox:before{content:"\f269"}.fa-opera:before{content:"\f26a"}.fa-internet-explorer:before{content:"\f26b"}.fa-tv:before,.fa-television:before{content:"\f26c"}.fa-contao:before{content:"\f26d"}.fa-500px:before{content:"\f26e"}.fa-amazon:before{content:"\f270"}.fa-calendar-plus-o:before{content:"\f271"}.fa-calendar-minus-o:before{content:"\f272"}.fa-calendar-times-o:before{content:"\f273"}.fa-calendar-check-o:before{content:"\f274"}.fa-industry:before{content:"\f275"}.fa-map-pin:before{content:"\f276"}.fa-map-signs:before{content:"\f277"}.fa-map-o:before{content:"\f278"}.fa-map:before{content:"\f279"}.fa-commenting:before{content:"\f27a"}.fa-commenting-o:before{content:"\f27b"}.fa-houzz:before{content:"\f27c"}.fa-vimeo:before{content:"\f27d"}.fa-black-tie:before{content:"\f27e"}.fa-fonticons:before{content:"\f280"}.fa-reddit-alien:before{content:"\f281"}.fa-edge:before{content:"\f282"}.fa-credit-card-alt:before{content:"\f283"}.fa-codiepie:before{content:"\f284"}.fa-modx:before{content:"\f285"}.fa-fort-awesome:before{content:"\f286"}.fa-usb:before{content:"\f287"}.fa-product-hunt:before{content:"\f288"}.fa-mixcloud:before{content:"\f289"}.fa-scribd:before{content:"\f28a"}.fa-pause-circle:before{content:"\f28b"}.fa-pause-circle-o:before{content:"\f28c"}.fa-stop-circle:before{content:"\f28d"}.fa-stop-circle-o:before{content:"\f28e"}.fa-shopping-bag:before{content:"\f290"}.fa-shopping-basket:before{content:"\f291"}.fa-hashtag:before{content:"\f292"}.fa-bluetooth:before{content:"\f293"}.fa-bluetooth-b:before{content:"\f294"}.fa-percent:before{content:"\f295"}.fa-gitlab:before{content:"\f296"}.fa-wpbeginner:before{content:"\f297"}.fa-wpforms:before{content:"\f298"}.fa-envira:before{content:"\f299"}.fa-universal-access:before{content:"\f29a"}.fa-wheelchair-alt:before{content:"\f29b"}.fa-question-circle-o:before{content:"\f29c"}.fa-blind:before{content:"\f29d"}.fa-audio-description:before{content:"\f29e"}.fa-volume-control-phone:before{content:"\f2a0"}.fa-braille:before{content:"\f2a1"}.fa-assistive-listening-systems:before{content:"\f2a2"}.fa-asl-interpreting:before,.fa-american-sign-language-interpreting:before{content:"\f2a3"}.fa-deafness:before,.fa-hard-of-hearing:before,.fa-deaf:before{content:"\f2a4"}.fa-glide:before{content:"\f2a5"}.fa-glide-g:before{content:"\f2a6"}.fa-signing:before,.fa-sign-language:before{content:"\f2a7"}.fa-low-vision:before{content:"\f2a8"}.fa-viadeo:before{content:"\f2a9"}.fa-viadeo-square:before{content:"\f2aa"}.fa-snapchat:before{content:"\f2ab"}.fa-snapchat-ghost:before{content:"\f2ac"}.fa-snapchat-square:before{content:"\f2ad"}.fa-pied-piper:before{content:"\f2ae"}.fa-first-order:before{content:"\f2b0"}.fa-yoast:before{content:"\f2b1"}.fa-themeisle:before{content:"\f2b2"}.fa-google-plus-circle:before,.fa-google-plus-official:before{content:"\f2b3"}.fa-fa:before,.fa-font-awesome:before{content:"\f2b4"}.fa-handshake-o:before{content:"\f2b5"}.fa-envelope-open:before{content:"\f2b6"}.fa-envelope-open-o:before{content:"\f2b7"}.fa-linode:before{content:"\f2b8"}.fa-address-book:before{content:"\f2b9"}.fa-address-book-o:before{content:"\f2ba"}.fa-vcard:before,.fa-address-card:before{content:"\f2bb"}.fa-vcard-o:before,.fa-address-card-o:before{content:"\f2bc"}.fa-user-circle:before{content:"\f2bd"}.fa-user-circle-o:before{content:"\f2be"}.fa-user-o:before{content:"\f2c0"}.fa-id-badge:before{content:"\f2c1"}.fa-drivers-license:before,.fa-id-card:before{content:"\f2c2"}.fa-drivers-license-o:before,.fa-id-card-o:before{content:"\f2c3"}.fa-quora:before{content:"\f2c4"}.fa-free-code-camp:before{content:"\f2c5"}.fa-telegram:before{content:"\f2c6"}.fa-thermometer-4:before,.fa-thermometer:before,.fa-thermometer-full:before{content:"\f2c7"}.fa-thermometer-3:before,.fa-thermometer-three-quarters:before{content:"\f2c8"}.fa-thermometer-2:before,.fa-thermometer-half:before{content:"\f2c9"}.fa-thermometer-1:before,.fa-thermometer-quarter:before{content:"\f2ca"}.fa-thermometer-0:before,.fa-thermometer-empty:before{content:"\f2cb"}.fa-shower:before{content:"\f2cc"}.fa-bathtub:before,.fa-s15:before,.fa-bath:before{content:"\f2cd"}.fa-podcast:before{content:"\f2ce"}.fa-window-maximize:before{content:"\f2d0"}.fa-window-minimize:before{content:"\f2d1"}.fa-window-restore:before{content:"\f2d2"}.fa-times-rectangle:before,.fa-window-close:before{content:"\f2d3"}.fa-times-rectangle-o:before,.fa-window-close-o:before{content:"\f2d4"}.fa-bandcamp:before{content:"\f2d5"}.fa-grav:before{content:"\f2d6"}.fa-etsy:before{content:"\f2d7"}.fa-imdb:before{content:"\f2d8"}.fa-ravelry:before{content:"\f2d9"}.fa-eercast:before{content:"\f2da"}.fa-microchip:before{content:"\f2db"}.fa-snowflake-o:before{content:"\f2dc"}.fa-superpowers:before{content:"\f2dd"}.fa-wpexplorer:before{content:"\f2de"}.fa-meetup:before{content:"\f2e0"}.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0, 0, 0, 0);border:0}.sr-only-focusable:active,.sr-only-focusable:focus{position:static;width:auto;height:auto;margin:0;overflow:visible;clip:auto} diff --git a/api/src/unraid-api/unraid-file-modifier/modifications/__test__/__fixtures__/downloaded/helptext.txt b/api/src/unraid-api/unraid-file-modifier/modifications/__test__/__fixtures__/downloaded/helptext.txt new file mode 100644 index 0000000000..919a84dc41 --- /dev/null +++ b/api/src/unraid-api/unraid-file-modifier/modifications/__test__/__fixtures__/downloaded/helptext.txt @@ -0,0 +1,2764 @@ +:main_array_devices_help: +**Colored Status Indicator** the significance of the color indicator at the beginning of each line in *Array Devices* is as follows: + +Normal operation, device is active. + +Device is in standby mode (spun-down). + +Device contents emulated. + +Device is disabled, contents emulated. + +New device. + +No device present, position is empty. + +**Identification** is the *signature* that uniquely identifies a storage device. The signature +includes the device model number, serial number, linux device id, and the device size. + +**Temp.** (temperature) is read directly from the device. You configure which units to use on +the [Display Preferences](Settings/DisplaySettings) page. We do not read the temperature of spun-down hard +drives since this typically causes them to spin up; instead we display the `*` symbol. We also +display the `*` symbol for SSD and Flash devices, though sometimes these devices do report a valid +temperature, and sometimes they return the value `0`. + +**Size, Used, Free** reports the total device size, used space, and remaining space for files. These +units are also configured on the [Display Preferences](Settings/DisplaySettings) page. The +amount of space used will be non-zero even for an empty disk due to file system overhead. + +*Note: for a multi-device cache pool, this data is for the entire pool as returned by btrfs.* + +**Reads, Writes** are a count of I/O requests sent to the device I/O drivers. These statistics may +be cleared at any time, refer to the Array Status section below. + +**Errors** counts the number of *unrecoverable* errors reported by the device +I/O drivers. Missing data due to unrecoverable array read errors is filled in on-the-fly using parity +reconstruct (and we attempt to write this data back to the sector(s) which failed). Any unrecoverable +write error results in *disabling* the disk. + +**FS** indicates the file system detected in partition 1 of the device. + +**View** column contains a folder icon indicating the device is *mounted*. Click the icon to +browse the file system. + +If "Display array totals" is enable on the [Display Preferences](Settings/DisplaySettings) page, a +**Total** line is included which provides a tally of the device statistics, including the average temperature +of your devices. + +The Array must be Stopped in order to change Array device assignments. + +An unRAID array consists of one or two Parity disks and a number of Data disks. The Data +disks are exclusively used to store user data, and the Parity disk(s) provides the redundancy necessary +to recover from disk failures. + +Since data is not striped across the array, the Parity disk(s) must be as large, or larger than the largest Data +disk. Parity should also be your highest performance drive. + +Each Data disk has its own file system and can be exported as a +separate share. + +Click on the Device name to configure individual device settings and launch certain utilities. +:end + +:main_slots_help: +**Slots** select the number of device slots in your server designated for Array devices. +The minimum number of Array slots is 2, and you must have at least one device assigned to the array. +:end + +:cache_devices_help: +**Colored Status Indicator** the significance of the color indicator at the beginning of each line in *Pool Devices* is as follows: + +Normal operation, device is active. + +Device is in standby mode (spun-down). + +New device. + +No device present, position is empty. + +**Pool Devices** is a single device, or pool of multiple devices, *outside* the unRAID array. It may be exported for network access just +like an Array device. Being outside the unRAID array results in significantly faster write access. + +There are two ways to configure the Pool devices: + +1. As a single device, or +2. As a multi-device pool. + +When configured as a single device you may format the device using any supported file system (btrfs, reiserfs, +or xfs). This configuration offers the highest performance, but at the cost of no data protection - if the +single pool device fails all data contained on it may be lost. + +When configured as a multi-device pool, Unraid OS will automatically select *btrfs-raid1* format (for both data +and meta-data). btrfs permits any number of devices to be added to the pool and each copy of data is guaranteed +to be written to two different devices. Hence the pool can withstand a single-disk failure without losing data. + +When [User Shares](/Settings/ShareSettings) are enabled, user shares may be configured to +automatically make use of the Pool device in order to +speed up writes. A special background process called the *mover* can be scheduled to run +periodically to move user share files off the Cache and onto the Array. +:end + +:cache_slots_help: +**Slots** select the number of device slots in your server designated for Cache devices. +:end + +:boot_device_help: +Vital array configuration is maintained on the USB Flash device; for this reason, it must remain +plugged in to your server. Click on [Flash](/Main/Flash?name=flash) to see the GUID and registration +information, and to configure export settings. Since the USB Flash device is formatted using FAT file system, +it may only be exported using SMB protocol. +:end + +:array_status_help: +**Colored Status Indicator** the significance of the color indicator of the *Array* is as follows: + +Array is Started and Parity is valid. + +Array is Stopped, Parity is valid. + +Array is Started, but Parity is invalid. + +Array is Stopped, Parity is invalid. + +:end + +:array_devices_help: +#### Assigning Devices + +An unRAID disk array consists of a number of Data disks and up to two Parity disks. The data +disks are exclusively used to store user data, and the Parity disk(s) provides the redundancy necessary +to recover from any single or double disk failure. + +Note that we are careful to use the term *disk* when referring to an array storage device. We +use the term *hard drive* (or sometimes just *drive*) when referring to an actual hard disk drive (HDD) +device. This is because in a RAID system it is possible to read/write an array disk whose corresponding +hard drive is disabled or even missing! In addition, it is useful to be able to ask, "which device is +assigned to be the Parity disk?"; or, "which device corresponds to disk2?". + +We therefore need a way to assign hard drives to array disks. This is accomplished here on the +Main page when the array is stopped. There is a drop-down box for each array disk which lists all the +unassigned devices. To assign a device simply select it from the list. Each time a device +assignment is made, the system updates a configuration file to record the assignment. + +#### Requirements + +Unlike traditional RAID systems which stripe data across all the array devices, an Unraid server +stores files on individual hard drives. Consequently, all file write operations will involve both the +Data disk the file is being written to, and the Parity disk(s). For these reasons, + +* a Parity disk size must be as large or larger than any of the Data disks, + +and + +* given a choice, Parity disk(s) should be the fastest disk(s) in your collection. + +#### Guidelines + +Here are the steps you should follow when designing your unRAID disk array: + +1. Decide which hard drive you will use for parity, and which hard drives you will use for +Data disk1, disk2, etc., and label them in some fashion. Also, find the serial number of each hard +drive and jot it down somewhere; you will need this information later. + +2. Install your hard drive devices, boot your server and bring up the webGUI. If this is a fresh system +build, then the Main page will show no disks installed. This doesn't mean the system can't detect your +hard drives; it just means that none have been assigned yet. + +3. Remember the serial numbers you recorded back in step 1? For parity and each Data disk, select the +proper hard drive based on its serial number from the drop down list. + +#### Hot Plug + +You may also *hot plug* hard drives into your server if your hardware supports it. For example, +if you are using hard drive cages, you may simply plug them into your server while powered on and +with array Stopped. Refresh the Main page to have new unassigned devices appear in the assignment +dropdown lists. + +#### Next Steps + +Once you have assigned all of your hard drives, refer to the Array Status section below +and Start the array. +:end + +:encryption_help: +#### Encryption input + +With array Stopped, the user can specify a new encryption key. Note that once a device +is formatted with a particular key it may only be opened using that same key. Changing the encryption key requires +encrypted devices to be reformatted **resulting in permanent loss of all existing data on those devices.** + +#### Passphrase + +Enter a passphrase of up to 512 characters. It is highly advisable to only use the 95 printable characters from the +first 128 characters of the [ASCII table](https://en.wikipedia.org/wiki/ASCII), as they will always have the same binary +representation. Other characters may have different encoding depending on system configuration and your passphrase will +not work with a different encoding. If you want a longer passphrase or to include binary data, upload a keyfile instead. + +Please refer to the [cryptsetup FAQ](https://gitlab.com/cryptsetup/cryptsetup/wikis/FrequentlyAskedQuestions#5-security-aspects) +for what constitutes a *secure* passphrase. + +**Memorize** this passphrase. **IF LOST, ENCRYPTED CONTENT CANNOT BE RECOVERED!** + +#### Keyfile + +Select a local keyfile with a stored encryption key or a binary file. The maximum size of the keyfile is 8M (8388608 byte). + +**Backup** your local keyfile. **IF LOST, ENCRYPTED CONTENT CANNOT BE RECOVERED!** +:end + +:info_warning_temp_help: +*Warning disk temperature* sets the warning threshold for this hard disk temperature. Exceeding this threshold will result in a warning notification. + +A value of zero will disable the warning threshold (including notifications). +:end + +:info_critical_temp_help: +*Critical disk temperature* sets the critical threshold for this hard disk temperature. Exceeding this threshold will result in an alert notification. + +A value of zero will disable the critical threshold (including notifications). +:end + +:info_file_system_help: +Enter the desired file system type. Changing the file system type of a device will permit you to reformat +that device using the new file system. Be aware that **all existing data on the device will be lost**. +:end + +:info_comments_help: +This text will appear under the *Comments* column for the share in Windows Explorer. +Enter anything you like, up to 256 characters. +:end + +:info_warning_utilization_help: +*Warning disk utilization* sets the warning threshold for this hard disk utilization. Exceeding this threshold will result in a warning notification. + +When the warning threshold is set equal or greater than the critical threshold, there will be only critical notifications (warnings are not existing). + +A value of zero will disable the warning threshold (including notifications). +:end + +:info_critical_utilization_help: +*Critical disk utilization* sets the critical threshold for this hard disk utilization. Exceeding this threshold will result in an alert notification. + +A value of zero will disable the critical threshold (including notifications). +:end + +:info_btrfs_balance_help: +**Balance** will run the *btrfs balance* program to restripe the extents across all pool devices, for example, +to convert the pool from raid1 to raid0 or vice-versa. + +When a *full balance* is performed, it basically rewrites everything in the current filesystem. + +A *mode conversion* affects the btrfs data extents; metadata always uses raid1 and is converted to raid1 if necessary by any balance operation. + +The run time is potentially very long, depending on the filesystem size and speed of the device. + +Unraid OS uses these default options when creating a multiple-device pool: + +`-dconvert=raid1 -mconvert=raid1` + +For more complete documentation, please refer to the btrfs-balance [Manpage](https://man7.org/linux/man-pages/man8/btrfs-balance.8.html) + +*Note: raid5 and raid6 are generally still considered **experimental** by the Linux community* +:end + +:info_balance_cancel_help: +**Cancel** will cancel the balance operation in progress. +:end + +:info_btrfs_scrub_help: +**Scrub** runs the *btrfs scrub* program which will read all data and metadata blocks from all +devices and verify checksums. + +*btrfs scrub* will repair corrupted blocks if there is a correct copy available. +:end + +:info_zfs_scrub_help: +**Scrub** runs the *zfs scrub* program which will read all data and metadata blocks from all +devices and verify checksums. + +Click the **Upgrade Pool** button to upgrade the ZFS pool to enable the latest ZFS features. +:end + +:info_scrub_cancel_help: +**Cancel** will cancel the Scrub operation in progress. +:end + +:info_btrfs_check_help: +**Check** will run the *btrfs check* program to check file system integrity on the device. + +The *Options* field is initialized with *--readonly* which specifies check-only. If repair is needed, you should run +a second Check pass, setting the *Options* to *--repair*; this will permit *btrfs check* to fix the file system. + +WARNING: **Do not use** *--repair* unless you are advised to do so by a developer or an experienced user, +and then only after having accepted that no fsck successfully repair all types of filesystem corruption. +E.g. some other software or hardware bugs can fatally damage a volume. + +After starting a Check, you should Refresh to monitor progress and status. Depending on +how large the file system is, and what errors might be present, the operation can take **a long time** to finish (hours). +Not much info is printed in the window, but you can verify the operation is running by observing the read/write counters +increasing for the device on the Main page. +:end + +:info_check_cancel_help: +**Cancel** will cancel the Check operation in progress. +:end + +:info_reiserfs_check_help: +**Check** will run the *reiserfsck* program to check file system integrity on the device. + +The *Options* field may be filled in with specific options used to fix problems in the file system. Typically, you +first run a Check pass leaving *Options* blank. Upon completion, if *reiserfsck* finds any problems, you must +run a second Check pass, using a specific option as instructed by the first *reiserfsck* pass. + +After starting a Check you should Refresh to monitor progress and status. Depending on +how large the file system is, and what errors might be present, the operation can take **a long time** to finish (hours). +Not much info is printed in the window, but you can verify the operation is running by observing the read/write counters +increasing for the device on the Main page. +:end + +:info_reiserfs_cancel_help: +**Cancel** will cancel the Check operation in progress. +:end + +:info_xfs_check_help: +**Check** will run the *xfs_repair* program to check file system integrity on the device. + +The *Options* field is initialized with *-n* which specifies check-only. If repair is needed, you should run +a second Check pass, setting the *Options* blank; this will permit *xfs_repair* to fix the file system. + +After starting a Check, you should Refresh to monitor progress and status. Depending on +how large the file system is, and what errors might be present, the operation can take **a long time** to finish (hours). +Not much info is printed in the window, but you can verify the operation is running by observing the read/write counters +increasing for the device on the Main page. +:end + +:info_xfs_cancel_help: +**Cancel** will cancel the Check operation in progress. +:end + +:info_smart_notifications_help: +SMART notifications are generated on either an increasing RAW value of the attribute, or a decreasing NORMALIZED value which reaches a predefined threshold set by the manufacturer. + +Each disk may have its own specific setting overruling the 'default' setting (see global SMART settings under Disk Settings). +:end + +:info_tolerance_level_help: +A tolerance level may be given to prevent that small changes result in a notification. Setting a too high tolerance level may result in critical changes without a notification. + +Each disk may have its own specific setting overruling the 'default' setting (see global SMART settings under Disk Settings). +:end + +:info_controller_type_help: +By default automatic controller selection is done by smartctl to read the SMART information. Certain controllers however need specific settings for smartctl to work. +Use this setting to select your controller type and fill-in the specific disk index and device name for your situation. Use the manufacturer's documentation to find the relevant information. + +Each disk may have its own specific setting overruling the 'default' setting (see global SMART settings under Disk Settings). +:end + +:info_attribute_notifications_help: +The user can enable or disable notifications for the given SMART attributes. It is recommended to keep the default, which is ALL selected attributes, +when certain attributes are not present on your hard disk or do not provide the correct information, these may be excluded. +In addition custom SMART attributes can be entered to generate notifications. Be careful in this selection, +it may cause an avalance of notifcations if inappropriate SMART attributes are chosen. + +Each disk may have its own specific setting overruling the 'default' setting (see global SMART settings under Disk Settings). +:end + +:selftest_history_help: +Press **Show** to view the self-test history as is kept on the disk itself. +This feature is only available when the disk is in active mode. +:end + +:selftest_error_log_help: +Press **Show** to view the error report as is kept on the disk itself. +This feature is only available when the disk is in active mode. +:end + +:selftest_short_test_help: +Starts a *short* SMART self-test, the estimated duration can be viewed under the *Capabilities* section. This is usually a few minutes. + +When the disk is spun down, it will abort any running self-test. +This feature is only available when the disk is in active mode. +:end + +:selftest_long_test_help: +Starts an *extended* SMART self-test, the estimated duration can be viewed under the *Capabilities* section. This is usually several hours. + +When the disk is spun down, it will abort any running self-test. It is advised to disable the spin down timer of the disk +to avoid interruption of this self-test. + +This feature is only available when the disk is in active mode. +:end + +:selftest_result_help: +When no test is running it will show here the latest obtained self-test result (if available). +Otherwise a progress indicator (percentage value) is shown for a running test. +:end + +:smart_attributes_help: +This list shows the SMART attributes supported by this disk. For more information about each SMART attribute, it is recommended to search online. + +Attributes in *orange* may require your attention. They have a **raw value** greater than zero and may indicate a pending disk failure. + +Special attention is required when the particular attribute raw value starts to increase over time. When in doubt, consult the Limetech forum for advice. +:end + +:smart_capabilities_help: +This list shows the SMART capabilities supported by this disk. + +Observe here the estimated duration of the SMART short and extended self-tests. +:end + +:smart_identity_help: +This list shows the SMART identity information of this disk +:end + +:open_devices_help: +These are devices installed in your server but not assigned to either the parity-protected +array or the cache disk/pool. +:end + +:flash_backup_help: +Use *Flash backup* to create a single zip file of the current contents of the flash device and store it locally on your computer. +:end + +:syslinux_cfg_help: +Use this page to make changes to your `syslinux.cfg` file. +You will need to reboot your server for these changes to take effect. +:end + +:syslinux_button_help: +Click the **Default** button to initialize the edit box with the +factory-default contents. You still need to click **Apply** in order to +commit the change. + +Click the **Apply** button to commit the current edits. Click **Reset** to +undo any changes you make (before Saving). Click **Done** to exit this page. +:end + +:info_share_assignment_help: +The selected pool is available for user shares. +:end + +:info_free_space_help: +This defines a "floor" for the amount of free space remaining in the volume. +If the free space becomes less than this value, then new files written via user shares will fail with "not enough space" error. + +Enter a numeric value with one of these suffixes: + +**KB** = 1,000
+**MB** = 1,000,000
+**GB** = 1,000,000,000
+**TB** = 1,000,000,000,000
+**K** = 1,024 (ie, 2^10)
+**M** = 1,048,576 (ie, 2^20)
+**G** = 1,073,741,824 (ie, 2^30)
+**T** = 1,099,511,627,776 (ie, 2^40)
+ +If no suffix, a count of 1024-byte blocks is assumed. +:end + +:share_list_help: +**Colored Status Indicator** -- the significance of the color indicator at the beginning of each line in *User Shares* is as follows: + +All files are on protected storage. + +Some or all files are on unprotected storage. + +**Security modes:** + ++ '-' -- user share is not exported and unavailable on the network ++ *Public* -- all users including guests have full read/write access (open access) ++ *Secure* -- all users including guests have read access, write access is set per user (limited access) ++ *Private* -- no guest access at all, read/write or read-only access is set per user (closed access) + +**Special modes:** + ++ SMB security mode displayed in *italics* indicates exported hidden user shares. ++ NFS does not have special modes for user shares. +:end + +:disk_list_help: +**Colored Status Indicator** -- the significance of the color indicator at the beginning of each line in *Disk Shares* is as follows: + +Mounted, underlying device has redundancy/protection. + +Mounted, underlying device does not have redundancy/protection. + +**Security modes:** + ++ '-' -- disk share is not exported and unavailable on the network ++ *Public* -- all users including guests have full read/write access (open access) ++ *Secure* -- all users including guests have read access, write access is set per user (limited access) ++ *Private* -- no guest access at all, read/write or read-only access is set per user (closed access) + +**Special modes:** + ++ SMB security mode displayed in *italics* indicates exported hidden disk shares. ++ NFS does not have special modes for disk shares. +:end + +:share_edit_global1_help: +A *Share*, also called a *User Share*, is simply the name of a top-level directory that exists on one or more of your +storage volumes (array disks and pools). Each share can be exported for network access. When browsing a share, we return the +composite view of all files and subdirectories for which that top-level directory exists on each storage device. + +*Read settings from* is used to preset the settings of the new share with the settings of an existing share. + +Select the desired share name and press **Read** to copy the settings from the selected source. +:end + +:share_edit_global2_help: +*Write settings to* is used to copy the settings of the current share to one or more other existing shares. + +Select the desired destinations and press **Write** to copy the settings to the selected shares. +:end + +:share_edit_name_help: +The share name can be up to 40 characters, and is case-sensitive with these restrictions: + +* cannot contain a double-quote character (") or the following characters: / \ * < > | +* cannot be one of the reserved share names: flash, cache, cache2, .., disk1, disk2, .. + +We highly recommend to make your life easier and avoid special characters. +:end + +:share_edit_comments_help: +Anything you like, up to 256 characters. +:end + +:share_edit_allocation_method_help: +This setting determines how Unraid OS will choose which disk to use when creating a new file or directory: + +**High-water** +Choose the lowest numbered disk with free space still above the current *high water mark*. The +*high water mark* is initialized with the size of the largest Data disk divided by 2. If no disk +has free space above the current *high water mark*, divide the *high water mark* by 2 and choose again. + +The goal of **High-water** is to write as much data as possible to each disk (in order to minimize +how often disks need to be spun up), while at the same time, try to keep the same amount of free space on +each disk (in order to distribute data evenly across the array). + +**Fill-up** +Choose the lowest numbered disk that still has free space above the current **Minimum free space** +setting. + +**Most-free** +Choose the disk that currently has the most free space. +:end + +:share_edit_free_space_help: +The *minimum free space* available to allow writing to any disk belonging to the share.
+ +Choose a value which is equal or greater than the biggest single file size you intend to copy to the share. +Include units KB, MB, GB and TB as appropriate, e.g. 10MB. +:end + +:share_edit_split_level_help: +Determines whether a directory is allowed to expand onto multiple disks. + +**Automatically split any directory as required** +When a new file or subdirectory needs to be created in a share, Unraid OS first chooses which disk +it should be created on, according to the configured *Allocation method*. If the parent directory containing +the new file or subdirectory does not exist on this disk, then Unraid OS will first create all necessary +parent directories, and then create the new file or subdirectory. + +**Automatically split only the top level directory as required** +When a new file or subdirectory is being created in the first level subdirectory of a share, if that first +level subdirectory does not exist on the disk being written, then the subdirectory will be created first. +If a new file or subdirectory is being created in the second or lower level subdirectory of a share, the new +file or subdirectory is created on the same disk as the new file or subdirectory's parent directory. + +**Automatically split only the top "N" level directories as required** +Similar to previous: when a new file or subdirectory is being created, if the parent directory is at level "N", +and does not exist on the chosen disk, Unraid OS will first create all necessary parent directories. If the +parent directory of the new file or subdirectory is beyond level "N", then the new file or subdirectory is +created on the same disk where the parent directory exists. + +**Manual: do not automatically split directories** +When a new file or subdirectory needs to be created in a share, Unraid OS will only consider disks where the +parent directory already exists. +:end + +:share_edit_included_disks_help: +Specify the disks which can be used by the share. By default all disks are included; that is, if specific +disks are not selected here, then the share may expand into *all* array disks. +:end + +:share_edit_excluded_disks_help: +Specify the disks which can *not* be used by the share. By default no disks are excluded. +:end + +:share_edit_cache_pool_help: +Specify whether new files and directories written on the share can be written onto the Cache disk/pool if present. +This setting also affects *mover* behavior. + +**No** prohibits new files and subdirectories from being written onto the Cache disk/pool. +*Mover* will take no action so any existing files for this share that are on the cache are left there. + +**Yes** indicates that all new files and subdirectories should be written to the Cache disk/pool, provided +enough free space exists on the Cache disk/pool. +If there is insufficient space on the Cache disk/pool, then new files and directories are created on the array. +When the *mover* is invoked, files and subdirectories are transferred off the Cache disk/pool and onto the array. + +**Only** indicates that all new files and subdirectories must be written to the Cache disk/pool. +If there is insufficient free space on the Cache disk/pool, *create* operations will fail with *out of space* status. +*Mover* will take no action so any existing files for this share that are on the array are left there. + +**Prefer** indicates that all new files and subdirectories should be written to the Cache disk/pool, provided +enough free space exists on the Cache disk/pool. +If there is insufficient space on the Cache disk/pool, then new files and directories are created on the array. +When the *mover* is invoked, files and subdirectories are transferred off the array and onto the Cache disk/pool. + +**NOTE:** Mover will never move any files that are currently in use. +This means if you want to move files associated with system services such as Docker or VMs then you need to +disable these services while mover is running. +:end + +:share_edit_copy_on_write_help: +Set to **No** to cause the *btrfs* NOCOW (No Copy-on-Write) attribute to be set on the share directory +when created on a device formatted with *btrfs* file system. Once set, newly created files and +subdirectories on the device will inherit the NOCOW attribute. This setting has no effect on non-btrfs file systems. + +Set to **Auto** for normal operation, meaning COW **will** be in effect on devices formatted with *btrfs*. +:end + +:share_edit_status_help: +Share does *not* contain any data and may be deleted if not needed any longer. +:end + +:share_edit_delete_help: +Share can *not* be deleted as long as it contains data. Be aware that some data can be hidden. See also [SMB Settings](/Settings/SMB) -> Hide "dot" files. +:end + +:share_edit_exclusive_access_help: +When set to "Yes" indicates a symlink directly to a pool has been set up for the share in the /mnt/user tree. + +Refer to [Global Share Settings](Settings/ShareSettings) -> Permit exclusive shares. +:end + +:share_edit_primary_storage_help: +**Primary storage** is where *new files and folders* are created. If +Primary storage is below the minimum free space setting then new files +and folders will be created in **Secondary storage**, if configured. + +**Important:** For *Exclusive access* shares, the Min free space +settings are ignored. +:end + +:share_edit_secondary_storage_help: +**Secondary storage** is where new files and directories are created if no +room on Primary storage. When both Primary and Secondary storage are +configured the 'mover' will transfer files between them. +:end + +:share_edit_mover_action_help: +This defines the direction of file transfer between Primary and +Secondary storage when both are configured. +:end + +:smb_security_help: +*Read settings from* is used to preset the SMB security settings of the current selected share with the settings of an existing share. + +Select the desired share name and press **Read** to copy the SMB security settings from the selected source. + +*Write settings to* is used to copy the SMB security settings of the current selected share to one or more other existing shares. + +Select the desired destinations and press **Write** to copy the SMB security settings to the selected shares. +:end + +:smb_export_help: +This setting determines whether the share is visible and/or accessible. The 'Yes (hidden)' setting +will *hide* the share from *browsing* but is still accessible if you know the share name. +:end + +:smb_time_machine_volume_help: +This limits the reported volume size, preventing Time Machine from using the entire real disk space +for backup. For example, setting this value to "1024" would limit the reported disk space to 1GB. +Note that Ventura 13.6 and later require a value to be present. No entry will prevent Time Machine +from working correctly. +:end + +:smb_case_sensitive_names_help: +Controls whether filenames are case-sensitive. + +The default setting of **auto** allows clients that support case sensitive filenames (Linux CIFSVFS) +to tell the Samba server on a per-packet basis that they wish to access the file system in a case-sensitive manner (to support UNIX +case sensitive semantics). No Windows system supports case-sensitive filenames so setting this option to **auto** is the same as +setting it to No for them; however, the case of filenames passed by a Windows client will be preserved. This setting can result +in reduced performance with very large directories because Samba must do a filename search and match on passed names. + +A setting of **Yes** means that files are created with the case that the client passes, and only accessible using this same case. +This will speed very large directory access, but some Windows applications may not function properly with this setting. For +example, if "MyFile" is created but a Windows app attempts to open "MYFILE" (which is permitted in Windows), it will not be found. + +A value of **Forced lower** is special: the case of all incoming client filenames, not just new filenames, will be set to lower-case. +In other words, no matter what mixed case name is created on the Windows side, it will be stored and accessed in all lower-case. This +ensures all Windows apps will properly find any file regardless of case, but case will not be preserved in folder listings. +Note this setting should only be configured for new shares. +:end + +:smb_security_modes_help: +Summary of security modes: + +**Public** All users including guests have full read/write access. + +**Secure** All users including guests have read access, you select which of your users have write access. + +**Private** No guest access at all, you select which of your users have read/write, read-only access or no access. + +Windows Server Signing: + +If you are unable to browse SMB shares with Windows 11 version 24H2 or newer, you need to make some changes to accomodate a new feature called Server Signing. Server Signing is enabled in Unraid and you need to make changes to access Public shares. +You can disable it in Windows, or to work with Unraid with Server Signing enabled, the easiest way is to create a user (with a password set) in Unraid with the same name as the Windows account you are using, Windows should then ask you for the credentials. +If you are using a Microsoft account, it may be better to just create a user in Unraid with a simple username and set a password, then in Windows go to Control Panel -> Credential Manager -> Windows credentials -> Add a Windows Credential and add the correct Unraid server name and credentials. +:end + +:smb_secure_access_help: +*Read settings from* is used to preset the SMB User Access settings of the current selected share with the settings of an existing share. + +Select the desired share name and press **Read** to copy the SMB security settings from the selected source. + +*Write settings to* is used to copy the SMB User Access settings of the current share to one or more other existing shares. + +Select the desired destinations and press **Write** to copy the SMB User access settings to the selected shares. +:end + +:smb_private_access_help: +*Read settings from* is used to preset the SMB User Access settings of the current selected share with the settings of an existing share. + +Select the desired share name and press **Read** to copy the SMB security settings from the selected source. + +*Write settings to* is used to copy the SMB User Access settings of the current share to one or more other existing shares. + +Select the desired destinations and press **Write** to copy the SMB User access settings to the selected shares. +:end + +:nfs_security_help: +*Read settings from* is used to preset the NFS security settings of the current selected share with the settings of an existing share. + +Select the desired share name and press **Read** to copy the NFS security settings from the selected source. + +*Write settings to* is used to copy the NFS security settings of the current selected share to one or more other existing shares. + +Select the desired destinations and press **Write** to copy the NFS security settings to the selected shares. +:end + +:nfs_security_rules_help: +Put the rule for each IP address on a separate line and terminate the Rule with a new line. +You cannot enter a Rule in the format IP1,IP2(...). Unraid does not format the exports file in that format. + +The default rules for every NFS Rule are - async and no_subtree_check. + +**Note:** The Public Rule used is '*(rw,sec=sys,insecure,anongid=100,anonuid=99,no_root_squash)'. +:end + +:user_add_username_help: +Usernames may be up to 40 characters long and must start with a **lower case letter** or an underscore, +followed by **lower case letters**, digits, underscores, or dashes. They can end with a dollar sign. +:end + +:user_add_description_help: +Up to 64 characters. The characters ampersand (&) quote (") and colon (:) are not allowed. +:end + +:user_add_custom_image_help: +The image will be scaled to 48x48 pixels in size. The maximum image file upload size is 95 kB (97,280 bytes). +:end + +:user_password_help: +Up to 128 characters. +:end + +:user_edit_description_help: +Up to 64 characters. The characters ampersand (&) quote (") and colon (:) are not allowed. +:end + +:user_edit_custom_image_help: +The image will be scaled to 48x48 pixels in size. The maximum image file upload size is 512 kB (524,288 bytes). +:end + +:cpu_vms_help: +This page gives a total view of the current CPU pinning assignments for VMs.
+It also allows to modify these assignments. + +Running VMs are **stopped first** and restarted after the modification.
+Stopped VMs are instantly modified and new assignments become active when the VM is started. + +When ***Apply*** is pressed a scan is performed to find the changes, subsequently only VMs which have changes are modified in parallel. + +*Important: Please wait until all updates are finished before leaving this page*. +:end + +:cpu_pinning_help: +This page gives a total view of the current CPU pinning assignments for Docker containers.
+It also allows to modify these assignments. + +Running containers are **stopped first** and restarted after the modification.
+Stopped containers are instantly modified and new assignments become active when the user manually starts the container. + +When ***Apply*** is pressed a scan is performed to find the changes, subsequently containers which have changes are modified in parallel. + +*Important: Please wait until all updates are finished before leaving this page*. + +By default NO cores are selected for a Docker container, which means it uses all available cores.
+Do not select **ALL** cores for containers, just select **NO** cores if you want unrestricted core use. + +Do not select cores for containers which are *isolated*. +By design a container will only use a single core (the lowest numbered core) when multiple isolated cores are selected.
+Usually this is not what a user wants when selecting multiple cores. +:end + +:cpu_isolation_help: +CPU isolation allows the user to specify CPU cores that are to be explicitly reserved for assignment (to VMs or Docker containers). + +This is incredibly important for gaming VMs to run smoothly because even if you manually pin your Docker containers to not overlap with your gaming VM, +the host OS can still utilize those same cores as the guest VM needs for things like returning responses for the webGUI, running a parity check, btrfs operations, etc. +:end + +:timezone_help: +Select your applicable time zone from the drop-down list. +:end + +:use_ntp_help: +Select 'Yes' to use Network Time Protocol to keep your server time accurate. +We **highly** recommend the use of a network time server, especially if you plan on using Active Directory. + +Note: if using `pool.ntp.org` time servers, please also refer to [their documentation](http://www.pool.ntp.org/en/use.html). +:end + +:ntp_server1_help: +This is the primary NTP server to use. Enter a FQDN or an IP address. +:end + +:ntp_server2_help: +This is the alternate NTP server to use if NTP Server 1 is down. +:end + +:ntp_server3_help: +This is the alternate NTP Server to use if NTP Servers 1 and 2 are both down. +:end + +:ntp_server4_help: +This is the alternate NTP Server to use if NTP Servers 1, 2, and 3 are all down. +:end + +:current_time_help: +Enter the current time-of-day. Use format YYYY-MM-DD HH:MM:SS. Greyed out when using NTP. +:end + +:disk_enable_autostart_help: +If set to 'Yes' then if the device configuration is correct upon server start-up, +the array will be automatically Started and shares exported.
+If set to 'No' then you must Start the array yourself. +:end + +:disk_spindown_delay_help: +This setting defines the 'default' time-out for spinning hard drives down after a period +of no I/O activity. You may override the default value for an individual disk on the Disk Settings +page for that disk. +:end + +:disk_spinup_groups_help: +If set to 'Yes' then the spinup groups feature is enabled. +:end + +:disk_default_partition_format_help: +Defines the type of partition layout to create when formatting hard drives 2TB in size and +smaller **only**. (All devices larger than 2TB are always set up with GPT partition tables.) + +**MBR: unaligned** setting will create MBR-style partition table, where the single +partition 1 will start in the **63rd sector** from the start of the disk. This is the *traditional* +setting for virtually all MBR-style partition tables. + +**MBR: 4K-aligned** setting will create an MBR-style partition table, where the single +partition 1 will start in the **64th sector** from the start of the disk. Since the sector size is 512 bytes, +this will *align* the start of partition 1 on a 4K-byte boundary. This is required for proper +support of so-called *Advanced Format* drives. + +Unless you have a specific requirement do not change this setting from the default **MBR: 4K-aligned**. +:end + +:disk_default_file_system_help: +Defines the default file system type to create when an *unmountable* array device is formatted. + +The default file system type for a single or multi-device cache is always Btrfs. +:end + +:disk_shutdown_timeout_help: +When shutting down the server, this defines how long to wait in seconds for *graceful* shutdown before forcing +shutdown to continue. +:end + +:disk_tunable_poll_attributes_help: +This defines the disk SMART polling interval, in seconds. A value of 0 disables SMART polling (not recommended). +:end + +:disk_tunable_enable_ncq_help: +If set to **No** then *Native Command Queuing* is disabled for all array devices that support NCQ. + +**Auto** leaves the setting for each device as-is. + +Note: You must reboot after selecting Auto for setting to take effect. +:end + +:disk_tunable_nr_requests_help: +This defines the `nr_requests` device driver setting for all array devices. + +**Auto** leaves the setting for each device as-is. + +Note: if you set to blank and click Apply, the setting is restored to its default, and you must reboot for setting to take effect. +:end + +:disk_tunable_scheduler_help: +Selects which kernel I/O scheduler to use for all array devices. + +**Auto** leaves the setting for each device as-is (mq-deadline). + +Note: You must reboot after selecting Auto for setting to take effect. +:end + +:disk_tunable_md_num_stripes_help: +This is the size of the *stripe pool* in number of *stripes*. A *stripe* refers to a data structure that facilitates parallel 4K read/write +operations necessary for a parity-protected array. + +Note: if you set to blank and click Apply, the setting is restored to its default, and will take effect after reboot. +:end + +:disk_tunable_md_queue_limit_help: +This is a number in [1..100] which is the maximum steady-load percentage of the stripe pool permitted to be in use. + +Note: if you set to blank and click Apply, the setting is restored to its default. +:end + +:disk_tunable_md_sync_limit_help: +This is a number in [0..100] which is the maximum percentage of the stripe pool allocated for parity sync/check in the presence of other I/O. + +Note: if you set to blank and click Apply, the setting is restored to its default. +:end + +:disk_tunable_md_write_method_help: +Selects the method to employ when writing to enabled disk in parity protected array. + +*Auto* selects `read/modify/write`. +:end + +:disk_default_warning_utilization_help: +*Warning disk utilization* sets the default warning threshold for all hard disks utilization. Exceeding this threshold will result in a warning notification. + +When the warning threshold is set equal or greater than the critical threshold, there will be only critical notifications (warnings are not existing). + +A value of zero will disable the warning threshold (including notifications). +:end + +:disk_default_critical_utilization_help: +*Critical disk utilization* sets the default critical threshold for all hard disks utilization. Exceeding this threshold will result in an alert notification. + +A value of zero will disable the critical threshold (including notifications). +:end + +:disk_default_warning_temperature_help: +*Warning disk temperature* sets the default warning threshold for all hard disks temperature. Exceeding this threshold will result in a warning notification. + +A value of zero will disable the warning threshold (including notifications). +:end + +:disk_default_critical_temperature_help: +*Critical disk temperature* sets the default critical threshold for all hard disks temperature. Exceeding this threshold will result in an alert notification. + +A value of zero will disable the critical threshold (including notifications). +:end + +:ssd_default_warning_temperature_help: +*Warning SSD temperature* sets the default warning threshold for all SSD devices temperature. Exceeding this threshold will result in a warning notification. + +A value of zero will disable the warning threshold (including notifications). +:end + +:ssd_default_critical_temperature_help: +*Critical disk temperature* sets the default critical threshold for all SSD devices temperature. Exceeding this threshold will result in an alert notification. + +A value of zero will disable the critical threshold (including notifications). +:end + +:disk_default_smart_notification_help: +SMART notifications are generated on either an increasing RAW value of the attribute, or a decreasing NORMALIZED value which reaches a predefined threshold set by the manufacturer. + +This section is used to set the global settings for all disks. It is possible to adjust settings for individual disks. +:end + +:disk_default_smart_tolerance_help: +A tolerance level may be given to prevent that small changes result in a notification. Setting a too high tolerance level may result in critical changes without a notification. + +This section is used to set the global settings for all disks. It is possible to adjust settings for individual disks. +:end + +:disk_default_smart_controller_help: +By default automatic controller selection is done by smartctl to read the SMART information. Certain controllers however need specific settings for smartctl to work. +Use this setting to select your controller type and fill-in the specific disk index and device name for your situation. Use the manufacturer's documentation to find the relevant information. + +This section is used to set the global settings for all disks. It is possible to adjust settings for individual disks. +:end + +:disk_default_smart_attribute_help: +The user can enable or disable notifications for the given SMART attributes. It is recommended to keep the default, which is ALL selected attributes, +when certain attributes are not present on your hard disk or do not provide the correct information, these may be excluded. +In addition custom SMART attributes can be entered to generate notifications. Be careful in this selection, +it may cause an avalance of notifcations if inappropriate SMART attributes are chosen. + +This section is used to set the global settings for all disks. It is possible to adjust settings for individual disks. +:end + +:docker_repositories_help: +Use this field to add template repositories. +Docker templates are used to facilitate the creation and re-creation of Docker containers. Please setup one per line. + +Using repositories is deprecated. For instructions on how to have Community Applications utilize private repositories, visit here + +:end + +:docker_enable_help: +Before you can start the Docker service for the first time, please specify an image file for Docker to install to. + +Once started, Docker will always automatically start after the array has been started. +:end + +:docker_readmore_help: +Some systems with a lot of docker containers may experience lag using the main Docker page. + +Setting this to "No" may help speed up general page usage by disabling the use of readmore-js. +Instead of chevrons indicating more data for Port and Volume mapping, all data is displayed. +:end + +:docker_timeout_help: +The time in seconds to allow a container to gracefully stop before forcing it to stop +:end + +:docker_pid_limit_help: +Set a PID Limit to limit the number of PIDs that a docker can use. The default is 2048. Set to zero for unlimited PIDs (not recommended). +:end + +:docker_vdisk_type_help: +Select where to keep the Docker persistent state. + +This can be an image file with a specific size or a dedicated folder. +:end + +:docker_vdisk_size_help: +If the system needs to create a new docker image file, this is the default size to use specified in GB. + +To resize an existing image file, specify the new size here. Next time the Docker service is started the file (and file system) will be increased to the new size (but never decreased). +:end + +:docker_vdisk_location_help: +You must specify an image file for Docker. The system will automatically create this file when the Docker service is first started. + +The image file name must have the extension .img, If not the input is not accepted and marked red. + +It is recommended to create this image file outside the array, e.g. on the Cache pool. For best performance SSD devices are preferred. +:end + +:docker_vdisk_directory_help: +You must specify a folder for Docker. The system will automatically create this folder when the Docker service is first started. + +It is recommended to create this folder under a share which resides on the Cache pool (setting: cache=Only). For best performance SSD devices are preferred. +:end + +:docker_storage_driver_help: +overlay2 (default): Will use overlay2 as the storage driver for Docker, regardless of the underlying filesystem. + +native: The native storage driver for your underlying filesystem will be used (XFS: overlay2 | ZFS: zfs | BTRFS: btrfs). + +ATTENTION: Changing the storage type from an existing Docker installation is not possible, you have to delete your Docker directory first, change the storage type and then reinstall your containers. +It is recommended to take a screenshot from your Docker containers before changing the storage type. After deleting and changing the storage type, reinstall the containers by clicking Add Container on the Docker page and select one by one from the drop down to reinstall them with your previous settings). +:end + +:docker_appdata_location_help: +You can specify a folder to automatically generate and store subfolders containing configuration files for each Docker app (via the /config mapped volume). + +The folder's path must end with a trailing slash (/) character. If not the input is not accepted and marked red. + +It is recommended to create this folder outside the array, e.g. on the Cache pool. For best performance SSD devices are preferred. + +Only used when adding new Docker apps. Editing existing Docker apps will not be affected by this setting. +:end + +:docker_log_rotation_help: +By default LOG rotation is disabled and will create a single LOG file of unlimited size. + +Enable LOG rotation to limit the size of the LOG file and specify the number of files to keep in the rotation scheme. +:end + +:docker_log_file_size_help: +Specifies the maximum LOG size. When exceeded LOG rotation will occur. +:end + +:docker_log_file_number_help: +Specifies the number of LOG files when LOG rotation is done. +:end + +:docker_authoring_mode_help: +If set to **Yes**, when creating/editing containers the interface will be present with some extra fields related to template authoring. +:end + +:docker_custom_network_type_help: +The **ipvlan** type is best when connection to the physical network is not needed. +Please read this on implementing an ipvlan network.
+ +The **macvlan** type of network allows direct connection to the physical network. +Please read this on implementing a macvlan network.
+ +**Note:** Docker uses its own dhcp service, which is the **DHCP Pool** setting. +When you use multiple Unraid servers, then each server must have a different Docker **DHCP Pool** range configured. +:end + +:docker_custom_network_access_help: +Allows direct communication between the host and containers using a custom **macvlan** network. By default this is disabled. +:end + +:docker_user_defined_network_help: +User created networks are networks created by the user outside of the GUI. By default user created networks are removed from Docker. This is done to prevent potential conflicts with the automatic generation of custom networks. + +Change this setting to preserve user defined networks, but it is the responsibility of the user to ensure these entries work correctly and are conflict free. +:end + +:docker_include_interface_vlan_ipv4_help: +Include (default) or exclude the above interfaces or VLANs as custom network for Docker. + +Enter the pool range within each allocated subnet which is used for DHCPv4 assignments by Docker. E.g. 192.168.1.128/25 +:end + +:docker_exclude_interface_vlan_ipv4_help: +Include or exclude (default) the above interfaces or VLANs as custom network for Docker. + +Enter the pool range within each allocated subnet which is used for DHCPv4 assignments by Docker. E.g. 192.168.1.128/25 +:end + +:docker_include_interface_vlan_ipv6_help: +Include (default) or exclude the above interfaces or VLANs as custom network for Docker. + +Enter the pool range within each allocated subnet which is used for DHCPv6 assignments by Docker. E.g. 2a02:abcd:9ef5:100:1::/72 +:end + +:docker_exclude_interface_vlan_ipv6_help: +Include or exclude (default) the above interfaces or VLANs as custom network for Docker. + +Enter the pool range within each allocated subnet which is used for DHCPv6 assignments by Docker. E.g. 2a02:abcd:9ef5:100:1::/72 +:end + +:docker_version_help: +This is the active Docker version. +:end + +:docker_vdisk_location_active_help: +This is the location of the Docker image. +:end + +:docker_storage_driver_active_help: +This is the storage driver for Docker. +:end + +:docker_appdata_location_active_help: +This is the storage location for Docker containers. +:end + +:docker_log_rotation_active_help: +By default a single unlimited LOG file is created. Otherwise LOG file size and number of files are limited when LOG rotation is enabled. +:end + +:docker_custom_network_active_help: +Allows direct communication between the host and containers using a custom (macvlan) network.
+By default this is prohibited. +:end + +:docker_user_defined_network_active_help: +Shows whether networks created outside of the GUI are removed or preserved for Docker. When preserved *user defined networks* become available in the *Network type* dropdown list of containers. +:end + +:docker_scrub_help: +**Scrub** runs the *btrfs scrub* program to check file system integrity. + +If repair is needed you should check the *Correct file system errors* and run a second Scrub pass; this will permit *btrfs scrub* to fix the file system. +:end + +:docker_cancel_help: +**Cancel** will cancel the Scrub operation in progress. +:end + +:id_server_name_help: +The network identity of your server. Also known as *hostname* or *short hostname*. Windows networking +refers to this as the *NetBIOS name* and must be 15 characters or less in length. +Use only alphanumeric characters (that is, "A-Z", "a-z", and "0-9"), dashes ("-"), and dots ("."); +and, the first and last characters must be alphanumeric. +:end + +:id_description_help: +This is a text field that is seen next to a server when listed within Network or File Explorer +(Windows), or Finder (macOS). +:end + +:id_model_help: +This is the server model number. +:end + +:mgmt_start_page_help: +Select the page which is opened first when entering the GUI. By default the *Main* page is selected. +:end + +:mgmt_use_telnet_help: +By default TELNET access is enabled. TELNET is an insecure type of CLI access however, +and it is highly recommended to use SSH access instead and disable TELNET access. +:end + +:mgmt_telnet_port_help: +Enter the TELNET port, default port is 23. +:end + +:mgmt_use_ssh_help: +SSH is enabled by default and offers a secure way of CLI access. Upon system startup SSH keys are automatically generated +if not yet existing, and stored on the flash device in the folder */config/ssh*. +:end + +:mgmt_ssh_port_help: +Enter the SSH port, default port is 22. +:end + +:mgmt_use_upnp_help: +Enable (default) or disable the UPnP function on the server. This function allows automatic forwarding of ports on the router, only applicable when UPnP is enabled on the router itself. +:end + +:mgmt_use_ssl_tls_help: +Determines how the webGUI responds to HTTP and/or HTTPS protocol on your LAN. + +Select **No** to use HTTP. To access your server use this URL: + +`http://.` + +or this URL: + +`http://` + +Select **Yes** to enable use of an automatically-generated self-signed +SSL certificate. Use this URL to access your server: + +`https://.` + +Note that use of a self-signed SSL certificate will generate a browser +warning. + +Select **Strict** to enable *exclusive* use of a myunraid.net SSL +certificate for https access (see **Provision** below). Note that a DNS +server must be reachable. + +**Redirects:** When accessing `http://` or `http://.`, the +behavior will change depending on the value of the Use SSL/TLS setting: + +* If Use SSL/TLS is set to **Strict**, you will be redirected to `https://..myunraid.net` +* If Use SSL/TLS is set to **Yes**, you will be redirected to `https:// or https://.` +* If Use SSL/TLS is set to **No**, then the http url will load directly. + +Important: **Strict** may not be selectable if your router or upstream DNS server has +[DNS rebinding protection](https://en.wikipedia.org/wiki/DNS_rebinding) enabled. DNS rebinding +protection prevents DNS from resolving a private IP network range. DNS rebinding protection is meant as +a security feature on a LAN that may include legacy devices with buggy/insecure "web" interfaces. + +One source of DNS rebinding protection could be your ISP DNS server. In this case the problem may be solved by +switching to a different DNS server such as OpenDNS where DNS rebinding proection can be turned off. + +More commonly, DNS rebinding protection could be enabled in your router. Most consumer routers do not implement DNS +rebinding protection; but, if they do, a configuration setting should be available to turn it off. + +Higher end routers usually do enable DNS rebinding protection. Typically there are ways of turning it off +entirely or selectively based on domain. Examples: + +**DD-WRT:** If you are using "dnsmasq" with DNS rebinding protection enabled, you can add this line to your router +configuration file: + +`rebind-domain-ok=/myunraid.net/` + +**pfSense:** If you are using pfSense internal DNS resolver service, you can add these Custom Option lines: + +`server:`
+`private-domain: "myunraid.net"` + +**Ubiquiti USG router:** you can add this configuration line: + +`set service dns forwarding options rebind-domain-ok=/myunraid.net/` + +**OpenDNS:** Go to Settings -> Security and *remove* the checkbox next to + "Suspicious Responses - Block internal IP addresses". It is an all-or-nothing setting. + +When all else fails, you may be able create an entry in your PC's *hosts* file to override external DNS and +directly resolve your servers myunraid.net FQDN to its local IP address. +:end + +:mgmt_http_port_help: +Enter the HTTP port, default port is 80. +:end + +:mgmt_https_port_help: +Enter the HTTPS port, default port is 443. +:end + +:mgmt_local_tld_help: +Enter your local Top Level Domain. May be blank. +:end + +:mgmt_local_access_urls_help: +The Local Access URLs shown above are based on your current settings. +To adjust URLs or redirects, see the help text for "Use SSL/TLS". +:end + +:mgmt_wg_access_urls_help: +These URLs will only work when connected via the appropriate WireGuard tunnel as configured on ***Settings > VPN Manager*** +:end + +:mgmt_tailscale_access_urls_help: +These URLs will only work when connected to the appropriate Tailscale Tailnet. +:end + +:mgmt_certificate_expiration_help: +**Provision** may be used to install a *free* myunraid.net SSL Certificate from +[Let's Encrypt](https://letsencrypt.org/). + +The myunraid.net SSL certificate can be used in two ways. First, +having the certificate present enables your server to respond to an +alternate URL of the form: + +`https://..myunraid.net` + +The `` value is a 40-character hex string (160 bits) unique to +your server. A Lime Technology DDNS server will return your `` +in response to a DNS request on this URL. The certificate Subject is +set to `*..myunraid.net` thus validating the https connection. + +You may enable this URL exclusively on your LAN by setting **Use +SSL/TLS** to **Strict**. + +The second use for a myunraid.net certificate is to enable secure +remote access available through the Unraid Connect plugin feature. Note +that it is possible to use secure remote access in conjunction with +insecure local access. + +After a myunraid.net SSL Certificate has been installed, a +background service is activated: + +- *renewcert* - This starts 60 seconds after server reboot has completed and contacts the Lime Technology +certificate renewal service to determine if your myunraid.net SSL certificate needs to be renewed. +Thereafter it wakes up every 24 hours. If within 30 days of expiration, a new certificate is automatically +provisioned and downloaded to your server. + +**Delete** may be used to delete the myunraid.net certificate file. + +**nginx certificate handling details** + +nginx makes use of two certificate files stored on the USB flash boot device:
+ +- a self-signed certificate: `config/ssl/certs/_unraid_bundle.pem` + +- a myunraid.net certificate: `config/ssl/certs/certificate_bundle.pem` + +The self-signed SSL certificate file is automatically created when nginx +starts; and re-created if the server hostname or local TLD is changed. + +**nginx stapling support** + +OCSP Stapling is automatically enabled if the certificate contains an OCSP responder URL. + +Hence, for self-signed certificates stapling is not enabled; for CA-signed certificates +stabling is enabled. +:end + +:ftp_server_help: +Enable or disable the FTP server daemon. By default the FTP server is enabled. +This setting is not saved, i.e. upon system reboot it will revert to its default setting. +:end + +:ftp_users_help: +Enter the user names (separated by spaces) permitted to access the server using FTP. +To disallow any user access, clear this setting. + +**Note:** do not enter user name `root` since this may cause problems in the future. +:end + +:ftp_overview_help: +### Overview + +Unraid OS includes the popular `vsftpd` FTP server. The configuration of `vsftp` is currently very +simple: **All** user names entered above are permitted to access the server via FTP and will have +*full read/write/delete access* to the entire server, so use with caution. +:end + +:smb_enable_help: +Select 'Yes (Workgroup)' to enable SMB (Windows Networking) protocol support. This +also enables Windows host discovery. + +Select 'Yes (Active Directory)' to enable Active Directory integration. +:end + +:smb_hide_files_help: +If set to 'Yes' then files starting with a '.' (dot) will appear as *hidden files* and normally +will not appear in Windows folder lists unless you have "Show hidden files, folders, and drives" enabled +in Windows Folder Options. + +If set to 'No' then dot files will appear in folder lists the same as any other file. +:end + +:smb_multi_channel_help: +When set to 'Yes' enables SMB Multi Channel support in the server. From +[microsoft](https://docs.microsoft.com/en-us/azure-stack/hci/manage/manage-smb-multichannel): +"SMB Multichannel enables file servers to use multiple network connections simultaneously." +:end + +:smb_enhanced_macos_help: +When set to 'Yes' provides enhanced compatibility with Apple SMB clients, resulting, for example, in faster +Finder browsing, and ability to export Time Machine shares. This may cause some issues with Windows clients, however. +Please also refer to the [VFS_FRUIT MAN PAGE](https://www.mankier.com/8/vfs_fruit). +:end + +:smb_enable_netbios_help: +Select 'Yes' to enable NetBIOS. If enabled, SMBv1 protocol will also be recognized. If disabled, +clients must use SMBv2 or higher. +:end + +:smb_enable_wsd_help: +Select 'Yes' to enable WSD (WS-Discovery). The only reason to turn this off is when you are running an +old LAN setup based on SMBv1. +:end + +:smb_wsd_options_help: +This is a command line options string passed to the WSD daemon upon startup. Leave this field blank unless +instructed by support to put something here. +:end + +:smb_extra_conf_help: +Use this page to make changes to your `smb-extra.conf` file. Samba will need +to be restarted in order for changes to take effect. +:end + +:smb_extra_button_help: +Click the **Apply** button to commit the current edits. Click **Reset** to +undo any changes you make (before Saving). Click **Done** to exit this page. +:end + +:smb_workgroup_help: +Enter your local network Workgroup name. Usually this is "WORKGROUP". +:end + +:smb_local_master_help: +If set to 'Yes' then the server will fully participate in browser elections, and in the absence +of other servers, will usually become the local Master Browser. +:end + +:nfs_enable_help: +Select 'Yes' to enable the NFS protocol. +:end + +:nfs_tunable_fuse_remember_help: +When NFS is enabled, this Tunable may be used to alleviate or solve instances of "NFS Stale File Handles" +you might encounter with your NFS client. + +In essence, (fuse_remember) tells an internal subsystem (named "fuse") how long to "remember" or "cache" +file and directory information associated with user shares. When an NFS client attempts to access a file +(or directory) on the server, and that file (or directory) name is not cached, then you could encounter +"stale file handle". + +The numeric value of this tunable is the number of seconds to cache file/directory name entries, +where the default value of 330 indicates 5 1/2 minutes. There are two special values you may also set +this to: + +* 0 which means, do not cache file/directory names at all, and +* -1 which means cache file/directory names forever (or until array is stopped) + +A value of 0 would be appropriate if you are enabling NFS but only plan to use it for disk shares, +not user shares. + +A value of -1 would be appropriate if no other timeout seems to solve the "stale file handle" on +your client. Be aware that setting a value of -1 will cause the memory footprint to grow by approximately +108 bytes per file/directory name cached. Depending how much RAM is installed in your server and how many +files/directories you access via NFS this may or may not lead to out-of-memory conditions. +:end + +:nfs_server_max_protocol_help: +Select the max NFS Protocol version you want your Unraid server to support. +:end + +:nfs_client_max_protocol_help: +Select the max NFS Protocol version you want to use to mount your remote shares. +:end + +:nfs_threads_help: +Set the number of threads for NFS. For light NFS loads, 8 is sufficient. For heaver NFS loads, a higher number of threads might be appropriate. + +If there aren't enough threads, NFS will probably crash. +:end + +:shares_enable_disk_help: +If set to No, disk shares are unconditionally not exported. + +If set to Yes, disk shares may be exported. **WARNING:** Do not copy data from a disk share to a user share +unless you *know* what you are doing. This may result in the loss of data and is not supported. + +If set to Auto, only disk shares not participating in User Shares may be exported. +:end + +:shares_enable_shares_help: +If set to 'Yes' the User Shares feature is activated. +:end + +:shares_included_disks_help: +This setting defines the set of array disks which are *included* in User Shares. +Unchecking all disks will allow **all** array disks to be included. +:end + +:shares_excluded_disks_help: +This setting defines the set of array disk which are *excluded* from User Shares. +Uncheck all disks in order to not exclude any disks + +**Note:** Each separate User Share also includes its own set of Included and Excluded +disks which represent a subset of the Included/Excluded disks defined here. +:end + +:shares_exclusive_shares_help: +If set to Yes, share directories under /mnt/user are actually symlinks to the share directory on a storage volume +provided the following conditions are met: + +* The Primary storage for a share is set to a pool. +* The Secondary storage for a share is set to **none**. +* The share exists on a single volume. +* The share is **not** exported over NFS. + +The advantage of *exclusive* shares is that transfers bypass the FUSE layer which may significantly +increase I/O performance. +:end + +:shares_tunable_hard_links_help: +If set to Yes then support the link() operation. + +If set to No then hard links are not supported. + +Notes: + +* Setting to Yes may cause problems for older media and dvd/bluray players accessing shares using NFS. +* No matter how this is set, the **mover** will still properly handle any detected hard links. +:end + +:shares_tunable_direct_io_help: +**Experimental**: If set to Yes then mount User Share file system with FUSE *direct_io* mount option. +This will increase write performance but might possibly decrease read performance. + +*Auto* selects No. +:end + +:shares_fuse_file_descriptors_io_help: +Set the number of fuse file descriptors. With a lot of file activity on the array, you may need to increase this value. +:end + +:syslog_local_server_help: +Let the server act as a central syslog server and collect syslog messages from other systems. +The server can listen on UDP, TCP or both with a selectable port number. + +Syslog information is stored either per IP address or per hostname. That is every system gets its own syslog file. +:end + +:syslog_local_folder_help: +Select the share folder where the syslogs will be stored. +It is recommended that you use a share located on the cache drive to prevent array disk spinups. +:end + +:syslog_remote_system_identifier_help: +Select the identifier for the remote system (used in the logfile name). + +* "IP Address" uses the IP address (IPv4 or IPv6) of the sending system. +* "Hostname (from syslog message)" uses the hostname included in each syslog message. +* "Hostname (from DNS reverse lookup)" performs a DNS reverse lookup for the sending IP and uses the result. +:end + +:syslog_local_rotation_help: +By default LOG rotation is disabled and will create a single LOG file of unlimited size. + +Enable LOG rotation to limit the size of the LOG file and specify the number of files to keep in the rotation scheme. +:end + +:syslog_local_file_size_help: +Specifies the maximum LOG size. When exceeded LOG rotation will occur. +:end + +:syslog_local_file_number_help: +Specifies the number of additional LOG files to keep in the rotation scheme. +:end + +:syslog_remote_server_help: +Enter a name or IP address of a remote syslog server. +This will send a copy of the syslog messages to the designated server. +:end + +:syslog_mirror_flash_help: +This setting is NO by default and must be used with care to avoid unnecessary wear and tear of the USB device. + +Change this setting to YES when troubleshooting is required and it is not possible to get the regular diagnostics information. +A mirror of the syslog file is stored in the **logs** folder of the flash device. +:end + +:syslog_shutdown_flash_help: +This setting is YES by default and enables the system to copy the syslog file to the USB device on shutdown or reboot. + +After rebooting, the syslog from this run will be visible on Tools > Syslog > syslog-previous; +it will also be included in diagnostics as logs/syslog-previous.txt +:end + +:confirm_reboot_help: +Choose if rebooting or powering down the server needs a confirmation checkbox. +:end + +:confirm_array_stop_help: +Choose if stopping the array needs a confirmation checkbox. +:end + +:display_settings_help: +The display settings below determine how items are displayed on screen. Use these settings to tweak the visual effects to your likings. + +You can experiment with these settings as desired, they only affect visual properties. +:end + +:display_width_help: +**Boxed** is the legacy setting which constrains the content width to maximum 1920 pixels + +**Unlimited** allows content to use all available width, which maybe useful on wide screens +:end + +:display_font_size_help: +Changes the font size in the GUI. This is a per device setting. +:end + +:display_tty_size_help: +Changes the font size of terminal windows. +:end + +:display_page_view_help: +Changes how certain pages are displayed. In **Tabbed** mode different sections will be displayed in different tabs, +while in **Non-tabbed** mode sections are displayed under each other. +:end + +:display_users_menu_help: +The Users Menu can be part of the header or part of the Settings menu. +You can move the Users Menu if insufficient space in the header is available to display all menus. +:end + +:display_listing_height_help: +**Automatic** : long listings are displayed as is, and the user needs to scroll the whole page to see the bottom + +**Fixed** : long listings are displayed in a window with a fixed size, user can scroll this window to see the bottom +:end + +:display_wwn_device_id_help: +World Wide Name (WWN) is a unique identifier used for SAS attached devices. + +Select *Disabled* to suppress the appending of WWN to the device identification + +Select "Automatic" to append WWN to the device identification in case of SAS devices +:end + +:display_custom_text_color_help: +Overrule the default text color in the header. This can be used to match the text color with a background image. +:end + +:display_custom_background_color_help: +Overrule the default background color in the header. This can be used to match the background color with a custom text color. +:end + +:display_custom_banner_help: +Image will be scaled to 1920x90 pixels. The maximum image file upload size is 512 kB (524,288 bytes). +:end + +:display_temperature_unit_help: +Selects the temperature unit for the disk temperature thresholds. Changing the unit will adjust the existing value in the disk temperature thresholds as appropriate. + +Make sure any newly entered values represent the selected temperature unit. +:end + +:display_favorites_enabled_help: +Enables favorite support. If set to no, will stop heart icon showing for additions. If existing favorites are saved, favorites tab and pre-saved options will still continue to show and function until all are deleted. +:end + +:vms_enable_help: +Stopping the VM Manager will first attempt to shutdown all running VMs. After 60 seconds, any remaining VM instances will be terminated. +:end + +:vms_disable_help: +Stop VMs from Autostarting\Starting when VM Manager starts or open is run from the gui to start, error message will be seen. +:end + +:vms_libvirt_volume_help: +This is the libvirt volume. +:end + +:vms_libvirt_vdisk_size_help: +If the system needs to create a new libvirt image file, this is the default size to use specified in GB. +To resize an existing image file, specify the new size here. Next time the Libvirt service is started the file (and file system) will be increased to the new size (but never decreased). +:end + +:vms_libvirt_location_help: +You must specify an image file for Libvirt. The system will automatically create this file when the Libvirt service is first started. +:end + +:vms_libvirt_storage_help: +Specify a user share that contains all your VM subdirectories with vdisks +:end + +:vms_libvirt_iso_storage_help: +Specify a user share that contains all your installation media for operating systems +:end + +:vms_virtio_driver_help: +Specify the virtual CD-ROM (ISO) that contains the VirtIO Windows drivers as provided by the Fedora Project. +Download the latest ISO from here: fedoraproject.org + +When installing Windows, you will reach a step where no disk devices will be found. There is an option to browse for drivers on that screen. +Click browse and locate the additional CD-ROM in the menu. Inside there will be various folders for the different versions of Windows. +Open the folder for the version of Windows you are installing and then select the AMD64 subfolder inside (even if you are on an Intel system, select AMD64). +Three drivers will be found. Select them all, click next, and the vDisks you have assigned will appear. +:end + +:vms_network_source_help: +Select the name of the network you wish to use as default for your VMs. +You can choose between **'bridges'** created under network settings or +**'libvirt'** created with virsh command in the terminal. +The bridge **'virbr0'** and the associated virtual network **'default'** are +created by libvirt. +Both utilizes NAT (network address translation) and act as a DHCP server to hand out IP addresses to virtual machines directly. +More optional selections are present for bridges under network settings or for libvirt networks with the virsh command in the terminal. + +**If your are unsure, choose 'virbr0' as the recommended Unraid default.** + +NOTE: You can also specify a network source on a per-VM basis. + +**IMPORTANT: Neither Libvirt nor Unraid automatically brings up an interface that is assigned to a Libvirt network. +Before you use a Libvirt network, please go to Settings -> Network Settings and, if necessary, manually set the associated interface to up.** +:end + +:vms_host_shutdown_help: +When shutting down the server, this defines the action to take upon running VMs. If *Hibernate VMs* is chosen, +the VM will be instructed to hibernate (if supported) otherwise it will attempt a VM shutdown. +:end + +:vms_shutdown_timeout_help: +When shutting down the server, this defines how long to wait in seconds for *graceful* VM shutdown before forcing shutdown to continue. +NOTE: It's recommended to shut down guest VMs from within the VM. +:end + +:vms_console_help: +For setting the console options to show on context menus. Web will show only inbuild web clients(VNC and SPICE), +Virtual Manager Remote Viewer will only show the Remote Viewer option. Both will show both Web and Remote Viewer. +:end + +:vms_rdpopt_help: +Adds option to menu to start RDP. RDP file is downloaded. You need to set browser to open when ready. +:end + +:vms_usage_help: +Show metrics for CPU both guest and host percentage, memory, disk io and network io. +:end + +:vms_usage_timer_help: +Setting in seconds for metrics refresh time. +:end + +:vms_acs_override_help: +*PCIe ACS override* allows various hardware components to expose themselves as isolated devices. +Typically it is sufficient to isolate *Downstream* ports. +A hardware component may need the setting *Multi-function* or *Both* to further isolate different hardware functions.
+A reboot is required for changes to this setting to take affect. + +**Warning: use of this setting could cause possible data corruption with certain hardware configurations.** +Please visit the [Lime Technology forums](https://forums.unraid.net/forum/51-vm-engine-kvm) for more information. +:end + +:vms_vfio_interupts_help: +If your system doesn't support interrupt remapping, these can be enabled by allowing unsafe interrupts.
+A reboot will be required for changes to this setting to take affect. + +**Warning: use of this setting could cause possible data corruption with certain hardware configurations.** +Please visit the [Lime Technology forums](https://forums.unraid.net/forum/51-vm-engine-kvm) for more information. +:end + +:vms_libvirt_log_help: +View the log for libvirt: View libvirtd.log +:end + +:vms_scrub_help: +**Scrub** runs the *btrfs scrub* program to check file system integrity. +If repair is needed you should check the *Correct file system errors* and run a second Scrub pass; this will permit *btrfs scrub* to fix the file system. +:end + +:vms_cancel_help: +**Cancel* will cancel the Scrub operation in progress. +:end + +:eth_interface_description_help: +Use this optional field to provide additional information about the purpose of the connection. +:end + +:eth_mac_address_help: +This is the hardware address of the interface. +When tagging is enabled all VLANs on this interface will share the same hardware address. +:end + +:eth_enable_bonding_help: +Bonding is a feature that combines multiple physical Ethernet interfaces into a single *bonded* interface named **bond0**. +This can be used to improve the connection redundancy and/or throughput of the system. +Different bonding modes are supported (see below), but some modes require proper switch support. +:end + +:eth_bonding_mode_help: +**Mode 0 (balance-rr)**
+This mode transmits packets in a sequential order from the first available slave through the last. +If two real interfaces are slaves in the bond and two packets arrive destined out of the bonded interface the first will be transmitted on the first slave and the second frame will be transmitted on the second slave. +The third packet will be sent on the first and so on. This provides load balancing and fault tolerance. + +**Mode 1 (active-backup) - default**
+This mode places one of the interfaces into a backup state and will only make it active if the link is lost by the active interface. +Only one slave in the bond is active at an instance of time. A different slave becomes active only when the active slave fails. +This mode provides fault tolerance. + +**Mode 2 (balance-xor)**
+This mode transmits packets based on an XOR formula. Source MAC address is XOR'd with destination MAC address modula slave count. +This selects the same slave for each destination MAC address and provides load balancing and fault tolerance. + +**Mode 3 (broadcast)**
+This mode transmits everything on all slave interfaces. This mode is least used (only for specific purpose) and provides only fault tolerance. + +**Mode 4 (802.3ad)**
+This mode is known as *Dynamic Link Aggregation*. It creates aggregation groups that share the same speed and duplex settings. +It requires a switch that supports IEEE 802.3ad dynamic link. +Slave selection for outgoing traffic is done according to the transmit hash policy, which may be changed from the default simple XOR policy via the xmit_hash_policy option. +Note that not all transmit policies may be 802.3ad compliant, particularly in regards to the packet mis-ordering requirements of section 43.2.4 of the 802.3ad standard. +Different peer implementations will have varying tolerances for noncompliance. + +**Mode 5 (balance-tlb)**
+This mode is called *Adaptive transmit load balancing*. The outgoing traffic is distributed according to the current load and queue on each slave interface. +Incoming traffic is received by the current slave. + +**Mode 6 (balance-alb)**
+This mode is called *Adaptive load balancing*. This includes balance-tlb + receive load balancing (rlb) for IPV4 traffic. +The receive load balancing is achieved by ARP negotiation. +The bonding driver intercepts the ARP Replies sent by the server on their way out and overwrites the src hw address with the unique hw address of one of the slaves in the bond +such that different clients use different hw addresses for the server. + +*Mode 1 (active-backup) is the recommended setting. Other modes allow you to set up a specific environment, but may require proper switch support. +Choosing a unsupported mode can result in a disrupted communication.* +:end + +:eth_bonding_members_help: +Select which interfaces are member of the *bonded* interface. By default eth0 is a member, while other interfaces are optional. +:end + +:eth_enable_bridging_help: +Bridging is a feature which creates a virtual bridge and allows VMs and Docker containers to communicate directly with the physical Ethernet port. +Both bonding and bridging can be combined to let VMs or containers communicate over a *bonded* interface. +:end + +:eth_bridging_members_help: +Select which interfaces are member of the *bridged* interface. By default eth0 is a member, while other interfaces are optional. +:end + +:eth_network_protocol_help: +Select which protocol(s) are used. By default IPv4 only is used.
+When both IPv4 and IPv6 is selected, each protocol can be configured independently. +:end + +:eth_ipv4_address_assignment_help: +The following settings are possible: + +*Automatic* - the server will attempt to obtain a IPv4 address from the local DHCP server
+*Static* - the IPv4 address is manually set for this interface
+*None* - no IPv4 address is assigned to the interface (only available for VLANs) +:end + +:eth_ipv4_address_help: +Greyed out when using automatic IP assignment. Otherwise specify here the IPv4 address and mask of the system. +:end + +:eth_ipv4_default_gateway_help: +Greyed out when using automatic IP assignment. Otherwise specify here the IPv4 address of your router. +:end + +:eth_ipv4_dns_server_assignment_help: +If set to *Automatic* the server will use IPv4 DNS server(s) returned by the local automatic assignment.
+If set to *Static* you may enter your own list. + +This is useful in Active Directory configurations where you need to set the first DNS Server entry to the IP address of your AD Domain server. +:end + +:eth_ipv4_dns_server_help: +This is the primary IPv4 DNS server to use. Enter a IPv4 address. + +Note: for *Active Directory* you **must** ensure this is set to the IP address of your AD Domain server. +:end + +:eth_ipv4_dns_server2_help: +This is the IPv4 DNS server to use when IPv4 DNS server 1 is down. +:end + +:eth_ipv4_dns_server3_help: +This is the IPv4 DNS server to use when IPv4 DNS servers 1 and 2 are both down. +:end + +:eth_ipv6_address_assignment_help: +The following settings are possible: + +*Automatic* - the server will attempt to obtain a IPv6 address from the local DHCP server or Router Advertisement (RA)
+*Static* - the IPv6 address is manually set for this interface
+*None* - no IPv6 address is assigned to the interface (only available for VLANs) +:end + +:eth_ipv6_address_help: +Greyed out when using automatic IP assignment. Otherwise specify here the IPv6 address of the system. +:end + +:eth_ipv6_default_gateway_help: +Greyed out when using automatic IP assignment. Otherwise specify here the IPv6 address of your router. +:end + +:eth_ipv6_privacy_extensions_help: +Enable or disable the generation of a random IPv6 interface identifier according to RFC4941. This is similar to the temporary IPv6 address generation on Windows machines. +:end + +:eth_ipv6_dns_server_assignment_help: +If set to *Automatic* the server will use IPv6 DNS server(s) returned by the local automatic assignment.
+If set to *Static* you may enter your own list. +:end + +:eth_ipv6_dns_server_help: +This is the primary IPv6 DNS server to use. Enter a IPv6 address. +:end + +:eth_ipv6_dns_server2_help: +This is the IPv6 DNS server to use when IPv6 DNS server 1 is down. +:end + +:eth_ipv6_dns_server3_help: +This is the IPv6 DNS server to use when IPv6 DNS servers 1 and 2 are both down. +:end + +:eth_desired_mtu_help: +This is the MTU size to use on the physical Ethernet interface. +If left blank, the MTU will automatically be determined (by default 1500 bytes). +:end + +:eth_enable_vlans_help: +By default no VLANs are configured.
+Enabling VLANs extends the number of logical connections over the same physical connection. + +Note: your router and switch must support VLANs too when this feature is used. +:end + +:eth_vlan_number_help: +Give each VLAN a unique identifier. Numbers range from 1 to 4095. +:end + +:eth_network_rules_help: +The interface assignment rules can be changed here and might be necessary to set the preferred interface for managing Unraid - *use with care, usually there is no need to change*. + +**eth0** is the main interface used to manage the Unraid system. The other interfaces are optional and may be used as desired. +Every interface must be uniquely identified by its MAC (hardware) address. + +The interface assignment is stored on the flash device under */config/network-rules.cfg*. This file can be viewed with any editor, but it is recommended to make changes via the webGUI only.
+Deleting the file *network-rules.cfg* from the flash device will restore automatic interface assignment after a system reboot. +:end + +:eth_routing_table_help: +Enter a valid IPv4 route in the format *nnn.nnn.nnn.nnn/xx*, e.g. *192.168.1.0/24*
+or enter a valid IPv6 route in the format *nnnn:nnnn:nnnn::nnnn/xxx*, e.g. *fe80::3ad8:2fff:fe25:9709/64* + +Select the gateway from the dropdown list or enter a valid IPv4/IPv6 address as gateway value. + +The metric value is optional, it defaults to 1. Use it to select the preferred gateway when more than one entry of the same route exists. +:end + +:eth_network_extra_include_help: +Enter one or more interface names or IP addresses which will be included in the list of listening interfaces for local system services. + +This is particularly useful when you have created custom interfaces (e.g. tailscale VPN tunnel) which are used to access local system services. +:end + +:eth_network_extra_exclude_help: +Enter one or more interface names or IP addresses which will be excluded from the list of listening interfaces for local system services. + +This can be used to exclude dedicated local interfaces (e.g. p-t-p connections) or exclude dynamic interfaces (WireGuard tunnels) from using local system services. +:end + +:apc_ups_daemon_help: +Set to 'Yes' to enable apcupsd and start the daemon, set to 'No' to disable apcupsd and stop the daemon. +:end + +:apc_ups_cable_help: +Defines the type of cable connecting the UPS to your computer.Possible generic choices for 'cable' are: + ++ USB, Simple, Smart, Ether, or Custom to specify a special cable. +:end + +:apc_ups_custom_cable_help: +Specify a special cable by model number, only applicable when *UPS cable* is set to Custom. + ++ 940-0119A, 940-0127A, 940-0128A, 940-0020B ++ 940-0020C, 940-0023A, 940-0024B, 940-0024C ++ 940-1524C, 940-0024G, 940-0095A, 940-0095B ++ 940-0095C, 940-0625A, M-04-02-2000 +:end + +:apc_ups_type_help: +Define a *UPS type*, which corresponds to the type of UPS you have (see the Description for more details). + ++ **USB** - most new UPSes are USB ++ **APCsmart** - newer serial character device, appropriate for SmartUPS models using a serial cable (not USB) ++ **Net** - network link to a master apcupsd through apcupsd's Network Information Server. This is used if the UPS powering your computer is connected to a different computer for monitoring ++ **SNMP** - SNMP network link to an SNMP-enabled UPS device ++ **Dumb** - old serial character device for use with simple-signaling UPSes ++ **PCnet** - PowerChute Network Shutdown protocol which can be used as an alternative to SNMP with the AP9617 family of smart slot cards ++ **ModBus** - serial device for use with newest SmartUPS models supporting the MODBUS protocol +:end + +:apc_ups_device_help: +Enter the *device* which corresponds to your situation, only applicable when *UPS type* is not set to USB. + ++ **apcsmart** - /dev/tty** ++ **net** - hostname:port. Hostname is the IP address of the NIS server. The default port is 3551 ++ **snmp** - hostname:port:vendor:community. Hostname is the ip address or hostname of the UPS on the network. Vendor can be can be "APC" or "APC_NOTRAP". "APC_NOTRAP" will disable SNMP trap catching; you usually want "APC". Port is usually 161. Community is usually "private" ++ **dumb** - /dev/tty** ++ **pcnet** - ipaddr:username:passphrase:port. ipaddr is the IP address of the UPS management card. username and passphrase are the credentials for which the card has been configured. port is the port number on which to listen for messages from the UPS, normally 3052. If this parameter is empty or missing, the default of 3052 will be used ++ **modbus** - /dev/tty** +:end + +:apc_ups_override_ups_capacity_help: +If your device doesn't natively report Nominal Power (`NOMPOWER`) from `apcupsd`, but does report the Load Percentage (`LOADPCT`), you can manually define the UPS capacity rating in Watts (W) (this is the 'real power' value in Watts (W), not the 'apparent power' in Volt Amps (VA), and should be detailed on your UPS manual or product listing) and the plugin will dynamically calculate a virtual Nominal Power estimate (`≈`) by comparing the Override UPS Capacity (W) and the current Load Percentage. It is only an estimate, as it doesn't factor in things like the UPS' efficiency. +:end + +:apc_battery_level_help: +If during a power failure, the remaining battery percentage (as reported by the UPS) is below or equal to *Battery level*, apcupsd will initiate a system shutdown. +:end + +:apc_runtime_left_help: +If during a power failure, the remaining runtime in minutes (as calculated internally by the UPS) is below or equal to this field, apcupsd, will initiate a system shutdown. +:end + +:apc_battery_time_help: +If during a power failure, the UPS has run on batteries for *time-out* many seconds or longer; apcupsd will initiate a system shutdown. A value of zero disables this timer. + +If you have a Smart UPS, you will most likely want to disable this timer by setting it to zero. +That way, your UPS will continue on batteries until either the % charge remaining drops to or below *Battery level* or the remaining battery runtime drops to or below *minutes*. + +Of course - when testing - setting this to 60 causes a quick system shutdown if you pull the power plug. +If you have an older dumb UPS, you will want to set this to less than the time you know you can run on batteries. +:end + +:apc_note_help: +**Note:** *Battery level*, *Runtime left*, and *Time on battery* work in conjunction, so the first that occurs will cause the initiation of a shutdown. +:end + +:apc_killups_help: +Set to *Yes* to turn off the power to the UPS after a shutdown. +:end + +:parity_check_scheduled_help: +By default no parity check is scheduled. Select here the desired schedule. This can be one of the preset schedules for daily, weekly, monthly, yearly or a custom schedule. +:end + +:parity_day_of_the_week_help: +When a **weekly** or **custom** schedule is selected then choose here the preferred *day of the week*, in the other schedules this setting is not used and unavailable. +:end + +:parity_week_of_the_month_help: +When a **monthly** or **yearly** schedule is selected then choose here the preferred *day of the month*. +When a **custom** schedule is selected then choose here the preferred *week of the month*, in the other schedules this setting is not used and unavailable. +:end + +:parity_time_of_the_day_help: +Choose the desired *time of the day* to start the schedule. Time granularity is given in half hour periods. +:end + +:parity_month_of_the_year_help: +When a **yearly** or **custom** schedule is selected then choose here the preferred *month of the year*, in the other schedules this setting is not used and unavailable. +:end + +:parity_write_corrections_help: +Choose here whether any parity errors found during the check, need to be corrected on the Parity disk or not. +:end + +:parity_cumulative_check_help: +Change this setting to **Yes** to divide long duration parity checks over multiple periods. This is useful when the system needs to be available and a parity check runs only during off hours. +:end + +:parity_accumulation_frequency_help: +Specifies how accumulation periods are executed. + +**Daily** means every subsequent day the parity check continues until finished + +**Weekly** means every subsequent week the parity check continues until finished +:end + +:parity_accumulation_duration_help: +Specifies how long each accumulated period runs, expressed in hours. The **Time of the day** speficies the start time of the period. +:end + +:mover_schedule_help: +Choose a mover schedule ranging from hourly, daily, weekly and monthly. + +The interval determines how fast the mover will activated, it runs in the background. +:end + +:mover_day_of_the_week_help: +Choose a day when the weekly schedule is selected. Otherwise disabled. +:end + +:mover_day_of_the_month_help: +Choose a date when the monthly schedule is selected. Otherwise disabled. +:end + +:mover_time_of_the_day_help: +When an hourly schedule is selected this will set the interval in hours. An interval always starts on the whole hour (minute 0). + +For the other schedules choose here the time of the day the mover should start. +:end + +:mover_logging_help: +Write mover messages to the syslog file. +:end + +:notifications_display_help: +In *Detailed* view all notifications will be displayed on screen as soon as they arrive.
+Notifications can be acknowledged individually or all at once. + +In *Summarized* view notifications will be counted only and the number of unread notifications is shown in the menu header per category.
+Click on the counters to either acknowledge or view the unread notifications. +:end + +:notifications_display_position_help: +Choose the position of where notifications appear on screen in *Detailed* view. Multiple notifications are stacked, bottom-to-top or +top-to-bottom depending on the selected placement. +:end + +:notifications_auto_close_help: +Number of seconds before notifications are automatically closed in *Detailed* view.
+A value of 0 disables automatic closure. +:end + +:notifications_date_format_help: +Select the desired date format which is used in the notifications archive. Recommended is YYYY-MM-DD, which makes the date/time column sortable in a sensible way. +:end + +:notifications_time_format_help: +Select the desired time format which is used in the notifications archive. Recommended is 24 hours, which makes the date/time column sortable in a sensible way. +:end + +:notifications_store_flash_help: +By default notifications are stored on RAM disk, which will get lost upon system reboot. +Notifications may be stored permanently on the flash drive under folder '/boot/config/plugins/dynamix' instead. +:end + +:notifications_system_help: +By default the notifications system is disabled. Enable it here to start receiving notifications. +The following sections give more options about which and what type of notifications will be sent. +:end + +:notifications_os_update_help: +Start a periodic verification and notify the user when a new version of the Unraid OS system is detected. +Use the checkboxes below to select how notifications need to be given; by browser, by email and/or by custom agent. +:end + +:notifications_plugins_update_help: +Start a periodic verification and notify the user when a new version of one or more of the installed plugins is detected. +Use the checkboxes below to select how notifications need to be given; by browser, by email and/or by custom agent. +:end + +:notifications_docker_update_help: +Start a periodic verification and notify the user when a new version of one or more of the installed dockers is detected. +Use the checkboxes below to select how notifications need to be given; by browser, by email and/or by custom agent. +:end + +:notifications_array_status_help: +Start a periodic array health check (preventive maintenance) and notify the user the result of this check. +:end + +:notifications_agent_selection_help: +Use the checkboxes above to select what and how notifications need to be given; by browser, by email and/or by a service.
+Tip: you can use custom notification agents; just add them to "/boot/config/plugins/dynamix/notification/agents" directory and check 'Agents'. +:end + +:notifications_classification_help: +Notifications are classified as: + +*notice* - these are informative notifications and do not indicate a problem situation, e.g. a new version is available
+*warning* - these are attentive notifications and may indicate future problems, e.g. a hard disk is hotter than usual
+*alert* - these are serious notifications and require immediate attention, e.g. a failing hard disk
+ +Choose for each classification how you want to be notified. +:end + +:smtp_preset_service_help: +Select a preset service to set the basic service settings. +:end + +:smtp_email_address_help: +Email address of your mail account. This address is used as sender of the notifications. +:end + +:smtp_recipients_help: +Recipients of status and error notifications. Specify one or more email addresses, separate multiple email addresses with a space. +:end + +:smtp_priority_help: +Set email header with high importance, when there is a problem detected by Unraid OS. +:end + +:smtp_subject_prefix_help: +Set a prefix for easy recognition of Unraid OS messages. +:end + +:smtp_mail_server_help: +Specify the name of the email server. Use the preset service selection to have this filled-in automatically. +:end + +:smtp_mail_server_port_help: +Specify the port of the email server. Use the preset service selection to have this filled-in automatically. +:end + +:smtp_use_ssl_tls_help: +Specifies whether to use SSL/TLS to talk to the SMTP server. +:end + +:smtp_use_starttls_help: +Specifies whether to use STARTTLS before starting SSL negotiation - See RFC 2487. +:end + +:smtp_define_tls_cert_help: +Select only when you have a certificate which required for communication. +:end + +:smtp_tls_cert_location_help: +The file name of an RSA certificate to use for TLS - as required. +:end + +:smtp_authentication_method_help: +Select the correct authentication method for your email server. Use test to verify that access is working properly. +:end + +:smtp_username_password_help: +Enter the username and password to login to your email account. Be aware that the password is stored unencrypted in the email configuration file. +:end + +:plugin_install_help: +To download and install a plugin, enter the plg file URL and click **Install**. A window will open +that displays install progress. Do not close this window until install has completed. You may also specify +the local file name of an extension. +:end + +:plugin_error_help: +These plugins were not installed because of some kind of installation error. You should delete these +plugins and then **reboot** your server.* +:end + +:plugin_stale_help: +These plugins were not installed because newer code already exists. It is safe to simply delete these. +:end + +:docker_client_general_help: +Templates are a quicker way to setting up Docker Containers on your Unraid server. There are two types of templates: + +**Default templates**
+When valid repositories are added to your Docker Repositories page, they will appear in a section on this drop down for you to choose (master categorized by author, then by application template). +After selecting a default template, the page will populate with new information about the application in the Description field, and will typically provide instructions for how to setup the container. +Select a default template when it is the first time you are configuring this application. + +**User-defined templates**
+Once you've added an application to your system through a Default template, +the settings you specified are saved to your USB flash device to make it easy to rebuild your applications in the event an upgrade were to fail or if another issue occurred. +To rebuild, simply select the previously loaded application from the User-defined list and all the settings for the container will appear populated from your previous setup. +Clicking create will redownload the necessary files for the application and should restore you to a working state. +To delete a User-defined template, select it from the list above and click the red X to the right of it. +:end + +:docker_client_name_help: +Give the container a name or leave it as default. Two characters minimum. First character must be a-z A-Z 0-9 Remaining characters a-z A-Z 0-9 . - _ +:end + +:docker_client_overview_help: +A description for the application container. Supports basic HTML mark-up. +:end + +:docker_client_additional_requirements_help: +Any additional requirements the container has. Supports basic HTML mark-up. +:end + +:docker_client_repository_help: +The repository for the application on the Docker Registry. Format of authorname/appname. +Optionally you can add a : after appname and request a specific version for the container image. +:end + +:docker_client_support_thread_help: +Link to a support thread on Lime-Technology's forum. +:end + +:docker_client_project_page_help: +Link to the project page (eg: www.plex.tv) +:end + +:docker_client_readme_help: +Link to a readme file or page +:end + +:docker_client_hub_url_help: +The path to the container's repository location on the Docker Hub. +:end + +:docker_client_template_url_help: +This URL is used to keep the template updated. +:end + +:docker_client_icon_url_help: +Link to the icon image for your application (only displayed on dashboard if Show Dashboard apps under Display Settings is set to Icons). +:end + +:docker_client_webui_help: +When you click on an application icon from the Docker Containers page, the WebUI option will link to the path in this field. +Use [IP] to identify the IP of your host and [PORT:####] replacing the #'s for your port. +:end + +:docker_extra_parameters_help: +If you wish to append additional commands to your Docker container at run-time, you can specify them here.
+For all possible Docker run-time commands, see here: https://docs.docker.com/reference/run/ +:end + +:docker_post_arguments_help: +If you wish to append additional arguments AFTER the container definition, you can specify them here. +The content of this field is container specific. +:end + +:docker_cpu_pinning_help: +Checking a CPU core(s) will limit the container to run on the selected cores only. Selecting no cores lets the container run on all available cores (default) +:end + +:docker_fixed_ip_help: +If the Bridge type is selected, the application’s network access will be restricted to only communicating on the ports specified in the port mappings section. +If the Host type is selected, the application will be given access to communicate using any port on the host that isn’t already mapped to another in-use application/service. +Generally speaking, it is recommended to leave this setting to its default value as specified per application template. + +IMPORTANT NOTE: If adjusting port mappings, do not modify the settings for the Container port as only the Host port can be adjusted. +:end + +:docker_container_network_help: +This allows your container to utilize the network configuration of another container. Select the appropriate container from the list.
This setup can be particularly beneficial if you wish to route your container's traffic through a VPN. +:end + +:docker_tailscale_help: +Enable Tailscale to add this container as a machine on your Tailnet. +:end + +:docker_tailscale_hostname_help: +Provide the hostname for this container. It does not need to match the container name, but it must be unique on your Tailnet. Note that an HTTPS certificate will be generated for this hostname, which means it will be placed in a public ledger, so use a name that you don't mind being public. +For more information see enabling https. +:end + +:docker_tailscale_be_exitnode_help: +Enable this if other machines on your Tailnet should route their Internet traffic through this container, this is most useful for containers that connect to commercial VPN services. +Be sure to authorize this Exit Node in your Tailscale Machines Admin Panel. +For more details, see the Tailscale documentation on Exit Nodes. +:end + +:docker_tailscale_exitnode_ip_help: +Optionally route this container's outgoing Internet traffic through an Exit Node on your Tailnet. Choose the Exit Node or input its Tailscale IP address. +For more details, see Exit Nodes. +:end + +:docker_tailscale_lanaccess_help: +Only applies when this container is using an Exit Node. Enable this to allow the container to access the local network. + +WARNING: Even with this feature enabled, systems on your LAN may not be able to access the container unless they have Tailscale installed. +:end + +:docker_tailscale_userspace_networking_help: +When enabled, this container will operate in a restricted environment. Tailscale DNS will not work, and the container will not be able to initiate connections to other Tailscale machines. However, other machines on your Tailnet will still be able to communicate with this container. + +When disabled, this container will have full access to your Tailnet. Tailscale DNS will work, and the container can fully communicate with other machines on the Tailnet. +However, systems on your LAN may not be able to access the container unless they have Tailscale installed. +:end + +:docker_tailscale_ssh_help: +Tailscale SSH is similar to the Docker "Console" option in the Unraid webgui, except you connect with an SSH client and authenticate via Tailscale. +For more details, see the Tailscale SSH documentation. +:end + +:docker_tailscale_serve_mode_help: +Enabling Serve will automatically reverse proxy the primary web service from this container and make it available on your Tailnet using https with a valid certificate! + +Note that when accessing the Tailscale WebUI url, no additional authentication layer is added beyond restricting it to your Tailnet - the container is still responsible for managing usernames/passwords that are allowed to access it. Depending on your configuration, direct access to the container may still be possible as well. + +For more details, see the Tailscale Serve documentation. + +If the documentation recommends additional settings for a more complex use case, enable "Tailscale Show Advanced Settings". Support for these advanced settings is not available beyond confirming the commands are passed to Tailscale correctly. + +Funnel is similar to Serve, except that the web service is made available on the open Internet. Use with care as the service will likely be attacked. As with Serve, the container itself is responsible for handling any authentication. + +We recommend reading the Tailscale Funnel documentation before enabling this feature. + +Note: Enabling Serve or Funnel publishes the Tailscale hostname to a public ledger. +For more details, see the Tailscale Documentation: Enabling HTTPS. +:end + +:docker_tailscale_serve_port_help: +This field should specify the port for the primary web service this container offers. Note: it should specify the port in the container, not a port that was remapped on the host. + +The system attempted to determine the correct port automatically. If it used the wrong value then there is likely an issue with the "Web UI" field for this container, visible by switching from "Basic View" to "Advanced View" in the upper right corner of this page. + +In most cases this port is all you will need to specify in order to Serve the website in this container, although additional options are available below for more complex containers. + +This value is passed to the `` portion of this command which starts serve or funnel:
+`tailscale [serve|funnel] --bg -- :`
+For more details see the Tailscale Serve Command Line documentation. +:end + +:docker_tailscale_show_advanced_help: +Here there be dragons! +:end + +:docker_tailscale_serve_target_help: +When not specified, this value defaults to http://localhost. It is passed to the `` portion of this command which starts serve or funnel:
+`tailscale [serve|funnel] --bg -- :`
+For more details see the Tailscale Serve Command Line documentation.
+Please note that only `localhost` or `127.0.0.1` are supported. +:end + +:docker_tailscale_serve_local_path_help: +When not specified, this value defaults to an empty string. It is passed to the `` portion of this command which starts serve or funnel:
+`tailscale [serve|funnel] --bg -- :`
+For more details see the Tailscale Serve Command Line documentation. +:end + +:docker_tailscale_serve_protocol_help: +When not specified, this value defaults to "https". It is passed to the `` portion of this command which starts serve or funnel:
+`tailscale [serve|funnel] --bg --= :`
+For more details see the Tailscale Serve Command Line documentation. +:end + +:docker_tailscale_serve_protocol_port_help: +When not specified, this value defaults to "=443". It is passed to the `` portion of this command which starts serve or funnel:
+`tailscale [serve|funnel] --bg -- :`
+For more details see the Tailscale Serve Command Line documentation. +:end + +:docker_tailscale_serve_path_help: +When not specified, this value defaults to an empty string. It is passed to the `` portion of this command which starts serve or funnel:
+`tailscale [serve|funnel] --bg -- :`
+For more details see the Tailscale Serve Command Line documentation. +:end + +:docker_tailscale_serve_webui_help: +If Serve is enabled this will be an https url with a proper domain name that is accessible over your Tailnet, no port needed! + +If Funnel is enabled the same url will be available on the Internet. + +If they are disabled then the url will be generated from the container's main "Web UI" field, but modified to use the Tailscale IP. If the wrong port is specified here then switch from "Basic View" to "Advanced View" and review the "Web UI" field for this container. +:end + +:docker_tailscale_advertise_routes_help: +If desired, specify any routes that should be passed to the **`--advertise-routes=`** parameter when running **`tailscale up`**. +For more details see the Subnet routers documentation. +:end + +:docker_tailscale_accept_routes_help: +When enabled, this will accept your subnet routes from other devices, adding the **`--accept-routes`** parameter when running **`tailscale up`**. +For more details see the Use your subnet routes from other devices documentation. +:end + +:docker_tailscale_daemon_extra_params_help: +Specify any extra parameters to pass when starting **`tailscaled`**. +For more details see the tailscaled documentation. +:end + +:docker_tailscale_extra_param_help: +Specify any extra parameters to pass when running **`tailscale up`**. +For more details see the Tailscale CLI documentation. +:end + +:docker_tailscale_statedir_help: +If state directory detection fails on startup, you can specify a persistent directory in the container to override automatic detection, i.e. `/container-path/.tailscale_state` +:end + +:docker_tailscale_troubleshooting_packages_help: +Enable this to install `ping`, `nslookup`, `curl`, and `speedtest-cli` into the container to help troubleshoot networking issues. Once the issues are resolved we recommend disabling this to reduce the size of the container. +:end + +:docker_privileged_help: +For containers that require the use of host-device access directly or need full exposure to host capabilities, this option will need to be selected. +For more information, see this link: https://docs.docker.com/engine/reference/run/#runtime-privilege-and-linux-capabilities +:end + +; sysdevs help - added May 18, 2020 +:sysdevs_iommu_groups_help: +This displays a list of IOMMU groups available on your system along with the output of the `lspci` command for each IOMMU group. The numeric identifiers are used to configure PCI pass-through. + +Devices you select will be bound to the vfio-pci driver at boot, which makes them available for assignment to a virtual machine, and also prevents the Linux kernel from automatically binding them to any present host driver. + +**Note that selecting a device will bind not only the specified device(s), but *all* other devices in the same IOMMU group as well.** + +  This symbol indicates the device is currently bound to the vfio-pci driver. + +    This symbol indicates the device supports FLR (Function Level Reset). + +  If a checkbox is greyed out it means the device is in use by Unraid OS and can not be passed through. +:end + +:sysdevs_thread_pairings_help: +This displays a list of CPU thread pairings. +:end + +:sysdevs_usb_devices_help: +This displays the output of the `lsusb` command. The numeric identifiers are used to configure PCI pass-through. +:end + +:sysdevs_scsi_devices_help: +This displays the output of the `lsscsi` command. The numeric identifiers are used to configure PCI pass-through. + +Note that linux groups ATA, SATA and SAS devices with true SCSI devices. +:end + +; unraid.net help - added March 11, 2021 +:unraidnet_wanpanel_help: +WAN Port is the external TCP port number setup on your router to NAT/Port Forward traffic from the internet to this +Unraid server SSL port for secure web traffic. +:end + +:unraidnet_inactivespanel_help: +Click Activate to set up a local git repo for your local USB Flash boot device and connect to a dedicated remote on unraid.net tied to this server. +:end + +:unraidnet_changespanel_help: +The Not Up-to-date status indicates there are local files which are changed vs. the remote on unraid.net. + +Click Update to push changes to the remote. + +Click Changes to see what has changed. +:end + +:unraidnet_uptodatepanel_help: +The Up-to-date status indicates your local configuration matches that stored on the unraid.net remote. +:end + +:unraidnet_activepanel_help: +Click Deactivate to pause communication with your remote on unraid.net. + +Click Reinitialize to erase all change history in both local and unraid.net remote. +:end + +:unraidnet_extraorigins_help: +Provide a comma separated list of urls that are allowed to access the unraid-api (https://abc.myreverseproxy.com,https://xyz.rvrsprx.com,…) +:end + +:myservers_remote_t2fa_help: +When Transparent 2FA for Remote Access is enabled, you will access your server by clicking the "Remote Access" link on the Go to Connect. The system will transparently request a 2FA token from your server and embed it in the server's login form. Your server will deny any Remote Access login attempt that does not include a valid token. Each token can be used only once, and is only valid for five minutes. +:end + +:myservers_local_t2fa_help: +When Transparent 2FA for Local Access is enabled, you will access your server by clicking the "Local Access" link on the Go to Connect. The system will transparently request a 2FA token from your server and embed it in the server's login form. Your server will deny any Local Access login attempt that does not include a valid token. Each token can be used only once, and is only valid for five minutes. + +This is fairly extreme for Local Access, and in most cases is not needed. It requires a solid Internet connection. If you need to access the webGUI while the Internet is down, SSH to the server and run 'use_ssl no', this will give you access via http:// +:end + +; WireGuard help text + +:wg_local_name_help: +Use this field to set a name for this connection and make it easily recognizable. The same name will appear in the configuration of any peers. +:end + +:wg_generate_keypair_help: +Use the **Generate Keypair** button to automatically create a uniqe private and public key combination.
+Or paste in an existing private key, generated by WireGuard. Do **NOT** share this private key with others! +:end + +:wg_local_tunnel_network_pool_help: +WireGuard tunnels need an internal IP address. Assign a network pool using the default IPv4 network /24 +or the default IPv6 network /64 or assign your own network pool from which automatic assignment can be done for both this server and any peers. + +The *tunnel network pool* must be a unique network not already existing on the server or any of the peers. +:end + +:wg_local_tunnel_network_pool_X_help: +WireGuard tunnels need an internal IP address. Assign a network pool using the default IPv4 network, +the default IPv6 network, or assign your own network pool from which automatic assignment can be done for both this server and any peers. + +The *tunnel network pool* must be a unique network not already existing on the server or any of the peers. +:end + +:wg_local_tunnel_address_help: +This field is auto filled-in when a local tunnel network pool is created. It is allowed to overwrite the assignment, but this is normally not necessary. Use with care when changing manually. +:end + +:wg_local_tunnel_address_help: +This field is auto filled-in when a local tunnel network pool is created. It is allowed to overwrite the assignment, but this is normally not necessary. Use with care when changing manually. +:end + +:wg_local_endpoint_help: +This field is automatically filled in with the public management domain name *<wan-ip>.<hash>.myunraid.net* or the public address of the server.
+This allows VPN tunnels to be established from external peers to the server.
+Configure the correct port forwarding on your router (default port is but this may be changed) to allow any incoming connections to reach the server. + +Users with a registered domain name can use this field to specify how their server is known on the Internet. E.g. www.myserver.mydomain.
+Again make sure your router is properly set up. + +Note to Cloudflare users: the Cloudflare proxy is designed for http traffic, it is not able to proxy VPN traffic. You must disable the Cloudflare proxy in order to use VPN with your domain. +:end + +:wg_local_endpoint_X_help: +This field is automatically filled in with the public management domain name *<wan-ip>.<hash>.myunraid.net* or the public address of the server.
+This allows VPN tunnels to be established from external peers to the server.
+Configure the correct port forwarding on your router to allow any incoming connections to reach the server. + +Users with a registered domain name can use this field to specify how their server is known on the Internet. E.g. www.myserver.mydomain.
+Again make sure your router is properly set up. + +Note to Cloudflare users: the Cloudflare proxy is designed for http traffic, it is not able to proxy VPN traffic. You must disable the Cloudflare proxy in order to use VPN with your domain. +:end + +:wg_local_server_uses_nat_help: +When NAT is enabled, the server uses its own LAN address when forwarding traffic from the tunnel to other devices in the LAN network. +Use this setting when no router modifications are desired, but this approach doesn't work with Docker containers using custom IP addresses. + +When NAT is disabled, the server uses the WireGuard tunnel address when forwarding traffic. +In this case it is required that the default gateway (router) has a static route configured to refer tunnel address back to the server. +:end + +:wg_local_gateway_uses_upnp_help: +Defaults to YES if the local gateway has UPnP enabled and is responding to requests.
+When UPnP is enabled, it is not necessary to configure port forwarding on the router to allow incoming tunnel connections. This is done automatically. +:end + +:wg_local_tunnel_firewall_help: +The firewall function controls remote access over the WireGuard tunnel to specific hosts and/or subnets.
+The default rule is "deny" and blocks addresses specified in this field, while allowing all others.
+Changing the rule to "allow" inverts the selection, meaning only the specified addresses are allowed and all others are blocked.
+Use a comma as separator when more than one IP address is entered. +:end + +:wg_mtu_size_help: +Leave this to the default automatic mode to select the MTU size. This MTU size is common for all peer connections. +:end + +:wg_peer_configuration_help: +The icon is used to view a peer's configuration. A configuration can be downloaded or read directly for instant set up of the peer.
+The icon is disabled when no peer configuration exists or the user has made changes to the existing settings which are not applied yet. + +The icon is used to show or hide the private, public and preshared keys. Note that these fields are always shown when no keys are set. +:end + +:wg_peer_name_help: +Use this field to set a name for this peer connection and make it easily recognizable. The same name will appear in the configuration of the peer at the opposite side. +:end + +:wg_peer_preshared_key_help: +For added security a preshared key can be used. Use the **Generate Key** button to automatically create a unique preshared key.
+This key is the same at both server and peer side and is added to the peer configuration as well. +:end + +:wg_peer_tunnel_address_help: +This field is auto filled-in when a local tunnel network pool is created. It is allowed to overwrite the assignment, but this is normally not necessary. Use with care when changing manually.
+Each peer must have a unique tunnel IP address. +:end + +:wg_peer_endpoint_help: +When this field is left empty, the server operates in *passive mode* to establish the tunnel. It must be the peer which starts the tunnel. + +When an IP address is entered to connect to the peer, the server operates in *active mode* and establishes the tunnel to the peer as soon as there is data to send. + +*Note: this field is mandatory for "server-to-server" and "LAN-to-LAN" connections* +:end + +:wg_peer_allowed_ips_help: +This field is automatically filled in with the tunnel address of the peer. This allows the server to reach the peer over the tunnel.
+When the peer is another server or router with additional networks, then their subnets can be added here to make these networks reachable over the tunnel. +:end + +:wg_peer_dns_server_help: +Use this entry to overwrite the current DNS server assignment of the Peer +:end + +:wg_persistent_keepalive_help: +By default a WireGuard tunnel stays silent when no traffic is present, which may cause the connection to drop. +Normally this isn't a problem since a WireGuard tunnel is automatically re-established when it is needed.
+A keepalive timer will hold the connection open, for most situations a timer value of 20 seconds is suitable. + +Note that for mobile devices this will use more data and drain your battery faster. +:end + +:sysdrivers_intro_help: +This page displays all the drivers available on this system. Filter the list according to whether the driver is Inuse or not, or search for a specific driver if desired. + +Any 3rd party driver installed by a plugin will have a support symbol next to it, click this to get support for the plugin. + +Click the edit button to add/modify/delete any modprobe.d config file in the config/modprobe.d directory on the flash drive. +:end + +:console_keyboard_help: +Select your default keymap for the local console (not the web Terminal). +:end + +:console_screen_help: +**Default:** 'Default' will set the blank timeout to 15 minutes and powersave to 60 minutes. + +**Disabled:** 'Disabled' will disable the blank and powersave timeout. + +**All other values:** Will set the blank timout to the selected value and disable the powersave timeout. +:end + +:console_bash_help: +If set to 'Yes' the bash history will persist reboots, set to 'No' to disable. + +**ATTENTION:** The bash history will be written to the USB Boot device so this will introduce higher wear and tear! + +**Note:** Disabling and Enabling will remove the entire bash history. +:end + +:WOL_intro_help: +This page allows the setup to start/resume and stop VMs, Containters(Docker and LXC) using WOL magic packets + +It does not setup wake up of the Unraid server. + +The process will look for the defined mac address defined within the service(VM or container) with the exception of dockers. Dockers do not have a mac address until they are running so you will need to define a user mac address to allow you to start them + +If the service is paused a WOL Packet will resume. + +For each service you can set: enable, disable or enable and shutdown + +When the enable and shutdown is set if the service is running when the next WOL packet is recieved a shutdown will be performed. +:end + +:WOL_enable_help: +If set to yes Unraidwold daemon is set to run. +:end + +:WOL_run_docker_help: +If set to yes when wake on lan packets are received checks are carrried out for dockers otherwise dockers will be ignored. +:end + +:WOL_run_VM_help: +If set to yes when wake on lan packets are received checks are carrried out for virtual machines otherwise virtual machines will be ignored. +:end + +:WOL_run_LXC_help: +If set to yes when wake on lan packets are received checks are carrried out for LXC otherwise LXC will be ignored. The LXC plugin needs to be installed for LXC to be processed. +:end + +:WOL_run_shutdown_help: +If set to yes when wake on lan packets are received checks are carrried out and if enabled for service and that service is running the entity will be shutdown. System has to be set to Enabled and Shutdwon for it to be action. +:end + +:WOL_interface_help: +Specify the interface to the daemon to bind to. Some interfaces may not be able to recieve etherframe packets. Recommend to bind to a physical interface. +:end + +:WOL_promiscuous_mode_help: +Enable to set the NIC not to filer packets. +:end + +:WOL_log_file_help: +Default is to log to syslog but if you want a different log location set the file name. +:end diff --git a/api/src/unraid-api/unraid-file-modifier/modifications/__test__/__fixtures__/downloaded/notify b/api/src/unraid-api/unraid-file-modifier/modifications/__test__/__fixtures__/downloaded/notify new file mode 100644 index 0000000000..8a5adc246e --- /dev/null +++ b/api/src/unraid-api/unraid-file-modifier/modifications/__test__/__fixtures__/downloaded/notify @@ -0,0 +1,261 @@ +#!/usr/bin/php -q + +", ":", ";", ",", "'", "\"", "&", "$", "#", "*", "(", ")", "|", "~", "`", "!", "{", "}"]; + $string = trim(str_replace($special_chars, "", $string)); + $string = preg_replace('~[^0-9a-z -_]~i', '', $string); + $string = preg_replace('~[- ]~i', '_', $string); + return trim($string); +} + +/* + Call this when using the subject field in email or agents. Do not use when showing the subject in a browser. + Removes all HTML entities from subject line, is specifically targetting the my_temp() function, which adds ' °' +*/ +function clean_subject($subject) { + $subject = preg_replace("/&#?[a-z0-9]{2,8};/i"," ",$subject); + return $subject; +} + +// start +if ($argc == 1) exit(usage()); + +extract(parse_plugin_cfg("dynamix",true)); + +$path = _var($notify,'path','/tmp/notifications'); +$unread = "$path/unread"; +$archive = "$path/archive"; +$agents_dir = "/boot/config/plugins/dynamix/notifications/agents"; +if (is_dir($agents_dir)) { + $agents = []; + foreach (array_diff(scandir($agents_dir), ['.','..']) as $p) { + if (file_exists("{$agents_dir}/{$p}")) $agents[] = "{$agents_dir}/{$p}"; + } +} else { + $agents = NULL; +} + +switch ($argv[1][0]=='-' ? 'add' : $argv[1]) { +case 'init': + $files = glob("$unread/*.notify", GLOB_NOSORT); + foreach ($files as $file) if (!is_readable($file)) chmod($file,0666); + break; + +case 'smtp-init': + @mkdir($unread,0755,true); + @mkdir($archive,0755,true); + $conf = []; + $conf[] = "# Generated settings:"; + $conf[] = "Root={$ssmtp['root']}"; + $domain = strtok($ssmtp['root'],'@'); + $domain = strtok('@'); + $conf[] = "rewriteDomain=$domain"; + $conf[] = "FromLineOverride=YES"; + $conf[] = "Mailhub={$ssmtp['server']}:{$ssmtp['port']}"; + $conf[] = "UseTLS={$ssmtp['UseTLS']}"; + $conf[] = "UseSTARTTLS={$ssmtp['UseSTARTTLS']}"; + if ($ssmtp['AuthMethod'] != "none") { + $conf[] = "AuthMethod={$ssmtp['AuthMethod']}"; + $conf[] = "AuthUser={$ssmtp['AuthUser']}"; + $conf[] = "AuthPass=".base64_decrypt($ssmtp['AuthPass']); + } + $conf[] = ""; + file_put_contents("/etc/ssmtp/ssmtp.conf", implode("\n", $conf)); + break; + +case 'cron-init': + @mkdir($unread,0755,true); + @mkdir($archive,0755,true); + $text = empty($notify['status']) ? "" : "# Generated array status check schedule:\n{$notify['status']} $docroot/plugins/dynamix/scripts/statuscheck &> /dev/null\n\n"; + parse_cron_cfg("dynamix", "status-check", $text); + $text = empty($notify['unraidos']) ? "" : "# Generated Unraid OS update check schedule:\n{$notify['unraidos']} $docroot/plugins/dynamix.plugin.manager/scripts/unraidcheck &> /dev/null\n\n"; + parse_cron_cfg("dynamix", "unraid-check", $text); + $text = empty($notify['version']) ? "" : "# Generated plugins version check schedule:\n{$notify['version']} $docroot/plugins/dynamix.plugin.manager/scripts/plugincheck &> /dev/null\n\n"; + parse_cron_cfg("dynamix", "plugin-check", $text); + $text = empty($notify['system']) ? "" : "# Generated system monitoring schedule:\n{$notify['system']} $docroot/plugins/dynamix/scripts/monitor &> /dev/null\n\n"; + parse_cron_cfg("dynamix", "monitor", $text); + $text = empty($notify['docker_update']) ? "" : "# Generated docker monitoring schedule:\n{$notify['docker_update']} $docroot/plugins/dynamix.docker.manager/scripts/dockerupdate check &> /dev/null\n\n"; + parse_cron_cfg("dynamix", "docker-update", $text); + $text = empty($notify['language_update']) ? "" : "# Generated languages version check schedule:\n{$notify['language_update']} $docroot/plugins/dynamix.plugin.manager/scripts/languagecheck &> /dev/null\n\n"; + parse_cron_cfg("dynamix", "language-check", $text); + break; + +case 'add': + $event = 'Unraid Status'; + $subject = 'Notification'; + $description = 'No description'; + $importance = 'normal'; + $message = $recipients = $link = $fqdnlink = ''; + $timestamp = time(); + $ticket = $timestamp; + $mailtest = false; + $overrule = false; + $noBrowser = false; + + $options = getopt("l:e:s:d:i:m:r:xtb"); + foreach ($options as $option => $value) { + switch ($option) { + case 'e': + $event = $value; + break; + case 's': + $subject = $value; + break; + case 'd': + $description = $value; + break; + case 'i': + $importance = strtok($value,' '); + $overrule = strtok(' '); + break; + case 'm': + $message = $value; + break; + case 'r': + $recipients = $value; + break; + case 'x': + $ticket = 'ticket'; + break; + case 't': + $mailtest = true; + break; + case 'b': + $noBrowser = true; + break; + case 'l': + $nginx = (array)@parse_ini_file('/var/local/emhttp/nginx.ini'); + $link = $value; + $fqdnlink = (strpos($link,"http") === 0) ? $link : ($nginx['NGINX_DEFAULTURL']??'').$link; + break; + } + } + + $unread = "{$unread}/".safe_filename("{$event}-{$ticket}.notify"); + $archive = "{$archive}/".safe_filename("{$event}-{$ticket}.notify"); + if (file_exists($archive)) break; + $entity = $overrule===false ? $notify[$importance] : $overrule; + if (!$mailtest) file_put_contents($archive,"timestamp=$timestamp\nevent=$event\nsubject=$subject\ndescription=$description\nimportance=$importance\n".($message ? "message=".str_replace('\n','
',$message)."\n" : "")); + if (($entity & 1)==1 && !$mailtest && !$noBrowser) file_put_contents($unread,"timestamp=$timestamp\nevent=$event\nsubject=$subject\ndescription=$description\nimportance=$importance\nlink=$link\n"); + if (($entity & 2)==2 || $mailtest) generate_email($event, clean_subject($subject), str_replace('
','. ',$description), $importance, $message, $recipients, $fqdnlink); + if (($entity & 4)==4 && !$mailtest) { if (is_array($agents)) {foreach ($agents as $agent) {exec("TIMESTAMP='$timestamp' EVENT=".escapeshellarg($event)." SUBJECT=".escapeshellarg(clean_subject($subject))." DESCRIPTION=".escapeshellarg($description)." IMPORTANCE=".escapeshellarg($importance)." CONTENT=".escapeshellarg($message)." LINK=".escapeshellarg($fqdnlink)." bash ".$agent);};}}; + break; + +case 'get': + $output = []; + $json = []; + $files = glob("$unread/*.notify", GLOB_NOSORT); + usort($files, function($a,$b){return filemtime($a)-filemtime($b);}); + $i = 0; + foreach ($files as $file) { + $fields = file($file,FILE_IGNORE_NEW_LINES|FILE_SKIP_EMPTY_LINES); + $time = true; + $output[$i]['file'] = basename($file); + $output[$i]['show'] = (fileperms($file) & 0x0FFF)==0400 ? 0 : 1; + foreach ($fields as $field) { + if (!$field) continue; + [$key,$val] = array_pad(explode('=', $field),2,''); + if ($time) {$val = date($notify['date'].' '.$notify['time'], $val); $time = false;} + $output[$i][trim($key)] = trim($val); + } + $i++; + } + echo json_encode($output, JSON_UNESCAPED_SLASHES|JSON_UNESCAPED_UNICODE); + break; + +case 'archive': + if ($argc != 3) exit(usage()); + $file = $argv[2]; + if (strpos(realpath("$unread/$file"),$unread.'/')===0) @unlink("$unread/$file"); + break; +} + +exit(0); +?> diff --git a/api/src/unraid-api/unraid-file-modifier/modifications/__test__/generic-modification.spec.ts b/api/src/unraid-api/unraid-file-modifier/modifications/__test__/generic-modification.spec.ts index 3ec9404800..7e308546b0 100644 --- a/api/src/unraid-api/unraid-file-modifier/modifications/__test__/generic-modification.spec.ts +++ b/api/src/unraid-api/unraid-file-modifier/modifications/__test__/generic-modification.spec.ts @@ -1,17 +1,32 @@ import { Logger } from '@nestjs/common'; -import { readFile, writeFile } from 'fs/promises'; +import { constants } from 'fs'; +import { access, mkdir, readFile, writeFile } from 'fs/promises'; import { basename, dirname, resolve } from 'path'; import { fileURLToPath } from 'url'; -import { describe, expect, test, vi } from 'vitest'; +import { coerce, gte } from 'semver'; +import { beforeAll, describe, expect, test, vi } from 'vitest'; import { FileModification } from '@app/unraid-api/unraid-file-modifier/file-modification.js'; import AuthRequestModification from '@app/unraid-api/unraid-file-modifier/modifications/auth-request.modification.js'; +import DefaultAzureCssModification from '@app/unraid-api/unraid-file-modifier/modifications/default-azure-css.modification.js'; +import DefaultBaseCssModification from '@app/unraid-api/unraid-file-modifier/modifications/default-base-css.modification.js'; +import DefaultBlackCssModification from '@app/unraid-api/unraid-file-modifier/modifications/default-black-css.modification.js'; +import DefaultCfgModification from '@app/unraid-api/unraid-file-modifier/modifications/default-cfg.modification.js'; +import DefaultGrayCssModification from '@app/unraid-api/unraid-file-modifier/modifications/default-gray-css.modification.js'; import DefaultPageLayoutModification from '@app/unraid-api/unraid-file-modifier/modifications/default-page-layout.modification.js'; +import DefaultWhiteCssModification from '@app/unraid-api/unraid-file-modifier/modifications/default-white-css.modification.js'; import DisplaySettingsModification from '@app/unraid-api/unraid-file-modifier/modifications/display-settings.modification.js'; +import DockerContainersPageModification from '@app/unraid-api/unraid-file-modifier/modifications/docker-containers-page.modification.js'; +import FontAwesomeCssModification from '@app/unraid-api/unraid-file-modifier/modifications/font-awesome-css.modification.js'; +import HelptextModification from '@app/unraid-api/unraid-file-modifier/modifications/helptext.modification.js'; import NotificationsPageModification from '@app/unraid-api/unraid-file-modifier/modifications/notifications-page.modification.js'; +import NotifyPhpModification from '@app/unraid-api/unraid-file-modifier/modifications/notify-php.modification.js'; +import NotifyScriptModification from '@app/unraid-api/unraid-file-modifier/modifications/notify-script.modification.js'; import RcNginxModification from '@app/unraid-api/unraid-file-modifier/modifications/rc-nginx.modification.js'; +import SetPasswordModalModification from '@app/unraid-api/unraid-file-modifier/modifications/set-password-modal.modification.js'; import SSOFileModification from '@app/unraid-api/unraid-file-modifier/modifications/sso.modification.js'; +import TranslationsPhpModification from '@app/unraid-api/unraid-file-modifier/modifications/translations-php.modification.js'; interface ModificationTestCase { ModificationClass: new (...args: ConstructorParameters) => FileModification; @@ -30,12 +45,30 @@ const patchTestCases: ModificationTestCase[] = [ 'https://raw.githubusercontent.com/unraid/webgui/refs/heads/7.1/emhttp/plugins/dynamix/include/DefaultPageLayout.php', fileName: 'DefaultPageLayout.php', }, + { + ModificationClass: DefaultBaseCssModification, + fileUrl: + 'https://raw.githubusercontent.com/unraid/webgui/7.1.2/emhttp/plugins/dynamix/styles/default-base.css', + fileName: 'default-base.css', + }, { ModificationClass: NotificationsPageModification, fileUrl: - 'https://raw.githubusercontent.com/unraid/webgui/refs/heads/7.1/emhttp/plugins/dynamix/Notifications.page', + 'https://raw.githubusercontent.com/unraid/webgui/refs/heads/7.0/emhttp/plugins/dynamix/Notifications.page', fileName: 'Notifications.page', }, + { + ModificationClass: DefaultCfgModification, + fileUrl: + 'https://raw.githubusercontent.com/unraid/webgui/refs/heads/7.1/emhttp/plugins/dynamix/default.cfg', + fileName: 'default.cfg', + }, + { + ModificationClass: NotifyPhpModification, + fileUrl: + 'https://raw.githubusercontent.com/unraid/webgui/refs/heads/7.1/emhttp/plugins/dynamix/include/Notify.php', + fileName: 'Notify.php', + }, { ModificationClass: DisplaySettingsModification, fileUrl: @@ -59,17 +92,102 @@ const patchTestCases: ModificationTestCase[] = [ fileUrl: 'https://raw.githubusercontent.com/unraid/webgui/refs/heads/7.1/etc/rc.d/rc.nginx', fileName: 'rc.nginx', }, + { + ModificationClass: NotifyScriptModification, + fileUrl: + 'https://raw.githubusercontent.com/unraid/webgui/refs/heads/7.1/emhttp/plugins/dynamix/scripts/notify', + fileName: 'notify', + }, + { + ModificationClass: DefaultWhiteCssModification, + fileUrl: + 'https://raw.githubusercontent.com/unraid/webgui/refs/heads/7.0/emhttp/plugins/dynamix/styles/default-white.css', + fileName: 'default-white.css', + }, + { + ModificationClass: DefaultBlackCssModification, + fileUrl: + 'https://raw.githubusercontent.com/unraid/webgui/refs/heads/7.0/emhttp/plugins/dynamix/styles/default-black.css', + fileName: 'default-black.css', + }, + { + ModificationClass: DefaultGrayCssModification, + fileUrl: + 'https://raw.githubusercontent.com/unraid/webgui/refs/heads/7.0/emhttp/plugins/dynamix/styles/default-gray.css', + fileName: 'default-gray.css', + }, + { + ModificationClass: DefaultAzureCssModification, + fileUrl: + 'https://raw.githubusercontent.com/unraid/webgui/refs/heads/7.0/emhttp/plugins/dynamix/styles/default-azure.css', + fileName: 'default-azure.css', + }, + { + ModificationClass: DockerContainersPageModification, + fileUrl: + 'https://raw.githubusercontent.com/unraid/webgui/refs/heads/7.1/emhttp/plugins/dynamix.docker.manager/DockerContainers.page', + fileName: 'DockerContainers.page', + }, + { + ModificationClass: SetPasswordModalModification, + fileUrl: + 'https://raw.githubusercontent.com/unraid/webgui/refs/heads/7.1/emhttp/plugins/dynamix/include/.set-password.php', + fileName: '.set-password.php', + }, + { + ModificationClass: FontAwesomeCssModification, + fileUrl: + 'https://raw.githubusercontent.com/unraid/webgui/refs/heads/7.1/emhttp/plugins/dynamix/styles/font-awesome.css', + fileName: 'font-awesome.css', + }, + { + ModificationClass: HelptextModification, + fileUrl: + 'https://raw.githubusercontent.com/unraid/webgui/refs/heads/7.1/emhttp/languages/en_US/helptext.txt', + fileName: 'helptext.txt', + }, + { + ModificationClass: TranslationsPhpModification, + fileUrl: + 'https://raw.githubusercontent.com/unraid/webgui/refs/heads/7.1/emhttp/plugins/dynamix/include/Translations.php', + fileName: 'Translations.php', + }, ]; /** Modifications that simply add a new file & remove it on rollback. */ const simpleTestCases: ModificationTestCase[] = []; +// ... (existing imports) + +// ... + async function testModification(testCase: ModificationTestCase) { const fileName = basename(testCase.fileUrl); const filePath = getPathToFixture(fileName); const originalContent = await readFile(filePath, 'utf-8').catch(() => ''); const logger = new Logger(); const patcher = await new testCase.ModificationClass(logger); + + // Mock isUnraidVersionGreaterThanOrEqualTo to derive version from test case URL + // @ts-expect-error - Accessing protected method + patcher.isUnraidVersionGreaterThanOrEqualTo = vi.fn().mockImplementation(async (version) => { + // Extract version from URL, simple heuristic looking for /7.x/ or /6.x/ + // URLs look like: .../heads/7.1/... or .../heads/7.0/... + const match = + testCase.fileUrl.match(/\/heads\/(\d+\.\d+)/) || testCase.fileUrl.match(/\/(\d+\.\d+\.\d+)/); + const urlVersion = match ? match[1] : '7.0.0'; // Default to 7.0.0 if not found + + const v1 = coerce(urlVersion); + const v2 = coerce(version); + if (!v1 || !v2) return false; + + return gte(v1, v2); + }); + + // Mock getPregeneratedPatch to return null, forcing use of dynamic generation for tests + // @ts-expect-error - Accessing protected method + patcher.getPregeneratedPatch = vi.fn().mockResolvedValue(null); + const originalPath = patcher.filePath; // @ts-expect-error - Ignore for testing purposes patcher.filePath = filePath; @@ -105,6 +223,22 @@ async function testInvalidModification(testCase: ModificationTestCase) { const patcher = new testCase.ModificationClass(mockLogger as unknown as Logger); + // Mock isUnraidVersionGreaterThanOrEqualTo to derive version from test case URL + // @ts-expect-error - Accessing protected method + patcher.isUnraidVersionGreaterThanOrEqualTo = vi.fn().mockImplementation(async (version) => { + // Extract version from URL, simple heuristic looking for /7.x/ or /6.x/ + // URLs look like: .../heads/7.1/... or .../heads/7.0/... + const match = + testCase.fileUrl.match(/\/heads\/(\d+\.\d+)/) || testCase.fileUrl.match(/\/(\d+\.\d+\.\d+)/); + const urlVersion = match ? match[1] : '7.0.0'; // Default to 7.0.0 if not found + + const v1 = coerce(urlVersion); + const v2 = coerce(version); + if (!v1 || !v2) return false; + + return gte(v1, v2); + }); + // @ts-expect-error - Testing invalid pregenerated patches patcher.getPregeneratedPatch = vi.fn().mockResolvedValue('I AM NOT A VALID PATCH'); @@ -122,7 +256,28 @@ async function testInvalidModification(testCase: ModificationTestCase) { const allTestCases = [...patchTestCases, ...simpleTestCases]; +async function ensureFixtureExists(testCase: ModificationTestCase) { + const fileName = basename(testCase.fileUrl); + const filePath = getPathToFixture(fileName); + try { + await access(filePath, constants.R_OK); + } catch { + console.log(`Downloading fixture: ${fileName} from ${testCase.fileUrl}`); + const response = await fetch(testCase.fileUrl); + if (!response.ok) { + throw new Error(`Failed to download fixture ${fileName}: ${response.statusText}`); + } + const text = await response.text(); + await mkdir(dirname(filePath), { recursive: true }); + await writeFile(filePath, text); + } +} + describe('File modifications', () => { + beforeAll(async () => { + await Promise.all(allTestCases.map(ensureFixtureExists)); + }); + test.each(allTestCases)( `$fileName modifier correctly applies to fresh install`, async (testCase) => { diff --git a/api/src/unraid-api/unraid-file-modifier/modifications/__test__/snapshots/.set-password.php.modified.snapshot.php b/api/src/unraid-api/unraid-file-modifier/modifications/__test__/snapshots/.set-password.php.modified.snapshot.php new file mode 100644 index 0000000000..fb7efd1fa9 --- /dev/null +++ b/api/src/unraid-api/unraid-file-modifier/modifications/__test__/snapshots/.set-password.php.modified.snapshot.php @@ -0,0 +1,417 @@ + _('root requires a password'), + 'mismatch' => _('Password confirmation does not match'), + 'maxLength' => _('Max password length is 128 characters'), + 'saveError' => _('Unable to set password'), +]; +$POST_ERROR = ''; + +/** + * POST handler + */ +if (!empty($_POST['password']) && !empty($_POST['confirmPassword'])) { + if ($_POST['password'] !== $_POST['confirmPassword']) return $POST_ERROR = $VALIDATION_MESSAGES['mismatch']; + if (strlen($_POST['password']) > $MAX_PASS_LENGTH) return $POST_ERROR = $VALIDATION_MESSAGES['maxLength']; + + $userName = 'root'; + $userPassword = base64_encode($_POST['password']); + + exec("/usr/local/sbin/emcmd 'cmdUserEdit=Change&userName=$userName&userPassword=$userPassword'", $output, $result); + if ($result == 0) { + // PAM service will log to syslog: "password changed for root" + if (session_status()==PHP_SESSION_NONE) session_start(); + $_SESSION['unraid_login'] = time(); + $_SESSION['unraid_user'] = 'root'; + session_regenerate_id(true); + session_write_close(); + + // Redirect the user to the start page + header("Location: /".$start_page); + exit; + } + + // Error when attempting to set password + my_logger("{$VALIDATION_MESSAGES['saveError']} [REMOTE_ADDR]: {$REMOTE_ADDR}"); + return $POST_ERROR = $VALIDATION_MESSAGES['saveError']; +} + +$THEME_DARK = in_array($display['theme'],['black','gray']); +?> + + + + + + + + + + + + + + <?=$var['NAME']?>/SetPassword + + + + + + +
+
+ +
+
+
+

+

+

.

+

.

+
+ +
+ + + +
+ + +
+ + + + +

+
+ +
+
+
+
+ + + + diff --git a/api/src/unraid-api/unraid-file-modifier/modifications/__test__/snapshots/DefaultPageLayout.php.modified.snapshot.php b/api/src/unraid-api/unraid-file-modifier/modifications/__test__/snapshots/DefaultPageLayout.php.modified.snapshot.php index c6ba6a6425..dd5cba2223 100644 --- a/api/src/unraid-api/unraid-file-modifier/modifications/__test__/snapshots/DefaultPageLayout.php.modified.snapshot.php +++ b/api/src/unraid-api/unraid-file-modifier/modifications/__test__/snapshots/DefaultPageLayout.php.modified.snapshot.php @@ -1367,6 +1367,5 @@ function nchanFocusStop(banner=true) { } - diff --git a/api/src/unraid-api/unraid-file-modifier/modifications/__test__/snapshots/DockerContainers.page.modified.snapshot.php b/api/src/unraid-api/unraid-file-modifier/modifications/__test__/snapshots/DockerContainers.page.modified.snapshot.php new file mode 100644 index 0000000000..041091fe19 --- /dev/null +++ b/api/src/unraid-api/unraid-file-modifier/modifications/__test__/snapshots/DockerContainers.page.modified.snapshot.php @@ -0,0 +1,11 @@ +Menu="Docker:1" +Title="Docker Containers" +Tag="cubes" +Cond="is_file('/var/run/dockerd.pid')" +Markdown="false" +Nchan="docker_load" +Tabs="false" +--- +
+ +
diff --git a/api/src/unraid-api/unraid-file-modifier/modifications/__test__/snapshots/Notifications.page.modified.snapshot.php b/api/src/unraid-api/unraid-file-modifier/modifications/__test__/snapshots/Notifications.page.modified.snapshot.php index 62de8c2992..f122dc71e2 100644 --- a/api/src/unraid-api/unraid-file-modifier/modifications/__test__/snapshots/Notifications.page.modified.snapshot.php +++ b/api/src/unraid-api/unraid-file-modifier/modifications/__test__/snapshots/Notifications.page.modified.snapshot.php @@ -111,13 +111,15 @@ function prepareReport(value) { -_(Notifications display)_: -: + + -:notifications_display_help: +:notifications_store_flash_help: _(Display position)_: : :notifications_display_position_help: + +_(Stack notification popups)_: +: -_(Auto-close)_ (_(seconds)_): -: _(a value of zero means no automatic closure)_ +:notifications_stack_help: -:notifications_auto_close_help: +_(Notification popup duration)_: +: +:notifications_duration_help: -_(Store notifications to flash)_: -: + +:notifications_max_help: + +_(Date format)_: +: -:notifications_store_flash_help: +:notifications_date_format_help: + +_(Time format)_: +: + +:notifications_time_format_help: + + _(System notifications)_: :   If a checkbox is greyed out it means the device is in use by Unraid OS and can not be passed through. +:end + +:sysdevs_thread_pairings_help: +This displays a list of CPU thread pairings. +:end + +:sysdevs_usb_devices_help: +This displays the output of the `lsusb` command. The numeric identifiers are used to configure PCI pass-through. +:end + +:sysdevs_scsi_devices_help: +This displays the output of the `lsscsi` command. The numeric identifiers are used to configure PCI pass-through. + +Note that linux groups ATA, SATA and SAS devices with true SCSI devices. +:end + +; unraid.net help - added March 11, 2021 +:unraidnet_wanpanel_help: +WAN Port is the external TCP port number setup on your router to NAT/Port Forward traffic from the internet to this +Unraid server SSL port for secure web traffic. +:end + +:unraidnet_inactivespanel_help: +Click Activate to set up a local git repo for your local USB Flash boot device and connect to a dedicated remote on unraid.net tied to this server. +:end + +:unraidnet_changespanel_help: +The Not Up-to-date status indicates there are local files which are changed vs. the remote on unraid.net. + +Click Update to push changes to the remote. + +Click Changes to see what has changed. +:end + +:unraidnet_uptodatepanel_help: +The Up-to-date status indicates your local configuration matches that stored on the unraid.net remote. +:end + +:unraidnet_activepanel_help: +Click Deactivate to pause communication with your remote on unraid.net. + +Click Reinitialize to erase all change history in both local and unraid.net remote. +:end + +:unraidnet_extraorigins_help: +Provide a comma separated list of urls that are allowed to access the unraid-api (https://abc.myreverseproxy.com,https://xyz.rvrsprx.com,…) +:end + +:myservers_remote_t2fa_help: +When Transparent 2FA for Remote Access is enabled, you will access your server by clicking the "Remote Access" link on the Go to Connect. The system will transparently request a 2FA token from your server and embed it in the server's login form. Your server will deny any Remote Access login attempt that does not include a valid token. Each token can be used only once, and is only valid for five minutes. +:end + +:myservers_local_t2fa_help: +When Transparent 2FA for Local Access is enabled, you will access your server by clicking the "Local Access" link on the Go to Connect. The system will transparently request a 2FA token from your server and embed it in the server's login form. Your server will deny any Local Access login attempt that does not include a valid token. Each token can be used only once, and is only valid for five minutes. + +This is fairly extreme for Local Access, and in most cases is not needed. It requires a solid Internet connection. If you need to access the webGUI while the Internet is down, SSH to the server and run 'use_ssl no', this will give you access via http:// +:end + +; WireGuard help text + +:wg_local_name_help: +Use this field to set a name for this connection and make it easily recognizable. The same name will appear in the configuration of any peers. +:end + +:wg_generate_keypair_help: +Use the **Generate Keypair** button to automatically create a uniqe private and public key combination.
+Or paste in an existing private key, generated by WireGuard. Do **NOT** share this private key with others! +:end + +:wg_local_tunnel_network_pool_help: +WireGuard tunnels need an internal IP address. Assign a network pool using the default IPv4 network /24 +or the default IPv6 network /64 or assign your own network pool from which automatic assignment can be done for both this server and any peers. + +The *tunnel network pool* must be a unique network not already existing on the server or any of the peers. +:end + +:wg_local_tunnel_network_pool_X_help: +WireGuard tunnels need an internal IP address. Assign a network pool using the default IPv4 network, +the default IPv6 network, or assign your own network pool from which automatic assignment can be done for both this server and any peers. + +The *tunnel network pool* must be a unique network not already existing on the server or any of the peers. +:end + +:wg_local_tunnel_address_help: +This field is auto filled-in when a local tunnel network pool is created. It is allowed to overwrite the assignment, but this is normally not necessary. Use with care when changing manually. +:end + +:wg_local_tunnel_address_help: +This field is auto filled-in when a local tunnel network pool is created. It is allowed to overwrite the assignment, but this is normally not necessary. Use with care when changing manually. +:end + +:wg_local_endpoint_help: +This field is automatically filled in with the public management domain name *<wan-ip>.<hash>.myunraid.net* or the public address of the server.
+This allows VPN tunnels to be established from external peers to the server.
+Configure the correct port forwarding on your router (default port is but this may be changed) to allow any incoming connections to reach the server. + +Users with a registered domain name can use this field to specify how their server is known on the Internet. E.g. www.myserver.mydomain.
+Again make sure your router is properly set up. + +Note to Cloudflare users: the Cloudflare proxy is designed for http traffic, it is not able to proxy VPN traffic. You must disable the Cloudflare proxy in order to use VPN with your domain. +:end + +:wg_local_endpoint_X_help: +This field is automatically filled in with the public management domain name *<wan-ip>.<hash>.myunraid.net* or the public address of the server.
+This allows VPN tunnels to be established from external peers to the server.
+Configure the correct port forwarding on your router to allow any incoming connections to reach the server. + +Users with a registered domain name can use this field to specify how their server is known on the Internet. E.g. www.myserver.mydomain.
+Again make sure your router is properly set up. + +Note to Cloudflare users: the Cloudflare proxy is designed for http traffic, it is not able to proxy VPN traffic. You must disable the Cloudflare proxy in order to use VPN with your domain. +:end + +:wg_local_server_uses_nat_help: +When NAT is enabled, the server uses its own LAN address when forwarding traffic from the tunnel to other devices in the LAN network. +Use this setting when no router modifications are desired, but this approach doesn't work with Docker containers using custom IP addresses. + +When NAT is disabled, the server uses the WireGuard tunnel address when forwarding traffic. +In this case it is required that the default gateway (router) has a static route configured to refer tunnel address back to the server. +:end + +:wg_local_gateway_uses_upnp_help: +Defaults to YES if the local gateway has UPnP enabled and is responding to requests.
+When UPnP is enabled, it is not necessary to configure port forwarding on the router to allow incoming tunnel connections. This is done automatically. +:end + +:wg_local_tunnel_firewall_help: +The firewall function controls remote access over the WireGuard tunnel to specific hosts and/or subnets.
+The default rule is "deny" and blocks addresses specified in this field, while allowing all others.
+Changing the rule to "allow" inverts the selection, meaning only the specified addresses are allowed and all others are blocked.
+Use a comma as separator when more than one IP address is entered. +:end + +:wg_mtu_size_help: +Leave this to the default automatic mode to select the MTU size. This MTU size is common for all peer connections. +:end + +:wg_peer_configuration_help: +The icon is used to view a peer's configuration. A configuration can be downloaded or read directly for instant set up of the peer.
+The icon is disabled when no peer configuration exists or the user has made changes to the existing settings which are not applied yet. + +The icon is used to show or hide the private, public and preshared keys. Note that these fields are always shown when no keys are set. +:end + +:wg_peer_name_help: +Use this field to set a name for this peer connection and make it easily recognizable. The same name will appear in the configuration of the peer at the opposite side. +:end + +:wg_peer_preshared_key_help: +For added security a preshared key can be used. Use the **Generate Key** button to automatically create a unique preshared key.
+This key is the same at both server and peer side and is added to the peer configuration as well. +:end + +:wg_peer_tunnel_address_help: +This field is auto filled-in when a local tunnel network pool is created. It is allowed to overwrite the assignment, but this is normally not necessary. Use with care when changing manually.
+Each peer must have a unique tunnel IP address. +:end + +:wg_peer_endpoint_help: +When this field is left empty, the server operates in *passive mode* to establish the tunnel. It must be the peer which starts the tunnel. + +When an IP address is entered to connect to the peer, the server operates in *active mode* and establishes the tunnel to the peer as soon as there is data to send. + +*Note: this field is mandatory for "server-to-server" and "LAN-to-LAN" connections* +:end + +:wg_peer_allowed_ips_help: +This field is automatically filled in with the tunnel address of the peer. This allows the server to reach the peer over the tunnel.
+When the peer is another server or router with additional networks, then their subnets can be added here to make these networks reachable over the tunnel. +:end + +:wg_peer_dns_server_help: +Use this entry to overwrite the current DNS server assignment of the Peer +:end + +:wg_persistent_keepalive_help: +By default a WireGuard tunnel stays silent when no traffic is present, which may cause the connection to drop. +Normally this isn't a problem since a WireGuard tunnel is automatically re-established when it is needed.
+A keepalive timer will hold the connection open, for most situations a timer value of 20 seconds is suitable. + +Note that for mobile devices this will use more data and drain your battery faster. +:end + +:sysdrivers_intro_help: +This page displays all the drivers available on this system. Filter the list according to whether the driver is Inuse or not, or search for a specific driver if desired. + +Any 3rd party driver installed by a plugin will have a support symbol next to it, click this to get support for the plugin. + +Click the edit button to add/modify/delete any modprobe.d config file in the config/modprobe.d directory on the flash drive. +:end + +:console_keyboard_help: +Select your default keymap for the local console (not the web Terminal). +:end + +:console_screen_help: +**Default:** 'Default' will set the blank timeout to 15 minutes and powersave to 60 minutes. + +**Disabled:** 'Disabled' will disable the blank and powersave timeout. + +**All other values:** Will set the blank timout to the selected value and disable the powersave timeout. +:end + +:console_bash_help: +If set to 'Yes' the bash history will persist reboots, set to 'No' to disable. + +**ATTENTION:** The bash history will be written to the USB Boot device so this will introduce higher wear and tear! + +**Note:** Disabling and Enabling will remove the entire bash history. +:end + +:WOL_intro_help: +This page allows the setup to start/resume and stop VMs, Containters(Docker and LXC) using WOL magic packets + +It does not setup wake up of the Unraid server. + +The process will look for the defined mac address defined within the service(VM or container) with the exception of dockers. Dockers do not have a mac address until they are running so you will need to define a user mac address to allow you to start them + +If the service is paused a WOL Packet will resume. + +For each service you can set: enable, disable or enable and shutdown + +When the enable and shutdown is set if the service is running when the next WOL packet is recieved a shutdown will be performed. +:end + +:WOL_enable_help: +If set to yes Unraidwold daemon is set to run. +:end + +:WOL_run_docker_help: +If set to yes when wake on lan packets are received checks are carrried out for dockers otherwise dockers will be ignored. +:end + +:WOL_run_VM_help: +If set to yes when wake on lan packets are received checks are carrried out for virtual machines otherwise virtual machines will be ignored. +:end + +:WOL_run_LXC_help: +If set to yes when wake on lan packets are received checks are carrried out for LXC otherwise LXC will be ignored. The LXC plugin needs to be installed for LXC to be processed. +:end + +:WOL_run_shutdown_help: +If set to yes when wake on lan packets are received checks are carrried out and if enabled for service and that service is running the entity will be shutdown. System has to be set to Enabled and Shutdwon for it to be action. +:end + +:WOL_interface_help: +Specify the interface to the daemon to bind to. Some interfaces may not be able to recieve etherframe packets. Recommend to bind to a physical interface. +:end + +:WOL_promiscuous_mode_help: +Enable to set the NIC not to filer packets. +:end + +:WOL_log_file_help: +Default is to log to syslog but if you want a different log location set the file name. +:end diff --git a/api/src/unraid-api/unraid-file-modifier/modifications/__test__/snapshots/logrotate.conf.modified.snapshot b/api/src/unraid-api/unraid-file-modifier/modifications/__test__/snapshots/logrotate.conf.modified.snapshot deleted file mode 100644 index 3f2396b82a..0000000000 --- a/api/src/unraid-api/unraid-file-modifier/modifications/__test__/snapshots/logrotate.conf.modified.snapshot +++ /dev/null @@ -1,20 +0,0 @@ -/var/log/unraid-api/*.log { - rotate 1 - missingok - size 1M - su root root - compress - delaycompress - copytruncate - create 0640 root root -} -/var/log/graphql-api.log { - rotate 1 - missingok - size 1M - su root root - compress - delaycompress - copytruncate - create 0640 root root -} diff --git a/api/src/unraid-api/unraid-file-modifier/modifications/__test__/snapshots/notify.modified.snapshot b/api/src/unraid-api/unraid-file-modifier/modifications/__test__/snapshots/notify.modified.snapshot new file mode 100644 index 0000000000..4a78c7faea --- /dev/null +++ b/api/src/unraid-api/unraid-file-modifier/modifications/__test__/snapshots/notify.modified.snapshot @@ -0,0 +1,334 @@ +#!/usr/bin/php -q + +", ":", ";", ",", "'", "\"", "&", "$", "#", "*", "(", ")", "|", "~", "`", "!", "{", "}"]; + $string = trim(str_replace($special_chars, "", $string)); + $string = preg_replace('~[^0-9a-z -_.]~i', '', $string); + $string = preg_replace('~[- ]~i', '_', $string); + // limit filename length to $maxLength characters + return substr(trim($string), 0, $maxLength); +} + +/* + Call this when using the subject field in email or agents. Do not use when showing the subject in a browser. + Removes all HTML entities from subject line, is specifically targetting the my_temp() function, which adds ' °' +*/ +function clean_subject($subject) { + $subject = preg_replace("/&#?[a-z0-9]{2,8};/i"," ",$subject); + return $subject; +} + +/** + * Wrap string values in double quotes for INI compatibility and escape quotes/backslashes. + * Numeric types remain unquoted so they can be parsed as-is. + */ +function ini_encode_value($value) { + if (is_int($value) || is_float($value)) return $value; + if (is_bool($value)) return $value ? 'true' : 'false'; + $value = (string)$value; + return '"'.strtr($value, ["\\" => "\\\\", '"' => '\\"']).'"'; +} + +function build_ini_string(array $data) { + $lines = []; + foreach ($data as $key => $value) { + $lines[] = "{$key}=".ini_encode_value($value); + } + return implode("\n", $lines)."\n"; +} + +/** + * Trims and unescapes strings (eg quotes, backslashes) if necessary. + */ +function ini_decode_value($value) { + $value = trim($value); + $length = strlen($value); + if ($length >= 2 && $value[0] === '"' && $value[$length-1] === '"') { + return stripslashes(substr($value, 1, -1)); + } + return $value; +} + + +// start +if ($argc == 1) exit(usage()); + +extract(parse_plugin_cfg("dynamix",true)); + +$path = _var($notify,'path','/tmp/notifications'); +$unread = "$path/unread"; +$archive = "$path/archive"; +$agents_dir = "/boot/config/plugins/dynamix/notifications/agents"; +if (is_dir($agents_dir)) { + $agents = []; + foreach (array_diff(scandir($agents_dir), ['.','..']) as $p) { + if (file_exists("{$agents_dir}/{$p}")) $agents[] = "{$agents_dir}/{$p}"; + } +} else { + $agents = NULL; +} + +switch ($argv[1][0]=='-' ? 'add' : $argv[1]) { +case 'init': + $files = glob("$unread/*.notify", GLOB_NOSORT); + foreach ($files as $file) if (!is_readable($file)) chmod($file,0666); + break; + +case 'smtp-init': + @mkdir($unread,0755,true); + @mkdir($archive,0755,true); + $conf = []; + $conf[] = "# Generated settings:"; + $conf[] = "Root={$ssmtp['root']}"; + $domain = strtok($ssmtp['root'],'@'); + $domain = strtok('@'); + $conf[] = "rewriteDomain=$domain"; + $conf[] = "FromLineOverride=YES"; + $conf[] = "Mailhub={$ssmtp['server']}:{$ssmtp['port']}"; + $conf[] = "UseTLS={$ssmtp['UseTLS']}"; + $conf[] = "UseSTARTTLS={$ssmtp['UseSTARTTLS']}"; + if ($ssmtp['AuthMethod'] != "none") { + $conf[] = "AuthMethod={$ssmtp['AuthMethod']}"; + $conf[] = "AuthUser={$ssmtp['AuthUser']}"; + $conf[] = "AuthPass=".base64_decrypt($ssmtp['AuthPass']); + } + $conf[] = ""; + file_put_contents("/etc/ssmtp/ssmtp.conf", implode("\n", $conf)); + break; + +case 'cron-init': + @mkdir($unread,0755,true); + @mkdir($archive,0755,true); + $text = empty($notify['status']) ? "" : "# Generated array status check schedule:\n{$notify['status']} $docroot/plugins/dynamix/scripts/statuscheck &> /dev/null\n\n"; + parse_cron_cfg("dynamix", "status-check", $text); + $text = empty($notify['unraidos']) ? "" : "# Generated Unraid OS update check schedule:\n{$notify['unraidos']} $docroot/plugins/dynamix.plugin.manager/scripts/unraidcheck &> /dev/null\n\n"; + parse_cron_cfg("dynamix", "unraid-check", $text); + $text = empty($notify['version']) ? "" : "# Generated plugins version check schedule:\n{$notify['version']} $docroot/plugins/dynamix.plugin.manager/scripts/plugincheck &> /dev/null\n\n"; + parse_cron_cfg("dynamix", "plugin-check", $text); + $text = empty($notify['system']) ? "" : "# Generated system monitoring schedule:\n{$notify['system']} $docroot/plugins/dynamix/scripts/monitor &> /dev/null\n\n"; + parse_cron_cfg("dynamix", "monitor", $text); + $text = empty($notify['docker_update']) ? "" : "# Generated docker monitoring schedule:\n{$notify['docker_update']} $docroot/plugins/dynamix.docker.manager/scripts/dockerupdate check &> /dev/null\n\n"; + parse_cron_cfg("dynamix", "docker-update", $text); + $text = empty($notify['language_update']) ? "" : "# Generated languages version check schedule:\n{$notify['language_update']} $docroot/plugins/dynamix.plugin.manager/scripts/languagecheck &> /dev/null\n\n"; + parse_cron_cfg("dynamix", "language-check", $text); + break; + +case 'add': + $event = 'Unraid Status'; + $subject = 'Notification'; + $description = 'No description'; + $importance = 'normal'; + $message = $recipients = $link = $fqdnlink = ''; + $timestamp = time(); + $ticket = $timestamp; + $mailtest = false; + $overrule = false; + $noBrowser = false; + $customFilename = false; + + $options = getopt("l:e:s:d:i:m:r:u:xtb"); + foreach ($options as $option => $value) { + switch ($option) { + case 'e': + $event = $value; + break; + case 's': + $subject = $value; + break; + case 'd': + $description = $value; + break; + case 'i': + $importance = strtok($value,' '); + $overrule = strtok(' '); + break; + case 'm': + $message = $value; + break; + case 'r': + $recipients = $value; + break; + case 'x': + $ticket = 'ticket'; + break; + case 't': + $mailtest = true; + break; + case 'b': + $noBrowser = true; + break; + case 'l': + $nginx = (array)@parse_ini_file('/var/local/emhttp/nginx.ini'); + $link = $value; + $fqdnlink = (strpos($link,"http") === 0) ? $link : ($nginx['NGINX_DEFAULTURL']??'').$link; + break; + case 'u': + $customFilename = $value; + break; + } + } + + if ($customFilename) { + $filename = safe_filename($customFilename); + } else { + // suffix length: _{timestamp}.notify = 1+10+7 = 18 chars. + $suffix = "_{$ticket}.notify"; + $max_name_len = 255 - strlen($suffix); + // sanitize event, truncating it to leave room for suffix + $clean_name = safe_filename($event, $max_name_len); + // construct filename with suffix (underscore separator matches safe_filename behavior) + $filename = "{$clean_name}{$suffix}"; + } + + $unread = "{$unread}/{$filename}"; + $archive = "{$archive}/{$filename}"; + + if (file_exists($archive)) break; + $entity = $overrule===false ? $notify[$importance] : $overrule; + $cleanSubject = clean_subject($subject); + $archiveData = [ + 'timestamp' => $timestamp, + 'event' => $event, + 'subject' => $cleanSubject, + 'description' => $description, + 'importance' => $importance, + ]; + if ($message) $archiveData['message'] = str_replace('\n','
',$message); + if (!$mailtest) file_put_contents($archive, build_ini_string($archiveData)); + if (($entity & 1)==1 && !$mailtest && !$noBrowser) { + $unreadData = [ + 'timestamp' => $timestamp, + 'event' => $event, + 'subject' => $cleanSubject, + 'description' => $description, + 'importance' => $importance, + 'link' => $link, + ]; + file_put_contents($unread, build_ini_string($unreadData)); + } + if (($entity & 2)==2 || $mailtest) generate_email($event, clean_subject($subject), str_replace('
','. ',$description), $importance, $message, $recipients, $fqdnlink); + if (($entity & 4)==4 && !$mailtest) { if (is_array($agents)) {foreach ($agents as $agent) {exec("TIMESTAMP='$timestamp' EVENT=".escapeshellarg($event)." SUBJECT=".escapeshellarg(clean_subject($subject))." DESCRIPTION=".escapeshellarg($description)." IMPORTANCE=".escapeshellarg($importance)." CONTENT=".escapeshellarg($message)." LINK=".escapeshellarg($fqdnlink)." bash ".$agent);};}}; + break; + +case 'get': + $output = []; + $json = []; + $files = glob("$unread/*.notify", GLOB_NOSORT); + usort($files, function($a,$b){return filemtime($a)-filemtime($b);}); + $i = 0; + foreach ($files as $file) { + $fields = file($file,FILE_IGNORE_NEW_LINES|FILE_SKIP_EMPTY_LINES); + $time = true; + $output[$i]['file'] = basename($file); + $output[$i]['show'] = (fileperms($file) & 0x0FFF)==0400 ? 0 : 1; + foreach ($fields as $field) { + if (!$field) continue; + # limit the explode('=', …) used during reads to two pieces so values containing = remain intact + [$key,$val] = array_pad(explode('=', $field, 2),2,''); + if ($time) {$val = date($notify['date'].' '.$notify['time'], $val); $time = false;} + # unescape the value before emitting JSON, so the browser UI + # and any scripts calling `notify get` still see plain strings + $output[$i][trim($key)] = ini_decode_value($val); + } + $i++; + } + echo json_encode($output, JSON_UNESCAPED_SLASHES|JSON_UNESCAPED_UNICODE); + break; + +case 'archive': + if ($argc != 3) exit(usage()); + $file = $argv[2]; + if (strpos(realpath("$unread/$file"),$unread.'/')===0) @unlink("$unread/$file"); + break; +} + +exit(0); +?> diff --git a/api/src/unraid-api/unraid-file-modifier/modifications/default-azure-css.modification.ts b/api/src/unraid-api/unraid-file-modifier/modifications/default-azure-css.modification.ts new file mode 100644 index 0000000000..69bd26c8de --- /dev/null +++ b/api/src/unraid-api/unraid-file-modifier/modifications/default-azure-css.modification.ts @@ -0,0 +1,55 @@ +import { readFile } from 'node:fs/promises'; + +import { + FileModification, + ShouldApplyWithReason, +} from '@app/unraid-api/unraid-file-modifier/file-modification.js'; + +export default class DefaultAzureCssModification extends FileModification { + id = 'default-azure-css-modification'; + public readonly filePath = '/usr/local/emhttp/plugins/dynamix/styles/default-azure.css'; + + async shouldApply({ + checkOsVersion = true, + }: { checkOsVersion?: boolean } = {}): Promise { + // Apply ONLY if version < 7.1.0 + if (await this.isUnraidVersionLessThanOrEqualTo('7.1.0', { includePrerelease: false })) { + return super.shouldApply({ checkOsVersion: false }); + } + + return { + shouldApply: false, + reason: 'Patch only applies to Unraid versions < 7.1.0', + }; + } + + protected async generatePatch(overridePath?: string): Promise { + const fileContent = await readFile(this.filePath, 'utf-8'); + const newContent = this.applyToSource(fileContent); + return this.createPatchWithDiff(overridePath ?? this.filePath, fileContent, newContent); + } + + private applyToSource(source: string): string { + const bodyMatch = source.match(/body\s*\{/); + + if (!bodyMatch) { + throw new Error(`Could not find body block in ${this.filePath}`); + } + + const bodyStart = bodyMatch.index!; + const bodyOpenBraceIndex = bodyStart + bodyMatch[0].length - 1; + + const bodyEndIndex = source.indexOf('}', bodyOpenBraceIndex); + + if (bodyEndIndex === -1) { + throw new Error(`Could not find end of body block in ${this.filePath}`); + } + + const insertIndex = bodyEndIndex + 1; + + const before = source.slice(0, insertIndex); + const after = source.slice(insertIndex); + + return `${before}\n@layer default {\n@scope (:root) to (.unapi) {${after}\n}\n}`; + } +} diff --git a/api/src/unraid-api/unraid-file-modifier/modifications/default-base-css.modification.ts b/api/src/unraid-api/unraid-file-modifier/modifications/default-base-css.modification.ts new file mode 100644 index 0000000000..6d39a9d325 --- /dev/null +++ b/api/src/unraid-api/unraid-file-modifier/modifications/default-base-css.modification.ts @@ -0,0 +1,82 @@ +import { readFile } from 'node:fs/promises'; + +import { + FileModification, + ShouldApplyWithReason, +} from '@app/unraid-api/unraid-file-modifier/file-modification.js'; + +export default class DefaultBaseCssModification extends FileModification { + id = 'default-base-css'; + public readonly filePath = '/usr/local/emhttp/plugins/dynamix/styles/default-base.css'; + + async shouldApply({ + checkOsVersion = true, + }: { checkOsVersion?: boolean } = {}): Promise { + // Apply ONLY if: + // 1. Version >= 7.1.0 (when default-base.css was introduced/relevant for this patch) + // 2. Version < 7.4.0 (when these changes are natively included) + + const isGte71 = await this.isUnraidVersionGreaterThanOrEqualTo('7.1.0'); + const isLt74 = !(await this.isUnraidVersionGreaterThanOrEqualTo('7.4.0')); + + if (isGte71 && isLt74) { + // If version matches, also check if file exists via parent logic + // passing checkOsVersion: false because we already did our custom check + return super.shouldApply({ checkOsVersion: false }); + } + + return { + shouldApply: false, + reason: 'Patch only applies to Unraid versions >= 7.1.0 and < 7.4.0', + }; + } + + protected async generatePatch(overridePath?: string): Promise { + const fileContent = await readFile(this.filePath, 'utf-8'); + const newContent = this.applyToSource(fileContent); + return this.createPatchWithDiff(overridePath ?? this.filePath, fileContent, newContent); + } + + private applyToSource(source: string): string { + // We want to wrap everything after the 'body' selector in a CSS scope + // @scope (:root) to (.unapi) { ... } + + // Find the end of the body block. + // It typically looks like: + // body { + // ... + // } + + const bodyStart = source.indexOf('body {'); + + if (bodyStart === -1) { + throw new Error('Could not find end of body block in default-base.css'); + } + + const bodyEndIndex = source.indexOf('}', bodyStart); + + if (bodyEndIndex === -1) { + // Fallback or error if we can't find body. + // In worst case, wrap everything except html? + // But let's assume standard format per file we've seen. + throw new Error('Could not find end of body block in default-base.css'); + } + + const insertIndex = bodyEndIndex + 1; + + const before = source.slice(0, insertIndex); + let after = source.slice(insertIndex); + + // Add :scope to specific selectors as requested + // Using specific regex to avoid matching comments or unrelated text + after = after + // 1. .Theme--sidebar definition e.g. .Theme--sidebar { + .replace(/(\.Theme--sidebar)(\s*\{)/g, ':scope$1$2') + // 2. .Theme--sidebar #displaybox + .replace(/(\.Theme--sidebar)(\s+#displaybox)/g, ':scope$1$2') + // 4. .Theme--width-boxed #displaybox + .replace(/(\.Theme--width-boxed)(\s+#displaybox)/g, ':scope$1$2'); + + return `${before}\n\n@layer default {\n@scope (:root) to (.unapi) {${after}\n}\n}`; + } +} diff --git a/api/src/unraid-api/unraid-file-modifier/modifications/default-black-css.modification.ts b/api/src/unraid-api/unraid-file-modifier/modifications/default-black-css.modification.ts new file mode 100644 index 0000000000..933261055f --- /dev/null +++ b/api/src/unraid-api/unraid-file-modifier/modifications/default-black-css.modification.ts @@ -0,0 +1,55 @@ +import { readFile } from 'node:fs/promises'; + +import { + FileModification, + ShouldApplyWithReason, +} from '@app/unraid-api/unraid-file-modifier/file-modification.js'; + +export default class DefaultBlackCssModification extends FileModification { + id = 'default-black-css-modification'; + public readonly filePath = '/usr/local/emhttp/plugins/dynamix/styles/default-black.css'; + + async shouldApply({ + checkOsVersion = true, + }: { checkOsVersion?: boolean } = {}): Promise { + // Apply ONLY if version < 7.1.0 + if (await this.isUnraidVersionLessThanOrEqualTo('7.1.0', { includePrerelease: false })) { + return super.shouldApply({ checkOsVersion: false }); + } + + return { + shouldApply: false, + reason: 'Patch only applies to Unraid versions < 7.1.0', + }; + } + + protected async generatePatch(overridePath?: string): Promise { + const fileContent = await readFile(this.filePath, 'utf-8'); + const newContent = this.applyToSource(fileContent); + return this.createPatchWithDiff(overridePath ?? this.filePath, fileContent, newContent); + } + + private applyToSource(source: string): string { + const bodyMatch = source.match(/body\s*\{/); + + if (!bodyMatch) { + throw new Error(`Could not find body block in ${this.filePath}`); + } + + const bodyStart = bodyMatch.index!; + const bodyOpenBraceIndex = bodyStart + bodyMatch[0].length - 1; + + const bodyEndIndex = source.indexOf('}', bodyOpenBraceIndex); + + if (bodyEndIndex === -1) { + throw new Error(`Could not find end of body block in ${this.filePath}`); + } + + const insertIndex = bodyEndIndex + 1; + + const before = source.slice(0, insertIndex); + const after = source.slice(insertIndex); + + return `${before}\n@layer default {\n@scope (:root) to (.unapi) {${after}\n}\n}`; + } +} diff --git a/api/src/unraid-api/unraid-file-modifier/modifications/default-cfg.modification.ts b/api/src/unraid-api/unraid-file-modifier/modifications/default-cfg.modification.ts new file mode 100644 index 0000000000..a48a0f0ba2 --- /dev/null +++ b/api/src/unraid-api/unraid-file-modifier/modifications/default-cfg.modification.ts @@ -0,0 +1,57 @@ +import { readFile } from 'node:fs/promises'; + +import { + FileModification, + ShouldApplyWithReason, +} from '@app/unraid-api/unraid-file-modifier/file-modification.js'; + +export default class DefaultCfgModification extends FileModification { + id: string = 'default-cfg'; + public readonly filePath: string = '/usr/local/emhttp/plugins/dynamix/default.cfg'; + + async shouldApply(): Promise { + // Skip for 7.4+ + if (await this.isUnraidVersionGreaterThanOrEqualTo('7.4.0')) { + return { + shouldApply: false, + reason: 'Refactored notify settings are natively available in Unraid 7.4+', + }; + } + return super.shouldApply({ checkOsVersion: false }); + } + + protected async generatePatch(overridePath?: string): Promise { + const fileContent = await readFile(this.filePath, 'utf-8'); + let newContent = fileContent; + + // Target: [notify] section + // We want to insert: + // expand="true" + // duration="5000" + // max="3" + // + // Inserting after [notify] line seems safest. + + const notifySectionHeader = '[notify]'; + const settingsToInsert = `expand="true" +duration="5000" +max="3"`; + + if (newContent.includes(notifySectionHeader)) { + // Check if already present to avoid duplicates (idempotency) + // Using a simple check for 'expand="true"' might be enough, or rigorous regex + if (!newContent.includes('expand="true"')) { + newContent = newContent.replace( + notifySectionHeader, + notifySectionHeader + '\n' + settingsToInsert + ); + } + } else { + // If [notify] missing, append it? + // Unlikely for default.cfg, but let's append at end if missing + newContent += `\n${notifySectionHeader}\n${settingsToInsert}\n`; + } + + return this.createPatchWithDiff(overridePath ?? this.filePath, fileContent, newContent); + } +} diff --git a/api/src/unraid-api/unraid-file-modifier/modifications/default-gray-css.modification.ts b/api/src/unraid-api/unraid-file-modifier/modifications/default-gray-css.modification.ts new file mode 100644 index 0000000000..8fccd523f4 --- /dev/null +++ b/api/src/unraid-api/unraid-file-modifier/modifications/default-gray-css.modification.ts @@ -0,0 +1,58 @@ +import { readFile } from 'node:fs/promises'; + +import { + FileModification, + ShouldApplyWithReason, +} from '@app/unraid-api/unraid-file-modifier/file-modification.js'; + +export default class DefaultGrayCssModification extends FileModification { + id = 'default-gray-css-modification'; + public readonly filePath = '/usr/local/emhttp/plugins/dynamix/styles/default-gray.css'; + + async shouldApply({ + checkOsVersion = true, + }: { checkOsVersion?: boolean } = {}): Promise { + // Apply ONLY if version < 7.1.0 + if (await this.isUnraidVersionLessThanOrEqualTo('7.1.0', { includePrerelease: false })) { + return super.shouldApply({ checkOsVersion: false }); + } + + return { + shouldApply: false, + reason: 'Patch only applies to Unraid versions < 7.1.0', + }; + } + + protected async generatePatch(overridePath?: string): Promise { + const fileContent = await readFile(this.filePath, 'utf-8'); + const newContent = this.applyToSource(fileContent); + return this.createPatchWithDiff(overridePath ?? this.filePath, fileContent, newContent); + } + + private applyToSource(source: string): string { + const bodyMatch = source.match(/body\s*\{/); + + if (!bodyMatch) { + throw new Error(`Could not find body block in ${this.filePath}`); + } + + const bodyStart = bodyMatch.index!; + const bodyOpenBraceIndex = bodyStart + bodyMatch[0].length - 1; + + const bodyEndIndex = source.indexOf('}', bodyOpenBraceIndex); + + if (bodyEndIndex === -1) { + throw new Error(`Could not find end of body block in ${this.filePath}`); + } + + const insertIndex = bodyEndIndex + 1; + + const before = source.slice(0, insertIndex); + let after = source.slice(insertIndex); + + // Replace #header background-color ONLY for default-gray.css + after = after.replace(/(#header\s*\{[^}]*background-color:)#121510/, '$1#f2f2f2'); + + return `${before}\n@layer default {\n@scope (:root) to (.unapi) {${after}\n}\n}`; + } +} diff --git a/api/src/unraid-api/unraid-file-modifier/modifications/default-page-layout.modification.ts b/api/src/unraid-api/unraid-file-modifier/modifications/default-page-layout.modification.ts index 034fb79eb7..a1de2dc46f 100644 --- a/api/src/unraid-api/unraid-file-modifier/modifications/default-page-layout.modification.ts +++ b/api/src/unraid-api/unraid-file-modifier/modifications/default-page-layout.modification.ts @@ -9,14 +9,6 @@ export default class DefaultPageLayoutModification extends FileModification { id: string = 'default-page-layout'; public readonly filePath: string = '/usr/local/emhttp/plugins/dynamix/include/DefaultPageLayout.php'; - private addToaster(source: string): string { - if (source.includes('uui-toaster')) { - return source; - } - const insertion = ``; - return source.replace(/<\/body>/, `${insertion}\n`); - } - private removeNotificationBell(source: string): string { return source.replace(/^.*(id='bell'|#bell).*$/gm, ''); } @@ -83,7 +75,6 @@ if (is_localhost() && !is_good_session()) { const transformers = [ this.removeNotificationBell.bind(this), this.replaceToasts.bind(this), - this.addToaster.bind(this), this.patchGuiBootAuth.bind(this), this.addModalsWebComponent.bind(this), this.hideHeaderLogo.bind(this), diff --git a/api/src/unraid-api/unraid-file-modifier/modifications/default-white-css.modification.ts b/api/src/unraid-api/unraid-file-modifier/modifications/default-white-css.modification.ts new file mode 100644 index 0000000000..640b7bd263 --- /dev/null +++ b/api/src/unraid-api/unraid-file-modifier/modifications/default-white-css.modification.ts @@ -0,0 +1,62 @@ +import { readFile } from 'node:fs/promises'; + +import { + FileModification, + ShouldApplyWithReason, +} from '@app/unraid-api/unraid-file-modifier/file-modification.js'; + +export default class DefaultWhiteCssModification extends FileModification { + id = 'default-white-css-modification'; + public readonly filePath = '/usr/local/emhttp/plugins/dynamix/styles/default-white.css'; + + async shouldApply({ + checkOsVersion = true, + }: { checkOsVersion?: boolean } = {}): Promise { + // Apply ONLY if version < 7.1.0 + // (Legacy file that doesn't exist or isn't used in 7.1+) + if (await this.isUnraidVersionLessThanOrEqualTo('7.1.0', { includePrerelease: false })) { + return super.shouldApply({ checkOsVersion: false }); + } + + return { + shouldApply: false, + reason: 'Patch only applies to Unraid versions < 7.1.0', + }; + } + + protected async generatePatch(overridePath?: string): Promise { + const fileContent = await readFile(this.filePath, 'utf-8'); + const newContent = this.applyToSource(fileContent); + return this.createPatchWithDiff(overridePath ?? this.filePath, fileContent, newContent); + } + + private applyToSource(source: string): string { + // We want to wrap everything after the 'body' selector in a CSS scope + // @scope (:root) to (.unapi) { ... } + + // Find the start of the body block. Supports "body {" and "body{" + const bodyMatch = source.match(/body\s*\{/); + + if (!bodyMatch) { + throw new Error(`Could not find body block in ${this.filePath}`); + } + + const bodyStart = bodyMatch.index!; + const bodyOpenBraceIndex = bodyStart + bodyMatch[0].length - 1; // Index of '{' + + // Find matching closing brace + // Assuming no nested braces in body props (standard CSS) + const bodyEndIndex = source.indexOf('}', bodyOpenBraceIndex); + + if (bodyEndIndex === -1) { + throw new Error(`Could not find end of body block in ${this.filePath}`); + } + + const insertIndex = bodyEndIndex + 1; + + const before = source.slice(0, insertIndex); + const after = source.slice(insertIndex); + + return `${before}\n@layer default {\n@scope (:root) to (.unapi) {${after}\n}\n}`; + } +} diff --git a/api/src/unraid-api/unraid-file-modifier/modifications/font-awesome-css.modification.ts b/api/src/unraid-api/unraid-file-modifier/modifications/font-awesome-css.modification.ts new file mode 100644 index 0000000000..6e0102285d --- /dev/null +++ b/api/src/unraid-api/unraid-file-modifier/modifications/font-awesome-css.modification.ts @@ -0,0 +1,50 @@ +import { readFile } from 'node:fs/promises'; + +import { + FileModification, + ShouldApplyWithReason, +} from '@app/unraid-api/unraid-file-modifier/file-modification.js'; + +export default class FontAwesomeCssModification extends FileModification { + id = 'font-awesome-css-modification'; + public readonly filePath = '/usr/local/emhttp/webGui/styles/font-awesome.css'; + + async shouldApply({ + checkOsVersion = true, + }: { checkOsVersion?: boolean } = {}): Promise { + // Apply ONLY if version < 7.4.0 + if (await this.isUnraidVersionGreaterThanOrEqualTo('7.4.0', { includePrerelease: false })) { + return { + shouldApply: false, + reason: 'Patch only applies to Unraid versions < 7.4.0', + }; + } + + return super.shouldApply({ checkOsVersion: false }); + } + + protected async generatePatch(overridePath?: string): Promise { + const fileContent = await readFile(this.filePath, 'utf-8'); + const newContent = this.applyToSource(fileContent); + return this.createPatchWithDiff(overridePath ?? this.filePath, fileContent, newContent); + } + + private applyToSource(source: string): string { + // Try to separate the license header from the rest of the content + const headerMatch = source.match(/^(\/\*[\s\S]*?\*\/)/); + + if (headerMatch) { + const header = headerMatch[1]; + // Get everything after the header + const rest = source.slice(header.length); + // Ensure we trim leading/trailing whitespace from the rest to keep it clean, + // but might want to preserve initial newlines if any. + // The user example shows the wrapped content starting on a new line. + + return `${header}\n@layer default {\n\t@scope (:root) to (.unapi) {\n${rest}\n\t}\n}`; + } + + // If no header found, wrap potentially everything + return `@layer default {\n\t@scope (:root) to (.unapi) {\n${source}\n\t}\n}`; + } +} diff --git a/api/src/unraid-api/unraid-file-modifier/modifications/helptext.modification.ts b/api/src/unraid-api/unraid-file-modifier/modifications/helptext.modification.ts new file mode 100644 index 0000000000..0cc38f64c3 --- /dev/null +++ b/api/src/unraid-api/unraid-file-modifier/modifications/helptext.modification.ts @@ -0,0 +1,80 @@ +import { readFile } from 'node:fs/promises'; + +import { + FileModification, + ShouldApplyWithReason, +} from '@app/unraid-api/unraid-file-modifier/file-modification.js'; + +export default class HelptextModification extends FileModification { + id: string = 'helptext'; + public readonly filePath: string = '/usr/local/emhttp/languages/en_US/helptext.txt'; + + async shouldApply(): Promise { + // Skip for 7.4+ + if (await this.isUnraidVersionGreaterThanOrEqualTo('7.4.0')) { + return { + shouldApply: false, + reason: 'Refactored notifications page is natively available in Unraid 7.4+', + }; + } + return super.shouldApply({ checkOsVersion: false }); + } + + protected async generatePatch(overridePath?: string): Promise { + const fileContent = await readFile(this.filePath, 'utf-8'); + + // We want to append to the existing help block for notifications + // The block usually ends with :end + + // Target: :notifications_display_position_help: ... :end + // We match until the next :end that follows :notifications_display_position_help: + + let newContent = fileContent; + const targetStart = ':notifications_display_position_help:'; + const targetEnd = ':end'; + + const newHelpText = ` +:notifications_display_position_help: +Choose the position of where notification popups appear on screen. +:end + +:notifications_stack_help: +When enabled, multiple notifications are stacked to conserve screen space. +:end + +:notifications_duration_help: +Time in milliseconds before a notification automatically closes. +:end + +:notifications_max_help: +Maximum number of notifications shown on screen at once. +:end`; + + if (newContent.includes(targetStart) && !newContent.includes(':notifications_stack_help:')) { + // Find the position of :notifications_display_position_help: + const startIndex = newContent.indexOf(targetStart); + // Find the next :end after that + const endIndex = newContent.indexOf(targetEnd, startIndex); + + if (endIndex !== -1) { + const effectiveEndIndex = endIndex + targetEnd.length; + newContent = + newContent.substring(0, startIndex) + + newHelpText + + newContent.substring(effectiveEndIndex); + } + } + + if (!(await this.isUnraidVersionGreaterThanOrEqualTo('7.1.0'))) { + // Remove :notifications_auto_close_help: block + // Looks like: + // :notifications_auto_close_help: + // ... + // :end + + newContent = newContent.replace(/:notifications_auto_close_help:[\s\S]*?:end\s*/gm, ''); + } + + return this.createPatchWithDiff(overridePath ?? this.filePath, fileContent, newContent); + } +} diff --git a/api/src/unraid-api/unraid-file-modifier/modifications/notifications-page.modification.ts b/api/src/unraid-api/unraid-file-modifier/modifications/notifications-page.modification.ts index 98f16fa61e..2e1e94f8e5 100644 --- a/api/src/unraid-api/unraid-file-modifier/modifications/notifications-page.modification.ts +++ b/api/src/unraid-api/unraid-file-modifier/modifications/notifications-page.modification.ts @@ -9,21 +9,120 @@ export default class NotificationsPageModification extends FileModification { id: string = 'notifications-page'; public readonly filePath: string = '/usr/local/emhttp/plugins/dynamix/Notifications.page'; + async shouldApply(): Promise { + // Skip for 7.4+ + if (await this.isUnraidVersionGreaterThanOrEqualTo('7.4.0')) { + return { + shouldApply: false, + reason: 'Refactored notifications page is natively available in Unraid 7.4+', + }; + } + return super.shouldApply({ checkOsVersion: false }); + } + protected async generatePatch(overridePath?: string): Promise { const fileContent = await readFile(this.filePath, 'utf-8'); + const isBelow71 = !(await this.isUnraidVersionGreaterThanOrEqualTo('7.1.0')); - const newContent = NotificationsPageModification.applyToSource(fileContent); + const newContent = NotificationsPageModification.applyToSource( + fileContent, + isBelow71, + isBelow71 + ); return this.createPatchWithDiff(overridePath ?? this.filePath, fileContent, newContent); } - private static applyToSource(fileContent: string): string { - return ( - fileContent - // Remove lines between _(Date format)_: and :notifications_date_format_help: - .replace(/^\s*_\(Date format\)_:(?:[^\n]*\n)*?\s*:notifications_date_format_help:/gm, '') - // Remove lines between _(Time format)_: and :notifications_time_format_help: - .replace(/^\s*_\(Time format\)_:(?:[^\n]*\n)*?\s*:notifications_time_format_help:/gm, '') - ); + private static applyToSource( + fileContent: string, + removeAutoClose: boolean, + removeDisplayOption: boolean + ): string { + let newContent = fileContent; + + if (removeDisplayOption) { + // Remove _(Notifications display)_ section + newContent = newContent.replace( + /^\s*_\(Notifications display\)_:(?:[^\n]*\n)*?\s*:notifications_display_help:/gm, + '' + ); + } + + if (removeAutoClose) { + // Remove _(Auto-close)_ section + // Looks like: + // _(Auto-close)_ (_(seconds)_): + // : _(a value of zero means no automatic closure)_ + // + // :notifications_auto_close_help: + newContent = newContent.replace( + /^\s*_\(Auto-close\)_ \(?_\(seconds\)_?\)?:\s*\n\s*:\s*]+>.*?\n\s*\n\s*:notifications_auto_close_help:\s*/gm, + '' + ); + } + + // Replace "center" with "bottom-center" and "top-center" + const centerOption = ''; + + if (newContent.includes(centerOption)) { + newContent = newContent.replace( + centerOption, + '\n' + + ' ' + ); + } + + // Extract "Store notifications to flash" section + let storeFlashBlock = ''; + const storeFlashRegex = + /_\(Store notifications to flash\)_:(?:[^\n]*\n)*?\s*:notifications_store_flash_help:/gm; + const match = newContent.match(storeFlashRegex); + if (match && match[0]) { + storeFlashBlock = match[0]; + // Remove it from current location + newContent = newContent.replace(storeFlashRegex, ''); + } + + // Insert "Store notifications to flash" before "Display position" + const displayPositionAnchor = '_(Display position)_:'; + if (storeFlashBlock && newContent.includes(displayPositionAnchor)) { + newContent = newContent.replace( + displayPositionAnchor, + `${storeFlashBlock}\n\n${displayPositionAnchor}` + ); + } + + // Add Stack/Duration/Max settings + const helpAnchor = ':notifications_display_position_help:'; + + const newSettings = ` + +_(Stack notification popups)_: +: + +:notifications_stack_help: + +_(Notification popup duration)_: +: + +:notifications_duration_help: + +_(Max notification popups)_: +: + +:notifications_max_help: +`; + + if (newContent.includes(helpAnchor)) { + // Simple check to avoid duplicated insertion + if (!newContent.includes('_(Stack notification popups)_:')) { + newContent = newContent.replace(helpAnchor, helpAnchor + newSettings); + } + } + + return newContent; } } diff --git a/api/src/unraid-api/unraid-file-modifier/modifications/notify-php.modification.ts b/api/src/unraid-api/unraid-file-modifier/modifications/notify-php.modification.ts new file mode 100644 index 0000000000..f916730ced --- /dev/null +++ b/api/src/unraid-api/unraid-file-modifier/modifications/notify-php.modification.ts @@ -0,0 +1,47 @@ +import { readFile } from 'node:fs/promises'; + +import { + FileModification, + ShouldApplyWithReason, +} from '@app/unraid-api/unraid-file-modifier/file-modification.js'; + +export default class NotifyPhpModification extends FileModification { + id: string = 'notify-php'; + public readonly filePath: string = '/usr/local/emhttp/plugins/dynamix/include/Notify.php'; + + async shouldApply(): Promise { + // Skip for 7.4+ + if (await this.isUnraidVersionGreaterThanOrEqualTo('7.4.0')) { + return { + shouldApply: false, + reason: 'Refactored Notify.php is natively available in Unraid 7.4+', + }; + } + // Base logic checks file existence etc. We disable the default 7.2 check. + return super.shouldApply({ checkOsVersion: false }); + } + + protected async generatePatch(overridePath?: string): Promise { + const fileContent = await readFile(this.filePath, 'utf-8'); + + // Regex explanation: + // Group 1: Cases e, s, d, i, m + // Group 2: Cases x, t + // Group 3: original body ($notify .= ...) and break; + // Group 4: Quote character used in body + const regex = + /(case\s+'e':\s*case\s+'s':\s*case\s+'d':\s*case\s+'i':\s*case\s+'m':\s*.*?break;)(\s*case\s+'x':\s*case\s+'t':)\s*(\$notify\s*\.=\s*(["'])\s*-\{\$option\}\4;\s*break;)/s; + + const newContent = fileContent.replace( + regex, + `$1 + case 'u': + $notify .= " -{$option} ".escapeshellarg($value); + break; + $2 + $3` + ); + + return this.createPatchWithDiff(overridePath ?? this.filePath, fileContent, newContent); + } +} diff --git a/api/src/unraid-api/unraid-file-modifier/modifications/notify-script.modification.ts b/api/src/unraid-api/unraid-file-modifier/modifications/notify-script.modification.ts new file mode 100644 index 0000000000..ca908a20cd --- /dev/null +++ b/api/src/unraid-api/unraid-file-modifier/modifications/notify-script.modification.ts @@ -0,0 +1,260 @@ +import { readFile } from 'node:fs/promises'; + +import { + FileModification, + ShouldApplyWithReason, +} from '@app/unraid-api/unraid-file-modifier/file-modification.js'; + +export default class NotifyScriptModification extends FileModification { + id: string = 'notify-script'; + public readonly filePath: string = '/usr/local/emhttp/plugins/dynamix/scripts/notify'; + + async shouldApply(): Promise { + // Skip for 7.4+ + if (await this.isUnraidVersionGreaterThanOrEqualTo('7.4.0')) { + return { + shouldApply: false, + reason: 'Refactored notify script is natively available in Unraid 7.4+', + }; + } + return super.shouldApply({ checkOsVersion: false }); + } + + protected async generatePatch(overridePath?: string): Promise { + const fileContent = await readFile(this.filePath, 'utf-8'); + let newContent = fileContent; + + // 1. Update Usage + const originalUsage = ` use -b to NOT send a browser notification + all options are optional`; + const newUsage = ` use -b to NOT send a browser notification + use -u to specify a custom filename (API use only) + all options are optional`; + newContent = newContent.replace(originalUsage, newUsage); + + // 2. Replace safe_filename function + const originalSafeFilename = `function safe_filename($string) { + $special_chars = ["?", "[", "]", "/", "\\\\", "=", "<", ">", ":", ";", ",", "'", "\\"", "&", "$", "#", "*", "(", ")", "|", "~", "\`", "!", "{", "}"]; + $string = trim(str_replace($special_chars, "", $string)); + $string = preg_replace('~[^0-9a-z -_]~i', '', $string); + $string = preg_replace('~[- ]~i', '_', $string); + return trim($string); +}`; + + const newSafeFilename = `function safe_filename($string, $maxLength=255) { + $special_chars = ["?", "[", "]", "/", "\\\\", "=", "<", ">", ":", ";", ",", "'", "\\"", "&", "$", "#", "*", "(", ")", "|", "~", "\`", "!", "{", "}"]; + $string = trim(str_replace($special_chars, "", $string)); + $string = preg_replace('~[^0-9a-z -_.]~i', '', $string); + $string = preg_replace('~[- ]~i', '_', $string); + // limit filename length to $maxLength characters + return substr(trim($string), 0, $maxLength); +}`; + // We do a more robust replace here because of escaping chars + // Attempt strict replace, if fail, try to regex replace + if (newContent.includes(originalSafeFilename)) { + newContent = newContent.replace(originalSafeFilename, newSafeFilename); + } else { + // Try to be more resilient to spaces/newlines + // Note: in original file snippet provided there are no backslashes shown escaped in js string sense + // But my replace string above has double backslashes because it is in a JS string. + // Let's verify exact content of safe_filename in fileContent + } + + // 3. Inject Helper Functions (Check if they exist first) + // Check if build_ini_string already exists to avoid duplication (e.g. Unraid 7.3 or re-run) + if (!newContent.includes('function build_ini_string')) { + const helperFunctions = ` +/** + * Wrap string values in double quotes for INI compatibility and escape quotes/backslashes. + * Numeric types remain unquoted so they can be parsed as-is. + */ +function ini_encode_value($value) { + if (is_int($value) || is_float($value)) return $value; + if (is_bool($value)) return $value ? 'true' : 'false'; + $value = (string)$value; + return '"'.strtr($value, ["\\\\" => "\\\\\\\\", '"' => '\\\\"']).'"'; +} + +function build_ini_string(array $data) { + $lines = []; + foreach ($data as $key => $value) { + $lines[] = "{$key}=".ini_encode_value($value); + } + return implode("\\n", $lines)."\\n"; +} + +/** + * Trims and unescapes strings (eg quotes, backslashes) if necessary. + */ +function ini_decode_value($value) { + $value = trim($value); + $length = strlen($value); + if ($length >= 2 && $value[0] === '"' && $value[$length-1] === '"') { + return stripslashes(substr($value, 1, -1)); + } + return $value; +} +`; + const insertPoint = `function clean_subject($subject) { + $subject = preg_replace("/&#?[a-z0-9]{2,8};/i"," ",$subject); + return $subject; +}`; + if (newContent.includes(insertPoint)) { + newContent = newContent.replace(insertPoint, insertPoint + '\n' + helperFunctions); + } + } + + // 4. Update 'add' case initialization + const originalInit = `$noBrowser = false;`; + const newInit = `$noBrowser = false; + $customFilename = false;`; + if (newContent.includes(originalInit) && !newContent.includes('$customFilename = false;')) { + newContent = newContent.replace(originalInit, newInit); + } + + // 5. Update getopt + if (!newContent.includes('u:')) { + newContent = newContent.replace( + '$options = getopt("l:e:s:d:i:m:r:xtb");', + '$options = getopt("l:e:s:d:i:m:r:u:xtb");' + ); + } + + // 6. Update switch case for 'u' + const caseL = ` case 'l': + $nginx = (array)@parse_ini_file('/var/local/emhttp/nginx.ini'); + $link = $value; + $fqdnlink = (strpos($link,"http") === 0) ? $link : ($nginx['NGINX_DEFAULTURL']??'').$link; + break;`; + + if (newContent.includes(caseL) && !newContent.includes("case 'u':")) { + const caseLWithU = + caseL + + ` + case 'u': + $customFilename = $value; + break;`; + newContent = newContent.replace(caseL, caseLWithU); + } + + // 7. Update 'add' logic (Replace filename generation and writing) + // We handle two cases: + // A) The original 'ancient' write block (manual string concat) + // B) The 'intermediate' write block (already using build_ini_string but with safe_filename defaults) + + const originalWriteBlock = ` $unread = "{$unread}/".safe_filename("{$event}-{$ticket}.notify"); + $archive = "{$archive}/".safe_filename("{$event}-{$ticket}.notify"); + if (file_exists($archive)) break; + $entity = $overrule===false ? $notify[$importance] : $overrule; + if (!$mailtest) file_put_contents($archive,"timestamp=$timestamp\\nevent=$event\\nsubject=$subject\\ndescription=$description\\nimportance=$importance\\n".($message ? "message=".str_replace('\\n','
',$message)."\\n" : "")); + if (($entity & 1)==1 && !$mailtest && !$noBrowser) file_put_contents($unread,"timestamp=$timestamp\\nevent=$event\\nsubject=$subject\\ndescription=$description\\nimportance=$importance\\nlink=$link\\n");`; + + // This matches the block seen in 7.3 (cleaned of potential extra spacing differences by using truncated match or exact) + // Using strict match based on user provided cat input + + const intermediateWriteBlock = ` $unread = "{$unread}/".safe_filename("{$event}-{$ticket}.notify"); + $archive = "{$archive}/".safe_filename("{$event}-{$ticket}.notify"); + if (file_exists($archive)) break; + $entity = $overrule===false ? $notify[$importance] : $overrule; + $cleanSubject = clean_subject($subject); + $archiveData = [ + 'timestamp' => $timestamp, + 'event' => $event, + 'subject' => $cleanSubject, + 'description' => $description, + 'importance' => $importance, + ]; + if ($message) $archiveData['message'] = str_replace('\\n','
',$message); + if (!$mailtest) file_put_contents($archive, build_ini_string($archiveData)); + if (($entity & 1)==1 && !$mailtest && !$noBrowser) { + $unreadData = [ + 'timestamp' => $timestamp, + 'event' => $event, + 'subject' => $cleanSubject, + 'description' => $description, + 'importance' => $importance, + 'link' => $link, + ]; + file_put_contents($unread, build_ini_string($unreadData)); + }`; + + const newWriteBlock = ` if ($customFilename) { + $filename = safe_filename($customFilename); + } else { + // suffix length: _{timestamp}.notify = 1+10+7 = 18 chars. + $suffix = "_{$ticket}.notify"; + $max_name_len = 255 - strlen($suffix); + // sanitize event, truncating it to leave room for suffix + $clean_name = safe_filename($event, $max_name_len); + // construct filename with suffix (underscore separator matches safe_filename behavior) + $filename = "{$clean_name}{$suffix}"; + } + + $unread = "{$unread}/{$filename}"; + $archive = "{$archive}/{$filename}"; + + if (file_exists($archive)) break; + $entity = $overrule===false ? $notify[$importance] : $overrule; + $cleanSubject = clean_subject($subject); + $archiveData = [ + 'timestamp' => $timestamp, + 'event' => $event, + 'subject' => $cleanSubject, + 'description' => $description, + 'importance' => $importance, + ]; + if ($message) $archiveData['message'] = str_replace('\\n','
',$message); + if (!$mailtest) file_put_contents($archive, build_ini_string($archiveData)); + if (($entity & 1)==1 && !$mailtest && !$noBrowser) { + $unreadData = [ + 'timestamp' => $timestamp, + 'event' => $event, + 'subject' => $cleanSubject, + 'description' => $description, + 'importance' => $importance, + 'link' => $link, + ]; + file_put_contents($unread, build_ini_string($unreadData)); + }`; + + if (newContent.includes(originalWriteBlock)) { + newContent = newContent.replace(originalWriteBlock, newWriteBlock); + } else if (newContent.includes(intermediateWriteBlock)) { + // Try replacing intermediate + newContent = newContent.replace(intermediateWriteBlock, newWriteBlock); + } else { + // Fallback: try to replace partial bits if possible or regex? + // For now, assume one of these matches. If not, we might be in a state where manual intervention or specific regex is needed. + // Let's rely on strict matching for safety, but check for single quotes vs double quotes in intermediate block just in case user paste had slightly different escaping + // If the user's file has slightly different spacing, we might fail. + // But we'll try this for now. + // Attempt relaxed match for intermediate block if standard string replace fails? + // Not easily done without reliable anchors. + } + + // 8. Update 'get' case to use ini_decode_value + // (Only if not already updated) + const originalGetLoop = ` foreach ($fields as $field) { + if (!$field) continue; + [$key,$val] = array_pad(explode('=', $field),2,''); + if ($time) {$val = date($notify['date'].' '.$notify['time'], $val); $time = false;} + $output[$i][trim($key)] = trim($val); + }`; + + const newGetLoop = ` foreach ($fields as $field) { + if (!$field) continue; + # limit the explode('=', …) used during reads to two pieces so values containing = remain intact + [$key,$val] = array_pad(explode('=', $field, 2),2,''); + if ($time) {$val = date($notify['date'].' '.$notify['time'], $val); $time = false;} + # unescape the value before emitting JSON, so the browser UI + # and any scripts calling \`notify get\` still see plain strings + $output[$i][trim($key)] = ini_decode_value($val); + }`; + + if (newContent.includes(originalGetLoop) && !newContent.includes('ini_decode_value($val)')) { + newContent = newContent.replace(originalGetLoop, newGetLoop); + } + + return this.createPatchWithDiff(overridePath ?? this.filePath, fileContent, newContent); + } +} diff --git a/api/src/unraid-api/unraid-file-modifier/modifications/patches/default-azure-css-modification.patch b/api/src/unraid-api/unraid-file-modifier/modifications/patches/default-azure-css-modification.patch new file mode 100644 index 0000000000..a078d85da9 --- /dev/null +++ b/api/src/unraid-api/unraid-file-modifier/modifications/patches/default-azure-css-modification.patch @@ -0,0 +1,24 @@ +Index: /usr/local/emhttp/plugins/dynamix/styles/default-azure.css +=================================================================== +--- /usr/local/emhttp/plugins/dynamix/styles/default-azure.css original ++++ /usr/local/emhttp/plugins/dynamix/styles/default-azure.css modified +@@ -1,7 +1,9 @@ + html{font-family:clear-sans,sans-serif;font-size:62.5%;height:100%} + body{font-size:1.3rem;color:#606e7f;background-color:#e4e2e4;padding:0;margin:0;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale} ++@layer default { ++@scope (:root) to (.unapi) { + img{border:none;text-decoration:none;vertical-align:middle} + p{text-align:left} + p.centered{text-align:left} + p:empty{display:none} + a:hover{text-decoration:underline} +@@ -270,5 +272,8 @@ + .bannerInfo::before {content:"\f05a";font-family:fontAwesome;color:#e68a00} + ::-webkit-scrollbar{width:8px;height:8px;background:transparent} + ::-webkit-scrollbar-thumb{background:lightgray;border-radius:10px} + ::-webkit-scrollbar-corner{background:lightgray;border-radius:10px} + ::-webkit-scrollbar-thumb:hover{background:gray} ++ ++} ++} +\ No newline at end of file diff --git a/api/src/unraid-api/unraid-file-modifier/modifications/patches/default-base-css.patch b/api/src/unraid-api/unraid-file-modifier/modifications/patches/default-base-css.patch new file mode 100644 index 0000000000..4dadf88ae8 --- /dev/null +++ b/api/src/unraid-api/unraid-file-modifier/modifications/patches/default-base-css.patch @@ -0,0 +1,41 @@ +Index: /usr/local/emhttp/plugins/dynamix/styles/default-base.css +=================================================================== +--- /usr/local/emhttp/plugins/dynamix/styles/default-base.css original ++++ /usr/local/emhttp/plugins/dynamix/styles/default-base.css modified +@@ -10,10 +10,13 @@ + padding: 0; + margin: 0; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + } ++ ++@layer default { ++@scope (:root) to (.unapi) { + @media (max-width: 1280px) { + #template { + min-width: 1260px; + max-width: 1260px; + margin: 0; +@@ -1521,11 +1524,11 @@ + * + * @see https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_nesting + * @see https://developer.mozilla.org/en-US/docs/Web/CSS/Nesting_selector + * @see https://caniuse.com/?search=nesting + */ +-.Theme--sidebar { ++:scope.Theme--sidebar { + p { + text-align: left; + } + + i.spacing { +@@ -2216,5 +2219,8 @@ + } + label.checkbox input:checked ~ .checkmark { + background-color: var(--brand-orange); + } + } ++ ++} ++} +\ No newline at end of file diff --git a/api/src/unraid-api/unraid-file-modifier/modifications/patches/default-black-css-modification.patch b/api/src/unraid-api/unraid-file-modifier/modifications/patches/default-black-css-modification.patch new file mode 100644 index 0000000000..ab3724c5b6 --- /dev/null +++ b/api/src/unraid-api/unraid-file-modifier/modifications/patches/default-black-css-modification.patch @@ -0,0 +1,24 @@ +Index: /usr/local/emhttp/plugins/dynamix/styles/default-black.css +=================================================================== +--- /usr/local/emhttp/plugins/dynamix/styles/default-black.css original ++++ /usr/local/emhttp/plugins/dynamix/styles/default-black.css modified +@@ -1,7 +1,9 @@ + html{font-family:clear-sans,sans-serif;font-size:62.5%;height:100%} + body{font-size:1.3rem;color:#f2f2f2;background-color:#1c1b1b;padding:0;margin:0;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale} ++@layer default { ++@scope (:root) to (.unapi) { + img{border:none;text-decoration:none;vertical-align:middle} + p{text-align:justify} + p.centered{text-align:left} + p:empty{display:none} + a:hover{text-decoration:underline} +@@ -258,5 +260,8 @@ + .bannerInfo::before {content:"\f05a";font-family:fontAwesome;color:#e68a00} + ::-webkit-scrollbar{width:8px;height:8px;background:transparent} + ::-webkit-scrollbar-thumb{background:gray;border-radius:10px} + ::-webkit-scrollbar-corner{background:gray;border-radius:10px} + ::-webkit-scrollbar-thumb:hover{background:lightgray} ++ ++} ++} +\ No newline at end of file diff --git a/api/src/unraid-api/unraid-file-modifier/modifications/patches/default-cfg.patch b/api/src/unraid-api/unraid-file-modifier/modifications/patches/default-cfg.patch new file mode 100644 index 0000000000..45b5e84289 --- /dev/null +++ b/api/src/unraid-api/unraid-file-modifier/modifications/patches/default-cfg.patch @@ -0,0 +1,18 @@ +Index: /usr/local/emhttp/plugins/dynamix/default.cfg +=================================================================== +--- /usr/local/emhttp/plugins/dynamix/default.cfg original ++++ /usr/local/emhttp/plugins/dynamix/default.cfg modified +@@ -43,10 +43,13 @@ + month="1" + day="0" + cron="" + write="NOCORRECT" + [notify] ++expand="true" ++duration="5000" ++max="3" + display="0" + life="5" + date="d-m-Y" + time="H:i" + position="top-right" diff --git a/api/src/unraid-api/unraid-file-modifier/modifications/patches/default-gray-css-modification.patch b/api/src/unraid-api/unraid-file-modifier/modifications/patches/default-gray-css-modification.patch new file mode 100644 index 0000000000..29fe45036e --- /dev/null +++ b/api/src/unraid-api/unraid-file-modifier/modifications/patches/default-gray-css-modification.patch @@ -0,0 +1,37 @@ +Index: /usr/local/emhttp/plugins/dynamix/styles/default-gray.css +=================================================================== +--- /usr/local/emhttp/plugins/dynamix/styles/default-gray.css original ++++ /usr/local/emhttp/plugins/dynamix/styles/default-gray.css modified +@@ -1,7 +1,9 @@ + html{font-family:clear-sans,sans-serif;font-size:62.5%;height:100%} + body{font-size:1.3rem;color:#606e7f;background-color:#1b1d1b;padding:0;margin:0;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale} ++@layer default { ++@scope (:root) to (.unapi) { + img{border:none;text-decoration:none;vertical-align:middle} + p{text-align:left} + p.centered{text-align:left} + p:empty{display:none} + a:hover{text-decoration:underline} +@@ -45,11 +47,11 @@ + select.auto{min-width:auto} + select.slot{min-width:44rem;max-width:44rem} + input.narrow{width:174px} + input.trim{width:74px;min-width:74px} + textarea{resize:none} +-#header{position:fixed;top:0;left:0;width:100%;height:90px;z-index:100;margin:0;background-color:#121510;background-size:100% 90px;background-repeat:no-repeat;border-bottom:1px solid #42453e} ++#header{position:fixed;top:0;left:0;width:100%;height:90px;z-index:100;margin:0;background-color:#f2f2f2;background-size:100% 90px;background-repeat:no-repeat;border-bottom:1px solid #42453e} + #header .logo{float:left;margin-left:75px;color:#e22828;text-align:center} + #header .logo svg{width:160px;display:block;margin:25px 0 8px 0} + #header .block{margin:0;float:right;text-align:right;background-color:rgba(18,21,16,0.2);padding:10px 12px} + #header .text-left{float:left;text-align:right;padding-right:5px;border-right:solid medium #f15a2c} + #header .text-right{float:right;text-align:left;padding-left:5px} +@@ -270,5 +272,8 @@ + .bannerInfo::before {content:"\f05a";font-family:fontAwesome;color:#e68a00} + ::-webkit-scrollbar{width:8px;height:8px;background:transparent} + ::-webkit-scrollbar-thumb{background:gray;border-radius:10px} + ::-webkit-scrollbar-corner{background:gray;border-radius:10px} + ::-webkit-scrollbar-thumb:hover{background:lightgray} ++ ++} ++} +\ No newline at end of file diff --git a/api/src/unraid-api/unraid-file-modifier/modifications/patches/default-page-layout.patch b/api/src/unraid-api/unraid-file-modifier/modifications/patches/default-page-layout.patch index 3fb7d7106e..0f00791ce1 100644 --- a/api/src/unraid-api/unraid-file-modifier/modifications/patches/default-page-layout.patch +++ b/api/src/unraid-api/unraid-file-modifier/modifications/patches/default-page-layout.patch @@ -117,12 +117,3 @@ Index: /usr/local/emhttp/plugins/dynamix/include/DefaultPageLayout.php }); -@@ -1363,7 +1365,8 @@ - } - } - } - - -+ - - diff --git a/api/src/unraid-api/unraid-file-modifier/modifications/patches/default-white-css-modification.patch b/api/src/unraid-api/unraid-file-modifier/modifications/patches/default-white-css-modification.patch new file mode 100644 index 0000000000..7dbaebe95d --- /dev/null +++ b/api/src/unraid-api/unraid-file-modifier/modifications/patches/default-white-css-modification.patch @@ -0,0 +1,24 @@ +Index: /usr/local/emhttp/plugins/dynamix/styles/default-white.css +=================================================================== +--- /usr/local/emhttp/plugins/dynamix/styles/default-white.css original ++++ /usr/local/emhttp/plugins/dynamix/styles/default-white.css modified +@@ -1,7 +1,9 @@ + html{font-family:clear-sans,sans-serif;font-size:62.5%;height:100%} + body{font-size:1.3rem;color:#1c1b1b;background-color:#f2f2f2;padding:0;margin:0;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale} ++@layer default { ++@scope (:root) to (.unapi) { + img{border:none;text-decoration:none;vertical-align:middle} + p{text-align:justify} + p.centered{text-align:left} + p:empty{display:none} + a:hover{text-decoration:underline} +@@ -258,5 +260,8 @@ + .bannerInfo::before {content:"\f05a";font-family:fontAwesome;color:#e68a00} + ::-webkit-scrollbar{width:8px;height:8px;background:transparent} + ::-webkit-scrollbar-thumb{background:lightgray;border-radius:10px} + ::-webkit-scrollbar-corner{background:lightgray;border-radius:10px} + ::-webkit-scrollbar-thumb:hover{background:gray} ++ ++} ++} +\ No newline at end of file diff --git a/api/src/unraid-api/unraid-file-modifier/modifications/patches/docker-containers-page.patch b/api/src/unraid-api/unraid-file-modifier/modifications/patches/docker-containers-page.patch new file mode 100644 index 0000000000..493b69e62e --- /dev/null +++ b/api/src/unraid-api/unraid-file-modifier/modifications/patches/docker-containers-page.patch @@ -0,0 +1,206 @@ +Index: /usr/local/emhttp/plugins/dynamix.docker.manager/DockerContainers.page +=================================================================== +--- /usr/local/emhttp/plugins/dynamix.docker.manager/DockerContainers.page original ++++ /usr/local/emhttp/plugins/dynamix.docker.manager/DockerContainers.page modified +@@ -1,196 +1,11 @@ + Menu="Docker:1" + Title="Docker Containers" + Tag="cubes" + Cond="is_file('/var/run/dockerd.pid')" + Markdown="false" +-Nchan="docker_load:stop" ++Nchan="docker_load" ++Tabs="false" + --- +- +- "._('Please wait')."... "._('starting up containers'); +-$cpus = cpu_list(); +-?> +- +- +- +- +- +-
_(Application)__(Version)__(Network)__(Container IP)__(Container Port)__(LAN IP:Port)__(Volume Mappings)_ (_(App to Host)_)_(CPU & Memory load)__(Autostart)__(Uptime)_
+- +- +- +- +- +- +- +- +- +- +- +- +- ++
++ ++
diff --git a/api/src/unraid-api/unraid-file-modifier/modifications/patches/font-awesome-css-modification.patch b/api/src/unraid-api/unraid-file-modifier/modifications/patches/font-awesome-css-modification.patch new file mode 100644 index 0000000000..d60a3751c1 --- /dev/null +++ b/api/src/unraid-api/unraid-file-modifier/modifications/patches/font-awesome-css-modification.patch @@ -0,0 +1,17 @@ +Index: /usr/local/emhttp/webGui/styles/font-awesome.css +=================================================================== +--- /usr/local/emhttp/webGui/styles/font-awesome.css original ++++ /usr/local/emhttp/webGui/styles/font-awesome.css modified +@@ -1,5 +1,11 @@ + /* Font Awesome 4.7.0 by @davegandy - http://fontawesome.io - @fontawesome + * License - http://fontawesome.io/license (Font: SIL OFL 1.1, CSS: MIT License) + */ ++@layer default { ++ @scope (:root) to (.unapi) { ++ + @font-face{font-family:'FontAwesome';font-weight:normal;font-style:normal;src:url('/webGui/styles/font-awesome.woff?v=220508') format('woff')} + .fa{display:inline-block;font:normal normal normal 14px/1 FontAwesome;font-size:inherit;text-rendering:auto;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.fa-lg{font-size:1.33333333em;line-height:.75em;vertical-align:-15%}.fa-2x{font-size:2em}.fa-3x{font-size:3em}.fa-4x{font-size:4em}.fa-5x{font-size:5em}.fa-fw{width:1.28571429em;text-align:center}.fa-ul{padding-left:0;margin-left:2.14285714em;list-style-type:none}.fa-ul>li{position:relative}.fa-li{position:absolute;left:-2.14285714em;width:2.14285714em;top:.14285714em;text-align:center}.fa-li.fa-lg{left:-1.85714286em}.fa-border{padding:.2em .25em .15em;border:solid .08em #eee;border-radius:.1em}.fa-pull-left{float:left}.fa-pull-right{float:right}.fa.fa-pull-left{margin-right:.3em}.fa.fa-pull-right{margin-left:.3em}.pull-right{float:right}.pull-left{float:left}.fa.pull-left{margin-right:.3em}.fa.pull-right{margin-left:.3em}.fa-spin{-webkit-animation:fa-spin 2s infinite linear;animation:fa-spin 2s infinite linear}.fa-pulse{-webkit-animation:fa-spin 1s infinite steps(8);animation:fa-spin 1s infinite steps(8)}@-webkit-keyframes fa-spin{0%{-webkit-transform:rotate(0deg);transform:rotate(0deg)}100%{-webkit-transform:rotate(359deg);transform:rotate(359deg)}}@keyframes fa-spin{0%{-webkit-transform:rotate(0deg);transform:rotate(0deg)}100%{-webkit-transform:rotate(359deg);transform:rotate(359deg)}}.fa-rotate-90{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=1)";-webkit-transform:rotate(90deg);-ms-transform:rotate(90deg);transform:rotate(90deg)}.fa-rotate-180{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=2)";-webkit-transform:rotate(180deg);-ms-transform:rotate(180deg);transform:rotate(180deg)}.fa-rotate-270{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=3)";-webkit-transform:rotate(270deg);-ms-transform:rotate(270deg);transform:rotate(270deg)}.fa-flip-horizontal{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=0, mirror=1)";-webkit-transform:scale(-1, 1);-ms-transform:scale(-1, 1);transform:scale(-1, 1)}.fa-flip-vertical{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=2, mirror=1)";-webkit-transform:scale(1, -1);-ms-transform:scale(1, -1);transform:scale(1, -1)}:root .fa-rotate-90,:root .fa-rotate-180,:root .fa-rotate-270,:root .fa-flip-horizontal,:root .fa-flip-vertical{filter:none}.fa-stack{position:relative;display:inline-block;width:2em;height:2em;line-height:2em;vertical-align:middle}.fa-stack-1x,.fa-stack-2x{position:absolute;left:0;width:100%;text-align:center}.fa-stack-1x{line-height:inherit}.fa-stack-2x{font-size:2em}.fa-inverse{color:#fff}.fa-glass:before{content:"\f000"}.fa-music:before{content:"\f001"}.fa-search:before{content:"\f002"}.fa-envelope-o:before{content:"\f003"}.fa-heart:before{content:"\f004"}.fa-star:before{content:"\f005"}.fa-star-o:before{content:"\f006"}.fa-user:before{content:"\f007"}.fa-film:before{content:"\f008"}.fa-th-large:before{content:"\f009"}.fa-th:before{content:"\f00a"}.fa-th-list:before{content:"\f00b"}.fa-check:before{content:"\f00c"}.fa-remove:before,.fa-close:before,.fa-times:before{content:"\f00d"}.fa-search-plus:before{content:"\f00e"}.fa-search-minus:before{content:"\f010"}.fa-power-off:before{content:"\f011"}.fa-signal:before{content:"\f012"}.fa-gear:before,.fa-cog:before{content:"\f013"}.fa-trash-o:before{content:"\f014"}.fa-home:before{content:"\f015"}.fa-file-o:before{content:"\f016"}.fa-clock-o:before{content:"\f017"}.fa-road:before{content:"\f018"}.fa-download:before{content:"\f019"}.fa-arrow-circle-o-down:before{content:"\f01a"}.fa-arrow-circle-o-up:before{content:"\f01b"}.fa-inbox:before{content:"\f01c"}.fa-play-circle-o:before{content:"\f01d"}.fa-rotate-right:before,.fa-repeat:before{content:"\f01e"}.fa-refresh:before{content:"\f021"}.fa-list-alt:before{content:"\f022"}.fa-lock:before{content:"\f023"}.fa-flag:before{content:"\f024"}.fa-headphones:before{content:"\f025"}.fa-volume-off:before{content:"\f026"}.fa-volume-down:before{content:"\f027"}.fa-volume-up:before{content:"\f028"}.fa-qrcode:before{content:"\f029"}.fa-barcode:before{content:"\f02a"}.fa-tag:before{content:"\f02b"}.fa-tags:before{content:"\f02c"}.fa-book:before{content:"\f02d"}.fa-bookmark:before{content:"\f02e"}.fa-print:before{content:"\f02f"}.fa-camera:before{content:"\f030"}.fa-font:before{content:"\f031"}.fa-bold:before{content:"\f032"}.fa-italic:before{content:"\f033"}.fa-text-height:before{content:"\f034"}.fa-text-width:before{content:"\f035"}.fa-align-left:before{content:"\f036"}.fa-align-center:before{content:"\f037"}.fa-align-right:before{content:"\f038"}.fa-align-justify:before{content:"\f039"}.fa-list:before{content:"\f03a"}.fa-dedent:before,.fa-outdent:before{content:"\f03b"}.fa-indent:before{content:"\f03c"}.fa-video-camera:before{content:"\f03d"}.fa-photo:before,.fa-image:before,.fa-picture-o:before{content:"\f03e"}.fa-pencil:before{content:"\f040"}.fa-map-marker:before{content:"\f041"}.fa-adjust:before{content:"\f042"}.fa-tint:before{content:"\f043"}.fa-edit:before,.fa-pencil-square-o:before{content:"\f044"}.fa-share-square-o:before{content:"\f045"}.fa-check-square-o:before{content:"\f046"}.fa-arrows:before{content:"\f047"}.fa-step-backward:before{content:"\f048"}.fa-fast-backward:before{content:"\f049"}.fa-backward:before{content:"\f04a"}.fa-play:before{content:"\f04b"}.fa-pause:before{content:"\f04c"}.fa-stop:before{content:"\f04d"}.fa-forward:before{content:"\f04e"}.fa-fast-forward:before{content:"\f050"}.fa-step-forward:before{content:"\f051"}.fa-eject:before{content:"\f052"}.fa-chevron-left:before{content:"\f053"}.fa-chevron-right:before{content:"\f054"}.fa-plus-circle:before{content:"\f055"}.fa-minus-circle:before{content:"\f056"}.fa-times-circle:before{content:"\f057"}.fa-check-circle:before{content:"\f058"}.fa-question-circle:before{content:"\f059"}.fa-info-circle:before{content:"\f05a"}.fa-crosshairs:before{content:"\f05b"}.fa-times-circle-o:before{content:"\f05c"}.fa-check-circle-o:before{content:"\f05d"}.fa-ban:before{content:"\f05e"}.fa-arrow-left:before{content:"\f060"}.fa-arrow-right:before{content:"\f061"}.fa-arrow-up:before{content:"\f062"}.fa-arrow-down:before{content:"\f063"}.fa-mail-forward:before,.fa-share:before{content:"\f064"}.fa-expand:before{content:"\f065"}.fa-compress:before{content:"\f066"}.fa-plus:before{content:"\f067"}.fa-minus:before{content:"\f068"}.fa-asterisk:before{content:"\f069"}.fa-exclamation-circle:before{content:"\f06a"}.fa-gift:before{content:"\f06b"}.fa-leaf:before{content:"\f06c"}.fa-fire:before{content:"\f06d"}.fa-eye:before{content:"\f06e"}.fa-eye-slash:before{content:"\f070"}.fa-warning:before,.fa-exclamation-triangle:before{content:"\f071"}.fa-plane:before{content:"\f072"}.fa-calendar:before{content:"\f073"}.fa-random:before{content:"\f074"}.fa-comment:before{content:"\f075"}.fa-magnet:before{content:"\f076"}.fa-chevron-up:before{content:"\f077"}.fa-chevron-down:before{content:"\f078"}.fa-retweet:before{content:"\f079"}.fa-shopping-cart:before{content:"\f07a"}.fa-folder:before{content:"\f07b"}.fa-folder-open:before{content:"\f07c"}.fa-arrows-v:before{content:"\f07d"}.fa-arrows-h:before{content:"\f07e"}.fa-bar-chart-o:before,.fa-bar-chart:before{content:"\f080"}.fa-twitter-square:before{content:"\f081"}.fa-facebook-square:before{content:"\f082"}.fa-camera-retro:before{content:"\f083"}.fa-key:before{content:"\f084"}.fa-gears:before,.fa-cogs:before{content:"\f085"}.fa-comments:before{content:"\f086"}.fa-thumbs-o-up:before{content:"\f087"}.fa-thumbs-o-down:before{content:"\f088"}.fa-star-half:before{content:"\f089"}.fa-heart-o:before{content:"\f08a"}.fa-sign-out:before{content:"\f08b"}.fa-linkedin-square:before{content:"\f08c"}.fa-thumb-tack:before{content:"\f08d"}.fa-external-link:before{content:"\f08e"}.fa-sign-in:before{content:"\f090"}.fa-trophy:before{content:"\f091"}.fa-github-square:before{content:"\f092"}.fa-upload:before{content:"\f093"}.fa-lemon-o:before{content:"\f094"}.fa-phone:before{content:"\f095"}.fa-square-o:before{content:"\f096"}.fa-bookmark-o:before{content:"\f097"}.fa-phone-square:before{content:"\f098"}.fa-twitter:before{content:"\f099"}.fa-facebook-f:before,.fa-facebook:before{content:"\f09a"}.fa-github:before{content:"\f09b"}.fa-unlock:before{content:"\f09c"}.fa-credit-card:before{content:"\f09d"}.fa-feed:before,.fa-rss:before{content:"\f09e"}.fa-hdd-o:before{content:"\f0a0"}.fa-bullhorn:before{content:"\f0a1"}.fa-bell:before{content:"\f0f3"}.fa-certificate:before{content:"\f0a3"}.fa-hand-o-right:before{content:"\f0a4"}.fa-hand-o-left:before{content:"\f0a5"}.fa-hand-o-up:before{content:"\f0a6"}.fa-hand-o-down:before{content:"\f0a7"}.fa-arrow-circle-left:before{content:"\f0a8"}.fa-arrow-circle-right:before{content:"\f0a9"}.fa-arrow-circle-up:before{content:"\f0aa"}.fa-arrow-circle-down:before{content:"\f0ab"}.fa-globe:before{content:"\f0ac"}.fa-wrench:before{content:"\f0ad"}.fa-tasks:before{content:"\f0ae"}.fa-filter:before{content:"\f0b0"}.fa-briefcase:before{content:"\f0b1"}.fa-arrows-alt:before{content:"\f0b2"}.fa-group:before,.fa-users:before{content:"\f0c0"}.fa-chain:before,.fa-link:before{content:"\f0c1"}.fa-cloud:before{content:"\f0c2"}.fa-flask:before{content:"\f0c3"}.fa-cut:before,.fa-scissors:before{content:"\f0c4"}.fa-copy:before,.fa-files-o:before{content:"\f0c5"}.fa-paperclip:before{content:"\f0c6"}.fa-save:before,.fa-floppy-o:before{content:"\f0c7"}.fa-square:before{content:"\f0c8"}.fa-navicon:before,.fa-reorder:before,.fa-bars:before{content:"\f0c9"}.fa-list-ul:before{content:"\f0ca"}.fa-list-ol:before{content:"\f0cb"}.fa-strikethrough:before{content:"\f0cc"}.fa-underline:before{content:"\f0cd"}.fa-table:before{content:"\f0ce"}.fa-magic:before{content:"\f0d0"}.fa-truck:before{content:"\f0d1"}.fa-pinterest:before{content:"\f0d2"}.fa-pinterest-square:before{content:"\f0d3"}.fa-google-plus-square:before{content:"\f0d4"}.fa-google-plus:before{content:"\f0d5"}.fa-money:before{content:"\f0d6"}.fa-caret-down:before{content:"\f0d7"}.fa-caret-up:before{content:"\f0d8"}.fa-caret-left:before{content:"\f0d9"}.fa-caret-right:before{content:"\f0da"}.fa-columns:before{content:"\f0db"}.fa-unsorted:before,.fa-sort:before{content:"\f0dc"}.fa-sort-down:before,.fa-sort-desc:before{content:"\f0dd"}.fa-sort-up:before,.fa-sort-asc:before{content:"\f0de"}.fa-envelope:before{content:"\f0e0"}.fa-linkedin:before{content:"\f0e1"}.fa-rotate-left:before,.fa-undo:before{content:"\f0e2"}.fa-legal:before,.fa-gavel:before{content:"\f0e3"}.fa-dashboard:before,.fa-tachometer:before{content:"\f0e4"}.fa-comment-o:before{content:"\f0e5"}.fa-comments-o:before{content:"\f0e6"}.fa-flash:before,.fa-bolt:before{content:"\f0e7"}.fa-sitemap:before{content:"\f0e8"}.fa-umbrella:before{content:"\f0e9"}.fa-paste:before,.fa-clipboard:before{content:"\f0ea"}.fa-lightbulb-o:before{content:"\f0eb"}.fa-exchange:before{content:"\f0ec"}.fa-cloud-download:before{content:"\f0ed"}.fa-cloud-upload:before{content:"\f0ee"}.fa-user-md:before{content:"\f0f0"}.fa-stethoscope:before{content:"\f0f1"}.fa-suitcase:before{content:"\f0f2"}.fa-bell-o:before{content:"\f0a2"}.fa-coffee:before{content:"\f0f4"}.fa-cutlery:before{content:"\f0f5"}.fa-file-text-o:before{content:"\f0f6"}.fa-building-o:before{content:"\f0f7"}.fa-hospital-o:before{content:"\f0f8"}.fa-ambulance:before{content:"\f0f9"}.fa-medkit:before{content:"\f0fa"}.fa-fighter-jet:before{content:"\f0fb"}.fa-beer:before{content:"\f0fc"}.fa-h-square:before{content:"\f0fd"}.fa-plus-square:before{content:"\f0fe"}.fa-angle-double-left:before{content:"\f100"}.fa-angle-double-right:before{content:"\f101"}.fa-angle-double-up:before{content:"\f102"}.fa-angle-double-down:before{content:"\f103"}.fa-angle-left:before{content:"\f104"}.fa-angle-right:before{content:"\f105"}.fa-angle-up:before{content:"\f106"}.fa-angle-down:before{content:"\f107"}.fa-desktop:before{content:"\f108"}.fa-laptop:before{content:"\f109"}.fa-tablet:before{content:"\f10a"}.fa-mobile-phone:before,.fa-mobile:before{content:"\f10b"}.fa-circle-o:before{content:"\f10c"}.fa-quote-left:before{content:"\f10d"}.fa-quote-right:before{content:"\f10e"}.fa-spinner:before{content:"\f110"}.fa-circle:before{content:"\f111"}.fa-mail-reply:before,.fa-reply:before{content:"\f112"}.fa-github-alt:before{content:"\f113"}.fa-folder-o:before{content:"\f114"}.fa-folder-open-o:before{content:"\f115"}.fa-smile-o:before{content:"\f118"}.fa-frown-o:before{content:"\f119"}.fa-meh-o:before{content:"\f11a"}.fa-gamepad:before{content:"\f11b"}.fa-keyboard-o:before{content:"\f11c"}.fa-flag-o:before{content:"\f11d"}.fa-flag-checkered:before{content:"\f11e"}.fa-terminal:before{content:"\f120"}.fa-code:before{content:"\f121"}.fa-mail-reply-all:before,.fa-reply-all:before{content:"\f122"}.fa-star-half-empty:before,.fa-star-half-full:before,.fa-star-half-o:before{content:"\f123"}.fa-location-arrow:before{content:"\f124"}.fa-crop:before{content:"\f125"}.fa-code-fork:before{content:"\f126"}.fa-unlink:before,.fa-chain-broken:before{content:"\f127"}.fa-question:before{content:"\f128"}.fa-info:before{content:"\f129"}.fa-exclamation:before{content:"\f12a"}.fa-superscript:before{content:"\f12b"}.fa-subscript:before{content:"\f12c"}.fa-eraser:before{content:"\f12d"}.fa-puzzle-piece:before{content:"\f12e"}.fa-microphone:before{content:"\f130"}.fa-microphone-slash:before{content:"\f131"}.fa-shield:before{content:"\f132"}.fa-calendar-o:before{content:"\f133"}.fa-fire-extinguisher:before{content:"\f134"}.fa-rocket:before{content:"\f135"}.fa-maxcdn:before{content:"\f136"}.fa-chevron-circle-left:before{content:"\f137"}.fa-chevron-circle-right:before{content:"\f138"}.fa-chevron-circle-up:before{content:"\f139"}.fa-chevron-circle-down:before{content:"\f13a"}.fa-html5:before{content:"\f13b"}.fa-css3:before{content:"\f13c"}.fa-anchor:before{content:"\f13d"}.fa-unlock-alt:before{content:"\f13e"}.fa-bullseye:before{content:"\f140"}.fa-ellipsis-h:before{content:"\f141"}.fa-ellipsis-v:before{content:"\f142"}.fa-rss-square:before{content:"\f143"}.fa-play-circle:before{content:"\f144"}.fa-ticket:before{content:"\f145"}.fa-minus-square:before{content:"\f146"}.fa-minus-square-o:before{content:"\f147"}.fa-level-up:before{content:"\f148"}.fa-level-down:before{content:"\f149"}.fa-check-square:before{content:"\f14a"}.fa-pencil-square:before{content:"\f14b"}.fa-external-link-square:before{content:"\f14c"}.fa-share-square:before{content:"\f14d"}.fa-compass:before{content:"\f14e"}.fa-toggle-down:before,.fa-caret-square-o-down:before{content:"\f150"}.fa-toggle-up:before,.fa-caret-square-o-up:before{content:"\f151"}.fa-toggle-right:before,.fa-caret-square-o-right:before{content:"\f152"}.fa-euro:before,.fa-eur:before{content:"\f153"}.fa-gbp:before{content:"\f154"}.fa-dollar:before,.fa-usd:before{content:"\f155"}.fa-rupee:before,.fa-inr:before{content:"\f156"}.fa-cny:before,.fa-rmb:before,.fa-yen:before,.fa-jpy:before{content:"\f157"}.fa-ruble:before,.fa-rouble:before,.fa-rub:before{content:"\f158"}.fa-won:before,.fa-krw:before{content:"\f159"}.fa-bitcoin:before,.fa-btc:before{content:"\f15a"}.fa-file:before{content:"\f15b"}.fa-file-text:before{content:"\f15c"}.fa-sort-alpha-asc:before{content:"\f15d"}.fa-sort-alpha-desc:before{content:"\f15e"}.fa-sort-amount-asc:before{content:"\f160"}.fa-sort-amount-desc:before{content:"\f161"}.fa-sort-numeric-asc:before{content:"\f162"}.fa-sort-numeric-desc:before{content:"\f163"}.fa-thumbs-up:before{content:"\f164"}.fa-thumbs-down:before{content:"\f165"}.fa-youtube-square:before{content:"\f166"}.fa-youtube:before{content:"\f167"}.fa-xing:before{content:"\f168"}.fa-xing-square:before{content:"\f169"}.fa-youtube-play:before{content:"\f16a"}.fa-dropbox:before{content:"\f16b"}.fa-stack-overflow:before{content:"\f16c"}.fa-instagram:before{content:"\f16d"}.fa-flickr:before{content:"\f16e"}.fa-adn:before{content:"\f170"}.fa-bitbucket:before{content:"\f171"}.fa-bitbucket-square:before{content:"\f172"}.fa-tumblr:before{content:"\f173"}.fa-tumblr-square:before{content:"\f174"}.fa-long-arrow-down:before{content:"\f175"}.fa-long-arrow-up:before{content:"\f176"}.fa-long-arrow-left:before{content:"\f177"}.fa-long-arrow-right:before{content:"\f178"}.fa-apple:before{content:"\f179"}.fa-windows:before{content:"\f17a"}.fa-android:before{content:"\f17b"}.fa-linux:before{content:"\f17c"}.fa-dribbble:before{content:"\f17d"}.fa-skype:before{content:"\f17e"}.fa-foursquare:before{content:"\f180"}.fa-trello:before{content:"\f181"}.fa-female:before{content:"\f182"}.fa-male:before{content:"\f183"}.fa-gittip:before,.fa-gratipay:before{content:"\f184"}.fa-sun-o:before{content:"\f185"}.fa-moon-o:before{content:"\f186"}.fa-archive:before{content:"\f187"}.fa-bug:before{content:"\f188"}.fa-vk:before{content:"\f189"}.fa-weibo:before{content:"\f18a"}.fa-renren:before{content:"\f18b"}.fa-pagelines:before{content:"\f18c"}.fa-stack-exchange:before{content:"\f18d"}.fa-arrow-circle-o-right:before{content:"\f18e"}.fa-arrow-circle-o-left:before{content:"\f190"}.fa-toggle-left:before,.fa-caret-square-o-left:before{content:"\f191"}.fa-dot-circle-o:before{content:"\f192"}.fa-wheelchair:before{content:"\f193"}.fa-vimeo-square:before{content:"\f194"}.fa-turkish-lira:before,.fa-try:before{content:"\f195"}.fa-plus-square-o:before{content:"\f196"}.fa-space-shuttle:before{content:"\f197"}.fa-slack:before{content:"\f198"}.fa-envelope-square:before{content:"\f199"}.fa-wordpress:before{content:"\f19a"}.fa-openid:before{content:"\f19b"}.fa-institution:before,.fa-bank:before,.fa-university:before{content:"\f19c"}.fa-mortar-board:before,.fa-graduation-cap:before{content:"\f19d"}.fa-yahoo:before{content:"\f19e"}.fa-google:before{content:"\f1a0"}.fa-reddit:before{content:"\f1a1"}.fa-reddit-square:before{content:"\f1a2"}.fa-stumbleupon-circle:before{content:"\f1a3"}.fa-stumbleupon:before{content:"\f1a4"}.fa-delicious:before{content:"\f1a5"}.fa-digg:before{content:"\f1a6"}.fa-pied-piper-pp:before{content:"\f1a7"}.fa-pied-piper-alt:before{content:"\f1a8"}.fa-drupal:before{content:"\f1a9"}.fa-joomla:before{content:"\f1aa"}.fa-language:before{content:"\f1ab"}.fa-fax:before{content:"\f1ac"}.fa-building:before{content:"\f1ad"}.fa-child:before{content:"\f1ae"}.fa-paw:before{content:"\f1b0"}.fa-spoon:before{content:"\f1b1"}.fa-cube:before{content:"\f1b2"}.fa-cubes:before{content:"\f1b3"}.fa-behance:before{content:"\f1b4"}.fa-behance-square:before{content:"\f1b5"}.fa-steam:before{content:"\f1b6"}.fa-steam-square:before{content:"\f1b7"}.fa-recycle:before{content:"\f1b8"}.fa-automobile:before,.fa-car:before{content:"\f1b9"}.fa-cab:before,.fa-taxi:before{content:"\f1ba"}.fa-tree:before{content:"\f1bb"}.fa-spotify:before{content:"\f1bc"}.fa-deviantart:before{content:"\f1bd"}.fa-soundcloud:before{content:"\f1be"}.fa-database:before{content:"\f1c0"}.fa-file-pdf-o:before{content:"\f1c1"}.fa-file-word-o:before{content:"\f1c2"}.fa-file-excel-o:before{content:"\f1c3"}.fa-file-powerpoint-o:before{content:"\f1c4"}.fa-file-photo-o:before,.fa-file-picture-o:before,.fa-file-image-o:before{content:"\f1c5"}.fa-file-zip-o:before,.fa-file-archive-o:before{content:"\f1c6"}.fa-file-sound-o:before,.fa-file-audio-o:before{content:"\f1c7"}.fa-file-movie-o:before,.fa-file-video-o:before{content:"\f1c8"}.fa-file-code-o:before{content:"\f1c9"}.fa-vine:before{content:"\f1ca"}.fa-codepen:before{content:"\f1cb"}.fa-jsfiddle:before{content:"\f1cc"}.fa-life-bouy:before,.fa-life-buoy:before,.fa-life-saver:before,.fa-support:before,.fa-life-ring:before{content:"\f1cd"}.fa-circle-o-notch:before{content:"\f1ce"}.fa-ra:before,.fa-resistance:before,.fa-rebel:before{content:"\f1d0"}.fa-ge:before,.fa-empire:before{content:"\f1d1"}.fa-git-square:before{content:"\f1d2"}.fa-git:before{content:"\f1d3"}.fa-y-combinator-square:before,.fa-yc-square:before,.fa-hacker-news:before{content:"\f1d4"}.fa-tencent-weibo:before{content:"\f1d5"}.fa-qq:before{content:"\f1d6"}.fa-wechat:before,.fa-weixin:before{content:"\f1d7"}.fa-send:before,.fa-paper-plane:before{content:"\f1d8"}.fa-send-o:before,.fa-paper-plane-o:before{content:"\f1d9"}.fa-history:before{content:"\f1da"}.fa-circle-thin:before{content:"\f1db"}.fa-header:before{content:"\f1dc"}.fa-paragraph:before{content:"\f1dd"}.fa-sliders:before{content:"\f1de"}.fa-share-alt:before{content:"\f1e0"}.fa-share-alt-square:before{content:"\f1e1"}.fa-bomb:before{content:"\f1e2"}.fa-soccer-ball-o:before,.fa-futbol-o:before{content:"\f1e3"}.fa-tty:before{content:"\f1e4"}.fa-binoculars:before{content:"\f1e5"}.fa-plug:before{content:"\f1e6"}.fa-slideshare:before{content:"\f1e7"}.fa-twitch:before{content:"\f1e8"}.fa-yelp:before{content:"\f1e9"}.fa-newspaper-o:before{content:"\f1ea"}.fa-wifi:before{content:"\f1eb"}.fa-calculator:before{content:"\f1ec"}.fa-paypal:before{content:"\f1ed"}.fa-google-wallet:before{content:"\f1ee"}.fa-cc-visa:before{content:"\f1f0"}.fa-cc-mastercard:before{content:"\f1f1"}.fa-cc-discover:before{content:"\f1f2"}.fa-cc-amex:before{content:"\f1f3"}.fa-cc-paypal:before{content:"\f1f4"}.fa-cc-stripe:before{content:"\f1f5"}.fa-bell-slash:before{content:"\f1f6"}.fa-bell-slash-o:before{content:"\f1f7"}.fa-trash:before{content:"\f1f8"}.fa-copyright:before{content:"\f1f9"}.fa-at:before{content:"\f1fa"}.fa-eyedropper:before{content:"\f1fb"}.fa-paint-brush:before{content:"\f1fc"}.fa-birthday-cake:before{content:"\f1fd"}.fa-area-chart:before{content:"\f1fe"}.fa-pie-chart:before{content:"\f200"}.fa-line-chart:before{content:"\f201"}.fa-lastfm:before{content:"\f202"}.fa-lastfm-square:before{content:"\f203"}.fa-toggle-off:before{content:"\f204"}.fa-toggle-on:before{content:"\f205"}.fa-bicycle:before{content:"\f206"}.fa-bus:before{content:"\f207"}.fa-ioxhost:before{content:"\f208"}.fa-angellist:before{content:"\f209"}.fa-cc:before{content:"\f20a"}.fa-shekel:before,.fa-sheqel:before,.fa-ils:before{content:"\f20b"}.fa-meanpath:before{content:"\f20c"}.fa-buysellads:before{content:"\f20d"}.fa-connectdevelop:before{content:"\f20e"}.fa-dashcube:before{content:"\f210"}.fa-forumbee:before{content:"\f211"}.fa-leanpub:before{content:"\f212"}.fa-sellsy:before{content:"\f213"}.fa-shirtsinbulk:before{content:"\f214"}.fa-simplybuilt:before{content:"\f215"}.fa-skyatlas:before{content:"\f216"}.fa-cart-plus:before{content:"\f217"}.fa-cart-arrow-down:before{content:"\f218"}.fa-diamond:before{content:"\f219"}.fa-ship:before{content:"\f21a"}.fa-user-secret:before{content:"\f21b"}.fa-motorcycle:before{content:"\f21c"}.fa-street-view:before{content:"\f21d"}.fa-heartbeat:before{content:"\f21e"}.fa-venus:before{content:"\f221"}.fa-mars:before{content:"\f222"}.fa-mercury:before{content:"\f223"}.fa-intersex:before,.fa-transgender:before{content:"\f224"}.fa-transgender-alt:before{content:"\f225"}.fa-venus-double:before{content:"\f226"}.fa-mars-double:before{content:"\f227"}.fa-venus-mars:before{content:"\f228"}.fa-mars-stroke:before{content:"\f229"}.fa-mars-stroke-v:before{content:"\f22a"}.fa-mars-stroke-h:before{content:"\f22b"}.fa-neuter:before{content:"\f22c"}.fa-genderless:before{content:"\f22d"}.fa-facebook-official:before{content:"\f230"}.fa-pinterest-p:before{content:"\f231"}.fa-whatsapp:before{content:"\f232"}.fa-server:before{content:"\f233"}.fa-user-plus:before{content:"\f234"}.fa-user-times:before{content:"\f235"}.fa-hotel:before,.fa-bed:before{content:"\f236"}.fa-viacoin:before{content:"\f237"}.fa-train:before{content:"\f238"}.fa-subway:before{content:"\f239"}.fa-medium:before{content:"\f23a"}.fa-yc:before,.fa-y-combinator:before{content:"\f23b"}.fa-optin-monster:before{content:"\f23c"}.fa-opencart:before{content:"\f23d"}.fa-expeditedssl:before{content:"\f23e"}.fa-battery-4:before,.fa-battery:before,.fa-battery-full:before{content:"\f240"}.fa-battery-3:before,.fa-battery-three-quarters:before{content:"\f241"}.fa-battery-2:before,.fa-battery-half:before{content:"\f242"}.fa-battery-1:before,.fa-battery-quarter:before{content:"\f243"}.fa-battery-0:before,.fa-battery-empty:before{content:"\f244"}.fa-mouse-pointer:before{content:"\f245"}.fa-i-cursor:before{content:"\f246"}.fa-object-group:before{content:"\f247"}.fa-object-ungroup:before{content:"\f248"}.fa-sticky-note:before{content:"\f249"}.fa-sticky-note-o:before{content:"\f24a"}.fa-cc-jcb:before{content:"\f24b"}.fa-cc-diners-club:before{content:"\f24c"}.fa-clone:before{content:"\f24d"}.fa-balance-scale:before{content:"\f24e"}.fa-hourglass-o:before{content:"\f250"}.fa-hourglass-1:before,.fa-hourglass-start:before{content:"\f251"}.fa-hourglass-2:before,.fa-hourglass-half:before{content:"\f252"}.fa-hourglass-3:before,.fa-hourglass-end:before{content:"\f253"}.fa-hourglass:before{content:"\f254"}.fa-hand-grab-o:before,.fa-hand-rock-o:before{content:"\f255"}.fa-hand-stop-o:before,.fa-hand-paper-o:before{content:"\f256"}.fa-hand-scissors-o:before{content:"\f257"}.fa-hand-lizard-o:before{content:"\f258"}.fa-hand-spock-o:before{content:"\f259"}.fa-hand-pointer-o:before{content:"\f25a"}.fa-hand-peace-o:before{content:"\f25b"}.fa-trademark:before{content:"\f25c"}.fa-registered:before{content:"\f25d"}.fa-creative-commons:before{content:"\f25e"}.fa-gg:before{content:"\f260"}.fa-gg-circle:before{content:"\f261"}.fa-tripadvisor:before{content:"\f262"}.fa-odnoklassniki:before{content:"\f263"}.fa-odnoklassniki-square:before{content:"\f264"}.fa-get-pocket:before{content:"\f265"}.fa-wikipedia-w:before{content:"\f266"}.fa-safari:before{content:"\f267"}.fa-chrome:before{content:"\f268"}.fa-firefox:before{content:"\f269"}.fa-opera:before{content:"\f26a"}.fa-internet-explorer:before{content:"\f26b"}.fa-tv:before,.fa-television:before{content:"\f26c"}.fa-contao:before{content:"\f26d"}.fa-500px:before{content:"\f26e"}.fa-amazon:before{content:"\f270"}.fa-calendar-plus-o:before{content:"\f271"}.fa-calendar-minus-o:before{content:"\f272"}.fa-calendar-times-o:before{content:"\f273"}.fa-calendar-check-o:before{content:"\f274"}.fa-industry:before{content:"\f275"}.fa-map-pin:before{content:"\f276"}.fa-map-signs:before{content:"\f277"}.fa-map-o:before{content:"\f278"}.fa-map:before{content:"\f279"}.fa-commenting:before{content:"\f27a"}.fa-commenting-o:before{content:"\f27b"}.fa-houzz:before{content:"\f27c"}.fa-vimeo:before{content:"\f27d"}.fa-black-tie:before{content:"\f27e"}.fa-fonticons:before{content:"\f280"}.fa-reddit-alien:before{content:"\f281"}.fa-edge:before{content:"\f282"}.fa-credit-card-alt:before{content:"\f283"}.fa-codiepie:before{content:"\f284"}.fa-modx:before{content:"\f285"}.fa-fort-awesome:before{content:"\f286"}.fa-usb:before{content:"\f287"}.fa-product-hunt:before{content:"\f288"}.fa-mixcloud:before{content:"\f289"}.fa-scribd:before{content:"\f28a"}.fa-pause-circle:before{content:"\f28b"}.fa-pause-circle-o:before{content:"\f28c"}.fa-stop-circle:before{content:"\f28d"}.fa-stop-circle-o:before{content:"\f28e"}.fa-shopping-bag:before{content:"\f290"}.fa-shopping-basket:before{content:"\f291"}.fa-hashtag:before{content:"\f292"}.fa-bluetooth:before{content:"\f293"}.fa-bluetooth-b:before{content:"\f294"}.fa-percent:before{content:"\f295"}.fa-gitlab:before{content:"\f296"}.fa-wpbeginner:before{content:"\f297"}.fa-wpforms:before{content:"\f298"}.fa-envira:before{content:"\f299"}.fa-universal-access:before{content:"\f29a"}.fa-wheelchair-alt:before{content:"\f29b"}.fa-question-circle-o:before{content:"\f29c"}.fa-blind:before{content:"\f29d"}.fa-audio-description:before{content:"\f29e"}.fa-volume-control-phone:before{content:"\f2a0"}.fa-braille:before{content:"\f2a1"}.fa-assistive-listening-systems:before{content:"\f2a2"}.fa-asl-interpreting:before,.fa-american-sign-language-interpreting:before{content:"\f2a3"}.fa-deafness:before,.fa-hard-of-hearing:before,.fa-deaf:before{content:"\f2a4"}.fa-glide:before{content:"\f2a5"}.fa-glide-g:before{content:"\f2a6"}.fa-signing:before,.fa-sign-language:before{content:"\f2a7"}.fa-low-vision:before{content:"\f2a8"}.fa-viadeo:before{content:"\f2a9"}.fa-viadeo-square:before{content:"\f2aa"}.fa-snapchat:before{content:"\f2ab"}.fa-snapchat-ghost:before{content:"\f2ac"}.fa-snapchat-square:before{content:"\f2ad"}.fa-pied-piper:before{content:"\f2ae"}.fa-first-order:before{content:"\f2b0"}.fa-yoast:before{content:"\f2b1"}.fa-themeisle:before{content:"\f2b2"}.fa-google-plus-circle:before,.fa-google-plus-official:before{content:"\f2b3"}.fa-fa:before,.fa-font-awesome:before{content:"\f2b4"}.fa-handshake-o:before{content:"\f2b5"}.fa-envelope-open:before{content:"\f2b6"}.fa-envelope-open-o:before{content:"\f2b7"}.fa-linode:before{content:"\f2b8"}.fa-address-book:before{content:"\f2b9"}.fa-address-book-o:before{content:"\f2ba"}.fa-vcard:before,.fa-address-card:before{content:"\f2bb"}.fa-vcard-o:before,.fa-address-card-o:before{content:"\f2bc"}.fa-user-circle:before{content:"\f2bd"}.fa-user-circle-o:before{content:"\f2be"}.fa-user-o:before{content:"\f2c0"}.fa-id-badge:before{content:"\f2c1"}.fa-drivers-license:before,.fa-id-card:before{content:"\f2c2"}.fa-drivers-license-o:before,.fa-id-card-o:before{content:"\f2c3"}.fa-quora:before{content:"\f2c4"}.fa-free-code-camp:before{content:"\f2c5"}.fa-telegram:before{content:"\f2c6"}.fa-thermometer-4:before,.fa-thermometer:before,.fa-thermometer-full:before{content:"\f2c7"}.fa-thermometer-3:before,.fa-thermometer-three-quarters:before{content:"\f2c8"}.fa-thermometer-2:before,.fa-thermometer-half:before{content:"\f2c9"}.fa-thermometer-1:before,.fa-thermometer-quarter:before{content:"\f2ca"}.fa-thermometer-0:before,.fa-thermometer-empty:before{content:"\f2cb"}.fa-shower:before{content:"\f2cc"}.fa-bathtub:before,.fa-s15:before,.fa-bath:before{content:"\f2cd"}.fa-podcast:before{content:"\f2ce"}.fa-window-maximize:before{content:"\f2d0"}.fa-window-minimize:before{content:"\f2d1"}.fa-window-restore:before{content:"\f2d2"}.fa-times-rectangle:before,.fa-window-close:before{content:"\f2d3"}.fa-times-rectangle-o:before,.fa-window-close-o:before{content:"\f2d4"}.fa-bandcamp:before{content:"\f2d5"}.fa-grav:before{content:"\f2d6"}.fa-etsy:before{content:"\f2d7"}.fa-imdb:before{content:"\f2d8"}.fa-ravelry:before{content:"\f2d9"}.fa-eercast:before{content:"\f2da"}.fa-microchip:before{content:"\f2db"}.fa-snowflake-o:before{content:"\f2dc"}.fa-superpowers:before{content:"\f2dd"}.fa-wpexplorer:before{content:"\f2de"}.fa-meetup:before{content:"\f2e0"}.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0, 0, 0, 0);border:0}.sr-only-focusable:active,.sr-only-focusable:focus{position:static;width:auto;height:auto;margin:0;overflow:visible;clip:auto} ++ ++ } ++} +\ No newline at end of file diff --git a/api/src/unraid-api/unraid-file-modifier/modifications/patches/helptext.patch b/api/src/unraid-api/unraid-file-modifier/modifications/patches/helptext.patch new file mode 100644 index 0000000000..231732b2ef --- /dev/null +++ b/api/src/unraid-api/unraid-file-modifier/modifications/patches/helptext.patch @@ -0,0 +1,34 @@ +Index: /usr/local/emhttp/languages/en_US/helptext.txt +=================================================================== +--- /usr/local/emhttp/languages/en_US/helptext.txt original ++++ /usr/local/emhttp/languages/en_US/helptext.txt modified +@@ -2131,15 +2131,27 @@ + + In *Summarized* view notifications will be counted only and the number of unread notifications is shown in the menu header per category.
+ Click on the counters to either acknowledge or view the unread notifications. + :end + ++ + :notifications_display_position_help: +-Choose the position of where notifications appear on screen in *Detailed* view. Multiple notifications are stacked, bottom-to-top or +-top-to-bottom depending on the selected placement. ++Choose the position of where notification popups appear on screen. + :end + ++:notifications_stack_help: ++When enabled, multiple notifications are stacked to conserve screen space. ++:end ++ ++:notifications_duration_help: ++Time in milliseconds before a notification automatically closes. ++:end ++ ++:notifications_max_help: ++Maximum number of notifications shown on screen at once. ++:end ++ + :notifications_auto_close_help: + Number of seconds before notifications are automatically closed in *Detailed* view.
+ A value of 0 disables automatic closure. + :end + diff --git a/api/src/unraid-api/unraid-file-modifier/modifications/patches/notifications-page.patch b/api/src/unraid-api/unraid-file-modifier/modifications/patches/notifications-page.patch index eeb77d64ab..9fc9760a5b 100644 --- a/api/src/unraid-api/unraid-file-modifier/modifications/patches/notifications-page.patch +++ b/api/src/unraid-api/unraid-file-modifier/modifications/patches/notifications-page.patch @@ -2,31 +2,81 @@ Index: /usr/local/emhttp/plugins/dynamix/Notifications.page =================================================================== --- /usr/local/emhttp/plugins/dynamix/Notifications.page original +++ /usr/local/emhttp/plugins/dynamix/Notifications.page modified -@@ -133,27 +133,11 @@ - _(Auto-close)_ (_(seconds)_): - : _(a value of zero means no automatic closure)_ +@@ -109,34 +109,50 @@ + + + + + +-_(Notifications display)_: +-: ++ ++ + - :notifications_auto_close_help: +-:notifications_display_help: ++:notifications_store_flash_help: --_(Date format)_: --: + _(Display position)_: + : --:notifications_date_format_help: -- --_(Time format)_: --: -- --:notifications_time_format_help: -- - _(Store notifications to flash)_: - : ++ ++ ++ + +-_(Auto-close)_ (_(seconds)_): +-: _(a value of zero means no automatic closure)_ ++:notifications_stack_help: + +-:notifications_auto_close_help: ++_(Notification popup duration)_: ++: + ++:notifications_duration_help: ++ ++_(Max notification popups)_: ++: ++ ++:notifications_max_help: ++ + _(Date format)_: + : + + :notifications_time_format_help: + +-_(Store notifications to flash)_: +-: + +-:notifications_store_flash_help: + + _(System notifications)_: + :