Sync DNS2 production changes - removed obsolete test suite and refactored structure
21
dashcaddy-api/assets/.htaccess
Normal file
@@ -0,0 +1,21 @@
|
||||
# Font file headers to prevent sanitizer issues
|
||||
<FilesMatch "\.(woff2|woff|ttf|eot)$">
|
||||
Header set Access-Control-Allow-Origin "*"
|
||||
Header set Access-Control-Allow-Methods "GET, POST, OPTIONS"
|
||||
Header set Access-Control-Allow-Headers "Content-Type"
|
||||
Header set Cache-Control "public, max-age=31536000"
|
||||
|
||||
# Proper MIME types
|
||||
<IfModule mod_mime.c>
|
||||
AddType font/woff2 .woff2
|
||||
AddType font/woff .woff
|
||||
AddType font/ttf .ttf
|
||||
AddType application/vnd.ms-fontobject .eot
|
||||
</IfModule>
|
||||
</FilesMatch>
|
||||
|
||||
# Prevent direct access to font conversion scripts
|
||||
<FilesMatch "\.(py|bat)$">
|
||||
Order allow,deny
|
||||
Deny from all
|
||||
</FilesMatch>
|
||||
0
dashcaddy-api/assets/New Text Document.txt
Normal file
BIN
dashcaddy-api/assets/SAMI-CLOUD.png
Normal file
|
After Width: | Height: | Size: 356 KiB |
1
dashcaddy-api/assets/SAMI-CLOUD.svg
Normal file
|
After Width: | Height: | Size: 972 KiB |
BIN
dashcaddy-api/assets/Slink.png
Normal file
|
After Width: | Height: | Size: 122 KiB |
BIN
dashcaddy-api/assets/apple-touch-icon.png
Normal file
|
After Width: | Height: | Size: 12 KiB |
BIN
dashcaddy-api/assets/certificate-icon.png
Normal file
|
After Width: | Height: | Size: 104 KiB |
BIN
dashcaddy-api/assets/chat.png
Normal file
|
After Width: | Height: | Size: 10 KiB |
BIN
dashcaddy-api/assets/cloud-favicon-512.png
Normal file
|
After Width: | Height: | Size: 53 KiB |
BIN
dashcaddy-api/assets/custom-logo.png
Normal file
|
After Width: | Height: | Size: 356 KiB |
1
dashcaddy-api/assets/custom-logo.svg
Normal file
|
After Width: | Height: | Size: 972 KiB |
BIN
dashcaddy-api/assets/dashcaddy-favicon.ico
Normal file
|
After Width: | Height: | Size: 15 KiB |
BIN
dashcaddy-api/assets/dashcaddy-favicon.png
Normal file
|
After Width: | Height: | Size: 1.5 KiB |
BIN
dashcaddy-api/assets/dashcaddy-logo.png
Normal file
|
After Width: | Height: | Size: 104 KiB |
1
dashcaddy-api/assets/dashcaddy-logo.svg
Normal file
|
After Width: | Height: | Size: 972 KiB |
321
dashcaddy-api/assets/dns-template-selector.js
Normal file
@@ -0,0 +1,321 @@
|
||||
/**
|
||||
* DNS Template Selector
|
||||
* Presents DNS server template options when user chooses to set up DNS
|
||||
*/
|
||||
|
||||
(function(window) {
|
||||
'use strict';
|
||||
|
||||
class DnsTemplateSelector {
|
||||
constructor(progressTracker) {
|
||||
this.progressTracker = progressTracker;
|
||||
this.modal = null;
|
||||
this.onTemplateSelected = null;
|
||||
console.log('[DnsTemplateSelector] Module loaded');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get available DNS server templates from app templates
|
||||
* @returns {Array} Array of DNS template objects
|
||||
*/
|
||||
getDnsTemplates() {
|
||||
// In a real implementation, this would fetch from app-templates.js
|
||||
// For now, return hardcoded templates matching what we added
|
||||
return [
|
||||
{
|
||||
id: 'technitium',
|
||||
name: 'Technitium DNS Server',
|
||||
description: 'Modern DNS server with web UI for managing private zones',
|
||||
icon: '🌐',
|
||||
difficulty: 'Easy',
|
||||
features: [
|
||||
'Web-based management interface',
|
||||
'Private zone management for .sami domain',
|
||||
'DHCP server integration',
|
||||
'DNS-over-HTTPS and DNS-over-TLS support'
|
||||
],
|
||||
recommended: true
|
||||
},
|
||||
{
|
||||
id: 'bind9',
|
||||
name: 'BIND9 DNS Server',
|
||||
description: 'Industry-standard DNS server - powerful and flexible',
|
||||
icon: '🔧',
|
||||
difficulty: 'Advanced',
|
||||
features: [
|
||||
'Industry standard DNS server',
|
||||
'Full RFC compliance',
|
||||
'Advanced zone management',
|
||||
'DNSSEC support'
|
||||
],
|
||||
recommended: false
|
||||
},
|
||||
{
|
||||
id: 'pihole',
|
||||
name: 'Pi-hole',
|
||||
description: 'Network-wide ad blocker with DNS capabilities',
|
||||
icon: '🛡️',
|
||||
difficulty: 'Intermediate',
|
||||
features: [
|
||||
'Ad blocking at DNS level',
|
||||
'Web interface for management',
|
||||
'DHCP server included',
|
||||
'Query logging and statistics'
|
||||
],
|
||||
recommended: false
|
||||
},
|
||||
{
|
||||
id: 'powerdns',
|
||||
name: 'PowerDNS',
|
||||
description: 'High-performance DNS server with SQL backend',
|
||||
icon: '⚡',
|
||||
difficulty: 'Intermediate',
|
||||
features: [
|
||||
'SQL database backend',
|
||||
'RESTful API for automation',
|
||||
'Geographic load balancing',
|
||||
'DNSSEC support'
|
||||
],
|
||||
recommended: false
|
||||
},
|
||||
{
|
||||
id: 'coredns',
|
||||
name: 'CoreDNS',
|
||||
description: 'Cloud-native DNS server - lightweight and flexible',
|
||||
icon: '☁️',
|
||||
difficulty: 'Intermediate',
|
||||
features: [
|
||||
'Plugin-based architecture',
|
||||
'Kubernetes-native',
|
||||
'Lightweight and fast',
|
||||
'Prometheus metrics'
|
||||
],
|
||||
recommended: false
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Show DNS template selection modal
|
||||
*/
|
||||
showTemplateSelector() {
|
||||
// Create modal if it doesn't exist
|
||||
if (!this.modal) {
|
||||
this.createModal();
|
||||
}
|
||||
|
||||
// Populate with templates
|
||||
this.populateTemplates();
|
||||
|
||||
// Show modal
|
||||
this.modal.style.display = 'flex';
|
||||
document.body.style.overflow = 'hidden';
|
||||
}
|
||||
|
||||
/**
|
||||
* Create the modal HTML structure
|
||||
* @private
|
||||
*/
|
||||
createModal() {
|
||||
const modal = document.createElement('div');
|
||||
modal.id = 'dns-template-modal';
|
||||
modal.className = 'dns-template-modal';
|
||||
modal.innerHTML = `
|
||||
<div class="dns-template-modal-content">
|
||||
<div class="dns-template-header">
|
||||
<h2>🌐 Choose a DNS Server</h2>
|
||||
<p>Setting up a DNS server is essential for managing your private .sami domain</p>
|
||||
<button class="dns-template-close" aria-label="Close">×</button>
|
||||
</div>
|
||||
<div class="dns-template-grid" id="dns-template-grid">
|
||||
<!-- Templates will be inserted here -->
|
||||
</div>
|
||||
<div class="dns-template-footer">
|
||||
<button class="dns-template-later-btn" id="dns-setup-later">Set up later</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
document.body.appendChild(modal);
|
||||
this.modal = modal;
|
||||
|
||||
// Add event listeners
|
||||
modal.querySelector('.dns-template-close').addEventListener('click', () => this.close());
|
||||
modal.querySelector('#dns-setup-later').addEventListener('click', () => this.handleSetupLater());
|
||||
|
||||
// Close on overlay click
|
||||
modal.addEventListener('click', (e) => {
|
||||
if (e.target === modal) {
|
||||
this.close();
|
||||
}
|
||||
});
|
||||
|
||||
// Close on Escape key
|
||||
document.addEventListener('keydown', (e) => {
|
||||
if (e.key === 'Escape' && modal.style.display === 'flex') {
|
||||
this.close();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Populate modal with DNS templates
|
||||
* @private
|
||||
*/
|
||||
populateTemplates() {
|
||||
const grid = document.getElementById('dns-template-grid');
|
||||
if (!grid) return;
|
||||
|
||||
const templates = this.getDnsTemplates();
|
||||
grid.innerHTML = '';
|
||||
|
||||
templates.forEach(template => {
|
||||
const card = this.createTemplateCard(template);
|
||||
grid.appendChild(card);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a template card element
|
||||
* @private
|
||||
*/
|
||||
createTemplateCard(template) {
|
||||
const card = document.createElement('div');
|
||||
card.className = 'dns-template-card';
|
||||
if (template.recommended) {
|
||||
card.classList.add('recommended');
|
||||
}
|
||||
|
||||
const difficultyClass = template.difficulty.toLowerCase();
|
||||
|
||||
card.innerHTML = `
|
||||
${template.recommended ? '<div class="recommended-badge">Recommended</div>' : ''}
|
||||
<div class="dns-template-icon">${template.icon}</div>
|
||||
<h3>${template.name}</h3>
|
||||
<p class="dns-template-description">${template.description}</p>
|
||||
<div class="dns-template-difficulty difficulty-${difficultyClass}">
|
||||
${template.difficulty}
|
||||
</div>
|
||||
<ul class="dns-template-features">
|
||||
${template.features.slice(0, 3).map(f => `<li>${f}</li>`).join('')}
|
||||
</ul>
|
||||
<button class="dns-template-select-btn" data-template-id="${template.id}">
|
||||
Select ${template.name}
|
||||
</button>
|
||||
`;
|
||||
|
||||
// Add click handler to select button
|
||||
const selectBtn = card.querySelector('.dns-template-select-btn');
|
||||
selectBtn.addEventListener('click', () => this.handleTemplateSelection(template));
|
||||
|
||||
return card;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle template selection
|
||||
* @private
|
||||
*/
|
||||
handleTemplateSelection(template) {
|
||||
console.log(`[DnsTemplateSelector] Template selected: ${template.id}`);
|
||||
|
||||
// Close modal
|
||||
this.close();
|
||||
|
||||
// Trigger callback if set
|
||||
if (this.onTemplateSelected) {
|
||||
this.onTemplateSelected(template);
|
||||
} else {
|
||||
// Default behavior: open app selector with DNS filter
|
||||
this.openAppSelector(template.id);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle "Set up later" button
|
||||
* @private
|
||||
*/
|
||||
handleSetupLater() {
|
||||
console.log('[DnsTemplateSelector] DNS setup deferred');
|
||||
|
||||
// Mark as deferred in progress tracker
|
||||
if (this.progressTracker) {
|
||||
this.progressTracker.markDnsSetupDeferred();
|
||||
}
|
||||
|
||||
// Close modal
|
||||
this.close();
|
||||
|
||||
// Show notification
|
||||
this.showNotification('DNS setup deferred. You can set it up later from the App Selector.');
|
||||
}
|
||||
|
||||
/**
|
||||
* Open app selector with specific template
|
||||
* @private
|
||||
*/
|
||||
openAppSelector(templateId) {
|
||||
// Try to open the app selector modal if it exists
|
||||
const appSelectorBtn = document.querySelector('[onclick*="showAppSelector"]');
|
||||
if (appSelectorBtn) {
|
||||
appSelectorBtn.click();
|
||||
|
||||
// Wait a bit then filter to the selected template
|
||||
setTimeout(() => {
|
||||
const searchInput = document.querySelector('#app-search');
|
||||
if (searchInput) {
|
||||
searchInput.value = templateId;
|
||||
searchInput.dispatchEvent(new Event('input', { bubbles: true }));
|
||||
}
|
||||
}, 300);
|
||||
} else {
|
||||
// Fallback: show instructions
|
||||
this.showNotification(`To deploy ${templateId}, use the App Selector and search for "${templateId}"`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Show notification message
|
||||
* @private
|
||||
*/
|
||||
showNotification(message) {
|
||||
// Simple notification - could be enhanced
|
||||
const notification = document.createElement('div');
|
||||
notification.className = 'dns-template-notification';
|
||||
notification.textContent = message;
|
||||
notification.style.cssText = `
|
||||
position: fixed;
|
||||
top: 20px;
|
||||
right: 20px;
|
||||
background: var(--card-base);
|
||||
color: var(--fg);
|
||||
padding: 15px 20px;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 4px 12px rgba(0,0,0,0.3);
|
||||
z-index: 10001;
|
||||
max-width: 300px;
|
||||
`;
|
||||
|
||||
document.body.appendChild(notification);
|
||||
|
||||
setTimeout(() => {
|
||||
notification.style.opacity = '0';
|
||||
notification.style.transition = 'opacity 0.3s';
|
||||
setTimeout(() => notification.remove(), 300);
|
||||
}, 3000);
|
||||
}
|
||||
|
||||
/**
|
||||
* Close the modal
|
||||
*/
|
||||
close() {
|
||||
if (this.modal) {
|
||||
this.modal.style.display = 'none';
|
||||
document.body.style.overflow = '';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
window.DnsTemplateSelector = DnsTemplateSelector;
|
||||
console.log('[DnsTemplateSelector] Module loaded');
|
||||
|
||||
})(window);
|
||||
1
dashcaddy-api/assets/driver.min.css
vendored
Normal file
@@ -0,0 +1 @@
|
||||
.driver-active .driver-overlay,.driver-active *{pointer-events:none}.driver-active .driver-active-element,.driver-active .driver-active-element *,.driver-popover,.driver-popover *{pointer-events:auto}@keyframes animate-fade-in{0%{opacity:0}to{opacity:1}}.driver-fade .driver-overlay{animation:animate-fade-in .2s ease-in-out}.driver-fade .driver-popover{animation:animate-fade-in .2s}.driver-popover{all:unset;box-sizing:border-box;color:#2d2d2d;margin:0;padding:15px;border-radius:5px;min-width:250px;max-width:300px;box-shadow:0 1px 10px #0006;z-index:1000000000;position:fixed;top:0;right:0;background-color:#fff}.driver-popover *{font-family:Helvetica Neue,Inter,ui-sans-serif,"Apple Color Emoji",Helvetica,Arial,sans-serif}.driver-popover-title{font:19px/normal sans-serif;font-weight:700;display:block;position:relative;line-height:1.5;zoom:1;margin:0}.driver-popover-close-btn{all:unset;position:absolute;top:0;right:0;width:32px;height:28px;cursor:pointer;font-size:18px;font-weight:500;color:#d2d2d2;z-index:1;text-align:center;transition:color;transition-duration:.2s}.driver-popover-close-btn:hover,.driver-popover-close-btn:focus{color:#2d2d2d}.driver-popover-title[style*=block]+.driver-popover-description{margin-top:5px}.driver-popover-description{margin-bottom:0;font:14px/normal sans-serif;line-height:1.5;font-weight:400;zoom:1}.driver-popover-footer{margin-top:15px;text-align:right;zoom:1;display:flex;align-items:center;justify-content:space-between}.driver-popover-progress-text{font-size:13px;font-weight:400;color:#727272;zoom:1}.driver-popover-footer button{all:unset;display:inline-block;box-sizing:border-box;padding:3px 7px;text-decoration:none;text-shadow:1px 1px 0 #fff;background-color:#fff;color:#2d2d2d;font:12px/normal sans-serif;cursor:pointer;outline:0;zoom:1;line-height:1.3;border:1px solid #ccc;border-radius:3px}.driver-popover-footer .driver-popover-btn-disabled{opacity:.5;pointer-events:none}:not(body):has(>.driver-active-element){overflow:hidden!important}.driver-no-interaction,.driver-no-interaction *{pointer-events:none!important}.driver-popover-footer button:hover,.driver-popover-footer button:focus{background-color:#f7f7f7}.driver-popover-navigation-btns{display:flex;flex-grow:1;justify-content:flex-end}.driver-popover-navigation-btns button+button{margin-left:4px}.driver-popover-arrow{content:"";position:absolute;border:5px solid #fff}.driver-popover-arrow-side-over{display:none}.driver-popover-arrow-side-left{left:100%;border-right-color:transparent;border-bottom-color:transparent;border-top-color:transparent}.driver-popover-arrow-side-right{right:100%;border-left-color:transparent;border-bottom-color:transparent;border-top-color:transparent}.driver-popover-arrow-side-top{top:100%;border-right-color:transparent;border-bottom-color:transparent;border-left-color:transparent}.driver-popover-arrow-side-bottom{bottom:100%;border-left-color:transparent;border-top-color:transparent;border-right-color:transparent}.driver-popover-arrow-side-center{display:none}.driver-popover-arrow-side-left.driver-popover-arrow-align-start,.driver-popover-arrow-side-right.driver-popover-arrow-align-start{top:15px}.driver-popover-arrow-side-top.driver-popover-arrow-align-start,.driver-popover-arrow-side-bottom.driver-popover-arrow-align-start{left:15px}.driver-popover-arrow-align-end.driver-popover-arrow-side-left,.driver-popover-arrow-align-end.driver-popover-arrow-side-right{bottom:15px}.driver-popover-arrow-side-top.driver-popover-arrow-align-end,.driver-popover-arrow-side-bottom.driver-popover-arrow-align-end{right:15px}.driver-popover-arrow-side-left.driver-popover-arrow-align-center,.driver-popover-arrow-side-right.driver-popover-arrow-align-center{top:50%;margin-top:-5px}.driver-popover-arrow-side-top.driver-popover-arrow-align-center,.driver-popover-arrow-side-bottom.driver-popover-arrow-align-center{left:50%;margin-left:-5px}.driver-popover-arrow-none{display:none}
|
||||
2
dashcaddy-api/assets/driver.min.js
vendored
Normal file
BIN
dashcaddy-api/assets/emby.png
Normal file
|
After Width: | Height: | Size: 9.0 KiB |
259
dashcaddy-api/assets/error-handler.js
Normal file
@@ -0,0 +1,259 @@
|
||||
/**
|
||||
* Error Handler
|
||||
* Handles errors gracefully without breaking the onboarding tour
|
||||
*/
|
||||
|
||||
(function(window) {
|
||||
'use strict';
|
||||
|
||||
class ErrorHandler {
|
||||
constructor() {
|
||||
this.errors = [];
|
||||
this.maxErrors = 50; // Keep last 50 errors
|
||||
}
|
||||
|
||||
/**
|
||||
* Log an error without breaking the tour
|
||||
* @param {string} context - Context where error occurred
|
||||
* @param {Error|string} error - The error object or message
|
||||
* @param {Object} metadata - Additional metadata
|
||||
*/
|
||||
logError(context, error, metadata = {}) {
|
||||
const errorEntry = {
|
||||
timestamp: new Date().toISOString(),
|
||||
context,
|
||||
message: error instanceof Error ? error.message : error,
|
||||
stack: error instanceof Error ? error.stack : null,
|
||||
metadata
|
||||
};
|
||||
|
||||
// Add to errors array
|
||||
this.errors.push(errorEntry);
|
||||
|
||||
// Keep only last maxErrors
|
||||
if (this.errors.length > this.maxErrors) {
|
||||
this.errors.shift();
|
||||
}
|
||||
|
||||
// Log to console
|
||||
console.error(`[Onboarding Error] ${context}:`, error, metadata);
|
||||
|
||||
// Optionally send to error tracking service
|
||||
// this.sendToErrorTracking(errorEntry);
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempt to recover from an error and continue tour
|
||||
* @param {Error} error - The error object
|
||||
* @param {number} currentStep - Current step index
|
||||
* @returns {Object} Recovery action
|
||||
*/
|
||||
recoverFromError(error, currentStep) {
|
||||
const errorType = this.classifyError(error);
|
||||
|
||||
switch (errorType) {
|
||||
case 'ELEMENT_NOT_FOUND':
|
||||
this.logError('Element Not Found', error, { currentStep });
|
||||
return {
|
||||
action: 'SKIP_STEP',
|
||||
nextStep: currentStep + 1,
|
||||
message: 'Target element not found, skipping to next step'
|
||||
};
|
||||
|
||||
case 'STORAGE_UNAVAILABLE':
|
||||
this.logError('Storage Unavailable', error);
|
||||
return {
|
||||
action: 'USE_MEMORY_STORAGE',
|
||||
message: 'Local storage unavailable, using in-memory storage'
|
||||
};
|
||||
|
||||
case 'DRIVER_NOT_LOADED':
|
||||
this.logError('Driver.js Not Loaded', error);
|
||||
return {
|
||||
action: 'ABORT_TOUR',
|
||||
message: 'Driver.js library not loaded, cannot start tour'
|
||||
};
|
||||
|
||||
case 'INVALID_TOOLTIP':
|
||||
this.logError('Invalid Tooltip Configuration', error, { currentStep });
|
||||
return {
|
||||
action: 'SKIP_STEP',
|
||||
nextStep: currentStep + 1,
|
||||
message: 'Invalid tooltip configuration, skipping'
|
||||
};
|
||||
|
||||
case 'THEME_DETECTION_FAILED':
|
||||
this.logError('Theme Detection Failed', error);
|
||||
return {
|
||||
action: 'USE_DEFAULT_THEME',
|
||||
message: 'Using default dark theme'
|
||||
};
|
||||
|
||||
default:
|
||||
this.logError('Unknown Error', error, { currentStep });
|
||||
return {
|
||||
action: 'ABORT_TOUR',
|
||||
message: 'Unexpected error occurred, aborting tour'
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Classify error type
|
||||
* @private
|
||||
* @param {Error} error - The error object
|
||||
* @returns {string} Error type
|
||||
*/
|
||||
classifyError(error) {
|
||||
const message = error.message || error.toString();
|
||||
|
||||
if (message.includes('element') && message.includes('not found')) {
|
||||
return 'ELEMENT_NOT_FOUND';
|
||||
}
|
||||
if (message.includes('storage') || message.includes('quota')) {
|
||||
return 'STORAGE_UNAVAILABLE';
|
||||
}
|
||||
if (message.includes('driver') || message.includes('undefined')) {
|
||||
return 'DRIVER_NOT_LOADED';
|
||||
}
|
||||
if (message.includes('invalid') || message.includes('validation')) {
|
||||
return 'INVALID_TOOLTIP';
|
||||
}
|
||||
if (message.includes('theme')) {
|
||||
return 'THEME_DETECTION_FAILED';
|
||||
}
|
||||
|
||||
return 'UNKNOWN';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all logged errors
|
||||
* @returns {Array} Array of error entries
|
||||
*/
|
||||
getErrors() {
|
||||
return [...this.errors];
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear all logged errors
|
||||
*/
|
||||
clearErrors() {
|
||||
this.errors = [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get error statistics
|
||||
* @returns {Object} Error statistics
|
||||
*/
|
||||
getStatistics() {
|
||||
const stats = {
|
||||
total: this.errors.length,
|
||||
byContext: {},
|
||||
byType: {},
|
||||
recent: this.errors.slice(-10)
|
||||
};
|
||||
|
||||
this.errors.forEach(error => {
|
||||
// Count by context
|
||||
stats.byContext[error.context] = (stats.byContext[error.context] || 0) + 1;
|
||||
|
||||
// Count by type
|
||||
const type = this.classifyError({ message: error.message });
|
||||
stats.byType[type] = (stats.byType[type] || 0) + 1;
|
||||
});
|
||||
|
||||
return stats;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle graceful degradation when Driver.js fails to load
|
||||
* @returns {boolean} Whether fallback was successful
|
||||
*/
|
||||
handleDriverLoadFailure() {
|
||||
this.logError('Driver.js Load Failure', 'Driver.js library failed to load');
|
||||
|
||||
// Show fallback message
|
||||
const fallbackMessage = document.createElement('div');
|
||||
fallbackMessage.id = 'onboarding-fallback';
|
||||
fallbackMessage.style.cssText = `
|
||||
position: fixed;
|
||||
bottom: 20px;
|
||||
right: 20px;
|
||||
background: var(--card-base, #2a2a2a);
|
||||
color: var(--fg, #ffffff);
|
||||
padding: 15px 20px;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 4px 12px rgba(0,0,0,0.3);
|
||||
z-index: 9999;
|
||||
max-width: 300px;
|
||||
font-size: 14px;
|
||||
`;
|
||||
fallbackMessage.innerHTML = `
|
||||
<strong>Welcome to DashCaddy!</strong><br>
|
||||
<p style="margin: 10px 0 0 0; font-size: 12px;">
|
||||
The interactive tour is unavailable, but you can explore the dashboard freely.
|
||||
Check the documentation for help getting started.
|
||||
</p>
|
||||
`;
|
||||
|
||||
document.body.appendChild(fallbackMessage);
|
||||
|
||||
// Auto-remove after 10 seconds
|
||||
setTimeout(() => {
|
||||
if (fallbackMessage.parentNode) {
|
||||
fallbackMessage.parentNode.removeChild(fallbackMessage);
|
||||
}
|
||||
}, 10000);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle storage unavailable scenario
|
||||
* @returns {Object} In-memory storage fallback
|
||||
*/
|
||||
handleStorageUnavailable() {
|
||||
this.logError('Storage Unavailable', 'Local storage is not available');
|
||||
|
||||
// Create in-memory storage
|
||||
const memoryStorage = {
|
||||
data: {},
|
||||
getItem(key) {
|
||||
return this.data[key] || null;
|
||||
},
|
||||
setItem(key, value) {
|
||||
this.data[key] = value;
|
||||
},
|
||||
removeItem(key) {
|
||||
delete this.data[key];
|
||||
},
|
||||
clear() {
|
||||
this.data = {};
|
||||
}
|
||||
};
|
||||
|
||||
console.warn('[ErrorHandler] Using in-memory storage - progress will not persist');
|
||||
return memoryStorage;
|
||||
}
|
||||
|
||||
/**
|
||||
* Send error to tracking service (placeholder)
|
||||
* @private
|
||||
* @param {Object} errorEntry - Error entry to send
|
||||
*/
|
||||
sendToErrorTracking(errorEntry) {
|
||||
// Placeholder for error tracking integration
|
||||
// Could integrate with Sentry, LogRocket, etc.
|
||||
// Example:
|
||||
// if (window.Sentry) {
|
||||
// Sentry.captureException(new Error(errorEntry.message), {
|
||||
// extra: errorEntry.metadata
|
||||
// });
|
||||
// }
|
||||
}
|
||||
}
|
||||
|
||||
window.ErrorHandler = ErrorHandler;
|
||||
console.log('[ErrorHandler] Module loaded');
|
||||
|
||||
})(window);
|
||||
BIN
dashcaddy-api/assets/favicon.ico
Normal file
|
After Width: | Height: | Size: 15 KiB |
BIN
dashcaddy-api/assets/favicon.png
Normal file
|
After Width: | Height: | Size: 1.5 KiB |
1
dashcaddy-api/assets/favicon.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64"><rect width="64" height="64" rx="12" fill="#0e1116"/><path d="M16 38h20a8 8 0 1 0-1.8-15.7A9.5 9.5 0 0 0 12 30c0 4.4 3.6 8 8 8z" fill="#8FD6FF"/></svg>
|
||||
|
After Width: | Height: | Size: 211 B |
BIN
dashcaddy-api/assets/filebrowser.png
Normal file
|
After Width: | Height: | Size: 25 KiB |
91
dashcaddy-api/assets/fonts.css
Normal file
@@ -0,0 +1,91 @@
|
||||
/* Sami Sans Font Family - External CSS */
|
||||
|
||||
@font-face {
|
||||
font-family: 'Sami Sans';
|
||||
src: url('fonts/SamiSans-Regular.woff2') format('woff2'),
|
||||
url('fonts/SamiSans-Regular.ttf') format('truetype');
|
||||
font-weight: 400;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Sami Sans';
|
||||
src: url('fonts/SamiSans-Regular.woff2') format('woff2'),
|
||||
url('fonts/SamiSans-Italic.ttf') format('truetype');
|
||||
font-weight: 400;
|
||||
font-style: italic;
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Sami Sans';
|
||||
src: url('fonts/SamiSans-Medium.woff2') format('woff2'),
|
||||
url('fonts/SamiSans-Medium.ttf') format('truetype');
|
||||
font-weight: 500;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Sami Sans';
|
||||
src: url('fonts/SamiSans-SemiBold.woff2') format('woff2'),
|
||||
url('fonts/SamiSans-SemiBold.ttf') format('truetype');
|
||||
font-weight: 600;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Sami Sans';
|
||||
src: url('fonts/SamiSans-Bold.woff2') format('woff2'),
|
||||
url('fonts/SamiSans-Bold.ttf') format('truetype');
|
||||
font-weight: 700;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Sami Sans';
|
||||
src: url('fonts/SamiSans-ExtraBold.woff2') format('woff2'),
|
||||
url('fonts/SamiSans-ExtraBold.ttf') format('truetype');
|
||||
font-weight: 800;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Sami Sans';
|
||||
src: url('fonts/SamiSans-Black.woff2') format('woff2'),
|
||||
url('fonts/SamiSans-Black.ttf') format('truetype');
|
||||
font-weight: 900;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Sami Sans';
|
||||
src: url('fonts/SamiSans-Light.woff2') format('woff2'),
|
||||
url('fonts/SamiSans-Light.ttf') format('truetype');
|
||||
font-weight: 300;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Sami Sans';
|
||||
src: url('fonts/SamiSans-ExtraLight.woff2') format('woff2'),
|
||||
url('fonts/SamiSans-ExtraLight.ttf') format('truetype');
|
||||
font-weight: 200;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Sami Sans';
|
||||
src: url('fonts/SamiSans-Thin.woff2') format('woff2'),
|
||||
url('fonts/SamiSans-Thin.ttf') format('truetype');
|
||||
font-weight: 100;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
}
|
||||
BIN
dashcaddy-api/assets/fonts/SamiGrotesk-Black.ttf
Normal file
BIN
dashcaddy-api/assets/fonts/SamiGrotesk-BlackItalic.ttf
Normal file
BIN
dashcaddy-api/assets/fonts/SamiGrotesk-Bold.ttf
Normal file
BIN
dashcaddy-api/assets/fonts/SamiGrotesk-BoldItalic.ttf
Normal file
BIN
dashcaddy-api/assets/fonts/SamiGrotesk-Light.ttf
Normal file
BIN
dashcaddy-api/assets/fonts/SamiGrotesk-LightItalic.ttf
Normal file
BIN
dashcaddy-api/assets/fonts/SamiGrotesk-Medium.ttf
Normal file
BIN
dashcaddy-api/assets/fonts/SamiGrotesk-MediumItalic.ttf
Normal file
BIN
dashcaddy-api/assets/fonts/SamiGrotesk-Regular.ttf
Normal file
BIN
dashcaddy-api/assets/fonts/SamiGrotesk-RegularItalic.ttf
Normal file
BIN
dashcaddy-api/assets/fonts/SamiSans-Black.ttf
Normal file
BIN
dashcaddy-api/assets/fonts/SamiSans-Black.woff2
Normal file
BIN
dashcaddy-api/assets/fonts/SamiSans-BlackItalic.ttf
Normal file
BIN
dashcaddy-api/assets/fonts/SamiSans-BlackItalic.woff2
Normal file
BIN
dashcaddy-api/assets/fonts/SamiSans-Bold.ttf
Normal file
BIN
dashcaddy-api/assets/fonts/SamiSans-Bold.woff2
Normal file
BIN
dashcaddy-api/assets/fonts/SamiSans-BoldItalic.ttf
Normal file
BIN
dashcaddy-api/assets/fonts/SamiSans-BoldItalic.woff2
Normal file
BIN
dashcaddy-api/assets/fonts/SamiSans-ExtraBold.ttf
Normal file
BIN
dashcaddy-api/assets/fonts/SamiSans-ExtraBold.woff2
Normal file
BIN
dashcaddy-api/assets/fonts/SamiSans-ExtraBoldItalic.ttf
Normal file
BIN
dashcaddy-api/assets/fonts/SamiSans-ExtraBoldItalic.woff2
Normal file
BIN
dashcaddy-api/assets/fonts/SamiSans-ExtraLight.ttf
Normal file
BIN
dashcaddy-api/assets/fonts/SamiSans-ExtraLight.woff2
Normal file
BIN
dashcaddy-api/assets/fonts/SamiSans-ExtraLightItalic.ttf
Normal file
BIN
dashcaddy-api/assets/fonts/SamiSans-ExtraLightItalic.woff2
Normal file
BIN
dashcaddy-api/assets/fonts/SamiSans-Italic.ttf
Normal file
BIN
dashcaddy-api/assets/fonts/SamiSans-Italic.woff2
Normal file
BIN
dashcaddy-api/assets/fonts/SamiSans-Light.ttf
Normal file
BIN
dashcaddy-api/assets/fonts/SamiSans-Light.woff2
Normal file
BIN
dashcaddy-api/assets/fonts/SamiSans-LightItalic.ttf
Normal file
BIN
dashcaddy-api/assets/fonts/SamiSans-LightItalic.woff2
Normal file
BIN
dashcaddy-api/assets/fonts/SamiSans-Medium.ttf
Normal file
BIN
dashcaddy-api/assets/fonts/SamiSans-Medium.woff2
Normal file
BIN
dashcaddy-api/assets/fonts/SamiSans-MediumItalic.ttf
Normal file
BIN
dashcaddy-api/assets/fonts/SamiSans-MediumItalic.woff2
Normal file
BIN
dashcaddy-api/assets/fonts/SamiSans-Regular.ttf
Normal file
BIN
dashcaddy-api/assets/fonts/SamiSans-Regular.woff2
Normal file
BIN
dashcaddy-api/assets/fonts/SamiSans-SemiBold.ttf
Normal file
BIN
dashcaddy-api/assets/fonts/SamiSans-SemiBold.woff2
Normal file
BIN
dashcaddy-api/assets/fonts/SamiSans-SemiBoldItalic.ttf
Normal file
BIN
dashcaddy-api/assets/fonts/SamiSans-SemiBoldItalic.woff2
Normal file
BIN
dashcaddy-api/assets/fonts/SamiSans-Thin.ttf
Normal file
BIN
dashcaddy-api/assets/fonts/SamiSans-Thin.woff2
Normal file
BIN
dashcaddy-api/assets/fonts/SamiSans-ThinItalic.ttf
Normal file
BIN
dashcaddy-api/assets/fonts/SamiSans-ThinItalic.woff2
Normal file
BIN
dashcaddy-api/assets/fonts/sami-grotesk/SamiGrotesk-Black.ttf
Normal file
BIN
dashcaddy-api/assets/fonts/sami-grotesk/SamiGrotesk-Bold.ttf
Normal file
BIN
dashcaddy-api/assets/fonts/sami-grotesk/SamiGrotesk-Light.ttf
Normal file
BIN
dashcaddy-api/assets/fonts/sami-grotesk/SamiGrotesk-Medium.ttf
Normal file
BIN
dashcaddy-api/assets/fonts/sami-grotesk/SamiGrotesk-Regular.ttf
Normal file
BIN
dashcaddy-api/assets/icon-192.png
Normal file
|
After Width: | Height: | Size: 13 KiB |
BIN
dashcaddy-api/assets/icon-512.png
Normal file
|
After Width: | Height: | Size: 53 KiB |
BIN
dashcaddy-api/assets/jellyfin.png
Normal file
|
After Width: | Height: | Size: 45 KiB |
BIN
dashcaddy-api/assets/nginx.png
Normal file
|
After Width: | Height: | Size: 13 KiB |
354
dashcaddy-api/assets/onboarding.css
Normal file
@@ -0,0 +1,354 @@
|
||||
/**
|
||||
* Onboarding Tooltip Styles
|
||||
* Custom styling for Driver.js tooltips to match DashCaddy theme
|
||||
*/
|
||||
|
||||
/* Driver.js overrides are injected dynamically by ThemeAdapter */
|
||||
/* This file contains additional custom styles */
|
||||
|
||||
.driver-popover {
|
||||
max-width: 500px !important;
|
||||
z-index: 10000 !important;
|
||||
}
|
||||
|
||||
.driver-popover-title {
|
||||
font-size: 1.2rem !important;
|
||||
margin-bottom: 12px !important;
|
||||
}
|
||||
|
||||
.driver-popover-description {
|
||||
font-size: 0.95rem !important;
|
||||
line-height: 1.6 !important;
|
||||
}
|
||||
|
||||
.driver-popover-description p {
|
||||
margin: 8px 0 !important;
|
||||
}
|
||||
|
||||
.driver-popover-description ul {
|
||||
margin: 8px 0 !important;
|
||||
padding-left: 20px !important;
|
||||
}
|
||||
|
||||
.driver-popover-description li {
|
||||
margin: 4px 0 !important;
|
||||
}
|
||||
|
||||
.driver-popover-description code {
|
||||
background: rgba(0, 0, 0, 0.1) !important;
|
||||
padding: 2px 6px !important;
|
||||
border-radius: 3px !important;
|
||||
font-family: 'Courier New', monospace !important;
|
||||
font-size: 0.9em !important;
|
||||
}
|
||||
|
||||
.driver-popover-footer {
|
||||
margin-top: 16px !important;
|
||||
display: flex !important;
|
||||
gap: 8px !important;
|
||||
justify-content: flex-end !important;
|
||||
}
|
||||
|
||||
.driver-popover-footer button {
|
||||
padding: 8px 16px !important;
|
||||
border-radius: 8px !important;
|
||||
font-size: 0.9rem !important;
|
||||
cursor: pointer !important;
|
||||
transition: all 0.2s ease !important;
|
||||
}
|
||||
|
||||
.driver-popover-footer button:hover {
|
||||
transform: translateY(-1px) !important;
|
||||
}
|
||||
|
||||
.driver-popover-close-btn {
|
||||
position: absolute !important;
|
||||
top: 12px !important;
|
||||
right: 12px !important;
|
||||
width: 24px !important;
|
||||
height: 24px !important;
|
||||
border-radius: 50% !important;
|
||||
display: flex !important;
|
||||
align-items: center !important;
|
||||
justify-content: center !important;
|
||||
cursor: pointer !important;
|
||||
opacity: 0.6 !important;
|
||||
transition: opacity 0.2s ease !important;
|
||||
}
|
||||
|
||||
.driver-popover-close-btn:hover {
|
||||
opacity: 1 !important;
|
||||
}
|
||||
|
||||
.driver-popover-arrow {
|
||||
border-width: 8px !important;
|
||||
}
|
||||
|
||||
/* Progress indicator */
|
||||
.driver-popover-progress-text {
|
||||
font-size: 0.85rem !important;
|
||||
margin-bottom: 8px !important;
|
||||
}
|
||||
|
||||
/* Mobile responsive */
|
||||
@media (max-width: 768px) {
|
||||
.driver-popover {
|
||||
max-width: calc(100vw - 32px) !important;
|
||||
}
|
||||
|
||||
.driver-popover-title {
|
||||
font-size: 1.1rem !important;
|
||||
}
|
||||
|
||||
.driver-popover-description {
|
||||
font-size: 0.9rem !important;
|
||||
}
|
||||
|
||||
.driver-popover-footer button {
|
||||
padding: 6px 12px !important;
|
||||
font-size: 0.85rem !important;
|
||||
}
|
||||
}
|
||||
|
||||
/* Restart tour button in dashboard */
|
||||
#restart-tour-btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
#restart-tour-btn::before {
|
||||
content: "🎓";
|
||||
font-size: 1.1em;
|
||||
}
|
||||
|
||||
|
||||
/* DNS Template Selector Modal */
|
||||
.dns-template-modal {
|
||||
display: none;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: rgba(0, 0, 0, 0.8);
|
||||
z-index: 10000;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.dns-template-modal-content {
|
||||
background: var(--card-base);
|
||||
border-radius: 12px;
|
||||
max-width: 900px;
|
||||
width: 100%;
|
||||
max-height: 90vh;
|
||||
overflow-y: auto;
|
||||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.4);
|
||||
}
|
||||
|
||||
.dns-template-header {
|
||||
padding: 30px;
|
||||
border-bottom: 1px solid var(--border);
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.dns-template-header h2 {
|
||||
margin: 0 0 10px 0;
|
||||
color: var(--fg);
|
||||
font-size: 28px;
|
||||
}
|
||||
|
||||
.dns-template-header p {
|
||||
margin: 0;
|
||||
color: var(--fg-muted);
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.dns-template-close {
|
||||
position: absolute;
|
||||
top: 20px;
|
||||
right: 20px;
|
||||
background: none;
|
||||
border: none;
|
||||
font-size: 32px;
|
||||
color: var(--fg-muted);
|
||||
cursor: pointer;
|
||||
padding: 0;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 50%;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.dns-template-close:hover {
|
||||
background: var(--hover);
|
||||
color: var(--fg);
|
||||
}
|
||||
|
||||
.dns-template-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
|
||||
gap: 20px;
|
||||
padding: 30px;
|
||||
}
|
||||
|
||||
.dns-template-card {
|
||||
background: var(--card-hover);
|
||||
border: 2px solid var(--border);
|
||||
border-radius: 12px;
|
||||
padding: 20px;
|
||||
transition: all 0.3s;
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.dns-template-card:hover {
|
||||
transform: translateY(-4px);
|
||||
box-shadow: 0 8px 16px rgba(0, 0, 0, 0.2);
|
||||
border-color: var(--accent);
|
||||
}
|
||||
|
||||
.dns-template-card.recommended {
|
||||
border-color: var(--accent);
|
||||
background: linear-gradient(135deg, var(--card-hover) 0%, var(--card-base) 100%);
|
||||
}
|
||||
|
||||
.recommended-badge {
|
||||
position: absolute;
|
||||
top: -10px;
|
||||
right: 20px;
|
||||
background: var(--accent);
|
||||
color: white;
|
||||
padding: 4px 12px;
|
||||
border-radius: 12px;
|
||||
font-size: 11px;
|
||||
font-weight: bold;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
.dns-template-icon {
|
||||
font-size: 48px;
|
||||
margin-bottom: 15px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.dns-template-card h3 {
|
||||
margin: 0 0 10px 0;
|
||||
color: var(--fg);
|
||||
font-size: 18px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.dns-template-description {
|
||||
color: var(--fg-muted);
|
||||
font-size: 13px;
|
||||
margin: 0 0 15px 0;
|
||||
text-align: center;
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.dns-template-difficulty {
|
||||
display: inline-block;
|
||||
padding: 4px 12px;
|
||||
border-radius: 12px;
|
||||
font-size: 11px;
|
||||
font-weight: bold;
|
||||
text-align: center;
|
||||
margin: 0 auto 15px auto;
|
||||
}
|
||||
|
||||
.difficulty-easy {
|
||||
background: #2ecc71;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.difficulty-intermediate {
|
||||
background: #f39c12;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.difficulty-advanced {
|
||||
background: #e74c3c;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.dns-template-features {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 0 0 20px 0;
|
||||
font-size: 12px;
|
||||
color: var(--fg-muted);
|
||||
}
|
||||
|
||||
.dns-template-features li {
|
||||
padding: 6px 0;
|
||||
padding-left: 20px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.dns-template-features li:before {
|
||||
content: "✓";
|
||||
position: absolute;
|
||||
left: 0;
|
||||
color: var(--accent);
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.dns-template-select-btn {
|
||||
background: var(--accent);
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 12px 20px;
|
||||
border-radius: 8px;
|
||||
font-size: 14px;
|
||||
font-weight: bold;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.dns-template-select-btn:hover {
|
||||
background: var(--accent-strong);
|
||||
transform: scale(1.02);
|
||||
}
|
||||
|
||||
.dns-template-footer {
|
||||
padding: 20px 30px;
|
||||
border-top: 1px solid var(--border);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.dns-template-later-btn {
|
||||
background: transparent;
|
||||
color: var(--fg-muted);
|
||||
border: 1px solid var(--border);
|
||||
padding: 10px 24px;
|
||||
border-radius: 8px;
|
||||
font-size: 14px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.dns-template-later-btn:hover {
|
||||
background: var(--hover);
|
||||
color: var(--fg);
|
||||
border-color: var(--fg-muted);
|
||||
}
|
||||
|
||||
/* Responsive design */
|
||||
@media (max-width: 768px) {
|
||||
.dns-template-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.dns-template-modal-content {
|
||||
max-height: 95vh;
|
||||
}
|
||||
}
|
||||
177
dashcaddy-api/assets/onboarding.js
Normal file
@@ -0,0 +1,177 @@
|
||||
/**
|
||||
* DashCaddy User Onboarding System
|
||||
* Main entry point for the tooltip-based onboarding experience
|
||||
*
|
||||
* This file initializes the onboarding system and coordinates between
|
||||
* the various components (TourManager, ProgressTracker, ThemeAdapter, etc.)
|
||||
*/
|
||||
|
||||
(function() {
|
||||
'use strict';
|
||||
|
||||
let progressTracker;
|
||||
let themeAdapter;
|
||||
let tourManager;
|
||||
let dnsTemplateSelector;
|
||||
let errorHandler;
|
||||
|
||||
/**
|
||||
* Initialize the onboarding system
|
||||
*/
|
||||
async function initializeOnboarding() {
|
||||
try {
|
||||
console.log('[Onboarding] Initializing system...');
|
||||
|
||||
// Initialize Error Handler first
|
||||
errorHandler = new ErrorHandler();
|
||||
console.log('[Onboarding] Error Handler initialized');
|
||||
|
||||
// Initialize Progress Tracker
|
||||
progressTracker = new ProgressTracker('dashcaddy_onboarding');
|
||||
console.log('[Onboarding] Progress Tracker initialized');
|
||||
|
||||
// Initialize Theme Adapter
|
||||
themeAdapter = new ThemeAdapter();
|
||||
console.log('[Onboarding] Theme Adapter initialized');
|
||||
|
||||
// Initialize DNS Template Selector
|
||||
dnsTemplateSelector = new DnsTemplateSelector(progressTracker);
|
||||
console.log('[Onboarding] DNS Template Selector initialized');
|
||||
|
||||
// Initialize Tour Manager
|
||||
tourManager = new TourManager(progressTracker, themeAdapter, dnsTemplateSelector);
|
||||
console.log('[Onboarding] Tour Manager initialized');
|
||||
|
||||
// Check if tour should auto-start
|
||||
if (tourManager.shouldAutoStart()) {
|
||||
console.log('[Onboarding] Auto-starting tour for first-time user');
|
||||
// Wait a bit for page to fully load
|
||||
setTimeout(() => {
|
||||
tourManager.startTour();
|
||||
}, 1000);
|
||||
} else {
|
||||
const tourCompleted = progressTracker.isTourCompleted();
|
||||
const currentStep = progressTracker.getCurrentStep();
|
||||
console.log(`[Onboarding] Tour not auto-starting (completed: ${tourCompleted}, step: ${currentStep})`);
|
||||
|
||||
// If tour is in progress, offer to resume
|
||||
if (!tourCompleted && currentStep > 0) {
|
||||
console.log('[Onboarding] Tour in progress, can be resumed manually');
|
||||
}
|
||||
}
|
||||
|
||||
// Add restart tour button to tools row
|
||||
addRestartTourButton();
|
||||
|
||||
// Expose to global scope for manual triggering
|
||||
window.DashCaddyOnboarding = {
|
||||
startTour: () => tourManager.startTour(),
|
||||
restartTour: () => tourManager.restartTour(),
|
||||
showTooltip: (id) => tourManager.showTooltip(id),
|
||||
showWhatsNew: () => tourManager.showWhatsNew(),
|
||||
resetProgress: () => progressTracker.resetProgress(),
|
||||
getErrors: () => errorHandler.getErrors(),
|
||||
getErrorStats: () => errorHandler.getStatistics()
|
||||
};
|
||||
|
||||
console.log('[Onboarding] System initialized successfully');
|
||||
} catch (error) {
|
||||
console.error('[Onboarding] Initialization error:', error);
|
||||
|
||||
// Use error handler if available
|
||||
if (errorHandler) {
|
||||
errorHandler.logError('Initialization', error);
|
||||
}
|
||||
|
||||
// Graceful degradation - don't break the dashboard
|
||||
console.warn('[Onboarding] System failed to initialize, dashboard will continue without onboarding');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add restart tour button to tools row
|
||||
*/
|
||||
function addRestartTourButton() {
|
||||
const toolsRow = document.querySelector('.tools');
|
||||
if (!toolsRow) return;
|
||||
|
||||
const clickHandler = () => {
|
||||
if (tourManager) {
|
||||
console.log('[Onboarding] Starting tour via button click');
|
||||
tourManager.restartTour();
|
||||
} else {
|
||||
console.error('[Onboarding] Tour manager not initialized');
|
||||
alert('Tour is not available. Check browser console for errors.\n\nPossible issues:\n- Driver.js library failed to load\n- JavaScript errors during initialization');
|
||||
}
|
||||
};
|
||||
|
||||
// If button already exists in the HTML, just attach the handler
|
||||
const existing = document.getElementById('restart-tour-btn');
|
||||
if (existing) {
|
||||
existing.onclick = clickHandler;
|
||||
return;
|
||||
}
|
||||
|
||||
const button = document.createElement('button');
|
||||
button.id = 'restart-tour-btn';
|
||||
button.textContent = 'Help Tour';
|
||||
button.title = 'Restart the onboarding tour';
|
||||
button.onclick = clickHandler;
|
||||
toolsRow.appendChild(button);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if Driver.js is loaded
|
||||
*/
|
||||
function checkDriverLoaded() {
|
||||
// Driver.js v1.x IIFE: window.driver.js.driver is the factory function
|
||||
const driverFactory = window.driver?.js?.driver || window.driver?.driver || window.driver;
|
||||
if (typeof driverFactory !== 'function') {
|
||||
console.warn('[Onboarding] Driver.js not loaded yet, will retry... window.driver:', window.driver);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Wait for Driver.js to load, then initialize
|
||||
*/
|
||||
function waitForDriver() {
|
||||
let retries = 0;
|
||||
const maxRetries = 10;
|
||||
|
||||
function attemptInit() {
|
||||
if (checkDriverLoaded()) {
|
||||
initializeOnboarding();
|
||||
} else {
|
||||
retries++;
|
||||
if (retries < maxRetries) {
|
||||
// Retry after a short delay
|
||||
setTimeout(attemptInit, 500);
|
||||
} else {
|
||||
// Max retries reached, show fallback
|
||||
console.error('[Onboarding] Driver.js failed to load after multiple attempts');
|
||||
if (errorHandler) {
|
||||
errorHandler.handleDriverLoadFailure();
|
||||
} else {
|
||||
// Create temporary error handler for fallback
|
||||
const tempHandler = new ErrorHandler();
|
||||
tempHandler.handleDriverLoadFailure();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
attemptInit();
|
||||
}
|
||||
|
||||
// Start initialization when DOM is ready
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', waitForDriver);
|
||||
} else {
|
||||
waitForDriver();
|
||||
}
|
||||
|
||||
console.log('[Onboarding] System loaded');
|
||||
|
||||
})();
|
||||
BIN
dashcaddy-api/assets/pics.png
Normal file
|
After Width: | Height: | Size: 122 KiB |
BIN
dashcaddy-api/assets/plex.png
Normal file
|
After Width: | Height: | Size: 10 KiB |
BIN
dashcaddy-api/assets/portainer.png
Normal file
|
After Width: | Height: | Size: 710 B |
282
dashcaddy-api/assets/progress-tracker.js
Normal file
@@ -0,0 +1,282 @@
|
||||
/**
|
||||
* Progress Tracker
|
||||
* Manages persistent storage of user progress through the onboarding flow
|
||||
* using browser local storage.
|
||||
*
|
||||
* Storage Schema:
|
||||
* {
|
||||
* "version": "1.0",
|
||||
* "tourCompleted": false,
|
||||
* "completedTooltips": ["welcome", "dns-priority", ...],
|
||||
* "currentStep": 3,
|
||||
* "completionTimestamp": "2024-01-15T10:30:00Z",
|
||||
* "dnsSetupDeferred": false,
|
||||
* "lastVisit": "2024-01-15T10:30:00Z"
|
||||
* }
|
||||
*/
|
||||
|
||||
(function(window) {
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* ProgressTracker class
|
||||
* Manages persistent storage of onboarding progress
|
||||
*
|
||||
* @class
|
||||
* @param {string} storageKey - The key to use for local storage (default: 'dashcaddy_onboarding')
|
||||
*/
|
||||
class ProgressTracker {
|
||||
constructor(storageKey = 'dashcaddy_onboarding') {
|
||||
this.storageKey = storageKey;
|
||||
this.storageVersion = '1.0';
|
||||
|
||||
// Initialize storage if it doesn't exist
|
||||
this._initializeStorage();
|
||||
|
||||
// Update last visit timestamp
|
||||
this._updateLastVisit();
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize storage with default values if it doesn't exist
|
||||
* @private
|
||||
*/
|
||||
_initializeStorage() {
|
||||
const existing = this._getStorage();
|
||||
if (!existing || existing.version !== this.storageVersion) {
|
||||
const defaultState = {
|
||||
version: this.storageVersion,
|
||||
tourCompleted: false,
|
||||
completedTooltips: [],
|
||||
currentStep: 0,
|
||||
completionTimestamp: null,
|
||||
dnsSetupDeferred: false,
|
||||
lastVisit: new Date().toISOString()
|
||||
};
|
||||
this._setStorage(defaultState);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the current storage state
|
||||
* @private
|
||||
* @returns {Object|null} The storage state or null if unavailable
|
||||
*/
|
||||
_getStorage() {
|
||||
try {
|
||||
const data = localStorage.getItem(this.storageKey);
|
||||
return data ? JSON.parse(data) : null;
|
||||
} catch (error) {
|
||||
console.error('[ProgressTracker] Error reading from storage:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the storage state
|
||||
* @private
|
||||
* @param {Object} state - The state to save
|
||||
*/
|
||||
_setStorage(state) {
|
||||
try {
|
||||
localStorage.setItem(this.storageKey, JSON.stringify(state));
|
||||
} catch (error) {
|
||||
console.error('[ProgressTracker] Error writing to storage:', error);
|
||||
// Handle quota exceeded or storage unavailable
|
||||
// Fall back to session storage or in-memory storage
|
||||
this._handleStorageError(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle storage errors (quota exceeded, unavailable, etc.)
|
||||
* @private
|
||||
* @param {Error} error - The error that occurred
|
||||
*/
|
||||
_handleStorageError(error) {
|
||||
// Try session storage as fallback
|
||||
try {
|
||||
sessionStorage.setItem(this.storageKey, JSON.stringify(this._getStorage()));
|
||||
console.warn('[ProgressTracker] Falling back to session storage');
|
||||
} catch (sessionError) {
|
||||
console.error('[ProgressTracker] Session storage also unavailable:', sessionError);
|
||||
// Could implement in-memory fallback here if needed
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the last visit timestamp
|
||||
* @private
|
||||
*/
|
||||
_updateLastVisit() {
|
||||
const state = this._getStorage();
|
||||
if (state) {
|
||||
state.lastVisit = new Date().toISOString();
|
||||
this._setStorage(state);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a specific tooltip has been completed
|
||||
* @param {string} tooltipId - The ID of the tooltip to check
|
||||
* @returns {boolean} True if the tooltip has been completed
|
||||
*/
|
||||
isTooltipCompleted(tooltipId) {
|
||||
const state = this._getStorage();
|
||||
if (!state) return false;
|
||||
return state.completedTooltips.includes(tooltipId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark a tooltip as completed with timestamp
|
||||
* @param {string} tooltipId - The ID of the tooltip to mark as completed
|
||||
*/
|
||||
markTooltipCompleted(tooltipId) {
|
||||
const state = this._getStorage();
|
||||
if (!state) return;
|
||||
|
||||
// Add tooltip to completed list if not already there
|
||||
if (!state.completedTooltips.includes(tooltipId)) {
|
||||
state.completedTooltips.push(tooltipId);
|
||||
|
||||
// Store timestamp for this specific tooltip
|
||||
if (!state.tooltipTimestamps) {
|
||||
state.tooltipTimestamps = {};
|
||||
}
|
||||
state.tooltipTimestamps[tooltipId] = new Date().toISOString();
|
||||
|
||||
this._setStorage(state);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the entire tour has been completed
|
||||
* @returns {boolean} True if the tour is completed
|
||||
*/
|
||||
isTourCompleted() {
|
||||
const state = this._getStorage();
|
||||
if (!state) return false;
|
||||
return state.tourCompleted === true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark the entire tour as completed
|
||||
*/
|
||||
markTourCompleted() {
|
||||
const state = this._getStorage();
|
||||
if (!state) return;
|
||||
|
||||
state.tourCompleted = true;
|
||||
state.completionTimestamp = new Date().toISOString();
|
||||
this._setStorage(state);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the current step index
|
||||
* @returns {number} The current step index (0-based)
|
||||
*/
|
||||
getCurrentStep() {
|
||||
const state = this._getStorage();
|
||||
if (!state) return 0;
|
||||
return state.currentStep || 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the current step index
|
||||
* @param {number} stepIndex - The step index to set (0-based)
|
||||
*/
|
||||
setCurrentStep(stepIndex) {
|
||||
const state = this._getStorage();
|
||||
if (!state) return;
|
||||
|
||||
state.currentStep = stepIndex;
|
||||
this._setStorage(state);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset all progress and clear storage
|
||||
*/
|
||||
resetProgress() {
|
||||
const defaultState = {
|
||||
version: this.storageVersion,
|
||||
tourCompleted: false,
|
||||
completedTooltips: [],
|
||||
currentStep: 0,
|
||||
completionTimestamp: null,
|
||||
dnsSetupDeferred: false,
|
||||
lastVisit: new Date().toISOString()
|
||||
};
|
||||
this._setStorage(defaultState);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the completion timestamp
|
||||
* @returns {Date|null} The completion timestamp or null if not completed
|
||||
*/
|
||||
getCompletionTimestamp() {
|
||||
const state = this._getStorage();
|
||||
if (!state || !state.completionTimestamp) return null;
|
||||
return new Date(state.completionTimestamp);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if DNS setup was deferred
|
||||
* @returns {boolean} True if DNS setup was deferred
|
||||
*/
|
||||
isDnsSetupDeferred() {
|
||||
const state = this._getStorage();
|
||||
if (!state) return false;
|
||||
return state.dnsSetupDeferred === true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark DNS setup as deferred
|
||||
*/
|
||||
markDnsSetupDeferred() {
|
||||
const state = this._getStorage();
|
||||
if (!state) return;
|
||||
|
||||
state.dnsSetupDeferred = true;
|
||||
this._setStorage(state);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the timestamp for a specific tooltip completion
|
||||
* @param {string} tooltipId - The ID of the tooltip
|
||||
* @returns {Date|null} The timestamp or null if not completed
|
||||
*/
|
||||
getTooltipTimestamp(tooltipId) {
|
||||
const state = this._getStorage();
|
||||
if (!state || !state.tooltipTimestamps || !state.tooltipTimestamps[tooltipId]) {
|
||||
return null;
|
||||
}
|
||||
return new Date(state.tooltipTimestamps[tooltipId]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all completed tooltip IDs
|
||||
* @returns {string[]} Array of completed tooltip IDs
|
||||
*/
|
||||
getCompletedTooltips() {
|
||||
const state = this._getStorage();
|
||||
if (!state) return [];
|
||||
return state.completedTooltips || [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the last visit timestamp
|
||||
* @returns {Date|null} The last visit timestamp
|
||||
*/
|
||||
getLastVisit() {
|
||||
const state = this._getStorage();
|
||||
if (!state || !state.lastVisit) return null;
|
||||
return new Date(state.lastVisit);
|
||||
}
|
||||
}
|
||||
|
||||
// Export to global scope
|
||||
window.ProgressTracker = ProgressTracker;
|
||||
|
||||
console.log('[ProgressTracker] Module loaded');
|
||||
|
||||
})(window);
|
||||
BIN
dashcaddy-api/assets/prowlarr.png
Normal file
|
After Width: | Height: | Size: 98 KiB |
BIN
dashcaddy-api/assets/qBittorrent.png
Normal file
|
After Width: | Height: | Size: 135 KiB |
BIN
dashcaddy-api/assets/radarr.png
Normal file
|
After Width: | Height: | Size: 18 KiB |
BIN
dashcaddy-api/assets/router.png
Normal file
|
After Width: | Height: | Size: 41 KiB |
BIN
dashcaddy-api/assets/sami-favicon.png
Normal file
|
After Width: | Height: | Size: 77 KiB |
BIN
dashcaddy-api/assets/sami-logo.png
Normal file
|
After Width: | Height: | Size: 163 KiB |
20
dashcaddy-api/assets/site.webmanifest
Normal file
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"name": "SAMI-CLOUD Status",
|
||||
"short_name": "SAMI-CLOUD",
|
||||
"start_url": "index.html",
|
||||
"display": "standalone",
|
||||
"background_color": "#0b0f1a",
|
||||
"theme_color": "#0e1116",
|
||||
"icons": [
|
||||
{
|
||||
"src": "assets/icon-192.png",
|
||||
"sizes": "192x192",
|
||||
"type": "image/png"
|
||||
},
|
||||
{
|
||||
"src": "assets/icon-512.png",
|
||||
"sizes": "512x512",
|
||||
"type": "image/png"
|
||||
}
|
||||
]
|
||||
}
|
||||
BIN
dashcaddy-api/assets/sonarr.png
Normal file
|
After Width: | Height: | Size: 13 KiB |
BIN
dashcaddy-api/assets/syncthing.png
Normal file
|
After Width: | Height: | Size: 27 KiB |