Skip to content

feat: replace refresh DB flag polling with durably job trigger#208

Merged
coji merged 8 commits intomainfrom
feat/durably-refresh-scheduling
Mar 17, 2026
Merged

feat: replace refresh DB flag polling with durably job trigger#208
coji merged 8 commits intomainfrom
feat/durably-refresh-scheduling

Conversation

@coji
Copy link
Owner

@coji coji commented Mar 17, 2026

Summary

  • Replace the refreshRequestedAt DB flag + hourly polling flow with a direct durably crawl job trigger from the UI
  • User clicks "Refresh" → crawl job starts immediately with real-time progress tracking (no more waiting up to 1 hour)
  • Extract shared RunStatusAlerts component to deduplicate progress/status UI between Refresh and Recalculate sections
  • Drop refresh_requested_at column from organization_settings (tenant migration)

Closes #206 (refresh scheduling candidate)

Test plan

  • Click "Refresh" on Data Management page → crawl job starts immediately, progress bar shows
  • Verify button stays disabled during the run (no duplicate triggers)
  • Verify hourly scheduler still triggers incremental crawls (refresh: false)
  • Run pnpm db:setup to verify migration applies cleanly
  • pnpm validate passes

🤖 Generated with Claude Code

Summary by CodeRabbit

  • 新機能

    • 実行ステータスアラートを導入し、実行中・完了・失敗・出力を一貫表示
  • 改善

    • フルリフレッシュが即時トリガーへ変更され、UIラベルを「Refresh/Running」に簡素化
    • 進捗・エラー表示を統合して各セクションで共通のステータスUIを利用
    • ページのロード時データ取得を削除し公開インターフェースを簡素化
  • Chores

    • スケジューリング用DB列を削除するマイグレーション追加、スキーマ/マイグレーション構成を再編成

coji and others added 2 commits March 17, 2026 21:23
Instead of storing refreshRequestedAt in the DB and waiting for the hourly
scheduler to pick it up, the refresh button now triggers a durably crawl
job immediately with real-time progress tracking in the UI.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Extract shared RunStatusAlerts component to deduplicate progress/status
  UI between RefreshSection and RecalculateSection
- Remove empty loader (no data needed, avoids unnecessary revalidation)
- Remove unreachable error alert from RefreshSection
- Tighter button disabling: disable when runId exists to prevent gap
  between submit completion and useRun activation

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@coderabbitai
Copy link

coderabbitai bot commented Mar 17, 2026

Caution

Review failed

The pull request is closed.

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 367e8426-ec1e-4345-95c4-d4daa3bd9f56

📥 Commits

Reviewing files that changed from the base of the PR and between 717cb54 and 05a8315.

📒 Files selected for processing (1)
  • app/routes/$orgSlug/settings/data-management/index.tsx

📝 Walkthrough

Walkthrough

DBフラグ refresh_requested_at を削除し、ルートの loader を廃止。UI は action で Durably ジョブを起動して runId を返し、useRun/RunStatusAlerts によって進捗・完了・失敗を表示する構成に変更された。

変更内容

Cohort / File(s) Summary
Data Management ページ
app/routes/$orgSlug/settings/data-management/index.tsx
loader を削除。action('refresh') が Durably ジョブを起動して runId を返すよう変更。RunStatusAlerts を追加し、RefreshSection/RecalculateSection の進捗表示を統一。コンポーネント署名を更新。
ジョブサービス(crawl)
app/services/jobs/crawl.server.ts
refreshRequestedAt の読み取り・消費ロジックと関連 import を削除。crawl の残りロジックは維持。
型定義
app/services/tenant-type.ts
OrganizationSettings から refreshRequestedAt フィールドを削除。
バッチ DB クエリ
batch/db/queries.ts
getTenantData の選択から refreshRequestedAt を除去(返り値スキーマ変更)。
ジョブスケジューラー
batch/job-scheduler.ts
refresh フラグ計算を廃止。durably.jobs.crawl.trigger に常に refresh: false を渡すよう変更。
DB マイグレーション / スキーマ
db/migrations/tenant/20260317120000_drop_refresh_requested_at.sql, db/tenant.sql
organization_settings.refresh_requested_at カラムを削除するマイグレーション追加とスキーマ更新。
ドキュメント / スキーマ整理
CLAUDE.md, db/schema.sql
スキーマファイルの再配置と古い db/schema.sql の削除、ドキュメント更新(shared/tenant 分割)。

