From 417af66f308fb61c7534a644d708768839575f56 Mon Sep 17 00:00:00 2001 From: Jason Rasmussen Date: Wed, 21 Jan 2026 13:13:02 -0500 Subject: [PATCH 01/10] refactor(web): on person thumbnail (#25422) --- .../faces-page/person-side-panel.svelte | 9 ++++---- web/src/lib/managers/event-manager.svelte.ts | 1 + web/src/lib/stores/websocket.ts | 1 + web/src/routes/(user)/explore/+page.svelte | 21 +++++++++---------- 4 files changed, 17 insertions(+), 15 deletions(-) diff --git a/web/src/lib/components/faces-page/person-side-panel.svelte b/web/src/lib/components/faces-page/person-side-panel.svelte index 6a0d467124..d9530e6114 100644 --- a/web/src/lib/components/faces-page/person-side-panel.svelte +++ b/web/src/lib/components/faces-page/person-side-panel.svelte @@ -1,9 +1,9 @@ + +
eventManager.emit('ReleaseEvent', event)) .on('on_session_delete', () => authManager.logout()) .on('on_user_delete', (id) => eventManager.emit('UserAdminDeleted', { id })) + .on('on_person_thumbnail', (id) => eventManager.emit('PersonThumbnailReady', { id })) .on('on_notification', () => notificationManager.refresh()) .on('connect_error', (e) => console.log('Websocket Connect Error', e)); diff --git a/web/src/routes/(user)/explore/+page.svelte b/web/src/routes/(user)/explore/+page.svelte index 580d84e888..513d211a56 100644 --- a/web/src/routes/(user)/explore/+page.svelte +++ b/web/src/routes/(user)/explore/+page.svelte @@ -1,15 +1,14 @@ + + {#if hasPeople}
From dc82c13ddc609c1c5e1d30a5f0292c3126f3224e Mon Sep 17 00:00:00 2001 From: Jason Rasmussen Date: Wed, 21 Jan 2026 13:13:07 -0500 Subject: [PATCH 02/10] refactor(web): user setting actions (#25424) --- web/src/lib/services/keyboard.service.ts | 14 +++++++++++ .../routes/(user)/user-settings/+page.svelte | 23 ++++++------------- 2 files changed, 21 insertions(+), 16 deletions(-) create mode 100644 web/src/lib/services/keyboard.service.ts diff --git a/web/src/lib/services/keyboard.service.ts b/web/src/lib/services/keyboard.service.ts new file mode 100644 index 0000000000..f5c9e59b75 --- /dev/null +++ b/web/src/lib/services/keyboard.service.ts @@ -0,0 +1,14 @@ +import ShortcutsModal from '$lib/modals/ShortcutsModal.svelte'; +import { modalManager, type ActionItem } from '@immich/ui'; +import { mdiKeyboard } from '@mdi/js'; +import type { MessageFormatter } from 'svelte-i18n'; + +export const getKeyboardActions = ($t: MessageFormatter) => { + const KeyboardShortcuts: ActionItem = { + title: $t('show_keyboard_shortcuts'), + icon: mdiKeyboard, + onAction: () => modalManager.show(ShortcutsModal, {}), + }; + + return { KeyboardShortcuts }; +}; diff --git a/web/src/routes/(user)/user-settings/+page.svelte b/web/src/routes/(user)/user-settings/+page.svelte index 43c214fd07..bf4f5b00f1 100644 --- a/web/src/routes/(user)/user-settings/+page.svelte +++ b/web/src/routes/(user)/user-settings/+page.svelte @@ -1,30 +1,21 @@ - - {#snippet buttons()} - modalManager.show(ShortcutsModal, {})} - /> - {/snippet} + From 1b032339aab242f437446d4ccd4335f3c3e1b3be Mon Sep 17 00:00:00 2001 From: Jason Rasmussen Date: Wed, 21 Jan 2026 13:13:16 -0500 Subject: [PATCH 03/10] refactor(web): asset job actions (#25426) --- .../asset-viewer/asset-viewer-nav-bar.spec.ts | 2 +- .../asset-viewer/asset-viewer-nav-bar.svelte | 58 +++++++++---------- .../asset-viewer/asset-viewer.svelte | 15 +---- .../timeline/actions/AssetJobActions.svelte | 6 +- web/src/lib/services/asset.service.ts | 50 +++++++++++++++- web/src/lib/utils.ts | 33 +++-------- 6 files changed, 90 insertions(+), 74 deletions(-) diff --git a/web/src/lib/components/asset-viewer/asset-viewer-nav-bar.spec.ts b/web/src/lib/components/asset-viewer/asset-viewer-nav-bar.spec.ts index 1c802b0dce..5ee6dbf93e 100644 --- a/web/src/lib/components/asset-viewer/asset-viewer-nav-bar.spec.ts +++ b/web/src/lib/components/asset-viewer/asset-viewer-nav-bar.spec.ts @@ -16,7 +16,7 @@ describe('AssetViewerNavBar component', () => { preAction: () => {}, onZoomImage: () => {}, onAction: () => {}, - onRunJob: () => {}, + onEdit: () => {}, onPlaySlideshow: () => {}, onClose: () => {}, playOriginalVideo: false, diff --git a/web/src/lib/components/asset-viewer/asset-viewer-nav-bar.svelte b/web/src/lib/components/asset-viewer/asset-viewer-nav-bar.svelte index cf8301d755..93d1a4acd1 100644 --- a/web/src/lib/components/asset-viewer/asset-viewer-nav-bar.svelte +++ b/web/src/lib/components/asset-viewer/asset-viewer-nav-bar.svelte @@ -28,12 +28,11 @@ import { photoViewerImgElement } from '$lib/stores/assets-store.svelte'; import { user } from '$lib/stores/user.store'; import { photoZoomState } from '$lib/stores/zoom-image.store'; - import { getAssetJobName, getSharedLink, withoutIcons } from '$lib/utils'; + import { getSharedLink, withoutIcons } from '$lib/utils'; import type { OnUndoDelete } from '$lib/utils/actions'; import { canCopyImageToClipboard } from '$lib/utils/asset-utils'; import { toTimelineAsset } from '$lib/utils/timeline-util'; import { - AssetJobName, AssetTypeEnum, AssetVisibility, type AlbumResponseDto, @@ -44,13 +43,9 @@ import { CommandPaletteDefaultProvider, IconButton, type ActionItem } from '@immich/ui'; import { mdiArrowLeft, - mdiCogRefreshOutline, mdiCompare, mdiContentCopy, - mdiDatabaseRefreshOutline, mdiDotsVertical, - mdiHeadSyncOutline, - mdiImageRefreshOutline, mdiImageSearch, mdiMagnifyMinusOutline, mdiMagnifyPlusOutline, @@ -71,7 +66,6 @@ preAction: PreAction; onAction: OnAction; onUndoDelete?: OnUndoDelete; - onRunJob: (name: AssetJobName) => void; onPlaySlideshow: () => void; onEdit: () => void; onClose?: () => void; @@ -90,7 +84,6 @@ preAction, onAction, onUndoDelete = undefined, - onRunJob, onPlaySlideshow, onClose, onEdit, @@ -124,6 +117,10 @@ PlayMotionPhoto, StopMotionPhoto, Info, + RefreshFacesJob, + RefreshMetadataJob, + RegenerateThumbnailJob, + TranscodeVideoJob, } = $derived(getAssetActions($t, asset)); const sharedLink = getSharedLink(); @@ -140,7 +137,24 @@
- onRunJob(AssetJobName.RefreshFaces)} - text={$getAssetJobName(AssetJobName.RefreshFaces)} - /> - onRunJob(AssetJobName.RefreshMetadata)} - text={$getAssetJobName(AssetJobName.RefreshMetadata)} - /> - onRunJob(AssetJobName.RegenerateThumbnail)} - text={$getAssetJobName(AssetJobName.RegenerateThumbnail)} - /> - {#if asset.type === AssetTypeEnum.Video} - onRunJob(AssetJobName.TranscodeVideo)} - text={$getAssetJobName(AssetJobName.TranscodeVideo)} - /> - {/if} + + + + {/if} {/if} diff --git a/web/src/lib/components/asset-viewer/asset-viewer.svelte b/web/src/lib/components/asset-viewer/asset-viewer.svelte index ac9c07df94..9e3c121024 100644 --- a/web/src/lib/components/asset-viewer/asset-viewer.svelte +++ b/web/src/lib/components/asset-viewer/asset-viewer.svelte @@ -19,7 +19,7 @@ import { alwaysLoadOriginalVideo } from '$lib/stores/preferences.store'; import { SlideshowNavigation, SlideshowState, slideshowStore } from '$lib/stores/slideshow.store'; import { user } from '$lib/stores/user.store'; - import { getAssetJobMessage, getAssetUrl, getSharedLink, handlePromiseError } from '$lib/utils'; + import { getAssetUrl, getSharedLink, handlePromiseError } from '$lib/utils'; import type { OnUndoDelete } from '$lib/utils/actions'; import { navigateToAsset } from '$lib/utils/asset-utils'; import { handleError } from '$lib/utils/handle-error'; @@ -28,18 +28,15 @@ import { preloadImageUrl } from '$lib/utils/sw-messaging'; import { toTimelineAsset } from '$lib/utils/timeline-util'; import { - AssetJobName, AssetTypeEnum, getAllAlbums, getAssetInfo, getStack, - runAssetJobs, type AlbumResponseDto, type AssetResponseDto, type PersonResponseDto, type StackResponseDto, } from '@immich/sdk'; - import { toastManager } from '@immich/ui'; import { onDestroy, onMount, untrack } from 'svelte'; import { t } from 'svelte-i18n'; import { fly } from 'svelte/transition'; @@ -262,15 +259,6 @@ isShowEditor = !isShowEditor; }; - const handleRunJob = async (name: AssetJobName) => { - try { - await runAssetJobs({ assetJobsDto: { assetIds: [asset.id], name } }); - toastManager.success($getAssetJobMessage(name)); - } catch (error) { - handleError(error, $t('errors.unable_to_submit_job')); - } - }; - /** * Slide show mode */ @@ -473,7 +461,6 @@ onAction={handleAction} {onUndoDelete} onEdit={showEditor} - onRunJob={handleRunJob} onPlaySlideshow={() => ($slideshowState = SlideshowState.PlaySlideshow)} onClose={onClose ? () => onClose(asset) : undefined} {playOriginalVideo} diff --git a/web/src/lib/components/timeline/actions/AssetJobActions.svelte b/web/src/lib/components/timeline/actions/AssetJobActions.svelte index 249b3c5d14..b4a3bdbefd 100644 --- a/web/src/lib/components/timeline/actions/AssetJobActions.svelte +++ b/web/src/lib/components/timeline/actions/AssetJobActions.svelte @@ -1,7 +1,7 @@ @@ -89,14 +96,19 @@ {#each libraries as library (library.id + library.name)} {@const { photos, usage, videos } = statistics[library.id]} {@const [diskUsage, diskUsageUnit] = getBytesWithUnit(usage, 0)} + {@const owner = owners[library.id]} - {library.name} - {owners[library.id].name} + + {library.name} + + + {owner.name} + {photos.toLocaleString($locale)} {videos.toLocaleString($locale)} {diskUsage} {diskUsageUnit} - + {/each} From 2dcb4efc407733fd85d419ce069e1f8b85515371 Mon Sep 17 00:00:00 2001 From: Daniel Dietzler <36593685+danieldietzler@users.noreply.github.com> Date: Wed, 21 Jan 2026 20:20:05 -0600 Subject: [PATCH 05/10] fix: lock tags column on update (#25435) --- server/src/services/tag.service.spec.ts | 10 +++--- server/src/services/tag.service.ts | 3 +- .../medium/specs/services/tag.service.spec.ts | 33 +++++++++++++++++-- 3 files changed, 38 insertions(+), 8 deletions(-) diff --git a/server/src/services/tag.service.spec.ts b/server/src/services/tag.service.spec.ts index ff706552a9..a80e6d508b 100644 --- a/server/src/services/tag.service.spec.ts +++ b/server/src/services/tag.service.spec.ts @@ -206,15 +206,15 @@ describe(TagService.name, () => { count: 6, }); expect(mocks.asset.upsertExif).toHaveBeenCalledWith( - { assetId: 'asset-1', tags: ['tag-1', 'tag-2'] }, + { assetId: 'asset-1', lockedProperties: ['tags'], tags: ['tag-1', 'tag-2'] }, { lockedPropertiesBehavior: 'append' }, ); expect(mocks.asset.upsertExif).toHaveBeenCalledWith( - { assetId: 'asset-2', tags: ['tag-1', 'tag-2'] }, + { assetId: 'asset-2', lockedProperties: ['tags'], tags: ['tag-1', 'tag-2'] }, { lockedPropertiesBehavior: 'append' }, ); expect(mocks.asset.upsertExif).toHaveBeenCalledWith( - { assetId: 'asset-3', tags: ['tag-1', 'tag-2'] }, + { assetId: 'asset-3', lockedProperties: ['tags'], tags: ['tag-1', 'tag-2'] }, { lockedPropertiesBehavior: 'append' }, ); expect(mocks.tag.upsertAssetIds).toHaveBeenCalledWith([ @@ -255,11 +255,11 @@ describe(TagService.name, () => { ]); expect(mocks.asset.upsertExif).not.toHaveBeenCalledWith( - { assetId: 'asset-1', tags: ['tag-1'] }, + { assetId: 'asset-1', lockedProperties: ['tags'], tags: ['tag-1'] }, { lockedPropertiesBehavior: 'append' }, ); expect(mocks.asset.upsertExif).toHaveBeenCalledWith( - { assetId: 'asset-2', tags: ['tag-1'] }, + { assetId: 'asset-2', lockedProperties: ['tags'], tags: ['tag-1'] }, { lockedPropertiesBehavior: 'append' }, ); expect(mocks.tag.getAssetIds).toHaveBeenCalledWith('tag-1', ['asset-1', 'asset-2']); diff --git a/server/src/services/tag.service.ts b/server/src/services/tag.service.ts index 3b3000f759..20303421c1 100644 --- a/server/src/services/tag.service.ts +++ b/server/src/services/tag.service.ts @@ -16,6 +16,7 @@ import { JobName, JobStatus, Permission, QueueName } from 'src/enum'; import { TagAssetTable } from 'src/schema/tables/tag-asset.table'; import { BaseService } from 'src/services/base.service'; import { addAssets, removeAssets } from 'src/utils/asset.util'; +import { updateLockedColumns } from 'src/utils/database'; import { upsertTags } from 'src/utils/tag'; @Injectable() @@ -152,7 +153,7 @@ export class TagService extends BaseService { private async updateTags(assetId: string) { const asset = await this.assetRepository.getById(assetId, { tags: true }); await this.assetRepository.upsertExif( - { assetId, tags: asset?.tags?.map(({ value }) => value) ?? [] }, + updateLockedColumns({ assetId, tags: asset?.tags?.map(({ value }) => value) ?? [] }), { lockedPropertiesBehavior: 'append' }, ); } diff --git a/server/test/medium/specs/services/tag.service.spec.ts b/server/test/medium/specs/services/tag.service.spec.ts index 2ec498e56d..989e4f535f 100644 --- a/server/test/medium/specs/services/tag.service.spec.ts +++ b/server/test/medium/specs/services/tag.service.spec.ts @@ -1,12 +1,15 @@ import { Kysely } from 'kysely'; import { JobStatus } from 'src/enum'; import { AccessRepository } from 'src/repositories/access.repository'; +import { AssetRepository } from 'src/repositories/asset.repository'; +import { EventRepository } from 'src/repositories/event.repository'; import { LoggingRepository } from 'src/repositories/logging.repository'; import { TagRepository } from 'src/repositories/tag.repository'; import { DB } from 'src/schema'; import { TagService } from 'src/services/tag.service'; import { upsertTags } from 'src/utils/tag'; import { newMediumService } from 'test/medium.factory'; +import { factory } from 'test/small.factory'; import { getKyselyDB } from 'test/utils'; let defaultDatabase: Kysely; @@ -14,8 +17,8 @@ let defaultDatabase: Kysely; const setup = (db?: Kysely) => { return newMediumService(TagService, { database: db || defaultDatabase, - real: [TagRepository, AccessRepository], - mock: [LoggingRepository], + real: [AssetRepository, TagRepository, AccessRepository], + mock: [EventRepository, LoggingRepository], }); }; @@ -24,6 +27,32 @@ beforeAll(async () => { }); describe(TagService.name, () => { + describe('addAssets', () => { + it('should lock exif column', async () => { + const { sut, ctx } = setup(); + ctx.getMock(EventRepository).emit.mockResolvedValue(); + const { user } = await ctx.newUser(); + const { asset } = await ctx.newAsset({ ownerId: user.id }); + const [tag] = await upsertTags(ctx.get(TagRepository), { userId: user.id, tags: ['tag-1'] }); + const authDto = factory.auth({ user }); + + await sut.addAssets(authDto, tag.id, { ids: [asset.id] }); + await expect( + ctx.database + .selectFrom('asset_exif') + .select(['lockedProperties', 'tags']) + .where('assetId', '=', asset.id) + .executeTakeFirstOrThrow(), + ).resolves.toEqual({ + lockedProperties: ['tags'], + tags: ['tag-1'], + }); + await expect(ctx.get(TagRepository).getByValue(user.id, 'tag-1')).resolves.toEqual( + expect.objectContaining({ id: tag.id }), + ); + await expect(ctx.get(TagRepository).getAssetIds(tag.id, [asset.id])).resolves.toContain(asset.id); + }); + }); describe('deleteEmptyTags', () => { it('single tag exists, not connected to any assets, and is deleted', async () => { const { sut, ctx } = setup(); From 3304c8efd87e3f8f9131cfecc4ba2515dbe8ed8d Mon Sep 17 00:00:00 2001 From: solluh <42142710+solluh@users.noreply.github.com> Date: Thu, 22 Jan 2026 12:55:44 +0100 Subject: [PATCH 06/10] docs: update README_de_DE.md (#25443) --- readme_i18n/README_de_DE.md | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/readme_i18n/README_de_DE.md b/readme_i18n/README_de_DE.md index a8685e0902..488b05abcc 100644 --- a/readme_i18n/README_de_DE.md +++ b/readme_i18n/README_de_DE.md @@ -38,11 +38,6 @@ ภาษาไทย

-## Warnung - -- ⚠️ Das Projekt befindet sich in **sehr aktiver** Entwicklung. -- ⚠️ Gehe von möglichen Fehlern und von Änderungen mit Breaking-Changes aus. -- ⚠️ **Nutze die App auf keinen Fall als einziges Speichermedium für deine Fotos und Videos.** - ⚠️ Befolge immer die [3-2-1](https://www.backblaze.com/blog/the-3-2-1-backup-strategy/) Backup-Regel für deine wertvollen Fotos und Videos! > [!NOTE] @@ -62,7 +57,7 @@ ## Demo -Die Web-Demo kannst Du unter https://demo.immich.app finden. Für die Handy-App kannst Du `https://demo.immich.app` als `Server Endpoint URL` angeben. +Die Web-Demo kannst Du unter https://demo.immich.app finden. Für die Smartphone-App kannst Du `https://demo.immich.app` als `Server Endpoint URL` angeben. ### Login Daten @@ -93,7 +88,7 @@ Die Web-Demo kannst Du unter https://demo.immich.app finden. Für die Handy-App | LivePhoto/MotionPhoto Sicherung und Wiedergabe | Ja | Ja | | Unterstützung für 360-Grad-Bilder | Nein | Ja | | Benutzerdefinierte Speicherstruktur | Ja | Ja | -| Öffentliches Teilen | Nein | Ja | +| Öffentliches Teilen | Ja | Ja | | Archiv und Favoriten | Ja | Ja | | Globale Karte | Ja | Ja | | Partnerfreigabe (Teilen) | Ja | Ja | @@ -103,7 +98,7 @@ Die Web-Demo kannst Du unter https://demo.immich.app finden. Für die Handy-App | Schreibgeschützte Gallerie | Ja | Ja | | Gestapelte Bilder | Ja | Ja | | Tags | Nein | Ja | -| Ordner-Ansicht | Nein | Ja | +| Ordner-Ansicht | Ja | Ja | ## Übersetzungen From c320146538cfc28f4ccaa7359ca1b6600e51400a Mon Sep 17 00:00:00 2001 From: Mees Frensel <33722705+meesfrensel@users.noreply.github.com> Date: Thu, 22 Jan 2026 13:43:29 +0100 Subject: [PATCH 07/10] fix: add scoped API permissions to map endpoints (#25423) --- mobile/openapi/lib/model/permission.dart | 6 ++++++ open-api/immich-openapi-specs.json | 4 ++++ open-api/typescript-sdk/src/fetch-client.ts | 2 ++ server/src/controllers/map.controller.ts | 6 +++--- server/src/enum.ts | 3 +++ 5 files changed, 18 insertions(+), 3 deletions(-) diff --git a/mobile/openapi/lib/model/permission.dart b/mobile/openapi/lib/model/permission.dart index d5b9bf5086..37aecc8b9c 100644 --- a/mobile/openapi/lib/model/permission.dart +++ b/mobile/openapi/lib/model/permission.dart @@ -82,6 +82,8 @@ class Permission { static const timelinePeriodRead = Permission._(r'timeline.read'); static const timelinePeriodDownload = Permission._(r'timeline.download'); static const maintenance = Permission._(r'maintenance'); + static const mapPeriodRead = Permission._(r'map.read'); + static const mapPeriodSearch = Permission._(r'map.search'); static const memoryPeriodCreate = Permission._(r'memory.create'); static const memoryPeriodRead = Permission._(r'memory.read'); static const memoryPeriodUpdate = Permission._(r'memory.update'); @@ -238,6 +240,8 @@ class Permission { timelinePeriodRead, timelinePeriodDownload, maintenance, + mapPeriodRead, + mapPeriodSearch, memoryPeriodCreate, memoryPeriodRead, memoryPeriodUpdate, @@ -429,6 +433,8 @@ class PermissionTypeTransformer { case r'timeline.read': return Permission.timelinePeriodRead; case r'timeline.download': return Permission.timelinePeriodDownload; case r'maintenance': return Permission.maintenance; + case r'map.read': return Permission.mapPeriodRead; + case r'map.search': return Permission.mapPeriodSearch; case r'memory.create': return Permission.memoryPeriodCreate; case r'memory.read': return Permission.memoryPeriodRead; case r'memory.update': return Permission.memoryPeriodUpdate; diff --git a/open-api/immich-openapi-specs.json b/open-api/immich-openapi-specs.json index 7f09f7b336..cb0c8f8a67 100644 --- a/open-api/immich-openapi-specs.json +++ b/open-api/immich-openapi-specs.json @@ -6305,6 +6305,7 @@ "state": "Stable" } ], + "x-immich-permission": "map.read", "x-immich-state": "Stable" } }, @@ -6376,6 +6377,7 @@ "state": "Stable" } ], + "x-immich-permission": "map.search", "x-immich-state": "Stable" } }, @@ -18966,6 +18968,8 @@ "timeline.read", "timeline.download", "maintenance", + "map.read", + "map.search", "memory.create", "memory.read", "memory.update", diff --git a/open-api/typescript-sdk/src/fetch-client.ts b/open-api/typescript-sdk/src/fetch-client.ts index 97745cc5a1..09a0860539 100644 --- a/open-api/typescript-sdk/src/fetch-client.ts +++ b/open-api/typescript-sdk/src/fetch-client.ts @@ -5534,6 +5534,8 @@ export enum Permission { TimelineRead = "timeline.read", TimelineDownload = "timeline.download", Maintenance = "maintenance", + MapRead = "map.read", + MapSearch = "map.search", MemoryCreate = "memory.create", MemoryRead = "memory.read", MemoryUpdate = "memory.update", diff --git a/server/src/controllers/map.controller.ts b/server/src/controllers/map.controller.ts index dbd1082561..ae3b56af28 100644 --- a/server/src/controllers/map.controller.ts +++ b/server/src/controllers/map.controller.ts @@ -8,7 +8,7 @@ import { MapReverseGeocodeDto, MapReverseGeocodeResponseDto, } from 'src/dtos/map.dto'; -import { ApiTag } from 'src/enum'; +import { ApiTag, Permission } from 'src/enum'; import { Auth, Authenticated } from 'src/middleware/auth.guard'; import { MapService } from 'src/services/map.service'; @@ -18,7 +18,7 @@ export class MapController { constructor(private service: MapService) {} @Get('markers') - @Authenticated() + @Authenticated({ permission: Permission.MapRead }) @Endpoint({ summary: 'Retrieve map markers', description: 'Retrieve a list of latitude and longitude coordinates for every asset with location data.', @@ -28,8 +28,8 @@ export class MapController { return this.service.getMapMarkers(auth, options); } - @Authenticated() @Get('reverse-geocode') + @Authenticated({ permission: Permission.MapSearch }) @HttpCode(HttpStatus.OK) @Endpoint({ summary: 'Reverse geocode coordinates', diff --git a/server/src/enum.ts b/server/src/enum.ts index 8a7e1dc789..5a0f6bdbe0 100644 --- a/server/src/enum.ts +++ b/server/src/enum.ts @@ -160,6 +160,9 @@ export enum Permission { Maintenance = 'maintenance', + MapRead = 'map.read', + MapSearch = 'map.search', + MemoryCreate = 'memory.create', MemoryRead = 'memory.read', MemoryUpdate = 'memory.update', From 7cbfc12e0dc9a8e1eebb04ce3561c2124c81d4ba Mon Sep 17 00:00:00 2001 From: Alex Date: Thu, 22 Jan 2026 06:44:08 -0600 Subject: [PATCH 08/10] chore: use context menu for user table (#25428) * chore: use context menu for user table * chore: reorder columns --------- Co-authored-by: Jason Rasmussen --- web/src/lib/services/user-admin.service.ts | 9 +++++- .../routes/admin/users/(list)/+layout.svelte | 32 ++++++++++++------- .../routes/admin/users/[id]/+layout.svelte | 4 +-- 3 files changed, 31 insertions(+), 14 deletions(-) diff --git a/web/src/lib/services/user-admin.service.ts b/web/src/lib/services/user-admin.service.ts index 71d20bd9ce..233eb88657 100644 --- a/web/src/lib/services/user-admin.service.ts +++ b/web/src/lib/services/user-admin.service.ts @@ -23,6 +23,7 @@ import { import { modalManager, toastManager, type ActionItem } from '@immich/ui'; import { mdiDeleteRestore, + mdiInformationOutline, mdiLockReset, mdiLockSmart, mdiPencilOutline, @@ -46,6 +47,12 @@ export const getUserAdminsActions = ($t: MessageFormatter) => { }; export const getUserAdminActions = ($t: MessageFormatter, user: UserAdminResponseDto) => { + const Detail: ActionItem = { + icon: mdiInformationOutline, + title: $t('details'), + onAction: () => goto(Route.viewUser(user)), + }; + const Update: ActionItem = { icon: mdiPencilOutline, title: $t('edit'), @@ -92,7 +99,7 @@ export const getUserAdminActions = ($t: MessageFormatter, user: UserAdminRespons onAction: () => handleResetPinCodeUserAdmin(user), }; - return { Update, Delete, Restore, ResetPassword, ResetPinCode }; + return { Detail, Update, Delete, Restore, ResetPassword, ResetPinCode }; }; export const handleCreateUserAdmin = async (dto: UserAdminCreateDto) => { diff --git a/web/src/routes/admin/users/(list)/+layout.svelte b/web/src/routes/admin/users/(list)/+layout.svelte index 368ce5ed18..b895e72de5 100644 --- a/web/src/routes/admin/users/(list)/+layout.svelte +++ b/web/src/routes/admin/users/(list)/+layout.svelte @@ -1,15 +1,18 @@ @@ -68,16 +76,18 @@ - {$t('email')} - {$t('name')} + {$t('name')} + {$t('email')} {$t('has_quota')} {#each users as user (user.id)} - {user.email} - {user.name} + + {user.name} + + {user.email}
{#if user.quotaSizeInBytes !== null && user.quotaSizeInBytes >= 0} @@ -88,7 +98,7 @@
- +
{/each} diff --git a/web/src/routes/admin/users/[id]/+layout.svelte b/web/src/routes/admin/users/[id]/+layout.svelte index 92d29aa5e2..61fd184303 100644 --- a/web/src/routes/admin/users/[id]/+layout.svelte +++ b/web/src/routes/admin/users/[id]/+layout.svelte @@ -198,8 +198,8 @@ })} >

