refactor: Add reliability fixes, centralized version fetching, and CHANGELOG

Reliability fixes:
- Fix race condition on uninitialized HTTP server in transport.go
- Add proper error logging for HTTP server startup errors
- Fix potential goroutine leak in process cleanup (xmrig_start.go)
- Propagate context to DB writes for proper timeout handling

Architecture improvements:
- Centralize GitHub version fetching via FetchLatestGitHubVersion()
- Add respondWithMiningError() helper for standardized API error responses
- Update XMRig and TTMiner to use centralized version fetcher

Documentation:
- Add CHANGELOG.md with release history
- Update FUTURE_IDEAS.md with demo GIF task

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
snider 2025-12-31 13:33:42 +00:00
parent 84635c3c17
commit fa3047a314
9 changed files with 166 additions and 56 deletions

78
CHANGELOG.md Normal file
View file

@ -0,0 +1,78 @@
# Changelog
All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [Unreleased]
### Added
- Structured logging package with configurable log levels
- Rate limiter with automatic cleanup for API protection
- X-Request-ID middleware for request tracing
- Structured API error responses with error codes and suggestions
- MinerFactory pattern for centralized miner instantiation
- StatsCollector pattern for parallel stats collection
- Context propagation throughout the codebase
- WebSocket event system for real-time updates
- Simulation mode for UI development and testing
- Mermaid architecture diagrams in documentation
### Changed
- Optimized `collectMinerStats()` for parallel execution
- Replaced `log.Printf` with structured logging throughout
- Improved hashrate history with two-tier storage (high-res and low-res)
- Enhanced shutdown handling with proper cleanup
### Fixed
- Race conditions in concurrent database access
- Memory leaks in hashrate history retention
- Context cancellation propagation to database operations
## [0.0.9] - 2025-12-11
### Added
- Enhanced dashboard layout with responsive stats bar
- Setup wizard for first-time configuration
- Admin panel for miner management
- Profile management with multiple miner support
- Live hashrate visualization with Highcharts
- Comprehensive docstrings throughout the mining package
- CI/CD matrix testing and conditional releases
### Changed
- Refactored profile selection to support multiple miners
- Improved UI layout and accessibility
- Enhanced mining configuration management
### Fixed
- UI build and server configuration issues
## [0.0.8] - 2025-11-09
### Added
- Web dashboard (`mbe-mining-dashboard.js`) included in release binaries
- Interactive web interface for miner-cli
## [0.0.7] - 2025-11-09
### Fixed
- Windows build compatibility
## [0.0.6] - 2025-11-09
### Added
- Initial public release
- XMRig miner support
- TT-Miner (GPU) support
- RESTful API with Swagger documentation
- CLI with interactive shell
- Miner autostart configuration
- Hashrate history tracking
[Unreleased]: https://github.com/Snider/Mining/compare/v0.0.9...HEAD
[0.0.9]: https://github.com/Snider/Mining/compare/v0.0.8...v0.0.9
[0.0.8]: https://github.com/Snider/Mining/compare/v0.0.7...v0.0.8
[0.0.7]: https://github.com/Snider/Mining/compare/v0.0.6...v0.0.7
[0.0.6]: https://github.com/Snider/Mining/releases/tag/v0.0.6

View file

@ -133,6 +133,7 @@ deploy/
### GitHub Repository Optimization
- [ ] Add topic tags: `mining`, `monero`, `xmrig`, `cryptocurrency`, `dashboard`, `self-hosted`, `golang`, `angular`
- [ ] Add social preview image
- [ ] Create demo GIF for README showcasing the dashboard UI
- [ ] Create GitHub Discussions for community Q&A
- [ ] Add "Used By" section in README

View file

