Skip to content

Refactor premium cloud sync repositories - #124

Merged
AlejandroAkbal merged 4 commits into
mainfrom
codex/premium-cloud-repositories
Jun 4, 2026
Merged

AlejandroAkbal merged 4 commits into
mainfrom
codex/premium-cloud-repositories

Conversation

@AlejandroAkbal

@AlejandroAkbal AlejandroAkbal commented Jun 2, 2026 •

Copy link
Copy Markdown
Member

Summary

  • Move Premium cloud persistence behind PremiumCloudRepository and stop using localStorage for cloud-owned saved posts, tag collections, custom boorus, and custom blocklists.
  • Keep /premium/saved-posts on the hardcoded production domain and remove saved-post domain discovery.
  • Replace brittle source/translation tests with browser-level Premium cloud coverage and durable locale parity checks.

Test Plan

  • corepack pnpm format:check
  • corepack pnpm lint
  • corepack pnpm typecheck
  • corepack pnpm test:typecheck
  • corepack pnpm test
  • corepack pnpm build
  • git diff --check

Notes

  • Local pnpm check wrapper is blocked on this machine because nested bare pnpm resolves to Homebrew pnpm 11.5.0 while the repo pins 11.2.2; the same gate components were run directly through corepack pnpm.

Summary by CodeRabbit

  • New Features

    • Central Premium Cloud repository, repository-backed saved-posts flow, and safer auth-cookie parsing.
  • Refactor

    • UI now reads saved-post state from the cloud-sync composable; several client-only localStorage stores moved to centralized runtime state. Saved-posts page uses a single production domain and redirects legacy domain routes.
  • Documentation

    • Clarified testing/import guidance and revised premium auth scoping advice (reload-backed transitions).
  • Tests

    • Added integration suites and richer PocketBase mocks; removed obsolete test files.
  • Chores

    • Pinned Vite dev dependency.

@coderabbitai

coderabbitai Bot commented Jun 2, 2026 •

Copy link
Copy Markdown
Contributor

Review Change Stack

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: ca09f918-e133-4d1d-86b9-66fdaab5a0e0

📥 Commits

Reviewing files that changed from the base of the PR and between 37b7637 and 14957bf.

📒 Files selected for processing (3)
  • app/composables/usePremiumCloudSync.ts
  • app/repositories/PremiumCloudRepository.ts
  • test/assets/premium-cloud-sync.test.ts
📜 Recent review details
🧰 Additional context used
📓 Path-based instructions (6)
**/*.{ts,tsx,vue}

📄 CodeRabbit inference engine (AGENTS.md)

Use Nuxt 4 with Vue 3 and TypeScript for the SSR application with Nitro server

Use PocketBase batch writes for multi-record replacement/reorder operations instead of emitting one HTTP write per changed row

Files:

  • test/assets/premium-cloud-sync.test.ts
  • app/repositories/PremiumCloudRepository.ts
  • app/composables/usePremiumCloudSync.ts
test/**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

Use Vitest with @nuxt/test-utils and Playwright browser mode for testing

Keep plain Vitest suites importable through relative paths or direct app paths instead of relying on Nuxt runtime alias resolution

Files:

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

📄 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/assets/premium-cloud-sync.test.ts
  • app/repositories/PremiumCloudRepository.ts
  • app/composables/usePremiumCloudSync.ts
**/*.{js,ts,tsx,vue}

📄 CodeRabbit inference engine (AGENTS.md)

Use Nuxt flat ESLint configuration via @nuxt/eslint

Files:

  • test/assets/premium-cloud-sync.test.ts
  • app/repositories/PremiumCloudRepository.ts
  • app/composables/usePremiumCloudSync.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 object is needed

Files:

  • test/assets/premium-cloud-sync.test.ts
  • app/repositories/PremiumCloudRepository.ts
  • app/composables/usePremiumCloudSync.ts
**/*.ts

📄 CodeRabbit inference engine (AGENTS.md)

Custom router scroll behavior skips scroll-to-top when only the page query param changes between same-route navigations

Files:

  • test/assets/premium-cloud-sync.test.ts
  • app/repositories/PremiumCloudRepository.ts
  • app/composables/usePremiumCloudSync.ts
🧠 Learnings (4)
📓 Common learnings
Learnt from: CR
Repo: Rule-34/App PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-05-30T19:57:20.193Z
Learning: Saved posts use the same premium cloud realtime runtime as tag collections, custom boorus, and the custom blocklist
Learnt from: CR
Repo: Rule-34/App

Timestamp: 2026-06-03T09:42:42.851Z
Learning: Use canonical URLs pointing to production (https://r34.app/…) from project.urls.production even when served from clone domains
Learnt from: CR
Repo: Rule-34/App

Timestamp: 2026-06-03T09:42:42.851Z
Learning: Keep Schema.org breadcrumb item URLs local/locale-relative; do not convert breadcrumb items to absolute production URLs
Learnt from: CR
Repo: Rule-34/App

Timestamp: 2026-06-03T09:42:42.851Z
Learning: Prefer high-impact, measurable performance optimizations over small rewrites; verify with production builds, tests, request traces, and Lighthouse before keeping changes
Learnt from: CR
Repo: Rule-34/App

Timestamp: 2026-06-03T09:42:42.851Z
Learning: Keep Cloudflare Brotli compression in mind; do not add app-level HTML compression unless direct-origin deployment needs it and is verified with headers, byte sizes, warm TTFB, and Lighthouse
Learnt from: CR
Repo: Rule-34/App

Timestamp: 2026-06-03T09:42:42.851Z
Learning: Keep the global TanStack Vue Query plugin unless a larger measured payoff appears
Learnt from: CR
Repo: Rule-34/App

Timestamp: 2026-06-03T09:42:42.851Z
Learning: Empty cloud state means no user-authored cloud override; do not seed PocketBase from local defaults during initial load, only write after explicit user edits
Learnt from: CR
Repo: Rule-34/App

Timestamp: 2026-06-03T09:42:42.851Z
Learning: Git submodule at app/assets/lib/rule-34-shared-resources must be cloned with --recursive flag
Learnt from: CR
Repo: Rule-34/App

Timestamp: 2026-06-03T09:42:42.851Z
Learning: The app calls a separate API service at NUXT_PUBLIC_API_URL (default http://localhost:8081); API codebase is at github.com/Rule-34/API
📚 Learning: 2026-05-30T19:57:20.193Z
Learnt from: CR
Repo: Rule-34/App PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-05-30T19:57:20.193Z
Learning: Saved posts use the same premium cloud realtime runtime as tag collections, custom boorus, and the custom blocklist

Applied to files:

  • app/repositories/PremiumCloudRepository.ts
  • app/composables/usePremiumCloudSync.ts
📚 Learning: 2026-05-30T19:57:20.193Z
Learnt from: CR
Repo: Rule-34/App PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-05-30T19:57:20.193Z
Learning: Applies to **/*.{ts,tsx,vue} : Use PocketBase batch writes for multi-record replacement/reorder operations instead of one HTTP write per changed row

Applied to files:

  • app/repositories/PremiumCloudRepository.ts
📚 Learning: 2026-05-30T19:57:20.193Z
Learnt from: CR
Repo: Rule-34/App PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-05-30T19:57:20.193Z
Learning: Only write premium cloud records after explicit user edits, not from local defaults during initial load

