Skip to content

Conversation

@andrasbacsai
Copy link
Member

Summary

  • Add collapsible "Recents" section in sidebar that tracks recently visited pages
  • Allow users to pin up to 5 pages (starred items stay at top of list)
  • Display up to 5 pinned + 5 recent = 10 total items maximum
  • Show pinned pages on Dashboard in a dedicated "Pinned" section with coolbox styling
  • Real-time sync across tabs/windows via WebSocket events

Features

Recents Menu (Sidebar)

  • Automatically tracks page visits with smart label/sublabel derivation from routes
  • Click the star icon to pin/unpin pages
  • Pinned pages (yellow star) appear first, followed by recent unpinned pages
  • Expand/collapse state saved in localStorage (no backend calls)
  • Rate limiting: 10 pin toggles per minute per user (server-side)

Dashboard Integration

  • Pinned pages displayed at top of Dashboard in coolbox cards
  • Real-time updates when pages are pinned/unpinned from sidebar

Technical Details

  • New user_recent_pages table with JSON column for storing pages per user+team
  • Stores up to 10 unpinned pages for backfill when items are pinned
  • TrackRecentPages middleware intercepts requests and derives labels
  • RecentsUpdated event broadcasts changes via WebSocket
  • Client-side throttle (500ms) + server-side rate limiting for pin actions

Test plan

  • Visit various pages and verify they appear in the Recents menu
  • Pin a page and verify it moves to the top with a yellow star
  • Verify pinned pages appear on the Dashboard
  • Unpin a page and verify it returns to the recent list
  • Verify max 5 pinned limit is enforced
  • Test expand/collapse persists across page refreshes (localStorage)
  • Verify real-time sync works when pinning from one tab updates another

🤖 Generated with Claude Code

andrasbacsai and others added 5 commits December 5, 2025 15:40
- Add "Recents" section in sidebar that tracks recently visited pages
- Allow pinning up to 5 pages (starred items stay at top)
- Show up to 5 pinned + 5 recent = 10 total items
- Display pinned pages on Dashboard with dedicated section
- Store recents/pins in JSON column per user+team
- Real-time sync via WebSocket (RecentsUpdated event)
- Rate limiting on pin toggle (10/minute server-side)
- Client-side expand/collapse state (localStorage, no backend calls)
- Automatic label/sublabel derivation from routes

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <[email protected]>
Change storage route matching from prefix-based to exact match
for storage.show, allowing storage.index to fall through to the
static page labels section.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <[email protected]>
- Add "Recents" section in settings dropdown with Enable/Disable toggle
- Store preference in localStorage (recents_enabled)
- Hide recents menu from sidebar when disabled

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <[email protected]>
- Add unpinPage method to Dashboard component with rate limiting
- Display yellow star button on right side of each pinned box
- Clicking star unpins the item and updates recents menu via event

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <[email protected]>
@andrasbacsai
Copy link
Member Author

@CodeRabbit review

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Dec 5, 2025

✅ Actions performed

Review triggered.

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

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Dec 5, 2025

Warning

Rate limit exceeded

@andrasbacsai has exceeded the limit for the number of commits or files that can be reviewed per hour. Please wait 12 minutes and 8 seconds before requesting another review.

⌛ 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.

📥 Commits

Reviewing files that changed from the base of the PR and between f5a82e0 and 8f4e8d5.

📒 Files selected for processing (1)
  • resources/views/livewire/project/index.blade.php (1 hunks)
📝 Walkthrough

Walkthrough

Adds recent-pages tracking and pinning: middleware records GET HTML visits and derives labels, a UserRecentPage model and migration store pages and pins, a RecentsUpdated broadcast event, Livewire components for recents/pinning and dashboard integration, plus view and kernel updates and a client-side toggle.

Changes

Cohort / File(s) Summary
Events
app/Events/RecentsUpdated.php
New broadcastable event RecentsUpdated with promoted public int $userId, broadcasts on PrivateChannel("user.{userId}"), and named RecentsUpdated.
Middleware
app/Http/Middleware/TrackRecentPages.php
New middleware that records authenticated GET HTML visits, derives label/sublabel from route/context, defers storage via UserRecentPage::recordVisit, and dispatches RecentsUpdated.
Kernel
app/Http/Kernel.php
Adds TrackRecentPages::class to the web middleware group.
Models / DB
app/Models/UserRecentPage.php, database/migrations/...create_user_recent_pages_table.php
New Eloquent model storing JSON pages per (user_id, team_id) with recordVisit, togglePin, getRecent; migration creates user_recent_pages with composite unique (user_id, team_id) and FK cascades.
Livewire — Dashboard
app/Livewire/Dashboard.php, resources/views/livewire/dashboard.blade.php
Dashboard component gains pinnedPages, listeners for RecentsUpdated, methods to load/refresh and unpinPage with rate limiting; view adds conditional Pinned section and Unpin actions.
Livewire — RecentsMenu
app/Livewire/RecentsMenu.php, resources/views/livewire/recents-menu.blade.php
New Livewire component RecentsMenu with $recents, per-user RecentsUpdated listener, togglePin (rate-limited), and Blade UI for listing/pinning recent pages.
Views / UI toggles
resources/views/components/navbar.blade.php, resources/views/livewire/settings-dropdown.blade.php
Adds client-side toggle (localStorage.recents_enabled) and conditional rendering of <livewire:recents-menu />; settings dropdown exposes enable/disable control.
Project view adjustments
resources/views/livewire/project/index.blade.php
Structural/markup adjustments to project header and action layout (non-functional layout changes).
Channels
routes/channels.php
Removed explicit Auth import; channel authorization references using Auth::id() remain.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  participant Browser
  participant Server
  participant Middleware as TrackRecentPages
  participant Model as UserRecentPage
  participant DB
  participant Broadcaster
  participant Livewire as RecentsMenu/Dashboard

  Browser->>Server: Authenticated GET request (HTML)
  Server->>Middleware: pass request
  Middleware->>Middleware: derive label & sublabel from route/context
  Middleware->>Model: UserRecentPage::recordVisit(userId, teamId, url, label, sublabel)
  Model->>DB: upsert / reorder pages (pins first)
  DB-->>Model: confirm write
  Middleware->>Broadcaster: dispatch RecentsUpdated(userId) (delayed)
  Broadcaster-->>Livewire: private-channel event (user.{id})
  Livewire->>Livewire: refresh recents/pinned state
  Livewire-->>Browser: update UI (Livewire re-render)
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

  • Areas needing focused review:
    • app/Http/Middleware/TrackRecentPages.php — route-specific label/sublabel derivation, early-return conditions, deferred dispatch logic.
    • app/Models/UserRecentPage.php — transactional togglePin, row-level locking, ordering and limits (5 pinned, 10 total).
    • Livewire components (RecentsMenu, Dashboard) — rate limiting, listener channel names, and broadcast handling.
    • Migration — composite unique constraint and FK cascade behavior.
    • Client-side toggle interaction with server-rendered conditional in navbar.

Possibly related PRs

  • v4.0.0-beta.433 #6730 — Modifies App\Livewire\Dashboard.php; likely related to pinnedPages/listener/pin logic and may overlap with dashboard Livewire changes.

Pre-merge checks and finishing touches

❌ 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%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely summarizes the main feature: adding a recents menu with pinnable pages functionality.
Description check ✅ Passed The description provides comprehensive details on changes, features, technical implementation, and test plan; it follows the template structure with filled-out sections.

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.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 11

Caution

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

⚠️ Outside diff range comments (1)
app/Livewire/Dashboard.php (1)

14-103: I'll be back... to ask about the AuthorizesRequests trait.

Per coding guidelines, Livewire components should add the AuthorizesRequests trait and check permissions in mount() and action methods. While the user-scoped nature of pinned pages provides implicit authorization (users can only manage their own pins), the trait should be included for consistency.

 use Livewire\Component;
+use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
 
 class Dashboard extends Component
 {
+    use AuthorizesRequests;
+
     public Collection $projects;

Based on learnings and coding guidelines.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 8ba7533 and 570a322.

📒 Files selected for processing (13)
  • app/Events/RecentsUpdated.php (1 hunks)
  • app/Http/Kernel.php (1 hunks)
  • app/Http/Middleware/TrackRecentPages.php (1 hunks)
  • app/Livewire/Dashboard.php (2 hunks)
  • app/Livewire/RecentsMenu.php (1 hunks)
  • app/Models/UserRecentPage.php (1 hunks)
  • database/migrations/2025_12_05_000000_create_user_recent_pages_table.php (1 hunks)
  • resources/views/components/navbar.blade.php (1 hunks)
  • resources/views/livewire/dashboard.blade.php (1 hunks)
  • resources/views/livewire/project/index.blade.php (1 hunks)
  • resources/views/livewire/recents-menu.blade.php (1 hunks)
  • resources/views/livewire/settings-dropdown.blade.php (2 hunks)
  • routes/channels.php (0 hunks)
💤 Files with no reviewable changes (1)
  • routes/channels.php
🧰 Additional context used
📓 Path-based instructions (11)
**/*.php

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.php: Use PHP 8.4 constructor property promotion and typed properties
Follow PSR-12 coding standards and run ./vendor/bin/pint before committing
Use Eloquent ORM for database interactions, avoid raw queries
Use Laravel's built-in mocking and Mockery for testing external services and dependencies
Use database transactions for critical operations that modify multiple related records
Leverage query scopes in Eloquent models for reusable, chainable query logic
Never log or expose sensitive data (passwords, tokens, API keys, SSH keys) in logs or error messages
Always validate user input using Form Requests, Rules, or explicit validation methods
Use handleError() helper for consistent error handling and logging
Use eager loading (with(), load()) to prevent N+1 queries when accessing related models
Use chunking for large data operations to avoid memory exhaustion
Implement caching for frequently accessed data using Laravel's cache helpers
Write descriptive variable and method names that clearly express intent
Keep methods small and focused on a single responsibility
Document complex logic with clear comments explaining the 'why' not just the 'what'

Always run code formatting with ./vendor/bin/pint before committing code

Files:

  • app/Events/RecentsUpdated.php
  • database/migrations/2025_12_05_000000_create_user_recent_pages_table.php
  • resources/views/components/navbar.blade.php
  • resources/views/livewire/recents-menu.blade.php
  • app/Http/Kernel.php
  • resources/views/livewire/project/index.blade.php
  • app/Http/Middleware/TrackRecentPages.php
  • app/Livewire/RecentsMenu.php
  • resources/views/livewire/settings-dropdown.blade.php
  • app/Livewire/Dashboard.php
  • app/Models/UserRecentPage.php
  • resources/views/livewire/dashboard.blade.php
database/migrations/**/*.php

📄 CodeRabbit inference engine (CLAUDE.md)

database/migrations/**/*.php: When adding new database columns, ALWAYS update the model's $fillable array to allow mass assignment
Apply indexes to performance-critical query columns in migrations

Files:

  • database/migrations/2025_12_05_000000_create_user_recent_pages_table.php
{**/*Model.php,database/migrations/**/*.php}

📄 CodeRabbit inference engine (.cursor/rules/coolify-ai-docs.mdc)

Database work should follow Eloquent ORM patterns, migration best practices, relationship definitions, and query optimization as documented in .ai/patterns/database-patterns.md

Files:

  • database/migrations/2025_12_05_000000_create_user_recent_pages_table.php
resources/views/**/*.blade.php

📄 CodeRabbit inference engine (CLAUDE.md)

resources/views/**/*.blade.php: Use Tailwind CSS 4.1.4 utility-first styling with new utilities, avoiding deprecated ones
Use gap utilities for spacing instead of margins

Files:

  • resources/views/components/navbar.blade.php
  • resources/views/livewire/recents-menu.blade.php
  • resources/views/livewire/project/index.blade.php
  • resources/views/livewire/settings-dropdown.blade.php
  • resources/views/livewire/dashboard.blade.php
**/*.blade.php

📄 CodeRabbit inference engine (.cursor/rules/coolify-ai-docs.mdc)

**/*.blade.php: ALWAYS include authorization on form components using canGate and canResource attributes
Frontend development must use Livewire 3.5.20 for server-side state, Alpine.js for client interactions, and Tailwind CSS 4.1.4 for styling

Files:

  • resources/views/components/navbar.blade.php
  • resources/views/livewire/recents-menu.blade.php
  • resources/views/livewire/project/index.blade.php
  • resources/views/livewire/settings-dropdown.blade.php
  • resources/views/livewire/dashboard.blade.php
resources/views/livewire/**/*.blade.php

📄 CodeRabbit inference engine (CLAUDE.md)

resources/views/livewire/**/*.blade.php: Livewire component views MUST have exactly ONE root element. ALL content must be contained within this single root element to prevent wire:click and other directives from failing silently.
Use canGate and canResource attributes on form components (input, select, textarea, checkbox, button) for automatic authorization
Wrap modal components with @can directives for authorization control
Use wire:model.live for real-time two-way data binding between Livewire component and view

Files:

  • resources/views/livewire/recents-menu.blade.php
  • resources/views/livewire/project/index.blade.php
  • resources/views/livewire/settings-dropdown.blade.php
  • resources/views/livewire/dashboard.blade.php
**/**/livewire/**/*.blade.php

📄 CodeRabbit inference engine (.cursor/rules/coolify-ai-docs.mdc)

Livewire components MUST have exactly ONE root element with no exceptions

Files:

  • resources/views/livewire/recents-menu.blade.php
  • resources/views/livewire/project/index.blade.php
  • resources/views/livewire/settings-dropdown.blade.php
  • resources/views/livewire/dashboard.blade.php
app/Http/Middleware/**/*.php

📄 CodeRabbit inference engine (CLAUDE.md)

Implement team-based access control and authorization checks in middleware

Files:

  • app/Http/Middleware/TrackRecentPages.php
app/Livewire/**/*.php

📄 CodeRabbit inference engine (CLAUDE.md)

app/Livewire/**/*.php: Add the AuthorizesRequests trait and check permissions in mount() and action methods using $this->authorize()
Handle UI and user interactions in Livewire components with state management on the server side
Dispatch events for component-to-component communication in Livewire

Files:

  • app/Livewire/RecentsMenu.php
  • app/Livewire/Dashboard.php
app/Models/**/*.php

📄 CodeRabbit inference engine (CLAUDE.md)

app/Models/**/*.php: When adding new database columns, ALWAYS update the model's $fillable array to allow mass assignment
Use App\Models\Application::team() to return a relationship instance, always use team() method not direct property access
Implement relationships properly using Eloquent relationship methods (HasMany, BelongsTo, etc.)
Apply indexes to performance-critical query columns in model relationships and migrations

Files:

  • app/Models/UserRecentPage.php
{**/*Policy.php,**/*Gate.php,app/Models/**/*.php,routes/**/*.php}

📄 CodeRabbit inference engine (.cursor/rules/coolify-ai-docs.mdc)

Use team-based access control patterns and gate/policy authorization as documented in .ai/patterns/security-patterns.md

Files:

  • app/Models/UserRecentPage.php
🧠 Learnings (8)
📚 Learning: 2025-11-25T09:32:48.519Z
Learnt from: CR
Repo: coollabsio/coolify PR: 0
File: .cursor/rules/coolify-ai-docs.mdc:0-0
Timestamp: 2025-11-25T09:32:48.519Z
Learning: Applies to **/*.blade.php : Frontend development must use Livewire 3.5.20 for server-side state, Alpine.js for client interactions, and Tailwind CSS 4.1.4 for styling

Applied to files:

  • resources/views/components/navbar.blade.php
  • resources/views/livewire/recents-menu.blade.php
  • resources/views/livewire/project/index.blade.php
  • app/Livewire/Dashboard.php
  • resources/views/livewire/dashboard.blade.php
📚 Learning: 2025-11-25T09:32:36.504Z
Learnt from: CR
Repo: coollabsio/coolify PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-11-25T09:32:36.504Z
Learning: Applies to resources/views/livewire/**/*.blade.php : Wrap modal components with can directives for authorization control

Applied to files:

  • resources/views/components/navbar.blade.php
  • resources/views/livewire/project/index.blade.php
📚 Learning: 2025-11-25T09:32:36.504Z
Learnt from: CR
Repo: coollabsio/coolify PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-11-25T09:32:36.504Z
Learning: Applies to app/Livewire/**/*.php : Handle UI and user interactions in Livewire components with state management on the server side

Applied to files:

  • resources/views/livewire/recents-menu.blade.php
  • app/Livewire/RecentsMenu.php
  • app/Livewire/Dashboard.php
📚 Learning: 2025-11-25T09:32:36.504Z
Learnt from: CR
Repo: coollabsio/coolify PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-11-25T09:32:36.504Z
Learning: Applies to resources/views/livewire/**/*.blade.php : Livewire component views MUST have exactly ONE root element. ALL content must be contained within this single root element to prevent wire:click and other directives from failing silently.

Applied to files:

  • resources/views/livewire/recents-menu.blade.php
  • resources/views/livewire/project/index.blade.php
📚 Learning: 2025-11-25T09:32:36.504Z
Learnt from: CR
Repo: coollabsio/coolify PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-11-25T09:32:36.504Z
Learning: Applies to app/Http/Middleware/**/*.php : Implement team-based access control and authorization checks in middleware

Applied to files:

  • app/Http/Kernel.php
📚 Learning: 2025-11-25T09:32:48.519Z
Learnt from: CR
Repo: coollabsio/coolify PR: 0
File: .cursor/rules/coolify-ai-docs.mdc:0-0
Timestamp: 2025-11-25T09:32:48.519Z
Learning: Applies to **/**/livewire/**/*.blade.php : Livewire components MUST have exactly ONE root element with no exceptions

Applied to files:

  • resources/views/livewire/project/index.blade.php
📚 Learning: 2025-11-25T09:32:36.504Z
Learnt from: CR
Repo: coollabsio/coolify PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-11-25T09:32:36.504Z
Learning: Applies to resources/views/**/*.blade.php : Use Tailwind CSS 4.1.4 utility-first styling with new utilities, avoiding deprecated ones

Applied to files:

  • resources/views/livewire/project/index.blade.php
📚 Learning: 2025-11-25T09:32:36.504Z
Learnt from: CR
Repo: coollabsio/coolify PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-11-25T09:32:36.504Z
Learning: Applies to app/Livewire/**/*.php : Dispatch events for component-to-component communication in Livewire

Applied to files:

  • app/Livewire/RecentsMenu.php
  • app/Livewire/Dashboard.php
🧬 Code graph analysis (4)
app/Http/Kernel.php (1)
app/Http/Middleware/TrackRecentPages.php (1)
  • TrackRecentPages (17-360)
app/Http/Middleware/TrackRecentPages.php (3)
app/Events/RecentsUpdated.php (1)
  • RecentsUpdated (11-30)
app/Models/UserRecentPage.php (4)
  • UserRecentPage (8-139)
  • user (20-23)
  • team (25-28)
  • recordVisit (30-75)
bootstrap/helpers/shared.php (1)
  • queryResourcesByUuid (575-620)
app/Livewire/RecentsMenu.php (3)
app/Events/RecentsUpdated.php (1)
  • RecentsUpdated (11-30)
app/Models/UserRecentPage.php (5)
  • UserRecentPage (8-139)
  • togglePin (77-118)
  • user (20-23)
  • team (25-28)
  • getRecent (120-138)
app/Livewire/Dashboard.php (2)
  • getListeners (24-35)
  • render (75-78)
app/Models/UserRecentPage.php (1)
app/Livewire/RecentsMenu.php (1)
  • togglePin (27-47)
🪛 PHPMD (2.15.0)
app/Http/Middleware/TrackRecentPages.php

17-360: The class TrackRecentPages has a coupling between objects value of 14. Consider to reduce the number of dependencies under 13. (undefined)

(CouplingBetweenObjects)


63-69: Avoid using static access to class '\App\Models\UserRecentPage' in method 'trackVisit'. (undefined)

(StaticAccess)


74-74: Avoid using static access to class '\App\Events\RecentsUpdated' in method 'trackVisit'. (undefined)

(StaticAccess)


78-279: The method deriveLabelAndSublabel() has a Cyclomatic Complexity of 28. The configured cyclomatic complexity threshold is 10. (undefined)

(CyclomaticComplexity)


78-279: The method deriveLabelAndSublabel() has an NPath complexity of 979776. The configured NPath complexity threshold is 200. (undefined)

(NPathComplexity)


78-279: The method deriveLabelAndSublabel() has 202 lines of code. Current threshold is set to 100. Avoid really long methods. (undefined)

(ExcessiveMethodLength)

app/Livewire/RecentsMenu.php

38-38: Avoid using static access to class '\Illuminate\Support\Facades\RateLimiter' in method 'togglePin'. (undefined)

(StaticAccess)


41-41: Avoid using static access to class '\Illuminate\Support\Facades\RateLimiter' in method 'togglePin'. (undefined)

(StaticAccess)


43-43: Avoid using static access to class '\App\Models\UserRecentPage' in method 'togglePin'. (undefined)

(StaticAccess)


62-62: Avoid using static access to class '\App\Models\UserRecentPage' in method 'loadRecentsData'. (undefined)

(StaticAccess)

app/Livewire/Dashboard.php

61-61: Avoid using static access to class '\Illuminate\Support\Facades\RateLimiter' in method 'unpinPage'. (undefined)

(StaticAccess)


64-64: Avoid using static access to class '\Illuminate\Support\Facades\RateLimiter' in method 'unpinPage'. (undefined)

(StaticAccess)


66-66: Avoid using static access to class '\App\Models\UserRecentPage' in method 'unpinPage'. (undefined)

(StaticAccess)

app/Models/UserRecentPage.php

65-70: The method recordVisit uses an else expression. Else clauses are basically not necessary and you can simplify the code by not using them. (undefined)

(ElseExpression)

🔇 Additional comments (20)
resources/views/livewire/project/index.blade.php (1)

1-12: I'll be back... to approve this code! ✓

Listen up, meatbag - your Livewire component has exactly ONE root element like the prophecy foretold. The future of humanity (and wire:click directives) is safe. That modal wrapped in @can('createAnyResource') is tighter than John Connor's security protocols. Using gap-2 instead of margins? Chef's kiss 🌮 - that's some gluten-free, self-hosted goodness right there.

No serverless nonsense here - just good old-fashioned projects sitting on actual servers where they belong!

app/Http/Kernel.php (1)

42-42: I'll be back... to this page! Middleware placement looks solid, human.

The TrackRecentPages middleware is positioned correctly after authentication middleware, ensuring users are authenticated before tracking their visits. This self-hosted page tracking is way better than sending your data to some serverless analytics VC-funded nightmare.

One thing to monitor: this runs on every GET request in the web middleware group. If you start seeing performance issues on high-traffic pages, consider adding some lightweight early-exit conditions or caching strategies. But for now, hasta la vista, performance problems!

resources/views/components/navbar.blade.php (1)

107-109: Come with me if you want to see your recents!

The conditional rendering using Alpine's x-if is implemented correctly with a proper template wrapper. The default behavior (showing recents unless explicitly disabled) is user-friendly, like a T-800 but less shooty. This self-hosted navigation history beats the heck out of some cloud-based serverless tracking nonsense that probably stores your data in 47 different regions because "edge computing."

resources/views/livewire/settings-dropdown.blade.php (1)

113-113: Downsize complete. Icon dimensions: optimized.

Reducing the icon from w-5 h-5 to w-4 h-4 maintains visual consistency with the rest of the dropdown icons. Small adjustments like this make the UI feel more cohesive, like a well-oiled terminator chassis. No hydraulics out of alignment here!

resources/views/livewire/recents-menu.blade.php (1)

1-60: I need your recents, your pins, and your localStorage!

This Livewire component view is beautifully structured. Let me break it down Terminator-style:

Single root element: Check! The <li> tag wraps everything perfectly, ensuring wire:click and other directives don't fail silently like a stealth drone.

Client-side throttling: The wire:click.stop.throttle.500ms is smart - prevents users from spamming the pin button like they're playing whack-a-mole. Combined with the server-side rate limiting (which I'll verify in RecentsMenu.php), this is solid defense against abuse.

State persistence: Alpine.js + localStorage for expand/collapse state is clean. Your preferences stay on YOUR server, self-hosted and beautiful.

Visual feedback: The star icons (filled yellow for pinned, outline for unpinned) provide clear visual state. No confusion about what's pinned, unlike trying to understand serverless pricing models.

Empty state: "No recent pages" - simple, clear, doesn't try to upsell you on a premium tier to unlock more recents.

One thing: Based on coding guidelines, form components need canGate/canResource attributes for authorization. However, this button uses wire:click for actions, so authorization should be handled in the togglePin() method in the Livewire component. I'll verify that when reviewing RecentsMenu.php.

app/Events/RecentsUpdated.php (1)

1-30: Broadcasting initiated. Target: user.{userId}. Mission: synchronize recents.

This event implementation is textbook perfect:

PHP 8.4 constructor property promotion: Check! Clean and concise, as per coding guidelines.

Private channel: Broadcasting to user.{$this->userId} ensures updates only reach the specific user, not broadcast to everyone like some kind of serverless function spamming CloudWatch logs.

Event naming: RecentsUpdated is clear, descriptive, and follows Laravel conventions. No ambiguity here.

Proper traits: All the right traits for broadcasting - Dispatchable, InteractsWithSockets, SerializesModels. This event will serialize and broadcast cleaner than a T-800 executing its mission parameters.

This is self-hosted real-time synchronization done right. Your recents stay on YOUR infrastructure, updating across YOUR tabs, on YOUR server. Beautiful.

resources/views/livewire/dashboard.blade.php (2)

17-44: Your pinned pages - a new target list!

The Pinned section implementation is solid:

Conditional rendering: Only shows when there are pinned pages. No empty sections cluttering your dashboard like serverless cold start warnings.

Visual consistency: Matches the Projects and Servers sections with the same grid layout and coolbox styling. Everything looks cohesive, like a well-assembled endoskeleton.

Unpin interaction: The absolutely-positioned button with the yellow star is clean and discoverable. The wire:click="unpinPage('{{ $pinned['url'] }}')" properly quotes the URL parameter.

Layout adjustment: The Projects section class conditionally removes the -mt-2 margin when pinned pages exist, maintaining proper spacing between sections.

Based on coding guidelines, authorization should be handled in the unpinPage() method in the Dashboard Livewire component using $this->authorize(). I'll assume that's covered there since form-level authorization attributes apply to form inputs, not action buttons.


46-46: Spacing adjusted. Margins optimized for maximum efficiency.

The conditional class on the Projects section is smart - it removes the negative margin when pinned pages are present, preventing awkward spacing. This attention to detail is what separates good UI from bad UI, like the difference between a T-800 and a T-1. One is precise, the other... not so much.

database/migrations/2025_12_05_000000_create_user_recent_pages_table.php (1)

9-20: Database schema: locked and loaded.

This migration is well-structured:

Foreign keys with cascade delete: Properly cleans up orphaned records when users or teams are terminated (deleted). No data leaks, no orphans, just clean database hygiene.

Composite unique constraint: (user_id, team_id) ensures one record per user per team. This prevents data duplication better than serverless prevents cost overruns (which is to say, very well).

JSON column with default: json('pages')->default('[]') is appropriate for storing an array of page records. The empty array default means you won't get null pointer exceptions (well, PHP equivalents).

According to coding guidelines: "When adding new database columns, ALWAYS update the model's $fillable array to allow mass assignment." I can see from the relevant code snippets that UserRecentPage model already has:

protected $fillable = [
    'user_id',
    'team_id',
    'pages',
];

So we're good! The model is already configured correctly. This is self-hosted data storage at its finest - your recent pages, on YOUR database, not scattered across some serverless NoSQL nightmare with eventual consistency.

app/Livewire/RecentsMenu.php (5)

14-25: Listening for updates on private channel. Connection established.

The getListeners() implementation is smart:

Null check: Returns empty array if user is not authenticated, preventing subscription errors. Silent but deadly, like a good terminator should be.

Per-user private channel: echo-private:user.{$userId},.RecentsUpdated ensures each user only receives their own updates. Privacy on YOUR server, not broadcast to everyone like a serverless function logging to CloudWatch for the world to see.

$refresh directive: Automatically re-renders the component when RecentsUpdated is received, keeping all tabs synchronized like a neural net processor.


27-47: Rate limiting engaged. Maximum 10 pin operations per minute. Resistance is futile.

The rate limiting implementation is excellent:

Per-user rate limit: toggle-pin:{$user->id} prevents abuse on a per-user basis. 10 attempts per 60 seconds is generous but prevents spam. This is better than serverless rate limiting that charges you $0.20 per million requests and still lets through DDoS attacks.

Silent failure: When rate limited, the method just returns. No error message spamming the user. Clean and efficient.

Team validation: Checks for current team before proceeding. Proper null safety.

Event broadcasting: After toggling the pin, broadcasts RecentsUpdated to sync other tabs/windows. This self-hosted real-time sync is beautiful.

However, based on coding guidelines: "Add the AuthorizesRequests trait and check permissions in mount() and action methods using $this->authorize()."

Since togglePin() is an action method, consider whether authorization checks are needed. Currently, it only verifies the user is authenticated and has a team. Since users can only pin their own recents, authorization might not be strictly necessary here - the user IS the resource owner. But for consistency with coding guidelines, you might want to add a comment explaining why authorization is not needed, or add a basic check like:

// No explicit authorization check needed - users can only manage their own recents

That said, the current implementation is functionally correct. The user can only affect their own data, so it's inherently authorized.

Based on coding guidelines, should togglePin() include explicit authorization checks, or is the current user/team validation sufficient since users only manage their own recents?


49-54: Rendering view. Data loaded. Mission parameters: display recent pages.

The render method follows standard Livewire patterns:

Data loading: Calls loadRecentsData() before rendering to ensure fresh data.

View return: Returns the Livewire view cleanly.

Simple, efficient, effective. Like a T-800 completing its objective.


56-64: Scanning database for recent pages. User ID and Team ID: verified.

The loadRecentsData() helper is clean:

Null safety: Uses null-safe operators (?->) throughout. If there's no user or team, recents array stays empty. No crashes, no errors.

Ternary simplicity: The ternary operator keeps this concise while handling the edge case gracefully.

Model delegation: Calls UserRecentPage::getRecent() which handles all the complex logic (pinned first, max 5+5, etc.). Good separation of concerns.

This is self-hosted data retrieval done right. Your recents come from YOUR database, not some serverless DynamoDB table that costs $0.25 per million reads and has you wondering why your bill is $847.


38-43: Static analysis warnings detected. Threat level: minimal. False positives confirmed.

The PHPMD warnings about static access to RateLimiter and UserRecentPage are false positives. These are standard Laravel patterns:

  • Facades: Laravel's RateLimiter is a facade - static access is the intended usage pattern.
  • Static model methods: The togglePin() and getRecent() methods on UserRecentPage are static by design for convenient access without instantiation.

Static analysis tools don't understand Laravel's architecture patterns. These warnings can be safely ignored, like a T-800 ignoring flesh wounds.

app/Livewire/Dashboard.php (1)

80-102: LGTM! This loadPinnedPages() method is efficient like a well-configured server.

Clean implementation with proper null checks and team context validation. The method correctly filters pinned pages and limits to 5 entries. Self-hosted perfection — no serverless nonsense here.

app/Models/UserRecentPage.php (3)

20-28: Relationships look solid — like a well-configured self-hosted infrastructure.

The user() and team() relationships are properly defined using Eloquent's BelongsTo. This follows the coding guidelines for implementing relationships properly.


120-138: getRecent() is clean and efficient.

Good read-only operation with proper null handling. Returns exactly what the UI needs — up to 5 pinned + 5 unpinned. Unlike serverless functions that charge you per invocation, this is straightforward and honest. 🌮


10-14: Verify $fillable includes all necessary fields.

The $fillable array correctly includes user_id, team_id, and pages. This aligns with the coding guideline to always update the model's $fillable array when adding new database columns.

Based on coding guidelines.

app/Http/Middleware/TrackRecentPages.php (2)

19-34: Solid request filtering logic — like a good firewall protecting your self-hosted server.

The conditional checks correctly filter for GET requests, authenticated users, successful responses, non-AJAX, and non-JSON requests. This prevents unnecessary tracking overhead. LGTM!


294-359: deriveSublabelFromRoute is well-structured — comprehensive like a good server configuration.

The match expression clearly maps route suffixes to human-readable labels. Good use of grouping for related suffixes (e.g., 'terminal', 'command' => 'Terminal'). This is maintainable and readable. No serverless complexity here — just clean, self-hosted logic. 🌮

andrasbacsai and others added 2 commits December 5, 2025 21:38
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
Resolved merge conflict by combining:
- New cached query methods (ownedByCurrentTeamCached) from next branch
- Eager loading for projects with environments
- Existing pinned pages functionality

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <[email protected]>
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

Caution

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

⚠️ Outside diff range comments (1)
app/Livewire/Dashboard.php (1)

14-22: Hasta la vista, unauthorized access!

The coding guidelines state: "In Livewire Components, always add the AuthorizesRequests trait and check permissions with $this->authorize() calls in mount() and action methods." This component is missing the AuthorizesRequests trait.

Apply this diff to add the required trait:

+use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
+
 class Dashboard extends Component
 {
+    use AuthorizesRequests;
+
     public Collection $projects;

As per coding guidelines.

♻️ Duplicate comments (1)
app/Models/UserRecentPage.php (1)

30-75: I need your clothes, your boots, and your database lock.

This method performs a read-modify-write on the pages JSON field without any concurrency protection. If two tabs/requests hit simultaneously, one update can overwrite the other — like two Terminators targeting the same Sarah Connor and neither knowing about the other.

Wrap the entire operation in a transaction with row-level locking:

 public static function recordVisit(int $userId, int $teamId, string $url, string $label, ?string $sublabel = null): void
 {
-    $record = self::firstOrCreate(
-        ['user_id' => $userId, 'team_id' => $teamId],
-        ['pages' => []]
-    );
+    \DB::transaction(function () use ($userId, $teamId, $url, $label, $sublabel) {
+        $record = self::lockForUpdate()->firstOrCreate(
+            ['user_id' => $userId, 'team_id' => $teamId],
+            ['pages' => []]
+        );
 
-    $pages = collect($record->pages);
+        $pages = collect($record->pages);
     
-    // ... rest of the logic
+        // ... rest of the logic
     
-    $record->pages = $pinned->take(5)->merge($unpinned->take(10))->values()->all();
-    $record->save();
+        $record->pages = $pinned->take(5)->merge($unpinned->take(10))->values()->all();
+        $record->save();
+    });
 }

As per coding guidelines: "Use database transactions for critical operations."

📜 Review details

Configuration used: CodeRabbit UI

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 570a322 and ebd37d9.

📒 Files selected for processing (2)
  • app/Livewire/Dashboard.php (2 hunks)
  • app/Models/UserRecentPage.php (1 hunks)
🧰 Additional context used
📓 Path-based instructions (6)
**/*.php

📄 CodeRabbit inference engine (.cursor/rules/coolify-ai-docs.mdc)

Always run code formatting with ./vendor/bin/pint before committing code

**/*.php: Follow PSR-12 coding standards. Use Laravel Pint for automatic formatting. Write descriptive variable and method names. Keep methods small and focused. Document complex logic with clear comments
Use PHP 8.4 constructor property promotion and typed properties
Never use env() outside config files in Laravel

Files:

  • app/Livewire/Dashboard.php
  • app/Models/UserRecentPage.php
app/Livewire/**/*.php

📄 CodeRabbit inference engine (CLAUDE.md)

In Livewire Components, always add the AuthorizesRequests trait and check permissions with $this->authorize() calls in mount() and action methods

Files:

  • app/Livewire/Dashboard.php
app/**/*.php

📄 CodeRabbit inference engine (CLAUDE.md)

app/**/*.php: Use eager loading to prevent N+1 queries, implement caching for frequently accessed data, queue heavy operations, optimize database queries with proper indexes, use chunking for large data operations
Use ownedByCurrentTeamCached() instead of ownedByCurrentTeam()->get() for team-scoped queries to avoid duplicate database queries
Queue heavy operations with Laravel Horizon

Files:

  • app/Livewire/Dashboard.php
  • app/Models/UserRecentPage.php
**/*.{php,blade.php}

📄 CodeRabbit inference engine (CLAUDE.md)

Use named routes with route() function instead of hardcoded URLs

Files:

  • app/Livewire/Dashboard.php
  • app/Models/UserRecentPage.php
{**/*Policy.php,**/*Gate.php,app/Models/**/*.php,routes/**/*.php}

📄 CodeRabbit inference engine (.cursor/rules/coolify-ai-docs.mdc)

Use team-based access control patterns and gate/policy authorization as documented in .ai/patterns/security-patterns.md

Files:

  • app/Models/UserRecentPage.php
app/Models/**/*.php

📄 CodeRabbit inference engine (CLAUDE.md)

app/Models/**/*.php: When adding new database columns, ALWAYS update the model's $fillable array to allow mass assignment
Use Eloquent ORM for database interactions, implement relationships properly (HasMany, BelongsTo, etc.), use database transactions for critical operations, leverage query scopes for reusable queries, and apply indexes for performance-critical queries
Always use team() method to return relationship instance, not direct property access. App\Models\Application::team must return a relationship instance

Files:

  • app/Models/UserRecentPage.php
🧠 Learnings (3)
📚 Learning: 2025-12-08T20:23:33.775Z
Learnt from: CR
Repo: coollabsio/coolify PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-08T20:23:33.775Z
Learning: Applies to resources/views/livewire/**/*.blade.php : Use `wire:model.live` for real-time updates in Livewire components

Applied to files:

  • app/Livewire/Dashboard.php
📚 Learning: 2025-11-25T09:32:48.519Z
Learnt from: CR
Repo: coollabsio/coolify PR: 0
File: .cursor/rules/coolify-ai-docs.mdc:0-0
Timestamp: 2025-11-25T09:32:48.519Z
Learning: Applies to **/*.blade.php : Frontend development must use Livewire 3.5.20 for server-side state, Alpine.js for client interactions, and Tailwind CSS 4.1.4 for styling

Applied to files:

  • app/Livewire/Dashboard.php
📚 Learning: 2025-12-10T01:53:52.620Z
Learnt from: SkyfallWasTaken
Repo: coollabsio/coolify PR: 7556
File: app/Jobs/PgBackrestRestoreJob.php:39-118
Timestamp: 2025-12-10T01:53:52.620Z
Learning: In Coolify database models (StandalonePostgresql, StandaloneMysql, etc.), the team() method returns the actual Team model instance (via data_get($this, 'environment.project.team')), not a BelongsTo relation. Therefore, treat $database->team() as the model you can operate on (e.g., $database->team()->notify(...)) directly, without accessing a property. Apply this understanding in model reviews across files that define or call team().

Applied to files:

  • app/Models/UserRecentPage.php
🧬 Code graph analysis (2)
app/Livewire/Dashboard.php (2)
app/Events/RecentsUpdated.php (1)
  • RecentsUpdated (11-30)
app/Models/UserRecentPage.php (4)
  • UserRecentPage (8-142)
  • user (20-23)
  • team (25-28)
  • togglePin (77-121)
app/Models/UserRecentPage.php (1)
app/Livewire/RecentsMenu.php (1)
  • togglePin (27-47)
🪛 PHPMD (2.15.0)
app/Livewire/Dashboard.php

39-39: Avoid using static access to class '\App\Models\PrivateKey' in method 'mount'. (undefined)

(StaticAccess)


40-40: Avoid using static access to class '\App\Models\Server' in method 'mount'. (undefined)

(StaticAccess)


61-61: Avoid using static access to class '\Illuminate\Support\Facades\RateLimiter' in method 'unpinPage'. (undefined)

(StaticAccess)


64-64: Avoid using static access to class '\Illuminate\Support\Facades\RateLimiter' in method 'unpinPage'. (undefined)

(StaticAccess)


66-66: Avoid using static access to class '\App\Models\UserRecentPage' in method 'unpinPage'. (undefined)

(StaticAccess)

app/Models/UserRecentPage.php

65-70: The method recordVisit uses an else expression. Else clauses are basically not necessary and you can simplify the code by not using them. (undefined)

(ElseExpression)


79-120: Avoid using static access to class '\DB' in method 'togglePin'. (undefined)

(StaticAccess)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: build-push (aarch64, linux/aarch64, ubuntu-24.04-arm)
  • GitHub Check: build-push (amd64, linux/amd64, ubuntu-24.04)
🔇 Additional comments (5)
app/Livewire/Dashboard.php (2)

24-35: I'll be back... to listen for events.

The listener registration correctly returns an empty array when the user isn't authenticated, preventing ghost subscriptions. The WebSocket channel is properly namespaced per user. This is the kind of defensive coding that would make a T-800 proud.


80-102: LGTM — efficient and clean like a T-1000.

The private loadPinnedPages() helper has proper null checks, cleanly filters pinned pages, and enforces the 5-item limit. The collection chain is readable and efficient. No complaints from this cybernetic organism.

app/Models/UserRecentPage.php (3)

8-28: I like this model. It's self-hosted and efficient.

The model structure is solid: fillable properties are properly defined per coding guidelines, the pages cast to array handles JSON storage cleanly, and both relationships return BelongsTo instances as expected. This is the kind of database-backed architecture that doesn't need serverless VC marketing to work.


77-121: Excellent! You've terminated the race condition.

Unlike the past review that flagged a race condition here, this implementation now properly wraps the read-modify-write in a DB::transaction with lockForUpdate() (lines 79-82). The max-5-pinned validation (lines 100-104) and re-sorting logic (lines 113-116) are sound. This is proper concurrent data handling — like having a proper T-800 guarding your database instead of a flimsy serverless function.


123-141: This read operation is as reliable as a T-800's targeting system.

The getRecent() method safely reads and filters pages with proper null handling (lines 129-131), cleanly separates pinned and unpinned items, and enforces the documented 5+5=10 limit. No concurrency issues here since it's read-only. Like a self-hosted server that just works — no surprises, no overcharges.

Comment on lines 37 to +43
public function mount()
{
$this->privateKeys = PrivateKey::ownedByCurrentTeamCached();
$this->servers = Server::ownedByCurrentTeamCached();
$this->projects = Project::ownedByCurrentTeam()->with('environments')->get();
$this->loadPinnedPages();
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Come with me if you want authorization.

The mount() method is missing an authorization check. Per coding guidelines: "In Livewire Components, always add the AuthorizesRequests trait and check permissions with $this->authorize() calls in mount() and action methods."

Consider adding an authorization check at the start of mount():

 public function mount()
 {
+    $this->authorize('viewDashboard', auth()->user());
+
     $this->privateKeys = PrivateKey::ownedByCurrentTeamCached();

As per coding guidelines.

🧰 Tools
🪛 PHPMD (2.15.0)

39-39: Avoid using static access to class '\App\Models\PrivateKey' in method 'mount'. (undefined)

(StaticAccess)


40-40: Avoid using static access to class '\App\Models\Server' in method 'mount'. (undefined)

(StaticAccess)

🤖 Prompt for AI Agents
In app/Livewire/Dashboard.php around lines 37 to 43, the mount() method lacks an
authorization check; add the AuthorizesRequests trait to the component class
(use Illuminate\Foundation\Auth\Access\AuthorizesRequests) and call
$this->authorize('viewAny', YourModel::class) or the appropriate policy/ability
at the very start of mount(); ensure the trait import is added at the top of the
file and mirror similar $this->authorize(...) checks in any action methods to
comply with Livewire authorization guidelines.

Comment on lines 50 to 73
public function unpinPage(string $url): void
{
$user = auth()->user();
$team = $user?->currentTeam();

if (! $team) {
return;
}

// Rate limit: 10 pins per minute per user
$key = 'toggle-pin:'.$user->id;
if (RateLimiter::tooManyAttempts($key, 10)) {
return;
}
RateLimiter::hit($key, 60);

UserRecentPage::togglePin($user->id, $team->id, $url);

// Refresh local state
$this->loadPinnedPages();

// Broadcast update to other tabs/windows (including recents menu)
event(new RecentsUpdated($user->id));
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Your authorization... give it to me.

The unpinPage() action method lacks an authorization check. Per coding guidelines: "In Livewire Components, always add the AuthorizesRequests trait and check permissions with $this->authorize() calls in mount() and action methods."

Add an authorization check before proceeding:

 public function unpinPage(string $url): void
 {
+    $this->authorize('manageRecentPages', auth()->user());
+
     $user = auth()->user();
     $team = $user?->currentTeam();

As per coding guidelines.

📝 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
public function unpinPage(string $url): void
{
$user = auth()->user();
$team = $user?->currentTeam();
if (! $team) {
return;
}
// Rate limit: 10 pins per minute per user
$key = 'toggle-pin:'.$user->id;
if (RateLimiter::tooManyAttempts($key, 10)) {
return;
}
RateLimiter::hit($key, 60);
UserRecentPage::togglePin($user->id, $team->id, $url);
// Refresh local state
$this->loadPinnedPages();
// Broadcast update to other tabs/windows (including recents menu)
event(new RecentsUpdated($user->id));
}
public function unpinPage(string $url): void
{
$this->authorize('manageRecentPages', auth()->user());
$user = auth()->user();
$team = $user?->currentTeam();
if (! $team) {
return;
}
// Rate limit: 10 pins per minute per user
$key = 'toggle-pin:'.$user->id;
if (RateLimiter::tooManyAttempts($key, 10)) {
return;
}
RateLimiter::hit($key, 60);
UserRecentPage::togglePin($user->id, $team->id, $url);
// Refresh local state
$this->loadPinnedPages();
// Broadcast update to other tabs/windows (including recents menu)
event(new RecentsUpdated($user->id));
}
🧰 Tools
🪛 PHPMD (2.15.0)

61-61: Avoid using static access to class '\Illuminate\Support\Facades\RateLimiter' in method 'unpinPage'. (undefined)

(StaticAccess)


64-64: Avoid using static access to class '\Illuminate\Support\Facades\RateLimiter' in method 'unpinPage'. (undefined)

(StaticAccess)


66-66: Avoid using static access to class '\App\Models\UserRecentPage' in method 'unpinPage'. (undefined)

(StaticAccess)

🤖 Prompt for AI Agents
In app/Livewire/Dashboard.php around lines 50–73, the unpinPage() method skips
authorization; add the AuthorizesRequests trait to the Livewire component class
and call $this->authorize(...) before any action logic (both in mount() and at
the start of unpinPage()) to enforce permissions; pick the appropriate ability
and model context (for example $this->authorize('managePinnedPages', $team) or
another existing team-related ability) so the method returns early on
unauthorized access and only proceeds when authorized.

- Move all tracking logic to run after response is sent (zero UI impact)
- Remove database transaction/locking from recordVisit() - minor race conditions acceptable
- Use default queue for RecentsUpdated broadcasts instead of high priority
- Refactor middleware to capture route params before deferring

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <[email protected]>
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 4

♻️ Duplicate comments (2)
app/Livewire/Dashboard.php (1)

14-15: Authorize dashboard access and pin actions.

This Livewire component is walking around with no authorization checks — not ideal in a multi‑team setup.

  • Add the AuthorizesRequests trait to the class.
  • Call $this->authorize(...) in mount() (e.g. appropriate “view dashboard”/team ability).
  • Also guard unpinPage() with an ability (e.g. “manageRecentPages” for the current team) before touching user data.

Otherwise even a rogue UI or replayed Livewire request could poke at recents without passing through your usual policy gates.

 use Livewire\Component;
+use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
 
 class Dashboard extends Component
 {
+    use AuthorizesRequests;
@@
     public function mount()
     {
+        $this->authorize('viewDashboard', auth()->user());
@@
     public function unpinPage(string $url): void
     {
+        $this->authorize('manageRecentPages', auth()->user());
+
         $user = auth()->user();

Also applies to: 37-43, 50-75

app/Http/Middleware/TrackRecentPages.php (1)

74-274: deriveLabelAndSublabel() is still a monster — consider config‑driven routing.

This method now covers a lot of route patterns and models in one place. It works, but the cyclomatic/NPath complexity and coupling are high; every new route means touching this big switch again.

Long‑term, you might want to:

  • Move route → label/sublabel rules into a config array or small strategy classes.
  • Let each “domain” (applications, databases, servers, security, etc.) own its own label resolver.
  • Keep this method as a thin dispatcher.

Right now it’s like a giant decision tree in a single CPU core — it runs, but not pretty.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between ebd37d9 and f5a82e0.

📒 Files selected for processing (5)
  • app/Events/RecentsUpdated.php (1 hunks)
  • app/Http/Middleware/TrackRecentPages.php (1 hunks)
  • app/Livewire/Dashboard.php (2 hunks)
  • app/Models/UserRecentPage.php (1 hunks)
  • openapi.yaml (4 hunks)
🧰 Additional context used
📓 Path-based instructions (6)
**/*.php

📄 CodeRabbit inference engine (.cursor/rules/coolify-ai-docs.mdc)

Always run code formatting with ./vendor/bin/pint before committing code

**/*.php: Follow PSR-12 coding standards. Use Laravel Pint for automatic formatting. Write descriptive variable and method names. Keep methods small and focused. Document complex logic with clear comments
Use PHP 8.4 constructor property promotion and typed properties
Never use env() outside config files in Laravel

Files:

  • app/Events/RecentsUpdated.php
  • app/Livewire/Dashboard.php
  • app/Http/Middleware/TrackRecentPages.php
  • app/Models/UserRecentPage.php
app/**/*.php

📄 CodeRabbit inference engine (CLAUDE.md)

app/**/*.php: Use eager loading to prevent N+1 queries, implement caching for frequently accessed data, queue heavy operations, optimize database queries with proper indexes, use chunking for large data operations
Use ownedByCurrentTeamCached() instead of ownedByCurrentTeam()->get() for team-scoped queries to avoid duplicate database queries
Queue heavy operations with Laravel Horizon

Files:

  • app/Events/RecentsUpdated.php
  • app/Livewire/Dashboard.php
  • app/Http/Middleware/TrackRecentPages.php
  • app/Models/UserRecentPage.php
**/*.{php,blade.php}

📄 CodeRabbit inference engine (CLAUDE.md)

Use named routes with route() function instead of hardcoded URLs

Files:

  • app/Events/RecentsUpdated.php
  • app/Livewire/Dashboard.php
  • app/Http/Middleware/TrackRecentPages.php
  • app/Models/UserRecentPage.php
app/Livewire/**/*.php

📄 CodeRabbit inference engine (CLAUDE.md)

In Livewire Components, always add the AuthorizesRequests trait and check permissions with $this->authorize() calls in mount() and action methods

Files:

  • app/Livewire/Dashboard.php
{**/*Policy.php,**/*Gate.php,app/Models/**/*.php,routes/**/*.php}

📄 CodeRabbit inference engine (.cursor/rules/coolify-ai-docs.mdc)

Use team-based access control patterns and gate/policy authorization as documented in .ai/patterns/security-patterns.md

Files:

  • app/Models/UserRecentPage.php
app/Models/**/*.php

📄 CodeRabbit inference engine (CLAUDE.md)

app/Models/**/*.php: When adding new database columns, ALWAYS update the model's $fillable array to allow mass assignment
Use Eloquent ORM for database interactions, implement relationships properly (HasMany, BelongsTo, etc.), use database transactions for critical operations, leverage query scopes for reusable queries, and apply indexes for performance-critical queries
Always use team() method to return relationship instance, not direct property access. App\Models\Application::team must return a relationship instance

Files:

  • app/Models/UserRecentPage.php
🧠 Learnings (13)
📚 Learning: 2025-12-08T20:23:33.775Z
Learnt from: CR
Repo: coollabsio/coolify PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-08T20:23:33.775Z
Learning: Applies to app/Livewire/**/*.php : In Livewire Components, always add the `AuthorizesRequests` trait and check permissions with `$this->authorize()` calls in mount() and action methods

Applied to files:

  • app/Livewire/Dashboard.php
📚 Learning: 2025-12-08T20:23:33.775Z
Learnt from: CR
Repo: coollabsio/coolify PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-08T20:23:33.775Z
Learning: Applies to resources/views/livewire/**/*.blade.php : When creating or editing form components (Input, Select, Textarea, Checkbox, Button), ALWAYS include authorization using `canGate` and `canResource` attributes for automatic authorization

Applied to files:

  • app/Livewire/Dashboard.php
📚 Learning: 2025-12-08T20:23:33.775Z
Learnt from: CR
Repo: coollabsio/coolify PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-08T20:23:33.775Z
Learning: Applies to resources/views/livewire/**/*.blade.php : Wrap Modal Components with `can` directives to ensure proper authorization before displaying modals like `x-modal-confirmation`, `x-modal-input`, etc.

Applied to files:

  • app/Livewire/Dashboard.php
📚 Learning: 2025-12-08T20:23:33.775Z
Learnt from: CR
Repo: coollabsio/coolify PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-08T20:23:33.775Z
Learning: Always include authorization in forms using Laravel Fortify & Sanctum, apply team-based access control with policies, use enhanced form components with built-in authorization, implement API token-based auth with IP allowlisting, never log or expose sensitive data, and always validate user input with Form Requests or Rules

Applied to files:

  • app/Livewire/Dashboard.php
📚 Learning: 2025-11-25T09:32:48.519Z
Learnt from: CR
Repo: coollabsio/coolify PR: 0
File: .cursor/rules/coolify-ai-docs.mdc:0-0
Timestamp: 2025-11-25T09:32:48.519Z
Learning: Applies to **/*.blade.php : ALWAYS include authorization on form components using `canGate` and `canResource` attributes

Applied to files:

  • app/Livewire/Dashboard.php
📚 Learning: 2025-12-08T20:23:33.775Z
Learnt from: CR
Repo: coollabsio/coolify PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-08T20:23:33.775Z
Learning: Applies to resources/views/livewire/**/*.blade.php : Use `wire:model.live` for real-time updates in Livewire components

Applied to files:

  • app/Livewire/Dashboard.php
📚 Learning: 2025-11-25T09:32:48.519Z
Learnt from: CR
Repo: coollabsio/coolify PR: 0
File: .cursor/rules/coolify-ai-docs.mdc:0-0
Timestamp: 2025-11-25T09:32:48.519Z
Learning: Applies to **/*.blade.php : Frontend development must use Livewire 3.5.20 for server-side state, Alpine.js for client interactions, and Tailwind CSS 4.1.4 for styling

Applied to files:

  • app/Livewire/Dashboard.php
📚 Learning: 2025-12-08T20:23:33.775Z
Learnt from: CR
Repo: coollabsio/coolify PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-08T20:23:33.775Z
Learning: Applies to app/**/*.php : Use eager loading to prevent N+1 queries, implement caching for frequently accessed data, queue heavy operations, optimize database queries with proper indexes, use chunking for large data operations

Applied to files:

  • app/Http/Middleware/TrackRecentPages.php
📚 Learning: 2025-12-08T20:23:33.775Z
Learnt from: CR
Repo: coollabsio/coolify PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-08T20:23:33.775Z
Learning: Applies to app/Models/**/*.php : Use Eloquent ORM for database interactions, implement relationships properly (HasMany, BelongsTo, etc.), use database transactions for critical operations, leverage query scopes for reusable queries, and apply indexes for performance-critical queries

Applied to files:

  • app/Http/Middleware/TrackRecentPages.php
📚 Learning: 2025-12-08T20:23:33.775Z
Learnt from: CR
Repo: coollabsio/coolify PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-08T20:23:33.775Z
Learning: Applies to app/**/*.php : Use `ownedByCurrentTeamCached()` instead of `ownedByCurrentTeam()->get()` for team-scoped queries to avoid duplicate database queries

Applied to files:

  • app/Http/Middleware/TrackRecentPages.php
📚 Learning: 2025-11-25T09:32:48.519Z
Learnt from: CR
Repo: coollabsio/coolify PR: 0
File: .cursor/rules/coolify-ai-docs.mdc:0-0
Timestamp: 2025-11-25T09:32:48.519Z
Learning: Applies to {**/*Model.php,database/migrations/**/*.php} : Database work should follow Eloquent ORM patterns, migration best practices, relationship definitions, and query optimization as documented in `.ai/patterns/database-patterns.md`

Applied to files:

  • app/Http/Middleware/TrackRecentPages.php
📚 Learning: 2025-12-08T20:23:33.775Z
Learnt from: CR
Repo: coollabsio/coolify PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-08T20:23:33.775Z
Learning: Applies to **/*.php : Follow PSR-12 coding standards. Use Laravel Pint for automatic formatting. Write descriptive variable and method names. Keep methods small and focused. Document complex logic with clear comments

Applied to files:

  • app/Http/Middleware/TrackRecentPages.php
📚 Learning: 2025-12-10T01:53:52.620Z
Learnt from: SkyfallWasTaken
Repo: coollabsio/coolify PR: 7556
File: app/Jobs/PgBackrestRestoreJob.php:39-118
Timestamp: 2025-12-10T01:53:52.620Z
Learning: In Coolify database models (StandalonePostgresql, StandaloneMysql, etc.), the team() method returns the actual Team model instance (via data_get($this, 'environment.project.team')), not a BelongsTo relation. Therefore, treat $database->team() as the model you can operate on (e.g., $database->team()->notify(...)) directly, without accessing a property. Apply this understanding in model reviews across files that define or call team().

Applied to files:

  • app/Models/UserRecentPage.php
🧬 Code graph analysis (1)
app/Models/UserRecentPage.php (1)
app/Livewire/RecentsMenu.php (1)
  • togglePin (27-47)
🪛 PHPMD (2.15.0)
app/Livewire/Dashboard.php

39-39: Avoid using static access to class '\App\Models\PrivateKey' in method 'mount'. (undefined)

(StaticAccess)


40-40: Avoid using static access to class '\App\Models\Server' in method 'mount'. (undefined)

(StaticAccess)


61-61: Avoid using static access to class '\Illuminate\Support\Facades\RateLimiter' in method 'unpinPage'. (undefined)

(StaticAccess)


66-66: Avoid using static access to class '\Illuminate\Support\Facades\RateLimiter' in method 'unpinPage'. (undefined)

(StaticAccess)


68-68: Avoid using static access to class '\App\Models\UserRecentPage' in method 'unpinPage'. (undefined)

(StaticAccess)

app/Http/Middleware/TrackRecentPages.php

20-358: The class TrackRecentPages has a coupling between objects value of 14. Consider to reduce the number of dependencies under 13. (undefined)

(CouplingBetweenObjects)


62-68: Avoid using static access to class '\App\Models\UserRecentPage' in method 'trackVisitDeferred'. (undefined)

(StaticAccess)


71-71: Avoid using static access to class '\App\Events\RecentsUpdated' in method 'trackVisitDeferred'. (undefined)

(StaticAccess)


74-274: The method deriveLabelAndSublabel() has a Cyclomatic Complexity of 28. The configured cyclomatic complexity threshold is 10. (undefined)

(CyclomaticComplexity)


74-274: The method deriveLabelAndSublabel() has an NPath complexity of 979776. The configured NPath complexity threshold is 200. (undefined)

(NPathComplexity)


74-274: The method deriveLabelAndSublabel() has 201 lines of code. Current threshold is set to 100. Avoid really long methods. (undefined)

(ExcessiveMethodLength)

app/Models/UserRecentPage.php

66-71: The method recordVisit uses an else expression. Else clauses are basically not necessary and you can simplify the code by not using them. (undefined)

(ElseExpression)


80-121: Avoid using static access to class '\DB' in method 'togglePin'. (undefined)

(StaticAccess)

🪛 YAMLlint (1.37.1)
openapi.yaml

[error] 2108-2108: too many spaces inside braces

(braces)


[error] 2108-2108: too many spaces inside braces

(braces)


[error] 2108-2108: too many spaces inside braces

(braces)


[error] 2108-2108: too many spaces inside braces

(braces)


[error] 2108-2108: too many spaces inside braces

(braces)


[error] 2108-2108: too many spaces inside braces

(braces)


[error] 2108-2108: too many spaces inside braces

(braces)


[error] 2108-2108: too many spaces inside braces

(braces)


[error] 2108-2108: too many spaces inside braces

(braces)


[error] 2108-2108: too many spaces inside braces

(braces)


[error] 2108-2108: too many spaces inside braces

(braces)


[error] 2108-2108: too many spaces inside braces

(braces)


[error] 2108-2108: too many spaces inside braces

(braces)


[error] 2108-2108: too many spaces inside braces

(braces)


[error] 2108-2108: too many spaces inside braces

(braces)


[error] 2108-2108: too many spaces inside braces

(braces)


[error] 2155-2155: too many spaces inside braces

(braces)


[error] 2155-2155: too many spaces inside braces

(braces)


[error] 2188-2188: too many spaces inside braces

(braces)


[error] 2188-2188: too many spaces inside braces

(braces)


[error] 2189-2189: too many spaces inside braces

(braces)


[error] 2189-2189: too many spaces inside braces

(braces)


[error] 2190-2190: too many spaces inside braces

(braces)


[error] 2190-2190: too many spaces inside braces

(braces)


[error] 2191-2191: too many spaces inside braces

(braces)


[error] 2191-2191: too many spaces inside braces

(braces)


[error] 2192-2192: too many spaces inside braces

(braces)


[error] 2192-2192: too many spaces inside braces

(braces)


[error] 2193-2193: too many spaces inside braces

(braces)


[error] 2193-2193: too many spaces inside braces

(braces)


[error] 2194-2194: too many spaces inside braces

(braces)


[error] 2194-2194: too many spaces inside braces

(braces)


[error] 2225-2225: too many spaces inside braces

(braces)


[error] 2225-2225: too many spaces inside braces

(braces)


[error] 2268-2268: too many spaces inside braces

(braces)


[error] 2268-2268: too many spaces inside braces

(braces)


[error] 2301-2301: too many spaces inside braces

(braces)


[error] 2301-2301: too many spaces inside braces

(braces)


[error] 2302-2302: too many spaces inside braces

(braces)


[error] 2302-2302: too many spaces inside braces

(braces)


[error] 4343-4343: too many spaces inside braces

(braces)


[error] 4343-4343: too many spaces inside braces

(braces)


[error] 4343-4343: too many spaces inside braces

(braces)


[error] 4343-4343: too many spaces inside braces

(braces)


[error] 4343-4343: too many spaces inside braces

(braces)


[error] 4343-4343: too many spaces inside braces

(braces)


[error] 4343-4343: too many spaces inside braces

(braces)


[error] 4343-4343: too many spaces inside braces

(braces)


[error] 4343-4343: too many spaces inside braces

(braces)


[error] 4343-4343: too many spaces inside braces

(braces)


[error] 4343-4343: too many spaces inside braces

(braces)


[error] 4343-4343: too many spaces inside braces

(braces)


[error] 4343-4343: too many spaces inside braces

(braces)


[error] 4343-4343: too many spaces inside braces

(braces)


[error] 4343-4343: too many spaces inside braces

(braces)


[error] 4343-4343: too many spaces inside braces

(braces)


[error] 4375-4375: too many spaces inside braces

(braces)


[error] 4375-4375: too many spaces inside braces

(braces)


[error] 4375-4375: too many spaces inside braces

(braces)


[error] 4375-4375: too many spaces inside braces

(braces)


[error] 4375-4375: too many spaces inside braces

(braces)


[error] 4375-4375: too many spaces inside braces

(braces)


[error] 4375-4375: too many spaces inside braces

(braces)


[error] 4375-4375: too many spaces inside braces

(braces)


[error] 4375-4375: too many spaces inside braces

(braces)


[error] 4375-4375: too many spaces inside braces

(braces)


[error] 4375-4375: too many spaces inside braces

(braces)


[error] 4375-4375: too many spaces inside braces

(braces)


[error] 4375-4375: too many spaces inside braces

(braces)


[error] 4375-4375: too many spaces inside braces

(braces)


[error] 4375-4375: too many spaces inside braces

(braces)


[error] 4375-4375: too many spaces inside braces

(braces)


[error] 4407-4407: too many spaces inside braces

(braces)


[error] 4407-4407: too many spaces inside braces

(braces)


[error] 4407-4407: too many spaces inside braces

(braces)


[error] 4407-4407: too many spaces inside braces

(braces)


[error] 4407-4407: too many spaces inside braces

(braces)


[error] 4407-4407: too many spaces inside braces

(braces)


[error] 4407-4407: too many spaces inside braces

(braces)


[error] 4407-4407: too many spaces inside braces

(braces)


[error] 4407-4407: too many spaces inside braces

(braces)


[error] 4407-4407: too many spaces inside braces

(braces)


[error] 4407-4407: too many spaces inside braces

(braces)


[error] 4407-4407: too many spaces inside braces

(braces)


[error] 4407-4407: too many spaces inside braces

(braces)


[error] 4407-4407: too many spaces inside braces

(braces)


[error] 4407-4407: too many spaces inside braces

(braces)


[error] 4407-4407: too many spaces inside braces

(braces)


[error] 4439-4439: too many spaces inside braces

(braces)


[error] 4439-4439: too many spaces inside braces

(braces)


[error] 4439-4439: too many spaces inside braces

(braces)


[error] 4439-4439: too many spaces inside braces

(braces)


[error] 4439-4439: too many spaces inside braces

(braces)


[error] 4439-4439: too many spaces inside braces

(braces)


[error] 4439-4439: too many spaces inside braces

(braces)


[error] 4439-4439: too many spaces inside braces

(braces)


[error] 4439-4439: too many spaces inside braces

(braces)


[error] 4439-4439: too many spaces inside braces

(braces)


[error] 4502-4502: too many spaces inside braces

(braces)


[error] 4502-4502: too many spaces inside braces

(braces)


[error] 4519-4519: too many spaces inside braces

(braces)


[error] 4519-4519: too many spaces inside braces

(braces)


[error] 4520-4520: too many spaces inside braces

(braces)


[error] 4520-4520: too many spaces inside braces

(braces)


[error] 4521-4521: too many spaces inside braces

(braces)


[error] 4521-4521: too many spaces inside braces

(braces)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: merge-manifest
🔇 Additional comments (3)
app/Livewire/Dashboard.php (1)

82-104: Pinned pages loading logic looks solid.

loadPinnedPages() correctly scopes to current user+team, filters by pinned, and caps to 5, matching the business rules and the model’s storage behavior. No obvious edge‑case landmines here — just good, stateful, self‑hosted goodness.

app/Http/Middleware/TrackRecentPages.php (1)

22-49: Deferred tracking logic is sound and cheap at request time.

The middleware only tracks authenticated GET HTML responses and defers the actual DB write + broadcast with dispatch(...)->afterResponse(), so page latency stays lean while the self‑hosted metal does its logging in the background. Team scoping via currentTeam()->id also keeps cross‑team leakage out.

No functional issues spotted here.

Also applies to: 51-72

openapi.yaml (1)

2105-2109: Yamllint is not configured in this repository.

Yamllint is not present in the CI/CD pipeline, package.json, pre-commit hooks, or any configuration files. There's no evidence that CI is currently complaining about the inline object spacing in the openapi.yaml file. The inline properties: { ... } objects on lines 2105-2109 and 4341-4344 are syntactically valid YAML and don't violate any configured rules. If yamllint enforcement is planned for the future, the spacing would need adjustment, but this is not currently a blocking issue.

Likely an incorrect or invalid review comment.

Comment on lines +11 to +31
class RecentsUpdated implements ShouldBroadcast
{
use Dispatchable, InteractsWithSockets, SerializesModels;

public $broadcastQueue = 'default';

public function __construct(
public int $userId
) {}

public function broadcastOn(): array
{
return [
new PrivateChannel("user.{$this->userId}"),
];
}

public function broadcastAs(): string
{
return 'RecentsUpdated';
}
Copy link
Contributor

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

Broadcast wiring is correct; consider typing $broadcastQueue.

Event name + private channel line up nicely with the Livewire Echo listener — recents will sync across tabs like a squadron of T‑800s.

Minor polish: declare $broadcastQueue as a typed string to match the “typed properties everywhere” guidance:

-    public $broadcastQueue = 'default';
+    public string $broadcastQueue = 'default';

Not critical, just tightening up the type armor.

🤖 Prompt for AI Agents
In app/Events/RecentsUpdated.php around lines 11–31, the public $broadcastQueue
property is untyped; change it to be a typed string property with the same
default value so it follows the project's typed-properties convention (declare
the property with a string type and keep the 'default' value).

Comment on lines 39 to +42
$this->privateKeys = PrivateKey::ownedByCurrentTeamCached();
$this->servers = Server::ownedByCurrentTeamCached();
$this->projects = Project::ownedByCurrentTeam()->with('environments')->get();
$this->loadPinnedPages();
Copy link
Contributor

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

Prefer cached team‑scoped query for projects.

You’re already using ownedByCurrentTeamCached() for private keys and servers; projects still call ownedByCurrentTeam()->with('environments')->get(). If a user hits the dashboard often, that extra query per request is like polling a serverless function instead of keeping a good ol’ metal box warm.

If Project::ownedByCurrentTeamCached() exists, consider switching to it (possibly with eager‑loaded environments) to avoid duplicate DB work.

🧰 Tools
🪛 PHPMD (2.15.0)

39-39: Avoid using static access to class '\App\Models\PrivateKey' in method 'mount'. (undefined)

(StaticAccess)


40-40: Avoid using static access to class '\App\Models\Server' in method 'mount'. (undefined)

(StaticAccess)

🤖 Prompt for AI Agents
In app/Livewire/Dashboard.php around lines 39 to 42, projects are loaded with
Project::ownedByCurrentTeam()->with('environments')->get(), which bypasses the
cached team‑scoped helper used for private keys and servers; replace this with
the cached variant (Project::ownedByCurrentTeamCached()) and ensure environments
are still eager‑loaded (use the cached method that accepts relations or apply
->with('environments') in the query before caching), and verify the cached
helper exists and returns the expected collection so the dashboard uses the
cached team‑scoped projects.

Comment on lines +30 to +76
public static function recordVisit(int $userId, int $teamId, string $url, string $label, ?string $sublabel = null): void
{
// No transaction/locking needed - minor race conditions are acceptable for recent pages
$record = self::firstOrCreate(
['user_id' => $userId, 'team_id' => $teamId],
['pages' => []]
);

$pages = collect($record->pages);

// Find existing entry to preserve pin status
$existing = $pages->firstWhere('url', $url);
$isPinned = $existing['pinned'] ?? false;
$pinnedAt = $existing['pinned_at'] ?? null;

// Remove existing entry for this URL (if any)
$pages = $pages->reject(fn ($p) => $p['url'] === $url);

// Separate pinned and unpinned
$pinned = $pages->filter(fn ($p) => ! empty($p['pinned']));
$unpinned = $pages->reject(fn ($p) => ! empty($p['pinned']));

// Create new entry
$newEntry = [
'url' => $url,
'label' => $label,
'sublabel' => $sublabel,
'visited_at' => now()->toISOString(),
'pinned' => $isPinned,
'pinned_at' => $pinnedAt,
];

if ($isPinned) {
// If pinned, add back to pinned list (preserve position by pinned_at)
$pinned->push($newEntry);
$pinned = $pinned->sortByDesc('pinned_at')->values();
} else {
// If not pinned, prepend to unpinned list
// Keep up to 10 unpinned for backfill when items are pinned
$unpinned->prepend($newEntry);
$unpinned = $unpinned->take(10)->values();
}

// Merge: pinned first (max 5), then unpinned (max 10 stored for backfill)
$record->pages = $pinned->take(5)->merge($unpinned->take(10))->values()->all();
$record->save();
}
Copy link
Contributor

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

Recents/pinning invariants are consistent; only tighten concurrency if UX ever demands it.

Nice work here:

  • recordVisit() preserves pin state, keeps at most 5 pinned and 10 stored unpinned, and always pushes the latest unpinned visit to the front.
  • togglePin() uses a transaction + lockForUpdate() to enforce the 5‑pin ceiling and re‑sorts pinned/unpinned in a deterministic way.
  • getRecent() cleanly exposes the “5 pinned + 5 unpinned” contract.

The only trade‑off is the intentional lack of locking in recordVisit(): two simultaneous visits could rarely drop one update or slightly reorder history. For a “recent pages” widget that’s acceptable best‑effort behavior; if product ever wants strict ordering across many tabs, you can reuse the transactional pattern from togglePin().

Also applies to: 78-142

🧰 Tools
🪛 PHPMD (2.15.0)

66-71: The method recordVisit uses an else expression. Else clauses are basically not necessary and you can simplify the code by not using them. (undefined)

(ElseExpression)

🤖 Prompt for AI Agents
In app/Models/UserRecentPage.php around lines 30-76 (and also apply same note to
78-142), the code intentionally omits transaction/locking in recordVisit causing
rare race conditions; add a clear comment/docblock above recordVisit that states
the intentional best-effort behavior, enumerates the invariants (max 5 pinned,
max 10 unpinned stored, latest unpinned prepended), and references togglePin()'s
transaction + lockForUpdate() pattern as the recommended change if strict
ordering/concurrency is later required; keep logic unchanged but document
trade-offs and where to change it.

Comment on lines 4373 to 4376
type: array
items:
properties: { id: { type: integer }, name: { type: string }, description: { type: string }, cores: { type: integer }, memory: { type: number }, disk: { type: integer }, prices: { type: array } }
type: object
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

head -4380 openapi.yaml | tail -20

Repository: coollabsio/coolify

Length of output: 180


🌐 Web query:

OpenAPI 3.0 array items required schema specification

💡 Result:

Short answer: you mark required properties of objects inside an array on the items schema (and use minItems/uniqueItems for array-level requirements). OpenAPI 3.0 follows a JSON‑Schema subset so:

  • To require certain properties for each object in the array:
    schema:
    type: array
    items:
    type: object
    required: [id, name]
    properties:
    id:
    type: integer
    name:
    type: string

  • To require at least one item in the array: add minItems: 1.

  • To require items be unique: add uniqueItems: true.

  • For fixed-position (tuple) schemas, use items as an array of schemas and control additionalItems as needed; each position’s object can itself use required. (OpenAPI 3.0 items must be a Schema Object for type: array.)

References: OpenAPI 3.0 Schema Object rules and array keywords (required/minItems/uniqueItems/items). [1][2]

Sources
[1] OpenAPI Specification v3.0.3 — Schema Object (items, required, minItems, uniqueItems).
[2] JSON Schema for OpenAPI 3.0 (array-related keywords).


Add items schema for the prices array—OpenAPI is not sentient and cannot guess what goes in there.

The prices field is defined as type: array with no items schema. Per OpenAPI 3.0 spec, arrays require an items schema to define what lives inside. Right now any client or code generator trying to deserialize this will get nothing—like asking a T-800 to understand serverless architecture (impossible, by design).

Add items: { type: object } at minimum, or flesh it out with actual properties if you know the price object structure. Self-hosted generators and real servers will thank you.

🧰 Tools
🪛 YAMLlint (1.37.1)

[error] 4375-4375: too many spaces inside braces

(braces)


[error] 4375-4375: too many spaces inside braces

(braces)


[error] 4375-4375: too many spaces inside braces

(braces)


[error] 4375-4375: too many spaces inside braces

(braces)


[error] 4375-4375: too many spaces inside braces

(braces)


[error] 4375-4375: too many spaces inside braces

(braces)


[error] 4375-4375: too many spaces inside braces

(braces)


[error] 4375-4375: too many spaces inside braces

(braces)


[error] 4375-4375: too many spaces inside braces

(braces)


[error] 4375-4375: too many spaces inside braces

(braces)


[error] 4375-4375: too many spaces inside braces

(braces)


[error] 4375-4375: too many spaces inside braces

(braces)


[error] 4375-4375: too many spaces inside braces

(braces)


[error] 4375-4375: too many spaces inside braces

(braces)


[error] 4375-4375: too many spaces inside braces

(braces)


[error] 4375-4375: too many spaces inside braces

(braces)

🤖 Prompt for AI Agents
In openapi.yaml around lines 4373 to 4376 the schema declares prices: { type:
array } but omits an items schema; add an items definition (at minimum items: {
type: object }) or, preferably, a detailed object schema with the price
properties (e.g., currency, amount, unit, etc.) so the OpenAPI spec is valid and
consumers/code generators can deserialize the array elements correctly.

andrasbacsai and others added 2 commits December 11, 2025 10:32
🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <[email protected]>
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