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:
William Valentin
2025-09-14 14:58:35 -07:00
commit 860f60591c
37 changed files with 11599 additions and 0 deletions

View File

@@ -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;

382
frontend/static/js/main.js Normal file
View File

@@ -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;

View File

@@ -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;
});