Applied to files:

  • app/repositories/PremiumCloudRepository.ts
  • app/composables/usePremiumCloudSync.ts
🔇 Additional comments (12)
app/repositories/PremiumCloudRepository.ts (5)

77-130: LGTM!

The createLatestAsyncQueue implementation correctly tracks generations for invalidation, gates error capture on isCurrent(), and exposes invalidate() to clear stale payloads.


397-450: LGTM!

The replaceRecords reconciliation correctly matches payloads to existing records by key, prevents record reuse via usedRecordIds, falls back to position-based matching, and batches mutations when multiple operations are needed.


502-511: LGTM!

The clientFilter fallback uses JSON.stringify to safely escape parameter values when the PocketBase filter method is unavailable, which is adequate for test/mock scenarios.


568-592: LGTM!

The cloudValuesEqual function provides correct deep equality for arrays and plain objects, with Object.is handling primitives and edge cases like null, undefined, and NaN.


238-271: LGTM!

The implementation correctly addresses all previous review concerns:

  • loadSavedPostsPage returns real pagination metadata (no skipTotal)
  • saveCustomBlockList reconciles to a single record by deleting extras
  • subscribeToPremiumCloudChanges subscribes sequentially and cleans up on failure
  • deleteCollectionRecords uses batch mutations instead of individual deletes

Also applies to: 273-278, 304-323, 346-371, 489-496

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

83-107: LGTM!

The test correctly verifies that the queue resolves when the latest payload succeeds even after an earlier save fails, and that only the latest payload is persisted.


109-134: LGTM!

The new test properly validates the invalidate() behavior by checking that isCurrent() returns false after invalidation, preventing stale mutations from updating local state.

app/composables/usePremiumCloudSync.ts (5)

46-51: LGTM!

The module-level queues cleverly use createLatestAsyncQueue with a higher-order function pattern: the payload is itself a save function that receives isCurrent, allowing callers to gate both remote saves and local state updates on staleness.


123-146: LGTM!

The setTagCollections flow correctly gates on isCurrent() both before the remote save and before updating local state, preventing stale writes from corrupting local state after invalidation.


161-184: LGTM!

setUserBooruList follows the same correct pattern as setTagCollections, gating remote saves and local state updates on isCurrent().


317-327: LGTM!

clearSyncedLocalState correctly invalidates both queues before clearing state, ensuring any in-flight saves are marked stale and won't update local state after the clear completes.


438-446: LGTM!

getSavedPost correctly returns undefined when the runtime isn't initialized, ensuring consumers (like PostSave.vue) don't act on stale or missing data.


📝 Walkthrough

Walkthrough

This PR centralizes premium cloud sync into PremiumCloudRepository, migrates premium runtime state to Nuxt useState, rewires PostSave and the saved-posts page to use the repository, adds PocketBase Playwright mocks and new integration/unit tests, and updates docs and a devDependency pin.

Changes

Premium Cloud Sync Repository Refactoring

Layer / File(s) Summary
PremiumCloudRepository implementation
app/repositories/PremiumCloudRepository.ts
New repository with collection constants, exported types, createLatestAsyncQueue, paginated saved-post loading, save/delete helpers, replaceRecords reconciliation, batch mutation and realtime subscription helpers, and payload mappers/aliases.
Composable state refactor to useState
app/composables/usePocketbase.ts, app/composables/usePremiumCloudSync.ts, app/composables/useBooruList.ts, app/composables/useBlockLists.ts, app/composables/useTagCollections.ts
Composables now use Nuxt useState atoms for cross-SSR state, removed client-only useLocalStorage refs. usePremiumCloudSync exposes isInitialized, savedPostList, and getSavedPost, and cloud-write helpers return false when writes are disallowed.
Component and page integration with repository
app/components/pages/posts/post/PostSave.vue, app/pages/premium/saved-posts/[domain].vue, app/plugins/030.pocketbase.ts, app/assets/js/PremiumCloudSync.ts
PostSave.vue uses usePremiumCloudSync().getSavedPost() with initialization gating and background init. Saved-posts page delegates fetching to PremiumCloudRepository.loadSavedPostsPage(), uses the production hostname as the booru, and redirects legacy domains to production. PocketBase plugin adds parsePocketBaseAuthCookie. Legacy assets module now re-exports the repository.
Test infrastructure and PocketBase mocking
test/pocketbase-mock.ts, test/assets/premium-cloud-sync-repository.test.ts, test/pages/premium-cloud.test.ts, test/assets/premium-cloud-sync.test.ts
New Playwright PocketBase mock with auth cookie helper, in-memory collections, paginated GET handling, SSE simulation, and mutations. Repository tests extended for pagination, duplicate blocklist handling, clearTagCollections batch deletes, subscription cleanup, and createLatestAsyncQueue invalidation. New integration tests cover sign-in and premium saved-post flows.
Configuration, locales, and docs
package.json, AGENTS.md, test/i18n/audience-locales.test.ts
Pin vite devDependency to 7.3.3. AGENTS.md updated with guidance on Vitest import paths and premium auth reload-backed state clearing. Locale tests refactored to derive non-default locales and deduplicate placeholders.

Possibly Related PRs

  • Rule-34/App#123: Continues premium cloud sync work and previously introduced related cloud-sync architecture and helpers.
  • Rule-34/App#121: Overlaps saved-posts page changes and domain/filter handling.
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title clearly summarizes the main change: refactoring the premium cloud sync system to use a repository pattern.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

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

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch codex/premium-cloud-repositories
✨ Simplify code
  • Create PR with simplified code
  • Commit simplified code in branch codex/premium-cloud-repositories

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 ESLint

If the error stems from missing dependencies, add them to the package.json file. For unrecoverable errors (e.g., due to private dependencies), disable the tool in the CodeRabbit configuration.

app/composables/usePremiumCloudSync.ts

ESLint skipped: missing config or dependency (missing-dependency). The ESLint configuration references a package that is not available in the sandbox.

app/repositories/PremiumCloudRepository.ts

ESLint skipped: the ESLint configuration for this file references a package that is not available in the sandbox.

test/assets/premium-cloud-sync.test.ts

ESLint skipped: the ESLint configuration for this file references a package that is not available in the sandbox.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

@coderabbitai coderabbitai Bot left a comment

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.

Actionable comments posted: 8

