Initial commit with server startup infrastructure
Core infrastructure: - Uber fx dependency injection - Chi router with middleware stack - SQLite database with embedded migrations - Embedded templates and static assets - Structured logging with slog Features implemented: - Authentication (login, logout, session management, argon2id hashing) - App management (create, edit, delete, list) - Deployment pipeline (clone, build, deploy, health check) - Webhook processing for Gitea - Notifications (ntfy, Slack) - Environment variables, labels, volumes per app - SSH key generation for deploy keys Server startup: - Server.Run() starts HTTP server on configured port - Server.Shutdown() for graceful shutdown - SetupRoutes() wires all handlers with chi router
This commit is contained in:
215
static/js/app.js
Normal file
215
static/js/app.js
Normal file
@@ -0,0 +1,215 @@
|
||||
/**
|
||||
* upaas - Frontend JavaScript utilities
|
||||
* Vanilla JS, no dependencies
|
||||
*/
|
||||
|
||||
(function() {
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* Copy text to clipboard
|
||||
* @param {string} text - Text to copy
|
||||
* @param {HTMLElement} button - Button element to update feedback
|
||||
*/
|
||||
function copyToClipboard(text, button) {
|
||||
const originalText = button.textContent;
|
||||
const originalTitle = button.getAttribute('title');
|
||||
|
||||
navigator.clipboard.writeText(text).then(function() {
|
||||
// Success feedback
|
||||
button.textContent = 'Copied!';
|
||||
button.classList.add('text-success-500');
|
||||
|
||||
setTimeout(function() {
|
||||
button.textContent = originalText;
|
||||
button.classList.remove('text-success-500');
|
||||
if (originalTitle) {
|
||||
button.setAttribute('title', originalTitle);
|
||||
}
|
||||
}, 2000);
|
||||
}).catch(function(err) {
|
||||
// Fallback for older browsers
|
||||
console.error('Failed to copy:', err);
|
||||
|
||||
// Try fallback method
|
||||
var textArea = document.createElement('textarea');
|
||||
textArea.value = text;
|
||||
textArea.style.position = 'fixed';
|
||||
textArea.style.left = '-9999px';
|
||||
document.body.appendChild(textArea);
|
||||
textArea.select();
|
||||
|
||||
try {
|
||||
document.execCommand('copy');
|
||||
button.textContent = 'Copied!';
|
||||
setTimeout(function() {
|
||||
button.textContent = originalText;
|
||||
}, 2000);
|
||||
} catch (e) {
|
||||
button.textContent = 'Failed';
|
||||
setTimeout(function() {
|
||||
button.textContent = originalText;
|
||||
}, 2000);
|
||||
}
|
||||
|
||||
document.body.removeChild(textArea);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize copy buttons
|
||||
* Looks for elements with data-copy attribute
|
||||
*/
|
||||
function initCopyButtons() {
|
||||
var copyButtons = document.querySelectorAll('[data-copy]');
|
||||
|
||||
copyButtons.forEach(function(button) {
|
||||
button.addEventListener('click', function(e) {
|
||||
e.preventDefault();
|
||||
var text = button.getAttribute('data-copy');
|
||||
copyToClipboard(text, button);
|
||||
});
|
||||
});
|
||||
|
||||
// Also handle buttons that copy content from a sibling element
|
||||
var copyTargetButtons = document.querySelectorAll('[data-copy-target]');
|
||||
|
||||
copyTargetButtons.forEach(function(button) {
|
||||
button.addEventListener('click', function(e) {
|
||||
e.preventDefault();
|
||||
var targetId = button.getAttribute('data-copy-target');
|
||||
var target = document.getElementById(targetId);
|
||||
if (target) {
|
||||
var text = target.textContent || target.value;
|
||||
copyToClipboard(text, button);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Confirm destructive actions
|
||||
* Looks for forms with data-confirm attribute
|
||||
*/
|
||||
function initConfirmations() {
|
||||
var confirmForms = document.querySelectorAll('form[data-confirm]');
|
||||
|
||||
confirmForms.forEach(function(form) {
|
||||
form.addEventListener('submit', function(e) {
|
||||
var message = form.getAttribute('data-confirm');
|
||||
if (!confirm(message)) {
|
||||
e.preventDefault();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Also handle buttons with data-confirm
|
||||
var confirmButtons = document.querySelectorAll('button[data-confirm]');
|
||||
|
||||
confirmButtons.forEach(function(button) {
|
||||
button.addEventListener('click', function(e) {
|
||||
var message = button.getAttribute('data-confirm');
|
||||
if (!confirm(message)) {
|
||||
e.preventDefault();
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggle visibility of elements
|
||||
* Looks for buttons with data-toggle attribute
|
||||
*/
|
||||
function initToggles() {
|
||||
var toggleButtons = document.querySelectorAll('[data-toggle]');
|
||||
|
||||
toggleButtons.forEach(function(button) {
|
||||
button.addEventListener('click', function(e) {
|
||||
e.preventDefault();
|
||||
var targetId = button.getAttribute('data-toggle');
|
||||
var target = document.getElementById(targetId);
|
||||
if (target) {
|
||||
target.classList.toggle('hidden');
|
||||
|
||||
// Update button text if data-toggle-text is provided
|
||||
var toggleText = button.getAttribute('data-toggle-text');
|
||||
if (toggleText) {
|
||||
var currentText = button.textContent;
|
||||
button.textContent = toggleText;
|
||||
button.setAttribute('data-toggle-text', currentText);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Auto-dismiss alerts after a delay
|
||||
* Looks for elements with data-auto-dismiss attribute
|
||||
*/
|
||||
function initAutoDismiss() {
|
||||
var dismissElements = document.querySelectorAll('[data-auto-dismiss]');
|
||||
|
||||
dismissElements.forEach(function(element) {
|
||||
var delay = parseInt(element.getAttribute('data-auto-dismiss'), 10) || 5000;
|
||||
|
||||
setTimeout(function() {
|
||||
element.style.transition = 'opacity 0.3s ease-out';
|
||||
element.style.opacity = '0';
|
||||
|
||||
setTimeout(function() {
|
||||
element.remove();
|
||||
}, 300);
|
||||
}, delay);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Manual dismiss for alerts
|
||||
* Looks for buttons with data-dismiss attribute
|
||||
*/
|
||||
function initDismissButtons() {
|
||||
var dismissButtons = document.querySelectorAll('[data-dismiss]');
|
||||
|
||||
dismissButtons.forEach(function(button) {
|
||||
button.addEventListener('click', function(e) {
|
||||
e.preventDefault();
|
||||
var targetId = button.getAttribute('data-dismiss');
|
||||
var target = targetId ? document.getElementById(targetId) : button.closest('.alert');
|
||||
|
||||
if (target) {
|
||||
target.style.transition = 'opacity 0.3s ease-out';
|
||||
target.style.opacity = '0';
|
||||
|
||||
setTimeout(function() {
|
||||
target.remove();
|
||||
}, 300);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize all features when DOM is ready
|
||||
*/
|
||||
function init() {
|
||||
initCopyButtons();
|
||||
initConfirmations();
|
||||
initToggles();
|
||||
initAutoDismiss();
|
||||
initDismissButtons();
|
||||
}
|
||||
|
||||
// Run on DOM ready
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', init);
|
||||
} else {
|
||||
init();
|
||||
}
|
||||
|
||||
// Expose copyToClipboard globally for inline onclick handlers if needed
|
||||
window.upaas = {
|
||||
copyToClipboard: copyToClipboard
|
||||
};
|
||||
|
||||
})();
|
||||
Reference in New Issue
Block a user