Sync DNS2 production changes - removed obsolete test suite and refactored structure
No files matched your search
@@ -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>
|
||||
|
After Width: | Height: | Size: 357 KiB |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="2880" zoomAndPan="magnify" viewBox="0 0 2160 719.999996" height="960" preserveAspectRatio="xMidYMid meet" version="1.0"><defs><filter x="0%" y="0%" width="100%" height="100%" id="4a5894a8fc"><feColorMatrix values="0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 0 0 0 1 0" color-interpolation-filters="sRGB"/></filter><filter x="0%" y="0%" width="100%" height="100%" id="f9928a4cb4"><feColorMatrix values="0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 0.2126 0.7152 0.0722 0 0" color-interpolation-filters="sRGB"/></filter><g/><clipPath id="cfc277b445"><path d="M 10.71875 1 L 261.511719 1 L 261.511719 407.753906 L 10.71875 407.753906 Z M 10.71875 1 " clip-rule="nonzero"/></clipPath><mask id="f4f1ffa525"><g filter="url(#4a5894a8fc)"><g filter="url(#f9928a4cb4)" transform="matrix(0.263399, 0, 0, 0.264939, 0.0292621, 0.809016)"><image x="0" y="0" width="1024" xlink:href="data:image/png;base64,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 truncated
|
||||
|
After Width: | Height: | Size: 972 KiB |
|
After Width: | Height: | Size: 122 KiB |
|
After Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 104 KiB |
|
After Width: | Height: | Size: 10 KiB |
|
After Width: | Height: | Size: 53 KiB |
|
After Width: | Height: | Size: 357 KiB |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="2880" zoomAndPan="magnify" viewBox="0 0 2160 719.999996" height="960" preserveAspectRatio="xMidYMid meet" version="1.0"><defs><filter x="0%" y="0%" width="100%" height="100%" id="4a5894a8fc"><feColorMatrix values="0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 0 0 0 1 0" color-interpolation-filters="sRGB"/></filter><filter x="0%" y="0%" width="100%" height="100%" id="f9928a4cb4"><feColorMatrix values="0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 0.2126 0.7152 0.0722 0 0" color-interpolation-filters="sRGB"/></filter><g/><clipPath id="cfc277b445"><path d="M 10.71875 1 L 261.511719 1 L 261.511719 407.753906 L 10.71875 407.753906 Z M 10.71875 1 " clip-rule="nonzero"/></clipPath><mask id="f4f1ffa525"><g filter="url(#4a5894a8fc)"><g filter="url(#f9928a4cb4)" transform="matrix(0.263399, 0, 0, 0.264939, 0.0292621, 0.809016)"><image x="0" y="0" width="1024" xlink:href="data:image/png;base64,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 truncated
|
||||
|
After Width: | Height: | Size: 972 KiB |
|
After Width: | Height: | Size: 15 KiB |
|
After Width: | Height: | Size: 1.5 KiB |
|
After Width: | Height: | Size: 104 KiB |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="2880" zoomAndPan="magnify" viewBox="0 0 2160 719.999996" height="960" preserveAspectRatio="xMidYMid meet" version="1.0"><defs><filter x="0%" y="0%" width="100%" height="100%" id="4a5894a8fc"><feColorMatrix values="0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 0 0 0 1 0" color-interpolation-filters="sRGB"/></filter><filter x="0%" y="0%" width="100%" height="100%" id="f9928a4cb4"><feColorMatrix values="0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 0.2126 0.7152 0.0722 0 0" color-interpolation-filters="sRGB"/></filter><g/><clipPath id="cfc277b445"><path d="M 10.71875 1 L 261.511719 1 L 261.511719 407.753906 L 10.71875 407.753906 Z M 10.71875 1 " clip-rule="nonzero"/></clipPath><mask id="f4f1ffa525"><g filter="url(#4a5894a8fc)"><g filter="url(#f9928a4cb4)" transform="matrix(0.263399, 0, 0, 0.264939, 0.0292621, 0.809016)"><image x="0" y="0" width="1024" xlink:href="data:image/png;base64,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 truncated
|
||||
|
After Width: | Height: | Size: 972 KiB |
@@ -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);
|
||||
@@ -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}
|
||||
@@ -0,0 +1,2 @@
|
||||
this.driver=this.driver||{};this.driver.js=function(D){"use strict";let F={};function z(e={}){F={animate:!0,allowClose:!0,overlayOpacity:.7,smoothScroll:!1,disableActiveInteraction:!1,showProgress:!1,stagePadding:10,stageRadius:5,popoverOffset:10,showButtons:["next","previous","close"],disableButtons:[],overlayColor:"#000",...e}}function a(e){return e?F[e]:F}function W(e,o,t,i){return(e/=i/2)<1?t/2*e*e+o:-t/2*(--e*(e-2)-1)+o}function q(e){const o='a[href]:not([disabled]), button:not([disabled]), textarea:not([disabled]), input[type="text"]:not([disabled]), input[type="radio"]:not([disabled]), input[type="checkbox"]:not([disabled]), select:not([disabled])';return e.flatMap(t=>{const i=t.matches(o),p=Array.from(t.querySelectorAll(o));return[...i?[t]:[],...p]}).filter(t=>getComputedStyle(t).pointerEvents!=="none"&&ae(t))}function V(e){if(!e||se(e))return;const o=a("smoothScroll");e.scrollIntoView({behavior:!o||re(e)?"auto":"smooth",inline:"center",block:"center"})}function re(e){if(!e||!e.parentElement)return;const o=e.parentElement;return o.scrollHeight>o.clientHeight}function se(e){const o=e.getBoundingClientRect();return o.top>=0&&o.left>=0&&o.bottom<=(window.innerHeight||document.documentElement.clientHeight)&&o.right<=(window.innerWidth||document.documentElement.clientWidth)}function ae(e){return!!(e.offsetWidth||e.offsetHeight||e.getClientRects().length)}let N={};function b(e,o){N[e]=o}function l(e){return e?N[e]:N}function K(){N={}}let E={};function O(e,o){E[e]=o}function _(e){var o;(o=E[e])==null||o.call(E)}function ce(){E={}}function le(e,o,t,i){let p=l("__activeStagePosition");const n=p||t.getBoundingClientRect(),f=i.getBoundingClientRect(),w=W(e,n.x,f.x-n.x,o),r=W(e,n.y,f.y-n.y,o),v=W(e,n.width,f.width-n.width,o),s=W(e,n.height,f.height-n.height,o);p={x:w,y:r,width:v,height:s},Y(p),b("__activeStagePosition",p)}function X(e){if(!e)return;const o=e.getBoundingClientRect(),t={x:o.x,y:o.y,width:o.width,height:o.height};b("__activeStagePosition",t),Y(t)}function de(){const e=l("__activeStagePosition"),o=l("__overlaySvg");if(!e)return;if(!o){console.warn("No stage svg found.");return}const t=window.innerWidth,i=window.innerHeight;o.setAttribute("viewBox",`0 0 ${t} ${i}`)}function pe(e){const o=ue(e);document.body.appendChild(o),G(o,t=>{t.target.tagName==="path"&&_("overlayClick")}),b("__overlaySvg",o)}function Y(e){const o=l("__overlaySvg");if(!o){pe(e);return}const t=o.firstElementChild;if((t==null?void 0:t.tagName)!=="path")throw new Error("no path element found in stage svg");t.setAttribute("d",j(e))}function ue(e){const o=window.innerWidth,t=window.innerHeight,i=document.createElementNS("http://www.w3.org/2000/svg","svg");i.classList.add("driver-overlay","driver-overlay-animated"),i.setAttribute("viewBox",`0 0 ${o} ${t}`),i.setAttribute("xmlSpace","preserve"),i.setAttribute("xmlnsXlink","http://www.w3.org/1999/xlink"),i.setAttribute("version","1.1"),i.setAttribute("preserveAspectRatio","xMinYMin slice"),i.style.fillRule="evenodd",i.style.clipRule="evenodd",i.style.strokeLinejoin="round",i.style.strokeMiterlimit="2",i.style.zIndex="10000",i.style.position="fixed",i.style.top="0",i.style.left="0",i.style.width="100%",i.style.height="100%";const p=document.createElementNS("http://www.w3.org/2000/svg","path");return p.setAttribute("d",j(e)),p.style.fill=a("overlayColor")||"rgb(0,0,0)",p.style.opacity=`${a("overlayOpacity")}`,p.style.pointerEvents="auto",p.style.cursor="auto",i.appendChild(p),i}function j(e){const o=window.innerWidth,t=window.innerHeight,i=a("stagePadding")||0,p=a("stageRadius")||0,n=e.width+i*2,f=e.height+i*2,w=Math.min(p,n/2,f/2),r=Math.floor(Math.max(w,0)),v=e.x-i+r,s=e.y-i,c=n-r*2,d=f-r*2;return`M${o},0L0,0L0,${t}L${o},${t}L${o},0Z
|
||||
M${v},${s} h${c} a${r},${r} 0 0 1 ${r},${r} v${d} a${r},${r} 0 0 1 -${r},${r} h-${c} a${r},${r} 0 0 1 -${r},-${r} v-${d} a${r},${r} 0 0 1 ${r},-${r} z`}function ve(){const e=l("__overlaySvg");e&&e.remove()}function fe(){const e=document.getElementById("driver-dummy-element");if(e)return e;let o=document.createElement("div");return o.id="driver-dummy-element",o.style.width="0",o.style.height="0",o.style.pointerEvents="none",o.style.opacity="0",o.style.position="fixed",o.style.top="50%",o.style.left="50%",document.body.appendChild(o),o}function Q(e){const{element:o}=e;let t=typeof o=="string"?document.querySelector(o):o;t||(t=fe()),ge(t,e)}function he(){const e=l("__activeElement"),o=l("__activeStep");e&&(X(e),de(),oe(e,o))}function ge(e,o){const i=Date.now(),p=l("__activeStep"),n=l("__activeElement")||e,f=!n||n===e,w=e.id==="driver-dummy-element",r=n.id==="driver-dummy-element",v=a("animate"),s=o.onHighlightStarted||a("onHighlightStarted"),c=(o==null?void 0:o.onHighlighted)||a("onHighlighted"),d=(p==null?void 0:p.onDeselected)||a("onDeselected"),m=a(),g=l();!f&&d&&d(r?void 0:n,p,{config:m,state:g}),s&&s(w?void 0:e,o,{config:m,state:g});const u=!f&&v;let h=!1;xe(),b("previousStep",p),b("previousElement",n),b("activeStep",o),b("activeElement",e);const P=()=>{if(l("__transitionCallback")!==P)return;const x=Date.now()-i,y=400-x<=400/2;o.popover&&y&&!h&&u&&(J(e,o),h=!0),a("animate")&&x<400?le(x,400,n,e):(X(e),c&&c(w?void 0:e,o,{config:a(),state:l()}),b("__transitionCallback",void 0),b("__previousStep",p),b("__previousElement",n),b("__activeStep",o),b("__activeElement",e)),window.requestAnimationFrame(P)};b("__transitionCallback",P),window.requestAnimationFrame(P),V(e),!u&&o.popover&&J(e,o),n.classList.remove("driver-active-element","driver-no-interaction"),n.removeAttribute("aria-haspopup"),n.removeAttribute("aria-expanded"),n.removeAttribute("aria-controls"),a("disableActiveInteraction")&&e.classList.add("driver-no-interaction"),e.classList.add("driver-active-element"),e.setAttribute("aria-haspopup","dialog"),e.setAttribute("aria-expanded","true"),e.setAttribute("aria-controls","driver-popover-content")}function we(){var e;(e=document.getElementById("driver-dummy-element"))==null||e.remove(),document.querySelectorAll(".driver-active-element").forEach(o=>{o.classList.remove("driver-active-element","driver-no-interaction"),o.removeAttribute("aria-haspopup"),o.removeAttribute("aria-expanded"),o.removeAttribute("aria-controls")})}function A(){const e=l("__resizeTimeout");e&&window.cancelAnimationFrame(e),b("__resizeTimeout",window.requestAnimationFrame(he))}function me(e){var r;if(!l("isInitialized")||!(e.key==="Tab"||e.keyCode===9))return;const i=l("__activeElement"),p=(r=l("popover"))==null?void 0:r.wrapper,n=q([...p?[p]:[],...i?[i]:[]]),f=n[0],w=n[n.length-1];if(e.preventDefault(),e.shiftKey){const v=n[n.indexOf(document.activeElement)-1]||w;v==null||v.focus()}else{const v=n[n.indexOf(document.activeElement)+1]||f;v==null||v.focus()}}function Z(e){var t;((t=a("allowKeyboardControl"))==null||t)&&(e.key==="Escape"?_("escapePress"):e.key==="ArrowRight"?_("arrowRightPress"):e.key==="ArrowLeft"&&_("arrowLeftPress"))}function G(e,o,t){const i=(n,f)=>{const w=n.target;e.contains(w)&&((!t||t(w))&&(n.preventDefault(),n.stopPropagation(),n.stopImmediatePropagation()),f==null||f(n))};document.addEventListener("pointerdown",i,!0),document.addEventListener("mousedown",i,!0),document.addEventListener("pointerup",i,!0),document.addEventListener("mouseup",i,!0),document.addEventListener("click",n=>{i(n,o)},!0)}function ye(){window.addEventListener("keyup",Z,!1),window.addEventListener("keydown",me,!1),window.addEventListener("resize",A),window.addEventListener("scroll",A)}function be(){window.removeEventListener("keyup",Z),window.removeEventListener("resize",A),window.removeEventListener("scroll",A)}function xe(){const e=l("popover");e&&(e.wrapper.style.display="none")}function J(e,o){var C,y;let t=l("popover");t&&document.body.removeChild(t.wrapper),t=Pe(),document.body.appendChild(t.wrapper);const{title:i,description:p,showButtons:n,disableButtons:f,showProgress:w,nextBtnText:r=a("nextBtnText")||"Next →",prevBtnText:v=a("prevBtnText")||"← Previous",progressText:s=a("progressText")||"{current} of {total}"}=o.popover||{};t.nextButton.innerHTML=r,t.previousButton.innerHTML=v,t.progress.innerHTML=s,i?(t.title.innerHTML=i,t.title.style.display="block"):t.title.style.display="none",p?(t.description.innerHTML=p,t.description.style.display="block"):t.description.style.display="none";const c=n||a("showButtons"),d=w||a("showProgress")||!1,m=(c==null?void 0:c.includes("next"))||(c==null?void 0:c.includes("previous"))||d;t.closeButton.style.display=c.includes("close")?"block":"none",m?(t.footer.style.display="flex",t.progress.style.display=d?"block":"none",t.nextButton.style.display=c.includes("next")?"block":"none",t.previousButton.style.display=c.includes("previous")?"block":"none"):t.footer.style.display="none";const g=f
Line truncated
|
||||
|
After Width: | Height: | Size: 9.0 KiB |
@@ -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);
|
||||
|
After Width: | Height: | Size: 15 KiB |
|
After Width: | Height: | Size: 1.5 KiB |
@@ -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 |
|
After Width: | Height: | Size: 25 KiB |
@@ -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;
|
||||
}
|
||||
|
After Width: | Height: | Size: 13 KiB |
|
After Width: | Height: | Size: 53 KiB |
|
After Width: | Height: | Size: 45 KiB |
|
After Width: | Height: | Size: 13 KiB |
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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');
|
||||
|
||||
})();
|
||||
|
After Width: | Height: | Size: 122 KiB |
|
After Width: | Height: | Size: 10 KiB |
|
After Width: | Height: | Size: 710 B |
@@ -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);
|
||||
|
After Width: | Height: | Size: 98 KiB |
|
After Width: | Height: | Size: 135 KiB |
|
After Width: | Height: | Size: 18 KiB |
|
After Width: | Height: | Size: 41 KiB |
|
After Width: | Height: | Size: 77 KiB |
|
After Width: | Height: | Size: 163 KiB |
@@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
After Width: | Height: | Size: 13 KiB |
|
After Width: | Height: | Size: 27 KiB |