diff --git a/pkg/database/database.go b/pkg/database/database.go index ec097d8..34f2b40 100644 --- a/pkg/database/database.go +++ b/pkg/database/database.go @@ -28,8 +28,8 @@ type Config struct { RetentionDays int `json:"retentionDays,omitempty"` } -// DefaultConfig returns the default database configuration -func DefaultConfig() Config { +// defaultConfig returns the default database configuration +func defaultConfig() Config { return Config{ Enabled: true, Path: "", @@ -99,8 +99,8 @@ func Close() error { return err } -// IsInitialized returns true if the database is ready -func IsInitialized() bool { +// isInitialized returns true if the database is ready +func isInitialized() bool { dbMu.RLock() defer dbMu.RUnlock() return db != nil @@ -168,8 +168,8 @@ func Cleanup(retentionDays int) error { return err } -// VacuumDB optimizes the database file size -func VacuumDB() error { +// vacuumDB optimizes the database file size +func vacuumDB() error { dbMu.RLock() defer dbMu.RUnlock() diff --git a/pkg/database/hashrate.go b/pkg/database/hashrate.go index c924674..13cb312 100644 --- a/pkg/database/hashrate.go +++ b/pkg/database/hashrate.go @@ -1,16 +1,11 @@ package database import ( - "context" - "database/sql" "fmt" "log" "time" ) -// dbOperationTimeout is the maximum time for database operations -const dbOperationTimeout = 30 * time.Second - // 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 { @@ -68,48 +63,6 @@ func InsertHashratePoint(minerName, minerType string, point HashratePoint, resol return err } -// InsertHashratePoints stores multiple hashrate measurements in a single transaction -func InsertHashratePoints(minerName, minerType string, points []HashratePoint, resolution Resolution) error { - dbMu.RLock() - defer dbMu.RUnlock() - - if db == nil { - return nil - } - - if len(points) == 0 { - return nil - } - - // Use context with timeout to prevent hanging on locked database - ctx, cancel := context.WithTimeout(context.Background(), dbOperationTimeout) - defer cancel() - - tx, err := db.BeginTx(ctx, nil) - if err != nil { - return fmt.Errorf("failed to begin transaction: %w", err) - } - defer tx.Rollback() - - stmt, err := tx.PrepareContext(ctx, ` - INSERT INTO hashrate_history (miner_name, miner_type, timestamp, hashrate, resolution) - VALUES (?, ?, ?, ?, ?) - `) - if err != nil { - return fmt.Errorf("failed to prepare statement: %w", err) - } - defer stmt.Close() - - for _, point := range points { - _, err := stmt.ExecContext(ctx, minerName, minerType, point.Timestamp, point.Hashrate, string(resolution)) - if err != nil { - return fmt.Errorf("failed to insert point: %w", err) - } - } - - return tx.Commit() -} - // 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() @@ -145,76 +98,6 @@ func GetHashrateHistory(minerName string, resolution Resolution, since, until ti return points, rows.Err() } -// GetLatestHashrate retrieves the most recent hashrate for a miner -func GetLatestHashrate(minerName string) (*HashratePoint, error) { - dbMu.RLock() - defer dbMu.RUnlock() - - if db == nil { - return nil, nil - } - - var point HashratePoint - err := db.QueryRow(` - SELECT timestamp, hashrate - FROM hashrate_history - WHERE miner_name = ? - ORDER BY timestamp DESC - LIMIT 1 - `, minerName).Scan(&point.Timestamp, &point.Hashrate) - - if err != nil { - if err == sql.ErrNoRows { - return nil, nil // No data found is not an error - } - return nil, fmt.Errorf("failed to get latest hashrate: %w", err) - } - - return &point, nil -} - -// GetAverageHashrate calculates the average hashrate for a miner in a time range -func GetAverageHashrate(minerName string, since, until time.Time) (int, error) { - dbMu.RLock() - defer dbMu.RUnlock() - - if db == nil { - return 0, nil - } - - var avg float64 - err := db.QueryRow(` - SELECT COALESCE(AVG(hashrate), 0) - FROM hashrate_history - WHERE miner_name = ? - AND timestamp >= ? - AND timestamp <= ? - `, minerName, since, until).Scan(&avg) - - return int(avg), err -} - -// GetMaxHashrate retrieves the maximum hashrate for a miner in a time range -func GetMaxHashrate(minerName string, since, until time.Time) (int, error) { - dbMu.RLock() - defer dbMu.RUnlock() - - if db == nil { - return 0, nil - } - - var max int - err := db.QueryRow(` - SELECT COALESCE(MAX(hashrate), 0) - FROM hashrate_history - WHERE miner_name = ? - AND timestamp >= ? - AND timestamp <= ? - `, minerName, since, until).Scan(&max) - - return max, err -} - // GetHashrateStats retrieves aggregated stats for a miner type HashrateStats struct { MinerName string `json:"minerName"` @@ -331,22 +214,3 @@ func GetAllMinerStats() ([]HashrateStats, error) { return allStats, rows.Err() } - -// CleanupOldData removes hashrate data older than the specified duration -func CleanupOldData(resolution Resolution, maxAge time.Duration) error { - dbMu.RLock() - defer dbMu.RUnlock() - - if db == nil { - return nil - } - - cutoff := time.Now().Add(-maxAge) - _, err := db.Exec(` - DELETE FROM hashrate_history - WHERE resolution = ? - AND timestamp < ? - `, string(resolution), cutoff) - - return err -} diff --git a/pkg/database/session.go b/pkg/database/session.go index 1217a70..747b7ff 100644 --- a/pkg/database/session.go +++ b/pkg/database/session.go @@ -1,279 +1,5 @@ package database -import ( - "fmt" - "time" -) - -// MinerSession represents a mining session -type MinerSession struct { - ID int64 `json:"id"` - MinerName string `json:"minerName"` - MinerType string `json:"minerType"` - StartedAt time.Time `json:"startedAt"` - StoppedAt *time.Time `json:"stoppedAt,omitempty"` - TotalShares int `json:"totalShares"` - RejectedShares int `json:"rejectedShares"` - AverageHashrate int `json:"averageHashrate"` -} - -// StartSession records the start of a new mining session -func StartSession(minerName, minerType string) (int64, error) { - dbMu.RLock() - defer dbMu.RUnlock() - - if db == nil { - return 0, nil - } - - result, err := db.Exec(` - INSERT INTO miner_sessions (miner_name, miner_type, started_at) - VALUES (?, ?, ?) - `, minerName, minerType, time.Now()) - if err != nil { - return 0, fmt.Errorf("failed to start session: %w", err) - } - - return result.LastInsertId() -} - -// EndSession marks a session as complete with final stats -func EndSession(sessionID int64, totalShares, rejectedShares, averageHashrate int) error { - dbMu.RLock() - defer dbMu.RUnlock() - - if db == nil { - return nil - } - - _, err := db.Exec(` - UPDATE miner_sessions - SET stopped_at = ?, - total_shares = ?, - rejected_shares = ?, - average_hashrate = ? - WHERE id = ? - `, time.Now(), totalShares, rejectedShares, averageHashrate, sessionID) - - return err -} - -// EndSessionByName marks the most recent session for a miner as complete -func EndSessionByName(minerName string, totalShares, rejectedShares, averageHashrate int) error { - dbMu.RLock() - defer dbMu.RUnlock() - - if db == nil { - return nil - } - - _, err := db.Exec(` - UPDATE miner_sessions - SET stopped_at = ?, - total_shares = ?, - rejected_shares = ?, - average_hashrate = ? - WHERE miner_name = ? - AND stopped_at IS NULL - `, time.Now(), totalShares, rejectedShares, averageHashrate, minerName) - - return err -} - -// GetSession retrieves a session by ID -func GetSession(sessionID int64) (*MinerSession, error) { - dbMu.RLock() - defer dbMu.RUnlock() - - if db == nil { - return nil, nil - } - - var session MinerSession - var stoppedAt *time.Time - - err := db.QueryRow(` - SELECT id, miner_name, miner_type, started_at, stopped_at, - total_shares, rejected_shares, average_hashrate - FROM miner_sessions - WHERE id = ? - `, sessionID).Scan( - &session.ID, - &session.MinerName, - &session.MinerType, - &session.StartedAt, - &stoppedAt, - &session.TotalShares, - &session.RejectedShares, - &session.AverageHashrate, - ) - if err != nil { - return nil, err - } - - session.StoppedAt = stoppedAt - return &session, nil -} - -// GetActiveSessions retrieves all currently active (non-stopped) sessions -func GetActiveSessions() ([]MinerSession, error) { - dbMu.RLock() - defer dbMu.RUnlock() - - if db == nil { - return nil, nil - } - - rows, err := db.Query(` - SELECT id, miner_name, miner_type, started_at, stopped_at, - total_shares, rejected_shares, average_hashrate - FROM miner_sessions - WHERE stopped_at IS NULL - ORDER BY started_at DESC - `) - if err != nil { - return nil, err - } - defer rows.Close() - - var sessions []MinerSession - for rows.Next() { - var session MinerSession - var stoppedAt *time.Time - if err := rows.Scan( - &session.ID, - &session.MinerName, - &session.MinerType, - &session.StartedAt, - &stoppedAt, - &session.TotalShares, - &session.RejectedShares, - &session.AverageHashrate, - ); err != nil { - return nil, err - } - session.StoppedAt = stoppedAt - sessions = append(sessions, session) - } - - return sessions, rows.Err() -} - -// GetRecentSessions retrieves the most recent sessions for a miner -func GetRecentSessions(minerName string, limit int) ([]MinerSession, error) { - dbMu.RLock() - defer dbMu.RUnlock() - - if db == nil { - return nil, nil - } - - rows, err := db.Query(` - SELECT id, miner_name, miner_type, started_at, stopped_at, - total_shares, rejected_shares, average_hashrate - FROM miner_sessions - WHERE miner_name = ? - ORDER BY started_at DESC - LIMIT ? - `, minerName, limit) - if err != nil { - return nil, err - } - defer rows.Close() - - var sessions []MinerSession - for rows.Next() { - var session MinerSession - var stoppedAt *time.Time - if err := rows.Scan( - &session.ID, - &session.MinerName, - &session.MinerType, - &session.StartedAt, - &stoppedAt, - &session.TotalShares, - &session.RejectedShares, - &session.AverageHashrate, - ); err != nil { - return nil, err - } - session.StoppedAt = stoppedAt - sessions = append(sessions, session) - } - - return sessions, rows.Err() -} - -// GetSessionStats retrieves aggregated session statistics for a miner -type SessionStats struct { - MinerName string `json:"minerName"` - TotalSessions int `json:"totalSessions"` - TotalUptime time.Duration `json:"totalUptime"` - TotalShares int `json:"totalShares"` - TotalRejected int `json:"totalRejected"` - AvgSessionTime time.Duration `json:"avgSessionTime"` - AvgHashrate int `json:"avgHashrate"` - LastSessionAt time.Time `json:"lastSessionAt"` -} - -func GetSessionStats(minerName string) (*SessionStats, error) { - dbMu.RLock() - defer dbMu.RUnlock() - - if db == nil { - return nil, nil - } - - var stats SessionStats - stats.MinerName = minerName - - // Get basic aggregates - err := db.QueryRow(` - SELECT - COUNT(*), - COALESCE(SUM(total_shares), 0), - COALESCE(SUM(rejected_shares), 0), - COALESCE(AVG(average_hashrate), 0), - MAX(started_at) - FROM miner_sessions - WHERE miner_name = ? - AND stopped_at IS NOT NULL - `, minerName).Scan( - &stats.TotalSessions, - &stats.TotalShares, - &stats.TotalRejected, - &stats.AvgHashrate, - &stats.LastSessionAt, - ) - if err != nil { - return nil, err - } - - // Calculate total uptime - rows, err := db.Query(` - SELECT started_at, stopped_at - FROM miner_sessions - WHERE miner_name = ? - AND stopped_at IS NOT NULL - `, minerName) - if err != nil { - return nil, err - } - defer rows.Close() - - var totalSeconds int64 - for rows.Next() { - var started, stopped time.Time - if err := rows.Scan(&started, &stopped); err != nil { - return nil, err - } - totalSeconds += int64(stopped.Sub(started).Seconds()) - } - - stats.TotalUptime = time.Duration(totalSeconds) * time.Second - if stats.TotalSessions > 0 { - stats.AvgSessionTime = stats.TotalUptime / time.Duration(stats.TotalSessions) - } - - return &stats, nil -} +// 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/pkg/mining/config_manager.go b/pkg/mining/config_manager.go index 3553b00..84fa9fd 100644 --- a/pkg/mining/config_manager.go +++ b/pkg/mining/config_manager.go @@ -28,8 +28,8 @@ type DatabaseConfig struct { RetentionDays int `json:"retentionDays,omitempty"` } -// DefaultDatabaseConfig returns the default database configuration. -func DefaultDatabaseConfig() DatabaseConfig { +// defaultDatabaseConfig returns the default database configuration. +func defaultDatabaseConfig() DatabaseConfig { return DatabaseConfig{ Enabled: true, RetentionDays: 30, @@ -42,8 +42,8 @@ type MinersConfig struct { Database DatabaseConfig `json:"database"` } -// GetMinersConfigPath returns the path to the miners configuration file. -func GetMinersConfigPath() (string, error) { +// getMinersConfigPath returns the path to the miners configuration file. +func getMinersConfigPath() (string, error) { return xdg.ConfigFile("lethean-desktop/miners/config.json") } @@ -52,7 +52,7 @@ func LoadMinersConfig() (*MinersConfig, error) { configMu.RLock() defer configMu.RUnlock() - configPath, err := GetMinersConfigPath() + configPath, err := getMinersConfigPath() if err != nil { return nil, fmt.Errorf("could not determine miners config path: %w", err) } @@ -63,7 +63,7 @@ func LoadMinersConfig() (*MinersConfig, error) { // Return empty config with defaults if file doesn't exist return &MinersConfig{ Miners: []MinerAutostartConfig{}, - Database: DefaultDatabaseConfig(), + Database: defaultDatabaseConfig(), }, nil } return nil, fmt.Errorf("failed to read miners config file: %w", err) @@ -76,7 +76,7 @@ func LoadMinersConfig() (*MinersConfig, error) { // Apply default database config if not set (for backwards compatibility) if cfg.Database.RetentionDays == 0 { - cfg.Database = DefaultDatabaseConfig() + cfg.Database = defaultDatabaseConfig() } return &cfg, nil @@ -88,7 +88,7 @@ func SaveMinersConfig(cfg *MinersConfig) error { configMu.Lock() defer configMu.Unlock() - configPath, err := GetMinersConfigPath() + configPath, err := getMinersConfigPath() if err != nil { return fmt.Errorf("could not determine miners config path: %w", err) } diff --git a/pkg/mining/events.go b/pkg/mining/events.go index 86e2cbd..9f9220e 100644 --- a/pkg/mining/events.go +++ b/pkg/mining/events.go @@ -22,11 +22,6 @@ const ( EventMinerError EventType = "miner.error" EventMinerConnected EventType = "miner.connected" - // Profile events - EventProfileCreated EventType = "profile.created" - EventProfileUpdated EventType = "profile.updated" - EventProfileDeleted EventType = "profile.deleted" - // System events EventPong EventType = "pong" ) diff --git a/pkg/mining/version.go b/pkg/mining/version.go index d2b1267..d374805 100644 --- a/pkg/mining/version.go +++ b/pkg/mining/version.go @@ -10,3 +10,13 @@ var ( 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 +} diff --git a/pkg/node/bundle.go b/pkg/node/bundle.go index ec14588..9e2299f 100644 --- a/pkg/node/bundle.go +++ b/pkg/node/bundle.go @@ -129,71 +129,6 @@ func CreateMinerBundle(minerPath string, profileJSON []byte, name string, passwo }, nil } -// CreateFullBundle creates an encrypted bundle with miners and all profiles. -func CreateFullBundle(minerPaths []string, profiles [][]byte, name string, password string) (*Bundle, error) { - files := make(map[string][]byte) - - // Add each miner - for _, minerPath := range minerPaths { - minerData, err := os.ReadFile(minerPath) - if err != nil { - return nil, fmt.Errorf("failed to read miner %s: %w", minerPath, err) - } - files["miners/"+filepath.Base(minerPath)] = minerData - } - - // Add each profile - for i, profile := range profiles { - profileName := fmt.Sprintf("profiles/profile_%d.json", i) - files[profileName] = profile - } - - // Create tarball - tarData, err := createTarball(files) - if err != nil { - return nil, fmt.Errorf("failed to create tarball: %w", err) - } - - // Create DataNode from tarball - dn, err := datanode.FromTar(tarData) - if err != nil { - return nil, fmt.Errorf("failed to create datanode: %w", err) - } - - // Create TIM from DataNode - t, err := tim.FromDataNode(dn) - if err != nil { - return nil, fmt.Errorf("failed to create TIM: %w", err) - } - - // Create manifest as config - manifest := BundleManifest{ - Type: BundleFull, - Name: name, - ProfileIDs: make([]string, len(profiles)), - } - manifestJSON, err := json.Marshal(manifest) - if err != nil { - return nil, fmt.Errorf("failed to create manifest: %w", err) - } - t.Config = manifestJSON - - // Encrypt to STIM format - stimData, err := t.ToSigil(password) - if err != nil { - return nil, fmt.Errorf("failed to encrypt bundle: %w", err) - } - - checksum := calculateChecksum(stimData) - - return &Bundle{ - Type: BundleFull, - Name: name, - Data: stimData, - Checksum: checksum, - }, nil -} - // ExtractProfileBundle decrypts and extracts a profile bundle. func ExtractProfileBundle(bundle *Bundle, password string) ([]byte, error) { // Verify checksum first @@ -243,39 +178,6 @@ func ExtractMinerBundle(bundle *Bundle, password string, destDir string) (string return minerPath, t.Config, nil } -// ExtractFullBundle decrypts and extracts a full bundle. -func ExtractFullBundle(bundle *Bundle, password string, destDir string) (*BundleManifest, error) { - // Verify checksum - if calculateChecksum(bundle.Data) != bundle.Checksum { - return nil, fmt.Errorf("checksum mismatch - bundle may be corrupted") - } - - // Decrypt STIM format - t, err := tim.FromSigil(bundle.Data, password) - if err != nil { - return nil, fmt.Errorf("failed to decrypt bundle: %w", err) - } - - // Parse manifest - var manifest BundleManifest - if err := json.Unmarshal(t.Config, &manifest); err != nil { - return nil, fmt.Errorf("failed to parse manifest: %w", err) - } - - // Convert rootfs to tarball and extract - tarData, err := t.RootFS.ToTar() - if err != nil { - return nil, fmt.Errorf("failed to convert rootfs to tar: %w", err) - } - - // Extract tarball to destination - if _, err := extractTarball(tarData, destDir); err != nil { - return nil, fmt.Errorf("failed to extract tarball: %w", err) - } - - return &manifest, nil -} - // VerifyBundle checks if a bundle's checksum is valid. func VerifyBundle(bundle *Bundle) bool { return calculateChecksum(bundle.Data) == bundle.Checksum diff --git a/pkg/node/controller.go b/pkg/node/controller.go index 15e571a..3914801 100644 --- a/pkg/node/controller.go +++ b/pkg/node/controller.go @@ -111,6 +111,9 @@ func (c *Controller) sendRequest(peerID string, msg *Message, timeout time.Durat // GetRemoteStats requests miner statistics from a remote peer. func (c *Controller) GetRemoteStats(peerID string) (*StatsPayload, error) { identity := c.node.GetIdentity() + if identity == nil { + return nil, fmt.Errorf("node identity not initialized") + } msg, err := NewMessage(MsgGetStats, identity.ID, peerID, nil) if err != nil { @@ -145,6 +148,9 @@ func (c *Controller) GetRemoteStats(peerID string) (*StatsPayload, error) { // StartRemoteMiner requests a remote peer to start a miner with a given profile. func (c *Controller) StartRemoteMiner(peerID, profileID string, configOverride json.RawMessage) error { identity := c.node.GetIdentity() + if identity == nil { + return fmt.Errorf("node identity not initialized") + } payload := StartMinerPayload{ ProfileID: profileID, @@ -188,6 +194,9 @@ func (c *Controller) StartRemoteMiner(peerID, profileID string, configOverride j // StopRemoteMiner requests a remote peer to stop a miner. func (c *Controller) StopRemoteMiner(peerID, minerName string) error { identity := c.node.GetIdentity() + if identity == nil { + return fmt.Errorf("node identity not initialized") + } payload := StopMinerPayload{ MinerName: minerName, @@ -230,6 +239,9 @@ func (c *Controller) StopRemoteMiner(peerID, minerName string) error { // GetRemoteLogs requests console logs from a remote miner. func (c *Controller) GetRemoteLogs(peerID, minerName string, lines int) ([]string, error) { identity := c.node.GetIdentity() + if identity == nil { + return nil, fmt.Errorf("node identity not initialized") + } payload := GetLogsPayload{ MinerName: minerName, @@ -266,51 +278,6 @@ func (c *Controller) GetRemoteLogs(peerID, minerName string, lines int) ([]strin return logs.Lines, nil } -// DeployProfile sends a profile configuration to a remote peer. -func (c *Controller) DeployProfile(peerID string, bundleData []byte, name string, checksum string) error { - identity := c.node.GetIdentity() - - payload := DeployPayload{ - BundleType: "profile", - Data: bundleData, - Checksum: checksum, - Name: name, - } - - msg, err := NewMessage(MsgDeploy, identity.ID, peerID, payload) - if err != nil { - return fmt.Errorf("failed to create message: %w", err) - } - - resp, err := c.sendRequest(peerID, msg, 60*time.Second) - if err != nil { - return err - } - - if resp.Type == MsgError { - var errPayload ErrorPayload - if err := resp.ParsePayload(&errPayload); err != nil { - return fmt.Errorf("remote error (unable to parse)") - } - return fmt.Errorf("remote error: %s", errPayload.Message) - } - - if resp.Type != MsgDeployAck { - return fmt.Errorf("unexpected response type: %s", resp.Type) - } - - var ack DeployAckPayload - if err := resp.ParsePayload(&ack); err != nil { - return fmt.Errorf("failed to parse ack: %w", err) - } - - if !ack.Success { - return fmt.Errorf("deployment failed: %s", ack.Error) - } - - return nil -} - // GetAllStats fetches stats from all connected peers. func (c *Controller) GetAllStats() map[string]*StatsPayload { peers := c.peers.GetConnectedPeers() @@ -336,26 +303,12 @@ func (c *Controller) GetAllStats() map[string]*StatsPayload { return results } -// GetTotalHashrate calculates total hashrate across all connected peers. -func (c *Controller) GetTotalHashrate() float64 { - allStats := c.GetAllStats() - var total float64 - - for _, stats := range allStats { - if stats == nil { - continue - } - for _, miner := range stats.Miners { - total += miner.Hashrate - } - } - - return total -} - // PingPeer sends a ping to a peer and updates metrics. func (c *Controller) PingPeer(peerID string) (float64, error) { identity := c.node.GetIdentity() + if identity == nil { + return 0, fmt.Errorf("node identity not initialized") + } sentAt := time.Now() payload := PingPayload{ diff --git a/pkg/node/identity.go b/pkg/node/identity.go index 969b155..22a98c4 100644 --- a/pkg/node/identity.go +++ b/pkg/node/identity.go @@ -171,16 +171,6 @@ func (n *NodeManager) DeriveSharedSecret(peerPubKeyBase64 string) ([]byte, error return hash[:], nil } -// GetPublicKey returns the node's public key in base64 format. -func (n *NodeManager) GetPublicKey() string { - n.mu.RLock() - defer n.mu.RUnlock() - if n.identity == nil { - return "" - } - return n.identity.PublicKey -} - // savePrivateKey saves the private key to disk with restricted permissions. func (n *NodeManager) savePrivateKey() error { // Ensure directory exists @@ -249,32 +239,6 @@ func (n *NodeManager) loadIdentity() error { return nil } -// UpdateName updates the node's display name. -func (n *NodeManager) UpdateName(name string) error { - n.mu.Lock() - defer n.mu.Unlock() - - if n.identity == nil { - return fmt.Errorf("node identity not initialized") - } - - n.identity.Name = name - return n.saveIdentity() -} - -// UpdateRole updates the node's operational role. -func (n *NodeManager) UpdateRole(role NodeRole) error { - n.mu.Lock() - defer n.mu.Unlock() - - if n.identity == nil { - return fmt.Errorf("node identity not initialized") - } - - n.identity.Role = role - return n.saveIdentity() -} - // Delete removes the node identity and keys from disk. func (n *NodeManager) Delete() error { n.mu.Lock() diff --git a/pkg/node/peer_test.go b/pkg/node/peer_test.go index a4dd871..884de2c 100644 --- a/pkg/node/peer_test.go +++ b/pkg/node/peer_test.go @@ -168,6 +168,9 @@ func TestPeerRegistry_UpdateMetrics(t *testing.T) { } updated := pr.GetPeer("metrics-test") + if updated == nil { + t.Fatal("expected peer to exist") + } if updated.PingMS != 50.5 { t.Errorf("expected ping 50.5, got %f", updated.PingMS) } @@ -197,6 +200,9 @@ func TestPeerRegistry_UpdateScore(t *testing.T) { } updated := pr.GetPeer("score-test") + if updated == nil { + t.Fatal("expected peer to exist") + } if updated.Score != 85.5 { t.Errorf("expected score 85.5, got %f", updated.Score) } @@ -208,6 +214,9 @@ func TestPeerRegistry_UpdateScore(t *testing.T) { } updated = pr.GetPeer("score-test") + if updated == nil { + t.Fatal("expected peer to exist") + } if updated.Score != 100 { t.Errorf("expected score clamped to 100, got %f", updated.Score) } @@ -219,6 +228,9 @@ func TestPeerRegistry_UpdateScore(t *testing.T) { } updated = pr.GetPeer("score-test") + if updated == nil { + t.Fatal("expected peer to exist") + } if updated.Score != 0 { t.Errorf("expected score clamped to 0, got %f", updated.Score) } @@ -239,6 +251,9 @@ func TestPeerRegistry_SetConnected(t *testing.T) { pr.SetConnected("connect-test", true) updated := pr.GetPeer("connect-test") + if updated == nil { + t.Fatal("expected peer to exist") + } if !updated.Connected { t.Error("peer should be connected") } @@ -248,6 +263,9 @@ func TestPeerRegistry_SetConnected(t *testing.T) { pr.SetConnected("connect-test", false) updated = pr.GetPeer("connect-test") + if updated == nil { + t.Fatal("expected peer to exist") + } if updated.Connected { t.Error("peer should be disconnected") } diff --git a/pkg/node/transport.go b/pkg/node/transport.go index 6e47b5f..28e3815 100644 --- a/pkg/node/transport.go +++ b/pkg/node/transport.go @@ -327,6 +327,10 @@ func (t *Transport) handleWSUpgrade(w http.ResponseWriter, r *http.Request) { // Send handshake acknowledgment identity := t.node.GetIdentity() + if identity == nil { + conn.Close() + return + } ackPayload := HandshakeAckPayload{ Identity: *identity, Accepted: true, @@ -380,6 +384,9 @@ func (t *Transport) performHandshake(pc *PeerConnection) error { }() identity := t.node.GetIdentity() + if identity == nil { + return fmt.Errorf("node identity not initialized") + } payload := HandshakePayload{ Identity: *identity, diff --git a/pkg/node/worker.go b/pkg/node/worker.go index f0ec6a1..6be5583 100644 --- a/pkg/node/worker.go +++ b/pkg/node/worker.go @@ -83,14 +83,17 @@ func (w *Worker) HandleMessage(conn *PeerConnection, msg *Message) { if err != nil { // Send error response - errMsg, _ := NewErrorMessage( - w.node.GetIdentity().ID, - msg.From, - ErrCodeOperationFailed, - err.Error(), - msg.ID, - ) - conn.Send(errMsg) + identity := w.node.GetIdentity() + if identity != nil { + errMsg, _ := NewErrorMessage( + identity.ID, + msg.From, + ErrCodeOperationFailed, + err.Error(), + msg.ID, + ) + conn.Send(errMsg) + } return } @@ -122,6 +125,9 @@ func (w *Worker) handlePing(msg *Message) (*Message, error) { // handleGetStats responds with current miner statistics. func (w *Worker) handleGetStats(msg *Message) (*Message, error) { identity := w.node.GetIdentity() + if identity == nil { + return nil, fmt.Errorf("node identity not initialized") + } stats := StatsPayload{ NodeID: identity.ID, diff --git a/pkg/node/worker_test.go b/pkg/node/worker_test.go index 7684953..f9d0303 100644 --- a/pkg/node/worker_test.go +++ b/pkg/node/worker_test.go @@ -127,6 +127,9 @@ func TestWorker_HandlePing(t *testing.T) { // Create a ping message identity := nm.GetIdentity() + if identity == nil { + t.Fatal("expected identity to be generated") + } pingPayload := PingPayload{SentAt: time.Now().UnixMilli()} pingMsg, err := NewMessage(MsgPing, "sender-id", identity.ID, pingPayload) if err != nil { @@ -183,6 +186,9 @@ func TestWorker_HandleGetStats(t *testing.T) { // Create a get_stats message identity := nm.GetIdentity() + if identity == nil { + t.Fatal("expected identity to be generated") + } msg, err := NewMessage(MsgGetStats, "sender-id", identity.ID, nil) if err != nil { t.Fatalf("failed to create get_stats message: %v", err) @@ -238,6 +244,9 @@ func TestWorker_HandleStartMiner_NoManager(t *testing.T) { // Create a start_miner message identity := nm.GetIdentity() + if identity == nil { + t.Fatal("expected identity to be generated") + } payload := StartMinerPayload{ProfileID: "test-profile"} msg, err := NewMessage(MsgStartMiner, "sender-id", identity.ID, payload) if err != nil { @@ -273,6 +282,9 @@ func TestWorker_HandleStopMiner_NoManager(t *testing.T) { // Create a stop_miner message identity := nm.GetIdentity() + if identity == nil { + t.Fatal("expected identity to be generated") + } payload := StopMinerPayload{MinerName: "test-miner"} msg, err := NewMessage(MsgStopMiner, "sender-id", identity.ID, payload) if err != nil { @@ -308,6 +320,9 @@ func TestWorker_HandleGetLogs_NoManager(t *testing.T) { // Create a get_logs message identity := nm.GetIdentity() + if identity == nil { + t.Fatal("expected identity to be generated") + } payload := GetLogsPayload{MinerName: "test-miner", Lines: 100} msg, err := NewMessage(MsgGetLogs, "sender-id", identity.ID, payload) if err != nil { @@ -343,6 +358,9 @@ func TestWorker_HandleDeploy_Profile(t *testing.T) { // Create a deploy message for profile identity := nm.GetIdentity() + if identity == nil { + t.Fatal("expected identity to be generated") + } payload := DeployPayload{ BundleType: "profile", Data: []byte(`{"id": "test", "name": "Test Profile"}`), @@ -382,6 +400,9 @@ func TestWorker_HandleDeploy_UnknownType(t *testing.T) { // Create a deploy message with unknown type identity := nm.GetIdentity() + if identity == nil { + t.Fatal("expected identity to be generated") + } payload := DeployPayload{ BundleType: "unknown", Data: []byte(`{}`), diff --git a/qodana.yaml b/qodana.yaml new file mode 100644 index 0000000..5435f03 --- /dev/null +++ b/qodana.yaml @@ -0,0 +1,46 @@ +#-------------------------------------------------------------------------------# +# Qodana analysis is configured by qodana.yaml file # +# https://www.jetbrains.com/help/qodana/qodana-yaml.html # +#-------------------------------------------------------------------------------# + +################################################################################# +# WARNING: Do not store sensitive information in this file, # +# as its contents will be included in the Qodana report. # +################################################################################# +version: "1.0" + +#Specify inspection profile for code analysis +profile: + name: qodana.starter + +#Enable inspections +#include: +# - name: + +#Disable inspections +#exclude: +# - name: +# paths: +# - + +#Execute shell command before Qodana execution (Applied in CI/CD pipeline) +#bootstrap: sh ./prepare-qodana.sh + +#Install IDE plugins before Qodana execution (Applied in CI/CD pipeline) +#plugins: +# - id: #(plugin id can be found at https://plugins.jetbrains.com) + +# Quality gate. Will fail the CI/CD pipeline if any condition is not met +# severityThresholds - configures maximum thresholds for different problem severities +# testCoverageThresholds - configures minimum code coverage on a whole project and newly added code +# Code Coverage is available in Ultimate and Ultimate Plus plans +#failureConditions: +# severityThresholds: +# any: 15 +# critical: 5 +# testCoverageThresholds: +# fresh: 70 +# total: 50 + +#Specify Qodana linter for analysis (Applied in CI/CD pipeline) +linter: jetbrains/qodana-go:2025.3