Sequence Diagram(s)

sequenceDiagram
    participant User as User (ブラウザ)
    participant UI as DataManagementPage UI
    participant Server as Remix Route Action
    participant Durably as Durably Service
    participant DB as Tenant DB

    User->>UI: 「Refresh」クリック
    UI->>Server: POST action (trigger crawl)
    Server->>Durably: enqueue crawl job
    Durably-->>Server: returns runId
    Server-->>UI: 200 { runId }
    UI->>Durably: useRun(runId) / subscribe
    Durably-->>UI: run status updates (running -> completed/failed)
    Note over DB,Server: `refresh_requested_at` は DB に保存されない(列削除)
Loading

推定コード審査時間

🎯 4 (Complex) | ⏱️ ~45 分

Possibly related PRs

🐇 ボタンを押せば走るよ、ジョブぴょんと
runId ひとつでみんな集まる
進捗はぴょんぴょん届くよ
成功も失敗も一緒に見るんだ
にんじん持って更新祝い 🥕✨

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes the main change: replacing DB flag polling with direct Durably job triggering for refresh functionality.
Linked Issues check ✅ Passed The PR fully implements the primary objective from issue #206: replacing refreshRequestedAt flag polling with a durable scheduled refresh flow, with direct job triggering, UI progress tracking, and duplicate execution protection.
Out of Scope Changes check ✅ Passed All changes are aligned with the stated objectives: removing refreshRequestedAt infrastructure, adding RunStatusAlerts component, updating job-scheduler logic, and reorganizing schema/migration files as documented.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/durably-refresh-scheduling
📝 Coding Plan
  • Generate coding plan for human review comments

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 and usage tips.

