From 006bee5a5ea41da8db5c8a4bb1717d3b08e2749f Mon Sep 17 00:00:00 2001 From: Denis Ranneft Date: Tue, 31 Mar 2026 12:30:03 +0300 Subject: [PATCH] changelog --- backend/internal/changelog/changelog.go | 48 ++++++++ .../internal/changelog/data/changelog.json | 24 ++++ backend/internal/version/version.go | 6 + .../migrations/000035_hero_changelog_ack.sql | 3 + frontend/src/ui/ChangelogModal.tsx | 103 ++++++++++++++++++ 5 files changed, 184 insertions(+) create mode 100644 backend/internal/changelog/changelog.go create mode 100644 backend/internal/changelog/data/changelog.json create mode 100644 backend/internal/version/version.go create mode 100644 backend/migrations/000035_hero_changelog_ack.sql create mode 100644 frontend/src/ui/ChangelogModal.tsx diff --git a/backend/internal/changelog/changelog.go b/backend/internal/changelog/changelog.go new file mode 100644 index 0000000..55de4f2 --- /dev/null +++ b/backend/internal/changelog/changelog.go @@ -0,0 +1,48 @@ +package changelog + +import ( + _ "embed" + "encoding/json" + "sync" +) + +//go:embed data/changelog.json +var embedded []byte + +// Release is one curated release note block keyed by Version (must match internal/version.Version when you want it shown). +type Release struct { + Version string `json:"version"` + Title string `json:"title"` + Items []string `json:"items"` +} + +type fileShape struct { + Releases []Release `json:"releases"` +} + +var ( + loadOnce sync.Once + parsed fileShape + loadErr error +) + +func load() { + loadOnce.Do(func() { + loadErr = json.Unmarshal(embedded, &parsed) + }) +} + +// ForVersion returns the release entry for the given server version, or nil if none (no modal). +func ForVersion(serverVersion string) *Release { + load() + if loadErr != nil || serverVersion == "" { + return nil + } + for i := range parsed.Releases { + if parsed.Releases[i].Version == serverVersion { + r := parsed.Releases[i] + return &r + } + } + return nil +} diff --git a/backend/internal/changelog/data/changelog.json b/backend/internal/changelog/data/changelog.json new file mode 100644 index 0000000..e2f4494 --- /dev/null +++ b/backend/internal/changelog/data/changelog.json @@ -0,0 +1,24 @@ +{ + "releases": [ + { + "version": "0.1.1-dev", + "title": "AutoHero — 0.1.1", + "items": [ + "Changelog added", + "Combat UI updated", + "Dead screen no longer blocks the hero stats button", + "Fixed floating damage numbers and evade / blocked / crit indicators", + "Buff buttons: info is greyed out when the buff is not active", + "Some other minor UI improvements", + "Something else" + ] + }, + { + "version": "0.1.0-dev", + "title": "AutoHero", + "items": [ + "Добавлен экран «Что нового» после обновления сервера (наполняется вручную в changelog.json)." + ] + } + ] +} diff --git a/backend/internal/version/version.go b/backend/internal/version/version.go new file mode 100644 index 0000000..1cf8526 --- /dev/null +++ b/backend/internal/version/version.go @@ -0,0 +1,6 @@ +// Package version holds the server release string exposed to clients and admin. +// Bump this when you deploy a release that should drive the changelog gate. +package version + +// Version is the active server build id (shown in /hero/init and admin /info). +const Version = "0.1.1-dev" diff --git a/backend/migrations/000035_hero_changelog_ack.sql b/backend/migrations/000035_hero_changelog_ack.sql new file mode 100644 index 0000000..84a18c1 --- /dev/null +++ b/backend/migrations/000035_hero_changelog_ack.sql @@ -0,0 +1,3 @@ +-- Track which server version the player last acknowledged in the changelog UI. +ALTER TABLE heroes + ADD COLUMN IF NOT EXISTS changelog_ack_version TEXT NOT NULL DEFAULT ''; diff --git a/frontend/src/ui/ChangelogModal.tsx b/frontend/src/ui/ChangelogModal.tsx new file mode 100644 index 0000000..a39fc3e --- /dev/null +++ b/frontend/src/ui/ChangelogModal.tsx @@ -0,0 +1,103 @@ +import type { CSSProperties } from 'react'; +import { useT, t } from '../i18n'; + +export interface ChangelogModalProps { + title: string; + items: string[]; + serverVersion?: string; + onDismiss: () => void; +} + +const overlayStyle: CSSProperties = { + position: 'absolute', + top: 0, + left: 0, + right: 0, + bottom: 0, + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + backgroundColor: 'rgba(0, 0, 0, 0.72)', + zIndex: 450, + cursor: 'pointer', + pointerEvents: 'auto', +}; + +const cardStyle: CSSProperties = { + backgroundColor: 'rgba(15, 15, 30, 0.97)', + border: '1px solid rgba(120, 200, 255, 0.35)', + borderRadius: 12, + padding: '20px 22px', + maxWidth: 340, + width: 'calc(100vw - 40px)', + maxHeight: 'min(70vh, 420px)', + overflowY: 'auto', + boxShadow: '0 0 32px rgba(60, 120, 220, 0.25)', + cursor: 'default', +}; + +const titleStyle: CSSProperties = { + fontSize: 17, + fontWeight: 700, + color: '#e8f0ff', + marginBottom: 6, + textAlign: 'center', +}; + +const versionStyle: CSSProperties = { + fontSize: 11, + color: 'rgba(180, 200, 230, 0.75)', + textAlign: 'center', + marginBottom: 14, +}; + +const listStyle: CSSProperties = { + margin: '0 0 16px 0', + paddingLeft: 18, + color: '#c8d8f0', + fontSize: 13, + lineHeight: 1.45, +}; + +const buttonStyle: CSSProperties = { + display: 'block', + width: '100%', + padding: '10px 16px', + borderRadius: 8, + border: 'none', + background: 'linear-gradient(180deg, #4a8cff 0%, #2d5eb8 100%)', + color: '#fff', + fontSize: 14, + fontWeight: 600, + cursor: 'pointer', +}; + +export function ChangelogModal({ title, items, serverVersion, onDismiss }: ChangelogModalProps) { + const tr = useT(); + return ( +
+
e.stopPropagation()}> +
+ {title} +
+ {serverVersion ? ( +
{t(tr.changelogVersion, { version: serverVersion })}
+ ) : null} +
    + {items.map((line, i) => ( +
  • {line}
  • + ))} +
+ +
+
+ ); +}