refactor(web): remove deprecated dashboard components and enhance KPI grid
quality / commitlint (push) Skipped
quality / changes (push) Successful in 9s
quality / docker-check (push) Skipped
quality / openapi (push) Successful in 46s
quality / web (push) Successful in 1m16s
quality / go (push) Successful in 2m42s
quality / bird2 (push) Successful in 16s
CD / quality (push) Successful in 5m19s
CD / publish (push) Successful in 7m19s

- Deleted unused components: `DashboardActivityTimeline`, `DashboardFramePanel`, `DashboardModulesGrid`, `DashboardRecentJobsGrid`, and `DashboardRecentRevisionsGrid` to streamline the dashboard.
- Updated `DashboardKpiGrid` to improve KPI display logic, including progress indicators and enhanced badge functionality.
- Refactored `DashboardNetworkHealth` to provide better status representation based on loading states and network conditions.
- Introduced new properties for KPI cards to support progress tracking and improved visual feedback.

This cleanup aims to enhance performance and maintainability of the dashboard while providing a better user experience.
This commit is contained in:
Denozordec
2026-08-31 10:15:59 +07:00
parent e469c421ca
commit dc803bcb34
51 changed files with 1895 additions and 1078 deletions
+15
View File
@@ -116,11 +116,19 @@ type Backend interface {
GetModulePrefixSnapshot(tenantID, moduleID string) (*ModulePrefixSnapshot, bool, error)
SetModulePrefixSnapshot(tenantID, moduleID, inputHash string, prefixes []PrefixRow) error
DeleteModulePrefixSnapshot(tenantID, moduleID string) error
// SetModuleInputHash stores the ingest fingerprint on the module row (O(1) snapshot check).
SetModuleInputHash(tenantID, moduleID, hash string) error
// LockModuleSnapshot serializes read-modify-write of one module snapshot; unlock must be called.
LockModuleSnapshot(tenantID, moduleID string) (unlock func())
// ASNPrefixCache stores RIPEstat announced-prefixes per ASN (global TTL cache).
GetASNPrefixCache(asn int64) (*ASNPrefixCacheEntry, bool, error)
SetASNPrefixCache(asn int64, holder string, prefixes []string) error
// DomainResolveCache stores DoH results per FQDN (global TTL cache).
GetDomainResolveCache(fqdn string) (*DomainResolveCacheEntry, bool, error)
SetDomainResolveCache(fqdn string, addrs []string) error
// Ping verifies backend connectivity (no-op for in-memory).
Ping(ctx context.Context) error
@@ -178,6 +186,13 @@ type ASNPrefixCacheEntry struct {
FetchedAt time.Time
}
// DomainResolveCacheEntry is a cached DoH A/AAAA result for one FQDN.
type DomainResolveCacheEntry struct {
FQDN string
Addrs []string
ResolvedAt time.Time
}
// ModulePrefixSnapshot is the cached materialization for one module between refreshes.
type ModulePrefixSnapshot struct {
InputHash string
+18
View File
@@ -44,7 +44,9 @@ type Memory struct {
settings map[string]map[string]any // tenantID -> key -> JSON-compatible value
revPrefixes map[string][]PrefixRow
moduleSnapshots map[string]*moduleSnapshotRec
snapshotLocks sync.Map // key -> *sync.Mutex (per-module snapshot RMW)
asnPrefixCache map[int64]*ASNPrefixCacheEntry
domainResolveCache map[string]*DomainResolveCacheEntry
apiKeys map[string]*apiKeyRec
firewallClients map[string]*firewallClientRec
firewallRules map[string]*FirewallRule
@@ -99,6 +101,7 @@ type Module struct {
LastRefreshedAt *time.Time
DeletedAt *time.Time
CreatedByUserID string // portal JWT sub; empty = system / API key
InputHash string // ingest fingerprint; empty = not yet computed
}
type Revision struct {
@@ -156,6 +159,7 @@ func NewMemory() *Memory {
revPrefixes: make(map[string][]PrefixRow),
moduleSnapshots: make(map[string]*moduleSnapshotRec),
asnPrefixCache: make(map[int64]*ASNPrefixCacheEntry),
domainResolveCache: make(map[string]*DomainResolveCacheEntry),
apiKeys: make(map[string]*apiKeyRec),
firewallClients: make(map[string]*firewallClientRec),
firewallRules: make(map[string]*FirewallRule),
@@ -696,6 +700,20 @@ func cloneStringPtr(s *string) *string {
return &v
}
func (m *Memory) clearModuleInputHashLocked(moduleID string) {
if mod, ok := m.modules[moduleID]; ok && mod != nil {
mod.InputHash = ""
}
}
func (m *Memory) clearTenantModuleHashesLocked(tenantID string) {
for _, mod := range m.modules {
if mod != nil && mod.TenantID == tenantID && mod.DeletedAt == nil {
mod.InputHash = ""
}
}
}
func cloneModule(m *Module) *Module {
if m == nil {
return nil
+24 -4
View File
@@ -81,6 +81,7 @@ func (m *Memory) UpdateModule(tenantID, moduleID string, patch *ModulePatch) (*M
t := patch.LastRefreshedAt.UTC()
mod.LastRefreshedAt = &t
}
mod.InputHash = ""
return cloneModule(mod), nil
}
@@ -152,6 +153,7 @@ func (m *Memory) CreateCDNSource(tenantID, moduleID string, in *CDNSource) (*CDN
LastRefreshedAt: in.LastRefreshedAt,
}
m.cdnSources[id] = s
mod.InputHash = ""
return s, nil
}
@@ -161,7 +163,8 @@ func (m *Memory) UpdateCDNSource(tenantID, moduleID, sourceID string, patch *CDN
}
m.mu.Lock()
defer m.mu.Unlock()
if _, err := m.moduleWriteOK(tenantID, moduleID); err != nil {
mod, err := m.moduleWriteOK(tenantID, moduleID)
if err != nil {
return nil, err
}
s, ok := m.cdnSources[sourceID]
@@ -195,6 +198,9 @@ func (m *Memory) UpdateCDNSource(tenantID, moduleID, sourceID string, patch *CDN
t := patch.LastRefreshedAt.UTC()
s.LastRefreshedAt = &t
}
if CDNPatchAffectsInputHash(patch) {
mod.InputHash = ""
}
return s, nil
}
@@ -209,6 +215,7 @@ func (m *Memory) DeleteCDNSource(tenantID, moduleID, sourceID string) error {
return ErrNotFound
}
delete(m.cdnSources, sourceID)
m.clearModuleInputHashLocked(moduleID)
return nil
}
@@ -247,6 +254,7 @@ func (m *Memory) CreateASEntry(tenantID, moduleID string, in *ASEntry) (*ASEntry
id := uuid.NewString()
e := &ASEntry{ID: id, ModuleID: moduleID, ASN: in.ASN, CommunityID: in.CommunityID}
m.asEntries[id] = e
mod.InputHash = ""
return e, nil
}
@@ -256,7 +264,8 @@ func (m *Memory) UpdateASEntry(tenantID, moduleID, entryID string, patch *ASEntr
}
m.mu.Lock()
defer m.mu.Unlock()
if _, err := m.moduleWriteOK(tenantID, moduleID); err != nil {
mod, err := m.moduleWriteOK(tenantID, moduleID)
if err != nil {
return nil, err
}
e, ok := m.asEntries[entryID]
@@ -283,6 +292,7 @@ func (m *Memory) UpdateASEntry(tenantID, moduleID, entryID string, patch *ASEntr
e.PrefixCount = nil
e.ASNResolvedAt = nil
}
mod.InputHash = ""
return e, nil
}
@@ -324,6 +334,7 @@ func (m *Memory) DeleteASEntry(tenantID, moduleID, entryID string) error {
return ErrNotFound
}
delete(m.asEntries, entryID)
m.clearModuleInputHashLocked(moduleID)
return nil
}
@@ -362,6 +373,7 @@ func (m *Memory) CreateDomainEntry(tenantID, moduleID string, in *DomainEntry) (
id := uuid.NewString()
e := &DomainEntry{ID: id, ModuleID: moduleID, FQDN: strings.TrimSpace(in.FQDN), CommunityID: in.CommunityID}
m.domainEnt[id] = e
mod.InputHash = ""
return e, nil
}
@@ -371,7 +383,8 @@ func (m *Memory) UpdateDomainEntry(tenantID, moduleID, entryID string, patch *Do
}
m.mu.Lock()
defer m.mu.Unlock()
if _, err := m.moduleWriteOK(tenantID, moduleID); err != nil {
mod, err := m.moduleWriteOK(tenantID, moduleID)
if err != nil {
return nil, err
}
e, ok := m.domainEnt[entryID]
@@ -389,6 +402,7 @@ func (m *Memory) UpdateDomainEntry(tenantID, moduleID, entryID string, patch *Do
e.CommunityID = &v
}
}
mod.InputHash = ""
return e, nil
}
@@ -403,6 +417,7 @@ func (m *Memory) DeleteDomainEntry(tenantID, moduleID, entryID string) error {
return ErrNotFound
}
delete(m.domainEnt, entryID)
m.clearModuleInputHashLocked(moduleID)
return nil
}
@@ -441,6 +456,7 @@ func (m *Memory) CreateIPRangeEntry(tenantID, moduleID string, in *IPRangeEntry)
id := uuid.NewString()
e := &IPRangeEntry{ID: id, ModuleID: moduleID, Prefix: strings.TrimSpace(in.Prefix), CommunityID: in.CommunityID}
m.ipRanges[id] = e
mod.InputHash = ""
return e, nil
}
@@ -450,7 +466,8 @@ func (m *Memory) UpdateIPRangeEntry(tenantID, moduleID, entryID string, patch *I
}
m.mu.Lock()
defer m.mu.Unlock()
if _, err := m.moduleWriteOK(tenantID, moduleID); err != nil {
mod, err := m.moduleWriteOK(tenantID, moduleID)
if err != nil {
return nil, err
}
e, ok := m.ipRanges[entryID]
@@ -468,6 +485,7 @@ func (m *Memory) UpdateIPRangeEntry(tenantID, moduleID, entryID string, patch *I
e.CommunityID = &v
}
}
mod.InputHash = ""
return e, nil
}
@@ -482,6 +500,7 @@ func (m *Memory) DeleteIPRangeEntry(tenantID, moduleID, entryID string) error {
return ErrNotFound
}
delete(m.ipRanges, entryID)
m.clearModuleInputHashLocked(moduleID)
return nil
}
@@ -551,6 +570,7 @@ func (m *Memory) UpdateDohProfile(tenantID, id string, patch *DohProfilePatch) (
if patch.SecretRef != nil {
p.SecretRef = patch.SecretRef
}
m.clearTenantModuleHashesLocked(tenantID)
return p, nil
}
+41
View File
@@ -0,0 +1,41 @@
package store
import (
"strings"
"time"
)
func (m *Memory) GetDomainResolveCache(fqdn string) (*DomainResolveCacheEntry, bool, error) {
m.mu.RLock()
defer m.mu.RUnlock()
if m.domainResolveCache == nil {
return nil, false, nil
}
e, ok := m.domainResolveCache[strings.ToLower(strings.TrimSpace(fqdn))]
if !ok || e == nil {
return nil, false, nil
}
return &DomainResolveCacheEntry{
FQDN: e.FQDN,
Addrs: append([]string(nil), e.Addrs...),
ResolvedAt: e.ResolvedAt,
}, true, nil
}
func (m *Memory) SetDomainResolveCache(fqdn string, addrs []string) error {
key := strings.ToLower(strings.TrimSpace(fqdn))
if key == "" {
return ErrInvalidInput
}
m.mu.Lock()
defer m.mu.Unlock()
if m.domainResolveCache == nil {
m.domainResolveCache = make(map[string]*DomainResolveCacheEntry)
}
m.domainResolveCache[key] = &DomainResolveCacheEntry{
FQDN: key,
Addrs: append([]string(nil), addrs...),
ResolvedAt: time.Now().UTC(),
}
return nil
}
+23
View File
@@ -2,6 +2,7 @@ package store
import (
"strings"
"sync"
"time"
)
@@ -51,6 +52,28 @@ func (m *Memory) DeleteModulePrefixSnapshot(tenantID, moduleID string) error {
return nil
}
func (m *Memory) SetModuleInputHash(tenantID, moduleID, hash string) error {
m.mu.Lock()
defer m.mu.Unlock()
mod, ok := m.modules[moduleID]
if !ok || mod.DeletedAt != nil {
return ErrNotFound
}
if mod.TenantID != tenantID {
return ErrTenantScope
}
mod.InputHash = strings.TrimSpace(hash)
return nil
}
func (m *Memory) LockModuleSnapshot(tenantID, moduleID string) func() {
key := moduleSnapshotKey(tenantID, moduleID)
v, _ := m.snapshotLocks.LoadOrStore(key, &sync.Mutex{})
mu := v.(*sync.Mutex)
mu.Lock()
return func() { mu.Unlock() }
}
type moduleSnapshotRec struct {
InputHash string
CollectedAt time.Time
+123
View File
@@ -0,0 +1,123 @@
package store
import (
"crypto/sha256"
"encoding/hex"
"fmt"
"sort"
"strings"
)
// ComputeModuleInputHash fingerprints module config and child entries so snapshots
// invalidate on CRUD without re-reading every child at render time.
func ComputeModuleInputHash(st Backend, tenantID string, mod *Module) (string, error) {
if st == nil || mod == nil {
return "", fmt.Errorf("store: module hash: missing store or module")
}
h := sha256.New()
_, _ = fmt.Fprintf(h, "type=%s\n", strings.TrimSpace(mod.Type))
_, _ = fmt.Fprintf(h, "enabled=%t\n", mod.Enabled)
if mod.DefaultCommunityID != nil {
_, _ = fmt.Fprintf(h, "default_community=%s\n", strings.TrimSpace(*mod.DefaultCommunityID))
}
_, _ = fmt.Fprintf(h, "doh_policy=%s\n", NormalizeDohResolverPolicy(mod.DohResolverPolicy))
for _, pid := range mod.EffectiveDohProfileIDs() {
_, _ = fmt.Fprintf(h, "doh_profile=%s\n", pid)
if prof, err := st.GetDohProfile(tenantID, pid); err == nil && prof != nil {
_, _ = fmt.Fprintf(h, "doh_url=%s\n", strings.TrimSpace(prof.URL))
if prof.TimeoutMs != nil {
_, _ = fmt.Fprintf(h, "doh_timeout=%d\n", *prof.TimeoutMs)
}
}
}
switch mod.Type {
case "IP_RANGES":
list, err := st.ListIPRangeEntries(tenantID, mod.ID)
if err != nil {
return "", err
}
sort.Slice(list, func(i, j int) bool { return list[i].Prefix < list[j].Prefix })
for _, e := range list {
comm := ""
if e.CommunityID != nil {
comm = *e.CommunityID
}
_, _ = fmt.Fprintf(h, "ip=%s|c=%s\n", e.Prefix, comm)
}
case "AS_PREFIXES":
list, err := st.ListASEntries(tenantID, mod.ID)
if err != nil {
return "", err
}
sort.Slice(list, func(i, j int) bool { return list[i].ASN < list[j].ASN })
for _, e := range list {
comm := ""
if e.CommunityID != nil {
comm = *e.CommunityID
}
_, _ = fmt.Fprintf(h, "as=%d|c=%s\n", e.ASN, comm)
}
case "CDN_CIDRS":
list, err := st.ListCDNSources(tenantID, mod.ID)
if err != nil {
return "", err
}
sort.Slice(list, func(i, j int) bool { return list[i].ID < list[j].ID })
for _, s := range list {
comm := ""
if s.CommunityID != nil {
comm = *s.CommunityID
}
interval := 0
if s.RefreshIntervalSec != nil {
interval = *s.RefreshIntervalSec
}
_, _ = fmt.Fprintf(h, "cdn=%s|url=%s|kind=%s|path=%s|c=%s|etag=%s|interval=%d\n",
s.ID, strings.TrimSpace(s.URL), s.SourceKind, strings.TrimSpace(s.PrefixPath), comm,
strings.TrimSpace(s.Etag), interval)
}
case "DOMAINS":
list, err := st.ListDomainEntries(tenantID, mod.ID)
if err != nil {
return "", err
}
sort.Slice(list, func(i, j int) bool { return list[i].FQDN < list[j].FQDN })
for _, e := range list {
comm := ""
if e.CommunityID != nil {
comm = *e.CommunityID
}
_, _ = fmt.Fprintf(h, "dom=%s|c=%s\n", strings.TrimSpace(e.FQDN), comm)
}
default:
_, _ = fmt.Fprintf(h, "unknown_type=%s\n", mod.Type)
}
return hex.EncodeToString(h.Sum(nil)), nil
}
// TouchModuleInputHash recomputes and stores module.input_hash (ARCH-01: hash lives in store).
func TouchModuleInputHash(st Backend, tenantID, moduleID string) {
if st == nil {
return
}
mod, err := st.GetModule(tenantID, moduleID)
if err != nil || mod == nil {
return
}
h, err := ComputeModuleInputHash(st, tenantID, mod)
if err != nil {
return
}
_ = st.SetModuleInputHash(tenantID, moduleID, h)
}
// CDNPatchAffectsInputHash reports whether a CDN source patch changes ingest fingerprint
// fields (URL/kind/path/community/interval). ETag and last_refreshed_at do not.
func CDNPatchAffectsInputHash(patch *CDNSourcePatch) bool {
if patch == nil {
return false
}
return patch.SourceKind != nil || patch.URL != nil || patch.PrefixPath != nil ||
patch.CommunityID != nil || patch.RefreshIntervalSec != nil
}