This commit is contained in:
Ajit Mehrotra
2025-12-31 01:04:07 +00:00
committed by GitHub
111 changed files with 13792 additions and 1050 deletions

3
.gitignore vendored
View File

@@ -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/

View File

@@ -86,4 +86,4 @@ unraid-sso-button.unapi {
--text-7xl: 4.5rem;
--text-8xl: 6rem;
--text-9xl: 8rem;
}
}

View File

@@ -2,4 +2,4 @@
@import './css-variables.css';
@import './unraid-theme.css';
@import './theme-variants.css';
@import './base-utilities.css';
@import './base-utilities.css';

View File

@@ -65,4 +65,4 @@
/* Dark Mode Overrides */
.dark {
--color-border: #383735;
}
}

View File

@@ -3,7 +3,5 @@
"extraOrigins": [],
"sandbox": true,
"ssoSubIds": [],
"plugins": [
"unraid-api-plugin-connect"
]
}
"plugins": ["unraid-api-plugin-connect"]
}

View File

@@ -1395,6 +1395,13 @@ type NotificationCounts {
total: Int!
}
type NotificationSettings {
position: String!
expand: Boolean!
duration: Int!
max: Int!
}
type NotificationOverview {
unread: NotificationCounts!
archive: NotificationCounts!
@@ -1438,6 +1445,7 @@ type Notifications implements Node {
Deduplicated list of unread warning and alert notifications, sorted latest first.
"""
warningsAndAlerts: [Notification!]!
settings: NotificationSettings!
}
input NotificationFilter {

View File

@@ -93,6 +93,9 @@ interface Notify {
system: string;
version: string;
docker_update: string;
expand?: string | boolean;
duration?: string | number;
max?: string | number;
}
interface Ssmtp {

View File

@@ -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<Scalars['Float']['output']>;
/** Temperature per package (°C) */
temp: Array<Scalars['Float']['output']>;
/** 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<Scalars['PrefixedID']['input']>;
};
@@ -1065,6 +1089,7 @@ export type InfoCpu = Node & {
manufacturer?: Maybe<Scalars['String']['output']>;
/** CPU model */
model?: Maybe<Scalars['String']['output']>;
packages: CpuPackages;
/** Number of physical processors */
processors?: Maybe<Scalars['Int']['output']>;
/** CPU revision */
@@ -1081,6 +1106,8 @@ export type InfoCpu = Node & {
stepping?: Maybe<Scalars['Int']['output']>;
/** Number of CPU threads */
threads?: Maybe<Scalars['Int']['output']>;
/** Per-package array of core/thread pairs, e.g. [[[0,1],[2,3]], [[4,5],[6,7]]] */
topology: Array<Array<Array<Scalars['Int']['output']>>>;
/** CPU vendor */
vendor?: Maybe<Scalars['String']['output']>;
/** 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;
@@ -1659,6 +1687,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 +1706,7 @@ export type Notifications = Node & {
list: Array<Notification>;
/** 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<Notification>;
};
@@ -2269,6 +2306,7 @@ export type Subscription = {
parityHistorySubscription: ParityCheck;
serversSubscription: Server;
systemMetricsCpu: CpuUtilization;
systemMetricsCpuTelemetry: CpuPackages;
systemMetricsMemory: MemoryUtilization;
upsUpdates: UpsDevice;
};

View File

@@ -0,0 +1 @@
export const DOCKER_SERVICE_TOKEN = Symbol('DOCKER_SERVICE');

View File

@@ -8,10 +8,13 @@ import {
registerEnumType,
} from '@nestjs/graphql';
import { type Layout } from '@jsonforms/core';
import { Node } from '@unraid/shared/graphql.model.js';
import { PrefixedID } from '@unraid/shared/prefixed-id-scalar.js';
import { GraphQLBigInt, GraphQLJSON, GraphQLPort } from 'graphql-scalars';
import { DataSlice } from '@app/unraid-api/types/json-forms.js';
export enum ContainerPortType {
TCP = 'TCP',
UDP = 'UDP',

View File

@@ -5,6 +5,7 @@ import { type Cache } from 'cache-manager';
import Docker from 'dockerode';
import { execa } from 'execa';
import { AppError } from '@app/core/errors/app-error.js';
import { pubsub, PUBSUB_CHANNEL } from '@app/core/pubsub.js';
import { catchHandlers } from '@app/core/utils/misc/catch-handlers.js';
import { sleep } from '@app/core/utils/misc/sleep.js';

View File

@@ -3,6 +3,7 @@ import { Injectable, Logger } from '@nestjs/common';
import type { ContainerListOptions } from 'dockerode';
import { AppError } from '@app/core/errors/app-error.js';
import { DockerTemplateIconService } from '@app/unraid-api/graph/resolvers/docker/docker-template-icon.service.js';
import { DockerContainer } from '@app/unraid-api/graph/resolvers/docker/docker.model.js';
import { DockerService } from '@app/unraid-api/graph/resolvers/docker/docker.service.js';
import { DockerOrganizerConfigService } from '@app/unraid-api/graph/resolvers/docker/organizer/docker-organizer-config.service.js';
@@ -51,7 +52,8 @@ export class DockerOrganizerService {
private readonly logger = new Logger(DockerOrganizerService.name);
constructor(
private readonly dockerConfigService: DockerOrganizerConfigService,
private readonly dockerService: DockerService
private readonly dockerService: DockerService,
private readonly dockerTemplateIconService: DockerTemplateIconService
) {}
async getResources(

View File

@@ -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',
@@ -99,6 +99,31 @@ export class NotificationCounts {
total!: number;
}
@ObjectType('NotificationSettings')
export class NotificationSettings {
@Field()
@IsString()
@IsNotEmpty()
position!: string;
@Field(() => Boolean)
@IsBoolean()
@IsNotEmpty()
expand!: boolean;
@Field(() => Int)
@IsInt()
@Min(1)
@IsNotEmpty()
duration!: number;
@Field(() => Int)
@IsInt()
@Min(1)
@IsNotEmpty()
max!: number;
}
@ObjectType('NotificationOverview')
export class NotificationOverview {
@Field(() => NotificationCounts)
@@ -170,4 +195,8 @@ export class Notifications extends Node {
})
@IsNotEmpty()
warningsAndAlerts!: Notification[];
@Field(() => NotificationSettings)
@IsNotEmpty()
settings!: NotificationSettings;
}

View File

@@ -13,6 +13,7 @@ import {
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 +42,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 })

View File

@@ -25,6 +25,7 @@ import {
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';
@@ -98,12 +99,12 @@ 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;
}
@@ -111,9 +112,39 @@ export class NotificationsService {
private async handleNotificationAdd(path: string) {
// 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.debug(`[handleNotificationAdd] Adding ${type} Notification: ${path}`);
const notification = await this.loadNotificationFile(path, NotificationType[type]);
// Note: We intentionally track duplicate files (files in both unread and archive)
// because the frontend relies on (Archive Total - Unread Total) to calculate the
// "Archived Only" count. If we ignore duplicates here, the math breaks.
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));
}
}
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) {
@@ -123,6 +154,10 @@ export class NotificationsService {
});
void this.publishWarningsAndAlerts();
}
// Also publish overview updates for archive adds, so counts stay in sync
if (type === NotificationType.ARCHIVE) {
this.publishOverview();
}
}
/**
@@ -137,6 +172,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,
@@ -216,10 +271,10 @@ export class NotificationsService {
const fileData = this.makeNotificationFileData(data);
try {
const [command, args] = this.getLegacyScriptArgs(fileData);
const [command, args] = this.getLegacyScriptArgs(fileData, id);
await execa(command, args);
} catch (error) {
// manually write the file if the script fails
// manually write the file if the script fails entirely
this.logger.debug(`[createNotification] legacy notifier failed: ${error}`);
this.logger.verbose(`[createNotification] Writing: ${JSON.stringify(fileData, null, 4)}`);
@@ -243,7 +298,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 +309,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()];
}
@@ -431,6 +489,7 @@ export class NotificationsService {
public async archiveNotification({ id }: Pick<Notification, 'id'>): Promise<Notification> {
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,12 +509,32 @@ 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);
// CRITICAL FIX: If the file already existed in archive, it should have been counted
// by handleNotificationAdd (since we removed the ignore logic).
// Therefore, we do NOT increment the archive count here to avoid double counting.
} else {
// File not in archive, move it there
await rename(unreadPath, archivePath);
// 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.publishWarningsAndAlerts();
@@ -499,18 +578,20 @@ export class NotificationsService {
return { overview: NotificationsService.overview };
}
const overviewSnapshot = this.getOverview();
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 stats = await batchProcess(notifications, archive);
const archiveAction = async (notification: Notification) => {
// Reuse archiveNotification which handles the "exists" check logic
await this.archiveNotification({ id: notification.id });
};
const stats = await batchProcess(notifications, archiveAction);
void this.publishWarningsAndAlerts();
return { ...stats, overview: overviewSnapshot };
// Return the *actual* current state of the service, which is properly updated
// by the individual archiveNotification calls.
return { ...stats, overview: this.getOverview() };
}
public async unarchiveAll(importance?: NotificationImportance) {
@@ -682,6 +763,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 +803,39 @@ export class NotificationsService {
filters: Partial<NotificationFilter>
): 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 = <T>(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];
}
/**

View File

@@ -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', () => {
</domain>
`;
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;

View File

@@ -215,11 +215,13 @@ export function enrichFlatEntries(
*
* @param view - The flat organizer view to resolve
* @param resources - The collection of all available resources
* @param iconMap - Optional map of resource IDs to icon URLs
* @returns A resolved view with nested objects instead of ID references
*/
export function resolveOrganizerView(
view: OrganizerView,
resources: OrganizerV1['resources']
resources: OrganizerV1['resources'],
iconMap?: Map<string, string>
): ResolvedOrganizerView {
const flatEntries = enrichFlatEntries(view, resources);
@@ -237,13 +239,17 @@ export function resolveOrganizerView(
* are replaced with actual objects for frontend convenience.
*
* @param organizer - The flat organizer structure to resolve
* @param iconMap - Optional map of resource IDs to icon URLs
* @returns A resolved organizer with nested objects instead of ID references
*/
export function resolveOrganizer(organizer: OrganizerV1): ResolvedOrganizerV1 {
export function resolveOrganizer(
organizer: OrganizerV1,
iconMap?: Map<string, string>
): ResolvedOrganizerV1 {
const resolvedViews: ResolvedOrganizerView[] = [];
for (const [viewId, view] of Object.entries(organizer.views)) {
resolvedViews.push(resolveOrganizerView(view, organizer.resources));
resolvedViews.push(resolveOrganizerView(view, organizer.resources, iconMap));
}
return {

View File

@@ -0,0 +1,416 @@
<?php
// included in login.php
$REMOTE_ADDR = $_SERVER['REMOTE_ADDR'] ?? "unknown";
$MAX_PASS_LENGTH = 128;
$VALIDATION_MESSAGES = [
'empty' => _('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']);
?>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta http-equiv="content-type" content="text/html; charset=UTF-8">
<meta http-equiv="Cache-Control" content="no-cache">
<meta http-equiv="Pragma" content="no-cache">
<meta http-equiv="Expires" content="0">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width,initial-scale=1">
<meta name="robots" content="noindex, nofollow">
<meta http-equiv="Content-Security-Policy" content="block-all-mixed-content">
<meta name="referrer" content="same-origin">
<title><?=$var['NAME']?>/SetPassword</title>
<link rel="icon" href="webGui/images/animated-logo.svg" sizes="any" type="image/svg+xml">
<style>
/************************
/
/ Fonts
/
/************************/
@font-face{font-family:clear-sans;font-weight:normal;font-style:normal; src:url('/webGui/styles/clear-sans.woff?v=20220513') format('woff')}
@font-face{font-family:clear-sans;font-weight:bold;font-style:normal; src:url('/webGui/styles/clear-sans-bold.woff?v=20220513') format('woff')}
@font-face{font-family:clear-sans;font-weight:normal;font-style:italic; src:url('/webGui/styles/clear-sans-italic.woff?v=20220513') format('woff')}
@font-face{font-family:clear-sans;font-weight:bold;font-style:italic; src:url('/webGui/styles/clear-sans-bold-italic.woff?v=20220513') format('woff')}
@font-face{font-family:bitstream;font-weight:normal;font-style:normal; src:url('/webGui/styles/bitstream.woff?v=20220513') format('woff')}
@font-face{font-family:bitstream;font-weight:bold;font-style:normal; src:url('/webGui/styles/bitstream-bold.woff?v=20220513') format('woff')}
@font-face{font-family:bitstream;font-weight:normal;font-style:italic; src:url('/webGui/styles/bitstream-italic.woff?v=20220513') format('woff')}
@font-face{font-family:bitstream;font-weight:bold;font-style:italic; src:url('/webGui/styles/bitstream-bold-italic.woff?v=20220513') format('woff')}
/************************
/
/ General styling
/
/************************/
:root {
--body-bg: <?= $THEME_DARK ? '#1c1b1b' : '#f2f2f2' ?>;
--body-text-color: <?= $THEME_DARK ? '#fff' : '#1c1b1b' ?>;
--section-bg: <?= $THEME_DARK ? '#1c1b1b' : '#f2f2f2' ?>;
--shadow: <?= $THEME_DARK ? 'rgba(115,115,115,.12)' : 'rgba(0,0,0,.12)' ?>;
--form-text-color: <?= $THEME_DARK ? '#f2f2f2' : '#1c1b1b' ?>;
--form-bg-color: <?= $THEME_DARK ? 'rgba(26,26,26,0.4)' : '#f2f2f2' ?>;
--form-border-color: <?= $THEME_DARK ? '#2B2A29' : '#ccc' ?>;
}
body {
background: var(--body-bg);
color: var(--body-text-color);
font-family: clear-sans, sans-serif;
font-size: .875rem;
padding: 0;
margin: 0;
}
a {
text-transform: uppercase;
font-weight: bold;
letter-spacing: 2px;
color: #FF8C2F;
text-decoration: none;
}
a:hover {
color: #f15a2c;
}
h1 {
font-size: 1.8rem;
margin: 0;
}
h2 {
font-size: .8rem;
margin-top: 0;
margin-bottom: 1em;
}
.button {
color: #ff8c2f;
font-family: clear-sans, sans-serif;
background: -webkit-gradient(linear,left top,right top,from(#e03237),to(#fd8c3c)) 0 0 no-repeat,-webkit-gradient(linear,left top,right top,from(#e03237),to(#fd8c3c)) 0 100% no-repeat,-webkit-gradient(linear,left bottom,left top,from(#e03237),to(#e03237)) 0 100% no-repeat,-webkit-gradient(linear,left bottom,left top,from(#fd8c3c),to(#fd8c3c)) 100% 100% no-repeat;
background: linear-gradient(90deg,#e03237 0,#fd8c3c) 0 0 no-repeat,linear-gradient(90deg,#e03237 0,#fd8c3c) 0 100% no-repeat,linear-gradient(0deg,#e03237 0,#e03237) 0 100% no-repeat,linear-gradient(0deg,#fd8c3c 0,#fd8c3c) 100% 100% no-repeat;
background-size: 100% 2px,100% 2px,2px 100%,2px 100%;
}
.button:disabled {
opacity: .5;
cursor: not-allowed;
}
.button:hover,
.button:focus {
color: #fff;
background-color: #f15a2c;
background: -webkit-gradient(linear,left top,right top,from(#e22828),to(#ff8c2f));
background: linear-gradient(90deg,#e22828 0,#ff8c2f);
-webkit-box-shadow: none;
box-shadow: none;
cursor: pointer;
outline: none;
}
.button--small {
font-size: .875rem;
font-weight: 600;
line-height: 1;
text-transform: uppercase;
letter-spacing: 2px;
text-align: center;
text-decoration: none;
display: inline-block;
background-color: transparent;
border-radius: .125rem;
border: 0;
-webkit-transition: none;
transition: none;
padding: .75rem 1.5rem;
}
[type=password],
[type=text] {
color: var(--form-text-color);
font-family: clear-sans, sans-serif;
font-size: .875rem;
background-color: var(--form-bg-color);
width: 100%;
margin-top: .25rem;
margin-bottom: 1rem;
border: 2px solid var(--form-border-color);
padding: .75rem 1rem;
-webkit-box-sizing: border-box;
box-sizing: border-box;
border-radius: 0;
-webkit-appearance: none;
}
[type=password]:focus,
[type=text]:focus {
border-color: #ff8c2f;
outline: none;
}
[type=password]:disabled,
[type=text]:disabled {
cursor: not-allowed;
opacity: .5;
}
/************************
/
/ Utility Classes
/
/************************/
.w-100px { width: 100px }
.w-full { width: 100% }
.relative { position: relative }
.flex { display: flex }
.flex-auto { flex: auto }
.flex-col { flex-direction: column }
.flex-row { flex-direction: row }
.justify-between { justify-content: space-between }
.justify-end { justify-content: flex-end }
.invisible { visibility: hidden }
/************************
/
/ Login spesific styling
/
/************************/
section {
width: 500px;
margin: 6rem auto;
border-radius: 10px;
background: var(--section-bg);
-webkit-box-shadow: 0 2px 8px 0 var(--shadow);
box-shadow: 0 2px 8px 0 var(--shadow);
}
.logo {
z-index: 1;
position: relative;
padding: 2rem;
width: 100px;
}
.error {
color: #E22828;
font-weight: bold;
margin-top: 0;
}
.content { padding: 2rem }
.angle {
position: relative;
overflow: hidden;
height: 120px;
border-radius: 10px 10px 0 0;
}
.angle:after {
content: "";
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 120px;
background-color: #f15a2c;
background: -webkit-gradient(linear,left top,right top,from(#e22828),to(#ff8c2f));
background: linear-gradient(90deg,#e22828 0,#ff8c2f);
-webkit-transform-origin: bottom left;
transform-origin: bottom left;
-webkit-transform: skewY(-6deg);
transform: skewY(-6deg);
-webkit-transition: -webkit-transform .15s linear;
transition: -webkit-transform .15s linear;
transition: transform .15s linear;
transition: transform .15s linear,-webkit-transform .15s linear;
}
.pass-toggle {
color: #ff8c2f;
border: 0;
appearance: none;
background: transparent;
}
.pass-toggle:hover,
.pass-toggle:focus {
color: #f15a2c;
outline: none;
}
.pass-toggle svg {
fill: currentColor;
height: 1rem;
width: 1rem;
}
/************************
/
/ Media queries for mobile responsive
/
/************************/
@media (max-width: 500px) {
body {
background: var(--section-bg);
}
[type=password],
[type=text] {
font-size: 16px; /* This prevents the mobile browser from zooming in on the input-field. */
}
section {
margin: 0;
border-radius: 0;
width: 100%;
box-shadow: none;
}
.angle { border-radius: 0 }
}
</style>
<noscript>
<style type="text/css">
.js-validate { display: none }
</style>
</noscript>
</head>
<body>
<section>
<div class="angle">
<div class="logo">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 222.4 39"><path fill="#ffffff" d="M146.70000000000002 29.5H135l-3 9h-6.5L138.9 0h8l13.4 38.5h-7.1L142.6 6.9l-5.8 16.9h8.2l1.7 5.7zM29.7 0v25.4c0 8.9-5.8 13.6-14.9 13.6C5.8 39 0 34.3 0 25.4V0h6.5v25.4c0 5.2 3.2 7.9 8.2 7.9 5.2 0 8.4-2.7 8.4-7.9V0h6.6zM50.9 12v26.5h-6.5V0h6.1l17 26.5V0H74v38.5h-6.1L50.9 12zM171.3 0h6.5v38.5h-6.5V0zM222.4 24.7c0 9-5.9 13.8-15.2 13.8h-14.5V0h14.6c9.2 0 15.1 4.8 15.1 13.8v10.9zm-6.6-10.9c0-5.3-3.3-8.1-8.5-8.1h-8.1v27.1h8c5.3 0 8.6-2.8 8.6-8.1V13.8zM108.3 23.9c4.3-1.6 6.9-5.3 6.9-11.5 0-8.7-5.1-12.4-12.8-12.4H88.8v38.5h6.5V5.7h6.9c3.8 0 6.2 1.8 6.2 6.7s-2.4 6.8-6.2 6.8h-3.4l9.2 19.4h7.5l-7.2-14.7z"></path></svg>
</div>
</div>
<div class="content">
<header>
<h1><?=htmlspecialchars($var['NAME'])?></h1>
<h2><?=htmlspecialchars($var['COMMENT'])?></h2>
<p><?=_('Please set a password for the root user account')?>.</p>
<p><?=_('Max password length is 128 characters')?>.</p>
</header>
<noscript>
<p class="error"><?=_('The Unraid OS webgui requires JavaScript')?>. <?=_('Please enable it')?>.</p>
<p class="error"><?=_('Please also ensure you have cookies enabled')?>.</p>
</noscript>
<form action="/login" method="POST" class="js-validate w-full flex flex-col">
<label for="password"><?= _('Username') ?></label>
<input name="username" type="text" value="root" disabled title="<?=_('Username not changeable')?>">
<div class="flex flex-row items-center justify-between">
<label for="password" class="flex-auto"><?=_('Password')?></label>
<button type="button" tabIndex="-1" class="js-pass-toggle pass-toggle" title="<?=_('Show Password')?>">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48">
<path d="M24,9A23.654,23.654,0,0,0,2,24a23.633,23.633,0,0,0,44,0A23.643,23.643,0,0,0,24,9Zm0,25A10,10,0,1,1,34,24,10,10,0,0,1,24,34Zm0-16a6,6,0,1,0,6,6A6,6,0,0,0,24,18Z"/>
<g class="js-pass-toggle-hide">
<rect x="20.133" y="2.117" height="44" transform="translate(23.536 -8.587) rotate(45)" />
<rect x="22" y="3.984" width="4" height="44" transform="translate(25.403 -9.36) rotate(45)" fill="#f2f2f2" />
</g>
</svg>
</button>
</div>
<input id="password" name="password" type="password" max="128" autocomplete="new-password" autofocus required>
<label for="confirmPassword"><?=_('Confirm Password')?></label>
<input id="confirmPassword" name="confirmPassword" type="password" max="128" autocomplete="new-password" required>
<p class="js-error error"><?=@$POST_ERROR?></p>
<div class="flex justify-end">
<button disabled type="submit" class="js-submit button button--small"><?=_('Set Password')?></button>
</div>
</form>
</div>
</section>
<script type="text/javascript">
// cookie check
document.cookie = "cookietest=1";
cookieEnabled = document.cookie.indexOf("cookietest=")!=-1;
document.cookie = "cookietest=1; expires=Thu, 01-Jan-1970 00:00:01 GMT";
if (!cookieEnabled) {
const errorElement = document.createElement('p');
errorElement.classList.add('error');
errorElement.textContent = "<?=_('Please enable cookies to use the Unraid webGUI')?>";
document.body.textContent = '';
document.body.appendChild(errorElement);
}
// Password toggling
const $passToggle = document.querySelector('.js-pass-toggle');
const $passToggleHideSvg = $passToggle.querySelector('.js-pass-toggle-hide');
const $passInputs = document.querySelectorAll('[type=password]');
let hidePass = true;
$passToggle.addEventListener('click', () => {
hidePass = !hidePass;
if (!hidePass) $passToggleHideSvg.classList.add('invisible'); // toggle svg elements
else $passToggleHideSvg.classList.remove('invisible');
$passInputs.forEach($el => $el.type = hidePass ? 'password' : 'text'); // change input types
$passToggle.setAttribute('title', hidePass ? "<?=_('Show Password')?>" : "<?=_('Hide Password')?>"); // change toggle title
});
// front-end validation
const $submitBtn = document.querySelector('.js-submit');
const $passInput = document.querySelector('[name=password]');
const $confirmPassInput = document.querySelector('[name=confirmPassword]');
const $errorTarget = document.querySelector('.js-error');
const maxPassLength = <?= $MAX_PASS_LENGTH ?>;
let displayValidation = false; // user put values in both inputs. now always check on change or debounced blur.
// helper functions
function debounce(func, timeout = 300){
let timer;
return (...args) => {
clearTimeout(timer);
timer = setTimeout(() => { func.apply(this, args); }, timeout);
};
}
function validate() {
// User has entered values into both password fields. Let's start to nag them until they can submit
if ($passInput.value && $confirmPassInput.value) displayValidation = true;
const inputsEmpty = !$passInput.value || !$confirmPassInput.value;
const inputsMismatch = $passInput.value !== $confirmPassInput.value;
const passTooLong = $passInput.value.length > maxPassLength || $confirmPassInput.value.length > maxPassLength;
if (inputsEmpty || inputsMismatch || passTooLong) {
$submitBtn.setAttribute('disabled', true); // always ensure we keep disabled when no match
// only display error when we know the user has put values into both fields. Don't want to annoy the crap out of them too much.
if (displayValidation) {
if (inputsMismatch) return $errorTarget.innerText = '<?=$VALIDATION_MESSAGES['mismatch']?>';
if (inputsEmpty) return $errorTarget.innerText = '<?=$VALIDATION_MESSAGES['empty']?>';
if (passTooLong) return $errorTarget.innerText = '<?=$VALIDATION_MESSAGES['maxLength']?>';
}
return false;
}
// passwords match remove errors and allow submission
$errorTarget.innerText = '';
$submitBtn.removeAttribute('disabled');
return true;
};
// event 🦻
$passInputs.forEach($el => {
$el.addEventListener('change', () => debounce(validate()));
$el.addEventListener('keyup', () => {
if (displayValidation) debounce(validate()); // Wait until displayValidation is swapped in a change event
});
});
</script>
</body>
</html>

View File

@@ -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"
---
<?PHP
/* Copyright 2005-2023, Lime Technology
* Copyright 2012-2023, Bergware International.
* Copyright 2014-2021, Guilherme Jardim, Eric Schultz, Jon Panozzo.
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License version 2,
* as published by the Free Software Foundation.
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*/
?>
<?
require_once "$docroot/plugins/dynamix.docker.manager/include/DockerClient.php";
$width = in_array($theme,['white','black']) ? -58: -44;
$top = in_array($theme,['white','black']) ? 40 : 20;
$busy = "<i class='fa fa-spin fa-circle-o-notch'></i> "._('Please wait')."... "._('starting up containers');
$cpus = cpu_list();
?>
<link type="text/css" rel="stylesheet" href="<?autov('/webGui/styles/jquery.switchbutton.css')?>">
<table id="docker_containers" class="tablesorter shift">
<thead><tr><th><a id="resetsort" class="nohand" onclick="resetSorting()" title="_(Reset sorting)_"><i class="fa fa-th-list"></i></a>_(Application)_</th><th>_(Version)_</th><th>_(Network)_</th><th>_(Container IP)_</th><th>_(Container Port)_</th><th>_(LAN IP:Port)_</th><th>_(Volume Mappings)_ <small>(_(App to Host)_)</small></th><th class="load advanced">_(CPU & Memory load)_</th><th class="nine">_(Autostart)_</th><th class="five">_(Uptime)_</th></tr></thead>
<tbody id="docker_list"><tr><td colspan='9'></td></tr></tbody>
</table>
<input type="button" onclick="addContainer()" value="_(Add Container)_" style="display:none">
<input type="button" onclick="startAll()" value="_(Start All)_" style="display:none">
<input type="button" onclick="stopAll()" value="_(Stop All)_" style="display:none">
<input type="button" onclick="pauseAll()" value="_(Pause All)_" style="display:none">
<input type="button" onclick="resumeAll()" value="_(Resume All)_" style="display:none">
<input type="button" onclick="checkAll()" value="_(Check for Updates)_" id="checkAll" style="display:none">
<input type="button" onclick="updateAll()" value="_(Update All)_" id="updateAll" style="display:none">
<input type="button" onclick="contSizes()" value="_(Container Size)_" style="display:none">
<div id="iframe-popup" style="display:none;-webkit-overflow-scrolling:touch;"></div>
<script src="<?autov('/webGui/javascript/jquery.switchbutton.js')?>"></script>
<script src="<?autov('/plugins/dynamix.docker.manager/javascript/docker.js')?>"></script>
<script>
var docker = [];
<?if (!$tabbed):?>
$('.title').append("<span id='busy' class='red-text strong' style='display:none;margin-left:40px'><?=$busy?></span>");
<?else:?>
$('.tabs').append("<span id='busy' class='red-text strong' style='display:none;position:relative;top:<?=$top?>px;left:40px;font-size:1.4rem;letter-spacing:2px'><?=$busy?></span>");
<?endif;?>
<?if (_var($display,'resize')):?>
function resize() {
$('#docker_list').height(Math.max(window.innerHeight-340,330));
$('#docker_containers thead,#docker_containers tbody').removeClass('fixed');
$('#docker_containers thead tr th').each(function(){$(this).width($(this).width());});
$('#docker_containers tbody tr td').each(function(){$(this).width($(this).width());});
$('#docker_containers thead,#docker_containers tbody').addClass('fixed');
}
<?endif;?>
function resetSorting() {
if ($.cookie('lockbutton')==null) return;
$('input[type=button]').prop('disabled',true);
$.post('/plugins/dynamix.docker.manager/include/UserPrefs.php',{reset:true},function(){loadlist();});
}
function listview() {
var more = $.cookie('docker_listview_mode')=='advanced';
<?if(($dockercfg['DOCKER_READMORE']??'yes') === 'yes'):?>
$('.docker_readmore').readmore({maxHeight:32,moreLink:"<a href='#' style='text-align:center'><i class='fa fa-chevron-down'></i></a>",lessLink:"<a href='#' style='text-align:center'><i class='fa fa-chevron-up'></i></a>"});
<?endif;?>
$('input.autostart').each(function(){
var wait = $('#'+$(this).prop('id').replace('auto','wait'));
var auto = $(this).prop('checked');
if (auto && more) wait.show(); else wait.hide();
});
}
function LockButton() {
if ($.cookie('lockbutton')==null) {
$.cookie('lockbutton','lockbutton');
$('#resetsort').removeClass('nohand').addClass('hand');
$('i.mover').show();
$('#docker_list .sortable').css({'cursor':'move'});
<?if ($themes1):?>
$('div.nav-item.LockButton').find('a').prop('title',"_(Lock sortable items)_");
$('div.nav-item.LockButton').find('b').removeClass('icon-u-lock green-text').addClass('icon-u-lock-open red-text');
<?endif;?>
$('div.nav-item.LockButton').find('span').text("_(Lock sortable items)_");
$('#docker_list').sortable({helper:'clone',items:'.sortable',cursor:'grab',axis:'y',containment:'parent',cancel:'span.docker_readmore,input',delay:100,opacity:0.5,zIndex:9999,forcePlaceholderSize:true,
update:function(e,ui){
var row = $('#docker_list').find('tr:first');
var names = ''; var index = '';
row.parent().children().find('td.ct-name').each(function(){names+=$(this).find('.appname').text()+';';index+=$(this).parent().parent().children().index($(this).parent())+';';});
$.post('/plugins/dynamix.docker.manager/include/UserPrefs.php',{names:names,index:index});
}});
} else {
$.removeCookie('lockbutton');
$('#resetsort').removeClass('hand').addClass('nohand');
$('i.mover').hide();
$('#docker_list .sortable').css({'cursor':'default'});
<?if ($themes1):?>
$('div.nav-item.LockButton').find('a').prop('title',"_(Unlock sortable items)_");
$('div.nav-item.LockButton').find('b').removeClass('icon-u-lock-open red-text').addClass('icon-u-lock green-text');
<?endif;?>
$('div.nav-item.LockButton').find('span').text("_(Unlock sortable items)_");
$('#docker_list').sortable('destroy');
}
}
function loadlist(init) {
timers.docker = setTimeout(function(){$('div.spinner.fixed').show('slow');},500);
docker = [];
$.get('/plugins/dynamix.docker.manager/include/DockerContainers.php',function(d) {
clearTimeout(timers.docker);
var data = d.split(/\0/);
$(".TS_tooltip").tooltipster("destroy");
$('#docker_list').html(data[0]);
$('.TS_tooltip').tooltipster({
animation: 'fade',
delay: 200,
trigger: 'custom',
triggerOpen: {click:true,touchstart:true,mouseenter:true},
triggerClose:{click:true,scroll:false,mouseleave:true},
interactive: true,
viewportAware: true,
contentAsHTML: true,
functionBefore: function(instance,helper) {
var origin = $(helper.origin);
var TScontent = $(origin).attr("data-tstitle");
instance.content(TScontent);
}
});
$('head').append('<script>'+data[1]+'<\/script>');
<?if (_var($display,'resize')):?>
resize();
if (init) $(window).bind('resize',function(){resize();});
<?endif;?>
$('.iconstatus').each(function(){
if ($(this).hasClass('stopped')) $('div.'+$(this).prop('id')).hide();
});
$('.autostart').switchButton({labels_placement:'right', on_label:"_(On)_", off_label:"_(Off)_"});
$('.autostart').change(function(){
var more = $.cookie('docker_listview_mode')=='advanced';
var wait = $('#'+$(this).prop('id').replace('auto','wait'));
var auto = $(this).prop('checked');
if (auto && more) wait.show(); else wait.hide();
$.post('/plugins/dynamix.docker.manager/include/UpdateConfig.php',{action:'autostart',container:$(this).attr('container'),auto:auto,wait:wait.find('input.wait').val()});
});
$('input.wait').change(function(){
$.post('/plugins/dynamix.docker.manager/include/UpdateConfig.php',{action:'wait',container:$(this).attr('container'),wait:$(this).val()});
});
if ($.cookie('docker_listview_mode')=='advanced') {$('.advanced').show(); $('.basic').hide();}
$('input[type=button]').prop('disabled',false).show('slow');
var update = false, rebuild = false;
for (var i=0,ct; ct=docker[i]; i++) {
if (ct.update==1) update = true;
if (ct.update==2) rebuild = true;
}
listview();
$('div.spinner.fixed').hide('slow');
if (data[2]==1) {$('#busy').show(); setTimeout(loadlist,5000);} else if ($('#busy').is(':visible')) {$('#busy').hide(); setTimeout(loadlist,3000);}
if (!update) $('input#updateAll').prop('disabled',true);
if (rebuild) rebuildAll();
});
}
function contSizes() {
// show spinner over window
$('div.spinner.fixed').css({'z-index':'100000'}).show();
openPlugin('container_size', "_(Container Size)_");
}
var dockerload = new NchanSubscriber('/sub/dockerload',{subscriber:'websocket'});
dockerload.on('message', function(msg){
var data = msg.split('\n');
for (var i=0,row; row=data[i]; i++) {
var id = row.split(';');
var w1 = Math.round(Math.min(id[1].slice(0,-1)/<?=count($cpus)*count(preg_split('/[,-]/',$cpus[0]))?>,100)*100)/100+'%';
$('.cpu-'+id[0]).text(w1.replace('.','<?=_var($display,'number','.,')[0]?>'));
$('.mem-'+id[0]).text(id[2]);
$('#cpu-'+id[0]).css('width',w1);
}
});
$(function() {
$(".tabs").append('<span class="status"><span><input type="checkbox" class="advancedview"></span></span>');
$('.advancedview').switchButton({labels_placement:'left', on_label:"_(Advanced View)_", off_label:"_(Basic View)_", checked:$.cookie('docker_listview_mode')=='advanced'});
$('.advancedview').change(function(){
$('.advanced').toggle('slow');
$('.basic').toggle('slow');
$.cookie('docker_listview_mode',$('.advancedview').is(':checked')?'advanced':'basic',{expires:3650});
listview();
});
$.removeCookie('lockbutton');
loadlist(true);
dockerload.start().monitor();
});
</script>

View File

@@ -0,0 +1,59 @@
<?PHP
/* Copyright 2005-2023, Lime Technology
* Copyright 2012-2023, Bergware International.
* Copyright 2012, Andrew Hamer-Adams, http://www.pixeleyes.co.nz.
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License version 2,
* as published by the Free Software Foundation.
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*/
?>
<?
$docroot ??= ($_SERVER['DOCUMENT_ROOT'] ?: '/usr/local/emhttp');
$notify = "$docroot/webGui/scripts/notify";
switch ($_POST['cmd']??'') {
case 'init':
shell_exec("$notify init");
break;
case 'smtp-init':
shell_exec("$notify smtp-init");
break;
case 'cron-init':
shell_exec("$notify cron-init");
break;
case 'add':
foreach ($_POST as $option => $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;
}
?>

View File

@@ -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}

View File

@@ -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}

View File

@@ -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}

View File

@@ -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}

View File

@@ -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=""

View File

@@ -0,0 +1,261 @@
#!/usr/bin/php -q
<?PHP
/* Copyright 2005-2023, Lime Technology
* Copyright 2012-2023, Bergware International.
* Copyright 2012, Andrew Hamer-Adams, http://www.pixeleyes.co.nz.
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License version 2,
* as published by the Free Software Foundation.
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*/
?>
<?
$docroot ??= ($_SERVER['DOCUMENT_ROOT'] ?: '/usr/local/emhttp');
require_once "$docroot/webGui/include/Wrappers.php";
require_once "$docroot/webGui/include/Encryption.php";
function usage() {
echo <<<EOT
notify [-e "event"] [-s "subject"] [-d "description"] [-i "normal|warning|alert"] [-m "message"] [-x] [-t] [-b] [add]
create a notification
use -e to specify the event
use -s to specify a subject
use -d to specify a short description
use -i to specify the severity
use -m to specify a message (long description)
use -l to specify a link (clicking the notification will take you to that location)
use -x to create a single notification ticket
use -r to specify recipients and not use default
use -t to force send email only (for testing)
use -b to NOT send a browser notification
all options are optional
notify init
Initialize the notification subsystem.
notify smtp-init
Initialize sendmail configuration (ssmtp in our case).
notify get
Output a json-encoded list of all the unread notifications.
notify archive file
Move file from 'unread' state to 'archive' state.
EOT;
return 1;
}
function generate_email($event, $subject, $description, $importance, $message, $recipients, $fqdnlink) {
global $ssmtp;
$rcpt = $ssmtp['RcptTo'];
if (!$recipients)
$to = implode(',', explode(' ', trim($rcpt)));
else
$to = $recipients;
if (empty($to)) return;
$subj = "{$ssmtp['Subject']}$subject";
$headers = [];
$headers[] = "MIME-Version: 1.0";
$headers[] = "X-Mailer: PHP/".phpversion();
$headers[] = "Content-type: text/plain; charset=utf-8";
$headers[] = "From: {$ssmtp['root']}";
$headers[] = "Reply-To: {$ssmtp['root']}";
if (($importance == "warning" || $importance == "alert") && $ssmtp['SetEmailPriority']=="True") {
$headers[] = "X-Priority: 1 (highest)";
$headers[] = "X-Mms-Priority: High";
}
$headers[] = "";
$body = [];
if (!empty($fqdnlink)) {
$body[] = "Link: $fqdnlink";
$body[] = "";
}
$body[] = "Event: $event";
$body[] = "Subject: $subject";
$body[] = "Description: $description";
$body[] = "Importance: $importance";
if (!empty($message)) {
$body[] = "";
foreach (explode('\n',$message) as $line)
$body[] = $line;
}
$body[] = "";
return mail($to, $subj, implode("\n", $body), implode("\n", $headers));
}
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);
}
/*
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 '&#8201;&#176;'
*/
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','<br>',$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('<br>','. ',$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);
?>

View File

@@ -1,16 +1,27 @@
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 { 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 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';
interface ModificationTestCase {
@@ -30,12 +41,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',
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,6 +88,48 @@ 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',
},
];
/** Modifications that simply add a new file & remove it on rollback. */
@@ -122,7 +193,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) => {

View File

@@ -0,0 +1,417 @@
<?php
// included in login.php
$REMOTE_ADDR = $_SERVER['REMOTE_ADDR'] ?? "unknown";
$MAX_PASS_LENGTH = 128;
$VALIDATION_MESSAGES = [
'empty' => _('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']);
?>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta http-equiv="content-type" content="text/html; charset=UTF-8">
<meta http-equiv="Cache-Control" content="no-cache">
<meta http-equiv="Pragma" content="no-cache">
<meta http-equiv="Expires" content="0">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width,initial-scale=1">
<meta name="robots" content="noindex, nofollow">
<meta http-equiv="Content-Security-Policy" content="block-all-mixed-content">
<meta name="referrer" content="same-origin">
<title><?=$var['NAME']?>/SetPassword</title>
<link rel="icon" href="webGui/images/animated-logo.svg" sizes="any" type="image/svg+xml">
<style>
/************************
/
/ Fonts
/
/************************/
@font-face{font-family:clear-sans;font-weight:normal;font-style:normal; src:url('/webGui/styles/clear-sans.woff?v=20220513') format('woff')}
@font-face{font-family:clear-sans;font-weight:bold;font-style:normal; src:url('/webGui/styles/clear-sans-bold.woff?v=20220513') format('woff')}
@font-face{font-family:clear-sans;font-weight:normal;font-style:italic; src:url('/webGui/styles/clear-sans-italic.woff?v=20220513') format('woff')}
@font-face{font-family:clear-sans;font-weight:bold;font-style:italic; src:url('/webGui/styles/clear-sans-bold-italic.woff?v=20220513') format('woff')}
@font-face{font-family:bitstream;font-weight:normal;font-style:normal; src:url('/webGui/styles/bitstream.woff?v=20220513') format('woff')}
@font-face{font-family:bitstream;font-weight:bold;font-style:normal; src:url('/webGui/styles/bitstream-bold.woff?v=20220513') format('woff')}
@font-face{font-family:bitstream;font-weight:normal;font-style:italic; src:url('/webGui/styles/bitstream-italic.woff?v=20220513') format('woff')}
@font-face{font-family:bitstream;font-weight:bold;font-style:italic; src:url('/webGui/styles/bitstream-bold-italic.woff?v=20220513') format('woff')}
/************************
/
/ General styling
/
/************************/
:root {
--body-bg: <?= $THEME_DARK ? '#1c1b1b' : '#f2f2f2' ?>;
--body-text-color: <?= $THEME_DARK ? '#fff' : '#1c1b1b' ?>;
--section-bg: <?= $THEME_DARK ? '#1c1b1b' : '#f2f2f2' ?>;
--shadow: <?= $THEME_DARK ? 'rgba(115,115,115,.12)' : 'rgba(0,0,0,.12)' ?>;
--form-text-color: <?= $THEME_DARK ? '#f2f2f2' : '#1c1b1b' ?>;
--form-bg-color: <?= $THEME_DARK ? 'rgba(26,26,26,0.4)' : '#f2f2f2' ?>;
--form-border-color: <?= $THEME_DARK ? '#2B2A29' : '#ccc' ?>;
}
body {
background: var(--body-bg);
color: var(--body-text-color);
font-family: clear-sans, sans-serif;
font-size: .875rem;
padding: 0;
margin: 0;
}
a {
text-transform: uppercase;
font-weight: bold;
letter-spacing: 2px;
color: #FF8C2F;
text-decoration: none;
}
a:hover {
color: #f15a2c;
}
h1 {
font-size: 1.8rem;
margin: 0;
}
h2 {
font-size: .8rem;
margin-top: 0;
margin-bottom: 1em;
}
.button {
color: #ff8c2f;
font-family: clear-sans, sans-serif;
background: -webkit-gradient(linear,left top,right top,from(#e03237),to(#fd8c3c)) 0 0 no-repeat,-webkit-gradient(linear,left top,right top,from(#e03237),to(#fd8c3c)) 0 100% no-repeat,-webkit-gradient(linear,left bottom,left top,from(#e03237),to(#e03237)) 0 100% no-repeat,-webkit-gradient(linear,left bottom,left top,from(#fd8c3c),to(#fd8c3c)) 100% 100% no-repeat;
background: linear-gradient(90deg,#e03237 0,#fd8c3c) 0 0 no-repeat,linear-gradient(90deg,#e03237 0,#fd8c3c) 0 100% no-repeat,linear-gradient(0deg,#e03237 0,#e03237) 0 100% no-repeat,linear-gradient(0deg,#fd8c3c 0,#fd8c3c) 100% 100% no-repeat;
background-size: 100% 2px,100% 2px,2px 100%,2px 100%;
}
.button:disabled {
opacity: .5;
cursor: not-allowed;
}
.button:hover,
.button:focus {
color: #fff;
background-color: #f15a2c;
background: -webkit-gradient(linear,left top,right top,from(#e22828),to(#ff8c2f));
background: linear-gradient(90deg,#e22828 0,#ff8c2f);
-webkit-box-shadow: none;
box-shadow: none;
cursor: pointer;
outline: none;
}
.button--small {
font-size: .875rem;
font-weight: 600;
line-height: 1;
text-transform: uppercase;
letter-spacing: 2px;
text-align: center;
text-decoration: none;
display: inline-block;
background-color: transparent;
border-radius: .125rem;
border: 0;
-webkit-transition: none;
transition: none;
padding: .75rem 1.5rem;
}
[type=password],
[type=text] {
color: var(--form-text-color);
font-family: clear-sans, sans-serif;
font-size: .875rem;
background-color: var(--form-bg-color);
width: 100%;
margin-top: .25rem;
margin-bottom: 1rem;
border: 2px solid var(--form-border-color);
padding: .75rem 1rem;
-webkit-box-sizing: border-box;
box-sizing: border-box;
border-radius: 0;
-webkit-appearance: none;
}
[type=password]:focus,
[type=text]:focus {
border-color: #ff8c2f;
outline: none;
}
[type=password]:disabled,
[type=text]:disabled {
cursor: not-allowed;
opacity: .5;
}
/************************
/
/ Utility Classes
/
/************************/
.w-100px { width: 100px }
.w-full { width: 100% }
.relative { position: relative }
.flex { display: flex }
.flex-auto { flex: auto }
.flex-col { flex-direction: column }
.flex-row { flex-direction: row }
.justify-between { justify-content: space-between }
.justify-end { justify-content: flex-end }
.invisible { visibility: hidden }
/************************
/
/ Login spesific styling
/
/************************/
section {
width: 500px;
margin: 6rem auto;
border-radius: 10px;
background: var(--section-bg);
-webkit-box-shadow: 0 2px 8px 0 var(--shadow);
box-shadow: 0 2px 8px 0 var(--shadow);
}
.logo {
z-index: 1;
position: relative;
padding: 2rem;
width: 100px;
}
.error {
color: #E22828;
font-weight: bold;
margin-top: 0;
}
.content { padding: 2rem }
.angle {
position: relative;
overflow: hidden;
height: 120px;
border-radius: 10px 10px 0 0;
}
.angle:after {
content: "";
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 120px;
background-color: #f15a2c;
background: -webkit-gradient(linear,left top,right top,from(#e22828),to(#ff8c2f));
background: linear-gradient(90deg,#e22828 0,#ff8c2f);
-webkit-transform-origin: bottom left;
transform-origin: bottom left;
-webkit-transform: skewY(-6deg);
transform: skewY(-6deg);
-webkit-transition: -webkit-transform .15s linear;
transition: -webkit-transform .15s linear;
transition: transform .15s linear;
transition: transform .15s linear,-webkit-transform .15s linear;
}
.pass-toggle {
color: #ff8c2f;
border: 0;
appearance: none;
background: transparent;
}
.pass-toggle:hover,
.pass-toggle:focus {
color: #f15a2c;
outline: none;
}
.pass-toggle svg {
fill: currentColor;
height: 1rem;
width: 1rem;
}
/************************
/
/ Media queries for mobile responsive
/
/************************/
@media (max-width: 500px) {
body {
background: var(--section-bg);
}
[type=password],
[type=text] {
font-size: 16px; /* This prevents the mobile browser from zooming in on the input-field. */
}
section {
margin: 0;
border-radius: 0;
width: 100%;
box-shadow: none;
}
.angle { border-radius: 0 }
}
</style>
<noscript>
<style type="text/css">
.js-validate { display: none }
</style>
</noscript>
</head>
<body>
<section>
<div class="angle">
<div class="logo">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 222.4 39"><path fill="#ffffff" d="M146.70000000000002 29.5H135l-3 9h-6.5L138.9 0h8l13.4 38.5h-7.1L142.6 6.9l-5.8 16.9h8.2l1.7 5.7zM29.7 0v25.4c0 8.9-5.8 13.6-14.9 13.6C5.8 39 0 34.3 0 25.4V0h6.5v25.4c0 5.2 3.2 7.9 8.2 7.9 5.2 0 8.4-2.7 8.4-7.9V0h6.6zM50.9 12v26.5h-6.5V0h6.1l17 26.5V0H74v38.5h-6.1L50.9 12zM171.3 0h6.5v38.5h-6.5V0zM222.4 24.7c0 9-5.9 13.8-15.2 13.8h-14.5V0h14.6c9.2 0 15.1 4.8 15.1 13.8v10.9zm-6.6-10.9c0-5.3-3.3-8.1-8.5-8.1h-8.1v27.1h8c5.3 0 8.6-2.8 8.6-8.1V13.8zM108.3 23.9c4.3-1.6 6.9-5.3 6.9-11.5 0-8.7-5.1-12.4-12.8-12.4H88.8v38.5h6.5V5.7h6.9c3.8 0 6.2 1.8 6.2 6.7s-2.4 6.8-6.2 6.8h-3.4l9.2 19.4h7.5l-7.2-14.7z"></path></svg>
</div>
</div>
<div class="content">
<header>
<h1><?=htmlspecialchars($var['NAME'])?></h1>
<h2><?=htmlspecialchars($var['COMMENT'])?></h2>
<p><?=_('Please set a password for the root user account')?>.</p>
<p><?=_('Max password length is 128 characters')?>.</p>
</header>
<noscript>
<p class="error"><?=_('The Unraid OS webgui requires JavaScript')?>. <?=_('Please enable it')?>.</p>
<p class="error"><?=_('Please also ensure you have cookies enabled')?>.</p>
</noscript>
<form action="/login" method="POST" class="js-validate w-full flex flex-col">
<label for="password"><?= _('Username') ?></label>
<input name="username" type="text" value="root" disabled title="<?=_('Username not changeable')?>">
<div class="flex flex-row items-center justify-between">
<label for="password" class="flex-auto"><?=_('Password')?></label>
<button type="button" tabIndex="-1" class="js-pass-toggle pass-toggle" title="<?=_('Show Password')?>">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48">
<path d="M24,9A23.654,23.654,0,0,0,2,24a23.633,23.633,0,0,0,44,0A23.643,23.643,0,0,0,24,9Zm0,25A10,10,0,1,1,34,24,10,10,0,0,1,24,34Zm0-16a6,6,0,1,0,6,6A6,6,0,0,0,24,18Z"/>
<g class="js-pass-toggle-hide">
<rect x="20.133" y="2.117" height="44" transform="translate(23.536 -8.587) rotate(45)" />
<rect x="22" y="3.984" width="4" height="44" transform="translate(25.403 -9.36) rotate(45)" fill="#f2f2f2" />
</g>
</svg>
</button>
</div>
<input id="password" name="password" type="password" max="128" autocomplete="new-password" autofocus required>
<label for="confirmPassword"><?=_('Confirm Password')?></label>
<input id="confirmPassword" name="confirmPassword" type="password" max="128" autocomplete="new-password" required>
<p class="js-error error"><?=@$POST_ERROR?></p>
<div class="flex justify-end">
<button disabled type="submit" class="js-submit button button--small"><?=_('Set Password')?></button>
</div>
</form>
</div>
</section>
<script type="text/javascript">
// cookie check
document.cookie = "cookietest=1";
cookieEnabled = document.cookie.indexOf("cookietest=")!=-1;
document.cookie = "cookietest=1; expires=Thu, 01-Jan-1970 00:00:01 GMT";
if (!cookieEnabled) {
const errorElement = document.createElement('p');
errorElement.classList.add('error');
errorElement.textContent = "<?=_('Please enable cookies to use the Unraid webGUI')?>";
document.body.textContent = '';
document.body.appendChild(errorElement);
}
// Password toggling
const $passToggle = document.querySelector('.js-pass-toggle');
const $passToggleHideSvg = $passToggle.querySelector('.js-pass-toggle-hide');
const $passInputs = document.querySelectorAll('[type=password]');
let hidePass = true;
$passToggle.addEventListener('click', () => {
hidePass = !hidePass;
if (!hidePass) $passToggleHideSvg.classList.add('invisible'); // toggle svg elements
else $passToggleHideSvg.classList.remove('invisible');
$passInputs.forEach($el => $el.type = hidePass ? 'password' : 'text'); // change input types
$passToggle.setAttribute('title', hidePass ? "<?=_('Show Password')?>" : "<?=_('Hide Password')?>"); // change toggle title
});
// front-end validation
const $submitBtn = document.querySelector('.js-submit');
const $passInput = document.querySelector('[name=password]');
const $confirmPassInput = document.querySelector('[name=confirmPassword]');
const $errorTarget = document.querySelector('.js-error');
const maxPassLength = <?= $MAX_PASS_LENGTH ?>;
let displayValidation = false; // user put values in both inputs. now always check on change or debounced blur.
// helper functions
function debounce(func, timeout = 300){
let timer;
return (...args) => {
clearTimeout(timer);
timer = setTimeout(() => { func.apply(this, args); }, timeout);
};
}
function validate() {
// User has entered values into both password fields. Let's start to nag them until they can submit
if ($passInput.value && $confirmPassInput.value) displayValidation = true;
const inputsEmpty = !$passInput.value || !$confirmPassInput.value;
const inputsMismatch = $passInput.value !== $confirmPassInput.value;
const passTooLong = $passInput.value.length > maxPassLength || $confirmPassInput.value.length > maxPassLength;
if (inputsEmpty || inputsMismatch || passTooLong) {
$submitBtn.setAttribute('disabled', true); // always ensure we keep disabled when no match
// only display error when we know the user has put values into both fields. Don't want to annoy the crap out of them too much.
if (displayValidation) {
if (inputsMismatch) return $errorTarget.innerText = '<?=$VALIDATION_MESSAGES['mismatch']?>';
if (inputsEmpty) return $errorTarget.innerText = '<?=$VALIDATION_MESSAGES['empty']?>';
if (passTooLong) return $errorTarget.innerText = '<?=$VALIDATION_MESSAGES['maxLength']?>';
}
return false;
}
// passwords match remove errors and allow submission
$errorTarget.innerText = '';
$submitBtn.removeAttribute('disabled');
return true;
};
// event 🦻
$passInputs.forEach($el => {
$el.addEventListener('change', () => debounce(validate()));
$el.addEventListener('keyup', () => {
if (displayValidation) debounce(validate()); // Wait until displayValidation is swapped in a change event
});
});
</script>
<?include "$docroot/plugins/dynamix.my.servers/include/welcome-modal.php"?>
</body>
</html>

View File

@@ -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"
---
<div class="unapi">
<unraid-docker-container-overview></unraid-docker-container-overview>
</div>

View File

@@ -129,6 +129,25 @@ _(Display position)_:
</select>
:notifications_display_position_help:
:
_(Stack notifications)_:
: <select name="expand">
<?=mk_option($notify['expand'] ?? 'true', "true", _("Yes"))?>
<?=mk_option($notify['expand'] ?? 'true', "false", _("No"))?>
</select>
:notifications_stack_help:
_(Duration)_:
: <input type="number" name="duration" value="<?=$notify['duration'] ?? 5000?>" min="1000" step="500">
:notifications_duration_help:
_(Max notifications)_:
: <input type="number" name="max" value="<?=$notify['max'] ?? 3?>" min="1" max="10">
:notifications_max_help:
_(Auto-close)_ (_(seconds)_):
: <input type="number" name="life" class="a" min="0" max="60" value="<?=$notify['life']?>"> _(a value of zero means no automatic closure)_

View File

@@ -0,0 +1,63 @@
<?PHP
/* Copyright 2005-2023, Lime Technology
* Copyright 2012-2023, Bergware International.
* Copyright 2012, Andrew Hamer-Adams, http://www.pixeleyes.co.nz.
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License version 2,
* as published by the Free Software Foundation.
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*/
?>
<?
$docroot ??= ($_SERVER['DOCUMENT_ROOT'] ?: '/usr/local/emhttp');
$notify = "$docroot/webGui/scripts/notify";
switch ($_POST['cmd']??'') {
case 'init':
shell_exec("$notify init");
break;
case 'smtp-init':
shell_exec("$notify smtp-init");
break;
case 'cron-init':
shell_exec("$notify cron-init");
break;
case 'add':
foreach ($_POST as $option => $value) {
switch ($option) {
case 'e':
case 's':
case 'd':
case 'i':
case 'm':
$notify .= " -{$option} ".escapeshellarg($value);
break;
case 'u':
$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;
}
?>

View File

@@ -0,0 +1,279 @@
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}
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}
}
}

View File

@@ -0,0 +1,267 @@
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}
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}
}
}

View File

@@ -0,0 +1,279 @@
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}
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:#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}
#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}
}
}

View File

@@ -0,0 +1,267 @@
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}
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}
}
}

View File

@@ -0,0 +1,85 @@
[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]
expand="true"
duration="5000"
max="3"
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=""

View File

@@ -0,0 +1,333 @@
#!/usr/bin/php -q
<?PHP
/* Copyright 2005-2023, Lime Technology
* Copyright 2012-2023, Bergware International.
* Copyright 2012, Andrew Hamer-Adams, http://www.pixeleyes.co.nz.
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License version 2,
* as published by the Free Software Foundation.
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*/
?>
<?
$docroot ??= ($_SERVER['DOCUMENT_ROOT'] ?: '/usr/local/emhttp');
require_once "$docroot/webGui/include/Wrappers.php";
require_once "$docroot/webGui/include/Encryption.php";
function usage() {
echo <<<EOT
notify [-e "event"] [-s "subject"] [-d "description"] [-i "normal|warning|alert"] [-m "message"] [-x] [-t] [-b] [add]
create a notification
use -e to specify the event
use -s to specify a subject
use -d to specify a short description
use -i to specify the severity
use -m to specify a message (long description)
use -l to specify a link (clicking the notification will take you to that location)
use -x to create a single notification ticket
use -r to specify recipients and not use default
use -t to force send email only (for testing)
use -b to NOT send a browser notification
use -u to specify a custom filename (API use only)
all options are optional
notify init
Initialize the notification subsystem.
notify smtp-init
Initialize sendmail configuration (ssmtp in our case).
notify get
Output a json-encoded list of all the unread notifications.
notify archive file
Move file from 'unread' state to 'archive' state.
EOT;
return 1;
}
function generate_email($event, $subject, $description, $importance, $message, $recipients, $fqdnlink) {
global $ssmtp;
$rcpt = $ssmtp['RcptTo'];
if (!$recipients)
$to = implode(',', explode(' ', trim($rcpt)));
else
$to = $recipients;
if (empty($to)) return;
$subj = "{$ssmtp['Subject']}$subject";
$headers = [];
$headers[] = "MIME-Version: 1.0";
$headers[] = "X-Mailer: PHP/".phpversion();
$headers[] = "Content-type: text/plain; charset=utf-8";
$headers[] = "From: {$ssmtp['root']}";
$headers[] = "Reply-To: {$ssmtp['root']}";
if (($importance == "warning" || $importance == "alert") && $ssmtp['SetEmailPriority']=="True") {
$headers[] = "X-Priority: 1 (highest)";
$headers[] = "X-Mms-Priority: High";
}
$headers[] = "";
$body = [];
if (!empty($fqdnlink)) {
$body[] = "Link: $fqdnlink";
$body[] = "";
}
$body[] = "Event: $event";
$body[] = "Subject: $subject";
$body[] = "Description: $description";
$body[] = "Importance: $importance";
if (!empty($message)) {
$body[] = "";
foreach (explode('\n',$message) as $line)
$body[] = $line;
}
$body[] = "";
return mail($to, $subj, implode("\n", $body), implode("\n", $headers));
}
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);
}
/*
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 '&#8201;&#176;'
*/
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','<br>',$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('<br>','. ',$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);
?>

View File

@@ -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<ShouldApplyWithReason> {
// 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<string> {
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}`;
}
}

View File

@@ -0,0 +1,88 @@
import { Logger } from '@nestjs/common';
import { readFile } from 'node:fs/promises';
import { beforeEach, describe, expect, it, vi } from 'vitest';
import DefaultBaseCssModification from '@app/unraid-api/unraid-file-modifier/modifications/default-base-css.modification.js';
// Mock node:fs/promises
vi.mock('node:fs/promises', () => ({
readFile: vi.fn(),
}));
describe('DefaultBaseCssModification', () => {
let modification: DefaultBaseCssModification;
let logger: Logger;
beforeEach(() => {
logger = new Logger('test');
modification = new DefaultBaseCssModification(logger);
});
it('should correctly apply :scope to selectors', async () => {
const inputCss = `
body {
padding: 0;
}
.Theme--sidebar {
color: red;
}
.Theme--sidebar #displaybox {
width: 100%;
}
.Theme--nav-top .LanguageButton {
font-size: 10px;
}
.Theme--width-boxed #displaybox {
max-width: 1000px;
}
`;
// Mock readFile to return our inputCss
vi.mocked(readFile).mockResolvedValue(inputCss);
// Access the private method applyToSource by casting to any or using a publicly exposed way.
// Since generatePatch calls applyToSource, we can interpret 'generatePatch' output,
// OR we can spy on applyToSource if we want to be tricky,
// BUT simpler is to inspect the patch string OR expose applyToSource for testing if possible.
// However, I can't easily change the class just for this without editing it.
// Let's use 'generatePatch' and see the diff.
// OR, better yet, since I am adding this test to verify the logic, allow me to access the private method via 'any' cast.
// @ts-expect-error accessing private method
const result = modification.applyToSource(inputCss);
expect(result).toContain(':scope.Theme--sidebar {');
expect(result).toContain(':scope.Theme--sidebar #displaybox {');
expect(result).not.toContain(':scope.Theme--nav-top .LanguageButton {');
expect(result).toContain(':scope.Theme--width-boxed #displaybox {');
// Ensure @scope wrapper is present
expect(result).toContain('@scope (:root) to (.unapi) {');
expect(result).toMatch(/@scope \(:root\) to \(\.unapi\) \{[\s\S]*:scope\.Theme--sidebar \{/);
});
it('should not modify other selectors', async () => {
const inputCss = `
body {
padding: 0;
}
.OtherClass {
color: blue;
}
`;
vi.mocked(readFile).mockResolvedValue(inputCss);
// @ts-expect-error accessing private method
const result = modification.applyToSource(inputCss);
expect(result).toContain('.OtherClass {');
expect(result).not.toContain(':scope.OtherClass');
});
it('should throw if body block end is not found', () => {
const inputCss = `html { }`;
// @ts-expect-error accessing private method
expect(() => modification.applyToSource(inputCss)).toThrow('Could not find end of body block');
});
});

View File

@@ -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<ShouldApplyWithReason> {
// 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<string> {
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@scope (:root) to (.unapi) {${after}\n}`;
}
}

View File

@@ -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<ShouldApplyWithReason> {
// 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<string> {
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}`;
}
}

View File

@@ -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<ShouldApplyWithReason> {
// 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<string> {
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);
}
}

View File

@@ -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<ShouldApplyWithReason> {
// 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<string> {
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}`;
}
}

View File

@@ -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<ShouldApplyWithReason> {
// 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<string> {
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}`;
}
}

View File

@@ -9,6 +9,17 @@ export default class NotificationsPageModification extends FileModification {
id: string = 'notifications-page';
public readonly filePath: string = '/usr/local/emhttp/plugins/dynamix/Notifications.page';
async shouldApply(): Promise<ShouldApplyWithReason> {
// 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<string> {
const fileContent = await readFile(this.filePath, 'utf-8');
@@ -18,12 +29,59 @@ export default class NotificationsPageModification extends FileModification {
}
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, '')
);
let newContent = 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, '');
// Add bottom-center and top-center position options if not present
const positionSelectStart = '<select name="position">';
const positionSelectEnd = '</select>';
const bottomCenterOption =
' <?=mk_option($notify[\'position\'], "bottom-center", _("bottom-center"))?>';
const topCenterOption = ' <?=mk_option($notify[\'position\'], "top-center", _("top-center"))?>';
if (newContent.includes(positionSelectStart) && !newContent.includes(bottomCenterOption)) {
newContent = newContent.replace(
'<?=mk_option($notify[\'position\'], "bottom-right", _("bottom-right"))?>',
'<?=mk_option($notify[\'position\'], "bottom-right", _("bottom-right"))?>\n' +
bottomCenterOption +
'\n' +
topCenterOption
);
}
// Add Stack/Duration/Max settings
const helpAnchor = ':notifications_display_position_help:';
const newSettings = `
:
_(Stack notifications)_:
: <select name="expand">
<?=mk_option($notify['expand'] ?? 'true', "true", _("Yes"))?>
<?=mk_option($notify['expand'] ?? 'true', "false", _("No"))?>
</select>
:notifications_stack_help:
_(Duration)_:
: <input type="number" name="duration" value="<?=$notify['duration'] ?? 5000?>" min="1000" step="500">
:notifications_duration_help:
_(Max notifications)_:
: <input type="number" name="max" value="<?=$notify['max'] ?? 3?>" min="1" max="10">
:notifications_max_help:
`;
if (newContent.includes(helpAnchor)) {
// Simple check to avoid duplicated insertion
if (!newContent.includes('_(Stack notifications)_:')) {
newContent = newContent.replace(helpAnchor, helpAnchor + newSettings);
}
}
return newContent;
}
}

View File

@@ -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<ShouldApplyWithReason> {
// 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<string> {
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);
}
}

View File

@@ -0,0 +1,198 @@
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<ShouldApplyWithReason> {
// 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<string> {
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 (ini_encode_value, build_ini_string, ini_decode_value)
// Similar to before, but we can just append them after safe_filename or clean_subject
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;
}`;
newContent = newContent.replace(insertPoint, insertPoint + '\n' + helperFunctions);
// 4. Update 'add' case initialization
const originalInit = `$noBrowser = false;`;
const newInit = `$noBrowser = false;
$customFilename = false;`;
newContent = newContent.replace(originalInit, newInit);
// 5. Update getopt
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;`;
const caseLWithU =
caseL +
`
case 'u':
$customFilename = $value;
break;`;
newContent = newContent.replace(caseL, caseLWithU);
// 7. Update 'add' logic (Replace filename generation and writing)
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','<br>',$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");`;
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','<br>',$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));
}`;
newContent = newContent.replace(originalWriteBlock, newWriteBlock);
// 8. Update 'get' case to use ini_decode_value
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);
}`;
newContent = newContent.replace(originalGetLoop, newGetLoop);
return this.createPatchWithDiff(overridePath ?? this.filePath, fileContent, newContent);
}
}

View File

@@ -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

View File

@@ -0,0 +1,39 @@
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,12 @@
padding: 0;
margin: 0;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
+
+@scope (:root) to (.unapi) {
@media (max-width: 1280px) {
#template {
min-width: 1260px;
max-width: 1260px;
margin: 0;
@@ -1521,11 +1523,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 +2218,7 @@
}
label.checkbox input:checked ~ .checkmark {
background-color: var(--brand-orange);
}
}
+
+}
\ No newline at end of file

View File

@@ -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

View File

@@ -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"

View File

@@ -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

View File

@@ -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

View File

@@ -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"
---
-<?PHP
-/* Copyright 2005-2023, Lime Technology
- * Copyright 2012-2023, Bergware International.
- * Copyright 2014-2021, Guilherme Jardim, Eric Schultz, Jon Panozzo.
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU General Public License version 2,
- * as published by the Free Software Foundation.
- *
- * The above copyright notice and this permission notice shall be included in
- * all copies or substantial portions of the Software.
- */
-?>
-<?
-require_once "$docroot/plugins/dynamix.docker.manager/include/DockerClient.php";
-
-$width = in_array($theme,['white','black']) ? -58: -44;
-$top = in_array($theme,['white','black']) ? 40 : 20;
-$busy = "<i class='fa fa-spin fa-circle-o-notch'></i> "._('Please wait')."... "._('starting up containers');
-$cpus = cpu_list();
-?>
-<link type="text/css" rel="stylesheet" href="<?autov('/webGui/styles/jquery.switchbutton.css')?>">
-
-<table id="docker_containers" class="tablesorter shift">
-<thead><tr><th><a id="resetsort" class="nohand" onclick="resetSorting()" title="_(Reset sorting)_"><i class="fa fa-th-list"></i></a>_(Application)_</th><th>_(Version)_</th><th>_(Network)_</th><th>_(Container IP)_</th><th>_(Container Port)_</th><th>_(LAN IP:Port)_</th><th>_(Volume Mappings)_ <small>(_(App to Host)_)</small></th><th class="load advanced">_(CPU & Memory load)_</th><th class="nine">_(Autostart)_</th><th class="five">_(Uptime)_</th></tr></thead>
-<tbody id="docker_list"><tr><td colspan='9'></td></tr></tbody>
-</table>
-<input type="button" onclick="addContainer()" value="_(Add Container)_" style="display:none">
-<input type="button" onclick="startAll()" value="_(Start All)_" style="display:none">
-<input type="button" onclick="stopAll()" value="_(Stop All)_" style="display:none">
-<input type="button" onclick="pauseAll()" value="_(Pause All)_" style="display:none">
-<input type="button" onclick="resumeAll()" value="_(Resume All)_" style="display:none">
-<input type="button" onclick="checkAll()" value="_(Check for Updates)_" id="checkAll" style="display:none">
-<input type="button" onclick="updateAll()" value="_(Update All)_" id="updateAll" style="display:none">
-<input type="button" onclick="contSizes()" value="_(Container Size)_" style="display:none">
-<div id="iframe-popup" style="display:none;-webkit-overflow-scrolling:touch;"></div>
-
-<script src="<?autov('/webGui/javascript/jquery.switchbutton.js')?>"></script>
-<script src="<?autov('/plugins/dynamix.docker.manager/javascript/docker.js')?>"></script>
-<script>
-var docker = [];
-<?if (!$tabbed):?>
-$('.title').append("<span id='busy' class='red-text strong' style='display:none;margin-left:40px'><?=$busy?></span>");
-<?else:?>
-$('.tabs').append("<span id='busy' class='red-text strong' style='display:none;position:relative;top:<?=$top?>px;left:40px;font-size:1.4rem;letter-spacing:2px'><?=$busy?></span>");
-<?endif;?>
-<?if (_var($display,'resize')):?>
-function resize() {
- $('#docker_list').height(Math.max(window.innerHeight-340,330));
- $('#docker_containers thead,#docker_containers tbody').removeClass('fixed');
- $('#docker_containers thead tr th').each(function(){$(this).width($(this).width());});
- $('#docker_containers tbody tr td').each(function(){$(this).width($(this).width());});
- $('#docker_containers thead,#docker_containers tbody').addClass('fixed');
-}
-<?endif;?>
-function resetSorting() {
- if ($.cookie('lockbutton')==null) return;
- $('input[type=button]').prop('disabled',true);
- $.post('/plugins/dynamix.docker.manager/include/UserPrefs.php',{reset:true},function(){loadlist();});
-}
-function listview() {
- var more = $.cookie('docker_listview_mode')=='advanced';
- <?if(($dockercfg['DOCKER_READMORE']??'yes') === 'yes'):?>
- $('.docker_readmore').readmore({maxHeight:32,moreLink:"<a href='#' style='text-align:center'><i class='fa fa-chevron-down'></i></a>",lessLink:"<a href='#' style='text-align:center'><i class='fa fa-chevron-up'></i></a>"});
- <?endif;?>
- $('input.autostart').each(function(){
- var wait = $('#'+$(this).prop('id').replace('auto','wait'));
- var auto = $(this).prop('checked');
- if (auto && more) wait.show(); else wait.hide();
- });
-}
-function LockButton() {
- if ($.cookie('lockbutton')==null) {
- $.cookie('lockbutton','lockbutton');
- $('#resetsort').removeClass('nohand').addClass('hand');
- $('i.mover').show();
- $('#docker_list .sortable').css({'cursor':'move'});
-<?if ($themes1):?>
- $('div.nav-item.LockButton').find('a').prop('title',"_(Lock sortable items)_");
- $('div.nav-item.LockButton').find('b').removeClass('icon-u-lock green-text').addClass('icon-u-lock-open red-text');
-<?endif;?>
- $('div.nav-item.LockButton').find('span').text("_(Lock sortable items)_");
- $('#docker_list').sortable({helper:'clone',items:'.sortable',cursor:'grab',axis:'y',containment:'parent',cancel:'span.docker_readmore,input',delay:100,opacity:0.5,zIndex:9999,forcePlaceholderSize:true,
- update:function(e,ui){
- var row = $('#docker_list').find('tr:first');
- var names = ''; var index = '';
- row.parent().children().find('td.ct-name').each(function(){names+=$(this).find('.appname').text()+';';index+=$(this).parent().parent().children().index($(this).parent())+';';});
- $.post('/plugins/dynamix.docker.manager/include/UserPrefs.php',{names:names,index:index});
- }});
- } else {
- $.removeCookie('lockbutton');
- $('#resetsort').removeClass('hand').addClass('nohand');
- $('i.mover').hide();
- $('#docker_list .sortable').css({'cursor':'default'});
-<?if ($themes1):?>
- $('div.nav-item.LockButton').find('a').prop('title',"_(Unlock sortable items)_");
- $('div.nav-item.LockButton').find('b').removeClass('icon-u-lock-open red-text').addClass('icon-u-lock green-text');
-<?endif;?>
- $('div.nav-item.LockButton').find('span').text("_(Unlock sortable items)_");
- $('#docker_list').sortable('destroy');
- }
-}
-function loadlist(init) {
- timers.docker = setTimeout(function(){$('div.spinner.fixed').show('slow');},500);
- docker = [];
- $.get('/plugins/dynamix.docker.manager/include/DockerContainers.php',function(d) {
- clearTimeout(timers.docker);
- var data = d.split(/\0/);
- $(".TS_tooltip").tooltipster("destroy");
- $('#docker_list').html(data[0]);
- $('.TS_tooltip').tooltipster({
- animation: 'fade',
- delay: 200,
- trigger: 'custom',
- triggerOpen: {click:true,touchstart:true,mouseenter:true},
- triggerClose:{click:true,scroll:false,mouseleave:true},
- interactive: true,
- viewportAware: true,
- contentAsHTML: true,
- functionBefore: function(instance,helper) {
- var origin = $(helper.origin);
- var TScontent = $(origin).attr("data-tstitle");
- instance.content(TScontent);
- }
- });
- $('head').append('<script>'+data[1]+'<\/script>');
-<?if (_var($display,'resize')):?>
- resize();
- if (init) $(window).bind('resize',function(){resize();});
-<?endif;?>
- $('.iconstatus').each(function(){
- if ($(this).hasClass('stopped')) $('div.'+$(this).prop('id')).hide();
- });
- $('.autostart').switchButton({labels_placement:'right', on_label:"_(On)_", off_label:"_(Off)_"});
- $('.autostart').change(function(){
- var more = $.cookie('docker_listview_mode')=='advanced';
- var wait = $('#'+$(this).prop('id').replace('auto','wait'));
- var auto = $(this).prop('checked');
- if (auto && more) wait.show(); else wait.hide();
- $.post('/plugins/dynamix.docker.manager/include/UpdateConfig.php',{action:'autostart',container:$(this).attr('container'),auto:auto,wait:wait.find('input.wait').val()});
- });
- $('input.wait').change(function(){
- $.post('/plugins/dynamix.docker.manager/include/UpdateConfig.php',{action:'wait',container:$(this).attr('container'),wait:$(this).val()});
- });
- if ($.cookie('docker_listview_mode')=='advanced') {$('.advanced').show(); $('.basic').hide();}
- $('input[type=button]').prop('disabled',false).show('slow');
- var update = false, rebuild = false;
- for (var i=0,ct; ct=docker[i]; i++) {
- if (ct.update==1) update = true;
- if (ct.update==2) rebuild = true;
- }
- listview();
- $('div.spinner.fixed').hide('slow');
- if (data[2]==1) {$('#busy').show(); setTimeout(loadlist,5000);} else if ($('#busy').is(':visible')) {$('#busy').hide(); setTimeout(loadlist,3000);}
- if (!update) $('input#updateAll').prop('disabled',true);
- if (rebuild) rebuildAll();
- });
-}
-function contSizes() {
- // show spinner over window
- $('div.spinner.fixed').css({'z-index':'100000'}).show();
- openPlugin('container_size', "_(Container Size)_");
-}
-var dockerload = new NchanSubscriber('/sub/dockerload',{subscriber:'websocket'});
-dockerload.on('message', function(msg){
- var data = msg.split('\n');
- for (var i=0,row; row=data[i]; i++) {
- var id = row.split(';');
- var w1 = Math.round(Math.min(id[1].slice(0,-1)/<?=count($cpus)*count(preg_split('/[,-]/',$cpus[0]))?>,100)*100)/100+'%';
- $('.cpu-'+id[0]).text(w1.replace('.','<?=_var($display,'number','.,')[0]?>'));
- $('.mem-'+id[0]).text(id[2]);
- $('#cpu-'+id[0]).css('width',w1);
- }
-});
-$(function() {
- $(".tabs").append('<span class="status"><span><input type="checkbox" class="advancedview"></span></span>');
- $('.advancedview').switchButton({labels_placement:'left', on_label:"_(Advanced View)_", off_label:"_(Basic View)_", checked:$.cookie('docker_listview_mode')=='advanced'});
- $('.advancedview').change(function(){
- $('.advanced').toggle('slow');
- $('.basic').toggle('slow');
- $.cookie('docker_listview_mode',$('.advancedview').is(':checked')?'advanced':'basic',{expires:3650});
- listview();
- });
- $.removeCookie('lockbutton');
- loadlist(true);
- dockerload.start().monitor();
-});
-
-</script>
+<div class="unapi">
+ <unraid-docker-container-overview></unraid-docker-container-overview>
+</div>

View File

@@ -2,11 +2,26 @@ 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)_):
: <input type="number" name="life" class="a" min="0" max="60" value="<?=$notify['life']?>"> _(a value of zero means no automatic closure)_
@@ -127,33 +127,36 @@
<?=mk_option($notify['position'], "bottom-right", _("bottom-right"))?>
<?=mk_option($notify['position'], "center", _("center"))?>
</select>
:notifications_auto_close_help:
:notifications_display_position_help:
+:
+ _(Stack notifications)_:
+: <select name="expand">
+ <?=mk_option($notify['expand'] ?? 'true', "true", _("Yes"))?>
+ <?=mk_option($notify['expand'] ?? 'true', "false", _("No"))?>
+ </select>
-_(Auto-close)_ (_(seconds)_):
-: <input type="number" name="life" class="a" min="0" max="60" value="<?=$notify['life']?>"> _(a value of zero means no automatic closure)_
+:notifications_stack_help:
-:notifications_auto_close_help:
+_(Duration)_:
+: <input type="number" name="duration" value="<?=$notify['duration'] ?? 5000?>" min="1000" step="500">
-_(Date format)_:
-: <select name="date" class="a">
@@ -14,17 +29,27 @@ Index: /usr/local/emhttp/plugins/dynamix/Notifications.page
- <?=mk_option($notify['date'], "m-d-Y", _("MM-DD-YYYY"))?>
- <?=mk_option($notify['date'], "Y-m-d", _("YYYY-MM-DD"))?>
- </select>
+:notifications_duration_help:
-:notifications_date_format_help:
-
+_(Max notifications)_:
+: <input type="number" name="max" value="<?=$notify['max'] ?? 3?>" min="1" max="10">
-_(Time format)_:
-: <select name="time" class="a">
- <?=mk_option($notify['time'], "h:i A", _("12 hours"))?>
- <?=mk_option($notify['time'], "H:i", _("24 hours"))?>
- </select>
-
+:notifications_max_help:
-:notifications_time_format_help:
-
+_(Auto-close)_ (_(seconds)_):
+: <input type="number" name="life" class="a" min="0" max="60" value="<?=$notify['life']?>"> _(a value of zero means no automatic closure)_
+
+:notifications_auto_close_help:
+
+
_(Store notifications to flash)_:
: <select name="path" class="a">
<?=mk_option($notify['path'], "/tmp/notifications", _("No"))?>

View File

@@ -0,0 +1,19 @@
Index: /usr/local/emhttp/plugins/dynamix/include/Notify.php
===================================================================
--- /usr/local/emhttp/plugins/dynamix/include/Notify.php original
+++ /usr/local/emhttp/plugins/dynamix/include/Notify.php modified
@@ -34,10 +34,14 @@
case 'd':
case 'i':
case 'm':
$notify .= " -{$option} ".escapeshellarg($value);
break;
+ case 'u':
+ $notify .= " -{$option} ".escapeshellarg($value);
+ break;
+
case 'x':
case 't':
$notify .= " -{$option}";
break;
}

View File

@@ -0,0 +1,172 @@
Index: /usr/local/emhttp/plugins/dynamix/scripts/notify
===================================================================
--- /usr/local/emhttp/plugins/dynamix/scripts/notify original
+++ /usr/local/emhttp/plugins/dynamix/scripts/notify modified
@@ -29,10 +29,11 @@
use -l to specify a link (clicking the notification will take you to that location)
use -x to create a single notification ticket
use -r to specify recipients and not use default
use -t to force send email only (for testing)
use -b to NOT send a browser notification
+ use -u to specify a custom filename (API use only)
all options are optional
notify init
Initialize the notification subsystem.
@@ -85,16 +86,17 @@
}
$body[] = "";
return mail($to, $subj, implode("\n", $body), implode("\n", $headers));
}
-function safe_filename($string) {
+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('~[^0-9a-z -_.]~i', '', $string);
$string = preg_replace('~[- ]~i', '_', $string);
- return trim($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 '&#8201;&#176;'
@@ -102,10 +104,42 @@
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));
@@ -176,12 +210,13 @@
$timestamp = time();
$ticket = $timestamp;
$mailtest = false;
$overrule = false;
$noBrowser = false;
+ $customFilename = false;
- $options = getopt("l:e:s:d:i:m:r:xtb");
+ $options = getopt("l:e:s:d:i:m:r:u:xtb");
foreach ($options as $option => $value) {
switch ($option) {
case 'e':
$event = $value;
break;
@@ -213,19 +248,53 @@
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;
}
}
- $unread = "{$unread}/".safe_filename("{$event}-{$ticket}.notify");
- $archive = "{$archive}/".safe_filename("{$event}-{$ticket}.notify");
+ 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;
- if (!$mailtest) file_put_contents($archive,"timestamp=$timestamp\nevent=$event\nsubject=$subject\ndescription=$description\nimportance=$importance\n".($message ? "message=".str_replace('\n','<br>',$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");
+ $cleanSubject = clean_subject($subject);
+ $archiveData = [
+ 'timestamp' => $timestamp,
+ 'event' => $event,
+ 'subject' => $cleanSubject,
+ 'description' => $description,
+ 'importance' => $importance,
+ ];
+ if ($message) $archiveData['message'] = str_replace('\n','<br>',$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('<br>','. ',$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':
@@ -239,13 +308,16 @@
$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,'');
+ # 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;}
- $output[$i][trim($key)] = trim($val);
+ # 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;

View File

@@ -0,0 +1,13 @@
Index: /usr/local/emhttp/plugins/dynamix/include/.set-password.php
===================================================================
--- /usr/local/emhttp/plugins/dynamix/include/.set-password.php original
+++ /usr/local/emhttp/plugins/dynamix/include/.set-password.php modified
@@ -410,7 +410,8 @@
$el.addEventListener('keyup', () => {
if (displayValidation) debounce(validate()); // Wait until displayValidation is swapped in a change event
});
});
</script>
+<?include "$docroot/plugins/dynamix.my.servers/include/welcome-modal.php"?>
</body>
</html>

View File

@@ -181,6 +181,7 @@ echo "Backing up original files..."
# Define files to backup in a shell variable
FILES_TO_BACKUP=(
"/usr/local/emhttp/plugins/dynamix/scripts/notify"
"/usr/local/emhttp/plugins/dynamix/DisplaySettings.page"
"/usr/local/emhttp/plugins/dynamix/Registration.page"
"/usr/local/emhttp/plugins/dynamix/include/DefaultPageLayout.php"
@@ -325,6 +326,7 @@ exit 0
# Define files to restore in a shell variable - must match backup list
FILES_TO_RESTORE=(
"/usr/local/emhttp/plugins/dynamix/scripts/notify"
"/usr/local/emhttp/plugins/dynamix/DisplaySettings.page"
"/usr/local/emhttp/plugins/dynamix/Registration.page"
"/usr/local/emhttp/plugins/dynamix/include/DefaultPageLayout.php"

View File

@@ -0,0 +1,376 @@
Menu="UserPreferences"
Type="xmenu"
Title="Notification Settings"
Icon="icon-notifications"
Tag="phone-square"
---
<?PHP
/* Copyright 2005-2023, Lime Technology
* Copyright 2012-2023, Bergware International.
* Copyright 2012, Andrew Hamer-Adams, http://www.pixeleyes.co.nz.
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License version 2,
* as published by the Free Software Foundation.
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*/
?>
<?
$events = explode('|', $notify['events'] ?? '');
$disabled = $notify['system'] ? '' : 'disabled';
?>
<script>
function prepareNotify(form) {
form.entity.value = form.normal1.checked | form.warning1.checked | form.alert1.checked;
form.normal.value = form.normal1.checked*1 + form.normal2.checked*2 + form.normal3.checked*4;
form.warning.value = form.warning1.checked*1 + form.warning2.checked*2 + form.warning3.checked*4;
form.alert.value = form.alert1.checked*1 + form.alert2.checked*2 + form.alert3.checked*4;
form.unraid.value = form.unraid1.checked*1 + form.unraid2.checked*2 + form.unraid3.checked*4;
form.plugin.value = form.plugin1.checked*1 + form.plugin2.checked*2 + form.plugin3.checked*4;
form.docker_notify.value = form.docker_notify1.checked*1 + form.docker_notify2.checked*2 + form.docker_notify3.checked*4;
form.language_notify.value = form.language_notify1.checked*1 + form.language_notify2.checked*2 + form.language_notify3.checked*4;
form.report.value = form.report1.checked*1 + form.report2.checked*2 + form.report3.checked*4;
form.normal1.disabled = true;
form.normal2.disabled = true;
form.normal3.disabled = true;
form.warning1.disabled = true;
form.warning2.disabled = true;
form.warning3.disabled = true;
form.alert1.disabled = true;
form.alert2.disabled = true;
form.alert3.disabled = true;
form.unraid1.disabled = true;
form.unraid2.disabled = true;
form.unraid3.disabled = true;
form.plugin1.disabled = true;
form.plugin2.disabled = true;
form.plugin3.disabled = true;
form.docker_notify1.disabled = true;
form.docker_notify2.disabled = true;
form.docker_notify3.disabled = true;
form.language_notify1.disabled = true;
form.language_notify2.disabled = true;
form.language_notify3.disabled = true;
form.report1.disabled = true;
form.report2.disabled = true;
form.report3.disabled = true;
}
function prepareSystem(index) {
if (index==0) $('.checkbox').attr('disabled','disabled'); else $('.checkbox').removeAttr('disabled');
}
function prepareTitle() {
var title = '_(Available notifications)_:';
$('#unraidTitle,#pluginTitle,#dockerTitle,#languageTitle,#reportTitle').html('&nbsp;');
if ($('.unraid').is(':visible')) {$('#unraidTitle').html(title); return;}
if ($('.plugin').is(':visible')) {$('#pluginTitle').html(title); return;}
if ($('.docker').is(':visible')) {$('#dockerTitle').html(title); return;}
if ($('.language').is(':visible')) {$('#languageTitle').html(title); return;}
if ($('.report').is(':visible')) {$('#reportTitle').html(title); return;}
}
function prepareUnraid(value) {
if (value=='') $('.unraid').hide(); else $('.unraid').show();
prepareTitle();
}
function preparePlugin(value) {
if (value=='') $('.plugin').hide(); else $('.plugin').show();
prepareTitle();
}
function prepareDocker(value) {
if (value=='') $('.docker').hide(); else $('.docker').show();
prepareTitle();
}
function prepareLanguage(value) {
if (value=='') $('.language').hide(); else $('.language').show();
prepareTitle();
}
function prepareReport(value) {
if (value=='') $('.report').hide(); else $('.report').show();
prepareTitle();
}
$(function(){
prepareUnraid(document.notify_settings.unraidos.value);
preparePlugin(document.notify_settings.version.value);
prepareDocker(document.notify_settings.docker_update.value);
prepareLanguage(document.notify_settings.language_update.value);
prepareReport(document.notify_settings.status.value);
});
</script>
<form markdown="1" name="notify_settings" method="POST" action="/update.php" target="progressFrame" onsubmit="prepareNotify(this)">
<input type="hidden" name="#file" value="dynamix/dynamix.cfg">
<input type="hidden" name="#section" value="notify">
<input type="hidden" name="#command" value="/webGui/scripts/notify">
<input type="hidden" name="#arg[1]" value="cron-init">
<input type="hidden" name="entity">
<input type="hidden" name="normal">
<input type="hidden" name="warning">
<input type="hidden" name="alert">
<input type="hidden" name="unraid">
<input type="hidden" name="plugin">
<input type="hidden" name="docker_notify">
<input type="hidden" name="language_notify">
<input type="hidden" name="report">
_(Display position)_:
: <select name="position">
<?=mk_option($notify['position'], "top-left", _("top-left"))?>
<?=mk_option($notify['position'], "top-right", _("top-right"))?>
<?=mk_option($notify['position'], "bottom-left", _("bottom-left"))?>
<?=mk_option($notify['position'], "bottom-right", _("bottom-right"))?>
<?=mk_option($notify['position'], "bottom-center", _("bottom-center"))?>
<?=mk_option($notify['position'], "top-center", _("top-center"))?>
</select>
:notifications_display_position_help:
_(Stack notifications)_:
: <select name="expand">
<?=mk_option($notify['expand'] ?? 'true', "true", _("Yes"))?>
<?=mk_option($notify['expand'] ?? 'true', "false", _("No"))?>
</select>
:notifications_stack_help:
_(Duration)_:
: <input type="number" name="duration" value="<?=$notify['duration'] ?? 5000?>" min="1000" step="500">
:notifications_duration_help:
_(Max notifications)_:
: <input type="number" name="max" value="<?=$notify['max'] ?? 3?>" min="1" max="10">
:notifications_max_help:
_(Date format)_:
: <select name="date">
<?=mk_option($notify['date'], "d-m-Y", _("DD-MM-YYYY"))?>
<?=mk_option($notify['date'], "m-d-Y", _("MM-DD-YYYY"))?>
<?=mk_option($notify['date'], "Y-m-d", _("YYYY-MM-DD"))?>
</select>
:notifications_date_format_help:
_(Time format)_:
: <select name="time">
<?=mk_option($notify['time'], "h:i A", _("12 hours"))?>
<?=mk_option($notify['time'], "H:i", _("24 hours"))?>
</select>
:notifications_time_format_help:
_(Store notifications to flash)_:
: <select name="path">
<?=mk_option($notify['path'], "/tmp/notifications", _("No"))?>
<?=mk_option($notify['path'], "/boot/config/plugins/dynamix/notifications", _("Yes"))?>
</select>
:notifications_store_flash_help:
_(System notifications)_:
: <select name="system" onchange="prepareSystem(this.selectedIndex)">
<?=mk_option($notify['system'], "", _("Disabled"))?>
<?=mk_option($notify['system'], "*/1 * * * *", _("Enabled"))?>
</select>
:notifications_system_help:
_(Unraid OS update notification)_:
: <select name="unraidos" onchange="prepareUnraid(this.value)">
<?=mk_option($notify['unraidos'], "", _("Never check"))?>
<?=mk_option($notify['unraidos'], "11 */6 * * *", _("Check four times a day"))?>
<?=mk_option($notify['unraidos'], "11 0,12 * * *", _("Check twice a day"))?>
<?=mk_option($notify['unraidos'], "11 0 * * *", _("Check once a day"))?>
<?=mk_option($notify['unraidos'], "11 0 * * 1", _("Check once a week"))?>
<?=mk_option($notify['unraidos'], "11 0 1 * *", _("Check once a month"))?>
</select>
:notifications_os_update_help:
_(Plugins update notification)_:
: <select name="version" onchange="preparePlugin(this.value)">
<?=mk_option($notify['version'], "", _("Never check"))?>
<?=mk_option($notify['version'], "10 */6 * * *", _("Check four times a day"))?>
<?=mk_option($notify['version'], "10 0,12 * * *", _("Check twice a day"))?>
<?=mk_option($notify['version'], "10 0 * * *", _("Check once a day"))?>
<?=mk_option($notify['version'], "10 0 * * 1", _("Check once a week"))?>
<?=mk_option($notify['version'], "10 0 1 * *", _("Check once a month"))?>
</select>
:notifications_plugins_update_help:
_(Docker update notification)_:
: <select name="docker_update" onchange="prepareDocker(this.value)">
<?=mk_option($notify['docker_update'], "", _("Never check"))?>
<?=mk_option($notify['docker_update'], "10 */6 * * *", _("Check four times a day"))?>
<?=mk_option($notify['docker_update'], "10 0,12 * * *", _("Check twice a day"))?>
<?=mk_option($notify['docker_update'], "10 0 * * *", _("Check once a day"))?>
<?=mk_option($notify['docker_update'], "10 0 * * 1", _("Check once a week"))?>
<?=mk_option($notify['docker_update'], "10 0 1 * *", _("Check once a month"))?>
</select>
:notifications_docker_update_help:
_(Language update notification)_:
: <select name="language_update" onchange="prepareLanguage(this.value)">
<?=mk_option($notify['language_update'], "", _("Never check"))?>
<?=mk_option($notify['language_update'], "10 */6 * * *", _("Check four times a day"))?>
<?=mk_option($notify['language_update'], "10 0,12 * * *", _("Check twice a day"))?>
<?=mk_option($notify['language_update'], "10 0 * * *", _("Check once a day"))?>
<?=mk_option($notify['language_update'], "10 0 * * 1", _("Check once a week"))?>
<?=mk_option($notify['language_update'], "10 0 1 * *", _("Check once a month"))?>
</select>
_(Array status notification)_:
: <select name="status" onchange="prepareReport(this.value)">
<?=mk_option($notify['status'], "", _("Never send"))?>
<?=mk_option($notify['status'], "20 * * * *", _("Send every hour"))?>
<?=mk_option($notify['status'], "20 */2 * * *", _("Send every two hours"))?>
<?=mk_option($notify['status'], "20 */6 * * *", _("Send four times a day"))?>
<?=mk_option($notify['status'], "20 */8 * * *", _("Send three times a day"))?>
<?=mk_option($notify['status'], "20 0,12 * * *", _("Send twice a day"))?>
<?=mk_option($notify['status'], "20 0 * * *", _("Send once a day"))?>
<?=mk_option($notify['status'], "20 0 * * 1", _("Send once a week"))?>
<?=mk_option($notify['status'], "20 0 1 * *", _("Send once a month"))?>
</select>
:notifications_array_status_help:
<span id="unraidTitle" class="unraid" style="display:none">&nbsp;</span>
: <span class="unraid" style="display:none">
<span class="notifications-checkbox-group-title">_(Unraid OS update)_</span>
<span class="inline-block">
<label class="inline-block">
<input type="checkbox" name="unraid1"<?=($notify['unraid'] & 1)==1 ? ' checked' : ''?>>_(Browser)_ &nbsp;
</label>
<label class="inline-block">
<input type="checkbox" name="unraid2"<?=($notify['unraid'] & 2)==2 ? ' checked' : ''?>>_(Email)_ &nbsp;
</label>
<label class="inline-block">
<input type="checkbox" name="unraid3"<?=($notify['unraid'] & 4)==4 ? ' checked' : ''?>>_(Agents)_ &nbsp;
</label>
</span>
</span>
<span id="pluginTitle" class="plugin" style="display:none">&nbsp;</span>
: <span class="plugin" style="display:none">
<span class="notifications-checkbox-group-title">_(Plugins update)_</span>
<span class="inline-block">
<label class="inline-block">
<input type="checkbox" name="plugin1"<?=($notify['plugin'] & 1)==1 ? ' checked' : ''?>>_(Browser)_ &nbsp;
</label>
<label class="inline-block">
<input type="checkbox" name="plugin2"<?=($notify['plugin'] & 2)==2 ? ' checked' : ''?>>_(Email)_ &nbsp;
</label>
<label class="inline-block">
<input type="checkbox" name="plugin3"<?=($notify['plugin'] & 4)==4 ? ' checked' : ''?>>_(Agents)_ &nbsp;
</label>
</span>
</span>
<span id="dockerTitle" class="docker" style="display:none">&nbsp;</span>
: <span class="docker" style="display:none">
<span class="notifications-checkbox-group-title">_(Docker update)_</span>
<span class="inline-block">
<label class="inline-block">
<input type="checkbox" name="docker_notify1"<?=($notify['docker_notify'] & 1)==1 ? ' checked' : ''?>>_(Browser)_ &nbsp;
</label>
<label class="inline-block">
<input type="checkbox" name="docker_notify2"<?=($notify['docker_notify'] & 2)==2 ? ' checked' : ''?>>_(Email)_ &nbsp;
</label>
<label class="inline-block">
<input type="checkbox" name="docker_notify3"<?=($notify['docker_notify'] & 4)==4 ? ' checked' : ''?>>_(Agents)_ &nbsp;
</label>
</span>
</span>
<span id="languageTitle" class="language" style="display:none">&nbsp;</span>
: <span class="language" style="display:none">
<span class="notifications-checkbox-group-title">_(Language update)_</span>
<span class="inline-block">
<label class="inline-block">
<input type="checkbox" name="language_notify1"<?=($notify['language_notify'] & 1)==1 ? ' checked' : ''?>>_(Browser)_ &nbsp;
</label>
<label class="inline-block">
<input type="checkbox" name="language_notify2"<?=($notify['language_notify'] & 2)==2 ? ' checked' : ''?>>_(Email)_ &nbsp;
</label>
<label class="inline-block">
<input type="checkbox" name="language_notify3"<?=($notify['language_notify'] & 4)==4 ? ' checked' : ''?>>_(Agents)_ &nbsp;
</label>
</span>
</span>
<span id="reportTitle" class="report" style="display:none">&nbsp;</span>
: <span class="report" style="display:none">
<span class="notifications-checkbox-group-title">_(Array status)_</span>
<span>
<span>
<input type="checkbox" name="report1"<?=($notify['report'] & 1)==1 ? ' checked' : ''?>>_(Browser)_ &nbsp;
</span>
<span>
<input type="checkbox" name="report2"<?=($notify['report'] & 2)==2 ? ' checked' : ''?>>_(Email)_ &nbsp;
</span>
<span>
<input type="checkbox" name="report3"<?=($notify['report'] & 4)==4 ? ' checked' : ''?>>_(Agents)_ &nbsp;
</span>
</span>
</span>
</span>
:notifications_agent_selection_help:
_(Notification entity)_:
: <span>
<span class="notifications-checkbox-group-title">_(Notices)_</span>
<span class="inline-block">
<label class="inline-block">
<input type="checkbox" class="checkbox" name="normal1"<?=($notify['normal'] & 1)==1 ? " checked $disabled" : $disabled?>>_(Browser)_ &nbsp;
</label>
<label class="inline-block">
<input type="checkbox" class="checkbox" name="normal2"<?=($notify['normal'] & 2)==2 ? " checked $disabled" : $disabled?>>_(Email)_ &nbsp;
</label>
<label class="inline-block">
<input type="checkbox" class="checkbox" name="normal3"<?=($notify['normal'] & 4)==4 ? " checked $disabled" : $disabled?>>_(Agents)_ &nbsp;
</label>
</span>
</span>
&nbsp;
: <span>
<span class="notifications-checkbox-group-title">_(Warnings)_</span>
<span class="inline-block">
<label class="inline-block">
<input type="checkbox" class="checkbox" name="warning1"<?=($notify['warning'] & 1)==1 ? " checked $disabled" : $disabled?>>_(Browser)_ &nbsp;
</label>
<label class="inline-block">
<input type="checkbox" class="checkbox" name="warning2"<?=($notify['warning'] & 2)==2 ? " checked $disabled" : $disabled?>>_(Email)_ &nbsp;
</label>
<label class="inline-block">
<input type="checkbox" class="checkbox" name="warning3"<?=($notify['warning'] & 4)==4 ? " checked $disabled" : $disabled?>>_(Agents)_ &nbsp;
</label>
</span>
</span>
&nbsp;
: <span>
<span class="notifications-checkbox-group-title">_(Alerts)_</span>
<span>
<span>
<input type="checkbox" class="checkbox" name="alert1"<?=($notify['alert'] & 1)==1 ? " checked $disabled" : $disabled?>>_(Browser)_ &nbsp;
</span>
<span>
<input type="checkbox" class="checkbox" name="alert2"<?=($notify['alert'] & 2)==2 ? " checked $disabled" : $disabled?>>_(Email)_ &nbsp;
</span>
<span>
<input type="checkbox" class="checkbox" name="alert3"<?=($notify['alert'] & 4)==4 ? " checked $disabled" : $disabled?>>_(Agents)_ &nbsp;
</span>
</span>
:notifications_classification_help:
<input type="submit" name="#default" value="_(Default)_">
: <span class="inline-block">
<input type="submit" name="#apply" value="_(Apply)_" disabled>
<input type="button" value="_(Done)_" onclick="done()">
</span>
</form>

View File

@@ -0,0 +1,60 @@
<?PHP
/* Copyright 2005-2023, Lime Technology
* Copyright 2012-2023, Bergware International.
* Copyright 2012, Andrew Hamer-Adams, http://www.pixeleyes.co.nz.
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License version 2,
* as published by the Free Software Foundation.
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*/
?>
<?
$docroot ??= ($_SERVER['DOCUMENT_ROOT'] ?: '/usr/local/emhttp');
$notify = "$docroot/webGui/scripts/notify";
switch ($_POST['cmd']??'') {
case 'init':
shell_exec("$notify init");
break;
case 'smtp-init':
shell_exec("$notify smtp-init");
break;
case 'cron-init':
shell_exec("$notify cron-init");
break;
case 'add':
foreach ($_POST as $option => $value) {
switch ($option) {
case 'e':
case 's':
case 'd':
case 'i':
case 'm':
case 'u':
$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;
}
?>

View File

@@ -0,0 +1,350 @@
#!/usr/bin/php -q
<?PHP
/* Copyright 2005-2023, Lime Technology
* Copyright 2012-2023, Bergware International.
* Copyright 2012, Andrew Hamer-Adams, http://www.pixeleyes.co.nz.
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License version 2,
* as published by the Free Software Foundation.
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*/
?>
<?
$docroot ??= ($_SERVER['DOCUMENT_ROOT'] ?: '/usr/local/emhttp');
require_once "$docroot/webGui/include/Wrappers.php";
require_once "$docroot/webGui/include/Encryption.php";
function usage()
{
echo <<<EOT
notify [-e "event"] [-s "subject"] [-d "description"] [-i "normal|warning|alert"] [-m "message"] [-x] [-t] [-b] [add]
create a notification
use -e to specify the event
use -s to specify a subject
use -d to specify a short description
use -i to specify the severity
use -m to specify a message (long description)
use -l to specify a link (clicking the notification will take you to that location)
use -x to create a single notification ticket
use -r to specify recipients and not use default
use -t to force send email only (for testing)
use -b to NOT send a browser notification
use -u to specify a custom filename (API use only)
all options are optional
notify init
Initialize the notification subsystem.
notify smtp-init
Initialize sendmail configuration (ssmtp in our case).
notify get
Output a json-encoded list of all the unread notifications.
notify archive file
Move file from 'unread' state to 'archive' state.
EOT;
return 1;
}
function generate_email($event, $subject, $description, $importance, $message, $recipients, $fqdnlink)
{
global $ssmtp;
$rcpt = $ssmtp['RcptTo'];
if (!$recipients)
$to = implode(',', explode(' ', trim($rcpt)));
else
$to = $recipients;
if (empty($to)) return;
$subj = "{$ssmtp['Subject']}$subject";
$headers = [];
$headers[] = "MIME-Version: 1.0";
$headers[] = "X-Mailer: PHP/" . phpversion();
$headers[] = "Content-type: text/plain; charset=utf-8";
$headers[] = "From: {$ssmtp['root']}";
$headers[] = "Reply-To: {$ssmtp['root']}";
if (($importance == "warning" || $importance == "alert") && $ssmtp['SetEmailPriority'] == "True") {
$headers[] = "X-Priority: 1 (highest)";
$headers[] = "X-Mms-Priority: High";
}
$headers[] = "";
$body = [];
if (!empty($fqdnlink)) {
$body[] = "Link: $fqdnlink";
$body[] = "";
}
$body[] = "Event: $event";
$body[] = "Subject: $subject";
$body[] = "Description: $description";
$body[] = "Importance: $importance";
if (!empty($message)) {
$body[] = "";
foreach (explode('\n', $message) as $line)
$body[] = $line;
}
$body[] = "";
return mail($to, $subj, implode("\n", $body), implode("\n", $headers));
}
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);
}
/*
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 '&#8201;&#176;'
*/
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', '<br>', $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, $cleanSubject, str_replace('<br>', '. ', $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($cleanSubject) . " 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);
?>

2279
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,8 +0,0 @@
/* eslint-disable no-var */
declare global {
/** loaded by Toaster.vue */
var toast: (typeof import('vue-sonner'))['toast'];
}
// an export or import statement is required to make this file a module
export {};

View File

@@ -1,3 +1,16 @@
{
"prettier.configPath": "./.prettierrc.mjs"
"prettier.configPath": "./.prettierrc.mjs",
"files.associations": {
"*.css": "tailwindcss"
},
"editor.quickSuggestions": {
"strings": "on"
},
"tailwindCSS.classAttributes": ["class", "ui"],
"tailwindCSS.experimental.classRegex": [
["ui:\\s*{([^)]*)\\s*}", "(?:'|\"|`)([^']*)(?:'|\"|`)"]
]
}

View File

@@ -65,6 +65,9 @@ const mockLocation = {
set href(value: string) {
mockLocationHref = value;
},
assign: vi.fn((url: string) => {
mockLocationHref = url;
}),
};
vi.stubGlobal('location', mockLocation);
vi.stubGlobal('URLSearchParams', URLSearchParams);
@@ -261,7 +264,7 @@ describe('SsoButtons', () => {
const redirectUri = `${mockLocation.origin}/graphql/api/auth/oidc/callback`;
const expectedUrl = `/graphql/api/auth/oidc/authorize/unraid-net?state=${encodeURIComponent(generatedState)}&redirect_uri=${encodeURIComponent(redirectUri)}`;
expect(mockLocation.href).toBe(expectedUrl);
expect(mockLocation.assign).toHaveBeenCalledWith(expectedUrl);
});
it('handles OIDC callback with token successfully', async () => {
@@ -383,7 +386,7 @@ describe('SsoButtons', () => {
// Should redirect to the OIDC callback endpoint
const expectedUrl = `/graphql/api/auth/oidc/callback?code=${encodeURIComponent(mockCode)}&state=${encodeURIComponent(mockState)}`;
expect(mockLocation.href).toBe(expectedUrl);
expect(mockLocation.assign).toHaveBeenCalledWith(expectedUrl);
});
it('handles HTTPS with non-standard port correctly', async () => {
@@ -430,7 +433,7 @@ describe('SsoButtons', () => {
const redirectUri = 'https://unraid.mytailnet.ts.net:1443/graphql/api/auth/oidc/callback';
const expectedUrl = `/graphql/api/auth/oidc/authorize/tsidp?state=${encodeURIComponent(generatedState)}&redirect_uri=${encodeURIComponent(redirectUri)}`;
expect(mockLocation.href).toBe(expectedUrl);
expect(mockLocation.assign).toHaveBeenCalledWith(expectedUrl);
// Reset location mock for other tests
mockLocation.protocol = 'http:';

View File

@@ -42,6 +42,7 @@ vi.mock('@vueuse/core', () => ({
}
return ref(storage.get(key) ?? initialValue);
},
createSharedComposable: (fn: unknown) => fn,
}));
vi.mock('@unraid/ui', () => ({

View File

@@ -135,6 +135,8 @@ describe('mount-engine', () => {
// Clean up DOM
document.body.innerHTML = '';
lastUAppPortal = undefined;
mockApolloClient.query.mockResolvedValue({ data: { notifications: { settings: {} } } });
});
afterEach(() => {

View File

@@ -137,6 +137,7 @@ describe('UpdateOs Store', () => {
...originalLocation,
origin: 'https://littlebox.tail45affd.ts.net',
href: 'https://littlebox.tail45affd.ts.net/Plugins',
assign: vi.fn(),
},
});
@@ -217,6 +218,9 @@ describe('UpdateOs Store', () => {
set href(value) {
hrefValue = value;
},
assign: vi.fn((value) => {
hrefValue = value;
}),
},
});

View File

@@ -1,13 +1,63 @@
// Objective: avoid hard-coded custom colors wherever possible, letting our theme system manage
// styling consistently. During the migration from the legacy WebGUI, some components still depend
// on specific colors to maintain visual continuity. This config file centralizes all temporary
// overrides required for that transition.
//
// Pending migration cleanup:
// - Notifications/Sidebar.vue → notification bell has temporary custom hover color to match legacy styles.
export default {
ui: {
colors: {
primary: 'blue',
neutral: 'gray',
// overrided by tailwind-shared/css-variables.css
// these shared tailwind styles and colors are imported in src/assets/main.css
},
// https://ui.nuxt.com/docs/components/button#theme
button: {
//keep in mind, there is a "variant" AND a "variants" property
variants: {
variant: {
ghost: '',
link: 'hover:underline focus:underline',
},
},
},
// https://ui.nuxt.com/docs/components/tabs#theme
tabs: {
variants: {
pill: {},
},
},
// https://ui.nuxt.com/docs/components/slideover#theme
slideover: {
slots: {
// title: 'text-3xl font-normal',
},
variants: {
right: {},
},
},
//css theming/style-overrides for the toast component
// https://ui.nuxt.com/docs/components/toast#theme
toast: {
slots: {
title: 'truncate', // can also use break-words instead of truncating
description: 'truncate',
},
},
// fallback, overridden by webgui settings
// Also, for toasts, BUT this is imported in the Root UApp in mount-engine.ts
// https://ui.nuxt.com/docs/components/toast#examples
toaster: {
position: 'top-right' as const,
expand: true,
duration: 5000,
max: 3,
},
},
toaster: {
position: 'bottom-right' as const,
expand: true,
duration: 5000,
},
};

View File

@@ -13,14 +13,17 @@ declare global {
const extractShortcuts: typeof import('../node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.4_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_655bac6707ae017754653173419b3890/node_modules/@nuxt/ui/dist/runtime/composables/defineShortcuts.js')['extractShortcuts']
const fieldGroupInjectionKey: typeof import('../node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.4_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_655bac6707ae017754653173419b3890/node_modules/@nuxt/ui/dist/runtime/composables/useFieldGroup.js')['fieldGroupInjectionKey']
const formBusInjectionKey: typeof import('../node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.4_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_655bac6707ae017754653173419b3890/node_modules/@nuxt/ui/dist/runtime/composables/useFormField.js')['formBusInjectionKey']
const formErrorsInjectionKey: typeof import('../node_modules/.pnpm/@nuxt+ui@4.2.1_@babel+parser@7.28.4_@netlify+blobs@9.1.2_change-case@5.4.4_db0@0.3.2_em_abe87a60859daf93a7fe8018ff1a0969/node_modules/@nuxt/ui/dist/runtime/composables/useFormField.js').formErrorsInjectionKey
const formFieldInjectionKey: typeof import('../node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.4_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_655bac6707ae017754653173419b3890/node_modules/@nuxt/ui/dist/runtime/composables/useFormField.js')['formFieldInjectionKey']
const formInputsInjectionKey: typeof import('../node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.4_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_655bac6707ae017754653173419b3890/node_modules/@nuxt/ui/dist/runtime/composables/useFormField.js')['formInputsInjectionKey']
const formLoadingInjectionKey: typeof import('../node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.4_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_655bac6707ae017754653173419b3890/node_modules/@nuxt/ui/dist/runtime/composables/useFormField.js')['formLoadingInjectionKey']
const formOptionsInjectionKey: typeof import('../node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.4_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_655bac6707ae017754653173419b3890/node_modules/@nuxt/ui/dist/runtime/composables/useFormField.js')['formOptionsInjectionKey']
const formStateInjectionKey: typeof import('../node_modules/.pnpm/@nuxt+ui@4.2.1_@babel+parser@7.28.4_@netlify+blobs@9.1.2_change-case@5.4.4_db0@0.3.2_em_abe87a60859daf93a7fe8018ff1a0969/node_modules/@nuxt/ui/dist/runtime/composables/useFormField.js').formStateInjectionKey
const inputIdInjectionKey: typeof import('../node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.4_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_655bac6707ae017754653173419b3890/node_modules/@nuxt/ui/dist/runtime/composables/useFormField.js')['inputIdInjectionKey']
const kbdKeysMap: typeof import('../node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.4_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_655bac6707ae017754653173419b3890/node_modules/@nuxt/ui/dist/runtime/composables/useKbd.js')['kbdKeysMap']
const localeContextInjectionKey: typeof import('../node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.4_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_655bac6707ae017754653173419b3890/node_modules/@nuxt/ui/dist/runtime/composables/useLocale.js')['localeContextInjectionKey']
const portalTargetInjectionKey: typeof import('../node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.4_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_655bac6707ae017754653173419b3890/node_modules/@nuxt/ui/dist/runtime/composables/usePortal.js')['portalTargetInjectionKey']
const toastMaxInjectionKey: typeof import('../node_modules/.pnpm/@nuxt+ui@4.2.1_@babel+parser@7.28.4_@netlify+blobs@9.1.2_change-case@5.4.4_db0@0.3.2_em_abe87a60859daf93a7fe8018ff1a0969/node_modules/@nuxt/ui/dist/runtime/composables/useToast.js').toastMaxInjectionKey
const useAppConfig: typeof import('../node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.4_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_655bac6707ae017754653173419b3890/node_modules/@nuxt/ui/dist/runtime/vue/composables/useAppConfig.js')['useAppConfig']
const useAvatarGroup: typeof import('../node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.4_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_655bac6707ae017754653173419b3890/node_modules/@nuxt/ui/dist/runtime/composables/useAvatarGroup.js')['useAvatarGroup']
const useComponentIcons: typeof import('../node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.4_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_655bac6707ae017754653173419b3890/node_modules/@nuxt/ui/dist/runtime/composables/useComponentIcons.js')['useComponentIcons']

31
web/components.d.ts vendored
View File

@@ -129,32 +129,33 @@ declare module 'vue' {
'ThemeSwitcher.standalone': typeof import('./src/components/ThemeSwitcher.standalone.vue')['default']
ThirdPartyDrivers: typeof import('./src/components/UpdateOs/ThirdPartyDrivers.vue')['default']
Trial: typeof import('./src/components/UserProfile/Trial.vue')['default']
UAlert: typeof import('./../node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.4_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_655bac6707ae017754653173419b3890/node_modules/@nuxt/ui/dist/runtime/components/Alert.vue')['default']
UBadge: typeof import('./../node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.4_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_655bac6707ae017754653173419b3890/node_modules/@nuxt/ui/dist/runtime/components/Badge.vue')['default']
UAlert: typeof import('./../node_modules/.pnpm/@nuxt+ui@4.2.1_@babel+parser@7.28.4_@netlify+blobs@9.1.2_change-case@5.4.4_db0@0.3.2_em_abe87a60859daf93a7fe8018ff1a0969/node_modules/@nuxt/ui/dist/runtime/components/Alert.vue')['default']
UBadge: typeof import('./../node_modules/.pnpm/@nuxt+ui@4.2.1_@babel+parser@7.28.4_@netlify+blobs@9.1.2_change-case@5.4.4_db0@0.3.2_em_abe87a60859daf93a7fe8018ff1a0969/node_modules/@nuxt/ui/dist/runtime/components/Badge.vue')['default']
UButton: typeof import('./../node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.4_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_655bac6707ae017754653173419b3890/node_modules/@nuxt/ui/dist/runtime/components/Button.vue')['default']
UCard: typeof import('./../node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.4_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_655bac6707ae017754653173419b3890/node_modules/@nuxt/ui/dist/runtime/components/Card.vue')['default']
UCheckbox: typeof import('./../node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.4_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_655bac6707ae017754653173419b3890/node_modules/@nuxt/ui/dist/runtime/components/Checkbox.vue')['default']
UDrawer: typeof import('./../node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.4_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_655bac6707ae017754653173419b3890/node_modules/@nuxt/ui/dist/runtime/components/Drawer.vue')['default']
UDropdownMenu: typeof import('./../node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.4_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_655bac6707ae017754653173419b3890/node_modules/@nuxt/ui/dist/runtime/components/DropdownMenu.vue')['default']
UFormField: typeof import('./../node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.4_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_655bac6707ae017754653173419b3890/node_modules/@nuxt/ui/dist/runtime/components/FormField.vue')['default']
UCard: typeof import('./../node_modules/.pnpm/@nuxt+ui@4.2.1_@babel+parser@7.28.4_@netlify+blobs@9.1.2_change-case@5.4.4_db0@0.3.2_em_abe87a60859daf93a7fe8018ff1a0969/node_modules/@nuxt/ui/dist/runtime/components/Card.vue')['default']
UCheckbox: typeof import('./../node_modules/.pnpm/@nuxt+ui@4.2.1_@babel+parser@7.28.4_@netlify+blobs@9.1.2_change-case@5.4.4_db0@0.3.2_em_abe87a60859daf93a7fe8018ff1a0969/node_modules/@nuxt/ui/dist/runtime/components/Checkbox.vue')['default']
UDrawer: typeof import('./../node_modules/.pnpm/@nuxt+ui@4.2.1_@babel+parser@7.28.4_@netlify+blobs@9.1.2_change-case@5.4.4_db0@0.3.2_em_abe87a60859daf93a7fe8018ff1a0969/node_modules/@nuxt/ui/dist/runtime/components/Drawer.vue')['default']
UDropdownMenu: typeof import('./../node_modules/.pnpm/@nuxt+ui@4.2.1_@babel+parser@7.28.4_@netlify+blobs@9.1.2_change-case@5.4.4_db0@0.3.2_em_abe87a60859daf93a7fe8018ff1a0969/node_modules/@nuxt/ui/dist/runtime/components/DropdownMenu.vue')['default']
UFormField: typeof import('./../node_modules/.pnpm/@nuxt+ui@4.2.1_@babel+parser@7.28.4_@netlify+blobs@9.1.2_change-case@5.4.4_db0@0.3.2_em_abe87a60859daf93a7fe8018ff1a0969/node_modules/@nuxt/ui/dist/runtime/components/FormField.vue')['default']
UIcon: typeof import('./../node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.4_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_655bac6707ae017754653173419b3890/node_modules/@nuxt/ui/dist/runtime/vue/components/Icon.vue')['default']
UInput: typeof import('./../node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.4_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_655bac6707ae017754653173419b3890/node_modules/@nuxt/ui/dist/runtime/components/Input.vue')['default']
UModal: typeof import('./../node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.4_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_655bac6707ae017754653173419b3890/node_modules/@nuxt/ui/dist/runtime/components/Modal.vue')['default']
UNavigationMenu: typeof import('./../node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.4_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_655bac6707ae017754653173419b3890/node_modules/@nuxt/ui/dist/runtime/components/NavigationMenu.vue')['default']
UnraidToaster: typeof import('./src/components/UnraidToaster.vue')['default']
UInput: typeof import('./../node_modules/.pnpm/@nuxt+ui@4.2.1_@babel+parser@7.28.4_@netlify+blobs@9.1.2_change-case@5.4.4_db0@0.3.2_em_abe87a60859daf93a7fe8018ff1a0969/node_modules/@nuxt/ui/dist/runtime/components/Input.vue')['default']
UModal: typeof import('./../node_modules/.pnpm/@nuxt+ui@4.2.1_@babel+parser@7.28.4_@netlify+blobs@9.1.2_change-case@5.4.4_db0@0.3.2_em_abe87a60859daf93a7fe8018ff1a0969/node_modules/@nuxt/ui/dist/runtime/components/Modal.vue')['default']
UNavigationMenu: typeof import('./../node_modules/.pnpm/@nuxt+ui@4.2.1_@babel+parser@7.28.4_@netlify+blobs@9.1.2_change-case@5.4.4_db0@0.3.2_em_abe87a60859daf93a7fe8018ff1a0969/node_modules/@nuxt/ui/dist/runtime/components/NavigationMenu.vue')['default']
Update: typeof import('./src/components/UpdateOs/Update.vue')['default']
UpdateExpiration: typeof import('./src/components/Registration/UpdateExpiration.vue')['default']
UpdateExpirationAction: typeof import('./src/components/Registration/UpdateExpirationAction.vue')['default']
UpdateIneligible: typeof import('./src/components/UpdateOs/UpdateIneligible.vue')['default']
'UpdateOs.standalone': typeof import('./src/components/UpdateOs.standalone.vue')['default']
UPopover: typeof import('./../node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.4_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_655bac6707ae017754653173419b3890/node_modules/@nuxt/ui/dist/runtime/components/Popover.vue')['default']
UPopover: typeof import('./../node_modules/.pnpm/@nuxt+ui@4.2.1_@babel+parser@7.28.4_@netlify+blobs@9.1.2_change-case@5.4.4_db0@0.3.2_em_abe87a60859daf93a7fe8018ff1a0969/node_modules/@nuxt/ui/dist/runtime/components/Popover.vue')['default']
UptimeExpire: typeof import('./src/components/UserProfile/UptimeExpire.vue')['default']
USelectMenu: typeof import('./../node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.4_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_655bac6707ae017754653173419b3890/node_modules/@nuxt/ui/dist/runtime/components/SelectMenu.vue')['default']
USelectMenu: typeof import('./../node_modules/.pnpm/@nuxt+ui@4.2.1_@babel+parser@7.28.4_@netlify+blobs@9.1.2_change-case@5.4.4_db0@0.3.2_em_abe87a60859daf93a7fe8018ff1a0969/node_modules/@nuxt/ui/dist/runtime/components/SelectMenu.vue')['default']
'UserProfile.standalone': typeof import('./src/components/UserProfile.standalone.vue')['default']
USkeleton: typeof import('./../node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.4_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_655bac6707ae017754653173419b3890/node_modules/@nuxt/ui/dist/runtime/components/Skeleton.vue')['default']
USwitch: typeof import('./../node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.4_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_655bac6707ae017754653173419b3890/node_modules/@nuxt/ui/dist/runtime/components/Switch.vue')['default']
UTable: typeof import('./../node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.4_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_655bac6707ae017754653173419b3890/node_modules/@nuxt/ui/dist/runtime/components/Table.vue')['default']
USlideover: typeof import('./../node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.4_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_655bac6707ae017754653173419b3890/node_modules/@nuxt/ui/dist/runtime/components/Slideover.vue')['default']
USwitch: typeof import('./../node_modules/.pnpm/@nuxt+ui@4.2.1_@babel+parser@7.28.4_@netlify+blobs@9.1.2_change-case@5.4.4_db0@0.3.2_em_abe87a60859daf93a7fe8018ff1a0969/node_modules/@nuxt/ui/dist/runtime/components/Switch.vue')['default']
UTable: typeof import('./../node_modules/.pnpm/@nuxt+ui@4.2.1_@babel+parser@7.28.4_@netlify+blobs@9.1.2_change-case@5.4.4_db0@0.3.2_em_abe87a60859daf93a7fe8018ff1a0969/node_modules/@nuxt/ui/dist/runtime/components/Table.vue')['default']
UTabs: typeof import('./../node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.4_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_655bac6707ae017754653173419b3890/node_modules/@nuxt/ui/dist/runtime/components/Tabs.vue')['default']
UTooltip: typeof import('./../node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.4_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_655bac6707ae017754653173419b3890/node_modules/@nuxt/ui/dist/runtime/components/Tooltip.vue')['default']
'WanIpCheck.standalone': typeof import('./src/components/WanIpCheck.standalone.vue')['default']
'WelcomeModal.standalone': typeof import('./src/components/Activation/WelcomeModal.standalone.vue')['default']
}

View File

@@ -144,6 +144,7 @@ export default [
rules: {
...commonRules,
...vueRules,
'no-undef': 'off', // Allow TypeScript to handle global variable validation (fixes auto-import false positives)
},
}, // Ignores
{

View File

@@ -109,7 +109,7 @@
"@jsonforms/vue": "3.6.0",
"@jsonforms/vue-vanilla": "3.6.0",
"@jsonforms/vue-vuetify": "3.6.0",
"@nuxt/ui": "4.0.0-alpha.0",
"@nuxt/ui": "4.2.1",
"@tanstack/vue-table": "^8.21.3",
"@unraid/shared-callbacks": "3.0.0",
"@unraid/ui": "link:../unraid-ui",

View File

@@ -10,6 +10,28 @@ fi
# Set server name from command-line argument
server_name="$1"
# Common SSH options for reliability
SSH_OPTS='-o ConnectTimeout=5 -o ConnectionAttempts=3 -o ServerAliveInterval=5 -o ServerAliveCountMax=2'
# Simple retry helper: retry <attempts> <delay_seconds> <command...>
retry() {
local attempts="$1"; shift
local delay_seconds="$1"; shift
local try=1
while true; do
"$@"
local exit_code=$?
if [ $exit_code -eq 0 ]; then
return 0
fi
if [ $try -ge $attempts ]; then
return $exit_code
fi
sleep "$delay_seconds"
try=$((try + 1))
done
}
# Source directory paths
standalone_directory="dist/"
@@ -33,11 +55,11 @@ exit_code=0
if [ "$has_standalone" = true ]; then
echo "Deploying standalone apps..."
# Ensure remote directory exists
ssh root@"${server_name}" "mkdir -p /usr/local/emhttp/plugins/dynamix.my.servers/unraid-components/standalone/"
retry 3 2 ssh $SSH_OPTS root@"${server_name}" "mkdir -p /usr/local/emhttp/plugins/dynamix.my.servers/unraid-components/standalone/"
# Clear the remote standalone directory before rsyncing
ssh root@"${server_name}" "rm -rf /usr/local/emhttp/plugins/dynamix.my.servers/unraid-components/*"
retry 3 2 ssh $SSH_OPTS root@"${server_name}" "rm -rf /usr/local/emhttp/plugins/dynamix.my.servers/unraid-components/*"
# Run rsync with proper quoting
rsync -avz --delete -e "ssh" "$standalone_directory" "root@${server_name}:/usr/local/emhttp/plugins/dynamix.my.servers/unraid-components/standalone/"
retry 3 2 rsync -avz --delete --timeout=20 -e "ssh $SSH_OPTS" "$standalone_directory" "root@${server_name}:/usr/local/emhttp/plugins/dynamix.my.servers/unraid-components/standalone/"
standalone_exit_code=$?
# If standalone rsync failed, update exit_code
if [ "$standalone_exit_code" -ne 0 ]; then
@@ -49,7 +71,7 @@ fi
update_auth_request() {
local server_name="$1"
# SSH into server and update auth-request.php
ssh "root@${server_name}" /bin/bash -s << 'EOF'
retry 3 2 ssh $SSH_OPTS "root@${server_name}" /bin/bash -s << 'EOF'
set -euo pipefail
set -o errtrace
AUTH_REQUEST_FILE='/usr/local/emhttp/auth-request.php'

View File

@@ -92,4 +92,4 @@ iframe#progressFrame {
.has-banner-gradient #header.image > * {
position: relative;
z-index: 1;
}
}

View File

@@ -3,6 +3,7 @@ import { defineStore, storeToRefs } from 'pinia';
import { useSessionStorage } from '@vueuse/core';
import { ACTIVATION_CODE_MODAL_HIDDEN_STORAGE_KEY } from '~/consts';
import { navigate } from '~/helpers/external-navigation';
import { useActivationCodeDataStore } from '~/components/Activation/store/activationCodeData';
import { useCallbackActionsStore } from '~/store/callbackActions';
@@ -66,7 +67,7 @@ export const useActivationCodeModalStore = defineStore('activationCodeModal', ()
if (sequenceIndex === keySequence.length) {
setIsHidden(true);
// Redirect only if explicitly hidden via konami code, not just closed normally
window.location.href = '/Tools/Registration';
navigate('/Tools/Registration');
}
};

View File

@@ -35,6 +35,7 @@ import {
TooltipProvider,
TooltipTrigger,
} from '@unraid/ui';
import { navigate } from '~/helpers/external-navigation';
import { extractGraphQLErrorMessage } from '~/helpers/functions';
import type { ApiKeyFragment, AuthAction, Role } from '~/composables/gql/graphql';
@@ -165,7 +166,7 @@ function applyTemplate() {
params.forEach((value, key) => {
authUrl.searchParams.append(key, value);
});
window.location.href = authUrl.toString();
navigate(authUrl.toString());
cancelTemplateInput();
} catch (_err) {

View File

@@ -4,6 +4,7 @@ import { storeToRefs } from 'pinia';
import { ClipboardDocumentIcon, EyeIcon, EyeSlashIcon } from '@heroicons/vue/24/outline';
import { Button, Input } from '@unraid/ui';
import { navigate } from '~/helpers/external-navigation';
import ApiKeyCreate from '~/components/ApiKey/ApiKeyCreate.vue';
import { useAuthorizationLink } from '~/composables/useAuthorizationLink.js';
@@ -93,12 +94,12 @@ const deny = () => {
if (hasValidRedirectUri.value) {
try {
const url = buildCallbackUrl(undefined, 'access_denied');
window.location.href = url;
navigate(url);
} catch {
window.location.href = '/';
navigate('/');
}
} else {
window.location.href = '/';
navigate('/');
}
};
@@ -108,7 +109,7 @@ const returnToApp = () => {
try {
const url = buildCallbackUrl(createdApiKey.value, undefined);
window.location.href = url;
navigate(url);
} catch (_err) {
error.value = 'Failed to redirect back to application';
}

View File

@@ -26,6 +26,7 @@ defineOptions({
});
const { connectPluginInstalled } = storeToRefs(useServerStore());
const toast = useToast();
/**--------------------------------------------
* Settings State & Form definition
@@ -74,10 +75,12 @@ watchDebounced(
// show a toast when the update is done
onMutateSettingsDone((result) => {
actualRestartRequired.value = result.data?.updateSettings?.restartRequired ?? false;
globalThis.toast.success(t('connectSettings.updatedApiSettingsToast'), {
toast.add({
title: t('connectSettings.updatedApiSettingsToast'),
description: actualRestartRequired.value
? t('connectSettings.apiRestartingToastDescription')
: undefined,
color: 'success',
});
});

View File

@@ -20,6 +20,7 @@ import SingleDockerLogViewer from '@/components/Docker/SingleDockerLogViewer.vue
import LogViewerToolbar from '@/components/Logs/LogViewerToolbar.vue';
import { useDockerConsoleSessions } from '@/composables/useDockerConsoleSessions';
import { useDockerEditNavigation } from '@/composables/useDockerEditNavigation';
import { navigate } from '@/helpers/external-navigation';
import { stripLeadingSlash } from '@/utils/docker';
import { useAutoAnimate } from '@formkit/auto-animate/vue';
@@ -258,7 +259,7 @@ function handleAddContainerClick() {
const sanitizedPath = rawPath.replace(/\?.*$/, '').replace(/\/+$/, '');
const withoutAdd = sanitizedPath.replace(/\/AddContainer$/i, '');
const targetPath = withoutAdd ? `${withoutAdd}/AddContainer` : '/AddContainer';
window.location.assign(targetPath);
navigate(targetPath);
}
async function refreshContainers() {
@@ -650,7 +651,7 @@ const [transitionContainerRef] = useAutoAnimate({
</div>
<DockerOverview v-else :item="detailsItem" :details="details" />
<div v-if="isDetailsLoading" class="absolute inset-0 grid place-items-center">
<USkeleton class="h-6 w-6" />
<USkeleton class="size-6" />
</div>
</div>
</UCard>

View File

@@ -0,0 +1,28 @@
import { gql } from '@apollo/client';
export const GET_DOCKER_ACTIVE_CONTAINER = gql`
query GetDockerActiveContainer($id: PrefixedID!) {
docker {
id
containers {
id
names
image
created
state
status
autoStart
ports {
privatePort
publicPort
type
}
hostConfig {
networkMode
}
networkSettings
labels
}
}
}
`;

View File

@@ -0,0 +1,44 @@
import { gql } from '@apollo/client';
export const RENAME_DOCKER_FOLDER = gql`
mutation RenameDockerFolder($folderId: String!, $newName: String!) {
renameDockerFolder(folderId: $folderId, newName: $newName) {
version
views {
id
name
rootId
flatEntries {
id
type
name
parentId
depth
position
path
hasChildren
childrenIds
meta {
id
names
state
status
image
ports {
privatePort
publicPort
type
}
autoStart
hostConfig {
networkMode
}
created
isUpdateAvailable
isRebuildReady
}
}
}
}
}
`;

View File

@@ -0,0 +1,15 @@
import { gql } from '@apollo/client';
export const UPDATE_DOCKER_CONTAINER = gql`
mutation UpdateDockerContainer($id: PrefixedID!) {
docker {
updateContainer(id: $id) {
id
names
state
isUpdateAvailable
isRebuildReady
}
}
}
`;

View File

@@ -21,6 +21,7 @@ import {
DropdownMenuSeparator,
DropdownMenuTrigger,
} from '@unraid/ui';
import { navigate } from '~/helpers/external-navigation';
import { getReleaseNotesUrl, WEBGUI_TOOLS_DOWNGRADE, WEBGUI_TOOLS_UPDATE } from '~/helpers/urls';
import { useActivationCodeDataStore } from '~/components/Activation/store/activationCodeData';
@@ -38,7 +39,24 @@ const { copyWithNotification } = useClipboardWithToast();
onMounted(() => {
nextTick(() => {
const logoWrapper = document.querySelector('.logo');
logoWrapper?.classList.remove('logo');
if (logoWrapper) {
logoWrapper.classList.remove('logo');
// Fix for header overlap on Azure/Gray themes in Unraid 7.0
// These themes have a sidebar and require an offset which was provided by the .logo class
const isAzureOrGray = !!document.querySelector(
'link[href*="-azure.css"], link[href*="-gray.css"]'
);
const version = parseFloat(osVersion.value || '0');
// Apply offset only for versions < 7.1 and affected themes
// We check > 0 to ensure we have a valid version loaded
if (version > 0 && version < 7.1 && isAzureOrGray) {
(logoWrapper as HTMLElement).style.float = 'left';
(logoWrapper as HTMLElement).style.marginLeft = '75px';
}
}
});
});
@@ -126,7 +144,7 @@ const handleUpdateStatusClick = () => {
if (updateOsStatus.value.click) {
updateOsStatus.value.click();
} else if (updateOsStatus.value.href) {
window.location.href = updateOsStatus.value.href;
navigate(updateOsStatus.value.href);
}
};

View File

@@ -2,7 +2,7 @@
import { computed, reactive, ref, watch } from 'vue';
import { useMutation, useQuery, useSubscription } from '@vue/apollo-composable';
import { AlertTriangle, Octagon } from 'lucide-vue-next';
import { navigate } from '~/helpers/external-navigation';
import type { FragmentType } from '~/composables/gql';
import type {
@@ -11,6 +11,7 @@ import type {
WarningAndAlertNotificationsQueryVariables,
} from '~/composables/gql/graphql';
import { NOTIFICATION_ICONS, NOTIFICATION_TOAST_COLORS } from '~/components/Notifications/constants';
import {
archiveNotification,
NOTIFICATION_FRAGMENT,
@@ -23,6 +24,8 @@ import {
import { useFragment } from '~/composables/gql';
import { NotificationImportance } from '~/composables/gql/graphql';
const toast = useToast();
const { result, loading, error, refetch } = useQuery<
WarningAndAlertNotificationsQuery,
WarningAndAlertNotificationsQueryVariables
@@ -88,24 +91,24 @@ const formatTimestamp = (notification: NotificationFragmentFragment) => {
const importanceMeta: Record<
NotificationImportance,
{ label: string; badge: string; icon: typeof AlertTriangle; accent: string }
{ label: string; badge: string; icon: string; accent: string }
> = {
[NotificationImportance.ALERT]: {
label: 'Alert',
badge: 'bg-red-100 text-red-700 border border-red-300',
icon: Octagon,
icon: NOTIFICATION_ICONS[NotificationImportance.ALERT],
accent: 'text-red-600',
},
[NotificationImportance.WARNING]: {
label: 'Warning',
badge: 'bg-amber-100 text-amber-700 border border-amber-300',
icon: AlertTriangle,
icon: NOTIFICATION_ICONS[NotificationImportance.WARNING],
accent: 'text-amber-600',
},
[NotificationImportance.INFO]: {
label: 'Info',
badge: 'bg-blue-100 text-blue-700 border border-blue-300',
icon: AlertTriangle,
icon: NOTIFICATION_ICONS[NotificationImportance.INFO],
accent: 'text-blue-600',
},
};
@@ -142,46 +145,38 @@ const dismissNotification = async (notification: NotificationFragmentFragment) =
const { onResult: onNotificationAdded } = useSubscription(notificationAddedSubscription);
onNotificationAdded(({ data }) => {
if (!data?.notificationAdded) {
if (!data) {
return;
}
// Access raw subscription data directly - don't call useFragment in async callback
const rawNotification = data.notificationAdded as unknown as NotificationFragmentFragment;
const notification = useFragment(NOTIFICATION_FRAGMENT, data.notificationAdded);
if (
!rawNotification ||
(rawNotification.importance !== NotificationImportance.ALERT &&
rawNotification.importance !== NotificationImportance.WARNING)
!notification ||
(notification.importance !== NotificationImportance.ALERT &&
notification.importance !== NotificationImportance.WARNING)
) {
return;
}
void refetch();
if (!globalThis.toast) {
return;
}
if (rawNotification.timestamp) {
if (notification.timestamp) {
// Trigger the global toast in tandem with the subscription update.
const funcMapping: Record<
NotificationImportance,
(typeof globalThis)['toast']['info' | 'error' | 'warning']
> = {
[NotificationImportance.ALERT]: globalThis.toast.error,
[NotificationImportance.WARNING]: globalThis.toast.warning,
[NotificationImportance.INFO]: globalThis.toast.info,
};
const toast = funcMapping[rawNotification.importance];
const color = NOTIFICATION_TOAST_COLORS[notification.importance];
const createOpener = () => ({
label: 'Open',
onClick: () => rawNotification.link && window.open(rawNotification.link, '_blank', 'noopener'),
onClick: () => {
if (notification.link) {
navigate(notification.link);
}
},
});
requestAnimationFrame(() =>
toast(rawNotification.title, {
description: rawNotification.subject,
action: rawNotification.link ? createOpener() : undefined,
toast.add({
title: notification.title,
description: notification.subject,
color,
actions: notification.link ? [createOpener()] : undefined,
})
);
}
@@ -192,7 +187,11 @@ onNotificationAdded(({ data }) => {
<section class="flex flex-col gap-4 rounded-lg border border-amber-200 bg-white p-4 shadow-sm">
<header class="flex items-center justify-between gap-3">
<div class="flex items-center gap-2">
<AlertTriangle class="h-5 w-5 text-amber-600" aria-hidden="true" />
<UIcon
name="i-heroicons-exclamation-triangle-20-solid"
class="size-5 text-amber-600"
aria-hidden="true"
/>
<h2 class="text-base font-semibold text-gray-900">Warnings & Alerts</h2>
</div>
<span
@@ -208,7 +207,7 @@ onNotificationAdded(({ data }) => {
</div>
<div v-else-if="loading" class="flex items-center gap-2 text-sm text-gray-500">
<span class="h-2 w-2 animate-pulse rounded-full bg-amber-400" aria-hidden="true" />
<span class="size-2 animate-pulse rounded-full bg-amber-400" aria-hidden="true" />
Loading latest notifications
</div>
@@ -219,9 +218,9 @@ onNotificationAdded(({ data }) => {
class="grid gap-2 rounded-md border border-gray-200 p-3 transition hover:border-amber-300"
>
<div class="flex items-start gap-3">
<component
:is="meta.icon"
class="mt-0.5 h-5 w-5 flex-none"
<UIcon
:name="meta.icon"
class="mt-0.5 size-5 flex-none"
:class="meta.accent"
aria-hidden="true"
/>
@@ -269,7 +268,7 @@ onNotificationAdded(({ data }) => {
<div v-else class="flex flex-col items-start gap-2 rounded-md border border-gray-200 p-3">
<div class="flex items-center gap-2 text-sm font-medium text-gray-700">
<span class="h-2 w-2 rounded-full bg-emerald-400" aria-hidden="true" />
<span class="size-2 rounded-full bg-emerald-400" aria-hidden="true" />
All clear. No active warnings or alerts.
</div>
<p class="text-sm text-gray-500">

View File

@@ -1,12 +1,11 @@
<script setup lang="ts">
import { computed } from 'vue';
import { BellIcon, ExclamationTriangleIcon, ShieldExclamationIcon } from '@heroicons/vue/24/solid';
import { cn } from '@unraid/ui';
import type { OverviewQuery } from '~/composables/gql/graphql';
import type { Component } from 'vue';
import { NOTIFICATION_COLORS, NOTIFICATION_ICONS } from '~/components/Notifications/constants';
import { NotificationImportance as Importance } from '~/composables/gql/graphql';
const props = defineProps<{ overview?: OverviewQuery['notifications']['overview']; seen?: boolean }>();
@@ -27,33 +26,32 @@ const indicatorLevel = computed(() => {
}
});
const icon = computed<{ component: Component; color: string } | null>(() => {
switch (indicatorLevel.value) {
case Importance.WARNING:
return {
component: ExclamationTriangleIcon,
color: 'text-yellow-500 translate-y-0.5',
};
case Importance.ALERT:
return {
component: ShieldExclamationIcon,
color: 'text-unraid-red',
};
const icon = computed<{ name: string; color: string } | null>(() => {
const level = indicatorLevel.value;
if (level !== Importance.WARNING && level !== Importance.ALERT) {
return null;
}
return null;
return {
name: NOTIFICATION_ICONS[level],
color: cn(NOTIFICATION_COLORS[level], {
'translate-y-0.5': level === Importance.WARNING,
}),
};
});
</script>
<template>
<div class="relative flex items-center justify-center">
<BellIcon class="text-header-text-primary h-6 w-6" />
<UIcon name="i-heroicons-bell-20-solid" class="size-6" />
<div
v-if="!seen && indicatorLevel === 'UNREAD'"
class="border-muted bg-unraid-green absolute top-0 right-0 size-2.5 rounded-full border"
/>
<component
:is="icon.component"
<UIcon
v-else-if="!seen && icon && indicatorLevel"
:name="icon.name"
:class="cn('absolute -top-1 -right-1 size-4 rounded-full', icon.color)"
/>
</div>

View File

@@ -4,20 +4,12 @@ import { useI18n } from 'vue-i18n';
import { useMutation } from '@vue/apollo-composable';
import { computedAsync } from '@vueuse/core';
import {
ArchiveBoxIcon,
CheckBadgeIcon,
ExclamationTriangleIcon,
LinkIcon,
ShieldExclamationIcon,
TrashIcon,
} from '@heroicons/vue/24/solid';
import { Button } from '@unraid/ui';
import { Markdown } from '@/helpers/markdown';
import { navigate } from '~/helpers/external-navigation';
import type { NotificationFragmentFragment } from '~/composables/gql/graphql';
import type { Component } from 'vue';
import { NOTIFICATION_COLORS, NOTIFICATION_ICONS } from '~/components/Notifications/constants';
import {
archiveNotification as archiveMutation,
deleteNotification as deleteMutation,
@@ -37,25 +29,15 @@ const descriptionMarkup = computedAsync(async () => {
}
}, '');
const icon = computed<{ component: Component; color: string } | null>(() => {
switch (props.importance) {
case 'INFO':
return {
component: CheckBadgeIcon,
color: 'text-unraid-green',
};
case 'WARNING':
return {
component: ExclamationTriangleIcon,
color: 'text-yellow-accent',
};
case 'ALERT':
return {
component: ShieldExclamationIcon,
color: 'text-unraid-red',
};
const icon = computed<{ name: string; color: string } | null>(() => {
if (!props.importance || !NOTIFICATION_ICONS[props.importance]) {
return null;
}
return null;
return {
name: NOTIFICATION_ICONS[props.importance],
color: NOTIFICATION_COLORS[props.importance],
};
});
const archive = reactive(
@@ -74,6 +56,12 @@ const mutationError = computed(() => {
return archive.error?.message ?? deleteNotification.error?.message;
});
const openLink = () => {
if (props.link) {
navigate(props.link);
}
};
const reformattedTimestamp = computed<string>(() => {
if (!props.timestamp) return '';
const userLocale = navigator.language ?? 'en-US'; // Get the user's browser language (e.g., 'en-US', 'fr-FR')
@@ -94,16 +82,11 @@ const reformattedTimestamp = computed<string>(() => {
<div class="group/item relative flex flex-col gap-2 py-3 text-base">
<header class="flex -translate-y-1 flex-row items-baseline justify-between gap-2">
<h3
class="m-0 flex flex-row items-baseline gap-2 overflow-x-hidden text-base font-semibold normal-case"
class="m-0 flex min-w-0 flex-row items-baseline gap-2 overflow-x-hidden text-base font-semibold normal-case"
>
<!-- the `translate` compensates for extra space added by the `svg` element when rendered -->
<component
:is="icon.component"
v-if="icon"
class="size-5 shrink-0 translate-y-1"
:class="icon.color"
/>
<span class="flex-1 truncate" :title="title">{{ title }}</span>
<UIcon v-if="icon" :name="icon.name" class="size-5 shrink-0 translate-y-1" :class="icon.color" />
<span class="min-w-0 flex-1 break-words" :title="title">{{ title }}</span>
</h3>
<div
@@ -114,41 +97,42 @@ const reformattedTimestamp = computed<string>(() => {
</div>
</header>
<h4 class="m-0 font-normal">
<h4 class="m-0 font-normal break-words">
{{ subject }}
</h4>
<div class="flex flex-row items-center justify-between gap-2">
<div class="" v-html="descriptionMarkup" />
<div class="min-w-0 break-words" v-html="descriptionMarkup" />
</div>
<p v-if="mutationError" class="text-red-600">{{ t('common.error') }}: {{ mutationError }}</p>
<p v-if="mutationError" class="text-destructive">{{ t('common.error') }}: {{ mutationError }}</p>
<div class="flex items-baseline justify-end gap-4">
<a
<UButton
v-if="link"
:href="link"
class="text-primary inline-flex items-center justify-center text-sm font-medium hover:underline focus:underline"
variant="link"
icon="i-heroicons-link-20-solid"
color="neutral"
@click="openLink"
>
<LinkIcon class="mr-2 size-4" />
<span class="text-sm">{{ t('notifications.item.viewLink') }}</span>
</a>
<Button
{{ t('notifications.item.viewLink') }}
</UButton>
<UButton
v-if="type === NotificationType.UNREAD"
:disabled="archive.loading"
@click="() => archive.mutate({ id: props.id })"
:loading="archive.loading"
icon="i-heroicons-archive-box-20-solid"
@click="() => void archive.mutate({ id: props.id })"
>
<ArchiveBoxIcon class="mr-2 size-4" />
<span class="text-sm">{{ t('notifications.item.archive') }}</span>
</Button>
<Button
{{ t('notifications.item.archive') }}
</UButton>
<UButton
v-if="type === NotificationType.ARCHIVE"
:disabled="deleteNotification.loading"
@click="() => deleteNotification.mutate({ id: props.id, type: props.type })"
:loading="deleteNotification.loading"
icon="i-heroicons-trash-20-solid"
@click="() => void deleteNotification.mutate({ id: props.id, type: props.type })"
>
<TrashIcon class="mr-2 size-4" />
<span class="text-sm">{{ t('notifications.item.delete') }}</span>
</Button>
{{ t('notifications.item.delete') }}
</UButton>
</div>
</div>
</template>

View File

@@ -1,18 +1,27 @@
<script setup lang="ts">
import { computed, ref, watch } from 'vue';
import { useI18n } from 'vue-i18n';
import { storeToRefs } from 'pinia';
import { useQuery } from '@vue/apollo-composable';
import { vInfiniteScroll } from '@vueuse/components';
import { useDebounceFn } from '@vueuse/core';
import { CheckIcon } from '@heroicons/vue/24/solid';
import { Error as LoadingError, Spinner as LoadingSpinner } from '@unraid/ui';
import { extractGraphQLErrorMessage } from '~/helpers/functions';
import type { NotificationImportance as Importance, NotificationType } from '~/composables/gql/graphql';
// import { dbgApolloError } from '~/helpers/functions';
import type { ApolloError } from '@apollo/client/errors';
import type {
NotificationImportance as Importance,
Notification,
NotificationType,
} from '~/composables/gql/graphql';
import {
getNotifications,
NOTIFICATION_FRAGMENT,
} from '~/components/Notifications/graphql/notification.query';
import { notificationAddedSubscription } from '~/components/Notifications/graphql/notification.subscription';
import NotificationsItem from '~/components/Notifications/Item.vue';
import { useHaveSeenNotifications } from '~/composables/api/use-notifications';
import { useFragment } from '~/composables/gql/fragment-masking';
@@ -28,11 +37,18 @@ const props = withDefaults(
importance?: Importance;
}>(),
{
pageSize: 15,
// Increased to 50 to minimize "pagination drift" (race conditions) where
// new items added during a fetch shift the offsets of subsequent pages,
// causing the client to fetch duplicate items it already has.
pageSize: 50,
importance: undefined,
}
);
const emit = defineEmits<{
(e: 'refetched'): void;
}>();
/** whether we should continue trying to load more notifications */
const canLoadMore = ref(true);
/** reset custom state when props (e.g. props.type filter) change*/
@@ -40,22 +56,89 @@ watch(props, () => {
canLoadMore.value = true;
});
const { offlineError } = useUnraidApiStore();
const { result, error, loading, fetchMore, refetch } = useQuery(getNotifications, () => ({
filter: {
offset: 0,
limit: props.pageSize,
type: props.type,
importance: props.importance,
const unraidApiStore = useUnraidApiStore();
const { offlineError } = storeToRefs(unraidApiStore);
const { result, error, loading, fetchMore, refetch, subscribeToMore, onResult } = useQuery(
getNotifications,
() => ({
filter: {
offset: 0,
limit: props.pageSize,
type: props.type,
importance: props.importance,
},
})
);
onResult((res) => {
if (res.data) {
emit('refetched');
if (unraidApiStore.unraidApiStatus === 'offline') {
unraidApiStore.unraidApiStatus = 'online';
}
}
});
// Debounce refetch to handle mass-add scenarios efficiently.
// Increased to 500ms to ensure we capture the entire batch of events in a single refetch,
// preventing partial updates that can lead to race conditions.
const debouncedRefetch = useDebounceFn(() => {
console.log('[Notifications] Refetching due to subscription update');
canLoadMore.value = true; // Reset load state so infinite scroll works again from top
void refetch();
}, 500);
subscribeToMore({
document: notificationAddedSubscription,
updateQuery: (previousResult, { subscriptionData }) => {
if (!subscriptionData.data) return previousResult;
const newNotification = subscriptionData.data.notificationAdded;
// Check filters - only refetch if the new notification is relevant to this list
let isRelevant = newNotification.type === props.type;
if (isRelevant && props.importance) {
isRelevant = newNotification.importance === props.importance;
}
if (isRelevant) {
// Debug log to confirm event reception
console.log('[Notifications] Relevant subscription event received:', newNotification.id);
debouncedRefetch();
} else {
// console.log('[Notifications] Irrelevant subscription event ignored:', newNotification.id);
}
// Return previous result unchanged. We rely on refetch() to update the list.
// This avoids the "stale previousResult" issue where rapid updates overwrite each other.
return previousResult;
},
}));
});
// for debugging purposes:
// watch(error, (e) => dbgApolloError('useQuery error', e as ApolloError | null | undefined), {
// immediate: true,
// });
watch(offlineError, (o) => {
if (o) console.log('[Notifications] offlineError:', o.message);
});
watch([error, offlineError], ([e, o]) => {
if (!e && !o) {
canLoadMore.value = true;
} else if (o) {
canLoadMore.value = false;
}
});
const notifications = computed(() => {
if (!result.value?.notifications.list) return [];
const list = useFragment(NOTIFICATION_FRAGMENT, result.value?.notifications.list);
// necessary because some items in this list may change their type (e.g. archival)
// and we don't want to display them in the wrong list client-side.
return list.filter((n) => n.type === props.type);
const filtered = list.filter((n) => n.type === props.type);
console.log('[Notifications] Computed list updated. Length:', filtered.length);
return filtered;
});
const { t } = useI18n();
@@ -68,7 +151,7 @@ watch(
const [latest] = notifications.value;
if (!latest?.timestamp) return;
if (new Date(latest.timestamp) > new Date(latestSeenTimestamp.value)) {
console.log('[notif list] setting last seen timestamp', latest.timestamp);
// console.log('[notif list] setting last seen timestamp', latest.timestamp);
latestSeenTimestamp.value = latest.timestamp;
}
},
@@ -76,20 +159,77 @@ watch(
);
async function onLoadMore() {
console.log('[getNotifications] onLoadMore');
const incoming = await fetchMore({
variables: {
filter: {
offset: notifications.value.length,
limit: props.pageSize,
type: props.type,
importance: props.importance,
const currentLength = notifications.value.length;
console.log('[Notifications] onLoadMore triggered. Current Offset:', currentLength);
if (loading.value) {
console.log('[Notifications] Skipping load more because loading is true');
return;
}
try {
const incoming = await fetchMore({
variables: {
filter: {
offset: currentLength,
limit: props.pageSize,
type: props.type,
importance: props.importance,
},
},
},
});
const incomingCount = incoming?.data.notifications.list.length ?? 0;
if (incomingCount === 0 || incomingCount < props.pageSize) {
updateQuery: (previousResult, { fetchMoreResult }) => {
if (!fetchMoreResult) return previousResult;
const currentList = previousResult.notifications.list || [];
const incomingList = fetchMoreResult.notifications.list;
console.log('[Notifications] fetchMore UpdateQuery.');
console.log(' - Previous List Length:', currentList.length);
console.log(' - Incoming List Length:', incomingList.length);
const existingIds = new Set(currentList.map((n: Notification) => n.id));
const newUniqueItems = incomingList.filter((n: Notification) => !existingIds.has(n.id));
console.log(' - Unique Items to Append:', newUniqueItems.length);
// DETECT PAGINATION DRIFT (Shifted Offsets)
// If we fetched items, but they are ALL duplicates, it implies new items were added
// to the top of the list, pushing existing items down into our requested page range.
// In this case, our current list is stale/misaligned. We must force a full refetch.
if (incomingList.length > 0 && newUniqueItems.length === 0) {
console.warn(
'[Notifications] Pagination Drift Detected! Fetched items are all duplicates. Triggering Refetch.'
);
// Trigger refetch asynchronously to avoid side-effects during render cycle
setTimeout(() => {
debouncedRefetch();
}, 0);
return previousResult;
}
return {
...previousResult,
notifications: {
...previousResult.notifications,
list: [...currentList, ...newUniqueItems],
},
};
},
});
const incomingCount = incoming?.data.notifications.list.length ?? 0;
console.log('[Notifications] fetchMore Result.');
console.log(' - Incoming Count from Network:', incomingCount);
console.log(' - Page Size:', props.pageSize);
if (incomingCount === 0 || incomingCount < props.pageSize) {
console.log('[Notifications] Reached End (incoming < pageSize). Disabling Infinite Scroll.');
canLoadMore.value = false;
}
} catch (error) {
console.error('[Notifications] fetchMore Error:', error);
canLoadMore.value = false;
throw error;
}
}
@@ -114,12 +254,19 @@ const noNotificationsMessage = computed(() => {
importance: importanceLabel.value.toLowerCase(),
});
});
const displayErrorMessage = computed(() => {
if (offlineError.value) return offlineError.value.message;
const apolloErr = error.value as ApolloError | null | undefined;
return extractGraphQLErrorMessage(apolloErr);
});
</script>
<template>
<div
v-if="notifications?.length > 0"
v-infinite-scroll="[onLoadMore, { canLoadMore: () => canLoadMore }]"
v-infinite-scroll="[onLoadMore, { canLoadMore: () => canLoadMore && !loading && !offlineError }]"
class="flex min-h-0 flex-1 flex-col overflow-y-scroll px-3"
>
<TransitionGroup
@@ -139,17 +286,58 @@ const noNotificationsMessage = computed(() => {
/>
</TransitionGroup>
<div v-if="loading" class="grid place-content-center py-3">
<LoadingSpinner />
<!-- 3 skeletons to replace shadcn's LoadingSpinner -->
<div v-if="loading" class="space-y-4 py-3">
<div v-for="n in 3" :key="n" class="py-3">
<div class="flex items-center gap-2">
<USkeleton class="size-5 rounded-full" />
<USkeleton class="h-4 w-40" />
<div class="ml-auto">
<USkeleton class="h-3 w-24" />
</div>
</div>
<div class="mt-2">
<USkeleton class="h-3 w-3/4" />
</div>
</div>
</div>
</div>
<div v-if="!canLoadMore" class="text-secondary-foreground grid place-content-center py-3">
{{ t('notifications.list.reachedEnd') }}
</div>
</div>
<LoadingError v-else :loading="loading" :error="offlineError ?? error" @retry="refetch">
<div v-if="notifications?.length === 0" class="contents">
<CheckIcon class="h-10 translate-y-3 text-green-600" />
<!-- USkeleton for loading and error states -->
<div v-else class="flex h-full flex-col items-center justify-center gap-3 px-3">
<div v-if="loading" class="w-full max-w-md space-y-4">
<div v-for="n in 3" :key="n" class="py-1.5">
<div class="flex items-center gap-2">
<USkeleton class="size-5 rounded-full" />
<USkeleton class="h-4 w-40" />
</div>
<div class="mt-2">
<USkeleton class="h-3 w-3/4" />
</div>
</div>
<p class="text-muted-foreground text-center text-sm">Loading Notifications...</p>
</div>
<!-- Error (centered, icon + title + message + full-width button) -->
<div v-else-if="offlineError || error" class="w-full max-w-sm space-y-3">
<div class="flex justify-center">
<UIcon name="i-heroicons-shield-exclamation-20-solid" class="text-destructive size-10" />
</div>
<div class="text-center">
<h3 class="font-bold">Error</h3>
<p>{{ displayErrorMessage }}</p>
</div>
<UButton block @click="() => void refetch()">Try Again</UButton>
</div>
<!-- Default (empty state) -->
<div v-else class="contents">
<UIcon name="i-heroicons-check-20-solid" class="text-unraid-green size-10 translate-y-3" />
{{ noNotificationsMessage }}
</div>
</LoadingError>
</div>
</template>

View File

@@ -1,27 +1,12 @@
<script setup lang="ts">
import { computed, ref } from 'vue';
import { computed, ref, watch } from 'vue';
import { useI18n } from 'vue-i18n';
import { useMutation, useQuery, useSubscription } from '@vue/apollo-composable';
import {
Button,
Sheet,
SheetContent,
SheetHeader,
SheetTitle,
SheetTrigger,
Tabs,
TabsContent,
TabsList,
TabsTrigger,
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from '@unraid/ui';
import { Settings } from 'lucide-vue-next';
import { navigate } from '~/helpers/external-navigation';
import ConfirmDialog from '~/components/ConfirmDialog.vue';
import { NOTIFICATION_TOAST_COLORS } from '~/components/Notifications/constants';
import {
archiveAllNotifications,
deleteArchivedNotifications,
@@ -39,6 +24,12 @@ import { useTrackLatestSeenNotification } from '~/composables/api/use-notificati
import { useFragment } from '~/composables/gql';
import { NotificationImportance as Importance, NotificationType } from '~/composables/gql/graphql';
import { useConfirm } from '~/composables/useConfirm';
import { useThemeStore } from '~/store/theme';
import { useUnraidApiStore } from '~/store/unraidApi';
const toast = useToast();
const themeStore = useThemeStore();
const unraidApiStore = useUnraidApiStore();
const { mutate: archiveAll, loading: loadingArchiveAll } = useMutation(archiveAllNotifications);
const { mutate: deleteArchives, loading: loadingDeleteAll } = useMutation(deleteArchivedNotifications);
@@ -48,8 +39,15 @@ const importance = ref<Importance | undefined>(undefined);
const { t } = useI18n();
const filterOptions = computed<Array<{ label: string; value?: Importance }>>(() => [
{ label: t('notifications.sidebar.filters.all') },
const activeFilter = computed({
get: () => importance.value ?? 'all',
set: (val) => {
importance.value = val === 'all' ? undefined : (val as Importance);
},
});
const filterTabs = computed(() => [
{ label: t('notifications.sidebar.filters.all'), value: 'all' as const },
{ label: t('notifications.sidebar.filters.alert'), value: Importance.ALERT },
{ label: t('notifications.sidebar.filters.info'), value: Importance.INFO },
{ label: t('notifications.sidebar.filters.warning'), value: Importance.WARNING },
@@ -79,7 +77,7 @@ const confirmAndDeleteArchives = async () => {
}
};
const { result, subscribeToMore } = useQuery(notificationsOverview);
const { result, subscribeToMore, refetch } = useQuery(notificationsOverview);
subscribeToMore({
document: notificationOverviewSubscription,
updateQuery: (prev, { subscriptionData }) => {
@@ -88,6 +86,22 @@ subscribeToMore({
return snapshot;
},
});
const handleRefetch = () => {
void recalculateOverview().finally(() => {
void refetch();
});
};
watch(
() => unraidApiStore.unraidApiStatus,
(status) => {
if (status === 'online') {
handleRefetch();
}
}
);
const { latestNotificationTimestamp, haveSeenNotifications } = useTrackLatestSeenNotification();
const { onResult: onNotificationAdded } = useSubscription(notificationAddedSubscription);
@@ -99,25 +113,19 @@ onNotificationAdded(({ data }) => {
if (notif.timestamp) {
latestNotificationTimestamp.value = notif.timestamp;
}
if (!globalThis.toast) {
return;
}
const funcMapping: Record<Importance, (typeof globalThis)['toast']['info' | 'error' | 'warning']> = {
[Importance.ALERT]: globalThis.toast.error,
[Importance.WARNING]: globalThis.toast.warning,
[Importance.INFO]: globalThis.toast.info,
};
const toast = funcMapping[notif.importance];
const color = NOTIFICATION_TOAST_COLORS[notif.importance];
const createOpener = () => ({
label: t('notifications.sidebar.toastOpen'),
onClick: () => window.location.assign(notif.link as string),
onClick: () => navigate(notif.link as string),
});
requestAnimationFrame(() =>
toast(notif.title, {
toast.add({
title: notif.title,
description: notif.subject,
action: notif.link ? createOpener() : undefined,
color,
actions: notif.link ? [createOpener()] : undefined,
})
);
});
@@ -139,122 +147,130 @@ const readArchivedCount = computed(() => {
const prepareToViewNotifications = () => {
void recalculateOverview();
};
const isOpen = ref(false);
const activeTab = ref<'unread' | 'archived'>('unread');
const tabs = computed(() => [
{
label: t('notifications.sidebar.unreadTab'),
value: 'unread' as const,
badge: overview.value?.unread.total ?? 0,
},
{
label: t('notifications.sidebar.archivedTab'),
value: 'archived' as const,
badge: readArchivedCount.value ?? 0,
},
]);
</script>
<template>
<Sheet>
<SheetTrigger as-child>
<Button variant="header" size="header" @click="prepareToViewNotifications">
<span class="sr-only">{{ t('notifications.sidebar.openButtonSr') }}</span>
<NotificationsIndicator :overview="overview" :seen="haveSeenNotifications" />
</Button>
</SheetTrigger>
<SheetContent
side="right"
class="flex h-screen max-h-screen min-h-screen w-full max-w-screen flex-col gap-5 px-0 pb-0 sm:max-w-[540px]"
<div>
<UButton
variant="ghost"
color="neutral"
class="text-header-text-primary hover:bg-[color-mix(in_oklab,hsl(var(--accent))_20%,transparent)] active:bg-transparent"
:style="{ color: themeStore.theme.textColor || undefined }"
@click="
() => {
isOpen = true;
prepareToViewNotifications();
}
"
>
<div class="relative flex h-full w-full flex-col">
<SheetHeader class="ml-1 items-baseline gap-1 px-3 pb-2">
<SheetTitle class="text-2xl">{{ t('notifications.sidebar.title') }}</SheetTitle>
</SheetHeader>
<Tabs
default-value="unread"
class="flex min-h-0 flex-1 flex-col"
:aria-label="t('notifications.sidebar.statusTabsAria')"
>
<div class="flex flex-row flex-wrap items-center justify-between gap-3 px-3">
<TabsList class="flex" :aria-label="t('notifications.sidebar.statusTabsListAria')">
<TabsTrigger value="unread" as-child>
<Button variant="ghost" size="sm" class="inline-flex items-center gap-1 px-3 py-1">
<span>{{ t('notifications.sidebar.unreadTab') }}</span>
<span v-if="overview" class="font-normal">({{ overview.unread.total }})</span>
</Button>
</TabsTrigger>
<TabsTrigger value="archived" as-child>
<Button variant="ghost" size="sm" class="inline-flex items-center gap-1 px-3 py-1">
<span>{{ t('notifications.sidebar.archivedTab') }}</span>
<span v-if="overview" class="font-normal">({{ readArchivedCount }})</span>
</Button>
</TabsTrigger>
</TabsList>
<TabsContent value="unread" class="flex-col items-end">
<Button
:disabled="loadingArchiveAll"
variant="link"
size="sm"
class="text-foreground hover:text-destructive transition-none"
@click="confirmAndArchiveAll"
>
{{ t('notifications.sidebar.archiveAllAction') }}
</Button>
</TabsContent>
<TabsContent value="archived" class="flex-col items-end">
<Button
:disabled="loadingDeleteAll"
variant="link"
size="sm"
class="text-foreground hover:text-destructive transition-none"
@click="confirmAndDeleteArchives"
>
{{ t('notifications.sidebar.deleteAllAction') }}
</Button>
</TabsContent>
</div>
<span class="sr-only">{{ t('notifications.sidebar.openButtonSr') }}</span>
<NotificationsIndicator :overview="overview" :seen="haveSeenNotifications" />
</UButton>
<div class="mt-3 flex items-start justify-between gap-3 px-3">
<div class="flex min-w-0 flex-1 flex-col gap-2">
<div
class="border-border/60 bg-muted/60 flex flex-wrap items-center gap-1 rounded-xl border p-1"
role="group"
>
<Button
v-for="option in filterOptions"
:key="option.value ?? 'all'"
variant="ghost"
size="sm"
class="h-8 rounded-lg border border-transparent px-3 text-xs font-medium transition-colors"
:class="
importance === option.value
? 'border-border bg-background text-foreground'
: 'text-muted-foreground hover:border-border/60 hover:bg-muted/40 hover:text-foreground'
"
:aria-pressed="importance === option.value"
@click="importance = option.value"
<USlideover v-model:open="isOpen" side="right" :title="t('notifications.sidebar.title')">
<template #body>
<div class="flex h-full flex-col">
<div class="flex flex-1 flex-col overflow-hidden">
<!-- Controls Area -->
<div class="flex flex-col gap-3 px-0 py-3">
<!-- Tabs & Action Button Row -->
<div class="flex items-center justify-between gap-3">
<UTabs
v-model="activeTab"
:items="tabs"
:content="false"
variant="pill"
color="primary"
/>
<!-- Action Button -->
<UButton
v-if="activeTab === 'unread'"
:disabled="loadingArchiveAll"
variant="link"
color="neutral"
@click="confirmAndArchiveAll"
>
{{ option.label }}
</Button>
{{ t('notifications.sidebar.archiveAllAction') }}
</UButton>
<UButton
v-else
:disabled="loadingDeleteAll"
variant="link"
color="neutral"
@click="confirmAndDeleteArchives"
>
{{ t('notifications.sidebar.deleteAllAction') }}
</UButton>
</div>
<!-- Filters & Settings Row -->
<div class="flex items-center justify-between gap-3">
<!-- Filter Button Group -->
<UTabs
v-model="activeFilter"
:items="filterTabs"
:content="false"
variant="pill"
color="neutral"
/>
<!-- Settings Icon -->
<UTooltip
:delay-duration="0"
:content="{
align: 'center',
side: 'top',
sideOffset: 8,
}"
:text="t('notifications.sidebar.editSettingsTooltip')"
>
<UButton
variant="ghost"
color="neutral"
icon="i-heroicons-cog-6-tooth-20-solid"
@click="navigate('/Settings/Notifications')"
/>
</UTooltip>
</div>
</div>
<div class="shrink-0">
<TooltipProvider>
<Tooltip :delay-duration="0">
<TooltipTrigger as-child>
<a href="/Settings/Notifications">
<Button variant="ghost" size="sm" class="h-8 w-8 p-0">
<Settings class="h-4 w-4" />
</Button>
</a>
</TooltipTrigger>
<TooltipContent>
<p>{{ t('notifications.sidebar.editSettingsTooltip') }}</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
<!-- Notifications List Content -->
<div class="flex flex-1 flex-col overflow-hidden">
<NotificationsList
v-if="activeTab === 'unread'"
:importance="importance"
:type="NotificationType.UNREAD"
class="flex-1"
@refetched="handleRefetch"
/>
<NotificationsList
v-else
:importance="importance"
:type="NotificationType.ARCHIVE"
class="flex-1"
@refetched="handleRefetch"
/>
</div>
</div>
<TabsContent value="unread" class="min-h-0 flex-1 flex-col">
<NotificationsList :importance="importance" :type="NotificationType.UNREAD" />
</TabsContent>
<TabsContent value="archived" class="min-h-0 flex-1 flex-col">
<NotificationsList :importance="importance" :type="NotificationType.ARCHIVE" />
</TabsContent>
</Tabs>
</div>
</SheetContent>
</Sheet>
<!-- Global Confirm Dialog -->
<ConfirmDialog />
</div>
</template>
</USlideover>
<!-- Global Confirm Dialog -->
<ConfirmDialog />
</div>
</template>

View File

@@ -0,0 +1,23 @@
import { NotificationImportance } from '~/composables/gql/graphql';
export const NOTIFICATION_ICONS: Record<NotificationImportance, string> = {
[NotificationImportance.INFO]: 'i-heroicons-check-badge-20-solid',
[NotificationImportance.WARNING]: 'i-heroicons-exclamation-triangle-20-solid',
[NotificationImportance.ALERT]: 'i-heroicons-shield-exclamation-20-solid',
};
export const NOTIFICATION_COLORS: Record<NotificationImportance, string> = {
[NotificationImportance.INFO]: 'text-unraid-green',
[NotificationImportance.WARNING]: 'text-yellow-accent',
[NotificationImportance.ALERT]: 'text-unraid-red',
};
// Toast color mapping (used in Sidebar and CriticalNotifications)
export const NOTIFICATION_TOAST_COLORS: Record<
NotificationImportance,
'error' | 'warning' | 'info' | 'success'
> = {
[NotificationImportance.ALERT]: 'error',
[NotificationImportance.WARNING]: 'warning',
[NotificationImportance.INFO]: 'success',
};

View File

@@ -1,3 +1,5 @@
import gql from 'graphql-tag';
import { graphql } from '~/composables/gql/gql';
export const NOTIFICATION_FRAGMENT = graphql(/* GraphQL */ `
@@ -34,6 +36,20 @@ export const getNotifications = graphql(/* GraphQL */ `
}
`);
export const getNotificationSettings = gql`
query GetNotificationSettings {
notifications {
id
settings {
position
expand
duration
max
}
}
}
`;
export const warningsAndAlerts = graphql(/* GraphQL */ `
query WarningAndAlertNotifications {
notifications {

View File

@@ -132,13 +132,15 @@ const submitForm = async () => {
};
// Handle successful creation
const toast = useToast();
onCreateDone(async ({ data }) => {
// Show success message
if (window.toast) {
window.toast.success('Remote Configuration Created', {
description: `Successfully created remote "${formState.value.name}"`,
});
}
toast.add({
title: 'Remote Configuration Created',
description: `Successfully created remote "${formState.value.name}"`,
color: 'success',
});
console.log('[RCloneConfig] onCreateDone', data);

View File

@@ -36,20 +36,22 @@ const {
refetchQueries: [{ query: GET_RCLONE_REMOTES }],
});
const toast = useToast();
onDeleteDone((result) => {
const data = result?.data;
if (data?.rclone?.deleteRCloneRemote) {
if (window.toast) {
window.toast.success('Remote Deleted', {
description: 'Remote deleted successfully',
});
}
toast.add({
title: 'Remote Deleted',
description: 'Remote deleted successfully',
color: 'success',
});
} else {
if (window.toast) {
window.toast.error('Deletion Failed', {
description: 'Failed to delete remote. Please try again.',
});
}
toast.add({
title: 'Deletion Failed',
description: 'Failed to delete remote. Please try again.',
color: 'error',
});
}
});

View File

@@ -1,31 +0,0 @@
<script lang="ts" setup>
import { computed } from 'vue';
import { Toaster } from '@unraid/ui';
import { useThemeStore } from '~/store/theme';
const themeStore = useThemeStore();
// Get dark mode from theme store
const theme = computed(() => (themeStore.darkMode ? 'dark' : 'light'));
withDefaults(
defineProps<{
position?:
| 'top-center'
| 'top-right'
| 'top-left'
| 'bottom-center'
| 'bottom-right'
| 'bottom-left';
}>(),
{
position: 'top-right',
}
);
</script>
<template>
<Toaster rich-colors close-button :position="position" :theme="theme" />
</template>

View File

@@ -13,6 +13,7 @@ import {
XCircleIcon,
} from '@heroicons/vue/24/solid';
import { Badge, BrandLoading, Button } from '@unraid/ui';
import { navigate } from '~/helpers/external-navigation';
import { WEBGUI_TOOLS_REGISTRATION } from '~/helpers/urls';
import useDateTimeHelper from '~/composables/dateTime';
@@ -113,7 +114,7 @@ const checkButton = computed(() => {
const navigateToRegistration = () => {
if (typeof window !== 'undefined') {
window.location.href = WEBGUI_TOOLS_REGISTRATION;
navigate(WEBGUI_TOOLS_REGISTRATION);
}
};
</script>

View File

@@ -134,11 +134,6 @@ export const componentMappings: ComponentMapping[] = [
selector: 'unraid-color-switcher',
appId: 'color-switcher',
},
{
component: defineAsyncComponent(() => import('@/components/UnraidToaster.vue')),
selector: ['unraid-toaster', 'uui-toaster'],
appId: 'toaster',
},
{
component: defineAsyncComponent(() => import('../UpdateOs/TestUpdateModal.standalone.vue')),
selector: 'unraid-test-update-modal',
@@ -165,4 +160,9 @@ export const componentMappings: ComponentMapping[] = [
appId: 'docker-container-overview',
decorateContainer: true,
},
{
component: defineAsyncComponent(() => import('@nuxt/ui/components/Toast.vue')),
selector: ['unraid-toaster', 'uui-toaster'],
appId: 'toaster',
},
];

View File

@@ -8,10 +8,14 @@ import { isDarkModeActive } from '@unraid/ui';
import { componentMappings } from '@/components/Wrapper/component-registry';
import { client } from '~/helpers/create-apollo-client';
import { createI18nInstance, ensureLocale, getWindowLocale } from '~/helpers/i18n-loader';
import { type ToastPosition } from '~/types/notifications';
import { getNotificationSettings } from '~/components/Notifications/graphql/notification.query';
// Import Pinia for use in Vue apps
import { globalPinia } from '~/store/globalPinia';
import { ensureUnapiScope, ensureUnapiScopeForSelectors, observeUnapiScope } from '~/utils/unapiScope';
// Import the app config to pass runtime settings (like toaster position)
import appConfig from '../../../app.config';
// Ensure Apollo client is singleton
const apolloClient = (typeof window !== 'undefined' && window.apolloClient) || client;
@@ -140,6 +144,62 @@ export async function mountUnifiedApp() {
app.use(ui);
app.provide(DefaultApolloClient, apolloClient);
// Fetch notification settings
interface NotificationSettingsResponse {
notifications?: {
settings?: {
position?: string;
expand?: boolean;
duration?: number;
max?: number;
};
};
}
const toasterSettings = { ...appConfig.ui.toaster } as {
position: ToastPosition;
expand: boolean;
duration: number;
max: number;
};
try {
const { data } = await apolloClient.query<NotificationSettingsResponse>({
query: getNotificationSettings,
fetchPolicy: 'network-only',
});
const fetchedSettings = data?.notifications?.settings;
console.log('[UnifiedMount] Fetched settings:', fetchedSettings);
if (fetchedSettings) {
if (fetchedSettings.position) {
const map: Record<string, ToastPosition> = {
'top-left': 'top-left',
'top-right': 'top-right',
'bottom-left': 'bottom-left',
'bottom-right': 'bottom-right',
'bottom-center': 'bottom-center',
'top-center': 'top-center',
};
const mappedPosition = map[fetchedSettings.position];
if (mappedPosition) {
toasterSettings.position = mappedPosition;
}
}
if (fetchedSettings.expand !== undefined && fetchedSettings.expand !== null) {
toasterSettings.expand = fetchedSettings.expand;
}
if (fetchedSettings.duration) {
toasterSettings.duration = fetchedSettings.duration;
}
if (fetchedSettings.max) {
toasterSettings.max = fetchedSettings.max;
}
}
} catch (e) {
console.error('[UnifiedMount] Failed to fetch notification settings', e);
}
// Mount the app to establish context
let rootElement = document.getElementById('unraid-unified-root');
if (!rootElement) {
@@ -244,6 +304,7 @@ export async function mountUnifiedApp() {
UApp,
{
portal: portalTarget,
toaster: toasterSettings,
},
{
default: () => h(component, props),

Some files were not shown because too many files have changed in this diff Show More