Skip to content

feat(api): expose sync status#1635

Merged
jjmata merged 12 commits intowe-promise:mainfrom
JSONbored:codex/feat-api-sync-status
May 6, 2026
Merged

feat(api): expose sync status#1635
jjmata merged 12 commits intowe-promise:mainfrom
JSONbored:codex/feat-api-sync-status

Conversation

@JSONbored
Copy link
Copy Markdown
Contributor

@JSONbored JSONbored commented May 1, 2026

Summary

  • Adds read-only API endpoints for family-scoped sync status history.
  • Keeps raw provider and queue error details out of API responses.

What changed

  • Adds GET /api/v1/syncs, GET /api/v1/syncs/latest, and GET /api/v1/syncs/{id}.
  • Scopes sync visibility to the authenticated user's family, accounts, and provider connections.
  • Adds sanitized Jbuilder responses, Minitest coverage, and docs-only rswag/OpenAPI updates.

Why

API clients and self-hosted operators need a stable way to poll sync status without scraping web UI state or exposing provider internals.

Validation

  • docker compose -f .devcontainer/docker-compose.yml exec -T app bash -lc 'cd /workspace && bundle exec rails test test/controllers/api/v1/syncs_controller_test.rb'
  • docker compose -f .devcontainer/docker-compose.yml exec -T app bash -lc 'cd /workspace && RAILS_ENV=test bundle exec rake rswag:specs:swaggerize'
  • docker compose -f .devcontainer/docker-compose.yml exec -T app bash -lc 'cd /workspace && bundle exec rubocop app/controllers/api/v1/syncs_controller.rb test/controllers/api/v1/syncs_controller_test.rb spec/requests/api/v1/syncs_spec.rb spec/swagger_helper.rb'
  • git diff --check

Notes

  • Draft only; not ready for maintainer review yet.

Summary by CodeRabbit

  • New Features

    • Added API endpoints to list sync history (paginated), fetch the latest sync (nullable when none), and retrieve a single sync record; responses include status, progress/terminal flags, related entity info, timestamps, and error metadata.
  • Bug Fixes / Behavior

    • Pagination params are sanitized and clamped; malformed or missing IDs return not-found; most-recent ordering made deterministic; provider error payloads are sanitized and exposed in a structured form; read-scope enforced.
  • Documentation

    • OpenAPI docs updated with new endpoints and schemas.
  • Tests

    • Added request and model tests covering auth, pagination, latest/show behavior, ordering, and error handling.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 1, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds paginated, family-scoped Syncs API endpoints (index, latest, show); moves UUID validation and pagination sanitizers to Api::V1::BaseController; introduces Sync.for_family and sync predicates/error payloads; adds Jbuilder views, OpenAPI schemas, routes, request and controller tests, and a model tie-breaker for ordered scope.

Changes

Syncs API Endpoint Implementation

Layer / File(s) Summary
Routing
config/routes.rb
Adds resources :syncs, only: [:index, :show], as: :sync_records and get :latest collection route under api/v1.
Data Shape / Model
app/models/sync.rb
Adds SYNCABLE_ASSOCIATIONS mapping; Sync.for_family(family, resource_owner: nil) to scope syncs to a family; updates ordered to include id DESC; adds in_progress?, terminal?, and api_error_payload.
Controller: surface & wiring
app/controllers/api/v1/syncs_controller.rb
Adds index, latest, show; enforces read scope via ensure_read_scope; paginates family_syncs_query.preload(:syncable, :children).ordered with safe_page_param/safe_per_page_param; latest returns data: null when absent; set_sync validates UUID via valid_uuid? and loads scoped record.
Controller: helpers
app/controllers/api/v1/base_controller.rb
Introduces private UUID_PATTERN and valid_uuid?(value) plus pagination sanitizers safe_page_param and safe_per_page_param.
Views / Serialization
app/views/api/v1/syncs/_sync.json.jbuilder, .../index.json.jbuilder, .../show.json.jbuilder
Adds _sync partial serializing sync attributes (id, status, in_progress, terminal, syncable summary, parent_id, children_count, window dates, lifecycle timestamps, sanitized error, created_at/updated_at); index renders data and pagination meta; show emits nullable data.
API Spec / Docs
docs/api/openapi.yaml, spec/swagger_helper.rb
Adds component schemas SyncableSummary, SyncErrorSummary, SyncResource, SyncResponse, SyncCollection; documents GET /api/v1/syncs, GET /api/v1/syncs/latest, GET /api/v1/syncs/{id} with auth and error responses.
Tests
spec/requests/api/v1/syncs_spec.rb, test/controllers/api/v1/syncs_controller_test.rb, test/models/sync_test.rb
Adds RSpec request specs covering the three endpoints for auth, scopes, and responses; Minitest integration tests for family/account scoping, latest/null behavior, error sanitization, malformed ID handling, and auth/scope enforcement; adds model test for deterministic tie-breaker ordering.

