# Sites & Social Accounts Manager Implementation Plan > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. **Goal:** Build a Livewire-based CRUD UI for two independent resources (Sites, Social Accounts) plus a read-only, unauthenticated JSON API for both, so an AI agent can later consume the data over the local network. **Architecture:** Standard Laravel 13 app. Two Eloquent models (`Site`, `SocialAccount`) each with their own migration, Livewire full-page components for CRUD (index + modal form per resource), and lightweight `Illuminate\Routing\Controller` + API Resource classes for the read-only `/api/sites` and `/api/accounts` endpoints. No auth middleware anywhere. Tests use Pest with `RefreshDatabase` against the existing MySQL connection (test DB `testing`, configured in `phpunit.xml`). **Tech Stack:** Laravel 13, PHP 8.4, Livewire 3, Pest 4 (`pestphp/pest-plugin-laravel`), MySQL, Tailwind 4 (already installed) for styling. ## Global Constraints - No authentication/authorization anywhere (UI or API) — local network only, per spec. - Passwords stored and returned as plain text — no hashing/encryption, per spec. - Sites and Social Accounts are fully independent — no foreign key or relationship between them. - API is read-only: only `index`/`show` routes exist for `/api/sites` and `/api/accounts`. No store/update/destroy API routes. - `network` enum values: `twitter`, `instagram`, `facebook`, `reddit`, `tiktok`, `linkedin`, `youtube`, `other`. - `type` enum values: `post`, `comment`, `like`, or `null`. --- ## File Structure - `database/migrations/xxxx_create_sites_table.php` — new - `database/migrations/xxxx_create_social_accounts_table.php` — new - `app/Models/Site.php` — new - `app/Models/SocialAccount.php` — new - `database/factories/SiteFactory.php` — new - `database/factories/SocialAccountFactory.php` — new - `app/Livewire/Sites/Index.php` + `resources/views/livewire/sites/index.blade.php` — new (list + create/edit/delete UI) - `app/Livewire/SocialAccounts/Index.php` + `resources/views/livewire/social-accounts/index.blade.php` — new - `resources/views/components/layouts/app.blade.php` — new (shared nav layout) - `routes/web.php` — modify (add Livewire page routes) - `app/Http/Controllers/Api/SiteController.php` — new - `app/Http/Controllers/Api/SocialAccountController.php` — new - `app/Http/Resources/SiteResource.php` — new - `app/Http/Resources/SocialAccountResource.php` — new - `routes/api.php` — new (doesn't exist yet, needs creating + registering) - `bootstrap/app.php` — modify (register `api.php` routes) - `tests/Pest.php` — modify (enable `RefreshDatabase` for Feature tests) - `tests/Feature/Livewire/SitesTest.php` — new - `tests/Feature/Livewire/SocialAccountsTest.php` — new - `tests/Feature/Api/SitesApiTest.php` — new - `tests/Feature/Api/SocialAccountsApiTest.php` — new --- ## Task 1: Install Livewire and enable RefreshDatabase for tests **Files:** - Modify: `composer.json` (via composer require) - Modify: `tests/Pest.php` **Interfaces:** - Produces: Livewire package available (`Livewire\Component`, `Livewire\Livewire` test helper), `RefreshDatabase` applied to all Feature tests. - [ ] **Step 1: Install Livewire** Run: `composer require livewire/livewire` Expected: package installs cleanly, `vendor/livewire` exists. - [ ] **Step 2: Enable RefreshDatabase in Pest config** In `tests/Pest.php`, change: ```php pest()->extend(TestCase::class) // ->use(RefreshDatabase::class) ->in('Feature'); ``` to: ```php pest()->extend(TestCase::class) ->use(RefreshDatabase::class) ->in('Feature'); ``` - [ ] **Step 3: Verify the test DB exists and the existing example test still passes** Run: `php artisan db:show` to confirm connection, then: Run: `php artisan test tests/Feature/ExampleTest.php` Expected: PASS (1 test). If the `testing` database doesn't exist, create it first: `mysql -h 127.0.0.1 -u sail -ppassword -e "CREATE DATABASE IF NOT EXISTS testing"` (adjust host/credentials to match your local MySQL setup — check `.env` and `phpunit.xml` for the actual values in use). - [ ] **Step 4: Commit** ```bash git add composer.json composer.lock tests/Pest.php git commit -m "chore: install livewire, enable RefreshDatabase for feature tests" ``` --- ## Task 2: Site model, migration, and factory **Files:** - Create: `database/migrations/xxxx_create_sites_table.php` (via `php artisan make:migration`) - Create: `app/Models/Site.php` - Create: `database/factories/SiteFactory.php` - Test: `tests/Feature/SiteModelTest.php` **Interfaces:** - Produces: `Site` model with fillable `name`, `url`, `description`; `SiteFactory` producing valid random sites for tests. - [ ] **Step 1: Generate the migration** Run: `php artisan make:migration create_sites_table` - [ ] **Step 2: Write the migration** Edit the generated file in `database/migrations/`: ```php id(); $table->string('name'); $table->string('url'); $table->text('description')->nullable(); $table->timestamps(); }); } public function down(): void { Schema::dropIfExists('sites'); } }; ``` - [ ] **Step 3: Write the failing model test** Create `tests/Feature/SiteModelTest.php`: ```php 'My Blog', 'url' => 'https://example.com', 'description' => 'A blog about things', ]); expect($site->fresh()) ->name->toBe('My Blog') ->url->toBe('https://example.com') ->description->toBe('A blog about things'); }); test('a site can be created without a description', function () { $site = Site::create([ 'name' => 'My Blog', 'url' => 'https://example.com', ]); expect($site->fresh()->description)->toBeNull(); }); ``` - [ ] **Step 4: Run test to verify it fails** Run: `php artisan test tests/Feature/SiteModelTest.php` Expected: FAIL — `Site` class not found, or table doesn't exist. - [ ] **Step 5: Create the Site model** Create `app/Models/Site.php`: ```php fake()->company(), 'url' => fake()->url(), 'description' => fake()->optional()->sentence(), ]; } } ``` - [ ] **Step 7: Run migrations and test to verify it passes** Run: `php artisan migrate` Run: `php artisan test tests/Feature/SiteModelTest.php` Expected: PASS (2 tests) - [ ] **Step 8: Commit** ```bash git add database/migrations database/factories/SiteFactory.php app/Models/Site.php tests/Feature/SiteModelTest.php git commit -m "feat: add Site model, migration, and factory" ``` --- ## Task 3: SocialAccount model, migration, and factory **Files:** - Create: `database/migrations/xxxx_create_social_accounts_table.php` - Create: `app/Models/SocialAccount.php` - Create: `database/factories/SocialAccountFactory.php` - Test: `tests/Feature/SocialAccountModelTest.php` **Interfaces:** - Produces: `SocialAccount` model with fillable `network`, `username`, `email`, `password`, `type`; constant `SocialAccount::NETWORKS` and `SocialAccount::TYPES` arrays for use in Livewire dropdowns (Task 5) and validation; `SocialAccountFactory`. - [ ] **Step 1: Generate the migration** Run: `php artisan make:migration create_social_accounts_table` - [ ] **Step 2: Write the migration** ```php id(); $table->enum('network', [ 'twitter', 'instagram', 'facebook', 'reddit', 'tiktok', 'linkedin', 'youtube', 'other', ]); $table->string('username'); $table->string('email'); $table->string('password'); $table->enum('type', ['post', 'comment', 'like'])->nullable(); $table->timestamps(); }); } public function down(): void { Schema::dropIfExists('social_accounts'); } }; ``` - [ ] **Step 3: Write the failing model test** Create `tests/Feature/SocialAccountModelTest.php`: ```php 'twitter', 'username' => 'my_handle', 'email' => 'handle@example.com', 'password' => 'plain-text-password', 'type' => 'post', ]); expect($account->fresh()) ->network->toBe('twitter') ->username->toBe('my_handle') ->email->toBe('handle@example.com') ->password->toBe('plain-text-password') ->type->toBe('post'); }); test('a social account can be created with a null type', function () { $account = SocialAccount::create([ 'network' => 'instagram', 'username' => 'my_handle', 'email' => 'handle@example.com', 'password' => 'plain-text-password', ]); expect($account->fresh()->type)->toBeNull(); }); ``` - [ ] **Step 4: Run test to verify it fails** Run: `php artisan test tests/Feature/SocialAccountModelTest.php` Expected: FAIL — `SocialAccount` class not found. - [ ] **Step 5: Create the SocialAccount model** Create `app/Models/SocialAccount.php`: ```php fake()->randomElement(SocialAccount::NETWORKS), 'username' => fake()->userName(), 'email' => fake()->safeEmail(), 'password' => fake()->password(), 'type' => fake()->optional()->randomElement(SocialAccount::TYPES), ]; } } ``` - [ ] **Step 7: Run migrations and test to verify it passes** Run: `php artisan migrate` Run: `php artisan test tests/Feature/SocialAccountModelTest.php` Expected: PASS (2 tests) - [ ] **Step 8: Commit** ```bash git add database/migrations database/factories/SocialAccountFactory.php app/Models/SocialAccount.php tests/Feature/SocialAccountModelTest.php git commit -m "feat: add SocialAccount model, migration, and factory" ``` --- ## Task 4: Shared app layout **Files:** - Create: `resources/views/components/layouts/app.blade.php` **Interfaces:** - Produces: `` Blade component with a `$slot` and a `title` prop, providing `@livewireStyles`/`@livewireScripts` and a top nav linking to Sites and Social Accounts pages. Later Livewire full-page components render inside this via `#[Layout('components.layouts.app')]`. - [ ] **Step 1: Create the layout** Create `resources/views/components/layouts/app.blade.php`: ```blade {{ $title ?? 'Muninn' }} @vite('resources/css/app.css') @livewireStyles
{{ $slot }}
@livewireScripts ``` - [ ] **Step 2: Verify `resources/css/app.css` exists (Tailwind entrypoint)** Run: `cat resources/css/app.css 2>&1 || echo MISSING` If missing, create `resources/css/app.css`: ```css @import "tailwindcss"; ``` And confirm `vite.config.js` already lists it as an input (check the file — Laravel's default Vite config typically includes `resources/css/app.css` and `resources/js/app.js`). If it's not listed, add it to the `laravel-vite-plugin` `input` array in `vite.config.js`. - [ ] **Step 3: Commit** ```bash git add resources/views/components/layouts/app.blade.php resources/css/app.css vite.config.js git commit -m "feat: add shared app layout with nav" ``` (Note: `route('sites.index')` and `route('social-accounts.index')` won't resolve until Task 5/6 add those routes — this task alone won't be independently testable in the browser, but it introduces no failing automated test since no test targets this file directly. It will be exercised by Task 5's tests.) --- ## Task 5: Sites Livewire CRUD **Files:** - Create: `app/Livewire/Sites/Index.php` - Create: `resources/views/livewire/sites/index.blade.php` - Modify: `routes/web.php` - Test: `tests/Feature/Livewire/SitesTest.php` **Interfaces:** - Consumes: `App\Models\Site` (Task 2), `components.layouts.app` layout (Task 4). - Produces: route named `sites.index` at `/sites`; Livewire component `App\Livewire\Sites\Index` with public properties `$name`, `$url`, `$description`, `$editingId`, `$showForm`, and methods `create()`, `edit(int $id)`, `save()`, `delete(int $id)`, `resetForm()`. - [ ] **Step 1: Write the failing feature test** Create `tests/Feature/Livewire/SitesTest.php`: ```php create(['name' => 'Example Site']); $this->get(route('sites.index')) ->assertOk() ->assertSee('Example Site'); }); test('a new site can be created', function () { Livewire::test(Index::class) ->set('name', 'New Site') ->set('url', 'https://newsite.test') ->set('description', 'Some description') ->call('save'); expect(Site::where('name', 'New Site')->exists())->toBeTrue(); }); test('name and url are required to create a site', function () { Livewire::test(Index::class) ->set('name', '') ->set('url', '') ->call('save') ->assertHasErrors(['name', 'url']); }); test('an existing site can be edited', function () { $site = Site::factory()->create(['name' => 'Old Name']); Livewire::test(Index::class) ->call('edit', $site->id) ->set('name', 'Updated Name') ->call('save'); expect($site->fresh()->name)->toBe('Updated Name'); }); test('a site can be deleted', function () { $site = Site::factory()->create(); Livewire::test(Index::class)->call('delete', $site->id); expect(Site::find($site->id))->toBeNull(); }); ``` - [ ] **Step 2: Run test to verify it fails** Run: `php artisan test tests/Feature/Livewire/SitesTest.php` Expected: FAIL — route `sites.index` not defined / component class not found. - [ ] **Step 3: Create the Livewire component** Run: `php artisan make:livewire Sites/Index --test=false` (or create files manually if the command scaffolds unwanted content) Replace `app/Livewire/Sites/Index.php` with: ```php Site::orderBy('name')->get(), ]); } public function create(): void { $this->resetForm(); $this->showForm = true; } public function edit(int $id): void { $site = Site::findOrFail($id); $this->editingId = $site->id; $this->name = $site->name; $this->url = $site->url; $this->description = $site->description; $this->showForm = true; } public function save(): void { $validated = $this->validate([ 'name' => ['required', 'string', 'max:255'], 'url' => ['required', 'string', 'max:255'], 'description' => ['nullable', 'string'], ]); Site::updateOrCreate(['id' => $this->editingId], $validated); $this->resetForm(); $this->showForm = false; } public function delete(int $id): void { Site::findOrFail($id)->delete(); } public function resetForm(): void { $this->editingId = null; $this->name = ''; $this->url = ''; $this->description = null; $this->resetValidation(); } } ``` - [ ] **Step 4: Create the Blade view** Create `resources/views/livewire/sites/index.blade.php`: ```blade

