diff --git a/.claude/settings.local.json b/.claude/settings.local.json new file mode 100644 index 000000000..64b936302 --- /dev/null +++ b/.claude/settings.local.json @@ -0,0 +1,12 @@ +{ + "permissions": { + "allow": [ + "Bash(docker-compose up:*)", + "Bash(python:*)", + "Bash(cat:*)", + "Bash(find:*)", + "Bash(docker-compose exec:*)", + "mcp__sequentialthinking__sequentialthinking" + ] + } +} diff --git a/Layout/default/Cpeprovisioning/Index.php b/Layout/default/Cpeprovisioning/Index.php index 398cdb01a..9e1209544 100644 --- a/Layout/default/Cpeprovisioning/Index.php +++ b/Layout/default/Cpeprovisioning/Index.php @@ -318,6 +318,9 @@ $pagination_entity_name = "Zu provisionierende CPEs"; + diff --git a/Layout/default/Device/Detail.php b/Layout/default/Device/Detail.php index 622a52b66..c4d393830 100644 --- a/Layout/default/Device/Detail.php +++ b/Layout/default/Device/Detail.php @@ -408,7 +408,7 @@ foreach ($devicesall as $deviceall) { success == "true" && $devicesconfig->data > 0) { + if ($devicesconfig->success == "true" && $devicesconfig->data) { ?>
| OLT |
|
+ |
+
+
+
+
+ |
+
|
+ |
+
+
+ articleNumber); ?>
+ title); ?>
+ |
+
Connect to browse remote files
+No file selected
'; + document.getElementById('restore-btn').disabled = true; + + hideStatus(); +} + +async function browse(path) { + if (!isConnected) return; + + try { + const response = await fetch('/api/browse', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ path: path }) + }); + + const data = await response.json(); + + if (data.success) { + currentPath = data.path; + renderFiles(data.files, data.path); + updateBreadcrumb(data.path); + } else { + showStatus('Browse failed: ' + data.error, 'error'); + } + } catch (error) { + showStatus('Browse error: ' + error.message, 'error'); + } +} + +function renderFiles(files, path) { + const container = document.getElementById('file-browser'); + + if (files.length === 0) { + container.innerHTML = '${file.name}
+${file.size_human} - ${file.mtime_human}
+ `; + document.getElementById('restore-btn').disabled = false; + + // Auto-detect target database from filename + const filename = file.name.toLowerCase(); + const targetDbSelect = document.getElementById('target-db'); + const availableDbs = Array.from(targetDbSelect.options).map(o => o.value); + + for (const db of availableDbs) { + if (filename.includes(db.toLowerCase())) { + targetDbSelect.value = db; + break; + } + } + + // Re-render to show selection + browse(currentPath); +} + +function updateBreadcrumb(path) { + const parts = path.split('/').filter(p => p); + let html = `/`; + + let currentPathBuild = ''; + parts.forEach((part, index) => { + currentPathBuild += '/' + part; + const isLast = index === parts.length - 1; + html += ` + / + ${part} + `; + }); + + document.getElementById('breadcrumb').innerHTML = html; +} + +async function startRestore() { + if (!selectedFile) return; + + const targetDb = document.getElementById('target-db').value; + + if (!confirm(`Are you sure you want to restore ${selectedFile.name} to database "${targetDb}"?\n\nThis will DROP ALL TABLES in the database!`)) { + return; + } + + const btn = document.getElementById('restore-btn'); + btn.disabled = true; + btn.textContent = 'Starting...'; + + try { + const response = await fetch('/api/restore', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + file: selectedFile.path, + database: targetDb + }) + }); + + const data = await response.json(); + + if (data.success) { + currentJobId = data.job_id; + showProgressPanel(); + startPolling(); + } else { + showStatus('Restore failed: ' + data.error, 'error'); + btn.disabled = false; + btn.textContent = 'Start Restore'; + } + } catch (error) { + showStatus('Restore error: ' + error.message, 'error'); + btn.disabled = false; + btn.textContent = 'Start Restore'; + } +} + +function showProgressPanel() { + document.getElementById('progress-panel').classList.remove('hidden'); + document.getElementById('progress-bar').style.width = '0%'; + document.getElementById('progress-bar').classList.remove('bg-green-600', 'bg-red-600', 'bg-yellow-600'); + document.getElementById('progress-bar').classList.add('bg-blue-600'); + document.getElementById('progress-percent').textContent = '0%'; + document.getElementById('progress-status').textContent = 'Starting...'; + document.getElementById('progress-message').textContent = 'Initializing...'; + document.getElementById('cancel-btn').classList.remove('hidden'); + document.getElementById('cancel-btn').disabled = false; + document.getElementById('cancel-btn').textContent = 'Cancel Restore'; +} + +function startPolling() { + if (pollInterval) clearInterval(pollInterval); + + pollInterval = setInterval(async () => { + try { + const response = await fetch(`/api/status/${currentJobId}`); + const data = await response.json(); + + if (data.success) { + updateProgress(data); + + if (data.status === 'completed' || data.status === 'error' || data.status === 'cancelled') { + stopPolling(); + document.getElementById('restore-btn').disabled = false; + document.getElementById('restore-btn').textContent = 'Start Restore'; + + if (data.status === 'completed') { + showStatus('Restore completed successfully!', 'success'); + } else if (data.status === 'cancelled') { + showStatus('Restore was cancelled', 'error'); + } else { + showStatus('Restore failed: ' + data.error, 'error'); + } + } + } + } catch (error) { + console.error('Polling error:', error); + } + }, 250); +} + +function stopPolling() { + if (pollInterval) { + clearInterval(pollInterval); + pollInterval = null; + } +} + +async function cancelRestore() { + if (!currentJobId) return; + + if (!confirm('Are you sure you want to cancel the restore?')) { + return; + } + + const btn = document.getElementById('cancel-btn'); + btn.disabled = true; + btn.textContent = 'Cancelling...'; + + try { + const response = await fetch(`/api/cancel/${currentJobId}`, { + method: 'POST' + }); + + const data = await response.json(); + + if (data.success) { + showStatus('Cancellation requested...', 'error'); + } else { + showStatus('Cancel failed: ' + data.error, 'error'); + btn.disabled = false; + btn.textContent = 'Cancel Restore'; + } + } catch (error) { + showStatus('Cancel error: ' + error.message, 'error'); + btn.disabled = false; + btn.textContent = 'Cancel Restore'; + } +} + +function updateProgress(data) { + const bar = document.getElementById('progress-bar'); + const percent = document.getElementById('progress-percent'); + const status = document.getElementById('progress-status'); + const message = document.getElementById('progress-message'); + const elapsed = document.getElementById('progress-elapsed'); + const cancelBtn = document.getElementById('cancel-btn'); + + bar.style.width = data.progress + '%'; + percent.textContent = data.progress + '%'; + + // Update status label + const statusLabels = { + 'starting': 'Starting', + 'downloading': 'Downloading', + 'restoring': 'Restoring', + 'completed': 'Completed', + 'error': 'Error', + 'cancelled': 'Cancelled' + }; + status.textContent = statusLabels[data.status] || data.status; + + // Update color based on status + bar.classList.remove('bg-green-600', 'bg-red-600', 'bg-yellow-600'); + if (data.status === 'completed') { + bar.classList.remove('bg-blue-600'); + bar.classList.add('bg-green-600'); + } else if (data.status === 'error') { + bar.classList.remove('bg-blue-600'); + bar.classList.add('bg-red-600'); + } else if (data.status === 'cancelled') { + bar.classList.remove('bg-blue-600'); + bar.classList.add('bg-yellow-600'); + } + + // Show/hide cancel button based on job status + if (data.status === 'completed' || data.status === 'error' || data.status === 'cancelled') { + cancelBtn.classList.add('hidden'); + } else { + cancelBtn.classList.remove('hidden'); + cancelBtn.disabled = false; + cancelBtn.textContent = 'Cancel Restore'; + } + + message.textContent = data.message || ''; + if (data.elapsed) { + elapsed.textContent = 'Elapsed: ' + data.elapsed; + } +} + +function showStatus(message, type) { + const status = document.getElementById('connection-status'); + status.classList.remove('hidden', 'bg-green-100', 'bg-red-100', 'text-green-800', 'text-red-800'); + + if (type === 'success') { + status.classList.add('bg-green-100', 'text-green-800'); + } else if (type === 'error') { + status.classList.add('bg-red-100', 'text-red-800'); + } + + status.textContent = message; +} + +function hideStatus() { + document.getElementById('connection-status').classList.add('hidden'); +} diff --git a/docker/db-downloader/templates/index.html b/docker/db-downloader/templates/index.html new file mode 100644 index 000000000..2ea9fed98 --- /dev/null +++ b/docker/db-downloader/templates/index.html @@ -0,0 +1,187 @@ + + + + + +Browse and restore database backups from remote server
+No file selected
++ Warning: This will DROP all tables in the selected database before restoring! +
+Connect to browse remote files
+Starting...
+ +{{ rimoLogContent }}
+
+ | Kundennummer | Erstellt am | Betreff | Letztes Update | @@ -16,7 +18,6 @@ Vue.component('AddressTickets', {
|---|---|---|---|
| {{ ticket.customField7 }} | {{ formatDate(ticket.createdAt) }} | {{ ticket.subject }} | {{ formatDate(ticket.updatedAt) }} | @@ -30,7 +31,10 @@ Vue.component('AddressTickets', {
+
+