From b7977a81a1d93599d98bbfb48b3f186b31ddbad5 Mon Sep 17 00:00:00 2001 From: Twyco Date: Wed, 15 Apr 2026 10:22:33 +0200 Subject: [PATCH 1/5] Organize web routes by domain --- routes/admin.php | 17 +++++ routes/projects.php | 161 ++++++++++++++++++++++++++++++++++++++++ routes/settings.php | 53 +++++++------ routes/teams.php | 22 ++++++ routes/web.php | 176 +------------------------------------------- 5 files changed, 235 insertions(+), 194 deletions(-) create mode 100644 routes/admin.php create mode 100644 routes/projects.php create mode 100644 routes/teams.php diff --git a/routes/admin.php b/routes/admin.php new file mode 100644 index 0000000..b9d6e09 --- /dev/null +++ b/routes/admin.php @@ -0,0 +1,17 @@ +prefix('admin') + ->name('admin.') + ->group(function (): void { + Route::prefix('users') + ->name('users.') + ->controller(UserManagementController::class) + ->group(function (): void { + Route::get('/', 'index')->name('index'); + Route::patch('{user}/permissions', 'update')->name('update'); + }); + }); diff --git a/routes/projects.php b/routes/projects.php new file mode 100644 index 0000000..0e8f24f --- /dev/null +++ b/routes/projects.php @@ -0,0 +1,161 @@ +prefix('projects') + ->name('projects.') + ->group(function (): void { + Route::get('/', [ProjectController::class, 'index'])->name('index'); + + Route::middleware('can:create-project')->group(function (): void { + Route::get('create', [ProjectController::class, 'create'])->name('create'); + Route::post('/', [ProjectController::class, 'store'])->name('store'); + }); + + Route::prefix('{project}') + ->group(function (): void { + Route::middleware('can:view-project,project')->group(function (): void { + Route::get('/', [ProjectController::class, 'show'])->name('show'); + Route::get('board', [ProjectBoardController::class, 'index'])->name('board.index'); + Route::get('backlog', [ProjectController::class, 'backlog'])->name('backlog.index'); + }); + + Route::delete('/', [ProjectController::class, 'destroy']) + ->middleware('can:access-admin') + ->name('destroy'); + + Route::middleware('can:admin-project,project')->group(function (): void { + Route::get('access', [ProjectController::class, 'access'])->name('access'); + + Route::prefix('permissions')->name('permissions.')->group(function (): void { + Route::patch('users/{user}', [ProjectController::class, 'updateUserPermissions']) + ->name('users.update'); + Route::patch('teams/{team}', [ProjectController::class, 'updateTeamPermissions']) + ->name('teams.update'); + }); + }); + + Route::scopeBindings()->group(function (): void { + Route::prefix('tickets') + ->name('tickets.') + ->group(function (): void { + Route::middleware('can:write-project,project')->group(function (): void { + Route::get('create', [ProjectTicketController::class, 'create'])->name('create'); + Route::post('/', [ProjectTicketController::class, 'store'])->name('store'); + }); + + Route::prefix('{ticket}')->group(function (): void { + Route::get('/', [ProjectTicketController::class, 'show']) + ->middleware('can:view-project,project') + ->name('show'); + + Route::middleware('can:write-project,project')->group(function (): void { + Route::patch('/', [ProjectTicketController::class, 'update'])->name('update'); + Route::patch('status', [ProjectBoardController::class, 'move'])->name('status.update'); + + Route::prefix('branches') + ->name('branches.') + ->controller(ProjectTicketBranchController::class) + ->group(function (): void { + Route::get('validate', 'validate')->name('validate'); + Route::post('/', 'store')->name('store'); + }); + + Route::prefix('links') + ->name('links.') + ->controller(ProjectTicketLinkController::class) + ->group(function (): void { + Route::post('/', 'store')->name('store'); + Route::delete('{ticketLink}', 'destroy') + ->withoutScopedBindings() + ->name('destroy'); + }); + }); + }); + }); + + Route::prefix('settings') + ->name('settings.') + ->middleware('can:admin-project,project') + ->group(function (): void { + Route::get('general', [ProjectController::class, 'general'])->name('general'); + + Route::get('board', [ProjectBoardSettingsController::class, 'show'])->name('board'); + + Route::prefix('board') + ->name('board.') + ->controller(ProjectBoardSettingsController::class) + ->group(function (): void { + Route::prefix('columns')->name('columns.')->group(function (): void { + Route::post('/', 'store')->name('store'); + Route::patch('{boardColumn}', 'update')->name('update'); + Route::delete('{boardColumn}', 'destroy')->name('destroy'); + }); + + Route::prefix('statuses')->name('statuses.')->group(function (): void { + Route::post('/', 'storeStatus')->name('store'); + Route::patch('{ticketStatus}', 'updateStatus')->name('update'); + Route::delete('{ticketStatus}', 'destroyStatus')->name('destroy'); + Route::patch('{ticketStatus}/transitions', 'updateStatusTransitions') + ->name('transitions.update'); + }); + }); + + Route::get('metadata', [ProjectTicketMetadataSettingsController::class, 'show']) + ->name('metadata'); + Route::patch('metadata', [ProjectTicketMetadataSettingsController::class, 'update']) + ->name('metadata.update'); + + Route::prefix('metadata') + ->name('metadata.') + ->controller(ProjectTicketMetadataSettingsController::class) + ->group(function (): void { + Route::prefix('types')->name('types.')->group(function (): void { + Route::post('/', 'storeType')->name('store'); + Route::patch('{ticketType}', 'updateType')->name('update'); + Route::delete('{ticketType}', 'destroyType')->name('destroy'); + }); + + Route::prefix('priorities')->name('priorities.')->group(function (): void { + Route::post('/', 'storePriority')->name('store'); + Route::patch('{ticketPriority}', 'updatePriority')->name('update'); + Route::delete('{ticketPriority}', 'destroyPriority')->name('destroy'); + }); + + Route::prefix('fields')->name('fields.')->group(function (): void { + Route::post('/', 'storeFieldDefinition')->name('store'); + Route::patch('{ticketFieldDefinition}', 'updateFieldDefinition')->name('update'); + Route::delete('{ticketFieldDefinition}', 'destroyFieldDefinition')->name('destroy'); + }); + + Route::prefix('link-types')->name('link-types.')->group(function (): void { + Route::post('/', 'storeLinkType')->name('store'); + Route::patch('{ticketLinkType}', 'updateLinkType')->name('update'); + Route::delete('{ticketLinkType}', 'destroyLinkType')->name('destroy'); + }); + }); + + Route::get('repositories', [ProjectRepositorySettingsController::class, 'show']) + ->name('repositories'); + + Route::prefix('repositories') + ->name('repositories.') + ->controller(ProjectRepositorySettingsController::class) + ->group(function (): void { + Route::post('/', 'store')->name('store'); + Route::patch('{repositoryConnection}', 'update')->name('update'); + Route::delete('{repositoryConnection}', 'destroy')->name('destroy'); + }); + }); + }); + }); + }); diff --git a/routes/settings.php b/routes/settings.php index 484c10f..c03052c 100644 --- a/routes/settings.php +++ b/routes/settings.php @@ -5,30 +5,41 @@ use App\Http\Controllers\Settings\ProfileController; use App\Http\Controllers\Settings\SecurityController; use Illuminate\Support\Facades\Route; -Route::middleware(['auth'])->group(function () { - Route::redirect('settings', '/settings/profile'); +Route::middleware('auth') + ->prefix('settings') + ->group(function (): void { + Route::redirect('/', '/settings/profile'); - Route::get('settings/profile', [ProfileController::class, 'edit'])->name('profile.edit'); - Route::patch('settings/profile', [ProfileController::class, 'update'])->name('profile.update'); -}); + Route::controller(ProfileController::class) + ->prefix('profile') + ->group(function (): void { + Route::get('/', 'edit')->name('profile.edit'); + Route::patch('/', 'update')->name('profile.update'); + }); + }); -Route::middleware(['auth', 'verified'])->group(function () { - Route::delete('settings/profile', [ProfileController::class, 'destroy'])->name('profile.destroy'); +Route::middleware(['auth', 'verified']) + ->prefix('settings') + ->group(function (): void { + Route::delete('profile', [ProfileController::class, 'destroy'])->name('profile.destroy'); - Route::get('settings/security', [SecurityController::class, 'edit'])->name('security.edit'); + Route::controller(SecurityController::class)->group(function (): void { + Route::get('security', 'edit')->name('security.edit'); - Route::put('settings/password', [SecurityController::class, 'update']) - ->middleware('throttle:6,1') - ->name('user-password.update'); + Route::put('password', 'update') + ->middleware('throttle:6,1') + ->name('user-password.update'); + }); - Route::get('settings/git-connections', [GitConnectionController::class, 'edit']) - ->name('git-connections.edit'); - Route::post('settings/git-connections', [GitConnectionController::class, 'store']) - ->name('git-connections.store'); - Route::patch('settings/git-connections/{gitConnection}', [GitConnectionController::class, 'update']) - ->name('git-connections.update'); - Route::delete('settings/git-connections/{gitConnection}', [GitConnectionController::class, 'destroy']) - ->name('git-connections.destroy'); + Route::prefix('git-connections') + ->name('git-connections.') + ->controller(GitConnectionController::class) + ->group(function (): void { + Route::get('/', 'edit')->name('edit'); + Route::post('/', 'store')->name('store'); + Route::patch('{gitConnection}', 'update')->name('update'); + Route::delete('{gitConnection}', 'destroy')->name('destroy'); + }); - Route::inertia('settings/appearance', 'settings/Appearance')->name('appearance.edit'); -}); + Route::inertia('appearance', 'settings/Appearance')->name('appearance.edit'); + }); diff --git a/routes/teams.php b/routes/teams.php new file mode 100644 index 0000000..ee990f5 --- /dev/null +++ b/routes/teams.php @@ -0,0 +1,22 @@ +prefix('teams') + ->name('teams.') + ->controller(TeamController::class) + ->group(function (): void { + Route::get('/', 'index') + ->middleware('can:view-teams') + ->name('index'); + + Route::post('/', 'store') + ->middleware('can:create-team') + ->name('store'); + + Route::patch('{team}/members', 'updateMembers')->name('members.update'); + Route::patch('{team}/permissions/users/{user}', 'updateUserPermissions') + ->name('permissions.users.update'); + }); diff --git a/routes/web.php b/routes/web.php index e47a323..2fa1e60 100644 --- a/routes/web.php +++ b/routes/web.php @@ -1,15 +1,5 @@ group(function () { Route::inertia('dashboard', 'Dashboard')->name('dashboard'); - - Route::get('projects', [ProjectController::class, 'index']) - ->name('projects.index'); - Route::get('projects/create', [ProjectController::class, 'create']) - ->middleware('can:create-project') - ->name('projects.create'); - Route::post('projects', [ProjectController::class, 'store']) - ->middleware('can:create-project') - ->name('projects.store'); - Route::get('projects/{project}', [ProjectController::class, 'show']) - ->middleware('can:view-project,project') - ->name('projects.show'); - - Route::delete('projects/{project}', [ProjectController::class, 'destroy']) - ->middleware('can:access-admin') - ->name('projects.destroy'); - - Route::get('projects/{project}/access', [ProjectController::class, 'access']) - ->middleware('can:admin-project,project') - ->name('projects.access'); - Route::patch('projects/{project}/permissions/users/{user}', [ProjectController::class, 'updateUserPermissions']) - ->middleware('can:admin-project,project') - ->name('projects.permissions.users.update'); - Route::patch('projects/{project}/permissions/teams/{team}', [ProjectController::class, 'updateTeamPermissions']) - ->middleware('can:admin-project,project') - ->name('projects.permissions.teams.update'); - - Route::scopeBindings()->group(function (): void { - Route::get('projects/{project}/board', [ProjectBoardController::class, 'index']) - ->middleware('can:view-project,project') - ->name('projects.board.index'); - Route::get('projects/{project}/backlog', [ProjectController::class, 'backlog']) - ->middleware('can:view-project,project') - ->name('projects.backlog.index'); - Route::get('projects/{project}/tickets/create', [ProjectTicketController::class, 'create']) - ->middleware('can:write-project,project') - ->name('projects.tickets.create'); - Route::post('projects/{project}/tickets', [ProjectTicketController::class, 'store']) - ->middleware('can:write-project,project') - ->name('projects.tickets.store'); - Route::get('projects/{project}/tickets/{ticket}', [ProjectTicketController::class, 'show']) - ->middleware('can:view-project,project') - ->name('projects.tickets.show'); - Route::patch('projects/{project}/tickets/{ticket}', [ProjectTicketController::class, 'update']) - ->middleware('can:write-project,project') - ->name('projects.tickets.update'); - Route::get('projects/{project}/tickets/{ticket}/branches/validate', [ProjectTicketBranchController::class, 'validate']) - ->middleware('can:write-project,project') - ->name('projects.tickets.branches.validate'); - Route::post('projects/{project}/tickets/{ticket}/branches', [ProjectTicketBranchController::class, 'store']) - ->middleware('can:write-project,project') - ->name('projects.tickets.branches.store'); - Route::post('projects/{project}/tickets/{ticket}/links', [ProjectTicketLinkController::class, 'store']) - ->middleware('can:write-project,project') - ->name('projects.tickets.links.store'); - Route::delete('projects/{project}/tickets/{ticket}/links/{ticketLink}', [ProjectTicketLinkController::class, 'destroy']) - ->middleware('can:write-project,project') - ->withoutScopedBindings() - ->name('projects.tickets.links.destroy'); - Route::patch('projects/{project}/tickets/{ticket}/status', [ProjectBoardController::class, 'move']) - ->middleware('can:write-project,project') - ->name('projects.tickets.status.update'); - Route::get('projects/{project}/settings/general', [ProjectController::class, 'general']) - ->middleware('can:admin-project,project') - ->name('projects.settings.general'); - Route::get('projects/{project}/settings/board', [ProjectBoardSettingsController::class, 'show']) - ->middleware('can:admin-project,project') - ->name('projects.settings.board'); - Route::post('projects/{project}/settings/board/columns', [ProjectBoardSettingsController::class, 'store']) - ->middleware('can:admin-project,project') - ->name('projects.settings.board.columns.store'); - Route::patch('projects/{project}/settings/board/columns/{boardColumn}', [ProjectBoardSettingsController::class, 'update']) - ->middleware('can:admin-project,project') - ->name('projects.settings.board.columns.update'); - Route::delete('projects/{project}/settings/board/columns/{boardColumn}', [ProjectBoardSettingsController::class, 'destroy']) - ->middleware('can:admin-project,project') - ->name('projects.settings.board.columns.destroy'); - Route::post('projects/{project}/settings/board/statuses', [ProjectBoardSettingsController::class, 'storeStatus']) - ->middleware('can:admin-project,project') - ->name('projects.settings.board.statuses.store'); - Route::patch('projects/{project}/settings/board/statuses/{ticketStatus}', [ProjectBoardSettingsController::class, 'updateStatus']) - ->middleware('can:admin-project,project') - ->name('projects.settings.board.statuses.update'); - Route::delete('projects/{project}/settings/board/statuses/{ticketStatus}', [ProjectBoardSettingsController::class, 'destroyStatus']) - ->middleware('can:admin-project,project') - ->name('projects.settings.board.statuses.destroy'); - Route::patch('projects/{project}/settings/board/statuses/{ticketStatus}/transitions', [ProjectBoardSettingsController::class, 'updateStatusTransitions']) - ->middleware('can:admin-project,project') - ->name('projects.settings.board.statuses.transitions.update'); - Route::get('projects/{project}/settings/metadata', [ProjectTicketMetadataSettingsController::class, 'show']) - ->middleware('can:admin-project,project') - ->name('projects.settings.metadata'); - Route::get('projects/{project}/settings/repositories', [ProjectRepositorySettingsController::class, 'show']) - ->middleware('can:admin-project,project') - ->name('projects.settings.repositories'); - Route::post('projects/{project}/settings/repositories', [ProjectRepositorySettingsController::class, 'store']) - ->middleware('can:admin-project,project') - ->name('projects.settings.repositories.store'); - Route::patch('projects/{project}/settings/repositories/{repositoryConnection}', [ProjectRepositorySettingsController::class, 'update']) - ->middleware('can:admin-project,project') - ->name('projects.settings.repositories.update'); - Route::delete('projects/{project}/settings/repositories/{repositoryConnection}', [ProjectRepositorySettingsController::class, 'destroy']) - ->middleware('can:admin-project,project') - ->name('projects.settings.repositories.destroy'); - Route::post('projects/{project}/settings/metadata/types', [ProjectTicketMetadataSettingsController::class, 'storeType']) - ->middleware('can:admin-project,project') - ->name('projects.settings.metadata.types.store'); - Route::patch('projects/{project}/settings/metadata/types/{ticketType}', [ProjectTicketMetadataSettingsController::class, 'updateType']) - ->middleware('can:admin-project,project') - ->name('projects.settings.metadata.types.update'); - Route::patch('projects/{project}/settings/metadata', [ProjectTicketMetadataSettingsController::class, 'update']) - ->middleware('can:admin-project,project') - ->name('projects.settings.metadata.update'); - Route::delete('projects/{project}/settings/metadata/types/{ticketType}', [ProjectTicketMetadataSettingsController::class, 'destroyType']) - ->middleware('can:admin-project,project') - ->name('projects.settings.metadata.types.destroy'); - Route::post('projects/{project}/settings/metadata/priorities', [ProjectTicketMetadataSettingsController::class, 'storePriority']) - ->middleware('can:admin-project,project') - ->name('projects.settings.metadata.priorities.store'); - Route::patch('projects/{project}/settings/metadata/priorities/{ticketPriority}', [ProjectTicketMetadataSettingsController::class, 'updatePriority']) - ->middleware('can:admin-project,project') - ->name('projects.settings.metadata.priorities.update'); - Route::delete('projects/{project}/settings/metadata/priorities/{ticketPriority}', [ProjectTicketMetadataSettingsController::class, 'destroyPriority']) - ->middleware('can:admin-project,project') - ->name('projects.settings.metadata.priorities.destroy'); - Route::post('projects/{project}/settings/metadata/fields', [ProjectTicketMetadataSettingsController::class, 'storeFieldDefinition']) - ->middleware('can:admin-project,project') - ->name('projects.settings.metadata.fields.store'); - Route::patch('projects/{project}/settings/metadata/fields/{ticketFieldDefinition}', [ProjectTicketMetadataSettingsController::class, 'updateFieldDefinition']) - ->middleware('can:admin-project,project') - ->name('projects.settings.metadata.fields.update'); - Route::delete('projects/{project}/settings/metadata/fields/{ticketFieldDefinition}', [ProjectTicketMetadataSettingsController::class, 'destroyFieldDefinition']) - ->middleware('can:admin-project,project') - ->name('projects.settings.metadata.fields.destroy'); - Route::post('projects/{project}/settings/metadata/link-types', [ProjectTicketMetadataSettingsController::class, 'storeLinkType']) - ->middleware('can:admin-project,project') - ->name('projects.settings.metadata.link-types.store'); - Route::patch('projects/{project}/settings/metadata/link-types/{ticketLinkType}', [ProjectTicketMetadataSettingsController::class, 'updateLinkType']) - ->middleware('can:admin-project,project') - ->name('projects.settings.metadata.link-types.update'); - Route::delete('projects/{project}/settings/metadata/link-types/{ticketLinkType}', [ProjectTicketMetadataSettingsController::class, 'destroyLinkType']) - ->middleware('can:admin-project,project') - ->name('projects.settings.metadata.link-types.destroy'); - }); - - Route::get('teams', [TeamController::class, 'index']) - ->middleware('can:view-teams') - ->name('teams.index'); - Route::post('teams', [TeamController::class, 'store']) - ->middleware('can:create-team') - ->name('teams.store'); - Route::patch('teams/{team}/members', [TeamController::class, 'updateMembers']) - ->name('teams.members.update'); - Route::patch('teams/{team}/permissions/users/{user}', [TeamController::class, 'updateUserPermissions']) - ->name('teams.permissions.users.update'); }); -Route::middleware(['auth', 'verified', 'can:access-admin']) - ->prefix('admin') - ->name('admin.') - ->group(function () { - Route::get('users', [UserManagementController::class, 'index'])->name('users.index'); - Route::patch('users/{user}/permissions', [UserManagementController::class, 'update'])->name('users.update'); - }); - +require __DIR__.'/admin.php'; +require __DIR__.'/projects.php'; require __DIR__.'/settings.php'; +require __DIR__.'/teams.php'; From 4a3a53b69316a099027e7f27d87c6078eefac253 Mon Sep 17 00:00:00 2001 From: Twyco Date: Wed, 15 Apr 2026 10:25:55 +0200 Subject: [PATCH 2/5] Document project route structure skill --- .../skills/project-route-structure/SKILL.md | 66 +++++++++++++++++++ AGENTS.md | 1 + 2 files changed, 67 insertions(+) create mode 100644 .agents/skills/project-route-structure/SKILL.md diff --git a/.agents/skills/project-route-structure/SKILL.md b/.agents/skills/project-route-structure/SKILL.md new file mode 100644 index 0000000..c558e2d --- /dev/null +++ b/.agents/skills/project-route-structure/SKILL.md @@ -0,0 +1,66 @@ +--- +name: project-route-structure +description: "Always apply in this project when creating, moving, or refactoring Laravel route definitions in routes/*.php; changing route prefixes, names, middleware, scoped bindings, or route files; or adding frontend-facing endpoints that Wayfinder generates. Documents the KanbanGit route-file layout and grouping conventions." +--- + +# Project Route Structure + +## When to Apply + +Activate this skill whenever work in this project changes: + +- `routes/*.php` +- Route names, URI prefixes, middleware groups, scoped bindings, or controller route registrations +- Frontend-facing endpoints that require regenerated Wayfinder output + +Use this together with `laravel-best-practices`. Also activate `wayfinder-development` when route changes affect generated frontend route/action imports. + +## Route Files + +- `routes/web.php` is the web entry point only. Keep public shell routes here, such as `home` and `dashboard`, then require domain route files. +- `routes/projects.php` owns all `projects.*` routes. +- `routes/admin.php` owns all `admin.*` routes. +- `routes/teams.php` owns all `teams.*` routes. +- `routes/settings.php` owns account settings routes, but keeps their existing route names such as `profile.edit`, `security.edit`, and `git-connections.edit`. + +Add a new route file only when a domain gets large enough to stand alone. Otherwise, extend the existing domain file. + +## Grouping Pattern + +- Put shared middleware, URI prefixes, route name prefixes, and controller declarations on groups instead of repeating them on each route. +- Prefer this nesting order when it reads naturally: `middleware(...)`, `prefix(...)`, `name(...)`, `controller(...)`, then `group(...)`. +- Keep public route names stable unless the user explicitly asks for a public interface change. +- Keep route URIs stable when reorganizing route files; this kind of change should be structural. +- Use `function (): void` for route group closures. + +## Project Routes + +Project routes should follow this shape: + +- Top-level group: `middleware(['auth', 'verified'])`, `prefix('projects')`, `name('projects.')`. +- Project create/store routes live under `can:create-project`. +- Routes under `{project}` use project gates by capability: + - Read pages: `can:view-project,project` + - Mutations: `can:write-project,project` + - Project settings and access management: `can:admin-project,project` + - Project delete: `can:access-admin` +- `projects/{project}/permissions/...` must not be inside `Route::scopeBindings()`, because `Project` does not define `users()` or `teams()` child relationships. +- Use `Route::scopeBindings()` only around nested project resources that should bind through project-owned relationships, such as tickets and project settings children. +- Keep ticket subroutes grouped under `tickets.` and settings subroutes grouped under `settings.`. + +## Settings Routes + +- Keep the `settings` URI prefix grouped. +- Keep profile edit/update behind `auth` only. +- Keep profile deletion, security, git connections, and appearance behind `auth` plus `verified`. +- Do not rename settings routes to a `settings.*` namespace unless the frontend/tests are intentionally updated. + +## Verification + +After changing route files: + +1. Run `php artisan route:list --except-vendor`. +2. Run `php artisan wayfinder:generate --with-form --no-interaction` when any route signature, name, method, or controller action changed. +3. Run `vendor/bin/pint --dirty --format agent` for PHP route formatting. +4. Run the affected feature tests, usually `php artisan test --compact tests/Feature/Projects tests/Feature/Teams tests/Feature/Admin tests/Feature/Settings`. +5. Run `git diff --check` before finalizing. diff --git a/AGENTS.md b/AGENTS.md index db0b49c..aed5aa0 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -34,6 +34,7 @@ This project has domain-specific skills available. You MUST activate the relevan - `fortify-development` — ACTIVATE when the user works on authentication in Laravel. This includes login, registration, password reset, email verification, two-factor authentication (2FA/TOTP/QR codes/recovery codes), profile updates, password confirmation, or any auth-related routes and controllers. Activate when the user mentions Fortify, auth, authentication, login, register, signup, forgot password, verify email, 2FA, or references app/Actions/Fortify/, CreateNewUser, UpdateUserProfileInformation, FortifyServiceProvider, config/fortify.php, or auth guards. Fortify is the frontend-agnostic authentication backend for Laravel that registers all auth routes and controllers. Also activate when building SPA or headless authentication, customizing login redirects, overriding response contracts like LoginResponse, or configuring login throttling. Do NOT activate for Laravel Passport (OAuth2 API tokens), Socialite (OAuth social login), or non-auth Laravel features. - `laravel-best-practices` — Apply this skill whenever writing, reviewing, or refactoring Laravel PHP code. This includes creating or modifying controllers, models, migrations, form requests, policies, jobs, scheduled commands, service classes, and Eloquent queries. Triggers for N+1 and query performance issues, caching strategies, authorization and security patterns, validation, error handling, queue and job configuration, route definitions, and architectural decisions. Also use for Laravel code reviews and refactoring existing Laravel code to follow best practices. Covers any task involving Laravel backend PHP code patterns. +- `project-route-structure` — Always apply in this project when creating, moving, or refactoring Laravel route definitions in routes/*.php; changing route prefixes, names, middleware, scoped bindings, or route files; or adding frontend-facing endpoints that Wayfinder generates. Documents the KanbanGit route-file layout and grouping conventions. - `wayfinder-development` — Use this skill for Laravel Wayfinder which auto-generates typed functions for Laravel controllers and routes. ALWAYS use this skill when frontend code needs to call backend routes or controller actions. Trigger when: connecting any React/Vue/Svelte/Inertia frontend to Laravel controllers, routes, building end-to-end features with both frontend and backend, wiring up forms or links to backend endpoints, fixing route-related TypeScript errors, importing from @/actions or @/routes, or running wayfinder:generate. Use Wayfinder route functions instead of hardcoded URLs. Covers: wayfinder() vite plugin, .url()/.get()/.post()/.form(), query params, route model binding, tree-shaking. Do not use for backend-only task - `inertia-vue-development` — Develops Inertia.js v2 Vue client-side applications. Activates when creating Vue pages, forms, or navigation; using ,
, useForm, or router; working with deferred props, prefetching, or polling; or when user mentions Vue with Inertia, Vue pages, Vue forms, or Vue navigation. - `tailwindcss-development` — Always invoke when the user's message includes 'tailwind' in any form. Also invoke for: building responsive grid layouts (multi-column card grids, product grids), flex/grid page structures (dashboards with sidebars, fixed topbars, mobile-toggle navs), styling UI components (cards, tables, navbars, pricing sections, forms, inputs, badges), adding dark mode variants, fixing spacing or typography, and Tailwind v3/v4 work. The core use case: writing or fixing Tailwind utility classes in HTML templates (Blade, JSX, Vue). Skip for backend PHP logic, database queries, API routes, JavaScript with no HTML/CSS component, CSS file audits, build tool configuration, and vanilla CSS. From 0ed2b2429ca1d0aa073cc4b9d7bce6c3ba0aabdd Mon Sep 17 00:00:00 2001 From: Twyco Date: Wed, 15 Apr 2026 15:56:26 +0200 Subject: [PATCH 3/5] pr improvements --- bootstrap/app.php | 2 +- routes/console.php | 7 -- routes/projects.php | 161 ---------------------------------- routes/teams.php | 22 ----- routes/web.php | 9 +- routes/{ => web}/admin.php | 2 +- routes/web/projects.php | 158 +++++++++++++++++++++++++++++++++ routes/{ => web}/settings.php | 0 routes/web/teams.php | 14 +++ 9 files changed, 179 insertions(+), 196 deletions(-) delete mode 100644 routes/projects.php delete mode 100644 routes/teams.php rename routes/{ => web}/admin.php (89%) create mode 100644 routes/web/projects.php rename routes/{ => web}/settings.php (100%) create mode 100644 routes/web/teams.php diff --git a/bootstrap/app.php b/bootstrap/app.php index c4f1cc5..d68d267 100644 --- a/bootstrap/app.php +++ b/bootstrap/app.php @@ -10,7 +10,7 @@ use Illuminate\Http\Middleware\AddLinkHeadersForPreloadedAssets; return Application::configure(basePath: dirname(__DIR__)) ->withRouting( web: __DIR__.'/../routes/web.php', - commands: __DIR__.'/../routes/console.php', + commands: __DIR__ . '/../routes/console.php', health: '/up', ) ->withMiddleware(function (Middleware $middleware): void { diff --git a/routes/console.php b/routes/console.php index 3c9adf1..b3d9bbc 100644 --- a/routes/console.php +++ b/routes/console.php @@ -1,8 +1 @@ comment(Inspiring::quote()); -})->purpose('Display an inspiring quote'); diff --git a/routes/projects.php b/routes/projects.php deleted file mode 100644 index 0e8f24f..0000000 --- a/routes/projects.php +++ /dev/null @@ -1,161 +0,0 @@ -prefix('projects') - ->name('projects.') - ->group(function (): void { - Route::get('/', [ProjectController::class, 'index'])->name('index'); - - Route::middleware('can:create-project')->group(function (): void { - Route::get('create', [ProjectController::class, 'create'])->name('create'); - Route::post('/', [ProjectController::class, 'store'])->name('store'); - }); - - Route::prefix('{project}') - ->group(function (): void { - Route::middleware('can:view-project,project')->group(function (): void { - Route::get('/', [ProjectController::class, 'show'])->name('show'); - Route::get('board', [ProjectBoardController::class, 'index'])->name('board.index'); - Route::get('backlog', [ProjectController::class, 'backlog'])->name('backlog.index'); - }); - - Route::delete('/', [ProjectController::class, 'destroy']) - ->middleware('can:access-admin') - ->name('destroy'); - - Route::middleware('can:admin-project,project')->group(function (): void { - Route::get('access', [ProjectController::class, 'access'])->name('access'); - - Route::prefix('permissions')->name('permissions.')->group(function (): void { - Route::patch('users/{user}', [ProjectController::class, 'updateUserPermissions']) - ->name('users.update'); - Route::patch('teams/{team}', [ProjectController::class, 'updateTeamPermissions']) - ->name('teams.update'); - }); - }); - - Route::scopeBindings()->group(function (): void { - Route::prefix('tickets') - ->name('tickets.') - ->group(function (): void { - Route::middleware('can:write-project,project')->group(function (): void { - Route::get('create', [ProjectTicketController::class, 'create'])->name('create'); - Route::post('/', [ProjectTicketController::class, 'store'])->name('store'); - }); - - Route::prefix('{ticket}')->group(function (): void { - Route::get('/', [ProjectTicketController::class, 'show']) - ->middleware('can:view-project,project') - ->name('show'); - - Route::middleware('can:write-project,project')->group(function (): void { - Route::patch('/', [ProjectTicketController::class, 'update'])->name('update'); - Route::patch('status', [ProjectBoardController::class, 'move'])->name('status.update'); - - Route::prefix('branches') - ->name('branches.') - ->controller(ProjectTicketBranchController::class) - ->group(function (): void { - Route::get('validate', 'validate')->name('validate'); - Route::post('/', 'store')->name('store'); - }); - - Route::prefix('links') - ->name('links.') - ->controller(ProjectTicketLinkController::class) - ->group(function (): void { - Route::post('/', 'store')->name('store'); - Route::delete('{ticketLink}', 'destroy') - ->withoutScopedBindings() - ->name('destroy'); - }); - }); - }); - }); - - Route::prefix('settings') - ->name('settings.') - ->middleware('can:admin-project,project') - ->group(function (): void { - Route::get('general', [ProjectController::class, 'general'])->name('general'); - - Route::get('board', [ProjectBoardSettingsController::class, 'show'])->name('board'); - - Route::prefix('board') - ->name('board.') - ->controller(ProjectBoardSettingsController::class) - ->group(function (): void { - Route::prefix('columns')->name('columns.')->group(function (): void { - Route::post('/', 'store')->name('store'); - Route::patch('{boardColumn}', 'update')->name('update'); - Route::delete('{boardColumn}', 'destroy')->name('destroy'); - }); - - Route::prefix('statuses')->name('statuses.')->group(function (): void { - Route::post('/', 'storeStatus')->name('store'); - Route::patch('{ticketStatus}', 'updateStatus')->name('update'); - Route::delete('{ticketStatus}', 'destroyStatus')->name('destroy'); - Route::patch('{ticketStatus}/transitions', 'updateStatusTransitions') - ->name('transitions.update'); - }); - }); - - Route::get('metadata', [ProjectTicketMetadataSettingsController::class, 'show']) - ->name('metadata'); - Route::patch('metadata', [ProjectTicketMetadataSettingsController::class, 'update']) - ->name('metadata.update'); - - Route::prefix('metadata') - ->name('metadata.') - ->controller(ProjectTicketMetadataSettingsController::class) - ->group(function (): void { - Route::prefix('types')->name('types.')->group(function (): void { - Route::post('/', 'storeType')->name('store'); - Route::patch('{ticketType}', 'updateType')->name('update'); - Route::delete('{ticketType}', 'destroyType')->name('destroy'); - }); - - Route::prefix('priorities')->name('priorities.')->group(function (): void { - Route::post('/', 'storePriority')->name('store'); - Route::patch('{ticketPriority}', 'updatePriority')->name('update'); - Route::delete('{ticketPriority}', 'destroyPriority')->name('destroy'); - }); - - Route::prefix('fields')->name('fields.')->group(function (): void { - Route::post('/', 'storeFieldDefinition')->name('store'); - Route::patch('{ticketFieldDefinition}', 'updateFieldDefinition')->name('update'); - Route::delete('{ticketFieldDefinition}', 'destroyFieldDefinition')->name('destroy'); - }); - - Route::prefix('link-types')->name('link-types.')->group(function (): void { - Route::post('/', 'storeLinkType')->name('store'); - Route::patch('{ticketLinkType}', 'updateLinkType')->name('update'); - Route::delete('{ticketLinkType}', 'destroyLinkType')->name('destroy'); - }); - }); - - Route::get('repositories', [ProjectRepositorySettingsController::class, 'show']) - ->name('repositories'); - - Route::prefix('repositories') - ->name('repositories.') - ->controller(ProjectRepositorySettingsController::class) - ->group(function (): void { - Route::post('/', 'store')->name('store'); - Route::patch('{repositoryConnection}', 'update')->name('update'); - Route::delete('{repositoryConnection}', 'destroy')->name('destroy'); - }); - }); - }); - }); - }); diff --git a/routes/teams.php b/routes/teams.php deleted file mode 100644 index ee990f5..0000000 --- a/routes/teams.php +++ /dev/null @@ -1,22 +0,0 @@ -prefix('teams') - ->name('teams.') - ->controller(TeamController::class) - ->group(function (): void { - Route::get('/', 'index') - ->middleware('can:view-teams') - ->name('index'); - - Route::post('/', 'store') - ->middleware('can:create-team') - ->name('store'); - - Route::patch('{team}/members', 'updateMembers')->name('members.update'); - Route::patch('{team}/permissions/users/{user}', 'updateUserPermissions') - ->name('permissions.users.update'); - }); diff --git a/routes/web.php b/routes/web.php index 2fa1e60..d12eeb7 100644 --- a/routes/web.php +++ b/routes/web.php @@ -9,9 +9,10 @@ Route::inertia('/', 'Welcome', [ Route::middleware(['auth', 'verified'])->group(function () { Route::inertia('dashboard', 'Dashboard')->name('dashboard'); + + require __DIR__.'/web/admin.php'; + require __DIR__.'/web/projects.php'; + require __DIR__.'/web/teams.php'; }); -require __DIR__.'/admin.php'; -require __DIR__.'/projects.php'; -require __DIR__.'/settings.php'; -require __DIR__.'/teams.php'; +require __DIR__.'/web/settings.php'; diff --git a/routes/admin.php b/routes/web/admin.php similarity index 89% rename from routes/admin.php rename to routes/web/admin.php index b9d6e09..84b0a82 100644 --- a/routes/admin.php +++ b/routes/web/admin.php @@ -3,7 +3,7 @@ use App\Http\Controllers\Admin\UserManagementController; use Illuminate\Support\Facades\Route; -Route::middleware(['auth', 'verified', 'can:access-admin']) +Route::middleware('can:access-admin') ->prefix('admin') ->name('admin.') ->group(function (): void { diff --git a/routes/web/projects.php b/routes/web/projects.php new file mode 100644 index 0000000..49dc713 --- /dev/null +++ b/routes/web/projects.php @@ -0,0 +1,158 @@ +name('projects.')->group(function (): void { + Route::get('/', [ProjectController::class, 'index'])->name('index'); + + Route::middleware('can:create-project')->group(function (): void { + Route::get('create', [ProjectController::class, 'create'])->name('create'); + Route::post('/', [ProjectController::class, 'store'])->name('store'); + }); + + Route::prefix('{project}') + ->group(function (): void { + Route::middleware('can:view-project,project')->group(function (): void { + Route::get('/', [ProjectController::class, 'show'])->name('show'); + Route::get('board', [ProjectBoardController::class, 'index'])->name('board.index'); + Route::get('backlog', [ProjectController::class, 'backlog'])->name('backlog.index'); + }); + + Route::delete('/', [ProjectController::class, 'destroy']) + ->middleware('can:access-admin') + ->name('destroy'); + + Route::middleware('can:admin-project,project')->group(function (): void { + Route::get('access', [ProjectController::class, 'access'])->name('access'); + + Route::prefix('permissions')->name('permissions.')->group(function (): void { + Route::patch('users/{user}', [ProjectController::class, 'updateUserPermissions']) + ->name('users.update'); + Route::patch('teams/{team}', [ProjectController::class, 'updateTeamPermissions']) + ->name('teams.update'); + }); + }); + + Route::scopeBindings()->group(function (): void { + Route::prefix('tickets') + ->name('tickets.') + ->group(function (): void { + Route::middleware('can:write-project,project')->group(function (): void { + Route::get('create', [ProjectTicketController::class, 'create'])->name('create'); + Route::post('/', [ProjectTicketController::class, 'store'])->name('store'); + }); + + Route::prefix('{ticket}')->group(function (): void { + Route::get('/', [ProjectTicketController::class, 'show']) + ->middleware('can:view-project,project') + ->name('show'); + + Route::middleware('can:write-project,project')->group(function (): void { + Route::patch('/', [ProjectTicketController::class, 'update'])->name('update'); + Route::patch('status', [ProjectBoardController::class, 'move'])->name('status.update'); + + Route::prefix('branches') + ->name('branches.') + ->controller(ProjectTicketBranchController::class) + ->group(function (): void { + Route::get('validate', 'validate')->name('validate'); + Route::post('/', 'store')->name('store'); + }); + + Route::prefix('links') + ->name('links.') + ->controller(ProjectTicketLinkController::class) + ->group(function (): void { + Route::post('/', 'store')->name('store'); + Route::delete('{ticketLink}', 'destroy') + ->withoutScopedBindings() + ->name('destroy'); + }); + }); + }); + }); + + Route::prefix('settings') + ->name('settings.') + ->middleware('can:admin-project,project') + ->group(function (): void { + Route::get('general', [ProjectController::class, 'general'])->name('general'); + + Route::get('board', [ProjectBoardSettingsController::class, 'show'])->name('board'); + + Route::prefix('board') + ->name('board.') + ->controller(ProjectBoardSettingsController::class) + ->group(function (): void { + Route::prefix('columns')->name('columns.')->group(function (): void { + Route::post('/', 'store')->name('store'); + Route::patch('{boardColumn}', 'update')->name('update'); + Route::delete('{boardColumn}', 'destroy')->name('destroy'); + }); + + Route::prefix('statuses')->name('statuses.')->group(function (): void { + Route::post('/', 'storeStatus')->name('store'); + Route::patch('{ticketStatus}', 'updateStatus')->name('update'); + Route::delete('{ticketStatus}', 'destroyStatus')->name('destroy'); + Route::patch('{ticketStatus}/transitions', 'updateStatusTransitions') + ->name('transitions.update'); + }); + }); + + Route::get('metadata', [ProjectTicketMetadataSettingsController::class, 'show']) + ->name('metadata'); + Route::patch('metadata', [ProjectTicketMetadataSettingsController::class, 'update']) + ->name('metadata.update'); + + Route::prefix('metadata') + ->name('metadata.') + ->controller(ProjectTicketMetadataSettingsController::class) + ->group(function (): void { + Route::prefix('types')->name('types.')->group(function (): void { + Route::post('/', 'storeType')->name('store'); + Route::patch('{ticketType}', 'updateType')->name('update'); + Route::delete('{ticketType}', 'destroyType')->name('destroy'); + }); + + Route::prefix('priorities')->name('priorities.')->group(function (): void { + Route::post('/', 'storePriority')->name('store'); + Route::patch('{ticketPriority}', 'updatePriority')->name('update'); + Route::delete('{ticketPriority}', 'destroyPriority')->name('destroy'); + }); + + Route::prefix('fields')->name('fields.')->group(function (): void { + Route::post('/', 'storeFieldDefinition')->name('store'); + Route::patch('{ticketFieldDefinition}', 'updateFieldDefinition')->name('update'); + Route::delete('{ticketFieldDefinition}', 'destroyFieldDefinition')->name('destroy'); + }); + + Route::prefix('link-types')->name('link-types.')->group(function (): void { + Route::post('/', 'storeLinkType')->name('store'); + Route::patch('{ticketLinkType}', 'updateLinkType')->name('update'); + Route::delete('{ticketLinkType}', 'destroyLinkType')->name('destroy'); + }); + }); + + Route::get('repositories', [ProjectRepositorySettingsController::class, 'show']) + ->name('repositories'); + + Route::prefix('repositories') + ->name('repositories.') + ->controller(ProjectRepositorySettingsController::class) + ->group(function (): void { + Route::post('/', 'store')->name('store'); + Route::patch('{repositoryConnection}', 'update')->name('update'); + Route::delete('{repositoryConnection}', 'destroy')->name('destroy'); + }); + }); + }); + }); +}); diff --git a/routes/settings.php b/routes/web/settings.php similarity index 100% rename from routes/settings.php rename to routes/web/settings.php diff --git a/routes/web/teams.php b/routes/web/teams.php new file mode 100644 index 0000000..646606d --- /dev/null +++ b/routes/web/teams.php @@ -0,0 +1,14 @@ +name('teams.')->group(function (): void { + + Route::controller(TeamController::class)->group(function () { + Route::get('/', 'index')->middleware('can:view-teams')->name('index'); + Route::post('/', 'store')->middleware('can:create-team')->name('store'); + Route::patch('{team}/members', 'updateMembers')->name('members.update'); + Route::patch('{team}/permissions/users/{user}', 'updateUserPermissions')->name('permissions.users.update'); + }); +}); From 75a573dca62f29ec1d7a60bc2a32fbf1d4c17779 Mon Sep 17 00:00:00 2001 From: Twyco Date: Wed, 15 Apr 2026 16:07:02 +0200 Subject: [PATCH 4/5] Split nested project route sections --- .../skills/project-route-structure/SKILL.md | 20 ++-- AGENTS.md | 2 +- routes/web/project/settings.php | 37 ++++++ routes/web/project/settings/board.php | 16 +++ routes/web/project/settings/metadata.php | 27 +++++ routes/web/project/settings/repositories.php | 7 ++ routes/web/project/tickets.php | 41 +++++++ routes/web/projects.php | 109 +----------------- 8 files changed, 144 insertions(+), 115 deletions(-) create mode 100644 routes/web/project/settings.php create mode 100644 routes/web/project/settings/board.php create mode 100644 routes/web/project/settings/metadata.php create mode 100644 routes/web/project/settings/repositories.php create mode 100644 routes/web/project/tickets.php diff --git a/.agents/skills/project-route-structure/SKILL.md b/.agents/skills/project-route-structure/SKILL.md index c558e2d..d1acc42 100644 --- a/.agents/skills/project-route-structure/SKILL.md +++ b/.agents/skills/project-route-structure/SKILL.md @@ -1,6 +1,6 @@ --- name: project-route-structure -description: "Always apply in this project when creating, moving, or refactoring Laravel route definitions in routes/*.php; changing route prefixes, names, middleware, scoped bindings, or route files; or adding frontend-facing endpoints that Wayfinder generates. Documents the KanbanGit route-file layout and grouping conventions." +description: "Always apply in this project when creating, moving, or refactoring Laravel route definitions in routes/**/*.php; changing route prefixes, names, middleware, scoped bindings, or route files; or adding frontend-facing endpoints that Wayfinder generates. Documents the KanbanGit route-file layout and grouping conventions." --- # Project Route Structure @@ -9,7 +9,7 @@ description: "Always apply in this project when creating, moving, or refactoring Activate this skill whenever work in this project changes: -- `routes/*.php` +- `routes/**/*.php` - Route names, URI prefixes, middleware groups, scoped bindings, or controller route registrations - Frontend-facing endpoints that require regenerated Wayfinder output @@ -18,10 +18,11 @@ Use this together with `laravel-best-practices`. Also activate `wayfinder-develo ## Route Files - `routes/web.php` is the web entry point only. Keep public shell routes here, such as `home` and `dashboard`, then require domain route files. -- `routes/projects.php` owns all `projects.*` routes. -- `routes/admin.php` owns all `admin.*` routes. -- `routes/teams.php` owns all `teams.*` routes. -- `routes/settings.php` owns account settings routes, but keeps their existing route names such as `profile.edit`, `security.edit`, and `git-connections.edit`. +- `routes/web/projects.php` owns the main `projects.*` structure. +- `routes/web/admin.php` owns all `admin.*` routes. +- `routes/web/teams.php` owns all `teams.*` routes. +- `routes/web/settings.php` owns account settings routes, but keeps their existing route names such as `profile.edit`, `security.edit`, and `git-connections.edit`. +- `routes/web/project/*.php` contains larger nested project route sections that would make `routes/web/projects.php` too deeply indented. Add a new route file only when a domain gets large enough to stand alone. Otherwise, extend the existing domain file. @@ -32,6 +33,7 @@ Add a new route file only when a domain gets large enough to stand alone. Otherw - Keep public route names stable unless the user explicitly asks for a public interface change. - Keep route URIs stable when reorganizing route files; this kind of change should be structural. - Use `function (): void` for route group closures. +- For large nested sections, keep the structural group in the parent file and `require` a child file from inside the group. For example, keep `Route::prefix('tickets')->name('tickets.')->group(...)` in `routes/web/projects.php`, then require `routes/web/project/tickets.php` inside it. ## Project Routes @@ -46,7 +48,11 @@ Project routes should follow this shape: - Project delete: `can:access-admin` - `projects/{project}/permissions/...` must not be inside `Route::scopeBindings()`, because `Project` does not define `users()` or `teams()` child relationships. - Use `Route::scopeBindings()` only around nested project resources that should bind through project-owned relationships, such as tickets and project settings children. -- Keep ticket subroutes grouped under `tickets.` and settings subroutes grouped under `settings.`. +- Keep the outer `{project}` group in `routes/web/projects.php`; it is part of the main project structure. +- Keep compact view routes, such as show, board, and backlog, directly in `routes/web/projects.php`. +- Keep ticket subroutes grouped under `tickets.` in `routes/web/projects.php`, with the route contents in `routes/web/project/tickets.php`. +- Keep settings subroutes grouped under `settings.` in `routes/web/projects.php`, with the route contents in `routes/web/project/settings.php`. +- Split dense settings subsections further under `routes/web/project/settings/`, such as `board.php`, `metadata.php`, and `repositories.php`. ## Settings Routes diff --git a/AGENTS.md b/AGENTS.md index aed5aa0..b4031f6 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -34,7 +34,7 @@ This project has domain-specific skills available. You MUST activate the relevan - `fortify-development` — ACTIVATE when the user works on authentication in Laravel. This includes login, registration, password reset, email verification, two-factor authentication (2FA/TOTP/QR codes/recovery codes), profile updates, password confirmation, or any auth-related routes and controllers. Activate when the user mentions Fortify, auth, authentication, login, register, signup, forgot password, verify email, 2FA, or references app/Actions/Fortify/, CreateNewUser, UpdateUserProfileInformation, FortifyServiceProvider, config/fortify.php, or auth guards. Fortify is the frontend-agnostic authentication backend for Laravel that registers all auth routes and controllers. Also activate when building SPA or headless authentication, customizing login redirects, overriding response contracts like LoginResponse, or configuring login throttling. Do NOT activate for Laravel Passport (OAuth2 API tokens), Socialite (OAuth social login), or non-auth Laravel features. - `laravel-best-practices` — Apply this skill whenever writing, reviewing, or refactoring Laravel PHP code. This includes creating or modifying controllers, models, migrations, form requests, policies, jobs, scheduled commands, service classes, and Eloquent queries. Triggers for N+1 and query performance issues, caching strategies, authorization and security patterns, validation, error handling, queue and job configuration, route definitions, and architectural decisions. Also use for Laravel code reviews and refactoring existing Laravel code to follow best practices. Covers any task involving Laravel backend PHP code patterns. -- `project-route-structure` — Always apply in this project when creating, moving, or refactoring Laravel route definitions in routes/*.php; changing route prefixes, names, middleware, scoped bindings, or route files; or adding frontend-facing endpoints that Wayfinder generates. Documents the KanbanGit route-file layout and grouping conventions. +- `project-route-structure` — Always apply in this project when creating, moving, or refactoring Laravel route definitions in routes/**/*.php; changing route prefixes, names, middleware, scoped bindings, or route files; or adding frontend-facing endpoints that Wayfinder generates. Documents the KanbanGit route-file layout and grouping conventions. - `wayfinder-development` — Use this skill for Laravel Wayfinder which auto-generates typed functions for Laravel controllers and routes. ALWAYS use this skill when frontend code needs to call backend routes or controller actions. Trigger when: connecting any React/Vue/Svelte/Inertia frontend to Laravel controllers, routes, building end-to-end features with both frontend and backend, wiring up forms or links to backend endpoints, fixing route-related TypeScript errors, importing from @/actions or @/routes, or running wayfinder:generate. Use Wayfinder route functions instead of hardcoded URLs. Covers: wayfinder() vite plugin, .url()/.get()/.post()/.form(), query params, route model binding, tree-shaking. Do not use for backend-only task - `inertia-vue-development` — Develops Inertia.js v2 Vue client-side applications. Activates when creating Vue pages, forms, or navigation; using , , useForm, or router; working with deferred props, prefetching, or polling; or when user mentions Vue with Inertia, Vue pages, Vue forms, or Vue navigation. - `tailwindcss-development` — Always invoke when the user's message includes 'tailwind' in any form. Also invoke for: building responsive grid layouts (multi-column card grids, product grids), flex/grid page structures (dashboards with sidebars, fixed topbars, mobile-toggle navs), styling UI components (cards, tables, navbars, pricing sections, forms, inputs, badges), adding dark mode variants, fixing spacing or typography, and Tailwind v3/v4 work. The core use case: writing or fixing Tailwind utility classes in HTML templates (Blade, JSX, Vue). Skip for backend PHP logic, database queries, API routes, JavaScript with no HTML/CSS component, CSS file audits, build tool configuration, and vanilla CSS. diff --git a/routes/web/project/settings.php b/routes/web/project/settings.php new file mode 100644 index 0000000..d89d5f8 --- /dev/null +++ b/routes/web/project/settings.php @@ -0,0 +1,37 @@ +name('general'); + +Route::get('board', [ProjectBoardSettingsController::class, 'show'])->name('board'); + +Route::prefix('board') + ->name('board.') + ->controller(ProjectBoardSettingsController::class) + ->group(function (): void { + require __DIR__.'/settings/board.php'; + }); + +Route::get('metadata', [ProjectTicketMetadataSettingsController::class, 'show'])->name('metadata'); +Route::patch('metadata', [ProjectTicketMetadataSettingsController::class, 'update'])->name('metadata.update'); + +Route::prefix('metadata') + ->name('metadata.') + ->controller(ProjectTicketMetadataSettingsController::class) + ->group(function (): void { + require __DIR__.'/settings/metadata.php'; + }); + +Route::get('repositories', [ProjectRepositorySettingsController::class, 'show'])->name('repositories'); + +Route::prefix('repositories') + ->name('repositories.') + ->controller(ProjectRepositorySettingsController::class) + ->group(function (): void { + require __DIR__.'/settings/repositories.php'; + }); diff --git a/routes/web/project/settings/board.php b/routes/web/project/settings/board.php new file mode 100644 index 0000000..b0e38cb --- /dev/null +++ b/routes/web/project/settings/board.php @@ -0,0 +1,16 @@ +name('columns.')->group(function (): void { + Route::post('/', 'store')->name('store'); + Route::patch('{boardColumn}', 'update')->name('update'); + Route::delete('{boardColumn}', 'destroy')->name('destroy'); +}); + +Route::prefix('statuses')->name('statuses.')->group(function (): void { + Route::post('/', 'storeStatus')->name('store'); + Route::patch('{ticketStatus}', 'updateStatus')->name('update'); + Route::delete('{ticketStatus}', 'destroyStatus')->name('destroy'); + Route::patch('{ticketStatus}/transitions', 'updateStatusTransitions')->name('transitions.update'); +}); diff --git a/routes/web/project/settings/metadata.php b/routes/web/project/settings/metadata.php new file mode 100644 index 0000000..28023f1 --- /dev/null +++ b/routes/web/project/settings/metadata.php @@ -0,0 +1,27 @@ +name('types.')->group(function (): void { + Route::post('/', 'storeType')->name('store'); + Route::patch('{ticketType}', 'updateType')->name('update'); + Route::delete('{ticketType}', 'destroyType')->name('destroy'); +}); + +Route::prefix('priorities')->name('priorities.')->group(function (): void { + Route::post('/', 'storePriority')->name('store'); + Route::patch('{ticketPriority}', 'updatePriority')->name('update'); + Route::delete('{ticketPriority}', 'destroyPriority')->name('destroy'); +}); + +Route::prefix('fields')->name('fields.')->group(function (): void { + Route::post('/', 'storeFieldDefinition')->name('store'); + Route::patch('{ticketFieldDefinition}', 'updateFieldDefinition')->name('update'); + Route::delete('{ticketFieldDefinition}', 'destroyFieldDefinition')->name('destroy'); +}); + +Route::prefix('link-types')->name('link-types.')->group(function (): void { + Route::post('/', 'storeLinkType')->name('store'); + Route::patch('{ticketLinkType}', 'updateLinkType')->name('update'); + Route::delete('{ticketLinkType}', 'destroyLinkType')->name('destroy'); +}); diff --git a/routes/web/project/settings/repositories.php b/routes/web/project/settings/repositories.php new file mode 100644 index 0000000..a2974b3 --- /dev/null +++ b/routes/web/project/settings/repositories.php @@ -0,0 +1,7 @@ +name('store'); +Route::patch('{repositoryConnection}', 'update')->name('update'); +Route::delete('{repositoryConnection}', 'destroy')->name('destroy'); diff --git a/routes/web/project/tickets.php b/routes/web/project/tickets.php new file mode 100644 index 0000000..d43e249 --- /dev/null +++ b/routes/web/project/tickets.php @@ -0,0 +1,41 @@ +group(function (): void { + Route::get('create', [ProjectTicketController::class, 'create'])->name('create'); + Route::post('/', [ProjectTicketController::class, 'store'])->name('store'); +}); + +Route::prefix('{ticket}')->group(function (): void { + Route::get('/', [ProjectTicketController::class, 'show']) + ->middleware('can:view-project,project') + ->name('show'); + + Route::middleware('can:write-project,project')->group(function (): void { + Route::patch('/', [ProjectTicketController::class, 'update'])->name('update'); + Route::patch('status', [ProjectBoardController::class, 'move'])->name('status.update'); + + Route::prefix('branches') + ->name('branches.') + ->controller(ProjectTicketBranchController::class) + ->group(function (): void { + Route::get('validate', 'validate')->name('validate'); + Route::post('/', 'store')->name('store'); + }); + + Route::prefix('links') + ->name('links.') + ->controller(ProjectTicketLinkController::class) + ->group(function (): void { + Route::post('/', 'store')->name('store'); + Route::delete('{ticketLink}', 'destroy') + ->withoutScopedBindings() + ->name('destroy'); + }); + }); +}); diff --git a/routes/web/projects.php b/routes/web/projects.php index 49dc713..3330cbe 100644 --- a/routes/web/projects.php +++ b/routes/web/projects.php @@ -1,13 +1,7 @@ name('projects.')->group(function (): void { @@ -45,113 +39,14 @@ Route::prefix('projects')->name('projects.')->group(function (): void { Route::prefix('tickets') ->name('tickets.') ->group(function (): void { - Route::middleware('can:write-project,project')->group(function (): void { - Route::get('create', [ProjectTicketController::class, 'create'])->name('create'); - Route::post('/', [ProjectTicketController::class, 'store'])->name('store'); - }); - - Route::prefix('{ticket}')->group(function (): void { - Route::get('/', [ProjectTicketController::class, 'show']) - ->middleware('can:view-project,project') - ->name('show'); - - Route::middleware('can:write-project,project')->group(function (): void { - Route::patch('/', [ProjectTicketController::class, 'update'])->name('update'); - Route::patch('status', [ProjectBoardController::class, 'move'])->name('status.update'); - - Route::prefix('branches') - ->name('branches.') - ->controller(ProjectTicketBranchController::class) - ->group(function (): void { - Route::get('validate', 'validate')->name('validate'); - Route::post('/', 'store')->name('store'); - }); - - Route::prefix('links') - ->name('links.') - ->controller(ProjectTicketLinkController::class) - ->group(function (): void { - Route::post('/', 'store')->name('store'); - Route::delete('{ticketLink}', 'destroy') - ->withoutScopedBindings() - ->name('destroy'); - }); - }); - }); + require __DIR__.'/project/tickets.php'; }); Route::prefix('settings') ->name('settings.') ->middleware('can:admin-project,project') ->group(function (): void { - Route::get('general', [ProjectController::class, 'general'])->name('general'); - - Route::get('board', [ProjectBoardSettingsController::class, 'show'])->name('board'); - - Route::prefix('board') - ->name('board.') - ->controller(ProjectBoardSettingsController::class) - ->group(function (): void { - Route::prefix('columns')->name('columns.')->group(function (): void { - Route::post('/', 'store')->name('store'); - Route::patch('{boardColumn}', 'update')->name('update'); - Route::delete('{boardColumn}', 'destroy')->name('destroy'); - }); - - Route::prefix('statuses')->name('statuses.')->group(function (): void { - Route::post('/', 'storeStatus')->name('store'); - Route::patch('{ticketStatus}', 'updateStatus')->name('update'); - Route::delete('{ticketStatus}', 'destroyStatus')->name('destroy'); - Route::patch('{ticketStatus}/transitions', 'updateStatusTransitions') - ->name('transitions.update'); - }); - }); - - Route::get('metadata', [ProjectTicketMetadataSettingsController::class, 'show']) - ->name('metadata'); - Route::patch('metadata', [ProjectTicketMetadataSettingsController::class, 'update']) - ->name('metadata.update'); - - Route::prefix('metadata') - ->name('metadata.') - ->controller(ProjectTicketMetadataSettingsController::class) - ->group(function (): void { - Route::prefix('types')->name('types.')->group(function (): void { - Route::post('/', 'storeType')->name('store'); - Route::patch('{ticketType}', 'updateType')->name('update'); - Route::delete('{ticketType}', 'destroyType')->name('destroy'); - }); - - Route::prefix('priorities')->name('priorities.')->group(function (): void { - Route::post('/', 'storePriority')->name('store'); - Route::patch('{ticketPriority}', 'updatePriority')->name('update'); - Route::delete('{ticketPriority}', 'destroyPriority')->name('destroy'); - }); - - Route::prefix('fields')->name('fields.')->group(function (): void { - Route::post('/', 'storeFieldDefinition')->name('store'); - Route::patch('{ticketFieldDefinition}', 'updateFieldDefinition')->name('update'); - Route::delete('{ticketFieldDefinition}', 'destroyFieldDefinition')->name('destroy'); - }); - - Route::prefix('link-types')->name('link-types.')->group(function (): void { - Route::post('/', 'storeLinkType')->name('store'); - Route::patch('{ticketLinkType}', 'updateLinkType')->name('update'); - Route::delete('{ticketLinkType}', 'destroyLinkType')->name('destroy'); - }); - }); - - Route::get('repositories', [ProjectRepositorySettingsController::class, 'show']) - ->name('repositories'); - - Route::prefix('repositories') - ->name('repositories.') - ->controller(ProjectRepositorySettingsController::class) - ->group(function (): void { - Route::post('/', 'store')->name('store'); - Route::patch('{repositoryConnection}', 'update')->name('update'); - Route::delete('{repositoryConnection}', 'destroy')->name('destroy'); - }); + require __DIR__.'/project/settings.php'; }); }); }); From 5a7972df7c6308a4ea9a3465b676ee7ef4102cb7 Mon Sep 17 00:00:00 2001 From: Twyco Date: Wed, 15 Apr 2026 16:11:31 +0200 Subject: [PATCH 5/5] missing features update --- docs/missing-features.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/docs/missing-features.md b/docs/missing-features.md index a17ac00..3914618 100644 --- a/docs/missing-features.md +++ b/docs/missing-features.md @@ -3,9 +3,11 @@ - resources benutzen - Für jedes Model / Route, resources benutzen, kein mapping/feste arrays mehr in den controller. - kein 'model' => $model (bspw. 'project' => $project) mehr. - - niemals das model in reponse return. für jedes model deshalb eine resource und diese benutzen. + - niemals das model in response return. für jedes model deshalb eine resource und diese benutzen. - in der resource niemals mit only oder manuel die props übergeben. - - Ziel: neue attribute von model sollen nicht ausversehen in responses auftauchen & responses kleiner, falls manche attr nicht gebraucht werden + - Ziel: + - neue attribute von model sollen nicht ausversehen in responses auftauchen & responses kleiner, falls manche attr nicht gebraucht werden + - für spätere api routes die selben ressourcen für responses - folgende actions löschen und durch resource benutzen: - app/Actions/Projects/BuildProjectBacklogPageProps.php - app/Actions/Projects/BuildProjectBoardPageProps.php