feat(hub): Configuration page, asset seedOrUpdate, English UI
- Add Configuration page with "Refresh Assets" button - Replace seedIfEmpty with seedOrUpdate (SHA-256 compare on startup) - Translate all Hungarian text on Apps pages to English - Add Configuration tab to all template navigation - Expand isAssetFile to match favicon patterns - Add felhom-logo.svg to website assets for the pipeline Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,5 +1,20 @@
|
||||
# Felhom Hub — Changelog
|
||||
|
||||
## v0.5.0 (2026-02-25)
|
||||
|
||||
### Added
|
||||
- **Configuration page** (`GET /configuration`) — New "Configuration" tab in the web UI with asset management controls. Displays asset file count, manifest generation timestamp, and a "Refresh Assets from Image" button.
|
||||
- **Manual asset re-seed** (`POST /configuration`, action=`refresh_assets`) — Re-reads the baked-in seed directory, compares SHA-256 checksums with PVC assets, and updates changed files. Rebuilds the manifest afterward. Controllers pick up changes on their next daily sync.
|
||||
- **`ReSeed()` method** (`internal/assets/assets.go`) — Public method for triggering asset re-seed + manifest rebuild from the web UI.
|
||||
|
||||
### Changed
|
||||
- **Asset seeding: `seedIfEmpty()` → `seedOrUpdate()`** (`internal/assets/assets.go`) — On startup the Hub now compares SHA-256 checksums between the image seed directory and the PVC, updating any changed files instead of only seeding into an empty directory. This means redeploying the Hub image with updated assets automatically propagates them without PVC deletion.
|
||||
- **`isAssetFile()` expanded** — Now also matches `*-favicon.svg` and `*-favicon.ico` patterns, allowing branding assets like `felhom-favicon.svg` in the manifest.
|
||||
- **`RebuildManifest()` refactored** — Internal logic extracted to `rebuildManifestLocked()` for reuse by `ReSeed()`.
|
||||
- **Web Server struct** — Added `assetsMgr` field and `SetAssetManager()` method. Wired in `main.go`.
|
||||
- **All templates translated to English** — The "Alkalmazások" nav link and telemetry pages (apps.html, app_detail.html, customer_unified.html telemetry section) are now in English, consistent with the rest of the Hub UI.
|
||||
- **Navigation updated** — All templates now show four tabs: Dashboard, Customers, Apps, Configuration.
|
||||
|
||||
## v0.4.1 (2026-02-23)
|
||||
|
||||
### Added
|
||||
|
||||
+5
-3
@@ -4,7 +4,7 @@
|
||||
|
||||
A lightweight Go service that receives periodic reports and structured events from felhom-controller instances, stores them in SQLite, and provides a web dashboard for fleet monitoring. Also serves as the infrastructure backup store for disaster recovery, event-based dead man's switch monitoring, and notification dispatch.
|
||||
|
||||
**Current version: v0.4.0**
|
||||
**Current version: v0.5.0**
|
||||
|
||||
---
|
||||
|
||||
@@ -237,7 +237,9 @@ Retention: configurable (default 90 days), daily prune at 04:30 Budapest time.
|
||||
|
||||
### PVC Asset Storage
|
||||
|
||||
App assets (logos, screenshots) are stored on the PVC at `<dataDir>/assets/`. On first run (empty directory), assets are seeded from `/usr/share/felhom/assets-seed/` (baked into the Docker image during build). This means assets survive container rebuilds but fresh deploys get a full set from the image seed.
|
||||
App assets (logos, screenshots, branding) are stored on the PVC at `<dataDir>/assets/`. On every startup, the Hub compares SHA-256 checksums between the image seed (`/usr/share/felhom/assets-seed/`) and the PVC, updating any changed files. This means redeploying the Hub image with updated assets automatically propagates changes without PVC deletion.
|
||||
|
||||
A manual "Refresh Assets from Image" button is available on the **Configuration** page (`/configuration`) for triggering a re-seed + manifest rebuild on demand.
|
||||
|
||||
## Configuration
|
||||
|
||||
@@ -308,7 +310,7 @@ The Dockerfile includes `COPY assets/ /usr/share/felhom/assets-seed/` which bake
|
||||
| **Report/event prune** | Daily 04:30 Budapest | Deletes reports and events older than retention period (default 90 days) |
|
||||
| **Registry version check** | Every 30min | Checks Gitea registry for new controller image tags |
|
||||
| **Template refresh** | Every 1h | Fetches latest `controller.yaml.example` from Gitea |
|
||||
| **Asset seeding** | On startup | Seeds PVC assets from Docker image if `<dataDir>/assets/` is empty |
|
||||
| **Asset seeding** | On startup | Compares SHA-256 checksums and updates changed assets from Docker image seed |
|
||||
|
||||
## Internal Packages
|
||||
|
||||
|
||||
@@ -143,6 +143,7 @@ func main() {
|
||||
|
||||
webServer := web.New(dataStore, cfg.Auth.PasswordHash, cfg.API.ReportAPIKey, Version, staleThreshold, logger)
|
||||
webServer.SetTemplateFetcher(templateFetcher)
|
||||
webServer.SetAssetManager(assetsMgr)
|
||||
|
||||
// Build HTTP mux
|
||||
mux := http.NewServeMux()
|
||||
|
||||
@@ -39,7 +39,7 @@ type Manager struct {
|
||||
logger *log.Logger
|
||||
}
|
||||
|
||||
// New creates a Manager, seeds assets from the image if the PVC dir is empty,
|
||||
// New creates a Manager, syncs assets from the image seed directory,
|
||||
// and builds the initial manifest.
|
||||
func New(assetsDir, seedDir string, logger *log.Logger) *Manager {
|
||||
m := &Manager{
|
||||
@@ -52,7 +52,7 @@ func New(assetsDir, seedDir string, logger *log.Logger) *Manager {
|
||||
logger.Printf("[ERROR] Failed to create assets dir %s: %v", assetsDir, err)
|
||||
}
|
||||
|
||||
m.seedIfEmpty()
|
||||
m.seedOrUpdate()
|
||||
|
||||
if err := m.RebuildManifest(); err != nil {
|
||||
logger.Printf("[ERROR] Failed to build asset manifest: %v", err)
|
||||
@@ -61,42 +61,47 @@ func New(assetsDir, seedDir string, logger *log.Logger) *Manager {
|
||||
return m
|
||||
}
|
||||
|
||||
// seedIfEmpty copies assets from the seed directory to the PVC directory
|
||||
// if the PVC directory has no asset files.
|
||||
func (m *Manager) seedIfEmpty() {
|
||||
entries, err := os.ReadDir(m.assetsDir)
|
||||
if err != nil {
|
||||
m.logger.Printf("[WARN] Cannot read assets dir: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
// Check if there are any asset files (not just directories or manifest.json)
|
||||
hasAssets := false
|
||||
for _, e := range entries {
|
||||
if !e.IsDir() && isAssetFile(e.Name()) {
|
||||
hasAssets = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if hasAssets {
|
||||
m.logger.Printf("[INFO] Assets directory has files, skipping seed")
|
||||
return
|
||||
}
|
||||
|
||||
// Seed from image
|
||||
// seedOrUpdate copies new or changed assets from the seed directory to the PVC directory.
|
||||
// On first run (empty PVC) this seeds all files. On subsequent runs it updates only files
|
||||
// whose SHA-256 checksums differ, ensuring redeployed images propagate asset changes.
|
||||
func (m *Manager) seedOrUpdate() {
|
||||
seedEntries, err := os.ReadDir(m.seedDir)
|
||||
if err != nil {
|
||||
m.logger.Printf("[WARN] Cannot read seed dir %s: %v (no assets will be available until uploaded)", m.seedDir, err)
|
||||
return
|
||||
}
|
||||
|
||||
copied := 0
|
||||
// Build hash map of existing PVC assets for comparison
|
||||
existingHashes := make(map[string]string)
|
||||
pvcEntries, _ := os.ReadDir(m.assetsDir)
|
||||
for _, e := range pvcEntries {
|
||||
if e.IsDir() || !isAssetFile(e.Name()) {
|
||||
continue
|
||||
}
|
||||
h, err := fileSHA256(filepath.Join(m.assetsDir, e.Name()))
|
||||
if err == nil {
|
||||
existingHashes[e.Name()] = h
|
||||
}
|
||||
}
|
||||
|
||||
copied, skipped := 0, 0
|
||||
for _, e := range seedEntries {
|
||||
if e.IsDir() || !isAssetFile(e.Name()) {
|
||||
continue
|
||||
}
|
||||
src := filepath.Join(m.seedDir, e.Name())
|
||||
|
||||
// Compare checksums — skip if unchanged
|
||||
seedHash, err := fileSHA256(src)
|
||||
if err != nil {
|
||||
m.logger.Printf("[WARN] Cannot hash seed asset %s: %v", e.Name(), err)
|
||||
continue
|
||||
}
|
||||
if existingHashes[e.Name()] == seedHash {
|
||||
skipped++
|
||||
continue
|
||||
}
|
||||
|
||||
dst := filepath.Join(m.assetsDir, e.Name())
|
||||
if err := copyFile(src, dst); err != nil {
|
||||
m.logger.Printf("[WARN] Failed to seed asset %s: %v", e.Name(), err)
|
||||
@@ -105,11 +110,32 @@ func (m *Manager) seedIfEmpty() {
|
||||
copied++
|
||||
}
|
||||
|
||||
m.logger.Printf("[INFO] Seeded %d assets from image to PVC", copied)
|
||||
if copied > 0 {
|
||||
m.logger.Printf("[INFO] Asset seed: %d updated, %d unchanged", copied, skipped)
|
||||
} else {
|
||||
m.logger.Printf("[INFO] Asset seed: all %d files up-to-date", skipped)
|
||||
}
|
||||
}
|
||||
|
||||
// ReSeed re-runs the seed-or-update process and rebuilds the manifest.
|
||||
// Used by the web UI "Refresh Assets" button.
|
||||
func (m *Manager) ReSeed() error {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
|
||||
m.seedOrUpdate()
|
||||
return m.rebuildManifestLocked()
|
||||
}
|
||||
|
||||
// RebuildManifest rescans the assets directory and recomputes SHA-256 hashes.
|
||||
func (m *Manager) RebuildManifest() error {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
return m.rebuildManifestLocked()
|
||||
}
|
||||
|
||||
// rebuildManifestLocked does the actual manifest rebuild. Caller must hold m.mu.
|
||||
func (m *Manager) rebuildManifestLocked() error {
|
||||
entries, err := os.ReadDir(m.assetsDir)
|
||||
if err != nil {
|
||||
return fmt.Errorf("reading assets dir: %w", err)
|
||||
@@ -147,9 +173,7 @@ func (m *Manager) RebuildManifest() error {
|
||||
Files: files,
|
||||
}
|
||||
|
||||
m.mu.Lock()
|
||||
m.manifest = manifest
|
||||
m.mu.Unlock()
|
||||
|
||||
m.logger.Printf("[INFO] Asset manifest built: %d files", len(files))
|
||||
return nil
|
||||
@@ -201,7 +225,8 @@ func (m *Manager) FileCount() int {
|
||||
return len(m.manifest.Files)
|
||||
}
|
||||
|
||||
// isAssetFile returns true if the filename matches the app asset naming convention.
|
||||
// isAssetFile returns true if the filename matches the asset naming convention.
|
||||
// Matches: *-logo.svg, *-logo.png, *-screenshot-*.webp, felhom-logo.svg, felhom-favicon.svg
|
||||
func isAssetFile(name string) bool {
|
||||
if strings.Contains(name, "-logo.") {
|
||||
return strings.HasSuffix(name, ".svg") || strings.HasSuffix(name, ".png")
|
||||
@@ -209,6 +234,9 @@ func isAssetFile(name string) bool {
|
||||
if strings.Contains(name, "-screenshot-") {
|
||||
return strings.HasSuffix(name, ".webp")
|
||||
}
|
||||
if strings.Contains(name, "-favicon.") {
|
||||
return strings.HasSuffix(name, ".svg") || strings.HasSuffix(name, ".ico")
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
|
||||
@@ -17,6 +17,7 @@ import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"gitea.dooplex.hu/admin/felhom-hub/internal/assets"
|
||||
"gitea.dooplex.hu/admin/felhom-hub/internal/store"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
)
|
||||
@@ -38,6 +39,7 @@ type Server struct {
|
||||
staleThreshold time.Duration
|
||||
versionChecker *VersionChecker
|
||||
templateFetcher *TemplateFetcher
|
||||
assetsMgr *assets.Manager
|
||||
|
||||
sessions map[string]*hubSession
|
||||
sessionsMu sync.RWMutex
|
||||
@@ -113,6 +115,11 @@ func (s *Server) SetTemplateFetcher(tf *TemplateFetcher) {
|
||||
s.templateFetcher = tf
|
||||
}
|
||||
|
||||
// SetAssetManager sets the asset manager for the Configuration page (optional).
|
||||
func (s *Server) SetAssetManager(am *assets.Manager) {
|
||||
s.assetsMgr = am
|
||||
}
|
||||
|
||||
// ServeHTTP routes web requests.
|
||||
func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
path := r.URL.Path
|
||||
@@ -138,6 +145,12 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/javascript")
|
||||
w.Header().Set("Cache-Control", "public, max-age=86400")
|
||||
w.Write(chartJS)
|
||||
case path == "/configuration":
|
||||
if r.Method == http.MethodPost {
|
||||
s.handleConfigurationAction(w, r)
|
||||
} else {
|
||||
s.handleConfiguration(w, r)
|
||||
}
|
||||
case path == "/apps" || path == "/apps/":
|
||||
s.handleApps(w, r)
|
||||
case strings.HasPrefix(path, "/apps/") && strings.HasSuffix(path, "/reset-telemetry"):
|
||||
@@ -606,3 +619,48 @@ func statusColor(status string) string {
|
||||
func statusIcon(status string) string {
|
||||
return "●"
|
||||
}
|
||||
|
||||
// handleConfiguration renders the Configuration page.
|
||||
func (s *Server) handleConfiguration(w http.ResponseWriter, r *http.Request) {
|
||||
csrfToken := s.getCSRFToken(r)
|
||||
|
||||
assetCount := 0
|
||||
assetLastSync := ""
|
||||
if s.assetsMgr != nil {
|
||||
assetCount = s.assetsMgr.FileCount()
|
||||
if m := s.assetsMgr.GetManifest(); m != nil {
|
||||
assetLastSync = m.Generated
|
||||
}
|
||||
}
|
||||
|
||||
data := map[string]interface{}{
|
||||
"CSRFToken": csrfToken,
|
||||
"AssetCount": assetCount,
|
||||
"AssetLastSync": assetLastSync,
|
||||
"Flash": r.URL.Query().Get("flash"),
|
||||
}
|
||||
if err := s.templates.ExecuteTemplate(w, "configuration.html", data); err != nil {
|
||||
s.logger.Printf("[ERROR] configuration.html template: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// handleConfigurationAction handles POST actions on the Configuration page.
|
||||
func (s *Server) handleConfigurationAction(w http.ResponseWriter, r *http.Request) {
|
||||
action := r.FormValue("action")
|
||||
switch action {
|
||||
case "refresh_assets":
|
||||
if s.assetsMgr == nil {
|
||||
http.Redirect(w, r, "/configuration?flash=assets_not_configured", http.StatusSeeOther)
|
||||
return
|
||||
}
|
||||
if err := s.assetsMgr.ReSeed(); err != nil {
|
||||
s.logger.Printf("[ERROR] Asset re-seed failed: %v", err)
|
||||
http.Redirect(w, r, "/configuration?flash=assets_error", http.StatusSeeOther)
|
||||
return
|
||||
}
|
||||
s.logger.Printf("[INFO] Manual asset re-seed completed")
|
||||
http.Redirect(w, r, "/configuration?flash=assets_refreshed", http.StatusSeeOther)
|
||||
default:
|
||||
http.Redirect(w, r, "/configuration", http.StatusSeeOther)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="hu">
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
@@ -14,21 +14,22 @@
|
||||
<nav class="nav-links">
|
||||
<a href="/" class="nav-link">Dashboard</a>
|
||||
<a href="/configs" class="nav-link">Customers</a>
|
||||
<a href="/apps" class="nav-link active">Alkalmazások</a>
|
||||
<a href="/apps" class="nav-link active">Apps</a>
|
||||
<a href="/configuration" class="nav-link">Configuration</a>
|
||||
</nav>
|
||||
</header>
|
||||
|
||||
<a href="/apps{{if .Period}}?period={{.Period}}{{end}}" class="back-link">← Alkalmazások</a>
|
||||
<a href="/apps{{if .Period}}?period={{.Period}}{{end}}" class="back-link">← Apps</a>
|
||||
|
||||
<!-- Period selector -->
|
||||
<div class="period-selector" style="margin-top: 1rem;">
|
||||
<a href="?period=24h" class="period-btn{{if eq .Period "24h"}} active{{end}}">24 óra</a>
|
||||
<a href="?period=7d" class="period-btn{{if or (eq .Period "7d") (eq .Period "")}} active{{end}}">7 nap</a>
|
||||
<a href="?period=30d" class="period-btn{{if eq .Period "30d"}} active{{end}}">30 nap</a>
|
||||
<a href="?period=24h" class="period-btn{{if eq .Period "24h"}} active{{end}}">24h</a>
|
||||
<a href="?period=7d" class="period-btn{{if or (eq .Period "7d") (eq .Period "")}} active{{end}}">7d</a>
|
||||
<a href="?period=30d" class="period-btn{{if eq .Period "30d"}} active{{end}}">30d</a>
|
||||
</div>
|
||||
|
||||
{{if eq .Flash "telemetry_reset"}}
|
||||
<div class="flash flash-success" style="margin-top: 1rem;">Telemetria sikeresen törölve.</div>
|
||||
<div class="flash flash-success" style="margin-top: 1rem;">Telemetry data deleted successfully.</div>
|
||||
{{end}}
|
||||
|
||||
<!-- Overview card -->
|
||||
@@ -36,45 +37,45 @@
|
||||
<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.');">
|
||||
onsubmit="return confirm('Are you sure you want to delete all telemetry data for {{.AppName}}? This cannot be undone.');">
|
||||
<input type="hidden" name="_csrf" value="{{.CSRFToken}}">
|
||||
<button type="submit" class="btn btn-sm btn-danger">Telemetria törlése</button>
|
||||
<button type="submit" class="btn btn-sm btn-danger">Reset Telemetry</button>
|
||||
</form>
|
||||
</div>
|
||||
<div class="info-grid">
|
||||
<div class="info-item">
|
||||
<span class="label">App neve</span>
|
||||
<span class="label">App Name</span>
|
||||
<span class="value" style="font-family: var(--font-mono)">{{.AppName}}</span>
|
||||
</div>
|
||||
{{if .Summary}}
|
||||
<div class="info-item">
|
||||
<span class="label">Telepítések</span>
|
||||
<span class="label">Deployments</span>
|
||||
<span class="value">{{.Summary.DeploymentCount}}</span>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<span class="label">Katalógus becslés</span>
|
||||
<span class="label">Catalog Estimate</span>
|
||||
<span class="value">{{if .Summary.CatalogEstimate}}{{.Summary.CatalogEstimate}}{{else}}—{{end}}</span>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<span class="label">Katalógus limit</span>
|
||||
<span class="label">Catalog Limit</span>
|
||||
<span class="value">{{if .Summary.CatalogLimit}}{{.Summary.CatalogLimit}}{{else}}—{{end}}</span>
|
||||
</div>
|
||||
{{if .SuggestedLimit}}
|
||||
<div class="info-item">
|
||||
<span class="label">Javasolt limit (P95×1.2)</span>
|
||||
<span class="label">Suggested Limit (P95×1.2)</span>
|
||||
<span class="value" style="color: var(--yellow)">{{.SuggestedLimit}} MB</span>
|
||||
</div>
|
||||
{{end}}
|
||||
<div class="info-item">
|
||||
<span class="label">Átl. memória</span>
|
||||
<span class="label">Avg Memory</span>
|
||||
<span class="value">{{formatFloat .Summary.AvgMemoryMB}} MB</span>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<span class="label">P95 memória</span>
|
||||
<span class="label">P95 Memory</span>
|
||||
<span class="value {{accuracyClass .Summary.P95MemoryMB .Summary.CatalogLimit}}">{{formatFloat .Summary.P95MemoryMB}} MB</span>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<span class="label">Átl. CPU</span>
|
||||
<span class="label">Avg CPU</span>
|
||||
<span class="value">{{formatFloat .Summary.AvgCPU}}%</span>
|
||||
</div>
|
||||
{{end}}
|
||||
@@ -83,7 +84,7 @@
|
||||
|
||||
<!-- Memory trend chart -->
|
||||
<section class="card">
|
||||
<h2>Memória trend</h2>
|
||||
<h2>Memory Trend</h2>
|
||||
<div class="chart-container">
|
||||
<canvas id="memoryChart"></canvas>
|
||||
</div>
|
||||
@@ -91,13 +92,13 @@
|
||||
(function() {
|
||||
var chartData = {{json .ChartData}};
|
||||
if (!chartData || !chartData.labels || chartData.labels.length === 0) {
|
||||
document.getElementById('memoryChart').parentElement.innerHTML = '<p class="text-muted">Nincs elegendő adat a grafikonhoz.</p>';
|
||||
document.getElementById('memoryChart').parentElement.innerHTML = '<p class="text-muted">Not enough data for the chart.</p>';
|
||||
return;
|
||||
}
|
||||
var ctx = document.getElementById('memoryChart').getContext('2d');
|
||||
var datasets = [
|
||||
{
|
||||
label: 'Átl. memória (MB)',
|
||||
label: 'Avg Memory (MB)',
|
||||
data: chartData.avg_memory,
|
||||
borderColor: '#60a5fa',
|
||||
backgroundColor: 'rgba(96,165,250,0.1)',
|
||||
@@ -106,7 +107,7 @@
|
||||
pointRadius: 2
|
||||
},
|
||||
{
|
||||
label: 'Csúcs memória (MB)',
|
||||
label: 'Peak Memory (MB)',
|
||||
data: chartData.peak_memory,
|
||||
borderColor: '#f87171',
|
||||
backgroundColor: 'transparent',
|
||||
@@ -118,7 +119,7 @@
|
||||
];
|
||||
if (chartData.catalog_limit > 0) {
|
||||
datasets.push({
|
||||
label: 'Katalógus limit',
|
||||
label: 'Catalog Limit',
|
||||
data: chartData.labels.map(function() { return chartData.catalog_limit; }),
|
||||
borderColor: '#4ade80',
|
||||
backgroundColor: 'transparent',
|
||||
@@ -150,16 +151,16 @@
|
||||
<!-- Customer breakdown -->
|
||||
{{if .Customers}}
|
||||
<section class="card">
|
||||
<h2>Ügyfél bontás</h2>
|
||||
<h2>Customer Breakdown</h2>
|
||||
<table class="data-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Ügyfél</th>
|
||||
<th>Átl. memória</th>
|
||||
<th>Csúcs memória</th>
|
||||
<th>Átl. CPU</th>
|
||||
<th>Hibák összesen</th>
|
||||
<th>Utolsó riport</th>
|
||||
<th>Customer</th>
|
||||
<th>Avg Memory</th>
|
||||
<th>Peak Memory</th>
|
||||
<th>Avg CPU</th>
|
||||
<th>Total Errors</th>
|
||||
<th>Last Report</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@@ -181,16 +182,16 @@
|
||||
<!-- Known issues -->
|
||||
{{if .Issues}}
|
||||
<section class="card">
|
||||
<h2>Ismert hibák</h2>
|
||||
<h2>Known Issues</h2>
|
||||
<table class="data-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Súlyosság</th>
|
||||
<th>Üzenet</th>
|
||||
<th>Előfordulások</th>
|
||||
<th>Érintett ügyfelek</th>
|
||||
<th>Első észlelés</th>
|
||||
<th>Utolsó észlelés</th>
|
||||
<th>Severity</th>
|
||||
<th>Message</th>
|
||||
<th>Occurrences</th>
|
||||
<th>Affected Customers</th>
|
||||
<th>First Seen</th>
|
||||
<th>Last Seen</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="hu">
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Alkalmazások — Felhom Hub</title>
|
||||
<title>Apps — Felhom Hub</title>
|
||||
<link rel="stylesheet" href="/style.css">
|
||||
</head>
|
||||
<body>
|
||||
@@ -13,32 +13,33 @@
|
||||
<nav class="nav-links">
|
||||
<a href="/" class="nav-link">Dashboard</a>
|
||||
<a href="/configs" class="nav-link">Customers</a>
|
||||
<a href="/apps" class="nav-link active">Alkalmazások</a>
|
||||
<a href="/apps" class="nav-link active">Apps</a>
|
||||
<a href="/configuration" class="nav-link">Configuration</a>
|
||||
</nav>
|
||||
</header>
|
||||
|
||||
<h2 style="margin-bottom: 1rem;">Alkalmazás telemetria</h2>
|
||||
<h2 style="margin-bottom: 1rem;">App Telemetry</h2>
|
||||
|
||||
<!-- Period selector -->
|
||||
<div class="period-selector">
|
||||
<a href="?period=24h{{if .Sort}}&sort={{.Sort}}&order={{.Order}}{{end}}" class="period-btn{{if eq .Period "24h"}} active{{end}}">24 óra</a>
|
||||
<a href="?period=7d{{if .Sort}}&sort={{.Sort}}&order={{.Order}}{{end}}" class="period-btn{{if or (eq .Period "7d") (eq .Period "")}} active{{end}}">7 nap</a>
|
||||
<a href="?period=30d{{if .Sort}}&sort={{.Sort}}&order={{.Order}}{{end}}" class="period-btn{{if eq .Period "30d"}} active{{end}}">30 nap</a>
|
||||
<a href="?period=24h{{if .Sort}}&sort={{.Sort}}&order={{.Order}}{{end}}" class="period-btn{{if eq .Period "24h"}} active{{end}}">24h</a>
|
||||
<a href="?period=7d{{if .Sort}}&sort={{.Sort}}&order={{.Order}}{{end}}" class="period-btn{{if or (eq .Period "7d") (eq .Period "")}} active{{end}}">7d</a>
|
||||
<a href="?period=30d{{if .Sort}}&sort={{.Sort}}&order={{.Order}}{{end}}" class="period-btn{{if eq .Period "30d"}} active{{end}}">30d</a>
|
||||
</div>
|
||||
|
||||
<!-- Summary cards -->
|
||||
<div class="summary-cards">
|
||||
<div class="summary-card">
|
||||
<div class="card-number">{{.TotalApps}}</div>
|
||||
<div class="card-label">Alkalmazás összesen</div>
|
||||
<div class="card-label">Total Apps</div>
|
||||
</div>
|
||||
<div class="summary-card">
|
||||
<div class="card-number">{{.TotalDeployments}}</div>
|
||||
<div class="card-label">Telepítések száma</div>
|
||||
<div class="card-label">Deployments</div>
|
||||
</div>
|
||||
<div class="summary-card">
|
||||
<div class="card-number" {{if gt .AppsWithErrors 0}}style="color: var(--red)"{{end}}>{{.AppsWithErrors}}</div>
|
||||
<div class="card-label">Hibás alkalmazások</div>
|
||||
<div class="card-label">Apps with Errors</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -48,15 +49,15 @@
|
||||
<table class="data-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th><a href="?period={{.Period}}&sort=name&order={{if eq .Sort "name"}}{{if eq .Order "asc"}}desc{{else}}asc{{end}}{{else}}asc{{end}}">Alkalmazás</a></th>
|
||||
<th><a href="?period={{.Period}}&sort=deployments&order={{if eq .Sort "deployments"}}{{if eq .Order "asc"}}desc{{else}}asc{{end}}{{else}}desc{{end}}">Telepítések</a></th>
|
||||
<th><a href="?period={{.Period}}&sort=memory&order={{if eq .Sort "memory"}}{{if eq .Order "asc"}}desc{{else}}asc{{end}}{{else}}desc{{end}}">Átl. memória</a></th>
|
||||
<th>P95 memória</th>
|
||||
<th>Katalógus becslés</th>
|
||||
<th>Katalógus limit</th>
|
||||
<th>Pontosság</th>
|
||||
<th><a href="?period={{.Period}}&sort=errors&order={{if eq .Sort "errors"}}{{if eq .Order "asc"}}desc{{else}}asc{{end}}{{else}}desc{{end}}">Hibák</a></th>
|
||||
<th>Figyelmeztetések</th>
|
||||
<th><a href="?period={{.Period}}&sort=name&order={{if eq .Sort "name"}}{{if eq .Order "asc"}}desc{{else}}asc{{end}}{{else}}asc{{end}}">App</a></th>
|
||||
<th><a href="?period={{.Period}}&sort=deployments&order={{if eq .Sort "deployments"}}{{if eq .Order "asc"}}desc{{else}}asc{{end}}{{else}}desc{{end}}">Deployments</a></th>
|
||||
<th><a href="?period={{.Period}}&sort=memory&order={{if eq .Sort "memory"}}{{if eq .Order "asc"}}desc{{else}}asc{{end}}{{else}}desc{{end}}">Avg Memory</a></th>
|
||||
<th>P95 Memory</th>
|
||||
<th>Catalog Estimate</th>
|
||||
<th>Catalog Limit</th>
|
||||
<th>Accuracy</th>
|
||||
<th><a href="?period={{.Period}}&sort=errors&order={{if eq .Sort "errors"}}{{if eq .Order "asc"}}desc{{else}}asc{{end}}{{else}}desc{{end}}">Errors</a></th>
|
||||
<th>Warnings</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@@ -70,9 +71,9 @@
|
||||
<td>{{if .CatalogLimit}}{{.CatalogLimit}}{{else}}—{{end}}</td>
|
||||
<td>
|
||||
{{$ac := accuracyClass .P95MemoryMB .CatalogLimit}}
|
||||
{{if eq $ac "ok"}}<span class="accuracy-dot accuracy-ok" title="P95 rendben"></span>
|
||||
{{else if eq $ac "warn"}}<span class="accuracy-dot accuracy-warn" title="P95 > limit 50%"></span>
|
||||
{{else if eq $ac "danger"}}<span class="accuracy-dot accuracy-danger" title="P95 meghaladja a limitet"></span>
|
||||
{{if eq $ac "ok"}}<span class="accuracy-dot accuracy-ok" title="P95 within limit"></span>
|
||||
{{else if eq $ac "warn"}}<span class="accuracy-dot accuracy-warn" title="P95 > 50% of limit"></span>
|
||||
{{else if eq $ac "danger"}}<span class="accuracy-dot accuracy-danger" title="P95 exceeds limit"></span>
|
||||
{{else}}—{{end}}
|
||||
</td>
|
||||
<td>{{if gt .TotalErrors 0}}<span class="badge badge-error">{{.TotalErrors}}</span>{{else}}0{{end}}</td>
|
||||
@@ -84,8 +85,8 @@
|
||||
</section>
|
||||
{{else}}
|
||||
<div class="empty-state">
|
||||
<p>Nincs telemetria adat a kiválasztott időszakra.</p>
|
||||
<p class="hint">Az alkalmazás telemetria a következő riport beérkezése után jelenik meg (v0.28.0+ vezérlő szükséges).</p>
|
||||
<p>No telemetry data for the selected period.</p>
|
||||
<p class="hint">App telemetry will appear after the next report is received (requires controller v0.28.0+).</p>
|
||||
</div>
|
||||
{{end}}
|
||||
|
||||
|
||||
@@ -13,7 +13,8 @@
|
||||
<nav class="nav-links">
|
||||
<a href="/" class="nav-link">Dashboard</a>
|
||||
<a href="/configs" class="nav-link active">Customers</a>
|
||||
<a href="/apps" class="nav-link">Alkalmazások</a>
|
||||
<a href="/apps" class="nav-link">Apps</a>
|
||||
<a href="/configuration" class="nav-link">Configuration</a>
|
||||
</nav>
|
||||
</header>
|
||||
|
||||
|
||||
@@ -13,7 +13,8 @@
|
||||
<nav class="nav-links">
|
||||
<a href="/" class="nav-link">Dashboard</a>
|
||||
<a href="/configs" class="nav-link active">Customers</a>
|
||||
<a href="/apps" class="nav-link">Alkalmazások</a>
|
||||
<a href="/apps" class="nav-link">Apps</a>
|
||||
<a href="/configuration" class="nav-link">Configuration</a>
|
||||
</nav>
|
||||
</header>
|
||||
|
||||
|
||||
@@ -13,7 +13,8 @@
|
||||
<nav class="nav-links">
|
||||
<a href="/" class="nav-link">Dashboard</a>
|
||||
<a href="/configs" class="nav-link active">Customers</a>
|
||||
<a href="/apps" class="nav-link">Alkalmazások</a>
|
||||
<a href="/apps" class="nav-link">Apps</a>
|
||||
<a href="/configuration" class="nav-link">Configuration</a>
|
||||
</nav>
|
||||
</header>
|
||||
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Configuration — Felhom Hub</title>
|
||||
<link rel="stylesheet" href="/style.css">
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<header>
|
||||
<h1>Felhom Hub</h1>
|
||||
<nav class="nav-links">
|
||||
<a href="/" class="nav-link">Dashboard</a>
|
||||
<a href="/configs" class="nav-link">Customers</a>
|
||||
<a href="/apps" class="nav-link">Apps</a>
|
||||
<a href="/configuration" class="nav-link active">Configuration</a>
|
||||
</nav>
|
||||
</header>
|
||||
|
||||
<h2 style="margin-bottom: 1rem;">Configuration</h2>
|
||||
|
||||
{{if eq .Flash "assets_refreshed"}}
|
||||
<div class="flash flash-success">Assets refreshed successfully from image seed.</div>
|
||||
{{end}}
|
||||
{{if eq .Flash "assets_error"}}
|
||||
<div class="flash flash-error">Asset refresh failed. Check server logs for details.</div>
|
||||
{{end}}
|
||||
{{if eq .Flash "assets_not_configured"}}
|
||||
<div class="flash flash-error">Asset manager is not configured.</div>
|
||||
{{end}}
|
||||
|
||||
<!-- Assets section -->
|
||||
<section class="card">
|
||||
<h3 style="margin-top: 0;">Assets</h3>
|
||||
<p class="text-muted" style="margin-bottom: 1rem;">
|
||||
App logos and screenshots served to controllers. Assets are seeded from the Docker image
|
||||
and synced to controllers daily via the asset manifest API.
|
||||
</p>
|
||||
<div class="info-grid">
|
||||
<div class="info-item">
|
||||
<span class="label">Files in manifest</span>
|
||||
<span class="value">{{.AssetCount}}</span>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<span class="label">Manifest generated</span>
|
||||
<span class="value" style="font-family: var(--font-mono); font-size: 0.85em;">{{if .AssetLastSync}}{{.AssetLastSync}}{{else}}—{{end}}</span>
|
||||
</div>
|
||||
</div>
|
||||
<form method="POST" action="/configuration" style="margin-top: 1rem;">
|
||||
<input type="hidden" name="_csrf" value="{{.CSRFToken}}">
|
||||
<input type="hidden" name="action" value="refresh_assets">
|
||||
<button type="submit" class="btn" onclick="this.disabled=true;this.textContent='Refreshing…';this.form.submit();">Refresh Assets from Image</button>
|
||||
</form>
|
||||
<p class="text-muted" style="margin-top: 0.75rem; font-size: 0.8rem;">
|
||||
Re-reads the baked-in asset seed directory and updates changed files.
|
||||
Controllers will pick up changes on their next daily sync or manual trigger.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<footer style="margin-top: 2rem; color: var(--text-muted); font-size: 0.8rem; text-align: center;">
|
||||
Felhom Hub <span style="font-family: var(--font-mono)">v{{hubVersion}}</span>
|
||||
</footer>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -13,7 +13,8 @@
|
||||
<nav class="nav-links" style="margin-bottom: 0.5rem;">
|
||||
<a href="/" class="nav-link">Dashboard</a>
|
||||
<a href="/configs" class="nav-link">Customers</a>
|
||||
<a href="/apps" class="nav-link">Alkalmazások</a>
|
||||
<a href="/apps" class="nav-link">Apps</a>
|
||||
<a href="/configuration" class="nav-link">Configuration</a>
|
||||
</nav>
|
||||
<a href="/" class="back-link">← Back to Dashboard</a>
|
||||
<h1>
|
||||
|
||||
@@ -15,7 +15,8 @@
|
||||
<nav class="nav-links" style="margin-bottom: 0.5rem;">
|
||||
<a href="/" class="nav-link">Dashboard</a>
|
||||
<a href="/configs" class="nav-link active">Customers</a>
|
||||
<a href="/apps" class="nav-link">Alkalmazások</a>
|
||||
<a href="/apps" class="nav-link">Apps</a>
|
||||
<a href="/configuration" class="nav-link">Configuration</a>
|
||||
</nav>
|
||||
<a href="/configs" class="back-link">← All Customers</a>
|
||||
<h1>
|
||||
@@ -471,20 +472,20 @@
|
||||
{{end}}
|
||||
</section>
|
||||
|
||||
<!-- Alkalmazás telemetria -->
|
||||
<!-- App telemetry -->
|
||||
{{if .HasAppTelemetry}}
|
||||
<section class="card">
|
||||
<h2>Alkalmazás telemetria <span class="text-muted" style="font-size: 0.85em; font-weight: normal;">(utolsó 7 nap)</span></h2>
|
||||
<h2>App Telemetry <span class="text-muted" style="font-size: 0.85em; font-weight: normal;">(last 7 days)</span></h2>
|
||||
<table class="data-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Alkalmazás</th>
|
||||
<th>Memória (jelenlegi)</th>
|
||||
<th>Memória (átlag 7d)</th>
|
||||
<th>Memória (csúcs 7d)</th>
|
||||
<th>Katalógus limit</th>
|
||||
<th>Hibák</th>
|
||||
<th>Figyelmeztetések</th>
|
||||
<th>App</th>
|
||||
<th>Memory (current)</th>
|
||||
<th>Memory (avg 7d)</th>
|
||||
<th>Memory (peak 7d)</th>
|
||||
<th>Catalog Limit</th>
|
||||
<th>Errors</th>
|
||||
<th>Warnings</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
|
||||
@@ -14,7 +14,8 @@
|
||||
<nav class="nav-links">
|
||||
<a href="/" class="nav-link active">Dashboard</a>
|
||||
<a href="/configs" class="nav-link">Customers</a>
|
||||
<a href="/apps" class="nav-link">Alkalmazások</a>
|
||||
<a href="/apps" class="nav-link">Apps</a>
|
||||
<a href="/configuration" class="nav-link">Configuration</a>
|
||||
</nav>
|
||||
</header>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user