-
-
Notifications
You must be signed in to change notification settings - Fork 3.3k
feat: Add recents menu with pinnable pages #7516
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: next
Are you sure you want to change the base?
Conversation
- 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]>
|
@CodeRabbit review |
✅ Actions performedReview triggered.
|
|
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 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. 📒 Files selected for processing (1)
📝 WalkthroughWalkthroughAdds 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
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)
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes
Possibly related PRs
Pre-merge checks and finishing touches❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
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. Comment |
There was a problem hiding this 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 theAuthorizesRequeststrait.Per coding guidelines, Livewire components should add the
AuthorizesRequeststrait and check permissions inmount()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
📒 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/pintbefore 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
UsehandleError()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/pintbefore committing code
Files:
app/Events/RecentsUpdated.phpdatabase/migrations/2025_12_05_000000_create_user_recent_pages_table.phpresources/views/components/navbar.blade.phpresources/views/livewire/recents-menu.blade.phpapp/Http/Kernel.phpresources/views/livewire/project/index.blade.phpapp/Http/Middleware/TrackRecentPages.phpapp/Livewire/RecentsMenu.phpresources/views/livewire/settings-dropdown.blade.phpapp/Livewire/Dashboard.phpapp/Models/UserRecentPage.phpresources/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$fillablearray 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
Usegaputilities for spacing instead of margins
Files:
resources/views/components/navbar.blade.phpresources/views/livewire/recents-menu.blade.phpresources/views/livewire/project/index.blade.phpresources/views/livewire/settings-dropdown.blade.phpresources/views/livewire/dashboard.blade.php
**/*.blade.php
📄 CodeRabbit inference engine (.cursor/rules/coolify-ai-docs.mdc)
**/*.blade.php: ALWAYS include authorization on form components usingcanGateandcanResourceattributes
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.phpresources/views/livewire/recents-menu.blade.phpresources/views/livewire/project/index.blade.phpresources/views/livewire/settings-dropdown.blade.phpresources/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.
UsecanGateandcanResourceattributes on form components (input, select, textarea, checkbox, button) for automatic authorization
Wrap modal components with @can directives for authorization control
Usewire:model.livefor real-time two-way data binding between Livewire component and view
Files:
resources/views/livewire/recents-menu.blade.phpresources/views/livewire/project/index.blade.phpresources/views/livewire/settings-dropdown.blade.phpresources/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.phpresources/views/livewire/project/index.blade.phpresources/views/livewire/settings-dropdown.blade.phpresources/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 theAuthorizesRequeststrait 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.phpapp/Livewire/Dashboard.php
app/Models/**/*.php
📄 CodeRabbit inference engine (CLAUDE.md)
app/Models/**/*.php: When adding new database columns, ALWAYS update the model's$fillablearray 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.phpresources/views/livewire/recents-menu.blade.phpresources/views/livewire/project/index.blade.phpapp/Livewire/Dashboard.phpresources/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.phpresources/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.phpapp/Livewire/RecentsMenu.phpapp/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.phpresources/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.phpapp/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:clickdirectives) is safe. That modal wrapped in@can('createAnyResource')is tighter than John Connor's security protocols. Usinggap-2instead 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-ifis implemented correctly with a propertemplatewrapper. 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-5tow-4 h-4maintains 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.500msis 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/canResourceattributes for authorization. However, this button useswire:clickfor actions, so authorization should be handled in thetogglePin()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:
RecentsUpdatedis 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-2margin 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},.RecentsUpdatedensures 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
RecentsUpdatedto 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 recentsThat 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
RateLimiterandUserRecentPageare false positives. These are standard Laravel patterns:
- Facades: Laravel's
RateLimiteris a facade - static access is the intended usage pattern.- Static model methods: The
togglePin()andgetRecent()methods onUserRecentPageare 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! ThisloadPinnedPages()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()andteam()relationships are properly defined using Eloquent'sBelongsTo. 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$fillableincludes all necessary fields.The
$fillablearray correctly includesuser_id,team_id, andpages. This aligns with the coding guideline to always update the model's$fillablearray 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:deriveSublabelFromRouteis 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. 🌮
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]>
There was a problem hiding this 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
AuthorizesRequeststrait and check permissions with$this->authorize()calls in mount() and action methods." This component is missing theAuthorizesRequeststrait.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
pagesJSON 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
📒 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/pintbefore 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 useenv()outside config files in Laravel
Files:
app/Livewire/Dashboard.phpapp/Models/UserRecentPage.php
app/Livewire/**/*.php
📄 CodeRabbit inference engine (CLAUDE.md)
In Livewire Components, always add the
AuthorizesRequeststrait 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
UseownedByCurrentTeamCached()instead ofownedByCurrentTeam()->get()for team-scoped queries to avoid duplicate database queries
Queue heavy operations with Laravel Horizon
Files:
app/Livewire/Dashboard.phpapp/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.phpapp/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$fillablearray 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
pagescast to array handles JSON storage cleanly, and both relationships returnBelongsToinstances 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::transactionwithlockForUpdate()(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.
| public function mount() | ||
| { | ||
| $this->privateKeys = PrivateKey::ownedByCurrentTeamCached(); | ||
| $this->servers = Server::ownedByCurrentTeamCached(); | ||
| $this->projects = Project::ownedByCurrentTeam()->with('environments')->get(); | ||
| $this->loadPinnedPages(); | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
| 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)); | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
| 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]>
There was a problem hiding this 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
AuthorizesRequeststrait to the class.- Call
$this->authorize(...)inmount()(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
📒 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/pintbefore 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 useenv()outside config files in Laravel
Files:
app/Events/RecentsUpdated.phpapp/Livewire/Dashboard.phpapp/Http/Middleware/TrackRecentPages.phpapp/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
UseownedByCurrentTeamCached()instead ofownedByCurrentTeam()->get()for team-scoped queries to avoid duplicate database queries
Queue heavy operations with Laravel Horizon
Files:
app/Events/RecentsUpdated.phpapp/Livewire/Dashboard.phpapp/Http/Middleware/TrackRecentPages.phpapp/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.phpapp/Livewire/Dashboard.phpapp/Http/Middleware/TrackRecentPages.phpapp/Models/UserRecentPage.php
app/Livewire/**/*.php
📄 CodeRabbit inference engine (CLAUDE.md)
In Livewire Components, always add the
AuthorizesRequeststrait 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$fillablearray 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 bypinned, 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 viacurrentTeam()->idalso 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.
| 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'; | ||
| } |
There was a problem hiding this comment.
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).
| $this->privateKeys = PrivateKey::ownedByCurrentTeamCached(); | ||
| $this->servers = Server::ownedByCurrentTeamCached(); | ||
| $this->projects = Project::ownedByCurrentTeam()->with('environments')->get(); | ||
| $this->loadPinnedPages(); |
There was a problem hiding this comment.
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.
| 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(); | ||
| } |
There was a problem hiding this comment.
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.
| 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 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
🏁 Script executed:
head -4380 openapi.yaml | tail -20Repository: 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.
🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <[email protected]>
🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <[email protected]>
Summary
Features
Recents Menu (Sidebar)
Dashboard Integration
Technical Details
user_recent_pagestable with JSON column for storing pages per user+teamTrackRecentPagesmiddleware intercepts requests and derives labelsRecentsUpdatedevent broadcasts changes via WebSocketTest plan
🤖 Generated with Claude Code