Sites

@foreach ($sites as $site) @endforeach
Name URL Description
{{ $site->name }} {{ $site->url }} {{ $site->description }}
@if ($showForm)

{{ $editingId ? 'Edit Site' : 'Add Site' }}

@error('name') {{ $message }} @enderror
@error('url') {{ $message }} @enderror
@endif
``` - [ ] **Step 5: Register the route** In `routes/web.php`, replace the file contents with: ```php name('sites.index'); ``` - [ ] **Step 6: Run test to verify it passes** Run: `php artisan test tests/Feature/Livewire/SitesTest.php` Expected: PASS (5 tests) — note `sites.index` GET test will still hit `route('social-accounts.index')` in the layout via the nav; that route is added in Task 6, so temporarily this test will fail with "Route [social-accounts.index] not defined" when rendering the layout. To unblock Task 5 independently, register a placeholder route now so the nav link resolves, replacing it properly in Task 6: ```php Route::get('/social-accounts', SitesIndex::class)->name('social-accounts.index'); // placeholder, replaced in Task 6 ``` Add this line to `routes/web.php` temporarily, rerun the test, confirm PASS, then proceed to Task 6 which replaces the placeholder with the real component. - [ ] **Step 7: Commit** ```bash git add app/Livewire/Sites resources/views/livewire/sites routes/web.php tests/Feature/Livewire/SitesTest.php git commit -m "feat: add Sites Livewire CRUD page" ``` --- ## Task 6: Social Accounts Livewire CRUD **Files:** - Create: `app/Livewire/SocialAccounts/Index.php` - Create: `resources/views/livewire/social-accounts/index.blade.php` - Modify: `routes/web.php` (replace Task 5's placeholder route) - Test: `tests/Feature/Livewire/SocialAccountsTest.php` **Interfaces:** - Consumes: `App\Models\SocialAccount` (Task 3, incl. `NETWORKS`/`TYPES` constants), `components.layouts.app` layout (Task 4). - Produces: route named `social-accounts.index` at `/social-accounts`; Livewire component `App\Livewire\SocialAccounts\Index` with public properties `$network`, `$username`, `$email`, `$password`, `$type`, `$editingId`, `$showForm`, `$revealedId`, and methods `create()`, `edit(int $id)`, `save()`, `delete(int $id)`, `resetForm()`, `toggleReveal(int $id)`. - [ ] **Step 1: Write the failing feature test** Create `tests/Feature/Livewire/SocialAccountsTest.php`: ```php create(['username' => 'example_user']); $this->get(route('social-accounts.index')) ->assertOk() ->assertSee('example_user'); }); test('a new social account can be created', function () { Livewire::test(Index::class) ->set('network', 'twitter') ->set('username', 'new_user') ->set('email', 'new_user@example.com') ->set('password', 'secret123') ->set('type', 'post') ->call('save'); expect(SocialAccount::where('username', 'new_user')->exists())->toBeTrue(); }); test('network, username, email, and password are required', function () { Livewire::test(Index::class) ->set('network', '') ->set('username', '') ->set('email', '') ->set('password', '') ->call('save') ->assertHasErrors(['network', 'username', 'email', 'password']); }); test('type can be left null', function () { Livewire::test(Index::class) ->set('network', 'reddit') ->set('username', 'no_type_user') ->set('email', 'no_type@example.com') ->set('password', 'secret123') ->set('type', null) ->call('save'); $account = SocialAccount::where('username', 'no_type_user')->first(); expect($account->type)->toBeNull(); }); test('an existing account can be edited', function () { $account = SocialAccount::factory()->create(['username' => 'old_name']); Livewire::test(Index::class) ->call('edit', $account->id) ->set('username', 'new_name') ->call('save'); expect($account->fresh()->username)->toBe('new_name'); }); test('an account can be deleted', function () { $account = SocialAccount::factory()->create(); Livewire::test(Index::class)->call('delete', $account->id); expect(SocialAccount::find($account->id))->toBeNull(); }); ``` - [ ] **Step 2: Run test to verify it fails** Run: `php artisan test tests/Feature/Livewire/SocialAccountsTest.php` Expected: FAIL — route/component not found. - [ ] **Step 3: Create the Livewire component** Create `app/Livewire/SocialAccounts/Index.php`: ```php SocialAccount::orderBy('username')->get(), 'networks' => SocialAccount::NETWORKS, 'types' => SocialAccount::TYPES, ]); } public function create(): void { $this->resetForm(); $this->showForm = true; } public function edit(int $id): void { $account = SocialAccount::findOrFail($id); $this->editingId = $account->id; $this->network = $account->network; $this->username = $account->username; $this->email = $account->email; $this->password = $account->password; $this->type = $account->type; $this->showForm = true; } public function save(): void { $validated = $this->validate([ 'network' => ['required', 'string', 'in:' . implode(',', SocialAccount::NETWORKS)], 'username' => ['required', 'string', 'max:255'], 'email' => ['required', 'string', 'max:255'], 'password' => ['required', 'string', 'max:255'], 'type' => ['nullable', 'string', 'in:' . implode(',', SocialAccount::TYPES)], ]); SocialAccount::updateOrCreate(['id' => $this->editingId], $validated); $this->resetForm(); $this->showForm = false; } public function delete(int $id): void { SocialAccount::findOrFail($id)->delete(); } public function toggleReveal(int $id): void { $this->revealedId = $this->revealedId === $id ? null : $id; } public function resetForm(): void { $this->editingId = null; $this->network = ''; $this->username = ''; $this->email = ''; $this->password = ''; $this->type = null; $this->resetValidation(); } } ``` - [ ] **Step 4: Create the Blade view** Create `resources/views/livewire/social-accounts/index.blade.php`: ```blade

