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:
200
static/css/input.css
Normal file
200
static/css/input.css
Normal file
@@ -0,0 +1,200 @@
|
||||
@import "tailwindcss";
|
||||
|
||||
/* Source the templates */
|
||||
@source "../../templates/**/*.html";
|
||||
|
||||
/* Material Design inspired theme customization */
|
||||
@theme {
|
||||
/* Primary colors */
|
||||
--color-primary-50: #e3f2fd;
|
||||
--color-primary-100: #bbdefb;
|
||||
--color-primary-200: #90caf9;
|
||||
--color-primary-300: #64b5f6;
|
||||
--color-primary-400: #42a5f5;
|
||||
--color-primary-500: #2196f3;
|
||||
--color-primary-600: #1e88e5;
|
||||
--color-primary-700: #1976d2;
|
||||
--color-primary-800: #1565c0;
|
||||
--color-primary-900: #0d47a1;
|
||||
|
||||
/* Error colors */
|
||||
--color-error-50: #ffebee;
|
||||
--color-error-500: #f44336;
|
||||
--color-error-700: #d32f2f;
|
||||
|
||||
/* Success colors */
|
||||
--color-success-50: #e8f5e9;
|
||||
--color-success-500: #4caf50;
|
||||
--color-success-700: #388e3c;
|
||||
|
||||
/* Warning colors */
|
||||
--color-warning-50: #fff3e0;
|
||||
--color-warning-500: #ff9800;
|
||||
--color-warning-700: #f57c00;
|
||||
|
||||
/* Material Design elevation shadows */
|
||||
--shadow-elevation-1: 0 1px 3px rgba(0,0,0,0.12), 0 1px 2px rgba(0,0,0,0.24);
|
||||
--shadow-elevation-2: 0 3px 6px rgba(0,0,0,0.16), 0 3px 6px rgba(0,0,0,0.23);
|
||||
--shadow-elevation-3: 0 10px 20px rgba(0,0,0,0.19), 0 6px 6px rgba(0,0,0,0.23);
|
||||
}
|
||||
|
||||
/* Material Design component styles */
|
||||
@layer components {
|
||||
/* Buttons - base styles inlined */
|
||||
.btn-primary {
|
||||
@apply inline-flex items-center justify-center px-4 py-2 rounded-md font-medium text-sm transition-all duration-200 focus:outline-none focus:ring-2 focus:ring-offset-2 disabled:opacity-50 disabled:cursor-not-allowed bg-primary-600 text-white hover:bg-primary-700 active:bg-primary-800 focus:ring-primary-500 shadow-elevation-1 hover:shadow-elevation-2;
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
@apply inline-flex items-center justify-center px-4 py-2 rounded-md font-medium text-sm transition-all duration-200 focus:outline-none focus:ring-2 focus:ring-offset-2 disabled:opacity-50 disabled:cursor-not-allowed bg-white text-gray-700 border border-gray-300 hover:bg-gray-50 active:bg-gray-100 focus:ring-primary-500;
|
||||
}
|
||||
|
||||
.btn-danger {
|
||||
@apply inline-flex items-center justify-center px-4 py-2 rounded-md font-medium text-sm transition-all duration-200 focus:outline-none focus:ring-2 focus:ring-offset-2 disabled:opacity-50 disabled:cursor-not-allowed bg-error-500 text-white hover:bg-error-700 active:bg-red-800 focus:ring-red-500 shadow-elevation-1 hover:shadow-elevation-2;
|
||||
}
|
||||
|
||||
.btn-success {
|
||||
@apply inline-flex items-center justify-center px-4 py-2 rounded-md font-medium text-sm transition-all duration-200 focus:outline-none focus:ring-2 focus:ring-offset-2 disabled:opacity-50 disabled:cursor-not-allowed bg-success-500 text-white hover:bg-success-700 active:bg-green-800 focus:ring-green-500 shadow-elevation-1 hover:shadow-elevation-2;
|
||||
}
|
||||
|
||||
.btn-text {
|
||||
@apply inline-flex items-center justify-center px-4 py-2 rounded-md font-medium text-sm transition-all duration-200 focus:outline-none focus:ring-2 focus:ring-offset-2 disabled:opacity-50 disabled:cursor-not-allowed text-primary-600 hover:bg-primary-50 active:bg-primary-100;
|
||||
}
|
||||
|
||||
.btn-icon {
|
||||
@apply p-2 rounded-full hover:bg-gray-100 active:bg-gray-200 transition-colors;
|
||||
}
|
||||
|
||||
/* Cards */
|
||||
.card {
|
||||
@apply bg-white rounded-lg shadow-elevation-1 overflow-hidden;
|
||||
}
|
||||
|
||||
.card-elevated {
|
||||
@apply bg-white rounded-lg shadow-elevation-1 overflow-hidden hover:shadow-elevation-2 transition-shadow;
|
||||
}
|
||||
|
||||
/* Form inputs - Material Design style */
|
||||
.input {
|
||||
@apply w-full px-4 py-3 border border-gray-300 rounded-md text-gray-900 placeholder-gray-500 focus:outline-none focus:ring-2 focus:ring-primary-500 focus:border-transparent transition-all;
|
||||
}
|
||||
|
||||
.input-error {
|
||||
@apply w-full px-4 py-3 border border-error-500 rounded-md text-gray-900 placeholder-gray-500 focus:outline-none focus:ring-2 focus:ring-error-500 focus:border-transparent transition-all;
|
||||
}
|
||||
|
||||
.label {
|
||||
@apply block text-sm font-medium text-gray-700 mb-1;
|
||||
}
|
||||
|
||||
.form-group {
|
||||
@apply mb-4;
|
||||
}
|
||||
|
||||
/* Status badges */
|
||||
.badge-success {
|
||||
@apply inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-success-50 text-success-700;
|
||||
}
|
||||
|
||||
.badge-warning {
|
||||
@apply inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-warning-50 text-warning-700;
|
||||
}
|
||||
|
||||
.badge-error {
|
||||
@apply inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-error-50 text-error-700;
|
||||
}
|
||||
|
||||
.badge-info {
|
||||
@apply inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-primary-50 text-primary-700;
|
||||
}
|
||||
|
||||
.badge-neutral {
|
||||
@apply inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-gray-100 text-gray-700;
|
||||
}
|
||||
|
||||
/* Tables */
|
||||
.table {
|
||||
@apply min-w-full divide-y divide-gray-200;
|
||||
}
|
||||
|
||||
.table-header {
|
||||
@apply bg-gray-50;
|
||||
}
|
||||
|
||||
.table-header th {
|
||||
@apply px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider;
|
||||
}
|
||||
|
||||
.table-body {
|
||||
@apply bg-white divide-y divide-gray-200;
|
||||
}
|
||||
|
||||
.table-body td {
|
||||
@apply px-6 py-4 whitespace-nowrap text-sm;
|
||||
}
|
||||
|
||||
.table-row-hover:hover {
|
||||
@apply bg-gray-50;
|
||||
}
|
||||
|
||||
/* App bar / Navigation */
|
||||
.app-bar {
|
||||
@apply bg-white shadow-elevation-1 px-6 py-4;
|
||||
}
|
||||
|
||||
/* Copy button styling */
|
||||
.copy-field {
|
||||
@apply flex items-center gap-2 bg-gray-100 rounded-md p-2 font-mono text-sm;
|
||||
}
|
||||
|
||||
.copy-field-value {
|
||||
@apply flex-1 overflow-x-auto whitespace-nowrap;
|
||||
}
|
||||
|
||||
.copy-btn {
|
||||
@apply p-2 rounded-full hover:bg-gray-200 active:bg-gray-300 transition-colors text-gray-500 hover:text-gray-700 shrink-0;
|
||||
}
|
||||
|
||||
/* Alert / Message boxes */
|
||||
.alert-error {
|
||||
@apply p-4 rounded-md mb-4 bg-error-50 text-error-700 border border-error-500/20;
|
||||
}
|
||||
|
||||
.alert-success {
|
||||
@apply p-4 rounded-md mb-4 bg-success-50 text-success-700 border border-success-500/20;
|
||||
}
|
||||
|
||||
.alert-warning {
|
||||
@apply p-4 rounded-md mb-4 bg-warning-50 text-warning-700 border border-warning-500/20;
|
||||
}
|
||||
|
||||
.alert-info {
|
||||
@apply p-4 rounded-md mb-4 bg-primary-50 text-primary-700 border border-primary-500/20;
|
||||
}
|
||||
|
||||
/* Section headers */
|
||||
.section-header {
|
||||
@apply flex items-center justify-between pb-4 border-b border-gray-200 mb-4;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
@apply text-lg font-medium text-gray-900;
|
||||
}
|
||||
|
||||
/* Empty state */
|
||||
.empty-state {
|
||||
@apply text-center py-12;
|
||||
}
|
||||
|
||||
.empty-state-icon {
|
||||
@apply mx-auto h-12 w-12 text-gray-400;
|
||||
}
|
||||
|
||||
.empty-state-title {
|
||||
@apply mt-2 text-sm font-medium text-gray-900;
|
||||
}
|
||||
|
||||
.empty-state-description {
|
||||
@apply mt-1 text-sm text-gray-500;
|
||||
}
|
||||
}
|
||||
2
static/css/tailwind.css
Normal file
2
static/css/tailwind.css
Normal file
File diff suppressed because one or more lines are too long
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
|
||||
};
|
||||
|
||||
})();
|
||||
9
static/static.go
Normal file
9
static/static.go
Normal file
@@ -0,0 +1,9 @@
|
||||
// Package static provides embedded static assets.
|
||||
package static
|
||||
|
||||
import "embed"
|
||||
|
||||
// Static contains embedded CSS and JavaScript files for serving web assets.
|
||||
//
|
||||
//go:embed css js
|
||||
var Static embed.FS
|
||||
Reference in New Issue
Block a user