# Developer guide

How to extend AGS Vocab. Read [architecture.md](architecture.md) first.

## Adding a new feature — the general pattern

1. **Migration** in `database/migrations/` — name with the alphabetical-ordering trick if the new table depends on another created at the same timestamp
2. **Model** in `app/Models/` — add `$fillable`, `$casts`, relationships
3. **Seeder** in `database/seeders/` — make it idempotent (`updateOrCreate`), then register it in `DatabaseSeeder::run`
4. **Filament resource** via `php artisan make:filament-resource <Model> --generate` — then set `$navigationGroup`, `$navigationIcon`, `$navigationSort`, `$navigationLabel`
5. **API endpoint** — controller in `app/Http/Controllers/Api/`, route in `routes/api.php`
6. **Flutter integration** — `api_client.dart` method, model, screen update
7. **Docs** — add a section to `docs/architecture.md` and run README hyperlinks

## Adding a word seeder batch

The word bank grows by dropping new data files into `database/seeders/data/`. The seeder picks them up via glob.

1. Create `database/seeders/data/bulk_words_partN.php` returning an array of rows: `[lemma, pos_code, level_code, difficulty_code, definition]`
2. Run `php artisan db:seed --class=BulkWordsSeeder` — idempotent, words and questions are upserted

Status: currently 1458 words across 4 parts. Target: 5000. See [word-bank.md](word-bank.md) for the full plan.

## Adding a new test type

1. Add a row to `test_types` via a migration or update `Create_test_types_table` seed if you're regenerating
2. Add the integer constant to `App\Models\TestType` (e.g. `public const NEW_TYPE = 4`)
3. Write `App\Services\Irt\Strategies\NewTypeStrategy` implementing `TestStrategy` (sessionDefaults, pickNext, shouldStop, shouldUpdateCanonicalScore, checkEligibility)
4. Register it in `TestStrategyResolver::for()` and `forCode()`
5. Update the validation in `TestController::start` to accept the new code
6. The Flutter HomeScreen auto-renders the new card because `/api/test-types` is data-driven

## Adding a new genre

The fast path: open `/admin/genres`, click **New Genre**, fill the form.

The seed path:
1. Edit `database/seeders/GenresSeeder.php`, add a new row to the array
2. Edit `database/seeders/GenreAssignSeeder.php`, add lemma map entries or keyword rules to back-fill existing words
3. Run `php artisan db:seed --class=GenresSeeder && php artisan db:seed --class=GenreAssignSeeder`

## Adding a new reader voice / accent

Fast path: `/admin/voices` → **New Voice**.

Seed path: edit `database/seeders/VoicesSeeder.php`, add a row, run `php artisan db:seed --class=VoicesSeeder`. The `voice_hints` array is searched (in order) against the browser's installed voice names; first substring match wins. For an entirely new accent, also create personas covering common characters (girl / boy / man / woman / teacher / elder).

## Adding an admin-tunable config

1. Add a row to `database/seeders/ConfigSeeder.php`:
   ```php
   ['my_new_flag', 'bool', 'feature', 'My new flag', 'Description shown in admin', 'true', 460, true],
   ```
   Fields: `[key, type, category, label, description, default_value, display_order, is_public]`
2. Run `php artisan db:seed --class=ConfigSeeder`
3. Read it anywhere via `app(SiteConfig::class)->get('my_new_flag', true)`
4. If your code path is hot, the 60s cache in `SiteConfig::all()` will dominate the cost

To force-refresh after an admin change: `SiteConfig::flush()`.

## Adding a Filament resource

```pwsh
php artisan make:filament-resource MyModel --generate
```

Then edit the generated `app/Filament/Admin/Resources/MyModels/MyModelResource.php`:

```php
protected static string|BackedEnum|null $navigationIcon = Heroicon::OutlinedTag;
protected static string|\UnitEnum|null $navigationGroup = 'Taxonomy';   // must use this widened type
protected static ?string $navigationLabel = 'My Models';
protected static ?int $navigationSort = 50;
```

The `string|\UnitEnum|null` type is **required** — Filament v5's parent class declares the property that way, narrower types fail at boot. See `feedback_composer_caret.md` style notes for other Filament gotchas.

## Branding override path

When the Filament admin needs to look different per deployment, edit `resources/views/filament/admin-theme.blade.php` — it's a Blade view loaded via a render hook in `AdminPanelProvider::panel()`, so changes are live on next request. All colors pull from SiteConfig so most tweaks happen in `/admin/configs` not in CSS.

## Flutter platform-aware base URL

`mobile/lib/api_client.dart::_resolveDefaultBaseUrl()` returns:
- `--dart-define=VOCABILE_API_URL=...` if set
- Else: `http://127.0.0.1:8000/api` on web, `http://10.0.2.2:8000/api` on Android

Override per-build with `--dart-define=VOCABILE_API_URL=http://192.168.1.108:8000/api` for testing from a real phone on the same Wi-Fi.

## Flutter web build gotcha — service workers

`flutter build web` ships a `flutter_service_worker.js` that aggressively caches the old bundle. Our build script **deletes it** after every build so testers don't get trapped on a stale build. If you ever see "request URL is 10.0.2.2 from a Windows browser" or similar staleness, the SW from a previous build is in their cache — instruct them to open in Incognito, or open DevTools → Application → Service Workers → Unregister.

## Running the test suite

There's no Pest/PHPUnit suite written yet. The IRT math is exercised by the seeded data + manual smoke runs:

```pwsh
# Smoke test the full CAT flow:
pwsh tests/manual/run_session.ps1
```

Adding tests would start in `tests/Feature/` (HTTP-level) and `tests/Unit/` (the IRT services).

## Deployment notes (not yet wired)

- Frontend (Flutter web bundle) is static — serve from any CDN / S3 / nginx
- Backend (Laravel) needs PHP 8.2 + MySQL + a queue runner (for the kudos cross-product sync `php artisan kudos:sync` cron)
- Configure the `ALLGIFTED_ACCOUNT_URL` env var when the central account service exists to enable unified kudos totals across AGS products
- Set `APP_ENV=production` to disable the dev_code in OTP responses (the code only appears in API responses + logs in `local`/`testing`)
