diff --git a/cmd/webserver.go b/cmd/webserver.go index 4156e9f8..97d6e2d0 100644 --- a/cmd/webserver.go +++ b/cmd/webserver.go @@ -367,6 +367,9 @@ func startWebServer(c *core.CliContext) error { apiV1Route.GET("/exchange_rates/latest.json", bindApi(api.ExchangeRates.LatestExchangeRateHandler)) apiV1Route.POST("/exchange_rates/user_custom/update.json", bindApi(api.ExchangeRates.UserCustomExchangeRateUpdateHandler)) apiV1Route.POST("/exchange_rates/user_custom/delete.json", bindApi(api.ExchangeRates.UserCustomExchangeRateDeleteHandler)) + + // System + apiV1Route.GET("/systems/version.json", bindApi(api.Systems.VersionHandler)) } } diff --git a/ezbookkeeping.go b/ezbookkeeping.go index f84b2c08..24577f88 100644 --- a/ezbookkeeping.go +++ b/ezbookkeeping.go @@ -28,6 +28,7 @@ var ( func main() { settings.Version = Version settings.CommitHash = CommitHash + settings.BuildTime = BuildUnixTime cmd := &cli.Command{ Name: "ezBookkeeping", diff --git a/pkg/api/systems.go b/pkg/api/systems.go new file mode 100644 index 00000000..7b926867 --- /dev/null +++ b/pkg/api/systems.go @@ -0,0 +1,29 @@ +package api + +import ( + "github.com/mayswind/ezbookkeeping/pkg/core" + "github.com/mayswind/ezbookkeeping/pkg/errs" + "github.com/mayswind/ezbookkeeping/pkg/settings" +) + +// SystemsApi represents system api +type SystemsApi struct{} + +// Initialize a system api singleton instance +var ( + Systems = &SystemsApi{} +) + +// VersionHandler returns the server version and commit hash +func (a *SystemsApi) VersionHandler(c *core.WebContext) (any, *errs.Error) { + result := make(map[string]string) + + result["version"] = settings.Version + result["commitHash"] = settings.CommitHash + + if settings.BuildTime != "" { + result["buildTime"] = settings.BuildTime + } + + return result, nil +} diff --git a/pkg/settings/setting_container.go b/pkg/settings/setting_container.go index c9d161d6..9301871a 100644 --- a/pkg/settings/setting_container.go +++ b/pkg/settings/setting_container.go @@ -9,6 +9,7 @@ type ConfigContainer struct { var ( Version string CommitHash string + BuildTime string Container = &ConfigContainer{} ) diff --git a/src/core/version.ts b/src/core/version.ts new file mode 100644 index 00000000..bf7169e9 --- /dev/null +++ b/src/core/version.ts @@ -0,0 +1,5 @@ +export interface VersionInfo { + readonly version: string; + readonly commitHash: string; + readonly buildTime?: string; +} diff --git a/src/lib/services.ts b/src/lib/services.ts index e8cd937a..3a322db8 100644 --- a/src/lib/services.ts +++ b/src/lib/services.ts @@ -5,6 +5,9 @@ import type { ApiResponse } from '@/core/api.ts'; import type { ApplicationCloudSetting } from '@/core/setting.ts'; +import type { + VersionInfo +} from '@/core/version.ts'; import { TransactionType } from '@/core/transaction.ts'; @@ -599,6 +602,9 @@ export default { deleteUserCustomExchangeRate: (req: UserCustomExchangeRateDeleteRequest): ApiResponsePromise => { return axios.post>('v1/exchange_rates/user_custom/delete.json', req); }, + getServerVersion: (): ApiResponsePromise => { + return axios.get>('v1/systems/version.json'); + }, generateQrCodeUrl: (qrCodeName: string): string => { return `${getBasePath()}${BASE_QRCODE_PATH}/${qrCodeName}.png`; }, diff --git a/src/lib/ui/common.ts b/src/lib/ui/common.ts index 43f8ac81..7479a9c7 100644 --- a/src/lib/ui/common.ts +++ b/src/lib/ui/common.ts @@ -135,3 +135,39 @@ export function startDownloadFile(fileName: string, fileData: Blob): void { dataLink.click(); } + +export function clearBrowserCaches(): Promise { + if (!window.caches) { + logger.error('caches API is not supported in this browser'); + return Promise.reject(); + } + + return new Promise((resolve, reject) => { + window.caches.keys().then(cacheNames => { + const promises = []; + + for (let i = 0; i < cacheNames.length; i++) { + const cacheName = cacheNames[i]; + promises.push(window.caches.delete(cacheName).then(success => { + if (success) { + logger.info(`cache "${cacheName}" cleared successfully`); + return Promise.resolve(cacheName); + } else { + logger.warn(`failed to clear cache "${cacheName}"`); + return Promise.reject(cacheName); + } + })); + } + + Promise.all(promises).then(() => { + logger.info("all caches cleared successfully"); + resolve(); + }).catch(() => { + resolve(); + }); + }).catch(error => { + logger.warn("failed to clear cache", error); + reject(error); + }); + }); +} diff --git a/src/lib/version.ts b/src/lib/version.ts index 28b208a0..6aa3f098 100644 --- a/src/lib/version.ts +++ b/src/lib/version.ts @@ -1,13 +1,17 @@ +import type { VersionInfo } from '@/core/version.ts'; + import { getBasePath } from './web.ts'; -export function isProduction(): boolean { - return __EZBOOKKEEPING_IS_PRODUCTION__; -} +const clientVersionHolder: VersionInfo = { + version: __EZBOOKKEEPING_VERSION__, + commitHash: __EZBOOKKEEPING_BUILD_COMMIT_HASH__, + buildTime: __EZBOOKKEEPING_BUILD_UNIX_TIME__ +}; -export function getVersion(): string { - const isRelease = !getBuildTime(); - const commitHash = __EZBOOKKEEPING_BUILD_COMMIT_HASH__; - let version = __EZBOOKKEEPING_VERSION__; +export function formatDisplayVersion(versionInfo: VersionInfo): string { + const isRelease = !versionInfo.buildTime; + const commitHash = versionInfo.commitHash; + let version = versionInfo.version; if (version && (!isRelease || !isProduction())) { version += '-dev'; @@ -15,6 +19,8 @@ export function getVersion(): string { if (!version) { version = 'unknown'; + } else { + version = 'v' + version; } if (commitHash) { @@ -24,8 +30,20 @@ export function getVersion(): string { return version; } -export function getBuildTime(): string { - return __EZBOOKKEEPING_BUILD_UNIX_TIME__; +export function isProduction(): boolean { + return __EZBOOKKEEPING_IS_PRODUCTION__; +} + +export function getClientVersionInfo(): VersionInfo { + return clientVersionHolder; +} + +export function getClientDisplayVersion(): string { + return formatDisplayVersion(clientVersionHolder); +} + +export function getClientBuildTime(): string { + return clientVersionHolder.buildTime || ''; } export function getMobileVersionPath(): string { diff --git a/src/locales/de.json b/src/locales/de.json index f33e9c1d..7d0621ac 100644 --- a/src/locales/de.json +++ b/src/locales/de.json @@ -2123,6 +2123,9 @@ "Switch to Desktop Version": "Zur Desktop-Version wechseln", "Are you sure you want to switch to desktop version?": "Sind Sie sicher, dass Sie zur Desktop-Version wechseln möchten?", "About": "Über", + "Refresh Browser Cache": "Refresh Browser Cache", + "Frontend Version": "Frontend Version", + "Backend Version": "Backend Version", "Build Time": "Erstellungszeit", "Official Website": "Offizielle Website", "Report Issue": "Problem melden", diff --git a/src/locales/en.json b/src/locales/en.json index 7a4b6709..cd409f54 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -2123,6 +2123,9 @@ "Switch to Desktop Version": "Switch to Desktop Version", "Are you sure you want to switch to desktop version?": "Are you sure you want to switch to desktop version?", "About": "About", + "Refresh Browser Cache": "Refresh Browser Cache", + "Frontend Version": "Frontend Version", + "Backend Version": "Backend Version", "Build Time": "Build Time", "Official Website": "Official Website", "Report Issue": "Report Issue", diff --git a/src/locales/es.json b/src/locales/es.json index 2f14a9fc..87e2c1ec 100644 --- a/src/locales/es.json +++ b/src/locales/es.json @@ -2123,6 +2123,9 @@ "Switch to Desktop Version": "Cambiar a la versión de escritorio", "Are you sure you want to switch to desktop version?": "¿Estás seguro de que quieres cambiar a la versión de escritorio?", "About": "Acerca de", + "Refresh Browser Cache": "Refresh Browser Cache", + "Frontend Version": "Frontend Version", + "Backend Version": "Backend Version", "Build Time": "Tiempo de construcción", "Official Website": "Sitio web oficial", "Report Issue": "Informar problema", diff --git a/src/locales/it.json b/src/locales/it.json index f3f79d9c..22a59da9 100644 --- a/src/locales/it.json +++ b/src/locales/it.json @@ -2123,6 +2123,9 @@ "Switch to Desktop Version": "Passa alla versione desktop", "Are you sure you want to switch to desktop version?": "Sei sicuro di voler passare alla versione desktop?", "About": "Informazioni", + "Refresh Browser Cache": "Refresh Browser Cache", + "Frontend Version": "Frontend Version", + "Backend Version": "Backend Version", "Build Time": "Ora di compilazione", "Official Website": "Sito ufficiale", "Report Issue": "Segnala problema", diff --git a/src/locales/ja.json b/src/locales/ja.json index a77b7c76..a8c41b25 100644 --- a/src/locales/ja.json +++ b/src/locales/ja.json @@ -2123,6 +2123,9 @@ "Switch to Desktop Version": "デスクトップバージョンに切り替え", "Are you sure you want to switch to desktop version?": "デスクトップバージョンに切り替えますか?", "About": "About", + "Refresh Browser Cache": "Refresh Browser Cache", + "Frontend Version": "Frontend Version", + "Backend Version": "Backend Version", "Build Time": "ビルドタイム", "Official Website": "公式ウェブサイト", "Report Issue": "問題を報告", diff --git a/src/locales/pt_BR.json b/src/locales/pt_BR.json index ad535dee..332afaed 100644 --- a/src/locales/pt_BR.json +++ b/src/locales/pt_BR.json @@ -2123,6 +2123,9 @@ "Switch to Desktop Version": "Mudar para a versão desktop", "Are you sure you want to switch to desktop version?": "Tem certeza de que deseja mudar para a versão desktop?", "About": "Sobre", + "Refresh Browser Cache": "Refresh Browser Cache", + "Frontend Version": "Frontend Version", + "Backend Version": "Backend Version", "Build Time": "Hora da construção", "Official Website": "Site oficial", "Report Issue": "Relatar problema", diff --git a/src/locales/ru.json b/src/locales/ru.json index 0ad03188..184df1b6 100644 --- a/src/locales/ru.json +++ b/src/locales/ru.json @@ -2123,6 +2123,9 @@ "Switch to Desktop Version": "Переключиться на десктопную версию", "Are you sure you want to switch to desktop version?": "Вы уверены, что хотите переключиться на десктопную версию?", "About": "О программе", + "Refresh Browser Cache": "Refresh Browser Cache", + "Frontend Version": "Frontend Version", + "Backend Version": "Backend Version", "Build Time": "Время сборки", "Official Website": "Официальный сайт", "Report Issue": "Сообщить о проблеме", diff --git a/src/locales/uk.json b/src/locales/uk.json index 3a6ae1dd..1f04ede3 100644 --- a/src/locales/uk.json +++ b/src/locales/uk.json @@ -2123,6 +2123,9 @@ "Switch to Desktop Version": "Переключитися на десктопну версію", "Are you sure you want to switch to desktop version?": "Ви впевнені, що хочете перейти на десктопну версію?", "About": "Про застосунок", + "Refresh Browser Cache": "Refresh Browser Cache", + "Frontend Version": "Frontend Version", + "Backend Version": "Backend Version", "Build Time": "Час збірки", "Official Website": "Офіційний сайт", "Report Issue": "Повідомити про проблему", diff --git a/src/locales/vi.json b/src/locales/vi.json index c6243dfb..c9549b68 100644 --- a/src/locales/vi.json +++ b/src/locales/vi.json @@ -2123,6 +2123,9 @@ "Switch to Desktop Version": "Chuyển sang phiên bản máy tính để bàn", "Are you sure you want to switch to desktop version?": "Are you sure you want to switch to desktop version?", "About": "Giới thiệu", + "Refresh Browser Cache": "Refresh Browser Cache", + "Frontend Version": "Frontend Version", + "Backend Version": "Backend Version", "Build Time": "Thời gian xây dựng", "Official Website": "Trang web chính thức", "Report Issue": "Báo cáo sự cố", diff --git a/src/locales/zh_Hans.json b/src/locales/zh_Hans.json index 0402d7cc..683a04b5 100644 --- a/src/locales/zh_Hans.json +++ b/src/locales/zh_Hans.json @@ -2123,6 +2123,9 @@ "Switch to Desktop Version": "切换到桌面版", "Are you sure you want to switch to desktop version?": "您确定要切换到桌面版?", "About": "关于", + "Refresh Browser Cache": "刷新浏览器缓存", + "Frontend Version": "前端版本", + "Backend Version": "后端版本", "Build Time": "编译时间", "Official Website": "官方网站", "Report Issue": "报告问题", diff --git a/src/locales/zh_Hant.json b/src/locales/zh_Hant.json index e651f426..0e8db2aa 100644 --- a/src/locales/zh_Hant.json +++ b/src/locales/zh_Hant.json @@ -2123,6 +2123,9 @@ "Switch to Desktop Version": "切換到桌面版", "Are you sure you want to switch to desktop version?": "您確定要切換到桌面版?", "About": "關於", + "Refresh Browser Cache": "刷新瀏覽器快取", + "Frontend Version": "前端版本", + "Backend Version": "後端版本", "Build Time": "建置時間", "Official Website": "官方網站", "Report Issue": "回報問題", diff --git a/src/stores/system.ts b/src/stores/system.ts new file mode 100644 index 00000000..f3b8d39a --- /dev/null +++ b/src/stores/system.ts @@ -0,0 +1,60 @@ +import { defineStore } from 'pinia'; + +import type { VersionInfo } from '@/core/version.ts'; + +import logger from '@/lib/logger.ts'; +import services from '@/lib/services.ts'; +import { getClientVersionInfo } from '@/lib/version.ts'; + +export const useSystemsStore = defineStore('systems', () => { + function checkIfClientVersionMatchServerVersion(): Promise<{ match: boolean, version: VersionInfo }> { + return new Promise((resolve, reject) => { + services.getServerVersion().then(response => { + const data = response.data; + + if (!data || !data.success || !data.result) { + reject({ message: 'Unable to retrieve server version' }); + return; + } + + const clientVersionInfo = getClientVersionInfo(); + + if (data.result.version && clientVersionInfo.version !== data.result.version) { + logger.warn(`client version \"${clientVersionInfo.version}\" does not match server version \"${data.result.version}\"`); + resolve({ + match: false, + version: data.result + }); + } + + if (data.result.commitHash && clientVersionInfo.commitHash !== data.result.commitHash) { + logger.warn(`client commit hash \"${clientVersionInfo.commitHash}\" does not match server commit hash \"${data.result.commitHash}\"`); + resolve({ + match: false, + version: data.result + }); + } + + resolve({ + match: true, + version: data.result + }); + }).catch(error => { + logger.error('failed to retrieve server version', error); + + if (error.response && error.response.data && error.response.data.errorMessage) { + reject({ error: error.response.data }); + } else if (!error.processed) { + reject({ message: 'Unable to retrieve server version' }); + } else { + reject(error); + } + }); + }); + } + + return { + // functions + checkIfClientVersionMatchServerVersion, + }; +}); diff --git a/src/views/base/AboutPageBase.ts b/src/views/base/AboutPageBase.ts index 33979b95..4e89f36a 100644 --- a/src/views/base/AboutPageBase.ts +++ b/src/views/base/AboutPageBase.ts @@ -1,25 +1,41 @@ -import { computed } from 'vue'; +import { ref, computed } from 'vue'; import { useI18n } from '@/locales/helpers.ts'; +import { useSystemsStore } from '@/stores/system.ts'; import { useExchangeRatesStore } from '@/stores/exchangeRates.ts'; +import type { VersionInfo } from '@/core/version.ts'; + import type { LatestExchangeRateResponse } from '@/models/exchange_rate.ts'; import { getMapProvider } from '@/lib/server_settings.ts'; import { getMapWebsite } from '@/lib/map/index.ts'; import { getLicense, getThirdPartyLicenses } from '@/lib/licenses.ts'; -import { getVersion, getBuildTime } from '@/lib/version.ts'; +import { formatDisplayVersion, getClientDisplayVersion, getClientBuildTime } from '@/lib/version.ts'; +import { clearBrowserCaches } from '@/lib/ui/common.ts'; export function useAboutPageBase() { const { tt, formatUnixTimeToLongDateTime } = useI18n(); + const systemsStore = useSystemsStore(); const exchangeRatesStore = useExchangeRatesStore(); - const version = `v${getVersion()}`; + const clientVersion = `${getClientDisplayVersion()}`; - const buildTime = computed(() => { - const time = getBuildTime(); + const serverVersion = ref(null); + const clientVersionMatchServerVersion = ref(true); + + const serverDisplayVersion = computed(() => { + if (!serverVersion.value) { + return ''; + } + + return formatDisplayVersion(serverVersion.value); + }); + + const clientBuildTime = computed(() => { + const time = getClientBuildTime(); if (!time) { return time; @@ -40,16 +56,35 @@ export function useAboutPageBase() { const licenseLines = computed(() => getLicense().replace(/\r/g, '').split('\n')); const thirdPartyLicenses = computed(() => getThirdPartyLicenses()); + function refreshBrowserCache(): void { + clearBrowserCaches().then(() => { + location.reload(); + }); + } + + function init(): void { + systemsStore.checkIfClientVersionMatchServerVersion().then(({ match, version }) => { + serverVersion.value = version; + clientVersionMatchServerVersion.value = match; + }); + } + return { // constants - version, + clientVersion, + // states + clientVersionMatchServerVersion, // computed states - buildTime, + serverDisplayVersion, + clientBuildTime, exchangeRatesData, isUserCustomExchangeRates, mapProviderName, mapProviderWebsite, licenseLines, - thirdPartyLicenses + thirdPartyLicenses, + // functions + refreshBrowserCache, + init }; } diff --git a/src/views/base/LoginPageBase.ts b/src/views/base/LoginPageBase.ts index 4e20936d..634e197f 100644 --- a/src/views/base/LoginPageBase.ts +++ b/src/views/base/LoginPageBase.ts @@ -9,7 +9,7 @@ import { useExchangeRatesStore } from '@/stores/exchangeRates.ts'; import type { AuthResponse } from '@/models/auth_response.ts'; import { getLoginPageTips } from '@/lib/server_settings.ts'; -import { getVersion } from '@/lib/version.ts'; +import { getClientDisplayVersion } from '@/lib/version.ts'; import { setExpenseAndIncomeAmountColor } from '@/lib/ui/common.ts'; export function useLoginPageBase() { @@ -19,7 +19,7 @@ export function useLoginPageBase() { const settingsStore = useSettingsStore(); const exchangeRatesStore = useExchangeRatesStore(); - const version = `v${getVersion()}`; + const version = `${getClientDisplayVersion()}`; const username = ref(''); const password = ref(''); diff --git a/src/views/base/UnlockPageBase.ts b/src/views/base/UnlockPageBase.ts index c0be1b55..72f5c9f6 100644 --- a/src/views/base/UnlockPageBase.ts +++ b/src/views/base/UnlockPageBase.ts @@ -11,7 +11,7 @@ import { useExchangeRatesStore } from '@/stores/exchangeRates.ts'; import { isWebAuthnSupported } from '@/lib/webauthn.ts'; import { hasWebAuthnConfig } from '@/lib/userstate.ts'; -import { getVersion } from '@/lib/version.ts'; +import { getClientDisplayVersion } from '@/lib/version.ts'; import { setExpenseAndIncomeAmountColor } from '@/lib/ui/common.ts'; export function useUnlockPageBase() { @@ -24,7 +24,7 @@ export function useUnlockPageBase() { const transactionsStore = useTransactionsStore(); const exchangeRatesStore = useExchangeRatesStore(); - const version: string = `v${getVersion()}`; + const version: string = `${getClientDisplayVersion()}`; const pinCode = ref(''); diff --git a/src/views/desktop/AboutPage.vue b/src/views/desktop/AboutPage.vue index f2dc4948..e7fba449 100644 --- a/src/views/desktop/AboutPage.vue +++ b/src/views/desktop/AboutPage.vue @@ -1,22 +1,34 @@