Skip to content

feat: sync premium data to cloud#123

Open
AlejandroAkbal wants to merge 11 commits into
mainfrom
feat/premium-critical-cloud-sync
Open

feat: sync premium data to cloud#123
AlejandroAkbal wants to merge 11 commits into
mainfrom
feat/premium-critical-cloud-sync

Conversation

@AlejandroAkbal
Copy link
Copy Markdown
Member

@AlejandroAkbal AlejandroAkbal commented May 28, 2026

Summary by CodeRabbit

  • New Features

    • Premium cloud sync: authenticated realtime sync for tag collections, boorus, custom blocklist, saved posts, background init, and account/cloud-data deletion.
  • Removed Features

    • Legacy backup/restore UI and backup APIs/types removed.
  • Updates

    • Premium UI now persists changes asynchronously (save-on-success UX); sign-in redirect behavior changed; per-user saved-post namespacing; ad weight tweak; plugin naming; dashboard billing/ deletion flows; +11 new/updated locales.
  • Tests

    • New test suites for cloud sync, realtime, locales, path/canonical matching, and related runtime behaviors.
  • Docs

    • Testing guidance updated with Premium Cloud Sync notes.

Review Change Stack

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 28, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 66c4a578-4568-42c8-89cd-0cbe2e0fc195

📥 Commits

Reviewing files that changed from the base of the PR and between 7b4d9a8 and 110384e.

📒 Files selected for processing (3)
  • app/composables/usePremiumCloudSync.ts
  • i18n/locales/id.json
  • test/composables/premium-cloud-sync-runtime.test.ts
💤 Files with no reviewable changes (1)
  • app/composables/usePremiumCloudSync.ts
📜 Recent review details
🧰 Additional context used
📓 Path-based instructions (5)
**/*.{vue,ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

Import Vue components as <ComponentName> not <Folder/ComponentName> due to auto-import flat registration (pathPrefix: false)

Files:

  • test/composables/premium-cloud-sync-runtime.test.ts
**/*.{ts,tsx,js,vue}

📄 CodeRabbit inference engine (AGENTS.md)

Define locales in config/i18n.ts as the single source of truth; do not hardcode locale strings elsewhere

Use Prettier with 120-char print width, no semicolons, single quotes, trailing commas removed, single attribute per line in Vue templates

Files:

  • test/composables/premium-cloud-sync-runtime.test.ts
**/*.{ts,tsx,vue}

📄 CodeRabbit inference engine (AGENTS.md)

Use flat bracket keys for query filters (filter[sort], filter[rating], etc.) with Vue Router's default query handling; do not add qs library unless URL contract changes

Files:

  • test/composables/premium-cloud-sync-runtime.test.ts
**/*.{ts,tsx,js}

📄 CodeRabbit inference engine (AGENTS.md)

For URL validation/parsing, prefer URL.canParse() or URL.parse() over constructor try/catch; use URL.parse() when URL object is needed

Do not add service worker logic; the service worker is intentionally disabled (selfDestroying: true)

Files:

  • test/composables/premium-cloud-sync-runtime.test.ts
