diff --git a/js/nc_connector_edit_v2.js b/js/nc_connector_edit_v2.js index 31ae999..e9a0ca8 100644 --- a/js/nc_connector_edit_v2.js +++ b/js/nc_connector_edit_v2.js @@ -1,6 +1,134 @@ (function () { 'use strict'; + var statusEndpoint = 'plugins/bratonien_tools/nc-connector-status.php'; + + function ensureLiveStatus(detail, actions) { + var node = detail.querySelector('[data-nc-run-live]'); + if (node) return node; + + node = document.createElement('div'); + node.setAttribute('data-nc-run-live', '1'); + node.className = 'bratonien-base-note'; + node.style.marginTop = '.6rem'; + node.hidden = true; + actions.parentNode.insertBefore(node, actions.nextSibling); + return node; + } + + function renderLiveStatus(node, data) { + var state = String(data && data.state || ''); + var message = String(data && data.message || ''); + var detail = String(data && data.error_detail || ''); + + node.hidden = false; + node.innerHTML = ''; + + var strong = document.createElement('strong'); + if (state === 'queued') strong.textContent = 'Angefordert: '; + else if (state === 'running') strong.textContent = 'Läuft: '; + else if (state === 'ok' || state === 'success') strong.textContent = 'Erfolgreich: '; + else if (state === 'error') strong.textContent = 'Fehler: '; + else strong.textContent = 'Status: '; + node.appendChild(strong); + node.appendChild(document.createTextNode(message || state || 'Status wird ermittelt …')); + + if (detail) { + var details = document.createElement('details'); + details.style.marginTop = '.35rem'; + var summary = document.createElement('summary'); + summary.textContent = 'Technische Laufzeitdetails'; + var pre = document.createElement('pre'); + pre.style.whiteSpace = 'pre-wrap'; + pre.style.wordBreak = 'break-word'; + pre.textContent = detail; + details.appendChild(summary); + details.appendChild(pre); + node.appendChild(details); + } + } + + function pollConnection(connectionId, node, button) { + var attempts = 0; + var maxAttempts = 180; + var timer = null; + + function stop() { + if (timer) window.clearInterval(timer); + timer = null; + if (button) { + button.disabled = false; + button.textContent = 'Jetzt abgleichen'; + } + } + + function poll() { + attempts += 1; + fetch(statusEndpoint + '?connection_id=' + encodeURIComponent(connectionId) + '&_=' + Date.now(), { + credentials: 'same-origin', + cache: 'no-store', + headers: {'Accept': 'application/json'} + }) + .then(function (response) { + if (!response.ok) throw new Error('HTTP ' + response.status); + return response.json(); + }) + .then(function (data) { + renderLiveStatus(node, data); + var state = String(data && data.state || ''); + if (state === 'ok' || state === 'success' || state === 'error' || attempts >= maxAttempts) stop(); + }) + .catch(function (error) { + if (attempts >= maxAttempts) { + renderLiveStatus(node, {state: 'error', message: 'Status konnte nicht gelesen werden.', error_detail: error.message}); + stop(); + } + }); + } + + poll(); + timer = window.setInterval(poll, 1000); + } + + function bindRunNow(form, detail, liveNode) { + if (form.dataset.ncRunBound === '1') return; + form.dataset.ncRunBound = '1'; + + form.addEventListener('submit', function (event) { + event.preventDefault(); + detail.open = true; + + var button = form.querySelector('button[value="nc_connector_run_now"]'); + var connectionInput = form.querySelector('input[name="connection_id"]'); + if (!connectionInput) return; + + if (button) { + button.disabled = true; + button.textContent = 'Abgleich wird gestartet …'; + } + renderLiveStatus(liveNode, {state: 'queued', message: 'Abgleich wird angefordert …'}); + + fetch(form.action || window.location.href, { + method: 'POST', + credentials: 'same-origin', + cache: 'no-store', + body: new FormData(form) + }) + .then(function (response) { + if (!response.ok) throw new Error('HTTP ' + response.status); + renderLiveStatus(liveNode, {state: 'queued', message: 'Abgleich wurde angefordert.'}); + pollConnection(connectionInput.value, liveNode, button); + }) + .catch(function (error) { + renderLiveStatus(liveNode, {state: 'error', message: 'Abgleich konnte nicht angefordert werden.', error_detail: error.message}); + if (button) { + button.disabled = false; + button.textContent = 'Jetzt abgleichen'; + } + }); + }); + } + function restoreRunNowButtons() { var section = document.getElementById('nc-connector'); if (!section) return; @@ -18,36 +146,42 @@ var details = connectionCard.querySelectorAll(':scope > details'); details.forEach(function (detail) { var actions = detail.querySelector('.bratonien-actions'); - if (!actions || actions.querySelector('[value="nc_connector_run_now"]')) return; + if (!actions) return; var connectionInput = detail.querySelector('input[name="connection_id"]'); var tokenInput = detail.querySelector('input[name="pwg_token"]'); if (!connectionInput || !tokenInput) return; - var form = document.createElement('form'); - form.method = 'post'; + var form = actions.querySelector('form[data-nc-run-now]'); + if (!form) { + form = document.createElement('form'); + form.method = 'post'; + form.setAttribute('data-nc-run-now', '1'); - var token = document.createElement('input'); - token.type = 'hidden'; - token.name = 'pwg_token'; - token.value = tokenInput.value; - form.appendChild(token); + var token = document.createElement('input'); + token.type = 'hidden'; + token.name = 'pwg_token'; + token.value = tokenInput.value; + form.appendChild(token); - var connection = document.createElement('input'); - connection.type = 'hidden'; - connection.name = 'connection_id'; - connection.value = connectionInput.value; - form.appendChild(connection); + var connection = document.createElement('input'); + connection.type = 'hidden'; + connection.name = 'connection_id'; + connection.value = connectionInput.value; + form.appendChild(connection); - var button = document.createElement('button'); - button.className = 'buttonLike'; - button.type = 'submit'; - button.name = 'bratonien_tool'; - button.value = 'nc_connector_run_now'; - button.textContent = 'Jetzt abgleichen'; - form.appendChild(button); + var button = document.createElement('button'); + button.className = 'buttonLike'; + button.type = 'submit'; + button.name = 'bratonien_tool'; + button.value = 'nc_connector_run_now'; + button.textContent = 'Jetzt abgleichen'; + form.appendChild(button); - actions.insertBefore(form, actions.firstChild); + actions.insertBefore(form, actions.firstChild); + } + + bindRunNow(form, detail, ensureLiveStatus(detail, actions)); }); } diff --git a/main.inc.php b/main.inc.php index 18547ff..ed2477f 100644 --- a/main.inc.php +++ b/main.inc.php @@ -1,7 +1,7 @@ 'idle', @@ -41,11 +42,16 @@ $latest = array( 'route_timestamp'=>0, 'route_time_label'=>'Nicht verfügbar', 'route_detail'=>'', + 'connection_id'=>$requested_connection_id, ); if (is_dir($dir)) { - foreach (glob($dir.'/connection-*.json') ?: array() as $file) + $files = $requested_connection_id > 0 + ? array($dir.'/connection-'.$requested_connection_id.'.json') + : (glob($dir.'/connection-*.json') ?: array()); + + foreach ($files as $file) { if (!is_readable($file)) { @@ -63,60 +69,58 @@ if (is_dir($dir)) } } - $route_file = $dir.'/route-status.json'; - if (is_readable($route_file)) + if ($requested_connection_id === 0) { - $route = json_decode((string)@file_get_contents($route_file), true); - if (is_array($route)) + $route_file = $dir.'/route-status.json'; + if (is_readable($route_file)) { - $route_name = (string)($route['route'] ?? ''); - $route_timestamp = (int)($route['timestamp'] ?? 0); - $route_label = (string)($route['label'] ?? ''); + $route = json_decode((string)@file_get_contents($route_file), true); + if (is_array($route)) + { + $route_name = (string)($route['route'] ?? ''); + $route_timestamp = (int)($route['timestamp'] ?? 0); + $route_label = (string)($route['label'] ?? ''); - if ($route_name === 'webdav') - { - $route_label = 'WEBDAV PRIMÄR'; - } - elseif ($route_name === 'legacy_fallback') - { - $route_label = 'LEGACY-FALLBACK AKTIV'; - } - elseif ($route_name === 'failed') - { - $route_label = 'FEHLER - KEIN ERFOLGREICHER DATENWEG'; - } - elseif ($route_label === '') - { - $route_label = 'UNBEKANNTER DATENWEG'; - } + if ($route_name === 'webdav') + { + $route_label = 'WEBDAV PRIMÄR'; + } + elseif ($route_name === 'legacy_fallback') + { + $route_label = 'LEGACY-FALLBACK AKTIV'; + } + elseif ($route_name === 'failed') + { + $route_label = 'FEHLER - KEIN ERFOLGREICHER DATENWEG'; + } + elseif ($route_label === '') + { + $route_label = 'UNBEKANNTER DATENWEG'; + } - $route_detail = trim((string)($route['detail'] ?? '')); - $latest['route'] = $route_name; - $latest['route_label'] = $route_label; - $latest['route_timestamp'] = $route_timestamp; - $latest['route_time_label'] = $route_timestamp > 0 ? date('d.m.Y H:i:s', $route_timestamp) : 'Nicht verfügbar'; - $latest['route_detail'] = $route_detail; + $route_detail = trim((string)($route['detail'] ?? '')); + $latest['route'] = $route_name; + $latest['route_label'] = $route_label; + $latest['route_timestamp'] = $route_timestamp; + $latest['route_time_label'] = $route_timestamp > 0 ? date('d.m.Y H:i:s', $route_timestamp) : 'Nicht verfügbar'; + $latest['route_detail'] = $route_detail; - $base_message = trim((string)($latest['message'] ?? '')); - $message_parts = array($route_label); - if ($route_name !== 'webdav' && $route_detail !== '') - { - $message_parts[] = $route_detail; + $base_message = trim((string)($latest['message'] ?? '')); + $message_parts = array($route_label); + if ($route_name !== 'webdav' && $route_detail !== '') + { + $message_parts[] = $route_detail; + } + if ($base_message !== '') + { + $message_parts[] = $base_message; + } + $latest['message'] = implode(' · ', $message_parts); } - if ($base_message !== '') - { - $message_parts[] = $base_message; - } - $latest['message'] = implode(' · ', $message_parts); } } } -if ((int)$latest['timestamp'] > 0) -{ - $latest['last_run_label'] = date('d.m.Y H:i:s', (int)$latest['timestamp']); -} - $system_file = BRATONIEN_TOOLS_PATH.'include/nc_connector_system.inc.php'; if (is_readable($system_file)) { @@ -129,4 +133,27 @@ if (is_readable($system_file)) } } +$scheduler_file = PHPWG_ROOT_PATH.'_data/bratonien-tools/nc-connector-scheduler/state.json'; +if ($requested_connection_id > 0 && is_readable($scheduler_file)) +{ + $scheduler = json_decode((string)@file_get_contents($scheduler_file), true); + if ( + is_array($scheduler) + && (int)($scheduler['connection_id'] ?? 0) === $requested_connection_id + && in_array((string)($scheduler['state'] ?? ''), array('queued','running'), true) + && (int)($scheduler['timestamp'] ?? 0) >= (int)($latest['timestamp'] ?? 0) + ) + { + $latest['state'] = (string)$scheduler['state']; + $latest['message'] = (string)($scheduler['message'] ?? 'NC-Abgleich läuft.'); + $latest['timestamp'] = (int)($scheduler['timestamp'] ?? time()); + $latest['error_detail'] = ''; + } +} + +if ((int)$latest['timestamp'] > 0) +{ + $latest['last_run_label'] = date('d.m.Y H:i:s', (int)$latest['timestamp']); +} + echo json_encode($latest, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); diff --git a/runtime/sync-webdav.sh b/runtime/sync-webdav.sh index 252f0a6..3533033 100644 --- a/runtime/sync-webdav.sh +++ b/runtime/sync-webdav.sh @@ -78,7 +78,9 @@ done < "$WEBDAV_ROOTS_FILE" WEBDAV_CONNECT_IP="$(php "$SCRIPT_DIR/lib/resolve-nextcloud-target.php" "$WEBDAV_BASE_URL")" [[ -n "$WEBDAV_CONNECT_IP" ]] || { write_status error "Nextcloud-Zieladresse konnte nicht ermittelt werden"; exit 1; } -python3 "$SCRIPT_DIR/lib/build_webdav_placeholder_source.py" \ +PLACEHOLDER_OUTPUT="" +PLACEHOLDER_EXIT=0 +if PLACEHOLDER_OUTPUT="$(python3 "$SCRIPT_DIR/lib/build_webdav_placeholder_source.py" \ --base-url "$WEBDAV_BASE_URL" \ --connect-ip "$WEBDAV_CONNECT_IP" \ --user "$WEBDAV_USER" \ @@ -86,12 +88,42 @@ python3 "$SCRIPT_DIR/lib/build_webdav_placeholder_source.py" \ "${ROOT_ARGS[@]}" \ --source-dir "$WEBDAV_SOURCE_DIR" \ --manifest "$MANIFEST" \ - --mapping "$WEBDAV_MAPPING_FILE" + --mapping "$WEBDAV_MAPPING_FILE" 2>&1)"; then + PLACEHOLDER_EXIT=0 +else + PLACEHOLDER_EXIT=$? +fi +[[ -z "$PLACEHOLDER_OUTPUT" ]] || printf '%s\n' "$PLACEHOLDER_OUTPUT" +if [[ "$PLACEHOLDER_EXIT" -ne 0 ]]; then + DETAIL="Exit-Code: $PLACEHOLDER_EXIT" + if [[ -n "$PLACEHOLDER_OUTPUT" ]]; then + DETAIL+="; Ausgabe: $(printf '%s\n' "$PLACEHOLDER_OUTPUT" | tail -n 20 | tr '\n' ' ' | sed 's/[[:space:]]\+/ /g; s/^[[:space:]]//; s/[[:space:]]$//')" + fi + trap - ERR + write_status error "WebDAV-Shadow-Tree fehlgeschlagen" "$DETAIL" + exit "$PLACEHOLDER_EXIT" +fi -python3 "$SCRIPT_DIR/lib/shadow_tree.py" \ +SHADOW_OUTPUT="" +SHADOW_EXIT=0 +if SHADOW_OUTPUT="$(python3 "$SCRIPT_DIR/lib/shadow_tree.py" \ --manifest "$MANIFEST" \ --destination "$GALLERY_ROOT" \ - --state "$SHADOW_MAP_FILE" + --state "$SHADOW_MAP_FILE" 2>&1)"; then + SHADOW_EXIT=0 +else + SHADOW_EXIT=$? +fi +[[ -z "$SHADOW_OUTPUT" ]] || printf '%s\n' "$SHADOW_OUTPUT" +if [[ "$SHADOW_EXIT" -ne 0 ]]; then + DETAIL="Exit-Code: $SHADOW_EXIT" + if [[ -n "$SHADOW_OUTPUT" ]]; then + DETAIL+="; Ausgabe: $(printf '%s\n' "$SHADOW_OUTPUT" | tail -n 20 | tr '\n' ' ' | sed 's/[[:space:]]\+/ /g; s/^[[:space:]]//; s/[[:space:]]$//')" + fi + trap - ERR + write_status error "WebDAV-Shadow-Tree fehlgeschlagen" "$DETAIL" + exit "$SHADOW_EXIT" +fi trap - ERR