coji and others added 2 commits March 17, 2026 21:39
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
runId persists in fetcher.data after completion, so isBusy was always
true. Revert to using isRunning for the disabled state.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@app/routes/`$orgSlug/settings/data-management/index.tsx:
- Around line 35-43: The call to serverDurably.jobs.crawl.trigger in the refresh
handler is uncaught so failures bubble up as a 500 and the UI has no way to show
the error; wrap the trigger call in a try/catch, catch errors from
serverDurably.jobs.crawl.trigger and return a structured response (e.g. return
data({ intent: 'refresh', ok: false, error: err.message || String(err), runId:
null })), replacing the existing return data({ intent: 'refresh' as const, ok:
true, runId: run.id }) on success; also update the client-side fetcher handling
for the same flow (the other occurrence around serverDurably.jobs.crawl.trigger
at the 194-202 region) to detect ok:false and render a destructive alert with
the returned error message.
- Around line 157-189: The busy check wrongly includes runId != null which stays
set after a completed/failed run and keeps the Refresh button disabled; update
the isBusy calculation in the component to only consider submission or an active
non-terminal run (use isSubmitting || isRunning) and remove the runId != null
clause so that completed/failed runs no longer block re-submission; adjust any
related logic that relied on runId (e.g., where you pass runId into
durably.crawl.useRun) if you need to show previous run details, but do not use
runId alone to gate button enabled state.

In `@db/migrations/tenant/20260317120000_drop_refresh_requested_at.sql`:
- Around line 1-3: The migration drops the column
organization_settings.refresh_requested_at but db/schema.sql still contains that
column (see organization_settings.refresh_requested_at in db/schema.sql), so
update db/schema.sql to remove the refresh_requested_at column definition to
keep schema and migrations in sync; after editing db/schema.sql, apply the
migration and refresh the canonical schema by running the repository DB tasks
(e.g., generate/apply per project workflow: run pnpm db:migrate if you need to
generate migrations or pnpm db:apply then pnpm db:setup to ensure db/schema.sql
and the live DB are consistent).

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: e4293d26-b68d-4fe5-8e9e-f2dad4ae6bb2

📥 Commits

Reviewing files that changed from the base of the PR and between eb77957 and 64d2dc2.

⛔ Files ignored due to path filters (1)
  • db/migrations/tenant/atlas.sum is excluded by !**/*.sum
📒 Files selected for processing (7)
  • app/routes/$orgSlug/settings/data-management/index.tsx
  • app/services/jobs/crawl.server.ts
  • app/services/tenant-type.ts
  • batch/db/queries.ts
  • batch/job-scheduler.ts
  • db/migrations/tenant/20260317120000_drop_refresh_requested_at.sql
  • db/tenant.sql
💤 Files with no reviewable changes (4)
  • app/services/tenant-type.ts
  • db/tenant.sql
  • app/services/jobs/crawl.server.ts
  • batch/db/queries.ts

Comment on lines +1 to +3
-- Drop column "refresh_requested_at" from table: "organization_settings"
-- Refresh scheduling is now handled directly via durably jobs instead of DB flag polling
ALTER TABLE `organization_settings` DROP COLUMN `refresh_requested_at`;
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

この migration だけだと db/schema.sql と実DBがずれます。

関連する db/schema.sql:103-118 にはまだ organization_settings.refresh_requested_at が残っています。このままだと pnpm db:setup や次回の migration 生成が古い定義を基準にしてしまうので、対応する schema 定義も同じ PR で更新してください。

As per coding guidelines, "Store database schema changes in db/schema.sql and generate migrations via pnpm db:migrate, then apply with pnpm db:apply".

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@db/migrations/tenant/20260317120000_drop_refresh_requested_at.sql` around
lines 1 - 3, The migration drops the column
organization_settings.refresh_requested_at but db/schema.sql still contains that
column (see organization_settings.refresh_requested_at in db/schema.sql), so
update db/schema.sql to remove the refresh_requested_at column definition to
keep schema and migrations in sync; after editing db/schema.sql, apply the
migration and refresh the canonical schema by running the repository DB tasks
(e.g., generate/apply per project workflow: run pnpm db:migrate if you need to
generate migrations or pnpm db:apply then pnpm db:setup to ensure db/schema.sql
and the live DB are consistent).

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
app/routes/$orgSlug/settings/data-management/index.tsx (1)

63-75: ⚠️ Potential issue | 🟠 Major

Recalculate トリガーにも同様のエラーハンドリングが必要です。

バリデーションエラー(lines 53-61)は適切に処理されていますが、serverDurably.jobs.recalculate.trigger() の呼び出し自体が失敗した場合のハンドリングがありません。Refresh と同様に try/catch で囲む必要があります。

