From 1aae29a0b845c37306dd72e99a98f848ab27833f Mon Sep 17 00:00:00 2001 From: martin <74269598+martabal@users.noreply.github.com> Date: Tue, 24 Oct 2023 17:05:42 +0200 Subject: [PATCH] refactor(server, web)!: store latest immich version available on the server (#3565) * refactor: store latest immich version available on the server * don't store admins acknowledgement * merge main * fix: api * feat: custom interval * pr feedback * remove unused code * update environment-variables * pr feedback * ci: fix server tests * fix: dart number * pr feedback * remove proxy * pr feedback * feat: make stringToVersion more flexible * feat(web): disable check * feat: working version * remove env * fix: check if interval exists when updating the interval * feat: show last check * fix: tests * fix: remove availableVersion when updated * fix merge * fix: web * fix e2e tests * merge main * merge main * pr feedback * pr feedback * fix: tests * pr feedback * pr feedback * pr feedback * pr feedback * pr feedback * fix: migration * regenerate api * fix: typo * fix: compare versions * pr feedback * fix * pr feedback * fix: checkIntervalTime on startup * refactor: websockets and interval logic * chore: open api * chore: remove unused code * fix: use interval instead of cron * mobile: handle WS event data as json object --------- Co-authored-by: Jason Rasmussen Co-authored-by: shalong-tanwen <139912620+shalong-tanwen@users.noreply.github.com> --- cli/src/api/open-api/api.ts | 19 ++++ .../shared/providers/websocket.provider.dart | 14 +-- mobile/openapi/.openapi-generator/FILES | 3 + mobile/openapi/README.md | Bin 21915 -> 21991 bytes mobile/openapi/doc/SystemConfigDto.md | Bin 1271 -> 1371 bytes .../doc/SystemConfigNewVersionCheckDto.md | Bin 0 -> 425 bytes mobile/openapi/lib/api.dart | Bin 7159 -> 7214 bytes mobile/openapi/lib/api_client.dart | Bin 21370 -> 21482 bytes .../openapi/lib/model/system_config_dto.dart | Bin 5671 -> 6044 bytes .../system_config_new_version_check_dto.dart | Bin 0 -> 3050 bytes .../openapi/test/system_config_dto_test.dart | Bin 1787 -> 1926 bytes ...tem_config_new_version_check_dto_test.dart | Bin 0 -> 610 bytes server/immich-openapi-specs.json | 15 +++ server/src/domain/album/dto/get-albums.dto.ts | 2 +- server/src/domain/domain.constant.spec.ts | 71 +++++++++++++- server/src/domain/domain.constant.ts | 51 ++++++++-- server/src/domain/job/job.constants.ts | 2 +- .../repositories/communication.repository.ts | 4 + server/src/domain/repositories/index.ts | 1 + .../src/domain/repositories/job.repository.ts | 2 +- .../repositories/server-info.repository.ts | 15 +++ .../server-info/server-info.service.spec.ts | 22 ++++- .../domain/server-info/server-info.service.ts | 73 +++++++++++++- .../system-config-new-version-check.dto.ts | 6 ++ .../system-config/dto/system-config.dto.ts | 6 ++ .../system-config/system-config.core.ts | 5 +- .../system-config.service.spec.ts | 7 +- server/src/immich/app.service.ts | 10 +- server/src/immich/app.utils.ts | 4 +- server/src/immich/main.ts | 6 +- .../infra/entities/system-config.entity.ts | 5 + server/src/infra/infra.module.ts | 3 + .../repositories/communication.repository.ts | 13 ++- server/src/infra/repositories/index.ts | 1 + .../repositories/server-info.repository.ts | 12 +++ server/src/microservices/app.service.ts | 8 +- server/src/microservices/main.ts | 5 +- .../communication.repository.mock.ts | 1 + server/test/repositories/index.ts | 1 + .../system-info.repository.mock.ts | 7 ++ web/src/api/open-api/api.ts | 19 ++++ .../new-version-check-settings.svelte | 92 ++++++++++++++++++ .../version-announcement-box.svelte | 44 ++++----- web/src/lib/stores/websocket.ts | 19 +++- web/src/lib/utils/get-github-version.ts | 15 --- web/src/routes/+layout.server.ts | 6 +- web/src/routes/+layout.svelte | 2 +- .../routes/admin/system-settings/+page.svelte | 5 + 48 files changed, 497 insertions(+), 99 deletions(-) create mode 100644 mobile/openapi/doc/SystemConfigNewVersionCheckDto.md create mode 100644 mobile/openapi/lib/model/system_config_new_version_check_dto.dart create mode 100644 mobile/openapi/test/system_config_new_version_check_dto_test.dart create mode 100644 server/src/domain/repositories/server-info.repository.ts create mode 100644 server/src/domain/system-config/dto/system-config-new-version-check.dto.ts create mode 100644 server/src/infra/repositories/server-info.repository.ts create mode 100644 server/test/repositories/system-info.repository.mock.ts create mode 100644 web/src/lib/components/admin-page/settings/new-version-check-settings/new-version-check-settings.svelte delete mode 100644 web/src/lib/utils/get-github-version.ts diff --git a/cli/src/api/open-api/api.ts b/cli/src/api/open-api/api.ts index 2133d04113..fffb9914a5 100644 --- a/cli/src/api/open-api/api.ts +++ b/cli/src/api/open-api/api.ts @@ -3283,6 +3283,12 @@ export interface SystemConfigDto { * @memberof SystemConfigDto */ 'map': SystemConfigMapDto; + /** + * + * @type {SystemConfigNewVersionCheckDto} + * @memberof SystemConfigDto + */ + 'newVersionCheck': SystemConfigNewVersionCheckDto; /** * * @type {SystemConfigOAuthDto} @@ -3572,6 +3578,19 @@ export interface SystemConfigMapDto { */ 'tileUrl': string; } +/** + * + * @export + * @interface SystemConfigNewVersionCheckDto + */ +export interface SystemConfigNewVersionCheckDto { + /** + * + * @type {boolean} + * @memberof SystemConfigNewVersionCheckDto + */ + 'enabled': boolean; +} /** * * @export diff --git a/mobile/lib/shared/providers/websocket.provider.dart b/mobile/lib/shared/providers/websocket.provider.dart index 1dda262f50..83fcb000fe 100644 --- a/mobile/lib/shared/providers/websocket.provider.dart +++ b/mobile/lib/shared/providers/websocket.provider.dart @@ -1,5 +1,3 @@ -import 'dart:convert'; - import 'package:flutter/foundation.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/modules/login/providers/authentication.provider.dart'; @@ -175,9 +173,8 @@ class WebsocketNotifier extends StateNotifier { .where((c) => c.action == PendingAction.assetDelete) .toList(); if (deleteChanges.isNotEmpty) { - List remoteIds = deleteChanges - .map((a) => jsonDecode(a.value.toString()).toString()) - .toList(); + List remoteIds = + deleteChanges.map((a) => a.value.toString()).toList(); ref.read(syncServiceProvider).handleRemoteAssetRemoval(remoteIds); state = state.copyWith( pendingChanges: state.pendingChanges @@ -188,21 +185,20 @@ class WebsocketNotifier extends StateNotifier { } _handleOnUploadSuccess(dynamic data) { - final jsonString = jsonDecode(data.toString()); - final dto = AssetResponseDto.fromJson(jsonString); + final dto = AssetResponseDto.fromJson(data); if (dto != null) { final newAsset = Asset.remote(dto); ref.watch(assetProvider.notifier).onNewAssetUploaded(newAsset); } } - _handleOnConfigUpdate(dynamic data) { + _handleOnConfigUpdate(dynamic _) { ref.read(serverInfoProvider.notifier).getServerFeatures(); ref.read(serverInfoProvider.notifier).getServerConfig(); } // Refresh updated assets - _handleServerUpdates(dynamic data) { + _handleServerUpdates(dynamic _) { ref.read(assetProvider.notifier).getAllAsset(); } diff --git a/mobile/openapi/.openapi-generator/FILES b/mobile/openapi/.openapi-generator/FILES index 747c435dd3..3bb00bb242 100644 --- a/mobile/openapi/.openapi-generator/FILES +++ b/mobile/openapi/.openapi-generator/FILES @@ -130,6 +130,7 @@ doc/SystemConfigFFmpegDto.md doc/SystemConfigJobDto.md doc/SystemConfigMachineLearningDto.md doc/SystemConfigMapDto.md +doc/SystemConfigNewVersionCheckDto.md doc/SystemConfigOAuthDto.md doc/SystemConfigPasswordLoginDto.md doc/SystemConfigReverseGeocodingDto.md @@ -298,6 +299,7 @@ lib/model/system_config_f_fmpeg_dto.dart lib/model/system_config_job_dto.dart lib/model/system_config_machine_learning_dto.dart lib/model/system_config_map_dto.dart +lib/model/system_config_new_version_check_dto.dart lib/model/system_config_o_auth_dto.dart lib/model/system_config_password_login_dto.dart lib/model/system_config_reverse_geocoding_dto.dart @@ -453,6 +455,7 @@ test/system_config_f_fmpeg_dto_test.dart test/system_config_job_dto_test.dart test/system_config_machine_learning_dto_test.dart test/system_config_map_dto_test.dart +test/system_config_new_version_check_dto_test.dart test/system_config_o_auth_dto_test.dart test/system_config_password_login_dto_test.dart test/system_config_reverse_geocoding_dto_test.dart diff --git a/mobile/openapi/README.md b/mobile/openapi/README.md index 07d2cf402fe02b8fdc9cba55f9f41908f00f6d80..80735dcd012e154ff5c0902aedd1d75b79840215 100644 GIT binary patch delta 54 vcmbQen(_H+#tpaq`2AAL!%~ZiGxPJDGg6bYCqM8L!w{S3FR}Ty-vKrNc(N9< delta 14 WcmaF9nsN4O#tpaqHmmy|U;_X)ss^I~ diff --git a/mobile/openapi/doc/SystemConfigDto.md b/mobile/openapi/doc/SystemConfigDto.md index d426bef34564b45e9173e6933d329929f85a0794..98a6266402860fda08c1b95c7bafa289fbca4e3d 100644 GIT binary patch delta 67 wcmey)d7EnkI}?9iYI#^{QE_H|o^wWOa`t3JW)XfrG_i+_!sud)zcVre06mZxrvLx| delta 12 Tcmcc3^__DAJJaTDrVmU2A%z67 diff --git a/mobile/openapi/doc/SystemConfigNewVersionCheckDto.md b/mobile/openapi/doc/SystemConfigNewVersionCheckDto.md new file mode 100644 index 0000000000000000000000000000000000000000..18b922de2039c07def64bf3dab2b2ee2699217c8 GIT binary patch literal 425 zcma)2O-lnY5WVMD4D6vcknLShwcCSWm!jgO6gJ&V+t5uWBqLJr$D6DLs~62Bd6V}s zGZj!kuY;`}864{8b7FK0@_Tz|@%>`x7o5LE?_qz&*9VN^iS{Yf8CbX{k3 zWMN&P$S}FoZ!i9{l2HhgyFwkE)-iuZwwOe#9l{5^oI|-9OntP0b96>6ab~EilZ#hK zslTw0G7q^M&`6oNQtpU`@tKhjo6UV50+?V`mH>lIyxrc^_3UXmZ&u6NnaWWA=Fqb) kO{qOaHIARA@U5P&AO5M$%TVY|AnW;u#aH03@Tm~u6IDlp+5i9m literal 0 HcmV?d00001 diff --git a/mobile/openapi/lib/api.dart b/mobile/openapi/lib/api.dart index 091a38e36d25dc67a14d31842cd67dcfeb7e7904..b030be56f4bb7e612892bce491927b78fbd498d9 100644 GIT binary patch delta 34 pcmexvzRqGpu9#q6YI%HFYEf}!eqMZXMrv~QT)V delta 14 WcmaF0oblH(#tqtzn`b-v3jqK&H3mQc diff --git a/mobile/openapi/lib/model/system_config_dto.dart b/mobile/openapi/lib/model/system_config_dto.dart index 8e15d7e21996579bd0ad8fdf5cd41c47410e341a..89c7e5f7d2cb7e45118e7e9ac2c466aed73ebc9e 100644 GIT binary patch delta 315 zcmZ3kGe>{JFGl{n)bg;@qT*1$jl#gJN?ouQdm{;N%6O?EpxxbyxrZ delta 45 zcmV+|0Mh@QFQ+W9`T?^Y0$l;4tpftH-UYG&v#|#^0kaATmI1TZ3Z(fZP~&mB1$4Mrn)^Veej?Co?m{q$xry@A_zAEqJP%waZPz{mN`yW79d z(2Oi!r%c=Ni|F?k1Nte}QfVG%O2?Tf_$5@bGCYlW!8csmu=y=ErPB7O2P?L0XX3K5 zvF86&LZiE68~k4~jo+4QgTb{q?w%-VER!}CDJB$|;M%#Hqsa;(xrqxU*Jx&QCgVSz z$4Sn#84NI;1(ks+xn!k?@b79cND5{Q-0h6zn`y1_LFaQ`K z9G|$Yv^2m#@+~a;y;dMx=?Ra}z27#V3;+#4U~^$2Ec3qLX^5}(7g#F5y;23#%WmPd z`o^_JXquO}InAunFdhf<`!77fE!&*y3m8wpLzu84p!i|)=KFX51mYwR=jX0YZ{HM% zxqe!dkQUb5Le5Nntx}Fp9AVWCeV|B^bjG)(`tD zkC3xi>S3K=xI*hLtKLjIK{;Dm*b~XWAT(ey1>cJXgf;Mu6%{v-X|<^%FWnMqGed(R zNqLF8UWFj5qVT+Kg>jPYa8zoZVMn=U)K!84Gcido#@@6diLs4yl@e!u;e^~@xr!XD zHO#)!24gHPaCMR-vAkv--~fGR5+%&oA9x77P<&HS)1Ig(z|#GvChQV6to%&y&9lX> z8krA*%I%#u6DZK=7D+pR$mf1=Ltw4d zmZk%$Dr1G=;l9DhBc!>h3hNZwpK7`Cbra;2fDoq=;r!gmG7@H{>~vGwU6TP>zyTA~ ziPRS7oHKoV-|*x)9G}2GB6+%<;-+dC3Cc$moQ<74y9sDm(iS6@rd}%@PftQa#sMX@ zmsPaMpn>rcnkN^<^c@`UT&Jb5o3OT|CMzYHgaAa4o|R;AmS9q zdZMpdfwuRfzEW1 zqS!bOjwp%LcWMeu$ej$2YAS@MO#+P3Rojyi{~yggQBF^hW<60mQ|6@daF>U2=;tgV zYFk)40u0+h@Hi5}R+U~<2lE6Icf>S;Mb2B@wAVS5sKL{pp&%-h)IA40Pz|P~W@7OE jZa$+sVyzQ*%C>jGKgqYfX-pm*IPJBE#;}PKc!|UH>hp$R4*8gIZReBm{ls&lD5TQJ za-~@=Dx5+qTZ0+H3Tu>hbe=`sC~Yq?cy-{NVNGk9#;;arqDx*=ZMdf2qBbP1&1HJ3 zq_JFDCNeS zKl=x { for (const { mimetype, extension } of [ @@ -188,7 +188,74 @@ describe('mimeTypes', () => { for (const [ext, v] of Object.entries(mimeTypes.sidecar)) { it(`should lookup ${ext}`, () => { - expect(mimeTypes.lookup(`test.${ext}`)).toEqual(v[0]); + expect(mimeTypes.lookup(`it.${ext}`)).toEqual(v[0]); + }); + } + }); +}); + +describe('ServerVersion', () => { + describe('isNewerThan', () => { + it('should work on patch versions', () => { + expect(new ServerVersion(0, 0, 1).isNewerThan(new ServerVersion(0, 0, 0))).toBe(true); + expect(new ServerVersion(1, 72, 1).isNewerThan(new ServerVersion(1, 72, 0))).toBe(true); + + expect(new ServerVersion(0, 0, 0).isNewerThan(new ServerVersion(0, 0, 1))).toBe(false); + expect(new ServerVersion(1, 72, 0).isNewerThan(new ServerVersion(1, 72, 1))).toBe(false); + }); + + it('should work on minor versions', () => { + expect(new ServerVersion(0, 1, 0).isNewerThan(new ServerVersion(0, 0, 0))).toBe(true); + expect(new ServerVersion(1, 72, 0).isNewerThan(new ServerVersion(1, 71, 0))).toBe(true); + expect(new ServerVersion(1, 72, 0).isNewerThan(new ServerVersion(1, 71, 9))).toBe(true); + + expect(new ServerVersion(0, 0, 0).isNewerThan(new ServerVersion(0, 1, 0))).toBe(false); + expect(new ServerVersion(1, 71, 0).isNewerThan(new ServerVersion(1, 72, 0))).toBe(false); + expect(new ServerVersion(1, 71, 9).isNewerThan(new ServerVersion(1, 72, 0))).toBe(false); + }); + + it('should work on major versions', () => { + expect(new ServerVersion(1, 0, 0).isNewerThan(new ServerVersion(0, 0, 0))).toBe(true); + expect(new ServerVersion(2, 0, 0).isNewerThan(new ServerVersion(1, 71, 0))).toBe(true); + + expect(new ServerVersion(0, 0, 0).isNewerThan(new ServerVersion(1, 0, 0))).toBe(false); + expect(new ServerVersion(1, 71, 0).isNewerThan(new ServerVersion(2, 0, 0))).toBe(false); + }); + + it('should work on equal', () => { + for (const version of [ + new ServerVersion(0, 0, 0), + new ServerVersion(0, 0, 1), + new ServerVersion(0, 1, 1), + new ServerVersion(0, 1, 0), + new ServerVersion(1, 1, 1), + new ServerVersion(1, 0, 0), + new ServerVersion(1, 72, 1), + new ServerVersion(1, 72, 0), + new ServerVersion(1, 73, 9), + ]) { + expect(version.isNewerThan(version)).toBe(false); + } + }); + }); + + describe('fromString', () => { + const tests = [ + { scenario: 'leading v', value: 'v1.72.2', expected: new ServerVersion(1, 72, 2) }, + { scenario: 'uppercase v', value: 'V1.72.2', expected: new ServerVersion(1, 72, 2) }, + { scenario: 'missing v', value: '1.72.2', expected: new ServerVersion(1, 72, 2) }, + { scenario: 'large patch', value: '1.72.123', expected: new ServerVersion(1, 72, 123) }, + { scenario: 'large minor', value: '1.123.0', expected: new ServerVersion(1, 123, 0) }, + { scenario: 'large major', value: '123.0.0', expected: new ServerVersion(123, 0, 0) }, + { scenario: 'major bump', value: 'v2.0.0', expected: new ServerVersion(2, 0, 0) }, + ]; + + for (const { scenario, value, expected } of tests) { + it(`should correctly parse ${scenario}`, () => { + const actual = ServerVersion.fromString(value); + expect(actual.major).toEqual(expected.major); + expect(actual.minor).toEqual(expected.minor); + expect(actual.patch).toEqual(expected.patch); }); } }); diff --git a/server/src/domain/domain.constant.ts b/server/src/domain/domain.constant.ts index e89b90fb28..1b301be565 100644 --- a/server/src/domain/domain.constant.ts +++ b/server/src/domain/domain.constant.ts @@ -4,8 +4,7 @@ import { extname } from 'node:path'; import pkg from 'src/../../package.json'; export const AUDIT_LOG_MAX_DURATION = Duration.fromObject({ days: 100 }); - -const [major, minor, patch] = pkg.version.split('.'); +export const ONE_HOUR = Duration.fromObject({ hours: 1 }); export interface IServerVersion { major: number; @@ -13,13 +12,49 @@ export interface IServerVersion { patch: number; } -export const serverVersion: IServerVersion = { - major: Number(major), - minor: Number(minor), - patch: Number(patch), -}; +export class ServerVersion implements IServerVersion { + constructor( + public readonly major: number, + public readonly minor: number, + public readonly patch: number, + ) {} -export const SERVER_VERSION = `${serverVersion.major}.${serverVersion.minor}.${serverVersion.patch}`; + toString() { + return `${this.major}.${this.minor}.${this.patch}`; + } + + toJSON() { + const { major, minor, patch } = this; + return { major, minor, patch }; + } + + static fromString(version: string): ServerVersion { + const regex = /(?:v)?(?\d+)\.(?\d+)\.(?\d+)/i; + const matchResult = version.match(regex); + if (matchResult) { + const [, major, minor, patch] = matchResult.map(Number); + return new ServerVersion(major, minor, patch); + } else { + throw new Error(`Invalid version format: ${version}`); + } + } + + isNewerThan(version: ServerVersion): boolean { + const equalMajor = this.major === version.major; + const equalMinor = this.minor === version.minor; + + return ( + this.major > version.major || + (equalMajor && this.minor > version.minor) || + (equalMajor && equalMinor && this.patch > version.patch) + ); + } +} + +export const envName = (process.env.NODE_ENV || 'development').toUpperCase(); +export const isDev = process.env.NODE_ENV === 'development'; + +export const serverVersion = ServerVersion.fromString(pkg.version); export const APP_MEDIA_LOCATION = process.env.IMMICH_MEDIA_LOCATION || './upload'; diff --git a/server/src/domain/job/job.constants.ts b/server/src/domain/job/job.constants.ts index ec1307cba4..7782167b56 100644 --- a/server/src/domain/job/job.constants.ts +++ b/server/src/domain/job/job.constants.ts @@ -169,7 +169,7 @@ export const JOBS_TO_QUEUE: Record = { [JobName.SIDECAR_DISCOVERY]: QueueName.SIDECAR, [JobName.SIDECAR_SYNC]: QueueName.SIDECAR, - // Library managment + // Library management [JobName.LIBRARY_SCAN_ASSET]: QueueName.LIBRARY, [JobName.LIBRARY_SCAN]: QueueName.LIBRARY, [JobName.LIBRARY_DELETE]: QueueName.LIBRARY, diff --git a/server/src/domain/repositories/communication.repository.ts b/server/src/domain/repositories/communication.repository.ts index f4c06a1e9a..958adb8033 100644 --- a/server/src/domain/repositories/communication.repository.ts +++ b/server/src/domain/repositories/communication.repository.ts @@ -9,9 +9,13 @@ export enum CommunicationEvent { PERSON_THUMBNAIL = 'on_person_thumbnail', SERVER_VERSION = 'on_server_version', CONFIG_UPDATE = 'on_config_update', + NEW_RELEASE = 'on_new_release', } +export type Callback = (userId: string) => Promise; + export interface ICommunicationRepository { send(event: CommunicationEvent, userId: string, data: any): void; broadcast(event: CommunicationEvent, data: any): void; + addEventListener(event: 'connect', callback: Callback): void; } diff --git a/server/src/domain/repositories/index.ts b/server/src/domain/repositories/index.ts index 28f1cf6a57..2c4a10cc24 100644 --- a/server/src/domain/repositories/index.ts +++ b/server/src/domain/repositories/index.ts @@ -14,6 +14,7 @@ export * from './move.repository'; export * from './partner.repository'; export * from './person.repository'; export * from './search.repository'; +export * from './server-info.repository'; export * from './shared-link.repository'; export * from './smart-info.repository'; export * from './storage.repository'; diff --git a/server/src/domain/repositories/job.repository.ts b/server/src/domain/repositories/job.repository.ts index 0f571dc991..3527c9ea62 100644 --- a/server/src/domain/repositories/job.repository.ts +++ b/server/src/domain/repositories/job.repository.ts @@ -85,7 +85,7 @@ export type JobItem = | { name: JobName.ASSET_DELETION; data: IAssetDeletionJob } | { name: JobName.ASSET_DELETION_CHECK; data?: IBaseJob } - // Library Managment + // Library Management | { name: JobName.LIBRARY_SCAN_ASSET; data: ILibraryFileJob } | { name: JobName.LIBRARY_SCAN; data: ILibraryRefreshJob } | { name: JobName.LIBRARY_REMOVE_OFFLINE; data: IEntityJob } diff --git a/server/src/domain/repositories/server-info.repository.ts b/server/src/domain/repositories/server-info.repository.ts new file mode 100644 index 0000000000..a4168d4c3e --- /dev/null +++ b/server/src/domain/repositories/server-info.repository.ts @@ -0,0 +1,15 @@ +export interface GitHubRelease { + id: number; + url: string; + tag_name: string; + name: string; + created_at: string; + published_at: string; + body: string; +} + +export const IServerInfoRepository = 'IServerInfoRepository'; + +export interface IServerInfoRepository { + getGitHubRelease(): Promise; +} diff --git a/server/src/domain/server-info/server-info.service.spec.ts b/server/src/domain/server-info/server-info.service.spec.ts index 53115594c9..4f6b24680d 100644 --- a/server/src/domain/server-info/server-info.service.spec.ts +++ b/server/src/domain/server-info/server-info.service.spec.ts @@ -1,20 +1,36 @@ -import { newStorageRepositoryMock, newSystemConfigRepositoryMock, newUserRepositoryMock } from '@test'; +import { + newCommunicationRepositoryMock, + newServerInfoRepositoryMock, + newStorageRepositoryMock, + newSystemConfigRepositoryMock, + newUserRepositoryMock, +} from '@test'; import { serverVersion } from '../domain.constant'; -import { IStorageRepository, ISystemConfigRepository, IUserRepository } from '../repositories'; +import { + ICommunicationRepository, + IServerInfoRepository, + IStorageRepository, + ISystemConfigRepository, + IUserRepository, +} from '../repositories'; import { ServerInfoService } from './server-info.service'; describe(ServerInfoService.name, () => { let sut: ServerInfoService; + let communicationMock: jest.Mocked; let configMock: jest.Mocked; + let serverInfoMock: jest.Mocked; let storageMock: jest.Mocked; let userMock: jest.Mocked; beforeEach(() => { configMock = newSystemConfigRepositoryMock(); + communicationMock = newCommunicationRepositoryMock(); + serverInfoMock = newServerInfoRepositoryMock(); storageMock = newStorageRepositoryMock(); userMock = newUserRepositoryMock(); - sut = new ServerInfoService(configMock, userMock, storageMock); + sut = new ServerInfoService(communicationMock, configMock, userMock, serverInfoMock, storageMock); }); it('should work', () => { diff --git a/server/src/domain/server-info/server-info.service.ts b/server/src/domain/server-info/server-info.service.ts index 1406423abe..e386fa17c0 100644 --- a/server/src/domain/server-info/server-info.service.ts +++ b/server/src/domain/server-info/server-info.service.ts @@ -1,7 +1,16 @@ -import { Inject, Injectable } from '@nestjs/common'; -import { mimeTypes, serverVersion } from '../domain.constant'; +import { Inject, Injectable, Logger } from '@nestjs/common'; +import { DateTime } from 'luxon'; +import { ServerVersion, isDev, mimeTypes, serverVersion } from '../domain.constant'; import { asHumanReadable } from '../domain.util'; -import { IStorageRepository, ISystemConfigRepository, IUserRepository, UserStatsQueryResponse } from '../repositories'; +import { + CommunicationEvent, + ICommunicationRepository, + IServerInfoRepository, + IStorageRepository, + ISystemConfigRepository, + IUserRepository, + UserStatsQueryResponse, +} from '../repositories'; import { StorageCore, StorageFolder } from '../storage'; import { SystemConfigCore } from '../system-config'; import { @@ -16,14 +25,20 @@ import { @Injectable() export class ServerInfoService { + private logger = new Logger(ServerInfoService.name); private configCore: SystemConfigCore; + private releaseVersion = serverVersion; + private releaseVersionCheckedAt: DateTime | null = null; constructor( + @Inject(ICommunicationRepository) private communicationRepository: ICommunicationRepository, @Inject(ISystemConfigRepository) configRepository: ISystemConfigRepository, @Inject(IUserRepository) private userRepository: IUserRepository, + @Inject(IServerInfoRepository) private repository: IServerInfoRepository, @Inject(IStorageRepository) private storageRepository: IStorageRepository, ) { this.configCore = SystemConfigCore.create(configRepository); + this.communicationRepository.addEventListener('connect', (userId) => this.handleConnect(userId)); } async getInfo(): Promise { @@ -101,4 +116,56 @@ export class ServerInfoService { sidecar: Object.keys(mimeTypes.sidecar), }; } + + async handleVersionCheck(): Promise { + try { + if (isDev) { + return true; + } + + const { newVersionCheck } = await this.configCore.getConfig(); + if (!newVersionCheck.enabled) { + return true; + } + + // check once per hour (max) + if (this.releaseVersionCheckedAt && this.releaseVersionCheckedAt.diffNow().as('minutes') < 60) { + return true; + } + + const githubRelease = await this.repository.getGitHubRelease(); + const githubVersion = ServerVersion.fromString(githubRelease.tag_name); + const publishedAt = new Date(githubRelease.published_at); + this.releaseVersion = githubVersion; + this.releaseVersionCheckedAt = DateTime.now(); + + if (githubVersion.isNewerThan(serverVersion)) { + this.logger.log(`Found ${githubVersion.toString()}, released at ${publishedAt.toLocaleString()}`); + this.newReleaseNotification(); + } + } catch (error: Error | any) { + this.logger.warn(`Unable to run version check: ${error}`, error?.stack); + } + + return true; + } + + private async handleConnect(userId: string) { + this.communicationRepository.send(CommunicationEvent.SERVER_VERSION, userId, serverVersion); + this.newReleaseNotification(userId); + } + + private newReleaseNotification(userId?: string) { + const event = CommunicationEvent.NEW_RELEASE; + const payload = { + isAvailable: this.releaseVersion.isNewerThan(serverVersion), + checkedAt: this.releaseVersionCheckedAt, + serverVersion, + releaseVersion: this.releaseVersion, + }; + + userId + ? this.communicationRepository.send(event, userId, payload) + : this.communicationRepository.broadcast(event, payload); + } } diff --git a/server/src/domain/system-config/dto/system-config-new-version-check.dto.ts b/server/src/domain/system-config/dto/system-config-new-version-check.dto.ts new file mode 100644 index 0000000000..c276739243 --- /dev/null +++ b/server/src/domain/system-config/dto/system-config-new-version-check.dto.ts @@ -0,0 +1,6 @@ +import { IsBoolean } from 'class-validator'; + +export class SystemConfigNewVersionCheckDto { + @IsBoolean() + enabled!: boolean; +} diff --git a/server/src/domain/system-config/dto/system-config.dto.ts b/server/src/domain/system-config/dto/system-config.dto.ts index 6a88e758c6..975f5df893 100644 --- a/server/src/domain/system-config/dto/system-config.dto.ts +++ b/server/src/domain/system-config/dto/system-config.dto.ts @@ -5,6 +5,7 @@ import { SystemConfigFFmpegDto } from './system-config-ffmpeg.dto'; import { SystemConfigJobDto } from './system-config-job.dto'; import { SystemConfigMachineLearningDto } from './system-config-machine-learning.dto'; import { SystemConfigMapDto } from './system-config-map.dto'; +import { SystemConfigNewVersionCheckDto } from './system-config-new-version-check.dto'; import { SystemConfigOAuthDto } from './system-config-oauth.dto'; import { SystemConfigPasswordLoginDto } from './system-config-password-login.dto'; import { SystemConfigReverseGeocodingDto } from './system-config-reverse-geocoding.dto'; @@ -29,6 +30,11 @@ export class SystemConfigDto implements SystemConfig { @IsObject() map!: SystemConfigMapDto; + @Type(() => SystemConfigNewVersionCheckDto) + @ValidateNested() + @IsObject() + newVersionCheck!: SystemConfigNewVersionCheckDto; + @Type(() => SystemConfigOAuthDto) @ValidateNested() @IsObject() diff --git a/server/src/domain/system-config/system-config.core.ts b/server/src/domain/system-config/system-config.core.ts index 4fd2faa295..29f9c363d3 100644 --- a/server/src/domain/system-config/system-config.core.ts +++ b/server/src/domain/system-config/system-config.core.ts @@ -1,8 +1,8 @@ import { AudioCodec, - CQMode, CitiesFile, Colorspace, + CQMode, SystemConfig, SystemConfigEntity, SystemConfigKey, @@ -110,6 +110,9 @@ export const defaults = Object.freeze({ quality: 80, colorspace: Colorspace.P3, }, + newVersionCheck: { + enabled: true, + }, trash: { enabled: true, days: 30, diff --git a/server/src/domain/system-config/system-config.service.spec.ts b/server/src/domain/system-config/system-config.service.spec.ts index 36cdb6543e..1dcbd013dc 100644 --- a/server/src/domain/system-config/system-config.service.spec.ts +++ b/server/src/domain/system-config/system-config.service.spec.ts @@ -1,8 +1,8 @@ import { AudioCodec, - CQMode, CitiesFile, Colorspace, + CQMode, SystemConfig, SystemConfigEntity, SystemConfigKey, @@ -15,7 +15,7 @@ import { BadRequestException } from '@nestjs/common'; import { newCommunicationRepositoryMock, newJobRepositoryMock, newSystemConfigRepositoryMock } from '@test'; import { JobName, QueueName } from '../job'; import { ICommunicationRepository, IJobRepository, ISystemConfigRepository } from '../repositories'; -import { SystemConfigValidator, defaults } from './system-config.core'; +import { defaults, SystemConfigValidator } from './system-config.core'; import { SystemConfigService } from './system-config.service'; const updates: SystemConfigEntity[] = [ @@ -111,6 +111,9 @@ const updatedConfig = Object.freeze({ quality: 80, colorspace: Colorspace.P3, }, + newVersionCheck: { + enabled: true, + }, trash: { enabled: true, days: 10, diff --git a/server/src/immich/app.service.ts b/server/src/immich/app.service.ts index 6f386eb126..110380753e 100644 --- a/server/src/immich/app.service.ts +++ b/server/src/immich/app.service.ts @@ -1,6 +1,6 @@ -import { JobService, SearchService, ServerInfoService, StorageService } from '@app/domain'; +import { JobService, ONE_HOUR, SearchService, ServerInfoService, StorageService } from '@app/domain'; import { Injectable, Logger } from '@nestjs/common'; -import { Cron, CronExpression } from '@nestjs/schedule'; +import { Cron, CronExpression, Interval } from '@nestjs/schedule'; @Injectable() export class AppService { @@ -13,6 +13,11 @@ export class AppService { private serverService: ServerInfoService, ) {} + @Interval(ONE_HOUR.as('milliseconds')) + async onVersionCheck() { + await this.serverService.handleVersionCheck(); + } + @Cron(CronExpression.EVERY_DAY_AT_MIDNIGHT) async onNightlyJob() { await this.jobService.handleNightlyJobs(); @@ -21,6 +26,7 @@ export class AppService { async init() { this.storageService.init(); await this.searchService.init(); + await this.serverService.handleVersionCheck(); this.logger.log(`Feature Flags: ${JSON.stringify(await this.serverService.getFeatures(), null, 2)}`); } diff --git a/server/src/immich/app.utils.ts b/server/src/immich/app.utils.ts index 61ee81c624..1a27b6f30b 100644 --- a/server/src/immich/app.utils.ts +++ b/server/src/immich/app.utils.ts @@ -3,7 +3,7 @@ import { IMMICH_API_KEY_HEADER, IMMICH_API_KEY_NAME, ImmichReadStream, - SERVER_VERSION, + serverVersion, } from '@app/domain'; import { INestApplication, StreamableFile } from '@nestjs/common'; import { @@ -91,7 +91,7 @@ export const useSwagger = (app: INestApplication, isDev: boolean) => { const config = new DocumentBuilder() .setTitle('Immich') .setDescription('Immich API') - .setVersion(SERVER_VERSION) + .setVersion(serverVersion.toString()) .addBearerAuth({ type: 'http', scheme: 'Bearer', diff --git a/server/src/immich/main.ts b/server/src/immich/main.ts index f262a173cd..e3ac816317 100644 --- a/server/src/immich/main.ts +++ b/server/src/immich/main.ts @@ -1,4 +1,4 @@ -import { getLogLevels, SERVER_VERSION } from '@app/domain'; +import { envName, getLogLevels, isDev, serverVersion } from '@app/domain'; import { RedisIoAdapter } from '@app/infra'; import { Logger } from '@nestjs/common'; import { NestFactory } from '@nestjs/core'; @@ -9,9 +9,7 @@ import { AppModule } from './app.module'; import { useSwagger } from './app.utils'; const logger = new Logger('ImmichServer'); -const envName = (process.env.NODE_ENV || 'development').toUpperCase(); const port = Number(process.env.SERVER_PORT) || 3001; -const isDev = process.env.NODE_ENV === 'development'; export async function bootstrap() { const app = await NestFactory.create(AppModule, { logger: getLogLevels() }); @@ -29,5 +27,5 @@ export async function bootstrap() { const server = await app.listen(port); server.requestTimeout = 30 * 60 * 1000; - logger.log(`Immich Server is listening on ${await app.getUrl()} [v${SERVER_VERSION}] [${envName}] `); + logger.log(`Immich Server is listening on ${await app.getUrl()} [v${serverVersion}] [${envName}] `); } diff --git a/server/src/infra/entities/system-config.entity.ts b/server/src/infra/entities/system-config.entity.ts index 6bd552111a..2e0a7473c0 100644 --- a/server/src/infra/entities/system-config.entity.ts +++ b/server/src/infra/entities/system-config.entity.ts @@ -67,6 +67,8 @@ export enum SystemConfigKey { REVERSE_GEOCODING_ENABLED = 'reverseGeocoding.enabled', REVERSE_GEOCODING_CITIES_FILE_OVERRIDE = 'reverseGeocoding.citiesFileOverride', + NEW_VERSION_CHECK_ENABLED = 'newVersionCheck.enabled', + OAUTH_ENABLED = 'oauth.enabled', OAUTH_ISSUER_URL = 'oauth.issuerUrl', OAUTH_CLIENT_ID = 'oauth.clientId', @@ -219,6 +221,9 @@ export interface SystemConfig { quality: number; colorspace: Colorspace; }; + newVersionCheck: { + enabled: boolean; + }; trash: { enabled: boolean; days: number; diff --git a/server/src/infra/infra.module.ts b/server/src/infra/infra.module.ts index ac9cb8484e..367458169f 100644 --- a/server/src/infra/infra.module.ts +++ b/server/src/infra/infra.module.ts @@ -15,6 +15,7 @@ import { IPartnerRepository, IPersonRepository, ISearchRepository, + IServerInfoRepository, ISharedLinkRepository, ISmartInfoRepository, IStorageRepository, @@ -48,6 +49,7 @@ import { MoveRepository, PartnerRepository, PersonRepository, + ServerInfoRepository, SharedLinkRepository, SmartInfoRepository, SystemConfigRepository, @@ -73,6 +75,7 @@ const providers: Provider[] = [ { provide: IPartnerRepository, useClass: PartnerRepository }, { provide: IPersonRepository, useClass: PersonRepository }, { provide: ISearchRepository, useClass: TypesenseRepository }, + { provide: IServerInfoRepository, useClass: ServerInfoRepository }, { provide: ISharedLinkRepository, useClass: SharedLinkRepository }, { provide: ISmartInfoRepository, useClass: SmartInfoRepository }, { provide: IStorageRepository, useClass: FilesystemProvider }, diff --git a/server/src/infra/repositories/communication.repository.ts b/server/src/infra/repositories/communication.repository.ts index 01239eb52b..8cc5ce0e07 100644 --- a/server/src/infra/repositories/communication.repository.ts +++ b/server/src/infra/repositories/communication.repository.ts @@ -1,4 +1,4 @@ -import { AuthService, CommunicationEvent, ICommunicationRepository, serverVersion } from '@app/domain'; +import { AuthService, Callback, CommunicationEvent, ICommunicationRepository } from '@app/domain'; import { Logger } from '@nestjs/common'; import { OnGatewayConnection, OnGatewayDisconnect, WebSocketGateway, WebSocketServer } from '@nestjs/websockets'; import { Server, Socket } from 'socket.io'; @@ -6,18 +6,25 @@ import { Server, Socket } from 'socket.io'; @WebSocketGateway({ cors: true }) export class CommunicationRepository implements OnGatewayConnection, OnGatewayDisconnect, ICommunicationRepository { private logger = new Logger(CommunicationRepository.name); + private onConnectCallbacks: Callback[] = []; constructor(private authService: AuthService) {} @WebSocketServer() server!: Server; + addEventListener(event: 'connect', callback: Callback) { + this.onConnectCallbacks.push(callback); + } + async handleConnection(client: Socket) { try { this.logger.log(`New websocket connection: ${client.id}`); const user = await this.authService.validate(client.request.headers, {}); if (user) { await client.join(user.id); - this.send(CommunicationEvent.SERVER_VERSION, user.id, serverVersion); + for (const callback of this.onConnectCallbacks) { + await callback(user.id); + } } else { client.emit('error', 'unauthorized'); client.disconnect(); @@ -34,7 +41,7 @@ export class CommunicationRepository implements OnGatewayConnection, OnGatewayDi } send(event: CommunicationEvent, userId: string, data: any) { - this.server.to(userId).emit(event, JSON.stringify(data)); + this.server.to(userId).emit(event, data); } broadcast(event: CommunicationEvent, data: any) { diff --git a/server/src/infra/repositories/index.ts b/server/src/infra/repositories/index.ts index 5229e610ed..bc2fba7666 100644 --- a/server/src/infra/repositories/index.ts +++ b/server/src/infra/repositories/index.ts @@ -14,6 +14,7 @@ export * from './metadata.repository'; export * from './move.repository'; export * from './partner.repository'; export * from './person.repository'; +export * from './server-info.repository'; export * from './shared-link.repository'; export * from './smart-info.repository'; export * from './system-config.repository'; diff --git a/server/src/infra/repositories/server-info.repository.ts b/server/src/infra/repositories/server-info.repository.ts new file mode 100644 index 0000000000..61fa448f97 --- /dev/null +++ b/server/src/infra/repositories/server-info.repository.ts @@ -0,0 +1,12 @@ +import { GitHubRelease, IServerInfoRepository } from '@app/domain'; +import { Injectable } from '@nestjs/common'; +import axios from 'axios'; + +@Injectable() +export class ServerInfoRepository implements IServerInfoRepository { + getGitHubRelease(): Promise { + return axios + .get('https://api.github.com/repos/immich-app/immich/releases/latest') + .then((response) => response.data); + } +} diff --git a/server/src/microservices/app.service.ts b/server/src/microservices/app.service.ts index 365b073299..67d995e331 100644 --- a/server/src/microservices/app.service.ts +++ b/server/src/microservices/app.service.ts @@ -9,6 +9,7 @@ import { MetadataService, PersonService, SearchService, + ServerInfoService, SmartInfoService, StorageService, StorageTemplateService, @@ -23,19 +24,20 @@ export class AppService { private logger = new Logger(AppService.name); constructor( - private jobService: JobService, + private auditService: AuditService, private assetService: AssetService, + private jobService: JobService, + private libraryService: LibraryService, private mediaService: MediaService, private metadataService: MetadataService, private personService: PersonService, private searchService: SearchService, + private serverInfoService: ServerInfoService, private smartInfoService: SmartInfoService, private storageTemplateService: StorageTemplateService, private storageService: StorageService, private systemConfigService: SystemConfigService, private userService: UserService, - private auditService: AuditService, - private libraryService: LibraryService, ) {} async init() { diff --git a/server/src/microservices/main.ts b/server/src/microservices/main.ts index 2937e6213b..bdb2893638 100644 --- a/server/src/microservices/main.ts +++ b/server/src/microservices/main.ts @@ -1,4 +1,4 @@ -import { getLogLevels, SERVER_VERSION } from '@app/domain'; +import { envName, getLogLevels, serverVersion } from '@app/domain'; import { RedisIoAdapter } from '@app/infra'; import { Logger } from '@nestjs/common'; import { NestFactory } from '@nestjs/core'; @@ -7,7 +7,6 @@ import { MicroservicesModule } from './microservices.module'; const logger = new Logger('ImmichMicroservice'); const port = Number(process.env.MICROSERVICES_PORT) || 3002; -const envName = (process.env.NODE_ENV || 'development').toUpperCase(); export async function bootstrap() { const app = await NestFactory.create(MicroservicesModule, { logger: getLogLevels() }); @@ -17,5 +16,5 @@ export async function bootstrap() { await app.get(AppService).init(); await app.listen(port); - logger.log(`Immich Microservices is listening on ${await app.getUrl()} [v${SERVER_VERSION}] [${envName}] `); + logger.log(`Immich Microservices is listening on ${await app.getUrl()} [v${serverVersion}] [${envName}] `); } diff --git a/server/test/repositories/communication.repository.mock.ts b/server/test/repositories/communication.repository.mock.ts index d8374e8b27..2db02e5277 100644 --- a/server/test/repositories/communication.repository.mock.ts +++ b/server/test/repositories/communication.repository.mock.ts @@ -4,5 +4,6 @@ export const newCommunicationRepositoryMock = (): jest.Mocked => { + return { + getGitHubRelease: jest.fn(), + }; +}; diff --git a/web/src/api/open-api/api.ts b/web/src/api/open-api/api.ts index 2133d04113..fffb9914a5 100644 --- a/web/src/api/open-api/api.ts +++ b/web/src/api/open-api/api.ts @@ -3283,6 +3283,12 @@ export interface SystemConfigDto { * @memberof SystemConfigDto */ 'map': SystemConfigMapDto; + /** + * + * @type {SystemConfigNewVersionCheckDto} + * @memberof SystemConfigDto + */ + 'newVersionCheck': SystemConfigNewVersionCheckDto; /** * * @type {SystemConfigOAuthDto} @@ -3572,6 +3578,19 @@ export interface SystemConfigMapDto { */ 'tileUrl': string; } +/** + * + * @export + * @interface SystemConfigNewVersionCheckDto + */ +export interface SystemConfigNewVersionCheckDto { + /** + * + * @type {boolean} + * @memberof SystemConfigNewVersionCheckDto + */ + 'enabled': boolean; +} /** * * @export diff --git a/web/src/lib/components/admin-page/settings/new-version-check-settings/new-version-check-settings.svelte b/web/src/lib/components/admin-page/settings/new-version-check-settings/new-version-check-settings.svelte new file mode 100644 index 0000000000..acb83f0ca7 --- /dev/null +++ b/web/src/lib/components/admin-page/settings/new-version-check-settings/new-version-check-settings.svelte @@ -0,0 +1,92 @@ + + +
+ {#await getConfigs() then} +
+
+
+
+ + +
+
+
+
+ {/await} +
diff --git a/web/src/lib/components/shared-components/version-announcement-box.svelte b/web/src/lib/components/shared-components/version-announcement-box.svelte index af98568283..b2c535cd65 100644 --- a/web/src/lib/components/shared-components/version-announcement-box.svelte +++ b/web/src/lib/components/shared-components/version-announcement-box.svelte @@ -1,43 +1,35 @@ {#if showModal} @@ -63,9 +55,9 @@
Your friend, Alex
- Server Version: {serverVersionName} + Server Version: {serverVersion}
- Latest Version: {githubVersion} + Latest Version: {releaseVersion}
diff --git a/web/src/lib/stores/websocket.ts b/web/src/lib/stores/websocket.ts index af250984a3..213dadfda7 100644 --- a/web/src/lib/stores/websocket.ts +++ b/web/src/lib/stores/websocket.ts @@ -3,6 +3,13 @@ import { io } from 'socket.io-client'; import { writable } from 'svelte/store'; import { loadConfig } from './server-config.store'; +export interface ReleaseEvent { + isAvailable: boolean; + checkedAt: Date; + serverVersion: ServerVersionResponseDto; + releaseVersion: ServerVersionResponseDto; +} + export const websocketStore = { onUploadSuccess: writable(), onAssetDelete: writable(), @@ -10,6 +17,7 @@ export const websocketStore = { onPersonThumbnail: writable(), serverVersion: writable(), connected: writable(false), + onRelease: writable(), }; export const openWebsocketConnection = () => { @@ -24,12 +32,13 @@ export const openWebsocketConnection = () => { websocket .on('connect', () => websocketStore.connected.set(true)) .on('disconnect', () => websocketStore.connected.set(false)) - // .on('on_upload_success', (data) => websocketStore.onUploadSuccess.set(JSON.parse(data) as AssetResponseDto)) - .on('on_asset_delete', (data) => websocketStore.onAssetDelete.set(JSON.parse(data) as string)) - .on('on_asset_trash', (data) => websocketStore.onAssetTrash.set(JSON.parse(data) as string[])) - .on('on_person_thumbnail', (data) => websocketStore.onPersonThumbnail.set(JSON.parse(data) as string)) - .on('on_server_version', (data) => websocketStore.serverVersion.set(JSON.parse(data) as ServerVersionResponseDto)) + // .on('on_upload_success', (data) => websocketStore.onUploadSuccess.set(data)) + .on('on_asset_delete', (data) => websocketStore.onAssetDelete.set(data)) + .on('on_asset_trash', (data) => websocketStore.onAssetTrash.set(data)) + .on('on_person_thumbnail', (data) => websocketStore.onPersonThumbnail.set(data)) + .on('on_server_version', (data) => websocketStore.serverVersion.set(data)) .on('on_config_update', () => loadConfig()) + .on('on_new_release', (data) => websocketStore.onRelease.set(data)) .on('error', (e) => console.log('Websocket Error', e)); return () => websocket?.close(); diff --git a/web/src/lib/utils/get-github-version.ts b/web/src/lib/utils/get-github-version.ts deleted file mode 100644 index 5748585050..0000000000 --- a/web/src/lib/utils/get-github-version.ts +++ /dev/null @@ -1,15 +0,0 @@ -import axios from 'axios'; - -type GithubRelease = { - tag_name: string; -}; - -export const getGithubVersion = async (): Promise => { - const { data } = await axios.get('https://api.github.com/repos/immich-app/immich/releases/latest', { - headers: { - Accept: 'application/vnd.github.v3+json', - }, - }); - - return data.tag_name; -}; diff --git a/web/src/routes/+layout.server.ts b/web/src/routes/+layout.server.ts index 6ee3890737..804071cd71 100644 --- a/web/src/routes/+layout.server.ts +++ b/web/src/routes/+layout.server.ts @@ -1,7 +1,5 @@ import type { LayoutServerLoad } from './$types'; -export const load = (async ({ locals: { api, user } }) => { - const { data: serverVersion } = await api.serverInfoApi.getServerVersion(); - - return { serverVersion, user }; +export const load = (async ({ locals: { user } }) => { + return { user }; }) satisfies LayoutServerLoad; diff --git a/web/src/routes/+layout.svelte b/web/src/routes/+layout.svelte index a0cc96aba8..b658f26e27 100644 --- a/web/src/routes/+layout.svelte +++ b/web/src/routes/+layout.svelte @@ -108,7 +108,7 @@ {#if data.user?.isAdmin} - + {/if} {#if $page.route.id?.includes('(user)')} diff --git a/web/src/routes/admin/system-settings/+page.svelte b/web/src/routes/admin/system-settings/+page.svelte index 04a46bbed0..6d81c6093c 100644 --- a/web/src/routes/admin/system-settings/+page.svelte +++ b/web/src/routes/admin/system-settings/+page.svelte @@ -21,6 +21,7 @@ import ContentCopy from 'svelte-material-icons/ContentCopy.svelte'; import Download from 'svelte-material-icons/Download.svelte'; import type { PageData } from './$types'; + import NewVersionCheckSettings from '$lib/components/admin-page/settings/new-version-check-settings/new-version-check-settings.svelte'; export let data: PageData; @@ -109,6 +110,10 @@ + + + +