🤖 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/components/pages/posts/post/PostSave.vue`:
- Around line 12-25: The save button can be clicked before the cloud-synced list
finishes loading because you call initialize() in onMounted and use
getSavedPost() synchronously; change startup to use initializeInBackground() for
fire-and-forget (so it won't produce unhandled rejections) and ensure the UI
gates actions until initialization completes: expose the initialization state
from usePremiumCloudSync (or use the existing initialize promise) and make
postInSavedList/isPostSaved respect that flag so the save button is disabled or
onClick is no-op until initialization finishes; update usages of getSavedPost,
postInSavedList and isPostSaved to check the init-complete flag (or preload
initialize higher up) before allowing savePost/deleteSavedPost to run.

In `@app/composables/usePremiumCloudSync.ts`:
- Line 60: The savedPostList useState is keyed globally causing different users
to share cached saved-post IDs and leading getSavedPost()/deleteSavedPost() to
operate on stale data; update the useState key in the composable (where
savedPostList is created) to include the current user's unique id (e.g.,
useState<ISimplePocketbasePost[]>(`premium-saved-post-list-${userId}`, ...)) so
the cache is user-scoped, ensure initialize() uses the same user-scoped key when
loading, and update clearAuthBoundRuntimeState() to also reset/clear the owner
id and clear the user-scoped savedPostList so switches between accounts cannot
reuse the previous user's cached entries.

In `@app/pages/premium/saved-posts/`[domain].vue:
- Around line 51-60: The current logic constructs booruList (computed booruList)
only with project.urls.production.hostname and then later throws when route
param [domain] isn't found; instead, update the route handling that uses
booruList (the computed booruList and the code path that currently throws for
missing booru) to treat unknown legacy domains as the production hostname: when
the incoming route param domain does not match any entry in booruList,
programmatically redirect (or replace) to the same route but with domain =
project.urls.production.hostname while preserving the existing query object
(route.query), rather than throwing; ensure this redirect uses the same
navigation method used elsewhere (router.replace or similar) so bookmarks/old
links seamlessly resolve to the production booru.

In `@app/repositories/PremiumCloudRepository.ts`:
- Around line 456-462: The deleteCollectionRecords method currently iterates
records and calls collection.delete for each row, causing N sequential HTTP
deletes and partial failures; change it to use the repository's batch delete
capability: fetch ids via collection.getFullList (as already done), then send
them in batched calls (using the existing batch API on this.client or
collection.batch/deleteBatch method) instead of per-record await
collection.delete, handling errors per-batch and retry/backoff as needed to
avoid partial clears; update deleteCollectionRecords to build id chunks and call
the batch delete helper so
clearTagCollections/deleteCloudData/clearBoorus/deleteAccount use efficient
batched deletes.
- Around line 327-337: The current subscribeToPremiumCloudChanges uses
Promise.all which rejects on the first subscribe failure and leaks earlier
active subscriptions; change the logic to subscribe incrementally (call
this.subscribeToCollection for each premiumCloudCollections entry one-by-one or
via Promise.allSettled), collect successful unsubscribe handles as you go, and
if any subscribe fails perform cleanup by calling all collected unsubscribe
handles (use Promise.all to await those) before rethrowing the error; reference
subscribeToPremiumCloudChanges and subscribeToCollection when making these
changes and ensure the returned teardown always returns a function that awaits
all active unsubscribe handles.
- Around line 292-303: saveCustomBlockList currently only updates records[0] and
leaves any extra rows; modify it to enforce a single-record invariant: call
listBlockLists(), build payload via customBlockListPayloadFromState(this.userId,
tags), get collection via
this.client.collection(premiumCloudCollections.blocklists), then if
records.length > 0 update the first record with collection.update(record.id,
payload) and delete all other records (loop over records.slice(1) and call
collection.delete(id)); if no existing record, create one with
collection.create(payload); ensure in both branches any extra records are
removed so only one record remains.
- Around line 240-257: In PremiumCloudRepository (the method calling
this.client.collection(premiumCloudCollections.posts).getList), remove
skipTotal: true from the getList options so PocketBase will populate
response.totalItems/response.totalPages, or if you must keep skipTotal then do
not use response.totalItems/response.totalPages in the returned meta (derive
totals or set them to null/undefined); update the return meta block
(meta.items_count, meta.total_items, meta.current_page, meta.total_pages,
meta.items_per_page) accordingly so it no longer exposes bogus -1 values when
skipTotal is set.

In `@test/i18n/audience-locales.test.ts`:
- Line 5: Replace the recomputed non-default locale list in the test with the
canonical exported value: import and use prefixedLocaleCodes from config/i18n.ts
instead of recalculating via locales.filter(...).map(...); update the test to
reference prefixedLocaleCodes (rather than locales and defaultLocale) so the
test stays in sync with the source definition.
🪄 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: 3c8fcaf2-78aa-45ef-ac74-9b22b26030c1

📥 Commits

Reviewing files that changed from the base of the PR and between 70ea08c and cc1bc4e.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (21)
  • AGENTS.md
  • app/assets/js/PremiumCloudSync.ts
  • app/components/pages/posts/post/PostSave.vue
  • app/composables/useBlockLists.ts
  • app/composables/useBooruList.ts
  • app/composables/usePocketbase.ts
  • app/composables/usePremiumCloudSync.ts
  • app/composables/useTagCollections.ts
  • app/pages/premium/saved-posts/[domain].vue
  • app/plugins/030.pocketbase.ts
  • app/repositories/PremiumCloudRepository.ts
  • package.json
  • test/assets/premium-cloud-sync-repository.test.ts
  • test/assets/premium-cloud-sync.test.ts
  • test/composables/lazy-toast-runtime.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-cloud.test.ts
  • test/pages/premium-sign-in.test.ts
  • test/pocketbase-mock.ts
💤 Files with no reviewable changes (4)
  • test/pages/premium-sign-in.test.ts
  • test/composables/premium-cloud-sync-runtime.test.ts
  • test/composables/lazy-toast-runtime.test.ts
  • test/i18n/premium-cloud-sync-locales.test.ts
📜 Review details
🧰 Additional context used
📓 Path-based instructions (11)
**/*.{js,ts,tsx,vue,json}

📄 CodeRabbit inference engine (AGENTS.md)

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

Files:

  • package.json
  • test/i18n/audience-locales.test.ts
  • app/components/pages/posts/post/PostSave.vue
  • app/composables/useBlockLists.ts
  • test/assets/premium-cloud-sync.test.ts
  • test/pages/premium-cloud.test.ts
  • test/assets/premium-cloud-sync-repository.test.ts
  • app/plugins/030.pocketbase.ts
  • app/composables/useTagCollections.ts
  • test/pocketbase-mock.ts
  • app/composables/useBooruList.ts
  • app/assets/js/PremiumCloudSync.ts
  • app/pages/premium/saved-posts/[domain].vue
  • app/repositories/PremiumCloudRepository.ts
  • app/composables/usePremiumCloudSync.ts
  • app/composables/usePocketbase.ts
**/*.{vue,ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

Components in Nuxt are auto-imported without path prefix; import them as <ComponentName> not <Folder/ComponentName>

Files:

  • test/i18n/audience-locales.test.ts
  • app/components/pages/posts/post/PostSave.vue
  • app/composables/useBlockLists.ts
  • test/assets/premium-cloud-sync.test.ts
  • test/pages/premium-cloud.test.ts
  • test/assets/premium-cloud-sync-repository.test.ts
  • app/plugins/030.pocketbase.ts
  • app/composables/useTagCollections.ts
  • test/pocketbase-mock.ts
  • app/composables/useBooruList.ts
  • app/assets/js/PremiumCloudSync.ts
  • app/pages/premium/saved-posts/[domain].vue
  • app/repositories/PremiumCloudRepository.ts
  • app/composables/usePremiumCloudSync.ts
  • app/composables/usePocketbase.ts
**/*.{ts,tsx,vue}

📄 CodeRabbit inference engine (AGENTS.md)

Query filters use flat bracket keys (filter[sort], filter[rating], etc.); do not re-add qs for nested query objects unless the URL contract changes

Use useLazyToast() to lazy-load vue-sonner; the first toast must wait for ClientToaster to mount (plain nextTick() can fire before the component loads)

Empty cloud state means 'no user-authored cloud override'; do not seed PocketBase from local defaults during initial load; only write after explicit user edits

PocketBase realtime subscriptions echo local writes; separate write requests from realtime-triggered refreshes when debugging sync performance

Files:

  • test/i18n/audience-locales.test.ts
  • app/components/pages/posts/post/PostSave.vue
  • app/composables/useBlockLists.ts
  • test/assets/premium-cloud-sync.test.ts
  • test/pages/premium-cloud.test.ts
  • test/assets/premium-cloud-sync-repository.test.ts
  • app/plugins/030.pocketbase.ts
  • app/composables/useTagCollections.ts
  • test/pocketbase-mock.ts
  • app/composables/useBooruList.ts
  • app/assets/js/PremiumCloudSync.ts
  • app/pages/premium/saved-posts/[domain].vue
  • app/repositories/PremiumCloudRepository.ts
  • app/composables/usePremiumCloudSync.ts
  • app/composables/usePocketbase.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 object is needed

Files:

  • test/i18n/audience-locales.test.ts
  • app/composables/useBlockLists.ts
  • test/assets/premium-cloud-sync.test.ts
  • test/pages/premium-cloud.test.ts
  • test/assets/premium-cloud-sync-repository.test.ts
  • app/plugins/030.pocketbase.ts
  • app/composables/useTagCollections.ts
  • test/pocketbase-mock.ts
  • app/composables/useBooruList.ts
  • app/assets/js/PremiumCloudSync.ts
  • app/repositories/PremiumCloudRepository.ts
  • app/composables/usePremiumCloudSync.ts
  • app/composables/usePocketbase.ts
**/*.ts

📄 CodeRabbit inference engine (AGENTS.md)

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

Files:

  • test/i18n/audience-locales.test.ts
  • app/composables/useBlockLists.ts
  • test/assets/premium-cloud-sync.test.ts
  • test/pages/premium-cloud.test.ts
  • test/assets/premium-cloud-sync-repository.test.ts
  • app/plugins/030.pocketbase.ts
  • app/composables/useTagCollections.ts
  • test/pocketbase-mock.ts
  • app/composables/useBooruList.ts
  • app/assets/js/PremiumCloudSync.ts
  • app/repositories/PremiumCloudRepository.ts
  • app/composables/usePremiumCloudSync.ts
  • app/composables/usePocketbase.ts
test/**/*.{test,spec}.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

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

Use debugBrowserOptions imported from test/helper.ts for headful Playwright playback with slowMo in tests

Plain Vitest suites that import app modules directly do not get Nuxt's runtime alias resolution; keep repository/pure modules importable through relative paths

For premium and PocketBase flows, use a real authenticated browser session for final investigation when possible; unit tests prove behavior but real-browser traces catch request bursts and state changes

Files:

  • test/i18n/audience-locales.test.ts
  • test/assets/premium-cloud-sync.test.ts
  • test/pages/premium-cloud.test.ts
  • test/assets/premium-cloud-sync-repository.test.ts
**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

Use PocketBase batch writes for multi-record replacement/reorder operations instead of one HTTP write per changed row

Files:

  • test/i18n/audience-locales.test.ts
  • app/composables/useBlockLists.ts
  • test/assets/premium-cloud-sync.test.ts
  • test/pages/premium-cloud.test.ts
  • test/assets/premium-cloud-sync-repository.test.ts
  • app/plugins/030.pocketbase.ts
  • app/composables/useTagCollections.ts
  • test/pocketbase-mock.ts
  • app/composables/useBooruList.ts
  • app/assets/js/PremiumCloudSync.ts
  • app/repositories/PremiumCloudRepository.ts
  • app/composables/usePremiumCloudSync.ts
  • app/composables/usePocketbase.ts
**/*.vue

📄 CodeRabbit inference engine (AGENTS.md)

OG image URLs must be absolute; build them dynamically with useRequestURL().origin on the server only

Canonical URLs must point to production (https://r34.app/…) even when served from clone domains; use project.urls.production for canonicals

Schema.org breadcrumb item URLs should stay local/locale-relative; do not convert breadcrumb URLs to project.urls.production

Use <NuxtImg> with the custom imgproxy provider; images are deliberately generated at 1x density only (webp format)

Keep @formkit/auto-animate route-scoped unless used broadly; use local vAutoAnimate imports on premium CSR pages to avoid including the runtime in first-load entry

For state immediately persisted after VueUse moveArrayElement(), build the reordered array synchronously instead of reading it on nextTick

Files:

  • app/components/pages/posts/post/PostSave.vue
  • app/pages/premium/saved-posts/[domain].vue
app/pages/**/*.vue

📄 CodeRabbit inference engine (AGENTS.md)

Use useSeoMeta for page-specific SEO tags (title, description) in the page component

Files:

  • app/pages/premium/saved-posts/[domain].vue
app/pages/**/premium/**/*.vue

📄 CodeRabbit inference engine (AGENTS.md)

Saved posts use the same premium cloud realtime runtime as tag collections, custom boorus, and custom blocklist; empty saved-post state means no saved posts

Files:

  • app/pages/premium/saved-posts/[domain].vue
app/pages/premium/saved-posts/**/*.vue

📄 CodeRabbit inference engine (AGENTS.md)

In /premium/saved-posts, unsaving a post should not remove the row or prune cached infinite-query data; keep the viewer stable and let the save button update immediately

Files:

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

Timestamp: 2026-06-02T09:09:27.992Z
Learning: Prefer high-impact, measurable performance optimizations; verify changes with production builds, tests, request traces, and Lighthouse before committing
Learnt from: CR
Repo: Rule-34/App

Timestamp: 2026-06-02T09:09:27.992Z
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-06-02T09:09:27.992Z
Learning: Keep the global TanStack Vue Query plugin unless a larger measured payoff appears; a route-scoped experiment did not provide sufficient gains
🔇 Additional comments (5)
package.json (1)

85-85: LGTM!

AGENTS.md (1)

167-168: LGTM!

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

9-51: LGTM!

test/pocketbase-mock.ts (1)

64-176: LGTM!

test/pages/premium-cloud.test.ts (1)

1-122: LGTM!

Comment thread app/components/pages/posts/post/PostSave.vue Outdated
Comment thread app/composables/usePremiumCloudSync.ts
Comment thread app/pages/premium/saved-posts/[domain].vue Outdated
Comment thread app/repositories/PremiumCloudRepository.ts
Comment thread app/repositories/PremiumCloudRepository.ts
Comment thread app/repositories/PremiumCloudRepository.ts
Comment thread app/repositories/PremiumCloudRepository.ts
Comment thread test/i18n/audience-locales.test.ts Outdated

@coderabbitai coderabbitai Bot left a comment

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.

Caution

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

⚠️ Outside diff range comments (3)
app/pages/premium/saved-posts/[domain].vue (1)

358-375: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

User-scope the saved-post query cache.

This query key is route-scoped but not account-scoped. After a logout/login or account switch, TanStack Query can reuse the previous user's cached /premium/saved-posts pages for up to five minutes, which exposes another account's saved posts until the refetch lands. Include the PocketBase user id in the key, or clear all ['saved-posts'] queries during auth-bound cleanup.

🤖 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/saved-posts/`[domain].vue around lines 358 - 375, The query
key for useInfiniteQuery (the queryKey array built around 'saved-posts',
selectedBooru, selectedTags, selectedFilters, selectedPage.value,
postsPerPage.value) is missing account scoping so cached saved-posts can leak
between users; fix by including the current PocketBase user id (or other auth
UID) in that queryKey (e.g., add currentUser.id or authUserId to the array used
by useInfiniteQuery) or, alternatively, add a cleanup step that clears
queryClient.removeQueries(['saved-posts']) on logout/account switch to ensure
per-account isolation.
test/assets/premium-cloud-sync-repository.test.ts (1)

