Skip to content

feat(v2): add no-migration feature support - #663

Open
hmjn023 wants to merge 2 commits into
developfrom
feat/v2-no-migration-features
Open

feat(v2): add no-migration feature support#663
hmjn023 wants to merge 2 commits into
developfrom
feat/v2-no-migration-features

Conversation

@hmjn023

@hmjn023 hmjn023 commented Aug 10, 2026

Copy link
Copy Markdown
Owner

概要

REPORT.mdで不足していた機能のうち、DB migrationなしで自然に提供できる機能をV2へ接続します。このPRは既存のperf/v2-media-gallery-performance向けPR #662に積み上げています。

変更内容

  • Jobs一覧・詳細・失敗JobのRetry・リアルタイム更新
  • Sourceのメディア件数・同期状態
  • Project / IP / Characterの利用件数
  • AI接続確認とレイテンシ表示
  • CollectionのGrid / List切り替え
  • メディア詳細の前後移動用コンテキスト保存
  • Tauri向けV2 route adapter
  • OpenAPIとREPORT.mdの更新

未対応

  • Job Cancelは永続的な状態遷移と権限制御が必要なため未対応
  • Export / RestoreのJobs追跡は現行の直接転送方式に対して不自然なため未対応

検証

  • bun run typecheck
  • Biome check
  • packages/ui unit tests: 66 passed
  • apps/server unit tests: 164 passed
  • apps/server integration tests: 68 passed
  • Tauri build / typecheck

Summary by CodeRabbit

  • 新機能

    • ジョブの一覧・詳細表示、ステータス絞り込み、進捗確認、失敗ジョブの再試行に対応しました。
    • AIサービスのヘルスチェックを設定画面から実行できるようになりました。
    • メディアソースの同期状態とメディア件数を一覧で確認できます。
    • 検索結果やソース内メディアで、グリッド表示とリスト表示を切り替えられます。
    • メディア詳細画面で一覧内の前後メディアへ移動できるようになりました。
  • ドキュメント

    • スタックブランチと依存PRの操作手順を追加しました。

@coderabbitai

coderabbitai Bot commented Aug 10, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

V2画面を実データに接続しました。ジョブ管理、AIヘルスチェック、ソース同期状態、メディア件数、リスト表示、メディア詳細の前後移動、Tauriルート変換を追加しました。gh-stackの運用手順も追加しました。

Changes

gh-stack運用文書

Layer / File(s) Summary
gh-stackの運用手順
.agents/skills/gh-stack/SKILL.md, AGENTS.md
スタックの作成、同期、rebase、レビュー対応、checkout、再構成、マージ手順を追加しました。

V2機能とAPI拡張

Layer / File(s) Summary
API契約と共有スキーマ
apps/server/public/openapi.json, packages/core/src/domain/...
ジョブ、AIヘルスチェック、ソース同期状態、メディア件数の契約と検証スキーマを追加しました。
バックエンドAPIと同期状態
apps/server/src/application/..., apps/server/src/infrastructure/api/...
同期状態イベント、メディア件数集計、AIヘルスチェック、ジョブ一覧・詳細・再試行、ソース概要を実装しました。
ソースイベント更新
packages/ui/src/hooks/..., apps/server/src/components/v2/v2-source-list.tsx
同期状態イベントを処理し、ソース表示、クエリ更新、同期ボタン状態を変更しました。
ジョブ画面
packages/ui/src/hooks/use-job-events.ts, packages/ui/src/query-options/..., apps/server/src/routes/v2/jobs.tsx, packages/ui/src/screens/v2-jobs-screen.tsx
ジョブ一覧、状態フィルター、進捗、詳細、再試行、イベントによる更新を追加しました。
メディアコンテキストと詳細ナビゲーション
apps/server/src/routes/v2/media-context.ts, apps/server/src/routes/v2/..., packages/ui/src/source-media-*.tsx
詳細表示前に一覧コンテキストを保存し、前後メディアへの移動を実装しました。
リスト表示とV2ルート接続
packages/ui/src/source-media-grid.tsx, packages/ui/src/v2/search-toolbar.tsx, apps/tauri/src/routes/v2/..., packages/ui/src/screens/v2-config-screen.tsx
メディアのリスト表示、表示モード切り替え、AIヘルスチェック表示、Tauriの旧V2ルート変換を追加しました。

Estimated code review effort: 5 (Critical) | ~120 minutes

Sequence Diagram(s)

sequenceDiagram
  participant V2JobsScreen
  participant V2JobsRoute
  participant jobsRouter
  participant Database
  V2JobsScreen->>V2JobsRoute: request refresh or retry
  V2JobsRoute->>jobsRouter: call jobs API
  jobsRouter->>Database: query or update job
  Database-->>jobsRouter: return job DTO data
  jobsRouter-->>V2JobsRoute: return jobs response
  V2JobsRoute-->>V2JobsScreen: update list and inspector
Loading
sequenceDiagram
  participant DirectorySyncService
  participant RealtimeEventBus
  participant useSourcesEvents
  participant V2SourceList
  DirectorySyncService->>RealtimeEventBus: publish source-sync-status
  RealtimeEventBus->>useSourcesEvents: validate and handle event
  useSourcesEvents->>V2SourceList: invalidate query and update status
Loading

Possibly related PRs

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 3.77% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed タイトルは、データベースマイグレーションを必要としないV2機能の追加という変更の主目的を明確に要約しています。
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/v2-no-migration-features

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

Base automatically changed from perf/v2-media-gallery-performance to develop August 10, 2026 15:22
@hmjn023
hmjn023 force-pushed the feat/v2-no-migration-features branch from de3e087 to a331c5c Compare August 10, 2026 15:22

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 14

🧹 Nitpick comments (4)
packages/ui/src/screens/v2-jobs-screen.tsx (1)

404-407: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

型アサーションの理由をコメントで示してください。

value as JobFilter は外部ライブラリ境界での最小スコープのアサーションです。コーディングガイドラインは、やむを得ない場合に理由をコメントで示すことを求めています。

♻️ 提案する変更
 					<Tabs
-						onChange={(value) => setActiveFilter(value as JobFilter)}
+						// Kobalte の onChange は string を渡す。値は JOB_FILTERS の value に限定される。
+						onChange={(value) => setActiveFilter(value as JobFilter)}
 						value={activeFilter()}
 					>

コーディングガイドラインに基づく指摘です(「外部ライブラリ境界では……型アサーションがやむを得ない場合は最小スコープに限定して理由をコメントする」)。

🤖 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-jobs-screen.tsx` around lines 404 - 407, Tabs の
onChange コールバックにある value as JobFilter の型アサーション直前へ、外部ライブラリの値が JobFilter
として型付けされていないため最小スコープでアサーションしている理由を示すコメントを追加してください。

Source: Coding guidelines

packages/ui/src/query-options/jobs-query.ts (1)

7-11: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

キーのプレフィックスを all() から派生させてください。

"jobs" のリテラルが3箇所に重複しています。all() を基点にすると、プレフィックスの不整合を防げます。無効化は all() のプレフィックス一致に依存するため、この整合は重要です。

♻️ 提案する変更
 export const jobsQueryKeys = {
 	all: () => ["jobs"] as const,
-	list: (input: JobListRequest) => ["jobs", "list", input] as const,
-	detail: (jobId: string) => ["jobs", "detail", jobId] as const,
+	list: (input: JobListRequest) =>
+		[...jobsQueryKeys.all(), "list", input] as const,
+	detail: (jobId: string) => [...jobsQueryKeys.all(), "detail", jobId] as const,
 };
🤖 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/query-options/jobs-query.ts` around lines 7 - 11, Update
jobsQueryKeys so list and detail derive their shared prefix from all() instead
of repeating the "jobs" literal, while preserving their existing key shapes and
all() prefix-matching behavior.
.agents/skills/gh-stack/SKILL.md (1)

