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
|
# 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)
|
## v0.4.1 (2026-02-23)
|
||||||
|
|
||||||
### Added
|
### 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.
|
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
|
### 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
|
## 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) |
|
| **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 |
|
| **Registry version check** | Every 30min | Checks Gitea registry for new controller image tags |
|
||||||
| **Template refresh** | Every 1h | Fetches latest `controller.yaml.example` from Gitea |
|
| **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
|
## Internal Packages
|
||||||
|
|
||||||
|
|||||||
@@ -143,6 +143,7 @@ func main() {
|
|||||||
|
|
||||||
webServer := web.New(dataStore, cfg.Auth.PasswordHash, cfg.API.ReportAPIKey, Version, staleThreshold, logger)
|
webServer := web.New(dataStore, cfg.Auth.PasswordHash, cfg.API.ReportAPIKey, Version, staleThreshold, logger)
|
||||||
webServer.SetTemplateFetcher(templateFetcher)
|
webServer.SetTemplateFetcher(templateFetcher)
|
||||||
|
webServer.SetAssetManager(assetsMgr)
|
||||||
|
|
||||||
// Build HTTP mux
|
// Build HTTP mux
|
||||||
mux := http.NewServeMux()
|
mux := http.NewServeMux()
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ type Manager struct {
|
|||||||
logger *log.Logger
|
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.
|
// and builds the initial manifest.
|
||||||
func New(assetsDir, seedDir string, logger *log.Logger) *Manager {
|
func New(assetsDir, seedDir string, logger *log.Logger) *Manager {
|
||||||
m := &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)
|
logger.Printf("[ERROR] Failed to create assets dir %s: %v", assetsDir, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
m.seedIfEmpty()
|
m.seedOrUpdate()
|
||||||
|
|
||||||
if err := m.RebuildManifest(); err != nil {
|
if err := m.RebuildManifest(); err != nil {
|
||||||
logger.Printf("[ERROR] Failed to build asset manifest: %v", err)
|
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
|
return m
|
||||||
}
|
}
|
||||||
|
|
||||||
// seedIfEmpty copies assets from the seed directory to the PVC directory
|
// seedOrUpdate copies new or changed assets from the seed directory to the PVC directory.
|
||||||
// if the PVC directory has no asset files.
|
// On first run (empty PVC) this seeds all files. On subsequent runs it updates only files
|
||||||
func (m *Manager) seedIfEmpty() {
|
// whose SHA-256 checksums differ, ensuring redeployed images propagate asset changes.
|
||||||
entries, err := os.ReadDir(m.assetsDir)
|
func (m *Manager) seedOrUpdate() {
|
||||||
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
|
|
||||||
seedEntries, err := os.ReadDir(m.seedDir)
|
seedEntries, err := os.ReadDir(m.seedDir)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
m.logger.Printf("[WARN] Cannot read seed dir %s: %v (no assets will be available until uploaded)", m.seedDir, err)
|
m.logger.Printf("[WARN] Cannot read seed dir %s: %v (no assets will be available until uploaded)", m.seedDir, err)
|
||||||
return
|
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 {
|
for _, e := range seedEntries {
|
||||||
if e.IsDir() || !isAssetFile(e.Name()) {
|
if e.IsDir() || !isAssetFile(e.Name()) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
src := filepath.Join(m.seedDir, e.Name())
|
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())
|
dst := filepath.Join(m.assetsDir, e.Name())
|
||||||
if err := copyFile(src, dst); err != nil {
|
if err := copyFile(src, dst); err != nil {
|
||||||
m.logger.Printf("[WARN] Failed to seed asset %s: %v", e.Name(), err)
|
m.logger.Printf("[WARN] Failed to seed asset %s: %v", e.Name(), err)
|
||||||
@@ -105,11 +110,32 @@ func (m *Manager) seedIfEmpty() {
|
|||||||
copied++
|
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.
|
// RebuildManifest rescans the assets directory and recomputes SHA-256 hashes.
|
||||||
func (m *Manager) RebuildManifest() error {
|
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)
|
entries, err := os.ReadDir(m.assetsDir)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("reading assets dir: %w", err)
|
return fmt.Errorf("reading assets dir: %w", err)
|
||||||
@@ -147,9 +173,7 @@ func (m *Manager) RebuildManifest() error {
|
|||||||
Files: files,
|
Files: files,
|
||||||
}
|
}
|
||||||
|
|
||||||
m.mu.Lock()
|
|
||||||
m.manifest = manifest
|
m.manifest = manifest
|
||||||
m.mu.Unlock()
|
|
||||||
|
|
||||||
m.logger.Printf("[INFO] Asset manifest built: %d files", len(files))
|
m.logger.Printf("[INFO] Asset manifest built: %d files", len(files))
|
||||||
return nil
|
return nil
|
||||||
@@ -201,7 +225,8 @@ func (m *Manager) FileCount() int {
|
|||||||
return len(m.manifest.Files)
|
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 {
|
func isAssetFile(name string) bool {
|
||||||
if strings.Contains(name, "-logo.") {
|
if strings.Contains(name, "-logo.") {
|
||||||
return strings.HasSuffix(name, ".svg") || strings.HasSuffix(name, ".png")
|
return strings.HasSuffix(name, ".svg") || strings.HasSuffix(name, ".png")
|
||||||
@@ -209,6 +234,9 @@ func isAssetFile(name string) bool {
|
|||||||
if strings.Contains(name, "-screenshot-") {
|
if strings.Contains(name, "-screenshot-") {
|
||||||
return strings.HasSuffix(name, ".webp")
|
return strings.HasSuffix(name, ".webp")
|
||||||
}
|
}
|
||||||
|
if strings.Contains(name, "-favicon.") {
|
||||||
|
return strings.HasSuffix(name, ".svg") || strings.HasSuffix(name, ".ico")
|
||||||
|
}
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ import (
|
|||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"gitea.dooplex.hu/admin/felhom-hub/internal/assets"
|
||||||
"gitea.dooplex.hu/admin/felhom-hub/internal/store"
|
"gitea.dooplex.hu/admin/felhom-hub/internal/store"
|
||||||
"golang.org/x/crypto/bcrypt"
|
"golang.org/x/crypto/bcrypt"
|
||||||
)
|
)
|
||||||
@@ -38,6 +39,7 @@ type Server struct {
|
|||||||
staleThreshold time.Duration
|
staleThreshold time.Duration
|
||||||
versionChecker *VersionChecker
|
versionChecker *VersionChecker
|
||||||
templateFetcher *TemplateFetcher
|
templateFetcher *TemplateFetcher
|
||||||
|
assetsMgr *assets.Manager
|
||||||
|
|
||||||
sessions map[string]*hubSession
|
sessions map[string]*hubSession
|
||||||
sessionsMu sync.RWMutex
|
sessionsMu sync.RWMutex
|
||||||
@@ -113,6 +115,11 @@ func (s *Server) SetTemplateFetcher(tf *TemplateFetcher) {
|
|||||||
s.templateFetcher = tf
|
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.
|
// ServeHTTP routes web requests.
|
||||||
func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||||
path := r.URL.Path
|
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("Content-Type", "application/javascript")
|
||||||
w.Header().Set("Cache-Control", "public, max-age=86400")
|
w.Header().Set("Cache-Control", "public, max-age=86400")
|
||||||
w.Write(chartJS)
|
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/":
|
case path == "/apps" || path == "/apps/":
|
||||||
s.handleApps(w, r)
|
s.handleApps(w, r)
|
||||||
case strings.HasPrefix(path, "/apps/") && strings.HasSuffix(path, "/reset-telemetry"):
|
case strings.HasPrefix(path, "/apps/") && strings.HasSuffix(path, "/reset-telemetry"):
|
||||||
@@ -606,3 +619,48 @@ func statusColor(status string) string {
|
|||||||
func statusIcon(status string) string {
|
func statusIcon(status string) string {
|
||||||
return "●"
|
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>
|
<!DOCTYPE html>
|
||||||
<html lang="hu">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
@@ -14,21 +14,22 @@
|
|||||||
<nav class="nav-links">
|
<nav class="nav-links">
|
||||||
<a href="/" class="nav-link">Dashboard</a>
|
<a href="/" class="nav-link">Dashboard</a>
|
||||||
<a href="/configs" class="nav-link">Customers</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>
|
</nav>
|
||||||
</header>
|
</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 -->
|
<!-- Period selector -->
|
||||||
<div class="period-selector" style="margin-top: 1rem;">
|
<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=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}}">7 nap</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}}">30 nap</a>
|
<a href="?period=30d" class="period-btn{{if eq .Period "30d"}} active{{end}}">30d</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{{if eq .Flash "telemetry_reset"}}
|
{{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}}
|
{{end}}
|
||||||
|
|
||||||
<!-- Overview card -->
|
<!-- Overview card -->
|
||||||
@@ -36,45 +37,45 @@
|
|||||||
<div style="display: flex; justify-content: space-between; align-items: center; flex-wrap: wrap; gap: 0.5rem;">
|
<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>
|
<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}}"
|
<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}}">
|
<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>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
<div class="info-grid">
|
<div class="info-grid">
|
||||||
<div class="info-item">
|
<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>
|
<span class="value" style="font-family: var(--font-mono)">{{.AppName}}</span>
|
||||||
</div>
|
</div>
|
||||||
{{if .Summary}}
|
{{if .Summary}}
|
||||||
<div class="info-item">
|
<div class="info-item">
|
||||||
<span class="label">Telepítések</span>
|
<span class="label">Deployments</span>
|
||||||
<span class="value">{{.Summary.DeploymentCount}}</span>
|
<span class="value">{{.Summary.DeploymentCount}}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="info-item">
|
<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>
|
<span class="value">{{if .Summary.CatalogEstimate}}{{.Summary.CatalogEstimate}}{{else}}—{{end}}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="info-item">
|
<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>
|
<span class="value">{{if .Summary.CatalogLimit}}{{.Summary.CatalogLimit}}{{else}}—{{end}}</span>
|
||||||
</div>
|
</div>
|
||||||
{{if .SuggestedLimit}}
|
{{if .SuggestedLimit}}
|
||||||
<div class="info-item">
|
<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>
|
<span class="value" style="color: var(--yellow)">{{.SuggestedLimit}} MB</span>
|
||||||
</div>
|
</div>
|
||||||
{{end}}
|
{{end}}
|
||||||
<div class="info-item">
|
<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>
|
<span class="value">{{formatFloat .Summary.AvgMemoryMB}} MB</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="info-item">
|
<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>
|
<span class="value {{accuracyClass .Summary.P95MemoryMB .Summary.CatalogLimit}}">{{formatFloat .Summary.P95MemoryMB}} MB</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="info-item">
|
<div class="info-item">
|
||||||
<span class="label">Átl. CPU</span>
|
<span class="label">Avg CPU</span>
|
||||||
<span class="value">{{formatFloat .Summary.AvgCPU}}%</span>
|
<span class="value">{{formatFloat .Summary.AvgCPU}}%</span>
|
||||||
</div>
|
</div>
|
||||||
{{end}}
|
{{end}}
|
||||||
@@ -83,7 +84,7 @@
|
|||||||
|
|
||||||
<!-- Memory trend chart -->
|
<!-- Memory trend chart -->
|
||||||
<section class="card">
|
<section class="card">
|
||||||
<h2>Memória trend</h2>
|
<h2>Memory Trend</h2>
|
||||||
<div class="chart-container">
|
<div class="chart-container">
|
||||||
<canvas id="memoryChart"></canvas>
|
<canvas id="memoryChart"></canvas>
|
||||||
</div>
|
</div>
|
||||||
@@ -91,13 +92,13 @@
|
|||||||
(function() {
|
(function() {
|
||||||
var chartData = {{json .ChartData}};
|
var chartData = {{json .ChartData}};
|
||||||
if (!chartData || !chartData.labels || chartData.labels.length === 0) {
|
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;
|
return;
|
||||||
}
|
}
|
||||||
var ctx = document.getElementById('memoryChart').getContext('2d');
|
var ctx = document.getElementById('memoryChart').getContext('2d');
|
||||||
var datasets = [
|
var datasets = [
|
||||||
{
|
{
|
||||||
label: 'Átl. memória (MB)',
|
label: 'Avg Memory (MB)',
|
||||||
data: chartData.avg_memory,
|
data: chartData.avg_memory,
|
||||||
borderColor: '#60a5fa',
|
borderColor: '#60a5fa',
|
||||||
backgroundColor: 'rgba(96,165,250,0.1)',
|
backgroundColor: 'rgba(96,165,250,0.1)',
|
||||||
@@ -106,7 +107,7 @@
|
|||||||
pointRadius: 2
|
pointRadius: 2
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Csúcs memória (MB)',
|
label: 'Peak Memory (MB)',
|
||||||
data: chartData.peak_memory,
|
data: chartData.peak_memory,
|
||||||
borderColor: '#f87171',
|
borderColor: '#f87171',
|
||||||
backgroundColor: 'transparent',
|
backgroundColor: 'transparent',
|
||||||
@@ -118,7 +119,7 @@
|
|||||||
];
|
];
|
||||||
if (chartData.catalog_limit > 0) {
|
if (chartData.catalog_limit > 0) {
|
||||||
datasets.push({
|
datasets.push({
|
||||||
label: 'Katalógus limit',
|
label: 'Catalog Limit',
|
||||||
data: chartData.labels.map(function() { return chartData.catalog_limit; }),
|
data: chartData.labels.map(function() { return chartData.catalog_limit; }),
|
||||||
borderColor: '#4ade80',
|
borderColor: '#4ade80',
|
||||||
backgroundColor: 'transparent',
|
backgroundColor: 'transparent',
|
||||||
@@ -150,16 +151,16 @@
|
|||||||
<!-- Customer breakdown -->
|
<!-- Customer breakdown -->
|
||||||
{{if .Customers}}
|
{{if .Customers}}
|
||||||
<section class="card">
|
<section class="card">
|
||||||
<h2>Ügyfél bontás</h2>
|
<h2>Customer Breakdown</h2>
|
||||||
<table class="data-table">
|
<table class="data-table">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>Ügyfél</th>
|
<th>Customer</th>
|
||||||
<th>Átl. memória</th>
|
<th>Avg Memory</th>
|
||||||
<th>Csúcs memória</th>
|
<th>Peak Memory</th>
|
||||||
<th>Átl. CPU</th>
|
<th>Avg CPU</th>
|
||||||
<th>Hibák összesen</th>
|
<th>Total Errors</th>
|
||||||
<th>Utolsó riport</th>
|
<th>Last Report</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
@@ -181,16 +182,16 @@
|
|||||||
<!-- Known issues -->
|
<!-- Known issues -->
|
||||||
{{if .Issues}}
|
{{if .Issues}}
|
||||||
<section class="card">
|
<section class="card">
|
||||||
<h2>Ismert hibák</h2>
|
<h2>Known Issues</h2>
|
||||||
<table class="data-table">
|
<table class="data-table">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>Súlyosság</th>
|
<th>Severity</th>
|
||||||
<th>Üzenet</th>
|
<th>Message</th>
|
||||||
<th>Előfordulások</th>
|
<th>Occurrences</th>
|
||||||
<th>Érintett ügyfelek</th>
|
<th>Affected Customers</th>
|
||||||
<th>Első észlelés</th>
|
<th>First Seen</th>
|
||||||
<th>Utolsó észlelés</th>
|
<th>Last Seen</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="hu">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<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">
|
<link rel="stylesheet" href="/style.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
@@ -13,32 +13,33 @@
|
|||||||
<nav class="nav-links">
|
<nav class="nav-links">
|
||||||
<a href="/" class="nav-link">Dashboard</a>
|
<a href="/" class="nav-link">Dashboard</a>
|
||||||
<a href="/configs" class="nav-link">Customers</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>
|
</nav>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<h2 style="margin-bottom: 1rem;">Alkalmazás telemetria</h2>
|
<h2 style="margin-bottom: 1rem;">App Telemetry</h2>
|
||||||
|
|
||||||
<!-- Period selector -->
|
<!-- Period selector -->
|
||||||
<div class="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=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}}">7 nap</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}}">30 nap</a>
|
<a href="?period=30d{{if .Sort}}&sort={{.Sort}}&order={{.Order}}{{end}}" class="period-btn{{if eq .Period "30d"}} active{{end}}">30d</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Summary cards -->
|
<!-- Summary cards -->
|
||||||
<div class="summary-cards">
|
<div class="summary-cards">
|
||||||
<div class="summary-card">
|
<div class="summary-card">
|
||||||
<div class="card-number">{{.TotalApps}}</div>
|
<div class="card-number">{{.TotalApps}}</div>
|
||||||
<div class="card-label">Alkalmazás összesen</div>
|
<div class="card-label">Total Apps</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="summary-card">
|
<div class="summary-card">
|
||||||
<div class="card-number">{{.TotalDeployments}}</div>
|
<div class="card-number">{{.TotalDeployments}}</div>
|
||||||
<div class="card-label">Telepítések száma</div>
|
<div class="card-label">Deployments</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="summary-card">
|
<div class="summary-card">
|
||||||
<div class="card-number" {{if gt .AppsWithErrors 0}}style="color: var(--red)"{{end}}>{{.AppsWithErrors}}</div>
|
<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>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -48,15 +49,15 @@
|
|||||||
<table class="data-table">
|
<table class="data-table">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<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=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}}">Telepítések</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}}">Átl. memória</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 memória</th>
|
<th>P95 Memory</th>
|
||||||
<th>Katalógus becslés</th>
|
<th>Catalog Estimate</th>
|
||||||
<th>Katalógus limit</th>
|
<th>Catalog Limit</th>
|
||||||
<th>Pontosság</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}}">Hibák</a></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>Figyelmeztetések</th>
|
<th>Warnings</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
@@ -70,9 +71,9 @@
|
|||||||
<td>{{if .CatalogLimit}}{{.CatalogLimit}}{{else}}—{{end}}</td>
|
<td>{{if .CatalogLimit}}{{.CatalogLimit}}{{else}}—{{end}}</td>
|
||||||
<td>
|
<td>
|
||||||
{{$ac := accuracyClass .P95MemoryMB .CatalogLimit}}
|
{{$ac := accuracyClass .P95MemoryMB .CatalogLimit}}
|
||||||
{{if eq $ac "ok"}}<span class="accuracy-dot accuracy-ok" title="P95 rendben"></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 > limit 50%"></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 meghaladja a limitet"></span>
|
{{else if eq $ac "danger"}}<span class="accuracy-dot accuracy-danger" title="P95 exceeds limit"></span>
|
||||||
{{else}}—{{end}}
|
{{else}}—{{end}}
|
||||||
</td>
|
</td>
|
||||||
<td>{{if gt .TotalErrors 0}}<span class="badge badge-error">{{.TotalErrors}}</span>{{else}}0{{end}}</td>
|
<td>{{if gt .TotalErrors 0}}<span class="badge badge-error">{{.TotalErrors}}</span>{{else}}0{{end}}</td>
|
||||||
@@ -84,8 +85,8 @@
|
|||||||
</section>
|
</section>
|
||||||
{{else}}
|
{{else}}
|
||||||
<div class="empty-state">
|
<div class="empty-state">
|
||||||
<p>Nincs telemetria adat a kiválasztott időszakra.</p>
|
<p>No telemetry data for the selected period.</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 class="hint">App telemetry will appear after the next report is received (requires controller v0.28.0+).</p>
|
||||||
</div>
|
</div>
|
||||||
{{end}}
|
{{end}}
|
||||||
|
|
||||||
|
|||||||
@@ -13,7 +13,8 @@
|
|||||||
<nav class="nav-links">
|
<nav class="nav-links">
|
||||||
<a href="/" class="nav-link">Dashboard</a>
|
<a href="/" class="nav-link">Dashboard</a>
|
||||||
<a href="/configs" class="nav-link active">Customers</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>
|
</nav>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
|
|||||||
@@ -13,7 +13,8 @@
|
|||||||
<nav class="nav-links">
|
<nav class="nav-links">
|
||||||
<a href="/" class="nav-link">Dashboard</a>
|
<a href="/" class="nav-link">Dashboard</a>
|
||||||
<a href="/configs" class="nav-link active">Customers</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>
|
</nav>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
|
|||||||
@@ -13,7 +13,8 @@
|
|||||||
<nav class="nav-links">
|
<nav class="nav-links">
|
||||||
<a href="/" class="nav-link">Dashboard</a>
|
<a href="/" class="nav-link">Dashboard</a>
|
||||||
<a href="/configs" class="nav-link active">Customers</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>
|
</nav>
|
||||||
</header>
|
</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;">
|
<nav class="nav-links" style="margin-bottom: 0.5rem;">
|
||||||
<a href="/" class="nav-link">Dashboard</a>
|
<a href="/" class="nav-link">Dashboard</a>
|
||||||
<a href="/configs" class="nav-link">Customers</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>
|
</nav>
|
||||||
<a href="/" class="back-link">← Back to Dashboard</a>
|
<a href="/" class="back-link">← Back to Dashboard</a>
|
||||||
<h1>
|
<h1>
|
||||||
|
|||||||
@@ -15,7 +15,8 @@
|
|||||||
<nav class="nav-links" style="margin-bottom: 0.5rem;">
|
<nav class="nav-links" style="margin-bottom: 0.5rem;">
|
||||||
<a href="/" class="nav-link">Dashboard</a>
|
<a href="/" class="nav-link">Dashboard</a>
|
||||||
<a href="/configs" class="nav-link active">Customers</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>
|
</nav>
|
||||||
<a href="/configs" class="back-link">← All Customers</a>
|
<a href="/configs" class="back-link">← All Customers</a>
|
||||||
<h1>
|
<h1>
|
||||||
@@ -471,20 +472,20 @@
|
|||||||
{{end}}
|
{{end}}
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<!-- Alkalmazás telemetria -->
|
<!-- App telemetry -->
|
||||||
{{if .HasAppTelemetry}}
|
{{if .HasAppTelemetry}}
|
||||||
<section class="card">
|
<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">
|
<table class="data-table">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>Alkalmazás</th>
|
<th>App</th>
|
||||||
<th>Memória (jelenlegi)</th>
|
<th>Memory (current)</th>
|
||||||
<th>Memória (átlag 7d)</th>
|
<th>Memory (avg 7d)</th>
|
||||||
<th>Memória (csúcs 7d)</th>
|
<th>Memory (peak 7d)</th>
|
||||||
<th>Katalógus limit</th>
|
<th>Catalog Limit</th>
|
||||||
<th>Hibák</th>
|
<th>Errors</th>
|
||||||
<th>Figyelmeztetések</th>
|
<th>Warnings</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
|
|||||||
@@ -14,7 +14,8 @@
|
|||||||
<nav class="nav-links">
|
<nav class="nav-links">
|
||||||
<a href="/" class="nav-link active">Dashboard</a>
|
<a href="/" class="nav-link active">Dashboard</a>
|
||||||
<a href="/configs" class="nav-link">Customers</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>
|
</nav>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,255 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<svg
|
||||||
|
viewBox="0 0 645.30703 408.36403"
|
||||||
|
version="1.1"
|
||||||
|
id="svg14"
|
||||||
|
sodipodi:docname="logo.svg"
|
||||||
|
inkscape:version="1.4.2 (f4327f4, 2025-05-13)"
|
||||||
|
xml:space="preserve"
|
||||||
|
inkscape:export-filename="logo.png"
|
||||||
|
inkscape:export-xdpi="96"
|
||||||
|
inkscape:export-ydpi="96"
|
||||||
|
width="645.30701"
|
||||||
|
height="408.36404"
|
||||||
|
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||||
|
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||||
|
xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
xmlns:svg="http://www.w3.org/2000/svg"
|
||||||
|
xmlns:bx="https://boxy-svg.com"><sodipodi:namedview
|
||||||
|
id="namedview14"
|
||||||
|
pagecolor="#505050"
|
||||||
|
bordercolor="#eeeeee"
|
||||||
|
borderopacity="1"
|
||||||
|
inkscape:showpageshadow="0"
|
||||||
|
inkscape:pageopacity="0"
|
||||||
|
inkscape:pagecheckerboard="0"
|
||||||
|
inkscape:deskcolor="#505050"
|
||||||
|
inkscape:zoom="0.70710678"
|
||||||
|
inkscape:cx="140.71425"
|
||||||
|
inkscape:cy="58.689863"
|
||||||
|
inkscape:window-width="1920"
|
||||||
|
inkscape:window-height="1009"
|
||||||
|
inkscape:window-x="-8"
|
||||||
|
inkscape:window-y="-8"
|
||||||
|
inkscape:window-maximized="1"
|
||||||
|
inkscape:current-layer="svg14"><inkscape:page
|
||||||
|
x="0"
|
||||||
|
y="0"
|
||||||
|
width="645.30701"
|
||||||
|
height="408.36404"
|
||||||
|
id="page2"
|
||||||
|
margin="0"
|
||||||
|
bleed="0" /></sodipodi:namedview><defs
|
||||||
|
id="defs4"><linearGradient
|
||||||
|
id="linearGradient9"
|
||||||
|
bx:pinned="true"><stop
|
||||||
|
offset="0"
|
||||||
|
style=""
|
||||||
|
id="stop8" /><stop
|
||||||
|
offset="0.99875164"
|
||||||
|
style="stop-color: rgb(4, 114, 187);"
|
||||||
|
id="stop9" /></linearGradient><linearGradient
|
||||||
|
id="gradient-1"
|
||||||
|
bx:pinned="true"><stop
|
||||||
|
offset="0"
|
||||||
|
style="stop-color: rgb(0, 64, 141);"
|
||||||
|
id="stop1" /><stop
|
||||||
|
offset="1"
|
||||||
|
style="stop-color: rgb(0, 141, 223);"
|
||||||
|
id="stop2" /></linearGradient><linearGradient
|
||||||
|
id="gradient-1-0"
|
||||||
|
href="#gradient-1"
|
||||||
|
gradientUnits="userSpaceOnUse"
|
||||||
|
x1="30.771"
|
||||||
|
y1="2283.52"
|
||||||
|
x2="30.771"
|
||||||
|
y2="2416.4089"
|
||||||
|
spreadMethod="pad"
|
||||||
|
gradientTransform="matrix(0.999122,0,0,0.848244,1717.8096,192.633)" /><linearGradient
|
||||||
|
id="gradient-1-1"
|
||||||
|
href="#gradient-1"
|
||||||
|
gradientUnits="userSpaceOnUse"
|
||||||
|
x1="30.849001"
|
||||||
|
y1="2446.3101"
|
||||||
|
x2="30.849001"
|
||||||
|
y2="2588.6721"
|
||||||
|
gradientTransform="matrix(0.996573,0,0,0.791798,1717.8096,192.63306)" /><linearGradient
|
||||||
|
id="gradient-2"
|
||||||
|
bx:pinned="true"><stop
|
||||||
|
offset="0.002"
|
||||||
|
style=""
|
||||||
|
id="stop3" /><stop
|
||||||
|
offset="1"
|
||||||
|
style="stop-color: rgb(4, 114, 187);"
|
||||||
|
id="stop4" /></linearGradient><linearGradient
|
||||||
|
id="gradient-2-0"
|
||||||
|
href="#gradient-2"
|
||||||
|
gradientUnits="userSpaceOnUse"
|
||||||
|
x1="255.14799"
|
||||||
|
y1="321.186"
|
||||||
|
x2="255.14799"
|
||||||
|
y2="321.71799"
|
||||||
|
gradientTransform="matrix(1.47e-4,6.198697,270.23999,-0.112229,-86674.622,-1260.2124)" /><linearGradient
|
||||||
|
id="gradient-2-1"
|
||||||
|
href="#gradient-2"
|
||||||
|
gradientUnits="userSpaceOnUse"
|
||||||
|
x1="255.08755"
|
||||||
|
y1="321.37799"
|
||||||
|
x2="255.09705"
|
||||||
|
y2="321.90982"
|
||||||
|
gradientTransform="matrix(1.17e-4,21.428825,213.85692,-0.389689,-68565.13,-5073.7432)" /><linearGradient
|
||||||
|
id="gradient-2-1-0"
|
||||||
|
href="#linearGradient9"
|
||||||
|
gradientUnits="userSpaceOnUse"
|
||||||
|
x1="255.87483"
|
||||||
|
y1="321.37949"
|
||||||
|
x2="255.87479"
|
||||||
|
y2="321.90854"
|
||||||
|
gradientTransform="matrix(1.17e-4,21.428825,213.85692,-0.389689,-68565.13,-5073.7432)" /><linearGradient
|
||||||
|
inkscape:collect="always"
|
||||||
|
xlink:href="#linearGradient16"
|
||||||
|
id="linearGradient14"
|
||||||
|
gradientUnits="userSpaceOnUse"
|
||||||
|
gradientTransform="matrix(8.4922907e-5,-0.999122,0.848244,7.2098649e-5,-1738.1491,381.17896)"
|
||||||
|
x1="30.771"
|
||||||
|
y1="2283.52"
|
||||||
|
x2="30.771"
|
||||||
|
y2="2416.4089"
|
||||||
|
spreadMethod="pad" /><linearGradient
|
||||||
|
id="linearGradient16"
|
||||||
|
bx:pinned="true"><stop
|
||||||
|
offset="0"
|
||||||
|
style="stop-color:#051343;stop-opacity:1;"
|
||||||
|
id="stop13" /><stop
|
||||||
|
offset="1"
|
||||||
|
style="stop-color: rgb(0, 141, 223);"
|
||||||
|
id="stop14" /></linearGradient><linearGradient
|
||||||
|
inkscape:collect="always"
|
||||||
|
xlink:href="#linearGradient16"
|
||||||
|
id="linearGradient15"
|
||||||
|
gradientUnits="userSpaceOnUse"
|
||||||
|
gradientTransform="matrix(8.4706248e-5,-0.996573,0.791798,6.7300878e-5,-1738.149,381.17906)"
|
||||||
|
x1="30.849001"
|
||||||
|
y1="2446.3101"
|
||||||
|
x2="30.849001"
|
||||||
|
y2="2588.6721" /><linearGradient
|
||||||
|
inkscape:collect="always"
|
||||||
|
xlink:href="#linearGradient10"
|
||||||
|
id="linearGradient17"
|
||||||
|
x1="160.76199"
|
||||||
|
y1="268.35672"
|
||||||
|
x2="284.18887"
|
||||||
|
y2="268.80402"
|
||||||
|
gradientUnits="userSpaceOnUse"
|
||||||
|
gradientTransform="translate(34.548823,75.050781)" /><linearGradient
|
||||||
|
id="linearGradient10"
|
||||||
|
bx:pinned="true"><stop
|
||||||
|
offset="0"
|
||||||
|
style="stop-color:#051343;stop-opacity:1;"
|
||||||
|
id="stop7" /><stop
|
||||||
|
offset="0.99875164"
|
||||||
|
style="stop-color: rgb(4, 114, 187);"
|
||||||
|
id="stop10" /></linearGradient><linearGradient
|
||||||
|
inkscape:collect="always"
|
||||||
|
xlink:href="#linearGradient10"
|
||||||
|
id="linearGradient1"
|
||||||
|
gradientUnits="userSpaceOnUse"
|
||||||
|
x1="160.76199"
|
||||||
|
y1="268.35672"
|
||||||
|
x2="284.18887"
|
||||||
|
y2="268.80402"
|
||||||
|
gradientTransform="translate(25.265938,90.769558)" /></defs><rect
|
||||||
|
x="1740.2544"
|
||||||
|
y="2129.6233"
|
||||||
|
width="16.597"
|
||||||
|
height="112.721"
|
||||||
|
style="fill:url(#gradient-1-0);stroke:url(#gradient-1-1);paint-order:fill"
|
||||||
|
transform="rotate(-89.99513)"
|
||||||
|
id="rect13" /><text
|
||||||
|
style="font-size:28px;font-family:Arial, sans-serif;white-space:pre;fill:#333333"
|
||||||
|
x="813.82861"
|
||||||
|
y="302.39877"
|
||||||
|
id="text14" /><text
|
||||||
|
xml:space="preserve"
|
||||||
|
style="font-size:106.667px;text-align:center;writing-mode:lr-tb;direction:ltr;text-anchor:middle;fill:#00408d;fill-opacity:1;stroke:#000000;stroke-width:2;stroke-dasharray:none;stroke-opacity:1"
|
||||||
|
x="324.56198"
|
||||||
|
y="377.59503"
|
||||||
|
id="text1"><tspan
|
||||||
|
sodipodi:role="line"
|
||||||
|
id="tspan1"
|
||||||
|
x="324.56198"
|
||||||
|
y="377.59503"
|
||||||
|
style="font-style:normal;font-variant:normal;font-weight:900;font-stretch:normal;font-size:106.667px;font-family:'M+ 2c';-inkscape-font-specification:'M+ 2c Heavy';fill:#00408d;fill-opacity:1;stroke:#000000;stroke-width:2;stroke-dasharray:none;stroke-opacity:1" /></text><text
|
||||||
|
xml:space="preserve"
|
||||||
|
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:140.132px;font-family:'Vremena Grotesk';-inkscape-font-specification:'Vremena Grotesk';text-align:center;writing-mode:lr-tb;direction:ltr;text-anchor:middle;fill:#ffffff;fill-opacity:1;stroke:#051343;stroke-width:2.33554;stroke-dasharray:none;stroke-opacity:1"
|
||||||
|
x="232.56667"
|
||||||
|
y="401.04272"
|
||||||
|
id="text2"><tspan
|
||||||
|
sodipodi:role="line"
|
||||||
|
id="tspan2"
|
||||||
|
x="232.56667"
|
||||||
|
y="401.04272"
|
||||||
|
style="font-size:140.132px;stroke-width:2.33554;fill:#ffffff;fill-opacity:1">f<tspan
|
||||||
|
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-family:'M+ 2c';-inkscape-font-specification:'M+ 2c';stroke-width:2.33554;fill:#ffffff;fill-opacity:1"
|
||||||
|
id="tspan4">e</tspan>lhom</tspan></text><text
|
||||||
|
xml:space="preserve"
|
||||||
|
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:140.132px;font-family:'M+ 2c';-inkscape-font-specification:'M+ 2c';text-align:center;writing-mode:lr-tb;direction:ltr;text-anchor:middle;fill:#008ddf;fill-opacity:1;stroke:#051343;stroke-width:2.33554;stroke-dasharray:none;stroke-opacity:1"
|
||||||
|
x="563.68787"
|
||||||
|
y="400.21695"
|
||||||
|
id="text5"><tspan
|
||||||
|
sodipodi:role="line"
|
||||||
|
id="tspan5"
|
||||||
|
x="563.68787"
|
||||||
|
y="400.21695"
|
||||||
|
style="stroke:#051343;stroke-width:2.33554;stroke-opacity:1">e<tspan
|
||||||
|
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-family:'Vremena Grotesk';-inkscape-font-specification:'Vremena Grotesk';stroke:#051343;stroke-width:2.33554;stroke-opacity:1"
|
||||||
|
id="tspan6">u</tspan></tspan></text><circle
|
||||||
|
style="fill:#008ddf;fill-opacity:1;stroke:#051343;stroke-width:1.85226;stroke-dasharray:none;stroke-opacity:1"
|
||||||
|
id="path6"
|
||||||
|
cx="469.65689"
|
||||||
|
cy="391.54669"
|
||||||
|
r="10.150504" /><g
|
||||||
|
id="g2"
|
||||||
|
transform="translate(-34.626437,-75)"><path
|
||||||
|
style="fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:0.745956;stroke-dasharray:none;stroke-opacity:1"
|
||||||
|
d="m 184.76065,350.14894 c -15.98877,-4.50438 -28.82636,-12.40521 -41.07759,-25.28104 -19.29776,-20.28158 -25.9034,-45.05671 -19.24091,-72.16505 5.44415,-22.15119 21.9613,-42.47639 42.05694,-51.75321 10.46476,-4.83088 15.41718,-5.83792 29.10384,-5.91808 6.75769,-0.0394 13.78198,0.2012 15.60955,0.53505 l 3.32286,0.60701 0.46604,-6.7663 c 1.11599,-16.20306 7.59666,-35.0318 17.25505,-50.13235 5.22044,-8.16196 20.3749,-24.12314 28.39374,-29.90526 15.86366,-11.438698 32.8931,-15.404258 52.541,-18.490808 11.05595,-1.73682 26.69069,0.35712 37.13887,2.71288 40.8437,9.209108 78.44375,33.380298 91.01513,71.148868 1.09236,3.28174 2.14751,5.96681 2.34482,5.96681 0.1973,0 3.1737,-1.19976 6.6142,-2.6661 9.80615,-4.1794 17.28246,-5.85315 28.33643,-6.34387 11.05001,-0.49053 18.45598,0.39016 27.96449,3.32542 28.80326,8.89154 53.06677,47.54197 52.24723,61.73235 l -0.80326,13.90855 -4.28191,0.0455 c -2.64695,0.0281 -9.43186,2.02792 -11.29568,2.61754 -5.78846,1.83115 -1.84497,-4.47147 -48.23835,33.83846 -32.22413,26.60949 -59.34963,42.82359 -93.44795,55.85791 -15.09262,5.76925 -36.3971,12.02181 -46.1779,13.55253 -1.59004,0.24885 -34.79244,4.83384 -73.35101,5.86764 -38.55855,1.03382 -64.73196,-0.41814 -80.59412,-1.26238 -9.06729,-0.48259 -11.66601,0.16115 -15.90151,-1.03209 z"
|
||||||
|
id="path15-1"
|
||||||
|
sodipodi:nodetypes="sssscscssssssssssscssssssss" /><path
|
||||||
|
id="path4"
|
||||||
|
style="fill:#051343;fill-opacity:1;stroke:#051343;stroke-width:2px;stroke-opacity:1"
|
||||||
|
d="m 188.36409,236.4686 v 28.90235 H 294.06526 V 254.41001 H 269.77815 V 236.4686 Z m 20.66992,8.46485 a 5.5560002,5.5560002 0 0 1 5.55664,5.55664 5.5560002,5.5560002 0 0 1 -5.55664,5.55468 5.5560002,5.5560002 0 0 1 -5.55664,-5.55468 5.5560002,5.5560002 0 0 1 5.55664,-5.55664 z m 20.47852,0.0215 a 5.5560002,5.5560002 0 0 1 5.55664,5.55664 5.5560002,5.5560002 0 0 1 -5.55664,5.55664 5.5560002,5.5560002 0 0 1 -5.55664,-5.55664 5.5560002,5.5560002 0 0 1 5.55664,-5.55664 z m 20.25586,0.14258 a 5.5560002,5.5560002 0 0 1 5.55664,5.55664 5.5560002,5.5560002 0 0 1 -5.55664,5.55469 5.5560002,5.5560002 0 0 1 -5.55664,-5.55469 5.5560002,5.5560002 0 0 1 5.55664,-5.55664 z" /><path
|
||||||
|
id="rect6"
|
||||||
|
style="fill:#051343;fill-opacity:1;stroke:#051343;stroke-width:2px;stroke-opacity:1"
|
||||||
|
d="m 188.2762,275.99985 v 28.90039 h 105.70117 v -28.90039 z m 20.67188,9.46289 a 5.5560002,5.5560002 0 0 1 5.55468,5.55664 5.5560002,5.5560002 0 0 1 -5.55468,5.55664 5.5560002,5.5560002 0 0 1 -5.55665,-5.55664 5.5560002,5.5560002 0 0 1 5.55665,-5.55664 z m 20.47656,0.0234 a 5.5560002,5.5560002 0 0 1 5.55664,5.55469 5.5560002,5.5560002 0 0 1 -5.55664,5.55664 5.5560002,5.5560002 0 0 1 -5.55469,-5.55664 5.5560002,5.5560002 0 0 1 5.55469,-5.55469 z" /><path
|
||||||
|
id="path10"
|
||||||
|
style="fill:#051343;fill-opacity:1;stroke:#051343;stroke-width:1.9;stroke-dasharray:none;stroke-opacity:1"
|
||||||
|
d="m 232.32308,196.26352 v 28.90039 h 53.76953 l 38.35351,-28.90039 z m 20.67187,9.46289 a 5.5560002,5.5560002 0 0 1 5.55469,5.55664 5.5560002,5.5560002 0 0 1 -5.55469,5.55469 5.5560002,5.5560002 0 0 1 -5.55664,-5.55469 5.5560002,5.5560002 0 0 1 5.55664,-5.55664 z" /><path
|
||||||
|
d="m 292.49531,339.95157 c -53.034,0 -99.796,-10.762 -127.437,-27.136 19.291,8.807 47.768,14.377 79.534,14.377 23.242,0 44.724,-2.982 62.135,-8.032 v -75.023 l -28.665,-0.001 115.772,-87.28 48.409,36.495 v -12.853 h 25.034 v 31.726 l 42.329,31.912 h -28.353 v 77.268 l -78.822,0.005 c -27.916,11.441 -66.856,18.542 -109.935,18.542 z m 102.245,-115.364 c -12.363,0 -22.385,10.022 -22.385,22.385 0,8.343 4.565,15.621 11.334,19.471 l -7.782,41.652 h 37.666 l -7.782,-41.652 c 6.769,-3.85 11.334,-11.127 11.334,-19.471 0,-12.363 -10.022,-22.385 -22.385,-22.385 z"
|
||||||
|
style="fill:#051343;fill-opacity:1;stroke:#051343;stroke-width:2px;stroke-opacity:1"
|
||||||
|
id="path11" /><path
|
||||||
|
id="path12"
|
||||||
|
style="fill:#051343;fill-opacity:1;stroke:#051343;stroke-opacity:1"
|
||||||
|
d="m 331.89843,80.5 c -65.399,0 -119.66288,43.77067 -129.92188,101.13867 -0.925,-0.029 -1.85411,-0.043 -2.78711,-0.043 -48.98199,0 -88.68944,39.70841 -88.68944,88.69141 0,48.968 50.02496,85.48542 92.35683,89.13334 l -3.4892,-16.75561 c -32.82055,-3.42533 -71.54688,-33.52411 -71.54688,-73.16211 0,-40.362 33.06174,-73.31914 73.49174,-73.31914 1.843,0 3.67147,0.0672 5.48047,0.20117 v -0.2246 c 4,0.257 7.90183,0.8211 11.67383,1.6621 2.252,-55.91699 52.66917,-100.619135 114.53515,-100.619135 53.04001,0 97.66433,32.857755 110.73633,77.468755 0.309,-0.159 0.61964,-0.31571 0.93164,-0.47071 0.006,0.02 0.0116,0.0405 0.0176,0.0605 11.026,-5.378 23.57472,-8.41992 36.88672,-8.41992 37.486,0 68.91847,24.12451 77.35547,56.60351 -3.99102,-38.94897 -38.6409,-69.3945 -80.7969,-69.3945 -8.339,0 -16.38403,1.1913 -23.95703,3.4043 C 434.75677,111.94208 387.32643,80.5 331.89843,80.5 Z"
|
||||||
|
sodipodi:nodetypes="scssccsscccscccscscs" /><path
|
||||||
|
style="fill:none;fill-opacity:1;stroke:#051343;stroke-width:1;stroke-dasharray:none;stroke-opacity:1"
|
||||||
|
d="m 558.54392,219.94889 c 0.55822,2.06578 1.0625,10.5625 0.375,14.46875"
|
||||||
|
id="path1-2"
|
||||||
|
sodipodi:nodetypes="cc" /><path
|
||||||
|
id="path2"
|
||||||
|
style="fill:#008ddf;stroke:#0472bb"
|
||||||
|
d="m 556.93163,234.32031 c -16.13,0.388 -18.85575,5.44224 -48.34375,27.74024 -31.401,25.206 -62.71092,49.374 -117.91992,67 -12.11688,3.8684 -22.84543,6.83673 -36.66992,9.16601 -3.52327,0.53538 -7.07021,1.0572 -10.49219,1.52734 -11.11981,1.52774 -21.588,2.81477 -31.93359,3.76563 l -0.37205,20.49219 c 13.36471,0.34187 35.32192,0.38638 61.20023,-0.23523 25.87831,-0.62162 55.67773,-1.90936 84.72687,-4.23198 29.04914,-2.32263 73.441,-8.14839 90.28521,-12.88581 11.83463,-3.32848 28.09255,-8.72249 37.92989,-19.04615 9.46724,-10.59763 17.11931,-21.57912 18.56188,-36.64771 0.76581,-7.99943 -1.41558,-15.75159 -4.01758,-22.93359 -7.941,-19.793 -24.84508,-34.14694 -42.95508,-33.71094 z"
|
||||||
|
sodipodi:nodetypes="ccscsccssscscc" /><path
|
||||||
|
id="rect2"
|
||||||
|
style="fill:url(#linearGradient14);stroke:url(#linearGradient15);paint-order:fill"
|
||||||
|
d="m 198.84375,342.69727 -0.12538,15.70117 37.20599,3.55222 76.0147,1.95755 -0.37303,-20.15625 c -45.07268,4.1197 -82.74936,3.01733 -112.72267,-1.05469 z"
|
||||||
|
sodipodi:nodetypes="ccccccc"
|
||||||
|
inkscape:label="rect2" /><path
|
||||||
|
style="fill:none;fill-opacity:1;stroke:url(#linearGradient17);stroke-width:1;stroke-dasharray:none"
|
||||||
|
d="m 195.7971,342.19746 c 40.90805,5.43949 88.8177,6.68378 159.17408,-4.13383"
|
||||||
|
id="path14"
|
||||||
|
sodipodi:nodetypes="cc" /><path
|
||||||
|
style="fill:none;fill-opacity:1;stroke:url(#linearGradient1);stroke-width:1;stroke-dasharray:none"
|
||||||
|
d="m 186.51421,356.47993 c 19.27385,6.64292 99.62693,7.03138 134.89483,7.72"
|
||||||
|
id="path14-8"
|
||||||
|
sodipodi:nodetypes="cc" /></g></svg>
|
||||||
|
After Width: | Height: | Size: 15 KiB |
Reference in New Issue
Block a user