Initial commit: DashCaddy v1.0
Full codebase including API server (32 modules + routes), dashboard frontend, DashCA certificate distribution, installer script, and deployment skills.
This commit is contained in:
622
status/js/keyboard-shortcuts.js
Normal file
622
status/js/keyboard-shortcuts.js
Normal file
@@ -0,0 +1,622 @@
|
||||
/**
|
||||
* 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
|
||||
};
|
||||
|
||||
})();
|
||||
Reference in New Issue
Block a user