Sequence Diagram

sequenceDiagram
  participant Client
  participant API as "API Server\n(Api::V1::SyncsController)"
  participant Base as "Api::V1::BaseController\n(valid_uuid?/pagination helpers)"
  participant DB as "Database\n(Syncs + syncable records)"
  participant Views as "Jbuilder\n(index/show/_sync)"

  Client->>API: GET /api/v1/syncs (X-Api-Key)
  API->>API: authorize_scope!(:read)
  API->>Base: safe_page_param / safe_per_page_param
  API->>DB: Sync.for_family(...).ordered.preload(...).paginate
  DB-->>API: sync records
  API->>Views: render index.json.jbuilder (`@syncs`, `@pagy`)
  Views-->>Client: JSON collection + meta

  Client->>API: GET /api/v1/syncs/latest
  API->>API: authorize_scope!(:read)
  API->>DB: Sync.for_family(...).ordered.preload(...).first
  DB-->>API: single record or nil
  API->>Views: render show.json.jbuilder (data or null)
  Views-->>Client: JSON resource

  Client->>API: GET /api/v1/syncs/:id
  API->>Base: valid_uuid?(params[:id])
  API->>DB: family_scoped_query.preload(...).find(id)
  DB-->>API: record or not_found
  API->>Views: render show.json.jbuilder
  Views-->>Client: JSON resource
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Suggested reviewers

  • jjmata

Poem

🐰 I hopped through routes and tidy tests,
Paginated lists and safe UUID quests,
Latest can be null, errors kept small,
Scoped to family, I guard them all,
A carrot for clean JSON rests.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 10.00% 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 The title 'feat(api): expose sync status' directly describes the main change: adding new API endpoints to expose sync status information with proper scoping and sanitization.
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.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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.

@JSONbored JSONbored force-pushed the codex/feat-api-sync-status branch from ccb7ca5 to 9698e47 Compare May 2, 2026 00:52
@JSONbored JSONbored marked this pull request as ready for review May 2, 2026 06:08
@brin-security-scanner brin-security-scanner Bot added contributor:flagged Contributor flagged for review by trust analysis. pr:verified PR passed security analysis. and removed contributor:flagged Contributor flagged for review by trust analysis. labels May 2, 2026
@brin-security-scanner
Copy link
Copy Markdown

brin-security-scanner Bot commented May 2, 2026

⚠️ Contributor Trust Check — Review Recommended

This contributor's profile shows patterns that may warrant additional review. This is based on their GitHub activity, not the contents of this PR.

JSONbored · Score: 68/100

Dimension breakdown
Dimension Score What it measures
Identity 30 Account age, contribution history, GPG keys, org memberships
Behavior 80 PR patterns, unsolicited contribution ratio, activity cadence
Content 100 PR body substance, issue linkage, contribution quality
Graph 30 Cross-repo trust, co-contributor relationships

Analyzed by Brin · Full profile

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 9698e4720a

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread app/controllers/api/v1/syncs_controller.rb Outdated
Comment thread app/controllers/api/v1/syncs_controller.rb
Copy link
Copy Markdown
Contributor

@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

🧹 Nitpick comments (1)
test/controllers/api/v1/syncs_controller_test.rb (1)

110-113: ⚡ Quick win

Add unauthenticated coverage for show and latest too.

This file only checks 401 on index, so auth regressions on the new member/collection sync endpoints could slip through. Since these are new API actions, the controller test should exercise all three read paths.

