fix(build): frontend build determinism across line endings (DC-119) [glm-grade=A]
The deploy host showed phantom dist drift on every git pull: committed
dist bundles could not be reproduced on DNS2. Root cause (verified with
esbuild 0.25.12 probes): the production transform uses sourcemap:'both',
whose inline map base64-embeds RAW source bytes as sourcesContent — a CRLF
working copy (Windows dev, core.autocrlf=true) vs an LF checkout produces
different dist bytes and a different sw.js cache tag.
- build.js: normalizeSource (\r\n -> \n) on every source read (bundle
inputs + sw.js read); exported + require.main guard so tests can import
it without triggering a build
- .gitattributes: * text=auto eol=lf (git-layer kill of the CRLF vector)
+ binary exclusions; 9 CRLF-in-index asset files renormalized
- tests/build-determinism.test.js: 3-case pin (byte-identity after
normalization, divergence pre-normalization, CR-strip contract) importing
the ACTUAL normalizeSource from build.js
- package.json: declare jsdom devDependency — 2 committed test files
require('jsdom') but it was never declared, so fresh-checkout
npm test failed (it only passed where a stray ancestor node_modules
happened to contain it)
- dist/features.js + sw.js: canonical deterministic rebuild
(cache tag 3354f5fd96 -> d39ab69dd4)
Verified: status 54/54, API 2858/2858 (128 suites); CRLF-sim tree and LF
tree of same HEAD produce byte-identical dist artifacts (sha256-equal).
Judge: glm-5.3 cold round 1 = A, round 2 (post-fold) = A clean, 0 blocking.
This commit is contained in:
@@ -0,0 +1,19 @@
|
|||||||
|
# DC-119: normalize text file line endings at the git layer.
|
||||||
|
# The frontend build is byte-sensitive to CRLF (esbuild inline sourcemap
|
||||||
|
# embeds raw source bytes — see status/build.js DC-119 comment), and the
|
||||||
|
# Windows dev tree runs core.autocrlf=true while DNS2 checks out LF.
|
||||||
|
# eol=lf forces LF working copies for text files on ALL platforms, killing
|
||||||
|
# the phantom dist drift at the source. Binary types stay untouched.
|
||||||
|
* text=auto eol=lf
|
||||||
|
*.png binary
|
||||||
|
*.jpg binary
|
||||||
|
*.ico binary
|
||||||
|
*.woff binary
|
||||||
|
*.woff2 binary
|
||||||
|
*.ttf binary
|
||||||
|
*.eot binary
|
||||||
|
*.webp binary
|
||||||
|
*.gif binary
|
||||||
|
*.mp4 binary
|
||||||
|
*.zip binary
|
||||||
|
*.gz binary
|
||||||
@@ -1,21 +1,21 @@
|
|||||||
# Font file headers to prevent sanitizer issues
|
# Font file headers to prevent sanitizer issues
|
||||||
<FilesMatch "\.(woff2|woff|ttf|eot)$">
|
<FilesMatch "\.(woff2|woff|ttf|eot)$">
|
||||||
Header set Access-Control-Allow-Origin "*"
|
Header set Access-Control-Allow-Origin "*"
|
||||||
Header set Access-Control-Allow-Methods "GET, POST, OPTIONS"
|
Header set Access-Control-Allow-Methods "GET, POST, OPTIONS"
|
||||||
Header set Access-Control-Allow-Headers "Content-Type"
|
Header set Access-Control-Allow-Headers "Content-Type"
|
||||||
Header set Cache-Control "public, max-age=31536000"
|
Header set Cache-Control "public, max-age=31536000"
|
||||||
|
|
||||||
# Proper MIME types
|
# Proper MIME types
|
||||||
<IfModule mod_mime.c>
|
<IfModule mod_mime.c>
|
||||||
AddType font/woff2 .woff2
|
AddType font/woff2 .woff2
|
||||||
AddType font/woff .woff
|
AddType font/woff .woff
|
||||||
AddType font/ttf .ttf
|
AddType font/ttf .ttf
|
||||||
AddType application/vnd.ms-fontobject .eot
|
AddType application/vnd.ms-fontobject .eot
|
||||||
</IfModule>
|
</IfModule>
|
||||||
</FilesMatch>
|
</FilesMatch>
|
||||||
|
|
||||||
# Prevent direct access to font conversion scripts
|
# Prevent direct access to font conversion scripts
|
||||||
<FilesMatch "\.(py|bat)$">
|
<FilesMatch "\.(py|bat)$">
|
||||||
Order allow,deny
|
Order allow,deny
|
||||||
Deny from all
|
Deny from all
|
||||||
</FilesMatch>
|
</FilesMatch>
|
||||||
@@ -1,321 +1,321 @@
|
|||||||
/**
|
/**
|
||||||
* DNS Template Selector
|
* DNS Template Selector
|
||||||
* Presents DNS server template options when user chooses to set up DNS
|
* Presents DNS server template options when user chooses to set up DNS
|
||||||
*/
|
*/
|
||||||
|
|
||||||
(function(window) {
|
(function(window) {
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
class DnsTemplateSelector {
|
class DnsTemplateSelector {
|
||||||
constructor(progressTracker) {
|
constructor(progressTracker) {
|
||||||
this.progressTracker = progressTracker;
|
this.progressTracker = progressTracker;
|
||||||
this.modal = null;
|
this.modal = null;
|
||||||
this.onTemplateSelected = null;
|
this.onTemplateSelected = null;
|
||||||
console.log('[DnsTemplateSelector] Module loaded');
|
console.log('[DnsTemplateSelector] Module loaded');
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get available DNS server templates from app templates
|
* Get available DNS server templates from app templates
|
||||||
* @returns {Array} Array of DNS template objects
|
* @returns {Array} Array of DNS template objects
|
||||||
*/
|
*/
|
||||||
getDnsTemplates() {
|
getDnsTemplates() {
|
||||||
// In a real implementation, this would fetch from app-templates.js
|
// In a real implementation, this would fetch from app-templates.js
|
||||||
// For now, return hardcoded templates matching what we added
|
// For now, return hardcoded templates matching what we added
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
id: 'technitium',
|
id: 'technitium',
|
||||||
name: 'Technitium DNS Server',
|
name: 'Technitium DNS Server',
|
||||||
description: 'Modern DNS server with web UI for managing private zones',
|
description: 'Modern DNS server with web UI for managing private zones',
|
||||||
icon: '🌐',
|
icon: '🌐',
|
||||||
difficulty: 'Easy',
|
difficulty: 'Easy',
|
||||||
features: [
|
features: [
|
||||||
'Web-based management interface',
|
'Web-based management interface',
|
||||||
'Private zone management for .sami domain',
|
'Private zone management for .sami domain',
|
||||||
'DHCP server integration',
|
'DHCP server integration',
|
||||||
'DNS-over-HTTPS and DNS-over-TLS support'
|
'DNS-over-HTTPS and DNS-over-TLS support'
|
||||||
],
|
],
|
||||||
recommended: true
|
recommended: true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'bind9',
|
id: 'bind9',
|
||||||
name: 'BIND9 DNS Server',
|
name: 'BIND9 DNS Server',
|
||||||
description: 'Industry-standard DNS server - powerful and flexible',
|
description: 'Industry-standard DNS server - powerful and flexible',
|
||||||
icon: '🔧',
|
icon: '🔧',
|
||||||
difficulty: 'Advanced',
|
difficulty: 'Advanced',
|
||||||
features: [
|
features: [
|
||||||
'Industry standard DNS server',
|
'Industry standard DNS server',
|
||||||
'Full RFC compliance',
|
'Full RFC compliance',
|
||||||
'Advanced zone management',
|
'Advanced zone management',
|
||||||
'DNSSEC support'
|
'DNSSEC support'
|
||||||
],
|
],
|
||||||
recommended: false
|
recommended: false
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'pihole',
|
id: 'pihole',
|
||||||
name: 'Pi-hole',
|
name: 'Pi-hole',
|
||||||
description: 'Network-wide ad blocker with DNS capabilities',
|
description: 'Network-wide ad blocker with DNS capabilities',
|
||||||
icon: '🛡️',
|
icon: '🛡️',
|
||||||
difficulty: 'Intermediate',
|
difficulty: 'Intermediate',
|
||||||
features: [
|
features: [
|
||||||
'Ad blocking at DNS level',
|
'Ad blocking at DNS level',
|
||||||
'Web interface for management',
|
'Web interface for management',
|
||||||
'DHCP server included',
|
'DHCP server included',
|
||||||
'Query logging and statistics'
|
'Query logging and statistics'
|
||||||
],
|
],
|
||||||
recommended: false
|
recommended: false
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'powerdns',
|
id: 'powerdns',
|
||||||
name: 'PowerDNS',
|
name: 'PowerDNS',
|
||||||
description: 'High-performance DNS server with SQL backend',
|
description: 'High-performance DNS server with SQL backend',
|
||||||
icon: '⚡',
|
icon: '⚡',
|
||||||
difficulty: 'Intermediate',
|
difficulty: 'Intermediate',
|
||||||
features: [
|
features: [
|
||||||
'SQL database backend',
|
'SQL database backend',
|
||||||
'RESTful API for automation',
|
'RESTful API for automation',
|
||||||
'Geographic load balancing',
|
'Geographic load balancing',
|
||||||
'DNSSEC support'
|
'DNSSEC support'
|
||||||
],
|
],
|
||||||
recommended: false
|
recommended: false
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'coredns',
|
id: 'coredns',
|
||||||
name: 'CoreDNS',
|
name: 'CoreDNS',
|
||||||
description: 'Cloud-native DNS server - lightweight and flexible',
|
description: 'Cloud-native DNS server - lightweight and flexible',
|
||||||
icon: '☁️',
|
icon: '☁️',
|
||||||
difficulty: 'Intermediate',
|
difficulty: 'Intermediate',
|
||||||
features: [
|
features: [
|
||||||
'Plugin-based architecture',
|
'Plugin-based architecture',
|
||||||
'Kubernetes-native',
|
'Kubernetes-native',
|
||||||
'Lightweight and fast',
|
'Lightweight and fast',
|
||||||
'Prometheus metrics'
|
'Prometheus metrics'
|
||||||
],
|
],
|
||||||
recommended: false
|
recommended: false
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Show DNS template selection modal
|
* Show DNS template selection modal
|
||||||
*/
|
*/
|
||||||
showTemplateSelector() {
|
showTemplateSelector() {
|
||||||
// Create modal if it doesn't exist
|
// Create modal if it doesn't exist
|
||||||
if (!this.modal) {
|
if (!this.modal) {
|
||||||
this.createModal();
|
this.createModal();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Populate with templates
|
// Populate with templates
|
||||||
this.populateTemplates();
|
this.populateTemplates();
|
||||||
|
|
||||||
// Show modal
|
// Show modal
|
||||||
this.modal.style.display = 'flex';
|
this.modal.style.display = 'flex';
|
||||||
document.body.style.overflow = 'hidden';
|
document.body.style.overflow = 'hidden';
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create the modal HTML structure
|
* Create the modal HTML structure
|
||||||
* @private
|
* @private
|
||||||
*/
|
*/
|
||||||
createModal() {
|
createModal() {
|
||||||
const modal = document.createElement('div');
|
const modal = document.createElement('div');
|
||||||
modal.id = 'dns-template-modal';
|
modal.id = 'dns-template-modal';
|
||||||
modal.className = 'dns-template-modal';
|
modal.className = 'dns-template-modal';
|
||||||
modal.innerHTML = `
|
modal.innerHTML = `
|
||||||
<div class="dns-template-modal-content">
|
<div class="dns-template-modal-content">
|
||||||
<div class="dns-template-header">
|
<div class="dns-template-header">
|
||||||
<h2>🌐 Choose a DNS Server</h2>
|
<h2>🌐 Choose a DNS Server</h2>
|
||||||
<p>Setting up a DNS server is essential for managing your private .sami domain</p>
|
<p>Setting up a DNS server is essential for managing your private .sami domain</p>
|
||||||
<button class="dns-template-close" aria-label="Close">×</button>
|
<button class="dns-template-close" aria-label="Close">×</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="dns-template-grid" id="dns-template-grid">
|
<div class="dns-template-grid" id="dns-template-grid">
|
||||||
<!-- Templates will be inserted here -->
|
<!-- Templates will be inserted here -->
|
||||||
</div>
|
</div>
|
||||||
<div class="dns-template-footer">
|
<div class="dns-template-footer">
|
||||||
<button class="dns-template-later-btn" id="dns-setup-later">Set up later</button>
|
<button class="dns-template-later-btn" id="dns-setup-later">Set up later</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
document.body.appendChild(modal);
|
document.body.appendChild(modal);
|
||||||
this.modal = modal;
|
this.modal = modal;
|
||||||
|
|
||||||
// Add event listeners
|
// Add event listeners
|
||||||
modal.querySelector('.dns-template-close').addEventListener('click', () => this.close());
|
modal.querySelector('.dns-template-close').addEventListener('click', () => this.close());
|
||||||
modal.querySelector('#dns-setup-later').addEventListener('click', () => this.handleSetupLater());
|
modal.querySelector('#dns-setup-later').addEventListener('click', () => this.handleSetupLater());
|
||||||
|
|
||||||
// Close on overlay click
|
// Close on overlay click
|
||||||
modal.addEventListener('click', (e) => {
|
modal.addEventListener('click', (e) => {
|
||||||
if (e.target === modal) {
|
if (e.target === modal) {
|
||||||
this.close();
|
this.close();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Close on Escape key
|
// Close on Escape key
|
||||||
document.addEventListener('keydown', (e) => {
|
document.addEventListener('keydown', (e) => {
|
||||||
if (e.key === 'Escape' && modal.style.display === 'flex') {
|
if (e.key === 'Escape' && modal.style.display === 'flex') {
|
||||||
this.close();
|
this.close();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Populate modal with DNS templates
|
* Populate modal with DNS templates
|
||||||
* @private
|
* @private
|
||||||
*/
|
*/
|
||||||
populateTemplates() {
|
populateTemplates() {
|
||||||
const grid = document.getElementById('dns-template-grid');
|
const grid = document.getElementById('dns-template-grid');
|
||||||
if (!grid) return;
|
if (!grid) return;
|
||||||
|
|
||||||
const templates = this.getDnsTemplates();
|
const templates = this.getDnsTemplates();
|
||||||
grid.innerHTML = '';
|
grid.innerHTML = '';
|
||||||
|
|
||||||
templates.forEach(template => {
|
templates.forEach(template => {
|
||||||
const card = this.createTemplateCard(template);
|
const card = this.createTemplateCard(template);
|
||||||
grid.appendChild(card);
|
grid.appendChild(card);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create a template card element
|
* Create a template card element
|
||||||
* @private
|
* @private
|
||||||
*/
|
*/
|
||||||
createTemplateCard(template) {
|
createTemplateCard(template) {
|
||||||
const card = document.createElement('div');
|
const card = document.createElement('div');
|
||||||
card.className = 'dns-template-card';
|
card.className = 'dns-template-card';
|
||||||
if (template.recommended) {
|
if (template.recommended) {
|
||||||
card.classList.add('recommended');
|
card.classList.add('recommended');
|
||||||
}
|
}
|
||||||
|
|
||||||
const difficultyClass = template.difficulty.toLowerCase();
|
const difficultyClass = template.difficulty.toLowerCase();
|
||||||
|
|
||||||
card.innerHTML = `
|
card.innerHTML = `
|
||||||
${template.recommended ? '<div class="recommended-badge">Recommended</div>' : ''}
|
${template.recommended ? '<div class="recommended-badge">Recommended</div>' : ''}
|
||||||
<div class="dns-template-icon">${template.icon}</div>
|
<div class="dns-template-icon">${template.icon}</div>
|
||||||
<h3>${template.name}</h3>
|
<h3>${template.name}</h3>
|
||||||
<p class="dns-template-description">${template.description}</p>
|
<p class="dns-template-description">${template.description}</p>
|
||||||
<div class="dns-template-difficulty difficulty-${difficultyClass}">
|
<div class="dns-template-difficulty difficulty-${difficultyClass}">
|
||||||
${template.difficulty}
|
${template.difficulty}
|
||||||
</div>
|
</div>
|
||||||
<ul class="dns-template-features">
|
<ul class="dns-template-features">
|
||||||
${template.features.slice(0, 3).map(f => `<li>${f}</li>`).join('')}
|
${template.features.slice(0, 3).map(f => `<li>${f}</li>`).join('')}
|
||||||
</ul>
|
</ul>
|
||||||
<button class="dns-template-select-btn" data-template-id="${template.id}">
|
<button class="dns-template-select-btn" data-template-id="${template.id}">
|
||||||
Select ${template.name}
|
Select ${template.name}
|
||||||
</button>
|
</button>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
// Add click handler to select button
|
// Add click handler to select button
|
||||||
const selectBtn = card.querySelector('.dns-template-select-btn');
|
const selectBtn = card.querySelector('.dns-template-select-btn');
|
||||||
selectBtn.addEventListener('click', () => this.handleTemplateSelection(template));
|
selectBtn.addEventListener('click', () => this.handleTemplateSelection(template));
|
||||||
|
|
||||||
return card;
|
return card;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handle template selection
|
* Handle template selection
|
||||||
* @private
|
* @private
|
||||||
*/
|
*/
|
||||||
handleTemplateSelection(template) {
|
handleTemplateSelection(template) {
|
||||||
console.log(`[DnsTemplateSelector] Template selected: ${template.id}`);
|
console.log(`[DnsTemplateSelector] Template selected: ${template.id}`);
|
||||||
|
|
||||||
// Close modal
|
// Close modal
|
||||||
this.close();
|
this.close();
|
||||||
|
|
||||||
// Trigger callback if set
|
// Trigger callback if set
|
||||||
if (this.onTemplateSelected) {
|
if (this.onTemplateSelected) {
|
||||||
this.onTemplateSelected(template);
|
this.onTemplateSelected(template);
|
||||||
} else {
|
} else {
|
||||||
// Default behavior: open app selector with DNS filter
|
// Default behavior: open app selector with DNS filter
|
||||||
this.openAppSelector(template.id);
|
this.openAppSelector(template.id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handle "Set up later" button
|
* Handle "Set up later" button
|
||||||
* @private
|
* @private
|
||||||
*/
|
*/
|
||||||
handleSetupLater() {
|
handleSetupLater() {
|
||||||
console.log('[DnsTemplateSelector] DNS setup deferred');
|
console.log('[DnsTemplateSelector] DNS setup deferred');
|
||||||
|
|
||||||
// Mark as deferred in progress tracker
|
// Mark as deferred in progress tracker
|
||||||
if (this.progressTracker) {
|
if (this.progressTracker) {
|
||||||
this.progressTracker.markDnsSetupDeferred();
|
this.progressTracker.markDnsSetupDeferred();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Close modal
|
// Close modal
|
||||||
this.close();
|
this.close();
|
||||||
|
|
||||||
// Show notification
|
// Show notification
|
||||||
this.showNotification('DNS setup deferred. You can set it up later from the App Selector.');
|
this.showNotification('DNS setup deferred. You can set it up later from the App Selector.');
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Open app selector with specific template
|
* Open app selector with specific template
|
||||||
* @private
|
* @private
|
||||||
*/
|
*/
|
||||||
openAppSelector(templateId) {
|
openAppSelector(templateId) {
|
||||||
// Try to open the app selector modal if it exists
|
// Try to open the app selector modal if it exists
|
||||||
const appSelectorBtn = document.querySelector('[onclick*="showAppSelector"]');
|
const appSelectorBtn = document.querySelector('[onclick*="showAppSelector"]');
|
||||||
if (appSelectorBtn) {
|
if (appSelectorBtn) {
|
||||||
appSelectorBtn.click();
|
appSelectorBtn.click();
|
||||||
|
|
||||||
// Wait a bit then filter to the selected template
|
// Wait a bit then filter to the selected template
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
const searchInput = document.querySelector('#app-search');
|
const searchInput = document.querySelector('#app-search');
|
||||||
if (searchInput) {
|
if (searchInput) {
|
||||||
searchInput.value = templateId;
|
searchInput.value = templateId;
|
||||||
searchInput.dispatchEvent(new Event('input', { bubbles: true }));
|
searchInput.dispatchEvent(new Event('input', { bubbles: true }));
|
||||||
}
|
}
|
||||||
}, 300);
|
}, 300);
|
||||||
} else {
|
} else {
|
||||||
// Fallback: show instructions
|
// Fallback: show instructions
|
||||||
this.showNotification(`To deploy ${templateId}, use the App Selector and search for "${templateId}"`);
|
this.showNotification(`To deploy ${templateId}, use the App Selector and search for "${templateId}"`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Show notification message
|
* Show notification message
|
||||||
* @private
|
* @private
|
||||||
*/
|
*/
|
||||||
showNotification(message) {
|
showNotification(message) {
|
||||||
// Simple notification - could be enhanced
|
// Simple notification - could be enhanced
|
||||||
const notification = document.createElement('div');
|
const notification = document.createElement('div');
|
||||||
notification.className = 'dns-template-notification';
|
notification.className = 'dns-template-notification';
|
||||||
notification.textContent = message;
|
notification.textContent = message;
|
||||||
notification.style.cssText = `
|
notification.style.cssText = `
|
||||||
position: fixed;
|
position: fixed;
|
||||||
top: 20px;
|
top: 20px;
|
||||||
right: 20px;
|
right: 20px;
|
||||||
background: var(--card-base);
|
background: var(--card-base);
|
||||||
color: var(--fg);
|
color: var(--fg);
|
||||||
padding: 15px 20px;
|
padding: 15px 20px;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
box-shadow: 0 4px 12px rgba(0,0,0,0.3);
|
box-shadow: 0 4px 12px rgba(0,0,0,0.3);
|
||||||
z-index: 10001;
|
z-index: 10001;
|
||||||
max-width: 300px;
|
max-width: 300px;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
document.body.appendChild(notification);
|
document.body.appendChild(notification);
|
||||||
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
notification.style.opacity = '0';
|
notification.style.opacity = '0';
|
||||||
notification.style.transition = 'opacity 0.3s';
|
notification.style.transition = 'opacity 0.3s';
|
||||||
setTimeout(() => notification.remove(), 300);
|
setTimeout(() => notification.remove(), 300);
|
||||||
}, 3000);
|
}, 3000);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Close the modal
|
* Close the modal
|
||||||
*/
|
*/
|
||||||
close() {
|
close() {
|
||||||
if (this.modal) {
|
if (this.modal) {
|
||||||
this.modal.style.display = 'none';
|
this.modal.style.display = 'none';
|
||||||
document.body.style.overflow = '';
|
document.body.style.overflow = '';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
window.DnsTemplateSelector = DnsTemplateSelector;
|
window.DnsTemplateSelector = DnsTemplateSelector;
|
||||||
console.log('[DnsTemplateSelector] Module loaded');
|
console.log('[DnsTemplateSelector] Module loaded');
|
||||||
|
|
||||||
})(window);
|
})(window);
|
||||||
|
|||||||
@@ -1,259 +1,259 @@
|
|||||||
/**
|
/**
|
||||||
* Error Handler
|
* Error Handler
|
||||||
* Handles errors gracefully without breaking the onboarding tour
|
* Handles errors gracefully without breaking the onboarding tour
|
||||||
*/
|
*/
|
||||||
|
|
||||||
(function(window) {
|
(function(window) {
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
class ErrorHandler {
|
class ErrorHandler {
|
||||||
constructor() {
|
constructor() {
|
||||||
this.errors = [];
|
this.errors = [];
|
||||||
this.maxErrors = 50; // Keep last 50 errors
|
this.maxErrors = 50; // Keep last 50 errors
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Log an error without breaking the tour
|
* Log an error without breaking the tour
|
||||||
* @param {string} context - Context where error occurred
|
* @param {string} context - Context where error occurred
|
||||||
* @param {Error|string} error - The error object or message
|
* @param {Error|string} error - The error object or message
|
||||||
* @param {Object} metadata - Additional metadata
|
* @param {Object} metadata - Additional metadata
|
||||||
*/
|
*/
|
||||||
logError(context, error, metadata = {}) {
|
logError(context, error, metadata = {}) {
|
||||||
const errorEntry = {
|
const errorEntry = {
|
||||||
timestamp: new Date().toISOString(),
|
timestamp: new Date().toISOString(),
|
||||||
context,
|
context,
|
||||||
message: error instanceof Error ? error.message : error,
|
message: error instanceof Error ? error.message : error,
|
||||||
stack: error instanceof Error ? error.stack : null,
|
stack: error instanceof Error ? error.stack : null,
|
||||||
metadata
|
metadata
|
||||||
};
|
};
|
||||||
|
|
||||||
// Add to errors array
|
// Add to errors array
|
||||||
this.errors.push(errorEntry);
|
this.errors.push(errorEntry);
|
||||||
|
|
||||||
// Keep only last maxErrors
|
// Keep only last maxErrors
|
||||||
if (this.errors.length > this.maxErrors) {
|
if (this.errors.length > this.maxErrors) {
|
||||||
this.errors.shift();
|
this.errors.shift();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Log to console
|
// Log to console
|
||||||
console.error(`[Onboarding Error] ${context}:`, error, metadata);
|
console.error(`[Onboarding Error] ${context}:`, error, metadata);
|
||||||
|
|
||||||
// Optionally send to error tracking service
|
// Optionally send to error tracking service
|
||||||
// this.sendToErrorTracking(errorEntry);
|
// this.sendToErrorTracking(errorEntry);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Attempt to recover from an error and continue tour
|
* Attempt to recover from an error and continue tour
|
||||||
* @param {Error} error - The error object
|
* @param {Error} error - The error object
|
||||||
* @param {number} currentStep - Current step index
|
* @param {number} currentStep - Current step index
|
||||||
* @returns {Object} Recovery action
|
* @returns {Object} Recovery action
|
||||||
*/
|
*/
|
||||||
recoverFromError(error, currentStep) {
|
recoverFromError(error, currentStep) {
|
||||||
const errorType = this.classifyError(error);
|
const errorType = this.classifyError(error);
|
||||||
|
|
||||||
switch (errorType) {
|
switch (errorType) {
|
||||||
case 'ELEMENT_NOT_FOUND':
|
case 'ELEMENT_NOT_FOUND':
|
||||||
this.logError('Element Not Found', error, { currentStep });
|
this.logError('Element Not Found', error, { currentStep });
|
||||||
return {
|
return {
|
||||||
action: 'SKIP_STEP',
|
action: 'SKIP_STEP',
|
||||||
nextStep: currentStep + 1,
|
nextStep: currentStep + 1,
|
||||||
message: 'Target element not found, skipping to next step'
|
message: 'Target element not found, skipping to next step'
|
||||||
};
|
};
|
||||||
|
|
||||||
case 'STORAGE_UNAVAILABLE':
|
case 'STORAGE_UNAVAILABLE':
|
||||||
this.logError('Storage Unavailable', error);
|
this.logError('Storage Unavailable', error);
|
||||||
return {
|
return {
|
||||||
action: 'USE_MEMORY_STORAGE',
|
action: 'USE_MEMORY_STORAGE',
|
||||||
message: 'Local storage unavailable, using in-memory storage'
|
message: 'Local storage unavailable, using in-memory storage'
|
||||||
};
|
};
|
||||||
|
|
||||||
case 'DRIVER_NOT_LOADED':
|
case 'DRIVER_NOT_LOADED':
|
||||||
this.logError('Driver.js Not Loaded', error);
|
this.logError('Driver.js Not Loaded', error);
|
||||||
return {
|
return {
|
||||||
action: 'ABORT_TOUR',
|
action: 'ABORT_TOUR',
|
||||||
message: 'Driver.js library not loaded, cannot start tour'
|
message: 'Driver.js library not loaded, cannot start tour'
|
||||||
};
|
};
|
||||||
|
|
||||||
case 'INVALID_TOOLTIP':
|
case 'INVALID_TOOLTIP':
|
||||||
this.logError('Invalid Tooltip Configuration', error, { currentStep });
|
this.logError('Invalid Tooltip Configuration', error, { currentStep });
|
||||||
return {
|
return {
|
||||||
action: 'SKIP_STEP',
|
action: 'SKIP_STEP',
|
||||||
nextStep: currentStep + 1,
|
nextStep: currentStep + 1,
|
||||||
message: 'Invalid tooltip configuration, skipping'
|
message: 'Invalid tooltip configuration, skipping'
|
||||||
};
|
};
|
||||||
|
|
||||||
case 'THEME_DETECTION_FAILED':
|
case 'THEME_DETECTION_FAILED':
|
||||||
this.logError('Theme Detection Failed', error);
|
this.logError('Theme Detection Failed', error);
|
||||||
return {
|
return {
|
||||||
action: 'USE_DEFAULT_THEME',
|
action: 'USE_DEFAULT_THEME',
|
||||||
message: 'Using default dark theme'
|
message: 'Using default dark theme'
|
||||||
};
|
};
|
||||||
|
|
||||||
default:
|
default:
|
||||||
this.logError('Unknown Error', error, { currentStep });
|
this.logError('Unknown Error', error, { currentStep });
|
||||||
return {
|
return {
|
||||||
action: 'ABORT_TOUR',
|
action: 'ABORT_TOUR',
|
||||||
message: 'Unexpected error occurred, aborting tour'
|
message: 'Unexpected error occurred, aborting tour'
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Classify error type
|
* Classify error type
|
||||||
* @private
|
* @private
|
||||||
* @param {Error} error - The error object
|
* @param {Error} error - The error object
|
||||||
* @returns {string} Error type
|
* @returns {string} Error type
|
||||||
*/
|
*/
|
||||||
classifyError(error) {
|
classifyError(error) {
|
||||||
const message = error.message || error.toString();
|
const message = error.message || error.toString();
|
||||||
|
|
||||||
if (message.includes('element') && message.includes('not found')) {
|
if (message.includes('element') && message.includes('not found')) {
|
||||||
return 'ELEMENT_NOT_FOUND';
|
return 'ELEMENT_NOT_FOUND';
|
||||||
}
|
}
|
||||||
if (message.includes('storage') || message.includes('quota')) {
|
if (message.includes('storage') || message.includes('quota')) {
|
||||||
return 'STORAGE_UNAVAILABLE';
|
return 'STORAGE_UNAVAILABLE';
|
||||||
}
|
}
|
||||||
if (message.includes('driver') || message.includes('undefined')) {
|
if (message.includes('driver') || message.includes('undefined')) {
|
||||||
return 'DRIVER_NOT_LOADED';
|
return 'DRIVER_NOT_LOADED';
|
||||||
}
|
}
|
||||||
if (message.includes('invalid') || message.includes('validation')) {
|
if (message.includes('invalid') || message.includes('validation')) {
|
||||||
return 'INVALID_TOOLTIP';
|
return 'INVALID_TOOLTIP';
|
||||||
}
|
}
|
||||||
if (message.includes('theme')) {
|
if (message.includes('theme')) {
|
||||||
return 'THEME_DETECTION_FAILED';
|
return 'THEME_DETECTION_FAILED';
|
||||||
}
|
}
|
||||||
|
|
||||||
return 'UNKNOWN';
|
return 'UNKNOWN';
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get all logged errors
|
* Get all logged errors
|
||||||
* @returns {Array} Array of error entries
|
* @returns {Array} Array of error entries
|
||||||
*/
|
*/
|
||||||
getErrors() {
|
getErrors() {
|
||||||
return [...this.errors];
|
return [...this.errors];
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Clear all logged errors
|
* Clear all logged errors
|
||||||
*/
|
*/
|
||||||
clearErrors() {
|
clearErrors() {
|
||||||
this.errors = [];
|
this.errors = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get error statistics
|
* Get error statistics
|
||||||
* @returns {Object} Error statistics
|
* @returns {Object} Error statistics
|
||||||
*/
|
*/
|
||||||
getStatistics() {
|
getStatistics() {
|
||||||
const stats = {
|
const stats = {
|
||||||
total: this.errors.length,
|
total: this.errors.length,
|
||||||
byContext: {},
|
byContext: {},
|
||||||
byType: {},
|
byType: {},
|
||||||
recent: this.errors.slice(-10)
|
recent: this.errors.slice(-10)
|
||||||
};
|
};
|
||||||
|
|
||||||
this.errors.forEach(error => {
|
this.errors.forEach(error => {
|
||||||
// Count by context
|
// Count by context
|
||||||
stats.byContext[error.context] = (stats.byContext[error.context] || 0) + 1;
|
stats.byContext[error.context] = (stats.byContext[error.context] || 0) + 1;
|
||||||
|
|
||||||
// Count by type
|
// Count by type
|
||||||
const type = this.classifyError({ message: error.message });
|
const type = this.classifyError({ message: error.message });
|
||||||
stats.byType[type] = (stats.byType[type] || 0) + 1;
|
stats.byType[type] = (stats.byType[type] || 0) + 1;
|
||||||
});
|
});
|
||||||
|
|
||||||
return stats;
|
return stats;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handle graceful degradation when Driver.js fails to load
|
* Handle graceful degradation when Driver.js fails to load
|
||||||
* @returns {boolean} Whether fallback was successful
|
* @returns {boolean} Whether fallback was successful
|
||||||
*/
|
*/
|
||||||
handleDriverLoadFailure() {
|
handleDriverLoadFailure() {
|
||||||
this.logError('Driver.js Load Failure', 'Driver.js library failed to load');
|
this.logError('Driver.js Load Failure', 'Driver.js library failed to load');
|
||||||
|
|
||||||
// Show fallback message
|
// Show fallback message
|
||||||
const fallbackMessage = document.createElement('div');
|
const fallbackMessage = document.createElement('div');
|
||||||
fallbackMessage.id = 'onboarding-fallback';
|
fallbackMessage.id = 'onboarding-fallback';
|
||||||
fallbackMessage.style.cssText = `
|
fallbackMessage.style.cssText = `
|
||||||
position: fixed;
|
position: fixed;
|
||||||
bottom: 20px;
|
bottom: 20px;
|
||||||
right: 20px;
|
right: 20px;
|
||||||
background: var(--card-base, #2a2a2a);
|
background: var(--card-base, #2a2a2a);
|
||||||
color: var(--fg, #ffffff);
|
color: var(--fg, #ffffff);
|
||||||
padding: 15px 20px;
|
padding: 15px 20px;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
box-shadow: 0 4px 12px rgba(0,0,0,0.3);
|
box-shadow: 0 4px 12px rgba(0,0,0,0.3);
|
||||||
z-index: 9999;
|
z-index: 9999;
|
||||||
max-width: 300px;
|
max-width: 300px;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
`;
|
`;
|
||||||
fallbackMessage.innerHTML = `
|
fallbackMessage.innerHTML = `
|
||||||
<strong>Welcome to DashCaddy!</strong><br>
|
<strong>Welcome to DashCaddy!</strong><br>
|
||||||
<p style="margin: 10px 0 0 0; font-size: 12px;">
|
<p style="margin: 10px 0 0 0; font-size: 12px;">
|
||||||
The interactive tour is unavailable, but you can explore the dashboard freely.
|
The interactive tour is unavailable, but you can explore the dashboard freely.
|
||||||
Check the documentation for help getting started.
|
Check the documentation for help getting started.
|
||||||
</p>
|
</p>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
document.body.appendChild(fallbackMessage);
|
document.body.appendChild(fallbackMessage);
|
||||||
|
|
||||||
// Auto-remove after 10 seconds
|
// Auto-remove after 10 seconds
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
if (fallbackMessage.parentNode) {
|
if (fallbackMessage.parentNode) {
|
||||||
fallbackMessage.parentNode.removeChild(fallbackMessage);
|
fallbackMessage.parentNode.removeChild(fallbackMessage);
|
||||||
}
|
}
|
||||||
}, 10000);
|
}, 10000);
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handle storage unavailable scenario
|
* Handle storage unavailable scenario
|
||||||
* @returns {Object} In-memory storage fallback
|
* @returns {Object} In-memory storage fallback
|
||||||
*/
|
*/
|
||||||
handleStorageUnavailable() {
|
handleStorageUnavailable() {
|
||||||
this.logError('Storage Unavailable', 'Local storage is not available');
|
this.logError('Storage Unavailable', 'Local storage is not available');
|
||||||
|
|
||||||
// Create in-memory storage
|
// Create in-memory storage
|
||||||
const memoryStorage = {
|
const memoryStorage = {
|
||||||
data: {},
|
data: {},
|
||||||
getItem(key) {
|
getItem(key) {
|
||||||
return this.data[key] || null;
|
return this.data[key] || null;
|
||||||
},
|
},
|
||||||
setItem(key, value) {
|
setItem(key, value) {
|
||||||
this.data[key] = value;
|
this.data[key] = value;
|
||||||
},
|
},
|
||||||
removeItem(key) {
|
removeItem(key) {
|
||||||
delete this.data[key];
|
delete this.data[key];
|
||||||
},
|
},
|
||||||
clear() {
|
clear() {
|
||||||
this.data = {};
|
this.data = {};
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
console.warn('[ErrorHandler] Using in-memory storage - progress will not persist');
|
console.warn('[ErrorHandler] Using in-memory storage - progress will not persist');
|
||||||
return memoryStorage;
|
return memoryStorage;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Send error to tracking service (placeholder)
|
* Send error to tracking service (placeholder)
|
||||||
* @private
|
* @private
|
||||||
* @param {Object} errorEntry - Error entry to send
|
* @param {Object} errorEntry - Error entry to send
|
||||||
*/
|
*/
|
||||||
sendToErrorTracking(errorEntry) {
|
sendToErrorTracking(errorEntry) {
|
||||||
// Placeholder for error tracking integration
|
// Placeholder for error tracking integration
|
||||||
// Could integrate with Sentry, LogRocket, etc.
|
// Could integrate with Sentry, LogRocket, etc.
|
||||||
// Example:
|
// Example:
|
||||||
// if (window.Sentry) {
|
// if (window.Sentry) {
|
||||||
// Sentry.captureException(new Error(errorEntry.message), {
|
// Sentry.captureException(new Error(errorEntry.message), {
|
||||||
// extra: errorEntry.metadata
|
// extra: errorEntry.metadata
|
||||||
// });
|
// });
|
||||||
// }
|
// }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
window.ErrorHandler = ErrorHandler;
|
window.ErrorHandler = ErrorHandler;
|
||||||
console.log('[ErrorHandler] Module loaded');
|
console.log('[ErrorHandler] Module loaded');
|
||||||
|
|
||||||
})(window);
|
})(window);
|
||||||
|
|||||||
@@ -1,91 +1,91 @@
|
|||||||
/* Sami Sans Font Family - External CSS */
|
/* Sami Sans Font Family - External CSS */
|
||||||
|
|
||||||
@font-face {
|
@font-face {
|
||||||
font-family: 'Sami Sans';
|
font-family: 'Sami Sans';
|
||||||
src: url('fonts/SamiSans-Regular.woff2') format('woff2'),
|
src: url('fonts/SamiSans-Regular.woff2') format('woff2'),
|
||||||
url('fonts/SamiSans-Regular.ttf') format('truetype');
|
url('fonts/SamiSans-Regular.ttf') format('truetype');
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
font-display: swap;
|
font-display: swap;
|
||||||
}
|
}
|
||||||
|
|
||||||
@font-face {
|
@font-face {
|
||||||
font-family: 'Sami Sans';
|
font-family: 'Sami Sans';
|
||||||
src: url('fonts/SamiSans-Regular.woff2') format('woff2'),
|
src: url('fonts/SamiSans-Regular.woff2') format('woff2'),
|
||||||
url('fonts/SamiSans-Italic.ttf') format('truetype');
|
url('fonts/SamiSans-Italic.ttf') format('truetype');
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
font-style: italic;
|
font-style: italic;
|
||||||
font-display: swap;
|
font-display: swap;
|
||||||
}
|
}
|
||||||
|
|
||||||
@font-face {
|
@font-face {
|
||||||
font-family: 'Sami Sans';
|
font-family: 'Sami Sans';
|
||||||
src: url('fonts/SamiSans-Medium.woff2') format('woff2'),
|
src: url('fonts/SamiSans-Medium.woff2') format('woff2'),
|
||||||
url('fonts/SamiSans-Medium.ttf') format('truetype');
|
url('fonts/SamiSans-Medium.ttf') format('truetype');
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
font-display: swap;
|
font-display: swap;
|
||||||
}
|
}
|
||||||
|
|
||||||
@font-face {
|
@font-face {
|
||||||
font-family: 'Sami Sans';
|
font-family: 'Sami Sans';
|
||||||
src: url('fonts/SamiSans-SemiBold.woff2') format('woff2'),
|
src: url('fonts/SamiSans-SemiBold.woff2') format('woff2'),
|
||||||
url('fonts/SamiSans-SemiBold.ttf') format('truetype');
|
url('fonts/SamiSans-SemiBold.ttf') format('truetype');
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
font-display: swap;
|
font-display: swap;
|
||||||
}
|
}
|
||||||
|
|
||||||
@font-face {
|
@font-face {
|
||||||
font-family: 'Sami Sans';
|
font-family: 'Sami Sans';
|
||||||
src: url('fonts/SamiSans-Bold.woff2') format('woff2'),
|
src: url('fonts/SamiSans-Bold.woff2') format('woff2'),
|
||||||
url('fonts/SamiSans-Bold.ttf') format('truetype');
|
url('fonts/SamiSans-Bold.ttf') format('truetype');
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
font-display: swap;
|
font-display: swap;
|
||||||
}
|
}
|
||||||
|
|
||||||
@font-face {
|
@font-face {
|
||||||
font-family: 'Sami Sans';
|
font-family: 'Sami Sans';
|
||||||
src: url('fonts/SamiSans-ExtraBold.woff2') format('woff2'),
|
src: url('fonts/SamiSans-ExtraBold.woff2') format('woff2'),
|
||||||
url('fonts/SamiSans-ExtraBold.ttf') format('truetype');
|
url('fonts/SamiSans-ExtraBold.ttf') format('truetype');
|
||||||
font-weight: 800;
|
font-weight: 800;
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
font-display: swap;
|
font-display: swap;
|
||||||
}
|
}
|
||||||
|
|
||||||
@font-face {
|
@font-face {
|
||||||
font-family: 'Sami Sans';
|
font-family: 'Sami Sans';
|
||||||
src: url('fonts/SamiSans-Black.woff2') format('woff2'),
|
src: url('fonts/SamiSans-Black.woff2') format('woff2'),
|
||||||
url('fonts/SamiSans-Black.ttf') format('truetype');
|
url('fonts/SamiSans-Black.ttf') format('truetype');
|
||||||
font-weight: 900;
|
font-weight: 900;
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
font-display: swap;
|
font-display: swap;
|
||||||
}
|
}
|
||||||
|
|
||||||
@font-face {
|
@font-face {
|
||||||
font-family: 'Sami Sans';
|
font-family: 'Sami Sans';
|
||||||
src: url('fonts/SamiSans-Light.woff2') format('woff2'),
|
src: url('fonts/SamiSans-Light.woff2') format('woff2'),
|
||||||
url('fonts/SamiSans-Light.ttf') format('truetype');
|
url('fonts/SamiSans-Light.ttf') format('truetype');
|
||||||
font-weight: 300;
|
font-weight: 300;
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
font-display: swap;
|
font-display: swap;
|
||||||
}
|
}
|
||||||
|
|
||||||
@font-face {
|
@font-face {
|
||||||
font-family: 'Sami Sans';
|
font-family: 'Sami Sans';
|
||||||
src: url('fonts/SamiSans-ExtraLight.woff2') format('woff2'),
|
src: url('fonts/SamiSans-ExtraLight.woff2') format('woff2'),
|
||||||
url('fonts/SamiSans-ExtraLight.ttf') format('truetype');
|
url('fonts/SamiSans-ExtraLight.ttf') format('truetype');
|
||||||
font-weight: 200;
|
font-weight: 200;
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
font-display: swap;
|
font-display: swap;
|
||||||
}
|
}
|
||||||
|
|
||||||
@font-face {
|
@font-face {
|
||||||
font-family: 'Sami Sans';
|
font-family: 'Sami Sans';
|
||||||
src: url('fonts/SamiSans-Thin.woff2') format('woff2'),
|
src: url('fonts/SamiSans-Thin.woff2') format('woff2'),
|
||||||
url('fonts/SamiSans-Thin.ttf') format('truetype');
|
url('fonts/SamiSans-Thin.ttf') format('truetype');
|
||||||
font-weight: 100;
|
font-weight: 100;
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
font-display: swap;
|
font-display: swap;
|
||||||
}
|
}
|
||||||
|
|||||||
+354
-354
@@ -1,354 +1,354 @@
|
|||||||
/**
|
/**
|
||||||
* Onboarding Tooltip Styles
|
* Onboarding Tooltip Styles
|
||||||
* Custom styling for Driver.js tooltips to match DashCaddy theme
|
* Custom styling for Driver.js tooltips to match DashCaddy theme
|
||||||
*/
|
*/
|
||||||
|
|
||||||
/* Driver.js overrides are injected dynamically by ThemeAdapter */
|
/* Driver.js overrides are injected dynamically by ThemeAdapter */
|
||||||
/* This file contains additional custom styles */
|
/* This file contains additional custom styles */
|
||||||
|
|
||||||
.driver-popover {
|
.driver-popover {
|
||||||
max-width: 500px !important;
|
max-width: 500px !important;
|
||||||
z-index: 10000 !important;
|
z-index: 10000 !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.driver-popover-title {
|
.driver-popover-title {
|
||||||
font-size: 1.2rem !important;
|
font-size: 1.2rem !important;
|
||||||
margin-bottom: 12px !important;
|
margin-bottom: 12px !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.driver-popover-description {
|
.driver-popover-description {
|
||||||
font-size: 0.95rem !important;
|
font-size: 0.95rem !important;
|
||||||
line-height: 1.6 !important;
|
line-height: 1.6 !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.driver-popover-description p {
|
.driver-popover-description p {
|
||||||
margin: 8px 0 !important;
|
margin: 8px 0 !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.driver-popover-description ul {
|
.driver-popover-description ul {
|
||||||
margin: 8px 0 !important;
|
margin: 8px 0 !important;
|
||||||
padding-left: 20px !important;
|
padding-left: 20px !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.driver-popover-description li {
|
.driver-popover-description li {
|
||||||
margin: 4px 0 !important;
|
margin: 4px 0 !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.driver-popover-description code {
|
.driver-popover-description code {
|
||||||
background: rgba(0, 0, 0, 0.1) !important;
|
background: rgba(0, 0, 0, 0.1) !important;
|
||||||
padding: 2px 6px !important;
|
padding: 2px 6px !important;
|
||||||
border-radius: 3px !important;
|
border-radius: 3px !important;
|
||||||
font-family: 'Courier New', monospace !important;
|
font-family: 'Courier New', monospace !important;
|
||||||
font-size: 0.9em !important;
|
font-size: 0.9em !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.driver-popover-footer {
|
.driver-popover-footer {
|
||||||
margin-top: 16px !important;
|
margin-top: 16px !important;
|
||||||
display: flex !important;
|
display: flex !important;
|
||||||
gap: 8px !important;
|
gap: 8px !important;
|
||||||
justify-content: flex-end !important;
|
justify-content: flex-end !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.driver-popover-footer button {
|
.driver-popover-footer button {
|
||||||
padding: 8px 16px !important;
|
padding: 8px 16px !important;
|
||||||
border-radius: 8px !important;
|
border-radius: 8px !important;
|
||||||
font-size: 0.9rem !important;
|
font-size: 0.9rem !important;
|
||||||
cursor: pointer !important;
|
cursor: pointer !important;
|
||||||
transition: all 0.2s ease !important;
|
transition: all 0.2s ease !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.driver-popover-footer button:hover {
|
.driver-popover-footer button:hover {
|
||||||
transform: translateY(-1px) !important;
|
transform: translateY(-1px) !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.driver-popover-close-btn {
|
.driver-popover-close-btn {
|
||||||
position: absolute !important;
|
position: absolute !important;
|
||||||
top: 12px !important;
|
top: 12px !important;
|
||||||
right: 12px !important;
|
right: 12px !important;
|
||||||
width: 24px !important;
|
width: 24px !important;
|
||||||
height: 24px !important;
|
height: 24px !important;
|
||||||
border-radius: 50% !important;
|
border-radius: 50% !important;
|
||||||
display: flex !important;
|
display: flex !important;
|
||||||
align-items: center !important;
|
align-items: center !important;
|
||||||
justify-content: center !important;
|
justify-content: center !important;
|
||||||
cursor: pointer !important;
|
cursor: pointer !important;
|
||||||
opacity: 0.6 !important;
|
opacity: 0.6 !important;
|
||||||
transition: opacity 0.2s ease !important;
|
transition: opacity 0.2s ease !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.driver-popover-close-btn:hover {
|
.driver-popover-close-btn:hover {
|
||||||
opacity: 1 !important;
|
opacity: 1 !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.driver-popover-arrow {
|
.driver-popover-arrow {
|
||||||
border-width: 8px !important;
|
border-width: 8px !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Progress indicator */
|
/* Progress indicator */
|
||||||
.driver-popover-progress-text {
|
.driver-popover-progress-text {
|
||||||
font-size: 0.85rem !important;
|
font-size: 0.85rem !important;
|
||||||
margin-bottom: 8px !important;
|
margin-bottom: 8px !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Mobile responsive */
|
/* Mobile responsive */
|
||||||
@media (max-width: 768px) {
|
@media (max-width: 768px) {
|
||||||
.driver-popover {
|
.driver-popover {
|
||||||
max-width: calc(100vw - 32px) !important;
|
max-width: calc(100vw - 32px) !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.driver-popover-title {
|
.driver-popover-title {
|
||||||
font-size: 1.1rem !important;
|
font-size: 1.1rem !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.driver-popover-description {
|
.driver-popover-description {
|
||||||
font-size: 0.9rem !important;
|
font-size: 0.9rem !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.driver-popover-footer button {
|
.driver-popover-footer button {
|
||||||
padding: 6px 12px !important;
|
padding: 6px 12px !important;
|
||||||
font-size: 0.85rem !important;
|
font-size: 0.85rem !important;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Restart tour button in dashboard */
|
/* Restart tour button in dashboard */
|
||||||
#restart-tour-btn {
|
#restart-tour-btn {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 6px;
|
gap: 6px;
|
||||||
}
|
}
|
||||||
|
|
||||||
#restart-tour-btn::before {
|
#restart-tour-btn::before {
|
||||||
content: "🎓";
|
content: "🎓";
|
||||||
font-size: 1.1em;
|
font-size: 1.1em;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/* DNS Template Selector Modal */
|
/* DNS Template Selector Modal */
|
||||||
.dns-template-modal {
|
.dns-template-modal {
|
||||||
display: none;
|
display: none;
|
||||||
position: fixed;
|
position: fixed;
|
||||||
top: 0;
|
top: 0;
|
||||||
left: 0;
|
left: 0;
|
||||||
right: 0;
|
right: 0;
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
background: rgba(0, 0, 0, 0.8);
|
background: rgba(0, 0, 0, 0.8);
|
||||||
z-index: 10000;
|
z-index: 10000;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
padding: 20px;
|
padding: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.dns-template-modal-content {
|
.dns-template-modal-content {
|
||||||
background: var(--card-base);
|
background: var(--card-base);
|
||||||
border-radius: 12px;
|
border-radius: 12px;
|
||||||
max-width: 900px;
|
max-width: 900px;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
max-height: 90vh;
|
max-height: 90vh;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.4);
|
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.4);
|
||||||
}
|
}
|
||||||
|
|
||||||
.dns-template-header {
|
.dns-template-header {
|
||||||
padding: 30px;
|
padding: 30px;
|
||||||
border-bottom: 1px solid var(--border);
|
border-bottom: 1px solid var(--border);
|
||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
.dns-template-header h2 {
|
.dns-template-header h2 {
|
||||||
margin: 0 0 10px 0;
|
margin: 0 0 10px 0;
|
||||||
color: var(--fg);
|
color: var(--fg);
|
||||||
font-size: 28px;
|
font-size: 28px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.dns-template-header p {
|
.dns-template-header p {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
color: var(--fg-muted);
|
color: var(--fg-muted);
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.dns-template-close {
|
.dns-template-close {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 20px;
|
top: 20px;
|
||||||
right: 20px;
|
right: 20px;
|
||||||
background: none;
|
background: none;
|
||||||
border: none;
|
border: none;
|
||||||
font-size: 32px;
|
font-size: 32px;
|
||||||
color: var(--fg-muted);
|
color: var(--fg-muted);
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
width: 40px;
|
width: 40px;
|
||||||
height: 40px;
|
height: 40px;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
transition: all 0.2s;
|
transition: all 0.2s;
|
||||||
}
|
}
|
||||||
|
|
||||||
.dns-template-close:hover {
|
.dns-template-close:hover {
|
||||||
background: var(--hover);
|
background: var(--hover);
|
||||||
color: var(--fg);
|
color: var(--fg);
|
||||||
}
|
}
|
||||||
|
|
||||||
.dns-template-grid {
|
.dns-template-grid {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
|
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
|
||||||
gap: 20px;
|
gap: 20px;
|
||||||
padding: 30px;
|
padding: 30px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.dns-template-card {
|
.dns-template-card {
|
||||||
background: var(--card-hover);
|
background: var(--card-hover);
|
||||||
border: 2px solid var(--border);
|
border: 2px solid var(--border);
|
||||||
border-radius: 12px;
|
border-radius: 12px;
|
||||||
padding: 20px;
|
padding: 20px;
|
||||||
transition: all 0.3s;
|
transition: all 0.3s;
|
||||||
position: relative;
|
position: relative;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
|
|
||||||
.dns-template-card:hover {
|
.dns-template-card:hover {
|
||||||
transform: translateY(-4px);
|
transform: translateY(-4px);
|
||||||
box-shadow: 0 8px 16px rgba(0, 0, 0, 0.2);
|
box-shadow: 0 8px 16px rgba(0, 0, 0, 0.2);
|
||||||
border-color: var(--accent);
|
border-color: var(--accent);
|
||||||
}
|
}
|
||||||
|
|
||||||
.dns-template-card.recommended {
|
.dns-template-card.recommended {
|
||||||
border-color: var(--accent);
|
border-color: var(--accent);
|
||||||
background: linear-gradient(135deg, var(--card-hover) 0%, var(--card-base) 100%);
|
background: linear-gradient(135deg, var(--card-hover) 0%, var(--card-base) 100%);
|
||||||
}
|
}
|
||||||
|
|
||||||
.recommended-badge {
|
.recommended-badge {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: -10px;
|
top: -10px;
|
||||||
right: 20px;
|
right: 20px;
|
||||||
background: var(--accent);
|
background: var(--accent);
|
||||||
color: white;
|
color: white;
|
||||||
padding: 4px 12px;
|
padding: 4px 12px;
|
||||||
border-radius: 12px;
|
border-radius: 12px;
|
||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
letter-spacing: 0.5px;
|
letter-spacing: 0.5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.dns-template-icon {
|
.dns-template-icon {
|
||||||
font-size: 48px;
|
font-size: 48px;
|
||||||
margin-bottom: 15px;
|
margin-bottom: 15px;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.dns-template-card h3 {
|
.dns-template-card h3 {
|
||||||
margin: 0 0 10px 0;
|
margin: 0 0 10px 0;
|
||||||
color: var(--fg);
|
color: var(--fg);
|
||||||
font-size: 18px;
|
font-size: 18px;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.dns-template-description {
|
.dns-template-description {
|
||||||
color: var(--fg-muted);
|
color: var(--fg-muted);
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
margin: 0 0 15px 0;
|
margin: 0 0 15px 0;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.dns-template-difficulty {
|
.dns-template-difficulty {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
padding: 4px 12px;
|
padding: 4px 12px;
|
||||||
border-radius: 12px;
|
border-radius: 12px;
|
||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
margin: 0 auto 15px auto;
|
margin: 0 auto 15px auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.difficulty-easy {
|
.difficulty-easy {
|
||||||
background: #2ecc71;
|
background: #2ecc71;
|
||||||
color: white;
|
color: white;
|
||||||
}
|
}
|
||||||
|
|
||||||
.difficulty-intermediate {
|
.difficulty-intermediate {
|
||||||
background: #f39c12;
|
background: #f39c12;
|
||||||
color: white;
|
color: white;
|
||||||
}
|
}
|
||||||
|
|
||||||
.difficulty-advanced {
|
.difficulty-advanced {
|
||||||
background: #e74c3c;
|
background: #e74c3c;
|
||||||
color: white;
|
color: white;
|
||||||
}
|
}
|
||||||
|
|
||||||
.dns-template-features {
|
.dns-template-features {
|
||||||
list-style: none;
|
list-style: none;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
margin: 0 0 20px 0;
|
margin: 0 0 20px 0;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
color: var(--fg-muted);
|
color: var(--fg-muted);
|
||||||
}
|
}
|
||||||
|
|
||||||
.dns-template-features li {
|
.dns-template-features li {
|
||||||
padding: 6px 0;
|
padding: 6px 0;
|
||||||
padding-left: 20px;
|
padding-left: 20px;
|
||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
.dns-template-features li:before {
|
.dns-template-features li:before {
|
||||||
content: "✓";
|
content: "✓";
|
||||||
position: absolute;
|
position: absolute;
|
||||||
left: 0;
|
left: 0;
|
||||||
color: var(--accent);
|
color: var(--accent);
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
}
|
}
|
||||||
|
|
||||||
.dns-template-select-btn {
|
.dns-template-select-btn {
|
||||||
background: var(--accent);
|
background: var(--accent);
|
||||||
color: white;
|
color: white;
|
||||||
border: none;
|
border: none;
|
||||||
padding: 12px 20px;
|
padding: 12px 20px;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: all 0.2s;
|
transition: all 0.2s;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.dns-template-select-btn:hover {
|
.dns-template-select-btn:hover {
|
||||||
background: var(--accent-strong);
|
background: var(--accent-strong);
|
||||||
transform: scale(1.02);
|
transform: scale(1.02);
|
||||||
}
|
}
|
||||||
|
|
||||||
.dns-template-footer {
|
.dns-template-footer {
|
||||||
padding: 20px 30px;
|
padding: 20px 30px;
|
||||||
border-top: 1px solid var(--border);
|
border-top: 1px solid var(--border);
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.dns-template-later-btn {
|
.dns-template-later-btn {
|
||||||
background: transparent;
|
background: transparent;
|
||||||
color: var(--fg-muted);
|
color: var(--fg-muted);
|
||||||
border: 1px solid var(--border);
|
border: 1px solid var(--border);
|
||||||
padding: 10px 24px;
|
padding: 10px 24px;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: all 0.2s;
|
transition: all 0.2s;
|
||||||
}
|
}
|
||||||
|
|
||||||
.dns-template-later-btn:hover {
|
.dns-template-later-btn:hover {
|
||||||
background: var(--hover);
|
background: var(--hover);
|
||||||
color: var(--fg);
|
color: var(--fg);
|
||||||
border-color: var(--fg-muted);
|
border-color: var(--fg-muted);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Responsive design */
|
/* Responsive design */
|
||||||
@media (max-width: 768px) {
|
@media (max-width: 768px) {
|
||||||
.dns-template-grid {
|
.dns-template-grid {
|
||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
}
|
}
|
||||||
|
|
||||||
.dns-template-modal-content {
|
.dns-template-modal-content {
|
||||||
max-height: 95vh;
|
max-height: 95vh;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+177
-177
@@ -1,177 +1,177 @@
|
|||||||
/**
|
/**
|
||||||
* DashCaddy User Onboarding System
|
* DashCaddy User Onboarding System
|
||||||
* Main entry point for the tooltip-based onboarding experience
|
* Main entry point for the tooltip-based onboarding experience
|
||||||
*
|
*
|
||||||
* This file initializes the onboarding system and coordinates between
|
* This file initializes the onboarding system and coordinates between
|
||||||
* the various components (TourManager, ProgressTracker, ThemeAdapter, etc.)
|
* the various components (TourManager, ProgressTracker, ThemeAdapter, etc.)
|
||||||
*/
|
*/
|
||||||
|
|
||||||
(function() {
|
(function() {
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
let progressTracker;
|
let progressTracker;
|
||||||
let themeAdapter;
|
let themeAdapter;
|
||||||
let tourManager;
|
let tourManager;
|
||||||
let dnsTemplateSelector;
|
let dnsTemplateSelector;
|
||||||
let errorHandler;
|
let errorHandler;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Initialize the onboarding system
|
* Initialize the onboarding system
|
||||||
*/
|
*/
|
||||||
async function initializeOnboarding() {
|
async function initializeOnboarding() {
|
||||||
try {
|
try {
|
||||||
console.log('[Onboarding] Initializing system...');
|
console.log('[Onboarding] Initializing system...');
|
||||||
|
|
||||||
// Initialize Error Handler first
|
// Initialize Error Handler first
|
||||||
errorHandler = new ErrorHandler();
|
errorHandler = new ErrorHandler();
|
||||||
console.log('[Onboarding] Error Handler initialized');
|
console.log('[Onboarding] Error Handler initialized');
|
||||||
|
|
||||||
// Initialize Progress Tracker
|
// Initialize Progress Tracker
|
||||||
progressTracker = new ProgressTracker('dashcaddy_onboarding');
|
progressTracker = new ProgressTracker('dashcaddy_onboarding');
|
||||||
console.log('[Onboarding] Progress Tracker initialized');
|
console.log('[Onboarding] Progress Tracker initialized');
|
||||||
|
|
||||||
// Initialize Theme Adapter
|
// Initialize Theme Adapter
|
||||||
themeAdapter = new ThemeAdapter();
|
themeAdapter = new ThemeAdapter();
|
||||||
console.log('[Onboarding] Theme Adapter initialized');
|
console.log('[Onboarding] Theme Adapter initialized');
|
||||||
|
|
||||||
// Initialize DNS Template Selector
|
// Initialize DNS Template Selector
|
||||||
dnsTemplateSelector = new DnsTemplateSelector(progressTracker);
|
dnsTemplateSelector = new DnsTemplateSelector(progressTracker);
|
||||||
console.log('[Onboarding] DNS Template Selector initialized');
|
console.log('[Onboarding] DNS Template Selector initialized');
|
||||||
|
|
||||||
// Initialize Tour Manager
|
// Initialize Tour Manager
|
||||||
tourManager = new TourManager(progressTracker, themeAdapter, dnsTemplateSelector);
|
tourManager = new TourManager(progressTracker, themeAdapter, dnsTemplateSelector);
|
||||||
console.log('[Onboarding] Tour Manager initialized');
|
console.log('[Onboarding] Tour Manager initialized');
|
||||||
|
|
||||||
// Check if tour should auto-start
|
// Check if tour should auto-start
|
||||||
if (tourManager.shouldAutoStart()) {
|
if (tourManager.shouldAutoStart()) {
|
||||||
console.log('[Onboarding] Auto-starting tour for first-time user');
|
console.log('[Onboarding] Auto-starting tour for first-time user');
|
||||||
// Wait a bit for page to fully load
|
// Wait a bit for page to fully load
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
tourManager.startTour();
|
tourManager.startTour();
|
||||||
}, 1000);
|
}, 1000);
|
||||||
} else {
|
} else {
|
||||||
const tourCompleted = progressTracker.isTourCompleted();
|
const tourCompleted = progressTracker.isTourCompleted();
|
||||||
const currentStep = progressTracker.getCurrentStep();
|
const currentStep = progressTracker.getCurrentStep();
|
||||||
console.log(`[Onboarding] Tour not auto-starting (completed: ${tourCompleted}, step: ${currentStep})`);
|
console.log(`[Onboarding] Tour not auto-starting (completed: ${tourCompleted}, step: ${currentStep})`);
|
||||||
|
|
||||||
// If tour is in progress, offer to resume
|
// If tour is in progress, offer to resume
|
||||||
if (!tourCompleted && currentStep > 0) {
|
if (!tourCompleted && currentStep > 0) {
|
||||||
console.log('[Onboarding] Tour in progress, can be resumed manually');
|
console.log('[Onboarding] Tour in progress, can be resumed manually');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add restart tour button to tools row
|
// Add restart tour button to tools row
|
||||||
addRestartTourButton();
|
addRestartTourButton();
|
||||||
|
|
||||||
// Expose to global scope for manual triggering
|
// Expose to global scope for manual triggering
|
||||||
window.DashCaddyOnboarding = {
|
window.DashCaddyOnboarding = {
|
||||||
startTour: () => tourManager.startTour(),
|
startTour: () => tourManager.startTour(),
|
||||||
restartTour: () => tourManager.restartTour(),
|
restartTour: () => tourManager.restartTour(),
|
||||||
showTooltip: (id) => tourManager.showTooltip(id),
|
showTooltip: (id) => tourManager.showTooltip(id),
|
||||||
showWhatsNew: () => tourManager.showWhatsNew(),
|
showWhatsNew: () => tourManager.showWhatsNew(),
|
||||||
resetProgress: () => progressTracker.resetProgress(),
|
resetProgress: () => progressTracker.resetProgress(),
|
||||||
getErrors: () => errorHandler.getErrors(),
|
getErrors: () => errorHandler.getErrors(),
|
||||||
getErrorStats: () => errorHandler.getStatistics()
|
getErrorStats: () => errorHandler.getStatistics()
|
||||||
};
|
};
|
||||||
|
|
||||||
console.log('[Onboarding] System initialized successfully');
|
console.log('[Onboarding] System initialized successfully');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('[Onboarding] Initialization error:', error);
|
console.error('[Onboarding] Initialization error:', error);
|
||||||
|
|
||||||
// Use error handler if available
|
// Use error handler if available
|
||||||
if (errorHandler) {
|
if (errorHandler) {
|
||||||
errorHandler.logError('Initialization', error);
|
errorHandler.logError('Initialization', error);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Graceful degradation - don't break the dashboard
|
// Graceful degradation - don't break the dashboard
|
||||||
console.warn('[Onboarding] System failed to initialize, dashboard will continue without onboarding');
|
console.warn('[Onboarding] System failed to initialize, dashboard will continue without onboarding');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Add restart tour button to tools row
|
* Add restart tour button to tools row
|
||||||
*/
|
*/
|
||||||
function addRestartTourButton() {
|
function addRestartTourButton() {
|
||||||
const toolsRow = document.querySelector('.tools');
|
const toolsRow = document.querySelector('.tools');
|
||||||
if (!toolsRow) return;
|
if (!toolsRow) return;
|
||||||
|
|
||||||
const clickHandler = () => {
|
const clickHandler = () => {
|
||||||
if (tourManager) {
|
if (tourManager) {
|
||||||
console.log('[Onboarding] Starting tour via button click');
|
console.log('[Onboarding] Starting tour via button click');
|
||||||
tourManager.restartTour();
|
tourManager.restartTour();
|
||||||
} else {
|
} else {
|
||||||
console.error('[Onboarding] Tour manager not initialized');
|
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');
|
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
|
// If button already exists in the HTML, just attach the handler
|
||||||
const existing = document.getElementById('restart-tour-btn');
|
const existing = document.getElementById('restart-tour-btn');
|
||||||
if (existing) {
|
if (existing) {
|
||||||
existing.onclick = clickHandler;
|
existing.onclick = clickHandler;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const button = document.createElement('button');
|
const button = document.createElement('button');
|
||||||
button.id = 'restart-tour-btn';
|
button.id = 'restart-tour-btn';
|
||||||
button.textContent = 'Help Tour';
|
button.textContent = 'Help Tour';
|
||||||
button.title = 'Restart the onboarding tour';
|
button.title = 'Restart the onboarding tour';
|
||||||
button.onclick = clickHandler;
|
button.onclick = clickHandler;
|
||||||
toolsRow.appendChild(button);
|
toolsRow.appendChild(button);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check if Driver.js is loaded
|
* Check if Driver.js is loaded
|
||||||
*/
|
*/
|
||||||
function checkDriverLoaded() {
|
function checkDriverLoaded() {
|
||||||
// Driver.js v1.x IIFE: window.driver.js.driver is the factory function
|
// Driver.js v1.x IIFE: window.driver.js.driver is the factory function
|
||||||
const driverFactory = window.driver?.js?.driver || window.driver?.driver || window.driver;
|
const driverFactory = window.driver?.js?.driver || window.driver?.driver || window.driver;
|
||||||
if (typeof driverFactory !== 'function') {
|
if (typeof driverFactory !== 'function') {
|
||||||
console.warn('[Onboarding] Driver.js not loaded yet, will retry... window.driver:', window.driver);
|
console.warn('[Onboarding] Driver.js not loaded yet, will retry... window.driver:', window.driver);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Wait for Driver.js to load, then initialize
|
* Wait for Driver.js to load, then initialize
|
||||||
*/
|
*/
|
||||||
function waitForDriver() {
|
function waitForDriver() {
|
||||||
let retries = 0;
|
let retries = 0;
|
||||||
const maxRetries = 10;
|
const maxRetries = 10;
|
||||||
|
|
||||||
function attemptInit() {
|
function attemptInit() {
|
||||||
if (checkDriverLoaded()) {
|
if (checkDriverLoaded()) {
|
||||||
initializeOnboarding();
|
initializeOnboarding();
|
||||||
} else {
|
} else {
|
||||||
retries++;
|
retries++;
|
||||||
if (retries < maxRetries) {
|
if (retries < maxRetries) {
|
||||||
// Retry after a short delay
|
// Retry after a short delay
|
||||||
setTimeout(attemptInit, 500);
|
setTimeout(attemptInit, 500);
|
||||||
} else {
|
} else {
|
||||||
// Max retries reached, show fallback
|
// Max retries reached, show fallback
|
||||||
console.error('[Onboarding] Driver.js failed to load after multiple attempts');
|
console.error('[Onboarding] Driver.js failed to load after multiple attempts');
|
||||||
if (errorHandler) {
|
if (errorHandler) {
|
||||||
errorHandler.handleDriverLoadFailure();
|
errorHandler.handleDriverLoadFailure();
|
||||||
} else {
|
} else {
|
||||||
// Create temporary error handler for fallback
|
// Create temporary error handler for fallback
|
||||||
const tempHandler = new ErrorHandler();
|
const tempHandler = new ErrorHandler();
|
||||||
tempHandler.handleDriverLoadFailure();
|
tempHandler.handleDriverLoadFailure();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
attemptInit();
|
attemptInit();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Start initialization when DOM is ready
|
// Start initialization when DOM is ready
|
||||||
if (document.readyState === 'loading') {
|
if (document.readyState === 'loading') {
|
||||||
document.addEventListener('DOMContentLoaded', waitForDriver);
|
document.addEventListener('DOMContentLoaded', waitForDriver);
|
||||||
} else {
|
} else {
|
||||||
waitForDriver();
|
waitForDriver();
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('[Onboarding] System loaded');
|
console.log('[Onboarding] System loaded');
|
||||||
|
|
||||||
})();
|
})();
|
||||||
|
|||||||
@@ -1,282 +1,282 @@
|
|||||||
/**
|
/**
|
||||||
* Progress Tracker
|
* Progress Tracker
|
||||||
* Manages persistent storage of user progress through the onboarding flow
|
* Manages persistent storage of user progress through the onboarding flow
|
||||||
* using browser local storage.
|
* using browser local storage.
|
||||||
*
|
*
|
||||||
* Storage Schema:
|
* Storage Schema:
|
||||||
* {
|
* {
|
||||||
* "version": "1.0",
|
* "version": "1.0",
|
||||||
* "tourCompleted": false,
|
* "tourCompleted": false,
|
||||||
* "completedTooltips": ["welcome", "dns-priority", ...],
|
* "completedTooltips": ["welcome", "dns-priority", ...],
|
||||||
* "currentStep": 3,
|
* "currentStep": 3,
|
||||||
* "completionTimestamp": "2024-01-15T10:30:00Z",
|
* "completionTimestamp": "2024-01-15T10:30:00Z",
|
||||||
* "dnsSetupDeferred": false,
|
* "dnsSetupDeferred": false,
|
||||||
* "lastVisit": "2024-01-15T10:30:00Z"
|
* "lastVisit": "2024-01-15T10:30:00Z"
|
||||||
* }
|
* }
|
||||||
*/
|
*/
|
||||||
|
|
||||||
(function(window) {
|
(function(window) {
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* ProgressTracker class
|
* ProgressTracker class
|
||||||
* Manages persistent storage of onboarding progress
|
* Manages persistent storage of onboarding progress
|
||||||
*
|
*
|
||||||
* @class
|
* @class
|
||||||
* @param {string} storageKey - The key to use for local storage (default: 'dashcaddy_onboarding')
|
* @param {string} storageKey - The key to use for local storage (default: 'dashcaddy_onboarding')
|
||||||
*/
|
*/
|
||||||
class ProgressTracker {
|
class ProgressTracker {
|
||||||
constructor(storageKey = 'dashcaddy_onboarding') {
|
constructor(storageKey = 'dashcaddy_onboarding') {
|
||||||
this.storageKey = storageKey;
|
this.storageKey = storageKey;
|
||||||
this.storageVersion = '1.0';
|
this.storageVersion = '1.0';
|
||||||
|
|
||||||
// Initialize storage if it doesn't exist
|
// Initialize storage if it doesn't exist
|
||||||
this._initializeStorage();
|
this._initializeStorage();
|
||||||
|
|
||||||
// Update last visit timestamp
|
// Update last visit timestamp
|
||||||
this._updateLastVisit();
|
this._updateLastVisit();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Initialize storage with default values if it doesn't exist
|
* Initialize storage with default values if it doesn't exist
|
||||||
* @private
|
* @private
|
||||||
*/
|
*/
|
||||||
_initializeStorage() {
|
_initializeStorage() {
|
||||||
const existing = this._getStorage();
|
const existing = this._getStorage();
|
||||||
if (!existing || existing.version !== this.storageVersion) {
|
if (!existing || existing.version !== this.storageVersion) {
|
||||||
const defaultState = {
|
const defaultState = {
|
||||||
version: this.storageVersion,
|
version: this.storageVersion,
|
||||||
tourCompleted: false,
|
tourCompleted: false,
|
||||||
completedTooltips: [],
|
completedTooltips: [],
|
||||||
currentStep: 0,
|
currentStep: 0,
|
||||||
completionTimestamp: null,
|
completionTimestamp: null,
|
||||||
dnsSetupDeferred: false,
|
dnsSetupDeferred: false,
|
||||||
lastVisit: new Date().toISOString()
|
lastVisit: new Date().toISOString()
|
||||||
};
|
};
|
||||||
this._setStorage(defaultState);
|
this._setStorage(defaultState);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the current storage state
|
* Get the current storage state
|
||||||
* @private
|
* @private
|
||||||
* @returns {Object|null} The storage state or null if unavailable
|
* @returns {Object|null} The storage state or null if unavailable
|
||||||
*/
|
*/
|
||||||
_getStorage() {
|
_getStorage() {
|
||||||
try {
|
try {
|
||||||
const data = localStorage.getItem(this.storageKey);
|
const data = localStorage.getItem(this.storageKey);
|
||||||
return data ? JSON.parse(data) : null;
|
return data ? JSON.parse(data) : null;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('[ProgressTracker] Error reading from storage:', error);
|
console.error('[ProgressTracker] Error reading from storage:', error);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Set the storage state
|
* Set the storage state
|
||||||
* @private
|
* @private
|
||||||
* @param {Object} state - The state to save
|
* @param {Object} state - The state to save
|
||||||
*/
|
*/
|
||||||
_setStorage(state) {
|
_setStorage(state) {
|
||||||
try {
|
try {
|
||||||
localStorage.setItem(this.storageKey, JSON.stringify(state));
|
localStorage.setItem(this.storageKey, JSON.stringify(state));
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('[ProgressTracker] Error writing to storage:', error);
|
console.error('[ProgressTracker] Error writing to storage:', error);
|
||||||
// Handle quota exceeded or storage unavailable
|
// Handle quota exceeded or storage unavailable
|
||||||
// Fall back to session storage or in-memory storage
|
// Fall back to session storage or in-memory storage
|
||||||
this._handleStorageError(error);
|
this._handleStorageError(error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handle storage errors (quota exceeded, unavailable, etc.)
|
* Handle storage errors (quota exceeded, unavailable, etc.)
|
||||||
* @private
|
* @private
|
||||||
* @param {Error} error - The error that occurred
|
* @param {Error} error - The error that occurred
|
||||||
*/
|
*/
|
||||||
_handleStorageError(error) {
|
_handleStorageError(error) {
|
||||||
// Try session storage as fallback
|
// Try session storage as fallback
|
||||||
try {
|
try {
|
||||||
sessionStorage.setItem(this.storageKey, JSON.stringify(this._getStorage()));
|
sessionStorage.setItem(this.storageKey, JSON.stringify(this._getStorage()));
|
||||||
console.warn('[ProgressTracker] Falling back to session storage');
|
console.warn('[ProgressTracker] Falling back to session storage');
|
||||||
} catch (sessionError) {
|
} catch (sessionError) {
|
||||||
console.error('[ProgressTracker] Session storage also unavailable:', sessionError);
|
console.error('[ProgressTracker] Session storage also unavailable:', sessionError);
|
||||||
// Could implement in-memory fallback here if needed
|
// Could implement in-memory fallback here if needed
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Update the last visit timestamp
|
* Update the last visit timestamp
|
||||||
* @private
|
* @private
|
||||||
*/
|
*/
|
||||||
_updateLastVisit() {
|
_updateLastVisit() {
|
||||||
const state = this._getStorage();
|
const state = this._getStorage();
|
||||||
if (state) {
|
if (state) {
|
||||||
state.lastVisit = new Date().toISOString();
|
state.lastVisit = new Date().toISOString();
|
||||||
this._setStorage(state);
|
this._setStorage(state);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check if a specific tooltip has been completed
|
* Check if a specific tooltip has been completed
|
||||||
* @param {string} tooltipId - The ID of the tooltip to check
|
* @param {string} tooltipId - The ID of the tooltip to check
|
||||||
* @returns {boolean} True if the tooltip has been completed
|
* @returns {boolean} True if the tooltip has been completed
|
||||||
*/
|
*/
|
||||||
isTooltipCompleted(tooltipId) {
|
isTooltipCompleted(tooltipId) {
|
||||||
const state = this._getStorage();
|
const state = this._getStorage();
|
||||||
if (!state) return false;
|
if (!state) return false;
|
||||||
return state.completedTooltips.includes(tooltipId);
|
return state.completedTooltips.includes(tooltipId);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Mark a tooltip as completed with timestamp
|
* Mark a tooltip as completed with timestamp
|
||||||
* @param {string} tooltipId - The ID of the tooltip to mark as completed
|
* @param {string} tooltipId - The ID of the tooltip to mark as completed
|
||||||
*/
|
*/
|
||||||
markTooltipCompleted(tooltipId) {
|
markTooltipCompleted(tooltipId) {
|
||||||
const state = this._getStorage();
|
const state = this._getStorage();
|
||||||
if (!state) return;
|
if (!state) return;
|
||||||
|
|
||||||
// Add tooltip to completed list if not already there
|
// Add tooltip to completed list if not already there
|
||||||
if (!state.completedTooltips.includes(tooltipId)) {
|
if (!state.completedTooltips.includes(tooltipId)) {
|
||||||
state.completedTooltips.push(tooltipId);
|
state.completedTooltips.push(tooltipId);
|
||||||
|
|
||||||
// Store timestamp for this specific tooltip
|
// Store timestamp for this specific tooltip
|
||||||
if (!state.tooltipTimestamps) {
|
if (!state.tooltipTimestamps) {
|
||||||
state.tooltipTimestamps = {};
|
state.tooltipTimestamps = {};
|
||||||
}
|
}
|
||||||
state.tooltipTimestamps[tooltipId] = new Date().toISOString();
|
state.tooltipTimestamps[tooltipId] = new Date().toISOString();
|
||||||
|
|
||||||
this._setStorage(state);
|
this._setStorage(state);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check if the entire tour has been completed
|
* Check if the entire tour has been completed
|
||||||
* @returns {boolean} True if the tour is completed
|
* @returns {boolean} True if the tour is completed
|
||||||
*/
|
*/
|
||||||
isTourCompleted() {
|
isTourCompleted() {
|
||||||
const state = this._getStorage();
|
const state = this._getStorage();
|
||||||
if (!state) return false;
|
if (!state) return false;
|
||||||
return state.tourCompleted === true;
|
return state.tourCompleted === true;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Mark the entire tour as completed
|
* Mark the entire tour as completed
|
||||||
*/
|
*/
|
||||||
markTourCompleted() {
|
markTourCompleted() {
|
||||||
const state = this._getStorage();
|
const state = this._getStorage();
|
||||||
if (!state) return;
|
if (!state) return;
|
||||||
|
|
||||||
state.tourCompleted = true;
|
state.tourCompleted = true;
|
||||||
state.completionTimestamp = new Date().toISOString();
|
state.completionTimestamp = new Date().toISOString();
|
||||||
this._setStorage(state);
|
this._setStorage(state);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the current step index
|
* Get the current step index
|
||||||
* @returns {number} The current step index (0-based)
|
* @returns {number} The current step index (0-based)
|
||||||
*/
|
*/
|
||||||
getCurrentStep() {
|
getCurrentStep() {
|
||||||
const state = this._getStorage();
|
const state = this._getStorage();
|
||||||
if (!state) return 0;
|
if (!state) return 0;
|
||||||
return state.currentStep || 0;
|
return state.currentStep || 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Set the current step index
|
* Set the current step index
|
||||||
* @param {number} stepIndex - The step index to set (0-based)
|
* @param {number} stepIndex - The step index to set (0-based)
|
||||||
*/
|
*/
|
||||||
setCurrentStep(stepIndex) {
|
setCurrentStep(stepIndex) {
|
||||||
const state = this._getStorage();
|
const state = this._getStorage();
|
||||||
if (!state) return;
|
if (!state) return;
|
||||||
|
|
||||||
state.currentStep = stepIndex;
|
state.currentStep = stepIndex;
|
||||||
this._setStorage(state);
|
this._setStorage(state);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Reset all progress and clear storage
|
* Reset all progress and clear storage
|
||||||
*/
|
*/
|
||||||
resetProgress() {
|
resetProgress() {
|
||||||
const defaultState = {
|
const defaultState = {
|
||||||
version: this.storageVersion,
|
version: this.storageVersion,
|
||||||
tourCompleted: false,
|
tourCompleted: false,
|
||||||
completedTooltips: [],
|
completedTooltips: [],
|
||||||
currentStep: 0,
|
currentStep: 0,
|
||||||
completionTimestamp: null,
|
completionTimestamp: null,
|
||||||
dnsSetupDeferred: false,
|
dnsSetupDeferred: false,
|
||||||
lastVisit: new Date().toISOString()
|
lastVisit: new Date().toISOString()
|
||||||
};
|
};
|
||||||
this._setStorage(defaultState);
|
this._setStorage(defaultState);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the completion timestamp
|
* Get the completion timestamp
|
||||||
* @returns {Date|null} The completion timestamp or null if not completed
|
* @returns {Date|null} The completion timestamp or null if not completed
|
||||||
*/
|
*/
|
||||||
getCompletionTimestamp() {
|
getCompletionTimestamp() {
|
||||||
const state = this._getStorage();
|
const state = this._getStorage();
|
||||||
if (!state || !state.completionTimestamp) return null;
|
if (!state || !state.completionTimestamp) return null;
|
||||||
return new Date(state.completionTimestamp);
|
return new Date(state.completionTimestamp);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check if DNS setup was deferred
|
* Check if DNS setup was deferred
|
||||||
* @returns {boolean} True if DNS setup was deferred
|
* @returns {boolean} True if DNS setup was deferred
|
||||||
*/
|
*/
|
||||||
isDnsSetupDeferred() {
|
isDnsSetupDeferred() {
|
||||||
const state = this._getStorage();
|
const state = this._getStorage();
|
||||||
if (!state) return false;
|
if (!state) return false;
|
||||||
return state.dnsSetupDeferred === true;
|
return state.dnsSetupDeferred === true;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Mark DNS setup as deferred
|
* Mark DNS setup as deferred
|
||||||
*/
|
*/
|
||||||
markDnsSetupDeferred() {
|
markDnsSetupDeferred() {
|
||||||
const state = this._getStorage();
|
const state = this._getStorage();
|
||||||
if (!state) return;
|
if (!state) return;
|
||||||
|
|
||||||
state.dnsSetupDeferred = true;
|
state.dnsSetupDeferred = true;
|
||||||
this._setStorage(state);
|
this._setStorage(state);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the timestamp for a specific tooltip completion
|
* Get the timestamp for a specific tooltip completion
|
||||||
* @param {string} tooltipId - The ID of the tooltip
|
* @param {string} tooltipId - The ID of the tooltip
|
||||||
* @returns {Date|null} The timestamp or null if not completed
|
* @returns {Date|null} The timestamp or null if not completed
|
||||||
*/
|
*/
|
||||||
getTooltipTimestamp(tooltipId) {
|
getTooltipTimestamp(tooltipId) {
|
||||||
const state = this._getStorage();
|
const state = this._getStorage();
|
||||||
if (!state || !state.tooltipTimestamps || !state.tooltipTimestamps[tooltipId]) {
|
if (!state || !state.tooltipTimestamps || !state.tooltipTimestamps[tooltipId]) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
return new Date(state.tooltipTimestamps[tooltipId]);
|
return new Date(state.tooltipTimestamps[tooltipId]);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get all completed tooltip IDs
|
* Get all completed tooltip IDs
|
||||||
* @returns {string[]} Array of completed tooltip IDs
|
* @returns {string[]} Array of completed tooltip IDs
|
||||||
*/
|
*/
|
||||||
getCompletedTooltips() {
|
getCompletedTooltips() {
|
||||||
const state = this._getStorage();
|
const state = this._getStorage();
|
||||||
if (!state) return [];
|
if (!state) return [];
|
||||||
return state.completedTooltips || [];
|
return state.completedTooltips || [];
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the last visit timestamp
|
* Get the last visit timestamp
|
||||||
* @returns {Date|null} The last visit timestamp
|
* @returns {Date|null} The last visit timestamp
|
||||||
*/
|
*/
|
||||||
getLastVisit() {
|
getLastVisit() {
|
||||||
const state = this._getStorage();
|
const state = this._getStorage();
|
||||||
if (!state || !state.lastVisit) return null;
|
if (!state || !state.lastVisit) return null;
|
||||||
return new Date(state.lastVisit);
|
return new Date(state.lastVisit);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Export to global scope
|
// Export to global scope
|
||||||
window.ProgressTracker = ProgressTracker;
|
window.ProgressTracker = ProgressTracker;
|
||||||
|
|
||||||
console.log('[ProgressTracker] Module loaded');
|
console.log('[ProgressTracker] Module loaded');
|
||||||
|
|
||||||
})(window);
|
})(window);
|
||||||
|
|||||||
@@ -1,337 +1,337 @@
|
|||||||
/**
|
/**
|
||||||
* Tooltip Definitions
|
* Tooltip Definitions
|
||||||
* Defines all tooltip content, positioning, and behavior for the onboarding system
|
* Defines all tooltip content, positioning, and behavior for the onboarding system
|
||||||
*/
|
*/
|
||||||
|
|
||||||
(function(window) {
|
(function(window) {
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Validate a tooltip definition
|
* Validate a tooltip definition
|
||||||
* @param {Object} tooltip - The tooltip definition to validate
|
* @param {Object} tooltip - The tooltip definition to validate
|
||||||
* @returns {Object} { valid: boolean, errors: string[] }
|
* @returns {Object} { valid: boolean, errors: string[] }
|
||||||
*/
|
*/
|
||||||
function validateTooltipDefinition(tooltip) {
|
function validateTooltipDefinition(tooltip) {
|
||||||
const errors = [];
|
const errors = [];
|
||||||
|
|
||||||
// Required fields
|
// Required fields
|
||||||
if (!tooltip.id || typeof tooltip.id !== 'string') {
|
if (!tooltip.id || typeof tooltip.id !== 'string') {
|
||||||
errors.push('Tooltip must have a valid string id');
|
errors.push('Tooltip must have a valid string id');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!tooltip.element) {
|
if (!tooltip.element) {
|
||||||
errors.push('Tooltip must have an element selector or HTMLElement');
|
errors.push('Tooltip must have an element selector or HTMLElement');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!tooltip.popover || typeof tooltip.popover !== 'object') {
|
if (!tooltip.popover || typeof tooltip.popover !== 'object') {
|
||||||
errors.push('Tooltip must have a popover object');
|
errors.push('Tooltip must have a popover object');
|
||||||
} else {
|
} else {
|
||||||
// Validate popover fields
|
// Validate popover fields
|
||||||
if (!tooltip.popover.title || typeof tooltip.popover.title !== 'string') {
|
if (!tooltip.popover.title || typeof tooltip.popover.title !== 'string') {
|
||||||
errors.push('Tooltip popover must have a valid string title');
|
errors.push('Tooltip popover must have a valid string title');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!tooltip.popover.description || typeof tooltip.popover.description !== 'string') {
|
if (!tooltip.popover.description || typeof tooltip.popover.description !== 'string') {
|
||||||
errors.push('Tooltip popover must have a valid string description');
|
errors.push('Tooltip popover must have a valid string description');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validate position if provided
|
// Validate position if provided
|
||||||
if (tooltip.popover.position) {
|
if (tooltip.popover.position) {
|
||||||
const validPositions = ['top', 'bottom', 'left', 'right', 'center'];
|
const validPositions = ['top', 'bottom', 'left', 'right', 'center'];
|
||||||
if (!validPositions.includes(tooltip.popover.position)) {
|
if (!validPositions.includes(tooltip.popover.position)) {
|
||||||
errors.push(`Invalid position: ${tooltip.popover.position}. Must be one of: ${validPositions.join(', ')}`);
|
errors.push(`Invalid position: ${tooltip.popover.position}. Must be one of: ${validPositions.join(', ')}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validate align if provided
|
// Validate align if provided
|
||||||
if (tooltip.popover.align) {
|
if (tooltip.popover.align) {
|
||||||
const validAligns = ['start', 'center', 'end'];
|
const validAligns = ['start', 'center', 'end'];
|
||||||
if (!validAligns.includes(tooltip.popover.align)) {
|
if (!validAligns.includes(tooltip.popover.align)) {
|
||||||
errors.push(`Invalid align: ${tooltip.popover.align}. Must be one of: ${validAligns.join(', ')}`);
|
errors.push(`Invalid align: ${tooltip.popover.align}. Must be one of: ${validAligns.join(', ')}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validate showButtons if provided
|
// Validate showButtons if provided
|
||||||
if (tooltip.popover.showButtons && !Array.isArray(tooltip.popover.showButtons)) {
|
if (tooltip.popover.showButtons && !Array.isArray(tooltip.popover.showButtons)) {
|
||||||
errors.push('showButtons must be an array');
|
errors.push('showButtons must be an array');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validate callbacks if provided
|
// Validate callbacks if provided
|
||||||
const callbacks = ['onNext', 'onPrevious', 'onClose', 'onSetupNow', 'onLater'];
|
const callbacks = ['onNext', 'onPrevious', 'onClose', 'onSetupNow', 'onLater'];
|
||||||
callbacks.forEach(callback => {
|
callbacks.forEach(callback => {
|
||||||
if (tooltip.popover[callback] && typeof tooltip.popover[callback] !== 'function') {
|
if (tooltip.popover[callback] && typeof tooltip.popover[callback] !== 'function') {
|
||||||
errors.push(`${callback} must be a function`);
|
errors.push(`${callback} must be a function`);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validate condition if provided
|
// Validate condition if provided
|
||||||
if (tooltip.condition && typeof tooltip.condition !== 'function') {
|
if (tooltip.condition && typeof tooltip.condition !== 'function') {
|
||||||
errors.push('condition must be a function');
|
errors.push('condition must be a function');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validate priority if provided
|
// Validate priority if provided
|
||||||
if (tooltip.priority !== undefined && typeof tooltip.priority !== 'number') {
|
if (tooltip.priority !== undefined && typeof tooltip.priority !== 'number') {
|
||||||
errors.push('priority must be a number');
|
errors.push('priority must be a number');
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
valid: errors.length === 0,
|
valid: errors.length === 0,
|
||||||
errors
|
errors
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Validate an array of tooltip definitions
|
* Validate an array of tooltip definitions
|
||||||
* @param {Array} tooltips - Array of tooltip definitions
|
* @param {Array} tooltips - Array of tooltip definitions
|
||||||
* @returns {Object} { valid: boolean, errors: Object[] }
|
* @returns {Object} { valid: boolean, errors: Object[] }
|
||||||
*/
|
*/
|
||||||
function validateTooltipDefinitions(tooltips) {
|
function validateTooltipDefinitions(tooltips) {
|
||||||
if (!Array.isArray(tooltips)) {
|
if (!Array.isArray(tooltips)) {
|
||||||
return {
|
return {
|
||||||
valid: false,
|
valid: false,
|
||||||
errors: [{ tooltip: null, errors: ['tooltips must be an array'] }]
|
errors: [{ tooltip: null, errors: ['tooltips must be an array'] }]
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const allErrors = [];
|
const allErrors = [];
|
||||||
const ids = new Set();
|
const ids = new Set();
|
||||||
|
|
||||||
tooltips.forEach((tooltip, index) => {
|
tooltips.forEach((tooltip, index) => {
|
||||||
const validation = validateTooltipDefinition(tooltip);
|
const validation = validateTooltipDefinition(tooltip);
|
||||||
|
|
||||||
if (!validation.valid) {
|
if (!validation.valid) {
|
||||||
allErrors.push({
|
allErrors.push({
|
||||||
tooltip: tooltip.id || `index ${index}`,
|
tooltip: tooltip.id || `index ${index}`,
|
||||||
errors: validation.errors
|
errors: validation.errors
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check for duplicate IDs
|
// Check for duplicate IDs
|
||||||
if (tooltip.id) {
|
if (tooltip.id) {
|
||||||
if (ids.has(tooltip.id)) {
|
if (ids.has(tooltip.id)) {
|
||||||
allErrors.push({
|
allErrors.push({
|
||||||
tooltip: tooltip.id,
|
tooltip: tooltip.id,
|
||||||
errors: [`Duplicate tooltip ID: ${tooltip.id}`]
|
errors: [`Duplicate tooltip ID: ${tooltip.id}`]
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
ids.add(tooltip.id);
|
ids.add(tooltip.id);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
valid: allErrors.length === 0,
|
valid: allErrors.length === 0,
|
||||||
errors: allErrors
|
errors: allErrors
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Error handler for tooltip system
|
* Error handler for tooltip system
|
||||||
*/
|
*/
|
||||||
class TooltipError extends Error {
|
class TooltipError extends Error {
|
||||||
constructor(message, tooltipId = null) {
|
constructor(message, tooltipId = null) {
|
||||||
super(message);
|
super(message);
|
||||||
this.name = 'TooltipError';
|
this.name = 'TooltipError';
|
||||||
this.tooltipId = tooltipId;
|
this.tooltipId = tooltipId;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handle tooltip definition errors
|
* Handle tooltip definition errors
|
||||||
* @param {Object} validation - Validation result
|
* @param {Object} validation - Validation result
|
||||||
* @throws {TooltipError} If validation fails
|
* @throws {TooltipError} If validation fails
|
||||||
*/
|
*/
|
||||||
function handleValidationErrors(validation) {
|
function handleValidationErrors(validation) {
|
||||||
if (!validation.valid) {
|
if (!validation.valid) {
|
||||||
const errorMessages = validation.errors.map(e =>
|
const errorMessages = validation.errors.map(e =>
|
||||||
`${e.tooltip}: ${e.errors.join(', ')}`
|
`${e.tooltip}: ${e.errors.join(', ')}`
|
||||||
).join('\n');
|
).join('\n');
|
||||||
|
|
||||||
console.error('[TooltipDefinitions] Validation errors:', errorMessages);
|
console.error('[TooltipDefinitions] Validation errors:', errorMessages);
|
||||||
throw new TooltipError(`Tooltip validation failed:\n${errorMessages}`);
|
throw new TooltipError(`Tooltip validation failed:\n${errorMessages}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Export to global scope
|
// Export to global scope
|
||||||
window.TooltipValidation = {
|
window.TooltipValidation = {
|
||||||
validateTooltipDefinition,
|
validateTooltipDefinition,
|
||||||
validateTooltipDefinitions,
|
validateTooltipDefinitions,
|
||||||
handleValidationErrors,
|
handleValidationErrors,
|
||||||
TooltipError
|
TooltipError
|
||||||
};
|
};
|
||||||
|
|
||||||
console.log('[TooltipDefinitions] Validation module loaded');
|
console.log('[TooltipDefinitions] Validation module loaded');
|
||||||
|
|
||||||
})(window);
|
})(window);
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Tooltip Definitions Array
|
* Tooltip Definitions Array
|
||||||
* Defines all tooltips for the onboarding tour
|
* Defines all tooltips for the onboarding tour
|
||||||
*/
|
*/
|
||||||
const TOOLTIP_DEFINITIONS = [
|
const TOOLTIP_DEFINITIONS = [
|
||||||
// 1. Welcome tooltip pointing to logo
|
// 1. Welcome tooltip pointing to logo
|
||||||
{
|
{
|
||||||
id: 'welcome',
|
id: 'welcome',
|
||||||
element: '#brand',
|
element: '#brand',
|
||||||
popover: {
|
popover: {
|
||||||
title: 'Welcome to DashCaddy!',
|
title: 'Welcome to DashCaddy!',
|
||||||
description: `
|
description: `
|
||||||
<p>Your personal dashboard for managing services with Caddy reverse proxy.</p>
|
<p>Your personal dashboard for managing services with Caddy reverse proxy.</p>
|
||||||
<p>Let's take a quick tour to help you get started.</p>
|
<p>Let's take a quick tour to help you get started.</p>
|
||||||
<p style="margin-top: 8px; font-size: 0.85rem; opacity: 0.8;">Tip: You can customize this logo in Settings.</p>
|
<p style="margin-top: 8px; font-size: 0.85rem; opacity: 0.8;">Tip: You can customize this logo in Settings.</p>
|
||||||
`,
|
`,
|
||||||
position: 'bottom',
|
position: 'bottom',
|
||||||
align: 'start',
|
align: 'start',
|
||||||
showButtons: ['next'],
|
showButtons: ['next'],
|
||||||
showProgress: true
|
showProgress: true
|
||||||
},
|
},
|
||||||
priority: 1,
|
priority: 1,
|
||||||
isNewFeature: false
|
isNewFeature: false
|
||||||
},
|
},
|
||||||
|
|
||||||
// 2. Add Service button
|
// 2. Add Service button
|
||||||
{
|
{
|
||||||
id: 'add-service',
|
id: 'add-service',
|
||||||
element: '#add-service-btn',
|
element: '#add-service-btn',
|
||||||
popover: {
|
popover: {
|
||||||
title: 'Adding New Services',
|
title: 'Adding New Services',
|
||||||
description: `
|
description: `
|
||||||
<p>Click <strong>+ Add Service</strong> to deploy new apps or add existing services to your dashboard.</p>
|
<p>Click <strong>+ Add Service</strong> to deploy new apps or add existing services to your dashboard.</p>
|
||||||
<p>Choose from 50+ templates including:</p>
|
<p>Choose from 50+ templates including:</p>
|
||||||
<ul>
|
<ul>
|
||||||
<li>Media servers (Plex, Jellyfin, Emby)</li>
|
<li>Media servers (Plex, Jellyfin, Emby)</li>
|
||||||
<li>Download managers (qBittorrent, Transmission)</li>
|
<li>Download managers (qBittorrent, Transmission)</li>
|
||||||
<li>DNS servers (Technitium, Pi-hole)</li>
|
<li>DNS servers (Technitium, Pi-hole)</li>
|
||||||
</ul>
|
</ul>
|
||||||
`,
|
`,
|
||||||
position: 'bottom',
|
position: 'bottom',
|
||||||
showButtons: ['previous', 'next'],
|
showButtons: ['previous', 'next'],
|
||||||
showProgress: true
|
showProgress: true
|
||||||
},
|
},
|
||||||
priority: 2,
|
priority: 2,
|
||||||
isNewFeature: false,
|
isNewFeature: false,
|
||||||
condition: () => {
|
condition: () => {
|
||||||
return document.getElementById('add-service-btn') !== null;
|
return document.getElementById('add-service-btn') !== null;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
// 3. App Grid explanation
|
// 3. App Grid explanation
|
||||||
{
|
{
|
||||||
id: 'app-grid',
|
id: 'app-grid',
|
||||||
element: '#cards',
|
element: '#cards',
|
||||||
popover: {
|
popover: {
|
||||||
title: 'Your Services',
|
title: 'Your Services',
|
||||||
description: `
|
description: `
|
||||||
<p>This is your service grid where all your deployed applications appear.</p>
|
<p>This is your service grid where all your deployed applications appear.</p>
|
||||||
<p>Each card shows:</p>
|
<p>Each card shows:</p>
|
||||||
<ul>
|
<ul>
|
||||||
<li>Service status (online/offline)</li>
|
<li>Service status (online/offline)</li>
|
||||||
<li>Response time</li>
|
<li>Response time</li>
|
||||||
<li>Quick actions (restart, open, logs, settings)</li>
|
<li>Quick actions (restart, open, logs, settings)</li>
|
||||||
</ul>
|
</ul>
|
||||||
`,
|
`,
|
||||||
position: 'top',
|
position: 'top',
|
||||||
showButtons: ['previous', 'next'],
|
showButtons: ['previous', 'next'],
|
||||||
showProgress: true
|
showProgress: true
|
||||||
},
|
},
|
||||||
priority: 3,
|
priority: 3,
|
||||||
isNewFeature: false
|
isNewFeature: false
|
||||||
},
|
},
|
||||||
|
|
||||||
// 4. Theme selector
|
// 4. Theme selector
|
||||||
{
|
{
|
||||||
id: 'theme-selector',
|
id: 'theme-selector',
|
||||||
element: '#theme',
|
element: '#theme',
|
||||||
popover: {
|
popover: {
|
||||||
title: 'Customize Your Theme',
|
title: 'Customize Your Theme',
|
||||||
description: `
|
description: `
|
||||||
<p>DashCaddy comes with 7 themes. Click here to switch between them.</p>
|
<p>DashCaddy comes with 7 themes. Click here to switch between them.</p>
|
||||||
<p>Your preference is saved automatically.</p>
|
<p>Your preference is saved automatically.</p>
|
||||||
`,
|
`,
|
||||||
position: 'bottom',
|
position: 'bottom',
|
||||||
showButtons: ['previous', 'close'],
|
showButtons: ['previous', 'close'],
|
||||||
showProgress: true
|
showProgress: true
|
||||||
},
|
},
|
||||||
priority: 4,
|
priority: 4,
|
||||||
isNewFeature: false,
|
isNewFeature: false,
|
||||||
condition: () => {
|
condition: () => {
|
||||||
return document.getElementById('theme') !== null;
|
return document.getElementById('theme') !== null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get tooltip definitions
|
* Get tooltip definitions
|
||||||
* @returns {Array} Array of tooltip definitions
|
* @returns {Array} Array of tooltip definitions
|
||||||
*/
|
*/
|
||||||
function getTooltipDefinitions() {
|
function getTooltipDefinitions() {
|
||||||
return TOOLTIP_DEFINITIONS;
|
return TOOLTIP_DEFINITIONS;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get a specific tooltip by ID
|
* Get a specific tooltip by ID
|
||||||
* @param {string} id - Tooltip ID
|
* @param {string} id - Tooltip ID
|
||||||
* @returns {Object|null} Tooltip definition or null if not found
|
* @returns {Object|null} Tooltip definition or null if not found
|
||||||
*/
|
*/
|
||||||
function getTooltipById(id) {
|
function getTooltipById(id) {
|
||||||
return TOOLTIP_DEFINITIONS.find(t => t.id === id) || null;
|
return TOOLTIP_DEFINITIONS.find(t => t.id === id) || null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get tooltips filtered by condition
|
* Get tooltips filtered by condition
|
||||||
* @returns {Array} Array of tooltips that pass their condition check
|
* @returns {Array} Array of tooltips that pass their condition check
|
||||||
*/
|
*/
|
||||||
function getActiveTooltips() {
|
function getActiveTooltips() {
|
||||||
return TOOLTIP_DEFINITIONS.filter(tooltip => {
|
return TOOLTIP_DEFINITIONS.filter(tooltip => {
|
||||||
if (tooltip.condition && typeof tooltip.condition === 'function') {
|
if (tooltip.condition && typeof tooltip.condition === 'function') {
|
||||||
try {
|
try {
|
||||||
return tooltip.condition();
|
return tooltip.condition();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`[TooltipDefinitions] Error evaluating condition for ${tooltip.id}:`, error);
|
console.error(`[TooltipDefinitions] Error evaluating condition for ${tooltip.id}:`, error);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get tooltips sorted by priority
|
* Get tooltips sorted by priority
|
||||||
* @returns {Array} Array of tooltips sorted by priority (ascending)
|
* @returns {Array} Array of tooltips sorted by priority (ascending)
|
||||||
*/
|
*/
|
||||||
function getSortedTooltips() {
|
function getSortedTooltips() {
|
||||||
const tooltips = getActiveTooltips();
|
const tooltips = getActiveTooltips();
|
||||||
return tooltips.sort((a, b) => {
|
return tooltips.sort((a, b) => {
|
||||||
const priorityA = a.priority || 999;
|
const priorityA = a.priority || 999;
|
||||||
const priorityB = b.priority || 999;
|
const priorityB = b.priority || 999;
|
||||||
return priorityA - priorityB;
|
return priorityA - priorityB;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get tooltips marked as new features
|
* Get tooltips marked as new features
|
||||||
* @returns {Array} Array of tooltips marked with isNewFeature flag
|
* @returns {Array} Array of tooltips marked with isNewFeature flag
|
||||||
*/
|
*/
|
||||||
function getNewFeatureTooltips() {
|
function getNewFeatureTooltips() {
|
||||||
const tooltips = getActiveTooltips();
|
const tooltips = getActiveTooltips();
|
||||||
return tooltips.filter(tooltip => tooltip.isNewFeature === true)
|
return tooltips.filter(tooltip => tooltip.isNewFeature === true)
|
||||||
.sort((a, b) => {
|
.sort((a, b) => {
|
||||||
const priorityA = a.priority || 999;
|
const priorityA = a.priority || 999;
|
||||||
const priorityB = b.priority || 999;
|
const priorityB = b.priority || 999;
|
||||||
return priorityA - priorityB;
|
return priorityA - priorityB;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Export to global scope
|
// Export to global scope
|
||||||
window.TooltipDefinitions = {
|
window.TooltipDefinitions = {
|
||||||
TOOLTIP_DEFINITIONS,
|
TOOLTIP_DEFINITIONS,
|
||||||
getTooltipDefinitions,
|
getTooltipDefinitions,
|
||||||
getTooltipById,
|
getTooltipById,
|
||||||
getActiveTooltips,
|
getActiveTooltips,
|
||||||
getSortedTooltips,
|
getSortedTooltips,
|
||||||
getNewFeatureTooltips
|
getNewFeatureTooltips
|
||||||
};
|
};
|
||||||
|
|
||||||
console.log('[TooltipDefinitions] Definitions loaded:', TOOLTIP_DEFINITIONS.length, 'tooltips');
|
console.log('[TooltipDefinitions] Definitions loaded:', TOOLTIP_DEFINITIONS.length, 'tooltips');
|
||||||
|
|
||||||
|
|||||||
@@ -1,363 +1,363 @@
|
|||||||
/**
|
/**
|
||||||
* Tour Manager
|
* Tour Manager
|
||||||
* Orchestrates the onboarding tour using Driver.js
|
* Orchestrates the onboarding tour using Driver.js
|
||||||
*/
|
*/
|
||||||
|
|
||||||
(function(window) {
|
(function(window) {
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
class TourManager {
|
class TourManager {
|
||||||
constructor(progressTracker, themeAdapter, dnsTemplateSelector) {
|
constructor(progressTracker, themeAdapter, dnsTemplateSelector) {
|
||||||
this.progressTracker = progressTracker;
|
this.progressTracker = progressTracker;
|
||||||
this.themeAdapter = themeAdapter;
|
this.themeAdapter = themeAdapter;
|
||||||
this.dnsTemplateSelector = dnsTemplateSelector;
|
this.dnsTemplateSelector = dnsTemplateSelector;
|
||||||
this.driver = null;
|
this.driver = null;
|
||||||
this.currentStepIndex = 0;
|
this.currentStepIndex = 0;
|
||||||
this.isActive = false;
|
this.isActive = false;
|
||||||
this.resizeHandler = null;
|
this.resizeHandler = null;
|
||||||
this.layoutChangeHandler = null;
|
this.layoutChangeHandler = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Initialize Driver.js with theme-aware configuration
|
* Initialize Driver.js with theme-aware configuration
|
||||||
*/
|
*/
|
||||||
async initializeDriver() {
|
async initializeDriver() {
|
||||||
// Driver.js v1.x IIFE: window.driver.js.driver is the factory function
|
// Driver.js v1.x IIFE: window.driver.js.driver is the factory function
|
||||||
const driverFactory = window.driver?.js?.driver || window.driver?.driver || window.driver;
|
const driverFactory = window.driver?.js?.driver || window.driver?.driver || window.driver;
|
||||||
|
|
||||||
if (typeof driverFactory !== 'function') {
|
if (typeof driverFactory !== 'function') {
|
||||||
console.error('[TourManager] Driver.js not loaded or invalid. window.driver:', window.driver);
|
console.error('[TourManager] Driver.js not loaded or invalid. window.driver:', window.driver);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
const themeConfig = this.themeAdapter.getDriverTheme();
|
const themeConfig = this.themeAdapter.getDriverTheme();
|
||||||
|
|
||||||
this.driver = driverFactory({
|
this.driver = driverFactory({
|
||||||
showProgress: true,
|
showProgress: true,
|
||||||
showButtons: ['next', 'previous', 'close'],
|
showButtons: ['next', 'previous', 'close'],
|
||||||
allowClose: true,
|
allowClose: true,
|
||||||
overlayClickNext: false,
|
overlayClickNext: false,
|
||||||
overlayOpacity: 0,
|
overlayOpacity: 0,
|
||||||
stagePadding: 0,
|
stagePadding: 0,
|
||||||
stageRadius: 0,
|
stageRadius: 0,
|
||||||
allowKeyboardControl: true,
|
allowKeyboardControl: true,
|
||||||
popoverClass: 'dashcaddy-popover',
|
popoverClass: 'dashcaddy-popover',
|
||||||
onDestroyed: () => this.onTourComplete(),
|
onDestroyed: () => this.onTourComplete(),
|
||||||
onDestroyStarted: () => {
|
onDestroyStarted: () => {
|
||||||
if (!this.progressTracker.isTourCompleted()) {
|
if (!this.progressTracker.isTourCompleted()) {
|
||||||
this.onTourSkip();
|
this.onTourSkip();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Apply theme
|
// Apply theme
|
||||||
this.themeAdapter.applyTheme(this.driver);
|
this.themeAdapter.applyTheme(this.driver);
|
||||||
|
|
||||||
// Listen for theme changes
|
// Listen for theme changes
|
||||||
this.themeAdapter.onThemeChange(() => {
|
this.themeAdapter.onThemeChange(() => {
|
||||||
this.themeAdapter.applyTheme(this.driver);
|
this.themeAdapter.applyTheme(this.driver);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Set up dynamic repositioning
|
// Set up dynamic repositioning
|
||||||
this.setupDynamicRepositioning();
|
this.setupDynamicRepositioning();
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check if tour should auto-start
|
* Check if tour should auto-start
|
||||||
*/
|
*/
|
||||||
shouldAutoStart() {
|
shouldAutoStart() {
|
||||||
return !this.progressTracker.isTourCompleted() &&
|
return !this.progressTracker.isTourCompleted() &&
|
||||||
this.progressTracker.getCurrentStep() === 0;
|
this.progressTracker.getCurrentStep() === 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Start the onboarding tour
|
* Start the onboarding tour
|
||||||
*/
|
*/
|
||||||
async startTour() {
|
async startTour() {
|
||||||
if (!this.driver) {
|
if (!this.driver) {
|
||||||
const initialized = await this.initializeDriver();
|
const initialized = await this.initializeDriver();
|
||||||
if (!initialized) return;
|
if (!initialized) return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get active tooltips (filtered by conditions)
|
// Get active tooltips (filtered by conditions)
|
||||||
const allTooltips = window.TooltipDefinitions.getSortedTooltips();
|
const allTooltips = window.TooltipDefinitions.getSortedTooltips();
|
||||||
|
|
||||||
// Filter out completed tooltips
|
// Filter out completed tooltips
|
||||||
const completedIds = this.progressTracker.getCompletedTooltips();
|
const completedIds = this.progressTracker.getCompletedTooltips();
|
||||||
const activeTooltips = allTooltips.filter(t => !completedIds.includes(t.id));
|
const activeTooltips = allTooltips.filter(t => !completedIds.includes(t.id));
|
||||||
|
|
||||||
if (activeTooltips.length === 0) {
|
if (activeTooltips.length === 0) {
|
||||||
console.log('[TourManager] No tooltips to show');
|
console.log('[TourManager] No tooltips to show');
|
||||||
this.progressTracker.markTourCompleted();
|
this.progressTracker.markTourCompleted();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Convert to Driver.js steps with navigation logic
|
// Convert to Driver.js steps with navigation logic
|
||||||
const steps = activeTooltips.map((tooltip, index) => {
|
const steps = activeTooltips.map((tooltip, index) => {
|
||||||
const isFirst = index === 0;
|
const isFirst = index === 0;
|
||||||
const isLast = index === activeTooltips.length - 1;
|
const isLast = index === activeTooltips.length - 1;
|
||||||
|
|
||||||
const step = {
|
const step = {
|
||||||
element: tooltip.element,
|
element: tooltip.element,
|
||||||
popover: {
|
popover: {
|
||||||
title: tooltip.popover.title,
|
title: tooltip.popover.title,
|
||||||
description: tooltip.popover.description,
|
description: tooltip.popover.description,
|
||||||
side: tooltip.popover.position || 'bottom',
|
side: tooltip.popover.position || 'bottom',
|
||||||
align: tooltip.popover.align || 'start',
|
align: tooltip.popover.align || 'start',
|
||||||
showButtons: this._getButtonsForStep(tooltip, isFirst, isLast),
|
showButtons: this._getButtonsForStep(tooltip, isFirst, isLast),
|
||||||
showProgress: tooltip.popover.showProgress !== false,
|
showProgress: tooltip.popover.showProgress !== false,
|
||||||
onNextClick: () => {
|
onNextClick: () => {
|
||||||
this.progressTracker.markTooltipCompleted(tooltip.id);
|
this.progressTracker.markTooltipCompleted(tooltip.id);
|
||||||
this.progressTracker.setCurrentStep(index + 1);
|
this.progressTracker.setCurrentStep(index + 1);
|
||||||
this.currentStepIndex = index + 1;
|
this.currentStepIndex = index + 1;
|
||||||
this.driver.moveNext();
|
this.driver.moveNext();
|
||||||
},
|
},
|
||||||
onPrevClick: () => {
|
onPrevClick: () => {
|
||||||
this.progressTracker.setCurrentStep(Math.max(0, index - 1));
|
this.progressTracker.setCurrentStep(Math.max(0, index - 1));
|
||||||
this.currentStepIndex = Math.max(0, index - 1);
|
this.currentStepIndex = Math.max(0, index - 1);
|
||||||
this.driver.movePrevious();
|
this.driver.movePrevious();
|
||||||
},
|
},
|
||||||
onCloseClick: () => {
|
onCloseClick: () => {
|
||||||
this.skipTour();
|
this.skipTour();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Add custom handlers for DNS tooltip
|
// Add custom handlers for DNS tooltip
|
||||||
if (tooltip.id === 'dns-priority' && this.dnsTemplateSelector) {
|
if (tooltip.id === 'dns-priority' && this.dnsTemplateSelector) {
|
||||||
step.popover.onSetupNowClick = () => {
|
step.popover.onSetupNowClick = () => {
|
||||||
console.log('[TourManager] Opening DNS template selector');
|
console.log('[TourManager] Opening DNS template selector');
|
||||||
this.dnsTemplateSelector.showTemplateSelector();
|
this.dnsTemplateSelector.showTemplateSelector();
|
||||||
// Mark tooltip as completed and move to next
|
// Mark tooltip as completed and move to next
|
||||||
this.progressTracker.markTooltipCompleted(tooltip.id);
|
this.progressTracker.markTooltipCompleted(tooltip.id);
|
||||||
this.progressTracker.setCurrentStep(index + 1);
|
this.progressTracker.setCurrentStep(index + 1);
|
||||||
this.currentStepIndex = index + 1;
|
this.currentStepIndex = index + 1;
|
||||||
this.driver.moveNext();
|
this.driver.moveNext();
|
||||||
};
|
};
|
||||||
|
|
||||||
step.popover.onLaterClick = () => {
|
step.popover.onLaterClick = () => {
|
||||||
console.log('[TourManager] DNS setup deferred');
|
console.log('[TourManager] DNS setup deferred');
|
||||||
this.progressTracker.markDnsSetupDeferred();
|
this.progressTracker.markDnsSetupDeferred();
|
||||||
// Mark tooltip as completed and move to next
|
// Mark tooltip as completed and move to next
|
||||||
this.progressTracker.markTooltipCompleted(tooltip.id);
|
this.progressTracker.markTooltipCompleted(tooltip.id);
|
||||||
this.progressTracker.setCurrentStep(index + 1);
|
this.progressTracker.setCurrentStep(index + 1);
|
||||||
this.currentStepIndex = index + 1;
|
this.currentStepIndex = index + 1;
|
||||||
this.driver.moveNext();
|
this.driver.moveNext();
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
return step;
|
return step;
|
||||||
});
|
});
|
||||||
|
|
||||||
this.isActive = true;
|
this.isActive = true;
|
||||||
this.driver.setSteps(steps);
|
this.driver.setSteps(steps);
|
||||||
this.driver.drive();
|
this.driver.drive();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Resume tour from last step
|
* Resume tour from last step
|
||||||
*/
|
*/
|
||||||
async resumeTour() {
|
async resumeTour() {
|
||||||
const currentStep = this.progressTracker.getCurrentStep();
|
const currentStep = this.progressTracker.getCurrentStep();
|
||||||
if (currentStep > 0) {
|
if (currentStep > 0) {
|
||||||
await this.startTour();
|
await this.startTour();
|
||||||
// Driver.js will start from beginning, we'd need to skip to current step
|
// Driver.js will start from beginning, we'd need to skip to current step
|
||||||
// This is a simplified implementation
|
// This is a simplified implementation
|
||||||
} else {
|
} else {
|
||||||
await this.startTour();
|
await this.startTour();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Skip the entire tour
|
* Skip the entire tour
|
||||||
*/
|
*/
|
||||||
skipTour() {
|
skipTour() {
|
||||||
if (this.driver) {
|
if (this.driver) {
|
||||||
this.driver.destroy();
|
this.driver.destroy();
|
||||||
}
|
}
|
||||||
this.cleanupDynamicRepositioning();
|
this.cleanupDynamicRepositioning();
|
||||||
this.isActive = false;
|
this.isActive = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Restart tour from beginning
|
* Restart tour from beginning
|
||||||
*/
|
*/
|
||||||
async restartTour() {
|
async restartTour() {
|
||||||
this.progressTracker.resetProgress();
|
this.progressTracker.resetProgress();
|
||||||
await this.startTour();
|
await this.startTour();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Show a specific tooltip by ID
|
* Show a specific tooltip by ID
|
||||||
*/
|
*/
|
||||||
async showTooltip(tooltipId) {
|
async showTooltip(tooltipId) {
|
||||||
const tooltip = window.TooltipDefinitions.getTooltipById(tooltipId);
|
const tooltip = window.TooltipDefinitions.getTooltipById(tooltipId);
|
||||||
if (!tooltip) {
|
if (!tooltip) {
|
||||||
console.error(`[TourManager] Tooltip not found: ${tooltipId}`);
|
console.error(`[TourManager] Tooltip not found: ${tooltipId}`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!this.driver) {
|
if (!this.driver) {
|
||||||
await this.initializeDriver();
|
await this.initializeDriver();
|
||||||
}
|
}
|
||||||
|
|
||||||
const step = {
|
const step = {
|
||||||
element: tooltip.element,
|
element: tooltip.element,
|
||||||
popover: {
|
popover: {
|
||||||
title: tooltip.popover.title,
|
title: tooltip.popover.title,
|
||||||
description: tooltip.popover.description,
|
description: tooltip.popover.description,
|
||||||
side: tooltip.popover.position || 'bottom',
|
side: tooltip.popover.position || 'bottom',
|
||||||
align: tooltip.popover.align || 'start'
|
align: tooltip.popover.align || 'start'
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
this.driver.highlight(step);
|
this.driver.highlight(step);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Show "What's New" tour - only tooltips marked as new features
|
* Show "What's New" tour - only tooltips marked as new features
|
||||||
*/
|
*/
|
||||||
async showWhatsNew() {
|
async showWhatsNew() {
|
||||||
if (!this.driver) {
|
if (!this.driver) {
|
||||||
const initialized = await this.initializeDriver();
|
const initialized = await this.initializeDriver();
|
||||||
if (!initialized) return;
|
if (!initialized) return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get only new feature tooltips
|
// Get only new feature tooltips
|
||||||
const newFeatureTooltips = window.TooltipDefinitions.getNewFeatureTooltips();
|
const newFeatureTooltips = window.TooltipDefinitions.getNewFeatureTooltips();
|
||||||
|
|
||||||
if (newFeatureTooltips.length === 0) {
|
if (newFeatureTooltips.length === 0) {
|
||||||
console.log('[TourManager] No new features to show');
|
console.log('[TourManager] No new features to show');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(`[TourManager] Showing ${newFeatureTooltips.length} new features`);
|
console.log(`[TourManager] Showing ${newFeatureTooltips.length} new features`);
|
||||||
|
|
||||||
// Convert to Driver.js steps
|
// Convert to Driver.js steps
|
||||||
const steps = newFeatureTooltips.map((tooltip, index) => {
|
const steps = newFeatureTooltips.map((tooltip, index) => {
|
||||||
const isFirst = index === 0;
|
const isFirst = index === 0;
|
||||||
const isLast = index === newFeatureTooltips.length - 1;
|
const isLast = index === newFeatureTooltips.length - 1;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
element: tooltip.element,
|
element: tooltip.element,
|
||||||
popover: {
|
popover: {
|
||||||
title: `✨ NEW: ${tooltip.popover.title}`,
|
title: `✨ NEW: ${tooltip.popover.title}`,
|
||||||
description: tooltip.popover.description,
|
description: tooltip.popover.description,
|
||||||
side: tooltip.popover.position || 'bottom',
|
side: tooltip.popover.position || 'bottom',
|
||||||
align: tooltip.popover.align || 'start',
|
align: tooltip.popover.align || 'start',
|
||||||
showButtons: this._getButtonsForStep(tooltip, isFirst, isLast),
|
showButtons: this._getButtonsForStep(tooltip, isFirst, isLast),
|
||||||
showProgress: true,
|
showProgress: true,
|
||||||
onNextClick: () => {
|
onNextClick: () => {
|
||||||
this.driver.moveNext();
|
this.driver.moveNext();
|
||||||
},
|
},
|
||||||
onPrevClick: () => {
|
onPrevClick: () => {
|
||||||
this.driver.movePrevious();
|
this.driver.movePrevious();
|
||||||
},
|
},
|
||||||
onCloseClick: () => {
|
onCloseClick: () => {
|
||||||
this.skipTour();
|
this.skipTour();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
this.isActive = true;
|
this.isActive = true;
|
||||||
this.driver.setSteps(steps);
|
this.driver.setSteps(steps);
|
||||||
this.driver.drive();
|
this.driver.drive();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Set up dynamic repositioning for window resize and layout changes
|
* Set up dynamic repositioning for window resize and layout changes
|
||||||
*/
|
*/
|
||||||
setupDynamicRepositioning() {
|
setupDynamicRepositioning() {
|
||||||
// Window resize handler with debouncing
|
// Window resize handler with debouncing
|
||||||
let resizeTimeout;
|
let resizeTimeout;
|
||||||
this.resizeHandler = () => {
|
this.resizeHandler = () => {
|
||||||
clearTimeout(resizeTimeout);
|
clearTimeout(resizeTimeout);
|
||||||
resizeTimeout = setTimeout(() => {
|
resizeTimeout = setTimeout(() => {
|
||||||
if (this.isActive && this.driver) {
|
if (this.isActive && this.driver) {
|
||||||
console.log('[TourManager] Window resized, repositioning tooltip');
|
console.log('[TourManager] Window resized, repositioning tooltip');
|
||||||
this.driver.refresh();
|
this.driver.refresh();
|
||||||
}
|
}
|
||||||
}, 150); // Debounce for 150ms
|
}, 150); // Debounce for 150ms
|
||||||
};
|
};
|
||||||
|
|
||||||
// Layout change handler (for theme changes, DOM mutations)
|
// Layout change handler (for theme changes, DOM mutations)
|
||||||
this.layoutChangeHandler = () => {
|
this.layoutChangeHandler = () => {
|
||||||
if (this.isActive && this.driver) {
|
if (this.isActive && this.driver) {
|
||||||
console.log('[TourManager] Layout changed, repositioning tooltip');
|
console.log('[TourManager] Layout changed, repositioning tooltip');
|
||||||
// Small delay to allow layout to settle
|
// Small delay to allow layout to settle
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
if (this.driver) {
|
if (this.driver) {
|
||||||
this.driver.refresh();
|
this.driver.refresh();
|
||||||
}
|
}
|
||||||
}, 100);
|
}, 100);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Add event listeners
|
// Add event listeners
|
||||||
window.addEventListener('resize', this.resizeHandler);
|
window.addEventListener('resize', this.resizeHandler);
|
||||||
|
|
||||||
// Listen for theme changes (already handled by ThemeAdapter, but also trigger reposition)
|
// Listen for theme changes (already handled by ThemeAdapter, but also trigger reposition)
|
||||||
this.themeAdapter.onThemeChange(this.layoutChangeHandler);
|
this.themeAdapter.onThemeChange(this.layoutChangeHandler);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Clean up dynamic repositioning listeners
|
* Clean up dynamic repositioning listeners
|
||||||
*/
|
*/
|
||||||
cleanupDynamicRepositioning() {
|
cleanupDynamicRepositioning() {
|
||||||
if (this.resizeHandler) {
|
if (this.resizeHandler) {
|
||||||
window.removeEventListener('resize', this.resizeHandler);
|
window.removeEventListener('resize', this.resizeHandler);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get buttons to show for a specific step
|
* Get buttons to show for a specific step
|
||||||
* @private
|
* @private
|
||||||
*/
|
*/
|
||||||
_getButtonsForStep(tooltip, isFirst, isLast) {
|
_getButtonsForStep(tooltip, isFirst, isLast) {
|
||||||
// Check if tooltip has custom buttons defined
|
// Check if tooltip has custom buttons defined
|
||||||
if (tooltip.popover.showButtons) {
|
if (tooltip.popover.showButtons) {
|
||||||
return tooltip.popover.showButtons;
|
return tooltip.popover.showButtons;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Default button configuration
|
// Default button configuration
|
||||||
const buttons = [];
|
const buttons = [];
|
||||||
|
|
||||||
if (!isFirst) {
|
if (!isFirst) {
|
||||||
buttons.push('previous');
|
buttons.push('previous');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!isLast) {
|
if (!isLast) {
|
||||||
buttons.push('next');
|
buttons.push('next');
|
||||||
} else {
|
} else {
|
||||||
buttons.push('close');
|
buttons.push('close');
|
||||||
}
|
}
|
||||||
|
|
||||||
return buttons;
|
return buttons;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handle tour completion
|
* Handle tour completion
|
||||||
*/
|
*/
|
||||||
onTourComplete() {
|
onTourComplete() {
|
||||||
this.progressTracker.markTourCompleted();
|
this.progressTracker.markTourCompleted();
|
||||||
this.isActive = false;
|
this.isActive = false;
|
||||||
console.log('[TourManager] Tour completed');
|
console.log('[TourManager] Tour completed');
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handle tour skip
|
* Handle tour skip
|
||||||
*/
|
*/
|
||||||
onTourSkip() {
|
onTourSkip() {
|
||||||
// Save current progress but don't mark as completed
|
// Save current progress but don't mark as completed
|
||||||
console.log('[TourManager] Tour skipped');
|
console.log('[TourManager] Tour skipped');
|
||||||
this.isActive = false;
|
this.isActive = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
window.TourManager = TourManager;
|
window.TourManager = TourManager;
|
||||||
console.log('[TourManager] Module loaded');
|
console.log('[TourManager] Module loaded');
|
||||||
|
|
||||||
})(window);
|
})(window);
|
||||||
|
|||||||
+49
-17
@@ -3,6 +3,12 @@ const path = require('path');
|
|||||||
const crypto = require('crypto');
|
const crypto = require('crypto');
|
||||||
const esbuild = require('esbuild');
|
const esbuild = require('esbuild');
|
||||||
|
|
||||||
|
// DC-119: single source of truth for the CRLF->LF normalization applied to
|
||||||
|
// every source read before minification (see the long comment in build()).
|
||||||
|
// Exported so tests/build-determinism.test.js pins THE ACTUAL regex, not a
|
||||||
|
// re-implementation that would silently drift if this one changes.
|
||||||
|
const normalizeSource = (s) => s.replace(/\r\n/g, '\n');
|
||||||
|
|
||||||
const JS = (...parts) => path.join(__dirname, 'js', ...parts);
|
const JS = (...parts) => path.join(__dirname, 'js', ...parts);
|
||||||
const DIST = path.join(__dirname, 'dist');
|
const DIST = path.join(__dirname, 'dist');
|
||||||
const INDEX_HTML = path.join(__dirname, 'index.html');
|
const INDEX_HTML = path.join(__dirname, 'index.html');
|
||||||
@@ -149,7 +155,20 @@ async function build() {
|
|||||||
console.warn(` WARN: ${path.relative(__dirname, file)} not found, skipping`);
|
console.warn(` WARN: ${path.relative(__dirname, file)} not found, skipping`);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
parts.push(fs.readFileSync(file, 'utf8'));
|
// DC-119: normalize CRLF -> LF before minifying. Root cause (verified
|
||||||
|
// empirically with esbuild 0.25.12 probes): the production transform
|
||||||
|
// uses sourcemap:'both', which base64-embeds the RAW source bytes as
|
||||||
|
// sourcesContent in the inline map — CR bytes survive into dist, so a
|
||||||
|
// CRLF working copy (Windows dev tree, core.autocrlf=true) and an LF
|
||||||
|
// checkout (DNS2) of the same commit produce different dist bytes and
|
||||||
|
// a different sw.js cache tag. With this normalization both are
|
||||||
|
// byte-identical (sha256-verified). Before it, every Linux rebuild of
|
||||||
|
// a Windows-committed bundle showed phantom drift on `git pull` in
|
||||||
|
// /opt/dashcaddy (the recurring "pre-pull drift" stashes — note those
|
||||||
|
// also contained minified-identifier renames, a second vector from
|
||||||
|
// esbuild version drift across the ^0.25.0 caret range, already
|
||||||
|
// pinned by package-lock.json).
|
||||||
|
parts.push(normalizeSource(fs.readFileSync(file, 'utf8')));
|
||||||
}
|
}
|
||||||
const concatenated = parts.join(';\n');
|
const concatenated = parts.join(';\n');
|
||||||
|
|
||||||
@@ -197,7 +216,11 @@ async function build() {
|
|||||||
// the SW's activate handler wipes all older caches, so users never get
|
// the SW's activate handler wipes all older caches, so users never get
|
||||||
// stuck on stale precached bundles after a release.
|
// stuck on stale precached bundles after a release.
|
||||||
function updateServiceWorkerCache() {
|
function updateServiceWorkerCache() {
|
||||||
const sw = fs.readFileSync(SW_JS, 'utf8');
|
// DC-119: normalize on read — same rationale as the bundle sources above.
|
||||||
|
// A CRLF sw.js would otherwise keep its CR bytes through the regex
|
||||||
|
// replace, so the written sw.js (and its committed bytes) would differ
|
||||||
|
// per-platform even with an identical cache tag.
|
||||||
|
const sw = normalizeSource(fs.readFileSync(SW_JS, 'utf8'));
|
||||||
const hash = crypto.createHash('sha256');
|
const hash = crypto.createHash('sha256');
|
||||||
for (const name of Object.keys(bundles)) {
|
for (const name of Object.keys(bundles)) {
|
||||||
hash.update(fs.readFileSync(path.join(DIST, name)));
|
hash.update(fs.readFileSync(path.join(DIST, name)));
|
||||||
@@ -214,20 +237,29 @@ function updateServiceWorkerCache() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Watch mode
|
// Watch mode
|
||||||
if (process.argv.includes('--watch')) {
|
// DC-119: only auto-run when invoked directly (`node build.js`). Requiring
|
||||||
console.log(' Watching for changes...\n');
|
// build.js as a module (as tests/build-determinism.test.js does, to pin the
|
||||||
build();
|
// normalizeSource regex) must NOT trigger a full dist rebuild.
|
||||||
|
if (require.main === module) {
|
||||||
|
if (process.argv.includes('--watch')) {
|
||||||
|
console.log(' Watching for changes...\n');
|
||||||
|
build();
|
||||||
|
|
||||||
const jsDir = path.join(__dirname, 'js');
|
const jsDir = path.join(__dirname, 'js');
|
||||||
let debounce = null;
|
let debounce = null;
|
||||||
fs.watch(jsDir, { recursive: true }, (event, filename) => {
|
fs.watch(jsDir, { recursive: true }, (event, filename) => {
|
||||||
if (!filename || !filename.endsWith('.js')) return;
|
if (!filename || !filename.endsWith('.js')) return;
|
||||||
clearTimeout(debounce);
|
clearTimeout(debounce);
|
||||||
debounce = setTimeout(() => {
|
debounce = setTimeout(() => {
|
||||||
console.log(` Changed: ${filename}`);
|
console.log(` Changed: ${filename}`);
|
||||||
build();
|
build();
|
||||||
}, 200);
|
}, 200);
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
build();
|
build();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// DC-119: export for tests (normalizeSource is pinned by
|
||||||
|
// tests/build-determinism.test.js). build/bundles stay internal.
|
||||||
|
module.exports = { normalizeSource };
|
||||||
|
|||||||
Vendored
+122
-95
File diff suppressed because one or more lines are too long
Generated
+541
-1
@@ -8,7 +8,194 @@
|
|||||||
"name": "dashcaddy-frontend",
|
"name": "dashcaddy-frontend",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"esbuild": "^0.25.0"
|
"esbuild": "^0.25.0",
|
||||||
|
"jsdom": "^30.0.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@asamuzakjp/css-color": {
|
||||||
|
"version": "6.0.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-6.0.7.tgz",
|
||||||
|
"integrity": "sha512-vC/bk1Lz7Tn/EfU9/apOTBk80/8dyGyWMowPoV1tJ52muDGsDqt2HPT2klrFUiY60MQmQv9q8yIht15JnBgDGw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@csstools/css-calc": "^3.3.0",
|
||||||
|
"@csstools/css-color-parser": "^4.1.10",
|
||||||
|
"@csstools/css-parser-algorithms": "^4.0.0",
|
||||||
|
"@csstools/css-tokenizer": "^4.0.0",
|
||||||
|
"lru-cache": "^11.5.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": "^22.13.0 || >=24.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@asamuzakjp/dom-selector": {
|
||||||
|
"version": "8.3.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@asamuzakjp/dom-selector/-/dom-selector-8.3.2.tgz",
|
||||||
|
"integrity": "sha512-93Z1N+BQNXysodoicpOIyNh2drHfz/CTf9nnT0FEx72GJcIiwgydD7tGAr78j41LsYn3hlRn+LdGPuBLn1Bl8Q==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"bidi-js": "^1.0.3",
|
||||||
|
"css-tree": "^3.2.1",
|
||||||
|
"is-potential-custom-element-name": "^1.0.1",
|
||||||
|
"lru-cache": "^11.5.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": "^22.13.0 || >=24.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@bramus/specificity": {
|
||||||
|
"version": "2.4.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@bramus/specificity/-/specificity-2.4.2.tgz",
|
||||||
|
"integrity": "sha512-ctxtJ/eA+t+6q2++vj5j7FYX3nRu311q1wfYH3xjlLOsczhlhxAg2FWNUXhpGvAw3BWo1xBcvOV6/YLc2r5FJw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"css-tree": "^3.0.0"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"specificity": "bin/cli.js"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@csstools/color-helpers": {
|
||||||
|
"version": "6.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-6.1.1.tgz",
|
||||||
|
"integrity": "sha512-gLNsunvwf3mCi5u5o46/Z/JcJMnhbHSaZ69rkgPzNM3J4s8hWwpPUQB6/tt0EDFyCiWzxANlx+2LJwpYj4zS1w==",
|
||||||
|
"dev": true,
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/csstools"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/csstools"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"license": "MIT-0",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=20.19.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@csstools/css-calc": {
|
||||||
|
"version": "3.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-3.3.0.tgz",
|
||||||
|
"integrity": "sha512-c5ihYsPkdG6JCkU2zTMm4+k6r7RXuGxtWYhu5DHMIiF1FHzrfmHL5so11AoFpUv/tu61xfcmT4AmKoFfMPoqdQ==",
|
||||||
|
"dev": true,
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/csstools"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/csstools"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=20.19.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@csstools/css-parser-algorithms": "^4.0.0",
|
||||||
|
"@csstools/css-tokenizer": "^4.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@csstools/css-color-parser": {
|
||||||
|
"version": "4.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-4.2.0.tgz",
|
||||||
|
"integrity": "sha512-5+5LEmFuY1AjXdYhmgjTJogtQnP1evJ1zrBZGUNZ0thkpwnnmKxcHdAMn/OtFjAb25zA+jKDVYVRl+5G7rjv1A==",
|
||||||
|
"dev": true,
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/csstools"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/csstools"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@csstools/color-helpers": "^6.1.1",
|
||||||
|
"@csstools/css-calc": "^3.3.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=20.19.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@csstools/css-parser-algorithms": "^4.0.0",
|
||||||
|
"@csstools/css-tokenizer": "^4.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@csstools/css-parser-algorithms": {
|
||||||
|
"version": "4.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-4.0.0.tgz",
|
||||||
|
"integrity": "sha512-+B87qS7fIG3L5h3qwJ/IFbjoVoOe/bpOdh9hAjXbvx0o8ImEmUsGXN0inFOnk2ChCFgqkkGFQ+TpM5rbhkKe4w==",
|
||||||
|
"dev": true,
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/csstools"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/csstools"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=20.19.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@csstools/css-tokenizer": "^4.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@csstools/css-syntax-patches-for-csstree": {
|
||||||
|
"version": "1.1.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/@csstools/css-syntax-patches-for-csstree/-/css-syntax-patches-for-csstree-1.1.8.tgz",
|
||||||
|
"integrity": "sha512-CpMLjAvwQg3BL5S0IeqsZNMH7EQrEWi0kLKOC13ZBF0ZwERiLWlibNPJr8G1kdU3Ms/r2KiNrF81pUh2HwAHdg==",
|
||||||
|
"dev": true,
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/csstools"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/csstools"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"license": "MIT-0",
|
||||||
|
"peerDependencies": {
|
||||||
|
"css-tree": "^3.2.1"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"css-tree": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@csstools/css-tokenizer": {
|
||||||
|
"version": "4.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-4.0.0.tgz",
|
||||||
|
"integrity": "sha512-QxULHAm7cNu72w97JUNCBFODFaXpbDg+dP8b/oWFAZ2MTRppA3U00Y2L1HqaS4J6yBqxwa/Y3nMBaxVKbB/NsA==",
|
||||||
|
"dev": true,
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/csstools"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/csstools"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=20.19.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@esbuild/aix-ppc64": {
|
"node_modules/@esbuild/aix-ppc64": {
|
||||||
@@ -453,6 +640,97 @@
|
|||||||
"node": ">=18"
|
"node": ">=18"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@exodus/bytes": {
|
||||||
|
"version": "1.15.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@exodus/bytes/-/bytes-1.15.1.tgz",
|
||||||
|
"integrity": "sha512-S6mL0yNB/Abt9Ei4tq8gDhcczc4S3+vQ4ra7vxnAf+YHC02srtqxKKZghx2Dq6p0e66THKwR6r8N6P95wEty7Q==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": "^20.19.0 || ^22.12.0 || >=24.0.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@noble/hashes": "^1.8.0 || ^2.0.0"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@noble/hashes": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/bidi-js": {
|
||||||
|
"version": "1.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/bidi-js/-/bidi-js-1.0.3.tgz",
|
||||||
|
"integrity": "sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"require-from-string": "^2.0.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/css-tree": {
|
||||||
|
"version": "3.2.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/css-tree/-/css-tree-3.2.1.tgz",
|
||||||
|
"integrity": "sha512-X7sjQzceUhu1u7Y/ylrRZFU2FS6LRiFVp6rKLPg23y3x3c3DOKAwuXGDp+PAGjh6CSnCjYeAul8pcT8bAl+lSA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"mdn-data": "2.27.1",
|
||||||
|
"source-map-js": "^1.2.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/data-urls": {
|
||||||
|
"version": "7.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/data-urls/-/data-urls-7.0.0.tgz",
|
||||||
|
"integrity": "sha512-23XHcCF+coGYevirZceTVD7NdJOqVn+49IHyxgszm+JIiHLoB2TkmPtsYkNWT1pvRSGkc35L6NHs0yHkN2SumA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"whatwg-mimetype": "^5.0.0",
|
||||||
|
"whatwg-url": "^16.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": "^20.19.0 || ^22.12.0 || >=24.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/data-urls/node_modules/whatwg-url": {
|
||||||
|
"version": "16.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-16.0.1.tgz",
|
||||||
|
"integrity": "sha512-1to4zXBxmXHV3IiSSEInrreIlu02vUOvrhxJJH5vcxYTBDAx51cqZiKdyTxlecdKNSjj8EcxGBxNf6Vg+945gw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@exodus/bytes": "^1.11.0",
|
||||||
|
"tr46": "^6.0.0",
|
||||||
|
"webidl-conversions": "^8.0.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": "^20.19.0 || ^22.12.0 || >=24.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/decimal.js": {
|
||||||
|
"version": "10.6.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.6.0.tgz",
|
||||||
|
"integrity": "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/entities": {
|
||||||
|
"version": "8.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/entities/-/entities-8.0.0.tgz",
|
||||||
|
"integrity": "sha512-zwfzJecQ/Uej6tusMqwAqU/6KL2XaB2VZ2Jg54Je6ahNBGNH6Ek6g3jjNCF0fG9EWQKGZNddNjU5F1ZQn/sBnA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "BSD-2-Clause",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=20.19.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/fb55/entities?sponsor=1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/esbuild": {
|
"node_modules/esbuild": {
|
||||||
"version": "0.25.12",
|
"version": "0.25.12",
|
||||||
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz",
|
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz",
|
||||||
@@ -494,6 +772,268 @@
|
|||||||
"@esbuild/win32-ia32": "0.25.12",
|
"@esbuild/win32-ia32": "0.25.12",
|
||||||
"@esbuild/win32-x64": "0.25.12"
|
"@esbuild/win32-x64": "0.25.12"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"node_modules/html-encoding-sniffer": {
|
||||||
|
"version": "6.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-6.0.0.tgz",
|
||||||
|
"integrity": "sha512-CV9TW3Y3f8/wT0BRFc1/KAVQ3TUHiXmaAb6VW9vtiMFf7SLoMd1PdAc4W3KFOFETBJUb90KatHqlsZMWV+R9Gg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@exodus/bytes": "^1.6.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": "^20.19.0 || ^22.12.0 || >=24.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/is-potential-custom-element-name": {
|
||||||
|
"version": "1.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz",
|
||||||
|
"integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/jsdom": {
|
||||||
|
"version": "30.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/jsdom/-/jsdom-30.0.1.tgz",
|
||||||
|
"integrity": "sha512-52v7mUVUfNQVYYqE1lcdaymWL0njO7lTLUog6ZvW2U5KsbiLk/GnZlVJ+qx0xfNJZ6Gn+KSpPNE52vurbxZwrA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@asamuzakjp/css-color": "^6.0.5",
|
||||||
|
"@asamuzakjp/dom-selector": "^8.3.0",
|
||||||
|
"@bramus/specificity": "^2.4.2",
|
||||||
|
"@csstools/css-syntax-patches-for-csstree": "^1.1.7",
|
||||||
|
"@exodus/bytes": "^1.15.1",
|
||||||
|
"css-tree": "^3.2.1",
|
||||||
|
"data-urls": "^7.0.0",
|
||||||
|
"decimal.js": "^10.6.0",
|
||||||
|
"html-encoding-sniffer": "^6.0.0",
|
||||||
|
"is-potential-custom-element-name": "^1.0.1",
|
||||||
|
"lru-cache": "^11.5.2",
|
||||||
|
"parse5": "^8.0.1",
|
||||||
|
"saxes": "^6.0.0",
|
||||||
|
"symbol-tree": "^3.2.4",
|
||||||
|
"tough-cookie": "^6.0.2",
|
||||||
|
"undici": "^8.9.0",
|
||||||
|
"w3c-xmlserializer": "^5.0.0",
|
||||||
|
"webidl-conversions": "^8.0.1",
|
||||||
|
"whatwg-mimetype": "^5.0.0",
|
||||||
|
"whatwg-url": "^17.1.0",
|
||||||
|
"xml-name-validator": "^5.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": "^22.22.2 || ^24.15.0 || >=26.0.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"canvas": "^3.2.3"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"canvas": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/lru-cache": {
|
||||||
|
"version": "11.5.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.2.tgz",
|
||||||
|
"integrity": "sha512-4pfM1Ff0x50o0tQwb5ucw/RzNyD0/YJME6IVcStalZuMWxdt3sR3huStTtxz4PUmvZfRguvDejasvQ2kifR11g==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "BlueOak-1.0.0",
|
||||||
|
"engines": {
|
||||||
|
"node": "20 || >=22"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/mdn-data": {
|
||||||
|
"version": "2.27.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.27.1.tgz",
|
||||||
|
"integrity": "sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "CC0-1.0"
|
||||||
|
},
|
||||||
|
"node_modules/parse5": {
|
||||||
|
"version": "8.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/parse5/-/parse5-8.0.1.tgz",
|
||||||
|
"integrity": "sha512-z1e/HMG90obSGeidlli3hj7cbocou0/wa5HacvI3ASx34PecNjNQeaHNo5WIZpWofN9kgkqV1q5YvXe3F0FoPw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"entities": "^8.0.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/inikulin/parse5?sponsor=1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/punycode": {
|
||||||
|
"version": "2.3.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
|
||||||
|
"integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/require-from-string": {
|
||||||
|
"version": "2.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz",
|
||||||
|
"integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.10.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/saxes": {
|
||||||
|
"version": "6.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz",
|
||||||
|
"integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"xmlchars": "^2.2.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=v12.22.7"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/source-map-js": {
|
||||||
|
"version": "1.2.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
|
||||||
|
"integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "BSD-3-Clause",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.10.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/symbol-tree": {
|
||||||
|
"version": "3.2.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz",
|
||||||
|
"integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/tldts": {
|
||||||
|
"version": "7.4.10",
|
||||||
|
"resolved": "https://registry.npmjs.org/tldts/-/tldts-7.4.10.tgz",
|
||||||
|
"integrity": "sha512-GgouD1B+sWwvkaEq8vXC15DjQitxbvs12oIXELpconwm+Tg3zfcEv4jgzq3vtKverDXsg3VI8aRgNL2Nra0Iog==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"tldts-core": "^7.4.10"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"tldts": "bin/cli.js"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/tldts-core": {
|
||||||
|
"version": "7.4.10",
|
||||||
|
"resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-7.4.10.tgz",
|
||||||
|
"integrity": "sha512-KnQjp53ZekKgm/r3l+u8kJGGzYgrWdP8+Mql7a4vijh2WE0IrZWspQj/TpTxDho/YxO+AnOZnIjQcCD+q6iJsw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/tough-cookie": {
|
||||||
|
"version": "6.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-6.0.2.tgz",
|
||||||
|
"integrity": "sha512-exgYmnmL/sJpR3upZfXG5PoatXQii55xAiXGXzY+sROLZ/Y+SLcp9PgJNI9Vz37HpQ74WvDcLT8eqm+kV3FzrA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "BSD-3-Clause",
|
||||||
|
"dependencies": {
|
||||||
|
"tldts": "^7.0.5"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=16"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/tr46": {
|
||||||
|
"version": "6.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/tr46/-/tr46-6.0.0.tgz",
|
||||||
|
"integrity": "sha512-bLVMLPtstlZ4iMQHpFHTR7GAGj2jxi8Dg0s2h2MafAE4uSWF98FC/3MomU51iQAMf8/qDUbKWf5GxuvvVcXEhw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"punycode": "^2.3.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=20"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/undici": {
|
||||||
|
"version": "8.10.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/undici/-/undici-8.10.0.tgz",
|
||||||
|
"integrity": "sha512-HvltHd7avK13QIw/oLe4qoOLyoVSoafqJ2jYOrtMRBkbYT31eiBQ8O0ehRKZiEZCMEyLFQNIADpgCWC5fALvYQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=22.19.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/w3c-xmlserializer": {
|
||||||
|
"version": "5.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz",
|
||||||
|
"integrity": "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"xml-name-validator": "^5.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/webidl-conversions": {
|
||||||
|
"version": "8.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-8.0.1.tgz",
|
||||||
|
"integrity": "sha512-BMhLD/Sw+GbJC21C/UgyaZX41nPt8bUTg+jWyDeg7e7YN4xOM05YPSIXceACnXVtqyEw/LMClUQMtMZ+PGGpqQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "BSD-2-Clause",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=20"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/whatwg-mimetype": {
|
||||||
|
"version": "5.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-5.0.0.tgz",
|
||||||
|
"integrity": "sha512-sXcNcHOC51uPGF0P/D4NVtrkjSU2fNsm9iog4ZvZJsL3rjoDAzXZhkm2MWt1y+PUdggKAYVoMAIYcs78wJ51Cw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=20"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/whatwg-url": {
|
||||||
|
"version": "17.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-17.1.0.tgz",
|
||||||
|
"integrity": "sha512-3GeworPmc2ZfEEHP7lEbUfBX/L75wdEsi0rLNhXcXxnoN5jyq0SL5gCy06SGW2cyTIZdTvWIDQNQoza++vKeaw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@exodus/bytes": "^1.15.1",
|
||||||
|
"tr46": "^6.0.0",
|
||||||
|
"webidl-conversions": "^8.0.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": "^22.14.0 || >=24.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/xml-name-validator": {
|
||||||
|
"version": "5.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-5.0.0.tgz",
|
||||||
|
"integrity": "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/xmlchars": {
|
||||||
|
"version": "2.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz",
|
||||||
|
"integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+2
-1
@@ -8,6 +8,7 @@
|
|||||||
"watch": "node build.js --watch"
|
"watch": "node build.js --watch"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"esbuild": "^0.25.0"
|
"esbuild": "^0.25.0",
|
||||||
|
"jsdom": "^30.0.1"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
const CACHE = 'dashcaddy-shell-3354f5fd96';
|
const CACHE = 'dashcaddy-shell-d39ab69dd4';
|
||||||
const PRECACHE = [
|
const PRECACHE = [
|
||||||
'/',
|
'/',
|
||||||
'/index.html',
|
'/index.html',
|
||||||
|
|||||||
@@ -0,0 +1,127 @@
|
|||||||
|
'use strict';
|
||||||
|
/**
|
||||||
|
* DC-119: frontend build determinism across line endings.
|
||||||
|
*
|
||||||
|
* The frontend build (status/build.js) concatenates raw source files and
|
||||||
|
* minifies them with esbuild using sourcemap:'both' — the inline map
|
||||||
|
* base64-embeds the raw source bytes (sourcesContent), CRs included. A CRLF
|
||||||
|
* working copy (Windows dev tree, core.autocrlf=true) vs an LF working copy
|
||||||
|
* (DNS2 Linux checkout) of the SAME commit therefore produces different
|
||||||
|
* dist bytes and a different sw.js cache tag — so the committed dist could
|
||||||
|
* never be reproduced on the deploy host, showing up as permanent phantom
|
||||||
|
* drift on `git pull` in /opt/dashcaddy (the recurring "pre-pull drift"
|
||||||
|
* stashes).
|
||||||
|
*
|
||||||
|
* build.js now normalizes every source read to LF (\r\n -> \n) before
|
||||||
|
* concatenation. This test pins that behavior at the transform level: the
|
||||||
|
* SAME input, CRLF vs LF, must produce byte-identical minified output, and
|
||||||
|
* the normalization regex used by build.js must strip all CR bytes.
|
||||||
|
*
|
||||||
|
* It deliberately does NOT shell out to `node build.js` (slow, writes
|
||||||
|
* dist/) — it exercises the exact transform + normalization logic inline.
|
||||||
|
*/
|
||||||
|
|
||||||
|
const test = require('node:test');
|
||||||
|
const assert = require('node:assert');
|
||||||
|
|
||||||
|
// Same devDependency esbuild the build itself uses.
|
||||||
|
const esbuild = require('esbuild');
|
||||||
|
|
||||||
|
// DC-119: import THE ACTUAL normalization from build.js — not a local
|
||||||
|
// re-implementation — so this test fails if the build's regex ever changes.
|
||||||
|
const { normalizeSource: normalize } = require('../build.js');
|
||||||
|
|
||||||
|
// Representative source: top-level names, nested scopes, strings with
|
||||||
|
// escapes, template literals, regex literals, comments — the constructs
|
||||||
|
// whose minified renames shifted pre-fix.
|
||||||
|
const SAMPLE = `// feature module
|
||||||
|
const logoCustomization = {
|
||||||
|
position: 'left',
|
||||||
|
cacheTag: 'dashcaddy-shell-abc123',
|
||||||
|
};
|
||||||
|
|
||||||
|
function applyPosition(position, elem) {
|
||||||
|
const normalized = position || logoCustomization.position;
|
||||||
|
elem.setAttribute('data-logo-pos', normalized);
|
||||||
|
return normalized.replace(/_/g, ' ');
|
||||||
|
}
|
||||||
|
|
||||||
|
const summarize = (items) => {
|
||||||
|
let total = 0;
|
||||||
|
for (const item of items) {
|
||||||
|
total += item.count ?? 0;
|
||||||
|
}
|
||||||
|
return \`total: \${total} (\${items.length} items)\`;
|
||||||
|
};
|
||||||
|
|
||||||
|
async function loadConfig(url) {
|
||||||
|
try {
|
||||||
|
const res = await fetch(url);
|
||||||
|
if (!res.ok) throw new Error('HTTP ' + res.status);
|
||||||
|
return await res.json();
|
||||||
|
} catch (err) {
|
||||||
|
console.warn('load failed:', err.message);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = { applyPosition, summarize, loadConfig };
|
||||||
|
`;
|
||||||
|
|
||||||
|
// Production transform options — MUST mirror build.js. The CRLF divergence
|
||||||
|
// lives in the INLINE SOURCEMAP: sourcemap:'both' embeds the raw source as
|
||||||
|
// base64 sourcesContent, so CRLF bytes survive into dist and shift both the
|
||||||
|
// bundle bytes and the sw.js content-hash cache tag.
|
||||||
|
async function minify(source) {
|
||||||
|
const { code } = await esbuild.transform(source, {
|
||||||
|
minify: true,
|
||||||
|
target: 'es2020',
|
||||||
|
sourcemap: 'both',
|
||||||
|
});
|
||||||
|
return code;
|
||||||
|
}
|
||||||
|
|
||||||
|
test('DC-119: CRLF and LF inputs produce byte-identical minified output', async () => {
|
||||||
|
const lf = SAMPLE;
|
||||||
|
const crlf = SAMPLE.replace(/\n/g, '\r\n');
|
||||||
|
|
||||||
|
// Sanity: the two raw inputs really do differ.
|
||||||
|
assert.notEqual(lf, crlf, 'fixture setup: CRLF variant must differ from LF');
|
||||||
|
|
||||||
|
const outLf = await minify(normalize(lf));
|
||||||
|
const outCrlf = await minify(normalize(crlf));
|
||||||
|
assert.strictEqual(
|
||||||
|
outCrlf,
|
||||||
|
outLf,
|
||||||
|
'minified output must be byte-identical after CRLF->LF normalization'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('DC-119: without normalization, CRLF vs LF differ (documents the bug)', async () => {
|
||||||
|
const lf = SAMPLE;
|
||||||
|
const crlf = SAMPLE.replace(/\n/g, '\r\n');
|
||||||
|
|
||||||
|
const outLf = await minify(lf);
|
||||||
|
const outCrlf = await minify(crlf);
|
||||||
|
|
||||||
|
// Documents WHY the normalization exists: the inline sourcemap's
|
||||||
|
// sourcesContent base64-encodes the raw bytes, CRs included. If esbuild
|
||||||
|
// ever normalizes sourcesContent itself, this may flip to equal — then
|
||||||
|
// the normalization is redundant but harmless; update the DC-119 comment
|
||||||
|
// in build.js when that happens.
|
||||||
|
assert.notEqual(
|
||||||
|
outCrlf,
|
||||||
|
outLf,
|
||||||
|
'expected CRLF/LF divergence pre-normalization (inline sourcemap sourcesContent); if equal, esbuild changed behavior — update the DC-119 comment in build.js'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('DC-119: normalization strips every CR from CRLF input and leaves LF untouched', () => {
|
||||||
|
const crlf = 'line1\r\nline2\r\n';
|
||||||
|
const lf = 'line1\nline2\n';
|
||||||
|
// Lone \r (old-Mac style) is NOT produced by git autocrlf and is NOT
|
||||||
|
// claimed to be handled — assert only the CRLF contract.
|
||||||
|
assert.strictEqual(normalize(crlf), 'line1\nline2\n');
|
||||||
|
assert.strictEqual(normalize(lf), 'line1\nline2\n');
|
||||||
|
assert.ok(!normalize(crlf).includes('\r'), 'no CR may survive normalization');
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user