// 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 = `
Validating unit file...
`; 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 = `
Validation failed: ${error.message}
`; 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 = `
Click "Validate" to check your unit file.
`; } // 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;