Files
dashcaddy/status/js/tooltip-definitions.js
Sami f61e85d9a7 Initial commit: DashCaddy v1.0
Full codebase including API server (32 modules + routes), dashboard frontend,
DashCA certificate distribution, installer script, and deployment skills.
2026-03-05 02:26:12 -08:00

539 lines
18 KiB
JavaScript

/**
* Tooltip Definitions
* Defines all tooltip content, positioning, and behavior for the onboarding system
*/
(function(window) {
'use strict';
/**
* 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');
console.error('[TooltipDefinitions] Validation errors:', errorMessages);
throw new TooltipError(`Tooltip validation failed:\n${errorMessages}`);
}
}
// Export to global scope
window.TooltipValidation = {
validateTooltipDefinition,
validateTooltipDefinitions,
handleValidationErrors,
TooltipError
};
console.log('[TooltipDefinitions] Validation module loaded');
})(window);
/**
* 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> &mdash; 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> &mdash; your Technitium DNS servers</li>
<li><strong>Internet</strong> &mdash; live connectivity check with packet indicators</li>
<li><strong>Auth</strong> &mdash; TOTP authentication status</li>
<li><strong>DashCA</strong> &mdash; 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> &mdash; green = online, red = offline (pulses when down)</li>
<li><strong>ON/OFF badge</strong> &mdash; current state at a glance</li>
<li><strong>Response time</strong> &mdash; how fast the service responds (color-coded)</li>
<li><strong>Uptime bar</strong> &mdash; 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 &mdash; 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 &mdash; 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;">&#9733; 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 &mdash; 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> &mdash; auto-detects running containers</li>
<li><strong>External services</strong> &mdash; any URL you want to monitor</li>
<li><strong>Test services</strong> &mdash; 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 &rarr; 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> &mdash; live CPU, memory, and disk usage for every container</li>
<li><strong>Health</strong> &mdash; service health dashboard with uptime history and alerts</li>
<li><strong>Updates</strong> &mdash; 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> &mdash; view error logs from your API server and services</li>
<li><strong>Alerts</strong> &mdash; configure notification rules (email, webhook, etc.)</li>
<li><strong>Audit</strong> &mdash; 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 &amp; Configuration',
description: `
<p>Click <strong>Admin</strong> for setup and maintenance:</p>
<ul>
<li><strong>Tokens</strong> &mdash; manage API keys for DNS and other integrations</li>
<li><strong>Export / Import</strong> &mdash; backup and restore your dashboard configuration</li>
<li><strong>Backup</strong> &mdash; full system backup and restore</li>
<li><strong>License</strong> &mdash; activate your license code to unlock premium features</li>
<li><strong>API</strong> &mdash; 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 &mdash; 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 &mdash; 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> &mdash; press <code>?</code> anytime to see them all</li>
<li><strong>DashCA</strong> &mdash; 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;">&#9733; 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> &mdash; sign into every app automatically, no more password juggling</li>
<li><strong>Recipes</strong> &mdash; deploy full stacks (media server, dev environment) in one click</li>
<li><strong>Docker Swarm</strong> &mdash; 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 &rarr; 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 &rarr; 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) {
console.error(`[TooltipDefinitions] Error evaluating condition for ${tooltip.id}:`, error);
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
};
console.log('[TooltipDefinitions] Definitions loaded:', TOOLTIP_DEFINITIONS.length, 'tooltips');