Fix contrast issues with text-muted and bg-dark classes
- Fixed Bootstrap bg-dark class to use better contrasting color - Added comprehensive text-muted contrast fixes for various contexts - Improved dark theme colors for better accessibility - Fixed CSS inheritance issues for code elements in dark contexts - All color choices meet WCAG AA contrast requirements
This commit is contained in:
@@ -0,0 +1,547 @@
|
||||
/* UnitForge CSS Styles */
|
||||
|
||||
:root {
|
||||
--primary-color: #0d6efd;
|
||||
--secondary-color: #6c757d;
|
||||
--success-color: #198754;
|
||||
--danger-color: #dc3545;
|
||||
--warning-color: #ffc107;
|
||||
--info-color: #0dcaf0;
|
||||
--light-color: #f8f9fa;
|
||||
--dark-color: #212529;
|
||||
--border-radius: 0.375rem;
|
||||
--box-shadow: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.075);
|
||||
--box-shadow-lg: 0 0.5rem 1rem rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
/* General Styles */
|
||||
body {
|
||||
font-family: "Segoe UI", Tahoma, Geneva, Verdana, sans-serif;
|
||||
line-height: 1.6;
|
||||
color: var(--dark-color);
|
||||
}
|
||||
|
||||
.hero-section {
|
||||
background: linear-gradient(135deg, #f8f9fa 0%, #e9ecef 100%);
|
||||
border-bottom: 1px solid #dee2e6;
|
||||
}
|
||||
|
||||
/* Feature Icons */
|
||||
.feature-icon {
|
||||
width: 4rem;
|
||||
height: 4rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
/* Card Hover Effects */
|
||||
.hover-lift {
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.hover-lift:hover {
|
||||
transform: translateY(-5px);
|
||||
box-shadow: var(--box-shadow-lg);
|
||||
}
|
||||
|
||||
/* Unit Type Badges */
|
||||
.unit-type-badge {
|
||||
background: var(--light-color);
|
||||
border: 1px solid #dee2e6;
|
||||
border-radius: var(--border-radius);
|
||||
padding: 0.75rem 1rem;
|
||||
text-align: center;
|
||||
font-weight: 500;
|
||||
color: var(--dark-color);
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.unit-type-badge:hover {
|
||||
background: var(--primary-color);
|
||||
color: white;
|
||||
border-color: var(--primary-color);
|
||||
}
|
||||
|
||||
/* Code Preview */
|
||||
.code-preview {
|
||||
border-radius: var(--border-radius);
|
||||
box-shadow: var(--box-shadow-lg);
|
||||
}
|
||||
|
||||
.code-preview pre {
|
||||
margin: 0;
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.code-preview code {
|
||||
font-size: 0.875rem;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
/* Fix contrast for dark backgrounds */
|
||||
.bg-dark pre,
|
||||
.bg-dark code {
|
||||
color: #f8f9fa !important;
|
||||
}
|
||||
|
||||
.card.bg-dark .card-body pre,
|
||||
.card.bg-dark .card-body code {
|
||||
color: #f8f9fa !important;
|
||||
}
|
||||
|
||||
/* Override Bootstrap bg-dark for better contrast */
|
||||
.bg-dark {
|
||||
background-color: #1a202c !important;
|
||||
color: #f7fafc !important;
|
||||
}
|
||||
|
||||
.bg-dark pre,
|
||||
.bg-dark code {
|
||||
color: #f7fafc !important;
|
||||
}
|
||||
|
||||
/* Fix text-muted contrast issues */
|
||||
.text-muted {
|
||||
color: #6c757d !important;
|
||||
}
|
||||
|
||||
/* Better contrast for text-muted on dark backgrounds */
|
||||
.bg-dark .text-muted,
|
||||
.card.bg-dark .text-muted {
|
||||
color: #cbd5e0 !important;
|
||||
}
|
||||
|
||||
/* Better contrast for text-muted in cards */
|
||||
.card .text-muted {
|
||||
color: #495057 !important;
|
||||
}
|
||||
|
||||
/* Ensure text-muted in footer has proper contrast */
|
||||
footer.bg-dark .text-muted {
|
||||
color: #9ca3af !important;
|
||||
}
|
||||
|
||||
/* Fix text-muted in modals with dark backgrounds */
|
||||
.modal.bg-dark .text-muted,
|
||||
.modal-content.bg-dark .text-muted {
|
||||
color: #cbd5e0 !important;
|
||||
}
|
||||
|
||||
/* Better contrast for text-muted in forms on dark backgrounds */
|
||||
.bg-dark .form-text.text-muted {
|
||||
color: #a0aec0 !important;
|
||||
}
|
||||
|
||||
/* Specific override for hero section text-muted */
|
||||
.hero-section .text-muted {
|
||||
color: #495057 !important;
|
||||
}
|
||||
|
||||
/* Editor Specific Styles */
|
||||
.unit-type-fields {
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.unit-type-fields.d-none {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
#editor {
|
||||
font-family: "Courier New", Consolas, "Liberation Mono", monospace;
|
||||
font-size: 0.875rem;
|
||||
line-height: 1.5;
|
||||
border: none !important;
|
||||
resize: none;
|
||||
outline: none;
|
||||
box-shadow: none !important;
|
||||
}
|
||||
|
||||
#editor:focus {
|
||||
box-shadow: none !important;
|
||||
border: none !important;
|
||||
}
|
||||
|
||||
/* Validation Styles */
|
||||
.validation-error {
|
||||
background-color: #f8d7da;
|
||||
border-color: #f5c6cb;
|
||||
color: #721c24;
|
||||
padding: 0.75rem;
|
||||
margin-bottom: 0.5rem;
|
||||
border-radius: var(--border-radius);
|
||||
border-left: 4px solid var(--danger-color);
|
||||
}
|
||||
|
||||
.validation-warning {
|
||||
background-color: #fff3cd;
|
||||
border-color: #ffeaa7;
|
||||
color: #856404;
|
||||
padding: 0.75rem;
|
||||
margin-bottom: 0.5rem;
|
||||
border-radius: var(--border-radius);
|
||||
border-left: 4px solid var(--warning-color);
|
||||
}
|
||||
|
||||
.validation-success {
|
||||
background-color: #d1e7dd;
|
||||
border-color: #badbcc;
|
||||
color: #0f5132;
|
||||
padding: 0.75rem;
|
||||
margin-bottom: 0.5rem;
|
||||
border-radius: var(--border-radius);
|
||||
border-left: 4px solid var(--success-color);
|
||||
}
|
||||
|
||||
.validation-info {
|
||||
background-color: #cff4fc;
|
||||
border-color: #b8daff;
|
||||
color: #055160;
|
||||
padding: 0.75rem;
|
||||
margin-bottom: 0.5rem;
|
||||
border-radius: var(--border-radius);
|
||||
border-left: 4px solid var(--info-color);
|
||||
}
|
||||
|
||||
/* Info Items */
|
||||
.info-item {
|
||||
padding: 0.25rem 0;
|
||||
border-bottom: 1px solid #f1f3f4;
|
||||
}
|
||||
|
||||
.info-item:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
/* Help Content */
|
||||
.help-content {
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.help-item {
|
||||
padding: 0.25rem 0;
|
||||
}
|
||||
|
||||
/* Template Cards */
|
||||
.template-card {
|
||||
transition: all 0.3s ease;
|
||||
cursor: pointer;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.template-card:hover {
|
||||
transform: translateY(-3px);
|
||||
box-shadow: var(--box-shadow-lg);
|
||||
}
|
||||
|
||||
.template-card .card-header {
|
||||
background: linear-gradient(135deg, var(--primary-color), #0b5ed7);
|
||||
color: white;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.template-tags {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.25rem;
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
|
||||
.template-tag {
|
||||
background: var(--light-color);
|
||||
color: var(--secondary-color);
|
||||
padding: 0.125rem 0.5rem;
|
||||
border-radius: 1rem;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
/* Navigation Pills */
|
||||
.nav-pills .nav-link {
|
||||
color: var(--secondary-color);
|
||||
border-radius: var(--border-radius);
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.nav-pills .nav-link.active {
|
||||
background-color: var(--primary-color);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.nav-pills .nav-link:hover:not(.active) {
|
||||
background-color: var(--light-color);
|
||||
color: var(--dark-color);
|
||||
}
|
||||
|
||||
/* Form Styles */
|
||||
.form-control:focus {
|
||||
border-color: var(--primary-color);
|
||||
box-shadow: 0 0 0 0.2rem rgba(13, 110, 253, 0.25);
|
||||
}
|
||||
|
||||
.form-select:focus {
|
||||
border-color: var(--primary-color);
|
||||
box-shadow: 0 0 0 0.2rem rgba(13, 110, 253, 0.25);
|
||||
}
|
||||
|
||||
/* Button Styles */
|
||||
.btn {
|
||||
border-radius: var(--border-radius);
|
||||
font-weight: 500;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background-color: var(--primary-color);
|
||||
border-color: var(--primary-color);
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
background-color: #0b5ed7;
|
||||
border-color: #0a58ca;
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.btn-outline-primary:hover {
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
/* Modal Styles */
|
||||
.modal-content {
|
||||
border: none;
|
||||
border-radius: var(--border-radius);
|
||||
box-shadow: var(--box-shadow-lg);
|
||||
}
|
||||
|
||||
.modal-header {
|
||||
border-bottom: 1px solid #f1f3f4;
|
||||
background: var(--light-color);
|
||||
}
|
||||
|
||||
.modal-footer {
|
||||
border-top: 1px solid #f1f3f4;
|
||||
background: var(--light-color);
|
||||
}
|
||||
|
||||
/* Loading States */
|
||||
.spinner-border {
|
||||
width: 3rem;
|
||||
height: 3rem;
|
||||
}
|
||||
|
||||
/* Parameter Form Styles */
|
||||
.parameter-group {
|
||||
margin-bottom: 1rem;
|
||||
padding: 1rem;
|
||||
background: var(--light-color);
|
||||
border-radius: var(--border-radius);
|
||||
border: 1px solid #e9ecef;
|
||||
}
|
||||
|
||||
.parameter-label {
|
||||
font-weight: 600;
|
||||
color: var(--dark-color);
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
.parameter-description {
|
||||
font-size: 0.875rem;
|
||||
color: var(--secondary-color);
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.parameter-required {
|
||||
color: var(--danger-color);
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.parameter-optional {
|
||||
color: var(--secondary-color);
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
/* Preview Code Block */
|
||||
.preview-code {
|
||||
background: #1a202c;
|
||||
color: #f7fafc;
|
||||
border-radius: var(--border-radius);
|
||||
padding: 1rem;
|
||||
font-family: "Courier New", Consolas, "Liberation Mono", monospace;
|
||||
font-size: 0.875rem;
|
||||
line-height: 1.5;
|
||||
overflow-x: auto;
|
||||
max-height: 400px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
/* Responsive Adjustments */
|
||||
@media (max-width: 768px) {
|
||||
.hero-section {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.feature-icon {
|
||||
width: 3rem;
|
||||
height: 3rem;
|
||||
font-size: 1.25rem;
|
||||
}
|
||||
|
||||
.display-4 {
|
||||
font-size: 2.5rem;
|
||||
}
|
||||
|
||||
.nav-pills {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.nav-pills .nav-link {
|
||||
text-align: center;
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 576px) {
|
||||
.container-fluid .row > div {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.modal-dialog {
|
||||
margin: 0.5rem;
|
||||
}
|
||||
|
||||
.btn-group .btn {
|
||||
padding: 0.25rem 0.5rem;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
}
|
||||
|
||||
/* Animation Classes */
|
||||
.fade-in {
|
||||
animation: fadeIn 0.5s ease-in;
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(20px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
.slide-up {
|
||||
animation: slideUp 0.3s ease-out;
|
||||
}
|
||||
|
||||
@keyframes slideUp {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(30px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
/* Utility Classes */
|
||||
.text-monospace {
|
||||
font-family: "Courier New", Consolas, "Liberation Mono", monospace;
|
||||
}
|
||||
|
||||
.bg-gradient {
|
||||
background-image: linear-gradient(
|
||||
180deg,
|
||||
rgba(255, 255, 255, 0.15),
|
||||
rgba(255, 255, 255, 0)
|
||||
);
|
||||
}
|
||||
|
||||
.border-start-primary {
|
||||
border-left: 4px solid var(--primary-color) !important;
|
||||
}
|
||||
|
||||
.border-start-success {
|
||||
border-left: 4px solid var(--success-color) !important;
|
||||
}
|
||||
|
||||
.border-start-warning {
|
||||
border-left: 4px solid var(--warning-color) !important;
|
||||
}
|
||||
|
||||
.border-start-danger {
|
||||
border-left: 4px solid var(--danger-color) !important;
|
||||
}
|
||||
|
||||
/* Dark Theme Support */
|
||||
@media (prefers-color-scheme: dark) {
|
||||
.card {
|
||||
background-color: #2d3748;
|
||||
border-color: #4a5568;
|
||||
color: #f7fafc;
|
||||
}
|
||||
|
||||
.card pre,
|
||||
.card code {
|
||||
color: #f7fafc !important;
|
||||
}
|
||||
|
||||
.card .text-muted {
|
||||
color: #cbd5e0 !important;
|
||||
}
|
||||
|
||||
.text-muted {
|
||||
color: #a0aec0 !important;
|
||||
}
|
||||
|
||||
/* Better text-muted for dark theme forms */
|
||||
.form-text.text-muted {
|
||||
color: #9ca3af !important;
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
background-color: #2d3748;
|
||||
color: #f7fafc;
|
||||
}
|
||||
|
||||
.form-control {
|
||||
background-color: #2d3748;
|
||||
border-color: #4a5568;
|
||||
color: #f7fafc;
|
||||
}
|
||||
|
||||
.form-control:focus {
|
||||
background-color: #2d3748;
|
||||
border-color: var(--primary-color);
|
||||
color: #f7fafc;
|
||||
}
|
||||
}
|
||||
|
||||
/* Print Styles */
|
||||
@media print {
|
||||
.navbar,
|
||||
.modal,
|
||||
.btn,
|
||||
footer {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.container {
|
||||
width: 100% !important;
|
||||
max-width: none !important;
|
||||
}
|
||||
|
||||
.card {
|
||||
border: 1px solid #000 !important;
|
||||
box-shadow: none !important;
|
||||
}
|
||||
|
||||
pre,
|
||||
code {
|
||||
white-space: pre-wrap !important;
|
||||
word-break: break-all !important;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,567 @@
|
||||
// UnitForge Editor JavaScript
|
||||
// Handles the unit file editor functionality
|
||||
|
||||
class UnitFileEditor {
|
||||
constructor() {
|
||||
this.currentUnitType = 'service';
|
||||
this.currentContent = '';
|
||||
this.init();
|
||||
}
|
||||
|
||||
init() {
|
||||
this.setupEventListeners();
|
||||
this.initializeEditor();
|
||||
this.updateFilename();
|
||||
}
|
||||
|
||||
setupEventListeners() {
|
||||
// Unit type change
|
||||
const unitTypeSelect = document.getElementById('unitType');
|
||||
if (unitTypeSelect) {
|
||||
unitTypeSelect.addEventListener('change', this.changeUnitType.bind(this));
|
||||
}
|
||||
|
||||
// Unit name change
|
||||
const unitNameInput = document.getElementById('unitName');
|
||||
if (unitNameInput) {
|
||||
unitNameInput.addEventListener('input', this.updateFilename.bind(this));
|
||||
}
|
||||
|
||||
// Editor content change
|
||||
const editor = document.getElementById('editor');
|
||||
if (editor) {
|
||||
editor.addEventListener('input', this.debounce(this.onEditorChange.bind(this), 300));
|
||||
}
|
||||
|
||||
// File upload
|
||||
const fileInput = document.getElementById('fileInput');
|
||||
if (fileInput) {
|
||||
fileInput.addEventListener('change', this.handleFileUpload.bind(this));
|
||||
}
|
||||
}
|
||||
|
||||
initializeEditor() {
|
||||
this.generateBasicUnit();
|
||||
}
|
||||
|
||||
changeUnitType() {
|
||||
const unitType = document.getElementById('unitType').value;
|
||||
this.currentUnitType = unitType;
|
||||
|
||||
// Show/hide type-specific fields
|
||||
this.toggleTypeFields(unitType);
|
||||
|
||||
// Update filename
|
||||
this.updateFilename();
|
||||
|
||||
// Generate new basic unit
|
||||
this.generateBasicUnit();
|
||||
}
|
||||
|
||||
toggleTypeFields(unitType) {
|
||||
// Hide all type-specific fields
|
||||
const allFields = document.querySelectorAll('.unit-type-fields');
|
||||
allFields.forEach(field => field.classList.add('d-none'));
|
||||
|
||||
// Show relevant fields
|
||||
const targetFields = document.getElementById(`${unitType}Fields`);
|
||||
if (targetFields) {
|
||||
targetFields.classList.remove('d-none');
|
||||
}
|
||||
}
|
||||
|
||||
updateFilename() {
|
||||
const unitName = document.getElementById('unitName').value || 'myservice';
|
||||
const unitType = document.getElementById('unitType').value;
|
||||
const filename = `${unitName}.${unitType}`;
|
||||
|
||||
const filenameElement = document.getElementById('filename');
|
||||
if (filenameElement) {
|
||||
filenameElement.textContent = filename;
|
||||
}
|
||||
}
|
||||
|
||||
generateBasicUnit() {
|
||||
const unitType = this.currentUnitType;
|
||||
const unitName = document.getElementById('unitName').value || 'myservice';
|
||||
const description = document.getElementById('unitDescription').value || 'My Service Description';
|
||||
|
||||
let content = `[Unit]\nDescription=${description}\n`;
|
||||
|
||||
// Add common dependencies
|
||||
if (unitType === 'service' || unitType === 'timer') {
|
||||
content += 'After=network.target\n';
|
||||
}
|
||||
|
||||
content += '\n';
|
||||
|
||||
// Add type-specific sections
|
||||
switch (unitType) {
|
||||
case 'service':
|
||||
content += '[Service]\n';
|
||||
content += 'Type=simple\n';
|
||||
content += 'ExecStart=/usr/bin/myapp\n';
|
||||
content += 'User=www-data\n';
|
||||
content += 'Restart=on-failure\n';
|
||||
break;
|
||||
|
||||
case 'timer':
|
||||
content += '[Timer]\n';
|
||||
content += 'OnCalendar=daily\n';
|
||||
content += 'Persistent=true\n';
|
||||
break;
|
||||
|
||||
case 'socket':
|
||||
content += '[Socket]\n';
|
||||
content += 'ListenStream=127.0.0.1:8080\n';
|
||||
content += 'SocketUser=www-data\n';
|
||||
break;
|
||||
|
||||
case 'mount':
|
||||
content += '[Mount]\n';
|
||||
content += 'What=/dev/disk/by-uuid/12345678-1234-1234-1234-123456789abc\n';
|
||||
content += 'Where=/mnt/mydisk\n';
|
||||
content += 'Type=ext4\n';
|
||||
content += 'Options=defaults\n';
|
||||
break;
|
||||
|
||||
case 'target':
|
||||
content += '[Unit]\n';
|
||||
content += 'Requires=multi-user.target\n';
|
||||
break;
|
||||
|
||||
case 'path':
|
||||
content += '[Path]\n';
|
||||
content += 'PathExists=/var/spool/myapp\n';
|
||||
content += 'Unit=myapp.service\n';
|
||||
break;
|
||||
}
|
||||
|
||||
// Add Install section for most types
|
||||
if (unitType !== 'target') {
|
||||
content += '\n[Install]\n';
|
||||
if (unitType === 'timer') {
|
||||
content += 'WantedBy=timers.target\n';
|
||||
} else if (unitType === 'socket') {
|
||||
content += 'WantedBy=sockets.target\n';
|
||||
} else {
|
||||
content += 'WantedBy=multi-user.target\n';
|
||||
}
|
||||
}
|
||||
|
||||
this.setEditorContent(content);
|
||||
}
|
||||
|
||||
setEditorContent(content) {
|
||||
const editor = document.getElementById('editor');
|
||||
if (editor) {
|
||||
editor.value = content;
|
||||
this.currentContent = content;
|
||||
this.updateUnitInfo();
|
||||
}
|
||||
}
|
||||
|
||||
onEditorChange() {
|
||||
const editor = document.getElementById('editor');
|
||||
if (editor) {
|
||||
this.currentContent = editor.value;
|
||||
this.updateUnitInfo();
|
||||
}
|
||||
}
|
||||
|
||||
updateUnitInfo() {
|
||||
// Update basic info display
|
||||
const lines = this.currentContent.split('\n');
|
||||
const sections = this.countSections(lines);
|
||||
|
||||
const infoType = document.getElementById('infoType');
|
||||
const infoSections = document.getElementById('infoSections');
|
||||
|
||||
if (infoType) {
|
||||
infoType.textContent = this.currentUnitType;
|
||||
}
|
||||
|
||||
if (infoSections) {
|
||||
infoSections.textContent = sections;
|
||||
}
|
||||
}
|
||||
|
||||
countSections(lines) {
|
||||
let count = 0;
|
||||
for (const line of lines) {
|
||||
if (line.trim().match(/^\[.+\]$/)) {
|
||||
count++;
|
||||
}
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
updateField(section, key, value) {
|
||||
if (!value.trim()) return;
|
||||
|
||||
const lines = this.currentContent.split('\n');
|
||||
let newLines = [];
|
||||
let currentSection = '';
|
||||
let foundSection = false;
|
||||
let foundKey = false;
|
||||
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
const line = lines[i];
|
||||
const sectionMatch = line.match(/^\[(.+)\]$/);
|
||||
|
||||
if (sectionMatch) {
|
||||
currentSection = sectionMatch[1];
|
||||
foundSection = (currentSection === section);
|
||||
newLines.push(line);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (foundSection && line.includes('=')) {
|
||||
const [lineKey] = line.split('=', 2);
|
||||
if (lineKey.trim() === key) {
|
||||
newLines.push(`${key}=${value}`);
|
||||
foundKey = true;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
newLines.push(line);
|
||||
}
|
||||
|
||||
// If section or key wasn't found, add them
|
||||
if (!foundKey) {
|
||||
this.addKeyToSection(newLines, section, key, value);
|
||||
}
|
||||
|
||||
this.setEditorContent(newLines.join('\n'));
|
||||
}
|
||||
|
||||
addKeyToSection(lines, section, key, value) {
|
||||
let sectionIndex = -1;
|
||||
let nextSectionIndex = -1;
|
||||
|
||||
// Find the target section
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
const sectionMatch = lines[i].match(/^\[(.+)\]$/);
|
||||
if (sectionMatch) {
|
||||
if (sectionMatch[1] === section) {
|
||||
sectionIndex = i;
|
||||
} else if (sectionIndex !== -1 && nextSectionIndex === -1) {
|
||||
nextSectionIndex = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (sectionIndex === -1) {
|
||||
// Section doesn't exist, add it
|
||||
lines.push('');
|
||||
lines.push(`[${section}]`);
|
||||
lines.push(`${key}=${value}`);
|
||||
} else {
|
||||
// Section exists, add key
|
||||
const insertIndex = nextSectionIndex === -1 ? lines.length : nextSectionIndex;
|
||||
lines.splice(insertIndex, 0, `${key}=${value}`);
|
||||
}
|
||||
}
|
||||
|
||||
async validateUnit() {
|
||||
const content = this.currentContent;
|
||||
const filename = document.getElementById('filename').textContent;
|
||||
|
||||
const resultsDiv = document.getElementById('validationResults');
|
||||
if (!resultsDiv) return;
|
||||
|
||||
// Show loading
|
||||
resultsDiv.innerHTML = `
|
||||
<div class="alert alert-info">
|
||||
<i class="fas fa-spinner fa-spin me-2"></i>
|
||||
Validating unit file...
|
||||
</div>
|
||||
`;
|
||||
|
||||
try {
|
||||
const result = await unitforge.validateUnitFile(content, filename);
|
||||
resultsDiv.innerHTML = unitforge.formatValidationResults(result);
|
||||
|
||||
if (result.valid) {
|
||||
unitforge.showToast('Unit file is valid!', 'success');
|
||||
} else {
|
||||
unitforge.showToast(`Found ${result.errors.length} error(s)`, 'warning');
|
||||
}
|
||||
} catch (error) {
|
||||
resultsDiv.innerHTML = `
|
||||
<div class="alert alert-danger">
|
||||
<i class="fas fa-exclamation-circle me-2"></i>
|
||||
Validation failed: ${error.message}
|
||||
</div>
|
||||
`;
|
||||
unitforge.showToast('Validation failed', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
resetEditor() {
|
||||
// Reset form fields
|
||||
document.getElementById('unitName').value = '';
|
||||
document.getElementById('unitDescription').value = '';
|
||||
document.getElementById('unitType').value = 'service';
|
||||
|
||||
// Reset type-specific fields
|
||||
document.getElementById('execStart').value = '';
|
||||
document.getElementById('user').value = '';
|
||||
document.getElementById('workingDirectory').value = '';
|
||||
|
||||
// Clear validation results
|
||||
const resultsDiv = document.getElementById('validationResults');
|
||||
if (resultsDiv) {
|
||||
resultsDiv.innerHTML = `
|
||||
<div class="alert alert-info">
|
||||
<i class="fas fa-info-circle me-2"></i>
|
||||
Click "Validate" to check your unit file.
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
// Regenerate basic unit
|
||||
this.currentUnitType = 'service';
|
||||
this.toggleTypeFields('service');
|
||||
this.updateFilename();
|
||||
this.generateBasicUnit();
|
||||
|
||||
unitforge.showToast('Editor reset', 'info');
|
||||
}
|
||||
|
||||
formatUnit() {
|
||||
const lines = this.currentContent.split('\n');
|
||||
const formatted = [];
|
||||
let inSection = false;
|
||||
|
||||
for (const line of lines) {
|
||||
const trimmed = line.trim();
|
||||
|
||||
if (trimmed.match(/^\[.+\]$/)) {
|
||||
// Section header
|
||||
if (inSection) {
|
||||
formatted.push(''); // Add blank line before new section
|
||||
}
|
||||
formatted.push(trimmed);
|
||||
inSection = true;
|
||||
} else if (trimmed === '') {
|
||||
// Empty line - only add if not consecutive
|
||||
if (formatted.length > 0 && formatted[formatted.length - 1] !== '') {
|
||||
formatted.push('');
|
||||
}
|
||||
} else if (trimmed.includes('=')) {
|
||||
// Key-value pair
|
||||
const [key, ...valueParts] = trimmed.split('=');
|
||||
const value = valueParts.join('=');
|
||||
formatted.push(`${key.trim()}=${value.trim()}`);
|
||||
} else {
|
||||
// Other content
|
||||
formatted.push(trimmed);
|
||||
}
|
||||
}
|
||||
|
||||
// Remove trailing empty lines
|
||||
while (formatted.length > 0 && formatted[formatted.length - 1] === '') {
|
||||
formatted.pop();
|
||||
}
|
||||
|
||||
this.setEditorContent(formatted.join('\n'));
|
||||
unitforge.showToast('Unit file formatted', 'success');
|
||||
}
|
||||
|
||||
async copyToClipboard() {
|
||||
await unitforge.copyToClipboard(this.currentContent);
|
||||
}
|
||||
|
||||
downloadFile() {
|
||||
const filename = document.getElementById('filename').textContent;
|
||||
unitforge.downloadTextFile(this.currentContent, filename);
|
||||
unitforge.showToast(`Downloaded ${filename}`, 'success');
|
||||
}
|
||||
|
||||
insertPattern(patternType) {
|
||||
let pattern = '';
|
||||
|
||||
switch (patternType) {
|
||||
case 'web-service':
|
||||
pattern = `[Unit]
|
||||
Description=Web Application Service
|
||||
After=network.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
ExecStart=/usr/bin/node /opt/webapp/server.js
|
||||
User=webapp
|
||||
Group=webapp
|
||||
WorkingDirectory=/opt/webapp
|
||||
Environment=NODE_ENV=production
|
||||
Environment=PORT=3000
|
||||
Restart=on-failure
|
||||
RestartSec=5
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target`;
|
||||
break;
|
||||
|
||||
case 'background-job':
|
||||
pattern = `[Unit]
|
||||
Description=Background Job Service
|
||||
After=network.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
ExecStart=/usr/bin/python3 /opt/app/worker.py
|
||||
User=worker
|
||||
Group=worker
|
||||
WorkingDirectory=/opt/app
|
||||
Restart=on-failure
|
||||
RestartSec=10
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target`;
|
||||
break;
|
||||
|
||||
case 'database':
|
||||
pattern = `[Unit]
|
||||
Description=Database Service
|
||||
After=network.target
|
||||
|
||||
[Service]
|
||||
Type=notify
|
||||
ExecStart=/usr/bin/mysqld --defaults-file=/etc/mysql/my.cnf
|
||||
User=mysql
|
||||
Group=mysql
|
||||
Restart=on-failure
|
||||
TimeoutSec=300
|
||||
PrivateTmp=true
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target`;
|
||||
break;
|
||||
}
|
||||
|
||||
if (pattern) {
|
||||
this.setEditorContent(pattern);
|
||||
unitforge.showToast(`Inserted ${patternType} pattern`, 'success');
|
||||
}
|
||||
}
|
||||
|
||||
async handleFileUpload(event) {
|
||||
const file = event.target.files[0];
|
||||
if (!file) return;
|
||||
|
||||
try {
|
||||
const result = await unitforge.uploadUnitFile(file);
|
||||
|
||||
// Load content into editor
|
||||
this.setEditorContent(result.content);
|
||||
|
||||
// Update filename if available
|
||||
if (result.filename) {
|
||||
const nameWithoutExt = result.filename.replace(/\.[^/.]+$/, "");
|
||||
document.getElementById('unitName').value = nameWithoutExt;
|
||||
this.updateFilename();
|
||||
}
|
||||
|
||||
// Update unit type if detected
|
||||
if (result.unit_type) {
|
||||
document.getElementById('unitType').value = result.unit_type;
|
||||
this.currentUnitType = result.unit_type;
|
||||
this.toggleTypeFields(result.unit_type);
|
||||
}
|
||||
|
||||
// Show validation results
|
||||
const resultsDiv = document.getElementById('validationResults');
|
||||
if (resultsDiv && result.validation) {
|
||||
resultsDiv.innerHTML = unitforge.formatValidationResults(result.validation);
|
||||
}
|
||||
|
||||
// Close upload modal
|
||||
const modal = bootstrap.Modal.getInstance(document.getElementById('uploadModal'));
|
||||
if (modal) {
|
||||
modal.hide();
|
||||
}
|
||||
|
||||
unitforge.showToast(`Loaded ${file.name}`, 'success');
|
||||
|
||||
} catch (error) {
|
||||
unitforge.showToast(`Failed to load file: ${error.message}`, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
debounce(func, wait) {
|
||||
let timeout;
|
||||
return function executedFunction(...args) {
|
||||
const later = () => {
|
||||
clearTimeout(timeout);
|
||||
func(...args);
|
||||
};
|
||||
clearTimeout(timeout);
|
||||
timeout = setTimeout(later, wait);
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Global functions for HTML onclick handlers
|
||||
function changeUnitType() {
|
||||
editor.changeUnitType();
|
||||
}
|
||||
|
||||
function updateFilename() {
|
||||
editor.updateFilename();
|
||||
}
|
||||
|
||||
function updateField(section, key, value) {
|
||||
editor.updateField(section, key, value);
|
||||
}
|
||||
|
||||
function validateUnit() {
|
||||
editor.validateUnit();
|
||||
}
|
||||
|
||||
function resetEditor() {
|
||||
editor.resetEditor();
|
||||
}
|
||||
|
||||
function formatUnit() {
|
||||
editor.formatUnit();
|
||||
}
|
||||
|
||||
function copyToClipboard() {
|
||||
editor.copyToClipboard();
|
||||
}
|
||||
|
||||
function downloadFile() {
|
||||
editor.downloadFile();
|
||||
}
|
||||
|
||||
function insertPattern(patternType) {
|
||||
editor.insertPattern(patternType);
|
||||
}
|
||||
|
||||
function loadFile() {
|
||||
const fileInput = document.getElementById('fileInput');
|
||||
const file = fileInput.files[0];
|
||||
|
||||
if (!file) {
|
||||
unitforge.showToast('Please select a file first', 'warning');
|
||||
return;
|
||||
}
|
||||
|
||||
// Trigger the file upload handling
|
||||
editor.handleFileUpload({ target: { files: [file] } });
|
||||
}
|
||||
|
||||
function showUploadModal() {
|
||||
const modal = new bootstrap.Modal(document.getElementById('uploadModal'));
|
||||
modal.show();
|
||||
}
|
||||
|
||||
// Initialize the editor
|
||||
const editor = new UnitFileEditor();
|
||||
|
||||
// Export for use in other modules
|
||||
window.UnitFileEditor = UnitFileEditor;
|
||||
window.editor = editor;
|
||||
@@ -0,0 +1,382 @@
|
||||
// UnitForge Main JavaScript
|
||||
// Handles general functionality across the application
|
||||
|
||||
class UnitForge {
|
||||
constructor() {
|
||||
this.baseUrl = window.location.origin;
|
||||
this.apiUrl = `${this.baseUrl}/api`;
|
||||
this.init();
|
||||
}
|
||||
|
||||
init() {
|
||||
this.setupEventListeners();
|
||||
this.initializeTooltips();
|
||||
}
|
||||
|
||||
setupEventListeners() {
|
||||
// Upload modal functionality
|
||||
const fileInput = document.getElementById('fileInput');
|
||||
if (fileInput) {
|
||||
fileInput.addEventListener('change', this.handleFileSelect.bind(this));
|
||||
}
|
||||
|
||||
// Copy to clipboard functionality
|
||||
document.addEventListener('click', (e) => {
|
||||
if (e.target.matches('[data-copy]') || e.target.closest('[data-copy]')) {
|
||||
const target = e.target.matches('[data-copy]') ? e.target : e.target.closest('[data-copy]');
|
||||
this.copyToClipboard(target.dataset.copy);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
initializeTooltips() {
|
||||
// Initialize Bootstrap tooltips
|
||||
const tooltipTriggerList = [].slice.call(document.querySelectorAll('[data-bs-toggle="tooltip"]'));
|
||||
tooltipTriggerList.map(function (tooltipTriggerEl) {
|
||||
return new bootstrap.Tooltip(tooltipTriggerEl);
|
||||
});
|
||||
}
|
||||
|
||||
// File upload handling
|
||||
handleFileSelect(event) {
|
||||
const file = event.target.files[0];
|
||||
if (!file) return;
|
||||
|
||||
const reader = new FileReader();
|
||||
reader.onload = (e) => {
|
||||
this.displayFileContent(e.target.result, file.name);
|
||||
};
|
||||
reader.readAsText(file);
|
||||
}
|
||||
|
||||
displayFileContent(content, filename) {
|
||||
// This will be overridden in specific pages
|
||||
console.log('File loaded:', filename, content);
|
||||
}
|
||||
|
||||
// API calls
|
||||
async apiCall(endpoint, options = {}) {
|
||||
const url = `${this.apiUrl}${endpoint}`;
|
||||
const defaultOptions = {
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
};
|
||||
|
||||
const finalOptions = { ...defaultOptions, ...options };
|
||||
|
||||
try {
|
||||
const response = await fetch(url, finalOptions);
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json().catch(() => ({}));
|
||||
throw new Error(errorData.detail || `HTTP ${response.status}: ${response.statusText}`);
|
||||
}
|
||||
|
||||
return await response.json();
|
||||
} catch (error) {
|
||||
console.error('API call failed:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async validateUnitFile(content, filename = null) {
|
||||
return await this.apiCall('/validate', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
content: content,
|
||||
filename: filename
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
async uploadUnitFile(file) {
|
||||
const formData = new FormData();
|
||||
formData.append('file', file);
|
||||
|
||||
const response = await fetch(`${this.apiUrl}/upload`, {
|
||||
method: 'POST',
|
||||
body: formData
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json().catch(() => ({}));
|
||||
throw new Error(errorData.detail || `Upload failed: ${response.statusText}`);
|
||||
}
|
||||
|
||||
return await response.json();
|
||||
}
|
||||
|
||||
async downloadUnitFile(content, filename) {
|
||||
return await this.apiCall('/download', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
content: content,
|
||||
filename: filename
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
async getTemplates() {
|
||||
return await this.apiCall('/templates');
|
||||
}
|
||||
|
||||
async getTemplate(name) {
|
||||
return await this.apiCall(`/templates/${name}`);
|
||||
}
|
||||
|
||||
async generateFromTemplate(templateName, parameters, filename = null) {
|
||||
return await this.apiCall('/generate', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
template_name: templateName,
|
||||
parameters: parameters,
|
||||
filename: filename
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
// Utility functions
|
||||
async copyToClipboard(text) {
|
||||
try {
|
||||
await navigator.clipboard.writeText(text);
|
||||
this.showToast('Copied to clipboard!', 'success');
|
||||
} catch (err) {
|
||||
// Fallback for older browsers
|
||||
const textArea = document.createElement('textarea');
|
||||
textArea.value = text;
|
||||
textArea.style.position = 'fixed';
|
||||
textArea.style.left = '-999999px';
|
||||
textArea.style.top = '-999999px';
|
||||
document.body.appendChild(textArea);
|
||||
textArea.focus();
|
||||
textArea.select();
|
||||
|
||||
try {
|
||||
document.execCommand('copy');
|
||||
this.showToast('Copied to clipboard!', 'success');
|
||||
} catch (err) {
|
||||
this.showToast('Failed to copy to clipboard', 'error');
|
||||
}
|
||||
|
||||
document.body.removeChild(textArea);
|
||||
}
|
||||
}
|
||||
|
||||
showToast(message, type = 'info', duration = 3000) {
|
||||
// Create toast container if it doesn't exist
|
||||
let toastContainer = document.getElementById('toast-container');
|
||||
if (!toastContainer) {
|
||||
toastContainer = document.createElement('div');
|
||||
toastContainer.id = 'toast-container';
|
||||
toastContainer.className = 'position-fixed top-0 end-0 p-3';
|
||||
toastContainer.style.zIndex = '9999';
|
||||
document.body.appendChild(toastContainer);
|
||||
}
|
||||
|
||||
// Create toast element
|
||||
const toastId = `toast-${Date.now()}`;
|
||||
const toastHtml = `
|
||||
<div id="${toastId}" class="toast" role="alert" aria-live="assertive" aria-atomic="true">
|
||||
<div class="toast-header">
|
||||
<i class="fas fa-${this.getToastIcon(type)} text-${this.getToastColor(type)} me-2"></i>
|
||||
<strong class="me-auto">UnitForge</strong>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="toast" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="toast-body">
|
||||
${message}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
toastContainer.insertAdjacentHTML('beforeend', toastHtml);
|
||||
|
||||
// Initialize and show toast
|
||||
const toastElement = document.getElementById(toastId);
|
||||
const toast = new bootstrap.Toast(toastElement, {
|
||||
autohide: true,
|
||||
delay: duration
|
||||
});
|
||||
|
||||
toast.show();
|
||||
|
||||
// Remove toast element after it's hidden
|
||||
toastElement.addEventListener('hidden.bs.toast', () => {
|
||||
toastElement.remove();
|
||||
});
|
||||
}
|
||||
|
||||
getToastIcon(type) {
|
||||
const icons = {
|
||||
success: 'check-circle',
|
||||
error: 'exclamation-circle',
|
||||
warning: 'exclamation-triangle',
|
||||
info: 'info-circle'
|
||||
};
|
||||
return icons[type] || 'info-circle';
|
||||
}
|
||||
|
||||
getToastColor(type) {
|
||||
const colors = {
|
||||
success: 'success',
|
||||
error: 'danger',
|
||||
warning: 'warning',
|
||||
info: 'info'
|
||||
};
|
||||
return colors[type] || 'info';
|
||||
}
|
||||
|
||||
formatValidationResults(validation) {
|
||||
if (!validation) return '';
|
||||
|
||||
let html = '';
|
||||
|
||||
if (validation.valid) {
|
||||
html += `
|
||||
<div class="validation-success">
|
||||
<i class="fas fa-check-circle me-2"></i>
|
||||
Unit file is valid!
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
if (validation.errors && validation.errors.length > 0) {
|
||||
html += '<div class="mb-3">';
|
||||
html += `<h6 class="text-danger"><i class="fas fa-exclamation-circle me-2"></i>Errors (${validation.errors.length})</h6>`;
|
||||
|
||||
validation.errors.forEach(error => {
|
||||
const location = error.section + (error.key ? `.${error.key}` : '');
|
||||
html += `
|
||||
<div class="validation-error">
|
||||
<strong>[${location}]</strong> ${error.message}
|
||||
</div>
|
||||
`;
|
||||
});
|
||||
html += '</div>';
|
||||
}
|
||||
|
||||
if (validation.warnings && validation.warnings.length > 0) {
|
||||
html += '<div class="mb-3">';
|
||||
html += `<h6 class="text-warning"><i class="fas fa-exclamation-triangle me-2"></i>Warnings (${validation.warnings.length})</h6>`;
|
||||
|
||||
validation.warnings.forEach(warning => {
|
||||
const location = warning.section + (warning.key ? `.${warning.key}` : '');
|
||||
html += `
|
||||
<div class="validation-warning">
|
||||
<strong>[${location}]</strong> ${warning.message}
|
||||
</div>
|
||||
`;
|
||||
});
|
||||
html += '</div>';
|
||||
}
|
||||
|
||||
return html;
|
||||
}
|
||||
|
||||
downloadTextFile(content, filename) {
|
||||
const blob = new Blob([content], { type: 'text/plain' });
|
||||
const url = window.URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = filename;
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
document.body.removeChild(a);
|
||||
window.URL.revokeObjectURL(url);
|
||||
}
|
||||
|
||||
escapeHtml(text) {
|
||||
const div = document.createElement('div');
|
||||
div.textContent = text;
|
||||
return div.innerHTML;
|
||||
}
|
||||
|
||||
debounce(func, wait) {
|
||||
let timeout;
|
||||
return function executedFunction(...args) {
|
||||
const later = () => {
|
||||
clearTimeout(timeout);
|
||||
func(...args);
|
||||
};
|
||||
clearTimeout(timeout);
|
||||
timeout = setTimeout(later, wait);
|
||||
};
|
||||
}
|
||||
|
||||
showLoading(element, message = 'Loading...') {
|
||||
if (typeof element === 'string') {
|
||||
element = document.getElementById(element);
|
||||
}
|
||||
|
||||
if (element) {
|
||||
element.innerHTML = `
|
||||
<div class="text-center py-5">
|
||||
<div class="spinner-border text-primary" role="status">
|
||||
<span class="visually-hidden">${message}</span>
|
||||
</div>
|
||||
<p class="mt-3 text-muted">${message}</p>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
hideLoading(element) {
|
||||
if (typeof element === 'string') {
|
||||
element = document.getElementById(element);
|
||||
}
|
||||
|
||||
if (element) {
|
||||
element.innerHTML = '';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Global functions for HTML onclick handlers
|
||||
function showUploadModal() {
|
||||
const modal = new bootstrap.Modal(document.getElementById('uploadModal'));
|
||||
modal.show();
|
||||
}
|
||||
|
||||
function showCliModal() {
|
||||
const modal = new bootstrap.Modal(document.getElementById('cliModal'));
|
||||
modal.show();
|
||||
}
|
||||
|
||||
async function validateFile() {
|
||||
const fileInput = document.getElementById('fileInput');
|
||||
const file = fileInput.files[0];
|
||||
|
||||
if (!file) {
|
||||
unitforge.showToast('Please select a file first', 'warning');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await unitforge.uploadUnitFile(file);
|
||||
|
||||
// Display results
|
||||
const resultsDiv = document.getElementById('uploadResults');
|
||||
const outputDiv = document.getElementById('validationOutput');
|
||||
|
||||
if (resultsDiv && outputDiv) {
|
||||
outputDiv.innerHTML = unitforge.formatValidationResults(result.validation);
|
||||
resultsDiv.classList.remove('d-none');
|
||||
}
|
||||
|
||||
if (result.validation.valid) {
|
||||
unitforge.showToast('File validation completed successfully!', 'success');
|
||||
} else {
|
||||
unitforge.showToast(`Validation found ${result.validation.errors.length} error(s)`, 'warning');
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
unitforge.showToast(`Validation failed: ${error.message}`, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize the application
|
||||
const unitforge = new UnitForge();
|
||||
|
||||
// Export for use in other modules
|
||||
window.UnitForge = UnitForge;
|
||||
window.unitforge = unitforge;
|
||||
@@ -0,0 +1,670 @@
|
||||
// UnitForge Templates JavaScript
|
||||
// Handles the templates browser functionality
|
||||
|
||||
class TemplatesBrowser {
|
||||
constructor() {
|
||||
this.templates = [];
|
||||
this.filteredTemplates = [];
|
||||
this.currentCategory = "all";
|
||||
this.currentTemplate = null;
|
||||
this.init();
|
||||
}
|
||||
|
||||
init() {
|
||||
this.setupEventListeners();
|
||||
this.loadTemplates();
|
||||
}
|
||||
|
||||
setupEventListeners() {
|
||||
// Search input
|
||||
const searchInput = document.getElementById("searchInput");
|
||||
if (searchInput) {
|
||||
searchInput.addEventListener(
|
||||
"keyup",
|
||||
this.debounce(this.filterTemplates.bind(this), 300),
|
||||
);
|
||||
}
|
||||
|
||||
// Template form changes
|
||||
document.addEventListener("change", (e) => {
|
||||
if (
|
||||
e.target.matches(
|
||||
"#templateForm input, #templateForm select, #templateForm textarea",
|
||||
)
|
||||
) {
|
||||
this.updatePreview();
|
||||
}
|
||||
});
|
||||
|
||||
document.addEventListener("input", (e) => {
|
||||
if (e.target.matches("#templateForm input, #templateForm textarea")) {
|
||||
this.updatePreview();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async loadTemplates() {
|
||||
const loadingState = document.getElementById("loadingState");
|
||||
const templatesGrid = document.getElementById("templatesGrid");
|
||||
|
||||
try {
|
||||
// Temporarily use static JSON file for testing
|
||||
const response = await fetch("/static/templates.json");
|
||||
this.templates = await response.json();
|
||||
this.filteredTemplates = [...this.templates];
|
||||
|
||||
if (loadingState) loadingState.classList.add("d-none");
|
||||
if (templatesGrid) templatesGrid.classList.remove("d-none");
|
||||
|
||||
this.renderTemplates();
|
||||
this.updateCategoryCounts();
|
||||
} catch (error) {
|
||||
if (loadingState) {
|
||||
loadingState.innerHTML = `
|
||||
<div class="text-center py-5">
|
||||
<i class="fas fa-exclamation-triangle fa-3x text-danger mb-3"></i>
|
||||
<h4>Failed to load templates</h4>
|
||||
<p class="text-muted">${error.message}</p>
|
||||
<button class="btn btn-primary" onclick="location.reload()">Retry</button>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
unitforge.showToast("Failed to load templates", "error");
|
||||
}
|
||||
}
|
||||
|
||||
renderTemplates() {
|
||||
const grid = document.getElementById("templatesGrid");
|
||||
const noResults = document.getElementById("noResults");
|
||||
|
||||
if (!grid) return;
|
||||
|
||||
if (this.filteredTemplates.length === 0) {
|
||||
grid.classList.add("d-none");
|
||||
if (noResults) noResults.classList.remove("d-none");
|
||||
return;
|
||||
}
|
||||
|
||||
if (noResults) noResults.classList.add("d-none");
|
||||
grid.classList.remove("d-none");
|
||||
|
||||
grid.innerHTML = this.filteredTemplates
|
||||
.map((template) => this.createTemplateCard(template))
|
||||
.join("");
|
||||
}
|
||||
|
||||
createTemplateCard(template) {
|
||||
const tags = template.tags
|
||||
.map((tag) => `<span class="template-tag">${this.escapeHtml(tag)}</span>`)
|
||||
.join("");
|
||||
|
||||
const requiredParams = template.parameters.filter((p) => p.required).length;
|
||||
const totalParams = template.parameters.length;
|
||||
|
||||
return `
|
||||
<div class="col-md-6 col-lg-4">
|
||||
<div class="card template-card border-0 shadow-sm" onclick="openTemplate('${template.name}')">
|
||||
<div class="card-header">
|
||||
<div class="d-flex justify-content-between align-items-start">
|
||||
<div>
|
||||
<h6 class="card-title mb-1">${this.escapeHtml(template.name)}</h6>
|
||||
<small class="opacity-75">
|
||||
<i class="fas fa-${this.getUnitTypeIcon(template.unit_type)} me-1"></i>
|
||||
${template.unit_type}
|
||||
</small>
|
||||
</div>
|
||||
<span class="badge bg-light text-dark">${template.category}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<p class="card-text text-muted mb-3">${this.escapeHtml(template.description)}</p>
|
||||
|
||||
<div class="d-flex justify-content-between align-items-center mb-3">
|
||||
<small class="text-muted">
|
||||
<i class="fas fa-sliders-h me-1"></i>
|
||||
${requiredParams}/${totalParams} parameters
|
||||
</small>
|
||||
</div>
|
||||
|
||||
${tags ? `<div class="template-tags">${tags}</div>` : ""}
|
||||
</div>
|
||||
<div class="card-footer bg-transparent border-0">
|
||||
<div class="d-grid">
|
||||
<button class="btn btn-primary btn-sm" onclick="event.stopPropagation(); openTemplate('${template.name}')">
|
||||
<i class="fas fa-play me-2"></i>Use Template
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
getUnitTypeIcon(unitType) {
|
||||
const icons = {
|
||||
service: "play-circle",
|
||||
timer: "clock",
|
||||
socket: "plug",
|
||||
mount: "hdd",
|
||||
target: "bullseye",
|
||||
path: "folder",
|
||||
};
|
||||
return icons[unitType] || "file";
|
||||
}
|
||||
|
||||
updateCategoryCounts() {
|
||||
const categories = {
|
||||
all: this.templates.length,
|
||||
"Web Services": 0,
|
||||
"Database Services": 0,
|
||||
"System Maintenance": 0,
|
||||
"Container Services": 0,
|
||||
"Network Services": 0,
|
||||
};
|
||||
|
||||
this.templates.forEach((template) => {
|
||||
if (categories.hasOwnProperty(template.category)) {
|
||||
categories[template.category]++;
|
||||
}
|
||||
});
|
||||
|
||||
// Update count badges
|
||||
const categoryMappings = {
|
||||
all: "count-all",
|
||||
"Web Services": "count-web",
|
||||
"Database Services": "count-database",
|
||||
"System Maintenance": "count-maintenance",
|
||||
"Container Services": "count-container",
|
||||
"Network Services": "count-network",
|
||||
};
|
||||
|
||||
Object.keys(categories).forEach((category) => {
|
||||
const elementId = categoryMappings[category];
|
||||
if (elementId) {
|
||||
const countElement = document.getElementById(elementId);
|
||||
if (countElement) {
|
||||
countElement.textContent = categories[category];
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
filterTemplates() {
|
||||
const searchTerm = document
|
||||
.getElementById("searchInput")
|
||||
.value.toLowerCase();
|
||||
|
||||
this.filteredTemplates = this.templates.filter((template) => {
|
||||
const matchesSearch =
|
||||
!searchTerm ||
|
||||
template.name.toLowerCase().includes(searchTerm) ||
|
||||
template.description.toLowerCase().includes(searchTerm) ||
|
||||
template.tags.some((tag) => tag.toLowerCase().includes(searchTerm));
|
||||
|
||||
const matchesCategory =
|
||||
this.currentCategory === "all" ||
|
||||
template.category === this.currentCategory;
|
||||
|
||||
return matchesSearch && matchesCategory;
|
||||
});
|
||||
|
||||
this.renderTemplates();
|
||||
}
|
||||
|
||||
filterByCategory(category) {
|
||||
this.currentCategory = category;
|
||||
|
||||
// Update active tab
|
||||
document.querySelectorAll("#categoryTabs .nav-link").forEach((tab) => {
|
||||
tab.classList.remove("active");
|
||||
});
|
||||
|
||||
const tabMappings = {
|
||||
all: "tab-all",
|
||||
"Web Services": "tab-web",
|
||||
"Database Services": "tab-database",
|
||||
"System Maintenance": "tab-maintenance",
|
||||
"Container Services": "tab-container",
|
||||
"Network Services": "tab-network",
|
||||
};
|
||||
|
||||
const activeTab = document.getElementById(tabMappings[category]);
|
||||
if (activeTab) {
|
||||
activeTab.classList.add("active");
|
||||
}
|
||||
|
||||
this.filterTemplates();
|
||||
}
|
||||
|
||||
clearSearch() {
|
||||
const searchInput = document.getElementById("searchInput");
|
||||
if (searchInput) {
|
||||
searchInput.value = "";
|
||||
}
|
||||
this.currentCategory = "all";
|
||||
|
||||
// Reset active tab
|
||||
document.querySelectorAll("#categoryTabs .nav-link").forEach((tab) => {
|
||||
tab.classList.remove("active");
|
||||
});
|
||||
document.getElementById("tab-all").classList.add("active");
|
||||
|
||||
this.filterTemplates();
|
||||
}
|
||||
|
||||
async openTemplate(templateName) {
|
||||
try {
|
||||
this.currentTemplate = await unitforge.getTemplate(templateName);
|
||||
this.showTemplateModal();
|
||||
} catch (error) {
|
||||
unitforge.showToast(`Failed to load template: ${error.message}`, "error");
|
||||
}
|
||||
}
|
||||
|
||||
showTemplateModal() {
|
||||
if (!this.currentTemplate) return;
|
||||
|
||||
const modal = document.getElementById("templateModal");
|
||||
const title = document.getElementById("templateModalTitle");
|
||||
const info = document.getElementById("templateInfo");
|
||||
const parametersDiv = document.getElementById("templateParameters");
|
||||
const previewFilename = document.getElementById("previewFilename");
|
||||
|
||||
// Update modal title
|
||||
if (title) {
|
||||
title.innerHTML = `
|
||||
<i class="fas fa-file-code me-2"></i>
|
||||
${this.escapeHtml(this.currentTemplate.name)}
|
||||
`;
|
||||
}
|
||||
|
||||
// Update template info
|
||||
if (info) {
|
||||
info.innerHTML = `
|
||||
<div class="row">
|
||||
<div class="col-sm-3"><strong>Name:</strong></div>
|
||||
<div class="col-sm-9">${this.escapeHtml(this.currentTemplate.name)}</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-sm-3"><strong>Type:</strong></div>
|
||||
<div class="col-sm-9">
|
||||
<i class="fas fa-${this.getUnitTypeIcon(this.currentTemplate.unit_type)} me-1"></i>
|
||||
${this.currentTemplate.unit_type}
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-sm-3"><strong>Category:</strong></div>
|
||||
<div class="col-sm-9">${this.escapeHtml(this.currentTemplate.category)}</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-sm-3"><strong>Description:</strong></div>
|
||||
<div class="col-sm-9">${this.escapeHtml(this.currentTemplate.description)}</div>
|
||||
</div>
|
||||
${
|
||||
this.currentTemplate.tags.length > 0
|
||||
? `
|
||||
<div class="row">
|
||||
<div class="col-sm-3"><strong>Tags:</strong></div>
|
||||
<div class="col-sm-9">
|
||||
${this.currentTemplate.tags
|
||||
.map(
|
||||
(tag) =>
|
||||
`<span class="badge bg-secondary me-1">${this.escapeHtml(tag)}</span>`,
|
||||
)
|
||||
.join("")}
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
: ""
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
// Update filename
|
||||
if (previewFilename) {
|
||||
const defaultName = this.getDefaultName();
|
||||
previewFilename.textContent = `${defaultName}.${this.currentTemplate.unit_type}`;
|
||||
}
|
||||
|
||||
// Generate parameters form
|
||||
if (parametersDiv) {
|
||||
parametersDiv.innerHTML = this.generateParametersForm();
|
||||
}
|
||||
|
||||
// Show modal
|
||||
const bsModal = new bootstrap.Modal(modal);
|
||||
bsModal.show();
|
||||
|
||||
// Update preview
|
||||
this.updatePreview();
|
||||
}
|
||||
|
||||
generateParametersForm() {
|
||||
if (!this.currentTemplate || !this.currentTemplate.parameters) {
|
||||
return '<p class="text-muted">No parameters required for this template.</p>';
|
||||
}
|
||||
|
||||
return this.currentTemplate.parameters
|
||||
.map((param) => {
|
||||
const isRequired = param.required;
|
||||
const fieldId = `param-${param.name}`;
|
||||
|
||||
let inputHtml = "";
|
||||
|
||||
switch (param.type) {
|
||||
case "boolean":
|
||||
inputHtml = `
|
||||
<select class="form-select" id="${fieldId}" ${isRequired ? "required" : ""}>
|
||||
<option value="true" ${param.default === true ? "selected" : ""}>True</option>
|
||||
<option value="false" ${param.default === false ? "selected" : ""}>False</option>
|
||||
</select>
|
||||
`;
|
||||
break;
|
||||
|
||||
case "choice":
|
||||
const options = param.choices
|
||||
.map(
|
||||
(choice) =>
|
||||
`<option value="${choice}" ${param.default === choice ? "selected" : ""}>${choice}</option>`,
|
||||
)
|
||||
.join("");
|
||||
inputHtml = `
|
||||
<select class="form-select" id="${fieldId}" ${isRequired ? "required" : ""}>
|
||||
${!isRequired ? '<option value="">-- Select --</option>' : ""}
|
||||
${options}
|
||||
</select>
|
||||
`;
|
||||
break;
|
||||
|
||||
case "integer":
|
||||
inputHtml = `
|
||||
<input type="number" class="form-control" id="${fieldId}"
|
||||
value="${param.default || ""}"
|
||||
placeholder="${param.example || ""}"
|
||||
${isRequired ? "required" : ""}>
|
||||
`;
|
||||
break;
|
||||
|
||||
default: // string, list
|
||||
inputHtml = `
|
||||
<input type="text" class="form-control" id="${fieldId}"
|
||||
value="${param.default || ""}"
|
||||
placeholder="${param.example || ""}"
|
||||
${isRequired ? "required" : ""}>
|
||||
`;
|
||||
}
|
||||
|
||||
return `
|
||||
<div class="parameter-group">
|
||||
<label for="${fieldId}" class="parameter-label">
|
||||
${this.escapeHtml(param.name)}
|
||||
${isRequired ? '<span class="parameter-required">*</span>' : '<span class="parameter-optional">(optional)</span>'}
|
||||
</label>
|
||||
<div class="parameter-description">${this.escapeHtml(param.description)}</div>
|
||||
${inputHtml}
|
||||
${param.example ? `<div class="form-text">Example: ${this.escapeHtml(param.example)}</div>` : ""}
|
||||
</div>
|
||||
`;
|
||||
})
|
||||
.join("");
|
||||
}
|
||||
|
||||
getDefaultName() {
|
||||
const nameParam = this.currentTemplate.parameters.find(
|
||||
(p) => p.name === "name",
|
||||
);
|
||||
if (nameParam && nameParam.example) {
|
||||
return nameParam.example;
|
||||
}
|
||||
return this.currentTemplate.name.toLowerCase().replace(/[^a-z0-9]/g, "");
|
||||
}
|
||||
|
||||
getFormParameters() {
|
||||
const parameters = {};
|
||||
|
||||
if (!this.currentTemplate || !this.currentTemplate.parameters) {
|
||||
return parameters;
|
||||
}
|
||||
|
||||
this.currentTemplate.parameters.forEach((param) => {
|
||||
const element = document.getElementById(`param-${param.name}`);
|
||||
if (element && element.value.trim()) {
|
||||
let value = element.value.trim();
|
||||
|
||||
// Type conversion
|
||||
switch (param.type) {
|
||||
case "boolean":
|
||||
value = value === "true";
|
||||
break;
|
||||
case "integer":
|
||||
value = parseInt(value);
|
||||
if (isNaN(value)) value = param.default || 0;
|
||||
break;
|
||||
case "list":
|
||||
value = value
|
||||
.split(",")
|
||||
.map((v) => v.trim())
|
||||
.filter((v) => v);
|
||||
break;
|
||||
}
|
||||
|
||||
parameters[param.name] = value;
|
||||
}
|
||||
});
|
||||
|
||||
return parameters;
|
||||
}
|
||||
|
||||
async updatePreview() {
|
||||
if (!this.currentTemplate) return;
|
||||
|
||||
const preview = document.getElementById("templatePreview");
|
||||
const validation = document.getElementById("validationResults");
|
||||
|
||||
if (!preview) return;
|
||||
|
||||
const parameters = this.getFormParameters();
|
||||
|
||||
// Check if required parameters are missing
|
||||
const missingRequired = this.currentTemplate.parameters
|
||||
.filter((p) => p.required && !parameters.hasOwnProperty(p.name))
|
||||
.map((p) => p.name);
|
||||
|
||||
if (missingRequired.length > 0) {
|
||||
preview.innerHTML = `<code># Please fill in required parameters: ${missingRequired.join(", ")}</code>`;
|
||||
if (validation) validation.classList.add("d-none");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await unitforge.generateFromTemplate(
|
||||
this.currentTemplate.name,
|
||||
parameters,
|
||||
`${parameters.name || "generated"}.${this.currentTemplate.unit_type}`,
|
||||
);
|
||||
|
||||
preview.innerHTML = `<code>${this.escapeHtml(result.content)}</code>`;
|
||||
|
||||
// Update filename
|
||||
const filenameElement = document.getElementById("previewFilename");
|
||||
if (filenameElement) {
|
||||
filenameElement.textContent = result.filename;
|
||||
}
|
||||
|
||||
// Show validation results
|
||||
if (validation && result.validation) {
|
||||
validation.innerHTML = unitforge.formatValidationResults(
|
||||
result.validation,
|
||||
);
|
||||
validation.classList.remove("d-none");
|
||||
}
|
||||
} catch (error) {
|
||||
preview.innerHTML = `<code># Error generating preview: ${error.message}</code>`;
|
||||
if (validation) validation.classList.add("d-none");
|
||||
}
|
||||
}
|
||||
|
||||
async validateTemplate() {
|
||||
const parameters = this.getFormParameters();
|
||||
const validation = document.getElementById("validationResults");
|
||||
|
||||
if (!validation) return;
|
||||
|
||||
validation.innerHTML = `
|
||||
<div class="alert alert-info">
|
||||
<i class="fas fa-spinner fa-spin me-2"></i>
|
||||
Validating template...
|
||||
</div>
|
||||
`;
|
||||
|
||||
try {
|
||||
const result = await unitforge.generateFromTemplate(
|
||||
this.currentTemplate.name,
|
||||
parameters,
|
||||
`${parameters.name || "generated"}.${this.currentTemplate.unit_type}`,
|
||||
);
|
||||
|
||||
validation.innerHTML = unitforge.formatValidationResults(
|
||||
result.validation,
|
||||
);
|
||||
|
||||
if (result.validation.valid) {
|
||||
unitforge.showToast("Template validation passed!", "success");
|
||||
} else {
|
||||
unitforge.showToast(
|
||||
`Validation found ${result.validation.errors.length} error(s)`,
|
||||
"warning",
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
validation.innerHTML = `
|
||||
<div class="alert alert-danger">
|
||||
<i class="fas fa-exclamation-circle me-2"></i>
|
||||
Validation failed: ${error.message}
|
||||
</div>
|
||||
`;
|
||||
unitforge.showToast("Validation failed", "error");
|
||||
}
|
||||
}
|
||||
|
||||
async generateAndDownload() {
|
||||
const generateBtn = document.getElementById("generateBtn");
|
||||
const originalText = generateBtn.innerHTML;
|
||||
|
||||
generateBtn.innerHTML =
|
||||
'<i class="fas fa-spinner fa-spin me-2"></i>Generating...';
|
||||
generateBtn.disabled = true;
|
||||
|
||||
try {
|
||||
const parameters = this.getFormParameters();
|
||||
const result = await unitforge.generateFromTemplate(
|
||||
this.currentTemplate.name,
|
||||
parameters,
|
||||
`${parameters.name || "generated"}.${this.currentTemplate.unit_type}`,
|
||||
);
|
||||
|
||||
// Download the file
|
||||
unitforge.downloadTextFile(result.content, result.filename);
|
||||
unitforge.showToast(`Downloaded ${result.filename}`, "success");
|
||||
|
||||
// Close modal
|
||||
const modal = bootstrap.Modal.getInstance(
|
||||
document.getElementById("templateModal"),
|
||||
);
|
||||
if (modal) {
|
||||
modal.hide();
|
||||
}
|
||||
} catch (error) {
|
||||
unitforge.showToast(`Generation failed: ${error.message}`, "error");
|
||||
} finally {
|
||||
generateBtn.innerHTML = originalText;
|
||||
generateBtn.disabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
openInEditor() {
|
||||
const parameters = this.getFormParameters();
|
||||
|
||||
// Store template data in session storage for the editor
|
||||
sessionStorage.setItem(
|
||||
"templateData",
|
||||
JSON.stringify({
|
||||
template: this.currentTemplate.name,
|
||||
parameters: parameters,
|
||||
}),
|
||||
);
|
||||
|
||||
// Open editor in new tab/window or navigate
|
||||
window.open("/editor", "_blank");
|
||||
}
|
||||
|
||||
async copyPreviewToClipboard() {
|
||||
const preview = document.getElementById("templatePreview");
|
||||
if (preview) {
|
||||
const content = preview.textContent;
|
||||
await unitforge.copyToClipboard(content);
|
||||
}
|
||||
}
|
||||
|
||||
escapeHtml(text) {
|
||||
const div = document.createElement("div");
|
||||
div.textContent = text;
|
||||
return div.innerHTML;
|
||||
}
|
||||
|
||||
debounce(func, wait) {
|
||||
let timeout;
|
||||
return function executedFunction(...args) {
|
||||
const later = () => {
|
||||
clearTimeout(timeout);
|
||||
func(...args);
|
||||
};
|
||||
clearTimeout(timeout);
|
||||
timeout = setTimeout(later, wait);
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Global functions for HTML onclick handlers
|
||||
function filterByCategory(category) {
|
||||
templatesBrowser.filterByCategory(category);
|
||||
}
|
||||
|
||||
function filterTemplates() {
|
||||
templatesBrowser.filterTemplates();
|
||||
}
|
||||
|
||||
function clearSearch() {
|
||||
templatesBrowser.clearSearch();
|
||||
}
|
||||
|
||||
function openTemplate(templateName) {
|
||||
templatesBrowser.openTemplate(templateName);
|
||||
}
|
||||
|
||||
function validateTemplate() {
|
||||
templatesBrowser.validateTemplate();
|
||||
}
|
||||
|
||||
function generateAndDownload() {
|
||||
templatesBrowser.generateAndDownload();
|
||||
}
|
||||
|
||||
function openInEditor() {
|
||||
templatesBrowser.openInEditor();
|
||||
}
|
||||
|
||||
function copyPreviewToClipboard() {
|
||||
templatesBrowser.copyPreviewToClipboard();
|
||||
}
|
||||
|
||||
// Initialize the templates browser when DOM is ready
|
||||
let templatesBrowser;
|
||||
|
||||
document.addEventListener("DOMContentLoaded", function () {
|
||||
templatesBrowser = new TemplatesBrowser();
|
||||
|
||||
// Export for use in other modules
|
||||
window.TemplatesBrowser = TemplatesBrowser;
|
||||
window.templatesBrowser = templatesBrowser;
|
||||
});
|
||||
@@ -0,0 +1,515 @@
|
||||
[
|
||||
{
|
||||
"name": "webapp",
|
||||
"description": "Web application service (Node.js, Python, etc.)",
|
||||
"unit_type": "service",
|
||||
"category": "Web Services",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "name",
|
||||
"description": "Service name",
|
||||
"type": "string",
|
||||
"required": true,
|
||||
"default": null,
|
||||
"choices": null,
|
||||
"example": "myapp"
|
||||
},
|
||||
{
|
||||
"name": "description",
|
||||
"description": "Service description",
|
||||
"type": "string",
|
||||
"required": true,
|
||||
"default": null,
|
||||
"choices": null,
|
||||
"example": "My Web Application"
|
||||
},
|
||||
{
|
||||
"name": "exec_start",
|
||||
"description": "Command to start the application",
|
||||
"type": "string",
|
||||
"required": true,
|
||||
"default": null,
|
||||
"choices": null,
|
||||
"example": "/usr/bin/node /opt/myapp/server.js"
|
||||
},
|
||||
{
|
||||
"name": "user",
|
||||
"description": "User to run the service as",
|
||||
"type": "string",
|
||||
"required": true,
|
||||
"default": "www-data",
|
||||
"choices": null,
|
||||
"example": "myapp"
|
||||
},
|
||||
{
|
||||
"name": "group",
|
||||
"description": "Group to run the service as",
|
||||
"type": "string",
|
||||
"required": true,
|
||||
"default": "www-data",
|
||||
"choices": null,
|
||||
"example": "myapp"
|
||||
},
|
||||
{
|
||||
"name": "working_directory",
|
||||
"description": "Working directory",
|
||||
"type": "string",
|
||||
"required": true,
|
||||
"default": null,
|
||||
"choices": null,
|
||||
"example": "/opt/myapp"
|
||||
},
|
||||
{
|
||||
"name": "environment_file",
|
||||
"description": "Environment file path",
|
||||
"type": "string",
|
||||
"required": false,
|
||||
"default": null,
|
||||
"choices": null,
|
||||
"example": "/etc/myapp/environment"
|
||||
},
|
||||
{
|
||||
"name": "port",
|
||||
"description": "Port number",
|
||||
"type": "integer",
|
||||
"required": false,
|
||||
"default": null,
|
||||
"choices": null,
|
||||
"example": "3000"
|
||||
},
|
||||
{
|
||||
"name": "restart_policy",
|
||||
"description": "Restart policy",
|
||||
"type": "choice",
|
||||
"required": true,
|
||||
"default": "on-failure",
|
||||
"choices": [
|
||||
"no",
|
||||
"always",
|
||||
"on-failure",
|
||||
"on-success"
|
||||
],
|
||||
"example": null
|
||||
},
|
||||
{
|
||||
"name": "private_tmp",
|
||||
"description": "Use private /tmp",
|
||||
"type": "boolean",
|
||||
"required": true,
|
||||
"default": true,
|
||||
"choices": null,
|
||||
"example": null
|
||||
},
|
||||
{
|
||||
"name": "protect_system",
|
||||
"description": "Protect system directories",
|
||||
"type": "choice",
|
||||
"required": true,
|
||||
"default": "strict",
|
||||
"choices": [
|
||||
"no",
|
||||
"yes",
|
||||
"strict",
|
||||
"full"
|
||||
],
|
||||
"example": null
|
||||
}
|
||||
],
|
||||
"tags": [
|
||||
"web",
|
||||
"application",
|
||||
"nodejs",
|
||||
"python",
|
||||
"service"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "database",
|
||||
"description": "Database service (PostgreSQL, MySQL, MongoDB, etc.)",
|
||||
"unit_type": "service",
|
||||
"category": "Database Services",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "name",
|
||||
"description": "Database service name",
|
||||
"type": "string",
|
||||
"required": true,
|
||||
"default": null,
|
||||
"choices": null,
|
||||
"example": "postgresql"
|
||||
},
|
||||
{
|
||||
"name": "description",
|
||||
"description": "Service description",
|
||||
"type": "string",
|
||||
"required": true,
|
||||
"default": null,
|
||||
"choices": null,
|
||||
"example": "PostgreSQL Database Server"
|
||||
},
|
||||
{
|
||||
"name": "exec_start",
|
||||
"description": "Database start command",
|
||||
"type": "string",
|
||||
"required": true,
|
||||
"default": null,
|
||||
"choices": null,
|
||||
"example": "/usr/lib/postgresql/13/bin/postgres -D /var/lib/postgresql/13/main"
|
||||
},
|
||||
{
|
||||
"name": "user",
|
||||
"description": "Database user",
|
||||
"type": "string",
|
||||
"required": true,
|
||||
"default": null,
|
||||
"choices": null,
|
||||
"example": "postgres"
|
||||
},
|
||||
{
|
||||
"name": "group",
|
||||
"description": "Database group",
|
||||
"type": "string",
|
||||
"required": true,
|
||||
"default": null,
|
||||
"choices": null,
|
||||
"example": "postgres"
|
||||
},
|
||||
{
|
||||
"name": "data_directory",
|
||||
"description": "Data directory",
|
||||
"type": "string",
|
||||
"required": true,
|
||||
"default": null,
|
||||
"choices": null,
|
||||
"example": "/var/lib/postgresql/13/main"
|
||||
},
|
||||
{
|
||||
"name": "pid_file",
|
||||
"description": "PID file path",
|
||||
"type": "string",
|
||||
"required": false,
|
||||
"default": null,
|
||||
"choices": null,
|
||||
"example": "/var/run/postgresql/13-main.pid"
|
||||
},
|
||||
{
|
||||
"name": "timeout_sec",
|
||||
"description": "Startup timeout",
|
||||
"type": "integer",
|
||||
"required": true,
|
||||
"default": 300,
|
||||
"choices": null,
|
||||
"example": null
|
||||
}
|
||||
],
|
||||
"tags": [
|
||||
"database",
|
||||
"postgresql",
|
||||
"mysql",
|
||||
"mongodb",
|
||||
"service"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "backup-timer",
|
||||
"description": "Scheduled backup service with timer",
|
||||
"unit_type": "timer",
|
||||
"category": "System Maintenance",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "name",
|
||||
"description": "Backup job name",
|
||||
"type": "string",
|
||||
"required": true,
|
||||
"default": null,
|
||||
"choices": null,
|
||||
"example": "daily-backup"
|
||||
},
|
||||
{
|
||||
"name": "description",
|
||||
"description": "Backup description",
|
||||
"type": "string",
|
||||
"required": true,
|
||||
"default": null,
|
||||
"choices": null,
|
||||
"example": "Daily database backup"
|
||||
},
|
||||
{
|
||||
"name": "schedule",
|
||||
"description": "Backup schedule",
|
||||
"type": "choice",
|
||||
"required": true,
|
||||
"default": "daily",
|
||||
"choices": [
|
||||
"daily",
|
||||
"weekly",
|
||||
"monthly",
|
||||
"custom"
|
||||
],
|
||||
"example": null
|
||||
},
|
||||
{
|
||||
"name": "custom_schedule",
|
||||
"description": "Custom schedule (OnCalendar format)",
|
||||
"type": "string",
|
||||
"required": false,
|
||||
"default": null,
|
||||
"choices": null,
|
||||
"example": "*-*-* 02:00:00"
|
||||
},
|
||||
{
|
||||
"name": "backup_script",
|
||||
"description": "Backup script path",
|
||||
"type": "string",
|
||||
"required": true,
|
||||
"default": null,
|
||||
"choices": null,
|
||||
"example": "/usr/local/bin/backup.sh"
|
||||
},
|
||||
{
|
||||
"name": "backup_user",
|
||||
"description": "User to run backup as",
|
||||
"type": "string",
|
||||
"required": true,
|
||||
"default": "backup",
|
||||
"choices": null,
|
||||
"example": null
|
||||
},
|
||||
{
|
||||
"name": "persistent",
|
||||
"description": "Run missed backups on boot",
|
||||
"type": "boolean",
|
||||
"required": true,
|
||||
"default": true,
|
||||
"choices": null,
|
||||
"example": null
|
||||
},
|
||||
{
|
||||
"name": "randomized_delay",
|
||||
"description": "Randomized delay in minutes",
|
||||
"type": "integer",
|
||||
"required": false,
|
||||
"default": 0,
|
||||
"choices": null,
|
||||
"example": null
|
||||
}
|
||||
],
|
||||
"tags": [
|
||||
"backup",
|
||||
"timer",
|
||||
"maintenance",
|
||||
"scheduled"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "proxy-socket",
|
||||
"description": "Socket-activated proxy service",
|
||||
"unit_type": "socket",
|
||||
"category": "Network Services",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "name",
|
||||
"description": "Socket service name",
|
||||
"type": "string",
|
||||
"required": true,
|
||||
"default": null,
|
||||
"choices": null,
|
||||
"example": "myapp-proxy"
|
||||
},
|
||||
{
|
||||
"name": "description",
|
||||
"description": "Socket description",
|
||||
"type": "string",
|
||||
"required": true,
|
||||
"default": null,
|
||||
"choices": null,
|
||||
"example": "Proxy socket for myapp"
|
||||
},
|
||||
{
|
||||
"name": "listen_port",
|
||||
"description": "Port to listen on",
|
||||
"type": "integer",
|
||||
"required": true,
|
||||
"default": null,
|
||||
"choices": null,
|
||||
"example": "8080"
|
||||
},
|
||||
{
|
||||
"name": "listen_address",
|
||||
"description": "Address to bind to",
|
||||
"type": "string",
|
||||
"required": true,
|
||||
"default": "0.0.0.0",
|
||||
"choices": null,
|
||||
"example": "127.0.0.1"
|
||||
},
|
||||
{
|
||||
"name": "socket_user",
|
||||
"description": "Socket owner user",
|
||||
"type": "string",
|
||||
"required": false,
|
||||
"default": null,
|
||||
"choices": null,
|
||||
"example": "www-data"
|
||||
},
|
||||
{
|
||||
"name": "socket_group",
|
||||
"description": "Socket owner group",
|
||||
"type": "string",
|
||||
"required": false,
|
||||
"default": null,
|
||||
"choices": null,
|
||||
"example": "www-data"
|
||||
},
|
||||
{
|
||||
"name": "socket_mode",
|
||||
"description": "Socket file permissions",
|
||||
"type": "string",
|
||||
"required": true,
|
||||
"default": "0644",
|
||||
"choices": null,
|
||||
"example": "0660"
|
||||
},
|
||||
{
|
||||
"name": "accept",
|
||||
"description": "Accept multiple connections",
|
||||
"type": "boolean",
|
||||
"required": true,
|
||||
"default": false,
|
||||
"choices": null,
|
||||
"example": null
|
||||
},
|
||||
{
|
||||
"name": "max_connections",
|
||||
"description": "Maximum connections",
|
||||
"type": "integer",
|
||||
"required": false,
|
||||
"default": 64,
|
||||
"choices": null,
|
||||
"example": null
|
||||
}
|
||||
],
|
||||
"tags": [
|
||||
"socket",
|
||||
"proxy",
|
||||
"network",
|
||||
"activation"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "container",
|
||||
"description": "Containerized service (Docker/Podman)",
|
||||
"unit_type": "service",
|
||||
"category": "Container Services",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "name",
|
||||
"description": "Container service name",
|
||||
"type": "string",
|
||||
"required": true,
|
||||
"default": null,
|
||||
"choices": null,
|
||||
"example": "webapp-container"
|
||||
},
|
||||
{
|
||||
"name": "description",
|
||||
"description": "Container description",
|
||||
"type": "string",
|
||||
"required": true,
|
||||
"default": null,
|
||||
"choices": null,
|
||||
"example": "My Web App Container"
|
||||
},
|
||||
{
|
||||
"name": "container_runtime",
|
||||
"description": "Container runtime",
|
||||
"type": "choice",
|
||||
"required": true,
|
||||
"default": "docker",
|
||||
"choices": [
|
||||
"docker",
|
||||
"podman"
|
||||
],
|
||||
"example": null
|
||||
},
|
||||
{
|
||||
"name": "image",
|
||||
"description": "Container image",
|
||||
"type": "string",
|
||||
"required": true,
|
||||
"default": null,
|
||||
"choices": null,
|
||||
"example": "nginx:latest"
|
||||
},
|
||||
{
|
||||
"name": "ports",
|
||||
"description": "Port mappings",
|
||||
"type": "string",
|
||||
"required": false,
|
||||
"default": null,
|
||||
"choices": null,
|
||||
"example": "80:8080,443:8443"
|
||||
},
|
||||
{
|
||||
"name": "volumes",
|
||||
"description": "Volume mounts",
|
||||
"type": "string",
|
||||
"required": false,
|
||||
"default": null,
|
||||
"choices": null,
|
||||
"example": "/data:/app/data,/config:/app/config"
|
||||
},
|
||||
{
|
||||
"name": "environment",
|
||||
"description": "Environment variables",
|
||||
"type": "string",
|
||||
"required": false,
|
||||
"default": null,
|
||||
"choices": null,
|
||||
"example": "ENV=production,DEBUG=false"
|
||||
},
|
||||
{
|
||||
"name": "network",
|
||||
"description": "Container network",
|
||||
"type": "string",
|
||||
"required": false,
|
||||
"default": null,
|
||||
"choices": null,
|
||||
"example": "bridge"
|
||||
},
|
||||
{
|
||||
"name": "restart_policy",
|
||||
"description": "Container restart policy",
|
||||
"type": "choice",
|
||||
"required": true,
|
||||
"default": "unless-stopped",
|
||||
"choices": [
|
||||
"no",
|
||||
"always",
|
||||
"unless-stopped",
|
||||
"on-failure"
|
||||
],
|
||||
"example": null
|
||||
},
|
||||
{
|
||||
"name": "pull_policy",
|
||||
"description": "Image pull policy",
|
||||
"type": "choice",
|
||||
"required": true,
|
||||
"default": "missing",
|
||||
"choices": [
|
||||
"always",
|
||||
"missing",
|
||||
"never"
|
||||
],
|
||||
"example": null
|
||||
}
|
||||
],
|
||||
"tags": [
|
||||
"container",
|
||||
"docker",
|
||||
"podman",
|
||||
"service"
|
||||
]
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,322 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Editor - UnitForge</title>
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css" rel="stylesheet">
|
||||
<link href="https://cdnjs.cloudflare.com/ajax/libs/codemirror/6.0.1/codemirror.min.css" rel="stylesheet">
|
||||
<link href="https://cdnjs.cloudflare.com/ajax/libs/codemirror/6.0.1/theme/monokai.min.css" rel="stylesheet">
|
||||
<link href="/static/css/style.css" rel="stylesheet">
|
||||
</head>
|
||||
<body>
|
||||
<nav class="navbar navbar-expand-lg navbar-dark bg-primary">
|
||||
<div class="container">
|
||||
<a class="navbar-brand" href="/">
|
||||
<i class="fas fa-cogs me-2"></i>UnitForge
|
||||
</a>
|
||||
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav">
|
||||
<span class="navbar-toggler-icon"></span>
|
||||
</button>
|
||||
<div class="collapse navbar-collapse" id="navbarNav">
|
||||
<ul class="navbar-nav me-auto">
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="/">Home</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link active" href="/editor">Editor</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="/templates">Templates</a>
|
||||
</li>
|
||||
</ul>
|
||||
<div class="navbar-nav">
|
||||
<button class="btn btn-outline-light btn-sm me-2" onclick="downloadFile()">
|
||||
<i class="fas fa-download me-1"></i>Download
|
||||
</button>
|
||||
<button class="btn btn-outline-light btn-sm" onclick="showUploadModal()">
|
||||
<i class="fas fa-upload me-1"></i>Upload
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<div class="container-fluid py-4">
|
||||
<div class="row">
|
||||
<!-- Editor Configuration Panel -->
|
||||
<div class="col-lg-3">
|
||||
<div class="card border-0 shadow-sm h-100">
|
||||
<div class="card-header bg-light">
|
||||
<h6 class="card-title mb-0">
|
||||
<i class="fas fa-cog me-2"></i>Configuration
|
||||
</h6>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<!-- Basic Settings -->
|
||||
<div class="mb-4">
|
||||
<h6 class="text-muted mb-3">Basic Settings</h6>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="unitType" class="form-label">Unit Type</label>
|
||||
<select class="form-select" id="unitType" onchange="changeUnitType()">
|
||||
<option value="service">Service</option>
|
||||
<option value="timer">Timer</option>
|
||||
<option value="socket">Socket</option>
|
||||
<option value="mount">Mount</option>
|
||||
<option value="target">Target</option>
|
||||
<option value="path">Path</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="unitName" class="form-label">Unit Name</label>
|
||||
<input type="text" class="form-control" id="unitName" placeholder="myservice" oninput="updateFilename()">
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="unitDescription" class="form-label">Description</label>
|
||||
<input type="text" class="form-control" id="unitDescription" placeholder="My Service Description" oninput="updateField('Unit', 'Description', this.value)">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Service-specific fields -->
|
||||
<div id="serviceFields" class="unit-type-fields">
|
||||
<h6 class="text-muted mb-3">Service Configuration</h6>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="serviceType" class="form-label">Service Type</label>
|
||||
<select class="form-select" id="serviceType" onchange="updateField('Service', 'Type', this.value)">
|
||||
<option value="simple">Simple</option>
|
||||
<option value="exec">Exec</option>
|
||||
<option value="forking">Forking</option>
|
||||
<option value="oneshot">Oneshot</option>
|
||||
<option value="dbus">D-Bus</option>
|
||||
<option value="notify">Notify</option>
|
||||
<option value="idle">Idle</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="execStart" class="form-label">Exec Start</label>
|
||||
<input type="text" class="form-control" id="execStart" placeholder="/usr/bin/myapp" oninput="updateField('Service', 'ExecStart', this.value)">
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="user" class="form-label">User</label>
|
||||
<input type="text" class="form-control" id="user" placeholder="www-data" oninput="updateField('Service', 'User', this.value)">
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="workingDirectory" class="form-label">Working Directory</label>
|
||||
<input type="text" class="form-control" id="workingDirectory" placeholder="/opt/myapp" oninput="updateField('Service', 'WorkingDirectory', this.value)">
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="restart" class="form-label">Restart Policy</label>
|
||||
<select class="form-select" id="restart" onchange="updateField('Service', 'Restart', this.value)">
|
||||
<option value="no">No</option>
|
||||
<option value="always">Always</option>
|
||||
<option value="on-success">On Success</option>
|
||||
<option value="on-failure" selected>On Failure</option>
|
||||
<option value="on-abnormal">On Abnormal</option>
|
||||
<option value="on-abort">On Abort</option>
|
||||
<option value="on-watchdog">On Watchdog</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Timer-specific fields -->
|
||||
<div id="timerFields" class="unit-type-fields d-none">
|
||||
<h6 class="text-muted mb-3">Timer Configuration</h6>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="onCalendar" class="form-label">On Calendar</label>
|
||||
<input type="text" class="form-control" id="onCalendar" placeholder="daily" oninput="updateField('Timer', 'OnCalendar', this.value)">
|
||||
<div class="form-text">Examples: daily, weekly, monthly, *-*-* 02:00:00</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="persistent" class="form-label">Persistent</label>
|
||||
<select class="form-select" id="persistent" onchange="updateField('Timer', 'Persistent', this.value)">
|
||||
<option value="true" selected>True</option>
|
||||
<option value="false">False</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Socket-specific fields -->
|
||||
<div id="socketFields" class="unit-type-fields d-none">
|
||||
<h6 class="text-muted mb-3">Socket Configuration</h6>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="listenStream" class="form-label">Listen Stream</label>
|
||||
<input type="text" class="form-control" id="listenStream" placeholder="127.0.0.1:8080" oninput="updateField('Socket', 'ListenStream', this.value)">
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="socketUser" class="form-label">Socket User</label>
|
||||
<input type="text" class="form-control" id="socketUser" placeholder="www-data" oninput="updateField('Socket', 'SocketUser', this.value)">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Install section -->
|
||||
<div class="mb-4">
|
||||
<h6 class="text-muted mb-3">Install Configuration</h6>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="wantedBy" class="form-label">Wanted By</label>
|
||||
<input type="text" class="form-control" id="wantedBy" value="multi-user.target" oninput="updateField('Install', 'WantedBy', this.value)">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Actions -->
|
||||
<div class="d-grid gap-2">
|
||||
<button class="btn btn-success" onclick="validateUnit()">
|
||||
<i class="fas fa-check me-2"></i>Validate
|
||||
</button>
|
||||
<button class="btn btn-outline-primary" onclick="resetEditor()">
|
||||
<i class="fas fa-refresh me-2"></i>Reset
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Main Editor Area -->
|
||||
<div class="col-lg-6">
|
||||
<div class="card border-0 shadow-sm h-100">
|
||||
<div class="card-header bg-light d-flex justify-content-between align-items-center">
|
||||
<div>
|
||||
<h6 class="card-title mb-0">
|
||||
<i class="fas fa-file-code me-2"></i>
|
||||
<span id="filename">myservice.service</span>
|
||||
</h6>
|
||||
</div>
|
||||
<div class="btn-group btn-group-sm">
|
||||
<button class="btn btn-outline-secondary" onclick="formatUnit()" title="Format">
|
||||
<i class="fas fa-indent"></i>
|
||||
</button>
|
||||
<button class="btn btn-outline-secondary" onclick="copyToClipboard()" title="Copy">
|
||||
<i class="fas fa-copy"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body p-0">
|
||||
<textarea id="editor" class="form-control border-0" rows="25" style="font-family: 'Courier New', monospace; resize: none;">[Unit]
|
||||
Description=My Service Description
|
||||
After=network.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
ExecStart=/usr/bin/myapp
|
||||
User=www-data
|
||||
Restart=on-failure
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target</textarea>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Validation & Preview Panel -->
|
||||
<div class="col-lg-3">
|
||||
<div class="card border-0 shadow-sm h-100">
|
||||
<div class="card-header bg-light">
|
||||
<h6 class="card-title mb-0">
|
||||
<i class="fas fa-clipboard-check me-2"></i>Validation & Info
|
||||
</h6>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<!-- Validation Results -->
|
||||
<div id="validationResults" class="mb-4">
|
||||
<div class="alert alert-info">
|
||||
<i class="fas fa-info-circle me-2"></i>
|
||||
Click "Validate" to check your unit file.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Unit File Info -->
|
||||
<div class="mb-4">
|
||||
<h6 class="text-muted mb-3">Unit Information</h6>
|
||||
<div id="unitInfo">
|
||||
<div class="info-item">
|
||||
<strong>Type:</strong> <span id="infoType">service</span>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<strong>Sections:</strong> <span id="infoSections">3</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Quick Help -->
|
||||
<div class="mb-4">
|
||||
<h6 class="text-muted mb-3">Quick Help</h6>
|
||||
<div class="help-content">
|
||||
<div class="help-item mb-2">
|
||||
<small></small><strong>[Unit]</strong> - Basic metadata and dependencies</small>
|
||||
</div>
|
||||
<div class="help-item mb-2">
|
||||
<small><strong>[Service]</strong> - Service-specific configuration</small>
|
||||
</div>
|
||||
<div class="help-item mb-2">
|
||||
<small><strong>[Install]</strong> - Installation and enabling info</small>
|
||||
</div>
|
||||
<div class="help-item">
|
||||
<small><a href="https://www.freedesktop.org/software/systemd/man/systemd.unit.html" target="_blank">systemd documentation <i class="fas fa-external-link-alt"></i></a></small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Common Patterns -->
|
||||
<div>
|
||||
<h6 class="text-muted mb-3">Common Patterns</h6>
|
||||
<div class="d-grid gap-2">
|
||||
<button class="btn btn-outline-secondary btn-sm" onclick="insertPattern('web-service')">
|
||||
Web Service
|
||||
</button>
|
||||
<button class="btn btn-outline-secondary btn-sm" onclick="insertPattern('background-job')">
|
||||
Background Job
|
||||
</button>
|
||||
<button class="btn btn-outline-secondary btn-sm" onclick="insertPattern('database')">
|
||||
Database Service
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Upload Modal -->
|
||||
<div class="modal fade" id="uploadModal" tabindex="-1">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">
|
||||
<i class="fas fa-upload me-2"></i>Upload Unit File
|
||||
</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="mb-3">
|
||||
<label for="fileInput" class="form-label">Select unit file:</label>
|
||||
<input type="file" class="form-control" id="fileInput" accept=".service,.timer,.socket,.mount,.target,.path">
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
|
||||
<button type="button" class="btn btn-primary" onclick="loadFile()">
|
||||
<i class="fas fa-upload me-2"></i>Load
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
|
||||
<script src="/static/js/editor.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,313 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>UnitForge - Systemd Unit File Creator</title>
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css" rel="stylesheet">
|
||||
<link href="/static/css/style.css" rel="stylesheet">
|
||||
</head>
|
||||
<body>
|
||||
<nav class="navbar navbar-expand-lg navbar-dark bg-primary">
|
||||
<div class="container">
|
||||
<a class="navbar-brand" href="/">
|
||||
<i class="fas fa-cogs me-2"></i>UnitForge
|
||||
</a>
|
||||
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav">
|
||||
<span class="navbar-toggler-icon"></span>
|
||||
</button>
|
||||
<div class="collapse navbar-collapse" id="navbarNav">
|
||||
<ul class="navbar-nav me-auto">
|
||||
<li class="nav-item">
|
||||
<a class="nav-link active" href="/">Home</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="/editor">Editor</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="/templates">Templates</a>
|
||||
</li>
|
||||
</ul>
|
||||
<ul class="navbar-nav">
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="/api/docs" target="_blank">
|
||||
<i class="fas fa-book me-1"></i>API Docs
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<div class="hero-section bg-light py-5">
|
||||
<div class="container">
|
||||
<div class="row align-items-center">
|
||||
<div class="col-lg-6">
|
||||
<h1 class="display-4 fw-bold mb-3">UnitForge</h1>
|
||||
<p class="lead mb-4">
|
||||
Create, validate, and manage systemd unit files with ease.
|
||||
Whether you're deploying web services, scheduling tasks, or managing containers,
|
||||
UnitForge provides the tools you need.
|
||||
</p>
|
||||
<div class="d-flex gap-3">
|
||||
<a href="/editor" class="btn btn-primary btn-lg">
|
||||
<i class="fas fa-edit me-2"></i>Start Creating
|
||||
</a>
|
||||
<a href="/templates" class="btn btn-outline-primary btn-lg">
|
||||
<i class="fas fa-templates me-2"></i>Browse Templates
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-lg-6">
|
||||
<div class="code-preview bg-dark text-light p-4 rounded">
|
||||
<div class="d-flex align-items-center mb-3">
|
||||
<i class="fas fa-file-code me-2"></i>
|
||||
<span class="fw-bold">myapp.service</span>
|
||||
</div>
|
||||
<pre class="mb-0"><code>[Unit]
|
||||
Description=My Web Application
|
||||
After=network.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
ExecStart=/usr/bin/node /opt/myapp/server.js
|
||||
User=myapp
|
||||
Group=myapp
|
||||
Restart=on-failure
|
||||
WorkingDirectory=/opt/myapp
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target</code></pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="container my-5">
|
||||
<div class="row">
|
||||
<div class="col-lg-8 mx-auto text-center mb-5">
|
||||
<h2 class="h1 mb-3">Choose Your Approach</h2>
|
||||
<p class="lead text-muted">
|
||||
UnitForge offers multiple ways to create systemd unit files,
|
||||
from quick templates to detailed manual editing.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row g-4 mb-5">
|
||||
<div class="col-md-6 col-lg-3">
|
||||
<div class="card h-100 border-0 shadow-sm hover-lift">
|
||||
<div class="card-body text-center">
|
||||
<div class="feature-icon bg-primary bg-gradient rounded-circle mx-auto mb-3">
|
||||
<i class="fas fa-magic text-white"></i>
|
||||
</div>
|
||||
<h5 class="card-title">Quick Templates</h5>
|
||||
<p class="card-text text-muted">
|
||||
Use pre-built templates for common services like web apps, databases, and containers.
|
||||
</p>
|
||||
<a href="/templates" class="btn btn-outline-primary">
|
||||
Browse Templates
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-6 col-lg-3">
|
||||
<div class="card h-100 border-0 shadow-sm hover-lift">
|
||||
<div class="card-body text-center">
|
||||
<div class="feature-icon bg-success bg-gradient rounded-circle mx-auto mb-3">
|
||||
<i class="fas fa-edit text-white"></i>
|
||||
</div>
|
||||
<h5 class="card-title">Visual Editor</h5>
|
||||
<p class="card-text text-muted">
|
||||
Create and edit unit files with our intuitive form-based editor with real-time validation.
|
||||
</p>
|
||||
<a href="/editor" class="btn btn-outline-success">
|
||||
Open Editor
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-6 col-lg-3">
|
||||
<div class="card h-100 border-0 shadow-sm hover-lift">
|
||||
<div class="card-body text-center">
|
||||
<div class="feature-icon bg-warning bg-gradient rounded-circle mx-auto mb-3">
|
||||
<i class="fas fa-check-circle text-white"></i>
|
||||
</div>
|
||||
<h5 class="card-title">Validation</h5>
|
||||
<p class="card-text text-muted">
|
||||
Validate existing unit files and get detailed feedback on syntax and best practices.
|
||||
</p>
|
||||
<button class="btn btn-outline-warning" onclick="showUploadModal()">
|
||||
Validate File
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-6 col-lg-3">
|
||||
<div class="card h-100 border-0 shadow-sm hover-lift">
|
||||
<div class="card-body text-center">
|
||||
<div class="feature-icon bg-info bg-gradient rounded-circle mx-auto mb-3">
|
||||
<i class="fas fa-terminal text-white"></i>
|
||||
</div>
|
||||
<h5 class="card-title">CLI Tool</h5>
|
||||
<p class="card-text text-muted">
|
||||
Use the command-line interface for automation and integration with your development workflow.
|
||||
</p>
|
||||
<button class="btn btn-outline-info" onclick="showCliModal()">
|
||||
View CLI
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-lg-8 mx-auto">
|
||||
<div class="text-center mb-4">
|
||||
<h3></h3>Supported Unit Types</h3>
|
||||
<p class="text-muted">UnitForge supports all major systemd unit types</p>
|
||||
</div>
|
||||
|
||||
<div class="row g-3">
|
||||
<div class="col-6 col-md-4">
|
||||
<div class="unit-type-badge">
|
||||
<i class="fas fa-play-circle me-2"></i>Service
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-6 col-md-4">
|
||||
<div class="unit-type-badge">
|
||||
<i class="fas fa-clock me-2"></i>Timer
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-6 col-md-4">
|
||||
<div class="unit-type-badge">
|
||||
<i class="fas fa-plug me-2"></i>Socket
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-6 col-md-4">
|
||||
<div class="unit-type-badge">
|
||||
<i class="fas fa-hdd me-2"></i>Mount
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-6 col-md-4">
|
||||
<div class="unit-type-badge">
|
||||
<i class="fas fa-bullseye me-2"></i>Target
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-6 col-md-4">
|
||||
<div class="unit-type-badge">
|
||||
<i class="fas fa-folder me-2"></i>Path
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Upload Modal -->
|
||||
<div class="modal fade" id="uploadModal" tabindex="-1">
|
||||
<div class="modal-dialog modal-lg">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">
|
||||
<i class="fas fa-upload me-2"></i>Validate Unit File
|
||||
</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="mb-3">
|
||||
<label for="fileInput" class="form-label">Select unit file to validate:</label>
|
||||
<input type="file" class="form-control" id="fileInput" accept=".service,.timer,.socket,.mount,.target,.path">
|
||||
</div>
|
||||
<div id="uploadResults" class="d-none">
|
||||
<h6>Validation Results:</h6>
|
||||
<div id="validationOutput"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
|
||||
<button type="button" class="btn btn-primary" onclick="validateFile()">
|
||||
<i class="fas fa-check me-2"></i>Validate
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- CLI Modal -->
|
||||
<div class="modal fade" id="cliModal" tabindex="-1">
|
||||
<div class="modal-dialog modal-lg">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">
|
||||
<i class="fas fa-terminal me-2"></i>CLI Usage
|
||||
</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<h6>Installation:</h6>
|
||||
<pre class="bg-dark text-light p-3 rounded"><code>pip install -e .</code></pre>
|
||||
|
||||
<h6 class="mt-4">Common Commands:</h6>
|
||||
<pre class="bg-dark text-light p-3 rounded"><code># Create a new service
|
||||
unitforge create --type service --name myapp --exec-start "/usr/bin/myapp"
|
||||
|
||||
# Validate a unit file
|
||||
unitforge validate /etc/systemd/system/myapp.service
|
||||
|
||||
# Generate from template
|
||||
unitforge template generate webapp --interactive
|
||||
|
||||
# List available templates
|
||||
unitforge template list</code></pre>
|
||||
|
||||
<h6 class="mt-4">Template Usage:</h6>
|
||||
<pre class="bg-dark text-light p-3 rounded"><code># Generate web app service
|
||||
unitforge template generate webapp \
|
||||
--param name=mywebapp \
|
||||
--param exec_start="/usr/bin/node server.js" \
|
||||
--param user=www-data \
|
||||
--param working_directory=/opt/mywebapp</code></pre>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
|
||||
<a href="/api/docs" class="btn btn-primary" target="_blank">
|
||||
<i class="fas fa-book me-2"></i>Full Documentation
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<footer class="bg-dark text-light py-4 mt-5">
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<h6></h6><i class="fas fa-cogs me-2"></i>UnitForge</h6>
|
||||
<p class="text-muted small">
|
||||
A comprehensive tool for creating and managing systemd unit files.
|
||||
</p>
|
||||
</div>
|
||||
<div class="col-md-6 text-md-end">
|
||||
<div class="d-flex justify-content-md-end gap-3">
|
||||
<a href="/api/docs" class="text-light text-decoration-none">
|
||||
<i class="fas fa-book me-1"></i>API Docs
|
||||
</a>
|
||||
<a href="https://github.com/unitforge/unitforge" class="text-light text-decoration-none">
|
||||
<i class="fab fa-github me-1"></i>GitHub
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
|
||||
<script src="/static/js/main.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,237 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Templates - UnitForge</title>
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css" rel="stylesheet">
|
||||
<link href="/static/css/style.css" rel="stylesheet">
|
||||
</head>
|
||||
<body>
|
||||
<nav class="navbar navbar-expand-lg navbar-dark bg-primary">
|
||||
<div class="container">
|
||||
<a class="navbar-brand" href="/">
|
||||
<i class="fas fa-cogs me-2"></i>UnitForge
|
||||
</a>
|
||||
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav">
|
||||
<span class="navbar-toggler-icon"></span>
|
||||
</button>
|
||||
<div class="collapse navbar-collapse" id="navbarNav">
|
||||
<ul class="navbar-nav me-auto">
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="/">Home</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="/editor">Editor</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link active" href="/templates">Templates</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<div class="container py-4">
|
||||
<!-- Header -->
|
||||
<div class="row mb-4">
|
||||
<div class="col-lg-8">
|
||||
<h1 class="display-5 fw-bold mb-3">
|
||||
<i class="fas fa-templates me-3"></i>Unit File Templates
|
||||
</h1>
|
||||
<p class="lead text-muted">
|
||||
Choose from pre-built templates for common systemd unit configurations.
|
||||
Each template provides a solid foundation that you can customize for your specific needs.
|
||||
</p>
|
||||
</div>
|
||||
<div class="col-lg-4 text-lg-end">
|
||||
<div class="input-group">
|
||||
<input type="text" class="form-control" id="searchInput" placeholder="Search templates..." onkeyup="filterTemplates()">
|
||||
<button class="btn btn-outline-secondary" type="button" onclick="clearSearch()">
|
||||
<i class="fas fa-times"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Filter Tabs -->
|
||||
<div class="row mb-4">
|
||||
<div class="col">
|
||||
<ul class="nav nav-pills nav-fill" id="categoryTabs">
|
||||
<li class="nav-item">
|
||||
<button class="nav-link active" onclick="filterByCategory('all')" id="tab-all">
|
||||
All Templates <span class="badge bg-secondary ms-2" id="count-all">0</span>
|
||||
</button>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<button class="nav-link" onclick="filterByCategory('Web Services')" id="tab-web">
|
||||
Web Services <span class="badge bg-secondary ms-2" id="count-web">0</span>
|
||||
</button>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<button class="nav-link" onclick="filterByCategory('Database Services')" id="tab-database">
|
||||
Database <span class="badge bg-secondary ms-2" id="count-database">0</span>
|
||||
</button>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<button class="nav-link" onclick="filterByCategory('System Maintenance')" id="tab-maintenance">
|
||||
Maintenance <span class="badge bg-secondary ms-2" id="count-maintenance">0</span>
|
||||
</button>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<button class="nav-link" onclick="filterByCategory('Container Services')" id="tab-container">
|
||||
Containers <span class="badge bg-secondary ms-2" id="count-container">0</span>
|
||||
</button>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<button class="nav-link" onclick="filterByCategory('Network Services')" id="tab-network">
|
||||
Network <span class="badge bg-secondary ms-2" id="count-network">0</span>
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Loading State -->
|
||||
<div id="loadingState" class="text-center py-5">
|
||||
<div class="spinner-border text-primary" role="status">
|
||||
<span class="visually-hidden">Loading templates...</span>
|
||||
</div>
|
||||
<p class="mt-3 text-muted">Loading templates...</p>
|
||||
</div>
|
||||
|
||||
<!-- Templates Grid -->
|
||||
<div id="templatesGrid" class="row g-4 d-none">
|
||||
<!-- Templates will be populated here by JavaScript -->
|
||||
</div>
|
||||
|
||||
<!-- No Results -->
|
||||
<div id="noResults" class="text-center py-5 d-none">
|
||||
<i class="fas fa-search fa-3x text-muted mb-3"></i>
|
||||
<h4>No templates found</h4>
|
||||
<p class="text-muted">Try adjusting your search criteria or browse all templates.</p>
|
||||
<button class="btn btn-primary" onclick="clearSearch()">Show All Templates</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Template Detail Modal -->
|
||||
<div class="modal fade" id="templateModal" tabindex="-1">
|
||||
<div class="modal-dialog modal-xl">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title" id="templateModalTitle">
|
||||
<i class="fas fa-file-code me-2"></i>Template Details
|
||||
</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="row">
|
||||
<!-- Template Info -->
|
||||
<div class="col-lg-6">
|
||||
<div class="mb-4">
|
||||
<h6 class="text-muted mb-3">Template Information</h6>
|
||||
<div id="templateInfo">
|
||||
<!-- Template info will be populated here -->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Parameters Form -->
|
||||
<div class="mb-4">
|
||||
<h6 class="text-muted mb-3">Configuration Parameters</h6>
|
||||
<form id="templateForm">
|
||||
<div id="templateParameters">
|
||||
<!-- Parameters will be populated here -->
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Preview -->
|
||||
<div class="col-lg-6">
|
||||
<div class="mb-4">
|
||||
<h6 class="text-muted mb-3">Preview</h6>
|
||||
<div class="card bg-dark text-light">
|
||||
<div class="card-header d-flex justify-content-between align-items-center">
|
||||
<span><i class="fas fa-file-code me-2"></i><span id="previewFilename">unit.service</span></span>
|
||||
<button class="btn btn-outline-light btn-sm" onclick="copyPreviewToClipboard()" title="Copy to clipboard">
|
||||
<i class="fas fa-copy"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<pre id="templatePreview" class="mb-0" style="font-size: 0.875rem; line-height: 1.4;"><code># Configure parameters to see preview</code></pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Validation Results -->
|
||||
<div id="validationResults" class="d-none">
|
||||
<h6 class="text-muted mb-3">Validation</h6>
|
||||
<div id="validationOutput"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
|
||||
<button type="button" class="btn btn-success" onclick="validateTemplate()" id="validateBtn">
|
||||
<i class="fas fa-check me-2"></i>Validate
|
||||
</button>
|
||||
<button type="button" class="btn btn-primary" onclick="generateAndDownload()" id="generateBtn">
|
||||
<i class="fas fa-download me-2"></i>Generate & Download
|
||||
</button>
|
||||
<button type="button" class="btn btn-info" onclick="openInEditor()" id="editorBtn">
|
||||
<i class="fas fa-edit me-2"></i>Open in Editor
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Generate Status Modal -->
|
||||
<div class="modal fade" id="generateModal" tabindex="-1">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">
|
||||
<i class="fas fa-cog fa-spin me-2"></i>Generating Unit File
|
||||
</h5>
|
||||
</div>
|
||||
<div class="modal-body text-center">
|
||||
<div class="spinner-border text-primary mb-3" role="status">
|
||||
<span class="visually-hidden">Generating...</span>
|
||||
</div>
|
||||
<p class="mb-0">Please wait while we generate your unit file...</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<footer class="bg-dark text-light py-4 mt-5">
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<h6></h6><i class="fas fa-cogs me-2"></i>UnitForge</h6>
|
||||
<p class="text-muted small">
|
||||
A comprehensive tool for creating and managing systemd unit files.
|
||||
</p>
|
||||
</div>
|
||||
<div class="col-md-6 text-md-end">
|
||||
<div class="d-flex justify-content-md-end gap-3">
|
||||
<a href="/api/docs" class="text-light text-decoration-none">
|
||||
<i class="fas fa-book me-1"></i>API Docs
|
||||
</a>
|
||||
<a href="https://github.com/unitforge/unitforge" class="text-light text-decoration-none">
|
||||
<i class="fab fa-github me-1"></i>GitHub
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
|
||||
<script src="/static/js/main.js"></script>
|
||||
<script src="/static/js/templates.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user