test/**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

Use @nuxt/test-utils with Playwright inside describe blocks that call await setup({ browser: true })

Mock server-side API calls via test-only Nitro plugin at test/server-mocks/plugin.ts, injected through nuxt.config.ts$test.nitro.plugins

Files:

  • test/composables/premium-cloud-sync-runtime.test.ts
🧠 Learnings (1)
📓 Common learnings
Learnt from: CR
Repo: Rule-34/App

Timestamp: 2026-05-29T15:55:53.577Z
Learning: Validate image delivery in an environment where imgproxy can resolve the source URL, especially for non-premium SPA navigations
Learnt from: CR
Repo: Rule-34/App

Timestamp: 2026-05-29T15:55:53.577Z
Learning: Prefer high-impact, measurable optimizations over small rewrites; keep battle-tested dependencies unless replacement has clear, measured payoff
Learnt from: CR
Repo: Rule-34/App

Timestamp: 2026-05-29T15:55:53.577Z
Learning: Verify performance changes with production build, relevant tests, request traces, and Lighthouse against built app before keeping changes
Learnt from: CR
Repo: Rule-34/App

Timestamp: 2026-05-29T15:55:53.577Z
Learning: Do not add app-level HTML compression unless direct-origin deployment needs it; verify with headers, byte sizes, warm TTFB, and Lighthouse
Learnt from: CR
Repo: Rule-34/App

Timestamp: 2026-05-29T15:55:53.577Z
Learning: Keep the global TanStack Vue Query plugin unless larger measured payoff appears for route-scoped QueryClient
Learnt from: CR
Repo: Rule-34/App

Timestamp: 2026-05-29T15:55:53.577Z
Learning: Source map uploads only happen in production Docker builds with `SENTRY_ORG`, `SENTRY_PROJECT`, `SENTRY_AUTH_TOKEN` build args
Learnt from: CR
Repo: Rule-34/App

Timestamp: 2026-05-29T15:55:53.577Z
Learning: For premium and PocketBase flows, use real authenticated browser session for final investigation when possible; real-browser traces catch request bursts and auth redirects
🔇 Additional comments (2)
i18n/locales/id.json (1)

508-508: LGTM!

test/composables/premium-cloud-sync-runtime.test.ts (1)

134-134: LGTM!


📝 Walkthrough

Walkthrough

Migrates premium data persistence from local backups to a PocketBase-backed cloud sync: adds PremiumCloudSync repository and composable, wires Nuxt plugins and UI pages to persist via cloud, removes legacy backup artifacts, expands locales, and adds comprehensive tests and runtime behavior guarantees.

Changes

Premium Cloud Sync Migration

Layer / File(s) Summary
Cloud Sync Core Types and Repository
app/assets/js/PremiumCloudSync.ts
Defines record/payload types for tag collections, boorus, and blocklists; implements PremiumCloudSyncRepository with state loading, reconciliation, batch mutation support, deletion and realtime subscribe flows; includes conversion helpers and deep-equality utilities.
Cloud Sync Composable and Runtime Lifecycle
app/composables/usePremiumCloudSync.ts
Coordinates gated initialization, realtime subscription, queued refresh/coalescing, cloud setters/resetters for tagCollections/boorus/blocklist/saved-posts, failure-to-toast routing, and local state hydration/clearing.
UI wiring: pages & components
app/pages/premium/*, app/components/pages/posts/post/*, app/pages/settings.vue
Pages and components now use the composable API (setTagCollections, setUserBooruList, setCustomBlockList, savePost, deleteSavedPost), convert CRUD flows to async+persist, reorder immutably, and gate UI state updates on successful persistence.
Cloud Sync Plugin and PocketBase Setup
app/plugins/050.premium-cloud-sync.client.ts, app/plugins/030.pocketbase.ts
Adds client plugin that triggers background initialization when auth is valid; sets PocketBase plugin name for dependency wiring.
Saved-Posts UI & PostSave Wiring
app/components/pages/posts/post/PostSave.vue, app/pages/premium/saved-posts/[domain].vue
Rewires save/delete to use composable API; loads distinct domains async; removes savedPostList length from infinite query key; sets $autoCancel:false for posts getList.
User Isolation & TagCollection i18n
app/composables/usePocketbase.ts, app/composables/useTagCollections.ts, app/pages/premium/sign-in.vue, server/plugins/fix-canonical-queries.ts
Namespaces saved-post local cache by authenticated userId; uses Nuxt-provided i18n for default tag collection names; sign-in redirect switched to full-page window.location.href; canonical regex now generated from configured locale codes.
Advertisement Configuration Updates
app/composables/useAdvertisements.ts
Adjusts two popunder ads: updates one script URL and changes weights to alter selection probabilities.
Cloud Sync Unit and Integration Tests
test/assets/premium-cloud-sync-repository.test.ts, test/assets/premium-cloud-sync.test.ts
Adds tests for repository load/save/delete/reconcile behavior, payload serialization, and the createLatestAsyncQueue regression case.
Cloud Sync Runtime and Behavioral Tests
test/composables/premium-cloud-sync-runtime.test.ts
Adds runtime-level tests asserting initialization, failure routing, local-state-before-persist semantics, background init plugin usage, realtime subscription/coalescing, and queued-refresh clearing.
Locale Validation and Route Tests
test/i18n/audience-locales.test.ts, test/i18n/premium-cloud-sync-locales.test.ts, test/pages/premium-sign-in.test.ts, test/server/fix-canonical-queries-pattern.test.ts, test/server/path.test.ts
Validates locale coverage and key parity, premium cloud sync localization, sign-in hard-redirect behavior, and configurable posts-index path regex.
Internationalization Expansion and Localization Updates
config/i18n.ts, i18n/locales/*
Adds many new locale files and updates existing locales to remove backup-related keys and add cloud sync/account-deletion messaging.
Vitest Worker Configuration & Docs
vitest.config.mts, AGENTS.md
Uses VITEST_MAX_WORKERS env var to configure test.maxWorkers with a default fallback of 3; updates AGENTS guidance for premium-cloud-sync debugging.

Possibly related PRs

  • Rule-34/App#120: Overlaps with backup/restore and localization changes touching premium pages and blocklist flows.
✨ 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/premium-critical-cloud-sync
✨ Simplify code
  • Create PR with simplified code
  • Commit simplified code in branch feat/premium-critical-cloud-sync

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: 16

Caution

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

⚠️ Outside diff range comments (1)
app/pages/premium/additional-boorus.vue (1)

163-190: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Reject duplicate domains during edit, not just create.

createBooru() blocks duplicates, but editBooru() can currently change an entry to another row's domain. That breaks the :key="booru.domain" contract in the template and makes saveBoorus() ambiguous because the cloud sync repository reconciles boorus by domain. Add the same uniqueness check here, excluding the row being edited.

Suggested fix
   const booruType = booruTypeList.find((booruType) => booruType.type === currentBooru.value.type)

   if (!booruType) {
     toast.error(t('toasts.invalidBooruType'))
     return
   }
+
+  if (
+    userBooruList.value.some(
+      (booruFromList, index) => index !== dialogEditIndex.value && booruFromList.domain === currentBooru.value.domain
+    )
+  ) {
+    toast.error(t('toasts.booruAlreadyExists'))
+    return
+  }

   const nextBooruList = [...userBooruList.value]
🤖 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 `@app/pages/premium/additional-boorus.vue` around lines 163 - 190, editBooru
allows changing a booru's domain to one that already exists; add a uniqueness
check (same logic as createBooru) that scans userBooruList for an existing
domain equal to currentBooru.value.domain but ignores the entry at
dialogEditIndex.value, and if found call toast.error(t('toasts.duplicateBooru'))
and return; perform this check before constructing nextBooruList and before
calling setUserBooruList so the edit is rejected when it would create a
duplicate domain key.
🤖 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 `@app/assets/js/PremiumCloudSync.ts`:
- Around line 214-226: The current loop deletes unmatched records before
applying updates/creates, risking irreversible data loss if subsequent
collection.update or collection.create fails; change the order so you first
perform all updates (loop over updates calling collection.update) and creates
(loop over creates calling collection.create) and only after those Promises
resolve successfully iterate records and delete any not in matchedRecords
(collection.delete), and ensure the whole mutation sequence is awaited (or
wrapped in a try/catch) so deletes run only on successful completion of
updates/creates.

In `@app/composables/usePocketbase.ts`:
- Around line 11-12: The per-user saved-post localStorage keys are never removed
on auth changes so stale keys remain; update the auth transition and sync-clear
flows to explicitly remove the old per-user key. Concretely: when computing
userId in usePocketbase (symbol: userId) and initializing savedPostList via
useLocalState, persist the current userId in a small sentinel (or read previous
from localStorage) and on auth changes (hook into $pocketBase.authStore events
or where $pocketBase.authStore.clear() is called) call
localStorage.removeItem(`pocketbase-savedPostList-${oldUserId}`) for the
previous id before switching to the new id; likewise modify
clearSyncedLocalState() to remove
localStorage.removeItem(`pocketbase-savedPostList-${userId}`) in addition to
setting savedPostList.value = [] so the per-user entry is deleted. Ensure
references to useLocalState, savedPostList, userId,
$pocketBase.authStore.clear(), and clearSyncedLocalState() are updated
accordingly.

In `@app/composables/usePremiumCloudSync.ts`:
- Around line 11-20: The composable fails to retain and dispose the realtime
subscription and never resets runtime.value.subscribed, causing stale state
across auth changes; add a subscription holder to PremiumCloudSyncRuntime (e.g.,
realtimeUnsubscribe: (() => void) | null), store the unsubscribe function when
you create the realtime handler, call it and set
runtime.value.realtimeUnsubscribe = null and runtime.value.subscribed = false
whenever auth is cleared or before re-subscribing, and ensure subscribe logic
checks and clears any existing subscription before creating a new one so
re-subscription in the same session works correctly while preserving
initializing/initialized behavior.

In `@app/pages/premium/additional-boorus.vue`:
- Around line 34-36: The reorder path mutates UI state then calls
setUserBooruList(...) immediately, causing overlapping cloud writes when users
drag quickly; change this to serialize or debounce saves: either implement a
short debounce around calls to setUserBooruList so multiple moveArrayElement
invocations collapse into one save, or maintain a single in-flight promise
(e.g., a queue or awaiting a "currentSave" promise) so subsequent calls to
setUserBooruList wait for the prior save to finish before persisting; update the
code that calls moveArrayElement and setUserBooruList (referenced by
moveArrayElement, userBooruList.value, and setUserBooruList) to use one of these
approaches to prevent out-of-order writes.

In `@app/pages/premium/tag-collections.vue`:
- Around line 33-35: Reorders are firing concurrent writes: when you call
setTagCollections(nextTagCollections) from the move handler (which uses
moveArrayElement and tagCollections.value) you can get stale persistence if an
older request completes last; fix it by serializing/collapsing reorder saves —
add a small reorder queue/locker in the component that tracks a single in-flight
save (e.g. pendingReorderPromise and latestReorderPayload): on each reorder
assign latestReorderPayload = nextTagCollections and chain a promise so saves
run one-at-a-time (pending = pending.then(() =>
setTagCollections(latestReorderPayload))) or use an incrementing token/version
checked before committing so only the most recent nextTagCollections is
persisted; update the move handler to enqueue the payload instead of directly
calling setTagCollections.

In `@i18n/locales/hi.json`:
- Around line 307-310: The linked-key references for planMonthlyName and
planYearlyName are pointing to non-existent Hindi keys ("सामान्य.मासिक" /
"सामान्य.वार्षिक"); update those entries so they reference the existing common
keys (common.monthly and common.yearly) used elsewhere in this file (i.e.,
change the values of planMonthlyName and planYearlyName to use the Nuxt i18n
linked-key syntax pointing to common.monthly and common.yearly respectively) so
the labels resolve correctly.

In `@i18n/locales/id.json`:
- Line 508: The string value for the "blockedTitle" key includes an incorrect
flag emoji (currently "🇷🇮"); update the "blockedTitle" entry in
i18n/locales/id.json to use the correct flag emoji for the intended country or
remove the emoji entirely so it matches other locales' formatting (adjust the
value of "blockedTitle" accordingly to either "Diblokir di negara Anda?" or
"Diblokir di negara Anda? 🇮🇩" depending on the intended flag).
- Around line 307-310: Replace the invalid i18n references and the emoji typo in
the Indonesian locale: update the keys planMonthlyName and planYearlyName to use
the existing common keys (change "@:umum.bulanan" to "@:common.monthly" and
"@:umum.setiap tahun" to "@:common.yearly"), leave plan30DaysName as-is if
correct, and change news.blockedTitle's emoji from 🇷🇮 to 🇷🇺 to match other
locales.

In `@i18n/locales/it.json`:
- Around line 307-310: The i18n linked-key paths used for plan labels are
incorrect: update the values for "planMonthlyName" and "planYearlyName" to
reference the canonical keys used elsewhere; replace "@:common.mensile" with
"@:common.monthly" for planMonthlyName and replace "@:comune.annuale" with
"@:common.yearly" for planYearlyName so the links resolve correctly.

In `@i18n/locales/ko.json`:
- Around line 307-310: The i18n keys planMonthlyName and planYearlyName in
i18n/locales/ko.json currently reference non-existent namespace keys using
"@:공통.월간" and "@:공통.연간"; update those values to use the correct Nuxt i18n linked
refs "@:common.monthly" for planMonthlyName and "@:common.yearly" for
planYearlyName so they resolve to the existing common.monthly and common.yearly
entries.

In `@i18n/locales/pl.json`:
- Around line 307-310: The Polish locale uses broken linked-key paths for plan
labels: replace the incorrect '@:wspólny.miesięczny' and '@:wspólny.roczny'
values for the planMonthlyName and planYearlyName keys with the correct common
namespace links (e.g. '@:common.monthly' and '@:common.yearly') so the i18n
resolver can find the shared strings; update the two values in the JSON for
planMonthlyName and planYearlyName accordingly.

In `@i18n/locales/th.json`:
- Line 307: The linked-key value for planMonthlyName is pointing to a
non-existent key (`@:common.รายเดือน`) so it will render raw text; update
planMonthlyName to reference the correct existing common key (for example
`@:common.monthly`) or replace the linked reference with the actual localized
string ("รายเดือน") so the monthly plan label resolves correctly.

In `@i18n/locales/tr.json`:
- Line 310: The linked key for planYearlyName uses a non-canonical character
form "@:ortak.yıllık"; update the value of "planYearlyName" to reference the
canonical i18n key path used elsewhere (e.g. replace "@:ortak.yıllık" with the
matching canonical path such as "@:ortak.yillik" or whatever exact key name
exists in the ortak namespace) so the lookup resolves; confirm the exact
canonical key in your other locale/i18n files and use that exact path.

In `@test/assets/premium-cloud-sync-repository.test.ts`:
- Around line 31-35: The mock update implementation in the test ("update"
function) silently writes when findIndex returns -1; modify the update stub (the
vi.fn defined in the test) to check the index returned from
records[name].findIndex and if index === -1 immediately throw or return a
rejected error (e.g., new Error(`Record not found: ${id}`)) instead of
proceeding to assign to records[name][-1], otherwise merge payload into
records[name][index] and return it; reference the update mock, the records
variable, and the findIndex call when making this change.
- Around line 67-80: Add a test that exercises the update branch of
PremiumCloudSyncRepository.saveCustomBlockList by seeding createFakePocketBase
with an existing tag_blocklists record (e.g., include an object with id and
user_id) and asserting that repository.saveCustomBlockList(['loli']) triggers a
calls entry with method: 'update' (and the correct collection 'tag_blocklists'
and args containing the existing record id and updated tags), rather than a
'create' call; use the same client/calls fixture and ensure the test name
clarifies it's the "existing-blocklist update path".

In `@test/pages/premium-sign-in.test.ts`:
- Around line 8-11: The test currently asserts an exact source string for
window.location.href which is brittle; update the assertions in
premium-sign-in.test.ts to use a semantic regex against source that verifies
localePath was called with path '/premium/dashboard' and a query containing
initialLogin: 'true' (e.g., match "localePath" and the path and initialLogin
tokens rather than the entire formatted string), and keep a separate assertion
that source does not contain any invocation of navigateTo( or await navigateTo(
(use a regex like /navigateTo\s*\(/) to block client-side navigation; update the
expect calls referencing source, localePath, and navigateTo accordingly.

---

Outside diff comments:
In `@app/pages/premium/additional-boorus.vue`:
- Around line 163-190: editBooru allows changing a booru's domain to one that
already exists; add a uniqueness check (same logic as createBooru) that scans
userBooruList for an existing domain equal to currentBooru.value.domain but
ignores the entry at dialogEditIndex.value, and if found call
toast.error(t('toasts.duplicateBooru')) and return; perform this check before
constructing nextBooruList and before calling setUserBooruList so the edit is
rejected when it would create a duplicate domain key.
🪄 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: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 0586810f-da37-4171-b352-575f120893ee

📥 Commits

Reviewing files that changed from the base of the PR and between b1ec255 and 920266c.

📒 Files selected for processing (45)
  • app/assets/js/BackupHelper.ts
  • app/assets/js/PremiumCloudSync.ts
  • app/assets/js/oldBackup.d.ts
  • app/components/pages/posts/post/PostTag.vue
  • app/composables/useAdvertisements.ts
  • app/composables/usePocketbase.ts
  • app/composables/usePremiumCloudSync.ts
  • app/composables/useTagCollections.ts
  • app/pages/premium/additional-boorus.vue
  • app/pages/premium/backup.vue
  • app/pages/premium/dashboard.vue
  • app/pages/premium/sign-in.vue
  • app/pages/premium/tag-collections.vue
  • app/pages/settings.vue
  • app/plugins/030.pocketbase.ts
  • app/plugins/050.premium-cloud-sync.client.ts
  • config/i18n.ts
  • i18n/locales/de.json
  • i18n/locales/en.json
  • i18n/locales/es.json
  • i18n/locales/fil.json
  • i18n/locales/fr.json
  • i18n/locales/hi.json
  • i18n/locales/id.json
  • i18n/locales/it.json
  • i18n/locales/ja.json
  • i18n/locales/ko.json
  • i18n/locales/pl.json
  • i18n/locales/pt.json
  • i18n/locales/ru.json
  • i18n/locales/th.json
  • i18n/locales/tr.json
  • i18n/locales/vi.json
  • i18n/locales/zh.json
  • server/plugins/fix-canonical-queries.ts
  • test/assets/premium-cloud-sync-repository.test.ts
  • test/assets/premium-cloud-sync.test.ts
  • test/composables/premium-cloud-sync-runtime.test.ts
  • test/i18n/audience-locales.test.ts
  • test/i18n/premium-cloud-sync-locales.test.ts
  • test/pages/premium-sign-in.test.ts
  • test/pages/premium/backup.mock-data.json
  • test/server/fix-canonical-queries-pattern.test.ts
  • test/server/path.test.ts
  • vitest.config.mts
💤 Files with no reviewable changes (4)
  • app/assets/js/BackupHelper.ts
  • app/assets/js/oldBackup.d.ts
  • app/pages/premium/backup.vue
  • test/pages/premium/backup.mock-data.json
📜 Review details
🧰 Additional context used
📓 Path-based instructions (11)
test/**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

test/**/*.{ts,tsx}: Use Vitest with @nuxt/test-utils and Playwright browser mode for testing
Use @nuxt/test-utils with Playwright by calling await setup({ browser: true }) inside describe blocks for browser-based tests

Files:

  • test/server/path.test.ts
  • test/i18n/premium-cloud-sync-locales.test.ts
  • test/pages/premium-sign-in.test.ts
  • test/server/fix-canonical-queries-pattern.test.ts
  • test/i18n/audience-locales.test.ts
  • test/assets/premium-cloud-sync.test.ts
  • test/composables/premium-cloud-sync-runtime.test.ts
  • test/assets/premium-cloud-sync-repository.test.ts
