feat: implement version display with commit hash and date
This commit is contained in:
@@ -2,6 +2,8 @@ import com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar
|
|||||||
import org.gradle.api.tasks.testing.logging.TestLogEvent.*
|
import org.gradle.api.tasks.testing.logging.TestLogEvent.*
|
||||||
import java.nio.file.Files
|
import java.nio.file.Files
|
||||||
import java.nio.file.Paths
|
import java.nio.file.Paths
|
||||||
|
import java.time.Instant
|
||||||
|
import java.time.format.DateTimeFormatter
|
||||||
|
|
||||||
plugins {
|
plugins {
|
||||||
java
|
java
|
||||||
@@ -20,7 +22,7 @@ node {
|
|||||||
}
|
}
|
||||||
|
|
||||||
group = "com.example"
|
group = "com.example"
|
||||||
version = "1.0.0-SNAPSHOT"
|
version = "1.0.0-beta"
|
||||||
|
|
||||||
repositories {
|
repositories {
|
||||||
mavenCentral()
|
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")
|
||||||
|
}
|
||||||
|
|||||||
@@ -21,10 +21,12 @@
|
|||||||
</style>
|
</style>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { watch } from 'vue'
|
import { watch, onMounted } from 'vue'
|
||||||
import { useSettingsStore } from '@/stores/settings'
|
import { useSettingsStore } from '@/stores/settings'
|
||||||
|
import { useVersionStore } from '@/stores/version'
|
||||||
|
|
||||||
const settings = useSettingsStore()
|
const settings = useSettingsStore()
|
||||||
|
const versionStore = useVersionStore()
|
||||||
|
|
||||||
watch(() => settings.siteDescription, (desc) => {
|
watch(() => settings.siteDescription, (desc) => {
|
||||||
let meta = document.querySelector('meta[name="description"]')
|
let meta = document.querySelector('meta[name="description"]')
|
||||||
@@ -35,4 +37,7 @@ watch(() => settings.siteDescription, (desc) => {
|
|||||||
}
|
}
|
||||||
meta.setAttribute('content', desc || '')
|
meta.setAttribute('content', desc || '')
|
||||||
}, { immediate: true })
|
}, { immediate: true })
|
||||||
|
onMounted(() => {
|
||||||
|
versionStore.fetchVersion()
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -167,6 +167,16 @@
|
|||||||
{{ userInitials }}
|
{{ userInitials }}
|
||||||
</div>
|
</div>
|
||||||
</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>
|
</div>
|
||||||
</aside>
|
</aside>
|
||||||
|
|
||||||
@@ -256,9 +266,11 @@ import { useSettingsStore } from '@/stores/settings'
|
|||||||
import { useUserStore } from '@/stores/user'
|
import { useUserStore } from '@/stores/user'
|
||||||
import { useI18n } from 'vue-i18n'
|
import { useI18n } from 'vue-i18n'
|
||||||
import { useNotification } from '@/composables/useNotification'
|
import { useNotification } from '@/composables/useNotification'
|
||||||
|
import { useVersionStore } from '@/stores/version'
|
||||||
|
|
||||||
const { notification, showNotification } = useNotification()
|
const { notification, showNotification } = useNotification()
|
||||||
const settings = useSettingsStore()
|
const settings = useSettingsStore()
|
||||||
|
const versionStore = useVersionStore()
|
||||||
const userStore = useUserStore()
|
const userStore = useUserStore()
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
|||||||
@@ -46,7 +46,9 @@
|
|||||||
"deleteConfirmation": "Are you sure you want to delete this item? This action cannot be undone.",
|
"deleteConfirmation": "Are you sure you want to delete this item? This action cannot be undone.",
|
||||||
"operationSuccess": "Operation completed successfully",
|
"operationSuccess": "Operation completed successfully",
|
||||||
"operationFailed": "Operation failed",
|
"operationFailed": "Operation failed",
|
||||||
"networkError": "Network error"
|
"networkError": "Network error",
|
||||||
|
"version": "Version",
|
||||||
|
"versionFrom": "from"
|
||||||
},
|
},
|
||||||
"dashboard": {
|
"dashboard": {
|
||||||
"totalUsers": "Total Users",
|
"totalUsers": "Total Users",
|
||||||
|
|||||||
@@ -46,7 +46,9 @@
|
|||||||
"deleteConfirmation": "Вы уверены, что хотите удалить этот элемент? Это действие необратимо.",
|
"deleteConfirmation": "Вы уверены, что хотите удалить этот элемент? Это действие необратимо.",
|
||||||
"operationSuccess": "Операция выполнена успешно",
|
"operationSuccess": "Операция выполнена успешно",
|
||||||
"operationFailed": "Операция не удалась",
|
"operationFailed": "Операция не удалась",
|
||||||
"networkError": "Ошибка сети"
|
"networkError": "Ошибка сети",
|
||||||
|
"version": "Версия",
|
||||||
|
"versionFrom": "от"
|
||||||
},
|
},
|
||||||
"dashboard": {
|
"dashboard": {
|
||||||
"totalUsers": "Всего пользователей",
|
"totalUsers": "Всего пользователей",
|
||||||
|
|||||||
55
frontend/src/stores/version.ts
Normal file
55
frontend/src/stores/version.ts
Normal 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 }
|
||||||
|
})
|
||||||
@@ -96,6 +96,12 @@
|
|||||||
</div>
|
</div>
|
||||||
</transition>
|
</transition>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Блок версии внизу -->
|
||||||
|
<div class="mt-6 text-center text-xs text-gray-500">
|
||||||
|
{{ versionStore.getFormattedVersion(t) }}
|
||||||
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -105,12 +111,14 @@ import { ref } from 'vue'
|
|||||||
import { useRouter } from 'vue-router'
|
import { useRouter } from 'vue-router'
|
||||||
import { useSettingsStore } from '@/stores/settings'
|
import { useSettingsStore } from '@/stores/settings'
|
||||||
import { useUserStore } from '@/stores/user'
|
import { useUserStore } from '@/stores/user'
|
||||||
|
import { useVersionStore } from '@/stores/version'
|
||||||
import { useI18n } from 'vue-i18n'
|
import { useI18n } from 'vue-i18n'
|
||||||
|
|
||||||
const settings = useSettingsStore()
|
const settings = useSettingsStore()
|
||||||
const userStore = useUserStore()
|
const userStore = useUserStore()
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const { t, locale } = useI18n()
|
const { t, locale } = useI18n()
|
||||||
|
const versionStore = useVersionStore()
|
||||||
const form = ref({ login: '', password: '' })
|
const form = ref({ login: '', password: '' })
|
||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
const error = ref('')
|
const error = ref('')
|
||||||
|
|||||||
@@ -35,6 +35,12 @@
|
|||||||
{{ t('register.alreadyHaveAccount') }} <router-link to="/login" class="text-primary-600">{{ t('login.signin') }}</router-link>
|
{{ t('register.alreadyHaveAccount') }} <router-link to="/login" class="text-primary-600">{{ t('login.signin') }}</router-link>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Блок версии внизу -->
|
||||||
|
<div class="mt-6 text-center text-xs text-gray-500">
|
||||||
|
{{ versionStore.getFormattedVersion(t) }}
|
||||||
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -42,7 +48,9 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref } from 'vue'
|
import { ref } from 'vue'
|
||||||
import { useI18n } from 'vue-i18n'
|
import { useI18n } from 'vue-i18n'
|
||||||
|
import { useVersionStore } from '@/stores/version'
|
||||||
const { t } = useI18n()
|
const { t } = useI18n()
|
||||||
|
const versionStore = useVersionStore()
|
||||||
const form = ref({ login: '', email: '', password: '' })
|
const form = ref({ login: '', email: '', password: '' })
|
||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
const error = ref('')
|
const error = ref('')
|
||||||
|
|||||||
51
src/main/java/su/xserver/iikocon/BuildVersionProvider.java
Normal file
51
src/main/java/su/xserver/iikocon/BuildVersionProvider.java
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -30,6 +30,8 @@ import su.xserver.iikocon.iiko.IikoOlapClient;
|
|||||||
import su.xserver.iikocon.iiko.OlapQueryService;
|
import su.xserver.iikocon.iiko.OlapQueryService;
|
||||||
import su.xserver.iikocon.service.*;
|
import su.xserver.iikocon.service.*;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
import java.net.URI;
|
import java.net.URI;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
@@ -39,6 +41,7 @@ public class MainVerticle extends AbstractVerticle {
|
|||||||
|
|
||||||
private final Logger log = LoggerFactory.getLogger("[MainVerticle]");
|
private final Logger log = LoggerFactory.getLogger("[MainVerticle]");
|
||||||
|
|
||||||
|
private BuildVersionProvider versionProvider;
|
||||||
private DataBaseService db;
|
private DataBaseService db;
|
||||||
private RedisService redis;
|
private RedisService redis;
|
||||||
private HttpServer httpServer;
|
private HttpServer httpServer;
|
||||||
@@ -52,11 +55,13 @@ public class MainVerticle extends AbstractVerticle {
|
|||||||
private OlapQueryService olapQueryService;
|
private OlapQueryService olapQueryService;
|
||||||
|
|
||||||
@Override
|
@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("com.mysql.cj.jdbc.Driver");
|
||||||
Class.forName("org.postgresql.Driver");
|
Class.forName("org.postgresql.Driver");
|
||||||
|
|
||||||
|
versionProvider = new BuildVersionProvider();
|
||||||
|
|
||||||
ConfigStoreOptions classpathStore = new ConfigStoreOptions()
|
ConfigStoreOptions classpathStore = new ConfigStoreOptions()
|
||||||
.setType("file")
|
.setType("file")
|
||||||
.setFormat("json")
|
.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
|
// Rate Limiter Handler
|
||||||
RedisRateLimiter limiter = new RedisRateLimiter(
|
RedisRateLimiter limiter = new RedisRateLimiter(
|
||||||
redis.getRedis(), 60, 60_000
|
redis.getRedis(), 60, 60_000
|
||||||
|
|||||||
3
src/main/resources/version.properties
Normal file
3
src/main/resources/version.properties
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
version=1.0.0-beta
|
||||||
|
commit.hash=debf1b1
|
||||||
|
build.time=2026-05-09T11:03:39.671956300Z
|
||||||
Reference in New Issue
Block a user