♻️ Suggested additions
   test "requires authentication" do
     get api_v1_sync_records_url
     assert_response :unauthorized
   end
+
+  test "requires authentication for show" do
+    get api_v1_sync_record_url(SecureRandom.uuid)
+    assert_response :unauthorized
+  end
+
+  test "requires authentication for latest" do
+    get latest_api_v1_sync_records_url
+    assert_response :unauthorized
+  end
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test/controllers/api/v1/syncs_controller_test.rb` around lines 110 - 113,
Extend the existing "requires authentication" test to also exercise the member
and collection read endpoints by issuing unauthenticated GET requests to the
show and latest endpoints and asserting :unauthorized; specifically, after the
existing get api_v1_sync_records_url assertion, perform a GET to the member
route (api_v1_sync_record_url) using a valid record id (e.g., a fixture like
sync_records(:one) or a created SyncRecord) and assert_response :unauthorized,
and then perform a GET to the collection "latest" route
(latest_api_v1_sync_records_url) and assert_response :unauthorized so all three
read paths (index, show, latest) are covered.
🤖 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/controllers/api/v1/syncs_controller.rb`:
- Around line 37-40: The latest action sets `@sync` via
family_syncs_query.preload(:syncable, :children).ordered.first which can be nil;
before calling render :show add a nil check and return your API's empty-state
response (e.g., render nothing with status 404 or head :not_found) when `@sync` is
nil so the view isn't rendered with a missing `@sync`; update the latest method to
short-circuit on `@sync.nil`? and return the appropriate response.

In `@spec/requests/api/v1/syncs_spec.rb`:
- Around line 34-35: The test creates a shared Sync via let! which eagerly
persists a family sync and skews index/latest expectations; change the eager
helper to a lazy one by replacing the let! declaration for sync (the
Sync.create!(...) instance) with let(:sync) and ensure only the show example
(the spec that references id or inspects that single record) references sync so
other examples (index/latest) do not get the extra persisted record.

In `@spec/swagger_helper.rb`:
- Around line 656-677: The OpenAPI schemas using `{ type: :null }` inside the
oneOf unions are invalid; update the oneOf entries for the error property (in
the resource schema containing SyncErrorSummary) and the data property inside
SyncResponse to replace `{ type: :null }` with `{ type: :object, nullable: true
}`, ensuring the subschema explicitly allows nullability so the union permits
null values; keep the existing `{ '$ref' =>
'#/components/schemas/SyncErrorSummary' }` and `{ '$ref' =>
'#/components/schemas/SyncResource' }` entries unchanged.

---

Nitpick comments:
In `@test/controllers/api/v1/syncs_controller_test.rb`:
- Around line 110-113: Extend the existing "requires authentication" test to
also exercise the member and collection read endpoints by issuing
unauthenticated GET requests to the show and latest endpoints and asserting
:unauthorized; specifically, after the existing get api_v1_sync_records_url
assertion, perform a GET to the member route (api_v1_sync_record_url) using a
valid record id (e.g., a fixture like sync_records(:one) or a created
SyncRecord) and assert_response :unauthorized, and then perform a GET to the
collection "latest" route (latest_api_v1_sync_records_url) and assert_response
:unauthorized so all three read paths (index, show, latest) are covered.
🪄 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: defaults

Review profile: CHILL

Plan: Pro

Run ID: 8430a321-718c-4f9b-90d4-cf385e647af4

📥 Commits

Reviewing files that changed from the base of the PR and between 268f5ae and 9698e4720ad8e41da32e7427271ce9e9101ed311.

📒 Files selected for processing (9)
  • app/controllers/api/v1/syncs_controller.rb
  • app/views/api/v1/syncs/_sync.json.jbuilder
  • app/views/api/v1/syncs/index.json.jbuilder
  • app/views/api/v1/syncs/show.json.jbuilder
  • config/routes.rb
  • docs/api/openapi.yaml
  • spec/requests/api/v1/syncs_spec.rb
  • spec/swagger_helper.rb
  • test/controllers/api/v1/syncs_controller_test.rb

