diff --git a/CHANGELOG.md b/CHANGELOG.md index b82207d..afe240e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -25,6 +25,43 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 --- +## [0.2.0] - 2026-01-25 + +### Added +- **Full version string for hosting panels** - Added `FullVersion` field to artifact entries + - Format: `{version}-{hash}` (e.g., `24769-315823736cfbc085104ca0d32779311cd2f1a5a8`) + - Compatible with Pterodactyl, Pelican, and similar hosting panel egg configurations + +- **Artifact statistics in API response** - Added `stats` object to metadata + - Includes counts for: `total`, `recommended`, `latest`, `active`, `deprecated`, `eol` + - Calculated from filtered results before pagination + - Enables frontend to show accurate totals regardless of current page + +### Fixed +- **Pagination total count** - Fixed incorrect total count in pagination metadata + - Previously returned count of paginated results instead of total filtered results + - Created `ArtifactsResult` struct to properly track total count after filtering but before pagination + - `hasMore` now correctly indicates if more pages are available + +- **Latest vs Recommended logic** - Fixed support status assignment per CFX EOL policy + - **Latest** = Single newest version (for testing/bleeding edge) + - **Recommended** = Next 3 versions after Latest (stable for production) + - Support status now dynamically assigned based on version position, not hardcoded thresholds + - See https://aka.cfx.re/eol for CFX official policy + +- **EOL filter default** - Changed `includeEol` default from `true` to `false` + - EOL artifacts are now excluded by default for safety + - Users must explicitly opt-in to see end-of-life versions + +### Changed +- Updated all artifact handlers to use new `ArtifactsResult` return type +- Refactored `generateFullVersion()` helper to accept hash parameter +- Added `ArtifactStats` struct and `calculateStats()` helper function +- Refactored `ProcessGitHubTags` to dynamically assign Latest/Recommended based on sorted position +- Simplified `determineSupportStatus()` to only handle Active/Deprecated/EOL thresholds + +--- + ## [0.1.0] - 2026-01-25 ### Added diff --git a/internal/handlers/artifacts.go b/internal/handlers/artifacts.go index cfcdd64..57a0b0b 100644 --- a/internal/handlers/artifacts.go +++ b/internal/handlers/artifacts.go @@ -37,7 +37,7 @@ func GetArtifacts(c *fiber.Ctx) error { Status: services.SupportStatus(c.Query("status", "")), SortBy: c.Query("sortBy", "version"), SortOrder: c.Query("sortOrder", "desc"), - IncludeEOL: c.QueryBool("includeEol", true), + IncludeEOL: c.QueryBool("includeEol", false), } // Parse limit and offset @@ -91,6 +91,14 @@ func GetArtifacts(c *fiber.Ctx) error { "hasMore": offset+len(artifacts) < totalFiltered, "platforms": platforms, "supportStatuses": statuses, + "stats": fiber.Map{ + "total": result.Stats.Total, + "recommended": result.Stats.Recommended, + "latest": result.Stats.Latest, + "active": result.Stats.Active, + "deprecated": result.Stats.Deprecated, + "eol": result.Stats.EOL, + }, "query": fiber.Map{ "platform": query.Platform, "version": query.Version, diff --git a/internal/services/artifacts.go b/internal/services/artifacts.go index 1ee57f2..2d3d0d9 100644 --- a/internal/services/artifacts.go +++ b/internal/services/artifacts.go @@ -88,10 +88,21 @@ type ArtifactEntry struct { Size int64 } +// ArtifactStats holds counts by support status +type ArtifactStats struct { + Total int `json:"total"` + Recommended int `json:"recommended"` + Latest int `json:"latest"` + Active int `json:"active"` + Deprecated int `json:"deprecated"` + EOL int `json:"eol"` +} + // ArtifactsResult holds paginated results with metadata type ArtifactsResult struct { Data []ArtifactEntry Total int + Stats ArtifactStats FilteredBy string } @@ -228,7 +239,7 @@ func (s *ArtifactsService) ProcessGitHubTags(tags []GitHubTag) ArtifactData { } } - // Sort by version number (descending) + // Sort by version number (descending) - highest version first sort.Slice(artifactTags, func(i, j int) bool { versionA := s.extractVersionNumber(artifactTags[i].Name) versionB := s.extractVersionNumber(artifactTags[j].Name) @@ -237,15 +248,28 @@ func (s *ArtifactsService) ProcessGitHubTags(tags []GitHubTag) ArtifactData { now := time.Now().UTC().Format(time.RFC3339) - for _, tag := range artifactTags { + for idx, tag := range artifactTags { versionNumber := s.extractVersionNumber(tag.Name) version := strconv.Itoa(versionNumber) + // Determine support status based on position in sorted list + // Position 0 = Latest (newest single version) + // Position 1-3 = Recommended (stable versions) + // Rest based on version thresholds + var status SupportStatus + if idx == 0 { + status = Latest + } else if idx <= 3 { + status = Recommended + } else { + status = s.determineSupportStatus(versionNumber) + } + baseEntry := Artifact{ Version: version, Hash: tag.Commit.SHA, Date: now, - SupportStatus: s.determineSupportStatus(versionNumber), + SupportStatus: status, } // Windows artifact @@ -392,7 +416,7 @@ func (s *ArtifactsService) GetArtifacts(query ArtifactsQuery) (*ArtifactsResult, for version, artifact := range processedData.Windows { artifacts = append(artifacts, ArtifactEntry{ Version: version, - FullVersion: s.generateFullVersion(version), + FullVersion: s.generateFullVersion(version, artifact.Hash), Hash: artifact.Hash, Platform: Windows, Date: artifact.Date, @@ -404,7 +428,7 @@ func (s *ArtifactsService) GetArtifacts(query ArtifactsQuery) (*ArtifactsResult, for version, artifact := range processedData.Linux { artifacts = append(artifacts, ArtifactEntry{ Version: version, - FullVersion: s.generateFullVersion(version), + FullVersion: s.generateFullVersion(version, artifact.Hash), Hash: artifact.Hash, Platform: Linux, Date: artifact.Date, @@ -420,6 +444,9 @@ func (s *ArtifactsService) GetArtifacts(query ArtifactsQuery) (*ArtifactsResult, // Store total count after filtering but before pagination totalFiltered := len(filtered) + // Calculate stats from filtered results (before pagination) + stats := s.calculateStats(filtered) + // Apply sorting sorted := s.SortArtifacts(filtered, query.SortBy, query.SortOrder) @@ -432,6 +459,7 @@ func (s *ArtifactsService) GetArtifacts(query ArtifactsQuery) (*ArtifactsResult, return &ArtifactsResult{ Data: paginated, Total: totalFiltered, + Stats: stats, }, nil } @@ -449,14 +477,16 @@ func (s *ArtifactsService) extractVersionNumber(tagName string) int { } func (s *ArtifactsService) determineSupportStatus(version int) SupportStatus { + // Based on CFX EOL policy: https://aka.cfx.re/eol + // This is used for versions beyond the top 4 (Latest + 3 Recommended) + // which are dynamically assigned in ProcessGitHubTags + // Active = still supported but older + // Deprecated = support ending soon + // EOL = no longer supported switch { - case version >= 24500: - return Recommended - case version >= 24000: - return Latest - case version >= 23000: + case version >= 23000: // Still actively supported return Active - case version >= 20000: + case version >= 20000: // Support ending return Deprecated default: return EOL @@ -464,9 +494,31 @@ func (s *ArtifactsService) determineSupportStatus(version int) SupportStatus { } // generateFullVersion creates the full version string for hosting panels like Pterodactyl -// Format: v1.0.0.{build_number} (e.g., v1.0.0.12345) -func (s *ArtifactsService) generateFullVersion(version string) string { - return fmt.Sprintf("v1.0.0.%s", version) +// Format: {version}-{hash} (e.g., 24769-315823736cfbc085104ca0d32779311cd2f1a5a8) +func (s *ArtifactsService) generateFullVersion(version string, hash string) string { + return fmt.Sprintf("%s-%s", version, hash) +} + +// calculateStats calculates artifact counts by support status +func (s *ArtifactsService) calculateStats(artifacts []ArtifactEntry) ArtifactStats { + stats := ArtifactStats{ + Total: len(artifacts), + } + for _, artifact := range artifacts { + switch artifact.SupportStatus { + case Recommended: + stats.Recommended++ + case Latest: + stats.Latest++ + case Active: + stats.Active++ + case Deprecated: + stats.Deprecated++ + case EOL: + stats.EOL++ + } + } + return stats } func (s *ArtifactsService) estimateSize(version string, platform string) int64 {