Full codebase including API server (32 modules + routes), dashboard frontend, DashCA certificate distribution, installer script, and deployment skills.
623 lines
18 KiB
JavaScript
623 lines
18 KiB
JavaScript
/**
|
||
* DashCaddy Keyboard Shortcuts System
|
||
* Provides global keyboard shortcuts for improved navigation
|
||
*/
|
||
|
||
(function() {
|
||
'use strict';
|
||
|
||
// All modal selectors that can be closed with Escape
|
||
const MODAL_SELECTORS = [
|
||
'#app-selector-modal',
|
||
'#app-deploy-modal',
|
||
'#weather-modal',
|
||
'#token-management-modal',
|
||
'#service-edit-modal',
|
||
'#notifications-modal',
|
||
'#backup-modal',
|
||
'#stats-modal',
|
||
'#arr-setup-modal',
|
||
'#add-service-modal',
|
||
'#error-log-modal',
|
||
'#logs-modal',
|
||
'#dns-template-modal'
|
||
];
|
||
|
||
// Quick search state
|
||
let quickSearchModal = null;
|
||
let quickSearchInput = null;
|
||
let quickSearchResults = null;
|
||
|
||
/**
|
||
* Initialize the keyboard shortcuts system
|
||
*/
|
||
function init() {
|
||
try {
|
||
// Create quick search modal
|
||
createQuickSearchModal();
|
||
|
||
// Add global keyboard listener
|
||
document.addEventListener('keydown', handleKeyDown);
|
||
|
||
console.log('[Keyboard Shortcuts] Initialized');
|
||
console.log('[Keyboard Shortcuts] Press Ctrl+K to open quick search');
|
||
console.log('[Keyboard Shortcuts] Press Escape to close modals');
|
||
} catch (e) {
|
||
console.warn('[Keyboard Shortcuts] Failed to initialize:', e.message);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Create the quick search modal
|
||
*/
|
||
function createQuickSearchModal() {
|
||
quickSearchModal = document.createElement('div');
|
||
quickSearchModal.id = 'quick-search-modal';
|
||
quickSearchModal.className = 'quick-search-modal';
|
||
quickSearchModal.innerHTML = `
|
||
<div class="quick-search-content">
|
||
<div class="quick-search-input-wrapper">
|
||
<span class="quick-search-icon">🔍</span>
|
||
<input type="text" id="quick-search-input" placeholder="Search services, apps, or actions..." autocomplete="off">
|
||
<span class="quick-search-shortcut">ESC</span>
|
||
</div>
|
||
<div id="quick-search-results" class="quick-search-results"></div>
|
||
<div class="quick-search-footer">
|
||
<span><kbd>↑↓</kbd> Navigate</span>
|
||
<span><kbd>Enter</kbd> Select</span>
|
||
<span><kbd>Esc</kbd> Close</span>
|
||
</div>
|
||
</div>
|
||
`;
|
||
|
||
// Add styles
|
||
const style = document.createElement('style');
|
||
style.textContent = `
|
||
.quick-search-modal {
|
||
display: none;
|
||
position: fixed;
|
||
top: 0;
|
||
left: 0;
|
||
right: 0;
|
||
bottom: 0;
|
||
background: rgba(0, 0, 0, 0.7);
|
||
z-index: 100000;
|
||
justify-content: center;
|
||
align-items: flex-start;
|
||
padding-top: 15vh;
|
||
backdrop-filter: blur(4px);
|
||
}
|
||
|
||
.quick-search-modal.show {
|
||
display: flex;
|
||
}
|
||
|
||
.quick-search-content {
|
||
background: var(--card-base, #1a1a2e);
|
||
border: 1px solid var(--border, #333);
|
||
border-radius: 12px;
|
||
width: 90%;
|
||
max-width: 600px;
|
||
box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.5);
|
||
overflow: hidden;
|
||
}
|
||
|
||
.quick-search-input-wrapper {
|
||
display: flex;
|
||
align-items: center;
|
||
padding: 16px 20px;
|
||
border-bottom: 1px solid var(--border, #333);
|
||
gap: 12px;
|
||
}
|
||
|
||
.quick-search-icon {
|
||
font-size: 20px;
|
||
opacity: 0.6;
|
||
}
|
||
|
||
#quick-search-input {
|
||
flex: 1;
|
||
background: transparent;
|
||
border: none;
|
||
outline: none;
|
||
color: var(--fg, #fff);
|
||
font-size: 18px;
|
||
font-family: inherit;
|
||
}
|
||
|
||
#quick-search-input::placeholder {
|
||
color: var(--muted, #666);
|
||
}
|
||
|
||
.quick-search-shortcut {
|
||
background: var(--card-hover, #2a2a4e);
|
||
padding: 4px 8px;
|
||
border-radius: 4px;
|
||
font-size: 12px;
|
||
color: var(--muted, #666);
|
||
font-family: monospace;
|
||
}
|
||
|
||
.quick-search-results {
|
||
max-height: 400px;
|
||
overflow-y: auto;
|
||
}
|
||
|
||
.quick-search-category {
|
||
padding: 8px 20px;
|
||
font-size: 11px;
|
||
font-weight: 600;
|
||
text-transform: uppercase;
|
||
color: var(--muted, #666);
|
||
background: var(--card-hover, #2a2a4e);
|
||
letter-spacing: 0.5px;
|
||
}
|
||
|
||
.quick-search-item {
|
||
display: flex;
|
||
align-items: center;
|
||
padding: 12px 20px;
|
||
cursor: pointer;
|
||
transition: background 0.15s;
|
||
gap: 12px;
|
||
}
|
||
|
||
.quick-search-item:hover,
|
||
.quick-search-item.selected {
|
||
background: var(--accent, #3498db);
|
||
}
|
||
|
||
.quick-search-item-icon {
|
||
font-size: 24px;
|
||
width: 32px;
|
||
text-align: center;
|
||
}
|
||
|
||
.quick-search-item-content {
|
||
flex: 1;
|
||
}
|
||
|
||
.quick-search-item-title {
|
||
font-weight: 500;
|
||
color: var(--fg, #fff);
|
||
}
|
||
|
||
.quick-search-item-description {
|
||
font-size: 12px;
|
||
color: var(--muted, #aaa);
|
||
margin-top: 2px;
|
||
}
|
||
|
||
.quick-search-item-badge {
|
||
padding: 2px 8px;
|
||
border-radius: 4px;
|
||
font-size: 11px;
|
||
background: var(--card-hover, #2a2a4e);
|
||
color: var(--muted, #888);
|
||
}
|
||
|
||
.quick-search-empty {
|
||
padding: 40px 20px;
|
||
text-align: center;
|
||
color: var(--muted, #666);
|
||
}
|
||
|
||
.quick-search-footer {
|
||
display: flex;
|
||
justify-content: center;
|
||
gap: 24px;
|
||
padding: 12px 20px;
|
||
border-top: 1px solid var(--border, #333);
|
||
background: var(--card-hover, #2a2a4e);
|
||
font-size: 12px;
|
||
color: var(--muted, #666);
|
||
}
|
||
|
||
.quick-search-footer kbd {
|
||
background: var(--card-base, #1a1a2e);
|
||
padding: 2px 6px;
|
||
border-radius: 4px;
|
||
font-family: monospace;
|
||
margin-right: 4px;
|
||
}
|
||
`;
|
||
|
||
document.head.appendChild(style);
|
||
document.body.appendChild(quickSearchModal);
|
||
|
||
quickSearchInput = document.getElementById('quick-search-input');
|
||
quickSearchResults = document.getElementById('quick-search-results');
|
||
|
||
// Input handling
|
||
quickSearchInput.addEventListener('input', handleSearchInput);
|
||
quickSearchInput.addEventListener('keydown', handleSearchKeyDown);
|
||
|
||
// Close on backdrop click
|
||
quickSearchModal.addEventListener('click', (e) => {
|
||
if (e.target === quickSearchModal) {
|
||
closeQuickSearch();
|
||
}
|
||
});
|
||
}
|
||
|
||
/**
|
||
* Handle global keydown events
|
||
*/
|
||
function handleKeyDown(e) {
|
||
try {
|
||
// Ctrl+K or Cmd+K to open quick search
|
||
if ((e.ctrlKey || e.metaKey) && e.key === 'k') {
|
||
e.preventDefault();
|
||
openQuickSearch();
|
||
return;
|
||
}
|
||
|
||
// Escape to close modals
|
||
if (e.key === 'Escape') {
|
||
// First check if quick search is open
|
||
if (quickSearchModal && quickSearchModal.classList.contains('show')) {
|
||
closeQuickSearch();
|
||
return;
|
||
}
|
||
|
||
// Then close any other open modal
|
||
closeTopModal();
|
||
}
|
||
} catch (e2) {
|
||
console.warn('[Keyboard Shortcuts] Error handling keydown:', e2.message);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Open quick search modal
|
||
*/
|
||
function openQuickSearch() {
|
||
try {
|
||
quickSearchModal.classList.add('show');
|
||
quickSearchInput.value = '';
|
||
quickSearchInput.focus();
|
||
showDefaultResults();
|
||
} catch (e) {
|
||
console.warn('[Keyboard Shortcuts] Error opening quick search:', e.message);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Close quick search modal
|
||
*/
|
||
function closeQuickSearch() {
|
||
try {
|
||
quickSearchModal.classList.remove('show');
|
||
quickSearchInput.value = '';
|
||
quickSearchResults.innerHTML = '';
|
||
} catch (e) {
|
||
console.warn('[Keyboard Shortcuts] Error closing quick search:', e.message);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Close the topmost open modal
|
||
*/
|
||
function closeTopModal() {
|
||
for (const selector of MODAL_SELECTORS) {
|
||
const modal = document.querySelector(selector);
|
||
if (modal && (modal.classList.contains('show') || modal.style.display === 'flex')) {
|
||
modal.classList.remove('show');
|
||
modal.style.display = 'none';
|
||
return true;
|
||
}
|
||
}
|
||
return false;
|
||
}
|
||
|
||
/**
|
||
* Show default/suggested results
|
||
*/
|
||
function showDefaultResults() {
|
||
const html = `
|
||
<div class="quick-search-category">Quick Actions</div>
|
||
<div class="quick-search-item" data-action="refresh">
|
||
<span class="quick-search-item-icon">🔄</span>
|
||
<div class="quick-search-item-content">
|
||
<div class="quick-search-item-title">Refresh Dashboard</div>
|
||
<div class="quick-search-item-description">Refresh all service statuses</div>
|
||
</div>
|
||
</div>
|
||
<div class="quick-search-item" data-action="reload-caddy">
|
||
<span class="quick-search-item-icon">⚡</span>
|
||
<div class="quick-search-item-content">
|
||
<div class="quick-search-item-title">Reload Caddy</div>
|
||
<div class="quick-search-item-description">Reload Caddy configuration</div>
|
||
</div>
|
||
</div>
|
||
<div class="quick-search-item" data-action="add-service">
|
||
<span class="quick-search-item-icon">➕</span>
|
||
<div class="quick-search-item-content">
|
||
<div class="quick-search-item-title">Add Service</div>
|
||
<div class="quick-search-item-description">Open service configuration modal</div>
|
||
</div>
|
||
</div>
|
||
<div class="quick-search-item" data-action="app-selector">
|
||
<span class="quick-search-item-icon">📱</span>
|
||
<div class="quick-search-item-content">
|
||
<div class="quick-search-item-title">App Selector</div>
|
||
<div class="quick-search-item-description">Deploy new applications</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="quick-search-category">Services</div>
|
||
${getServiceItems()}
|
||
`;
|
||
|
||
quickSearchResults.innerHTML = html;
|
||
attachResultListeners();
|
||
}
|
||
|
||
/**
|
||
* Get service items from the dashboard
|
||
*/
|
||
function getServiceItems() {
|
||
const cards = document.querySelectorAll('.card[data-app], #cards .card');
|
||
let html = '';
|
||
|
||
cards.forEach(card => {
|
||
const name = card.querySelector('.name')?.textContent || 'Unknown';
|
||
const status = card.dataset.status || 'unknown';
|
||
const app = card.dataset.app || '';
|
||
|
||
if (name && name !== '--') {
|
||
html += `
|
||
<div class="quick-search-item" data-action="open-service" data-service="${app}">
|
||
<span class="quick-search-item-icon">${status === 'on' ? '🟢' : '🔴'}</span>
|
||
<div class="quick-search-item-content">
|
||
<div class="quick-search-item-title">${name}</div>
|
||
<div class="quick-search-item-description">Click to open service</div>
|
||
</div>
|
||
<span class="quick-search-item-badge">${status.toUpperCase()}</span>
|
||
</div>
|
||
`;
|
||
}
|
||
});
|
||
|
||
return html || '<div class="quick-search-empty">No services found</div>';
|
||
}
|
||
|
||
/**
|
||
* Handle search input
|
||
*/
|
||
function handleSearchInput(e) {
|
||
try {
|
||
const query = e.target.value.toLowerCase().trim();
|
||
|
||
if (!query) {
|
||
showDefaultResults();
|
||
return;
|
||
}
|
||
|
||
// Search through services and actions
|
||
const results = searchAll(query);
|
||
displaySearchResults(results);
|
||
} catch (e2) {
|
||
console.warn('[Keyboard Shortcuts] Error handling search input:', e2.message);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Search all items
|
||
*/
|
||
function searchAll(query) {
|
||
const results = {
|
||
actions: [],
|
||
services: []
|
||
};
|
||
|
||
// Search actions
|
||
const actions = [
|
||
{ id: 'refresh', title: 'Refresh Dashboard', icon: '🔄', keywords: 'refresh reload update status' },
|
||
{ id: 'reload-caddy', title: 'Reload Caddy', icon: '⚡', keywords: 'reload caddy proxy config' },
|
||
{ id: 'add-service', title: 'Add Service', icon: '➕', keywords: 'add new service create' },
|
||
{ id: 'app-selector', title: 'App Selector', icon: '📱', keywords: 'app deploy install docker container' },
|
||
{ id: 'backup', title: 'Backup & Restore', icon: '💾', keywords: 'backup restore export import' },
|
||
{ id: 'stats', title: 'Container Stats', icon: '📊', keywords: 'stats resources cpu memory' },
|
||
{ id: 'logs', title: 'View Logs', icon: '📋', keywords: 'logs error debug' },
|
||
{ id: 'tokens', title: 'Manage Tokens', icon: '🔑', keywords: 'tokens api keys credentials' },
|
||
{ id: 'notifications', title: 'Notifications', icon: '🔔', keywords: 'alerts notifications discord telegram' },
|
||
{ id: 'theme', title: 'Change Theme', icon: '🎨', keywords: 'theme dark light appearance' },
|
||
{ id: 'tour', title: 'Help Tour', icon: '🎓', keywords: 'help tour guide onboarding' }
|
||
];
|
||
|
||
actions.forEach(action => {
|
||
if (action.title.toLowerCase().includes(query) || action.keywords.includes(query)) {
|
||
results.actions.push(action);
|
||
}
|
||
});
|
||
|
||
// Search services
|
||
const cards = document.querySelectorAll('.card[data-app], #cards .card');
|
||
cards.forEach(card => {
|
||
const name = card.querySelector('.name')?.textContent || '';
|
||
const app = card.dataset.app || '';
|
||
const status = card.dataset.status || 'unknown';
|
||
|
||
if (name.toLowerCase().includes(query) || app.toLowerCase().includes(query)) {
|
||
results.services.push({
|
||
id: app,
|
||
title: name,
|
||
status: status,
|
||
icon: status === 'on' ? '🟢' : '🔴'
|
||
});
|
||
}
|
||
});
|
||
|
||
return results;
|
||
}
|
||
|
||
/**
|
||
* Display search results
|
||
*/
|
||
function displaySearchResults(results) {
|
||
let html = '';
|
||
|
||
if (results.actions.length > 0) {
|
||
html += '<div class="quick-search-category">Actions</div>';
|
||
results.actions.forEach(action => {
|
||
html += `
|
||
<div class="quick-search-item" data-action="${action.id}">
|
||
<span class="quick-search-item-icon">${action.icon}</span>
|
||
<div class="quick-search-item-content">
|
||
<div class="quick-search-item-title">${action.title}</div>
|
||
</div>
|
||
</div>
|
||
`;
|
||
});
|
||
}
|
||
|
||
if (results.services.length > 0) {
|
||
html += '<div class="quick-search-category">Services</div>';
|
||
results.services.forEach(service => {
|
||
html += `
|
||
<div class="quick-search-item" data-action="open-service" data-service="${service.id}">
|
||
<span class="quick-search-item-icon">${service.icon}</span>
|
||
<div class="quick-search-item-content">
|
||
<div class="quick-search-item-title">${service.title}</div>
|
||
</div>
|
||
<span class="quick-search-item-badge">${service.status.toUpperCase()}</span>
|
||
</div>
|
||
`;
|
||
});
|
||
}
|
||
|
||
if (!html) {
|
||
html = '<div class="quick-search-empty">No results found</div>';
|
||
}
|
||
|
||
quickSearchResults.innerHTML = html;
|
||
attachResultListeners();
|
||
}
|
||
|
||
/**
|
||
* Attach click listeners to result items
|
||
*/
|
||
function attachResultListeners() {
|
||
const items = quickSearchResults.querySelectorAll('.quick-search-item');
|
||
items.forEach((item, index) => {
|
||
item.addEventListener('click', () => executeAction(item));
|
||
|
||
// Select first item by default
|
||
if (index === 0) {
|
||
item.classList.add('selected');
|
||
}
|
||
});
|
||
}
|
||
|
||
/**
|
||
* Handle keyboard navigation in search
|
||
*/
|
||
function handleSearchKeyDown(e) {
|
||
try {
|
||
const items = quickSearchResults.querySelectorAll('.quick-search-item');
|
||
const selected = quickSearchResults.querySelector('.quick-search-item.selected');
|
||
const selectedIndex = Array.from(items).indexOf(selected);
|
||
|
||
if (e.key === 'ArrowDown') {
|
||
e.preventDefault();
|
||
if (selected) selected.classList.remove('selected');
|
||
const nextIndex = (selectedIndex + 1) % items.length;
|
||
items[nextIndex]?.classList.add('selected');
|
||
items[nextIndex]?.scrollIntoView({ block: 'nearest' });
|
||
} else if (e.key === 'ArrowUp') {
|
||
e.preventDefault();
|
||
if (selected) selected.classList.remove('selected');
|
||
const prevIndex = selectedIndex <= 0 ? items.length - 1 : selectedIndex - 1;
|
||
items[prevIndex]?.classList.add('selected');
|
||
items[prevIndex]?.scrollIntoView({ block: 'nearest' });
|
||
} else if (e.key === 'Enter') {
|
||
e.preventDefault();
|
||
if (selected) {
|
||
executeAction(selected);
|
||
}
|
||
}
|
||
} catch (e2) {
|
||
console.warn('[Keyboard Shortcuts] Error handling search navigation:', e2.message);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Execute an action from quick search
|
||
*/
|
||
function executeAction(item) {
|
||
try {
|
||
const action = item.dataset.action;
|
||
const service = item.dataset.service;
|
||
|
||
closeQuickSearch();
|
||
|
||
switch (action) {
|
||
case 'refresh':
|
||
document.getElementById('refresh')?.click();
|
||
break;
|
||
case 'reload-caddy':
|
||
document.getElementById('reload-caddy-top')?.click();
|
||
break;
|
||
case 'add-service':
|
||
document.getElementById('add-service')?.click();
|
||
break;
|
||
case 'app-selector':
|
||
document.getElementById('add-service-btn')?.click();
|
||
break;
|
||
case 'backup':
|
||
document.getElementById('backup-restore-btn')?.click();
|
||
break;
|
||
case 'stats':
|
||
document.getElementById('container-stats-btn')?.click();
|
||
break;
|
||
case 'logs':
|
||
document.getElementById('view-error-logs')?.click();
|
||
break;
|
||
case 'tokens':
|
||
document.getElementById('manage-tokens')?.click();
|
||
break;
|
||
case 'notifications':
|
||
document.getElementById('manage-notifications')?.click();
|
||
break;
|
||
case 'theme':
|
||
document.getElementById('theme')?.click();
|
||
break;
|
||
case 'tour':
|
||
document.getElementById('restart-tour-btn')?.click();
|
||
break;
|
||
case 'open-service':
|
||
if (service) {
|
||
const openBtn = document.querySelector(`[data-app="${service}"] [id$="-open"], [data-app="${service}"] button:not(.restart-btn):not(.logs-btn):not(.settings-btn)`);
|
||
if (openBtn) {
|
||
openBtn.click();
|
||
} else {
|
||
// Try to find the card and click it
|
||
const card = document.querySelector(`[data-app="${service}"]`);
|
||
if (card) card.click();
|
||
}
|
||
}
|
||
break;
|
||
default:
|
||
console.log('[Keyboard Shortcuts] Unknown action:', action);
|
||
}
|
||
} catch (e) {
|
||
console.warn('[Keyboard Shortcuts] Error executing action:', e.message);
|
||
}
|
||
}
|
||
|
||
// Initialize when DOM is ready
|
||
if (document.readyState === 'loading') {
|
||
document.addEventListener('DOMContentLoaded', init);
|
||
} else {
|
||
init();
|
||
}
|
||
|
||
// Expose to global scope
|
||
window.DashCaddyKeyboardShortcuts = {
|
||
openQuickSearch,
|
||
closeQuickSearch
|
||
};
|
||
|
||
})();
|