perf(ui): V2メディア一覧の初期表示を高速化 - #655
Conversation
Avoid mounting the full result set while virtual grid metrics are pending, prioritize the first two rows, and prefetch V2 filter metadata like the existing routes.
📝 WalkthroughWalkthroughサムネイルを256pxと512pxで扱えるようにし、生成ジョブ、API契約、ウォームアップ画面を追加します。仮想メディアグリッドは画像ロード優先度、 Changesサムネイル生成基盤
メディアグリッドと画像ロード
ウォームアップと検証
Estimated code review effort: 5 (Critical) | ~120 minutes Sequence Diagram(s)sequenceDiagram
participant ManagerScreen
participant ThumbnailRouter
participant ThumbnailService
participant JobWorker
participant ThumbnailImage
ManagerScreen->>ThumbnailRouter: request warmup for size 256
ThumbnailRouter->>ThumbnailService: start generation with missingOnly
ThumbnailService->>JobWorker: enqueue thumbnail jobs
JobWorker->>ThumbnailImage: generate and publish thumbnail
ThumbnailImage-->>ManagerScreen: update job progress
Possibly related PRs
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches 💡 1📝 Generate docstrings 💡
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Keep skeleton rows aligned to the responsive column count and eagerly load the mounted virtual range with a larger overscan buffer.
Match the source collection mount guard so the virtual grid initializes only after the client mount and persisted search state restoration begins.
Limit virtual image prefetching to the initial rows and keep search result queries fresh while navigating back from media details.
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@apps/server/src/hooks/use-media-source-events.ts`:
- Around line 83-89: Update useMediaSourceEvents so both /search and /v2/search
are treated as active routes when id is "*" or a specific source ID; retain the
existing source-specific route checks. Add a test covering the V2 search screen
with searchState.selectedSource set, verifying media add, removal, or change
events trigger updates.
In `@packages/ui/src/thumbnail-image.tsx`:
- Around line 29-47: Introduce a shared load-generation counter in the thumbnail
loading flow so only the newest request may update state. Update load,
applyResolvedUrl, the promise rejection path, handleError, and the
source.subscribe callback to capture and validate the current generation
alongside cancelled before calling setUrl, setError, or source.onError;
invalidate prior generations when props.source changes or a retry starts.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro Plus
Run ID: c84e2579-e48b-4df5-965f-5a2826d510b3
📒 Files selected for processing (10)
apps/server/src/hooks/use-media-source-events.tsapps/server/src/routes/api/sources.$mediaSourceId.thumbnail.$mediaId.tsapps/server/src/routes/v2/search.tsxapps/server/src/routes/v2/sources/$mediaSourceId/index.tsxapps/server/src/tests/e2e/ui-components.gallery.spec.tsapps/server/src/tests/e2e/ui-gallery/src.tsxpackages/ui/src/query-options/search-query.test.tspackages/ui/src/query-options/search-query.tspackages/ui/src/source-media-grid.tsxpackages/ui/src/thumbnail-image.tsx
🚧 Files skipped from review as they are similar to previous changes (1)
- packages/ui/src/source-media-grid.tsx
There was a problem hiding this comment.
Actionable comments posted: 5
♻️ Duplicate comments (1)
packages/ui/src/thumbnail-image.tsx (1)
24-98: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win非同期ロードの競合を解消してください。世代管理がありません。
load()とloadSrcSet()は同じcancelledフラグを共有します。source.subscribeがload()とloadSrcSet()を再実行すると、先行する非同期呼び出しと後続する呼び出しが同時に存在します。先行するPromiseが後から解決すると、applyResolvedUrlまたはsetSrcSetが最新の値を上書きします。仮想グリッドでセルが再利用される場合、または再試行(
onErrorによるcacheKey更新)が短時間に複数回発生する場合、この競合が発生します。古いレスポンスが新しいURLやsrcsetを上書きする可能性があります。世代番号(
requestIdなど)を追加してください。呼び出し時点の世代番号を記録し、状態更新の直前にその世代が最新であることを確認してください。🔒️ 世代管理を追加する修正案
createEffect(() => { const source = props.source; if (source !== previousSource) { previousSource = source; setUrl(null); setSrcSet(undefined); setError(false); } if (props.enabled === false) { return; } setError(false); - let cancelled = false; + let cancelled = false; + let requestId = 0; - const applyResolvedUrl = (resolved: string) => { - if (!cancelled) { + const applyResolvedUrl = (version: number, resolved: string) => { + if (!cancelled && version === requestId) { setUrl(resolved); } }; const load = () => { + const version = ++requestId; try { const resolved = source.getUrl(); if (typeof resolved === "string") { - applyResolvedUrl(resolved); + applyResolvedUrl(version, resolved); return; } - void resolved.then(applyResolvedUrl).catch(() => { - if (!cancelled) { + void resolved + .then((value) => applyResolvedUrl(version, value)) + .catch(() => { + if (!cancelled && version === requestId) { setError(true); source.onError?.(); } }); } catch { - if (!cancelled) { + if (!cancelled && version === requestId) { setError(true); source.onError?.(); } } }; const loadSrcSet = () => { + const version = requestId; try { const resolved = source.getSrcSet?.(); if (typeof resolved === "string" || resolved === undefined) { - if (!cancelled) setSrcSet(resolved); + if (!cancelled && version === requestId) setSrcSet(resolved); return; } void resolved.then((value) => { - if (!cancelled) setSrcSet(value); + if (!cancelled && version === requestId) setSrcSet(value); }); } catch { - if (!cancelled) setSrcSet(undefined); + if (!cancelled && version === requestId) setSrcSet(undefined); } };🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/ui/src/thumbnail-image.tsx` around lines 24 - 98, ThumbnailImageのcreateEffect内で、source.subscribeによる再実行を含む各load/loadSrcSet呼び出しを識別できる世代番号を追加してください。各リクエスト開始時のrequestIdをPromiseの完了処理で確認し、最新世代の場合のみsetUrl、setSrcSet、setError、source.onErrorを実行するように更新します。既存のcancelledチェックは維持し、後続リクエスト開始時に世代を進めて古いレスポンスを無視してください。
🧹 Nitpick comments (3)
apps/server/src/infrastructure/api-clients/thumbnails.ts (1)
13-20: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win共有された
ThumbnailSizeを使用してください。Line 15 の
256 | 512はドメイン契約を重複定義します。
@solid-imager/core/domain/thumbnails/schemasのThumbnailSizeを使用してください。
これにより、API クライアントと共有スキーマのサイズ定義が分離しません。As per coding guidelines, "Use Zod schemas for domain contracts and schema-driven development."
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@apps/server/src/infrastructure/api-clients/thumbnails.ts` around lines 13 - 20, Update startThumbnailGeneration to import and use the shared ThumbnailSize type from `@solid-imager/core/domain/thumbnails/schemas` instead of redeclaring 256 | 512, while preserving the existing options defaults and API call.Source: Coding guidelines
packages/ui/src/screens/v2-manager-screen.tsx (1)
824-859: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win「Current run」セクションを共通コンポーネントへ抽出してください。
このブロックは
BatchToolPanelの行732-767と完全に同一です。taggingStatus、jobProgress、activeJobIdの表示ロジックが2箇所に複製されます。将来どちらか一方だけを変更すると、表示が分岐します。CurrentRunSectionとして1つに集約してください。♻️ 抽出例
+function CurrentRunSection(props: { manager: UseManagerPageResult }) { + return ( + <Show when={props.manager.taggingStatus() || props.manager.jobProgress()}> + <section + aria-live="polite" + class="space-y-3 rounded-md border border-[var(--v2-border)] bg-[var(--v2-surface)] p-4" + > + {/* 既存の Current run マークアップをそのまま移動 */} + </section> + </Show> + ); +}
ThumbnailWarmupPanelとBatchToolPanelの両方で<CurrentRunSection manager={props.manager} />を使います。🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/ui/src/screens/v2-manager-screen.tsx` around lines 824 - 859, Extract the duplicated “Current run” markup and its taggingStatus, jobProgress, and activeJobId display logic into a shared CurrentRunSection component. Update both ThumbnailWarmupPanel and BatchToolPanel to render it via CurrentRunSection with the existing manager prop, preserving the current UI and behavior.packages/ui/src/screens/manager-screen.tsx (1)
376-418: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winソース選択の
Selectブロックを共通コンポーネントへ抽出してください。このファイル内で
Select+SelectValue<unknown>+find(...)のブロックが3箇所に重複します(行260-295のtagging、この行376-418のthumbnails、行470-509のduplicates)。packages/ui/src/screens/v2-manager-screen.tsxは同じ処理をSourceSelectとして抽出済みです。legacy画面でも同等のヘルパーを1つ定義すると、プレースホルダー文言やname抽出ロジックの分岐を1箇所に集約できます。♻️ 抽出例
+function LegacySourceSelect(props: { + manager: UseManagerPageResult; + onChange: (id: string | undefined) => void; + options: { id: string; name: string }[]; + placeholder: string; + value: { id: string; name: string } | null; +}) { + return ( + <Select + itemComponent={(selectProps) => ( + <SelectItem item={selectProps.item}> + {selectProps.item.rawValue.name} + </SelectItem> + )} + onChange={(value) => props.onChange(value?.id)} + options={props.options} + optionTextValue="name" + optionValue="id" + placeholder={props.placeholder} + value={props.value} + > + <SelectTrigger> + <SelectValue<unknown>> + {(state) => { + const option = state.selectedOption(); + return option && typeof option === "object" && "name" in option + ? (option as { name: string }).name + : props.placeholder; + }} + </SelectValue> + </SelectTrigger> + <SelectContent /> + </Select> + ); +}🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/ui/src/screens/manager-screen.tsx` around lines 376 - 418, Extract the duplicated source-selection Select markup into a shared SourceSelect helper within manager-screen.tsx, following the existing v2-manager-screen.tsx implementation. Replace the three tagging, thumbnails, and duplicates Select blocks while preserving their manager source options, selected-source updates, and surrounding labels or descriptions; centralize the placeholder and selected name logic in SourceSelect.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@apps/server/src/application/services/job-dispatch-service.ts`:
- Around line 9-12: job-dispatch-service.ts の infrastructure/jobs/thumbnails
直接参照を削除し、サムネイル処理を application 層のポート(インターフェース)として定義してください。Job dispatch service
はそのポートにのみ依存して実装を注入できる形にし、infrastructure 側で deleteThumbnail と
processThumbnailGenerationJob をポートへ適合させて composition root から注入してください。
In `@apps/server/src/routes/api/sources`.$mediaSourceId.thumbnail.$mediaId.ts:
- Around line 36-57: Update the missing-thumbnail branch in the route handler to
call queueThumbnailGeneration(mediaSourceId, mediaId, size) for every requested
size before the size === THUMBNAIL_SIZE_SMALL fallback logic. Preserve the
existing 256px-specific large-thumbnail fallback, and avoid queueing the same
request twice.
In `@packages/db/src/repositories/job-repository.ts`:
- Around line 72-90: Make active thumbnail job deduplication atomic: add a
partial unique index in the jobs schema covering type, source_id,
payload.mediaId, and payload.size for generate_thumbnail jobs with pending or
in_progress status, then update the job creation flow to use one INSERT ... ON
CONFLICT against that constraint instead of a separate lookup. Add an
integration test proving concurrent creation requests register only one job.
In `@packages/ui/src/screens/v2-manager-screen.tsx`:
- Around line 796-822: Update SourceSelect to accept a placeholder prop and use
it for both the unselected display and select placeholder instead of hardcoding
“All sources.” Pass “Select source” from ThumbnailWarmupPanel, preserving the
existing selection behavior and required-source button disabling.
In `@packages/ui/src/source-media-grid.tsx`:
- Around line 391-404: In the non-virtualized element-scroll branch of the
imageLoadPolicy in source-media-grid, change only the loading threshold from
INITIAL_HIGH_PRIORITY_MEDIA to initialPriorityMediaCount(). Keep fetchpriority’s
threshold as INITIAL_HIGH_PRIORITY_MEDIA and preserve the existing eager/lazy
behavior otherwise.
---
Duplicate comments:
In `@packages/ui/src/thumbnail-image.tsx`:
- Around line 24-98:
ThumbnailImageのcreateEffect内で、source.subscribeによる再実行を含む各load/loadSrcSet呼び出しを識別できる世代番号を追加してください。各リクエスト開始時のrequestIdをPromiseの完了処理で確認し、最新世代の場合のみsetUrl、setSrcSet、setError、source.onErrorを実行するように更新します。既存のcancelledチェックは維持し、後続リクエスト開始時に世代を進めて古いレスポンスを無視してください。
---
Nitpick comments:
In `@apps/server/src/infrastructure/api-clients/thumbnails.ts`:
- Around line 13-20: Update startThumbnailGeneration to import and use the
shared ThumbnailSize type from `@solid-imager/core/domain/thumbnails/schemas`
instead of redeclaring 256 | 512, while preserving the existing options defaults
and API call.
In `@packages/ui/src/screens/manager-screen.tsx`:
- Around line 376-418: Extract the duplicated source-selection Select markup
into a shared SourceSelect helper within manager-screen.tsx, following the
existing v2-manager-screen.tsx implementation. Replace the three tagging,
thumbnails, and duplicates Select blocks while preserving their manager source
options, selected-source updates, and surrounding labels or descriptions;
centralize the placeholder and selected name logic in SourceSelect.
In `@packages/ui/src/screens/v2-manager-screen.tsx`:
- Around line 824-859: Extract the duplicated “Current run” markup and its
taggingStatus, jobProgress, and activeJobId display logic into a shared
CurrentRunSection component. Update both ThumbnailWarmupPanel and BatchToolPanel
to render it via CurrentRunSection with the existing manager prop, preserving
the current UI and behavior.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro Plus
Run ID: b4f703b1-a7c0-4e69-8690-f20fb472f1af
📒 Files selected for processing (44)
apps/server/public/openapi.jsonapps/server/src/application/services/job-dispatch-service.tsapps/server/src/application/services/thumbnail-service.tsapps/server/src/components/media/media-grid-item.tsxapps/server/src/components/media/thumbnail-image.tsxapps/server/src/infrastructure/api-clients/thumbnails.tsapps/server/src/infrastructure/api/routers/thumbnails-router.tsapps/server/src/infrastructure/jobs/job-worker.tsapps/server/src/infrastructure/jobs/thumbnails.tsapps/server/src/routes/api/sources.$mediaSourceId.thumbnail.$mediaId.tsapps/server/src/routes/manager.tsxapps/server/src/routes/search.tsxapps/server/src/routes/sources/$mediaSourceId/components/source-media-page.tsxapps/server/src/routes/v2/manager.tsxapps/server/src/routes/v2/search.tsxapps/server/src/tests/e2e/ui-components.gallery.spec.tsapps/server/src/tests/e2e/ui-gallery/src.tsxapps/server/src/tests/e2e/ui-gallery/vite.config.tsapps/server/src/tests/unit/application/services/job-dispatch-service.test.tsapps/server/src/tests/unit/infrastructure/jobs/job-worker.test.tsapps/tauri/src/components/media/media-grid-item.tsxapps/tauri/src/components/media/thumbnail-image.tsxapps/tauri/src/infrastructure/api-clients/thumbnails-api.tsapps/tauri/src/infrastructure/media/thumbnail-runtime.tsapps/tauri/src/routes/manager.tsxapps/tauri/src/routes/search.tsxapps/tauri/src/routes/sources/$mediaSourceId/components/source-media-page.tsxpackages/application/src/ports/thumbnail-service.tspackages/application/src/services/thumbnail-service.tspackages/core/src/domain/contract/thumbnails.contract.tspackages/core/src/domain/sources/events.tspackages/core/src/domain/thumbnails/schemas.tspackages/db/src/repositories/job-repository.test.tspackages/db/src/repositories/job-repository.tspackages/ui/src/hooks/use-manager-page.tspackages/ui/src/media-grid-item.tsxpackages/ui/src/screens/manager-screen.tsxpackages/ui/src/screens/search-screen.tsxpackages/ui/src/screens/source-media-screen.tsxpackages/ui/src/screens/v2-manager-screen.tsxpackages/ui/src/source-media-grid.tsxpackages/ui/src/thumbnail-image.tsxpackages/ui/src/thumbnail-source.test.tspackages/ui/src/thumbnail-source.ts
🚧 Files skipped from review as they are similar to previous changes (6)
- apps/server/src/routes/sources/$mediaSourceId/components/source-media-page.tsx
- apps/tauri/src/routes/sources/$mediaSourceId/components/source-media-page.tsx
- apps/server/src/routes/search.tsx
- apps/tauri/src/routes/search.tsx
- apps/server/src/routes/v2/search.tsx
- apps/server/src/tests/e2e/ui-gallery/src.tsx
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@apps/server/drizzle/0022_sleepy_turbo.sql`:
- Around line 3-12: UPDATE the DELETE condition in the duplicate jobs cleanup so
only jobs with status `pending` can be deleted; do not include `in_progress`
rows in either the deleted duplicate set or the retained-row matching logic
unless required for safe deduplication. Preserve the existing duplicate matching
criteria for type, source_id, payload fields, and creation ordering.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro Plus
Run ID: 7cb87692-c6b2-4cd4-ba57-784c7cbe0da2
📒 Files selected for processing (16)
apps/server/drizzle/0022_sleepy_turbo.sqlapps/server/drizzle/meta/0022_snapshot.jsonapps/server/drizzle/meta/_journal.jsonapps/server/src/application/services/job-dispatch-service.tsapps/server/src/hooks/use-media-source-events.tsapps/server/src/infrastructure/bootstrap.tsapps/server/src/routes/api/sources.$mediaSourceId.thumbnail.$mediaId.tsapps/server/src/tests/unit/application/services/job-dispatch-service.test.tspackages/db/src/repositories/job-repository.test.tspackages/db/src/repositories/job-repository.tspackages/db/src/schema.tspackages/ui/src/hooks/use-search-page.tspackages/ui/src/hooks/use-source-media-page.tspackages/ui/src/screens/v2-manager-screen.tsxpackages/ui/src/source-media-grid.tsxpackages/ui/src/thumbnail-image.tsx
🚧 Files skipped from review as they are similar to previous changes (8)
- apps/server/src/hooks/use-media-source-events.ts
- apps/server/src/tests/unit/application/services/job-dispatch-service.test.ts
- apps/server/src/routes/api/sources.$mediaSourceId.thumbnail.$mediaId.ts
- packages/db/src/repositories/job-repository.ts
- packages/db/src/repositories/job-repository.test.ts
- packages/ui/src/thumbnail-image.tsx
- packages/ui/src/screens/v2-manager-screen.tsx
- packages/ui/src/source-media-grid.tsx
| DELETE FROM "jobs" AS duplicate | ||
| USING "jobs" AS retained | ||
| WHERE duplicate."type" = 'generate_thumbnail' | ||
| AND duplicate."status" IN ('pending', 'in_progress') | ||
| AND retained."type" = duplicate."type" | ||
| AND retained."status" IN ('pending', 'in_progress') | ||
| AND retained."source_id" = duplicate."source_id" | ||
| AND retained."payload"->>'mediaId' = duplicate."payload"->>'mediaId' | ||
| AND retained."payload"->>'size' = duplicate."payload"->>'size' | ||
| AND (retained."created_at", retained."id") < (duplicate."created_at", duplicate."id"); |
There was a problem hiding this comment.
🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win
in_progress のジョブを物理削除します。
DELETE の条件は pending と in_progress の両方を対象にします。マイグレーション実行時にワーカーが稼働していると、実行中ジョブの行が消えます。その場合、ワーカーの完了更新は0行更新になり、結果とエラーが記録されません。
対策は2つあります。削除対象を pending のみに限定し、in_progress の重複は自然消滅を待つ方法が安全です。あるいは、マイグレーションをワーカー停止中に実行する運用手順を明記してください。
🛠️ pending のみを削除対象にする例
DELETE FROM "jobs" AS duplicate
USING "jobs" AS retained
WHERE duplicate."type" = 'generate_thumbnail'
- AND duplicate."status" IN ('pending', 'in_progress')
+ AND duplicate."status" = 'pending'
AND retained."type" = duplicate."type"
AND retained."status" IN ('pending', 'in_progress')この変更では、in_progress 行が残り、pending 側が削除されます。順序比較の条件は再検討が必要です。
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| DELETE FROM "jobs" AS duplicate | |
| USING "jobs" AS retained | |
| WHERE duplicate."type" = 'generate_thumbnail' | |
| AND duplicate."status" IN ('pending', 'in_progress') | |
| AND retained."type" = duplicate."type" | |
| AND retained."status" IN ('pending', 'in_progress') | |
| AND retained."source_id" = duplicate."source_id" | |
| AND retained."payload"->>'mediaId' = duplicate."payload"->>'mediaId' | |
| AND retained."payload"->>'size' = duplicate."payload"->>'size' | |
| AND (retained."created_at", retained."id") < (duplicate."created_at", duplicate."id"); | |
| DELETE FROM "jobs" AS duplicate | |
| USING "jobs" AS retained | |
| WHERE duplicate."type" = 'generate_thumbnail' | |
| AND duplicate."status" = 'pending' | |
| AND retained."type" = duplicate."type" | |
| AND retained."status" IN ('pending', 'in_progress') | |
| AND retained."source_id" = duplicate."source_id" | |
| AND retained."payload"->>'mediaId' = duplicate."payload"->>'mediaId' | |
| AND retained."payload"->>'size' = duplicate."payload"->>'size' | |
| AND (retained."created_at", retained."id") < (duplicate."created_at", duplicate."id"); |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@apps/server/drizzle/0022_sleepy_turbo.sql` around lines 3 - 12, UPDATE the
DELETE condition in the duplicate jobs cleanup so only jobs with status
`pending` can be deleted; do not include `in_progress` rows in either the
deleted duplicate set or the retained-row matching logic unless required for
safe deduplication. Preserve the existing duplicate matching criteria for type,
source_id, payload fields, and creation ordering.
概要
V2のメディア一覧で、画像が遅れてバラバラに表示される問題を改善します。
変更内容
検証
Summary by CodeRabbit
新機能
バグ修正