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:
2025-12-29 15:46:03 +07:00
commit 3f9d83c436
59 changed files with 11707 additions and 0 deletions

200
static/css/input.css Normal file
View 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

File diff suppressed because one or more lines are too long

215
static/js/app.js Normal file
View 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
View 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