**/*.{js,ts,tsx,vue,json}

📄 CodeRabbit inference engine (AGENTS.md)

Use Prettier for formatting with 120-char print width, no semicolons, single quotes, trailing commas removed, and single attribute per line in Vue templates

Files:

  • test/server/path.test.ts
  • config/i18n.ts
  • app/plugins/050.premium-cloud-sync.client.ts
  • test/i18n/premium-cloud-sync-locales.test.ts
  • test/pages/premium-sign-in.test.ts
  • test/server/fix-canonical-queries-pattern.test.ts
  • app/pages/premium/sign-in.vue
  • server/plugins/fix-canonical-queries.ts
  • i18n/locales/hi.json
  • i18n/locales/th.json
  • app/plugins/030.pocketbase.ts
  • app/pages/settings.vue
  • test/i18n/audience-locales.test.ts
  • test/assets/premium-cloud-sync.test.ts
  • app/composables/usePocketbase.ts
  • i18n/locales/pl.json
  • app/composables/useAdvertisements.ts
  • app/composables/usePremiumCloudSync.ts
  • i18n/locales/zh.json
  • app/pages/premium/tag-collections.vue
  • app/composables/useTagCollections.ts
  • app/components/pages/posts/post/PostTag.vue
  • i18n/locales/ko.json
  • i18n/locales/id.json
  • test/composables/premium-cloud-sync-runtime.test.ts
  • i18n/locales/tr.json
  • i18n/locales/fil.json
  • test/assets/premium-cloud-sync-repository.test.ts
  • i18n/locales/it.json
  • i18n/locales/vi.json
  • i18n/locales/fr.json
  • app/pages/premium/additional-boorus.vue
  • i18n/locales/ru.json
  • i18n/locales/es.json
  • i18n/locales/de.json
  • app/pages/premium/dashboard.vue
  • i18n/locales/pt.json
  • app/assets/js/PremiumCloudSync.ts
  • i18n/locales/ja.json
  • i18n/locales/en.json
**/*.{ts,tsx,js}

📄 CodeRabbit inference engine (AGENTS.md)

For URL validation/parsing, prefer URL.canParse() or URL.parse() over constructor try/catch; use URL.parse() when the parsed URL object is needed

Files:

  • test/server/path.test.ts
  • config/i18n.ts
  • app/plugins/050.premium-cloud-sync.client.ts
  • test/i18n/premium-cloud-sync-locales.test.ts
  • test/pages/premium-sign-in.test.ts
  • test/server/fix-canonical-queries-pattern.test.ts
  • server/plugins/fix-canonical-queries.ts
  • app/plugins/030.pocketbase.ts
  • test/i18n/audience-locales.test.ts
  • test/assets/premium-cloud-sync.test.ts
  • app/composables/usePocketbase.ts
  • app/composables/useAdvertisements.ts
  • app/composables/usePremiumCloudSync.ts
  • app/composables/useTagCollections.ts
  • test/composables/premium-cloud-sync-runtime.test.ts
  • test/assets/premium-cloud-sync-repository.test.ts
  • app/assets/js/PremiumCloudSync.ts
test/**/*.ts

📄 CodeRabbit inference engine (AGENTS.md)

Import debugBrowserOptions from test/helper.ts for headful Playwright playback with slowMo in test debug mode

Files:

  • test/server/path.test.ts
  • test/i18n/premium-cloud-sync-locales.test.ts
  • test/pages/premium-sign-in.test.ts
  • test/server/fix-canonical-queries-pattern.test.ts
  • test/i18n/audience-locales.test.ts
  • test/assets/premium-cloud-sync.test.ts
  • test/composables/premium-cloud-sync-runtime.test.ts
  • test/assets/premium-cloud-sync-repository.test.ts
{config/i18n.ts,i18n/locales/**/*.json}

📄 CodeRabbit inference engine (AGENTS.md)

Define locales in config/i18n.ts as the single source of truth; non-default locales (ru, es, ja) get URL prefixes with mirrored route rules

Files:

  • config/i18n.ts
  • i18n/locales/hi.json
  • i18n/locales/th.json
  • i18n/locales/pl.json
  • i18n/locales/zh.json
  • i18n/locales/ko.json
  • i18n/locales/id.json
  • i18n/locales/tr.json
  • i18n/locales/fil.json
  • i18n/locales/it.json
  • i18n/locales/vi.json
  • i18n/locales/fr.json
  • i18n/locales/ru.json
  • i18n/locales/es.json
  • i18n/locales/de.json
  • i18n/locales/pt.json
  • i18n/locales/ja.json
  • i18n/locales/en.json