17-17: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

markdownlint の警告を解消してください。

静的解析が MD040 を十個のコードフェンスで、MD028 を Line 321 で検出しています。ASCII 図とコマンド構文には text、実行例には bash などの言語識別子を付けてください。Line 321 の blockquote 内の空行は削除してください。

Also applies to: 84-84, 321-321, 416-416, 453-453, 502-502, 575-575, 629-629, 670-670, 803-803, 839-839

🤖 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 @.agents/skills/gh-stack/SKILL.md at line 17, SKILL.md
内の10個のコードフェンスに適切な言語識別子を追加し、ASCII図とコマンド構文にはtext、実行例にはbashなどを指定してください。あわせて、blockquote内の空行を削除してMD028警告を解消してください。

Source: Linters/SAST tools

apps/server/src/application/services/directory-sync-service.ts (1)

27-33: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

globalThis の型アサーションを除去してください。

Line 27 は拡張したグローバル型を型アサーションで作成しています。declare global__SOLID_IMAGER_SOURCE_SYNC_STATES__ を宣言し、直接参照してください。型アサーションを維持する場合は、必要な理由をコメントしてください。

As per coding guidelines, 「型アサーションがやむを得ない場合は最小スコープに限定して理由をコメントする。」

🤖 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/application/services/directory-sync-service.ts` around lines
27 - 33, Remove the globalThis type assertion around syncStateGlobal and add a
declare global declaration for __SOLID_IMAGER_SOURCE_SYNC_STATES__ with type
Map<string, SourceSyncStatus> | undefined. Update the sourceSyncStates
initialization to reference globalThis.__SOLID_IMAGER_SOURCE_SYNC_STATES__
directly while preserving its existing fallback and assignment behavior.

Source: Coding guidelines

🤖 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 @.agents/skills/gh-stack/SKILL.md:
- Around line 56-57: Update the gh stack submit behavior description in the
relevant numbered guidance to state that omitting --auto opens the full-screen
editor for the PR title, body, and draft/ready status; retain the recommendation
to always use --auto, explaining that it skips this editor.
- Line 322: Update the gh stack sync guidance to state that automation must not
proceed based solely on a successful exit code: in non-interactive divergence,
detect “ℹ Sync aborted” on stderr and stop processing. For flows that continue,
require validating the current stack state with gh stack view --json; direct
resolution toward unstacking and recreating the stack.

In `@apps/server/public/openapi.json`:
- Around line 6826-6831:
OpenAPI生成で、/jobs/list、/jobs/get、/jobs/retryの200レスポンスに具体的なスキーマを設定し、空のanyOf({} と
not
{})を除去してください。jobListResponseSchemaにはjobListResponseSchema、個別ジョブ応答にはjobDtoSchemaの契約を参照させ、生成クライアントがitems、total、ジョブDTOの型を取得できるようにします。

In `@apps/server/src/application/services/directory-sync-service.ts`:
- Around line 168-182: Make sync execution exclusive per source ID in
syncMediaSource by adding a per-mediaSourceId mutex or in-flight Promise guard
before the synchronization work begins. Skip or await duplicate calls according
to the existing flow, and ensure the guard is always released in a finally
block, including errors and early returns.
- Around line 267-271: Prevent internal error details from reaching clients: in
apps/server/src/application/services/directory-sync-service.ts:267-271, update
publishSyncStatus to use a fixed safe directory-sync failure message instead of
error.message, while retaining detailed errors in server logs. In
apps/server/src/infrastructure/api/routers/jobs-router.ts:53-65, map job.error
to an explicitly public error code or fixed safe message when constructing the
Safe Job DTO.

In `@apps/server/src/infrastructure/api/routers/jobs-router.ts`:
- Around line 111-123: Update the retry operation in the jobs router to
atomically update only when the job ID matches and its status is "failed" by
adding that status predicate to the update condition. When no row is returned,
preserve the not-found response for missing jobs and return the appropriate
current-state error for jobs that are no longer failed.
- Around line 72-125: 公開ハンドラの list、get、retry に共有レスポンススキーマを os.output(...)
で設定し、toJobDto の戻り値と list の items/total
を実行時に検証できるようにしてください。既存の入力検証とハンドラの処理内容は維持し、各エンドポイントで適切な共通出力スキーマを再利用してください。
- Around line 111-124: 再試行成功時にジョブ更新イベントが発行されていません。`requeued`
の存在確認後、`toJobDto(requeued)` で作成した成功 DTO を既存のジョブイベント契約で `RealtimeEventBus`
に発行してから返却し、未検出時は現在の `NOT_FOUND` 処理を維持してください。

In `@apps/server/src/routes/v2/jobs.tsx`:
- Around line 20-25: Update the useJobEvents invalidation callback so
high-frequency job-progress events do not invalidate jobsQueryKeys.all() on
every event. Restrict invalidation to job completion/failure events, or throttle
all invalidations to approximately 500ms–1s while preserving timely list
refreshes.

In `@apps/server/src/routes/v2/sources/`$mediaSourceId/$mediaId/index.tsx:
- Around line 51-62: Update navigateToNeighbor so neighbor navigation replaces
the current detail-page history entry by enabling replace: true on navigate.
Preserve the existing neighbor lookup, route parameters, and destination.

In `@apps/tauri/src/routes/v2/`$.tsx:
- Around line 11-16: Update the segment decoding in segments() to safely handle
decodeURIComponent failures, including malformed percent escapes and invalid
UTF-8. Return the original segment whenever decoding throws, so route resolution
can continue and NotFoundScreen remains reachable.

In `@packages/ui/src/hooks/use-job-events.ts`:
- Around line 10-18: Update useJobEvents so enabled is an Accessor<boolean>
matching use-media-source-events.ts, and evaluate enabled() inside its
createEffect guard. Preserve the existing server check and ensure the effect
starts or stops the event subscription reactively when the signal changes.

In `@packages/ui/src/screens/v2-jobs-screen.tsx`:
- Around line 143-200: Add the missing accessibility attributes in JobsTable:
provide a descriptive table caption, add scope="col" to each column header, and
set the row-selection button’s aria-pressed based on whether job.id matches
selectedJobId.
- Around line 56-63: Replace the local formatDate helper in the jobs screen with
the shared formatDate from v2-manager/utils, reusing its Date | string handling
and removing the duplicate dateFormatter definition.

---

Nitpick comments:
In @.agents/skills/gh-stack/SKILL.md:
- Line 17: SKILL.md
内の10個のコードフェンスに適切な言語識別子を追加し、ASCII図とコマンド構文にはtext、実行例にはbashなどを指定してください。あわせて、blockquote内の空行を削除してMD028警告を解消してください。

In `@apps/server/src/application/services/directory-sync-service.ts`:
- Around line 27-33: Remove the globalThis type assertion around syncStateGlobal
and add a declare global declaration for __SOLID_IMAGER_SOURCE_SYNC_STATES__
with type Map<string, SourceSyncStatus> | undefined. Update the sourceSyncStates
initialization to reference globalThis.__SOLID_IMAGER_SOURCE_SYNC_STATES__
directly while preserving its existing fallback and assignment behavior.

In `@packages/ui/src/query-options/jobs-query.ts`:
- Around line 7-11: Update jobsQueryKeys so list and detail derive their shared
prefix from all() instead of repeating the "jobs" literal, while preserving
their existing key shapes and all() prefix-matching behavior.

In `@packages/ui/src/screens/v2-jobs-screen.tsx`:
- Around line 404-407: Tabs の onChange コールバックにある value as JobFilter
の型アサーション直前へ、外部ライブラリの値が JobFilter
として型付けされていないため最小スコープでアサーションしている理由を示すコメントを追加してください。
🪄 Autofix

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: 0ddf43c7-d909-42b1-869e-c64e40294d39

📥 Commits

Reviewing files that changed from the base of the PR and between 676a964 and a331c5c.

📒 Files selected for processing (52)
  • .agents/skills/gh-stack/SKILL.md
  • AGENTS.md
  • REPORT.md
  • apps/server/public/openapi.json
  • apps/server/src/application/services/directory-sync-service.ts
  • apps/server/src/components/media/legacy-media-grid-item.tsx
  • apps/server/src/components/media/v2-media-grid-item.tsx
  • apps/server/src/components/v2/v2-source-list.tsx
  • apps/server/src/hooks/use-media-source-events.ts
  • apps/server/src/infrastructure/api-clients/queries/index.ts
  • apps/server/src/infrastructure/api/routers/ai-router.ts
  • apps/server/src/infrastructure/api/routers/characters-router.ts
  • apps/server/src/infrastructure/api/routers/entity-media-counts.ts
  • apps/server/src/infrastructure/api/routers/ips-router.ts
  • apps/server/src/infrastructure/api/routers/jobs-router.ts
  • apps/server/src/infrastructure/api/routers/projects-router.ts
  • apps/server/src/infrastructure/api/routers/sources-router.ts
  • apps/server/src/routes/sources/$mediaSourceId/components/source-media-page.tsx
  • apps/server/src/routes/sources/$mediaSourceId/components/v2-source-media-page.tsx
  • apps/server/src/routes/v2/config.tsx
  • apps/server/src/routes/v2/jobs.tsx
  • apps/server/src/routes/v2/media-context.ts
  • apps/server/src/routes/v2/search.tsx
  • apps/server/src/routes/v2/sources/$mediaSourceId/$mediaId/index.tsx
  • apps/tauri/src/hooks/use-media-source-events.ts
  • apps/tauri/src/routes/v2.tsx
  • apps/tauri/src/routes/v2/$.tsx
  • packages/core/src/domain/characters/schemas.ts
  • packages/core/src/domain/contract/ai.contract.ts
  • packages/core/src/domain/contract/jobs.contract.ts
  • packages/core/src/domain/ips/schemas.ts
  • packages/core/src/domain/jobs/schemas.ts
  • packages/core/src/domain/projects/schemas.ts
  • packages/core/src/domain/sources/events.ts
  • packages/core/src/domain/sources/schemas.ts
  • packages/core/src/domain/tagging/schemas.ts
  • packages/ui/src/hooks/use-job-events.ts
  • packages/ui/src/hooks/use-media-source-events.ts
  • packages/ui/src/hooks/use-sources-events.ts
  • packages/ui/src/query-options/index.ts
  • packages/ui/src/query-options/jobs-query.ts
  • packages/ui/src/screens/search-screen.types.ts
  • packages/ui/src/screens/source-media-screen.types.ts
  • packages/ui/src/screens/v2-config-screen.tsx
  • packages/ui/src/screens/v2-config-state-screen.tsx
  • packages/ui/src/screens/v2-jobs-screen.tsx
  • packages/ui/src/screens/v2-manager/entity-panel.tsx
  • packages/ui/src/screens/v2-search-screen.tsx
  • packages/ui/src/screens/v2-source-media-screen.tsx
  • packages/ui/src/source-media-grid.tsx
  • packages/ui/src/source-media-page.tsx
  • packages/ui/src/v2/search-toolbar.tsx

Comment on lines +56 to +57
2. **Always use `--auto` with `gh stack submit`** to auto-generate PR titles. Without `--auto`, `submit` prompts for a title for each new PR.
3. **Always use `--json` with `gh stack view`.** Without `--json`, the command launches an interactive TUI that cannot be operated by agents. There is no other appropriate flag — always pass `--json`.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== candidate file =="
git ls-files '.*agents/skills/gh-stack/SKILL.md' || true

echo "== relevant lines =="
if [ -f .agents/skills/gh-stack/SKILL.md ]; then
  sed -n '45,75p' .agents/skills/gh-stack/SKILL.md | cat -n
fi

echo "== reference lines =="
if [ -f .agents/skills/gh-stack/SKILL.md ]; then
  sed -n '1,20p' .agents/skills/gh-stack/SKILL.md | cat -n
fi

echo "== fetch upstream v0.1.0 README relevant sections =="
python3 - <<'PY'
import urllib.request
url = "https://github.com/github/gh-stack/raw/refs/tags/v0.1.0/README.md"
try:
    with urllib.request.urlopen(url, timeout=20) as r:
        text = r.read().decode("utf-8")
except Exception as e:
    print("FETCH_ERROR", repr(e))
    raise SystemExit(0)
for i, line in enumerate(text.splitlines(), 1):
    lower = line.lower()
    if any(s in lower for s in ["stack submit", "gh stack submit", "stack view", "gh stack view", "editor", "full-screen editor", "pager", "json", "auto"]):
        start=max(1,i-3); end=min(len(text.splitlines()),i+6)
        print(f"\n--- around line {i} ---")
        for no in range(start,end+1):
            print(f"{no}: {text.splitlines()[no-1]}")
PY

Repository: hmjn023/solid-imager

Length of output: 7098


🌐 Web query:

github gh-stack v0.1.0 README gh stack submit --auto gh stack view --json pager

💡 Result:

The gh-stack GitHub CLI extension provides commands to manage stacked pull requests. Below are the details for the specific commands you requested: gh stack submit --auto The gh stack submit command pushes all branches in the current stack to the remote and creates or updates pull requests on GitHub [1][2]. In an interactive terminal, this command normally opens a full-screen editor [3][2]. By passing the --auto flag (or running in a non-interactive environment like CI), you skip the editor and use automatically generated pull request titles [3][1]. By default, new pull requests created with --auto are set to draft status, unless you also provide the --open flag [3][2]. gh stack view --json The gh stack view command displays the current stack, including branch ordering, pull request links, and the most recent commit information [3][1]. By default, the output is piped through a pager (respecting your GIT_PAGER or PAGER environment variables, or defaulting to less -R) [3][2]. Using the --json flag causes the command to output the stack data as JSON instead of the human-readable table, which is useful for programmatic parsing [3][4]. When using --json, the structured data is written to standard output, while auxiliary status messages are sent to standard error [4].

Citations:


gh stack submit の挙動説明を更新してください。

Line 58 は、--auto を省略すると対話モードでタイトルを要求すると書いていますが、submit はタイトルだけではなく、フルスクリーン editor でタイトル・本文・draft/ready 状態を編集します。--auto はこの editor をスキップするため、編集内容の説明を削るか、editor 開く挙動に言い換える必要があります。--auto の推奨は保ってください。

🤖 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 @.agents/skills/gh-stack/SKILL.md around lines 56 - 57, Update the gh stack
submit behavior description in the relevant numbered guidance to state that
omitting --auto opens the full-screen editor for the PR title, body, and
draft/ready status; retain the recommendation to always use --auto, explaining
that it skips this editor.


> **Note for agents:** In non-interactive environments, the prune prompt is not shown. Use `--prune` explicitly to delete local branches for merged PRs.

> **Note for agents:** `sync` also mirrors the stack on GitHub locally. If PRs were added to the stack on github.com, their branches are pulled down and appended to the local stack automatically. If the local and remote stacks have **diverged** (you changed the local stack while the remote stack changed differently), sync can only prompt to resolve it in an interactive terminal — in non-interactive environments it aborts the sync (nothing is pushed or updated) and exits successfully with `ℹ Sync aborted`. Resolve a divergence by unstacking and recreating the stack.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== locate skill files =="
fd -a 'SKILL\.md$' . || true

echo "== file existence and relevant lines =="
if [ -f ".agents/skills/gh-stack/SKILL.md" ]; then
  sed -n '305,335p' .agents/skills/gh-stack/SKILL.md | cat -n
else
  echo ".agents/skills/gh-stack/SKILL.md not found"
fi

echo "== search sync/aborted/unstack/view usages in repo =="
rg -n "Sync aborted|stack sync|stack unstack|stack view" . || true

echo "== upstream reference if available via gh-stack public tag/source? =="
python3 - <<'PY'
import urllib.request, urllib.error
url="https://raw.githubusercontent.com/github/gh-stack/refs/tags/v0.1.0/skills/gh-stack/SKILL.md"
try:
    txt=urllib.request.urlopen(url, timeout=15).read().decode("utf-8")
    for i,line in enumerate(txt.splitlines(),1):
        if "Sync aborted" in line or "exit 0" in line or "local stack" in line[:120].lower():
            print(f"{url}:{i}:{line[:300]}")
except Exception as e:
    print(f"UPSTREAM_FETCH_ERROR: {e}")
PY

Repository: hmjn023/solid-imager

Length of output: 2231


🌐 Web query:

github gh-stack skills/gh-stack/SKILL.md Sync aborted exit successfully v0.1.0

💡 Result:

The message "Sync aborted" with an exit status of 0 (successful execution) occurs when running the gh stack sync command in a non-interactive terminal environment [1][2][3]. This happens specifically when the local stack and the remote stack on GitHub have diverged—for example, when you have added or modified branches locally while different pull requests have been added to the same stack on GitHub [2][3][4]. Because the tool cannot automatically reconcile these conflicting changes without human input, it safely aborts the synchronization process to prevent accidental data loss or incorrect updates [1][2][3]. In this scenario: - No changes are pushed to GitHub [1][2][3]. - No existing pull requests are updated [1][2][3]. - The command exits with a code of 0 (success) because it performed a safe, intentional abort rather than failing due to an error [1][3]. To resolve this divergence, you should typically unstack the current local state and recreate the stack [2][3]. The official documentation suggests using gh stack unstack to remove the local stack tracking, and then using gh stack submit to recreate the stack on GitHub [1][5].

Citations:


gh stack sync の成功終了コードだけで自動化を進めない規則を追加してください。

非対話的な stack divergence では変更が適用されないまま exit 0 になります。ℹ Sync aborted を stderr で検出したら、現在の処理を停止してください。解決は gh stack unstack と再構成を使うため、継続するケースでは gh stack view --json で現在の stack 状態を検証してください。

🧰 Tools
🪛 LanguageTool

[uncategorized] ~322-~322: The official name of this software platform is spelled with a capital “H”.
Context: ...ally. If PRs were added to the stack on github.com, their branches are pulled down and...

(GITHUB)

🤖 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 @.agents/skills/gh-stack/SKILL.md at line 322, Update the gh stack sync
guidance to state that automation must not proceed based solely on a successful
exit code: in non-interactive divergence, detect “ℹ Sync aborted” on stderr and
stop processing. For flows that continue, require validating the current stack
state with gh stack view --json; direct resolution toward unstacking and
recreating the stack.

Comment on lines +6826 to +6831
"anyOf": [
{},
{
"not": {}
}
]

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

ジョブAPIの成功レスポンスに具体的なスキーマを出力してください。

Line 6826、Line 6868、Line 6910 の {} はすべてのJSON値に一致します。
このため、/jobs/list/jobs/get/jobs/retry200 レスポンスは無制約です。
packages/core/src/domain/contract/jobs.contract.tsjobListResponseSchemajobDtoSchema の契約をOpenAPIへ出力してください。
このままでは生成クライアントが itemstotal、ジョブDTOの型を取得できません。

Also applies to: 6868-6873, 6910-6915

🤖 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/public/openapi.json` around lines 6826 - 6831,
OpenAPI生成で、/jobs/list、/jobs/get、/jobs/retryの200レスポンスに具体的なスキーマを設定し、空のanyOf({} と
not
{})を除去してください。jobListResponseSchemaにはjobListResponseSchema、個別ジョブ応答にはjobDtoSchemaの契約を参照させ、生成クライアントがitems、total、ジョブDTOの型を取得できるようにします。

