8.6 KiB
AGENTS.md
AI coding agent guidelines for NNTmux - a Laravel 13 Usenet indexer.
Quick Reference
php artisan test --compact --filter=TestName # Run single test (PHPUnit only)
./vendor/bin/pint --dirty # Format changed files
php artisan tmux:start # Start processing engine
npm run build # Required after frontend changes
php artisan route:cache # Refresh cached routes if new routes seem missing
Architecture
NNTmux scans Usenet servers, collects headers, organizes releases, and enriches with metadata. Data flow:
NNTP → NNTPService → BinariesRunner → ReleaseCreationService → ReleaseProcessingService → SearchService → API/Web
Key Patterns
| Pattern | Location | Example |
|---|---|---|
| Service Layer | app/Services/ |
50+ services with facades (Search::, Categorization::, TvProcessing::, Yenc::, Elasticsearch::) |
| Pipeline | */Pipes/ |
TvProcessingPipeline (TMDB→TVDB→TVMaze→Trakt), CategorizationPipeline (priority-driven; Music runs before Book for audiobook detection) |
| Driver | Search/Drivers/ |
Manticore/Elasticsearch via SEARCH_DRIVER env var |
| Runners | Runners/ |
BinariesRunner, ReleasesRunner, BackfillRunner, PostProcessRunner |
| DTO | */DTO/, app/Support/DTOs/ |
NameFixResult, ReleaseProcessingContext, ReleaseCreationResult |
| Enum | app/Enums/ |
UserRole, QueueType, FileCompletionStatus |
| Observer | app/Observers/, AppServiceProvider |
ReleaseObserver, MovieInfoObserver, RolePromotionObserver |
| View Composer | app/View/Composers/, AppServiceProvider |
GlobalDataComposer shared across layouts.* and admin.* |
Tmux Processing Engine
Multi-pane terminal orchestrator at app/Services/Tmux/. Components: TmuxSessionManager, TmuxLayoutBuilder, TmuxPaneManager, TmuxTaskRunner, TmuxMonitorService.
Sequential Modes (Settings::settingValue('sequential')):
- Mode 0: Full (3 windows, parallel panes)
- Mode 1: Basic (reduced)
- Mode 2: Stripped (minimal)
Commands: tmux:start, tmux:stop, tmux:attach, tmux:monitor, tmux:health-check
Config: config/tmux.php + database settings table
Testing
PHPUnit only (no Pest). Create tests: php artisan make:test --phpunit {name}
- In-memory SQLite (
DB_CONNECTION=testing) - App boot can hit
Settings::settingValue()viaCategorizationPipeline(app/Providers/CategorizationServiceProvider.php→app/Services/Categorization/CategorizationPipeline.php), even in focused controller tests - For isolated tests that bypass the normal app test DB setup, seed a minimal
settingstable before app bootstrap;categorizeforeignandcatwebdlare the minimum keys needed for this path, andtests/Feature/AdminContentControllerTest.phpshows the file-backed SQLite workaround whenphp artisan testwould otherwise fail during startup - Feature tests that render shared layouts or admin pages may need to clear
App\View\Composers\GlobalDataComposer::$resolvedData; seeresetGlobalComposerState()helpers intests/Feature/AdminContentControllerTest.php,AdminGroupControllerTest.php, andNzbAndRssAccessTest.php - All HTTP mocked - no real API calls
- Suites:
Install,Unit,Feature(alsotests/Integration/for live API tests, not in CI) - Use model factories; check for custom states first
- Mocks in
tests/Fixtures/,tests/mock_data/ - Test harnesses in
tests/Support/(e.g.,DatabaseTestCase,TestBinariesHarness) - PHPUnit 12 — use
#[Test]attributes ortestprefix naming
Project Conventions
Models (app/Models/)
- Casts in
casts()method, not$castsproperty - Foreign keys:
{table}_id(e.g.,groups_id) - Key:
Release,Video,TvEpisode,MovieInfo,UsenetGroup
API (app/Http/Controllers/Api/)
- v1: XML (newznab compat) -
ApiController.php - v2: JSON REST -
ApiV2Controller.php - RSS feeds are separate from
/api: editroutes/rss.php+App\Http\Controllers\RssController;/rss/*is mounted frombootstrap/app.phpandRssController::userCheck()validatesapi_token
Config
- App configs:
config/nntmux*.php,config/tmux.php,config/search.php - Never
env()outside config - useconfig('key') - Runtime settings:
Settings::settingValue() - Laravel 13 route/middleware wiring lives in
bootstrap/app.php; use that file when adding route groups, aliases, or middleware (for example the/rssmount) - In Docker/Sail,
Makefileexports.envSEARCH_DRIVERasCOMPOSE_PROFILES, so only the matching Manticore/Elasticsearch service starts
Commands
- 80+ auto-registered in
app/Console/Commands/ - Create with
php artisan make:+--no-interaction - Docker/Sail convenience targets live in
Makefile; prefermake artisan cmd="...",make test filter=TestName,make pint, andmake npm-buildwhen working inside containers - This workspace may have cached routes under
bootstrap/cache/routes-*.php; after adding/changing routes, refresh withphp artisan route:cacheif a route appears missing
Admin Content
- Admin content ordering is scoped by
contenttype, not global: Homepage rows only reorder Homepage rows, Useful Links only reorder Useful Links - The admin list at
resources/views/admin/content/index.blade.phprenders one draggable table per content group and uses Alpine componentcontentToggle resources/js/alpine/components/content-toggle.jsis the integration point for admin content interactions: grouped drag ordering, enable/disable toggles, and delete confirmations all live there- Reorder requests go to
AdminContentController::reorder()and must include the exact ID set for onecontenttype; mixed-type or partial payloads are rejected - The ordinal field is intentionally hidden on
resources/views/admin/content/add.blade.php; the server assigns new items to the bottom of their own group inAdminContentController::nextBottomOrdinal() - Deleting content does not renumber remaining items; gaps in per-group ordinals are expected
Code Formatting & Quality
After every code change, run all of the following before considering a task done:
1. Format changed PHP files
./vendor/bin/pint --dirty # Format only changed files
2. Check for static analysis errors
./vendor/bin/phpstan analyse --memory-limit=2G # Run PHPStan static analysis
3. Check for syntax / lint errors
find app -name "*.php" | xargs php -l # PHP syntax lint on all changed files
These steps are mandatory. Run them before considering any task done. Do not wait for the pre-commit hook to catch formatting or type errors. If PHPStan reports new errors introduced by your changes, fix them before finishing. If you add a PHPStan baseline entry, document why.
Pre-commit (CaptainHook)
Auto-runs: PHP lint, Composer lock validation, Pint formatting. Commit limits: 200 char subject, 72 char body.
Key Directories
| Path | Purpose |
|---|---|
app/Services/TvProcessing/ |
TV metadata pipeline |
app/Services/Search/ |
Manticore/ES abstraction |
app/Services/NameFixing/ |
Release name correction (see README.md there) |
app/Services/Tmux/ |
Tmux orchestration |
app/Facades/ |
Static service accessors |
External APIs
Requires .env keys: TMDB, TVDB, TVMaze, Trakt, OMDB (TV/Movies); IGDB, GiantBomb, Steam (Games); AniList, AniDB (Anime); NNTP credentials.
Frontend
Blade + TailwindCSS v4 + Vite bundling. Run npm run build after changes.
- Livewire 3: Used only in the forum package
- Alpine.js: CSP-safe build with component architecture in
resources/js/alpine/- Core components loaded eagerly in
alpine/index.js - Page-specific components lazy-loaded via
alpine/lazy-loader.js - Lazy-loaded pages must declare an
x-dataname that matches a key inalpine/lazy-loader.js, or the component JS will never load; example:resources/views/admin/content/index.blade.phpusesx-data="contentToggle"so delete/toggle handlers fromresources/js/alpine/components/content-toggle.jsare available - Stores in
alpine/stores/, components inalpine/components/
- Core components loaded eagerly in
- CSS: Main entry is
resources/css/app.css(importscsp-safe.cssfor component styles) - Vite entry points:
resources/js/app.js,resources/css/app.css,resources/forum/blade-tailwind/js/forum.js,resources/forum/blade-tailwind/css/forum.css
This structure ensures Content Security Policy (CSP) compliance by using Alpine.js CSP-safe build and keeping scripts and styles in external files.