app/**/*.{vue,ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

Use Nuxt 4 with Vue 3 and TypeScript for the application framework

Files:

  • app/plugins/050.premium-cloud-sync.client.ts
  • app/pages/premium/sign-in.vue
  • app/plugins/030.pocketbase.ts
  • app/pages/settings.vue
  • app/composables/usePocketbase.ts
  • app/composables/useAdvertisements.ts
  • app/composables/usePremiumCloudSync.ts
  • app/pages/premium/tag-collections.vue
  • app/composables/useTagCollections.ts
  • app/components/pages/posts/post/PostTag.vue
  • app/pages/premium/additional-boorus.vue
  • app/pages/premium/dashboard.vue
  • app/assets/js/PremiumCloudSync.ts
app/**/*.{js,ts,tsx,vue}

📄 CodeRabbit inference engine (AGENTS.md)

Use Nuxt flat ESLint configuration via @nuxt/eslint for linting

Files:

  • app/plugins/050.premium-cloud-sync.client.ts
  • app/pages/premium/sign-in.vue
  • app/plugins/030.pocketbase.ts
  • app/pages/settings.vue
  • app/composables/usePocketbase.ts
  • app/composables/useAdvertisements.ts
  • app/composables/usePremiumCloudSync.ts
  • app/pages/premium/tag-collections.vue
  • app/composables/useTagCollections.ts
  • app/components/pages/posts/post/PostTag.vue
  • app/pages/premium/additional-boorus.vue
  • app/pages/premium/dashboard.vue
  • app/assets/js/PremiumCloudSync.ts
app/**/*.{ts,vue}

📄 CodeRabbit inference engine (AGENTS.md)

Use Schema.org breadcrumb item URLs as local/locale-relative URLs, not production-absolute URLs (reserve production URLs for canonicals only)

Files:

  • app/plugins/050.premium-cloud-sync.client.ts
  • app/pages/premium/sign-in.vue
  • app/plugins/030.pocketbase.ts
  • app/pages/settings.vue
  • app/composables/usePocketbase.ts
  • app/composables/useAdvertisements.ts
  • app/composables/usePremiumCloudSync.ts
  • app/pages/premium/tag-collections.vue
  • app/composables/useTagCollections.ts
  • app/components/pages/posts/post/PostTag.vue
  • app/pages/premium/additional-boorus.vue
  • app/pages/premium/dashboard.vue
  • app/assets/js/PremiumCloudSync.ts
app/pages/**/*.vue

📄 CodeRabbit inference engine (AGENTS.md)

Use flat bracket keys for query filters (filter[sort], filter[rating], etc.) with Vue Router's default query handling; do not re-add qs for nested route.query.filter objects unless URL contract changes

Files:

  • app/pages/premium/sign-in.vue
  • app/pages/settings.vue
  • app/pages/premium/tag-collections.vue
  • app/pages/premium/additional-boorus.vue
  • app/pages/premium/dashboard.vue
app/components/**/*.vue

📄 CodeRabbit inference engine (AGENTS.md)

app/components/**/*.vue: Register Vue components without path prefix using pathPrefix: false; import components by name only (e.g., not <Input/DomainSelector>)
Use @nuxt/image v2 module API for image preload priority with preload: { fetchPriority: 'high' } instead of patching rendered HTML in Nitro
Validate image delivery in environments where imgproxy can resolve the source URL; non-premium SPA navigations keep direct image paths

Files:

  • app/components/pages/posts/post/PostTag.vue
{nuxt.config.ts,app/components/**/*.vue}

📄 CodeRabbit inference engine (AGENTS.md)

Use custom imgproxy provider for registered in nuxt.config.ts; generate images at 1x density only in webp format to reduce bandwidth

Files:

  • app/components/pages/posts/post/PostTag.vue
🪛 ast-grep (0.42.3)
server/plugins/fix-canonical-queries.ts

[warning] 35-35: Regular expression constructed from variable input detected. This can lead to Regular Expression Denial of Service (ReDoS) attacks if the variable contains malicious patterns. Use libraries like 'recheck' to validate regex safety or use static patterns.
Context: new RegExp(^${optionalLocalePrefixPattern}/posts/([^/]+)$)
Note: [CWE-1333] Inefficient Regular Expression Complexity [REFERENCES]
- https://owasp.org/www-community/attacks/Regular_expression_Denial_of_Service_-_ReDoS
- https://cwe.mitre.org/data/definitions/1333.html

(regexp-from-variable)

test/composables/premium-cloud-sync-runtime.test.ts

[warning] 8-8: Regular expression constructed from variable input detected. This can lead to Regular Expression Denial of Service (ReDoS) attacks if the variable contains malicious patterns. Use libraries like 'recheck' to validate regex safety or use static patterns.
Context: new RegExp((?:async )?function ${name}\\b)
Note: [CWE-1333] Inefficient Regular Expression Complexity [REFERENCES]
- https://owasp.org/www-community/attacks/Regular_expression_Denial_of_Service_-_ReDoS
- https://cwe.mitre.org/data/definitions/1333.html

(regexp-from-variable)

🔇 Additional comments (20)
app/composables/useAdvertisements.ts (1)

45-45: LGTM!

Also applies to: 80-81

app/plugins/050.premium-cloud-sync.client.ts (1)

1-17: LGTM!

app/plugins/030.pocketbase.ts (1)

10-12: LGTM!

test/i18n/premium-cloud-sync-locales.test.ts (1)

1-58: LGTM!

test/server/fix-canonical-queries-pattern.test.ts (1)

1-30: LGTM!

test/server/path.test.ts (1)

4-22: LGTM!

Also applies to: 34-35

test/i18n/audience-locales.test.ts (1)

1-73: LGTM!

config/i18n.ts (1)

12-22: LGTM!

i18n/locales/de.json (1)

98-100: LGTM!

Also applies to: 198-205, 499-504, 539-541

i18n/locales/en.json (1)

98-100: LGTM!

Also applies to: 198-205, 499-504, 539-541

vitest.config.mts (1)

3-10: LGTM!

app/pages/premium/dashboard.vue (1)

9-9: LGTM!

Also applies to: 23-23, 94-123, 124-124, 338-380

app/pages/settings.vue (1)

46-47: LGTM!

Also applies to: 60-60, 75-75, 99-101

app/components/pages/posts/post/PostTag.vue (1)

29-30: LGTM!

Also applies to: 65-65, 67-67, 69-74

test/assets/premium-cloud-sync.test.ts (1)

1-70: LGTM!

test/composables/premium-cloud-sync-runtime.test.ts (1)

1-129: LGTM!

app/composables/usePocketbase.ts (1)

24-30: LGTM!

app/composables/useTagCollections.ts (1)

4-19: LGTM!

app/pages/premium/sign-in.vue (1)

35-35: LGTM!

server/plugins/fix-canonical-queries.ts (1)

24-37: LGTM!

Also applies to: 56-62, 107-107

Comment thread app/assets/js/PremiumCloudSync.ts
Comment thread app/composables/usePocketbase.ts
Comment thread app/composables/usePremiumCloudSync.ts
Comment thread app/pages/premium/additional-boorus.vue
Comment thread app/pages/premium/tag-collections.vue
Comment thread i18n/locales/th.json Outdated
Comment thread i18n/locales/tr.json Outdated
Comment thread test/assets/premium-cloud-sync-repository.test.ts
Comment thread test/assets/premium-cloud-sync-repository.test.ts
Comment thread test/pages/premium-sign-in.test.ts 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: 7

🤖 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 `@app/assets/js/AsyncSaveQueue.ts`:
- Around line 25-33: The flush loop currently aborts when save(payload) rejects,
which can leave a newer latestPayload unsaved; modify flush (the function
calling save) to catch errors from save(payload) so the loop continues to check
and persist any newer latestPayload instead of bailing early—i.e., wrap the
save(payload) call in try/catch, record or surface the error via an onError
callback or aggregated error store, and continue processing so
hasLatestPayload/latestPayload are handled; keep existing finally behavior that
clears active. Reference symbols: flush, save, latestPayload, hasLatestPayload,
active.

In `@i18n/locales/de.json`:
- Around line 202-207: Replace the ASCII transliterations with proper German
diacritics and ß for the specified locale keys: update "deleteDataOrAccount",
"billingTitle", "billingDescription", "cancelOrManageSubscription",
"cancellationSupportEmailSubject", and "cancellationSupportEmailBody" to use
"löschen", "Möchtest", "kündigen", "Abrechnung & Support" etc.; ensure the email
body also uses "Konto‑E-Mail" and "Lizenz" with correct diacritics and preserves
the {email}/{license} placeholders and newline escapes.

In `@i18n/locales/es.json`:
- Around line 201-207: Fix the dropped diacritics and punctuation in the new
Spanish strings: update "moreAccountActions" to "Más acciones de cuenta",
"billingTitle" to "Facturación y soporte", "billingDescription" to "¿Necesitas
cancelar, actualizar el pago o encontrar tu compra? Abre la plataforma donde
compraste Premium.", "cancelOrManageSubscription" to "Cancelar o gestionar
suscripción", "cancellationSupportEmailSubject" to "Ayuda para cancelar mi
suscripción Premium", and "cancellationSupportEmailBody" to "Hola, necesito
ayuda para cancelar o gestionar mi suscripción Premium.\n\nCorreo de la cuenta:
{email}\nLicencia: {license}" so accents and sentence punctuation are restored
for keys mentioned.

In `@i18n/locales/fr.json`:
- Around line 200-207: Restore proper French punctuation and diacritics in the
affected locale keys: fix strings for "deleteDataOrAccount",
"billingDescription", "cancelOrManageSubscription",
"cancellationSupportEmailSubject" and "cancellationSupportEmailBody" (and any
nearby new billing/account keys) so they use correct apostrophes and accents
(e.g., "données", "compte", "annuler", "gérer", "où", "achat", "e‑mail",
"licence") and ensure escaped characters/newlines remain valid in the JSON value
formatting.

In `@i18n/locales/pt.json`:
- Around line 201-207: Update the Portuguese strings in i18n/locales/pt.json to
use proper diacritics for the new subscription keys: "moreAccountActions",
"deleteDataOrAccount", "billingTitle", "billingDescription",
"cancelOrManageSubscription", "cancellationSupportEmailSubject", and
"cancellationSupportEmailBody"; replace degraded spellings like "acoes",
"Faturacao", "Precisa", "Cancelar ou gerir subscricao", "Ajuda para cancelar a
minha subscricao Premium", and "Ola" with correctly accented forms (e.g.,
"ações", "Faturação", "Precisa", "Cancelar ou gerir subscrição", "Ajuda para
cancelar a minha subscrição Premium", "Olá") and ensure the license/account
placeholders ({email}, {license}) remain intact.

In `@i18n/locales/tr.json`:
- Around line 201-207: Several Turkish translation keys (moreAccountActions,
deleteDataOrAccount, billingTitle, billingDescription,
cancelOrManageSubscription, cancellationSupportEmailSubject,
cancellationSupportEmailBody) are using ASCII-only transliterations; replace
them with natural Turkish strings using correct characters (ğ, ş, ı, ö, ü, ç)
and proper phrasing while preserving placeholders like {email} and {license} and
newline sequences (\n\n) in cancellationSupportEmailBody so the UI reads
naturally for Turkish users.

In `@test/assets/async-save-queue.test.ts`:
- Around line 22-47: Add a new test case exercising the rejection path for
createLatestAsyncQueue: create a deferred (firstSave) and savedPayloads array,
call createLatestAsyncQueue with a handler that pushes payload to savedPayloads
and awaits/throws from firstSave when payload === 'first', then invoke
queue('first') storing the returned promise in `first`, queue a newer payload(s)
like 'second'/'third', ensure you await a microtick (await Promise.resolve()) to
let the first handler run, then reject firstSave to simulate failure and consume
the rejection (e.g. call first.catch(()=>{}) or use Promise.allSettled) so it
doesn't crash the test, await the remaining queue promises, and assert
savedPayloads includes the first entry and that the latest payload (e.g.
'third') was flushed (expect savedPayloads toEqual ['first', 'third']);
reference createLatestAsyncQueue, deferred, firstSave, savedPayloads, queue, and
first in the test.
🪄 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: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 34345290-9946-4e1e-b686-ea447f9732ca

📥 Commits

Reviewing files that changed from the base of the PR and between 920266c and 15baebf.

📒 Files selected for processing (30)
  • app/assets/js/AsyncSaveQueue.ts
  • app/assets/js/BooruList.ts
  • app/assets/js/PremiumCloudSync.ts
  • app/composables/usePremiumCloudSync.ts
  • app/pages/premium/additional-boorus.vue
  • app/pages/premium/dashboard.vue
  • app/pages/premium/tag-collections.vue
  • i18n/locales/de.json
  • i18n/locales/en.json
  • i18n/locales/es.json
  • i18n/locales/fil.json
  • i18n/locales/fr.json
  • i18n/locales/hi.json
  • i18n/locales/id.json
  • i18n/locales/it.json
  • i18n/locales/ja.json
  • i18n/locales/ko.json
  • i18n/locales/pl.json
  • i18n/locales/pt.json
  • i18n/locales/ru.json
  • i18n/locales/th.json
  • i18n/locales/tr.json
  • i18n/locales/vi.json
  • i18n/locales/zh.json
  • test/assets/async-save-queue.test.ts
  • test/assets/booru-list.test.ts
  • test/assets/premium-cloud-sync-repository.test.ts
  • test/i18n/audience-locales.test.ts
  • test/i18n/premium-cloud-sync-locales.test.ts
  • test/pages/premium-sign-in.test.ts
📜 Review details
🧰 Additional context used
📓 Path-based instructions (9)
test/**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

test/**/*.{ts,tsx}: Use Vitest with @nuxt/test-utils and Playwright browser mode for testing
Use @nuxt/test-utils with Playwright by calling await setup({ browser: true }) inside describe blocks for browser-based tests

Files:

  • test/pages/premium-sign-in.test.ts
  • test/assets/booru-list.test.ts
  • test/i18n/audience-locales.test.ts
  • test/assets/async-save-queue.test.ts
  • test/i18n/premium-cloud-sync-locales.test.ts
  • test/assets/premium-cloud-sync-repository.test.ts
**/*.{js,ts,tsx,vue,json}

📄 CodeRabbit inference engine (AGENTS.md)

Use Prettier for formatting with 120-char print width, no semicolons, single quotes, trailing commas removed, and single attribute per line in Vue templates

Files:

  • test/pages/premium-sign-in.test.ts
  • app/assets/js/BooruList.ts
  • i18n/locales/id.json
  • test/assets/booru-list.test.ts
  • test/i18n/audience-locales.test.ts
  • app/assets/js/AsyncSaveQueue.ts
  • i18n/locales/pl.json
  • test/assets/async-save-queue.test.ts
  • test/i18n/premium-cloud-sync-locales.test.ts
  • i18n/locales/vi.json
  • app/pages/premium/tag-collections.vue
  • i18n/locales/ko.json
  • i18n/locales/it.json
  • app/pages/premium/additional-boorus.vue
  • i18n/locales/hi.json
  • test/assets/premium-cloud-sync-repository.test.ts
  • i18n/locales/th.json
  • i18n/locales/en.json
  • i18n/locales/de.json
  • i18n/locales/fil.json
  • i18n/locales/es.json
  • i18n/locales/zh.json
  • app/composables/usePremiumCloudSync.ts
  • i18n/locales/ru.json
  • i18n/locales/fr.json
  • app/pages/premium/dashboard.vue
  • i18n/locales/tr.json
  • i18n/locales/pt.json
  • i18n/locales/ja.json
  • app/assets/js/PremiumCloudSync.ts
**/*.{ts,tsx,js}

📄 CodeRabbit inference engine (AGENTS.md)

For URL validation/parsing, prefer URL.canParse() or URL.parse() over constructor try/catch; use URL.parse() when the parsed URL object is needed

Files:

  • test/pages/premium-sign-in.test.ts
  • app/assets/js/BooruList.ts
  • test/assets/booru-list.test.ts
  • test/i18n/audience-locales.test.ts
  • app/assets/js/AsyncSaveQueue.ts
  • test/assets/async-save-queue.test.ts
  • test/i18n/premium-cloud-sync-locales.test.ts
  • test/assets/premium-cloud-sync-repository.test.ts
  • app/composables/usePremiumCloudSync.ts
  • app/assets/js/PremiumCloudSync.ts
test/**/*.ts

📄 CodeRabbit inference engine (AGENTS.md)

Import debugBrowserOptions from test/helper.ts for headful Playwright playback with slowMo in test debug mode

Files:

  • test/pages/premium-sign-in.test.ts
  • test/assets/booru-list.test.ts
  • test/i18n/audience-locales.test.ts
  • test/assets/async-save-queue.test.ts
  • test/i18n/premium-cloud-sync-locales.test.ts
  • test/assets/premium-cloud-sync-repository.test.ts
app/**/*.{vue,ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

Use Nuxt 4 with Vue 3 and TypeScript for the application framework

Files:

  • app/assets/js/BooruList.ts
  • app/assets/js/AsyncSaveQueue.ts
  • app/pages/premium/tag-collections.vue
  • app/pages/premium/additional-boorus.vue
  • app/composables/usePremiumCloudSync.ts
  • app/pages/premium/dashboard.vue
  • app/assets/js/PremiumCloudSync.ts
app/**/*.{js,ts,tsx,vue}

📄 CodeRabbit inference engine (AGENTS.md)

Use Nuxt flat ESLint configuration via @nuxt/eslint for linting

Files:

  • app/assets/js/BooruList.ts
  • app/assets/js/AsyncSaveQueue.ts
  • app/pages/premium/tag-collections.vue
  • app/pages/premium/additional-boorus.vue
  • app/composables/usePremiumCloudSync.ts
  • app/pages/premium/dashboard.vue
  • app/assets/js/PremiumCloudSync.ts
app/**/*.{ts,vue}

📄 CodeRabbit inference engine (AGENTS.md)

Use Schema.org breadcrumb item URLs as local/locale-relative URLs, not production-absolute URLs (reserve production URLs for canonicals only)

Files:

  • app/assets/js/BooruList.ts
  • app/assets/js/AsyncSaveQueue.ts
  • app/pages/premium/tag-collections.vue
  • app/pages/premium/additional-boorus.vue
  • app/composables/usePremiumCloudSync.ts
  • app/pages/premium/dashboard.vue
  • app/assets/js/PremiumCloudSync.ts
{config/i18n.ts,i18n/locales/**/*.json}

📄 CodeRabbit inference engine (AGENTS.md)

Define locales in config/i18n.ts as the single source of truth; non-default locales (ru, es, ja) get URL prefixes with mirrored route rules

Files:

  • i18n/locales/id.json
  • i18n/locales/pl.json
  • i18n/locales/vi.json
  • i18n/locales/ko.json
  • i18n/locales/it.json
  • i18n/locales/hi.json
  • i18n/locales/th.json
  • i18n/locales/en.json
  • i18n/locales/de.json
  • i18n/locales/fil.json
  • i18n/locales/es.json
  • i18n/locales/zh.json
  • i18n/locales/ru.json
  • i18n/locales/fr.json
  • i18n/locales/tr.json
  • i18n/locales/pt.json
  • i18n/locales/ja.json
app/pages/**/*.vue

📄 CodeRabbit inference engine (AGENTS.md)

Use flat bracket keys for query filters (filter[sort], filter[rating], etc.) with Vue Router's default query handling; do not re-add qs for nested route.query.filter objects unless URL contract changes

Files:

  • app/pages/premium/tag-collections.vue
  • app/pages/premium/additional-boorus.vue
  • app/pages/premium/dashboard.vue
🔇 Additional comments (25)
test/pages/premium-sign-in.test.ts (1)

8-11: LGTM!

app/assets/js/BooruList.ts (1)

1-15: LGTM!

i18n/locales/id.json (1)

200-207: LGTM!

Also applies to: 309-312, 510-510

test/assets/booru-list.test.ts (1)

1-12: LGTM!

test/i18n/audience-locales.test.ts (1)

30-42: LGTM!

Also applies to: 88-99

app/assets/js/AsyncSaveQueue.ts (1)

1-12: LGTM!

i18n/locales/pl.json (1)

200-207: LGTM!

Also applies to: 309-312

app/pages/premium/tag-collections.vue (1)

34-35: Unresolved reorder write race remains.

Line 35 still sends reorder saves without serialization/collapse, so rapid drags can persist stale order if an older write resolves last.

app/pages/premium/additional-boorus.vue (2)

36-37: Reorder persistence race is still present.

Line 37 still fires cloud saves concurrently from drag updates, so out-of-order completions can persist stale ordering.


140-141: LGTM!

Also applies to: 178-181

test/i18n/premium-cloud-sync-locales.test.ts (1)

10-17: LGTM!

i18n/locales/vi.json (1)

200-207: LGTM!

i18n/locales/ko.json (1)

200-207: LGTM!

Also applies to: 309-312

i18n/locales/it.json (1)

200-207: LGTM!

Also applies to: 309-312

i18n/locales/hi.json (1)

200-207: LGTM!

Also applies to: 309-312

test/assets/premium-cloud-sync-repository.test.ts (1)

34-37: LGTM!

Also applies to: 87-102, 162-181

app/composables/usePremiumCloudSync.ts (1)

151-155: Realtime subscription lifecycle issue is still present.

The composable still does not retain/dispose the realtime unsubscribe handler and does not reset runtime.value.subscribed, so re-login in the same session can still skip re-subscription.

Also applies to: 182-186, 256-263, 286-293

i18n/locales/th.json (1)

200-207: LGTM!

Also applies to: 309-309

i18n/locales/en.json (1)

200-207: LGTM!

i18n/locales/fil.json (1)

200-207: LGTM!

i18n/locales/zh.json (1)

200-207: LGTM!

i18n/locales/ru.json (1)

200-207: LGTM!

app/pages/premium/dashboard.vue (1)

5-11: LGTM!

Also applies to: 25-26, 75-85, 98-100, 105-105, 115-115, 186-186, 189-225, 351-359, 361-361, 363-363, 367-367, 370-377, 380-380, 382-390, 392-413, 415-469, 471-472

i18n/locales/ja.json (1)

200-207: LGTM!

app/assets/js/PremiumCloudSync.ts (1)

195-196: LGTM!

Also applies to: 207-207, 224-228

Comment thread app/assets/js/AsyncSaveQueue.ts Outdated
Comment on lines +25 to +33
await save(payload)
}
}

