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:
parent
84635c3c17
commit
fa3047a314
9 changed files with 166 additions and 56 deletions
78
CHANGELOG.md
Normal file
78
CHANGELOG.md
Normal 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
|
||||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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})
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue