Skip to content

Feat: bounces#85

Merged
TatevikGr merged 11 commits intodevfrom
feat/bounces
Apr 30, 2026
Merged

Feat: bounces#85
TatevikGr merged 11 commits intodevfrom
feat/bounces

Conversation

@TatevikGr
Copy link
Copy Markdown
Contributor

@TatevikGr TatevikGr commented Apr 30, 2026

Summary by CodeRabbit

  • New Features
    • Added Bounces management section to admin panel with filterable overview, paging, and dedicated view
    • New bounce rules UI for creating and managing detection patterns
    • Analytics dashboard showing bounce statistics by campaign and subscriber
    • Automatic redirect to login when authentication issues occur (preserves session handling)

Summary

Provide a general description of the code changes in your pull request …
were there any bugs you had fixed? If so, mention them. If these bugs have open
GitHub issues, be sure to tag them here as well, to keep the conversation
linked together.

Unit test

Are your changes covered with unit tests, and do they not break anything?

You can run the existing unit tests using this command:

vendor/bin/phpunit tests/

Code style

Have you checked that you code is well-documented and follows the PSR-2 coding
style?

You can check for this using this command:

vendor/bin/phpcs --standard=PSR2 src/ tests/

Other Information

If there's anything else that's important and relevant to your pull
request, mention that information here. This could include benchmarks,
or other information.

If you are updating any of the CHANGELOG files or are asked to update the
CHANGELOG files by reviewers, please add the CHANGELOG entry at the top of the file.

Thanks for contributing to phpList!

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 30, 2026

Warning

Rate limit exceeded

@TatevikGr has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 44 minutes and 34 seconds before requesting another review.

To keep reviews running without waiting, you can enable usage-based add-on for your organization. This allows additional reviews beyond the hourly cap. Account admins can enable it under billing.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 3a78aaef-bd65-40e8-a022-12eb743e57a2

📥 Commits

Reviewing files that changed from the base of the PR and between 3a668dc and 6615b9e.

