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:
2026-03-05 02:26:12 -08:00
commit f61e85d9a7
337 changed files with 75282 additions and 0 deletions

View 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
};
})();