{$t('storage')}

-
-
+
+
{/if} From 55477a8a1afb0062697ae528eb38b283666e6f63 Mon Sep 17 00:00:00 2001 From: bo0tzz Date: Thu, 22 Jan 2026 16:53:14 +0100 Subject: [PATCH 09/10] chore: revert mise-action bump (#25451) --- .github/workflows/docs-deploy.yml | 2 +- .github/workflows/docs-destroy.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/docs-deploy.yml b/.github/workflows/docs-deploy.yml index 1933b9d572..8c0bf76f30 100644 --- a/.github/workflows/docs-deploy.yml +++ b/.github/workflows/docs-deploy.yml @@ -131,7 +131,7 @@ jobs: token: ${{ steps.token.outputs.token }} - name: Setup Mise - uses: immich-app/devtools/actions/use-mise@b868e6e7c8cc212beec876330b4059e661ee44bb # use-mise-action-v1.1.1 + uses: immich-app/devtools/actions/use-mise@cd24790a7f5f6439ac32cc94f5523cb2de8bfa8c # use-mise-action-v1.1.0 - name: Load parameters id: parameters diff --git a/.github/workflows/docs-destroy.yml b/.github/workflows/docs-destroy.yml index 80cc17d32b..a7d068cb43 100644 --- a/.github/workflows/docs-destroy.yml +++ b/.github/workflows/docs-destroy.yml @@ -29,7 +29,7 @@ jobs: token: ${{ steps.token.outputs.token }} - name: Setup Mise - uses: immich-app/devtools/actions/use-mise@b868e6e7c8cc212beec876330b4059e661ee44bb # use-mise-action-v1.1.1 + uses: immich-app/devtools/actions/use-mise@cd24790a7f5f6439ac32cc94f5523cb2de8bfa8c # use-mise-action-v1.1.0 - name: Destroy Docs Subdomain env: From 78f400305b4e374722710f73052f98db7ab5b0fa Mon Sep 17 00:00:00 2001 From: Mees Frensel <33722705+meesfrensel@users.noreply.github.com> Date: Thu, 22 Jan 2026 17:07:05 +0100 Subject: [PATCH 10/10] fix(web): don't show ocr button on panoramas (#25450) --- web/src/lib/components/asset-viewer/asset-viewer.svelte | 1 + 1 file changed, 1 insertion(+) diff --git a/web/src/lib/components/asset-viewer/asset-viewer.svelte b/web/src/lib/components/asset-viewer/asset-viewer.svelte index 9e3c121024..91dc041754 100644 --- a/web/src/lib/components/asset-viewer/asset-viewer.svelte +++ b/web/src/lib/components/asset-viewer/asset-viewer.svelte @@ -431,6 +431,7 @@ const showOcrButton = $derived( $slideshowState === SlideshowState.None && asset.type === AssetTypeEnum.Image && + !(asset.exifInfo?.projectionType === 'EQUIRECTANGULAR') && !isShowEditor && ocrManager.hasOcrData, );