Comment thread app/controllers/api/v1/syncs_controller.rb
Comment thread spec/requests/api/v1/syncs_spec.rb Outdated
Comment thread spec/swagger_helper.rb Outdated
@brin-security-scanner brin-security-scanner Bot added the contributor:flagged Contributor flagged for review by trust analysis. label May 2, 2026
Copy link
Copy Markdown
Collaborator

@jjmata jjmata left a comment

Choose a reason for hiding this comment

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

Review: feat(api): expose sync status

Good foundation — scoping is sound, error sanitization is correct, the Minitest coverage is thorough, and the rswag/OpenAPI split follows the project conventions. A few things need attention before this is merge-ready.


Must fix

SYNCABLE_ASSOCIATIONS in the controller (see inline)
This hardcoded map violates the "fat model, skinny controller" convention and will need a manual update every time a new provider is added. The family-scoping logic belongs on the Sync model as a named scope (Sync.for_family(family)). It also has a latent NoMethodError risk if family.public_send(association_name) is called for an association that doesn't exist on Family in a given deployment mode (self-hosted vs managed).

Missing 403 in OpenAPI docs (see inline)
ensure_read_scope returns 403 when a valid key lacks the required scope. All three endpoints are missing this response code; other endpoints in the same API document it.

syncable&.try(:name) nil guard (see inline)
After preloading, a deleted syncable makes sync.syncable return nil, causing a NoMethodError in the partial.


Worth discussing

  • latest returning 200 + {"data": null} vs a 404 — either is defensible but the OpenAPI description should explicitly state the null-data behavior so consumers know to handle it.
  • in_progress / terminal as derived Jbuilder fields — these will silently mis-classify syncs if the state machine gains a new state. A terminal? predicate on the Sync model would be the authoritative source.
  • helper_method :sync_error_payload — works because Jbuilder evaluates in an ActionView context, but the implicit coupling is non-obvious. Worth a comment or restructuring as a plain private method called from a local variable in the partial.

Minor / nits

  • Sync.destroy_all in test setup — safe under transactional tests but brittle; scope to the family instead.
  • Three API keys + Redis cleanup created for every test; most tests only need one or two.
  • Route alias as: :sync_records creates confusing helper names (api_v1_sync_records_url) — at minimum add an inline comment explaining the collision.

Generated by Claude Code

Comment thread app/controllers/api/v1/syncs_controller.rb Outdated
Comment thread app/controllers/api/v1/syncs_controller.rb
Comment thread app/controllers/api/v1/syncs_controller.rb Outdated
Comment thread app/controllers/api/v1/syncs_controller.rb Outdated
Comment thread app/views/api/v1/syncs/show.json.jbuilder
Comment thread config/routes.rb Outdated
Comment thread docs/api/openapi.yaml
Comment thread test/controllers/api/v1/syncs_controller_test.rb
Comment thread test/controllers/api/v1/syncs_controller_test.rb Outdated
Comment thread spec/requests/api/v1/syncs_spec.rb
@JSONbored JSONbored force-pushed the codex/feat-api-sync-status branch from aadfecd to dfafaf9 Compare May 3, 2026 01:33
Copy link
Copy Markdown
Contributor

@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: 1