🛡️ エラーハンドリング追加の提案
     .with('recalculate', async () => {
       const selectedSteps = formData.getAll('steps').map(String)
       const steps = {
         upsert: selectedSteps.includes('upsert'),
         classify: selectedSteps.includes('classify'),
         export: selectedSteps.includes('export'),
       } satisfies JobSteps

       if (!steps.upsert && !steps.classify && !steps.export) {
         return data(
           {
             intent: 'recalculate' as const,
             error: 'At least one step must be selected',
           },
           { status: 400 },
         )
       }

-      const run = await serverDurably.jobs.recalculate.trigger(
-        { organizationId: org.id, steps },
-        {
-          concurrencyKey: `recalculate:${org.id}`,
-          labels: { organizationId: org.id },
-        },
-      )
-
-      return data({
-        intent: 'recalculate' as const,
-        ok: true,
-        runId: run.id,
-      })
+      try {
+        const run = await serverDurably.jobs.recalculate.trigger(
+          { organizationId: org.id, steps },
+          {
+            concurrencyKey: `recalculate:${org.id}`,
+            labels: { organizationId: org.id },
+          },
+        )
+
+        return data({
+          intent: 'recalculate' as const,
+          ok: true,
+          runId: run.id,
+        })
+      } catch {
+        return data(
+          {
+            intent: 'recalculate' as const,
+            ok: false,
+            error: 'Recalculate の開始に失敗しました。時間をおいて再試行してください。',
+          },
+          { status: 502 },
+        )
+      }
     })
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/routes/`$orgSlug/settings/data-management/index.tsx around lines 63 - 75,
The recalculate trigger call using serverDurably.jobs.recalculate.trigger(…)
lacks error handling; wrap the trigger invocation (where run is assigned) in a
try/catch mirroring the Refresh path, catching thrown errors and returning a
data response with intent: 'recalculate', ok: false and an error message (or
include error details) instead of assuming success, and ensure the catch also
logs or handles the error consistent with the existing refresh error handling.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Outside diff comments:
In `@app/routes/`$orgSlug/settings/data-management/index.tsx:
- Around line 63-75: The recalculate trigger call using
serverDurably.jobs.recalculate.trigger(…) lacks error handling; wrap the trigger
invocation (where run is assigned) in a try/catch mirroring the Refresh path,
catching thrown errors and returning a data response with intent: 'recalculate',
ok: false and an error message (or include error details) instead of assuming
success, and ensure the catch also logs or handles the error consistent with the
existing refresh error handling.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 9f5cba8c-3730-4504-b22b-19c11ba1cace

📥 Commits

Reviewing files that changed from the base of the PR and between 64d2dc2 and 4cf2b93.

📒 Files selected for processing (1)
  • app/routes/$orgSlug/settings/data-management/index.tsx

coji and others added 2 commits March 17, 2026 21:54
Wrap durably.jobs.crawl.trigger in try/catch so failures return a
structured error instead of a 500. Add error alert in RefreshSection UI.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
schema.sql was a pre-multi-tenant remnant not referenced by atlas.hcl.
Atlas uses shared.sql and tenant.sql as schema sources. Update CLAUDE.md
project structure and comments to reflect the actual dual-schema setup.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
app/routes/$orgSlug/settings/data-management/index.tsx (1)

69-81: ⚠️ Potential issue | 🟠 Major

recalculate トリガーにも try/catch が必要です。

refresh ハンドラは try/catch で保護されていますが、recalculate は保護されていません。trigger() が失敗すると 500 エラーとなり、ユーザーには失敗理由が表示されません。

🔧 修正案
-      const run = await serverDurably.jobs.recalculate.trigger(
-        { organizationId: org.id, steps },
-        {
-          concurrencyKey: `recalculate:${org.id}`,
-          labels: { organizationId: org.id },
-        },
-      )
-
-      return data({
-        intent: 'recalculate' as const,
-        ok: true,
-        runId: run.id,
-      })
+      try {
+        const run = await serverDurably.jobs.recalculate.trigger(
+          { organizationId: org.id, steps },
+          {
+            concurrencyKey: `recalculate:${org.id}`,
+            labels: { organizationId: org.id },
+          },
+        )
+        return data({
+          intent: 'recalculate' as const,
+          ok: true,
+          runId: run.id,
+        })
+      } catch {
+        return data(
+          { intent: 'recalculate' as const, error: 'Failed to start recalculation' },
+          { status: 500 },
+        )
+      }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/routes/`$orgSlug/settings/data-management/index.tsx around lines 69 - 81,
Wrap the call to serverDurably.jobs.recalculate.trigger in a try/catch: call
serverDurably.jobs.recalculate.trigger(...) inside a try block, assign the
result to run as before, and on success return the existing data payload with
intent/recalculate and runId; in the catch block log or capture the error
(including the thrown error details) and return a data response indicating ok:
false and an error message (or include the error in the response) so failures
from serverDurably.jobs.recalculate.trigger are handled gracefully instead of
causing a 500.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Outside diff comments:
In `@app/routes/`$orgSlug/settings/data-management/index.tsx:
- Around line 69-81: Wrap the call to serverDurably.jobs.recalculate.trigger in
a try/catch: call serverDurably.jobs.recalculate.trigger(...) inside a try
block, assign the result to run as before, and on success return the existing
data payload with intent/recalculate and runId; in the catch block log or
capture the error (including the thrown error details) and return a data
response indicating ok: false and an error message (or include the error in the
response) so failures from serverDurably.jobs.recalculate.trigger are handled
gracefully instead of causing a 500.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: a841833e-434e-4d77-a14d-debf87364b4d