30-39: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Make the fake getList() honor page and perPage.

Line 38 always returns the full collection, so these repository paging tests won't catch off-by-one or next-page regressions.

Proposed fix
           getList: vi.fn(async (page = 1, perPage = 30, ...args: unknown[]) => {
             calls.push({ collection: name, method: 'getList', args: [page, perPage, ...args] })
+            const start = (page - 1) * perPage
+            const end = start + perPage

             return {
               page,
               perPage,
               totalItems: records[name].length,
               totalPages: records[name].length ? Math.ceil(records[name].length / perPage) : 0,
-              items: records[name]
+              items: records[name].slice(start, end)
             }
           }),
🤖 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/premium-cloud-sync-repository.test.ts` around lines 30 - 39, The
fake getList implementation returns the entire collection regardless of
page/perPage; update the getList mock (the vi.fn for getList) to compute
totalItems = records[name].length, totalPages = totalItems ?
Math.ceil(totalItems / perPage) : 0 and set items = records[name].slice((page -
1) * perPage, (page - 1) * perPage + perPage) so the returned items reflect the
requested page and perPage; keep the existing calls.push({ collection: name,
method: 'getList', args: [...] }) and default parameters (page = 1, perPage =
30).
app/composables/usePremiumCloudSync.ts (1)

171-176: ⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

Scope or cancel the latest-only write queues per authenticated user.

These queued callbacks outlive the account that enqueued them. If user A leaves a pending setTagCollections()/setUserBooruList() flush behind and user B signs in before it drains, the callback will run against repository.value for user B with user A's payload. That can leak or overwrite premium cloud data across accounts.

As per coding guidelines, "Auth-bound premium useState caches must be explicitly scoped or owner-guarded by PocketBase user id to prevent exposing one account's data during auth switches."

Also applies to: 200-205, 366-421

🤖 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/composables/usePremiumCloudSync.ts` around lines 171 - 176, Queued
cloud-write callbacks (e.g., those created via saveTagCollectionsToCloud used by
setTagCollections/setUserBooruList) are not bound to the authenticating user and
can run after an auth switch, causing cross-account data leakage; modify
saveTagCollectionsToCloud (and the similar queues at lines noted) to scope or
cancel per-user by capturing the current PocketBase user id when enqueuing and:
1) store that owner id with the queued callback, 2) on execution compare it to
the current repository.value/auth user id and skip/abort if they differ, and 3)
cancel any pending callbacks for the previous user when sign-out or a new
sign-in occurs (or when enqueuing a new owner-scoped "latest-only" flush) so
repository.value, runtime.value.cloudBacked, tryEnsureRealtimeSubscription, and
similar methods only run against the owning user’s context.
🤖 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/composables/usePremiumCloudSync.ts`:
- Around line 171-176: Queued cloud-write callbacks (e.g., those created via
saveTagCollectionsToCloud used by setTagCollections/setUserBooruList) are not
bound to the authenticating user and can run after an auth switch, causing
cross-account data leakage; modify saveTagCollectionsToCloud (and the similar
queues at lines noted) to scope or cancel per-user by capturing the current
PocketBase user id when enqueuing and: 1) store that owner id with the queued
callback, 2) on execution compare it to the current repository.value/auth user
id and skip/abort if they differ, and 3) cancel any pending callbacks for the
previous user when sign-out or a new sign-in occurs (or when enqueuing a new
owner-scoped "latest-only" flush) so repository.value,
runtime.value.cloudBacked, tryEnsureRealtimeSubscription, and similar methods
only run against the owning user’s context.

In `@app/pages/premium/saved-posts/`[domain].vue:
- Around line 358-375: The query key for useInfiniteQuery (the queryKey array
built around 'saved-posts', selectedBooru, selectedTags, selectedFilters,
selectedPage.value, postsPerPage.value) is missing account scoping so cached
saved-posts can leak between users; fix by including the current PocketBase user
id (or other auth UID) in that queryKey (e.g., add currentUser.id or authUserId
to the array used by useInfiniteQuery) or, alternatively, add a cleanup step
that clears queryClient.removeQueries(['saved-posts']) on logout/account switch
to ensure per-account isolation.

In `@test/assets/premium-cloud-sync-repository.test.ts`:
- Around line 30-39: The fake getList implementation returns the entire
collection regardless of page/perPage; update the getList mock (the vi.fn for
getList) to compute totalItems = records[name].length, totalPages = totalItems ?
Math.ceil(totalItems / perPage) : 0 and set items = records[name].slice((page -
1) * perPage, (page - 1) * perPage + perPage) so the returned items reflect the
requested page and perPage; keep the existing calls.push({ collection: name,
method: 'getList', args: [...] }) and default parameters (page = 1, perPage =
30).

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: c0330d5d-3b7d-4ddb-81f0-acb98cf227e9

📥 Commits

Reviewing files that changed from the base of the PR and between cc1bc4e and 1836c3c.

📒 Files selected for processing (9)
  • AGENTS.md
  • app/components/pages/posts/post/PostSave.vue
  • app/composables/usePremiumCloudSync.ts
  • app/pages/premium/saved-posts/[domain].vue
  • app/repositories/PremiumCloudRepository.ts
  • test/assets/premium-cloud-sync-repository.test.ts
  • test/i18n/audience-locales.test.ts
  • test/pages/premium-cloud.test.ts
  • test/pocketbase-mock.ts
📜 Review details
🧰 Additional context used
📓 Path-based instructions (14)
**/*.{ts,tsx,vue}

📄 CodeRabbit inference engine (AGENTS.md)

Use Nuxt 4 (SSR, Nitro server) with Vue 3 + TypeScript

Use useLazyToast() to lazy-load vue-sonner and render ClientToaster; wait for ClientToaster to mount before calling toast.* with proper async handling

Files:

  • app/components/pages/posts/post/PostSave.vue
  • test/assets/premium-cloud-sync-repository.test.ts
  • test/i18n/audience-locales.test.ts
  • test/pocketbase-mock.ts
  • app/pages/premium/saved-posts/[domain].vue
  • test/pages/premium-cloud.test.ts
  • app/composables/usePremiumCloudSync.ts
  • app/repositories/PremiumCloudRepository.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, single attribute per line in Vue templates

Files:

  • app/components/pages/posts/post/PostSave.vue
  • test/assets/premium-cloud-sync-repository.test.ts
  • test/i18n/audience-locales.test.ts
  • test/pocketbase-mock.ts
  • app/pages/premium/saved-posts/[domain].vue
  • test/pages/premium-cloud.test.ts
  • app/composables/usePremiumCloudSync.ts
  • app/repositories/PremiumCloudRepository.ts
**/*.{js,ts,tsx,vue}

📄 CodeRabbit inference engine (AGENTS.md)

Use Nuxt flat ESLint via @nuxt/eslint for linting

Files:

  • app/components/pages/posts/post/PostSave.vue
  • test/assets/premium-cloud-sync-repository.test.ts
  • test/i18n/audience-locales.test.ts
  • test/pocketbase-mock.ts
  • app/pages/premium/saved-posts/[domain].vue
  • test/pages/premium-cloud.test.ts
  • app/composables/usePremiumCloudSync.ts
  • app/repositories/PremiumCloudRepository.ts
**/*.vue

📄 CodeRabbit inference engine (AGENTS.md)

Register custom imgproxy provider for and deliberately generate images at 1x density only (webp format) to reduce bandwidth

Canonical URLs must point to production (https://r34.app/…) even when served from clone domains; use project.urls.production for canonicals

Use @nuxt/image v2 module API for image preload priority with preload: { fetchPriority: 'high' } instead of patching rendered HTML in Nitro

Keep @formkit/auto-animate route-scoped (local vAutoAnimate imports) unless used broadly to reduce entry bundle size

Files:

  • app/components/pages/posts/post/PostSave.vue
  • app/pages/premium/saved-posts/[domain].vue
{app/components/**/*.vue,app/pages/**/*.vue,app/layouts/**/*.vue}

📄 CodeRabbit inference engine (AGENTS.md)

Components are auto-imported without path prefix (pathPrefix: false); import as not <Input/DomainSelector>

Files:

  • app/components/pages/posts/post/PostSave.vue
  • app/pages/premium/saved-posts/[domain].vue
test/**/*.{test,spec}.{ts,tsx,vue}

📄 CodeRabbit inference engine (AGENTS.md)

Use Vitest + @nuxt/test-utils with Playwright browser mode for testing

Files:

  • test/assets/premium-cloud-sync-repository.test.ts
  • test/i18n/audience-locales.test.ts
  • test/pages/premium-cloud.test.ts
**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

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

Files:

  • test/assets/premium-cloud-sync-repository.test.ts
  • test/i18n/audience-locales.test.ts
  • test/pocketbase-mock.ts
  • test/pages/premium-cloud.test.ts
  • app/composables/usePremiumCloudSync.ts
  • app/repositories/PremiumCloudRepository.ts
test/**/*.{test,spec}.ts

📄 CodeRabbit inference engine (AGENTS.md)

Tests use @nuxt/test-utils with Playwright inside describe blocks calling await setup({ browser: true })

Debug mode: import debugBrowserOptions from test/helper.ts for headful playback with slowMo

Plain Vitest suites that import app modules directly do not get Nuxt runtime alias resolution; keep repository/pure modules importable through relative paths or import from app path

Files:

  • test/assets/premium-cloud-sync-repository.test.ts
  • test/i18n/audience-locales.test.ts
  • test/pages/premium-cloud.test.ts
{app/pages/**/*.vue,app/composables/**/*.ts}

📄 CodeRabbit inference engine (AGENTS.md)

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

Files:

  • app/pages/premium/saved-posts/[domain].vue
  • app/composables/usePremiumCloudSync.ts
{app/composables/**/*[Pp]remium*.ts,app/pages/premium/**/*.vue}

📄 CodeRabbit inference engine (AGENTS.md)

Auth-bound premium useState caches must be explicitly scoped or owner-guarded by PocketBase user id to prevent exposing one account's data during auth switches

Files:

  • app/pages/premium/saved-posts/[domain].vue
  • app/composables/usePremiumCloudSync.ts
app/pages/premium/saved-posts/**/*.vue

📄 CodeRabbit inference engine (AGENTS.md)

In /premium/saved-posts, unsaving a post should not remove the row or prune cached infinite-query data to keep the viewer stable; save button updates immediately with row disappearing on reload

Files:

  • app/pages/premium/saved-posts/[domain].vue
{app/composables/**/*.ts,app/pages/premium/**/*.vue,server/api/**/*.ts}

📄 CodeRabbit inference engine (AGENTS.md)

Use PocketBase batch writes for multi-record replacement/reorder operations instead of one HTTP write per changed row

Files:

  • app/pages/premium/saved-posts/[domain].vue
  • app/composables/usePremiumCloudSync.ts
{app/pages/premium/**/*.vue,app/composables/**/*.ts}

📄 CodeRabbit inference engine (AGENTS.md)

VueUse moveArrayElement() applies array move on nextTick; for state immediately persisted, build the reordered array synchronously instead of reading before VueUse applies move

Files:

  • app/pages/premium/saved-posts/[domain].vue
  • app/composables/usePremiumCloudSync.ts
app/composables/**/*[Cc]loud*.ts

📄 CodeRabbit inference engine (AGENTS.md)

Empty cloud state means no user-authored cloud override; do not seed PocketBase from local defaults during initial load, only after explicit user edits

Files:

  • app/composables/usePremiumCloudSync.ts
🧠 Learnings (1)
📓 Common learnings
Learnt from: CR
Repo: Rule-34/App

Timestamp: 2026-06-02T10:34:15.916Z
Learning: Require Node 24 (package.json engines) and pnpm ≥ 11.2.2 (packageManager pins expected version)
Learnt from: CR
Repo: Rule-34/App

Timestamp: 2026-06-02T10:34:15.916Z
Learning: Clone repository with --recursive flag to include Git submodule at app/assets/lib/rule-34-shared-resources
Learnt from: CR
Repo: Rule-34/App

Timestamp: 2026-06-02T10:34:15.916Z
Learning: Keep HOST=127.0.0.1 in local .env for pnpm dev to avoid IPv6 resolution issues
Learnt from: CR
Repo: Rule-34/App

Timestamp: 2026-06-02T10:34:15.916Z
Learning: Non-premium SPA navigations keep direct image path; validate image delivery in an environment where imgproxy can resolve the source URL
Learnt from: CR
Repo: Rule-34/App

Timestamp: 2026-06-02T10:34:15.916Z
Learning: Prefer high-impact, measurable optimizations over small rewrites; verify substantial performance changes with production build, tests, request traces, and Lighthouse
Learnt from: CR
Repo: Rule-34/App

Timestamp: 2026-06-02T10:34:15.916Z
Learning: Do not add app-level HTML compression unless direct-origin deployment needs it; verify with headers, byte sizes, warm TTFB, and Lighthouse (Cloudflare Brotli-compresses responses)
Learnt from: CR
Repo: Rule-34/App

Timestamp: 2026-06-02T10:34:15.916Z
Learning: Service worker is intentionally disabled (selfDestroying: true); do not add service worker logic
Learnt from: CR
Repo: Rule-34/App

Timestamp: 2026-06-02T10:34:15.916Z
Learning: Source map uploads only happen in production Docker builds (requires SENTRY_ORG, SENTRY_PROJECT, SENTRY_AUTH_TOKEN build args)
Learnt from: CR
Repo: Rule-34/App

Timestamp: 2026-06-02T10:34:15.916Z
Learning: For premium and PocketBase flows, use a real authenticated browser session for final investigation when possible; real-browser traces catch request bursts and auth flows unit tests miss
Learnt from: CR
Repo: Rule-34/App

Timestamp: 2026-06-02T10:34:15.916Z
Learning: PocketBase realtime subscriptions echo local writes; inspect real network traces to separate write requests from realtime-triggered refreshes
Learnt from: CR
Repo: Rule-34/App

Timestamp: 2026-06-02T10:34:15.916Z
Learning: Saved posts use the same premium cloud realtime runtime as tag collections, custom boorus, and custom blocklist; empty cloud state means no saved posts
Learnt from: CR
Repo: Rule-34/App

Timestamp: 2026-06-02T10:34:15.916Z
Learning: Docker production build: multi-stage build uses pnpm install --frozen-lockfile; production stage copies only .output/ (no node_modules)
Learnt from: CR
Repo: Rule-34/App

Timestamp: 2026-06-02T10:34:15.916Z
Learning: Docker production build: set SENTRY_UPLOAD_SOURCE_MAPS=false to skip source map uploads if SENTRY_ORG, SENTRY_PROJECT, SENTRY_AUTH_TOKEN are not available
Learnt from: CR
Repo: Rule-34/App

Timestamp: 2026-06-02T10:34:15.916Z
Learning: NITRO_PRESET build arg selects the deployment target in Docker production build
🔇 Additional comments (1)
test/i18n/audience-locales.test.ts (1)

3-5: LGTM!

@coderabbitai coderabbitai Bot left a comment

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.

Caution

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

⚠️ Outside diff range comments (2)
app/composables/usePremiumCloudSync.ts (1)

46-48: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Invalidate queued cloud writes when sync state is cleared.

These queues live at module scope, and their callbacks mutate tagCollections.value / userBooruList.value after awaiting the repository write. clearSyncedLocalState() resets the atoms but never invalidates queued or in-flight callbacks, so a late save can repopulate cleared data after deleteCloudData() / deleteAccount(). If auth changes first, that queued callback can also run against the next session because it resolves repository.value at execution time. Add a generation/token check before and after the awaited write, and bump it inside clearSyncedLocalState().

Also applies to: 125-130, 154-159, 295-303

🤖 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/composables/usePremiumCloudSync.ts` around lines 46 - 48, The
module-scoped queues (saveTagCollectionsToCloud, saveBoorusToCloud created via
createLatestAsyncQueue) can run after clearSyncedLocalState and repopulate
tagCollections.value / userBooruList.value or run against a new session because
they resolve repository.value at execution time; modify each queued callback to
capture a local generation/token (e.g., read a new numeric generation variable)
before the awaited repository write, then re-check that the generation/token is
unchanged after the await before mutating tagCollections.value or
userBooruList.value, and increment/reset that generation inside
clearSyncedLocalState(); apply the same pattern to the other module-scoped
queues mentioned around the other ranges so all in-flight/queued saves are
invalidated when state is cleared.
app/repositories/PremiumCloudRepository.ts (1)