🤖 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/controllers/api/v1/syncs_controller.rb`:
- Around line 44-46: The method family_syncs_query currently uses
current_resource_owner.family which can raise when no resource owner is present
and bypasses the standardized auth context; update family_syncs_query to build
the scope using Current.family (and Current.user if needed) and call
Sync.for_family with Current.family while passing the authenticated user via
resource_owner: Current.user so the method uses the app's standardized auth
context (replace current_resource_owner.family and resource_owner:
current_resource_owner with Current.family and resource_owner: Current.user in
family_syncs_query and the Sync.for_family call).
🪄 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: defaults

Review profile: CHILL

Plan: Pro

Run ID: 6f9a7d6a-4649-41e1-8098-af8923bf6f8c

📥 Commits

Reviewing files that changed from the base of the PR and between aadfecd7d491a8709c412da3ed42bfbdfd591e10 and dfafaf9398660c09a851656ffe154de194db93a6.

📒 Files selected for processing (10)
  • app/controllers/api/v1/syncs_controller.rb
  • app/models/sync.rb
  • app/views/api/v1/syncs/_sync.json.jbuilder
  • app/views/api/v1/syncs/index.json.jbuilder
  • app/views/api/v1/syncs/show.json.jbuilder
  • config/routes.rb
  • docs/api/openapi.yaml
  • spec/requests/api/v1/syncs_spec.rb
  • spec/swagger_helper.rb
  • test/controllers/api/v1/syncs_controller_test.rb
✅ Files skipped from review due to trivial changes (3)
  • app/views/api/v1/syncs/index.json.jbuilder
  • spec/swagger_helper.rb
  • app/views/api/v1/syncs/show.json.jbuilder
🚧 Files skipped from review as they are similar to previous changes (5)
  • config/routes.rb
  • app/views/api/v1/syncs/_sync.json.jbuilder
  • docs/api/openapi.yaml
  • test/controllers/api/v1/syncs_controller_test.rb
  • spec/requests/api/v1/syncs_spec.rb

Comment thread app/controllers/api/v1/syncs_controller.rb Outdated
Copy link
Copy Markdown
Contributor

@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: 1

🤖 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/controllers/api/v1/syncs_controller.rb`:
- Around line 11-15: The current ordering is non-deterministic because
Sync.ordered only sorts by created_at DESC; update the ordering to include a
stable tiebreaker (id DESC) so pagination and .latest are deterministic. Fix by
changing the scope Sync.ordered in the Sync model to order(created_at: :desc,
id: :desc) (or by appending .order(id: :desc) where
family_syncs_query.preload(:syncable, :children).ordered is called) so both the
pagy call using family_syncs_query and any usages of Sync.ordered use created_at
DESC, id DESC as the 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: defaults

Review profile: CHILL

Plan: Pro

Run ID: eeb0a6bc-b6a0-4b2a-ab7f-717b1b4a1c53

📥 Commits

Reviewing files that changed from the base of the PR and between dfafaf9398660c09a851656ffe154de194db93a6 and 066507cf1db7ee02acee65b824207f6eb8802ee1.

📒 Files selected for processing (3)
  • app/controllers/api/v1/base_controller.rb
  • app/controllers/api/v1/syncs_controller.rb
  • test/controllers/api/v1/syncs_controller_test.rb
🚧 Files skipped from review as they are similar to previous changes (1)
  • test/controllers/api/v1/syncs_controller_test.rb

Comment thread app/controllers/api/v1/syncs_controller.rb
Copy link
Copy Markdown
Contributor

@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 (2)
app/controllers/api/v1/base_controller.rb (1)

224-233: ⚡ Quick win

safe_per_page_param silently resets to 25 for values > 100 instead of clamping.

A caller passing per_page=200 gets 25 items back — the default — rather than the enforced maximum of 100. This is surprising API behavior that can be hard to debug. Clamping out-of-range positive values to the maximum is the more conventional and transparent contract.

♻️ Proposed fix: clamp instead of reset for values > 100
 def safe_per_page_param
   per_page = params[:per_page].to_i

   case per_page
   when 1..100
     per_page
+  when 101..Float::INFINITY
+    100
   else
     25
   end
 end

Or more concisely:

 def safe_per_page_param
   per_page = params[:per_page].to_i
