Files
api/web/test-standalone.html
Eli Bosley 88087d5201 feat: mount vue apps, not web components (#1639)
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **New Features**
* Standalone web bundle with auto-mount utilities and a self-contained
test page.
* New responsive modal components for consistent mobile/desktop dialogs.
  * Header actions to copy OS/API versions.

* **Improvements**
* Refreshed UI styles (muted borders), accessibility and animation
refinements.
  * Theming updates and Tailwind v4–aligned, component-scoped styles.
  * Runtime GraphQL endpoint override and CSRF header support.

* **Bug Fixes**
* Safer network fetching and improved manifest/asset loading with
duplicate protection.

* **Tests/Chores**
* Parallel plugin tests, new extractor test suite, and updated
build/test scripts.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-09-03 15:42:21 -04:00

327 lines
12 KiB
HTML

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Standalone Vue Apps Test Page</title>
<style>
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, sans-serif;
padding: 20px;
background: #f5f5f5;
}
.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>
<!-- Mock configurations for local testing -->
<script>
// Set GraphQL endpoint directly to API server
// Change this to match your API server port
window.GRAPHQL_ENDPOINT = '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('unraid-header-os-version');
let mountedCount = 0;
mountedElements.forEach(el => {
if (el.innerHTML.trim() !== '') {
mountedCount++;
}
});
if (mountedCount > 0) {
status.className = 'status success';
status.textContent = `✅ Successfully mounted ${mountedCount} component(s)`;
// Update debug info
debugInfo.textContent = `
Components Found: ${mountedElements.length}
Components Mounted: ${mountedCount}
Vue Apps: ${window.mountedApps ? Object.keys(window.mountedApps).length : 0}
Pinia Store: ${window.globalPinia ? 'Initialized' : 'Not found'}
GraphQL Endpoint: ${window.GRAPHQL_ENDPOINT || 'Not configured'}
`.trim();
clearInterval(checkInterval);
}
}, 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';
wrapper.innerHTML = '<unraid-header-os-version></unraid-header-os-version>';
dynamicContainer.appendChild(wrapper);
// Trigger mount if app is already loaded
if (window.mountVueApp) {
window.mountVueApp({
component: window.HeaderOsVersion,
selector: 'unraid-header-os-version',
appId: `dynamic-${dynamicCount}`,
});
}
});
document.getElementById('removeComponent').addEventListener('click', () => {
const lastChild = dynamicContainer.lastElementChild;
if (lastChild) {
dynamicContainer.removeChild(lastChild);
dynamicCount = Math.max(0, dynamicCount - 1);
}
});
document.getElementById('remountAll').addEventListener('click', () => {
// This would require the mount function to be exposed globally
console.log('Remounting all components...');
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 the standalone app -->
<script type="module" src=".nuxt/standalone-apps/standalone-apps.js"></script>
</body>
</html>