mirror of
https://github.com/unraid/api.git
synced 2025-12-31 21:49:57 -06:00
Merge 18f3227d9e into 9ef1cf1eca
This commit is contained in:
3
.gitignore
vendored
3
.gitignore
vendored
@@ -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/
|
||||
|
||||
@@ -86,4 +86,4 @@ unraid-sso-button.unapi {
|
||||
--text-7xl: 4.5rem;
|
||||
--text-8xl: 6rem;
|
||||
--text-9xl: 8rem;
|
||||
}
|
||||
}
|
||||
@@ -2,4 +2,4 @@
|
||||
@import './css-variables.css';
|
||||
@import './unraid-theme.css';
|
||||
@import './theme-variants.css';
|
||||
@import './base-utilities.css';
|
||||
@import './base-utilities.css';
|
||||
@@ -65,4 +65,4 @@
|
||||
/* Dark Mode Overrides */
|
||||
.dark {
|
||||
--color-border: #383735;
|
||||
}
|
||||
}
|
||||
@@ -3,7 +3,5 @@
|
||||
"extraOrigins": [],
|
||||
"sandbox": true,
|
||||
"ssoSubIds": [],
|
||||
"plugins": [
|
||||
"unraid-api-plugin-connect"
|
||||
]
|
||||
}
|
||||
"plugins": ["unraid-api-plugin-connect"]
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -93,6 +93,9 @@ interface Notify {
|
||||
system: string;
|
||||
version: string;
|
||||
docker_update: string;
|
||||
expand?: string | boolean;
|
||||
duration?: string | number;
|
||||
max?: string | number;
|
||||
}
|
||||
|
||||
interface Ssmtp {
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
export const DOCKER_SERVICE_TOKEN = Symbol('DOCKER_SERVICE');
|
||||
@@ -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',
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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 })
|
||||
|
||||
@@ -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];
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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;
|
||||
}
|
||||
?>
|
||||
@@ -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}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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}
|
||||
@@ -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}
|
||||
@@ -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}
|
||||
@@ -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=""
|
||||
@@ -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 ' °'
|
||||
*/
|
||||
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);
|
||||
?>
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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)_
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
?>
|
||||
@@ -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}
|
||||
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -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}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -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}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -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=""
|
||||
@@ -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 ' °'
|
||||
*/
|
||||
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);
|
||||
?>
|
||||
@@ -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}`;
|
||||
}
|
||||
}
|
||||
@@ -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');
|
||||
});
|
||||
});
|
||||
@@ -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}`;
|
||||
}
|
||||
}
|
||||
@@ -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}`;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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}`;
|
||||
}
|
||||
}
|
||||
@@ -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}`;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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"
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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>
|
||||
@@ -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"))?>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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 ' °'
|
||||
@@ -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;
|
||||
@@ -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>
|
||||
@@ -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"
|
||||
|
||||
@@ -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(' ');
|
||||
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"> </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)_
|
||||
</label>
|
||||
<label class="inline-block">
|
||||
<input type="checkbox" name="unraid2"<?=($notify['unraid'] & 2)==2 ? ' checked' : ''?>>_(Email)_
|
||||
</label>
|
||||
<label class="inline-block">
|
||||
<input type="checkbox" name="unraid3"<?=($notify['unraid'] & 4)==4 ? ' checked' : ''?>>_(Agents)_
|
||||
</label>
|
||||
</span>
|
||||
</span>
|
||||
|
||||
<span id="pluginTitle" class="plugin" style="display:none"> </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)_
|
||||
</label>
|
||||
<label class="inline-block">
|
||||
<input type="checkbox" name="plugin2"<?=($notify['plugin'] & 2)==2 ? ' checked' : ''?>>_(Email)_
|
||||
</label>
|
||||
<label class="inline-block">
|
||||
<input type="checkbox" name="plugin3"<?=($notify['plugin'] & 4)==4 ? ' checked' : ''?>>_(Agents)_
|
||||
</label>
|
||||
</span>
|
||||
</span>
|
||||
|
||||
<span id="dockerTitle" class="docker" style="display:none"> </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)_
|
||||
</label>
|
||||
<label class="inline-block">
|
||||
<input type="checkbox" name="docker_notify2"<?=($notify['docker_notify'] & 2)==2 ? ' checked' : ''?>>_(Email)_
|
||||
</label>
|
||||
<label class="inline-block">
|
||||
<input type="checkbox" name="docker_notify3"<?=($notify['docker_notify'] & 4)==4 ? ' checked' : ''?>>_(Agents)_
|
||||
</label>
|
||||
</span>
|
||||
</span>
|
||||
|
||||
<span id="languageTitle" class="language" style="display:none"> </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)_
|
||||
</label>
|
||||
<label class="inline-block">
|
||||
<input type="checkbox" name="language_notify2"<?=($notify['language_notify'] & 2)==2 ? ' checked' : ''?>>_(Email)_
|
||||
</label>
|
||||
<label class="inline-block">
|
||||
<input type="checkbox" name="language_notify3"<?=($notify['language_notify'] & 4)==4 ? ' checked' : ''?>>_(Agents)_
|
||||
</label>
|
||||
</span>
|
||||
</span>
|
||||
|
||||
<span id="reportTitle" class="report" style="display:none"> </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)_
|
||||
</span>
|
||||
<span>
|
||||
<input type="checkbox" name="report2"<?=($notify['report'] & 2)==2 ? ' checked' : ''?>>_(Email)_
|
||||
</span>
|
||||
<span>
|
||||
<input type="checkbox" name="report3"<?=($notify['report'] & 4)==4 ? ' checked' : ''?>>_(Agents)_
|
||||
</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)_
|
||||
</label>
|
||||
<label class="inline-block">
|
||||
<input type="checkbox" class="checkbox" name="normal2"<?=($notify['normal'] & 2)==2 ? " checked $disabled" : $disabled?>>_(Email)_
|
||||
</label>
|
||||
<label class="inline-block">
|
||||
<input type="checkbox" class="checkbox" name="normal3"<?=($notify['normal'] & 4)==4 ? " checked $disabled" : $disabled?>>_(Agents)_
|
||||
</label>
|
||||
</span>
|
||||
</span>
|
||||
|
||||
|
||||
: <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)_
|
||||
</label>
|
||||
<label class="inline-block">
|
||||
<input type="checkbox" class="checkbox" name="warning2"<?=($notify['warning'] & 2)==2 ? " checked $disabled" : $disabled?>>_(Email)_
|
||||
</label>
|
||||
<label class="inline-block">
|
||||
<input type="checkbox" class="checkbox" name="warning3"<?=($notify['warning'] & 4)==4 ? " checked $disabled" : $disabled?>>_(Agents)_
|
||||
</label>
|
||||
</span>
|
||||
</span>
|
||||
|
||||
|
||||
: <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)_
|
||||
</span>
|
||||
<span>
|
||||
<input type="checkbox" class="checkbox" name="alert2"<?=($notify['alert'] & 2)==2 ? " checked $disabled" : $disabled?>>_(Email)_
|
||||
</span>
|
||||
<span>
|
||||
<input type="checkbox" class="checkbox" name="alert3"<?=($notify['alert'] & 4)==4 ? " checked $disabled" : $disabled?>>_(Agents)_
|
||||
</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>
|
||||
@@ -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;
|
||||
}
|
||||
?>
|
||||
@@ -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 ' °'
|
||||
*/
|
||||
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
2279
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
8
unraid-ui/src/global.d.ts
vendored
8
unraid-ui/src/global.d.ts
vendored
@@ -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 {};
|
||||
15
web/.vscode/settings.json
vendored
15
web/.vscode/settings.json
vendored
@@ -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*}", "(?:'|\"|`)([^']*)(?:'|\"|`)"]
|
||||
]
|
||||
|
||||
|
||||
}
|
||||
@@ -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:';
|
||||
|
||||
@@ -42,6 +42,7 @@ vi.mock('@vueuse/core', () => ({
|
||||
}
|
||||
return ref(storage.get(key) ?? initialValue);
|
||||
},
|
||||
createSharedComposable: (fn: unknown) => fn,
|
||||
}));
|
||||
|
||||
vi.mock('@unraid/ui', () => ({
|
||||
|
||||
@@ -135,6 +135,8 @@ describe('mount-engine', () => {
|
||||
// Clean up DOM
|
||||
document.body.innerHTML = '';
|
||||
lastUAppPortal = undefined;
|
||||
|
||||
mockApolloClient.query.mockResolvedValue({ data: { notifications: { settings: {} } } });
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
|
||||
@@ -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;
|
||||
}),
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
};
|
||||
|
||||
3
web/auto-imports.d.ts
vendored
3
web/auto-imports.d.ts
vendored
@@ -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
31
web/components.d.ts
vendored
@@ -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']
|
||||
}
|
||||
|
||||
@@ -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
|
||||
{
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -92,4 +92,4 @@ iframe#progressFrame {
|
||||
.has-banner-gradient #header.image > * {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
}
|
||||
@@ -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');
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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';
|
||||
}
|
||||
|
||||
@@ -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',
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
28
web/src/components/Docker/docker-active-container.query.ts
Normal file
28
web/src/components/Docker/docker-active-container.query.ts
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
44
web/src/components/Docker/docker-rename-folder.mutation.ts
Normal file
44
web/src/components/Docker/docker-rename-folder.mutation.ts
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
@@ -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);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
23
web/src/components/Notifications/constants.ts
Normal file
23
web/src/components/Notifications/constants.ts
Normal 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',
|
||||
};
|
||||
@@ -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 {
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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',
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
|
||||
@@ -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',
|
||||
},
|
||||
];
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user