-
-  case per_page
-  when 1..100
-    per_page
-  else
-    25
-  end
+  return 25 unless per_page >= 1
+  [per_page, 100].min
 end
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/controllers/api/v1/base_controller.rb` around lines 224 - 233, The method
safe_per_page_param currently returns the default 25 for any value outside
1..100, so a caller passing per_page=200 gets 25 instead of the intended
maximum; update safe_per_page_param to clamp positive out-of-range values by
returning 100 when params[:per_page].to_i > 100, return params[:per_page].to_i
when in 1..100, and keep returning 25 for non-positive or missing values—change
the conditional logic in safe_per_page_param accordingly.
app/models/sync.rb (1)

112-120: ⚡ Quick win

Use I18n.t() for the API-facing message strings.

Both "Sync became stale before completion" and "Sync failed" are hard-coded English strings that flow through Jbuilder into API responses visible to clients. Per the coding guidelines, all user-facing strings in **/*.{erb,rb} must use the t() helper.

♻️ Proposed refactor

Add to config/locales/en.yml:

en:
  sync:
    errors:
      failed: "Sync failed"
      stale: "Sync became stale before completion"

Then update api_error_payload:

 def api_error_payload
   return unless failed? || stale?
   return if stale? && error.blank?

   {
     present: error.present?,
-    message: stale? ? "Sync became stale before completion" : "Sync failed"
+    message: stale? ? I18n.t("sync.errors.stale") : I18n.t("sync.errors.failed")
   }
 end

As per coding guidelines: "Always use t() helper for user-facing strings."

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

In `@app/models/sync.rb` around lines 112 - 120, The api_error_payload method
currently returns hard-coded English messages; change it to use the Rails I18n
helper (t or I18n.t) for API-facing strings: replace the literal "Sync became
stale before completion" and "Sync failed" with translations (e.g.
t('sync.errors.stale') and t('sync.errors.failed')) and add corresponding keys
to config/locales/en.yml; ensure the method still guards with failed?, stale?,
and error presence as before.
🤖 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/controllers/api/v1/base_controller.rb`:
- Around line 224-233: The method safe_per_page_param currently returns the
default 25 for any value outside 1..100, so a caller passing per_page=200 gets
25 instead of the intended maximum; update safe_per_page_param to clamp positive
out-of-range values by returning 100 when params[:per_page].to_i > 100, return
params[:per_page].to_i when in 1..100, and keep returning 25 for non-positive or
missing values—change the conditional logic in safe_per_page_param accordingly.

In `@app/models/sync.rb`:
- Around line 112-120: The api_error_payload method currently returns hard-coded
English messages; change it to use the Rails I18n helper (t or I18n.t) for
API-facing strings: replace the literal "Sync became stale before completion"
and "Sync failed" with translations (e.g. t('sync.errors.stale') and
t('sync.errors.failed')) and add corresponding keys to config/locales/en.yml;
ensure the method still guards with failed?, stale?, and error presence as
before.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 1c910c27-0743-4790-88aa-5e93fe73a675

📥 Commits

Reviewing files that changed from the base of the PR and between 066507cf1db7ee02acee65b824207f6eb8802ee1 and 65e4235883b9764323acf54fd4b586732b5a2359.

📒 Files selected for processing (5)
  • app/controllers/api/v1/base_controller.rb
  • app/controllers/api/v1/syncs_controller.rb
  • app/models/sync.rb
  • test/controllers/api/v1/syncs_controller_test.rb
  • test/models/sync_test.rb
✅ Files skipped from review due to trivial changes (1)
  • test/controllers/api/v1/syncs_controller_test.rb
🚧 Files skipped from review as they are similar to previous changes (1)
  • app/controllers/api/v1/syncs_controller.rb

@JSONbored JSONbored force-pushed the codex/feat-api-sync-status branch from 4faee07 to d0f85e0 Compare May 3, 2026 16:49
@JSONbored JSONbored force-pushed the codex/feat-api-sync-status branch from d0f85e0 to 82aca49 Compare May 4, 2026 00:16
@JSONbored
Copy link
Copy Markdown
Contributor Author

Thanks for the detailed review. I pushed a follow-up in 3fa7df3 that should address the outstanding items:

  • malformed UUIDs are guarded before find
  • sync scoping now lives on Sync.for_family, and the remaining hardcoded provider map was replaced with Family association reflection
  • the Jbuilder partial calls sync.api_error_payload directly instead of relying on helper_method
  • deleted/orphaned syncables render a nil-safe name
  • in_progress / terminal are derived through Sync#in_progress? and Sync#terminal?
  • /latest documents the intentional 200 + data: null shape
  • all three sync endpoints document 403
  • test setup now scopes sync cleanup through Sync.for_family(@family)
  • rswag fixture setup was verified against the required Family/User attributes

I also tightened the sync error payload so emitted error objects are always actionable (present: true with a sanitized message), while stale syncs without stored error text still render error: null.

Validation:

  • bin/rails test test/controllers/api/v1/syncs_controller_test.rb test/controllers/api/v1/sync_controller_test.rb test/models/sync_test.rb
  • RAILS_ENV=test bundle exec rake rswag:specs:swaggerize
  • scoped bin/rubocop
  • git diff --check

GitHub CI is green now as well.

