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:
@@ -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