chore: nunjucks template engine for test pages (#1783)

This commit is contained in:
Eli Bosley
2025-11-13 11:35:43 -05:00
committed by GitHub
parent 64eb9ce9b5
commit 45cda4af80
33 changed files with 2412 additions and 2102 deletions

3
.gitignore vendored
View File

@@ -123,3 +123,6 @@ api/dev/Unraid.net/myservers.cfg
# local Mise settings
.mise.toml
# Compiled test pages (generated from Nunjucks templates)
web/public/test-pages/*.html

40
pnpm-lock.yaml generated
View File

@@ -1221,6 +1221,9 @@ importers:
'@types/node':
specifier: 22.18.0
version: 22.18.0
'@types/nunjucks':
specifier: ^3.2.6
version: 3.2.6
'@types/semver':
specifier: 7.7.0
version: 7.7.0
@@ -1284,6 +1287,9 @@ importers:
lodash-es:
specifier: 4.17.21
version: 4.17.21
nunjucks:
specifier: 3.2.4
version: 3.2.4(chokidar@3.6.0)
prettier:
specifier: 3.6.2
version: 3.6.2
@@ -4912,6 +4918,9 @@ packages:
'@types/normalize-package-data@2.4.4':
resolution: {integrity: sha512-37i+OaWTh9qeK4LSHPsyRC7NahnGotNuZvjLSgcPzblpHB3rrCJxAOgI5gCdKm7coonsaX1Of0ILiTcnZjbfxA==}
'@types/nunjucks@3.2.6':
resolution: {integrity: sha512-pHiGtf83na1nCzliuAdq8GowYiXvH5l931xZ0YEHaLMNFgynpEqx+IPStlu7UaDkehfvl01e4x/9Tpwhy7Ue3w==}
'@types/parse-path@7.1.0':
resolution: {integrity: sha512-EULJ8LApcVEPbrfND0cRQqutIOdiIgJ1Mgrhpy755r14xMohPTEpkV/k28SJvuOs9bHRFW8x+KeDAEPiGQPB9Q==}
deprecated: This is a stub types definition. parse-path provides its own type definitions, so you do not need this installed.
@@ -5662,6 +5671,9 @@ packages:
resolution: {integrity: sha512-FNoYzHawTMk/6KMQoEG5O4PuioX19UbwdQKF44yw0nLfOypfQdjtfZzo/UIJWAJ23sNIFbD1Ug9lbaDGMwbqQA==}
engines: {node: '>=8'}
a-sync-waterfall@1.0.1:
resolution: {integrity: sha512-RYTOHHdWipFUliRFMCS4X2Yn2X8M87V/OpSqWzKKOGhzqyUxzyVmhHDH9sAvG+ZuQf/TAOFsLCpMw09I1ufUnA==}
abbrev@3.0.0:
resolution: {integrity: sha512-+/kfrslGQ7TNV2ecmQwMJj/B65g5KVq1/L3SGVZ3tCYGqlzFuFCGBZJtMP99wH3NpEUyAjn0zPdPUg0D+DwrOA==}
engines: {node: ^18.17.0 || >=20.5.0}
@@ -6430,6 +6442,10 @@ packages:
commander@2.20.3:
resolution: {integrity: sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==}
commander@5.1.0:
resolution: {integrity: sha512-P0CysNDQ7rtVw4QIQtm+MRxV66vKFSvlsQvGYXZWR3qFU0jlMKHZZZgw8e+8DSah4UDKMqnknRDQz+xuQXQ/Zg==}
engines: {node: '>= 6'}
commander@6.2.1:
resolution: {integrity: sha512-U7VdrJFnJgo4xjrHpTzu0yrHPGImdsmD95ZlgYSEajAn2JKzDhDTPG9kBTefmObL2w/ngeZnilk+OV9CG3d7UA==}
engines: {node: '>= 6'}
@@ -9674,6 +9690,16 @@ packages:
nullthrows@1.1.1:
resolution: {integrity: sha512-2vPPEi+Z7WqML2jZYddDIfy5Dqb0r2fze2zTxNNknZaFpVHU3mFB3R+DWeJWGVx0ecvttSGlJTI+WG+8Z4cDWw==}
nunjucks@3.2.4:
resolution: {integrity: sha512-26XRV6BhkgK0VOxfbU5cQI+ICFUtMLixv1noZn1tGU38kQH5A5nmmbk/O45xdyBhD1esk47nKrY0mvQpZIhRjQ==}
engines: {node: '>= 6.9.0'}
hasBin: true
peerDependencies:
chokidar: ^3.3.0
peerDependenciesMeta:
chokidar:
optional: true
nuxt@4.1.1:
resolution: {integrity: sha512-xLDbWgz3ggAfUjcbmTzmLLPWOEB61thnjnqyasZlYyh/Ty2EDT1qvOiM9HT+9ycBxElI2DmyYewY8WOPRxWMiQ==}
engines: {node: ^20.19.0 || >=22.12.0}
@@ -16869,6 +16895,8 @@ snapshots:
'@types/normalize-package-data@2.4.4': {}
'@types/nunjucks@3.2.6': {}
'@types/parse-path@7.1.0':
dependencies:
parse-path: 7.1.0
@@ -17763,6 +17791,8 @@ snapshots:
dependencies:
tslib: 2.8.1
a-sync-waterfall@1.0.1: {}
abbrev@3.0.0: {}
abind@1.0.5: {}
@@ -18606,6 +18636,8 @@ snapshots:
commander@2.20.3: {}
commander@5.1.0: {}
commander@6.2.1: {}
commander@9.5.0:
@@ -22114,6 +22146,14 @@ snapshots:
nullthrows@1.1.1: {}
nunjucks@3.2.4(chokidar@3.6.0):
dependencies:
a-sync-waterfall: 1.0.1
asap: 2.0.6
commander: 5.1.0
optionalDependencies:
chokidar: 3.6.0
nuxt@4.1.1(@netlify/blobs@9.1.2)(@parcel/watcher@2.5.1)(@types/node@22.18.0)(@vue/compiler-sfc@3.5.20)(db0@0.3.2)(eslint@9.34.0(jiti@2.5.1))(ioredis@5.7.0)(lightningcss@1.30.1)(magicast@0.3.5)(meow@13.2.0)(optionator@0.9.4)(rollup@4.50.1)(stylus@0.57.0)(terser@5.43.1)(tsx@4.20.5)(typescript@5.9.2)(vite@7.1.3(@types/node@22.18.0)(jiti@2.5.1)(lightningcss@1.30.1)(stylus@0.57.0)(terser@5.43.1)(tsx@4.20.5)(yaml@2.8.1))(vue-tsc@3.0.6(typescript@5.9.2))(xml2js@0.6.2)(yaml@2.8.1):
dependencies:
'@nuxt/cli': 3.28.0(magicast@0.3.5)

View File

@@ -0,0 +1,45 @@
import { getTranslator, type JsonFormsState, type JsonFormsSubStates } from '@jsonforms/core';
import { inject } from 'vue';
type TranslationContext = Record<string, unknown> | undefined;
/**
* Exposes helpers that translate JsonForms i18n keys by reusing the
* translator registered on the nearest <JsonForms> provider.
*/
export function useJsonFormsTranslation() {
const jsonforms = inject<JsonFormsSubStates | undefined>('jsonforms', undefined);
const translateKey = (key?: string, defaultMessage?: string, context?: TranslationContext) => {
if (!key) return defaultMessage;
if (!jsonforms) {
return defaultMessage;
}
const translator = getTranslator()({ jsonforms } as JsonFormsState);
const translated = translator?.(key, defaultMessage, context);
if (typeof translated === 'string' && translated === key) {
return defaultMessage;
}
return translated ?? defaultMessage;
};
const translateWithPrefix = (
prefix?: string,
suffix?: string,
defaultMessage?: string,
context?: TranslationContext
) => {
if (!prefix) return defaultMessage;
const key = suffix ? `${prefix}.${suffix}` : prefix;
return translateKey(key, defaultMessage, context);
};
return {
translateKey,
translateWithPrefix,
};
}

View File

@@ -35,30 +35,30 @@ type LocaleMessages = typeof enUS;
const localeMessages: Record<string, LocaleMessages> = {
en_US: enUS,
ar,
bn,
ca,
cs,
da,
de,
es,
fr,
hi,
hr,
hu,
it,
ja,
ko,
lv,
nl,
no,
pl,
pt,
ro,
ru,
sv,
uk,
zh,
ar: ar as LocaleMessages,
bn: bn as LocaleMessages,
ca: ca as LocaleMessages,
cs: cs as LocaleMessages,
da: da as LocaleMessages,
de: de as LocaleMessages,
es: es as LocaleMessages,
fr: fr as LocaleMessages,
hi: hi as LocaleMessages,
hr: hr as LocaleMessages,
hu: hu as LocaleMessages,
it: it as LocaleMessages,
ja: ja as LocaleMessages,
ko: ko as LocaleMessages,
lv: lv as LocaleMessages,
nl: nl as LocaleMessages,
no: no as LocaleMessages,
pl: pl as LocaleMessages,
pt: pt as LocaleMessages,
ro: ro as LocaleMessages,
ru: ru as LocaleMessages,
sv: sv as LocaleMessages,
uk: uk as LocaleMessages,
zh: zh as LocaleMessages,
};
type AnyObject = Record<string, unknown>;

1
web/components.d.ts vendored
View File

@@ -44,6 +44,7 @@ declare module 'vue' {
DeveloperAuthorizationLink: typeof import('./src/components/ApiKey/DeveloperAuthorizationLink.vue')['default']
'DevModalTest.standalone': typeof import('./src/components/DevModalTest.standalone.vue')['default']
DevSettings: typeof import('./src/components/DevSettings.vue')['default']
'DevThemeSwitcher.standalone': typeof import('./src/components/DevThemeSwitcher.standalone.vue')['default']
Downgrade: typeof import('./src/components/UpdateOs/Downgrade.vue')['default']
'DowngradeOs.standalone': typeof import('./src/components/DowngradeOs.standalone.vue')['default']
'DownloadApiLogs.standalone': typeof import('./src/components/DownloadApiLogs.standalone.vue')['default']

View File

@@ -6,7 +6,7 @@
"license": "GPL-2.0-or-later",
"scripts": {
"// Development": "",
"predev": "node ./scripts/build-ui-if-needed.js",
"predev": "node ./scripts/build-ui-if-needed.js && node ./scripts/build-test-pages.js",
"dev": "vite --mode development",
"preview": "vite preview",
"serve": "NODE_ENV=production PORT=${PORT:-4321} vite preview --port ${PORT:-4321}",
@@ -16,6 +16,7 @@
"build": "NODE_ENV=production vite build && pnpm run manifest-ts",
"prebuild:watch": "pnpm predev",
"build:watch": "vite build --watch && pnpm run manifest-ts",
"test-pages:build": "node ./scripts/build-test-pages.js",
"manifest-ts": "node ./scripts/add-timestamp-standalone-manifest.js",
"// Deployment": "",
"unraid:deploy": "pnpm build:dev",
@@ -57,6 +58,7 @@
"@types/crypto-js": "4.2.2",
"@types/eslint-config-prettier": "6.11.3",
"@types/node": "22.18.0",
"@types/nunjucks": "^3.2.6",
"@types/semver": "7.7.0",
"@typescript-eslint/eslint-plugin": "8.41.0",
"@unraid/tailwind-rem-to-rem": "2.0.0",
@@ -78,6 +80,7 @@
"happy-dom": "18.0.1",
"kebab-case": "2.0.2",
"lodash-es": "4.17.21",
"nunjucks": "3.2.4",
"prettier": "3.6.2",
"prettier-plugin-tailwindcss": "0.6.14",
"tailwindcss": "4.1.12",

View File

@@ -1,371 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>All Components - Unraid Component Test</title>
<script src="https://code.jquery.com/jquery-3.7.1.min.js"></script>
<style>
body {
font-family: system-ui, -apple-system, sans-serif;
margin: 0;
padding: 20px;
background: #f3f4f6;
}
.container {
max-width: 1200px;
margin: 0 auto;
padding: 20px;
}
.component-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
gap: 20px;
margin-top: 20px;
}
.component-card {
background: white;
border-radius: 8px;
padding: 20px;
box-shadow: 0 1px 3px rgba(0,0,0,0.1);
}
.component-card h3 {
margin: 0 0 10px 0;
color: #1f2937;
font-size: 16px;
font-weight: 600;
}
.component-card .selector {
font-family: monospace;
font-size: 12px;
color: #6b7280;
margin-bottom: 15px;
background: #f3f4f6;
padding: 4px 8px;
border-radius: 4px;
display: inline-block;
}
.component-mount {
min-height: 50px;
border: 1px dashed #e5e7eb;
border-radius: 4px;
padding: 10px;
position: relative;
}
.status {
position: absolute;
top: 5px;
right: 5px;
width: 8px;
height: 8px;
border-radius: 50%;
background: #10b981;
}
.category-header {
background: #1f2937;
color: white;
padding: 10px 15px;
border-radius: 4px;
margin: 30px 0 15px 0;
font-weight: 600;
}
</style>
</head>
<body>
<div class="container">
<!-- Authentication & User -->
<div class="category-header">👤 Authentication & User</div>
<div class="component-grid">
<div class="component-card">
<h3>Authentication</h3>
<span class="selector">&lt;unraid-auth&gt;</span>
<div class="component-mount">
<unraid-auth></unraid-auth>
</div>
</div>
<div class="component-card">
<h3>User Profile</h3>
<span class="selector">&lt;unraid-user-profile&gt;</span>
<div class="component-mount">
<unraid-user-profile></unraid-user-profile>
</div>
</div>
<div class="component-card">
<h3>SSO Button</h3>
<span class="selector">&lt;unraid-sso-button&gt;</span>
<div class="component-mount">
<unraid-sso-button></unraid-sso-button>
</div>
</div>
<div class="component-card">
<h3>Registration</h3>
<span class="selector">&lt;unraid-registration&gt;</span>
<div class="component-mount">
<unraid-registration></unraid-registration>
</div>
</div>
</div>
<!-- System & Settings -->
<div class="category-header">⚙️ System & Settings</div>
<div class="component-grid">
<div class="component-card">
<h3>Connect Settings</h3>
<span class="selector">&lt;unraid-connect-settings&gt;</span>
<div class="component-mount">
<unraid-connect-settings></unraid-connect-settings>
</div>
</div>
<div class="component-card">
<h3>Theme Switcher</h3>
<span class="selector">&lt;unraid-theme-switcher&gt;</span>
<div class="component-mount">
<unraid-theme-switcher current="white"></unraid-theme-switcher>
</div>
</div>
<div class="component-card">
<h3>Header OS Version</h3>
<span class="selector">&lt;unraid-header-os-version&gt;</span>
<div class="component-mount">
<unraid-header-os-version></unraid-header-os-version>
</div>
</div>
<div class="component-card">
<h3>WAN IP Check</h3>
<span class="selector">&lt;unraid-wan-ip-check&gt;</span>
<div class="component-mount">
<unraid-wan-ip-check php-wan-ip="192.168.1.1"></unraid-wan-ip-check>
</div>
</div>
</div>
<!-- OS Management -->
<div class="category-header">💿 OS Management</div>
<div class="component-grid">
<div class="component-card">
<h3>Update OS</h3>
<span class="selector">&lt;unraid-update-os&gt;</span>
<div class="component-mount">
<unraid-update-os></unraid-update-os>
</div>
</div>
<div class="component-card">
<h3>Downgrade OS</h3>
<span class="selector">&lt;unraid-downgrade-os&gt;</span>
<div class="component-mount">
<unraid-downgrade-os></unraid-downgrade-os>
</div>
</div>
</div>
<!-- API & Developer -->
<div class="category-header">🔧 API & Developer</div>
<div class="component-grid">
<div class="component-card">
<h3>API Key Manager</h3>
<span class="selector">&lt;unraid-api-key-manager&gt;</span>
<div class="component-mount">
<unraid-api-key-manager></unraid-api-key-manager>
</div>
</div>
<div class="component-card">
<h3>API Key Authorize</h3>
<span class="selector">&lt;unraid-api-key-authorize&gt;</span>
<div class="component-mount">
<unraid-api-key-authorize></unraid-api-key-authorize>
</div>
</div>
<div class="component-card">
<h3>Download API Logs</h3>
<span class="selector">&lt;unraid-download-api-logs&gt;</span>
<div class="component-mount">
<unraid-download-api-logs></unraid-download-api-logs>
</div>
</div>
<div class="component-card">
<h3>Log Viewer</h3>
<span class="selector">&lt;unraid-log-viewer&gt;</span>
<div class="component-mount">
<unraid-log-viewer></unraid-log-viewer>
</div>
</div>
</div>
<!-- UI Components -->
<div class="category-header">🎨 UI Components</div>
<div class="component-grid">
<div class="component-card">
<h3>Modals</h3>
<span class="selector">&lt;unraid-modals&gt;</span>
<div class="component-mount">
<unraid-modals></unraid-modals>
</div>
</div>
<div class="component-card">
<h3>Welcome Modal</h3>
<span class="selector">&lt;unraid-welcome-modal&gt;</span>
<div class="component-mount">
<unraid-welcome-modal></unraid-welcome-modal>
</div>
</div>
<div class="component-card">
<h3>Dev Modal Test</h3>
<span class="selector">&lt;unraid-dev-modal-test&gt;</span>
<div class="component-mount">
<unraid-dev-modal-test></unraid-dev-modal-test>
</div>
</div>
<div class="component-card">
<h3>Toaster</h3>
<span class="selector">&lt;unraid-toaster&gt;</span>
<div class="component-mount">
<unraid-toaster></unraid-toaster>
</div>
</div>
</div>
<!-- Test Controls -->
<div class="category-header">🎮 Test Controls</div>
<div style="background: white; padding: 20px; border-radius: 8px; margin-top: 15px;">
<h3>Language Selection</h3>
<div style="margin-bottom: 20px;">
<unraid-locale-switcher></unraid-locale-switcher>
</div>
<h3>jQuery Interaction Tests</h3>
<div style="display: flex; gap: 10px; flex-wrap: wrap; margin-top: 15px;">
<button id="test-notification" class="test-btn">Trigger Notification</button>
<button id="test-modal" class="test-btn">Open Test Modal</button>
<button id="test-theme" class="test-btn">Toggle Theme</button>
<button id="test-update-profile" class="test-btn">Update Profile Data</button>
<button id="test-settings" class="test-btn">Update Settings</button>
</div>
<div style="margin-top: 20px;">
<h4>Console Output</h4>
<div id="test-output" style="background: #1f2937; color: #10b981; padding: 10px; border-radius: 4px; font-family: monospace; font-size: 12px; min-height: 100px; max-height: 200px; overflow-y: auto;">
> Ready for testing...
</div>
</div>
</div>
</div>
<style>
.test-btn {
padding: 8px 16px;
background: #3b82f6;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 14px;
}
.test-btn:hover {
background: #2563eb;
}
</style>
<!-- Load the manifest and inject resources -->
<script src="/test-pages/language-switcher.js"></script>
<script src="/test-pages/load-manifest.js"></script>
<script src="/test-pages/test-server-state.js"></script>
<script src="/test-pages/shared-header.js"></script>
<!-- Test interactions -->
<script>
$(document).ready(function() {
const output = $('#test-output');
function log(message) {
// Use shared header's testLog if available, otherwise local log
if (window.testLog) {
window.testLog(message);
}
if (output.length) {
const timestamp = new Date().toLocaleTimeString();
output.append('\n> [' + timestamp + '] ' + message);
output.scrollTop(output[0].scrollHeight);
}
}
// Test notification
$('#test-notification').on('click', function() {
log('Triggering notification...');
const event = new CustomEvent('unraid:notification', {
detail: {
title: 'Test Notification',
message: 'This is a test from jQuery!',
type: 'success'
}
});
document.dispatchEvent(event);
});
// Test modal
$('#test-modal').on('click', function() {
log('Opening test modal...');
// This would trigger the modal system
window.dispatchEvent(new CustomEvent('unraid:open-modal', {
detail: { modalId: 'test-modal' }
}));
});
// Test theme toggle
$('#test-theme').on('click', function() {
log('Toggling theme...');
const currentTheme = $('body').hasClass('dark') ? 'light' : 'dark';
$('body').toggleClass('dark');
log('Theme changed to: ' + currentTheme);
});
// Test profile update
$('#test-update-profile').on('click', function() {
log('Updating profile data...');
const profileData = {
name: 'Test User ' + Math.floor(Math.random() * 100),
email: 'test' + Math.floor(Math.random() * 100) + '@example.com',
username: 'testuser'
};
$('unraid-user-profile').attr('server', JSON.stringify(profileData));
log('Profile updated: ' + JSON.stringify(profileData));
});
// Test settings update
$('#test-settings').on('click', function() {
log('Updating connect settings...');
const settings = {
enabled: Math.random() > 0.5,
url: 'https://connect.unraid.net',
lastSync: new Date().toISOString()
};
$('unraid-connect-settings').attr('initial-settings', JSON.stringify(settings));
log('Settings updated: ' + JSON.stringify(settings));
});
// Listen for component events
$(document).on('unraid:theme-changed', function(e, data) {
log('Theme changed event received: ' + JSON.stringify(data));
});
$(document).on('unraid:settings-saved', function(e, data) {
log('Settings saved event received: ' + JSON.stringify(data));
});
log('Test page initialized - all components loaded');
});
</script>
</body>
</html>

View File

@@ -1,218 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Authentication Flow - Unraid Component Test</title>
<script src="https://code.jquery.com/jquery-3.7.1.min.js"></script>
<style>
body {
font-family: system-ui, -apple-system, sans-serif;
margin: 0;
padding: 0;
background: #f3f4f6;
}
.auth-container {
max-width: 600px;
margin: 50px auto;
padding: 20px;
}
.auth-card {
background: white;
border-radius: 8px;
padding: 30px;
box-shadow: 0 1px 3px rgba(0,0,0,0.1);
margin-bottom: 20px;
}
.auth-card h2 {
margin: 0 0 20px 0;
color: #1f2937;
}
.user-info {
background: #f3f4f6;
padding: 15px;
border-radius: 6px;
margin-top: 20px;
}
.status-badge {
display: inline-block;
padding: 4px 12px;
border-radius: 20px;
font-size: 12px;
font-weight: 600;
margin-top: 10px;
}
.status-badge.authenticated {
background: #10b981;
color: white;
}
.status-badge.unauthenticated {
background: #ef4444;
color: white;
}
</style>
</head>
<body>
<div class="auth-container">
<!-- Login Card -->
<div class="auth-card">
<h2>🔐 Authentication</h2>
<unraid-auth></unraid-auth>
<div class="user-info">
<strong>Session Status:</strong>
<div id="auth-status">
<span class="status-badge unauthenticated">Not Authenticated</span>
</div>
</div>
</div>
<!-- SSO Options -->
<div class="auth-card">
<h2>🔗 Single Sign-On</h2>
<p style="color: #6b7280; margin-bottom: 20px;">Alternative authentication methods</p>
<unraid-sso-button></unraid-sso-button>
</div>
<!-- User Profile -->
<div class="auth-card">
<h2>👤 User Profile</h2>
<p style="color: #6b7280; margin-bottom: 20px;">Displays when authenticated</p>
<unraid-user-profile id="user-profile"></unraid-user-profile>
</div>
<!-- Registration -->
<div class="auth-card">
<h2>📝 System Registration</h2>
<unraid-registration></unraid-registration>
</div>
<!-- Test Controls -->
<div class="auth-card" style="background: #1f2937; color: white;">
<h2 style="color: white;">🧪 Test Controls</h2>
<div style="display: flex; gap: 10px; flex-wrap: wrap;">
<button id="simulate-login" class="btn">Simulate Login</button>
<button id="simulate-logout" class="btn">Simulate Logout</button>
<button id="update-profile" class="btn">Update Profile</button>
<button id="check-session" class="btn">Check Session</button>
</div>
<div id="console-output" style="margin-top: 20px; padding: 10px; background: black; border-radius: 4px; font-family: monospace; font-size: 12px; min-height: 80px;">
> Ready...
</div>
</div>
</div>
<style>
.btn {
padding: 8px 16px;
background: #3b82f6;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 14px;
}
.btn:hover {
background: #2563eb;
}
</style>
<!-- Global Modals -->
<unraid-modals></unraid-modals>
<!-- Load the manifest -->
<script src="/test-pages/language-switcher.js"></script>
<script src="/test-pages/load-manifest.js"></script>
<!-- Authentication test logic -->
<script>
$(document).ready(function() {
const output = $('#console-output');
let isAuthenticated = false;
function log(message) {
const timestamp = new Date().toLocaleTimeString();
output.append('\n> [' + timestamp + '] ' + message);
output.scrollTop(output[0].scrollHeight);
}
// Simulate login
$('#simulate-login').on('click', function() {
log('Simulating login...');
isAuthenticated = true;
const userData = {
username: 'admin',
email: 'admin@unraid.local',
name: 'Administrator',
avatarUrl: '/webGui/images/default-avatar.png',
role: 'admin',
sessionId: 'sess_' + Math.random().toString(36).substr(2, 9)
};
// Update components
$('#user-profile').attr('server', JSON.stringify(userData));
$('#auth-status').html('<span class="status-badge authenticated">Authenticated as ' + userData.username + '</span>');
// Dispatch login event
$(document).trigger('unraid:auth-login', userData);
log('Login successful: ' + userData.username);
});
// Simulate logout
$('#simulate-logout').on('click', function() {
log('Simulating logout...');
isAuthenticated = false;
$('#user-profile').attr('server', '{}');
$('#auth-status').html('<span class="status-badge unauthenticated">Not Authenticated</span>');
// Dispatch logout event
$(document).trigger('unraid:auth-logout');
log('Logged out successfully');
});
// Update profile
$('#update-profile').on('click', function() {
if (!isAuthenticated) {
log('Error: Not authenticated');
return;
}
log('Updating profile...');
const updatedData = {
username: 'admin',
email: 'newemail@unraid.local',
name: 'Updated Admin',
lastModified: new Date().toISOString()
};
$('#user-profile').attr('server', JSON.stringify(updatedData));
log('Profile updated');
});
// Check session
$('#check-session').on('click', function() {
log('Checking session status...');
log('Authenticated: ' + (isAuthenticated ? 'Yes' : 'No'));
if (isAuthenticated) {
// Would normally make an API call here
log('Session valid until: ' + new Date(Date.now() + 3600000).toLocaleTimeString());
}
});
// Listen for auth events from components
$(document).on('unraid:auth-required', function() {
log('Authentication required by component');
});
$(document).on('unraid:session-expired', function() {
log('Session expired - please login again');
$('#simulate-logout').click();
});
log('Authentication test page ready');
});
</script>
</body>
</html>

View File

@@ -1,357 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Component Mounting Test - Unraid Component Test</title>
<script src="https://code.jquery.com/jquery-3.7.1.min.js"></script>
<style>
body {
font-family: system-ui, -apple-system, sans-serif;
margin: 0;
padding: 20px;
background: #f3f4f6;
}
.container {
max-width: 1200px;
margin: 0 auto;
}
.test-section {
background: white;
border-radius: 8px;
padding: 20px;
margin-bottom: 20px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
h1 {
color: #333;
margin-bottom: 10px;
}
h2 {
color: #666;
margin-top: 0;
margin-bottom: 15px;
font-size: 18px;
}
.status {
padding: 10px;
border-radius: 4px;
margin-bottom: 10px;
font-family: monospace;
font-size: 14px;
}
.status.loading {
background: #fff3cd;
color: #856404;
}
.status.success {
background: #d4edda;
color: #155724;
}
.status.error {
background: #f8d7da;
color: #721c24;
}
.mount-target {
padding: 20px;
background: #fafafa;
border: 2px dashed #ddd;
border-radius: 4px;
min-height: 100px;
position: relative;
}
.mount-target::before {
content: attr(data-label);
position: absolute;
top: -10px;
left: 10px;
background: white;
padding: 0 5px;
color: #999;
font-size: 12px;
}
.debug-info {
margin-top: 20px;
padding: 15px;
background: #f8f9fa;
border-radius: 4px;
font-family: monospace;
font-size: 12px;
white-space: pre-wrap;
}
.multiple-mounts {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
gap: 20px;
margin-top: 20px;
}
.test-button {
background: #007bff;
color: white;
border: none;
padding: 10px 20px;
border-radius: 4px;
cursor: pointer;
margin-right: 10px;
}
.test-button:hover {
background: #0056b3;
}
.test-button:disabled {
background: #ccc;
cursor: not-allowed;
}
</style>
</head>
<body>
<!-- Teleport target for dropdowns and modals -->
<div id="teleports"></div>
<!-- Mount point for Modals component -->
<unraid-modals></unraid-modals>
<div class="container">
<h1>🧪 Standalone Vue Apps Test Page</h1>
<div id="status" class="status loading">Loading...</div>
<!-- Test Section 1: Single Mount -->
<div class="test-section">
<h2>Test 1: Single Component Mount</h2>
<p>Testing single instance of HeaderOsVersion component</p>
<div class="mount-target" data-label="HeaderOsVersion Mount">
<unraid-header-os-version></unraid-header-os-version>
</div>
</div>
<!-- Test Section 2: Multiple Mounts -->
<div class="test-section">
<h2>Test 2: Multiple Component Mounts (Shared Pinia Store)</h2>
<p>Testing that multiple instances share the same Pinia store</p>
<div class="multiple-mounts">
<div class="mount-target" data-label="Instance 1">
<unraid-header-os-version></unraid-header-os-version>
</div>
<div class="mount-target" data-label="Instance 2">
<unraid-header-os-version></unraid-header-os-version>
</div>
<div class="mount-target" data-label="Instance 3">
<unraid-header-os-version></unraid-header-os-version>
</div>
</div>
</div>
<!-- Test Section 3: Dynamic Mount -->
<div class="test-section">
<h2>Test 3: Dynamic Component Creation</h2>
<p>Test dynamically adding components after page load</p>
<button class="test-button" id="addComponent">Add New Component</button>
<button class="test-button" id="removeComponent">Remove Last Component</button>
<button class="test-button" id="remountAll">Remount All</button>
<div id="dynamicContainer" style="margin-top: 20px;">
<!-- Dynamic components will be added here -->
</div>
</div>
<!-- Test Section 4: Modal Testing -->
<div class="test-section">
<h2>Test 4: Modal Components</h2>
<p>Test modal functionality</p>
<button class="test-button" onclick="testTrialModal()">Open Trial Modal</button>
<button class="test-button" onclick="testUpdateModal()">Open Update Modal</button>
<button class="test-button" onclick="testApiKeyModal()">Open API Key Modal</button>
<div style="margin-top: 10px;">
<small>Note: Modals require proper store state to display</small>
</div>
</div>
<!-- Debug Info -->
<div class="test-section">
<h2>Debug Information</h2>
<div class="debug-info" id="debugInfo">
Waiting for initialization...
</div>
</div>
</div>
<!-- Configuration for local testing -->
<script>
// Set GraphQL endpoint - handled by Vite proxy in dev mode
window.GRAPHQL_ENDPOINT = window.location.port === '3000' ? '/graphql' : 'http://localhost:3001/graphql';
// Mock webGui path for images
window.__WEBGUI_PATH__ = '';
// Add some debug logging
window.addEventListener('DOMContentLoaded', () => {
const status = document.getElementById('status');
const debugInfo = document.getElementById('debugInfo');
// Log when scripts are loaded
const observer = new MutationObserver((mutations) => {
mutations.forEach((mutation) => {
if (mutation.type === 'childList') {
mutation.addedNodes.forEach((node) => {
if (node.nodeName === 'SCRIPT') {
console.log('Script loaded:', node.src || 'inline');
}
});
}
});
});
observer.observe(document.head, { childList: true });
observer.observe(document.body, { childList: true });
// Check for Vue app mounting
let checkInterval = setInterval(() => {
const mountedElements = document.querySelectorAll('[data-vue-mounted="true"]');
let totalComponents = document.querySelectorAll('unraid-header-os-version, unraid-modals').length;
let mountedCount = mountedElements.length;
if (mountedCount > 0) {
status.className = 'status success';
status.textContent = `✅ Successfully mounted ${mountedCount} component(s)`;
// Update debug info
debugInfo.textContent = `
Components Found: ${totalComponents}
Components Mounted: ${mountedCount}
Unified Vue App: ${window.__unifiedApp ? 'Initialized' : 'Not found'}
Mounted Components: ${window.__mountedComponents ? window.__mountedComponents.length : 0}
Pinia Store: ${window.globalPinia ? 'Initialized' : 'Not found'}
GraphQL Endpoint: ${window.GRAPHQL_ENDPOINT || 'Not configured'}
`.trim();
clearInterval(checkInterval);
// Log to test console if available
if (window.testLog) {
window.testLog(`Mounted ${mountedCount} components successfully`, 'success');
}
}
}, 500);
// Timeout after 10 seconds
setTimeout(() => {
if (checkInterval) {
clearInterval(checkInterval);
if (status.className === 'status loading') {
status.className = 'status error';
status.textContent = '❌ Failed to mount components (timeout)';
}
}
}, 10000);
});
// Dynamic component controls
document.addEventListener('DOMContentLoaded', () => {
let dynamicCount = 0;
const dynamicContainer = document.getElementById('dynamicContainer');
document.getElementById('addComponent').addEventListener('click', () => {
dynamicCount++;
const wrapper = document.createElement('div');
wrapper.className = 'mount-target';
wrapper.setAttribute('data-label', `Dynamic Instance ${dynamicCount}`);
wrapper.style.marginBottom = '10px';
// Create the custom element
const element = document.createElement('unraid-header-os-version');
wrapper.appendChild(element);
dynamicContainer.appendChild(wrapper);
// The unified mount system doesn't support dynamic addition after initial mount
// For now, we'll just add the element and note it won't be mounted until reload
console.log('Note: Dynamic components require page reload to mount with the unified app system');
// Show a message that reload is needed
if (!wrapper.querySelector('.reload-note')) {
const note = document.createElement('div');
note.className = 'reload-note';
note.style.cssText = 'color: #666; font-size: 12px; margin-top: 10px;';
note.textContent = 'Reload page to mount this component';
wrapper.appendChild(note);
}
});
document.getElementById('removeComponent').addEventListener('click', () => {
const lastChild = dynamicContainer.lastElementChild;
if (lastChild) {
// If component was mounted, unmount it properly
const mountedElement = lastChild.querySelector('[data-vue-mounted="true"]');
if (mountedElement && window.__mountedComponents) {
const componentIndex = window.__mountedComponents.findIndex(c => c.element === mountedElement);
if (componentIndex !== -1) {
window.__mountedComponents[componentIndex].unmount();
window.__mountedComponents.splice(componentIndex, 1);
}
}
dynamicContainer.removeChild(lastChild);
dynamicCount = Math.max(0, dynamicCount - 1);
}
});
document.getElementById('remountAll').addEventListener('click', () => {
console.log('Remounting all components...');
// The unified app requires a full reload to remount
location.reload();
});
});
// Modal test functions
window.testTrialModal = function() {
console.log('Testing trial modal...');
if (window.globalPinia) {
const trialStore = window.globalPinia._s.get('trial');
if (trialStore) {
trialStore.trialModalVisible = true;
console.log('Trial modal triggered');
} else {
console.error('Trial store not found');
}
}
};
window.testUpdateModal = function() {
console.log('Testing update modal...');
if (window.globalPinia) {
const updateStore = window.globalPinia._s.get('updateOs');
if (updateStore) {
updateStore.updateOsModalVisible = true;
console.log('Update modal triggered');
} else {
console.error('Update store not found');
}
}
};
window.testApiKeyModal = function() {
console.log('Testing API key modal...');
if (window.globalPinia) {
const apiKeyStore = window.globalPinia._s.get('apiKey');
if (apiKeyStore) {
apiKeyStore.showCreateModal = true;
console.log('API key modal triggered');
} else {
console.error('API key store not found');
}
}
};
</script>
<!-- Load shared header and manifest resources -->
<script src="/test-pages/language-switcher.js"></script>
<script src="/test-pages/shared-header.js"></script>
<script src="/test-pages/load-manifest.js"></script>
<script src="/test-pages/test-server-state.js"></script>
<script>
// Initialize page
document.addEventListener('DOMContentLoaded', () => {
if (window.initializeSharedHeader) {
window.initializeSharedHeader('Component Mounting Test');
}
});
</script>
</body>
</html>

View File

@@ -1,204 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Dashboard - Unraid Component Test</title>
<script src="https://code.jquery.com/jquery-3.7.1.min.js"></script>
<style>
body {
font-family: system-ui, -apple-system, sans-serif;
margin: 0;
padding: 20px;
background: #f3f4f6;
}
.dashboard-header {
background: #1f2937;
color: white;
padding: 16px 20px;
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
border-radius: 8px;
}
.header-left {
display: flex;
align-items: center;
gap: 20px;
}
.logo {
font-size: 24px;
font-weight: bold;
}
.main-content {
padding: 20px;
max-width: 1200px;
margin: 0 auto;
}
.card {
background: white;
border-radius: 8px;
padding: 20px;
margin-bottom: 20px;
box-shadow: 0 1px 3px rgba(0,0,0,0.1);
}
.card h2 {
margin-top: 0;
color: #1f2937;
}
.breadcrumb {
padding: 10px 20px;
background: white;
border-bottom: 1px solid #e5e7eb;
}
.breadcrumb a {
color: #3b82f6;
text-decoration: none;
}
.component-mount {
padding: 10px;
border: 2px dashed #e5e7eb;
border-radius: 4px;
min-height: 50px;
position: relative;
}
.component-mount::before {
content: attr(data-component);
position: absolute;
top: -10px;
left: 10px;
background: white;
padding: 0 5px;
font-size: 12px;
color: #6b7280;
}
</style>
</head>
<body>
<!-- Main dashboard content -->
<div class="dashboard-header">
<div class="header-left">
<div class="logo">UNRAID</div>
<!-- OS Version Component -->
<unraid-header-os-version></unraid-header-os-version>
</div>
<div class="header-right">
<!-- User Profile Component -->
<unraid-user-profile id="header-user-profile"></unraid-user-profile>
</div>
</div>
<!-- Breadcrumb -->
<div class="breadcrumb">
<a href="/test-pages/">Test Pages</a> / Dashboard
</div>
<!-- Main Content -->
<div class="main-content">
<div class="card">
<h2>System Information</h2>
<div class="component-mount" data-component="unraid-header-os-version">
<unraid-header-os-version></unraid-header-os-version>
</div>
</div>
<div class="card">
<h2>Theme Settings</h2>
<div class="component-mount" data-component="unraid-theme-switcher">
<unraid-theme-switcher current="white"></unraid-theme-switcher>
</div>
</div>
<div class="card">
<h2>Authentication</h2>
<div class="component-mount" data-component="unraid-auth">
<unraid-auth></unraid-auth>
</div>
</div>
<div class="card">
<h2>WAN IP Check</h2>
<div class="component-mount" data-component="unraid-wan-ip-check">
<unraid-wan-ip-check php-wan-ip="192.168.1.1"></unraid-wan-ip-check>
</div>
</div>
<div class="card">
<h2>API Logs</h2>
<button id="toggle-logs" style="padding: 8px 16px; background: #3b82f6; color: white; border: none; border-radius: 4px; cursor: pointer;">
Toggle Log Component
</button>
<div id="logs-container" class="component-mount" data-component="unraid-download-api-logs" style="margin-top: 10px; display: none;">
<unraid-download-api-logs></unraid-download-api-logs>
</div>
</div>
</div>
<!-- Global Modals -->
<unraid-modals></unraid-modals>
<!-- Load the manifest and inject resources (mimics PHP extractor) -->
<script src="/test-pages/language-switcher.js"></script>
<script src="/test-pages/load-manifest.js"></script>
<script src="/test-pages/test-server-state.js"></script>
<script src="/test-pages/shared-header.js"></script>
<!-- jQuery interactions mimicking Unraid -->
<script>
$(document).ready(function() {
// Example: Pass server data to user profile component via jQuery
// This mimics how Unraid PHP would set data
var serverData = {
name: 'TestServer',
version: '6.12.4',
username: 'admin',
email: 'admin@unraid.net',
avatarUrl: '/webGui/images/default-avatar.png'
};
// Set attribute on the component (Vue will pick this up)
$('#header-user-profile').attr('server', JSON.stringify(serverData));
// Toggle logs visibility with jQuery
$('#toggle-logs').on('click', function() {
$('#logs-container').slideToggle();
});
// Example: Update WAN IP dynamically (like Unraid would do after an AJAX call)
setTimeout(function() {
$('unraid-wan-ip-check').attr('php-wan-ip', '203.0.113.42');
console.log('Updated WAN IP via jQuery');
}, 3000);
// Example: Trigger component methods from jQuery
// Components can expose methods via window object
window.updateUserProfile = function(newData) {
$('#header-user-profile').attr('server', JSON.stringify(newData));
};
// Example: Listen for events from Vue components
// Components can emit custom DOM events
$(document).on('unraid:theme-changed', function(e, data) {
console.log('Theme changed to:', data.theme);
// Update body class or do other jQuery operations
$('body').toggleClass('dark-mode', data.theme === 'dark');
});
// Example: Show a notification (like Unraid's addNotification)
window.addNotification = function(title, message, type) {
// This would trigger the Vue toast/notification system
var event = new CustomEvent('unraid:notification', {
detail: { title: title, message: message, type: type }
});
document.dispatchEvent(event);
};
// Test notification after 2 seconds
setTimeout(function() {
addNotification('Test Notification', 'This is from jQuery!', 'success');
}, 2000);
});
</script>
</body>
</html>

View File

@@ -0,0 +1,249 @@
(() => {
const localeOptions = [
{ value: 'en_US', label: 'English (US)' },
{ value: 'ar', label: 'العربية (Arabic)' },
{ value: 'bn', label: 'বাংলা (Bengali)' },
{ value: 'ca', label: 'Català (Catalan)' },
{ value: 'cs', label: 'Čeština (Czech)' },
{ value: 'da', label: 'Dansk (Danish)' },
{ value: 'de', label: 'Deutsch (German)' },
{ value: 'es', label: 'Español (Spanish)' },
{ value: 'fr', label: 'Français (French)' },
{ value: 'hi', label: 'हिन्दी (Hindi)' },
{ value: 'hr', label: 'Hrvatski (Croatian)' },
{ value: 'hu', label: 'Magyar (Hungarian)' },
{ value: 'it', label: 'Italiano (Italian)' },
{ value: 'ja', label: '日本語 (Japanese)' },
{ value: 'ko', label: '한국어 (Korean)' },
{ value: 'lv', label: 'Latviešu (Latvian)' },
{ value: 'nl', label: 'Nederlands (Dutch)' },
{ value: 'no', label: 'Norsk (Norwegian)' },
{ value: 'pl', label: 'Polski (Polish)' },
{ value: 'pt', label: 'Português (Portuguese)' },
{ value: 'ro', label: 'Română (Romanian)' },
{ value: 'ru', label: 'Русский (Russian)' },
{ value: 'sv', label: 'Svenska (Swedish)' },
{ value: 'uk', label: 'Українська (Ukrainian)' },
{ value: 'zh', label: '中文 (Chinese)' },
];
if (document.getElementById('dev-tools')) {
return;
}
const style = document.createElement('style');
style.textContent = `
#dev-tools {
position: fixed;
right: 16px;
bottom: 16px;
display: flex;
flex-direction: column;
gap: 12px;
padding: 16px;
border-radius: 8px;
background: rgba(17, 24, 39, 0.95);
color: #f9fafb;
box-shadow: 0 10px 30px rgba(15, 23, 42, 0.35);
z-index: 9999;
max-width: 260px;
font-family: system-ui, -apple-system, sans-serif;
backdrop-filter: blur(8px);
}
#dev-tools h3 {
margin: 0 0 8px 0;
font-size: 14px;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.05em;
color: #60a5fa;
}
#dev-tools .control-group {
display: flex;
flex-direction: column;
gap: 6px;
}
#dev-tools label {
font-size: 12px;
font-weight: 600;
letter-spacing: 0.02em;
color: rgba(226, 232, 240, 0.9);
}
#dev-tools select {
padding: 8px 10px;
border-radius: 6px;
border: 1px solid rgba(148, 163, 184, 0.4);
background: rgba(15, 23, 42, 0.9);
color: #f9fafb;
font-size: 13px;
outline: none;
cursor: pointer;
transition: all 0.2s;
}
#dev-tools select:hover {
border-color: rgba(148, 163, 184, 0.6);
}
#dev-tools select:focus {
border-color: #3b82f6;
box-shadow: 0 0 0 2px rgba(59, 130, 246, 0.3);
}
#dev-tools .info {
font-size: 11px;
color: rgba(226, 232, 240, 0.7);
line-height: 1.4;
margin-top: 4px;
}
#dev-tools .divider {
height: 1px;
background: rgba(148, 163, 184, 0.2);
margin: 4px 0;
}
`;
const container = document.createElement('div');
container.id = 'dev-tools';
const title = document.createElement('h3');
title.textContent = '🛠️ Dev Tools';
const localeGroup = document.createElement('div');
localeGroup.className = 'control-group';
const localeLabel = document.createElement('label');
localeLabel.htmlFor = 'dev-locale-select';
localeLabel.textContent = 'Language';
const localeSelect = document.createElement('select');
localeSelect.id = 'dev-locale-select';
const themeGroup = document.createElement('div');
themeGroup.className = 'control-group';
const themeLabel = document.createElement('label');
themeLabel.htmlFor = 'dev-theme-switcher';
themeLabel.textContent = 'Theme';
const themeSwitcherContainer = document.createElement('div');
themeSwitcherContainer.id = 'dev-theme-switcher-container';
const STORAGE_KEY_LOCALE = 'unraid:test:locale';
const availableLocales = new Set(localeOptions.map((option) => option.value));
const readPersistedLocale = () => {
try {
return window.localStorage?.getItem(STORAGE_KEY_LOCALE) ?? undefined;
} catch {
return undefined;
}
};
const resolveInitialLocale = () => {
const candidates = [
typeof window.LOCALE === 'string' ? window.LOCALE : undefined,
readPersistedLocale(),
'en_US',
];
for (const candidate of candidates) {
if (candidate && availableLocales.has(candidate)) {
return candidate;
}
}
return 'en_US';
};
const initialLocale = resolveInitialLocale();
window.LOCALE = initialLocale;
let currentLocale = initialLocale;
localeOptions.forEach((option) => {
const optionElement = document.createElement('option');
optionElement.value = option.value;
optionElement.textContent = option.label;
if (option.value === currentLocale) {
optionElement.selected = true;
}
localeSelect.appendChild(optionElement);
});
const createThemeSwitcher = () => {
const themeSwitcherElement = document.createElement('unraid-dev-theme-switcher');
themeSwitcherContainer.appendChild(themeSwitcherElement);
};
localeSelect.addEventListener('change', (event) => {
const nextLocale = event.target.value;
if (nextLocale === currentLocale) {
return;
}
try {
window.localStorage?.setItem(STORAGE_KEY_LOCALE, nextLocale);
} catch {
// ignore
}
window.LOCALE = nextLocale;
currentLocale = nextLocale;
window.location.reload();
});
const localeInfo = document.createElement('div');
localeInfo.className = 'info';
localeInfo.textContent = 'Reloads page to apply locale.';
const themeInfo = document.createElement('div');
themeInfo.className = 'info';
themeInfo.textContent = 'Updates theme instantly via Vue component.';
localeGroup.appendChild(localeLabel);
localeGroup.appendChild(localeSelect);
localeGroup.appendChild(localeInfo);
themeGroup.appendChild(themeLabel);
themeGroup.appendChild(themeSwitcherContainer);
themeGroup.appendChild(themeInfo);
container.appendChild(title);
container.appendChild(localeGroup);
const divider = document.createElement('div');
divider.className = 'divider';
container.appendChild(divider);
container.appendChild(themeGroup);
const attach = () => {
if (!document.head.contains(style)) {
document.head.appendChild(style);
}
if (!document.body.contains(container)) {
document.body.appendChild(container);
}
};
const initializeTheme = () => {
createThemeSwitcher();
};
if (document.readyState === 'loading') {
document.addEventListener(
'DOMContentLoaded',
() => {
attach();
initializeTheme();
},
{ once: true }
);
} else {
attach();
initializeTheme();
}
})();

View File

@@ -1,264 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Unraid Component Test Pages</title>
<script src="https://code.jquery.com/jquery-3.7.1.min.js"></script>
<style>
body {
font-family: system-ui, -apple-system, sans-serif;
margin: 0;
padding: 20px;
background: #f3f4f6;
}
.container {
max-width: 1200px;
margin: 0 auto;
}
.header {
background: linear-gradient(135deg, #1f2937 0%, #374151 100%);
color: white;
padding: 30px;
border-radius: 8px;
margin-bottom: 30px;
box-shadow: 0 4px 6px rgba(0,0,0,0.1);
}
.header h1 {
margin: 0 0 10px 0;
font-size: 32px;
}
.header p {
margin: 0;
opacity: 0.9;
}
.category-section {
margin-bottom: 30px;
}
.category-header {
background: #1f2937;
color: white;
padding: 12px 20px;
border-radius: 6px 6px 0 0;
font-weight: 600;
font-size: 16px;
display: flex;
align-items: center;
gap: 10px;
}
.page-list {
background: white;
border-radius: 0 0 6px 6px;
box-shadow: 0 1px 3px rgba(0,0,0,0.1);
}
.page-item {
padding: 15px 20px;
border-bottom: 1px solid #e5e7eb;
display: flex;
justify-content: space-between;
align-items: center;
transition: background 0.2s;
}
.page-item:hover {
background: #f9fafb;
}
.page-item:last-child {
border-bottom: none;
}
.page-item h3 {
margin: 0;
font-size: 18px;
color: #1f2937;
}
.page-item p {
margin: 5px 0 0 0;
color: #6b7280;
font-size: 14px;
}
.page-item .badge {
display: inline-block;
padding: 2px 8px;
background: #dbeafe;
color: #1e40af;
border-radius: 4px;
font-size: 11px;
font-weight: 600;
margin-left: 10px;
}
.page-item .badge.new {
background: #d1fae5;
color: #065f46;
}
.page-item a {
padding: 8px 16px;
background: #3b82f6;
color: white;
text-decoration: none;
border-radius: 6px;
font-size: 14px;
font-weight: 500;
transition: background 0.2s;
}
.page-item a:hover {
background: #2563eb;
}
.info-box {
background: #fef3c7;
border: 1px solid #f59e0b;
border-radius: 6px;
padding: 15px 20px;
margin-bottom: 30px;
color: #92400e;
}
.info-box strong {
color: #78350f;
}
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>🧪 Unraid Component Test Environment</h1>
<p>HTML-based test pages that mimic Unraid OS integration with jQuery and component mounting</p>
</div>
<div class="info-box">
<strong> Testing Mode:</strong> These pages replicate how Unraid OS mounts Vue components into existing HTML/PHP pages using jQuery for interaction. Each page demonstrates real-world integration patterns.
</div>
<!-- Core Test Pages -->
<div class="category-section">
<div class="category-header">
🎯 Core Test Pages
</div>
<div class="page-list">
<div class="page-item">
<div>
<h3>All Components <span class="badge new">COMPREHENSIVE</span></h3>
<p>Complete component library showcase with all available components</p>
</div>
<a href="/test-pages/all-components.html">Open →</a>
</div>
<div class="page-item">
<div>
<h3>Dashboard</h3>
<p>Main dashboard with header components, system status, and user profile</p>
</div>
<a href="/test-pages/dashboard.html">Open →</a>
</div>
<div class="page-item">
<div>
<h3>Settings</h3>
<p>System settings with theme switcher, Connect configuration, and API keys</p>
</div>
<a href="/test-pages/settings.html">Open →</a>
</div>
</div>
</div>
<!-- Feature-Specific Pages -->
<div class="category-section">
<div class="category-header">
⚡ Feature-Specific Pages
</div>
<div class="page-list">
<div class="page-item">
<div>
<h3>Authentication Flow <span class="badge new">NEW</span></h3>
<p>Complete auth workflow with login, SSO, user profile, and registration</p>
</div>
<a href="/test-pages/authentication.html">Open →</a>
</div>
<div class="page-item">
<div>
<h3>OS Management <span class="badge new">NEW</span></h3>
<p>System updates, downgrades, and version management with progress simulation</p>
</div>
<a href="/test-pages/os-management.html">Open →</a>
</div>
<div class="page-item">
<div>
<h3>Update Modal Testing <span class="badge new">NEW</span></h3>
<p>Test various update scenarios including expired licenses, renewals, and auth requirements</p>
</div>
<a href="/test-pages/update-modal.html">Open →</a>
</div>
<div class="page-item">
<div>
<h3>Component Mounting Test</h3>
<p>Test single and multiple component mounting with shared Pinia store and dynamic creation</p>
</div>
<a href="/test-pages/component-mounting.html">Open →</a>
</div>
</div>
</div>
<!-- Quick Links -->
<div class="category-section">
<div class="category-header">
🔗 Quick Component Tests
</div>
<div class="page-list">
<div class="page-item">
<div>
<h3>Theme Testing</h3>
<p>Light/dark mode switching and CSS variable management</p>
</div>
<a href="/test-pages/all-components.html#theme-switcher">Open →</a>
</div>
<div class="page-item">
<div>
<h3>Modal System</h3>
<p>Global modal management and event-driven popups</p>
</div>
<a href="/test-pages/all-components.html#modals">Open →</a>
</div>
<div class="page-item">
<div>
<h3>API & Logs</h3>
<p>API key management, log viewer, and debug tools</p>
</div>
<a href="/test-pages/all-components.html#api-developer">Open →</a>
</div>
</div>
</div>
<!-- Testing Info -->
<div style="background: white; padding: 20px; border-radius: 8px; margin-top: 30px; box-shadow: 0 1px 3px rgba(0,0,0,0.1);">
<h3 style="margin-top: 0;">🛠️ Testing Guidelines</h3>
<ul style="color: #6b7280; line-height: 1.8;">
<li>Each page loads components using the same mechanism as Unraid OS (manifest-based loading)</li>
<li>jQuery is available for simulating PHP/backend interactions</li>
<li>Components communicate via DOM attributes and custom events</li>
<li>Hot module replacement (HMR) is enabled in dev mode for instant updates</li>
<li>Use browser DevTools console to monitor component events and interactions</li>
</ul>
</div>
</div>
<!-- Load the manifest and inject resources (mimics PHP extractor) -->
<script src="/test-pages/language-switcher.js"></script>
<script src="/test-pages/load-manifest.js"></script>
<script>
$(document).ready(function() {
// Add some interactivity to the index page
$('.page-item').on('mouseenter', function() {
$(this).find('a').css('transform', 'translateX(2px)');
}).on('mouseleave', function() {
$(this).find('a').css('transform', 'translateX(0)');
});
// Set up smooth transitions
$('a').css('transition', 'all 0.2s ease');
});
</script>
</body>
</html>

View File

@@ -1,355 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>OS Management - Unraid Component Test</title>
<script src="https://code.jquery.com/jquery-3.7.1.min.js"></script>
<style>
body {
font-family: system-ui, -apple-system, sans-serif;
margin: 0;
padding: 0;
background: #f3f4f6;
}
.header {
background: #1f2937;
color: white;
padding: 20px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
.header h1 {
margin: 0;
display: flex;
align-items: center;
gap: 15px;
}
.container {
max-width: 1200px;
margin: 0 auto;
padding: 20px;
}
.info-bar {
background: white;
padding: 15px 20px;
border-radius: 8px;
margin-bottom: 20px;
display: flex;
justify-content: space-between;
align-items: center;
box-shadow: 0 1px 3px rgba(0,0,0,0.1);
}
.version-info {
display: flex;
gap: 30px;
}
.version-item {
display: flex;
flex-direction: column;
}
.version-label {
font-size: 12px;
color: #6b7280;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.version-value {
font-size: 20px;
font-weight: 600;
color: #1f2937;
margin-top: 4px;
}
.main-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 20px;
}
.card {
background: white;
border-radius: 8px;
padding: 25px;
box-shadow: 0 1px 3px rgba(0,0,0,0.1);
}
.card h2 {
margin: 0 0 20px 0;
color: #1f2937;
display: flex;
align-items: center;
gap: 10px;
}
.card-description {
color: #6b7280;
margin-bottom: 20px;
font-size: 14px;
}
.status-indicator {
display: inline-flex;
align-items: center;
gap: 8px;
padding: 6px 12px;
border-radius: 20px;
font-size: 14px;
font-weight: 500;
}
.status-indicator.up-to-date {
background: #d1fae5;
color: #065f46;
}
.status-indicator.update-available {
background: #fed7aa;
color: #92400e;
}
.status-indicator.checking {
background: #dbeafe;
color: #1e40af;
}
@media (max-width: 768px) {
.main-grid {
grid-template-columns: 1fr;
}
}
</style>
</head>
<body>
<!-- Header with OS Version -->
<div class="header">
<div class="container">
<h1>
💿 OS Management
<unraid-header-os-version></unraid-header-os-version>
</h1>
</div>
</div>
<div class="container">
<!-- System Information Bar -->
<div class="info-bar">
<div class="version-info">
<div class="version-item">
<span class="version-label">Current Version</span>
<span class="version-value" id="current-version">6.12.4</span>
</div>
<div class="version-item">
<span class="version-label">Latest Available</span>
<span class="version-value" id="latest-version">6.12.5</span>
</div>
<div class="version-item">
<span class="version-label">Update Channel</span>
<span class="version-value" id="update-channel">Stable</span>
</div>
</div>
<div class="status-indicator update-available" id="update-status">
<span></span> Update Available
</div>
</div>
<!-- Main Grid -->
<div class="main-grid">
<!-- Update OS Card -->
<div class="card">
<h2>⬆️ System Updates</h2>
<p class="card-description">Check for and install Unraid OS updates</p>
<unraid-update-os></unraid-update-os>
</div>
<!-- Downgrade OS Card -->
<div class="card">
<h2>⬇️ System Downgrade</h2>
<p class="card-description">Rollback to a previous Unraid OS version</p>
<unraid-downgrade-os></unraid-downgrade-os>
</div>
</div>
<!-- Test Controls -->
<div class="card" style="margin-top: 20px; background: #1f2937;">
<h2 style="color: white;">🧪 Test Scenarios</h2>
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 10px;">
<button class="test-btn" id="check-updates">Check for Updates</button>
<button class="test-btn" id="simulate-update">Simulate Update Available</button>
<button class="test-btn" id="simulate-current">Simulate Up-to-date</button>
<button class="test-btn" id="change-channel">Switch Channel (Beta)</button>
<button class="test-btn" id="simulate-download">Simulate Download Progress</button>
<button class="test-btn" id="simulate-install">Simulate Installation</button>
</div>
<div style="margin-top: 20px;">
<div style="background: black; color: #10b981; padding: 15px; border-radius: 6px; font-family: monospace; font-size: 12px; min-height: 120px; max-height: 200px; overflow-y: auto;" id="console">
> OS Management Test Console
> Ready for testing...
</div>
</div>
</div>
</div>
<style>
.test-btn {
padding: 10px 16px;
background: #3b82f6;
color: white;
border: none;
border-radius: 6px;
cursor: pointer;
font-size: 14px;
font-weight: 500;
transition: background 0.2s;
}
.test-btn:hover {
background: #2563eb;
}
.progress-bar {
width: 100%;
height: 30px;
background: #e5e7eb;
border-radius: 6px;
overflow: hidden;
margin-top: 15px;
}
.progress-fill {
height: 100%;
background: linear-gradient(90deg, #3b82f6, #2563eb);
transition: width 0.3s ease;
display: flex;
align-items: center;
justify-content: center;
color: white;
font-size: 14px;
font-weight: 500;
}
</style>
<!-- Global Modals -->
<unraid-modals></unraid-modals>
<!-- Load manifest -->
<script src="/test-pages/language-switcher.js"></script>
<script src="/test-pages/load-manifest.js"></script>
<!-- Test Logic -->
<script>
$(document).ready(function() {
const $console = $('#console');
let currentVersion = '6.12.4';
let latestVersion = '6.12.5';
let updateChannel = 'stable';
function log(message, type = 'info') {
const timestamp = new Date().toLocaleTimeString();
const prefix = type === 'error' ? '❌' : type === 'success' ? '✅' : '>';
$console.append('\n' + prefix + ' [' + timestamp + '] ' + message);
$console.scrollTop($console[0].scrollHeight);
}
// Check for updates
$('#check-updates').on('click', function() {
log('Checking for updates...');
$('#update-status').removeClass('up-to-date update-available').addClass('checking');
$('#update-status').html('<span>⟳</span> Checking...');
setTimeout(function() {
if (currentVersion !== latestVersion) {
$('#update-status').removeClass('checking up-to-date').addClass('update-available');
$('#update-status').html('<span>●</span> Update Available');
log('Update available: ' + latestVersion, 'success');
} else {
$('#update-status').removeClass('checking update-available').addClass('up-to-date');
$('#update-status').html('<span>✓</span> Up to Date');
log('System is up to date', 'success');
}
}, 2000);
});
// Simulate update available
$('#simulate-update').on('click', function() {
latestVersion = '6.12.6';
$('#latest-version').text(latestVersion);
$('#update-status').removeClass('up-to-date checking').addClass('update-available');
$('#update-status').html('<span>●</span> Update Available');
log('Simulated update available: ' + latestVersion);
// Trigger component update
$('unraid-update-os').attr('latest-version', latestVersion);
});
// Simulate up-to-date
$('#simulate-current').on('click', function() {
currentVersion = latestVersion = '6.12.5';
$('#current-version').text(currentVersion);
$('#latest-version').text(latestVersion);
$('#update-status').removeClass('update-available checking').addClass('up-to-date');
$('#update-status').html('<span>✓</span> Up to Date');
log('System is now up to date');
});
// Change channel
$('#change-channel').on('click', function() {
updateChannel = updateChannel === 'stable' ? 'beta' : 'stable';
$('#update-channel').text(updateChannel.charAt(0).toUpperCase() + updateChannel.slice(1));
log('Switched to ' + updateChannel + ' channel');
if (updateChannel === 'beta') {
latestVersion = '6.13.0-beta1';
$('#latest-version').text(latestVersion);
log('Beta version available: ' + latestVersion);
} else {
latestVersion = '6.12.5';
$('#latest-version').text(latestVersion);
}
});
// Simulate download progress
$('#simulate-download').on('click', function() {
log('Starting download simulation...');
const $card = $(this).closest('.card');
if (!$card.find('.progress-bar').length) {
$card.append('<div class="progress-bar"><div class="progress-fill" style="width: 0%">0%</div></div>');
}
let progress = 0;
const interval = setInterval(function() {
progress += 10;
$('.progress-fill').css('width', progress + '%').text(progress + '%');
log('Download progress: ' + progress + '%');
if (progress >= 100) {
clearInterval(interval);
log('Download complete!', 'success');
setTimeout(function() {
$('.progress-bar').fadeOut();
}, 2000);
}
}, 500);
});
// Simulate installation
$('#simulate-install').on('click', function() {
log('Preparing installation...');
log('Creating backup...');
setTimeout(function() {
log('Backup complete', 'success');
log('Installing update...');
setTimeout(function() {
log('Installation complete!', 'success');
log('System will restart in 30 seconds...');
currentVersion = latestVersion;
$('#current-version').text(currentVersion);
}, 3000);
}, 2000);
});
// Listen for component events
$(document).on('unraid:update-started', function(e, data) {
log('Update started: ' + data.version);
});
$(document).on('unraid:update-complete', function(e, data) {
log('Update complete: ' + data.version, 'success');
});
log('OS Management test page initialized');
});
</script>
</body>
</html>

View File

@@ -1,226 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Settings - Unraid Component Test</title>
<script src="https://code.jquery.com/jquery-3.7.1.min.js"></script>
<style>
body {
font-family: system-ui, -apple-system, sans-serif;
margin: 0;
padding: 20px;
background: #f3f4f6;
}
.header {
background: white;
padding: 16px 20px;
border-bottom: 1px solid #e5e7eb;
}
.header h1 {
margin: 0;
color: #1f2937;
}
.tabs {
background: white;
border-bottom: 1px solid #e5e7eb;
padding: 0 20px;
display: flex;
gap: 20px;
}
.tab {
padding: 12px 0;
border-bottom: 2px solid transparent;
cursor: pointer;
color: #6b7280;
}
.tab.active {
border-bottom-color: #3b82f6;
color: #3b82f6;
}
.main-content {
padding: 20px;
max-width: 800px;
margin: 0 auto;
}
.settings-section {
background: white;
border-radius: 8px;
padding: 20px;
margin-bottom: 20px;
box-shadow: 0 1px 3px rgba(0,0,0,0.1);
}
.settings-section h2 {
margin-top: 0;
padding-bottom: 10px;
border-bottom: 1px solid #e5e7eb;
color: #1f2937;
}
.setting-item {
padding: 15px 0;
border-bottom: 1px solid #f3f4f6;
}
.setting-item:last-child {
border-bottom: none;
}
.setting-label {
font-weight: 500;
color: #374151;
margin-bottom: 5px;
}
.setting-description {
font-size: 14px;
color: #6b7280;
margin-bottom: 10px;
}
</style>
</head>
<body>
<!-- Settings Container -->
<div class="settings-container" style="max-width: 1200px; margin: 0 auto;">
<!-- Tabs -->
<div class="tabs">
<div class="tab active" data-tab="general">
General
</div>
<div class="tab" data-tab="connect">
Connect
</div>
<div class="tab" data-tab="registration">
Registration
</div>
<div class="tab" data-tab="api">
API Keys
</div>
</div>
<!-- Main Content -->
<div class="main-content">
<!-- General Settings -->
<div class="tab-content" data-tab-content="general" style="display: block;">
<div class="settings-section">
<h2>Appearance</h2>
<div class="setting-item">
<div class="setting-label">Theme</div>
<div class="setting-description">Choose between light and dark theme</div>
<unraid-theme-switcher current="white"></unraid-theme-switcher>
</div>
</div>
<div class="settings-section">
<h2>System Information</h2>
<div class="setting-item">
<div class="setting-label">OS Version</div>
<unraid-header-os-version></unraid-header-os-version>
</div>
</div>
</div>
<!-- Connect Settings -->
<div class="tab-content" data-tab-content="connect" style="display: none;">
<div class="settings-section">
<h2>Unraid Connect Settings</h2>
<unraid-connect-settings></unraid-connect-settings>
</div>
</div>
<!-- Registration -->
<div class="tab-content" data-tab-content="registration" style="display: none;">
<div class="settings-section">
<h2>System Registration</h2>
<unraid-registration></unraid-registration>
</div>
</div>
<!-- API Keys -->
<div class="tab-content" data-tab-content="api" style="display: none;">
<div class="settings-section">
<h2>API Key Management</h2>
<unraid-api-key-manager></unraid-api-key-manager>
</div>
<div class="settings-section">
<h2>API Key Authorization</h2>
<unraid-api-key-authorize></unraid-api-key-authorize>
</div>
</div>
</div>
</div> <!-- End settings-container -->
<!-- Global Modals -->
<unraid-modals></unraid-modals>
<!-- Load the manifest and inject resources (mimics PHP extractor) -->
<script src="/test-pages/language-switcher.js"></script>
<script src="/test-pages/load-manifest.js"></script>
<script src="/test-pages/test-server-state.js"></script>
<script src="/test-pages/shared-header.js"></script>
<!-- jQuery tab functionality and component interaction -->
<script>
$(document).ready(function() {
// Tab switching functionality
$('.tab').on('click', function() {
var tabName = $(this).data('tab');
// Update active tab styling
$('.tab').removeClass('active');
$(this).addClass('active');
// Show/hide content
$('.tab-content').hide();
$('.tab-content[data-tab-content="' + tabName + '"]').fadeIn();
});
// Example: Programmatically update settings from jQuery
// This mimics how Unraid might update settings after saving
window.updateConnectSettings = function(settings) {
// You could pass this to the Vue component
console.log('Updating connect settings:', settings);
// The component would listen for attribute changes
$('unraid-connect-settings').attr('settings', JSON.stringify(settings));
};
// Example: Listen for events from Vue components
$(document).on('unraid:settings-saved', function(e, data) {
console.log('Settings saved:', data);
// Show a jQuery notification or update the UI
showSuccessMessage('Settings saved successfully!');
});
// Simple success message function (like Unraid's)
function showSuccessMessage(message) {
var $msg = $('<div class="success-message" style="position: fixed; top: 20px; right: 20px; background: #10b981; color: white; padding: 12px 20px; border-radius: 4px; z-index: 9999;">' + message + '</div>');
$('body').append($msg);
setTimeout(function() {
$msg.fadeOut(function() {
$(this).remove();
});
}, 3000);
}
// Example: Load settings via AJAX and update components
function loadSettings() {
// Simulate AJAX call
setTimeout(function() {
var settings = {
connect: { enabled: true, url: 'https://connect.unraid.net' },
theme: 'dark',
registration: { key: 'XXXX-XXXX-XXXX' }
};
// Update components with loaded data
$('unraid-connect-settings').attr('initial-settings', JSON.stringify(settings.connect));
$('unraid-registration').attr('registration-data', JSON.stringify(settings.registration));
console.log('Settings loaded via jQuery');
}, 1000);
}
// Load settings on page load
loadSettings();
});
</script>
</body>
</html>

View File

@@ -1,63 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Update Modal Test - Unraid Component Test</title>
<script src="https://code.jquery.com/jquery-3.7.1.min.js"></script>
<style>
html {
font-size: 10px;
}
body {
font-family: system-ui, -apple-system, sans-serif;
margin: 0;
padding: 20px;
background: #f3f4f6;
}
.header {
background: #1f2937;
color: white;
padding: 20px;
margin-bottom: 20px;
border-radius: 8px;
}
.header h1 {
margin: 0;
}
.back-link {
color: white;
text-decoration: none;
margin-bottom: 10px;
display: inline-block;
opacity: 0.8;
}
.back-link:hover {
opacity: 1;
}
</style>
</head>
<body>
<div class="header">
<div style="display: flex; justify-content: space-between; align-items: center;">
<div>
<a href="/test-pages/" class="back-link">← Back to Test Pages</a>
<h1>🧪 Update Modal Test Scenarios</h1>
</div>
<div>
<unraid-test-theme-switcher></unraid-test-theme-switcher>
</div>
</div>
</div>
<!-- Mount the test component -->
<unraid-test-update-modal></unraid-test-update-modal>
<!-- Mount the modals component which includes the changelog modal -->
<unraid-modals></unraid-modals>
<!-- Load the manifest and inject resources -->
<script src="/test-pages/language-switcher.js"></script>
<script src="/test-pages/load-manifest.js"></script>
</body>
</html>

View File

@@ -0,0 +1,57 @@
#!/usr/bin/env node
import { mkdir, writeFile } from 'node:fs/promises';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import { glob } from 'glob';
import nunjucks from 'nunjucks';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const rootDir = path.join(__dirname, '..');
const templatesDir = path.join(rootDir, 'test-pages');
const pagesDir = path.join(templatesDir, 'pages');
const outputDir = path.join(rootDir, 'public', 'test-pages');
const env = nunjucks.configure(templatesDir, {
autoescape: false,
noCache: true,
throwOnUndefined: false,
});
async function ensureDir(dirPath) {
await mkdir(dirPath, { recursive: true });
}
async function renderTemplates() {
const templateFiles = await glob('**/*.njk', { cwd: pagesDir, nodir: true });
if (templateFiles.length === 0) {
console.log('No test page templates found.');
return;
}
await ensureDir(outputDir);
const mode = process.env.NODE_ENV ?? 'development';
let renderedCount = 0;
for (const relativePath of templateFiles) {
const templateName = `pages/${relativePath}`.replace(/\\/g, '/');
const htmlOutput = env.render(templateName, { mode });
const targetPath = path.join(outputDir, relativePath).replace(/\.njk$/, '.html');
await ensureDir(path.dirname(targetPath));
await writeFile(targetPath, htmlOutput, 'utf-8');
renderedCount += 1;
}
console.log(
`Rendered ${renderedCount} test page template${renderedCount === 1 ? '' : 's'} to ${outputDir}`
);
}
renderTemplates().catch((error) => {
console.error('Failed to render test page templates:', error);
process.exit(1);
});

View File

@@ -38,13 +38,21 @@ function expandJsonFormsKey(key) {
return expanded;
}
// Keep any explicitly referenced error keys as-is
if (key.includes('.error.')) {
expanded.add(key);
return expanded;
}
// Don't add .label to keys that already have specific suffixes
if (key.endsWith('.title') || key.endsWith('.description')) {
expanded.add(key);
return expanded;
}
expanded.add(key.endsWith('.label') ? key : `${key}.label`);
const baseKey = key.endsWith('.label') ? key.slice(0, -'.label'.length) : key;
expanded.add(`${baseKey}.label`);
expanded.add(`${baseKey}.error.custom`);
return expanded;
}

View File

@@ -0,0 +1,150 @@
<script lang="ts" setup>
import { onMounted, ref, watch } from 'vue';
import { storeToRefs } from 'pinia';
import { useThemeStore } from '~/store/theme';
const themeStore = useThemeStore();
const themeOptions = [
{ value: 'white', label: 'White' },
{ value: 'black', label: 'Black' },
{ value: 'gray', label: 'Gray' },
{ value: 'azure', label: 'Azure' },
] as const;
const STORAGE_KEY_THEME = 'unraid:test:theme';
const { theme } = storeToRefs(themeStore);
const currentTheme = ref<string>(theme.value.name);
const getCurrentTheme = (): string => {
const urlParams = new URLSearchParams(window.location.search);
const urlTheme = urlParams.get('theme');
if (urlTheme && themeOptions.some((t) => t.value === urlTheme)) {
return urlTheme;
}
if (theme.value?.name) {
return theme.value.name;
}
try {
return window.localStorage?.getItem(STORAGE_KEY_THEME) || 'white';
} catch {
return 'white';
}
};
const updateTheme = (themeName: string, skipUrlUpdate = false) => {
if (!skipUrlUpdate) {
const url = new URL(window.location.href);
url.searchParams.set('theme', themeName);
window.history.replaceState({}, '', url);
}
try {
window.localStorage?.setItem(STORAGE_KEY_THEME, themeName);
} catch {
// ignore
}
themeStore.setTheme({ name: themeName }, true);
themeStore.setCssVars();
const linkId = 'dev-theme-css-link';
let themeLink = document.getElementById(linkId) as HTMLLinkElement | null;
const themeCssMap: Record<string, string> = {
azure: '/test-pages/unraid-assets/themes/azure.css',
black: '/test-pages/unraid-assets/themes/black.css',
gray: '/test-pages/unraid-assets/themes/gray.css',
white: '/test-pages/unraid-assets/themes/white.css',
};
const cssUrl = themeCssMap[themeName];
if (cssUrl) {
if (!themeLink) {
themeLink = document.createElement('link');
themeLink.id = linkId;
themeLink.rel = 'stylesheet';
document.head.appendChild(themeLink);
}
themeLink.href = cssUrl;
} else {
if (themeLink) {
themeLink.remove();
}
}
};
const handleThemeChange = (event: Event) => {
const newTheme = (event.target as HTMLSelectElement).value;
if (newTheme === currentTheme.value) {
return;
}
currentTheme.value = newTheme;
updateTheme(newTheme);
};
onMounted(() => {
themeStore.setDevOverride(true);
const initialTheme = getCurrentTheme();
currentTheme.value = initialTheme;
const existingLink = document.getElementById('dev-theme-css-link') as HTMLLinkElement | null;
if (!existingLink || !existingLink.href) {
updateTheme(initialTheme, true);
} else {
themeStore.setTheme({ name: initialTheme }, true);
themeStore.setCssVars();
}
});
watch(
() => theme.value.name,
(newName) => {
if (newName && newName !== currentTheme.value) {
currentTheme.value = newName;
const url = new URL(window.location.href);
url.searchParams.set('theme', newName);
window.history.replaceState({}, '', url);
}
}
);
</script>
<template>
<select :value="currentTheme" class="dev-theme-select" @change="handleThemeChange">
<option v-for="option in themeOptions" :key="option.value" :value="option.value">
{{ option.label }}
</option>
</select>
</template>
<style scoped>
.dev-theme-select {
padding: 8px 10px;
border-radius: 6px;
border: 1px solid rgba(148, 163, 184, 0.4);
background: rgba(15, 23, 42, 0.9);
color: #f9fafb;
font-size: 13px;
outline: none;
cursor: pointer;
transition: all 0.2s;
}
.dev-theme-select:hover {
border-color: rgba(148, 163, 184, 0.6);
}
.dev-theme-select:focus {
border-color: #3b82f6;
box-shadow: 0 0 0 2px rgba(59, 130, 246, 0.3);
}
</style>

View File

@@ -146,6 +146,11 @@ export const componentMappings: ComponentMapping[] = [
selector: 'unraid-test-theme-switcher',
appId: 'test-theme-switcher',
},
{
component: defineAsyncComponent(() => import('../DevThemeSwitcher.standalone.vue')),
selector: 'unraid-dev-theme-switcher',
appId: 'dev-theme-switcher',
},
{
component: defineAsyncComponent(() => import('../ApiStatus/ApiStatus.standalone.vue')),
selector: 'unraid-api-status-manager',

View File

@@ -75,125 +75,167 @@
"headerOsVersion.viewOsReleaseNotes": "View OS Release Notes",
"headerOsVersion.visitPartnerWebsite": "Visit Partner website",
"headerOsVersion.visitUnraidWebsite": "Visit Unraid website",
"jsonforms.apiKey.customPermissions.actions.error.custom": "Select at least one action for every custom permission entry.",
"jsonforms.apiKey.customPermissions.actions.label": "Actions",
"jsonforms.apiKey.customPermissions.actions.title": "Actions",
"jsonforms.apiKey.customPermissions.description": "Configure specific permissions",
"jsonforms.apiKey.customPermissions.error.custom": "Resolve the errors in Custom Permissions before continuing.",
"jsonforms.apiKey.customPermissions.label": "Permissions",
"jsonforms.apiKey.customPermissions.resources.error.custom": "Select at least one resource for every custom permission entry.",
"jsonforms.apiKey.customPermissions.resources.label": "Resources",
"jsonforms.apiKey.customPermissions.resources.title": "Resources",
"jsonforms.apiKey.customPermissions.title": "Custom Permissions",
"jsonforms.apiKey.description": "API Key Description",
"jsonforms.apiKey.description.title": "Description",
"jsonforms.apiKey.name.description": "A descriptive name for this API key",
"jsonforms.apiKey.name.error.custom": "Enter an API key name between 1 and 100 characters.",
"jsonforms.apiKey.name.label": "API Key Name",
"jsonforms.apiKey.name.title": "API Key Name",
"jsonforms.apiKey.permissionPresets.description": "Quick add common permission sets",
"jsonforms.apiKey.permissionPresets.error.custom": "Choose a valid preset or keep \"None\" selected.",
"jsonforms.apiKey.permissionPresets.label": "Add Permission Preset",
"jsonforms.apiKey.permissionPresets.title": "Permission Presets",
"jsonforms.apiKey.permissions.description": "Configure API key permissions",
"jsonforms.apiKey.permissions.description.label": "Select any combination of roles, permission groups, and custom permissions to define what this API key can access.",
"jsonforms.apiKey.permissions.header.error.custom": "Resolve the errors in the Permissions section.",
"jsonforms.apiKey.permissions.header.label": "Permissions Configuration",
"jsonforms.apiKey.permissions.help.error.custom": "Follow the permissions guidance and complete every required field.",
"jsonforms.apiKey.permissions.help.label": "Use the preset dropdown for common permission sets, or manually add custom permissions. You can select multiple resources that share the same actions.",
"jsonforms.apiKey.permissions.subheader.error.custom": "Fix the invalid fields within the Permissions section.",
"jsonforms.apiKey.permissions.subheader.label": "Permissions",
"jsonforms.apiKey.roles.description": "Select one or more roles to grant pre-defined permission sets",
"jsonforms.apiKey.roles.error.custom": "Select at least one valid role or clear the field.",
"jsonforms.apiKey.roles.label": "Roles",
"jsonforms.apiKey.roles.title": "Roles",
"jsonforms.apiSettings.sandbox.error.custom": "Choose whether the developer sandbox should be enabled.",
"jsonforms.apiSettings.sandbox.label": "Enable Developer Sandbox",
"jsonforms.apiSettings.sandbox.title": "Enable Developer Sandbox",
"jsonforms.oidc.accordion.advancedEndpoints.description": "Override auto-discovery settings (optional)",
"jsonforms.oidc.accordion.advancedEndpoints.error.custom": "Review the Advanced Endpoints section and correct the invalid fields.",
"jsonforms.oidc.accordion.advancedEndpoints.label": "Advanced Endpoints",
"jsonforms.oidc.accordion.advancedEndpoints.title": "Advanced Endpoints",
"jsonforms.oidc.accordion.authorizationRules.description": "Configure who can access your server",
"jsonforms.oidc.accordion.authorizationRules.error.custom": "Review the Authorization Rules section and correct the invalid fields.",
"jsonforms.oidc.accordion.authorizationRules.label": "Authorization Rules",
"jsonforms.oidc.accordion.authorizationRules.title": "Authorization Rules",
"jsonforms.oidc.accordion.basicConfiguration.description": "Essential provider settings",
"jsonforms.oidc.accordion.basicConfiguration.error.custom": "Review the Basic Configuration section and correct the invalid fields.",
"jsonforms.oidc.accordion.basicConfiguration.label": "Basic Configuration",
"jsonforms.oidc.accordion.basicConfiguration.title": "Basic Configuration",
"jsonforms.oidc.accordion.buttonCustomization.description": "Customize the appearance of the login button",
"jsonforms.oidc.accordion.buttonCustomization.error.custom": "Review the Button Customization section and correct the invalid fields.",
"jsonforms.oidc.accordion.buttonCustomization.label": "Button Customization",
"jsonforms.oidc.accordion.buttonCustomization.title": "Button Customization",
"jsonforms.oidc.buttons.description": "Customize the appearance of the login button",
"jsonforms.oidc.buttons.icon.description": "URL or base64 encoded icon for the login button",
"jsonforms.oidc.buttons.icon.error.custom": "Provide a valid icon URL or data URI.",
"jsonforms.oidc.buttons.icon.label": "Button Icon URL",
"jsonforms.oidc.buttons.icon.title": "Button Icon",
"jsonforms.oidc.buttons.style.description": "Custom inline CSS styles for the button (e.g., \"background: linear-gradient(to right, #4f46e5, #7c3aed); border-radius: 9999px;\")",
"jsonforms.oidc.buttons.style.error.custom": "Enter valid CSS for the button style or leave it blank.",
"jsonforms.oidc.buttons.style.label": "Custom CSS Styles",
"jsonforms.oidc.buttons.style.title": "Button Style",
"jsonforms.oidc.buttons.text.description": "Custom text for the login button",
"jsonforms.oidc.buttons.text.error.custom": "Enter the button text you want to display.",
"jsonforms.oidc.buttons.text.label": "Button Text",
"jsonforms.oidc.buttons.text.title": "Button Text",
"jsonforms.oidc.buttons.title": "Button Customization",
"jsonforms.oidc.buttons.variant.description": "Visual style of the login button",
"jsonforms.oidc.buttons.variant.error.custom": "Select one of the supported button styles.",
"jsonforms.oidc.buttons.variant.label": "Button Style",
"jsonforms.oidc.buttons.variant.title": "Button Style",
"jsonforms.oidc.provider.authorizationEndpoint.description": "Optional - will be auto-discovered if not provided",
"jsonforms.oidc.provider.authorizationEndpoint.error.custom": "Enter a valid authorization endpoint URL.",
"jsonforms.oidc.provider.authorizationEndpoint.label": "Authorization Endpoint",
"jsonforms.oidc.provider.authorizationEndpoint.title": "Authorization Endpoint",
"jsonforms.oidc.provider.clientId.description": "OAuth2 client ID registered with the provider",
"jsonforms.oidc.provider.clientId.error.custom": "Enter the OAuth client ID issued by your provider.",
"jsonforms.oidc.provider.clientId.label": "OAuth Client ID",
"jsonforms.oidc.provider.clientId.title": "OAuth Client ID",
"jsonforms.oidc.provider.clientSecret.description": "OAuth2 client secret (if required)",
"jsonforms.oidc.provider.clientSecret.error.custom": "Provide the OAuth client secret issued by your provider.",
"jsonforms.oidc.provider.clientSecret.label": "OAuth Client Secret",
"jsonforms.oidc.provider.clientSecret.title": "OAuth Client Secret",
"jsonforms.oidc.provider.discoveryToggle.error.custom": "Choose whether to use automatic discovery.",
"jsonforms.oidc.provider.discoveryToggle.label": "Use Automatic Discovery",
"jsonforms.oidc.provider.id.description": "Unique identifier for the provider",
"jsonforms.oidc.provider.id.error.custom": "Enter a unique provider ID (for example, \"google\").",
"jsonforms.oidc.provider.id.label": "Provider ID",
"jsonforms.oidc.provider.id.title": "Provider ID",
"jsonforms.oidc.provider.issuer.description": "OIDC issuer URL (e.g., https://accounts.google.com). Cannot contain /.well-known/ paths - use the base issuer URL instead of the full discovery endpoint. Must not end with a trailing slash.",
"jsonforms.oidc.provider.issuer.error.custom": "Enter the issuer URL (for example, https://accounts.example.com).",
"jsonforms.oidc.provider.issuer.label": "Issuer URL",
"jsonforms.oidc.provider.issuer.title": "Issuer URL",
"jsonforms.oidc.provider.jwksUri.description": "Optional - will be auto-discovered if not provided",
"jsonforms.oidc.provider.jwksUri.error.custom": "Provide a valid JWKS URI or rely on discovery.",
"jsonforms.oidc.provider.jwksUri.label": "JWKS URI",
"jsonforms.oidc.provider.jwksUri.title": "JWKS URI",
"jsonforms.oidc.provider.name.description": "Display name for the provider",
"jsonforms.oidc.provider.name.error.custom": "Enter the provider name shown to users.",
"jsonforms.oidc.provider.name.label": "Provider Name",
"jsonforms.oidc.provider.name.title": "Provider Name",
"jsonforms.oidc.provider.scopes.description": "OAuth2 scopes to request",
"jsonforms.oidc.provider.scopes.error.custom": "Specify at least one scope requested from the provider.",
"jsonforms.oidc.provider.scopes.label": "OAuth Scopes",
"jsonforms.oidc.provider.scopes.title": "OAuth Scopes",
"jsonforms.oidc.provider.tokenEndpoint.description": "Optional - will be auto-discovered if not provided",
"jsonforms.oidc.provider.tokenEndpoint.error.custom": "Enter a valid token endpoint URL.",
"jsonforms.oidc.provider.tokenEndpoint.label": "Token Endpoint",
"jsonforms.oidc.provider.tokenEndpoint.title": "Token Endpoint",
"jsonforms.oidc.provider.unraidNet.description": "This is the built-in Unraid.net provider. Only authorization rules can be modified.",
"jsonforms.oidc.provider.unraidNet.error.custom": "The Unraid.net provider has managed fields; only supported settings may be edited.",
"jsonforms.oidc.provider.unraidNet.label": "Unraid.net Provider",
"jsonforms.oidc.provider.unraidNet.title": "Unraid.net Provider",
"jsonforms.oidc.provider.userInfoEndpoint.error.custom": "Enter a valid UserInfo endpoint URL.",
"jsonforms.oidc.provider.userInfoEndpoint.label": "User Info Endpoint",
"jsonforms.oidc.restrictions.allowedDomains.description": "Email domains that are allowed to login (e.g., company.com)",
"jsonforms.oidc.restrictions.allowedDomains.error.custom": "List fully qualified domains (one per line) for allowed users.",
"jsonforms.oidc.restrictions.allowedDomains.label": "Allowed Email Domains",
"jsonforms.oidc.restrictions.allowedDomains.title": "Allowed Email Domains",
"jsonforms.oidc.restrictions.allowedEmails.description": "Specific email addresses that are allowed to login",
"jsonforms.oidc.restrictions.allowedEmails.error.custom": "List the specific email addresses that should be allowed.",
"jsonforms.oidc.restrictions.allowedEmails.label": "Specific Email Addresses",
"jsonforms.oidc.restrictions.allowedEmails.title": "Allowed Emails",
"jsonforms.oidc.restrictions.allowedUserIds.description": "Specific user IDs (sub claim) that are allowed to login",
"jsonforms.oidc.restrictions.allowedUserIds.error.custom": "List the user IDs (sub claims) that should be allowed.",
"jsonforms.oidc.restrictions.allowedUserIds.label": "Allowed User IDs",
"jsonforms.oidc.restrictions.allowedUserIds.title": "Allowed User IDs",
"jsonforms.oidc.restrictions.help.error.custom": "Review the Simple Authorization lists; each entry must be valid.",
"jsonforms.oidc.restrictions.help.label": "Configure simple allow lists for who can sign in.",
"jsonforms.oidc.restrictions.title": "Simple Authorization",
"jsonforms.oidc.restrictions.title.label": "Simple Authorization",
"jsonforms.oidc.restrictions.workspaceDomain.description": "Restrict to users from a specific Google Workspace domain",
"jsonforms.oidc.restrictions.workspaceDomain.error.custom": "Enter a valid Google Workspace domain such as example.com.",
"jsonforms.oidc.restrictions.workspaceDomain.label": "Google Workspace Domain",
"jsonforms.oidc.restrictions.workspaceDomain.title": "Google Workspace Domain",
"jsonforms.oidc.rules.claim.description": "JWT claim to check",
"jsonforms.oidc.rules.claim.error.custom": "Select the JWT claim to evaluate.",
"jsonforms.oidc.rules.claim.label": "JWT Claim",
"jsonforms.oidc.rules.claim.title": "JWT Claim",
"jsonforms.oidc.rules.collection.description": "Define authorization rules based on claims in the ID token. Rule mode can be configured: OR logic (any rule matches) or AND logic (all rules must match).",
"jsonforms.oidc.rules.collection.error.custom": "Ensure every authorization rule entry is complete.",
"jsonforms.oidc.rules.collection.label": "Claim Rules",
"jsonforms.oidc.rules.collection.title": "Claim Rules",
"jsonforms.oidc.rules.description": "Configure advanced authorization rules for fine-grained access control",
"jsonforms.oidc.rules.mode.description": "How to evaluate multiple rules: OR (any rule passes) or AND (all rules must pass)",
"jsonforms.oidc.rules.mode.error.custom": "Choose how multiple rules should be evaluated (AND or OR).",
"jsonforms.oidc.rules.mode.label": "Rule Mode",
"jsonforms.oidc.rules.mode.title": "Rule Mode",
"jsonforms.oidc.rules.operator.error.custom": "Select a comparison operator.",
"jsonforms.oidc.rules.operator.label": "Operator",
"jsonforms.oidc.rules.operator.title": "Operator",
"jsonforms.oidc.rules.title": "Advanced Authorization Rules",
"jsonforms.oidc.rules.title.label": "Advanced Authorization Rules",
"jsonforms.oidc.rules.value.description": "Values to match against",
"jsonforms.oidc.rules.value.error.custom": "Provide at least one value for this rule.",
"jsonforms.oidc.rules.value.label": "Values",
"jsonforms.oidc.rules.value.title": "Values",
"jsonforms.sso.defaultAllowedOrigins.description": "Additional trusted redirect origins to allow redirects from custom ports, reverse proxies, Tailscale, etc.",
"jsonforms.sso.defaultAllowedOrigins.error.custom": "Enter valid origins (protocol + host) separated by commas or new lines.",
"jsonforms.sso.defaultAllowedOrigins.label": "Default Allowed Redirect Origins",
"jsonforms.sso.defaultAllowedOrigins.title": "Default Allowed Redirect Origins",
"jsonforms.sso.providers.description": "Configure OpenID Connect providers for SSO authentication",
"jsonforms.sso.providers.error.custom": "Each OIDC provider entry must be valid—resolve the highlighted fields.",
"jsonforms.sso.providers.label": "OIDC Providers",
"jsonforms.sso.providers.title": "OIDC Providers",
"logs.customFilterLabel": "Custom {label}",

View File

@@ -54,6 +54,7 @@ export const useThemeStore = defineStore('theme', () => {
const activeColorVariables = ref<ThemeVariables>(defaultColors.white);
const hasServerTheme = ref(false);
const devOverride = ref(false);
const { result, onResult, onError } = useQuery<GetThemeQuery>(GET_THEME_QUERY, null, {
fetchPolicy: 'cache-and-network',
@@ -107,9 +108,9 @@ export const useThemeStore = defineStore('theme', () => {
});
// Actions
const setTheme = (data?: Partial<Theme>) => {
const setTheme = (data?: Partial<Theme>, force = false) => {
if (data) {
if (hasServerTheme.value) {
if (hasServerTheme.value && !force && !devOverride.value) {
return;
}
@@ -120,6 +121,10 @@ export const useThemeStore = defineStore('theme', () => {
}
};
const setDevOverride = (enabled: boolean) => {
devOverride.value = enabled;
};
const setCssVars = () => {
const selectedTheme = theme.value.name;
@@ -238,5 +243,6 @@ export const useThemeStore = defineStore('theme', () => {
// actions
setTheme,
setCssVars,
setDevOverride,
};
});

View File

@@ -0,0 +1,18 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{% block title %}Unraid Component Test{% endblock %}</title>
<script src="https://code.jquery.com/jquery-3.7.1.min.js"></script>
{% include "partials/styles.njk" %}
{% block pageStyles %}{% endblock %}
</head>
<body>
{% block content %}{% endblock %}
{% include "partials/scripts.njk" %}
{% block scripts %}{% endblock %}
</body>
</html>

View File

@@ -0,0 +1,80 @@
{% extends "layouts/base.njk" %}
{% block title %}API & Developer Tools - Unraid Component Test{% endblock %}
{% block pageStyles %}{% endblock %}
{% block content %}
<div class="container padded">
<div class="breadcrumb">
<a href="/test-pages/">Test Pages</a> / API & Developer Tools
</div>
<div class="card">
<h2>API Key Management</h2>
<p class="card-description">Manage API keys for programmatic access to your Unraid server</p>
<div class="component-mount" data-component="unraid-api-key-manager">
<unraid-api-key-manager></unraid-api-key-manager>
</div>
</div>
<div class="card">
<h2>API Key Authorization</h2>
<p class="card-description">Authorize API key requests from external applications</p>
<div class="component-mount" data-component="unraid-api-key-authorize">
<unraid-api-key-authorize></unraid-api-key-authorize>
</div>
</div>
<div class="card">
<h2>API Logs</h2>
<p class="card-description">Download and view API access logs</p>
<div class="component-mount" data-component="unraid-download-api-logs">
<unraid-download-api-logs></unraid-download-api-logs>
</div>
</div>
<div class="card">
<h2>Log Viewer</h2>
<p class="card-description">View and search through system logs</p>
<div class="component-mount" data-component="unraid-log-viewer">
<unraid-log-viewer></unraid-log-viewer>
</div>
</div>
</div>
<unraid-modals></unraid-modals>
{% endblock %}
{% block scripts %}
<script>
$(document).ready(function() {
console.log('API & Developer Tools page loaded');
$(document).on('unraid:api-key-created', function(e, data) {
console.log('API key created:', data);
var event = new CustomEvent('unraid:notification', {
detail: {
title: 'API Key Created',
message: 'A new API key has been generated',
type: 'success'
}
});
document.dispatchEvent(event);
});
$(document).on('unraid:api-key-deleted', function(e, data) {
console.log('API key deleted:', data);
var event = new CustomEvent('unraid:notification', {
detail: {
title: 'API Key Deleted',
message: 'The API key has been removed',
type: 'info'
}
});
document.dispatchEvent(event);
});
});
</script>
{% endblock %}

View File

@@ -0,0 +1,143 @@
{% extends "layouts/base.njk" %}
{% block title %}Authentication Flow - Unraid Component Test{% endblock %}
{% block pageStyles %}
<style>
body {
padding: 0;
}
</style>
{% endblock %}
{% block content %}
<div class="auth-container">
<div class="auth-card">
<h2>🔐 Authentication</h2>
<unraid-auth></unraid-auth>
<div class="user-info">
<strong>Session Status:</strong>
<div id="auth-status">
<span class="status-badge unauthenticated">Not Authenticated</span>
</div>
</div>
</div>
<div class="auth-card">
<h2>🔗 Single Sign-On</h2>
<p style="color: #6b7280; margin-bottom: 20px;">Alternative authentication methods</p>
<unraid-sso-button></unraid-sso-button>
</div>
<div class="auth-card">
<h2>👤 User Profile</h2>
<p style="color: #6b7280; margin-bottom: 20px;">Displays when authenticated</p>
<unraid-user-profile id="user-profile"></unraid-user-profile>
</div>
<div class="auth-card">
<h2>📝 System Registration</h2>
<unraid-registration></unraid-registration>
</div>
<div class="auth-card" style="background: #1f2937; color: white;">
<h2 style="color: white;">🧪 Test Controls</h2>
<div style="display: flex; gap: 10px; flex-wrap: wrap;">
<button id="simulate-login" class="btn">Simulate Login</button>
<button id="simulate-logout" class="btn">Simulate Logout</button>
<button id="update-profile" class="btn">Update Profile</button>
<button id="check-session" class="btn">Check Session</button>
</div>
<div id="console-output" style="margin-top: 20px; padding: 10px; background: black; border-radius: 4px; font-family: monospace; font-size: 12px; min-height: 80px;">
> Ready...
</div>
</div>
</div>
<unraid-modals></unraid-modals>
{% endblock %}
{% block scripts %}
<script>
$(document).ready(function() {
const output = $('#console-output');
let isAuthenticated = false;
function log(message) {
const timestamp = new Date().toLocaleTimeString();
output.append('\n> [' + timestamp + '] ' + message);
output.scrollTop(output[0].scrollHeight);
}
$('#simulate-login').on('click', function() {
log('Simulating login...');
isAuthenticated = true;
const userData = {
username: 'admin',
email: 'admin@unraid.local',
name: 'Administrator',
avatarUrl: '/webGui/images/default-avatar.png',
role: 'admin',
sessionId: 'sess_' + Math.random().toString(36).substr(2, 9)
};
$('#user-profile').attr('server', JSON.stringify(userData));
$('#auth-status').html('<span class="status-badge authenticated">Authenticated as ' + userData.username + '</span>');
$(document).trigger('unraid:auth-login', userData);
log('Login successful: ' + userData.username);
});
$('#simulate-logout').on('click', function() {
log('Simulating logout...');
isAuthenticated = false;
$('#user-profile').attr('server', '{}');
$('#auth-status').html('<span class="status-badge unauthenticated">Not Authenticated</span>');
$(document).trigger('unraid:auth-logout');
log('Logged out successfully');
});
$('#update-profile').on('click', function() {
if (!isAuthenticated) {
log('Error: Not authenticated');
return;
}
log('Updating profile...');
const updatedData = {
username: 'admin',
email: 'newemail@unraid.local',
name: 'Updated Admin',
lastModified: new Date().toISOString()
};
$('#user-profile').attr('server', JSON.stringify(updatedData));
log('Profile updated');
});
$('#check-session').on('click', function() {
log('Checking session status...');
log('Authenticated: ' + (isAuthenticated ? 'Yes' : 'No'));
if (isAuthenticated) {
log('Session valid until: ' + new Date(Date.now() + 3600000).toLocaleTimeString());
}
});
$(document).on('unraid:auth-required', function() {
log('Authentication required by component');
});
$(document).on('unraid:session-expired', function() {
log('Session expired - please login again');
$('#simulate-logout').click();
});
log('Authentication test page ready');
});
</script>
{% endblock %}

View File

@@ -0,0 +1,264 @@
{% extends "layouts/base.njk" %}
{% block title %}Component Mounting Test - Unraid Component Test{% endblock %}
{% block pageStyles %}
<style>
h1 {
color: var(--text-color, #333);
margin-bottom: 10px;
}
h2 {
color: var(--alt-text-color, #666);
margin-top: 0;
margin-bottom: 15px;
font-size: 18px;
}
.status {
padding: 10px;
border-radius: 4px;
margin-bottom: 10px;
font-family: monospace;
font-size: 14px;
}
.status.loading {
background: #fff3cd;
color: #856404;
}
.status.success {
background: #d4edda;
color: #155724;
}
.status.error {
background: #f8d7da;
color: #721c24;
}
.test-button {
background: #007bff;
margin-right: 10px;
}
.test-button:hover {
background: #0056b3;
}
</style>
{% endblock %}
{% block content %}
<div id="teleports"></div>
<unraid-modals></unraid-modals>
<div class="container">
<h1>🧪 Standalone Vue Apps Test Page</h1>
<div id="status" class="status loading">Loading...</div>
<div class="test-section">
<h2>Test 1: Single Component Mount</h2>
<p>Testing single instance of HeaderOsVersion component</p>
<div class="mount-target" data-label="HeaderOsVersion Mount">
<unraid-header-os-version></unraid-header-os-version>
</div>
</div>
<div class="test-section">
<h2>Test 2: Multiple Component Mounts (Shared Pinia Store)</h2>
<p>Testing that multiple instances share the same Pinia store</p>
<div class="multiple-mounts">
<div class="mount-target" data-label="Instance 1">
<unraid-header-os-version></unraid-header-os-version>
</div>
<div class="mount-target" data-label="Instance 2">
<unraid-header-os-version></unraid-header-os-version>
</div>
<div class="mount-target" data-label="Instance 3">
<unraid-header-os-version></unraid-header-os-version>
</div>
</div>
</div>
<div class="test-section">
<h2>Test 3: Dynamic Component Creation</h2>
<p>Test dynamically adding components after page load</p>
<button class="test-button" id="addComponent">Add New Component</button>
<button class="test-button" id="removeComponent">Remove Last Component</button>
<button class="test-button" id="remountAll">Remount All</button>
<div id="dynamicContainer" style="margin-top: 20px;"></div>
</div>
<div class="test-section">
<h2>Test 4: Modal Components</h2>
<p>Test modal functionality</p>
<button class="test-button" onclick="testTrialModal()">Open Trial Modal</button>
<button class="test-button" onclick="testUpdateModal()">Open Update Modal</button>
<button class="test-button" onclick="testApiKeyModal()">Open API Key Modal</button>
<div style="margin-top: 10px;">
<small>Note: Modals require proper store state to display</small>
</div>
</div>
<div class="test-section">
<h2>Debug Information</h2>
<div class="debug-info" id="debugInfo">
Waiting for initialization...
</div>
</div>
</div>
{% endblock %}
{% block scripts %}
<script>
window.GRAPHQL_ENDPOINT = window.location.port === '3000' ? '/graphql' : 'http://localhost:3001/graphql';
window.__WEBGUI_PATH__ = '';
window.addEventListener('DOMContentLoaded', () => {
const status = document.getElementById('status');
const debugInfo = document.getElementById('debugInfo');
const observer = new MutationObserver((mutations) => {
mutations.forEach((mutation) => {
if (mutation.type === 'childList') {
mutation.addedNodes.forEach((node) => {
if (node.nodeName === 'SCRIPT') {
console.log('Script loaded:', node.src || 'inline');
}
});
}
});
});
observer.observe(document.head, { childList: true });
observer.observe(document.body, { childList: true });
let checkInterval = setInterval(() => {
const mountedElements = document.querySelectorAll('[data-vue-mounted="true"]');
let totalComponents = document.querySelectorAll('unraid-header-os-version, unraid-modals').length;
let mountedCount = mountedElements.length;
if (mountedCount > 0) {
status.className = 'status success';
status.textContent = `✅ Successfully mounted ${mountedCount} component(s)`;
debugInfo.textContent = `
Components Found: ${totalComponents}
Components Mounted: ${mountedCount}
Unified Vue App: ${window.__unifiedApp ? 'Initialized' : 'Not found'}
Mounted Components: ${window.__mountedComponents ? window.__mountedComponents.length : 0}
Pinia Store: ${window.globalPinia ? 'Initialized' : 'Not found'}
GraphQL Endpoint: ${window.GRAPHQL_ENDPOINT || 'Not configured'}
`.trim();
clearInterval(checkInterval);
if (window.testLog) {
window.testLog(`Mounted ${mountedCount} components successfully`, 'success');
}
}
}, 500);
setTimeout(() => {
if (checkInterval) {
clearInterval(checkInterval);
if (status.className === 'status loading') {
status.className = 'status error';
status.textContent = '❌ Failed to mount components (timeout)';
}
}
}, 10000);
});
document.addEventListener('DOMContentLoaded', () => {
let dynamicCount = 0;
const dynamicContainer = document.getElementById('dynamicContainer');
document.getElementById('addComponent').addEventListener('click', () => {
dynamicCount++;
const wrapper = document.createElement('div');
wrapper.className = 'mount-target';
wrapper.setAttribute('data-label', `Dynamic Instance ${dynamicCount}`);
wrapper.style.marginBottom = '10px';
const element = document.createElement('unraid-header-os-version');
wrapper.appendChild(element);
dynamicContainer.appendChild(wrapper);
console.log('Note: Dynamic components require page reload to mount with the unified app system');
if (!wrapper.querySelector('.reload-note')) {
const note = document.createElement('div');
note.className = 'reload-note';
note.style.cssText = 'color: #666; font-size: 12px; margin-top: 10px;';
note.textContent = 'Reload page to mount this component';
wrapper.appendChild(note);
}
});
document.getElementById('removeComponent').addEventListener('click', () => {
const lastChild = dynamicContainer.lastElementChild;
if (lastChild) {
const mountedElement = lastChild.querySelector('[data-vue-mounted="true"]');
if (mountedElement && window.__mountedComponents) {
const componentIndex = window.__mountedComponents.findIndex(c => c.element === mountedElement);
if (componentIndex !== -1) {
window.__mountedComponents[componentIndex].unmount();
window.__mountedComponents.splice(componentIndex, 1);
}
}
dynamicContainer.removeChild(lastChild);
dynamicCount = Math.max(0, dynamicCount - 1);
}
});
document.getElementById('remountAll').addEventListener('click', () => {
console.log('Remounting all components...');
location.reload();
});
});
window.testTrialModal = function() {
console.log('Testing trial modal...');
if (window.globalPinia) {
const trialStore = window.globalPinia._s.get('trial');
if (trialStore) {
trialStore.trialModalVisible = true;
console.log('Trial modal triggered');
} else {
console.error('Trial store not found');
}
}
};
window.testUpdateModal = function() {
console.log('Testing update modal...');
if (window.globalPinia) {
const updateStore = window.globalPinia._s.get('updateOs');
if (updateStore) {
updateStore.updateOsModalVisible = true;
console.log('Update modal triggered');
} else {
console.error('Update store not found');
}
}
};
window.testApiKeyModal = function() {
console.log('Testing API key modal...');
if (window.globalPinia) {
const apiKeyStore = window.globalPinia._s.get('apiKey');
if (apiKeyStore) {
apiKeyStore.showCreateModal = true;
console.log('API key modal triggered');
} else {
console.error('API key store not found');
}
}
};
</script>
{% endblock %}

View File

@@ -0,0 +1,81 @@
{% extends "layouts/base.njk" %}
{% block title %}Connect Settings - Unraid Component Test{% endblock %}
{% block pageStyles %}{% endblock %}
{% block content %}
<div class="container padded">
<div class="breadcrumb">
<a href="/test-pages/">Test Pages</a> / Connect Settings
</div>
<div class="card">
<h2>Unraid API Settings</h2>
<p class="card-description">This page mirrors the Connect.page structure from Unraid OS, demonstrating the Connect Settings component in its native context.</p>
</div>
<div class="card">
<h2>Connect Settings</h2>
<div class="component-mount" data-component="unraid-connect-settings">
<unraid-connect-settings></unraid-connect-settings>
</div>
</div>
<div class="card">
<h2>Remote Access Configuration</h2>
<div class="setting-item">
<div class="setting-label">WAN IP Check</div>
<div class="setting-description">Verify your server's external IP address and port forwarding configuration</div>
<div class="component-mount" data-component="unraid-wan-ip-check" style="margin-top: 10px;">
<unraid-wan-ip-check php-wan-ip="203.0.113.42"></unraid-wan-ip-check>
</div>
</div>
</div>
<div class="card">
<h2>Registration</h2>
<div class="component-mount" data-component="unraid-registration">
<unraid-registration></unraid-registration>
</div>
</div>
</div>
<unraid-modals></unraid-modals>
{% endblock %}
{% block scripts %}
<script>
$(document).ready(function() {
setTimeout(function() {
var connectSettings = {
enabled: true,
url: 'https://connect.unraid.net',
registered: true,
lastSync: new Date().toISOString()
};
$('unraid-connect-settings').attr('initial-settings', JSON.stringify(connectSettings));
console.log('Loaded Connect settings via jQuery');
}, 1000);
$(document).on('unraid:settings-saved', function(e, data) {
console.log('Connect settings saved:', data);
var event = new CustomEvent('unraid:notification', {
detail: {
title: 'Settings Saved',
message: 'Connect settings have been updated successfully',
type: 'success'
}
});
document.dispatchEvent(event);
});
setTimeout(function() {
$('unraid-wan-ip-check').attr('php-wan-ip', '198.51.100.42');
console.log('Updated WAN IP via jQuery');
}, 2000);
});
</script>
{% endblock %}

View File

@@ -0,0 +1,77 @@
{% extends "layouts/base.njk" %}
{% block title %}Header - Unraid Component Test{% endblock %}
{% block pageStyles %}{% endblock %}
{% block content %}
<div class="container padded">
<div class="breadcrumb">
<a href="/test-pages/">Test Pages</a> / Header
</div>
<div class="card">
<h2>Unraid Header</h2>
<p class="card-description">This page demonstrates the header components as they appear in Unraid OS pages.</p>
<div class="header" style="margin: 20px 0; padding: 16px 20px; display: flex; justify-content: space-between; align-items: center;">
<div class="header-left" style="display: flex; align-items: center; gap: 20px;">
<unraid-header-os-version></unraid-header-os-version>
</div>
<div class="header-right">
<unraid-user-profile id="header-user-profile"></unraid-user-profile>
</div>
</div>
</div>
<div class="card">
<h2>Component Details</h2>
<div class="setting-item">
<div class="setting-label">OS Version Component</div>
<div class="setting-description">Displays the current Unraid OS version and update status</div>
<div class="component-mount" data-component="unraid-header-os-version" style="margin-top: 10px;">
<unraid-header-os-version></unraid-header-os-version>
</div>
</div>
<div class="setting-item">
<div class="setting-label">User Profile Component</div>
<div class="setting-description">Shows user information, authentication status, and SSO options</div>
<div class="component-mount" data-component="unraid-user-profile" style="margin-top: 10px;">
<unraid-user-profile id="standalone-user-profile"></unraid-user-profile>
</div>
</div>
</div>
</div>
<unraid-modals></unraid-modals>
{% endblock %}
{% block scripts %}
<script>
$(document).ready(function() {
var serverData = {
name: 'TestServer',
version: '6.12.4',
username: 'admin',
email: 'admin@unraid.net',
avatarUrl: '/webGui/images/default-avatar.png'
};
$('#header-user-profile, #standalone-user-profile').attr('server', JSON.stringify(serverData));
setTimeout(function() {
var updatedData = {
name: 'UpdatedServer',
version: '6.12.5',
username: 'admin',
email: 'admin@unraid.net',
avatarUrl: '/webGui/images/default-avatar.png'
};
$('#header-user-profile').attr('server', JSON.stringify(updatedData));
console.log('Updated header user profile via jQuery');
}, 3000);
});
</script>
{% endblock %}

View File

@@ -0,0 +1,114 @@
{% extends "layouts/base.njk" %}
{% block title %}Unraid Component Test Pages{% endblock %}
{% block pageStyles %}{% endblock %}
{% block content %}
<div class="container">
<div class="header">
<h1>🧪 Unraid Component Test Environment</h1>
<p>HTML-based test pages that mimic Unraid OS integration with jQuery and component mounting</p>
</div>
<div class="info-box">
<strong> Testing Mode:</strong> These pages replicate how Unraid OS mounts Vue components into existing HTML/PHP pages using jQuery for interaction. Each page demonstrates real-world integration patterns.
</div>
<div class="category-section">
<div class="category-header rounded-top">
📄 Page-Specific Test Pages
</div>
<div class="page-list">
<div class="page-item">
<div>
<h3>Header <span class="badge new">NEW</span></h3>
<p>Header components as they appear in Unraid OS: OS version and user profile</p>
</div>
<a href="/test-pages/header.html">Open →</a>
</div>
<div class="page-item">
<div>
<h3>Connect Settings <span class="badge new">NEW</span></h3>
<p>Unraid API Settings page mirroring Connect.page structure with Connect Settings component</p>
</div>
<a href="/test-pages/connect-settings.html">Open →</a>
</div>
<div class="page-item">
<div>
<h3>API & Developer Tools <span class="badge new">NEW</span></h3>
<p>API key management, authorization, logs, and developer tools</p>
</div>
<a href="/test-pages/api-developer.html">Open →</a>
</div>
<div class="page-item">
<div>
<h3>Authentication Flow</h3>
<p>Complete auth workflow with login, SSO, user profile, and registration</p>
</div>
<a href="/test-pages/authentication.html">Open →</a>
</div>
<div class="page-item">
<div>
<h3>OS Management</h3>
<p>System updates, downgrades, and version management with progress simulation</p>
</div>
<a href="/test-pages/os-management.html">Open →</a>
</div>
</div>
</div>
<div class="category-section">
<div class="category-header rounded-top">
🔧 Technical Test Pages
</div>
<div class="page-list">
<div class="page-item">
<div>
<h3>Update Modal Testing</h3>
<p>Test various update scenarios including expired licenses, renewals, and auth requirements</p>
</div>
<a href="/test-pages/update-modal.html">Open →</a>
</div>
<div class="page-item">
<div>
<h3>Component Mounting Test</h3>
<p>Test single and multiple component mounting with shared Pinia store and dynamic creation</p>
</div>
<a href="/test-pages/component-mounting.html">Open →</a>
</div>
</div>
</div>
<div class="card" style="margin-top: 30px;">
<h3 style="margin-top: 0;">🛠️ Testing Guidelines</h3>
<ul style="color: var(--muted-foreground, var(--alt-text-color)); line-height: 1.8;">
<li>Each page loads components using the same mechanism as Unraid OS (manifest-based loading)</li>
<li>jQuery is available for simulating PHP/backend interactions</li>
<li>Components communicate via DOM attributes and custom events</li>
<li>Hot module replacement (HMR) is enabled in dev mode for instant updates</li>
<li>Use browser DevTools console to monitor component events and interactions</li>
</ul>
</div>
</div>
{% endblock %}
{% block scripts %}
<script>
$(document).ready(function() {
$('.page-item').on('mouseenter', function() {
$(this).find('a').css('transform', 'translateX(2px)');
}).on('mouseleave', function() {
$(this).find('a').css('transform', 'translateX(0)');
});
$('a').css('transition', 'all 0.2s ease');
});
</script>
{% endblock %}

View File

@@ -0,0 +1,221 @@
{% extends "layouts/base.njk" %}
{% block title %}OS Management - Unraid Component Test{% endblock %}
{% block pageStyles %}
<style>
body {
padding: 0;
}
.header {
background: var(--header-background-color, #1f2937);
color: var(--header-text-primary, white);
padding: 20px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
.header h1 {
margin: 0;
display: flex;
align-items: center;
gap: 15px;
}
.test-btn.large {
padding: 10px 16px;
border-radius: 6px;
font-weight: 500;
}
</style>
{% endblock %}
{% block content %}
<div class="header">
<div class="container">
<h1>
💿 OS Management
<unraid-header-os-version></unraid-header-os-version>
</h1>
</div>
</div>
<div class="container">
<div class="info-bar">
<div class="version-info">
<div class="version-item">
<span class="version-label">Current Version</span>
<span class="version-value" id="current-version">6.12.4</span>
</div>
<div class="version-item">
<span class="version-label">Latest Available</span>
<span class="version-value" id="latest-version">6.12.5</span>
</div>
<div class="version-item">
<span class="version-label">Update Channel</span>
<span class="version-value" id="update-channel">Stable</span>
</div>
</div>
<div class="status-indicator update-available" id="update-status">
<span>●</span> Update Available
</div>
</div>
<div class="main-grid">
<div class="card">
<h2>⬆️ System Updates</h2>
<p class="card-description">Check for and install Unraid OS updates</p>
<unraid-update-os></unraid-update-os>
</div>
<div class="card">
<h2>⬇️ System Downgrade</h2>
<p class="card-description">Rollback to a previous Unraid OS version</p>
<unraid-downgrade-os></unraid-downgrade-os>
</div>
</div>
<div class="card" style="margin-top: 20px; background: #1f2937;">
<h2 style="color: white;">🧪 Test Scenarios</h2>
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 10px;">
<button class="test-btn" id="check-updates">Check for Updates</button>
<button class="test-btn" id="simulate-update">Simulate Update Available</button>
<button class="test-btn" id="simulate-current">Simulate Up-to-date</button>
<button class="test-btn" id="change-channel">Switch Channel (Beta)</button>
<button class="test-btn" id="simulate-download">Simulate Download Progress</button>
<button class="test-btn" id="simulate-install">Simulate Installation</button>
</div>
<div style="margin-top: 20px;">
<div style="background: black; color: #10b981; padding: 15px; border-radius: 6px; font-family: monospace; font-size: 12px; min-height: 120px; max-height: 200px; overflow-y: auto;" id="console">
> OS Management Test Console
> Ready for testing...
</div>
</div>
</div>
</div>
<unraid-modals></unraid-modals>
{% endblock %}
{% block scripts %}
<script>
$(document).ready(function() {
const $console = $('#console');
let currentVersion = '6.12.4';
let latestVersion = '6.12.5';
let updateChannel = 'stable';
function log(message, type = 'info') {
const timestamp = new Date().toLocaleTimeString();
const prefix = type === 'error' ? '❌' : type === 'success' ? '✅' : '>';
$console.append('\n' + prefix + ' [' + timestamp + '] ' + message);
$console.scrollTop($console[0].scrollHeight);
}
$('#check-updates').on('click', function() {
log('Checking for updates...');
$('#update-status').removeClass('up-to-date update-available').addClass('checking');
$('#update-status').html('<span>⟳</span> Checking...');
setTimeout(function() {
if (currentVersion !== latestVersion) {
$('#update-status').removeClass('checking up-to-date').addClass('update-available');
$('#update-status').html('<span>●</span> Update Available');
log('Update available: ' + latestVersion, 'success');
} else {
$('#update-status').removeClass('checking update-available').addClass('up-to-date');
$('#update-status').html('<span>✓</span> Up to Date');
log('System is up to date', 'success');
}
}, 2000);
});
$('#simulate-update').on('click', function() {
latestVersion = '6.12.6';
$('#latest-version').text(latestVersion);
$('#update-status').removeClass('up-to-date checking').addClass('update-available');
$('#update-status').html('<span>●</span> Update Available');
log('Simulated update available: ' + latestVersion);
$('unraid-update-os').attr('latest-version', latestVersion);
});
$('#simulate-current').on('click', function() {
currentVersion = latestVersion = '6.12.5';
$('#current-version').text(currentVersion);
$('#latest-version').text(latestVersion);
$('#update-status').removeClass('update-available checking').addClass('up-to-date');
$('#update-status').html('<span>✓</span> Up to Date');
log('System is now up to date');
});
$('#change-channel').on('click', function() {
updateChannel = updateChannel === 'stable' ? 'beta' : 'stable';
$('#update-channel').text(updateChannel.charAt(0).toUpperCase() + updateChannel.slice(1));
log('Switched to ' + updateChannel + ' channel');
if (updateChannel === 'beta') {
latestVersion = '6.13.0-beta1';
$('#latest-version').text(latestVersion);
log('Beta version available: ' + latestVersion);
} else {
latestVersion = '6.12.5';
$('#latest-version').text(latestVersion);
}
});
$('#simulate-download').on('click', function() {
log('Starting download simulation...');
const $card = $(this).closest('.card');
if (!$card.find('.progress-bar').length) {
$card.append('<div class="progress-bar"><div class="progress-fill" style="width: 0%">0%</div></div>');
}
let progress = 0;
const interval = setInterval(function() {
progress += 10;
$('.progress-fill').css('width', progress + '%').text(progress + '%');
log('Download progress: ' + progress + '%');
if (progress >= 100) {
clearInterval(interval);
log('Download complete!', 'success');
setTimeout(function() {
$('.progress-bar').fadeOut();
}, 2000);
}
}, 500);
});
$('#simulate-install').on('click', function() {
log('Preparing installation...');
log('Creating backup...');
setTimeout(function() {
log('Backup complete', 'success');
log('Installing update...');
setTimeout(function() {
log('Installation complete!', 'success');
log('System will restart in 30 seconds...');
currentVersion = latestVersion;
$('#current-version').text(currentVersion);
}, 3000);
}, 2000);
});
$(document).on('unraid:update-started', function(e, data) {
log('Update started: ' + data.version);
});
$(document).on('unraid:update-complete', function(e, data) {
log('Update complete: ' + data.version, 'success');
});
log('OS Management test page initialized');
});
</script>
{% endblock %}

View File

@@ -0,0 +1,42 @@
{% extends "layouts/base.njk" %}
{% block title %}Update Modal Test - Unraid Component Test{% endblock %}
{% block pageStyles %}
<style>
body {
padding: 0;
}
.header {
background: var(--header-background-color, #1f2937);
color: var(--header-text-primary, white);
padding: 20px;
margin-bottom: 20px;
border-radius: 8px;
}
.header h1 {
margin: 0;
}
</style>
{% endblock %}
{% block content %}
<div class="header">
<div style="display: flex; justify-content: space-between; align-items: center;">
<div>
<a href="/test-pages/" class="back-link">← Back to Test Pages</a>
<h1>🧪 Update Modal Test Scenarios</h1>
</div>
<div>
<unraid-test-theme-switcher></unraid-test-theme-switcher>
</div>
</div>
</div>
<unraid-test-update-modal></unraid-test-update-modal>
<unraid-modals></unraid-modals>
{% endblock %}

View File

@@ -0,0 +1,5 @@
<script src="/test-pages/dev-tools.js"></script>
<script src="/test-pages/load-manifest.js"></script>
<script src="/test-pages/test-server-state.js"></script>
<script src="/test-pages/shared-header.js"></script>

View File

@@ -0,0 +1,590 @@
{% if mode === 'development' %}
{% set activeTheme = query.theme or 'white' %}
<link rel="stylesheet" href="/test-pages/unraid-assets/default-fonts.css">
<link rel="stylesheet" href="/test-pages/unraid-assets/default-base.css">
<link rel="stylesheet" href="/test-pages/unraid-assets/default-cases.css">
<link rel="stylesheet" href="/test-pages/unraid-assets/default-color-palette.css">
<link rel="stylesheet" href="/test-pages/unraid-assets/default-dynamix.css">
<link rel="stylesheet" href="/test-pages/unraid-assets/font-awesome.css">
<link rel="stylesheet" href="/test-pages/unraid-assets/jquery.filetree.css">
<link rel="stylesheet" href="/test-pages/unraid-assets/jquery.sweetalert.css">
<link rel="stylesheet" href="/test-pages/unraid-assets/jquery.switchbutton.css">
<link rel="stylesheet" href="/test-pages/unraid-assets/jquery.ui.css">
<link rel="stylesheet" href="/test-pages/unraid-assets/context.standalone.css">
{% if activeTheme === 'azure' %}
<link rel="stylesheet" href="/test-pages/unraid-assets/themes/azure.css" id="dev-theme-css-link">
{% elif activeTheme === 'black' %}
<link rel="stylesheet" href="/test-pages/unraid-assets/themes/black.css" id="dev-theme-css-link">
{% elif activeTheme === 'gray' %}
<link rel="stylesheet" href="/test-pages/unraid-assets/themes/gray.css" id="dev-theme-css-link">
{% elif activeTheme === 'white' %}
<link rel="stylesheet" href="/test-pages/unraid-assets/themes/white.css" id="dev-theme-css-link">
{% endif %}
{% else %}
<link rel="stylesheet" href="/test-pages/unraid-assets/default-fonts.css">
<link rel="stylesheet" href="/test-pages/unraid-assets/default-base.css">
<link rel="stylesheet" href="/test-pages/unraid-assets/default-cases.css">
<link rel="stylesheet" href="/test-pages/unraid-assets/default-color-palette.css">
<link rel="stylesheet" href="/test-pages/unraid-assets/default-dynamix.css">
<link rel="stylesheet" href="/test-pages/unraid-assets/font-awesome.css">
<link rel="stylesheet" href="/test-pages/unraid-assets/jquery.filetree.css">
<link rel="stylesheet" href="/test-pages/unraid-assets/jquery.sweetalert.css">
<link rel="stylesheet" href="/test-pages/unraid-assets/jquery.switchbutton.css">
<link rel="stylesheet" href="/test-pages/unraid-assets/jquery.ui.css">
<link rel="stylesheet" href="/test-pages/unraid-assets/context.standalone.css">
{% endif %}
<style>
html {
font-size: 10px;
}
body {
font-family: clear-sans, system-ui, -apple-system, sans-serif;
margin: 0;
padding: 20px;
background-color: var(--background-color);
color: var(--text-color);
}
.container {
max-width: 1200px;
margin: 0 auto;
}
.container.padded {
padding: 20px;
}
.header {
background: linear-gradient(135deg, #1f2937 0%, #374151 100%);
color: white;
padding: 30px;
border-radius: 8px;
margin-bottom: 30px;
box-shadow: 0 4px 6px rgba(0,0,0,0.1);
}
.header h1 {
margin: 0 0 10px 0;
font-size: 32px;
}
.header p {
margin: 0;
opacity: 0.9;
}
.dashboard-header {
background: var(--header-background-color, #1f2937);
color: var(--header-text-primary, white);
padding: 16px 20px;
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
border-radius: 8px;
}
.header-left {
display: flex;
align-items: center;
gap: 20px;
}
.logo {
font-size: 24px;
font-weight: bold;
}
.main-content {
padding: 20px;
}
.main-content.centered {
max-width: 800px;
margin: 0 auto;
}
.breadcrumb {
padding: 10px 20px;
background-color: var(--card, var(--background-color));
border-bottom: 1px solid var(--border);
}
.breadcrumb a {
color: #3b82f6;
text-decoration: none;
}
.category-section {
margin-bottom: 30px;
}
.category-header {
background: var(--header-background-color, #1f2937);
color: var(--header-text-primary, white);
padding: 10px 15px;
border-radius: 4px;
margin: 30px 0 15px 0;
font-weight: 600;
}
.category-header.rounded-top {
padding: 12px 20px;
border-radius: 6px 6px 0 0;
font-size: 16px;
display: flex;
align-items: center;
gap: 10px;
}
.page-list {
background-color: var(--card, var(--background-color));
border-radius: 0 0 6px 6px;
box-shadow: 0 1px 3px rgba(0,0,0,0.1);
}
.page-item {
padding: 15px 20px;
border-bottom: 1px solid var(--border);
display: flex;
justify-content: space-between;
align-items: center;
transition: background 0.2s;
}
.page-item:hover {
background-color: var(--hover-table-row-background-color, var(--muted));
}
.page-item:last-child {
border-bottom: none;
}
.page-item h3 {
margin: 0;
font-size: 18px;
color: var(--card-foreground, var(--text-color));
}
.page-item p {
margin: 5px 0 0 0;
color: var(--muted-foreground, var(--alt-text-color));
font-size: 14px;
}
.page-item .badge {
display: inline-block;
padding: 2px 8px;
background: #dbeafe;
color: #1e40af;
border-radius: 4px;
font-size: 11px;
font-weight: 600;
margin-left: 10px;
}
.page-item .badge.new {
background: #d1fae5;
color: #065f46;
}
.page-item a {
padding: 8px 16px;
background: #3b82f6;
color: white;
text-decoration: none;
border-radius: 6px;
font-size: 14px;
font-weight: 500;
transition: background 0.2s;
}
.page-item a:hover {
background: #2563eb;
}
.info-box {
background: #fef3c7;
border: 1px solid #f59e0b;
border-radius: 6px;
padding: 15px 20px;
margin-bottom: 30px;
color: #92400e;
}
.info-box strong {
color: #78350f;
}
.component-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
gap: 20px;
margin-top: 20px;
}
.component-card {
background-color: var(--card, var(--background-color));
border-radius: 8px;
padding: 20px;
box-shadow: 0 1px 3px rgba(0,0,0,0.1);
}
.component-card h3 {
margin: 0 0 10px 0;
color: var(--card-foreground, var(--text-color));
font-size: 16px;
font-weight: 600;
}
.component-card .selector {
font-family: monospace;
font-size: 12px;
color: var(--muted-foreground, var(--alt-text-color));
margin-bottom: 15px;
background: var(--muted);
padding: 4px 8px;
border-radius: 4px;
display: inline-block;
}
.component-mount {
min-height: 50px;
border: 1px dashed var(--border);
border-radius: 4px;
padding: 10px;
position: relative;
}
.component-mount::before {
content: attr(data-component);
position: absolute;
top: -10px;
left: 10px;
background-color: var(--card, var(--background-color));
padding: 0 5px;
font-size: 12px;
color: var(--muted-foreground, var(--alt-text-color));
}
.status {
position: absolute;
top: 5px;
right: 5px;
width: 8px;
height: 8px;
border-radius: 50%;
background: #10b981;
}
.card {
background-color: var(--card, var(--background-color));
border-radius: 8px;
padding: 20px;
margin-bottom: 20px;
box-shadow: 0 1px 3px rgba(0,0,0,0.1);
}
.card h2 {
margin-top: 0;
color: var(--card-foreground, var(--text-color));
}
.tabs {
background-color: var(--card, var(--background-color));
border-bottom: 1px solid var(--border);
padding: 0 20px;
display: flex;
gap: 20px;
}
.tab {
padding: 12px 0;
border-bottom: 2px solid transparent;
cursor: pointer;
color: var(--muted-foreground, var(--alt-text-color));
}
.tab.active {
border-bottom-color: #3b82f6;
color: #3b82f6;
}
.settings-section {
border-radius: 8px;
padding: 20px;
margin-bottom: 20px;
}
.settings-section h2 {
margin-top: 0;
padding-bottom: 10px;
border-bottom: 1px solid var(--border);
color: var(--card-foreground, var(--text-color));
}
.setting-item {
padding: 15px 0;
border-bottom: 1px solid var(--border);
}
.setting-item:last-child {
border-bottom: none;
}
.setting-label {
font-weight: 500;
color: var(--card-foreground, var(--text-color));
margin-bottom: 5px;
}
.setting-description {
font-size: 14px;
color: var(--muted-foreground, var(--alt-text-color));
margin-bottom: 10px;
}
.auth-container {
max-width: 600px;
margin: 50px auto;
padding: 20px;
}
.auth-card {
border-radius: 8px;
padding: 30px;
margin-bottom: 20px;
}
.auth-card h2 {
margin: 0 0 20px 0;
color: var(--card-foreground, var(--text-color));
}
.auth-card {
background-color: var(--card, var(--background-color));
}
.user-info {
background: var(--muted);
padding: 15px;
border-radius: 6px;
margin-top: 20px;
}
.status-badge {
display: inline-block;
padding: 4px 12px;
border-radius: 20px;
font-size: 12px;
font-weight: 600;
margin-top: 10px;
}
.status-badge.authenticated {
background: #10b981;
color: white;
}
.status-badge.unauthenticated {
background: #ef4444;
color: white;
}
.info-bar {
padding: 15px 20px;
border-radius: 8px;
margin-bottom: 20px;
display: flex;
justify-content: space-between;
align-items: center;
}
.version-info {
display: flex;
gap: 30px;
}
.version-item {
display: flex;
flex-direction: column;
}
.version-label {
font-size: 12px;
color: var(--muted-foreground, var(--alt-text-color));
text-transform: uppercase;
letter-spacing: 0.5px;
}
.version-value {
font-size: 20px;
font-weight: 600;
color: var(--card-foreground, var(--text-color));
margin-top: 4px;
}
.info-bar {
background-color: var(--card, var(--background-color));
}
.main-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 20px;
}
.card-description {
color: var(--muted-foreground, var(--alt-text-color));
margin-bottom: 20px;
font-size: 14px;
}
.status-indicator {
display: inline-flex;
align-items: center;
gap: 8px;
padding: 6px 12px;
border-radius: 20px;
font-size: 14px;
font-weight: 500;
}
.status-indicator.up-to-date {
background: #d1fae5;
color: #065f46;
}
.status-indicator.update-available {
background: #fed7aa;
color: #92400e;
}
.status-indicator.checking {
background: #dbeafe;
color: #1e40af;
}
.test-btn,
.test-button,
.btn {
padding: 8px 16px;
background: #3b82f6;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 14px;
transition: background 0.2s;
}
.test-btn:hover,
.test-button:hover,
.btn:hover {
background: #2563eb;
}
.test-button:disabled {
background: #ccc;
cursor: not-allowed;
}
.test-btn.large {
padding: 10px 16px;
border-radius: 6px;
font-weight: 500;
}
.progress-bar {
width: 100%;
height: 30px;
background: var(--muted);
border-radius: 6px;
overflow: hidden;
margin-top: 15px;
}
.progress-fill {
height: 100%;
background: linear-gradient(90deg, #3b82f6, #2563eb);
transition: width 0.3s ease;
display: flex;
align-items: center;
justify-content: center;
color: white;
font-size: 14px;
font-weight: 500;
}
.test-section {
background-color: var(--card, var(--background-color));
border-radius: 8px;
padding: 20px;
margin-bottom: 20px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
.mount-target {
padding: 20px;
background: var(--muted);
border: 2px dashed var(--border);
border-radius: 4px;
min-height: 100px;
position: relative;
}
.mount-target::before {
content: attr(data-label);
position: absolute;
top: -10px;
left: 10px;
background-color: var(--card, var(--background-color));
padding: 0 5px;
color: var(--muted-foreground, var(--alt-text-color));
font-size: 12px;
}
.debug-info {
margin-top: 20px;
padding: 15px;
background: var(--muted);
border-radius: 4px;
font-family: monospace;
font-size: 12px;
white-space: pre-wrap;
color: var(--muted-foreground, var(--text-color));
}
.multiple-mounts {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
gap: 20px;
margin-top: 20px;
}
.back-link {
color: white;
text-decoration: none;
margin-bottom: 10px;
display: inline-block;
opacity: 0.8;
}
.back-link:hover {
opacity: 1;
}
@media (max-width: 768px) {
.main-grid {
grid-template-columns: 1fr;
}
body {
padding: 10px;
}
}
</style>
{% block pageStyles %}{% endblock %}

View File

@@ -2,39 +2,163 @@ import fs from 'node:fs';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import nunjucks from 'nunjucks';
import type { IncomingMessage, ServerResponse } from 'node:http';
import type { Plugin } from 'vite';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const publicDir = path.join(__dirname, 'public');
const templatesDir = path.join(__dirname, 'test-pages');
const pagesDir = path.join(templatesDir, 'pages');
const env = nunjucks.configure(templatesDir, {
autoescape: false,
noCache: true,
throwOnUndefined: false,
});
const GITHUB_RAW_BASE =
'https://raw.githubusercontent.com/unraid/webgui/189edb1a690cfaef3358db9d6bef281a5e1231bc/emhttp/plugins/dynamix/styles';
export function serveStaticHtml(): Plugin {
return {
name: 'serve-static-html',
configureServer(server) {
// Serve test pages from public/test-pages
server.watcher.add(path.join(templatesDir, '**/*'));
const handleUnraidAsset = async (res: ServerResponse, assetPath: string) => {
if (!assetPath || assetPath === '/') {
res.statusCode = 404;
res.end('Asset path required');
return;
}
try {
const assetUrl = `${GITHUB_RAW_BASE}${assetPath}`;
const response = await fetch(assetUrl);
if (!response.ok) {
res.statusCode = response.status;
res.end(`Failed to fetch asset: ${response.statusText}`);
return;
}
const ext = path.extname(assetPath).toLowerCase();
let contentType = 'text/plain';
if (ext === '.css') {
contentType = 'text/css';
} else if (ext === '.woff') {
contentType = 'font/woff';
} else if (ext === '.woff2') {
contentType = 'font/woff2';
}
let content: string | Buffer;
if (ext === '.woff' || ext === '.woff2') {
const arrayBuffer = await response.arrayBuffer();
content = Buffer.from(arrayBuffer);
} else {
content = await response.text();
}
res.setHeader('Content-Type', contentType);
res.setHeader('Cache-Control', 'public, max-age=3600');
res.end(content);
} catch (error) {
res.statusCode = 500;
res.end(`Error fetching asset: ${error instanceof Error ? error.message : 'Unknown error'}`);
}
};
server.middlewares.use(
'/test-pages/unraid-assets',
async (req: IncomingMessage, res: ServerResponse) => {
const url = new URL(req.url || '/', 'http://localhost');
const assetPath = url.pathname.replace('/test-pages/unraid-assets', '');
await handleUnraidAsset(res, assetPath);
}
);
server.middlewares.use('/webGui/styles', async (req: IncomingMessage, res: ServerResponse) => {
const url = new URL(req.url || '/', 'http://localhost');
const assetPath = url.pathname.replace('/webGui/styles', '');
await handleUnraidAsset(res, assetPath);
});
server.middlewares.use((req, res, next) => {
// Check if request is for test-pages
if (req.url?.startsWith('/test-pages')) {
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const filePath = path.join(__dirname, 'public', req.url);
if (!req.url?.startsWith('/test-pages')) {
next();
return;
}
// If it's a directory, serve index.html
let targetPath = filePath;
if (fs.existsSync(filePath) && fs.statSync(filePath).isDirectory()) {
targetPath = path.join(filePath, 'index.html');
const requestUrl = new URL(req.url, 'http://localhost');
let pathname = requestUrl.pathname;
if (pathname.endsWith('/')) {
pathname = `${pathname}index.html`;
}
const extension = path.extname(pathname);
if (!extension) {
pathname = `${pathname}.html`;
}
const relativePath = pathname.replace(/^\/test-pages\/?/, '');
const templatePath = path.join(pagesDir, relativePath.replace(/\.html$/, '.njk'));
if (extension === '' || extension === '.html') {
if (fs.existsSync(templatePath)) {
try {
const templateName = `pages/${relativePath.replace(/\.html$/, '.njk')}`.replace(
/\\/g,
'/'
);
const html = env.render(templateName, {
url: requestUrl.pathname,
query: Object.fromEntries(requestUrl.searchParams.entries()),
mode: server.config.mode,
});
res.setHeader('Content-Type', 'text/html');
res.end(html);
return;
} catch (error) {
res.statusCode = 500;
res.setHeader('Content-Type', 'text/plain');
res.end(
`Failed to render template: ${relativePath}\n\n${
error instanceof Error ? error.stack : error
}`
);
return;
}
}
}
// If no extension, try adding .html
if (!path.extname(targetPath) && !fs.existsSync(targetPath)) {
targetPath = targetPath + '.html';
}
const filePath = path.join(publicDir, pathname);
// Serve the file if it exists
if (fs.existsSync(targetPath)) {
const content = fs.readFileSync(targetPath, 'utf-8');
if (fs.existsSync(filePath) && fs.statSync(filePath).isDirectory()) {
const indexPath = path.join(filePath, 'index.html');
if (fs.existsSync(indexPath)) {
const content = fs.readFileSync(indexPath, 'utf-8');
res.setHeader('Content-Type', 'text/html');
res.end(content);
return;
}
}
if (fs.existsSync(filePath)) {
const contentType = path.extname(filePath) === '.js' ? 'application/javascript' : 'text/html';
res.setHeader('Content-Type', contentType);
const content = fs.readFileSync(filePath, 'utf-8');
res.end(content);
return;
}
next();
});
},