@JSONbored JSONbored requested a review from jjmata May 4, 2026 04:43
@jjmata jjmata removed the contributor:flagged Contributor flagged for review by trust analysis. label May 4, 2026
Copy link
Copy Markdown
Collaborator

@jjmata jjmata left a comment

Choose a reason for hiding this comment

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

Can you fix merge conflicts before next review, @JSONbored?

@brin-security-scanner brin-security-scanner Bot added the contributor:flagged Contributor flagged for review by trust analysis. label May 5, 2026
@JSONbored JSONbored force-pushed the codex/feat-api-sync-status branch from eb1532d to 1188b2d Compare May 5, 2026 00:31
@JSONbored JSONbored requested a review from jjmata May 5, 2026 00:33
@JSONbored
Copy link
Copy Markdown
Contributor Author

Can you fix merge conflicts before next review, @JSONbored?

Fixed!

Copy link
Copy Markdown

@sure-design sure-design left a comment

Choose a reason for hiding this comment

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

Re-review — independent code read

Verified the diff end-to-end against the live base controller and model. Summary below.


✅ Properly addressed since the previous review

Issue How it landed
P1 scope leak Sync.for_family now lives in the model and threads resource_owner through account_syncable_ids; controller passes Current.user.
P2 UUID validation valid_uuid? is called before find in set_sync; the helper is defined in BaseController with a strict regex — confirmed.
Deterministic ordering scope :ordered, -> { order(created_at: :desc, id: :desc) } — tiebreaker present.
latest nil case Early-returns render json: { data: nil } before hitting the view.
Current.* usage family_syncs_query correctly uses Current.family / Current.user.
Fat-model convention for_family, in_progress?, terminal?, api_error_payload all live in Sync, not the controller.
Orphaned syncable guard syncable&.try(:name) in the partial handles nil syncable.
Test teardown scope Sync.for_family(@family).destroy_all — scoped, not global.
403 documented All three paths now carry the 403 response in the YAML.
Route alias as: :sync_job on the existing POST prevents the name collision.

🔴 Still broken — OAS 3.0 $ref + nullable: true sibling issue

The CodeRabbit thread about type: :null was marked resolved after replacing the oneOf approach, but the replacement is also non-conformant with OpenAPI 3.0.x.

In OAS 3.0, $ref replaces the entire schema object at that level — any sibling keys (including nullable: true) are silently dropped by compliant validators. This affects two properties right now:

SyncResource.error (docs/api/openapi.yaml and spec/swagger_helper.rb):

# current — nullable is ignored by OAS 3.0 parsers
error:
  "$ref": "#/components/schemas/SyncErrorSummary"
  nullable: true
# correct OAS 3.0
error:
  nullable: true
  allOf:
    - "$ref": "#/components/schemas/SyncErrorSummary"

SyncResponse.data — same pattern, same fix needed.

In spec/swagger_helper.rb the Ruby equivalent would be:

error: { nullable: true, allOf: [ { '$ref' => '#/components/schemas/SyncErrorSummary' } ] }
data:  { nullable: true, allOf: [ { '$ref' => '#/components/schemas/SyncResource'      } ] }

Swagger UI and rswag tend to be lenient here so it may render fine, but swagger-parser --validate will flag it and any strict client generator will mishandle the nullability.


🟡 Threads that are functionally resolved but not marked so

  • P2 UUID thread (r3176234818) — is_resolved: false, is_outdated: false, but the code already calls valid_uuid? before find. Please resolve.
  • helper_method thread (r3177232155) — is_resolved: false, is_outdated: false, but there is no helper_method anywhere in the current controller diff. This comment appears to reference a version of the code that was reworked before the diff settled. Please resolve.
  • family.public_send NoMethodError guard (r3177232276) — outdated; family_syncable_associations now lives in the Sync model and iterates over Family.reflect_on_all_associations, so every association name it yields is guaranteed to exist on Family. The rescue NameError inside the block handles any bad klass references. Safe as written.
  • Routes naming (r3177232939) — outdated; as: :sync_job resolves the collision.
  • 403 docs (r3177233088) — outdated; 403 is present on all three paths.

🟡 Minor — rswag spec key-creation pattern differs from Minitest

Minitest creates keys with display_key::