return (payload: T) => {
latestPayload = payload
hasLatestPayload = true
active ??= flush().finally(() => {
active = null
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Queue can drop latest pending payload after a failed save

On Line 25, a rejected save(payload) aborts flush() immediately. If a newer payload was queued while that save was in flight, it may stay unsaved until another enqueue happens, which risks stale cloud-sync state.

💡 Proposed fix
 async function flush() {
+  let firstError: unknown
   while (hasLatestPayload) {
     const payload = latestPayload as T
     latestPayload = undefined
     hasLatestPayload = false

-    await save(payload)
+    try {
+      await save(payload)
+    } catch (error) {
+      firstError ??= error
+    }
   }
+
+  if (firstError !== undefined) {
+    throw firstError
+  }
 }
📝 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
await save(payload)
}
}
return (payload: T) => {
latestPayload = payload
hasLatestPayload = true
active ??= flush().finally(() => {
active = null
async function flush() {
let firstError: unknown
while (hasLatestPayload) {
const payload = latestPayload as T
latestPayload = undefined
hasLatestPayload = false
try {
await save(payload)
} catch (error) {
firstError ??= error
}
}
if (firstError !== undefined) {
throw firstError
}
}
return (payload: T) => {
latestPayload = payload
hasLatestPayload = true
active ??= flush().finally(() => {
active = null
})
}
🤖 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 `@app/assets/js/AsyncSaveQueue.ts` around lines 25 - 33, The flush loop
currently aborts when save(payload) rejects, which can leave a newer
latestPayload unsaved; modify flush (the function calling save) to catch errors
from save(payload) so the loop continues to check and persist any newer
latestPayload instead of bailing early—i.e., wrap the save(payload) call in
try/catch, record or surface the error via an onError callback or aggregated
error store, and continue processing so hasLatestPayload/latestPayload are
handled; keep existing finally behavior that clears active. Reference symbols:
flush, save, latestPayload, hasLatestPayload, active.

Comment thread i18n/locales/de.json Outdated
Comment on lines +202 to +207
"deleteDataOrAccount": "Daten oder Konto loeschen",
"billingTitle": "Abrechnung & Support",
"billingDescription": "Moechtest du kuendigen, die Zahlung aktualisieren oder deinen Kauf finden? Oeffne die Plattform, auf der du Premium gekauft hast.",
"cancelOrManageSubscription": "Abo kuendigen oder verwalten",
"cancellationSupportEmailSubject": "Hilfe beim Kuendigen meines Premium-Abos",
"cancellationSupportEmailBody": "Hallo, ich brauche Hilfe beim Kuendigen oder Verwalten meines Premium-Abos.\n\nKonto-E-Mail: {email}\nLizenz: {license}"
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Restore German diacritics in the new billing/account copy.

The new strings use ASCII transliterations in user-facing text (loeschen, Moechtest, kuendigen) and are inconsistent with the rest of this locale.

💡 Suggested fix
-        "deleteDataOrAccount": "Daten oder Konto loeschen",
+        "deleteDataOrAccount": "Daten oder Konto löschen",
-        "billingDescription": "Moechtest du kuendigen, die Zahlung aktualisieren oder deinen Kauf finden? Oeffne die Plattform, auf der du Premium gekauft hast.",
+        "billingDescription": "Möchtest du kündigen, die Zahlung aktualisieren oder deinen Kauf finden? Öffne die Plattform, auf der du Premium gekauft hast.",
-        "cancelOrManageSubscription": "Abo kuendigen oder verwalten",
+        "cancelOrManageSubscription": "Abo kündigen oder verwalten",
-        "cancellationSupportEmailSubject": "Hilfe beim Kuendigen meines Premium-Abos",
+        "cancellationSupportEmailSubject": "Hilfe beim Kündigen meines Premium-Abos",
-        "cancellationSupportEmailBody": "Hallo, ich brauche Hilfe beim Kuendigen oder Verwalten meines Premium-Abos.\n\nKonto-E-Mail: {email}\nLizenz: {license}"
+        "cancellationSupportEmailBody": "Hallo, ich brauche Hilfe beim Kündigen oder Verwalten meines Premium-Abos.\n\nKonto-E-Mail: {email}\nLizenz: {license}"
📝 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
"deleteDataOrAccount": "Daten oder Konto loeschen",
"billingTitle": "Abrechnung & Support",
"billingDescription": "Moechtest du kuendigen, die Zahlung aktualisieren oder deinen Kauf finden? Oeffne die Plattform, auf der du Premium gekauft hast.",
"cancelOrManageSubscription": "Abo kuendigen oder verwalten",
"cancellationSupportEmailSubject": "Hilfe beim Kuendigen meines Premium-Abos",
"cancellationSupportEmailBody": "Hallo, ich brauche Hilfe beim Kuendigen oder Verwalten meines Premium-Abos.\n\nKonto-E-Mail: {email}\nLizenz: {license}"
"deleteDataOrAccount": "Daten oder Konto löschen",
"billingTitle": "Abrechnung & Support",
"billingDescription": "Möchtest du kündigen, die Zahlung aktualisieren oder deinen Kauf finden? Öffne die Plattform, auf der du Premium gekauft hast.",
"cancelOrManageSubscription": "Abo kündigen oder verwalten",
"cancellationSupportEmailSubject": "Hilfe beim Kündigen meines Premium-Abos",
"cancellationSupportEmailBody": "Hallo, ich brauche Hilfe beim Kündigen oder Verwalten meines Premium-Abos.\n\nKonto-E-Mail: {email}\nLizenz: {license}"
🤖 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 `@i18n/locales/de.json` around lines 202 - 207, Replace the ASCII
transliterations with proper German diacritics and ß for the specified locale
keys: update "deleteDataOrAccount", "billingTitle", "billingDescription",
"cancelOrManageSubscription", "cancellationSupportEmailSubject", and
"cancellationSupportEmailBody" to use "löschen", "Möchtest", "kündigen",
"Abrechnung & Support" etc.; ensure the email body also uses "Konto‑E-Mail" and
"Lizenz" with correct diacritics and preserves the {email}/{license}
placeholders and newline escapes.

Comment thread i18n/locales/es.json Outdated
Comment thread i18n/locales/fr.json Outdated
Comment thread i18n/locales/pt.json Outdated
Comment thread i18n/locales/tr.json Outdated
Comment thread test/assets/async-save-queue.test.ts Outdated
Comment on lines +22 to +47
describe('createLatestAsyncQueue', () => {
it('serializes writes and collapses queued payloads to the latest value', async () => {
const firstSave = deferred()
const savedPayloads: string[] = []
const queue = createLatestAsyncQueue(async (payload: string) => {
savedPayloads.push(payload)

if (payload === 'first') {
await firstSave.promise
}
})

const first = queue('first')
const second = queue('second')
const third = queue('third')

await Promise.resolve()

expect(savedPayloads).toEqual(['first'])

firstSave.resolve()
await Promise.all([first, second, third])

expect(savedPayloads).toEqual(['first', 'third'])
})
})
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial | ⚡ Quick win

Add a rejection-path regression test for queue continuity

Current coverage only validates the success path. Please add a case where save('first') rejects while a newer payload is queued, and assert the latest payload still gets flushed.

🤖 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 `@test/assets/async-save-queue.test.ts` around lines 22 - 47, Add a new test
case exercising the rejection path for createLatestAsyncQueue: create a deferred
(firstSave) and savedPayloads array, call createLatestAsyncQueue with a handler
that pushes payload to savedPayloads and awaits/throws from firstSave when
payload === 'first', then invoke queue('first') storing the returned promise in
`first`, queue a newer payload(s) like 'second'/'third', ensure you await a
microtick (await Promise.resolve()) to let the first handler run, then reject
firstSave to simulate failure and consume the rejection (e.g. call
first.catch(()=>{}) or use Promise.allSettled) so it doesn't crash the test,
await the remaining queue promises, and assert savedPayloads includes the first
entry and that the latest payload (e.g. 'third') was flushed (expect
savedPayloads toEqual ['first', 'third']); reference createLatestAsyncQueue,
deferred, firstSave, savedPayloads, queue, and first in the test.

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.

Caution

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

⚠️ Outside diff range comments (1)
app/pages/premium/dashboard.vue (1)

73-83: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Fix mailto: subject/body encoding in cancellationSupportEmailHref

URLSearchParams.toString() uses + for spaces, which can lead to literal + characters in mailto: subject/body (instead of spaces) per RFC 6068. Encode the subject/body values with encodeURIComponent when building the mailto: query.

🛠️ Proposed fix
   const cancellationSupportEmailHref = computed(() => {
-    const params = new URLSearchParams({
-      subject: t('pages.premium.dashboard.cancellationSupportEmailSubject'),
-      body: t('pages.premium.dashboard.cancellationSupportEmailBody', {
-        email: email.value || '?',
-        license: license.value || '?'
-      })
-    })
-
-    return `mailto:${project.email}?${params.toString()}`
+    const subject = t('pages.premium.dashboard.cancellationSupportEmailSubject')
+    const body = t('pages.premium.dashboard.cancellationSupportEmailBody', {
+      email: email.value || '?',
+      license: license.value || '?'
+    })
+
+    return `mailto:${project.email}?subject=${encodeURIComponent(subject)}&body=${encodeURIComponent(body)}`
   })
🤖 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 `@app/pages/premium/dashboard.vue` around lines 73 - 83, The mailto link
currently uses URLSearchParams which turns spaces into '+' (per
application/x-www-form-urlencoded) causing literal '+' in mail clients; update
cancellationSupportEmailHref to build the query manually and encode subject and
body with encodeURIComponent: compute the subject via
t('pages.premium.dashboard.cancellationSupportEmailSubject') and the body via
t('pages.premium.dashboard.cancellationSupportEmailBody', { email: email.value
|| '?', license: license.value || '?' }) then create the href as
`mailto:${project.email}?subject=${encodeURIComponent(subject)}&body=${encodeURIComponent(body)}`
(replace the URLSearchParams construction inside the computed
cancellationSupportEmailHref).
🤖 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.

Outside diff comments:
In `@app/pages/premium/dashboard.vue`:
- Around line 73-83: The mailto link currently uses URLSearchParams which turns
spaces into '+' (per application/x-www-form-urlencoded) causing literal '+' in
mail clients; update cancellationSupportEmailHref to build the query manually
and encode subject and body with encodeURIComponent: compute the subject via
t('pages.premium.dashboard.cancellationSupportEmailSubject') and the body via
t('pages.premium.dashboard.cancellationSupportEmailBody', { email: email.value
|| '?', license: license.value || '?' }) then create the href as
`mailto:${project.email}?subject=${encodeURIComponent(subject)}&body=${encodeURIComponent(body)}`
(replace the URLSearchParams construction inside the computed
cancellationSupportEmailHref).

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 25e83fa7-364b-44ae-a2aa-bb6c6513041b

📥 Commits

Reviewing files that changed from the base of the PR and between 15baebf and 17f3058.

📒 Files selected for processing (24)
  • app/assets/js/PremiumCloudSync.ts
  • app/composables/usePremiumCloudSync.ts
  • app/pages/premium/additional-boorus.vue
  • app/pages/premium/dashboard.vue
  • app/pages/premium/tag-collections.vue
  • i18n/locales/de.json
  • i18n/locales/en.json
  • i18n/locales/es.json
  • i18n/locales/fil.json
  • i18n/locales/fr.json
  • i18n/locales/hi.json
  • i18n/locales/id.json
  • i18n/locales/it.json
  • i18n/locales/ja.json
  • i18n/locales/ko.json
  • i18n/locales/pl.json
  • i18n/locales/pt.json
  • i18n/locales/ru.json
  • i18n/locales/th.json
  • i18n/locales/tr.json
  • i18n/locales/vi.json
  • i18n/locales/zh.json
  • test/assets/premium-cloud-sync.test.ts
  • test/i18n/premium-cloud-sync-locales.test.ts
💤 Files with no reviewable changes (13)
  • i18n/locales/hi.json
  • test/i18n/premium-cloud-sync-locales.test.ts
  • i18n/locales/id.json
  • i18n/locales/it.json
  • i18n/locales/th.json
  • i18n/locales/en.json
  • i18n/locales/ja.json
  • i18n/locales/fil.json
  • i18n/locales/zh.json
  • i18n/locales/vi.json
  • i18n/locales/pl.json
  • i18n/locales/ru.json
  • i18n/locales/ko.json
📜 Review details
🧰 Additional context used
📓 Path-based instructions (9)
test/**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

test/**/*.{ts,tsx}: Use Vitest with @nuxt/test-utils and Playwright browser mode for testing
Use @nuxt/test-utils with Playwright by calling await setup({ browser: true }) inside describe blocks for browser-based tests

Files:

  • test/assets/premium-cloud-sync.test.ts
**/*.{js,ts,tsx,vue,json}

📄 CodeRabbit inference engine (AGENTS.md)

Use Prettier for formatting with 120-char print width, no semicolons, single quotes, trailing commas removed, and single attribute per line in Vue templates

Files:

  • test/assets/premium-cloud-sync.test.ts
  • app/composables/usePremiumCloudSync.ts
  • i18n/locales/pt.json
  • i18n/locales/es.json
  • app/pages/premium/tag-collections.vue
  • i18n/locales/fr.json
  • i18n/locales/de.json
  • app/assets/js/PremiumCloudSync.ts
  • app/pages/premium/dashboard.vue
  • i18n/locales/tr.json
  • app/pages/premium/additional-boorus.vue
**/*.{ts,tsx,js}

📄 CodeRabbit inference engine (AGENTS.md)

For URL validation/parsing, prefer URL.canParse() or URL.parse() over constructor try/catch; use URL.parse() when the parsed URL object is needed

Files:

  • test/assets/premium-cloud-sync.test.ts
  • app/composables/usePremiumCloudSync.ts
  • app/assets/js/PremiumCloudSync.ts
test/**/*.ts

📄 CodeRabbit inference engine (AGENTS.md)

Import debugBrowserOptions from test/helper.ts for headful Playwright playback with slowMo in test debug mode

Files:

  • test/assets/premium-cloud-sync.test.ts
app/**/*.{vue,ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

Use Nuxt 4 with Vue 3 and TypeScript for the application framework

Files:

  • app/composables/usePremiumCloudSync.ts
  • app/pages/premium/tag-collections.vue
  • app/assets/js/PremiumCloudSync.ts
  • app/pages/premium/dashboard.vue
  • app/pages/premium/additional-boorus.vue
app/**/*.{js,ts,tsx,vue}

📄 CodeRabbit inference engine (AGENTS.md)

Use Nuxt flat ESLint configuration via @nuxt/eslint for linting

Files:

  • app/composables/usePremiumCloudSync.ts
  • app/pages/premium/tag-collections.vue
  • app/assets/js/PremiumCloudSync.ts
  • app/pages/premium/dashboard.vue
  • app/pages/premium/additional-boorus.vue
app/**/*.{ts,vue}

📄 CodeRabbit inference engine (AGENTS.md)

Use Schema.org breadcrumb item URLs as local/locale-relative URLs, not production-absolute URLs (reserve production URLs for canonicals only)

Files:

  • app/composables/usePremiumCloudSync.ts
  • app/pages/premium/tag-collections.vue
  • app/assets/js/PremiumCloudSync.ts
  • app/pages/premium/dashboard.vue
  • app/pages/premium/additional-boorus.vue
{config/i18n.ts,i18n/locales/**/*.json}

📄 CodeRabbit inference engine (AGENTS.md)

Define locales in config/i18n.ts as the single source of truth; non-default locales (ru, es, ja) get URL prefixes with mirrored route rules

Files:

  • i18n/locales/pt.json
  • i18n/locales/es.json
  • i18n/locales/fr.json
  • i18n/locales/de.json
  • i18n/locales/tr.json
app/pages/**/*.vue

📄 CodeRabbit inference engine (AGENTS.md)

Use flat bracket keys for query filters (filter[sort], filter[rating], etc.) with Vue Router's default query handling; do not re-add qs for nested route.query.filter objects unless URL contract changes

Files:

  • app/pages/premium/tag-collections.vue
  • app/pages/premium/dashboard.vue
  • app/pages/premium/additional-boorus.vue
🔇 Additional comments (14)
i18n/locales/de.json (1)

202-205: LGTM!

i18n/locales/es.json (1)

200-205: LGTM!

i18n/locales/fr.json (1)

202-203: LGTM!

Also applies to: 205-205

i18n/locales/pt.json (1)

200-201: LGTM!

Also applies to: 203-205

i18n/locales/tr.json (1)

200-200: LGTM!

Also applies to: 202-205

app/composables/usePremiumCloudSync.ts (2)

286-293: Stale realtime subscription state persists across auth changes.

clearSyncedLocalState (used by deleteCloudData/deleteAccount) resets cloudBacked but never unsubscribes the realtime handler or resets runtime.value.subscribed. After data/account deletion, subscribed stays true, so ensureRealtimeSubscription (Lines 256-263) short-circuits and a subsequent login in the same session won't re-subscribe, missing cloud updates. The subscription itself also leaks.


143-158: Verify optimistic local-update timing against init hydration and write failures.

Two interactions worth confirming for setTagCollections/setUserBooruList:

  1. Local state is set optimistically (Line 144) before canWriteToCloud()initialize() runs. If a write happens before initial hydration completes, initializeSync (Lines 88-103) overwrites tagCollections.value with the old cloud state, while the queued closure still persists nextTagCollections. This relies on a realtime echo of the user's own write to re-reconcile.
  2. On cloud-save failure, withCloudSyncFailureToast returns false but the optimistic local value is not rolled back, and a failed write emits no realtime event — leaving local and cloud diverged until reload.

Note setCustomBlockList (Lines 205-218) avoids both by updating local only after a successful cloud save. Please confirm the divergence is intentional, or consider reconciling (e.g., refreshFromCloud()) in the failure path.

app/assets/js/PremiumCloudSync.ts (3)

66-100: LGTM!


228-271: LGTM!


283-290: ⚡ Quick win

Ensure PocketBase per-user scoping for PremiumCloudSync list/delete operations

PremiumCloudSyncRepository reads and then mutates/deletes unfiltered record sets:

  • listTagCollections, listBoorus, and listBlockLists use getFullList() with no filter, and their results feed replaceRecords (update/create/delete).
  • deleteCloudData() calls deleteCollectionRecords() for posts, tag_collections, boorus, and tag_blocklists; deleteCollectionRecords() uses getFullList() with no filter and deletes every returned id.

This repo has no PocketBase rule definitions (no request.auth.id/@request.auth.id expressions found), so backend user-scoping can’t be confirmed here. Either ensure server-side rules scope these collections to user_id = @request.auth.id``, or add defensive client-side filtering.

🛡️ Defensive filter
-    const records = await collection.getFullList<{ id: string }>({ fields: 'id' })
+    const records = await collection.getFullList<{ id: string }>({
+      fields: 'id',
+      filter: `user_id = "${this.userId}"`
+    })
test/assets/premium-cloud-sync.test.ts (1)

11-20: LGTM!

Also applies to: 83-108

app/pages/premium/tag-collections.vue (1)

4-4: LGTM!

Also applies to: 33-37

app/pages/premium/additional-boorus.vue (1)

5-5: LGTM!

Also applies to: 34-37, 140-143, 178-185

app/pages/premium/dashboard.vue (1)

9-11: LGTM!

Also applies to: 106-136, 216-233

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 current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@app/assets/js/PremiumCloudSync.ts`:
- Around line 294-323: The tryBatchMutations function currently awaits
batch.send() without handling errors; update it to catch exceptions from
this.client.createBatch()/batch.send() so that if PocketBase returns a 403
(e.g., "Batch requests are not allowed" or HTTP status 403) the function returns
false to signal callers (like replaceRecords) to fall back to per-record
update/create/delete, while rethrowing or logging other errors; specifically
wrap the batch creation/send block (references: tryBatchMutations,
this.client.createBatch, batch.send) in a try/catch, detect status/code/message
indicating 403, return false for that case, and propagate non-403 errors.
🪄 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: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: ce191b6e-39a9-41a3-ba46-1aeb0d60f1eb

📥 Commits

Reviewing files that changed from the base of the PR and between 17f3058 and d5186d5.

📒 Files selected for processing (4)
  • app/assets/js/PremiumCloudSync.ts
  • app/pages/premium/additional-boorus.vue
  • app/pages/premium/tag-collections.vue
  • test/assets/premium-cloud-sync-repository.test.ts
📜 Review details
🧰 Additional context used
📓 Path-based instructions (8)
app/**/*.{vue,ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

Use Nuxt 4 with Vue 3 and TypeScript for the application framework

Files:

  • app/pages/premium/tag-collections.vue
  • app/assets/js/PremiumCloudSync.ts
  • app/pages/premium/additional-boorus.vue
**/*.{js,ts,tsx,vue,json}

📄 CodeRabbit inference engine (AGENTS.md)

Use Prettier for formatting with 120-char print width, no semicolons, single quotes, trailing commas removed, and single attribute per line in Vue templates

Files:

  • app/pages/premium/tag-collections.vue
  • test/assets/premium-cloud-sync-repository.test.ts
  • app/assets/js/PremiumCloudSync.ts
  • app/pages/premium/additional-boorus.vue
app/**/*.{js,ts,tsx,vue}

📄 CodeRabbit inference engine (AGENTS.md)

Use Nuxt flat ESLint configuration via @nuxt/eslint for linting

Files:

  • app/pages/premium/tag-collections.vue
  • app/assets/js/PremiumCloudSync.ts
  • app/pages/premium/additional-boorus.vue
app/**/*.{ts,vue}

📄 CodeRabbit inference engine (AGENTS.md)

Use Schema.org breadcrumb item URLs as local/locale-relative URLs, not production-absolute URLs (reserve production URLs for canonicals only)

Files:

  • app/pages/premium/tag-collections.vue
  • app/assets/js/PremiumCloudSync.ts
  • app/pages/premium/additional-boorus.vue
app/pages/**/*.vue

📄 CodeRabbit inference engine (AGENTS.md)

Use flat bracket keys for query filters (filter[sort], filter[rating], etc.) with Vue Router's default query handling; do not re-add qs for nested route.query.filter objects unless URL contract changes

Files:

  • app/pages/premium/tag-collections.vue
  • app/pages/premium/additional-boorus.vue
test/**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

test/**/*.{ts,tsx}: Use Vitest with @nuxt/test-utils and Playwright browser mode for testing
Use @nuxt/test-utils with Playwright by calling await setup({ browser: true }) inside describe blocks for browser-based tests

Files:

  • test/assets/premium-cloud-sync-repository.test.ts
**/*.{ts,tsx,js}

📄 CodeRabbit inference engine (AGENTS.md)

For URL validation/parsing, prefer URL.canParse() or URL.parse() over constructor try/catch; use URL.parse() when the parsed URL object is needed

Files:

  • test/assets/premium-cloud-sync-repository.test.ts
  • app/assets/js/PremiumCloudSync.ts
test/**/*.ts

📄 CodeRabbit inference engine (AGENTS.md)

Import debugBrowserOptions from test/helper.ts for headful Playwright playback with slowMo in test debug mode

Files:

  • test/assets/premium-cloud-sync-repository.test.ts
🔇 Additional comments (7)
app/assets/js/PremiumCloudSync.ts (2)

114-132: LGTM!


271-292: LGTM!

app/pages/premium/additional-boorus.vue (2)

27-45: LGTM!


88-94: LGTM!

Also applies to: 122-217

app/pages/premium/tag-collections.vue (2)

26-44: LGTM!


69-75: LGTM!

Also applies to: 104-184

test/assets/premium-cloud-sync-repository.test.ts (1)

49-72: LGTM!

Also applies to: 153-208

Comment thread app/assets/js/PremiumCloudSync.ts Outdated
Comment on lines +294 to +323
private async tryBatchMutations<TRecord extends { id: string }, TPayload extends Record<string, unknown>>(
collectionName: string,
updates: ReadonlyArray<{ record: TRecord; payload: TPayload }>,
creates: readonly TPayload[],
deletes: readonly TRecord[]
) {
const mutationCount = updates.length + creates.length + deletes.length

if (mutationCount < 2 || !this.client.createBatch) {
return false
}

const batch = this.client.createBatch()
const collection = batch.collection(collectionName)

for (const { record, payload } of updates) {
collection.update(record.id, payload)
}

for (const payload of creates) {
collection.create(payload)
}

for (const record of deletes) {
collection.delete(record.id)
}

await batch.send()
return true
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🌐 Web query:

PocketBase batch API enabled by default settings 403 batch requests not allowed maxRequests

💡 Result:

In PocketBase, the Batch API is not enabled by default [1][2]. To use it, you must explicitly enable and configure it via the Admin Dashboard under Settings > Application [1][2]. If you receive a 403 Forbidden error with a message indicating that batch requests are disabled or not allowed, it means the feature has not been enabled in your application settings [3][2]. Regarding configuration: - You can manage batch settings in the Dashboard (Settings > Application > Batch) [1]. - The maxRequests setting determines the maximum number of individual operations allowed within a single batch request [1]. - If your request fails due to hitting these limits or because the feature is disabled, the server will return a 403 status code [3][1]. For further details on structure and error codes, refer to the official PocketBase documentation on API Records and API Settings [1][2].

Citations:


Handle PocketBase Batch API 403s in tryBatchMutations so bulk ops can fall back to per-record mutations.

PocketBase Batch API is not enabled by default; if it’s disabled (or maxRequests is exceeded), the server returns 403 (e.g., “Batch requests are not allowed”). Since tryBatchMutations doesn’t catch errors from await batch.send(), replaceRecords won’t reach the per-record update/create/delete path, so bulk edits/reorders can fail while single-record edits succeed.

🛡️ Add 403-based fallback
-    await batch.send()
-    return true
+    try {
+      await batch.send()
+      return true
+    } catch (error) {
+      // PocketBase replies 403 "Batch requests are not allowed" when the Batch
+      // API is disabled; fall back to the per-record mutation path.
+      if ((error as { status?: number })?.status === 403) {
+        return false
+      }
+      throw error
+    }

Also confirm the target PocketBase instance has Batch enabled and that maxRequests covers the largest expected batch size.

📝 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
private async tryBatchMutations<TRecord extends { id: string }, TPayload extends Record<string, unknown>>(
collectionName: string,
updates: ReadonlyArray<{ record: TRecord; payload: TPayload }>,
creates: readonly TPayload[],
deletes: readonly TRecord[]
) {
const mutationCount = updates.length + creates.length + deletes.length
if (mutationCount < 2 || !this.client.createBatch) {
return false
}
const batch = this.client.createBatch()
const collection = batch.collection(collectionName)
for (const { record, payload } of updates) {
collection.update(record.id, payload)
}
for (const payload of creates) {
collection.create(payload)
}
for (const record of deletes) {
collection.delete(record.id)
}
await batch.send()
return true
}
private async tryBatchMutations<TRecord extends { id: string }, TPayload extends Record<string, unknown>>(
collectionName: string,
updates: ReadonlyArray<{ record: TRecord; payload: TPayload }>,
creates: readonly TPayload[],
deletes: readonly TRecord[]
) {
const mutationCount = updates.length + creates.length + deletes.length
if (mutationCount < 2 || !this.client.createBatch) {
return false
}
const batch = this.client.createBatch()
const collection = batch.collection(collectionName)
for (const { record, payload } of updates) {
collection.update(record.id, payload)
}
for (const payload of creates) {
collection.create(payload)
}
for (const record of deletes) {
collection.delete(record.id)
}
try {
await batch.send()
return true
} catch (error) {
// PocketBase replies 403 "Batch requests are not allowed" when the Batch
// API is disabled; fall back to the per-record mutation path.
if ((error as { status?: number })?.status === 403) {
return false
}
throw error
}
}
🤖 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 `@app/assets/js/PremiumCloudSync.ts` around lines 294 - 323, The
tryBatchMutations function currently awaits batch.send() without handling
errors; update it to catch exceptions from
this.client.createBatch()/batch.send() so that if PocketBase returns a 403
(e.g., "Batch requests are not allowed" or HTTP status 403) the function returns
false to signal callers (like replaceRecords) to fall back to per-record
update/create/delete, while rethrowing or logging other errors; specifically
wrap the batch creation/send block (references: tryBatchMutations,
this.client.createBatch, batch.send) in a try/catch, detect status/code/message
indicating 403, return false for that case, and propagate non-403 errors.

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: 2

Caution

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

⚠️ Outside diff range comments (1)
app/assets/js/PremiumCloudSync.ts (1)

341-381: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Add 403-based fallback for PocketBase batch mutations

replaceRecords routes all multi-mutation writes into sendBatchMutations(...) and immediately returns; if batch.send() throws, the per-record fallback (collection.update/create/delete) never runs. sendBatchMutations has no error handling, so a PocketBase /api/batch 403 (e.g., Batch API not enabled in Settings → Application, or otherwise forbidden) can break bulk sync entirely.

🛡️ Recommended fix to add 403-based fallback
     if (updates.length + creates.length + deletes.length >= 2) {
-      await this.sendBatchMutations(collectionName, updates, creates, deletes)
-      return
+      try {
+        await this.sendBatchMutations(collectionName, updates, creates, deletes)
+        return
+      } catch (error) {
+        // PocketBase returns 403 when Batch API is disabled; fall back to individual ops
+        const status = (error as { status?: number })?.status
+        if (status !== 403) {
+          throw error
+        }
+        // Fall through to individual operations below
+      }
     }

Also ensure Settings → Application has PocketBase Batch API enabled and batch limits are sized correctly, but keep the fallback to prevent reliability issues from misconfiguration/blocked batch requests.

🤖 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 `@app/assets/js/PremiumCloudSync.ts` around lines 341 - 381, The
sendBatchMutations function currently calls batch.send() with no error handling
so a 403 from PocketBase's /api/batch will break replaceRecords (which returns
early into sendBatchMutations); wrap the batch.send() call in a try/catch,
detect a 403 response (e.g., check the thrown error's status/code for 403), and
on 403 perform the same per-record fallbacks used in replaceRecords (call
collection.update(record.id, payload), collection.create(payload),
collection.delete(record.id) against the non-batched collection) so writes still
succeed; rethrow or propagate non-403 errors after logging so other failures are
not silently swallowed.
🤖 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 `@app/composables/usePremiumCloudSync.ts`:
- Around line 458-466: The suggested change is unnecessary—do not alter
applyBoorusFromCloud; keep its current empty-branch logic (setting
userBooruList.value = [] and toggling runtime.value.cloudBacked.boorus) because
resetUserBooruList() in useBooruList.ts already yields the correct cross-device
behavior via booruList composition; leave the applyBoorusFromCloud function and
its interactions with runtime.value.cloudBacked.boorus and userBooruList.value
as-is.

In `@app/pages/premium/saved-posts/`[domain].vue:
- Around line 51-61: The top-level await on domainsFromPocketbase (const
domainsFromPocketbase = ref(await loadSavedPostDomains())) blocks SSR; change to
initialize domainsFromPocketbase as an empty ref (e.g., ref([])) and load data
asynchronously (e.g., call loadSavedPostDomains() inside async setup, onMounted,
or kick off a Promise without awaiting to run in parallel), then assign the
result to domainsFromPocketbase when ready; locate and update the load function
(loadSavedPostDomains) and the const domainsFromPocketbase usage to avoid
top-level awaiting $pocketBase.getFullList for SavedPostDomainRecord so domain
loading becomes non-blocking for SSR.

---

Outside diff comments:
In `@app/assets/js/PremiumCloudSync.ts`:
- Around line 341-381: The sendBatchMutations function currently calls
batch.send() with no error handling so a 403 from PocketBase's /api/batch will
break replaceRecords (which returns early into sendBatchMutations); wrap the
batch.send() call in a try/catch, detect a 403 response (e.g., check the thrown
error's status/code for 403), and on 403 perform the same per-record fallbacks
used in replaceRecords (call collection.update(record.id, payload),
collection.create(payload), collection.delete(record.id) against the non-batched
collection) so writes still succeed; rethrow or propagate non-403 errors after
logging so other failures are not silently swallowed.
🪄 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: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: f2bc9299-e9f8-4aca-bc74-f3ee36e67dae

📥 Commits

Reviewing files that changed from the base of the PR and between d5186d5 and d70bd21.

📒 Files selected for processing (10)
  • AGENTS.md
  • app/assets/js/PremiumCloudSync.ts
  • app/components/pages/posts/post/PostSave.vue
  • app/composables/usePocketbase.ts
  • app/composables/usePremiumCloudSync.ts
  • app/pages/premium/dashboard.vue
  • app/pages/premium/saved-posts/[domain].vue
  • test/assets/premium-cloud-sync-repository.test.ts
  • test/assets/premium-cloud-sync.test.ts
  • test/composables/premium-cloud-sync-runtime.test.ts
💤 Files with no reviewable changes (1)
  • app/composables/usePocketbase.ts
📜 Review details
🧰 Additional context used
📓 Path-based instructions (5)
**/*.{vue,ts,tsx,js,jsx}

📄 CodeRabbit inference engine (AGENTS.md)

Do not use path prefix for component auto-imports in Nuxt; import components as <DomainSelector> not <Input/DomainSelector>

Files:

  • test/composables/premium-cloud-sync-runtime.test.ts
  • test/assets/premium-cloud-sync.test.ts
  • app/pages/premium/saved-posts/[domain].vue
  • test/assets/premium-cloud-sync-repository.test.ts
  • app/components/pages/posts/post/PostSave.vue
  • app/assets/js/PremiumCloudSync.ts
  • app/pages/premium/dashboard.vue
  • app/composables/usePremiumCloudSync.ts
**/*.{ts,tsx,js,jsx}

📄 CodeRabbit inference engine (AGENTS.md)

Use URL.canParse() or URL.parse() for URL validation/parsing instead of constructor try/catch; use URL.parse() when the parsed URL object is needed

Files:

  • test/composables/premium-cloud-sync-runtime.test.ts
  • test/assets/premium-cloud-sync.test.ts
  • test/assets/premium-cloud-sync-repository.test.ts
  • app/assets/js/PremiumCloudSync.ts
  • app/composables/usePremiumCloudSync.ts
test/**/*.{test,spec}.{ts,tsx,js,jsx}

📄 CodeRabbit inference engine (AGENTS.md)

Mock server-side API calls in tests via a test-only Nitro plugin at test/server-mocks/plugin.ts injected through nuxt.config.ts$test.nitro.plugins

Use await setup({ browser: true }) inside describe blocks for page tests with Playwright in Vitest

Import debugBrowserOptions from test/helper.ts for headful Playwright playback with slowMo during test debugging

Files:

  • test/composables/premium-cloud-sync-runtime.test.ts
  • test/assets/premium-cloud-sync.test.ts
  • test/assets/premium-cloud-sync-repository.test.ts
**/*.{vue,ts,tsx,js,jsx,json,css,scss}

📄 CodeRabbit inference engine (AGENTS.md)

Use Prettier with 120-char print width, no semicolons, single quotes, trailing commas removed, and single attribute per line in Vue templates

Files:

  • test/composables/premium-cloud-sync-runtime.test.ts
  • test/assets/premium-cloud-sync.test.ts
  • app/pages/premium/saved-posts/[domain].vue
  • test/assets/premium-cloud-sync-repository.test.ts
  • app/components/pages/posts/post/PostSave.vue
  • app/assets/js/PremiumCloudSync.ts
  • app/pages/premium/dashboard.vue
  • app/composables/usePremiumCloudSync.ts
app/pages/**/*.vue

📄 CodeRabbit inference engine (AGENTS.md)

Page-specific SEO tags (title, description) should use useSeoMeta in the page component

Files:

  • app/pages/premium/saved-posts/[domain].vue
  • app/pages/premium/dashboard.vue
🧠 Learnings (1)
📓 Common learnings
Learnt from: CR
Repo: Rule-34/App

Timestamp: 2026-05-29T15:23:32.279Z
Learning: Use `project.urls.production` for canonical URLs to point to production even when served from clone domains, as canonicals prevent duplicate content
Learnt from: CR
Repo: Rule-34/App

Timestamp: 2026-05-29T15:23:32.279Z
Learning: Do not convert breadcrumb Schema.org item URLs to `project.urls.production`; keep them local/locale-relative
Learnt from: CR
Repo: Rule-34/App

Timestamp: 2026-05-29T15:23:32.279Z
Learning: Do not re-add `qs` library for nested `route.query.filter` objects; use Vue Router's default flat bracket key query handling (`filter[sort]`, `filter[rating]`, etc.)
Learnt from: CR
Repo: Rule-34/App

Timestamp: 2026-05-29T15:23:32.279Z
Learning: Images are generated at 1x density only in webp format using the custom imgproxy provider to reduce bandwidth
Learnt from: CR
Repo: Rule-34/App

Timestamp: 2026-05-29T15:23:32.279Z
Learning: Use `nuxt/image` v2 module API for image preload priority via `preload: { fetchPriority: 'high' }` instead of patching rendered HTML in Nitro
Learnt from: CR
Repo: Rule-34/App

Timestamp: 2026-05-29T15:23:32.279Z
Learning: Do not enable the service worker; keep `selfDestroying: true` in PWA configuration
Learnt from: CR
Repo: Rule-34/App

Timestamp: 2026-05-29T15:23:32.279Z
Learning: Prefer high-impact, measurable optimizations over small rewrites; verify performance changes with production builds, relevant tests, request traces, and Lighthouse before deciding to keep the change
Learnt from: CR
Repo: Rule-34/App

Timestamp: 2026-05-29T15:23:32.279Z
Learning: Do not add app-level HTML compression unless a direct-origin deployment needs it and the change is verified with headers, byte sizes, warm TTFB, and Lighthouse
Learnt from: CR
Repo: Rule-34/App

Timestamp: 2026-05-29T15:23:32.279Z
Learning: Keep the global TanStack Vue Query plugin unless a larger measured payoff appears
Learnt from: CR
Repo: Rule-34/App

Timestamp: 2026-05-29T15:23:32.279Z
Learning: Keep `formkit/auto-animate` route-scoped unless it is used broadly; prefer local `vAutoAnimate` imports on specific pages over global registration
Learnt from: CR
Repo: Rule-34/App

Timestamp: 2026-05-29T15:23:32.279Z
Learning: Do not seed PocketBase from local defaults during initial load; only write premium cloud records after explicit user edits
Learnt from: CR
Repo: Rule-34/App

Timestamp: 2026-05-29T15:23:32.279Z
Learning: Use PocketBase batch writes for multi-record replacement/reorder operations instead of emitting one HTTP write per changed row
Learnt from: CR
Repo: Rule-34/App

Timestamp: 2026-05-29T15:23:32.279Z
Learning: For state immediately persisted, build the reordered array synchronously instead of reading VueUse `moveArrayElement()` result before it applies on `nextTick`
Learnt from: CR
Repo: Rule-34/App

Timestamp: 2026-05-29T15:23:32.279Z
Learning: SSR canonical URL fix via `server/plugins/fix-canonical-queries.ts` patches the canonical `<link>` in rendered HTML; CSR canonical URL fix via `useHead` in `app/pages/posts/[domain]/index.vue` re-applies the canonical after i18n overwrites it on hydration
Learnt from: CR
Repo: Rule-34/App

Timestamp: 2026-05-29T15:23:32.279Z
Learning: Clone the Git submodule at `app/assets/lib/rule-34-shared-resources` with `--recursive` flag
Learnt from: CR
Repo: Rule-34/App

Timestamp: 2026-05-29T15:23:32.279Z
Learning: The app calls a separate external API service at `NUXT_PUBLIC_API_URL` (default `http://localhost:8081`); the API codebase is at github.com/Rule-34/API
Learnt from: CR
Repo: Rule-34/App

Timestamp: 2026-05-29T15:23:32.279Z
Learning: Non-default locales (ru, es, ja) get URL prefixes in routing
Learnt from: CR
Repo: Rule-34/App

Timestamp: 2026-05-29T15:23:32.279Z
Learning: Validate image delivery in an environment where imgproxy can resolve the source URL for non-premium SPA navigations
🔇 Additional comments (9)
AGENTS.md (1)

153-169: LGTM!

test/composables/premium-cloud-sync-runtime.test.ts (1)

122-155: LGTM!

test/assets/premium-cloud-sync.test.ts (1)

78-79: LGTM!

app/pages/premium/saved-posts/[domain].vue (1)

398-399: LGTM!

test/assets/premium-cloud-sync-repository.test.ts (1)

9-57: LGTM!

Also applies to: 86-398

app/components/pages/posts/post/PostSave.vue (1)

13-56: LGTM!

app/assets/js/PremiumCloudSync.ts (1)

2-18: LGTM!

Also applies to: 63-204, 259-284, 406-412

app/pages/premium/dashboard.vue (1)

73-134: LGTM!

Also applies to: 186-244

app/composables/usePremiumCloudSync.ts (1)

307-325: LGTM!

Also applies to: 398-412

Comment thread app/composables/usePremiumCloudSync.ts
Comment thread app/pages/premium/saved-posts/[domain].vue
Stop applying the client-side `user_id` filter to PremiumCloudSyncRepository collection queries. Several getFullList calls no longer pass a filter (and the userFilter getter was removed), and tests were updated accordingly. Also add an authentication assertion in deleteCloudData via assertAuthenticated() to ensure a user is present before bulk-deleting cloud records. Update tests and a test description to match the new call signatures.
@AlejandroAkbal
Copy link
Copy Markdown
Member Author

@coderabbitai review

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 29, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

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