Comment on lines +168 to +182
publishSyncStatus(mediaSourceId, "syncing");
try {
const source = await sourceRepo.findById(mediaSourceId);
if (source?.type !== "local") {
logger.info(
{ mediaSourceId },
"Skipping sync for non-local or missing source",
);
publishSyncStatus(
mediaSourceId,
"idle",
source
? "Source type does not support directory sync"
: "Source not found",
);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

同一ソースの同期を排他してください。

Line 168 は状態を更新するだけで、実行中の同期を予約しません。並行した syncMediaSource 呼び出しは両方とも同期処理を開始できます。追加処理と削除処理が同じソースを同時に更新します。

ソース ID ごとの mutex または in-flight Promise を追加し、完了時に必ず解放してください。

🤖 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/application/services/directory-sync-service.ts` around lines
168 - 182, Make sync execution exclusive per source ID in syncMediaSource by
adding a per-mediaSourceId mutex or in-flight Promise guard before the
synchronization work begins. Skip or await duplicate calls according to the
existing flow, and ensure the guard is always released in a finally block,
including errors and early returns.

Comment on lines +267 to +271
publishSyncStatus(
mediaSourceId,
"error",
error instanceof Error ? error.message : "Directory sync failed",
);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

内部エラー文をクライアントへ公開しないでください。

バックエンドの生エラーを公開 DTO として送信しています。Error.message と保存済みの jobs.error には、ファイルパス、接続情報、実装詳細が含まれる場合があります。サーバーログには詳細を保持し、クライアントには固定の安全なメッセージまたは公開エラーコードだけを送信してください。

  • apps/server/src/application/services/directory-sync-service.ts#L267-L271: error.message の代わりに、同期失敗を示す固定の公開メッセージをイベントへ設定してください。
  • apps/server/src/infrastructure/api/routers/jobs-router.ts#L53-L65: job.error を Safe Job DTO の公開可能なエラーコードまたは固定メッセージへマッピングしてください。

As per coding guidelines, 「機密情報を含み得るAPIレスポンスは Safe DTO にマッピングし、公開可能な型を明示する。」

📍 Affects 2 files
  • apps/server/src/application/services/directory-sync-service.ts#L267-L271 (this comment)
  • apps/server/src/infrastructure/api/routers/jobs-router.ts#L53-L65
🤖 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/application/services/directory-sync-service.ts` around lines
267 - 271, Prevent internal error details from reaching clients: in
apps/server/src/application/services/directory-sync-service.ts:267-271, update
publishSyncStatus to use a fixed safe directory-sync failure message instead of
error.message, while retaining detailed errors in server logs. In
apps/server/src/infrastructure/api/routers/jobs-router.ts:53-65, map job.error
to an explicitly public error code or fixed safe message when constructing the
Safe Job DTO.

Source: Coding guidelines

Comment on lines +51 to +62
const neighbors = () => findV2MediaNeighbors(props.media.id);
const navigateToNeighbor = (direction: "next" | "previous") => {
const neighbor = neighbors()[direction];
if (!neighbor) return;
void navigate({
params: {
mediaId: neighbor.id,
mediaSourceId: neighbor.mediaSourceId,
},
to: "/v2/sources/$mediaSourceId/$mediaId",
});
};

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "File candidates:"
fd -a 'index\.tsx$' . | grep 'apps/server/src/routes/v2/sources/.*/index.tsx' || true

file="apps/server/src/routes/v2/sources/\$mediaSourceId/\$mediaId/index.tsx"
if [ -f "$file" ]; then
  echo
  echo "Outlined symbols in $file:"
  ast-grep outline "$file" || true
  echo
  echo "Relevant lines:"
  cat -n "$file" | sed -n '1,120p'
else
  echo "File not found: $file"
fi

echo
echo "Search for navigate options and returnToCollection usages:"
rg -n "returnToCollection|navigate\s*\(|replace:\s*true|mediaSourceId|mediaId" apps/server/src -S || true

Repository: hmjn023/solid-imager

Length of output: 50377


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "Storage keys around v2:media-return:"
rg -n 'v2:media-return|media-return|SESSION_STORAGE|sessionStorage.setItem|sessionStorage\.setItem' apps/server/src/routes apps/server/src/components apps/server/src -S \
  --glob '*.{ts,tsx,js,jsx}' \
  --glob '!**/node_modules/**' || true

echo
echo "Routes with return path storage:"
rg -n 'setItem\(["'\'']v2:media-return|setItem \(("|'"'"')v2:media-return' apps/server/src/routes apps/server/src/components apps/server/src -S \
  --glob '*.{ts,tsx,js,jsx}' || true

echo
echo "Relevant returnPath usages:"
rg -n 'media-return|returnPath' apps/server/src \
  --glob '*.{ts,tsx,js,jsx}' || true

Repository: hmjn023/solid-imager

Length of output: 5308


前後移動で一覧への復帰履歴を維持してください。

navigate は詳細画面ごとに history entry を追加するため、一覧 → A → B の後で戻ると一覧ではなく A を表示します。前後移動には replace: true を指定するか、v2:media-return の return path へ明示的に遷移してください。

修正例
 		void navigate({
 			params: {
 				mediaId: neighbor.id,
 				mediaSourceId: neighbor.mediaSourceId,
 			},
+			replace: true,
 			to: "/v2/sources/$mediaSourceId/$mediaId",
 		});
📝 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.

Suggested change
const neighbors = () => findV2MediaNeighbors(props.media.id);
const navigateToNeighbor = (direction: "next" | "previous") => {
const neighbor = neighbors()[direction];
if (!neighbor) return;
void navigate({
params: {
mediaId: neighbor.id,
mediaSourceId: neighbor.mediaSourceId,
},
to: "/v2/sources/$mediaSourceId/$mediaId",
});
};
const neighbors = () => findV2MediaNeighbors(props.media.id);
const navigateToNeighbor = (direction: "next" | "previous") => {
const neighbor = neighbors()[direction];
if (!neighbor) return;
void navigate({
params: {
mediaId: neighbor.id,
mediaSourceId: neighbor.mediaSourceId,
},
replace: true,
to: "/v2/sources/$mediaSourceId/$mediaId",
});
};
🤖 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/routes/v2/sources/`$mediaSourceId/$mediaId/index.tsx around
lines 51 - 62, Update navigateToNeighbor so neighbor navigation replaces the
current detail-page history entry by enabling replace: true on navigate.
Preserve the existing neighbor lookup, route parameters, and destination.

Comment on lines +11 to +16
const segments = () =>
location()
.pathname.replace(/^\/v2\/?/, "")
.split("/")
.filter(Boolean)
.map((segment) => decodeURIComponent(segment));

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Locate and inspect the referenced catch-all route and nearby error boundary/NotFoundScreen handling.
if [ -f "apps/tauri/src/routes/v2/$.tsx" ]; then
  echo "== file =="
  wc -l "apps/tauri/src/routes/v2/$.tsx"
  echo
  echo "== route file =="
  cat -n "apps/tauri/src/routes/v2/$.tsx"
else
  echo "missing apps/tauri/src/routes/v2/$.tsx"
  fd -a '\$.tsx' apps/tauri/src/routes 2>/dev/null | sed 's#^\./##' | head -50 || true
fi

echo
echo "== search NotFoundScreen / ErrorBoundary / decodeURIComponent =="
rg -n "NotFoundScreen|ErrorBoundary|decodeURIComponent|segments" apps/tauri/src || true

Repository: hmjn023/solid-imager

Length of output: 3483


🌐 Web query:

MDN decodeURIComponent URIError invalid percent-encoding behavior

💡 Result:

The JavaScript function decodeURIComponent throws a URIError with the message "malformed URI sequence" when it encounters invalid percent-encoding in the input string [1][2]. Specifically, this error occurs in the following scenarios: 1. Invalid Escape Sequences: The function encounters a '%' character that is not followed by exactly two hexadecimal digits [1][3]. 2. Invalid UTF-8 Encoding: The escape sequences present do not encode a valid UTF-8 character [1][2]. Because decodeURIComponent interprets percent-encoded sequences as UTF-8 bytes, it expects sequences to adhere to the UTF-8 encoding standard [4]. If the bytes do not form a valid UTF-8 sequence, or if they represent incomplete characters (such as an orphan surrogate), the function throws a URIError [2][4]. To handle these cases, developers typically wrap the call in a try...catch block to manage the error gracefully [1][3]. Because this function is strictly designed to decode properly percent-encoded URI components (usually created by encodeURIComponent), it will fail on arbitrary strings containing standalone '%' characters or non-UTF-8 encoded percent sequences [5][6]. If you need to process strings that may contain literal '%' characters that are not part of a valid URI encoding, you must sanitize or encode them (e.g., replacing '%' with '%25') before passing the string to decodeURIComponent [5].

Citations:


不正な URL エンコードを安全に処理してください。

decodeURIComponent は不正な % エスケープや UTF-8 不正バイト列で URIError を送出します。このルートでは segments() のエラーが Switch の fallback までの間に発生し、NotFoundScreen を表示できません。復号を安全に処理し、復号失敗時は元の未復号値を返してください。

🤖 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/tauri/src/routes/v2/`$.tsx around lines 11 - 16, Update the segment
decoding in segments() to safely handle decodeURIComponent failures, including
malformed percent escapes and invalid UTF-8. Return the original segment
whenever decoding throws, so route resolution can continue and NotFoundScreen
remains reachable.

Comment on lines +10 to +18
export function useJobEvents(
openStream: JobEventStreamFactory,
onEvent: (event: JobEvent) => void,
enabled = true,
): void {
createEffect(() => {
if (isServer || !enabled) {
return;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# 既存のイベント購読フックの enabled 契約を確認する
fd -t f 'use-.*events\.ts$' | while IFS= read -r file; do
  echo "===== $file ====="
  rg -n -C 6 'enabled' "$file"
done

Repository: hmjn023/solid-imager

Length of output: 1842


🏁 Script executed:

#!/bin/bash
set -u

echo "===== locate use-job-events files ====="
git ls-files | rg 'packages/ui/src/hooks/use-job-events\.ts$|packages/ui/src/hooks/use-.*events\.ts$' || true

echo "===== inspect useJobEvents implementation ====="
cat -n packages/ui/src/hooks/use-job-events.ts

echo "===== references to useJobEvents ====="
rg -n -C 4 'useJobEvents|openSound' packages || true

echo "===== inspect package ui tsconfig/framework context ====="
for f in package.json packages/ui/package.json packages/ui/tsconfig.json packages/ui/vite.config.* packages/ui/tsconfig.*; do
  [ -e "$f" ] && { echo "===== $f ====="; sed -n '1,220p' "$f"; }
done

echo "===== search import context for createEffect signature if node_modules unavailable ====="
rg -n 'type Accessor|createEffect|Function' package.json packages/core packages/ui -g '*.{ts,tsx}' || true

echo "===== deterministic call-expression dependency analysis of event hook createEffect bodies ====="
python3 - <<'PY'
from pathlib import Path
import re

def create_effect_disabled_body_matches(path: Path) -> list[str]:
    text = path.read_text()
    if 'createEffect' not in text or 'enabled' not in text:
        return []
    out=[]
    for m in re.finditer(r'createEffect\s*\(\s*\(\)\s*=>\s*\{(.*?)\}\s*(?:\),)?\s*\n', text, re.S):
        body=m.group(1)
        if re.search(r'\byeserver\b|window\.document|navigator\.userAgent|window\.sessionStorage', body):
            continue
        out.append((m.start(), body))
    return out

for path in [Path('packages/ui/src/hooks/use-job-events.ts'), Path('packages/ui/src/hooks/use-batch-job-events.ts'), Path('apps/server/src/hooks/use-batch-job-events.ts')]:
    print(f"===== {path} =====")
    for pos, body in create_effect_disabled_body_matches(path):
        print('position', pos)
        print('\n'.join(f'{i+1}: {line}' for i, line in enumerate(body.splitlines())))
        print('calls enabled directly?', bool(re.search(r'\benabled\b(?!\s*\()', body)))
        print('calls enabled as function in guard?', bool(re.search(r'\benabled\s*\(', body)))

PY

Repository: hmjn023/solid-imager

Length of output: 13429


enabled をリアクティブな条件として扱ってください。

enabledAccessor<boolean> を想定し、createEffect 内で enabled() を返す形にしてください。use-job-events.ts では enable 引数の指定がないため実害はありませんが、このままだとシグナルを渡しても購読の開始と停止が切り替わりません。

use-media-source-events.ts と同じ型契約にすることで、他のイベント購読フックと一貫性が出ます。

🤖 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/hooks/use-job-events.ts` around lines 10 - 18, Update
useJobEvents so enabled is an Accessor<boolean> matching
use-media-source-events.ts, and evaluate enabled() inside its createEffect
guard. Preserve the existing server check and ensure the effect starts or stops
the event subscription reactively when the signal changes.

Comment on lines +56 to +63
const dateFormatter = new Intl.DateTimeFormat("ja-JP", {
dateStyle: "medium",
timeStyle: "short",
});

function formatDate(value: Date): string {
return dateFormatter.format(value);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# jobDtoSchema の日時フィールド定義を確認する
fd -t f 'schemas.ts$' -p 'domain/jobs' --exec cat -n
# UI 内の formatDate 実装の重複を確認する
rg -nP --type=tsx --type=ts -C 4 'function formatDate'

Repository: hmjn023/solid-imager

Length of output: 289


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "Repo files around ui and relevant package/dirs:"
git ls-files | sed -n '1,120p'
echo
echo "Find v2-jobs-screen:"
fd -a 'v2-jobs-screen\.tsx$' . || true
echo
echo "Find entity-panel:"
fd -a 'entity-panel\.tsx$' . || true
echo
echo "Find schema files with jobs:"
fd -t f 'schemas\.ts$|.*schema.*\.ts$' . | sed -n '1,200p'

Repository: hmjn023/solid-imager

Length of output: 10141


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "Package manifests / deps for zod/orpc:"
fd -t f '(^package\.json$|package-lock\.json$|pnpm-lock\.yaml$|yarn\.lock$|bun\.lockb$|bun\.lock$)' . -d 3 --max-count 80 --exec sh -c 'echo "--- $1"; rg -n "\"(zod|`@orpc`|orpc)\"|zod|`@orpc`|orpc" "$1" || true' sh {} \;

echo
echo "Jobs domain schema:"
cat -n packages/core/src/domain/jobs/schemas.ts | sed -n '1,220p'

echo
echo "Relevant UI files outline:"
ast-grep outline packages/ui/src/screens/v2-jobs-screen.tsx --view compact || true
ast-grep outline packages/ui/src/screens/v2-manager/entity-panel.tsx --view compact || true

echo
echo "formatDate occurrences:"
grep -RInE --exclude-dir=node_modules --exclude-dir=dist --exclude-dir=build 'function formatDate|const formatDate|formatDate\s*=' packages/ui/src app || true

echo
echo "v2-jobs-screen relevant section:"
cat -n packages/ui/src/screens/v2-jobs-screen.tsx | sed -n '1,140p'

echo
echo "entity-panel relevant section if formatDate appears:"
cat -n packages/ui/src/screens/v2-manager/entity-panel.tsx | sed -n '1,180p'

Repository: hmjn023/solid-imager

Length of output: 423


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "Package manifests / deps for zod/orpc:"
fd -t f '(^package\.json$|package-lock\.json$|pnpm-lock\.yaml$|yarn\.lock$|bun\.lockb$|bun\.lock$)' . -d 3 --max-results 80 --exec sh -c 'echo "--- $1"; rg -n "\"(zod|`@orpc`|orpc)\"|zod|`@orpc`|orpc" "$1" || true' sh {} \;

echo
echo "Jobs domain schema:"
cat -n packages/core/src/domain/jobs/schemas.ts | sed -n '1,220p'

echo
echo "Relevant UI files outline:"
ast-grep outline packages/ui/src/screens/v2-jobs-screen.tsx --view compact || true
ast-grep outline packages/ui/src/screens/v2-manager/entity-panel.tsx --view compact || true

echo
echo "formatDate occurrences:"
grep -RInE --exclude-dir=node_modules --exclude-dir=dist --exclude-dir=build 'function formatDate|const formatDate|formatDate\s*=' packages/ui/src app || true

echo
echo "v2-jobs-screen relevant section:"
cat -n packages/ui/src/screens/v2-jobs-screen.tsx | sed -n '1,180p'

echo
echo "entity-panel relevant section:"
cat -n packages/ui/src/screens/v2-manager/entity-panel.tsx | sed -n '1,220p'

Repository: hmjn023/solid-imager

Length of output: 449


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "Package manifests / deps for zod/orpc:"
while IFS= read -r -d '' file; do
  echo "--- $file"
  rg -n "\"(zod|`@orpc`|orpc)\"|zod|`@orpc`|orpc" "$file" || true
done < <(fd -t f '(^package\.json$|package-lock\.json$|pnpm-lock\.yaml$|yarn\.lock$|bun\.lockb$|bun\.lock$)' . -d 3 -x 'true' 2>/dev/null | head -n 80 | tr '\n' '\0')

echo
echo "Jobs domain schema:"
cat -n packages/core/src/domain/jobs/schemas.ts | sed -n '1,240p'

echo
echo "Relevant UI files outline:"
ast-grep outline packages/ui/src/screens/v2-jobs-screen.tsx --view compact || true
ast-grep outline packages/ui/src/screens/v2-manager/entity-panel.tsx --view compact || true

echo
echo "formatDate occurrences:"
grep -RInE --exclude-dir=node_modules --exclude-dir=dist --exclude-dir=build 'function formatDate|const formatDate|formatDate\s*=' packages/ui/src app || true

echo
echo "v2-jobs-screen relevant section:"
cat -n packages/ui/src/screens/v2-jobs-screen.tsx | sed -n '1,200p'

echo
echo "entity-panel relevant section:"
cat -n packages/ui/src/screens/v2-manager/entity-panel.tsx | sed -n '1,240p'

Repository: hmjn023/solid-imager

Length of output: 249


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "Package manifests / deps for zod/orpc:"
for file in $(fd -t f '(^package\.json$|package-lock\.json$|pnpm-lock\.yaml$|yarn\.lock$|bun\.lockb$|bun\.lock$)' . -d 3 | head -n 80); do
  echo "--- $file"
  rg -n "\"(zod|`@orpc`|orpc)\"|zod|`@orpc`|orpc" "$file" || true
done

echo
echo "Jobs domain schema:"
cat -n packages/core/src/domain/jobs/schemas.ts | sed -n '1,280p'

echo
echo "Relevant UI files outline:"
ast-grep outline packages/ui/src/screens/v2-jobs-screen.tsx --view compact || true
ast-grep outline packages/ui/src/screens/v2-manager/entity-panel.tsx --view compact || true

echo
echo "formatDate occurrences:"
grep -RInE --exclude-dir=node_modules --exclude-dir=dist --exclude-dir=build 'function formatDate|const formatDate|formatDate\s*=' packages/ui/src app || true

echo
echo "v2-jobs-screen relevant section:"
cat -n packages/ui/src/screens/v2-jobs-screen.tsx | sed -n '1,220p'

echo
echo "entity-panel relevant section:"
cat -n packages/ui/src/screens/v2-manager/entity-panel.tsx | sed -n '1,260p'

Repository: hmjn023/solid-imager

Length of output: 36514


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "packages/client contract/client setup:"
for file in packages/client/src/server-routes.ts packages/client/src/index.ts packages/client/src/client.ts packages/client/src/index.ts packages/client/src/*; do
  if [ -f "$file" ]; then
    echo "--- $file"
    rg -n "`@solid-imager/core`|zod|date|coerce|fromSchema|createORPC|Date|formatDate" "$file" || true
  fi
done

echo
echo "Search for formatDate in client/core:"
grep -RInE --exclude-dir=node_modules --exclude-dir=dist --exclude-dir=build 'formatDate|formatDate|Date' packages/client packages/core packages/ui/src | sed -n '1,220p'

echo
echo "packages/client relevant files containing jobDtoSchema:"
rg -n "jobDtoSchema|jobListResponseSchema|useManagerPage|`@solid-imager/core`" packages/client packages/ui/src | sed -n '1,240p'

echo
echo "packages/client source files:"
fd -t f . packages/client -d 3 | sed -n '1,200p'

Repository: hmjn023/solid-imager

Length of output: 23546


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "packages/client/src/create-client.ts:"
cat -n packages/client/src/create-client.ts | sed -n '1,260p'

echo
echo "packages/client/src/query-options/jobs-query.ts:"
cat -n packages/ui/src/query-options/jobs-query.ts | sed -n '1,220p'

echo
echo "packages/ui/src/screens/v2-jobs-screen.tsx usages:"
cat -n packages/ui/src/screens/v2-jobs-screen.tsx | sed -n '200,320p'

echo
echo "packages/ui/src/screens/v2-manager/utils.ts:"
cat -n packages/ui/src/screens/v2-manager/utils.ts | sed -n '1,90p'

echo
echo "packages/ui/src/v2/collection-inspector.tsx:"
cat -n packages/ui/src/v2/collection-inspector.tsx | sed -n '1,90p'

Repository: hmjn023/solid-imager

Length of output: 14294


🌐 Web query:

@orpc client zod date coerce schema deserialize from JSON Date object format

💡 Result:

In oRPC, handling the deserialization of date strings into Date objects is typically managed by either the RPC Serializer (for internal oRPC communication) or the Smart Coercion Plugin (for OpenAPI/external interfaces). 1. RPC Serializer (Internal oRPC) The oRPC RPCSerializer natively handles Date objects by serializing them to ISO strings and deserializing them back into Date instances automatically [1]. When using the default serializer, you do not need to manually configure Zod schemas to handle this transformation, as the serializer maintains type fidelity across the network boundary [1]. 2. Smart Coercion (OpenAPI/External) When working with external interfaces (like OpenAPI) where date-time values arrive as JSON strings, you can use the ZodSmartCoercionPlugin [2][3]. This plugin automates the conversion of valid ISO date strings into Date objects without requiring you to manually modify your Zod schemas with z.transform or z.coerce [2]. It is designed to work with Zod and supports standard ISO date-time formats (e.g., 2024-11-27T00:00:00.000Z) [2][3]. 3. Standard Zod Approach If you are not using oRPC's specific plugins and need to handle standard JSON input: - Use z.coerce.date in your Zod schema to automatically convert input strings to Date objects using the Date constructor [4][5]. - For more robust handling, especially when ensuring the input is a valid ISO string before converting, you can chain a validation step: z.string.pipe(z.coerce.date) [6]. Summary Recommendation - If using standard oRPC Client-to-Server communication: Rely on the built-in RPC Serializer [1]. - If processing external/OpenAPI JSON input: Use the Zod Smart Coercion Plugin for seamless, automatic conversion [2][3]. - If working with vanilla Zod/other frameworks: Use z.coerce.date [4].

Citations:


formatDateDate を前提としており、共有化も検討してください。

jobDtoSchema.createdAtupdatedAtz.coerce.date()Date にパースされ、oRPC シリアライザも日時を Date で渡す前提です。既存の packages/ui/src/screens/v2-manager/utils.ts::formatDateDate | string 対応で Date を扱えるため、v2-jobs-screen.tsx::formatDate は同じヘルパーに统一できます。

🤖 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-jobs-screen.tsx` around lines 56 - 63, Replace the
local formatDate helper in the jobs screen with the shared formatDate from
v2-manager/utils, reusing its Date | string handling and removing the duplicate
dateFormatter definition.

Comment on lines +143 to +200
function JobsTable(props: {
jobs: JobDto[];
onSelect: (job: JobDto) => void;
selectedJobId: string | null;
}) {
return (
<div class="overflow-hidden rounded-md border border-[var(--v2-border)] bg-[var(--v2-surface)]">
<div class="overflow-x-auto">
<table class="w-full min-w-[48rem] text-left text-sm">
<thead class="border-[var(--v2-border)] border-b bg-[var(--v2-surface-muted)] text-xs uppercase tracking-wide">
<tr>
<th class="px-4 py-3 font-medium">Type</th>
<th class="px-4 py-3 font-medium">Status</th>
<th class="px-4 py-3 font-medium">Progress</th>
<th class="px-4 py-3 font-medium">Updated</th>
</tr>
</thead>
<tbody class="divide-y divide-[var(--v2-border)]">
<For each={props.jobs}>
{(job) => (
<tr
class={
props.selectedJobId === job.id
? "bg-[var(--v2-primary-soft)]"
: ""
}
data-selected={props.selectedJobId === job.id}
>
<td class="px-2 py-2">
<button
class="w-full rounded px-2 py-2 text-left font-medium text-[var(--v2-text)] hover:bg-[var(--v2-surface-muted)] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--v2-primary)]"
onClick={() => props.onSelect(job)}
type="button"
>
<div>{jobTypeLabel(job.type)}</div>
<div class="mt-0.5 font-normal text-[var(--v2-text-muted)] text-xs">
{job.id.slice(0, 8)}
</div>
</button>
</td>
<td class="px-4 py-3">
<JobStatusBadge status={job.status} />
</td>
<td class="px-4 py-3">
<JobProgress progress={job.progress} />
</td>
<td class="px-4 py-3 whitespace-nowrap text-[var(--v2-text-secondary)]">
{formatDate(job.updatedAt)}
</td>
</tr>
)}
</For>
</tbody>
</table>
</div>
</div>
);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

テーブルのアクセシビリティ属性を追加してください。

同じリポジトリの packages/ui/src/screens/v2-manager/entity-panel.tsxcaptionscope="col"、行ボタンの aria-pressed を持ちます。このテーブルには3つとも不足しています。スクリーンリーダーの利用者は、列見出しの関連付けと選択状態を得られません。

♻️ 提案する変更
 				<table class="w-full min-w-[48rem] text-left text-sm">
+					<caption class="sr-only">
+						ジョブの一覧。{props.jobs.length}件。
+					</caption>
 					<thead class="border-[var(--v2-border)] border-b bg-[var(--v2-surface-muted)] text-xs uppercase tracking-wide">
 						<tr>
-							<th class="px-4 py-3 font-medium">Type</th>
-							<th class="px-4 py-3 font-medium">Status</th>
-							<th class="px-4 py-3 font-medium">Progress</th>
-							<th class="px-4 py-3 font-medium">Updated</th>
+							<th class="px-4 py-3 font-medium" scope="col">Type</th>
+							<th class="px-4 py-3 font-medium" scope="col">Status</th>
+							<th class="px-4 py-3 font-medium" scope="col">Progress</th>
+							<th class="px-4 py-3 font-medium" scope="col">Updated</th>
 						</tr>
 					</thead>
 									<td class="px-2 py-2">
 										<button
+											aria-pressed={props.selectedJobId === job.id}
 											class="w-full rounded px-2 py-2 text-left font-medium text-[var(--v2-text)] hover:bg-[var(--v2-surface-muted)] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--v2-primary)]"
 											onClick={() => props.onSelect(job)}
 											type="button"
 										>
📝 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.

Suggested change
function JobsTable(props: {
jobs: JobDto[];
onSelect: (job: JobDto) => void;
selectedJobId: string | null;
}) {
return (
<div class="overflow-hidden rounded-md border border-[var(--v2-border)] bg-[var(--v2-surface)]">
<div class="overflow-x-auto">
<table class="w-full min-w-[48rem] text-left text-sm">
<thead class="border-[var(--v2-border)] border-b bg-[var(--v2-surface-muted)] text-xs uppercase tracking-wide">
<tr>
<th class="px-4 py-3 font-medium">Type</th>
<th class="px-4 py-3 font-medium">Status</th>
<th class="px-4 py-3 font-medium">Progress</th>
<th class="px-4 py-3 font-medium">Updated</th>
</tr>
</thead>
<tbody class="divide-y divide-[var(--v2-border)]">
<For each={props.jobs}>
{(job) => (
<tr
class={
props.selectedJobId === job.id
? "bg-[var(--v2-primary-soft)]"
: ""
}
data-selected={props.selectedJobId === job.id}
>
<td class="px-2 py-2">
<button
class="w-full rounded px-2 py-2 text-left font-medium text-[var(--v2-text)] hover:bg-[var(--v2-surface-muted)] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--v2-primary)]"
onClick={() => props.onSelect(job)}
type="button"
>
<div>{jobTypeLabel(job.type)}</div>
<div class="mt-0.5 font-normal text-[var(--v2-text-muted)] text-xs">
{job.id.slice(0, 8)}
</div>
</button>
</td>
<td class="px-4 py-3">
<JobStatusBadge status={job.status} />
</td>
<td class="px-4 py-3">
<JobProgress progress={job.progress} />
</td>
<td class="px-4 py-3 whitespace-nowrap text-[var(--v2-text-secondary)]">
{formatDate(job.updatedAt)}
</td>
</tr>
)}
</For>
</tbody>
</table>
</div>
</div>
);
}
function JobsTable(props: {
jobs: JobDto[];
onSelect: (job: JobDto) => void;
selectedJobId: string | null;
}) {
return (
<div class="overflow-hidden rounded-md border border-[var(--v2-border)] bg-[var(--v2-surface)]">
<div class="overflow-x-auto">
<table class="w-full min-w-[48rem] text-left text-sm">
<caption class="sr-only">
ジョブの一覧。{props.jobs.length}件。
</caption>
<thead class="border-[var(--v2-border)] border-b bg-[var(--v2-surface-muted)] text-xs uppercase tracking-wide">
<tr>
<th class="px-4 py-3 font-medium" scope="col">Type</th>
<th class="px-4 py-3 font-medium" scope="col">Status</th>
<th class="px-4 py-3 font-medium" scope="col">Progress</th>
<th class="px-4 py-3 font-medium" scope="col">Updated</th>
</tr>
</thead>
<tbody class="divide-y divide-[var(--v2-border)]">
<For each={props.jobs}>
{(job) => (
<tr
class={
props.selectedJobId === job.id
? "bg-[var(--v2-primary-soft)]"
: ""
}
data-selected={props.selectedJobId === job.id}
>
<td class="px-2 py-2">
<button
aria-pressed={props.selectedJobId === job.id}
class="w-full rounded px-2 py-2 text-left font-medium text-[var(--v2-text)] hover:bg-[var(--v2-surface-muted)] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--v2-primary)]"
onClick={() => props.onSelect(job)}
type="button"
>
<div>{jobTypeLabel(job.type)}</div>
<div class="mt-0.5 font-normal text-[var(--v2-text-muted)] text-xs">
{job.id.slice(0, 8)}
</div>
</button>
</td>
<td class="px-4 py-3">
<JobStatusBadge status={job.status} />
</td>
<td class="px-4 py-3">
<JobProgress progress={job.progress} />
</td>
<td class="px-4 py-3 whitespace-nowrap text-[var(--v2-text-secondary)]">
{formatDate(job.updatedAt)}
</td>
</tr>
)}
</For>
</tbody>
</table>
</div>
</div>
);
}
🤖 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-jobs-screen.tsx` around lines 143 - 200, Add the
missing accessibility attributes in JobsTable: provide a descriptive table
caption, add scope="col" to each column header, and set the row-selection
button’s aria-pressed based on whether job.id matches selectedJobId.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant