feat: implement version display with commit hash and date

This commit is contained in:
2026-05-09 14:05:25 +03:00
parent debf1b165f
commit 1e7587e11b
11 changed files with 199 additions and 5 deletions

View File

@@ -2,6 +2,8 @@ import com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar
import org.gradle.api.tasks.testing.logging.TestLogEvent.*
import java.nio.file.Files
import java.nio.file.Paths
import java.time.Instant
import java.time.format.DateTimeFormatter
plugins {
java
@@ -20,7 +22,7 @@ node {
}
group = "com.example"
version = "1.0.0-SNAPSHOT"
version = "1.0.0-beta"
repositories {
mavenCentral()
@@ -189,3 +191,37 @@ tasks.register("countCodeLines") {
}
}
tasks.register("generateVersionFile") {
doLast {
// Версия из gradle.properties (по умолчанию 'unspecified', если не задана)
val version = project.version.takeIf { it.toString() != "unspecified" }?.toString() ?: "0.0.0"
// Получение короткого хэша коммита (с обработкой ошибки, если git не доступен)
val commitHash = try {
providers.exec {
commandLine("git", "rev-parse", "--short", "HEAD")
}.standardOutput.asText.get().trim()
} catch (e: Exception) {
logger.warn("Не удалось получить хэш коммита: ${e.message}")
"unknown"
}
val buildTime = DateTimeFormatter.ISO_INSTANT.format(Instant.now())
val propertiesContent = """
version=$version
commit.hash=$commitHash
build.time=$buildTime
""".trimIndent()
val resourceDir = file("src/main/resources")
resourceDir.mkdirs()
file("$resourceDir/version.properties").writeText(propertiesContent)
logger.lifecycle("✅ Файл version.properties создан: версия=$version, коммит=$commitHash")
}
}
tasks.processResources {
dependsOn("generateVersionFile")
}

View File

@@ -21,10 +21,12 @@
</style>
<script setup lang="ts">
import { watch } from 'vue'
import { watch, onMounted } from 'vue'
import { useSettingsStore } from '@/stores/settings'
import { useVersionStore } from '@/stores/version'
const settings = useSettingsStore()
const versionStore = useVersionStore()
watch(() => settings.siteDescription, (desc) => {
let meta = document.querySelector('meta[name="description"]')
@@ -35,4 +37,7 @@ watch(() => settings.siteDescription, (desc) => {
}
meta.setAttribute('content', desc || '')
}, { immediate: true })
onMounted(() => {
versionStore.fetchVersion()
})
</script>

View File

@@ -167,6 +167,16 @@
{{ userInitials }}
</div>
</div>
<!-- Версия сборки (всегда внизу) -->
<div v-if="!sidebarCollapsed" class="px-4 py-3 border-t border-gray-200 text-xs text-gray-500">
{{ versionStore.getFormattedVersion(t) }}
</div>
<div v-else class="p-2 border-t border-gray-200 flex justify-center">
<div class="text-xs text-gray-500 font-mono" :title="versionStore.getFormattedVersion(t)">
{{ versionStore.version?.commitHash?.slice(0, 6) }}
</div>
</div>
</div>
</aside>
@@ -256,9 +266,11 @@ import { useSettingsStore } from '@/stores/settings'
import { useUserStore } from '@/stores/user'
import { useI18n } from 'vue-i18n'
import { useNotification } from '@/composables/useNotification'
import { useVersionStore } from '@/stores/version'
const { notification, showNotification } = useNotification()
const settings = useSettingsStore()
const versionStore = useVersionStore()
const userStore = useUserStore()
const route = useRoute()
const router = useRouter()

View File

@@ -46,7 +46,9 @@
"deleteConfirmation": "Are you sure you want to delete this item? This action cannot be undone.",
"operationSuccess": "Operation completed successfully",
"operationFailed": "Operation failed",
"networkError": "Network error"
"networkError": "Network error",
"version": "Version",
"versionFrom": "from"
},
"dashboard": {
"totalUsers": "Total Users",

View File

@@ -46,7 +46,9 @@
"deleteConfirmation": "Вы уверены, что хотите удалить этот элемент? Это действие необратимо.",
"operationSuccess": "Операция выполнена успешно",
"operationFailed": "Операция не удалась",
"networkError": "Ошибка сети"
"networkError": "Ошибка сети",
"version": "Версия",
"versionFrom": "от"
},
"dashboard": {
"totalUsers": "Всего пользователей",

View File

@@ -0,0 +1,55 @@
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
export interface BuildVersion {
version: string
commitHash: string
buildTime: string // ISO строка, например "2025-04-03T12:34:56Z"
}
export const useVersionStore = defineStore('version', () => {
const version = ref<BuildVersion | null>(null)
const loading = ref(false)
const error = ref<string | null>(null)
async function fetchVersion() {
if (version.value) return
loading.value = true
try {
const res = await fetch('/api/build-info')
if (!res.ok) throw new Error('Failed to fetch version')
const data = await res.json()
version.value = {
version: data.version || '0.0.0',
commitHash: data.commitHash || 'unknown',
buildTime: data.buildTime || ''
}
} catch (err: any) {
console.error(err)
error.value = err.message
version.value = { version: 'dev', commitHash: 'unknown', buildTime: '' }
} finally {
loading.value = false
}
}
// Отформатированная дата сборки (только дата, без времени)
const buildDateFormatted = computed(() => {
if (!version.value?.buildTime) return ''
const date = new Date(version.value.buildTime)
if (isNaN(date.getTime())) return ''
// Формат YYYY-MM-DD (универсальный, без локализации)
return date.toISOString().split('T')[0]
})
// Полная строка версии: "Версия: 1.2.3 (build abc1234 от 2025-04-03)"
// Принимает функцию перевода для слова "от"/"from"
const getFormattedVersion = (t: (key: string) => string) => {
if (!version.value) return t('common.version') + ': ...'
const { version: ver, commitHash } = version.value
const datePart = buildDateFormatted.value ? ` ${t('common.versionFrom')} ${buildDateFormatted.value}` : ''
return `${t('common.version')}: ${ver} (build ${commitHash}${datePart})`
}
return { version, loading, error, fetchVersion, buildDateFormatted, getFormattedVersion }
})

View File

@@ -96,6 +96,12 @@
</div>
</transition>
</div>
<!-- Блок версии внизу -->
<div class="mt-6 text-center text-xs text-gray-500">
{{ versionStore.getFormattedVersion(t) }}
</div>
</div>
</div>
</template>
@@ -105,12 +111,14 @@ import { ref } from 'vue'
import { useRouter } from 'vue-router'
import { useSettingsStore } from '@/stores/settings'
import { useUserStore } from '@/stores/user'
import { useVersionStore } from '@/stores/version'
import { useI18n } from 'vue-i18n'
const settings = useSettingsStore()
const userStore = useUserStore()
const router = useRouter()
const { t, locale } = useI18n()
const versionStore = useVersionStore()
const form = ref({ login: '', password: '' })
const loading = ref(false)
const error = ref('')

View File

@@ -35,6 +35,12 @@
{{ t('register.alreadyHaveAccount') }} <router-link to="/login" class="text-primary-600">{{ t('login.signin') }}</router-link>
</p>
</div>
<!-- Блок версии внизу -->
<div class="mt-6 text-center text-xs text-gray-500">
{{ versionStore.getFormattedVersion(t) }}
</div>
</div>
</div>
</template>
@@ -42,7 +48,9 @@
<script setup lang="ts">
import { ref } from 'vue'
import { useI18n } from 'vue-i18n'
import { useVersionStore } from '@/stores/version'
const { t } = useI18n()
const versionStore = useVersionStore()
const form = ref({ login: '', email: '', password: '' })
const loading = ref(false)
const error = ref('')

View File

@@ -0,0 +1,51 @@
package su.xserver.iikocon;
import io.vertx.core.json.JsonObject;
import java.io.IOException;
import java.io.InputStream;
import java.util.Properties;
import java.util.logging.Logger;
public class BuildVersionProvider {
private static final Logger LOG = Logger.getLogger(BuildVersionProvider.class.getName());
private JsonObject cachedVersion;
public BuildVersionProvider() {
this.cachedVersion = loadVersionData();
}
private JsonObject loadVersionData() {
Properties props = new Properties();
try (InputStream is = getClass().getClassLoader().getResourceAsStream("version.properties")) {
if (is == null) {
LOG.warning("version.properties not found in classpath");
return createFallbackVersion();
}
props.load(is);
} catch (IOException e) {
LOG.severe("Failed to read version.properties: " + e.getMessage());
return createFallbackVersion();
}
String version = props.getProperty("version", "0.0.0");
String commitHash = props.getProperty("commit.hash", "unknown");
String buildTime = props.getProperty("build.time", "");
return new JsonObject()
.put("version", version)
.put("commitHash", commitHash)
.put("buildTime", buildTime);
}
private JsonObject createFallbackVersion() {
return new JsonObject()
.put("version", "0.0.0-dev")
.put("commitHash", "unknown")
.put("buildTime", "1970-01-01T00:00:00Z");
}
public JsonObject getVersion() {
return cachedVersion;
}
}

View File

@@ -30,6 +30,8 @@ import su.xserver.iikocon.iiko.IikoOlapClient;
import su.xserver.iikocon.iiko.OlapQueryService;
import su.xserver.iikocon.service.*;
import java.io.IOException;
import java.io.InputStream;
import java.net.URI;
import java.util.ArrayList;
import java.util.List;
@@ -39,6 +41,7 @@ public class MainVerticle extends AbstractVerticle {
private final Logger log = LoggerFactory.getLogger("[MainVerticle]");
private BuildVersionProvider versionProvider;
private DataBaseService db;
private RedisService redis;
private HttpServer httpServer;
@@ -52,11 +55,13 @@ public class MainVerticle extends AbstractVerticle {
private OlapQueryService olapQueryService;
@Override
public void start(Promise<Void> startPromise) throws ClassNotFoundException {
public void start(Promise<Void> startPromise) throws ClassNotFoundException, IOException {
Class.forName("com.mysql.cj.jdbc.Driver");
Class.forName("org.postgresql.Driver");
versionProvider = new BuildVersionProvider();
ConfigStoreOptions classpathStore = new ConfigStoreOptions()
.setType("file")
.setFormat("json")
@@ -276,6 +281,13 @@ public class MainVerticle extends AbstractVerticle {
}
});
router.get("/api/build-info").handler(rc -> {
JsonObject version = versionProvider.getVersion();
rc.response()
.putHeader("Content-Type", "application/json")
.end(version.encode());
});
// Rate Limiter Handler
RedisRateLimiter limiter = new RedisRateLimiter(
redis.getRedis(), 60, 60_000

View File

@@ -0,0 +1,3 @@
version=1.0.0-beta
commit.hash=debf1b1
build.time=2026-05-09T11:03:39.671956300Z