ApiKey.create!(display_key: "test_rw_#{SecureRandom.hex(8)}", source: "web")

The rswag spec uses key: and ApiKey.generate_secure_key:

key = ApiKey.generate_secure_key
ApiKey.create!(key: key, source: "web")

If ApiKey.find_by_value hashes the incoming header value and compares against the stored key column, the rswag pattern may be correct — but it differs from every other controller test in this project. If it's wrong, rswag:specs:swaggerize will silently produce 401 responses in the generated YAML instead of 200s for the success cases. Worth a quick sanity-check against one of the existing rswag specs (e.g., accounts_spec.rb) to make sure the pattern matches.


ℹ️ Design notes (not blocking, no action required unless you disagree)

  • api_error_payload for stale+error — returns { present: true, message: "Sync became stale before completion" } even though the raw error is suppressed. Returning nil for stale-no-error but an object for stale-with-error means clients need to branch on the object's existence rather than present. Consistent, but the field name present: true inside an object that is already non-null is redundant. The field could be dropped; presence of the object is the signal. This is a design call, not a correctness issue.
  • show.json.jbuilder 200+null for latest — the OpenAPI description already documents data: null, so this is fine. Noted only because jjmata's thread is still open.

Bottom line

One real defect remains: the OAS 3.0 $ref + nullable: true sibling pattern in both the YAML and the swagger helper. Everything else is addressed or is a design preference. Fixing that schema issue and resolving the stale threads would clear the PR for merge consideration.


Generated by Claude Code

@JSONbored JSONbored requested a review from sure-design May 5, 2026 21:20
Copy link
Copy Markdown

@sure-design sure-design left a comment

Choose a reason for hiding this comment

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

Re-review — full independent code read (commit 9bc4c7f)

All substantive issues from prior rounds are resolved. Below is an itemised verification, then the two things still worth a look before merge.


✅ Everything from the previous review is addressed

Issue Verification
OAS 3.0 $ref + nullable: true sibling bug Fixed: both SyncResource.error and SyncResponse.data now use nullable: true + allOf: [{ $ref }], which is the correct OAS 3.0 pattern.
present: semantic confusion in error payload Removed. api_error_payload now returns { message: "..." } or nil. SyncErrorSummary schema updated to match — only message in required and properties.
rswag ApiKey creation inconsistency Fixed: spec now sets both key: key and display_key: key, matching how ApiKey.find_by_value / plain_key work.
403 rswag test used update_column mutation Fixed: api_key_without_read_scope is a proper let with its own key, not a mid-test column mutation.
All other prior-round issues Confirmed addressed (UUID guard, Current.*, for_family in model, syncable&.try(:name), scoped destroy, 403 docs, route alias, deterministic ordering).

The base controller confirms valid_uuid?, safe_page_param, safe_per_page_param, and handle_not_found are all present — no hidden dependency issues.


🟡 Four open threads are addressed but not yet marked resolved

r3177231986 (skinny-controllers), r3177232701 (nil syncable guard), r3177232804 (terminal? coverage), r3177233290 (three keys / Redis) are all is_outdated: true — the code they pointed at was reworked. Please mark them resolved to keep the thread list clean.


ℹ️ Nit — nil branch in show.json.jbuilder is unreachable

# show.json.jbuilder
if @sync
  json.partial! "api/v1/syncs/sync", sync: @sync
else
  json.nil!   # ← dead code
end

The latest action returns early with render json: { data: nil } before this view is ever called with a nil @sync, and the show action always raises RecordNotFound rather than setting @sync = nil. The else branch is harmless but it's dead code. The early return in latest could instead flow through the view (drop the guard and let show.json.jbuilder handle the nil), or the view's nil branch can just be removed since it's never hit. Either way, not blocking.


Bottom line

No correctness issues remain. The OAS schema is now valid, the error payload semantics are clean, and test setup is consistent. Ready to merge once the four stale threads are closed.


Generated by Claude Code

@jjmata jjmata merged commit 9e36983 into we-promise:main May 6, 2026
9 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

contributor:flagged Contributor flagged for review by trust analysis. pr:verified PR passed security analysis.

Development

Successfully merging this pull request may close these issues.

4 participants