📥 Commits

Reviewing files that changed from the base of the PR and between 4cf2b93 and 08944b5.

📒 Files selected for processing (1)
  • app/routes/$orgSlug/settings/data-management/index.tsx

coji and others added 2 commits March 18, 2026 08:24
Wrap durably.jobs.recalculate.trigger in try/catch to match the refresh
handler, so failures return a structured error instead of a 500.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add triggerError prop to RunStatusAlerts to eliminate duplicated
fetcher error alert blocks in RefreshSection and RecalculateSection.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

🧹 Nitpick comments (1)
app/routes/$orgSlug/settings/data-management/index.tsx (1)

28-33: intent / steps の入力は parseWithZod + zx に寄せるのを推奨します。

現在の formData.get* ベースだと型安全性が弱く、許可外値の混入を見落としやすいです。ルート方針に合わせて Zod スキーマで受ける形に揃えると保守しやすくなります。

As per coding guidelines app/routes/**/*.ts{,x}: Use Conform with Zod for type-safe form validation: parseWithZod from @conform-to/zod/v4and zx from@coji/zodix/v4``.

Also applies to: 51-57

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/routes/`$orgSlug/settings/data-management/index.tsx around lines 28 - 33,
Replace the manual formData.get usage in the exported action with a Zod-backed
parse flow: define a Zod schema (e.g., IntentSchema) describing allowed
intent/steps values, then call parseWithZod(request, IntentSchema) and use zx
(from `@coji/zodix/v4`) to extract typed values; update the code paths that
currently read the raw intent variable (the intent constant in action) to use
the validated, typed result instead so only allowed values proceed. Ensure you
reference parseWithZod and zx imports, validate both intent and steps per the
schema, and return/handle errors from parseWithZod consistently inside action.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@app/routes/`$orgSlug/settings/data-management/index.tsx:
- Around line 28-33: Replace the manual formData.get usage in the exported
action with a Zod-backed parse flow: define a Zod schema (e.g., IntentSchema)
describing allowed intent/steps values, then call parseWithZod(request,
IntentSchema) and use zx (from `@coji/zodix/v4`) to extract typed values; update
the code paths that currently read the raw intent variable (the intent constant
in action) to use the validated, typed result instead so only allowed values
proceed. Ensure you reference parseWithZod and zx imports, validate both intent
and steps per the schema, and return/handle errors from parseWithZod
consistently inside action.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 98ea7152-0357-4a00-9b02-a140b5ea1718

📥 Commits

Reviewing files that changed from the base of the PR and between 08944b5 and 717cb54.

📒 Files selected for processing (3)
  • CLAUDE.md
  • app/routes/$orgSlug/settings/data-management/index.tsx
  • db/schema.sql
💤 Files with no reviewable changes (1)
  • db/schema.sql

@coji coji merged commit 81e0941 into main Mar 17, 2026
6 checks passed
@coji coji deleted the feat/durably-refresh-scheduling branch March 17, 2026 23:33
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.

Track next durably adoption targets

1 participant