feat: sync premium data to cloud#123
Conversation
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Organization UI Review profile: ASSERTIVE Plan: Pro Run ID: 📒 Files selected for processing (3)
💤 Files with no reviewable changes (1)
📜 Recent review details🧰 Additional context used📓 Path-based instructions (5)**/*.{vue,ts,tsx}📄 CodeRabbit inference engine (AGENTS.md)
Files:
**/*.{ts,tsx,js,vue}📄 CodeRabbit inference engine (AGENTS.md)
Files:
**/*.{ts,tsx,vue}📄 CodeRabbit inference engine (AGENTS.md)
Files:
**/*.{ts,tsx,js}📄 CodeRabbit inference engine (AGENTS.md)
Files:
test/**/*.{ts,tsx}📄 CodeRabbit inference engine (AGENTS.md)
Files:
🧠 Learnings (1)📓 Common learnings🔇 Additional comments (2)
📝 WalkthroughWalkthroughMigrates 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. ChangesPremium Cloud Sync Migration
Possibly related PRs
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
✨ Simplify code
|
There was a problem hiding this comment.
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 winReject duplicate domains during edit, not just create.
createBooru()blocks duplicates, buteditBooru()can currently change an entry to another row'sdomain. That breaks the:key="booru.domain"contract in the template and makessaveBoorus()ambiguous because the cloud sync repository reconciles boorus bydomain. 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
📒 Files selected for processing (45)
app/assets/js/BackupHelper.tsapp/assets/js/PremiumCloudSync.tsapp/assets/js/oldBackup.d.tsapp/components/pages/posts/post/PostTag.vueapp/composables/useAdvertisements.tsapp/composables/usePocketbase.tsapp/composables/usePremiumCloudSync.tsapp/composables/useTagCollections.tsapp/pages/premium/additional-boorus.vueapp/pages/premium/backup.vueapp/pages/premium/dashboard.vueapp/pages/premium/sign-in.vueapp/pages/premium/tag-collections.vueapp/pages/settings.vueapp/plugins/030.pocketbase.tsapp/plugins/050.premium-cloud-sync.client.tsconfig/i18n.tsi18n/locales/de.jsoni18n/locales/en.jsoni18n/locales/es.jsoni18n/locales/fil.jsoni18n/locales/fr.jsoni18n/locales/hi.jsoni18n/locales/id.jsoni18n/locales/it.jsoni18n/locales/ja.jsoni18n/locales/ko.jsoni18n/locales/pl.jsoni18n/locales/pt.jsoni18n/locales/ru.jsoni18n/locales/th.jsoni18n/locales/tr.jsoni18n/locales/vi.jsoni18n/locales/zh.jsonserver/plugins/fix-canonical-queries.tstest/assets/premium-cloud-sync-repository.test.tstest/assets/premium-cloud-sync.test.tstest/composables/premium-cloud-sync-runtime.test.tstest/i18n/audience-locales.test.tstest/i18n/premium-cloud-sync-locales.test.tstest/pages/premium-sign-in.test.tstest/pages/premium/backup.mock-data.jsontest/server/fix-canonical-queries-pattern.test.tstest/server/path.test.tsvitest.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-utilsand Playwright browser mode for testing
Use@nuxt/test-utilswith Playwright by calling await setup({ browser: true }) inside describe blocks for browser-based tests
Files:
test/server/path.test.tstest/i18n/premium-cloud-sync-locales.test.tstest/pages/premium-sign-in.test.tstest/server/fix-canonical-queries-pattern.test.tstest/i18n/audience-locales.test.tstest/assets/premium-cloud-sync.test.tstest/composables/premium-cloud-sync-runtime.test.tstest/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.tsconfig/i18n.tsapp/plugins/050.premium-cloud-sync.client.tstest/i18n/premium-cloud-sync-locales.test.tstest/pages/premium-sign-in.test.tstest/server/fix-canonical-queries-pattern.test.tsapp/pages/premium/sign-in.vueserver/plugins/fix-canonical-queries.tsi18n/locales/hi.jsoni18n/locales/th.jsonapp/plugins/030.pocketbase.tsapp/pages/settings.vuetest/i18n/audience-locales.test.tstest/assets/premium-cloud-sync.test.tsapp/composables/usePocketbase.tsi18n/locales/pl.jsonapp/composables/useAdvertisements.tsapp/composables/usePremiumCloudSync.tsi18n/locales/zh.jsonapp/pages/premium/tag-collections.vueapp/composables/useTagCollections.tsapp/components/pages/posts/post/PostTag.vuei18n/locales/ko.jsoni18n/locales/id.jsontest/composables/premium-cloud-sync-runtime.test.tsi18n/locales/tr.jsoni18n/locales/fil.jsontest/assets/premium-cloud-sync-repository.test.tsi18n/locales/it.jsoni18n/locales/vi.jsoni18n/locales/fr.jsonapp/pages/premium/additional-boorus.vuei18n/locales/ru.jsoni18n/locales/es.jsoni18n/locales/de.jsonapp/pages/premium/dashboard.vuei18n/locales/pt.jsonapp/assets/js/PremiumCloudSync.tsi18n/locales/ja.jsoni18n/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.tsconfig/i18n.tsapp/plugins/050.premium-cloud-sync.client.tstest/i18n/premium-cloud-sync-locales.test.tstest/pages/premium-sign-in.test.tstest/server/fix-canonical-queries-pattern.test.tsserver/plugins/fix-canonical-queries.tsapp/plugins/030.pocketbase.tstest/i18n/audience-locales.test.tstest/assets/premium-cloud-sync.test.tsapp/composables/usePocketbase.tsapp/composables/useAdvertisements.tsapp/composables/usePremiumCloudSync.tsapp/composables/useTagCollections.tstest/composables/premium-cloud-sync-runtime.test.tstest/assets/premium-cloud-sync-repository.test.tsapp/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.tstest/i18n/premium-cloud-sync-locales.test.tstest/pages/premium-sign-in.test.tstest/server/fix-canonical-queries-pattern.test.tstest/i18n/audience-locales.test.tstest/assets/premium-cloud-sync.test.tstest/composables/premium-cloud-sync-runtime.test.tstest/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.tsi18n/locales/hi.jsoni18n/locales/th.jsoni18n/locales/pl.jsoni18n/locales/zh.jsoni18n/locales/ko.jsoni18n/locales/id.jsoni18n/locales/tr.jsoni18n/locales/fil.jsoni18n/locales/it.jsoni18n/locales/vi.jsoni18n/locales/fr.jsoni18n/locales/ru.jsoni18n/locales/es.jsoni18n/locales/de.jsoni18n/locales/pt.jsoni18n/locales/ja.jsoni18n/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.tsapp/pages/premium/sign-in.vueapp/plugins/030.pocketbase.tsapp/pages/settings.vueapp/composables/usePocketbase.tsapp/composables/useAdvertisements.tsapp/composables/usePremiumCloudSync.tsapp/pages/premium/tag-collections.vueapp/composables/useTagCollections.tsapp/components/pages/posts/post/PostTag.vueapp/pages/premium/additional-boorus.vueapp/pages/premium/dashboard.vueapp/assets/js/PremiumCloudSync.ts
app/**/*.{js,ts,tsx,vue}
📄 CodeRabbit inference engine (AGENTS.md)
Use Nuxt flat ESLint configuration via
@nuxt/eslintfor linting
Files:
app/plugins/050.premium-cloud-sync.client.tsapp/pages/premium/sign-in.vueapp/plugins/030.pocketbase.tsapp/pages/settings.vueapp/composables/usePocketbase.tsapp/composables/useAdvertisements.tsapp/composables/usePremiumCloudSync.tsapp/pages/premium/tag-collections.vueapp/composables/useTagCollections.tsapp/components/pages/posts/post/PostTag.vueapp/pages/premium/additional-boorus.vueapp/pages/premium/dashboard.vueapp/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.tsapp/pages/premium/sign-in.vueapp/plugins/030.pocketbase.tsapp/pages/settings.vueapp/composables/usePocketbase.tsapp/composables/useAdvertisements.tsapp/composables/usePremiumCloudSync.tsapp/pages/premium/tag-collections.vueapp/composables/useTagCollections.tsapp/components/pages/posts/post/PostTag.vueapp/pages/premium/additional-boorus.vueapp/pages/premium/dashboard.vueapp/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.vueapp/pages/settings.vueapp/pages/premium/tag-collections.vueapp/pages/premium/additional-boorus.vueapp/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/imagev2 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
There was a problem hiding this comment.
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
📒 Files selected for processing (30)
app/assets/js/AsyncSaveQueue.tsapp/assets/js/BooruList.tsapp/assets/js/PremiumCloudSync.tsapp/composables/usePremiumCloudSync.tsapp/pages/premium/additional-boorus.vueapp/pages/premium/dashboard.vueapp/pages/premium/tag-collections.vuei18n/locales/de.jsoni18n/locales/en.jsoni18n/locales/es.jsoni18n/locales/fil.jsoni18n/locales/fr.jsoni18n/locales/hi.jsoni18n/locales/id.jsoni18n/locales/it.jsoni18n/locales/ja.jsoni18n/locales/ko.jsoni18n/locales/pl.jsoni18n/locales/pt.jsoni18n/locales/ru.jsoni18n/locales/th.jsoni18n/locales/tr.jsoni18n/locales/vi.jsoni18n/locales/zh.jsontest/assets/async-save-queue.test.tstest/assets/booru-list.test.tstest/assets/premium-cloud-sync-repository.test.tstest/i18n/audience-locales.test.tstest/i18n/premium-cloud-sync-locales.test.tstest/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-utilsand Playwright browser mode for testing
Use@nuxt/test-utilswith Playwright by calling await setup({ browser: true }) inside describe blocks for browser-based tests
Files:
test/pages/premium-sign-in.test.tstest/assets/booru-list.test.tstest/i18n/audience-locales.test.tstest/assets/async-save-queue.test.tstest/i18n/premium-cloud-sync-locales.test.tstest/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.tsapp/assets/js/BooruList.tsi18n/locales/id.jsontest/assets/booru-list.test.tstest/i18n/audience-locales.test.tsapp/assets/js/AsyncSaveQueue.tsi18n/locales/pl.jsontest/assets/async-save-queue.test.tstest/i18n/premium-cloud-sync-locales.test.tsi18n/locales/vi.jsonapp/pages/premium/tag-collections.vuei18n/locales/ko.jsoni18n/locales/it.jsonapp/pages/premium/additional-boorus.vuei18n/locales/hi.jsontest/assets/premium-cloud-sync-repository.test.tsi18n/locales/th.jsoni18n/locales/en.jsoni18n/locales/de.jsoni18n/locales/fil.jsoni18n/locales/es.jsoni18n/locales/zh.jsonapp/composables/usePremiumCloudSync.tsi18n/locales/ru.jsoni18n/locales/fr.jsonapp/pages/premium/dashboard.vuei18n/locales/tr.jsoni18n/locales/pt.jsoni18n/locales/ja.jsonapp/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.tsapp/assets/js/BooruList.tstest/assets/booru-list.test.tstest/i18n/audience-locales.test.tsapp/assets/js/AsyncSaveQueue.tstest/assets/async-save-queue.test.tstest/i18n/premium-cloud-sync-locales.test.tstest/assets/premium-cloud-sync-repository.test.tsapp/composables/usePremiumCloudSync.tsapp/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.tstest/assets/booru-list.test.tstest/i18n/audience-locales.test.tstest/assets/async-save-queue.test.tstest/i18n/premium-cloud-sync-locales.test.tstest/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.tsapp/assets/js/AsyncSaveQueue.tsapp/pages/premium/tag-collections.vueapp/pages/premium/additional-boorus.vueapp/composables/usePremiumCloudSync.tsapp/pages/premium/dashboard.vueapp/assets/js/PremiumCloudSync.ts
app/**/*.{js,ts,tsx,vue}
📄 CodeRabbit inference engine (AGENTS.md)
Use Nuxt flat ESLint configuration via
@nuxt/eslintfor linting
Files:
app/assets/js/BooruList.tsapp/assets/js/AsyncSaveQueue.tsapp/pages/premium/tag-collections.vueapp/pages/premium/additional-boorus.vueapp/composables/usePremiumCloudSync.tsapp/pages/premium/dashboard.vueapp/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.tsapp/assets/js/AsyncSaveQueue.tsapp/pages/premium/tag-collections.vueapp/pages/premium/additional-boorus.vueapp/composables/usePremiumCloudSync.tsapp/pages/premium/dashboard.vueapp/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.jsoni18n/locales/pl.jsoni18n/locales/vi.jsoni18n/locales/ko.jsoni18n/locales/it.jsoni18n/locales/hi.jsoni18n/locales/th.jsoni18n/locales/en.jsoni18n/locales/de.jsoni18n/locales/fil.jsoni18n/locales/es.jsoni18n/locales/zh.jsoni18n/locales/ru.jsoni18n/locales/fr.jsoni18n/locales/tr.jsoni18n/locales/pt.jsoni18n/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.vueapp/pages/premium/additional-boorus.vueapp/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
| await save(payload) | ||
| } | ||
| } | ||
|
|
||
| return (payload: T) => { | ||
| latestPayload = payload | ||
| hasLatestPayload = true | ||
| active ??= flush().finally(() => { | ||
| active = null |
There was a problem hiding this comment.
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.
| 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.
| "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}" |
There was a problem hiding this comment.
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.
| "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.
| 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']) | ||
| }) | ||
| }) |
There was a problem hiding this comment.
🧹 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.
There was a problem hiding this comment.
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 winFix
mailto:subject/body encoding incancellationSupportEmailHref
URLSearchParams.toString()uses+for spaces, which can lead to literal+characters inmailto:subject/body (instead of spaces) per RFC 6068. Encode thesubject/bodyvalues withencodeURIComponentwhen building themailto: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
📒 Files selected for processing (24)
app/assets/js/PremiumCloudSync.tsapp/composables/usePremiumCloudSync.tsapp/pages/premium/additional-boorus.vueapp/pages/premium/dashboard.vueapp/pages/premium/tag-collections.vuei18n/locales/de.jsoni18n/locales/en.jsoni18n/locales/es.jsoni18n/locales/fil.jsoni18n/locales/fr.jsoni18n/locales/hi.jsoni18n/locales/id.jsoni18n/locales/it.jsoni18n/locales/ja.jsoni18n/locales/ko.jsoni18n/locales/pl.jsoni18n/locales/pt.jsoni18n/locales/ru.jsoni18n/locales/th.jsoni18n/locales/tr.jsoni18n/locales/vi.jsoni18n/locales/zh.jsontest/assets/premium-cloud-sync.test.tstest/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-utilsand Playwright browser mode for testing
Use@nuxt/test-utilswith 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.tsapp/composables/usePremiumCloudSync.tsi18n/locales/pt.jsoni18n/locales/es.jsonapp/pages/premium/tag-collections.vuei18n/locales/fr.jsoni18n/locales/de.jsonapp/assets/js/PremiumCloudSync.tsapp/pages/premium/dashboard.vuei18n/locales/tr.jsonapp/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.tsapp/composables/usePremiumCloudSync.tsapp/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.tsapp/pages/premium/tag-collections.vueapp/assets/js/PremiumCloudSync.tsapp/pages/premium/dashboard.vueapp/pages/premium/additional-boorus.vue
app/**/*.{js,ts,tsx,vue}
📄 CodeRabbit inference engine (AGENTS.md)
Use Nuxt flat ESLint configuration via
@nuxt/eslintfor linting
Files:
app/composables/usePremiumCloudSync.tsapp/pages/premium/tag-collections.vueapp/assets/js/PremiumCloudSync.tsapp/pages/premium/dashboard.vueapp/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.tsapp/pages/premium/tag-collections.vueapp/assets/js/PremiumCloudSync.tsapp/pages/premium/dashboard.vueapp/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.jsoni18n/locales/es.jsoni18n/locales/fr.jsoni18n/locales/de.jsoni18n/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.vueapp/pages/premium/dashboard.vueapp/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 bydeleteCloudData/deleteAccount) resetscloudBackedbut never unsubscribes the realtime handler or resetsruntime.value.subscribed. After data/account deletion,subscribedstaystrue, soensureRealtimeSubscription(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:
- Local state is set optimistically (Line 144) before
canWriteToCloud()→initialize()runs. If a write happens before initial hydration completes,initializeSync(Lines 88-103) overwritestagCollections.valuewith the old cloud state, while the queued closure still persistsnextTagCollections. This relies on a realtime echo of the user's own write to re-reconcile.- On cloud-save failure,
withCloudSyncFailureToastreturnsfalsebut 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 winEnsure PocketBase per-user scoping for PremiumCloudSync list/delete operations
PremiumCloudSyncRepositoryreads and then mutates/deletes unfiltered record sets:
listTagCollections,listBoorus, andlistBlockListsusegetFullList()with nofilter, and their results feedreplaceRecords(update/create/delete).deleteCloudData()callsdeleteCollectionRecords()forposts,tag_collections,boorus, andtag_blocklists;deleteCollectionRecords()usesgetFullList()with nofilterand deletes every returnedid.This repo has no PocketBase rule definitions (no
request.auth.id/@request.auth.idexpressions found), so backend user-scoping can’t be confirmed here. Either ensure server-side rules scope these collections touser_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
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@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
📒 Files selected for processing (4)
app/assets/js/PremiumCloudSync.tsapp/pages/premium/additional-boorus.vueapp/pages/premium/tag-collections.vuetest/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.vueapp/assets/js/PremiumCloudSync.tsapp/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.vuetest/assets/premium-cloud-sync-repository.test.tsapp/assets/js/PremiumCloudSync.tsapp/pages/premium/additional-boorus.vue
app/**/*.{js,ts,tsx,vue}
📄 CodeRabbit inference engine (AGENTS.md)
Use Nuxt flat ESLint configuration via
@nuxt/eslintfor linting
Files:
app/pages/premium/tag-collections.vueapp/assets/js/PremiumCloudSync.tsapp/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.vueapp/assets/js/PremiumCloudSync.tsapp/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.vueapp/pages/premium/additional-boorus.vue
test/**/*.{ts,tsx}
📄 CodeRabbit inference engine (AGENTS.md)
test/**/*.{ts,tsx}: Use Vitest with@nuxt/test-utilsand Playwright browser mode for testing
Use@nuxt/test-utilswith 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.tsapp/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
| 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 | ||
| } |
There was a problem hiding this comment.
🧩 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:
- 1: https://pocketbase.io/docs/api-settings/
- 2: https://pocketbase.io/docs/api-records/
- 3: https://www.mintlify.com/pocketbase/pocketbase/api/batch
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.
| 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.
There was a problem hiding this comment.
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 winAdd 403-based fallback for PocketBase batch mutations
replaceRecordsroutes all multi-mutation writes intosendBatchMutations(...)and immediatelyreturns; ifbatch.send()throws, the per-record fallback (collection.update/create/delete) never runs.sendBatchMutationshas no error handling, so a PocketBase/api/batch403 (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
📒 Files selected for processing (10)
AGENTS.mdapp/assets/js/PremiumCloudSync.tsapp/components/pages/posts/post/PostSave.vueapp/composables/usePocketbase.tsapp/composables/usePremiumCloudSync.tsapp/pages/premium/dashboard.vueapp/pages/premium/saved-posts/[domain].vuetest/assets/premium-cloud-sync-repository.test.tstest/assets/premium-cloud-sync.test.tstest/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.tstest/assets/premium-cloud-sync.test.tsapp/pages/premium/saved-posts/[domain].vuetest/assets/premium-cloud-sync-repository.test.tsapp/components/pages/posts/post/PostSave.vueapp/assets/js/PremiumCloudSync.tsapp/pages/premium/dashboard.vueapp/composables/usePremiumCloudSync.ts
**/*.{ts,tsx,js,jsx}
📄 CodeRabbit inference engine (AGENTS.md)
Use
URL.canParse()orURL.parse()for URL validation/parsing instead of constructortry/catch; useURL.parse()when the parsed URL object is needed
Files:
test/composables/premium-cloud-sync-runtime.test.tstest/assets/premium-cloud-sync.test.tstest/assets/premium-cloud-sync-repository.test.tsapp/assets/js/PremiumCloudSync.tsapp/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.tsinjected throughnuxt.config.ts→$test.nitro.pluginsUse
await setup({ browser: true })insidedescribeblocks for page tests with Playwright in VitestImport
debugBrowserOptionsfromtest/helper.tsfor headful Playwright playback with slowMo during test debugging
Files:
test/composables/premium-cloud-sync-runtime.test.tstest/assets/premium-cloud-sync.test.tstest/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.tstest/assets/premium-cloud-sync.test.tsapp/pages/premium/saved-posts/[domain].vuetest/assets/premium-cloud-sync-repository.test.tsapp/components/pages/posts/post/PostSave.vueapp/assets/js/PremiumCloudSync.tsapp/pages/premium/dashboard.vueapp/composables/usePremiumCloudSync.ts
app/pages/**/*.vue
📄 CodeRabbit inference engine (AGENTS.md)
Page-specific SEO tags (title, description) should use
useSeoMetain the page component
Files:
app/pages/premium/saved-posts/[domain].vueapp/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
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.
|
@coderabbitai review |
✅ Actions performedReview triggered.
|
Summary by CodeRabbit
New Features
Removed Features
Updates
Tests
Docs