From 72cefcabafa75a9d7aef8e04823ceb1a94c54d20 Mon Sep 17 00:00:00 2001 From: bo0tzz Date: Sat, 14 Feb 2026 17:40:27 +0100 Subject: [PATCH 1/6] chore: discourage LLM-generated PRs (#26211) * chore: discourage LLM-generated PRs * chore: add reading CONTRIBUTING.md to PR checklist * chore: workflow to close LLM-generated PRs --- .github/pull_request_template.md | 1 + .github/workflows/close-llm-pr.yml | 38 ++++++++++++++++++++++++++++++ CONTRIBUTING.md | 2 +- 3 files changed, 40 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/close-llm-pr.yml diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index 0bd3b30814..2d1fdafa30 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -26,6 +26,7 @@ The `/api/something` endpoint is now `/api/something-else` ## Checklist: +- [ ] I have carefully read CONTRIBUTING.md - [ ] I have performed a self-review of my own code - [ ] I have made corresponding changes to the documentation if applicable - [ ] I have no unrelated changes in the PR. diff --git a/.github/workflows/close-llm-pr.yml b/.github/workflows/close-llm-pr.yml new file mode 100644 index 0000000000..f17d98e684 --- /dev/null +++ b/.github/workflows/close-llm-pr.yml @@ -0,0 +1,38 @@ +name: Close LLM-generated PRs + +on: + pull_request: + types: [labeled] + +permissions: {} + +jobs: + comment_and_close: + runs-on: ubuntu-latest + if: ${{ github.event.label.name == 'llm-generated' }} + permissions: + pull-requests: write + steps: + - name: Comment and close + env: + GH_TOKEN: ${{ github.token }} + NODE_ID: ${{ github.event.pull_request.node_id }} + run: | + gh api graphql \ + -f prId="$NODE_ID" \ + -f body="Thank you for your interest in contributing to Immich! Unfortunately this PR looks like it was generated using an LLM. As noted in our CONTRIBUTING.md, we request that you don't use LLMs to generate PRs as those are not a good use of maintainer time." \ + -f query=' + mutation CommentAndClosePR($prId: ID!, $body: String!) { + addComment(input: { + subjectId: $prId, + body: $body + }) { + __typename + } + + closePullRequest(input: { + pullRequestId: $prId + }) { + __typename + } + }' diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 109708cc6e..1695403cb4 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -17,7 +17,7 @@ If you are looking for something to work on, there are discussions and issues wi ## Use of generative AI -We generally discourage PRs entirely generated by an LLM. For any part generated by an LLM, please put extra effort into your self-review. By using generative AI without proper self-review, the time you save ends up being more work we need to put in for proper reviews and code cleanup. Please keep that in mind when submitting code by an LLM. Clearly state the use of LLMs/(generative) AI in your pull request as requested by the template. +We ask you not to open PRs generated with an LLM. We find that code generated like this tends to need a large amount of back-and-forth, which is a very inefficient use of our time. If we want LLM-generated code, it's much faster for us to use an LLM ourselves than to go through an intermediary via a pull request. ## Feature freezes From 2c9d69865c834d85ac30a19705aa002221760148 Mon Sep 17 00:00:00 2001 From: Min Idzelis Date: Sat, 14 Feb 2026 12:51:54 -0500 Subject: [PATCH 2/6] fix: e2e exit 135 (#26214) --- e2e/docker-compose.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/e2e/docker-compose.yml b/e2e/docker-compose.yml index a98a7013a4..5a79396aa5 100644 --- a/e2e/docker-compose.yml +++ b/e2e/docker-compose.yml @@ -53,6 +53,7 @@ services: POSTGRES_DB: immich ports: - 5435:5432 + shm_size: 128mb healthcheck: test: ['CMD-SHELL', 'pg_isready -U postgres -d immich'] interval: 1s From d264e78d3f1a1b06e37ac696fa03b0f7591d3d17 Mon Sep 17 00:00:00 2001 From: Min Idzelis Date: Sat, 14 Feb 2026 15:03:08 -0500 Subject: [PATCH 3/6] chore: pnpm workspace protocol for sibling packagages (#26218) --- cli/package.json | 2 +- e2e/package.json | 6 +++--- pnpm-lock.yaml | 10 +++++----- web/package.json | 2 +- 4 files changed, 10 insertions(+), 10 deletions(-) diff --git a/cli/package.json b/cli/package.json index 28bee420aa..d80efdd74a 100644 --- a/cli/package.json +++ b/cli/package.json @@ -14,7 +14,7 @@ ], "devDependencies": { "@eslint/js": "^9.8.0", - "@immich/sdk": "file:../open-api/typescript-sdk", + "@immich/sdk": "workspace:*", "@types/byte-size": "^8.1.0", "@types/cli-progress": "^3.11.0", "@types/lodash-es": "^4.17.12", diff --git a/e2e/package.json b/e2e/package.json index 01dd036a2f..abe46a39ca 100644 --- a/e2e/package.json +++ b/e2e/package.json @@ -21,9 +21,9 @@ "devDependencies": { "@eslint/js": "^9.8.0", "@faker-js/faker": "^10.1.0", - "@immich/cli": "file:../cli", - "@immich/e2e-auth-server": "file:../e2e-auth-server", - "@immich/sdk": "file:../open-api/typescript-sdk", + "@immich/cli": "workspace:*", + "@immich/e2e-auth-server": "workspace:*", + "@immich/sdk": "workspace:*", "@playwright/test": "^1.44.1", "@socket.io/component-emitter": "^3.1.2", "@types/luxon": "^3.4.2", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a435f3db6d..9af490b82a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -45,7 +45,7 @@ importers: specifier: ^9.8.0 version: 9.39.2 '@immich/sdk': - specifier: file:../open-api/typescript-sdk + specifier: workspace:* version: link:../open-api/typescript-sdk '@types/byte-size': specifier: ^8.1.0 @@ -202,13 +202,13 @@ importers: specifier: ^10.1.0 version: 10.3.0 '@immich/cli': - specifier: file:../cli + specifier: workspace:* version: link:../cli '@immich/e2e-auth-server': - specifier: file:../e2e-auth-server + specifier: workspace:* version: link:../e2e-auth-server '@immich/sdk': - specifier: file:../open-api/typescript-sdk + specifier: workspace:* version: link:../open-api/typescript-sdk '@playwright/test': specifier: ^1.44.1 @@ -738,7 +738,7 @@ importers: specifier: ^0.4.3 version: 0.4.3 '@immich/sdk': - specifier: file:../open-api/typescript-sdk + specifier: workspace:* version: link:../open-api/typescript-sdk '@immich/ui': specifier: ^0.62.1 diff --git a/web/package.json b/web/package.json index e172584c5d..5b66c75029 100644 --- a/web/package.json +++ b/web/package.json @@ -26,7 +26,7 @@ "dependencies": { "@formatjs/icu-messageformat-parser": "^3.0.0", "@immich/justified-layout-wasm": "^0.4.3", - "@immich/sdk": "file:../open-api/typescript-sdk", + "@immich/sdk": "workspace:*", "@immich/ui": "^0.62.1", "@mapbox/mapbox-gl-rtl-text": "0.2.3", "@mdi/js": "^7.4.47", From 9ab887d5d229be9ac506a002a01558a5580e7e63 Mon Sep 17 00:00:00 2001 From: Michel Heusschen <59014050+michelheusschen@users.noreply.github.com> Date: Sat, 14 Feb 2026 21:24:47 +0100 Subject: [PATCH 4/6] perf(web): speed up multi asset operations (#26217) --- .../timeline-manager/day-group.svelte.ts | 20 ++++++++----------- 1 file changed, 8 insertions(+), 12 deletions(-) diff --git a/web/src/lib/managers/timeline-manager/day-group.svelte.ts b/web/src/lib/managers/timeline-manager/day-group.svelte.ts index e21e54a6e5..ba12f4bb6c 100644 --- a/web/src/lib/managers/timeline-manager/day-group.svelte.ts +++ b/web/src/lib/managers/timeline-manager/day-group.svelte.ts @@ -102,25 +102,21 @@ export class DayGroup { } runAssetCallback(ids: Set, callback: (asset: TimelineAsset) => void | { remove?: boolean }) { - if (ids.size === 0) { - return { - moveAssets: [] as MoveAsset[], - processedIds: new SvelteSet(), - unprocessedIds: ids, - changedGeometry: false, - }; - } const unprocessedIds = new SvelteSet(ids); const processedIds = new SvelteSet(); const moveAssets: MoveAsset[] = []; let changedGeometry = false; - for (const assetId of unprocessedIds) { - const index = this.viewerAssets.findIndex((viewAsset) => viewAsset.id == assetId); - if (index === -1) { + + if (ids.size === 0) { + return { moveAssets, processedIds, unprocessedIds, changedGeometry }; + } + + for (let index = this.viewerAssets.length - 1; index >= 0; index--) { + const { id: assetId, asset } = this.viewerAssets[index]; + if (!ids.has(assetId)) { continue; } - const asset = this.viewerAssets[index].asset!; const oldTime = { ...asset.localDateTime }; const callbackResult = callback(asset); let remove = (callbackResult as { remove?: boolean } | undefined)?.remove ?? false; From 49ba833e4c12c84b405ff6f8c6d59c65652bc64d Mon Sep 17 00:00:00 2001 From: Alex Date: Sat, 14 Feb 2026 14:25:14 -0600 Subject: [PATCH 5/6] fix(web): Revert "add checkerboard background for transparent images (#26091)" (#26220) Revert "fix(web): add checkerboard background for transparent images (#26091)" This reverts commit bc7a1c838ca3ab5db3e86b2d8a98733d964e6e7c. --- web/src/lib/components/asset-viewer/photo-viewer.svelte | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/web/src/lib/components/asset-viewer/photo-viewer.svelte b/web/src/lib/components/asset-viewer/photo-viewer.svelte index 7c3ab2eb21..2101107f6e 100644 --- a/web/src/lib/components/asset-viewer/photo-viewer.svelte +++ b/web/src/lib/components/asset-viewer/photo-viewer.svelte @@ -226,7 +226,7 @@ alt={$getAltText(toTimelineAsset(asset))} class="h-full w-full {$slideshowState === SlideshowState.None ? 'object-contain' - : slideshowLookCssMapping[$slideshowLook]} checkerboard" + : slideshowLookCssMapping[$slideshowLook]}" draggable="false" /> @@ -259,8 +259,4 @@ visibility: hidden; animation: 0s linear 0.4s forwards delayedVisibility; } - .checkerboard { - background-image: conic-gradient(#808080 25%, #b0b0b0 25% 50%, #808080 50% 75%, #b0b0b0 75%); - background-size: 20px 20px; - } From ff7dca35f5c6af2a514b041a33fa4f2247aecfa0 Mon Sep 17 00:00:00 2001 From: Michel Heusschen <59014050+michelheusschen@users.noreply.github.com> Date: Sat, 14 Feb 2026 21:31:04 +0100 Subject: [PATCH 6/6] perf(web): speed up asset selection (#26216) --- .../lib/stores/asset-interaction.svelte.ts | 20 ++++++++----------- web/src/lib/utils/asset-utils.ts | 7 ++++--- 2 files changed, 12 insertions(+), 15 deletions(-) diff --git a/web/src/lib/stores/asset-interaction.svelte.ts b/web/src/lib/stores/asset-interaction.svelte.ts index 9cfc1b2c8e..817354e619 100644 --- a/web/src/lib/stores/asset-interaction.svelte.ts +++ b/web/src/lib/stores/asset-interaction.svelte.ts @@ -1,14 +1,15 @@ import type { TimelineAsset } from '$lib/managers/timeline-manager/types'; import { user } from '$lib/stores/user.store'; import { AssetVisibility, type UserAdminResponseDto } from '@immich/sdk'; -import { SvelteSet } from 'svelte/reactivity'; +import { SvelteMap, SvelteSet } from 'svelte/reactivity'; import { fromStore } from 'svelte/store'; export class AssetInteraction { - selectedAssets = $state([]); + private selectedAssetsMap = new SvelteMap(); + selectedAssets = $derived(Array.from(this.selectedAssetsMap.values())); selectAll = $state(false); hasSelectedAsset(assetId: string) { - return this.selectedAssets.some((asset) => asset.id === assetId); + return this.selectedAssetsMap.has(assetId); } selectedGroup = new SvelteSet(); assetSelectionCandidates = $state([]); @@ -16,7 +17,7 @@ export class AssetInteraction { return this.assetSelectionCandidates.some((asset) => asset.id === assetId); } assetSelectionStart = $state(null); - selectionActive = $derived(this.selectedAssets.length > 0); + selectionActive = $derived(this.selectedAssetsMap.size > 0); private user = fromStore(user); private userId = $derived(this.user.current?.id); @@ -27,9 +28,7 @@ export class AssetInteraction { isAllUserOwned = $derived(this.selectedAssets.every((asset) => asset.ownerId === this.userId)); selectAsset(asset: TimelineAsset) { - if (!this.hasSelectedAsset(asset.id)) { - this.selectedAssets.push(asset); - } + this.selectedAssetsMap.set(asset.id, asset); } selectAssets(assets: TimelineAsset[]) { @@ -39,10 +38,7 @@ export class AssetInteraction { } removeAssetFromMultiselectGroup(assetId: string) { - const index = this.selectedAssets.findIndex((a) => a.id == assetId); - if (index !== -1) { - this.selectedAssets.splice(index, 1); - } + this.selectedAssetsMap.delete(assetId); } addGroupToMultiselectGroup(group: string) { @@ -69,7 +65,7 @@ export class AssetInteraction { this.selectAll = false; // Multi-selection - this.selectedAssets = []; + this.selectedAssetsMap.clear(); this.selectedGroup.clear(); // Range selection diff --git a/web/src/lib/utils/asset-utils.ts b/web/src/lib/utils/asset-utils.ts index 84e386d620..47df967844 100644 --- a/web/src/lib/utils/asset-utils.ts +++ b/web/src/lib/utils/asset-utils.ts @@ -4,7 +4,6 @@ import { authManager } from '$lib/managers/auth-manager.svelte'; import { downloadManager } from '$lib/managers/download-manager.svelte'; import { TimelineManager } from '$lib/managers/timeline-manager/timeline-manager.svelte'; import type { TimelineAsset } from '$lib/managers/timeline-manager/types'; -import { assetsSnapshot } from '$lib/managers/timeline-manager/utils.svelte'; import { Route } from '$lib/route'; import type { AssetInteraction } from '$lib/stores/asset-interaction.svelte'; import { preferences } from '$lib/stores/user.store'; @@ -443,13 +442,15 @@ export const selectAllAssets = async (timelineManager: TimelineManager, assetInt try { for (const monthGroup of timelineManager.months) { - await timelineManager.loadMonthGroup(monthGroup.yearMonth); + if (!monthGroup.isLoaded) { + await timelineManager.loadMonthGroup(monthGroup.yearMonth); + } if (!assetInteraction.selectAll) { assetInteraction.clearMultiselect(); break; // Cancelled } - assetInteraction.selectAssets(assetsSnapshot([...monthGroup.assetsIterator()])); + assetInteraction.selectAssets([...monthGroup.assetsIterator()]); for (const dateGroup of monthGroup.dayGroups) { assetInteraction.addGroupToMultiselectGroup(dateGroup.groupTitle);