commit e9cab59ada9510d07222b8f8219304c2a7577259 Author: Claude Date: Mon Feb 16 15:47:12 2026 +0000 feat: extract mining engine from Mining repo XMRig/TTMiner mining engine with profile management, stats collection, circuit breakers, event system, supervisor, and SQLite persistence. P2P node service stubbed (moved to core/go-p2p). Ported from github.com/Snider/Mining/pkg/{mining,database,logging} Co-Authored-By: Claude Opus 4.6 diff --git a/database/database.go b/database/database.go new file mode 100644 index 0000000..8251e7d --- /dev/null +++ b/database/database.go @@ -0,0 +1,184 @@ +package database + +import ( + "database/sql" + "fmt" + "os" + "path/filepath" + "sync" + "time" + + "github.com/adrg/xdg" + _ "github.com/mattn/go-sqlite3" +) + +// DB is the global database instance +var ( + db *sql.DB + dbMu sync.RWMutex +) + +// Config holds database configuration options +type Config struct { + // Enabled determines if database persistence is active + Enabled bool `json:"enabled"` + // Path is the database file path (optional, uses default if empty) + Path string `json:"path,omitempty"` + // RetentionDays is how long to keep historical data (default 30) + RetentionDays int `json:"retentionDays,omitempty"` +} + +// defaultConfig returns the default database configuration +func defaultConfig() Config { + return Config{ + Enabled: true, + Path: "", + RetentionDays: 30, + } +} + +// defaultDBPath returns the default database file path +func defaultDBPath() (string, error) { + dataDir := filepath.Join(xdg.DataHome, "lethean-desktop") + if err := os.MkdirAll(dataDir, 0755); err != nil { + return "", fmt.Errorf("failed to create data directory: %w", err) + } + return filepath.Join(dataDir, "mining.db"), nil +} + +// Initialize opens the database connection and creates tables +func Initialize(cfg Config) error { + dbMu.Lock() + defer dbMu.Unlock() + + if !cfg.Enabled { + return nil + } + + dbPath := cfg.Path + if dbPath == "" { + var err error + dbPath, err = defaultDBPath() + if err != nil { + return err + } + } + + var err error + db, err = sql.Open("sqlite3", dbPath+"?_journal=WAL&_timeout=5000") + if err != nil { + return fmt.Errorf("failed to open database: %w", err) + } + + // Set connection pool settings + db.SetMaxOpenConns(1) // SQLite only supports one writer + db.SetMaxIdleConns(1) + db.SetConnMaxLifetime(time.Hour) + + // Create tables + if err := createTables(); err != nil { + // Nil out global before closing to prevent use of closed connection + closingDB := db + db = nil + closingDB.Close() + return fmt.Errorf("failed to create tables: %w", err) + } + + return nil +} + +// Close closes the database connection +func Close() error { + dbMu.Lock() + defer dbMu.Unlock() + + if db == nil { + return nil + } + + err := db.Close() + db = nil + return err +} + +// isInitialized returns true if the database is ready +func isInitialized() bool { + dbMu.RLock() + defer dbMu.RUnlock() + return db != nil +} + +// createTables creates all required database tables +func createTables() error { + schema := ` + -- Hashrate history table for storing miner performance data + CREATE TABLE IF NOT EXISTS hashrate_history ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + miner_name TEXT NOT NULL, + miner_type TEXT NOT NULL, + timestamp DATETIME NOT NULL, + hashrate INTEGER NOT NULL, + resolution TEXT NOT NULL DEFAULT 'high', + created_at DATETIME DEFAULT CURRENT_TIMESTAMP + ); + + -- Index for efficient queries by miner and time range + CREATE INDEX IF NOT EXISTS idx_hashrate_miner_time + ON hashrate_history(miner_name, timestamp DESC); + + -- Index for cleanup queries + CREATE INDEX IF NOT EXISTS idx_hashrate_resolution_time + ON hashrate_history(resolution, timestamp); + + -- Miner sessions table for tracking uptime + CREATE TABLE IF NOT EXISTS miner_sessions ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + miner_name TEXT NOT NULL, + miner_type TEXT NOT NULL, + started_at DATETIME NOT NULL, + stopped_at DATETIME, + total_shares INTEGER DEFAULT 0, + rejected_shares INTEGER DEFAULT 0, + average_hashrate INTEGER DEFAULT 0 + ); + + -- Index for session queries + CREATE INDEX IF NOT EXISTS idx_sessions_miner + ON miner_sessions(miner_name, started_at DESC); + ` + + _, err := db.Exec(schema) + return err +} + +// Cleanup removes old data based on retention settings +func Cleanup(retentionDays int) error { + dbMu.RLock() + defer dbMu.RUnlock() + + if db == nil { + return nil + } + + cutoff := time.Now().AddDate(0, 0, -retentionDays) + + _, err := db.Exec(` + DELETE FROM hashrate_history + WHERE timestamp < ? + `, cutoff) + + return err +} + +// vacuumDB optimizes the database file size +func vacuumDB() error { + dbMu.RLock() + defer dbMu.RUnlock() + + if db == nil { + return nil + } + + _, err := db.Exec("VACUUM") + return err +} diff --git a/database/database_race_test.go b/database/database_race_test.go new file mode 100644 index 0000000..02ae0ce --- /dev/null +++ b/database/database_race_test.go @@ -0,0 +1,277 @@ +package database + +import ( + "os" + "path/filepath" + "sync" + "testing" + "time" +) + +// setupRaceTestDB creates a fresh database for race testing +func setupRaceTestDB(t *testing.T) func() { + tmpDir := t.TempDir() + dbPath := filepath.Join(tmpDir, "race_test.db") + + cfg := Config{ + Enabled: true, + Path: dbPath, + RetentionDays: 7, + } + + if err := Initialize(cfg); err != nil { + t.Fatalf("Failed to initialize database: %v", err) + } + + return func() { + Close() + os.Remove(dbPath) + } +} + +// TestConcurrentHashrateInserts verifies that concurrent inserts +// don't cause race conditions +func TestConcurrentHashrateInserts(t *testing.T) { + cleanup := setupRaceTestDB(t) + defer cleanup() + + var wg sync.WaitGroup + + // 10 goroutines inserting points concurrently + for i := 0; i < 10; i++ { + wg.Add(1) + go func(minerIndex int) { + defer wg.Done() + minerName := "miner" + string(rune('A'+minerIndex)) + minerType := "xmrig" + + for j := 0; j < 100; j++ { + point := HashratePoint{ + Timestamp: time.Now().Add(time.Duration(-j) * time.Second), + Hashrate: 1000 + minerIndex*100 + j, + } + err := InsertHashratePoint(nil, minerName, minerType, point, ResolutionHigh) + if err != nil { + t.Errorf("Insert error for %s: %v", minerName, err) + } + } + }(i) + } + + wg.Wait() + + // Verify data was inserted + for i := 0; i < 10; i++ { + minerName := "miner" + string(rune('A'+i)) + history, err := GetHashrateHistory(minerName, ResolutionHigh, time.Now().Add(-2*time.Minute), time.Now()) + if err != nil { + t.Errorf("Failed to get history for %s: %v", minerName, err) + } + if len(history) == 0 { + t.Errorf("Expected history for %s, got none", minerName) + } + } +} + +// TestConcurrentInsertAndQuery verifies that concurrent reads and writes +// don't cause race conditions +func TestConcurrentInsertAndQuery(t *testing.T) { + cleanup := setupRaceTestDB(t) + defer cleanup() + + var wg sync.WaitGroup + stop := make(chan struct{}) + + // Writer goroutine + wg.Add(1) + go func() { + defer wg.Done() + for i := 0; ; i++ { + select { + case <-stop: + return + default: + point := HashratePoint{ + Timestamp: time.Now(), + Hashrate: 1000 + i, + } + InsertHashratePoint(nil, "concurrent-test", "xmrig", point, ResolutionHigh) + time.Sleep(time.Millisecond) + } + } + }() + + // Multiple reader goroutines + for i := 0; i < 5; i++ { + wg.Add(1) + go func() { + defer wg.Done() + for j := 0; j < 50; j++ { + select { + case <-stop: + return + default: + GetHashrateHistory("concurrent-test", ResolutionHigh, time.Now().Add(-time.Hour), time.Now()) + time.Sleep(2 * time.Millisecond) + } + } + }() + } + + // Let it run for a bit + time.Sleep(200 * time.Millisecond) + close(stop) + wg.Wait() + + // Test passes if no race detector warnings +} + +// TestConcurrentInsertAndCleanup verifies that cleanup doesn't race +// with ongoing inserts +func TestConcurrentInsertAndCleanup(t *testing.T) { + cleanup := setupRaceTestDB(t) + defer cleanup() + + var wg sync.WaitGroup + stop := make(chan struct{}) + + // Continuous inserts + wg.Add(1) + go func() { + defer wg.Done() + for i := 0; ; i++ { + select { + case <-stop: + return + default: + // Insert some old data and some new data + oldPoint := HashratePoint{ + Timestamp: time.Now().AddDate(0, 0, -10), // 10 days old + Hashrate: 500 + i, + } + InsertHashratePoint(nil, "cleanup-test", "xmrig", oldPoint, ResolutionHigh) + + newPoint := HashratePoint{ + Timestamp: time.Now(), + Hashrate: 1000 + i, + } + InsertHashratePoint(nil, "cleanup-test", "xmrig", newPoint, ResolutionHigh) + time.Sleep(time.Millisecond) + } + } + }() + + // Periodic cleanup + wg.Add(1) + go func() { + defer wg.Done() + for i := 0; i < 10; i++ { + select { + case <-stop: + return + default: + Cleanup(7) // 7 day retention + time.Sleep(20 * time.Millisecond) + } + } + }() + + // Let it run + time.Sleep(200 * time.Millisecond) + close(stop) + wg.Wait() + + // Test passes if no race detector warnings +} + +// TestConcurrentStats verifies that GetHashrateStats can be called +// concurrently without race conditions +func TestConcurrentStats(t *testing.T) { + cleanup := setupRaceTestDB(t) + defer cleanup() + + // Insert some test data + minerName := "stats-test" + for i := 0; i < 100; i++ { + point := HashratePoint{ + Timestamp: time.Now().Add(time.Duration(-i) * time.Second), + Hashrate: 1000 + i*10, + } + InsertHashratePoint(nil, minerName, "xmrig", point, ResolutionHigh) + } + + var wg sync.WaitGroup + + // Multiple goroutines querying stats + for i := 0; i < 20; i++ { + wg.Add(1) + go func() { + defer wg.Done() + for j := 0; j < 50; j++ { + stats, err := GetHashrateStats(minerName) + if err != nil { + t.Errorf("Stats error: %v", err) + } + if stats != nil && stats.TotalPoints == 0 { + // This is fine, data might be in flux + } + } + }() + } + + wg.Wait() + + // Test passes if no race detector warnings +} + +// TestConcurrentGetAllStats verifies that GetAllMinerStats can be called +// concurrently without race conditions +func TestConcurrentGetAllStats(t *testing.T) { + cleanup := setupRaceTestDB(t) + defer cleanup() + + // Insert data for multiple miners + for m := 0; m < 5; m++ { + minerName := "all-stats-" + string(rune('A'+m)) + for i := 0; i < 50; i++ { + point := HashratePoint{ + Timestamp: time.Now().Add(time.Duration(-i) * time.Second), + Hashrate: 1000 + m*100 + i, + } + InsertHashratePoint(nil, minerName, "xmrig", point, ResolutionHigh) + } + } + + var wg sync.WaitGroup + + // Multiple goroutines querying all stats + for i := 0; i < 10; i++ { + wg.Add(1) + go func() { + defer wg.Done() + for j := 0; j < 30; j++ { + _, err := GetAllMinerStats() + if err != nil { + t.Errorf("GetAllMinerStats error: %v", err) + } + } + }() + } + + // Concurrent inserts + wg.Add(1) + go func() { + defer wg.Done() + for i := 0; i < 50; i++ { + point := HashratePoint{ + Timestamp: time.Now(), + Hashrate: 2000 + i, + } + InsertHashratePoint(nil, "all-stats-new", "xmrig", point, ResolutionHigh) + } + }() + + wg.Wait() + + // Test passes if no race detector warnings +} diff --git a/database/database_test.go b/database/database_test.go new file mode 100644 index 0000000..1068d57 --- /dev/null +++ b/database/database_test.go @@ -0,0 +1,497 @@ +package database + +import ( + "os" + "path/filepath" + "testing" + "time" +) + +func setupTestDB(t *testing.T) func() { + tmpDir := t.TempDir() + dbPath := filepath.Join(tmpDir, "test.db") + + cfg := Config{ + Enabled: true, + Path: dbPath, + RetentionDays: 7, + } + + if err := Initialize(cfg); err != nil { + t.Fatalf("Failed to initialize database: %v", err) + } + + return func() { + Close() + os.Remove(dbPath) + } +} + +func TestInitialize(t *testing.T) { + cleanup := setupTestDB(t) + defer cleanup() + + // Database should be initialized + dbMu.RLock() + initialized := db != nil + dbMu.RUnlock() + + if !initialized { + t.Error("Database should be initialized") + } +} + +func TestInitialize_Disabled(t *testing.T) { + cfg := Config{ + Enabled: false, + } + + if err := Initialize(cfg); err != nil { + t.Errorf("Initialize with disabled should not error: %v", err) + } +} + +func TestClose(t *testing.T) { + cleanup := setupTestDB(t) + defer cleanup() + + // Close should not error + if err := Close(); err != nil { + t.Errorf("Close failed: %v", err) + } +} + +func TestHashrateStorage(t *testing.T) { + cleanup := setupTestDB(t) + defer cleanup() + + // Store some hashrate data + minerName := "test-miner" + minerType := "xmrig" + now := time.Now() + + points := []HashratePoint{ + {Timestamp: now.Add(-5 * time.Minute), Hashrate: 1000}, + {Timestamp: now.Add(-4 * time.Minute), Hashrate: 1100}, + {Timestamp: now.Add(-3 * time.Minute), Hashrate: 1200}, + } + + for _, p := range points { + if err := InsertHashratePoint(nil, minerName, minerType, p, ResolutionHigh); err != nil { + t.Fatalf("Failed to store hashrate point: %v", err) + } + } + + // Retrieve the data + retrieved, err := GetHashrateHistory(minerName, ResolutionHigh, now.Add(-10*time.Minute), now) + if err != nil { + t.Fatalf("Failed to get hashrate history: %v", err) + } + + if len(retrieved) != 3 { + t.Errorf("Expected 3 points, got %d", len(retrieved)) + } +} + +func TestGetHashrateStats(t *testing.T) { + cleanup := setupTestDB(t) + defer cleanup() + + minerName := "stats-test-miner" + minerType := "xmrig" + now := time.Now() + + // Store some test data + points := []HashratePoint{ + {Timestamp: now.Add(-2 * time.Minute), Hashrate: 500}, + {Timestamp: now.Add(-1 * time.Minute), Hashrate: 1000}, + {Timestamp: now, Hashrate: 1500}, + } + + for _, p := range points { + if err := InsertHashratePoint(nil, minerName, minerType, p, ResolutionHigh); err != nil { + t.Fatalf("Failed to store point: %v", err) + } + } + + stats, err := GetHashrateStats(minerName) + if err != nil { + t.Fatalf("Failed to get stats: %v", err) + } + + if stats.TotalPoints != 3 { + t.Errorf("Expected 3 total points, got %d", stats.TotalPoints) + } + + // Average should be (500+1000+1500)/3 = 1000 + if stats.AverageRate != 1000 { + t.Errorf("Expected average rate 1000, got %d", stats.AverageRate) + } + + if stats.MaxRate != 1500 { + t.Errorf("Expected max rate 1500, got %d", stats.MaxRate) + } + + if stats.MinRate != 500 { + t.Errorf("Expected min rate 500, got %d", stats.MinRate) + } +} + +func TestDefaultConfig(t *testing.T) { + cfg := defaultConfig() + + if !cfg.Enabled { + t.Error("Default config should have Enabled=true") + } + + if cfg.RetentionDays != 30 { + t.Errorf("Expected default retention 30, got %d", cfg.RetentionDays) + } +} + +func TestCleanupRetention(t *testing.T) { + cleanup := setupTestDB(t) + defer cleanup() + + minerName := "retention-test" + minerType := "xmrig" + now := time.Now() + + // Insert data at various ages: + // - 35 days old (should be deleted with 30-day retention) + // - 25 days old (should be kept with 30-day retention) + // - 5 days old (should be kept) + oldPoint := HashratePoint{ + Timestamp: now.AddDate(0, 0, -35), + Hashrate: 100, + } + midPoint := HashratePoint{ + Timestamp: now.AddDate(0, 0, -25), + Hashrate: 200, + } + newPoint := HashratePoint{ + Timestamp: now.AddDate(0, 0, -5), + Hashrate: 300, + } + + // Insert all points + if err := InsertHashratePoint(nil, minerName, minerType, oldPoint, ResolutionHigh); err != nil { + t.Fatalf("Failed to insert old point: %v", err) + } + if err := InsertHashratePoint(nil, minerName, minerType, midPoint, ResolutionHigh); err != nil { + t.Fatalf("Failed to insert mid point: %v", err) + } + if err := InsertHashratePoint(nil, minerName, minerType, newPoint, ResolutionHigh); err != nil { + t.Fatalf("Failed to insert new point: %v", err) + } + + // Verify all 3 points exist + history, err := GetHashrateHistory(minerName, ResolutionHigh, now.AddDate(0, 0, -40), now) + if err != nil { + t.Fatalf("Failed to get history before cleanup: %v", err) + } + if len(history) != 3 { + t.Errorf("Expected 3 points before cleanup, got %d", len(history)) + } + + // Run cleanup with 30-day retention + if err := Cleanup(30); err != nil { + t.Fatalf("Cleanup failed: %v", err) + } + + // Verify only 2 points remain (35-day old point should be deleted) + history, err = GetHashrateHistory(minerName, ResolutionHigh, now.AddDate(0, 0, -40), now) + if err != nil { + t.Fatalf("Failed to get history after cleanup: %v", err) + } + if len(history) != 2 { + t.Errorf("Expected 2 points after cleanup, got %d", len(history)) + } + + // Verify the remaining points are the mid and new ones + for _, point := range history { + if point.Hashrate == 100 { + t.Error("Old point (100 H/s) should have been deleted") + } + } +} + +func TestGetHashrateHistoryTimeRange(t *testing.T) { + cleanup := setupTestDB(t) + defer cleanup() + + minerName := "timerange-test" + minerType := "xmrig" + now := time.Now() + + // Insert points at specific times + times := []time.Duration{ + -10 * time.Minute, + -8 * time.Minute, + -6 * time.Minute, + -4 * time.Minute, + -2 * time.Minute, + } + + for i, offset := range times { + point := HashratePoint{ + Timestamp: now.Add(offset), + Hashrate: 1000 + i*100, + } + if err := InsertHashratePoint(nil, minerName, minerType, point, ResolutionHigh); err != nil { + t.Fatalf("Failed to insert point: %v", err) + } + } + + // Query for middle range (should get 3 points: -8, -6, -4 minutes) + since := now.Add(-9 * time.Minute) + until := now.Add(-3 * time.Minute) + history, err := GetHashrateHistory(minerName, ResolutionHigh, since, until) + if err != nil { + t.Fatalf("Failed to get history: %v", err) + } + + if len(history) != 3 { + t.Errorf("Expected 3 points in range, got %d", len(history)) + } + + // Query boundary condition - exact timestamp match + exactSince := now.Add(-6 * time.Minute) + exactUntil := now.Add(-6 * time.Minute).Add(time.Second) + history, err = GetHashrateHistory(minerName, ResolutionHigh, exactSince, exactUntil) + if err != nil { + t.Fatalf("Failed to get exact history: %v", err) + } + + // Should get at least 1 point + if len(history) < 1 { + t.Error("Expected at least 1 point at exact boundary") + } +} + +func TestMultipleMinerStats(t *testing.T) { + cleanup := setupTestDB(t) + defer cleanup() + + now := time.Now() + + // Create data for multiple miners + miners := []struct { + name string + hashrates []int + }{ + {"miner-A", []int{1000, 1100, 1200}}, + {"miner-B", []int{2000, 2100, 2200}}, + {"miner-C", []int{3000, 3100, 3200}}, + } + + for _, m := range miners { + for i, hr := range m.hashrates { + point := HashratePoint{ + Timestamp: now.Add(time.Duration(-i) * time.Minute), + Hashrate: hr, + } + if err := InsertHashratePoint(nil, m.name, "xmrig", point, ResolutionHigh); err != nil { + t.Fatalf("Failed to insert point for %s: %v", m.name, err) + } + } + } + + // Get all miner stats + allStats, err := GetAllMinerStats() + if err != nil { + t.Fatalf("Failed to get all stats: %v", err) + } + + if len(allStats) != 3 { + t.Errorf("Expected stats for 3 miners, got %d", len(allStats)) + } + + // Verify each miner's stats + statsMap := make(map[string]HashrateStats) + for _, s := range allStats { + statsMap[s.MinerName] = s + } + + // Check miner-A: avg = (1000+1100+1200)/3 = 1100 + if s, ok := statsMap["miner-A"]; ok { + if s.AverageRate != 1100 { + t.Errorf("miner-A: expected avg 1100, got %d", s.AverageRate) + } + } else { + t.Error("miner-A stats not found") + } + + // Check miner-C: avg = (3000+3100+3200)/3 = 3100 + if s, ok := statsMap["miner-C"]; ok { + if s.AverageRate != 3100 { + t.Errorf("miner-C: expected avg 3100, got %d", s.AverageRate) + } + } else { + t.Error("miner-C stats not found") + } +} + +func TestIsInitialized(t *testing.T) { + // Before initialization + Close() // Ensure clean state + if isInitialized() { + t.Error("Should not be initialized before Initialize()") + } + + cleanup := setupTestDB(t) + defer cleanup() + + // After initialization + if !isInitialized() { + t.Error("Should be initialized after Initialize()") + } + + // After close + Close() + if isInitialized() { + t.Error("Should not be initialized after Close()") + } +} + +func TestSchemaCreation(t *testing.T) { + cleanup := setupTestDB(t) + defer cleanup() + + // Verify tables exist by querying sqlite_master + dbMu.RLock() + defer dbMu.RUnlock() + + // Check hashrate_history table + var tableName string + err := db.QueryRow("SELECT name FROM sqlite_master WHERE type='table' AND name='hashrate_history'").Scan(&tableName) + if err != nil { + t.Errorf("hashrate_history table should exist: %v", err) + } + + // Check miner_sessions table + err = db.QueryRow("SELECT name FROM sqlite_master WHERE type='table' AND name='miner_sessions'").Scan(&tableName) + if err != nil { + t.Errorf("miner_sessions table should exist: %v", err) + } + + // Verify indexes exist + var indexName string + err = db.QueryRow("SELECT name FROM sqlite_master WHERE type='index' AND name='idx_hashrate_miner_time'").Scan(&indexName) + if err != nil { + t.Errorf("idx_hashrate_miner_time index should exist: %v", err) + } + + err = db.QueryRow("SELECT name FROM sqlite_master WHERE type='index' AND name='idx_sessions_miner'").Scan(&indexName) + if err != nil { + t.Errorf("idx_sessions_miner index should exist: %v", err) + } +} + +func TestReInitializeExistingDB(t *testing.T) { + tmpDir := t.TempDir() + dbPath := filepath.Join(tmpDir, "reinit_test.db") + + cfg := Config{ + Enabled: true, + Path: dbPath, + RetentionDays: 7, + } + + // First initialization + if err := Initialize(cfg); err != nil { + t.Fatalf("First initialization failed: %v", err) + } + + // Insert some data + minerName := "reinit-test-miner" + point := HashratePoint{ + Timestamp: time.Now(), + Hashrate: 1234, + } + if err := InsertHashratePoint(nil, minerName, "xmrig", point, ResolutionHigh); err != nil { + t.Fatalf("Failed to insert point: %v", err) + } + + // Close and re-initialize (simulates app restart) + if err := Close(); err != nil { + t.Fatalf("Close failed: %v", err) + } + + // Re-initialize with same path + if err := Initialize(cfg); err != nil { + t.Fatalf("Re-initialization failed: %v", err) + } + defer func() { + Close() + os.Remove(dbPath) + }() + + // Verify data persisted + history, err := GetHashrateHistory(minerName, ResolutionHigh, time.Now().Add(-time.Hour), time.Now().Add(time.Hour)) + if err != nil { + t.Fatalf("Failed to get history after reinit: %v", err) + } + + if len(history) != 1 { + t.Errorf("Expected 1 point after reinit, got %d", len(history)) + } + + if len(history) > 0 && history[0].Hashrate != 1234 { + t.Errorf("Expected hashrate 1234, got %d", history[0].Hashrate) + } +} + +func TestConcurrentDatabaseAccess(t *testing.T) { + cleanup := setupTestDB(t) + defer cleanup() + + const numGoroutines = 10 + const numOpsPerGoroutine = 20 + + done := make(chan bool, numGoroutines) + errors := make(chan error, numGoroutines*numOpsPerGoroutine) + + now := time.Now() + + // Launch multiple goroutines doing concurrent reads/writes + for i := 0; i < numGoroutines; i++ { + go func(id int) { + minerName := "concurrent-miner-" + string(rune('A'+id)) + for j := 0; j < numOpsPerGoroutine; j++ { + // Write + point := HashratePoint{ + Timestamp: now.Add(time.Duration(-j) * time.Second), + Hashrate: 1000 + j, + } + if err := InsertHashratePoint(nil, minerName, "xmrig", point, ResolutionHigh); err != nil { + errors <- err + } + + // Read + _, err := GetHashrateHistory(minerName, ResolutionHigh, now.Add(-time.Hour), now) + if err != nil { + errors <- err + } + } + done <- true + }(i) + } + + // Wait for all goroutines + for i := 0; i < numGoroutines; i++ { + <-done + } + close(errors) + + // Check for errors + var errCount int + for err := range errors { + t.Errorf("Concurrent access error: %v", err) + errCount++ + } + + if errCount > 0 { + t.Errorf("Got %d errors during concurrent access", errCount) + } +} diff --git a/database/hashrate.go b/database/hashrate.go new file mode 100644 index 0000000..ae4e8ba --- /dev/null +++ b/database/hashrate.go @@ -0,0 +1,233 @@ +package database + +import ( + "context" + "fmt" + "time" + + "forge.lthn.ai/core/mining/logging" +) + +// parseSQLiteTimestamp parses timestamp strings from SQLite which may use various formats. +// Logs a warning if parsing fails and returns zero time. +func parseSQLiteTimestamp(s string) time.Time { + if s == "" { + return time.Time{} + } + + // Try common SQLite timestamp formats + formats := []string{ + "2006-01-02 15:04:05.999999999-07:00", + time.RFC3339Nano, + time.RFC3339, + "2006-01-02 15:04:05", + "2006-01-02T15:04:05Z", + } + + for _, format := range formats { + if t, err := time.Parse(format, s); err == nil { + return t + } + } + + logging.Warn("failed to parse timestamp from database", logging.Fields{"timestamp": s}) + return time.Time{} +} + +// Resolution indicates the data resolution type +type Resolution string + +const ( + ResolutionHigh Resolution = "high" // 10-second intervals + ResolutionLow Resolution = "low" // 1-minute averages +) + +// HashratePoint represents a single hashrate measurement +type HashratePoint struct { + Timestamp time.Time `json:"timestamp"` + Hashrate int `json:"hashrate"` +} + +// dbInsertTimeout is the maximum time to wait for a database insert operation +const dbInsertTimeout = 5 * time.Second + +// InsertHashratePoint stores a hashrate measurement in the database. +// If ctx is nil, a default timeout context will be used. +func InsertHashratePoint(ctx context.Context, minerName, minerType string, point HashratePoint, resolution Resolution) error { + dbMu.RLock() + defer dbMu.RUnlock() + + if db == nil { + return nil // DB not enabled, silently skip + } + + // Use provided context or create one with default timeout + if ctx == nil { + var cancel context.CancelFunc + ctx, cancel = context.WithTimeout(context.Background(), dbInsertTimeout) + defer cancel() + } + + _, err := db.ExecContext(ctx, ` + INSERT INTO hashrate_history (miner_name, miner_type, timestamp, hashrate, resolution) + VALUES (?, ?, ?, ?, ?) + `, minerName, minerType, point.Timestamp, point.Hashrate, string(resolution)) + + return err +} + +// GetHashrateHistory retrieves hashrate history for a miner within a time range +func GetHashrateHistory(minerName string, resolution Resolution, since, until time.Time) ([]HashratePoint, error) { + dbMu.RLock() + defer dbMu.RUnlock() + + if db == nil { + return nil, nil + } + + rows, err := db.Query(` + SELECT timestamp, hashrate + FROM hashrate_history + WHERE miner_name = ? + AND resolution = ? + AND timestamp >= ? + AND timestamp <= ? + ORDER BY timestamp ASC + `, minerName, string(resolution), since, until) + if err != nil { + return nil, fmt.Errorf("failed to query hashrate history: %w", err) + } + defer rows.Close() + + var points []HashratePoint + for rows.Next() { + var point HashratePoint + if err := rows.Scan(&point.Timestamp, &point.Hashrate); err != nil { + return nil, fmt.Errorf("failed to scan row: %w", err) + } + points = append(points, point) + } + + return points, rows.Err() +} + +// GetHashrateStats retrieves aggregated stats for a miner +type HashrateStats struct { + MinerName string `json:"minerName"` + TotalPoints int `json:"totalPoints"` + AverageRate int `json:"averageRate"` + MaxRate int `json:"maxRate"` + MinRate int `json:"minRate"` + FirstSeen time.Time `json:"firstSeen"` + LastSeen time.Time `json:"lastSeen"` +} + +func GetHashrateStats(minerName string) (*HashrateStats, error) { + dbMu.RLock() + defer dbMu.RUnlock() + + if db == nil { + return nil, nil + } + + // First check if there are any rows for this miner + var count int + err := db.QueryRow(`SELECT COUNT(*) FROM hashrate_history WHERE miner_name = ?`, minerName).Scan(&count) + if err != nil { + return nil, err + } + + // No data for this miner + if count == 0 { + return nil, nil + } + + var stats HashrateStats + stats.MinerName = minerName + + // SQLite returns timestamps as strings and AVG as float64, so scan them appropriately + var firstSeenStr, lastSeenStr string + var avgRate float64 + err = db.QueryRow(` + SELECT + COUNT(*), + COALESCE(AVG(hashrate), 0), + COALESCE(MAX(hashrate), 0), + COALESCE(MIN(hashrate), 0), + MIN(timestamp), + MAX(timestamp) + FROM hashrate_history + WHERE miner_name = ? + `, minerName).Scan( + &stats.TotalPoints, + &avgRate, + &stats.MaxRate, + &stats.MinRate, + &firstSeenStr, + &lastSeenStr, + ) + stats.AverageRate = int(avgRate) + + if err != nil { + return nil, err + } + + // Parse timestamps using helper that logs errors + stats.FirstSeen = parseSQLiteTimestamp(firstSeenStr) + stats.LastSeen = parseSQLiteTimestamp(lastSeenStr) + + return &stats, nil +} + +// GetAllMinerStats retrieves stats for all miners +func GetAllMinerStats() ([]HashrateStats, error) { + dbMu.RLock() + defer dbMu.RUnlock() + + if db == nil { + return nil, nil + } + + rows, err := db.Query(` + SELECT + miner_name, + COUNT(*), + COALESCE(AVG(hashrate), 0), + COALESCE(MAX(hashrate), 0), + COALESCE(MIN(hashrate), 0), + MIN(timestamp), + MAX(timestamp) + FROM hashrate_history + GROUP BY miner_name + ORDER BY miner_name + `) + if err != nil { + return nil, err + } + defer rows.Close() + + var allStats []HashrateStats + for rows.Next() { + var stats HashrateStats + var firstSeenStr, lastSeenStr string + var avgRate float64 + if err := rows.Scan( + &stats.MinerName, + &stats.TotalPoints, + &avgRate, + &stats.MaxRate, + &stats.MinRate, + &firstSeenStr, + &lastSeenStr, + ); err != nil { + return nil, err + } + stats.AverageRate = int(avgRate) + // Parse timestamps using helper that logs errors + stats.FirstSeen = parseSQLiteTimestamp(firstSeenStr) + stats.LastSeen = parseSQLiteTimestamp(lastSeenStr) + allStats = append(allStats, stats) + } + + return allStats, rows.Err() +} diff --git a/database/interface.go b/database/interface.go new file mode 100644 index 0000000..0312687 --- /dev/null +++ b/database/interface.go @@ -0,0 +1,95 @@ +package database + +import ( + "context" + "time" +) + +// HashrateStore defines the interface for hashrate data persistence. +// This interface allows for dependency injection and easier testing. +type HashrateStore interface { + // InsertHashratePoint stores a hashrate measurement. + // If ctx is nil, a default timeout will be used. + InsertHashratePoint(ctx context.Context, minerName, minerType string, point HashratePoint, resolution Resolution) error + + // GetHashrateHistory retrieves hashrate history for a miner within a time range. + GetHashrateHistory(minerName string, resolution Resolution, since, until time.Time) ([]HashratePoint, error) + + // GetHashrateStats retrieves aggregated statistics for a specific miner. + GetHashrateStats(minerName string) (*HashrateStats, error) + + // GetAllMinerStats retrieves statistics for all miners. + GetAllMinerStats() ([]HashrateStats, error) + + // Cleanup removes old data based on retention settings. + Cleanup(retentionDays int) error + + // Close closes the store and releases resources. + Close() error +} + +// defaultStore implements HashrateStore using the global database connection. +// This provides backward compatibility while allowing interface-based usage. +type defaultStore struct{} + +// DefaultStore returns a HashrateStore that uses the global database connection. +// This is useful for gradual migration from package-level functions to interface-based usage. +func DefaultStore() HashrateStore { + return &defaultStore{} +} + +func (s *defaultStore) InsertHashratePoint(ctx context.Context, minerName, minerType string, point HashratePoint, resolution Resolution) error { + return InsertHashratePoint(ctx, minerName, minerType, point, resolution) +} + +func (s *defaultStore) GetHashrateHistory(minerName string, resolution Resolution, since, until time.Time) ([]HashratePoint, error) { + return GetHashrateHistory(minerName, resolution, since, until) +} + +func (s *defaultStore) GetHashrateStats(minerName string) (*HashrateStats, error) { + return GetHashrateStats(minerName) +} + +func (s *defaultStore) GetAllMinerStats() ([]HashrateStats, error) { + return GetAllMinerStats() +} + +func (s *defaultStore) Cleanup(retentionDays int) error { + return Cleanup(retentionDays) +} + +func (s *defaultStore) Close() error { + return Close() +} + +// NopStore returns a HashrateStore that does nothing. +// Useful for testing or when database is disabled. +func NopStore() HashrateStore { + return &nopStore{} +} + +type nopStore struct{} + +func (s *nopStore) InsertHashratePoint(ctx context.Context, minerName, minerType string, point HashratePoint, resolution Resolution) error { + return nil +} + +func (s *nopStore) GetHashrateHistory(minerName string, resolution Resolution, since, until time.Time) ([]HashratePoint, error) { + return nil, nil +} + +func (s *nopStore) GetHashrateStats(minerName string) (*HashrateStats, error) { + return nil, nil +} + +func (s *nopStore) GetAllMinerStats() ([]HashrateStats, error) { + return nil, nil +} + +func (s *nopStore) Cleanup(retentionDays int) error { + return nil +} + +func (s *nopStore) Close() error { + return nil +} diff --git a/database/interface_test.go b/database/interface_test.go new file mode 100644 index 0000000..dff17f1 --- /dev/null +++ b/database/interface_test.go @@ -0,0 +1,204 @@ +package database + +import ( + "context" + "testing" + "time" +) + +func TestDefaultStore(t *testing.T) { + cleanup := setupTestDB(t) + defer cleanup() + + store := DefaultStore() + + // Test InsertHashratePoint + point := HashratePoint{ + Timestamp: time.Now(), + Hashrate: 1500, + } + if err := store.InsertHashratePoint(nil, "interface-test", "xmrig", point, ResolutionHigh); err != nil { + t.Fatalf("InsertHashratePoint failed: %v", err) + } + + // Test GetHashrateHistory + history, err := store.GetHashrateHistory("interface-test", ResolutionHigh, time.Now().Add(-time.Hour), time.Now().Add(time.Hour)) + if err != nil { + t.Fatalf("GetHashrateHistory failed: %v", err) + } + if len(history) != 1 { + t.Errorf("Expected 1 point, got %d", len(history)) + } + + // Test GetHashrateStats + stats, err := store.GetHashrateStats("interface-test") + if err != nil { + t.Fatalf("GetHashrateStats failed: %v", err) + } + if stats == nil { + t.Fatal("Expected non-nil stats") + } + if stats.TotalPoints != 1 { + t.Errorf("Expected 1 total point, got %d", stats.TotalPoints) + } + + // Test GetAllMinerStats + allStats, err := store.GetAllMinerStats() + if err != nil { + t.Fatalf("GetAllMinerStats failed: %v", err) + } + if len(allStats) != 1 { + t.Errorf("Expected 1 miner in stats, got %d", len(allStats)) + } + + // Test Cleanup + if err := store.Cleanup(30); err != nil { + t.Fatalf("Cleanup failed: %v", err) + } +} + +func TestDefaultStore_WithContext(t *testing.T) { + cleanup := setupTestDB(t) + defer cleanup() + + store := DefaultStore() + ctx := context.Background() + + point := HashratePoint{ + Timestamp: time.Now(), + Hashrate: 2000, + } + if err := store.InsertHashratePoint(ctx, "ctx-test", "xmrig", point, ResolutionHigh); err != nil { + t.Fatalf("InsertHashratePoint with context failed: %v", err) + } + + history, err := store.GetHashrateHistory("ctx-test", ResolutionHigh, time.Now().Add(-time.Hour), time.Now().Add(time.Hour)) + if err != nil { + t.Fatalf("GetHashrateHistory failed: %v", err) + } + if len(history) != 1 { + t.Errorf("Expected 1 point, got %d", len(history)) + } +} + +func TestNopStore(t *testing.T) { + store := NopStore() + + // All operations should succeed without error + point := HashratePoint{ + Timestamp: time.Now(), + Hashrate: 1000, + } + if err := store.InsertHashratePoint(nil, "test", "xmrig", point, ResolutionHigh); err != nil { + t.Errorf("NopStore InsertHashratePoint should not error: %v", err) + } + + history, err := store.GetHashrateHistory("test", ResolutionHigh, time.Now().Add(-time.Hour), time.Now()) + if err != nil { + t.Errorf("NopStore GetHashrateHistory should not error: %v", err) + } + if history != nil { + t.Errorf("NopStore GetHashrateHistory should return nil, got %v", history) + } + + stats, err := store.GetHashrateStats("test") + if err != nil { + t.Errorf("NopStore GetHashrateStats should not error: %v", err) + } + if stats != nil { + t.Errorf("NopStore GetHashrateStats should return nil, got %v", stats) + } + + allStats, err := store.GetAllMinerStats() + if err != nil { + t.Errorf("NopStore GetAllMinerStats should not error: %v", err) + } + if allStats != nil { + t.Errorf("NopStore GetAllMinerStats should return nil, got %v", allStats) + } + + if err := store.Cleanup(30); err != nil { + t.Errorf("NopStore Cleanup should not error: %v", err) + } + + if err := store.Close(); err != nil { + t.Errorf("NopStore Close should not error: %v", err) + } +} + +// TestInterfaceCompatibility ensures all implementations satisfy HashrateStore +func TestInterfaceCompatibility(t *testing.T) { + var _ HashrateStore = DefaultStore() + var _ HashrateStore = NopStore() + var _ HashrateStore = &defaultStore{} + var _ HashrateStore = &nopStore{} +} + +func TestDefaultStore_ContextCancellation(t *testing.T) { + cleanup := setupTestDB(t) + defer cleanup() + + store := DefaultStore() + + // Create a cancelled context + ctx, cancel := context.WithCancel(context.Background()) + cancel() + + point := HashratePoint{ + Timestamp: time.Now(), + Hashrate: 1000, + } + + // Insert with cancelled context should fail + err := store.InsertHashratePoint(ctx, "cancel-test", "xmrig", point, ResolutionHigh) + if err == nil { + t.Log("InsertHashratePoint with cancelled context succeeded (SQLite may not check context)") + } else { + t.Logf("InsertHashratePoint with cancelled context: %v (expected)", err) + } +} + +func TestDefaultStore_ContextTimeout(t *testing.T) { + cleanup := setupTestDB(t) + defer cleanup() + + store := DefaultStore() + + // Create a context that expires very quickly + ctx, cancel := context.WithTimeout(context.Background(), 1*time.Nanosecond) + defer cancel() + + // Wait for timeout to expire + time.Sleep(1 * time.Millisecond) + + point := HashratePoint{ + Timestamp: time.Now(), + Hashrate: 1000, + } + + // Insert with expired context + err := store.InsertHashratePoint(ctx, "timeout-test", "xmrig", point, ResolutionHigh) + if err == nil { + t.Log("InsertHashratePoint with expired context succeeded (SQLite may not check context)") + } else { + t.Logf("InsertHashratePoint with expired context: %v (expected)", err) + } +} + +func TestNopStore_WithContext(t *testing.T) { + store := NopStore() + + // NopStore should work with any context, including cancelled ones + ctx, cancel := context.WithCancel(context.Background()) + cancel() + + point := HashratePoint{ + Timestamp: time.Now(), + Hashrate: 1000, + } + + // Should still succeed (nop store ignores context) + if err := store.InsertHashratePoint(ctx, "nop-cancel-test", "xmrig", point, ResolutionHigh); err != nil { + t.Errorf("NopStore should succeed even with cancelled context: %v", err) + } +} diff --git a/database/session.go b/database/session.go new file mode 100644 index 0000000..747b7ff --- /dev/null +++ b/database/session.go @@ -0,0 +1,5 @@ +package database + +// This file previously contained session tracking functions. +// Session tracking is not currently integrated into the mining manager. +// The database schema still supports sessions for future use. diff --git a/docs/docs.go b/docs/docs.go new file mode 100644 index 0000000..81a5975 --- /dev/null +++ b/docs/docs.go @@ -0,0 +1,946 @@ +// Package docs Code generated by swaggo/swag. DO NOT EDIT +package docs + +import "github.com/swaggo/swag" + +const docTemplate = `{ + "schemes": {{ marshal .Schemes }}, + "swagger": "2.0", + "info": { + "description": "{{escape .Description}}", + "title": "{{.Title}}", + "contact": {}, + "version": "{{.Version}}" + }, + "host": "{{.Host}}", + "basePath": "{{.BasePath}}", + "paths": { + "/doctor": { + "post": { + "description": "Performs a live check on all available miners to verify their installation status, version, and path.", + "produces": [ + "application/json" + ], + "tags": [ + "system" + ], + "summary": "Check miner installations", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/mining.SystemInfo" + } + } + } + } + }, + "/info": { + "get": { + "description": "Retrieves live installation details for all miners, along with system information.", + "produces": [ + "application/json" + ], + "tags": [ + "system" + ], + "summary": "Get live miner installation information", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/mining.SystemInfo" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + } + } + }, + "/miners": { + "get": { + "description": "Get a list of all running miners", + "produces": [ + "application/json" + ], + "tags": [ + "miners" + ], + "summary": "List all running miners", + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/mining.XMRigMiner" + } + } + } + } + } + }, + "/miners/available": { + "get": { + "description": "Get a list of all available miners", + "produces": [ + "application/json" + ], + "tags": [ + "miners" + ], + "summary": "List all available miners", + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/mining.AvailableMiner" + } + } + } + } + } + }, + "/miners/{miner_name}": { + "delete": { + "description": "Stop a running miner by its name", + "produces": [ + "application/json" + ], + "tags": [ + "miners" + ], + "summary": "Stop a running miner", + "parameters": [ + { + "type": "string", + "description": "Miner Name", + "name": "miner_name", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + } + } + }, + "/miners/{miner_name}/hashrate-history": { + "get": { + "description": "Get historical hashrate data for a running miner", + "produces": [ + "application/json" + ], + "tags": [ + "miners" + ], + "summary": "Get miner hashrate history", + "parameters": [ + { + "type": "string", + "description": "Miner Name", + "name": "miner_name", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/mining.HashratePoint" + } + } + } + } + } + }, + "/miners/{miner_name}/logs": { + "get": { + "description": "Get the captured stdout/stderr output from a running miner", + "produces": [ + "application/json" + ], + "tags": [ + "miners" + ], + "summary": "Get miner log output", + "parameters": [ + { + "type": "string", + "description": "Miner Name", + "name": "miner_name", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "type": "string" + } + } + } + } + } + }, + "/miners/{miner_name}/stats": { + "get": { + "description": "Get statistics for a running miner", + "produces": [ + "application/json" + ], + "tags": [ + "miners" + ], + "summary": "Get miner stats", + "parameters": [ + { + "type": "string", + "description": "Miner Name", + "name": "miner_name", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/mining.PerformanceMetrics" + } + } + } + } + }, + "/miners/{miner_type}/install": { + "post": { + "description": "Install a new miner or update an existing one.", + "produces": [ + "application/json" + ], + "tags": [ + "miners" + ], + "summary": "Install or update a miner", + "parameters": [ + { + "type": "string", + "description": "Miner Type to install/update", + "name": "miner_type", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + } + } + }, + "/miners/{miner_type}/uninstall": { + "delete": { + "description": "Removes all files for a specific miner.", + "produces": [ + "application/json" + ], + "tags": [ + "miners" + ], + "summary": "Uninstall a miner", + "parameters": [ + { + "type": "string", + "description": "Miner Type to uninstall", + "name": "miner_type", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + } + } + }, + "/profiles": { + "get": { + "description": "Get a list of all saved mining profiles", + "produces": [ + "application/json" + ], + "tags": [ + "profiles" + ], + "summary": "List all mining profiles", + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/mining.MiningProfile" + } + } + } + } + }, + "post": { + "description": "Create and save a new mining profile", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "profiles" + ], + "summary": "Create a new mining profile", + "parameters": [ + { + "description": "Mining Profile", + "name": "profile", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/mining.MiningProfile" + } + } + ], + "responses": { + "201": { + "description": "Created", + "schema": { + "$ref": "#/definitions/mining.MiningProfile" + } + } + } + } + }, + "/profiles/{id}": { + "get": { + "description": "Get a mining profile by its ID", + "produces": [ + "application/json" + ], + "tags": [ + "profiles" + ], + "summary": "Get a specific mining profile", + "parameters": [ + { + "type": "string", + "description": "Profile ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/mining.MiningProfile" + } + } + } + }, + "put": { + "description": "Update an existing mining profile", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "profiles" + ], + "summary": "Update a mining profile", + "parameters": [ + { + "type": "string", + "description": "Profile ID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "Updated Mining Profile", + "name": "profile", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/mining.MiningProfile" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/mining.MiningProfile" + } + } + } + }, + "delete": { + "description": "Delete a mining profile by its ID", + "produces": [ + "application/json" + ], + "tags": [ + "profiles" + ], + "summary": "Delete a mining profile", + "parameters": [ + { + "type": "string", + "description": "Profile ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + } + } + }, + "/profiles/{id}/start": { + "post": { + "description": "Start a new miner with the configuration from a saved profile", + "produces": [ + "application/json" + ], + "tags": [ + "profiles" + ], + "summary": "Start a new miner using a profile", + "parameters": [ + { + "type": "string", + "description": "Profile ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/mining.XMRigMiner" + } + } + } + } + }, + "/update": { + "post": { + "description": "Checks if any installed miners have a new version available for download.", + "produces": [ + "application/json" + ], + "tags": [ + "system" + ], + "summary": "Check for miner updates", + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + } + } + } + }, + "definitions": { + "mining.API": { + "type": "object", + "properties": { + "enabled": { + "type": "boolean" + }, + "listenHost": { + "type": "string" + }, + "listenPort": { + "type": "integer" + } + } + }, + "mining.AvailableMiner": { + "type": "object", + "properties": { + "description": { + "type": "string" + }, + "name": { + "type": "string" + } + } + }, + "mining.HashratePoint": { + "type": "object", + "properties": { + "hashrate": { + "type": "integer" + }, + "timestamp": { + "type": "string" + } + } + }, + "mining.InstallationDetails": { + "type": "object", + "properties": { + "config_path": { + "description": "Add path to the miner-specific config", + "type": "string" + }, + "is_installed": { + "type": "boolean" + }, + "miner_binary": { + "type": "string" + }, + "path": { + "type": "string" + }, + "version": { + "type": "string" + } + } + }, + "mining.MiningProfile": { + "type": "object", + "properties": { + "config": { + "description": "The raw JSON config for the specific miner", + "type": "object" + }, + "id": { + "type": "string" + }, + "minerType": { + "description": "e.g., \"xmrig\", \"ttminer\"", + "type": "string" + }, + "name": { + "type": "string" + } + } + }, + "mining.PerformanceMetrics": { + "type": "object", + "properties": { + "algorithm": { + "type": "string" + }, + "extraData": { + "type": "object", + "additionalProperties": true + }, + "hashrate": { + "type": "integer" + }, + "lastShare": { + "type": "integer" + }, + "rejected": { + "type": "integer" + }, + "shares": { + "type": "integer" + }, + "uptime": { + "type": "integer" + } + } + }, + "mining.SystemInfo": { + "type": "object", + "properties": { + "architecture": { + "type": "string" + }, + "available_cpu_cores": { + "type": "integer" + }, + "go_version": { + "type": "string" + }, + "installed_miners_info": { + "type": "array", + "items": { + "$ref": "#/definitions/mining.InstallationDetails" + } + }, + "os": { + "type": "string" + }, + "timestamp": { + "type": "string" + }, + "total_system_ram_gb": { + "type": "number" + } + } + }, + "mining.XMRigMiner": { + "type": "object", + "properties": { + "api": { + "$ref": "#/definitions/mining.API" + }, + "configPath": { + "type": "string" + }, + "full_stats": { + "$ref": "#/definitions/mining.XMRigSummary" + }, + "hashrateHistory": { + "type": "array", + "items": { + "$ref": "#/definitions/mining.HashratePoint" + } + }, + "lowResHashrateHistory": { + "type": "array", + "items": { + "$ref": "#/definitions/mining.HashratePoint" + } + }, + "miner_binary": { + "type": "string" + }, + "name": { + "type": "string" + }, + "path": { + "type": "string" + }, + "running": { + "type": "boolean" + }, + "url": { + "type": "string" + }, + "version": { + "type": "string" + } + } + }, + "mining.XMRigSummary": { + "type": "object", + "properties": { + "algo": { + "type": "string" + }, + "algorithms": { + "type": "array", + "items": { + "type": "string" + } + }, + "connection": { + "type": "object", + "properties": { + "accepted": { + "type": "integer" + }, + "algo": { + "type": "string" + }, + "avg_time": { + "type": "integer" + }, + "avg_time_ms": { + "type": "integer" + }, + "diff": { + "type": "integer" + }, + "failures": { + "type": "integer" + }, + "hashes_total": { + "type": "integer" + }, + "ip": { + "type": "string" + }, + "ping": { + "type": "integer" + }, + "pool": { + "type": "string" + }, + "rejected": { + "type": "integer" + }, + "tls": { + "type": "string" + }, + "tls-fingerprint": { + "type": "string" + }, + "uptime": { + "type": "integer" + }, + "uptime_ms": { + "type": "integer" + } + } + }, + "cpu": { + "type": "object", + "properties": { + "64_bit": { + "type": "boolean" + }, + "aes": { + "type": "boolean" + }, + "arch": { + "type": "string" + }, + "assembly": { + "type": "string" + }, + "avx2": { + "type": "boolean" + }, + "backend": { + "type": "string" + }, + "brand": { + "type": "string" + }, + "cores": { + "type": "integer" + }, + "family": { + "type": "integer" + }, + "flags": { + "type": "array", + "items": { + "type": "string" + } + }, + "l2": { + "type": "integer" + }, + "l3": { + "type": "integer" + }, + "model": { + "type": "integer" + }, + "msr": { + "type": "string" + }, + "nodes": { + "type": "integer" + }, + "packages": { + "type": "integer" + }, + "proc_info": { + "type": "integer" + }, + "stepping": { + "type": "integer" + }, + "threads": { + "type": "integer" + }, + "x64": { + "type": "boolean" + } + } + }, + "donate_level": { + "type": "integer" + }, + "features": { + "type": "array", + "items": { + "type": "string" + } + }, + "hashrate": { + "type": "object", + "properties": { + "highest": { + "type": "number" + }, + "total": { + "type": "array", + "items": { + "type": "number" + } + } + } + }, + "hugepages": { + "type": "array", + "items": { + "type": "integer" + } + }, + "id": { + "type": "string" + }, + "kind": { + "type": "string" + }, + "paused": { + "type": "boolean" + }, + "resources": { + "type": "object", + "properties": { + "hardware_concurrency": { + "type": "integer" + }, + "load_average": { + "type": "array", + "items": { + "type": "number" + } + }, + "memory": { + "type": "object", + "properties": { + "free": { + "type": "integer" + }, + "resident_set_memory": { + "type": "integer" + }, + "total": { + "type": "integer" + } + } + } + } + }, + "restricted": { + "type": "boolean" + }, + "results": { + "type": "object", + "properties": { + "avg_time": { + "type": "integer" + }, + "avg_time_ms": { + "type": "integer" + }, + "best": { + "type": "array", + "items": { + "type": "integer" + } + }, + "diff_current": { + "type": "integer" + }, + "hashes_total": { + "type": "integer" + }, + "shares_good": { + "type": "integer" + }, + "shares_total": { + "type": "integer" + } + } + }, + "ua": { + "type": "string" + }, + "uptime": { + "type": "integer" + }, + "version": { + "type": "string" + }, + "worker_id": { + "type": "string" + } + } + } + } +}` + +// SwaggerInfo holds exported Swagger Info so clients can modify it +var SwaggerInfo = &swag.Spec{ + Version: "1.0", + Host: "localhost:8080", + BasePath: "/api/v1/mining", + Schemes: []string{}, + Title: "Mining API", + Description: "This is a sample server for a mining application.", + InfoInstanceName: "swagger", + SwaggerTemplate: docTemplate, + LeftDelim: "{{", + RightDelim: "}}", +} + +func init() { + swag.Register(SwaggerInfo.InstanceName(), SwaggerInfo) +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..72e0ad8 --- /dev/null +++ b/go.mod @@ -0,0 +1,70 @@ +module forge.lthn.ai/core/mining + +go 1.25.5 + +require ( + github.com/Masterminds/semver/v3 v3.4.0 + github.com/adrg/xdg v0.5.3 + github.com/ckanthony/gin-mcp v0.0.0-20251107113615-3c631c4fa9f4 + github.com/gin-contrib/cors v1.7.6 + github.com/gin-gonic/gin v1.11.0 + github.com/google/uuid v1.6.0 + github.com/gorilla/websocket v1.5.3 + github.com/mattn/go-sqlite3 v1.14.34 + github.com/shirou/gopsutil/v4 v4.26.1 + github.com/swaggo/files v1.0.1 + github.com/swaggo/gin-swagger v1.6.1 + github.com/swaggo/swag v1.16.6 +) + +require ( + github.com/KyleBanks/depth v1.2.1 // indirect + github.com/PuerkitoBio/purell v1.1.1 // indirect + github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 // indirect + github.com/bytedance/sonic v1.14.0 // indirect + github.com/bytedance/sonic/loader v0.3.0 // indirect + github.com/cloudwego/base64x v0.1.6 // indirect + github.com/ebitengine/purego v0.9.1 // indirect + github.com/gabriel-vasile/mimetype v1.4.9 // indirect + github.com/gin-contrib/sse v1.1.0 // indirect + github.com/go-ole/go-ole v1.2.6 // indirect + github.com/go-openapi/jsonpointer v0.19.5 // indirect + github.com/go-openapi/jsonreference v0.19.6 // indirect + github.com/go-openapi/spec v0.20.4 // indirect + github.com/go-openapi/swag v0.19.15 // indirect + github.com/go-playground/locales v0.14.1 // indirect + github.com/go-playground/universal-translator v0.18.1 // indirect + github.com/go-playground/validator/v10 v10.27.0 // indirect + github.com/goccy/go-json v0.10.5 // indirect + github.com/goccy/go-yaml v1.18.0 // indirect + github.com/josharian/intern v1.0.0 // indirect + github.com/json-iterator/go v1.1.12 // indirect + github.com/klauspost/cpuid/v2 v2.3.0 // indirect + github.com/leodido/go-urn v1.4.0 // indirect + github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect + github.com/mailru/easyjson v0.7.6 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect + github.com/modern-go/reflect2 v1.0.2 // indirect + github.com/pelletier/go-toml/v2 v2.2.4 // indirect + github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect + github.com/quic-go/qpack v0.5.1 // indirect + github.com/quic-go/quic-go v0.54.0 // indirect + github.com/sirupsen/logrus v1.9.3 // indirect + github.com/tklauser/go-sysconf v0.3.16 // indirect + github.com/tklauser/numcpus v0.11.0 // indirect + github.com/twitchyliquid64/golang-asm v0.15.1 // indirect + github.com/ugorji/go/codec v1.3.0 // indirect + github.com/yusufpapurcu/wmi v1.2.4 // indirect + go.uber.org/mock v0.5.0 // indirect + golang.org/x/arch v0.20.0 // indirect + golang.org/x/crypto v0.40.0 // indirect + golang.org/x/mod v0.25.0 // indirect + golang.org/x/net v0.42.0 // indirect + golang.org/x/sync v0.16.0 // indirect + golang.org/x/sys v0.40.0 // indirect + golang.org/x/text v0.27.0 // indirect + golang.org/x/tools v0.34.0 // indirect + google.golang.org/protobuf v1.36.9 // indirect + gopkg.in/yaml.v2 v2.4.0 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..3606913 --- /dev/null +++ b/go.sum @@ -0,0 +1,207 @@ +github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc= +github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE= +github.com/Masterminds/semver/v3 v3.4.0 h1:Zog+i5UMtVoCU8oKka5P7i9q9HgrJeGzI9SA1Xbatp0= +github.com/Masterminds/semver/v3 v3.4.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM= +github.com/PuerkitoBio/purell v1.1.1 h1:WEQqlqaGbrPkxLJWfBwQmfEAE1Z7ONdDLqrN38tNFfI= +github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0= +github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 h1:d+Bc7a5rLufV/sSk/8dngufqelfh6jnri85riMAaF/M= +github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE= +github.com/adrg/xdg v0.5.3 h1:xRnxJXne7+oWDatRhR1JLnvuccuIeCoBu2rtuLqQB78= +github.com/adrg/xdg v0.5.3/go.mod h1:nlTsY+NNiCBGCK2tpm09vRqfVzrc2fLmXGpBLF0zlTQ= +github.com/bytedance/sonic v1.14.0 h1:/OfKt8HFw0kh2rj8N0F6C/qPGRESq0BbaNZgcNXXzQQ= +github.com/bytedance/sonic v1.14.0/go.mod h1:WoEbx8WTcFJfzCe0hbmyTGrfjt8PzNEBdxlNUO24NhA= +github.com/bytedance/sonic/loader v0.3.0 h1:dskwH8edlzNMctoruo8FPTJDF3vLtDT0sXZwvZJyqeA= +github.com/bytedance/sonic/loader v0.3.0/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI= +github.com/ckanthony/gin-mcp v0.0.0-20251107113615-3c631c4fa9f4 h1:V0tltxRKT8DZRXcn2ErLy4alznOBzWWmx4gnQbic9jE= +github.com/ckanthony/gin-mcp v0.0.0-20251107113615-3c631c4fa9f4/go.mod h1:eaCpaNzFM2bfCUXMPxbLFwI/ar67gAaVTNrltASGeoc= +github.com/cloudwego/base64x v0.1.6 h1:t11wG9AECkCDk5fMSoxmufanudBtJ+/HemLstXDLI2M= +github.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gEHfghB2IPU= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/ebitengine/purego v0.9.1 h1:a/k2f2HQU3Pi399RPW1MOaZyhKJL9w/xFpKAg4q1s0A= +github.com/ebitengine/purego v0.9.1/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ= +github.com/gabriel-vasile/mimetype v1.4.9 h1:5k+WDwEsD9eTLL8Tz3L0VnmVh9QxGjRmjBvAG7U/oYY= +github.com/gabriel-vasile/mimetype v1.4.9/go.mod h1:WnSQhFKJuBlRyLiKohA/2DtIlPFAbguNaG7QCHcyGok= +github.com/gin-contrib/cors v1.7.6 h1:3gQ8GMzs1Ylpf70y8bMw4fVpycXIeX1ZemuSQIsnQQY= +github.com/gin-contrib/cors v1.7.6/go.mod h1:Ulcl+xN4jel9t1Ry8vqph23a60FwH9xVLd+3ykmTjOk= +github.com/gin-contrib/gzip v0.0.6 h1:NjcunTcGAj5CO1gn4N8jHOSIeRFHIbn51z6K+xaN4d4= +github.com/gin-contrib/gzip v0.0.6/go.mod h1:QOJlmV2xmayAjkNS2Y8NQsMneuRShOU/kjovCXNuzzk= +github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w= +github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM= +github.com/gin-gonic/gin v1.11.0 h1:OW/6PLjyusp2PPXtyxKHU0RbX6I/l28FTdDlae5ueWk= +github.com/gin-gonic/gin v1.11.0/go.mod h1:+iq/FyxlGzII0KHiBGjuNn4UNENUlKbGlNmc+W50Dls= +github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY= +github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= +github.com/go-openapi/jsonpointer v0.19.3/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= +github.com/go-openapi/jsonpointer v0.19.5 h1:gZr+CIYByUqjcgeLXnQu2gHYQC9o73G2XUeOFYEICuY= +github.com/go-openapi/jsonpointer v0.19.5/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= +github.com/go-openapi/jsonreference v0.19.6 h1:UBIxjkht+AWIgYzCDSv2GN+E/togfwXUJFRTWhl2Jjs= +github.com/go-openapi/jsonreference v0.19.6/go.mod h1:diGHMEHg2IqXZGKxqyvWdfWU/aim5Dprw5bqpKkTvns= +github.com/go-openapi/spec v0.20.4 h1:O8hJrt0UMnhHcluhIdUgCLRWyM2x7QkBXRvOs7m+O1M= +github.com/go-openapi/spec v0.20.4/go.mod h1:faYFR1CvsJZ0mNsmsphTMSoRrNV3TEDoAM7FOEWeq8I= +github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk= +github.com/go-openapi/swag v0.19.15 h1:D2NRCBzS9/pEY3gP9Nl8aDqGUcPFrwG2p+CNFrLyrCM= +github.com/go-openapi/swag v0.19.15/go.mod h1:QYRuS/SOXUCsnplDa677K7+DxSOj6IPNl/eQntq43wQ= +github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= +github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= +github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= +github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= +github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= +github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= +github.com/go-playground/validator/v10 v10.27.0 h1:w8+XrWVMhGkxOaaowyKH35gFydVHOvC0/uWoy2Fzwn4= +github.com/go-playground/validator/v10 v10.27.0/go.mod h1:I5QpIEbmr8On7W0TktmJAumgzX4CA1XNl4ZmDuVHKKo= +github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4= +github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= +github.com/goccy/go-yaml v1.18.0 h1:8W7wMFS12Pcas7KU+VVkaiCng+kG8QiFeFwzFb+rwuw= +github.com/goccy/go-yaml v1.18.0/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA= +github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= +github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= +github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= +github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y= +github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= +github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= +github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= +github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 h1:6E+4a0GO5zZEnZ81pIr0yLvtUWk2if982qA3F3QD6H4= +github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I= +github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= +github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= +github.com/mailru/easyjson v0.7.6 h1:8yTIVnZgCoiM1TgqoeTl+LfU5Jg6/xL3QhGQnimLYnA= +github.com/mailru/easyjson v0.7.6/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-sqlite3 v1.14.34 h1:3NtcvcUnFBPsuRcno8pUtupspG/GM+9nZ88zgJcp6Zk= +github.com/mattn/go-sqlite3 v1.14.34/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= +github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4= +github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 h1:o4JXh1EVt9k/+g42oCprj/FisM4qX9L3sZB3upGN2ZU= +github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE= +github.com/quic-go/qpack v0.5.1 h1:giqksBPnT/HDtZ6VhtFKgoLOWmlyo9Ei6u9PqzIMbhI= +github.com/quic-go/qpack v0.5.1/go.mod h1:+PC4XFrEskIVkcLzpEkbLqq1uCoxPhQuvK5rH1ZgaEg= +github.com/quic-go/quic-go v0.54.0 h1:6s1YB9QotYI6Ospeiguknbp2Znb/jZYjZLRXn9kMQBg= +github.com/quic-go/quic-go v0.54.0/go.mod h1:e68ZEaCdyviluZmy44P6Iey98v/Wfz6HCjQEm+l8zTY= +github.com/rogpeppe/go-internal v1.8.0 h1:FCbCCtXNOY3UtUuHUYaghJg4y7Fd14rXifAYUAtL9R8= +github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE= +github.com/shirou/gopsutil/v4 v4.26.1 h1:TOkEyriIXk2HX9d4isZJtbjXbEjf5qyKPAzbzY0JWSo= +github.com/shirou/gopsutil/v4 v4.26.1/go.mod h1:medLI9/UNAb0dOI9Q3/7yWSqKkj00u+1tgY8nvv41pc= +github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= +github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +github.com/swaggo/files v1.0.1 h1:J1bVJ4XHZNq0I46UU90611i9/YzdrF7x92oX1ig5IdE= +github.com/swaggo/files v1.0.1/go.mod h1:0qXmMNH6sXNf+73t65aKeB+ApmgxdnkQzVTAj2uaMUg= +github.com/swaggo/gin-swagger v1.6.1 h1:Ri06G4gc9N4t4k8hekMigJ9zKTFSlqj/9paAQCQs7cY= +github.com/swaggo/gin-swagger v1.6.1/go.mod h1:LQ+hJStHakCWRiK/YNYtJOu4mR2FP+pxLnILT/qNiTw= +github.com/swaggo/swag v1.16.6 h1:qBNcx53ZaX+M5dxVyTrgQ0PJ/ACK+NzhwcbieTt+9yI= +github.com/swaggo/swag v1.16.6/go.mod h1:ngP2etMK5a0P3QBizic5MEwpRmluJZPHjXcMoj4Xesg= +github.com/tklauser/go-sysconf v0.3.16 h1:frioLaCQSsF5Cy1jgRBrzr6t502KIIwQ0MArYICU0nA= +github.com/tklauser/go-sysconf v0.3.16/go.mod h1:/qNL9xxDhc7tx3HSRsLWNnuzbVfh3e7gh/BmM179nYI= +github.com/tklauser/numcpus v0.11.0 h1:nSTwhKH5e1dMNsCdVBukSZrURJRoHbSEQjdEbY+9RXw= +github.com/tklauser/numcpus v0.11.0/go.mod h1:z+LwcLq54uWZTX0u/bGobaV34u6V7KNlTZejzM6/3MQ= +github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= +github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= +github.com/ugorji/go/codec v1.3.0 h1:Qd2W2sQawAfG8XSvzwhBeoGq71zXOC/Q1E9y/wUcsUA= +github.com/ugorji/go/codec v1.3.0/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0= +github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= +go.uber.org/mock v0.5.0 h1:KAMbZvZPyBPWgD14IrIQ38QCyjwpvVVV6K/bHl1IwQU= +go.uber.org/mock v0.5.0/go.mod h1:ge71pBPLYDk7QIi1LupWxdAykm7KIEFchiOqd6z7qMM= +golang.org/x/arch v0.20.0 h1:dx1zTU0MAE98U+TQ8BLl7XsJbgze2WnNKF/8tGp/Q6c= +golang.org/x/arch v0.20.0/go.mod h1:bdwinDaKcfZUGpH09BB7ZmOfhalA8lQdzl62l8gGWsk= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.40.0 h1:r4x+VvoG5Fm+eJcxMaY8CQM7Lb0l1lsmjGBQ6s8BfKM= +golang.org/x/crypto v0.40.0/go.mod h1:Qr1vMER5WyS2dfPHAlsOj01wgLbsyWtFn/aY+5+ZdxY= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/mod v0.25.0 h1:n7a+ZbQKQA/Ysbyb0/6IbB1H/X41mKgbhfv7AfG/44w= +golang.org/x/mod v0.25.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20210421230115-4e50805a0758/go.mod h1:72T/g9IO56b78aLF+1Kcs5dz7/ng1VjMUvfKvpfy+jM= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= +golang.org/x/net v0.42.0 h1:jzkYrhi3YQWD6MLBJcsklgQsoAcw89EcZbJw8Z614hs= +golang.org/x/net v0.42.0/go.mod h1:FF1RA5d3u7nAYA4z2TkclSCKh68eSXtiFwcWQpPXdt8= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw= +golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210420072515-93ed5bcd2bfe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ= +golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.27.0 h1:4fGWRpyh641NLlecmyl4LOe6yDdfaYNrGb2zdfo4JV4= +golang.org/x/text v0.27.0/go.mod h1:1D28KMCvyooCX9hBiosv5Tz/+YLxj0j7XhWjpSUF7CU= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/tools v0.34.0 h1:qIpSLOxeCYGg9TrcJokLBG4KFA6d795g0xkBkiESGlo= +golang.org/x/tools v0.34.0/go.mod h1:pAP9OwEaY1CAW3HOmg3hLZC5Z0CCmzjAF2UQMSqNARg= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/protobuf v1.36.9 h1:w2gp2mA27hUeUzj9Ex9FBjsBm40zfaDtEWow293U7Iw= +google.golang.org/protobuf v1.36.9/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/logging/logger.go b/logging/logger.go new file mode 100644 index 0000000..f400dc9 --- /dev/null +++ b/logging/logger.go @@ -0,0 +1,284 @@ +// Package logging provides structured logging with log levels and fields. +package logging + +import ( + "fmt" + "io" + "os" + "strings" + "sync" + "time" +) + +// Level represents the severity of a log message. +type Level int + +const ( + // LevelDebug is the most verbose log level. + LevelDebug Level = iota + // LevelInfo is for general informational messages. + LevelInfo + // LevelWarn is for warning messages. + LevelWarn + // LevelError is for error messages. + LevelError +) + +// String returns the string representation of the log level. +func (l Level) String() string { + switch l { + case LevelDebug: + return "DEBUG" + case LevelInfo: + return "INFO" + case LevelWarn: + return "WARN" + case LevelError: + return "ERROR" + default: + return "UNKNOWN" + } +} + +// Logger provides structured logging with configurable output and level. +type Logger struct { + mu sync.Mutex + output io.Writer + level Level + component string +} + +// Config holds configuration for creating a new Logger. +type Config struct { + Output io.Writer + Level Level + Component string +} + +// DefaultConfig returns the default logger configuration. +func DefaultConfig() Config { + return Config{ + Output: os.Stderr, + Level: LevelInfo, + Component: "", + } +} + +// New creates a new Logger with the given configuration. +func New(cfg Config) *Logger { + if cfg.Output == nil { + cfg.Output = os.Stderr + } + return &Logger{ + output: cfg.Output, + level: cfg.Level, + component: cfg.Component, + } +} + +// WithComponent returns a new Logger with the specified component name. +func (l *Logger) WithComponent(component string) *Logger { + return &Logger{ + output: l.output, + level: l.level, + component: component, + } +} + +// SetLevel sets the minimum log level. +func (l *Logger) SetLevel(level Level) { + l.mu.Lock() + defer l.mu.Unlock() + l.level = level +} + +// GetLevel returns the current log level. +func (l *Logger) GetLevel() Level { + l.mu.Lock() + defer l.mu.Unlock() + return l.level +} + +// Fields represents key-value pairs for structured logging. +type Fields map[string]interface{} + +// log writes a log message at the specified level. +func (l *Logger) log(level Level, msg string, fields Fields) { + l.mu.Lock() + defer l.mu.Unlock() + + if level < l.level { + return + } + + // Build the log line + var sb strings.Builder + timestamp := time.Now().Format("2006/01/02 15:04:05") + sb.WriteString(timestamp) + sb.WriteString(" [") + sb.WriteString(level.String()) + sb.WriteString("]") + + if l.component != "" { + sb.WriteString(" [") + sb.WriteString(l.component) + sb.WriteString("]") + } + + sb.WriteString(" ") + sb.WriteString(msg) + + // Add fields if present + if len(fields) > 0 { + sb.WriteString(" |") + for k, v := range fields { + sb.WriteString(" ") + sb.WriteString(k) + sb.WriteString("=") + sb.WriteString(fmt.Sprintf("%v", v)) + } + } + + sb.WriteString("\n") + fmt.Fprint(l.output, sb.String()) +} + +// Debug logs a debug message. +func (l *Logger) Debug(msg string, fields ...Fields) { + l.log(LevelDebug, msg, mergeFields(fields)) +} + +// Info logs an informational message. +func (l *Logger) Info(msg string, fields ...Fields) { + l.log(LevelInfo, msg, mergeFields(fields)) +} + +// Warn logs a warning message. +func (l *Logger) Warn(msg string, fields ...Fields) { + l.log(LevelWarn, msg, mergeFields(fields)) +} + +// Error logs an error message. +func (l *Logger) Error(msg string, fields ...Fields) { + l.log(LevelError, msg, mergeFields(fields)) +} + +// Debugf logs a formatted debug message. +func (l *Logger) Debugf(format string, args ...interface{}) { + l.log(LevelDebug, fmt.Sprintf(format, args...), nil) +} + +// Infof logs a formatted informational message. +func (l *Logger) Infof(format string, args ...interface{}) { + l.log(LevelInfo, fmt.Sprintf(format, args...), nil) +} + +// Warnf logs a formatted warning message. +func (l *Logger) Warnf(format string, args ...interface{}) { + l.log(LevelWarn, fmt.Sprintf(format, args...), nil) +} + +// Errorf logs a formatted error message. +func (l *Logger) Errorf(format string, args ...interface{}) { + l.log(LevelError, fmt.Sprintf(format, args...), nil) +} + +// mergeFields combines multiple Fields maps into one. +func mergeFields(fields []Fields) Fields { + if len(fields) == 0 { + return nil + } + result := make(Fields) + for _, f := range fields { + for k, v := range f { + result[k] = v + } + } + return result +} + +// --- Global logger for convenience --- + +var ( + globalLogger = New(DefaultConfig()) + globalMu sync.RWMutex +) + +// SetGlobal sets the global logger instance. +func SetGlobal(l *Logger) { + globalMu.Lock() + defer globalMu.Unlock() + globalLogger = l +} + +// GetGlobal returns the global logger instance. +func GetGlobal() *Logger { + globalMu.RLock() + defer globalMu.RUnlock() + return globalLogger +} + +// SetGlobalLevel sets the log level of the global logger. +func SetGlobalLevel(level Level) { + globalMu.RLock() + defer globalMu.RUnlock() + globalLogger.SetLevel(level) +} + +// Global convenience functions that use the global logger + +// Debug logs a debug message using the global logger. +func Debug(msg string, fields ...Fields) { + GetGlobal().Debug(msg, fields...) +} + +// Info logs an informational message using the global logger. +func Info(msg string, fields ...Fields) { + GetGlobal().Info(msg, fields...) +} + +// Warn logs a warning message using the global logger. +func Warn(msg string, fields ...Fields) { + GetGlobal().Warn(msg, fields...) +} + +// Error logs an error message using the global logger. +func Error(msg string, fields ...Fields) { + GetGlobal().Error(msg, fields...) +} + +// Debugf logs a formatted debug message using the global logger. +func Debugf(format string, args ...interface{}) { + GetGlobal().Debugf(format, args...) +} + +// Infof logs a formatted informational message using the global logger. +func Infof(format string, args ...interface{}) { + GetGlobal().Infof(format, args...) +} + +// Warnf logs a formatted warning message using the global logger. +func Warnf(format string, args ...interface{}) { + GetGlobal().Warnf(format, args...) +} + +// Errorf logs a formatted error message using the global logger. +func Errorf(format string, args ...interface{}) { + GetGlobal().Errorf(format, args...) +} + +// ParseLevel parses a string into a log level. +func ParseLevel(s string) (Level, error) { + switch strings.ToUpper(s) { + case "DEBUG": + return LevelDebug, nil + case "INFO": + return LevelInfo, nil + case "WARN", "WARNING": + return LevelWarn, nil + case "ERROR": + return LevelError, nil + default: + return LevelInfo, fmt.Errorf("unknown log level: %s", s) + } +} diff --git a/logging/logger_test.go b/logging/logger_test.go new file mode 100644 index 0000000..5fa5163 --- /dev/null +++ b/logging/logger_test.go @@ -0,0 +1,262 @@ +package logging + +import ( + "bytes" + "strings" + "testing" +) + +func TestLoggerLevels(t *testing.T) { + var buf bytes.Buffer + logger := New(Config{ + Output: &buf, + Level: LevelInfo, + }) + + // Debug should not appear at Info level + logger.Debug("debug message") + if buf.Len() > 0 { + t.Error("Debug message should not appear at Info level") + } + + // Info should appear + logger.Info("info message") + if !strings.Contains(buf.String(), "[INFO]") { + t.Error("Info message should appear") + } + if !strings.Contains(buf.String(), "info message") { + t.Error("Info message content should appear") + } + buf.Reset() + + // Warn should appear + logger.Warn("warn message") + if !strings.Contains(buf.String(), "[WARN]") { + t.Error("Warn message should appear") + } + buf.Reset() + + // Error should appear + logger.Error("error message") + if !strings.Contains(buf.String(), "[ERROR]") { + t.Error("Error message should appear") + } +} + +func TestLoggerDebugLevel(t *testing.T) { + var buf bytes.Buffer + logger := New(Config{ + Output: &buf, + Level: LevelDebug, + }) + + logger.Debug("debug message") + if !strings.Contains(buf.String(), "[DEBUG]") { + t.Error("Debug message should appear at Debug level") + } +} + +func TestLoggerWithFields(t *testing.T) { + var buf bytes.Buffer + logger := New(Config{ + Output: &buf, + Level: LevelInfo, + }) + + logger.Info("test message", Fields{"key": "value", "num": 42}) + output := buf.String() + + if !strings.Contains(output, "key=value") { + t.Error("Field key=value should appear") + } + if !strings.Contains(output, "num=42") { + t.Error("Field num=42 should appear") + } +} + +func TestLoggerWithComponent(t *testing.T) { + var buf bytes.Buffer + logger := New(Config{ + Output: &buf, + Level: LevelInfo, + Component: "TestComponent", + }) + + logger.Info("test message") + output := buf.String() + + if !strings.Contains(output, "[TestComponent]") { + t.Error("Component name should appear in log") + } +} + +func TestLoggerDerivedComponent(t *testing.T) { + var buf bytes.Buffer + parent := New(Config{ + Output: &buf, + Level: LevelInfo, + }) + + child := parent.WithComponent("ChildComponent") + child.Info("child message") + output := buf.String() + + if !strings.Contains(output, "[ChildComponent]") { + t.Error("Derived component name should appear") + } +} + +func TestLoggerFormatted(t *testing.T) { + var buf bytes.Buffer + logger := New(Config{ + Output: &buf, + Level: LevelInfo, + }) + + logger.Infof("formatted %s %d", "string", 123) + output := buf.String() + + if !strings.Contains(output, "formatted string 123") { + t.Errorf("Formatted message should appear, got: %s", output) + } +} + +func TestSetLevel(t *testing.T) { + var buf bytes.Buffer + logger := New(Config{ + Output: &buf, + Level: LevelError, + }) + + // Info should not appear at Error level + logger.Info("should not appear") + if buf.Len() > 0 { + t.Error("Info should not appear at Error level") + } + + // Change to Info level + logger.SetLevel(LevelInfo) + logger.Info("should appear now") + if !strings.Contains(buf.String(), "should appear now") { + t.Error("Info should appear after level change") + } + + // Verify GetLevel + if logger.GetLevel() != LevelInfo { + t.Error("GetLevel should return LevelInfo") + } +} + +func TestParseLevel(t *testing.T) { + tests := []struct { + input string + expected Level + wantErr bool + }{ + {"DEBUG", LevelDebug, false}, + {"debug", LevelDebug, false}, + {"INFO", LevelInfo, false}, + {"info", LevelInfo, false}, + {"WARN", LevelWarn, false}, + {"WARNING", LevelWarn, false}, + {"ERROR", LevelError, false}, + {"error", LevelError, false}, + {"invalid", LevelInfo, true}, + } + + for _, tt := range tests { + t.Run(tt.input, func(t *testing.T) { + level, err := ParseLevel(tt.input) + if tt.wantErr && err == nil { + t.Error("Expected error but got none") + } + if !tt.wantErr && err != nil { + t.Errorf("Unexpected error: %v", err) + } + if !tt.wantErr && level != tt.expected { + t.Errorf("Expected %v, got %v", tt.expected, level) + } + }) + } +} + +func TestGlobalLogger(t *testing.T) { + var buf bytes.Buffer + logger := New(Config{ + Output: &buf, + Level: LevelInfo, + }) + + SetGlobal(logger) + + Info("global test") + if !strings.Contains(buf.String(), "global test") { + t.Error("Global logger should write message") + } + + buf.Reset() + SetGlobalLevel(LevelError) + Info("should not appear") + if buf.Len() > 0 { + t.Error("Info should not appear at Error level") + } + + // Reset to default for other tests + SetGlobal(New(DefaultConfig())) +} + +func TestLevelString(t *testing.T) { + tests := []struct { + level Level + expected string + }{ + {LevelDebug, "DEBUG"}, + {LevelInfo, "INFO"}, + {LevelWarn, "WARN"}, + {LevelError, "ERROR"}, + {Level(99), "UNKNOWN"}, + } + + for _, tt := range tests { + if got := tt.level.String(); got != tt.expected { + t.Errorf("Level(%d).String() = %s, want %s", tt.level, got, tt.expected) + } + } +} + +func TestMergeFields(t *testing.T) { + // Empty fields + result := mergeFields(nil) + if result != nil { + t.Error("nil input should return nil") + } + + result = mergeFields([]Fields{}) + if result != nil { + t.Error("empty input should return nil") + } + + // Single fields + result = mergeFields([]Fields{{"key": "value"}}) + if result["key"] != "value" { + t.Error("Single field should be preserved") + } + + // Multiple fields + result = mergeFields([]Fields{ + {"key1": "value1"}, + {"key2": "value2"}, + }) + if result["key1"] != "value1" || result["key2"] != "value2" { + t.Error("Multiple fields should be merged") + } + + // Override + result = mergeFields([]Fields{ + {"key": "value1"}, + {"key": "value2"}, + }) + if result["key"] != "value2" { + t.Error("Later fields should override earlier ones") + } +} diff --git a/mining/auth.go b/mining/auth.go new file mode 100644 index 0000000..0e98376 --- /dev/null +++ b/mining/auth.go @@ -0,0 +1,274 @@ +package mining + +import ( + "crypto/md5" + "crypto/rand" + "crypto/subtle" + "encoding/hex" + "fmt" + "net/http" + "os" + "strings" + "sync" + "time" + + "forge.lthn.ai/core/mining/logging" + "github.com/gin-gonic/gin" +) + +// AuthConfig holds authentication configuration +type AuthConfig struct { + // Enabled determines if authentication is required + Enabled bool + // Username for basic/digest auth + Username string + // Password for basic/digest auth + Password string + // Realm for digest auth + Realm string + // NonceExpiry is how long a nonce is valid + NonceExpiry time.Duration +} + +// DefaultAuthConfig returns the default auth configuration. +// Auth is disabled by default for local development. +func DefaultAuthConfig() AuthConfig { + return AuthConfig{ + Enabled: false, + Username: "", + Password: "", + Realm: "Mining API", + NonceExpiry: 5 * time.Minute, + } +} + +// AuthConfigFromEnv creates auth config from environment variables. +// Set MINING_API_AUTH=true to enable, MINING_API_USER and MINING_API_PASS for credentials. +func AuthConfigFromEnv() AuthConfig { + config := DefaultAuthConfig() + + if os.Getenv("MINING_API_AUTH") == "true" { + config.Enabled = true + config.Username = os.Getenv("MINING_API_USER") + config.Password = os.Getenv("MINING_API_PASS") + + if config.Username == "" || config.Password == "" { + logging.Warn("API auth enabled but credentials not set", logging.Fields{ + "hint": "Set MINING_API_USER and MINING_API_PASS environment variables", + }) + config.Enabled = false + } + } + + if realm := os.Getenv("MINING_API_REALM"); realm != "" { + config.Realm = realm + } + + return config +} + +// DigestAuth implements HTTP Digest Authentication middleware +type DigestAuth struct { + config AuthConfig + nonces sync.Map // map[string]time.Time for nonce expiry tracking + stopChan chan struct{} + stopOnce sync.Once +} + +// NewDigestAuth creates a new digest auth middleware +func NewDigestAuth(config AuthConfig) *DigestAuth { + da := &DigestAuth{ + config: config, + stopChan: make(chan struct{}), + } + // Start nonce cleanup goroutine + go da.cleanupNonces() + return da +} + +// Stop gracefully shuts down the DigestAuth, stopping the cleanup goroutine. +// Safe to call multiple times. +func (da *DigestAuth) Stop() { + da.stopOnce.Do(func() { + close(da.stopChan) + }) +} + +// Middleware returns a Gin middleware that enforces digest authentication +func (da *DigestAuth) Middleware() gin.HandlerFunc { + return func(c *gin.Context) { + if !da.config.Enabled { + c.Next() + return + } + + authHeader := c.GetHeader("Authorization") + if authHeader == "" { + da.sendChallenge(c) + return + } + + // Try digest auth first + if strings.HasPrefix(authHeader, "Digest ") { + if da.validateDigest(c, authHeader) { + c.Next() + return + } + da.sendChallenge(c) + return + } + + // Fall back to basic auth + if strings.HasPrefix(authHeader, "Basic ") { + if da.validateBasic(c, authHeader) { + c.Next() + return + } + } + + da.sendChallenge(c) + } +} + +// sendChallenge sends a 401 response with digest auth challenge +func (da *DigestAuth) sendChallenge(c *gin.Context) { + nonce := da.generateNonce() + da.nonces.Store(nonce, time.Now()) + + challenge := fmt.Sprintf( + `Digest realm="%s", qop="auth", nonce="%s", opaque="%s"`, + da.config.Realm, + nonce, + da.generateOpaque(), + ) + + c.Header("WWW-Authenticate", challenge) + c.AbortWithStatusJSON(http.StatusUnauthorized, APIError{ + Code: "AUTH_REQUIRED", + Message: "Authentication required", + Suggestion: "Provide valid credentials using Digest or Basic authentication", + }) +} + +// validateDigest validates a digest auth header +func (da *DigestAuth) validateDigest(c *gin.Context, authHeader string) bool { + params := parseDigestParams(authHeader[7:]) // Skip "Digest " + + nonce := params["nonce"] + if nonce == "" { + return false + } + + // Check nonce validity + if storedTime, ok := da.nonces.Load(nonce); ok { + if time.Since(storedTime.(time.Time)) > da.config.NonceExpiry { + da.nonces.Delete(nonce) + return false + } + } else { + return false + } + + // Validate username with constant-time comparison to prevent timing attacks + if subtle.ConstantTimeCompare([]byte(params["username"]), []byte(da.config.Username)) != 1 { + return false + } + + // Calculate expected response + ha1 := md5Hash(fmt.Sprintf("%s:%s:%s", da.config.Username, da.config.Realm, da.config.Password)) + ha2 := md5Hash(fmt.Sprintf("%s:%s", c.Request.Method, params["uri"])) + + var expectedResponse string + if params["qop"] == "auth" { + expectedResponse = md5Hash(fmt.Sprintf("%s:%s:%s:%s:%s:%s", + ha1, nonce, params["nc"], params["cnonce"], params["qop"], ha2)) + } else { + expectedResponse = md5Hash(fmt.Sprintf("%s:%s:%s", ha1, nonce, ha2)) + } + + // Constant-time comparison to prevent timing attacks + return subtle.ConstantTimeCompare([]byte(expectedResponse), []byte(params["response"])) == 1 +} + +// validateBasic validates a basic auth header +func (da *DigestAuth) validateBasic(c *gin.Context, authHeader string) bool { + // Gin has built-in basic auth, but we do manual validation for consistency + user, pass, ok := c.Request.BasicAuth() + if !ok { + return false + } + + // Constant-time comparison to prevent timing attacks + userMatch := subtle.ConstantTimeCompare([]byte(user), []byte(da.config.Username)) == 1 + passMatch := subtle.ConstantTimeCompare([]byte(pass), []byte(da.config.Password)) == 1 + + return userMatch && passMatch +} + +// generateNonce creates a cryptographically random nonce +func (da *DigestAuth) generateNonce() string { + b := make([]byte, 16) + if _, err := rand.Read(b); err != nil { + // Cryptographic failure is critical - fall back to time-based nonce + // This should never happen on a properly configured system + return hex.EncodeToString([]byte(fmt.Sprintf("%d", time.Now().UnixNano()))) + } + return hex.EncodeToString(b) +} + +// generateOpaque creates an opaque value +func (da *DigestAuth) generateOpaque() string { + return md5Hash(da.config.Realm) +} + +// cleanupNonces removes expired nonces periodically +func (da *DigestAuth) cleanupNonces() { + interval := da.config.NonceExpiry + if interval <= 0 { + interval = 5 * time.Minute // Default if not set + } + ticker := time.NewTicker(interval) + defer ticker.Stop() + + for { + select { + case <-da.stopChan: + return + case <-ticker.C: + now := time.Now() + da.nonces.Range(func(key, value interface{}) bool { + if now.Sub(value.(time.Time)) > da.config.NonceExpiry { + da.nonces.Delete(key) + } + return true + }) + } + } +} + +// parseDigestParams parses the parameters from a digest auth header +func parseDigestParams(header string) map[string]string { + params := make(map[string]string) + parts := strings.Split(header, ",") + + for _, part := range parts { + part = strings.TrimSpace(part) + idx := strings.Index(part, "=") + if idx < 0 { + continue + } + key := strings.TrimSpace(part[:idx]) + value := strings.TrimSpace(part[idx+1:]) + // Remove quotes + value = strings.Trim(value, `"`) + params[key] = value + } + + return params +} + +// md5Hash returns the MD5 hash of a string as a hex string +func md5Hash(s string) string { + h := md5.Sum([]byte(s)) + return hex.EncodeToString(h[:]) +} diff --git a/mining/auth_test.go b/mining/auth_test.go new file mode 100644 index 0000000..b7bacb1 --- /dev/null +++ b/mining/auth_test.go @@ -0,0 +1,604 @@ +package mining + +import ( + "crypto/md5" + "encoding/base64" + "encoding/hex" + "fmt" + "net/http" + "net/http/httptest" + "os" + "testing" + "time" + + "github.com/gin-gonic/gin" +) + +func init() { + gin.SetMode(gin.TestMode) +} + +func TestDefaultAuthConfig(t *testing.T) { + cfg := DefaultAuthConfig() + + if cfg.Enabled { + t.Error("expected Enabled to be false by default") + } + if cfg.Username != "" { + t.Error("expected Username to be empty by default") + } + if cfg.Password != "" { + t.Error("expected Password to be empty by default") + } + if cfg.Realm != "Mining API" { + t.Errorf("expected Realm to be 'Mining API', got %s", cfg.Realm) + } + if cfg.NonceExpiry != 5*time.Minute { + t.Errorf("expected NonceExpiry to be 5 minutes, got %v", cfg.NonceExpiry) + } +} + +func TestAuthConfigFromEnv(t *testing.T) { + // Save original env + origAuth := os.Getenv("MINING_API_AUTH") + origUser := os.Getenv("MINING_API_USER") + origPass := os.Getenv("MINING_API_PASS") + origRealm := os.Getenv("MINING_API_REALM") + defer func() { + os.Setenv("MINING_API_AUTH", origAuth) + os.Setenv("MINING_API_USER", origUser) + os.Setenv("MINING_API_PASS", origPass) + os.Setenv("MINING_API_REALM", origRealm) + }() + + t.Run("auth disabled by default", func(t *testing.T) { + os.Setenv("MINING_API_AUTH", "") + cfg := AuthConfigFromEnv() + if cfg.Enabled { + t.Error("expected Enabled to be false when env not set") + } + }) + + t.Run("auth enabled with valid credentials", func(t *testing.T) { + os.Setenv("MINING_API_AUTH", "true") + os.Setenv("MINING_API_USER", "testuser") + os.Setenv("MINING_API_PASS", "testpass") + + cfg := AuthConfigFromEnv() + if !cfg.Enabled { + t.Error("expected Enabled to be true") + } + if cfg.Username != "testuser" { + t.Errorf("expected Username 'testuser', got %s", cfg.Username) + } + if cfg.Password != "testpass" { + t.Errorf("expected Password 'testpass', got %s", cfg.Password) + } + }) + + t.Run("auth disabled if credentials missing", func(t *testing.T) { + os.Setenv("MINING_API_AUTH", "true") + os.Setenv("MINING_API_USER", "") + os.Setenv("MINING_API_PASS", "") + + cfg := AuthConfigFromEnv() + if cfg.Enabled { + t.Error("expected Enabled to be false when credentials missing") + } + }) + + t.Run("custom realm", func(t *testing.T) { + os.Setenv("MINING_API_AUTH", "") + os.Setenv("MINING_API_REALM", "Custom Realm") + + cfg := AuthConfigFromEnv() + if cfg.Realm != "Custom Realm" { + t.Errorf("expected Realm 'Custom Realm', got %s", cfg.Realm) + } + }) +} + +func TestNewDigestAuth(t *testing.T) { + cfg := AuthConfig{ + Enabled: true, + Username: "user", + Password: "pass", + Realm: "Test", + NonceExpiry: time.Second, + } + + da := NewDigestAuth(cfg) + if da == nil { + t.Fatal("expected non-nil DigestAuth") + } + + // Cleanup + da.Stop() +} + +func TestDigestAuthStop(t *testing.T) { + cfg := DefaultAuthConfig() + da := NewDigestAuth(cfg) + + // Should not panic when called multiple times + da.Stop() + da.Stop() + da.Stop() +} + +func TestMiddlewareAuthDisabled(t *testing.T) { + cfg := AuthConfig{Enabled: false} + da := NewDigestAuth(cfg) + defer da.Stop() + + router := gin.New() + router.Use(da.Middleware()) + router.GET("/test", func(c *gin.Context) { + c.String(http.StatusOK, "success") + }) + + req := httptest.NewRequest("GET", "/test", nil) + w := httptest.NewRecorder() + router.ServeHTTP(w, req) + + if w.Code != http.StatusOK { + t.Errorf("expected status 200, got %d", w.Code) + } + if w.Body.String() != "success" { + t.Errorf("expected body 'success', got %s", w.Body.String()) + } +} + +func TestMiddlewareNoAuth(t *testing.T) { + cfg := AuthConfig{ + Enabled: true, + Username: "user", + Password: "pass", + Realm: "Test", + NonceExpiry: 5 * time.Minute, + } + da := NewDigestAuth(cfg) + defer da.Stop() + + router := gin.New() + router.Use(da.Middleware()) + router.GET("/test", func(c *gin.Context) { + c.String(http.StatusOK, "success") + }) + + req := httptest.NewRequest("GET", "/test", nil) + w := httptest.NewRecorder() + router.ServeHTTP(w, req) + + if w.Code != http.StatusUnauthorized { + t.Errorf("expected status 401, got %d", w.Code) + } + + wwwAuth := w.Header().Get("WWW-Authenticate") + if wwwAuth == "" { + t.Error("expected WWW-Authenticate header") + } + if !authTestContains(wwwAuth, "Digest") { + t.Error("expected Digest challenge in WWW-Authenticate") + } + if !authTestContains(wwwAuth, `realm="Test"`) { + t.Error("expected realm in WWW-Authenticate") + } +} + +func TestMiddlewareBasicAuthValid(t *testing.T) { + cfg := AuthConfig{ + Enabled: true, + Username: "user", + Password: "pass", + Realm: "Test", + NonceExpiry: 5 * time.Minute, + } + da := NewDigestAuth(cfg) + defer da.Stop() + + router := gin.New() + router.Use(da.Middleware()) + router.GET("/test", func(c *gin.Context) { + c.String(http.StatusOK, "success") + }) + + req := httptest.NewRequest("GET", "/test", nil) + req.SetBasicAuth("user", "pass") + w := httptest.NewRecorder() + router.ServeHTTP(w, req) + + if w.Code != http.StatusOK { + t.Errorf("expected status 200, got %d", w.Code) + } +} + +func TestMiddlewareBasicAuthInvalid(t *testing.T) { + cfg := AuthConfig{ + Enabled: true, + Username: "user", + Password: "pass", + Realm: "Test", + NonceExpiry: 5 * time.Minute, + } + da := NewDigestAuth(cfg) + defer da.Stop() + + router := gin.New() + router.Use(da.Middleware()) + router.GET("/test", func(c *gin.Context) { + c.String(http.StatusOK, "success") + }) + + testCases := []struct { + name string + user string + password string + }{ + {"wrong user", "wronguser", "pass"}, + {"wrong password", "user", "wrongpass"}, + {"both wrong", "wronguser", "wrongpass"}, + {"empty user", "", "pass"}, + {"empty password", "user", ""}, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + req := httptest.NewRequest("GET", "/test", nil) + req.SetBasicAuth(tc.user, tc.password) + w := httptest.NewRecorder() + router.ServeHTTP(w, req) + + if w.Code != http.StatusUnauthorized { + t.Errorf("expected status 401, got %d", w.Code) + } + }) + } +} + +func TestMiddlewareDigestAuthValid(t *testing.T) { + cfg := AuthConfig{ + Enabled: true, + Username: "testuser", + Password: "testpass", + Realm: "Test Realm", + NonceExpiry: 5 * time.Minute, + } + da := NewDigestAuth(cfg) + defer da.Stop() + + router := gin.New() + router.Use(da.Middleware()) + router.GET("/test", func(c *gin.Context) { + c.String(http.StatusOK, "success") + }) + + // First request to get nonce + req := httptest.NewRequest("GET", "/test", nil) + w := httptest.NewRecorder() + router.ServeHTTP(w, req) + + if w.Code != http.StatusUnauthorized { + t.Fatalf("expected 401 to get nonce, got %d", w.Code) + } + + wwwAuth := w.Header().Get("WWW-Authenticate") + params := parseDigestParams(wwwAuth[7:]) // Skip "Digest " + nonce := params["nonce"] + + if nonce == "" { + t.Fatal("nonce not found in challenge") + } + + // Build digest auth response + uri := "/test" + nc := "00000001" + cnonce := "abc123" + qop := "auth" + + ha1 := md5Hash(fmt.Sprintf("%s:%s:%s", cfg.Username, cfg.Realm, cfg.Password)) + ha2 := md5Hash(fmt.Sprintf("GET:%s", uri)) + response := md5Hash(fmt.Sprintf("%s:%s:%s:%s:%s:%s", ha1, nonce, nc, cnonce, qop, ha2)) + + authHeader := fmt.Sprintf( + `Digest username="%s", realm="%s", nonce="%s", uri="%s", qop=%s, nc=%s, cnonce="%s", response="%s"`, + cfg.Username, cfg.Realm, nonce, uri, qop, nc, cnonce, response, + ) + + // Second request with digest auth + req2 := httptest.NewRequest("GET", "/test", nil) + req2.Header.Set("Authorization", authHeader) + w2 := httptest.NewRecorder() + router.ServeHTTP(w2, req2) + + if w2.Code != http.StatusOK { + t.Errorf("expected status 200, got %d; body: %s", w2.Code, w2.Body.String()) + } +} + +func TestMiddlewareDigestAuthInvalidNonce(t *testing.T) { + cfg := AuthConfig{ + Enabled: true, + Username: "user", + Password: "pass", + Realm: "Test", + NonceExpiry: 5 * time.Minute, + } + da := NewDigestAuth(cfg) + defer da.Stop() + + router := gin.New() + router.Use(da.Middleware()) + router.GET("/test", func(c *gin.Context) { + c.String(http.StatusOK, "success") + }) + + // Try with a fake nonce that was never issued + authHeader := `Digest username="user", realm="Test", nonce="fakenonce123", uri="/test", qop=auth, nc=00000001, cnonce="abc", response="xxx"` + req := httptest.NewRequest("GET", "/test", nil) + req.Header.Set("Authorization", authHeader) + w := httptest.NewRecorder() + router.ServeHTTP(w, req) + + if w.Code != http.StatusUnauthorized { + t.Errorf("expected status 401 for invalid nonce, got %d", w.Code) + } +} + +func TestMiddlewareDigestAuthExpiredNonce(t *testing.T) { + cfg := AuthConfig{ + Enabled: true, + Username: "user", + Password: "pass", + Realm: "Test", + NonceExpiry: 50 * time.Millisecond, // Very short for testing + } + da := NewDigestAuth(cfg) + defer da.Stop() + + router := gin.New() + router.Use(da.Middleware()) + router.GET("/test", func(c *gin.Context) { + c.String(http.StatusOK, "success") + }) + + // Get a valid nonce + req := httptest.NewRequest("GET", "/test", nil) + w := httptest.NewRecorder() + router.ServeHTTP(w, req) + + wwwAuth := w.Header().Get("WWW-Authenticate") + params := parseDigestParams(wwwAuth[7:]) + nonce := params["nonce"] + + // Wait for nonce to expire + time.Sleep(100 * time.Millisecond) + + // Try to use expired nonce + uri := "/test" + ha1 := md5Hash(fmt.Sprintf("%s:%s:%s", cfg.Username, cfg.Realm, cfg.Password)) + ha2 := md5Hash(fmt.Sprintf("GET:%s", uri)) + response := md5Hash(fmt.Sprintf("%s:%s:%s", ha1, nonce, ha2)) + + authHeader := fmt.Sprintf( + `Digest username="%s", realm="%s", nonce="%s", uri="%s", response="%s"`, + cfg.Username, cfg.Realm, nonce, uri, response, + ) + + req2 := httptest.NewRequest("GET", "/test", nil) + req2.Header.Set("Authorization", authHeader) + w2 := httptest.NewRecorder() + router.ServeHTTP(w2, req2) + + if w2.Code != http.StatusUnauthorized { + t.Errorf("expected status 401 for expired nonce, got %d", w2.Code) + } +} + +func TestParseDigestParams(t *testing.T) { + testCases := []struct { + name string + input string + expected map[string]string + }{ + { + name: "basic params", + input: `username="john", realm="test"`, + expected: map[string]string{ + "username": "john", + "realm": "test", + }, + }, + { + name: "params with spaces", + input: ` username = "john" , realm = "test" `, + expected: map[string]string{ + "username": "john", + "realm": "test", + }, + }, + { + name: "unquoted values", + input: `qop=auth, nc=00000001`, + expected: map[string]string{ + "qop": "auth", + "nc": "00000001", + }, + }, + { + name: "full digest header", + input: `username="user", realm="Test", nonce="abc123", uri="/api", qop=auth, nc=00000001, cnonce="xyz", response="hash"`, + expected: map[string]string{ + "username": "user", + "realm": "Test", + "nonce": "abc123", + "uri": "/api", + "qop": "auth", + "nc": "00000001", + "cnonce": "xyz", + "response": "hash", + }, + }, + { + name: "empty string", + input: "", + expected: map[string]string{}, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + result := parseDigestParams(tc.input) + for key, expectedVal := range tc.expected { + if result[key] != expectedVal { + t.Errorf("key %s: expected %s, got %s", key, expectedVal, result[key]) + } + } + }) + } +} + +func TestMd5Hash(t *testing.T) { + testCases := []struct { + input string + expected string + }{ + {"hello", "5d41402abc4b2a76b9719d911017c592"}, + {"", "d41d8cd98f00b204e9800998ecf8427e"}, + {"user:realm:password", func() string { + h := md5.Sum([]byte("user:realm:password")) + return hex.EncodeToString(h[:]) + }()}, + } + + for _, tc := range testCases { + t.Run(tc.input, func(t *testing.T) { + result := md5Hash(tc.input) + if result != tc.expected { + t.Errorf("expected %s, got %s", tc.expected, result) + } + }) + } +} + +func TestNonceGeneration(t *testing.T) { + cfg := DefaultAuthConfig() + da := NewDigestAuth(cfg) + defer da.Stop() + + nonces := make(map[string]bool) + for i := 0; i < 100; i++ { + nonce := da.generateNonce() + if len(nonce) != 32 { // 16 bytes = 32 hex chars + t.Errorf("expected nonce length 32, got %d", len(nonce)) + } + if nonces[nonce] { + t.Error("duplicate nonce generated") + } + nonces[nonce] = true + } +} + +func TestOpaqueGeneration(t *testing.T) { + cfg := AuthConfig{Realm: "TestRealm"} + da := NewDigestAuth(cfg) + defer da.Stop() + + opaque1 := da.generateOpaque() + opaque2 := da.generateOpaque() + + // Same realm should produce same opaque + if opaque1 != opaque2 { + t.Error("opaque should be consistent for same realm") + } + + // Should be MD5 of realm + expected := md5Hash("TestRealm") + if opaque1 != expected { + t.Errorf("expected opaque %s, got %s", expected, opaque1) + } +} + +func TestNonceCleanup(t *testing.T) { + cfg := AuthConfig{ + Enabled: true, + Username: "user", + Password: "pass", + Realm: "Test", + NonceExpiry: 50 * time.Millisecond, + } + da := NewDigestAuth(cfg) + defer da.Stop() + + // Store a nonce + nonce := da.generateNonce() + da.nonces.Store(nonce, time.Now()) + + // Verify it exists + if _, ok := da.nonces.Load(nonce); !ok { + t.Error("nonce should exist immediately after storing") + } + + // Wait for cleanup (2x expiry to be safe) + time.Sleep(150 * time.Millisecond) + + // Verify it was cleaned up + if _, ok := da.nonces.Load(nonce); ok { + t.Error("expired nonce should have been cleaned up") + } +} + +// Helper function +func authTestContains(s, substr string) bool { + for i := 0; i <= len(s)-len(substr); i++ { + if s[i:i+len(substr)] == substr { + return true + } + } + return false +} + +// Benchmark tests +func BenchmarkMd5Hash(b *testing.B) { + input := "user:realm:password" + for i := 0; i < b.N; i++ { + md5Hash(input) + } +} + +func BenchmarkNonceGeneration(b *testing.B) { + cfg := DefaultAuthConfig() + da := NewDigestAuth(cfg) + defer da.Stop() + + for i := 0; i < b.N; i++ { + da.generateNonce() + } +} + +func BenchmarkBasicAuthValidation(b *testing.B) { + cfg := AuthConfig{ + Enabled: true, + Username: "user", + Password: "pass", + Realm: "Test", + NonceExpiry: 5 * time.Minute, + } + da := NewDigestAuth(cfg) + defer da.Stop() + + router := gin.New() + router.Use(da.Middleware()) + router.GET("/test", func(c *gin.Context) { + c.Status(http.StatusOK) + }) + + req := httptest.NewRequest("GET", "/test", nil) + req.Header.Set("Authorization", "Basic "+base64.StdEncoding.EncodeToString([]byte("user:pass"))) + + b.ResetTimer() + for i := 0; i < b.N; i++ { + w := httptest.NewRecorder() + router.ServeHTTP(w, req) + } +} diff --git a/mining/bufpool.go b/mining/bufpool.go new file mode 100644 index 0000000..a1282fc --- /dev/null +++ b/mining/bufpool.go @@ -0,0 +1,55 @@ +package mining + +import ( + "bytes" + "encoding/json" + "sync" +) + +// bufferPool provides reusable byte buffers for JSON encoding. +// This reduces allocation overhead in hot paths like WebSocket event serialization. +var bufferPool = sync.Pool{ + New: func() interface{} { + return bytes.NewBuffer(make([]byte, 0, 1024)) + }, +} + +// getBuffer retrieves a buffer from the pool. +func getBuffer() *bytes.Buffer { + buf := bufferPool.Get().(*bytes.Buffer) + buf.Reset() + return buf +} + +// putBuffer returns a buffer to the pool. +func putBuffer(buf *bytes.Buffer) { + // Don't pool buffers that grew too large (>64KB) + if buf.Cap() <= 65536 { + bufferPool.Put(buf) + } +} + +// MarshalJSON encodes a value to JSON using a pooled buffer. +// Returns a copy of the encoded bytes (safe to use after the function returns). +func MarshalJSON(v interface{}) ([]byte, error) { + buf := getBuffer() + defer putBuffer(buf) + + enc := json.NewEncoder(buf) + // Don't escape HTML characters (matches json.Marshal behavior for these use cases) + enc.SetEscapeHTML(false) + if err := enc.Encode(v); err != nil { + return nil, err + } + + // json.Encoder.Encode adds a newline; remove it to match json.Marshal + data := buf.Bytes() + if len(data) > 0 && data[len(data)-1] == '\n' { + data = data[:len(data)-1] + } + + // Return a copy since the buffer will be reused + result := make([]byte, len(data)) + copy(result, data) + return result, nil +} diff --git a/mining/circuit_breaker.go b/mining/circuit_breaker.go new file mode 100644 index 0000000..cfbdd0a --- /dev/null +++ b/mining/circuit_breaker.go @@ -0,0 +1,246 @@ +package mining + +import ( + "errors" + "sync" + "time" + + "forge.lthn.ai/core/mining/logging" +) + +// CircuitState represents the state of a circuit breaker +type CircuitState int + +const ( + // CircuitClosed means the circuit is functioning normally + CircuitClosed CircuitState = iota + // CircuitOpen means the circuit has tripped and requests are being rejected + CircuitOpen + // CircuitHalfOpen means the circuit is testing if the service has recovered + CircuitHalfOpen +) + +func (s CircuitState) String() string { + switch s { + case CircuitClosed: + return "closed" + case CircuitOpen: + return "open" + case CircuitHalfOpen: + return "half-open" + default: + return "unknown" + } +} + +// CircuitBreakerConfig holds configuration for a circuit breaker +type CircuitBreakerConfig struct { + // FailureThreshold is the number of failures before opening the circuit + FailureThreshold int + // ResetTimeout is how long to wait before attempting recovery + ResetTimeout time.Duration + // SuccessThreshold is the number of successes needed in half-open state to close + SuccessThreshold int +} + +// DefaultCircuitBreakerConfig returns sensible defaults +func DefaultCircuitBreakerConfig() CircuitBreakerConfig { + return CircuitBreakerConfig{ + FailureThreshold: 3, + ResetTimeout: 30 * time.Second, + SuccessThreshold: 1, + } +} + +// CircuitBreaker implements the circuit breaker pattern +type CircuitBreaker struct { + name string + config CircuitBreakerConfig + state CircuitState + failures int + successes int + lastFailure time.Time + mu sync.RWMutex + cachedResult interface{} + cachedErr error + lastCacheTime time.Time + cacheDuration time.Duration +} + +// ErrCircuitOpen is returned when the circuit is open +var ErrCircuitOpen = errors.New("circuit breaker is open") + +// NewCircuitBreaker creates a new circuit breaker +func NewCircuitBreaker(name string, config CircuitBreakerConfig) *CircuitBreaker { + return &CircuitBreaker{ + name: name, + config: config, + state: CircuitClosed, + cacheDuration: 5 * time.Minute, // Cache successful results for 5 minutes + } +} + +// State returns the current circuit state +func (cb *CircuitBreaker) State() CircuitState { + cb.mu.RLock() + defer cb.mu.RUnlock() + return cb.state +} + +// Execute runs the given function with circuit breaker protection +func (cb *CircuitBreaker) Execute(fn func() (interface{}, error)) (interface{}, error) { + // Check if we should allow this request + if !cb.allowRequest() { + // Return cached result if available + cb.mu.RLock() + if cb.cachedResult != nil && time.Since(cb.lastCacheTime) < cb.cacheDuration { + result := cb.cachedResult + cb.mu.RUnlock() + logging.Debug("circuit breaker returning cached result", logging.Fields{ + "name": cb.name, + "state": cb.state.String(), + }) + return result, nil + } + cb.mu.RUnlock() + return nil, ErrCircuitOpen + } + + // Execute the function + result, err := fn() + + // Record the result + if err != nil { + cb.recordFailure() + } else { + cb.recordSuccess(result) + } + + return result, err +} + +// allowRequest checks if a request should be allowed through +func (cb *CircuitBreaker) allowRequest() bool { + cb.mu.Lock() + defer cb.mu.Unlock() + + switch cb.state { + case CircuitClosed: + return true + + case CircuitOpen: + // Check if we should transition to half-open + if time.Since(cb.lastFailure) > cb.config.ResetTimeout { + cb.state = CircuitHalfOpen + cb.successes = 0 + logging.Info("circuit breaker transitioning to half-open", logging.Fields{ + "name": cb.name, + }) + return true + } + return false + + case CircuitHalfOpen: + // Allow probe requests through + return true + + default: + return false + } +} + +// recordFailure records a failed request +func (cb *CircuitBreaker) recordFailure() { + cb.mu.Lock() + defer cb.mu.Unlock() + + cb.failures++ + cb.lastFailure = time.Now() + + switch cb.state { + case CircuitClosed: + if cb.failures >= cb.config.FailureThreshold { + cb.state = CircuitOpen + logging.Warn("circuit breaker opened", logging.Fields{ + "name": cb.name, + "failures": cb.failures, + }) + } + + case CircuitHalfOpen: + // Probe failed, back to open + cb.state = CircuitOpen + logging.Warn("circuit breaker probe failed, reopening", logging.Fields{ + "name": cb.name, + }) + } +} + +// recordSuccess records a successful request +func (cb *CircuitBreaker) recordSuccess(result interface{}) { + cb.mu.Lock() + defer cb.mu.Unlock() + + // Cache the successful result + cb.cachedResult = result + cb.lastCacheTime = time.Now() + cb.cachedErr = nil + + switch cb.state { + case CircuitClosed: + // Reset failure count on success + cb.failures = 0 + + case CircuitHalfOpen: + cb.successes++ + if cb.successes >= cb.config.SuccessThreshold { + cb.state = CircuitClosed + cb.failures = 0 + logging.Info("circuit breaker closed after successful probe", logging.Fields{ + "name": cb.name, + }) + } + } +} + +// Reset manually resets the circuit breaker to closed state +func (cb *CircuitBreaker) Reset() { + cb.mu.Lock() + defer cb.mu.Unlock() + + cb.state = CircuitClosed + cb.failures = 0 + cb.successes = 0 + logging.Debug("circuit breaker manually reset", logging.Fields{ + "name": cb.name, + }) +} + +// GetCached returns the cached result if available +func (cb *CircuitBreaker) GetCached() (interface{}, bool) { + cb.mu.RLock() + defer cb.mu.RUnlock() + + if cb.cachedResult != nil && time.Since(cb.lastCacheTime) < cb.cacheDuration { + return cb.cachedResult, true + } + return nil, false +} + +// Global circuit breaker for GitHub API +var ( + githubCircuitBreaker *CircuitBreaker + githubCircuitBreakerOnce sync.Once +) + +// getGitHubCircuitBreaker returns the shared GitHub API circuit breaker +func getGitHubCircuitBreaker() *CircuitBreaker { + githubCircuitBreakerOnce.Do(func() { + githubCircuitBreaker = NewCircuitBreaker("github-api", CircuitBreakerConfig{ + FailureThreshold: 3, + ResetTimeout: 60 * time.Second, // Wait 1 minute before retrying + SuccessThreshold: 1, + }) + }) + return githubCircuitBreaker +} diff --git a/mining/circuit_breaker_test.go b/mining/circuit_breaker_test.go new file mode 100644 index 0000000..03363b0 --- /dev/null +++ b/mining/circuit_breaker_test.go @@ -0,0 +1,334 @@ +package mining + +import ( + "errors" + "sync" + "testing" + "time" +) + +func TestCircuitBreakerDefaultConfig(t *testing.T) { + cfg := DefaultCircuitBreakerConfig() + + if cfg.FailureThreshold != 3 { + t.Errorf("expected FailureThreshold 3, got %d", cfg.FailureThreshold) + } + if cfg.ResetTimeout != 30*time.Second { + t.Errorf("expected ResetTimeout 30s, got %v", cfg.ResetTimeout) + } + if cfg.SuccessThreshold != 1 { + t.Errorf("expected SuccessThreshold 1, got %d", cfg.SuccessThreshold) + } +} + +func TestCircuitBreakerStateString(t *testing.T) { + tests := []struct { + state CircuitState + expected string + }{ + {CircuitClosed, "closed"}, + {CircuitOpen, "open"}, + {CircuitHalfOpen, "half-open"}, + {CircuitState(99), "unknown"}, + } + + for _, tt := range tests { + if got := tt.state.String(); got != tt.expected { + t.Errorf("state %d: expected %s, got %s", tt.state, tt.expected, got) + } + } +} + +func TestCircuitBreakerClosed(t *testing.T) { + cb := NewCircuitBreaker("test", DefaultCircuitBreakerConfig()) + + if cb.State() != CircuitClosed { + t.Error("expected initial state to be closed") + } + + // Successful execution + result, err := cb.Execute(func() (interface{}, error) { + return "success", nil + }) + + if err != nil { + t.Errorf("unexpected error: %v", err) + } + if result != "success" { + t.Errorf("expected 'success', got %v", result) + } + if cb.State() != CircuitClosed { + t.Error("state should still be closed after success") + } +} + +func TestCircuitBreakerOpensAfterFailures(t *testing.T) { + cfg := CircuitBreakerConfig{ + FailureThreshold: 2, + ResetTimeout: time.Minute, + SuccessThreshold: 1, + } + cb := NewCircuitBreaker("test", cfg) + + testErr := errors.New("test error") + + // First failure + _, err := cb.Execute(func() (interface{}, error) { + return nil, testErr + }) + if err != testErr { + t.Errorf("expected test error, got %v", err) + } + if cb.State() != CircuitClosed { + t.Error("should still be closed after 1 failure") + } + + // Second failure - should open circuit + _, err = cb.Execute(func() (interface{}, error) { + return nil, testErr + }) + if err != testErr { + t.Errorf("expected test error, got %v", err) + } + if cb.State() != CircuitOpen { + t.Error("should be open after 2 failures") + } +} + +func TestCircuitBreakerRejectsWhenOpen(t *testing.T) { + cfg := CircuitBreakerConfig{ + FailureThreshold: 1, + ResetTimeout: time.Hour, // Long timeout to keep circuit open + SuccessThreshold: 1, + } + cb := NewCircuitBreaker("test", cfg) + + // Open the circuit + cb.Execute(func() (interface{}, error) { + return nil, errors.New("fail") + }) + + if cb.State() != CircuitOpen { + t.Fatal("circuit should be open") + } + + // Next request should be rejected + called := false + _, err := cb.Execute(func() (interface{}, error) { + called = true + return "should not run", nil + }) + + if called { + t.Error("function should not have been called when circuit is open") + } + if err != ErrCircuitOpen { + t.Errorf("expected ErrCircuitOpen, got %v", err) + } +} + +func TestCircuitBreakerTransitionsToHalfOpen(t *testing.T) { + cfg := CircuitBreakerConfig{ + FailureThreshold: 1, + ResetTimeout: 50 * time.Millisecond, + SuccessThreshold: 1, + } + cb := NewCircuitBreaker("test", cfg) + + // Open the circuit + cb.Execute(func() (interface{}, error) { + return nil, errors.New("fail") + }) + + if cb.State() != CircuitOpen { + t.Fatal("circuit should be open") + } + + // Wait for reset timeout + time.Sleep(100 * time.Millisecond) + + // Next request should transition to half-open and execute + result, err := cb.Execute(func() (interface{}, error) { + return "probe success", nil + }) + + if err != nil { + t.Errorf("unexpected error: %v", err) + } + if result != "probe success" { + t.Errorf("expected 'probe success', got %v", result) + } + if cb.State() != CircuitClosed { + t.Error("should be closed after successful probe") + } +} + +func TestCircuitBreakerHalfOpenFailureReopens(t *testing.T) { + cfg := CircuitBreakerConfig{ + FailureThreshold: 1, + ResetTimeout: 50 * time.Millisecond, + SuccessThreshold: 1, + } + cb := NewCircuitBreaker("test", cfg) + + // Open the circuit + cb.Execute(func() (interface{}, error) { + return nil, errors.New("fail") + }) + + // Wait for reset timeout + time.Sleep(100 * time.Millisecond) + + // Probe fails + cb.Execute(func() (interface{}, error) { + return nil, errors.New("probe failed") + }) + + if cb.State() != CircuitOpen { + t.Error("should be open after probe failure") + } +} + +func TestCircuitBreakerCaching(t *testing.T) { + cfg := CircuitBreakerConfig{ + FailureThreshold: 1, + ResetTimeout: time.Hour, + SuccessThreshold: 1, + } + cb := NewCircuitBreaker("test", cfg) + + // Successful call - caches result + result, err := cb.Execute(func() (interface{}, error) { + return "cached value", nil + }) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if result != "cached value" { + t.Fatalf("expected 'cached value', got %v", result) + } + + // Open the circuit + cb.Execute(func() (interface{}, error) { + return nil, errors.New("fail") + }) + + // Should return cached value when circuit is open + result, err = cb.Execute(func() (interface{}, error) { + return "should not run", nil + }) + + if err != nil { + t.Errorf("expected cached result, got error: %v", err) + } + if result != "cached value" { + t.Errorf("expected 'cached value', got %v", result) + } +} + +func TestCircuitBreakerGetCached(t *testing.T) { + cb := NewCircuitBreaker("test", DefaultCircuitBreakerConfig()) + + // No cache initially + _, ok := cb.GetCached() + if ok { + t.Error("expected no cached value initially") + } + + // Cache a value + cb.Execute(func() (interface{}, error) { + return "test value", nil + }) + + cached, ok := cb.GetCached() + if !ok { + t.Error("expected cached value") + } + if cached != "test value" { + t.Errorf("expected 'test value', got %v", cached) + } +} + +func TestCircuitBreakerReset(t *testing.T) { + cfg := CircuitBreakerConfig{ + FailureThreshold: 1, + ResetTimeout: time.Hour, + SuccessThreshold: 1, + } + cb := NewCircuitBreaker("test", cfg) + + // Open the circuit + cb.Execute(func() (interface{}, error) { + return nil, errors.New("fail") + }) + + if cb.State() != CircuitOpen { + t.Fatal("circuit should be open") + } + + // Manual reset + cb.Reset() + + if cb.State() != CircuitClosed { + t.Error("circuit should be closed after reset") + } +} + +func TestCircuitBreakerConcurrency(t *testing.T) { + cb := NewCircuitBreaker("test", DefaultCircuitBreakerConfig()) + + var wg sync.WaitGroup + for i := 0; i < 100; i++ { + wg.Add(1) + go func(n int) { + defer wg.Done() + cb.Execute(func() (interface{}, error) { + if n%3 == 0 { + return nil, errors.New("fail") + } + return "success", nil + }) + }(i) + } + wg.Wait() + + // Just verify no panics occurred + _ = cb.State() +} + +func TestGetGitHubCircuitBreaker(t *testing.T) { + cb1 := getGitHubCircuitBreaker() + cb2 := getGitHubCircuitBreaker() + + if cb1 != cb2 { + t.Error("expected singleton circuit breaker") + } + + if cb1.name != "github-api" { + t.Errorf("expected name 'github-api', got %s", cb1.name) + } +} + +// Benchmark tests +func BenchmarkCircuitBreakerExecute(b *testing.B) { + cb := NewCircuitBreaker("bench", DefaultCircuitBreakerConfig()) + + b.ResetTimer() + for i := 0; i < b.N; i++ { + cb.Execute(func() (interface{}, error) { + return "result", nil + }) + } +} + +func BenchmarkCircuitBreakerConcurrent(b *testing.B) { + cb := NewCircuitBreaker("bench", DefaultCircuitBreakerConfig()) + + b.RunParallel(func(pb *testing.PB) { + for pb.Next() { + cb.Execute(func() (interface{}, error) { + return "result", nil + }) + } + }) +} diff --git a/mining/component.go b/mining/component.go new file mode 100644 index 0000000..d4c86df --- /dev/null +++ b/mining/component.go @@ -0,0 +1,20 @@ +package mining + +import ( + "embed" + "io/fs" + "net/http" +) + +//go:embed component/* +var componentFS embed.FS + +// GetComponentFS returns the embedded file system containing the web component. +// This allows the component to be served even when the package is used as a module. +func GetComponentFS() (http.FileSystem, error) { + sub, err := fs.Sub(componentFS, "component") + if err != nil { + return nil, err + } + return http.FS(sub), nil +} diff --git a/mining/component/mining-dashboard.js b/mining/component/mining-dashboard.js new file mode 100644 index 0000000..767fb75 --- /dev/null +++ b/mining/component/mining-dashboard.js @@ -0,0 +1,423 @@ +(()=>{"use strict";const se=globalThis;function ee(e){return(se.__Zone_symbol_prefix||"__zone_symbol__")+e}const me=Object.getOwnPropertyDescriptor,Se=Object.defineProperty,Oe=Object.getPrototypeOf,dt=Object.create,_t=Array.prototype.slice,Ne="addEventListener",Ze="removeEventListener",Le=ee(Ne),Ie=ee(Ze),ae="true",le="false",ye=ee("");function Me(e,r){return Zone.current.wrap(e,r)}function Ae(e,r,c,t,i){return Zone.current.scheduleMacroTask(e,r,c,t,i)}const j=ee,Pe=typeof window<"u",pe=Pe?window:void 0,$=Pe&&pe||globalThis;function je(e,r){for(let c=e.length-1;c>=0;c--)"function"==typeof e[c]&&(e[c]=Me(e[c],r+"_"+c));return e}function Be(e){return!e||!1!==e.writable&&!("function"==typeof e.get&&typeof e.set>"u")}const ze=typeof WorkerGlobalScope<"u"&&self instanceof WorkerGlobalScope,Re=!("nw"in $)&&typeof $.process<"u"&&"[object process]"===$.process.toString(),He=!Re&&!ze&&!(!Pe||!pe.HTMLElement),Ue=typeof $.process<"u"&&"[object process]"===$.process.toString()&&!ze&&!(!Pe||!pe.HTMLElement),Ce={},gt=j("enable_beforeunload"),We=function(e){if(!(e=e||$.event))return;let r=Ce[e.type];r||(r=Ce[e.type]=j("ON_PROPERTY"+e.type));const c=this||e.target||$,t=c[r];let i;return He&&c===pe&&"error"===e.type?(i=t&&t.call(this,e.message,e.filename,e.lineno,e.colno,e.error),!0===i&&e.preventDefault()):(i=t&&t.apply(this,arguments),"beforeunload"===e.type&&$[gt]&&"string"==typeof i?e.returnValue=i:null!=i&&!i&&e.preventDefault()),i};function qe(e,r,c){let t=me(e,r);if(!t&&c&&me(c,r)&&(t={enumerable:!0,configurable:!0}),!t||!t.configurable)return;const i=j("on"+r+"patched");if(e.hasOwnProperty(i)&&e[i])return;delete t.writable,delete t.value;const u=t.get,E=t.set,T=r.slice(2);let m=Ce[T];m||(m=Ce[T]=j("ON_PROPERTY"+T)),t.set=function(D){let d=this;!d&&e===$&&(d=$),d&&("function"==typeof d[m]&&d.removeEventListener(T,We),E?.call(d,null),d[m]=D,"function"==typeof D&&d.addEventListener(T,We,!1))},t.get=function(){let D=this;if(!D&&e===$&&(D=$),!D)return null;const d=D[m];if(d)return d;if(u){let R=u.call(this);if(R)return t.set.call(this,R),"function"==typeof D.removeAttribute&&D.removeAttribute(r),R}return null},Se(e,r,t),e[i]=!0}function Xe(e,r,c){if(r)for(let t=0;tfunction(E,T){const m=c(E,T);return m.cbIdx>=0&&"function"==typeof T[m.cbIdx]?Ae(m.name,T[m.cbIdx],m,i):u.apply(E,T)})}function fe(e,r){e[j("OriginalDelegate")]=r}let Ye=!1,Ve=!1;function mt(){if(Ye)return Ve;Ye=!0;try{const e=pe.navigator.userAgent;(-1!==e.indexOf("MSIE ")||-1!==e.indexOf("Trident/")||-1!==e.indexOf("Edge/"))&&(Ve=!0)}catch{}return Ve}function $e(e){return"function"==typeof e}function Je(e){return"number"==typeof e}const yt={useG:!0},te={},Ke={},Qe=new RegExp("^"+ye+"(\\w+)(true|false)$"),et=j("propagationStopped");function tt(e,r){const c=(r?r(e):e)+le,t=(r?r(e):e)+ae,i=ye+c,u=ye+t;te[e]={},te[e][le]=i,te[e][ae]=u}function pt(e,r,c,t){const i=t&&t.add||Ne,u=t&&t.rm||Ze,E=t&&t.listeners||"eventListeners",T=t&&t.rmAll||"removeAllListeners",m=j(i),D="."+i+":",d="prependListener",R="."+d+":",M=function(p,h,H){if(p.isRemoved)return;const x=p.callback;let Y;"object"==typeof x&&x.handleEvent&&(p.callback=g=>x.handleEvent(g),p.originalDelegate=x);try{p.invoke(p,h,[H])}catch(g){Y=g}const F=p.options;return F&&"object"==typeof F&&F.once&&h[u].call(h,H.type,p.originalDelegate?p.originalDelegate:p.callback,F),Y};function V(p,h,H){if(!(h=h||e.event))return;const x=p||h.target||e,Y=x[te[h.type][H?ae:le]];if(Y){const F=[];if(1===Y.length){const g=M(Y[0],x,h);g&&F.push(g)}else{const g=Y.slice();for(let U=0;U{throw U})}}}const z=function(p){return V(this,p,!1)},J=function(p){return V(this,p,!0)};function K(p,h){if(!p)return!1;let H=!0;h&&void 0!==h.useG&&(H=h.useG);const x=h&&h.vh;let Y=!0;h&&void 0!==h.chkDup&&(Y=h.chkDup);let F=!1;h&&void 0!==h.rt&&(F=h.rt);let g=p;for(;g&&!g.hasOwnProperty(i);)g=Oe(g);if(!g&&p[i]&&(g=p),!g||g[m])return!1;const U=h&&h.eventNameToString,O={},C=g[m]=g[i],b=g[j(u)]=g[u],S=g[j(E)]=g[E],Q=g[j(T)]=g[T];let W;h&&h.prepend&&(W=g[j(h.prepend)]=g[h.prepend]);const q=H?function(s){if(!O.isExisting)return C.call(O.target,O.eventName,O.capture?J:z,O.options)}:function(s){return C.call(O.target,O.eventName,s.invoke,O.options)},A=H?function(s){if(!s.isRemoved){const l=te[s.eventName];let v;l&&(v=l[s.capture?ae:le]);const w=v&&s.target[v];if(w)for(let k=0;koe.zone.cancelTask(oe);s.call(ge,"abort",ce,{once:!0}),oe.removeAbortListener=()=>ge.removeEventListener("abort",ce)}return O.target=null,be&&(be.taskData=null),it&&(O.options.once=!0),"boolean"!=typeof oe.options&&(oe.options=ie),oe.target=L,oe.capture=Fe,oe.eventName=I,B&&(oe.originalDelegate=G),Z?ke.unshift(oe):ke.push(oe),k?L:void 0}};return g[i]=a(C,D,q,A,F),W&&(g[d]=a(W,R,function(s){return W.call(O.target,O.eventName,s.invoke,O.options)},A,F,!0)),g[u]=function(){const s=this||e;let l=arguments[0];h&&h.transferEventName&&(l=h.transferEventName(l));const v=arguments[2],w=!!v&&("boolean"==typeof v||v.capture),k=arguments[1];if(!k)return b.apply(this,arguments);if(x&&!x(b,k,s,arguments))return;const Z=te[l];let L;Z&&(L=Z[w?ae:le]);const I=L&&s[L];if(I)for(let G=0;Gfunction(i,u){i[et]=!0,t&&t.apply(i,u)})}const we=j("zoneTask");function Ee(e,r,c,t){let i=null,u=null;c+=t;const E={};function T(D){const d=D.data;d.args[0]=function(){return D.invoke.apply(this,arguments)};const R=i.apply(e,d.args);return Je(R)?d.handleId=R:(d.handle=R,d.isRefreshable=$e(R.refresh)),D}function m(D){const{handle:d,handleId:R}=D.data;return u.call(e,d??R)}i=ue(e,r+=t,D=>function(d,R){if($e(R[0])){const M={isRefreshable:!1,isPeriodic:"Interval"===t,delay:"Timeout"===t||"Interval"===t?R[1]||0:void 0,args:R},V=R[0];R[0]=function(){try{return V.apply(this,arguments)}finally{const{handle:H,handleId:x,isPeriodic:Y,isRefreshable:F}=M;!Y&&!F&&(x?delete E[x]:H&&(H[we]=null))}};const z=Ae(r,R[0],M,T,m);if(!z)return z;const{handleId:J,handle:K,isRefreshable:X,isPeriodic:p}=z.data;if(J)E[J]=z;else if(K&&(K[we]=z,X&&!p)){const h=K.refresh;K.refresh=function(){const{zone:H,state:x}=z;return"notScheduled"===x?(z._state="scheduled",H._updateTaskCount(z,1)):"running"===x&&(z._state="scheduling"),h.call(this)}}return K??J??z}return D.apply(e,R)}),u=ue(e,c,D=>function(d,R){const M=R[0];let V;Je(M)?(V=E[M],delete E[M]):(V=M?.[we],V?M[we]=null:V=M),V?.type?V.cancelFn&&V.zone.cancelTask(V):D.apply(e,R)})}function rt(e,r,c){if(!c||0===c.length)return r;const t=c.filter(u=>u.target===e);if(0===t.length)return r;const i=t[0].ignoreProperties;return r.filter(u=>-1===i.indexOf(u))}function ot(e,r,c,t){e&&Xe(e,rt(e,r,c),t)}function xe(e){return Object.getOwnPropertyNames(e).filter(r=>r.startsWith("on")&&r.length>2).map(r=>r.substring(2))}function Nt(e,r,c,t,i){const u=Zone.__symbol__(t);if(r[u])return;const E=r[u]=r[t];r[t]=function(T,m,D){return m&&m.prototype&&i.forEach(function(d){const R=`${c}.${t}::`+d,M=m.prototype;try{if(M.hasOwnProperty(d)){const V=e.ObjectGetOwnPropertyDescriptor(M,d);V&&V.value?(V.value=e.wrapWithCurrentZone(V.value,R),e._redefineProperty(m.prototype,d,V)):M[d]&&(M[d]=e.wrapWithCurrentZone(M[d],R))}else M[d]&&(M[d]=e.wrapWithCurrentZone(M[d],R))}catch{}}),E.call(r,T,m,D)},e.attachOriginToPatched(r[t],E)}const st=function ht(){const e=globalThis,r=!0===e[ee("forceDuplicateZoneCheck")];if(e.Zone&&(r||"function"!=typeof e.Zone.__symbol__))throw new Error("Zone already loaded.");return e.Zone??=function ft(){const e=se.performance;function r(N){e&&e.mark&&e.mark(N)}function c(N,_){e&&e.measure&&e.measure(N,_)}r("Zone");let t=(()=>{class N{static __symbol__=ee;static assertZonePatched(){if(se.Promise!==O.ZoneAwarePromise)throw new Error("Zone.js has detected that ZoneAwarePromise `(window|global).Promise` has been overwritten.\nMost likely cause is that a Promise polyfill has been loaded after Zone.js (Polyfilling Promise api is not necessary when zone.js is loaded. If you must load one, do so before loading zone.js.)")}static get root(){let n=N.current;for(;n.parent;)n=n.parent;return n}static get current(){return b.zone}static get currentTask(){return S}static __load_patch(n,o,y=!1){if(O.hasOwnProperty(n)){const P=!0===se[ee("forceDuplicateZoneCheck")];if(!y&&P)throw Error("Already loaded patch: "+n)}else if(!se["__Zone_disable_"+n]){const P="Zone:"+n;r(P),O[n]=o(se,N,C),c(P,P)}}get parent(){return this._parent}get name(){return this._name}_parent;_name;_properties;_zoneDelegate;constructor(n,o){this._parent=n,this._name=o?o.name||"unnamed":"",this._properties=o&&o.properties||{},this._zoneDelegate=new u(this,this._parent&&this._parent._zoneDelegate,o)}get(n){const o=this.getZoneWith(n);if(o)return o._properties[n]}getZoneWith(n){let o=this;for(;o;){if(o._properties.hasOwnProperty(n))return o;o=o._parent}return null}fork(n){if(!n)throw new Error("ZoneSpec required!");return this._zoneDelegate.fork(this,n)}wrap(n,o){if("function"!=typeof n)throw new Error("Expecting function got: "+n);const y=this._zoneDelegate.intercept(this,n,o),P=this;return function(){return P.runGuarded(y,this,arguments,o)}}run(n,o,y,P){b={parent:b,zone:this};try{return this._zoneDelegate.invoke(this,n,o,y,P)}finally{b=b.parent}}runGuarded(n,o=null,y,P){b={parent:b,zone:this};try{try{return this._zoneDelegate.invoke(this,n,o,y,P)}catch(q){if(this._zoneDelegate.handleError(this,q))throw q}}finally{b=b.parent}}runTask(n,o,y){if(n.zone!=this)throw new Error("A task can only be run in the zone of creation! (Creation: "+(n.zone||K).name+"; Execution: "+this.name+")");const P=n,{type:q,data:{isPeriodic:A=!1,isRefreshable:_e=!1}={}}=n;if(n.state===X&&(q===U||q===g))return;const he=n.state!=H;he&&P._transitionTo(H,h);const de=S;S=P,b={parent:b,zone:this};try{q==g&&n.data&&!A&&!_e&&(n.cancelFn=void 0);try{return this._zoneDelegate.invokeTask(this,P,o,y)}catch(re){if(this._zoneDelegate.handleError(this,re))throw re}}finally{const re=n.state;if(re!==X&&re!==Y)if(q==U||A||_e&&re===p)he&&P._transitionTo(h,H,p);else{const f=P._zoneDelegates;this._updateTaskCount(P,-1),he&&P._transitionTo(X,H,X),_e&&(P._zoneDelegates=f)}b=b.parent,S=de}}scheduleTask(n){if(n.zone&&n.zone!==this){let y=this;for(;y;){if(y===n.zone)throw Error(`can not reschedule task to ${this.name} which is descendants of the original zone ${n.zone.name}`);y=y.parent}}n._transitionTo(p,X);const o=[];n._zoneDelegates=o,n._zone=this;try{n=this._zoneDelegate.scheduleTask(this,n)}catch(y){throw n._transitionTo(Y,p,X),this._zoneDelegate.handleError(this,y),y}return n._zoneDelegates===o&&this._updateTaskCount(n,1),n.state==p&&n._transitionTo(h,p),n}scheduleMicroTask(n,o,y,P){return this.scheduleTask(new E(F,n,o,y,P,void 0))}scheduleMacroTask(n,o,y,P,q){return this.scheduleTask(new E(g,n,o,y,P,q))}scheduleEventTask(n,o,y,P,q){return this.scheduleTask(new E(U,n,o,y,P,q))}cancelTask(n){if(n.zone!=this)throw new Error("A task can only be cancelled in the zone of creation! (Creation: "+(n.zone||K).name+"; Execution: "+this.name+")");if(n.state===h||n.state===H){n._transitionTo(x,h,H);try{this._zoneDelegate.cancelTask(this,n)}catch(o){throw n._transitionTo(Y,x),this._zoneDelegate.handleError(this,o),o}return this._updateTaskCount(n,-1),n._transitionTo(X,x),n.runCount=-1,n}}_updateTaskCount(n,o){const y=n._zoneDelegates;-1==o&&(n._zoneDelegates=null);for(let P=0;PN.hasTask(n,o),onScheduleTask:(N,_,n,o)=>N.scheduleTask(n,o),onInvokeTask:(N,_,n,o,y,P)=>N.invokeTask(n,o,y,P),onCancelTask:(N,_,n,o)=>N.cancelTask(n,o)};class u{get zone(){return this._zone}_zone;_taskCounts={microTask:0,macroTask:0,eventTask:0};_parentDelegate;_forkDlgt;_forkZS;_forkCurrZone;_interceptDlgt;_interceptZS;_interceptCurrZone;_invokeDlgt;_invokeZS;_invokeCurrZone;_handleErrorDlgt;_handleErrorZS;_handleErrorCurrZone;_scheduleTaskDlgt;_scheduleTaskZS;_scheduleTaskCurrZone;_invokeTaskDlgt;_invokeTaskZS;_invokeTaskCurrZone;_cancelTaskDlgt;_cancelTaskZS;_cancelTaskCurrZone;_hasTaskDlgt;_hasTaskDlgtOwner;_hasTaskZS;_hasTaskCurrZone;constructor(_,n,o){this._zone=_,this._parentDelegate=n,this._forkZS=o&&(o&&o.onFork?o:n._forkZS),this._forkDlgt=o&&(o.onFork?n:n._forkDlgt),this._forkCurrZone=o&&(o.onFork?this._zone:n._forkCurrZone),this._interceptZS=o&&(o.onIntercept?o:n._interceptZS),this._interceptDlgt=o&&(o.onIntercept?n:n._interceptDlgt),this._interceptCurrZone=o&&(o.onIntercept?this._zone:n._interceptCurrZone),this._invokeZS=o&&(o.onInvoke?o:n._invokeZS),this._invokeDlgt=o&&(o.onInvoke?n:n._invokeDlgt),this._invokeCurrZone=o&&(o.onInvoke?this._zone:n._invokeCurrZone),this._handleErrorZS=o&&(o.onHandleError?o:n._handleErrorZS),this._handleErrorDlgt=o&&(o.onHandleError?n:n._handleErrorDlgt),this._handleErrorCurrZone=o&&(o.onHandleError?this._zone:n._handleErrorCurrZone),this._scheduleTaskZS=o&&(o.onScheduleTask?o:n._scheduleTaskZS),this._scheduleTaskDlgt=o&&(o.onScheduleTask?n:n._scheduleTaskDlgt),this._scheduleTaskCurrZone=o&&(o.onScheduleTask?this._zone:n._scheduleTaskCurrZone),this._invokeTaskZS=o&&(o.onInvokeTask?o:n._invokeTaskZS),this._invokeTaskDlgt=o&&(o.onInvokeTask?n:n._invokeTaskDlgt),this._invokeTaskCurrZone=o&&(o.onInvokeTask?this._zone:n._invokeTaskCurrZone),this._cancelTaskZS=o&&(o.onCancelTask?o:n._cancelTaskZS),this._cancelTaskDlgt=o&&(o.onCancelTask?n:n._cancelTaskDlgt),this._cancelTaskCurrZone=o&&(o.onCancelTask?this._zone:n._cancelTaskCurrZone),this._hasTaskZS=null,this._hasTaskDlgt=null,this._hasTaskDlgtOwner=null,this._hasTaskCurrZone=null;const y=o&&o.onHasTask;(y||n&&n._hasTaskZS)&&(this._hasTaskZS=y?o:i,this._hasTaskDlgt=n,this._hasTaskDlgtOwner=this,this._hasTaskCurrZone=this._zone,o.onScheduleTask||(this._scheduleTaskZS=i,this._scheduleTaskDlgt=n,this._scheduleTaskCurrZone=this._zone),o.onInvokeTask||(this._invokeTaskZS=i,this._invokeTaskDlgt=n,this._invokeTaskCurrZone=this._zone),o.onCancelTask||(this._cancelTaskZS=i,this._cancelTaskDlgt=n,this._cancelTaskCurrZone=this._zone))}fork(_,n){return this._forkZS?this._forkZS.onFork(this._forkDlgt,this.zone,_,n):new t(_,n)}intercept(_,n,o){return this._interceptZS?this._interceptZS.onIntercept(this._interceptDlgt,this._interceptCurrZone,_,n,o):n}invoke(_,n,o,y,P){return this._invokeZS?this._invokeZS.onInvoke(this._invokeDlgt,this._invokeCurrZone,_,n,o,y,P):n.apply(o,y)}handleError(_,n){return!this._handleErrorZS||this._handleErrorZS.onHandleError(this._handleErrorDlgt,this._handleErrorCurrZone,_,n)}scheduleTask(_,n){let o=n;if(this._scheduleTaskZS)this._hasTaskZS&&o._zoneDelegates.push(this._hasTaskDlgtOwner),o=this._scheduleTaskZS.onScheduleTask(this._scheduleTaskDlgt,this._scheduleTaskCurrZone,_,n),o||(o=n);else if(n.scheduleFn)n.scheduleFn(n);else{if(n.type!=F)throw new Error("Task is missing scheduleFn.");z(n)}return o}invokeTask(_,n,o,y){return this._invokeTaskZS?this._invokeTaskZS.onInvokeTask(this._invokeTaskDlgt,this._invokeTaskCurrZone,_,n,o,y):n.callback.apply(o,y)}cancelTask(_,n){let o;if(this._cancelTaskZS)o=this._cancelTaskZS.onCancelTask(this._cancelTaskDlgt,this._cancelTaskCurrZone,_,n);else{if(!n.cancelFn)throw Error("Task is not cancelable");o=n.cancelFn(n)}return o}hasTask(_,n){try{this._hasTaskZS&&this._hasTaskZS.onHasTask(this._hasTaskDlgt,this._hasTaskCurrZone,_,n)}catch(o){this.handleError(_,o)}}_updateTaskCount(_,n){const o=this._taskCounts,y=o[_],P=o[_]=y+n;if(P<0)throw new Error("More tasks executed then were scheduled.");0!=y&&0!=P||this.hasTask(this._zone,{microTask:o.microTask>0,macroTask:o.macroTask>0,eventTask:o.eventTask>0,change:_})}}class E{type;source;invoke;callback;data;scheduleFn;cancelFn;_zone=null;runCount=0;_zoneDelegates=null;_state="notScheduled";constructor(_,n,o,y,P,q){if(this.type=_,this.source=n,this.data=y,this.scheduleFn=P,this.cancelFn=q,!o)throw new Error("callback is not defined");this.callback=o;const A=this;this.invoke=_===U&&y&&y.useG?E.invokeTask:function(){return E.invokeTask.call(se,A,this,arguments)}}static invokeTask(_,n,o){_||(_=this),Q++;try{return _.runCount++,_.zone.runTask(_,n,o)}finally{1==Q&&J(),Q--}}get zone(){return this._zone}get state(){return this._state}cancelScheduleRequest(){this._transitionTo(X,p)}_transitionTo(_,n,o){if(this._state!==n&&this._state!==o)throw new Error(`${this.type} '${this.source}': can not transition to '${_}', expecting state '${n}'${o?" or '"+o+"'":""}, was '${this._state}'.`);this._state=_,_==X&&(this._zoneDelegates=null)}toString(){return this.data&&typeof this.data.handleId<"u"?this.data.handleId.toString():Object.prototype.toString.call(this)}toJSON(){return{type:this.type,state:this.state,source:this.source,zone:this.zone.name,runCount:this.runCount}}}const T=ee("setTimeout"),m=ee("Promise"),D=ee("then");let M,d=[],R=!1;function V(N){if(M||se[m]&&(M=se[m].resolve(0)),M){let _=M[D];_||(_=M.then),_.call(M,N)}else se[T](N,0)}function z(N){0===Q&&0===d.length&&V(J),N&&d.push(N)}function J(){if(!R){for(R=!0;d.length;){const N=d;d=[];for(let _=0;_b,onUnhandledError:W,microtaskDrainDone:W,scheduleMicroTask:z,showUncaughtError:()=>!t[ee("ignoreConsoleErrorUncaughtError")],patchEventTarget:()=>[],patchOnProperties:W,patchMethod:()=>W,bindArguments:()=>[],patchThen:()=>W,patchMacroTask:()=>W,patchEventPrototype:()=>W,isIEOrEdge:()=>!1,getGlobalObjects:()=>{},ObjectDefineProperty:()=>W,ObjectGetOwnPropertyDescriptor:()=>{},ObjectCreate:()=>{},ArraySlice:()=>[],patchClass:()=>W,wrapWithCurrentZone:()=>W,filterProperties:()=>[],attachOriginToPatched:()=>W,_redefineProperty:()=>W,patchCallbacks:()=>W,nativeScheduleMicroTask:V};let b={parent:null,zone:new t(null,null)},S=null,Q=0;function W(){}return c("Zone","Zone"),t}(),e.Zone}();(function Lt(e){(function St(e){e.__load_patch("ZoneAwarePromise",(r,c,t)=>{const i=Object.getOwnPropertyDescriptor,u=Object.defineProperty,T=t.symbol,m=[],D=!1!==r[T("DISABLE_WRAPPING_UNCAUGHT_PROMISE_REJECTION")],d=T("Promise"),R=T("then");t.onUnhandledError=f=>{if(t.showUncaughtError()){const a=f&&f.rejection;a?console.error("Unhandled Promise rejection:",a instanceof Error?a.message:a,"; Zone:",f.zone.name,"; Task:",f.task&&f.task.source,"; Value:",a,a instanceof Error?a.stack:void 0):console.error(f)}},t.microtaskDrainDone=()=>{for(;m.length;){const f=m.shift();try{f.zone.runGuarded(()=>{throw f.throwOriginal?f.rejection:f})}catch(a){z(a)}}};const V=T("unhandledPromiseRejectionHandler");function z(f){t.onUnhandledError(f);try{const a=c[V];"function"==typeof a&&a.call(this,f)}catch{}}function J(f){return f&&"function"==typeof f.then}function K(f){return f}function X(f){return A.reject(f)}const p=T("state"),h=T("value"),H=T("finally"),x=T("parentPromiseValue"),Y=T("parentPromiseState"),g=null,U=!0,O=!1;function b(f,a){return s=>{try{N(f,a,s)}catch(l){N(f,!1,l)}}}const S=function(){let f=!1;return function(s){return function(){f||(f=!0,s.apply(null,arguments))}}},Q="Promise resolved with itself",W=T("currentTaskTrace");function N(f,a,s){const l=S();if(f===s)throw new TypeError(Q);if(f[p]===g){let v=null;try{("object"==typeof s||"function"==typeof s)&&(v=s&&s.then)}catch(w){return l(()=>{N(f,!1,w)})(),f}if(a!==O&&s instanceof A&&s.hasOwnProperty(p)&&s.hasOwnProperty(h)&&s[p]!==g)n(s),N(f,s[p],s[h]);else if(a!==O&&"function"==typeof v)try{v.call(s,l(b(f,a)),l(b(f,!1)))}catch(w){l(()=>{N(f,!1,w)})()}else{f[p]=a;const w=f[h];if(f[h]=s,f[H]===H&&a===U&&(f[p]=f[Y],f[h]=f[x]),a===O&&s instanceof Error){const k=c.currentTask&&c.currentTask.data&&c.currentTask.data.__creationTrace__;k&&u(s,W,{configurable:!0,enumerable:!1,writable:!0,value:k})}for(let k=0;k{try{const Z=f[h],L=!!s&&H===s[H];L&&(s[x]=Z,s[Y]=w);const I=a.run(k,void 0,L&&k!==X&&k!==K?[]:[Z]);N(s,!0,I)}catch(Z){N(s,!1,Z)}},s)}const P=function(){},q=r.AggregateError;class A{static toString(){return"function ZoneAwarePromise() { [native code] }"}static resolve(a){return a instanceof A?a:N(new this(null),U,a)}static reject(a){return N(new this(null),O,a)}static withResolvers(){const a={};return a.promise=new A((s,l)=>{a.resolve=s,a.reject=l}),a}static any(a){if(!a||"function"!=typeof a[Symbol.iterator])return Promise.reject(new q([],"All promises were rejected"));const s=[];let l=0;try{for(let k of a)l++,s.push(A.resolve(k))}catch{return Promise.reject(new q([],"All promises were rejected"))}if(0===l)return Promise.reject(new q([],"All promises were rejected"));let v=!1;const w=[];return new A((k,Z)=>{for(let L=0;L{v||(v=!0,k(I))},I=>{w.push(I),l--,0===l&&(v=!0,Z(new q(w,"All promises were rejected")))})})}static race(a){let s,l,v=new this((Z,L)=>{s=Z,l=L});function w(Z){s(Z)}function k(Z){l(Z)}for(let Z of a)J(Z)||(Z=this.resolve(Z)),Z.then(w,k);return v}static all(a){return A.allWithCallback(a)}static allSettled(a){return(this&&this.prototype instanceof A?this:A).allWithCallback(a,{thenCallback:l=>({status:"fulfilled",value:l}),errorCallback:l=>({status:"rejected",reason:l})})}static allWithCallback(a,s){let l,v,w=new this((I,G)=>{l=I,v=G}),k=2,Z=0;const L=[];for(let I of a){J(I)||(I=this.resolve(I));const G=Z;try{I.then(B=>{L[G]=s?s.thenCallback(B):B,k--,0===k&&l(L)},B=>{s?(L[G]=s.errorCallback(B),k--,0===k&&l(L)):v(B)})}catch(B){v(B)}k++,Z++}return k-=2,0===k&&l(L),w}constructor(a){const s=this;if(!(s instanceof A))throw new Error("Must be an instanceof Promise.");s[p]=g,s[h]=[];try{const l=S();a&&a(l(b(s,U)),l(b(s,O)))}catch(l){N(s,!1,l)}}get[Symbol.toStringTag](){return"Promise"}get[Symbol.species](){return A}then(a,s){let l=this.constructor?.[Symbol.species];(!l||"function"!=typeof l)&&(l=this.constructor||A);const v=new l(P),w=c.current;return this[p]==g?this[h].push(w,v,a,s):o(this,w,v,a,s),v}catch(a){return this.then(null,a)}finally(a){let s=this.constructor?.[Symbol.species];(!s||"function"!=typeof s)&&(s=A);const l=new s(P);l[H]=H;const v=c.current;return this[p]==g?this[h].push(v,l,a,a):o(this,v,l,a,a),l}}A.resolve=A.resolve,A.reject=A.reject,A.race=A.race,A.all=A.all;const _e=r[d]=r.Promise;r.Promise=A;const he=T("thenPatched");function de(f){const a=f.prototype,s=i(a,"then");if(s&&(!1===s.writable||!s.configurable))return;const l=a.then;a[R]=l,f.prototype.then=function(v,w){return new A((Z,L)=>{l.call(this,Z,L)}).then(v,w)},f[he]=!0}return t.patchThen=de,_e&&(de(_e),ue(r,"fetch",f=>function re(f){return function(a,s){let l=f.apply(a,s);if(l instanceof A)return l;let v=l.constructor;return v[he]||de(v),l}}(f))),Promise[c.__symbol__("uncaughtPromiseErrors")]=m,A})})(e),function Ot(e){e.__load_patch("toString",r=>{const c=Function.prototype.toString,t=j("OriginalDelegate"),i=j("Promise"),u=j("Error"),E=function(){if("function"==typeof this){const d=this[t];if(d)return"function"==typeof d?c.call(d):Object.prototype.toString.call(d);if(this===Promise){const R=r[i];if(R)return c.call(R)}if(this===Error){const R=r[u];if(R)return c.call(R)}}return c.call(this)};E[t]=c,Function.prototype.toString=E;const T=Object.prototype.toString;Object.prototype.toString=function(){return"function"==typeof Promise&&this instanceof Promise?"[object Promise]":T.call(this)}})}(e),function Zt(e){e.__load_patch("util",(r,c,t)=>{const i=xe(r);t.patchOnProperties=Xe,t.patchMethod=ue,t.bindArguments=je,t.patchMacroTask=kt;const u=c.__symbol__("BLACK_LISTED_EVENTS"),E=c.__symbol__("UNPATCHED_EVENTS");r[E]&&(r[u]=r[E]),r[u]&&(c[u]=c[E]=r[u]),t.patchEventPrototype=vt,t.patchEventTarget=pt,t.isIEOrEdge=mt,t.ObjectDefineProperty=Se,t.ObjectGetOwnPropertyDescriptor=me,t.ObjectCreate=dt,t.ArraySlice=_t,t.patchClass=ve,t.wrapWithCurrentZone=Me,t.filterProperties=rt,t.attachOriginToPatched=fe,t._redefineProperty=Object.defineProperty,t.patchCallbacks=Nt,t.getGlobalObjects=()=>({globalSources:Ke,zoneSymbolEventNames:te,eventNames:i,isBrowser:He,isMix:Ue,isNode:Re,TRUE_STR:ae,FALSE_STR:le,ZONE_SYMBOL_PREFIX:ye,ADD_EVENT_LISTENER_STR:Ne,REMOVE_EVENT_LISTENER_STR:Ze})})}(e)})(st),function Dt(e){e.__load_patch("legacy",r=>{const c=r[e.__symbol__("legacyPatch")];c&&c()}),e.__load_patch("timers",r=>{const t="clear";Ee(r,"set",t,"Timeout"),Ee(r,"set",t,"Interval"),Ee(r,"set",t,"Immediate")}),e.__load_patch("requestAnimationFrame",r=>{Ee(r,"request","cancel","AnimationFrame"),Ee(r,"mozRequest","mozCancel","AnimationFrame"),Ee(r,"webkitRequest","webkitCancel","AnimationFrame")}),e.__load_patch("blocking",(r,c)=>{const t=["alert","prompt","confirm"];for(let i=0;ifunction(D,d){return c.current.run(E,r,d,m)})}),e.__load_patch("EventTarget",(r,c,t)=>{(function Ct(e,r){r.patchEventPrototype(e,r)})(r,t),function Rt(e,r){if(Zone[r.symbol("patchEventTarget")])return;const{eventNames:c,zoneSymbolEventNames:t,TRUE_STR:i,FALSE_STR:u,ZONE_SYMBOL_PREFIX:E}=r.getGlobalObjects();for(let m=0;m{ve("MutationObserver"),ve("WebKitMutationObserver")}),e.__load_patch("IntersectionObserver",(r,c,t)=>{ve("IntersectionObserver")}),e.__load_patch("FileReader",(r,c,t)=>{ve("FileReader")}),e.__load_patch("on_property",(r,c,t)=>{!function wt(e,r){if(Re&&!Ue||Zone[e.symbol("patchEvents")])return;const c=r.__Zone_ignore_on_properties;let t=[];if(He){const i=window;t=t.concat(["Document","SVGElement","Element","HTMLElement","HTMLBodyElement","HTMLMediaElement","HTMLFrameSetElement","HTMLFrameElement","HTMLIFrameElement","HTMLMarqueeElement","Worker"]);const u=[];ot(i,xe(i),c&&c.concat(u),Oe(i))}t=t.concat(["XMLHttpRequest","XMLHttpRequestEventTarget","IDBIndex","IDBRequest","IDBOpenDBRequest","IDBDatabase","IDBTransaction","IDBCursor","WebSocket"]);for(let i=0;i{!function Pt(e,r){const{isBrowser:c,isMix:t}=r.getGlobalObjects();(c||t)&&e.customElements&&"customElements"in e&&r.patchCallbacks(r,e.customElements,"customElements","define",["connectedCallback","disconnectedCallback","adoptedCallback","attributeChangedCallback","formAssociatedCallback","formDisabledCallback","formResetCallback","formStateRestoreCallback"])}(r,t)}),e.__load_patch("XHR",(r,c)=>{!function D(d){const R=d.XMLHttpRequest;if(!R)return;const M=R.prototype;let z=M[Le],J=M[Ie];if(!z){const C=d.XMLHttpRequestEventTarget;if(C){const b=C.prototype;z=b[Le],J=b[Ie]}}const K="readystatechange",X="scheduled";function p(C){const b=C.data,S=b.target;S[E]=!1,S[m]=!1;const Q=S[u];z||(z=S[Le],J=S[Ie]),Q&&J.call(S,K,Q);const W=S[u]=()=>{if(S.readyState===S.DONE)if(!b.aborted&&S[E]&&C.state===X){const _=S[c.__symbol__("loadfalse")];if(0!==S.status&&_&&_.length>0){const n=C.invoke;C.invoke=function(){const o=S[c.__symbol__("loadfalse")];for(let y=0;yfunction(C,b){return C[i]=0==b[2],C[T]=b[1],x.apply(C,b)}),F=j("fetchTaskAborting"),g=j("fetchTaskScheduling"),U=ue(M,"send",()=>function(C,b){if(!0===c.current[g]||C[i])return U.apply(C,b);{const S={target:C,url:C[T],isPeriodic:!1,args:b,aborted:!1},Q=Ae("XMLHttpRequest.send",h,S,p,H);C&&!0===C[m]&&!S.aborted&&Q.state===X&&Q.invoke()}}),O=ue(M,"abort",()=>function(C,b){const S=function V(C){return C[t]}(C);if(S&&"string"==typeof S.type){if(null==S.cancelFn||S.data&&S.data.aborted)return;S.zone.cancelTask(S)}else if(!0===c.current[F])return O.apply(C,b)})}(r);const t=j("xhrTask"),i=j("xhrSync"),u=j("xhrListener"),E=j("xhrScheduled"),T=j("xhrURL"),m=j("xhrErrorBeforeScheduled")}),e.__load_patch("geolocation",r=>{r.navigator&&r.navigator.geolocation&&function Et(e,r){const c=e.constructor.name;for(let t=0;t{const m=function(){return T.apply(this,je(arguments,c+"."+i))};return fe(m,T),m})(u)}}}(r.navigator.geolocation,["getCurrentPosition","watchPosition"])}),e.__load_patch("PromiseRejectionEvent",(r,c)=>{function t(i){return function(u){nt(r,i).forEach(T=>{const m=r.PromiseRejectionEvent;if(m){const D=new m(i,{promise:u.promise,reason:u.rejection});T.invoke(D)}})}}r.PromiseRejectionEvent&&(c[j("unhandledPromiseRejectionHandler")]=t("unhandledrejection"),c[j("rejectionHandledHandler")]=t("rejectionhandled"))}),e.__load_patch("queueMicrotask",(r,c,t)=>{!function bt(e,r){r.patchMethod(e,"queueMicrotask",c=>function(t,i){Zone.current.scheduleMicroTask("queueMicrotask",i[0])})}(r,t)})}(st)})();(()=>{var ze,oe,xx={136:oe=>{function ze(Ce){return Promise.resolve().then(()=>{var Ge=new Error("Cannot find module '"+Ce+"'");throw Ge.code="MODULE_NOT_FOUND",Ge})}ze.keys=()=>[],ze.resolve=ze,ze.id=136,oe.exports=ze},264:function(oe){var ze,Ce;Ce=()=>(()=>{"use strict";let ze,Ce;var Ge,_n,kt,We,sn,Ye,Ht,Ct,Qt,rt,ye,yi,el,fc,Eo,Do,Td,ss,v,zs={d:(v,s)=>{for(var l in s)zs.o(s,l)&&!zs.o(v,l)&&Object.defineProperty(v,l,{enumerable:!0,get:s[l]})},o:(v,s)=>Object.prototype.hasOwnProperty.call(v,s)},Id={};zs.d(Id,{default:()=>Mm}),(v=Ge||(Ge={})).SVG_NS="http://www.w3.org/2000/svg",v.product="Highcharts",v.version="12.4.0",v.win=typeof window<"u"?window:{},v.doc=v.win.document,v.svg=!!v.doc?.createElementNS?.(v.SVG_NS,"svg")?.createSVGRect,v.pageLang=v.doc?.documentElement?.closest("[lang]")?.lang,v.userAgent=v.win.navigator?.userAgent||"",v.isChrome=v.win.chrome,v.isFirefox=-1!==v.userAgent.indexOf("Firefox"),v.isMS=/(edge|msie|trident)/i.test(v.userAgent)&&!v.win.opera,v.isSafari=!v.isChrome&&-1!==v.userAgent.indexOf("Safari"),v.isTouchDevice=/(Mobile|Android|Windows Phone)/.test(v.userAgent),v.isWebKit=-1!==v.userAgent.indexOf("AppleWebKit"),v.deg2rad=2*Math.PI/360,v.marginNames=["plotTop","marginRight","marginBottom","plotLeft"],v.noop=function(){},v.supportsPassiveEvents=function(){let s=!1;if(!v.isMS){let l=Object.defineProperty({},"passive",{get:function(){s=!0}});v.win.addEventListener&&v.win.removeEventListener&&(v.win.addEventListener("testPassive",v.noop,l),v.win.removeEventListener("testPassive",v.noop,l))}return s}(),v.charts=[],v.composed=[],v.dateFormats={},v.seriesTypes={},v.symbolSizes={},v.chartCount=0;let Q=Ge,{charts:pc,doc:Hi,win:as}=Q;function Ui(v,s,l,c){let d=s?"Highcharts error":"Highcharts warning";32===v&&(v=`${d}: Deprecated member`);let h=nl(v),p=h?`${d} #${v}: www.highcharts.com/errors/${v}/`:v.toString();if(void 0!==c){let g="";h&&(p+="?"),To(c,function(m,b){g+=`\n - ${b}: ${m}`,h&&(p+=encodeURI(b)+"="+encodeURI(m))}),p+=g}jp(Q,"displayError",{chart:l,code:v,message:p,params:c},function(){if(s)throw Error(p);as.console&&-1===Ui.messages.indexOf(p)&&console.warn(p)}),Ui.messages.push(p)}function Bp(v,s){return parseInt(v,s||10)}function xo(v){return"string"==typeof v}function So(v){let s=Object.prototype.toString.call(v);return"[object Array]"===s||"[object Array Iterator]"===s}function ls(v,s){return!(!v||"object"!=typeof v||s&&So(v))}function tl(v){return ls(v)&&"number"==typeof v.nodeType}function cs(v){let s=v?.constructor;return!(!ls(v,!0)||tl(v)||!s?.name||"Object"===s.name)}function nl(v){return"number"==typeof v&&!isNaN(v)&&v<1/0&&v>-1/0}function rl(v){return null!=v}function Vp(v,s,l){let c,d=xo(s)&&!rl(l),h=(p,g)=>{rl(p)?v.setAttribute(g,p):d?(c=v.getAttribute(g))||"class"!==g||(c=v.getAttribute(g+"Name")):v.removeAttribute(g)};return xo(s)?h(l,s):To(s,h),c}function gc(v){return So(v)?v:[v]}function Ws(v,s){let l;for(l in v||(v={}),s)v[l]=s[l];return v}function Zr(){let v=arguments,s=v.length;for(let l=0;l1e14?v:parseFloat(v.toPrecision(s||14))}(Ui||(Ui={})).messages=[],Math.easeInOutSine=function(v){return-.5*(Math.cos(Math.PI*v)-1)};let S0=Array.prototype.find?function(v,s){return v.find(s)}:function(v,s){let l,c=v.length;for(l=0;lg.order-m.order),d.forEach(g=>{!1===g.fn.call(v,l)&&l.preventDefault()})}c&&!l.defaultPrevented&&c.call(v,l)}let mc=function(){let v=Math.random().toString(36).substring(2,9)+"-",s=0;return function(){return"highcharts-"+(ze?"":v)+s++}}();as.jQuery&&(as.jQuery.fn.highcharts=function(){let v=[].slice.call(arguments);if(this[0])return v[0]?(new(Q[xo(v[0])?v.shift():"Chart"])(this[0],v[0],v[1]),this):pc[Vp(this[0],"data-highcharts-chart")]});let we={addEvent:function(v,s,l,c={}){let d="function"==typeof v&&v.prototype||v;Object.hasOwnProperty.call(d,"hcEvents")||(d.hcEvents={});let h=d.hcEvents;Q.Point&&v instanceof Q.Point&&v.series&&v.series.chart&&(v.series.chart.runTrackerClick=!0);let p=v.addEventListener;return p&&p.call(v,s,l,!!Q.supportsPassiveEvents&&{passive:void 0===c.passive?-1!==s.indexOf("touch"):c.passive,capture:!1}),h[s]||(h[s]=[]),h[s].push({fn:l,order:"number"==typeof c.order?c.order:1/0}),h[s].sort((m,b)=>m.order-b.order),function(){Ad(v,s,l)}},arrayMax:function(v){let s=v.length,l=v[0];for(;s--;)v[s]>l&&(l=v[s]);return l},arrayMin:function(v){let s=v.length,l=v[0];for(;s--;)v[s]s?v-1&&g[E]){_=gc(_),m[E]=[];for(let x=0;x({center:.5,right:1,middle:.5,bottom:1}[v]||0),getClosestDistance:function(v,s){let l,c,d,h,p=!s;return v.forEach(g=>{if(g.length>1)for(h=c=g.length-1;h>0;h--)(d=g[h]-g[h-1])<0&&!p?(s?.(),s=void 0):d&&(void 0===l||d=p-1&&(p=Math.floor(g)),Math.max(0,p-(v(s,"padding-left",!0)||0)-(v(s,"padding-right",!0)||0))}if("height"===l)return Math.max(0,Math.min(s.offsetHeight,s.scrollHeight)-(v(s,"padding-top",!0)||0)-(v(s,"padding-bottom",!0)||0));let h=as.getComputedStyle(s,void 0);return h&&(d=h.getPropertyValue(l),Zr(c,"opacity"!==l)&&(d=Bp(d))),d},insertItem:function(v,s){let l,c=v.options.index,d=s.length;for(l=v.options.isInternal?d:0;l=v))&&(d||!(g<=(s[h]+(s[h+1]||s[h]))/2)));h++);return $p(p*l,-Math.round(Math.log(.001)/Math.LN10))},objectEach:To,offset:function(v){let s=Hi.documentElement,l=v.parentElement||v.parentNode?v.getBoundingClientRect():{top:0,left:0,width:0,height:0};return{top:l.top+(as.pageYOffset||s.scrollTop)-(s.clientTop||0),left:l.left+(as.pageXOffset||s.scrollLeft)-(s.clientLeft||0),width:l.width,height:l.height}},pad:function(v,s,l){return Array((s||2)+1-String(v).replace("-","").length).join(l||"0")+v},pick:Zr,pInt:Bp,pushUnique:function(v,s){return 0>v.indexOf(s)&&!!v.push(s)},relativeLength:function(v,s,l){return/%$/.test(v)?s*parseFloat(v)/100+(l||0):parseFloat(v)},removeEvent:Ad,replaceNested:function(v,...s){let l,c;do{for(c of(l=v,s))v=v.replace(c[0],c[1])}while(v!==l);return v},splat:gc,stableSort:function(v,s){let l,c,d=v.length;for(c=0;c0?setTimeout(v,s,l):(v.call(0,l),-1)},timeUnits:{millisecond:1,second:1e3,minute:6e4,hour:36e5,day:864e5,week:6048e5,month:24192e5,year:314496e5},ucfirst:function(v){return xo(v)?v.substring(0,1).toUpperCase()+v.substring(1):String(v)},uniqueKey:mc,useSerialIds:function(v){return ze=Zr(v,ze)},wrap:function(v,s,l){let c=v[s];v[s]=function(){let d=arguments,h=this;return l.apply(this,[function(){return c.apply(h,arguments.length?arguments:d)}].concat([].slice.call(arguments)))}}},{pageLang:Mx,win:yc}=Q,{defined:Gs,error:kd,extend:Hp,isNumber:vc,isObject:Od,isString:ft,merge:Rd,objectEach:Nd,pad:Kr,splat:Hn,timeUnits:bc,ucfirst:Up}=we,zp=Q.isSafari&&yc.Intl&&!yc.Intl.DateTimeFormat.prototype.formatRange,_c=class{constructor(v,s){this.options={timezone:"UTC"},this.variableTimezone=!1,this.Date=yc.Date,this.update(v),this.lang=s}update(v={}){this.dTLCache={},this.options=v=Rd(!0,this.options,v);let{timezoneOffset:s,useUTC:l,locale:c}=v;this.Date=v.Date||yc.Date||Date;let d=v.timezone;Gs(l)&&(d=l?"UTC":void 0),s&&s%60==0&&(d="Etc/GMT"+(s>0?"+":"")+s/60),this.variableTimezone="UTC"!==d&&0!==d?.indexOf("Etc/GMT"),this.timezone=d,this.lang&&c&&(this.lang.locale=c),["months","shortMonths","weekdays","shortWeekdays"].forEach(h=>{let p=/months/i.test(h),g=/short/.test(h),m={timeZone:"UTC"};m[p?"month":"weekday"]=g?"short":"long",this[h]=(p?[0,1,2,3,4,5,6,7,8,9,10,11]:[3,4,5,6,7,8,9]).map(b=>this.dateFormat(m,24*(p?31:1)*36e5*b))})}toParts(v){let[s,l,c,d,h,p,g]=this.dateTimeFormat({weekday:"narrow",day:"numeric",month:"numeric",year:"numeric",hour:"numeric",minute:"numeric",second:"numeric"},v,"es").split(/(?:, | |\/|:)/g);return[d,c-1,l,h,p,g,Math.floor(Number(v)||0)%1e3,"DLMXJVS".indexOf(s)].map(Number)}dateTimeFormat(v,s,l=this.options.locale||Mx){let c=JSON.stringify(v)+l;ft(v)&&(v=this.str2dtf(v));let d=this.dTLCache[c];if(!d){v.timeZone??(v.timeZone=this.timezone);try{d=new Intl.DateTimeFormat(l,v)}catch(h){/Invalid time zone/i.test(h.message)?(kd(34),v.timeZone="UTC",d=new Intl.DateTimeFormat(l,v)):kd(h.message,!1)}}return this.dTLCache[c]=d,d?.format(s)||""}str2dtf(v,s={}){let l={L:{fractionalSecondDigits:3},S:{second:"2-digit"},M:{minute:"numeric"},H:{hour:"2-digit"},k:{hour:"numeric"},E:{weekday:"narrow"},a:{weekday:"short"},A:{weekday:"long"},d:{day:"2-digit"},e:{day:"numeric"},b:{month:"short"},B:{month:"long"},m:{month:"2-digit"},o:{month:"numeric"},y:{year:"2-digit"},Y:{year:"numeric"}};return Object.keys(l).forEach(c=>{-1!==v.indexOf(c)&&Hp(s,l[c])}),s}makeTime(v,s,l=1,c=0,d,h,p){let g=this.Date.UTC(v,s,l,c,d||0,h||0,p||0);if("UTC"!==this.timezone){let m=this.getTimezoneOffset(g);if(g+=m,-1!==[2,3,8,9,10,11].indexOf(s)&&(c<5||c>20)){let b=this.getTimezoneOffset(g);m!==b?g+=b-m:m-36e5!==this.getTimezoneOffset(g-36e5)||zp||(g-=36e5)}}return g}parse(v){if(!ft(v))return v??void 0;let s=(v=v.replace(/\//g,"-").replace(/(GMT|UTC)/,"")).indexOf("Z")>-1||/([+-][0-9]{2}):?[0-9]{2}$/.test(v),l=/^[0-9]{4}-[0-9]{2}(-[0-9]{2}|)$/.test(v);s||l||(v+="Z");let c=Date.parse(v);return vc(c)?c+(!s||l?this.getTimezoneOffset(c):0):void 0}getTimezoneOffset(v){if("UTC"!==this.timezone){let[s,l,c,d,h=0]=this.dateTimeFormat({timeZoneName:"shortOffset"},v,"en").split(/(GMT|:)/).map(Number),p=-60*(c+h/60)*6e4;if(vc(p))return p}return 0}dateFormat(v,s,l){let c=this.lang;if(!Gs(s)||isNaN(s))return c?.invalidDate||"";if(ft(v=v??"%Y-%m-%d %H:%M:%S")){let d,h=/%\[([a-zA-Z]+)\]/g;for(;d=h.exec(v);)v=v.replace(d[0],this.dateTimeFormat(d[1],s,c?.locale))}if(ft(v)&&-1!==v.indexOf("%")){let d=this,[h,p,g,m,b,C,_,E]=this.toParts(s),x=c?.weekdays||this.weekdays,S=c?.shortWeekdays||this.shortWeekdays,M=c?.months||this.months,I=c?.shortMonths||this.shortMonths;Nd(Hp({a:S?S[E]:x[E].substr(0,3),A:x[E],d:Kr(g),e:Kr(g,2," "),w:E,v:c?.weekFrom??"",b:I[p],B:M[p],m:Kr(p+1),o:p+1,y:h.toString().substr(2,2),Y:h,H:Kr(m),k:m,I:Kr(m%12||12),l:m%12||12,M:Kr(b),p:m<12?"AM":"PM",P:m<12?"am":"pm",S:Kr(C),L:Kr(_,3)},Q.dateFormats),function(T,O){if(ft(v))for(;-1!==v.indexOf("%"+O);)v=v.replace("%"+O,"function"==typeof T?T.call(d,s):T)})}else if(Od(v)){let d=(this.getTimezoneOffset(s)||0)/36e5,h=this.timezone||"Etc/GMT"+(d>=0?"+":"")+d,{prefix:p="",suffix:g=""}=v;v=p+this.dateTimeFormat(Hp({timeZone:h},v),s)+g}return l?Up(v):v}resolveDTLFormat(v){return Od(v,!0)?Od(v,!0)&&(v=>void 0===v.main)(v)?{main:v}:v:{main:(v=Hn(v))[0],from:v[1],to:v[2]}}getDateFormat(v,s,l,c){let d=this.dateFormat("%m-%d %H:%M:%S.%L",s),h="01-01 00:00:00.000",p={millisecond:15,second:12,minute:9,hour:6,day:3},g="millisecond",m=g;for(g in bc){if(v&&v===bc.week&&+this.dateFormat("%w",s)===l&&d.substr(6)===h.substr(6)){g="week";break}if(v&&bc[g]>v){g=m;break}if(p[g]&&d.substr(p[g])!==h.substr(p[g]))break;"week"!==g&&(m=g)}return this.resolveDTLFormat(c[g]).main}},{defined:Pd,extend:Ld,timeUnits:Jt}=we,Fd=class extends _c{getTimeTicks(v,s,l,c){let I,d=this,h=[],p={},{count:g=1,unitRange:m}=v,[b,C,_,E,x,S]=d.toParts(s),M=(s||0)%1e3;if(c??(c=1),Pd(s)){if(M=m>=Jt.second?0:g*Math.floor(M/g),m>=Jt.second&&(S=m>=Jt.minute?0:g*Math.floor(S/g)),m>=Jt.minute&&(x=m>=Jt.hour?0:g*Math.floor(x/g)),m>=Jt.hour&&(E=m>=Jt.day?0:g*Math.floor(E/g)),m>=Jt.day&&(_=m>=Jt.month?1:Math.max(1,g*Math.floor(_/g))),m>=Jt.month&&(C=m>=Jt.year?0:g*Math.floor(C/g)),m>=Jt.year&&(b-=b%g),m===Jt.week){g&&(s=d.makeTime(b,C,_,E,x,S,M));let R=this.dateTimeFormat({timeZone:this.timezone,weekday:"narrow"},s,"es"),P="DLMXJVS".indexOf(R);_+=-P+c+(P4*Jt.month||d.getTimezoneOffset(s)!==d.getTimezoneOffset(l));let T=s,O=1;for(;T1?T=d.makeTime(b,C,_,E+O*g):T+=m*g:T=d.makeTime(b,C,_+O*g*(m===Jt.day?1:7)),O++;h.push(T),m<=Jt.hour&&h.length<1e4&&h.forEach(R=>{R%18e5==0&&"000000000"===d.dateFormat("%H%M%S%L",R)&&(p[R]="day")})}return h.info=Ld(v,{higherRanks:p,totalRange:m*g}),h}},{isTouchDevice:M0}=Q,{fireEvent:Bd,merge:ds}=we,Or={colors:["#2caffe","#544fc5","#00e272","#fe6a35","#6b8abc","#d568fb","#2ee0ca","#fa4b42","#feb56a","#91e8e1"],symbols:["circle","diamond","square","triangle","triangle-down"],lang:{weekFrom:"week from",chartTitle:"Chart title",locale:void 0,loading:"Loading...",months:void 0,seriesName:"Series {add index 1}",shortMonths:void 0,weekdays:void 0,numericSymbols:["k","M","G","T","P","E"],pieSliceName:"Slice",resetZoom:"Reset zoom",yAxisTitle:"Values",resetZoomTitle:"Reset zoom level 1:1"},global:{buttonTheme:{fill:"#f7f7f7",padding:8,r:2,stroke:"#cccccc","stroke-width":1,style:{color:"#333333",cursor:"pointer",fontSize:"0.8em",fontWeight:"normal"},states:{hover:{fill:"#e6e6e6"},select:{fill:"#e6e9ff",style:{color:"#000000",fontWeight:"bold"}},disabled:{style:{color:"#cccccc"}}}}},time:{Date:void 0,timezone:"UTC",timezoneOffset:0,useUTC:void 0},chart:{alignThresholds:!1,panning:{enabled:!1,type:"x"},styledMode:!1,borderRadius:0,colorCount:10,allowMutatingData:!0,ignoreHiddenSeries:!0,spacing:[10,10,15,10],resetZoomButton:{theme:{},position:{}},reflow:!0,type:"line",zooming:{singleTouch:!1,resetButton:{theme:{zIndex:6},position:{align:"right",x:-10,y:10}}},width:null,height:null,borderColor:"#334eff",backgroundColor:"#ffffff",plotBorderColor:"#cccccc"},title:{style:{color:"#333333",fontWeight:"bold"},text:"Chart title",margin:15,minScale:.67},subtitle:{style:{color:"#666666",fontSize:"0.8em"},text:""},caption:{margin:15,style:{color:"#666666",fontSize:"0.8em"},text:"",align:"left",verticalAlign:"bottom"},plotOptions:{},legend:{enabled:!0,align:"center",alignColumns:!0,className:"highcharts-no-tooltip",events:{},layout:"horizontal",itemMarginBottom:2,itemMarginTop:2,labelFormatter:function(){return this.name},borderColor:"#999999",borderRadius:0,navigation:{style:{fontSize:"0.8em"},activeColor:"#0022ff",inactiveColor:"#cccccc"},itemStyle:{color:"#333333",cursor:"pointer",fontSize:"0.8em",textDecoration:"none",textOverflow:"ellipsis"},itemHoverStyle:{color:"#000000"},itemHiddenStyle:{color:"#666666",textDecoration:"line-through"},shadow:!1,itemCheckboxStyle:{position:"absolute",width:"13px",height:"13px"},squareSymbol:!0,symbolPadding:5,verticalAlign:"bottom",x:0,y:0,title:{style:{color:"#333333",fontSize:"0.8em",fontWeight:"bold"}}},loading:{labelStyle:{fontWeight:"bold",position:"relative",top:"45%"},style:{position:"absolute",backgroundColor:"#ffffff",opacity:.5,textAlign:"center"}},tooltip:{enabled:!0,animation:{duration:300,easing:v=>Math.sqrt(1-Math.pow(v-1,2))},borderRadius:3,dateTimeLabelFormats:{millisecond:"%[AebHMSL]",second:"%[AebHMS]",minute:"%[AebHM]",hour:"%[AebHM]",day:"%[AebY]",week:"%v %[AebY]",month:"%[BY]",year:"%Y"},footerFormat:"",headerShape:"callout",hideDelay:500,padding:8,position:{x:0,y:3},shared:!1,snap:M0?25:10,headerFormat:'{ucfirst point.key}
',pointFormat:'\u25cf {series.name}: {point.y}
',backgroundColor:"#ffffff",borderWidth:void 0,stickOnContact:!1,style:{color:"#333333",cursor:"default",fontSize:"0.8em"},useHTML:!1},credits:{enabled:!0,href:"https://www.highcharts.com?credits",position:{align:"right",x:-10,verticalAlign:"bottom",y:-5},style:{cursor:"pointer",color:"#999999",fontSize:"0.6em"},text:"Highcharts.com"}},Vd=new Fd(Or.time,Or.lang),cr={defaultOptions:Or,defaultTime:Vd,getOptions:function(){return Or},setOptions:function(v){return Bd(Q,"setOptions",{options:v}),ds(!0,Or,v),v.time&&Vd.update(Or.time),v.lang&&"locale"in v.lang&&Vd.update({locale:v.lang.locale}),v.lang?.chartTitle&&(Or.title={...Or.title,text:v.lang.chartTitle}),Or}},{win:T0}=Q,{isNumber:hs,isString:I0,merge:$d,pInt:wn,defined:Wp}=we,wc=(v,s,l)=>`color-mix(in srgb,${v},${s} ${100*l}%)`,jd=v=>I0(v)&&!!v&&"none"!==v;class Ot{static parse(s){return s?new Ot(s):Ot.None}constructor(s){let l,c,d,h;this.rgba=[NaN,NaN,NaN,NaN],this.input=s;let p=Q.Color;if(p&&p!==Ot)return new p(s);if("object"==typeof s&&void 0!==s.stops)this.stops=s.stops.map(g=>new Ot(g[1]));else if("string"==typeof s)for(this.input=s=Ot.names[s.toLowerCase()]||s,d=Ot.parsers.length;d--&&!c;)(l=(h=Ot.parsers[d]).regex.exec(s))&&(c=h.parse(l));c&&(this.rgba=c)}get(s){let l=this.input,c=this.rgba;if(this.output)return this.output;if("object"==typeof l&&void 0!==this.stops){let d=$d(l);return d.stops=[].slice.call(d.stops),this.stops.forEach((h,p)=>{d.stops[p]=[d.stops[p][0],h.get(s)]}),d}return c&&hs(c[0])?"rgb"===s||!s&&1===c[3]?"rgb("+c[0]+","+c[1]+","+c[2]+")":"a"===s?`${c[3]}`:"rgba("+c.join(",")+")":l}brighten(s){let l=this.rgba;if(this.stops)this.stops.forEach(function(c){c.brighten(s)});else if(hs(s)&&0!==s)if(hs(l[0]))for(let c=0;c<3;c++)l[c]+=wn(255*s),l[c]<0&&(l[c]=0),l[c]>255&&(l[c]=255);else Ot.useColorMix&&jd(this.input)&&(this.output=wc(this.input,s>0?"white":"black",Math.abs(s)));return this}setOpacity(s){return this.rgba[3]=s,this}tweenTo(s,l){let c=this.rgba,d=s.rgba;if(!hs(c[0])||!hs(d[0]))return Ot.useColorMix&&jd(this.input)&&jd(s.input)&&l<.99?wc(this.input,s.input,l):s.input||"none";let h=1!==d[3]||1!==c[3],p=(m,b)=>m+(c[b]-m)*(1-l),g=d.slice(0,3).map(p).map(Math.round);return h&&g.push(p(d[3],3)),(h?"rgba(":"rgb(")+g.join(",")+")"}}Ot.names={white:"#ffffff",black:"#000000"},Ot.parsers=[{regex:/rgba\(\s*(\d{1,3})\s*,\s*(\d{1,3})\s*,\s*(\d{1,3})\s*,\s*(\d?(?:\.\d+)?)\s*\)/,parse:function(v){return[wn(v[1]),wn(v[2]),wn(v[3]),parseFloat(v[4],10)]}},{regex:/rgb\(\s*(\d{1,3})\s*,\s*(\d{1,3})\s*,\s*(\d{1,3})\s*\)/,parse:function(v){return[wn(v[1]),wn(v[2]),wn(v[3]),1]}},{regex:/^#([a-f0-9])([a-f0-9])([a-f0-9])([a-f0-9])?$/i,parse:function(v){return[wn(v[1]+v[1],16),wn(v[2]+v[2],16),wn(v[3]+v[3],16),Wp(v[4])?wn(v[4]+v[4],16)/255:1]}},{regex:/^#([a-f0-9]{2})([a-f0-9]{2})([a-f0-9]{2})([a-f0-9]{2})?$/i,parse:function(v){return[wn(v[1],16),wn(v[2],16),wn(v[3],16),Wp(v[4])?wn(v[4],16)/255:1]}}],Ot.useColorMix=T0.CSS?.supports("color","color-mix(in srgb,red,blue 9%)"),Ot.None=new Ot("");let{parse:Io}=Ot,{win:A0}=Q,{isNumber:Cc,objectEach:Ut}=we,Ao=(()=>{class v{constructor(l,c,d){this.pos=NaN,this.options=c,this.elem=l,this.prop=d}dSetter(){let l=this.paths,c=l?.[0],d=l?.[1],h=this.now||0,p=[];if(1!==h&&c&&d)if(c.length===d.length&&h<1)for(let g=0;g=b+this.startTime?(this.now=this.end,this.pos=1,this.update(),C[this.prop]=!0,d=!0,Ut(C,function(_){!0!==_&&(d=!1)}),d&&m&&m.call(g),c=!1):(this.pos=p.easing((h-this.startTime)/b),this.now=this.start+(this.end-this.start)*this.pos,this.update(),c=!0),c}initPath(l,c,d){let _,E,x,S,h=l.startX,p=l.endX,g=d.slice(),m=l.isArea,b=m?2:1,C=c&&d.length>c.length&&d.hasStackedCliffs,M=c?.slice();if(!M||C)return[g,g];function I(O,R){for(;O.length{let m=qp(g.options.animation);h=Ec(s)&&k0(s.defer)?c.defer:Math.max(h,m.duration+m.defer),p=Math.min(c.duration,m.duration)}),v.renderer.forExport&&(h=0),{defer:Math.max(0,h-p),duration:Math.min(h,p)}},setAnimation:function(v,s){s.renderer.globalAnimation=Gp(v,s.options.chart.animation,!0)},stop:Hd},{SVG_NS:Yp,win:ur}=Q,{attr:Xt,createElement:Tx,css:Ix,error:st,isFunction:N0,isString:Qn,objectEach:Dc,splat:Ax}=we,{trustedTypes:Ud}=ur,pt=Ud&&N0(Ud.createPolicy)&&Ud.createPolicy("highcharts",{createHTML:v=>v}),je=pt?pt.createHTML(""):"";class bt{static filterUserAttributes(s){return Dc(s,(l,c)=>{let d=!0;-1===bt.allowedAttributes.indexOf(c)&&(d=!1),-1!==["background","dynsrc","href","lowsrc","src"].indexOf(c)&&(d=Qn(l)&&bt.allowedReferences.some(h=>0===l.indexOf(h))),d||(st(33,!1,void 0,{"Invalid attribute in config":`${c}`}),delete s[c]),Qn(l)&&s[c]&&(s[c]=l.replace(/{let d=c.split(":").map(p=>p.trim()),h=d.shift();return h&&d.length&&(l[h.replace(/-([a-z])/g,p=>p[1].toUpperCase())]=d.join(":")),l},{})}static setElementHTML(s,l){s.innerHTML=bt.emptyHTML,l&&new bt(l).addToDOM(s)}constructor(s){this.nodes="string"==typeof s?this.parseMarkup(s):s}addToDOM(s){return function l(c,d){let h;return Ax(c).forEach(function(p){let g,m=p.tagName,b=p.textContent?Q.doc.createTextNode(p.textContent):void 0,C=bt.bypassHTMLFiltering;if(m)if("#text"===m)g=b;else if(-1!==bt.allowedTags.indexOf(m)||C){let E=Q.doc.createElementNS("svg"===m?Yp:d.namespaceURI||Yp,m),x=p.attributes||{};Dc(p,function(S,M){"tagName"!==M&&"attributes"!==M&&"children"!==M&&"style"!==M&&"textContent"!==M&&(x[M]=S)}),Xt(E,C?x:bt.filterUserAttributes(x)),p.style&&Ix(E,p.style),b&&E.appendChild(b),l(p.children||[],E),g=E}else st(33,!1,void 0,{"Invalid tagName in config":m});g&&d.appendChild(g),h=g}),h}(this.nodes,s)}parseMarkup(s){let l,c=[];s=s.trim().replace(/ style=(["'])/g," data-style=$1");try{l=(new DOMParser).parseFromString(pt?pt.createHTML(s):s,"text/html")}catch{}if(!l){let h=Tx("div");h.innerHTML=s,l={body:h}}let d=(h,p)=>{let g=h.nodeName.toLowerCase(),m={tagName:g};"#text"===g&&(m.textContent=h.textContent||"");let b=h.attributes;if(b){let C={};[].forEach.call(b,_=>{"data-style"===_.name?m.style=bt.parseStyle(_.value):C[_.name]=_.value}),m.attributes=C}if(h.childNodes.length){let C=[];[].forEach.call(h.childNodes,_=>{d(_,C)}),C.length&&(m.children=C)}p.push(m)};return[].forEach.call(l.body.childNodes,h=>d(h,c)),c}}bt.allowedAttributes=["alt","aria-controls","aria-describedby","aria-expanded","aria-haspopup","aria-hidden","aria-label","aria-labelledby","aria-live","aria-pressed","aria-readonly","aria-roledescription","aria-selected","class","clip-path","color","colspan","cx","cy","d","disabled","dx","dy","fill","filterUnits","flood-color","flood-opacity","height","href","id","in","in2","markerHeight","markerWidth","offset","opacity","operator","orient","padding","paddingLeft","paddingRight","patternUnits","r","radius","refX","refY","result","role","rowspan","scope","slope","src","startOffset","stdDeviation","stroke-linecap","stroke-width","stroke","style","summary","tabindex","tableValues","target","text-align","text-anchor","textAnchor","textLength","title","type","valign","width","x","x1","x2","xlink:href","y","y1","y2","zIndex"],bt.allowedReferences=["https://","http://","mailto:","/","../","./","#"],bt.allowedTags=["#text","a","abbr","b","br","button","caption","circle","clipPath","code","dd","defs","div","dl","dt","em","feComponentTransfer","feComposite","feDropShadow","feFlood","feFuncA","feFuncB","feFuncG","feFuncR","feGaussianBlur","feMerge","feMergeNode","feMorphology","feOffset","filter","h1","h2","h3","h4","h5","h6","hr","i","img","li","linearGradient","marker","ol","p","path","pattern","pre","rect","small","span","stop","strong","style","sub","sup","svg","table","tbody","td","text","textPath","th","thead","title","tr","tspan","u","ul"],bt.emptyHTML=je,bt.bypassHTMLFiltering=!1;let{defaultOptions:dr,defaultTime:P0}=cr,{pageLang:L0}=Q,{extend:kx,getNestedProperty:ko,isArray:Ox,isNumber:F0,isObject:Rx,isString:B0,pick:V0,ucfirst:Nx}=we,xc={add:(v,s)=>v+s,divide:(v,s)=>0!==s?v/s:"",eq:(v,s)=>v==s,each:function(v){let s=arguments[arguments.length-1];return!!Ox(v)&&v.map((l,c)=>_e(s.body,kx(Rx(l)?l:{"@this":l},{"@index":c,"@first":0===c,"@last":c===v.length-1}))).join("")},ge:(v,s)=>v>=s,gt:(v,s)=>v>s,if:v=>!!v,le:(v,s)=>v<=s,lt:(v,s)=>vv*s,ne:(v,s)=>v!=s,subtract:(v,s)=>v-s,ucfirst:Nx,unless:v=>!v},$0={},Nn=v=>/^["'].+["']$/.test(v);function _e(v="",s,l){let E,x,M,c=/\{([a-zA-Z\u00C0-\u017F\d:\.,;\-\/<>\[\]%_@+"'\u2019= #\(\)]+)\}/g,d=/\(([a-zA-Z\u00C0-\u017F\d:\.,;\-\/<>\[\]%_@+"'= ]+)\)/g,h=[],p=/f$/,g=/\.(\d)/,m=l?.options?.lang||dr.lang,b=l?.time||P0,C=l?.numberFormatter||j0.bind(l),_=(I="")=>{let T;return"true"===I||"false"!==I&&((T=Number(I)).toString()===I?T:Nn(I)?I.slice(1,-1):ko(I,s))},S=0;for(;null!==(E=c.exec(v));){let I=E,T=d.exec(E[1]);T&&(E=T,M=!0),x?.isBlock||(x={ctx:s,expression:E[1],find:E[0],isBlock:"#"===E[1].charAt(0),start:E.index,startInner:E.index+E[0].length,length:E[0].length});let O=(x.isBlock?I:E)[1].split(" ")[0].replace("#","");xc[O]&&(x.isBlock&&O===x.fn&&S++,x.fn||(x.fn=O));let R="else"===E[1];if(x.isBlock&&x.fn&&(E[1]===`/${x.fn}`||R))if(S)!R&&S--;else{let P=x.startInner,U=v.substr(P,E.index-P);void 0===x.body?(x.body=U,x.startInner=E.index+E[0].length):x.elseBody=U,x.find+=U+E[0],R||(h.push(x),x=void 0)}else x.isBlock||h.push(x);if(T&&!x?.isBlock)break}return h.forEach(I=>{let T,O,{body:R,elseBody:P,expression:U,fn:$}=I;if($){let X,F=[I],j=[],q=U.length,Z=0;for(O=0;O<=q;O++){let ue=U.charAt(O);X||'"'!==ue&&"'"!==ue?X===ue&&(X=""):X=ue,X||" "!==ue&&O!==q||(j.push(U.substr(Z,O-Z)),Z=O+1)}for(O=xc[$].length;O--;)F.unshift(_(j[O+1]));T=xc[$].apply(s,F),I.isBlock&&"boolean"==typeof T&&(T=_e(T?R:P,s,l))}else{let F=Nn(U)?[U]:U.split(":"),j=(T=_(F.shift()||""))%1!=0;if("number"==typeof T&&(F.length||j)){let q=F.join(":");if(p.test(q)||j){let Z=parseInt((q.match(g)||["","-1"])[1],10);null!==T&&(T=C(T,Z,m.decimalPoint,q.indexOf(",")>-1?m.thousandsSep:""))}else T=b.dateFormat(q,T)}d.lastIndex=0,d.test(I.find)&&B0(T)&&(T=`"${T}"`)}v=v.replace(I.find,V0(T,""))}),M?_e(v,s,l):v}function j0(v,s,l,c){s*=1;let d,h,[p,g]=(v=+v||0).toString().split("e").map(Number),m=this?.options?.lang||dr.lang,b=(v.toString().split(".")[1]||"").split("e")[0].length,C=s,_={};l??(l=m.decimalPoint),c??(c=m.thousandsSep),-1===s?s=Math.min(b,20):F0(s)?s&&g<0&&((h=s+g)>=0?(p=+p.toExponential(h).split("e")[0],s=h):(p=Math.floor(p),v=s<20?+(p*Math.pow(10,g)).toFixed(s):0,g=0)):s=2,g&&(s??(s=2),v=p),F0(s)&&s>=0&&(_.minimumFractionDigits=s,_.maximumFractionDigits=s),""===c&&(_.useGrouping=!1);let E=c||l,x=E?"en":this?.locale||m.locale||L0,S=JSON.stringify(_)+x;return d=($0[S]??($0[S]=new Intl.NumberFormat(x,_))).format(v),E&&(d=d.replace(/([,\.])/g,"_$1").replace(/_\,/g,c??",").replace("_.",l??".")),(s||0!=+d)&&(!(g<0)||C)||(d="0"),g&&0!=+d&&(d+="e"+(g<0?"":"+")+g),d}let Qr={dateFormat:function(v,s,l){return P0.dateFormat(v,s,l)},format:_e,helpers:xc,numberFormat:j0};!function(v){let s;v.rendererTypes={},v.getRendererType=function(l=s){return v.rendererTypes[l]||v.rendererTypes[s]},v.registerRendererType=function(l,c,d){v.rendererTypes[l]=c,(!s||d)&&(s=l,Q.Renderer=c)}}(_n||(_n={}));let ol=_n,{clamp:Px,pick:Lx,pushUnique:pe,stableSort:Xp}=we;(kt||(kt={})).distribute=function v(s,l,c){let _,E,x,M,I,O,d=s,h=d.reducedLen||l,p=(R,P)=>R.target-P.target,g=[],m=s.length,b=[],C=g.push,S=!0,T=0;for(_=m;_--;)T+=s[_].size;if(T>h){for(Xp(s,(R,P)=>(P.rank||0)-(R.rank||0)),x=(O=s[0].rank===s[s.length-1].rank)?m/2:-1,E=O?x:m-1;x&&T>h;)M=s[_=Math.floor(E)],pe(b,_)&&(T-=M.size),E+=x,O&&E>=s.length&&(x/=2,E=x);b.sort((R,P)=>P-R).forEach(R=>C.apply(g,s.splice(R,1)))}for(Xp(s,p),s=s.map(R=>({size:R.size,targets:[R.target],align:Lx(R.align,.5)}));S;){for(_=s.length;_--;)M=s[_],I=(Math.min.apply(0,M.targets)+Math.max.apply(0,M.targets))/2,M.pos=Px(I-M.size*M.align,0,l-M.size);for(_=s.length,S=!1;_--;)_>0&&s[_-1].pos+s[_-1].size>s[_].pos&&(s[_-1].size+=s[_].size,s[_-1].targets=s[_-1].targets.concat(s[_].targets),s[_-1].align=.5,s[_-1].pos+s[_-1].size>l&&(s[_-1].pos=l-s[_-1].size),s.splice(_,1),S=!0)}return C.apply(d,g),_=0,s.some(R=>{let P=0;return(R.targets||[]).some(()=>(d[_].pos=R.pos+P,void 0!==c&&Math.abs(d[_].pos-d[_].target)>c?(d.slice(0,_+1).forEach(U=>delete U.pos),d.reducedLen=(d.reducedLen||l)-.1*l,d.reducedLen>.1*l&&v(d,l,c),!0):(P+=d[_].size,_++,!1)))}),Xp(d,p),d};let Wi=kt,{animate:Sc,animObject:H0,stop:Zp}=G,{deg2rad:Kp,doc:Gi,svg:Mc,SVG_NS:qs,win:ee,isFirefox:zd}=Q,{addEvent:sl,attr:Ys,createElement:fs,crisp:Tc,css:Qp,defined:vi,erase:al,extend:Ic,fireEvent:Jp,getAlignFactor:eg,isArray:U0,isFunction:tg,isNumber:Fx,isObject:Wd,isString:Ac,merge:Gd,objectEach:Xs,pick:bi,pInt:_i,pushUnique:ll,replaceNested:ng,syncTimeout:Le,uniqueKey:Ze}=we;class Cn{_defaultGetter(s){let l=bi(this[s+"Value"],this[s],this.element?this.element.getAttribute(s):null,0);return/^-?[\d\.]+$/.test(l)&&(l=parseFloat(l)),l}_defaultSetter(s,l,c){c.setAttribute(l,s)}add(s){let l,c=this.renderer,d=this.element;return s&&(this.parentGroup=s),void 0!==this.textStr&&"text"===this.element.nodeName&&c.buildText(this),this.added=!0,(!s||s.handleZ||this.zIndex)&&(l=this.zIndexSetter()),l||(s?s.element:c.box).appendChild(d),this.onAdd&&this.onAdd(),this}addClass(s,l){let c=l?"":this.attr("class")||"";return(s=(s||"").split(/ /g).reduce(function(d,h){return-1===c.indexOf(h)&&d.push(h),d},c?[c]:[]).join(" "))!==c&&this.attr("class",s),this}afterSetters(){this.doTransform&&(this.updateTransform(),this.doTransform=!1)}align(s,l,c,d=!0){let h=this.renderer,p=h.alignedObjects,g=!!s;s?(this.alignOptions=s,this.alignByTranslate=l,this.alignTo=c):(s=this.alignOptions||{},l=this.alignByTranslate,c=this.alignTo);let m=!c||Ac(c)?c||"renderer":void 0;m&&(g&&ll(p,this),c=void 0);let b=bi(c,h[m],h),C=(b.x||0)+(s.x||0)+((b.width||0)-(s.width||0))*eg(s.align),_=(b.y||0)+(s.y||0)+((b.height||0)-(s.height||0))*eg(s.verticalAlign),E={"text-align":s?.align};return E[l?"translateX":"x"]=Math.round(C),E[l?"translateY":"y"]=Math.round(_),d&&(this[this.placed?"animate":"attr"](E),this.placed=!0),this.alignAttr=E,this}alignSetter(s){let l={left:"start",center:"middle",right:"end"};l[s]&&(this.alignValue=s,this.element.setAttribute("text-anchor",l[s]))}animate(s,l,c){let d=H0(bi(l,this.renderer.globalAnimation,!0)),h=d.defer;return Gi.hidden&&(d.duration=0),0!==d.duration?(c&&(d.complete=c),Le(()=>{this.element&&Sc(this,s,d)},h)):(this.attr(s,void 0,c||d.complete),Xs(s,function(p,g){d.step&&d.step.call(this,p,{prop:g,pos:1,elem:this})},this)),this}applyTextOutline(s){let l=this.element;-1!==s.indexOf("contrast")&&(s=s.replace(/contrast/g,this.renderer.getContrast(l.style.fill)));let c=s.indexOf(" "),d=s.substring(c+1),h=s.substring(0,c);if(h&&"none"!==h&&Q.svg){this.fakeTS=!0,h=h.replace(/(^[\d\.]+)(.*?)$/g,function(C,_,E){return 2*Number(_)+E}),this.removeTextOutline();let p=Gi.createElementNS(qs,"tspan");Ys(p,{class:"highcharts-text-outline",fill:d,stroke:d,"stroke-width":h,"stroke-linejoin":"round"});let g=l.querySelector("textPath")||l;[].forEach.call(g.childNodes,C=>{let _=C.cloneNode(!0);_.removeAttribute&&["fill","stroke","stroke-width","stroke"].forEach(E=>_.removeAttribute(E)),p.appendChild(_)});let m=0;[].forEach.call(g.querySelectorAll("text tspan"),C=>{m+=Number(C.getAttribute("dy"))});let b=Gi.createElementNS(qs,"tspan");b.textContent="\u200b",Ys(b,{x:Number(l.getAttribute("x")),dy:-m}),p.appendChild(b),g.insertBefore(p,g.firstChild)}}attr(s,l,c,d){let g,m,C,{element:h}=this,p=Cn.symbolCustomAttribs,b=this;return"string"==typeof s&&void 0!==l&&(g=s,(s={})[g]=l),"string"==typeof s?b=(this[s+"Getter"]||this._defaultGetter).call(this,s,h):(Xs(s,function(_,E){C=!1,d||Zp(this,E),this.symbolName&&-1!==p.indexOf(E)&&(m||(this.symbolAttr(s),m=!0),C=!0),this.rotation&&("x"===E||"y"===E)&&(this.doTransform=!0),C||(this[E+"Setter"]||this._defaultSetter).call(this,_,E,h)},this),this.afterSetters()),c&&c.call(this),b}clip(s){if(s&&!s.clipPath){let l=Ze()+"-",c=this.renderer.createElement("clipPath").attr({id:l}).add(this.renderer.defs);Ic(s,{clipPath:c,id:l,count:0}),s.add(c)}return this.attr("clip-path",s?`url(${this.renderer.url}#${s.id})`:"none")}crisp(s,l){l=Math.round(l||s.strokeWidth||0);let c=s.x||this.x||0,d=s.y||this.y||0,h=(s.width||this.width||0)+c,p=(s.height||this.height||0)+d,g=Tc(c,l),m=Tc(d,l);return Ic(s,{x:g,y:m,width:Tc(h,l)-g,height:Tc(p,l)-m}),vi(s.strokeWidth)&&(s.strokeWidth=l),s}complexColor(s,l,c){let h,p,g,m,b,C,_,E,x,S,I,d=this.renderer,M=[];Jp(this.renderer,"complexColor",{args:arguments},function(){if(s.radialGradient?p="radialGradient":s.linearGradient&&(p="linearGradient"),p){if(g=s[p],b=d.gradients,C=s.stops,x=c.radialReference,U0(g)&&(s[p]=g={x1:g[0],y1:g[1],x2:g[2],y2:g[3],gradientUnits:"userSpaceOnUse"}),"radialGradient"===p&&x&&!vi(g.gradientUnits)&&(m=g,g=Gd(g,d.getRadialAttr(x,m),{gradientUnits:"userSpaceOnUse"})),Xs(g,function(T,O){"id"!==O&&M.push(O,T)}),Xs(C,function(T){M.push(T)}),b[M=M.join(",")])S=b[M].attr("id");else{g.id=S=Ze();let T=b[M]=d.createElement(p).attr(g).add(d.defs);T.radAttr=m,T.stops=[],C.forEach(function(O){0===O[1].indexOf("rgba")?(_=(h=Ot.parse(O[1])).get("rgb"),E=h.get("a")):(_=O[1],E=1);let R=d.createElement("stop").attr({offset:O[0],"stop-color":_,"stop-opacity":E}).add(T);T.stops.push(R)})}I="url("+d.url+"#"+S+")",c.setAttribute(l,I),c.gradient=M,s.toString=function(){return I}}})}css(s){let h,l=this.styles,c={},d=this.element,p=!l;if(l&&Xs(s,function(g,m){l&&l[m]!==g&&(c[m]=g,p=!0)}),p){l&&(s=Ic(l,c)),null===s.width||"auto"===s.width?delete this.textWidth:"text"===d.nodeName.toLowerCase()&&s.width&&(h=this.textWidth=_i(s.width)),Ic(this.styles,s),h&&!Mc&&this.renderer.forExport&&delete s.width;let g=zd&&s.fontSize||null;g&&(Fx(g)||/^\d+$/.test(g))&&(s.fontSize+="px");let m=Gd(s);d.namespaceURI===this.SVG_NS&&(["textOutline","textOverflow","whiteSpace","width"].forEach(b=>m&&delete m[b]),m.color&&(m.fill=m.color,delete m.color)),Qp(d,m)}return this.added&&("text"===this.element.nodeName&&this.renderer.buildText(this),s.textOutline&&this.applyTextOutline(s.textOutline)),this}dashstyleSetter(s){let l,c=this["stroke-width"];if("inherit"===c&&(c=1),s){let d=(s=s.toLowerCase()).replace("shortdashdotdot","3,1,1,1,1,1,").replace("shortdashdot","3,1,1,1").replace("shortdot","1,1,").replace("shortdash","3,1,").replace("longdash","8,3,").replace(/dot/g,"1,3,").replace("dash","4,3,").replace(/,$/,"").split(",");for(l=d.length;l--;)d[l]=""+_i(d[l])*bi(c,NaN);s=d.join(",").replace(/NaN/g,"none"),this.element.setAttribute("stroke-dasharray",s)}}destroy(){let p,g,s=this,l=s.element||{},c=s.renderer,d=l.ownerSVGElement,h="SPAN"===l.nodeName&&s.parentGroup||void 0;if(l.onclick=l.onmouseout=l.onmouseover=l.onmousemove=l.point=null,Zp(s),s.clipPath&&d){let m=s.clipPath;[].forEach.call(d.querySelectorAll("[clip-path],[CLIP-PATH]"),function(b){b.getAttribute("clip-path").indexOf(m.element.id)>-1&&b.removeAttribute("clip-path")}),s.clipPath=m.destroy()}if(s.stops){for(g=0;g{(s[b]?.parentGroup===s||-1!==["connector","foreignObject"].indexOf(b))&&s[b]?.destroy?.(),delete s[b]})}dSetter(s,l,c){U0(s)&&("string"==typeof s[0]&&(s=this.renderer.pathToSegments(s)),this.pathArray=s,s=s.reduce((d,h,p)=>h?.join?(p?d+" ":"")+h.join(" "):(h||"").toString(),"")),/(NaN| {2}|^$)/.test(s)&&(s="M 0 0"),this[l]!==s&&(c.setAttribute(l,s),this[l]=s)}fillSetter(s,l,c){"string"==typeof s?c.setAttribute(l,s):s&&this.complexColor(s,l,c)}hrefSetter(s,l,c){c.setAttributeNS("http://www.w3.org/1999/xlink",l,s)}getBBox(s,l){let c,d,h,p,{alignValue:g,element:m,renderer:b,styles:C,textStr:_}=this,{cache:E,cacheKeys:x}=b,S=m.namespaceURI===this.SVG_NS,M=bi(l,this.rotation,0),I=b.styledMode?m&&Cn.prototype.getStyle.call(m,"font-size"):C.fontSize;if(vi(_)&&(-1===(p=_.toString()).indexOf("<")&&(p=p.replace(/\d/g,"0")),p+=["",b.rootFontSize,I,M,this.textWidth,g,C.lineClamp,C.textOverflow,C.fontWeight].join(",")),p&&!s&&(c=E[p]),!c||c.polygon){if(S||b.forExport){try{h=this.fakeTS&&function(O){let R=m.querySelector(".highcharts-text-outline");R&&Qp(R,{display:O})},tg(h)&&h("none"),c=m.getBBox?Ic({},m.getBBox()):{width:m.offsetWidth,height:m.offsetHeight,x:0,y:0},tg(h)&&h("")}catch{}(!c||c.width<0)&&(c={x:0,y:0,width:0,height:0})}else c=this.htmlGetBBox();d=c.height,S&&(c.height=d={"11px,17":14,"13px,20":16}[`${I||""},${Math.round(d)}`]||d),M&&(c=this.getRotatedBox(c,M));let T={bBox:c};Jp(this,"afterGetBBox",T),c=T.bBox}if(p&&(""===_||c.height>0)){for(;x.length>250;)delete E[x.shift()];E[p]||x.push(p),E[p]=c}return c}getRotatedBox(s,l){let{x:c,y:d,width:h,height:p}=s,{alignValue:g,translateY:m,rotationOriginX:b=0,rotationOriginY:C=0}=this,_=eg(g),E=Number(this.element.getAttribute("y")||0)-(m?0:d),x=l*Kp,S=(l-90)*Kp,M=Math.cos(x),I=Math.sin(x),T=h*M,O=h*I,R=Math.cos(S),P=Math.sin(S),[[U,$],[F,j]]=[b,C].map(Ie=>[Ie-Ie*M,Ie*I]),q=c+_*(h-T)+U+j+E*R,Z=q+T,X=Z-p*R,ue=X-T,me=d+E-_*O-$+F+E*P,ge=me+O,le=ge-p*P,ne=le-O,fe=Math.min(q,Z,X,ue),mt=Math.min(me,ge,le,ne);return{x:fe,y:mt,width:Math.max(q,Z,X,ue)-fe,height:Math.max(me,ge,le,ne)-mt,polygon:[[q,me],[Z,ge],[X,le],[ue,ne]]}}getStyle(s){return ee.getComputedStyle(this.element||this,"").getPropertyValue(s)}hasClass(s){return-1!==(""+this.attr("class")).split(" ").indexOf(s)}hide(){return this.attr({visibility:"hidden"})}htmlGetBBox(){return{height:0,width:0,x:0,y:0}}constructor(s,l){this.onEvents={},this.opacity=1,this.SVG_NS=qs,this.element="span"===l||"body"===l?fs(l):Gi.createElementNS(this.SVG_NS,l),this.renderer=s,this.styles={},Jp(this,"afterInit")}on(s,l){let{onEvents:c}=this;return c[s]&&c[s](),c[s]=sl(this.element,s,l),this}opacitySetter(s,l,c){let d=Number(Number(s).toFixed(3));this.opacity=d,c.setAttribute(l,d)}reAlign(){this.alignOptions?.width&&"left"!==this.alignOptions.align&&(this.alignOptions.width=this.getBBox().width,this.placed=!1,this.align())}removeClass(s){return this.attr("class",(""+this.attr("class")).replace(Ac(s)?RegExp(`(^| )${s}( |$)`):s," ").replace(/ +/g," ").trim())}removeTextOutline(){let s=this.element.querySelector("tspan.highcharts-text-outline");s&&this.safeRemoveChild(s)}safeRemoveChild(s){let l=s.parentNode;l&&l.removeChild(s)}setRadialReference(s){let l=this.element.gradient&&this.renderer.gradients[this.element.gradient]||void 0;return this.element.radialReference=s,l?.radAttr&&l.animate(this.renderer.getRadialAttr(s,l.radAttr)),this}shadow(s){let{renderer:l}=this,c=Gd(90===this.parentGroup?.rotation?{offsetX:-1,offsetY:-1}:{},Wd(s)?s:{}),d=l.shadowDefinition(c);return this.attr({filter:s?`url(${l.url}#${d})`:"none"})}show(s=!0){return this.attr({visibility:s?"inherit":"visible"})}"stroke-widthSetter"(s,l,c){this[l]=s,c.setAttribute(l,s)}strokeWidth(){if(!this.renderer.styledMode)return this["stroke-width"]||0;let c,s=this.getStyle("stroke-width"),l=0;return/px$/.test(s)?l=_i(s):""!==s&&(Ys(c=Gi.createElementNS(qs,"rect"),{width:s,"stroke-width":0}),this.element.parentNode.appendChild(c),l=c.getBBox().width,c.parentNode.removeChild(c)),l}symbolAttr(s){let l=this;Cn.symbolCustomAttribs.forEach(function(c){l[c]=bi(s[c],l[c])}),l.attr({d:l.renderer.symbols[l.symbolName](l.x,l.y,l.width,l.height,l)})}textSetter(s){s!==this.textStr&&(delete this.textPxLength,this.textStr=s,this.added&&this.renderer.buildText(this),this.reAlign())}titleSetter(s){let l=this.element,c=l.getElementsByTagName("title")[0]||Gi.createElementNS(this.SVG_NS,"title");l.insertBefore?l.insertBefore(c,l.firstChild):l.appendChild(c),c.textContent=ng(bi(s,""),[/<[^>]*>/g,""]).replace(/</g,"<").replace(/>/g,">")}toFront(){let s=this.element;return s.parentNode.appendChild(s),this}translate(s,l){return this.attr({translateX:s,translateY:l})}updateTransform(s="transform"){let{element:l,foreignObject:c,matrix:d,padding:h,rotation:p=0,rotationOriginX:g,rotationOriginY:m,scaleX:b,scaleY:C,text:_,translateX:E=0,translateY:x=0}=this,S=["translate("+E+","+x+")"];vi(d)&&S.push("matrix("+d.join(",")+")"),p&&(S.push("rotate("+p+" "+(g??l.getAttribute("x")??this.x??0)+" "+(m??l.getAttribute("y")??this.y??0)+")"),"SPAN"!==_?.element.tagName||_?.foreignObject||_.attr({rotation:p,rotationOriginX:(g||0)-h,rotationOriginY:(m||0)-h})),(vi(b)||vi(C))&&S.push("scale("+bi(b,1)+" "+bi(C,1)+")"),S.length&&!(_||this).textPath&&(c?.element||l).setAttribute(s,S.join(" "))}visibilitySetter(s,l,c){"inherit"===s?c.removeAttribute(l):this[l]!==s&&c.setAttribute(l,s),this[l]=s}xGetter(s){return"circle"===this.element.nodeName&&("x"===s?s="cx":"y"===s&&(s="cy")),this._defaultGetter(s)}zIndexSetter(s,l){let m,b,C,E,S,c=this.renderer,d=this.parentGroup,h=(d||c).element||c.box,p=this.element,g=h===c.box,_=!1,x=this.added;if(vi(s)?(p.setAttribute("data-z-index",s),this[l]===(s*=1)&&(x=!1)):vi(this[l])&&p.removeAttribute("data-z-index"),this[l]=s,x){for((s=this.zIndex)&&d&&(d.handleZ=!0),S=(m=h.childNodes).length-1;S>=0&&!_;S--)E=!vi(C=(b=m[S]).getAttribute("data-z-index")),b!==p&&(s<0&&E&&!g&&!S?(h.insertBefore(p,m[S]),_=!0):(_i(C)<=s||E&&(!vi(s)||s>=0))&&(h.insertBefore(p,m[S+1]),_=!0));_||(h.insertBefore(p,m[3*!!g]),_=!0)}return _}}Cn.symbolCustomAttribs=["anchorX","anchorY","clockwise","end","height","innerR","r","start","width","x","y"],Cn.prototype.strokeSetter=Cn.prototype.fillSetter,Cn.prototype.yGetter=Cn.prototype.xGetter,Cn.prototype.matrixSetter=Cn.prototype.rotationOriginXSetter=Cn.prototype.rotationOriginYSetter=Cn.prototype.rotationSetter=Cn.prototype.scaleXSetter=Cn.prototype.scaleYSetter=Cn.prototype.translateXSetter=Cn.prototype.translateYSetter=Cn.prototype.verticalAlignSetter=function(v,s){this[s]=v,this.doTransform=!0};let wi=Cn,{defined:kc,extend:rg,getAlignFactor:Zs,isNumber:ps,merge:Bx,pick:qd,removeEvent:ig}=we;class qi extends wi{constructor(s,l,c,d,h,p,g,m,b,C){let _;super(s,"g"),this.paddingLeftSetter=this.paddingSetter,this.paddingRightSetter=this.paddingSetter,this.doUpdate=!1,this.textStr=l,this.x=c,this.y=d,this.anchorX=p,this.anchorY=g,this.baseline=b,this.className=C,this.addClass("button"===C?"highcharts-no-tooltip":"highcharts-label"),C&&this.addClass("highcharts-"+C),this.text=s.text(void 0,0,0,m).attr({zIndex:1}),"string"==typeof h&&((_=/^url\((.*?)\)$/.test(h))||this.renderer.symbols[h])&&(this.symbolKey=h),this.bBox=qi.emptyBBox,this.padding=3,this.baselineOffset=0,this.needsBox=s.styledMode||_,this.deferredAttr={},this.alignFactor=0}alignSetter(s){let l=Zs(s);this.textAlign=s,l!==this.alignFactor&&(this.alignFactor=l,this.bBox&&ps(this.xSetting)&&this.attr({x:this.xSetting}))}anchorXSetter(s,l){this.anchorX=s,this.boxAttr(l,Math.round(s)-this.getCrispAdjust()-this.xSetting)}anchorYSetter(s,l){this.anchorY=s,this.boxAttr(l,s-this.ySetting)}boxAttr(s,l){this.box?this.box.attr(s,l):this.deferredAttr[s]=l}css(s){if(s){let l={};s=Bx(s),qi.textProps.forEach(c=>{void 0!==s[c]&&(l[c]=s[c],delete s[c])}),this.text.css(l),"fontSize"in l||"fontWeight"in l?this.updateTextPadding():("width"in l||"textOverflow"in l)&&this.updateBoxSize()}return wi.prototype.css.call(this,s)}destroy(){ig(this.element,"mouseenter"),ig(this.element,"mouseleave"),this.text&&this.text.destroy(),this.box&&(this.box=this.box.destroy()),wi.prototype.destroy.call(this)}fillSetter(s,l){s&&(this.needsBox=!0),this.fill=s,this.boxAttr(l,s)}getBBox(s,l){this.textStr&&0===this.bBox.width&&0===this.bBox.height&&this.updateBoxSize();let{padding:c,height:d=0,translateX:h=0,translateY:p=0,width:g=0}=this,m=qd(this.paddingLeft,c),b=l??(this.rotation||0),C={width:g,height:d,x:h+this.bBox.x-m,y:p+this.bBox.y-c+this.baselineOffset};return b&&(C=this.getRotatedBox(C,b)),C}getCrispAdjust(){return(this.renderer.styledMode&&this.box?this.box.strokeWidth():this["stroke-width"]?parseInt(this["stroke-width"],10):0)%2/2}heightSetter(s){this.heightSetting=s,this.doUpdate=!0}afterSetters(){super.afterSetters(),this.doUpdate&&(this.updateBoxSize(),this.doUpdate=!1)}onAdd(){this.text.add(this),this.attr({text:qd(this.textStr,""),x:this.x||0,y:this.y||0}),this.box&&kc(this.anchorX)&&this.attr({anchorX:this.anchorX,anchorY:this.anchorY})}paddingSetter(s,l){ps(s)?s!==this[l]&&(this[l]=s,this.updateTextPadding()):this[l]=void 0}rSetter(s,l){this.boxAttr(l,s)}strokeSetter(s,l){this.stroke=s,this.boxAttr(l,s)}"stroke-widthSetter"(s,l){s&&(this.needsBox=!0),this["stroke-width"]=s,this.boxAttr(l,s)}"text-alignSetter"(s){this.textAlign=this["text-align"]=s,this.updateTextPadding()}textSetter(s){void 0!==s&&this.text.attr({text:s}),this.updateTextPadding(),this.reAlign()}updateBoxSize(){let s,l=this.text,c={},d=this.padding,h=this.bBox=ps(this.widthSetting)&&ps(this.heightSetting)&&!this.textAlign||!kc(l.textStr)?qi.emptyBBox:l.getBBox(void 0,0);this.width=this.getPaddedWidth(),this.height=(this.heightSetting||h.height||0)+2*d;let p=this.renderer.fontMetrics(l);if(this.baselineOffset=d+Math.min((this.text.firstLineMetrics||p).b,h.height||1/0),this.heightSetting&&(this.baselineOffset+=(this.heightSetting-p.h)/2),this.needsBox&&!l.textPath){if(!this.box){let g=this.box=this.symbolKey?this.renderer.symbol(this.symbolKey):this.renderer.rect();g.addClass(("button"===this.className?"":"highcharts-label-box")+(this.className?" highcharts-"+this.className+"-box":"")),g.add(this)}c.x=s=this.getCrispAdjust(),c.y=(this.baseline?-this.baselineOffset:0)+s,c.width=Math.round(this.width),c.height=Math.round(this.height),this.box.attr(rg(c,this.deferredAttr)),this.deferredAttr={}}}updateTextPadding(){let s=this.text,l=s.styles.textAlign||this.textAlign;if(!s.textPath){this.updateBoxSize();let c=this.baseline?0:this.baselineOffset,d=(this.paddingLeft??this.padding)+Zs(l)*(this.widthSetting??this.bBox.width);(d!==s.x||c!==s.y)&&(s.attr({align:l,x:d}),void 0!==c&&s.attr("y",c)),s.x=d,s.y=c}}widthSetter(s){this.widthSetting=ps(s)?s:void 0,this.doUpdate=!0}getPaddedWidth(){let s=this.padding,l=qd(this.paddingLeft,s),c=qd(this.paddingRight,s);return(this.widthSetting||this.bBox.width||0)+l+c}xSetter(s){this.x=s,this.alignFactor&&(s-=this.alignFactor*this.getPaddedWidth(),this["forceAnimate:x"]=!0),this.anchorX&&(this["forceAnimate:anchorX"]=!0),this.xSetting=Math.round(s),this.attr("translateX",this.xSetting)}ySetter(s){this.anchorY&&(this["forceAnimate:anchorY"]=!0),this.ySetting=this.y=Math.round(s),this.attr("translateY",this.ySetting)}}qi.emptyBBox={width:0,height:0,x:0,y:0},qi.textProps=["color","direction","fontFamily","fontSize","fontStyle","fontWeight","lineClamp","lineHeight","textAlign","textDecoration","textOutline","textOverflow","whiteSpace","width"];let{defined:z0,isNumber:W0,pick:Ks}=we;function og(v,s,l,c,d){let h=[];if(d){let p=d.start||0,g=d.end||0,m=Ks(d.r,l),b=Ks(d.r,c||l),C=2e-4/(d.borderRadius?1:Math.max(m,1)),_=Math.abs(g-p-2*Math.PI)0&&m0)return b;if(v+g>l-p)if(m>s+p&&ms+p&&m0){let C=mc&&gp&&b.splice(1,1,["L",g-6,s],["L",g,s-6],["L",g+6,s],["L",l-h,s]);return b},circle:function(v,s,l,c){return og(v+l/2,s+c/2,l/2,c/2,{start:.5*Math.PI,end:2.5*Math.PI,open:!1})},diamond:function(v,s,l,c){return[["M",v+l/2,s],["L",v+l,s+c/2],["L",v+l/2,s+c],["L",v,s+c/2],["Z"]]},rect:sg,roundedRect:cl,square:sg,triangle:function(v,s,l,c){return[["M",v+l/2,s],["L",v+l,s+c],["L",v,s+c],["Z"]]},"triangle-down":function(v,s,l,c){return[["M",v,s],["L",v+l,s],["L",v+l/2,s+c],["Z"]]}},{doc:Un,SVG_NS:ag,win:G0}=Q,{attr:lg,extend:gs,fireEvent:Xd,isString:q0,objectEach:Y0,pick:ke}=we,Oc=(v,s)=>v.substring(0,s)+"\u2026",Y=class{constructor(v){let s=v.styles;this.renderer=v.renderer,this.svgElement=v,this.width=v.textWidth,this.textLineHeight=s?.lineHeight,this.textOutline=s?.textOutline,this.ellipsis="ellipsis"===s?.textOverflow,this.lineClamp=s?.lineClamp,this.noWrap="nowrap"===s?.whiteSpace}buildSVG(){let v=this.svgElement,s=v.element,l=v.renderer,c=ke(v.textStr,"").toString(),d=-1!==c.indexOf("<"),h=s.childNodes,p=!v.added&&l.box,g=[c,this.ellipsis,this.noWrap,this.textLineHeight,this.textOutline,v.getStyle("font-size"),v.styles.lineClamp,this.width].join(",");if(g!==v.textCache){v.textCache=g,delete v.actualWidth;for(let m=h.length;m--;)s.removeChild(h[m]);if(d||this.ellipsis||this.width||v.textPath||-1!==c.indexOf(" ")&&(!this.noWrap||//g.test(c))){if(""!==c){p&&p.appendChild(s);let m=new bt(c);this.modifyTree(m.nodes),m.addToDOM(s),this.modifyDOM(),this.ellipsis&&-1!==(s.textContent||"").indexOf("\u2026")&&v.attr("title",this.unescapeEntities(v.textStr||"",["<",">"])),p&&p.removeChild(s)}}else s.appendChild(Un.createTextNode(this.unescapeEntities(c)));q0(this.textOutline)&&v.applyTextOutline&&v.applyTextOutline(this.textOutline)}}modifyDOM(){let v,s=this.svgElement,l=lg(s.element,"x");for(s.firstLineMetrics=void 0;(v=s.element.firstChild)&&/^[\s\u200B]*$/.test(v.textContent||" ");)s.element.removeChild(v);[].forEach.call(s.element.querySelectorAll("tspan.highcharts-br"),(p,g)=>{p.nextSibling&&p.previousSibling&&(0===g&&1===p.previousSibling.nodeType&&(s.firstLineMetrics=s.renderer.fontMetrics(p.previousSibling)),lg(p,{dy:this.getLineHeight(p.nextSibling),x:l}))});let c=this.width||0;if(!c)return;let d=(p,g)=>{let m=p.textContent||"",b=m.replace(/([^\^])-/g,"$1- ").split(" "),C=!this.noWrap&&(b.length>1||s.element.childNodes.length>1),_=this.getLineHeight(g),E=Math.max(0,c-.8*_),x=0,S=s.actualWidth;if(C){let M=[],I=[];for(;g.firstChild&&g.firstChild!==p;)I.push(g.firstChild),g.removeChild(g.firstChild);for(;b.length;)if(b.length&&!this.noWrap&&x>0&&(M.push(p.textContent||""),p.textContent=b.join(" ").replace(/- /g,"-")),this.truncate(p,void 0,b,0===x&&S||0,c,E,(T,O)=>b.slice(0,O).join(" ").replace(/- /g,"-")),S=s.actualWidth,x++,this.lineClamp&&x>=this.lineClamp){b.length&&(this.truncate(p,p.textContent||"",void 0,0,c,E,Oc),p.textContent=p.textContent?.replace("\u2026","")+"\u2026");break}I.forEach(T=>{g.insertBefore(T,p)}),M.forEach(T=>{g.insertBefore(Un.createTextNode(T),p);let O=Un.createElementNS(ag,"tspan");O.textContent="\u200b",lg(O,{dy:_,x:l}),g.insertBefore(O,p)})}else this.ellipsis&&m&&this.truncate(p,m,void 0,0,c,E,Oc)},h=p=>{[].slice.call(p.childNodes).forEach(g=>{g.nodeType===G0.Node.TEXT_NODE?d(g,p):(-1!==g.className.baseVal.indexOf("highcharts-br")&&(s.actualWidth=0),h(g))})};h(s.element)}getLineHeight(v){let s=v.nodeType===G0.Node.TEXT_NODE?v.parentElement:v;return this.textLineHeight?parseInt(this.textLineHeight.toString(),10):this.renderer.fontMetrics(s||this.svgElement.element).h}modifyTree(v){let s=(l,c)=>{let{attributes:d={},children:h,style:p={},tagName:g}=l,m=this.renderer.styledMode;if("b"===g||"strong"===g?m?d.class="highcharts-strong":p.fontWeight="bold":("i"===g||"em"===g)&&(m?d.class="highcharts-emphasized":p.fontStyle="italic"),p?.color&&(p.fill=p.color),"br"===g){d.class="highcharts-br",l.textContent="\u200b";let b=v[c+1];b?.textContent&&(b.textContent=b.textContent.replace(/^ +/gm,""))}else"a"===g&&h&&h.some(b=>"#text"===b.tagName)&&(l.children=[{children:h,tagName:"tspan"}]);"#text"!==g&&"a"!==g&&(l.tagName="tspan"),gs(l,{attributes:d,style:p}),h&&h.filter(b=>"#text"!==b.tagName).forEach(s)};v.forEach(s),Xd(this.svgElement,"afterModifyTree",{nodes:v})}truncate(v,s,l,c,d,h,p){let g,m,b=this.svgElement,{rotation:C}=b,_=[],E=l&&!c?1:0,x=(s||l||"").length,S=x;l||(d=h);let M=function(I,T){let O=T||I,R=v.parentNode;if(R&&void 0===_[O]&&R.getSubStringLength)try{_[O]=c+R.getSubStringLength(0,l?O+1:O)}catch{}return _[O]};if(b.rotation=0,c+(m=M(v.textContent.length))>d){for(;E<=x;)S=Math.ceil((E+x)/2),l&&(g=p(l,S)),m=M(S,g&&g.length-1),E===x?E=x+1:m>d?x=S-1:E=S;0===x?v.textContent="":s&&x===s.length-1||(v.textContent=g||p(s||l,S)),this.ellipsis&&m>d&&this.truncate(v,v.textContent||"",void 0,0,d,h,Oc)}l&&l.splice(0,S),b.actualWidth=m,b.rotation=C}unescapeEntities(v,s){return Y0(this.renderer.escapes,function(l,c){s&&-1!==s.indexOf(l)||(v=v.toString().replace(RegExp(l,"g"),c))}),v}},{defaultOptions:ul}=cr,{charts:X0,deg2rad:Rc,doc:Ci,isFirefox:cg,isMS:Oo,isWebKit:Z0,noop:Rr,SVG_NS:Qs,symbolSizes:Nc,win:Js}=Q,{addEvent:ms,attr:dl,createElement:ug,crisp:hl,css:ea,defined:Jr,destroyObjectProperties:K0,extend:zn,isArray:Rt,isNumber:hr,isObject:ta,isString:Zd,merge:na,pick:Nr,pInt:Kd,replaceNested:ct,uniqueKey:Vx}=we;class En{constructor(s,l,c,d,h,p,g){let m,b;this.x=0,this.y=0;let C=this.createElement("svg").attr({version:"1.1",class:"highcharts-root"}),_=C.element;g||C.css(this.getStyle(d||{})),s.appendChild(_),dl(s,"dir","ltr"),-1===s.innerHTML.indexOf("xmlns")&&dl(_,"xmlns",this.SVG_NS),this.box=_,this.boxWrapper=C,this.alignedObjects=[],this.url=this.getReferenceURL(),this.createElement("desc").add().element.appendChild(Ci.createTextNode("Created with Highcharts 12.4.0")),this.defs=this.createElement("defs").add(),this.allowHTML=p,this.forExport=h,this.styledMode=g,this.gradients={},this.cache={},this.cacheKeys=[],this.imgCount=0,this.rootFontSize=C.getStyle("font-size"),this.setSize(l,c,!1),cg&&s.getBoundingClientRect&&((m=function(){ea(s,{left:0,top:0}),b=s.getBoundingClientRect(),ea(s,{left:Math.ceil(b.left)-b.left+"px",top:Math.ceil(b.top)-b.top+"px"})})(),this.unSubPixelFix=ms(Js,"resize",m))}definition(s){return new bt([s]).addToDOM(this.defs.element)}getReferenceURL(){if((cg||Z0)&&Ci.getElementsByTagName("base").length){if(!Jr(Ce)){let s=Vx(),l=new bt([{tagName:"svg",attributes:{width:8,height:8},children:[{tagName:"defs",children:[{tagName:"clipPath",attributes:{id:s},children:[{tagName:"rect",attributes:{width:4,height:4}}]}]},{tagName:"rect",attributes:{id:"hitme",width:8,height:8,"clip-path":`url(#${s})`,fill:"rgba(0,0,0,0.001)"}}]}]).addToDOM(Ci.body);ea(l,{position:"fixed",top:0,left:0,zIndex:9e5}),Ce="hitme"===Ci.elementFromPoint(6,6)?.id,Ci.body.removeChild(l)}if(Ce)return ct(Js.location.href.split("#")[0],[/<[^>]*>/g,""],[/([\('\)])/g,"\\$1"],[/ /g,"%20"])}return""}getStyle(s){return this.style=zn({fontFamily:'-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", sans-serif',fontSize:"1rem"},s),this.style}setStyle(s){this.boxWrapper.css(this.getStyle(s))}isHidden(){return!this.boxWrapper.getBBox().width}destroy(){let s=this.defs;return this.box=null,this.boxWrapper=this.boxWrapper.destroy(),K0(this.gradients||{}),this.gradients=null,this.defs=s.destroy(),this.unSubPixelFix&&this.unSubPixelFix(),this.alignedObjects=null,null}createElement(s){return new this.Element(this,s)}getRadialAttr(s,l){return{cx:s[0]-s[2]/2+(l.cx||0)*s[2],cy:s[1]-s[2]/2+(l.cy||0)*s[2],r:(l.r||0)*s[2]}}shadowDefinition(s){let l=[`highcharts-drop-shadow-${this.chartIndex}`,...Object.keys(s).map(d=>`${d}-${s[d]}`)].join("-").toLowerCase().replace(/[^a-z\d\-]/g,""),c=na({color:"#000000",offsetX:1,offsetY:1,opacity:.15,width:5},s);return this.defs.element.querySelector(`#${l}`)||this.definition({tagName:"filter",attributes:{id:l,filterUnits:c.filterUnits},children:this.getShadowFilterContent(c)}),l}getShadowFilterContent(s){return[{tagName:"feDropShadow",attributes:{dx:s.offsetX,dy:s.offsetY,"flood-color":s.color,"flood-opacity":Math.min(5*s.opacity,1),stdDeviation:s.width/2}}]}buildText(s){new Y(s).buildSVG()}getContrast(s){if("transparent"===s)return"#000000";let l=Ot.parse(s).rgba,c=" clamp(0,calc(9e9*(0.5 - (0.2126*r + 0.7152*g + 0.0722*b))),1)";if(hr(l[0])||!Ot.useColorMix){let d=l.map(p=>{let g=p/255;return g<=.04?g/12.92:Math.pow((g+.055)/1.055,2.4)}),h=.2126*d[0]+.7152*d[1]+.0722*d[2];return 1.05/(h+.05)>(h+.05)/.05?"#FFFFFF":"#000000"}return"color(from "+s+" srgb"+c+c+c+")"}button(s,l,c,d,h={},p,g,m,b,C){let _=this.label(s,l,c,b,void 0,void 0,C,void 0,"button"),E=this.styledMode,x=arguments,S=0;h=na(ul.global.buttonTheme,h),E&&(delete h.fill,delete h.stroke,delete h["stroke-width"]);let M=h.states||{},I=h.style||{};delete h.states,delete h.style;let T=[bt.filterUserAttributes(h)],O=[I];return E||["hover","select","disabled"].forEach((R,P)=>{T.push(na(T[0],bt.filterUserAttributes(x[P+5]||M[R]||{}))),O.push(T[P+1].style),delete T[P+1].style}),ms(_.element,Oo?"mouseover":"mouseenter",function(){3!==S&&_.setState(1)}),ms(_.element,Oo?"mouseout":"mouseleave",function(){3!==S&&_.setState(S)}),_.setState=(R=0)=>{if(1!==R&&(_.state=S=R),_.removeClass(/highcharts-button-(normal|hover|pressed|disabled)/).addClass("highcharts-button-"+["normal","hover","pressed","disabled"][R]),!E){_.attr(T[R]);let P=O[R];ta(P)&&_.css(P)}},_.attr(T[0]),!E&&(_.css(zn({cursor:"default"},I)),C&&_.text.css({pointerEvents:"none"})),_.on("touchstart",R=>R.stopPropagation()).on("click",function(R){3!==S&&d?.call(_,R)})}crispLine(s,l){let[c,d]=s;return Jr(c[1])&&c[1]===d[1]&&(c[1]=d[1]=hl(c[1],l)),Jr(c[2])&&c[2]===d[2]&&(c[2]=d[2]=hl(c[2],l)),s}path(s){let l=this.styledMode?{}:{fill:"none"};return Rt(s)?l.d=s:ta(s)&&zn(l,s),this.createElement("path").attr(l)}circle(s,l,c){let d=ta(s)?s:void 0===s?{}:{x:s,y:l,r:c},h=this.createElement("circle");return h.xSetter=h.ySetter=function(p,g,m){m.setAttribute("c"+g,p)},h.attr(d)}arc(s,l,c,d,h,p){let g;ta(s)?(l=(g=s).y,c=g.r,d=g.innerR,h=g.start,p=g.end,s=g.x):g={innerR:d,start:h,end:p};let m=this.symbol("arc",s,l,c,c,g);return m.r=c,m}rect(s,l,c,d,h,p){let g=ta(s)?s:void 0===s?{}:{x:s,y:l,r:h,width:Math.max(c||0,0),height:Math.max(d||0,0)},m=this.createElement("rect");return this.styledMode||(void 0!==p&&(g["stroke-width"]=p,zn(g,m.crisp(g))),g.fill="none"),m.rSetter=function(b,C,_){m.r=b,dl(_,{rx:b,ry:b})},m.rGetter=function(){return m.r||0},m.attr(g)}roundedRect(s){return this.symbol("roundedRect").attr(s)}setSize(s,l,c){this.width=s,this.height=l,this.boxWrapper.animate({width:s,height:l},{step:function(){this.attr({viewBox:"0 0 "+this.attr("width")+" "+this.attr("height")})},duration:Nr(c,!0)?void 0:0}),this.alignElements()}g(s){let l=this.createElement("g");return s?l.attr({class:"highcharts-"+s}):l}image(s,l,c,d,h,p){let g={preserveAspectRatio:"none"};hr(l)&&(g.x=l),hr(c)&&(g.y=c),hr(d)&&(g.width=d),hr(h)&&(g.height=h);let m=this.createElement("image").attr(g),b=function(C){m.attr({href:s}),p.call(m,C)};if(p){m.attr({href:"data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw=="});let C=new Js.Image;ms(C,"load",b),C.src=s,C.complete&&b({})}else m.attr({href:s});return m}symbol(s,l,c,d,h,p){let g,m,b,C,_=this,E=/^url\((.*?)\)$/,x=E.test(s),S=!x&&(this.symbols[s]?s:"circle"),M=S&&this.symbols[S];if(M)"number"==typeof l&&(m=M.call(this.symbols,l||0,c||0,d||0,h||0,p)),g=this.path(m),_.styledMode||g.attr("fill","none"),zn(g,{symbolName:S||void 0,x:l,y:c,width:d,height:h}),p&&zn(g,p);else if(x){b=s.match(E)[1];let I=g=this.image(b);I.imgwidth=Nr(p?.width,Nc[b]?.width),I.imgheight=Nr(p?.height,Nc[b]?.height),C=T=>T.attr({width:T.width,height:T.height}),["width","height"].forEach(T=>{I[`${T}Setter`]=function(O,R){this[R]=O;let{alignByTranslate:P,element:U,width:$,height:F,imgwidth:j,imgheight:q}=this,Z="width"===R?j:q,X=1;p&&"within"===p.backgroundSize&&$&&F&&j&&q?(X=Math.min($/j,F/q),dl(U,{width:Math.round(j*X),height:Math.round(q*X)})):U&&Z&&U.setAttribute(R,Z),!P&&j&&q&&this.translate((($||0)-j*X)/2,((F||0)-q*X)/2)}}),Jr(l)&&I.attr({x:l,y:c}),I.isImg=!0,I.symbolUrl=s,Jr(I.imgwidth)&&Jr(I.imgheight)?C(I):(I.attr({width:0,height:0}),ug("img",{onload:function(){let T=X0[_.chartIndex];0===this.width&&(ea(this,{position:"absolute",top:"-999em"}),Ci.body.appendChild(this)),Nc[b]={width:this.width,height:this.height},I.imgwidth=this.width,I.imgheight=this.height,I.element&&C(I),this.parentNode&&this.parentNode.removeChild(this),_.imgCount--,_.imgCount||!T||T.hasLoaded||T.onload()},src:b}),this.imgCount++)}return g}clipRect(s,l,c,d){return this.rect(s,l,c,d,0)}text(s,l,c,d){let h={};if(d&&(this.allowHTML||!this.forExport))return this.html(s,l,c);h.x=Math.round(l||0),c&&(h.y=Math.round(c)),Jr(s)&&(h.text=s);let p=this.createElement("text").attr(h);return d&&(!this.forExport||this.allowHTML)||(p.xSetter=function(g,m,b){let C=b.getElementsByTagName("tspan"),_=b.getAttribute(m);for(let x,E=0;Es.align())}}zn(En.prototype,{Element:wi,SVG_NS:Qs,escapes:{"&":"&","<":"<",">":">","'":"'",'"':"""},symbols:Yd,draw:Rr}),ol.registerRendererType("svg",En,!0);let{composed:ei,isFirefox:ra}=Q,{attr:Yi,css:Ro,createElement:Q0,defined:Pc,extend:dg,getAlignFactor:Lc,isNumber:Qd,pInt:Jd,pushUnique:J0}=we;function Fc(v,s,l){let c=this.div?.style;wi.prototype[`${s}Setter`].call(this,v,s,l),c&&(l.style[s]=c[s]=v)}let hg=(v,s)=>{if(!v.div){let l=Yi(v.element,"class"),c=v.css,d=Q0("div",l?{className:l}:void 0,{position:"absolute",left:`${v.translateX||0}px`,top:`${v.translateY||0}px`,...v.styles,display:v.display,opacity:v.opacity,visibility:v.visibility},v.parentGroup?.div||s);v.classSetter=(h,p,g)=>{g.setAttribute("class",h),d.className=h},v.translateXSetter=v.translateYSetter=(h,p)=>{v[p]=h,d.style["translateX"===p?"left":"top"]=`${h}px`,v.doTransform=!0},v.scaleXSetter=v.scaleYSetter=(h,p)=>{v[p]=h,v.doTransform=!0},v.opacitySetter=v.visibilitySetter=Fc,v.css=h=>(c.call(v,h),h.cursor&&(d.style.cursor=h.cursor),h.pointerEvents&&(d.style.pointerEvents=h.pointerEvents),v),v.on=function(){return wi.prototype.on.apply({element:d,onEvents:v.onEvents},arguments),v},v.div=d}return v.div};class ys extends wi{static compose(s){J0(ei,this.compose)&&(s.prototype.html=function(l,c,d){return new ys(this,"span").attr({text:l,x:Math.round(c),y:Math.round(d)})})}constructor(s,l){super(s,l),ys.useForeignObject?this.foreignObject=s.createElement("foreignObject").attr({zIndex:2}):this.css({position:"absolute",...s.styledMode?{}:{fontFamily:s.style.fontFamily,fontSize:s.style.fontSize}}),this.element.style.whiteSpace="nowrap"}getSpanCorrection(s,l,c){this.xCorr=-s*c,this.yCorr=-l}css(s){let l,{element:c}=this,d="SPAN"===c.tagName&&s&&"width"in s,h=d&&s.width;return d&&(delete s.width,this.textWidth=Jd(h)||void 0,l=!0),"ellipsis"===s?.textOverflow&&(s.overflow="hidden",s.whiteSpace="nowrap"),s?.lineClamp&&(s.display="-webkit-box",s.WebkitLineClamp=s.lineClamp,s.WebkitBoxOrient="vertical",s.overflow="hidden"),Qd(Number(s?.fontSize))&&(s.fontSize+="px"),dg(this.styles,s),Ro(c,s),l&&this.updateTransform(),this}htmlGetBBox(){let{element:s}=this;return{x:s.offsetLeft,y:s.offsetTop,width:s.offsetWidth,height:s.offsetHeight}}updateTransform(){if(!this.added)return void(this.alignOnAdd=!0);let{element:s,foreignObject:l,oldTextWidth:c,renderer:d,rotation:h,rotationOriginX:p,rotationOriginY:g,scaleX:m,scaleY:b,styles:{display:C="inline-block",whiteSpace:_},textAlign:E="left",textWidth:x,translateX:S=0,translateY:M=0,x:I=0,y:T=0}=this;if(l||Ro(s,{marginLeft:`${S}px`,marginTop:`${M}px`}),"SPAN"===s.tagName){let O,R=[h,E,s.innerHTML,x,this.textAlign].join(","),P=-1*this.parentGroup?.padding||0;if(x!==c){let j=this.textPxLength?this.textPxLength:(Ro(s,{width:"",whiteSpace:_||"nowrap"}),s.offsetWidth),q=x||0,Z=!d.styledMode&&""===s.style.textOverflow&&s.style.webkitLineClamp;(q>c||j>q||Z)&&(/[\-\s\u00AD]/.test(s.textContent||s.innerText)||"ellipsis"===s.style.textOverflow)&&(Ro(s,{width:(h||m||j>q||Z)&&Qd(x)?x+"px":"auto",display:C,whiteSpace:_||"normal"}),this.oldTextWidth=x)}l&&(Ro(s,{display:"inline-block",verticalAlign:"top"}),l.attr({width:d.width,height:d.height})),R!==this.cTT&&(O=d.fontMetrics(s).b,Pc(h)&&!l&&(h!==(this.oldRotation||0)||E!==this.oldAlign)&&Ro(s,{transform:`rotate(${h}deg)`,transformOrigin:`${P}% ${P}px`}),this.getSpanCorrection(!Pc(h)&&!this.textWidth&&this.textPxLength||s.offsetWidth,O,Lc(E)));let{xCorr:U=0,yCorr:$=0}=this,F={left:`${I+U}px`,top:`${T+$}px`,textAlign:E,transformOrigin:`${(p??I)-U-I-P}px ${(g??T)-$-T-P}px`};(m||b)&&(F.transform=`scale(${m??1},${b??1})`),l?(super.updateTransform(),Qd(I)&&Qd(T)?(l.attr({x:I+U,y:T+$,width:s.offsetWidth+3,height:s.offsetHeight,"transform-origin":s.getAttribute("transform-origin")||"0 0"}),Ro(s,{display:C,textAlign:E})):ra&&l.attr({width:0,height:0})):Ro(s,F),this.cTT=R,this.oldRotation=h,this.oldAlign=E}}add(s){let{foreignObject:l,renderer:c}=this,d=c.box.parentNode,h=[];if(l)l.add(s),super.add(c.createElement("body").attr({xmlns:"http://www.w3.org/1999/xhtml"}).css({background:"transparent",margin:"0 3px 0 0"}).add(l));else{let p;if(this.parentGroup=s,s&&!(p=s.div)){let g=s;for(;g;)h.push(g),g=g.parentGroup;for(let m of h.reverse())p=hg(m,d)}(p||d).appendChild(this.element)}return this.added=!0,this.alignOnAdd&&this.updateTransform(),this}textSetter(s){s!==this.textStr&&(delete this.bBox,delete this.oldTextWidth,bt.setElementHTML(this.element,s??""),this.textStr=s,this.doTransform=!0)}alignSetter(s){this.alignValue=this.textAlign=s,this.doTransform=!0}xSetter(s,l){this[l]=s,this.doTransform=!0}}let Jn=ys.prototype;Jn.visibilitySetter=Jn.opacitySetter=Fc,Jn.ySetter=Jn.rotationSetter=Jn.rotationOriginXSetter=Jn.rotationOriginYSetter=Jn.xSetter,function(v){v.xAxis={alignTicks:!0,allowDecimals:void 0,panningEnabled:!0,zIndex:2,zoomEnabled:!0,dateTimeLabelFormats:{millisecond:{main:"%[HMSL]",range:!1},second:{main:"%[HMS]",range:!1},minute:{main:"%[HM]",range:!1},hour:{main:"%[HM]",range:!1},day:{main:"%[eb]"},week:{main:"%[eb]"},month:{main:"%[bY]"},year:{main:"%Y"}},endOnTick:!1,gridLineDashStyle:"Solid",gridZIndex:1,labels:{autoRotationLimit:80,distance:15,enabled:!0,indentation:10,overflow:"justify",reserveSpace:void 0,rotation:void 0,staggerLines:0,step:0,useHTML:!1,zIndex:7,style:{color:"#333333",cursor:"default",fontSize:"0.8em",textOverflow:"ellipsis"}},maxPadding:.01,minorGridLineDashStyle:"Solid",minorTickLength:2,minorTickPosition:"outside",minorTicksPerMajor:5,minPadding:.01,offset:void 0,reversed:void 0,reversedStacks:!1,showEmpty:!0,showFirstLabel:!0,showLastLabel:!0,startOfWeek:1,startOnTick:!1,tickLength:10,tickPixelInterval:100,tickmarkPlacement:"between",tickPosition:"outside",title:{align:"middle",useHTML:!1,x:0,y:0,style:{color:"#666666",fontSize:"0.8em"}},visible:!0,minorGridLineColor:"#f2f2f2",minorGridLineWidth:1,minorTickColor:"#999999",lineColor:"#333333",lineWidth:1,gridLineColor:"#e6e6e6",gridLineWidth:void 0,tickColor:"#333333"},v.yAxis={reversedStacks:!0,endOnTick:!0,maxPadding:.05,minPadding:.05,tickPixelInterval:72,showLastLabel:!0,labels:{x:void 0},startOnTick:!0,title:{},stackLabels:{animation:{},allowOverlap:!1,enabled:!1,crop:!0,overflow:"justify",formatter:function(){let{numberFormatter:s}=this.axis.chart;return s(this.total||0,-1)},style:{color:"#000000",fontSize:"0.7em",fontWeight:"bold",textOutline:"1px contrast"}},gridLineWidth:1,lineWidth:0}}(We||(We={}));let fg=We,{addEvent:eh,isFunction:Bc,objectEach:pg,removeEvent:th}=we;(sn||(sn={})).registerEventOptions=function(v,s){v.eventOptions=v.eventOptions||{},pg(s.events,function(l,c){v.eventOptions[c]!==l&&(v.eventOptions[c]&&(th(v,c,v.eventOptions[c]),delete v.eventOptions[c]),Bc(l)&&(v.eventOptions[c]=l,eh(v,c,l,{order:0})))})};let ia=sn,{deg2rad:Dn}=Q,{clamp:vs,correctFloat:oa,defined:nh,destroyObjectProperties:eb,extend:rh,fireEvent:Xi,getAlignFactor:sa,isNumber:Vc,merge:tb,objectEach:nb,pick:Pr}=we,xn=class{constructor(v,s,l,c,d){this.isNew=!0,this.isNewLabel=!0,this.axis=v,this.pos=s,this.type=l||"",this.parameters=d||{},this.tickmarkOffset=this.parameters.tickmarkOffset,this.options=this.parameters.options,Xi(this,"init"),l||c||this.addLabel()}addLabel(){let M,I,T,v=this,s=v.axis,l=s.options,c=s.chart,d=s.categories,h=s.logarithmic,p=s.names,g=v.pos,m=Pr(v.options?.labels,l.labels),b=s.tickPositions,C=g===b[0],_=g===b[b.length-1],E=(!m.step||1===m.step)&&1===s.tickInterval,x=b.info,S=v.label,O=this.parameters.category||(d?Pr(d[g],p[g],g):g);h&&Vc(O)&&(O=oa(h.lin2log(O))),s.dateTime&&(x?M=(I=c.time.resolveDTLFormat(l.dateTimeLabelFormats[!l.grid?.enabled&&x.higherRanks[g]||x.unitName])).main:Vc(O)&&(M=s.dateTime.getXDateFormat(O,l.dateTimeLabelFormats||{}))),v.isFirst=C,v.isLast=_;let R={axis:s,chart:c,dateTimeLabelFormat:M,isFirst:C,isLast:_,pos:g,tick:v,tickPositionInfo:x,value:O};Xi(this,"labelFormat",R);let P=F=>m.formatter?m.formatter.call(F,F):m.format?(F.text=s.defaultLabelFormatter.call(F),Qr.format(m.format,F,c)):s.defaultLabelFormatter.call(F),U=P.call(R,R),$=I?.list;v.shortenLabel=$?function(){for(T=0;T<$.length;T++)if(rh(R,{dateTimeLabelFormat:$[T]}),S.attr({text:P.call(R,R)}),S.getBBox().width0&&c+C*_>g&&(M=Math.round((d-c)/Math.cos(b*Dn))):(c-C*_g&&(x=g-v.x+x*C,S=-1),(x=Math.min(E,x))x||s.autoRotation&&m?.styles?.width)&&(M=x)),M&&m&&(this.shortenLabel?this.shortenLabel():m.css(rh({},{width:Math.floor(M)+"px",lineClamp:+!s.isRadial})))}moveLabel(v,s){let p,l=this,c=l.label,d=l.axis,h=!1;c&&c.textStr===v?(l.movedLabel=c,h=!0,delete l.label):nb(d.ticks,function(g){h||g.isNew||g===l||!g.label||g.label.textStr!==v||(l.movedLabel=g.label,h=!0,g.labelPos=l.movedLabel.xy,delete g.label)}),!h&&(l.labelPos||c)&&(p=l.labelPos||c.xy,l.movedLabel=l.createLabel(v,s,p),l.movedLabel&&l.movedLabel.attr({opacity:0}))}render(v,s,l){let c=this.axis,d=c.horiz,h=this.pos,p=Pr(this.tickmarkOffset,c.tickmarkOffset),g=this.getPosition(d,h,p,s),C=c.pos,_=C+c.len,E=d?g.x:g.y,x=Pr(l,this.label?.newOpacity,1);!c.chart.polar&&(oa(E)_)&&(l=0),l??(l=1),this.isActive=!0,this.renderGridLine(s,l),this.renderMark(g,l),this.renderLabel(g,s,x,v),this.isNew=!1,Xi(this,"afterRender")}renderGridLine(v,s){let C,l=this.axis,c=l.options,d={},h=this.pos,p=this.type,g=Pr(this.tickmarkOffset,l.tickmarkOffset),m=l.chart.renderer,b=this.gridLine,_=c.gridLineWidth,E=c.gridLineColor,x=c.gridLineDashStyle;"minor"===this.type&&(_=c.minorGridLineWidth,E=c.minorGridLineColor,x=c.minorGridLineDashStyle),b||(l.chart.styledMode||(d.stroke=E,d["stroke-width"]=_||0,d.dashstyle=x),p||(d.zIndex=1),v&&(s=0),this.gridLine=b=m.path().attr(d).addClass("highcharts-"+(p?p+"-":"")+"grid-line").add(l.gridGroup)),b&&(C=l.getPlotLinePath({value:h+g,lineWidth:b.strokeWidth(),force:"pass",old:v,acrossPanes:!1}))&&b[v||this.isNew?"attr":"animate"]({d:C,opacity:s})}renderMark(v,s){let l=this.axis,c=l.options,d=l.chart.renderer,h=this.type,p=l.tickSize(h?h+"Tick":"tick"),g=v.x,m=v.y,b=Pr(c["minor"!==h?"tickWidth":"minorTickWidth"],!h&&l.isXAxis?1:0),C=c["minor"!==h?"tickColor":"minorTickColor"],_=this.mark,E=!_;p&&(l.opposite&&(p[0]=-p[0]),!_&&(this.mark=_=d.path().addClass("highcharts-"+(h?h+"-":"")+"tick").add(l.axisGroup),l.chart.styledMode||_.attr({stroke:C,"stroke-width":b})),_[E?"attr":"animate"]({d:this.getMarkPath(g,m,p[0],_.strokeWidth(),l.horiz,d),opacity:s}))}renderLabel(v,s,l,c){let d=this.axis,h=d.horiz,p=d.options,g=this.label,m=p.labels,b=m.step,C=Pr(this.tickmarkOffset,d.tickmarkOffset),_=v.x,E=v.y,x=!0;g&&Vc(_)&&(g.xy=v=this.getLabelPosition(_,E,g,h,m,C,c,b),this.isFirst&&!this.isLast&&!p.showFirstLabel||this.isLast&&!this.isFirst&&!p.showLastLabel?x=!1:!h||m.step||m.rotation||s||0===l||this.handleOverflow(v),b&&c%b&&(x=!1),x&&Vc(v.y)?(v.opacity=l,g[this.isNewLabel?"attr":"animate"](v).show(!0),this.isNewLabel=!1):(g.hide(),this.isNewLabel=!0))}replaceMovedLabel(){let v=this.label,s=this.axis;v&&!this.isNew&&(v.animate({opacity:0},void 0,v.destroy),delete this.label),s.isDirty=!0,this.label=this.movedLabel,delete this.movedLabel}},{animObject:ih}=G,{xAxis:rb,yAxis:an}=fg,{defaultOptions:J}=cr,{registerEventOptions:Te}=ia,{deg2rad:Zt}=Q,{arrayMax:Pn,arrayMin:Sn,clamp:Zi,correctFloat:Ln,defined:be,destroyObjectProperties:it,erase:vr,error:Re,extend:Ei,fireEvent:Et,getClosestDistance:bs,insertItem:en,isArray:Ki,isNumber:ve,isString:Di,merge:No,normalizeTickInterval:gg,objectEach:ti,pick:Be,relativeLength:xi,removeEvent:br,splat:aa,syncTimeout:Po}=we,Ee=(v,s)=>gg(s,void 0,void 0,Be(v.options.allowDecimals,s<.5||void 0!==v.tickAmount),!!v.tickAmount);Ei(J,{xAxis:rb,yAxis:No(rb,an)});let fl=(()=>{class v{constructor(l,c,d){this.init(l,c,d)}init(l,c,d=this.coll){let h="xAxis"===d,p=this.isZAxis||(l.inverted?!h:h);this.chart=l,this.horiz=p,this.isXAxis=h,this.coll=d,Et(this,"init",{userOptions:c}),this.opposite=Be(c.opposite,this.opposite),this.side=Be(c.side,this.side,p?2*!this.opposite:this.opposite?1:3),this.setOptions(c);let g=this.options,m=g.labels;this.type??(this.type=g.type||"linear"),this.uniqueNames??(this.uniqueNames=g.uniqueNames??!0),Et(this,"afterSetType"),this.userOptions=c,this.minPixelPadding=0,this.reversed=Be(g.reversed,this.reversed),this.visible=g.visible,this.zoomEnabled=g.zoomEnabled,this.hasNames="category"===this.type||!0===g.categories,this.categories=Ki(g.categories)&&g.categories||(this.hasNames?[]:void 0),this.names||(this.names=[],this.names.keys={}),this.plotLinesAndBandsGroups={},this.positiveValuesOnly=!!this.logarithmic,this.isLinked=be(g.linkedTo),this.ticks={},this.labelEdge=[],this.minorTicks={},this.plotLinesAndBands=[],this.alternateBands={},this.len??(this.len=0),this.minRange=this.userMinRange=g.minRange||g.maxZoom,this.range=g.range,this.offset=g.offset||0,this.max=void 0,this.min=void 0;let b=Be(g.crosshair,aa(l.options.tooltip.crosshairs)[+!h]);this.crosshair=!0===b?{}:b,-1===l.axes.indexOf(this)&&(h?l.axes.splice(l.xAxis.length,0,this):l.axes.push(this),en(this,l[this.coll])),l.orderItems(this.coll),this.series=this.series||[],l.inverted&&!this.isZAxis&&h&&!be(this.reversed)&&(this.reversed=!0),this.labelRotation=ve(m.rotation)?m.rotation:void 0,Te(this,g),Et(this,"afterInit")}setOptions(l){this.options=No(this.horiz?{labels:{autoRotation:[-45],padding:3},margin:15}:{labels:{padding:1},title:{rotation:90*this.side}},"yAxis"===this.coll?{title:{text:this.chart.options.lang.yAxisTitle}}:{},J[this.coll],l),Et(this,"afterSetOptions",{userOptions:l})}defaultLabelFormatter(){let x,S,l=this.axis,{numberFormatter:c}=this.chart,d=ve(this.value)?this.value:NaN,h=l.chart.time,p=l.categories,g=this.dateTimeLabelFormat,m=J.lang,b=m.numericSymbols,C=m.numericSymbolMagnitude||1e3,_=l.logarithmic?Math.abs(d):l.tickInterval,E=b?.length;if(p)S=`${this.value}`;else if(g)S=h.dateFormat(g,d,!0);else if(E&&b&&_>=1e3)for(;E--&&void 0===S;)_>=(x=Math.pow(C,E+1))&&10*d%x==0&&null!==b[E]&&0!==d&&(S=c(d/x,-1)+b[E]);return void 0===S&&(S=Math.abs(d)>=1e4?c(d,-1):c(d,-1,void 0,"")),S}getSeriesExtremes(){let l,c=this;Et(this,"getSeriesExtremes",null,function(){c.hasVisibleSeries=!1,c.dataMin=c.dataMax=c.threshold=void 0,c.softThreshold=!c.isXAxis,c.series.forEach(d=>{if(d.reserveSpace()){let p,m,b,h=d.options,g=h.threshold;if(c.hasVisibleSeries=!0,c.positiveValuesOnly&&0>=(g||0)&&(g=void 0),c.isXAxis)(p=d.getColumn("x")).length&&(p=c.logarithmic?p.filter(C=>C>0):p,m=(l=d.getXExtremes(p)).min,b=l.max,ve(m)||m instanceof Date||(p=p.filter(ve),m=(l=d.getXExtremes(p)).min,b=l.max),p.length&&(c.dataMin=Math.min(Be(c.dataMin,m),m),c.dataMax=Math.max(Be(c.dataMax,b),b)));else{let C=d.applyExtremes();ve(C.dataMin)&&(m=C.dataMin,c.dataMin=Math.min(Be(c.dataMin,m),m)),ve(C.dataMax)&&(b=C.dataMax,c.dataMax=Math.max(Be(c.dataMax,b),b)),be(g)&&(c.threshold=g),(!h.softThreshold||c.positiveValuesOnly)&&(c.softThreshold=!1)}}})}),Et(this,"afterGetSeriesExtremes")}translate(l,c,d,h,p,g){let m=this.linkedParent||this,b=h&&m.old?m.old.min:m.min;if(!ve(b))return NaN;let C=m.minPixelPadding,_=(m.isOrdinal||m.brokenAxis?.hasBreaks||m.logarithmic&&p)&&!!m.lin2val,E=1,x=0,S=h&&m.old?m.old.transA:m.transA,M=0;return S||(S=m.transA),d&&(E*=-1,x=m.len),m.reversed&&(E*=-1,x-=E*(m.sector||m.len)),c?(M=(l=l*E+x-C)/S+b,_&&(M=m.lin2val(M))):(_&&(l=m.val2lin(l)),M=E*(l-b)*S+x+E*C+(ve(g)?S*g:0),m.isRadial||(M=Ln(M))),M}toPixels(l,c){return this.translate(this.chart?.time.parse(l)??NaN,!1,!this.horiz,void 0,!0)+(c?0:this.pos)}toValue(l,c){return this.translate(l-(c?0:this.pos),!0,!this.horiz,void 0,!0)}getPlotLinePath(l){let M,I,T,O,R,c=this,d=c.chart,h=c.left,p=c.top,g=l.old,m=l.value,b=l.lineWidth,C=g&&d.oldChartHeight||d.chartHeight,_=g&&d.oldChartWidth||d.chartWidth,E=c.transB,x=l.translatedValue,S=l.force;function P($,F,j){return"pass"!==S&&($j)&&(S?$=Zi($,F,j):R=!0),$}let U={value:m,lineWidth:b,old:g,force:S,acrossPanes:l.acrossPanes,translatedValue:x};return Et(this,"getPlotLinePath",U,function($){M=T=(x=Zi(x=Be(x,c.translate(m,void 0,void 0,g)),-1e9,1e9))+E,I=O=C-x-E,ve(x)?c.horiz?(I=p,O=C-c.bottom+(c.options.isInternal?0:d.scrollablePixelsY||0),M=T=P(M,h,h+c.width)):(M=h,T=_-c.right+(d.scrollablePixelsX||0),I=O=P(I,p,p+c.height)):(R=!0,S=!1),$.path=R&&!S?void 0:d.renderer.crispLine([["M",M,I],["L",T,O]],b||1)}),U.path}getLinearTickPositions(l,c,d){let h,p,g,m=Ln(Math.floor(c/l)*l),b=Ln(Math.ceil(d/l)*l),C=[];if(Ln(m+l)===m&&(g=20),this.single)return[c];for(h=m;h<=b&&(C.push(h),(h=Ln(h+l,g))!==p);)p=h;return C}getMinorTickInterval(){let{minorTicks:l,minorTickInterval:c}=this.options;return!0===l?Be(c,"auto"):!1!==l?c:void 0}getMinorTickPositions(){let C,l=this.options,c=this.tickPositions,d=this.minorTickInterval,h=this.pointRangePadding||0,p=(this.min||0)-h,g=(this.max||0)+h,m=this.brokenAxis?.hasBreaks?this.brokenAxis.unitLength:g-p,b=[];if(m&&m/d{let x=E.getColumn("x");return E.xIncrement?x.slice(0,2):x}))||0),this.dataMax-this.dataMin)),ve(h)&&ve(p)&&ve(g)&&h-p=g,m=(g-h+p)/2,C=[p-m,d.parse(l.min)??p-m],b&&(C[2]=c?c.log2lin(this.dataMin):this.dataMin),_=[(p=Pn(C))+g,d.parse(l.max)??p+g],b&&(_[2]=c?c.log2lin(this.dataMax):this.dataMax),(h=Sn(_))-ph-p),l=bs([d]))}return l&&c?Math.min(l,c):l||c}nameToX(l){let p,c=Ki(this.options.categories),d=c?this.categories:this.names,h=l.options.x;return l.series.requireSorting=!1,be(h)||(h=this.uniqueNames&&d?c?d.indexOf(l.name):Be(d.keys[l.name],-1):l.series.autoIncrement()),-1===h?!c&&d&&(p=d.length):ve(h)&&(p=h),void 0!==p?(this.names[p]=l.name,this.names.keys[l.name]=p):l.x&&(p=l.x),p}updateNames(){let l=this,c=this.names;c.length>0&&(Object.keys(c.keys).forEach(function(d){delete c.keys[d]}),c.length=0,this.minRange=this.userMinRange,(this.series||[]).forEach(d=>{d.xIncrement=null,(!d.points||d.isDirtyData)&&(l.max=Math.max(l.max||0,d.dataTable.rowCount-1),d.processData(),d.generatePoints());let h=d.getColumn("x").slice();d.data.forEach((p,g)=>{let m=h[g];p?.options&&void 0!==p.name&&void 0!==(m=l.nameToX(p))&&m!==p.x&&(h[g]=p.x=m)}),d.dataTable.setColumn("x",h)}))}setAxisTranslation(){let m,_,l=this,c=l.max-l.min,d=l.linkedParent,h=!!l.categories,p=l.isXAxis,g=l.axisPointRange||0,b=0,C=0,E=l.transA;(p||h||g)&&(m=l.getClosest(),d?(b=d.minPointOffset,C=d.pointRangePadding):l.series.forEach(function(x){let S=h?1:p?Be(x.options.pointRange,m,0):l.axisPointRange||0,M=x.options.pointPlacement;if(g=Math.max(g,S),!l.single||h){let I=x.is("xrange")?!p:p;b=Math.max(b,I&&Di(M)?0:S/2),C=Math.max(C,I&&"on"===M?0:S)}}),_=l.ordinal?.slope&&m?l.ordinal.slope/m:1,l.minPointOffset=b*=_,l.pointRangePadding=C*=_,l.pointRange=Math.min(g,l.single&&h?1:c),p&&(l.closestPointRange=m)),l.translationSlope=l.transA=E=l.staticScale||l.len/(c+C||1),l.transB=l.horiz?l.left:l.bottom,l.minPixelPadding=E*b,Et(this,"afterSetAxisTranslation")}minFromRange(){let{max:l,min:c}=this;return ve(l)&&ve(c)&&l-c||void 0}setTickInterval(l){let q,X,ue,me,ge,{categories:c,chart:d,dataMax:h,dataMin:p,dateTime:g,isXAxis:m,logarithmic:b,options:C,softThreshold:_}=this,E=d.time,x=ve(this.threshold)?this.threshold:void 0,S=this.minRange||0,{ceiling:M,floor:I,linkedTo:T,softMax:O,softMin:R}=C,P=ve(T)&&d[this.coll]?.[T],U=C.tickPixelInterval,$=C.maxPadding,F=C.minPadding,j=0,Z=ve(C.tickInterval)&&C.tickInterval>=0?C.tickInterval:void 0;if(g||c||P||this.getTickAmount(),me=Be(this.userMin,E.parse(C.min)),ge=Be(this.userMax,E.parse(C.max)),P?(this.linkedParent=P,q=P.getExtremes(),this.min=Be(q.min,q.dataMin),this.max=Be(q.max,q.dataMax),this.type!==P.type&&Re(11,!0,d)):(_&&be(x)&&ve(h)&&ve(p)&&(p>=x?(X=x,F=0):h<=x&&(ue=x,$=0)),this.min=Be(me,X,p),this.max=Be(ge,ue,h)),ve(this.max)&&ve(this.min)&&(b&&(this.positiveValuesOnly&&!l&&0>=Math.min(this.min,Be(p,this.min))&&Re(10,!0,d),this.min=Ln(b.log2lin(this.min),16),this.max=Ln(b.log2lin(this.max),16)),this.range&&ve(p)&&(this.userMin=this.min=me=Math.max(p,this.minFromRange()||0),this.userMax=ge=this.max,this.range=void 0)),Et(this,"foundExtremes"),this.adjustForMinRange(),ve(this.min)&&ve(this.max)){if(!ve(this.userMin)&&ve(R)&&Rthis.max&&(this.max=ge=O),c||this.axisPointRange||this.stacking?.usePercentage||P||(j=this.max-this.min)&&(!be(me)&&F&&(this.min-=j*F),!be(ge)&&$&&(this.max+=j*$)),!ve(this.userMin)&&ve(I)&&(this.min=Math.max(this.min,I)),!ve(this.userMax)&&ve(M)&&(this.max=Math.min(this.max,M)),_&&ve(p)&&ve(h)){let ne=x||0;!be(me)&&this.min=ne?this.min=C.minRange?Math.min(ne,this.max-S):ne:!be(ge)&&this.max>ne&&h<=ne&&(this.max=C.minRange?Math.max(ne,this.min+S):ne)}!d.polar&&this.min>this.max&&(be(C.min)?this.max=this.min:be(C.max)&&(this.min=this.max)),j=this.max-this.min}if(this.tickInterval=this.min!==this.max&&ve(this.min)&&ve(this.max)?P&&!Z&&U===P.options.tickPixelInterval?Z=P.tickInterval:Be(Z,this.tickAmount?j/Math.max(this.tickAmount-1,1):void 0,c?1:j*U/Math.max(this.len,U)):1,m&&!l){let ne=this.min!==this.old?.min||this.max!==this.old?.max;this.series.forEach(function(fe){fe.forceCrop=fe.forceCropping?.(),fe.processData(ne)}),Et(this,"postProcessData",{hasExtremesChanged:ne})}this.setAxisTranslation(),Et(this,"initialAxisTranslation"),this.pointRange&&!Z&&(this.tickInterval=Math.max(this.pointRange,this.tickInterval));let le=Be(C.minTickInterval,g&&!this.series.some(ne=>!ne.sorted)?this.closestPointRange:0);!Z&&le&&this.tickIntervalMath.max(2*this.len,200))b=[this.min,this.max],Re(19,!1,this.chart);else if(this.dateTime)b=this.getTimeTicks(this.dateTime.normalizeTimeTickInterval(this.tickInterval,l.units),this.min,this.max,l.startOfWeek,this.ordinal?.positions,this.closestPointRange,!0);else if(this.logarithmic)b=this.logarithmic.getLogTickPositions(this.tickInterval,this.min,this.max);else{let _=this.tickInterval,E=_;for(;E<=2*_&&(b=this.getLinearTickPositions(this.tickInterval,this.min,this.max),this.tickAmount&&b.length>this.tickAmount);)this.tickInterval=Ee(this,E*=1.1)}b.length>this.len&&(b=[b[0],b[b.length-1]])[0]===b[1]&&(b.length=1),d&&(this.tickPositions=b,(C=d.apply(this,[this.min,this.max]))&&(b=C))}this.tickPositions=b,this.minorTickInterval="auto"===h&&this.tickInterval?this.tickInterval/l.minorTicksPerMajor:h,this.paddedTicks=b.slice(0),this.trimTicks(b,g,m),!this.isLinked&&ve(this.min)&&ve(this.max)&&(this.single&&b.length<2&&!this.categories&&!this.series.some(_=>_.is("heatmap")&&"between"===_.options.pointPlacement)&&(this.min-=.5,this.max+=.5),c||C||this.adjustTickAmount()),Et(this,"afterSetTickPositions")}trimTicks(l,c,d){let h=l[0],p=l[l.length-1],g=!this.isOrdinal&&this.minPointOffset||0;if(Et(this,"trimTicks"),!this.isLinked||!this.grid){if(c&&h!==-1/0)this.min=h;else for(;this.min-g>l[0];)l.shift();if(d)this.max=p;else for(;this.max+g{let{horiz:x,options:S}=E;return[x?S.left:S.top,S.width,S.height,S.pane].join(",")},_=C(this);d[this.coll].forEach(function(E){let{series:x}=E;x.length&&x.some(S=>S.visible)&&E!==c&&C(E)===_&&(l=!0,h.push(E))})}if(l&&m){h.forEach(_=>{let E=_.getThresholdAlignment(c);ve(E)&&b.push(E)});let C=b.length>1?b.reduce((_,E)=>_+E,0)/b.length:void 0;h.forEach(_=>{_.thresholdAlignment=C})}return l}getThresholdAlignment(l){if((!ve(this.dataMin)||this!==l&&this.series.some(c=>c.isDirty||c.isDirtyData))&&this.getSeriesExtremes(),ve(this.threshold)){let c=Zi((this.threshold-(this.dataMin||0))/((this.dataMax||0)-(this.dataMin||0)),0,1);return this.options.reversed&&(c=1-c),c}}getTickAmount(){let l=this.options,c=l.tickPixelInterval,d=l.tickAmount;be(l.tickInterval)||d||!(this.leng.push(Ln(g[g.length-1]+S)),T=()=>g.unshift(Ln(g[0]-S));if(ve(b)&&(M=b<.5?Math.ceil(b*(m-1)):Math.floor(b*(m-1)),p.reversed&&(M=m-1-M)),l.hasData()&&ve(h)&&ve(d)){let O=()=>{l.transA*=(C-1)/(m-1),l.min=p.startOnTick?g[0]:Math.min(h,g[0]),l.max=p.endOnTick?g[g.length-1]:Math.max(d,g[g.length-1])};if(ve(M)&&ve(l.threshold)){for(;g[M]!==_||g.length!==m||g[0]>h||g[g.length-1]l.threshold?T():I();if(S>8*l.tickInterval)break;S*=2}O()}else if(C0&&x{d=d||g.isDirtyData||g.isDirty,h=h||g.xAxis?.isDirty||!1}),this.setAxisSize();let p=this.len!==this.old?.len;p||d||h||this.isLinked||this.forceRedraw||this.userMin!==this.old?.userMin||this.userMax!==this.old?.userMax||this.alignToOthers()?(c&&"yAxis"===l&&c.buildStacks(),this.forceRedraw=!1,this.userMinRange||(this.minRange=void 0),this.getSeriesExtremes(),this.setTickInterval(),c&&"xAxis"===l&&c.buildStacks(),this.isDirty||(this.isDirty=p||this.min!==this.old?.min||this.max!==this.old?.max)):c&&c.cleanStacks(),d&&delete this.allExtremes,Et(this,"afterSetScale")}setExtremes(l,c,d=!0,h,p){let g=this.chart;this.series.forEach(m=>{delete m.kdTree}),l=g.time.parse(l),c=g.time.parse(c),Et(this,"setExtremes",p=Ei(p,{min:l,max:c}),m=>{this.userMin=m.min,this.userMax=m.max,this.eventArgs=m,d&&g.redraw(h)})}setAxisSize(){let l=this.chart,c=this.options,d=c.offsets||[0,0,0,0],h=this.horiz,p=this.width=Math.round(xi(Be(c.width,l.plotWidth-d[3]+d[1]),l.plotWidth)),g=this.height=Math.round(xi(Be(c.height,l.plotHeight-d[0]+d[2]),l.plotHeight)),m=this.top=Math.round(xi(Be(c.top,l.plotTop+d[0]),l.plotHeight,l.plotTop)),b=this.left=Math.round(xi(Be(c.left,l.plotLeft+d[3]),l.plotWidth,l.plotLeft));this.bottom=l.chartHeight-g-m,this.right=l.chartWidth-p-b,this.len=Math.max(h?p:g,0),this.pos=h?b:m}getExtremes(){let l=this.logarithmic;return{min:l?Ln(l.lin2log(this.min)):this.min,max:l?Ln(l.lin2log(this.max)):this.max,dataMin:this.dataMin,dataMax:this.dataMax,userMin:this.userMin,userMax:this.userMax}}getThreshold(l){let c=this.logarithmic,d=c?c.lin2log(this.min):this.min,h=c?c.lin2log(this.max):this.max;return null===l||l===-1/0?l=d:l===1/0?l=h:d>l?l=d:h15&&c<165?h.align="right":c>195&&c<345&&(h.align="left")}),d.align}tickSize(l){let p,c=this.options,d=Be(c["tick"===l?"tickWidth":"minorTickWidth"],"tick"===l&&this.isXAxis&&!this.categories?1:0),h=c["tick"===l?"tickLength":"minorTickLength"];d&&h&&("inside"===c[l+"Position"]&&(h=-h),p=[h,d]);let g={tickSize:p};return Et(this,"afterTickSize",g),g.tickSize}labelMetrics(){let l=this.chart.renderer,c=this.ticks,d=c[Object.keys(c)[0]]||{};return this.chart.renderer.fontMetrics(d.label||d.movedLabel||l.box)}unsquish(){let E,S,l=this.options.labels,c=l.padding||0,d=this.horiz,h=this.tickInterval,p=this.len/((+!!this.categories+this.max-this.min)/h),g=l.rotation,m=Ln(.8*this.labelMetrics().h),b=Math.max(this.max-this.min,0),C=function(M){let I=(M+2*c)/(p||1);return(I=I>1?Math.ceil(I):1)*h>b&&M!==1/0&&p!==1/0&&b&&(I=Math.ceil(b/h)),Ln(I*h)},_=h,x=Number.MAX_VALUE;if(d){if(!l.staggerLines&&(ve(g)?S=[g]:p=-90&&T<=90)&&(I=(M=C(Math.abs(m/Math.sin(Zt*T))))+Math.abs(T/360))I&&(I=R)}),this.maxLabelLength=I,this.autoRotation?I>C&&I>E.h?_.rotation=this.labelRotation:this.labelRotation=0:b&&(S=C),_.rotation&&(S=I>.5*l.chartHeight?.33*l.chartHeight:I,x||(M=1)),this.labelAlign=p.align||this.autoLabelAlign(this.labelRotation),this.labelAlign&&(_.align=this.labelAlign),d.forEach(function(T){let O=h[T],R=O?.label,P=g.width,U={};R&&(R.attr(_),O.shortenLabel?O.shortenLabel():S&&!P&&"nowrap"!==g.whiteSpace&&(S<(R.textPxLength||0)||"SPAN"===R.element.tagName)?R.css(Ei(U,{width:`${S}px`,lineClamp:M})):!R.styles.width||U.width||P||R.css({width:"auto"}),O.rotation=_.rotation)},this),this.tickRotCorr=c.rotCorr(E.b,this.labelRotation||0,0!==this.side)}hasData(){return this.series.some(function(l){return l.hasData()})||this.options.showEmpty&&be(this.min)&&be(this.max)}addTitle(l){let c,p=this.opposite,g=this.options.title,m=this.chart.styledMode;this.axisTitle||((c=g.textAlign)||(c=(this.horiz?{low:"left",middle:"center",high:"right"}:{low:p?"right":"left",middle:"center",high:p?"left":"right"})[g.align]),this.axisTitle=this.chart.renderer.text(g.text||"",0,0,g.useHTML).attr({zIndex:7,rotation:g.rotation||0,align:c}).addClass("highcharts-axis-title"),m||this.axisTitle.css(No(g.style)),this.axisTitle.add(this.axisGroup),this.axisTitle.isNew=!0),m||g.style.width||this.isRadial||this.axisTitle.css({width:this.len+"px"}),this.axisTitle[l?"show":"hide"](l)}generateTick(l){let c=this.ticks;c[l]?c[l].addLabel():c[l]=new xn(this,l)}createGroups(){let{axisParent:l,chart:c,coll:d,options:h}=this,p=c.renderer,g=(m,b,C)=>p.g(m).attr({zIndex:C}).addClass(`highcharts-${d.toLowerCase()}${b} `+(this.isRadial?`highcharts-radial-axis${b} `:"")+(h.className||"")).add(l);this.axisGroup||(this.gridGroup=g("grid","-grid",h.gridZIndex),this.axisGroup=g("axis","",h.zIndex),this.labelGroup=g("axis-labels","-labels",h.labels.zIndex))}getOffset(){let O,P,F,j,l=this,{chart:c,horiz:d,options:h,side:p,ticks:g,tickPositions:m,coll:b}=l,C=c.inverted&&!l.isZAxis?[1,0,3,2][p]:p,_=l.hasData(),E=h.title,x=h.labels,S=ve(h.crossing),M=c.axisOffset,I=c.clipOffset,T=[-1,1,1,-1][p],R=0,U=0,$=0;if(l.showAxis=O=_||h.showEmpty,l.staggerLines=l.horiz&&x.staggerLines||void 0,l.createGroups(),_||l.isLinked?(m.forEach(function(q){l.generateTick(q)}),l.renderUnsquish(),l.reserveSpaceDefault=0===p||2===p||{1:"left",3:"right"}[p]===l.labelAlign,Be(x.reserveSpace,!S&&null,"center"===l.labelAlign||null,l.reserveSpaceDefault)&&m.forEach(function(q){$=Math.max(g[q].getLabelSize(),$)}),l.staggerLines&&($*=l.staggerLines),l.labelOffset=$*(l.opposite?-1:1)):ti(g,function(q,Z){q.destroy(),delete g[Z]}),E?.text&&!1!==E.enabled&&(l.addTitle(O),O&&!S&&!1!==E.reserveSpace&&(l.titleOffset=R=l.axisTitle.getBBox()[d?"height":"width"],U=be(P=E.offset)?0:Be(E.margin,d?5:10))),l.renderLine(),l.offset=T*Be(h.offset,M[p]?M[p]+(h.margin||0):0),l.tickRotCorr=l.tickRotCorr||{x:0,y:0},j=0===p?-l.labelMetrics().h:2===p?l.tickRotCorr.y:0,F=Math.abs($)+U,$&&(F-=j,F+=T*(d?Be(x.y,l.tickRotCorr.y+T*x.distance):Be(x.x,T*x.distance))),l.axisTitleMargin=Be(P,F),l.getMaxLabelDimensions&&(l.maxLabelDimensions=l.getMaxLabelDimensions(g,m)),"colorAxis"!==b&&I){let q=this.tickSize("tick");M[p]=Math.max(M[p],(l.axisTitleMargin||0)+R+T*l.offset,F,m?.length&&q?q[0]+T*l.offset:0);let Z=!l.axisLine||h.offset?0:l.axisLine.strokeWidth()/2;I[C]=Math.max(I[C],Z)}Et(this,"afterGetOffset")}getLinePath(l){let c=this.chart,d=this.opposite,h=this.offset,p=this.horiz,g=this.left+(d?this.width:0)+h,m=c.chartHeight-this.bottom-(d?this.height:0)+h;return d&&(l*=-1),c.renderer.crispLine([["M",p?this.left:g,p?m:this.top],["L",p?c.chartWidth-this.right:g,p?m:c.chartHeight-this.bottom]],l)}renderLine(){!this.axisLine&&(this.axisLine=this.chart.renderer.path().addClass("highcharts-axis-line").add(this.axisGroup),this.chart.styledMode||this.axisLine.attr({stroke:this.options.lineColor,"stroke-width":this.options.lineWidth,zIndex:7}))}getTitlePosition(l){let c=this.horiz,d=this.left,h=this.top,p=this.len,g=this.options.title,m=c?d:h,b=this.opposite,C=this.offset,_=g.x,E=g.y,x=this.chart.renderer.fontMetrics(l),S=l?Math.max(l.getBBox(!1,0).height-x.h-1,0):0,M={low:m+(c?0:p),middle:m+p/2,high:m+(c?p:0)}[g.align],I=(c?h+this.height:d)+(c?1:-1)*(b?-1:1)*(this.axisTitleMargin||0)+[-S,S,x.f,-S][this.side],T={x:c?M+_:I+(b?this.width:0)+C+_,y:c?I+E-(b?this.height:0)+C:M+E};return Et(this,"afterGetTitlePosition",{titlePosition:T}),T}renderMinorTick(l,c){let d=this.minorTicks;d[l]||(d[l]=new xn(this,l,"minor")),c&&d[l].isNew&&d[l].render(null,!0),d[l].render(null,!1,1)}renderTick(l,c,d){let p=this.ticks;(!this.isLinked||l>=this.min&&l<=this.max||this.grid?.isColumn)&&(p[l]||(p[l]=new xn(this,l)),d&&p[l].isNew&&p[l].render(c,!0,-1),p[l].render(c))}render(){let l,c,d=this,h=d.chart,p=d.logarithmic,m=d.options,b=d.isLinked,C=d.tickPositions,_=d.axisTitle,E=d.ticks,x=d.minorTicks,S=d.alternateBands,M=m.stackLabels,I=m.alternateGridColor,T=m.crossing,O=d.tickmarkOffset,R=d.axisLine,P=d.showAxis,U=ih(h.renderer.globalAnimation);if(d.labelEdge.length=0,d.overlap=!1,[E,x,S].forEach(function($){ti($,function(F){F.isActive=!1})}),ve(T)){let $=this.isXAxis?h.yAxis[0]:h.xAxis[0],F=[1,-1,-1,1][this.side];if($){let j=$.toPixels(T,!0);d.horiz&&(j=$.len-j),d.offset=F*j}}if(d.hasData()||b){let $=d.chart.hasRendered&&d.old&&ve(d.old.min);d.minorTickInterval&&!d.categories&&d.getMinorTickPositions().forEach(function(F){d.renderMinorTick(F,$)}),C.length&&(C.forEach(function(F,j){d.renderTick(F,j,$)}),O&&(0===d.min||d.single)&&(E[-1]||(E[-1]=new xn(d,-1,null,!0)),E[-1].render(-1))),I&&C.forEach(function(F,j){c=void 0!==C[j+1]?C[j+1]+O:d.max-O,j%2==0&&F=.5)h=Math.round(h),E=b.getLinearTickPositions(h,p,g);else if(h>=.08){let x,S,M,I,T,O,R;for(x=h>.3?[1,2,4]:h>.15?[1,2,4,6,8]:[1,2,3,4,5,6,7,8,9],S=Math.floor(p);Sp&&(!m||O<=g)&&void 0!==O&&E.push(O),O>g&&(R=!0),O=T}else{let x=this.lin2log(p),S=this.lin2log(g),M=m?b.getMinorTickInterval():_.tickInterval;h=er(h=mg("auto"===M?null:M,this.minorAutoInterval,_.tickPixelInterval/(m?5:1)*(S-x)/((m?C/b.tickPositions.length:C)||1))),E=b.getLinearTickPositions(h,x,S).map(this.log2lin),m||(this.minorAutoInterval=h/5)}return m||(b.tickInterval=h),E}lin2log(h){return Math.pow(10,h)}log2lin(h){return Math.log(h)/Math.LN10}}v.Additions=c}(Ht||(Ht={}));let eo=Ht,{erase:la,extend:_r,isNumber:to}=we;!function(v){let s;function l(b){return this.addPlotBandOrLine(b,"plotBands")}function c(b,C){let _=this.userOptions,E=new s(this,b);if(this.visible&&(E=E.render()),E){if(this._addedPlotLB||(this._addedPlotLB=!0,(_.plotLines||[]).concat(_.plotBands||[]).forEach(x=>{this.addPlotBandOrLine(x)})),C){let x=_[C]||[];x.push(b),_[C]=x}this.plotLinesAndBands.push(E)}return E}function d(b){return this.addPlotBandOrLine(b,"plotLines")}function h(b,C,_){let T,R,E=this.getPlotLinePath({value:C,force:!0,acrossPanes:(_=_||this.options).acrossPanes}),x=[],S=this.horiz,M=!to(this.min)||!to(this.max)||bthis.max&&C>this.max,I=this.getPlotLinePath({value:b,force:!0,acrossPanes:_.acrossPanes}),O=1;if(I&&E)for(M&&(R=I.toString()===E.toString(),O=0),T=0;T{let c=[];for(let d of this.axes)for(let{label:h,options:p}of d.plotLinesAndBands)h&&!p?.label?.allowOverlap&&c.push(h);return c})}),$x.compose(sh,l)}constructor(s,l){this.axis=s,this.options=l,this.id=l.id}render(){Hx(this,"render");let R,{axis:s,options:l}=this,{horiz:c,logarithmic:d}=s,{color:h,events:p,zIndex:g=0}=l,{renderer:m,time:b}=s.chart,C={},_=b.parse(l.to),E=b.parse(l.from),x=b.parse(l.value),S=l.borderWidth,M=l.label,{label:I,svgElem:T}=this,O=[],P=no(E)&&no(_),U=no(x),$=!T,F={class:"highcharts-plot-"+(P?"band ":"line ")+(l.className||"")},j=P?"bands":"lines";if(!s.chart.styledMode&&(U?(F.stroke=h||"#999999",F["stroke-width"]=zx(l.width,1),l.dashStyle&&(F.dashstyle=l.dashStyle)):P&&(F.fill=h||"#e6e9ff",S&&(F.stroke=l.borderColor,F["stroke-width"]=S))),C.zIndex=g,j+="-"+g,(R=s.plotLinesAndBandsGroups[j])||(s.plotLinesAndBandsGroups[j]=R=m.g("plot-"+j).attr(C).add()),T||(this.svgElem=T=m.path().attr(F).add(R)),no(x))O=s.getPlotLinePath({value:d?.log2lin(x)??x,lineWidth:T.strokeWidth(),acrossPanes:l.acrossPanes});else{if(!no(E)||!no(_))return;O=s.getPlotBandPath(d?.log2lin(E)??E,d?.log2lin(_)??_,l)}return!this.eventsAdded&&p&&(Ux(p,(q,Z)=>{T?.on(Z,X=>{p[Z].apply(this,[X])})}),this.eventsAdded=!0),!$&&T.d||!O?.length?T&&(O?(T.show(),T.animate({d:O})):T.d&&(T.hide(),I&&(this.label=I=I.destroy()))):T.attr({d:O}),M&&(no(M.text)||no(M.formatter))&&O?.length&&s.width>0&&s.height>0&&!O.isFlat?(M=ob({align:c&&P?"center":void 0,x:c?!P&&4:10,verticalAlign:!c&&P?"middle":void 0,y:c?P?16:10:P?6:-4,rotation:c&&!P?90:0,...P?{inside:!0}:{}},M),this.renderLabel(M,O,P,g)):I&&I.hide(),this}renderLabel(s,l,c,d){let h=this.axis,p=h.chart.renderer,g=s.inside,m=this.label;m||(this.label=m=p.text(this.getLabelText(s),0,0,s.useHTML).attr({align:s.textAlign||s.align,rotation:s.rotation,class:"highcharts-plot-"+(c?"band":"line")+"-label "+(s.className||""),zIndex:d}),h.chart.styledMode||m.css(ob({color:h.chart.options.title?.style?.color,fontSize:"0.8em",textOverflow:c&&!g?"":"ellipsis"},s.style)),m.add());let b=l.xBounds||[l[0][1],l[1][1],c?l[2][1]:l[0][1]],C=l.yBounds||[l[0][2],l[1][2],c?l[2][2]:l[0][2]],_=oh(b),E=oh(C),x=Si(b)-_;m.align(s,!1,{x:_,y:E,width:x,height:Si(C)-E}),m.alignAttr.y-=p.fontMetrics(m).b,(!m.alignValue||"left"===m.alignValue||no(g))&&m.css({width:(s.style?.width||(c&&g?x:90===m.rotation?h.height-(m.alignAttr.y-h.top):(s.clip?h.width:h.chart.chartWidth)-(m.alignAttr.x-h.left)))+"px"}),m.show(!0)}getLabelText(s){return no(s.formatter)?s.formatter.call(this):s.text}destroy(){jc(this.axis.plotLinesAndBands,this),delete this.axis,ib(this)}}let{animObject:Wx}=G,{format:yg}=Qr,{composed:Gx,dateFormats:qx,doc:sb,isSafari:Yx}=Q,{distribute:vg}=Wi,{addEvent:bg,clamp:pl,css:ab,discardElement:Xx,extend:_g,fireEvent:Hc,getAlignFactor:fn,isArray:lb,isNumber:ca,isObject:ln,isString:ah,merge:ua,pick:Lr,pushUnique:lh,splat:Fn,syncTimeout:cb}=we;class gl{constructor(s,l,c){this.allowShared=!0,this.crosshairs=[],this.distance=0,this.isHidden=!0,this.isSticky=!1,this.options={},this.outside=!1,this.chart=s,this.init(s,l),this.pointer=c}bodyFormatter(s){return s.map(l=>{let c=l.series.tooltipOptions,d=l.formatPrefix||"point";return(c[d+"Formatter"]||l.tooltipFormatter).call(l,c[d+"Format"]||"")})}cleanSplit(s){this.chart.series.forEach(function(l){let c=l?.tt;c&&(!c.isActive||s?l.tt=c.destroy():c.isActive=!1)})}defaultFormatter(s){let l,c=this.points||Fn(this);return(l=(l=[s.headerFooterFormatter(c[0])]).concat(s.bodyFormatter(c))).push(s.headerFooterFormatter(c[0],!0)),l}destroy(){this.label&&(this.label=this.label.destroy()),this.split&&(this.cleanSplit(!0),this.tt&&(this.tt=this.tt.destroy())),this.renderer&&(this.renderer=this.renderer.destroy(),Xx(this.container)),we.clearTimeout(this.hideTimer)}getAnchor(s,l){let c,{chart:d,pointer:h}=this,p=d.inverted,g=d.plotTop,m=d.plotLeft;if(s=Fn(s),s[0].series?.yAxis&&!s[0].series.yAxis.options.reversedStacks&&(s=s.slice().reverse()),this.followPointer&&l)void 0===l.chartX&&(l=h.normalize(l)),c=[l.chartX-m,l.chartY-g];else if(s[0].tooltipPos)c=s[0].tooltipPos;else{let C=0,_=0;s.forEach(function(E){let x=E.pos(!0);x&&(C+=x[0],_+=x[1])}),C/=s.length,_/=s.length,this.shared&&s.length>1&&l&&(p?C=l.chartX:_=l.chartY),c=[C-m,_-g]}let b={point:s[0],ret:c};return Hc(this,"getAnchor",b),b.ret.map(Math.round)}getClassName(s,l,c){let h=s.series,p=h.options;return[this.options.className,"highcharts-label",c&&"highcharts-tooltip-header",l?"highcharts-tooltip-box":"highcharts-tooltip",!c&&"highcharts-color-"+Lr(s.colorIndex,h.colorIndex),p?.className].filter(ah).join(" ")}getLabel({anchorX:s,anchorY:l}={anchorX:0,anchorY:0}){let c=this,d=this.chart.styledMode,h=this.options,p=this.split&&this.allowShared,g=this.container,m=this.chart.renderer;if(this.label){let b=!this.label.hasClass("highcharts-label");(!p&&b||p&&!b)&&this.destroy()}if(!this.label){if(this.outside){let b=this.chart,C=b.options.chart.style,_=ol.getRendererType();this.container=g=Q.doc.createElement("div"),g.className="highcharts-tooltip-container "+(b.renderTo.className.match(/(highcharts[a-zA-Z0-9-]+)\s?/gm)||""),ab(g,{position:"absolute",top:"1px",pointerEvents:"none",zIndex:Math.max(this.options.style.zIndex||0,(C?.zIndex||0)+3)}),this.renderer=m=new _(g,0,0,C,void 0,void 0,m.styledMode)}if(p?this.label=m.g("tooltip"):(this.label=m.label("",s,l,h.shape||"callout",void 0,void 0,h.useHTML,void 0,"tooltip").attr({padding:h.padding,r:h.borderRadius}),d||this.label.attr({fill:h.backgroundColor,"stroke-width":h.borderWidth||0}).css(h.style).css({pointerEvents:h.style.pointerEvents||(this.shouldStickOnContact()?"auto":"none")})),c.outside){let b=this.label;[b.xSetter,b.ySetter].forEach((C,_)=>{b[_?"ySetter":"xSetter"]=E=>{C.call(b,c.distance),b[_?"y":"x"]=E,g&&(g.style[_?"top":"left"]=`${E}px`)}})}this.label.attr({zIndex:8}).shadow(h.shadow??!h.fixed).add()}return g&&!g.parentElement&&Q.doc.body.appendChild(g),this.label}getPlayingField(){let{body:s,documentElement:l}=sb,{chart:c,distance:d,outside:h}=this;return{width:h?Math.max(s.scrollWidth,l.scrollWidth,s.offsetWidth,l.offsetWidth,l.clientWidth)-2*d-2:c.chartWidth,height:h?Math.max(s.scrollHeight,l.scrollHeight,s.offsetHeight,l.offsetHeight,l.clientHeight):c.chartHeight}}getPosition(s,l,c){let j,{distance:d,chart:h,outside:p,pointer:g}=this,{inverted:m,plotLeft:b,plotTop:C,polar:_}=h,{plotX:E=0,plotY:x=0}=c,S={},M=m&&c.h||0,{height:I,width:T}=this.getPlayingField(),O=g.getChartPosition(),R=le=>le*O.scaleX,P=le=>le*O.scaleY,U=le=>{let ne="x"===le;return[le,ne?T:I,ne?s:l].concat(p?[ne?R(s):P(l),ne?O.left-d+R(E+b):O.top-d+P(x+C),0,ne?T:I]:[ne?s:l,ne?E+b:x+C,ne?b:C,ne?b+h.plotWidth:C+h.plotHeight])},$=U("y"),F=U("x"),q=!!c.negative;!_&&h.hoverSeries?.yAxis?.reversed&&(q=!q);let Z=!this.followPointer&&Lr(c.ttBelow,!_&&!m===q),X=function(le,ne,fe,mt,qe,tr,Ie){let De=p?"y"===le?P(d):R(d):d,Qe=(fe-mt)/2,Fe=mtne?Xe:Xe+M)}},ue=function(le,ne,fe,mt,qe){if(qene-d)return!1;S[le]=qene-mt/2?ne-mt-2:qe-fe/2},me=function(le){[$,F]=[F,$],j=le},ge=()=>{!1!==X.apply(0,$)?!1!==ue.apply(0,F)||j||(me(!0),ge()):j?S.x=S.y=0:(me(!0),ge())};return(m&&!_||this.len>1)&&me(),ge(),S}getFixedPosition(s,l,c){let d=c.series,{chart:h,options:p,split:g}=this,m=p.position,b=m.relativeTo,C=p.shared||d?.yAxis?.isRadial&&("pane"===b||!b)?"plotBox":b,_="chart"===C?h.renderer:h[C]||h.getClipBox(d,!0);return{x:_.x+(_.width-s)*fn(m.align)+m.x,y:_.y+(_.height-l)*fn(m.verticalAlign)+(!g&&m.y||0)}}hide(s){let l=this;we.clearTimeout(this.hideTimer),s=Lr(s,this.options.hideDelay),this.isHidden||(this.hideTimer=cb(function(){let c=l.getLabel();l.getLabel().animate({opacity:0},{duration:s&&150,complete:()=>{c.hide(),l.container&&l.container.remove()}}),l.isHidden=!0},s))}init(s,l){this.chart=s,this.options=l,this.crosshairs=[],this.isHidden=!0,this.split=l.split&&!s.inverted&&!s.polar,this.shared=l.shared||this.split,this.outside=Lr(l.outside,!(!s.scrollablePixelsX&&!s.scrollablePixelsY))}shouldStickOnContact(s){return!(this.followPointer||!this.options.stickOnContact||s&&!this.pointer.inClass(s.target,"highcharts-tooltip"))}move(s,l,c,d){let{followPointer:h,options:p}=this,g=Wx(!h&&!this.isHidden&&!p.fixed&&p.animation),b={x:s,y:l};h||(this.len||0)>1?b.anchorX=b.anchorY=NaN:(b.anchorX=c,b.anchorY=d),g.step=()=>this.drawTracker(),this.getLabel().animate(b,g)}refresh(s,l){let{chart:c,options:d,pointer:h,shared:p}=this,g=Fn(s),m=g[0],b=d.format,C=d.formatter||this.defaultFormatter,_=c.styledMode,E=this.allowShared;if(!d.enabled||!m.series)return;we.clearTimeout(this.hideTimer),this.allowShared=!(!lb(s)&&s.series&&s.series.noSharedTooltip),E=E&&!this.allowShared,this.followPointer=!this.split&&m.series.tooltipOptions.followPointer;let x=this.getAnchor(s,l),S=x[0],M=x[1];p&&this.allowShared&&(h.applyInactiveState(g),g.forEach(O=>O.setState("hover")),m.points=g),this.len=g.length;let I=ah(b)?yg(b,m,c):C.call(m,this);m.points=void 0;let T=m.series;if(this.distance=Lr(T.tooltipOptions.distance,16),!1===I)this.hide();else{if(this.split&&this.allowShared)this.renderSplit(I,g);else{let O=S,R=M;if(l&&h.isDirectTouch&&(O=l.chartX-c.plotLeft,R=l.chartY-c.plotTop),!c.polar&&!1!==T.options.clip&&!g.some(P=>h.isDirectTouch||P.series.shouldShowTooltip(O,R)))return void this.hide();{let P=this.getLabel(E&&this.tt||{});(!d.style.width||_)&&P.css({width:(this.outside?this.getPlayingField():c.spacingBox).width+"px"}),P.attr({class:this.getClassName(m),text:I&&I.join?I.join(""):I}),this.outside&&P.attr({x:pl(P.x||0,0,this.getPlayingField().width-(P.width||0)-1)}),_||P.attr({stroke:d.borderColor||m.color||T.color||"#666666"}),this.updatePosition({plotX:S,plotY:M,negative:m.negative,ttBelow:m.ttBelow,series:T,h:x[2]||0})}}this.isHidden&&this.label&&this.label.attr({opacity:1}).show(),this.isHidden=!1}Hc(this,"refresh")}renderSplit(s,l){let c=this,{chart:d,chart:{chartWidth:h,chartHeight:p,plotHeight:g,plotLeft:m,plotTop:b,scrollablePixelsY:C=0,scrollablePixelsX:_,styledMode:E},distance:x,options:S,options:{fixed:M,position:I,positioner:T},pointer:O}=c,{scrollLeft:R=0,scrollTop:P=0}=d.scrollablePlotArea?.scrollingContainer||{},U=c.outside&&"number"!=typeof _?sb.documentElement.getBoundingClientRect():{left:R,right:R+h,top:P,bottom:P+p},$=c.getLabel(),F=this.renderer||d.renderer,j=!!d.xAxis[0]?.opposite,{left:q,top:Z}=O.getChartPosition(),X=T||M,ue=b+P,me=0,ge=g-C,le=function(Ie,De,Qe,Fe=[0,0],yt=!0){let vt,Xe;if(Qe.isHeader)Xe=j?0:ge,vt=pl(Fe[0]-Ie/2,U.left,U.right-Ie-(c.outside?q:0));else if(M&&Qe){let St=c.getFixedPosition(Ie,De,Qe);vt=St.x,Xe=St.y-ue}else Xe=Fe[1]-ue,vt=pl(vt=yt?Fe[0]-Ie-x:Fe[0]+x,yt?vt:U.left,U.right);return{x:vt,y:Xe}};ah(s)&&(s=[!1,s]);let ne=s.slice(0,l.length+1).reduce(function(Ie,De,Qe){if(!1!==De&&""!==De){let Fe=l[Qe-1]||{isHeader:!0,plotX:l[0].plotX,plotY:g,series:{}},yt=Fe.isHeader,vt=yt?c:Fe.series,Xe=vt.tt=function($r,Bn,Oi){let Uo=$r,{isHeader:un,series:As}=Bn,jr=As.tooltipOptions||S;if(!Uo){let zo={padding:jr.padding,r:jr.borderRadius};E||(zo.fill=jr.backgroundColor,zo["stroke-width"]=jr.borderWidth??(M&&!un?0:1)),Uo=F.label("",0,0,jr[un?"headerShape":"shape"]||(M&&!un?"rect":"callout"),void 0,void 0,jr.useHTML).addClass(c.getClassName(Bn,!0,un)).attr(zo).add($)}return Uo.isActive=!0,Uo.attr({text:Oi}),E||Uo.css(jr.style).attr({stroke:jr.borderColor||Bn.color||As.color||"#333333"}),Uo}(vt.tt,Fe,De.toString()),St=Xe.getBBox(),nr=St.width+Xe.strokeWidth();yt&&(me=St.height,ge+=me,j&&(ue-=me));let{anchorX:pr,anchorY:Ho}=function($r){let Bn,Oi,{isHeader:Uo,plotX:un=0,plotY:As=0,series:jr}=$r;if(Uo)Bn=Math.max(m+un,m),Oi=b+g/2;else{let{xAxis:zo,yAxis:rr}=jr;Bn=zo.pos+pl(un,-x,zo.len+x),jr.shouldShowTooltip(0,rr.pos-b+As,{ignoreX:!0})&&(Oi=rr.pos+As)}return{anchorX:Bn=pl(Bn,U.left-x,U.right+x),anchorY:Oi}}(Fe);if("number"==typeof Ho){let $r=St.height+1,Bn=(T||le).call(c,nr,$r,Fe,[pr,Ho]);Ie.push({align:X?0:void 0,anchorX:pr,anchorY:Ho,boxWidth:nr,point:Fe,rank:Lr(Bn.rank,+!!yt),size:$r,target:Bn.y,tt:Xe,x:Bn.x})}else Xe.isActive=!1}return Ie},[]);!X&&ne.some(Ie=>{let{outside:De}=c,Qe=(De?q:0)+Ie.anchorX;return QeQe})&&(ne=ne.map(Ie=>{let{x:De,y:Qe}=le.call(this,Ie.boxWidth,Ie.size,Ie.point,[Ie.anchorX,Ie.anchorY],!1);return _g(Ie,{target:Qe,x:De})})),c.cleanSplit(),vg(ne,ge);let fe={left:q,right:q};ne.forEach(function(Ie){let{x:De,boxWidth:Qe,isHeader:Fe}=Ie;!Fe&&(c.outside&&q+Defe.right&&(fe.right=q+De))}),ne.forEach(function(Ie){let{x:De,anchorX:Qe,anchorY:Fe,pos:yt,point:{isHeader:vt}}=Ie,Xe={visibility:void 0===yt?"hidden":"inherit",x:De,y:(yt||0)+ue+(M&&I.y||0),anchorX:Qe,anchorY:Fe};if(c.outside&&De0&&(vt||(Xe.x=De+St,Xe.anchorX=Qe+St),vt&&(Xe.x=(fe.right-fe.left)/2,Xe.anchorX=Qe+St))}Ie.tt.attr(Xe)});let{container:mt,outside:qe,renderer:tr}=c;if(qe&&mt&&tr){let{width:Ie,height:De,x:Qe,y:Fe}=$.getBBox();tr.setSize(Ie+Qe,De+Fe,!1),mt.style.left=fe.left+"px",mt.style.top=Z+"px"}Yx&&$.attr({opacity:1===$.opacity?.999:1})}drawTracker(){if(!this.shouldStickOnContact())return void(this.tracker&&(this.tracker=this.tracker.destroy()));let s=this.chart,l=this.label,c=this.shared?s.hoverPoints:s.hoverPoint;if(!l||!c)return;let d={x:0,y:0,width:0,height:0},h=this.getAnchor(c),p=l.getBBox();h[0]+=s.plotLeft-(l.translateX||0),h[1]+=s.plotTop-(l.translateY||0),d.x=Math.min(0,h[0]),d.y=Math.min(0,h[1]),d.width=h[0]<0?Math.max(Math.abs(h[0]),p.width-h[0]):Math.max(Math.abs(h[0]),p.width),d.height=h[1]<0?Math.max(Math.abs(h[1]),p.height-Math.abs(h[1])):Math.max(Math.abs(h[1]),p.height),this.tracker?this.tracker.attr(d):(this.tracker=l.renderer.rect(d).addClass("highcharts-tracker").add(l),s.styledMode||this.tracker.attr({fill:"rgba(0,0,0,0)"}))}styledModeFormat(s){return s.replace('style="font-size: 0.8em"','class="highcharts-header"').replace(/style="color:{(point|series)\.color}"/g,'class="highcharts-color-{$1.colorIndex} {series.options.className} {point.options.className}"')}headerFooterFormatter(s,l){let c=s.series,d=c.tooltipOptions,h=c.xAxis,p=h?.dateTime,g={isFooter:l,point:s},m=d.xDateFormat||"",b=d[l?"footerFormat":"headerFormat"];return Hc(this,"headerFormatter",g,function(C){if(p&&!m&&ca(s.key)&&(m=p.getXDateFormat(s.key,d.dateTimeLabelFormats)),p&&m){if(ln(m)){let _=m;qx[0]=E=>c.chart.time.dateFormat(_,E),m="%0"}(s.tooltipDateKeys||["key"]).forEach(_=>{b=b.replace(RegExp("point\\."+_+"([ \\)}])"),`(point.${_}:${m})$1`)})}c.chart.styledMode&&(b=this.styledModeFormat(b)),C.text=yg(b,s,this.chart)}),g.text||""}update(s){this.destroy(),this.init(this.chart,ua(!0,this.options,s))}updatePosition(s){let P,{chart:l,container:c,distance:d,options:h,pointer:p,renderer:g}=this,{height:m=0,width:b=0}=this.getLabel(),{fixed:C,positioner:_}=h,{left:E,top:x,scaleX:S,scaleY:M}=p.getChartPosition(),I=(_||C&&this.getFixedPosition||this.getPosition).call(this,b,m,s),T=Q.doc,O=(s.plotX||0)+l.plotLeft,R=(s.plotY||0)+l.plotTop;if(g&&c){if(_||C){let{scrollLeft:U=0,scrollTop:$=0}=l.scrollablePlotArea?.scrollingContainer||{};I.x+=U+E-d,I.y+=$+x-d}P=(h.borderWidth||0)+2*d+2,g.setSize(pl(b+P,0,T.documentElement.clientWidth)-1,m+P,!1),(1!==S||1!==M)&&(ab(c,{transform:`scale(${S}, ${M})`}),O*=S,R*=M),O+=E-I.x,R+=x-I.y}this.move(Math.round(I.x),Math.round(I.y||0),O,R)}}!function(v){v.compose=function(s){lh(Gx,"Core.Tooltip")&&bg(s,"afterInit",function(){let l=this.chart;l.options.tooltip&&(l.tooltip=new v(l,l.options.tooltip,this))})}}(gl||(gl={}));let wg=gl,{animObject:Wn}=G,{defaultOptions:Cg}=cr,{format:ch}=Qr,{addEvent:Eg,crisp:ml,erase:uh,extend:ro,fireEvent:da,getNestedProperty:dh,isArray:io,isFunction:Dg,isNumber:oo,isObject:Uc,merge:Oe,pick:wr,syncTimeout:hh,removeEvent:zc,uniqueKey:ub}=we;class yl{animateBeforeDestroy(){let s=this,l={x:s.startXPos,opacity:0},c=s.getGraphicalProps();c.singular.forEach(function(d){s[d]=s[d].animate("dataLabel"===d?{x:s[d].startXPos,y:s[d].startYPos,opacity:0}:l)}),c.plural.forEach(function(d){s[d].forEach(function(h){h.element&&h.animate(ro({x:s.startXPos},h.startYPos?{x:h.startXPos,y:h.startYPos}:{}))})})}applyOptions(s,l){let c=this.series,d=c.options.pointValKey||c.pointValKey;return ro(this,s=yl.prototype.optionsToObject.call(this,s)),this.options=this.options?ro(this.options,s):s,s.group&&delete this.group,s.dataLabels&&delete this.dataLabels,d&&(this.y=yl.prototype.getNestedProperty.call(this,d)),this.selected&&(this.state="select"),"name"in this&&void 0===l&&c.xAxis&&c.xAxis.hasNames&&(this.x=c.xAxis.nameToX(this)),void 0===this.x&&c?this.x=l??c.autoIncrement():oo(s.x)&&c.options.relativeXValue?this.x=c.autoIncrement(s.x):"string"==typeof this.x&&(l??(l=c.chart.time.parse(this.x)),oo(l)&&(this.x=l)),this.isNull=this.isValid&&!this.isValid(),this.formatPrefix=this.isNull?"null":"point",this}destroy(){if(!this.destroyed){let s=this,l=s.series,c=l.chart,d=l.options.dataSorting,h=c.hoverPoints,p=Wn(s.series.chart.renderer.globalAnimation),g=()=>{for(let m in(s.graphic||s.graphics||s.dataLabel||s.dataLabels)&&(zc(s),s.destroyElements()),s)delete s[m]};s.legendItem&&c.legend.destroyItem(s),h&&(s.setState(),uh(h,s),h.length||(c.hoverPoints=null)),s===c.hoverPoint&&s.onMouseOut(),d?.enabled?(this.animateBeforeDestroy(),hh(g,p.duration)):g(),c.pointCount--}this.destroyed=!0}destroyElements(s){let l=this,c=l.getGraphicalProps(s);c.singular.forEach(function(d){l[d]=l[d].destroy()}),c.plural.forEach(function(d){l[d].forEach(function(h){h?.element&&h.destroy()}),delete l[d]})}firePointEvent(s,l,c){let d=this,h=this.series.options;d.manageEvent(s),"click"===s&&h.allowPointSelect&&(c=function(p){!d.destroyed&&d.select&&d.select(null,p.ctrlKey||p.metaKey||p.shiftKey)}),da(d,s,l,c)}getClassName(){return"highcharts-point"+(this.selected?" highcharts-point-select":"")+(this.negative?" highcharts-negative":"")+(this.isNull?" highcharts-null-point":"")+(void 0!==this.colorIndex?" highcharts-color-"+this.colorIndex:"")+(this.options.className?" "+this.options.className:"")+(this.zone?.className?" "+this.zone.className.replace("highcharts-negative",""):"")}getGraphicalProps(s){let l,c,d=this,h=[],p={singular:[],plural:[]};for((s=s||{graphic:1,dataLabel:1}).graphic&&h.push("graphic","connector"),s.dataLabel&&h.push("dataLabel","dataLabelPath","dataLabelUpper"),c=h.length;c--;)d[l=h[c]]&&p.singular.push(l);return["graphic","dataLabel"].forEach(function(g){let m=g+"s";s[g]&&d[m]&&p.plural.push(m)}),p}getNestedProperty(s){if(s)return 0===s.indexOf("custom.")?dh(s,this.options):this[s]}getZone(){let d,s=this.series,l=s.zones,c=s.zoneAxis||"y",h=0;for(d=l[0];this[c]>=d.value;)d=l[++h];return this.nonZonedColor||(this.nonZonedColor=this.color),this.color=d?.color&&!this.options.color?d.color:this.nonZonedColor,d}hasNewShapeType(){return(this.graphic&&(this.graphic.symbolName||this.graphic.element.nodeName))!==this.shapeType}constructor(s,l,c){this.formatPrefix="point",this.visible=!0,this.point=this,this.series=s,this.applyOptions(l,c),this.id??(this.id=ub()),this.resolveColor(),this.dataLabelOnNull??(this.dataLabelOnNull=s.options.nullInteraction),s.chart.pointCount++,da(this,"afterInit")}isValid(){return(oo(this.x)||this.x instanceof Date)&&oo(this.y)}optionsToObject(s){let g,l=this.series,c=l.options.keys,d=c||l.pointArrayMap||["y"],h=d.length,p={},m=0,b=0;if(oo(s)||null===s)p[d[0]]=s;else if(io(s))for(!c&&s.length>h&&("string"==(g=typeof s[0])?l.xAxis?.dateTime?p.x=l.chart.time.parse(s[0]):p.name=s[0]:"number"===g&&(p.x=s[0]),m++);b0?yl.prototype.setNestedProperty(p,s[m],d[b]):p[d[b]]=s[m]),m++,b++;else"object"==typeof s&&(p=s,s.dataLabels&&(l.hasDataLabels=()=>!0),s.marker&&(l._hasPointMarkers=!0));return p}pos(s,l=this.plotY){if(!this.destroyed){let{plotX:c,series:d}=this,{chart:h,xAxis:p,yAxis:g}=d,m=0,b=0;if(oo(c)&&oo(l))return s&&(m=p?p.pos:h.plotLeft,b=g?g.pos:h.plotTop),h.inverted&&p&&g?[g.len-l+b,p.len-c+m]:[c+m,l+b]}}resolveColor(){let d,h,g,s=this.series,c=s.chart.styledMode,p=s.chart.options.chart.colorCount;delete this.nonZonedColor,s.options.colorByPoint?(c||(d=(h=s.options.colors||s.chart.options.colors)[s.colorCounter],p=h.length),g=s.colorCounter,s.colorCounter++,s.colorCounter===p&&(s.colorCounter=0)):(c||(d=s.color),g=s.colorIndex),this.colorIndex=wr(this.options.colorIndex,g),this.color=wr(this.options.color,d)}setNestedProperty(s,l,c){return c.split(".").reduce(function(d,h,p,g){return d[h]=g.length-1===p?l:Uc(d[h],!0)?d[h]:{},d[h]},s),s}shouldDraw(){return!this.isNull}tooltipFormatter(s){let{chart:l,pointArrayMap:c=["y"],tooltipOptions:d}=this.series,{valueDecimals:h="",valuePrefix:p="",valueSuffix:g=""}=d;return l.styledMode&&(s=l.tooltip?.styledModeFormat(s)||s),c.forEach(m=>{m="{point."+m,(p||g)&&(s=s.replace(RegExp(m+"}","g"),p+m+"}"+g)),s=s.replace(RegExp(m+"}","g"),m+":,."+h+"f}")}),ch(s,this,l)}update(s,l,c,d){let h,p=this,g=p.series,m=p.graphic,b=g.chart,C=g.options;function _(){p.applyOptions(s);let E=m&&p.hasMockGraphic;m&&(null===p.y?!E:E)&&(p.graphic=m.destroy(),delete p.hasMockGraphic),Uc(s,!0)&&(m?.element&&s&&s.marker&&void 0!==s.marker.symbol&&(p.graphic=m.destroy()),s?.dataLabels&&p.dataLabel&&(p.dataLabel=p.dataLabel.destroy())),h=p.index;let S={};for(let M of g.dataColumnKeys())S[M]=p[M];g.dataTable.setRow(S,h),C.data[h]=Uc(C.data[h],!0)||Uc(s,!0)?p.options:wr(s,C.data[h]),g.isDirty=g.isDirtyData=!0,!g.fixedBox&&g.hasCartesianSeries&&(b.isDirtyBox=!0),"point"===C.legendType&&(b.isDirtyLegend=!0),l&&b.redraw(c)}l=wr(l,!0),!1===d?_():p.firePointEvent("update",{options:s},_)}remove(s,l){this.series.removePoint(this.series.data.indexOf(this),s,l)}select(s,l){let c=this,d=c.series,h=d.chart;s=wr(s,!c.selected),this.selectedStaging=s,c.firePointEvent(s?"select":"unselect",{accumulate:l},function(){c.selected=c.options.selected=s,d.options.data[d.data.indexOf(c)]=c.options,c.setState(s&&"select"),l||h.getSelectedPoints().forEach(function(p){let g=p.series;p.selected&&p!==c&&(p.selected=p.options.selected=!1,g.options.data[g.data.indexOf(p)]=p.options,p.setState(h.hoverPoints&&g.options.inactiveOtherPoints?"inactive":""),p.firePointEvent("unselect"))})}),delete this.selectedStaging}onMouseOver(s){let{inverted:l,pointer:c}=this.series.chart;c&&(s=s?c.normalize(s):c.getChartCoordinatesFromPoint(this,l),c.runPointActions(s,this))}onMouseOut(){let s=this.series.chart;this.firePointEvent("mouseOut"),this.series.options.inactiveOtherPoints||(s.hoverPoints||[]).forEach(function(l){l.setState()}),s.hoverPoints=s.hoverPoint=null}manageEvent(s){let l=Oe(this.series.options.point,this.options),c=l.events?.[s];!Dg(c)||this.hcEvents?.[s]&&-1!==this.hcEvents?.[s]?.map(d=>d.fn).indexOf(c)?this.importedUserEvent&&!c&&this.hcEvents?.[s]&&this.hcEvents?.[s].userEvent&&(zc(this,s),delete this.hcEvents[s],Object.keys(this.hcEvents)||delete this.importedUserEvent):(this.importedUserEvent?.(),this.importedUserEvent=Eg(this,s,c),this.hcEvents&&(this.hcEvents[s].userEvent=!0))}setState(s,l){let S,M,I,O,c=this.series,d=this.state,h=c.options.states[s||"normal"]||{},p=Cg.plotOptions[c.type].marker&&c.options.marker,g=p&&!1===p.enabled,m=p?.states?.[s||"normal"]||{},C=this.marker||{},_=c.chart,E=p&&c.markerAttribs,x=c.halo,T=c.stateMarkerGraphic;if((s=s||"")===this.state&&!l||this.selected&&"select"!==s||!1===h.enabled||s&&(!1===m.enabled||g&&!1===m.enabled)||s&&C.states&&C.states[s]&&!1===C.states[s].enabled)return;if(this.state=s,E&&(S=c.markerAttribs(this,s)),this.graphic&&!this.hasMockGraphic){if(d&&this.graphic.removeClass("highcharts-point-"+d),s&&this.graphic.addClass("highcharts-point-"+s),!_.styledMode){M=c.pointAttribs(this,s),I=wr(_.options.chart.animation,h.animation);let $=M.opacity;c.options.inactiveOtherPoints&&oo($)&&(this.dataLabels||[]).forEach(function(F){F&&!F.hasClass("highcharts-data-label-hidden")&&(F.animate({opacity:$},I),F.connector&&F.connector.animate({opacity:$},I))}),this.graphic.animate(M,I)}S&&this.graphic.animate(S,wr(_.options.chart.animation,m.animation,p.animation)),T&&T.hide()}else s&&m&&(O=C.symbol||c.symbol,T&&T.currentSymbol!==O&&(T=T.destroy()),S&&(T?T[l?"animate":"attr"]({x:S.x,y:S.y}):O&&(c.stateMarkerGraphic=T=_.renderer.symbol(O,S.x,S.y,S.width,S.height,Oe(p,m)).add(c.markerGroup),T.currentSymbol=O)),!_.styledMode&&T&&"inactive"!==this.state&&T.attr(c.pointAttribs(this,s))),T&&(T[s&&this.isInside?"show":"hide"](),T.element.point=this,T.addClass(this.getClassName(),!0));let R=h.halo,P=this.graphic||T,U=P?.visibility||"inherit";R?.size&&P&&"hidden"!==U&&!this.isCluster?(x||(c.halo=x=_.renderer.path().add(P.parentGroup)),x.show()[l?"animate":"attr"]({d:this.haloPath(R.size)}),x.attr({class:"highcharts-halo highcharts-color-"+wr(this.colorIndex,c.colorIndex)+(this.className?" "+this.className:""),visibility:U,zIndex:-1}),x.point=this,_.styledMode||x.attr(ro({fill:this.color||c.color,"fill-opacity":R.opacity},bt.filterUserAttributes(R.attributes||{})))):x?.point?.haloPath&&!x.point.destroyed&&x.animate({d:x.point.haloPath(0)},null,x.hide),da(this,"afterSetState",{state:s})}haloPath(s){let l=this.pos();return l?this.series.chart.renderer.symbols.circle(ml(l[0],1)-s,l[1]-s,2*s,2*s):[]}}let so=yl,{parse:fh}=Ot,{charts:Wc,composed:xg,isTouchDevice:db}=Q,{addEvent:ni,attr:hb,css:Gc,extend:K,find:Ve,fireEvent:Dt,isNumber:Gt,isObject:Ae,objectEach:Sg,offset:vl,pick:Mn,pushUnique:Mg,splat:ph}=we;class cn{applyInactiveState(s=[]){let l=[];for(let c of(s.forEach(d=>{let h=d.series;l.push(h),h.linkedParent&&l.push(h.linkedParent),h.linkedSeries&&l.push.apply(l,h.linkedSeries),h.navigatorSeries&&l.push(h.navigatorSeries),h.boosted&&h.markerGroup&&l.push.apply(l,this.chart.series.filter(p=>p.markerGroup===h.markerGroup))}),this.chart.series)){let d=c.options;!1!==d.states?.inactive?.enabled&&(-1===l.indexOf(c)?c.setState("inactive",!0):d.inactiveOtherPoints&&c.setAllPointsToState("inactive"))}}destroy(){let s=this;this.eventsToUnbind.forEach(l=>l()),this.eventsToUnbind=[],!Q.chartCount&&(cn.unbindDocumentMouseUp.forEach(l=>l.unbind()),cn.unbindDocumentMouseUp.length=0,cn.unbindDocumentTouchEnd&&(cn.unbindDocumentTouchEnd=cn.unbindDocumentTouchEnd())),clearInterval(s.tooltipTimeout),Sg(s,function(l,c){s[c]=void 0})}getSelectionMarkerAttrs(s,l){let c={args:{chartX:s,chartY:l},attrs:{},shapeType:"rect"};return Dt(this,"getSelectionMarkerAttrs",c,d=>{let h,{chart:p,zoomHor:g,zoomVert:m}=this,{mouseDownX:b=0,mouseDownY:C=0}=p,_=d.attrs;_.x=p.plotLeft,_.y=p.plotTop,_.width=g?1:p.plotWidth,_.height=m?1:p.plotHeight,g&&(_.width=Math.max(1,Math.abs(h=s-b)),_.x=(h>0?0:h)+b),m&&(_.height=Math.max(1,Math.abs(h=l-C)),_.y=(h>0?0:h)+C)}),c}drag(s){let I,{chart:l}=this,{mouseDownX:c=0,mouseDownY:d=0}=l,{panning:h,panKey:p,selectionMarkerFill:g}=l.options.chart,m=l.plotLeft,b=l.plotTop,C=l.plotWidth,_=l.plotHeight,E=Ae(h)?h.enabled:h,x=p&&s[`${p}Key`],S=s.chartX,M=s.chartY,T=this.selectionMarker;if((!T||!T.touch)&&(Sm+C&&(S=m+C),Mb+_&&(M=b+_),this.hasDragged=Math.sqrt(Math.pow(c-S,2)+Math.pow(d-M,2)),this.hasDragged>10)){I=l.isInsidePlot(c-m,d-b,{visiblePlotOnly:!0});let{shapeType:O,attrs:R}=this.getSelectionMarkerAttrs(S,M);this.hasZoom&&I&&!x&&!T&&(this.selectionMarker=T=l.renderer[O](),T.attr({class:"highcharts-selection-marker",zIndex:7}).add(),l.styledMode||T.attr({fill:g||fh("#334eff").setOpacity(.25).get()})),T&&T.attr(R),I&&!T&&E&&l.pan(s,h)}}dragStart(s){let l=this.chart;l.mouseIsDown=s.type,l.cancelClick=!1,l.mouseDownX=s.chartX,l.mouseDownY=s.chartY}getSelectionBox(s){let l={args:{marker:s},result:s.getBBox()};return Dt(this,"getSelectionBox",l),l.result}drop(s){let l,{chart:c,selectionMarker:d}=this;for(let h of c.axes)h.isPanning&&(h.isPanning=!1,(h.options.startOnTick||h.options.endOnTick||h.series.some(p=>p.boosted))&&(h.forceRedraw=!0,h.setExtremes(h.userMin,h.userMax,!1),l=!0));if(l&&c.redraw(),d&&s){if(this.hasDragged){let h=this.getSelectionBox(d);c.transform({axes:c.axes.filter(p=>p.zoomEnabled&&("xAxis"===p.coll&&this.zoomX||"yAxis"===p.coll&&this.zoomY)),selection:{originalEvent:s,xAxis:[],yAxis:[],...h},from:h})}Gt(c.index)&&(this.selectionMarker=d.destroy())}c&&Gt(c.index)&&(Gc(c.container,{cursor:c._cursor}),c.cancelClick=this.hasDragged>10,c.mouseIsDown=!1,this.hasDragged=0,this.pinchDown=[],this.hasPinchMoved=!1)}findNearestKDPoint(s,l,c){let d;return s.forEach(function(h){let p=!(h.noSharedTooltip&&l)&&0>h.options.findNearestPointBy.indexOf("y"),g=h.searchPoint(c,p);Ae(g,!0)&&g.series&&(!Ae(d,!0)||function(m,b){let _=m.distX-b.distX,E=m.dist-b.dist,x=b.series.group?.zIndex-m.series.group?.zIndex;return 0!==_&&l?_:0!==E?E:0!==x?x:m.series.index>b.series.index?-1:1}(d,g)>0)&&(d=g)}),d}getChartCoordinatesFromPoint(s,l){let{xAxis:c,yAxis:d}=s.series,h=s.shapeArgs;if(c&&d){let p=s.clientX??s.plotX??0,g=s.plotY||0;return s.isNode&&h&&Gt(h.x)&&Gt(h.y)&&(p=h.x,g=h.y),l?{chartX:d.len+d.pos-g,chartY:c.len+c.pos-p}:{chartX:p+c.pos,chartY:g+d.pos}}if(h?.x&&h.y)return{chartX:h.x,chartY:h.y}}getChartPosition(){if(this.chartPosition)return this.chartPosition;let{container:s}=this.chart,l=vl(s);this.chartPosition={left:l.left,top:l.top,scaleX:1,scaleY:1};let{offsetHeight:c,offsetWidth:d}=s;return d>2&&c>2&&(this.chartPosition.scaleX=l.width/d,this.chartPosition.scaleY=l.height/c),this.chartPosition}getCoordinates(s){let l={xAxis:[],yAxis:[]};for(let c of this.chart.axes)l[c.isXAxis?"xAxis":"yAxis"].push({axis:c,value:c.toValue(s[c.horiz?"chartX":"chartY"])});return l}getHoverData(s,l,c,d,h,p){let C,g=[],m=function(x){return x.visible&&!(!h&&x.directTouch)&&Mn(x.options.enableMouseTracking,!0)},b=l,_={chartX:p?p.chartX:void 0,chartY:p?p.chartY:void 0,shared:h};Dt(this,"beforeGetHoverData",_),C=b&&!b.stickyTracking?[b]:c.filter(x=>x.stickyTracking&&(_.filter||m)(x));let E=d&&s||!p?s:this.findNearestKDPoint(C,h,p);return b=E?.series,E&&(h&&!b.noSharedTooltip?(C=c.filter(function(x){return _.filter?_.filter(x):m(x)&&!x.noSharedTooltip})).forEach(function(x){let S=x.options?.nullInteraction,M=Ve(x.points,function(I){return!(I.x!==E.x||I.isNull&&!S)});Ae(M)&&(x.boosted&&x.boost&&(M=x.boost.getPoint(M)),g.push(M))}):g.push(E)),Dt(this,"afterGetHoverData",_={hoverPoint:E}),{hoverPoint:_.hoverPoint,hoverSeries:b,hoverPoints:g}}getPointFromEvent(s){let c,l=s.target;for(;l&&!c;)c=l.point,l=l.parentNode;return c}onTrackerMouseOut(s){let c=s.relatedTarget,d=this.chart.hoverSeries;this.isDirectTouch=!1,!d||!c||d.stickyTracking||this.inClass(c,"highcharts-tooltip")||this.inClass(c,"highcharts-series-"+d.index)&&this.inClass(c,"highcharts-tracker")||d.onMouseOut()}inClass(s,l){let d,c=s;for(;c;){if(d=hb(c,"class")){if(-1!==d.indexOf(l))return!0;if(-1!==d.indexOf("highcharts-container"))return!1}c=c.parentElement}}constructor(s,l){this.hasDragged=0,this.pointerCaptureEventsToUnbind=[],this.eventsToUnbind=[],this.options=l,this.chart=s,this.runChartClick=!!l.chart.events?.click,this.pinchDown=[],this.setDOMEvents(),Dt(this,"afterInit")}normalize(s,l){let c=s.touches,d=c?c.length?c.item(0):Mn(c.changedTouches,s.changedTouches)[0]:s;l||(l=this.getChartPosition());let h=d.pageX-l.left,p=d.pageY-l.top;return K(s,{chartX:Math.round(h/=l.scaleX),chartY:Math.round(p/=l.scaleY)})}onContainerClick(s){let l=this.chart,c=l.hoverPoint,d=this.normalize(s),h=l.plotLeft,p=l.plotTop;!l.cancelClick&&(c&&this.inClass(d.target,"highcharts-tracker")?(Dt(c.series,"click",K(d,{point:c})),l.hoverPoint&&c.firePointEvent("click",d)):(K(d,this.getCoordinates(d)),l.isInsidePlot(d.chartX-h,d.chartY-p,{visiblePlotOnly:!0})&&Dt(l,"click",d)))}onContainerMouseDown(s){let l=!(1&~(s.buttons||s.button));s=this.normalize(s),Q.isFirefox&&0!==s.button&&this.onContainerMouseMove(s),(void 0===s.button||l)&&(this.zoomOption(s),l&&s.preventDefault?.(),this.dragStart(s))}onContainerMouseLeave(s){let{pointer:l}=Wc[Mn(cn.hoverChartIndex,-1)]||{};s=this.normalize(s),this.onContainerMouseMove(s),l&&!this.inClass(s.relatedTarget,"highcharts-tooltip")&&(l.reset(),l.chartPosition=void 0)}onContainerMouseEnter(){delete this.chartPosition}onContainerMouseMove(s){let l=this.chart,c=l.tooltip,d=this.normalize(s);this.setHoverChartIndex(s),("mousedown"===l.mouseIsDown||this.touchSelect(d))&&this.drag(d),!l.exporting?.openMenu&&(this.inClass(d.target,"highcharts-tracker")||l.isInsidePlot(d.chartX-l.plotLeft,d.chartY-l.plotTop,{visiblePlotOnly:!0}))&&!c?.shouldStickOnContact(d)&&(this.inClass(d.target,"highcharts-no-tooltip")?this.reset(!1,0):this.runPointActions(d))}onDocumentTouchEnd(s){this.onDocumentMouseUp(s)}onContainerTouchMove(s){this.touchSelect(s)?this.onContainerMouseMove(s):this.touch(s)}onContainerTouchStart(s){this.touchSelect(s)?this.onContainerMouseDown(s):(this.zoomOption(s),this.touch(s,!0))}onDocumentMouseMove(s){let l=this.chart,c=l.tooltip,d=this.chartPosition,h=this.normalize(s,d);!d||l.isInsidePlot(h.chartX-l.plotLeft,h.chartY-l.plotTop,{visiblePlotOnly:!0})||c?.shouldStickOnContact(h)||h.target!==l.container.ownerDocument&&this.inClass(h.target,"highcharts-tracker")||this.reset()}onDocumentMouseUp(s){s?.touches&&this.hasPinchMoved&&s?.preventDefault?.(),Wc[Mn(cn.hoverChartIndex,-1)]?.pointer?.drop(s)}pinch(s){let l=this,{chart:c,hasZoom:d,lastTouches:h}=l,p=[].map.call(s.touches||[],_=>l.normalize(_)),g=p.length,m=1===g&&(l.inClass(s.target,"highcharts-tracker")&&c.runTrackerClick||l.runChartClick),b=c.tooltip,C=1===g&&Mn(b?.options.followTouchMove,!0);g>1?l.initiated=!0:C&&(l.initiated=!1),d&&l.initiated&&!m&&!1!==s.cancelable&&s.preventDefault(),"touchstart"===s.type?(l.pinchDown=p,l.res=!0,c.mouseDownX=s.chartX):C?this.runPointActions(l.normalize(s)):h&&(Dt(c,"touchpan",{originalEvent:s,touches:p},()=>{let _=E=>{let x=E[0],S=E[1]||x;return{x:x.chartX,y:x.chartY,width:S.chartX-x.chartX,height:S.chartY-x.chartY}};c.transform({axes:c.axes.filter(E=>E.zoomEnabled&&(this.zoomHor&&E.horiz||this.zoomVert&&!E.horiz)),to:_(p),from:_(h),trigger:s.type})}),l.res&&(l.res=!1,this.reset(!1,0))),l.lastTouches=p}reset(s,l){let c=this.chart,d=c.hoverSeries,h=c.hoverPoint,p=c.hoverPoints,g=c.tooltip,m=g?.shared?p:h;s&&m&&ph(m).forEach(function(b){b.series.isCartesian&&void 0===b.plotX&&(s=!1)}),s?g&&m&&ph(m).length&&(g.refresh(m),g.shared&&p?p.forEach(function(b){b.setState(b.state,!0),b.series.isCartesian&&(b.series.xAxis.crosshair&&b.series.xAxis.drawCrosshair(null,b),b.series.yAxis.crosshair&&b.series.yAxis.drawCrosshair(null,b))}):h&&(h.setState(h.state,!0),c.axes.forEach(function(b){b.crosshair&&h.series[b.coll]===b&&b.drawCrosshair(null,h)}))):(h&&h.onMouseOut(),p&&p.forEach(function(b){b.setState()}),d&&d.onMouseOut(),g&&g.hide(l),this.unDocMouseMove&&(this.unDocMouseMove=this.unDocMouseMove()),c.axes.forEach(function(b){b.hideCrosshair()}),c.hoverPoints=c.hoverPoint=void 0)}runPointActions(s,l,c){let d=this.chart,h=d.series,p=d.tooltip?.options.enabled?d.tooltip:void 0,g=!!p&&p.shared,m=l||d.hoverPoint,b=m?.series||d.hoverSeries,_=this.getHoverData(m,b,h,(!s||"touchmove"!==s.type)&&(!!l||b?.directTouch&&this.isDirectTouch),g,s);m=_.hoverPoint,b=_.hoverSeries;let E=_.hoverPoints,x=b?.tooltipOptions.followPointer&&!b.tooltipOptions.split,S=g&&b&&!b.noSharedTooltip;if(m&&(c||m!==d.hoverPoint||p?.isHidden)){if((d.hoverPoints||[]).forEach(function(M){-1===E.indexOf(M)&&M.setState()}),d.hoverSeries!==b&&b.onMouseOver(),this.applyInactiveState(E),(E||[]).forEach(function(M){M.setState("hover")}),d.hoverPoint&&d.hoverPoint.firePointEvent("mouseOut"),!m.series)return;d.hoverPoints=E,d.hoverPoint=m,m.firePointEvent("mouseOver",void 0,()=>{p&&m&&p.refresh(S?E:m,s)})}else if(x&&p&&!p.isHidden){let M=p.getAnchor([{}],s);d.isInsidePlot(M[0],M[1],{visiblePlotOnly:!0})&&p.updatePosition({plotX:M[0],plotY:M[1]})}this.unDocMouseMove||(this.unDocMouseMove=ni(d.container.ownerDocument,"mousemove",M=>Wc[cn.hoverChartIndex??-1]?.pointer?.onDocumentMouseMove(M)),this.eventsToUnbind.push(this.unDocMouseMove)),d.axes.forEach(function(M){let I,T=M.crosshair?.snap??!0;T&&((I=d.hoverPoint)&&I.series[M.coll]===M||(I=Ve(E,O=>O.series?.[M.coll]===M))),I||!T?M.drawCrosshair(s,I):M.hideCrosshair()})}setDOMEvents(){let s=this.chart.container,l=s.ownerDocument;s.onmousedown=this.onContainerMouseDown.bind(this),s.onmousemove=this.onContainerMouseMove.bind(this),s.onclick=this.onContainerClick.bind(this),this.eventsToUnbind.push(ni(s,"mouseenter",this.onContainerMouseEnter.bind(this)),ni(s,"mouseleave",this.onContainerMouseLeave.bind(this))),cn.unbindDocumentMouseUp.some(d=>d.doc===l)||cn.unbindDocumentMouseUp.push({doc:l,unbind:ni(l,"mouseup",this.onDocumentMouseUp.bind(this))});let c=this.chart.renderTo.parentElement;for(;c&&"BODY"!==c.tagName;)this.eventsToUnbind.push(ni(c,"scroll",()=>{delete this.chartPosition})),c=c.parentElement;this.eventsToUnbind.push(ni(s,"touchstart",this.onContainerTouchStart.bind(this),{passive:!1}),ni(s,"touchmove",this.onContainerTouchMove.bind(this),{passive:!1})),cn.unbindDocumentTouchEnd||(cn.unbindDocumentTouchEnd=ni(l,"touchend",this.onDocumentTouchEnd.bind(this),{passive:!1})),this.setPointerCapture(),ni(this.chart,"redraw",this.setPointerCapture.bind(this))}setPointerCapture(){if(!db)return;let s=this.pointerCaptureEventsToUnbind,l=this.chart,c=l.container,d=Mn(l.options.tooltip?.followTouchMove,!0)&&l.series.some(h=>h.options.findNearestPointBy.indexOf("y")>-1);!this.hasPointerCapture&&d?(s.push(ni(c,"pointerdown",h=>{h.target?.hasPointerCapture(h.pointerId)&&h.target?.releasePointerCapture(h.pointerId)}),ni(c,"pointermove",h=>{l.pointer?.getPointFromEvent(h)?.onMouseOver(h)})),l.styledMode||Gc(c,{"touch-action":"none"}),c.className+=" highcharts-no-touch-action",this.hasPointerCapture=!0):this.hasPointerCapture&&!d&&(s.forEach(h=>h()),s.length=0,l.styledMode||Gc(c,{"touch-action":Mn(l.options.chart.style?.["touch-action"],"manipulation")}),c.className=c.className.replace(" highcharts-no-touch-action",""),this.hasPointerCapture=!1)}setHoverChartIndex(s){let l=this.chart,c=Q.charts[Mn(cn.hoverChartIndex,-1)];if(c&&c!==l){let d={relatedTarget:l.container};s&&!s?.relatedTarget&&Object.assign({},s,d),c.pointer?.onContainerMouseLeave(s||d)}c?.mouseIsDown||(cn.hoverChartIndex=l.index)}touch(s,l){let c,{chart:d,pinchDown:h=[]}=this;this.setHoverChartIndex(),1===(s=this.normalize(s)).touches.length?d.isInsidePlot(s.chartX-d.plotLeft,s.chartY-d.plotTop,{visiblePlotOnly:!0})&&!d.exporting?.openMenu?(l&&this.runPointActions(s),"touchmove"===s.type&&(this.hasPinchMoved=c=!!h[0]&&Math.pow(h[0].chartX-s.chartX,2)+Math.pow(h[0].chartY-s.chartY,2)>=16),Mn(c,!0)&&this.pinch(s)):l&&this.reset():2===s.touches.length&&this.pinch(s)}touchSelect(s){return!(!this.chart.zooming.singleTouch||!s.touches||1!==s.touches.length)}zoomOption(s){let h,p,l=this.chart,c=l.inverted,d=l.zooming.type||"";/touch/.test(s.type)&&(d=Mn(l.zooming.pinchType,d)),this.zoomX=h=/x/.test(d),this.zoomY=p=/y/.test(d),this.zoomHor=h&&!c||p&&c,this.zoomVert=p&&!c||h&&c,this.hasZoom=h||p}}cn.unbindDocumentMouseUp=[],function(v){v.compose=function(s){Mg(xg,"Core.Pointer")&&ni(s,"beforeRender",function(){this.pointer=new v(this,this.options)})}}(cn||(cn={}));let fb=cn;!function(v){v.setLength=function(s,l,c){return Array.isArray(s)?(s.length=l,s):s[c?"subarray":"slice"](0,l)},v.splice=function(s,l,c,d,h=[]){if(Array.isArray(s))return Array.isArray(h)||(h=Array.from(h)),{removed:s.splice(l,c,...h),array:s};let p=Object.getPrototypeOf(s).constructor,g=s[d?"subarray":"slice"](l,l+c),m=new p(s.length-c+h.length);return m.set(s.subarray(0,l),0),m.set(h,l),m.set(s.subarray(l+c),l+h.length),{removed:g,array:m}}}(Qt||(Qt={}));let{setLength:Zx,splice:gh}=Qt,{fireEvent:qc,objectEach:Fo,uniqueKey:pn}=we,Fr=class{constructor(v={}){this.autoId=!v.id,this.columns={},this.id=v.id||pn(),this.modified=this,this.rowCount=0,this.versionTag=pn();let s=0;Fo(v.columns||{},(l,c)=>{this.columns[c]=l.slice(),s=Math.max(s,l.length)}),this.applyRowCount(s)}applyRowCount(v){this.rowCount=v,Fo(this.columns,(s,l)=>{s.length!==v&&(this.columns[l]=Zx(s,v))})}deleteRows(v,s=1){if(s>0&&v{this.columns[d]=gh(c,v,s).array,l=c.length}),this.rowCount=l}qc(this,"afterDeleteRows",{rowIndex:v,rowCount:s}),this.versionTag=pn()}getColumn(v,s){return this.columns[v]}getColumns(v,s){return(v||Object.keys(this.columns)).reduce((l,c)=>(l[c]=this.columns[c],l),{})}getRow(v,s){return(s||Object.keys(this.columns)).map(l=>this.columns[l]?.[v])}setColumn(v,s=[],l=0,c){this.setColumns({[v]:s},l,c)}setColumns(v,s,l){let c=this.rowCount;Fo(v,(d,h)=>{this.columns[h]=d.slice(),c=d.length}),this.applyRowCount(c),l?.silent||(qc(this,"afterSetColumns"),this.versionTag=pn())}setRow(v,s=this.rowCount,l,c){let{columns:d}=this,h=l?this.rowCount+1:s+1;Fo(v,(p,g)=>{let m=d[g]||!1!==c?.addColumns&&Array(h);m&&(l?m=gh(m,s,0,!0,[p]).array:m[s]=p,d[g]=m)}),h>this.rowCount&&this.applyRowCount(h),c?.silent||(qc(this,"afterSetRows"),this.versionTag=pn())}},{extend:Tg,merge:fr,pick:Mi}=we;!function(v){function s(l,c,d){let T,h=this.legendItem=this.legendItem||{},{chart:p,options:g}=this,{baseline:m=0,symbolWidth:b,symbolHeight:C}=l,_=this.symbol||"circle",E=C/2,x=p.renderer,S=h.group,M=m-Math.round((l.fontMetrics?.b||C)*(d?.4:.3)),I={},O=g.marker,R=0;if(p.styledMode||(I["stroke-width"]=Math.min(g.lineWidth||0,24),g.dashStyle?I.dashstyle=g.dashStyle:"square"!==g.linecap&&(I["stroke-linecap"]="round")),h.line=x.path().addClass("highcharts-graph").attr(I).add(S),d&&(h.area=x.path().addClass("highcharts-area").add(S)),I["stroke-linecap"]&&(R=Math.min(h.line.strokeWidth(),b)/2),b){let P=[["M",R,M],["L",b-R,M]];h.line.attr({d:P}),h.area?.attr({d:[...P,["L",b-R,m],["L",R,m]]})}if(O&&!1!==O.enabled&&b){let P=Math.min(Mi(O.radius,E),E);0===_.indexOf("url")&&(O=fr(O,{width:C,height:C}),P=0),h.symbol=T=x.symbol(_,b/2-P,M-P,2*P,2*P,Tg({context:"legend"},O)).addClass("highcharts-point").add(S),T.isMarker=!0}}v.areaMarker=function(l,c){s.call(this,l,c,!0)},v.lineMarker=s,v.rectangle=function(l,c){let d=c.legendItem||{},p=l.symbolHeight,g=l.options.squareSymbol;d.symbol=this.chart.renderer.rect(g?(l.symbolWidth-p)/2:0,l.baseline-p+1,g?p:l.symbolWidth,p,Mi(l.options.symbolRadius,p/2)).addClass("highcharts-point").attr({zIndex:3}).add(d.group)}}(rt||(rt={}));let Ig=rt,{defaultOptions:mh}=cr,{extend:pb,extendClass:gb,merge:yh}=we;!function(v){function s(l,c){let d=mh.plotOptions||{},h=c.defaultOptions,p=c.prototype;return p.type=l,p.pointClass||(p.pointClass=so),!v.seriesTypes[l]&&(h&&(d[l]=h),v.seriesTypes[l]=c,!0)}v.seriesTypes=Q.seriesTypes,v.registerSeriesType=s,v.seriesType=function(l,c,d,h,p){let g=mh.plotOptions||{};if(g[l]=yh(g[c=c||""],d),delete v.seriesTypes[l],s(l,gb(v.seriesTypes[c]||Er,h)),v.seriesTypes[l].prototype.type=l,p){class m extends so{}pb(m.prototype,p),v.seriesTypes[l].prototype.pointClass=m}return v.seriesTypes[l]}}(ye||(ye={}));let Bt=ye,{animObject:Yc,setAnimation:Xc}=G,{defaultOptions:Zc}=cr,{registerEventOptions:Ag}=ia,{svg:vh,win:kg}=Q,{seriesTypes:_s}=Bt,{format:Og}=Qr,{arrayMax:Kc,arrayMin:Qc,clamp:Rg,correctFloat:Tn,crisp:ws,defined:gt,destroyObjectProperties:mb,diffObjects:yb,erase:Ng,error:Jc,extend:ha,find:Pg,fireEvent:xt,getClosestDistance:bl,getNestedProperty:bh,insertItem:_h,isArray:Gn,isNumber:ut,isString:Cr,merge:fa,objectEach:wh,pick:dt,removeEvent:ri,syncTimeout:Ch}=we;class ii{constructor(){this.zoneAxis="y"}init(s,l){let c;xt(this,"init",{options:l}),this.dataTable??(this.dataTable=new Fr);let d=s.series;this.eventsToUnbind=[],this.chart=s,this.options=this.setOptions(l);let h=this.options,p=!1!==h.visible;this.linkedSeries=[],this.bindAxes(),ha(this,{name:h.name,state:"",visible:p,selected:!0===h.selected}),Ag(this,h),(h.events?.click||h.point?.events?.click||h.allowPointSelect)&&(s.runTrackerClick=!0),this.getColor(),this.getSymbol(),this.isCartesian&&(s.hasCartesianSeries=!0),d.length&&(c=d[d.length-1]),this._i=dt(c?._i,-1)+1,this.opacity=this.options.opacity,s.orderItems("series",_h(this,d)),h.dataSorting?.enabled?this.setDataSortingOptions():this.points||this.data||this.setData(h.data,!1),xt(this,"afterInit")}is(s){return _s[s]&&this instanceof _s[s]}bindAxes(){let s,l=this,c=l.options,d=l.chart;xt(this,"bindAxes",null,function(){(l.axisTypes||[]).forEach(function(h){(d[h]||[]).forEach(function(p){s=p.options,(dt(c[h],0)===p.index||void 0!==c[h]&&c[h]===s.id)&&(_h(l,p.series),l[h]=p,p.isDirty=!0)}),l[h]||l.optionalAxis===h||Jc(18,!0,d)})}),xt(this,"afterBindAxes")}hasData(){return this.visible&&void 0!==this.dataMax&&void 0!==this.dataMin||this.visible&&this.dataTable.rowCount>0}hasMarkerChanged(s,l){let c=s.marker,d=l.marker||{};return c&&(d.enabled&&!c.enabled||d.symbol!==c.symbol||d.height!==c.height||d.width!==c.width)}autoIncrement(s){let l,c=this.options,{pointIntervalUnit:d,relativeXValue:h}=this.options,p=this.chart.time,g=this.xIncrement??p.parse(c.pointStart)??0;if(this.pointInterval=l=dt(this.pointInterval,c.pointInterval,1),h&&ut(s)&&(l*=s),d){let m=p.toParts(g);"day"===d?m[2]+=l:"month"===d?m[1]+=l:"year"===d&&(m[0]+=l),l=p.makeTime.apply(p,m)-g}return h&&ut(s)?g+l:(this.xIncrement=g+l,g)}setDataSortingOptions(){let s=this.options;ha(this,{requireSorting:!1,sorted:!1,enabledDataSorting:!0,allowDG:!1}),gt(s.pointRange)||(s.pointRange=1)}setOptions(s){let l,c=this.chart,d=c.options.plotOptions,h=c.userOptions||{},p=fa(s),g=c.styledMode,m={plotOptions:d,userOptions:p};xt(this,"setOptions",m);let b=m.plotOptions[this.type],C=h.plotOptions||{},_=C.series||{},E=Zc.plotOptions[this.type]||{},x=C[this.type]||{};b.dataLabels=this.mergeArrays(E.dataLabels,b.dataLabels),this.userOptions=m.userOptions;let S=fa(b,d.series,x,p);this.tooltipOptions=fa(Zc.tooltip,Zc.plotOptions.series?.tooltip,E?.tooltip,c.userOptions.tooltip,C.series?.tooltip,x.tooltip,p.tooltip),this.stickyTracking=dt(p.stickyTracking,x.stickyTracking,_.stickyTracking,!!this.tooltipOptions.shared&&!this.noSharedTooltip||S.stickyTracking),null===b.marker&&delete S.marker,this.zoneAxis=S.zoneAxis||"y";let M=this.zones=(S.zones||[]).map(I=>({...I}));return(S.negativeColor||S.negativeFillColor)&&!S.zones&&(l={value:S[this.zoneAxis+"Threshold"]||S.threshold||0,className:"highcharts-negative"},g||(l.color=S.negativeColor,l.fillColor=S.negativeFillColor),M.push(l)),M.length&>(M[M.length-1].value)&&M.push(g?{}:{color:this.color,fillColor:this.fillColor}),xt(this,"afterSetOptions",{options:S}),S}getName(){return this.options.name??Og(this.chart.options.lang.seriesName,this,this.chart)}getCyclic(s,l,c){let d,h,p=this.chart,g=`${s}Index`,m=`${s}Counter`,b=c?.length||p.options.chart.colorCount;!l&&(gt(h=dt("color"===s?this.options.colorIndex:void 0,this[g]))?d=h:(p.series.length||(p[m]=0),d=p[m]%b,p[m]+=1),c&&(l=c[d])),void 0!==d&&(this[g]=d),this[s]=l}getColor(){this.chart.styledMode?this.getCyclic("color"):this.options.colorByPoint?this.color="#cccccc":this.getCyclic("color",this.options.color||Zc.plotOptions[this.type].color,this.chart.options.colors)}getPointsCollection(){return(this.hasGroupedData?this.points:this.data)||[]}getSymbol(){this.getCyclic("symbol",this.options.marker.symbol,this.chart.options.symbols)}getColumn(s,l){return(l?this.dataTable.modified:this.dataTable).getColumn(s,!0)||[]}findPointIndex(s,l){let c,d,h,{id:p,x:g}=s,m=this.points,b=this.options.dataSorting,C=this.cropStart||0;if(p){let _=this.chart.get(p);_ instanceof so&&(c=_)}else if(this.linkedParent||this.enabledDataSorting||this.options.relativeXValue){let _=E=>!E.touched&&E.index===s.index;if(b?.matchByName?_=E=>!E.touched&&E.name===s.name:this.options.relativeXValue&&(_=E=>!E.touched&&E.options.x===s.x),!(c=Pg(m,_)))return}return c&&void 0!==(h=c?.index)&&(d=!0),void 0===h&&ut(g)&&(h=this.getColumn("x").indexOf(g,l)),-1!==h&&void 0!==h&&this.cropped&&(h=h>=C?h-C:h),!d&&ut(h)&&m[h]?.touched&&(h=void 0),h}updateData(s,l){let b,C,_,E,{options:c,requireSorting:d}=this,h=c.dataSorting,p=this.points,g=[],m=s.length===p.length,x=!0;if(this.xIncrement=null,s.forEach((M,I)=>{let T,O=gt(M)&&this.pointClass.prototype.optionsToObject.call({series:this},M)||{},{id:R,x:P}=O;R||ut(P)?(-1===(T=this.findPointIndex(O,E))||void 0===T?g.push(M):p[T]&&M!==c.data?.[T]?(p[T].update(M,!1,void 0,!1),p[T].touched=!0,d&&(E=T+1)):p[T]&&(p[T].touched=!0),(!m||I!==T||h?.enabled||this.hasDerivedData)&&(b=!0)):g.push(M)},this),b)for(C=p.length;C--;)(_=p[C])&&!_.touched&&_.remove?.(!1,l);else m&&!h?.enabled?(s.forEach((M,I)=>{M===p[I].y||p[I].destroyed||p[I].update(M,!1,void 0,!1)}),g.length=0):x=!1;if(p.forEach(M=>{M&&(M.touched=!1)}),!x)return!1;g.forEach(M=>{this.addPoint(M,!1,void 0,void 0,!1)},this);let S=this.getColumn("x");return null===this.xIncrement&&S.length&&(this.xIncrement=Kc(S),this.autoIncrement()),!0}dataColumnKeys(){return["x",...this.pointArrayMap||["y"]]}setData(s,l=!0,c,d){let T,O,U,h=this.points,p=h?.length||0,g=this.options,m=this.chart,b=g.dataSorting,C=this.xAxis,_=g.turboThreshold,E=this.dataTable,x=this.dataColumnKeys(),S=this.pointValKey||"y",M=(this.pointArrayMap||[]).length,I=g.keys,R=0,P=1;m.options.chart.allowMutatingData||(g.data&&delete this.options.data,this.userOptions.data&&delete this.userOptions.data,U=fa(!0,s));let $=(s=U||s||[]).length;if(b?.enabled&&(s=this.sortData(s)),m.options.chart.allowMutatingData&&!1!==d&&$&&p&&!this.cropped&&!this.hasGroupedData&&this.visible&&!this.boosted&&(O=this.updateData(s,c)),!O){this.xIncrement=null,this.colorCounter=0;let F=_&&!g.relativeXValue&&$>_;if(F){let j=this.getFirstValidPoint(s),q=this.getFirstValidPoint(s,$-1,-1),Z=X=>!(!Gn(X)||!I&&!ut(X[0]));if(ut(j)&&ut(q)){let X=[],ue=[];for(let me of s)X.push(this.autoIncrement()),ue.push(me);E.setColumns({x:X,[S]:ue})}else if(Z(j)&&Z(q))if(M){let X=+(j.length===M),ue=Array(x.length).fill(0).map(()=>[]);for(let me of s){X&&ue[0].push(this.autoIncrement());for(let ge=X;ge<=M;ge++)ue[ge]?.push(me[ge-X])}E.setColumns(x.reduce((me,ge,le)=>(me[ge]=ue[le],me),{}))}else{I&&(R=I.indexOf("x"),P=I.indexOf("y"),R=R>=0?R:0,P=P>=0?P:1),1===j.length&&(P=0);let X=[],ue=[];if(R===P)for(let me of s)X.push(this.autoIncrement()),ue.push(me[P]);else for(let me of s)X.push(me[R]),ue.push(me[P]);E.setColumns({x:X,[S]:ue})}else F=!1}if(!F){let j=x.reduce((q,Z)=>(q[Z]=[],q),{});for(T=0;T<$;T++){let q=this.pointClass.prototype.applyOptions.apply({series:this},[s[T]]);for(let Z of x)j[Z][T]=q[Z]}E.setColumns(j)}for(Cr(this.getColumn("y")[0])&&Jc(14,!0,m),this.data=[],this.options.data=this.userOptions.data=s,T=p;T--;)h[T]?.destroy();C&&(C.minRange=C.userMinRange),this.isDirty=m.isDirtyBox=!0,this.isDirtyData=!!h,c=!1}"point"===g.legendType&&(this.processData(),this.generatePoints()),l&&m.redraw(c)}sortData(s){let l=this,c=l.options.dataSorting.sortKey||"y",d=function(h,p){return gt(p)&&h.pointClass.prototype.optionsToObject.call({series:h},p)||{}};return s.forEach(function(h,p){s[p]=d(l,h),s[p].index=p},this),s.concat().sort((h,p)=>{let g=bh(c,h),m=bh(c,p);return mg)}).forEach(function(h,p){h.x=p},this),l.linkedSeries&&l.linkedSeries.forEach(function(h){let p=h.options,g=p.data;!p.dataSorting?.enabled&&g&&(g.forEach(function(m,b){g[b]=d(h,m),s[b]&&(g[b].x=s[b].x,g[b].index=b)}),h.setData(g,!1))}),s}getProcessedData(s){let _,E,S,M,I,l=this,{dataTable:c,isCartesian:d,options:h,xAxis:p}=l,g=h.cropThreshold,m=s||l.getExtremesFromAll,b=p?.logarithmic,C=c.rowCount,x=0,T=l.getColumn("x"),O=c,R=!1;return p&&(M=(S=p.getExtremes()).min,I=S.max,R=!(!p.categories||p.names.length),d&&l.sorted&&!m&&(!g||C>g||l.forceCrop)&&(T[C-1]I?O=new Fr:l.getColumn(l.pointValKey||"y").length&&(T[0]I)&&(O=(_=this.cropData(c,M,I)).modified,x=_.start,E=!0))),T=O.getColumn("x")||[],{modified:O,cropped:E,cropStart:x,closestPointRange:bl([b?T.map(b.log2lin):T],()=>l.requireSorting&&!R&&Jc(15,!1,l.chart))}}processData(s){let c=this.dataTable;if(this.isCartesian&&!this.isDirty&&!this.xAxis.isDirty&&!this.yAxis.isDirty&&!s)return!1;let d=this.getProcessedData();c.modified=d.modified,this.cropped=d.cropped,this.cropStart=d.cropStart,this.closestPointRange=this.basePointRange=d.closestPointRange,xt(this,"afterProcessData")}cropData(s,l,c){let g,m,d=s.getColumn("x",!0)||[],h=d.length,p={},b=0,C=h;for(g=0;g=l){b=Math.max(0,g-1);break}for(m=g;mc){C=m+1;break}for(let _ of this.dataColumnKeys()){let E=s.getColumn(_,!0);E&&(p[_]=E.slice(b,C))}return{modified:new Fr({columns:p}),start:b,end:C}}generatePoints(){let M,I,T,O,P,s=this.options,l=this.processedData||s.data,c=this.dataTable.modified,d=this.getColumn("x",!0),h=this.pointClass,p=c.rowCount,g=this.cropStart||0,m=this.hasGroupedData,b=s.keys,C=[],_=s.dataGrouping?.groupAll?g:0,E=this.xAxis?.categories,x=this.pointArrayMap||["y"],S=this.dataColumnKeys(),R=this.data;if(!R&&!m){let U=[];U.length=l?.length||0,R=this.data=U}for(b&&m&&(this.options.keys=!1),O=0;Op.getColumn(U,!0)||[])||[],C=this.getColumn("x",!0),_=[],E=this.requireSorting&&!this.is("column")?1:0,x=!!d&&d.positiveValuesOnly,S=h||this.cropped||!c,O=0,R=0;for(c&&(O=(M=c.getExtremes()).min,R=M.max),T=0;T=O&&(C[T-E]||I)<=R)for(let U of b){let $=U[T];ut($)&&($>0||!x)&&_.push($)}let P={activeYData:_,dataMin:Qc(_),dataMax:Kc(_)};return xt(this,"afterGetExtremes",{dataExtremes:P}),P}applyExtremes(){let s=this.getExtremes();return this.dataMin=s.dataMin,this.dataMax=s.dataMax,s}getFirstValidPoint(s,l=0,c=1){let d=s.length,h=l;for(;h>=0&&h1)&&(p.step=function(E,x){_&&_.apply(x,arguments),"width"===x.prop&&b?.element&&b.attr(h?"height":"width",E+99)}),m.addClass("highcharts-animating").animate(C,p)}}afterAnimate(){this.setClip(),wh(this.chart.sharedClips,(s,l,c)=>{s&&!this.chart.container.querySelector(`[clip-path="url(#${s.id})"]`)&&(s.destroy(),delete c[l])}),this.finishedAnimating=!0,xt(this,"afterAnimate")}drawPoints(s=this.points){let l,c,d,h,p,g,m,b=this.chart,C=b.styledMode,{colorAxis:_,options:E}=this,x=E.marker,S=E.nullInteraction,M=this[this.specialGroup||"markerGroup"],I=this.xAxis,T=dt(x.enabled,!I||!!I.isRadial||null,this.closestPointRangePx>=x.enabledThreshold*x.radius);if(!1!==x.enabled||this._hasPointMarkers)for(l=0;l0||c.hasImage)&&(c.graphic=d=b.renderer.symbol(R,m.x,m.y,m.width,m.height,g?p:x).add(M),this.enabledDataSorting&&b.hasRendered&&(d.attr({x:c.startXPos}),h="animate")),d&&"animate"===h&&d[P?"show":"hide"](P).animate(m),d){let U=this.pointAttribs(c,C||!c.selected?void 0:"select");C?_&&d.css({fill:U.fill}):d[h](U)}d&&d.addClass(c.getClassName(),!0)}}}markerAttribs(s,l){let m,b,c=this.options,d=c.marker,h=s.marker||{},p=h.symbol||d.symbol,g={},C=dt(h.radius,d?.radius);l&&(m=d.states[l],b=h.states&&h.states[l],C=dt(b?.radius,m?.radius,C&&C+(m?.radiusPlus||0))),s.hasImage=p&&0===p.indexOf("url"),s.hasImage&&(C=0);let _=s.pos();return ut(C)&&_&&(c.crisp&&(_[0]=ws(_[0],s.hasImage?0:"rect"===p?d?.lineWidth||0:1)),g.x=_[0]-C,g.y=_[1]-C),C&&(g.width=g.height=2*C),g}pointAttribs(s,l){let C,_,x,S,c=this.options,d=c.marker,h=s?.options,p=h?.marker||{},g=h?.color,m=s?.color,b=s?.zone?.color,E=this.color,M=dt(p.lineWidth,d.lineWidth),I=s?.isNull&&c.nullInteraction?0:1;return E=g||b||m||E,x=p.fillColor||d.fillColor||E,S=p.lineColor||d.lineColor||E,C=d.states[l=l||"normal"]||{},M=dt((_=p.states&&p.states[l]||{}).lineWidth,C.lineWidth,M+dt(_.lineWidthPlus,C.lineWidthPlus,0)),x=_.fillColor||C.fillColor||x,S=_.lineColor||C.lineColor||S,{stroke:S,"stroke-width":M,fill:x,opacity:I=dt(_.opacity,C.opacity,I)}}destroy(s){let l,c,d=this,h=d.chart,p=/AppleWebKit\/533/.test(kg.navigator.userAgent),g=d.data||[];for(xt(d,"destroy",{keepEventsForUpdate:s}),this.removeEvents(s),(d.axisTypes||[]).forEach(function(m){c=d[m],c?.series&&(Ng(c.series,d),c.isDirty=c.forceRedraw=!0)}),d.legendItem&&d.chart.legend.destroyItem(d),l=g.length;l--;)g[l]?.destroy?.();for(let m of d.zones)mb(m,void 0,!0);we.clearTimeout(d.animationTimeout),wh(d,function(m,b){m instanceof wi&&!m.survive&&m[p&&"group"===b?"hide":"destroy"]()}),h.hoverSeries===d&&(h.hoverSeries=void 0),Ng(h.series,d),h.orderItems("series"),wh(d,function(m,b){s&&"hcEvents"===b||delete d[b]})}applyZones(){let{area:s,chart:l,graph:c,zones:d,points:h,xAxis:p,yAxis:g,zoneAxis:m}=this,{inverted:b,renderer:C}=l,_=this[`${m}Axis`],{isXAxis:E,len:x=0,minPointOffset:S=0}=_||{},M=(c?.strokeWidth()||0)/2+1,I=(T,O=0,R=0)=>{b&&(R=x-R);let{translated:P=0,lineClip:U}=T,$=R-P;U?.push(["L",O,Math.abs($){U.forEach(($,F)=>{("M"===$[0]||"L"===$[0])&&(U[F]=[$[0],E?x-$[1]:$[1],E?$[2]:x-$[2]])})};if(d.forEach(U=>{U.lineClip=[],U.translated=Rg(_.toPixels(dt(U.value,T),!0)||0,0,x)}),c&&!this.showLine&&c.hide(),s&&s.hide(),"y"===m&&h.length{let $=U.lineClip||[],F=Math.round(U.translated||0);p.reversed&&$.reverse();let{clip:j,simpleClip:q}=U,Z=0,X=0,ue=p.len,me=g.len;E?(Z=F,ue=P):(X=F,me=P);let ge=[["M",Z,X],["L",ue,X],["L",ue,me],["L",Z,me],["Z"]],le=[ge[0],...$,ge[1],ge[2],...R,ge[3],ge[4]];R=$.reverse(),P=F,b&&(O(le),s&&O(ge)),j?(j.animate({d:le}),q?.animate({d:ge})):(j=U.clip=C.path(le),s&&(q=U.simpleClip=C.path(ge))),c&&U.graph?.clip(j),s&&U.area?.clip(q)})}else this.visible&&(c&&c.show(),s&&s.show())}plotGroup(s,l,c,d,h){let p=this[s],g=!p,m={visibility:c,zIndex:d||.1};return gt(this.opacity)&&!this.chart.styledMode&&"inactive"!==this.state&&(m.opacity=this.opacity),p||(this[s]=p=this.chart.renderer.g().add(h)),p.addClass("highcharts-"+l+" highcharts-series-"+this.index+" highcharts-"+this.type+"-series "+(gt(this.colorIndex)?"highcharts-color-"+this.colorIndex+" ":"")+(this.options.className||"")+(p.hasClass("highcharts-tracker")?" highcharts-tracker":""),!0),p.attr(m)[g?"attr":"animate"](this.getPlotBox(l)),p}getPlotBox(s){let l=this.xAxis,c=this.yAxis,d=this.chart,h=d.inverted&&!d.polar&&l&&this.invertible&&"series"===s;d.inverted&&(l=c,c=this.xAxis);let p={scale:1,translateX:l?l.left:d.plotLeft,translateY:c?c.top:d.plotTop,name:s};xt(this,"getPlotBox",p);let{scale:g,translateX:m,translateY:b}=p;return{translateX:m,translateY:b,rotation:90*!!h,rotationOriginX:h?g*(l.len-c.len)/2:0,rotationOriginY:h?g*(l.len+c.len)/2:0,scaleX:h?-g:g,scaleY:g}}removeEvents(s){let{eventsToUnbind:l}=this;s||ri(this),l.length&&(l.forEach(c=>{c()}),l.length=0)}render(){let s=this,{chart:l,options:c,hasRendered:d}=s,h=Yc(c.animation),p=s.visible?"inherit":"hidden",g=c.zIndex,m=l.seriesGroup,b=s.finishedAnimating?0:h.duration;xt(this,"render"),s.plotGroup("group","series",p,g,m),s.markerGroup=s.plotGroup("markerGroup","markers",p,g,m),!1!==c.clip&&s.setClip(),b&&s.animate?.(!0),s.drawGraph&&(s.drawGraph(),s.applyZones()),s.visible&&s.drawPoints(),s.drawDataLabels?.(),s.redrawPoints?.(),c.enableMouseTracking&&s.drawTracker?.(),b&&s.animate?.(),d||(b&&h.defer&&(b+=h.defer),s.animationTimeout=Ch(()=>{s.afterAnimate()},b||0)),s.isDirty=!1,s.hasRendered=!0,xt(s,"afterRender")}redraw(){let s=this.isDirty||this.isDirtyData;this.translate(),this.render(),s&&delete this.kdTree}reserveSpace(){return this.visible||!this.chart.options.chart.ignoreHiddenSeries}searchPoint(s,l){let{xAxis:c,yAxis:d}=this,h=this.chart.inverted;return this.searchKDTree({clientX:h?c.len-s.chartY+c.pos:s.chartX-c.pos,plotY:h?d.len-s.chartX+d.pos:s.chartY-d.pos},l,s)}buildKDTree(s){this.buildingKdTree=!0;let l=this,c=l.options,d=c.findNearestPointBy.indexOf("y")>-1?2:1;delete l.kdTree,Ch(function(){l.kdTree=function h(p,g,m){let b,C,_=p?.length;if(_)return b=l.kdAxisArray[g%m],p.sort((E,x)=>(E[b]||0)-(x[b]||0)),{point:p[C=Math.floor(_/2)],left:h(p.slice(0,C),g+1,m),right:h(p.slice(C+1),g+1,m)}}(l.getValidPoints(void 0,!l.directTouch,c?.nullInteraction),d,d),l.buildingKdTree=!1},c.kdNow||"touchstart"===s?.type?0:1)}searchKDTree(s,l,c,d,h){let p=this,[g,m]=this.kdAxisArray,b=l?"distX":"dist",C=(p.options.findNearestPointBy||"").indexOf("y")>-1?2:1,_=!!p.isBubble,E=d||((S,M,I)=>{let T=S[I]||0,O=M[I]||0;return[T===O&&S.index>M.index||TS=0&&p<=(d?d.len:l.plotHeight)&&h>=0&&h<=(c?c.len:l.plotWidth)}drawTracker(){let s=this,l=s.options,c=l.trackByArea,d=[].concat((c?s.areaPath:s.graphPath)||[]),h=s.chart,p=h.pointer,g=h.renderer,m=h.options.tooltip?.snap||0,b=()=>{l.enableMouseTracking&&h.hoverSeries!==s&&s.onMouseOver()},C="rgba(192,192,192,"+(vh?1e-4:.002)+")",_=s.tracker;_?_.attr({d}):s.graph&&(s.tracker=_=g.path(d).attr({visibility:s.visible?"inherit":"hidden",zIndex:2}).addClass(c?"highcharts-tracker-area":"highcharts-tracker-line").add(s.group),h.styledMode||_.attr({"stroke-linecap":"round","stroke-linejoin":"round",stroke:C,fill:c?C:"none","stroke-width":s.graph.strokeWidth()+(c?0:2*m)}),[s.tracker,s.markerGroup,s.dataLabelsGroup].forEach(E=>{E&&(E.addClass("highcharts-tracker").on("mouseover",b).on("mouseout",x=>{p?.onTrackerMouseOut(x)}),l.cursor&&!h.styledMode&&E.css({cursor:l.cursor}),E.on("touchstart",b))})),xt(this,"afterDrawTracker")}addPoint(s,l,c,d,h){let p,g,m=this.options,{chart:b,data:C,dataTable:_,xAxis:E}=this,x=E?.hasNames&&E.names,S=m.data,M=this.getColumn("x");l=dt(l,!0);let I={series:this};this.pointClass.prototype.applyOptions.apply(I,[s]);let T=I.x;if(g=M.length,this.requireSorting&&TT;)g--;_.setRow(I,g,!0,{addColumns:!1}),x&&I.name&&(x[T]=I.name),S?.splice(g,0,s),(p||this.processedData)&&(this.data.splice(g,0,null),this.processData()),"point"===m.legendType&&this.generatePoints(),c&&(C[0]&&C[0].remove?C[0].remove(!1):([C,S].filter(gt).forEach(O=>{O.shift()}),_.deleteRows(0))),!1!==h&&xt(this,"addPoint",{point:I}),this.isDirty=!0,this.isDirtyData=!0,l&&b.redraw(d)}removePoint(s,l,c){let d=this,{chart:h,data:p,points:g,dataTable:m}=d,b=p[s],C=function(){[g?.length===p.length?g:void 0,p,d.options.data].filter(gt).forEach(_=>{_.splice(s,1)}),m.deleteRows(s),b?.destroy(),d.isDirty=!0,d.isDirtyData=!0,l&&h.redraw()};Xc(c,h),l=dt(l,!0),b?b.firePointEvent("remove",null,C):C()}remove(s,l,c,d){let h=this,p=h.chart;function g(){h.destroy(d),p.isDirtyLegend=p.isDirtyBox=!0,p.linkSeries(d),dt(s,!0)&&p.redraw(l)}!1!==c?xt(h,"remove",null,g):g()}update(s,l){xt(this,"update",{options:s=yb(s,this.userOptions)});let _,E,c=this,d=c.chart,h=c.userOptions,p=c.initialType||c.type,g=d.options.plotOptions,m=_s[p].prototype,b=c.finishedAnimating&&{animation:!1},C={},x=ii.keepProps.slice(),S=s.type||h.type||d.options.chart.type,M=!(this.hasDerivedData||S&&S!==this.type||void 0!==s.keys||void 0!==s.pointStart||void 0!==s.pointInterval||void 0!==s.relativeXValue||s.joinBy||s.mapData||["dataGrouping","pointStart","pointInterval","pointIntervalUnit","keys"].some(T=>c.hasOptionChanged(T)));S=S||p,M?(x.push.apply(x,ii.keepPropsForPoints),!1!==s.visible&&x.push("area","graph"),c.parallelArrays.forEach(function(T){x.push(T+"Data")}),s.data&&(s.dataSorting&&ha(c.options.dataSorting,s.dataSorting),this.setData(s.data,!1))):this.dataTable.modified=this.dataTable,s=fa(h,{index:void 0===h.index?c.index:h.index,pointStart:g?.series?.pointStart??h.pointStart??c.getColumn("x")[0]},!M&&{data:c.options.data},s,b),M&&s.data&&(s.data=c.options.data),(x=["group","markerGroup","dataLabelsGroup","transformGroup"].concat(x)).forEach(function(T){x[T]=c[T],delete c[T]});let I=!1;if(_s[S]){if(I=S!==c.type,c.remove(!1,!1,!1,!0),I)if(d.propFromSeries(),Object.setPrototypeOf)Object.setPrototypeOf(c,_s[S].prototype);else{let T=Object.hasOwnProperty.call(c,"hcEvents")&&c.hcEvents;for(E in m)c[E]=void 0;ha(c,_s[S].prototype),T?c.hcEvents=T:delete c.hcEvents}}else Jc(17,!0,d,{missingModuleFor:S});if(x.forEach(function(T){c[T]=x[T]}),c.init(d,s),M&&this.points)for(let T of(!1===(_=c.options).visible?(C.graphic=1,C.dataLabel=1):(this.hasMarkerChanged(_,h)&&(C.graphic=1),c.hasDataLabels?.()||(C.dataLabel=1)),this.points))T?.series&&(T.resolveColor(),Object.keys(C).length&&T.destroyElements(C),!1===_.showInLegend&&T.legendItem&&d.legend.destroyItem(T));c.initialType=p,d.linkSeries(),d.setSortedData(),I&&c.linkedSeries.length&&(c.isDirtyData=!0),xt(this,"afterUpdate"),dt(l,!0)&&d.redraw(!!M&&void 0)}setName(s){this.name=this.options.name=this.userOptions.name=s,this.chart.isDirtyLegend=!0}hasOptionChanged(s){let c=this.options[s],d=this.chart.options.plotOptions,h=this.userOptions[s],p=dt(d?.[this.type]?.[s],d?.series?.[s]);return h&&!gt(p)?c!==h:c!==dt(p,c)}onMouseOver(){let s=this.chart,l=s.hoverSeries;s.pointer?.setHoverChartIndex(),l&&l!==this&&l.onMouseOut(),this.options.events.mouseOver&&xt(this,"mouseOver"),this.setState("hover"),s.hoverSeries=this}onMouseOut(){let s=this.options,l=this.chart,c=l.tooltip,d=l.hoverPoint;l.hoverSeries=null,d&&d.onMouseOut(),this&&s.events.mouseOut&&xt(this,"mouseOut"),c&&!this.stickyTracking&&(!c.shared||this.noSharedTooltip)&&c.hide(),l.series.forEach(function(h){h.setState("",!0)})}setState(s,l){let c=this,d=c.options,h=c.graph,p=d.inactiveOtherPoints,g=d.states,m=dt(g[s||"normal"]&&g[s||"normal"].animation,c.chart.options.chart.animation),b=d.lineWidth,C=d.opacity;if(c.state!==(s=s||"")&&([c.group,c.markerGroup,c.dataLabelsGroup].forEach(function(_){_&&(c.state&&_.removeClass("highcharts-series-"+c.state),s&&_.addClass("highcharts-series-"+s))}),c.state=s,!c.chart.styledMode)){if(g[s]&&!1===g[s].enabled)return;if(s&&(b=g[s].lineWidth||b+(g[s].lineWidthPlus||0),C=dt(g[s].opacity,C)),h&&!h.dashstyle&&ut(b))for(let _ of[h,...this.zones.map(E=>E.graph)])_?.animate({"stroke-width":b},m);p||[c.group,c.markerGroup,c.dataLabelsGroup,c.labelBySeries].forEach(function(_){_&&_.animate({opacity:C},m)})}l&&p&&c.points&&c.setAllPointsToState(s||void 0)}setAllPointsToState(s){this.points.forEach(function(l){l.setState&&l.setState(s)})}setVisible(s,l){let c=this,d=c.chart,h=d.options.chart.ignoreHiddenSeries;c.visible=s=c.options.visible=c.userOptions.visible=void 0===s?!c.visible:s;let g=s?"show":"hide";["group","dataLabelsGroup","markerGroup","tracker","tt"].forEach(m=>{c[m]?.[g]()}),(d.hoverSeries===c||d.hoverPoint?.series===c)&&c.onMouseOut(),c.legendItem&&d.legend.colorizeItem(c,s),c.isDirty=!0,c.options.stacking&&d.series.forEach(m=>{m.options.stacking&&m.visible&&(m.isDirty=!0)}),c.linkedSeries.forEach(m=>{m.setVisible(s,!1)}),h&&(d.isDirtyBox=!0),xt(c,g),!1!==l&&d.redraw()}show(){this.setVisible(!0)}hide(){this.setVisible(!1)}select(s){this.selected=s=this.options.selected=void 0===s?!this.selected:s,this.checkbox&&(this.checkbox.checked=s),xt(this,s?"select":"unselect")}shouldShowTooltip(s,l,c={}){return c.series=this,c.visiblePlotOnly=!0,this.chart.isInsidePlot(s,l,c)}drawLegendSymbol(s,l){Ig[this.options.legendSymbol||"rectangle"]?.call(this,s,l)}}ii.defaultOptions={lineWidth:2,allowPointSelect:!1,crisp:!0,showCheckbox:!1,animation:{duration:1e3},enableMouseTracking:!0,events:{},marker:{enabledThreshold:2,lineColor:"#ffffff",lineWidth:0,radius:4,states:{normal:{animation:!0},hover:{animation:{duration:150},enabled:!0,radiusPlus:2,lineWidthPlus:1},select:{fillColor:"#cccccc",lineColor:"#000000",lineWidth:2}}},point:{events:{}},dataLabels:{animation:{},align:"center",borderWidth:0,defer:!0,formatter:function(){let{numberFormatter:v}=this.series.chart;return"number"!=typeof this.y?"":v(this.y,-1)},padding:5,style:{fontSize:"0.7em",fontWeight:"bold",color:"contrast",textOutline:"1px contrast"},verticalAlign:"bottom",x:0,y:0},cropThreshold:300,opacity:1,pointRange:0,softThreshold:!0,states:{normal:{animation:!0},hover:{animation:{duration:150},lineWidthPlus:1,marker:{},halo:{size:10,opacity:.25}},select:{animation:{duration:0}},inactive:{animation:{duration:150},opacity:.2}},stickyTracking:!0,turboThreshold:1e3,findNearestPointBy:"x"},ii.types=Bt.seriesTypes,ii.registerType=Bt.registerSeriesType,ii.keepProps=["colorIndex","eventOptions","navigatorSeries","symbolIndex","baseSeries"],ii.keepPropsForPoints=["data","isDirtyData","isDirtyCanvas","points","dataTable","processedData","xIncrement","cropped","_hasPointMarkers","hasDataLabels","nodes","layout","level","mapMap","mapData","minY","maxY","minX","maxX","transformGroups"],ha(ii.prototype,{axisTypes:["xAxis","yAxis"],coll:"series",colorCounter:0,directTouch:!1,invertible:!0,isCartesian:!0,kdAxisArray:["clientX","plotY"],parallelArrays:["x","y"],pointClass:so,requireSorting:!0,sorted:!0}),Bt.series=ii;let Er=ii,{animObject:Lg,setAnimation:Kx}=G,{registerEventOptions:tn}=ia,{composed:eu,marginNames:Eh}=Q,{distribute:Qx}=Wi,{format:tu}=Qr,{addEvent:nu,createElement:ao,css:Dh,defined:Fg,discardElement:Bg,find:Vg,fireEvent:qn,isNumber:ru,merge:oi,pick:Br,pushUnique:vb,relativeLength:$g,stableSort:Bo,syncTimeout:bb}=we;class xh{constructor(s,l){this.allItems=[],this.initialItemY=0,this.itemHeight=0,this.itemMarginBottom=0,this.itemMarginTop=0,this.itemX=0,this.itemY=0,this.lastItemY=0,this.lastLineHeight=0,this.legendHeight=0,this.legendWidth=0,this.maxItemWidth=0,this.maxLegendWidth=0,this.offsetWidth=0,this.padding=0,this.pages=[],this.symbolHeight=0,this.symbolWidth=0,this.titleHeight=0,this.totalItemWidth=0,this.widthOption=0,this.chart=s,this.setOptions(l),l.enabled&&(this.render(),tn(this,l),nu(this.chart,"endResize",function(){this.legend.positionCheckboxes()})),nu(this.chart,"render",()=>{this.options.enabled&&this.proximate&&(this.proximatePositions(),this.positionItems())})}setOptions(s){let l=Br(s.padding,8);this.options=s,this.chart.styledMode||(this.itemStyle=s.itemStyle,this.itemHiddenStyle=oi(this.itemStyle,s.itemHiddenStyle)),this.itemMarginTop=s.itemMarginTop,this.itemMarginBottom=s.itemMarginBottom,this.padding=l,this.initialItemY=l-5,this.symbolWidth=Br(s.symbolWidth,16),this.pages=[],this.proximate="proximate"===s.layout&&!this.chart.inverted,this.baseline=void 0}update(s,l){let c=this.chart;this.setOptions(oi(!0,this.options,s)),"events"in this.options&&tn(this,this.options),this.destroy(),c.isDirtyLegend=c.isDirtyBox=!0,Br(l,!0)&&c.redraw(),qn(this,"afterUpdate",{redraw:l})}colorizeItem(s,l){let c=s.color,{area:d,group:h,label:p,line:g,symbol:m}=s.legendItem||{};if((s instanceof Er||s instanceof so)&&(s.color=s.options?.legendSymbolColor||c),h?.[l?"removeClass":"addClass"]("highcharts-legend-item-hidden"),!this.chart.styledMode){let{itemHiddenStyle:b={}}=this,C=b.color,{fillColor:_,fillOpacity:E,lineColor:x,marker:S}=s.options,M=I=>(!l&&(I.fill&&(I.fill=C),I.stroke&&(I.stroke=C)),I);p?.css(oi(l?this.itemStyle:b)),g?.attr(M({stroke:x||s.color})),m&&m.attr(M(S&&m.isMarker?s.pointAttribs():{fill:s.color})),d?.attr(M({fill:_||s.color,"fill-opacity":_?1:E??.75}))}s.color=c,qn(this,"afterColorizeItem",{item:s,visible:l})}positionItems(){this.allItems.forEach(this.positionItem,this),this.chart.isResizing||this.positionCheckboxes()}positionItem(s){let{group:l,x:c=0,y:d=0}=s.legendItem||{},h=this.options,p=h.symbolPadding,g=!h.rtl,m=s.checkbox;if(l?.element){let b={translateX:g?c:this.legendWidth-c-2*p-4,translateY:d};l[Fg(l.translateY)?"animate":"attr"](b,void 0,()=>{qn(this,"afterPositionItem",{item:s})})}m&&(m.x=c,m.y=d)}destroyItem(s){let l=s.legendItem||{};for(let c of["group","label","line","symbol"])l[c]&&(l[c]=l[c].destroy());s.checkbox=Bg(s.checkbox),s.legendItem=void 0}destroy(){for(let s of this.getAllItems())this.destroyItem(s);for(let s of["clipRect","up","down","pager","nav","box","title","group"])this[s]&&(this[s]=this[s].destroy());this.display=null}positionCheckboxes(){let s,l=this.group?.alignAttr,c=this.clipHeight||this.legendHeight,d=this.titleHeight;l&&(s=l.translateY,this.allItems.forEach(function(h){let p,g=h.checkbox;g&&(p=s+d+g.y+(this.scrollOffset||0)+3,Dh(g,{left:l.translateX+h.checkboxOffset+g.x-20+"px",top:p+"px",display:this.proximate||p>s-6&&p1.5*$?U.height:$))}layoutItem(s){let l=this.options,c=this.padding,d="horizontal"===l.layout,h=s.itemHeight,p=this.itemMarginBottom,g=this.itemMarginTop,m=d?Br(l.itemDistance,20):0,b=this.maxLegendWidth,C=l.alignColumns&&this.totalItemWidth>b?this.maxItemWidth:s.itemWidth,_=s.legendItem||{};d&&this.itemX-c+C>b&&(this.itemX=c,this.lastLineHeight&&(this.itemY+=g+this.lastLineHeight+p),this.lastLineHeight=0),this.lastItemY=g+this.itemY+p,this.lastLineHeight=Math.max(h,this.lastLineHeight),_.x=this.itemX,_.y=this.itemY,d?this.itemX+=C:(this.itemY+=g+h+p,this.lastLineHeight=h),this.offsetWidth=this.widthOption||Math.max((d?this.itemX-c-(s.checkbox?0:m):C)+c,this.offsetWidth)}getAllItems(){let s=[];return this.chart.series.forEach(function(l){let c=l?.options;l&&Br(c.showInLegend,!Fg(c.linkedTo)&&void 0,!0)&&(s=s.concat(l.legendItem?.labels||("point"===c.legendType?l.data:l)))}),qn(this,"afterGetAllItems",{allItems:s}),s}getAlignment(){let s=this.options;return this.proximate?s.align.charAt(0)+"tv":s.floating?"":s.align.charAt(0)+s.verticalAlign.charAt(0)+s.layout.charAt(0)}adjustMargins(s,l){let c=this.chart,d=this.options,h=this.getAlignment();h&&[/(lth|ct|rth)/,/(rtv|rm|rbv)/,/(rbh|cb|lbh)/,/(lbv|lm|ltv)/].forEach((p,g)=>{p.test(h)&&!Fg(s[g])&&(c[Eh[g]]=Math.max(c[Eh[g]],c.legend[(g+1)%2?"legendHeight":"legendWidth"]+[1,-1,-1,1][g]*d[g%2?"x":"y"]+(d.margin??12)+l[g]+(c.titleOffset[g]||0)))})}proximatePositions(){let s,l=this.chart,c=[],d="left"===this.options.align;for(let h of(this.allItems.forEach(function(p){let g,m,C,_,b=d;p.yAxis&&(p.xAxis.options.reversed&&(b=!b),p.points&&(g=Vg(b?p.points:p.points.slice(0).reverse(),function(E){return ru(E.plotY)})),m=this.itemMarginTop+p.legendItem.label.getBBox().height+this.itemMarginBottom,_=p.yAxis.top-l.plotTop,C=p.visible?(g?g.plotY:p.yAxis.height)+(_-.3*m):_+p.yAxis.height,c.push({target:C,size:m,item:p}))},this),Qx(c,l.plotHeight)))s=h.item.legendItem||{},ru(h.pos)&&(s.y=l.plotTop-l.spacing[0]+h.pos)}render(){let p,g,m,C,s=this.chart,l=s.renderer,c=this.options,d=this.padding,h=this.getAllItems(),b=this.group,_=this.box;this.itemX=d,this.itemY=this.initialItemY,this.offsetWidth=0,this.lastItemY=0,this.widthOption=$g(c.width,s.spacingBox.width-d),C=s.spacingBox.width-2*d-c.x,["rm","lm"].indexOf(this.getAlignment().substring(0,2))>-1&&(C/=2),this.maxLegendWidth=this.widthOption||C,b||(this.group=b=l.g("legend").addClass(c.className||"").attr({zIndex:7}).add(),this.contentGroup=l.g().attr({zIndex:1}).add(b),this.scrollGroup=l.g().add(this.contentGroup)),this.renderTitle(),Bo(h,(E,x)=>(E.options?.legendIndex||0)-(x.options?.legendIndex||0)),c.reversed&&h.reverse(),this.allItems=h,this.display=p=!!h.length,this.lastLineHeight=0,this.maxItemWidth=0,this.totalItemWidth=0,this.itemHeight=0,h.forEach(this.renderItem,this),h.forEach(this.layoutItem,this),g=(this.widthOption||this.offsetWidth)+d,m=this.lastItemY+this.lastLineHeight+this.titleHeight,m=this.handleOverflow(m)+d,_||(this.box=_=l.rect().addClass("highcharts-legend-box").attr({r:c.borderRadius}).add(b)),s.styledMode||_.attr({stroke:c.borderColor,"stroke-width":c.borderWidth||0,fill:c.backgroundColor||"none"}).shadow(c.shadow),g>0&&m>0&&_[_.placed?"animate":"attr"](_.crisp.call({},{x:0,y:0,width:g,height:m},_.strokeWidth())),b[p?"show":"hide"](),s.styledMode&&"none"===b.getStyle("display")&&(g=m=0),this.legendWidth=g,this.legendHeight=m,p&&this.align(),this.proximate||this.positionItems(),qn(this,"afterRender")}align(s=this.chart.spacingBox){let l=this.chart,c=this.options,d=s.y;/(lth|ct|rth)/.test(this.getAlignment())&&l.titleOffset[0]>0?d+=l.titleOffset[0]:/(lbh|cb|rbh)/.test(this.getAlignment())&&l.titleOffset[2]>0&&(d-=l.titleOffset[2]),d!==s.y&&(s=oi(s,{y:d})),l.hasRendered||(this.group.placed=!1),this.group.align(oi(c,{width:this.legendWidth,height:this.legendHeight,verticalAlign:this.proximate?"top":c.verticalAlign}),!0,s)}handleOverflow(s){let T,O,R,P,l=this,c=this.chart,d=c.renderer,h=this.options,p=h.y,g="top"===h.verticalAlign,m=this.padding,b=h.maxHeight,C=h.navigation,_=Br(C.animation,!0),E=C.arrowSize||12,x=this.pages,S=this.allItems,M=function(j){"number"==typeof j?F.attr({height:j}):F&&(l.clipRect=F.destroy(),l.contentGroup.clip()),l.contentGroup.div&&(l.contentGroup.div.style.clip=j?"rect("+m+"px,9999px,"+(m+j)+"px,0)":"auto")},I=function(j){return l[j]=d.circle(0,0,1.3*E).translate(E/2,E/2).add($),c.styledMode||l[j].attr("fill","rgba(0,0,0,0.0001)"),l[j]},U=c.spacingBox.height+(g?-p:p)-m,$=this.nav,F=this.clipRect;return"horizontal"!==h.layout||"middle"===h.verticalAlign||h.floating||(U/=2),b&&(U=Math.min(U,b)),x.length=0,s&&U>0&&s>U&&!1!==C.enabled?(this.clipHeight=T=Math.max(U-20-this.titleHeight-m,0),this.currentPage=Br(this.currentPage,1),this.fullHeight=s,S.forEach((j,q)=>{let Z=(R=j.legendItem||{}).y||0,X=Math.round(R.label.getBBox().height),ue=x.length;(!ue||Z-x[ue-1]>T&&(O||Z)!==x[ue-1])&&(x.push(O||Z),ue++),R.pageIx=ue-1,O&&P&&(P.pageIx=ue-1),q===S.length-1&&Z+X-x[ue-1]>T&&Z>x[ue-1]&&(x.push(Z),R.pageIx=ue),Z!==O&&(O=Z),P=R}),F||(F=l.clipRect=d.clipRect(0,m-2,9999,0),l.contentGroup.clip(F)),M(T),$||(this.nav=$=d.g().attr({zIndex:1}).add(this.group),this.up=d.symbol("triangle",0,0,E,E).add($),I("upTracker").on("click",function(){l.scroll(-1,_)}),this.pager=d.text("",15,10).addClass("highcharts-legend-navigation"),!c.styledMode&&C.style&&this.pager.css(C.style),this.pager.add($),this.down=d.symbol("triangle-down",0,0,E,E).add($),I("downTracker").on("click",function(){l.scroll(1,_)})),l.scroll(0),s=U):$&&(M(),this.nav=$.destroy(),this.scrollGroup.attr({translateY:1}),this.clipHeight=0),s}scroll(s,l){let c=this.chart,d=this.pages,h=d.length,p=this.clipHeight,g=this.options.navigation,m=this.pager,b=this.padding,C=this.currentPage+s;C>h&&(C=h),C>0&&(void 0!==l&&Kx(l,c),this.nav.attr({translateX:b,translateY:p+this.padding+7+this.titleHeight,visibility:"inherit"}),[this.up,this.upTracker].forEach(function(_){_.attr({class:1===C?"highcharts-legend-nav-inactive":"highcharts-legend-nav-active"})}),m.attr({text:C+"/"+h}),[this.down,this.downTracker].forEach(function(_){_.attr({x:18+this.pager.getBBox().width,class:C===h?"highcharts-legend-nav-inactive":"highcharts-legend-nav-active"})},this),c.styledMode||(this.up.attr({fill:1===C?g.inactiveColor:g.activeColor}),this.upTracker.css({cursor:1===C?"default":"pointer"}),this.down.attr({fill:C===h?g.inactiveColor:g.activeColor}),this.downTracker.css({cursor:C===h?"default":"pointer"})),this.scrollOffset=-d[C-1]+this.initialItemY,this.scrollGroup.animate({translateY:this.scrollOffset}),this.currentPage=C,this.positionCheckboxes(),bb(()=>{qn(this,"afterScroll",{currentPage:C})},Lg(Br(l,c.renderer.globalAnimation,!0)).duration))}setItemEvents(s,l,c){let d=this,h=s.legendItem||{},p=d.chart.renderer.boxWrapper,g=s instanceof so,m=s instanceof Er,b="highcharts-legend-"+(g?"point":"series")+"-active",C=d.chart.styledMode,_=c?[l,h.symbol]:[h.group],E=x=>{d.allItems.forEach(S=>{s!==S&&[S].concat(S.linkedSeries||[]).forEach(M=>{M.setState(x,!g)})})};for(let x of _)x&&x.on("mouseover",function(){s.visible&&E("inactive"),s.setState("hover"),s.visible&&p.addClass(b),C||l.css(d.options.itemHoverStyle)}).on("mouseout",function(){d.chart.styledMode||l.css(oi(s.visible?d.itemStyle:d.itemHiddenStyle)),E(""),p.removeClass(b),s.setState()}).on("click",function(S){p.removeClass(b),qn(d,"itemClick",{browserEvent:S,legendItem:s},function(){s.setVisible&&s.setVisible(),E(s.visible?"inactive":"")}),g?s.firePointEvent("legendItemClick",{browserEvent:S}):m&&qn(s,"legendItemClick",{browserEvent:S})})}createCheckboxForItem(s){s.checkbox=ao("input",{type:"checkbox",className:"highcharts-legend-checkbox",checked:s.selected,defaultChecked:s.selected},this.options.itemCheckboxStyle,this.chart.container),nu(s.checkbox,"click",function(l){qn(s.series||s,"checkboxClick",{checked:l.target.checked,item:s},function(){s.select()})})}}!function(v){v.compose=function(s){vb(eu,"Core.Legend")&&nu(s,"beforeMargins",function(){this.legend=new v(this,this.options.legend)})}}(xh||(xh={}));let Sh=xh,{animate:jg,animObject:_b,setAnimation:iu}=G,{defaultOptions:ou}=cr,{numberFormat:Cs}=Qr,{registerEventOptions:_l}=ia,{charts:Es,doc:wl,marginNames:pa,svg:wb,win:Hg}=Q,{seriesTypes:su}=Bt,{addEvent:Mh,attr:Th,createElement:au,css:Ti,defined:lo,diffObjects:Ug,discardElement:Jx,erase:Cb,error:zg,extend:Ds,find:Wg,fireEvent:Ke,getAlignFactor:e1,getStyle:ga,isArray:t1,isNumber:co,isObject:n1,isString:Cl,merge:Ii,objectEach:Ih,pick:gn,pInt:Gg,relativeLength:Eb,removeEvent:qg,splat:lu,syncTimeout:r1,uniqueKey:i1}=we;class Vo{static chart(s,l,c){return new Vo(s,l,c)}constructor(s,l,c){this.sharedClips={};let d=[...arguments];(Cl(s)||s.nodeName)&&(this.renderTo=d.shift()),this.init(d[0],d[1])}setZoomOptions(){let s=this.options.chart,l=s.zooming;this.zooming={...l,type:gn(s.zoomType,l.type),key:gn(s.zoomKey,l.key),pinchType:gn(s.pinchType,l.pinchType),singleTouch:gn(s.zoomBySingleTouch,l.singleTouch,!1),resetButton:Ii(l.resetButton,s.resetZoomButton)}}init(s,l){Ke(this,"init",{args:arguments},function(){let c=Ii(ou,s),d=c.chart,h=this.renderTo||d.renderTo;this.userOptions=Ds({},s),(this.renderTo=Cl(h)?wl.getElementById(h):h)||zg(13,!0,this),this.margin=[],this.spacing=[],this.labelCollectors=[],this.callback=l,this.isResizing=0,this.options=c,this.axes=[],this.series=[],this.locale=c.lang.locale??this.renderTo.closest("[lang]")?.lang,this.time=new Fd(Ds(c.time||{},{locale:this.locale}),c.lang),c.time=this.time.options,this.numberFormatter=(d.numberFormatter||Cs).bind(this),this.styledMode=d.styledMode,this.hasCartesianSeries=d.showAxes,this.index=Es.length,Es.push(this),Q.chartCount++,_l(this,d),this.xAxis=[],this.yAxis=[],this.pointCount=this.colorCounter=this.symbolCounter=0,this.setZoomOptions(),Ke(this,"afterInit"),this.firstRender()})}initSeries(s){let c=s.type||this.options.chart.type,d=su[c];d||zg(17,!0,this,{missingModuleFor:c});let h=new d;return"function"==typeof h.init&&h.init(this,s),h}setSortedData(){this.getSeriesOrderByLinks().forEach(function(s){s.points||s.data||!s.enabledDataSorting||s.setData(s.options.data,!1)})}getSeriesOrderByLinks(){return this.series.concat().sort(function(s,l){return s.linkedSeries.length||l.linkedSeries.length?l.linkedSeries.length-s.linkedSeries.length:0})}orderItems(s,l=0){let c=this[s],d=this.options[s]=lu(this.options[s]).slice(),h=this.userOptions[s]=this.userOptions[s]?lu(this.userOptions[s]).slice():[];if(this.hasRendered&&(d.splice(l),h.splice(l)),c)for(let p=l,g=c.length;p=Math.max(b+p,I.pos)&&T<=Math.min(b+p+E.width,I.pos+I.len)||(M.isInsidePlot=!1)}if(!c.ignoreY&&M.isInsidePlot){let I=!d&&c.axis&&!c.axis.isXAxis&&c.axis||_&&(d?_.xAxis:_.yAxis)||{pos:g,len:1/0},T=c.paneCoordinates?I.pos+S:g+S;T>=Math.max(C+g,I.pos)&&T<=Math.min(C+g+E.height,I.pos+I.len)||(M.isInsidePlot=!1)}return Ke(this,"afterIsInsidePlot",M),M.isInsidePlot}redraw(s){Ke(this,"beforeRedraw");let C,_,E,M,l=this.hasCartesianSeries?this.axes:this.colorAxis||[],c=this.series,d=this.pointer,h=this.legend,p=this.userOptions.legend,g=this.renderer,m=g.isHidden(),b=[],x=this.isDirtyBox,S=this.isDirtyLegend;for(g.rootFontSize=g.boxWrapper.getStyle("font-size"),this.setResponsive&&this.setResponsive(!1),iu(!!this.hasRendered&&s,this),m&&this.temporaryDisplay(),this.layOutTitles(!1),E=c.length;E--;)if(((M=c[E]).options.stacking||M.options.centerInCategory)&&(_=!0,M.isDirty)){C=!0;break}if(C)for(E=c.length;E--;)(M=c[E]).options.stacking&&(M.isDirty=!0);c.forEach(function(I){I.isDirty&&("point"===I.options.legendType?("function"==typeof I.updateTotals&&I.updateTotals(),S=!0):p&&(p.labelFormatter||p.labelFormat)&&(S=!0)),I.isDirtyData&&Ke(I,"updatedData")}),S&&h&&h.options.enabled&&(h.render(),this.isDirtyLegend=!1),_&&this.getStacks(),l.forEach(function(I){I.updateNames(),I.setScale()}),this.getMargins(),l.forEach(function(I){I.isDirty&&(x=!0)}),l.forEach(function(I){let T=I.min+","+I.max;I.extKey!==T&&(I.extKey=T,b.push(function(){Ke(I,"afterSetExtremes",Ds(I.eventArgs,I.getExtremes())),delete I.eventArgs})),(x||_)&&I.redraw()}),x&&this.drawChartBox(),Ke(this,"predraw"),c.forEach(function(I){(x||I.isDirty)&&I.visible&&I.redraw(),I.isDirtyData=!1}),d&&d.reset(!0),g.draw(),Ke(this,"redraw"),Ke(this,"render"),m&&this.temporaryDisplay(!0),b.forEach(function(I){I.call()})}get(s){let l=this.series;function c(h){return h.id===s||h.options&&h.options.id===s}let d=Wg(this.axes,c)||Wg(this.series,c);for(let h=0;!d&&h(l.getPointsCollection().forEach(c=>{gn(c.selectedStaging,c.selected)&&s.push(c)}),s),[])}getSelectedSeries(){return this.series.filter(s=>s.selected)}setTitle(s,l,c){this.applyDescription("title",s),this.applyDescription("subtitle",l),this.applyDescription("caption",void 0),this.layOutTitles(c)}applyDescription(s,l){let c=this,d=this.options[s]=Ii(this.options[s],l),h=this[s];h&&l&&(this[s]=h=h.destroy()),d&&!h&&((h=this.renderer.text(d.text,0,0,d.useHTML).attr({align:d.align,class:"highcharts-"+s,zIndex:d.zIndex||4}).css({textOverflow:"ellipsis",whiteSpace:"nowrap"}).add()).update=function(p,g){c.applyDescription(s,p),c.layOutTitles(g)},this.styledMode||h.css(Ds("title"===s?{fontSize:this.options.isStock?"1em":"1.2em"}:{},d.style)),h.textPxLength=h.getBBox().width,h.css({whiteSpace:d.style?.whiteSpace}),this[s]=h)}layOutTitles(s=!0){let l=[0,0,0],{options:c,renderer:d,spacingBox:h}=this;["title","subtitle","caption"].forEach(g=>{let m=this[g],b=this.options[g],C=Ii(h),_=m?.textPxLength||0;if(m&&b){Ke(this,"layOutTitle",{alignTo:C,key:g,textPxLength:_});let E=d.fontMetrics(m),x=E.b,S=E.h,M=b.verticalAlign||"top",I="top"===M,T=I&&b.minScale||1,O="title"===g?I?-3:0:I?l[0]+2:0,R=Math.min(C.width/_,1),P=Math.max(T,R),U=Ii({y:"bottom"===M?x:O+x},{align:"title"===g?RT?this.chartWidth:C.width)/P)+"px";m.alignValue!==U.align&&(m.placed=!1);let F=Math.round(m.css({width:$}).getBBox(b.useHTML).height);if(U.height=F,m.align(U,!1,C).attr({align:U.align,scaleX:P,scaleY:P,"transform-origin":`${C.x+_*P*e1(U.align)} ${S}`}),!b.floating){let j=F*(F<1.2*S?1:P);"top"===M?l[0]=Math.ceil(l[0]+j):"bottom"===M&&(l[2]=Math.ceil(l[2]+j))}}},this),l[0]&&"top"===(c.title?.verticalAlign||"top")&&(l[0]+=c.title?.margin||0),l[2]&&"bottom"===c.caption?.verticalAlign&&(l[2]+=c.caption?.margin||0);let p=!this.titleOffset||this.titleOffset.join(",")!==l.join(",");this.titleOffset=l,Ke(this,"afterLayOutTitles"),!this.isDirtyBox&&p&&(this.isDirtyBox=this.isDirtyLegend=p,this.hasRendered&&s&&this.isDirtyBox&&this.redraw())}getContainerBox(){let s=[].map.call(this.renderTo.children,c=>{if(c!==this.container){let d=c.style.display;return c.style.display="none",[c,d]}}),l={width:ga(this.renderTo,"width",!0)||0,height:ga(this.renderTo,"height",!0)||0};return s.filter(Boolean).forEach(([c,d])=>{c.style.display=d}),l}getChartSize(){let s=this.options.chart,l=s.width,c=s.height,d=this.getContainerBox(),h=d.height<=1||!this.renderTo.parentElement?.style.height&&"100%"===this.renderTo.style.height;this.chartWidth=Math.max(0,l||d.width||600),this.chartHeight=Math.max(0,Eb(c,this.chartWidth)||(h?400:d.height)),this.containerBox=d}temporaryDisplay(s){let c,l=this.renderTo;if(s)for(;l?.style;)l.hcOrigStyle&&(Ti(l,l.hcOrigStyle),delete l.hcOrigStyle),l.hcOrigDetached&&(wl.body.removeChild(l),l.hcOrigDetached=!1),l=l.parentNode;else for(;l?.style&&(wl.body.contains(l)||l.parentNode||(l.hcOrigDetached=!0,wl.body.appendChild(l)),("none"===ga(l,"display",!1)||l.hcOricDetached)&&(l.hcOrigStyle={display:l.style.display,height:l.style.height,overflow:l.style.overflow},c={display:"block",overflow:"hidden"},l!==this.renderTo&&(c.height=0),Ti(l,c),l.offsetWidth||l.style.setProperty("display","block","important")),(l=l.parentNode)!==wl.body););}setClassName(s){this.container.className="highcharts-container "+(s||"")}getContainer(){let s,l=this.options,c=l.chart,d="data-highcharts-chart",h=i1(),p=this.renderTo,g=Gg(Th(p,d));co(g)&&Es[g]&&Es[g].hasRendered&&Es[g].destroy(),Th(p,d,this.index),p.innerHTML=bt.emptyHTML,c.skipClone||p.offsetWidth||this.temporaryDisplay(),this.getChartSize();let m=this.chartHeight,b=this.chartWidth;Ti(p,{overflow:"hidden"}),this.styledMode||(s=Ds({position:"relative",overflow:"hidden",width:b+"px",height:m+"px",textAlign:"left",lineHeight:"normal",zIndex:0,"-webkit-tap-highlight-color":"rgba(0,0,0,0)",userSelect:"none","touch-action":"manipulation",outline:"none",padding:"0px"},c.style||{}));let C=au("div",{id:h},s,p);this.container=C,this.getChartSize(),b!==this.chartWidth&&(b=this.chartWidth,this.styledMode||Ti(C,{width:gn(c.style?.width,b+"px")})),this.containerBox=this.getContainerBox(),this._cursor=C.style.cursor;let _=c.renderer||!wb?ol.getRendererType(c.renderer):En;if(this.renderer=new _(C,b,m,void 0,c.forExport,l.exporting?.allowHTML,this.styledMode),iu(void 0,this),this.setClassName(c.className),this.styledMode)for(let E in l.defs)this.renderer.definition(l.defs[E]);else this.renderer.setStyle(c.style);this.renderer.chartIndex=this.index,Ke(this,"afterGetContainer")}getMargins(s){let{spacing:l,margin:c,titleOffset:d}=this;this.resetMargins(),d[0]&&!lo(c[0])&&(this.plotTop=Math.max(this.plotTop,d[0]+l[0])),d[2]&&!lo(c[2])&&(this.marginBottom=Math.max(this.marginBottom,d[2]+l[2])),this.legend?.display&&this.legend.adjustMargins(c,l),Ke(this,"getMargins"),s||this.getAxisMargins()}getAxisMargins(){let s=this,l=s.axisOffset=[0,0,0,0],c=s.colorAxis,d=s.margin,h=p=>{p.forEach(g=>{g.visible&&g.getOffset()})};s.hasCartesianSeries?h(s.axes):c?.length&&h(c),pa.forEach((p,g)=>{lo(d[g])||(s[p]+=l[g])}),s.setChartSize()}getOptions(){return Ug(this.userOptions,ou)}reflow(s){let l=this,c=l.containerBox,d=l.getContainerBox();delete l.pointer?.chartPosition,!l.exporting?.isPrinting&&!l.isResizing&&c&&d.width&&((d.width!==c.width||d.height!==c.height)&&(we.clearTimeout(l.reflowTimeout),l.reflowTimeout=r1(function(){l.container&&l.setSize(void 0,void 0,!1)},100*!!s)),l.containerBox=d)}setReflow(){let s=this,l=c=>{s.options?.chart.reflow&&s.hasLoaded&&s.reflow(c)};if("function"==typeof ResizeObserver)new ResizeObserver(l).observe(s.renderTo);else{let c=Mh(Hg,"resize",l);Mh(this,"destroy",c)}}setSize(s,l,c){let d=this,h=d.renderer;d.isResizing+=1,iu(c,d);let p=h.globalAnimation;d.oldChartHeight=d.chartHeight,d.oldChartWidth=d.chartWidth,void 0!==s&&(d.options.chart.width=s),void 0!==l&&(d.options.chart.height=l),d.getChartSize();let{chartWidth:g,chartHeight:m,scrollablePixelsX:b=0,scrollablePixelsY:C=0}=d;(d.isDirtyBox||g!==d.oldChartWidth||m!==d.oldChartHeight)&&(d.styledMode||(p?jg:Ti)(d.container,{width:`${g+b}px`,height:`${m+C}px`},p),d.setChartSize(!0),h.setSize(g,m,p),d.axes.forEach(function(_){_.isDirty=!0,_.setScale()}),d.isDirtyLegend=!0,d.isDirtyBox=!0,d.layOutTitles(),d.getMargins(),d.redraw(p),d.oldChartHeight=void 0,Ke(d,"resize"),setTimeout(()=>{d&&Ke(d,"endResize")},_b(p).duration)),d.isResizing-=1}setChartSize(s){let l,c,d,h,{chartHeight:p,chartWidth:g,inverted:m,spacing:b,renderer:C}=this,_=this.clipOffset,E=Math[m?"floor":"round"];this.plotLeft=l=Math.round(this.plotLeft),this.plotTop=c=Math.round(this.plotTop),this.plotWidth=d=Math.max(0,Math.round(g-l-(this.marginRight??0))),this.plotHeight=h=Math.max(0,Math.round(p-c-(this.marginBottom??0))),this.plotSizeX=m?h:d,this.plotSizeY=m?d:h,this.spacingBox=C.spacingBox={x:b[3],y:b[0],width:g-b[3]-b[1],height:p-b[0]-b[2]},this.plotBox=C.plotBox={x:l,y:c,width:d,height:h},_&&(this.clipBox={x:E(_[3]),y:E(_[0]),width:E(this.plotSizeX-_[1]-_[3]),height:E(this.plotSizeY-_[0]-_[2])}),s||(this.axes.forEach(function(x){x.setAxisSize(),x.setAxisTranslation()}),C.alignElements()),Ke(this,"afterSetChartSize",{skipAxes:s})}resetMargins(){Ke(this,"resetMargins");let s=this,l=s.options.chart,c=l.plotBorderWidth||0,d=Math.round(c)/2;["margin","spacing"].forEach(h=>{let p=l[h],g=n1(p)?p:[p,p,p,p];["Top","Right","Bottom","Left"].forEach((m,b)=>{s[h][b]=l[`${h}${m}`]??g[b]})}),pa.forEach((h,p)=>{s[h]=s.margin[p]??s.spacing[p]}),s.axisOffset=[0,0,0,0],s.clipOffset=[d,d,d,d],s.plotBorderWidth=c}drawChartBox(){let P,U,$,s=this.options.chart,l=this.renderer,c=this.chartWidth,d=this.chartHeight,h=this.styledMode,p=this.plotBGImage,g=s.backgroundColor,m=s.plotBackgroundColor,b=s.plotBackgroundImage,C=this.plotLeft,_=this.plotTop,E=this.plotWidth,x=this.plotHeight,S=this.plotBox,M=this.clipRect,I=this.clipBox,T=this.chartBackground,O=this.plotBackground,R=this.plotBorder,F="animate";T||(this.chartBackground=T=l.rect().addClass("highcharts-background").add(),F="attr"),h?P=U=T.strokeWidth():(U=(P=s.borderWidth||0)+8*!!s.shadow,$={fill:g||"none"},(P||T["stroke-width"])&&($.stroke=s.borderColor,$["stroke-width"]=P),T.attr($).shadow(s.shadow)),T[F]({x:U/2,y:U/2,width:c-U-P%2,height:d-U-P%2,r:s.borderRadius}),F="animate",O||(F="attr",this.plotBackground=O=l.rect().addClass("highcharts-plot-background").add()),O[F](S),!h&&(O.attr({fill:m||"none"}).shadow(s.plotShadow),b&&(p?(b!==p.attr("href")&&p.attr("href",b),p.animate(S)):this.plotBGImage=l.image(b,C,_,E,x).add())),M?M.animate({width:I.width,height:I.height}):this.clipRect=l.clipRect(I),F="animate",R||(F="attr",this.plotBorder=R=l.rect().addClass("highcharts-plot-border").attr({zIndex:1}).add()),h||R.attr({stroke:s.plotBorderColor,"stroke-width":s.plotBorderWidth||0,fill:"none"}),R[F](R.crisp(S,-R.strokeWidth())),this.isDirtyBox=!1,Ke(this,"afterDrawChartBox")}propFromSeries(){let s,l,c,d=this,h=d.options.chart,p=d.options.series;["inverted","angular","polar"].forEach(function(g){for(l=su[h.type],c=h[g]||l&&l.prototype[g],s=p?.length;!c&&s--;)(l=su[p[s].type])&&l.prototype[g]&&(c=!0);d[g]=c})}linkSeries(s){let l=this,c=l.series;c.forEach(function(d){d.linkedSeries.length=0}),c.forEach(function(d){let{linkedTo:h}=d.options;if(Cl(h)){let p;(p=":previous"===h?l.series[d.index-1]:l.get(h))&&p.linkedParent!==d&&(p.linkedSeries.push(d),d.linkedParent=p,p.enabledDataSorting&&d.setDataSortingOptions(),d.visible=gn(d.options.visible,p.options.visible,d.visible))}}),Ke(this,"afterLinkSeries",{isUpdating:s})}renderSeries(){this.series.forEach(function(s){s.translate(),s.render()})}render(){let m,s=this.axes,l=this.colorAxis,c=this.renderer,d=this.options.chart.axisLayoutRuns||2,h=C=>{C.forEach(_=>{_.visible&&_.render()})},p=0,g=!0,b=0;for(let C of(this.setTitle(),Ke(this,"beforeMargins"),this.getStacks?.(),this.getMargins(!0),this.setChartSize(),s)){let{options:_}=C,{labels:E}=_;if(this.hasCartesianSeries&&C.horiz&&C.visible&&E.enabled&&C.series.length&&"colorAxis"!==C.coll&&!this.polar){p=_.tickLength,C.createGroups();let x=new xn(C,0,"",!0),S=x.createLabel("x",E);if(x.destroy(),S&&gn(E.reserveSpace,!co(_.crossing))&&(p=S.getBBox().height+E.distance+Math.max(_.offset||0,0)),p){S?.destroy();break}}}for(this.plotHeight=Math.max(this.plotHeight-p,0);(g||m||d>1)&&b(b?1:1.1),m=_/this.plotHeight>(b?1:1.05),b++}this.drawChartBox(),this.hasCartesianSeries?h(s):l?.length&&h(l),this.seriesGroup||(this.seriesGroup=c.g("series-group").attr({zIndex:3}).shadow(this.options.chart.seriesGroupShadow).add()),this.dataLabelsGroup||(this.dataLabelsGroup=c.g("datalabels-group").attr({zIndex:6}).add()),this.renderSeries(),this.addCredits(),this.setResponsive&&this.setResponsive(),this.hasRendered=!0}addCredits(s){let l=this,c=Ii(!0,this.options.credits,s);c.enabled&&!this.credits&&(this.credits=this.renderer.text(c.text+(this.mapCredits||""),0,0).addClass("highcharts-credits").on("click",function(){c.href&&(Hg.location.href=c.href)}).attr({align:c.position.align,zIndex:8}),l.styledMode||this.credits.css(c.style),this.credits.add().align(c.position),this.credits.update=function(d){l.credits=l.credits.destroy(),l.addCredits(d)})}destroy(){let s,l=this,c=l.axes,d=l.series,h=l.container,p=h?.parentNode;for(Ke(l,"destroy"),l.renderer.forExport?Cb(Es,l):Es[l.index]=void 0,Q.chartCount--,l.renderTo.removeAttribute("data-highcharts-chart"),qg(l),s=c.length;s--;)c[s]=c[s].destroy();for(this.scroller?.destroy?.(),s=d.length;s--;)d[s]=d[s].destroy();["title","subtitle","chartBackground","plotBackground","plotBGImage","plotBorder","seriesGroup","clipRect","credits","pointer","rangeSelector","legend","resetZoomButton","tooltip","renderer"].forEach(g=>{l[g]=l[g]?.destroy?.()}),h&&(h.innerHTML=bt.emptyHTML,qg(h),p&&Jx(h)),Ih(l,function(g,m){delete l[m]})}firstRender(){let s=this,l=s.options;s.getContainer(),s.resetMargins(),s.setChartSize(),s.propFromSeries(),s.createAxes();let c=t1(l.series)?l.series:[];l.series=[],c.forEach(function(d){s.initSeries(d)}),s.linkSeries(),s.setSortedData(),Ke(s,"beforeRender"),s.render(),s.pointer?.getChartPosition(),s.renderer.imgCount||s.hasLoaded||s.onload(),s.temporaryDisplay(!0)}onload(){this.callbacks.concat([this.callback]).forEach(function(s){s&&void 0!==this.index&&s.apply(this,[this])},this),Ke(this,"load"),Ke(this,"render"),lo(this.index)&&this.setReflow(),this.warnIfA11yModuleNotLoaded(),this.hasLoaded=!0}warnIfA11yModuleNotLoaded(){let{options:s,title:l}=this;s&&!this.accessibility&&(this.renderer.boxWrapper.attr({role:"img","aria-label":(l?.element.textContent||"").replace(/this.transform({reset:!0,trigger:"zoom"}))}pan(s,l){let c=this,d="object"==typeof l?l:{enabled:l,type:"x"},h=d.type,p=h&&c[{x:"xAxis",xy:"axes",y:"yAxis"}[h]].filter(m=>m.options.panningEnabled&&!m.options.isInternal),g=c.options.chart;g?.panning&&(g.panning=d),Ke(this,"pan",{originalEvent:s},()=>{c.transform({axes:p,event:s,to:{x:s.chartX-(c.mouseDownX||0),y:s.chartY-(c.mouseDownY||0)},trigger:"pan"}),Ti(c.container,{cursor:"move"})})}transform(s){let{axes:l=this.axes,event:c,from:d={},reset:h,selection:p,to:g={},trigger:m}=s,{inverted:b,time:C}=this;this.hoverPoints?.forEach(S=>S.setState()),Ke(this,"transform",s);let E,x,_=s.hasZoomed||!1;for(let S of l){let{horiz:M,len:I,minPointOffset:T=0,options:O,reversed:R}=S,P=M?"width":"height",U=M?"x":"y",$=gn(g[P],S.len),F=gn(d[P],S.len),j=10>Math.abs($)?1:$/F,q=(d[U]||0)+F/2-S.pos,Z=q-((g[U]??S.pos)+$/2-S.pos)/j,X=R&&!b||!R&&b?-1:1;if(!h&&(q<0||q>S.len))continue;let ue=S.chart.polar||S.isOrdinal?0:T*X||0,me=S.toValue(Z,!0),ge=S.toValue(Z+I/j,!0),le=me+ue,ne=ge-ue,fe=S.allExtremes;if(p&&p[S.coll].push({axis:S,min:Math.min(me,ge),max:Math.max(me,ge)}),le>ne&&([le,ne]=[ne,le]),1===j&&!h&&"yAxis"===S.coll&&!fe){for(let Bn of S.series){let Oi=Bn.getExtremes(Bn.getProcessedData(!0).modified.getColumn("y")||[],!0);fe??(fe={dataMin:Number.MAX_VALUE,dataMax:-Number.MAX_VALUE}),co(Oi.dataMin)&&co(Oi.dataMax)&&(fe.dataMin=Math.min(Oi.dataMin,fe.dataMin),fe.dataMax=Math.max(Oi.dataMax,fe.dataMax))}S.allExtremes=fe}let{dataMin:mt,dataMax:qe,min:tr,max:Ie}=Ds(S.getExtremes(),fe||{}),De=C.parse(O.min),Qe=C.parse(O.max),Fe=mt??De,yt=qe??Qe,vt=ne-le,Xe=S.categories?0:Math.min(vt,yt-Fe),St=Fe-Xe*(lo(De)?0:O.minPadding),nr=yt+Xe*(lo(Qe)?0:O.maxPadding),pr=S.allowZoomOutside||1===j||"zoom"!==m&&j>1,Ho=Math.min(De??St,St,pr?tr:St),$r=Math.max(Qe??nr,nr,pr?Ie:nr);(!S.isOrdinal||1!==j||h)&&(le=1&&(ne=le+vt)),ne>$r&&(ne=$r,j>=1&&(le=ne-vt)),(h||S.series.length&&(le!==tr||ne!==Ie)&&le>=Ho&&ne<=$r)&&(p?p[S.coll].push({axis:S,min:le,max:ne}):(S.isPanning="zoom"!==m,S.isPanning&&(x=!0),S.setExtremes(h?void 0:le,h?void 0:ne,!1,!1,{move:Z,trigger:m,scale:j}),!h&&(le>Ho||ne<$r)&&"mousewheel"!==m&&(E=!0)),_=!0),this.hasCartesianSeries||h||"mousewheel"===m||(E=!0),c&&(this[M?"mouseDownX":"mouseDownY"]=c[M?"chartX":"chartY"]))}return _&&(p?Ke(this,"selection",p,()=>{delete s.selection,s.trigger="zoom",this.transform(s)}):(!E||x||this.resetZoomButton?!E&&this.resetZoomButton&&(this.resetZoomButton=this.resetZoomButton.destroy()):this.showResetZoom(),this.redraw("zoom"===m&&(this.options.chart.animation??this.pointCount<100)))),_}}Ds(Vo.prototype,{callbacks:[],collectionsWithInit:{xAxis:[Vo.prototype.addAxis,[!0]],yAxis:[Vo.prototype.addAxis,[!1]],series:[Vo.prototype.addSeries]},collectionsWithUpdate:["xAxis","yAxis","series"],propsRequireDirtyBox:["backgroundColor","borderColor","borderWidth","borderRadius","plotBackgroundColor","plotBackgroundImage","plotBorderColor","plotBorderWidth","plotShadow","shadow"],propsRequireReflow:["margin","marginTop","marginRight","marginBottom","marginLeft","spacing","spacingTop","spacingRight","spacingBottom","spacingLeft"],propsRequireUpdateSeries:["chart.inverted","chart.polar","chart.ignoreHiddenSeries","chart.type","colors","plotOptions","time","tooltip"]});let{stop:Db}=G,{composed:xb}=Q,{addEvent:xs,createElement:Ah,css:kh,defined:Oh,erase:Sb,merge:Mb,pushUnique:Tb}=we;function Ib(){let v=this.scrollablePlotArea;(this.scrollablePixelsX||this.scrollablePixelsY)&&!v&&(this.scrollablePlotArea=v=new Xg(this)),v?.applyFixed()}function Yg(){this.chart.scrollablePlotArea&&(this.chart.scrollablePlotArea.isDirty=!0)}let Xg=(()=>{class v{static compose(l,c,d){Tb(xb,this.compose)&&(xs(l,"afterInit",Yg),xs(c,"afterSetChartSize",h=>this.afterSetSize(h.target,h)),xs(c,"render",Ib),xs(d,"show",Yg))}static afterSetSize(l,c){let d,h,p,{minWidth:g,minHeight:m}=l.options.chart.scrollablePlotArea||{},{clipBox:b,plotBox:C,inverted:_,renderer:E}=l;if(!E.forExport)if(g?(l.scrollablePixelsX=d=Math.max(0,g-l.chartWidth),d&&(l.scrollablePlotBox=Mb(l.plotBox),C.width=l.plotWidth+=d,b[_?"height":"width"]+=d,p=!0)):m&&(l.scrollablePixelsY=h=Math.max(0,m-l.chartHeight),Oh(h)&&(l.scrollablePlotBox=Mb(l.plotBox),C.height=l.plotHeight+=h,b[_?"width":"height"]+=h,p=!1)),Oh(p)){if(!c.skipAxes)for(let x of l.axes)(x.horiz===p||l.hasParallelCoordinates&&"yAxis"===x.coll)&&(x.setAxisSize(),x.setAxisTranslation())}else delete l.scrollablePlotBox}constructor(l){let c,d=l.options.chart,h=ol.getRendererType(),p=d.scrollablePlotArea||{},g=this.moveFixedElements.bind(this),m={WebkitOverflowScrolling:"touch",overflowX:"hidden",overflowY:"hidden"};l.scrollablePixelsX&&(m.overflowX="auto"),l.scrollablePixelsY&&(m.overflowY="auto"),this.chart=l;let b=this.parentDiv=Ah("div",{className:"highcharts-scrolling-parent"},{position:"relative"},l.renderTo),C=this.scrollingContainer=Ah("div",{className:"highcharts-scrolling"},m,b),_=this.innerContainer=Ah("div",{className:"highcharts-inner-container"},void 0,C),E=this.fixedDiv=Ah("div",{className:"highcharts-fixed"},{position:"absolute",overflow:"hidden",pointerEvents:"none",zIndex:(d.style?.zIndex||0)+2,top:0},void 0,!0),x=this.fixedRenderer=new h(E,l.chartWidth,l.chartHeight,d.style);this.mask=x.path().attr({fill:d.backgroundColor||"#fff","fill-opacity":p.opacity??.85,zIndex:-1}).addClass("highcharts-scrollable-mask").add(),C.parentNode.insertBefore(E,C),kh(l.renderTo,{overflow:"visible"}),xs(l,"afterShowResetZoom",g),xs(l,"afterApplyDrilldown",g),xs(l,"afterLayOutTitles",g),xs(C,"scroll",()=>{let{pointer:S,hoverPoint:M}=l;S&&(delete S.chartPosition,M&&(c=M),S.runPointActions(void 0,c,!0))}),_.appendChild(l.container)}applyFixed(){let{chart:l,fixedRenderer:c,isDirty:d,scrollingContainer:h}=this,{axisOffset:p,chartWidth:g,chartHeight:m,container:b,plotHeight:C,plotLeft:_,plotTop:E,plotWidth:x,scrollablePixelsX:S=0,scrollablePixelsY:M=0}=l,{scrollPositionX:I=0,scrollPositionY:T=0}=l.options.chart.scrollablePlotArea||{},O=g+S,R=m+M;c.setSize(g,m),(d??!0)&&(this.isDirty=!1,this.moveFixedElements()),Db(l.container),kh(b,{width:`${O}px`,height:`${R}px`}),l.renderer.boxWrapper.attr({width:O,height:R,viewBox:[0,0,O,R].join(" ")}),l.chartBackground?.attr({width:O,height:R}),kh(h,{width:`${g}px`,height:`${m}px`}),Oh(d)||(h.scrollLeft=S*I,h.scrollTop=M*T);let P=E-p[0]-1,U=_-p[3]-1,$=E+C+p[2]+1,F=_+x+p[1]+1,j=_+x-S,q=E+C-M,Z=[["M",0,0]];S?Z=[["M",0,P],["L",_-1,P],["L",_-1,$],["L",0,$],["Z"],["M",j,P],["L",g,P],["L",g,$],["L",j,$],["Z"]]:M&&(Z=[["M",U,0],["L",U,E-1],["L",F,E-1],["L",F,0],["Z"],["M",U,q],["L",U,m],["L",F,m],["L",F,q],["Z"]]),"adjustHeight"!==l.redrawTrigger&&this.mask.attr({d:Z})}moveFixedElements(){let l,{container:c,inverted:d,scrollablePixelsX:h,scrollablePixelsY:p}=this.chart,g=this.fixedRenderer,m=v.fixedSelectors;if(h&&!d?l=".highcharts-yaxis":h&&d||p&&!d?l=".highcharts-xaxis":p&&d&&(l=".highcharts-yaxis"),!l||this.chart.hasParallelCoordinates&&".highcharts-yaxis"===l)for(let b of[".highcharts-xaxis",".highcharts-yaxis"])for(let C of[`${b}:not(.highcharts-radial-axis)`,`${b}-labels:not(.highcharts-radial-axis-labels)`])Sb(m,C);else for(let b of[`${l}:not(.highcharts-radial-axis)`,`${l}-labels:not(.highcharts-radial-axis-labels)`])Tb(m,b);for(let b of m)[].forEach.call(c.querySelectorAll(b),C=>{(C.namespaceURI===g.SVG_NS?g.box:g.box.parentNode).appendChild(C),C.style.pointerEvents="auto"})}}return v.fixedSelectors=[".highcharts-breadcrumbs-group",".highcharts-contextbutton",".highcharts-caption",".highcharts-credits",".highcharts-drillup-button",".highcharts-legend",".highcharts-legend-checkbox",".highcharts-navigator-series",".highcharts-navigator-xaxis",".highcharts-navigator-yaxis",".highcharts-navigator",".highcharts-range-selector-group",".highcharts-reset-zoom",".highcharts-scrollbar",".highcharts-subtitle",".highcharts-title"],v})(),{format:o1}=Qr,{series:s1}=Bt,{destroyObjectProperties:Ab,fireEvent:Zg,getAlignFactor:Rh,isNumber:Kg,pick:ma}=we,Qg=class{constructor(v,s,l,c,d){let h=v.chart.inverted,p=v.reversed;this.axis=v;let g=this.isNegative=!!l!=!!p;this.options=s=s||{},this.x=c,this.total=null,this.cumulative=null,this.points={},this.hasValidPoints=!1,this.stack=d,this.leftCliff=0,this.rightCliff=0,this.alignOptions={align:s.align||(h?g?"left":"right":"center"),verticalAlign:s.verticalAlign||(h?"middle":g?"bottom":"top"),y:s.y,x:s.x},this.textAlign=s.textAlign||(h?g?"right":"left":"center")}destroy(){Ab(this,this.axis)}render(v){let s=this.axis.chart,l=this.options,c=l.format,d=c?o1(c,this,s):l.formatter.call(this);if(this.label)this.label.attr({text:d,visibility:"hidden"});else{this.label=s.renderer.label(d,null,void 0,l.shape,void 0,void 0,l.useHTML,!1,"stack-labels");let h={r:l.borderRadius||0,text:d,padding:ma(l.padding,5),visibility:"hidden"};s.styledMode||(h.fill=l.backgroundColor,h.stroke=l.borderColor,h["stroke-width"]=l.borderWidth,this.label.css(l.style||{})),this.label.attr(h),this.label.added||this.label.add(v)}this.label.labelrank=s.plotSizeY,Zg(this,"afterRender")}setOffset(v,s,l,c,d,h){let{alignOptions:p,axis:g,label:m,options:b,textAlign:C}=this,_=g.chart,E=this.getStackBox({xOffset:v,width:s,boxBottom:l,boxTop:c,defaultX:d,xAxis:h}),{verticalAlign:x}=p;if(m&&E){let T,S=m.getBBox(void 0,0),M=m.padding,I="justify"===ma(b.overflow,"justify");p.x=b.x||0,p.y=b.y||0;let{x:O,y:R}=this.adjustStackPosition({labelBox:S,verticalAlign:x,textAlign:C});E.x-=O,E.y-=R,m.align(p,!1,E),(T=_.isInsidePlot(m.alignAttr.x+p.x+O,m.alignAttr.y+p.y+R))||(I=!1),I&&s1.prototype.justifyDataLabel.call(g,m,p,m.alignAttr,S,E),m.attr({x:m.alignAttr.x,y:m.alignAttr.y,rotation:b.rotation,rotationOriginX:S.width*Rh(b.textAlign||"center"),rotationOriginY:S.height/2}),ma(!I&&b.crop,!0)&&(T=Kg(m.x)&&Kg(m.y)&&_.isInsidePlot(m.x-M+(m.width||0),m.y)&&_.isInsidePlot(m.x+M,m.y)),m[T?"show":"hide"]()}Zg(this,"afterSetOffset",{xOffset:v,width:s})}adjustStackPosition({labelBox:v,verticalAlign:s,textAlign:l}){return{x:v.width/2+v.width/2*(2*Rh(l)-1),y:v.height/2*2*(1-Rh(s))}}getStackBox(v){let s=this.axis,l=s.chart,{boxTop:c,defaultX:d,xOffset:h,width:p,boxBottom:g}=v,m=s.stacking.usePercentage?100:ma(c,this.total,0),b=s.toPixels(m),C=v.xAxis||l.xAxis[0],_=ma(d,C.translate(this.x))+h,E=Math.abs(b-s.toPixels(g||Kg(s.min)&&s.logarithmic&&s.logarithmic.lin2log(s.min)||0)),S=this.isNegative;return l.inverted?{x:(S?b:b-E)-l.plotLeft,y:C.height-_-p+C.top-l.plotTop,width:E,height:p}:{x:_+C.transB-l.plotLeft,y:(S?b-E:b)-l.plotTop,width:p,height:E}}},{getDeferredAnimation:_t}=G,{series:{prototype:a1}}=Bt,{addEvent:kb,correctFloat:cu,defined:Ob,destroyObjectProperties:l1,fireEvent:c1,isNumber:Jg,objectEach:Ss,pick:em}=we;function u1(){let v=this.inverted;this.axes.forEach(s=>{s.stacking?.stacks&&s.hasVisibleSeries&&(s.stacking.oldStacks=s.stacking.stacks)}),this.series.forEach(s=>{let l=s.xAxis?.options||{};s.options.stacking&&s.reserveSpace()&&(s.stackKey=[s.type,em(s.options.stack,""),v?l.top:l.left,v?l.height:l.width].join(","))})}function d1(){let v=this.stacking;if(v){let s=v.stacks;Ss(s,(l,c)=>{l1(l),delete s[c]}),v.stackTotalGroup?.destroy()}}function tm(){this.stacking||(this.stacking=new f1(this))}function Rb(v,s,l,c){return!Ob(v)||v.x!==s||c&&v.stackKey!==c?v={x:s,index:0,key:c,stackKey:c}:v.index++,v.key=[l,s,v.index].join(","),v}function Nb(){let v,s=this,c=s.stackKey||"",d=s.yAxis.stacking.stacks,h=s.getColumn("x",!0),g=s[s.options.stacking+"Stacker"];g&&[c,"-"+c].forEach(m=>{let C,_,E,b=h.length;for(;b--;)C=h[b],v=s.getStackIndicator(v,C,s.index,m),_=d[m]?.[C],(E=_?.points[v.key||""])&&g.call(s,E,_,b)})}function Nh(v,s,l){let c=s.total?100/s.total:0;v[0]=cu(v[0]*c),v[1]=cu(v[1]*c),this.stackedYData[l]=v[1]}function El(v){(this.is("column")||this.is("columnrange"))&&(this.options.centerInCategory&&this.chart.series.length>1?a1.setStackedPoints.call(this,v,"group"):v.stacking.resetStacks())}function h1(v,s){let l,c,d,h,p,g,m,b=s||this.options.stacking;if(!b||!this.reserveSpace()||({group:"xAxis"}[b]||"yAxis")!==v.coll)return;let C=this.getColumn("x",!0),_=this.getColumn(this.pointValKey||"y",!0),E=[],x=_.length,S=this.options,M=S.threshold||0,I=S.startFromThreshold?M:0,T=S.stack,O=s?`${this.type},${b}`:this.stackKey||"",R="-"+O,P=this.negStacks,U=v.stacking,$=U.stacks,F=U.oldStacks;for(U.stacksTouched+=1,m=0;m0&&!1===this.singleStacks&&(d.points[g][0]=d.points[this.index+","+j+",0"][0])):(delete d.points[g],delete d.points[this.index]);let X=d.total||0;"percent"===b?(h=c?O:R,X=P&&$[h]?.[j]?(h=$[h][j]).total=Math.max(h.total||0,X)+Math.abs(Z):cu(X+Math.abs(Z))):"group"===b?Jg(q)&&X++:X=cu(X+Z),d.cumulative="group"===b?(X||1)-1:cu(em(d.cumulative,I)+Z),d.total=X,null!==q&&(d.points[g].push(d.cumulative),E[m]=d.cumulative,d.hasValidPoints=!0)}"percent"===b&&(U.usePercentage=!0),"group"!==b&&(this.stackedYData=E),U.oldStacks={}}class f1{constructor(s){this.oldStacks={},this.stacks={},this.stacksTouched=0,this.axis=s}buildStacks(){let s,l,c=this.axis,d=c.series,h="xAxis"===c.coll,p=c.options.reversedStacks,g=d.length;for(this.resetStacks(),this.usePercentage=!1,l=g;l--;)s=d[p?l:g-l-1],h&&s.setGroupedPoints(c),s.setStackedPoints(c);if(!h)for(l=0;l{Ss(s,l=>{l.cumulative=l.total})}))}resetStacks(){Ss(this.stacks,s=>{Ss(s,(l,c)=>{Jg(l.touched)&&l.touched{Ss(g,m=>{m.render(p)})}),p.animate({opacity:1},h)}}(yi||(yi={})).compose=function(v,s,l){let c=s.prototype,d=l.prototype;c.getStacks||(kb(v,"init",tm),kb(v,"destroy",d1),c.getStacks=u1,d.getStackIndicator=Rb,d.modifyStacks=Nb,d.percentStacker=Nh,d.setGroupedPoints=El,d.setStackedPoints=h1)};let nm=yi,{defined:rm,merge:Pb,isObject:p1}=we;class uu extends Er{drawGraph(){let s=this.options,l=(this.gappedPath||this.getGraphPath).call(this),c=this.chart.styledMode;[this,...this.zones].forEach((d,h)=>{let p,g=d.graph,m=g?"animate":"attr",b=d.dashStyle||s.dashStyle;g?(g.endX=this.preventGraphAnimation?null:l.xMap,g.animate({d:l})):l.length&&(d.graph=g=this.chart.renderer.path(l).addClass("highcharts-graph"+(h?` highcharts-zone-graph-${h-1} `:" ")+(h&&d.className||"")).attr({zIndex:1}).add(this.group)),g&&!c&&(p={stroke:!h&&s.lineColor||d.color||this.color||"#cccccc","stroke-width":s.lineWidth||0,fill:this.fillGraph&&this.color||"none"},b?p.dashstyle=b:"square"!==s.linecap&&(p["stroke-linecap"]=p["stroke-linejoin"]="round"),g[m](p).shadow(s.shadow&&Pb({filterUnits:"userSpaceOnUse"},p1(s.shadow)?s.shadow:{}))),g&&(g.startX=l.xMap,g.isArea=l.isArea)})}getGraphPath(s,l,c){let m,d=this,h=d.options,p=[],g=[],b=h.step,C=(s=s||d.points).reversed;return C&&s.reverse(),(b={right:1,center:2}[b]||b&&3)&&C&&(b=4-b),(s=this.getValidPoints(s,!1,h.nullInteraction||!(h.connectNulls&&!l&&!c))).forEach(function(_,E){let x,S=_.plotX,M=_.plotY,I=s[E-1],T=_.isNull||"number"!=typeof M;(_.leftCliff||I?.rightCliff)&&!c&&(m=!0),T&&!rm(l)&&E>0?m=!h.connectNulls:T&&!l?m=!0:(0===E||m?x=[["M",_.plotX,_.plotY]]:d.getPointSpline?x=[d.getPointSpline(s,_,E)]:b?(x=1===b?[["L",I.plotX,M]]:2===b?[["L",(I.plotX+S)/2,I.plotY],["L",(I.plotX+S)/2,M]]:[["L",S,I.plotY]]).push(["L",S,M]):x=[["L",S,M]],g.push(_.x),b&&(g.push(_.x),2===b&&g.push(_.x)),p.push.apply(p,x),m=!1)}),p.xMap=g,d.graphPath=p,p}}uu.defaultOptions=Pb(Er.defaultOptions,{legendSymbol:"lineMarker"}),Bt.registerSeriesType("line",uu);let{seriesTypes:{line:du}}=Bt,{extend:im,merge:Lb,objectEach:om,pick:$o}=we;class sm extends du{drawGraph(){this.areaPath=[],super.drawGraph.apply(this);let{areaPath:s,options:l}=this;[this,...this.zones].forEach((c,d)=>{let h={},p=c.fillColor||l.fillColor,g=c.area,m=g?"animate":"attr";g?(g.endX=this.preventGraphAnimation?null:s.xMap,g.animate({d:s})):(h.zIndex=0,(g=c.area=this.chart.renderer.path(s).addClass("highcharts-area"+(d?` highcharts-zone-area-${d-1} `:" ")+(d&&c.className||"")).add(this.group)).isArea=!0),this.chart.styledMode||(h.fill=p||c.color||this.color,h["fill-opacity"]=p?1:l.fillOpacity??.75,g.css({pointerEvents:this.stickyTracking?"none":"auto"})),g[m](h),g.startX=s.xMap,g.shiftUnit=l.step?2:1})}getGraphPath(s){let l,c,d,h=du.prototype.getGraphPath,p=this.options,g=p.stacking,m=this.yAxis,b=[],C=[],_=this.index,E=m.stacking.stacks[this.stackKey],x=p.threshold,S=Math.round(m.getThreshold(p.threshold)),M=$o(p.connectNulls,"percent"===g),I=function($,F,j){let me,ge,q=s[$],Z=g&&E[q.x].points[_],X=q[j+"Null"]||0,ue=q[j+"Cliff"]||0,le=!0;ue||X?(me=(X?Z[0]:Z[1])+ue,ge=Z[0]+ue,le=!!X):!g&&s[F]&&s[F].isNull&&(me=ge=x),void 0!==me&&(C.push({plotX:l,plotY:null===me?S:m.getThreshold(me),isNull:le,isCliff:!0}),b.push({plotX:l,plotY:null===ge?S:m.getThreshold(ge),doCurve:!1}))};s=s||this.points,g&&(s=this.getStackPoints(s));for(let $=0,F=s.length;$1&&g&&C.some($=>$.isCliff)&&(P.hasStackedCliffs=U.hasStackedCliffs=!0),P.xMap=T.xMap,this.areaPath=P,U}getStackPoints(s){let l=this,c=[],d=[],h=this.xAxis,p=this.yAxis,g=p.stacking.stacks[this.stackKey],m={},b=p.series,C=b.length,_=p.options.reversedStacks?1:-1,E=b.indexOf(l);if(s=s||this.points,this.options.stacking){for(let S=0;SS.visible);d.forEach(function(S,M){let T,O,I=0;if(m[S]&&!m[S].isNull)c.push(m[S]),[-1,1].forEach(function(R){let P=1===R?"rightNull":"leftNull",U=g[d[M+R]],$=0;if(U){let F=E;for(;F>=0&&F=0&&RM&&h>b?(h=Math.max(M,b),g=2*b-h):hT&&g>b?(g=Math.max(T,b),h=2*b-g):g1){let h=this.xAxis.series.filter(b=>b.visible).map(b=>b.index),p=0,g=0;v1(this.xAxis.stacking?.stacks,b=>{let C="number"==typeof c.x?b[c.x.toString()]?.points:void 0,_=C?.[this.index],E={};if(C&&Gb(_)){let x=this.index,S=Object.keys(C).filter(M=>!M.match(",")&&C[M]&&C[M].length>1).map(parseFloat).filter(M=>-1!==h.indexOf(M)).filter(M=>{let I=this.chart.series[M].options,T=I.stacking&&I.stack;if(Ub(T)){if(Bh(E[T]))return x===M&&(x=E[T]),!1;E[T]=M}return!0}).sort((M,I)=>I-M);p=S.indexOf(x),g=S.length}}),p=this.xAxis.reversed?g-1-p:p,s=(c.plotX||0)+((g-1)*d.paddedWidth+l)/2-l-p*d.paddedWidth}return s}translate(){let s=this,l=s.chart,c=s.options,d=s.dense=s.closestPointRange*s.xAxis.transA<2,h=s.borderWidth=fu(c.borderWidth,+!d),p=s.xAxis,g=s.yAxis,m=c.threshold,b=fu(c.minPointLength,5),C=s.getColumnMetrics(),_=C.width,E=s.pointXOffset=C.offset,x=s.dataMin,S=s.dataMax,M=s.translatedThreshold=g.getThreshold(m),I=s.barW=Math.max(_,1+2*h);c.pointPadding&&c.crisp&&(I=Math.ceil(I)),Er.prototype.translate.apply(s),s.points.forEach(function(T){let $,O=fu(T.yBottom,M),R=999+Math.abs(O),P=T.plotX||0,U=hu(T.plotY,-R,g.len+R),F=Math.min(U,O),j=Math.max(U,O)-F,q=_,Z=P+E,X=I;b&&Math.abs(j)b?O-b:M-($?b:0)),Ub(T.options.pointWidth)&&(Z-=Math.round(((q=X=Math.ceil(T.options.pointWidth))-_)/2)),c.centerInCategory&&(Z=s.adjustForMissingColumns(Z,q,T,C)),T.barX=Z,T.pointWidth=q,T.tooltipPos=l.inverted?[hu(g.len+g.pos-l.plotLeft-U,g.pos-l.plotLeft,g.len+g.pos-l.plotLeft),p.len+p.pos-l.plotTop-Z-X/2,j]:[p.left-l.plotLeft+Z+X/2,hu(U+g.pos-l.plotTop,g.pos-l.plotTop,g.len+g.pos-l.plotTop),j],T.shapeType=s.pointClass.prototype.shapeType||"roundedRect",T.shapeArgs=s.crispCol(Z,F,X,T.isNull?0:j)}),Wb(this,"afterColumnTranslate")}drawGraph(){this.group[this.dense?"addClass":"removeClass"]("highcharts-dense-data")}pointAttribs(s,l){let g,m,b,c=this.options,d=this.pointAttrToOptions||{},h=d.stroke||"borderColor",p=d["stroke-width"]||"borderWidth",C=s&&s.color||this.color,_=s&&s[h]||c[h]||C,E=s&&s.options.dashStyle||c.dashStyle,x=s&&s[p]||c[p]||this[p]||0,S=s?.isNull&&c.nullInteraction?0:s?.opacity??c.opacity??1;s&&this.zones.length&&(m=s.getZone(),C=s.options.color||m&&(m.color||s.nonZonedColor)||this.color,m&&(_=m.borderColor||_,E=m.dashStyle||E,x=m.borderWidth||x)),l&&s&&(b=(g=cm(c.states[l],s.options.states&&s.options.states[l]||{})).brightness,C=g.color||void 0!==b&&y1(C).brighten(g.brightness).get()||C,_=g[h]||_,x=g[p]||x,E=g.dashStyle||E,S=fu(g.opacity,S));let M={fill:C,stroke:_,"stroke-width":x,opacity:S};return E&&(M.dashstyle=E),M}drawPoints(s=this.points){let l,c=this,d=this.chart,h=c.options,p=h.nullInteraction,g=d.renderer,m=h.animationLimit||250;s.forEach(function(b){let _=b.graphic,E=!!_,x=_&&d.pointCountC?.enabled)}function l(C,_,E,x,S){let{chart:M,enabledDataSorting:I}=this,T=this.isCartesian&&M.inverted,O=C.plotX,R=C.plotY,P=E.rotation||0,U=ya(O)&&ya(R)&&M.isInsidePlot(O,Math.round(R),{inverted:T,paneCoordinates:!0,series:this}),$=0===P&&"justify"===Dl(E.overflow,I?"none":"justify"),F=this.visible&&!1!==C.visible&&ya(O)&&(C.series.forceDL||I&&!$||U||Dl(E.inside,!!this.options.stacking)&&x&&M.isInsidePlot(O,T?x.x+1:x.y+x.height-1,{inverted:T,paneCoordinates:!0,series:this})),j=C.pos();if(F&&j){let Z=_.getBBox(),X=_.getBBox(void 0,0);if(x=Hh({x:j[0],y:Math.round(j[1]),width:0,height:0},x||{}),"plotEdges"===E.alignTo&&this.isCartesian&&(x[T?"x":"y"]=0,x[T?"width":"height"]=this.yAxis?.len||0),Hh(E,{width:Z.width,height:Z.height}),I&&this.xAxis&&!$&&this.setDataLabelStartPos(C,_,S,U,x),_.align(nn(E,{width:X.width,height:X.height}),!1,x,!1),_.alignAttr.x+=qb(E.align)*(X.width-Z.width),_.alignAttr.y+=qb(E.verticalAlign)*(X.height-Z.height),_[_.placed?"animate":"attr"]({"text-align":_.alignAttr["text-align"]||"center",x:_.alignAttr.x+(Z.width-X.width)/2,y:_.alignAttr.y+(Z.height-X.height)/2,rotationOriginX:(_.width||0)/2,rotationOriginY:(_.height||0)/2}),$&&x.height>=0)this.justifyDataLabel(_,E,_.alignAttr,Z,x,S);else if(Dl(E.crop,!0)){let{x:ue,y:me}=_.alignAttr;F=M.isInsidePlot(ue,me,{paneCoordinates:!0,series:this})&&M.isInsidePlot(ue+Z.width-1,me+Z.height-1,{paneCoordinates:!0,series:this})}E.shape&&!P&&_[S?"attr":"animate"]({anchorX:j[0],anchorY:j[1]})}S&&I&&(_.placed=!1),F||I&&!$?(_.show(),_.placed=!0):(_.hide(),_.placed=!1)}function c(){return this.plotGroup("dataLabelsGroup","data-labels",this.hasRendered?"inherit":"hidden",this.options.dataLabels.zIndex||6,this.chart.dataLabelsGroup)}function d(C){let _=this.hasRendered||0,E=this.initDataLabelsGroup().attr({opacity:+_});return!_&&E&&(this.visible&&E.show(),this.options.animation?E.animate({opacity:1},C):E.attr({opacity:1})),E}function h(C){let _;C=C||this.points;let E=this,x=E.chart,S=E.options,M=x.renderer,{backgroundColor:I,plotBackgroundColor:T}=x.options.chart,O=M.getContrast(ba(T)&&T||ba(I)&&I||"#000000"),R=m(E),{animation:P,defer:U}=R[0],$=U?b1(x,P,E):{defer:0,duration:0};va(this,"drawDataLabels"),E.hasDataLabels?.()&&(_=this.initDataLabels($),C.forEach(F=>{let j=F.dataLabels||[],q=F.color||E.color;Ai(g(R,F.dlOptions||F.options?.dataLabels)).forEach((X,ue)=>{let mt,qe,tr,Fe,me=X.enabled&&(F.visible||F.dataLabelOnHidden)&&(!F.isNull||F.dataLabelOnNull)&&function(yt,vt){let Xe=vt.filter;if(Xe){let St=Xe.operator,nr=yt[Xe.property],pr=Xe.value;return">"===St&&nr>pr||"<"===St&&nr="===St&&nr>=pr||"<="===St&&nr<=pr||"=="===St&&nr==pr||"==="===St&&nr===pr||"!="===St&&nr!=pr||"!=="===St&&nr!==pr||!1}return!0}(F,X),{backgroundColor:ge,borderColor:le,distance:ne,style:fe={}}=X,Ie={},De=j[ue],Qe=!De;me&&(qe=ya(mt=Dl(X[F.formatPrefix+"Format"],X.format))?jh(mt,F,x):(X[F.formatPrefix+"Formatter"]||X.formatter).call(F,X),tr=X.rotation,!x.styledMode&&(fe.color=Dl(X.color,fe.color,ba(E.color)?E.color:void 0,"#000000"),"contrast"===fe.color?("none"!==ge&&(Fe=ge),F.contrastColor=M.getContrast("auto"!==Fe&&ba(Fe)&&Fe||(ba(q)?q:"")),fe.color=Fe||!ya(ne)&&X.inside||0>tt(ne||0)||S.stacking?F.contrastColor:O):delete F.contrastColor,S.cursor&&(fe.cursor=S.cursor)),Ie={r:X.borderRadius||0,rotation:tr,padding:X.padding,zIndex:1},x.styledMode||(Ie.fill="auto"===ge?F.color:ge,Ie.stroke="auto"===le?F.color:le,Ie["stroke-width"]=X.borderWidth),mn(Ie,(yt,vt)=>{void 0===yt&&delete Ie[vt]})),!De||me&&ya(qe)&&!(!De.div&&!De.text?.foreignObject)==!!X.useHTML&&(De.rotation&&X.rotation||De.rotation===X.rotation)||(De=void 0,Qe=!0),me&&ya(qe)&&""!==qe&&(De?Ie.text=qe:(De=M.label(qe,0,0,X.shape,void 0,void 0,X.useHTML,void 0,"data-label")).addClass(" highcharts-data-label-color-"+F.colorIndex+" "+(X.className||"")+(X.useHTML?" highcharts-tracker":"")),De&&(De.options=X,De.attr(Ie),x.styledMode?fe.width&&De.css({width:fe.width,textOverflow:fe.textOverflow,whiteSpace:fe.whiteSpace}):De.css(fe).shadow(X.shadow),va(De,"beforeAddingDataLabel",{labelOptions:X,point:F}),De.added||De.add(_),E.alignDataLabel(F,De,X,void 0,Qe),De.isActive=!0,j[ue]&&j[ue]!==De&&j[ue].destroy(),j[ue]=De))});let Z=j.length;for(;Z--;)j[Z]?.isActive?j[Z].isActive=!1:(j[Z]?.destroy(),j.splice(Z,1));F.dataLabel=j[0],F.dataLabels=j})),va(this,"afterDrawDataLabels")}function p(C,_,E,x,S,M){let Z,X,I=this.chart,T=_.align,O=_.verticalAlign,R=C.box?0:C.padding||0,P=I.inverted?this.yAxis:this.xAxis,U=P?P.left-I.plotLeft:0,$=I.inverted?this.xAxis:this.yAxis,F=$?$.top-I.plotTop:0,{x:j=0,y:q=0}=_;return(Z=(E.x||0)+R+U)<0&&("right"===T&&j>=0?(_.align="left",_.inside=!0):j-=Z,X=!0),(Z=(E.x||0)+x.width-R+U)>I.plotWidth&&("left"===T&&j<=0?(_.align="right",_.inside=!0):j+=I.plotWidth-Z,X=!0),(Z=E.y+R+F)<0&&("bottom"===O&&q>=0?(_.verticalAlign="top",_.inside=!0):q-=Z,X=!0),(Z=(E.y||0)+x.height-R+F)>I.plotHeight&&("top"===O&&q<=0?(_.verticalAlign="bottom",_.inside=!0):q+=I.plotHeight-Z,X=!0),X&&(_.x=j,_.y=q,C.placed=!M,C.align(_,void 0,S)),X}function g(C,_){let x,E=[];if(Ms(C)&&!Ms(_))E=C.map(function(S){return nn(S,_)});else if(Ms(_)&&!Ms(C))E=_.map(function(S){return nn(C,S)});else if(Ms(C)||Ms(_)){if(Ms(C)&&Ms(_))for(x=Math.max(C.length,_.length);x--;)E[x]=nn(C[x],_[x])}else E=nn(C,_);return E}function m(C){let _=C.chart.options.plotOptions;return Ai(g(g(_?.series?.dataLabels,_?.[C.type]?.dataLabels),C.options.dataLabels))}function b(C,_,E,x,S){let M=this.chart,I=M.inverted,T=this.xAxis,O=T.reversed,R=((I?_.height:_.width)||0)/2,P=C.pointWidth,U=P?P/2:0;_.startXPos=I?S.x:O?-R-U:T.width-R+U,_.startYPos=I?O?this.yAxis.height-R+U:-R-U:S.y,x?"hidden"===_.visibility&&(_.show(),_.attr({opacity:0}).animate({opacity:1})):_.attr({opacity:1}).animate({opacity:0},void 0,_.hide),M.hasRendered&&(E&&_.attr({x:_.startXPos,y:_.startYPos}),_.placed=!0)}v.compose=function(C){let _=C.prototype;_.initDataLabels||(_.initDataLabels=d,_.initDataLabelsGroup=c,_.alignDataLabel=l,_.drawDataLabels=h,_.justifyDataLabel=p,_.mergeArrays=g,_.setDataLabelStartPos=b,_.hasDataLabels=s)}}(el||(el={}));let uo=el,{composed:_a}=Q,{series:Ts}=Bt,{merge:um,pushUnique:Uh}=we;!function(v){function s(l,c,d,h,p){let{chart:g,options:m}=this,b=g.inverted,C=this.xAxis?.len||g.plotSizeX||0,_=this.yAxis?.len||g.plotSizeY||0,E=l.dlBox||l.shapeArgs,x=l.below??(l.plotY||0)>(this.translatedThreshold??_),S=d.inside??!!m.stacking;if(E){if(h=um(E),"allow"!==d.overflow||!1!==d.crop||!1!==m.clip){h.y<0&&(h.height+=h.y,h.y=0);let M=h.y+h.height-_;M>0&&M\u25cf {series.name}
',pointFormat:"x: {point.x}
y: {point.y}
"}}),Vr(pu.prototype,{drawTracker:Xb.prototype.drawTracker,sorted:!1,requireSorting:!1,noSharedTooltip:!0,trackerGroups:["group","markerGroup","dataLabelsGroup"]}),Zb(pu,"afterTranslate",function(){this.applyJitter()}),Bt.registerSeriesType("scatter",pu);let{deg2rad:Ca}=Q,{fireEvent:Kb,isNumber:Wh,pick:gu,relativeLength:Qb}=we;!function(v){v.getCenter=function(){let b,E,x,s=this.options,l=this.chart,c=2*(s.slicedOffset||0),d=l.plotWidth-2*c,h=l.plotHeight-2*c,p=s.center,g=Math.min(d,h),m=s.thickness,C=s.size,_=s.innerSize||0;"string"==typeof C&&(C=parseFloat(C)),"string"==typeof _&&(_=parseFloat(_));let S=[gu(p?.[0],"50%"),gu(p?.[1],"50%"),gu(C&&C<0?void 0:s.size,"100%"),gu(_&&_<0?void 0:s.innerSize||0,"0%")];for(!l.angular||this instanceof Er||(S[3]=0),E=0;E<4;++E)x=S[E],b=E<2||2===E&&/%$/.test(x),S[E]=Qb(x,[d,h,g,S[2]][E])+(b?c:0);return S[3]>S[2]&&(S[3]=S[2]),Wh(m)&&2*m0&&(S[3]=S[2]-2*m),Kb(this,"afterGetCenter",{positions:S}),S},v.getStartAndEndRadians=function(s,l){let c=Wh(s)?s:0,d=Wh(l)&&l>c&&l-c<360?l:c+360;return{start:Ca*(c+-90),end:Ca*(d+-90)}}}(Eo||(Eo={}));let fm=Eo,{setAnimation:Jb}=G,{addEvent:mu,defined:yu,extend:e_,isNumber:pm,pick:si,relativeLength:gm}=we;class mm extends so{getConnectorPath(s){let l=s.dataLabelPosition,c=s.options||{},d=c.connectorShape;return l&&(this.connectorShapes[d]||d).call(this,{...l.computed,alignment:l.alignment},l.connectorPosition,c)||[]}getTranslate(){return this.sliced&&this.slicedTranslation||{translateX:0,translateY:0}}haloPath(s){let l=this.shapeArgs;return this.sliced||!this.visible?[]:this.series.chart.renderer.symbols.arc(l.x,l.y,l.r+s,l.r+s,{innerR:l.r-1,start:l.start,end:l.end,borderRadius:l.borderRadius})}constructor(s,l,c){super(s,l,c),this.half=0,this.name??(this.name=s.chart.options.lang.pieSliceName);let d=h=>{this.slice("select"===h.type)};mu(this,"select",d),mu(this,"unselect",d)}isValid(){return pm(this.y)&&this.y>=0}setVisible(s,l=!0){s!==this.visible&&this.update({visible:s??!this.visible},l,void 0,!1)}slice(s,l,c){let d=this.series;Jb(c,d.chart),l=si(l,!0),this.sliced=this.options.sliced=s=yu(s)?s:!this.sliced,d.options.data[d.data.indexOf(this)]=this.options,this.graphic&&this.graphic.animate(this.getTranslate())}}e_(mm.prototype,{connectorShapes:{fixedOffset:function(v,s,l){let c=s.breakAt,d=s.touchingSliceAt;return[["M",v.x,v.y],l.softConnector?["C",v.x+("left"===v.alignment?-5:5),v.y,2*c.x-d.x,2*c.y-d.y,c.x,c.y]:["L",c.x,c.y],["L",d.x,d.y]]},straight:function(v,s){let l=s.touchingSliceAt;return[["M",v.x,v.y],["L",l.x,l.y]]},crookedLine:function(v,s,l){let{angle:c=this.angle||0,breakAt:d,touchingSliceAt:h}=s,{series:p}=this,[g,m,b]=p.center,C=b/2,{plotLeft:_,plotWidth:E}=p.chart,x="left"===v.alignment,{x:S,y:M}=v,I=d.x;if(l.crookDistance){let O=gm(l.crookDistance,1);I=x?g+C+(E+_-g-C)*(1-O):_+(g-C)*O}else I=g+(m-M)*Math.tan(c-Math.PI/2);let T=[["M",S,M]];return(x?I<=S&&I>=d.x:I>=S&&I<=d.x)&&T.push(["L",I,M]),T.push(["L",d.x,d.y],["L",h.x,h.y]),T}}});let{getStartAndEndRadians:t_}=fm,{noop:Gh}=Q,{clamp:ym,extend:n_,fireEvent:jo,merge:qh,pick:r_}=we;class Yh extends Er{animate(s){let l=this,d=l.startAngleRad;s||l.points.forEach(function(h){let p=h.graphic,g=h.shapeArgs;p&&g&&(p.attr({r:r_(h.startR,l.center&&l.center[3]/2),start:d,end:d}),p.animate({r:g.r,start:g.start,end:g.end},l.options.animation))})}drawEmpty(){let s,l,c=this.startAngleRad,d=this.endAngleRad,h=this.options;0===this.total&&this.center?(s=this.center[0],l=this.center[1],this.graph||(this.graph=this.chart.renderer.arc(s,l,this.center[1]/2,0,c,d).addClass("highcharts-empty-series").add(this.group)),this.graph.attr({d:Yd.arc(s,l,this.center[2]/2,0,{start:c,end:d,innerR:this.center[3]/2})}),this.chart.styledMode||this.graph.attr({"stroke-width":h.borderWidth,fill:h.fillColor||"none",stroke:h.color||"#cccccc"})):this.graph&&(this.graph=this.graph.destroy())}drawPoints(){let s=this.chart.renderer;this.points.forEach(function(l){l.graphic&&l.hasNewShapeType()&&(l.graphic=l.graphic.destroy()),l.graphic||(l.graphic=s[l.shapeType](l.shapeArgs).add(l.series.group),l.delayedRendering=!0)})}generatePoints(){super.generatePoints(),this.updateTotals()}getX(s,l,c,d){let h=this.center,p=this.radii?this.radii[c.index]||0:h[2]/2,g=d.dataLabelPosition,m=g?.distance||0,b=Math.asin(ym((s-h[1])/(p+m),-1,1));return h[0]+Math.cos(b)*(p+m)*(l?-1:1)+(m>0?(l?-1:1)*(d.padding||0):0)}hasData(){return this.points.some(s=>s.visible)}redrawPoints(){let s,l,c,d,h=this,p=h.chart;this.drawEmpty(),h.group&&!p.styledMode&&h.group.shadow(h.options.shadow),h.points.forEach(function(g){let m={};l=g.graphic,!g.isNull&&l?(d=g.shapeArgs,s=g.getTranslate(),p.styledMode||(c=h.pointAttribs(g,g.selected&&"select")),g.delayedRendering?(l.setRadialReference(h.center).attr(d).attr(s),p.styledMode||l.attr(c).attr({"stroke-linejoin":"round"}),g.delayedRendering=!1):(l.setRadialReference(h.center),p.styledMode||qh(!0,m,c),qh(!0,m,d,s),l.animate(m)),l.attr({visibility:g.visible?"inherit":"hidden"}),l.addClass(g.getClassName(),!0)):l&&(g.graphic=l.destroy())})}sortByAngle(s,l){s.sort(function(c,d){return void 0!==c.angle&&(d.angle-c.angle)*l})}translate(s){jo(this,"translate"),this.generatePoints();let C,_,E,x,S,M,I,l=this.options,c=l.slicedOffset,d=t_(l.startAngle,l.endAngle),h=this.startAngleRad=d.start,p=(this.endAngleRad=d.end)-h,g=this.points,m=l.ignoreHiddenPoint,b=g.length,T=0;for(s||(this.center=s=this.getCenter()),M=0;M1.5*Math.PI?E-=2*Math.PI:E<-Math.PI/2&&(E+=2*Math.PI),I.slicedTranslation={translateX:Math.round(Math.cos(E)*c),translateY:Math.round(Math.sin(E)*c)},x=Math.cos(E)*s[2]/2,S=Math.sin(E)*s[2]/2,I.tooltipPos=[s[0]+.7*x,s[1]+.7*S],I.half=+(E<-Math.PI/2||E>Math.PI/2),I.angle=E}jo(this,"afterTranslate")}updateTotals(){let d,h,s=this.points,l=s.length,c=this.options.ignoreHiddenPoint,p=0;for(d=0;d0&&(h.visible||!c)?h.y/p*100:0,h.total=p}}Yh.defaultOptions=qh(Er.defaultOptions,{borderRadius:3,center:[null,null],clip:!1,colorByPoint:!0,dataLabels:{connectorPadding:5,connectorShape:"crookedLine",crookDistance:void 0,distance:30,enabled:!0,formatter:function(){return this.isNull?void 0:this.name},softConnector:!0,x:0},fillColor:void 0,ignoreHiddenPoint:!0,inactiveOtherPoints:!0,legendType:"point",marker:null,size:null,showInLegend:!1,slicedOffset:10,stickyTracking:!1,tooltip:{followPointer:!0},borderColor:"#ffffff",borderWidth:1,lineWidth:void 0,states:{hover:{brightness:.1}}}),n_(Yh.prototype,{axisTypes:[],directTouch:!0,drawGraph:void 0,drawTracker:$h.prototype.drawTracker,getCenter:fm.getCenter,getSymbol:Gh,invertible:!1,isCartesian:!1,noSharedTooltip:!0,pointAttribs:$h.prototype.pointAttribs,pointClass:mm,requireSorting:!1,searchPoint:Gh,trackerGroups:["group","dataLabelsGroup"]}),Bt.registerSeriesType("pie",Yh);let{composed:ht,noop:i_}=Q,{distribute:vm}=Wi,{series:xl}=Bt,{arrayMax:vu,clamp:bu,defined:Xh,isNumber:bm,pick:o_,pushUnique:Ea,relativeLength:Da}=we;!function(v){let s={radialDistributionY:function(p,g){return(g.dataLabelPosition?.top||0)+p.distributeBox.pos},radialDistributionX:function(p,g,m,b,C){let _=C.dataLabelPosition;return p.getX(m<(_?.top||0)+2||m>(_?.bottom||0)-2?b:m,g.half,g,C)},justify:function(p,g,m,b){return b[0]+(p.half?-1:1)*(m+(g.dataLabelPosition?.distance||0))},alignToPlotEdges:function(p,g,m,b){let C=p.getBBox().width;return g?C+b:m-C-b},alignToConnectors:function(p,g,m,b){let _,C=0;return p.forEach(function(E){(_=E.dataLabel.getBBox().width)>C&&(C=_)}),g?C+b:m-C-b}};function l(p,g){let m=Math.PI/2,{start:b=0,end:C=0}=p.shapeArgs||{},_=p.angle||0;g>0&&bm&&_>m/2&&_<1.5*m&&(_=_<=m?Math.max(m/2,(b+m)/2):Math.min(1.5*m,(m+C)/2));let{center:E,options:x}=this,S=E[2]/2,M=Math.cos(_),I=Math.sin(_),T=E[0]+M*S,O=E[1]+I*S,R=Math.min((x.slicedOffset||0)+(x.borderWidth||0),g/5);return{natural:{x:T+M*g,y:O+I*g},computed:{},alignment:g<0?"center":p.half?"right":"left",connectorPosition:{angle:_,breakAt:{x:T+M*R,y:O+I*R},touchingSliceAt:{x:T,y:O}},distance:g}}function c(){let R,P,U,p=this,g=p.points,m=p.chart,b=m.plotWidth,C=m.plotHeight,_=m.plotLeft,E=Math.round(m.chartWidth/3),x=p.center,S=x[2]/2,M=x[1],I=[[],[]],T=[0,0,0,0],O=p.dataLabelPositioners,$=0;p.visible&&p.hasDataLabels?.()&&(g.forEach(F=>{(F.dataLabels||[]).forEach(j=>{j.shortened&&(j.attr({width:"auto"}).css({width:"auto",textOverflow:"clip"}),j.shortened=!1)})}),xl.prototype.drawDataLabels.apply(p),g.forEach(F=>{(F.dataLabels||[]).forEach((j,q)=>{let Z=x[2]/2,X=j.options,ue=Da(X?.distance||0,Z);0===q&&I[F.half].push(F),!Xh(X?.style?.width)&&j.getBBox().width>E&&(j.css({width:Math.round(.7*E)+"px"}),j.shortened=!0),j.dataLabelPosition=this.getDataLabelPosition(F,ue),$=Math.max($,ue)})}),I.forEach((F,j)=>{let X,ue,ge,Z=[],me=0;F.length&&(p.sortByAngle(F,j-.5),$>0&&(X=Math.max(0,M-S-$),ue=Math.min(M+S+$,m.plotHeight),F.forEach(le=>{(le.dataLabels||[]).forEach(ne=>{let fe=ne.dataLabelPosition;fe&&fe.distance>0&&(fe.top=Math.max(0,M-S-fe.distance),fe.bottom=Math.min(M+S+fe.distance,m.plotHeight),me=ne.getBBox().height||21,ne.lineHeight=m.renderer.fontMetrics(ne.text||ne).h+2*ne.padding,le.distributeBox={target:(ne.dataLabelPosition?.natural.y||0)-fe.top+ne.lineHeight/2,size:me,rank:le.y},Z.push(le.distributeBox))})}),vm(Z,ge=ue+me-X,ge/5)),F.forEach(le=>{(le.dataLabels||[]).forEach(ne=>{let fe=ne.options||{},mt=le.distributeBox,qe=ne.dataLabelPosition,tr=qe?.natural.y||0,Ie=fe.connectorPadding||0,De=ne.lineHeight||21,Qe=(De-ne.getBBox().height)/2,Fe=0,yt=tr,vt="inherit";if(qe){if(Z&&Xh(mt)&&qe.distance>0&&(void 0===mt.pos?vt="hidden":(U=mt.size,yt=O.radialDistributionY(le,ne))),fe.justify)Fe=O.justify(le,ne,S,x);else switch(fe.alignTo){case"connectors":Fe=O.alignToConnectors(F,j,b,_);break;case"plotEdges":Fe=O.alignToPlotEdges(ne,j,b,_);break;default:Fe=O.radialDistributionX(p,le,yt-Qe,tr,ne)}if(qe.attribs={visibility:vt,align:qe.alignment},qe.posAttribs={x:Fe+(fe.x||0)+({left:Ie,right:-Ie}[qe.alignment]||0),y:yt+(fe.y||0)-De/2},qe.computed.x=Fe,qe.computed.y=yt-Qe,o_(fe.crop,!0)){let Xe;Fe-(P=ne.getBBox().width)b-Ie&&0===j&&(Xe=Math.round(Fe+P-b+Ie),T[1]=Math.max(Xe,T[1])),yt-U/2<0?T[0]=Math.max(Math.round(U/2-yt),T[0]):yt+U/2>C&&(T[2]=Math.max(Math.round(yt+U/2-C),T[2])),qe.sideOverflow=Xe}}})}))}),(0===vu(T)||this.verifyDataLabelOverflow(T))&&(this.placeDataLabels(),this.points.forEach(F=>{(F.dataLabels||[]).forEach(j=>{let{connectorColor:q,connectorWidth:Z=1}=j.options||{},X=j.dataLabelPosition;if(bm(Z)){let ue;R=j.connector,X&&X.distance>0?(ue=!R,R||(j.connector=R=m.renderer.path().addClass("highcharts-data-label-connector highcharts-color-"+F.colorIndex+(F.className?" "+F.className:"")).add(p.dataLabelsGroup)),m.styledMode||R.attr({"stroke-width":Z,stroke:q||F.color||"#666666"}),R[ue?"attr":"animate"]({d:F.getConnectorPath(j)}),R.attr({visibility:X.attribs?.visibility})):R&&(j.connector=R.destroy())}})})))}function d(){this.points.forEach(p=>{(p.dataLabels||[]).forEach(g=>{let m=g.dataLabelPosition;m?(m.sideOverflow&&(g.css({width:Math.max(g.getBBox().width-m.sideOverflow,0)+"px",textOverflow:g.options?.style?.textOverflow||"ellipsis"}),g.shortened=!0),g.attr(m.attribs),g[g.moved?"animate":"attr"](m.posAttribs),g.moved=!0):g&&g.attr({y:-9999})}),delete p.distributeBox},this)}function h(p){let g=this.center,m=this.options,b=m.center,C=m.minSize||80,_=C,E=null!==m.size;return!E&&(null!==b[0]?_=Math.max(g[2]-Math.max(p[1],p[3]),C):(_=Math.max(g[2]-p[1]-p[3],C),g[0]+=(p[3]-p[1])/2),null!==b[1]?_=bu(_,C,g[2]-Math.max(p[0],p[2])):(_=bu(_,C,g[2]-p[0]-p[2]),g[1]+=(p[0]-p[2])/2),_(c.x+=d.x,c.y+=d.y,c),{x:0,y:0});return{x:l.x/s.length,y:l.y/s.length}},v.getDistanceBetweenPoints=function(s,l){return Math.sqrt(Math.pow(l.x-s.x,2)+Math.pow(l.y-s.y,2))},v.getAngleBetweenPoints=function(s,l){return Math.atan2(l.x-s.x,l.y-s.y)},v.pointInPolygon=function({x:s,y:l},c){let h,p,d=c.length,g=!1;for(h=0,p=d-1;hl!=_>l&&s<(C-m)*(l-b)/(_-b)+m&&(g=!g)}return g}}(Td||(Td={}));let{pointInPolygon:_m}=Td,{addEvent:w1,getAlignFactor:s_,fireEvent:wm,objectEach:a_,pick:C1}=we;function l_(v){let d,h,p,g,m,s=v.length,l=(C,_)=>!(_.x>=C.x+C.width||_.x+_.width<=C.x||_.y>=C.y+C.height||_.y+_.height<=C.y),c=(C,_)=>{for(let E of C)if(_m({x:E[0],y:E[1]},_))return!0;return!1},b=!1;for(let C=0;C(_.labelrank||0)-(C.labelrank||0));for(let C=0;C{a_(c,d=>{d.label&&s.push(d.label)})});for(let l of v.series||[])if(l.visible&&l.hasDataLabels?.()){let c=d=>{for(let h of d)h.visible&&(h.dataLabels||[]).forEach(p=>{let g=p.options||{};p.labelrank=C1(g.labelrank,h.labelrank,h.shapeArgs?.height),g.allowOverlap??Number(g.distance)>0?(p.oldOpacity=p.opacity,p.newOpacity=1,Kh(p,v)):s.push(p)})};c(l.nodes||[]),c(l.points)}this.hideOverlappingLabels(s)}let Is={compose:function(v){let s=v.prototype;s.hideOverlappingLabels||(s.hideOverlappingLabels=l_,w1(v,"render",Cm))}},{defaultOptions:Em}=cr,{noop:_u}=Q,{addEvent:xa,extend:c_,isObject:Sa,merge:Qh,relativeLength:Ma}=we,E1={radius:0,scope:"stack",where:void 0},Jh=_u,ef=_u;function u_(v,s,l,c,d={}){let h=Jh(v,s,l,c,d),{brStart:p=!0,brEnd:g=!0,innerR:m=0,r:b=l,start:C=0,end:_=0}=d;if(d.open||!d.borderRadius)return h;let E=_-C,x=Math.sin(E/2),S=Math.max(Math.min(Ma(d.borderRadius||0,b-m),(b-m)/2,b*x/(1+x)),0),M=Math.min(S,E/Math.PI*2*m),I=h.length-1;for(;I--;)(p||0!==I&&3!==I)&&(g||1!==I&&2!==I)&&function(T,O,R){let P,U,$,F=T[O],j=T[O+1];if("Z"===j[0]&&(j=T[0]),"M"!==F[0]&&"L"!==F[0]||"A"!==j[0]?"A"===F[0]&&("M"===j[0]||"L"===j[0])&&(P=j,U=F):(P=F,U=j,$=!0),P&&U&&U.params){let q=U[1],Z=U[5],X=U.params,{start:ue,end:me,cx:ge,cy:le}=X,ne=Z?q-R:q+R,fe=ne?Math.asin(R/ne):0,mt=Z?fe:-fe,qe=Math.cos(fe)*ne;$?(X.start=ue+mt,P[1]=ge+qe*Math.cos(ue),P[2]=le+qe*Math.sin(ue),T.splice(O+1,0,["A",R,R,0,0,1,ge+q*Math.cos(X.start),le+q*Math.sin(X.start)])):(X.end=me-mt,U[6]=ge+q*Math.cos(X.end),U[7]=le+q*Math.sin(X.end),T.splice(O+1,0,["A",R,R,0,0,1,ge+qe*Math.cos(me),le+qe*Math.sin(me)])),U[4]=Math.abs(X.end-X.start)1?M:S);return h}function ki(){if(this.options.borderRadius&&(!this.chart.is3d||!this.chart.is3d())){let{options:v,yAxis:s}=this,l="percent"===v.stacking,c=Em.plotOptions?.[this.type]?.borderRadius,d=tf(v.borderRadius,Sa(c)?c:{}),h=s.options.reversed;for(let p of this.points){let{shapeArgs:g}=p;if("roundedRect"===p.shapeType&&g){let{width:m=0,height:b=0,y:C=0}=g,_=C,E=b;if("stack"===d.scope&&p.stackTotal){let I=s.translate(l?100:p.stackTotal,!1,!0,!1,!0),T=s.translate(v.threshold||0,!1,!0,!1,!0),O=this.crispCol(0,Math.min(I,T),0,Math.abs(I-T));_=O.y,E=O.height}let x=(p.negative?-1:1)*(h?-1:1)==-1,S=d.where;!S&&this.is("waterfall")&&Math.abs((p.yBottom||0)-(this.translatedThreshold||0))>this.borderWidth&&(S="all"),S||(S="end");let M=Math.min(Ma(d.radius,m),m/2,"all"===S?b/2:1/0)||0;"end"===S&&(x&&(_-=M),E+=M),c_(g,{brBoxHeight:E,brBoxY:_,r:M})}}}}function tf(v,s){return Sa(v)||(v={radius:v||0}),Qh(E1,s,v)}function wu(){let v=tf(this.options.borderRadius);for(let s of this.points){let l=s.shapeArgs;l&&(l.borderRadius=Ma(v.radius,(l.r||0)-(l.innerR||0)))}}function nf(v,s,l,c,d={}){let h=ef(v,s,l,c,d),{r:p=0,brBoxHeight:g=c,brBoxY:m=s}=d,b=s-m,C=m+g-(s+c),_=b-p>-.1?0:p,E=C-p>-.1?0:p,x=Math.max(_&&b,0),S=Math.max(E&&C,0),M=[v+_,s],I=[v+l-_,s],T=[v+l,s+_],O=[v+l,s+c-E],R=[v+l-E,s+c],P=[v+E,s+c],U=[v,s+c-E],$=[v,s+_],F=(j,q)=>Math.sqrt(Math.pow(j,2)-Math.pow(q,2));if(x){let j=F(_,_-x);M[0]-=j,I[0]+=j,T[1]=$[1]=s+_-x}if(c<_-x){let j=F(_,_-x-c);T[0]=O[0]=v+l-_+j,R[0]=Math.min(T[0],R[0]),P[0]=Math.max(O[0],P[0]),U[0]=$[0]=v+_-j,T[1]=$[1]=s+c}if(S){let j=F(E,E-S);R[0]+=j,P[0]-=j,O[1]=U[1]=s+c-E+S}if(c=Sl(h.minWidth,0)&&this.chartHeight>=Sl(h.minHeight,0)}).call(this)&&d.push(c._id)}function l(c,d){let m,h=this.options.responsive,p=this.currentResponsive,g=[];!d&&h&&h.rules&&h.rules.forEach(_=>{void 0===_._id&&(_._id=xm()),this.matchResponsiveRule(_,g)},this);let b=d_(...g.map(_=>rf(h?.rules||[],E=>E._id===_)).map(_=>_?.chartOptions));b.isResponsiveOptions=!0,g=g.toString()||void 0;let C=p?.ruleIds;g!==C&&(p&&(this.currentResponsive=void 0,this.updatingResponsive=!0,this.update(p.undoOptions,c,!0),this.updatingResponsive=!1),g?((m=Dm(b,this.options,!0,this.collectionsWithUpdate)).isResponsiveOptions=!0,this.currentResponsive={ruleIds:g,mergedOptions:b,undoOptions:m},this.updatingResponsive||this.update(b,c,!0)):this.currentResponsive=void 0)}v.compose=function(c){let d=c.prototype;return d.matchResponsiveRule||Cu(d,{matchResponsiveRule:s,setResponsive:l}),c}}(ss||(ss={}));let Sm=ss;Q.AST=bt,Q.Axis=fl,Q.Chart=Vo,Q.Color=Ot,Q.DataLabel=uo,Q.DataTableCore=Fr,Q.Fx=Ao,Q.HTMLElement=ys,Q.Legend=Sh,Q.LegendSymbol=Ig,Q.OverlappingDataLabels=Q.OverlappingDataLabels||Is,Q.PlotLineOrBand=sh,Q.Point=so,Q.Pointer=fb,Q.RendererRegistry=ol,Q.Series=Er,Q.SeriesRegistry=Bt,Q.StackItem=Qg,Q.SVGElement=wi,Q.SVGRenderer=En,Q.Templating=Qr,Q.Tick=xn,Q.Time=Fd,Q.Tooltip=wg,Q.animate=G.animate,Q.animObject=G.animObject,Q.chart=Vo.chart,Q.color=Ot.parse,Q.dateFormat=Qr.dateFormat,Q.defaultOptions=cr.defaultOptions,Q.distribute=Wi.distribute,Q.format=Qr.format,Q.getDeferredAnimation=G.getDeferredAnimation,Q.getOptions=cr.getOptions,Q.numberFormat=Qr.numberFormat,Q.seriesType=Bt.seriesType,Q.setAnimation=G.setAnimation,Q.setOptions=cr.setOptions,Q.stop=G.stop,Q.time=cr.defaultTime,Q.timers=Ao.timers,function(v,s,l){let c=v.types.pie;if(!s.symbolCustomAttribs.includes("borderRadius")){let d=l.prototype.symbols;xa(v,"afterColumnTranslate",ki,{order:9}),xa(c,"afterTranslate",wu),s.symbolCustomAttribs.push("borderRadius","brBoxHeight","brBoxY","brEnd","brStart"),Jh=d.arc,ef=d.roundedRect,d.arc=u_,d.roundedRect=nf}}(Q.Series,Q.SVGElement,Q.SVGRenderer),wa.compose(Q.Series.types.column),uo.compose(Q.Series),Ft.compose(Q.Axis),ys.compose(Q.SVGRenderer),Sh.compose(Q.Chart),eo.compose(Q.Axis),Is.compose(Q.Chart),Zh.compose(Q.Series.types.pie),sh.compose(Q.Chart,Q.Axis),fb.compose(Q.Chart),Sm.compose(Q.Chart),Xg.compose(Q.Axis,Q.Chart,Q.Series),nm.compose(Q.Axis,Q.Chart,Q.Series),wg.compose(Q.Pointer),we.extend(Q,we);let Mm=Q;return Id.default})(),(ze=typeof window>"u"?this:window)._Highcharts=Ce(),oe.exports=ze._Highcharts},467:(oe,ze,Ce)=>{"use strict";function Ge(kt,We,sn,Ye,Ht,Ct,Qt){try{var rt=kt[Ct](Qt),ye=rt.value}catch(yi){return void sn(yi)}rt.done?We(ye):Promise.resolve(ye).then(Ye,Ht)}function _n(kt){return function(){var We=this,sn=arguments;return new Promise(function(Ye,Ht){var Ct=kt.apply(We,sn);function Qt(ye){Ge(Ct,Ye,Ht,Qt,rt,"next",ye)}function rt(ye){Ge(Ct,Ye,Ht,Qt,rt,"throw",ye)}Qt(void 0)})}}Ce.d(ze,{A:()=>_n})}},Sx={};function Se(oe){var ze=Sx[oe];if(void 0!==ze)return ze.exports;var Ce=Sx[oe]={exports:{}};return xx[oe].call(Ce.exports,Ce,Ce.exports,Se),Ce.exports}Se.m=xx,oe=Object.getPrototypeOf?Ce=>Object.getPrototypeOf(Ce):Ce=>Ce.__proto__,Se.t=function(Ce,Ge){if(1&Ge&&(Ce=this(Ce)),8&Ge||"object"==typeof Ce&&Ce&&(4&Ge&&Ce.__esModule||16&Ge&&"function"==typeof Ce.then))return Ce;var _n=Object.create(null);Se.r(_n);var kt={};ze=ze||[null,oe({}),oe([]),oe(oe)];for(var We=2&Ge&&Ce;("object"==typeof We||"function"==typeof We)&&!~ze.indexOf(We);We=oe(We))Object.getOwnPropertyNames(We).forEach(sn=>kt[sn]=()=>Ce[sn]);return kt.default=()=>Ce,Se.d(_n,kt),_n},Se.d=(oe,ze)=>{for(var Ce in ze)Se.o(ze,Ce)&&!Se.o(oe,Ce)&&Object.defineProperty(oe,Ce,{enumerable:!0,get:ze[Ce]})},Se.f={},Se.e=oe=>Promise.all(Object.keys(Se.f).reduce((ze,Ce)=>(Se.f[Ce](oe,ze),ze),[])),Se.u=oe=>oe+".js",Se.miniCssF=oe=>{},Se.o=(oe,ze)=>Object.prototype.hasOwnProperty.call(oe,ze),(()=>{var oe={};Se.l=(Ce,Ge,_n,kt)=>{if(oe[Ce])oe[Ce].push(Ge);else{var We,sn;if(void 0!==_n)for(var Ye=document.getElementsByTagName("script"),Ht=0;Ht{We.onerror=We.onload=null,clearTimeout(rt);var el=oe[Ce];if(delete oe[Ce],We.parentNode&&We.parentNode.removeChild(We),el&&el.forEach(fc=>fc(yi)),ye)return ye(yi)},rt=setTimeout(Qt.bind(null,void 0,{type:"timeout",target:We}),12e4);We.onerror=Qt.bind(null,We.onerror),We.onload=Qt.bind(null,We.onload),sn&&document.head.appendChild(We)}}})(),Se.r=oe=>{typeof Symbol<"u"&&Symbol.toStringTag&&Object.defineProperty(oe,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(oe,"__esModule",{value:!0})},(()=>{var oe;Se.tt=()=>(void 0===oe&&(oe={createScriptURL:ze=>ze},typeof trustedTypes<"u"&&trustedTypes.createPolicy&&(oe=trustedTypes.createPolicy("angular#bundler",oe))),oe)})(),Se.tu=oe=>Se.tt().createScriptURL(oe),Se.p="",(()=>{var oe={792:0};Se.f.j=(Ge,_n)=>{var kt=Se.o(oe,Ge)?oe[Ge]:void 0;if(0!==kt)if(kt)_n.push(kt[2]);else{var We=new Promise((Ct,Qt)=>kt=oe[Ge]=[Ct,Qt]);_n.push(kt[2]=We);var sn=Se.p+Se.u(Ge),Ye=new Error;Se.l(sn,Ct=>{if(Se.o(oe,Ge)&&(0!==(kt=oe[Ge])&&(oe[Ge]=void 0),kt)){var Qt=Ct&&("load"===Ct.type?"missing":Ct.type),rt=Ct&&Ct.target&&Ct.target.src;Ye.message="Loading chunk "+Ge+" failed.\n("+Qt+": "+rt+")",Ye.name="ChunkLoadError",Ye.type=Qt,Ye.request=rt,kt[1](Ye)}},"chunk-"+Ge,Ge)}};var ze=(Ge,_n)=>{var Ye,Ht,[kt,We,sn]=_n,Ct=0;if(kt.some(rt=>0!==oe[rt])){for(Ye in We)Se.o(We,Ye)&&(Se.m[Ye]=We[Ye]);sn&&sn(Se)}for(Ge&&Ge(_n);Ct{"use strict";var oe=Se(467);let ze;function Ce(){return ze}function Ge(e){const t=ze;return ze=e,t}const kt=Symbol("NotFound");function sn(e){return e===kt||"\u0275NotFound"===e?.name}Error;let Ye=null,Ht=!1,Ct=1;const rt=Symbol("SIGNAL");function ye(e){const t=Ye;return Ye=e,t}const Eo={version:0,lastCleanEpoch:0,dirty:!1,producers:void 0,producersTail:void 0,consumers:void 0,consumersTail:void 0,recomputing:!1,consumerAllowSignalWrites:!1,consumerIsAlwaysLive:!1,kind:"unknown",producerMustRecompute:()=>!1,producerRecomputeValue:()=>{},consumerMarkedDirty:()=>{},consumerOnSignalRead:()=>{}};function Do(e){if(Ht)throw new Error("");if(null===Ye)return;Ye.consumerOnSignalRead(e);const t=Ye.producersTail;if(void 0!==t&&t.producer===e)return;let n;const r=Ye.recomputing;if(r&&(n=void 0!==t?t.nextProducer:Ye.producers,void 0!==n&&n.producer===e))return Ye.producersTail=n,void(n.lastReadVersion=e.version);const i=e.consumersTail;if(void 0!==i&&i.consumer===Ye&&(!r||function Vp(e,t){const n=t.producersTail;if(void 0!==n){let r=t.producers;do{if(r===e)return!0;if(r===n)break;r=r.nextProducer}while(void 0!==r)}return!1}(i,Ye)))return;const o=cs(Ye),a={producer:e,consumer:Ye,nextProducer:n,prevConsumer:i,lastReadVersion:e.version,nextConsumer:void 0};Ye.producersTail=a,void 0!==t?t.nextProducer=a:Ye.producers=a,o&&ls(e,a)}function ss(e){if((!cs(e)||e.dirty)&&(e.dirty||e.lastCleanEpoch!==Ct)){if(!e.producerMustRecompute(e)&&!xo(e))return void pc(e);e.producerRecomputeValue(e),pc(e)}}function zs(e){if(void 0===e.consumers)return;const t=Ht;Ht=!0;try{for(let n=e.consumers;void 0!==n;n=n.nextConsumer){const r=n.consumer;r.dirty||Q(r)}}finally{Ht=t}}function Id(){return!1!==Ye?.consumerAllowSignalWrites}function Q(e){e.dirty=!0,zs(e),e.consumerMarkedDirty?.(e)}function pc(e){e.dirty=!1,e.lastCleanEpoch=Ct}function Hi(e){return e&&function as(e){e.producersTail=void 0,e.recomputing=!0}(e),ye(e)}function Ui(e,t){ye(t),e&&function Bp(e){e.recomputing=!1;const t=e.producersTail;let n=void 0!==t?t.nextProducer:e.producers;if(void 0!==n){if(cs(e))do{n=tl(n)}while(void 0!==n);void 0!==t?t.nextProducer=void 0:e.producers=void 0}}(e)}function xo(e){for(let t=e.producers;void 0!==t;t=t.nextProducer){const n=t.producer,r=t.lastReadVersion;if(r!==n.version||(ss(n),r!==n.version))return!0}return!1}function So(e){if(cs(e)){let t=e.producers;for(;void 0!==t;)t=tl(t)}e.producers=void 0,e.producersTail=void 0,e.consumers=void 0,e.consumersTail=void 0}function ls(e,t){const n=e.consumersTail,r=cs(e);if(void 0!==n?(t.nextConsumer=n.nextConsumer,n.nextConsumer=t):(t.nextConsumer=void 0,e.consumers=t),t.prevConsumer=n,e.consumersTail=t,!r)for(let i=e.producers;void 0!==i;i=i.nextProducer)ls(i.producer,i)}function tl(e){const t=e.producer,n=e.nextProducer,r=e.nextConsumer,i=e.prevConsumer;if(e.nextConsumer=void 0,e.prevConsumer=void 0,void 0!==r?r.prevConsumer=i:t.consumersTail=i,void 0!==i)i.nextConsumer=r;else if(t.consumers=r,!cs(t)){let o=t.producers;for(;void 0!==o;)o=tl(o)}return n}function cs(e){return e.consumerIsAlwaysLive||void 0!==e.consumers}function gc(e,t){return Object.is(e,t)}const Zr=Symbol("UNSET"),Mo=Symbol("COMPUTING"),zi=Symbol("ERRORED"),$p={...Eo,value:Zr,dirty:!0,error:null,equal:gc,kind:"computed",producerMustRecompute:e=>e.value===Zr||e.value===Mo,producerRecomputeValue(e){if(e.value===Mo)throw new Error("");const t=e.value;e.value=Mo;const n=Hi(e);let r,i=!1;try{r=e.computation(),ye(null),i=t!==Zr&&t!==zi&&r!==zi&&e.equal(t,r)}catch(o){r=zi,e.error=o}finally{Ui(e,n)}i?e.value=t:(e.value=r,e.version++)}};let To=function S0(){throw new Error};function Ad(e){To(e)}function we(e,t){const n=Object.create(vc);n.value=e,void 0!==t&&(n.equal=t);const r=()=>function yc(e){return Do(e),e.value}(n);return r[rt]=n,[r,a=>Gs(n,a),a=>function kd(e,t){Id()||Ad(e),Gs(e,t(e.value))}(n,a)]}function Gs(e,t){Id()||Ad(e),e.equal(e.value,t)||(e.value=t,function Od(e){e.version++,function Td(){Ct++}(),zs(e)}(e))}const vc={...Eo,equal:gc,value:void 0,kind:"signal"};function ft(e){return"function"==typeof e}function Rd(e){const n=e(r=>{Error.call(r),r.stack=(new Error).stack});return n.prototype=Object.create(Error.prototype),n.prototype.constructor=n,n}const Nd=Rd(e=>function(n){e(this),this.message=n?`${n.length} errors occurred during unsubscription:\n${n.map((r,i)=>`${i+1}) ${r.toString()}`).join("\n ")}`:"",this.name="UnsubscriptionError",this.errors=n});function Kr(e,t){if(e){const n=e.indexOf(t);0<=n&&e.splice(n,1)}}class Hn{constructor(t){this.initialTeardown=t,this.closed=!1,this._parentage=null,this._finalizers=null}unsubscribe(){let t;if(!this.closed){this.closed=!0;const{_parentage:n}=this;if(n)if(this._parentage=null,Array.isArray(n))for(const o of n)o.remove(this);else n.remove(this);const{initialTeardown:r}=this;if(ft(r))try{r()}catch(o){t=o instanceof Nd?o.errors:[o]}const{_finalizers:i}=this;if(i){this._finalizers=null;for(const o of i)try{zp(o)}catch(a){t=t??[],a instanceof Nd?t=[...t,...a.errors]:t.push(a)}}if(t)throw new Nd(t)}}add(t){var n;if(t&&t!==this)if(this.closed)zp(t);else{if(t instanceof Hn){if(t.closed||t._hasParent(this))return;t._addParent(this)}(this._finalizers=null!==(n=this._finalizers)&&void 0!==n?n:[]).push(t)}}_hasParent(t){const{_parentage:n}=this;return n===t||Array.isArray(n)&&n.includes(t)}_addParent(t){const{_parentage:n}=this;this._parentage=Array.isArray(n)?(n.push(t),n):n?[n,t]:t}_removeParent(t){const{_parentage:n}=this;n===t?this._parentage=null:Array.isArray(n)&&Kr(n,t)}remove(t){const{_finalizers:n}=this;n&&Kr(n,t),t instanceof Hn&&t._removeParent(this)}}Hn.EMPTY=(()=>{const e=new Hn;return e.closed=!0,e})();const bc=Hn.EMPTY;function Up(e){return e instanceof Hn||e&&"closed"in e&&ft(e.remove)&&ft(e.add)&&ft(e.unsubscribe)}function zp(e){ft(e)?e():e.unsubscribe()}const us={onUnhandledError:null,onStoppedNotification:null,Promise:void 0,useDeprecatedSynchronousErrorHandling:!1,useDeprecatedNextContext:!1},_c={setTimeout(e,t,...n){const{delegate:r}=_c;return r?.setTimeout?r.setTimeout(e,t,...n):setTimeout(e,t,...n)},clearTimeout(e){const{delegate:t}=_c;return(t?.clearTimeout||clearTimeout)(e)},delegate:void 0};function Pd(e){_c.setTimeout(()=>{const{onUnhandledError:t}=us;if(!t)throw e;t(e)})}function Ld(){}const Jt=Bd("C",void 0,void 0);function Bd(e,t,n){return{kind:e,value:t,error:n}}let ds=null;function Or(e){if(us.useDeprecatedSynchronousErrorHandling){const t=!ds;if(t&&(ds={errorThrown:!1,error:null}),e(),t){const{errorThrown:n,error:r}=ds;if(ds=null,n)throw r}}else e()}class cr extends Hn{constructor(t){super(),this.isStopped=!1,t?(this.destination=t,Up(t)&&t.add(this)):this.destination=jd}static create(t,n,r){return new $d(t,n,r)}next(t){this.isStopped?wc(function M0(e){return Bd("N",e,void 0)}(t),this):this._next(t)}error(t){this.isStopped?wc(function Fd(e){return Bd("E",void 0,e)}(t),this):(this.isStopped=!0,this._error(t))}complete(){this.isStopped?wc(Jt,this):(this.isStopped=!0,this._complete())}unsubscribe(){this.closed||(this.isStopped=!0,super.unsubscribe(),this.destination=null)}_next(t){this.destination.next(t)}_error(t){try{this.destination.error(t)}finally{this.unsubscribe()}}_complete(){try{this.destination.complete()}finally{this.unsubscribe()}}}const T0=Function.prototype.bind;function hs(e,t){return T0.call(e,t)}class I0{constructor(t){this.partialObserver=t}next(t){const{partialObserver:n}=this;if(n.next)try{n.next(t)}catch(r){wn(r)}}error(t){const{partialObserver:n}=this;if(n.error)try{n.error(t)}catch(r){wn(r)}else wn(t)}complete(){const{partialObserver:t}=this;if(t.complete)try{t.complete()}catch(n){wn(n)}}}class $d extends cr{constructor(t,n,r){let i;if(super(),ft(t)||!t)i={next:t??void 0,error:n??void 0,complete:r??void 0};else{let o;this&&us.useDeprecatedNextContext?(o=Object.create(t),o.unsubscribe=()=>this.unsubscribe(),i={next:t.next&&hs(t.next,o),error:t.error&&hs(t.error,o),complete:t.complete&&hs(t.complete,o)}):i=t}this.destination=new I0(i)}}function wn(e){us.useDeprecatedSynchronousErrorHandling?function Vd(e){us.useDeprecatedSynchronousErrorHandling&&ds&&(ds.errorThrown=!0,ds.error=e)}(e):Pd(e)}function wc(e,t){const{onStoppedNotification:n}=us;n&&_c.setTimeout(()=>n(e,t))}const jd={closed:!0,next:Ld,error:function Wp(e){throw e},complete:Ld},Ot="function"==typeof Symbol&&Symbol.observable||"@@observable";function Io(e){return e}function Cc(e){return 0===e.length?Io:1===e.length?e[0]:function(n){return e.reduce((r,i)=>i(r),n)}}let Ut=(()=>{class e{constructor(n){n&&(this._subscribe=n)}lift(n){const r=new e;return r.source=this,r.operator=n,r}subscribe(n,r,i){const o=function O0(e){return e&&e instanceof cr||function k0(e){return e&&ft(e.next)&&ft(e.error)&&ft(e.complete)}(e)&&Up(e)}(n)?n:new $d(n,r,i);return Or(()=>{const{operator:a,source:u}=this;o.add(a?a.call(o,u):u?this._subscribe(o):this._trySubscribe(o))}),o}_trySubscribe(n){try{return this._subscribe(n)}catch(r){n.error(r)}}forEach(n,r){return new(r=Ao(r))((i,o)=>{const a=new $d({next:u=>{try{n(u)}catch(f){o(f),a.unsubscribe()}},error:o,complete:i});this.subscribe(a)})}_subscribe(n){var r;return null===(r=this.source)||void 0===r?void 0:r.subscribe(n)}[Ot](){return this}pipe(...n){return Cc(n)(this)}toPromise(n){return new(n=Ao(n))((r,i)=>{let o;this.subscribe(a=>o=a,a=>i(a),()=>r(o))})}}return e.create=t=>new e(t),e})();function Ao(e){var t;return null!==(t=e??us.Promise)&&void 0!==t?t:Promise}const R0=Rd(e=>function(){e(this),this.name="ObjectUnsubscribedError",this.message="object unsubscribed"});let Kn=(()=>{class e extends Ut{constructor(){super(),this.closed=!1,this.currentObservers=null,this.observers=[],this.isStopped=!1,this.hasError=!1,this.thrownError=null}lift(n){const r=new Ec(this,this);return r.operator=n,r}_throwIfClosed(){if(this.closed)throw new R0}next(n){Or(()=>{if(this._throwIfClosed(),!this.isStopped){this.currentObservers||(this.currentObservers=Array.from(this.observers));for(const r of this.currentObservers)r.next(n)}})}error(n){Or(()=>{if(this._throwIfClosed(),!this.isStopped){this.hasError=this.isStopped=!0,this.thrownError=n;const{observers:r}=this;for(;r.length;)r.shift().error(n)}})}complete(){Or(()=>{if(this._throwIfClosed(),!this.isStopped){this.isStopped=!0;const{observers:n}=this;for(;n.length;)n.shift().complete()}})}unsubscribe(){this.isStopped=this.closed=!0,this.observers=this.currentObservers=null}get observed(){var n;return(null===(n=this.observers)||void 0===n?void 0:n.length)>0}_trySubscribe(n){return this._throwIfClosed(),super._trySubscribe(n)}_subscribe(n){return this._throwIfClosed(),this._checkFinalizedStatuses(n),this._innerSubscribe(n)}_innerSubscribe(n){const{hasError:r,isStopped:i,observers:o}=this;return r||i?bc:(this.currentObservers=null,o.push(n),new Hn(()=>{this.currentObservers=null,Kr(o,n)}))}_checkFinalizedStatuses(n){const{hasError:r,thrownError:i,isStopped:o}=this;r?n.error(i):o&&n.complete()}asObservable(){const n=new Ut;return n.source=this,n}}return e.create=(t,n)=>new Ec(t,n),e})();class Ec extends Kn{constructor(t,n){super(),this.destination=t,this.source=n}next(t){var n,r;null===(r=null===(n=this.destination)||void 0===n?void 0:n.next)||void 0===r||r.call(n,t)}error(t){var n,r;null===(r=null===(n=this.destination)||void 0===n?void 0:n.error)||void 0===r||r.call(n,t)}complete(){var t,n;null===(n=null===(t=this.destination)||void 0===t?void 0:t.complete)||void 0===n||n.call(t)}_subscribe(t){var n,r;return null!==(r=null===(n=this.source)||void 0===n?void 0:n.subscribe(t))&&void 0!==r?r:bc}}class yr extends Kn{constructor(t){super(),this._value=t}get value(){return this.getValue()}_subscribe(t){const n=super._subscribe(t);return!n.closed&&t.next(this._value),n}getValue(){const{hasError:t,thrownError:n,_value:r}=this;if(t)throw n;return this._throwIfClosed(),r}next(t){super.next(this._value=t)}}class G extends Error{code;constructor(t,n){super(ur(t,n)),this.code=t}}function ur(e,t){return`${function Yp(e){return`NG0${Math.abs(e)}`}(e)}${t?": "+t:""}`}const Xt=globalThis;function st(e){for(let t in e)if(e[t]===st)return t;throw Error("")}function N0(e,t){for(const n in t)t.hasOwnProperty(n)&&!e.hasOwnProperty(n)&&(e[n]=t[n])}function Qn(e){if("string"==typeof e)return e;if(Array.isArray(e))return`[${e.map(Qn).join(", ")}]`;if(null==e)return""+e;const t=e.overriddenName||e.name;if(t)return`${t}`;const n=e.toString();if(null==n)return""+n;const r=n.indexOf("\n");return r>=0?n.slice(0,r):n}function Dc(e,t){return e?t?`${e} ${t}`:e:t||""}const Ud=st({__forward_ref__:st});function pt(e){return e.__forward_ref__=pt,e.toString=function(){return Qn(this())},e}function je(e){return bt(e)?e():e}function bt(e){return"function"==typeof e&&e.hasOwnProperty(Ud)&&e.__forward_ref__===pt}function pe(e){return{token:e.token,providedIn:e.providedIn||null,factory:e.factory,value:void 0}}function Wi(e){return{providers:e.providers||[],imports:e.imports||[]}}function Sc(e){return function Zp(e,t){return e.hasOwnProperty(t)&&e[t]||null}(e,Mc)}function Gi(e){return e&&e.hasOwnProperty(qs)?e[qs]:null}const Mc=st({\u0275prov:st}),qs=st({\u0275inj:st});class ee{_desc;ngMetadataName="InjectionToken";\u0275prov;constructor(t,n){this._desc=t,this.\u0275prov=void 0,"number"==typeof n?this.__NG_ELEMENT_ID__=n:void 0!==n&&(this.\u0275prov=pe({token:this,providedIn:n.providedIn||"root",factory:n.factory}))}get multi(){return this}toString(){return`InjectionToken ${this._desc}`}}function Wd(e){return e&&!!e.\u0275providers}const Ac=st({\u0275cmp:st}),Gd=st({\u0275dir:st}),Xs=st({\u0275pipe:st}),bi=st({\u0275mod:st}),_i=st({\u0275fac:st}),ll=st({__NG_ELEMENT_ID__:st}),ng=st({__NG_ENV_ID__:st});function Le(e){return"string"==typeof e?e:null==e?"":String(e)}const kc=st({ngErrorCode:st}),rg=st({ngErrorMessage:st}),Zs=st({ngTokenPath:st});function ps(e,t){return Ks("",-200,t)}function qi(e,t){throw new G(-201,!1)}function Ks(e,t,n){const r=new G(t,e);return r[kc]=t,r[rg]=e,n&&(r[Zs]=n),r}let cl;function Yd(){return cl}function Un(e){const t=cl;return cl=e,t}function ag(e,t,n){const r=Sc(e);return r&&"root"==r.providedIn?void 0===r.value?r.value=r.factory():r.value:8&n?null:void 0!==t?t:void qi()}const gs={};class q0{injector;constructor(t){this.injector=t}retrieve(t,n){const r=ul(n)||0;try{return this.injector.get(t,8&r?null:gs,r)}catch(i){if(sn(i))return i;throw i}}}function Y0(e,t=0){const n=Ce();if(void 0===n)throw new G(-203,!1);if(null===n)return ag(e,void 0,t);{const r=function X0(e){return{optional:!!(8&e),host:!!(1&e),self:!!(2&e),skipSelf:!!(4&e)}}(t),i=n.retrieve(e,r);if(sn(i)){if(r.optional)return null;throw i}return i}}function ke(e,t=0){return(Yd()||Y0)(je(e),t)}function Y(e,t){return ke(e,ul(t))}function ul(e){return typeof e>"u"||"number"==typeof e?e:0|(e.optional&&8)|(e.host&&1)|(e.self&&2)|(e.skipSelf&&4)}function Rc(e){const t=[];for(let n=0;nArray.isArray(n)?Qs(n,t):t(n))}function Nc(e,t,n){t>=e.length?e.push(n):e.splice(t,0,n)}function Js(e,t){return t>=e.length-1?e.pop():e.splice(t,1)[0]}function hl(e,t,n){let r=Jr(e,t);return r>=0?e[1|r]=n:(r=~r,function ug(e,t,n,r){let i=e.length;if(i==t)e.push(n,r);else if(1===i)e.push(r,e[0]),e[0]=n;else{for(i--,e.push(e[i-1],e[i]);i>t;)e[i]=e[i-2],i--;e[t]=n,e[t+1]=r}}(e,r,t,n)),r}function ea(e,t){const n=Jr(e,t);if(n>=0)return e[1|n]}function Jr(e,t){return function K0(e,t,n){let r=0,i=e.length>>n;for(;i!==r;){const o=r+(i-r>>1),a=e[o<t?i=o:r=o+1}return~(i<{n.push(a)};return Qs(t,a=>{const u=a;Lc(u,o,[],r)&&(i||=[],i.push(u))}),void 0!==i&&dg(i,o),n}function dg(e,t){for(let n=0;n{t(o,r)})}}function Lc(e,t,n,r){if(!(e=je(e)))return!1;let i=null,o=Gi(e);const a=!o&&ct(e);if(o||a){if(a&&!a.standalone)return!1;i=e}else{const f=e.ngModule;if(o=Gi(f),!o)return!1;i=f}const u=r.has(i);if(a){if(u)return!1;if(r.add(i),a.dependencies){const f="function"==typeof a.dependencies?a.dependencies():a.dependencies;for(const y of f)Lc(y,t,n,r)}}else{if(!o)return!1;{if(null!=o.imports&&!u){let y;r.add(i);try{Qs(o.imports,w=>{Lc(w,t,n,r)&&(y||=[],y.push(w))})}finally{}void 0!==y&&dg(y,t)}if(!u){const y=Oo(i)||(()=>new i);t({provide:i,useFactory:y,deps:Rt},i),t({provide:Zd,useValue:i,multi:!0},i),t({provide:hr,useValue:()=>ke(i),multi:!0},i)}const f=o.providers;if(null!=f&&!u){const y=e;Jd(f,w=>{t(w,y)})}}}return i!==e&&void 0!==e.providers}function Jd(e,t){for(let n of e)Wd(n)&&(n=n.\u0275providers),Array.isArray(n)?Jd(n,t):t(n)}const J0=st({provide:String,useValue:st});function Fc(e){return null!==e&&"object"==typeof e&&J0 in e}function Jn(e){return"function"==typeof e}const eh=new ee(""),Bc={},pg={};let th;function ia(){return void 0===th&&(th=new na),th}class Dn{}class vs extends Dn{parent;source;scopes;records=new Map;_ngOnDestroyHooks=new Set;_onDestroyHooks=[];get destroyed(){return this._destroyed}_destroyed=!1;injectorDefTypes;constructor(t,n,r,i){super(),this.parent=n,this.source=r,this.scopes=i,Pr(t,a=>this.processProvider(a)),this.records.set(ta,sa(void 0,this)),i.has("environment")&&this.records.set(Dn,sa(void 0,this));const o=this.records.get(eh);null!=o&&"string"==typeof o.value&&this.scopes.add(o.value),this.injectorDefTypes=new Set(this.get(Zd,Rt,{self:!0}))}retrieve(t,n){const r=ul(n)||0;try{return this.get(t,gs,r)}catch(i){if(sn(i))return i;throw i}}destroy(){Xi(this),this._destroyed=!0;const t=ye(null);try{for(const r of this._ngOnDestroyHooks)r.ngOnDestroy();const n=this._onDestroyHooks;this._onDestroyHooks=[];for(const r of n)r()}finally{this.records.clear(),this._ngOnDestroyHooks.clear(),this.injectorDefTypes.clear(),ye(t)}}onDestroy(t){return Xi(this),this._onDestroyHooks.push(t),()=>this.removeOnDestroy(t)}runInContext(t){Xi(this);const n=Ge(this),r=Un(void 0);try{return t()}finally{Ge(n),Un(r)}}get(t,n=gs,r){if(Xi(this),t.hasOwnProperty(ng))return t[ng](this);const i=ul(r),a=Ge(this),u=Un(void 0);try{if(!(4&i)){let y=this.records.get(t);if(void 0===y){const w=function nb(e){return"function"==typeof e||"object"==typeof e&&"InjectionToken"===e.ngMetadataName}(t)&&Sc(t);y=w&&this.injectableDefInScope(w)?sa(oa(t),Bc):null,this.records.set(t,y)}if(null!=y)return this.hydrate(t,y,i)}return(2&i?ia():this.parent).get(t,n=8&i&&n===gs?null:n)}catch(f){const y=function og(e){return e[kc]}(f);throw-200===y||-201===y?new G(y,null):f}finally{Un(u),Ge(a)}}resolveInjectorInitializers(){const t=ye(null),n=Ge(this),r=Un(void 0);try{const o=this.get(hr,Rt,{self:!0});for(const a of o)a()}finally{Ge(n),Un(r),ye(t)}}toString(){const t=[],n=this.records;for(const r of n.keys())t.push(Qn(r));return`R3Injector[${t.join(", ")}]`}processProvider(t){let n=Jn(t=je(t))?t:je(t&&t.provide);const r=function eb(e){return Fc(e)?sa(void 0,e.useValue):sa(rh(e),Bc)}(t);if(!Jn(t)&&!0===t.multi){let i=this.records.get(n);i||(i=sa(void 0,Bc,!0),i.factory=()=>Rc(i.multi),this.records.set(n,i)),n=t,i.multi.push(t)}this.records.set(n,r)}hydrate(t,n,r){const i=ye(null);try{if(n.value===pg)throw ps(Qn(t));return n.value===Bc&&(n.value=pg,n.value=n.factory(void 0,r)),"object"==typeof n.value&&n.value&&function tb(e){return null!==e&&"object"==typeof e&&"function"==typeof e.ngOnDestroy}(n.value)&&this._ngOnDestroyHooks.add(n.value),n.value}finally{ye(i)}}injectableDefInScope(t){if(!t.providedIn)return!1;const n=je(t.providedIn);return"string"==typeof n?"any"===n||this.scopes.has(n):this.injectorDefTypes.has(n)}removeOnDestroy(t){const n=this._onDestroyHooks.indexOf(t);-1!==n&&this._onDestroyHooks.splice(n,1)}}function oa(e){const t=Sc(e),n=null!==t?t.factory:Oo(e);if(null!==n)return n;if(e instanceof ee)throw new G(204,!1);if(e instanceof Function)return function nh(e){if(e.length>0)throw new G(204,!1);const n=function Kp(e){return(e?.[Mc]??null)||null}(e);return null!==n?()=>n.factory(e):()=>new e}(e);throw new G(204,!1)}function rh(e,t,n){let r;if(Jn(e)){const i=je(e);return Oo(i)||oa(i)}if(Fc(e))r=()=>je(e.useValue);else if(function ys(e){return!(!e||!e.useFactory)}(e))r=()=>e.useFactory(...Rc(e.deps||[]));else if(function hg(e){return!(!e||!e.useExisting)}(e))r=(i,o)=>ke(je(e.useExisting),void 0!==o&&8&o?8:void 0);else{const i=je(e&&(e.useClass||e.provide));if(!function Vc(e){return!!e.deps}(e))return Oo(i)||oa(i);r=()=>new i(...Rc(e.deps))}return r}function Xi(e){if(e.destroyed)throw new G(205,!1)}function sa(e,t,n=!1){return{factory:e,value:t,multi:n?[]:void 0}}function Pr(e,t){for(const n of e)Array.isArray(n)?Pr(n,t):n&&Wd(n)?Pr(n.\u0275providers,t):t(n)}function xn(e,t){let n;e instanceof vs?(Xi(e),n=e):n=new q0(e);const i=Ge(n),o=Un(void 0);try{return t()}finally{Ge(i),Un(o)}}function zt(e){return Array.isArray(e)&&"object"==typeof e[1]}function er(e){return Array.isArray(e)&&!0===e[1]}function mg(e){return!!(4&e.flags)}function eo(e){return e.componentOffset>-1}function la(e){return!(1&~e.flags)}function _r(e){return!!e.template}function to(e){return!!(512&e[2])}function Si(e){return!(256&~e[2])}function fn(e){for(;Array.isArray(e);)e=e[0];return e}function ca(e,t){return fn(t[e])}function ln(e,t){return fn(t[e.index])}function ua(e,t){return e.data[t]}function Lr(e,t){return e[t]}function Fn(e,t){const n=t[e];return zt(n)?n:n[0]}function gl(e){return!(128&~e[2])}function Wn(e,t){return null==t?null:e[t]}function Cg(e){e[17]=0}function ch(e){1024&e[2]||(e[2]|=1024,gl(e)&&ro(e))}function ml(e){return!!(9216&e[2]||e[24]?.dirty)}function uh(e){e[10].changeDetectionScheduler?.notify(8),64&e[2]&&(e[2]|=1024),ml(e)&&ro(e)}function ro(e){e[10].changeDetectionScheduler?.notify(0);let t=io(e);for(;null!==t&&!(8192&t[2])&&(t[2]|=8192,gl(t));)t=io(t)}function da(e,t){if(Si(e))throw new G(911,!1);null===e[21]&&(e[21]=[]),e[21].push(t)}function io(e){const t=e[3];return er(t)?t[3]:t}const Oe={lFrame:_s(null),bindingsEnabled:!0,skipHydrationRootTNode:null};let zc=!1;function so(){Oe.lFrame.elementDepthCount--}function xg(e){return Oe.skipHydrationRootTNode===e}function Gc(){Oe.skipHydrationRootTNode=null}function K(){return Oe.lFrame.lView}function Ve(){return Oe.lFrame.tView}function Dt(e){return Oe.lFrame.contextLView=e,e[8]}function Gt(e){return Oe.lFrame.contextLView=null,e}function Ae(){let e=Sg();for(;null!==e&&64===e.type;)e=e.parent;return e}function Sg(){return Oe.lFrame.currentTNode}function Mn(e,t){const n=Oe.lFrame;n.currentTNode=e,n.isParent=t}function Mg(){return Oe.lFrame.isParent}function qc(){return zc}function Fo(e){const t=zc;return zc=e,t}function pn(){const e=Oe.lFrame;let t=e.bindingRootIndex;return-1===t&&(t=e.bindingRootIndex=e.tView.bindingStartIndex),t}function fr(){return Oe.lFrame.bindingIndex++}function Mi(e){const t=Oe.lFrame,n=t.bindingIndex;return t.bindingIndex=t.bindingIndex+e,n}function pb(e,t){const n=Oe.lFrame;n.bindingIndex=n.bindingRootIndex=e,yh(t)}function yh(e){Oe.lFrame.currentDirectiveIndex=e}function Xc(e){Oe.lFrame.currentQueryIndex=e}function Zc(e){const t=e[1];return 2===t.type?t.declTNode:1===t.type?e[5]:null}function Ag(e,t,n){if(4&n){let i=t,o=e;for(;!(i=i.parent,null!==i||1&n||(i=Zc(o),null===i||(o=o[14],10&i.type))););if(null===i)return!1;t=i,e=o}const r=Oe.lFrame=kg();return r.currentTNode=t,r.lView=e,!0}function vh(e){const t=kg(),n=e[1];Oe.lFrame=t,t.currentTNode=n.firstChild,t.lView=e,t.tView=n,t.contextLView=e,t.bindingIndex=n.bindingStartIndex,t.inI18n=!1}function kg(){const e=Oe.lFrame,t=null===e?null:e.child;return null===t?_s(e):t}function _s(e){const t={currentTNode:null,isParent:!0,lView:null,tView:null,selectedIndex:-1,contextLView:null,elementDepthCount:0,currentNamespace:null,currentDirectiveIndex:-1,bindingRootIndex:-1,bindingIndex:-1,currentQueryIndex:0,parent:e,child:null,inI18n:!1};return null!==e&&(e.child=t),t}function Og(){const e=Oe.lFrame;return Oe.lFrame=e.parent,e.currentTNode=null,e.lView=null,e}const Kc=Og;function Qc(){const e=Og();e.isParent=!0,e.tView=null,e.selectedIndex=-1,e.contextLView=null,e.elementDepthCount=0,e.currentDirectiveIndex=-1,e.currentNamespace=null,e.bindingRootIndex=-1,e.bindingIndex=-1,e.currentQueryIndex=0}function Tn(){return Oe.lFrame.selectedIndex}function ws(e){Oe.lFrame.selectedIndex=e}function gt(){const e=Oe.lFrame;return ua(e.tView,e.selectedIndex)}let Pg=!0;function xt(){return Pg}function bl(e){Pg=e}function bh(e,t=null,n=null,r){const i=_h(e,t,n,r);return i.resolveInjectorInitializers(),i}function _h(e,t=null,n=null,r,i=new Set){const o=[n||Rt,Q0(e)];return r=r||("object"==typeof e?void 0:Qn(e)),new vs(o,t||ia(),r||null,i)}class Gn{static THROW_IF_NOT_FOUND=gs;static NULL=new na;static create(t,n){if(Array.isArray(t))return bh({name:""},n,t,"");{const r=t.name??"";return bh({name:r},t.parent,t.providers,r)}}static \u0275prov=pe({token:Gn,providedIn:"any",factory:()=>ke(ta)});static __NG_ELEMENT_ID__=-1}const ut=new ee("");let Cr=(()=>class e{static __NG_ELEMENT_ID__=wh;static __NG_ENV_ID__=n=>n})();class fa extends Cr{_lView;constructor(t){super(),this._lView=t}get destroyed(){return Si(this._lView)}onDestroy(t){const n=this._lView;return da(n,t),()=>function dh(e,t){if(null===e[21])return;const n=e[21].indexOf(t);-1!==n&&e[21].splice(n,1)}(n,t)}}function wh(){return new fa(K())}class dt{_console=console;handleError(t){this._console.error("ERROR",t)}}const ri=new ee("",{providedIn:"root",factory:()=>{const e=Y(Dn);let t;return n=>{e.destroyed&&!t?setTimeout(()=>{throw n}):(t??=e.get(dt),t.handleError(n))}}}),Ch={provide:hr,useValue:()=>{Y(dt)},multi:!0};function tn(e,t){const[n,r,i]=we(e,t?.equal),o=n;return o.set=r,o.update=i,o.asReadonly=eu.bind(o),o}function eu(){const e=this[rt];if(void 0===e.readonlyFn){const t=()=>this();t[rt]=e,e.readonlyFn=t}return e.readonlyFn}let tu=(()=>class e{view;node;constructor(n,r){this.view=n,this.node=r}static __NG_ELEMENT_ID__=nu})();function nu(){return new tu(K(),Ae())}class ao{}const Dh=new ee("",{providedIn:"root",factory:()=>!1}),Bg=new ee(""),Vg=new ee("");let qn=(()=>{class e{taskId=0;pendingTasks=new Set;destroyed=!1;pendingTask=new yr(!1);get hasPendingTasks(){return!this.destroyed&&this.pendingTask.value}get hasPendingTasksObservable(){return this.destroyed?new Ut(n=>{n.next(!1),n.complete()}):this.pendingTask}add(){!this.hasPendingTasks&&!this.destroyed&&this.pendingTask.next(!0);const n=this.taskId++;return this.pendingTasks.add(n),n}has(n){return this.pendingTasks.has(n)}remove(n){this.pendingTasks.delete(n),0===this.pendingTasks.size&&this.hasPendingTasks&&this.pendingTask.next(!1)}ngOnDestroy(){this.pendingTasks.clear(),this.hasPendingTasks&&this.pendingTask.next(!1),this.destroyed=!0,this.pendingTask.unsubscribe()}static \u0275prov=pe({token:e,providedIn:"root",factory:()=>new e})}return e})(),ru=(()=>{class e{internalPendingTasks=Y(qn);scheduler=Y(ao);errorHandler=Y(ri);add(){const n=this.internalPendingTasks.add();return()=>{this.internalPendingTasks.has(n)&&(this.scheduler.notify(11),this.internalPendingTasks.remove(n))}}run(n){const r=this.add();n().catch(this.errorHandler).finally(r)}static \u0275prov=pe({token:e,providedIn:"root",factory:()=>new e})}return e})();function oi(...e){}let Br=(()=>{class e{static \u0275prov=pe({token:e,providedIn:"root",factory:()=>new vb})}return e})();class vb{dirtyEffectCount=0;queues=new Map;add(t){this.enqueue(t),this.schedule(t)}schedule(t){t.dirty&&this.dirtyEffectCount++}remove(t){const r=this.queues.get(t.zone);r.has(t)&&(r.delete(t),t.dirty&&this.dirtyEffectCount--)}enqueue(t){const n=t.zone;this.queues.has(n)||this.queues.set(n,new Set);const r=this.queues.get(n);r.has(t)||r.add(t)}flush(){for(;this.dirtyEffectCount>0;){let t=!1;for(const[n,r]of this.queues)t||=null===n?this.flushQueue(r):n.run(()=>this.flushQueue(r));t||(this.dirtyEffectCount=0)}}flushQueue(t){let n=!1;for(const r of t)r.dirty&&(this.dirtyEffectCount--,n=!0,r.run());return n}}let $g=null;function Bo(){return $g}class xh{}let Sh=(()=>{class e{historyGo(n){throw new Error("")}static \u0275fac=function(r){return new(r||e)};static \u0275prov=pe({token:e,factory:()=>Y(_b),providedIn:"platform"})}return e})(),_b=(()=>{class e extends Sh{_location;_history;_doc=Y(ut);constructor(){super(),this._location=window.location,this._history=window.history}getBaseHrefFromDOM(){return Bo().getBaseHref(this._doc)}onPopState(n){const r=Bo().getGlobalEventTarget(this._doc,"window");return r.addEventListener("popstate",n,!1),()=>r.removeEventListener("popstate",n)}onHashChange(n){const r=Bo().getGlobalEventTarget(this._doc,"window");return r.addEventListener("hashchange",n,!1),()=>r.removeEventListener("hashchange",n)}get href(){return this._location.href}get protocol(){return this._location.protocol}get hostname(){return this._location.hostname}get port(){return this._location.port}get pathname(){return this._location.pathname}get search(){return this._location.search}get hash(){return this._location.hash}set pathname(n){this._location.pathname=n}pushState(n,r,i){this._history.pushState(n,r,i)}replaceState(n,r,i){this._history.replaceState(n,r,i)}forward(){this._history.forward()}back(){this._history.back()}historyGo(n=0){this._history.go(n)}getState(){return this._history.state}static \u0275fac=function(r){return new(r||e)};static \u0275prov=pe({token:e,factory:()=>new e,providedIn:"platform"})}return e})();function iu(e,t){return e?t?e.endsWith("/")?t.startsWith("/")?e+t.slice(1):e+t:t.startsWith("/")?e+t:`${e}/${t}`:e:t}function ou(e){const t=e.search(/#|\?|$/);return"/"===e[t-1]?e.slice(0,t-1)+e.slice(t):e}function Cs(e){return e&&"?"!==e[0]?`?${e}`:e}let _l=(()=>{class e{historyGo(n){throw new Error("")}static \u0275fac=function(r){return new(r||e)};static \u0275prov=pe({token:e,factory:()=>Y(wl),providedIn:"root"})}return e})();const Es=new ee("");let wl=(()=>{class e extends _l{_platformLocation;_baseHref;_removeListenerFns=[];constructor(n,r){super(),this._platformLocation=n,this._baseHref=r??this._platformLocation.getBaseHrefFromDOM()??Y(ut).location?.origin??""}ngOnDestroy(){for(;this._removeListenerFns.length;)this._removeListenerFns.pop()()}onPopState(n){this._removeListenerFns.push(this._platformLocation.onPopState(n),this._platformLocation.onHashChange(n))}getBaseHref(){return this._baseHref}prepareExternalUrl(n){return iu(this._baseHref,n)}path(n=!1){const r=this._platformLocation.pathname+Cs(this._platformLocation.search),i=this._platformLocation.hash;return i&&n?`${r}${i}`:r}pushState(n,r,i,o){const a=this.prepareExternalUrl(i+Cs(o));this._platformLocation.pushState(n,r,a)}replaceState(n,r,i,o){const a=this.prepareExternalUrl(i+Cs(o));this._platformLocation.replaceState(n,r,a)}forward(){this._platformLocation.forward()}back(){this._platformLocation.back()}getState(){return this._platformLocation.getState()}historyGo(n=0){this._platformLocation.historyGo?.(n)}static \u0275fac=function(r){return new(r||e)(ke(Sh),ke(Es,8))};static \u0275prov=pe({token:e,factory:e.\u0275fac,providedIn:"root"})}return e})(),pa=(()=>{class e{_subject=new Kn;_basePath;_locationStrategy;_urlChangeListeners=[];_urlChangeSubscription=null;constructor(n){this._locationStrategy=n;const r=this._locationStrategy.getBaseHref();this._basePath=function Mh(e){if(new RegExp("^(https?:)?//").test(e)){const[,n]=e.split(/\/\/[^\/]+/);return n}return e}(ou(su(r))),this._locationStrategy.onPopState(i=>{this._subject.next({url:this.path(!0),pop:!0,state:i.state,type:i.type})})}ngOnDestroy(){this._urlChangeSubscription?.unsubscribe(),this._urlChangeListeners=[]}path(n=!1){return this.normalize(this._locationStrategy.path(n))}getState(){return this._locationStrategy.getState()}isCurrentPathEqualTo(n,r=""){return this.path()==this.normalize(n+Cs(r))}normalize(n){return e.stripTrailingSlash(function Hg(e,t){if(!e||!t.startsWith(e))return t;const n=t.substring(e.length);return""===n||["/",";","?","#"].includes(n[0])?n:t}(this._basePath,su(n)))}prepareExternalUrl(n){return n&&"/"!==n[0]&&(n="/"+n),this._locationStrategy.prepareExternalUrl(n)}go(n,r="",i=null){this._locationStrategy.pushState(i,"",n,r),this._notifyUrlChangeListeners(this.prepareExternalUrl(n+Cs(r)),i)}replaceState(n,r="",i=null){this._locationStrategy.replaceState(i,"",n,r),this._notifyUrlChangeListeners(this.prepareExternalUrl(n+Cs(r)),i)}forward(){this._locationStrategy.forward()}back(){this._locationStrategy.back()}historyGo(n=0){this._locationStrategy.historyGo?.(n)}onUrlChange(n){return this._urlChangeListeners.push(n),this._urlChangeSubscription??=this.subscribe(r=>{this._notifyUrlChangeListeners(r.url,r.state)}),()=>{const r=this._urlChangeListeners.indexOf(n);this._urlChangeListeners.splice(r,1),0===this._urlChangeListeners.length&&(this._urlChangeSubscription?.unsubscribe(),this._urlChangeSubscription=null)}}_notifyUrlChangeListeners(n="",r){this._urlChangeListeners.forEach(i=>i(n,r))}subscribe(n,r,i){return this._subject.subscribe({next:n,error:r??void 0,complete:i??void 0})}static normalizeQueryParams=Cs;static joinWithSlash=iu;static stripTrailingSlash=ou;static \u0275fac=function(r){return new(r||e)(ke(_l))};static \u0275prov=pe({token:e,factory:()=>function wb(){return new pa(ke(_l))}(),providedIn:"root"})}return e})();function su(e){return e.replace(/\/index.html$/,"")}function Th(e,t){t=encodeURIComponent(t);for(const n of e.split(";")){const r=n.indexOf("="),[i,o]=-1==r?[n,""]:[n.slice(0,r),n.slice(r+1)];if(i.trim()===t)return decodeURIComponent(o)}return null}class au{}function ba(e){return ft(e?.lift)}function nn(e){return t=>{if(ba(t))return t.lift(function(n){try{return e(n,this)}catch(r){this.error(r)}});throw new TypeError("Unable to lift unknown Observable type")}}function mn(e,t,n,r,i){return new Dl(e,t,n,r,i)}class Dl extends cr{constructor(t,n,r,i,o,a){super(t),this.onFinalize=o,this.shouldUnsubscribe=a,this._next=n?function(u){try{n(u)}catch(f){t.error(f)}}:super._next,this._error=i?function(u){try{i(u)}catch(f){t.error(f)}finally{this.unsubscribe()}}:super._error,this._complete=r?function(){try{r()}catch(u){t.error(u)}finally{this.unsubscribe()}}:super._complete}unsubscribe(){var t;if(!this.shouldUnsubscribe||this.shouldUnsubscribe()){const{closed:n}=this;super.unsubscribe(),!n&&(null===(t=this.onFinalize)||void 0===t||t.call(this))}}}function tt(e,t){return nn((n,r)=>{let i=0;n.subscribe(mn(r,o=>{r.next(e.call(t,o,i++))}))})}function Ai(e){return{toString:e}.toString()}class e_{previousValue;currentValue;firstChange;constructor(t,n,r){this.previousValue=t,this.currentValue=n,this.firstChange=r}isFirstChange(){return this.firstChange}}function pm(e,t,n,r){null!==t?t.applyValueToInputSignal(t,r):e[n]=r}const si=(()=>{const e=()=>gm;return e.ngInherit=!0,e})();function gm(e){return e.type.prototype.ngOnChanges&&(e.setInput=t_),mm}function mm(){const e=ym(this),t=e?.current;if(t){const n=e.previous;if(n===zn)e.previous=t;else for(let r in t)n[r]=t[r];e.current=null,this.ngOnChanges(t)}}function t_(e,t,n,r,i){const o=this.declaredInputs[r],a=ym(e)||function n_(e,t){return e[Gh]=t}(e,{previous:zn,current:null}),u=a.current||(a.current={}),f=a.previous,y=f[o];u[o]=new e_(y&&y.currentValue,n,f===zn),pm(e,t,i,n)}const Gh="__ngSimpleChanges__";function ym(e){return e[Gh]||null}const jo=[],ht=function(e,t=null,n){for(let r=0;r=r)break}else t[f]<0&&(e[17]+=65536),(u>14>16&&(3&e[2])===t&&(e[2]+=16384,bm(u,o)):bm(u,o)}class Da{factory;name;injectImpl;resolving=!1;canSeeViewProviders;multi;componentProviders;index;providerFactory;constructor(t,n,r,i){this.factory=t,this.name=i,this.canSeeViewProviders=n,this.injectImpl=r}}function Kh(e){return 3===e||4===e||6===e}function Cm(e){return 64===e.charCodeAt(0)}function Is(e,t){if(null!==t&&0!==t.length)if(null===e||0===e.length)e=t.slice();else{let n=-1;for(let r=0;rt){a=o-1;break}}}for(;o>16}(e),r=t;for(;n>0;)r=r[14],n--;return r}let Qh=!0;function Ma(e){const t=Qh;return Qh=e,t}let u_=0;const ki={};function wu(e,t){const n=Dm(e,t);if(-1!==n)return n;const r=t[1];r.firstCreatePass&&(e.injectorIndex=t.length,nf(r.data,e),nf(t,null),nf(r.blueprint,null));const i=Cu(e,t),o=e.injectorIndex;if(_u(i)){const a=xa(i),u=Sa(i,t),f=u[1].data;for(let y=0;y<8;y++)t[o+y]=u[a+y]|f[a+y]}return t[o+8]=i,o}function nf(e,t){e.push(0,0,0,0,0,0,0,0,t)}function Dm(e,t){return-1===e.injectorIndex||e.parent&&e.parent.injectorIndex===e.injectorIndex||null===t[e.injectorIndex+8]?-1:e.injectorIndex}function Cu(e,t){if(e.parent&&-1!==e.parent.injectorIndex)return e.parent.injectorIndex;let n=0,r=null,i=t;for(;null!==i;){if(r=S(i),null===r)return-1;if(n++,i=i[14],-1!==r.injectorIndex)return r.injectorIndex|n<<16}return-1}function rf(e,t,n){!function tf(e,t,n){let r;"string"==typeof n?r=n.charCodeAt(0)||0:n.hasOwnProperty(ll)&&(r=n[ll]),null==r&&(r=n[ll]=u_++);const i=255&r;t.data[e+(i>>5)]|=1<=0?255&t:C:t}(n);if("function"==typeof o){if(!Ag(t,e,r))return 1&r?Sl(i,0,r):xm(t,n,r,i);try{let a;if(a=o(r),null!=a||8&r)return a;qi()}finally{Kc()}}else if("number"==typeof o){let a=null,u=Dm(e,t),f=-1,y=1&r?t[15][5]:null;for((-1===u||4&r)&&(f=-1===u?Cu(e,t):t[u+8],-1!==f&&p(r,!1)?(a=t[1],u=xa(f),t=Sa(f,t)):u=-1);-1!==u;){const w=t[1];if(h(o,u,w.data)){const D=v(u,t,n,a,r,y);if(D!==ki)return D}f=t[u+8],-1!==f&&p(r,t[1].data[u+8]===y)&&h(o,u,t)?(a=w,u=xa(f),t=Sa(f,t)):u=-1}}return i}function v(e,t,n,r,i,o){const a=t[1],u=a.data[e+8],w=function s(e,t,n,r,i){const o=e.providerIndexes,a=t.data,u=1048575&o,f=e.directiveStart,w=o>>20,k=i?u+w:e.directiveEnd;for(let A=r?u:u+w;A=f&&N.type===n)return A}if(i){const A=a[f];if(A&&_r(A)&&A.type===n)return f}return null}(u,a,n,null==r?eo(u)&&Qh:r!=a&&!!(3&u.type),1&i&&o===u);return null!==w?c(t,a,w,u,i):ki}function c(e,t,n,r,i){let o=e[n];const a=t.data;if(o instanceof Da){const u=o;if(u.resolving)throw function Ze(e){return"function"==typeof e?e.name||e.toString():"object"==typeof e&&null!=e&&"function"==typeof e.type?e.type.name||e.type.toString():Le(e)}(a[n]),ps();const f=Ma(u.canSeeViewProviders);u.resolving=!0;const D=u.injectImpl?Un(u.injectImpl):null;Ag(e,r,0);try{o=e[n]=u.factory(void 0,i,a,e,r),t.firstCreatePass&&n>=r.directiveStart&&function i_(e,t,n){const{ngOnChanges:r,ngOnInit:i,ngDoCheck:o}=t.type.prototype;if(r){const a=gm(t);(n.preOrderHooks??=[]).push(e,a),(n.preOrderCheckHooks??=[]).push(e,a)}i&&(n.preOrderHooks??=[]).push(0-e,i),o&&((n.preOrderHooks??=[]).push(e,o),(n.preOrderCheckHooks??=[]).push(e,o))}(n,a[n],t)}finally{null!==D&&Un(D),Ma(f),u.resolving=!1,Kc()}}return o}function h(e,t,n){return!!(n[t+(e>>5)]&1<{const t=e.prototype.constructor,n=t[_i]||E(t),r=Object.prototype;let i=Object.getPrototypeOf(e.prototype).constructor;for(;i&&i!==r;){const o=i[_i]||E(i);if(o&&o!==n)return o;i=Object.getPrototypeOf(i)}return o=>new o})}function E(e){return bt(e)?()=>{const t=E(je(e));return t&&t()}:Oo(e)}function S(e){const t=e[1],n=t.type;return 2===n?t.declTNode:1===n?e[5]:null}function ge(){return le(Ae(),K())}function le(e,t){return new ne(ln(e,t))}let ne=(()=>class e{nativeElement;constructor(n){this.nativeElement=n}static __NG_ELEMENT_ID__=ge})();function Fe(e){return!(128&~e.flags)}Symbol;var Xe=function(e){return e[e.OnPush=0]="OnPush",e[e.Default=1]="Default",e}(Xe||{});const St=new Map;let nr=0;function Bn(e){St.delete(e[19])}const zo="__ngContext__";function rr(e,t){zt(t)?(e[zo]=t[19],function Ho(e){St.set(e[19],e)}(t)):e[zo]=t}function I1(e){return k1(e[12])}function A1(e){return k1(e[4])}function k1(e){for(;null!==e&&!er(e);)e=e[4];return e}let h_;const af=new ee("",{providedIn:"root",factory:()=>TL}),TL="ng",B1=new ee(""),Im=new ee("",{providedIn:"platform",factory:()=>"unknown"}),V1=new ee("",{providedIn:"root",factory:()=>function Ta(){if(void 0!==h_)return h_;if(typeof document<"u")return document;throw new G(210,!1)}().body?.querySelector("[ngCspNonce]")?.getAttribute("ngCspNonce")||null}),LL=new ee("",{providedIn:"root",factory:()=>!1});function Rm(e){return!(32&~e.flags)}function uS(e,t){const n=e.contentQueries;if(null!==n){const r=ye(null);try{for(let i=0;i-1){let o;for(;++io?"":i[w+1].toLowerCase(),2&r&&y!==D){if(ho(r))return!1;a=!0}}}}else{if(!a&&!ho(r)&&!ho(f))return!1;if(a&&ho(f))continue;a=!1,r=f|1&r}}return ho(r)||a}function ho(e){return!(1&e)}function e3(e,t,n,r){if(null===t)return-1;let i=0;if(r||!n){let o=!1;for(;i-1)for(n++;n0?'="'+u+'"':"")+"]"}else 8&r?i+="."+a:4&r&&(i+=" "+a);else""!==i&&!ho(a)&&(t+=FS(o,i),i=""),r=a,o=o||!ho(r);n++}return""!==i&&(t+=FS(o,i)),t}const Mt={};function Um(e,t,n){return e.createElement(t,n)}function Ml(e,t,n,r,i){e.insertBefore(t,n,r,i)}function VS(e,t,n){e.appendChild(t,n)}function $S(e,t,n,r,i){null!==r?Ml(e,t,n,r,i):VS(e,t,n)}function ff(e,t,n,r){e.removeChild(null,t,n,r)}function HS(e,t,n){const{mergedAttrs:r,classes:i,styles:o}=n;null!==r&&function l_(e,t,n){let r=0;for(;rnull),a=r;if(t&&"object"==typeof t){const f=t;i=f.next?.bind(f),o=f.error?.bind(f),a=f.complete?.bind(f)}this.__isAsync&&(o=this.wrapInTimeout(o),i&&(i=this.wrapInTimeout(i)),a&&(a=this.wrapInTimeout(a)));const u=super.subscribe({next:i,error:o,complete:a});return t instanceof Hn&&t.add(u),u}wrapInTimeout(t){return n=>{const r=this.pendingTasks?.add();setTimeout(()=>{try{t(n)}finally{void 0!==r&&this.pendingTasks?.remove(r)}})}}};function ZS(e){let t,n;function r(){e=oi;try{void 0!==n&&"function"==typeof cancelAnimationFrame&&cancelAnimationFrame(n),void 0!==t&&clearTimeout(t)}catch{}}return t=setTimeout(()=>{e(),r()}),"function"==typeof requestAnimationFrame&&(n=requestAnimationFrame(()=>{e(),r()})),()=>r()}function KS(e){return queueMicrotask(()=>e()),()=>{e=oi}}const Y_="isAngularZone",qm=Y_+"_ID";let v3=0;class lt{hasPendingMacrotasks=!1;hasPendingMicrotasks=!1;isStable=!0;onUnstable=new In(!1);onMicrotaskEmpty=new In(!1);onStable=new In(!1);onError=new In(!1);constructor(t){const{enableLongStackTrace:n=!1,shouldCoalesceEventChangeDetection:r=!1,shouldCoalesceRunChangeDetection:i=!1,scheduleInRootZone:o=XS}=t;if(typeof Zone>"u")throw new G(908,!1);Zone.assertZonePatched();const a=this;a._nesting=0,a._outer=a._inner=Zone.current,Zone.TaskTrackingZoneSpec&&(a._inner=a._inner.fork(new Zone.TaskTrackingZoneSpec)),n&&Zone.longStackTraceZoneSpec&&(a._inner=a._inner.fork(Zone.longStackTraceZoneSpec)),a.shouldCoalesceEventChangeDetection=!i&&r,a.shouldCoalesceRunChangeDetection=i,a.callbackScheduled=!1,a.scheduleInRootZone=o,function w3(e){const t=()=>{!function _3(e){function t(){ZS(()=>{e.callbackScheduled=!1,Z_(e),e.isCheckStableRunning=!0,X_(e),e.isCheckStableRunning=!1})}e.isCheckStableRunning||e.callbackScheduled||(e.callbackScheduled=!0,e.scheduleInRootZone?Zone.root.run(()=>{t()}):e._outer.run(()=>{t()}),Z_(e))}(e)},n=v3++;e._inner=e._inner.fork({name:"angular",properties:{[Y_]:!0,[qm]:n,[qm+n]:!0},onInvokeTask:(r,i,o,a,u,f)=>{if(function C3(e){return eM(e,"__ignore_ng_zone__")}(f))return r.invokeTask(o,a,u,f);try{return QS(e),r.invokeTask(o,a,u,f)}finally{(e.shouldCoalesceEventChangeDetection&&"eventTask"===a.type||e.shouldCoalesceRunChangeDetection)&&t(),JS(e)}},onInvoke:(r,i,o,a,u,f,y)=>{try{return QS(e),r.invoke(o,a,u,f,y)}finally{e.shouldCoalesceRunChangeDetection&&!e.callbackScheduled&&!function E3(e){return eM(e,"__scheduler_tick__")}(f)&&t(),JS(e)}},onHasTask:(r,i,o,a)=>{r.hasTask(o,a),i===o&&("microTask"==a.change?(e._hasPendingMicrotasks=a.microTask,Z_(e),X_(e)):"macroTask"==a.change&&(e.hasPendingMacrotasks=a.macroTask))},onHandleError:(r,i,o,a)=>(r.handleError(o,a),e.runOutsideAngular(()=>e.onError.emit(a)),!1)})}(a)}static isInAngularZone(){return typeof Zone<"u"&&!0===Zone.current.get(Y_)}static assertInAngularZone(){if(!lt.isInAngularZone())throw new G(909,!1)}static assertNotInAngularZone(){if(lt.isInAngularZone())throw new G(909,!1)}run(t,n,r){return this._inner.run(t,n,r)}runTask(t,n,r,i){const o=this._inner,a=o.scheduleEventTask("NgZoneEvent: "+i,t,b3,oi,oi);try{return o.runTask(a,n,r)}finally{o.cancelTask(a)}}runGuarded(t,n,r){return this._inner.runGuarded(t,n,r)}runOutsideAngular(t){return this._outer.run(t)}}const b3={};function X_(e){if(0==e._nesting&&!e.hasPendingMicrotasks&&!e.isStable)try{e._nesting++,e.onMicrotaskEmpty.emit(null)}finally{if(e._nesting--,!e.hasPendingMicrotasks)try{e.runOutsideAngular(()=>e.onStable.emit(null))}finally{e.isStable=!0}}}function Z_(e){e.hasPendingMicrotasks=!!(e._hasPendingMicrotasks||(e.shouldCoalesceEventChangeDetection||e.shouldCoalesceRunChangeDetection)&&!0===e.callbackScheduled)}function QS(e){e._nesting++,e.isStable&&(e.isStable=!1,e.onUnstable.emit(null))}function JS(e){e._nesting--,X_(e)}class K_{hasPendingMicrotasks=!1;hasPendingMacrotasks=!1;isStable=!0;onUnstable=new In;onMicrotaskEmpty=new In;onStable=new In;onError=new In;run(t,n,r){return t.apply(n,r)}runGuarded(t,n,r){return t.apply(n,r)}runOutsideAngular(t){return t()}runTask(t,n,r,i){return t.apply(n,r)}}function eM(e,t){return!(!Array.isArray(e)||1!==e.length)&&!0===e[0]?.data?.[t]}let Q_=(()=>{class e{impl=null;execute(){this.impl?.execute()}static \u0275prov=pe({token:e,providedIn:"root",factory:()=>new e})}return e})();const tM=[0,1,2,3];let nM=(()=>{class e{ngZone=Y(lt);scheduler=Y(ao);errorHandler=Y(dt,{optional:!0});sequences=new Set;deferredRegistrations=new Set;executing=!1;constructor(){Y(Ou,{optional:!0})}execute(){const n=this.sequences.size>0;n&&ht(16),this.executing=!0;for(const r of tM)for(const i of this.sequences)if(!i.erroredOrDestroyed&&i.hooks[r])try{i.pipelinedValue=this.ngZone.runOutsideAngular(()=>this.maybeTrace(()=>(0,i.hooks[r])(i.pipelinedValue),i.snapshot))}catch(o){i.erroredOrDestroyed=!0,this.errorHandler?.handleError(o)}this.executing=!1;for(const r of this.sequences)r.afterRun(),r.once&&(this.sequences.delete(r),r.destroy());for(const r of this.deferredRegistrations)this.sequences.add(r);this.deferredRegistrations.size>0&&this.scheduler.notify(7),this.deferredRegistrations.clear(),n&&ht(17)}register(n){const{view:r}=n;void 0!==r?((r[25]??=[]).push(n),ro(r),r[2]|=8192):this.executing?this.deferredRegistrations.add(n):this.addSequence(n)}addSequence(n){this.sequences.add(n),this.scheduler.notify(7)}unregister(n){this.executing&&this.sequences.has(n)?(n.erroredOrDestroyed=!0,n.pipelinedValue=void 0,n.once=!0):(this.sequences.delete(n),this.deferredRegistrations.delete(n))}maybeTrace(n,r){return r?r.run(q_.AFTER_NEXT_RENDER,n):n()}static \u0275prov=pe({token:e,providedIn:"root",factory:()=>new e})}return e})();class rM{impl;hooks;view;once;snapshot;erroredOrDestroyed=!1;pipelinedValue=void 0;unregisterOnDestroy;constructor(t,n,r,i,o,a=null){this.impl=t,this.hooks=n,this.view=r,this.once=i,this.snapshot=a,this.unregisterOnDestroy=o?.onDestroy(()=>this.destroy())}afterRun(){this.erroredOrDestroyed=!1,this.pipelinedValue=void 0,this.snapshot?.dispose(),this.snapshot=null}destroy(){this.impl.unregister(this),this.unregisterOnDestroy?.();const t=this.view?.[25];t&&(this.view[25]=t.filter(n=>n!==this))}}const ew=new ee("",{providedIn:"root",factory:()=>({queue:new Set,isScheduled:!1,scheduler:null})});function oM(e,t){const n=e.get(ew);if(Array.isArray(t))for(const r of t)n.queue.add(r);else n.queue.add(t);n.scheduler&&n.scheduler(e)}function sM(e,t,n,r){const i=e?.[26]?.enter;null!==t&&i&&i.has(n.index)&&function tw(e,t){for(const[n,r]of t)oM(e,r.animateFns)}(r,i)}function Ru(e,t,n,r,i,o,a,u){if(null!=i){let f,y=!1;er(i)?f=i:zt(i)&&(y=!0,i=i[0]);const w=fn(i);0===e&&null!==r?(sM(u,r,o,n),null==a?VS(t,r,w):Ml(t,r,w,a||null,!0)):1===e&&null!==r?(sM(u,r,o,n),Ml(t,r,w,a||null,!0)):2===e?cM(u,o,n,D=>{ff(t,w,y,D)}):3===e&&cM(u,o,n,()=>{t.destroyNode(w)}),null!=f&&function N3(e,t,n,r,i,o,a){const u=r[7];u!==fn(r)&&Ru(t,e,n,o,u,i,a);for(let y=10;y=0?r[u]():r[-u].unsubscribe(),a+=2}else n[a].call(r[n[a+1]]);null!==r&&(t[7]=null);const i=t[21];if(null!==i){t[21]=null;for(let a=0;a{if(i.leave&&i.leave.has(t.index)){const a=i.leave.get(t.index),u=[];if(a)for(let f=0;f{e[26].running=void 0,ku.delete(e),t(!0)}):t(!1)}(e,r)}else e&&ku.delete(e),r(!1)})):r(!1)}function iw(e,t,n){return function uM(e,t,n){let r=t;for(;null!==r&&168&r.type;)r=(t=r).parent;if(null===r)return n[0];if(eo(r)){const{encapsulation:i}=e.data[r.directiveStart+r.componentOffset];if(i===ks.None||i===ks.Emulated)return null}return ln(r,n)}(e,t.parent,n)}let fM=function hM(e,t,n){return 40&e.type?ln(e,n):null};function sw(e,t,n,r){const i=iw(e,r,t),o=t[11],u=function dM(e,t,n){return fM(e,t,n)}(r.parent||t[5],r,t);if(null!=i)if(Array.isArray(n))for(let f=0;f27&&zS(e,t,27,!1),ht(a?2:0,i,n),n(r,i)}finally{ws(o),ht(a?3:1,i,n)}}function Km(e,t,n){(function $3(e,t,n){const r=n.directiveStart,i=n.directiveEnd;eo(n)&&function d3(e,t,n){const r=ln(t,e),i=function US(e){const t=e.tView;return null===t||t.incompleteFirstPass?e.tView=H_(1,null,e.template,e.decls,e.vars,e.directiveDefs,e.pipeDefs,e.viewQuery,e.schemas,e.consts,e.id):t}(n),o=e[10].rendererFactory,a=z_(e,zm(e,i,null,U_(n),r,t,null,o.createRenderer(r,n),null,null,null));e[t.index]=a}(t,n,e.data[r+n.componentOffset]),e.firstCreatePass||wu(n,t);const o=n.initialInputs;for(let a=r;anull;function lw(e,t,n,r,i,o){ty(e,t[1],t,n,r)?eo(e)&&function bM(e,t){const n=Fn(t,e);16&n[2]||(n[2]|=64)}(t,e.index):(3&e.type&&(n=function V3(e){return"class"===e?"className":"for"===e?"htmlFor":"formaction"===e?"formAction":"innerHtml"===e?"innerHTML":"readonly"===e?"readOnly":"tabindex"===e?"tabIndex":e}(n)),cw(e,t,n,r,i,o))}function cw(e,t,n,r,i,o){if(3&e.type){const a=ln(e,t);r=null!=o?o(r,e.value||"",n):r,i.setProperty(a,n,r)}}function H3(e,t){null!==e.hostBindings&&e.hostBindings(1,t)}function uw(e,t){const n=e.directiveRegistry;let r=null;if(n)for(let i=0;i{ro(e.lView)},consumerOnSignalRead(){this.lView[24]=this}},tB={...Eo,consumerIsAlwaysLive:!0,kind:"template",consumerMarkedDirty:e=>{let t=io(e.lView);for(;t&&!DM(t[1]);)t=io(t);t&&ch(t)},consumerOnSignalRead(){this.lView[24]=this}};function DM(e){return 2!==e.type}function xM(e){if(null===e[23])return;let t=!0;for(;t;){let n=!1;for(const r of e[23])r.dirty&&(n=!0,null===r.zone||Zone.current===r.zone?r.run():r.zone.run(()=>r.run()));t=n&&!!(8192&e[2])}}function ry(e,t=0){const r=e[10].rendererFactory;r.begin?.();try{!function rB(e,t){const n=qc();try{Fo(!0),hw(e,t);let r=0;for(;ml(e);){if(100===r)throw new G(103,!1);r++,hw(e,1)}}finally{Fo(n)}}(e,t)}finally{r.end?.()}}function SM(e,t,n,r){if(Si(t))return;const i=t[2];vh(t);let u=!0,f=null,y=null;DM(e)?(y=function Z3(e){return e[24]??function K3(e){const t=EM.pop()??Object.create(J3);return t.lView=e,t}(e)}(t),f=Hi(y)):null===function yi(){return Ye}()?(u=!1,y=function eB(e){const t=e[24]??Object.create(tB);return t.lView=e,t}(t),f=Hi(y)):t[24]&&(So(t[24]),t[24]=null);try{Cg(t),function Tg(e){return Oe.lFrame.bindingIndex=e}(e.bindingStartIndex),null!==n&&yM(e,t,n,2,r);const w=!(3&~i);if(w){const A=e.preOrderCheckHooks;null!==A&&xl(t,A,null)}else{const A=e.preOrderHooks;null!==A&&vu(t,A,0,null),bu(t,0)}if(function iB(e){for(let t=I1(e);null!==t;t=A1(t)){if(!(2&t[2]))continue;const n=t[9];for(let r=0;r0&&(n[i-1][4]=t),r0&&(e[n-1][4]=r[4]);const o=Js(e,10+t);!function aM(e,t){lM(e,t),t[0]=null,t[5]=null}(r[1],r);const a=o[18];null!==a&&a.detachView(o[1]),r[3]=null,r[4]=null,r[2]&=-129}return r}function OM(e,t){const n=e[9],r=t[3];(zt(r)||t[15]!==r[3][15])&&(e[2]|=2),null===n?e[9]=[t]:n.push(t)}class bf{_lView;_cdRefInjectingView;_appRef=null;_attachedToViewContainer=!1;exhaustive;get rootNodes(){const t=this._lView,n=t[1];return yf(n,t,n.firstChild,[])}constructor(t,n){this._lView=t,this._cdRefInjectingView=n}get context(){return this._lView[8]}set context(t){this._lView[8]=t}get destroyed(){return Si(this._lView)}destroy(){if(this._appRef)this._appRef.detachView(this);else if(this._attachedToViewContainer){const t=this._lView[3];if(er(t)){const n=t[8],r=n?n.indexOf(this):-1;r>-1&&(vf(t,r),Js(n,r))}this._attachedToViewContainer=!1}mf(this._lView[1],this._lView)}onDestroy(t){da(this._lView,t)}markForCheck(){Lu(this._cdRefInjectingView||this._lView,4)}detach(){this._lView[2]&=-129}reattach(){uh(this._lView),this._lView[2]|=128}detectChanges(){this._lView[2]|=1024,ry(this._lView)}checkNoChanges(){}attachToViewContainerRef(){if(this._appRef)throw new G(902,!1);this._attachedToViewContainer=!0}detachFromAppRef(){this._appRef=null;const t=to(this._lView),n=this._lView[16];null!==n&&!t&&nw(n,this._lView),lM(this._lView[1],this._lView)}attachToAppRef(t){if(this._attachedToViewContainer)throw new G(902,!1);this._appRef=t;const n=to(this._lView),r=this._lView[16];null!==r&&!n&&OM(r,this._lView),uh(this._lView)}}function Al(e,t,n,r,i){let o=e.data[t];if(null===o)o=function yw(e,t,n,r,i){const o=Sg(),a=Mg(),f=e.data[t]=function bB(e,t,n,r,i,o){let a=t?t.injectorIndex:-1,u=0;return function Wc(){return null!==Oe.skipHydrationRootTNode}()&&(u|=128),{type:n,index:r,insertBeforeIndex:null,injectorIndex:a,directiveStart:-1,directiveEnd:-1,directiveStylingLast:-1,componentOffset:-1,propertyBindings:null,flags:u,providerIndexes:0,value:i,attrs:o,mergedAttrs:null,localNames:null,initialInputs:null,inputs:null,hostDirectiveInputs:null,outputs:null,hostDirectiveOutputs:null,directiveToIndex:null,tView:null,next:null,prev:null,projectionNext:null,child:null,parent:t,projection:null,styles:null,stylesWithoutHost:null,residualStyles:void 0,classes:null,classesWithoutHost:null,residualClasses:void 0,classBindings:0,styleBindings:0}}(0,a?o:o&&o.parent,n,t,r,i);return function vB(e,t,n,r){null===e.firstChild&&(e.firstChild=t),null!==n&&(r?null==n.child&&null!==t.parent&&(n.child=t):null===n.next&&(n.next=t,t.prev=n))}(e,f,o,a),f}(e,t,n,r,i),function Ig(){return Oe.lFrame.inI18n}()&&(o.flags|=32);else if(64&o.type){o.type=n,o.value=r,o.attrs=i;const a=function vl(){const e=Oe.lFrame,t=e.currentTNode;return e.isParent?t:t.parent}();o.injectorIndex=null===a?-1:a.injectorIndex}return Mn(o,!0),o}function KM(e,t){let n=0,r=e.firstChild;if(r){const i=e.data.r;for(;n{class e{static \u0275prov=pe({token:e,providedIn:"root",factory:()=>null})}return e})();const Sw={};class $u{injector;parentInjector;constructor(t,n){this.injector=t,this.parentInjector=n}get(t,n,r){const i=this.injector.get(t,Sw,r);return i!==Sw||n===Sw?i:this.parentInjector.get(t,n,r)}}function fy(e,t,n){let r=n?e.styles:null,i=n?e.classes:null,o=0;if(null!==t)for(let a=0;a0;){const n=e[--t];if("number"==typeof n&&n<0)return n}return 0})(a)!=u&&a.push(u),a.push(n,r,o)}}(e,t,r,pf(e,n,i.hostVars,Mt),i)}function mV(e,t,n){if(n){if(t.exportAs)for(let r=0;r0&&(n.directiveToIndex=new Map);for(let k=0;kf?u[f]:null}"string"==typeof a&&(o+=2)}return null}(t,n,o,e.index)),null!==w)(w.__ngLastListenerFn__||w).__ngNextListenerFn__=a,w.__ngLastListenerFn__=a,y=!0;else{const D=ln(e,n),k=r?r(D):D,A=i.listen(k,o,u);(function wV(e){return e.startsWith("animation")||e.startsWith("transition")})(o)||fT(r?L=>r(fn(L[e.index])):e.index,t,n,o,u,A,!1)}return y}function fT(e,t,n,r,i,o,a){const u=t.firstCreatePass?function oo(e){return e.cleanup??=[]}(t):null,f=function Dg(e){return e[7]??=[]}(n),y=f.length;f.push(i,o),u&&u.push(r,e,y,(y+1)*(a?-1:1))}function my(e,t,n,r,i,o){const u=t[1],D=t[n][u.data[n].outputs[r]].subscribe(o);fT(e.index,u,t,i,o,D,!0)}const Ra=Symbol("BINDING");class pT extends Vu{ngModule;constructor(t){super(),this.ngModule=t}resolveComponentFactory(t){const n=ct(t);return new Aw(n,this.ngModule)}}class Aw extends rT{componentDef;ngModule;selector;componentType;ngContentSelectors;isBoundToModule;cachedInputs=null;cachedOutputs=null;get inputs(){return this.cachedInputs??=function AV(e){return Object.keys(e).map(t=>{const[n,r,i]=e[t],o={propName:n,templateName:t,isSignal:0!==(r&Wm.SignalBased)};return i&&(o.transform=i),o})}(this.componentDef.inputs),this.cachedInputs}get outputs(){return this.cachedOutputs??=function kV(e){return Object.keys(e).map(t=>({propName:e[t],templateName:t}))}(this.componentDef.outputs),this.cachedOutputs}constructor(t,n){super(),this.componentDef=t,this.ngModule=n,this.componentType=t.type,this.selector=function s3(e){return e.map(o3).join(",")}(t.selectors),this.ngContentSelectors=t.ngContentSelectors??[],this.isBoundToModule=!!n}create(t,n,r,i,o,a){ht(22);const u=ye(null);try{const f=this.componentDef,y=function LV(e,t,n,r){const i=e?["ng-version","20.3.10"]:function a3(e){const t=[],n=[];let r=1,i=2;for(;r{if(1&n&&e)for(const r of e)r.create();if(2&n&&t)for(const r of t)r.update()}:null}(o,a),1,u,f,null,null,null,[i],null)}(r,f,a,o),w=function OV(e,t,n){let r=t instanceof Dn?t:t?.injector;return r&&null!==e.getStandaloneInjector&&(r=e.getStandaloneInjector(r)||r),r?new $u(n,r):n}(f,i||this.ngModule,t),D=function RV(e){const t=e.get(Dw,null);if(null===t)throw new G(407,!1);return{rendererFactory:t,sanitizer:e.get(iV,null),changeDetectionScheduler:e.get(ao,null),ngReflect:!1}}(w),k=D.rendererFactory.createRenderer(null,f),A=r?function L3(e,t,n,r){const o=r.get(LL,!1)||n===ks.ShadowDom,a=e.selectRootElement(t,o);return function F3(e){vM(e)}(a),a}(k,r,f.encapsulation,w):function NV(e,t){const n=function PV(e){return(e.selectors[0][0]||"div").toLowerCase()}(e);return Um(t,n,"svg"===n?"svg":"math"===n?"math":null)}(f,k),N=a?.some(gT)||o?.some(V=>"function"!=typeof V&&V.bindings.some(gT)),L=zm(null,y,null,512|U_(f),null,null,D,k,w,null,null);L[27]=A,vh(L);let H=null;try{const V=Mw(27,L,2,"#host",()=>y.directiveRegistry,!0,0);HS(k,A,V),rr(A,L),Km(y,L,V),k_(y,V,L),Tw(y,V),void 0!==n&&function VV(e,t,n){const r=e.projection=[];for(let i=0;iclass e{static __NG_ELEMENT_ID__=$V})();function $V(){return function vT(e,t){let n;const r=t[e.index];return er(r)?n=r:(n=AM(r,t,null,e),t[e.index]=n,z_(t,n)),bT(n,t,e,r),new mT(n,e,t)}(Ae(),K())}const jV=fo,mT=class extends jV{_lContainer;_hostTNode;_hostLView;constructor(t,n,r){super(),this._lContainer=t,this._hostTNode=n,this._hostLView=r}get element(){return le(this._hostTNode,this._hostLView)}get injector(){return new b(this._hostTNode,this._hostLView)}get parentInjector(){const t=Cu(this._hostTNode,this._hostLView);if(_u(t)){const n=Sa(t,this._hostLView),r=xa(t);return new b(n[1].data[r+8],n)}return new b(null,this._hostLView)}clear(){for(;this.length>0;)this.remove(this.length-1)}get(t){const n=yT(this._lContainer);return null!==n&&n[t]||null}get length(){return this._lContainer.length-10}createEmbeddedView(t,n,r){let i,o;"number"==typeof r?i=r:null!=r&&(i=r.index,o=r.injector);const u=t.createEmbeddedViewImpl(n||{},o,null);return this.insertImpl(u,i,Il(this._hostTNode,null)),u}createComponent(t,n,r,i,o,a,u){const f=t&&!function Ca(e){return"function"==typeof e}(t);let y;if(f)y=n;else{const H=n||{};y=H.index,r=H.injector,i=H.projectableNodes,o=H.environmentInjector||H.ngModuleRef,a=H.directives,u=H.bindings}const w=f?t:new Aw(ct(t)),D=r||this.parentInjector;if(!o&&null==w.ngModule){const V=(f?D:this.parentInjector).get(Dn,null);V&&(o=V)}ct(w.componentType??{});const L=w.create(D,i,null,o,a,u);return this.insertImpl(L.hostView,y,Il(this._hostTNode,null)),L}insert(t,n){return this.insertImpl(t,n,!0)}insertImpl(t,n,r){const i=t._lView;if(function wg(e){return er(e[3])}(i)){const u=this.indexOf(t);if(-1!==u)this.detach(u);else{const f=i[3],y=new mT(f,f[5],f[3]);y.detach(y.indexOf(t))}}const o=this._adjustIndex(n),a=this._lContainer;return Fu(a,i,o,r),t.attachToViewContainerRef(),Nc(kw(a),o,t),t}move(t,n){return this.insert(t,n)}indexOf(t){const n=yT(this._lContainer);return null!==n?n.indexOf(t):-1}remove(t){const n=this._adjustIndex(t,-1),r=vf(this._lContainer,n);r&&(Js(kw(this._lContainer),n),mf(r[1],r))}detach(t){const n=this._adjustIndex(t,-1),r=vf(this._lContainer,n);return r&&null!=Js(kw(this._lContainer),n)?new bf(r):null}_adjustIndex(t,n=0){return t??this.length+n}};function yT(e){return e[8]}function kw(e){return e[8]||(e[8]=[])}let bT=function wT(e,t,n,r){if(e[7])return;let i;i=8&n.type?fn(r):function HV(e,t){const n=e[11],r=n.createComment(""),i=ln(t,e),o=n.parentNode(i);return Ml(n,o,r,n.nextSibling(i),!1),r}(t,n),e[7]=i};let Nl=class{},RT=class{};class Hw extends Nl{ngModuleType;_parent;_bootstrapComponents=[];_r3Injector;instance;destroyCbs=[];componentFactoryResolver=new pT(this);constructor(t,n,r,i=!0){super(),this.ngModuleType=t,this._parent=n;const o=Nr(t);this._bootstrapComponents=Wo(o.bootstrap),this._r3Injector=_h(t,n,[{provide:Nl,useValue:this},{provide:Vu,useValue:this.componentFactoryResolver},...r],Qn(t),new Set(["environment"])),i&&this.resolveInjectorInitializers()}resolveInjectorInitializers(){this._r3Injector.resolveInjectorInitializers(),this.instance=this._r3Injector.get(this.ngModuleType)}get injector(){return this._r3Injector}destroy(){const t=this._r3Injector;!t.destroyed&&t.destroy(),this.destroyCbs.forEach(n=>n()),this.destroyCbs=null}onDestroy(t){this.destroyCbs.push(t)}}class NT extends RT{moduleType;constructor(t){super(),this.moduleType=t}create(t){return new Hw(this.moduleType,t,[])}}class PT extends Nl{injector;componentFactoryResolver=new pT(this);instance=null;constructor(t){super();const n=new vs([...t.providers,{provide:Nl,useValue:this},{provide:Vu,useValue:this.componentFactoryResolver}],t.parent||ia(),t.debugName,new Set(["environment"]));this.injector=n,t.runEnvironmentInitializers&&n.resolveInjectorInitializers()}destroy(){this.injector.destroy()}onDestroy(t){this.injector.onDestroy(t)}}function Uw(e,t,n=null){return new PT({providers:e,parent:t,debugName:n,runEnvironmentInitializers:!0}).injector}let l$=(()=>{class e{_injector;cachedInjectors=new Map;constructor(n){this._injector=n}getOrCreateStandaloneInjector(n){if(!n.standalone)return null;if(!this.cachedInjectors.has(n)){const r=Pc(0,n.type),i=r.length>0?Uw([r],this._injector,`Standalone[${n.type.name}]`):null;this.cachedInjectors.set(n,i)}return this.cachedInjectors.get(n)}ngOnDestroy(){try{for(const n of this.cachedInjectors.values())null!==n&&n.destroy()}finally{this.cachedInjectors.clear()}}static \u0275prov=pe({token:e,providedIn:"environment",factory:()=>new e(ke(Dn))})}return e})();function qo(e){return Ai(()=>{const t=FT(e),n={...t,decls:e.decls,vars:e.vars,template:e.template,consts:e.consts||null,ngContentSelectors:e.ngContentSelectors,onPush:e.changeDetection===Xe.OnPush,directiveDefs:null,pipeDefs:null,dependencies:t.standalone&&e.dependencies||null,getStandaloneInjector:t.standalone?i=>i.get(l$).getOrCreateStandaloneInjector(n):null,getExternalStyles:null,signals:e.signals??!1,data:e.data||{},encapsulation:e.encapsulation||ks.Emulated,styles:e.styles||Rt,_:null,schemas:e.schemas||null,tView:null,id:""};t.standalone&&gr("NgStandalone"),BT(n);const r=e.dependencies;return n.directiveDefs=yy(r,LT),n.pipeDefs=yy(r,ei),n.id=function h$(e){let t=0;const r=[e.selectors,e.ngContentSelectors,e.hostVars,e.hostAttrs,"function"==typeof e.consts?"":e.consts,e.vars,e.decls,e.encapsulation,e.standalone,e.signals,e.exportAs,JSON.stringify(e.inputs),JSON.stringify(e.outputs),Object.getOwnPropertyNames(e.type.prototype),!!e.contentQueries,!!e.viewQuery];for(const o of r.join("|"))t=Math.imul(31,t)+o.charCodeAt(0)|0;return t+=2147483648,"c"+t}(n),n})}function LT(e){return ct(e)||En(e)}function Of(e){return Ai(()=>({type:e.type,bootstrap:e.bootstrap||Rt,declarations:e.declarations||Rt,imports:e.imports||Rt,exports:e.exports||Rt,transitiveCompileScopes:null,schemas:e.schemas||null,id:e.id||null}))}function c$(e,t){if(null==e)return zn;const n={};for(const r in e)if(e.hasOwnProperty(r)){const i=e[r];let o,a,u,f;Array.isArray(i)?(u=i[0],o=i[1],a=i[2]??o,f=i[3]||null):(o=i,a=i,u=Wm.None,f=null),n[o]=[r,u,f],t[o]=a}return n}function u$(e){if(null==e)return zn;const t={};for(const n in e)e.hasOwnProperty(n)&&(t[e[n]]=n);return t}function Ne(e){return Ai(()=>{const t=FT(e);return BT(t),t})}function Hr(e){return{type:e.type,name:e.name,factory:null,pure:!1!==e.pure,standalone:e.standalone??!0,onDestroy:e.type.prototype.ngOnDestroy||null}}function FT(e){const t={};return{type:e.type,providersResolver:null,factory:null,hostBindings:e.hostBindings||null,hostVars:e.hostVars||0,hostAttrs:e.hostAttrs||null,contentQueries:e.contentQueries||null,declaredInputs:t,inputConfig:e.inputs||zn,exportAs:e.exportAs||null,standalone:e.standalone??!0,signals:!0===e.signals,selectors:e.selectors||Rt,viewQuery:e.viewQuery||null,features:e.features||null,setInput:null,resolveHostDirectives:null,hostDirectives:null,inputs:c$(e.inputs,t),outputs:u$(e.outputs),debugInfo:null}}function BT(e){e.features?.forEach(t=>t(e))}function yy(e,t){return e?()=>{const n="function"==typeof e?e():e,r=[];for(const i of n){const o=t(i);null!==o&&r.push(o)}return r}:null}function Tt(e){let t=function VT(e){return Object.getPrototypeOf(e.prototype).constructor}(e.type),n=!0;const r=[e];for(;t;){let i;if(_r(e))i=t.\u0275cmp||t.\u0275dir;else{if(t.\u0275cmp)throw new G(903,!1);i=t.\u0275dir}if(i){if(n){r.push(i);const a=e;a.inputs=zw(e.inputs),a.declaredInputs=zw(e.declaredInputs),a.outputs=zw(e.outputs);const u=i.hostBindings;u&&y$(e,u);const f=i.viewQuery,y=i.contentQueries;if(f&&g$(e,f),y&&m$(e,y),f$(e,i),N0(e.outputs,i.outputs),_r(i)&&i.data.animation){const w=e.data;w.animation=(w.animation||[]).concat(i.data.animation)}}const o=i.features;if(o)for(let a=0;a=0;r--){const i=e[r];i.hostVars=t+=i.hostVars,i.hostAttrs=Is(i.hostAttrs,n=Is(n,i.hostAttrs))}}(r)}function f$(e,t){for(const n in t.inputs){if(!t.inputs.hasOwnProperty(n)||e.inputs.hasOwnProperty(n))continue;const r=t.inputs[n];void 0!==r&&(e.inputs[n]=r,e.declaredInputs[n]=t.declaredInputs[n])}}function zw(e){return e===zn?{}:e===Rt?[]:e}function g$(e,t){const n=e.viewQuery;e.viewQuery=n?(r,i)=>{t(r,i),n(r,i)}:t}function m$(e,t){const n=e.contentQueries;e.contentQueries=n?(r,i,o)=>{t(r,i,o),n(r,i,o)}:t}function y$(e,t){const n=e.hostBindings;e.hostBindings=n?(r,i)=>{t(r,i),n(r,i)}:t}function $T(e){const t=n=>{const r=Array.isArray(e);null===n.hostDirectives?(n.resolveHostDirectives=w$,n.hostDirectives=r?e.map(Ww):[e]):r?n.hostDirectives.unshift(...e.map(Ww)):n.hostDirectives.unshift(e)};return t.ngInherit=!0,t}function w$(e){const t=[];let n=!1,r=null,i=null;for(let o=0;o{class e{log(n){console.log(n)}warn(n){console.warn(n)}static \u0275fac=function(r){return new(r||e)};static \u0275prov=pe({token:e,factory:e.\u0275fac,providedIn:"platform"})}return e})();const EI=new ee("");function Ff(e){return!!e&&"function"==typeof e.then}function DI(e){return!!e&&"function"==typeof e.subscribe}const xI=new ee("");let SI=(()=>{class e{resolve;reject;initialized=!1;done=!1;donePromise=new Promise((n,r)=>{this.resolve=n,this.reject=r});appInits=Y(xI,{optional:!0})??[];injector=Y(Gn);constructor(){}runInitializers(){if(this.initialized)return;const n=[];for(const i of this.appInits){const o=xn(this.injector,i);if(Ff(o))n.push(o);else if(DI(o)){const a=new Promise((u,f)=>{o.subscribe({complete:u,error:f})});n.push(a)}}const r=()=>{this.done=!0,this.resolve()};Promise.all(n).then(()=>{r()}).catch(i=>{this.reject(i)}),0===n.length&&r(),this.initialized=!0}static \u0275fac=function(r){return new(r||e)};static \u0275prov=pe({token:e,factory:e.\u0275fac,providedIn:"root"})}return e})();const MI=new ee("");let Yo=(()=>{class e{_runningTick=!1;_destroyed=!1;_destroyListeners=[];_views=[];internalErrorHandler=Y(ri);afterRenderManager=Y(Q_);zonelessEnabled=Y(Dh);rootEffectScheduler=Y(Br);dirtyFlags=0;tracingSnapshot=null;allTestViews=new Set;autoDetectTestViews=new Set;includeAllTestViews=!1;afterTick=new Kn;get allViews(){return[...(this.includeAllTestViews?this.allTestViews:this.autoDetectTestViews).keys(),...this._views]}get destroyed(){return this._destroyed}componentTypes=[];components=[];internalPendingTask=Y(qn);get isStable(){return this.internalPendingTask.hasPendingTasksObservable.pipe(tt(n=>!n))}constructor(){Y(Ou,{optional:!0})}whenStable(){let n;return new Promise(r=>{n=this.isStable.subscribe({next:i=>{i&&r()}})}).finally(()=>{n.unsubscribe()})}_injector=Y(Dn);_rendererFactory=null;get injector(){return this._injector}bootstrap(n,r){return this.bootstrapImpl(n,r)}bootstrapImpl(n,r,i=Gn.NULL){return this._injector.get(lt).run(()=>{ht(10);const a=n instanceof rT;if(!this._injector.get(SI).done)throw new G(405,"");let f;f=a?n:this._injector.get(Vu).resolveComponentFactory(n),this.componentTypes.push(f.componentType);const y=function Rj(e){return e.isBoundToModule}(f)?void 0:this._injector.get(Nl),D=f.create(i,[],r||f.selector,y),k=D.location.nativeElement,A=D.injector.get(EI,null);return A?.registerApplication(k),D.onDestroy(()=>{this.detachView(D.hostView),Sy(this.components,D),A?.unregisterApplication(k)}),this._loadComponent(D),ht(11,D),D})}tick(){this.zonelessEnabled||(this.dirtyFlags|=1),this._tick()}_tick(){ht(12),null!==this.tracingSnapshot?this.tracingSnapshot.run(q_.CHANGE_DETECTION,this.tickImpl):this.tickImpl()}tickImpl=()=>{if(this._runningTick)throw new G(101,!1);const n=ye(null);try{this._runningTick=!0,this.synchronize()}finally{this._runningTick=!1,this.tracingSnapshot?.dispose(),this.tracingSnapshot=null,ye(n),this.afterTick.next(),ht(13)}};synchronize(){null===this._rendererFactory&&!this._injector.destroyed&&(this._rendererFactory=this._injector.get(Dw,null,{optional:!0}));let n=0;for(;0!==this.dirtyFlags&&n++<10;)ht(14),this.synchronizeOnce(),ht(15)}synchronizeOnce(){16&this.dirtyFlags&&(this.dirtyFlags&=-17,this.rootEffectScheduler.flush());let n=!1;if(7&this.dirtyFlags){const r=!!(1&this.dirtyFlags);this.dirtyFlags&=-8,this.dirtyFlags|=8;for(let{_lView:i}of this.allViews)(r||ml(i))&&(ry(i,r&&!this.zonelessEnabled?0:1),n=!0);if(this.dirtyFlags&=-5,this.syncDirtyFlagsWithViews(),23&this.dirtyFlags)return}n||(this._rendererFactory?.begin?.(),this._rendererFactory?.end?.()),8&this.dirtyFlags&&(this.dirtyFlags&=-9,this.afterRenderManager.execute()),this.syncDirtyFlagsWithViews()}syncDirtyFlagsWithViews(){this.allViews.some(({_lView:n})=>ml(n))?this.dirtyFlags|=2:this.dirtyFlags&=-8}attachView(n){const r=n;this._views.push(r),r.attachToAppRef(this)}detachView(n){const r=n;Sy(this._views,r),r.detachFromAppRef()}_loadComponent(n){this.attachView(n.hostView);try{this.tick()}catch(i){this.internalErrorHandler(i)}this.components.push(n),this._injector.get(MI,[]).forEach(i=>i(n))}ngOnDestroy(){if(!this._destroyed)try{this._destroyListeners.forEach(n=>n()),this._views.slice().forEach(n=>n.destroy())}finally{this._destroyed=!0,this._views=[],this._destroyListeners=[]}}onDestroy(n){return this._destroyListeners.push(n),()=>Sy(this._destroyListeners,n)}destroy(){if(this._destroyed)throw new G(406,!1);const n=this._injector;n.destroy&&!n.destroyed&&n.destroy()}get viewCount(){return this._views.length}static \u0275fac=function(r){return new(r||e)};static \u0275prov=pe({token:e,factory:e.\u0275fac,providedIn:"root"})}return e})();function Sy(e,t){const n=e.indexOf(t);n>-1&&e.splice(n,1)}typeof document<"u"&&document;class A5{destroy(t){}updateValue(t,n){}swap(t,n){const r=Math.min(t,n),i=Math.max(t,n),o=this.detach(i);if(i-r>1){const a=this.detach(r);this.attach(r,o),this.attach(i,a)}else this.attach(r,o)}move(t,n){this.attach(n,this.detach(t,!0))}}function dC(e,t,n,r,i){return e===n&&Object.is(t,r)?1:Object.is(i(e,t),i(n,r))?-1:0}function hC(e,t,n,r){return!(void 0===t||!t.has(r)||(e.attach(n,t.get(r)),t.delete(r),0))}function jI(e,t,n,r,i){if(hC(e,t,r,n(r,i)))e.updateValue(r,i);else{const o=e.create(r,i);e.attach(r,o)}}function HI(e,t,n,r){const i=new Set;for(let o=t;o<=n;o++)i.add(r(o,e.at(o)));return i}class UI{kvMap=new Map;_vMap=void 0;has(t){return this.kvMap.has(t)}delete(t){if(!this.has(t))return!1;const n=this.kvMap.get(t);return void 0!==this._vMap&&this._vMap.has(n)?(this.kvMap.set(t,this._vMap.get(n)),this._vMap.delete(n)):this.kvMap.delete(t),!0}get(t){return this.kvMap.get(t)}set(t,n){if(this.kvMap.has(t)){let r=this.kvMap.get(t);void 0===this._vMap&&(this._vMap=new Map);const i=this._vMap;for(;i.has(r);)r=i.get(r);i.set(r,n)}else this.kvMap.set(t,n)}forEach(t){for(let[n,r]of this.kvMap)if(t(r,n),void 0!==this._vMap){const i=this._vMap;for(;i.has(r);)r=i.get(r),t(r,n)}}}function or(e,t,n,r,i,o,a,u){gr("NgControlFlow");const f=K(),y=Ve();return Pl(f,y,e,t,n,r,i,Wn(y.consts,o),256,a,u),fC}function fC(e,t,n,r,i,o,a,u){gr("NgControlFlow");const f=K(),y=Ve();return Pl(f,y,e,t,n,r,i,Wn(y.consts,o),512,a,u),fC}function sr(e,t){gr("NgControlFlow");const n=K(),r=fr(),i=n[r]!==Mt?n[r]:-1,o=-1!==i?Py(n,27+i):void 0;if(qt(n,r,e)){const u=ye(null);try{if(void 0!==o&&fw(o,0),-1!==e){const f=27+e,y=Py(n,f),w=pC(n[1],f),D=null;Fu(y,Pu(n,w,t,{dehydratedView:D}),0,Il(w,D))}}finally{ye(u)}}else if(void 0!==o){const u=kM(o,0);void 0!==u&&(u[8]=t)}}class O5{lContainer;$implicit;$index;constructor(t,n,r){this.lContainer=t,this.$implicit=n,this.$index=r}get $count(){return this.lContainer.length-10}}function zI(e,t){return t}class N5{hasEmptyBlock;trackByFn;liveCollection;constructor(t,n,r){this.hasEmptyBlock=t,this.trackByFn=n,this.liveCollection=r}}function La(e,t,n,r,i,o,a,u,f,y,w,D,k){gr("NgControlFlow");const A=K(),N=Ve(),L=void 0!==f,H=K(),V=u?a.bind(H[15][8]):a,z=new N5(L,V);H[27+e]=z,Pl(A,N,e+1,t,n,r,i,Wn(N.consts,o),256),L&&Pl(A,N,e+2,f,y,w,D,Wn(N.consts,k),512)}class P5 extends A5{lContainer;hostLView;templateTNode;operationsCounter=void 0;needsIndexUpdate=!1;constructor(t,n,r){super(),this.lContainer=t,this.hostLView=n,this.templateTNode=r}get length(){return this.lContainer.length-10}at(t){return this.getLView(t)[8].$implicit}attach(t,n){const r=n[6];this.needsIndexUpdate||=t!==this.length,Fu(this.lContainer,n,t,Il(this.templateTNode,r))}detach(t,n){return this.needsIndexUpdate||=t!==this.length-1,n&&function L5(e,t){if(e.length<=10)return;const r=e[10+t];r&&r[26]&&(r[26].skipLeaveAnimations=!0)}(this.lContainer,t),function F5(e,t){return vf(e,t)}(this.lContainer,t)}create(t,n){const i=Pu(this.hostLView,this.templateTNode,new O5(this.lContainer,n,t),{dehydratedView:null});return this.operationsCounter?.recordCreate(),i}destroy(t){mf(t[1],t),this.operationsCounter?.recordDestroy()}updateValue(t,n){this.getLView(t)[8].$implicit=n}reset(){this.needsIndexUpdate=!1,this.operationsCounter?.reset()}updateIndexes(){if(this.needsIndexUpdate)for(let t=0;t{e.destroy(f)})}(f,e,o.trackByFn),f.updateIndexes(),o.hasEmptyBlock){const y=fr(),w=0===f.length;if(qt(r,y,w)){const D=n+2,k=Py(r,D);if(w){const A=pC(i,D),N=null;Fu(k,Pu(r,A,void 0,{dehydratedView:N}),0,Il(A,N))}else i.firstUpdatePass&&function cy(e){const t=e[6]??[],r=e[3][11],i=[];for(const o of t)void 0!==o.data.di?i.push(o):KM(o,r);e[6]=i}(k),fw(k,0)}}}finally{ye(t)}}function Py(e,t){return e[t]}function pC(e,t){return ua(e,t)}function Ni(e,t,n){const r=K();return qt(r,fr(),t)&&(Ve(),lw(gt(),r,e,t,r[11],n)),Ni}function gC(e,t,n,r,i){ty(t,e,n,i?"class":"style",r)}function se(e,t,n,r){const i=K(),o=i[1],a=e+27,u=o.firstCreatePass?Mw(a,i,2,t,uw,function fh(){return Oe.bindingsEnabled}(),n,r):o.data[a];if(Jm(u,i,e,t,mC),la(u)){const f=i[1];Km(f,i,u),k_(f,u,i)}return null!=r&&Nu(i,u),se}function ce(){const e=Ve(),n=ey(Ae());return e.firstCreatePass&&Tw(e,n),xg(n)&&Gc(),so(),null!=n.classesWithoutHost&&function s_(e){return!!(8&e.flags)}(n)&&gC(e,n,K(),n.classesWithoutHost,!0),null!=n.stylesWithoutHost&&function wm(e){return!!(16&e.flags)}(n)&&gC(e,n,K(),n.stylesWithoutHost,!1),ce}function Yn(e,t,n,r){return se(e,t,n,r),ce(),Yn}function He(e,t,n,r){const i=K(),o=i[1],a=e+27,u=o.firstCreatePass?function uT(e,t,n,r,i,o){const a=t.consts,f=Al(t,e,n,r,Wn(a,i));if(f.mergedAttrs=Is(f.mergedAttrs,f.attrs),null!=o){const y=Wn(a,o);f.localNames=[];for(let w=0;w(bl(!0),Um(t[11],r,function ha(){return Oe.lFrame.currentNamespace}()));function Zo(){return K()}function li(e,t,n){const r=K();return qt(r,fr(),t)&&(Ve(),cw(gt(),r,e,t,r[11],n)),li}const Fy=void 0;var H5=["en",[["a","p"],["AM","PM"]],[["AM","PM"]],[["S","M","T","W","T","F","S"],["Sun","Mon","Tue","Wed","Thu","Fri","Sat"],["Sunday","Monday","Tuesday","Wednesday","Thursday","Friday","Saturday"],["Su","Mo","Tu","We","Th","Fr","Sa"]],Fy,[["J","F","M","A","M","J","J","A","S","O","N","D"],["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"],["January","February","March","April","May","June","July","August","September","October","November","December"]],Fy,[["B","A"],["BC","AD"],["Before Christ","Anno Domini"]],0,[6,0],["M/d/yy","MMM d, y","MMMM d, y","EEEE, MMMM d, y"],["h:mm a","h:mm:ss a","h:mm:ss a z","h:mm:ss a zzzz"],["{1}, {0}",Fy,"{1} 'at' {0}",Fy],[".",",",";","%","+","-","E","\xd7","\u2030","\u221e","NaN",":"],["#,##0.###","#,##0%","\xa4#,##0.00","#E0"],"USD","$","US Dollar",{},"ltr",function j5(e){const t=Math.floor(Math.abs(e)),n=e.toString().replace(/^[^.]*\.?/,"").length;return 1===t&&0===n?1:5}];let Zu={};function Dr(e){const t=function U5(e){return e.toLowerCase().replace(/_/g,"-")}(e);let n=ZI(t);if(n)return n;const r=t.split("-")[0];if(n=ZI(r),n)return n;if("en"===r)return H5;throw new G(701,!1)}function ZI(e){return e in Zu||(Zu[e]=Xt.ng&&Xt.ng.common&&Xt.ng.common.locales&&Xt.ng.common.locales[e]),Zu[e]}var Yt=function(e){return e[e.LocaleId=0]="LocaleId",e[e.DayPeriodsFormat=1]="DayPeriodsFormat",e[e.DayPeriodsStandalone=2]="DayPeriodsStandalone",e[e.DaysFormat=3]="DaysFormat",e[e.DaysStandalone=4]="DaysStandalone",e[e.MonthsFormat=5]="MonthsFormat",e[e.MonthsStandalone=6]="MonthsStandalone",e[e.Eras=7]="Eras",e[e.FirstDayOfWeek=8]="FirstDayOfWeek",e[e.WeekendRange=9]="WeekendRange",e[e.DateFormat=10]="DateFormat",e[e.TimeFormat=11]="TimeFormat",e[e.DateTimeFormat=12]="DateTimeFormat",e[e.NumberSymbols=13]="NumberSymbols",e[e.NumberFormats=14]="NumberFormats",e[e.CurrencyCode=15]="CurrencyCode",e[e.CurrencySymbol=16]="CurrencySymbol",e[e.CurrencyName=17]="CurrencyName",e[e.Currencies=18]="Currencies",e[e.Directionality=19]="Directionality",e[e.PluralCase=20]="PluralCase",e[e.ExtraData=21]="ExtraData",e}(Yt||{});const By="en-US";let KI=By;function rn(e,t,n){const r=K(),i=Ve(),o=Ae();return function CC(e,t,n,r,i,o,a){let u=!0,f=null;if((3&r.type||a)&&(f??=Af(r,t,o),hT(r,e,t,a,n,i,o,f)&&(u=!1)),u){const y=r.outputs?.[i],w=r.hostDirectiveOutputs?.[i];if(w&&w.length)for(let D=0;D0;)t=t[14],e--;return t}(e,Oe.lFrame.contextLView))[8]}(e)}function Hy(e,t){return e<<17|t<<2}function Ul(e){return e>>17&32767}function EC(e){return 2|e}function Ku(e){return(131068&e)>>2}function DC(e,t){return-131069&e|t<<2}function xC(e){return 1|e}function bA(e,t,n,r){const i=e[n+1],o=null===t;let a=r?Ul(i):Ku(i),u=!1;for(;0!==a&&(!1===u||o);){const y=e[a+1];zH(e[a],t)&&(u=!0,e[a+1]=r?xC(y):EC(y)),a=r?Ul(y):Ku(y)}u&&(e[n+1]=r?EC(i):xC(i))}function zH(e,t){return null===e||null==t||(Array.isArray(e)?e[1]:e)===t||!(!Array.isArray(e)||"string"!=typeof t)&&Jr(e,t)>=0}function zf(e,t){return function MA(e,t,n,r){const i=K(),o=Ve(),a=Mi(2);o.firstUpdatePass&&function AA(e,t,n,r){const i=e.data;if(null===i[n+1]){const o=i[Tn()],a=function IA(e,t){return t>=e.expandoStartIndex}(e,n);(function NA(e,t){return!!(e.flags&(t?8:16))})(o,r)&&null===t&&!a&&(t=!1),t=function tU(e,t,n,r){const i=function Bt(e){const t=Oe.lFrame.currentDirectiveIndex;return-1===t?null:e[t]}(e);let o=r?t.residualClasses:t.residualStyles;if(null===i)0===(r?t.classBindings:t.styleBindings)&&(n=Wf(n=SC(null,e,t,n,r),t.attrs,r),o=null);else{const a=t.directiveStylingLast;if(-1===a||e[a]!==i)if(n=SC(i,e,t,n,r),null===o){let f=function nU(e,t,n){const r=n?t.classBindings:t.styleBindings;if(0!==Ku(r))return e[Ul(r)]}(e,t,r);void 0!==f&&Array.isArray(f)&&(f=SC(null,e,t,f[1],r),f=Wf(f,t.attrs,r),function rU(e,t,n,r){e[Ul(n?t.classBindings:t.styleBindings)]=r}(e,t,r,f))}else o=function iU(e,t,n){let r;const i=t.directiveEnd;for(let o=1+t.directiveStylingLast;o0)&&(y=!0)):w=n,i)if(0!==f){const k=Ul(e[u+1]);e[r+1]=Hy(k,u),0!==k&&(e[k+1]=DC(e[k+1],r)),e[u+1]=function $H(e,t){return 131071&e|t<<17}(e[u+1],r)}else e[r+1]=Hy(u,0),0!==u&&(e[u+1]=DC(e[u+1],r)),u=r;else e[r+1]=Hy(f,0),0===u?u=r:e[f+1]=DC(e[f+1],r),f=r;y&&(e[r+1]=EC(e[r+1])),bA(e,w,r,!0),bA(e,w,r,!1),function UH(e,t,n,r,i){const o=i?e.residualClasses:e.residualStyles;null!=o&&"string"==typeof t&&Jr(o,t)>=0&&(n[r+1]=xC(n[r+1]))}(t,w,e,r,o),a=Hy(u,f),o?t.classBindings=a:t.styleBindings=a}(i,o,t,n,a,r)}}(o,e,a,r),t!==Mt&&qt(i,a,t)&&function OA(e,t,n,r,i,o,a,u){if(!(3&t.type))return;const f=e.data,y=f[u+1],w=function jH(e){return!(1&~e)}(y)?RA(f,t,n,i,Ku(y),a):void 0;Uy(w)||(Uy(o)||function VH(e){return!(2&~e)}(y)&&(o=RA(f,null,n,i,u,a)),function P3(e,t,n,r,i){if(t)i?e.addClass(n,r):e.removeClass(n,r);else{let o=-1===r.indexOf("-")?void 0:ka.DashCase;null==i?e.removeStyle(n,r,o):("string"==typeof i&&i.endsWith("!important")&&(i=i.slice(0,-10),o|=ka.Important),e.setStyle(n,r,i,o))}}(r,a,ca(Tn(),n),i,o))}(o,o.data[Tn()],i,i[11],e,i[a+1]=function lU(e,t){return null==e||""===e||("string"==typeof t?e+=t:"object"==typeof e&&(e=Qn(function Aa(e){return e instanceof gS?e.changingThisBreaksApplicationSecurity:e}(e)))),e}(t,n),r,a)}(e,t,null,!0),zf}function SC(e,t,n,r,i){let o=null;const a=n.directiveEnd;let u=n.directiveStylingLast;for(-1===u?u=n.directiveStart:u++;u0;){const f=e[i],y=Array.isArray(f),w=y?f[1]:f,D=null===w;let k=n[i+1];k===Mt&&(k=D?Rt:void 0);let A=D?ea(k,r):w===r?k:void 0;if(y&&!Uy(A)&&(A=ea(f,r)),Uy(A)&&(u=A,a))return u;const N=e[i+1];i=a?Ul(N):Ku(N)}if(null!==t){let f=o?t.residualClasses:t.residualStyles;null!=f&&(u=ea(f,r))}return u}function Uy(e){return void 0!==e}function te(e,t=""){const n=K(),r=Ve(),i=e+27,o=r.firstCreatePass?Al(r,i,1,t,null):r.data[i],a=PA(r,n,o,t,e);n[i]=a,xt()&&sw(r,n,a,o),Mn(o,!1)}let PA=(e,t,n,r,i)=>(bl(!0),function $_(e,t){return e.createText(t)}(t[11],r));function BA(e,t,n,r,i,o=""){const u=Rl(e,function Fr(){return Oe.lFrame.bindingIndex}(),n,i);return Mi(2),u?t+Le(n)+r+Le(i)+o:Mt}function Pt(e){return Pi("",e),Pt}function Pi(e,t,n){const r=K(),i=function FA(e,t,n,r=""){return qt(e,fr(),n)?t+Le(n)+r:Mt}(r,e,t,n);return i!==Mt&&Ps(r,Tn(),i),Pi}function Gf(e,t,n,r,i){const o=K(),a=BA(o,e,t,n,r,i);return a!==Mt&&Ps(o,Tn(),a),Gf}function Ps(e,t,n){const r=ca(t,e);!function BS(e,t,n){e.setValue(t,n)}(e[11],r,n)}function MC(e,t,n,r,i){if(e=je(e),Array.isArray(e))for(let o=0;o>20;if(Jn(e)||!e.multi){const A=new Da(y,i,re,null),N=IC(f,t,i?w:w+k,D);-1===N?(rf(wu(u,a),o,f),TC(o,e,t.length),t.push(f),u.directiveStart++,u.directiveEnd++,i&&(u.providerIndexes+=1048576),n.push(A),a.push(A)):(n[N]=A,a[N]=A)}else{const A=IC(f,t,w+k,D),N=IC(f,t,w,w+k),H=N>=0&&n[N];if(i&&!H||!i&&!(A>=0&&n[A])){rf(wu(u,a),o,f);const V=function MU(e,t,n,r,i){const a=new Da(e,n,re,null);return a.multi=[],a.index=t,a.componentProviders=0,nk(a,i,r&&!n),a}(i?SU:xU,n.length,i,r,y);!i&&H&&(n[N].providerFactory=V),TC(o,e,t.length,0),t.push(f),u.directiveStart++,u.directiveEnd++,i&&(u.providerIndexes+=1048576),n.push(V),a.push(V)}else TC(o,e,A>-1?A:N,nk(n[i?N:A],y,!i&&r));!i&&r&&H&&n[N].componentProviders++}}}function TC(e,t,n,r){const i=Jn(t),o=function fg(e){return!!e.useClass}(t);if(i||o){const f=(o?je(t.useClass):t).prototype.ngOnDestroy;if(f){const y=e.destroyHooks||(e.destroyHooks=[]);if(!i&&t.multi){const w=y.indexOf(n);-1===w?y.push(n,[r,f]):y[w+1].push(r,f)}else y.push(n,f)}}}function nk(e,t,n){return n&&e.componentProviders++,e.multi.push(t)-1}function IC(e,t,n,r){for(let i=n;i{n.providersResolver=(r,i)=>function DU(e,t,n){const r=Ve();if(r.firstCreatePass){const i=_r(e);MC(n,r.data,r.blueprint,i,!0),MC(t,r.data,r.blueprint,i,!1)}}(r,i?i(e):e,t)}}function qf(e,t){const n=e[t];return n===Mt?void 0:n}function zl(e,t){const n=Ve();let r;const i=e+27;n.firstCreatePass?(r=function jU(e,t){if(t)for(let n=t.length-1;n>=0;n--){const r=t[n];if(e===r.name)return r}}(t,n.pipeRegistry),n.data[i]=r,r.onDestroy&&(n.destroyHooks??=[]).push(i,r.onDestroy)):r=n.data[i];const o=r.factory||(r.factory=Oo(r.type)),u=Un(re);try{const f=Ma(!1),y=o();return Ma(f),function lh(e,t,n,r){n>=e.data.length&&(e.data[n]=null,e.blueprint[n]=null),t[n]=r}(n,K(),i,y),y}finally{Un(u)}}function Wy(e,t,n){const r=e+27,i=K(),o=Lr(i,r);return Yf(i,r)?function ik(e,t,n,r,i,o){const a=t+n;return qt(e,a,i)?Go(e,a+1,o?r.call(o,i):r(i)):qf(e,a+1)}(i,pn(),t,o.transform,n,o):o.transform(n)}function Gy(e,t,n,r){const i=e+27,o=K(),a=Lr(o,i);return Yf(o,i)?function ok(e,t,n,r,i,o,a){const u=t+n;return Rl(e,u,i,o)?Go(e,u+2,a?r.call(a,i,o):r(i,o)):qf(e,u+2)}(o,pn(),t,a.transform,n,r,a):a.transform(n,r)}function Yf(e,t){return e[1].data[t].pure}class b4{ngModuleFactory;componentFactories;constructor(t,n){this.ngModuleFactory=t,this.componentFactories=n}}let _4=(()=>{class e{compileModuleSync(n){return new NT(n)}compileModuleAsync(n){return Promise.resolve(this.compileModuleSync(n))}compileModuleAndAllComponentsSync(n){const r=this.compileModuleSync(n),o=Wo(Nr(n).declarations).reduce((a,u)=>{const f=ct(u);return f&&a.push(new Aw(f)),a},[]);return new b4(r,o)}compileModuleAndAllComponentsAsync(n){return Promise.resolve(this.compileModuleAndAllComponentsSync(n))}clearCache(){}clearCacheFor(n){}getModuleId(n){}static \u0275fac=function(r){return new(r||e)};static \u0275prov=pe({token:e,factory:e.\u0275fac,providedIn:"root"})}return e})(),C4=(()=>{class e{zone=Y(lt);changeDetectionScheduler=Y(ao);applicationRef=Y(Yo);applicationErrorHandler=Y(ri);_onMicrotaskEmptySubscription;initialize(){this._onMicrotaskEmptySubscription||(this._onMicrotaskEmptySubscription=this.zone.onMicrotaskEmpty.subscribe({next:()=>{this.changeDetectionScheduler.runningTick||this.zone.run(()=>{try{this.applicationRef.dirtyFlags|=1,this.applicationRef._tick()}catch(n){this.applicationErrorHandler(n)}})}}))}ngOnDestroy(){this._onMicrotaskEmptySubscription?.unsubscribe()}static \u0275fac=function(r){return new(r||e)};static \u0275prov=pe({token:e,factory:e.\u0275fac,providedIn:"root"})}return e})();const E4=new ee("",{factory:()=>!1});function PC({ngZoneFactory:e,ignoreChangesOutsideZone:t,scheduleInRootZone:n}){return e??=()=>new lt({...LC(),scheduleInRootZone:n}),[{provide:lt,useFactory:e},{provide:hr,multi:!0,useFactory:()=>{const r=Y(C4,{optional:!0});return()=>r.initialize()}},{provide:hr,multi:!0,useFactory:()=>{const r=Y(x4);return()=>{r.initialize()}}},!0===t?{provide:Bg,useValue:!0}:[],{provide:Vg,useValue:n??XS},{provide:ri,useFactory:()=>{const r=Y(lt),i=Y(Dn);let o;return a=>{r.runOutsideAngular(()=>{i.destroyed&&!o?setTimeout(()=>{throw a}):(o??=i.get(dt),o.handleError(a))})}}}]}function LC(e){return{enableLongStackTrace:!1,shouldCoalesceEventChangeDetection:e?.eventCoalescing??!1,shouldCoalesceRunChangeDetection:e?.runCoalescing??!1}}let x4=(()=>{class e{subscription=new Hn;initialized=!1;zone=Y(lt);pendingTasks=Y(qn);initialize(){if(this.initialized)return;this.initialized=!0;let n=null;!this.zone.isStable&&!this.zone.hasPendingMacrotasks&&!this.zone.hasPendingMicrotasks&&(n=this.pendingTasks.add()),this.zone.runOutsideAngular(()=>{this.subscription.add(this.zone.onStable.subscribe(()=>{lt.assertNotInAngularZone(),queueMicrotask(()=>{null!==n&&!this.zone.hasPendingMacrotasks&&!this.zone.hasPendingMicrotasks&&(this.pendingTasks.remove(n),n=null)})}))}),this.subscription.add(this.zone.onUnstable.subscribe(()=>{lt.assertInAngularZone(),n??=this.pendingTasks.add()}))}ngOnDestroy(){this.subscription.unsubscribe()}static \u0275fac=function(r){return new(r||e)};static \u0275prov=pe({token:e,factory:e.\u0275fac,providedIn:"root"})}return e})(),BC=(()=>{class e{applicationErrorHandler=Y(ri);appRef=Y(Yo);taskService=Y(qn);ngZone=Y(lt);zonelessEnabled=Y(Dh);tracing=Y(Ou,{optional:!0});disableScheduling=Y(Bg,{optional:!0})??!1;zoneIsDefined=typeof Zone<"u"&&!!Zone.root.run;schedulerTickApplyArgs=[{data:{__scheduler_tick__:!0}}];subscriptions=new Hn;angularZoneId=this.zoneIsDefined?this.ngZone._inner?.get(qm):null;scheduleInRootZone=!this.zonelessEnabled&&this.zoneIsDefined&&(Y(Vg,{optional:!0})??!1);cancelScheduledCallback=null;useMicrotaskScheduler=!1;runningTick=!1;pendingRenderTaskId=null;constructor(){this.subscriptions.add(this.appRef.afterTick.subscribe(()=>{this.runningTick||this.cleanup()})),this.subscriptions.add(this.ngZone.onUnstable.subscribe(()=>{this.runningTick||this.cleanup()})),this.disableScheduling||=!this.zonelessEnabled&&(this.ngZone instanceof K_||!this.zoneIsDefined)}notify(n){if(!this.zonelessEnabled&&5===n)return;let r=!1;switch(n){case 0:this.appRef.dirtyFlags|=2;break;case 3:case 2:case 4:case 5:case 1:this.appRef.dirtyFlags|=4;break;case 6:case 13:this.appRef.dirtyFlags|=2,r=!0;break;case 12:this.appRef.dirtyFlags|=16,r=!0;break;case 11:r=!0;break;default:this.appRef.dirtyFlags|=8}if(this.appRef.tracingSnapshot=this.tracing?.snapshot(this.appRef.tracingSnapshot)??null,!this.shouldScheduleTick(r))return;const i=this.useMicrotaskScheduler?KS:ZS;this.pendingRenderTaskId=this.taskService.add(),this.cancelScheduledCallback=this.scheduleInRootZone?Zone.root.run(()=>i(()=>this.tick())):this.ngZone.runOutsideAngular(()=>i(()=>this.tick()))}shouldScheduleTick(n){return!(this.disableScheduling&&!n||this.appRef.destroyed||null!==this.pendingRenderTaskId||this.runningTick||this.appRef._runningTick||!this.zonelessEnabled&&this.zoneIsDefined&&Zone.current.get(qm+this.angularZoneId))}tick(){if(this.runningTick||this.appRef.destroyed)return;if(0===this.appRef.dirtyFlags)return void this.cleanup();!this.zonelessEnabled&&7&this.appRef.dirtyFlags&&(this.appRef.dirtyFlags|=1);const n=this.taskService.add();try{this.ngZone.run(()=>{this.runningTick=!0,this.appRef._tick()},void 0,this.schedulerTickApplyArgs)}catch(r){this.taskService.remove(n),this.applicationErrorHandler(r)}finally{this.cleanup()}this.useMicrotaskScheduler=!0,KS(()=>{this.useMicrotaskScheduler=!1,this.taskService.remove(n)})}ngOnDestroy(){this.subscriptions.unsubscribe(),this.cleanup()}cleanup(){if(this.runningTick=!1,this.cancelScheduledCallback?.(),this.cancelScheduledCallback=null,null!==this.pendingRenderTaskId){const n=this.pendingRenderTaskId;this.pendingRenderTaskId=null,this.taskService.remove(n)}}static \u0275fac=function(r){return new(r||e)};static \u0275prov=pe({token:e,factory:e.\u0275fac,providedIn:"root"})}return e})();const Va=new ee("",{providedIn:"root",factory:()=>Y(Va,{optional:!0,skipSelf:!0})||function S4(){return typeof $localize<"u"&&$localize.locale||By}()}),L4={...Eo,consumerIsAlwaysLive:!0,consumerAllowSignalWrites:!0,dirty:!0,kind:"effect"};class Ok{destroyed=!1;listeners=null;errorHandler=Y(dt,{optional:!0});destroyRef=Y(Cr);constructor(){this.destroyRef.onDestroy(()=>{this.destroyed=!0,this.listeners=null})}subscribe(t){if(this.destroyed)throw new G(953,!1);return(this.listeners??=[]).push(t),{unsubscribe:()=>{const n=this.listeners?.indexOf(t);void 0!==n&&-1!==n&&this.listeners?.splice(n,1)}}}emit(t){if(this.destroyed)return void console.warn(ur(953,!1));if(null===this.listeners)return;const n=ye(null);try{for(const r of this.listeners)try{r(t)}catch(i){this.errorHandler?.handleError(i)}}finally{ye(n)}}}function Vt(e){return function P4(e){const t=ye(null);try{return e()}finally{ye(t)}}(e)}function It(e,t){return function Ws(e,t){const n=Object.create($p);n.computation=e,void 0!==t&&(n.equal=t);const r=()=>{if(ss(n),Do(n),n.value===zi)throw n.error;return n.value};return r[rt]=n,r}(e,t?.equal)}class B4{[rt];constructor(t){this[rt]=t}destroy(){this[rt].destroy()}}function HC(e,t){const n=t?.injector??Y(Gn);let i,r=!0!==t?.manualCleanup?n.get(Cr):null;const o=n.get(tu,null,{optional:!0}),a=n.get(ao);return null!==o?(i=function j4(e,t,n){const r=Object.create($4);return r.view=e,r.zone=typeof Zone<"u"?Zone.current:null,r.notifier=t,r.fn=Nk(r,n),e[23]??=new Set,e[23].add(r),r.consumerMarkedDirty(r),r}(o.view,a,e),r instanceof fa&&r._lView===o.view&&(r=null)):i=function H4(e,t,n){const r=Object.create(V4);return r.fn=Nk(r,e),r.scheduler=t,r.notifier=n,r.zone=typeof Zone<"u"?Zone.current:null,r.scheduler.add(r),r.notifier.notify(12),r}(e,n.get(Br),a),i.injector=n,null!==r&&(i.onDestroyFn=r.onDestroy(()=>i.destroy())),new B4(i)}const Rk={...L4,cleanupFns:void 0,zone:null,onDestroyFn:oi,run(){const e=Fo(!1);try{!function F4(e){if(e.dirty=!1,e.version>0&&!xo(e))return;e.version++;const t=Hi(e);try{e.cleanup(),e.fn()}finally{Ui(e,t)}}(this)}finally{Fo(e)}},cleanup(){if(!this.cleanupFns?.length)return;const e=ye(null);try{for(;this.cleanupFns.length;)this.cleanupFns.pop()()}finally{this.cleanupFns=[],ye(e)}}},V4={...Rk,consumerMarkedDirty(){this.scheduler.schedule(this),this.notifier.notify(12)},destroy(){So(this),this.onDestroyFn(),this.cleanup(),this.scheduler.remove(this)}},$4={...Rk,consumerMarkedDirty(){this.view[2]|=8192,ro(this.view),this.notifier.notify(13)},destroy(){So(this),this.onDestroyFn(),this.cleanup(),this.view[23]?.delete(this)}};function Nk(e,t){return()=>{t(n=>(e.cleanupFns??=[]).push(n))}}Error,Error;const Ky=Symbol("InputSignalNode#UNSET"),$k={...vc,transformFn:void 0,applyValueToInputSignal(e,t){Gs(e,t)}};function jk(e,t){const n=Object.create($k);function r(){if(Do(n),n.value===Ky)throw new G(-950,null);return n.value}return n.value=e,n.transformFn=t?.transform,r[rt]=n,r}function Hk(e,t){return jk(e,t)}new ee("").__NG_ELEMENT_ID__=e=>{const t=Ae();if(null===t)throw new G(204,!1);if(2&t.type)return t.value;if(8&e)return null;throw new G(204,!1)};const Qy=(Hk.required=function nz(e){return jk(Ky,e)},Hk);function Uk(e,t){const n=Object.create($k),r=new Ok;function i(){return Do(n),zk(n.value),n.value}return n.value=e,i[rt]=n,i.asReadonly=eu.bind(i),i.set=o=>{n.equal(n.value,o)||(Gs(n,o),r.emit(o))},i.update=o=>{zk(n.value),i.set(o(n.value))},i.subscribe=r.subscribe.bind(r),i.destroyRef=r.destroyRef,i}function zk(e){if(e===Ky)throw new G(952,!1)}function Wk(e,t){return Uk(e)}const iz=(Wk.required=function rz(e){return Uk(Ky)},Wk),Jy=new ee(""),cz=new ee("");function Zf(e){return!e.moduleRef}let Xk,td=null;let Kf=(()=>class e{static __NG_ELEMENT_ID__=gz})();function gz(e){return function mz(e,t,n){if(eo(e)&&!n){const r=Fn(e.index,t);return new bf(r,r)}return 175&e.type?new bf(t[15],t):null}(Ae(),K(),!(16&~e))}function xz(e){const{rootComponent:t,appProviders:n,platformProviders:r,platformRef:i}=e;ht(8);try{const o=i?.injector??function pz(e=[]){if(td)return td;const t=function Qk(e=[],t){return Gn.create({name:t,providers:[{provide:eh,useValue:"platform"},{provide:Jy,useValue:new Set([()=>td=null])},...e]})}(e);return td=t,function Oj(){!function jp(e){To=e}(()=>{throw new G(600,"")})}(),function Jk(e){const t=e.get(B1,null);xn(e,()=>{t?.forEach(n=>n())})}(t),t}(r),a=[PC({}),{provide:ao,useExisting:BC},Ch,...n||[]];return function Yk(e){const t=Zf(e)?e.r3Injector:e.moduleRef.injector,n=t.get(lt);return n.run(()=>{Zf(e)?e.r3Injector.resolveInjectorInitializers():e.moduleRef.resolveInjectorInitializers();const r=t.get(ri);let i;if(n.runOutsideAngular(()=>{i=n.onError.subscribe({next:r})}),Zf(e)){const o=()=>t.destroy(),a=e.platformInjector.get(Jy);a.add(o),t.onDestroy(()=>{i.unsubscribe(),a.delete(o)})}else{const o=()=>e.moduleRef.destroy(),a=e.platformInjector.get(Jy);a.add(o),e.moduleRef.onDestroy(()=>{Sy(e.allPlatformModules,e.moduleRef),i.unsubscribe(),a.delete(o)})}return function dz(e,t,n){try{const r=n();return Ff(r)?r.catch(i=>{throw t.runOutsideAngular(()=>e(i)),i}):r}catch(r){throw t.runOutsideAngular(()=>e(r)),r}}(r,n,()=>{const o=t.get(qn),a=o.add(),u=t.get(SI);return u.runInitializers(),u.donePromise.then(()=>{if(function q5(e){"string"==typeof e&&(KI=e.toLowerCase().replace(/_/g,"-"))}(t.get(Va,By)||By),!t.get(cz,!0))return Zf(e)?t.get(Yo):(e.allPlatformModules.push(e.moduleRef),e.moduleRef);if(Zf(e)){const w=t.get(Yo);return void 0!==e.rootComponent&&w.bootstrap(e.rootComponent),w}return Xk?.(e.moduleRef,e.allPlatformModules),e.moduleRef}).finally(()=>{o.remove(a)})})})}({r3Injector:new PT({providers:a,parent:o,debugName:"",runEnvironmentInitializers:!1}).injector,platformInjector:o,rootComponent:t})}catch(o){return Promise.reject(o)}finally{ht(9)}}const GC=Symbol("NOT_SET"),_O=new Set,Xz={...vc,consumerIsAlwaysLive:!0,consumerAllowSignalWrites:!0,value:GC,cleanup:null,consumerMarkedDirty(){if(this.sequence.impl.executing){if(null===this.sequence.lastPhase||this.sequence.lastPhase(Do(y),y.value),y.signal[rt]=y,y.registerCleanupFn=w=>(y.cleanup??=new Set).add(w),this.nodes[u]=y,this.hooks[u]=w=>y.phaseFn(w)}}afterRun(){super.afterRun(),this.lastPhase=null}destroy(){super.destroy();for(const t of this.nodes)if(t)try{for(const n of t.cleanup??_O)n()}finally{So(t)}}}class wO{_doc;constructor(t){this._doc=t}manager}let qC=(()=>{class e extends wO{constructor(n){super(n)}supports(n){return!0}addEventListener(n,r,i,o){return n.addEventListener(r,i,o),()=>this.removeEventListener(n,r,i,o)}removeEventListener(n,r,i,o){return n.removeEventListener(r,i,o)}static \u0275fac=function(r){return new(r||e)(ke(ut))};static \u0275prov=pe({token:e,factory:e.\u0275fac})}return e})();const YC=new ee("");let CO=(()=>{class e{_zone;_plugins;_eventNameToPlugin=new Map;constructor(n,r){this._zone=r,n.forEach(a=>{a.manager=this});const i=n.filter(a=>!(a instanceof qC));this._plugins=i.slice().reverse();const o=n.find(a=>a instanceof qC);o&&this._plugins.push(o)}addEventListener(n,r,i,o){return this._findPluginFor(r).addEventListener(n,r,i,o)}getZone(){return this._zone}_findPluginFor(n){let r=this._eventNameToPlugin.get(n);if(r)return r;if(r=this._plugins.find(o=>o.supports(n)),!r)throw new G(5101,!1);return this._eventNameToPlugin.set(n,r),r}static \u0275fac=function(r){return new(r||e)(ke(YC),ke(lt))};static \u0275prov=pe({token:e,factory:e.\u0275fac})}return e})();const XC="ng-app-id";function EO(e){for(const t of e)t.remove()}function DO(e,t){const n=t.createElement("style");return n.textContent=e,n}function ZC(e,t){const n=t.createElement("link");return n.setAttribute("rel","stylesheet"),n.setAttribute("href",e),n}let xO=(()=>{class e{doc;appId;nonce;inline=new Map;external=new Map;hosts=new Set;constructor(n,r,i,o={}){this.doc=n,this.appId=r,this.nonce=i,function Jz(e,t,n,r){const i=e.head?.querySelectorAll(`style[${XC}="${t}"],link[${XC}="${t}"]`);if(i)for(const o of i)o.removeAttribute(XC),o instanceof HTMLLinkElement?r.set(o.href.slice(o.href.lastIndexOf("/")+1),{usage:0,elements:[o]}):o.textContent&&n.set(o.textContent,{usage:0,elements:[o]})}(n,r,this.inline,this.external),this.hosts.add(n.head)}addStyles(n,r){for(const i of n)this.addUsage(i,this.inline,DO);r?.forEach(i=>this.addUsage(i,this.external,ZC))}removeStyles(n,r){for(const i of n)this.removeUsage(i,this.inline);r?.forEach(i=>this.removeUsage(i,this.external))}addUsage(n,r,i){const o=r.get(n);o?o.usage++:r.set(n,{usage:1,elements:[...this.hosts].map(a=>this.addElement(a,i(n,this.doc)))})}removeUsage(n,r){const i=r.get(n);i&&(i.usage--,i.usage<=0&&(EO(i.elements),r.delete(n)))}ngOnDestroy(){for(const[,{elements:n}]of[...this.inline,...this.external])EO(n);this.hosts.clear()}addHost(n){this.hosts.add(n);for(const[r,{elements:i}]of this.inline)i.push(this.addElement(n,DO(r,this.doc)));for(const[r,{elements:i}]of this.external)i.push(this.addElement(n,ZC(r,this.doc)))}removeHost(n){this.hosts.delete(n)}addElement(n,r){return this.nonce&&r.setAttribute("nonce",this.nonce),n.appendChild(r)}static \u0275fac=function(r){return new(r||e)(ke(ut),ke(af),ke(V1,8),ke(Im))};static \u0275prov=pe({token:e,factory:e.\u0275fac})}return e})();const KC={svg:"http://www.w3.org/2000/svg",xhtml:"http://www.w3.org/1999/xhtml",xlink:"http://www.w3.org/1999/xlink",xml:"http://www.w3.org/XML/1998/namespace",xmlns:"http://www.w3.org/2000/xmlns/",math:"http://www.w3.org/1998/Math/MathML"},QC=/%COMP%/g,o8=new ee("",{providedIn:"root",factory:()=>!0});function MO(e,t){return t.map(n=>n.replace(QC,e))}let TO=(()=>{class e{eventManager;sharedStylesHost;appId;removeStylesOnCompDestroy;doc;platformId;ngZone;nonce;tracingService;rendererByCompId=new Map;defaultRenderer;platformIsServer;constructor(n,r,i,o,a,u,f,y=null,w=null){this.eventManager=n,this.sharedStylesHost=r,this.appId=i,this.removeStylesOnCompDestroy=o,this.doc=a,this.platformId=u,this.ngZone=f,this.nonce=y,this.tracingService=w,this.platformIsServer=!1,this.defaultRenderer=new JC(n,a,f,this.platformIsServer,this.tracingService)}createRenderer(n,r){if(!n||!r)return this.defaultRenderer;const i=this.getOrCreateRenderer(n,r);return i instanceof AO?i.applyToHost(n):i instanceof eE&&i.applyStyles(),i}getOrCreateRenderer(n,r){const i=this.rendererByCompId;let o=i.get(r.id);if(!o){const a=this.doc,u=this.ngZone,f=this.eventManager,y=this.sharedStylesHost,w=this.removeStylesOnCompDestroy,D=this.platformIsServer,k=this.tracingService;switch(r.encapsulation){case ks.Emulated:o=new AO(f,y,r,this.appId,w,a,u,D,k);break;case ks.ShadowDom:return new c8(f,y,n,r,a,u,this.nonce,D,k);default:o=new eE(f,y,r,w,a,u,D,k)}i.set(r.id,o)}return o}ngOnDestroy(){this.rendererByCompId.clear()}componentReplaced(n){this.rendererByCompId.delete(n)}static \u0275fac=function(r){return new(r||e)(ke(CO),ke(xO),ke(af),ke(o8),ke(ut),ke(Im),ke(lt),ke(V1),ke(Ou,8))};static \u0275prov=pe({token:e,factory:e.\u0275fac})}return e})();class JC{eventManager;doc;ngZone;platformIsServer;tracingService;data=Object.create(null);throwOnSyntheticProps=!0;constructor(t,n,r,i,o){this.eventManager=t,this.doc=n,this.ngZone=r,this.platformIsServer=i,this.tracingService=o}destroy(){}destroyNode=null;createElement(t,n){return n?this.doc.createElementNS(KC[n]||n,t):this.doc.createElement(t)}createComment(t){return this.doc.createComment(t)}createText(t){return this.doc.createTextNode(t)}appendChild(t,n){(IO(t)?t.content:t).appendChild(n)}insertBefore(t,n,r){t&&(IO(t)?t.content:t).insertBefore(n,r)}removeChild(t,n){n.remove()}selectRootElement(t,n){let r="string"==typeof t?this.doc.querySelector(t):t;if(!r)throw new G(-5104,!1);return n||(r.textContent=""),r}parentNode(t){return t.parentNode}nextSibling(t){return t.nextSibling}setAttribute(t,n,r,i){if(i){n=i+":"+n;const o=KC[i];o?t.setAttributeNS(o,n,r):t.setAttribute(n,r)}else t.setAttribute(n,r)}removeAttribute(t,n,r){if(r){const i=KC[r];i?t.removeAttributeNS(i,n):t.removeAttribute(`${r}:${n}`)}else t.removeAttribute(n)}addClass(t,n){t.classList.add(n)}removeClass(t,n){t.classList.remove(n)}setStyle(t,n,r,i){i&(ka.DashCase|ka.Important)?t.style.setProperty(n,r,i&ka.Important?"important":""):t.style[n]=r}removeStyle(t,n,r){r&ka.DashCase?t.style.removeProperty(n):t.style[n]=""}setProperty(t,n,r){null!=t&&(t[n]=r)}setValue(t,n){t.nodeValue=n}listen(t,n,r,i){if("string"==typeof t&&!(t=Bo().getGlobalEventTarget(this.doc,t)))throw new G(5102,!1);let o=this.decoratePreventDefault(r);return this.tracingService?.wrapEventListener&&(o=this.tracingService.wrapEventListener(t,n,o)),this.eventManager.addEventListener(t,n,o,i)}decoratePreventDefault(t){return n=>{if("__ngUnwrap__"===n)return t;!1===t(n)&&n.preventDefault()}}}function IO(e){return"TEMPLATE"===e.tagName&&void 0!==e.content}class c8 extends JC{sharedStylesHost;hostEl;shadowRoot;constructor(t,n,r,i,o,a,u,f,y){super(t,o,a,f,y),this.sharedStylesHost=n,this.hostEl=r,this.shadowRoot=r.attachShadow({mode:"open"}),this.sharedStylesHost.addHost(this.shadowRoot);let w=i.styles;w=MO(i.id,w);for(const k of w){const A=document.createElement("style");u&&A.setAttribute("nonce",u),A.textContent=k,this.shadowRoot.appendChild(A)}const D=i.getExternalStyles?.();if(D)for(const k of D){const A=ZC(k,o);u&&A.setAttribute("nonce",u),this.shadowRoot.appendChild(A)}}nodeOrShadowRoot(t){return t===this.hostEl?this.shadowRoot:t}appendChild(t,n){return super.appendChild(this.nodeOrShadowRoot(t),n)}insertBefore(t,n,r){return super.insertBefore(this.nodeOrShadowRoot(t),n,r)}removeChild(t,n){return super.removeChild(null,n)}parentNode(t){return this.nodeOrShadowRoot(super.parentNode(this.nodeOrShadowRoot(t)))}destroy(){this.sharedStylesHost.removeHost(this.shadowRoot)}}class eE extends JC{sharedStylesHost;removeStylesOnCompDestroy;styles;styleUrls;constructor(t,n,r,i,o,a,u,f,y){super(t,o,a,u,f),this.sharedStylesHost=n,this.removeStylesOnCompDestroy=i;let w=r.styles;this.styles=y?MO(y,w):w,this.styleUrls=r.getExternalStyles?.(y)}applyStyles(){this.sharedStylesHost.addStyles(this.styles,this.styleUrls)}destroy(){this.removeStylesOnCompDestroy&&0===ku.size&&this.sharedStylesHost.removeStyles(this.styles,this.styleUrls)}}class AO extends eE{contentAttr;hostAttr;constructor(t,n,r,i,o,a,u,f,y){const w=i+"-"+r.id;super(t,n,r,o,a,u,f,y,w),this.contentAttr=function s8(e){return"_ngcontent-%COMP%".replace(QC,e)}(w),this.hostAttr=function a8(e){return"_nghost-%COMP%".replace(QC,e)}(w)}applyToHost(t){this.applyStyles(),this.setAttribute(t,this.hostAttr,"")}createElement(t,n){const r=super.createElement(t,n);return super.setAttribute(r,this.contentAttr,""),r}}class tE extends xh{supportsDOMEvents=!0;static makeCurrent(){!function bb(e){$g??=e}(new tE)}onAndCancel(t,n,r,i){return t.addEventListener(n,r,i),()=>{t.removeEventListener(n,r,i)}}dispatchEvent(t,n){t.dispatchEvent(n)}remove(t){t.remove()}createElement(t,n){return(n=n||this.getDefaultDocument()).createElement(t)}createHtmlDocument(){return document.implementation.createHTMLDocument("fakeTitle")}getDefaultDocument(){return document}isElementNode(t){return t.nodeType===Node.ELEMENT_NODE}isShadowRoot(t){return t instanceof DocumentFragment}getGlobalEventTarget(t,n){return"window"===n?window:"document"===n?t:"body"===n?t.body:null}getBaseHref(t){const n=function u8(){return Jf=Jf||document.head.querySelector("base"),Jf?Jf.getAttribute("href"):null}();return null==n?null:function d8(e){return new URL(e,document.baseURI).pathname}(n)}resetBaseElement(){Jf=null}getUserAgent(){return window.navigator.userAgent}getCookie(t){return Th(document.cookie,t)}}let Jf=null,f8=(()=>{class e{build(){return new XMLHttpRequest}static \u0275fac=function(r){return new(r||e)};static \u0275prov=pe({token:e,factory:e.\u0275fac})}return e})();const kO=["alt","control","meta","shift"],p8={"\b":"Backspace","\t":"Tab","\x7f":"Delete","\x1b":"Escape",Del:"Delete",Esc:"Escape",Left:"ArrowLeft",Right:"ArrowRight",Up:"ArrowUp",Down:"ArrowDown",Menu:"ContextMenu",Scroll:"ScrollLock",Win:"OS"},g8={alt:e=>e.altKey,control:e=>e.ctrlKey,meta:e=>e.metaKey,shift:e=>e.shiftKey};const w8=[{provide:Im,useValue:"browser"},{provide:B1,useValue:function v8(){tE.makeCurrent()},multi:!0},{provide:ut,useFactory:function _8(){return function ML(e){h_=e}(document),document}}],E8=[{provide:eh,useValue:"root"},{provide:dt,useFactory:function b8(){return new dt}},{provide:YC,useClass:qC,multi:!0,deps:[ut]},{provide:YC,useClass:(()=>{class e extends wO{constructor(n){super(n)}supports(n){return null!=e.parseEventName(n)}addEventListener(n,r,i,o){const a=e.parseEventName(r),u=e.eventCallback(a.fullKey,i,this.manager.getZone());return this.manager.getZone().runOutsideAngular(()=>Bo().onAndCancel(n,a.domEventName,u,o))}static parseEventName(n){const r=n.toLowerCase().split("."),i=r.shift();if(0===r.length||"keydown"!==i&&"keyup"!==i)return null;const o=e._normalizeKey(r.pop());let a="",u=r.indexOf("code");if(u>-1&&(r.splice(u,1),a="code."),kO.forEach(y=>{const w=r.indexOf(y);w>-1&&(r.splice(w,1),a+=y+".")}),a+=o,0!=r.length||0===o.length)return null;const f={};return f.domEventName=i,f.fullKey=a,f}static matchEventFullKeyCode(n,r){let i=p8[n.key]||n.key,o="";return r.indexOf("code.")>-1&&(i=n.code,o="code."),!(null==i||!i)&&(i=i.toLowerCase()," "===i?i="space":"."===i&&(i="dot"),kO.forEach(a=>{a!==i&&(0,g8[a])(n)&&(o+=a+".")}),o+=i,o===r)}static eventCallback(n,r,i){return o=>{e.matchEventFullKeyCode(o,n)&&i.runGuarded(()=>r(o))}}static _normalizeKey(n){return"esc"===n?"escape":n}static \u0275fac=function(r){return new(r||e)(ke(ut))};static \u0275prov=pe({token:e,factory:e.\u0275fac})}return e})(),multi:!0,deps:[ut]},TO,xO,CO,{provide:Dw,useExisting:TO},{provide:au,useClass:f8},[]],nE={now:()=>(nE.delegate||Date).now(),delegate:void 0};class D8 extends Kn{constructor(t=1/0,n=1/0,r=nE){super(),this._bufferSize=t,this._windowTime=n,this._timestampProvider=r,this._buffer=[],this._infiniteTimeWindow=!0,this._infiniteTimeWindow=n===1/0,this._bufferSize=Math.max(1,t),this._windowTime=Math.max(1,n)}next(t){const{isStopped:n,_buffer:r,_infiniteTimeWindow:i,_timestampProvider:o,_windowTime:a}=this;n||(r.push(t),!i&&r.push(o.now()+a)),this._trimBuffer(),super.next(t)}_subscribe(t){this._throwIfClosed(),this._trimBuffer();const n=this._innerSubscribe(t),{_infiniteTimeWindow:r,_buffer:i}=this,o=i.slice();for(let a=0;a=e.length&&(e=void 0),{value:e&&e[r++],done:!e}}};throw new TypeError(t?"Object is not iterable.":"Symbol.iterator is not defined.")}(e),n={},r("next"),r("throw"),r("return"),n[Symbol.asyncIterator]=function(){return this},n);function r(o){n[o]=e[o]&&function(a){return new Promise(function(u,f){!function i(o,a,u,f){Promise.resolve(f).then(function(y){o({value:y,done:u})},a)}(u,f,(a=e[o](a)).done,a.value)})}}}"function"==typeof SuppressedError&&SuppressedError;const FO=e=>e&&"number"==typeof e.length&&"function"!=typeof e;function BO(e){return ft(e?.then)}function VO(e){return ft(e[Ot])}function $O(e){return Symbol.asyncIterator&&ft(e?.[Symbol.asyncIterator])}function jO(e){return new TypeError(`You provided ${null!==e&&"object"==typeof e?"an invalid object":`'${e}'`} where a stream was expected. You can provide an Observable, Promise, ReadableStream, Array, AsyncIterable, or Iterable.`)}const HO=function K8(){return"function"==typeof Symbol&&Symbol.iterator?Symbol.iterator:"@@iterator"}();function UO(e){return ft(e?.[HO])}function zO(e){return function PO(e,t,n){if(!Symbol.asyncIterator)throw new TypeError("Symbol.asyncIterator is not defined.");var i,r=n.apply(e,t||[]),o=[];return i=Object.create(("function"==typeof AsyncIterator?AsyncIterator:Object).prototype),u("next"),u("throw"),u("return",function a(A){return function(N){return Promise.resolve(N).then(A,D)}}),i[Symbol.asyncIterator]=function(){return this},i;function u(A,N){r[A]&&(i[A]=function(L){return new Promise(function(H,V){o.push([A,L,H,V])>1||f(A,L)})},N&&(i[A]=N(i[A])))}function f(A,N){try{!function y(A){A.value instanceof $a?Promise.resolve(A.value.v).then(w,D):k(o[0][2],A)}(r[A](N))}catch(L){k(o[0][3],L)}}function w(A){f("next",A)}function D(A){f("throw",A)}function k(A,N){A(N),o.shift(),o.length&&f(o[0][0],o[0][1])}}(this,arguments,function*(){const n=e.getReader();try{for(;;){const{value:r,done:i}=yield $a(n.read());if(i)return yield $a(void 0);yield yield $a(r)}}finally{n.releaseLock()}})}function WO(e){return ft(e?.getReader)}function po(e){if(e instanceof Ut)return e;if(null!=e){if(VO(e))return function Q8(e){return new Ut(t=>{const n=e[Ot]();if(ft(n.subscribe))return n.subscribe(t);throw new TypeError("Provided object does not correctly implement Symbol.observable")})}(e);if(FO(e))return function J8(e){return new Ut(t=>{for(let n=0;n{e.then(n=>{t.closed||(t.next(n),t.complete())},n=>t.error(n)).then(null,Pd)})}(e);if($O(e))return GO(e);if(UO(e))return function t6(e){return new Ut(t=>{for(const n of e)if(t.next(n),t.closed)return;t.complete()})}(e);if(WO(e))return function n6(e){return GO(zO(e))}(e)}throw jO(e)}function GO(e){return new Ut(t=>{(function r6(e,t){var n,r,i,o;return function RO(e,t,n,r){return new(n||(n=Promise))(function(o,a){function u(w){try{y(r.next(w))}catch(D){a(D)}}function f(w){try{y(r.throw(w))}catch(D){a(D)}}function y(w){w.done?o(w.value):function i(o){return o instanceof n?o:new n(function(a){a(o)})}(w.value).then(u,f)}y((r=r.apply(e,t||[])).next())})}(this,void 0,void 0,function*(){try{for(n=LO(e);!(r=yield n.next()).done;)if(t.next(r.value),t.closed)return}catch(a){i={error:a}}finally{try{r&&!r.done&&(o=n.return)&&(yield o.call(n))}finally{if(i)throw i.error}}t.complete()})})(e,t).catch(n=>t.error(n))})}function Ls(e,t,n,r=0,i=!1){const o=t.schedule(function(){n(),i?e.add(this.schedule(null,r)):this.unsubscribe()},r);if(e.add(o),!i)return o}function xr(e,t,n=1/0){return ft(t)?xr((r,i)=>tt((o,a)=>t(r,o,i,a))(po(e(r,i))),n):("number"==typeof t&&(n=t),nn((r,i)=>function i6(e,t,n,r,i,o,a,u){const f=[];let y=0,w=0,D=!1;const k=()=>{D&&!f.length&&!y&&t.complete()},A=L=>y{o&&t.next(L),y++;let H=!1;po(n(L,w++)).subscribe(mn(t,V=>{i?.(V),o?A(V):t.next(V)},()=>{H=!0},void 0,()=>{if(H)try{for(y--;f.length&&yN(V)):N(V)}k()}catch(V){t.error(V)}}))};return e.subscribe(mn(t,A,()=>{D=!0,k()})),()=>{u?.()}}(r,i,e,n)))}function sv(e=1/0){return xr(Io,e)}const Li=new Ut(e=>e.complete());function qO(e){return e&&ft(e.schedule)}function lE(e){return e[e.length-1]}function YO(e){return ft(lE(e))?e.pop():void 0}function ep(e){return qO(lE(e))?e.pop():void 0}function XO(e,t=0){return nn((n,r)=>{n.subscribe(mn(r,i=>Ls(r,e,()=>r.next(i),t),()=>Ls(r,e,()=>r.complete(),t),i=>Ls(r,e,()=>r.error(i),t)))})}function ZO(e,t=0){return nn((n,r)=>{r.add(e.schedule(()=>n.subscribe(r),t))})}function KO(e,t){if(!e)throw new Error("Iterable cannot be null");return new Ut(n=>{Ls(n,t,()=>{const r=e[Symbol.asyncIterator]();Ls(n,t,()=>{r.next().then(i=>{i.done?n.complete():n.next(i.value)})},0,!0)})})}function lr(e,t){return t?function h6(e,t){if(null!=e){if(VO(e))return function a6(e,t){return po(e).pipe(ZO(t),XO(t))}(e,t);if(FO(e))return function c6(e,t){return new Ut(n=>{let r=0;return t.schedule(function(){r===e.length?n.complete():(n.next(e[r++]),n.closed||this.schedule())})})}(e,t);if(BO(e))return function l6(e,t){return po(e).pipe(ZO(t),XO(t))}(e,t);if($O(e))return KO(e,t);if(UO(e))return function u6(e,t){return new Ut(n=>{let r;return Ls(n,t,()=>{r=e[HO](),Ls(n,t,()=>{let i,o;try{({value:i,done:o}=r.next())}catch(a){return void n.error(a)}o?n.complete():n.next(i)},0,!0)}),()=>ft(r?.return)&&r.return()})}(e,t);if(WO(e))return function d6(e,t){return KO(zO(e),t)}(e,t)}throw jO(e)}(e,t):po(e)}function Sr(e,t){return nn((n,r)=>{let i=null,o=0,a=!1;const u=()=>a&&!i&&r.complete();n.subscribe(mn(r,f=>{i?.unsubscribe();let y=0;const w=o++;po(e(f,w)).subscribe(i=mn(r,D=>r.next(t?t(f,D,w,y++):D),()=>{i=null,u()}))},()=>{a=!0,u()}))})}const p6={schedule(e,t){const n=setTimeout(e,t);return()=>clearTimeout(n)}};let cE;function w6(e,t,n){let r=n;return function m6(e){return!!e&&e.nodeType===Node.ELEMENT_NODE}(e)&&t.some((i,o)=>!("*"===i||!function y6(e,t){if(!cE){const n=Element.prototype;cE=n.matches||n.matchesSelector||n.mozMatchesSelector||n.msMatchesSelector||n.oMatchesSelector||n.webkitMatchesSelector}return e.nodeType===Node.ELEMENT_NODE&&cE.call(e,t)}(e,i)||(r=o,0))),r}class E6{componentFactory;inputMap=new Map;constructor(t,n){this.componentFactory=n.get(Vu).resolveComponentFactory(t);for(const r of this.componentFactory.inputs)this.inputMap.set(r.propName,r.templateName)}create(t){return new D6(this.componentFactory,t,this.inputMap)}}class D6{componentFactory;injector;inputMap;eventEmitters=new D8(1);events=this.eventEmitters.pipe(Sr(t=>function f6(...e){const t=ep(e),n=function s6(e,t){return"number"==typeof lE(e)?e.pop():t}(e,1/0),r=e;return r.length?1===r.length?po(r[0]):sv(n)(lr(r,t)):Li}(...t)));componentRef=null;scheduledDestroyFn=null;initialInputValues=new Map;ngZone;elementZone;appRef;cdScheduler;constructor(t,n,r){this.componentFactory=t,this.injector=n,this.inputMap=r,this.ngZone=this.injector.get(lt),this.appRef=this.injector.get(Yo),this.cdScheduler=n.get(ao),this.elementZone=typeof Zone>"u"?null:this.ngZone.run(()=>Zone.current)}connect(t){this.runInZone(()=>{if(null!==this.scheduledDestroyFn)return this.scheduledDestroyFn(),void(this.scheduledDestroyFn=null);null===this.componentRef&&this.initializeComponent(t)})}disconnect(){this.runInZone(()=>{null===this.componentRef||null!==this.scheduledDestroyFn||(this.scheduledDestroyFn=p6.schedule(()=>{null!==this.componentRef&&(this.componentRef.destroy(),this.componentRef=null)},10))})}getInputValue(t){return this.runInZone(()=>null===this.componentRef?this.initialInputValues.get(t):this.componentRef.instance[t])}setInputValue(t,n){null!==this.componentRef?this.runInZone(()=>{this.componentRef.setInput(this.inputMap.get(t)??t,n),function lB(e){return ml(e._lView)||!!(64&e._lView[2])}(this.componentRef.hostView)&&(function cB(e){ch(e._lView)}(this.componentRef.changeDetectorRef),this.cdScheduler.notify(6))}):this.initialInputValues.set(t,n)}initializeComponent(t){const n=Gn.create({providers:[],parent:this.injector}),r=function _6(e,t){const n=e.childNodes,r=t.map(()=>[]);let i=-1;t.some((o,a)=>"*"===o&&(i=a,!0));for(let o=0,a=n.length;o{const o=t.instance[r];return new Ut(a=>{const u=o.subscribe(f=>a.next({name:i,value:f}));return()=>u.unsubscribe()})});this.eventEmitters.next(n)}runInZone(t){return this.elementZone&&Zone.current!==this.elementZone?this.ngZone.run(t):t()}}class x6 extends HTMLElement{ngElementEventsSubscription=null}function Gl(e,t){const n=function b6(e,t){return t.get(Vu).resolveComponentFactory(e).inputs}(e,t.injector),r=t.strategyFactory||new E6(e,t.injector),i=function v6(e){const t={};return e.forEach(({propName:n,templateName:r,transform:i})=>{t[function g6(e){return e.replace(/[A-Z]/g,t=>`-${t.toLowerCase()}`)}(r)]=[n,i]}),t}(n);class o extends x6{injector;static observedAttributes=Object.keys(i);get ngElementStrategy(){if(!this._ngElementStrategy){const u=this._ngElementStrategy=r.create(this.injector||t.injector);n.forEach(({propName:f,transform:y})=>{if(!this.hasOwnProperty(f))return;const w=this[f];delete this[f],u.setInputValue(f,w,y)})}return this._ngElementStrategy}_ngElementStrategy;constructor(u){super(),this.injector=u}attributeChangedCallback(u,f,y,w){const[D,k]=i[u];this.ngElementStrategy.setInputValue(D,y,k)}connectedCallback(){let u=!1;this.ngElementStrategy.events&&(this.subscribeToEvents(),u=!0),this.ngElementStrategy.connect(this),u||this.subscribeToEvents()}disconnectedCallback(){this._ngElementStrategy&&this._ngElementStrategy.disconnect(),this.ngElementEventsSubscription&&(this.ngElementEventsSubscription.unsubscribe(),this.ngElementEventsSubscription=null)}subscribeToEvents(){this.ngElementEventsSubscription=this.ngElementStrategy.events.subscribe(u=>{const f=new CustomEvent(u.name,{detail:u.value});this.dispatchEvent(f)})}}return n.forEach(({propName:a,transform:u})=>{Object.defineProperty(o.prototype,a,{get(){return this.ngElementStrategy.getInputValue(a)},set(f){this.ngElementStrategy.setInputValue(a,f,u)},configurable:!0,enumerable:!0})}),o}var av=function(e){return e[e.Decimal=0]="Decimal",e[e.Percent=1]="Percent",e[e.Currency=2]="Currency",e[e.Scientific=3]="Scientific",e}(av||{});function Fi(e,t){const n=Dr(e),r=n[Yt.NumberSymbols][t];if(typeof r>"u"){if(12===t)return n[Yt.NumberSymbols][0];if(13===t)return n[Yt.NumberSymbols][1]}return r}const Q6=/^(\d+)?\.((\d+)(-(\d+))?)?$/;function yE(e){const t=parseInt(e);if(isNaN(t))throw new G(2305,!1);return t}let hR=(()=>{class e{_locale;constructor(n){this._locale=n}transform(n,r,i){if(!function CE(e){return!(null==e||""===e||e!=e)}(n))return null;i||=this._locale;try{return function i9(e,t,n){return function gE(e,t,n,r,i,o,a=!1){let u="",f=!1;if(isFinite(e)){let y=function s9(e){let r,i,o,a,u,t=Math.abs(e)+"",n=0;for((i=t.indexOf("."))>-1&&(t=t.replace(".","")),(o=t.search(/e/i))>0?(i<0&&(i=o),i+=+t.slice(o+1),t=t.substring(0,o)):i<0&&(i=t.length),o=0;"0"===t.charAt(o);o++);if(o===(u=t.length))r=[0],i=1;else{for(u--;"0"===t.charAt(u);)u--;for(i-=o,r=[],a=0;o<=u;o++,a++)r[a]=Number(t.charAt(o))}return i>22&&(r=r.splice(0,21),n=i-1,i=1),{digits:r,exponent:n,integerLen:i}}(e);a&&(y=function o9(e){if(0===e.digits[0])return e;const t=e.digits.length-e.integerLen;return e.exponent?e.exponent+=2:(0===t?e.digits.push(0,0):1===t&&e.digits.push(0),e.integerLen+=2),e}(y));let w=t.minInt,D=t.minFrac,k=t.maxFrac;if(o){const z=o.match(Q6);if(null===z)throw new G(2306,!1);const ie=z[1],ae=z[3],de=z[5];null!=ie&&(w=yE(ie)),null!=ae&&(D=yE(ae)),null!=de?k=yE(de):null!=ae&&D>k&&(k=D)}!function a9(e,t,n){if(t>n)throw new G(2307,!1);let r=e.digits,i=r.length-e.integerLen;const o=Math.min(Math.max(t,i),n);let a=o+e.integerLen,u=r[a];if(a>0){r.splice(Math.max(e.integerLen,a));for(let D=a;D=5)if(a-1<0){for(let D=0;D>a;D--)r.unshift(0),e.integerLen++;r.unshift(1),e.integerLen++}else r[a-1]++;for(;i=y?N.pop():f=!1),k>=10?1:0},0);w&&(r.unshift(w),e.integerLen++)}(y,D,k);let A=y.digits,N=y.integerLen;const L=y.exponent;let H=[];for(f=A.every(z=>!z);N0?H=A.splice(N,A.length):(H=A,A=[0]);const V=[];for(A.length>=t.lgSize&&V.unshift(A.splice(-t.lgSize,A.length).join(""));A.length>t.gSize;)V.unshift(A.splice(-t.gSize,A.length).join(""));A.length&&V.unshift(A.join("")),u=V.join(Fi(n,r)),H.length&&(u+=Fi(n,i)+H.join("")),L&&(u+=Fi(n,6)+"+"+L)}else u=Fi(n,9);return u=e<0&&!f?t.negPre+u+t.negSuf:t.posPre+u+t.posSuf,u}(e,function mE(e,t="-"){const n={minInt:1,minFrac:0,maxFrac:0,posPre:"",posSuf:"",negPre:"",negSuf:"",gSize:0,lgSize:0},r=e.split(";"),i=r[0],o=r[1],a=-1!==i.indexOf(".")?i.split("."):[i.substring(0,i.lastIndexOf("0")+1),i.substring(i.lastIndexOf("0")+1)],u=a[0],f=a[1]||"";n.posPre=u.substring(0,u.indexOf("#"));for(let w=0;w{class e{static \u0275fac=function(r){return new(r||e)};static \u0275mod=Of({type:e});static \u0275inj=Wi({})}return e})();const{isArray:B9}=Array,{getPrototypeOf:V9,prototype:$9,keys:j9}=Object;function fR(e){if(1===e.length){const t=e[0];if(B9(t))return{args:t,keys:null};if(function H9(e){return e&&"object"==typeof e&&V9(e)===$9}(t)){const n=j9(t);return{args:n.map(r=>t[r]),keys:n}}}return{args:e,keys:null}}const{isArray:U9}=Array;function pR(e){return tt(t=>function z9(e,t){return U9(t)?e(...t):e(t)}(e,t))}function gR(e,t){return e.reduce((n,r,i)=>(n[r]=t[i],n),{})}function DE(...e){const t=YO(e),{args:n,keys:r}=fR(e),i=new Ut(o=>{const{length:a}=n;if(!a)return void o.complete();const u=new Array(a);let f=a,y=a;for(let w=0;w{D||(D=!0,y--),u[w]=k},()=>f--,void 0,()=>{(!f||!D)&&(y||o.next(r?gR(r,u):u),o.complete())}))}});return t?i.pipe(pR(t)):i}function xe(...e){return lr(e,ep(e))}class W9 extends Hn{constructor(t,n){super()}schedule(t,n=0){return this}}const vv={setInterval(e,t,...n){const{delegate:r}=vv;return r?.setInterval?r.setInterval(e,t,...n):setInterval(e,t,...n)},clearInterval(e){const{delegate:t}=vv;return(t?.clearInterval||clearInterval)(e)},delegate:void 0};class np{constructor(t,n=np.now){this.schedulerActionCtor=t,this.now=n}schedule(t,n=0,r){return new this.schedulerActionCtor(this,t).schedule(r,n)}}np.now=nE.now;const mR=new class q9 extends np{constructor(t,n=np.now){super(t,n),this.actions=[],this._active=!1}flush(t){const{actions:n}=this;if(this._active)return void n.push(t);let r;this._active=!0;do{if(r=t.execute(t.state,t.delay))break}while(t=n.shift());if(this._active=!1,r){for(;t=n.shift();)t.unsubscribe();throw r}}}(class G9 extends W9{constructor(t,n){super(t,n),this.scheduler=t,this.work=n,this.pending=!1}schedule(t,n=0){var r;if(this.closed)return this;this.state=t;const i=this.id,o=this.scheduler;return null!=i&&(this.id=this.recycleAsyncId(o,i,n)),this.pending=!0,this.delay=n,this.id=null!==(r=this.id)&&void 0!==r?r:this.requestAsyncId(o,this.id,n),this}requestAsyncId(t,n,r=0){return vv.setInterval(t.flush.bind(t,this),r)}recycleAsyncId(t,n,r=0){if(null!=r&&this.delay===r&&!1===this.pending)return n;null!=n&&vv.clearInterval(n)}execute(t,n){if(this.closed)return new Error("executing a cancelled action");this.pending=!1;const r=this._execute(t,n);if(r)return r;!1===this.pending&&null!=this.id&&(this.id=this.recycleAsyncId(this.scheduler,this.id,null))}_execute(t,n){let i,r=!1;try{this.work(t)}catch(o){r=!0,i=o||new Error("Scheduled action threw falsy error")}if(r)return this.unsubscribe(),i}unsubscribe(){if(!this.closed){const{id:t,scheduler:n}=this,{actions:r}=n;this.work=this.state=this.scheduler=null,this.pending=!1,Kr(r,this),null!=t&&(this.id=this.recycleAsyncId(n,t,null)),this.delay=null,super.unsubscribe()}}}),Y9=mR;function Tr(e){return nn((t,n)=>{let o,r=null,i=!1;r=t.subscribe(mn(n,void 0,void 0,a=>{o=po(e(a,Tr(e)(t))),r?(r.unsubscribe(),r=null,o.subscribe(n)):i=!0})),i&&(r.unsubscribe(),r=null,o.subscribe(n))})}function vn(e,t,n){const r=ft(e)||t||n?{next:e,error:t,complete:n}:e;return r?nn((i,o)=>{var a;null===(a=r.subscribe)||void 0===a||a.call(r);let u=!0;i.subscribe(mn(o,f=>{var y;null===(y=r.next)||void 0===y||y.call(r,f),o.next(f)},()=>{var f;u=!1,null===(f=r.complete)||void 0===f||f.call(r),o.complete()},f=>{var y;u=!1,null===(y=r.error)||void 0===y||y.call(r,f),o.error(f)},()=>{var f,y;u&&(null===(f=r.unsubscribe)||void 0===f||f.call(r)),null===(y=r.finalize)||void 0===y||y.call(r)}))}):Io}function rp(e,t){return ft(t)?xr(e,t,1):xr(e,1)}function Bs(e,t){return nn((n,r)=>{let i=0;n.subscribe(mn(r,o=>e.call(t,o,i++)&&r.next(o)))})}function bv(e){return nn((t,n)=>{try{t.subscribe(n)}finally{n.add(e)}})}class _v{}class wv{}class yo{headers;normalizedNames=new Map;lazyInit;lazyUpdate=null;constructor(t){t?"string"==typeof t?this.lazyInit=()=>{this.headers=new Map,t.split("\n").forEach(n=>{const r=n.indexOf(":");if(r>0){const i=n.slice(0,r),o=n.slice(r+1).trim();this.addHeaderEntry(i,o)}})}:typeof Headers<"u"&&t instanceof Headers?(this.headers=new Map,t.forEach((n,r)=>{this.addHeaderEntry(r,n)})):this.lazyInit=()=>{this.headers=new Map,Object.entries(t).forEach(([n,r])=>{this.setHeaderEntries(n,r)})}:this.headers=new Map}has(t){return this.init(),this.headers.has(t.toLowerCase())}get(t){this.init();const n=this.headers.get(t.toLowerCase());return n&&n.length>0?n[0]:null}keys(){return this.init(),Array.from(this.normalizedNames.values())}getAll(t){return this.init(),this.headers.get(t.toLowerCase())||null}append(t,n){return this.clone({name:t,value:n,op:"a"})}set(t,n){return this.clone({name:t,value:n,op:"s"})}delete(t,n){return this.clone({name:t,value:n,op:"d"})}maybeSetNormalizedName(t,n){this.normalizedNames.has(n)||this.normalizedNames.set(n,t)}init(){this.lazyInit&&(this.lazyInit instanceof yo?this.copyFrom(this.lazyInit):this.lazyInit(),this.lazyInit=null,this.lazyUpdate&&(this.lazyUpdate.forEach(t=>this.applyUpdate(t)),this.lazyUpdate=null))}copyFrom(t){t.init(),Array.from(t.headers.keys()).forEach(n=>{this.headers.set(n,t.headers.get(n)),this.normalizedNames.set(n,t.normalizedNames.get(n))})}clone(t){const n=new yo;return n.lazyInit=this.lazyInit&&this.lazyInit instanceof yo?this.lazyInit:this,n.lazyUpdate=(this.lazyUpdate||[]).concat([t]),n}applyUpdate(t){const n=t.name.toLowerCase();switch(t.op){case"a":case"s":let r=t.value;if("string"==typeof r&&(r=[r]),0===r.length)return;this.maybeSetNormalizedName(t.name,n);const i=("a"===t.op?this.headers.get(n):void 0)||[];i.push(...r),this.headers.set(n,i);break;case"d":const o=t.value;if(o){let a=this.headers.get(n);if(!a)return;a=a.filter(u=>-1===o.indexOf(u)),0===a.length?(this.headers.delete(n),this.normalizedNames.delete(n)):this.headers.set(n,a)}else this.headers.delete(n),this.normalizedNames.delete(n)}}addHeaderEntry(t,n){const r=t.toLowerCase();this.maybeSetNormalizedName(t,r),this.headers.has(r)?this.headers.get(r).push(n):this.headers.set(r,[n])}setHeaderEntries(t,n){const r=(Array.isArray(n)?n:[n]).map(o=>o.toString()),i=t.toLowerCase();this.headers.set(i,r),this.maybeSetNormalizedName(t,i)}forEach(t){this.init(),Array.from(this.normalizedNames.keys()).forEach(n=>t(this.normalizedNames.get(n),this.headers.get(n)))}}class Q9{encodeKey(t){return yR(t)}encodeValue(t){return yR(t)}decodeKey(t){return decodeURIComponent(t)}decodeValue(t){return decodeURIComponent(t)}}const eW=/%(\d[a-f0-9])/gi,tW={40:"@","3A":":",24:"$","2C":",","3B":";","3D":"=","3F":"?","2F":"/"};function yR(e){return encodeURIComponent(e).replace(eW,(t,n)=>tW[n]??t)}function Cv(e){return`${e}`}class ja{map;encoder;updates=null;cloneFrom=null;constructor(t={}){if(this.encoder=t.encoder||new Q9,t.fromString){if(t.fromObject)throw new G(2805,!1);this.map=function J9(e,t){const n=new Map;return e.length>0&&e.replace(/^\?/,"").split("&").forEach(i=>{const o=i.indexOf("="),[a,u]=-1==o?[t.decodeKey(i),""]:[t.decodeKey(i.slice(0,o)),t.decodeValue(i.slice(o+1))],f=n.get(a)||[];f.push(u),n.set(a,f)}),n}(t.fromString,this.encoder)}else t.fromObject?(this.map=new Map,Object.keys(t.fromObject).forEach(n=>{const r=t.fromObject[n],i=Array.isArray(r)?r.map(Cv):[Cv(r)];this.map.set(n,i)})):this.map=null}has(t){return this.init(),this.map.has(t)}get(t){this.init();const n=this.map.get(t);return n?n[0]:null}getAll(t){return this.init(),this.map.get(t)||null}keys(){return this.init(),Array.from(this.map.keys())}append(t,n){return this.clone({param:t,value:n,op:"a"})}appendAll(t){const n=[];return Object.keys(t).forEach(r=>{const i=t[r];Array.isArray(i)?i.forEach(o=>{n.push({param:r,value:o,op:"a"})}):n.push({param:r,value:i,op:"a"})}),this.clone(n)}set(t,n){return this.clone({param:t,value:n,op:"s"})}delete(t,n){return this.clone({param:t,value:n,op:"d"})}toString(){return this.init(),this.keys().map(t=>{const n=this.encoder.encodeKey(t);return this.map.get(t).map(r=>n+"="+this.encoder.encodeValue(r)).join("&")}).filter(t=>""!==t).join("&")}clone(t){const n=new ja({encoder:this.encoder});return n.cloneFrom=this.cloneFrom||this,n.updates=(this.updates||[]).concat(t),n}init(){null===this.map&&(this.map=new Map),null!==this.cloneFrom&&(this.cloneFrom.init(),this.cloneFrom.keys().forEach(t=>this.map.set(t,this.cloneFrom.map.get(t))),this.updates.forEach(t=>{switch(t.op){case"a":case"s":const n=("a"===t.op?this.map.get(t.param):void 0)||[];n.push(Cv(t.value)),this.map.set(t.param,n);break;case"d":if(void 0===t.value){this.map.delete(t.param);break}{let r=this.map.get(t.param)||[];const i=r.indexOf(Cv(t.value));-1!==i&&r.splice(i,1),r.length>0?this.map.set(t.param,r):this.map.delete(t.param)}}}),this.cloneFrom=this.updates=null)}}class nW{map=new Map;set(t,n){return this.map.set(t,n),this}get(t){return this.map.has(t)||this.map.set(t,t.defaultValue()),this.map.get(t)}delete(t){return this.map.delete(t),this}has(t){return this.map.has(t)}keys(){return this.map.keys()}}function vR(e){return typeof ArrayBuffer<"u"&&e instanceof ArrayBuffer}function bR(e){return typeof Blob<"u"&&e instanceof Blob}function _R(e){return typeof FormData<"u"&&e instanceof FormData}const ip="Content-Type",xE="X-Request-URL",wR="text/plain",CR="application/json",ER=`${CR}, ${wR}, */*`;class op{url;body=null;headers;context;reportProgress=!1;withCredentials=!1;credentials;keepalive=!1;cache;priority;mode;redirect;referrer;integrity;responseType="json";method;params;urlWithParams;transferCache;timeout;constructor(t,n,r,i){let o;if(this.url=n,this.method=t.toUpperCase(),function rW(e){switch(e){case"DELETE":case"GET":case"HEAD":case"OPTIONS":case"JSONP":return!1;default:return!0}}(this.method)||i?(this.body=void 0!==r?r:null,o=i):o=r,o){if(this.reportProgress=!!o.reportProgress,this.withCredentials=!!o.withCredentials,this.keepalive=!!o.keepalive,o.responseType&&(this.responseType=o.responseType),o.headers&&(this.headers=o.headers),o.context&&(this.context=o.context),o.params&&(this.params=o.params),o.priority&&(this.priority=o.priority),o.cache&&(this.cache=o.cache),o.credentials&&(this.credentials=o.credentials),"number"==typeof o.timeout){if(o.timeout<1||!Number.isInteger(o.timeout))throw new G(2822,"");this.timeout=o.timeout}o.mode&&(this.mode=o.mode),o.redirect&&(this.redirect=o.redirect),o.integrity&&(this.integrity=o.integrity),o.referrer&&(this.referrer=o.referrer),this.transferCache=o.transferCache}if(this.headers??=new yo,this.context??=new nW,this.params){const a=this.params.toString();if(0===a.length)this.urlWithParams=n;else{const u=n.indexOf("?");this.urlWithParams=n+(-1===u?"?":ude.set(Me,t.setHeaders[Me]),z)),t.setParams&&(ie=Object.keys(t.setParams).reduce((de,Me)=>de.set(Me,t.setParams[Me]),ie)),new op(n,r,L,{params:ie,headers:z,context:ae,reportProgress:V,responseType:i,withCredentials:H,transferCache:A,keepalive:o,cache:u,priority:a,timeout:N,mode:f,redirect:y,credentials:w,referrer:D,integrity:k})}}var Ha=function(e){return e[e.Sent=0]="Sent",e[e.UploadProgress=1]="UploadProgress",e[e.ResponseHeader=2]="ResponseHeader",e[e.DownloadProgress=3]="DownloadProgress",e[e.Response=4]="Response",e[e.User=5]="User",e}(Ha||{});class SE{headers;status;statusText;url;ok;type;redirected;constructor(t,n=200,r="OK"){this.headers=t.headers||new yo,this.status=void 0!==t.status?t.status:n,this.statusText=t.statusText||r,this.url=t.url||null,this.redirected=t.redirected,this.ok=this.status>=200&&this.status<300}}class Dv extends SE{constructor(t={}){super(t)}type=Ha.ResponseHeader;clone(t={}){return new Dv({headers:t.headers||this.headers,status:void 0!==t.status?t.status:this.status,statusText:t.statusText||this.statusText,url:t.url||this.url||void 0})}}class sp extends SE{body;constructor(t={}){super(t),this.body=void 0!==t.body?t.body:null}type=Ha.Response;clone(t={}){return new sp({body:void 0!==t.body?t.body:this.body,headers:t.headers||this.headers,status:void 0!==t.status?t.status:this.status,statusText:t.statusText||this.statusText,url:t.url||this.url||void 0,redirected:t.redirected??this.redirected})}}class Yl extends SE{name="HttpErrorResponse";message;error;ok=!1;constructor(t){super(t,0,"Unknown Error"),this.message=this.status>=200&&this.status<300?`Http failure during parsing for ${t.url||"(unknown url)"}`:`Http failure response for ${t.url||"(unknown url)"}: ${t.status} ${t.statusText}`,this.error=t.error||null}}function ME(e,t){return{body:t,headers:e.headers,context:e.context,observe:e.observe,params:e.params,reportProgress:e.reportProgress,responseType:e.responseType,withCredentials:e.withCredentials,credentials:e.credentials,transferCache:e.transferCache,timeout:e.timeout,keepalive:e.keepalive,priority:e.priority,cache:e.cache,mode:e.mode,redirect:e.redirect,integrity:e.integrity,referrer:e.referrer}}let xR=(()=>{class e{handler;constructor(n){this.handler=n}request(n,r,i={}){let o;if(n instanceof op)o=n;else{let f,y;f=i.headers instanceof yo?i.headers:new yo(i.headers),i.params&&(y=i.params instanceof ja?i.params:new ja({fromObject:i.params})),o=new op(n,r,void 0!==i.body?i.body:null,{headers:f,context:i.context,params:y,reportProgress:i.reportProgress,responseType:i.responseType||"json",withCredentials:i.withCredentials,transferCache:i.transferCache,keepalive:i.keepalive,priority:i.priority,cache:i.cache,mode:i.mode,redirect:i.redirect,credentials:i.credentials,referrer:i.referrer,integrity:i.integrity,timeout:i.timeout})}const a=xe(o).pipe(rp(f=>this.handler.handle(f)));if(n instanceof op||"events"===i.observe)return a;const u=a.pipe(Bs(f=>f instanceof sp));switch(i.observe||"body"){case"body":switch(o.responseType){case"arraybuffer":return u.pipe(tt(f=>{if(null!==f.body&&!(f.body instanceof ArrayBuffer))throw new G(2806,!1);return f.body}));case"blob":return u.pipe(tt(f=>{if(null!==f.body&&!(f.body instanceof Blob))throw new G(2807,!1);return f.body}));case"text":return u.pipe(tt(f=>{if(null!==f.body&&"string"!=typeof f.body)throw new G(2808,!1);return f.body}));default:return u.pipe(tt(f=>f.body))}case"response":return u;default:throw new G(2809,!1)}}delete(n,r={}){return this.request("DELETE",n,r)}get(n,r={}){return this.request("GET",n,r)}head(n,r={}){return this.request("HEAD",n,r)}jsonp(n,r){return this.request("JSONP",n,{params:(new ja).append(r,"JSONP_CALLBACK"),observe:"body",responseType:"json"})}options(n,r={}){return this.request("OPTIONS",n,r)}patch(n,r,i={}){return this.request("PATCH",n,ME(i,r))}post(n,r,i={}){return this.request("POST",n,ME(i,r))}put(n,r,i={}){return this.request("PUT",n,ME(i,r))}static \u0275fac=function(r){return new(r||e)(ke(_v))};static \u0275prov=pe({token:e,factory:e.\u0275fac})}return e})();const MR=new ee("");function TR(e,t){return t(e)}const ap=new ee(""),fW=new ee(""),IR=new ee("",{providedIn:"root",factory:()=>!0});let AR=(()=>{class e extends _v{backend;injector;chain=null;pendingTasks=Y(ru);contributeToStability=Y(IR);constructor(n,r){super(),this.backend=n,this.injector=r}handle(n){if(null===this.chain){const r=Array.from(new Set([...this.injector.get(ap),...this.injector.get(fW,[])]));this.chain=r.reduceRight((i,o)=>function dW(e,t,n){return(r,i)=>xn(n,()=>t(r,o=>e(o,i)))}(i,o,this.injector),TR)}if(this.contributeToStability){const r=this.pendingTasks.add();return this.chain(n,i=>this.backend.handle(i)).pipe(bv(r))}return this.chain(n,r=>this.backend.handle(r))}static \u0275fac=function(r){return new(r||e)(ke(wv),ke(Dn))};static \u0275prov=pe({token:e,factory:e.\u0275fac})}return e})();const vW=/^\)\]\}',?\n/,bW=RegExp(`^${xE}:`,"m");let OR=(()=>{class e{xhrFactory;constructor(n){this.xhrFactory=n}handle(n){if("JSONP"===n.method)throw new G(-2800,!1);const r=this.xhrFactory;return xe(null).pipe(Sr(()=>new Ut(o=>{const a=r.build();if(a.open(n.method,n.urlWithParams),n.withCredentials&&(a.withCredentials=!0),n.headers.forEach((H,V)=>a.setRequestHeader(H,V.join(","))),n.headers.has("Accept")||a.setRequestHeader("Accept",ER),!n.headers.has(ip)){const H=n.detectContentTypeHeader();null!==H&&a.setRequestHeader(ip,H)}if(n.timeout&&(a.timeout=n.timeout),n.responseType){const H=n.responseType.toLowerCase();a.responseType="json"!==H?H:"text"}const u=n.serializeBody();let f=null;const y=()=>{if(null!==f)return f;const H=a.statusText||"OK",V=new yo(a.getAllResponseHeaders()),z=function _W(e){return"responseURL"in e&&e.responseURL?e.responseURL:bW.test(e.getAllResponseHeaders())?e.getResponseHeader(xE):null}(a)||n.url;return f=new Dv({headers:V,status:a.status,statusText:H,url:z}),f},w=()=>{let{headers:H,status:V,statusText:z,url:ie}=y(),ae=null;204!==V&&(ae=typeof a.response>"u"?a.responseText:a.response),0===V&&(V=ae?200:0);let de=V>=200&&V<300;if("json"===n.responseType&&"string"==typeof ae){const Me=ae;ae=ae.replace(vW,"");try{ae=""!==ae?JSON.parse(ae):null}catch(et){ae=Me,de&&(de=!1,ae={error:et,text:ae})}}de?(o.next(new sp({body:ae,headers:H,status:V,statusText:z,url:ie||void 0})),o.complete()):o.error(new Yl({error:ae,headers:H,status:V,statusText:z,url:ie||void 0}))},D=H=>{const{url:V}=y(),z=new Yl({error:H,status:a.status||0,statusText:a.statusText||"Unknown Error",url:V||void 0});o.error(z)};let k=D;n.timeout&&(k=H=>{const{url:V}=y(),z=new Yl({error:new DOMException("Request timed out","TimeoutError"),status:a.status||0,statusText:a.statusText||"Request timeout",url:V||void 0});o.error(z)});let A=!1;const N=H=>{A||(o.next(y()),A=!0);let V={type:Ha.DownloadProgress,loaded:H.loaded};H.lengthComputable&&(V.total=H.total),"text"===n.responseType&&a.responseText&&(V.partialText=a.responseText),o.next(V)},L=H=>{let V={type:Ha.UploadProgress,loaded:H.loaded};H.lengthComputable&&(V.total=H.total),o.next(V)};return a.addEventListener("load",w),a.addEventListener("error",D),a.addEventListener("timeout",k),a.addEventListener("abort",D),n.reportProgress&&(a.addEventListener("progress",N),null!==u&&a.upload&&a.upload.addEventListener("progress",L)),a.send(u),o.next({type:Ha.Sent}),()=>{a.removeEventListener("error",D),a.removeEventListener("abort",D),a.removeEventListener("load",w),a.removeEventListener("timeout",k),n.reportProgress&&(a.removeEventListener("progress",N),null!==u&&a.upload&&a.upload.removeEventListener("progress",L)),a.readyState!==a.DONE&&a.abort()}})))}static \u0275fac=function(r){return new(r||e)(ke(au))};static \u0275prov=pe({token:e,factory:e.\u0275fac})}return e})();const AE=new ee(""),RR=new ee("",{providedIn:"root",factory:()=>"XSRF-TOKEN"}),NR=new ee("",{providedIn:"root",factory:()=>"X-XSRF-TOKEN"});class PR{}let EW=(()=>{class e{doc;cookieName;lastCookieString="";lastToken=null;parseCount=0;constructor(n,r){this.doc=n,this.cookieName=r}getToken(){const n=this.doc.cookie||"";return n!==this.lastCookieString&&(this.parseCount++,this.lastToken=Th(n,this.cookieName),this.lastCookieString=n),this.lastToken}static \u0275fac=function(r){return new(r||e)(ke(ut),ke(RR))};static \u0275prov=pe({token:e,factory:e.\u0275fac})}return e})();function DW(e,t){const n=e.url.toLowerCase();if(!Y(AE)||"GET"===e.method||"HEAD"===e.method||n.startsWith("http://")||n.startsWith("https://"))return t(e);const r=Y(PR).getToken(),i=Y(NR);return null!=r&&!e.headers.has(i)&&(e=e.clone({headers:e.headers.set(i,r)})),t(e)}let Zl=(()=>{class e{http;apiBaseUrl="http://localhost:9090/api/v1/mining";pollingSubscription;state=tn({needsSetup:!1,apiAvailable:!0,error:null,systemInfo:{},manageableMiners:[],installedMiners:[],runningMiners:[],profiles:[]});hashrateHistory=tn(new Map);runningMiners=It(()=>this.state().runningMiners);installedMiners=It(()=>this.state().installedMiners);apiAvailable=It(()=>this.state().apiAvailable);profiles=It(()=>this.state().profiles);constructor(n){this.http=n,this.forceRefreshState(),this.startPollingLive_Data()}ngOnDestroy(){this.stopPolling()}forceRefreshState(){DE({available:this.getAvailableMiners().pipe(Tr(()=>xe([]))),info:this.getSystemInfo().pipe(Tr(()=>xe({installed_miners_info:[]}))),running:this.getRunningMiners().pipe(Tr(()=>xe([]))),profiles:this.getProfiles().pipe(Tr(()=>xe([])))}).pipe(tt(({available:n,info:r,running:i,profiles:o})=>this.processSystemState(n,r,i,o)),Tr(n=>this.handleApiError(n))).subscribe(n=>{n&&(this.state.set(n),this.updateHashrateHistory(n.runningMiners))})}startPollingLive_Data(){this.pollingSubscription=function K9(e=0,t=mR){return e<0&&(e=0),function Z9(e=0,t,n=Y9){let r=-1;return null!=t&&(qO(t)?n=t:r=t),new Ut(i=>{let o=function X9(e){return e instanceof Date&&!isNaN(e)}(e)?+e-n.now():e;o<0&&(o=0);let a=0;return n.schedule(function(){i.closed||(i.next(a++),0<=r?this.schedule(void 0,r):i.complete())},o)})}(e,e,t)}(5e3).pipe(Sr(()=>this.getRunningMiners().pipe(Tr(()=>xe([]))))).subscribe(n=>{this.state.update(r=>({...r,runningMiners:n})),this.updateHashrateHistory(n)})}stopPolling(){this.pollingSubscription?.unsubscribe()}refreshProfiles(){this.getProfiles().pipe(Tr(()=>xe(this.state().profiles))).subscribe(n=>{this.state.update(r=>({...r,profiles:n}))})}refreshSystemInfo(){DE({available:this.getAvailableMiners().pipe(Tr(()=>xe([]))),info:this.getSystemInfo().pipe(Tr(()=>xe({installed_miners_info:[]})))}).subscribe(({available:n,info:r})=>{const{manageableMiners:i,installedMiners:o}=this.processStaticMinerInfo(n,r);this.state.update(a=>({...a,manageableMiners:i,installedMiners:o,systemInfo:r}))})}installMiner(n){return this.http.post(`${this.apiBaseUrl}/miners/${n}/install`,{}).pipe(vn(()=>setTimeout(()=>this.refreshSystemInfo(),1e3)))}uninstallMiner(n){return this.http.delete(`${this.apiBaseUrl}/miners/${n}/uninstall`).pipe(vn(()=>setTimeout(()=>this.refreshSystemInfo(),1e3)))}startMiner(n){return this.http.post(`${this.apiBaseUrl}/profiles/${n}/start`,{}).pipe()}stopMiner(n){return this.http.delete(`${this.apiBaseUrl}/miners/${n}`).pipe()}createProfile(n){return this.http.post(`${this.apiBaseUrl}/profiles`,n).pipe(vn(()=>this.refreshProfiles()))}updateProfile(n){return this.http.put(`${this.apiBaseUrl}/profiles/${n.id}`,n).pipe(vn(()=>this.refreshProfiles()))}deleteProfile(n){return this.http.delete(`${this.apiBaseUrl}/profiles/${n}`).pipe(vn(()=>this.refreshProfiles()))}getAvailableMiners=()=>this.http.get(`${this.apiBaseUrl}/miners/available`);getSystemInfo=()=>this.http.get(`${this.apiBaseUrl}/info`);getRunningMiners=()=>this.http.get(`${this.apiBaseUrl}/miners`);getProfiles=()=>this.http.get(`${this.apiBaseUrl}/profiles`);updateHashrateHistory(n){const r=new Map;n.forEach(i=>{i.hashrateHistory&&r.set(i.name,i.hashrateHistory)}),this.hashrateHistory.set(r)}processStaticMinerInfo(n,r){const i=new Map;(r.installed_miners_info||[]).forEach(u=>{if(u.is_installed){const f=this.getMinerType(u);i.set(f,{...u,type:f})}});const o=Array.from(i.values());return{manageableMiners:n.map(u=>({...u,is_installed:i.has(u.name)})),installedMiners:o}}processSystemState(n,r,i,o){const{manageableMiners:a,installedMiners:u}=this.processStaticMinerInfo(n,r);return{needsSetup:0===u.length,apiAvailable:!0,error:null,systemInfo:r,manageableMiners:a,installedMiners:u,runningMiners:i,profiles:o}}handleApiError(n){return console.error("API not available or needs setup:",n),this.hashrateHistory.set(new Map),this.state.set({needsSetup:!1,apiAvailable:!1,error:"Failed to connect to the mining API.",systemInfo:{},manageableMiners:[],installedMiners:[],runningMiners:[],profiles:[]}),xe(null)}getMinerType(n){if(!n.path)return"unknown";const r=n.path.split("/").filter(i=>i);return r.length>1?r[r.length-2]:r[r.length-1]||"unknown"}static \u0275fac=function(r){return new(r||e)(ke(xR))};static \u0275prov=pe({token:e,factory:e.\u0275fac,providedIn:"root"})}return e})();var kE="",OE="";function FR(e){kE=e}var Vs="7.0.1",kW={name:"default",resolver:(e,t="classic",n="solid")=>function IW(e,t,n){const r=function TW(){if(!OE){const e=document.querySelector("[data-fa-kit-code]");e&&function MW(e){OE=e}(e.getAttribute("data-fa-kit-code")||"")}return OE}(),i=r.length>0;let o="solid";return"notdog"===t?("solid"===n&&(o="solid"),"duo-solid"===n&&(o="duo-solid"),`https://ka-p.fontawesome.com/releases/v${Vs}/svgs/notdog-${o}/${e}.svg?token=${encodeURIComponent(r)}`):"chisel"===t?`https://ka-p.fontawesome.com/releases/v${Vs}/svgs/chisel-regular/${e}.svg?token=${encodeURIComponent(r)}`:"etch"===t?`https://ka-p.fontawesome.com/releases/v${Vs}/svgs/etch-solid/${e}.svg?token=${encodeURIComponent(r)}`:"jelly"===t?("regular"===n&&(o="regular"),"duo-regular"===n&&(o="duo-regular"),"fill-regular"===n&&(o="fill-regular"),`https://ka-p.fontawesome.com/releases/v${Vs}/svgs/jelly-${o}/${e}.svg?token=${encodeURIComponent(r)}`):"slab"===t?(("solid"===n||"regular"===n)&&(o="regular"),"press-regular"===n&&(o="press-regular"),`https://ka-p.fontawesome.com/releases/v${Vs}/svgs/slab-${o}/${e}.svg?token=${encodeURIComponent(r)}`):"thumbprint"===t?`https://ka-p.fontawesome.com/releases/v${Vs}/svgs/thumbprint-light/${e}.svg?token=${encodeURIComponent(r)}`:"whiteboard"===t?`https://ka-p.fontawesome.com/releases/v${Vs}/svgs/whiteboard-semibold/${e}.svg?token=${encodeURIComponent(r)}`:("classic"===t&&("thin"===n&&(o="thin"),"light"===n&&(o="light"),"regular"===n&&(o="regular"),"solid"===n&&(o="solid")),"sharp"===t&&("thin"===n&&(o="sharp-thin"),"light"===n&&(o="sharp-light"),"regular"===n&&(o="sharp-regular"),"solid"===n&&(o="sharp-solid")),"duotone"===t&&("thin"===n&&(o="duotone-thin"),"light"===n&&(o="duotone-light"),"regular"===n&&(o="duotone-regular"),"solid"===n&&(o="duotone")),"sharp-duotone"===t&&("thin"===n&&(o="sharp-duotone-thin"),"light"===n&&(o="sharp-duotone-light"),"regular"===n&&(o="sharp-duotone-regular"),"solid"===n&&(o="sharp-duotone-solid")),"brands"===t&&(o="brands"),i?`https://ka-p.fontawesome.com/releases/v${Vs}/svgs/${o}/${e}.svg?token=${encodeURIComponent(r)}`:`https://ka-f.fontawesome.com/releases/v${Vs}/svgs/${o}/${e}.svg`)}(e,t,n),mutator:(e,t)=>{if(t?.family&&!e.hasAttribute("data-duotone-initialized")){const{family:n,variant:r}=t;if("duotone"===n||"sharp-duotone"===n||"notdog"===n&&"duo-solid"===r||"jelly"===n&&"duo-regular"===r||"thumbprint"===n){const i=[...e.querySelectorAll("path")],o=i.find(u=>!u.hasAttribute("opacity")),a=i.find(u=>u.hasAttribute("opacity"));if(!o||!a)return;if(o.setAttribute("data-duotone-primary",""),a.setAttribute("data-duotone-secondary",""),t.swapOpacity&&o&&a){const u=a.getAttribute("opacity")||"0.4";o.style.setProperty("--path-opacity",u),a.style.setProperty("--path-opacity","1")}e.setAttribute("data-duotone-initialized","")}}}};function RE(e){return NE.apply(this,arguments)}function NE(){return(NE=(0,oe.A)(function*(e){const t=e instanceof Element?e.tagName.toLowerCase():"",n=t?.startsWith("wa-"),r=[...e.querySelectorAll(":not(:defined)")].map(a=>a.tagName.toLowerCase()).filter(a=>a.startsWith("wa-"));n&&!customElements.get(t)&&r.push(t);const i=[...new Set(r)],o=yield Promise.allSettled(i.map(a=>function OW(e){if(customElements.get(e))return Promise.resolve();const t=e.replace(/^wa-/i,""),n=function SW(e=""){if(!kE){const t=document.querySelector("[data-webawesome]");if(t?.hasAttribute("data-webawesome"))FR(new URL(t.getAttribute("data-webawesome")??"",window.location.href).pathname);else{const r=[...document.getElementsByTagName("script")].find(i=>i.src.endsWith("webawesome.js")||i.src.endsWith("webawesome.loader.js")||i.src.endsWith("webawesome.ssr-loader.js"));r&&FR(String(r.getAttribute("src")).split("/").slice(0,-1).join("/"))}}return kE.replace(/\/$/,"")+(e?`/${e.replace(/^\//,"")}`:"")}(`components/${t}/${t}.js`);return new Promise((r,i)=>{Se(136)(n).then(()=>r()).catch(()=>i(new Error(`Unable to autoload <${e}> from ${n}`)))})}(a)));for(const a of o)"rejected"===a.status&&console.warn(a.reason);yield new Promise(requestAnimationFrame),e.dispatchEvent(new CustomEvent("wa-discovery-complete",{bubbles:!1,cancelable:!1,composed:!0}))})).apply(this,arguments)}new MutationObserver(e=>{for(const{addedNodes:t}of e)for(const n of t)n.nodeType===Node.ELEMENT_NODE&&RE(n)});const LE=new Set,id=new Map;let Kl,FE="ltr",BE="en";const VR=typeof MutationObserver<"u"&&typeof document<"u"&&typeof document.documentElement<"u";if(VR){const e=new MutationObserver(jR);FE=document.documentElement.dir||"ltr",BE=document.documentElement.lang||navigator.language,e.observe(document.documentElement,{attributes:!0,attributeFilter:["dir","lang"]})}function $R(...e){e.map(t=>{const n=t.$code.toLowerCase();id.has(n)?id.set(n,Object.assign(Object.assign({},id.get(n)),t)):id.set(n,t),Kl||(Kl=t)}),jR()}function jR(){VR&&(FE=document.documentElement.dir||"ltr",BE=document.documentElement.lang||navigator.language),[...LE.keys()].map(e=>{"function"==typeof e.requestUpdate&&e.requestUpdate()})}class RW{constructor(t){this.host=t,this.host.addController(this)}hostConnected(){LE.add(this.host)}hostDisconnected(){LE.delete(this.host)}dir(){return`${this.host.dir||FE}`.toLowerCase()}lang(){return`${this.host.lang||BE}`.toLowerCase()}getTranslationData(t){var n,r;const i=new Intl.Locale(t.replace(/_/g,"-")),o=i?.language.toLowerCase(),a=null!==(r=null===(n=i?.region)||void 0===n?void 0:n.toLowerCase())&&void 0!==r?r:"";return{locale:i,language:o,region:a,primary:id.get(`${o}-${a}`),secondary:id.get(o)}}exists(t,n){var r;const{primary:i,secondary:o}=this.getTranslationData(null!==(r=n.lang)&&void 0!==r?r:this.lang());return n=Object.assign({includeFallback:!1},n),!!(i&&i[t]||o&&o[t]||n.includeFallback&&Kl&&Kl[t])}term(t,...n){const{primary:r,secondary:i}=this.getTranslationData(this.lang());let o;if(r&&r[t])o=r[t];else if(i&&i[t])o=i[t];else{if(!Kl||!Kl[t])return console.error(`No translation found for: ${String(t)}`),String(t);o=Kl[t]}return"function"==typeof o?o(...n):o}date(t,n){return t=new Date(t),new Intl.DateTimeFormat(this.lang(),n).format(t)}number(t,n){return t=Number(t),isNaN(t)?"":new Intl.NumberFormat(this.lang(),n).format(t)}relativeTime(t,n,r){return new Intl.RelativeTimeFormat(this.lang(),r).format(t,n)}}var HR={$code:"en",$name:"English",$dir:"ltr",carousel:"Carousel",clearEntry:"Clear entry",close:"Close",copied:"Copied",copy:"Copy",currentValue:"Current value",error:"Error",goToSlide:(e,t)=>`Go to slide ${e} of ${t}`,hidePassword:"Hide password",loading:"Loading",nextSlide:"Next slide",numOptionsSelected:e=>0===e?"No options selected":1===e?"1 option selected":`${e} options selected`,pauseAnimation:"Pause animation",playAnimation:"Play animation",previousSlide:"Previous slide",progress:"Progress",remove:"Remove",resize:"Resize",scrollableRegion:"Scrollable region",scrollToEnd:"Scroll to end",scrollToStart:"Scroll to start",selectAColorFromTheScreen:"Select a color from the screen",showPassword:"Show password",slideNum:e=>`Slide ${e}`,toggleColorFormat:"Toggle color format",zoomIn:"Zoom in",zoomOut:"Zoom out"};$R(HR);var NW=HR,za=class extends RW{};$R(NW);var VE={solid:{check:'\x3c!--! Font Awesome Free 7.0.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2025 Fonticons, Inc. --\x3e',"chevron-down":'\x3c!--! Font Awesome Free 7.0.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2025 Fonticons, Inc. --\x3e',"chevron-left":'\x3c!--! Font Awesome Free 7.0.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2025 Fonticons, Inc. --\x3e',"chevron-right":'\x3c!--! Font Awesome Free 7.0.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2025 Fonticons, Inc. --\x3e',circle:'\x3c!--! Font Awesome Free 7.0.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2025 Fonticons, Inc. --\x3e',eyedropper:'\x3c!--! Font Awesome Free 7.0.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2025 Fonticons, Inc. --\x3e',"grip-vertical":'\x3c!--! Font Awesome Free 7.0.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2025 Fonticons, Inc. --\x3e',indeterminate:'',minus:'\x3c!--! Font Awesome Free 7.0.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2025 Fonticons, Inc. --\x3e',pause:'\x3c!--! Font Awesome Free 7.0.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2025 Fonticons, Inc. --\x3e',play:'\x3c!--! Font Awesome Free 7.0.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2025 Fonticons, Inc. --\x3e',star:'\x3c!--! Font Awesome Free 7.0.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2025 Fonticons, Inc. --\x3e',user:'\x3c!--! Font Awesome Free 7.0.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2025 Fonticons, Inc. --\x3e',xmark:'\x3c!--! Font Awesome Free 7.0.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2025 Fonticons, Inc. --\x3e'},regular:{"circle-question":'\x3c!--! Font Awesome Free 7.0.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2025 Fonticons, Inc. --\x3e',"circle-xmark":'\x3c!--! Font Awesome Free 7.0.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2025 Fonticons, Inc. --\x3e',copy:'\x3c!--! Font Awesome Free 7.0.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2025 Fonticons, Inc. --\x3e',eye:'\x3c!--! Font Awesome Free 7.0.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2025 Fonticons, Inc. --\x3e',"eye-slash":'\x3c!--! Font Awesome Free 7.0.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2025 Fonticons, Inc. --\x3e',star:'\x3c!--! Font Awesome Free 7.0.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2025 Fonticons, Inc. --\x3e'}},xv=[kW,{name:"system",resolver:(e,t="classic",n="solid")=>{let i=VE[n][e]??VE.regular[e]??VE.regular["circle-question"];return i?function PW(e){return`data:image/svg+xml,${encodeURIComponent(e)}`}(i):""}}],lp=[];function $E(e){return xv.find(t=>t.name===e)}var HW=Object.defineProperty,UW=Object.getOwnPropertyDescriptor,zR=e=>{throw TypeError(e)},B=(e,t,n,r)=>{for(var a,i=r>1?void 0:r?UW(t,n):t,o=e.length-1;o>=0;o--)(a=e[o])&&(i=(r?a(t,n,i):a(i))||i);return r&&i&&HW(t,n,i),i},WR=(e,t,n)=>t.has(e)||zR("Cannot "+n),GR=()=>({checkValidity(e){const t=e.input,n={message:"",isValid:!0,invalidKeys:[]};if(!t)return n;let r=!0;if("checkValidity"in t&&(r=t.checkValidity()),r)return n;if(n.isValid=!1,"validationMessage"in t&&(n.message=t.validationMessage),!("validity"in t))return n.invalidKeys.push("customError"),n;for(const i in t.validity)"valid"!==i&&t.validity[i]&&n.invalidKeys.push(i);return n}});const Sv=globalThis,jE=Sv.ShadowRoot&&(void 0===Sv.ShadyCSS||Sv.ShadyCSS.nativeShadow)&&"adoptedStyleSheets"in Document.prototype&&"replace"in CSSStyleSheet.prototype,HE=Symbol(),qR=new WeakMap;class YR{constructor(t,n,r){if(this._$cssResult$=!0,r!==HE)throw Error("CSSResult is not constructable. Use `unsafeCSS` or `css` instead.");this.cssText=t,this.t=n}get styleSheet(){let t=this.o;const n=this.t;if(jE&&void 0===t){const r=void 0!==n&&1===n.length;r&&(t=qR.get(n)),void 0===t&&((this.o=t=new CSSStyleSheet).replaceSync(this.cssText),r&&qR.set(n,t))}return t}toString(){return this.cssText}}const XR=e=>new YR("string"==typeof e?e:e+"",void 0,HE),ZR=jE?e=>e:e=>e instanceof CSSStyleSheet?(t=>{let n="";for(const r of t.cssRules)n+=r.cssText;return XR(n)})(e):e,{is:YW,defineProperty:XW,getOwnPropertyDescriptor:ZW,getOwnPropertyNames:KW,getOwnPropertySymbols:QW,getPrototypeOf:JW}=Object,Mv=globalThis,KR=Mv.trustedTypes,eG=KR?KR.emptyScript:"",tG=Mv.reactiveElementPolyfillSupport,cp=(e,t)=>e,Tv={toAttribute(e,t){switch(t){case Boolean:e=e?eG:null;break;case Object:case Array:e=null==e?e:JSON.stringify(e)}return e},fromAttribute(e,t){let n=e;switch(t){case Boolean:n=null!==e;break;case Number:n=null===e?null:Number(e);break;case Object:case Array:try{n=JSON.parse(e)}catch{n=null}}return n}},UE=(e,t)=>!YW(e,t),QR={attribute:!0,type:String,converter:Tv,reflect:!1,useDefault:!1,hasChanged:UE};Symbol.metadata??=Symbol("metadata"),Mv.litPropertyMetadata??=new WeakMap;class od extends HTMLElement{static addInitializer(t){this._$Ei(),(this.l??=[]).push(t)}static get observedAttributes(){return this.finalize(),this._$Eh&&[...this._$Eh.keys()]}static createProperty(t,n=QR){if(n.state&&(n.attribute=!1),this._$Ei(),this.prototype.hasOwnProperty(t)&&((n=Object.create(n)).wrapped=!0),this.elementProperties.set(t,n),!n.noAccessor){const r=Symbol(),i=this.getPropertyDescriptor(t,r,n);void 0!==i&&XW(this.prototype,t,i)}}static getPropertyDescriptor(t,n,r){const{get:i,set:o}=ZW(this.prototype,t)??{get(){return this[n]},set(a){this[n]=a}};return{get:i,set(a){const u=i?.call(this);o?.call(this,a),this.requestUpdate(t,u,r)},configurable:!0,enumerable:!0}}static getPropertyOptions(t){return this.elementProperties.get(t)??QR}static _$Ei(){if(this.hasOwnProperty(cp("elementProperties")))return;const t=JW(this);t.finalize(),void 0!==t.l&&(this.l=[...t.l]),this.elementProperties=new Map(t.elementProperties)}static finalize(){if(this.hasOwnProperty(cp("finalized")))return;if(this.finalized=!0,this._$Ei(),this.hasOwnProperty(cp("properties"))){const n=this.properties,r=[...KW(n),...QW(n)];for(const i of r)this.createProperty(i,n[i])}const t=this[Symbol.metadata];if(null!==t){const n=litPropertyMetadata.get(t);if(void 0!==n)for(const[r,i]of n)this.elementProperties.set(r,i)}this._$Eh=new Map;for(const[n,r]of this.elementProperties){const i=this._$Eu(n,r);void 0!==i&&this._$Eh.set(i,n)}this.elementStyles=this.finalizeStyles(this.styles)}static finalizeStyles(t){const n=[];if(Array.isArray(t)){const r=new Set(t.flat(1/0).reverse());for(const i of r)n.unshift(ZR(i))}else void 0!==t&&n.push(ZR(t));return n}static _$Eu(t,n){const r=n.attribute;return!1===r?void 0:"string"==typeof r?r:"string"==typeof t?t.toLowerCase():void 0}constructor(){super(),this._$Ep=void 0,this.isUpdatePending=!1,this.hasUpdated=!1,this._$Em=null,this._$Ev()}_$Ev(){this._$ES=new Promise(t=>this.enableUpdating=t),this._$AL=new Map,this._$E_(),this.requestUpdate(),this.constructor.l?.forEach(t=>t(this))}addController(t){(this._$EO??=new Set).add(t),void 0!==this.renderRoot&&this.isConnected&&t.hostConnected?.()}removeController(t){this._$EO?.delete(t)}_$E_(){const t=new Map,n=this.constructor.elementProperties;for(const r of n.keys())this.hasOwnProperty(r)&&(t.set(r,this[r]),delete this[r]);t.size>0&&(this._$Ep=t)}createRenderRoot(){const t=this.shadowRoot??this.attachShadow(this.constructor.shadowRootOptions);return((e,t)=>{if(jE)e.adoptedStyleSheets=t.map(n=>n instanceof CSSStyleSheet?n:n.styleSheet);else for(const n of t){const r=document.createElement("style"),i=Sv.litNonce;void 0!==i&&r.setAttribute("nonce",i),r.textContent=n.cssText,e.appendChild(r)}})(t,this.constructor.elementStyles),t}connectedCallback(){this.renderRoot??=this.createRenderRoot(),this.enableUpdating(!0),this._$EO?.forEach(t=>t.hostConnected?.())}enableUpdating(t){}disconnectedCallback(){this._$EO?.forEach(t=>t.hostDisconnected?.())}attributeChangedCallback(t,n,r){this._$AK(t,r)}_$ET(t,n){const r=this.constructor.elementProperties.get(t),i=this.constructor._$Eu(t,r);if(void 0!==i&&!0===r.reflect){const o=(void 0!==r.converter?.toAttribute?r.converter:Tv).toAttribute(n,r.type);this._$Em=t,null==o?this.removeAttribute(i):this.setAttribute(i,o),this._$Em=null}}_$AK(t,n){const r=this.constructor,i=r._$Eh.get(t);if(void 0!==i&&this._$Em!==i){const o=r.getPropertyOptions(i),a="function"==typeof o.converter?{fromAttribute:o.converter}:void 0!==o.converter?.fromAttribute?o.converter:Tv;this._$Em=i;const u=a.fromAttribute(n,o.type);this[i]=u??this._$Ej?.get(i)??u,this._$Em=null}}requestUpdate(t,n,r){if(void 0!==t){const i=this.constructor,o=this[t];if(r??=i.getPropertyOptions(t),!((r.hasChanged??UE)(o,n)||r.useDefault&&r.reflect&&o===this._$Ej?.get(t)&&!this.hasAttribute(i._$Eu(t,r))))return;this.C(t,n,r)}!1===this.isUpdatePending&&(this._$ES=this._$EP())}C(t,n,{useDefault:r,reflect:i,wrapped:o},a){r&&!(this._$Ej??=new Map).has(t)&&(this._$Ej.set(t,a??n??this[t]),!0!==o||void 0!==a)||(this._$AL.has(t)||(this.hasUpdated||r||(n=void 0),this._$AL.set(t,n)),!0===i&&this._$Em!==t&&(this._$Eq??=new Set).add(t))}_$EP(){var t=this;return(0,oe.A)(function*(){t.isUpdatePending=!0;try{yield t._$ES}catch(r){Promise.reject(r)}const n=t.scheduleUpdate();return null!=n&&(yield n),!t.isUpdatePending})()}scheduleUpdate(){return this.performUpdate()}performUpdate(){if(!this.isUpdatePending)return;if(!this.hasUpdated){if(this.renderRoot??=this.createRenderRoot(),this._$Ep){for(const[i,o]of this._$Ep)this[i]=o;this._$Ep=void 0}const r=this.constructor.elementProperties;if(r.size>0)for(const[i,o]of r){const{wrapped:a}=o,u=this[i];!0!==a||this._$AL.has(i)||void 0===u||this.C(i,void 0,o,u)}}let t=!1;const n=this._$AL;try{t=this.shouldUpdate(n),t?(this.willUpdate(n),this._$EO?.forEach(r=>r.hostUpdate?.()),this.update(n)):this._$EM()}catch(r){throw t=!1,this._$EM(),r}t&&this._$AE(n)}willUpdate(t){}_$AE(t){this._$EO?.forEach(n=>n.hostUpdated?.()),this.hasUpdated||(this.hasUpdated=!0,this.firstUpdated(t)),this.updated(t)}_$EM(){this._$AL=new Map,this.isUpdatePending=!1}get updateComplete(){return this.getUpdateComplete()}getUpdateComplete(){return this._$ES}shouldUpdate(t){return!0}update(t){this._$Eq&&=this._$Eq.forEach(n=>this._$ET(n,this[n])),this._$EM()}updated(t){}firstUpdated(t){}}od.elementStyles=[],od.shadowRootOptions={mode:"open"},od[cp("elementProperties")]=new Map,od[cp("finalized")]=new Map,tG?.({ReactiveElement:od}),(Mv.reactiveElementVersions??=[]).push("2.1.1");const zE=globalThis,Iv=zE.trustedTypes,JR=Iv?Iv.createPolicy("lit-html",{createHTML:e=>e}):void 0,WE="$lit$",$s=`lit$${Math.random().toFixed(9).slice(2)}$`,GE="?"+$s,nG=`<${GE}>`,Ql=document,up=()=>Ql.createComment(""),dp=e=>null===e||"object"!=typeof e&&"function"!=typeof e,qE=Array.isArray,eN=e=>qE(e)||"function"==typeof e?.[Symbol.iterator],YE="[ \t\n\f\r]",hp=/<(?:(!--|\/[^a-zA-Z])|(\/?[a-zA-Z][^>\s]*)|(\/?$))/g,tN=/-->/g,nN=/>/g,Jl=RegExp(`>|${YE}(?:([^\\s"'>=/]+)(${YE}*=${YE}*(?:[^ \t\n\f\r"'\`<>=]|("|')|))|$)`,"g"),rN=/'/g,iN=/"/g,oN=/^(?:script|style|textarea|title)$/i,XE=e=>(t,...n)=>({_$litType$:e,strings:t,values:n}),At=XE(1),rG=XE(2),iG=XE(3),Vi=Symbol.for("lit-noChange"),hn=Symbol.for("lit-nothing"),sN=new WeakMap,ec=Ql.createTreeWalker(Ql,129);function aN(e,t){if(!qE(e)||!e.hasOwnProperty("raw"))throw Error("invalid template strings array");return void 0!==JR?JR.createHTML(t):t}const lN=(e,t)=>{const n=e.length-1,r=[];let i,o=2===t?"":3===t?"":"",a=hp;for(let u=0;u"===w[0]?(a=i??hp,D=-1):void 0===w[1]?D=-2:(D=a.lastIndex-w[2].length,y=w[1],a=void 0===w[3]?Jl:'"'===w[3]?iN:rN):a===iN||a===rN?a=Jl:a===tN||a===nN?a=hp:(a=Jl,i=void 0);const A=a===Jl&&e[u+1].startsWith("/>")?" ":"";o+=a===hp?f+nG:D>=0?(r.push(y),f.slice(0,D)+WE+f.slice(D)+$s+A):f+$s+(-2===D?u:A)}return[aN(e,o+(e[n]||"")+(2===t?"":3===t?"":"")),r]};class fp{constructor({strings:t,_$litType$:n},r){let i;this.parts=[];let o=0,a=0;const u=t.length-1,f=this.parts,[y,w]=lN(t,n);if(this.el=fp.createElement(y,r),ec.currentNode=this.el.content,2===n||3===n){const D=this.el.content.firstChild;D.replaceWith(...D.childNodes)}for(;null!==(i=ec.nextNode())&&f.length0){i.textContent=Iv?Iv.emptyScript:"";for(let A=0;A2||""!==r[0]||""!==r[1]?(this._$AH=Array(r.length-1).fill(new String),this.strings=r):this._$AH=hn}_$AI(t,n=this,r,i){const o=this.strings;let a=!1;if(void 0===o)t=tc(this,t,n,0),a=!dp(t)||t!==this._$AH&&t!==Vi,a&&(this._$AH=t);else{const u=t;let f,y;for(t=o[0],f=0;f{const r=n?.renderBefore??t;let i=r._$litPart$;if(void 0===i){const o=n?.renderBefore??null;r._$litPart$=i=new sd(t.insertBefore(up(),o),o,void 0,n??{})}return i._$AI(e),i})(n,this.renderRoot,this.renderOptions)}connectedCallback(){super.connectedCallback(),this._$Do?.setConnected(!0)}disconnectedCallback(){super.disconnectedCallback(),this._$Do?.setConnected(!1)}render(){return Vi}}gp._$litElement$=!0,gp.finalized=!0,ZE.litElementHydrateSupport?.({LitElement:gp});const lG=ZE.litElementPolyfillSupport;lG?.({LitElement:gp}),(ZE.litElementVersions??=[]).push("4.2.1");const ui=e=>(t,n)=>{void 0!==n?n.addInitializer(()=>{customElements.define(e,t)}):customElements.define(e,t)},cG={attribute:!0,type:String,converter:Tv,reflect:!1,hasChanged:UE},uG=(e=cG,t,n)=>{const{kind:r,metadata:i}=n;let o=globalThis.litPropertyMetadata.get(i);if(void 0===o&&globalThis.litPropertyMetadata.set(i,o=new Map),"setter"===r&&((e=Object.create(e)).wrapped=!0),o.set(n.name,e),"accessor"===r){const{name:a}=n;return{set(u){const f=t.get.call(this);t.set.call(this,u),this.requestUpdate(a,f,e)},init(u){return void 0!==u&&this.C(a,void 0,e,u),u}}}if("setter"===r){const{name:a}=n;return function(u){const f=this[a];t.call(this,u),this.requestUpdate(a,f,e)}}throw Error("Unsupported decorator location: "+r)};function W(e){return(t,n)=>"object"==typeof n?uG(e,t,n):((r,i,o)=>{const a=i.hasOwnProperty(o);return i.constructor.createProperty(o,r),a?Object.getOwnPropertyDescriptor(i,o):void 0})(e,t,n)}function $i(e){return W({...e,state:!0,attribute:!1})}const pN=(e,t,n)=>(n.configurable=!0,n.enumerable=!0,Reflect.decorate&&"object"!=typeof t&&Object.defineProperty(e,t,n),n);function $n(e,t){return(n,r,i)=>{const o=a=>a.renderRoot?.querySelector(e)??null;if(t){const{get:a,set:u}="object"==typeof r?n:i??(()=>{const f=Symbol();return{get(){return this[f]},set(y){this[f]=y}}})();return pN(n,r,{get(){let f=a.call(this);return void 0===f&&(f=o(this),(null!==f||this.hasUpdated)&&u.call(this,f)),f}})}return pN(n,r,{get(){return o(this)}})}}var Av,dG=":host {\n box-sizing: border-box !important;\n}\n\n:host *,\n:host *::before,\n:host *::after {\n box-sizing: inherit !important;\n}\n\n[hidden] {\n display: none !important;\n}\n",di=class extends gp{constructor(){super(),((e,t)=>{t.has(e)?zR("Cannot add the same private member more than once"):t instanceof WeakSet?t.add(e):t.set(e,!1)})(this,Av),this.initialReflectedProperties=new Map,this.didSSR=!!this.shadowRoot,this.customStates={set:(t,n)=>{if(this.internals?.states)try{n?this.internals.states.add(t):this.internals.states.delete(t)}catch(r){if(!String(r).includes("must start with '--'"))throw r;console.error("Your browser implements an outdated version of CustomStateSet. Consider using a polyfill")}},has:t=>{if(!this.internals?.states)return!1;try{return this.internals.states.has(t)}catch{return!1}}};try{this.internals=this.attachInternals()}catch{console.error("Element internals are not supported in your browser. Consider using a polyfill")}this.customStates.set("wa-defined",!0);let e=this.constructor;for(let[t,n]of e.elementProperties)"inherit"===n.default&&void 0!==n.initial&&"string"==typeof t&&this.customStates.set(`initial-${t}-${n.initial}`,!0)}static get styles(){const e=Array.isArray(this.css)?this.css:this.css?[this.css]:[];return[dG,...e].map(t=>"string"==typeof t?XR(t):t)}attributeChangedCallback(e,t,n){((e,t)=>(WR(e,t,"read from private field"),t.get(e)))(this,Av)||(this.constructor.elementProperties.forEach((r,i)=>{r.reflect&&null!=this[i]&&this.initialReflectedProperties.set(i,this[i])}),((e,t,n)=>{WR(e,t,"write to private field"),t.set(e,n)})(this,Av,!0)),super.attributeChangedCallback(e,t,n)}willUpdate(e){super.willUpdate(e),this.initialReflectedProperties.forEach((t,n)=>{e.has(n)&&null==this[n]&&(this[n]=t)})}firstUpdated(e){super.firstUpdated(e),this.didSSR&&this.shadowRoot?.querySelectorAll("slot").forEach(t=>{t.dispatchEvent(new Event("slotchange",{bubbles:!0,composed:!1,cancelable:!1}))})}update(e){try{super.update(e)}catch(t){if(this.didSSR&&!this.hasUpdated){const n=new Event("lit-hydration-error",{bubbles:!0,composed:!0,cancelable:!1});n.error=t,this.dispatchEvent(n)}throw t}}relayNativeEvent(e,t){e.stopImmediatePropagation(),this.dispatchEvent(new e.constructor(e.type,{...e,...t}))}};Av=new WeakMap,B([W()],di.prototype,"dir",2),B([W()],di.prototype,"lang",2),B([W({type:Boolean,reflect:!0,attribute:"did-ssr"})],di.prototype,"didSSR",2);var gN=class extends Event{constructor(){super("wa-invalid",{bubbles:!0,cancelable:!1,composed:!0})}},Gr=(()=>{let e=class extends di{constructor(){super(),this.name=null,this.disabled=!1,this.required=!1,this.assumeInteractionOn=["input"],this.validators=[],this.valueHasChanged=!1,this.hasInteracted=!1,this.customError=null,this.emittedEvents=[],this.emitInvalid=t=>{t.target===this&&(this.hasInteracted=!0,this.dispatchEvent(new gN))},this.handleInteraction=t=>{const n=this.emittedEvents;n.includes(t.type)||n.push(t.type),n.length===this.assumeInteractionOn?.length&&(this.hasInteracted=!0)},this.addEventListener("invalid",this.emitInvalid)}static get validators(){return[{observedAttributes:["custom-error"],checkValidity(e){const t={message:"",isValid:!0,invalidKeys:[]};return e.customError&&(t.message=e.customError,t.isValid=!1,t.invalidKeys=["customError"]),t}}]}static get observedAttributes(){const t=new Set(super.observedAttributes||[]);for(const n of this.validators)if(n.observedAttributes)for(const r of n.observedAttributes)t.add(r);return[...t]}connectedCallback(){super.connectedCallback(),this.updateValidity(),this.assumeInteractionOn.forEach(t=>{this.addEventListener(t,this.handleInteraction)})}firstUpdated(...t){super.firstUpdated(...t),this.updateValidity()}willUpdate(t){if(t.has("customError")&&(this.customError||(this.customError=null),this.setCustomValidity(this.customError||"")),t.has("value")||t.has("disabled")){const n=this.value;if(Array.isArray(n)){if(this.name){const r=new FormData;for(const i of n)r.append(this.name,i);this.setValue(r,r)}}else this.setValue(n,n)}t.has("disabled")&&(this.customStates.set("disabled",this.disabled),(this.hasAttribute("disabled")||!this.matches(":disabled"))&&this.toggleAttribute("disabled",this.disabled)),this.updateValidity(),super.willUpdate(t)}get labels(){return this.internals.labels}getForm(){return this.internals.form}get validity(){return this.internals.validity}get willValidate(){return this.internals.willValidate}get validationMessage(){return this.internals.validationMessage}checkValidity(){return this.updateValidity(),this.internals.checkValidity()}reportValidity(){return this.updateValidity(),this.hasInteracted=!0,this.internals.reportValidity()}get validationTarget(){return this.input||void 0}setValidity(...t){let i=t[2];i||(i=this.validationTarget),this.internals.setValidity(t[0],t[1],i||void 0),this.requestUpdate("validity"),this.setCustomStates()}setCustomStates(){const t=!!this.required,n=this.internals.validity.valid,r=this.hasInteracted;this.customStates.set("required",t),this.customStates.set("optional",!t),this.customStates.set("invalid",!n),this.customStates.set("valid",n),this.customStates.set("user-invalid",!n&&r),this.customStates.set("user-valid",n&&r)}setCustomValidity(t){if(!t)return this.customError=null,void this.setValidity({});this.customError=t,this.setValidity({customError:!0},t,this.validationTarget)}formResetCallback(){this.resetValidity(),this.hasInteracted=!1,this.valueHasChanged=!1,this.emittedEvents=[],this.updateValidity()}formDisabledCallback(t){this.disabled=t,this.updateValidity()}formStateRestoreCallback(t,n){this.value=t,"restore"===n&&this.resetValidity(),this.updateValidity()}setValue(...t){const[n,r]=t;this.internals.setFormValue(n,r)}get allValidators(){return[...this.constructor.validators||[],...this.validators||[]]}resetValidity(){this.setCustomValidity(""),this.setValidity({})}updateValidity(){if(this.disabled||this.hasAttribute("disabled")||!this.willValidate)return void this.resetValidity();const t=this.allValidators;if(!t?.length)return;const n={customError:!!this.customError},r=this.validationTarget||this.input||void 0;let i="";for(const o of t){const{isValid:a,message:u,invalidKeys:f}=o.checkValidity(this);a||(i||(i=u),f?.length>=0&&f.forEach(y=>n[y]=!0))}i||(i=this.validationMessage),this.setValidity(n,i,r)}};return e.formAssociated=!0,e})();B([W({reflect:!0})],Gr.prototype,"name",2),B([W({type:Boolean})],Gr.prototype,"disabled",2),B([W({state:!0,attribute:!1})],Gr.prototype,"valueHasChanged",2),B([W({state:!0,attribute:!1})],Gr.prototype,"hasInteracted",2),B([W({attribute:"custom-error",reflect:!0})],Gr.prototype,"customError",2),B([W({attribute:!1,state:!0,type:Object})],Gr.prototype,"validity",1);var KE="@layer wa-utilities {\n :where(:root),\n .wa-neutral,\n :host([variant='neutral']) {\n --wa-color-fill-loud: var(--wa-color-neutral-fill-loud);\n --wa-color-fill-normal: var(--wa-color-neutral-fill-normal);\n --wa-color-fill-quiet: var(--wa-color-neutral-fill-quiet);\n --wa-color-border-loud: var(--wa-color-neutral-border-loud);\n --wa-color-border-normal: var(--wa-color-neutral-border-normal);\n --wa-color-border-quiet: var(--wa-color-neutral-border-quiet);\n --wa-color-on-loud: var(--wa-color-neutral-on-loud);\n --wa-color-on-normal: var(--wa-color-neutral-on-normal);\n --wa-color-on-quiet: var(--wa-color-neutral-on-quiet);\n }\n\n .wa-brand,\n :host([variant='brand']) {\n --wa-color-fill-loud: var(--wa-color-brand-fill-loud);\n --wa-color-fill-normal: var(--wa-color-brand-fill-normal);\n --wa-color-fill-quiet: var(--wa-color-brand-fill-quiet);\n --wa-color-border-loud: var(--wa-color-brand-border-loud);\n --wa-color-border-normal: var(--wa-color-brand-border-normal);\n --wa-color-border-quiet: var(--wa-color-brand-border-quiet);\n --wa-color-on-loud: var(--wa-color-brand-on-loud);\n --wa-color-on-normal: var(--wa-color-brand-on-normal);\n --wa-color-on-quiet: var(--wa-color-brand-on-quiet);\n }\n\n .wa-success,\n :host([variant='success']) {\n --wa-color-fill-loud: var(--wa-color-success-fill-loud);\n --wa-color-fill-normal: var(--wa-color-success-fill-normal);\n --wa-color-fill-quiet: var(--wa-color-success-fill-quiet);\n --wa-color-border-loud: var(--wa-color-success-border-loud);\n --wa-color-border-normal: var(--wa-color-success-border-normal);\n --wa-color-border-quiet: var(--wa-color-success-border-quiet);\n --wa-color-on-loud: var(--wa-color-success-on-loud);\n --wa-color-on-normal: var(--wa-color-success-on-normal);\n --wa-color-on-quiet: var(--wa-color-success-on-quiet);\n }\n\n .wa-warning,\n :host([variant='warning']) {\n --wa-color-fill-loud: var(--wa-color-warning-fill-loud);\n --wa-color-fill-normal: var(--wa-color-warning-fill-normal);\n --wa-color-fill-quiet: var(--wa-color-warning-fill-quiet);\n --wa-color-border-loud: var(--wa-color-warning-border-loud);\n --wa-color-border-normal: var(--wa-color-warning-border-normal);\n --wa-color-border-quiet: var(--wa-color-warning-border-quiet);\n --wa-color-on-loud: var(--wa-color-warning-on-loud);\n --wa-color-on-normal: var(--wa-color-warning-on-normal);\n --wa-color-on-quiet: var(--wa-color-warning-on-quiet);\n }\n\n .wa-danger,\n :host([variant='danger']) {\n --wa-color-fill-loud: var(--wa-color-danger-fill-loud);\n --wa-color-fill-normal: var(--wa-color-danger-fill-normal);\n --wa-color-fill-quiet: var(--wa-color-danger-fill-quiet);\n --wa-color-border-loud: var(--wa-color-danger-border-loud);\n --wa-color-border-normal: var(--wa-color-danger-border-normal);\n --wa-color-border-quiet: var(--wa-color-danger-border-quiet);\n --wa-color-on-loud: var(--wa-color-danger-on-loud);\n --wa-color-on-normal: var(--wa-color-danger-on-normal);\n --wa-color-on-quiet: var(--wa-color-danger-on-quiet);\n }\n}\n",mp=class{constructor(e,...t){this.slotNames=[],this.handleSlotChange=n=>{const r=n.target;(this.slotNames.includes("[default]")&&!r.name||r.name&&this.slotNames.includes(r.name))&&this.host.requestUpdate()},(this.host=e).addController(this),this.slotNames=t}hasDefaultSlot(){return[...this.host.childNodes].some(e=>{if(e.nodeType===Node.TEXT_NODE&&""!==e.textContent.trim())return!0;if(e.nodeType===Node.ELEMENT_NODE){const t=e;if("wa-visually-hidden"===t.tagName.toLowerCase())return!1;if(!t.hasAttribute("slot"))return!0}return!1})}hasNamedSlot(e){return null!==this.host.querySelector(`:scope > [slot="${e}"]`)}test(e){return"[default]"===e?this.hasDefaultSlot():this.hasNamedSlot(e)}hostConnected(){this.host.shadowRoot.addEventListener("slotchange",this.handleSlotChange)}hostDisconnected(){this.host.shadowRoot.removeEventListener("slotchange",this.handleSlotChange)}},ad="@layer wa-utilities {\n :host([size='small']),\n .wa-size-s {\n font-size: var(--wa-font-size-s);\n }\n\n :host([size='medium']),\n .wa-size-m {\n font-size: var(--wa-font-size-m);\n }\n\n :host([size='large']),\n .wa-size-l {\n font-size: var(--wa-font-size-l);\n }\n}\n";function Ir(e,t){const n={waitUntilFirstUpdate:!1,...t};return(r,i)=>{const{update:o}=r,a=Array.isArray(e)?e:[e];r.update=function(u){a.forEach(f=>{const y=f;if(u.has(y)){const w=u.get(y),D=this[y];w!==D&&(!n.waitUntilFirstUpdate||this.hasUpdated)&&this[i](w,D)}}),o.call(this,u)}}}const QE=e=>(...t)=>({_$litDirective$:e,values:t});class JE{constructor(t){}get _$AU(){return this._$AM._$AU}_$AT(t,n,r){this._$Ct=t,this._$AM=n,this._$Ci=r}_$AS(t,n){return this.update(t,n)}update(t,n){return this.render(...n)}}const Ko=QE(class extends JE{constructor(e){if(super(e),1!==e.type||"class"!==e.name||e.strings?.length>2)throw Error("`classMap()` can only be used in the `class` attribute and must be the only part in the attribute.")}render(e){return" "+Object.keys(e).filter(t=>e[t]).join(" ")+" "}update(e,[t]){if(void 0===this.st){this.st=new Set,void 0!==e.strings&&(this.nt=new Set(e.strings.join(" ").split(/\s/).filter(r=>""!==r)));for(const r in t)t[r]&&!this.nt?.has(r)&&this.st.add(r);return this.render(t)}const n=e.element.classList;for(const r of this.st)r in t||(n.remove(r),this.st.delete(r));for(const r in t){const i=!!t[r];i===this.st.has(r)||this.nt?.has(r)||(i?(n.add(r),this.st.add(r)):(n.remove(r),this.st.delete(r)))}return Vi}}),on=e=>e??hn,eD=Symbol.for(""),fG=e=>{if(e?.r===eD)return e?._$litStatic$},mN=(e,...t)=>({_$litStatic$:t.reduce((n,r,i)=>n+(o=>{if(void 0!==o._$litStatic$)return o._$litStatic$;throw Error(`Value passed to 'literal' function must be a 'literal' result: ${o}. Use 'unsafeStatic' to pass non-literal values, but\n take care to ensure page security.`)})(r)+e[i+1],e[0]),r:eD}),yN=new Map,tD=e=>(t,...n)=>{const r=n.length;let i,o;const a=[],u=[];let f,y=0,w=!1;for(;y{this.hasAttribute(t)&&e.setAttribute(t,this.getAttribute(t))}),e}handleClick(){if(!this.getForm())return;const t=this.constructLightDOMButton();this.parentElement?.append(t),t.click(),t.remove()}handleInvalid(){this.dispatchEvent(new gN)}handleLabelSlotChange(){const e=this.labelSlot.assignedNodes({flatten:!0});let t=!1,n=!1,r=!1,i=!1;[...e].forEach(o=>{o.nodeType===Node.ELEMENT_NODE?"wa-icon"===o.localName?(n=!0,t||(t=void 0!==o.label)):i=!0:o.nodeType===Node.TEXT_NODE&&(o.textContent?.trim()||"").length>0&&(r=!0)}),this.isIconButton=n&&!r&&!i,this.isIconButton&&!t&&console.warn('Icon buttons must have a label for screen readers. Add to remove this warning.',this)}isButton(){return!this.href}isLink(){return!!this.href}handleDisabledChange(){this.updateValidity()}setValue(...e){}click(){this.button.click()}focus(e){this.button.focus(e)}blur(){this.button.blur()}render(){const e=this.isLink(),t=e?mN`a`:mN`button`;return nD` + <${t} + part="base" + class=${Ko({button:!0,caret:this.withCaret,disabled:this.disabled,loading:this.loading,rtl:"rtl"===this.localize.dir(),"has-label":this.hasSlotController.test("[default]"),"has-start":this.hasSlotController.test("start"),"has-end":this.hasSlotController.test("end"),"is-icon-button":this.isIconButton})} + ?disabled=${on(e?void 0:this.disabled)} + type=${on(e?void 0:this.type)} + title=${this.title} + name=${on(e?void 0:this.name)} + value=${on(e?void 0:this.value)} + href=${on(e?this.href:void 0)} + target=${on(e?this.target:void 0)} + download=${on(e?this.download:void 0)} + rel=${on(e&&this.rel?this.rel:void 0)} + role=${on(e?void 0:"button")} + aria-disabled=${this.disabled?"true":"false"} + tabindex=${this.disabled?"-1":"0"} + @invalid=${this.isButton()?this.handleInvalid:null} + @click=${this.handleClick} + > + + + + ${this.withCaret?nD` + + `:""} + ${this.loading?nD``:""} + + `}};wt.shadowRootOptions={...Gr.shadowRootOptions,delegatesFocus:!0},wt.css=["@layer wa-component {\n :host {\n display: inline-block;\n\n /* Workaround because Chrome doesn't like :host(:has()) below\n * https://issues.chromium.org/issues/40062355\n * Firefox doesn't like this nested rule, so both are needed */\n &:has(wa-badge) {\n position: relative;\n }\n }\n\n /* Apply relative positioning only when needed to position wa-badge\n * This avoids creating a new stacking context for every button */\n :host(:has(wa-badge)) {\n position: relative;\n }\n}\n\n.button {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n text-decoration: none;\n user-select: none;\n -webkit-user-select: none;\n white-space: nowrap;\n vertical-align: middle;\n transition-property: background, border, box-shadow, color;\n transition-duration: var(--wa-transition-fast);\n transition-timing-function: var(--wa-transition-easing);\n cursor: pointer;\n padding: 0 var(--wa-form-control-padding-inline);\n font-family: inherit;\n font-size: inherit;\n font-weight: var(--wa-font-weight-action);\n line-height: calc(var(--wa-form-control-height) - var(--border-width) * 2);\n height: var(--wa-form-control-height);\n width: 100%;\n\n background-color: var(--wa-color-fill-loud, var(--wa-color-neutral-fill-loud));\n border-color: transparent;\n color: var(--wa-color-on-loud, var(--wa-color-neutral-on-loud));\n border-radius: var(--wa-form-control-border-radius);\n border-style: var(--wa-border-style);\n border-width: var(--wa-border-width-s);\n}\n\n/* Appearance modifiers */\n:host([appearance='plain']) {\n .button {\n color: var(--wa-color-on-quiet, var(--wa-color-neutral-on-quiet));\n background-color: transparent;\n border-color: transparent;\n }\n @media (hover: hover) {\n .button:not(.disabled):not(.loading):hover {\n color: var(--wa-color-on-quiet, var(--wa-color-neutral-on-quiet));\n background-color: var(--wa-color-fill-quiet, var(--wa-color-neutral-fill-quiet));\n }\n }\n .button:not(.disabled):not(.loading):active {\n color: var(--wa-color-on-quiet, var(--wa-color-neutral-on-quiet));\n background-color: color-mix(\n in oklab,\n var(--wa-color-fill-quiet, var(--wa-color-neutral-fill-quiet)),\n var(--wa-color-mix-active)\n );\n }\n}\n\n:host([appearance='outlined']) {\n .button {\n color: var(--wa-color-on-quiet, var(--wa-color-neutral-on-quiet));\n background-color: transparent;\n border-color: var(--wa-color-border-loud, var(--wa-color-neutral-border-loud));\n }\n @media (hover: hover) {\n .button:not(.disabled):not(.loading):hover {\n color: var(--wa-color-on-quiet, var(--wa-color-neutral-on-quiet));\n background-color: var(--wa-color-fill-quiet, var(--wa-color-neutral-fill-quiet));\n }\n }\n .button:not(.disabled):not(.loading):active {\n color: var(--wa-color-on-quiet, var(--wa-color-neutral-on-quiet));\n background-color: color-mix(\n in oklab,\n var(--wa-color-fill-quiet, var(--wa-color-neutral-fill-quiet)),\n var(--wa-color-mix-active)\n );\n }\n}\n\n:host([appearance='filled']) {\n .button {\n color: var(--wa-color-on-normal, var(--wa-color-neutral-on-normal));\n background-color: var(--wa-color-fill-normal, var(--wa-color-neutral-fill-normal));\n border-color: transparent;\n }\n @media (hover: hover) {\n .button:not(.disabled):not(.loading):hover {\n color: var(--wa-color-on-normal, var(--wa-color-neutral-on-normal));\n background-color: color-mix(\n in oklab,\n var(--wa-color-fill-normal, var(--wa-color-neutral-fill-normal)),\n var(--wa-color-mix-hover)\n );\n }\n }\n .button:not(.disabled):not(.loading):active {\n color: var(--wa-color-on-normal, var(--wa-color-neutral-on-normal));\n background-color: color-mix(\n in oklab,\n var(--wa-color-fill-normal, var(--wa-color-neutral-fill-normal)),\n var(--wa-color-mix-active)\n );\n }\n}\n\n:host([appearance='filled-outlined']) {\n .button {\n color: var(--wa-color-on-normal, var(--wa-color-neutral-on-normal));\n background-color: var(--wa-color-fill-normal, var(--wa-color-neutral-fill-normal));\n border-color: var(--wa-color-border-normal, var(--wa-color-neutral-border-normal));\n }\n @media (hover: hover) {\n .button:not(.disabled):not(.loading):hover {\n color: var(--wa-color-on-normal, var(--wa-color-neutral-on-normal));\n background-color: color-mix(\n in oklab,\n var(--wa-color-fill-normal, var(--wa-color-neutral-fill-normal)),\n var(--wa-color-mix-hover)\n );\n }\n }\n .button:not(.disabled):not(.loading):active {\n color: var(--wa-color-on-normal, var(--wa-color-neutral-on-normal));\n background-color: color-mix(\n in oklab,\n var(--wa-color-fill-normal, var(--wa-color-neutral-fill-normal)),\n var(--wa-color-mix-active)\n );\n }\n}\n\n:host([appearance='accent']) {\n .button {\n color: var(--wa-color-on-loud, var(--wa-color-neutral-on-loud));\n background-color: var(--wa-color-fill-loud, var(--wa-color-neutral-fill-loud));\n border-color: transparent;\n }\n @media (hover: hover) {\n .button:not(.disabled):not(.loading):hover {\n background-color: color-mix(\n in oklab,\n var(--wa-color-fill-loud, var(--wa-color-neutral-fill-loud)),\n var(--wa-color-mix-hover)\n );\n }\n }\n .button:not(.disabled):not(.loading):active {\n background-color: color-mix(\n in oklab,\n var(--wa-color-fill-loud, var(--wa-color-neutral-fill-loud)),\n var(--wa-color-mix-active)\n );\n }\n}\n\n/* Focus states */\n.button:focus {\n outline: none;\n}\n\n.button:focus-visible {\n outline: var(--wa-focus-ring);\n outline-offset: var(--wa-focus-ring-offset);\n}\n\n/* Disabled state */\n.button.disabled {\n opacity: 0.5;\n cursor: not-allowed;\n}\n\n/* When disabled, prevent mouse events from bubbling up from children */\n.button.disabled * {\n pointer-events: none;\n}\n\n/* Keep it last so Safari doesn't stop parsing this block */\n.button::-moz-focus-inner {\n border: 0;\n}\n\n/* Icon buttons */\n.button.is-icon-button {\n outline-offset: 2px;\n width: var(--wa-form-control-height);\n aspect-ratio: 1;\n}\n\n.button.is-icon-button:has(wa-icon) {\n width: auto;\n}\n\n/* Pill modifier */\n:host([pill]) .button {\n border-radius: var(--wa-border-radius-pill);\n}\n\n/*\n * Label\n */\n\n.start,\n.end {\n flex: 0 0 auto;\n display: flex;\n align-items: center;\n pointer-events: none;\n}\n\n.label {\n display: inline-block;\n}\n\n.is-icon-button .label {\n display: flex;\n}\n\n.label::slotted(wa-icon) {\n align-self: center;\n}\n\n/*\n * Caret modifier\n */\n\nwa-icon[part='caret'] {\n display: flex;\n align-self: center;\n align-items: center;\n\n &::part(svg) {\n width: 0.875em;\n height: 0.875em;\n }\n\n .button:has(&) .end {\n display: none;\n }\n}\n\n/*\n * Loading modifier\n */\n\n.loading {\n position: relative;\n cursor: wait;\n\n .start,\n .label,\n .end,\n .caret {\n visibility: hidden;\n }\n\n wa-spinner {\n --indicator-color: currentColor;\n --track-color: color-mix(in oklab, currentColor, transparent 90%);\n\n position: absolute;\n font-size: 1em;\n height: 1em;\n width: 1em;\n top: calc(50% - 0.5em);\n left: calc(50% - 0.5em);\n }\n}\n\n/*\n * Badges\n */\n\n.button ::slotted(wa-badge) {\n border-color: var(--wa-color-surface-default);\n position: absolute;\n inset-block-start: 0;\n inset-inline-end: 0;\n translate: 50% -50%;\n pointer-events: none;\n}\n\n:host(:dir(rtl)) ::slotted(wa-badge) {\n translate: -50% -50%;\n}\n\n/*\n* Button spacing\n*/\n\nslot[name='start']::slotted(*) {\n margin-inline-end: 0.75em;\n}\n\nslot[name='end']::slotted(*),\n.button:not(.visually-hidden-label) [part='caret'] {\n margin-inline-start: 0.75em;\n}\n\n/*\n * Button group border radius modifications\n */\n\n/* Remove border radius from all grouped buttons by default */\n:host(.wa-button-group__button) .button {\n border-radius: 0;\n}\n\n/* Horizontal orientation */\n:host(.wa-button-group__horizontal.wa-button-group__button-first) .button {\n border-start-start-radius: var(--wa-form-control-border-radius);\n border-end-start-radius: var(--wa-form-control-border-radius);\n}\n\n:host(.wa-button-group__horizontal.wa-button-group__button-last) .button {\n border-start-end-radius: var(--wa-form-control-border-radius);\n border-end-end-radius: var(--wa-form-control-border-radius);\n}\n\n/* Vertical orientation */\n:host(.wa-button-group__vertical) {\n flex: 1 1 auto;\n}\n\n:host(.wa-button-group__vertical) .button {\n width: 100%;\n justify-content: start;\n}\n\n:host(.wa-button-group__vertical.wa-button-group__button-first) .button {\n border-start-start-radius: var(--wa-form-control-border-radius);\n border-start-end-radius: var(--wa-form-control-border-radius);\n}\n\n:host(.wa-button-group__vertical.wa-button-group__button-last) .button {\n border-end-start-radius: var(--wa-form-control-border-radius);\n border-end-end-radius: var(--wa-form-control-border-radius);\n}\n\n/* Handle pill modifier for button groups */\n:host([pill].wa-button-group__horizontal.wa-button-group__button-first) .button {\n border-start-start-radius: var(--wa-border-radius-pill);\n border-end-start-radius: var(--wa-border-radius-pill);\n}\n\n:host([pill].wa-button-group__horizontal.wa-button-group__button-last) .button {\n border-start-end-radius: var(--wa-border-radius-pill);\n border-end-end-radius: var(--wa-border-radius-pill);\n}\n\n:host([pill].wa-button-group__vertical.wa-button-group__button-first) .button {\n border-start-start-radius: var(--wa-border-radius-pill);\n border-start-end-radius: var(--wa-border-radius-pill);\n}\n\n:host([pill].wa-button-group__vertical.wa-button-group__button-last) .button {\n border-end-start-radius: var(--wa-border-radius-pill);\n border-end-end-radius: var(--wa-border-radius-pill);\n}\n",KE,ad],B([$n(".button")],wt.prototype,"button",2),B([$n("slot:not([name])")],wt.prototype,"labelSlot",2),B([$i()],wt.prototype,"invalid",2),B([$i()],wt.prototype,"isIconButton",2),B([W()],wt.prototype,"title",2),B([W({reflect:!0})],wt.prototype,"variant",2),B([W({reflect:!0})],wt.prototype,"appearance",2),B([W({reflect:!0})],wt.prototype,"size",2),B([W({attribute:"with-caret",type:Boolean,reflect:!0})],wt.prototype,"withCaret",2),B([W({type:Boolean})],wt.prototype,"disabled",2),B([W({type:Boolean,reflect:!0})],wt.prototype,"loading",2),B([W({type:Boolean,reflect:!0})],wt.prototype,"pill",2),B([W()],wt.prototype,"type",2),B([W({reflect:!0})],wt.prototype,"name",2),B([W({reflect:!0})],wt.prototype,"value",2),B([W({reflect:!0})],wt.prototype,"href",2),B([W()],wt.prototype,"target",2),B([W()],wt.prototype,"rel",2),B([W()],wt.prototype,"download",2),B([W({reflect:!0})],wt.prototype,"form",2),B([W({attribute:"formaction"})],wt.prototype,"formAction",2),B([W({attribute:"formenctype"})],wt.prototype,"formEnctype",2),B([W({attribute:"formmethod"})],wt.prototype,"formMethod",2),B([W({attribute:"formnovalidate",type:Boolean})],wt.prototype,"formNoValidate",2),B([W({attribute:"formtarget"})],wt.prototype,"formTarget",2),B([Ir("disabled",{waitUntilFirstUpdate:!0})],wt.prototype,"handleDisabledChange",1),wt=B([ui("wa-button")],wt);var vN=(()=>{let e=class extends di{constructor(){super(...arguments),this.localize=new za(this)}render(){return At` + + + + + `}};return e.css=":host {\n --track-width: 2px;\n --track-color: var(--wa-color-neutral-fill-normal);\n --indicator-color: var(--wa-color-brand-fill-loud);\n --speed: 2s;\n\n /* Resizing a spinner element using anything but font-size will break the animation because the animation uses em units.\n Therefore, if a spinner is used in a flex container without `flex: none` applied, the spinner can grow/shrink and\n break the animation. The use of `flex: none` on the host element prevents this by always having the spinner sized\n according to its actual dimensions.\n */\n flex: none;\n display: inline-flex;\n width: 1em;\n height: 1em;\n}\n\nsvg {\n width: 100%;\n height: 100%;\n aspect-ratio: 1;\n animation: spin var(--speed) linear infinite;\n}\n\n.track {\n stroke: var(--track-color);\n}\n\n.indicator {\n stroke: var(--indicator-color);\n stroke-dasharray: 75, 100;\n stroke-dashoffset: -5;\n animation: dash 1.5s ease-in-out infinite;\n stroke-linecap: round;\n}\n\n@keyframes spin {\n 0% {\n transform: rotate(0deg);\n }\n 100% {\n transform: rotate(360deg);\n }\n}\n\n@keyframes dash {\n 0% {\n stroke-dasharray: 1, 150;\n stroke-dashoffset: 0;\n }\n 50% {\n stroke-dasharray: 90, 150;\n stroke-dashoffset: -35;\n }\n 100% {\n stroke-dasharray: 90, 150;\n stroke-dashoffset: -124;\n }\n}\n",e})();vN=B([ui("wa-spinner")],vN);var mG=class extends Event{constructor(){super("wa-load",{bubbles:!0,cancelable:!1,composed:!0})}};const _G={};var rD,CG=class extends Event{constructor(){super("wa-error",{bubbles:!0,cancelable:!1,composed:!0})}},yp=Symbol(),kv=Symbol(),iD=new Map,hi=(()=>{let e=class extends di{constructor(){var t;super(...arguments),t=this,this.svg=null,this.autoWidth=!1,this.swapOpacity=!1,this.label="",this.library="default",this.resolveIcon=function(){var n=(0,oe.A)(function*(r,i){let o;if(i?.spriteSheet){t.hasUpdated||(yield t.updateComplete),t.svg=At` + + `,yield t.updateComplete;const a=t.shadowRoot.querySelector("[part='svg']");return"function"==typeof i.mutator&&i.mutator(a,t),t.svg}try{if(o=yield fetch(r,{mode:"cors"}),!o.ok)return 410===o.status?yp:kv}catch{return kv}try{const a=document.createElement("div");a.innerHTML=yield o.text();const u=a.firstElementChild;if("svg"!==u?.tagName?.toLowerCase())return yp;rD||(rD=new DOMParser);const y=rD.parseFromString(u.outerHTML,"text/html").body.querySelector("svg");return y?(y.part.add("svg"),document.adoptNode(y)):yp}catch{return yp}});return function(r,i){return n.apply(this,arguments)}}()}connectedCallback(){super.connectedCallback(),function BW(e){lp.push(e)}(this)}firstUpdated(t){super.firstUpdated(t),this.setIcon()}disconnectedCallback(){super.disconnectedCallback(),function VW(e){lp=lp.filter(t=>t!==e)}(this)}getIconSource(){const t=$E(this.library);return this.name&&t?{url:t.resolver(this.name,this.family||"classic",this.variant,this.autoWidth),fromLibrary:!0}:{url:this.src,fromLibrary:!1}}handleLabelChange(){"string"==typeof this.label&&this.label.length>0?(this.setAttribute("role","img"),this.setAttribute("aria-label",this.label),this.removeAttribute("aria-hidden")):(this.removeAttribute("role"),this.removeAttribute("aria-label"),this.setAttribute("aria-hidden","true"))}setIcon(){var t=this;return(0,oe.A)(function*(){const{url:n,fromLibrary:r}=t.getIconSource(),i=r?$E(t.library):void 0;if(!n)return void(t.svg=null);let o=iD.get(n);o||(o=t.resolveIcon(n,i),iD.set(n,o));const a=yield o;if(a===kv&&iD.delete(n),n===t.getIconSource().url){if((e=>void 0!==e?._$litType$)(a))return void(t.svg=a);switch(a){case kv:case yp:t.svg=null,t.dispatchEvent(new CG);break;default:t.svg=a.cloneNode(!0),i?.mutator?.(t.svg,t),t.dispatchEvent(new mG)}}})()}updated(t){super.updated(t);const n=$E(this.library),r=this.shadowRoot?.querySelector("svg");r&&n?.mutator?.(r,this)}render(){return this.hasUpdated?this.svg:At``}};return e.css=":host {\n --primary-color: currentColor;\n --primary-opacity: 1;\n --secondary-color: currentColor;\n --secondary-opacity: 0.4;\n\n box-sizing: content-box;\n display: inline-flex;\n align-items: center;\n justify-content: center;\n vertical-align: -0.125em;\n}\n\n/* Standard */\n:host(:not([auto-width])) {\n width: 1.25em;\n height: 1em;\n}\n\n/* Auto-width */\n:host([auto-width]) {\n width: auto;\n height: 1em;\n}\n\nsvg {\n height: 1em;\n fill: currentColor;\n overflow: visible;\n\n /* Duotone colors with path-specific opacity fallback */\n path[data-duotone-primary] {\n color: var(--primary-color);\n opacity: var(--path-opacity, var(--primary-opacity));\n }\n\n path[data-duotone-secondary] {\n color: var(--secondary-color);\n opacity: var(--path-opacity, var(--secondary-opacity));\n }\n}\n",e})();B([$i()],hi.prototype,"svg",2),B([W({reflect:!0})],hi.prototype,"name",2),B([W({reflect:!0})],hi.prototype,"family",2),B([W({reflect:!0})],hi.prototype,"variant",2),B([W({attribute:"auto-width",type:Boolean,reflect:!0})],hi.prototype,"autoWidth",2),B([W({attribute:"swap-opacity",type:Boolean,reflect:!0})],hi.prototype,"swapOpacity",2),B([W()],hi.prototype,"src",2),B([W()],hi.prototype,"label",2),B([W({reflect:!0})],hi.prototype,"library",2),B([Ir("label")],hi.prototype,"handleLabelChange",1),B([Ir(["family","name","library","variant","src","autoWidth","swapOpacity"])],hi.prototype,"setIcon",1),hi=B([ui("wa-icon")],hi);var nc=(()=>{let e=class extends di{constructor(){super(...arguments),this.hasSlotController=new mp(this,"footer","header","media"),this.appearance="outlined",this.withHeader=!1,this.withMedia=!1,this.withFooter=!1,this.orientation="vertical"}updated(){!this.withHeader&&this.hasSlotController.test("header")&&(this.withHeader=!0),!this.withMedia&&this.hasSlotController.test("media")&&(this.withMedia=!0),!this.withFooter&&this.hasSlotController.test("footer")&&(this.withFooter=!0)}render(){return"horizontal"===this.orientation?At` + + + + `:At` + + + ${this.hasSlotController.test("header-actions")?At`
+ + +
`:At`
+ +
`} + + + ${this.hasSlotController.test("footer-actions")?At`
+ + +
`:At`
+ +
`} + `}};return e.css=[ad,":host {\n --spacing: var(--wa-space-l);\n\n /* Internal calculated properties */\n --inner-border-radius: calc(var(--wa-panel-border-radius) - var(--wa-panel-border-width));\n\n display: flex;\n flex-direction: column;\n background-color: var(--wa-color-surface-default);\n border-color: var(--wa-color-surface-border);\n border-radius: var(--wa-panel-border-radius);\n border-style: var(--wa-panel-border-style);\n box-shadow: var(--wa-shadow-s);\n border-width: var(--wa-panel-border-width);\n color: var(--wa-color-text-normal);\n}\n\n/* Appearance modifiers */\n:host([appearance~='plain']) {\n background-color: transparent;\n border-color: transparent;\n box-shadow: none;\n}\n\n:host([appearance~='outlined']) {\n background-color: var(--wa-color-surface-default);\n border-color: var(--wa-color-surface-border);\n}\n\n:host([appearance~='filled']) {\n background-color: var(--wa-color-neutral-fill-quiet);\n border-color: transparent;\n}\n\n:host([appearance~='filled'][appearance~='outlined']) {\n border-color: var(--wa-color-neutral-border-quiet);\n}\n\n:host([appearance~='accent']) {\n color: var(--wa-color-neutral-on-loud);\n background-color: var(--wa-color-neutral-fill-loud);\n border-color: transparent;\n}\n\n/* Take care of top and bottom radii */\n.media,\n:host(:not([with-media])) .header,\n:host(:not([with-media], [with-header])) .body {\n border-start-start-radius: var(--inner-border-radius);\n border-start-end-radius: var(--inner-border-radius);\n}\n\n:host(:not([with-footer])) .body,\n.footer {\n border-end-start-radius: var(--inner-border-radius);\n border-end-end-radius: var(--inner-border-radius);\n}\n\n.media {\n display: flex;\n overflow: hidden;\n\n &::slotted(*) {\n display: block;\n width: 100%;\n border-radius: 0 !important;\n }\n}\n\n/* Round all corners for plain appearance */\n:host([appearance='plain']) .media {\n border-radius: var(--inner-border-radius);\n\n &::slotted(*) {\n border-radius: inherit !important;\n }\n}\n\n.header {\n display: block;\n border-block-end-style: inherit;\n border-block-end-color: var(--wa-color-surface-border);\n border-block-end-width: var(--wa-panel-border-width);\n padding: calc(var(--spacing) / 2) var(--spacing);\n}\n\n.body {\n display: block;\n padding: var(--spacing);\n}\n\n.footer {\n display: block;\n border-block-start-style: inherit;\n border-block-start-color: var(--wa-color-surface-border);\n border-block-start-width: var(--wa-panel-border-width);\n padding: var(--spacing);\n}\n\n/* Push slots to sides when the action slots renders */\n.has-actions {\n display: flex;\n align-items: center;\n justify-content: space-between;\n}\n\n:host(:not([with-header])) .header,\n:host(:not([with-footer])) .footer,\n:host(:not([with-media])) .media {\n display: none;\n}\n\n/* Orientation Styles */\n:host([orientation='horizontal']) {\n flex-direction: row;\n\n .media {\n border-start-start-radius: var(--inner-border-radius);\n border-end-start-radius: var(--inner-border-radius);\n border-start-end-radius: 0;\n\n &::slotted(*) {\n block-size: 100%;\n inline-size: 100%;\n object-fit: cover;\n }\n }\n}\n\n:host([orientation='horizontal']) ::slotted([slot='body']) {\n display: block;\n height: 100%;\n margin: 0;\n}\n\n:host([orientation='horizontal']) ::slotted([slot='actions']) {\n display: flex;\n align-items: center;\n padding: var(--spacing);\n}\n"],e})();B([W({reflect:!0})],nc.prototype,"appearance",2),B([W({attribute:"with-header",type:Boolean,reflect:!0})],nc.prototype,"withHeader",2),B([W({attribute:"with-media",type:Boolean,reflect:!0})],nc.prototype,"withMedia",2),B([W({attribute:"with-footer",type:Boolean,reflect:!0})],nc.prototype,"withFooter",2),B([W({reflect:!0})],nc.prototype,"orientation",2),nc=B([ui("wa-card")],nc);const xG=(e,t)=>t.name;function SG(e,t){1&e&&ai(0,"wa-spinner",9)}function MG(e,t){1&e&&(ai(0,"wa-icon",10),te(1," Uninstall "))}function TG(e,t){if(1&e){const n=Zo();He(0,"wa-button",8),ar("click",function(){Dt(n);const i=Pe().$implicit;return Gt(Pe().uninstallMiner(i.name))}),or(1,SG,1,0,"wa-spinner",9)(2,MG,2,0),$e()}if(2&e){const n=Pe().$implicit,r=Pe();li("disabled",r.actionInProgress()==="uninstall-"+n.name),he(),sr(r.actionInProgress()==="uninstall-"+n.name?1:2)}}function IG(e,t){1&e&&ai(0,"wa-spinner",9)}function AG(e,t){1&e&&(ai(0,"wa-icon",12),te(1," Install "))}function kG(e,t){if(1&e){const n=Zo();He(0,"wa-button",11),ar("click",function(){Dt(n);const i=Pe().$implicit;return Gt(Pe().installMiner(i.name))}),or(1,IG,1,0,"wa-spinner",9)(2,AG,2,0),$e()}if(2&e){const n=Pe().$implicit,r=Pe();li("disabled",r.actionInProgress()==="install-"+n.name),he(),sr(r.actionInProgress()==="install-"+n.name?1:2)}}function OG(e,t){if(1&e&&(He(0,"div",4)(1,"span"),te(2),$e(),or(3,TG,3,2,"wa-button",6)(4,kG,3,2,"wa-button",7),$e()),2&e){const n=t.$implicit;he(2),Pt(n.name),he(),sr(n.is_installed?3:4)}}function RG(e,t){1&e&&(He(0,"div",4)(1,"span"),te(2,"Could not load available miners."),$e()())}function NG(e,t){if(1&e&&(He(0,"wa-card",5)(1,"div",13),ai(2,"wa-icon",14),te(3," An Error Occurred "),$e(),He(4,"p"),te(5),$e()()),2&e){const n=Pe();he(5),Pt(n.error())}}let _N=(()=>{class e{minerService=Y(Zl);state=this.minerService.state;actionInProgress=tn(null);error=tn(null);installMiner(n){this.actionInProgress.set(`install-${n}`),this.error.set(null),this.minerService.installMiner(n).subscribe({next:()=>{this.actionInProgress.set(null)},error:r=>{this.handleError(r,`Failed to install ${n}`)}})}uninstallMiner(n){this.actionInProgress.set(`uninstall-${n}`),this.error.set(null),this.minerService.uninstallMiner(n).subscribe({next:()=>{this.actionInProgress.set(null)},error:r=>{this.handleError(r,`Failed to uninstall ${n}`)}})}handleError(n,r){console.error(n),this.actionInProgress.set(null),this.error.set(n.error&&n.error.error?`${r}: ${n.error.error}`:"string"==typeof n.error&&n.error.length<200?`${r}: ${n.error}`:`${r}. Please check the console for details.`)}static \u0275fac=function(r){return new(r||e)};static \u0275cmp=qo({type:e,selectors:[["snider-mining-setup-wizard"]],decls:14,vars:2,consts:[[1,"setup-wizard"],[1,"header-title"],["name","cpu",2,"font-size","1.5rem"],[1,"miner-list"],[1,"miner-item"],[1,"card-error"],["variant","danger","size","small",3,"disabled"],["variant","success","size","small",3,"disabled"],["variant","danger","size","small",3,"click","disabled"],[1,"button-spinner"],["name","trash","slot","prefix"],["variant","success","size","small",3,"click","disabled"],["name","download","slot","prefix"],["slot","header"],["name","exclamation-triangle",2,"font-size","1.5rem"]],template:function(r,i){1&r&&(He(0,"div",0)(1,"div",1),ai(2,"wa-icon",2),He(3,"span"),te(4,"Setup Required"),$e()(),He(5,"p"),te(6,"To begin, please install a miner from the list below."),$e(),He(7,"h4"),te(8,"Available Miners"),$e(),He(9,"div",3),La(10,OG,5,2,"div",4,xG,!1,RG,3,0,"div",4),$e(),or(13,NG,6,1,"wa-card",5),$e()),2&r&&(he(10),Fa(i.state().manageableMiners),he(3),sr(i.error()?13:-1))},dependencies:[ql],styles:["[_nghost-%COMP%]{display:block}.setup-wizard[_ngcontent-%COMP%]{display:flex;flex-direction:column;gap:1rem}.header-title[_ngcontent-%COMP%]{display:flex;align-items:center;gap:.5rem;font-size:1.5rem;font-weight:600}.miner-list[_ngcontent-%COMP%]{display:flex;flex-direction:column;gap:.5rem;border:1px solid var(--wa-color-neutral-300);padding:1rem;border-radius:var(--wa-border-radius-medium)}.miner-item[_ngcontent-%COMP%]{display:flex;justify-content:space-between;align-items:center}.button-spinner[_ngcontent-%COMP%]{font-size:1.2rem}.card-error[_ngcontent-%COMP%]{--wa-card-background-color: var(--wa-color-danger-50);--wa-card-border-color: var(--wa-color-danger-300);color:var(--wa-color-danger-800)}.card-error[_ngcontent-%COMP%] [slot=header][_ngcontent-%COMP%]{display:flex;align-items:center;gap:.5rem;font-weight:600}"]})}return e})();const mr=new ee(""),Ga=new ee("");function kN(e){return null!=e}function ON(e){return Ff(e)?lr(e):e}function RN(e){let t={};return e.forEach(n=>{t=null!=n?{...t,...n}:t}),0===Object.keys(t).length?null:t}function NN(e,t){return t.map(n=>n(e))}function PN(e){return e.map(t=>function $G(e){return!e.validate}(t)?t:n=>t.validate(n))}function lD(e){return null!=e?function LN(e){if(!e)return null;const t=e.filter(kN);return 0==t.length?null:function(n){return RN(NN(n,t))}}(PN(e)):null}function cD(e){return null!=e?function FN(e){if(!e)return null;const t=e.filter(kN);return 0==t.length?null:function(n){return DE(NN(n,t).map(ON)).pipe(tt(RN))}}(PN(e)):null}function BN(e,t){return null===e?[t]:Array.isArray(e)?[...e,t]:[e,t]}function uD(e){return e?Array.isArray(e)?e:[e]:[]}function Rv(e,t){return Array.isArray(e)?e.includes(t):e===t}function jN(e,t){const n=uD(t);return uD(e).forEach(i=>{Rv(n,i)||n.push(i)}),n}function HN(e,t){return uD(t).filter(n=>!Rv(e,n))}class UN{get value(){return this.control?this.control.value:null}get valid(){return this.control?this.control.valid:null}get invalid(){return this.control?this.control.invalid:null}get pending(){return this.control?this.control.pending:null}get disabled(){return this.control?this.control.disabled:null}get enabled(){return this.control?this.control.enabled:null}get errors(){return this.control?this.control.errors:null}get pristine(){return this.control?this.control.pristine:null}get dirty(){return this.control?this.control.dirty:null}get touched(){return this.control?this.control.touched:null}get status(){return this.control?this.control.status:null}get untouched(){return this.control?this.control.untouched:null}get statusChanges(){return this.control?this.control.statusChanges:null}get valueChanges(){return this.control?this.control.valueChanges:null}get path(){return null}_composedValidatorFn;_composedAsyncValidatorFn;_rawValidators=[];_rawAsyncValidators=[];_setValidators(t){this._rawValidators=t||[],this._composedValidatorFn=lD(this._rawValidators)}_setAsyncValidators(t){this._rawAsyncValidators=t||[],this._composedAsyncValidatorFn=cD(this._rawAsyncValidators)}get validator(){return this._composedValidatorFn||null}get asyncValidator(){return this._composedAsyncValidatorFn||null}_onDestroyCallbacks=[];_registerOnDestroy(t){this._onDestroyCallbacks.push(t)}_invokeOnDestroyCallbacks(){this._onDestroyCallbacks.forEach(t=>t()),this._onDestroyCallbacks=[]}reset(t=void 0){this.control&&this.control.reset(t)}hasError(t,n){return!!this.control&&this.control.hasError(t,n)}getError(t,n){return this.control?this.control.getError(t,n):null}}class Ar extends UN{name;get formDirective(){return null}get path(){return null}}class zN{_cd;constructor(t){this._cd=t}get isTouched(){return this._cd?.control?._touched?.(),!!this._cd?.control?.touched}get isUntouched(){return!!this._cd?.control?.untouched}get isPristine(){return this._cd?.control?._pristine?.(),!!this._cd?.control?.pristine}get isDirty(){return!!this._cd?.control?.dirty}get isValid(){return this._cd?.control?._status?.(),!!this._cd?.control?.valid}get isInvalid(){return!!this._cd?.control?.invalid}get isPending(){return!!this._cd?.control?.pending}get isSubmitted(){return this._cd?._submitted?.(),!!this._cd?.submitted}}let WN=(()=>{class e extends zN{constructor(n){super(n)}static \u0275fac=function(r){return new(r||e)(re(Ar,10))};static \u0275dir=Ne({type:e,selectors:[["","formGroupName",""],["","formArrayName",""],["","ngModelGroup",""],["","formGroup",""],["form",3,"ngNoForm",""],["","ngForm",""]],hostVars:16,hostBindings:function(r,i){2&r&&zf("ng-untouched",i.isUntouched)("ng-touched",i.isTouched)("ng-pristine",i.isPristine)("ng-dirty",i.isDirty)("ng-valid",i.isValid)("ng-invalid",i.isInvalid)("ng-pending",i.isPending)("ng-submitted",i.isSubmitted)},standalone:!1,features:[Tt]})}return e})();const vp="VALID",Pv="INVALID",ld="PENDING",bp="DISABLED";class cd{}class qN extends cd{value;source;constructor(t,n){super(),this.value=t,this.source=n}}class fD extends cd{pristine;source;constructor(t,n){super(),this.pristine=t,this.source=n}}class pD extends cd{touched;source;constructor(t,n){super(),this.touched=t,this.source=n}}class Lv extends cd{status;source;constructor(t,n){super(),this.status=t,this.source=n}}class YN extends cd{source;constructor(t){super(),this.source=t}}class gD extends cd{source;constructor(t){super(),this.source=t}}function Fv(e){return null!=e&&!Array.isArray(e)&&"object"==typeof e}class vD{_pendingDirty=!1;_hasOwnPendingAsyncValidator=null;_pendingTouched=!1;_onCollectionChange=()=>{};_updateOn;_parent=null;_asyncValidationSubscription;_composedValidatorFn;_composedAsyncValidatorFn;_rawValidators;_rawAsyncValidators;value;constructor(t,n){this._assignValidators(t),this._assignAsyncValidators(n)}get validator(){return this._composedValidatorFn}set validator(t){this._rawValidators=this._composedValidatorFn=t}get asyncValidator(){return this._composedAsyncValidatorFn}set asyncValidator(t){this._rawAsyncValidators=this._composedAsyncValidatorFn=t}get parent(){return this._parent}get status(){return Vt(this.statusReactive)}set status(t){Vt(()=>this.statusReactive.set(t))}_status=It(()=>this.statusReactive());statusReactive=tn(void 0);get valid(){return this.status===vp}get invalid(){return this.status===Pv}get pending(){return this.status==ld}get disabled(){return this.status===bp}get enabled(){return this.status!==bp}errors;get pristine(){return Vt(this.pristineReactive)}set pristine(t){Vt(()=>this.pristineReactive.set(t))}_pristine=It(()=>this.pristineReactive());pristineReactive=tn(!0);get dirty(){return!this.pristine}get touched(){return Vt(this.touchedReactive)}set touched(t){Vt(()=>this.touchedReactive.set(t))}_touched=It(()=>this.touchedReactive());touchedReactive=tn(!1);get untouched(){return!this.touched}_events=new Kn;events=this._events.asObservable();valueChanges;statusChanges;get updateOn(){return this._updateOn?this._updateOn:this.parent?this.parent.updateOn:"change"}setValidators(t){this._assignValidators(t)}setAsyncValidators(t){this._assignAsyncValidators(t)}addValidators(t){this.setValidators(jN(t,this._rawValidators))}addAsyncValidators(t){this.setAsyncValidators(jN(t,this._rawAsyncValidators))}removeValidators(t){this.setValidators(HN(t,this._rawValidators))}removeAsyncValidators(t){this.setAsyncValidators(HN(t,this._rawAsyncValidators))}hasValidator(t){return Rv(this._rawValidators,t)}hasAsyncValidator(t){return Rv(this._rawAsyncValidators,t)}clearValidators(){this.validator=null}clearAsyncValidators(){this.asyncValidator=null}markAsTouched(t={}){const n=!1===this.touched;this.touched=!0;const r=t.sourceControl??this;this._parent&&!t.onlySelf&&this._parent.markAsTouched({...t,sourceControl:r}),n&&!1!==t.emitEvent&&this._events.next(new pD(!0,r))}markAllAsDirty(t={}){this.markAsDirty({onlySelf:!0,emitEvent:t.emitEvent,sourceControl:this}),this._forEachChild(n=>n.markAllAsDirty(t))}markAllAsTouched(t={}){this.markAsTouched({onlySelf:!0,emitEvent:t.emitEvent,sourceControl:this}),this._forEachChild(n=>n.markAllAsTouched(t))}markAsUntouched(t={}){const n=!0===this.touched;this.touched=!1,this._pendingTouched=!1;const r=t.sourceControl??this;this._forEachChild(i=>{i.markAsUntouched({onlySelf:!0,emitEvent:t.emitEvent,sourceControl:r})}),this._parent&&!t.onlySelf&&this._parent._updateTouched(t,r),n&&!1!==t.emitEvent&&this._events.next(new pD(!1,r))}markAsDirty(t={}){const n=!0===this.pristine;this.pristine=!1;const r=t.sourceControl??this;this._parent&&!t.onlySelf&&this._parent.markAsDirty({...t,sourceControl:r}),n&&!1!==t.emitEvent&&this._events.next(new fD(!1,r))}markAsPristine(t={}){const n=!1===this.pristine;this.pristine=!0,this._pendingDirty=!1;const r=t.sourceControl??this;this._forEachChild(i=>{i.markAsPristine({onlySelf:!0,emitEvent:t.emitEvent})}),this._parent&&!t.onlySelf&&this._parent._updatePristine(t,r),n&&!1!==t.emitEvent&&this._events.next(new fD(!0,r))}markAsPending(t={}){this.status=ld;const n=t.sourceControl??this;!1!==t.emitEvent&&(this._events.next(new Lv(this.status,n)),this.statusChanges.emit(this.status)),this._parent&&!t.onlySelf&&this._parent.markAsPending({...t,sourceControl:n})}disable(t={}){const n=this._parentMarkedDirty(t.onlySelf);this.status=bp,this.errors=null,this._forEachChild(i=>{i.disable({...t,onlySelf:!0})}),this._updateValue();const r=t.sourceControl??this;!1!==t.emitEvent&&(this._events.next(new qN(this.value,r)),this._events.next(new Lv(this.status,r)),this.valueChanges.emit(this.value),this.statusChanges.emit(this.status)),this._updateAncestors({...t,skipPristineCheck:n},this),this._onDisabledChange.forEach(i=>i(!0))}enable(t={}){const n=this._parentMarkedDirty(t.onlySelf);this.status=vp,this._forEachChild(r=>{r.enable({...t,onlySelf:!0})}),this.updateValueAndValidity({onlySelf:!0,emitEvent:t.emitEvent}),this._updateAncestors({...t,skipPristineCheck:n},this),this._onDisabledChange.forEach(r=>r(!1))}_updateAncestors(t,n){this._parent&&!t.onlySelf&&(this._parent.updateValueAndValidity(t),t.skipPristineCheck||this._parent._updatePristine({},n),this._parent._updateTouched({},n))}setParent(t){this._parent=t}getRawValue(){return this.value}updateValueAndValidity(t={}){if(this._setInitialStatus(),this._updateValue(),this.enabled){const r=this._cancelExistingSubscription();this.errors=this._runValidator(),this.status=this._calculateStatus(),(this.status===vp||this.status===ld)&&this._runAsyncValidator(r,t.emitEvent)}const n=t.sourceControl??this;!1!==t.emitEvent&&(this._events.next(new qN(this.value,n)),this._events.next(new Lv(this.status,n)),this.valueChanges.emit(this.value),this.statusChanges.emit(this.status)),this._parent&&!t.onlySelf&&this._parent.updateValueAndValidity({...t,sourceControl:n})}_updateTreeValidity(t={emitEvent:!0}){this._forEachChild(n=>n._updateTreeValidity(t)),this.updateValueAndValidity({onlySelf:!0,emitEvent:t.emitEvent})}_setInitialStatus(){this.status=this._allControlsDisabled()?bp:vp}_runValidator(){return this.validator?this.validator(this):null}_runAsyncValidator(t,n){if(this.asyncValidator){this.status=ld,this._hasOwnPendingAsyncValidator={emitEvent:!1!==n,shouldHaveEmitted:!1!==t};const r=ON(this.asyncValidator(this));this._asyncValidationSubscription=r.subscribe(i=>{this._hasOwnPendingAsyncValidator=null,this.setErrors(i,{emitEvent:n,shouldHaveEmitted:t})})}}_cancelExistingSubscription(){if(this._asyncValidationSubscription){this._asyncValidationSubscription.unsubscribe();const t=(this._hasOwnPendingAsyncValidator?.emitEvent||this._hasOwnPendingAsyncValidator?.shouldHaveEmitted)??!1;return this._hasOwnPendingAsyncValidator=null,t}return!1}setErrors(t,n={}){this.errors=t,this._updateControlsErrors(!1!==n.emitEvent,this,n.shouldHaveEmitted)}get(t){let n=t;return null==n||(Array.isArray(n)||(n=n.split(".")),0===n.length)?null:n.reduce((r,i)=>r&&r._find(i),this)}getError(t,n){const r=n?this.get(n):this;return r&&r.errors?r.errors[t]:null}hasError(t,n){return!!this.getError(t,n)}get root(){let t=this;for(;t._parent;)t=t._parent;return t}_updateControlsErrors(t,n,r){this.status=this._calculateStatus(),t&&this.statusChanges.emit(this.status),(t||r)&&this._events.next(new Lv(this.status,n)),this._parent&&this._parent._updateControlsErrors(t,n,r)}_initObservables(){this.valueChanges=new In,this.statusChanges=new In}_calculateStatus(){return this._allControlsDisabled()?bp:this.errors?Pv:this._hasOwnPendingAsyncValidator||this._anyControlsHaveStatus(ld)?ld:this._anyControlsHaveStatus(Pv)?Pv:vp}_anyControlsHaveStatus(t){return this._anyControls(n=>n.status===t)}_anyControlsDirty(){return this._anyControls(t=>t.dirty)}_anyControlsTouched(){return this._anyControls(t=>t.touched)}_updatePristine(t,n){const r=!this._anyControlsDirty(),i=this.pristine!==r;this.pristine=r,this._parent&&!t.onlySelf&&this._parent._updatePristine(t,n),i&&this._events.next(new fD(this.pristine,n))}_updateTouched(t={},n){this.touched=this._anyControlsTouched(),this._events.next(new pD(this.touched,n)),this._parent&&!t.onlySelf&&this._parent._updateTouched(t,n)}_onDisabledChange=[];_registerOnCollectionChange(t){this._onCollectionChange=t}_setUpdateStrategy(t){Fv(t)&&null!=t.updateOn&&(this._updateOn=t.updateOn)}_parentMarkedDirty(t){return!t&&!(!this._parent||!this._parent.dirty)&&!this._parent._anyControlsDirty()}_find(t){return null}_assignValidators(t){this._rawValidators=Array.isArray(t)?t.slice():t,this._composedValidatorFn=function YG(e){return Array.isArray(e)?lD(e):e||null}(this._rawValidators)}_assignAsyncValidators(t){this._rawAsyncValidators=Array.isArray(t)?t.slice():t,this._composedAsyncValidatorFn=function XG(e){return Array.isArray(e)?cD(e):e||null}(this._rawAsyncValidators)}}class bD extends vD{constructor(t,n,r){super(function mD(e){return(Fv(e)?e.validators:e)||null}(n),function yD(e,t){return(Fv(t)?t.asyncValidators:e)||null}(r,n)),this.controls=t,this._initObservables(),this._setUpdateStrategy(n),this._setUpControls(),this.updateValueAndValidity({onlySelf:!0,emitEvent:!!this.asyncValidator})}controls;registerControl(t,n){return this.controls[t]?this.controls[t]:(this.controls[t]=n,n.setParent(this),n._registerOnCollectionChange(this._onCollectionChange),n)}addControl(t,n,r={}){this.registerControl(t,n),this.updateValueAndValidity({emitEvent:r.emitEvent}),this._onCollectionChange()}removeControl(t,n={}){this.controls[t]&&this.controls[t]._registerOnCollectionChange(()=>{}),delete this.controls[t],this.updateValueAndValidity({emitEvent:n.emitEvent}),this._onCollectionChange()}setControl(t,n,r={}){this.controls[t]&&this.controls[t]._registerOnCollectionChange(()=>{}),delete this.controls[t],n&&this.registerControl(t,n),this.updateValueAndValidity({emitEvent:r.emitEvent}),this._onCollectionChange()}contains(t){return this.controls.hasOwnProperty(t)&&this.controls[t].enabled}setValue(t,n={}){(function ZN(e,t,n){e._forEachChild((r,i)=>{if(void 0===n[i])throw new G(1002,"")})})(this,0,t),Object.keys(t).forEach(r=>{(function XN(e,t,n){const r=e.controls;if(!(t?Object.keys(r):r).length)throw new G(1e3,"");if(!r[n])throw new G(1001,"")})(this,!0,r),this.controls[r].setValue(t[r],{onlySelf:!0,emitEvent:n.emitEvent})}),this.updateValueAndValidity(n)}patchValue(t,n={}){null!=t&&(Object.keys(t).forEach(r=>{const i=this.controls[r];i&&i.patchValue(t[r],{onlySelf:!0,emitEvent:n.emitEvent})}),this.updateValueAndValidity(n))}reset(t={},n={}){this._forEachChild((r,i)=>{r.reset(t?t[i]:null,{onlySelf:!0,emitEvent:n.emitEvent})}),this._updatePristine(n,this),this._updateTouched(n,this),this.updateValueAndValidity(n),!1!==n?.emitEvent&&this._events.next(new gD(this))}getRawValue(){return this._reduceChildren({},(t,n,r)=>(t[r]=n.getRawValue(),t))}_syncPendingControls(){let t=this._reduceChildren(!1,(n,r)=>!!r._syncPendingControls()||n);return t&&this.updateValueAndValidity({onlySelf:!0}),t}_forEachChild(t){Object.keys(this.controls).forEach(n=>{const r=this.controls[n];r&&t(r,n)})}_setUpControls(){this._forEachChild(t=>{t.setParent(this),t._registerOnCollectionChange(this._onCollectionChange)})}_updateValue(){this.value=this._reduceValue()}_anyControls(t){for(const[n,r]of Object.entries(this.controls))if(this.contains(n)&&t(r))return!0;return!1}_reduceValue(){return this._reduceChildren({},(n,r,i)=>((r.enabled||this.disabled)&&(n[i]=r.value),n))}_reduceChildren(t,n){let r=t;return this._forEachChild((i,o)=>{r=n(r,i,o)}),r}_allControlsDisabled(){for(const t of Object.keys(this.controls))if(this.controls[t].enabled)return!1;return Object.keys(this.controls).length>0||this.disabled}_find(t){return this.controls.hasOwnProperty(t)?this.controls[t]:null}}const ud=new ee("",{providedIn:"root",factory:()=>Bv}),Bv="always";function jv(e,t){e.forEach(n=>{n.registerOnValidatorChange&&n.registerOnValidatorChange(t)})}function _D(e,t){const n=function VN(e){return e._rawValidators}(e);null!==t.validator?e.setValidators(BN(n,t.validator)):"function"==typeof n&&e.setValidators([n]);const r=function $N(e){return e._rawAsyncValidators}(e);null!==t.asyncValidator?e.setAsyncValidators(BN(r,t.asyncValidator)):"function"==typeof r&&e.setAsyncValidators([r]);const i=()=>e.updateValueAndValidity();jv(t._rawValidators,i),jv(t._rawAsyncValidators,i)}function KN(e,t){e._pendingDirty&&e.markAsDirty(),e.setValue(e._pendingValue,{emitModelToViewChange:!1}),t.viewToModelUpdate(e._pendingValue),e._pendingChange=!1}const o7={provide:Ar,useExisting:pt(()=>Cp)},wp=Promise.resolve();let Cp=(()=>{class e extends Ar{callSetDisabledState;get submitted(){return Vt(this.submittedReactive)}_submitted=It(()=>this.submittedReactive());submittedReactive=tn(!1);_directives=new Set;form;ngSubmit=new In;options;constructor(n,r,i){super(),this.callSetDisabledState=i,this.form=new bD({},lD(n),cD(r))}ngAfterViewInit(){this._setUpdateStrategy()}get formDirective(){return this}get control(){return this.form}get path(){return[]}get controls(){return this.form.controls}addControl(n){wp.then(()=>{const r=this._findContainer(n.path);n.control=r.registerControl(n.name,n.control),function _p(e,t,n=Bv){_D(e,t),t.valueAccessor.writeValue(e.value),(e.disabled||"always"===n)&&t.valueAccessor.setDisabledState?.(e.disabled),function QG(e,t){t.valueAccessor.registerOnChange(n=>{e._pendingValue=n,e._pendingChange=!0,e._pendingDirty=!0,"change"===e.updateOn&&KN(e,t)})}(e,t),function e7(e,t){const n=(r,i)=>{t.valueAccessor.writeValue(r),i&&t.viewToModelUpdate(r)};e.registerOnChange(n),t._registerOnDestroy(()=>{e._unregisterOnChange(n)})}(e,t),function JG(e,t){t.valueAccessor.registerOnTouched(()=>{e._pendingTouched=!0,"blur"===e.updateOn&&e._pendingChange&&KN(e,t),"submit"!==e.updateOn&&e.markAsTouched()})}(e,t),function KG(e,t){if(t.valueAccessor.setDisabledState){const n=r=>{t.valueAccessor.setDisabledState(r)};e.registerOnDisabledChange(n),t._registerOnDestroy(()=>{e._unregisterOnDisabledChange(n)})}}(e,t)}(n.control,n,this.callSetDisabledState),n.control.updateValueAndValidity({emitEvent:!1}),this._directives.add(n)})}getControl(n){return this.form.get(n.path)}removeControl(n){wp.then(()=>{const r=this._findContainer(n.path);r&&r.removeControl(n.name),this._directives.delete(n)})}addFormGroup(n){wp.then(()=>{const r=this._findContainer(n.path),i=new bD({});(function QN(e,t){_D(e,t)})(i,n),r.registerControl(n.name,i),i.updateValueAndValidity({emitEvent:!1})})}removeFormGroup(n){wp.then(()=>{const r=this._findContainer(n.path);r&&r.removeControl(n.name)})}getFormGroup(n){return this.form.get(n.path)}updateModel(n,r){wp.then(()=>{this.form.get(n.path).setValue(r)})}setValue(n){this.control.setValue(n)}onSubmit(n){return this.submittedReactive.set(!0),function JN(e,t){e._syncPendingControls(),t.forEach(n=>{const r=n.control;"submit"===r.updateOn&&r._pendingChange&&(n.viewToModelUpdate(r._pendingValue),r._pendingChange=!1)})}(this.form,this._directives),this.ngSubmit.emit(n),this.form._events.next(new YN(this.control)),"dialog"===n?.target?.method}onReset(){this.resetForm()}resetForm(n=void 0){this.form.reset(n),this.submittedReactive.set(!1)}_setUpdateStrategy(){this.options&&null!=this.options.updateOn&&(this.form._updateOn=this.options.updateOn)}_findContainer(n){return n.pop(),n.length?this.form.get(n):this.form}static \u0275fac=function(r){return new(r||e)(re(mr,10),re(Ga,10),re(ud,8))};static \u0275dir=Ne({type:e,selectors:[["form",3,"ngNoForm","",3,"formGroup",""],["ng-form"],["","ngForm",""]],hostBindings:function(r,i){1&r&&rn("submit",function(a){return i.onSubmit(a)})("reset",function(){return i.onReset()})},inputs:{options:[0,"ngFormOptions","options"]},outputs:{ngSubmit:"ngSubmit"},exportAs:["ngForm"],standalone:!1,features:[Kt([o7]),Tt]})}return e})();Promise.resolve();let oP=(()=>{class e{static \u0275fac=function(r){return new(r||e)};static \u0275dir=Ne({type:e,selectors:[["form",3,"ngNoForm","",3,"ngNativeValidate",""]],hostAttrs:["novalidate",""],standalone:!1})}return e})(),N7=(()=>{class e{static \u0275fac=function(r){return new(r||e)};static \u0275mod=Of({type:e});static \u0275inj=Wi({})}return e})(),MD=(()=>{class e{static withConfig(n){return{ngModule:e,providers:[{provide:ud,useValue:n.callSetDisabledState??Bv}]}}static \u0275fac=function(r){return new(r||e)};static \u0275mod=Of({type:e});static \u0275inj=Wi({imports:[N7]})}return e})();const DP=new ee("HIGHCHARTS_LOADER"),xP=new ee("HIGHCHARTS_ROOT_MODULES"),SP=new ee("HIGHCHARTS_OPTIONS"),MP=new ee("HIGHCHARTS_CONFIG"),TP=new ee("HIGHCHARTS_TIMEOUT");let L7=(()=>{var e;class t{constructor(){this.highcharts=tn(null),this.loader=Y(DP),this.globalOptions=Y(SP,{optional:!0}),this.globalModules=Y(xP,{optional:!0})}loadHighchartsWithModules(r){var i=this;return(0,oe.A)(function*(){const o=yield i.loader();return yield Promise.allSettled([...i.globalModules?.()??[],...r?.modules?.()??[]]),o})()}load(r){this.loadHighchartsWithModules(r).then(i=>{this.globalOptions&&i.setOptions(this.globalOptions),this.highcharts.set(i)})}static#e=e=()=>(this.\u0275fac=function(i){return new(i||t)},this.\u0275prov=pe({token:t,factory:t.\u0275fac,providedIn:"root"}))}return e(),t})(),F7=(()=>{var e;class t{delay(r){return new Promise(i=>setTimeout(i,r))}keepChartUpToDate(){var r=this;HC((0,oe.A)(function*(){r.update();const i=yield r.chart();r.chartCreated?i?.update(r.options(),!0,r.oneToOne()):i&&(r.chartCreated=!0)}))}destroyChart(){var r=this;return(0,oe.A)(function*(){const i=yield r.chart();i&&i.destroy()})()}constructor(){var r=this;this.constructorType=Qy("chart"),this.oneToOne=Qy(!1),this.options=Qy.required(),this.update=iz(),this.chartInstance=function tz(){return new Ok}(),this.destroyRef=Y(Cr),this.el=Y(ne),this.platformId=Y(Im),this.relativeConfig=Y(MP,{optional:!0}),this.timeout=Y(TP,{optional:!0}),this.highchartsChartService=Y(L7),this.chartCreated=!1,this.constructorChart=It(()=>{const i=this.highchartsChartService.highcharts();if(i)return i[this.constructorType()]}),this.chart=It((0,oe.A)(function*(){return yield r.delay(r.relativeConfig?.timeout??r.timeout??500),r.constructorChart()?.(r.el.nativeElement,Vt(()=>r.options()),i=>r.chartInstance.emit(i))})),(!this.platformId||!function Cb(e){return"server"===e}(this.platformId))&&(this.highchartsChartService.load(this.relativeConfig),this.destroyRef.onDestroy(()=>this.destroyChart()),function Kz(e,t){const n=t?.injector??Y(Gn),r=n.get(ao),i=n.get(Q_),o=n.get(Ou,null,{optional:!0});i.impl??=n.get(nM);let a=e;"function"==typeof a&&(a={mixedReadWrite:e});const u=n.get(tu,null,{optional:!0}),f=new Zz(i.impl,[a.earlyRead,a.write,a.mixedReadWrite,a.read],u?.view,r,n,o?.snapshot(null));i.impl.register(f)}(()=>{this.update()&&this.update.set(!1)}),this.keepChartUpToDate())}static#e=e=()=>(this.\u0275fac=function(i){return new(i||t)},this.\u0275dir=Ne({type:t,selectors:[["","highchartsChart",""]],inputs:{constructorType:[1,"constructorType"],oneToOne:[1,"oneToOne"],options:[1,"options"],update:[1,"update"]},outputs:{update:"updateChange",chartInstance:"chartInstance"}}))}return e(),t})(),B7=(()=>{var e;class t{static#e=e=()=>(this.\u0275fac=function(i){return new(i||t)},this.\u0275cmp=qo({type:t,selectors:[["highcharts-chart"]],features:[$T([{directive:F7,inputs:["constructorType","constructorType","oneToOne","oneToOne","options","options","update","update"],outputs:["chartInstance","chartInstance","updateChange","updateChange"]}])],decls:0,vars:0,template:function(i,o){},encapsulation:2,changeDetection:0}))}return e(),t})();const V7=()=>[],$7=()=>Se.e(444).then(Se.bind(Se,444)).then(e=>e.default);function j7(e){return Yi([{provide:DP,useValue:e??$7}])}function U7(e){return Yi([{provide:xP,useValue:e}])}var W7=Se(264);function G7(e,t){if(1&e&&Yn(0,"highcharts-chart",0),2&e){const n=Pe();Ni("Highcharts",n.Highcharts)("constructorType",n.chartConstructor)("options",n.chartOptions())("update",n.updateFlag())("oneToOne",!0)}}function q7(e,t){1&e&&(se(0,"p"),te(1,"Loading chart..."),ce())}let IP=(()=>{class e{minerName;minerService=Y(Zl);Highcharts=W7;chartConstructor="chart";chartOptions=tn({});updateFlag=tn(!1);constructor(){this.chartOptions.set(this.createBaseChartOptions()),HC(()=>{const n=this.minerService.hashrateHistory();let r={};if(this.minerName){const i=n.get(this.minerName),o=i?i.map(a=>[new Date(a.timestamp).getTime(),a.hashrate]):[];r=this.calculateYAxisBoundsForSingle(o.map(a=>a[1])),this.chartOptions.update(a=>({...a,title:{text:`${this.minerName} Hashrate`},chart:{type:"spline"},plotOptions:{area:void 0,spline:{marker:{enabled:!1}}},yAxis:{...a.yAxis,...r},series:[{type:"spline",name:"Hashrate",data:o}]}))}else if(0===n.size)this.chartOptions.update(i=>({...i,series:[]}));else{const i=[];n.forEach((o,a)=>{const u=o.map(f=>[new Date(f.timestamp).getTime(),f.hashrate]);i.push({type:"area",name:a,data:u})}),r=this.calculateYAxisBoundsForStacked(i),this.chartOptions.update(o=>({...o,title:{text:"Total Hashrate"},chart:{type:"area"},plotOptions:{area:{stacking:"normal",marker:{enabled:!1}}},yAxis:{...o.yAxis,...r},series:i}))}this.updateFlag.update(i=>!i)})}calculateYAxisBoundsForSingle(n){if(0===n.length)return{min:0,max:void 0};const r=Math.min(...n),i=Math.max(...n);if(r===i)return{min:Math.max(0,r-50),max:i+50};const o=.1*(i-r);return{min:Math.max(0,r-o),max:i+o}}calculateYAxisBoundsForStacked(n){const r={};n.forEach(u=>{const f=u.data;f&&f.forEach(([y,w])=>{r[y]=(r[y]||0)+w})});const i=Object.values(r);if(0===i.length)return{min:0,max:void 0};const o=Math.max(...i);return{min:0,max:o+.1*o}}createBaseChartOptions(){return{xAxis:{type:"datetime",title:{text:"Time"}},yAxis:{title:{text:"Hashrate (H/s)"}},series:[],credits:{enabled:!1},accessibility:{enabled:!1}}}static \u0275fac=function(r){return new(r||e)};static \u0275cmp=qo({type:e,selectors:[["snider-mining-chart"]],inputs:{minerName:"minerName"},decls:2,vars:1,consts:[[2,"width","100%","height","400px","display","block",3,"Highcharts","constructorType","options","update","oneToOne"]],template:function(r,i){1&r&&or(0,G7,1,5,"highcharts-chart",0)(1,q7,2,0,"p"),2&r&&sr(i.chartOptions()?0:1)},dependencies:[ql,B7],styles:[".chart[_ngcontent-%COMP%]{width:100%;height:300px;display:block}"]})}return e})();var Y7=class extends Event{constructor(){super("wa-reposition",{bubbles:!0,cancelable:!1,composed:!0})}};const Ya=Math.min,fi=Math.max,qv=Math.round,Yv=Math.floor,Jo=e=>({x:e,y:e}),X7={left:"right",right:"left",bottom:"top",top:"bottom"},Z7={start:"end",end:"start"};function TD(e,t,n){return fi(e,Ya(t,n))}function dd(e,t){return"function"==typeof e?e(t):e}function Xa(e){return e.split("-")[0]}function hd(e){return e.split("-")[1]}function AP(e){return"x"===e?"y":"x"}function ID(e){return"y"===e?"height":"width"}const K7=new Set(["top","bottom"]);function Hs(e){return K7.has(Xa(e))?"y":"x"}function AD(e){return AP(Hs(e))}function kD(e){return e.replace(/start|end/g,t=>Z7[t])}const kP=["left","right"],OP=["right","left"],eq=["top","bottom"],tq=["bottom","top"];function Xv(e){return e.replace(/left|right|bottom|top/g,t=>X7[t])}function RP(e){return"number"!=typeof e?function iq(e){return{top:0,right:0,bottom:0,left:0,...e}}(e):{top:e,right:e,bottom:e,left:e}}function Zv(e){const{x:t,y:n,width:r,height:i}=e;return{width:r,height:i,top:n,left:t,right:t+r,bottom:n+i,x:t,y:n}}function NP(e,t,n){let{reference:r,floating:i}=e;const o=Hs(t),a=AD(t),u=ID(a),f=Xa(t),y="y"===o,w=r.x+r.width/2-i.width/2,D=r.y+r.height/2-i.height/2,k=r[u]/2-i[u]/2;let A;switch(f){case"top":A={x:w,y:r.y-i.height};break;case"bottom":A={x:w,y:r.y+r.height};break;case"right":A={x:r.x+r.width,y:D};break;case"left":A={x:r.x-i.width,y:D};break;default:A={x:r.x,y:r.y}}switch(hd(t)){case"start":A[a]-=k*(n&&y?-1:1);break;case"end":A[a]+=k*(n&&y?-1:1)}return A}const oq=function(){var e=(0,oe.A)(function*(t,n,r){const{placement:i="bottom",strategy:o="absolute",middleware:a=[],platform:u}=r,f=a.filter(Boolean),y=yield null==u.isRTL?void 0:u.isRTL(n);let w=yield u.getElementRects({reference:t,floating:n,strategy:o}),{x:D,y:k}=NP(w,i,y),A=i,N={},L=0;for(let H=0;H"u")&&(e instanceof ShadowRoot||e instanceof pi(e).ShadowRoot)}const pq=new Set(["inline","contents"]);function Dp(e){const{overflow:t,overflowX:n,overflowY:r,display:i}=bo(e);return/auto|scroll|overlay|hidden|clip/.test(t+r+n)&&!pq.has(i)}const gq=new Set(["table","td","th"]);function mq(e){return gq.has(pd(e))}const yq=[":popover-open",":modal"];function Qv(e){return yq.some(t=>{try{return e.matches(t)}catch{return!1}})}const vq=["transform","translate","scale","rotate","perspective"],bq=["transform","translate","scale","rotate","perspective","filter"],_q=["paint","layout","strict","content"];function Jv(e){const t=ND(),n=vo(e)?bo(e):e;return vq.some(r=>!!n[r]&&"none"!==n[r])||!!n.containerType&&"normal"!==n.containerType||!t&&!!n.backdropFilter&&"none"!==n.backdropFilter||!t&&!!n.filter&&"none"!==n.filter||bq.some(r=>(n.willChange||"").includes(r))||_q.some(r=>(n.contain||"").includes(r))}function ND(){return!(typeof CSS>"u"||!CSS.supports)&&CSS.supports("-webkit-backdrop-filter","none")}const Cq=new Set(["html","body","#document"]);function gd(e){return Cq.has(pd(e))}function bo(e){return pi(e).getComputedStyle(e)}function e0(e){return vo(e)?{scrollLeft:e.scrollLeft,scrollTop:e.scrollTop}:{scrollLeft:e.scrollX,scrollTop:e.scrollY}}function Za(e){if("html"===pd(e))return e;const t=e.assignedSlot||e.parentNode||$P(e)&&e.host||es(e);return $P(t)?t.host:t}function jP(e){const t=Za(e);return gd(t)?e.ownerDocument?e.ownerDocument.body:e.body:ts(t)&&Dp(t)?t:jP(t)}function md(e,t,n){var r;void 0===t&&(t=[]),void 0===n&&(n=!0);const i=jP(e),o=i===(null==(r=e.ownerDocument)?void 0:r.body),a=pi(i);if(o){const u=PD(a);return t.concat(a,a.visualViewport||[],Dp(i)?i:[],u&&n?md(u):[])}return t.concat(i,md(i,[],n))}function PD(e){return e.parent&&Object.getPrototypeOf(e.parent)?e.frameElement:null}function HP(e){const t=bo(e);let n=parseFloat(t.width)||0,r=parseFloat(t.height)||0;const i=ts(e),o=i?e.offsetWidth:n,a=i?e.offsetHeight:r,u=qv(n)!==o||qv(r)!==a;return u&&(n=o,r=a),{width:n,height:r,$:u}}function LD(e){return vo(e)?e:e.contextElement}function yd(e){const t=LD(e);if(!ts(t))return Jo(1);const n=t.getBoundingClientRect(),{width:r,height:i,$:o}=HP(t);let a=(o?qv(n.width):n.width)/r,u=(o?qv(n.height):n.height)/i;return(!a||!Number.isFinite(a))&&(a=1),(!u||!Number.isFinite(u))&&(u=1),{x:a,y:u}}const Eq=Jo(0);function UP(e){const t=pi(e);return ND()&&t.visualViewport?{x:t.visualViewport.offsetLeft,y:t.visualViewport.offsetTop}:Eq}function oc(e,t,n,r){void 0===t&&(t=!1),void 0===n&&(n=!1);const i=e.getBoundingClientRect(),o=LD(e);let a=Jo(1);t&&(r?vo(r)&&(a=yd(r)):a=yd(e));const u=function Dq(e,t,n){return void 0===t&&(t=!1),!(!n||t&&n!==pi(e))&&t}(o,n,r)?UP(o):Jo(0);let f=(i.left+u.x)/a.x,y=(i.top+u.y)/a.y,w=i.width/a.x,D=i.height/a.y;if(o){const k=pi(o),A=r&&vo(r)?pi(r):r;let N=k,L=PD(N);for(;L&&r&&A!==N;){const H=yd(L),V=L.getBoundingClientRect(),z=bo(L),ie=V.left+(L.clientLeft+parseFloat(z.paddingLeft))*H.x,ae=V.top+(L.clientTop+parseFloat(z.paddingTop))*H.y;f*=H.x,y*=H.y,w*=H.x,D*=H.y,f+=ie,y+=ae,N=pi(L),L=PD(N)}}return Zv({width:w,height:D,x:f,y})}function t0(e,t){const n=e0(e).scrollLeft;return t?t.left+n:oc(es(e)).left+n}function zP(e,t){const n=e.getBoundingClientRect();return{x:n.left+t.scrollLeft-t0(e,n),y:n.top+t.scrollTop}}const Iq=new Set(["absolute","fixed"]);function GP(e,t,n){let r;if("viewport"===t)r=function Tq(e,t){const n=pi(e),r=es(e),i=n.visualViewport;let o=r.clientWidth,a=r.clientHeight,u=0,f=0;if(i){o=i.width,a=i.height;const w=ND();(!w||w&&"fixed"===t)&&(u=i.offsetLeft,f=i.offsetTop)}const y=t0(r);if(y<=0){const w=r.ownerDocument,D=w.body,k=getComputedStyle(D),A="CSS1Compat"===w.compatMode&&parseFloat(k.marginLeft)+parseFloat(k.marginRight)||0,N=Math.abs(r.clientWidth-D.clientWidth-A);N<=25&&(o-=N)}else y<=25&&(o+=y);return{width:o,height:a,x:u,y:f}}(e,n);else if("document"===t)r=function Mq(e){const t=es(e),n=e0(e),r=e.ownerDocument.body,i=fi(t.scrollWidth,t.clientWidth,r.scrollWidth,r.clientWidth),o=fi(t.scrollHeight,t.clientHeight,r.scrollHeight,r.clientHeight);let a=-n.scrollLeft+t0(e);const u=-n.scrollTop;return"rtl"===bo(r).direction&&(a+=fi(t.clientWidth,r.clientWidth)-i),{width:i,height:o,x:a,y:u}}(es(e));else if(vo(t))r=function Aq(e,t){const n=oc(e,!0,"fixed"===t),r=n.top+e.clientTop,i=n.left+e.clientLeft,o=ts(e)?yd(e):Jo(1);return{width:e.clientWidth*o.x,height:e.clientHeight*o.y,x:i*o.x,y:r*o.y}}(t,n);else{const i=UP(e);r={x:t.x-i.x,y:t.y-i.y,width:t.width,height:t.height}}return Zv(r)}function qP(e,t){const n=Za(e);return!(n===t||!vo(n)||gd(n))&&("fixed"===bo(n).position||qP(n,t))}function kq(e,t){const n=t.get(e);if(n)return n;let r=md(e,[],!1).filter(u=>vo(u)&&"body"!==pd(u)),i=null;const o="fixed"===bo(e).position;let a=o?Za(e):e;for(;vo(a)&&!gd(a);){const u=bo(a),f=Jv(a);!f&&"fixed"===u.position&&(i=null),(o?!f&&!i:!f&&"static"===u.position&&i&&Iq.has(i.position)||Dp(a)&&!f&&qP(e,a))?r=r.filter(w=>w!==a):i=u,a=Za(a)}return t.set(e,r),r}function Nq(e,t,n){const r=ts(t),i=es(t),o="fixed"===n,a=oc(e,!0,o,t);let u={scrollLeft:0,scrollTop:0};const f=Jo(0);function y(){f.x=t0(i)}if(r||!r&&!o)if(("body"!==pd(t)||Dp(i))&&(u=e0(t)),r){const A=oc(t,!0,o,t);f.x=A.x+t.clientLeft,f.y=A.y+t.clientTop}else i&&y();o&&!r&&i&&y();const w=!i||r||o?Jo(0):zP(i,u);return{x:a.left+u.scrollLeft-f.x-w.x,y:a.top+u.scrollTop-f.y-w.y,width:a.width,height:a.height}}function FD(e){return"static"===bo(e).position}function YP(e,t){if(!ts(e)||"fixed"===bo(e).position)return null;if(t)return t(e);let n=e.offsetParent;return es(e)===n&&(n=n.ownerDocument.body),n}function XP(e,t){const n=pi(e);if(Qv(e))return n;if(!ts(e)){let i=Za(e);for(;i&&!gd(i);){if(vo(i)&&!FD(i))return i;i=Za(i)}return n}let r=YP(e,t);for(;r&&mq(r)&&FD(r);)r=YP(r,t);return r&&gd(r)&&FD(r)&&!Jv(r)?n:r||function wq(e){let t=Za(e);for(;ts(t)&&!gd(t);){if(Jv(t))return t;if(Qv(t))return null;t=Za(t)}return null}(e)||n}const n0={convertOffsetParentRelativeRectToViewportRelativeRect:function xq(e){let{elements:t,rect:n,offsetParent:r,strategy:i}=e;const o="fixed"===i,a=es(r),u=!!t&&Qv(t.floating);if(r===a||u&&o)return n;let f={scrollLeft:0,scrollTop:0},y=Jo(1);const w=Jo(0),D=ts(r);if((D||!D&&!o)&&(("body"!==pd(r)||Dp(a))&&(f=e0(r)),ts(r))){const A=oc(r);y=yd(r),w.x=A.x+r.clientLeft,w.y=A.y+r.clientTop}const k=!a||D||o?Jo(0):zP(a,f);return{width:n.width*y.x,height:n.height*y.y,x:n.x*y.x-f.scrollLeft*y.x+w.x+k.x,y:n.y*y.y-f.scrollTop*y.y+w.y+k.y}},getDocumentElement:es,getClippingRect:function Oq(e){let{element:t,boundary:n,rootBoundary:r,strategy:i}=e;const a=[..."clippingAncestors"===n?Qv(t)?[]:kq(t,this._c):[].concat(n),r],f=a.reduce((y,w)=>{const D=GP(t,w,i);return y.top=fi(D.top,y.top),y.right=Ya(D.right,y.right),y.bottom=Ya(D.bottom,y.bottom),y.left=fi(D.left,y.left),y},GP(t,a[0],i));return{width:f.right-f.left,height:f.bottom-f.top,x:f.left,y:f.top}},getOffsetParent:XP,getElementRects:function(){var e=(0,oe.A)(function*(t){const n=this.getOffsetParent||XP,r=this.getDimensions,i=yield r(t.floating);return{reference:Nq(t.reference,yield n(t.floating),t.strategy),floating:{x:0,y:0,width:i.width,height:i.height}}});return function(n){return e.apply(this,arguments)}}(),getClientRects:function Sq(e){return Array.from(e.getClientRects())},getDimensions:function Rq(e){const{width:t,height:n}=HP(e);return{width:t,height:n}},getScale:yd,isElement:vo,isRTL:function Lq(e){return"rtl"===bo(e).direction}};function ZP(e,t){return e.x===t.x&&e.y===t.y&&e.width===t.width&&e.height===t.height}const Vq=function(e){return void 0===e&&(e=0),{name:"offset",options:e,fn:t=>(0,oe.A)(function*(){var n,r;const{x:i,y:o,placement:a,middlewareData:u}=t,f=yield function uq(e,t){return RD.apply(this,arguments)}(t,e);return a===(null==(n=u.offset)?void 0:n.placement)&&null!=(r=u.arrow)&&r.alignmentOffset?{}:{x:i+f.x,y:o+f.y,data:{...f,placement:a}}})()}},jq=function(e){return void 0===e&&(e={}),{name:"flip",options:e,fn:t=>(0,oe.A)(function*(){var n,r;const{placement:i,middlewareData:o,rects:a,initialPlacement:u,platform:f,elements:y}=t,{mainAxis:w=!0,crossAxis:D=!0,fallbackPlacements:k,fallbackStrategy:A="bestFit",fallbackAxisSideDirection:N="none",flipAlignment:L=!0,...H}=dd(e,t);if(null!=(n=o.arrow)&&n.alignmentOffset)return{};const V=Xa(i),z=Hs(u),ie=Xa(u)===u,ae=yield null==f.isRTL?void 0:f.isRTL(y.floating),de=k||(ie||!L?[Xv(u)]:function J7(e){const t=Xv(e);return[kD(e),t,kD(t)]}(u)),Me="none"!==N;!k&&Me&&de.push(...function rq(e,t,n,r){const i=hd(e);let o=function nq(e,t,n){switch(e){case"top":case"bottom":return n?t?OP:kP:t?kP:OP;case"left":case"right":return t?eq:tq;default:return[]}}(Xa(e),"start"===n,r);return i&&(o=o.map(a=>a+"-"+i),t&&(o=o.concat(o.map(kD)))),o}(u,L,N,ae));const et=[u,...de],bn=yield fd(t,H),Rn=[];let nt=(null==(r=o.flip)?void 0:r.overflows)||[];if(w&&Rn.push(bn[V]),D){const Zn=function Q7(e,t,n){void 0===n&&(n=!1);const r=hd(e),i=AD(e),o=ID(i);let a="x"===i?r===(n?"end":"start")?"right":"left":"start"===r?"bottom":"top";return t.reference[o]>t.floating[o]&&(a=Xv(a)),[a,Xv(a)]}(i,a,ae);Rn.push(bn[Zn[0]],bn[Zn[1]])}if(nt=[...nt,{placement:i,overflows:Rn}],!Rn.every(Zn=>Zn<=0)){var Xn,kr;const Zn=((null==(Xn=o.flip)?void 0:Xn.index)||0)+1,Co=et[Zn];if(Co&&("alignment"!==D||z===Hs(Co)||nt.every(mi=>Hs(mi.placement)!==z||mi.overflows[0]>0)))return{data:{index:Zn,overflows:nt},reset:{placement:Co}};let ji=null==(kr=nt.filter(gi=>gi.overflows[0]<=0).sort((gi,mi)=>gi.overflows[1]-mi.overflows[1])[0])?void 0:kr.placement;if(!ji)switch(A){case"bestFit":{var Xr;const gi=null==(Xr=nt.filter(mi=>{if(Me){const Ja=Hs(mi.placement);return Ja===z||"y"===Ja}return!0}).map(mi=>[mi.placement,mi.overflows.filter(Ja=>Ja>0).reduce((Ja,SK)=>Ja+SK,0)]).sort((mi,Ja)=>mi[1]-Ja[1])[0])?void 0:Xr[0];gi&&(ji=gi);break}case"initialPlacement":ji=u}if(i!==ji)return{reset:{placement:ji}}}return{}})()}},KP=function(e){return void 0===e&&(e={}),{name:"size",options:e,fn:t=>(0,oe.A)(function*(){var n,r;const{placement:i,rects:o,platform:a,elements:u}=t,{apply:f=()=>{},...y}=dd(e,t),w=yield fd(t,y),D=Xa(i),k=hd(i),A="y"===Hs(i),{width:N,height:L}=o.floating;let H,V;"top"===D||"bottom"===D?(H=D,V=k===((yield null==a.isRTL?void 0:a.isRTL(u.floating))?"start":"end")?"left":"right"):(V=D,H="end"===k?"top":"bottom");const z=L-w.top-w.bottom,ie=N-w.left-w.right;let et=Ya(L-w[H],z),bn=Ya(N-w[V],ie);if(null!=(n=t.middlewareData.shift)&&n.enabled.x&&(bn=ie),null!=(r=t.middlewareData.shift)&&r.enabled.y&&(et=z),!t.middlewareData.shift&&!k){const nt=fi(w.left,0),Xn=fi(w.right,0),kr=fi(w.top,0),Xr=fi(w.bottom,0);A?bn=N-2*(0!==nt||0!==Xn?nt+Xn:fi(w.left,w.right)):et=L-2*(0!==kr||0!==Xr?kr+Xr:fi(w.top,w.bottom))}yield f({...t,availableWidth:bn,availableHeight:et});const Rn=yield a.getDimensions(u.floating);return N!==Rn.width||L!==Rn.height?{reset:{rects:!0}}:{}})()}};function zq(e){return function VD(e){for(let t=e;t;t=BD(t))if(t instanceof Element&&"none"===getComputedStyle(t).display)return null;for(let t=BD(e);t;t=BD(t)){if(!(t instanceof Element))continue;const n=getComputedStyle(t);if("contents"!==n.display&&("static"!==n.position||Jv(n)||"BODY"===t.tagName))return t}return null}(e)}function BD(e){return e.assignedSlot?e.assignedSlot:e.parentNode instanceof ShadowRoot?e.parentNode.host:e.parentNode}function JP(e){return null!==e&&"object"==typeof e&&"getBoundingClientRect"in e&&(!("contextElement"in e)||e instanceof Element)}var r0=globalThis?.HTMLElement?.prototype.hasOwnProperty("popover"),jt=(()=>{let e=class extends di{constructor(){super(...arguments),this.localize=new za(this),this.active=!1,this.placement="top",this.boundary="viewport",this.distance=0,this.skidding=0,this.arrow=!1,this.arrowPlacement="anchor",this.arrowPadding=10,this.flip=!1,this.flipFallbackPlacements="",this.flipFallbackStrategy="best-fit",this.flipPadding=0,this.shift=!1,this.shiftPadding=0,this.autoSizePadding=0,this.hoverBridge=!1,this.updateHoverBridge=()=>{if(this.hoverBridge&&this.anchorEl){const t=this.anchorEl.getBoundingClientRect(),n=this.popup.getBoundingClientRect();let i=0,o=0,a=0,u=0,f=0,y=0,w=0,D=0;this.placement.includes("top")||this.placement.includes("bottom")?t.topsuper.connectedCallback,n=this;return(0,oe.A)(function*(){t().call(n),yield n.updateComplete,n.start()})()}disconnectedCallback(){super.disconnectedCallback(),this.stop()}updated(t){var n=()=>super.updated,r=this;return(0,oe.A)(function*(){n().call(r,t),t.has("active")&&(r.active?r.start():r.stop()),t.has("anchor")&&r.handleAnchorChange(),r.active&&(yield r.updateComplete,r.reposition())})()}handleAnchorChange(){var t=this;return(0,oe.A)(function*(){if(yield t.stop(),t.anchor&&"string"==typeof t.anchor){const n=t.getRootNode();t.anchorEl=n.getElementById(t.anchor)}else t.anchorEl=t.anchor instanceof Element||JP(t.anchor)?t.anchor:t.querySelector('[slot="anchor"]');t.anchorEl instanceof HTMLSlotElement&&(t.anchorEl=t.anchorEl.assignedElements({flatten:!0})[0]),t.anchorEl&&t.start()})()}start(){!this.anchorEl||!this.active||(this.popup.showPopover?.(),this.cleanup=function Bq(e,t,n,r){void 0===r&&(r={});const{ancestorScroll:i=!0,ancestorResize:o=!0,elementResize:a="function"==typeof ResizeObserver,layoutShift:u="function"==typeof IntersectionObserver,animationFrame:f=!1}=r,y=LD(e),w=i||o?[...y?md(y):[],...md(t)]:[];w.forEach(V=>{i&&V.addEventListener("scroll",n,{passive:!0}),o&&V.addEventListener("resize",n)});const D=y&&u?function Fq(e,t){let r,n=null;const i=es(e);function o(){var u;clearTimeout(r),null==(u=n)||u.disconnect(),n=null}return function a(u,f){void 0===u&&(u=!1),void 0===f&&(f=1),o();const y=e.getBoundingClientRect(),{left:w,top:D,width:k,height:A}=y;if(u||t(),!k||!A)return;const ie={rootMargin:-Yv(D)+"px "+-Yv(i.clientWidth-(w+k))+"px "+-Yv(i.clientHeight-(D+A))+"px "+-Yv(w)+"px",threshold:fi(0,Ya(1,f))||1};let ae=!0;function de(Me){const et=Me[0].intersectionRatio;if(et!==f){if(!ae)return a();et?a(!1,et):r=setTimeout(()=>{a(!1,1e-7)},1e3)}1===et&&!ZP(y,e.getBoundingClientRect())&&a(),ae=!1}try{n=new IntersectionObserver(de,{...ie,root:i.ownerDocument})}catch{n=new IntersectionObserver(de,ie)}n.observe(e)}(!0),o}(y,n):null;let k=-1,A=null;a&&(A=new ResizeObserver(V=>{let[z]=V;z&&z.target===y&&A&&(A.unobserve(t),cancelAnimationFrame(k),k=requestAnimationFrame(()=>{var ie;null==(ie=A)||ie.observe(t)})),n()}),y&&!f&&A.observe(y),A.observe(t));let N,L=f?oc(e):null;return f&&function H(){const V=oc(e);L&&!ZP(L,V)&&n(),L=V,N=requestAnimationFrame(H)}(),n(),()=>{var V;w.forEach(z=>{i&&z.removeEventListener("scroll",n),o&&z.removeEventListener("resize",n)}),D?.(),null==(V=A)||V.disconnect(),A=null,f&&cancelAnimationFrame(N)}}(this.anchorEl,this.popup,()=>{this.reposition()}))}stop(){var t=this;return(0,oe.A)(function*(){return new Promise(n=>{t.popup.hidePopover?.(),t.cleanup?(t.cleanup(),t.cleanup=void 0,t.removeAttribute("data-current-placement"),t.style.removeProperty("--auto-size-available-width"),t.style.removeProperty("--auto-size-available-height"),requestAnimationFrame(()=>n())):n()})})()}reposition(){if(!this.active||!this.anchorEl)return;const t=[Vq({mainAxis:this.distance,crossAxis:this.skidding})];let n;this.sync?t.push(KP({apply:({rects:i})=>{const a="height"===this.sync||"both"===this.sync;this.popup.style.width="width"===this.sync||"both"===this.sync?`${i.reference.width}px`:"",this.popup.style.height=a?`${i.reference.height}px`:""}})):(this.popup.style.width="",this.popup.style.height=""),r0&&!JP(this.anchor)&&"scroll"===this.boundary&&(n=md(this.anchorEl).filter(i=>i instanceof Element)),this.flip&&t.push(jq({boundary:this.flipBoundary||n,fallbackPlacements:this.flipFallbackPlacements,fallbackStrategy:"best-fit"===this.flipFallbackStrategy?"bestFit":"initialPlacement",padding:this.flipPadding})),this.shift&&t.push(function(e){return void 0===e&&(e={}),{name:"shift",options:e,fn:t=>(0,oe.A)(function*(){const{x:n,y:r,placement:i}=t,{mainAxis:o=!0,crossAxis:a=!1,limiter:u={fn:H=>{let{x:V,y:z}=H;return{x:V,y:z}}},...f}=dd(e,t),y={x:n,y:r},w=yield fd(t,f),D=Hs(Xa(i)),k=AP(D);let A=y[k],N=y[D];o&&(A=TD(A+w["y"===k?"top":"left"],A,A-w["y"===k?"bottom":"right"])),a&&(N=TD(N+w["y"===D?"top":"left"],N,N-w["y"===D?"bottom":"right"]));const L=u.fn({...t,[k]:A,[D]:N});return{...L,data:{x:L.x-n,y:L.y-r,enabled:{[k]:o,[D]:a}}}})()}}({boundary:this.shiftBoundary||n,padding:this.shiftPadding})),this.autoSize?t.push(KP({boundary:this.autoSizeBoundary||n,padding:this.autoSizePadding,apply:({availableWidth:i,availableHeight:o})=>{"vertical"===this.autoSize||"both"===this.autoSize?this.style.setProperty("--auto-size-available-height",`${o}px`):this.style.removeProperty("--auto-size-available-height"),"horizontal"===this.autoSize||"both"===this.autoSize?this.style.setProperty("--auto-size-available-width",`${i}px`):this.style.removeProperty("--auto-size-available-width")}})):(this.style.removeProperty("--auto-size-available-width"),this.style.removeProperty("--auto-size-available-height")),this.arrow&&t.push((e=>({name:"arrow",options:e,fn:t=>(0,oe.A)(function*(){const{x:n,y:r,placement:i,rects:o,platform:a,elements:u,middlewareData:f}=t,{element:y,padding:w=0}=dd(e,t)||{};if(null==y)return{};const D=RP(w),k={x:n,y:r},A=AD(i),N=ID(A),L=yield a.getDimensions(y),H="y"===A,V=H?"top":"left",z=H?"bottom":"right",ie=H?"clientHeight":"clientWidth",ae=o.reference[N]+o.reference[A]-k[A]-o.floating[N],de=k[A]-o.reference[A],Me=yield null==a.getOffsetParent?void 0:a.getOffsetParent(y);let et=Me?Me[ie]:0;(!et||!(yield null==a.isElement?void 0:a.isElement(Me)))&&(et=u.floating[ie]||o.floating[N]);const bn=ae/2-de/2,Rn=et/2-L[N]/2-1,nt=Ya(D[V],Rn),Xn=Ya(D[z],Rn),kr=nt,Xr=et-L[N]-Xn,Zn=et/2-L[N]/2+bn,Co=TD(kr,Zn,Xr),ji=!f.arrow&&null!=hd(i)&&Zn!==Co&&o.reference[N]/2-(Znn0.getOffsetParent(i,zq):n0.getOffsetParent;((e,t,n)=>{const i={platform:n0,...n},o={...i.platform,_c:new Map};return oq(e,t,{...i,platform:o})})(this.anchorEl,this.popup,{placement:this.placement,middleware:t,strategy:r0?"absolute":"fixed",platform:{...n0,getOffsetParent:r}}).then(({x:i,y:o,middlewareData:a,placement:u})=>{const f="rtl"===this.localize.dir(),y={top:"bottom",right:"left",bottom:"top",left:"right"}[u.split("-")[0]];if(this.setAttribute("data-current-placement",u),Object.assign(this.popup.style,{left:`${i}px`,top:`${o}px`}),this.arrow){const w=a.arrow.x,D=a.arrow.y;let k="",A="",N="",L="";if("start"===this.arrowPlacement){const H="number"==typeof w?`calc(${this.arrowPadding}px - var(--arrow-padding-offset))`:"";k="number"==typeof D?`calc(${this.arrowPadding}px - var(--arrow-padding-offset))`:"",A=f?H:"",L=f?"":H}else if("end"===this.arrowPlacement){const H="number"==typeof w?`calc(${this.arrowPadding}px - var(--arrow-padding-offset))`:"";A=f?"":H,L=f?H:"",N="number"==typeof D?`calc(${this.arrowPadding}px - var(--arrow-padding-offset))`:""}else"center"===this.arrowPlacement?(L="number"==typeof w?"calc(50% - var(--arrow-size-diagonal))":"",k="number"==typeof D?"calc(50% - var(--arrow-size-diagonal))":""):(L="number"==typeof w?`${w}px`:"",k="number"==typeof D?`${D}px`:"");Object.assign(this.arrowEl.style,{top:k,right:A,bottom:N,left:L,[y]:"calc(var(--arrow-size-diagonal) * -1)"})}}),requestAnimationFrame(()=>this.updateHoverBridge()),this.dispatchEvent(new Y7)}render(){return At` + + + + +
+ + ${this.arrow?At``:""} +
+ `}};return e.css=":host {\n --arrow-color: black;\n --arrow-size: var(--wa-tooltip-arrow-size);\n --show-duration: 100ms;\n --hide-duration: 100ms;\n\n /*\n * These properties are computed to account for the arrow's dimensions after being rotated 45\xba. The constant\n * 0.7071 is derived from sin(45), which is the diagonal size of the arrow's container after rotating.\n */\n --arrow-size-diagonal: calc(var(--arrow-size) * 0.7071);\n --arrow-padding-offset: calc(var(--arrow-size-diagonal) - var(--arrow-size));\n\n display: contents;\n}\n\n.popup {\n position: absolute;\n isolation: isolate;\n max-width: var(--auto-size-available-width, none);\n max-height: var(--auto-size-available-height, none);\n\n /* Clear UA styles for [popover] */\n :where(&) {\n inset: unset;\n padding: unset;\n margin: unset;\n width: unset;\n height: unset;\n color: unset;\n background: unset;\n border: unset;\n overflow: unset;\n }\n}\n\n.popup-fixed {\n position: fixed;\n}\n\n.popup:not(.popup-active) {\n display: none;\n}\n\n.arrow {\n position: absolute;\n width: calc(var(--arrow-size-diagonal) * 2);\n height: calc(var(--arrow-size-diagonal) * 2);\n rotate: 45deg;\n background: var(--arrow-color);\n z-index: 3;\n}\n\n:host([data-current-placement~='left']) .arrow {\n rotate: -45deg;\n}\n\n:host([data-current-placement~='right']) .arrow {\n rotate: 135deg;\n}\n\n:host([data-current-placement~='bottom']) .arrow {\n rotate: 225deg;\n}\n\n/* Hover bridge */\n.popup-hover-bridge:not(.popup-hover-bridge-visible) {\n display: none;\n}\n\n.popup-hover-bridge {\n position: fixed;\n z-index: 899;\n top: 0;\n right: 0;\n bottom: 0;\n left: 0;\n clip-path: polygon(\n var(--hover-bridge-top-left-x, 0) var(--hover-bridge-top-left-y, 0),\n var(--hover-bridge-top-right-x, 0) var(--hover-bridge-top-right-y, 0),\n var(--hover-bridge-bottom-right-x, 0) var(--hover-bridge-bottom-right-y, 0),\n var(--hover-bridge-bottom-left-x, 0) var(--hover-bridge-bottom-left-y, 0)\n );\n}\n\n/* Built-in animations */\n.show {\n animation: show var(--show-duration) ease;\n}\n\n.hide {\n animation: show var(--hide-duration) ease reverse;\n}\n\n@keyframes show {\n from {\n opacity: 0;\n }\n to {\n opacity: 1;\n }\n}\n\n.show-with-scale {\n animation: show-with-scale var(--show-duration) ease;\n}\n\n.hide-with-scale {\n animation: show-with-scale var(--hide-duration) ease reverse;\n}\n\n@keyframes show-with-scale {\n from {\n opacity: 0;\n scale: 0.8;\n }\n to {\n opacity: 1;\n scale: 1;\n }\n}\n",e})();B([$n(".popup")],jt.prototype,"popup",2),B([$n(".arrow")],jt.prototype,"arrowEl",2),B([W()],jt.prototype,"anchor",2),B([W({type:Boolean,reflect:!0})],jt.prototype,"active",2),B([W({reflect:!0})],jt.prototype,"placement",2),B([W()],jt.prototype,"boundary",2),B([W({type:Number})],jt.prototype,"distance",2),B([W({type:Number})],jt.prototype,"skidding",2),B([W({type:Boolean})],jt.prototype,"arrow",2),B([W({attribute:"arrow-placement"})],jt.prototype,"arrowPlacement",2),B([W({attribute:"arrow-padding",type:Number})],jt.prototype,"arrowPadding",2),B([W({type:Boolean})],jt.prototype,"flip",2),B([W({attribute:"flip-fallback-placements",converter:{fromAttribute:e=>e.split(" ").map(t=>t.trim()).filter(t=>""!==t),toAttribute:e=>e.join(" ")}})],jt.prototype,"flipFallbackPlacements",2),B([W({attribute:"flip-fallback-strategy"})],jt.prototype,"flipFallbackStrategy",2),B([W({type:Object})],jt.prototype,"flipBoundary",2),B([W({attribute:"flip-padding",type:Number})],jt.prototype,"flipPadding",2),B([W({type:Boolean})],jt.prototype,"shift",2),B([W({type:Object})],jt.prototype,"shiftBoundary",2),B([W({attribute:"shift-padding",type:Number})],jt.prototype,"shiftPadding",2),B([W({attribute:"auto-size"})],jt.prototype,"autoSize",2),B([W()],jt.prototype,"sync",2),B([W({type:Object})],jt.prototype,"autoSizeBoundary",2),B([W({attribute:"auto-size-padding",type:Number})],jt.prototype,"autoSizePadding",2),B([W({attribute:"hover-bridge",type:Boolean})],jt.prototype,"hoverBridge",2),jt=B([ui("wa-popup")],jt);var $D=class extends Event{constructor(){super("wa-after-hide",{bubbles:!0,cancelable:!1,composed:!0})}},jD=class extends Event{constructor(){super("wa-after-show",{bubbles:!0,cancelable:!1,composed:!0})}},HD=class extends Event{constructor(e){super("wa-hide",{bubbles:!0,cancelable:!0,composed:!0}),this.detail=e}},UD=class extends Event{constructor(){super("wa-show",{bubbles:!0,cancelable:!0,composed:!0})}};function vd(e,t){return new Promise(n=>{e.addEventListener(t,function r(i){i.target===e&&(e.removeEventListener(t,r),n())})})}function e2(e,t,n){return zD.apply(this,arguments)}function zD(){return(zD=(0,oe.A)(function*(e,t,n){return e.animate(t,n).finished.catch(()=>{})})).apply(this,arguments)}function o0(e,t){return new Promise(n=>{const r=new AbortController,{signal:i}=r;if(e.classList.contains(t))return;e.classList.remove(t),e.classList.add(t);let o=()=>{e.classList.remove(t),n(),r.abort()};e.addEventListener("animationend",o,{once:!0,signal:i}),e.addEventListener("animationcancel",o,{once:!0,signal:i})})}function t2(e){return(e=e.toString().toLowerCase()).indexOf("ms")>-1?parseFloat(e)||0:e.indexOf("s")>-1?1e3*(parseFloat(e)||0):parseFloat(e)||0}var On=(()=>{let e=class extends di{constructor(){super(...arguments),this.placement="top",this.disabled=!1,this.distance=8,this.open=!1,this.skidding=0,this.showDelay=150,this.hideDelay=0,this.trigger="hover focus",this.withoutArrow=!1,this.for=null,this.anchor=null,this.eventController=new AbortController,this.handleBlur=()=>{this.hasTrigger("focus")&&this.hide()},this.handleClick=()=>{this.hasTrigger("click")&&(this.open?this.hide():this.show())},this.handleFocus=()=>{this.hasTrigger("focus")&&this.show()},this.handleDocumentKeyDown=t=>{"Escape"===t.key&&(t.stopPropagation(),this.hide())},this.handleMouseOver=()=>{this.hasTrigger("hover")&&(clearTimeout(this.hoverTimeout),this.hoverTimeout=window.setTimeout(()=>this.show(),this.showDelay))},this.handleMouseOut=()=>{this.hasTrigger("hover")&&(clearTimeout(this.hoverTimeout),this.hoverTimeout=window.setTimeout(()=>this.hide(),this.hideDelay))}}connectedCallback(){super.connectedCallback(),this.eventController.signal.aborted&&(this.eventController=new AbortController),this.open&&(this.open=!1,this.updateComplete.then(()=>{this.open=!0})),this.id||(this.id=function Zq(e=""){return`${e}${((e=21)=>{let t="",n=crypto.getRandomValues(new Uint8Array(e|=0));for(;e--;)t+="useandom-26T198340PX75pxJACKVERYMINDBUSHWOLF_GQZbfghjklqvwyzrict"[63&n[e]];return t})()}`}("wa-tooltip-")),this.for&&this.anchor?(this.anchor=null,this.handleForChange()):this.for&&this.handleForChange()}disconnectedCallback(){super.disconnectedCallback(),document.removeEventListener("keydown",this.handleDocumentKeyDown),this.eventController.abort(),this.anchor&&this.removeFromAriaLabelledBy(this.anchor,this.id)}firstUpdated(){this.body.hidden=!this.open,this.open&&(this.popup.active=!0,this.popup.reposition())}hasTrigger(t){return this.trigger.split(" ").includes(t)}addToAriaLabelledBy(t,n){const i=(t.getAttribute("aria-labelledby")||"").split(/\s+/).filter(Boolean);i.includes(n)||(i.push(n),t.setAttribute("aria-labelledby",i.join(" ")))}removeFromAriaLabelledBy(t,n){const o=(t.getAttribute("aria-labelledby")||"").split(/\s+/).filter(Boolean).filter(a=>a!==n);o.length>0?t.setAttribute("aria-labelledby",o.join(" ")):t.removeAttribute("aria-labelledby")}handleOpenChange(){var t=this;return(0,oe.A)(function*(){if(t.open){if(t.disabled)return;const n=new UD;if(t.dispatchEvent(n),n.defaultPrevented)return void(t.open=!1);document.addEventListener("keydown",t.handleDocumentKeyDown,{signal:t.eventController.signal}),t.body.hidden=!1,t.popup.active=!0,yield o0(t.popup.popup,"show-with-scale"),t.popup.reposition(),t.dispatchEvent(new jD)}else{const n=new HD;if(t.dispatchEvent(n),n.defaultPrevented)return void(t.open=!1);document.removeEventListener("keydown",t.handleDocumentKeyDown),yield o0(t.popup.popup,"hide-with-scale"),t.popup.active=!1,t.body.hidden=!0,t.dispatchEvent(new $D)}})()}handleForChange(){const t=this.getRootNode();if(!t)return;const n=this.for?t.getElementById(this.for):null,r=this.anchor;if(n===r)return;const{signal:i}=this.eventController;n&&(this.addToAriaLabelledBy(n,this.id),n.addEventListener("blur",this.handleBlur,{capture:!0,signal:i}),n.addEventListener("focus",this.handleFocus,{capture:!0,signal:i}),n.addEventListener("click",this.handleClick,{signal:i}),n.addEventListener("mouseover",this.handleMouseOver,{signal:i}),n.addEventListener("mouseout",this.handleMouseOut,{signal:i})),r&&(this.removeFromAriaLabelledBy(r,this.id),r.removeEventListener("blur",this.handleBlur,{capture:!0}),r.removeEventListener("focus",this.handleFocus,{capture:!0}),r.removeEventListener("click",this.handleClick),r.removeEventListener("mouseover",this.handleMouseOver),r.removeEventListener("mouseout",this.handleMouseOut)),this.anchor=n}handleOptionsChange(){var t=this;return(0,oe.A)(function*(){t.hasUpdated&&(yield t.updateComplete,t.popup.reposition())})()}handleDisabledChange(){this.disabled&&this.open&&this.hide()}show(){var t=this;return(0,oe.A)(function*(){if(!t.open)return t.open=!0,vd(t,"wa-after-show")})()}hide(){var t=this;return(0,oe.A)(function*(){if(t.open)return t.open=!1,vd(t,"wa-after-hide")})()}render(){return At` + +
+ +
+
+ `}};return e.css=":host {\n --max-width: 30ch;\n\n /** These styles are added so we don't interfere in the DOM. */\n display: inline-block;\n position: absolute;\n\n /** Defaults for inherited CSS properties */\n color: var(--wa-tooltip-content-color);\n font-size: var(--wa-tooltip-font-size);\n line-height: var(--wa-tooltip-line-height);\n text-align: start;\n white-space: normal;\n}\n\n.tooltip {\n --arrow-size: var(--wa-tooltip-arrow-size);\n --arrow-color: var(--wa-tooltip-background-color);\n}\n\n.tooltip::part(popup) {\n z-index: 1000;\n}\n\n.tooltip[placement^='top']::part(popup) {\n transform-origin: bottom;\n}\n\n.tooltip[placement^='bottom']::part(popup) {\n transform-origin: top;\n}\n\n.tooltip[placement^='left']::part(popup) {\n transform-origin: right;\n}\n\n.tooltip[placement^='right']::part(popup) {\n transform-origin: left;\n}\n\n.body {\n display: block;\n width: max-content;\n max-width: var(--max-width);\n border-radius: var(--wa-tooltip-border-radius);\n background-color: var(--wa-tooltip-background-color);\n border: var(--wa-tooltip-border-width) var(--wa-tooltip-border-style) var(--wa-tooltip-border-color);\n padding: 0.25em 0.5em;\n user-select: none;\n -webkit-user-select: none;\n}\n\n.tooltip::part(arrow) {\n border-bottom: var(--wa-tooltip-border-width) var(--wa-tooltip-border-style) var(--wa-tooltip-border-color);\n border-right: var(--wa-tooltip-border-width) var(--wa-tooltip-border-style) var(--wa-tooltip-border-color);\n}\n",e.dependencies={"wa-popup":jt},e})();B([$n("slot:not([name])")],On.prototype,"defaultSlot",2),B([$n(".body")],On.prototype,"body",2),B([$n("wa-popup")],On.prototype,"popup",2),B([W()],On.prototype,"placement",2),B([W({type:Boolean,reflect:!0})],On.prototype,"disabled",2),B([W({type:Number})],On.prototype,"distance",2),B([W({type:Boolean,reflect:!0})],On.prototype,"open",2),B([W({type:Number})],On.prototype,"skidding",2),B([W({attribute:"show-delay",type:Number})],On.prototype,"showDelay",2),B([W({attribute:"hide-delay",type:Number})],On.prototype,"hideDelay",2),B([W()],On.prototype,"trigger",2),B([W({attribute:"without-arrow",type:Boolean,reflect:!0})],On.prototype,"withoutArrow",2),B([W()],On.prototype,"for",2),B([$i()],On.prototype,"anchor",2),B([Ir("open",{waitUntilFirstUpdate:!0})],On.prototype,"handleOpenChange",1),B([Ir("for")],On.prototype,"handleForChange",1),B([Ir(["distance","placement","skidding"])],On.prototype,"handleOptionsChange",1),B([Ir("disabled")],On.prototype,"handleDisabledChange",1),On=B([ui("wa-tooltip")],On);var n2=class extends Event{constructor(){super("wa-clear",{bubbles:!0,cancelable:!1,composed:!0})}},WD=":host {\n display: flex;\n flex-direction: column;\n}\n\n/* Label */\n:is([part~='form-control-label'], [part~='label']):has(*:not(:empty)) {\n display: inline-flex;\n color: var(--wa-form-control-label-color);\n font-weight: var(--wa-form-control-label-font-weight);\n line-height: var(--wa-form-control-label-line-height);\n margin-block-end: 0.5em;\n}\n\n:host([required]) :is([part~='form-control-label'], [part~='label'])::after {\n content: var(--wa-form-control-required-content);\n margin-inline-start: var(--wa-form-control-required-content-offset);\n color: var(--wa-form-control-required-content-color);\n}\n\n/* Help text */\n[part~='hint'] {\n display: block;\n color: var(--wa-form-control-hint-color);\n font-weight: var(--wa-form-control-hint-font-weight);\n line-height: var(--wa-form-control-hint-line-height);\n margin-block-start: 0.5em;\n font-size: var(--wa-font-size-smaller);\n line-height: var(--wa-form-control-label-line-height);\n\n &:not(.has-slotted) {\n display: none;\n }\n}\n";const GD=QE(class extends JE{constructor(e){if(super(e),3!==e.type&&1!==e.type&&4!==e.type)throw Error("The `live` directive is not allowed on child or event bindings");if(!(e=>void 0===e.strings)(e))throw Error("`live` bindings can only contain a single expression")}render(e){return e}update(e,[t]){if(t===Vi||t===hn)return t;const n=e.element,r=e.name;if(3===e.type){if(t===n[r])return Vi}else if(4===e.type){if(!!t===n.hasAttribute(r))return Vi}else if(1===e.type&&n.getAttribute(r)===t+"")return Vi;return((e,t=_G)=>{e._$AH=t})(e),t}});var Je=class extends Gr{constructor(){super(...arguments),this.assumeInteractionOn=["blur","input"],this.hasSlotController=new mp(this,"hint","label"),this.localize=new za(this),this.title="",this.type="text",this._value=null,this.defaultValue=this.getAttribute("value")||null,this.size="medium",this.appearance="outlined",this.pill=!1,this.label="",this.hint="",this.withClear=!1,this.placeholder="",this.readonly=!1,this.passwordToggle=!1,this.passwordVisible=!1,this.withoutSpinButtons=!1,this.form=null,this.required=!1,this.spellcheck=!0,this.withLabel=!1,this.withHint=!1}static get validators(){return[...super.validators,GR()]}get value(){return this.valueHasChanged?this._value:this._value??this.defaultValue}set value(e){this._value!==e&&(this.valueHasChanged=!0,this._value=e)}handleChange(e){this.value=this.input.value,this.relayNativeEvent(e,{bubbles:!0,composed:!0})}handleClearClick(e){e.preventDefault(),""!==this.value&&(this.value="",this.updateComplete.then(()=>{this.dispatchEvent(new n2),this.dispatchEvent(new InputEvent("input",{bubbles:!0,composed:!0})),this.dispatchEvent(new Event("change",{bubbles:!0,composed:!0}))})),this.input.focus()}handleInput(){this.value=this.input.value}handleKeyDown(e){!function Qq(e,t){"Enter"===e.key&&!(e.metaKey||e.ctrlKey||e.shiftKey||e.altKey)&&setTimeout(()=>{!e.defaultPrevented&&!e.isComposing&&function Jq(e){let t=null;if("form"in e&&(t=e.form),!t&&"getForm"in e&&(t=e.getForm()),!t)return;const n=[...t.elements];if(1===n.length)return void t.requestSubmit(null);const r=n.find(i=>"submit"===i.type&&!i.matches(":disabled"));r&&(["input","button"].includes(r.localName)?t.requestSubmit(r):r.click())}(t)})}(e,this)}handlePasswordToggle(){this.passwordVisible=!this.passwordVisible}updated(e){super.updated(e),e.has("value")&&this.customStates.set("blank",!this.value)}handleStepChange(){this.input.step=String(this.step),this.updateValidity()}focus(e){this.input.focus(e)}blur(){this.input.blur()}select(){this.input.select()}setSelectionRange(e,t,n="none"){this.input.setSelectionRange(e,t,n)}setRangeText(e,t,n,r="preserve"){this.input.setRangeText(e,t??this.input.selectionStart,n??this.input.selectionEnd,r),this.value!==this.input.value&&(this.value=this.input.value)}showPicker(){"showPicker"in HTMLInputElement.prototype&&this.input.showPicker()}stepUp(){this.input.stepUp(),this.value!==this.input.value&&(this.value=this.input.value)}stepDown(){this.input.stepDown(),this.value!==this.input.value&&(this.value=this.input.value)}formResetCallback(){this.value=this.defaultValue,super.formResetCallback()}render(){const e=this.hasUpdated?this.hasSlotController.test("label"):this.withLabel,t=this.hasUpdated?this.hasSlotController.test("hint"):this.withHint,r=!!this.hint||!!t,o=this.hasUpdated&&this.withClear&&!this.disabled&&!this.readonly&&("number"==typeof this.value||this.value&&this.value.length>0);return At` + + +
+ + + + + ${o?At` + + `:""} + ${this.passwordToggle&&!this.disabled?At` + + `:""} + + +
+ + ${this.hint} + `}};Je.css=[ad,WD,":host {\n border-width: 0;\n}\n\n.text-field {\n flex: auto;\n display: flex;\n align-items: stretch;\n justify-content: start;\n position: relative;\n transition: inherit;\n height: var(--wa-form-control-height);\n border-color: var(--wa-form-control-border-color);\n border-radius: var(--wa-form-control-border-radius);\n border-style: var(--wa-form-control-border-style);\n border-width: var(--wa-form-control-border-width);\n cursor: text;\n color: var(--wa-form-control-value-color);\n font-size: var(--wa-form-control-value-font-size);\n font-family: inherit;\n font-weight: var(--wa-form-control-value-font-weight);\n line-height: var(--wa-form-control-value-line-height);\n vertical-align: middle;\n width: 100%;\n transition:\n background-color var(--wa-transition-normal),\n border var(--wa-transition-normal),\n outline var(--wa-transition-fast);\n transition-timing-function: var(--wa-transition-easing);\n background-color: var(--wa-form-control-background-color);\n box-shadow: var(--box-shadow);\n padding: 0 var(--wa-form-control-padding-inline);\n\n &:focus-within {\n outline: var(--wa-focus-ring);\n outline-offset: var(--wa-focus-ring-offset);\n }\n\n /* Style disabled inputs */\n &:has(:disabled) {\n cursor: not-allowed;\n opacity: 0.5;\n }\n}\n\n/* Appearance modifiers */\n:host([appearance='outlined']) .text-field {\n background-color: var(--wa-form-control-background-color);\n border-color: var(--wa-form-control-border-color);\n}\n\n:host([appearance='filled']) .text-field {\n background-color: var(--wa-color-neutral-fill-quiet);\n border-color: var(--wa-color-neutral-fill-quiet);\n}\n\n:host([appearance='filled-outlined']) .text-field {\n background-color: var(--wa-color-neutral-fill-quiet);\n border-color: var(--wa-form-control-border-color);\n}\n\n:host([pill]) .text-field {\n border-radius: var(--wa-border-radius-pill) !important;\n}\n\n.text-field {\n /* Show autofill styles over the entire text field, not just the native */\n &:has(:autofill),\n &:has(:-webkit-autofill) {\n background-color: var(--wa-color-brand-fill-quiet) !important;\n }\n\n input,\n textarea {\n /*\n Fixes an alignment issue with placeholders.\n https://github.com/shoelace-style/webawesome/issues/342\n */\n height: 100%;\n\n padding: 0;\n border: none;\n outline: none;\n box-shadow: none;\n margin: 0;\n cursor: inherit;\n -webkit-appearance: none;\n font: inherit;\n\n /* Turn off Safari's autofill styles */\n &:-webkit-autofill,\n &:-webkit-autofill:hover,\n &:-webkit-autofill:focus,\n &:-webkit-autofill:active {\n -webkit-background-clip: text;\n background-color: transparent;\n -webkit-text-fill-color: inherit;\n }\n }\n}\n\ninput {\n flex: 1 1 auto;\n min-width: 0;\n height: 100%;\n transition: inherit;\n\n /* prettier-ignore */\n background-color: rgb(118 118 118 / 0); /* ensures proper placeholder styles in webkit's date input */\n height: calc(var(--wa-form-control-height) - var(--border-width) * 2);\n padding-block: 0;\n color: inherit;\n\n &:autofill {\n &,\n &:hover,\n &:focus,\n &:active {\n box-shadow: none;\n caret-color: var(--wa-form-control-value-color);\n }\n }\n\n &::placeholder {\n color: var(--wa-form-control-placeholder-color);\n user-select: none;\n -webkit-user-select: none;\n }\n\n &::-webkit-search-decoration,\n &::-webkit-search-cancel-button,\n &::-webkit-search-results-button,\n &::-webkit-search-results-decoration {\n -webkit-appearance: none;\n }\n\n &:focus {\n outline: none;\n }\n}\n\ntextarea {\n &:autofill {\n &,\n &:hover,\n &:focus,\n &:active {\n box-shadow: none;\n caret-color: var(--wa-form-control-value-color);\n }\n }\n\n &::placeholder {\n color: var(--wa-form-control-placeholder-color);\n user-select: none;\n -webkit-user-select: none;\n }\n}\n\n.start,\n.end {\n display: inline-flex;\n flex: 0 0 auto;\n align-items: center;\n cursor: default;\n\n &::slotted(wa-icon) {\n color: var(--wa-color-neutral-on-quiet);\n }\n}\n\n.start::slotted(*) {\n margin-inline-end: var(--wa-form-control-padding-inline);\n}\n\n.end::slotted(*) {\n margin-inline-start: var(--wa-form-control-padding-inline);\n}\n\n/*\n * Clearable + Password Toggle\n */\n\n.clear,\n.password-toggle {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n font-size: inherit;\n color: var(--wa-color-neutral-on-quiet);\n border: none;\n background: none;\n padding: 0;\n transition: var(--wa-transition-normal) color;\n cursor: pointer;\n margin-inline-start: var(--wa-form-control-padding-inline);\n\n @media (hover: hover) {\n &:hover {\n color: color-mix(in oklab, currentColor, var(--wa-color-mix-hover));\n }\n }\n\n &:active {\n color: color-mix(in oklab, currentColor, var(--wa-color-mix-active));\n }\n\n &:focus {\n outline: none;\n }\n}\n\n/* Don't show the browser's password toggle in Edge */\n::-ms-reveal {\n display: none;\n}\n\n/* Hide the built-in number spinner */\n:host([without-spin-buttons]) input[type='number'] {\n -moz-appearance: textfield;\n\n &::-webkit-outer-spin-button,\n &::-webkit-inner-spin-button {\n -webkit-appearance: none;\n display: none;\n }\n}\n"],Je.shadowRootOptions={...Gr.shadowRootOptions,delegatesFocus:!0},B([$n("input")],Je.prototype,"input",2),B([W()],Je.prototype,"title",2),B([W({reflect:!0})],Je.prototype,"type",2),B([$i()],Je.prototype,"value",1),B([W({attribute:"value",reflect:!0})],Je.prototype,"defaultValue",2),B([W({reflect:!0})],Je.prototype,"size",2),B([W({reflect:!0})],Je.prototype,"appearance",2),B([W({type:Boolean,reflect:!0})],Je.prototype,"pill",2),B([W()],Je.prototype,"label",2),B([W({attribute:"hint"})],Je.prototype,"hint",2),B([W({attribute:"with-clear",type:Boolean})],Je.prototype,"withClear",2),B([W()],Je.prototype,"placeholder",2),B([W({type:Boolean,reflect:!0})],Je.prototype,"readonly",2),B([W({attribute:"password-toggle",type:Boolean})],Je.prototype,"passwordToggle",2),B([W({attribute:"password-visible",type:Boolean})],Je.prototype,"passwordVisible",2),B([W({attribute:"without-spin-buttons",type:Boolean})],Je.prototype,"withoutSpinButtons",2),B([W({reflect:!0})],Je.prototype,"form",2),B([W({type:Boolean,reflect:!0})],Je.prototype,"required",2),B([W()],Je.prototype,"pattern",2),B([W({type:Number})],Je.prototype,"minlength",2),B([W({type:Number})],Je.prototype,"maxlength",2),B([W()],Je.prototype,"min",2),B([W()],Je.prototype,"max",2),B([W()],Je.prototype,"step",2),B([W()],Je.prototype,"autocapitalize",2),B([W()],Je.prototype,"autocorrect",2),B([W()],Je.prototype,"autocomplete",2),B([W({type:Boolean})],Je.prototype,"autofocus",2),B([W()],Je.prototype,"enterkeyhint",2),B([W({type:Boolean,converter:{fromAttribute:e=>!(!e||"false"===e),toAttribute:e=>e?"true":"false"}})],Je.prototype,"spellcheck",2),B([W()],Je.prototype,"inputmode",2),B([W({attribute:"with-label",type:Boolean})],Je.prototype,"withLabel",2),B([W({attribute:"with-hint",type:Boolean})],Je.prototype,"withHint",2),B([Ir("step",{waitUntilFirstUpdate:!0})],Je.prototype,"handleStepChange",1),Je=B([ui("wa-input")],Je);var r2=(e={})=>{let{validationElement:t,validationProperty:n}=e;t||(t=Object.assign(document.createElement("input"),{required:!0})),n||(n="value");const r={observedAttributes:["required"],message:t.validationMessage,checkValidity(i){const o={message:"",isValid:!0,invalidKeys:[]};return(i.required??i.hasAttribute("required"))&&!i[n]&&(o.message="function"==typeof r.message?r.message(i):r.message||"",o.isValid=!1,o.invalidKeys.push("valueMissing")),o}};return r};class YD extends JE{constructor(t){if(super(t),this.it=hn,2!==t.type)throw Error(this.constructor.directiveName+"() can only be used in child bindings")}render(t){if(t===hn||null==t)return this._t=void 0,this.it=t;if(t===Vi)return t;if("string"!=typeof t)throw Error(this.constructor.directiveName+"() called with a non-string value");if(t===this.it)return this._t;this.it=t;const n=[t];return n.raw=n,this._t={_$litType$:this.constructor.resultType,strings:n,values:[]}}}YD.directiveName="unsafeHTML",YD.resultType=1;const oY=QE(YD);var at=(()=>{let e=class extends Gr{constructor(){super(...arguments),this.assumeInteractionOn=["blur","input"],this.hasSlotController=new mp(this,"hint","label"),this.localize=new za(this),this.typeToSelectString="",this.displayLabel="",this.selectedOptions=[],this.name="",this._defaultValue=null,this.size="medium",this.placeholder="",this.multiple=!1,this.maxOptionsVisible=3,this.disabled=!1,this.withClear=!1,this.open=!1,this.appearance="outlined",this.pill=!1,this.label="",this.placement="bottom",this.hint="",this.withLabel=!1,this.withHint=!1,this.form=null,this.required=!1,this.getTag=t=>At` + + ${t.label} + + `,this.handleDocumentFocusIn=t=>{const n=t.composedPath();this&&!n.includes(this)&&this.hide()},this.handleDocumentKeyDown=t=>{const n=t.target,r=null!==n.closest('[part~="clear-button"]'),i=null!==n.closest("wa-button");if(!r&&!i){if("Escape"===t.key&&this.open&&(t.preventDefault(),t.stopPropagation(),this.hide(),this.displayInput.focus({preventScroll:!0})),"Enter"===t.key||" "===t.key&&""===this.typeToSelectString)return t.preventDefault(),t.stopImmediatePropagation(),this.open?void(this.currentOption&&!this.currentOption.disabled&&(this.valueHasChanged=!0,this.hasInteracted=!0,this.multiple?this.toggleOptionSelection(this.currentOption):this.setSelectedOptions(this.currentOption),this.updateComplete.then(()=>{this.dispatchEvent(new InputEvent("input",{bubbles:!0,composed:!0})),this.dispatchEvent(new Event("change",{bubbles:!0,composed:!0}))}),this.multiple||(this.hide(),this.displayInput.focus({preventScroll:!0})))):void this.show();if(["ArrowUp","ArrowDown","Home","End"].includes(t.key)){const o=this.getAllOptions(),a=o.indexOf(this.currentOption);let u=Math.max(0,a);if(t.preventDefault(),!this.open&&(this.show(),this.currentOption))return;"ArrowDown"===t.key?(u=a+1,u>o.length-1&&(u=0)):"ArrowUp"===t.key?(u=a-1,u<0&&(u=o.length-1)):"Home"===t.key?u=0:"End"===t.key&&(u=o.length-1),this.setCurrentOption(o[u])}if(1===t.key?.length||"Backspace"===t.key){const o=this.getAllOptions();if(t.metaKey||t.ctrlKey||t.altKey)return;if(!this.open){if("Backspace"===t.key)return;this.show()}t.stopPropagation(),t.preventDefault(),clearTimeout(this.typeToSelectTimeout),this.typeToSelectTimeout=window.setTimeout(()=>this.typeToSelectString="",1e3),"Backspace"===t.key?this.typeToSelectString=this.typeToSelectString.slice(0,-1):this.typeToSelectString+=t.key.toLowerCase();for(const a of o)if(a.label.toLowerCase().startsWith(this.typeToSelectString)){this.setCurrentOption(a);break}}}},this.handleDocumentMouseDown=t=>{const n=t.composedPath();this&&!n.includes(this)&&this.hide()}}static get validators(){const t=[r2({validationElement:Object.assign(document.createElement("select"),{required:!0})})];return[...super.validators,...t]}get validationTarget(){return this.valueInput}set defaultValue(t){this._defaultValue=this.convertDefaultValue(t)}get defaultValue(){return this.convertDefaultValue(this._defaultValue)}convertDefaultValue(t){return!(this.multiple||this.hasAttribute("multiple"))&&Array.isArray(t)&&(t=t[0]),t}set value(t){let n=this.value;t instanceof FormData&&(t=t.getAll(this.name)),null!=t&&!Array.isArray(t)&&(t=[t]),this._value=t??null,this.value!==n&&(this.valueHasChanged=!0,this.requestUpdate("value",n))}get value(){let t=this._value??this.defaultValue??null;null!=t&&(t=Array.isArray(t)?t:[t]),this.optionValues=null==t?new Set(null):new Set(this.getAllOptions().filter(r=>!r.disabled).map(r=>r.value));let n=t;return null!=t&&(n=t.filter(r=>this.optionValues.has(r)),n=this.multiple?n:n[0],n=n??null),n}connectedCallback(){super.connectedCallback(),this.handleDefaultSlotChange(),this.open=!1}updateDefaultValue(){const n=this.getAllOptions().filter(r=>r.hasAttribute("selected")||r.defaultSelected);if(n.length>0){const r=n.map(i=>i.value);this._defaultValue=this.multiple?r:r[0]}this.hasAttribute("value")&&(this._defaultValue=this.getAttribute("value")||null)}addOpenListeners(){document.addEventListener("focusin",this.handleDocumentFocusIn),document.addEventListener("keydown",this.handleDocumentKeyDown),document.addEventListener("mousedown",this.handleDocumentMouseDown),this.getRootNode()!==document&&this.getRootNode().addEventListener("focusin",this.handleDocumentFocusIn)}removeOpenListeners(){document.removeEventListener("focusin",this.handleDocumentFocusIn),document.removeEventListener("keydown",this.handleDocumentKeyDown),document.removeEventListener("mousedown",this.handleDocumentMouseDown),this.getRootNode()!==document&&this.getRootNode().removeEventListener("focusin",this.handleDocumentFocusIn)}handleFocus(){this.displayInput.setSelectionRange(0,0)}handleLabelClick(){this.displayInput.focus()}handleComboboxClick(t){t.preventDefault()}handleComboboxMouseDown(t){const r=t.composedPath().some(i=>i instanceof Element&&"wa-button"===i.tagName.toLowerCase());this.disabled||r||(t.preventDefault(),this.displayInput.focus({preventScroll:!0}),this.open=!this.open)}handleComboboxKeyDown(t){t.stopPropagation(),this.handleDocumentKeyDown(t)}handleClearClick(t){t.stopPropagation(),null!==this.value&&(this.setSelectedOptions([]),this.displayInput.focus({preventScroll:!0}),this.updateComplete.then(()=>{this.dispatchEvent(new n2),this.dispatchEvent(new InputEvent("input",{bubbles:!0,composed:!0})),this.dispatchEvent(new Event("change",{bubbles:!0,composed:!0}))}))}handleClearMouseDown(t){t.stopPropagation(),t.preventDefault()}handleOptionClick(t){const r=t.target.closest("wa-option");r&&!r.disabled&&(this.hasInteracted=!0,this.valueHasChanged=!0,this.multiple?this.toggleOptionSelection(r):this.setSelectedOptions(r),this.updateComplete.then(()=>this.displayInput.focus({preventScroll:!0})),this.requestUpdate("value"),this.updateComplete.then(()=>{this.dispatchEvent(new InputEvent("input",{bubbles:!0,composed:!0})),this.dispatchEvent(new Event("change",{bubbles:!0,composed:!0}))}),this.multiple||(this.hide(),this.displayInput.focus({preventScroll:!0})))}handleDefaultSlotChange(){customElements.get("wa-option")||customElements.whenDefined("wa-option").then(()=>this.handleDefaultSlotChange());const t=this.getAllOptions();this.optionValues=void 0,this.updateDefaultValue();let n=this.value;if(null==n||!this.valueHasChanged&&!this.hasInteracted)return void this.selectionChanged();Array.isArray(n)||(n=[n]);const r=t.filter(i=>n.includes(i.value));this.setSelectedOptions(r)}handleTagRemove(t,n){if(t.stopPropagation(),this.disabled)return;let r=n;if(!r){const i=t.target.closest("wa-tag[part~=tag]");if(i){const o=this.shadowRoot?.querySelector('[part="tags"]');if(o){const u=Array.from(o.children).indexOf(i);u>=0&&u{this.dispatchEvent(new InputEvent("input",{bubbles:!0,composed:!0})),this.dispatchEvent(new Event("change",{bubbles:!0,composed:!0}))}))}getAllOptions(){return this?.querySelectorAll?[...this.querySelectorAll("wa-option")]:[]}getFirstOption(){return this.querySelector("wa-option")}setCurrentOption(t){this.getAllOptions().forEach(r=>{r.current=!1,r.tabIndex=-1}),t&&(this.currentOption=t,t.current=!0,t.tabIndex=0,t.focus())}setSelectedOptions(t){const n=this.getAllOptions(),r=Array.isArray(t)?t:[t];n.forEach(i=>{r.includes(i)||(i.selected=!1)}),r.length&&r.forEach(i=>i.selected=!0),this.selectionChanged()}toggleOptionSelection(t,n){t.selected=!0===n||!1===n?n:!t.selected,this.selectionChanged()}selectionChanged(){const t=this.getAllOptions();this.selectedOptions=t.filter(r=>{if(!this.hasInteracted&&!this.valueHasChanged){const i=this.defaultValue,o=Array.isArray(i)?i:[i];return r.hasAttribute("selected")||r.defaultSelected||r.selected||o?.includes(r.value)}return r.selected});let n=new Set(this.selectedOptions.map(r=>r.value));if(n.size>0||this._value){const r=this._value;if(null==this._value){let i=this.defaultValue??[];this._value=Array.isArray(i)?i:[i]}this._value=this._value?.filter(i=>!this.optionValues?.has(i))??null,this._value?.unshift(...n),this.requestUpdate("value",r)}if(this.multiple)this.displayLabel=this.placeholder&&!this.value?.length?"":this.localize.term("numOptionsSelected",this.selectedOptions.length);else{const r=this.selectedOptions[0];this.displayLabel=r?.label??""}this.updateComplete.then(()=>{this.updateValidity()})}get tags(){return this.selectedOptions.map((t,n)=>{if(n+${this.selectedOptions.length-n} + `:null})}updated(t){super.updated(t),t.has("value")&&this.customStates.set("blank",!this.value)}handleDisabledChange(){this.disabled&&this.open&&(this.open=!1)}handleValueChange(){const t=this.getAllOptions(),n=Array.isArray(this.value)?this.value:[this.value],r=t.filter(i=>n.includes(i.value));this.setSelectedOptions(r),this.updateValidity()}handleOpenChange(){var t=this;return(0,oe.A)(function*(){if(t.open&&!t.disabled){t.setCurrentOption(t.selectedOptions[0]||t.getFirstOption());const n=new UD;if(t.dispatchEvent(n),n.defaultPrevented)return void(t.open=!1);t.addOpenListeners(),t.listbox.hidden=!1,t.popup.active=!0,requestAnimationFrame(()=>{t.setCurrentOption(t.currentOption)}),yield o0(t.popup.popup,"show"),t.currentOption&&function iY(e,t,n="vertical",r="smooth"){const i=function tY(e,t){return{top:Math.round(e.getBoundingClientRect().top-t.getBoundingClientRect().top),left:Math.round(e.getBoundingClientRect().left-t.getBoundingClientRect().left)}}(e,t),o=i.top+t.scrollTop,a=i.left+t.scrollLeft,f=t.scrollLeft+t.offsetWidth,y=t.scrollTop,w=t.scrollTop+t.offsetHeight;("horizontal"===n||"both"===n)&&(af&&t.scrollTo({left:a-t.offsetWidth+e.clientWidth,behavior:r})),("vertical"===n||"both"===n)&&(ow&&t.scrollTo({top:o-t.offsetHeight+e.clientHeight,behavior:r}))}(t.currentOption,t.listbox,"vertical","auto"),t.dispatchEvent(new jD)}else{const n=new HD;if(t.dispatchEvent(n),n.defaultPrevented)return void(t.open=!1);t.removeOpenListeners(),yield o0(t.popup.popup,"hide"),t.listbox.hidden=!0,t.popup.active=!1,t.dispatchEvent(new $D)}})()}show(){var t=this;return(0,oe.A)(function*(){if(!t.open&&!t.disabled)return t.open=!0,vd(t,"wa-after-show");t.open=!1})()}hide(){var t=this;return(0,oe.A)(function*(){if(t.open&&!t.disabled)return t.open=!1,vd(t,"wa-after-hide");t.open=!1})()}focus(t){this.displayInput.focus(t)}blur(){this.displayInput.blur()}formResetCallback(){this.value=this.defaultValue,super.formResetCallback(),this.handleValueChange(),this.updateComplete.then(()=>{this.dispatchEvent(new InputEvent("input",{bubbles:!0,composed:!0})),this.dispatchEvent(new Event("change",{bubbles:!0,composed:!0}))})}render(){const t=this.hasUpdated?this.hasSlotController.test("label"):this.withLabel,n=this.hasUpdated?this.hasSlotController.test("hint"):this.withHint,r=!!this.label||!!t,i=!!this.hint||!!n,o=!!this.hasUpdated&&this.withClear&&!this.disabled&&this.value&&this.value.length>0,a=!(!this.placeholder||this.value&&0!==this.value.length);return At` +
+ + +
+ +
+ + + + + + ${this.multiple&&this.hasUpdated?At`
${this.tags}
`:""} + + this.focus()} + /> + + ${o?At` + + `:""} + + + + + + +
+ +
+ +
+
+
+ + ${this.hint} +
+ `}};return e.css=[":host {\n --tag-max-size: 10ch;\n --show-duration: 100ms;\n --hide-duration: 100ms;\n}\n\n/* Add ellipses to multi select options */\n:host wa-tag::part(content) {\n display: initial;\n white-space: nowrap;\n text-overflow: ellipsis;\n overflow: hidden;\n max-width: var(--tag-max-size);\n}\n\n:host .disabled [part~='combobox'] {\n opacity: 0.5;\n cursor: not-allowed;\n outline: none;\n}\n\n:host .enabled:is(.open, :focus-within) [part~='combobox'] {\n outline: var(--wa-focus-ring);\n outline-offset: var(--wa-focus-ring-offset);\n}\n\n/** The popup */\n.select {\n flex: 1 1 auto;\n display: inline-flex;\n width: 100%;\n position: relative;\n vertical-align: middle;\n\n /* Pass through from select to the popup */\n --show-duration: inherit;\n --hide-duration: inherit;\n\n &::part(popup) {\n z-index: 900;\n }\n\n &[data-current-placement^='top']::part(popup) {\n transform-origin: bottom;\n }\n\n &[data-current-placement^='bottom']::part(popup) {\n transform-origin: top;\n }\n}\n\n/* Combobox */\n.combobox {\n flex: 1;\n display: flex;\n width: 100%;\n min-width: 0;\n align-items: center;\n justify-content: start;\n\n min-height: var(--wa-form-control-height);\n\n background-color: var(--wa-form-control-background-color);\n border-color: var(--wa-form-control-border-color);\n border-radius: var(--wa-form-control-border-radius);\n border-style: var(--wa-form-control-border-style);\n border-width: var(--wa-form-control-border-width);\n color: var(--wa-form-control-value-color);\n cursor: pointer;\n font-family: inherit;\n font-weight: var(--wa-form-control-value-font-weight);\n line-height: var(--wa-form-control-value-line-height);\n overflow: hidden;\n padding: 0 var(--wa-form-control-padding-inline);\n position: relative;\n vertical-align: middle;\n width: 100%;\n transition:\n background-color var(--wa-transition-normal),\n border var(--wa-transition-normal),\n outline var(--wa-transition-fast);\n transition-timing-function: var(--wa-transition-easing);\n\n :host([multiple]) .select:not(.placeholder-visible) & {\n padding-inline-start: 0;\n padding-block: calc(var(--wa-form-control-height) * 0.1 - var(--wa-form-control-border-width));\n }\n\n /* Pills */\n :host([pill]) & {\n border-radius: var(--wa-border-radius-pill);\n }\n}\n\n/* Appearance modifiers */\n:host([appearance='outlined']) .combobox {\n background-color: var(--wa-form-control-background-color);\n border-color: var(--wa-form-control-border-color);\n}\n\n:host([appearance='filled']) .combobox {\n background-color: var(--wa-color-neutral-fill-quiet);\n border-color: var(--wa-color-neutral-fill-quiet);\n}\n\n:host([appearance='filled-outlined']) .combobox {\n background-color: var(--wa-color-neutral-fill-quiet);\n border-color: var(--wa-form-control-border-color);\n}\n\n.display-input {\n position: relative;\n width: 100%;\n font: inherit;\n border: none;\n background: none;\n line-height: var(--wa-form-control-value-line-height);\n color: var(--wa-form-control-value-color);\n cursor: inherit;\n overflow: hidden;\n padding: 0;\n margin: 0;\n -webkit-appearance: none;\n\n &:focus {\n outline: none;\n }\n\n &::placeholder {\n color: var(--wa-form-control-placeholder-color);\n }\n}\n\n/* Visually hide the display input when multiple is enabled */\n:host([multiple]) .select:not(.placeholder-visible) .display-input {\n position: absolute;\n z-index: -1;\n top: 0;\n left: 0;\n width: 100%;\n height: 100%;\n opacity: 0;\n}\n\n.value-input {\n position: absolute;\n z-index: -1;\n top: 0;\n left: 0;\n width: 100%;\n height: 100%;\n opacity: 0;\n padding: 0;\n margin: 0;\n}\n\n.tags {\n display: flex;\n flex: 1;\n align-items: center;\n flex-wrap: wrap;\n margin-inline-start: 0.25em;\n gap: 0.25em;\n\n &::slotted(wa-tag) {\n cursor: pointer !important;\n }\n\n .disabled &,\n .disabled &::slotted(wa-tag) {\n cursor: not-allowed !important;\n }\n}\n\n/* Start and End */\n\n.start,\n.end {\n flex: 0;\n display: inline-flex;\n align-items: center;\n color: var(--wa-color-neutral-on-quiet);\n}\n\n.end::slotted(*) {\n margin-inline-start: var(--wa-form-control-padding-inline);\n}\n\n.start::slotted(*) {\n margin-inline-end: var(--wa-form-control-padding-inline);\n}\n\n:host([multiple]) .start::slotted(*) {\n margin-inline: var(--wa-form-control-padding-inline);\n}\n\n/* Clear button */\n[part~='clear-button'] {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n font-size: inherit;\n color: var(--wa-color-neutral-on-quiet);\n border: none;\n background: none;\n padding: 0;\n transition: color var(--wa-transition-normal);\n cursor: pointer;\n margin-inline-start: var(--wa-form-control-padding-inline);\n\n &:focus {\n outline: none;\n }\n\n @media (hover: hover) {\n &:hover {\n color: color-mix(in oklab, currentColor, var(--wa-color-mix-hover));\n }\n }\n\n &:active {\n color: color-mix(in oklab, currentColor, var(--wa-color-mix-active));\n }\n}\n\n/* Expand icon */\n.expand-icon {\n flex: 0 0 auto;\n display: flex;\n align-items: center;\n color: var(--wa-color-neutral-on-quiet);\n transition: rotate var(--wa-transition-slow) ease;\n rotate: 0deg;\n margin-inline-start: var(--wa-form-control-padding-inline);\n\n .open & {\n rotate: -180deg;\n }\n}\n\n/* Listbox */\n.listbox {\n display: block;\n position: relative;\n font: inherit;\n box-shadow: var(--wa-shadow-m);\n background: var(--wa-color-surface-raised);\n border-color: var(--wa-color-surface-border);\n border-radius: var(--wa-border-radius-m);\n border-style: var(--wa-border-style);\n border-width: var(--wa-border-width-s);\n padding-block: 0.5em;\n padding-inline: 0;\n overflow: auto;\n overscroll-behavior: none;\n\n /* Make sure it adheres to the popup's auto size */\n max-width: var(--auto-size-available-width);\n max-height: var(--auto-size-available-height);\n\n &::slotted(wa-divider) {\n --spacing: 0.5em;\n }\n}\n\nslot:not([name])::slotted(small) {\n display: block;\n font-size: var(--wa-font-size-smaller);\n font-weight: var(--wa-font-weight-semibold);\n color: var(--wa-color-text-quiet);\n padding-block: 0.5em;\n padding-inline: 2.25em;\n}\n",WD,ad],e})();B([$n(".select")],at.prototype,"popup",2),B([$n(".combobox")],at.prototype,"combobox",2),B([$n(".display-input")],at.prototype,"displayInput",2),B([$n(".value-input")],at.prototype,"valueInput",2),B([$n(".listbox")],at.prototype,"listbox",2),B([$i()],at.prototype,"displayLabel",2),B([$i()],at.prototype,"currentOption",2),B([$i()],at.prototype,"selectedOptions",2),B([$i()],at.prototype,"optionValues",2),B([W()],at.prototype,"name",2),B([W({attribute:!1})],at.prototype,"defaultValue",1),B([W({attribute:"value",reflect:!1})],at.prototype,"value",1),B([W({reflect:!0})],at.prototype,"size",2),B([W()],at.prototype,"placeholder",2),B([W({type:Boolean,reflect:!0})],at.prototype,"multiple",2),B([W({attribute:"max-options-visible",type:Number})],at.prototype,"maxOptionsVisible",2),B([W({type:Boolean})],at.prototype,"disabled",2),B([W({attribute:"with-clear",type:Boolean})],at.prototype,"withClear",2),B([W({type:Boolean,reflect:!0})],at.prototype,"open",2),B([W({reflect:!0})],at.prototype,"appearance",2),B([W({type:Boolean,reflect:!0})],at.prototype,"pill",2),B([W()],at.prototype,"label",2),B([W({reflect:!0})],at.prototype,"placement",2),B([W({attribute:"hint"})],at.prototype,"hint",2),B([W({attribute:"with-label",type:Boolean})],at.prototype,"withLabel",2),B([W({attribute:"with-hint",type:Boolean})],at.prototype,"withHint",2),B([W({reflect:!0})],at.prototype,"form",2),B([W({type:Boolean,reflect:!0})],at.prototype,"required",2),B([W({attribute:!1})],at.prototype,"getTag",2),B([Ir("disabled",{waitUntilFirstUpdate:!0})],at.prototype,"handleDisabledChange",1),B([Ir("value",{waitUntilFirstUpdate:!0})],at.prototype,"handleValueChange",1),B([Ir("open",{waitUntilFirstUpdate:!0})],at.prototype,"handleOpenChange",1),at=B([ui("wa-select")],at);var aY=class extends Event{constructor(){super("wa-remove",{bubbles:!0,cancelable:!1,composed:!0})}},sc=(()=>{let e=class extends di{constructor(){super(...arguments),this.localize=new za(this),this.variant="neutral",this.appearance="filled-outlined",this.size="medium",this.pill=!1,this.withRemove=!1}handleRemoveClick(){this.dispatchEvent(new aY)}render(){return At` + + + ${this.withRemove?At` + + + + `:""} + `}};return e.css=["@layer wa-component {\n :host {\n display: inline-flex;\n gap: 0.5em;\n border-radius: var(--wa-border-radius-m);\n align-items: center;\n background-color: var(--wa-color-fill-quiet, var(--wa-color-neutral-fill-quiet));\n border-color: var(--wa-color-border-normal, var(--wa-color-neutral-border-normal));\n border-style: var(--wa-border-style);\n border-width: var(--wa-border-width-s);\n color: var(--wa-color-on-quiet, var(--wa-color-neutral-on-quiet));\n font-size: inherit;\n line-height: 1;\n white-space: nowrap;\n user-select: none;\n -webkit-user-select: none;\n height: calc(var(--wa-form-control-height) * 0.8);\n line-height: calc(var(--wa-form-control-height) - var(--wa-form-control-border-width) * 2);\n padding: 0 0.75em;\n }\n\n /* Appearance modifiers */\n :host([appearance='outlined']) {\n color: var(--wa-color-on-quiet, var(--wa-color-neutral-on-quiet));\n background-color: transparent;\n border-color: var(--wa-color-border-loud, var(--wa-color-neutral-border-loud));\n }\n\n :host([appearance='filled']) {\n color: var(--wa-color-on-quiet, var(--wa-color-neutral-on-quiet));\n background-color: var(--wa-color-fill-quiet, var(--wa-color-neutral-fill-quiet));\n border-color: transparent;\n }\n\n :host([appearance='filled-outlined']) {\n color: var(--wa-color-on-quiet, var(--wa-color-neutral-on-quiet));\n background-color: var(--wa-color-fill-quiet, var(--wa-color-neutral-fill-quiet));\n border-color: var(--wa-color-border-normal, var(--wa-color-neutral-border-normal));\n }\n\n :host([appearance='accent']) {\n color: var(--wa-color-on-loud, var(--wa-color-neutral-on-loud));\n background-color: var(--wa-color-fill-loud, var(--wa-color-neutral-fill-loud));\n border-color: transparent;\n }\n}\n\n.content {\n font-size: var(--wa-font-size-smaller);\n}\n\n[part='remove-button'] {\n color: inherit;\n line-height: 1;\n}\n\n[part='remove-button']::part(base) {\n padding: 0;\n height: 1em;\n width: 1em;\n}\n\n@media (hover: hover) {\n :host(:hover) > [part='remove-button']::part(base) {\n color: color-mix(in oklab, currentColor, var(--wa-color-mix-hover));\n }\n}\n\n:host(:active) > [part='remove-button']::part(base) {\n color: color-mix(in oklab, currentColor, var(--wa-color-mix-active));\n}\n\n/*\n * Pill modifier\n */\n:host([pill]) {\n border-radius: var(--wa-border-radius-pill);\n}\n",KE,ad],e})();function xp(e,t=0){if(!e||!globalThis.Node)return"";if("function"==typeof e[Symbol.iterator])return(Array.isArray(e)?e:[...e]).map(i=>xp(i,--t)).join("");let n=e;if(n.nodeType===Node.TEXT_NODE)return n.textContent??"";if(n.nodeType===Node.ELEMENT_NODE){let r=n;if(r.hasAttribute("slot")||r.matches("style, script"))return"";if(r instanceof HTMLSlotElement){let i=r.assignedNodes({flatten:!0});if(i.length>0)return xp(i,--t)}return t>-1?xp(r,--t):r.textContent??""}return n.hasChildNodes()?xp(n.childNodes,--t):""}B([W({reflect:!0})],sc.prototype,"variant",2),B([W({reflect:!0})],sc.prototype,"appearance",2),B([W({reflect:!0})],sc.prototype,"size",2),B([W({type:Boolean,reflect:!0})],sc.prototype,"pill",2),B([W({attribute:"with-remove",type:Boolean})],sc.prototype,"withRemove",2),sc=B([ui("wa-tag")],sc);var ns=(()=>{let e=class extends di{constructor(){super(...arguments),this.localize=new za(this),this.isInitialized=!1,this.current=!1,this.value="",this.disabled=!1,this.selected=!1,this.defaultSelected=!1,this._label="",this.defaultLabel="",this.handleHover=t=>{"mouseenter"===t.type?this.customStates.set("hover",!0):"mouseleave"===t.type&&this.customStates.set("hover",!1)}}set label(t){const n=this._label;this._label=t||"",this._label!==n&&this.requestUpdate("label",n)}get label(){return this._label?this._label:(this.defaultLabel||this.updateDefaultLabel(),this.defaultLabel)}connectedCallback(){super.connectedCallback(),this.setAttribute("role","option"),this.setAttribute("aria-selected","false"),this.addEventListener("mouseenter",this.handleHover),this.addEventListener("mouseleave",this.handleHover),this.updateDefaultLabel()}disconnectedCallback(){super.disconnectedCallback(),this.removeEventListener("mouseenter",this.handleHover),this.removeEventListener("mouseleave",this.handleHover)}handleDefaultSlotChange(){this.updateDefaultLabel(),this.isInitialized?customElements.whenDefined("wa-select").then(()=>{const t=this.closest("wa-select");t&&(t.handleDefaultSlotChange(),t.selectionChanged?.())}):this.isInitialized=!0}willUpdate(t){if(t.has("defaultSelected")&&!this.closest("wa-select")?.hasInteracted){const n=this.selected;this.selected=this.defaultSelected,this.requestUpdate("selected",n)}super.willUpdate(t)}updated(t){super.updated(t),t.has("disabled")&&this.setAttribute("aria-disabled",this.disabled?"true":"false"),t.has("selected")&&(this.setAttribute("aria-selected",this.selected?"true":"false"),this.customStates.set("selected",this.selected),this.handleDefaultSlotChange()),t.has("value")&&("string"!=typeof this.value&&(this.value=String(this.value)),this.handleDefaultSlotChange()),t.has("current")&&this.customStates.set("current",this.current)}updateDefaultLabel(){let t=this.defaultLabel;this.defaultLabel=xp(this).trim();let n=this.defaultLabel!==t;return!this._label&&n&&this.requestUpdate("label",t),n}render(){return At` + + + + + `}};return e.css=":host {\n display: block;\n color: var(--wa-color-text-normal);\n -webkit-user-select: none;\n user-select: none;\n\n position: relative;\n display: flex;\n align-items: center;\n font: inherit;\n padding: 0.5em 1em 0.5em 0.25em;\n line-height: var(--wa-line-height-condensed);\n transition: fill var(--wa-transition-normal) var(--wa-transition-easing);\n cursor: pointer;\n}\n\n:host(:focus) {\n outline: none;\n}\n\n@media (hover: hover) {\n :host(:not([disabled], :state(current)):is(:state(hover), :hover)) {\n background-color: var(--wa-color-neutral-fill-normal);\n color: var(--wa-color-neutral-on-normal);\n }\n}\n\n:host(:state(current)),\n:host([disabled]:state(current)) {\n background-color: var(--wa-color-brand-fill-loud);\n color: var(--wa-color-brand-on-loud);\n opacity: 1;\n}\n\n:host([disabled]) {\n outline: none;\n opacity: 0.5;\n cursor: not-allowed;\n}\n\n.label {\n flex: 1 1 auto;\n display: inline-block;\n}\n\n.check {\n flex: 0 0 auto;\n display: flex;\n align-items: center;\n justify-content: center;\n font-size: var(--wa-font-size-smaller);\n visibility: hidden;\n width: 2em;\n}\n\n:host(:state(selected)) .check {\n visibility: visible;\n}\n\n.start,\n.end {\n flex: 0 0 auto;\n display: flex;\n align-items: center;\n}\n\n.start::slotted(*) {\n margin-inline-end: 0.5em;\n}\n\n.end::slotted(*) {\n margin-inline-start: 0.5em;\n}\n\n@media (forced-colors: active) {\n :host(:hover:not([aria-disabled='true'])) {\n outline: dashed 1px SelectedItem;\n outline-offset: -1px;\n }\n}\n",e})();B([$n(".label")],ns.prototype,"defaultSlot",2),B([$i()],ns.prototype,"current",2),B([W({reflect:!0})],ns.prototype,"value",2),B([W({type:Boolean})],ns.prototype,"disabled",2),B([W({type:Boolean,attribute:!1})],ns.prototype,"selected",2),B([W({type:Boolean,attribute:"selected"})],ns.prototype,"defaultSelected",2),B([W()],ns.prototype,"label",1),B([$i()],ns.prototype,"defaultLabel",2),ns=B([ui("wa-option")],ns);var bd=(()=>{let e=class extends di{constructor(){super(...arguments),this.variant="brand",this.appearance="accent",this.pill=!1,this.attention="none"}render(){return At` `}};return e.css=[KE,":host {\n --pulse-color: var(--wa-color-fill-loud, var(--wa-color-brand-fill-loud));\n\n display: inline-flex;\n align-items: center;\n justify-content: center;\n padding: 0.375em 0.625em;\n color: var(--wa-color-on-loud, var(--wa-color-brand-on-loud));\n font-size: max(var(--wa-font-size-2xs), 0.75em);\n font-weight: var(--wa-font-weight-semibold);\n line-height: 1;\n white-space: nowrap;\n background-color: var(--wa-color-fill-loud, var(--wa-color-brand-fill-loud));\n border-color: transparent;\n border-radius: var(--wa-border-radius-s);\n border-style: var(--wa-border-style);\n border-width: var(--wa-border-width-s);\n user-select: none;\n -webkit-user-select: none;\n cursor: inherit;\n}\n\n/* Appearance modifiers */\n:host([appearance='outlined']) {\n --pulse-color: var(--wa-color-border-loud, var(--wa-color-brand-border-loud));\n\n color: var(--wa-color-on-quiet, var(--wa-color-brand-on-quiet));\n background-color: transparent;\n border-color: var(--wa-color-border-loud, var(--wa-color-brand-border-loud));\n}\n\n:host([appearance='filled']) {\n --pulse-color: var(--wa-color-fill-normal, var(--wa-color-brand-fill-normal));\n\n color: var(--wa-color-on-normal, var(--wa-color-brand-on-normal));\n background-color: var(--wa-color-fill-normal, var(--wa-color-brand-fill-normal));\n border-color: transparent;\n}\n\n:host([appearance='filled-outlined']) {\n --pulse-color: var(--wa-color-border-normal, var(--wa-color-brand-border-normal));\n\n color: var(--wa-color-on-normal, var(--wa-color-brand-on-normal));\n background-color: var(--wa-color-fill-normal, var(--wa-color-brand-fill-normal));\n border-color: var(--wa-color-border-normal, var(--wa-color-brand-border-normal));\n}\n\n:host([appearance='accent']) {\n --pulse-color: var(--wa-color-fill-loud, var(--wa-color-brand-fill-loud));\n\n color: var(--wa-color-on-loud, var(--wa-color-brand-on-loud));\n background-color: var(--wa-color-fill-loud, var(--wa-color-brand-fill-loud));\n border-color: transparent;\n}\n\n/* Pill modifier */\n:host([pill]) {\n border-radius: var(--wa-border-radius-pill);\n}\n\n/* Pulse attention */\n:host([attention='pulse']) {\n animation: pulse 1.5s infinite;\n}\n\n@keyframes pulse {\n 0% {\n box-shadow: 0 0 0 0 var(--pulse-color);\n }\n 70% {\n box-shadow: 0 0 0 0.5rem transparent;\n }\n 100% {\n box-shadow: 0 0 0 0 transparent;\n }\n}\n\n/* Bounce attention */\n:host([attention='bounce']) {\n animation: bounce 1s cubic-bezier(0.28, 0.84, 0.42, 1) infinite;\n}\n\n@keyframes bounce {\n 0%,\n 20%,\n 50%,\n 80%,\n 100% {\n transform: translateY(0);\n }\n 40% {\n transform: translateY(-5px);\n }\n 60% {\n transform: translateY(-2px);\n }\n}\n\n::slotted(wa-icon) {\n margin-inline-end: var(--wa-space-2xs, 0.25em);\n opacity: 90%;\n line-height: 1;\n height: 0.85em;\n}\n"],e})();B([W({reflect:!0})],bd.prototype,"variant",2),B([W({reflect:!0})],bd.prototype,"appearance",2),B([W({type:Boolean,reflect:!0})],bd.prototype,"pill",2),B([W({reflect:!0})],bd.prototype,"attention",2),bd=B([ui("wa-badge")],bd);var qr=(()=>{let e=class extends di{constructor(){super(...arguments),this.localize=new za(this),this.isAnimating=!1,this.open=!1,this.disabled=!1,this.appearance="outlined",this.iconPlacement="end"}disconnectedCallback(){super.disconnectedCallback(),this.detailsObserver?.disconnect()}firstUpdated(){this.body.style.height=this.open?"auto":"0",this.open&&(this.details.open=!0),this.detailsObserver=new MutationObserver(t=>{for(const n of t)"attributes"===n.type&&"open"===n.attributeName&&(this.details.open?this.show():this.hide())}),this.detailsObserver.observe(this.details,{attributes:!0})}updated(t){t.has("isAnimating")&&this.customStates.set("animating",this.isAnimating)}handleSummaryClick(t){t.composedPath().some(i=>{if(!(i instanceof HTMLElement))return!1;const o=i.tagName?.toLowerCase();return!!["a","button","input","textarea","select"].includes(o)||i instanceof Gr&&(!("disabled"in i)||!i.disabled)})||(t.preventDefault(),this.disabled||(this.open?this.hide():this.show(),this.header.focus()))}handleSummaryKeyDown(t){("Enter"===t.key||" "===t.key)&&(t.preventDefault(),this.open?this.hide():this.show()),("ArrowUp"===t.key||"ArrowLeft"===t.key)&&(t.preventDefault(),this.hide()),("ArrowDown"===t.key||"ArrowRight"===t.key)&&(t.preventDefault(),this.show())}closeOthersWithSameName(){this.name&&this.getRootNode().querySelectorAll(`wa-details[name="${this.name}"]`).forEach(r=>{r!==this&&r.open&&(r.open=!1)})}handleOpenChange(){var t=this;return(0,oe.A)(function*(){if(t.open){t.details.open=!0;const n=new UD;if(t.dispatchEvent(n),n.defaultPrevented)return t.open=!1,void(t.details.open=!1);t.closeOthersWithSameName(),t.isAnimating=!0;const r=t2(getComputedStyle(t.body).getPropertyValue("--show-duration"));yield e2(t.body,[{height:"0",opacity:"0"},{height:`${t.body.scrollHeight}px`,opacity:"1"}],{duration:r,easing:"linear"}),t.body.style.height="auto",t.isAnimating=!1,t.dispatchEvent(new jD)}else{const n=new HD;if(t.dispatchEvent(n),n.defaultPrevented)return t.details.open=!0,void(t.open=!0);t.isAnimating=!0;const r=t2(getComputedStyle(t.body).getPropertyValue("--hide-duration"));yield e2(t.body,[{height:`${t.body.scrollHeight}px`,opacity:"1"},{height:"0",opacity:"0"}],{duration:r,easing:"linear"}),t.body.style.height="auto",t.isAnimating=!1,t.details.open=!1,t.dispatchEvent(new $D)}})()}show(){var t=this;return(0,oe.A)(function*(){if(!t.open&&!t.disabled)return t.open=!0,vd(t,"wa-after-show")})()}hide(){var t=this;return(0,oe.A)(function*(){if(t.open&&!t.disabled)return t.open=!1,vd(t,"wa-after-hide")})()}render(){const t=this.hasUpdated?"rtl"===this.localize.dir():"rtl"===this.dir;return At` +
+ + ${this.summary} + + + + + + + + + + + +
+ +
+
+ `}};return e.css=":host {\n --spacing: var(--wa-space-m);\n --show-duration: 200ms;\n --hide-duration: 200ms;\n\n display: block;\n}\n\ndetails {\n display: block;\n overflow-anchor: none;\n border: var(--wa-panel-border-width) var(--wa-color-surface-border) var(--wa-panel-border-style);\n background-color: var(--wa-color-surface-default);\n border-radius: var(--wa-panel-border-radius);\n color: var(--wa-color-text-normal);\n\n /* Print styles */\n @media print {\n background: none;\n border: solid var(--wa-border-width-s) var(--wa-color-surface-border);\n\n summary {\n list-style: none;\n }\n }\n}\n\n/* Appearance modifiers */\n:host([appearance='plain']) details {\n background-color: transparent;\n border-color: transparent;\n border-radius: 0;\n}\n\n:host([appearance='outlined']) details {\n background-color: var(--wa-color-surface-default);\n border-color: var(--wa-color-surface-border);\n}\n\n:host([appearance='filled']) details {\n background-color: var(--wa-color-neutral-fill-quiet);\n border-color: transparent;\n}\n\n:host([appearance='filled-outlined']) details {\n background-color: var(--wa-color-neutral-fill-quiet);\n border-color: var(--wa-color-neutral-border-quiet);\n}\n\n:host([disabled]) details {\n opacity: 0.5;\n cursor: not-allowed;\n}\n\nsummary {\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: var(--spacing);\n padding: var(--spacing); /* Add padding here */\n border-radius: calc(var(--wa-panel-border-radius) - var(--wa-panel-border-width));\n user-select: none;\n -webkit-user-select: none;\n cursor: pointer;\n\n &::marker,\n &::-webkit-details-marker {\n display: none;\n }\n\n &:focus {\n outline: none;\n }\n\n &:focus-visible {\n outline: var(--wa-focus-ring);\n outline-offset: calc(var(--wa-panel-border-width) + var(--wa-focus-ring-offset));\n }\n}\n\n:host([open]) summary {\n border-end-start-radius: 0;\n border-end-end-radius: 0;\n}\n\n/* 'Start' icon placement */\n:host([icon-placement='start']) summary {\n flex-direction: row-reverse;\n justify-content: start;\n}\n\n[part~='icon'] {\n flex: 0 0 auto;\n display: flex;\n align-items: center;\n color: var(--wa-color-text-quiet);\n transition: rotate var(--wa-transition-normal) var(--wa-transition-easing);\n}\n\n:host([open]) [part~='icon'] {\n rotate: 90deg;\n}\n\n:host([open]:dir(rtl)) [part~='icon'] {\n rotate: -90deg;\n}\n\n:host([open]) slot[name='expand-icon'],\n:host(:not([open])) slot[name='collapse-icon'] {\n display: none;\n}\n\n.body.animating {\n overflow: hidden;\n}\n\n.content {\n display: block;\n padding-block-start: var(--spacing);\n padding-inline: var(--spacing); /* Add horizontal padding */\n padding-block-end: var(--spacing); /* Add bottom padding */\n}\n",e})();B([$n("details")],qr.prototype,"details",2),B([$n("summary")],qr.prototype,"header",2),B([$n(".body")],qr.prototype,"body",2),B([$n(".expand-icon-slot")],qr.prototype,"expandIconSlot",2),B([$i()],qr.prototype,"isAnimating",2),B([W({type:Boolean,reflect:!0})],qr.prototype,"open",2),B([W()],qr.prototype,"summary",2),B([W({reflect:!0})],qr.prototype,"name",2),B([W({type:Boolean,reflect:!0})],qr.prototype,"disabled",2),B([W({reflect:!0})],qr.prototype,"appearance",2),B([W({attribute:"icon-placement",reflect:!0})],qr.prototype,"iconPlacement",2),B([Ir("open",{waitUntilFirstUpdate:!0})],qr.prototype,"handleOpenChange",1),qr=B([ui("wa-details")],qr);const hY=(e,t)=>t.id;function fY(e,t){if(1&e&&(se(0,"wa-card",1)(1,"div",3),Yn(2,"wa-icon",4),te(3," Error "),ce(),se(4,"p"),te(5),ce()()),2&e){const n=Pe();he(5),Pt(n.error())}}function pY(e,t){if(1&e){const n=Zo();se(0,"div",5)(1,"div",6)(2,"div",7)(3,"span",8),te(4),ce(),se(5,"span",9),te(6),ce()(),se(7,"div",10),te(8,"Current Hashrate"),ce(),se(9,"div",11),te(10),ce()(),se(11,"div",12)(12,"div",13),Yn(13,"wa-icon",14),se(14,"div",15),te(15),ce(),se(16,"div",16),te(17,"Accepted"),ce()(),se(18,"div",17),Yn(19,"wa-icon",18),se(20,"div",15),te(21),ce(),se(22,"div",16),te(23,"Rejected"),ce()(),se(24,"div",19),Yn(25,"wa-icon",20),se(26,"div",15),te(27),ce(),se(28,"div",16),te(29,"Uptime"),ce()(),se(30,"div",21),Yn(31,"wa-icon",22),se(32,"div",23),te(33),ce(),se(34,"div",16),te(35),ce()()()(),se(36,"div",24),Yn(37,"snider-mining-chart"),ce(),se(38,"div",25)(39,"div",26)(40,"wa-badge",27),Yn(41,"wa-icon",28),te(42),ce(),se(43,"span",29),te(44),ce(),se(45,"span",30),te(46),zl(47,"number"),ce()(),se(48,"div",31)(49,"wa-button",32),rn("click",function(){return Dt(n),Gt(Pe().stopMiner())}),Yn(50,"wa-icon",33),te(51," Stop Mining "),ce()()(),se(52,"wa-details",34)(53,"div",35)(54,"div",36)(55,"h4"),te(56,"Hashrate"),ce(),se(57,"dl")(58,"dt"),te(59,"10s Average"),ce(),se(60,"dd"),te(61),zl(62,"number"),ce(),se(63,"dt"),te(64,"60s Average"),ce(),se(65,"dd"),te(66),zl(67,"number"),ce(),se(68,"dt"),te(69,"15m Average"),ce(),se(70,"dd"),te(71),zl(72,"number"),ce()()(),se(73,"div",36)(74,"h4"),te(75,"Shares"),ce(),se(76,"dl")(77,"dt"),te(78,"Good Shares"),ce(),se(79,"dd"),te(80),ce(),se(81,"dt"),te(82,"Total Shares"),ce(),se(83,"dd"),te(84),ce(),se(85,"dt"),te(86,"Avg Time"),ce(),se(87,"dd"),te(88),ce(),se(89,"dt"),te(90,"Total Hashes"),ce(),se(91,"dd"),te(92),zl(93,"number"),ce()()(),se(94,"div",36)(95,"h4"),te(96,"Connection"),ce(),se(97,"dl")(98,"dt"),te(99,"Pool"),ce(),se(100,"dd"),te(101),ce(),se(102,"dt"),te(103,"IP"),ce(),se(104,"dd"),te(105),ce(),se(106,"dt"),te(107,"Ping"),ce(),se(108,"dd"),te(109),ce(),se(110,"dt"),te(111,"Difficulty"),ce(),se(112,"dd"),te(113),zl(114,"number"),ce()()(),se(115,"div",36)(116,"h4"),te(117,"System"),ce(),se(118,"dl")(119,"dt"),te(120,"CPU"),ce(),se(121,"dd"),te(122),ce(),se(123,"dt"),te(124,"Threads"),ce(),se(125,"dd"),te(126),ce(),se(127,"dt"),te(128,"Algorithm"),ce(),se(129,"dd"),te(130),ce(),se(131,"dt"),te(132,"Version"),ce(),se(133,"dd"),te(134),ce()()()()()}if(2&e){let n,r,i,o,a,u,f,y,w,D,k,A,N,L,H;const V=Pe();he(4),Pt(V.formatHashrate(V.currentHashrate())),he(2),Pt(V.getHashrateUnit(V.currentHashrate())),he(4),Gf(" Peak: ",V.formatHashrate(V.peakHashrate())," ",V.getHashrateUnit(V.peakHashrate())," "),he(5),Pt(V.acceptedShares()),he(3),zf("has-rejected",V.rejectedShares()>0),he(3),Pt(V.rejectedShares()),he(6),Pt(V.formatUptime(V.uptime())),he(6),Pt(V.poolName()),he(2),Pi("Pool (",V.poolPing(),"ms)"),he(7),Pi(" ",V.minerName()," "),he(2),Pt(V.algorithm()),he(2),Pi("Diff: ",Wy(47,29,V.difficulty())),he(15),Pi("",Gy(62,31,null==(n=V.stats())||null==n.hashrate?null:n.hashrate.total[0],"1.0-2")," H/s"),he(5),Pi("",Gy(67,34,null==(r=V.stats())||null==r.hashrate?null:r.hashrate.total[1],"1.0-2")," H/s"),he(5),Pi("",Gy(72,37,null==(i=V.stats())||null==i.hashrate?null:i.hashrate.total[2],"1.0-2")," H/s"),he(9),Pt(null==(o=V.stats())||null==o.results?null:o.results.shares_good),he(4),Pt(null==(a=V.stats())||null==a.results?null:a.results.shares_total),he(4),Pi("",null==(u=V.stats())||null==u.results?null:u.results.avg_time,"s"),he(4),Pt(Wy(93,40,null==(f=V.stats())||null==f.results?null:f.results.hashes_total)),he(9),Pt(null==(y=V.stats())||null==y.connection?null:y.connection.pool),he(4),Pt(null==(w=V.stats())||null==w.connection?null:w.connection.ip),he(4),Pi("",null==(D=V.stats())||null==D.connection?null:D.connection.ping,"ms"),he(4),Pt(Wy(114,42,null==(k=V.stats())||null==k.connection?null:k.connection.diff)),he(9),Pt(null==(A=V.stats())||null==A.cpu?null:A.cpu.brand),he(4),Pt(null==(N=V.stats())||null==N.cpu?null:N.cpu.threads),he(4),Pt(null==(L=V.stats())?null:L.algo),he(4),Pt(null==(H=V.stats())?null:H.version)}}function gY(e,t){if(1&e&&(se(0,"wa-option",42),te(1),ce()),2&e){const n=t.$implicit;Ni("value",n.id),he(),Gf("",n.name," (",n.minerType,")")}}function mY(e,t){if(1&e){const n=Zo();se(0,"div",39)(1,"wa-select",41),rn("change",function(i){return Dt(n),Gt(Pe(2).onProfileSelect(i))}),La(2,gY,2,3,"wa-option",42,hY),ce(),se(4,"wa-button",43),rn("click",function(){return Dt(n),Gt(Pe(2).startMining())}),Yn(5,"wa-icon",44),te(6," Start Mining "),ce()()}if(2&e){const n=Pe(2);he(2),Fa(n.state().profiles),he(2),Ni("disabled",!n.selectedProfileId())}}function yY(e,t){1&e&&(se(0,"p",40),te(1,"No profiles configured. Create one in the Profiles section."),ce())}function vY(e,t){if(1&e&&(se(0,"div",2)(1,"div",37),Yn(2,"wa-icon",38),ce(),se(3,"h3"),te(4,"No Miners Running"),ce(),se(5,"p"),te(6,"Select a profile and start mining to see real-time statistics."),ce(),or(7,mY,7,1,"div",39)(8,yY,2,0,"p",40),ce()),2&e){const n=Pe();he(7),sr(n.state().profiles.length>0?7:8)}}let i2=(()=>{class e{minerService=Y(Zl);state=this.minerService.state;error=tn(null);selectedProfileId=tn(null);stats=It(()=>{const n=this.state().runningMiners;return n.length>0?n[0].full_stats:null});currentHashrate=It(()=>this.stats()?.hashrate?.total?.[0]||0);peakHashrate=It(()=>this.stats()?.hashrate?.highest||0);acceptedShares=It(()=>this.stats()?.results?.shares_good||0);rejectedShares=It(()=>(this.stats()?.results?.shares_total||0)-(this.stats()?.results?.shares_good||0));uptime=It(()=>this.stats()?.uptime||0);poolName=It(()=>(this.stats()?.connection?.pool||"").split(":")[0]||"Not connected");poolPing=It(()=>this.stats()?.connection?.ping||0);minerName=It(()=>{const n=this.state().runningMiners;return n.length>0?n[0].name:""});algorithm=It(()=>this.stats()?.algo||"");difficulty=It(()=>this.stats()?.connection?.diff||0);formatHashrate(n){return n>=1e9?(n/1e9).toFixed(2):n>=1e6?(n/1e6).toFixed(2):n>=1e3?(n/1e3).toFixed(2):n.toFixed(0)}getHashrateUnit(n){return n>=1e9?"GH/s":n>=1e6?"MH/s":n>=1e3?"kH/s":"H/s"}formatUptime(n){return n<60?`${n}s`:n<3600?`${Math.floor(n/60)}m ${n%60}s`:`${Math.floor(n/3600)}h ${Math.floor(n%3600/60)}m`}onProfileSelect(n){this.selectedProfileId.set(n.target.value)}startMining(){const n=this.selectedProfileId();n&&this.minerService.startMiner(n).subscribe({error:r=>{this.error.set(r.error?.error||"Failed to start miner")}})}stopMiner(){const n=this.minerName();n&&this.minerService.stopMiner(n).subscribe({error:r=>{this.error.set(r.error?.error||"Failed to stop miner")}})}static \u0275fac=function(r){return new(r||e)};static \u0275cmp=qo({type:e,selectors:[["snider-mining-dashboard"]],decls:4,vars:2,consts:[[1,"mining-dashboard"],[1,"error-card"],[1,"idle-state"],["slot","header"],["name","exclamation-triangle"],[1,"hero-stats"],[1,"hashrate-hero"],[1,"hashrate-value"],[1,"number"],[1,"unit"],[1,"hashrate-label"],[1,"hashrate-peak"],[1,"quick-stats"],[1,"stat-card","accepted"],["name","check-circle"],[1,"stat-value"],[1,"stat-label"],[1,"stat-card","rejected"],["name","x-circle"],[1,"stat-card","uptime"],["name","clock"],[1,"stat-card","pool"],["name","globe"],[1,"stat-value","pool-name"],[1,"chart-section"],[1,"controls-section"],[1,"miner-info"],["variant","success","pulse",""],["name","cpu","slot","prefix"],[1,"algo-badge"],[1,"diff-badge"],[1,"control-buttons"],["variant","danger","size","small",3,"click"],["name","stop-circle","slot","prefix"],["summary","Detailed Statistics",1,"details-section"],[1,"detailed-stats"],[1,"stat-group"],[1,"idle-icon"],["name","cpu"],[1,"profile-select"],[1,"no-profiles"],["label","Select Profile",3,"change"],[3,"value"],["variant","success",3,"click","disabled"],["name","play-circle","slot","prefix"]],template:function(r,i){1&r&&(se(0,"div",0),or(1,fY,6,1,"wa-card",1),or(2,pY,135,44)(3,vY,9,1,"div",2),ce()),2&r&&(he(),sr(i.error()?1:-1),he(),sr(i.state().runningMiners.length>0?2:3))},dependencies:[ql,MD,IP,hR],styles:[".mining-dashboard[_ngcontent-%COMP%]{display:flex;flex-direction:column;gap:1.5rem;height:100%;padding:1rem;font-family:var(--wa-font-sans, system-ui, sans-serif)}.error-card[_ngcontent-%COMP%]{--wa-card-border-color: var(--wa-color-danger-600);background:var(--wa-color-danger-50)}.error-card[_ngcontent-%COMP%] [slot=header][_ngcontent-%COMP%]{color:var(--wa-color-danger-700);display:flex;align-items:center;gap:.5rem;font-weight:600}.hero-stats[_ngcontent-%COMP%]{display:flex;flex-direction:column;gap:1rem}.hashrate-hero[_ngcontent-%COMP%]{text-align:center;padding:1.5rem;background:linear-gradient(135deg,var(--wa-color-primary-600) 0%,var(--wa-color-primary-700) 100%);border-radius:12px;color:#fff;box-shadow:0 4px 20px #00000026}.hashrate-value[_ngcontent-%COMP%]{display:flex;align-items:baseline;justify-content:center;gap:.5rem}.hashrate-value[_ngcontent-%COMP%] .number[_ngcontent-%COMP%]{font-size:3.5rem;font-weight:700;font-variant-numeric:tabular-nums;line-height:1}.hashrate-value[_ngcontent-%COMP%] .unit[_ngcontent-%COMP%]{font-size:1.5rem;font-weight:500;opacity:.9}.hashrate-label[_ngcontent-%COMP%]{font-size:.9rem;text-transform:uppercase;letter-spacing:.1em;opacity:.85;margin-top:.5rem}.hashrate-peak[_ngcontent-%COMP%]{font-size:.85rem;opacity:.75;margin-top:.25rem}.quick-stats[_ngcontent-%COMP%]{display:grid;grid-template-columns:repeat(4,1fr);gap:.75rem}@media (max-width: 600px){.quick-stats[_ngcontent-%COMP%]{grid-template-columns:repeat(2,1fr)}}.stat-card[_ngcontent-%COMP%]{display:flex;flex-direction:column;align-items:center;padding:1rem;background:var(--wa-color-neutral-50);border:1px solid var(--wa-color-neutral-200);border-radius:8px;transition:all .2s ease}.stat-card[_ngcontent-%COMP%]:hover{transform:translateY(-2px);box-shadow:0 4px 12px #0000001a}.stat-card[_ngcontent-%COMP%] wa-icon[_ngcontent-%COMP%]{font-size:1.5rem;margin-bottom:.5rem}.stat-card.accepted[_ngcontent-%COMP%] wa-icon[_ngcontent-%COMP%]{color:var(--wa-color-success-600)}.stat-card.rejected[_ngcontent-%COMP%] wa-icon[_ngcontent-%COMP%]{color:var(--wa-color-neutral-400)}.stat-card.rejected.has-rejected[_ngcontent-%COMP%] wa-icon[_ngcontent-%COMP%]{color:var(--wa-color-danger-600)}.stat-card.uptime[_ngcontent-%COMP%] wa-icon[_ngcontent-%COMP%]{color:var(--wa-color-primary-600)}.stat-card.pool[_ngcontent-%COMP%] wa-icon[_ngcontent-%COMP%]{color:var(--wa-color-warning-600)}.stat-value[_ngcontent-%COMP%]{font-size:1.5rem;font-weight:700;color:var(--wa-color-neutral-900);font-variant-numeric:tabular-nums}.stat-value.pool-name[_ngcontent-%COMP%]{font-size:.9rem;font-weight:600;max-width:100%;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.stat-label[_ngcontent-%COMP%]{font-size:.75rem;color:var(--wa-color-neutral-600);text-transform:uppercase;letter-spacing:.05em;margin-top:.25rem}.chart-section[_ngcontent-%COMP%]{flex:1;min-height:250px;background:var(--wa-color-neutral-50);border:1px solid var(--wa-color-neutral-200);border-radius:8px;padding:1rem;overflow:hidden}.controls-section[_ngcontent-%COMP%]{display:flex;justify-content:space-between;align-items:center;padding:.75rem 1rem;background:var(--wa-color-neutral-100);border-radius:8px;flex-wrap:wrap;gap:.75rem}.miner-info[_ngcontent-%COMP%]{display:flex;align-items:center;gap:.75rem;flex-wrap:wrap}.algo-badge[_ngcontent-%COMP%], .diff-badge[_ngcontent-%COMP%]{display:inline-flex;align-items:center;padding:.25rem .75rem;background:var(--wa-color-neutral-200);border-radius:999px;font-size:.8rem;font-weight:500;color:var(--wa-color-neutral-700)}.control-buttons[_ngcontent-%COMP%]{display:flex;gap:.5rem}.details-section[_ngcontent-%COMP%]{margin-top:.5rem}.detailed-stats[_ngcontent-%COMP%]{display:grid;grid-template-columns:repeat(auto-fit,minmax(200px,1fr));gap:1.5rem;padding:1rem 0}.stat-group[_ngcontent-%COMP%] h4[_ngcontent-%COMP%]{font-size:.85rem;font-weight:600;color:var(--wa-color-neutral-700);text-transform:uppercase;letter-spacing:.05em;margin:0 0 .75rem;padding-bottom:.5rem;border-bottom:2px solid var(--wa-color-primary-500)}.stat-group[_ngcontent-%COMP%] dl[_ngcontent-%COMP%]{display:grid;grid-template-columns:1fr auto;gap:.5rem 1rem;margin:0}.stat-group[_ngcontent-%COMP%] dt[_ngcontent-%COMP%]{font-size:.85rem;color:var(--wa-color-neutral-600)}.stat-group[_ngcontent-%COMP%] dd[_ngcontent-%COMP%]{margin:0;font-weight:500;text-align:right;font-variant-numeric:tabular-nums;font-family:var(--wa-font-mono, monospace);font-size:.85rem}.idle-state[_ngcontent-%COMP%]{display:flex;flex-direction:column;align-items:center;justify-content:center;text-align:center;padding:3rem 2rem;flex:1}.idle-icon[_ngcontent-%COMP%]{width:80px;height:80px;display:flex;align-items:center;justify-content:center;background:var(--wa-color-neutral-100);border-radius:50%;margin-bottom:1.5rem}.idle-icon[_ngcontent-%COMP%] wa-icon[_ngcontent-%COMP%]{font-size:2.5rem;color:var(--wa-color-neutral-400)}.idle-state[_ngcontent-%COMP%] h3[_ngcontent-%COMP%]{font-size:1.5rem;font-weight:600;color:var(--wa-color-neutral-800);margin:0 0 .5rem}.idle-state[_ngcontent-%COMP%] p[_ngcontent-%COMP%]{color:var(--wa-color-neutral-600);margin:0 0 1.5rem}.profile-select[_ngcontent-%COMP%]{display:flex;flex-direction:column;align-items:center;gap:1rem;width:100%;max-width:300px}.profile-select[_ngcontent-%COMP%] wa-select[_ngcontent-%COMP%]{width:100%}.profile-select[_ngcontent-%COMP%] wa-button[_ngcontent-%COMP%]{width:100%}.no-profiles[_ngcontent-%COMP%]{font-size:.9rem;color:var(--wa-color-neutral-500);font-style:italic}@media (prefers-color-scheme: dark){.hashrate-hero[_ngcontent-%COMP%]{background:linear-gradient(135deg,var(--wa-color-primary-700) 0%,var(--wa-color-primary-800) 100%)}.stat-card[_ngcontent-%COMP%]{background:var(--wa-color-neutral-800);border-color:var(--wa-color-neutral-700)}.stat-value[_ngcontent-%COMP%]{color:var(--wa-color-neutral-100)}.chart-section[_ngcontent-%COMP%]{background:var(--wa-color-neutral-800);border-color:var(--wa-color-neutral-700)}.controls-section[_ngcontent-%COMP%], .idle-icon[_ngcontent-%COMP%]{background:var(--wa-color-neutral-800)}}"]})}return e})();function bY(e,t){1&e&&(se(0,"div",1),Yn(1,"wa-spinner",2),se(2,"p"),te(3,"Connecting to API..."),ce()())}function _Y(e,t){if(1&e){const n=Zo();se(0,"div",1)(1,"p"),te(2,"API Not Available. Please ensure the mining service is running."),ce(),se(3,"wa-button",3),rn("click",function(){return Dt(n),Gt(Pe().forceRefreshState())}),Yn(4,"wa-icon",4),te(5," Retry "),ce()()}}function wY(e,t){1&e&&Yn(0,"snider-mining-setup-wizard")}function CY(e,t){1&e&&Yn(0,"snider-mining-dashboard")}let EY=(()=>{class e{minerService=Y(Zl);state=this.minerService.state;forceRefreshState(){this.minerService.forceRefreshState()}static \u0275fac=function(r){return new(r||e)};static \u0275cmp=qo({type:e,selectors:[["snider-mining"]],decls:5,vars:1,consts:[[1,"mining-dashboard"],[1,"centered-container"],[2,"font-size","3rem","margin-top","1rem"],[3,"click"],["name","arrow-clockwise","slot","prefix"]],template:function(r,i){1&r&&(se(0,"div",0),or(1,bY,4,0,"div",1)(2,_Y,6,0,"div",1)(3,wY,1,0,"snider-mining-setup-wizard")(4,CY,1,0,"snider-mining-dashboard"),ce()),2&r&&(he(),sr(null!==i.state().systemInfo||i.state().needsSetup?i.state().apiAvailable?i.state().apiAvailable&&i.state().needsSetup?3:i.state().apiAvailable&&!i.state().needsSetup?4:-1:2:1))},dependencies:[ql,_N,i2],styles:[":host{display:block;font-family:sans-serif;width:100%}.mining-dashboard{padding:1rem;width:100%;box-sizing:border-box}.centered-container{display:flex;flex-direction:column;justify-content:center;align-items:center;text-align:center;width:100%}.card-overview,.card-error{width:100%;margin-bottom:1rem;box-sizing:border-box}.card-header{display:flex;justify-content:space-between;align-items:center;width:100%}.header-title{display:flex;align-items:center;gap:.5rem}.dashboard-summary{display:flex;flex-direction:column;gap:1rem;margin-top:1rem;padding-bottom:1rem;border-bottom:1px solid #e0e0e0}.miner-summary-item{display:flex;justify-content:space-between;align-items:center;padding:.75rem;border-radius:.25rem;border:1px solid #e0e0e0}.dashboard-charts{display:flex;flex-direction:column;gap:1rem;margin-top:1rem}.miner-chart-item{border:1px solid #e0e0e0;border-radius:.25rem;padding:.5rem}.miner-name{font-weight:700;display:flex;align-items:center;gap:.5rem}.start-buttons{display:flex;gap:.5rem}.start-options{display:flex;flex-direction:column;gap:1rem;border-top:1px solid #e0e0e0;padding-top:1rem}.button-spinner{font-size:1em;margin:0}wa-button{min-width:auto}wa-spinner{margin:1rem 0}.admin-panel{padding:1rem;border-top:1px solid #e0e0e0;margin-top:1rem;border-radius:.25rem;border:1px solid #e0e0e0}.admin-title{margin-top:0;border-bottom:1px solid #e0e0e0;padding-bottom:.5rem;margin-bottom:1rem}.path-list ul{list-style:none;padding:.5rem;margin:0;font-family:monospace;border-radius:.25rem;border:1px solid #e0e0e0}.path-list li{padding:.25rem 0}\n"],encapsulation:3})}return e})();const DY=(e,t)=>t.name;function xY(e,t){if(1&e&&(He(0,"wa-card",1)(1,"div",6),ai(2,"wa-icon",7),te(3," An Error Occurred "),$e(),He(4,"p"),te(5),$e()()),2&e){const n=Pe();he(5),Pt(n.error())}}function SY(e,t){1&e&&ai(0,"wa-spinner",11)}function MY(e,t){1&e&&(ai(0,"wa-icon",12),te(1," Uninstall "))}function TY(e,t){if(1&e){const n=Zo();He(0,"wa-button",10),ar("click",function(){Dt(n);const i=Pe().$implicit;return Gt(Pe().uninstallMiner(i.name))}),or(1,SY,1,0,"wa-spinner",11)(2,MY,2,0),$e()}if(2&e){const n=Pe().$implicit,r=Pe();li("disabled",r.actionInProgress()==="uninstall-"+n.name),he(),sr(r.actionInProgress()==="uninstall-"+n.name?1:2)}}function IY(e,t){1&e&&ai(0,"wa-spinner",11)}function AY(e,t){1&e&&(ai(0,"wa-icon",14),te(1," Install "))}function kY(e,t){if(1&e){const n=Zo();He(0,"wa-button",13),ar("click",function(){Dt(n);const i=Pe().$implicit;return Gt(Pe().installMiner(i.name))}),or(1,IY,1,0,"wa-spinner",11)(2,AY,2,0),$e()}if(2&e){const n=Pe().$implicit,r=Pe();li("disabled",r.actionInProgress()==="install-"+n.name),he(),sr(r.actionInProgress()==="install-"+n.name?1:2)}}function OY(e,t){if(1&e&&(He(0,"div",3)(1,"span"),te(2),$e(),or(3,TY,3,2,"wa-button",8)(4,kY,3,2,"wa-button",9),$e()),2&e){const n=t.$implicit;he(2),Pt(n.name),he(),sr(n.is_installed?3:4)}}function RY(e,t){1&e&&(He(0,"div",3)(1,"span"),te(2,"Could not load available miners."),$e()())}function NY(e,t){if(1&e&&(He(0,"li")(1,"code"),te(2),$e()()),2&e){const n=t.$implicit;he(2),Pt(n)}}function PY(e,t){1&e&&(He(0,"li"),te(1,"No paths to display. Install a miner to see required paths."),$e())}let LY=(()=>{class e{minerService=Y(Zl);state=this.minerService.state;actionInProgress=tn(null);error=tn(null);whitelistPaths=It(()=>{const n=new Set;return this.state().installedMiners.forEach(r=>{r.miner_binary&&n.add(r.miner_binary),r.config_path&&n.add(r.config_path)}),this.state().runningMiners.forEach(r=>{r.configPath&&n.add(r.configPath)}),Array.from(n)});installMiner(n){this.actionInProgress.set(`install-${n}`),this.error.set(null),this.minerService.installMiner(n).subscribe({next:()=>{this.actionInProgress.set(null)},error:r=>{this.handleError(r,`Failed to install ${n}`)}})}uninstallMiner(n){this.actionInProgress.set(`uninstall-${n}`),this.error.set(null),this.minerService.uninstallMiner(n).subscribe({next:()=>{this.actionInProgress.set(null)},error:r=>{this.handleError(r,`Failed to uninstall ${n}`)}})}handleError(n,r){console.error(n),this.actionInProgress.set(null),this.error.set(n.error&&n.error.error?`${r}: ${n.error.error}`:"string"==typeof n.error&&n.error.length<200?`${r}: ${n.error}`:`${r}. Please check the console for details.`)}static \u0275fac=function(r){return new(r||e)};static \u0275cmp=qo({type:e,selectors:[["snider-mining-admin"]],decls:17,vars:3,consts:[[1,"admin-panel"],[1,"card-error"],[1,"miner-list"],[1,"miner-item"],[1,"section-title"],[1,"path-list"],["slot","header"],["name","exclamation-triangle",2,"font-size","1.5rem"],["variant","danger","size","small",3,"disabled"],["variant","success","size","small",3,"disabled"],["variant","danger","size","small",3,"click","disabled"],[1,"button-spinner"],["name","trash","slot","prefix"],["variant","success","size","small",3,"click","disabled"],["name","download","slot","prefix"]],template:function(r,i){1&r&&(He(0,"div",0),or(1,xY,6,1,"wa-card",1),He(2,"h4"),te(3,"Manage Miners"),$e(),He(4,"div",2),La(5,OY,5,2,"div",3,DY,!1,RY,3,0,"div",3),$e(),He(8,"h4",4),te(9,"Antivirus Whitelist Paths"),$e(),He(10,"div",5)(11,"p"),te(12,"To prevent antivirus software from interfering, please add the following paths to your exclusion list:"),$e(),He(13,"ul"),La(14,NY,3,1,"li",null,zI,!1,PY,2,0,"li"),$e()()()),2&r&&(he(),sr(i.error()?1:-1),he(4),Fa(i.state().manageableMiners),he(9),Fa(i.whitelistPaths()))},dependencies:[ql],styles:["[_nghost-%COMP%]{display:block;font-family:sans-serif;width:100%}.admin-card[_ngcontent-%COMP%]{width:100%;box-sizing:border-box}.header-title[_ngcontent-%COMP%]{display:flex;align-items:center;gap:.5rem}.card-error[_ngcontent-%COMP%]{margin-bottom:1rem}.miner-list[_ngcontent-%COMP%]{display:flex;flex-direction:column;gap:1rem}.miner-item[_ngcontent-%COMP%]{display:flex;justify-content:space-between;align-items:center;padding:.5rem;border-radius:.25rem;border:1px solid #e0e0e0}.section-title[_ngcontent-%COMP%]{margin-top:1.5rem;border-top:1px solid #e0e0e0;padding-top:1.5rem}.path-list[_ngcontent-%COMP%] ul[_ngcontent-%COMP%]{list-style:none;padding:.5rem;margin:0;font-family:monospace;border-radius:.25rem;border:1px solid #e0e0e0;font-size:.749rem}.path-list[_ngcontent-%COMP%] li[_ngcontent-%COMP%]{padding:.25rem 0}.button-spinner[_ngcontent-%COMP%]{font-size:1em;margin:0}"]})}return e})();const FY=(e,t)=>t.id,BY=(e,t)=>t.name;function VY(e,t){if(1&e&&(He(0,"wa-option",5),te(1),$e()),2&e){const n=t.$implicit,r=Pe(3);li("value",n.name)("selected",n.name===r.editingProfile.minerType),he(),Pi(" ",n.name," ")}}function $Y(e,t){if(1&e){const n=Zo();He(0,"div",2)(1,"wa-input",3),ar("input",function(i){return Dt(n),Gt(Pe(2).onNameInput(i))}),$e(),He(2,"wa-select",4),ar("change",function(i){return Dt(n),Gt(Pe(2).onMinerTypeChange(i))}),La(3,VY,2,3,"wa-option",5,BY),$e(),He(5,"wa-input",6),ar("input",function(i){return Dt(n),Gt(Pe(2).onPoolInput(i))}),$e(),He(6,"wa-input",7),ar("input",function(i){return Dt(n),Gt(Pe(2).onWalletInput(i))}),$e(),He(7,"div",8)(8,"wa-checkbox",9),ar("change",function(i){return Dt(n),Gt(Pe(2).onTlsChange(i))}),te(9," TLS "),$e(),He(10,"wa-checkbox",9),ar("change",function(i){return Dt(n),Gt(Pe(2).onHugePagesChange(i))}),te(11," Huge Pages "),$e()(),He(12,"div",10)(13,"wa-button",11),ar("click",function(){return Dt(n),Gt(Pe(2).updateProfile())}),te(14,"Save"),$e(),He(15,"wa-button",12),ar("click",function(){return Dt(n),Gt(Pe(2).cancelEdit())}),te(16,"Cancel"),$e()()()}if(2&e){const n=Pe(2);he(),li("value",n.editingProfile.name),he(),li("value",n.editingProfile.minerType),he(),Fa(n.state().manageableMiners),he(2),li("value",n.editingProfile.config.pool),he(),li("value",n.editingProfile.config.wallet),he(2),li("checked",n.editingProfile.config.tls),he(2),li("checked",n.editingProfile.config.hugePages)}}function jY(e,t){if(1&e){const n=Zo();He(0,"span"),te(1),$e(),He(2,"div",10)(3,"wa-button",13),ar("click",function(){Dt(n);const i=Pe().$implicit;return Gt(Pe().startMiner(i.id))}),te(4,"Start"),$e(),He(5,"wa-button",14),ar("click",function(){Dt(n);const i=Pe().$implicit;return Gt(Pe().editProfile(i))}),te(6,"Edit"),$e(),He(7,"wa-button",15),ar("click",function(){Dt(n);const i=Pe().$implicit;return Gt(Pe().deleteProfile(i.id))}),te(8,"Delete"),$e()()}if(2&e){const n=Pe().$implicit;he(),Gf("",n.name," (",n.minerType,")")}}function HY(e,t){if(1&e&&(He(0,"div",1),or(1,$Y,17,6,"div",2)(2,jY,9,2),$e()),2&e){const n=t.$implicit,r=Pe();he(),sr(r.editingProfile&&r.editingProfile.id===n.id?1:2)}}function UY(e,t){1&e&&(He(0,"p"),te(1,"No profiles created yet."),$e())}let zY=(()=>{class e{minerService=Y(Zl);state=this.minerService.state;editingProfile=null;onNameInput(n){this.editingProfile&&(this.editingProfile.name=n.target.value)}onMinerTypeChange(n){this.editingProfile&&(this.editingProfile.minerType=n.target.value)}onPoolInput(n){this.editingProfile&&(this.editingProfile.config.pool=n.target.value)}onWalletInput(n){this.editingProfile&&(this.editingProfile.config.wallet=n.target.value)}onTlsChange(n){this.editingProfile&&(this.editingProfile.config.tls=n.target.checked)}onHugePagesChange(n){this.editingProfile&&(this.editingProfile.config.hugePages=n.target.checked)}startMiner(n){this.minerService.startMiner(n).subscribe()}deleteProfile(n){this.minerService.deleteProfile(n).subscribe()}editProfile(n){this.editingProfile=JSON.parse(JSON.stringify(n))}updateProfile(){this.editingProfile&&this.minerService.updateProfile(this.editingProfile).subscribe(()=>{this.editingProfile=null})}cancelEdit(){this.editingProfile=null}static \u0275fac=function(r){return new(r||e)};static \u0275cmp=qo({type:e,selectors:[["snider-mining-profile-list"]],decls:6,vars:1,consts:[[1,"profile-list"],[1,"profile-item"],[1,"profile-form"],["label","Profile Name",3,"input","value"],["label","Miner Type",3,"change","value"],[3,"value","selected"],["label","Pool Address",3,"input","value"],["label","Wallet Address",3,"input","value"],[1,"checkbox-group"],[3,"change","checked"],[1,"button-group"],[3,"click"],["variant","neutral",3,"click"],["size","small","variant","primary",3,"click"],["size","small",3,"click"],["size","small","variant","danger",3,"click"]],template:function(r,i){1&r&&(He(0,"div",0)(1,"h5"),te(2,"Existing Profiles"),$e(),La(3,HY,3,1,"div",1,FY,!1,UY,2,0,"p"),$e()),2&r&&(he(3),Fa(i.state().profiles))},dependencies:[ql,MD],styles:[".profile-list[_ngcontent-%COMP%]{display:flex;flex-direction:column;gap:1rem}.profile-item[_ngcontent-%COMP%]{display:flex;justify-content:space-between;align-items:center;padding:.5rem;border:1px solid #e0e0e0;border-radius:.25rem}.profile-form[_ngcontent-%COMP%]{display:flex;flex-direction:column;gap:1rem;padding:1rem;border:1px solid #e0e0e0;border-radius:.25rem;width:100%}"]})}return e})();var jn=class extends Gr{constructor(){super(...arguments),this.hasSlotController=new mp(this,"hint"),this.title="",this.name="",this._value=this.getAttribute("value")??null,this.size="medium",this.disabled=!1,this.indeterminate=!1,this.checked=this.hasAttribute("checked"),this.defaultChecked=this.hasAttribute("checked"),this.form=null,this.required=!1,this.hint=""}static get validators(){const e=[r2({validationProperty:"checked",validationElement:Object.assign(document.createElement("input"),{type:"checkbox",required:!0})})];return[...super.validators,...e]}get value(){return this.checked?this._value||"on":null}set value(e){this._value=e}handleClick(){this.hasInteracted=!0,this.checked=!this.checked,this.indeterminate=!1,this.updateComplete.then(()=>{this.dispatchEvent(new Event("change",{bubbles:!0,composed:!0}))})}handleDefaultCheckedChange(){!this.hasInteracted&&this.checked!==this.defaultChecked&&(this.checked=this.defaultChecked,this.handleValueOrCheckedChange())}handleValueOrCheckedChange(){this.setValue(this.checked?this.value:null,this._value),this.updateValidity()}handleStateChange(){this.hasUpdated&&(this.input.checked=this.checked,this.input.indeterminate=this.indeterminate),this.customStates.set("checked",this.checked),this.customStates.set("indeterminate",this.indeterminate),this.updateValidity()}handleDisabledChange(){this.customStates.set("disabled",this.disabled)}willUpdate(e){super.willUpdate(e),e.has("defaultChecked")&&(this.hasInteracted||(this.checked=this.defaultChecked)),(e.has("value")||e.has("checked"))&&this.handleValueOrCheckedChange()}formResetCallback(){this.checked=this.defaultChecked,super.formResetCallback(),this.handleValueOrCheckedChange()}click(){this.input.click()}focus(e){this.input.focus(e)}blur(){this.input.blur()}render(){const e=this.hasSlotController.test("hint"),t=!!this.hint||!!e,n=!this.checked&&this.indeterminate,r=n?"indeterminate":"check",i=n?"indeterminate":"check";return At` + + + + ${this.hint} + + `}};jn.css=[WD,ad,":host {\n --checked-icon-color: var(--wa-color-brand-on-loud);\n --checked-icon-scale: 0.8;\n\n display: inline-flex;\n color: var(--wa-form-control-value-color);\n font-family: inherit;\n font-weight: var(--wa-form-control-value-font-weight);\n line-height: var(--wa-form-control-value-line-height);\n user-select: none;\n -webkit-user-select: none;\n}\n\n[part~='control'] {\n display: inline-flex;\n flex: 0 0 auto;\n position: relative;\n align-items: center;\n justify-content: center;\n width: var(--wa-form-control-toggle-size);\n height: var(--wa-form-control-toggle-size);\n border-color: var(--wa-form-control-border-color);\n border-radius: min(\n calc(var(--wa-form-control-toggle-size) * 0.375),\n var(--wa-border-radius-s)\n ); /* min prevents entirely circular checkbox */\n border-style: var(--wa-border-style);\n border-width: var(--wa-form-control-border-width);\n background-color: var(--wa-form-control-background-color);\n transition:\n background var(--wa-transition-normal),\n border-color var(--wa-transition-fast),\n box-shadow var(--wa-transition-fast),\n color var(--wa-transition-fast);\n transition-timing-function: var(--wa-transition-easing);\n\n margin-inline-end: 0.5em;\n}\n\n[part~='base'] {\n display: flex;\n align-items: flex-start;\n position: relative;\n color: currentColor;\n vertical-align: middle;\n cursor: pointer;\n}\n\n[part~='label'] {\n display: inline;\n}\n\n/* Checked */\n[part~='control']:has(:checked, :indeterminate) {\n color: var(--checked-icon-color);\n border-color: var(--wa-form-control-activated-color);\n background-color: var(--wa-form-control-activated-color);\n}\n\n/* Focus */\n[part~='control']:has(> input:focus-visible:not(:disabled)) {\n outline: var(--wa-focus-ring);\n outline-offset: var(--wa-focus-ring-offset);\n}\n\n/* Disabled */\n:host [part~='base']:has(input:disabled) {\n opacity: 0.5;\n cursor: not-allowed;\n}\n\ninput {\n position: absolute;\n padding: 0;\n margin: 0;\n height: 100%;\n width: 100%;\n opacity: 0;\n pointer-events: none;\n}\n\n[part~='icon'] {\n display: flex;\n scale: var(--checked-icon-scale);\n\n /* Without this, Safari renders the icon slightly to the left */\n &::part(svg) {\n translate: 0.0009765625em;\n }\n\n input:not(:checked, :indeterminate) + & {\n visibility: hidden;\n }\n}\n\n:host([required]) [part~='label']::after {\n content: var(--wa-form-control-required-content);\n color: var(--wa-form-control-required-content-color);\n margin-inline-start: var(--wa-form-control-required-content-offset);\n}\n"],jn.shadowRootOptions={...Gr.shadowRootOptions,delegatesFocus:!0},B([$n('input[type="checkbox"]')],jn.prototype,"input",2),B([W()],jn.prototype,"title",2),B([W({reflect:!0})],jn.prototype,"name",2),B([W({reflect:!0})],jn.prototype,"value",1),B([W({reflect:!0})],jn.prototype,"size",2),B([W({type:Boolean})],jn.prototype,"disabled",2),B([W({type:Boolean,reflect:!0})],jn.prototype,"indeterminate",2),B([W({type:Boolean,attribute:!1})],jn.prototype,"checked",2),B([W({type:Boolean,reflect:!0,attribute:"checked"})],jn.prototype,"defaultChecked",2),B([W({reflect:!0})],jn.prototype,"form",2),B([W({type:Boolean,reflect:!0})],jn.prototype,"required",2),B([W()],jn.prototype,"hint",2),B([Ir("defaultChecked")],jn.prototype,"handleDefaultCheckedChange",1),B([Ir(["checked","indeterminate"])],jn.prototype,"handleStateChange",1),B([Ir("disabled")],jn.prototype,"handleDisabledChange",1),jn=B([ui("wa-checkbox")],jn);const GY=(e,t)=>t.name;function qY(e,t){if(1&e&&(se(0,"wa-card",1)(1,"p"),te(2),ce()()),2&e){const n=Pe();he(2),Pt(n.error)}}function YY(e,t){if(1&e&&(se(0,"wa-card",2)(1,"p"),te(2),ce()()),2&e){const n=Pe();he(2),Pt(n.success)}}function XY(e,t){if(1&e&&(se(0,"wa-option",5),te(1),ce()),2&e){const n=t.$implicit;Ni("value",n.name),he(),Pt(n.name)}}let ZY=(()=>{class e{minerService=Y(Zl);state=this.minerService.state;model={id:"",name:"",minerType:"",config:{pool:"",wallet:"",tls:!0,hugePages:!0}};error=null;success=null;onNameInput(n){this.model.name=n.target.value}onMinerTypeChange(n){this.model.minerType=n.target.value}onPoolInput(n){this.model.config.pool=n.target.value}onWalletInput(n){this.model.config.wallet=n.target.value}onTlsChange(n){this.model.config.tls=n.target.checked}onHugePagesChange(n){this.model.config.hugePages=n.target.checked}createProfile(){this.error=null,this.success=null,this.model.name&&this.model.minerType&&this.model.config.pool&&this.model.config.wallet?this.minerService.createProfile(this.model).subscribe({next:()=>{this.success="Profile created successfully!",this.model={id:"",name:"",minerType:"",config:{pool:"",wallet:"",tls:!0,hugePages:!0}},setTimeout(()=>this.success=null,3e3)},error:n=>{console.error(n),this.error=n.error&&n.error.error?`Failed to create profile: ${n.error.error}`:"string"==typeof n.error&&n.error.length<200?`Failed to create profile: ${n.error}`:"An unknown error occurred while creating the profile."}}):this.error="Please fill out all required fields."}static \u0275fac=function(r){return new(r||e)};static \u0275cmp=qo({type:e,selectors:[["snider-mining-profile-create"]],decls:21,vars:8,consts:[["id","profile-create-form",1,"profile-form",3,"submit"],[1,"card-error"],[1,"card-success"],["name","name","label","Profile Name","required","",3,"input","value"],["name","minerType","label","Miner Type","required","",3,"change","value"],[3,"value"],["name","config"],["name","pool","label","Pool Address","required","",3,"input","value"],["name","wallet","label","Wallet Address","required","",3,"input","value"],[1,"checkbox-group"],["name","tls",3,"change","checked"],["name","hugePages",3,"change","checked"],["type","submit"]],template:function(r,i){1&r&&(se(0,"form",0),rn("submit",function(a){return i.createProfile(),a.preventDefault()}),se(1,"h5"),te(2,"Create New Profile"),ce(),or(3,qY,3,1,"wa-card",1),or(4,YY,3,1,"wa-card",2),se(5,"wa-input",3),rn("input",function(a){return i.onNameInput(a)}),ce(),se(6,"wa-select",4),rn("change",function(a){return i.onMinerTypeChange(a)}),La(7,XY,2,2,"wa-option",5,GY),ce(),se(9,"fieldset",6)(10,"legend"),te(11,"Configuration"),ce(),se(12,"wa-input",7),rn("input",function(a){return i.onPoolInput(a)}),ce(),se(13,"wa-input",8),rn("input",function(a){return i.onWalletInput(a)}),ce(),se(14,"div",9)(15,"wa-checkbox",10),rn("change",function(a){return i.onTlsChange(a)}),te(16," TLS "),ce(),se(17,"wa-checkbox",11),rn("change",function(a){return i.onHugePagesChange(a)}),te(18," Huge Pages "),ce()()(),se(19,"wa-button",12),te(20,"Create Profile"),ce()()),2&r&&(he(3),sr(i.error?3:-1),he(),sr(i.success?4:-1),he(),Ni("value",i.model.name),he(),Ni("value",i.model.minerType),he(),Fa(i.state().manageableMiners),he(5),Ni("value",i.model.config.pool),he(),Ni("value",i.model.config.wallet),he(2),Ni("checked",i.model.config.tls),he(2),Ni("checked",i.model.config.hugePages))},dependencies:[ql,MD,oP,WN,Cp],styles:[".profile-form[_ngcontent-%COMP%]{display:flex;flex-direction:column;gap:1.5rem}fieldset[_ngcontent-%COMP%]{display:flex;flex-direction:column;gap:1rem;border:1px solid var(--wa-color-neutral-300);border-radius:var(--wa-border-radius-medium);padding:1rem}legend[_ngcontent-%COMP%]{padding:0 .5rem;font-weight:700;color:var(--wa-color-neutral-700)}.checkbox-group[_ngcontent-%COMP%]{display:flex;gap:2rem;align-items:center}"]})}return e})();const s0=Rd(e=>function(){e(this),this.name="EmptyError",this.message="no elements in sequence"});function o2(...e){const t=ep(e),n=YO(e),{args:r,keys:i}=fR(e);if(0===r.length)return lr([],t);const o=new Ut(function QY(e,t,n=Io){return r=>{s2(t,()=>{const{length:i}=e,o=new Array(i);let a=i,u=i;for(let f=0;f{const y=lr(e[f],t);let w=!1;y.subscribe(mn(r,D=>{o[f]=D,w||(w=!0,u--),u||r.next(n(o.slice()))},()=>{--a||r.complete()}))},r)},r)}}(r,t,i?a=>gR(i,a):Io));return n?o.pipe(pR(n)):o}function s2(e,t,n){e?Ls(n,e,t):t()}function XD(...e){return function JY(){return sv(1)}()(lr(e,ep(e)))}function ZD(e){return new Ut(t=>{po(e()).subscribe(t)})}function a0(e,t){const n=ft(e)?e:()=>e,r=i=>i.error(n());return new Ut(t?i=>t.schedule(r,0,i):r)}function KD(){return nn((e,t)=>{let n=null;e._refCount++;const r=mn(t,void 0,void 0,void 0,()=>{if(!e||e._refCount<=0||0<--e._refCount)return void(n=null);const i=e._connection,o=n;n=null,i&&(!o||i===o)&&i.unsubscribe(),t.unsubscribe()});e.subscribe(r),r.closed||(n=e.connect())})}class a2 extends Ut{constructor(t,n){super(),this.source=t,this.subjectFactory=n,this._subject=null,this._refCount=0,this._connection=null,ba(t)&&(this.lift=t.lift)}_subscribe(t){return this.getSubject().subscribe(t)}getSubject(){const t=this._subject;return(!t||t.isStopped)&&(this._subject=this.subjectFactory()),this._subject}_teardown(){this._refCount=0;const{_connection:t}=this;this._subject=this._connection=null,t?.unsubscribe()}connect(){let t=this._connection;if(!t){t=this._connection=new Hn;const n=this.getSubject();t.add(this.source.subscribe(mn(n,void 0,()=>{this._teardown(),n.complete()},r=>{this._teardown(),n.error(r)},()=>this._teardown()))),t.closed&&(this._connection=null,t=Hn.EMPTY)}return t}refCount(){return KD()(this)}}function _d(e){return e<=0?()=>Li:nn((t,n)=>{let r=0;t.subscribe(mn(n,i=>{++r<=e&&(n.next(i),e<=r&&n.complete())}))})}function l0(e){return nn((t,n)=>{let r=!1;t.subscribe(mn(n,i=>{r=!0,n.next(i)},()=>{r||n.next(e),n.complete()}))})}function l2(e=tX){return nn((t,n)=>{let r=!1;t.subscribe(mn(n,i=>{r=!0,n.next(i)},()=>r?n.complete():n.error(e())))})}function tX(){return new s0}function ac(e,t){const n=arguments.length>=2;return r=>r.pipe(e?Bs((i,o)=>e(i,o,r)):Io,_d(1),n?l0(t):l2(()=>new s0))}function QD(e){return e<=0?()=>Li:nn((t,n)=>{let r=[];t.subscribe(mn(n,i=>{r.push(i),e{for(const i of r)n.next(i);n.complete()},void 0,()=>{r=null}))})}function c2(e){return nn((t,n)=>{po(e).subscribe(mn(n,()=>n.complete(),Ld)),!n.closed&&t.subscribe(n)})}let oX=(()=>{class e{_doc;constructor(n){this._doc=n}getTitle(){return this._doc.title}setTitle(n){this._doc.title=n||""}static \u0275fac=function(r){return new(r||e)(ke(ut))};static \u0275prov=pe({token:e,factory:e.\u0275fac,providedIn:"root"})}return e})();const Ue="primary",Mp=Symbol("RouteTitle");class cX{params;constructor(t){this.params=t||{}}has(t){return Object.prototype.hasOwnProperty.call(this.params,t)}get(t){if(this.has(t)){const n=this.params[t];return Array.isArray(n)?n[0]:n}return null}getAll(t){if(this.has(t)){const n=this.params[t];return Array.isArray(n)?n:[n]}return[]}get keys(){return Object.keys(this.params)}}function wd(e){return new cX(e)}function uX(e,t,n){const r=n.path.split("/");if(r.length>e.length||"full"===n.pathMatch&&(t.hasChildren()||r.lengthr[o]===i)}return e===t}function f2(e){return e.length>0?e[e.length-1]:null}function Us(e){return function KY(e){return!!e&&(e instanceof Ut||ft(e.lift)&&ft(e.subscribe))}(e)?e:Ff(e)?lr(Promise.resolve(e)):xe(e)}const hX={exact:function m2(e,t,n){if(!cc(e.segments,t.segments)||!c0(e.segments,t.segments,n)||e.numberOfChildren!==t.numberOfChildren)return!1;for(const r in t.children)if(!e.children[r]||!m2(e.children[r],t.children[r],n))return!1;return!0},subset:y2},p2={exact:function fX(e,t){return rs(e,t)},subset:function pX(e,t){return Object.keys(t).length<=Object.keys(e).length&&Object.keys(t).every(n=>h2(e[n],t[n]))},ignored:()=>!0};function g2(e,t,n){return hX[n.paths](e.root,t.root,n.matrixParams)&&p2[n.queryParams](e.queryParams,t.queryParams)&&!("exact"===n.fragment&&e.fragment!==t.fragment)}function y2(e,t,n){return v2(e,t,t.segments,n)}function v2(e,t,n,r){if(e.segments.length>n.length){const i=e.segments.slice(0,n.length);return!(!cc(i,n)||t.hasChildren()||!c0(i,n,r))}if(e.segments.length===n.length){if(!cc(e.segments,n)||!c0(e.segments,n,r))return!1;for(const i in t.children)if(!e.children[i]||!y2(e.children[i],t.children[i],r))return!1;return!0}{const i=n.slice(0,e.segments.length),o=n.slice(e.segments.length);return!!(cc(e.segments,i)&&c0(e.segments,i,r)&&e.children[Ue])&&v2(e.children[Ue],t,o,r)}}function c0(e,t,n){return t.every((r,i)=>p2[n](e[i].parameters,r.parameters))}class lc{root;queryParams;fragment;_queryParamMap;constructor(t=new Lt([],{}),n={},r=null){this.root=t,this.queryParams=n,this.fragment=r}get queryParamMap(){return this._queryParamMap??=wd(this.queryParams),this._queryParamMap}toString(){return yX.serialize(this)}}class Lt{segments;children;parent=null;constructor(t,n){this.segments=t,this.children=n,Object.values(n).forEach(r=>r.parent=this)}hasChildren(){return this.numberOfChildren>0}get numberOfChildren(){return Object.keys(this.children).length}toString(){return h0(this)}}class Tp{path;parameters;_parameterMap;constructor(t,n){this.path=t,this.parameters=n}get parameterMap(){return this._parameterMap??=wd(this.parameters),this._parameterMap}toString(){return w2(this)}}function cc(e,t){return e.length===t.length&&e.every((n,r)=>n.path===t[r].path)}let u0=(()=>{class e{static \u0275fac=function(r){return new(r||e)};static \u0275prov=pe({token:e,factory:()=>new d0,providedIn:"root"})}return e})();class d0{parse(t){const n=new TX(t);return new lc(n.parseRootSegment(),n.parseQueryParams(),n.parseFragment())}serialize(t){const n=`/${Ip(t.root,!0)}`,r=function _X(e){const t=Object.entries(e).map(([n,r])=>Array.isArray(r)?r.map(i=>`${f0(n)}=${f0(i)}`).join("&"):`${f0(n)}=${f0(r)}`).filter(n=>n);return t.length?`?${t.join("&")}`:""}(t.queryParams);return`${n}${r}${"string"==typeof t.fragment?`#${function vX(e){return encodeURI(e)}(t.fragment)}`:""}`}}const yX=new d0;function h0(e){return e.segments.map(t=>w2(t)).join("/")}function Ip(e,t){if(!e.hasChildren())return h0(e);if(t){const n=e.children[Ue]?Ip(e.children[Ue],!1):"",r=[];return Object.entries(e.children).forEach(([i,o])=>{i!==Ue&&r.push(`${i}:${Ip(o,!1)}`)}),r.length>0?`${n}(${r.join("//")})`:n}{const n=function mX(e,t){let n=[];return Object.entries(e.children).forEach(([r,i])=>{r===Ue&&(n=n.concat(t(i,r)))}),Object.entries(e.children).forEach(([r,i])=>{r!==Ue&&(n=n.concat(t(i,r)))}),n}(e,(r,i)=>i===Ue?[Ip(e.children[Ue],!1)]:[`${i}:${Ip(r,!1)}`]);return 1===Object.keys(e.children).length&&null!=e.children[Ue]?`${h0(e)}/${n[0]}`:`${h0(e)}/(${n.join("//")})`}}function b2(e){return encodeURIComponent(e).replace(/%40/g,"@").replace(/%3A/gi,":").replace(/%24/g,"$").replace(/%2C/gi,",")}function f0(e){return b2(e).replace(/%3B/gi,";")}function ex(e){return b2(e).replace(/\(/g,"%28").replace(/\)/g,"%29").replace(/%26/gi,"&")}function p0(e){return decodeURIComponent(e)}function _2(e){return p0(e.replace(/\+/g,"%20"))}function w2(e){return`${ex(e.path)}${function bX(e){return Object.entries(e).map(([t,n])=>`;${ex(t)}=${ex(n)}`).join("")}(e.parameters)}`}const wX=/^[^\/()?;#]+/;function tx(e){const t=e.match(wX);return t?t[0]:""}const CX=/^[^\/()?;=#]+/,DX=/^[^=?&#]+/,SX=/^[^&#]+/;class TX{url;remaining;constructor(t){this.url=t,this.remaining=t}parseRootSegment(){return this.consumeOptional("/"),""===this.remaining||this.peekStartsWith("?")||this.peekStartsWith("#")?new Lt([],{}):new Lt([],this.parseChildren())}parseQueryParams(){const t={};if(this.consumeOptional("?"))do{this.parseQueryParam(t)}while(this.consumeOptional("&"));return t}parseFragment(){return this.consumeOptional("#")?decodeURIComponent(this.remaining):null}parseChildren(){if(""===this.remaining)return{};this.consumeOptional("/");const t=[];for(this.peekStartsWith("(")||t.push(this.parseSegment());this.peekStartsWith("/")&&!this.peekStartsWith("//")&&!this.peekStartsWith("/(");)this.capture("/"),t.push(this.parseSegment());let n={};this.peekStartsWith("/(")&&(this.capture("/"),n=this.parseParens(!0));let r={};return this.peekStartsWith("(")&&(r=this.parseParens(!1)),(t.length>0||Object.keys(n).length>0)&&(r[Ue]=new Lt(t,n)),r}parseSegment(){const t=tx(this.remaining);if(""===t&&this.peekStartsWith(";"))throw new G(4009,!1);return this.capture(t),new Tp(p0(t),this.parseMatrixParams())}parseMatrixParams(){const t={};for(;this.consumeOptional(";");)this.parseParam(t);return t}parseParam(t){const n=function EX(e){const t=e.match(CX);return t?t[0]:""}(this.remaining);if(!n)return;this.capture(n);let r="";if(this.consumeOptional("=")){const i=tx(this.remaining);i&&(r=i,this.capture(r))}t[p0(n)]=p0(r)}parseQueryParam(t){const n=function xX(e){const t=e.match(DX);return t?t[0]:""}(this.remaining);if(!n)return;this.capture(n);let r="";if(this.consumeOptional("=")){const a=function MX(e){const t=e.match(SX);return t?t[0]:""}(this.remaining);a&&(r=a,this.capture(r))}const i=_2(n),o=_2(r);if(t.hasOwnProperty(i)){let a=t[i];Array.isArray(a)||(a=[a],t[i]=a),a.push(o)}else t[i]=o}parseParens(t){const n={};for(this.capture("(");!this.consumeOptional(")")&&this.remaining.length>0;){const r=tx(this.remaining),i=this.remaining[r.length];if("/"!==i&&")"!==i&&";"!==i)throw new G(4010,!1);let o;r.indexOf(":")>-1?(o=r.slice(0,r.indexOf(":")),this.capture(o),this.capture(":")):t&&(o=Ue);const a=this.parseChildren();n[o??Ue]=1===Object.keys(a).length&&a[Ue]?a[Ue]:new Lt([],a),this.consumeOptional("//")}return n}peekStartsWith(t){return this.remaining.startsWith(t)}consumeOptional(t){return!!this.peekStartsWith(t)&&(this.remaining=this.remaining.substring(t.length),!0)}capture(t){if(!this.consumeOptional(t))throw new G(4011,!1)}}function C2(e){return e.segments.length>0?new Lt([],{[Ue]:e}):e}function E2(e){const t={};for(const[r,i]of Object.entries(e.children)){const o=E2(i);if(r===Ue&&0===o.segments.length&&o.hasChildren())for(const[a,u]of Object.entries(o.children))t[a]=u;else(o.segments.length>0||o.hasChildren())&&(t[r]=o)}return function IX(e){if(1===e.numberOfChildren&&e.children[Ue]){const t=e.children[Ue];return new Lt(e.segments.concat(t.segments),t.children)}return e}(new Lt(e.segments,t))}function uc(e){return e instanceof lc}function D2(e){let t;const i=C2(function n(o){const a={};for(const f of o.children){const y=n(f);a[f.outlet]=y}const u=new Lt(o.url,a);return o===e&&(t=u),u}(e.root));return t??i}function x2(e,t,n,r){let i=e;for(;i.parent;)i=i.parent;if(0===t.length)return nx(i,i,i,n,r);const o=function kX(e){if("string"==typeof e[0]&&1===e.length&&"/"===e[0])return new M2(!0,0,e);let t=0,n=!1;const r=e.reduce((i,o,a)=>{if("object"==typeof o&&null!=o){if(o.outlets){const u={};return Object.entries(o.outlets).forEach(([f,y])=>{u[f]="string"==typeof y?y.split("/"):y}),[...i,{outlets:u}]}if(o.segmentPath)return[...i,o.segmentPath]}return"string"!=typeof o?[...i,o]:0===a?(o.split("/").forEach((u,f)=>{0==f&&"."===u||(0==f&&""===u?n=!0:".."===u?t++:""!=u&&i.push(u))}),i):[...i,o]},[]);return new M2(n,t,r)}(t);if(o.toRoot())return nx(i,i,new Lt([],{}),n,r);const a=function OX(e,t,n){if(e.isAbsolute)return new m0(t,!0,0);if(!n)return new m0(t,!1,NaN);if(null===n.parent)return new m0(n,!0,0);const r=g0(e.commands[0])?0:1;return function RX(e,t,n){let r=e,i=t,o=n;for(;o>i;){if(o-=i,r=r.parent,!r)throw new G(4005,!1);i=r.segments.length}return new m0(r,!1,i-o)}(n,n.segments.length-1+r,e.numberOfDoubleDots)}(o,i,e),u=a.processChildren?kp(a.segmentGroup,a.index,o.commands):T2(a.segmentGroup,a.index,o.commands);return nx(i,a.segmentGroup,u,n,r)}function g0(e){return"object"==typeof e&&null!=e&&!e.outlets&&!e.segmentPath}function Ap(e){return"object"==typeof e&&null!=e&&e.outlets}function nx(e,t,n,r,i){let a,o={};r&&Object.entries(r).forEach(([f,y])=>{o[f]=Array.isArray(y)?y.map(w=>`${w}`):`${y}`}),a=e===t?n:S2(e,t,n);const u=C2(E2(a));return new lc(u,o,i)}function S2(e,t,n){const r={};return Object.entries(e.children).forEach(([i,o])=>{r[i]=o===t?n:S2(o,t,n)}),new Lt(e.segments,r)}class M2{isAbsolute;numberOfDoubleDots;commands;constructor(t,n,r){if(this.isAbsolute=t,this.numberOfDoubleDots=n,this.commands=r,t&&r.length>0&&g0(r[0]))throw new G(4003,!1);const i=r.find(Ap);if(i&&i!==f2(r))throw new G(4004,!1)}toRoot(){return this.isAbsolute&&1===this.commands.length&&"/"==this.commands[0]}}class m0{segmentGroup;processChildren;index;constructor(t,n,r){this.segmentGroup=t,this.processChildren=n,this.index=r}}function T2(e,t,n){if(e??=new Lt([],{}),0===e.segments.length&&e.hasChildren())return kp(e,t,n);const r=function PX(e,t,n){let r=0,i=t;const o={match:!1,pathIndex:0,commandIndex:0};for(;i=n.length)return o;const a=e.segments[i],u=n[r];if(Ap(u))break;const f=`${u}`,y=r0&&void 0===f)break;if(f&&y&&"object"==typeof y&&void 0===y.outlets){if(!A2(f,y,a))return o;r+=2}else{if(!A2(f,{},a))return o;r++}i++}return{match:!0,pathIndex:i,commandIndex:r}}(e,t,n),i=n.slice(r.commandIndex);if(r.match&&r.pathIndexo!==Ue)&&e.children[Ue]&&1===e.numberOfChildren&&0===e.children[Ue].segments.length){const o=kp(e.children[Ue],t,n);return new Lt(e.segments,o.children)}return Object.entries(r).forEach(([o,a])=>{"string"==typeof a&&(a=[a]),null!==a&&(i[o]=T2(e.children[o],t,a))}),Object.entries(e.children).forEach(([o,a])=>{void 0===r[o]&&(i[o]=a)}),new Lt(e.segments,i)}}function rx(e,t,n){const r=e.segments.slice(0,t);let i=0;for(;i{"string"==typeof r&&(r=[r]),null!==r&&(t[n]=rx(new Lt([],{}),0,r))}),t}function I2(e){const t={};return Object.entries(e).forEach(([n,r])=>t[n]=`${r}`),t}function A2(e,t,n){return e==n.path&&rs(t,n.parameters)}const y0="imperative";var ot=function(e){return e[e.NavigationStart=0]="NavigationStart",e[e.NavigationEnd=1]="NavigationEnd",e[e.NavigationCancel=2]="NavigationCancel",e[e.NavigationError=3]="NavigationError",e[e.RoutesRecognized=4]="RoutesRecognized",e[e.ResolveStart=5]="ResolveStart",e[e.ResolveEnd=6]="ResolveEnd",e[e.GuardsCheckStart=7]="GuardsCheckStart",e[e.GuardsCheckEnd=8]="GuardsCheckEnd",e[e.RouteConfigLoadStart=9]="RouteConfigLoadStart",e[e.RouteConfigLoadEnd=10]="RouteConfigLoadEnd",e[e.ChildActivationStart=11]="ChildActivationStart",e[e.ChildActivationEnd=12]="ChildActivationEnd",e[e.ActivationStart=13]="ActivationStart",e[e.ActivationEnd=14]="ActivationEnd",e[e.Scroll=15]="Scroll",e[e.NavigationSkipped=16]="NavigationSkipped",e}(ot||{});class is{id;url;constructor(t,n){this.id=t,this.url=n}}class ix extends is{type=ot.NavigationStart;navigationTrigger;restoredState;constructor(t,n,r="imperative",i=null){super(t,n),this.navigationTrigger=r,this.restoredState=i}toString(){return`NavigationStart(id: ${this.id}, url: '${this.url}')`}}class dc extends is{urlAfterRedirects;type=ot.NavigationEnd;constructor(t,n,r){super(t,n),this.urlAfterRedirects=r}toString(){return`NavigationEnd(id: ${this.id}, url: '${this.url}', urlAfterRedirects: '${this.urlAfterRedirects}')`}}var Yr=function(e){return e[e.Redirect=0]="Redirect",e[e.SupersededByNewNavigation=1]="SupersededByNewNavigation",e[e.NoDataFromResolver=2]="NoDataFromResolver",e[e.GuardRejected=3]="GuardRejected",e[e.Aborted=4]="Aborted",e}(Yr||{}),ox=function(e){return e[e.IgnoredSameUrlNavigation=0]="IgnoredSameUrlNavigation",e[e.IgnoredByUrlHandlingStrategy=1]="IgnoredByUrlHandlingStrategy",e}(ox||{});class hc extends is{reason;code;type=ot.NavigationCancel;constructor(t,n,r,i){super(t,n),this.reason=r,this.code=i}toString(){return`NavigationCancel(id: ${this.id}, url: '${this.url}')`}}class Op extends is{reason;code;type=ot.NavigationSkipped;constructor(t,n,r,i){super(t,n),this.reason=r,this.code=i}}class sx extends is{error;target;type=ot.NavigationError;constructor(t,n,r,i){super(t,n),this.error=r,this.target=i}toString(){return`NavigationError(id: ${this.id}, url: '${this.url}', error: ${this.error})`}}class k2 extends is{urlAfterRedirects;state;type=ot.RoutesRecognized;constructor(t,n,r,i){super(t,n),this.urlAfterRedirects=r,this.state=i}toString(){return`RoutesRecognized(id: ${this.id}, url: '${this.url}', urlAfterRedirects: '${this.urlAfterRedirects}', state: ${this.state})`}}class FX extends is{urlAfterRedirects;state;type=ot.GuardsCheckStart;constructor(t,n,r,i){super(t,n),this.urlAfterRedirects=r,this.state=i}toString(){return`GuardsCheckStart(id: ${this.id}, url: '${this.url}', urlAfterRedirects: '${this.urlAfterRedirects}', state: ${this.state})`}}class BX extends is{urlAfterRedirects;state;shouldActivate;type=ot.GuardsCheckEnd;constructor(t,n,r,i,o){super(t,n),this.urlAfterRedirects=r,this.state=i,this.shouldActivate=o}toString(){return`GuardsCheckEnd(id: ${this.id}, url: '${this.url}', urlAfterRedirects: '${this.urlAfterRedirects}', state: ${this.state}, shouldActivate: ${this.shouldActivate})`}}class VX extends is{urlAfterRedirects;state;type=ot.ResolveStart;constructor(t,n,r,i){super(t,n),this.urlAfterRedirects=r,this.state=i}toString(){return`ResolveStart(id: ${this.id}, url: '${this.url}', urlAfterRedirects: '${this.urlAfterRedirects}', state: ${this.state})`}}class $X extends is{urlAfterRedirects;state;type=ot.ResolveEnd;constructor(t,n,r,i){super(t,n),this.urlAfterRedirects=r,this.state=i}toString(){return`ResolveEnd(id: ${this.id}, url: '${this.url}', urlAfterRedirects: '${this.urlAfterRedirects}', state: ${this.state})`}}class jX{route;type=ot.RouteConfigLoadStart;constructor(t){this.route=t}toString(){return`RouteConfigLoadStart(path: ${this.route.path})`}}class HX{route;type=ot.RouteConfigLoadEnd;constructor(t){this.route=t}toString(){return`RouteConfigLoadEnd(path: ${this.route.path})`}}class UX{snapshot;type=ot.ChildActivationStart;constructor(t){this.snapshot=t}toString(){return`ChildActivationStart(path: '${this.snapshot.routeConfig&&this.snapshot.routeConfig.path||""}')`}}class zX{snapshot;type=ot.ChildActivationEnd;constructor(t){this.snapshot=t}toString(){return`ChildActivationEnd(path: '${this.snapshot.routeConfig&&this.snapshot.routeConfig.path||""}')`}}class WX{snapshot;type=ot.ActivationStart;constructor(t){this.snapshot=t}toString(){return`ActivationStart(path: '${this.snapshot.routeConfig&&this.snapshot.routeConfig.path||""}')`}}class GX{snapshot;type=ot.ActivationEnd;constructor(t){this.snapshot=t}toString(){return`ActivationEnd(path: '${this.snapshot.routeConfig&&this.snapshot.routeConfig.path||""}')`}}class ax{}class v0{url;navigationBehaviorOptions;constructor(t,n){this.url=t,this.navigationBehaviorOptions=n}}function _o(e){return e.outlet||Ue}function Cd(e){if(!e)return null;if(e.routeConfig?._injector)return e.routeConfig._injector;for(let t=e.parent;t;t=t.parent){const n=t.routeConfig;if(n?._loadedInjector)return n._loadedInjector;if(n?._injector)return n._injector}return null}class JX{rootInjector;outlet=null;route=null;children;attachRef=null;get injector(){return Cd(this.route?.snapshot)??this.rootInjector}constructor(t){this.rootInjector=t,this.children=new Rp(this.rootInjector)}}let Rp=(()=>{class e{rootInjector;contexts=new Map;constructor(n){this.rootInjector=n}onChildOutletCreated(n,r){const i=this.getOrCreateContext(n);i.outlet=r,this.contexts.set(n,i)}onChildOutletDestroyed(n){const r=this.getContext(n);r&&(r.outlet=null,r.attachRef=null)}onOutletDeactivated(){const n=this.contexts;return this.contexts=new Map,n}onOutletReAttached(n){this.contexts=n}getOrCreateContext(n){let r=this.getContext(n);return r||(r=new JX(this.rootInjector),this.contexts.set(n,r)),r}getContext(n){return this.contexts.get(n)||null}static \u0275fac=function(r){return new(r||e)(ke(Dn))};static \u0275prov=pe({token:e,factory:e.\u0275fac,providedIn:"root"})}return e})();class O2{_root;constructor(t){this._root=t}get root(){return this._root.value}parent(t){const n=this.pathFromRoot(t);return n.length>1?n[n.length-2]:null}children(t){const n=lx(t,this._root);return n?n.children.map(r=>r.value):[]}firstChild(t){const n=lx(t,this._root);return n&&n.children.length>0?n.children[0].value:null}siblings(t){const n=cx(t,this._root);return n.length<2?[]:n[n.length-2].children.map(i=>i.value).filter(i=>i!==t)}pathFromRoot(t){return cx(t,this._root).map(n=>n.value)}}function lx(e,t){if(e===t.value)return t;for(const n of t.children){const r=lx(e,n);if(r)return r}return null}function cx(e,t){if(e===t.value)return[t];for(const n of t.children){const r=cx(e,n);if(r.length)return r.unshift(t),r}return[]}class wo{value;children;constructor(t,n){this.value=t,this.children=n}toString(){return`TreeNode(${this.value})`}}function Ed(e){const t={};return e&&e.children.forEach(n=>t[n.value.outlet]=n),t}class R2 extends O2{snapshot;constructor(t,n){super(t),this.snapshot=n,ux(this,t)}toString(){return this.snapshot.toString()}}function N2(e){const t=function eZ(e){const o=new _0([],{},{},"",{},Ue,e,null,{});return new P2("",new wo(o,[]))}(e),n=new yr([new Tp("",{})]),r=new yr({}),i=new yr({}),o=new yr({}),a=new yr(""),u=new Dd(n,r,o,a,i,Ue,e,t.root);return u.snapshot=t.root,new R2(new wo(u,[]),t)}class Dd{urlSubject;paramsSubject;queryParamsSubject;fragmentSubject;dataSubject;outlet;component;snapshot;_futureSnapshot;_routerState;_paramMap;_queryParamMap;title;url;params;queryParams;fragment;data;constructor(t,n,r,i,o,a,u,f){this.urlSubject=t,this.paramsSubject=n,this.queryParamsSubject=r,this.fragmentSubject=i,this.dataSubject=o,this.outlet=a,this.component=u,this._futureSnapshot=f,this.title=this.dataSubject?.pipe(tt(y=>y[Mp]))??xe(void 0),this.url=t,this.params=n,this.queryParams=r,this.fragment=i,this.data=o}get routeConfig(){return this._futureSnapshot.routeConfig}get root(){return this._routerState.root}get parent(){return this._routerState.parent(this)}get firstChild(){return this._routerState.firstChild(this)}get children(){return this._routerState.children(this)}get pathFromRoot(){return this._routerState.pathFromRoot(this)}get paramMap(){return this._paramMap??=this.params.pipe(tt(t=>wd(t))),this._paramMap}get queryParamMap(){return this._queryParamMap??=this.queryParams.pipe(tt(t=>wd(t))),this._queryParamMap}toString(){return this.snapshot?this.snapshot.toString():`Future(${this._futureSnapshot})`}}function b0(e,t,n="emptyOnly"){let r;const{routeConfig:i}=e;return r=null===t||"always"!==n&&""!==i?.path&&(t.component||t.routeConfig?.loadComponent)?{params:{...e.params},data:{...e.data},resolve:{...e.data,...e._resolvedData??{}}}:{params:{...t.params,...e.params},data:{...t.data,...e.data},resolve:{...e.data,...t.data,...i?.data,...e._resolvedData}},i&&F2(i)&&(r.resolve[Mp]=i.title),r}class _0{url;params;queryParams;fragment;data;outlet;component;routeConfig;_resolve;_resolvedData;_routerState;_paramMap;_queryParamMap;get title(){return this.data?.[Mp]}constructor(t,n,r,i,o,a,u,f,y){this.url=t,this.params=n,this.queryParams=r,this.fragment=i,this.data=o,this.outlet=a,this.component=u,this.routeConfig=f,this._resolve=y}get root(){return this._routerState.root}get parent(){return this._routerState.parent(this)}get firstChild(){return this._routerState.firstChild(this)}get children(){return this._routerState.children(this)}get pathFromRoot(){return this._routerState.pathFromRoot(this)}get paramMap(){return this._paramMap??=wd(this.params),this._paramMap}get queryParamMap(){return this._queryParamMap??=wd(this.queryParams),this._queryParamMap}toString(){return`Route(url:'${this.url.map(r=>r.toString()).join("/")}', path:'${this.routeConfig?this.routeConfig.path:""}')`}}class P2 extends O2{url;constructor(t,n){super(n),this.url=t,ux(this,n)}toString(){return L2(this._root)}}function ux(e,t){t.value._routerState=e,t.children.forEach(n=>ux(e,n))}function L2(e){const t=e.children.length>0?` { ${e.children.map(L2).join(", ")} } `:"";return`${e.value}${t}`}function dx(e){if(e.snapshot){const t=e.snapshot,n=e._futureSnapshot;e.snapshot=n,rs(t.queryParams,n.queryParams)||e.queryParamsSubject.next(n.queryParams),t.fragment!==n.fragment&&e.fragmentSubject.next(n.fragment),rs(t.params,n.params)||e.paramsSubject.next(n.params),function dX(e,t){if(e.length!==t.length)return!1;for(let n=0;nrs(n.parameters,t[r].parameters))}(e.url,t.url);return n&&!(!e.parent!=!t.parent)&&(!e.parent||hx(e.parent,t.parent))}function F2(e){return"string"==typeof e.title||null===e.title}const tZ=new ee("");let B2=(()=>{class e{activated=null;get activatedComponentRef(){return this.activated}_activatedRoute=null;name=Ue;activateEvents=new In;deactivateEvents=new In;attachEvents=new In;detachEvents=new In;routerOutletData=Qy();parentContexts=Y(Rp);location=Y(fo);changeDetector=Y(Kf);inputBinder=Y(fx,{optional:!0});supportsBindingToComponentInputs=!0;ngOnChanges(n){if(n.name){const{firstChange:r,previousValue:i}=n.name;if(r)return;this.isTrackedInParentContexts(i)&&(this.deactivate(),this.parentContexts.onChildOutletDestroyed(i)),this.initializeOutletWithName()}}ngOnDestroy(){this.isTrackedInParentContexts(this.name)&&this.parentContexts.onChildOutletDestroyed(this.name),this.inputBinder?.unsubscribeFromRouteData(this)}isTrackedInParentContexts(n){return this.parentContexts.getContext(n)?.outlet===this}ngOnInit(){this.initializeOutletWithName()}initializeOutletWithName(){if(this.parentContexts.onChildOutletCreated(this.name,this),this.activated)return;const n=this.parentContexts.getContext(this.name);n?.route&&(n.attachRef?this.attach(n.attachRef,n.route):this.activateWith(n.route,n.injector))}get isActivated(){return!!this.activated}get component(){if(!this.activated)throw new G(4012,!1);return this.activated.instance}get activatedRoute(){if(!this.activated)throw new G(4012,!1);return this._activatedRoute}get activatedRouteData(){return this._activatedRoute?this._activatedRoute.snapshot.data:{}}detach(){if(!this.activated)throw new G(4012,!1);this.location.detach();const n=this.activated;return this.activated=null,this._activatedRoute=null,this.detachEvents.emit(n.instance),n}attach(n,r){this.activated=n,this._activatedRoute=r,this.location.insert(n.hostView),this.inputBinder?.bindActivatedRouteToOutletComponent(this),this.attachEvents.emit(n.instance)}deactivate(){if(this.activated){const n=this.component;this.activated.destroy(),this.activated=null,this._activatedRoute=null,this.deactivateEvents.emit(n)}}activateWith(n,r){if(this.isActivated)throw new G(4013,!1);this._activatedRoute=n;const i=this.location,a=n.snapshot.component,u=this.parentContexts.getOrCreateContext(this.name).children,f=new nZ(n,u,i.injector,this.routerOutletData);this.activated=i.createComponent(a,{index:i.length,injector:f,environmentInjector:r}),this.changeDetector.markForCheck(),this.inputBinder?.bindActivatedRouteToOutletComponent(this),this.activateEvents.emit(this.activated.instance)}static \u0275fac=function(r){return new(r||e)};static \u0275dir=Ne({type:e,selectors:[["router-outlet"]],inputs:{name:"name",routerOutletData:[1,"routerOutletData"]},outputs:{activateEvents:"activate",deactivateEvents:"deactivate",attachEvents:"attach",detachEvents:"detach"},exportAs:["outlet"],features:[si]})}return e})();class nZ{route;childContexts;parent;outletData;constructor(t,n,r,i){this.route=t,this.childContexts=n,this.parent=r,this.outletData=i}get(t,n){return t===Dd?this.route:t===Rp?this.childContexts:t===tZ?this.outletData:this.parent.get(t,n)}}const fx=new ee("");let V2=(()=>{class e{static \u0275fac=function(r){return new(r||e)};static \u0275cmp=qo({type:e,selectors:[["ng-component"]],exportAs:["emptyRouterOutlet"],decls:1,vars:0,template:function(r,i){1&r&&Yn(0,"router-outlet")},dependencies:[B2],encapsulation:2})}return e})();function px(e){const t=e.children&&e.children.map(px),n=t?{...e,children:t}:{...e};return!n.component&&!n.loadComponent&&(t||n.loadChildren)&&n.outlet&&n.outlet!==Ue&&(n.component=V2),n}function Np(e,t,n){if(n&&e.shouldReuseRoute(t.value,n.value.snapshot)){const r=n.value;r._futureSnapshot=t.value;const i=function iZ(e,t,n){return t.children.map(r=>{for(const i of n.children)if(e.shouldReuseRoute(r.value,i.value.snapshot))return Np(e,r,i);return Np(e,r)})}(e,t,n);return new wo(r,i)}{if(e.shouldAttach(t.value)){const o=e.retrieve(t.value);if(null!==o){const a=o.route;return a.value._futureSnapshot=t.value,a.children=t.children.map(u=>Np(e,u)),a}}const r=function oZ(e){return new Dd(new yr(e.url),new yr(e.params),new yr(e.queryParams),new yr(e.fragment),new yr(e.data),e.outlet,e.component,e)}(t.value),i=t.children.map(o=>Np(e,o));return new wo(r,i)}}class gx{redirectTo;navigationBehaviorOptions;constructor(t,n){this.redirectTo=t,this.navigationBehaviorOptions=n}}const $2="ngNavigationCancelingError";function w0(e,t){const{redirectTo:n,navigationBehaviorOptions:r}=uc(t)?{redirectTo:t,navigationBehaviorOptions:void 0}:t,i=j2(!1,Yr.Redirect);return i.url=n,i.navigationBehaviorOptions=r,i}function j2(e,t){const n=new Error(`NavigationCancelingError: ${e||""}`);return n[$2]=!0,n.cancellationCode=t,n}function H2(e){return!!e&&e[$2]}class lZ{routeReuseStrategy;futureState;currState;forwardEvent;inputBindingEnabled;constructor(t,n,r,i,o){this.routeReuseStrategy=t,this.futureState=n,this.currState=r,this.forwardEvent=i,this.inputBindingEnabled=o}activate(t){const n=this.futureState._root,r=this.currState?this.currState._root:null;this.deactivateChildRoutes(n,r,t),dx(this.futureState.root),this.activateChildRoutes(n,r,t)}deactivateChildRoutes(t,n,r){const i=Ed(n);t.children.forEach(o=>{const a=o.value.outlet;this.deactivateRoutes(o,i[a],r),delete i[a]}),Object.values(i).forEach(o=>{this.deactivateRouteAndItsChildren(o,r)})}deactivateRoutes(t,n,r){const i=t.value,o=n?n.value:null;if(i===o)if(i.component){const a=r.getContext(i.outlet);a&&this.deactivateChildRoutes(t,n,a.children)}else this.deactivateChildRoutes(t,n,r);else o&&this.deactivateRouteAndItsChildren(n,r)}deactivateRouteAndItsChildren(t,n){t.value.component&&this.routeReuseStrategy.shouldDetach(t.value.snapshot)?this.detachAndStoreRouteSubtree(t,n):this.deactivateRouteAndOutlet(t,n)}detachAndStoreRouteSubtree(t,n){const r=n.getContext(t.value.outlet),i=r&&t.value.component?r.children:n,o=Ed(t);for(const a of Object.values(o))this.deactivateRouteAndItsChildren(a,i);if(r&&r.outlet){const a=r.outlet.detach(),u=r.children.onOutletDeactivated();this.routeReuseStrategy.store(t.value.snapshot,{componentRef:a,route:t,contexts:u})}}deactivateRouteAndOutlet(t,n){const r=n.getContext(t.value.outlet),i=r&&t.value.component?r.children:n,o=Ed(t);for(const a of Object.values(o))this.deactivateRouteAndItsChildren(a,i);r&&(r.outlet&&(r.outlet.deactivate(),r.children.onOutletDeactivated()),r.attachRef=null,r.route=null)}activateChildRoutes(t,n,r){const i=Ed(n);t.children.forEach(o=>{this.activateRoutes(o,i[o.value.outlet],r),this.forwardEvent(new GX(o.value.snapshot))}),t.children.length&&this.forwardEvent(new zX(t.value.snapshot))}activateRoutes(t,n,r){const i=t.value,o=n?n.value:null;if(dx(i),i===o)if(i.component){const a=r.getOrCreateContext(i.outlet);this.activateChildRoutes(t,n,a.children)}else this.activateChildRoutes(t,n,r);else if(i.component){const a=r.getOrCreateContext(i.outlet);if(this.routeReuseStrategy.shouldAttach(i.snapshot)){const u=this.routeReuseStrategy.retrieve(i.snapshot);this.routeReuseStrategy.store(i.snapshot,null),a.children.onOutletReAttached(u.contexts),a.attachRef=u.componentRef,a.route=u.route.value,a.outlet&&a.outlet.attach(u.componentRef,u.route.value),dx(u.route.value),this.activateChildRoutes(t,null,a.children)}else a.attachRef=null,a.route=i,a.outlet&&a.outlet.activateWith(i,a.injector),this.activateChildRoutes(t,null,a.children)}else this.activateChildRoutes(t,null,r)}}class U2{path;route;constructor(t){this.path=t,this.route=this.path[this.path.length-1]}}class C0{component;route;constructor(t,n){this.component=t,this.route=n}}function cZ(e,t,n){const r=e._root;return Pp(r,t?t._root:null,n,[r.value])}function xd(e,t){const n=Symbol(),r=t.get(e,n);return r===n?"function"!=typeof e||function H0(e){return null!==Sc(e)}(e)?t.get(e):e:r}function Pp(e,t,n,r,i={canDeactivateChecks:[],canActivateChecks:[]}){const o=Ed(t);return e.children.forEach(a=>{(function dZ(e,t,n,r,i={canDeactivateChecks:[],canActivateChecks:[]}){const o=e.value,a=t?t.value:null,u=n?n.getContext(e.value.outlet):null;if(a&&o.routeConfig===a.routeConfig){const f=function hZ(e,t,n){if("function"==typeof n)return n(e,t);switch(n){case"pathParamsChange":return!cc(e.url,t.url);case"pathParamsOrQueryParamsChange":return!cc(e.url,t.url)||!rs(e.queryParams,t.queryParams);case"always":return!0;case"paramsOrQueryParamsChange":return!hx(e,t)||!rs(e.queryParams,t.queryParams);default:return!hx(e,t)}}(a,o,o.routeConfig.runGuardsAndResolvers);f?i.canActivateChecks.push(new U2(r)):(o.data=a.data,o._resolvedData=a._resolvedData),Pp(e,t,o.component?u?u.children:null:n,r,i),f&&u&&u.outlet&&u.outlet.isActivated&&i.canDeactivateChecks.push(new C0(u.outlet.component,a))}else a&&Lp(t,u,i),i.canActivateChecks.push(new U2(r)),Pp(e,null,o.component?u?u.children:null:n,r,i)})(a,o[a.value.outlet],n,r.concat([a.value]),i),delete o[a.value.outlet]}),Object.entries(o).forEach(([a,u])=>Lp(u,n.getContext(a),i)),i}function Lp(e,t,n){const r=Ed(e),i=e.value;Object.entries(r).forEach(([o,a])=>{Lp(a,i.component?t?t.children.getContext(o):null:t,n)}),n.canDeactivateChecks.push(new C0(i.component&&t&&t.outlet&&t.outlet.isActivated?t.outlet.component:null,i))}function Fp(e){return"function"==typeof e}function z2(e){return e instanceof s0||"EmptyError"===e?.name}const E0=Symbol("INITIAL_VALUE");function Sd(){return Sr(e=>o2(e.map(t=>t.pipe(_d(1),function eX(...e){const t=ep(e);return nn((n,r)=>{(t?XD(e,n,t):XD(e,n)).subscribe(r)})}(E0)))).pipe(tt(t=>{for(const n of t)if(!0!==n){if(n===E0)return E0;if(!1===n||bZ(n))return n}return!0}),Bs(t=>t!==E0),_d(1)))}function bZ(e){return uc(e)||e instanceof gx}function W2(e){return function A0(...e){return Cc(e)}(vn(t=>{if("boolean"!=typeof t)throw w0(0,t)}),tt(t=>!0===t))}class mx{segmentGroup;constructor(t){this.segmentGroup=t||null}}class yx extends Error{urlTree;constructor(t){super(),this.urlTree=t}}function Md(e){return a0(new mx(e))}function AZ(e){return a0(new G(4e3,!1))}class OZ{urlSerializer;urlTree;constructor(t,n){this.urlSerializer=t,this.urlTree=n}lineralizeSegments(t,n){let r=[],i=n.root;for(;;){if(r=r.concat(i.segments),0===i.numberOfChildren)return xe(r);if(i.numberOfChildren>1||!i.children[Ue])return AZ();i=i.children[Ue]}}applyRedirectCommands(t,n,r,i,o){return function RZ(e,t,n){if("string"==typeof e)return xe(e);const r=e,{queryParams:i,fragment:o,routeConfig:a,url:u,outlet:f,params:y,data:w,title:D}=t;return Us(xn(n,()=>r({params:y,data:w,queryParams:i,fragment:o,routeConfig:a,url:u,outlet:f,title:D})))}(n,i,o).pipe(tt(a=>{if(a instanceof lc)throw new yx(a);const u=this.applyRedirectCreateUrlTree(a,this.urlSerializer.parse(a),t,r);if("/"===a[0])throw new yx(u);return u}))}applyRedirectCreateUrlTree(t,n,r,i){const o=this.createSegmentGroup(t,n.root,r,i);return new lc(o,this.createQueryParams(n.queryParams,this.urlTree.queryParams),n.fragment)}createQueryParams(t,n){const r={};return Object.entries(t).forEach(([i,o])=>{if("string"==typeof o&&":"===o[0]){const u=o.substring(1);r[i]=n[u]}else r[i]=o}),r}createSegmentGroup(t,n,r,i){const o=this.createSegments(t,n.segments,r,i);let a={};return Object.entries(n.children).forEach(([u,f])=>{a[u]=this.createSegmentGroup(t,f,r,i)}),new Lt(o,a)}createSegments(t,n,r,i){return n.map(o=>":"===o.path[0]?this.findPosParam(t,o,i):this.findOrReturn(o,r))}findPosParam(t,n,r){const i=r[n.path.substring(1)];if(!i)throw new G(4001,!1);return i}findOrReturn(t,n){let r=0;for(const i of n){if(i.path===t.path)return n.splice(r),i;r++}return t}}const vx={matched:!1,consumedSegments:[],remainingSegments:[],parameters:{},positionalParamSegments:{}};function NZ(e,t,n,r,i){const o=G2(e,t,n);return o.matched?(r=function YX(e,t){return e.providers&&!e._injector&&(e._injector=Uw(e.providers,t,`Route: ${e.path}`)),e._injector??t}(t,r),function IZ(e,t,n,r){const i=t.canMatch;return i&&0!==i.length?xe(i.map(a=>{const u=xd(a,e);return Us(function vZ(e){return e&&Fp(e.canMatch)}(u)?u.canMatch(t,n):xn(e,()=>u(t,n)))})).pipe(Sd(),W2()):xe(!0)}(r,t,n).pipe(tt(a=>!0===a?o:{...vx}))):xe(o)}function G2(e,t,n){if("**"===t.path)return function PZ(e){return{matched:!0,parameters:e.length>0?f2(e).parameters:{},consumedSegments:e,remainingSegments:[],positionalParamSegments:{}}}(n);if(""===t.path)return"full"===t.pathMatch&&(e.hasChildren()||n.length>0)?{...vx}:{matched:!0,consumedSegments:[],remainingSegments:n,parameters:{},positionalParamSegments:{}};const i=(t.matcher||uX)(n,e,t);if(!i)return{...vx};const o={};Object.entries(i.posParams??{}).forEach(([u,f])=>{o[u]=f.path});const a=i.consumed.length>0?{...o,...i.consumed[i.consumed.length-1].parameters}:o;return{matched:!0,consumedSegments:i.consumed,remainingSegments:n.slice(i.consumed.length),parameters:a,positionalParamSegments:i.posParams??{}}}function q2(e,t,n,r){return n.length>0&&function BZ(e,t,n){return n.some(r=>D0(e,t,r)&&_o(r)!==Ue)}(e,n,r)?{segmentGroup:new Lt(t,FZ(r,new Lt(n,e.children))),slicedSegments:[]}:0===n.length&&function VZ(e,t,n){return n.some(r=>D0(e,t,r))}(e,n,r)?{segmentGroup:new Lt(e.segments,LZ(e,n,r,e.children)),slicedSegments:n}:{segmentGroup:new Lt(e.segments,e.children),slicedSegments:n}}function LZ(e,t,n,r){const i={};for(const o of n)if(D0(e,t,o)&&!r[_o(o)]){const a=new Lt([],{});i[_o(o)]=a}return{...r,...i}}function FZ(e,t){const n={};n[Ue]=t;for(const r of e)if(""===r.path&&_o(r)!==Ue){const i=new Lt([],{});n[_o(r)]=i}return n}function D0(e,t,n){return(!(e.hasChildren()||t.length>0)||"full"!==n.pathMatch)&&""===n.path}class jZ{}class zZ{injector;configLoader;rootComponentType;config;urlTree;paramsInheritanceStrategy;urlSerializer;applyRedirects;absoluteRedirectCount=0;allowRedirects=!0;constructor(t,n,r,i,o,a,u){this.injector=t,this.configLoader=n,this.rootComponentType=r,this.config=i,this.urlTree=o,this.paramsInheritanceStrategy=a,this.urlSerializer=u,this.applyRedirects=new OZ(this.urlSerializer,this.urlTree)}noMatchError(t){return new G(4002,`'${t.segmentGroup}'`)}recognize(){const t=q2(this.urlTree.root,[],[],this.config).segmentGroup;return this.match(t).pipe(tt(({children:n,rootSnapshot:r})=>{const i=new wo(r,n),o=new P2("",i),a=function AX(e,t,n=null,r=null){return x2(D2(e),t,n,r)}(r,[],this.urlTree.queryParams,this.urlTree.fragment);return a.queryParams=this.urlTree.queryParams,o.url=this.urlSerializer.serialize(a),{state:o,tree:a}}))}match(t){const n=new _0([],Object.freeze({}),Object.freeze({...this.urlTree.queryParams}),this.urlTree.fragment,Object.freeze({}),Ue,this.rootComponentType,null,{});return this.processSegmentGroup(this.injector,this.config,t,Ue,n).pipe(tt(r=>({children:r,rootSnapshot:n})),Tr(r=>{if(r instanceof yx)return this.urlTree=r.urlTree,this.match(r.urlTree.root);throw r instanceof mx?this.noMatchError(r):r}))}processSegmentGroup(t,n,r,i,o){return 0===r.segments.length&&r.hasChildren()?this.processChildren(t,n,r,o):this.processSegment(t,n,r,r.segments,i,!0,o).pipe(tt(a=>a instanceof wo?[a]:[]))}processChildren(t,n,r,i){const o=[];for(const a of Object.keys(r.children))"primary"===a?o.unshift(a):o.push(a);return lr(o).pipe(rp(a=>{const u=r.children[a],f=function QX(e,t){const n=e.filter(r=>_o(r)===t);return n.push(...e.filter(r=>_o(r)!==t)),n}(n,a);return this.processSegmentGroup(t,f,u,a,i)}),function rX(e,t){return nn(function nX(e,t,n,r,i){return(o,a)=>{let u=n,f=t,y=0;o.subscribe(mn(a,w=>{const D=y++;f=u?e(f,w,D):(u=!0,w),r&&a.next(f)},i&&(()=>{u&&a.next(f),a.complete()})))}}(e,t,arguments.length>=2,!0))}((a,u)=>(a.push(...u),a)),l0(null),function iX(e,t){const n=arguments.length>=2;return r=>r.pipe(e?Bs((i,o)=>e(i,o,r)):Io,QD(1),n?l0(t):l2(()=>new s0))}(),xr(a=>{if(null===a)return Md(r);const u=Y2(a);return function WZ(e){e.sort((t,n)=>t.value.outlet===Ue?-1:n.value.outlet===Ue?1:t.value.outlet.localeCompare(n.value.outlet))}(u),xe(u)}))}processSegment(t,n,r,i,o,a,u){return lr(n).pipe(rp(f=>this.processSegmentAgainstRoute(f._injector??t,n,f,r,i,o,a,u).pipe(Tr(y=>{if(y instanceof mx)return xe(null);throw y}))),ac(f=>!!f),Tr(f=>{if(z2(f))return function $Z(e,t,n){return 0===t.length&&!e.children[n]}(r,i,o)?xe(new jZ):Md(r);throw f}))}processSegmentAgainstRoute(t,n,r,i,o,a,u,f){return _o(r)===a||a!==Ue&&D0(i,o,r)?void 0===r.redirectTo?this.matchSegmentAgainstRoute(t,i,r,o,a,f):this.allowRedirects&&u?this.expandSegmentAgainstRouteUsingRedirect(t,i,n,r,o,a,f):Md(i):Md(i)}expandSegmentAgainstRouteUsingRedirect(t,n,r,i,o,a,u){const{matched:f,parameters:y,consumedSegments:w,positionalParamSegments:D,remainingSegments:k}=G2(n,i,o);if(!f)return Md(n);"string"==typeof i.redirectTo&&"/"===i.redirectTo[0]&&(this.absoluteRedirectCount++,this.absoluteRedirectCount>31&&(this.allowRedirects=!1));const A=new _0(o,y,Object.freeze({...this.urlTree.queryParams}),this.urlTree.fragment,X2(i),_o(i),i.component??i._loadedComponent??null,i,Z2(i)),N=b0(A,u,this.paramsInheritanceStrategy);return A.params=Object.freeze(N.params),A.data=Object.freeze(N.data),this.applyRedirects.applyRedirectCommands(w,i.redirectTo,D,A,t).pipe(Sr(H=>this.applyRedirects.lineralizeSegments(i,H)),xr(H=>this.processSegment(t,r,n,H.concat(k),a,!1,u)))}matchSegmentAgainstRoute(t,n,r,i,o,a){const u=NZ(n,r,i,t);return"**"===r.path&&(n.children={}),u.pipe(Sr(f=>f.matched?this.getChildConfig(t=r._injector??t,r,i).pipe(Sr(({routes:y})=>{const w=r._loadedInjector??t,{parameters:D,consumedSegments:k,remainingSegments:A}=f,N=new _0(k,D,Object.freeze({...this.urlTree.queryParams}),this.urlTree.fragment,X2(r),_o(r),r.component??r._loadedComponent??null,r,Z2(r)),L=b0(N,a,this.paramsInheritanceStrategy);N.params=Object.freeze(L.params),N.data=Object.freeze(L.data);const{segmentGroup:H,slicedSegments:V}=q2(n,k,A,y);if(0===V.length&&H.hasChildren())return this.processChildren(w,y,H,N).pipe(tt(ie=>new wo(N,ie)));if(0===y.length&&0===V.length)return xe(new wo(N,[]));const z=_o(r)===o;return this.processSegment(w,y,H,V,z?Ue:o,!0,N).pipe(tt(ie=>new wo(N,ie instanceof wo?[ie]:[])))})):Md(n)))}getChildConfig(t,n,r){return n.children?xe({routes:n.children,injector:t}):n.loadChildren?void 0!==n._loadedRoutes?xe({routes:n._loadedRoutes,injector:n._loadedInjector}):function TZ(e,t,n,r){const i=t.canLoad;return void 0===i||0===i.length?xe(!0):xe(i.map(a=>{const u=xd(a,e);return Us(function pZ(e){return e&&Fp(e.canLoad)}(u)?u.canLoad(t,n):xn(e,()=>u(t,n)))})).pipe(Sd(),W2())}(t,n,r).pipe(xr(i=>i?this.configLoader.loadChildren(t,n).pipe(vn(o=>{n._loadedRoutes=o.routes,n._loadedInjector=o.injector})):function kZ(){return a0(j2(!1,Yr.GuardRejected))}())):xe({routes:[],injector:t})}}function GZ(e){const t=e.value.routeConfig;return t&&""===t.path}function Y2(e){const t=[],n=new Set;for(const r of e){if(!GZ(r)){t.push(r);continue}const i=t.find(o=>r.value.routeConfig===o.value.routeConfig);void 0!==i?(i.children.push(...r.children),n.add(i)):t.push(r)}for(const r of n){const i=Y2(r.children);t.push(new wo(r.value,i))}return t.filter(r=>!n.has(r))}function X2(e){return e.data||{}}function Z2(e){return e.resolve||{}}function K2(e){const t=e.children.map(n=>K2(n)).flat();return[e,...t]}function bx(e){return Sr(t=>{const n=e(t);return n?lr(n).pipe(tt(()=>t)):xe(t)})}let Q2=(()=>{class e{buildTitle(n){let r,i=n.root;for(;void 0!==i;)r=this.getResolvedTitleForRoute(i)??r,i=i.children.find(o=>o.outlet===Ue);return r}getResolvedTitleForRoute(n){return n.data[Mp]}static \u0275fac=function(r){return new(r||e)};static \u0275prov=pe({token:e,factory:()=>Y(QZ),providedIn:"root"})}return e})(),QZ=(()=>{class e extends Q2{title;constructor(n){super(),this.title=n}updateTitle(n){const r=this.buildTitle(n);void 0!==r&&this.title.setTitle(r)}static \u0275fac=function(r){return new(r||e)(ke(oX))};static \u0275prov=pe({token:e,factory:e.\u0275fac,providedIn:"root"})}return e})();const x0=new ee("",{providedIn:"root",factory:()=>({})}),_x=new ee("");let J2=(()=>{class e{componentLoaders=new WeakMap;childrenLoaders=new WeakMap;onLoadStartListener;onLoadEndListener;compiler=Y(_4);loadComponent(n,r){if(this.componentLoaders.get(r))return this.componentLoaders.get(r);if(r._loadedComponent)return xe(r._loadedComponent);this.onLoadStartListener&&this.onLoadStartListener(r);const i=Us(xn(n,()=>r.loadComponent())).pipe(tt(eL),Sr(tL),vn(a=>{this.onLoadEndListener&&this.onLoadEndListener(r),r._loadedComponent=a}),bv(()=>{this.componentLoaders.delete(r)})),o=new a2(i,()=>new Kn).pipe(KD());return this.componentLoaders.set(r,o),o}loadChildren(n,r){if(this.childrenLoaders.get(r))return this.childrenLoaders.get(r);if(r._loadedRoutes)return xe({routes:r._loadedRoutes,injector:r._loadedInjector});this.onLoadStartListener&&this.onLoadStartListener(r);const o=function JZ(e,t,n,r){return Us(xn(n,()=>e.loadChildren())).pipe(tt(eL),Sr(tL),xr(i=>i instanceof RT||Array.isArray(i)?xe(i):lr(t.compileModuleAsync(i))),tt(i=>{r&&r(e);let o,a,u=!1;return Array.isArray(i)?(a=i,!0):(o=i.create(n).injector,a=o.get(_x,[],{optional:!0,self:!0}).flat()),{routes:a.map(px),injector:o}}))}(r,this.compiler,n,this.onLoadEndListener).pipe(bv(()=>{this.childrenLoaders.delete(r)})),a=new a2(o,()=>new Kn).pipe(KD());return this.childrenLoaders.set(r,a),a}static \u0275fac=function(r){return new(r||e)};static \u0275prov=pe({token:e,factory:e.\u0275fac,providedIn:"root"})}return e})();function eL(e){return function eK(e){return e&&"object"==typeof e&&"default"in e}(e)?e.default:e}function tL(e){return xe(e)}let wx=(()=>{class e{static \u0275fac=function(r){return new(r||e)};static \u0275prov=pe({token:e,factory:()=>Y(tK),providedIn:"root"})}return e})(),tK=(()=>{class e{shouldProcessUrl(n){return!0}extract(n){return n}merge(n,r){return n}static \u0275fac=function(r){return new(r||e)};static \u0275prov=pe({token:e,factory:e.\u0275fac,providedIn:"root"})}return e})();const nK=new ee(""),oK=new ee("");let sK=(()=>{class e{currentNavigation=tn(null,{equal:()=>!1});currentTransition=null;lastSuccessfulNavigation=null;events=new Kn;transitionAbortWithErrorSubject=new Kn;configLoader=Y(J2);environmentInjector=Y(Dn);destroyRef=Y(Cr);urlSerializer=Y(u0);rootContexts=Y(Rp);location=Y(pa);inputBindingEnabled=null!==Y(fx,{optional:!0});titleStrategy=Y(Q2);options=Y(x0,{optional:!0})||{};paramsInheritanceStrategy=this.options.paramsInheritanceStrategy||"emptyOnly";urlHandlingStrategy=Y(wx);createViewTransition=Y(nK,{optional:!0});navigationErrorHandler=Y(oK,{optional:!0});navigationId=0;get hasRequestedNavigation(){return 0!==this.navigationId}transitions;afterPreactivation=()=>xe(void 0);rootComponentType=null;destroyed=!1;constructor(){this.configLoader.onLoadEndListener=i=>this.events.next(new HX(i)),this.configLoader.onLoadStartListener=i=>this.events.next(new jX(i)),this.destroyRef.onDestroy(()=>{this.destroyed=!0})}complete(){this.transitions?.complete()}handleNavigationRequest(n){const r=++this.navigationId;Vt(()=>{this.transitions?.next({...n,extractedUrl:this.urlHandlingStrategy.extract(n.rawUrl),targetSnapshot:null,targetRouterState:null,guards:{canActivateChecks:[],canDeactivateChecks:[]},guardsResult:null,abortController:new AbortController,id:r})})}setupNavigations(n){return this.transitions=new yr(null),this.transitions.pipe(Bs(r=>null!==r),Sr(r=>{let i=!1;return xe(r).pipe(Sr(o=>{if(this.navigationId>r.id)return this.cancelNavigationTransition(r,"",Yr.SupersededByNewNavigation),Li;this.currentTransition=r,this.currentNavigation.set({id:o.id,initialUrl:o.rawUrl,extractedUrl:o.extractedUrl,targetBrowserUrl:"string"==typeof o.extras.browserUrl?this.urlSerializer.parse(o.extras.browserUrl):o.extras.browserUrl,trigger:o.source,extras:o.extras,previousNavigation:this.lastSuccessfulNavigation?{...this.lastSuccessfulNavigation,previousNavigation:null}:null,abort:()=>o.abortController.abort()});const a=!n.navigated||this.isUpdatingInternalState()||this.isUpdatedBrowserUrl();if(!a&&"reload"!==(o.extras.onSameUrlNavigation??n.onSameUrlNavigation))return this.events.next(new Op(o.id,this.urlSerializer.serialize(o.rawUrl),"",ox.IgnoredSameUrlNavigation)),o.resolve(!1),Li;if(this.urlHandlingStrategy.shouldProcessUrl(o.rawUrl))return xe(o).pipe(Sr(f=>(this.events.next(new ix(f.id,this.urlSerializer.serialize(f.extractedUrl),f.source,f.restoredState)),f.id!==this.navigationId?Li:Promise.resolve(f))),function qZ(e,t,n,r,i,o){return xr(a=>function HZ(e,t,n,r,i,o,a="emptyOnly"){return new zZ(e,t,n,r,i,a,o).recognize()}(e,t,n,r,a.extractedUrl,i,o).pipe(tt(({state:u,tree:f})=>({...a,targetSnapshot:u,urlAfterRedirects:f}))))}(this.environmentInjector,this.configLoader,this.rootComponentType,n.config,this.urlSerializer,this.paramsInheritanceStrategy),vn(f=>{r.targetSnapshot=f.targetSnapshot,r.urlAfterRedirects=f.urlAfterRedirects,this.currentNavigation.update(w=>(w.finalUrl=f.urlAfterRedirects,w));const y=new k2(f.id,this.urlSerializer.serialize(f.extractedUrl),this.urlSerializer.serialize(f.urlAfterRedirects),f.targetSnapshot);this.events.next(y)}));if(a&&this.urlHandlingStrategy.shouldProcessUrl(o.currentRawUrl)){const{id:f,extractedUrl:y,source:w,restoredState:D,extras:k}=o,A=new ix(f,this.urlSerializer.serialize(y),w,D);this.events.next(A);const N=N2(this.rootComponentType).snapshot;return this.currentTransition=r={...o,targetSnapshot:N,urlAfterRedirects:y,extras:{...k,skipLocationChange:!1,replaceUrl:!1}},this.currentNavigation.update(L=>(L.finalUrl=y,L)),xe(r)}return this.events.next(new Op(o.id,this.urlSerializer.serialize(o.extractedUrl),"",ox.IgnoredByUrlHandlingStrategy)),o.resolve(!1),Li}),vn(o=>{const a=new FX(o.id,this.urlSerializer.serialize(o.extractedUrl),this.urlSerializer.serialize(o.urlAfterRedirects),o.targetSnapshot);this.events.next(a)}),tt(o=>(this.currentTransition=r={...o,guards:cZ(o.targetSnapshot,o.currentSnapshot,this.rootContexts)},r)),function _Z(e,t){return xr(n=>{const{targetSnapshot:r,currentSnapshot:i,guards:{canActivateChecks:o,canDeactivateChecks:a}}=n;return 0===a.length&&0===o.length?xe({...n,guardsResult:!0}):function wZ(e,t,n,r){return lr(e).pipe(xr(i=>function MZ(e,t,n,r,i){const o=t&&t.routeConfig?t.routeConfig.canDeactivate:null;return o&&0!==o.length?xe(o.map(u=>{const f=Cd(t)??i,y=xd(u,f);return Us(function yZ(e){return e&&Fp(e.canDeactivate)}(y)?y.canDeactivate(e,t,n,r):xn(f,()=>y(e,t,n,r))).pipe(ac())})).pipe(Sd()):xe(!0)}(i.component,i.route,n,t,r)),ac(i=>!0!==i,!0))}(a,r,i,e).pipe(xr(u=>u&&function fZ(e){return"boolean"==typeof e}(u)?function CZ(e,t,n,r){return lr(t).pipe(rp(i=>XD(function DZ(e,t){return null!==e&&t&&t(new UX(e)),xe(!0)}(i.route.parent,r),function EZ(e,t){return null!==e&&t&&t(new WX(e)),xe(!0)}(i.route,r),function SZ(e,t,n){const r=t[t.length-1],o=t.slice(0,t.length-1).reverse().map(a=>function uZ(e){const t=e.routeConfig?e.routeConfig.canActivateChild:null;return t&&0!==t.length?{node:e,guards:t}:null}(a)).filter(a=>null!==a).map(a=>ZD(()=>xe(a.guards.map(f=>{const y=Cd(a.node)??n,w=xd(f,y);return Us(function mZ(e){return e&&Fp(e.canActivateChild)}(w)?w.canActivateChild(r,e):xn(y,()=>w(r,e))).pipe(ac())})).pipe(Sd())));return xe(o).pipe(Sd())}(e,i.path,n),function xZ(e,t,n){const r=t.routeConfig?t.routeConfig.canActivate:null;if(!r||0===r.length)return xe(!0);const i=r.map(o=>ZD(()=>{const a=Cd(t)??n,u=xd(o,a);return Us(function gZ(e){return e&&Fp(e.canActivate)}(u)?u.canActivate(t,e):xn(a,()=>u(t,e))).pipe(ac())}));return xe(i).pipe(Sd())}(e,i.route,n))),ac(i=>!0!==i,!0))}(r,o,e,t):xe(u)),tt(u=>({...n,guardsResult:u})))})}(this.environmentInjector,o=>this.events.next(o)),vn(o=>{if(r.guardsResult=o.guardsResult,o.guardsResult&&"boolean"!=typeof o.guardsResult)throw w0(0,o.guardsResult);const a=new BX(o.id,this.urlSerializer.serialize(o.extractedUrl),this.urlSerializer.serialize(o.urlAfterRedirects),o.targetSnapshot,!!o.guardsResult);this.events.next(a)}),Bs(o=>!!o.guardsResult||(this.cancelNavigationTransition(o,"",Yr.GuardRejected),!1)),bx(o=>{if(0!==o.guards.canActivateChecks.length)return xe(o).pipe(vn(a=>{const u=new VX(a.id,this.urlSerializer.serialize(a.extractedUrl),this.urlSerializer.serialize(a.urlAfterRedirects),a.targetSnapshot);this.events.next(u)}),Sr(a=>{let u=!1;return xe(a).pipe(function YZ(e,t){return xr(n=>{const{targetSnapshot:r,guards:{canActivateChecks:i}}=n;if(!i.length)return xe(n);const o=new Set(i.map(f=>f.route)),a=new Set;for(const f of o)if(!a.has(f))for(const y of K2(f))a.add(y);let u=0;return lr(a).pipe(rp(f=>o.has(f)?function XZ(e,t,n,r){const i=e.routeConfig,o=e._resolve;return void 0!==i?.title&&!F2(i)&&(o[Mp]=i.title),ZD(()=>(e.data=b0(e,e.parent,n).resolve,function ZZ(e,t,n,r){const i=JD(e);if(0===i.length)return xe({});const o={};return lr(i).pipe(xr(a=>function KZ(e,t,n,r){const i=Cd(t)??r,o=xd(e,i);return Us(o.resolve?o.resolve(t,n):xn(i,()=>o(t,n)))}(e[a],t,n,r).pipe(ac(),vn(u=>{if(u instanceof gx)throw w0(new d0,u);o[a]=u}))),QD(1),tt(()=>o),Tr(a=>z2(a)?Li:a0(a)))}(o,e,t,r).pipe(tt(a=>(e._resolvedData=a,e.data={...e.data,...a},null)))))}(f,r,e,t):(f.data=b0(f,f.parent,e).resolve,xe(void 0))),vn(()=>u++),QD(1),xr(f=>u===a.size?xe(n):Li))})}(this.paramsInheritanceStrategy,this.environmentInjector),vn({next:()=>u=!0,complete:()=>{u||this.cancelNavigationTransition(a,"",Yr.NoDataFromResolver)}}))}),vn(a=>{const u=new $X(a.id,this.urlSerializer.serialize(a.extractedUrl),this.urlSerializer.serialize(a.urlAfterRedirects),a.targetSnapshot);this.events.next(u)}))}),bx(o=>{const a=u=>{const f=[];if(u.routeConfig?.loadComponent){const y=Cd(u)??this.environmentInjector;f.push(this.configLoader.loadComponent(y,u.routeConfig).pipe(vn(w=>{u.component=w}),tt(()=>{})))}for(const y of u.children)f.push(...a(y));return f};return o2(a(o.targetSnapshot.root)).pipe(l0(null),_d(1))}),bx(()=>this.afterPreactivation()),Sr(()=>{const{currentSnapshot:o,targetSnapshot:a}=r,u=this.createViewTransition?.(this.environmentInjector,o.root,a.root);return u?lr(u).pipe(tt(()=>r)):xe(r)}),tt(o=>{const a=function rZ(e,t,n){const r=Np(e,t._root,n?n._root:void 0);return new R2(r,t)}(n.routeReuseStrategy,o.targetSnapshot,o.currentRouterState);return this.currentTransition=r={...o,targetRouterState:a},this.currentNavigation.update(u=>(u.targetRouterState=a,u)),r}),vn(()=>{this.events.next(new ax)}),((e,t,n,r)=>tt(i=>(new lZ(t,i.targetRouterState,i.currentRouterState,n,r).activate(e),i)))(this.rootContexts,n.routeReuseStrategy,o=>this.events.next(o),this.inputBindingEnabled),_d(1),c2(new Ut(o=>{const a=r.abortController.signal,u=()=>o.next();return a.addEventListener("abort",u),()=>a.removeEventListener("abort",u)}).pipe(Bs(()=>!i&&!r.targetRouterState),vn(()=>{this.cancelNavigationTransition(r,r.abortController.signal.reason+"",Yr.Aborted)}))),vn({next:o=>{i=!0,this.lastSuccessfulNavigation=Vt(this.currentNavigation),this.events.next(new dc(o.id,this.urlSerializer.serialize(o.extractedUrl),this.urlSerializer.serialize(o.urlAfterRedirects))),this.titleStrategy?.updateTitle(o.targetRouterState.snapshot),o.resolve(!0)},complete:()=>{i=!0}}),c2(this.transitionAbortWithErrorSubject.pipe(vn(o=>{throw o}))),bv(()=>{i||this.cancelNavigationTransition(r,"",Yr.SupersededByNewNavigation),this.currentTransition?.id===r.id&&(this.currentNavigation.set(null),this.currentTransition=null)}),Tr(o=>{if(this.destroyed)return r.resolve(!1),Li;if(i=!0,H2(o))this.events.next(new hc(r.id,this.urlSerializer.serialize(r.extractedUrl),o.message,o.cancellationCode)),function sZ(e){return H2(e)&&uc(e.url)}(o)?this.events.next(new v0(o.url,o.navigationBehaviorOptions)):r.resolve(!1);else{const a=new sx(r.id,this.urlSerializer.serialize(r.extractedUrl),o,r.targetSnapshot??void 0);try{const u=xn(this.environmentInjector,()=>this.navigationErrorHandler?.(a));if(!(u instanceof gx))throw this.events.next(a),o;{const{message:f,cancellationCode:y}=w0(0,u);this.events.next(new hc(r.id,this.urlSerializer.serialize(r.extractedUrl),f,y)),this.events.next(new v0(u.redirectTo,u.navigationBehaviorOptions))}}catch(u){this.options.resolveNavigationPromiseOnError?r.resolve(!1):r.reject(u)}}return Li}))}))}cancelNavigationTransition(n,r,i){const o=new hc(n.id,this.urlSerializer.serialize(n.extractedUrl),r,i);this.events.next(o),n.resolve(!1)}isUpdatingInternalState(){return this.currentTransition?.extractedUrl.toString()!==this.currentTransition?.currentUrlTree.toString()}isUpdatedBrowserUrl(){const n=this.urlHandlingStrategy.extract(this.urlSerializer.parse(this.location.path(!0))),r=Vt(this.currentNavigation),i=r?.targetBrowserUrl??r?.extractedUrl;return n.toString()!==i?.toString()&&!r?.extras.skipLocationChange}static \u0275fac=function(r){return new(r||e)};static \u0275prov=pe({token:e,factory:e.\u0275fac,providedIn:"root"})}return e})();function aK(e){return e!==y0}let lK=(()=>{class e{static \u0275fac=function(r){return new(r||e)};static \u0275prov=pe({token:e,factory:()=>Y(uK),providedIn:"root"})}return e})();class cK{shouldDetach(t){return!1}store(t,n){}shouldAttach(t){return!1}retrieve(t){return null}shouldReuseRoute(t,n){return t.routeConfig===n.routeConfig}}let uK=(()=>{class e extends cK{static \u0275fac=(()=>{let n;return function(i){return(n||(n=_(e)))(i||e)}})();static \u0275prov=pe({token:e,factory:e.\u0275fac,providedIn:"root"})}return e})(),nL=(()=>{class e{urlSerializer=Y(u0);options=Y(x0,{optional:!0})||{};canceledNavigationResolution=this.options.canceledNavigationResolution||"replace";location=Y(pa);urlHandlingStrategy=Y(wx);urlUpdateStrategy=this.options.urlUpdateStrategy||"deferred";currentUrlTree=new lc;getCurrentUrlTree(){return this.currentUrlTree}rawUrlTree=this.currentUrlTree;getRawUrlTree(){return this.rawUrlTree}createBrowserPath({finalUrl:n,initialUrl:r,targetBrowserUrl:i}){const o=void 0!==n?this.urlHandlingStrategy.merge(n,r):r,a=i??o;return a instanceof lc?this.urlSerializer.serialize(a):a}commitTransition({targetRouterState:n,finalUrl:r,initialUrl:i}){r&&n?(this.currentUrlTree=r,this.rawUrlTree=this.urlHandlingStrategy.merge(r,i),this.routerState=n):this.rawUrlTree=i}routerState=N2(null);getRouterState(){return this.routerState}stateMemento=this.createStateMemento();updateStateMemento(){this.stateMemento=this.createStateMemento()}createStateMemento(){return{rawUrlTree:this.rawUrlTree,currentUrlTree:this.currentUrlTree,routerState:this.routerState}}resetInternalState({finalUrl:n}){this.routerState=this.stateMemento.routerState,this.currentUrlTree=this.stateMemento.currentUrlTree,this.rawUrlTree=this.urlHandlingStrategy.merge(this.currentUrlTree,n??this.rawUrlTree)}static \u0275fac=function(r){return new(r||e)};static \u0275prov=pe({token:e,factory:()=>Y(dK),providedIn:"root"})}return e})(),dK=(()=>{class e extends nL{currentPageId=0;lastSuccessfulId=-1;restoredState(){return this.location.getState()}get browserPageId(){return"computed"!==this.canceledNavigationResolution?this.currentPageId:this.restoredState()?.\u0275routerPageId??this.currentPageId}registerNonRouterCurrentEntryChangeListener(n){return this.location.subscribe(r=>{"popstate"===r.type&&setTimeout(()=>{n(r.url,r.state,"popstate")})})}handleRouterEvent(n,r){n instanceof ix?this.updateStateMemento():n instanceof Op?this.commitTransition(r):n instanceof k2?"eager"===this.urlUpdateStrategy&&(r.extras.skipLocationChange||this.setBrowserUrl(this.createBrowserPath(r),r)):n instanceof ax?(this.commitTransition(r),"deferred"===this.urlUpdateStrategy&&!r.extras.skipLocationChange&&this.setBrowserUrl(this.createBrowserPath(r),r)):n instanceof hc&&n.code!==Yr.SupersededByNewNavigation&&n.code!==Yr.Redirect?this.restoreHistory(r):n instanceof sx?this.restoreHistory(r,!0):n instanceof dc&&(this.lastSuccessfulId=n.id,this.currentPageId=this.browserPageId)}setBrowserUrl(n,{extras:r,id:i}){const{replaceUrl:o,state:a}=r;if(this.location.isCurrentPathEqualTo(n)||o){const u=this.browserPageId,f={...a,...this.generateNgRouterState(i,u)};this.location.replaceState(n,"",f)}else{const u={...a,...this.generateNgRouterState(i,this.browserPageId+1)};this.location.go(n,"",u)}}restoreHistory(n,r=!1){if("computed"===this.canceledNavigationResolution){const o=this.currentPageId-this.browserPageId;0!==o?this.location.historyGo(o):this.getCurrentUrlTree()===n.finalUrl&&0===o&&(this.resetInternalState(n),this.resetUrlToCurrentUrlTree())}else"replace"===this.canceledNavigationResolution&&(r&&this.resetInternalState(n),this.resetUrlToCurrentUrlTree())}resetUrlToCurrentUrlTree(){this.location.replaceState(this.urlSerializer.serialize(this.getRawUrlTree()),"",this.generateNgRouterState(this.lastSuccessfulId,this.currentPageId))}generateNgRouterState(n,r){return"computed"===this.canceledNavigationResolution?{navigationId:n,\u0275routerPageId:r}:{navigationId:n}}static \u0275fac=(()=>{let n;return function(i){return(n||(n=_(e)))(i||e)}})();static \u0275prov=pe({token:e,factory:e.\u0275fac,providedIn:"root"})}return e})();const fK={paths:"exact",fragment:"ignored",matrixParams:"ignored",queryParams:"exact"},pK={paths:"subset",fragment:"ignored",matrixParams:"ignored",queryParams:"subset"};let Qa=(()=>{class e{get currentUrlTree(){return this.stateManager.getCurrentUrlTree()}get rawUrlTree(){return this.stateManager.getRawUrlTree()}disposed=!1;nonRouterCurrentEntryChangeSubscription;console=Y(lI);stateManager=Y(nL);options=Y(x0,{optional:!0})||{};pendingTasks=Y(qn);urlUpdateStrategy=this.options.urlUpdateStrategy||"deferred";navigationTransitions=Y(sK);urlSerializer=Y(u0);location=Y(pa);urlHandlingStrategy=Y(wx);injector=Y(Dn);_events=new Kn;get events(){return this._events}get routerState(){return this.stateManager.getRouterState()}navigated=!1;routeReuseStrategy=Y(lK);onSameUrlNavigation=this.options.onSameUrlNavigation||"ignore";config=Y(_x,{optional:!0})?.flat()??[];componentInputBindingEnabled=!!Y(fx,{optional:!0});currentNavigation=this.navigationTransitions.currentNavigation.asReadonly();constructor(){this.resetConfig(this.config),this.navigationTransitions.setupNavigations(this).subscribe({error:n=>{this.console.warn(n)}}),this.subscribeToNavigationEvents()}eventsSubscription=new Hn;subscribeToNavigationEvents(){const n=this.navigationTransitions.events.subscribe(r=>{try{const i=this.navigationTransitions.currentTransition,o=Vt(this.navigationTransitions.currentNavigation);if(null!==i&&null!==o)if(this.stateManager.handleRouterEvent(r,o),r instanceof hc&&r.code!==Yr.Redirect&&r.code!==Yr.SupersededByNewNavigation)this.navigated=!0;else if(r instanceof dc)this.navigated=!0;else if(r instanceof v0){const a=r.navigationBehaviorOptions,u=this.urlHandlingStrategy.merge(r.url,i.currentRawUrl),f={browserUrl:i.extras.browserUrl,info:i.extras.info,skipLocationChange:i.extras.skipLocationChange,replaceUrl:i.extras.replaceUrl||"eager"===this.urlUpdateStrategy||aK(i.source),...a};this.scheduleNavigation(u,y0,null,f,{resolve:i.resolve,reject:i.reject,promise:i.promise})}(function qX(e){return!(e instanceof ax||e instanceof v0)})(r)&&this._events.next(r)}catch(i){this.navigationTransitions.transitionAbortWithErrorSubject.next(i)}});this.eventsSubscription.add(n)}resetRootComponentType(n){this.routerState.root.component=n,this.navigationTransitions.rootComponentType=n}initialNavigation(){this.setUpLocationChangeListener(),this.navigationTransitions.hasRequestedNavigation||this.navigateToSyncWithBrowser(this.location.path(!0),y0,this.stateManager.restoredState())}setUpLocationChangeListener(){this.nonRouterCurrentEntryChangeSubscription??=this.stateManager.registerNonRouterCurrentEntryChangeListener((n,r,i)=>{this.navigateToSyncWithBrowser(n,i,r)})}navigateToSyncWithBrowser(n,r,i){const o={replaceUrl:!0},a=i?.navigationId?i:null;if(i){const f={...i};delete f.navigationId,delete f.\u0275routerPageId,0!==Object.keys(f).length&&(o.state=f)}const u=this.parseUrl(n);this.scheduleNavigation(u,r,a,o).catch(f=>{this.disposed||this.injector.get(ri)(f)})}get url(){return this.serializeUrl(this.currentUrlTree)}getCurrentNavigation(){return Vt(this.navigationTransitions.currentNavigation)}get lastSuccessfulNavigation(){return this.navigationTransitions.lastSuccessfulNavigation}resetConfig(n){this.config=n.map(px),this.navigated=!1}ngOnDestroy(){this.dispose()}dispose(){this._events.unsubscribe(),this.navigationTransitions.complete(),this.nonRouterCurrentEntryChangeSubscription&&(this.nonRouterCurrentEntryChangeSubscription.unsubscribe(),this.nonRouterCurrentEntryChangeSubscription=void 0),this.disposed=!0,this.eventsSubscription.unsubscribe()}createUrlTree(n,r={}){const{relativeTo:i,queryParams:o,fragment:a,queryParamsHandling:u,preserveFragment:f}=r,y=f?this.currentUrlTree.fragment:a;let D,w=null;switch(u??this.options.defaultQueryParamsHandling){case"merge":w={...this.currentUrlTree.queryParams,...o};break;case"preserve":w=this.currentUrlTree.queryParams;break;default:w=o||null}null!==w&&(w=this.removeEmptyProps(w));try{D=D2(i?i.snapshot:this.routerState.snapshot.root)}catch{("string"!=typeof n[0]||"/"!==n[0][0])&&(n=[]),D=this.currentUrlTree.root}return x2(D,n,w,y??null)}navigateByUrl(n,r={skipLocationChange:!1}){const i=uc(n)?n:this.parseUrl(n),o=this.urlHandlingStrategy.merge(i,this.rawUrlTree);return this.scheduleNavigation(o,y0,null,r)}navigate(n,r={skipLocationChange:!1}){return function gK(e){for(let t=0;t(null!=o&&(r[i]=o),r),{})}scheduleNavigation(n,r,i,o,a){if(this.disposed)return Promise.resolve(!1);let u,f,y;a?(u=a.resolve,f=a.reject,y=a.promise):y=new Promise((D,k)=>{u=D,f=k});const w=this.pendingTasks.add();return function hK(e,t){e.events.pipe(Bs(n=>n instanceof dc||n instanceof hc||n instanceof sx||n instanceof Op),tt(n=>n instanceof dc||n instanceof Op?0:n instanceof hc&&(n.code===Yr.Redirect||n.code===Yr.SupersededByNewNavigation)?2:1),Bs(n=>2!==n),_d(1)).subscribe(()=>{t()})}(this,()=>{queueMicrotask(()=>this.pendingTasks.remove(w))}),this.navigationTransitions.handleNavigationRequest({source:r,restoredState:i,currentUrlTree:this.currentUrlTree,currentRawUrl:this.currentUrlTree,rawUrl:n,extras:o,resolve:u,reject:f,promise:y,currentSnapshot:this.routerState.snapshot,currentRouterState:this.routerState}),y.catch(D=>Promise.reject(D))}static \u0275fac=function(r){return new(r||e)};static \u0275prov=pe({token:e,factory:e.\u0275fac,providedIn:"root"})}return e})();const Ex=new ee("");function iL(e){return e.routerState.root}function oL(){const e=Y(Gn);return t=>{const n=e.get(Yo);if(t!==n.components[0])return;const r=e.get(Qa),i=e.get(sL);1===e.get(Dx)&&r.initialNavigation(),e.get(aL,null,{optional:!0})?.setUpPreloading(),e.get(Ex,null,{optional:!0})?.init(),r.resetRootComponentType(n.componentTypes[0]),i.closed||(i.next(),i.complete(),i.unsubscribe())}}const sL=new ee("",{factory:()=>new Kn}),Dx=new ee("",{providedIn:"root",factory:()=>1}),aL=new ee(""),xK={providers:[function D4(e){const t=e?.ignoreChangesOutsideZone,n=e?.scheduleInRootZone,r=PC({ngZoneFactory:()=>{const i=LC(e);return i.scheduleInRootZone=n,i.shouldCoalesceEventChangeDetection&&gr("NgZone_CoalesceEvent"),new lt(i)},ignoreChangesOutsideZone:t,scheduleInRootZone:n});return Yi([{provide:E4,useValue:!0},{provide:Dh,useValue:!1},r])}({eventCoalescing:!0}),function _K(e,...t){return Yi([{provide:_x,multi:!0,useValue:e},[],{provide:Dd,useFactory:iL,deps:[Qa]},{provide:MI,multi:!0,useFactory:oL},t.map(n=>n.\u0275providers)])}([]),function xW(...e){const t=[xR,OR,AR,{provide:_v,useExisting:AR},{provide:wv,useFactory:()=>Y(MR,{optional:!0})??Y(OR)},{provide:ap,useValue:DW,multi:!0},{provide:AE,useValue:!0},{provide:PR,useClass:EW}];for(const n of e)t.push(...n.\u0275providers);return Yi(t)}(),function z7(e={}){const t=[j7(e.instance),U7(e.modules??V7),{provide:TP,useValue:e.timeout}];return e.options&&t.push(function H7(e){return Yi([{provide:SP,useValue:e}])}(e.options)),Yi(t)}({instance:()=>Promise.resolve().then(Se.t.bind(Se,264,23)),options:{title:{style:{color:"tomato"}},legend:{enabled:!1}},modules:()=>[Promise.all([Se.e(444),Se.e(871)]).then(Se.bind(Se,871)),Promise.all([Se.e(444),Se.e(581)]).then(Se.bind(Se,581)),Promise.all([Se.e(444),Se.e(940)]).then(Se.bind(Se,940))]})]};(0,oe.A)(function*(){const e=yield function y8(e){return xz(function OO(e){return{appProviders:[...E8,...e?.providers??[]],platformProviders:w8}}(e))}(xK),t=Gl(EY,{injector:e.injector});customElements.define("snider-mining",t);const n=Gl(_N,{injector:e.injector});customElements.define("snider-mining-setup-wizard",n);const r=Gl(i2,{injector:e.injector});customElements.define("snider-mining-dashboard",r);const i=Gl(IP,{injector:e.injector});customElements.define("snider-mining-chart",i);const o=Gl(LY,{injector:e.injector});customElements.define("snider-mining-admin",o);const a=Gl(zY,{injector:e.injector});customElements.define("snider-mining-profile-list",a);const u=Gl(ZY,{injector:e.injector});customElements.define("snider-mining-profile-create",u),console.log("All Snider Mining custom elements registered.")})()})()})(); \ No newline at end of file diff --git a/mining/config_manager.go b/mining/config_manager.go new file mode 100644 index 0000000..0307e12 --- /dev/null +++ b/mining/config_manager.go @@ -0,0 +1,158 @@ +package mining + +import ( + "encoding/json" + "fmt" + "os" + "path/filepath" + "sync" + + "github.com/adrg/xdg" +) + +// configMu protects concurrent access to config file operations +var configMu sync.RWMutex + +// MinerAutostartConfig represents the configuration for a single miner's autostart settings. +type MinerAutostartConfig struct { + MinerType string `json:"minerType"` + Autostart bool `json:"autostart"` + Config *Config `json:"config,omitempty"` // Store the last used config +} + +// DatabaseConfig holds configuration for SQLite database persistence. +type DatabaseConfig struct { + // Enabled determines if database persistence is active (default: true) + Enabled bool `json:"enabled"` + // RetentionDays is how long to keep historical data (default: 30) + RetentionDays int `json:"retentionDays,omitempty"` +} + +// defaultDatabaseConfig returns the default database configuration. +func defaultDatabaseConfig() DatabaseConfig { + return DatabaseConfig{ + Enabled: true, + RetentionDays: 30, + } +} + +// MinersConfig represents the overall configuration for all miners, including autostart settings. +type MinersConfig struct { + Miners []MinerAutostartConfig `json:"miners"` + Database DatabaseConfig `json:"database"` +} + +// getMinersConfigPath returns the path to the miners configuration file. +func getMinersConfigPath() (string, error) { + return xdg.ConfigFile("lethean-desktop/miners/config.json") +} + +// LoadMinersConfig loads the miners configuration from the file system. +func LoadMinersConfig() (*MinersConfig, error) { + configMu.RLock() + defer configMu.RUnlock() + + configPath, err := getMinersConfigPath() + if err != nil { + return nil, fmt.Errorf("could not determine miners config path: %w", err) + } + + data, err := os.ReadFile(configPath) + if err != nil { + if os.IsNotExist(err) { + // Return empty config with defaults if file doesn't exist + return &MinersConfig{ + Miners: []MinerAutostartConfig{}, + Database: defaultDatabaseConfig(), + }, nil + } + return nil, fmt.Errorf("failed to read miners config file: %w", err) + } + + var cfg MinersConfig + if err := json.Unmarshal(data, &cfg); err != nil { + return nil, fmt.Errorf("failed to unmarshal miners config: %w", err) + } + + // Apply default database config if not set (for backwards compatibility) + if cfg.Database.RetentionDays == 0 { + cfg.Database = defaultDatabaseConfig() + } + + return &cfg, nil +} + +// SaveMinersConfig saves the miners configuration to the file system. +// Uses atomic write pattern: write to temp file, then rename. +func SaveMinersConfig(cfg *MinersConfig) error { + configMu.Lock() + defer configMu.Unlock() + + configPath, err := getMinersConfigPath() + if err != nil { + return fmt.Errorf("could not determine miners config path: %w", err) + } + + dir := filepath.Dir(configPath) + if err := os.MkdirAll(dir, 0755); err != nil { + return fmt.Errorf("failed to create config directory: %w", err) + } + + data, err := json.MarshalIndent(cfg, "", " ") + if err != nil { + return fmt.Errorf("failed to marshal miners config: %w", err) + } + + return AtomicWriteFile(configPath, data, 0600) +} + +// UpdateMinersConfig atomically loads, modifies, and saves the miners config. +// This prevents race conditions in read-modify-write operations. +func UpdateMinersConfig(fn func(*MinersConfig) error) error { + configMu.Lock() + defer configMu.Unlock() + + configPath, err := getMinersConfigPath() + if err != nil { + return fmt.Errorf("could not determine miners config path: %w", err) + } + + // Load current config + var cfg MinersConfig + data, err := os.ReadFile(configPath) + if err != nil { + if os.IsNotExist(err) { + cfg = MinersConfig{ + Miners: []MinerAutostartConfig{}, + Database: defaultDatabaseConfig(), + } + } else { + return fmt.Errorf("failed to read miners config file: %w", err) + } + } else { + if err := json.Unmarshal(data, &cfg); err != nil { + return fmt.Errorf("failed to unmarshal miners config: %w", err) + } + if cfg.Database.RetentionDays == 0 { + cfg.Database = defaultDatabaseConfig() + } + } + + // Apply the modification + if err := fn(&cfg); err != nil { + return err + } + + // Save atomically + dir := filepath.Dir(configPath) + if err := os.MkdirAll(dir, 0755); err != nil { + return fmt.Errorf("failed to create config directory: %w", err) + } + + newData, err := json.MarshalIndent(cfg, "", " ") + if err != nil { + return fmt.Errorf("failed to marshal miners config: %w", err) + } + + return AtomicWriteFile(configPath, newData, 0600) +} diff --git a/mining/container.go b/mining/container.go new file mode 100644 index 0000000..899286b --- /dev/null +++ b/mining/container.go @@ -0,0 +1,259 @@ +package mining + +import ( + "context" + "fmt" + "sync" + + "forge.lthn.ai/core/mining/database" + "forge.lthn.ai/core/mining/logging" +) + +// ContainerConfig holds configuration for the service container. +type ContainerConfig struct { + // Database configuration + Database database.Config + + // ListenAddr is the address to listen on (e.g., ":9090") + ListenAddr string + + // DisplayAddr is the address shown in Swagger docs + DisplayAddr string + + // SwaggerNamespace is the API path prefix + SwaggerNamespace string + + // SimulationMode enables simulation mode for testing + SimulationMode bool +} + +// DefaultContainerConfig returns sensible defaults for the container. +func DefaultContainerConfig() ContainerConfig { + return ContainerConfig{ + Database: database.Config{ + Enabled: true, + RetentionDays: 30, + }, + ListenAddr: ":9090", + DisplayAddr: "localhost:9090", + SwaggerNamespace: "/api/v1/mining", + SimulationMode: false, + } +} + +// Container manages the lifecycle of all services. +// It provides centralized initialization, dependency injection, and graceful shutdown. +type Container struct { + config ContainerConfig + mu sync.RWMutex + + // Core services + manager ManagerInterface + profileManager *ProfileManager + nodeService *NodeService + eventHub *EventHub + service *Service + + // Database store (interface for testing) + hashrateStore database.HashrateStore + + // Initialization state + initialized bool + transportStarted bool + shutdownCh chan struct{} +} + +// NewContainer creates a new service container with the given configuration. +func NewContainer(config ContainerConfig) *Container { + return &Container{ + config: config, + shutdownCh: make(chan struct{}), + } +} + +// Initialize sets up all services in the correct order. +// This should be called before Start(). +func (c *Container) Initialize(ctx context.Context) error { + c.mu.Lock() + defer c.mu.Unlock() + + if c.initialized { + return fmt.Errorf("container already initialized") + } + + // 1. Initialize database (optional) + if c.config.Database.Enabled { + if err := database.Initialize(c.config.Database); err != nil { + return fmt.Errorf("failed to initialize database: %w", err) + } + c.hashrateStore = database.DefaultStore() + logging.Info("database initialized", logging.Fields{"retention_days": c.config.Database.RetentionDays}) + } else { + c.hashrateStore = database.NopStore() + logging.Info("database disabled, using no-op store", nil) + } + + // 2. Initialize profile manager + var err error + c.profileManager, err = NewProfileManager() + if err != nil { + return fmt.Errorf("failed to initialize profile manager: %w", err) + } + + // 3. Initialize miner manager + if c.config.SimulationMode { + c.manager = NewManagerForSimulation() + } else { + c.manager = NewManager() + } + + // 4. Initialize node service (optional - P2P features) + c.nodeService, err = NewNodeService() + if err != nil { + logging.Warn("node service unavailable", logging.Fields{"error": err}) + // Continue without node service - P2P features will be unavailable + } + + // 5. Initialize event hub for WebSocket + c.eventHub = NewEventHub() + + // Wire up event hub to manager + if mgr, ok := c.manager.(*Manager); ok { + mgr.SetEventHub(c.eventHub) + } + + c.initialized = true + logging.Info("service container initialized", nil) + return nil +} + +// Start begins all background services. +func (c *Container) Start(ctx context.Context) error { + c.mu.RLock() + defer c.mu.RUnlock() + + if !c.initialized { + return fmt.Errorf("container not initialized") + } + + // Start event hub + go c.eventHub.Run() + + // Start node transport if available + if c.nodeService != nil { + if err := c.nodeService.StartTransport(); err != nil { + logging.Warn("failed to start node transport", logging.Fields{"error": err}) + } else { + c.transportStarted = true + } + } + + logging.Info("service container started", nil) + return nil +} + +// Shutdown gracefully stops all services in reverse order. +func (c *Container) Shutdown(ctx context.Context) error { + c.mu.Lock() + defer c.mu.Unlock() + + if !c.initialized { + return nil + } + + logging.Info("shutting down service container", nil) + + var errs []error + + // 1. Stop service (HTTP server) + if c.service != nil { + // Service shutdown is handled externally + } + + // 2. Stop node transport (only if it was started) + if c.nodeService != nil && c.transportStarted { + if err := c.nodeService.StopTransport(); err != nil { + errs = append(errs, fmt.Errorf("node transport: %w", err)) + } + c.transportStarted = false + } + + // 3. Stop event hub + if c.eventHub != nil { + c.eventHub.Stop() + } + + // 4. Stop miner manager + if mgr, ok := c.manager.(*Manager); ok { + mgr.Stop() + } + + // 5. Close database + if err := database.Close(); err != nil { + errs = append(errs, fmt.Errorf("database: %w", err)) + } + + c.initialized = false + close(c.shutdownCh) + + if len(errs) > 0 { + return fmt.Errorf("shutdown errors: %v", errs) + } + + logging.Info("service container shutdown complete", nil) + return nil +} + +// Manager returns the miner manager. +func (c *Container) Manager() ManagerInterface { + c.mu.RLock() + defer c.mu.RUnlock() + return c.manager +} + +// ProfileManager returns the profile manager. +func (c *Container) ProfileManager() *ProfileManager { + c.mu.RLock() + defer c.mu.RUnlock() + return c.profileManager +} + +// NodeService returns the node service (may be nil if P2P is unavailable). +func (c *Container) NodeService() *NodeService { + c.mu.RLock() + defer c.mu.RUnlock() + return c.nodeService +} + +// EventHub returns the event hub for WebSocket connections. +func (c *Container) EventHub() *EventHub { + c.mu.RLock() + defer c.mu.RUnlock() + return c.eventHub +} + +// HashrateStore returns the hashrate store interface. +func (c *Container) HashrateStore() database.HashrateStore { + c.mu.RLock() + defer c.mu.RUnlock() + return c.hashrateStore +} + +// SetHashrateStore allows injecting a custom hashrate store (useful for testing). +func (c *Container) SetHashrateStore(store database.HashrateStore) { + c.mu.Lock() + defer c.mu.Unlock() + c.hashrateStore = store +} + +// ShutdownCh returns a channel that's closed when shutdown is complete. +func (c *Container) ShutdownCh() <-chan struct{} { + return c.shutdownCh +} + +// IsInitialized returns true if the container has been initialized. +func (c *Container) IsInitialized() bool { + c.mu.RLock() + defer c.mu.RUnlock() + return c.initialized +} diff --git a/mining/container_test.go b/mining/container_test.go new file mode 100644 index 0000000..e374346 --- /dev/null +++ b/mining/container_test.go @@ -0,0 +1,316 @@ +package mining + +import ( + "context" + "os" + "path/filepath" + "testing" + "time" + + "forge.lthn.ai/core/mining/database" +) + +func setupContainerTestEnv(t *testing.T) func() { + tmpDir := t.TempDir() + os.Setenv("XDG_CONFIG_HOME", filepath.Join(tmpDir, "config")) + os.Setenv("XDG_DATA_HOME", filepath.Join(tmpDir, "data")) + return func() { + os.Unsetenv("XDG_CONFIG_HOME") + os.Unsetenv("XDG_DATA_HOME") + } +} + +func TestNewContainer(t *testing.T) { + config := DefaultContainerConfig() + container := NewContainer(config) + + if container == nil { + t.Fatal("NewContainer returned nil") + } + + if container.IsInitialized() { + t.Error("Container should not be initialized before Initialize() is called") + } +} + +func TestDefaultContainerConfig(t *testing.T) { + config := DefaultContainerConfig() + + if !config.Database.Enabled { + t.Error("Database should be enabled by default") + } + + if config.Database.RetentionDays != 30 { + t.Errorf("Expected 30 retention days, got %d", config.Database.RetentionDays) + } + + if config.ListenAddr != ":9090" { + t.Errorf("Expected :9090, got %s", config.ListenAddr) + } + + if config.SimulationMode { + t.Error("SimulationMode should be false by default") + } +} + +func TestContainer_Initialize(t *testing.T) { + cleanup := setupContainerTestEnv(t) + defer cleanup() + + config := DefaultContainerConfig() + config.Database.Enabled = true + config.Database.Path = filepath.Join(t.TempDir(), "test.db") + config.SimulationMode = true // Use simulation mode for faster tests + + container := NewContainer(config) + ctx := context.Background() + + if err := container.Initialize(ctx); err != nil { + t.Fatalf("Initialize failed: %v", err) + } + + if !container.IsInitialized() { + t.Error("Container should be initialized after Initialize()") + } + + // Verify services are available + if container.Manager() == nil { + t.Error("Manager should not be nil after initialization") + } + + if container.ProfileManager() == nil { + t.Error("ProfileManager should not be nil after initialization") + } + + if container.EventHub() == nil { + t.Error("EventHub should not be nil after initialization") + } + + if container.HashrateStore() == nil { + t.Error("HashrateStore should not be nil after initialization") + } + + // Cleanup + if err := container.Shutdown(ctx); err != nil { + t.Errorf("Shutdown failed: %v", err) + } +} + +func TestContainer_InitializeTwice(t *testing.T) { + cleanup := setupContainerTestEnv(t) + defer cleanup() + + config := DefaultContainerConfig() + config.Database.Enabled = false + config.SimulationMode = true + + container := NewContainer(config) + ctx := context.Background() + + if err := container.Initialize(ctx); err != nil { + t.Fatalf("First Initialize failed: %v", err) + } + + // Second initialization should fail + if err := container.Initialize(ctx); err == nil { + t.Error("Second Initialize should fail") + } + + container.Shutdown(ctx) +} + +func TestContainer_DatabaseDisabled(t *testing.T) { + cleanup := setupContainerTestEnv(t) + defer cleanup() + + config := DefaultContainerConfig() + config.Database.Enabled = false + config.SimulationMode = true + + container := NewContainer(config) + ctx := context.Background() + + if err := container.Initialize(ctx); err != nil { + t.Fatalf("Initialize failed: %v", err) + } + + // Should use NopStore when database is disabled + store := container.HashrateStore() + if store == nil { + t.Fatal("HashrateStore should not be nil") + } + + // NopStore should accept inserts without error + point := database.HashratePoint{ + Timestamp: time.Now(), + Hashrate: 1000, + } + if err := store.InsertHashratePoint(nil, "test", "xmrig", point, database.ResolutionHigh); err != nil { + t.Errorf("NopStore insert should not fail: %v", err) + } + + container.Shutdown(ctx) +} + +func TestContainer_SetHashrateStore(t *testing.T) { + cleanup := setupContainerTestEnv(t) + defer cleanup() + + config := DefaultContainerConfig() + config.Database.Enabled = false + config.SimulationMode = true + + container := NewContainer(config) + ctx := context.Background() + + if err := container.Initialize(ctx); err != nil { + t.Fatalf("Initialize failed: %v", err) + } + + // Inject custom store + customStore := database.NopStore() + container.SetHashrateStore(customStore) + + if container.HashrateStore() != customStore { + t.Error("SetHashrateStore should update the store") + } + + container.Shutdown(ctx) +} + +func TestContainer_StartWithoutInitialize(t *testing.T) { + config := DefaultContainerConfig() + container := NewContainer(config) + ctx := context.Background() + + if err := container.Start(ctx); err == nil { + t.Error("Start should fail if Initialize was not called") + } +} + +func TestContainer_ShutdownWithoutInitialize(t *testing.T) { + config := DefaultContainerConfig() + container := NewContainer(config) + ctx := context.Background() + + // Shutdown on uninitialized container should not error + if err := container.Shutdown(ctx); err != nil { + t.Errorf("Shutdown on uninitialized container should not error: %v", err) + } +} + +func TestContainer_ShutdownChannel(t *testing.T) { + cleanup := setupContainerTestEnv(t) + defer cleanup() + + config := DefaultContainerConfig() + config.Database.Enabled = false + config.SimulationMode = true + + container := NewContainer(config) + ctx := context.Background() + + if err := container.Initialize(ctx); err != nil { + t.Fatalf("Initialize failed: %v", err) + } + + shutdownCh := container.ShutdownCh() + + // Channel should be open before shutdown + select { + case <-shutdownCh: + t.Error("ShutdownCh should not be closed before Shutdown()") + default: + // Expected + } + + if err := container.Shutdown(ctx); err != nil { + t.Errorf("Shutdown failed: %v", err) + } + + // Channel should be closed after shutdown + select { + case <-shutdownCh: + // Expected + case <-time.After(time.Second): + t.Error("ShutdownCh should be closed after Shutdown()") + } +} + +func TestContainer_InitializeWithCancelledContext(t *testing.T) { + cleanup := setupContainerTestEnv(t) + defer cleanup() + + config := DefaultContainerConfig() + config.Database.Enabled = false + config.SimulationMode = true + + container := NewContainer(config) + + // Use a pre-cancelled context + ctx, cancel := context.WithCancel(context.Background()) + cancel() + + // Initialize should still succeed (context is checked at operation start) + // But operations that check context should respect cancellation + if err := container.Initialize(ctx); err != nil { + // This is acceptable - initialization may fail with cancelled context + t.Logf("Initialize with cancelled context: %v (acceptable)", err) + } + + // Cleanup if initialized + if container.IsInitialized() { + container.Shutdown(context.Background()) + } +} + +func TestContainer_ShutdownWithTimeout(t *testing.T) { + cleanup := setupContainerTestEnv(t) + defer cleanup() + + config := DefaultContainerConfig() + config.Database.Enabled = false + config.SimulationMode = true + + container := NewContainer(config) + ctx := context.Background() + + if err := container.Initialize(ctx); err != nil { + t.Fatalf("Initialize failed: %v", err) + } + + // Use a context with very short timeout + timeoutCtx, cancel := context.WithTimeout(context.Background(), 10*time.Millisecond) + defer cancel() + + // Shutdown should still complete (cleanup is fast without real miners) + if err := container.Shutdown(timeoutCtx); err != nil { + t.Logf("Shutdown with timeout: %v (may be acceptable)", err) + } +} + +func TestContainer_DoubleShutdown(t *testing.T) { + cleanup := setupContainerTestEnv(t) + defer cleanup() + + config := DefaultContainerConfig() + config.Database.Enabled = false + config.SimulationMode = true + + container := NewContainer(config) + ctx := context.Background() + + if err := container.Initialize(ctx); err != nil { + t.Fatalf("Initialize failed: %v", err) + } + + // First shutdown + if err := container.Shutdown(ctx); err != nil { + t.Errorf("First shutdown failed: %v", err) + } + + // Second shutdown should not panic or error + if err := container.Shutdown(ctx); err != nil { + t.Logf("Second shutdown returned: %v (expected no-op)", err) + } +} diff --git a/mining/dual_mining_test.go b/mining/dual_mining_test.go new file mode 100644 index 0000000..e1f1308 --- /dev/null +++ b/mining/dual_mining_test.go @@ -0,0 +1,127 @@ +package mining + +import ( + "context" + "testing" + "time" +) + +// TestDualMiningCPUAndGPU tests running CPU and GPU mining together +// This test requires XMRig installed and a GPU with OpenCL support +func TestDualMiningCPUAndGPU(t *testing.T) { + if testing.Short() { + t.Skip("Skipping dual mining test in short mode") + } + + miner := NewXMRigMiner() + details, err := miner.CheckInstallation() + if err != nil || !details.IsInstalled { + t.Skip("XMRig not installed, skipping dual mining test") + } + + manager := NewManager() + defer manager.Stop() + + // Dual mining config: + // - CPU: 25% threads on RandomX + // - GPU: OpenCL device 0 (discrete GPU, not iGPU) + config := &Config{ + Pool: "stratum+tcp://pool.supportxmr.com:3333", + Wallet: "44AFFq5kSiGBoZ4NMDwYtN18obc8AemS33DBLWs3H7otXft3XjrpDtQGv7SqSsaBYBb98uNbr2VBBEt7f2wfn3RVGQBEP3A", + Algo: "rx/0", + CPUMaxThreadsHint: 25, // 25% CPU + + // GPU config - explicit device selection required! + GPUEnabled: true, + OpenCL: true, // AMD GPU + Devices: "0", // Device 0 only - user must pick + } + + minerInstance, err := manager.StartMiner(context.Background(), "xmrig", config) + if err != nil { + t.Fatalf("Failed to start dual miner: %v", err) + } + t.Logf("Started dual miner: %s", minerInstance.GetName()) + + // Let it warm up + time.Sleep(20 * time.Second) + + // Get stats + stats, err := minerInstance.GetStats(context.Background()) + if err != nil { + t.Logf("Warning: couldn't get stats: %v", err) + } else { + t.Logf("Hashrate: %d H/s, Shares: %d, Algo: %s", + stats.Hashrate, stats.Shares, stats.Algorithm) + } + + // Check logs for GPU initialization + logs := minerInstance.GetLogs() + gpuFound := false + for _, line := range logs { + if contains(line, "OpenCL") || contains(line, "GPU") { + gpuFound = true + t.Logf("GPU log: %s", line) + } + } + + if !gpuFound { + t.Log("No GPU-related log lines found - GPU may not be mining") + } + + // Clean up + manager.StopMiner(context.Background(), minerInstance.GetName()) +} + +// TestGPUDeviceSelection tests that GPU mining requires explicit device selection +func TestGPUDeviceSelection(t *testing.T) { + tmpDir := t.TempDir() + + miner := &XMRigMiner{ + BaseMiner: BaseMiner{ + Name: "xmrig-device-test", + API: &API{ + Enabled: true, + ListenHost: "127.0.0.1", + ListenPort: 54321, + }, + }, + } + + origGetPath := getXMRigConfigPath + getXMRigConfigPath = func(name string) (string, error) { + return tmpDir + "/" + name + ".json", nil + } + defer func() { getXMRigConfigPath = origGetPath }() + + // Config WITHOUT device selection - GPU should be disabled + configNoDevice := &Config{ + Pool: "stratum+tcp://pool.supportxmr.com:3333", + Wallet: "test_wallet", + Algo: "rx/0", + GPUEnabled: true, + OpenCL: true, + // NO Devices specified! + } + + err := miner.createConfig(configNoDevice) + if err != nil { + t.Fatalf("Failed to create config: %v", err) + } + + // GPU should be disabled because no device was specified + t.Log("Config without explicit device - GPU should be disabled (safe default)") +} + +func contains(s, substr string) bool { + return len(s) >= len(substr) && (s == substr || len(s) > 0 && containsAt(s, substr, 0)) +} + +func containsAt(s, substr string, start int) bool { + for i := start; i <= len(s)-len(substr); i++ { + if s[i:i+len(substr)] == substr { + return true + } + } + return false +} diff --git a/mining/errors.go b/mining/errors.go new file mode 100644 index 0000000..96d5288 --- /dev/null +++ b/mining/errors.go @@ -0,0 +1,248 @@ +package mining + +import ( + "fmt" + "net/http" +) + +// Error codes for the mining package +const ( + ErrCodeMinerNotFound = "MINER_NOT_FOUND" + ErrCodeMinerExists = "MINER_EXISTS" + ErrCodeMinerNotRunning = "MINER_NOT_RUNNING" + ErrCodeInstallFailed = "INSTALL_FAILED" + ErrCodeStartFailed = "START_FAILED" + ErrCodeStopFailed = "STOP_FAILED" + ErrCodeInvalidConfig = "INVALID_CONFIG" + ErrCodeInvalidInput = "INVALID_INPUT" + ErrCodeUnsupportedMiner = "UNSUPPORTED_MINER" + ErrCodeNotSupported = "NOT_SUPPORTED" + ErrCodeConnectionFailed = "CONNECTION_FAILED" + ErrCodeServiceUnavailable = "SERVICE_UNAVAILABLE" + ErrCodeTimeout = "TIMEOUT" + ErrCodeDatabaseError = "DATABASE_ERROR" + ErrCodeProfileNotFound = "PROFILE_NOT_FOUND" + ErrCodeProfileExists = "PROFILE_EXISTS" + ErrCodeInternalError = "INTERNAL_ERROR" + ErrCodeInternal = "INTERNAL_ERROR" // Alias for consistency +) + +// MiningError is a structured error type for the mining package +type MiningError struct { + Code string // Machine-readable error code + Message string // Human-readable message + Details string // Technical details (for debugging) + Suggestion string // What to do next + Retryable bool // Can the client retry? + HTTPStatus int // HTTP status code to return + Cause error // Underlying error +} + +// Error implements the error interface +func (e *MiningError) Error() string { + if e.Cause != nil { + return fmt.Sprintf("%s: %s (%v)", e.Code, e.Message, e.Cause) + } + return fmt.Sprintf("%s: %s", e.Code, e.Message) +} + +// Unwrap returns the underlying error +func (e *MiningError) Unwrap() error { + return e.Cause +} + +// WithCause adds an underlying error +func (e *MiningError) WithCause(err error) *MiningError { + e.Cause = err + return e +} + +// WithDetails adds technical details +func (e *MiningError) WithDetails(details string) *MiningError { + e.Details = details + return e +} + +// WithSuggestion adds a suggestion for the user +func (e *MiningError) WithSuggestion(suggestion string) *MiningError { + e.Suggestion = suggestion + return e +} + +// IsRetryable returns whether the error is retryable +func (e *MiningError) IsRetryable() bool { + return e.Retryable +} + +// StatusCode returns the HTTP status code for this error +func (e *MiningError) StatusCode() int { + if e.HTTPStatus == 0 { + return http.StatusInternalServerError + } + return e.HTTPStatus +} + +// NewMiningError creates a new MiningError +func NewMiningError(code, message string) *MiningError { + return &MiningError{ + Code: code, + Message: message, + HTTPStatus: http.StatusInternalServerError, + } +} + +// Predefined error constructors for common errors + +// ErrMinerNotFound creates a miner not found error +func ErrMinerNotFound(name string) *MiningError { + return &MiningError{ + Code: ErrCodeMinerNotFound, + Message: fmt.Sprintf("miner '%s' not found", name), + Suggestion: "Check that the miner name is correct and that it is running", + Retryable: false, + HTTPStatus: http.StatusNotFound, + } +} + +// ErrMinerExists creates a miner already exists error +func ErrMinerExists(name string) *MiningError { + return &MiningError{ + Code: ErrCodeMinerExists, + Message: fmt.Sprintf("miner '%s' is already running", name), + Suggestion: "Stop the existing miner first or use a different configuration", + Retryable: false, + HTTPStatus: http.StatusConflict, + } +} + +// ErrMinerNotRunning creates a miner not running error +func ErrMinerNotRunning(name string) *MiningError { + return &MiningError{ + Code: ErrCodeMinerNotRunning, + Message: fmt.Sprintf("miner '%s' is not running", name), + Suggestion: "Start the miner first before performing this operation", + Retryable: false, + HTTPStatus: http.StatusBadRequest, + } +} + +// ErrInstallFailed creates an installation failed error +func ErrInstallFailed(minerType string) *MiningError { + return &MiningError{ + Code: ErrCodeInstallFailed, + Message: fmt.Sprintf("failed to install %s", minerType), + Suggestion: "Check your internet connection and try again", + Retryable: true, + HTTPStatus: http.StatusInternalServerError, + } +} + +// ErrStartFailed creates a start failed error +func ErrStartFailed(name string) *MiningError { + return &MiningError{ + Code: ErrCodeStartFailed, + Message: fmt.Sprintf("failed to start miner '%s'", name), + Suggestion: "Check the miner configuration and logs for details", + Retryable: true, + HTTPStatus: http.StatusInternalServerError, + } +} + +// ErrStopFailed creates a stop failed error +func ErrStopFailed(name string) *MiningError { + return &MiningError{ + Code: ErrCodeStopFailed, + Message: fmt.Sprintf("failed to stop miner '%s'", name), + Suggestion: "The miner process may need to be terminated manually", + Retryable: true, + HTTPStatus: http.StatusInternalServerError, + } +} + +// ErrInvalidConfig creates an invalid configuration error +func ErrInvalidConfig(reason string) *MiningError { + return &MiningError{ + Code: ErrCodeInvalidConfig, + Message: fmt.Sprintf("invalid configuration: %s", reason), + Suggestion: "Review the configuration and ensure all required fields are provided", + Retryable: false, + HTTPStatus: http.StatusBadRequest, + } +} + +// ErrUnsupportedMiner creates an unsupported miner type error +func ErrUnsupportedMiner(minerType string) *MiningError { + return &MiningError{ + Code: ErrCodeUnsupportedMiner, + Message: fmt.Sprintf("unsupported miner type: %s", minerType), + Suggestion: "Use one of the supported miner types: xmrig, tt-miner", + Retryable: false, + HTTPStatus: http.StatusBadRequest, + } +} + +// ErrConnectionFailed creates a connection failed error +func ErrConnectionFailed(target string) *MiningError { + return &MiningError{ + Code: ErrCodeConnectionFailed, + Message: fmt.Sprintf("failed to connect to %s", target), + Suggestion: "Check network connectivity and try again", + Retryable: true, + HTTPStatus: http.StatusServiceUnavailable, + } +} + +// ErrTimeout creates a timeout error +func ErrTimeout(operation string) *MiningError { + return &MiningError{ + Code: ErrCodeTimeout, + Message: fmt.Sprintf("operation timed out: %s", operation), + Suggestion: "The operation is taking longer than expected, try again later", + Retryable: true, + HTTPStatus: http.StatusGatewayTimeout, + } +} + +// ErrDatabaseError creates a database error +func ErrDatabaseError(operation string) *MiningError { + return &MiningError{ + Code: ErrCodeDatabaseError, + Message: fmt.Sprintf("database error during %s", operation), + Suggestion: "This may be a temporary issue, try again", + Retryable: true, + HTTPStatus: http.StatusInternalServerError, + } +} + +// ErrProfileNotFound creates a profile not found error +func ErrProfileNotFound(id string) *MiningError { + return &MiningError{ + Code: ErrCodeProfileNotFound, + Message: fmt.Sprintf("profile '%s' not found", id), + Suggestion: "Check that the profile ID is correct", + Retryable: false, + HTTPStatus: http.StatusNotFound, + } +} + +// ErrProfileExists creates a profile already exists error +func ErrProfileExists(name string) *MiningError { + return &MiningError{ + Code: ErrCodeProfileExists, + Message: fmt.Sprintf("profile '%s' already exists", name), + Suggestion: "Use a different name or update the existing profile", + Retryable: false, + HTTPStatus: http.StatusConflict, + } +} + +// ErrInternal creates a generic internal error +func ErrInternal(message string) *MiningError { + return &MiningError{ + Code: ErrCodeInternalError, + Message: message, + Suggestion: "Please report this issue if it persists", + Retryable: true, + HTTPStatus: http.StatusInternalServerError, + } +} diff --git a/mining/errors_test.go b/mining/errors_test.go new file mode 100644 index 0000000..06f6245 --- /dev/null +++ b/mining/errors_test.go @@ -0,0 +1,151 @@ +package mining + +import ( + "errors" + "net/http" + "testing" +) + +func TestMiningError_Error(t *testing.T) { + err := NewMiningError(ErrCodeMinerNotFound, "miner not found") + expected := "MINER_NOT_FOUND: miner not found" + if err.Error() != expected { + t.Errorf("Expected %q, got %q", expected, err.Error()) + } +} + +func TestMiningError_ErrorWithCause(t *testing.T) { + cause := errors.New("underlying error") + err := NewMiningError(ErrCodeStartFailed, "failed to start").WithCause(cause) + + // Should include cause in error message + if err.Cause != cause { + t.Error("Cause was not set") + } + + // Should be unwrappable + if errors.Unwrap(err) != cause { + t.Error("Unwrap did not return cause") + } +} + +func TestMiningError_WithDetails(t *testing.T) { + err := NewMiningError(ErrCodeInvalidConfig, "invalid config"). + WithDetails("port must be between 1024 and 65535") + + if err.Details != "port must be between 1024 and 65535" { + t.Errorf("Details not set correctly: %s", err.Details) + } +} + +func TestMiningError_WithSuggestion(t *testing.T) { + err := NewMiningError(ErrCodeConnectionFailed, "connection failed"). + WithSuggestion("check your network") + + if err.Suggestion != "check your network" { + t.Errorf("Suggestion not set correctly: %s", err.Suggestion) + } +} + +func TestMiningError_StatusCode(t *testing.T) { + tests := []struct { + name string + err *MiningError + expected int + }{ + {"default", NewMiningError("TEST", "test"), http.StatusInternalServerError}, + {"not found", ErrMinerNotFound("test"), http.StatusNotFound}, + {"conflict", ErrMinerExists("test"), http.StatusConflict}, + {"bad request", ErrInvalidConfig("bad"), http.StatusBadRequest}, + {"service unavailable", ErrConnectionFailed("pool"), http.StatusServiceUnavailable}, + {"timeout", ErrTimeout("operation"), http.StatusGatewayTimeout}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if tt.err.StatusCode() != tt.expected { + t.Errorf("Expected status %d, got %d", tt.expected, tt.err.StatusCode()) + } + }) + } +} + +func TestMiningError_IsRetryable(t *testing.T) { + tests := []struct { + name string + err *MiningError + retryable bool + }{ + {"not found", ErrMinerNotFound("test"), false}, + {"exists", ErrMinerExists("test"), false}, + {"invalid config", ErrInvalidConfig("bad"), false}, + {"install failed", ErrInstallFailed("xmrig"), true}, + {"start failed", ErrStartFailed("test"), true}, + {"connection failed", ErrConnectionFailed("pool"), true}, + {"timeout", ErrTimeout("operation"), true}, + {"database error", ErrDatabaseError("query"), true}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if tt.err.IsRetryable() != tt.retryable { + t.Errorf("Expected retryable=%v, got %v", tt.retryable, tt.err.IsRetryable()) + } + }) + } +} + +func TestPredefinedErrors(t *testing.T) { + tests := []struct { + name string + err *MiningError + code string + }{ + {"ErrMinerNotFound", ErrMinerNotFound("test"), ErrCodeMinerNotFound}, + {"ErrMinerExists", ErrMinerExists("test"), ErrCodeMinerExists}, + {"ErrMinerNotRunning", ErrMinerNotRunning("test"), ErrCodeMinerNotRunning}, + {"ErrInstallFailed", ErrInstallFailed("xmrig"), ErrCodeInstallFailed}, + {"ErrStartFailed", ErrStartFailed("test"), ErrCodeStartFailed}, + {"ErrStopFailed", ErrStopFailed("test"), ErrCodeStopFailed}, + {"ErrInvalidConfig", ErrInvalidConfig("bad port"), ErrCodeInvalidConfig}, + {"ErrUnsupportedMiner", ErrUnsupportedMiner("unknown"), ErrCodeUnsupportedMiner}, + {"ErrConnectionFailed", ErrConnectionFailed("pool:3333"), ErrCodeConnectionFailed}, + {"ErrTimeout", ErrTimeout("GetStats"), ErrCodeTimeout}, + {"ErrDatabaseError", ErrDatabaseError("insert"), ErrCodeDatabaseError}, + {"ErrProfileNotFound", ErrProfileNotFound("abc123"), ErrCodeProfileNotFound}, + {"ErrProfileExists", ErrProfileExists("My Profile"), ErrCodeProfileExists}, + {"ErrInternal", ErrInternal("unexpected error"), ErrCodeInternalError}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if tt.err.Code != tt.code { + t.Errorf("Expected code %s, got %s", tt.code, tt.err.Code) + } + if tt.err.Message == "" { + t.Error("Message should not be empty") + } + }) + } +} + +func TestMiningError_Chaining(t *testing.T) { + cause := errors.New("network timeout") + err := ErrConnectionFailed("pool:3333"). + WithCause(cause). + WithDetails("timeout after 30s"). + WithSuggestion("check firewall settings") + + if err.Code != ErrCodeConnectionFailed { + t.Errorf("Code changed: %s", err.Code) + } + if err.Cause != cause { + t.Error("Cause not set") + } + if err.Details != "timeout after 30s" { + t.Errorf("Details not set: %s", err.Details) + } + if err.Suggestion != "check firewall settings" { + t.Errorf("Suggestion not set: %s", err.Suggestion) + } +} diff --git a/mining/events.go b/mining/events.go new file mode 100644 index 0000000..0520d8a --- /dev/null +++ b/mining/events.go @@ -0,0 +1,423 @@ +package mining + +import ( + "encoding/json" + "sync" + "time" + + "forge.lthn.ai/core/mining/logging" + "github.com/gorilla/websocket" +) + +// EventType represents the type of mining event +type EventType string + +const ( + // Miner lifecycle events + EventMinerStarting EventType = "miner.starting" + EventMinerStarted EventType = "miner.started" + EventMinerStopping EventType = "miner.stopping" + EventMinerStopped EventType = "miner.stopped" + EventMinerStats EventType = "miner.stats" + EventMinerError EventType = "miner.error" + EventMinerConnected EventType = "miner.connected" + + // System events + EventPong EventType = "pong" + EventStateSync EventType = "state.sync" // Initial state on connect/reconnect +) + +// Event represents a mining event that can be broadcast to clients +type Event struct { + Type EventType `json:"type"` + Timestamp time.Time `json:"timestamp"` + Data interface{} `json:"data,omitempty"` +} + +// MinerStatsData contains stats data for a miner event +type MinerStatsData struct { + Name string `json:"name"` + Hashrate int `json:"hashrate"` + Shares int `json:"shares"` + Rejected int `json:"rejected"` + Uptime int `json:"uptime"` + Algorithm string `json:"algorithm,omitempty"` + DiffCurrent int `json:"diffCurrent,omitempty"` +} + +// MinerEventData contains basic miner event data +type MinerEventData struct { + Name string `json:"name"` + ProfileID string `json:"profileId,omitempty"` + Reason string `json:"reason,omitempty"` + Error string `json:"error,omitempty"` + Pool string `json:"pool,omitempty"` +} + +// wsClient represents a WebSocket client connection +type wsClient struct { + conn *websocket.Conn + send chan []byte + hub *EventHub + miners map[string]bool // subscribed miners, "*" for all + minersMu sync.RWMutex // protects miners map from concurrent access + closeOnce sync.Once +} + +// safeClose closes the send channel exactly once to prevent panic on double close +func (c *wsClient) safeClose() { + c.closeOnce.Do(func() { + close(c.send) + }) +} + +// StateProvider is a function that returns the current state for sync +type StateProvider func() interface{} + +// EventHub manages WebSocket connections and event broadcasting +type EventHub struct { + // Registered clients + clients map[*wsClient]bool + + // Inbound events to broadcast + broadcast chan Event + + // Register requests from clients + register chan *wsClient + + // Unregister requests from clients + unregister chan *wsClient + + // Mutex for thread-safe access + mu sync.RWMutex + + // Stop signal + stop chan struct{} + + // Ensure Stop() is called only once + stopOnce sync.Once + + // Connection limits + maxConnections int + + // State provider for sync on connect + stateProvider StateProvider +} + +// DefaultMaxConnections is the default maximum WebSocket connections +const DefaultMaxConnections = 100 + +// NewEventHub creates a new EventHub with default settings +func NewEventHub() *EventHub { + return NewEventHubWithOptions(DefaultMaxConnections) +} + +// NewEventHubWithOptions creates a new EventHub with custom settings +func NewEventHubWithOptions(maxConnections int) *EventHub { + if maxConnections <= 0 { + maxConnections = DefaultMaxConnections + } + return &EventHub{ + clients: make(map[*wsClient]bool), + broadcast: make(chan Event, 256), + register: make(chan *wsClient, 16), + unregister: make(chan *wsClient, 16), // Buffered to prevent goroutine leaks on shutdown + stop: make(chan struct{}), + maxConnections: maxConnections, + } +} + +// Run starts the EventHub's main loop +func (h *EventHub) Run() { + for { + select { + case <-h.stop: + // Close all client connections + h.mu.Lock() + for client := range h.clients { + client.safeClose() + delete(h.clients, client) + } + h.mu.Unlock() + return + + case client := <-h.register: + h.mu.Lock() + h.clients[client] = true + stateProvider := h.stateProvider + h.mu.Unlock() + logging.Debug("client connected", logging.Fields{"total": len(h.clients)}) + + // Send initial state sync if provider is set + if stateProvider != nil { + go func(c *wsClient) { + defer func() { + if r := recover(); r != nil { + logging.Error("panic in state sync goroutine", logging.Fields{"panic": r}) + } + }() + state := stateProvider() + if state != nil { + event := Event{ + Type: EventStateSync, + Timestamp: time.Now(), + Data: state, + } + data, err := MarshalJSON(event) + if err != nil { + logging.Error("failed to marshal state sync", logging.Fields{"error": err}) + return + } + select { + case c.send <- data: + default: + // Client buffer full + } + } + }(client) + } + + case client := <-h.unregister: + h.mu.Lock() + if _, ok := h.clients[client]; ok { + delete(h.clients, client) + client.safeClose() + // Decrement WebSocket connection metrics + RecordWSConnection(false) + } + h.mu.Unlock() + logging.Debug("client disconnected", logging.Fields{"total": len(h.clients)}) + + case event := <-h.broadcast: + data, err := MarshalJSON(event) + if err != nil { + logging.Error("failed to marshal event", logging.Fields{"error": err}) + continue + } + + h.mu.RLock() + for client := range h.clients { + // Check if client is subscribed to this miner + if h.shouldSendToClient(client, event) { + select { + case client.send <- data: + default: + // Client buffer full, close connection + go func(c *wsClient) { + h.unregister <- c + }(client) + } + } + } + h.mu.RUnlock() + } + } +} + +// shouldSendToClient checks if an event should be sent to a client +func (h *EventHub) shouldSendToClient(client *wsClient, event Event) bool { + // Always send pong and system events + if event.Type == EventPong { + return true + } + + // Check miner subscription for miner events (protected by mutex) + client.minersMu.RLock() + defer client.minersMu.RUnlock() + + if client.miners == nil || len(client.miners) == 0 { + // No subscription filter, send all + return true + } + + // Check for wildcard subscription + if client.miners["*"] { + return true + } + + // Extract miner name from event data + minerName := "" + switch data := event.Data.(type) { + case MinerStatsData: + minerName = data.Name + case MinerEventData: + minerName = data.Name + case map[string]interface{}: + if name, ok := data["name"].(string); ok { + minerName = name + } + } + + if minerName == "" { + // Non-miner event, send to all + return true + } + + return client.miners[minerName] +} + +// Stop stops the EventHub (safe to call multiple times) +func (h *EventHub) Stop() { + h.stopOnce.Do(func() { + close(h.stop) + }) +} + +// SetStateProvider sets the function that provides current state for new clients +func (h *EventHub) SetStateProvider(provider StateProvider) { + h.mu.Lock() + defer h.mu.Unlock() + h.stateProvider = provider +} + +// Broadcast sends an event to all subscribed clients +func (h *EventHub) Broadcast(event Event) { + if event.Timestamp.IsZero() { + event.Timestamp = time.Now() + } + select { + case h.broadcast <- event: + default: + logging.Warn("broadcast channel full, dropping event", logging.Fields{"type": event.Type}) + } +} + +// ClientCount returns the number of connected clients +func (h *EventHub) ClientCount() int { + h.mu.RLock() + defer h.mu.RUnlock() + return len(h.clients) +} + +// NewEvent creates a new event with the current timestamp +func NewEvent(eventType EventType, data interface{}) Event { + return Event{ + Type: eventType, + Timestamp: time.Now(), + Data: data, + } +} + +// writePump pumps messages from the hub to the websocket connection +func (c *wsClient) writePump() { + ticker := time.NewTicker(30 * time.Second) + defer func() { + ticker.Stop() + c.conn.Close() + }() + + for { + select { + case message, ok := <-c.send: + c.conn.SetWriteDeadline(time.Now().Add(10 * time.Second)) + if !ok { + // Hub closed the channel + c.conn.WriteMessage(websocket.CloseMessage, []byte{}) + return + } + + w, err := c.conn.NextWriter(websocket.TextMessage) + if err != nil { + return + } + if _, err := w.Write(message); err != nil { + logging.Debug("WebSocket write error", logging.Fields{"error": err}) + return + } + + if err := w.Close(); err != nil { + return + } + + case <-ticker.C: + c.conn.SetWriteDeadline(time.Now().Add(10 * time.Second)) + if err := c.conn.WriteMessage(websocket.PingMessage, nil); err != nil { + return + } + } + } +} + +// readPump pumps messages from the websocket connection to the hub +func (c *wsClient) readPump() { + defer func() { + c.hub.unregister <- c + c.conn.Close() + }() + + c.conn.SetReadLimit(512) + c.conn.SetReadDeadline(time.Now().Add(60 * time.Second)) + c.conn.SetPongHandler(func(string) error { + c.conn.SetReadDeadline(time.Now().Add(60 * time.Second)) + return nil + }) + + for { + _, message, err := c.conn.ReadMessage() + if err != nil { + if websocket.IsUnexpectedCloseError(err, websocket.CloseGoingAway, websocket.CloseAbnormalClosure) { + logging.Debug("WebSocket error", logging.Fields{"error": err}) + } + break + } + + // Parse client message + var msg struct { + Type string `json:"type"` + Miners []string `json:"miners,omitempty"` + } + if err := json.Unmarshal(message, &msg); err != nil { + continue + } + + switch msg.Type { + case "subscribe": + // Update miner subscription (protected by mutex) + c.minersMu.Lock() + c.miners = make(map[string]bool) + for _, m := range msg.Miners { + c.miners[m] = true + } + c.minersMu.Unlock() + logging.Debug("client subscribed to miners", logging.Fields{"miners": msg.Miners}) + + case "ping": + // Respond with pong + c.hub.Broadcast(Event{ + Type: EventPong, + Timestamp: time.Now(), + }) + } + } +} + +// ServeWs handles websocket requests from clients. +// Returns false if the connection was rejected due to limits. +func (h *EventHub) ServeWs(conn *websocket.Conn) bool { + // Check connection limit + h.mu.RLock() + currentCount := len(h.clients) + h.mu.RUnlock() + + if currentCount >= h.maxConnections { + logging.Warn("connection rejected: limit reached", logging.Fields{"current": currentCount, "max": h.maxConnections}) + conn.WriteMessage(websocket.CloseMessage, + websocket.FormatCloseMessage(websocket.CloseTryAgainLater, "connection limit reached")) + conn.Close() + return false + } + + client := &wsClient{ + conn: conn, + send: make(chan []byte, 256), + hub: h, + miners: map[string]bool{"*": true}, // Subscribe to all by default + } + + h.register <- client + + // Start read/write pumps + go client.writePump() + go client.readPump() + return true +} diff --git a/mining/events_test.go b/mining/events_test.go new file mode 100644 index 0000000..1ed9046 --- /dev/null +++ b/mining/events_test.go @@ -0,0 +1,201 @@ +package mining + +import ( + "encoding/json" + "sync" + "testing" + "time" + + "github.com/gorilla/websocket" +) + +func TestNewEventHub(t *testing.T) { + hub := NewEventHub() + if hub == nil { + t.Fatal("NewEventHub returned nil") + } + + if hub.clients == nil { + t.Error("clients map should be initialized") + } + + if hub.maxConnections != DefaultMaxConnections { + t.Errorf("Expected maxConnections %d, got %d", DefaultMaxConnections, hub.maxConnections) + } +} + +func TestNewEventHubWithOptions(t *testing.T) { + hub := NewEventHubWithOptions(50) + if hub.maxConnections != 50 { + t.Errorf("Expected maxConnections 50, got %d", hub.maxConnections) + } + + // Test with invalid value + hub2 := NewEventHubWithOptions(0) + if hub2.maxConnections != DefaultMaxConnections { + t.Errorf("Expected default maxConnections for 0, got %d", hub2.maxConnections) + } + + hub3 := NewEventHubWithOptions(-1) + if hub3.maxConnections != DefaultMaxConnections { + t.Errorf("Expected default maxConnections for -1, got %d", hub3.maxConnections) + } +} + +func TestEventHubBroadcast(t *testing.T) { + hub := NewEventHub() + go hub.Run() + defer hub.Stop() + + // Create an event + event := Event{ + Type: EventMinerStarted, + Timestamp: time.Now(), + Data: MinerEventData{Name: "test-miner"}, + } + + // Broadcast should not block even with no clients + done := make(chan struct{}) + go func() { + hub.Broadcast(event) + close(done) + }() + + select { + case <-done: + // Success + case <-time.After(time.Second): + t.Error("Broadcast blocked unexpectedly") + } +} + +func TestEventHubClientCount(t *testing.T) { + hub := NewEventHub() + go hub.Run() + defer hub.Stop() + + // Initial count should be 0 + if count := hub.ClientCount(); count != 0 { + t.Errorf("Expected 0 clients, got %d", count) + } +} + +func TestEventHubStop(t *testing.T) { + hub := NewEventHub() + go hub.Run() + + // Stop should not panic + defer func() { + if r := recover(); r != nil { + t.Errorf("Stop panicked: %v", r) + } + }() + + hub.Stop() + + // Give time for cleanup + time.Sleep(50 * time.Millisecond) +} + +func TestNewEvent(t *testing.T) { + data := MinerEventData{Name: "test-miner"} + event := NewEvent(EventMinerStarted, data) + + if event.Type != EventMinerStarted { + t.Errorf("Expected type %s, got %s", EventMinerStarted, event.Type) + } + + if event.Timestamp.IsZero() { + t.Error("Timestamp should not be zero") + } + + eventData, ok := event.Data.(MinerEventData) + if !ok { + t.Error("Data should be MinerEventData") + } + if eventData.Name != "test-miner" { + t.Errorf("Expected miner name 'test-miner', got '%s'", eventData.Name) + } +} + +func TestEventJSON(t *testing.T) { + event := Event{ + Type: EventMinerStats, + Timestamp: time.Now(), + Data: MinerStatsData{ + Name: "test-miner", + Hashrate: 1000, + Shares: 10, + Rejected: 1, + Uptime: 3600, + }, + } + + data, err := json.Marshal(event) + if err != nil { + t.Fatalf("Failed to marshal event: %v", err) + } + + var decoded Event + if err := json.Unmarshal(data, &decoded); err != nil { + t.Fatalf("Failed to unmarshal event: %v", err) + } + + if decoded.Type != EventMinerStats { + t.Errorf("Expected type %s, got %s", EventMinerStats, decoded.Type) + } +} + +func TestSetStateProvider(t *testing.T) { + hub := NewEventHub() + go hub.Run() + defer hub.Stop() + + called := false + var mu sync.Mutex + + provider := func() interface{} { + mu.Lock() + called = true + mu.Unlock() + return map[string]string{"status": "ok"} + } + + hub.SetStateProvider(provider) + + // The provider should be set but not called until a client connects + mu.Lock() + wasCalled := called + mu.Unlock() + + if wasCalled { + t.Error("Provider should not be called until client connects") + } +} + +// MockWebSocketConn provides a minimal mock for testing +type MockWebSocketConn struct { + websocket.Conn + written [][]byte + mu sync.Mutex +} + +func TestEventTypes(t *testing.T) { + types := []EventType{ + EventMinerStarting, + EventMinerStarted, + EventMinerStopping, + EventMinerStopped, + EventMinerStats, + EventMinerError, + EventMinerConnected, + EventPong, + EventStateSync, + } + + for _, et := range types { + if et == "" { + t.Error("Event type should not be empty") + } + } +} diff --git a/mining/file_utils.go b/mining/file_utils.go new file mode 100644 index 0000000..23e95a5 --- /dev/null +++ b/mining/file_utils.go @@ -0,0 +1,57 @@ +package mining + +import ( + "fmt" + "os" + "path/filepath" +) + +// AtomicWriteFile writes data to a file atomically by writing to a temp file +// first, syncing to disk, then renaming to the target path. This prevents +// corruption if the process is interrupted during write. +func AtomicWriteFile(path string, data []byte, perm os.FileMode) error { + dir := filepath.Dir(path) + + // Create temp file in the same directory for atomic rename + tmpFile, err := os.CreateTemp(dir, ".tmp-*") + if err != nil { + return fmt.Errorf("failed to create temp file: %w", err) + } + tmpPath := tmpFile.Name() + + // Clean up temp file on error + success := false + defer func() { + if !success { + os.Remove(tmpPath) + } + }() + + if _, err := tmpFile.Write(data); err != nil { + tmpFile.Close() + return fmt.Errorf("failed to write temp file: %w", err) + } + + // Sync to ensure data is flushed to disk before rename + if err := tmpFile.Sync(); err != nil { + tmpFile.Close() + return fmt.Errorf("failed to sync temp file: %w", err) + } + + if err := tmpFile.Close(); err != nil { + return fmt.Errorf("failed to close temp file: %w", err) + } + + // Set permissions before rename + if err := os.Chmod(tmpPath, perm); err != nil { + return fmt.Errorf("failed to set file permissions: %w", err) + } + + // Atomic rename (on POSIX systems) + if err := os.Rename(tmpPath, path); err != nil { + return fmt.Errorf("failed to rename temp file: %w", err) + } + + success = true + return nil +} diff --git a/mining/manager.go b/mining/manager.go new file mode 100644 index 0000000..177a9ac --- /dev/null +++ b/mining/manager.go @@ -0,0 +1,775 @@ +package mining + +import ( + "context" + "fmt" + "net" + "regexp" + "strings" + "sync" + "time" + + "forge.lthn.ai/core/mining/database" + "forge.lthn.ai/core/mining/logging" +) + +// sanitizeInstanceName ensures the instance name only contains safe characters. +var instanceNameRegex = regexp.MustCompile(`[^a-zA-Z0-9_/-]`) + +// ManagerInterface defines the contract for a miner manager. +type ManagerInterface interface { + StartMiner(ctx context.Context, minerType string, config *Config) (Miner, error) + StopMiner(ctx context.Context, name string) error + GetMiner(name string) (Miner, error) + ListMiners() []Miner + ListAvailableMiners() []AvailableMiner + GetMinerHashrateHistory(name string) ([]HashratePoint, error) + UninstallMiner(ctx context.Context, minerType string) error + Stop() +} + +// Manager handles the lifecycle and operations of multiple miners. +type Manager struct { + miners map[string]Miner + mu sync.RWMutex + stopChan chan struct{} + stopOnce sync.Once + waitGroup sync.WaitGroup + dbEnabled bool + dbRetention int + eventHub *EventHub + eventHubMu sync.RWMutex // Separate mutex for eventHub to avoid deadlock with main mu +} + +// SetEventHub sets the event hub for broadcasting miner events +func (m *Manager) SetEventHub(hub *EventHub) { + m.eventHubMu.Lock() + defer m.eventHubMu.Unlock() + m.eventHub = hub +} + +// emitEvent broadcasts an event if an event hub is configured +// Uses separate eventHubMu to avoid deadlock when called while holding m.mu +func (m *Manager) emitEvent(eventType EventType, data interface{}) { + m.eventHubMu.RLock() + hub := m.eventHub + m.eventHubMu.RUnlock() + + if hub != nil { + hub.Broadcast(NewEvent(eventType, data)) + } +} + +var _ ManagerInterface = (*Manager)(nil) + +// NewManager creates a new miner manager and autostarts miners based on config. +func NewManager() *Manager { + m := &Manager{ + miners: make(map[string]Miner), + stopChan: make(chan struct{}), + waitGroup: sync.WaitGroup{}, + } + m.syncMinersConfig() // Ensure config file is populated + m.initDatabase() + m.autostartMiners() + m.startStatsCollection() + return m +} + +// NewManagerForSimulation creates a manager for simulation mode. +// It skips autostarting real miners and config sync, suitable for UI testing. +func NewManagerForSimulation() *Manager { + m := &Manager{ + miners: make(map[string]Miner), + stopChan: make(chan struct{}), + waitGroup: sync.WaitGroup{}, + } + // Skip syncMinersConfig and autostartMiners for simulation + m.startStatsCollection() + return m +} + +// initDatabase initializes the SQLite database based on config. +func (m *Manager) initDatabase() { + cfg, err := LoadMinersConfig() + if err != nil { + logging.Warn("could not load config for database init", logging.Fields{"error": err}) + return + } + + m.dbEnabled = cfg.Database.Enabled + m.dbRetention = cfg.Database.RetentionDays + if m.dbRetention == 0 { + m.dbRetention = 30 + } + + if !m.dbEnabled { + logging.Debug("database persistence is disabled") + return + } + + dbCfg := database.Config{ + Enabled: true, + RetentionDays: m.dbRetention, + } + + if err := database.Initialize(dbCfg); err != nil { + logging.Warn("failed to initialize database", logging.Fields{"error": err}) + m.dbEnabled = false + return + } + + logging.Info("database persistence enabled", logging.Fields{"retention_days": m.dbRetention}) + + // Start periodic cleanup + m.startDBCleanup() +} + +// startDBCleanup starts a goroutine that periodically cleans old data. +func (m *Manager) startDBCleanup() { + m.waitGroup.Add(1) + go func() { + defer m.waitGroup.Done() + defer func() { + if r := recover(); r != nil { + logging.Error("panic in database cleanup goroutine", logging.Fields{"panic": r}) + } + }() + // Run cleanup once per hour + ticker := time.NewTicker(time.Hour) + defer ticker.Stop() + + // Run initial cleanup + if err := database.Cleanup(m.dbRetention); err != nil { + logging.Warn("database cleanup failed", logging.Fields{"error": err}) + } + + for { + select { + case <-ticker.C: + if err := database.Cleanup(m.dbRetention); err != nil { + logging.Warn("database cleanup failed", logging.Fields{"error": err}) + } + case <-m.stopChan: + return + } + } + }() +} + +// syncMinersConfig ensures the miners.json config file has entries for all available miners. +func (m *Manager) syncMinersConfig() { + cfg, err := LoadMinersConfig() + if err != nil { + logging.Warn("could not load miners config for sync", logging.Fields{"error": err}) + return + } + + availableMiners := m.ListAvailableMiners() + configUpdated := false + + for _, availableMiner := range availableMiners { + found := false + for _, configuredMiner := range cfg.Miners { + if strings.EqualFold(configuredMiner.MinerType, availableMiner.Name) { + found = true + break + } + } + if !found { + cfg.Miners = append(cfg.Miners, MinerAutostartConfig{ + MinerType: availableMiner.Name, + Autostart: false, + Config: nil, // No default config + }) + configUpdated = true + logging.Info("added default config for missing miner", logging.Fields{"miner": availableMiner.Name}) + } + } + + if configUpdated { + if err := SaveMinersConfig(cfg); err != nil { + logging.Warn("failed to save updated miners config", logging.Fields{"error": err}) + } + } +} + +// autostartMiners loads the miners config and starts any miners marked for autostart. +func (m *Manager) autostartMiners() { + cfg, err := LoadMinersConfig() + if err != nil { + logging.Warn("could not load miners config for autostart", logging.Fields{"error": err}) + return + } + + for _, minerCfg := range cfg.Miners { + if minerCfg.Autostart && minerCfg.Config != nil { + logging.Info("autostarting miner", logging.Fields{"type": minerCfg.MinerType}) + if _, err := m.StartMiner(context.Background(), minerCfg.MinerType, minerCfg.Config); err != nil { + logging.Error("failed to autostart miner", logging.Fields{"type": minerCfg.MinerType, "error": err}) + } + } + } +} + +// findAvailablePort finds an available TCP port on the local machine. +func findAvailablePort() (int, error) { + addr, err := net.ResolveTCPAddr("tcp", "localhost:0") + if err != nil { + return 0, err + } + l, err := net.ListenTCP("tcp", addr) + if err != nil { + return 0, err + } + defer l.Close() + return l.Addr().(*net.TCPAddr).Port, nil +} + +// StartMiner starts a new miner and saves its configuration. +// The context can be used to cancel the operation. +func (m *Manager) StartMiner(ctx context.Context, minerType string, config *Config) (Miner, error) { + // Check for cancellation before acquiring lock + select { + case <-ctx.Done(): + return nil, ctx.Err() + default: + } + + m.mu.Lock() + defer m.mu.Unlock() + + if config == nil { + config = &Config{} + } + + miner, err := CreateMiner(minerType) + if err != nil { + return nil, err + } + + instanceName := miner.GetName() + if config.Algo != "" { + // Sanitize algo to prevent directory traversal or invalid filenames + sanitizedAlgo := instanceNameRegex.ReplaceAllString(config.Algo, "_") + instanceName = fmt.Sprintf("%s-%s", instanceName, sanitizedAlgo) + } else { + instanceName = fmt.Sprintf("%s-%d", instanceName, time.Now().UnixNano()%1000) + } + + if _, exists := m.miners[instanceName]; exists { + return nil, fmt.Errorf("a miner with a similar configuration is already running: %s", instanceName) + } + + // Validate user-provided HTTPPort if specified + if config.HTTPPort != 0 { + if config.HTTPPort < 1024 || config.HTTPPort > 65535 { + return nil, fmt.Errorf("HTTPPort must be between 1024 and 65535, got %d", config.HTTPPort) + } + } + + apiPort, err := findAvailablePort() + if err != nil { + return nil, fmt.Errorf("failed to find an available port for the miner API: %w", err) + } + if config.HTTPPort == 0 { + config.HTTPPort = apiPort + } + + if xmrigMiner, ok := miner.(*XMRigMiner); ok { + xmrigMiner.Name = instanceName + if xmrigMiner.API != nil { + xmrigMiner.API.ListenPort = apiPort + } + } + if ttMiner, ok := miner.(*TTMiner); ok { + ttMiner.Name = instanceName + if ttMiner.API != nil { + ttMiner.API.ListenPort = apiPort + } + } + + // Emit starting event before actually starting + m.emitEvent(EventMinerStarting, MinerEventData{ + Name: instanceName, + }) + + if err := miner.Start(config); err != nil { + // Emit error event + m.emitEvent(EventMinerError, MinerEventData{ + Name: instanceName, + Error: err.Error(), + }) + return nil, err + } + + m.miners[instanceName] = miner + + if err := m.updateMinerConfig(minerType, true, config); err != nil { + logging.Warn("failed to save miner config for autostart", logging.Fields{"error": err}) + } + + logMessage := fmt.Sprintf("CryptoCurrency Miner started: %s (Binary: %s)", miner.GetName(), miner.GetBinaryPath()) + logToSyslog(logMessage) + + // Emit started event + m.emitEvent(EventMinerStarted, MinerEventData{ + Name: instanceName, + }) + + RecordMinerStart() + return miner, nil +} + +// UninstallMiner stops, uninstalls, and removes a miner's configuration. +// The context can be used to cancel the operation. +func (m *Manager) UninstallMiner(ctx context.Context, minerType string) error { + // Check for cancellation before acquiring lock + select { + case <-ctx.Done(): + return ctx.Err() + default: + } + + m.mu.Lock() + // Collect miners to stop and delete (can't modify map during iteration) + minersToDelete := make([]string, 0) + minersToStop := make([]Miner, 0) + for name, runningMiner := range m.miners { + if rm, ok := runningMiner.(*XMRigMiner); ok && strings.EqualFold(rm.ExecutableName, minerType) { + minersToStop = append(minersToStop, runningMiner) + minersToDelete = append(minersToDelete, name) + } + if rm, ok := runningMiner.(*TTMiner); ok && strings.EqualFold(rm.ExecutableName, minerType) { + minersToStop = append(minersToStop, runningMiner) + minersToDelete = append(minersToDelete, name) + } + } + // Delete from map first, then release lock before stopping (Stop may block) + for _, name := range minersToDelete { + delete(m.miners, name) + } + m.mu.Unlock() + + // Stop miners outside the lock to avoid blocking + for i, miner := range minersToStop { + if err := miner.Stop(); err != nil { + logging.Warn("failed to stop running miner during uninstall", logging.Fields{"miner": minersToDelete[i], "error": err}) + } + } + + miner, err := CreateMiner(minerType) + if err != nil { + return err + } + + if err := miner.Uninstall(); err != nil { + return fmt.Errorf("failed to uninstall miner files: %w", err) + } + + return UpdateMinersConfig(func(cfg *MinersConfig) error { + var updatedMiners []MinerAutostartConfig + for _, minerCfg := range cfg.Miners { + if !strings.EqualFold(minerCfg.MinerType, minerType) { + updatedMiners = append(updatedMiners, minerCfg) + } + } + cfg.Miners = updatedMiners + return nil + }) +} + +// updateMinerConfig saves the autostart and last-used config for a miner. +func (m *Manager) updateMinerConfig(minerType string, autostart bool, config *Config) error { + return UpdateMinersConfig(func(cfg *MinersConfig) error { + found := false + for i, minerCfg := range cfg.Miners { + if strings.EqualFold(minerCfg.MinerType, minerType) { + cfg.Miners[i].Autostart = autostart + cfg.Miners[i].Config = config + found = true + break + } + } + + if !found { + cfg.Miners = append(cfg.Miners, MinerAutostartConfig{ + MinerType: minerType, + Autostart: autostart, + Config: config, + }) + } + return nil + }) +} + +// StopMiner stops a running miner and removes it from the manager. +// If the miner is already stopped, it will still be removed from the manager. +// The context can be used to cancel the operation. +func (m *Manager) StopMiner(ctx context.Context, name string) error { + // Check for cancellation before acquiring lock + select { + case <-ctx.Done(): + return ctx.Err() + default: + } + + m.mu.Lock() + defer m.mu.Unlock() + + miner, exists := m.miners[name] + if !exists { + for k := range m.miners { + if strings.HasPrefix(k, name) { + miner = m.miners[k] + name = k + exists = true + break + } + } + } + + if !exists { + return fmt.Errorf("miner not found: %s", name) + } + + // Emit stopping event + m.emitEvent(EventMinerStopping, MinerEventData{ + Name: name, + }) + + // Try to stop the miner, but always remove it from the map + // This handles the case where a miner crashed or was killed externally + stopErr := miner.Stop() + + // Always remove from map - if it's not running, we still want to clean it up + delete(m.miners, name) + + // Emit stopped event + reason := "stopped" + if stopErr != nil && stopErr.Error() != "miner is not running" { + reason = stopErr.Error() + } + m.emitEvent(EventMinerStopped, MinerEventData{ + Name: name, + Reason: reason, + }) + + // Only return error if it wasn't just "miner is not running" + if stopErr != nil && stopErr.Error() != "miner is not running" { + return stopErr + } + + RecordMinerStop() + return nil +} + +// GetMiner retrieves a running miner by its name. +func (m *Manager) GetMiner(name string) (Miner, error) { + m.mu.RLock() + defer m.mu.RUnlock() + miner, exists := m.miners[name] + if !exists { + return nil, fmt.Errorf("miner not found: %s", name) + } + return miner, nil +} + +// ListMiners returns a slice of all running miners. +func (m *Manager) ListMiners() []Miner { + m.mu.RLock() + defer m.mu.RUnlock() + miners := make([]Miner, 0, len(m.miners)) + for _, miner := range m.miners { + miners = append(miners, miner) + } + return miners +} + +// RegisterMiner registers an already-started miner with the manager. +// This is useful for simulated miners or externally managed miners. +func (m *Manager) RegisterMiner(miner Miner) error { + name := miner.GetName() + + m.mu.Lock() + if _, exists := m.miners[name]; exists { + m.mu.Unlock() + return fmt.Errorf("miner %s is already registered", name) + } + m.miners[name] = miner + m.mu.Unlock() + + logging.Info("registered miner", logging.Fields{"name": name}) + + // Emit miner started event (outside lock) + m.emitEvent(EventMinerStarted, map[string]interface{}{ + "name": name, + }) + + return nil +} + +// ListAvailableMiners returns a list of available miners that can be started. +func (m *Manager) ListAvailableMiners() []AvailableMiner { + return []AvailableMiner{ + { + Name: "xmrig", + Description: "XMRig is a high performance, open source, cross platform RandomX, KawPow, CryptoNight and AstroBWT CPU/GPU miner and RandomX benchmark.", + }, + { + Name: "tt-miner", + Description: "TT-Miner is a high performance NVIDIA GPU miner for various algorithms including Ethash, KawPow, ProgPow, and more. Requires CUDA.", + }, + } +} + +// startStatsCollection starts a goroutine to periodically collect stats from active miners. +func (m *Manager) startStatsCollection() { + m.waitGroup.Add(1) + go func() { + defer m.waitGroup.Done() + defer func() { + if r := recover(); r != nil { + logging.Error("panic in stats collection goroutine", logging.Fields{"panic": r}) + } + }() + ticker := time.NewTicker(HighResolutionInterval) + defer ticker.Stop() + + for { + select { + case <-ticker.C: + m.collectMinerStats() + case <-m.stopChan: + return + } + } + }() +} + +// statsCollectionTimeout is the maximum time to wait for stats from a single miner. +const statsCollectionTimeout = 5 * time.Second + +// collectMinerStats iterates through active miners and collects their stats. +// Stats are collected in parallel to reduce overall collection time. +func (m *Manager) collectMinerStats() { + // Take a snapshot of miners under read lock - minimize lock duration + m.mu.RLock() + if len(m.miners) == 0 { + m.mu.RUnlock() + return + } + + type minerInfo struct { + miner Miner + minerType string + } + miners := make([]minerInfo, 0, len(m.miners)) + for _, miner := range m.miners { + // Use the miner's GetType() method for proper type identification + miners = append(miners, minerInfo{miner: miner, minerType: miner.GetType()}) + } + dbEnabled := m.dbEnabled // Copy to avoid holding lock + m.mu.RUnlock() + + now := time.Now() + + // Collect stats from all miners in parallel + var wg sync.WaitGroup + for _, mi := range miners { + wg.Add(1) + go func(miner Miner, minerType string) { + defer wg.Done() + defer func() { + if r := recover(); r != nil { + logging.Error("panic in single miner stats collection", logging.Fields{ + "panic": r, + "miner": miner.GetName(), + }) + } + }() + m.collectSingleMinerStats(miner, minerType, now, dbEnabled) + }(mi.miner, mi.minerType) + } + wg.Wait() +} + +// statsRetryCount is the number of retries for transient stats failures. +const statsRetryCount = 2 + +// statsRetryDelay is the delay between stats collection retries. +const statsRetryDelay = 500 * time.Millisecond + +// collectSingleMinerStats collects stats from a single miner with retry logic. +// This is called concurrently for each miner. +func (m *Manager) collectSingleMinerStats(miner Miner, minerType string, now time.Time, dbEnabled bool) { + minerName := miner.GetName() + + var stats *PerformanceMetrics + var lastErr error + + // Retry loop for transient failures + for attempt := 0; attempt <= statsRetryCount; attempt++ { + // Use context with timeout to prevent hanging on unresponsive miner APIs + ctx, cancel := context.WithTimeout(context.Background(), statsCollectionTimeout) + stats, lastErr = miner.GetStats(ctx) + cancel() // Release context immediately + + if lastErr == nil { + break // Success + } + + // Log retry attempts at debug level + if attempt < statsRetryCount { + logging.Debug("retrying stats collection", logging.Fields{ + "miner": minerName, + "attempt": attempt + 1, + "error": lastErr.Error(), + }) + time.Sleep(statsRetryDelay) + } + } + + if lastErr != nil { + logging.Error("failed to get miner stats after retries", logging.Fields{ + "miner": minerName, + "error": lastErr.Error(), + "retries": statsRetryCount, + }) + RecordStatsCollection(true, true) + return + } + + // Record stats collection (retried if we did any retries) + RecordStatsCollection(stats != nil && lastErr == nil, false) + + point := HashratePoint{ + Timestamp: now, + Hashrate: stats.Hashrate, + } + + // Add to in-memory history (rolling window) + // Note: AddHashratePoint and ReduceHashrateHistory must be thread-safe + miner.AddHashratePoint(point) + miner.ReduceHashrateHistory(now) + + // Persist to database if enabled + if dbEnabled { + dbPoint := database.HashratePoint{ + Timestamp: point.Timestamp, + Hashrate: point.Hashrate, + } + // Create a new context for DB writes (original context is from retry loop) + dbCtx, dbCancel := context.WithTimeout(context.Background(), statsCollectionTimeout) + if err := database.InsertHashratePoint(dbCtx, minerName, minerType, dbPoint, database.ResolutionHigh); err != nil { + logging.Warn("failed to persist hashrate", logging.Fields{"miner": minerName, "error": err}) + } + dbCancel() + } + + // Emit stats event for real-time WebSocket updates + m.emitEvent(EventMinerStats, MinerStatsData{ + Name: minerName, + Hashrate: stats.Hashrate, + Shares: stats.Shares, + Rejected: stats.Rejected, + Uptime: stats.Uptime, + Algorithm: stats.Algorithm, + DiffCurrent: stats.DiffCurrent, + }) +} + +// GetMinerHashrateHistory returns the hashrate history for a specific miner. +func (m *Manager) GetMinerHashrateHistory(name string) ([]HashratePoint, error) { + m.mu.RLock() + defer m.mu.RUnlock() + miner, exists := m.miners[name] + if !exists { + return nil, fmt.Errorf("miner not found: %s", name) + } + return miner.GetHashrateHistory(), nil +} + +// ShutdownTimeout is the maximum time to wait for goroutines during shutdown +const ShutdownTimeout = 10 * time.Second + +// Stop stops all running miners, background goroutines, and closes resources. +// Safe to call multiple times - subsequent calls are no-ops. +func (m *Manager) Stop() { + m.stopOnce.Do(func() { + // Stop all running miners first + m.mu.Lock() + for name, miner := range m.miners { + if err := miner.Stop(); err != nil { + logging.Warn("failed to stop miner", logging.Fields{"miner": name, "error": err}) + } + } + m.mu.Unlock() + + close(m.stopChan) + + // Wait for goroutines with timeout + done := make(chan struct{}) + go func() { + m.waitGroup.Wait() + close(done) + }() + + select { + case <-done: + logging.Info("all goroutines stopped gracefully") + case <-time.After(ShutdownTimeout): + logging.Warn("shutdown timeout - some goroutines may not have stopped") + } + + // Close the database + if m.dbEnabled { + if err := database.Close(); err != nil { + logging.Warn("failed to close database", logging.Fields{"error": err}) + } + } + }) +} + +// GetMinerHistoricalStats returns historical stats from the database for a miner. +func (m *Manager) GetMinerHistoricalStats(minerName string) (*database.HashrateStats, error) { + if !m.dbEnabled { + return nil, fmt.Errorf("database persistence is disabled") + } + return database.GetHashrateStats(minerName) +} + +// GetMinerHistoricalHashrate returns historical hashrate data from the database. +func (m *Manager) GetMinerHistoricalHashrate(minerName string, since, until time.Time) ([]HashratePoint, error) { + if !m.dbEnabled { + return nil, fmt.Errorf("database persistence is disabled") + } + + dbPoints, err := database.GetHashrateHistory(minerName, database.ResolutionHigh, since, until) + if err != nil { + return nil, err + } + + // Convert database points to mining points + points := make([]HashratePoint, len(dbPoints)) + for i, p := range dbPoints { + points[i] = HashratePoint{ + Timestamp: p.Timestamp, + Hashrate: p.Hashrate, + } + } + return points, nil +} + +// GetAllMinerHistoricalStats returns historical stats for all miners from the database. +func (m *Manager) GetAllMinerHistoricalStats() ([]database.HashrateStats, error) { + if !m.dbEnabled { + return nil, fmt.Errorf("database persistence is disabled") + } + return database.GetAllMinerStats() +} + +// IsDatabaseEnabled returns whether database persistence is enabled. +func (m *Manager) IsDatabaseEnabled() bool { + return m.dbEnabled +} diff --git a/mining/manager_interface.go b/mining/manager_interface.go new file mode 100644 index 0000000..ffb8394 --- /dev/null +++ b/mining/manager_interface.go @@ -0,0 +1,5 @@ +package mining + +// This file is intentionally left with only a package declaration +// to resolve a redeclaration error. The ManagerInterface is defined +// in manager.go. diff --git a/mining/manager_race_test.go b/mining/manager_race_test.go new file mode 100644 index 0000000..6b0e830 --- /dev/null +++ b/mining/manager_race_test.go @@ -0,0 +1,314 @@ +package mining + +import ( + "context" + "sync" + "testing" + "time" +) + +// TestConcurrentStartMultipleMiners verifies that concurrent StartMiner calls +// with different algorithms create unique miners without race conditions +func TestConcurrentStartMultipleMiners(t *testing.T) { + m := setupTestManager(t) + defer m.Stop() + + var wg sync.WaitGroup + errors := make(chan error, 10) + + // Try to start 10 miners concurrently with different algos + for i := 0; i < 10; i++ { + wg.Add(1) + go func(index int) { + defer wg.Done() + config := &Config{ + HTTPPort: 10000 + index, + Pool: "test:1234", + Wallet: "testwallet", + Algo: "algo" + string(rune('A'+index)), // algoA, algoB, etc. + } + _, err := m.StartMiner(context.Background(), "xmrig", config) + if err != nil { + errors <- err + } + }(i) + } + + wg.Wait() + close(errors) + + // Collect errors + var errCount int + for err := range errors { + t.Logf("Concurrent start error: %v", err) + errCount++ + } + + // Some failures are expected due to port conflicts, but shouldn't crash + t.Logf("Started miners with %d errors out of 10 attempts", errCount) + + // Verify no data races occurred (test passes if no race detector warnings) +} + +// TestConcurrentStartDuplicateMiner verifies that starting the same miner +// concurrently results in only one success +func TestConcurrentStartDuplicateMiner(t *testing.T) { + m := setupTestManager(t) + defer m.Stop() + + var wg sync.WaitGroup + successes := make(chan struct{}, 10) + failures := make(chan error, 10) + + // Try to start the same miner 10 times concurrently + for i := 0; i < 10; i++ { + wg.Add(1) + go func() { + defer wg.Done() + config := &Config{ + HTTPPort: 11000, + Pool: "test:1234", + Wallet: "testwallet", + Algo: "duplicate_test", // Same algo = same instance name + } + _, err := m.StartMiner(context.Background(), "xmrig", config) + if err != nil { + failures <- err + } else { + successes <- struct{}{} + } + }() + } + + wg.Wait() + close(successes) + close(failures) + + successCount := len(successes) + failureCount := len(failures) + + t.Logf("Duplicate miner test: %d successes, %d failures", successCount, failureCount) + + // Only one should succeed (or zero if there's a timing issue) + if successCount > 1 { + t.Errorf("Expected at most 1 success for duplicate miner, got %d", successCount) + } +} + +// TestConcurrentStartStop verifies that starting and stopping miners +// concurrently doesn't cause race conditions +func TestConcurrentStartStop(t *testing.T) { + m := setupTestManager(t) + defer m.Stop() + + var wg sync.WaitGroup + + // Start some miners + for i := 0; i < 5; i++ { + config := &Config{ + HTTPPort: 12000 + i, + Pool: "test:1234", + Wallet: "testwallet", + Algo: "startstop" + string(rune('A'+i)), + } + _, err := m.StartMiner(context.Background(), "xmrig", config) + if err != nil { + t.Logf("Setup error (may be expected): %v", err) + } + } + + // Give miners time to start + time.Sleep(100 * time.Millisecond) + + // Now concurrently start new ones and stop existing ones + for i := 0; i < 10; i++ { + wg.Add(2) + + // Start a new miner + go func(index int) { + defer wg.Done() + config := &Config{ + HTTPPort: 12100 + index, + Pool: "test:1234", + Wallet: "testwallet", + Algo: "new" + string(rune('A'+index)), + } + m.StartMiner(context.Background(), "xmrig", config) + }(i) + + // Stop a miner + go func(index int) { + defer wg.Done() + minerName := "xmrig-startstop" + string(rune('A'+index%5)) + m.StopMiner(context.Background(), minerName) + }(i) + } + + wg.Wait() + + // Test passes if no race detector warnings +} + +// TestConcurrentListMiners verifies that listing miners while modifying +// the miner map doesn't cause race conditions +func TestConcurrentListMiners(t *testing.T) { + m := setupTestManager(t) + defer m.Stop() + + var wg sync.WaitGroup + ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second) + defer cancel() + + // Continuously list miners + wg.Add(1) + go func() { + defer wg.Done() + for { + select { + case <-ctx.Done(): + return + default: + miners := m.ListMiners() + _ = len(miners) // Use the result + } + } + }() + + // Continuously start miners + wg.Add(1) + go func() { + defer wg.Done() + for i := 0; i < 20; i++ { + select { + case <-ctx.Done(): + return + default: + config := &Config{ + HTTPPort: 13000 + i, + Pool: "test:1234", + Wallet: "testwallet", + Algo: "list" + string(rune('A'+i%26)), + } + m.StartMiner(context.Background(), "xmrig", config) + time.Sleep(10 * time.Millisecond) + } + } + }() + + wg.Wait() + + // Test passes if no race detector warnings +} + +// TestConcurrentGetMiner verifies that getting a miner while others +// are being started/stopped doesn't cause race conditions +func TestConcurrentGetMiner(t *testing.T) { + m := setupTestManager(t) + defer m.Stop() + + // Start a miner first + config := &Config{ + HTTPPort: 14000, + Pool: "test:1234", + Wallet: "testwallet", + Algo: "gettest", + } + miner, err := m.StartMiner(context.Background(), "xmrig", config) + if err != nil { + t.Skipf("Could not start test miner: %v", err) + } + minerName := miner.GetName() + + var wg sync.WaitGroup + ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second) + defer cancel() + + // Continuously get the miner + for i := 0; i < 5; i++ { + wg.Add(1) + go func() { + defer wg.Done() + for { + select { + case <-ctx.Done(): + return + default: + m.GetMiner(minerName) + time.Sleep(time.Millisecond) + } + } + }() + } + + // Start more miners in parallel + wg.Add(1) + go func() { + defer wg.Done() + for i := 0; i < 10; i++ { + select { + case <-ctx.Done(): + return + default: + config := &Config{ + HTTPPort: 14100 + i, + Pool: "test:1234", + Wallet: "testwallet", + Algo: "parallel" + string(rune('A'+i)), + } + m.StartMiner(context.Background(), "xmrig", config) + } + } + }() + + wg.Wait() + + // Test passes if no race detector warnings +} + +// TestConcurrentStatsCollection verifies that stats collection +// doesn't race with miner operations +func TestConcurrentStatsCollection(t *testing.T) { + m := setupTestManager(t) + defer m.Stop() + + // Start some miners + for i := 0; i < 3; i++ { + config := &Config{ + HTTPPort: 15000 + i, + Pool: "test:1234", + Wallet: "testwallet", + Algo: "stats" + string(rune('A'+i)), + } + m.StartMiner(context.Background(), "xmrig", config) + } + + var wg sync.WaitGroup + + // Simulate stats collection (normally done by background goroutine) + wg.Add(1) + go func() { + defer wg.Done() + for i := 0; i < 50; i++ { + miners := m.ListMiners() + for _, miner := range miners { + miner.GetStats(context.Background()) + } + time.Sleep(10 * time.Millisecond) + } + }() + + // Concurrently stop miners + wg.Add(1) + go func() { + defer wg.Done() + time.Sleep(100 * time.Millisecond) // Let stats collection start + for _, name := range []string{"xmrig-statsA", "xmrig-statsB", "xmrig-statsC"} { + m.StopMiner(context.Background(), name) + time.Sleep(50 * time.Millisecond) + } + }() + + wg.Wait() + + // Test passes if no race detector warnings +} diff --git a/mining/manager_test.go b/mining/manager_test.go new file mode 100644 index 0000000..3ee1a96 --- /dev/null +++ b/mining/manager_test.go @@ -0,0 +1,140 @@ +package mining + +import ( + "context" + "os" + "path/filepath" + "runtime" + "testing" +) + +// setupTestManager creates a new Manager and a dummy executable for tests. +// It also temporarily modifies the PATH to include the dummy executable's directory. +func setupTestManager(t *testing.T) *Manager { + dummyDir := t.TempDir() + executableName := "miner" + if runtime.GOOS == "windows" { + executableName += ".exe" + } + dummyPath := filepath.Join(dummyDir, executableName) + + // Create a script that prints version and exits + var script []byte + if runtime.GOOS == "windows" { + script = []byte("@echo off\necho XMRig 6.24.0\n") + } else { + script = []byte("#!/bin/sh\necho 'XMRig 6.24.0'\n") + } + + if err := os.WriteFile(dummyPath, script, 0755); err != nil { + t.Fatalf("Failed to create dummy miner executable: %v", err) + } + + // Prepend the dummy directory to the PATH + originalPath := os.Getenv("PATH") + t.Cleanup(func() { + os.Setenv("PATH", originalPath) + }) + os.Setenv("PATH", dummyDir+string(os.PathListSeparator)+originalPath) + + return NewManager() +} + +// TestStartMiner tests the StartMiner function +func TestStartMiner_Good(t *testing.T) { + t.Skip("Skipping test that runs miner process as per request") +} + +func TestStartMiner_Bad(t *testing.T) { + m := setupTestManager(t) + defer m.Stop() + + config := &Config{ + HTTPPort: 9001, // Use a different port to avoid conflict + Pool: "test:1234", + Wallet: "testwallet", + } + + // Case 2: Attempt to start an unsupported miner + _, err := m.StartMiner(context.Background(), "unsupported", config) + if err == nil { + t.Error("Expected an error when starting an unsupported miner, but got nil") + } +} + +func TestStartMiner_Ugly(t *testing.T) { + t.Skip("Skipping test that runs miner process") +} + +// TestStopMiner tests the StopMiner function +func TestStopMiner_Good(t *testing.T) { + t.Skip("Skipping test that runs miner process") +} + +func TestStopMiner_Bad(t *testing.T) { + m := setupTestManager(t) + defer m.Stop() + + // Case 2: Attempt to stop a non-existent miner + err := m.StopMiner(context.Background(), "nonexistent") + if err == nil { + t.Error("Expected an error when stopping a non-existent miner, but got nil") + } +} + +// TestGetMiner tests the GetMiner function +func TestGetMiner_Good(t *testing.T) { + m := setupTestManager(t) + defer m.Stop() + + // Case 1: Get an existing miner (manually injected) + miner := NewXMRigMiner() + // Set name to match what StartMiner would produce usually ("xmrig") + // Since we inject it, we can use the default name or set one. + miner.Name = "xmrig-test" + m.mu.Lock() + m.miners["xmrig-test"] = miner + m.mu.Unlock() + + retrievedMiner, err := m.GetMiner("xmrig-test") + if err != nil { + t.Fatalf("Expected to get miner, but got error: %v", err) + } + if retrievedMiner.GetName() != "xmrig-test" { + t.Errorf("Expected to get miner 'xmrig-test', but got %s", retrievedMiner.GetName()) + } +} + +func TestGetMiner_Bad(t *testing.T) { + m := setupTestManager(t) + defer m.Stop() + + // Case 2: Attempt to get a non-existent miner + _, err := m.GetMiner("nonexistent") + if err == nil { + t.Error("Expected an error when getting a non-existent miner, but got nil") + } +} + +// TestListMiners tests the ListMiners function +func TestListMiners_Good(t *testing.T) { + m := setupTestManager(t) + defer m.Stop() + + // Get initial count (may include autostarted miners from config) + initialMiners := m.ListMiners() + initialCount := len(initialMiners) + + // Case 2: List miners when not empty (manually injected) + miner := NewXMRigMiner() + miner.Name = "xmrig-test" + m.mu.Lock() + m.miners["xmrig-test"] = miner + m.mu.Unlock() + + finalMiners := m.ListMiners() + expectedCount := initialCount + 1 + if len(finalMiners) != expectedCount { + t.Errorf("Expected %d miners, but got %d", expectedCount, len(finalMiners)) + } +} diff --git a/mining/metrics.go b/mining/metrics.go new file mode 100644 index 0000000..5a62015 --- /dev/null +++ b/mining/metrics.go @@ -0,0 +1,169 @@ +package mining + +import ( + "sync" + "sync/atomic" + "time" +) + +// Metrics provides simple instrumentation counters for the mining package. +// These can be exposed via Prometheus or other metrics systems in the future. +type Metrics struct { + // API metrics + RequestsTotal atomic.Int64 + RequestsErrored atomic.Int64 + RequestLatency *LatencyHistogram + + // Miner metrics + MinersStarted atomic.Int64 + MinersStopped atomic.Int64 + MinersErrored atomic.Int64 + + // Stats collection metrics + StatsCollected atomic.Int64 + StatsRetried atomic.Int64 + StatsFailed atomic.Int64 + + // WebSocket metrics + WSConnections atomic.Int64 + WSMessages atomic.Int64 + + // P2P metrics + P2PMessagesSent atomic.Int64 + P2PMessagesReceived atomic.Int64 + P2PConnectionsTotal atomic.Int64 +} + +// LatencyHistogram tracks request latencies with basic percentile support. +type LatencyHistogram struct { + mu sync.Mutex + samples []time.Duration + maxSize int +} + +// NewLatencyHistogram creates a new latency histogram with a maximum sample size. +func NewLatencyHistogram(maxSize int) *LatencyHistogram { + return &LatencyHistogram{ + samples: make([]time.Duration, 0, maxSize), + maxSize: maxSize, + } +} + +// Record adds a latency sample. +func (h *LatencyHistogram) Record(d time.Duration) { + h.mu.Lock() + defer h.mu.Unlock() + + if len(h.samples) >= h.maxSize { + // Ring buffer behavior - overwrite oldest + copy(h.samples, h.samples[1:]) + h.samples = h.samples[:len(h.samples)-1] + } + h.samples = append(h.samples, d) +} + +// Average returns the average latency. +func (h *LatencyHistogram) Average() time.Duration { + h.mu.Lock() + defer h.mu.Unlock() + + if len(h.samples) == 0 { + return 0 + } + + var total time.Duration + for _, d := range h.samples { + total += d + } + return total / time.Duration(len(h.samples)) +} + +// Count returns the number of samples. +func (h *LatencyHistogram) Count() int { + h.mu.Lock() + defer h.mu.Unlock() + return len(h.samples) +} + +// DefaultMetrics is the global metrics instance. +var DefaultMetrics = &Metrics{ + RequestLatency: NewLatencyHistogram(1000), +} + +// RecordRequest records an API request. +func RecordRequest(errored bool, latency time.Duration) { + DefaultMetrics.RequestsTotal.Add(1) + if errored { + DefaultMetrics.RequestsErrored.Add(1) + } + DefaultMetrics.RequestLatency.Record(latency) +} + +// RecordMinerStart records a miner start event. +func RecordMinerStart() { + DefaultMetrics.MinersStarted.Add(1) +} + +// RecordMinerStop records a miner stop event. +func RecordMinerStop() { + DefaultMetrics.MinersStopped.Add(1) +} + +// RecordMinerError records a miner error event. +func RecordMinerError() { + DefaultMetrics.MinersErrored.Add(1) +} + +// RecordStatsCollection records a stats collection event. +func RecordStatsCollection(retried bool, failed bool) { + DefaultMetrics.StatsCollected.Add(1) + if retried { + DefaultMetrics.StatsRetried.Add(1) + } + if failed { + DefaultMetrics.StatsFailed.Add(1) + } +} + +// RecordWSConnection increments or decrements WebSocket connection count. +func RecordWSConnection(connected bool) { + if connected { + DefaultMetrics.WSConnections.Add(1) + } else { + DefaultMetrics.WSConnections.Add(-1) + } +} + +// RecordWSMessage records a WebSocket message. +func RecordWSMessage() { + DefaultMetrics.WSMessages.Add(1) +} + +// RecordP2PMessage records a P2P message. +func RecordP2PMessage(sent bool) { + if sent { + DefaultMetrics.P2PMessagesSent.Add(1) + } else { + DefaultMetrics.P2PMessagesReceived.Add(1) + } +} + +// GetMetricsSnapshot returns a snapshot of current metrics. +func GetMetricsSnapshot() map[string]interface{} { + return map[string]interface{}{ + "requests_total": DefaultMetrics.RequestsTotal.Load(), + "requests_errored": DefaultMetrics.RequestsErrored.Load(), + "request_latency_avg_ms": DefaultMetrics.RequestLatency.Average().Milliseconds(), + "request_latency_samples": DefaultMetrics.RequestLatency.Count(), + "miners_started": DefaultMetrics.MinersStarted.Load(), + "miners_stopped": DefaultMetrics.MinersStopped.Load(), + "miners_errored": DefaultMetrics.MinersErrored.Load(), + "stats_collected": DefaultMetrics.StatsCollected.Load(), + "stats_retried": DefaultMetrics.StatsRetried.Load(), + "stats_failed": DefaultMetrics.StatsFailed.Load(), + "ws_connections": DefaultMetrics.WSConnections.Load(), + "ws_messages": DefaultMetrics.WSMessages.Load(), + "p2p_messages_sent": DefaultMetrics.P2PMessagesSent.Load(), + "p2p_messages_received": DefaultMetrics.P2PMessagesReceived.Load(), + } +} diff --git a/mining/miner.go b/mining/miner.go new file mode 100644 index 0000000..0bc7b76 --- /dev/null +++ b/mining/miner.go @@ -0,0 +1,635 @@ +package mining + +import ( + "archive/tar" + "archive/zip" + "bytes" + "compress/gzip" + "errors" + "fmt" + "io" + "net/http" + "os" + "os/exec" + "path/filepath" + "runtime" + "sort" + "strconv" + "strings" + "sync" + "syscall" + "time" + + "forge.lthn.ai/core/mining/logging" + "github.com/adrg/xdg" +) + +// LogBuffer is a thread-safe ring buffer for capturing miner output. +type LogBuffer struct { + lines []string + maxLines int + mu sync.RWMutex +} + +// NewLogBuffer creates a new log buffer with the specified max lines. +func NewLogBuffer(maxLines int) *LogBuffer { + return &LogBuffer{ + lines: make([]string, 0, maxLines), + maxLines: maxLines, + } +} + +// maxLineLength is the maximum length of a single log line to prevent memory bloat. +const maxLineLength = 2000 + +// Write implements io.Writer for capturing output. +func (lb *LogBuffer) Write(p []byte) (n int, err error) { + lb.mu.Lock() + defer lb.mu.Unlock() + + // Split input into lines + text := string(p) + newLines := strings.Split(text, "\n") + + for _, line := range newLines { + if line == "" { + continue + } + // Truncate excessively long lines to prevent memory bloat + if len(line) > maxLineLength { + line = line[:maxLineLength] + "... [truncated]" + } + // Add timestamp prefix + timestampedLine := fmt.Sprintf("[%s] %s", time.Now().Format("15:04:05"), line) + lb.lines = append(lb.lines, timestampedLine) + + // Trim if over max - force reallocation to release memory + if len(lb.lines) > lb.maxLines { + newSlice := make([]string, lb.maxLines) + copy(newSlice, lb.lines[len(lb.lines)-lb.maxLines:]) + lb.lines = newSlice + } + } + return len(p), nil +} + +// GetLines returns all captured log lines. +func (lb *LogBuffer) GetLines() []string { + lb.mu.RLock() + defer lb.mu.RUnlock() + result := make([]string, len(lb.lines)) + copy(result, lb.lines) + return result +} + +// Clear clears the log buffer. +func (lb *LogBuffer) Clear() { + lb.mu.Lock() + defer lb.mu.Unlock() + lb.lines = lb.lines[:0] +} + +// BaseMiner provides a foundation for specific miner implementations. +type BaseMiner struct { + Name string `json:"name"` + MinerType string `json:"miner_type"` // Type identifier (e.g., "xmrig", "tt-miner") + Version string `json:"version"` + URL string `json:"url"` + Path string `json:"path"` + MinerBinary string `json:"miner_binary"` + ExecutableName string `json:"-"` + Running bool `json:"running"` + ConfigPath string `json:"configPath"` + API *API `json:"api"` + mu sync.RWMutex + cmd *exec.Cmd + stdinPipe io.WriteCloser `json:"-"` + HashrateHistory []HashratePoint `json:"hashrateHistory"` + LowResHashrateHistory []HashratePoint `json:"lowResHashrateHistory"` + LastLowResAggregation time.Time `json:"-"` + LogBuffer *LogBuffer `json:"-"` +} + +// GetType returns the miner type identifier. +func (b *BaseMiner) GetType() string { + return b.MinerType +} + +// GetName returns the name of the miner. +func (b *BaseMiner) GetName() string { + b.mu.RLock() + defer b.mu.RUnlock() + return b.Name +} + +// GetPath returns the base installation directory for the miner type. +// It uses the stable ExecutableName field to ensure the correct path. +func (b *BaseMiner) GetPath() string { + dataPath, err := xdg.DataFile(fmt.Sprintf("lethean-desktop/miners/%s", b.ExecutableName)) + if err != nil { + home, err := os.UserHomeDir() + if err != nil { + return "" + } + return filepath.Join(home, ".lethean-desktop", "miners", b.ExecutableName) + } + return dataPath +} + +// GetBinaryPath returns the full path to the miner's executable file. +func (b *BaseMiner) GetBinaryPath() string { + b.mu.RLock() + defer b.mu.RUnlock() + return b.MinerBinary +} + +// Stop terminates the miner process gracefully. +// It first tries SIGTERM to allow cleanup, then SIGKILL if needed. +func (b *BaseMiner) Stop() error { + b.mu.Lock() + + if !b.Running || b.cmd == nil { + b.mu.Unlock() + return errors.New("miner is not running") + } + + // Close stdin pipe if open + if b.stdinPipe != nil { + b.stdinPipe.Close() + b.stdinPipe = nil + } + + // Capture cmd locally to avoid race with Wait() goroutine + cmd := b.cmd + process := cmd.Process + + // Mark as not running immediately to prevent concurrent Stop() calls + b.Running = false + b.cmd = nil + b.mu.Unlock() + + // Try graceful shutdown with SIGTERM first (Unix only) + if runtime.GOOS != "windows" { + if err := process.Signal(syscall.SIGTERM); err == nil { + // Wait up to 3 seconds for graceful shutdown + done := make(chan struct{}) + go func() { + process.Wait() + close(done) + }() + + select { + case <-done: + return nil + case <-time.After(3 * time.Second): + // Process didn't exit gracefully, force kill below + } + } + } + + // Force kill and wait for process to exit + if err := process.Kill(); err != nil { + return err + } + + // Wait for process to fully terminate to avoid zombies + process.Wait() + return nil +} + +// stdinWriteTimeout is the maximum time to wait for stdin write to complete. +const stdinWriteTimeout = 5 * time.Second + +// WriteStdin sends input to the miner's stdin (for console commands). +func (b *BaseMiner) WriteStdin(input string) error { + b.mu.RLock() + stdinPipe := b.stdinPipe + running := b.Running + b.mu.RUnlock() + + if !running || stdinPipe == nil { + return errors.New("miner is not running or stdin not available") + } + + // Append newline if not present + if !strings.HasSuffix(input, "\n") { + input += "\n" + } + + // Write with timeout to prevent blocking indefinitely. + // Use buffered channel size 1 so goroutine can exit even if we don't read the result. + done := make(chan error, 1) + go func() { + _, err := stdinPipe.Write([]byte(input)) + // Non-blocking send - if timeout already fired, this won't block + select { + case done <- err: + default: + // Timeout already occurred, goroutine exits cleanly + } + }() + + select { + case err := <-done: + return err + case <-time.After(stdinWriteTimeout): + return errors.New("stdin write timeout: miner may be unresponsive") + } +} + +// Uninstall removes all files related to the miner. +func (b *BaseMiner) Uninstall() error { + return os.RemoveAll(b.GetPath()) +} + +// InstallFromURL handles the generic download and extraction process for a miner. +func (b *BaseMiner) InstallFromURL(url string) error { + tmpfile, err := os.CreateTemp("", b.ExecutableName+"-") + if err != nil { + return err + } + defer os.Remove(tmpfile.Name()) + defer tmpfile.Close() + + resp, err := getHTTPClient().Get(url) + if err != nil { + return err + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + _, _ = io.Copy(io.Discard, resp.Body) // Drain body to allow connection reuse (error ignored intentionally) + return fmt.Errorf("failed to download release: unexpected status code %d", resp.StatusCode) + } + + if _, err := io.Copy(tmpfile, resp.Body); err != nil { + // Drain remaining body to allow connection reuse (error ignored intentionally) + _, _ = io.Copy(io.Discard, resp.Body) + return err + } + + baseInstallPath := b.GetPath() + if err := os.MkdirAll(baseInstallPath, 0755); err != nil { + return err + } + + if strings.HasSuffix(url, ".zip") { + err = b.unzip(tmpfile.Name(), baseInstallPath) + } else { + err = b.untar(tmpfile.Name(), baseInstallPath) + } + if err != nil { + return fmt.Errorf("failed to extract miner: %w", err) + } + + return nil +} + +// parseVersion parses a version string (e.g., "6.24.0") into a slice of integers for comparison. +func parseVersion(v string) []int { + parts := strings.Split(v, ".") + intParts := make([]int, len(parts)) + for i, p := range parts { + val, err := strconv.Atoi(p) + if err != nil { + return []int{0} // Malformed version, treat as very old + } + intParts[i] = val + } + return intParts +} + +// compareVersions compares two version slices. Returns 1 if v1 > v2, -1 if v1 < v2, 0 if equal. +func compareVersions(v1, v2 []int) int { + minLen := len(v1) + if len(v2) < minLen { + minLen = len(v2) + } + + for i := 0; i < minLen; i++ { + if v1[i] > v2[i] { + return 1 + } + if v1[i] < v2[i] { + return -1 + } + } + + if len(v1) > len(v2) { + return 1 + } + if len(v1) < len(v2) { + return -1 + } + return 0 +} + +// findMinerBinary searches for the miner's executable file. +// It returns the absolute path to the executable if found, prioritizing the highest versioned installation. +func (b *BaseMiner) findMinerBinary() (string, error) { + executableName := b.ExecutableName + if runtime.GOOS == "windows" { + executableName += ".exe" + } + + baseInstallPath := b.GetPath() + searchedPaths := []string{} + + var highestVersion []int + var highestVersionDir string + + // 1. Check the standard installation directory first + if _, err := os.Stat(baseInstallPath); err == nil { + dirs, err := os.ReadDir(baseInstallPath) + if err == nil { + for _, d := range dirs { + if d.IsDir() && strings.HasPrefix(d.Name(), b.ExecutableName+"-") { + // Extract version string, e.g., "xmrig-6.24.0" -> "6.24.0" + versionStr := strings.TrimPrefix(d.Name(), b.ExecutableName+"-") + currentVersion := parseVersion(versionStr) + + if highestVersionDir == "" || compareVersions(currentVersion, highestVersion) > 0 { + highestVersion = currentVersion + highestVersionDir = d.Name() + } + versionedPath := filepath.Join(baseInstallPath, d.Name()) + fullPath := filepath.Join(versionedPath, executableName) + searchedPaths = append(searchedPaths, fullPath) + } + } + } + + if highestVersionDir != "" { + fullPath := filepath.Join(baseInstallPath, highestVersionDir, executableName) + if _, err := os.Stat(fullPath); err == nil { + logging.Debug("found miner binary at highest versioned path", logging.Fields{"path": fullPath}) + return fullPath, nil + } + } + } + + // 2. Fallback to searching the system PATH + path, err := exec.LookPath(executableName) + if err == nil { + absPath, err := filepath.Abs(path) + if err != nil { + return "", fmt.Errorf("failed to get absolute path for '%s': %w", path, err) + } + logging.Debug("found miner binary in system PATH", logging.Fields{"path": absPath}) + return absPath, nil + } + + // If not found, return a detailed error + return "", fmt.Errorf("miner executable '%s' not found. Searched in: %s and system PATH", executableName, strings.Join(searchedPaths, ", ")) +} + +// CheckInstallation verifies if the miner is installed correctly. +func (b *BaseMiner) CheckInstallation() (*InstallationDetails, error) { + binaryPath, err := b.findMinerBinary() + if err != nil { + return &InstallationDetails{IsInstalled: false}, err + } + + b.MinerBinary = binaryPath + b.Path = filepath.Dir(binaryPath) + + cmd := exec.Command(binaryPath, "--version") + var out bytes.Buffer + cmd.Stdout = &out + if err := cmd.Run(); err != nil { + b.Version = "Unknown (could not run executable)" + } else { + fields := strings.Fields(out.String()) + if len(fields) >= 2 { + b.Version = fields[1] + } else { + b.Version = "Unknown (could not parse version)" + } + } + + return &InstallationDetails{ + IsInstalled: true, + MinerBinary: b.MinerBinary, + Path: b.Path, + Version: b.Version, + }, nil +} + +// GetHashrateHistory returns the combined hashrate history. +func (b *BaseMiner) GetHashrateHistory() []HashratePoint { + b.mu.RLock() + defer b.mu.RUnlock() + combinedHistory := make([]HashratePoint, 0, len(b.LowResHashrateHistory)+len(b.HashrateHistory)) + combinedHistory = append(combinedHistory, b.LowResHashrateHistory...) + combinedHistory = append(combinedHistory, b.HashrateHistory...) + return combinedHistory +} + +// AddHashratePoint adds a new hashrate measurement. +func (b *BaseMiner) AddHashratePoint(point HashratePoint) { + b.mu.Lock() + defer b.mu.Unlock() + b.HashrateHistory = append(b.HashrateHistory, point) +} + +// GetHighResHistoryLength returns the number of high-resolution hashrate points. +func (b *BaseMiner) GetHighResHistoryLength() int { + b.mu.RLock() + defer b.mu.RUnlock() + return len(b.HashrateHistory) +} + +// GetLowResHistoryLength returns the number of low-resolution hashrate points. +func (b *BaseMiner) GetLowResHistoryLength() int { + b.mu.RLock() + defer b.mu.RUnlock() + return len(b.LowResHashrateHistory) +} + +// GetLogs returns the captured log output from the miner process. +func (b *BaseMiner) GetLogs() []string { + b.mu.RLock() + logBuffer := b.LogBuffer + b.mu.RUnlock() + + if logBuffer == nil { + return []string{} + } + return logBuffer.GetLines() +} + +// ReduceHashrateHistory aggregates and trims hashrate data. +func (b *BaseMiner) ReduceHashrateHistory(now time.Time) { + b.mu.Lock() + defer b.mu.Unlock() + + if !b.LastLowResAggregation.IsZero() && now.Sub(b.LastLowResAggregation) < LowResolutionInterval { + return + } + + var pointsToAggregate []HashratePoint + var newHighResHistory []HashratePoint + cutoff := now.Add(-HighResolutionDuration) + + for _, p := range b.HashrateHistory { + if p.Timestamp.Before(cutoff) { + pointsToAggregate = append(pointsToAggregate, p) + } else { + newHighResHistory = append(newHighResHistory, p) + } + } + // Force reallocation if significantly oversized to free memory + if cap(b.HashrateHistory) > 1000 && len(newHighResHistory) < cap(b.HashrateHistory)/2 { + trimmed := make([]HashratePoint, len(newHighResHistory)) + copy(trimmed, newHighResHistory) + b.HashrateHistory = trimmed + } else { + b.HashrateHistory = newHighResHistory + } + + if len(pointsToAggregate) == 0 { + b.LastLowResAggregation = now + return + } + + minuteGroups := make(map[time.Time][]int) + for _, p := range pointsToAggregate { + minute := p.Timestamp.Truncate(LowResolutionInterval) + minuteGroups[minute] = append(minuteGroups[minute], p.Hashrate) + } + + var newLowResPoints []HashratePoint + for minute, hashrates := range minuteGroups { + if len(hashrates) > 0 { + totalHashrate := 0 + for _, hr := range hashrates { + totalHashrate += hr + } + avgHashrate := totalHashrate / len(hashrates) + newLowResPoints = append(newLowResPoints, HashratePoint{Timestamp: minute, Hashrate: avgHashrate}) + } + } + + sort.Slice(newLowResPoints, func(i, j int) bool { + return newLowResPoints[i].Timestamp.Before(newLowResPoints[j].Timestamp) + }) + + b.LowResHashrateHistory = append(b.LowResHashrateHistory, newLowResPoints...) + + lowResCutoff := now.Add(-LowResHistoryRetention) + firstValidLowResIndex := 0 + for i, p := range b.LowResHashrateHistory { + if p.Timestamp.After(lowResCutoff) || p.Timestamp.Equal(lowResCutoff) { + firstValidLowResIndex = i + break + } + if i == len(b.LowResHashrateHistory)-1 { + firstValidLowResIndex = len(b.LowResHashrateHistory) + } + } + + // Force reallocation if significantly oversized to free memory + newLowResLen := len(b.LowResHashrateHistory) - firstValidLowResIndex + if cap(b.LowResHashrateHistory) > 1000 && newLowResLen < cap(b.LowResHashrateHistory)/2 { + trimmed := make([]HashratePoint, newLowResLen) + copy(trimmed, b.LowResHashrateHistory[firstValidLowResIndex:]) + b.LowResHashrateHistory = trimmed + } else { + b.LowResHashrateHistory = b.LowResHashrateHistory[firstValidLowResIndex:] + } + b.LastLowResAggregation = now +} + +// unzip extracts a zip archive. +func (b *BaseMiner) unzip(src, dest string) error { + r, err := zip.OpenReader(src) + if err != nil { + return err + } + defer r.Close() + + for _, f := range r.File { + fpath := filepath.Join(dest, f.Name) + if !strings.HasPrefix(fpath, filepath.Clean(dest)+string(os.PathSeparator)) { + return fmt.Errorf("%s: illegal file path", fpath) + } + if f.FileInfo().IsDir() { + if err := os.MkdirAll(fpath, os.ModePerm); err != nil { + return fmt.Errorf("failed to create directory %s: %w", fpath, err) + } + continue + } + + if err = os.MkdirAll(filepath.Dir(fpath), os.ModePerm); err != nil { + return err + } + outFile, err := os.OpenFile(fpath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, f.Mode()) + if err != nil { + return err + } + rc, err := f.Open() + if err != nil { + outFile.Close() + return err + } + _, err = io.Copy(outFile, rc) + outFile.Close() + rc.Close() + if err != nil { + return err + } + } + return nil +} + +// untar extracts a tar.gz archive. +func (b *BaseMiner) untar(src, dest string) error { + file, err := os.Open(src) + if err != nil { + return err + } + defer file.Close() + + gzr, err := gzip.NewReader(file) + if err != nil { + return err + } + defer gzr.Close() + + tr := tar.NewReader(gzr) + + for { + header, err := tr.Next() + if err == io.EOF { + return nil + } + if err != nil { + return err + } + + target := filepath.Join(dest, header.Name) + if !strings.HasPrefix(target, filepath.Clean(dest)+string(os.PathSeparator)) { + return fmt.Errorf("%s: illegal file path in archive", header.Name) + } + + switch header.Typeflag { + case tar.TypeDir: + if err := os.MkdirAll(target, 0755); err != nil { + return err + } + case tar.TypeReg: + if err := os.MkdirAll(filepath.Dir(target), 0755); err != nil { + return err + } + f, err := os.OpenFile(target, os.O_CREATE|os.O_RDWR|os.O_TRUNC, os.FileMode(header.Mode)) + if err != nil { + return err + } + if _, err := io.Copy(f, tr); err != nil { + f.Close() + return err + } + f.Close() + } + } +} diff --git a/mining/miner_factory.go b/mining/miner_factory.go new file mode 100644 index 0000000..2f90cb2 --- /dev/null +++ b/mining/miner_factory.go @@ -0,0 +1,139 @@ +package mining + +import ( + "fmt" + "strings" + "sync" +) + +// MinerConstructor is a function that creates a new miner instance +type MinerConstructor func() Miner + +// MinerFactory handles miner instantiation and registration +type MinerFactory struct { + mu sync.RWMutex + constructors map[string]MinerConstructor + aliases map[string]string // maps aliases to canonical names +} + +// globalFactory is the default factory instance +var globalFactory = NewMinerFactory() + +// NewMinerFactory creates a new MinerFactory with default miners registered +func NewMinerFactory() *MinerFactory { + f := &MinerFactory{ + constructors: make(map[string]MinerConstructor), + aliases: make(map[string]string), + } + f.registerDefaults() + return f +} + +// registerDefaults registers all built-in miners +func (f *MinerFactory) registerDefaults() { + // XMRig miner (CPU/GPU RandomX, Cryptonight, etc.) + f.Register("xmrig", func() Miner { return NewXMRigMiner() }) + + // TT-Miner (GPU Kawpow, etc.) + f.Register("tt-miner", func() Miner { return NewTTMiner() }) + f.RegisterAlias("ttminer", "tt-miner") + + // Simulated miner for testing and development + f.Register(MinerTypeSimulated, func() Miner { + return NewSimulatedMiner(SimulatedMinerConfig{ + Name: "simulated-miner", + Algorithm: "rx/0", + BaseHashrate: 1000, + Variance: 0.1, + }) + }) +} + +// Register adds a miner constructor to the factory +func (f *MinerFactory) Register(name string, constructor MinerConstructor) { + f.mu.Lock() + defer f.mu.Unlock() + f.constructors[strings.ToLower(name)] = constructor +} + +// RegisterAlias adds an alias for an existing miner type +func (f *MinerFactory) RegisterAlias(alias, canonicalName string) { + f.mu.Lock() + defer f.mu.Unlock() + f.aliases[strings.ToLower(alias)] = strings.ToLower(canonicalName) +} + +// Create instantiates a miner of the specified type +func (f *MinerFactory) Create(minerType string) (Miner, error) { + f.mu.RLock() + defer f.mu.RUnlock() + + name := strings.ToLower(minerType) + + // Check for alias first + if canonical, ok := f.aliases[name]; ok { + name = canonical + } + + constructor, ok := f.constructors[name] + if !ok { + return nil, fmt.Errorf("unsupported miner type: %s", minerType) + } + + return constructor(), nil +} + +// IsSupported checks if a miner type is registered +func (f *MinerFactory) IsSupported(minerType string) bool { + f.mu.RLock() + defer f.mu.RUnlock() + + name := strings.ToLower(minerType) + + // Check alias + if canonical, ok := f.aliases[name]; ok { + name = canonical + } + + _, ok := f.constructors[name] + return ok +} + +// ListTypes returns all registered miner type names (excluding aliases) +func (f *MinerFactory) ListTypes() []string { + f.mu.RLock() + defer f.mu.RUnlock() + + types := make([]string, 0, len(f.constructors)) + for name := range f.constructors { + types = append(types, name) + } + return types +} + +// --- Global factory functions for convenience --- + +// CreateMiner creates a miner using the global factory +func CreateMiner(minerType string) (Miner, error) { + return globalFactory.Create(minerType) +} + +// IsMinerSupported checks if a miner type is supported using the global factory +func IsMinerSupported(minerType string) bool { + return globalFactory.IsSupported(minerType) +} + +// ListMinerTypes returns all registered miner types from the global factory +func ListMinerTypes() []string { + return globalFactory.ListTypes() +} + +// RegisterMinerType adds a miner constructor to the global factory +func RegisterMinerType(name string, constructor MinerConstructor) { + globalFactory.Register(name, constructor) +} + +// RegisterMinerAlias adds an alias to the global factory +func RegisterMinerAlias(alias, canonicalName string) { + globalFactory.RegisterAlias(alias, canonicalName) +} diff --git a/mining/miner_factory_test.go b/mining/miner_factory_test.go new file mode 100644 index 0000000..c4d7da6 --- /dev/null +++ b/mining/miner_factory_test.go @@ -0,0 +1,155 @@ +package mining + +import ( + "testing" +) + +func TestMinerFactory_Create(t *testing.T) { + factory := NewMinerFactory() + + tests := []struct { + name string + minerType string + wantErr bool + }{ + {"xmrig lowercase", "xmrig", false}, + {"xmrig uppercase", "XMRIG", false}, + {"xmrig mixed case", "XmRig", false}, + {"tt-miner", "tt-miner", false}, + {"ttminer alias", "ttminer", false}, + {"unknown type", "unknown", true}, + {"empty type", "", true}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + miner, err := factory.Create(tt.minerType) + if tt.wantErr { + if err == nil { + t.Errorf("Create(%q) expected error, got nil", tt.minerType) + } + } else { + if err != nil { + t.Errorf("Create(%q) unexpected error: %v", tt.minerType, err) + } + if miner == nil { + t.Errorf("Create(%q) returned nil miner", tt.minerType) + } + } + }) + } +} + +func TestMinerFactory_IsSupported(t *testing.T) { + factory := NewMinerFactory() + + tests := []struct { + minerType string + want bool + }{ + {"xmrig", true}, + {"tt-miner", true}, + {"ttminer", true}, // alias + {"unknown", false}, + {"", false}, + } + + for _, tt := range tests { + t.Run(tt.minerType, func(t *testing.T) { + if got := factory.IsSupported(tt.minerType); got != tt.want { + t.Errorf("IsSupported(%q) = %v, want %v", tt.minerType, got, tt.want) + } + }) + } +} + +func TestMinerFactory_ListTypes(t *testing.T) { + factory := NewMinerFactory() + + types := factory.ListTypes() + if len(types) < 2 { + t.Errorf("ListTypes() returned %d types, expected at least 2", len(types)) + } + + // Check that expected types are present + typeMap := make(map[string]bool) + for _, typ := range types { + typeMap[typ] = true + } + + expectedTypes := []string{"xmrig", "tt-miner"} + for _, expected := range expectedTypes { + if !typeMap[expected] { + t.Errorf("ListTypes() missing expected type %q", expected) + } + } +} + +func TestMinerFactory_Register(t *testing.T) { + factory := NewMinerFactory() + + // Register a custom miner type + called := false + factory.Register("custom-miner", func() Miner { + called = true + return NewXMRigMiner() // Return something valid for testing + }) + + if !factory.IsSupported("custom-miner") { + t.Error("custom-miner should be supported after registration") + } + + _, err := factory.Create("custom-miner") + if err != nil { + t.Errorf("Create custom-miner failed: %v", err) + } + if !called { + t.Error("custom constructor was not called") + } +} + +func TestMinerFactory_RegisterAlias(t *testing.T) { + factory := NewMinerFactory() + + // Register an alias for xmrig + factory.RegisterAlias("x", "xmrig") + + if !factory.IsSupported("x") { + t.Error("alias 'x' should be supported") + } + + miner, err := factory.Create("x") + if err != nil { + t.Errorf("Create with alias failed: %v", err) + } + if miner == nil { + t.Error("Create with alias returned nil miner") + } +} + +func TestGlobalFactory_CreateMiner(t *testing.T) { + // Test global convenience functions + miner, err := CreateMiner("xmrig") + if err != nil { + t.Errorf("CreateMiner failed: %v", err) + } + if miner == nil { + t.Error("CreateMiner returned nil") + } +} + +func TestGlobalFactory_IsMinerSupported(t *testing.T) { + if !IsMinerSupported("xmrig") { + t.Error("xmrig should be supported") + } + if IsMinerSupported("nosuchminer") { + t.Error("nosuchminer should not be supported") + } +} + +func TestGlobalFactory_ListMinerTypes(t *testing.T) { + types := ListMinerTypes() + if len(types) < 2 { + t.Errorf("ListMinerTypes() returned %d types, expected at least 2", len(types)) + } +} diff --git a/mining/mining.go b/mining/mining.go new file mode 100644 index 0000000..f322a0b --- /dev/null +++ b/mining/mining.go @@ -0,0 +1,355 @@ +package mining + +import ( + "context" + "fmt" + "strings" + "time" +) + +const ( + HighResolutionDuration = 5 * time.Minute + HighResolutionInterval = 10 * time.Second + LowResolutionInterval = 1 * time.Minute + LowResHistoryRetention = 24 * time.Hour +) + +// Miner defines the standard interface for a cryptocurrency miner. +// The interface is logically grouped into focused capabilities: +// +// Lifecycle - Installation and process management: +// - Install, Uninstall, Start, Stop +// +// Stats - Performance metrics collection: +// - GetStats +// +// Info - Miner identification and installation details: +// - GetType, GetName, GetPath, GetBinaryPath, CheckInstallation, GetLatestVersion +// +// History - Hashrate history management: +// - GetHashrateHistory, AddHashratePoint, ReduceHashrateHistory +// +// IO - Interactive input/output: +// - GetLogs, WriteStdin +type Miner interface { + // Lifecycle operations + Install() error + Uninstall() error + Start(config *Config) error + Stop() error + + // Stats operations + GetStats(ctx context.Context) (*PerformanceMetrics, error) + + // Info operations + GetType() string // Returns miner type identifier (e.g., "xmrig", "tt-miner") + GetName() string + GetPath() string + GetBinaryPath() string + CheckInstallation() (*InstallationDetails, error) + GetLatestVersion() (string, error) + + // History operations + GetHashrateHistory() []HashratePoint + AddHashratePoint(point HashratePoint) + ReduceHashrateHistory(now time.Time) + + // IO operations + GetLogs() []string + WriteStdin(input string) error +} + +// InstallationDetails contains information about an installed miner. +type InstallationDetails struct { + IsInstalled bool `json:"is_installed"` + Version string `json:"version"` + Path string `json:"path"` + MinerBinary string `json:"miner_binary"` + ConfigPath string `json:"config_path,omitempty"` // Add path to the miner-specific config +} + +// SystemInfo provides general system and miner installation information. +type SystemInfo struct { + Timestamp time.Time `json:"timestamp"` + OS string `json:"os"` + Architecture string `json:"architecture"` + GoVersion string `json:"go_version"` + AvailableCPUCores int `json:"available_cpu_cores"` + TotalSystemRAMGB float64 `json:"total_system_ram_gb"` + InstalledMinersInfo []*InstallationDetails `json:"installed_miners_info"` +} + +// Config represents the configuration for a miner. +type Config struct { + Miner string `json:"miner"` + Pool string `json:"pool"` + Wallet string `json:"wallet"` + Threads int `json:"threads"` + TLS bool `json:"tls"` + HugePages bool `json:"hugePages"` + Algo string `json:"algo,omitempty"` + Coin string `json:"coin,omitempty"` + Password string `json:"password,omitempty"` + UserPass string `json:"userPass,omitempty"` + Proxy string `json:"proxy,omitempty"` + Keepalive bool `json:"keepalive,omitempty"` + Nicehash bool `json:"nicehash,omitempty"` + RigID string `json:"rigId,omitempty"` + TLSSingerprint string `json:"tlsFingerprint,omitempty"` + Retries int `json:"retries,omitempty"` + RetryPause int `json:"retryPause,omitempty"` + UserAgent string `json:"userAgent,omitempty"` + DonateLevel int `json:"donateLevel,omitempty"` + DonateOverProxy bool `json:"donateOverProxy,omitempty"` + NoCPU bool `json:"noCpu,omitempty"` + CPUAffinity string `json:"cpuAffinity,omitempty"` + AV int `json:"av,omitempty"` + CPUPriority int `json:"cpuPriority,omitempty"` + CPUMaxThreadsHint int `json:"cpuMaxThreadsHint,omitempty"` + CPUMemoryPool int `json:"cpuMemoryPool,omitempty"` + CPUNoYield bool `json:"cpuNoYield,omitempty"` + HugepageSize int `json:"hugepageSize,omitempty"` + HugePagesJIT bool `json:"hugePagesJIT,omitempty"` + ASM string `json:"asm,omitempty"` + Argon2Impl string `json:"argon2Impl,omitempty"` + RandomXInit int `json:"randomXInit,omitempty"` + RandomXNoNUMA bool `json:"randomXNoNuma,omitempty"` + RandomXMode string `json:"randomXMode,omitempty"` + RandomX1GBPages bool `json:"randomX1GBPages,omitempty"` + RandomXWrmsr string `json:"randomXWrmsr,omitempty"` + RandomXNoRdmsr bool `json:"randomXNoRdmsr,omitempty"` + RandomXCacheQoS bool `json:"randomXCacheQoS,omitempty"` + APIWorkerID string `json:"apiWorkerId,omitempty"` + APIID string `json:"apiId,omitempty"` + HTTPHost string `json:"httpHost,omitempty"` + HTTPPort int `json:"httpPort,omitempty"` + HTTPAccessToken string `json:"httpAccessToken,omitempty"` + HTTPNoRestricted bool `json:"httpNoRestricted,omitempty"` + Syslog bool `json:"syslog,omitempty"` + LogFile string `json:"logFile,omitempty"` + PrintTime int `json:"printTime,omitempty"` + HealthPrintTime int `json:"healthPrintTime,omitempty"` + NoColor bool `json:"noColor,omitempty"` + Verbose bool `json:"verbose,omitempty"` + LogOutput bool `json:"logOutput,omitempty"` + Background bool `json:"background,omitempty"` + Title string `json:"title,omitempty"` + NoTitle bool `json:"noTitle,omitempty"` + PauseOnBattery bool `json:"pauseOnBattery,omitempty"` + PauseOnActive int `json:"pauseOnActive,omitempty"` + Stress bool `json:"stress,omitempty"` + Bench string `json:"bench,omitempty"` + Submit bool `json:"submit,omitempty"` + Verify string `json:"verify,omitempty"` + Seed string `json:"seed,omitempty"` + Hash string `json:"hash,omitempty"` + NoDMI bool `json:"noDMI,omitempty"` + // GPU-specific options (for XMRig dual CPU+GPU mining) + GPUEnabled bool `json:"gpuEnabled,omitempty"` // Enable GPU mining + GPUPool string `json:"gpuPool,omitempty"` // Separate pool for GPU (can differ from CPU) + GPUWallet string `json:"gpuWallet,omitempty"` // Wallet for GPU pool (defaults to main Wallet) + GPUAlgo string `json:"gpuAlgo,omitempty"` // Algorithm for GPU (e.g., "kawpow", "ethash") + GPUPassword string `json:"gpuPassword,omitempty"` // Password for GPU pool + GPUIntensity int `json:"gpuIntensity,omitempty"` // GPU mining intensity (0-100) + GPUThreads int `json:"gpuThreads,omitempty"` // GPU threads per card + Devices string `json:"devices,omitempty"` // GPU device selection (e.g., "0,1,2") + OpenCL bool `json:"opencl,omitempty"` // Enable OpenCL (AMD/Intel GPUs) + CUDA bool `json:"cuda,omitempty"` // Enable CUDA (NVIDIA GPUs) + Intensity int `json:"intensity,omitempty"` // Mining intensity for GPU miners + CLIArgs string `json:"cliArgs,omitempty"` // Additional CLI arguments +} + +// Validate checks the Config for common errors and security issues. +// Returns nil if valid, otherwise returns a descriptive error. +func (c *Config) Validate() error { + // Pool URL validation + if c.Pool != "" { + // Block shell metacharacters in pool URL + if containsShellChars(c.Pool) { + return fmt.Errorf("pool URL contains invalid characters") + } + } + + // Wallet validation (basic alphanumeric + special chars allowed in addresses) + if c.Wallet != "" { + if containsShellChars(c.Wallet) { + return fmt.Errorf("wallet address contains invalid characters") + } + // Most wallet addresses are 40-128 chars + if len(c.Wallet) > 256 { + return fmt.Errorf("wallet address too long (max 256 chars)") + } + } + + // Thread count validation + if c.Threads < 0 { + return fmt.Errorf("threads cannot be negative") + } + if c.Threads > 1024 { + return fmt.Errorf("threads value too high (max 1024)") + } + + // Algorithm validation (alphanumeric, dash, slash) + if c.Algo != "" { + if !isValidAlgo(c.Algo) { + return fmt.Errorf("algorithm name contains invalid characters") + } + } + + // Intensity validation + if c.Intensity < 0 || c.Intensity > 100 { + return fmt.Errorf("intensity must be between 0 and 100") + } + if c.GPUIntensity < 0 || c.GPUIntensity > 100 { + return fmt.Errorf("GPU intensity must be between 0 and 100") + } + + // Donate level validation + if c.DonateLevel < 0 || c.DonateLevel > 100 { + return fmt.Errorf("donate level must be between 0 and 100") + } + + // CLIArgs validation - check for shell metacharacters + if c.CLIArgs != "" { + if containsShellChars(c.CLIArgs) { + return fmt.Errorf("CLI arguments contain invalid characters") + } + // Limit length to prevent abuse + if len(c.CLIArgs) > 1024 { + return fmt.Errorf("CLI arguments too long (max 1024 chars)") + } + } + + return nil +} + +// containsShellChars checks for shell metacharacters that could enable injection +func containsShellChars(s string) bool { + dangerous := []string{";", "|", "&", "`", "$", "(", ")", "{", "}", "<", ">", "\n", "\r", "\\", "'", "\"", "!"} + for _, d := range dangerous { + if strings.Contains(s, d) { + return true + } + } + return false +} + +// isValidAlgo checks if an algorithm name contains only valid characters +func isValidAlgo(algo string) bool { + for _, r := range algo { + if !((r >= 'a' && r <= 'z') || (r >= 'A' && r <= 'Z') || (r >= '0' && r <= '9') || r == '-' || r == '/' || r == '_') { + return false + } + } + return true +} + +// PerformanceMetrics represents the performance metrics for a miner. +type PerformanceMetrics struct { + Hashrate int `json:"hashrate"` + Shares int `json:"shares"` + Rejected int `json:"rejected"` + Uptime int `json:"uptime"` + LastShare int64 `json:"lastShare"` + Algorithm string `json:"algorithm"` + AvgDifficulty int `json:"avgDifficulty"` // Average difficulty per accepted share (HashesTotal/SharesGood) + DiffCurrent int `json:"diffCurrent"` // Current job difficulty from pool + ExtraData map[string]interface{} `json:"extraData,omitempty"` +} + +// HashratePoint represents a single hashrate measurement at a specific time. +type HashratePoint struct { + Timestamp time.Time `json:"timestamp"` + Hashrate int `json:"hashrate"` +} + +// API represents the miner's API configuration. +type API struct { + Enabled bool `json:"enabled"` + ListenHost string `json:"listenHost"` + ListenPort int `json:"listenPort"` +} + +// XMRigSummary represents the full JSON response from the XMRig API. +type XMRigSummary struct { + ID string `json:"id"` + WorkerID string `json:"worker_id"` + Uptime int `json:"uptime"` + Restricted bool `json:"restricted"` + Resources struct { + Memory struct { + Free int64 `json:"free"` + Total int64 `json:"total"` + ResidentSetMemory int64 `json:"resident_set_memory"` + } `json:"memory"` + LoadAverage []float64 `json:"load_average"` + HardwareConcurrency int `json:"hardware_concurrency"` + } `json:"resources"` + Features []string `json:"features"` + Results struct { + DiffCurrent int `json:"diff_current"` + SharesGood int `json:"shares_good"` + SharesTotal int `json:"shares_total"` + AvgTime int `json:"avg_time"` + AvgTimeMS int `json:"avg_time_ms"` + HashesTotal int `json:"hashes_total"` + Best []int `json:"best"` + } `json:"results"` + Algo string `json:"algo"` + Connection struct { + Pool string `json:"pool"` + IP string `json:"ip"` + Uptime int `json:"uptime"` + UptimeMS int `json:"uptime_ms"` + Ping int `json:"ping"` + Failures int `json:"failures"` + TLS string `json:"tls"` + TLSFingerprint string `json:"tls-fingerprint"` + Algo string `json:"algo"` + Diff int `json:"diff"` + Accepted int `json:"accepted"` + Rejected int `json:"rejected"` + AvgTime int `json:"avg_time"` + AvgTimeMS int `json:"avg_time_ms"` + HashesTotal int `json:"hashes_total"` + } `json:"connection"` + Version string `json:"version"` + Kind string `json:"kind"` + UA string `json:"ua"` + CPU struct { + Brand string `json:"brand"` + Family int `json:"family"` + Model int `json:"model"` + Stepping int `json:"stepping"` + ProcInfo int `json:"proc_info"` + AES bool `json:"aes"` + AVX2 bool `json:"avx2"` + X64 bool `json:"x64"` + Is64Bit bool `json:"64_bit"` + L2 int `json:"l2"` + L3 int `json:"l3"` + Cores int `json:"cores"` + Threads int `json:"threads"` + Packages int `json:"packages"` + Nodes int `json:"nodes"` + Backend string `json:"backend"` + MSR string `json:"msr"` + Assembly string `json:"assembly"` + Arch string `json:"arch"` + Flags []string `json:"flags"` + } `json:"cpu"` + DonateLevel int `json:"donate_level"` + Paused bool `json:"paused"` + Algorithms []string `json:"algorithms"` + Hashrate struct { + Total []float64 `json:"total"` + Highest float64 `json:"highest"` + } `json:"hashrate"` + Hugepages []int `json:"hugepages"` +} + +// AvailableMiner represents a miner that is available for use. +type AvailableMiner struct { + Name string `json:"name"` + Description string `json:"description"` +} diff --git a/mining/mining_profile.go b/mining/mining_profile.go new file mode 100644 index 0000000..1c0c906 --- /dev/null +++ b/mining/mining_profile.go @@ -0,0 +1,36 @@ +package mining + +import ( + "errors" +) + +// RawConfig is a raw encoded JSON value. +// It implements Marshaler and Unmarshaler and can be used to delay JSON decoding or precompute a JSON encoding. +// We define it as []byte (like json.RawMessage) to avoid swagger parsing issues with the json package. +type RawConfig []byte + +// MiningProfile represents a saved configuration for running a specific miner. +// It decouples the UI from the underlying miner's specific config structure. +type MiningProfile struct { + ID string `json:"id"` + Name string `json:"name"` + MinerType string `json:"minerType"` // e.g., "xmrig", "ttminer" + Config RawConfig `json:"config" swaggertype:"object"` // The raw JSON config for the specific miner +} + +// MarshalJSON returns m as the JSON encoding of m. +func (m RawConfig) MarshalJSON() ([]byte, error) { + if m == nil { + return []byte("null"), nil + } + return m, nil +} + +// UnmarshalJSON sets *m to a copy of data. +func (m *RawConfig) UnmarshalJSON(data []byte) error { + if m == nil { + return errors.New("RawConfig: UnmarshalJSON on nil pointer") + } + *m = append((*m)[0:0], data...) + return nil +} diff --git a/mining/mining_test.go b/mining/mining_test.go new file mode 100644 index 0000000..4a3ce9e --- /dev/null +++ b/mining/mining_test.go @@ -0,0 +1,60 @@ +package mining + +import ( + "testing" +) + +func TestNewManager(t *testing.T) { + manager := NewManager() + defer manager.Stop() + + if manager == nil { + t.Fatal("NewManager returned nil") + } + if manager.miners == nil { + t.Error("Manager miners map is nil") + } +} + +func TestStartAndStopMiner(t *testing.T) { + t.Skip("Skipping test that attempts to run miner process") +} + +func TestGetNonExistentMiner(t *testing.T) { + manager := NewManager() + defer manager.Stop() + + _, err := manager.GetMiner("non-existent") + if err == nil { + t.Error("Expected error for getting non-existent miner") + } +} + +func TestListMiners(t *testing.T) { + manager := NewManager() + defer manager.Stop() + + // ListMiners should return a valid slice (may include autostarted miners) + miners := manager.ListMiners() + if miners == nil { + t.Error("ListMiners returned nil") + } + // Note: count may be > 0 if autostart is configured +} + +func TestListAvailableMiners(t *testing.T) { + manager := NewManager() + defer manager.Stop() + + miners := manager.ListAvailableMiners() + if len(miners) == 0 { + t.Error("Expected at least one available miner") + } +} + +func TestGetVersion(t *testing.T) { + version := GetVersion() + if version == "" { + t.Error("Version is empty") + } +} diff --git a/mining/node_service.go b/mining/node_service.go new file mode 100644 index 0000000..72b8578 --- /dev/null +++ b/mining/node_service.go @@ -0,0 +1,29 @@ +package mining + +import ( + "fmt" + + "github.com/gin-gonic/gin" +) + +// NodeService handles P2P node-related API endpoints. +// This is a stub — the full implementation lives in core/go-p2p. +// When P2P is needed, inject a concrete NodeService via the container. +type NodeService struct{} + +// NewNodeService returns an error because P2P node support has moved to core/go-p2p. +// Callers (Container, Service) handle nil NodeService gracefully. +func NewNodeService() (*NodeService, error) { + return nil, fmt.Errorf("P2P node service not available (moved to core/go-p2p)") +} + +// SetupRoutes is a no-op stub. +func (ns *NodeService) SetupRoutes(router *gin.RouterGroup) {} + +// StartTransport is a no-op stub. +func (ns *NodeService) StartTransport() error { + return fmt.Errorf("P2P transport not available") +} + +// StopTransport is a no-op stub. +func (ns *NodeService) StopTransport() error { return nil } diff --git a/mining/profile_manager.go b/mining/profile_manager.go new file mode 100644 index 0000000..b7e33dd --- /dev/null +++ b/mining/profile_manager.go @@ -0,0 +1,164 @@ +package mining + +import ( + "encoding/json" + "fmt" + "os" + "path/filepath" + "sync" + + "github.com/adrg/xdg" + "github.com/google/uuid" +) + +const profileConfigFileName = "mining_profiles.json" + +// ProfileManager handles CRUD operations for MiningProfiles. +type ProfileManager struct { + mu sync.RWMutex + profiles map[string]*MiningProfile + configPath string +} + +// NewProfileManager creates and initializes a new ProfileManager. +func NewProfileManager() (*ProfileManager, error) { + configPath, err := xdg.ConfigFile(filepath.Join("lethean-desktop", profileConfigFileName)) + if err != nil { + return nil, fmt.Errorf("could not resolve config path: %w", err) + } + + pm := &ProfileManager{ + profiles: make(map[string]*MiningProfile), + configPath: configPath, + } + + if err := pm.loadProfiles(); err != nil { + // If the file doesn't exist, that's fine, but any other error is a problem. + if !os.IsNotExist(err) { + return nil, fmt.Errorf("could not load profiles: %w", err) + } + } + + return pm, nil +} + +// loadProfiles reads the profiles from the JSON file into memory. +func (pm *ProfileManager) loadProfiles() error { + pm.mu.Lock() + defer pm.mu.Unlock() + + data, err := os.ReadFile(pm.configPath) + if err != nil { + return err + } + + var profiles []*MiningProfile + if err := json.Unmarshal(data, &profiles); err != nil { + return err + } + + pm.profiles = make(map[string]*MiningProfile) + for _, p := range profiles { + pm.profiles[p.ID] = p + } + + return nil +} + +// saveProfiles writes the current profiles from memory to the JSON file. +// This is an internal method and assumes the caller holds the appropriate lock. +// Uses atomic write pattern: write to temp file, sync, then rename. +func (pm *ProfileManager) saveProfiles() error { + profileList := make([]*MiningProfile, 0, len(pm.profiles)) + for _, p := range pm.profiles { + profileList = append(profileList, p) + } + + data, err := json.MarshalIndent(profileList, "", " ") + if err != nil { + return err + } + + return AtomicWriteFile(pm.configPath, data, 0600) +} + +// CreateProfile adds a new profile and saves it. +func (pm *ProfileManager) CreateProfile(profile *MiningProfile) (*MiningProfile, error) { + pm.mu.Lock() + defer pm.mu.Unlock() + + profile.ID = uuid.New().String() + pm.profiles[profile.ID] = profile + + if err := pm.saveProfiles(); err != nil { + // Rollback + delete(pm.profiles, profile.ID) + return nil, err + } + + return profile, nil +} + +// GetProfile retrieves a profile by its ID. +func (pm *ProfileManager) GetProfile(id string) (*MiningProfile, bool) { + pm.mu.RLock() + defer pm.mu.RUnlock() + profile, exists := pm.profiles[id] + return profile, exists +} + +// GetAllProfiles returns a list of all profiles. +func (pm *ProfileManager) GetAllProfiles() []*MiningProfile { + pm.mu.RLock() + defer pm.mu.RUnlock() + + profileList := make([]*MiningProfile, 0, len(pm.profiles)) + for _, p := range pm.profiles { + profileList = append(profileList, p) + } + return profileList +} + +// UpdateProfile modifies an existing profile. +func (pm *ProfileManager) UpdateProfile(profile *MiningProfile) error { + pm.mu.Lock() + defer pm.mu.Unlock() + + oldProfile, exists := pm.profiles[profile.ID] + if !exists { + return fmt.Errorf("profile with ID %s not found", profile.ID) + } + + // Update in-memory state + pm.profiles[profile.ID] = profile + + // Save to disk - rollback if save fails + if err := pm.saveProfiles(); err != nil { + // Restore old profile on save failure + pm.profiles[profile.ID] = oldProfile + return fmt.Errorf("failed to save profile: %w", err) + } + + return nil +} + +// DeleteProfile removes a profile by its ID. +func (pm *ProfileManager) DeleteProfile(id string) error { + pm.mu.Lock() + defer pm.mu.Unlock() + + profile, exists := pm.profiles[id] + if !exists { + return fmt.Errorf("profile with ID %s not found", id) + } + delete(pm.profiles, id) + + // Save to disk - rollback if save fails + if err := pm.saveProfiles(); err != nil { + // Restore profile on save failure + pm.profiles[id] = profile + return fmt.Errorf("failed to delete profile: %w", err) + } + + return nil +} diff --git a/mining/profile_manager_test.go b/mining/profile_manager_test.go new file mode 100644 index 0000000..3716430 --- /dev/null +++ b/mining/profile_manager_test.go @@ -0,0 +1,365 @@ +package mining + +import ( + "encoding/json" + "os" + "path/filepath" + "sync" + "testing" +) + +// setupTestProfileManager creates a ProfileManager with a temp config path. +func setupTestProfileManager(t *testing.T) (*ProfileManager, func()) { + tmpDir, err := os.MkdirTemp("", "profile-manager-test") + if err != nil { + t.Fatalf("failed to create temp dir: %v", err) + } + + configPath := filepath.Join(tmpDir, "mining_profiles.json") + + pm := &ProfileManager{ + profiles: make(map[string]*MiningProfile), + configPath: configPath, + } + + cleanup := func() { + os.RemoveAll(tmpDir) + } + + return pm, cleanup +} + +func TestProfileManagerCreate(t *testing.T) { + pm, cleanup := setupTestProfileManager(t) + defer cleanup() + + profile := &MiningProfile{ + Name: "Test Profile", + MinerType: "xmrig", + Config: RawConfig(`{"pool": "test.pool.com:3333"}`), + } + + created, err := pm.CreateProfile(profile) + if err != nil { + t.Fatalf("failed to create profile: %v", err) + } + + if created.ID == "" { + t.Error("created profile should have an ID") + } + + if created.Name != "Test Profile" { + t.Errorf("expected name 'Test Profile', got '%s'", created.Name) + } + + // Verify it's stored + retrieved, exists := pm.GetProfile(created.ID) + if !exists { + t.Error("profile should exist after creation") + } + + if retrieved.Name != created.Name { + t.Errorf("retrieved name doesn't match: expected '%s', got '%s'", created.Name, retrieved.Name) + } +} + +func TestProfileManagerGet(t *testing.T) { + pm, cleanup := setupTestProfileManager(t) + defer cleanup() + + // Get non-existent profile + _, exists := pm.GetProfile("non-existent-id") + if exists { + t.Error("GetProfile should return false for non-existent ID") + } + + // Create and get + profile := &MiningProfile{ + Name: "Get Test", + MinerType: "xmrig", + } + created, _ := pm.CreateProfile(profile) + + retrieved, exists := pm.GetProfile(created.ID) + if !exists { + t.Error("GetProfile should return true for existing ID") + } + + if retrieved.ID != created.ID { + t.Error("GetProfile returned wrong profile") + } +} + +func TestProfileManagerGetAll(t *testing.T) { + pm, cleanup := setupTestProfileManager(t) + defer cleanup() + + // Empty list initially + profiles := pm.GetAllProfiles() + if len(profiles) != 0 { + t.Errorf("expected 0 profiles initially, got %d", len(profiles)) + } + + // Create multiple profiles + for i := 0; i < 3; i++ { + pm.CreateProfile(&MiningProfile{ + Name: "Profile", + MinerType: "xmrig", + }) + } + + profiles = pm.GetAllProfiles() + if len(profiles) != 3 { + t.Errorf("expected 3 profiles, got %d", len(profiles)) + } +} + +func TestProfileManagerUpdate(t *testing.T) { + pm, cleanup := setupTestProfileManager(t) + defer cleanup() + + // Update non-existent profile + err := pm.UpdateProfile(&MiningProfile{ID: "non-existent"}) + if err == nil { + t.Error("UpdateProfile should fail for non-existent profile") + } + + // Create profile + profile := &MiningProfile{ + Name: "Original Name", + MinerType: "xmrig", + } + created, _ := pm.CreateProfile(profile) + + // Update it + created.Name = "Updated Name" + created.MinerType = "ttminer" + err = pm.UpdateProfile(created) + if err != nil { + t.Fatalf("failed to update profile: %v", err) + } + + // Verify update + retrieved, _ := pm.GetProfile(created.ID) + if retrieved.Name != "Updated Name" { + t.Errorf("expected name 'Updated Name', got '%s'", retrieved.Name) + } + if retrieved.MinerType != "ttminer" { + t.Errorf("expected miner type 'ttminer', got '%s'", retrieved.MinerType) + } +} + +func TestProfileManagerDelete(t *testing.T) { + pm, cleanup := setupTestProfileManager(t) + defer cleanup() + + // Delete non-existent profile + err := pm.DeleteProfile("non-existent") + if err == nil { + t.Error("DeleteProfile should fail for non-existent profile") + } + + // Create and delete + profile := &MiningProfile{ + Name: "Delete Me", + MinerType: "xmrig", + } + created, _ := pm.CreateProfile(profile) + + err = pm.DeleteProfile(created.ID) + if err != nil { + t.Fatalf("failed to delete profile: %v", err) + } + + // Verify deletion + _, exists := pm.GetProfile(created.ID) + if exists { + t.Error("profile should not exist after deletion") + } +} + +func TestProfileManagerPersistence(t *testing.T) { + tmpDir, err := os.MkdirTemp("", "profile-persist-test") + if err != nil { + t.Fatalf("failed to create temp dir: %v", err) + } + defer os.RemoveAll(tmpDir) + + configPath := filepath.Join(tmpDir, "mining_profiles.json") + + // Create first manager and add profile + pm1 := &ProfileManager{ + profiles: make(map[string]*MiningProfile), + configPath: configPath, + } + + profile := &MiningProfile{ + Name: "Persistent Profile", + MinerType: "xmrig", + Config: RawConfig(`{"pool": "persist.pool.com"}`), + } + created, err := pm1.CreateProfile(profile) + if err != nil { + t.Fatalf("failed to create profile: %v", err) + } + + // Create second manager with same path - should load existing profile + pm2 := &ProfileManager{ + profiles: make(map[string]*MiningProfile), + configPath: configPath, + } + err = pm2.loadProfiles() + if err != nil { + t.Fatalf("failed to load profiles: %v", err) + } + + // Verify profile persisted + loaded, exists := pm2.GetProfile(created.ID) + if !exists { + t.Fatal("profile should be loaded from file") + } + + if loaded.Name != "Persistent Profile" { + t.Errorf("expected name 'Persistent Profile', got '%s'", loaded.Name) + } +} + +func TestProfileManagerConcurrency(t *testing.T) { + pm, cleanup := setupTestProfileManager(t) + defer cleanup() + + var wg sync.WaitGroup + numGoroutines := 10 + + // Concurrent creates + for i := 0; i < numGoroutines; i++ { + wg.Add(1) + go func(n int) { + defer wg.Done() + pm.CreateProfile(&MiningProfile{ + Name: "Concurrent Profile", + MinerType: "xmrig", + }) + }(i) + } + wg.Wait() + + profiles := pm.GetAllProfiles() + if len(profiles) != numGoroutines { + t.Errorf("expected %d profiles, got %d", numGoroutines, len(profiles)) + } + + // Concurrent reads + for i := 0; i < numGoroutines; i++ { + wg.Add(1) + go func() { + defer wg.Done() + pm.GetAllProfiles() + }() + } + wg.Wait() +} + +func TestProfileManagerInvalidJSON(t *testing.T) { + tmpDir, err := os.MkdirTemp("", "profile-invalid-test") + if err != nil { + t.Fatalf("failed to create temp dir: %v", err) + } + defer os.RemoveAll(tmpDir) + + configPath := filepath.Join(tmpDir, "mining_profiles.json") + + // Write invalid JSON + err = os.WriteFile(configPath, []byte("invalid json{{{"), 0644) + if err != nil { + t.Fatalf("failed to write invalid JSON: %v", err) + } + + pm := &ProfileManager{ + profiles: make(map[string]*MiningProfile), + configPath: configPath, + } + + err = pm.loadProfiles() + if err == nil { + t.Error("loadProfiles should fail with invalid JSON") + } +} + +func TestProfileManagerFileNotFound(t *testing.T) { + pm := &ProfileManager{ + profiles: make(map[string]*MiningProfile), + configPath: "/non/existent/path/profiles.json", + } + + err := pm.loadProfiles() + if err == nil { + t.Error("loadProfiles should fail when file not found") + } + + if !os.IsNotExist(err) { + t.Errorf("expected 'file not found' error, got: %v", err) + } +} + +func TestProfileManagerCreateRollback(t *testing.T) { + pm := &ProfileManager{ + profiles: make(map[string]*MiningProfile), + configPath: "/invalid/path/that/cannot/be/written/profiles.json", + } + + profile := &MiningProfile{ + Name: "Rollback Test", + MinerType: "xmrig", + } + + _, err := pm.CreateProfile(profile) + if err == nil { + t.Error("CreateProfile should fail when save fails") + } + + // Verify rollback - profile should not be in memory + profiles := pm.GetAllProfiles() + if len(profiles) != 0 { + t.Error("failed create should rollback - no profile should be in memory") + } +} + +func TestProfileManagerConfigWithData(t *testing.T) { + pm, cleanup := setupTestProfileManager(t) + defer cleanup() + + config := RawConfig(`{ + "pool": "pool.example.com:3333", + "wallet": "wallet123", + "threads": 4, + "algorithm": "rx/0" + }`) + + profile := &MiningProfile{ + Name: "Config Test", + MinerType: "xmrig", + Config: config, + } + + created, err := pm.CreateProfile(profile) + if err != nil { + t.Fatalf("failed to create profile: %v", err) + } + + retrieved, _ := pm.GetProfile(created.ID) + + // Parse config to verify + var parsedConfig map[string]interface{} + err = json.Unmarshal(retrieved.Config, &parsedConfig) + if err != nil { + t.Fatalf("failed to parse config: %v", err) + } + + if parsedConfig["pool"] != "pool.example.com:3333" { + t.Error("config pool value not preserved") + } + if parsedConfig["threads"].(float64) != 4 { + t.Error("config threads value not preserved") + } +} diff --git a/mining/ratelimiter.go b/mining/ratelimiter.go new file mode 100644 index 0000000..15a872c --- /dev/null +++ b/mining/ratelimiter.go @@ -0,0 +1,119 @@ +package mining + +import ( + "net/http" + "sync" + "time" + + "github.com/gin-gonic/gin" +) + +// RateLimiter provides token bucket rate limiting per IP address +type RateLimiter struct { + requestsPerSecond int + burst int + clients map[string]*rateLimitClient + mu sync.RWMutex + stopChan chan struct{} + stopped bool +} + +type rateLimitClient struct { + tokens float64 + lastCheck time.Time +} + +// NewRateLimiter creates a new rate limiter with the specified limits +func NewRateLimiter(requestsPerSecond, burst int) *RateLimiter { + rl := &RateLimiter{ + requestsPerSecond: requestsPerSecond, + burst: burst, + clients: make(map[string]*rateLimitClient), + stopChan: make(chan struct{}), + } + + // Start cleanup goroutine + go rl.cleanupLoop() + + return rl +} + +// cleanupLoop removes stale clients periodically +func (rl *RateLimiter) cleanupLoop() { + ticker := time.NewTicker(time.Minute) + defer ticker.Stop() + + for { + select { + case <-rl.stopChan: + return + case <-ticker.C: + rl.cleanup() + } + } +} + +// cleanup removes clients that haven't made requests in 5 minutes +func (rl *RateLimiter) cleanup() { + rl.mu.Lock() + defer rl.mu.Unlock() + + for ip, c := range rl.clients { + if time.Since(c.lastCheck) > 5*time.Minute { + delete(rl.clients, ip) + } + } +} + +// Stop stops the rate limiter's cleanup goroutine +func (rl *RateLimiter) Stop() { + rl.mu.Lock() + defer rl.mu.Unlock() + + if !rl.stopped { + close(rl.stopChan) + rl.stopped = true + } +} + +// Middleware returns a Gin middleware handler for rate limiting +func (rl *RateLimiter) Middleware() gin.HandlerFunc { + return func(c *gin.Context) { + ip := c.ClientIP() + + rl.mu.Lock() + cl, exists := rl.clients[ip] + if !exists { + cl = &rateLimitClient{tokens: float64(rl.burst), lastCheck: time.Now()} + rl.clients[ip] = cl + } + + // Token bucket algorithm + now := time.Now() + elapsed := now.Sub(cl.lastCheck).Seconds() + cl.tokens += elapsed * float64(rl.requestsPerSecond) + if cl.tokens > float64(rl.burst) { + cl.tokens = float64(rl.burst) + } + cl.lastCheck = now + + if cl.tokens < 1 { + rl.mu.Unlock() + respondWithError(c, http.StatusTooManyRequests, "RATE_LIMITED", + "too many requests", "rate limit exceeded") + c.Abort() + return + } + + cl.tokens-- + rl.mu.Unlock() + c.Next() + } +} + +// ClientCount returns the number of tracked clients (for testing/monitoring) +func (rl *RateLimiter) ClientCount() int { + rl.mu.RLock() + defer rl.mu.RUnlock() + return len(rl.clients) +} diff --git a/mining/ratelimiter_test.go b/mining/ratelimiter_test.go new file mode 100644 index 0000000..9dfa469 --- /dev/null +++ b/mining/ratelimiter_test.go @@ -0,0 +1,194 @@ +package mining + +import ( + "net/http" + "net/http/httptest" + "testing" + "time" + + "github.com/gin-gonic/gin" +) + +func TestNewRateLimiter(t *testing.T) { + rl := NewRateLimiter(10, 20) + if rl == nil { + t.Fatal("NewRateLimiter returned nil") + } + defer rl.Stop() + + if rl.requestsPerSecond != 10 { + t.Errorf("Expected requestsPerSecond 10, got %d", rl.requestsPerSecond) + } + if rl.burst != 20 { + t.Errorf("Expected burst 20, got %d", rl.burst) + } +} + +func TestRateLimiterStop(t *testing.T) { + rl := NewRateLimiter(10, 20) + + // Stop should not panic + defer func() { + if r := recover(); r != nil { + t.Errorf("Stop panicked: %v", r) + } + }() + + rl.Stop() + + // Calling Stop again should not panic (idempotent) + rl.Stop() +} + +func TestRateLimiterMiddleware(t *testing.T) { + gin.SetMode(gin.TestMode) + rl := NewRateLimiter(10, 5) // 10 req/s, burst of 5 + defer rl.Stop() + + router := gin.New() + router.Use(rl.Middleware()) + router.GET("/test", func(c *gin.Context) { + c.String(http.StatusOK, "ok") + }) + + // First 5 requests should succeed (burst) + for i := 0; i < 5; i++ { + req := httptest.NewRequest("GET", "/test", nil) + req.RemoteAddr = "192.168.1.1:12345" + w := httptest.NewRecorder() + router.ServeHTTP(w, req) + + if w.Code != http.StatusOK { + t.Errorf("Request %d: expected 200, got %d", i+1, w.Code) + } + } + + // 6th request should be rate limited + req := httptest.NewRequest("GET", "/test", nil) + req.RemoteAddr = "192.168.1.1:12345" + w := httptest.NewRecorder() + router.ServeHTTP(w, req) + + if w.Code != http.StatusTooManyRequests { + t.Errorf("Expected 429 Too Many Requests, got %d", w.Code) + } +} + +func TestRateLimiterDifferentIPs(t *testing.T) { + gin.SetMode(gin.TestMode) + rl := NewRateLimiter(10, 2) // 10 req/s, burst of 2 + defer rl.Stop() + + router := gin.New() + router.Use(rl.Middleware()) + router.GET("/test", func(c *gin.Context) { + c.String(http.StatusOK, "ok") + }) + + // Exhaust rate limit for IP1 + for i := 0; i < 2; i++ { + req := httptest.NewRequest("GET", "/test", nil) + req.RemoteAddr = "192.168.1.1:12345" + w := httptest.NewRecorder() + router.ServeHTTP(w, req) + } + + // IP1 should be rate limited + req := httptest.NewRequest("GET", "/test", nil) + req.RemoteAddr = "192.168.1.1:12345" + w := httptest.NewRecorder() + router.ServeHTTP(w, req) + if w.Code != http.StatusTooManyRequests { + t.Errorf("IP1 should be rate limited, got %d", w.Code) + } + + // IP2 should still be able to make requests + req = httptest.NewRequest("GET", "/test", nil) + req.RemoteAddr = "192.168.1.2:12345" + w = httptest.NewRecorder() + router.ServeHTTP(w, req) + if w.Code != http.StatusOK { + t.Errorf("IP2 should not be rate limited, got %d", w.Code) + } +} + +func TestRateLimiterClientCount(t *testing.T) { + rl := NewRateLimiter(10, 5) + defer rl.Stop() + + gin.SetMode(gin.TestMode) + router := gin.New() + router.Use(rl.Middleware()) + router.GET("/test", func(c *gin.Context) { + c.String(http.StatusOK, "ok") + }) + + // Initial count should be 0 + if count := rl.ClientCount(); count != 0 { + t.Errorf("Expected 0 clients, got %d", count) + } + + // Make a request + req := httptest.NewRequest("GET", "/test", nil) + req.RemoteAddr = "192.168.1.1:12345" + w := httptest.NewRecorder() + router.ServeHTTP(w, req) + + // Should have 1 client now + if count := rl.ClientCount(); count != 1 { + t.Errorf("Expected 1 client, got %d", count) + } + + // Make request from different IP + req = httptest.NewRequest("GET", "/test", nil) + req.RemoteAddr = "192.168.1.2:12345" + w = httptest.NewRecorder() + router.ServeHTTP(w, req) + + // Should have 2 clients now + if count := rl.ClientCount(); count != 2 { + t.Errorf("Expected 2 clients, got %d", count) + } +} + +func TestRateLimiterTokenRefill(t *testing.T) { + gin.SetMode(gin.TestMode) + rl := NewRateLimiter(100, 1) // 100 req/s, burst of 1 (refills quickly) + defer rl.Stop() + + router := gin.New() + router.Use(rl.Middleware()) + router.GET("/test", func(c *gin.Context) { + c.String(http.StatusOK, "ok") + }) + + // First request succeeds + req := httptest.NewRequest("GET", "/test", nil) + req.RemoteAddr = "192.168.1.1:12345" + w := httptest.NewRecorder() + router.ServeHTTP(w, req) + if w.Code != http.StatusOK { + t.Errorf("First request should succeed, got %d", w.Code) + } + + // Second request should fail (burst exhausted) + req = httptest.NewRequest("GET", "/test", nil) + req.RemoteAddr = "192.168.1.1:12345" + w = httptest.NewRecorder() + router.ServeHTTP(w, req) + if w.Code != http.StatusTooManyRequests { + t.Errorf("Second request should be rate limited, got %d", w.Code) + } + + // Wait for token to refill (at 100 req/s, 1 token takes 10ms) + time.Sleep(20 * time.Millisecond) + + // Third request should succeed (token refilled) + req = httptest.NewRequest("GET", "/test", nil) + req.RemoteAddr = "192.168.1.1:12345" + w = httptest.NewRecorder() + router.ServeHTTP(w, req) + if w.Code != http.StatusOK { + t.Errorf("Third request should succeed after refill, got %d", w.Code) + } +} diff --git a/mining/repository.go b/mining/repository.go new file mode 100644 index 0000000..f7e2191 --- /dev/null +++ b/mining/repository.go @@ -0,0 +1,158 @@ +package mining + +import ( + "encoding/json" + "fmt" + "os" + "path/filepath" + "sync" +) + +// Repository defines a generic interface for data persistence. +// Implementations can store data in files, databases, etc. +type Repository[T any] interface { + // Load reads data from the repository + Load() (T, error) + + // Save writes data to the repository + Save(data T) error + + // Update atomically loads, modifies, and saves data + Update(fn func(*T) error) error +} + +// FileRepository provides atomic file-based persistence for JSON data. +// It uses atomic writes (temp file + rename) to prevent corruption. +type FileRepository[T any] struct { + mu sync.RWMutex + path string + defaults func() T +} + +// FileRepositoryOption configures a FileRepository. +type FileRepositoryOption[T any] func(*FileRepository[T]) + +// WithDefaults sets the default value factory for when the file doesn't exist. +func WithDefaults[T any](fn func() T) FileRepositoryOption[T] { + return func(r *FileRepository[T]) { + r.defaults = fn + } +} + +// NewFileRepository creates a new file-based repository. +func NewFileRepository[T any](path string, opts ...FileRepositoryOption[T]) *FileRepository[T] { + r := &FileRepository[T]{ + path: path, + } + for _, opt := range opts { + opt(r) + } + return r +} + +// Load reads and deserializes data from the file. +// Returns defaults if file doesn't exist. +func (r *FileRepository[T]) Load() (T, error) { + r.mu.RLock() + defer r.mu.RUnlock() + + var result T + + data, err := os.ReadFile(r.path) + if err != nil { + if os.IsNotExist(err) { + if r.defaults != nil { + return r.defaults(), nil + } + return result, nil + } + return result, fmt.Errorf("failed to read file: %w", err) + } + + if err := json.Unmarshal(data, &result); err != nil { + return result, fmt.Errorf("failed to unmarshal data: %w", err) + } + + return result, nil +} + +// Save serializes and writes data to the file atomically. +func (r *FileRepository[T]) Save(data T) error { + r.mu.Lock() + defer r.mu.Unlock() + + return r.saveUnlocked(data) +} + +// saveUnlocked saves data without acquiring the lock (caller must hold lock). +func (r *FileRepository[T]) saveUnlocked(data T) error { + dir := filepath.Dir(r.path) + if err := os.MkdirAll(dir, 0755); err != nil { + return fmt.Errorf("failed to create directory: %w", err) + } + + jsonData, err := json.MarshalIndent(data, "", " ") + if err != nil { + return fmt.Errorf("failed to marshal data: %w", err) + } + + return AtomicWriteFile(r.path, jsonData, 0600) +} + +// Update atomically loads, modifies, and saves data. +// The modification function receives a pointer to the data. +func (r *FileRepository[T]) Update(fn func(*T) error) error { + r.mu.Lock() + defer r.mu.Unlock() + + // Load current data + var data T + fileData, err := os.ReadFile(r.path) + if err != nil { + if os.IsNotExist(err) { + if r.defaults != nil { + data = r.defaults() + } + } else { + return fmt.Errorf("failed to read file: %w", err) + } + } else { + if err := json.Unmarshal(fileData, &data); err != nil { + return fmt.Errorf("failed to unmarshal data: %w", err) + } + } + + // Apply modification + if err := fn(&data); err != nil { + return err + } + + // Save atomically + return r.saveUnlocked(data) +} + +// Path returns the file path of this repository. +func (r *FileRepository[T]) Path() string { + return r.path +} + +// Exists returns true if the repository file exists. +func (r *FileRepository[T]) Exists() bool { + r.mu.RLock() + defer r.mu.RUnlock() + + _, err := os.Stat(r.path) + return err == nil +} + +// Delete removes the repository file. +func (r *FileRepository[T]) Delete() error { + r.mu.Lock() + defer r.mu.Unlock() + + err := os.Remove(r.path) + if os.IsNotExist(err) { + return nil + } + return err +} diff --git a/mining/repository_test.go b/mining/repository_test.go new file mode 100644 index 0000000..f285166 --- /dev/null +++ b/mining/repository_test.go @@ -0,0 +1,401 @@ +package mining + +import ( + "errors" + "os" + "path/filepath" + "testing" +) + +type testData struct { + Name string `json:"name"` + Value int `json:"value"` +} + +func TestFileRepository_Load(t *testing.T) { + t.Run("NonExistentFile", func(t *testing.T) { + tmpDir := t.TempDir() + path := filepath.Join(tmpDir, "nonexistent.json") + repo := NewFileRepository[testData](path) + + data, err := repo.Load() + if err != nil { + t.Fatalf("Load should not error for non-existent file: %v", err) + } + if data.Name != "" || data.Value != 0 { + t.Error("Expected zero value for non-existent file") + } + }) + + t.Run("NonExistentFileWithDefaults", func(t *testing.T) { + tmpDir := t.TempDir() + path := filepath.Join(tmpDir, "nonexistent.json") + repo := NewFileRepository[testData](path, WithDefaults(func() testData { + return testData{Name: "default", Value: 42} + })) + + data, err := repo.Load() + if err != nil { + t.Fatalf("Load should not error: %v", err) + } + if data.Name != "default" || data.Value != 42 { + t.Errorf("Expected default values, got %+v", data) + } + }) + + t.Run("ExistingFile", func(t *testing.T) { + tmpDir := t.TempDir() + path := filepath.Join(tmpDir, "test.json") + + // Write test data + if err := os.WriteFile(path, []byte(`{"name":"test","value":123}`), 0600); err != nil { + t.Fatalf("Failed to write test file: %v", err) + } + + repo := NewFileRepository[testData](path) + data, err := repo.Load() + if err != nil { + t.Fatalf("Load failed: %v", err) + } + if data.Name != "test" || data.Value != 123 { + t.Errorf("Unexpected data: %+v", data) + } + }) + + t.Run("InvalidJSON", func(t *testing.T) { + tmpDir := t.TempDir() + path := filepath.Join(tmpDir, "invalid.json") + + if err := os.WriteFile(path, []byte(`{invalid json}`), 0600); err != nil { + t.Fatalf("Failed to write test file: %v", err) + } + + repo := NewFileRepository[testData](path) + _, err := repo.Load() + if err == nil { + t.Error("Expected error for invalid JSON") + } + }) +} + +func TestFileRepository_Save(t *testing.T) { + t.Run("NewFile", func(t *testing.T) { + tmpDir := t.TempDir() + path := filepath.Join(tmpDir, "subdir", "new.json") + repo := NewFileRepository[testData](path) + + data := testData{Name: "saved", Value: 456} + if err := repo.Save(data); err != nil { + t.Fatalf("Save failed: %v", err) + } + + // Verify file was created + if !repo.Exists() { + t.Error("File should exist after save") + } + + // Verify content + loaded, err := repo.Load() + if err != nil { + t.Fatalf("Load after save failed: %v", err) + } + if loaded.Name != "saved" || loaded.Value != 456 { + t.Errorf("Unexpected loaded data: %+v", loaded) + } + }) + + t.Run("OverwriteExisting", func(t *testing.T) { + tmpDir := t.TempDir() + path := filepath.Join(tmpDir, "existing.json") + repo := NewFileRepository[testData](path) + + // Save initial data + if err := repo.Save(testData{Name: "first", Value: 1}); err != nil { + t.Fatalf("First save failed: %v", err) + } + + // Overwrite + if err := repo.Save(testData{Name: "second", Value: 2}); err != nil { + t.Fatalf("Second save failed: %v", err) + } + + // Verify overwrite + loaded, err := repo.Load() + if err != nil { + t.Fatalf("Load failed: %v", err) + } + if loaded.Name != "second" || loaded.Value != 2 { + t.Errorf("Expected overwritten data, got: %+v", loaded) + } + }) +} + +func TestFileRepository_Update(t *testing.T) { + t.Run("UpdateExisting", func(t *testing.T) { + tmpDir := t.TempDir() + path := filepath.Join(tmpDir, "update.json") + repo := NewFileRepository[testData](path) + + // Save initial data + if err := repo.Save(testData{Name: "initial", Value: 10}); err != nil { + t.Fatalf("Initial save failed: %v", err) + } + + // Update + err := repo.Update(func(data *testData) error { + data.Value += 5 + return nil + }) + if err != nil { + t.Fatalf("Update failed: %v", err) + } + + // Verify update + loaded, err := repo.Load() + if err != nil { + t.Fatalf("Load failed: %v", err) + } + if loaded.Value != 15 { + t.Errorf("Expected value 15, got %d", loaded.Value) + } + }) + + t.Run("UpdateNonExistentWithDefaults", func(t *testing.T) { + tmpDir := t.TempDir() + path := filepath.Join(tmpDir, "new.json") + repo := NewFileRepository[testData](path, WithDefaults(func() testData { + return testData{Name: "default", Value: 100} + })) + + err := repo.Update(func(data *testData) error { + data.Value *= 2 + return nil + }) + if err != nil { + t.Fatalf("Update failed: %v", err) + } + + // Verify update started from defaults + loaded, err := repo.Load() + if err != nil { + t.Fatalf("Load failed: %v", err) + } + if loaded.Value != 200 { + t.Errorf("Expected value 200, got %d", loaded.Value) + } + }) + + t.Run("UpdateWithError", func(t *testing.T) { + tmpDir := t.TempDir() + path := filepath.Join(tmpDir, "error.json") + repo := NewFileRepository[testData](path) + + if err := repo.Save(testData{Name: "test", Value: 1}); err != nil { + t.Fatalf("Initial save failed: %v", err) + } + + // Update that returns error + testErr := errors.New("update error") + err := repo.Update(func(data *testData) error { + data.Value = 999 // This change should not be saved + return testErr + }) + if err != testErr { + t.Errorf("Expected test error, got: %v", err) + } + + // Verify original data unchanged + loaded, err := repo.Load() + if err != nil { + t.Fatalf("Load failed: %v", err) + } + if loaded.Value != 1 { + t.Errorf("Expected value 1 (unchanged), got %d", loaded.Value) + } + }) +} + +func TestFileRepository_Delete(t *testing.T) { + tmpDir := t.TempDir() + path := filepath.Join(tmpDir, "delete.json") + repo := NewFileRepository[testData](path) + + // Save data + if err := repo.Save(testData{Name: "temp", Value: 1}); err != nil { + t.Fatalf("Save failed: %v", err) + } + + if !repo.Exists() { + t.Error("File should exist after save") + } + + // Delete + if err := repo.Delete(); err != nil { + t.Fatalf("Delete failed: %v", err) + } + + if repo.Exists() { + t.Error("File should not exist after delete") + } + + // Delete non-existent should not error + if err := repo.Delete(); err != nil { + t.Errorf("Delete non-existent should not error: %v", err) + } +} + +func TestFileRepository_Path(t *testing.T) { + path := "/some/path/config.json" + repo := NewFileRepository[testData](path) + + if repo.Path() != path { + t.Errorf("Expected path %s, got %s", path, repo.Path()) + } +} + +func TestFileRepository_UpdateWithLoadError(t *testing.T) { + tmpDir := t.TempDir() + path := filepath.Join(tmpDir, "corrupt.json") + repo := NewFileRepository[testData](path) + + // Write invalid JSON + if err := os.WriteFile(path, []byte(`{invalid}`), 0600); err != nil { + t.Fatalf("Failed to write corrupt file: %v", err) + } + + // Update should fail to load the corrupt file + err := repo.Update(func(data *testData) error { + data.Value = 999 + return nil + }) + if err == nil { + t.Error("Expected error for corrupt file during Update") + } +} + +func TestFileRepository_SaveToReadOnlyDirectory(t *testing.T) { + if os.Getuid() == 0 { + t.Skip("Test skipped when running as root") + } + + tmpDir := t.TempDir() + readOnlyDir := filepath.Join(tmpDir, "readonly") + if err := os.Mkdir(readOnlyDir, 0555); err != nil { + t.Fatalf("Failed to create readonly dir: %v", err) + } + defer os.Chmod(readOnlyDir, 0755) // Restore permissions for cleanup + + path := filepath.Join(readOnlyDir, "test.json") + repo := NewFileRepository[testData](path) + + // Save should fail due to permission denied + err := repo.Save(testData{Name: "test", Value: 1}) + if err == nil { + t.Error("Expected error when saving to read-only directory") + } +} + +func TestFileRepository_DeleteNonExistent(t *testing.T) { + tmpDir := t.TempDir() + path := filepath.Join(tmpDir, "nonexistent.json") + repo := NewFileRepository[testData](path) + + // Delete on non-existent file should not error + if err := repo.Delete(); err != nil { + t.Errorf("Delete on non-existent file should not error: %v", err) + } +} + +func TestFileRepository_ExistsOnInvalidPath(t *testing.T) { + // Use a path that definitely doesn't exist + repo := NewFileRepository[testData]("/nonexistent/path/to/file.json") + + if repo.Exists() { + t.Error("Exists should return false for invalid path") + } +} + +func TestFileRepository_ConcurrentUpdates(t *testing.T) { + tmpDir := t.TempDir() + path := filepath.Join(tmpDir, "concurrent.json") + repo := NewFileRepository[testData](path, WithDefaults(func() testData { + return testData{Name: "initial", Value: 0} + })) + + // Run multiple concurrent updates + const numUpdates = 10 + done := make(chan bool) + + for i := 0; i < numUpdates; i++ { + go func() { + err := repo.Update(func(data *testData) error { + data.Value++ + return nil + }) + if err != nil { + t.Logf("Concurrent update error: %v", err) + } + done <- true + }() + } + + // Wait for all updates + for i := 0; i < numUpdates; i++ { + <-done + } + + // Verify final value equals number of updates + data, err := repo.Load() + if err != nil { + t.Fatalf("Load failed: %v", err) + } + if data.Value != numUpdates { + t.Errorf("Expected value %d after concurrent updates, got %d", numUpdates, data.Value) + } +} + +// Test with slice data +func TestFileRepository_SliceData(t *testing.T) { + type item struct { + ID string `json:"id"` + Name string `json:"name"` + } + + tmpDir := t.TempDir() + path := filepath.Join(tmpDir, "items.json") + repo := NewFileRepository[[]item](path, WithDefaults(func() []item { + return []item{} + })) + + // Save slice + items := []item{ + {ID: "1", Name: "First"}, + {ID: "2", Name: "Second"}, + } + if err := repo.Save(items); err != nil { + t.Fatalf("Save failed: %v", err) + } + + // Load and verify + loaded, err := repo.Load() + if err != nil { + t.Fatalf("Load failed: %v", err) + } + if len(loaded) != 2 { + t.Errorf("Expected 2 items, got %d", len(loaded)) + } + + // Update slice + err = repo.Update(func(data *[]item) error { + *data = append(*data, item{ID: "3", Name: "Third"}) + return nil + }) + if err != nil { + t.Fatalf("Update failed: %v", err) + } + + loaded, _ = repo.Load() + if len(loaded) != 3 { + t.Errorf("Expected 3 items after update, got %d", len(loaded)) + } +} diff --git a/mining/service.go b/mining/service.go new file mode 100644 index 0000000..d6e6ebb --- /dev/null +++ b/mining/service.go @@ -0,0 +1,1415 @@ +package mining + +import ( + "context" + "encoding/base64" + "encoding/json" + "fmt" + "net" + "net/http" + "net/url" + "os" + "path/filepath" + "runtime" + "strings" + "sync/atomic" + "time" + + "github.com/Masterminds/semver/v3" + "forge.lthn.ai/core/mining/docs" + "forge.lthn.ai/core/mining/logging" + "github.com/adrg/xdg" + ginmcp "github.com/ckanthony/gin-mcp" + "github.com/gin-contrib/cors" + "github.com/gin-gonic/gin" + "github.com/gorilla/websocket" + "github.com/shirou/gopsutil/v4/mem" + "github.com/swaggo/swag" + + swaggerFiles "github.com/swaggo/files" + ginSwagger "github.com/swaggo/gin-swagger" +) + +// Service encapsulates the gin-gonic router and the mining manager. +type Service struct { + Manager ManagerInterface + ProfileManager *ProfileManager + NodeService *NodeService + EventHub *EventHub + Router *gin.Engine + Server *http.Server + DisplayAddr string + SwaggerInstanceName string + APIBasePath string + SwaggerUIPath string + rateLimiter *RateLimiter + auth *DigestAuth + mcpServer *ginmcp.GinMCP +} + +// APIError represents a structured error response for the API +type APIError struct { + Code string `json:"code"` // Machine-readable error code + Message string `json:"message"` // Human-readable message + Details string `json:"details,omitempty"` // Technical details (for debugging) + Suggestion string `json:"suggestion,omitempty"` // What to do next + Retryable bool `json:"retryable"` // Can the client retry? +} + +// debugErrorsEnabled controls whether internal error details are exposed in API responses. +// In production, this should be false to prevent information disclosure. +var debugErrorsEnabled = os.Getenv("DEBUG_ERRORS") == "true" || os.Getenv("GIN_MODE") != "release" + +// sanitizeErrorDetails filters potentially sensitive information from error details. +// In production mode (debugErrorsEnabled=false), returns empty string. +func sanitizeErrorDetails(details string) string { + if debugErrorsEnabled { + return details + } + // In production, don't expose internal error details + return "" +} + +// Error codes are defined in errors.go + +// respondWithError sends a structured error response +func respondWithError(c *gin.Context, status int, code string, message string, details string) { + apiErr := APIError{ + Code: code, + Message: message, + Details: sanitizeErrorDetails(details), + Retryable: isRetryableError(status), + } + + // Add suggestions based on error code + switch code { + case ErrCodeMinerNotFound: + apiErr.Suggestion = "Check the miner name or install the miner first" + case ErrCodeProfileNotFound: + apiErr.Suggestion = "Create a new profile or check the profile ID" + case ErrCodeInstallFailed: + apiErr.Suggestion = "Check your internet connection and try again" + case ErrCodeStartFailed: + apiErr.Suggestion = "Check the miner configuration and logs" + case ErrCodeInvalidInput: + apiErr.Suggestion = "Verify the request body matches the expected format" + case ErrCodeServiceUnavailable: + apiErr.Suggestion = "The service is temporarily unavailable, try again later" + apiErr.Retryable = true + } + + c.JSON(status, apiErr) +} + +// respondWithMiningError sends a structured error response from a MiningError. +// This allows using pre-built error constructors from errors.go. +func respondWithMiningError(c *gin.Context, err *MiningError) { + details := "" + if err.Cause != nil { + details = err.Cause.Error() + } + if err.Details != "" { + if details != "" { + details += "; " + } + details += err.Details + } + + apiErr := APIError{ + Code: err.Code, + Message: err.Message, + Details: sanitizeErrorDetails(details), + Suggestion: err.Suggestion, + Retryable: err.Retryable, + } + + c.JSON(err.StatusCode(), apiErr) +} + +// isRetryableError determines if an error status code is retryable +func isRetryableError(status int) bool { + return status == http.StatusServiceUnavailable || + status == http.StatusTooManyRequests || + status == http.StatusGatewayTimeout +} + +// securityHeadersMiddleware adds security headers to all responses. +// This helps protect against common web vulnerabilities. +func securityHeadersMiddleware() gin.HandlerFunc { + return func(c *gin.Context) { + // Prevent MIME type sniffing + c.Header("X-Content-Type-Options", "nosniff") + // Prevent clickjacking + c.Header("X-Frame-Options", "DENY") + // Enable XSS filter in older browsers + c.Header("X-XSS-Protection", "1; mode=block") + // Restrict referrer information + c.Header("Referrer-Policy", "strict-origin-when-cross-origin") + // Content Security Policy for API responses + c.Header("Content-Security-Policy", "default-src 'none'; frame-ancestors 'none'") + c.Next() + } +} + +// contentTypeValidationMiddleware ensures POST/PUT requests have proper Content-Type. +func contentTypeValidationMiddleware() gin.HandlerFunc { + return func(c *gin.Context) { + method := c.Request.Method + if method != http.MethodPost && method != http.MethodPut && method != http.MethodPatch { + c.Next() + return + } + + // Skip if no body expected + if c.Request.ContentLength == 0 { + c.Next() + return + } + + contentType := c.GetHeader("Content-Type") + // Allow JSON and form data + if strings.HasPrefix(contentType, "application/json") || + strings.HasPrefix(contentType, "application/x-www-form-urlencoded") || + strings.HasPrefix(contentType, "multipart/form-data") { + c.Next() + return + } + + respondWithError(c, http.StatusUnsupportedMediaType, ErrCodeInvalidInput, + "Unsupported Content-Type", + "Use application/json for API requests") + c.Abort() + } +} + +// requestIDMiddleware adds a unique request ID to each request for tracing +func requestIDMiddleware() gin.HandlerFunc { + return func(c *gin.Context) { + // Use existing request ID from header if provided, otherwise generate one + requestID := c.GetHeader("X-Request-ID") + if requestID == "" { + requestID = generateRequestID() + } + + // Set in context for use by handlers + c.Set("requestID", requestID) + + // Set in response header + c.Header("X-Request-ID", requestID) + + c.Next() + } +} + +// generateRequestID creates a unique request ID using timestamp and random bytes +func generateRequestID() string { + b := make([]byte, 8) + _, _ = base64.StdEncoding.Decode(b, []byte(fmt.Sprintf("%d", time.Now().UnixNano()))) + return fmt.Sprintf("%d-%x", time.Now().UnixMilli(), b[:4]) +} + +// getRequestID extracts the request ID from gin context +func getRequestID(c *gin.Context) string { + if id, exists := c.Get("requestID"); exists { + if s, ok := id.(string); ok { + return s + } + } + return "" +} + +// logWithRequestID logs a message with request ID correlation +func logWithRequestID(c *gin.Context, level string, message string, fields logging.Fields) { + if fields == nil { + fields = logging.Fields{} + } + if reqID := getRequestID(c); reqID != "" { + fields["request_id"] = reqID + } + switch level { + case "error": + logging.Error(message, fields) + case "warn": + logging.Warn(message, fields) + case "info": + logging.Info(message, fields) + default: + logging.Debug(message, fields) + } +} + +// csrfMiddleware protects against CSRF attacks for browser-based requests. +// For state-changing methods (POST, PUT, DELETE), it requires one of: +// - Authorization header (API clients) +// - X-Requested-With header (AJAX clients) +// - Origin header matching allowed origins (already handled by CORS) +// GET requests are always allowed as they should be idempotent. +func csrfMiddleware() gin.HandlerFunc { + return func(c *gin.Context) { + // Only check state-changing methods + method := c.Request.Method + if method == http.MethodGet || method == http.MethodHead || method == http.MethodOptions { + c.Next() + return + } + + // Allow if Authorization header present (API client) + if c.GetHeader("Authorization") != "" { + c.Next() + return + } + + // Allow if X-Requested-With header present (AJAX/XHR request) + if c.GetHeader("X-Requested-With") != "" { + c.Next() + return + } + + // Allow if Content-Type is application/json (not sent by HTML forms) + contentType := c.GetHeader("Content-Type") + if strings.HasPrefix(contentType, "application/json") { + c.Next() + return + } + + // Reject the request as potential CSRF + respondWithError(c, http.StatusForbidden, "CSRF_PROTECTION", + "Request blocked by CSRF protection", + "Include X-Requested-With header or use application/json content type") + c.Abort() + } +} + +// DefaultRequestTimeout is the default timeout for API requests. +const DefaultRequestTimeout = 30 * time.Second + +// Cache-Control header constants +const ( + CacheNoStore = "no-store" + CacheNoCache = "no-cache" + CachePublic1Min = "public, max-age=60" + CachePublic5Min = "public, max-age=300" +) + +// cacheMiddleware adds Cache-Control headers based on the endpoint. +func cacheMiddleware() gin.HandlerFunc { + return func(c *gin.Context) { + // Only cache GET requests + if c.Request.Method != http.MethodGet { + c.Header("Cache-Control", CacheNoStore) + c.Next() + return + } + + path := c.Request.URL.Path + + // Static-ish resources that can be cached briefly + switch { + case strings.HasSuffix(path, "/available"): + // Available miners list - can be cached for 5 minutes + c.Header("Cache-Control", CachePublic5Min) + case strings.HasSuffix(path, "/info"): + // System info - can be cached for 1 minute + c.Header("Cache-Control", CachePublic1Min) + case strings.Contains(path, "/swagger"): + // Swagger docs - can be cached + c.Header("Cache-Control", CachePublic5Min) + default: + // Dynamic data (stats, miners, profiles) - don't cache + c.Header("Cache-Control", CacheNoCache) + } + + c.Next() + } +} + +// requestTimeoutMiddleware adds a timeout to request handling. +// This prevents slow requests from consuming resources indefinitely. +func requestTimeoutMiddleware(timeout time.Duration) gin.HandlerFunc { + return func(c *gin.Context) { + // Skip timeout for WebSocket upgrades and streaming endpoints + if c.GetHeader("Upgrade") == "websocket" { + c.Next() + return + } + if strings.HasSuffix(c.Request.URL.Path, "/events") { + c.Next() + return + } + + // Create context with timeout + ctx, cancel := context.WithTimeout(c.Request.Context(), timeout) + defer cancel() + + // Replace request context + c.Request = c.Request.WithContext(ctx) + + // Use atomic flag to prevent race condition between handler and timeout response + // Only one of them should write to the response + var responded int32 + + // Channel to signal completion + done := make(chan struct{}) + + go func() { + c.Next() + // Mark that the handler has completed (and likely written a response) + atomic.StoreInt32(&responded, 1) + close(done) + }() + + select { + case <-done: + // Request completed normally + case <-ctx.Done(): + // Timeout occurred - only respond if handler hasn't already + if atomic.CompareAndSwapInt32(&responded, 0, 1) { + c.Abort() + respondWithError(c, http.StatusGatewayTimeout, ErrCodeTimeout, + "Request timed out", fmt.Sprintf("Request exceeded %s timeout", timeout)) + } + } + } +} + +// WebSocket upgrader for the events endpoint +var wsUpgrader = websocket.Upgrader{ + ReadBufferSize: 1024, + WriteBufferSize: 1024, + CheckOrigin: func(r *http.Request) bool { + // Allow connections from localhost origins only + origin := r.Header.Get("Origin") + if origin == "" { + return true // No origin header (non-browser clients) + } + // Parse the origin URL properly to prevent bypass attacks + u, err := url.Parse(origin) + if err != nil { + return false + } + host := u.Hostname() + // Only allow exact localhost matches + return host == "localhost" || host == "127.0.0.1" || host == "::1" || + host == "wails.localhost" + }, +} + +// NewService creates a new mining service +func NewService(manager ManagerInterface, listenAddr string, displayAddr string, swaggerNamespace string) (*Service, error) { + apiBasePath := "/" + strings.Trim(swaggerNamespace, "/") + swaggerUIPath := apiBasePath + "/swagger" + + docs.SwaggerInfo.Title = "Mining Module API" + docs.SwaggerInfo.Version = "1.0" + docs.SwaggerInfo.Host = displayAddr + docs.SwaggerInfo.BasePath = apiBasePath + instanceName := "swagger_" + strings.ReplaceAll(strings.Trim(swaggerNamespace, "/"), "/", "_") + swag.Register(instanceName, docs.SwaggerInfo) + + profileManager, err := NewProfileManager() + if err != nil { + logging.Warn("failed to initialize profile manager", logging.Fields{"error": err}) + // Continue without profile manager - profile features will be degraded + // Create a minimal in-memory profile manager as fallback + profileManager = &ProfileManager{ + profiles: make(map[string]*MiningProfile), + } + } + + // Initialize node service (optional - only fails if XDG paths are broken) + nodeService, err := NewNodeService() + if err != nil { + logging.Warn("failed to initialize node service", logging.Fields{"error": err}) + // Continue without node service - P2P features will be unavailable + } + + // Initialize event hub for WebSocket real-time updates + eventHub := NewEventHub() + go eventHub.Run() + + // Wire up event hub to manager for miner events + if mgr, ok := manager.(*Manager); ok { + mgr.SetEventHub(eventHub) + } + + // Set up state provider for WebSocket state sync on reconnect + eventHub.SetStateProvider(func() interface{} { + miners := manager.ListMiners() + if len(miners) == 0 { + return nil + } + // Return current state of all miners + state := make([]map[string]interface{}, 0, len(miners)) + for _, miner := range miners { + stats, _ := miner.GetStats(context.Background()) + minerState := map[string]interface{}{ + "name": miner.GetName(), + "status": "running", + } + if stats != nil { + minerState["hashrate"] = stats.Hashrate + minerState["shares"] = stats.Shares + minerState["rejected"] = stats.Rejected + minerState["uptime"] = stats.Uptime + } + state = append(state, minerState) + } + return map[string]interface{}{ + "miners": state, + } + }) + + // Initialize authentication from environment + authConfig := AuthConfigFromEnv() + var auth *DigestAuth + if authConfig.Enabled { + auth = NewDigestAuth(authConfig) + logging.Info("API authentication enabled", logging.Fields{"realm": authConfig.Realm}) + } + + return &Service{ + Manager: manager, + ProfileManager: profileManager, + NodeService: nodeService, + EventHub: eventHub, + Server: &http.Server{ + Addr: listenAddr, + ReadTimeout: 30 * time.Second, + WriteTimeout: 30 * time.Second, + IdleTimeout: 60 * time.Second, + ReadHeaderTimeout: 10 * time.Second, + }, + DisplayAddr: displayAddr, + SwaggerInstanceName: instanceName, + APIBasePath: apiBasePath, + SwaggerUIPath: swaggerUIPath, + auth: auth, + }, nil +} + +// InitRouter initializes the Gin router and sets up all routes without starting an HTTP server. +// Use this when embedding the mining service in another application (e.g., Wails). +// After calling InitRouter, you can use the Router field directly as an http.Handler. +func (s *Service) InitRouter() { + s.Router = gin.Default() + + // Extract port safely from server address for CORS + serverPort := "9090" // default fallback + if s.Server.Addr != "" { + if _, port, err := net.SplitHostPort(s.Server.Addr); err == nil && port != "" { + serverPort = port + } + } + + // Configure CORS to only allow local origins + corsConfig := cors.Config{ + AllowOrigins: []string{ + "http://localhost:4200", // Angular dev server + "http://127.0.0.1:4200", + "http://localhost:9090", // Default API port + "http://127.0.0.1:9090", + "http://localhost:" + serverPort, + "http://127.0.0.1:" + serverPort, + "http://wails.localhost", // Wails desktop app (uses localhost origin) + }, + AllowMethods: []string{"GET", "POST", "PUT", "DELETE", "OPTIONS"}, + AllowHeaders: []string{"Origin", "Content-Type", "Accept", "Authorization", "X-Request-ID", "X-Requested-With"}, + ExposeHeaders: []string{"Content-Length", "X-Request-ID"}, + AllowCredentials: true, + MaxAge: 12 * time.Hour, + } + s.Router.Use(cors.New(corsConfig)) + + // Add security headers (SEC-LOW-4) + s.Router.Use(securityHeadersMiddleware()) + + // Add Content-Type validation for POST/PUT (API-MED-8) + s.Router.Use(contentTypeValidationMiddleware()) + + // Add request body size limit middleware (1MB max) + s.Router.Use(func(c *gin.Context) { + c.Request.Body = http.MaxBytesReader(c.Writer, c.Request.Body, 1<<20) // 1MB + c.Next() + }) + + // Add CSRF protection for browser requests (SEC-MED-3) + // Requires X-Requested-With or Authorization header for state-changing methods + s.Router.Use(csrfMiddleware()) + + // Add request timeout middleware (RESIL-MED-8) + s.Router.Use(requestTimeoutMiddleware(DefaultRequestTimeout)) + + // Add cache headers middleware (API-MED-7) + s.Router.Use(cacheMiddleware()) + + // Add X-Request-ID middleware for request tracing + s.Router.Use(requestIDMiddleware()) + + // Add rate limiting (10 requests/second with burst of 20) + s.rateLimiter = NewRateLimiter(10, 20) + s.Router.Use(s.rateLimiter.Middleware()) + + s.SetupRoutes() +} + +// Stop gracefully stops the service and cleans up resources +func (s *Service) Stop() { + if s.rateLimiter != nil { + s.rateLimiter.Stop() + } + if s.EventHub != nil { + s.EventHub.Stop() + } + if s.auth != nil { + s.auth.Stop() + } + if s.NodeService != nil { + if err := s.NodeService.StopTransport(); err != nil { + logging.Warn("failed to stop node service transport", logging.Fields{"error": err}) + } + } +} + +// ServiceStartup initializes the router and starts the HTTP server. +// For embedding without a standalone server, use InitRouter() instead. +func (s *Service) ServiceStartup(ctx context.Context) error { + s.InitRouter() + s.Server.Handler = s.Router + + // Channel to capture server startup errors + errChan := make(chan error, 1) + + go func() { + if err := s.Server.ListenAndServe(); err != nil && err != http.ErrServerClosed { + logging.Error("server error", logging.Fields{"addr": s.Server.Addr, "error": err}) + errChan <- err + } + close(errChan) // Prevent goroutine leak + }() + + go func() { + <-ctx.Done() + s.Stop() // Clean up service resources (auth, event hub, node service) + s.Manager.Stop() + ctxShutdown, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + if err := s.Server.Shutdown(ctxShutdown); err != nil { + logging.Error("server shutdown error", logging.Fields{"error": err}) + } + }() + + // Verify server is actually listening by attempting to connect + maxRetries := 50 // 50 * 100ms = 5 seconds max + for i := 0; i < maxRetries; i++ { + select { + case err := <-errChan: + if err != nil { + return fmt.Errorf("failed to start server: %w", err) + } + return nil // Channel closed without error means server shut down + default: + // Try to connect to verify server is listening + conn, err := net.DialTimeout("tcp", s.Server.Addr, 50*time.Millisecond) + if err == nil { + conn.Close() + return nil // Server is ready + } + time.Sleep(100 * time.Millisecond) + } + } + + return fmt.Errorf("server failed to start listening on %s within timeout", s.Server.Addr) +} + +// SetupRoutes configures all API routes on the Gin router. +// This is called automatically by ServiceStartup, but can also be called +// manually after InitRouter for embedding in other applications. +func (s *Service) SetupRoutes() { + apiGroup := s.Router.Group(s.APIBasePath) + + // Health endpoints (no auth required for orchestration/monitoring) + apiGroup.GET("/health", s.handleHealth) + apiGroup.GET("/ready", s.handleReady) + + // Apply authentication middleware if enabled + if s.auth != nil { + apiGroup.Use(s.auth.Middleware()) + } + + { + apiGroup.GET("/info", s.handleGetInfo) + apiGroup.GET("/metrics", s.handleMetrics) + apiGroup.POST("/doctor", s.handleDoctor) + apiGroup.POST("/update", s.handleUpdateCheck) + + minersGroup := apiGroup.Group("/miners") + { + minersGroup.GET("", s.handleListMiners) + minersGroup.GET("/available", s.handleListAvailableMiners) + minersGroup.POST("/:miner_name/install", s.handleInstallMiner) + minersGroup.DELETE("/:miner_name/uninstall", s.handleUninstallMiner) + minersGroup.DELETE("/:miner_name", s.handleStopMiner) + minersGroup.GET("/:miner_name/stats", s.handleGetMinerStats) + minersGroup.GET("/:miner_name/hashrate-history", s.handleGetMinerHashrateHistory) + minersGroup.GET("/:miner_name/logs", s.handleGetMinerLogs) + minersGroup.POST("/:miner_name/stdin", s.handleMinerStdin) + } + + // Historical data endpoints (database-backed) + historyGroup := apiGroup.Group("/history") + { + historyGroup.GET("/status", s.handleHistoryStatus) + historyGroup.GET("/miners", s.handleAllMinersHistoricalStats) + historyGroup.GET("/miners/:miner_name", s.handleMinerHistoricalStats) + historyGroup.GET("/miners/:miner_name/hashrate", s.handleMinerHistoricalHashrate) + } + + profilesGroup := apiGroup.Group("/profiles") + { + profilesGroup.GET("", s.handleListProfiles) + profilesGroup.POST("", s.handleCreateProfile) + profilesGroup.GET("/:id", s.handleGetProfile) + profilesGroup.PUT("/:id", s.handleUpdateProfile) + profilesGroup.DELETE("/:id", s.handleDeleteProfile) + profilesGroup.POST("/:id/start", s.handleStartMinerWithProfile) + } + + // WebSocket endpoint for real-time events + wsGroup := apiGroup.Group("/ws") + { + wsGroup.GET("/events", s.handleWebSocketEvents) + } + + // Add P2P node endpoints if node service is available + if s.NodeService != nil { + s.NodeService.SetupRoutes(apiGroup) + } + } + + // Serve the embedded web component + componentFS, err := GetComponentFS() + if err == nil { + s.Router.StaticFS("/component", componentFS) + } + + swaggerURL := ginSwagger.URL(fmt.Sprintf("http://%s%s/doc.json", s.DisplayAddr, s.SwaggerUIPath)) + s.Router.GET(s.SwaggerUIPath+"/*any", ginSwagger.WrapHandler(swaggerFiles.Handler, swaggerURL, ginSwagger.InstanceName(s.SwaggerInstanceName))) + + // Initialize MCP server for AI assistant integration + // This exposes API endpoints as MCP tools for Claude, Cursor, etc. + s.mcpServer = ginmcp.New(s.Router, &ginmcp.Config{ + Name: "Mining API", + Description: "Mining dashboard API exposed via Model Context Protocol (MCP)", + BaseURL: fmt.Sprintf("http://%s", s.DisplayAddr), + }) + s.mcpServer.Mount(s.APIBasePath + "/mcp") + logging.Info("MCP server enabled", logging.Fields{"endpoint": s.APIBasePath + "/mcp"}) +} + +// HealthResponse represents the health check response +type HealthResponse struct { + Status string `json:"status"` + Components map[string]string `json:"components,omitempty"` +} + +// handleHealth godoc +// @Summary Health check endpoint +// @Description Returns service health status. Used for liveness probes. +// @Tags system +// @Produce json +// @Success 200 {object} HealthResponse +// @Router /health [get] +func (s *Service) handleHealth(c *gin.Context) { + c.JSON(http.StatusOK, HealthResponse{ + Status: "healthy", + }) +} + +// handleReady godoc +// @Summary Readiness check endpoint +// @Description Returns service readiness with component status. Used for readiness probes. +// @Tags system +// @Produce json +// @Success 200 {object} HealthResponse +// @Success 503 {object} HealthResponse +// @Router /ready [get] +func (s *Service) handleReady(c *gin.Context) { + components := make(map[string]string) + allReady := true + + // Check manager + if s.Manager != nil { + components["manager"] = "ready" + } else { + components["manager"] = "not initialized" + allReady = false + } + + // Check profile manager + if s.ProfileManager != nil { + components["profiles"] = "ready" + } else { + components["profiles"] = "degraded" + // Don't fail readiness for degraded profile manager + } + + // Check event hub + if s.EventHub != nil { + components["events"] = "ready" + } else { + components["events"] = "not initialized" + allReady = false + } + + // Check node service (optional) + if s.NodeService != nil { + components["p2p"] = "ready" + } else { + components["p2p"] = "disabled" + } + + status := "ready" + httpStatus := http.StatusOK + if !allReady { + status = "not ready" + httpStatus = http.StatusServiceUnavailable + } + + c.JSON(httpStatus, HealthResponse{ + Status: status, + Components: components, + }) +} + +// handleGetInfo godoc +// @Summary Get live miner installation information +// @Description Retrieves live installation details for all miners, along with system information. +// @Tags system +// @Produce json +// @Success 200 {object} SystemInfo +// @Failure 500 {object} map[string]string "Internal server error" +// @Router /info [get] +func (s *Service) handleGetInfo(c *gin.Context) { + systemInfo, err := s.updateInstallationCache() + if err != nil { + respondWithMiningError(c, ErrInternal("failed to get system info").WithCause(err)) + return + } + c.JSON(http.StatusOK, systemInfo) +} + +// updateInstallationCache performs a live check and updates the cache file. +func (s *Service) updateInstallationCache() (*SystemInfo, error) { + // Always create a complete SystemInfo object + systemInfo := &SystemInfo{ + Timestamp: time.Now(), + OS: runtime.GOOS, + Architecture: runtime.GOARCH, + GoVersion: runtime.Version(), + AvailableCPUCores: runtime.NumCPU(), + InstalledMinersInfo: []*InstallationDetails{}, // Initialize as empty slice + } + + vMem, err := mem.VirtualMemory() + if err == nil { + systemInfo.TotalSystemRAMGB = float64(vMem.Total) / (1024 * 1024 * 1024) + } + + for _, availableMiner := range s.Manager.ListAvailableMiners() { + miner, err := CreateMiner(availableMiner.Name) + if err != nil { + continue // Skip unsupported miner types + } + details, err := miner.CheckInstallation() + if err != nil { + logging.Warn("failed to check installation", logging.Fields{"miner": availableMiner.Name, "error": err}) + } + systemInfo.InstalledMinersInfo = append(systemInfo.InstalledMinersInfo, details) + } + + configDir, err := xdg.ConfigFile("lethean-desktop/miners") + if err != nil { + return nil, fmt.Errorf("could not get config directory: %w", err) + } + if err := os.MkdirAll(configDir, 0755); err != nil { + return nil, fmt.Errorf("could not create config directory: %w", err) + } + configPath := filepath.Join(configDir, "config.json") + + data, err := json.MarshalIndent(systemInfo, "", " ") + if err != nil { + return nil, fmt.Errorf("could not marshal cache data: %w", err) + } + + if err := os.WriteFile(configPath, data, 0600); err != nil { + return nil, fmt.Errorf("could not write cache file: %w", err) + } + + return systemInfo, nil +} + +// handleDoctor godoc +// @Summary Check miner installations +// @Description Performs a live check on all available miners to verify their installation status, version, and path. +// @Tags system +// @Produce json +// @Success 200 {object} SystemInfo +// @Router /doctor [post] +func (s *Service) handleDoctor(c *gin.Context) { + systemInfo, err := s.updateInstallationCache() + if err != nil { + respondWithMiningError(c, ErrInternal("failed to update cache").WithCause(err)) + return + } + c.JSON(http.StatusOK, systemInfo) +} + +// handleUpdateCheck godoc +// @Summary Check for miner updates +// @Description Checks if any installed miners have a new version available for download. +// @Tags system +// @Produce json +// @Success 200 {object} map[string]string +// @Router /update [post] +func (s *Service) handleUpdateCheck(c *gin.Context) { + updates := make(map[string]string) + for _, availableMiner := range s.Manager.ListAvailableMiners() { + miner, err := CreateMiner(availableMiner.Name) + if err != nil { + continue // Skip unsupported miner types + } + + details, err := miner.CheckInstallation() + if err != nil || !details.IsInstalled { + continue + } + + latestVersionStr, err := miner.GetLatestVersion() + if err != nil { + continue + } + + latestVersion, err := semver.NewVersion(latestVersionStr) + if err != nil { + continue + } + + installedVersion, err := semver.NewVersion(details.Version) + if err != nil { + continue + } + + if latestVersion.GreaterThan(installedVersion) { + updates[miner.GetName()] = latestVersion.String() + } + } + + if len(updates) == 0 { + c.JSON(http.StatusOK, gin.H{"status": "All miners are up to date."}) + return + } + + c.JSON(http.StatusOK, gin.H{"updates_available": updates}) +} + +// handleUninstallMiner godoc +// @Summary Uninstall a miner +// @Description Removes all files for a specific miner. +// @Tags miners +// @Produce json +// @Param miner_type path string true "Miner Type to uninstall" +// @Success 200 {object} map[string]string +// @Router /miners/{miner_type}/uninstall [delete] +func (s *Service) handleUninstallMiner(c *gin.Context) { + minerType := c.Param("miner_name") + if err := s.Manager.UninstallMiner(c.Request.Context(), minerType); err != nil { + respondWithMiningError(c, ErrInternal("failed to uninstall miner").WithCause(err)) + return + } + if _, err := s.updateInstallationCache(); err != nil { + logging.Warn("failed to update cache after uninstall", logging.Fields{"error": err}) + } + c.JSON(http.StatusOK, gin.H{"status": minerType + " uninstalled successfully."}) +} + +// handleListMiners godoc +// @Summary List all running miners +// @Description Get a list of all running miners +// @Tags miners +// @Produce json +// @Success 200 {array} XMRigMiner +// @Router /miners [get] +func (s *Service) handleListMiners(c *gin.Context) { + miners := s.Manager.ListMiners() + c.JSON(http.StatusOK, miners) +} + +// handleListAvailableMiners godoc +// @Summary List all available miners +// @Description Get a list of all available miners +// @Tags miners +// @Produce json +// @Success 200 {array} AvailableMiner +// @Router /miners/available [get] +func (s *Service) handleListAvailableMiners(c *gin.Context) { + miners := s.Manager.ListAvailableMiners() + c.JSON(http.StatusOK, miners) +} + +// handleInstallMiner godoc +// @Summary Install or update a miner +// @Description Install a new miner or update an existing one. +// @Tags miners +// @Produce json +// @Param miner_type path string true "Miner Type to install/update" +// @Success 200 {object} map[string]string +// @Router /miners/{miner_type}/install [post] +func (s *Service) handleInstallMiner(c *gin.Context) { + minerType := c.Param("miner_name") + miner, err := CreateMiner(minerType) + if err != nil { + respondWithMiningError(c, ErrUnsupportedMiner(minerType)) + return + } + + if err := miner.Install(); err != nil { + respondWithMiningError(c, ErrInstallFailed(minerType).WithCause(err)) + return + } + + if _, err := s.updateInstallationCache(); err != nil { + logging.Warn("failed to update cache after install", logging.Fields{"error": err}) + } + + details, err := miner.CheckInstallation() + if err != nil { + respondWithMiningError(c, ErrInternal("failed to verify installation").WithCause(err)) + return + } + + c.JSON(http.StatusOK, gin.H{"status": "installed", "version": details.Version, "path": details.Path}) +} + +// handleStartMinerWithProfile godoc +// @Summary Start a new miner using a profile +// @Description Start a new miner with the configuration from a saved profile +// @Tags profiles +// @Produce json +// @Param id path string true "Profile ID" +// @Success 200 {object} XMRigMiner +// @Router /profiles/{id}/start [post] +func (s *Service) handleStartMinerWithProfile(c *gin.Context) { + profileID := c.Param("id") + profile, exists := s.ProfileManager.GetProfile(profileID) + if !exists { + respondWithMiningError(c, ErrProfileNotFound(profileID)) + return + } + + var config Config + if err := json.Unmarshal(profile.Config, &config); err != nil { + respondWithMiningError(c, ErrInvalidConfig("failed to parse profile config").WithCause(err)) + return + } + + // Validate config from profile to prevent shell injection and other issues + if err := config.Validate(); err != nil { + respondWithMiningError(c, ErrInvalidConfig("profile config validation failed").WithCause(err)) + return + } + + miner, err := s.Manager.StartMiner(c.Request.Context(), profile.MinerType, &config) + if err != nil { + respondWithMiningError(c, ErrStartFailed(profile.Name).WithCause(err)) + return + } + c.JSON(http.StatusOK, miner) +} + +// handleStopMiner godoc +// @Summary Stop a running miner +// @Description Stop a running miner by its name +// @Tags miners +// @Produce json +// @Param miner_name path string true "Miner Name" +// @Success 200 {object} map[string]string +// @Router /miners/{miner_name} [delete] +func (s *Service) handleStopMiner(c *gin.Context) { + minerName := c.Param("miner_name") + if err := s.Manager.StopMiner(c.Request.Context(), minerName); err != nil { + respondWithMiningError(c, ErrStopFailed(minerName).WithCause(err)) + return + } + c.JSON(http.StatusOK, gin.H{"status": "stopped"}) +} + +// handleGetMinerStats godoc +// @Summary Get miner stats +// @Description Get statistics for a running miner +// @Tags miners +// @Produce json +// @Param miner_name path string true "Miner Name" +// @Success 200 {object} PerformanceMetrics +// @Router /miners/{miner_name}/stats [get] +func (s *Service) handleGetMinerStats(c *gin.Context) { + minerName := c.Param("miner_name") + miner, err := s.Manager.GetMiner(minerName) + if err != nil { + respondWithMiningError(c, ErrMinerNotFound(minerName).WithCause(err)) + return + } + stats, err := miner.GetStats(c.Request.Context()) + if err != nil { + respondWithMiningError(c, ErrInternal("failed to get miner stats").WithCause(err)) + return + } + c.JSON(http.StatusOK, stats) +} + +// handleGetMinerHashrateHistory godoc +// @Summary Get miner hashrate history +// @Description Get historical hashrate data for a running miner +// @Tags miners +// @Produce json +// @Param miner_name path string true "Miner Name" +// @Success 200 {array} HashratePoint +// @Router /miners/{miner_name}/hashrate-history [get] +func (s *Service) handleGetMinerHashrateHistory(c *gin.Context) { + minerName := c.Param("miner_name") + history, err := s.Manager.GetMinerHashrateHistory(minerName) + if err != nil { + respondWithMiningError(c, ErrMinerNotFound(minerName).WithCause(err)) + return + } + c.JSON(http.StatusOK, history) +} + +// handleGetMinerLogs godoc +// @Summary Get miner log output +// @Description Get the captured stdout/stderr output from a running miner. Log lines are base64 encoded to preserve ANSI escape codes and special characters. +// @Tags miners +// @Produce json +// @Param miner_name path string true "Miner Name" +// @Success 200 {array} string "Base64 encoded log lines" +// @Router /miners/{miner_name}/logs [get] +func (s *Service) handleGetMinerLogs(c *gin.Context) { + minerName := c.Param("miner_name") + miner, err := s.Manager.GetMiner(minerName) + if err != nil { + respondWithMiningError(c, ErrMinerNotFound(minerName).WithCause(err)) + return + } + logs := miner.GetLogs() + // Base64 encode each log line to preserve ANSI escape codes and special characters + encodedLogs := make([]string, len(logs)) + for i, line := range logs { + encodedLogs[i] = base64.StdEncoding.EncodeToString([]byte(line)) + } + c.JSON(http.StatusOK, encodedLogs) +} + +// StdinInput represents input to send to miner's stdin +type StdinInput struct { + Input string `json:"input" binding:"required"` +} + +// handleMinerStdin godoc +// @Summary Send input to miner stdin +// @Description Send console commands to a running miner's stdin (e.g., 'h' for hashrate, 'p' for pause) +// @Tags miners +// @Accept json +// @Produce json +// @Param miner_name path string true "Miner Name" +// @Param input body StdinInput true "Input to send" +// @Success 200 {object} map[string]string +// @Failure 400 {object} map[string]string +// @Failure 404 {object} map[string]string +// @Router /miners/{miner_name}/stdin [post] +func (s *Service) handleMinerStdin(c *gin.Context) { + minerName := c.Param("miner_name") + miner, err := s.Manager.GetMiner(minerName) + if err != nil { + respondWithError(c, http.StatusNotFound, ErrCodeMinerNotFound, "miner not found", err.Error()) + return + } + + var input StdinInput + if err := c.ShouldBindJSON(&input); err != nil { + respondWithMiningError(c, ErrInvalidConfig("invalid input format").WithCause(err)) + return + } + + if err := miner.WriteStdin(input.Input); err != nil { + respondWithMiningError(c, ErrInternal("failed to write to stdin").WithCause(err)) + return + } + + c.JSON(http.StatusOK, gin.H{"status": "sent", "input": input.Input}) +} + +// handleListProfiles godoc +// @Summary List all mining profiles +// @Description Get a list of all saved mining profiles +// @Tags profiles +// @Produce json +// @Success 200 {array} MiningProfile +// @Router /profiles [get] +func (s *Service) handleListProfiles(c *gin.Context) { + profiles := s.ProfileManager.GetAllProfiles() + c.JSON(http.StatusOK, profiles) +} + +// handleCreateProfile godoc +// @Summary Create a new mining profile +// @Description Create and save a new mining profile +// @Tags profiles +// @Accept json +// @Produce json +// @Param profile body MiningProfile true "Mining Profile" +// @Success 201 {object} MiningProfile +// @Failure 400 {object} APIError "Invalid profile data" +// @Router /profiles [post] +func (s *Service) handleCreateProfile(c *gin.Context) { + var profile MiningProfile + if err := c.ShouldBindJSON(&profile); err != nil { + respondWithError(c, http.StatusBadRequest, ErrCodeInvalidInput, "invalid profile data", err.Error()) + return + } + + // Validate required fields + if profile.Name == "" { + respondWithError(c, http.StatusBadRequest, ErrCodeInvalidInput, "profile name is required", "") + return + } + if profile.MinerType == "" { + respondWithError(c, http.StatusBadRequest, ErrCodeInvalidInput, "miner type is required", "") + return + } + + createdProfile, err := s.ProfileManager.CreateProfile(&profile) + if err != nil { + respondWithError(c, http.StatusInternalServerError, ErrCodeInternal, "failed to create profile", err.Error()) + return + } + + c.JSON(http.StatusCreated, createdProfile) +} + +// handleGetProfile godoc +// @Summary Get a specific mining profile +// @Description Get a mining profile by its ID +// @Tags profiles +// @Produce json +// @Param id path string true "Profile ID" +// @Success 200 {object} MiningProfile +// @Router /profiles/{id} [get] +func (s *Service) handleGetProfile(c *gin.Context) { + profileID := c.Param("id") + profile, exists := s.ProfileManager.GetProfile(profileID) + if !exists { + respondWithError(c, http.StatusNotFound, ErrCodeProfileNotFound, "profile not found", "") + return + } + c.JSON(http.StatusOK, profile) +} + +// handleUpdateProfile godoc +// @Summary Update a mining profile +// @Description Update an existing mining profile +// @Tags profiles +// @Accept json +// @Produce json +// @Param id path string true "Profile ID" +// @Param profile body MiningProfile true "Updated Mining Profile" +// @Success 200 {object} MiningProfile +// @Failure 404 {object} APIError "Profile not found" +// @Router /profiles/{id} [put] +func (s *Service) handleUpdateProfile(c *gin.Context) { + profileID := c.Param("id") + var profile MiningProfile + if err := c.ShouldBindJSON(&profile); err != nil { + respondWithError(c, http.StatusBadRequest, ErrCodeInvalidInput, "invalid profile data", err.Error()) + return + } + profile.ID = profileID + + if err := s.ProfileManager.UpdateProfile(&profile); err != nil { + // Check if error is "not found" + if strings.Contains(err.Error(), "not found") { + respondWithError(c, http.StatusNotFound, ErrCodeProfileNotFound, "profile not found", err.Error()) + return + } + respondWithError(c, http.StatusInternalServerError, ErrCodeInternal, "failed to update profile", err.Error()) + return + } + c.JSON(http.StatusOK, profile) +} + +// handleDeleteProfile godoc +// @Summary Delete a mining profile +// @Description Delete a mining profile by its ID. Idempotent - returns success even if profile doesn't exist. +// @Tags profiles +// @Produce json +// @Param id path string true "Profile ID" +// @Success 200 {object} map[string]string +// @Router /profiles/{id} [delete] +func (s *Service) handleDeleteProfile(c *gin.Context) { + profileID := c.Param("id") + if err := s.ProfileManager.DeleteProfile(profileID); err != nil { + // Make DELETE idempotent - if profile doesn't exist, still return success + if strings.Contains(err.Error(), "not found") { + c.JSON(http.StatusOK, gin.H{"status": "profile deleted"}) + return + } + respondWithError(c, http.StatusInternalServerError, ErrCodeInternal, "failed to delete profile", err.Error()) + return + } + c.JSON(http.StatusOK, gin.H{"status": "profile deleted"}) +} + +// handleHistoryStatus godoc +// @Summary Get database history status +// @Description Get the status of database persistence for historical data +// @Tags history +// @Produce json +// @Success 200 {object} map[string]interface{} +// @Router /history/status [get] +func (s *Service) handleHistoryStatus(c *gin.Context) { + if manager, ok := s.Manager.(*Manager); ok { + c.JSON(http.StatusOK, gin.H{ + "enabled": manager.IsDatabaseEnabled(), + "retentionDays": manager.dbRetention, + }) + return + } + c.JSON(http.StatusOK, gin.H{"enabled": false, "error": "manager type not supported"}) +} + +// handleAllMinersHistoricalStats godoc +// @Summary Get historical stats for all miners +// @Description Get aggregated historical statistics for all miners from the database +// @Tags history +// @Produce json +// @Success 200 {array} database.HashrateStats +// @Router /history/miners [get] +func (s *Service) handleAllMinersHistoricalStats(c *gin.Context) { + manager, ok := s.Manager.(*Manager) + if !ok { + respondWithMiningError(c, ErrInternal("manager type not supported")) + return + } + + stats, err := manager.GetAllMinerHistoricalStats() + if err != nil { + respondWithMiningError(c, ErrDatabaseError("get historical stats").WithCause(err)) + return + } + + c.JSON(http.StatusOK, stats) +} + +// handleMinerHistoricalStats godoc +// @Summary Get historical stats for a specific miner +// @Description Get aggregated historical statistics for a specific miner from the database +// @Tags history +// @Produce json +// @Param miner_name path string true "Miner Name" +// @Success 200 {object} database.HashrateStats +// @Router /history/miners/{miner_name} [get] +func (s *Service) handleMinerHistoricalStats(c *gin.Context) { + minerName := c.Param("miner_name") + manager, ok := s.Manager.(*Manager) + if !ok { + respondWithMiningError(c, ErrInternal("manager type not supported")) + return + } + + stats, err := manager.GetMinerHistoricalStats(minerName) + if err != nil { + respondWithMiningError(c, ErrDatabaseError("get miner stats").WithCause(err)) + return + } + + if stats == nil { + respondWithMiningError(c, ErrMinerNotFound(minerName).WithDetails("no historical data found")) + return + } + + c.JSON(http.StatusOK, stats) +} + +// handleMinerHistoricalHashrate godoc +// @Summary Get historical hashrate data for a specific miner +// @Description Get detailed historical hashrate data for a specific miner from the database +// @Tags history +// @Produce json +// @Param miner_name path string true "Miner Name" +// @Param since query string false "Start time (RFC3339 format)" +// @Param until query string false "End time (RFC3339 format)" +// @Success 200 {array} HashratePoint +// @Router /history/miners/{miner_name}/hashrate [get] +func (s *Service) handleMinerHistoricalHashrate(c *gin.Context) { + minerName := c.Param("miner_name") + manager, ok := s.Manager.(*Manager) + if !ok { + respondWithMiningError(c, ErrInternal("manager type not supported")) + return + } + + // Parse time range from query params, default to last 24 hours + until := time.Now() + since := until.Add(-24 * time.Hour) + + if sinceStr := c.Query("since"); sinceStr != "" { + if t, err := time.Parse(time.RFC3339, sinceStr); err == nil { + since = t + } + } + if untilStr := c.Query("until"); untilStr != "" { + if t, err := time.Parse(time.RFC3339, untilStr); err == nil { + until = t + } + } + + history, err := manager.GetMinerHistoricalHashrate(minerName, since, until) + if err != nil { + respondWithMiningError(c, ErrDatabaseError("get hashrate history").WithCause(err)) + return + } + + c.JSON(http.StatusOK, history) +} + +// handleWebSocketEvents godoc +// @Summary WebSocket endpoint for real-time mining events +// @Description Upgrade to WebSocket for real-time mining stats and events. +// @Description Events include: miner.starting, miner.started, miner.stopping, miner.stopped, miner.stats, miner.error +// @Tags websocket +// @Success 101 {string} string "Switching Protocols" +// @Router /ws/events [get] +func (s *Service) handleWebSocketEvents(c *gin.Context) { + conn, err := wsUpgrader.Upgrade(c.Writer, c.Request, nil) + if err != nil { + logging.Error("failed to upgrade WebSocket connection", logging.Fields{"error": err}) + return + } + + logging.Info("new WebSocket connection", logging.Fields{"remote": c.Request.RemoteAddr}) + // Only record connection after successful registration to avoid metrics race + if s.EventHub.ServeWs(conn) { + RecordWSConnection(true) + } else { + logging.Warn("WebSocket connection rejected", logging.Fields{"remote": c.Request.RemoteAddr, "reason": "limit reached"}) + } +} + +// handleMetrics godoc +// @Summary Get internal metrics +// @Description Returns internal metrics for monitoring and debugging +// @Tags system +// @Produce json +// @Success 200 {object} map[string]interface{} +// @Router /metrics [get] +func (s *Service) handleMetrics(c *gin.Context) { + c.JSON(http.StatusOK, GetMetricsSnapshot()) +} diff --git a/mining/service_test.go b/mining/service_test.go new file mode 100644 index 0000000..de4a1f3 --- /dev/null +++ b/mining/service_test.go @@ -0,0 +1,226 @@ +package mining + +import ( + "context" + "net/http" + "net/http/httptest" + "testing" + "time" + + "github.com/gin-gonic/gin" +) + +// MockMiner is a mock implementation of the Miner interface for testing. +type MockMiner struct { + InstallFunc func() error + UninstallFunc func() error + StartFunc func(config *Config) error + StopFunc func() error + GetStatsFunc func(ctx context.Context) (*PerformanceMetrics, error) + GetTypeFunc func() string + GetNameFunc func() string + GetPathFunc func() string + GetBinaryPathFunc func() string + CheckInstallationFunc func() (*InstallationDetails, error) + GetLatestVersionFunc func() (string, error) + GetHashrateHistoryFunc func() []HashratePoint + AddHashratePointFunc func(point HashratePoint) + ReduceHashrateHistoryFunc func(now time.Time) + GetLogsFunc func() []string + WriteStdinFunc func(input string) error +} + +func (m *MockMiner) Install() error { return m.InstallFunc() } +func (m *MockMiner) Uninstall() error { return m.UninstallFunc() } +func (m *MockMiner) Start(config *Config) error { return m.StartFunc(config) } +func (m *MockMiner) Stop() error { return m.StopFunc() } +func (m *MockMiner) GetStats(ctx context.Context) (*PerformanceMetrics, error) { + return m.GetStatsFunc(ctx) +} +func (m *MockMiner) GetType() string { + if m.GetTypeFunc != nil { + return m.GetTypeFunc() + } + return "mock" +} +func (m *MockMiner) GetName() string { return m.GetNameFunc() } +func (m *MockMiner) GetPath() string { return m.GetPathFunc() } +func (m *MockMiner) GetBinaryPath() string { return m.GetBinaryPathFunc() } +func (m *MockMiner) CheckInstallation() (*InstallationDetails, error) { + return m.CheckInstallationFunc() +} +func (m *MockMiner) GetLatestVersion() (string, error) { return m.GetLatestVersionFunc() } +func (m *MockMiner) GetHashrateHistory() []HashratePoint { return m.GetHashrateHistoryFunc() } +func (m *MockMiner) AddHashratePoint(point HashratePoint) { m.AddHashratePointFunc(point) } +func (m *MockMiner) ReduceHashrateHistory(now time.Time) { m.ReduceHashrateHistoryFunc(now) } +func (m *MockMiner) GetLogs() []string { return m.GetLogsFunc() } +func (m *MockMiner) WriteStdin(input string) error { return m.WriteStdinFunc(input) } + +// MockManager is a mock implementation of the Manager for testing. +type MockManager struct { + ListMinersFunc func() []Miner + ListAvailableMinersFunc func() []AvailableMiner + StartMinerFunc func(ctx context.Context, minerType string, config *Config) (Miner, error) + StopMinerFunc func(ctx context.Context, minerName string) error + GetMinerFunc func(minerName string) (Miner, error) + GetMinerHashrateHistoryFunc func(minerName string) ([]HashratePoint, error) + UninstallMinerFunc func(ctx context.Context, minerType string) error + StopFunc func() +} + +func (m *MockManager) ListMiners() []Miner { return m.ListMinersFunc() } +func (m *MockManager) ListAvailableMiners() []AvailableMiner { return m.ListAvailableMinersFunc() } +func (m *MockManager) StartMiner(ctx context.Context, minerType string, config *Config) (Miner, error) { + return m.StartMinerFunc(ctx, minerType, config) +} +func (m *MockManager) StopMiner(ctx context.Context, minerName string) error { + return m.StopMinerFunc(ctx, minerName) +} +func (m *MockManager) GetMiner(minerName string) (Miner, error) { + return m.GetMinerFunc(minerName) +} +func (m *MockManager) GetMinerHashrateHistory(minerName string) ([]HashratePoint, error) { + return m.GetMinerHashrateHistoryFunc(minerName) +} +func (m *MockManager) UninstallMiner(ctx context.Context, minerType string) error { + return m.UninstallMinerFunc(ctx, minerType) +} +func (m *MockManager) Stop() { m.StopFunc() } + +var _ ManagerInterface = (*MockManager)(nil) + +func setupTestRouter() (*gin.Engine, *MockManager) { + gin.SetMode(gin.TestMode) + router := gin.Default() + mockManager := &MockManager{ + ListMinersFunc: func() []Miner { return []Miner{} }, + ListAvailableMinersFunc: func() []AvailableMiner { return []AvailableMiner{} }, + StartMinerFunc: func(ctx context.Context, minerType string, config *Config) (Miner, error) { + return nil, nil + }, + StopMinerFunc: func(ctx context.Context, minerName string) error { return nil }, + GetMinerFunc: func(minerName string) (Miner, error) { return nil, nil }, + GetMinerHashrateHistoryFunc: func(minerName string) ([]HashratePoint, error) { + return nil, nil + }, + UninstallMinerFunc: func(ctx context.Context, minerType string) error { return nil }, + StopFunc: func() {}, + } + service := &Service{ + Manager: mockManager, + Router: router, + APIBasePath: "/", + SwaggerUIPath: "/swagger", + } + service.SetupRoutes() + return router, mockManager +} + +func TestHandleListMiners(t *testing.T) { + router, mockManager := setupTestRouter() + mockManager.ListMinersFunc = func() []Miner { + return []Miner{&XMRigMiner{BaseMiner: BaseMiner{Name: "test-miner"}}} + } + + req, _ := http.NewRequest("GET", "/miners", nil) + w := httptest.NewRecorder() + router.ServeHTTP(w, req) + + if w.Code != http.StatusOK { + t.Errorf("expected status %d, got %d", http.StatusOK, w.Code) + } +} + +func TestHandleGetInfo(t *testing.T) { + router, _ := setupTestRouter() + + // Case 1: Successful response + req, _ := http.NewRequest("GET", "/info", nil) + w := httptest.NewRecorder() + router.ServeHTTP(w, req) + + if w.Code != http.StatusOK { + t.Errorf("expected status %d, got %d", http.StatusOK, w.Code) + } +} + +func TestHandleDoctor(t *testing.T) { + router, mockManager := setupTestRouter() + mockManager.ListAvailableMinersFunc = func() []AvailableMiner { + return []AvailableMiner{{Name: "xmrig"}} + } + + // Case 1: Successful response + req, _ := http.NewRequest("POST", "/doctor", nil) + w := httptest.NewRecorder() + router.ServeHTTP(w, req) + + if w.Code != http.StatusOK { + t.Errorf("expected status %d, got %d", http.StatusOK, w.Code) + } +} + +func TestHandleInstallMiner(t *testing.T) { + router, _ := setupTestRouter() + + // Test installing a miner + req, _ := http.NewRequest("POST", "/miners/xmrig/install", nil) + req.Header.Set("Content-Type", "application/json") + w := httptest.NewRecorder() + router.ServeHTTP(w, req) + + // Installation endpoint should be accessible + if w.Code != http.StatusOK && w.Code != http.StatusInternalServerError { + t.Errorf("expected status 200 or 500, got %d", w.Code) + } +} + +func TestHandleStopMiner(t *testing.T) { + router, mockManager := setupTestRouter() + mockManager.StopMinerFunc = func(ctx context.Context, minerName string) error { + return nil + } + + req, _ := http.NewRequest("DELETE", "/miners/test-miner", nil) + w := httptest.NewRecorder() + router.ServeHTTP(w, req) + + if w.Code != http.StatusOK { + t.Errorf("expected status %d, got %d", http.StatusOK, w.Code) + } +} + +func TestHandleGetMinerStats(t *testing.T) { + router, mockManager := setupTestRouter() + mockManager.GetMinerFunc = func(minerName string) (Miner, error) { + return &MockMiner{ + GetStatsFunc: func(ctx context.Context) (*PerformanceMetrics, error) { + return &PerformanceMetrics{Hashrate: 100}, nil + }, + GetLogsFunc: func() []string { return []string{} }, + }, nil + } + + req, _ := http.NewRequest("GET", "/miners/test-miner/stats", nil) + w := httptest.NewRecorder() + router.ServeHTTP(w, req) + + if w.Code != http.StatusOK { + t.Errorf("expected status %d, got %d", http.StatusOK, w.Code) + } +} + +func TestHandleGetMinerHashrateHistory(t *testing.T) { + router, mockManager := setupTestRouter() + mockManager.GetMinerHashrateHistoryFunc = func(minerName string) ([]HashratePoint, error) { + return []HashratePoint{{Timestamp: time.Now(), Hashrate: 100}}, nil + } + + req, _ := http.NewRequest("GET", "/miners/test-miner/hashrate-history", nil) + w := httptest.NewRecorder() + router.ServeHTTP(w, req) + + if w.Code != http.StatusOK { + t.Errorf("expected status %d, got %d", http.StatusOK, w.Code) + } +} diff --git a/mining/settings_manager.go b/mining/settings_manager.go new file mode 100644 index 0000000..bd96c78 --- /dev/null +++ b/mining/settings_manager.go @@ -0,0 +1,225 @@ +package mining + +import ( + "encoding/json" + "fmt" + "os" + "path/filepath" + "sync" + + "github.com/adrg/xdg" +) + +const settingsFileName = "settings.json" + +// WindowState stores the last window position and size +type WindowState struct { + X int `json:"x"` + Y int `json:"y"` + Width int `json:"width"` + Height int `json:"height"` + Maximized bool `json:"maximized"` +} + +// MinerDefaults stores default configuration for miners +type MinerDefaults struct { + DefaultPool string `json:"defaultPool,omitempty"` + DefaultWallet string `json:"defaultWallet,omitempty"` + DefaultAlgorithm string `json:"defaultAlgorithm,omitempty"` + CPUMaxThreadsHint int `json:"cpuMaxThreadsHint,omitempty"` // Default CPU throttle percentage + CPUThrottleThreshold int `json:"cpuThrottleThreshold,omitempty"` // Throttle when CPU exceeds this % +} + +// AppSettings stores application-wide settings +type AppSettings struct { + // Window settings + Window WindowState `json:"window"` + + // Behavior settings + StartOnBoot bool `json:"startOnBoot"` + MinimizeToTray bool `json:"minimizeToTray"` + StartMinimized bool `json:"startMinimized"` + AutostartMiners bool `json:"autostartMiners"` + ShowNotifications bool `json:"showNotifications"` + + // Mining settings + MinerDefaults MinerDefaults `json:"minerDefaults"` + PauseOnBattery bool `json:"pauseOnBattery"` + PauseOnUserActive bool `json:"pauseOnUserActive"` + PauseOnUserActiveDelay int `json:"pauseOnUserActiveDelay"` // Seconds of inactivity before resuming + + // Performance settings + EnableCPUThrottle bool `json:"enableCpuThrottle"` + CPUThrottlePercent int `json:"cpuThrottlePercent"` // Target max CPU % when throttling + CPUMonitorInterval int `json:"cpuMonitorInterval"` // Seconds between CPU checks + AutoThrottleOnHighTemp bool `json:"autoThrottleOnHighTemp"` // Throttle when CPU temp is high + + // Theme + Theme string `json:"theme"` // "light", "dark", "system" +} + +// DefaultSettings returns sensible defaults for app settings +func DefaultSettings() *AppSettings { + return &AppSettings{ + Window: WindowState{ + Width: 1400, + Height: 900, + }, + StartOnBoot: false, + MinimizeToTray: true, + StartMinimized: false, + AutostartMiners: false, + ShowNotifications: true, + MinerDefaults: MinerDefaults{ + CPUMaxThreadsHint: 50, // Default to 50% CPU + CPUThrottleThreshold: 80, // Throttle if CPU > 80% + }, + PauseOnBattery: true, + PauseOnUserActive: false, + PauseOnUserActiveDelay: 60, + EnableCPUThrottle: false, + CPUThrottlePercent: 70, + CPUMonitorInterval: 5, + AutoThrottleOnHighTemp: false, + Theme: "system", + } +} + +// SettingsManager handles loading and saving app settings +type SettingsManager struct { + mu sync.RWMutex + settings *AppSettings + settingsPath string +} + +// NewSettingsManager creates a new settings manager +func NewSettingsManager() (*SettingsManager, error) { + settingsPath, err := xdg.ConfigFile(filepath.Join("lethean-desktop", settingsFileName)) + if err != nil { + return nil, fmt.Errorf("could not resolve settings path: %w", err) + } + + sm := &SettingsManager{ + settings: DefaultSettings(), + settingsPath: settingsPath, + } + + if err := sm.Load(); err != nil { + // If file doesn't exist, use defaults and save them + if os.IsNotExist(err) { + if saveErr := sm.Save(); saveErr != nil { + return nil, fmt.Errorf("could not save default settings: %w", saveErr) + } + } else { + return nil, fmt.Errorf("could not load settings: %w", err) + } + } + + return sm, nil +} + +// Load reads settings from disk +func (sm *SettingsManager) Load() error { + sm.mu.Lock() + defer sm.mu.Unlock() + + data, err := os.ReadFile(sm.settingsPath) + if err != nil { + return err + } + + var settings AppSettings + if err := json.Unmarshal(data, &settings); err != nil { + return err + } + + sm.settings = &settings + return nil +} + +// Save writes settings to disk +func (sm *SettingsManager) Save() error { + sm.mu.Lock() + defer sm.mu.Unlock() + + data, err := json.MarshalIndent(sm.settings, "", " ") + if err != nil { + return err + } + + return os.WriteFile(sm.settingsPath, data, 0600) +} + +// Get returns a copy of the current settings +func (sm *SettingsManager) Get() *AppSettings { + sm.mu.RLock() + defer sm.mu.RUnlock() + + // Return a copy to prevent concurrent modification + copy := *sm.settings + return © +} + +// Update applies changes to settings and saves +func (sm *SettingsManager) Update(fn func(*AppSettings)) error { + sm.mu.Lock() + defer sm.mu.Unlock() + + fn(sm.settings) + + data, err := json.MarshalIndent(sm.settings, "", " ") + if err != nil { + return err + } + + return os.WriteFile(sm.settingsPath, data, 0600) +} + +// UpdateWindowState saves the current window state +func (sm *SettingsManager) UpdateWindowState(x, y, width, height int, maximized bool) error { + return sm.Update(func(s *AppSettings) { + s.Window.X = x + s.Window.Y = y + s.Window.Width = width + s.Window.Height = height + s.Window.Maximized = maximized + }) +} + +// GetWindowState returns the saved window state +func (sm *SettingsManager) GetWindowState() WindowState { + sm.mu.RLock() + defer sm.mu.RUnlock() + return sm.settings.Window +} + +// SetStartOnBoot enables/disables start on boot +func (sm *SettingsManager) SetStartOnBoot(enabled bool) error { + return sm.Update(func(s *AppSettings) { + s.StartOnBoot = enabled + }) +} + +// SetAutostartMiners enables/disables miner autostart +func (sm *SettingsManager) SetAutostartMiners(enabled bool) error { + return sm.Update(func(s *AppSettings) { + s.AutostartMiners = enabled + }) +} + +// SetCPUThrottle configures CPU throttling +func (sm *SettingsManager) SetCPUThrottle(enabled bool, percent int) error { + return sm.Update(func(s *AppSettings) { + s.EnableCPUThrottle = enabled + if percent > 0 && percent <= 100 { + s.CPUThrottlePercent = percent + } + }) +} + +// SetMinerDefaults updates default miner configuration +func (sm *SettingsManager) SetMinerDefaults(defaults MinerDefaults) error { + return sm.Update(func(s *AppSettings) { + s.MinerDefaults = defaults + }) +} diff --git a/mining/settings_manager_test.go b/mining/settings_manager_test.go new file mode 100644 index 0000000..69e9d2c --- /dev/null +++ b/mining/settings_manager_test.go @@ -0,0 +1,211 @@ +package mining + +import ( + "os" + "path/filepath" + "testing" +) + +func TestSettingsManager_DefaultSettings(t *testing.T) { + defaults := DefaultSettings() + + if defaults.Window.Width != 1400 { + t.Errorf("Expected default width 1400, got %d", defaults.Window.Width) + } + if defaults.Window.Height != 900 { + t.Errorf("Expected default height 900, got %d", defaults.Window.Height) + } + if defaults.MinerDefaults.CPUMaxThreadsHint != 50 { + t.Errorf("Expected default CPU hint 50, got %d", defaults.MinerDefaults.CPUMaxThreadsHint) + } + if defaults.MinerDefaults.CPUThrottleThreshold != 80 { + t.Errorf("Expected default throttle threshold 80, got %d", defaults.MinerDefaults.CPUThrottleThreshold) + } + if !defaults.PauseOnBattery { + t.Error("Expected PauseOnBattery to be true by default") + } +} + +func TestSettingsManager_SaveAndLoad(t *testing.T) { + // Use a temp directory for testing + tmpDir := t.TempDir() + settingsPath := filepath.Join(tmpDir, "settings.json") + + // Create settings manager with custom path + sm := &SettingsManager{ + settings: DefaultSettings(), + settingsPath: settingsPath, + } + + // Modify settings + sm.settings.Window.Width = 1920 + sm.settings.Window.Height = 1080 + sm.settings.StartOnBoot = true + sm.settings.AutostartMiners = true + sm.settings.CPUThrottlePercent = 50 + + // Save + err := sm.Save() + if err != nil { + t.Fatalf("Failed to save settings: %v", err) + } + + // Verify file exists + if _, err := os.Stat(settingsPath); os.IsNotExist(err) { + t.Fatal("Settings file was not created") + } + + // Create new manager and load + sm2 := &SettingsManager{ + settings: DefaultSettings(), + settingsPath: settingsPath, + } + err = sm2.Load() + if err != nil { + t.Fatalf("Failed to load settings: %v", err) + } + + // Verify loaded values + if sm2.settings.Window.Width != 1920 { + t.Errorf("Expected width 1920, got %d", sm2.settings.Window.Width) + } + if sm2.settings.Window.Height != 1080 { + t.Errorf("Expected height 1080, got %d", sm2.settings.Window.Height) + } + if !sm2.settings.StartOnBoot { + t.Error("Expected StartOnBoot to be true") + } + if !sm2.settings.AutostartMiners { + t.Error("Expected AutostartMiners to be true") + } + if sm2.settings.CPUThrottlePercent != 50 { + t.Errorf("Expected CPUThrottlePercent 50, got %d", sm2.settings.CPUThrottlePercent) + } +} + +func TestSettingsManager_UpdateWindowState(t *testing.T) { + tmpDir := t.TempDir() + settingsPath := filepath.Join(tmpDir, "settings.json") + + sm := &SettingsManager{ + settings: DefaultSettings(), + settingsPath: settingsPath, + } + + err := sm.UpdateWindowState(100, 200, 800, 600, false) + if err != nil { + t.Fatalf("Failed to update window state: %v", err) + } + + state := sm.GetWindowState() + if state.X != 100 { + t.Errorf("Expected X 100, got %d", state.X) + } + if state.Y != 200 { + t.Errorf("Expected Y 200, got %d", state.Y) + } + if state.Width != 800 { + t.Errorf("Expected Width 800, got %d", state.Width) + } + if state.Height != 600 { + t.Errorf("Expected Height 600, got %d", state.Height) + } +} + +func TestSettingsManager_SetCPUThrottle(t *testing.T) { + tmpDir := t.TempDir() + settingsPath := filepath.Join(tmpDir, "settings.json") + + sm := &SettingsManager{ + settings: DefaultSettings(), + settingsPath: settingsPath, + } + + // Test enabling throttle + err := sm.SetCPUThrottle(true, 30) + if err != nil { + t.Fatalf("Failed to set CPU throttle: %v", err) + } + + settings := sm.Get() + if !settings.EnableCPUThrottle { + t.Error("Expected EnableCPUThrottle to be true") + } + if settings.CPUThrottlePercent != 30 { + t.Errorf("Expected CPUThrottlePercent 30, got %d", settings.CPUThrottlePercent) + } + + // Test invalid percentage (should be ignored) + err = sm.SetCPUThrottle(true, 150) + if err != nil { + t.Fatalf("Failed to set CPU throttle: %v", err) + } + settings = sm.Get() + if settings.CPUThrottlePercent != 30 { // Should remain unchanged + t.Errorf("Expected CPUThrottlePercent to remain 30, got %d", settings.CPUThrottlePercent) + } +} + +func TestSettingsManager_SetMinerDefaults(t *testing.T) { + tmpDir := t.TempDir() + settingsPath := filepath.Join(tmpDir, "settings.json") + + sm := &SettingsManager{ + settings: DefaultSettings(), + settingsPath: settingsPath, + } + + defaults := MinerDefaults{ + DefaultPool: "stratum+tcp://pool.example.com:3333", + DefaultWallet: "wallet123", + DefaultAlgorithm: "rx/0", + CPUMaxThreadsHint: 25, + CPUThrottleThreshold: 90, + } + + err := sm.SetMinerDefaults(defaults) + if err != nil { + t.Fatalf("Failed to set miner defaults: %v", err) + } + + settings := sm.Get() + if settings.MinerDefaults.DefaultPool != "stratum+tcp://pool.example.com:3333" { + t.Errorf("Expected pool to be set, got %s", settings.MinerDefaults.DefaultPool) + } + if settings.MinerDefaults.CPUMaxThreadsHint != 25 { + t.Errorf("Expected CPUMaxThreadsHint 25, got %d", settings.MinerDefaults.CPUMaxThreadsHint) + } +} + +func TestSettingsManager_ConcurrentAccess(t *testing.T) { + tmpDir := t.TempDir() + settingsPath := filepath.Join(tmpDir, "settings.json") + + sm := &SettingsManager{ + settings: DefaultSettings(), + settingsPath: settingsPath, + } + + // Concurrent reads and writes + done := make(chan bool) + for i := 0; i < 10; i++ { + go func(n int) { + for j := 0; j < 100; j++ { + _ = sm.Get() + sm.UpdateWindowState(n*10, n*10, 800+n, 600+n, false) + } + done <- true + }(i) + } + + // Wait for all goroutines + for i := 0; i < 10; i++ { + <-done + } + + // Should complete without race conditions + state := sm.GetWindowState() + if state.Width < 800 || state.Width > 900 { + t.Errorf("Unexpected width after concurrent access: %d", state.Width) + } +} diff --git a/mining/simulated_miner.go b/mining/simulated_miner.go new file mode 100644 index 0000000..dfffedc --- /dev/null +++ b/mining/simulated_miner.go @@ -0,0 +1,457 @@ +package mining + +import ( + "context" + "fmt" + "math" + "math/rand" + "sync" + "time" +) + +// MinerTypeSimulated is the type identifier for simulated miners. +const MinerTypeSimulated = "simulated" + +// SimulatedMiner is a mock miner that generates realistic-looking stats for UI testing. +type SimulatedMiner struct { + // Exported fields for JSON serialization + Name string `json:"name"` + MinerType string `json:"miner_type"` + Version string `json:"version"` + URL string `json:"url"` + Path string `json:"path"` + MinerBinary string `json:"miner_binary"` + Running bool `json:"running"` + Algorithm string `json:"algorithm"` + HashrateHistory []HashratePoint `json:"hashrateHistory"` + LowResHistory []HashratePoint `json:"lowResHashrateHistory"` + Stats *PerformanceMetrics `json:"stats,omitempty"` + FullStats *XMRigSummary `json:"full_stats,omitempty"` // XMRig-compatible format for UI + + // Internal fields (not exported) + baseHashrate int + peakHashrate int + variance float64 + startTime time.Time + shares int + rejected int + logs []string + mu sync.RWMutex + stopChan chan struct{} + poolName string + difficultyBase int +} + +// SimulatedMinerConfig holds configuration for creating a simulated miner. +type SimulatedMinerConfig struct { + Name string // Miner instance name (e.g., "sim-xmrig-001") + Algorithm string // Algorithm name (e.g., "rx/0", "kawpow", "ethash") + BaseHashrate int // Base hashrate in H/s + Variance float64 // Variance as percentage (0.0-0.2 for 20% variance) + PoolName string // Simulated pool name + Difficulty int // Base difficulty +} + +// NewSimulatedMiner creates a new simulated miner instance. +func NewSimulatedMiner(config SimulatedMinerConfig) *SimulatedMiner { + if config.Variance <= 0 { + config.Variance = 0.1 // Default 10% variance + } + if config.PoolName == "" { + config.PoolName = "sim-pool.example.com:3333" + } + if config.Difficulty <= 0 { + config.Difficulty = 10000 + } + + return &SimulatedMiner{ + Name: config.Name, + MinerType: MinerTypeSimulated, + Version: "1.0.0-simulated", + URL: "https://github.com/simulated/miner", + Path: "/simulated/miner", + MinerBinary: "/simulated/miner/sim-miner", + Algorithm: config.Algorithm, + HashrateHistory: make([]HashratePoint, 0), + LowResHistory: make([]HashratePoint, 0), + baseHashrate: config.BaseHashrate, + variance: config.Variance, + poolName: config.PoolName, + difficultyBase: config.Difficulty, + logs: make([]string, 0), + } +} + +// GetType returns the miner type identifier. +func (m *SimulatedMiner) GetType() string { + return m.MinerType +} + +// Install is a no-op for simulated miners. +func (m *SimulatedMiner) Install() error { + return nil +} + +// Uninstall is a no-op for simulated miners. +func (m *SimulatedMiner) Uninstall() error { + return nil +} + +// Start begins the simulated mining process. +func (m *SimulatedMiner) Start(config *Config) error { + m.mu.Lock() + if m.Running { + m.mu.Unlock() + return fmt.Errorf("simulated miner %s is already running", m.Name) + } + + m.Running = true + m.startTime = time.Now() + m.shares = 0 + m.rejected = 0 + m.stopChan = make(chan struct{}) + m.HashrateHistory = make([]HashratePoint, 0) + m.LowResHistory = make([]HashratePoint, 0) + m.logs = []string{ + fmt.Sprintf("[%s] Simulated miner starting...", time.Now().Format("15:04:05")), + fmt.Sprintf("[%s] Connecting to %s", time.Now().Format("15:04:05"), m.poolName), + fmt.Sprintf("[%s] Pool connected, algorithm: %s", time.Now().Format("15:04:05"), m.Algorithm), + } + m.mu.Unlock() + + // Start background simulation + go m.runSimulation() + + return nil +} + +// Stop stops the simulated miner. +func (m *SimulatedMiner) Stop() error { + m.mu.Lock() + defer m.mu.Unlock() + + if !m.Running { + return fmt.Errorf("simulated miner %s is not running", m.Name) + } + + close(m.stopChan) + m.Running = false + m.logs = append(m.logs, fmt.Sprintf("[%s] Miner stopped", time.Now().Format("15:04:05"))) + + return nil +} + +// runSimulation runs the background simulation loop. +func (m *SimulatedMiner) runSimulation() { + ticker := time.NewTicker(HighResolutionInterval) + defer ticker.Stop() + + shareTicker := time.NewTicker(time.Duration(5+rand.Intn(10)) * time.Second) + defer shareTicker.Stop() + + for { + select { + case <-m.stopChan: + return + case <-ticker.C: + m.updateHashrate() + case <-shareTicker.C: + m.simulateShare() + // Randomize next share time + shareTicker.Reset(time.Duration(5+rand.Intn(15)) * time.Second) + } + } +} + +// updateHashrate generates a new hashrate value with realistic variation. +func (m *SimulatedMiner) updateHashrate() { + m.mu.Lock() + defer m.mu.Unlock() + + // Generate hashrate with variance and smooth transitions + now := time.Now() + uptime := now.Sub(m.startTime).Seconds() + + // Ramp up period (first 30 seconds) + rampFactor := math.Min(1.0, uptime/30.0) + + // Add some sine wave variation for realistic fluctuation + sineVariation := math.Sin(uptime/10) * 0.05 + + // Random noise + noise := (rand.Float64() - 0.5) * 2 * m.variance + + // Calculate final hashrate + hashrate := int(float64(m.baseHashrate) * rampFactor * (1.0 + sineVariation + noise)) + if hashrate < 0 { + hashrate = 0 + } + + point := HashratePoint{ + Timestamp: now, + Hashrate: hashrate, + } + + m.HashrateHistory = append(m.HashrateHistory, point) + + // Track peak hashrate + if hashrate > m.peakHashrate { + m.peakHashrate = hashrate + } + + // Update stats for JSON serialization + uptimeInt := int(uptime) + diffCurrent := m.difficultyBase + rand.Intn(m.difficultyBase/2) + + m.Stats = &PerformanceMetrics{ + Hashrate: hashrate, + Shares: m.shares, + Rejected: m.rejected, + Uptime: uptimeInt, + Algorithm: m.Algorithm, + AvgDifficulty: m.difficultyBase, + DiffCurrent: diffCurrent, + } + + // Update XMRig-compatible full_stats for UI + m.FullStats = &XMRigSummary{ + ID: m.Name, + WorkerID: m.Name, + Uptime: uptimeInt, + Algo: m.Algorithm, + Version: m.Version, + } + m.FullStats.Hashrate.Total = []float64{float64(hashrate)} + m.FullStats.Hashrate.Highest = float64(m.peakHashrate) + m.FullStats.Results.SharesGood = m.shares + m.FullStats.Results.SharesTotal = m.shares + m.rejected + m.FullStats.Results.DiffCurrent = diffCurrent + m.FullStats.Results.AvgTime = 15 + rand.Intn(10) // Simulated avg share time + m.FullStats.Results.HashesTotal = m.shares * diffCurrent + m.FullStats.Connection.Pool = m.poolName + m.FullStats.Connection.Uptime = uptimeInt + m.FullStats.Connection.Diff = diffCurrent + m.FullStats.Connection.Accepted = m.shares + m.FullStats.Connection.Rejected = m.rejected + m.FullStats.Connection.Algo = m.Algorithm + m.FullStats.Connection.Ping = 50 + rand.Intn(50) + + // Trim high-res history to last 5 minutes + cutoff := now.Add(-HighResolutionDuration) + for len(m.HashrateHistory) > 0 && m.HashrateHistory[0].Timestamp.Before(cutoff) { + m.HashrateHistory = m.HashrateHistory[1:] + } +} + +// simulateShare simulates finding a share. +func (m *SimulatedMiner) simulateShare() { + m.mu.Lock() + defer m.mu.Unlock() + + // 2% chance of rejected share + if rand.Float64() < 0.02 { + m.rejected++ + m.logs = append(m.logs, fmt.Sprintf("[%s] Share rejected (stale)", time.Now().Format("15:04:05"))) + } else { + m.shares++ + diff := m.difficultyBase + rand.Intn(m.difficultyBase/2) + m.logs = append(m.logs, fmt.Sprintf("[%s] Share accepted (%d/%d) diff %d", time.Now().Format("15:04:05"), m.shares, m.rejected, diff)) + } + + // Keep last 100 log lines + if len(m.logs) > 100 { + m.logs = m.logs[len(m.logs)-100:] + } +} + +// GetStats returns current performance metrics. +func (m *SimulatedMiner) GetStats(ctx context.Context) (*PerformanceMetrics, error) { + m.mu.RLock() + defer m.mu.RUnlock() + + if !m.Running { + return nil, fmt.Errorf("simulated miner %s is not running", m.Name) + } + + // Calculate current hashrate from recent history + var hashrate int + if len(m.HashrateHistory) > 0 { + hashrate = m.HashrateHistory[len(m.HashrateHistory)-1].Hashrate + } + + uptime := int(time.Since(m.startTime).Seconds()) + + // Calculate average difficulty + avgDiff := m.difficultyBase + if m.shares > 0 { + avgDiff = m.difficultyBase + rand.Intn(m.difficultyBase/4) + } + + return &PerformanceMetrics{ + Hashrate: hashrate, + Shares: m.shares, + Rejected: m.rejected, + Uptime: uptime, + LastShare: time.Now().Unix() - int64(rand.Intn(30)), + Algorithm: m.Algorithm, + AvgDifficulty: avgDiff, + DiffCurrent: m.difficultyBase + rand.Intn(m.difficultyBase/2), + ExtraData: map[string]interface{}{ + "pool": m.poolName, + "simulated": true, + }, + }, nil +} + +// GetName returns the miner's name. +func (m *SimulatedMiner) GetName() string { + return m.Name +} + +// GetPath returns a simulated path. +func (m *SimulatedMiner) GetPath() string { + return m.Path +} + +// GetBinaryPath returns a simulated binary path. +func (m *SimulatedMiner) GetBinaryPath() string { + return m.MinerBinary +} + +// CheckInstallation returns simulated installation details. +func (m *SimulatedMiner) CheckInstallation() (*InstallationDetails, error) { + return &InstallationDetails{ + IsInstalled: true, + Version: "1.0.0-simulated", + Path: "/simulated/miner", + MinerBinary: "simulated-miner", + ConfigPath: "/simulated/config.json", + }, nil +} + +// GetLatestVersion returns a simulated version. +func (m *SimulatedMiner) GetLatestVersion() (string, error) { + return "1.0.0-simulated", nil +} + +// GetHashrateHistory returns the hashrate history. +func (m *SimulatedMiner) GetHashrateHistory() []HashratePoint { + m.mu.RLock() + defer m.mu.RUnlock() + + result := make([]HashratePoint, len(m.HashrateHistory)) + copy(result, m.HashrateHistory) + return result +} + +// AddHashratePoint adds a point to the history. +func (m *SimulatedMiner) AddHashratePoint(point HashratePoint) { + m.mu.Lock() + defer m.mu.Unlock() + m.HashrateHistory = append(m.HashrateHistory, point) +} + +// ReduceHashrateHistory reduces the history (called by manager). +func (m *SimulatedMiner) ReduceHashrateHistory(now time.Time) { + m.mu.Lock() + defer m.mu.Unlock() + + // Move old high-res points to low-res + cutoff := now.Add(-HighResolutionDuration) + var toMove []HashratePoint + + newHistory := make([]HashratePoint, 0) + for _, point := range m.HashrateHistory { + if point.Timestamp.Before(cutoff) { + toMove = append(toMove, point) + } else { + newHistory = append(newHistory, point) + } + } + m.HashrateHistory = newHistory + + // Average the old points and add to low-res + if len(toMove) > 0 { + var sum int + for _, p := range toMove { + sum += p.Hashrate + } + avg := sum / len(toMove) + m.LowResHistory = append(m.LowResHistory, HashratePoint{ + Timestamp: toMove[len(toMove)-1].Timestamp, + Hashrate: avg, + }) + } + + // Trim low-res history + lowResCutoff := now.Add(-LowResHistoryRetention) + newLowRes := make([]HashratePoint, 0) + for _, point := range m.LowResHistory { + if !point.Timestamp.Before(lowResCutoff) { + newLowRes = append(newLowRes, point) + } + } + m.LowResHistory = newLowRes +} + +// GetLogs returns the simulated logs. +func (m *SimulatedMiner) GetLogs() []string { + m.mu.RLock() + defer m.mu.RUnlock() + + result := make([]string, len(m.logs)) + copy(result, m.logs) + return result +} + +// WriteStdin simulates stdin input. +func (m *SimulatedMiner) WriteStdin(input string) error { + m.mu.Lock() + defer m.mu.Unlock() + + if !m.Running { + return fmt.Errorf("simulated miner %s is not running", m.Name) + } + + m.logs = append(m.logs, fmt.Sprintf("[%s] stdin: %s", time.Now().Format("15:04:05"), input)) + return nil +} + +// SimulatedMinerPresets provides common presets for simulated miners. +var SimulatedMinerPresets = map[string]SimulatedMinerConfig{ + "cpu-low": { + Algorithm: "rx/0", + BaseHashrate: 500, + Variance: 0.15, + PoolName: "pool.hashvault.pro:443", + Difficulty: 50000, + }, + "cpu-medium": { + Algorithm: "rx/0", + BaseHashrate: 5000, + Variance: 0.10, + PoolName: "pool.hashvault.pro:443", + Difficulty: 100000, + }, + "cpu-high": { + Algorithm: "rx/0", + BaseHashrate: 15000, + Variance: 0.08, + PoolName: "pool.hashvault.pro:443", + Difficulty: 200000, + }, + "gpu-ethash": { + Algorithm: "ethash", + BaseHashrate: 30000000, // 30 MH/s + Variance: 0.05, + PoolName: "eth.2miners.com:2020", + Difficulty: 4000000000, + }, + "gpu-kawpow": { + Algorithm: "kawpow", + BaseHashrate: 15000000, // 15 MH/s + Variance: 0.06, + PoolName: "rvn.2miners.com:6060", + Difficulty: 1000000000, + }, +} diff --git a/mining/stats_collector.go b/mining/stats_collector.go new file mode 100644 index 0000000..e968e44 --- /dev/null +++ b/mining/stats_collector.go @@ -0,0 +1,57 @@ +package mining + +import ( + "context" + "encoding/json" + "fmt" + "io" + "net/http" +) + +// StatsCollector defines the interface for collecting miner statistics. +// This allows different miner types to implement their own stats collection logic +// while sharing common HTTP fetching infrastructure. +type StatsCollector interface { + // CollectStats fetches and returns performance metrics from the miner. + CollectStats(ctx context.Context) (*PerformanceMetrics, error) +} + +// HTTPStatsConfig holds configuration for HTTP-based stats collection. +type HTTPStatsConfig struct { + Host string + Port int + Endpoint string // e.g., "/2/summary" for XMRig, "/summary" for TT-Miner +} + +// FetchJSONStats performs an HTTP GET request and decodes the JSON response. +// This is a common helper for HTTP-based miner stats collection. +// The caller must provide the target struct to decode into. +func FetchJSONStats[T any](ctx context.Context, config HTTPStatsConfig, target *T) error { + if config.Port == 0 { + return fmt.Errorf("API port is zero") + } + + url := fmt.Sprintf("http://%s:%d%s", config.Host, config.Port, config.Endpoint) + + req, err := http.NewRequestWithContext(ctx, "GET", url, nil) + if err != nil { + return fmt.Errorf("failed to create request: %w", err) + } + + resp, err := getHTTPClient().Do(req) + if err != nil { + return fmt.Errorf("HTTP request failed: %w", err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + io.Copy(io.Discard, resp.Body) // Drain body to allow connection reuse + return fmt.Errorf("unexpected status code %d", resp.StatusCode) + } + + if err := json.NewDecoder(resp.Body).Decode(target); err != nil { + return fmt.Errorf("failed to decode response: %w", err) + } + + return nil +} diff --git a/mining/stats_collector_test.go b/mining/stats_collector_test.go new file mode 100644 index 0000000..dc597ab --- /dev/null +++ b/mining/stats_collector_test.go @@ -0,0 +1,140 @@ +package mining + +import ( + "context" + "encoding/json" + "net" + "net/http" + "net/http/httptest" + "testing" + "time" +) + +func TestFetchJSONStats(t *testing.T) { + t.Run("SuccessfulFetch", func(t *testing.T) { + // Create a test server + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.URL.Path != "/test/endpoint" { + t.Errorf("Unexpected path: %s", r.URL.Path) + } + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(map[string]interface{}{ + "value": 42, + "name": "test", + }) + })) + defer server.Close() + + // Get port from server listener + addr := server.Listener.Addr().(*net.TCPAddr) + + config := HTTPStatsConfig{ + Host: "127.0.0.1", + Port: addr.Port, + Endpoint: "/test/endpoint", + } + + var result struct { + Value int `json:"value"` + Name string `json:"name"` + } + + ctx := context.Background() + err := FetchJSONStats(ctx, config, &result) + if err != nil { + t.Fatalf("FetchJSONStats failed: %v", err) + } + + if result.Value != 42 { + t.Errorf("Expected value 42, got %d", result.Value) + } + if result.Name != "test" { + t.Errorf("Expected name 'test', got '%s'", result.Name) + } + }) + + t.Run("ZeroPort", func(t *testing.T) { + config := HTTPStatsConfig{ + Host: "localhost", + Port: 0, + Endpoint: "/test", + } + + var result map[string]interface{} + err := FetchJSONStats(context.Background(), config, &result) + if err == nil { + t.Error("Expected error for zero port") + } + }) + + t.Run("ContextCancellation", func(t *testing.T) { + config := HTTPStatsConfig{ + Host: "127.0.0.1", + Port: 12345, // Intentionally wrong port to trigger connection timeout + Endpoint: "/test", + } + + ctx, cancel := context.WithTimeout(context.Background(), 1*time.Millisecond) + defer cancel() + + var result map[string]interface{} + err := FetchJSONStats(ctx, config, &result) + if err == nil { + t.Error("Expected error for cancelled context") + } + }) +} + +func TestMinerTypeRegistry(t *testing.T) { + t.Run("KnownTypes", func(t *testing.T) { + if !IsMinerSupported(MinerTypeXMRig) { + t.Error("xmrig should be a known miner type") + } + if !IsMinerSupported(MinerTypeTTMiner) { + t.Error("tt-miner should be a known miner type") + } + if !IsMinerSupported(MinerTypeSimulated) { + t.Error("simulated should be a known miner type") + } + }) + + t.Run("UnknownType", func(t *testing.T) { + if IsMinerSupported("unknown-miner") { + t.Error("unknown-miner should not be a known miner type") + } + }) + + t.Run("ListMinerTypes", func(t *testing.T) { + types := ListMinerTypes() + if len(types) == 0 { + t.Error("ListMinerTypes should return registered types") + } + }) +} + +func TestGetType(t *testing.T) { + t.Run("XMRigMiner", func(t *testing.T) { + miner := NewXMRigMiner() + if miner.GetType() != MinerTypeXMRig { + t.Errorf("Expected type %s, got %s", MinerTypeXMRig, miner.GetType()) + } + }) + + t.Run("TTMiner", func(t *testing.T) { + miner := NewTTMiner() + if miner.GetType() != MinerTypeTTMiner { + t.Errorf("Expected type %s, got %s", MinerTypeTTMiner, miner.GetType()) + } + }) + + t.Run("SimulatedMiner", func(t *testing.T) { + miner := NewSimulatedMiner(SimulatedMinerConfig{ + Name: "test-sim", + Algorithm: "rx/0", + BaseHashrate: 1000, + }) + if miner.GetType() != MinerTypeSimulated { + t.Errorf("Expected type %s, got %s", MinerTypeSimulated, miner.GetType()) + } + }) +} diff --git a/mining/supervisor.go b/mining/supervisor.go new file mode 100644 index 0000000..b00fba8 --- /dev/null +++ b/mining/supervisor.go @@ -0,0 +1,203 @@ +package mining + +import ( + "context" + "sync" + "time" + + "forge.lthn.ai/core/mining/logging" +) + +// TaskFunc is a function that can be supervised. +type TaskFunc func(ctx context.Context) + +// SupervisedTask represents a background task with restart capability. +type SupervisedTask struct { + name string + task TaskFunc + restartDelay time.Duration + maxRestarts int + restartCount int + running bool + lastStartTime time.Time + cancel context.CancelFunc + mu sync.Mutex +} + +// TaskSupervisor manages background tasks with automatic restart on failure. +type TaskSupervisor struct { + tasks map[string]*SupervisedTask + ctx context.Context + cancel context.CancelFunc + wg sync.WaitGroup + mu sync.RWMutex + started bool +} + +// NewTaskSupervisor creates a new task supervisor. +func NewTaskSupervisor() *TaskSupervisor { + ctx, cancel := context.WithCancel(context.Background()) + return &TaskSupervisor{ + tasks: make(map[string]*SupervisedTask), + ctx: ctx, + cancel: cancel, + } +} + +// RegisterTask registers a task for supervision. +// The task will be automatically restarted if it exits or panics. +func (s *TaskSupervisor) RegisterTask(name string, task TaskFunc, restartDelay time.Duration, maxRestarts int) { + s.mu.Lock() + defer s.mu.Unlock() + + s.tasks[name] = &SupervisedTask{ + name: name, + task: task, + restartDelay: restartDelay, + maxRestarts: maxRestarts, + } +} + +// Start starts all registered tasks. +func (s *TaskSupervisor) Start() { + s.mu.Lock() + if s.started { + s.mu.Unlock() + return + } + s.started = true + s.mu.Unlock() + + s.mu.RLock() + for name, task := range s.tasks { + s.startTask(name, task) + } + s.mu.RUnlock() +} + +// startTask starts a single supervised task. +func (s *TaskSupervisor) startTask(name string, st *SupervisedTask) { + st.mu.Lock() + if st.running { + st.mu.Unlock() + return + } + st.running = true + st.lastStartTime = time.Now() + + taskCtx, taskCancel := context.WithCancel(s.ctx) + st.cancel = taskCancel + st.mu.Unlock() + + s.wg.Add(1) + go func() { + defer s.wg.Done() + + for { + select { + case <-s.ctx.Done(): + return + default: + } + + // Run the task with panic recovery + func() { + defer func() { + if r := recover(); r != nil { + logging.Error("supervised task panicked", logging.Fields{ + "task": name, + "panic": r, + }) + } + }() + st.task(taskCtx) + }() + + // Check if we should restart + st.mu.Lock() + st.restartCount++ + shouldRestart := st.restartCount <= st.maxRestarts || st.maxRestarts < 0 + restartDelay := st.restartDelay + st.mu.Unlock() + + if !shouldRestart { + logging.Warn("supervised task reached max restarts", logging.Fields{ + "task": name, + "maxRestart": st.maxRestarts, + }) + return + } + + select { + case <-s.ctx.Done(): + return + case <-time.After(restartDelay): + logging.Info("restarting supervised task", logging.Fields{ + "task": name, + "restartCount": st.restartCount, + }) + } + } + }() + + logging.Info("started supervised task", logging.Fields{"task": name}) +} + +// Stop stops all supervised tasks. +func (s *TaskSupervisor) Stop() { + s.cancel() + s.wg.Wait() + + s.mu.Lock() + s.started = false + for _, task := range s.tasks { + task.mu.Lock() + task.running = false + task.mu.Unlock() + } + s.mu.Unlock() + + logging.Info("task supervisor stopped") +} + +// GetTaskStatus returns the status of a task. +func (s *TaskSupervisor) GetTaskStatus(name string) (running bool, restartCount int, found bool) { + s.mu.RLock() + task, ok := s.tasks[name] + s.mu.RUnlock() + + if !ok { + return false, 0, false + } + + task.mu.Lock() + defer task.mu.Unlock() + return task.running, task.restartCount, true +} + +// GetAllTaskStatuses returns status of all tasks. +func (s *TaskSupervisor) GetAllTaskStatuses() map[string]TaskStatus { + s.mu.RLock() + defer s.mu.RUnlock() + + statuses := make(map[string]TaskStatus, len(s.tasks)) + for name, task := range s.tasks { + task.mu.Lock() + statuses[name] = TaskStatus{ + Name: name, + Running: task.running, + RestartCount: task.restartCount, + LastStart: task.lastStartTime, + } + task.mu.Unlock() + } + return statuses +} + +// TaskStatus contains the status of a supervised task. +type TaskStatus struct { + Name string `json:"name"` + Running bool `json:"running"` + RestartCount int `json:"restartCount"` + LastStart time.Time `json:"lastStart"` +} diff --git a/mining/syslog_unix.go b/mining/syslog_unix.go new file mode 100644 index 0000000..06faa81 --- /dev/null +++ b/mining/syslog_unix.go @@ -0,0 +1,33 @@ +//go:build !windows + +package mining + +import ( + "log/syslog" + + "forge.lthn.ai/core/mining/logging" +) + +var syslogWriter *syslog.Writer + +func init() { + // Initialize syslog writer globally. + // LOG_NOTICE is for normal but significant condition. + // LOG_DAEMON is for system daemons. + // "mining-service" is the tag for the log messages. + var err error + syslogWriter, err = syslog.New(syslog.LOG_NOTICE|syslog.LOG_DAEMON, "mining-service") + if err != nil { + logging.Warn("failed to connect to syslog, syslog logging disabled", logging.Fields{"error": err}) + syslogWriter = nil // Ensure it's nil on failure + } +} + +// logToSyslog sends a message to syslog if available, otherwise falls back to standard log. +func logToSyslog(message string) { + if syslogWriter != nil { + _ = syslogWriter.Notice(message) + } else { + logging.Info(message) + } +} diff --git a/mining/syslog_windows.go b/mining/syslog_windows.go new file mode 100644 index 0000000..0001a0e --- /dev/null +++ b/mining/syslog_windows.go @@ -0,0 +1,15 @@ +//go:build windows + +package mining + +import ( + "forge.lthn.ai/core/mining/logging" +) + +// On Windows, syslog is not available. We'll use a dummy implementation +// that logs to the standard logger. + +// logToSyslog logs a message to the standard logger, mimicking the syslog function's signature. +func logToSyslog(message string) { + logging.Info(message) +} diff --git a/mining/throttle_test.go b/mining/throttle_test.go new file mode 100644 index 0000000..e068cdd --- /dev/null +++ b/mining/throttle_test.go @@ -0,0 +1,314 @@ +package mining + +import ( + "context" + "runtime" + "testing" + "time" + + "github.com/shirou/gopsutil/v4/cpu" + "github.com/shirou/gopsutil/v4/process" +) + +// TestCPUThrottleSingleMiner tests that a single miner respects CPU throttle settings +func TestCPUThrottleSingleMiner(t *testing.T) { + if testing.Short() { + t.Skip("Skipping CPU throttle test in short mode") + } + + miner := NewXMRigMiner() + details, err := miner.CheckInstallation() + if err != nil || !details.IsInstalled { + t.Skip("XMRig not installed, skipping throttle test") + } + + // Use simulation manager to avoid autostart conflicts + manager := NewManagerForSimulation() + defer manager.Stop() + + // Configure miner to use only 10% of CPU + config := &Config{ + Pool: "stratum+tcp://pool.supportxmr.com:3333", + Wallet: "44AFFq5kSiGBoZ4NMDwYtN18obc8AemS33DBLWs3H7otXft3XjrpDtQGv7SqSsaBYBb98uNbr2VBBEt7f2wfn3RVGQBEP3A", + CPUMaxThreadsHint: 10, // 10% CPU usage + Algo: "throttle-single", + } + + minerInstance, err := manager.StartMiner(context.Background(), "xmrig", config) + if err != nil { + t.Fatalf("Failed to start miner: %v", err) + } + t.Logf("Started miner: %s", minerInstance.GetName()) + + // Let miner warm up + time.Sleep(15 * time.Second) + + // Measure CPU usage + avgCPU := measureCPUUsage(t, 10*time.Second) + + t.Logf("Configured: 10%% CPU, Measured: %.1f%% CPU", avgCPU) + + // Allow 15% margin (10% target + 5% tolerance) + if avgCPU > 25 { + t.Errorf("CPU usage %.1f%% exceeds expected ~10%% (with tolerance)", avgCPU) + } + + manager.StopMiner(context.Background(), minerInstance.GetName()) +} + +// TestCPUThrottleDualMiners tests that two miners together respect combined CPU limits +func TestCPUThrottleDualMiners(t *testing.T) { + if testing.Short() { + t.Skip("Skipping CPU throttle test in short mode") + } + + miner1 := NewXMRigMiner() + details, err := miner1.CheckInstallation() + if err != nil || !details.IsInstalled { + t.Skip("XMRig not installed, skipping throttle test") + } + + // Use simulation manager to avoid autostart conflicts + manager := NewManagerForSimulation() + defer manager.Stop() + + // Start first miner at 10% CPU with RandomX + config1 := &Config{ + Pool: "stratum+tcp://pool.supportxmr.com:3333", + Wallet: "44AFFq5kSiGBoZ4NMDwYtN18obc8AemS33DBLWs3H7otXft3XjrpDtQGv7SqSsaBYBb98uNbr2VBBEt7f2wfn3RVGQBEP3A", + CPUMaxThreadsHint: 10, + Algo: "throttle-dual-1", + } + + miner1Instance, err := manager.StartMiner(context.Background(), "xmrig", config1) + if err != nil { + t.Fatalf("Failed to start first miner: %v", err) + } + t.Logf("Started miner 1: %s", miner1Instance.GetName()) + + // Start second miner at 10% CPU with different algo + config2 := &Config{ + Pool: "stratum+tcp://pool.supportxmr.com:5555", + Wallet: "44AFFq5kSiGBoZ4NMDwYtN18obc8AemS33DBLWs3H7otXft3XjrpDtQGv7SqSsaBYBb98uNbr2VBBEt7f2wfn3RVGQBEP3A", + CPUMaxThreadsHint: 10, + Algo: "throttle-dual-2", + } + + miner2Instance, err := manager.StartMiner(context.Background(), "xmrig", config2) + if err != nil { + t.Fatalf("Failed to start second miner: %v", err) + } + t.Logf("Started miner 2: %s", miner2Instance.GetName()) + + // Let miners warm up + time.Sleep(20 * time.Second) + + // Verify both miners are running + miners := manager.ListMiners() + if len(miners) != 2 { + t.Fatalf("Expected 2 miners running, got %d", len(miners)) + } + + // Measure combined CPU usage + avgCPU := measureCPUUsage(t, 15*time.Second) + + t.Logf("Configured: 2x10%% CPU, Measured: %.1f%% CPU", avgCPU) + + // Combined should be ~20% with tolerance + if avgCPU > 40 { + t.Errorf("Combined CPU usage %.1f%% exceeds expected ~20%% (with tolerance)", avgCPU) + } + + // Clean up + manager.StopMiner(context.Background(), miner1Instance.GetName()) + manager.StopMiner(context.Background(), miner2Instance.GetName()) +} + +// TestCPUThrottleThreadCount tests thread-based CPU limiting +func TestCPUThrottleThreadCount(t *testing.T) { + if testing.Short() { + t.Skip("Skipping CPU throttle test in short mode") + } + + miner := NewXMRigMiner() + details, err := miner.CheckInstallation() + if err != nil || !details.IsInstalled { + t.Skip("XMRig not installed, skipping throttle test") + } + + // Use simulation manager to avoid autostart conflicts + manager := NewManagerForSimulation() + defer manager.Stop() + + numCPU := runtime.NumCPU() + targetThreads := 1 // Use only 1 thread + expectedMaxCPU := float64(100) / float64(numCPU) * float64(targetThreads) * 1.5 // 50% tolerance + + config := &Config{ + Pool: "stratum+tcp://pool.supportxmr.com:3333", + Wallet: "44AFFq5kSiGBoZ4NMDwYtN18obc8AemS33DBLWs3H7otXft3XjrpDtQGv7SqSsaBYBb98uNbr2VBBEt7f2wfn3RVGQBEP3A", + Threads: targetThreads, + Algo: "throttle-thread", + } + + minerInstance, err := manager.StartMiner(context.Background(), "xmrig", config) + if err != nil { + t.Fatalf("Failed to start miner: %v", err) + } + t.Logf("Started miner: %s", minerInstance.GetName()) + defer manager.StopMiner(context.Background(), minerInstance.GetName()) + + // Let miner warm up + time.Sleep(15 * time.Second) + + avgCPU := measureCPUUsage(t, 10*time.Second) + + t.Logf("CPUs: %d, Threads: %d, Expected max: %.1f%%, Measured: %.1f%%", + numCPU, targetThreads, expectedMaxCPU, avgCPU) + + if avgCPU > expectedMaxCPU { + t.Errorf("CPU usage %.1f%% exceeds expected max %.1f%% for %d thread(s)", + avgCPU, expectedMaxCPU, targetThreads) + } +} + +// TestMinerResourceIsolation tests that miners don't interfere with each other +func TestMinerResourceIsolation(t *testing.T) { + if testing.Short() { + t.Skip("Skipping resource isolation test in short mode") + } + + miner := NewXMRigMiner() + details, err := miner.CheckInstallation() + if err != nil || !details.IsInstalled { + t.Skip("XMRig not installed, skipping test") + } + + // Use simulation manager to avoid autostart conflicts + manager := NewManagerForSimulation() + defer manager.Stop() + + // Start first miner + config1 := &Config{ + Pool: "stratum+tcp://pool.supportxmr.com:3333", + Wallet: "44AFFq5kSiGBoZ4NMDwYtN18obc8AemS33DBLWs3H7otXft3XjrpDtQGv7SqSsaBYBb98uNbr2VBBEt7f2wfn3RVGQBEP3A", + CPUMaxThreadsHint: 25, + Algo: "isolation-1", + } + + miner1, err := manager.StartMiner(context.Background(), "xmrig", config1) + if err != nil { + t.Fatalf("Failed to start miner 1: %v", err) + } + + time.Sleep(10 * time.Second) + + // Get baseline hashrate for miner 1 alone + stats1Alone, err := miner1.GetStats(context.Background()) + if err != nil { + t.Logf("Warning: couldn't get stats for miner 1: %v", err) + } + baselineHashrate := 0 + if stats1Alone != nil { + baselineHashrate = stats1Alone.Hashrate + } + + // Start second miner + config2 := &Config{ + Pool: "stratum+tcp://pool.supportxmr.com:5555", + Wallet: "44AFFq5kSiGBoZ4NMDwYtN18obc8AemS33DBLWs3H7otXft3XjrpDtQGv7SqSsaBYBb98uNbr2VBBEt7f2wfn3RVGQBEP3A", + CPUMaxThreadsHint: 25, + Algo: "isolation-2", + } + + miner2, err := manager.StartMiner(context.Background(), "xmrig", config2) + if err != nil { + t.Fatalf("Failed to start miner 2: %v", err) + } + + time.Sleep(15 * time.Second) + + // Check both miners are running and producing hashrate + stats1, err := miner1.GetStats(context.Background()) + if err != nil { + t.Logf("Warning: couldn't get stats for miner 1: %v", err) + } + stats2, err := miner2.GetStats(context.Background()) + if err != nil { + t.Logf("Warning: couldn't get stats for miner 2: %v", err) + } + + t.Logf("Miner 1 baseline: %d H/s, with miner 2: %d H/s", baselineHashrate, getHashrate(stats1)) + t.Logf("Miner 2 hashrate: %d H/s", getHashrate(stats2)) + + // Both miners should be producing some hashrate + if stats1 != nil && stats1.Hashrate == 0 { + t.Error("Miner 1 has zero hashrate") + } + if stats2 != nil && stats2.Hashrate == 0 { + t.Error("Miner 2 has zero hashrate") + } + + // Clean up + manager.StopMiner(context.Background(), miner1.GetName()) + manager.StopMiner(context.Background(), miner2.GetName()) +} + +// measureCPUUsage measures average CPU usage over a duration +func measureCPUUsage(t *testing.T, duration time.Duration) float64 { + t.Helper() + + samples := int(duration.Seconds()) + if samples < 1 { + samples = 1 + } + + var totalCPU float64 + for i := 0; i < samples; i++ { + percentages, err := cpu.Percent(time.Second, false) + if err != nil { + t.Logf("Warning: failed to get CPU percentage: %v", err) + continue + } + if len(percentages) > 0 { + totalCPU += percentages[0] + } + } + + return totalCPU / float64(samples) +} + +// measureProcessCPU measures CPU usage of a specific process +func measureProcessCPU(t *testing.T, pid int32, duration time.Duration) float64 { + t.Helper() + + proc, err := process.NewProcess(pid) + if err != nil { + t.Logf("Warning: failed to get process: %v", err) + return 0 + } + + samples := int(duration.Seconds()) + if samples < 1 { + samples = 1 + } + + var totalCPU float64 + for i := 0; i < samples; i++ { + pct, err := proc.CPUPercent() + if err != nil { + continue + } + totalCPU += pct + time.Sleep(time.Second) + } + + return totalCPU / float64(samples) +} + +func getHashrate(stats *PerformanceMetrics) int { + if stats == nil { + return 0 + } + return stats.Hashrate +} diff --git a/mining/ttminer.go b/mining/ttminer.go new file mode 100644 index 0000000..647e854 --- /dev/null +++ b/mining/ttminer.go @@ -0,0 +1,187 @@ +package mining + +import ( + "bytes" + "errors" + "fmt" + "os" + "os/exec" + "path/filepath" + "runtime" + "strings" + "time" +) + +// TTMiner represents a TT-Miner (GPU miner), embedding the BaseMiner for common functionality. +type TTMiner struct { + BaseMiner + FullStats *TTMinerSummary `json:"-"` // Excluded from JSON to prevent race during marshaling +} + +// TTMinerSummary represents the stats response from TT-Miner API +type TTMinerSummary struct { + Name string `json:"name"` + Version string `json:"version"` + Uptime int `json:"uptime"` + Algo string `json:"algo"` + GPUs []struct { + Name string `json:"name"` + ID int `json:"id"` + Hashrate float64 `json:"hashrate"` + Temp int `json:"temp"` + Fan int `json:"fan"` + Power int `json:"power"` + Accepted int `json:"accepted"` + Rejected int `json:"rejected"` + Intensity float64 `json:"intensity"` + } `json:"gpus"` + Results struct { + SharesGood int `json:"shares_good"` + SharesTotal int `json:"shares_total"` + AvgTime int `json:"avg_time"` + } `json:"results"` + Connection struct { + Pool string `json:"pool"` + Ping int `json:"ping"` + Diff int `json:"diff"` + } `json:"connection"` + Hashrate struct { + Total []float64 `json:"total"` + Highest float64 `json:"highest"` + } `json:"hashrate"` +} + +// MinerTypeTTMiner is the type identifier for TT-Miner miners. +const MinerTypeTTMiner = "tt-miner" + +// NewTTMiner creates a new TT-Miner instance with default settings. +func NewTTMiner() *TTMiner { + return &TTMiner{ + BaseMiner: BaseMiner{ + Name: "tt-miner", + MinerType: MinerTypeTTMiner, + ExecutableName: "TT-Miner", + Version: "latest", + URL: "https://github.com/TrailingStop/TT-Miner-release", + API: &API{ + Enabled: true, + ListenHost: "127.0.0.1", + ListenPort: 4068, // TT-Miner default port + }, + HashrateHistory: make([]HashratePoint, 0), + LowResHashrateHistory: make([]HashratePoint, 0), + LastLowResAggregation: time.Now(), + LogBuffer: NewLogBuffer(500), // Keep last 500 lines + }, + } +} + +// getTTMinerConfigPath returns the platform-specific path for the tt-miner config file. +func getTTMinerConfigPath() (string, error) { + homeDir, err := os.UserHomeDir() + if err != nil { + return "", err + } + return filepath.Join(homeDir, ".config", "lethean-desktop", "tt-miner.json"), nil +} + +// GetLatestVersion fetches the latest version of TT-Miner from the GitHub API. +func (m *TTMiner) GetLatestVersion() (string, error) { + return FetchLatestGitHubVersion("TrailingStop", "TT-Miner-release") +} + +// Install determines the correct download URL for the latest version of TT-Miner +// and then calls the generic InstallFromURL method on the BaseMiner. +func (m *TTMiner) Install() error { + version, err := m.GetLatestVersion() + if err != nil { + return err + } + m.Version = version + + var url string + switch runtime.GOOS { + case "windows": + // Windows version - uses .zip + url = fmt.Sprintf("https://github.com/TrailingStop/TT-Miner-release/releases/download/%s/TT-Miner-%s.zip", version, version) + case "linux": + // Linux version - uses .tar.gz + url = fmt.Sprintf("https://github.com/TrailingStop/TT-Miner-release/releases/download/%s/TT-Miner-%s.tar.gz", version, version) + default: + return errors.New("TT-Miner is only available for Windows and Linux (requires CUDA)") + } + + if err := m.InstallFromURL(url); err != nil { + return err + } + + // After installation, verify it. + _, err = m.CheckInstallation() + if err != nil { + return fmt.Errorf("failed to verify installation after extraction: %w", err) + } + + return nil +} + +// Uninstall removes all files related to the TT-Miner, including its specific config file. +func (m *TTMiner) Uninstall() error { + // Remove the specific tt-miner config file + configPath, err := getTTMinerConfigPath() + if err == nil { + os.Remove(configPath) // Ignore error if it doesn't exist + } + + // Call the base uninstall method to remove the installation directory + return m.BaseMiner.Uninstall() +} + +// CheckInstallation verifies if the TT-Miner is installed correctly. +// Thread-safe: properly locks before modifying shared fields. +func (m *TTMiner) CheckInstallation() (*InstallationDetails, error) { + binaryPath, err := m.findMinerBinary() + if err != nil { + return &InstallationDetails{IsInstalled: false}, err + } + + // Run version command before acquiring lock (I/O operation) + cmd := exec.Command(binaryPath, "--version") + var out bytes.Buffer + cmd.Stdout = &out + var version string + if err := cmd.Run(); err != nil { + version = "Unknown (could not run executable)" + } else { + // Parse version from output + output := strings.TrimSpace(out.String()) + fields := strings.Fields(output) + if len(fields) >= 2 { + version = fields[1] + } else if len(fields) >= 1 { + version = fields[0] + } else { + version = "Unknown (could not parse version)" + } + } + + // Get the config path using the helper + configPath, err := getTTMinerConfigPath() + if err != nil { + configPath = "Error: Could not determine config path" + } + + // Update shared fields under lock + m.mu.Lock() + m.MinerBinary = binaryPath + m.Path = filepath.Dir(binaryPath) + m.Version = version + m.mu.Unlock() + + return &InstallationDetails{ + IsInstalled: true, + MinerBinary: binaryPath, + Path: filepath.Dir(binaryPath), + Version: version, + ConfigPath: configPath, + }, nil +} diff --git a/mining/ttminer_start.go b/mining/ttminer_start.go new file mode 100644 index 0000000..c881745 --- /dev/null +++ b/mining/ttminer_start.go @@ -0,0 +1,235 @@ +package mining + +import ( + "errors" + "fmt" + "io" + "os" + "os/exec" + "strings" + "time" + + "forge.lthn.ai/core/mining/logging" +) + +// Start launches the TT-Miner with the given configuration. +func (m *TTMiner) Start(config *Config) error { + // Check installation BEFORE acquiring lock (CheckInstallation takes its own locks) + m.mu.RLock() + needsInstallCheck := m.MinerBinary == "" + m.mu.RUnlock() + + if needsInstallCheck { + if _, err := m.CheckInstallation(); err != nil { + return err // Propagate the detailed error from CheckInstallation + } + } + + m.mu.Lock() + defer m.mu.Unlock() + + if m.Running { + return errors.New("miner is already running") + } + + if m.API != nil && config.HTTPPort != 0 { + m.API.ListenPort = config.HTTPPort + } else if m.API != nil && m.API.ListenPort == 0 { + return errors.New("miner API port not assigned") + } + + // Build command line arguments for TT-Miner + args := m.buildArgs(config) + + logging.Info("executing TT-Miner command", logging.Fields{"binary": m.MinerBinary, "args": strings.Join(args, " ")}) + + m.cmd = exec.Command(m.MinerBinary, args...) + + // Create stdin pipe for console commands + stdinPipe, err := m.cmd.StdinPipe() + if err != nil { + return fmt.Errorf("failed to create stdin pipe: %w", err) + } + m.stdinPipe = stdinPipe + + // Always capture output to LogBuffer + if m.LogBuffer != nil { + m.cmd.Stdout = m.LogBuffer + m.cmd.Stderr = m.LogBuffer + } + // Also output to console if requested + if config.LogOutput { + m.cmd.Stdout = io.MultiWriter(m.LogBuffer, os.Stdout) + m.cmd.Stderr = io.MultiWriter(m.LogBuffer, os.Stderr) + } + + if err := m.cmd.Start(); err != nil { + stdinPipe.Close() + return fmt.Errorf("failed to start TT-Miner: %w", err) + } + + m.Running = true + + // Capture cmd locally to avoid race with Stop() + cmd := m.cmd + go func() { + // Use a channel to detect if Wait() completes + done := make(chan error, 1) + go func() { + done <- cmd.Wait() + }() + + // Wait with timeout to prevent goroutine leak on zombie processes + var err error + select { + case err = <-done: + // Normal exit + case <-time.After(5 * time.Minute): + // Process didn't exit after 5 minutes - force cleanup + logging.Warn("TT-Miner process wait timeout, forcing cleanup") + if cmd.Process != nil { + cmd.Process.Kill() + } + // Wait for inner goroutine with secondary timeout to prevent leak + select { + case err = <-done: + // Inner goroutine completed + case <-time.After(10 * time.Second): + logging.Error("TT-Miner process cleanup timed out after kill", logging.Fields{"miner": m.Name}) + err = nil + } + } + + m.mu.Lock() + // Only clear if this is still the same command (not restarted) + if m.cmd == cmd { + m.Running = false + m.cmd = nil + } + m.mu.Unlock() + if err != nil { + logging.Debug("TT-Miner exited with error", logging.Fields{"error": err}) + } else { + logging.Debug("TT-Miner exited normally") + } + }() + + return nil +} + +// buildArgs constructs the command line arguments for TT-Miner +func (m *TTMiner) buildArgs(config *Config) []string { + var args []string + + // Pool configuration + if config.Pool != "" { + args = append(args, "-P", config.Pool) + } + + // Wallet/user configuration + if config.Wallet != "" { + args = append(args, "-u", config.Wallet) + } + + // Password + if config.Password != "" { + args = append(args, "-p", config.Password) + } else { + args = append(args, "-p", "x") + } + + // Algorithm selection + if config.Algo != "" { + args = append(args, "-a", config.Algo) + } + + // API binding for stats collection + if m.API != nil && m.API.Enabled { + args = append(args, "-b", fmt.Sprintf("%s:%d", m.API.ListenHost, m.API.ListenPort)) + } + + // GPU device selection (if specified) + if config.Devices != "" { + args = append(args, "-d", config.Devices) + } + + // Intensity (if specified) + if config.Intensity > 0 { + args = append(args, "-i", fmt.Sprintf("%d", config.Intensity)) + } + + // Additional CLI arguments + addTTMinerCliArgs(config, &args) + + return args +} + +// addTTMinerCliArgs adds any additional CLI arguments from config +func addTTMinerCliArgs(config *Config, args *[]string) { + // Add any extra arguments passed via CLIArgs + if config.CLIArgs != "" { + extraArgs := strings.Fields(config.CLIArgs) + for _, arg := range extraArgs { + // Skip potentially dangerous arguments + if isValidCLIArg(arg) { + *args = append(*args, arg) + } else { + logging.Warn("skipping invalid CLI argument", logging.Fields{"arg": arg}) + } + } + } +} + +// isValidCLIArg validates CLI arguments to prevent injection or dangerous patterns. +// Uses a combination of allowlist patterns and blocklist for security. +func isValidCLIArg(arg string) bool { + // Empty or whitespace-only args are invalid + if strings.TrimSpace(arg) == "" { + return false + } + + // Must start with dash (standard CLI argument format) + // This is an allowlist approach - only accept valid argument patterns + if !strings.HasPrefix(arg, "-") { + // Allow values for flags (e.g., the "3" in "-i 3") + // Values must not contain shell metacharacters + return isValidArgValue(arg) + } + + // Block shell metacharacters and dangerous patterns + if !isValidArgValue(arg) { + return false + } + + // Block arguments that could override security-related settings + blockedPrefixes := []string{ + "--api-access-token", "--api-worker-id", // TT-Miner API settings + "--config", // Could load arbitrary config + "--log-file", // Could write to arbitrary locations + "--coin-file", // Could load arbitrary coin configs + "-o", "--out", // Output redirection + } + lowerArg := strings.ToLower(arg) + for _, blocked := range blockedPrefixes { + if lowerArg == blocked || strings.HasPrefix(lowerArg, blocked+"=") { + return false + } + } + + return true +} + +// isValidArgValue checks if a value contains dangerous patterns +func isValidArgValue(arg string) bool { + // Block shell metacharacters and command injection patterns + dangerousPatterns := []string{ + ";", "|", "&", "`", "$", "(", ")", "{", "}", + "<", ">", "\n", "\r", "\\", "'", "\"", "!", + } + for _, p := range dangerousPatterns { + if strings.Contains(arg, p) { + return false + } + } + return true +} diff --git a/mining/ttminer_stats.go b/mining/ttminer_stats.go new file mode 100644 index 0000000..752f78a --- /dev/null +++ b/mining/ttminer_stats.go @@ -0,0 +1,66 @@ +package mining + +import ( + "context" + "errors" +) + +// GetStats retrieves performance metrics from the TT-Miner API. +func (m *TTMiner) GetStats(ctx context.Context) (*PerformanceMetrics, error) { + // Read state under RLock, then release before HTTP call + m.mu.RLock() + if !m.Running { + m.mu.RUnlock() + return nil, errors.New("miner is not running") + } + if m.API == nil || m.API.ListenPort == 0 { + m.mu.RUnlock() + return nil, errors.New("miner API not configured or port is zero") + } + config := HTTPStatsConfig{ + Host: m.API.ListenHost, + Port: m.API.ListenPort, + Endpoint: "/summary", + } + m.mu.RUnlock() + + // Create request with context and timeout + reqCtx, cancel := context.WithTimeout(ctx, statsTimeout) + defer cancel() + + // Use the common HTTP stats fetcher + var summary TTMinerSummary + if err := FetchJSONStats(reqCtx, config, &summary); err != nil { + return nil, err + } + + // Store the full summary in the miner struct (requires lock) + m.mu.Lock() + m.FullStats = &summary + m.mu.Unlock() + + // Calculate total hashrate from all GPUs + var totalHashrate float64 + if len(summary.Hashrate.Total) > 0 { + totalHashrate = summary.Hashrate.Total[0] + } else { + // Sum individual GPU hashrates + for _, gpu := range summary.GPUs { + totalHashrate += gpu.Hashrate + } + } + + // For TT-Miner, we use the connection difficulty as both current and avg + // since TT-Miner doesn't expose per-share difficulty data + diffCurrent := summary.Connection.Diff + + return &PerformanceMetrics{ + Hashrate: int(totalHashrate), + Shares: summary.Results.SharesGood, + Rejected: summary.Results.SharesTotal - summary.Results.SharesGood, + Uptime: summary.Uptime, + Algorithm: summary.Algo, + AvgDifficulty: diffCurrent, // Use pool diff as approximation + DiffCurrent: diffCurrent, + }, nil +} diff --git a/mining/version.go b/mining/version.go new file mode 100644 index 0000000..2e2c8c8 --- /dev/null +++ b/mining/version.go @@ -0,0 +1,89 @@ +package mining + +import ( + "encoding/json" + "fmt" + "io" + "net/http" +) + +var ( + version = "dev" + commit = "none" + date = "unknown" +) + +// GetVersion returns the version of the application +func GetVersion() string { + return version +} + +// GetCommit returns the git commit hash +func GetCommit() string { + return commit +} + +// GetBuildDate returns the build date +func GetBuildDate() string { + return date +} + +// GitHubRelease represents the structure of a GitHub release response. +type GitHubRelease struct { + TagName string `json:"tag_name"` + Name string `json:"name"` +} + +// FetchLatestGitHubVersion fetches the latest release version from a GitHub repository. +// It takes the repository owner and name (e.g., "xmrig", "xmrig") and returns the tag name. +// Uses a circuit breaker to prevent cascading failures when GitHub API is unavailable. +func FetchLatestGitHubVersion(owner, repo string) (string, error) { + cb := getGitHubCircuitBreaker() + + result, err := cb.Execute(func() (interface{}, error) { + return fetchGitHubVersionDirect(owner, repo) + }) + + if err != nil { + // If circuit is open, try to return cached value with warning + if err == ErrCircuitOpen { + if cached, ok := cb.GetCached(); ok { + if tagName, ok := cached.(string); ok { + return tagName, nil + } + } + return "", fmt.Errorf("github API unavailable (circuit breaker open): %w", err) + } + return "", err + } + + tagName, ok := result.(string) + if !ok { + return "", fmt.Errorf("unexpected result type from circuit breaker") + } + + return tagName, nil +} + +// fetchGitHubVersionDirect is the actual GitHub API call, wrapped by circuit breaker +func fetchGitHubVersionDirect(owner, repo string) (string, error) { + url := fmt.Sprintf("https://api.github.com/repos/%s/%s/releases/latest", owner, repo) + + resp, err := getHTTPClient().Get(url) + if err != nil { + return "", fmt.Errorf("failed to fetch version: %w", err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + io.Copy(io.Discard, resp.Body) // Drain body to allow connection reuse + return "", fmt.Errorf("failed to get latest release: unexpected status code %d", resp.StatusCode) + } + + var release GitHubRelease + if err := json.NewDecoder(resp.Body).Decode(&release); err != nil { + return "", fmt.Errorf("failed to decode release: %w", err) + } + + return release.TagName, nil +} diff --git a/mining/xmrig.go b/mining/xmrig.go new file mode 100644 index 0000000..39f7c0e --- /dev/null +++ b/mining/xmrig.go @@ -0,0 +1,199 @@ +package mining + +import ( + "bytes" + "errors" + "fmt" + "net/http" + "os" + "os/exec" + "path/filepath" + "runtime" + "strings" + "sync" + "time" + + "github.com/adrg/xdg" +) + +// XMRigMiner represents an XMRig miner, embedding the BaseMiner for common functionality. +type XMRigMiner struct { + BaseMiner + FullStats *XMRigSummary `json:"-"` // Excluded from JSON to prevent race during marshaling +} + +var ( + httpClient = &http.Client{ + Timeout: 30 * time.Second, + Transport: &http.Transport{ + MaxIdleConns: 100, + MaxIdleConnsPerHost: 10, + IdleConnTimeout: 90 * time.Second, + }, + } + httpClientMu sync.RWMutex +) + +// getHTTPClient returns the HTTP client with proper synchronization +func getHTTPClient() *http.Client { + httpClientMu.RLock() + defer httpClientMu.RUnlock() + return httpClient +} + +// setHTTPClient sets the HTTP client (for testing) +func setHTTPClient(client *http.Client) { + httpClientMu.Lock() + defer httpClientMu.Unlock() + httpClient = client +} + +// MinerTypeXMRig is the type identifier for XMRig miners. +// Note: This type now supports the Miner Platform binary ("miner") as the default. +const MinerTypeXMRig = "xmrig" + +// NewXMRigMiner creates a new XMRig miner instance with default settings. +// The executable name defaults to "miner" (Miner Platform) but can also find "xmrig". +func NewXMRigMiner() *XMRigMiner { + return &XMRigMiner{ + BaseMiner: BaseMiner{ + Name: "miner", + MinerType: MinerTypeXMRig, + ExecutableName: "miner", + Version: "latest", + URL: "", // Local build only - no remote download + API: &API{ + Enabled: true, + ListenHost: "127.0.0.1", + }, + HashrateHistory: make([]HashratePoint, 0), + LowResHashrateHistory: make([]HashratePoint, 0), + LastLowResAggregation: time.Now(), + LogBuffer: NewLogBuffer(500), // Keep last 500 lines + }, + } +} + +// getXMRigConfigPath returns the platform-specific path for the xmrig.json file. +// If instanceName is provided, it creates an instance-specific config file. +// This is a variable so it can be overridden in tests. +var getXMRigConfigPath = func(instanceName string) (string, error) { + configFileName := "xmrig.json" + if instanceName != "" && instanceName != "xmrig" { + // Use instance-specific config file (e.g., xmrig-78.json) + configFileName = instanceName + ".json" + } + + path, err := xdg.ConfigFile("lethean-desktop/" + configFileName) + if err != nil { + // Fallback for non-XDG environments or when XDG variables are not set + homeDir, homeErr := os.UserHomeDir() + if homeErr != nil { + return "", homeErr + } + return filepath.Join(homeDir, ".config", "lethean-desktop", configFileName), nil + } + return path, nil +} + +// GetLatestVersion fetches the latest version of XMRig from the GitHub API. +func (m *XMRigMiner) GetLatestVersion() (string, error) { + return FetchLatestGitHubVersion("xmrig", "xmrig") +} + +// Install determines the correct download URL for the latest version of XMRig +// and then calls the generic InstallFromURL method on the BaseMiner. +func (m *XMRigMiner) Install() error { + version, err := m.GetLatestVersion() + if err != nil { + return err + } + m.Version = version + + var url string + switch runtime.GOOS { + case "windows": + url = fmt.Sprintf("https://github.com/xmrig/xmrig/releases/download/%s/xmrig-%s-windows-x64.zip", version, strings.TrimPrefix(version, "v")) + case "linux": + url = fmt.Sprintf("https://github.com/xmrig/xmrig/releases/download/%s/xmrig-%s-linux-static-x64.tar.gz", version, strings.TrimPrefix(version, "v")) + case "darwin": + url = fmt.Sprintf("https://github.com/xmrig/xmrig/releases/download/%s/xmrig-%s-macos-x64.tar.gz", version, strings.TrimPrefix(version, "v")) + default: + return errors.New("unsupported operating system") + } + + if err := m.InstallFromURL(url); err != nil { + return err + } + + // After installation, verify it. + _, err = m.CheckInstallation() + if err != nil { + return fmt.Errorf("failed to verify installation after extraction: %w", err) + } + + return nil +} + +// Uninstall removes all files related to the XMRig miner, including its specific config file. +func (m *XMRigMiner) Uninstall() error { + // Remove the instance-specific config file + configPath, err := getXMRigConfigPath(m.Name) + if err == nil { + os.Remove(configPath) // Ignore error if it doesn't exist + } + + // Call the base uninstall method to remove the installation directory + return m.BaseMiner.Uninstall() +} + +// CheckInstallation verifies if the XMRig miner is installed correctly. +// Thread-safe: properly locks before modifying shared fields. +func (m *XMRigMiner) CheckInstallation() (*InstallationDetails, error) { + binaryPath, err := m.findMinerBinary() + if err != nil { + return &InstallationDetails{IsInstalled: false}, err + } + + // Run version command before acquiring lock (I/O operation) + cmd := exec.Command(binaryPath, "--version") + var out bytes.Buffer + cmd.Stdout = &out + var version string + if err := cmd.Run(); err != nil { + version = "Unknown (could not run executable)" + } else { + fields := strings.Fields(out.String()) + if len(fields) >= 2 { + version = fields[1] + } else { + version = "Unknown (could not parse version)" + } + } + + // Get the config path using the helper (use instance name if set) + m.mu.RLock() + instanceName := m.Name + m.mu.RUnlock() + + configPath, err := getXMRigConfigPath(instanceName) + if err != nil { + // Log the error but don't fail CheckInstallation if config path can't be determined + configPath = "Error: Could not determine config path" + } + + // Update shared fields under lock + m.mu.Lock() + m.MinerBinary = binaryPath + m.Path = filepath.Dir(binaryPath) + m.Version = version + m.mu.Unlock() + + return &InstallationDetails{ + IsInstalled: true, + MinerBinary: binaryPath, + Path: filepath.Dir(binaryPath), + Version: version, + ConfigPath: configPath, + }, nil +} diff --git a/mining/xmrig_gpu_test.go b/mining/xmrig_gpu_test.go new file mode 100644 index 0000000..a158b11 --- /dev/null +++ b/mining/xmrig_gpu_test.go @@ -0,0 +1,241 @@ +package mining + +import ( + "encoding/json" + "os" + "path/filepath" + "testing" +) + +func TestXMRigDualMiningConfig(t *testing.T) { + // Create a temp directory for the config + tmpDir := t.TempDir() + + miner := &XMRigMiner{ + BaseMiner: BaseMiner{ + Name: "xmrig-dual-test", + API: &API{ + Enabled: true, + ListenHost: "127.0.0.1", + ListenPort: 12345, + }, + }, + } + + // Temporarily override config path + origGetPath := getXMRigConfigPath + getXMRigConfigPath = func(name string) (string, error) { + return filepath.Join(tmpDir, name+".json"), nil + } + defer func() { getXMRigConfigPath = origGetPath }() + + // Config with CPU mining rx/0 and GPU mining kawpow on different pools + config := &Config{ + // CPU config + Pool: "stratum+tcp://pool.supportxmr.com:3333", + Wallet: "cpu_wallet_address", + Algo: "rx/0", + CPUMaxThreadsHint: 50, + + // GPU config - separate pool and algo + // MUST specify Devices explicitly - no auto-picking! + GPUEnabled: true, + GPUPool: "stratum+tcp://ravencoin.pool.com:3333", + GPUWallet: "gpu_wallet_address", + GPUAlgo: "kawpow", + CUDA: true, // NVIDIA + OpenCL: false, + Devices: "0", // Explicit device selection required + } + + err := miner.createConfig(config) + if err != nil { + t.Fatalf("Failed to create config: %v", err) + } + + // Read and parse the generated config + data, err := os.ReadFile(miner.ConfigPath) + if err != nil { + t.Fatalf("Failed to read config: %v", err) + } + + var generatedConfig map[string]interface{} + if err := json.Unmarshal(data, &generatedConfig); err != nil { + t.Fatalf("Failed to parse config: %v", err) + } + + // Verify pools + pools, ok := generatedConfig["pools"].([]interface{}) + if !ok { + t.Fatal("pools not found in config") + } + if len(pools) != 2 { + t.Errorf("Expected 2 pools (CPU + GPU), got %d", len(pools)) + } + + // Verify CPU pool + cpuPool := pools[0].(map[string]interface{}) + if cpuPool["url"] != "stratum+tcp://pool.supportxmr.com:3333" { + t.Errorf("CPU pool URL mismatch: %v", cpuPool["url"]) + } + if cpuPool["user"] != "cpu_wallet_address" { + t.Errorf("CPU wallet mismatch: %v", cpuPool["user"]) + } + if cpuPool["algo"] != "rx/0" { + t.Errorf("CPU algo mismatch: %v", cpuPool["algo"]) + } + + // Verify GPU pool + gpuPool := pools[1].(map[string]interface{}) + if gpuPool["url"] != "stratum+tcp://ravencoin.pool.com:3333" { + t.Errorf("GPU pool URL mismatch: %v", gpuPool["url"]) + } + if gpuPool["user"] != "gpu_wallet_address" { + t.Errorf("GPU wallet mismatch: %v", gpuPool["user"]) + } + if gpuPool["algo"] != "kawpow" { + t.Errorf("GPU algo mismatch: %v", gpuPool["algo"]) + } + + // Verify CUDA enabled, OpenCL disabled + cuda := generatedConfig["cuda"].(map[string]interface{}) + if cuda["enabled"] != true { + t.Error("CUDA should be enabled") + } + + opencl := generatedConfig["opencl"].(map[string]interface{}) + if opencl["enabled"] != false { + t.Error("OpenCL should be disabled") + } + + // Verify CPU config + cpu := generatedConfig["cpu"].(map[string]interface{}) + if cpu["enabled"] != true { + t.Error("CPU should be enabled") + } + if cpu["max-threads-hint"] != float64(50) { + t.Errorf("CPU max-threads-hint mismatch: %v", cpu["max-threads-hint"]) + } + + t.Logf("Generated dual-mining config:\n%s", string(data)) +} + +func TestXMRigGPUOnlyConfig(t *testing.T) { + tmpDir := t.TempDir() + + miner := &XMRigMiner{ + BaseMiner: BaseMiner{ + Name: "xmrig-gpu-only", + API: &API{ + Enabled: true, + ListenHost: "127.0.0.1", + ListenPort: 12346, + }, + }, + } + + origGetPath := getXMRigConfigPath + getXMRigConfigPath = func(name string) (string, error) { + return filepath.Join(tmpDir, name+".json"), nil + } + defer func() { getXMRigConfigPath = origGetPath }() + + // GPU-only config using same pool for simplicity + // MUST specify Devices explicitly - no auto-picking! + config := &Config{ + Pool: "stratum+tcp://pool.supportxmr.com:3333", + Wallet: "test_wallet", + Algo: "rx/0", + NoCPU: true, // Disable CPU + GPUEnabled: true, + OpenCL: true, // AMD GPU + CUDA: true, // Also NVIDIA + Devices: "0,1", // Explicit device selection required + } + + err := miner.createConfig(config) + if err != nil { + t.Fatalf("Failed to create config: %v", err) + } + + data, err := os.ReadFile(miner.ConfigPath) + if err != nil { + t.Fatalf("Failed to read config: %v", err) + } + + var generatedConfig map[string]interface{} + json.Unmarshal(data, &generatedConfig) + + // Both GPU backends should be enabled + cuda := generatedConfig["cuda"].(map[string]interface{}) + opencl := generatedConfig["opencl"].(map[string]interface{}) + + if cuda["enabled"] != true { + t.Error("CUDA should be enabled") + } + if opencl["enabled"] != true { + t.Error("OpenCL should be enabled") + } + + t.Logf("Generated GPU config:\n%s", string(data)) +} + +func TestXMRigCPUOnlyConfig(t *testing.T) { + tmpDir := t.TempDir() + + miner := &XMRigMiner{ + BaseMiner: BaseMiner{ + Name: "xmrig-cpu-only", + API: &API{ + Enabled: true, + ListenHost: "127.0.0.1", + ListenPort: 12347, + }, + }, + } + + origGetPath := getXMRigConfigPath + getXMRigConfigPath = func(name string) (string, error) { + return filepath.Join(tmpDir, name+".json"), nil + } + defer func() { getXMRigConfigPath = origGetPath }() + + // CPU-only config (GPUEnabled defaults to false) + config := &Config{ + Pool: "stratum+tcp://pool.supportxmr.com:3333", + Wallet: "test_wallet", + Algo: "rx/0", + } + + err := miner.createConfig(config) + if err != nil { + t.Fatalf("Failed to create config: %v", err) + } + + data, err := os.ReadFile(miner.ConfigPath) + if err != nil { + t.Fatalf("Failed to read config: %v", err) + } + + var generatedConfig map[string]interface{} + json.Unmarshal(data, &generatedConfig) + + // GPU backends should be disabled + cuda := generatedConfig["cuda"].(map[string]interface{}) + opencl := generatedConfig["opencl"].(map[string]interface{}) + + if cuda["enabled"] != false { + t.Error("CUDA should be disabled for CPU-only config") + } + if opencl["enabled"] != false { + t.Error("OpenCL should be disabled for CPU-only config") + } + + // Should only have 1 pool + pools := generatedConfig["pools"].([]interface{}) + if len(pools) != 1 { + t.Errorf("Expected 1 pool for CPU-only, got %d", len(pools)) + } + + t.Logf("Generated CPU-only config:\n%s", string(data)) +} diff --git a/mining/xmrig_start.go b/mining/xmrig_start.go new file mode 100644 index 0000000..7fb5b10 --- /dev/null +++ b/mining/xmrig_start.go @@ -0,0 +1,312 @@ +package mining + +import ( + "encoding/json" + "errors" + "fmt" + "io" + "os" + "os/exec" + "path/filepath" + "strings" + "time" + + "forge.lthn.ai/core/mining/logging" +) + +// Start launches the XMRig miner with the specified configuration. +func (m *XMRigMiner) Start(config *Config) error { + // Check installation BEFORE acquiring lock (CheckInstallation takes its own locks) + m.mu.RLock() + needsInstallCheck := m.MinerBinary == "" + m.mu.RUnlock() + + if needsInstallCheck { + if _, err := m.CheckInstallation(); err != nil { + return err // Propagate the detailed error from CheckInstallation + } + } + + m.mu.Lock() + defer m.mu.Unlock() + + if m.Running { + return errors.New("miner is already running") + } + + if m.API != nil && config.HTTPPort != 0 { + m.API.ListenPort = config.HTTPPort + } else if m.API != nil && m.API.ListenPort == 0 { + return errors.New("miner API port not assigned") + } + + if config.Pool != "" && config.Wallet != "" { + if err := m.createConfig(config); err != nil { + return err + } + } else { + // Use the centralized helper to get the instance-specific config path + configPath, err := getXMRigConfigPath(m.Name) + if err != nil { + return fmt.Errorf("could not determine config file path: %w", err) + } + m.ConfigPath = configPath + if _, err := os.Stat(m.ConfigPath); os.IsNotExist(err) { + return errors.New("config file does not exist and no pool/wallet provided to create one") + } + } + + args := []string{"-c", m.ConfigPath} + + if m.API != nil && m.API.Enabled { + args = append(args, "--http-host", m.API.ListenHost, "--http-port", fmt.Sprintf("%d", m.API.ListenPort)) + } + + addCliArgs(config, &args) + + logging.Info("executing miner command", logging.Fields{"binary": m.MinerBinary, "args": strings.Join(args, " ")}) + + m.cmd = exec.Command(m.MinerBinary, args...) + + // Create stdin pipe for console commands + stdinPipe, err := m.cmd.StdinPipe() + if err != nil { + return fmt.Errorf("failed to create stdin pipe: %w", err) + } + m.stdinPipe = stdinPipe + + // Always capture output to LogBuffer + if m.LogBuffer != nil { + m.cmd.Stdout = m.LogBuffer + m.cmd.Stderr = m.LogBuffer + } + // Also output to console if requested + if config.LogOutput { + m.cmd.Stdout = io.MultiWriter(m.LogBuffer, os.Stdout) + m.cmd.Stderr = io.MultiWriter(m.LogBuffer, os.Stderr) + } + + if err := m.cmd.Start(); err != nil { + stdinPipe.Close() + // Clean up config file on failed start + if m.ConfigPath != "" { + os.Remove(m.ConfigPath) + } + return fmt.Errorf("failed to start miner: %w", err) + } + + m.Running = true + + // Capture cmd locally to avoid race with Stop() + cmd := m.cmd + minerName := m.Name // Capture name for logging + go func() { + // Use a channel to detect if Wait() completes + done := make(chan struct{}) + var waitErr error + go func() { + waitErr = cmd.Wait() + close(done) + }() + + // Wait with timeout to prevent goroutine leak on zombie processes + select { + case <-done: + // Normal exit - log the exit status + if waitErr != nil { + logging.Info("miner process exited", logging.Fields{ + "miner": minerName, + "error": waitErr.Error(), + }) + } else { + logging.Info("miner process exited normally", logging.Fields{ + "miner": minerName, + }) + } + case <-time.After(5 * time.Minute): + // Process didn't exit after 5 minutes - force cleanup + logging.Warn("miner process wait timeout, forcing cleanup", logging.Fields{"miner": minerName}) + if cmd.Process != nil { + cmd.Process.Kill() + } + // Wait with timeout to prevent goroutine leak if Wait() never returns + select { + case <-done: + // Inner goroutine completed + case <-time.After(10 * time.Second): + logging.Error("process cleanup timed out after kill", logging.Fields{"miner": minerName}) + } + } + + m.mu.Lock() + // Only clear if this is still the same command (not restarted) + if m.cmd == cmd { + m.Running = false + m.cmd = nil + } + m.mu.Unlock() + }() + + return nil +} + +// Stop terminates the miner process and cleans up the instance-specific config file. +func (m *XMRigMiner) Stop() error { + // Call the base Stop to kill the process + if err := m.BaseMiner.Stop(); err != nil { + return err + } + + // Clean up the instance-specific config file + if m.ConfigPath != "" { + os.Remove(m.ConfigPath) // Ignore error if it doesn't exist + } + + return nil +} + +// addCliArgs is a helper to append command line arguments based on the config. +func addCliArgs(config *Config, args *[]string) { + if config.Pool != "" { + *args = append(*args, "-o", config.Pool) + } + if config.Wallet != "" { + *args = append(*args, "-u", config.Wallet) + } + if config.Threads != 0 { + *args = append(*args, "-t", fmt.Sprintf("%d", config.Threads)) + } + if !config.HugePages { + *args = append(*args, "--no-huge-pages") + } + if config.TLS { + *args = append(*args, "--tls") + } + *args = append(*args, "--donate-level", "1") +} + +// createConfig creates a JSON configuration file for the XMRig miner. +func (m *XMRigMiner) createConfig(config *Config) error { + // Use the centralized helper to get the instance-specific config path + configPath, err := getXMRigConfigPath(m.Name) + if err != nil { + return err + } + m.ConfigPath = configPath + + if err := os.MkdirAll(filepath.Dir(m.ConfigPath), 0755); err != nil { + return err + } + + apiListen := "127.0.0.1:0" + if m.API != nil { + apiListen = fmt.Sprintf("%s:%d", m.API.ListenHost, m.API.ListenPort) + } + + cpuConfig := map[string]interface{}{ + "enabled": true, + "huge-pages": config.HugePages, + } + + // Set thread count or max-threads-hint for CPU throttling + if config.Threads > 0 { + cpuConfig["threads"] = config.Threads + } + if config.CPUMaxThreadsHint > 0 { + cpuConfig["max-threads-hint"] = config.CPUMaxThreadsHint + } + if config.CPUPriority > 0 { + cpuConfig["priority"] = config.CPUPriority + } + + // Build pools array - CPU pool first + cpuPool := map[string]interface{}{ + "url": config.Pool, + "user": config.Wallet, + "pass": "x", + "keepalive": true, + "tls": config.TLS, + } + // Add algo or coin (coin takes precedence for algorithm auto-detection) + if config.Coin != "" { + cpuPool["coin"] = config.Coin + } else if config.Algo != "" { + cpuPool["algo"] = config.Algo + } + pools := []map[string]interface{}{cpuPool} + + // Add separate GPU pool if configured + if config.GPUEnabled && config.GPUPool != "" { + gpuWallet := config.GPUWallet + if gpuWallet == "" { + gpuWallet = config.Wallet // Default to main wallet + } + gpuPass := config.GPUPassword + if gpuPass == "" { + gpuPass = "x" + } + gpuPool := map[string]interface{}{ + "url": config.GPUPool, + "user": gpuWallet, + "pass": gpuPass, + "keepalive": true, + } + // Add GPU algo (typically etchash, ethash, kawpow, progpowz for GPU mining) + if config.GPUAlgo != "" { + gpuPool["algo"] = config.GPUAlgo + } + pools = append(pools, gpuPool) + } + + // Build OpenCL (AMD/Intel GPU) config + // GPU mining requires explicit device selection - no auto-picking + openclConfig := map[string]interface{}{ + "enabled": config.GPUEnabled && config.OpenCL && config.Devices != "", + } + if config.GPUEnabled && config.OpenCL && config.Devices != "" { + // User must explicitly specify devices (e.g., "0" or "0,1") + openclConfig["devices"] = config.Devices + if config.GPUIntensity > 0 { + openclConfig["intensity"] = config.GPUIntensity + } + if config.GPUThreads > 0 { + openclConfig["threads"] = config.GPUThreads + } + } + + // Build CUDA (NVIDIA GPU) config + // GPU mining requires explicit device selection - no auto-picking + cudaConfig := map[string]interface{}{ + "enabled": config.GPUEnabled && config.CUDA && config.Devices != "", + } + if config.GPUEnabled && config.CUDA && config.Devices != "" { + // User must explicitly specify devices (e.g., "0" or "0,1") + cudaConfig["devices"] = config.Devices + if config.GPUIntensity > 0 { + cudaConfig["intensity"] = config.GPUIntensity + } + if config.GPUThreads > 0 { + cudaConfig["threads"] = config.GPUThreads + } + } + + c := map[string]interface{}{ + "api": map[string]interface{}{ + "enabled": m.API != nil && m.API.Enabled, + "listen": apiListen, + "restricted": true, + }, + "pools": pools, + "cpu": cpuConfig, + "opencl": openclConfig, + "cuda": cudaConfig, + "pause-on-active": config.PauseOnActive, + "pause-on-battery": config.PauseOnBattery, + } + + data, err := json.MarshalIndent(c, "", " ") + if err != nil { + return err + } + return os.WriteFile(m.ConfigPath, data, 0600) +} diff --git a/mining/xmrig_stats.go b/mining/xmrig_stats.go new file mode 100644 index 0000000..b116e8b --- /dev/null +++ b/mining/xmrig_stats.go @@ -0,0 +1,66 @@ +package mining + +import ( + "context" + "errors" + "time" +) + +// statsTimeout is the timeout for stats HTTP requests (shorter than general timeout) +const statsTimeout = 5 * time.Second + +// GetStats retrieves the performance statistics from the running XMRig miner. +func (m *XMRigMiner) GetStats(ctx context.Context) (*PerformanceMetrics, error) { + // Read state under RLock, then release before HTTP call + m.mu.RLock() + if !m.Running { + m.mu.RUnlock() + return nil, errors.New("miner is not running") + } + if m.API == nil || m.API.ListenPort == 0 { + m.mu.RUnlock() + return nil, errors.New("miner API not configured or port is zero") + } + config := HTTPStatsConfig{ + Host: m.API.ListenHost, + Port: m.API.ListenPort, + Endpoint: "/2/summary", + } + m.mu.RUnlock() + + // Create request with context and timeout + reqCtx, cancel := context.WithTimeout(ctx, statsTimeout) + defer cancel() + + // Use the common HTTP stats fetcher + var summary XMRigSummary + if err := FetchJSONStats(reqCtx, config, &summary); err != nil { + return nil, err + } + + // Store the full summary in the miner struct (requires lock) + m.mu.Lock() + m.FullStats = &summary + m.mu.Unlock() + + var hashrate int + if len(summary.Hashrate.Total) > 0 { + hashrate = int(summary.Hashrate.Total[0]) + } + + // Calculate average difficulty per accepted share + var avgDifficulty int + if summary.Results.SharesGood > 0 { + avgDifficulty = summary.Results.HashesTotal / summary.Results.SharesGood + } + + return &PerformanceMetrics{ + Hashrate: hashrate, + Shares: summary.Results.SharesGood, + Rejected: summary.Results.SharesTotal - summary.Results.SharesGood, + Uptime: summary.Uptime, + Algorithm: summary.Algo, + AvgDifficulty: avgDifficulty, + DiffCurrent: summary.Results.DiffCurrent, + }, nil +} diff --git a/mining/xmrig_test.go b/mining/xmrig_test.go new file mode 100644 index 0000000..21d88c5 --- /dev/null +++ b/mining/xmrig_test.go @@ -0,0 +1,266 @@ +package mining + +import ( + "context" + "encoding/json" + "fmt" + "io" + "net/http" + "net/http/httptest" + "os" + "path/filepath" + "runtime" + "strings" + "testing" + "time" +) + +// MockRoundTripper is a mock implementation of http.RoundTripper for testing. +type MockRoundTripper func(req *http.Request) *http.Response + +// RoundTrip executes a single HTTP transaction, returning a Response for the given Request. +func (f MockRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) { + return f(req), nil +} + +// newTestClient returns *http.Client with Transport replaced to avoid making real calls. +func newTestClient(fn MockRoundTripper) *http.Client { + return &http.Client{ + Transport: fn, + } +} + +// helper function to create a temporary directory for testing +func tempDir(t *testing.T) string { + dir, err := os.MkdirTemp("", "test") + if err != nil { + t.Fatalf("Failed to create temp dir: %v", err) + } + t.Cleanup(func() { os.RemoveAll(dir) }) + return dir +} + +func TestNewXMRigMiner_Good(t *testing.T) { + miner := NewXMRigMiner() + if miner == nil { + t.Fatal("NewXMRigMiner returned nil") + } + if miner.Name != "miner" { + t.Errorf("Expected miner name to be 'miner', got '%s'", miner.Name) + } + if miner.Version != "latest" { + t.Errorf("Expected miner version to be 'latest', got '%s'", miner.Version) + } + if !miner.API.Enabled { + t.Error("Expected API to be enabled by default") + } +} + +func TestXMRigMiner_GetName_Good(t *testing.T) { + miner := NewXMRigMiner() + if name := miner.GetName(); name != "miner" { + t.Errorf("Expected GetName() to return 'miner', got '%s'", name) + } +} + +func TestXMRigMiner_GetLatestVersion_Good(t *testing.T) { + originalClient := getHTTPClient() + setHTTPClient(newTestClient(func(req *http.Request) *http.Response { + if req.URL.String() != "https://api.github.com/repos/xmrig/xmrig/releases/latest" { + return &http.Response{ + StatusCode: http.StatusNotFound, + Body: io.NopCloser(strings.NewReader("Not Found")), + Header: make(http.Header), + } + } + return &http.Response{ + StatusCode: http.StatusOK, + Body: io.NopCloser(strings.NewReader(`{"tag_name": "v6.18.0"}`)), + Header: make(http.Header), + } + })) + defer setHTTPClient(originalClient) + + miner := NewXMRigMiner() + version, err := miner.GetLatestVersion() + if err != nil { + t.Fatalf("GetLatestVersion() returned an error: %v", err) + } + if version != "v6.18.0" { + t.Errorf("Expected version 'v6.18.0', got '%s'", version) + } +} + +func TestXMRigMiner_GetLatestVersion_Bad(t *testing.T) { + originalClient := getHTTPClient() + setHTTPClient(newTestClient(func(req *http.Request) *http.Response { + return &http.Response{ + StatusCode: http.StatusNotFound, + Body: io.NopCloser(strings.NewReader("Not Found")), + Header: make(http.Header), + } + })) + defer setHTTPClient(originalClient) + + miner := NewXMRigMiner() + _, err := miner.GetLatestVersion() + if err == nil { + t.Fatalf("GetLatestVersion() did not return an error") + } +} + +func TestXMRigMiner_Start_Stop_Good(t *testing.T) { + t.Skip("Skipping test that runs miner process as per request") +} + +func TestXMRigMiner_Start_Stop_Bad(t *testing.T) { + t.Skip("Skipping test that attempts to spawn miner process") +} + +func TestXMRigMiner_CheckInstallation(t *testing.T) { + tmpDir := t.TempDir() + // Use "miner" since that's what NewXMRigMiner() sets as ExecutableName + executableName := "miner" + if runtime.GOOS == "windows" { + executableName += ".exe" + } + dummyExePath := filepath.Join(tmpDir, executableName) + + if runtime.GOOS == "windows" { + // Create a dummy batch file that prints version + if err := os.WriteFile(dummyExePath, []byte("@echo off\necho XMRig 6.24.0\n"), 0755); err != nil { + t.Fatalf("failed to create dummy executable: %v", err) + } + } else { + // Create a dummy shell script that prints version + if err := os.WriteFile(dummyExePath, []byte("#!/bin/sh\necho 'XMRig 6.24.0'\n"), 0755); err != nil { + t.Fatalf("failed to create dummy executable: %v", err) + } + } + + // Prepend tmpDir to PATH so findMinerBinary can find it + originalPath := os.Getenv("PATH") + t.Cleanup(func() { os.Setenv("PATH", originalPath) }) + os.Setenv("PATH", tmpDir+string(os.PathListSeparator)+originalPath) + + miner := NewXMRigMiner() + // Clear any binary path to force search + miner.MinerBinary = "" + + details, err := miner.CheckInstallation() + if err != nil { + t.Fatalf("CheckInstallation failed: %v", err) + } + if !details.IsInstalled { + t.Error("Expected IsInstalled to be true") + } + if details.Version != "6.24.0" { + t.Errorf("Expected version '6.24.0', got '%s'", details.Version) + } + // On Windows, the path might be canonicalized differently (e.g. 8.3 names), so checking Base is safer or full path equality if we trust os.Path + if filepath.Base(details.MinerBinary) != executableName { + t.Errorf("Expected binary name '%s', got '%s'", executableName, filepath.Base(details.MinerBinary)) + } +} + +func TestXMRigMiner_GetStats_Good(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + summary := XMRigSummary{ + Hashrate: struct { + Total []float64 `json:"total"` + Highest float64 `json:"highest"` + }{Total: []float64{123.45}, Highest: 130.0}, + Results: struct { + DiffCurrent int `json:"diff_current"` + SharesGood int `json:"shares_good"` + SharesTotal int `json:"shares_total"` + AvgTime int `json:"avg_time"` + AvgTimeMS int `json:"avg_time_ms"` + HashesTotal int `json:"hashes_total"` + Best []int `json:"best"` + }{SharesGood: 10, SharesTotal: 12}, + Uptime: 600, + Algo: "rx/0", + } + json.NewEncoder(w).Encode(summary) + })) + defer server.Close() + + originalHTTPClient := getHTTPClient() + setHTTPClient(server.Client()) + defer setHTTPClient(originalHTTPClient) + + miner := NewXMRigMiner() + miner.Running = true // Mock running state + miner.API.ListenHost = strings.TrimPrefix(server.URL, "http://") + miner.API.ListenHost, miner.API.ListenPort = server.Listener.Addr().String(), 0 + parts := strings.Split(server.Listener.Addr().String(), ":") + miner.API.ListenHost = parts[0] + fmt.Sscanf(parts[1], "%d", &miner.API.ListenPort) + + stats, err := miner.GetStats(context.Background()) + if err != nil { + t.Fatalf("GetStats() returned an error: %v", err) + } + if stats.Hashrate != 123 { + t.Errorf("Expected hashrate 123, got %d", stats.Hashrate) + } + if stats.Shares != 10 { + t.Errorf("Expected 10 shares, got %d", stats.Shares) + } + if stats.Rejected != 2 { + t.Errorf("Expected 2 rejected shares, got %d", stats.Rejected) + } + if stats.Uptime != 600 { + t.Errorf("Expected uptime 600, got %d", stats.Uptime) + } + if stats.Algorithm != "rx/0" { + t.Errorf("Expected algorithm 'rx/0', got '%s'", stats.Algorithm) + } +} + +func TestXMRigMiner_GetStats_Bad(t *testing.T) { + // Don't start a server, so the API call will fail + miner := NewXMRigMiner() + miner.Running = true // Mock running state + miner.API.ListenHost = "127.0.0.1" + miner.API.ListenPort = 9999 // A port that is unlikely to be in use + + _, err := miner.GetStats(context.Background()) + if err == nil { + t.Fatalf("GetStats() did not return an error") + } +} + +func TestXMRigMiner_HashrateHistory_Good(t *testing.T) { + miner := NewXMRigMiner() + now := time.Now() + + // Add high-resolution points + for i := 0; i < 10; i++ { + miner.AddHashratePoint(HashratePoint{Timestamp: now.Add(time.Duration(i) * time.Second), Hashrate: 100 + i}) + } + + history := miner.GetHashrateHistory() + if len(history) != 10 { + t.Fatalf("Expected 10 hashrate points, got %d", len(history)) + } + + // Test ReduceHashrateHistory + // Move time forward to make some points eligible for reduction + future := now.Add(HighResolutionDuration + 30*time.Second) + miner.ReduceHashrateHistory(future) + + // After reduction, high-res history should be smaller + if miner.GetHighResHistoryLength() >= 10 { + t.Errorf("High-res history not reduced, size: %d", miner.GetHighResHistoryLength()) + } + if miner.GetLowResHistoryLength() == 0 { + t.Error("Low-res history not populated") + } + + combinedHistory := miner.GetHashrateHistory() + if len(combinedHistory) == 0 { + t.Error("GetHashrateHistory returned empty slice after reduction") + } +}