feat(debug): add Telemetria teszt section to debug page (v0.28.1)

- New GET /api/debug/telemetry endpoint runs full telemetry pipeline on-demand
- GetTelemetryPreview callback added to DebugCallbacks, wired in main.go
- BuildAppTelemetryForDebug() exported wrapper in report/telemetry.go
- Debug page: new collapsible section with per-app table (memory, CPU, log errors/warnings, issues) and raw JSON viewer
- Available regardless of hub configuration

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-23 11:09:06 +01:00
parent 6d9937bdc1
commit 4a6ab4d61c
6 changed files with 161 additions and 3 deletions
+40
View File
@@ -15,6 +15,7 @@ import (
"gitea.dooplex.hu/admin/felhom-controller/internal/backup"
"gitea.dooplex.hu/admin/felhom-controller/internal/monitor"
"gitea.dooplex.hu/admin/felhom-controller/internal/report"
"gitea.dooplex.hu/admin/felhom-controller/internal/stacks"
"gitea.dooplex.hu/admin/felhom-controller/internal/system"
)
@@ -27,6 +28,7 @@ type DebugCallbacks struct {
TriggerSetupMode func() error
HubConnectivityTest func() (statusCode int, latencyMs int64, err error)
GiteaConnectivityTest func() (statusCode int, latencyMs int64, err error)
GetTelemetryPreview func() ([]report.AppTelemetry, error)
}
// debugPageHandler renders the debug dashboard page.
@@ -80,6 +82,10 @@ func (s *Server) handleDebugAPI(w http.ResponseWriter, r *http.Request) {
case subpath == "gitea/test-connectivity" && r.Method == http.MethodPost:
s.debugGiteaConnectivity(w, r)
// Section: Telemetry testing
case subpath == "telemetry" && r.Method == http.MethodGet:
s.debugTelemetry(w, r)
// Section 6: Self-update
case subpath == "selfupdate/dry-run" && r.Method == http.MethodPost:
s.debugSelfUpdateDryRun(w, r)
@@ -538,6 +544,40 @@ func (s *Server) debugGiteaConnectivity(w http.ResponseWriter, r *http.Request)
fmt.Sprintf("Gitea elérhető (HTTP %d, %dms)", statusCode, latency), data)
}
// ── Section: Telemetry testing ───────────────────────────────────────
func (s *Server) debugTelemetry(w http.ResponseWriter, r *http.Request) {
if s.debugCallbacks == nil || s.debugCallbacks.GetTelemetryPreview == nil {
writeDebugJSON(w, http.StatusNotImplemented, false, "Nem bekötött", nil)
return
}
start := time.Now()
telemetry, err := s.debugCallbacks.GetTelemetryPreview()
latency := time.Since(start).Milliseconds()
if err != nil {
writeDebugJSON(w, http.StatusOK, false, err.Error(), map[string]interface{}{"latency_ms": latency})
return
}
totalErrors := 0
totalWarnings := 0
for _, app := range telemetry {
totalErrors += app.LogErrors
totalWarnings += app.LogWarnings
}
writeDebugJSON(w, http.StatusOK, true,
fmt.Sprintf("Telemetria összegyűjtve: %d app, %d hiba, %d figyelmeztetés (%dms)",
len(telemetry), totalErrors, totalWarnings, latency),
map[string]interface{}{
"latency_ms": latency,
"app_count": len(telemetry),
"total_errors": totalErrors,
"total_warnings": totalWarnings,
"app_telemetry": telemetry,
})
}
// ── Section 6: Self-update ──────────────────────────────────────────
func (s *Server) debugSelfUpdateDryRun(w http.ResponseWriter, r *http.Request) {
@@ -127,6 +127,22 @@
</div>
</div>
<!-- Section: Telemetry Testing -->
<div class="card debug-section" id="section-telemetry">
<div class="card-header debug-section-header" onclick="toggleSection('telemetry')">
<h3>Telemetria teszt</h3>
<span class="section-toggle"></span>
</div>
<div class="card-body debug-section-body" style="display:none">
<div id="telemetry-status"><span class="text-muted">Kattintson a gombra a telemetria futtatásához.</span></div>
<div class="debug-actions">
<button class="btn btn-primary btn-sm" id="btn-telemetry-run" data-label="Telemetria futtatása" onclick="runTelemetryTest()">Telemetria futtatása</button>
<span class="debug-result" id="btn-telemetry-run-result"></span>
</div>
<div id="telemetry-detail" style="display:none; margin-top:1rem;"></div>
</div>
</div>
<!-- Section 6: Self-Update Testing -->
<div class="card debug-section" id="section-selfupdate">
<div class="card-header debug-section-header" onclick="toggleSection('selfupdate')">
@@ -266,6 +282,7 @@ function loadSectionData(id) {
case 'backup': loadBackupStatus(); break;
case 'storage': loadWatchdogStatus(); break;
case 'hub': loadHubStatus(); break;
case 'telemetry': break; // no auto-load, user triggers manually
case 'selfupdate': loadSelfUpdateStatus(); break;
case 'dr': loadDRStatus(); break;
case 'logs': initLogViewer(); break;
@@ -597,6 +614,85 @@ function clearLogDisplay() {
lastLogTimestamp = '';
}
// ── Telemetry test ──
function runTelemetryTest() {
var btn = document.getElementById('btn-telemetry-run');
var result = document.getElementById('btn-telemetry-run-result');
var detail = document.getElementById('telemetry-detail');
btn.disabled = true;
btn.textContent = 'Folyamatban...';
result.className = 'debug-result';
result.textContent = '';
detail.style.display = 'none';
fetch('/api/debug/telemetry', {headers: csrfHeaders()}).then(function(r){return r.json()}).then(function(data) {
if (data.ok) {
result.className = 'debug-result debug-result-ok';
result.textContent = data.message;
if (data.data && data.data.app_telemetry) {
renderTelemetryDetail(data.data);
}
} else {
result.className = 'debug-result debug-result-error';
result.textContent = data.error || 'Hiba';
}
}).catch(function(e) {
result.className = 'debug-result debug-result-error';
result.textContent = 'Hálózati hiba: ' + e.message;
}).finally(function() {
btn.disabled = false;
btn.textContent = btn.dataset.label;
});
}
function renderTelemetryDetail(data) {
var detail = document.getElementById('telemetry-detail');
var apps = data.app_telemetry || [];
if (apps.length === 0) {
detail.innerHTML = '<span class="text-muted">Nincs telepített alkalmazás vagy nincs mérési adat.</span>';
detail.style.display = 'block';
return;
}
var html = '<table class="debug-table" style="width:100%;font-size:.85rem">';
html += '<thead><tr><th>Alkalmazás</th><th>Konténerek</th><th>Memória (jelen.)</th><th>Memória (átlag)</th><th>Memória (csúcs)</th><th>CPU (átlag)</th><th>Katalógus limit</th><th>Hibák</th><th>Figyelmeztetések</th></tr></thead><tbody>';
for (var i = 0; i < apps.length; i++) {
var a = apps[i];
var errorClass = a.log_errors > 0 ? ' style="color:var(--red);font-weight:600"' : '';
var warnClass = a.log_warnings > 0 ? ' style="color:var(--yellow);font-weight:600"' : '';
html += '<tr>';
html += '<td><strong>' + escapeHtml(a.display_name || a.app_name) + '</strong></td>';
html += '<td class="mono" style="font-size:.8rem">' + (a.containers||[]).map(escapeHtml).join(', ') + '</td>';
html += '<td>' + (a.memory_current_mb||0).toFixed(1) + ' MB</td>';
html += '<td>' + (a.memory_avg_mb||0).toFixed(1) + ' MB</td>';
html += '<td>' + (a.memory_peak_mb||0).toFixed(1) + ' MB</td>';
html += '<td>' + (a.cpu_avg_percent||0).toFixed(1) + '%</td>';
html += '<td class="mono">' + escapeHtml(a.catalog_limit || '-') + '</td>';
html += '<td' + errorClass + '>' + (a.log_errors||0) + '</td>';
html += '<td' + warnClass + '>' + (a.log_warnings||0) + '</td>';
html += '</tr>';
if (a.issues && a.issues.length > 0) {
for (var j = 0; j < a.issues.length; j++) {
var issue = a.issues[j];
var sevColor = issue.severity === 'error' ? 'var(--red)' : 'var(--yellow)';
html += '<tr style="font-size:.8rem;opacity:.85"><td></td>';
html += '<td colspan="6" style="padding-left:1.5rem"><span style="color:' + sevColor + '">' + escapeHtml(issue.severity.toUpperCase()) + '</span> ' + escapeHtml(issue.message) + '</td>';
html += '<td colspan="2">×' + (issue.count||0) + '</td></tr>';
}
}
}
html += '</tbody></table>';
html += '<details style="margin-top:1rem"><summary class="text-muted" style="cursor:pointer;font-size:.85rem">Nyers JSON</summary>';
html += '<pre class="mono" style="font-size:.75rem;max-height:400px;overflow:auto;padding:.5rem;background:rgba(0,0,0,.3);border-radius:.25rem;margin-top:.5rem">' + escapeHtml(JSON.stringify(apps, null, 2)) + '</pre>';
html += '</details>';
detail.innerHTML = html;
detail.style.display = 'block';
}
// ── Helpers ──
function fmtTime(ts) {
if (!ts) return '-';