package database import ( "context" "time" "forge.lthn.ai/Snider/Mining/pkg/logging" ) // t := parseSQLiteTimestamp("2006-01-02 15:04:05") // time.Time{...} // t := parseSQLiteTimestamp("") // time.Time{} (zero) func parseSQLiteTimestamp(raw string) time.Time { if raw == "" { 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 parsed, err := time.Parse(format, raw); err == nil { return parsed } } logging.Warn("failed to parse timestamp from database", logging.Fields{"timestamp": raw}) return time.Time{} } // database.ResolutionHigh // "high" — 10-second intervals // database.ResolutionLow // "low" — 1-minute averages type Resolution string const ( ResolutionHigh Resolution = "high" // 10-second intervals ResolutionLow Resolution = "low" // 1-minute averages ) // point := database.HashratePoint{Timestamp: time.Now(), Hashrate: 1234} type HashratePoint struct { Timestamp time.Time `json:"timestamp"` Hashrate int `json:"hashrate"` } // ctx, cancel := context.WithTimeout(ctx, dbInsertTimeout) // 5s ceiling for INSERT const dbInsertTimeout = 5 * time.Second // database.InsertHashratePoint(ctx, "xmrig", "xmrig", HashratePoint{Timestamp: time.Now(), Hashrate: 1234}, ResolutionHigh) func InsertHashratePoint(ctx context.Context, minerName, minerType string, point HashratePoint, resolution Resolution) error { databaseMutex.RLock() defer databaseMutex.RUnlock() if globalDatabase == 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 := globalDatabase.ExecContext(ctx, ` INSERT INTO hashrate_history (miner_name, miner_type, timestamp, hashrate, resolution) VALUES (?, ?, ?, ?, ?) `, minerName, minerType, point.Timestamp, point.Hashrate, string(resolution)) return err } // points, err := database.GetHashrateHistory("xmrig", database.ResolutionHigh, time.Now().Add(-time.Hour), time.Now()) func GetHashrateHistory(minerName string, resolution Resolution, since, until time.Time) ([]HashratePoint, error) { databaseMutex.RLock() defer databaseMutex.RUnlock() if globalDatabase == nil { return nil, nil } rows, err := globalDatabase.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, databaseError("query hashrate history", 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, databaseError("scan row", err) } points = append(points, point) } return points, rows.Err() } // stats, err := database.GetHashrateStats("xmrig") // if stats != nil { logging.Info("stats", logging.Fields{"average": stats.AverageRate}) } 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"` } // stats, err := database.GetHashrateStats("xmrig") // if stats != nil { logging.Info("stats", logging.Fields{"miner": minerName, "average": stats.AverageRate}) } func GetHashrateStats(minerName string) (*HashrateStats, error) { databaseMutex.RLock() defer databaseMutex.RUnlock() if globalDatabase == nil { return nil, nil } // First check if there are any rows for this miner var count int err := globalDatabase.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 = globalDatabase.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 } // allStats, err := database.GetAllMinerStats() // for _, stats := range allStats { logging.Info("stats", logging.Fields{"miner": stats.MinerName, "average": stats.AverageRate}) } func GetAllMinerStats() ([]HashrateStats, error) { databaseMutex.RLock() defer databaseMutex.RUnlock() if globalDatabase == nil { return nil, nil } rows, err := globalDatabase.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() }