Social Accounts

@foreach ($accounts as $account) @endforeach
Network Username Email Password Type
{{ ucfirst($account->network) }} {{ $account->username }} {{ $account->email }} @if ($revealedId === $account->id) {{ $account->password }} @else •••••••• @endif {{ $account->type ? ucfirst($account->type) : '—' }}
@if ($showForm)

{{ $editingId ? 'Edit Account' : 'Add Account' }}

@error('network') {{ $message }} @enderror
@error('username') {{ $message }} @enderror
@error('email') {{ $message }} @enderror
@error('password') {{ $message }} @enderror
@error('type') {{ $message }} @enderror
@endif
``` - [ ] **Step 5: Replace the placeholder route in `routes/web.php`** Update `routes/web.php` to: ```php name('sites.index'); Route::get('/social-accounts', SocialAccountsIndex::class)->name('social-accounts.index'); ``` - [ ] **Step 6: Run both Livewire test files to verify everything passes** Run: `php artisan test tests/Feature/Livewire/` Expected: PASS (11 tests total: 5 from SitesTest + 6 from SocialAccountsTest) - [ ] **Step 7: Commit** ```bash git add app/Livewire/SocialAccounts resources/views/livewire/social-accounts routes/web.php tests/Feature/Livewire/SocialAccountsTest.php git commit -m "feat: add Social Accounts Livewire CRUD page" ``` --- ## Task 7: Read-only Sites API **Files:** - Create: `app/Http/Controllers/Api/SiteController.php` - Create: `app/Http/Resources/SiteResource.php` - Create: `routes/api.php` - Modify: `bootstrap/app.php` - Test: `tests/Feature/Api/SitesApiTest.php` **Interfaces:** - Consumes: `App\Models\Site` (Task 2). - Produces: `GET /api/sites` (list, optional `?name=` partial filter), `GET /api/sites/{site}` (show). JSON shape: `{id, name, url, description, created_at, updated_at}`. - [ ] **Step 1: Write the failing test** Create `tests/Feature/Api/SitesApiTest.php`: ```php count(3)->create(); $response = $this->getJson('/api/sites'); $response->assertOk()->assertJsonCount(3, 'data'); }); test('it filters sites by partial name match', function () { Site::factory()->create(['name' => 'My Awesome Blog']); Site::factory()->create(['name' => 'Other Site']); $response = $this->getJson('/api/sites?name=Awesome'); $response->assertOk() ->assertJsonCount(1, 'data') ->assertJsonPath('data.0.name', 'My Awesome Blog'); }); test('it shows a single site', function () { $site = Site::factory()->create(['name' => 'Solo Site']); $response = $this->getJson("/api/sites/{$site->id}"); $response->assertOk()->assertJsonPath('data.name', 'Solo Site'); }); test('it returns 404 for a missing site', function () { $this->getJson('/api/sites/999')->assertNotFound(); }); ``` - [ ] **Step 2: Run test to verify it fails** Run: `php artisan test tests/Feature/Api/SitesApiTest.php` Expected: FAIL — 404 or route not found for `/api/sites`. - [ ] **Step 3: Create the API Resource** Create `app/Http/Resources/SiteResource.php`: ```php $this->id, 'name' => $this->name, 'url' => $this->url, 'description' => $this->description, 'created_at' => $this->created_at, 'updated_at' => $this->updated_at, ]; } } ``` - [ ] **Step 4: Create the controller** Create `app/Http/Controllers/Api/SiteController.php`: ```php filled('name')) { $query->where('name', 'like', '%' . $request->string('name') . '%'); } return SiteResource::collection($query->orderBy('name')->get()); } public function show(Site $site) { return new SiteResource($site); } } ``` - [ ] **Step 5: Create `routes/api.php`** Create `routes/api.php`: ```php withRouting( web: __DIR__.'/../routes/web.php', commands: __DIR__.'/../routes/console.php', health: '/up', ) ->withMiddleware(function (Middleware $middleware) { // }) ->withExceptions(function (Exceptions $exceptions) { // })->create(); ``` Add the `api` key so it becomes: ```php ->withRouting( web: __DIR__.'/../routes/web.php', api: __DIR__.'/../routes/api.php', commands: __DIR__.'/../routes/console.php', health: '/up', ) ``` - [ ] **Step 7: Run test to verify it passes** Run: `php artisan test tests/Feature/Api/SitesApiTest.php` Expected: PASS (4 tests) - [ ] **Step 8: Commit** ```bash git add app/Http/Controllers/Api/SiteController.php app/Http/Resources/SiteResource.php routes/api.php bootstrap/app.php tests/Feature/Api/SitesApiTest.php git commit -m "feat: add read-only Sites API" ``` --- ## Task 8: Read-only Social Accounts API **Files:** - Create: `app/Http/Controllers/Api/SocialAccountController.php` - Create: `app/Http/Resources/SocialAccountResource.php` - Modify: `routes/api.php` - Test: `tests/Feature/Api/SocialAccountsApiTest.php` **Interfaces:** - Consumes: `App\Models\SocialAccount` (Task 3). - Produces: `GET /api/accounts` (list, optional `?network=` and `?type=` exact-match filters), `GET /api/accounts/{social_account}` (show). JSON shape includes plain-text `password`: `{id, network, username, email, password, type, created_at, updated_at}`. - [ ] **Step 1: Write the failing test** Create `tests/Feature/Api/SocialAccountsApiTest.php`: ```php count(3)->create(); $response = $this->getJson('/api/accounts'); $response->assertOk()->assertJsonCount(3, 'data'); expect($response->json('data.0'))->toHaveKey('password'); }); test('it filters accounts by network', function () { SocialAccount::factory()->create(['network' => 'twitter']); SocialAccount::factory()->create(['network' => 'instagram']); $response = $this->getJson('/api/accounts?network=twitter'); $response->assertOk() ->assertJsonCount(1, 'data') ->assertJsonPath('data.0.network', 'twitter'); }); test('it filters accounts by type', function () { SocialAccount::factory()->create(['type' => 'post']); SocialAccount::factory()->create(['type' => 'like']); $response = $this->getJson('/api/accounts?type=post'); $response->assertOk() ->assertJsonCount(1, 'data') ->assertJsonPath('data.0.type', 'post'); }); test('it shows a single account with password', function () { $account = SocialAccount::factory()->create(['password' => 'super-secret']); $response = $this->getJson("/api/accounts/{$account->id}"); $response->assertOk()->assertJsonPath('data.password', 'super-secret'); }); test('it returns 404 for a missing account', function () { $this->getJson('/api/accounts/999')->assertNotFound(); }); ``` - [ ] **Step 2: Run test to verify it fails** Run: `php artisan test tests/Feature/Api/SocialAccountsApiTest.php` Expected: FAIL — route not found. - [ ] **Step 3: Create the API Resource** Create `app/Http/Resources/SocialAccountResource.php`: ```php $this->id, 'network' => $this->network, 'username' => $this->username, 'email' => $this->email, 'password' => $this->password, 'type' => $this->type, 'created_at' => $this->created_at, 'updated_at' => $this->updated_at, ]; } } ``` - [ ] **Step 4: Create the controller** Create `app/Http/Controllers/Api/SocialAccountController.php`: ```php filled('network')) { $query->where('network', $request->string('network')); } if ($request->filled('type')) { $query->where('type', $request->string('type')); } return SocialAccountResource::collection($query->orderBy('username')->get()); } public function show(SocialAccount $socialAccount) { return new SocialAccountResource($socialAccount); } } ``` - [ ] **Step 5: Add routes to `routes/api.php`** Update `routes/api.php` to: ```php

Muninn

Manage your sites and social accounts.

``` - [ ] **Step 2: Update `tests/Feature/ExampleTest.php` to match the new welcome content** The existing test only asserts a 200 status, so no change is strictly required. Run it to confirm: Run: `php artisan test tests/Feature/ExampleTest.php` Expected: PASS - [ ] **Step 3: Manually verify in the browser** Run: `npm run build` (or start `npm run dev` in one terminal and `php artisan serve` in another) Visit `http://localhost:8000/` and confirm: - The home page links to Sites and Social Accounts - `/sites`: can add, edit, delete a site - `/social-accounts`: can add, edit, delete an account; password show/hide toggle works - `GET /api/sites` and `GET /api/accounts` (e.g. via `curl http://localhost:8000/api/sites`) return JSON matching what's in the UI - [ ] **Step 4: Commit** ```bash git add resources/views/welcome.blade.php git commit -m "feat: add home page linking to Sites and Social Accounts" ``` --- ## Self-Review Notes - **Spec coverage:** Sites CRUD (Task 5), Social Accounts CRUD (Task 6), read-only API with filters for both resources (Tasks 7–8), no-auth throughout (no middleware added anywhere), plain-text password storage/display (Tasks 3, 6, 8) — all spec requirements are covered. - **Placeholder scan:** No TBD/TODO markers; Task 5 Step 6 contains a temporary placeholder route explicitly called out and immediately resolved in Task 6 Step 5 — this is a real, working intermediate state, not an unfinished placeholder. - **Type consistency:** `Site` fillable fields (`name`, `url`, `description`) match across model, factory, Livewire component, and API resource. `SocialAccount` fillable fields (`network`, `username`, `email`, `password`, `type`) and the `NETWORKS`/`TYPES` constants are consistent across model, factory, Livewire component/view, and API resource/controller.