82-99: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Only reject the queue when the latest payload fails.

flush() keeps the first error for the whole drain. If save A fails, save B is queued, and B succeeds, the returned promise still rejects with A's error. Downstream callers then show a failure toast even though the latest state was persisted and local state was already updated. Clear/replace the stored error after a later successful save so the promise reflects the outcome of the newest payload.

Suggested change
   async function flush() {
     let error: unknown

     while (hasLatestPayload) {
       const payload = latestPayload as T
       latestPayload = undefined
       hasLatestPayload = false

       try {
         await save(payload)
+        error = undefined
       } catch (caughtError) {
-        error ??= caughtError
+        error = caughtError
       }
     }

     if (error) {
       throw error

Also applies to: 102-109

🤖 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/repositories/PremiumCloudRepository.ts` around lines 82 - 99, The flush()
loop currently preserves the first failure via error ??= caughtError which
causes flush to reject even if a subsequent save succeeds; change the logic so
that after a successful await save(payload) you clear any prior error (set error
= undefined) and on failures overwrite the stored error (use error =
caughtError) so the promise outcome reflects the newest payload; apply the same
fix to the analogous block referenced around lines 102-109 (replace the
conditional accumulation with overwrite on catch and clear on successful save).
🤖 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/composables/usePremiumCloudSync.ts`:
- Around line 46-48: The module-scoped queues (saveTagCollectionsToCloud,
saveBoorusToCloud created via createLatestAsyncQueue) can run after
clearSyncedLocalState and repopulate tagCollections.value / userBooruList.value
or run against a new session because they resolve repository.value at execution
time; modify each queued callback to capture a local generation/token (e.g.,
read a new numeric generation variable) before the awaited repository write,
then re-check that the generation/token is unchanged after the await before
mutating tagCollections.value or userBooruList.value, and increment/reset that
generation inside clearSyncedLocalState(); apply the same pattern to the other
module-scoped queues mentioned around the other ranges so all in-flight/queued
saves are invalidated when state is cleared.

In `@app/repositories/PremiumCloudRepository.ts`:
- Around line 82-99: The flush() loop currently preserves the first failure via
error ??= caughtError which causes flush to reject even if a subsequent save
succeeds; change the logic so that after a successful await save(payload) you
clear any prior error (set error = undefined) and on failures overwrite the
stored error (use error = caughtError) so the promise outcome reflects the
newest payload; apply the same fix to the analogous block referenced around
lines 102-109 (replace the conditional accumulation with overwrite on catch and
clear on successful save).

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 95fd0b3f-ed84-402d-bc17-dd50ab3b1347

📥 Commits

Reviewing files that changed from the base of the PR and between 1836c3c and 37b7637.

📒 Files selected for processing (6)
  • AGENTS.md
  • app/components/pages/posts/post/PostSave.vue
  • app/composables/usePremiumCloudSync.ts
  • app/pages/premium/saved-posts/[domain].vue
  • app/repositories/PremiumCloudRepository.ts
  • test/assets/premium-cloud-sync-repository.test.ts
📜 Review details
🧰 Additional context used
📓 Path-based instructions (8)
**/{app,server}/**/*.{vue,ts}

📄 CodeRabbit inference engine (AGENTS.md)

Use TypeScript in Vue components, Nuxt pages, composables, and server API routes

Files:

  • app/components/pages/posts/post/PostSave.vue
  • app/pages/premium/saved-posts/[domain].vue
  • app/repositories/PremiumCloudRepository.ts
  • app/composables/usePremiumCloudSync.ts
**/*.vue

📄 CodeRabbit inference engine (AGENTS.md)

Import Vue components without path prefix using flat auto-import naming (e.g., <DomainSelector> not <Input/DomainSelector>)

Use @nuxt/image v2 module API for image preload priority with preload: { fetchPriority: 'high' } instead of patching rendered HTML in Nitro

Files:

  • app/components/pages/posts/post/PostSave.vue
  • app/pages/premium/saved-posts/[domain].vue
**/*.{vue,ts,tsx,js,jsx}

📄 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

Use ESLint flat config via @nuxt/eslint for linting

Files:

  • app/components/pages/posts/post/PostSave.vue
  • test/assets/premium-cloud-sync-repository.test.ts
  • app/pages/premium/saved-posts/[domain].vue
  • app/repositories/PremiumCloudRepository.ts
  • app/composables/usePremiumCloudSync.ts
**/{app,server}/**/*.{ts,tsx,vue}

📄 CodeRabbit inference engine (AGENTS.md)

Use flat bracket keys for query filters (e.g., filter[sort], filter[rating]) with Vue Router's default query handling, not qs library

Use useLazyToast() to lazy-load vue-sonner and wait for ClientToaster to mount before calling toast.* methods

Use PocketBase batch writes for multi-record replacement and reorder operations instead of one HTTP write per changed row

Build reordered arrays synchronously in state that is immediately persisted, not relying on VueUse moveArrayElement() nextTick behavior

Files:

  • app/components/pages/posts/post/PostSave.vue
  • app/pages/premium/saved-posts/[domain].vue
  • app/repositories/PremiumCloudRepository.ts
  • app/composables/usePremiumCloudSync.ts
test/**/*.{test,spec}.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

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

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

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

Keep repository and pure modules importable through relative paths in plain Vitest suites that do not have Nuxt runtime alias resolution

Use plain Vitest suites that import app modules directly; keep repository modules importable through relative paths since they do not have Nuxt runtime alias resolution

Files:

  • test/assets/premium-cloud-sync-repository.test.ts
app/pages/**/*.vue

📄 CodeRabbit inference engine (AGENTS.md)

Use useSeoMeta for page-specific tags (title, description) in page components

Files:

  • app/pages/premium/saved-posts/[domain].vue
app/pages/premium/**/*.vue

📄 CodeRabbit inference engine (AGENTS.md)

Keep @formkit/auto-animate route-scoped unless used broadly; prefer local vAutoAnimate imports on premium CSR pages

Files:

  • app/pages/premium/saved-posts/[domain].vue
**/{app,server}/**/*.{ts,tsx,js}

📄 CodeRabbit inference engine (AGENTS.md)

Use URL.canParse() or URL.parse() for URL validation and parsing instead of constructor try/catch

Files:

  • app/repositories/PremiumCloudRepository.ts
  • app/composables/usePremiumCloudSync.ts
🧠 Learnings (8)
📓 Common learnings
Learnt from: CR
Repo: Rule-34/App PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-05-30T19:57:20.193Z
Learning: Saved posts use the same premium cloud realtime runtime as tag collections, custom boorus, and the custom blocklist
Learnt from: CR
Repo: Rule-34/App

Timestamp: 2026-06-03T09:04:47.258Z
Learning: Use canonical URLs pointing to production (https://r34.app/…) via project.urls.production for SEO, even when served from clone domains
Learnt from: CR
Repo: Rule-34/App

Timestamp: 2026-06-03T09:04:47.258Z
Learning: Keep breadcrumb item URLs locale-relative and local, not absolute production URLs
Learnt from: CR
Repo: Rule-34/App

Timestamp: 2026-06-03T09:04:47.258Z
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-06-03T09:04:47.258Z
Learning: Do not remove rows or prune cached infinite-query data when unsaving a post; keep the viewer stable for scroll/progress preservation
Learnt from: CR
Repo: Rule-34/App

Timestamp: 2026-06-03T09:04:47.258Z
Learning: Clone the repository with --recursive to include Git submodule at app/assets/lib/rule-34-shared-resources
Learnt from: CR
Repo: Rule-34/App

Timestamp: 2026-06-03T09:04:47.258Z
Learning: Prefer high-impact, measurable optimizations over small rewrites; keep battle-tested dependencies unless a clear, measured payoff exists
Learnt from: CR
Repo: Rule-34/App

Timestamp: 2026-06-03T09:04:47.258Z
Learning: Verify production build changes with relevant tests, request traces, and Lighthouse before deciding changes are worth keeping
Learnt from: CR
Repo: Rule-34/App

Timestamp: 2026-06-03T09:04:47.258Z
Learning: Do not add app-level HTML compression unless direct-origin deployment needs it and change is verified with headers, byte sizes, warm TTFB, and Lighthouse
Learnt from: CR
Repo: Rule-34/App

Timestamp: 2026-06-03T09:04:47.258Z
Learning: Keep the global TanStack Vue Query plugin unless larger measured payoff appears
Learnt from: CR
Repo: Rule-34/App

Timestamp: 2026-06-03T09:04:47.258Z
Learning: Do not add service worker logic; the service worker is intentionally disabled with selfDestroying: true
Learnt from: CR
Repo: Rule-34/App

Timestamp: 2026-06-03T09:04:47.258Z
Learning: Use real authenticated browser sessions for premium and PocketBase flows in final investigation when possible, not just unit tests
Learnt from: CR
Repo: Rule-34/App

Timestamp: 2026-06-03T09:04:47.258Z
Learning: Validate image delivery in an environment where imgproxy can resolve the source URL, especially for non-premium SPA navigations
📚 Learning: 2026-05-30T19:57:20.193Z
Learnt from: CR
Repo: Rule-34/App PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-05-30T19:57:20.193Z
Learning: Saved posts use the same premium cloud realtime runtime as tag collections, custom boorus, and the custom blocklist

Applied to files:

  • app/components/pages/posts/post/PostSave.vue
  • AGENTS.md
  • app/pages/premium/saved-posts/[domain].vue
  • app/repositories/PremiumCloudRepository.ts
  • app/composables/usePremiumCloudSync.ts
📚 Learning: 2026-05-30T19:57:20.193Z
Learnt from: CR
Repo: Rule-34/App PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-05-30T19:57:20.193Z
Learning: Only write premium cloud records after explicit user edits, not from local defaults during initial load

Applied to files:

  • AGENTS.md
  • app/repositories/PremiumCloudRepository.ts
  • app/composables/usePremiumCloudSync.ts
📚 Learning: 2026-05-30T19:57:20.193Z
Learnt from: CR
Repo: Rule-34/App PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-05-30T19:57:20.193Z
Learning: Empty cloud state means no user-authored cloud override; do not seed PocketBase from local defaults during initial load

Applied to files:

  • AGENTS.md
  • app/composables/usePremiumCloudSync.ts
📚 Learning: 2026-05-30T19:57:20.193Z
Learnt from: CR
Repo: Rule-34/App PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-05-30T19:57:20.193Z
Learning: Applies to app/pages/posts/[domain]/index.vue : Fix canonical query parameters via `server/plugins/fix-canonical-queries.ts` for SSR and `useHead` in `app/pages/posts/[domain]/index.vue` for CSR until upstream i18n module fixes `canonicalQueries`

Applied to files:

  • app/pages/premium/saved-posts/[domain].vue
📚 Learning: 2026-05-30T19:57:20.193Z
Learnt from: CR
Repo: Rule-34/App PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-05-30T19:57:20.193Z
Learning: Applies to server/middleware/redirect-to-posts.get.ts : Legacy redirect: `server/middleware/redirect-to-posts.get.ts` redirects `/?domain=x&page=…&tags=…` → `/posts/x?page=…&tags=…` (301)

Applied to files:

  • app/pages/premium/saved-posts/[domain].vue
📚 Learning: 2026-05-30T19:57:20.193Z
Learnt from: CR
Repo: Rule-34/App PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-05-30T19:57:20.193Z
Learning: Applies to app/components/**/*.vue : Components are auto-imported without path prefix — use `<DomainSelector>` not `<Input/DomainSelector>`

Applied to files:

  • app/pages/premium/saved-posts/[domain].vue
📚 Learning: 2026-05-30T19:57:20.193Z
Learnt from: CR
Repo: Rule-34/App PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-05-30T19:57:20.193Z
Learning: Applies to **/*.{ts,tsx,vue} : Use PocketBase batch writes for multi-record replacement/reorder operations instead of one HTTP write per changed row

Applied to files:

  • app/repositories/PremiumCloudRepository.ts
🔇 Additional comments (1)
app/components/pages/posts/post/PostSave.vue (1)

12-22: LGTM!

Also applies to: 47-60

@AlejandroAkbal
AlejandroAkbal merged commit b657610 into main Jun 4, 2026
1 check passed
@AlejandroAkbal
AlejandroAkbal deleted the codex/premium-cloud-repositories branch June 4, 2026 16:52
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