⛔ Files ignored due to path filters (1)
  • yarn.lock is excluded by !**/yarn.lock, !**/*.lock
📒 Files selected for processing (5)
  • assets/vue/api.js
  • assets/vue/components/bounces/BounceRules.vue
  • assets/vue/components/campaigns/CampaignDirectory.vue
  • assets/vue/views/CampaignEditView.vue
  • package.json
📝 Walkthrough

Walkthrough

Adds a complete bounces management feature: new frontend routes, view, and components (Overview, Rules, Per, ActionsPanel) plus a exported bouncesClient and backendFetch helpers. Backend adds BouncesController to serve the SPA and passes bounce action enums, security config is prepended, OpenAPI gains bounce endpoints and schemas, templates API adjusted, composer/npm deps updated, templates/spa payload extended, and integration tests added/updated.

Sequence Diagram

sequenceDiagram
    actor User
    participant Router as Vue Router
    participant View as BouncesView
    participant Panel as BouncesActionsPanel
    participant Component as Bounce Component
    participant Client as bouncesClient / backendFetch
    participant API as Backend API

    User->>Router: Navigate to /bounces
    Router->>View: route match
    View->>Panel: render
    Panel->>Component: mount active tab

    alt Overview
        Component->>Client: list(after, limit, status)
        Client->>API: GET /api/v2/bounces
        API-->>Client: paginated bounces
        Client-->>Component: bounces
        Component-->>User: render list / pagination
    else Rules
        Component->>Client: listRegex()
        Client->>API: GET /api/v2/bounces/regex
        API-->>Client: rules
        Client-->>Component: rules
        User->>Component: submit rule
        Component->>Client: upsertRegex(payload)
        Client->>API: POST /api/v2/bounces/regex
        API-->>Client: success / validation error
        Client-->>Component: response
        Component->>Client: listRegex() (refresh)
    else Per
        par
            Component->>Client: getByCampaign()
            Component->>Client: getBySubscriber()
        and
            Client->>API: GET /api/v2/bounces/by-campaign
            Client->>API: GET /api/v2/bounces/by-subscriber
        end
        API-->>Client: aggregates
        Client-->>Component: datasets
        Component-->>User: render aggregates
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

  • Feat: campaigns #81 — Modifies the same frontend integration points (assets/router/index.js, assets/vue/api.js) and exports/adds API client instances, indicating strong overlap with the current route/client changes.
🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (1 warning, 1 inconclusive)

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.
Title check ❓ Inconclusive The title 'Feat: bounces' is vague and generic, using a conventional prefix but lacking specificity about what 'bounces' entails or which bounces feature is being added. Consider a more descriptive title like 'Add bounces management feature with overview, rules, and filtering' to better convey the scope and nature of the changes.
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/bounces

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
Review rate limit: 0/1 reviews remaining, refill in 44 minutes and 34 seconds.

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

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 6

🧹 Nitpick comments (3)
.gitignore (1)

20-20: 💤 Low value

Make the .codex ignore pattern explicit (root-only vs anywhere).

Right now .gitignore uses .codex (no leading /, no trailing /), which will match any file/dir named .codex at any depth in the repo. If you only intend to ignore the tool-generated directory at the repo root, consider tightening it to something like /.codex/ (or .codex/ if it can exist in subfolders but only as a directory).

♻️ Proposed tweak
- .codex
+ /.codex/
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.gitignore at line 20, Update the .gitignore entry for the Codex artifact:
replace the current ".codex" pattern with a root-only directory pattern
"/.codex/" (or "/.codex" if you also want to ignore a root file) so the ignore
applies only to the repo-root .codex directory/file; modify the line in
.gitignore that currently contains ".codex" accordingly.
assets/vue/components/bounces/BounceRules.vue (1)

318-329: ⚡ Quick win

Surface list-load failures in the UI.

A non-auth failure currently becomes allBounceRules = [] plus a console log, and this template has no loading/error/empty state. In practice that makes “fetch failed” look the same as “there are no rules”.

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

In `@assets/vue/components/bounces/BounceRules.vue` around lines 318 - 329, The
loadBounceRules function currently swallows non-auth errors and sets
allBounceRules to an empty array, which makes failures indistinguishable from an
empty result; add explicit UI state and wiring: introduce reactive flags (e.g.,
bounceRulesLoading and bounceRulesError) alongside allBounceRules, set
bounceRulesLoading=true at start and false in finally, set bounceRulesError to
the caught error (except when redirecting for AuthenticationException/status
401), and only set allBounceRules to [] on success or when there truly are no
rules; update the component template to render a loading indicator when
bounceRulesLoading, an error message when bounceRulesError is set, and the
existing empty-state when allBounceRules is empty and bounceRulesError is null.
tests/Integration/Controller/BouncesControllerTest.php (1)

39-43: ⚡ Quick win

Assert the new data-bounce-actions payload too.

The route/title/token assertions all pass even if the BounceAction::cases() mapping regresses, which is the feature-specific part of this controller. It’d be good to validate that the rendered SPA payload actually includes data-bounce-actions.

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

In `@tests/Integration/Controller/BouncesControllerTest.php` around lines 39 - 43,
The test currently checks status, title, token and base URL but misses asserting
the SPA payload for bounce actions; update the BouncesControllerTest assertions
to also verify the rendered HTML contains the data-bounce-actions attribute (and
ideally its value matches the current mapping from BounceAction::cases()).
Concretely, add an assertion against $content such as asserting presence of
'data-bounce-actions="' and compare the attribute value to
json_encode(array_values(BounceAction::cases())) (or otherwise decode/extract
the attribute and assert it equals the expected BounceAction::cases() payload)
so regressions in BounceAction::cases() are caught.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@assets/vue/components/bounces/BounceOverview.vue`:
- Around line 223-248: loadBounces currently iterates up to 100 backend pages
then slices to client pages; change it to fetch only the current client page by
keeping cursor state in the component (e.g., currentPage, pageSize, and a
cursors map like pageCursors) and call bouncesClient.list(afterCursor, pageSize,
selectedStatus.value) once per page instead of the for-loop in loadBounces;
update loadBounces (and pagination controls) to compute the afterCursor for the
requested page from pageCursors (or request subsequent pages incrementally and
store their nextCursor), assign the returned items directly to
allBounces/currentPageItems, and keep existing isLoading/errorMessage handling
while removing the large loop and client-side slicing logic.
- Around line 223-266: Concurrent loadBounces calls can overwrite allBounces
with stale data; add a request guard: create a module-level ref (e.g.,
currentRequestId) that you increment each time loadBounces starts, capture the
incremented id in a local variable inside loadBounces and only assign to
allBounces.value, isLoading.value and errorMessage.value if the captured id
still equals currentRequestId; alternatively, if bouncesClient.list supports
cancellation, create and store an AbortController per request and abort the
previous controller before starting a new request—apply this change around the
loadBounces function and where watch(statusFilter) calls it so stale responses
are ignored.

In `@assets/vue/components/bounces/BouncePer.vue`:
- Around line 198-205: The catch block for the bouncesClient.listByCampaign call
currently only sets campaignErrorMessage and bouncesPerCampaign; change it to
detect HTTP 401 responses and trigger the same logout/redirect path used by the
other bounces flows (e.g., call the shared logout/unauthorized handler or router
redirect used elsewhere) instead of just showing an error. Specifically, in the
catch for bouncesClient.listByCampaign (and the similar catch at the 213-220
block), inspect the caught error for a 401 (error.response?.status === 401 or
error?.status === 401) and invoke the existing logout/redirect function (the
same symbol used in the other bounces flows) to end the session; otherwise
continue setting campaignErrorMessage and clearing bouncesPerCampaign and
isLoadingCampaigns as before.

In `@assets/vue/components/bounces/BounceRules.vue`:
- Around line 244-250: createForm.action is left empty so when the form is
untouched payload.action is omitted and backend defaults are used; set
createForm.action to a sensible default (e.g. the first entry of your
actions/options list) or replace the empty string with a placeholder value and
enforce selection before submit so payload.action is always present. Update the
same pattern used in the other form object around lines 363-365 (the second form
declaration) so both createForm and the other form seed their .action field from
your actions/options array (or require explicit choice) before building
payload.action.
- Around line 388-397: When creating a rule via bouncesClient.upsertRegex fails
due to expired auth, the catch block should detect a 401 and trigger the same
logout/redirect path used by loadBounceRules instead of falling back to the
generic create error handling; update the catch in the upsertRegex call to check
the error response/status for 401 and call the same handler (or invoke
loadBounceRules) so the user is redirected, otherwise continue to set
createFieldErrors via normalizeValidationErrors and set createError as before
(referencing bouncesClient.upsertRegex, normalizeValidationErrors,
createFieldErrors, createError, and loadBounceRules).

In `@tests/Integration/Controller/BouncesControllerTest.php`:
- Line 42: The test is pinned to the literal string 'http://api.phplist.local/'
— update the assertion in BouncesControllerTest (the line using
assertStringContainsString for data-api-base-url) to read the expected URL from
the test container parameter instead of the hardcoded literal; fetch the
configured value (e.g. via self::$container->getParameter('API_BASE_URL') or the
project's configured parameter name) and use that variable in the
assertStringContainsString so the test respects overridden API_BASE_URL in the
test environment.

---

Nitpick comments:
In @.gitignore:
- Line 20: Update the .gitignore entry for the Codex artifact: replace the
current ".codex" pattern with a root-only directory pattern "/.codex/" (or
"/.codex" if you also want to ignore a root file) so the ignore applies only to
the repo-root .codex directory/file; modify the line in .gitignore that
currently contains ".codex" accordingly.

In `@assets/vue/components/bounces/BounceRules.vue`:
- Around line 318-329: The loadBounceRules function currently swallows non-auth
errors and sets allBounceRules to an empty array, which makes failures
indistinguishable from an empty result; add explicit UI state and wiring:
introduce reactive flags (e.g., bounceRulesLoading and bounceRulesError)
alongside allBounceRules, set bounceRulesLoading=true at start and false in
finally, set bounceRulesError to the caught error (except when redirecting for
AuthenticationException/status 401), and only set allBounceRules to [] on
success or when there truly are no rules; update the component template to
render a loading indicator when bounceRulesLoading, an error message when
bounceRulesError is set, and the existing empty-state when allBounceRules is
empty and bounceRulesError is null.

In `@tests/Integration/Controller/BouncesControllerTest.php`:
- Around line 39-43: The test currently checks status, title, token and base URL
but misses asserting the SPA payload for bounce actions; update the
BouncesControllerTest assertions to also verify the rendered HTML contains the
data-bounce-actions attribute (and ideally its value matches the current mapping
from BounceAction::cases()). Concretely, add an assertion against $content such
as asserting presence of 'data-bounce-actions="' and compare the attribute value
to json_encode(array_values(BounceAction::cases())) (or otherwise decode/extract
the attribute and assert it equals the expected BounceAction::cases() payload)
so regressions in BounceAction::cases() are caught.
🪄 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: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: bbfe6401-dd8f-4694-8641-98a5953700ba

📥 Commits

Reviewing files that changed from the base of the PR and between b710a31 and 9f19093.

⛔ Files ignored due to path filters (1)
  • yarn.lock is excluded by !**/yarn.lock, !**/*.lock
📒 Files selected for processing (14)
  • .gitignore
  • assets/router/index.js
  • assets/vue/api.js
  • assets/vue/components/bounces/BounceOverview.vue
  • assets/vue/components/bounces/BouncePer.vue
  • assets/vue/components/bounces/BounceRules.vue
  • assets/vue/components/bounces/BouncesActionsPanel.vue
  • assets/vue/views/BouncesView.vue
  • composer.json
  • package.json
  • src/Controller/BouncesController.php
  • src/DependencyInjection/PhpListFrontendExtension.php
  • templates/spa.html.twig
  • tests/Integration/Controller/BouncesControllerTest.php

Comment thread assets/vue/components/bounces/BounceOverview.vue Outdated
Comment thread assets/vue/components/bounces/BounceOverview.vue
Comment on lines +198 to +205
try {
const bounces = await bouncesClient.listByCampaign()
bouncesPerCampaign.value = Array.isArray(bounces) ? bounces : []
} catch (error) {
campaignErrorMessage.value = error?.message ?? 'Failed to load campaign bounce data.'
bouncesPerCampaign.value = []
} finally {
isLoadingCampaigns.value = false
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Treat API 401s as a logout path here too.

If the session expires, this panel just renders an error message and leaves the user stranded. The other bounces flows already redirect on 401, so this view should do the same instead of requiring a manual reload.

Possible fix
   try {
     const bounces = await bouncesClient.listByCampaign()
     bouncesPerCampaign.value = Array.isArray(bounces) ? bounces : []
   } catch (error) {
+    if (error?.name === 'AuthenticationException' || error?.status === 401) {
+      window.location.href = '/login'
+      return
+    }
     campaignErrorMessage.value = error?.message ?? 'Failed to load campaign bounce data.'
     bouncesPerCampaign.value = []
   } finally {
     isLoadingCampaigns.value = false
   }
@@
   try {
     const bounces = await bouncesClient.listBySubscriber()
     bouncesPerSubscriber.value = Array.isArray(bounces) ? bounces : []
   } catch (error) {
+    if (error?.name === 'AuthenticationException' || error?.status === 401) {
+      window.location.href = '/login'
+      return
+    }
     subscriberErrorMessage.value = error?.message ?? 'Failed to load subscriber bounce data.'
     bouncesPerSubscriber.value = []
   } finally {
     isLoadingSubscribers.value = false
   }

Also applies to: 213-220

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

In `@assets/vue/components/bounces/BouncePer.vue` around lines 198 - 205, The
catch block for the bouncesClient.listByCampaign call currently only sets
campaignErrorMessage and bouncesPerCampaign; change it to detect HTTP 401
responses and trigger the same logout/redirect path used by the other bounces
flows (e.g., call the shared logout/unauthorized handler or router redirect used
elsewhere) instead of just showing an error. Specifically, in the catch for
bouncesClient.listByCampaign (and the similar catch at the 213-220 block),
inspect the caught error for a 401 (error.response?.status === 401 or
error?.status === 401) and invoke the existing logout/redirect function (the
same symbol used in the other bounces flows) to end the session; otherwise
continue setting campaignErrorMessage and clearing bouncesPerCampaign and
isLoadingCampaigns as before.

Comment thread assets/vue/components/bounces/BounceRules.vue
Comment on lines +388 to +397
try {
await bouncesClient.upsertRegex(payload)
isCreateModalOpen.value = false
await loadBounceRules()
} catch (error) {
createFieldErrors.value = normalizeValidationErrors(error)
createError.value = Object.keys(createFieldErrors.value).length > 0
? ''
: error?.message ?? 'Failed to create rule.'
} finally {
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Redirect on expired auth during rule creation.

loadBounceRules() already treats 401 as a logout path, but create falls through to a generic error. If the token expires while the modal is open, the user is left on a form that can’t recover without a manual refresh.

Possible fix
   try {
     await bouncesClient.upsertRegex(payload)
     isCreateModalOpen.value = false
     await loadBounceRules()
   } catch (error) {
+    if (error?.name === 'AuthenticationException' || error?.status === 401) {
+      window.location.href = '/login'
+      return
+    }
+
     createFieldErrors.value = normalizeValidationErrors(error)
     createError.value = Object.keys(createFieldErrors.value).length > 0
       ? ''
       : error?.message ?? 'Failed to create rule.'
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
try {
await bouncesClient.upsertRegex(payload)
isCreateModalOpen.value = false
await loadBounceRules()
} catch (error) {
createFieldErrors.value = normalizeValidationErrors(error)
createError.value = Object.keys(createFieldErrors.value).length > 0
? ''
: error?.message ?? 'Failed to create rule.'
} finally {
try {
await bouncesClient.upsertRegex(payload)
isCreateModalOpen.value = false
await loadBounceRules()
} catch (error) {
if (error?.name === 'AuthenticationException' || error?.status === 401) {
window.location.href = '/login'
return
}
createFieldErrors.value = normalizeValidationErrors(error)
createError.value = Object.keys(createFieldErrors.value).length > 0
? ''
: error?.message ?? 'Failed to create rule.'
} finally {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@assets/vue/components/bounces/BounceRules.vue` around lines 388 - 397, When
creating a rule via bouncesClient.upsertRegex fails due to expired auth, the
catch block should detect a 401 and trigger the same logout/redirect path used
by loadBounceRules instead of falling back to the generic create error handling;
update the catch in the upsertRegex call to check the error response/status for
401 and call the same handler (or invoke loadBounceRules) so the user is
redirected, otherwise continue to set createFieldErrors via
normalizeValidationErrors and set createError as before (referencing
bouncesClient.upsertRegex, normalizeValidationErrors, createFieldErrors,
createError, and loadBounceRules).

Comment thread tests/Integration/Controller/BouncesControllerTest.php Outdated
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

Caution

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

⚠️ Outside diff range comments (3)
openapi.json (3)

3469-3486: ⚠️ Potential issue | 🔴 Critical

PUT /api/v2/templates/{templateId} is missing the required path parameter declaration.

This operation uses {templateId} in the path but doesn't declare it in parameters. The GET operation on the same path correctly includes it, but PUT is missing it. This violates OpenAPI 3.0 specs and will cause strict validators and code generators to fail.

🔧 Proposed fix
             "put": {
                 "tags": [
                     "templates"
                 ],
                 "summary": "Update template.",
@@
                 "parameters": [
                     {
                         "name": "php-auth-pw",
                         "in": "header",
                         "description": "Session key obtained from login",
                         "required": true,
                         "schema": {
                             "type": "string"
                         }
+                    },
+                    {
+                        "name": "templateId",
+                        "in": "path",
+                        "description": "Template ID",
+                        "required": true,
+                        "schema": {
+                            "type": "string"
+                        }
                     }
                 ],
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@openapi.json` around lines 3469 - 3486, The PUT operation for the path that
contains {templateId} (operationId a73b9801869083ca7a9739aed68d1b92) is missing
the required path parameter declaration; add a parameters entry for "templateId"
with "in": "path", "required": true and an appropriate schema (e.g., type:
"string" or the same type used by the GET operation for this path) so the PUT
matches the path template and OpenAPI validation/codegen succeeds.

1868-1874: ⚠️ Potential issue | 🟠 Major

Fix ruleId type inconsistency in DELETE bounce regex operation.

The ruleId parameter uses integer for both GET and PUT operations, but string for DELETE. This mismatch breaks generated clients and validation logic. Update the DELETE operation to match.

Suggested fix
                    {
                        "name": "ruleId",
                        "in": "path",
-                       "description": "Regex hash",
+                       "description": "Regex rule ID",
                        "required": true,
                        "schema": {
-                           "type": "string"
+                           "type": "integer"
                        }
                    }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@openapi.json` around lines 1868 - 1874, Update the DELETE operation's path
parameter "ruleId" to use the same integer type as GET and PUT: locate the
DELETE /bounce/regex/{ruleId} operation's parameter named "ruleId" and change
its schema type from "string" to "integer" (preserving any format if used),
ensuring required and description remain unchanged so generated clients and
validation match the other operations.

1-20: ⚠️ Potential issue | 🟠 Major

Auth should be defined as a reusable security scheme instead of duplicated header params.

The spec repeats php-auth-pw as a header parameter across 82+ operations instead of using components.securitySchemes. Define a security scheme and apply it globally or per-operation to reduce duplication, improve client generation, and enable policy checks.

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

In `@openapi.json` around lines 1 - 20, Replace the repeated "php-auth-pw" header
parameter with a reusable security scheme in components.securitySchemes (e.g.,
name it "PhpAuthPassword" or "phpAuthPw") that models the header-based auth
(type: http or apiKey in: header with name "php-auth-pw"), then remove the
duplicated header parameter entries from individual operations and reference the
scheme globally by adding a top-level "security" array that uses the new scheme
(or apply per-operation where needed); update any operation examples/parameters
that referenced "php-auth-pw" to rely on the security requirement instead.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@assets/vue/components/bounces/BounceOverview.vue`:
- Around line 104-117: The Next/Previous buttons and their handlers (nextPage,
previousPage) allow navigation while a request is in flight, causing undefined
cursor fetches; update the template button :disabled bindings to include
isLoading (e.g., :disabled="!canGoNext || isLoading") and in the methods
nextPage and previousPage add early returns when isLoading is true and verify
the existence of the relevant cursor (e.g., nextCursor/prevCursor) before
mutating currentPage or invoking the fetch; this ensures navigation is gated
until the current request completes and a valid cursor is available.

In `@openapi.json`:
- Around line 3252-3261: The OpenAPI responses incorrectly return an array and
use the wrong status for updates: change the response schemas for the
single-template endpoints to return a single Template object (remove "type:
array" and the "items" wrapper so the schema is a direct $ref to
"#/components/schemas/Template") and update the PUT endpoint's response status
from "201" to "200" (with description "Success" or "OK"); locate the response
blocks for the PUT and other single-template endpoints in the spec and apply
these two changes.
- Around line 1355-1401: The 200 response for the Bounces list currently returns
a raw array of BounceView items while the operation accepts cursor pagination
params (after_id, limit), causing inconsistency with other endpoints; change the
response schema for the 200 success to an object with two properties: "items"
(array of items with "$ref": "#/components/schemas/BounceView") and "pagination"
(a "$ref" to the existing CursorPagination schema), matching the
/api/v2/subscribers pattern so clients receive both data and pagination
metadata.

---

Outside diff comments:
In `@openapi.json`:
- Around line 3469-3486: The PUT operation for the path that contains
{templateId} (operationId a73b9801869083ca7a9739aed68d1b92) is missing the
required path parameter declaration; add a parameters entry for "templateId"
with "in": "path", "required": true and an appropriate schema (e.g., type:
"string" or the same type used by the GET operation for this path) so the PUT
matches the path template and OpenAPI validation/codegen succeeds.
- Around line 1868-1874: Update the DELETE operation's path parameter "ruleId"
to use the same integer type as GET and PUT: locate the DELETE
/bounce/regex/{ruleId} operation's parameter named "ruleId" and change its
schema type from "string" to "integer" (preserving any format if used), ensuring
required and description remain unchanged so generated clients and validation
match the other operations.
- Around line 1-20: Replace the repeated "php-auth-pw" header parameter with a
reusable security scheme in components.securitySchemes (e.g., name it
"PhpAuthPassword" or "phpAuthPw") that models the header-based auth (type: http
or apiKey in: header with name "php-auth-pw"), then remove the duplicated header
parameter entries from individual operations and reference the scheme globally
by adding a top-level "security" array that uses the new scheme (or apply
per-operation where needed); update any operation examples/parameters that
referenced "php-auth-pw" to rely on the security requirement instead.
🪄 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: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: aa60ad07-4321-4d09-af2c-8797cc27eef5

📥 Commits

Reviewing files that changed from the base of the PR and between 9f19093 and 3a668dc.

⛔ Files ignored due to path filters (1)
  • yarn.lock is excluded by !**/yarn.lock, !**/*.lock
📒 Files selected for processing (12)
  • assets/vue/api.js
  • assets/vue/components/bounces/BounceOverview.vue
  • assets/vue/components/bounces/BounceRules.vue
  • assets/vue/components/subscribers/SubscriberDirectory.vue
  • assets/vue/layouts/AdminLayout.vue
  • composer.json
  • openapi.json
  • package.json
  • src/DependencyInjection/PhpListFrontendExtension.php
  • tests/Integration/Controller/BouncesControllerTest.php
  • tests/Integration/Controller/DashboardControllerTest.php
  • tests/Integration/Controller/ListsControllerTest.php
✅ Files skipped from review due to trivial changes (4)
  • package.json
  • composer.json
  • tests/Integration/Controller/DashboardControllerTest.php
  • src/DependencyInjection/PhpListFrontendExtension.php
🚧 Files skipped from review as they are similar to previous changes (2)
  • tests/Integration/Controller/BouncesControllerTest.php
  • assets/vue/components/bounces/BounceRules.vue

Comment on lines +104 to +117
<button
type="button"
class="flex-1 sm:flex-none px-4 py-2 border border-slate-200 rounded-lg hover:bg-slate-50 transition-colors disabled:opacity-50"
:disabled="!canGoPrevious"
@click="previousPage"
>
Previous
</button>
<button
type="button"
class="flex-1 sm:flex-none px-4 py-2 border border-slate-200 rounded-lg hover:bg-slate-50 transition-colors disabled:opacity-50"
:disabled="!canGoNext"
@click="nextPage"
>
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Prevent pagination jumps while a page request is still in flight.

On Line 115 + Lines 199-205, Next can be clicked again before the current request finishes. That can advance currentPage with an undefined cursor and fetch the wrong dataset (often resetting to first-page cursor behavior). Gate navigation by isLoading and ensure a next cursor exists before advancing.

Suggested fix
-            :disabled="!canGoPrevious"
+            :disabled="!canGoPrevious || isLoading"
@@
-            :disabled="!canGoNext"
+            :disabled="!canGoNext || isLoading"
@@
 const nextPage = () => {
-  if (!canGoNext.value) return
+  if (!canGoNext.value || isLoading.value) return
+
+  const nextCursor = pageCursors.value[currentPage.value]
+  if (nextCursor === undefined) return

   currentPage.value += 1
-  currentCursor.value = pageCursors.value[currentPage.value - 1] ?? null
+  currentCursor.value = nextCursor ?? null
   loadBounces()
 }

Also applies to: 199-205

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

In `@assets/vue/components/bounces/BounceOverview.vue` around lines 104 - 117, The
Next/Previous buttons and their handlers (nextPage, previousPage) allow
navigation while a request is in flight, causing undefined cursor fetches;
update the template button :disabled bindings to include isLoading (e.g.,
:disabled="!canGoNext || isLoading") and in the methods nextPage and
previousPage add early returns when isLoading is true and verify the existence
of the relevant cursor (e.g., nextCursor/prevCursor) before mutating currentPage
or invoking the fetch; this ensures navigation is gated until the current
request completes and a valid cursor is available.

Comment thread openapi.json
Comment thread openapi.json
@TatevikGr TatevikGr merged commit 3ba58fd into dev Apr 30, 2026
3 checks passed
@TatevikGr TatevikGr deleted the feat/bounces branch April 30, 2026 10:32
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.

2 participants