feat: per-app telemetry reset button on app detail page
Adds "Telemetria törlése" button that deletes all telemetry records and known issues for a specific app. Useful after major app updates when old data is no longer representative. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,5 +1,12 @@
|
||||
# Felhom Hub — Changelog
|
||||
|
||||
## v0.4.1 (2026-02-23)
|
||||
|
||||
### Added
|
||||
- **Per-app telemetry reset** (`store/telemetry.go`, `web/apps.go`) — New "Telemetria törlése" button on the app detail page that deletes all telemetry records and known issues for the selected app. Useful after major app updates when old data is no longer representative. Includes confirmation dialog and flash notification.
|
||||
- **`DeleteAppTelemetry()`** and **`DeleteAppIssues()`** store methods (`store/telemetry.go`) — Delete all telemetry/issue rows for a specific app_name.
|
||||
- **`POST /apps/{name}/reset-telemetry`** route (`web/server.go`) — CSRF-protected endpoint that triggers the reset and redirects back with flash message.
|
||||
|
||||
## v0.4.0 (2026-02-23)
|
||||
|
||||
**App Telemetry & Analytics Dashboard**
|
||||
|
||||
@@ -446,3 +446,21 @@ func (s *Store) PruneStaleIssues(notSeenSince time.Time) (int64, error) {
|
||||
}
|
||||
return res.RowsAffected()
|
||||
}
|
||||
|
||||
// DeleteAppTelemetry removes all telemetry records for a specific app.
|
||||
func (s *Store) DeleteAppTelemetry(appName string) (int64, error) {
|
||||
res, err := s.db.Exec("DELETE FROM app_telemetry WHERE app_name = ?", appName)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return res.RowsAffected()
|
||||
}
|
||||
|
||||
// DeleteAppIssues removes all known-issue records for a specific app.
|
||||
func (s *Store) DeleteAppIssues(appName string) (int64, error) {
|
||||
res, err := s.db.Exec("DELETE FROM app_log_issues WHERE app_name = ?", appName)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return res.RowsAffected()
|
||||
}
|
||||
|
||||
@@ -99,12 +99,37 @@ func (s *Server) handleAppDetail(w http.ResponseWriter, r *http.Request, appName
|
||||
"SuggestedLimit": suggestedLimit,
|
||||
"Period": period,
|
||||
"CSRFToken": csrfToken,
|
||||
"Flash": r.URL.Query().Get("flash"),
|
||||
}
|
||||
if err := s.templates.ExecuteTemplate(w, "app_detail.html", data); err != nil {
|
||||
s.logger.Printf("[ERROR] app_detail.html template: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// handleResetAppTelemetry deletes all telemetry and issues for an app, then redirects back.
|
||||
func (s *Server) handleResetAppTelemetry(w http.ResponseWriter, r *http.Request, appName string) {
|
||||
telRows, err := s.store.DeleteAppTelemetry(appName)
|
||||
if err != nil {
|
||||
s.logger.Printf("[ERROR] Failed to reset telemetry for %s: %v", appName, err)
|
||||
http.Error(w, "Internal error", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
issueRows, err := s.store.DeleteAppIssues(appName)
|
||||
if err != nil {
|
||||
s.logger.Printf("[ERROR] Failed to reset issues for %s: %v", appName, err)
|
||||
http.Error(w, "Internal error", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
s.logger.Printf("[INFO] Telemetry reset for %s: %d telemetry rows, %d issues deleted", appName, telRows, issueRows)
|
||||
|
||||
period := r.URL.Query().Get("period")
|
||||
target := "/apps/" + appName + "?flash=telemetry_reset"
|
||||
if period != "" {
|
||||
target += "&period=" + period
|
||||
}
|
||||
http.Redirect(w, r, target, http.StatusSeeOther)
|
||||
}
|
||||
|
||||
// parsePeriod converts a period string to a time.Time cutoff.
|
||||
func parsePeriod(s string, defaultDur time.Duration) time.Time {
|
||||
switch s {
|
||||
|
||||
@@ -140,6 +140,14 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
w.Write(chartJS)
|
||||
case path == "/apps" || path == "/apps/":
|
||||
s.handleApps(w, r)
|
||||
case strings.HasPrefix(path, "/apps/") && strings.HasSuffix(path, "/reset-telemetry"):
|
||||
appName := strings.TrimPrefix(path, "/apps/")
|
||||
appName = strings.TrimSuffix(appName, "/reset-telemetry")
|
||||
if r.Method == http.MethodPost {
|
||||
s.handleResetAppTelemetry(w, r, appName)
|
||||
} else {
|
||||
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
||||
}
|
||||
case strings.HasPrefix(path, "/apps/"):
|
||||
appName := strings.TrimPrefix(path, "/apps/")
|
||||
s.handleAppDetail(w, r, appName)
|
||||
|
||||
@@ -27,9 +27,20 @@
|
||||
<a href="?period=30d" class="period-btn{{if eq .Period "30d"}} active{{end}}">30 nap</a>
|
||||
</div>
|
||||
|
||||
{{if eq .Flash "telemetry_reset"}}
|
||||
<div class="flash flash-success" style="margin-top: 1rem;">Telemetria sikeresen törölve.</div>
|
||||
{{end}}
|
||||
|
||||
<!-- Overview card -->
|
||||
<section class="card">
|
||||
<h2>{{if .Summary}}{{if .Summary.DisplayName}}{{.Summary.DisplayName}}{{else}}{{.AppName}}{{end}}{{else}}{{.AppName}}{{end}}</h2>
|
||||
<div style="display: flex; justify-content: space-between; align-items: center; flex-wrap: wrap; gap: 0.5rem;">
|
||||
<h2 style="margin: 0;">{{if .Summary}}{{if .Summary.DisplayName}}{{.Summary.DisplayName}}{{else}}{{.AppName}}{{end}}{{else}}{{.AppName}}{{end}}</h2>
|
||||
<form method="POST" action="/apps/{{.AppName}}/reset-telemetry{{if .Period}}?period={{.Period}}{{end}}"
|
||||
onsubmit="return confirm('Biztosan törlöd a(z) {{.AppName}} összes telemetriai adatát? Ez nem vonható vissza.');">
|
||||
<input type="hidden" name="csrf_token" value="{{.CSRFToken}}">
|
||||
<button type="submit" class="btn btn-sm btn-danger">Telemetria törlése</button>
|
||||
</form>
|
||||
</div>
|
||||
<div class="info-grid">
|
||||
<div class="info-item">
|
||||
<span class="label">App neve</span>
|
||||
|
||||
Reference in New Issue
Block a user