Three merge-fallout bugs that combined to leave the services grid empty
and most UI inert:
1. error-handler.js was bundled into onboarding.js (loaded 3rd), but
globals.js in core.js (loaded 1st) does `const errorHandler = new
ErrorHandler()` at top level. ErrorHandler was undefined when core.js
ran -> ReferenceError -> globals.js stopped, so window.APPS,
_showTotpOverlay, loadServices, etc. were never set, and init.js
blew up on every call into core's exports.
Moved error-handler.js to the start of the core.js bundle so the
class is on window before any other script touches it.
2. setup-wizard.js also declared `const errorHandler = new ErrorHandler()`
at top level. Classic scripts share the document's top-level lexical
environment, so this collided with globals.js's declaration ->
redeclaration SyntaxError in features.js. Removed setup-wizard.js's
copy; it picks up the global one.
3. tooltip-definitions.js closed its `(function(window){...})(window);`
IIFE at line ~171 ("Validation module loaded"), then the TOOLTIP_
DEFINITIONS array, getter helpers, window.TooltipDefinitions export,
and final `debug(...)` log all sat at top level — outside the IIFE,
where `debug` was no longer in scope. Removed the early close and
added one at EOF so the whole file is in one IIFE.
549 lines
18 KiB
JavaScript
549 lines
18 KiB
JavaScript
/**
|
|
* Tooltip Definitions
|
|
* Defines all tooltip content, positioning, and behavior for the onboarding system
|
|
*/
|
|
|
|
(function(window) {
|
|
'use strict';
|
|
|
|
const errorHandler = new ErrorHandler();
|
|
|
|
const debug = (...args) => {
|
|
if (window.DASHCADDY_DEBUG) { console.log(...args); }
|
|
};
|
|
|
|
/**
|
|
* Validate a tooltip definition
|
|
* @param {Object} tooltip - The tooltip definition to validate
|
|
* @returns {Object} { valid: boolean, errors: string[] }
|
|
*/
|
|
function validateTooltipDefinition(tooltip) {
|
|
const errors = [];
|
|
|
|
// Required fields
|
|
if (!tooltip.id || typeof tooltip.id !== 'string') {
|
|
errors.push('Tooltip must have a valid string id');
|
|
}
|
|
|
|
if (!tooltip.element) {
|
|
errors.push('Tooltip must have an element selector or HTMLElement');
|
|
}
|
|
|
|
if (!tooltip.popover || typeof tooltip.popover !== 'object') {
|
|
errors.push('Tooltip must have a popover object');
|
|
} else {
|
|
// Validate popover fields
|
|
if (!tooltip.popover.title || typeof tooltip.popover.title !== 'string') {
|
|
errors.push('Tooltip popover must have a valid string title');
|
|
}
|
|
|
|
if (!tooltip.popover.description || typeof tooltip.popover.description !== 'string') {
|
|
errors.push('Tooltip popover must have a valid string description');
|
|
}
|
|
|
|
// Validate position if provided
|
|
if (tooltip.popover.position) {
|
|
const validPositions = ['top', 'bottom', 'left', 'right', 'center'];
|
|
if (!validPositions.includes(tooltip.popover.position)) {
|
|
errors.push(`Invalid position: ${tooltip.popover.position}. Must be one of: ${validPositions.join(', ')}`);
|
|
}
|
|
}
|
|
|
|
// Validate align if provided
|
|
if (tooltip.popover.align) {
|
|
const validAligns = ['start', 'center', 'end'];
|
|
if (!validAligns.includes(tooltip.popover.align)) {
|
|
errors.push(`Invalid align: ${tooltip.popover.align}. Must be one of: ${validAligns.join(', ')}`);
|
|
}
|
|
}
|
|
|
|
// Validate showButtons if provided
|
|
if (tooltip.popover.showButtons && !Array.isArray(tooltip.popover.showButtons)) {
|
|
errors.push('showButtons must be an array');
|
|
}
|
|
|
|
// Validate callbacks if provided
|
|
const callbacks = ['onNext', 'onPrevious', 'onClose', 'onSetupNow', 'onLater'];
|
|
callbacks.forEach(callback => {
|
|
if (tooltip.popover[callback] && typeof tooltip.popover[callback] !== 'function') {
|
|
errors.push(`${callback} must be a function`);
|
|
}
|
|
});
|
|
}
|
|
|
|
// Validate condition if provided
|
|
if (tooltip.condition && typeof tooltip.condition !== 'function') {
|
|
errors.push('condition must be a function');
|
|
}
|
|
|
|
// Validate priority if provided
|
|
if (tooltip.priority !== undefined && typeof tooltip.priority !== 'number') {
|
|
errors.push('priority must be a number');
|
|
}
|
|
|
|
return {
|
|
valid: errors.length === 0,
|
|
errors
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Validate an array of tooltip definitions
|
|
* @param {Array} tooltips - Array of tooltip definitions
|
|
* @returns {Object} { valid: boolean, errors: Object[] }
|
|
*/
|
|
function validateTooltipDefinitions(tooltips) {
|
|
if (!Array.isArray(tooltips)) {
|
|
return {
|
|
valid: false,
|
|
errors: [{ tooltip: null, errors: ['tooltips must be an array'] }]
|
|
};
|
|
}
|
|
|
|
const allErrors = [];
|
|
const ids = new Set();
|
|
|
|
tooltips.forEach((tooltip, index) => {
|
|
const validation = validateTooltipDefinition(tooltip);
|
|
|
|
if (!validation.valid) {
|
|
allErrors.push({
|
|
tooltip: tooltip.id || `index ${index}`,
|
|
errors: validation.errors
|
|
});
|
|
}
|
|
|
|
// Check for duplicate IDs
|
|
if (tooltip.id) {
|
|
if (ids.has(tooltip.id)) {
|
|
allErrors.push({
|
|
tooltip: tooltip.id,
|
|
errors: [`Duplicate tooltip ID: ${tooltip.id}`]
|
|
});
|
|
}
|
|
ids.add(tooltip.id);
|
|
}
|
|
});
|
|
|
|
return {
|
|
valid: allErrors.length === 0,
|
|
errors: allErrors
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Error handler for tooltip system
|
|
*/
|
|
class TooltipError extends Error {
|
|
constructor(message, tooltipId = null) {
|
|
super(message);
|
|
this.name = 'TooltipError';
|
|
this.tooltipId = tooltipId;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Handle tooltip definition errors
|
|
* @param {Object} validation - Validation result
|
|
* @throws {TooltipError} If validation fails
|
|
*/
|
|
function handleValidationErrors(validation) {
|
|
if (!validation.valid) {
|
|
const errorMessages = validation.errors.map(e =>
|
|
`${e.tooltip}: ${e.errors.join(', ')}`
|
|
).join('\n');
|
|
|
|
errorHandler.logError('[TooltipDefinitions] Validation', errorMessages, { function: 'validateTooltip' });
|
|
throw new TooltipError(`Tooltip validation failed:\n${errorMessages}`);
|
|
}
|
|
}
|
|
|
|
// Export to global scope
|
|
window.TooltipValidation = {
|
|
validateTooltipDefinition,
|
|
validateTooltipDefinitions,
|
|
handleValidationErrors,
|
|
TooltipError
|
|
};
|
|
|
|
debug('[TooltipDefinitions] Validation module loaded');
|
|
|
|
// (IIFE close moved to EOF — the TOOLTIP_DEFINITIONS array and getter
|
|
// helpers below must stay inside this IIFE so they have access to `debug`
|
|
// and `errorHandler`. The early `})(window);` here used to fall out of
|
|
// scope, leaving the later `debug(...)` call referencing an undefined name.)
|
|
|
|
/**
|
|
* Tooltip Definitions Array
|
|
* Defines all tooltips for the onboarding tour
|
|
*/
|
|
const TOOLTIP_DEFINITIONS = [
|
|
// 1. Welcome
|
|
{
|
|
id: 'welcome',
|
|
element: '#brand',
|
|
popover: {
|
|
title: 'Welcome to DashCaddy!',
|
|
description: `
|
|
<p>Your unified control panel for <strong>Docker</strong>, <strong>Caddy</strong>, and <strong>DNS</strong> — all in one place.</p>
|
|
<p>This tour will walk you through every section so you know exactly where everything is.</p>
|
|
<p style="margin-top: 8px; font-size: 0.85rem; opacity: 0.7;">You can click your logo anytime to customize it.</p>
|
|
`,
|
|
position: 'bottom',
|
|
align: 'start',
|
|
showButtons: ['next'],
|
|
showProgress: true
|
|
},
|
|
priority: 1
|
|
},
|
|
|
|
// 2. Infrastructure row
|
|
{
|
|
id: 'infrastructure',
|
|
element: '.top',
|
|
popover: {
|
|
title: 'Infrastructure Overview',
|
|
description: `
|
|
<p>This top row shows the backbone of your setup:</p>
|
|
<ul>
|
|
<li><strong>DNS1 / DNS2 / DNS3</strong> — your Technitium DNS servers</li>
|
|
<li><strong>Internet</strong> — live connectivity check with packet indicators</li>
|
|
<li><strong>Auth</strong> — TOTP authentication status</li>
|
|
<li><strong>DashCA</strong> — your certificate authority for your custom domain</li>
|
|
</ul>
|
|
<p style="font-size: 0.85rem; opacity: 0.7;">These core services are always pinned here, separate from your app grid.</p>
|
|
`,
|
|
position: 'bottom',
|
|
showButtons: ['previous', 'next'],
|
|
showProgress: true
|
|
},
|
|
priority: 2
|
|
},
|
|
|
|
// 3. Service cards
|
|
{
|
|
id: 'service-cards',
|
|
element: '#cards',
|
|
popover: {
|
|
title: 'Your App Grid',
|
|
description: `
|
|
<p>Every app you deploy appears here as a card. Each one shows:</p>
|
|
<ul>
|
|
<li><strong>Status dot</strong> — green = online, red = offline (pulses when down)</li>
|
|
<li><strong>ON/OFF badge</strong> — current state at a glance</li>
|
|
<li><strong>Response time</strong> — how fast the service responds (color-coded)</li>
|
|
<li><strong>Uptime bar</strong> — historical uptime percentage</li>
|
|
</ul>
|
|
<p style="font-size: 0.85rem; opacity: 0.7;">Hover over a card to see action buttons: Logs, Restart, Update, Settings, and Open.</p>
|
|
`,
|
|
position: 'top',
|
|
showButtons: ['previous', 'next'],
|
|
showProgress: true
|
|
},
|
|
priority: 3
|
|
},
|
|
|
|
// 4. App Selector
|
|
{
|
|
id: 'app-selector',
|
|
element: '#add-service-btn',
|
|
popover: {
|
|
title: 'App Selector — Deploy in One Click',
|
|
description: `
|
|
<p>Browse <strong>50+ self-hosted apps</strong> organized by category:</p>
|
|
<ul>
|
|
<li>Media (Plex, Jellyfin, Emby, Overseerr)</li>
|
|
<li>Downloads (*arr stack, qBittorrent, Transmission)</li>
|
|
<li>Productivity (Nextcloud, Vaultwarden, Gitea)</li>
|
|
<li>Monitoring (Uptime Kuma, Grafana, Prometheus)</li>
|
|
</ul>
|
|
<p>Each app deploys with Docker, Caddy reverse proxy, and DNS — fully configured automatically.</p>
|
|
<p style="margin-top: 6px; padding: 6px 10px; border-radius: 6px; background: rgba(241,196,15,0.12); border: 1px solid rgba(241,196,15,0.25); font-size: 0.85rem;">
|
|
<strong style="color: #f1c40f;">★ Premium:</strong> <strong>Recipes</strong> let you deploy entire stacks (e.g., full media server) in one click with pre-wired configs.
|
|
</p>
|
|
`,
|
|
position: 'top',
|
|
showButtons: ['previous', 'next'],
|
|
showProgress: true
|
|
},
|
|
priority: 4,
|
|
condition: () => document.getElementById('add-service-btn') !== null
|
|
},
|
|
|
|
// 5. Smart Arr Connect
|
|
{
|
|
id: 'smart-arr',
|
|
element: '#arr-setup-btn',
|
|
popover: {
|
|
title: 'Smart Arr Connect',
|
|
description: `
|
|
<p>Automatically wire up your entire media stack:</p>
|
|
<ul>
|
|
<li>Detects running Arr services (Radarr, Sonarr, Prowlarr, etc.)</li>
|
|
<li>Connects them together with the right API keys</li>
|
|
<li>Links Plex/Jellyfin to Overseerr for request management</li>
|
|
</ul>
|
|
<p style="font-size: 0.85rem; opacity: 0.7;">No manual API key copying — DashCaddy handles it all.</p>
|
|
`,
|
|
position: 'top',
|
|
showButtons: ['previous', 'next'],
|
|
showProgress: true
|
|
},
|
|
priority: 5,
|
|
condition: () => document.getElementById('arr-setup-btn') !== null
|
|
},
|
|
|
|
// 6. Add App Manually (toolbar)
|
|
{
|
|
id: 'add-manual',
|
|
element: '#add-service',
|
|
popover: {
|
|
title: 'Add App Manually',
|
|
description: `
|
|
<p>Already have a service running? Add it to your dashboard manually.</p>
|
|
<p>You can add:</p>
|
|
<ul>
|
|
<li><strong>Docker containers</strong> — auto-detects running containers</li>
|
|
<li><strong>External services</strong> — any URL you want to monitor</li>
|
|
<li><strong>Test services</strong> — try the UI without deploying anything</li>
|
|
</ul>
|
|
`,
|
|
position: 'bottom',
|
|
showButtons: ['previous', 'next'],
|
|
showProgress: true
|
|
},
|
|
priority: 6
|
|
},
|
|
|
|
// 7. Theme
|
|
{
|
|
id: 'theme-selector',
|
|
element: '#theme',
|
|
popover: {
|
|
title: 'Themes',
|
|
description: `
|
|
<p>Switch between <strong>7 built-in themes</strong>:</p>
|
|
<p>Dark, Light, Blue, Nord, Dracula, Solarized Dark, and Solarized Light.</p>
|
|
<p>Your preference is saved automatically. You can also build custom themes with the Theme Builder (in Admin → settings).</p>
|
|
`,
|
|
position: 'bottom',
|
|
showButtons: ['previous', 'next'],
|
|
showProgress: true
|
|
},
|
|
priority: 7
|
|
},
|
|
|
|
// 8. Toolbar: Status section
|
|
{
|
|
id: 'toolbar-status',
|
|
element: '.tools-section[data-section="status"]',
|
|
popover: {
|
|
title: 'Status Tools',
|
|
description: `
|
|
<p>Click <strong>Status</strong> to expand these tools:</p>
|
|
<ul>
|
|
<li><strong>Monitor</strong> — live CPU, memory, and disk usage for every container</li>
|
|
<li><strong>Health</strong> — service health dashboard with uptime history and alerts</li>
|
|
<li><strong>Updates</strong> — check for and apply Docker image updates</li>
|
|
</ul>
|
|
<p style="font-size: 0.85rem; opacity: 0.7;">These sections remember whether you left them open or closed.</p>
|
|
`,
|
|
position: 'bottom',
|
|
showButtons: ['previous', 'next'],
|
|
showProgress: true
|
|
},
|
|
priority: 8
|
|
},
|
|
|
|
// 9. Toolbar: Tools section
|
|
{
|
|
id: 'toolbar-tools',
|
|
element: '.tools-section[data-section="tools"]',
|
|
popover: {
|
|
title: 'Operational Tools',
|
|
description: `
|
|
<p>Click <strong>Tools</strong> for day-to-day operations:</p>
|
|
<ul>
|
|
<li><strong>Logs</strong> — view error logs from your API server and services</li>
|
|
<li><strong>Alerts</strong> — configure notification rules (email, webhook, etc.)</li>
|
|
<li><strong>Audit</strong> — full audit trail of every action taken in DashCaddy</li>
|
|
</ul>
|
|
`,
|
|
position: 'bottom',
|
|
showButtons: ['previous', 'next'],
|
|
showProgress: true
|
|
},
|
|
priority: 9
|
|
},
|
|
|
|
// 10. Toolbar: Admin section
|
|
{
|
|
id: 'toolbar-admin',
|
|
element: '.tools-section[data-section="admin"]',
|
|
popover: {
|
|
title: 'Admin & Configuration',
|
|
description: `
|
|
<p>Click <strong>Admin</strong> for setup and maintenance:</p>
|
|
<ul>
|
|
<li><strong>Tokens</strong> — manage API keys for DNS and other integrations</li>
|
|
<li><strong>Export / Import</strong> — backup and restore your dashboard configuration</li>
|
|
<li><strong>Backup</strong> — full system backup and restore</li>
|
|
<li><strong>License</strong> — activate your license code to unlock premium features</li>
|
|
<li><strong>API</strong> — interactive API documentation (125+ endpoints)</li>
|
|
</ul>
|
|
`,
|
|
position: 'bottom',
|
|
showButtons: ['previous', 'next'],
|
|
showProgress: true
|
|
},
|
|
priority: 10
|
|
},
|
|
|
|
// 11. Weather widget
|
|
{
|
|
id: 'weather',
|
|
element: '#weather-widget',
|
|
popover: {
|
|
title: 'Weather Widget',
|
|
description: `
|
|
<p>Shows current conditions for your location. Click the <strong>gear icon</strong> to configure your city or switch temperature units.</p>
|
|
<p style="font-size: 0.85rem; opacity: 0.7;">Uses Open-Meteo — no API key required.</p>
|
|
`,
|
|
position: 'bottom',
|
|
showButtons: ['previous', 'next'],
|
|
showProgress: true
|
|
},
|
|
priority: 11,
|
|
condition: () => document.getElementById('weather-widget') !== null
|
|
},
|
|
|
|
// 12. Reload Caddy
|
|
{
|
|
id: 'reload-caddy',
|
|
element: '#reload-caddy-top',
|
|
popover: {
|
|
title: 'Reload Caddy',
|
|
description: `
|
|
<p>After changing Caddy configuration (adding reverse proxy rules, SSL settings, etc.), click here to apply the changes live.</p>
|
|
<p style="font-size: 0.85rem; opacity: 0.7;">This is a graceful reload — existing connections are not dropped.</p>
|
|
`,
|
|
position: 'bottom',
|
|
align: 'end',
|
|
showButtons: ['previous', 'next'],
|
|
showProgress: true
|
|
},
|
|
priority: 12,
|
|
condition: () => document.getElementById('reload-caddy-top') !== null
|
|
},
|
|
|
|
// 13. Finish
|
|
{
|
|
id: 'tour-complete',
|
|
element: '#brand',
|
|
popover: {
|
|
title: 'You\'re All Set!',
|
|
description: `
|
|
<p>That covers the essentials. A few tips to get the most out of DashCaddy:</p>
|
|
<ul>
|
|
<li><strong>Click any card</strong> to open that service directly</li>
|
|
<li><strong>Keyboard shortcuts</strong> — press <code>?</code> anytime to see them all</li>
|
|
<li><strong>DashCA</strong> — visit your CA page to install the root certificate on any device</li>
|
|
</ul>
|
|
<div style="margin-top: 10px; padding: 10px 12px; border-radius: 8px; background: linear-gradient(135deg, rgba(241,196,15,0.1), rgba(243,156,18,0.08)); border: 1px solid rgba(241,196,15,0.25);">
|
|
<p style="margin: 0 0 6px; font-weight: 600; color: #f1c40f;">★ Unlock Premium</p>
|
|
<p style="margin: 0; font-size: 0.85rem;">Premium adds powerful features for serious homelabbers:</p>
|
|
<ul style="margin: 6px 0 0; font-size: 0.85rem;">
|
|
<li><strong>Auto-Login SSO</strong> — sign into every app automatically, no more password juggling</li>
|
|
<li><strong>Recipes</strong> — deploy full stacks (media server, dev environment) in one click</li>
|
|
<li><strong>Docker Swarm</strong> — orchestrate multi-node clusters from your dashboard</li>
|
|
</ul>
|
|
<p style="margin: 8px 0 0; font-size: 0.82rem; opacity: 0.8;">Activate in <strong>Admin → License</strong></p>
|
|
</div>
|
|
<p style="margin-top: 10px; font-size: 0.85rem; opacity: 0.7;">You can restart this tour anytime from <strong>Admin → Help Tour</strong>.</p>
|
|
`,
|
|
position: 'bottom',
|
|
align: 'start',
|
|
showButtons: ['previous', 'close'],
|
|
showProgress: true
|
|
},
|
|
priority: 13
|
|
}
|
|
];
|
|
|
|
/**
|
|
* Get tooltip definitions
|
|
* @returns {Array} Array of tooltip definitions
|
|
*/
|
|
function getTooltipDefinitions() {
|
|
return TOOLTIP_DEFINITIONS;
|
|
}
|
|
|
|
/**
|
|
* Get a specific tooltip by ID
|
|
* @param {string} id - Tooltip ID
|
|
* @returns {Object|null} Tooltip definition or null if not found
|
|
*/
|
|
function getTooltipById(id) {
|
|
return TOOLTIP_DEFINITIONS.find(t => t.id === id) || null;
|
|
}
|
|
|
|
/**
|
|
* Get tooltips filtered by condition
|
|
* @returns {Array} Array of tooltips that pass their condition check
|
|
*/
|
|
function getActiveTooltips() {
|
|
return TOOLTIP_DEFINITIONS.filter(tooltip => {
|
|
if (tooltip.condition && typeof tooltip.condition === 'function') {
|
|
try {
|
|
return tooltip.condition();
|
|
} catch (error) {
|
|
errorHandler.logError('[TooltipDefinitions] Condition Eval', error, { function: 'evaluateCondition', tooltipId: tooltip.id });
|
|
return false;
|
|
}
|
|
}
|
|
return true;
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Get tooltips sorted by priority
|
|
* @returns {Array} Array of tooltips sorted by priority (ascending)
|
|
*/
|
|
function getSortedTooltips() {
|
|
const tooltips = getActiveTooltips();
|
|
return tooltips.sort((a, b) => {
|
|
const priorityA = a.priority || 999;
|
|
const priorityB = b.priority || 999;
|
|
return priorityA - priorityB;
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Get tooltips marked as new features
|
|
* @returns {Array} Array of tooltips marked with isNewFeature flag
|
|
*/
|
|
function getNewFeatureTooltips() {
|
|
const tooltips = getActiveTooltips();
|
|
return tooltips.filter(tooltip => tooltip.isNewFeature === true)
|
|
.sort((a, b) => {
|
|
const priorityA = a.priority || 999;
|
|
const priorityB = b.priority || 999;
|
|
return priorityA - priorityB;
|
|
});
|
|
}
|
|
|
|
// Export to global scope
|
|
window.TooltipDefinitions = {
|
|
TOOLTIP_DEFINITIONS,
|
|
getTooltipDefinitions,
|
|
getTooltipById,
|
|
getActiveTooltips,
|
|
getSortedTooltips,
|
|
getNewFeatureTooltips
|
|
};
|
|
|
|
debug('[TooltipDefinitions] Definitions loaded:', TOOLTIP_DEFINITIONS.length, 'tooltips');
|
|
|
|
})(window);
|
|
|