@ -582,9 +582,9 @@ func (m *Manager) collectSingleMinerStats(miner Miner, minerType string, now tim
// Use context with timeout to prevent hanging on unresponsive miner APIs
ctx, cancel := context.WithTimeout(context.Background(), statsCollectionTimeout)
stats, err := miner.GetStats(ctx)
cancel() // Release context resources immediately
defer cancel() // Ensure context is released after all operations
stats, err := miner.GetStats(ctx)
if err != nil {
logging.Error("failed to get miner stats", logging.Fields{"miner": minerName, "error": err})
return
@ -606,8 +606,8 @@ func (m *Manager) collectSingleMinerStats(miner Miner, minerType string, now tim
Timestamp: point.Timestamp,
Hashrate: point.Hashrate,
}
// Use nil context to let InsertHashratePoint use its default timeout
if err := database.InsertHashratePoint(nil, minerName, minerType, dbPoint, database.ResolutionHigh); err != nil {
// Use the same context for DB writes so they respect timeout/cancellation
if err := database.InsertHashratePoint(ctx, minerName, minerType, dbPoint, database.ResolutionHigh); err != nil {
logging.Warn("failed to persist hashrate", logging.Fields{"miner": minerName, "error": err})
}
}

View file

@ -82,6 +82,31 @@ func respondWithError(c *gin.Context, status int, code string, message string, d
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: 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 ||
@ -606,7 +631,7 @@ func (s *Service) handleStartMinerWithProfile(c *gin.Context) {
profileID := c.Param("id")
profile, exists := s.ProfileManager.GetProfile(profileID)
if !exists {
respondWithError(c, http.StatusNotFound, ErrCodeProfileNotFound, "profile not found", "")
respondWithMiningError(c, ErrProfileNotFound(profileID))
return
}
@ -653,7 +678,7 @@ func (s *Service) handleGetMinerStats(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())
respondWithMiningError(c, ErrMinerNotFound(minerName).WithCause(err))
return
}
stats, err := miner.GetStats(c.Request.Context())
@ -694,7 +719,7 @@ func (s *Service) handleGetMinerLogs(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())
respondWithMiningError(c, ErrMinerNotFound(minerName).WithCause(err))
return
}
logs := miner.GetLogs()

View file

@ -2,11 +2,8 @@ package mining
import (
"bytes"
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"os"
"os/exec"
"path/filepath"
@ -90,24 +87,7 @@ func getTTMinerConfigPath() (string, error) {
// GetLatestVersion fetches the latest version of TT-Miner from the GitHub API.
func (m *TTMiner) GetLatestVersion() (string, error) {
resp, err := getHTTPClient().Get("https://api.github.com/repos/TrailingStop/TT-Miner-release/releases/latest")
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
return "", fmt.Errorf("failed to get latest release: unexpected status code %d", resp.StatusCode)
}
var release struct {
TagName string `json:"tag_name"`
}
if err := json.NewDecoder(resp.Body).Decode(&release); err != nil {
return "", err
}
return release.TagName, nil
return FetchLatestGitHubVersion("TrailingStop", "TT-Miner-release")
}
// Install determines the correct download URL for the latest version of TT-Miner

View file

@ -1,5 +1,12 @@
package mining
import (
"encoding/json"
"fmt"
"io"
"net/http"
)
var (
version = "dev"
commit = "none"
@ -20,3 +27,33 @@ func GetCommit() string {
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.
func FetchLatestGitHubVersion(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
}

View file

@ -2,10 +2,8 @@ package mining
import (
"bytes"
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"os"
"os/exec"
@ -91,24 +89,7 @@ var getXMRigConfigPath = func(instanceName string) (string, error) {
// GetLatestVersion fetches the latest version of XMRig from the GitHub API.
func (m *XMRigMiner) GetLatestVersion() (string, error) {
resp, err := getHTTPClient().Get("https://api.github.com/repos/xmrig/xmrig/releases/latest")
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
return "", fmt.Errorf("failed to get latest release: unexpected status code %d", resp.StatusCode)
}
var release struct {
TagName string `json:"tag_name"`
}
if err := json.NewDecoder(resp.Body).Decode(&release); err != nil {
return "", err
}
return release.TagName, nil
return FetchLatestGitHubVersion("xmrig", "xmrig")
}
// Install determines the correct download URL for the latest version of XMRig

View file

@ -113,11 +113,17 @@ func (m *XMRigMiner) Start(config *Config) error {
// Normal exit
case <-time.After(5 * time.Minute):
// Process didn't exit after 5 minutes - force cleanup
logging.Warn("miner process wait timeout, forcing cleanup")
logging.Warn("miner process wait timeout, forcing cleanup", logging.Fields{"miner": m.Name})
if cmd.Process != nil {
cmd.Process.Kill()
}
<-done // Wait for the inner goroutine to finish
// 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": m.Name})
}
}
m.mu.Lock()

View file

@ -117,7 +117,7 @@ func (t *Transport) Start() error {
err = t.server.ListenAndServe()
}
if err != nil && err != http.ErrServerClosed {
// Log error
logging.Error("HTTP server error", logging.Fields{"error": err, "addr": t.config.ListenAddr})
}
}()
@ -135,12 +135,14 @@ func (t *Transport) Stop() error {
}
t.mu.Unlock()
// Shutdown HTTP server
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
// Shutdown HTTP server if it was started
if t.server != nil {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
if err := t.server.Shutdown(ctx); err != nil {
return fmt.Errorf("server shutdown error: %w", err)
if err := t.server.Shutdown(ctx); err != nil {
return fmt.Errorf("server shutdown error: %w", err)
}
}
t.wg.Wait()