214 lines
9.0 KiB
Markdown
214 lines
9.0 KiB
Markdown
# Agent Development Guide
|
|
|
|
This file contains architectural context and conventions for AI agents (and humans) working on the TapeHoard codebase.
|
|
|
|
## Project Structure
|
|
|
|
```
|
|
tapehoard/
|
|
├── backend/ # FastAPI + SQLAlchemy + SQLite
|
|
│ ├── app/
|
|
│ │ ├── api/ # API routers
|
|
│ │ │ ├── common.py # Shared helpers & schemas
|
|
│ │ │ ├── system/ # System endpoints (13 modules)
|
|
│ │ │ ├── archive.py # Archive file index endpoints
|
|
│ │ │ ├── backups.py # Backup job endpoints
|
|
│ │ │ ├── inventory.py # Media fleet endpoints
|
|
│ │ │ ├── restores.py # Restore queue endpoints
|
|
│ │ │ └── schemas.py # Shared Pydantic schemas
|
|
│ │ ├── db/
|
|
│ │ ├── services/ # Business logic (scanner, archiver, scheduler)
|
|
│ │ └── main.py # FastAPI app factory + router registration
|
|
│ └── tests/ # Pytest suite (77 tests)
|
|
├── frontend/ # SvelteKit + TypeScript
|
|
│ ├── src/lib/api/ # Auto-generated OpenAPI SDK
|
|
│ └── tests/ # Playwright E2E suite (34 tests)
|
|
└── docs/ # Additional documentation
|
|
```
|
|
|
|
## Backend Architecture
|
|
|
|
### API Router Organization
|
|
|
|
All API routes live under `app/api/`. The `system` endpoints are split into a package (`app/api/system/`) with focused submodules:
|
|
|
|
| Module | Endpoints |
|
|
|--------|-----------|
|
|
| `system/jobs.py` | `/system/jobs/*`, `/system/jobs/{id}/cancel`, `/system/jobs/{id}/retry`, `/system/jobs/stream` |
|
|
| `system/scan.py` | `/system/scan`, `/system/index/hash`, `/system/scan/status` |
|
|
| `system/filesystem.py` | `/system/browse`, `/system/search` |
|
|
| `system/tree.py` | `/system/tree` |
|
|
| `system/dashboard.py` | `/system/dashboard/stats` |
|
|
| `system/settings.py` | `/system/settings` |
|
|
| `system/hardware.py` | `/system/hardware/discover`, `/system/hardware/ignore` |
|
|
| `system/discrepancies.py` | `/system/discrepancies/*`, batch ops, tree, browse |
|
|
| `system/database.py` | `/system/database/export`, `/system/database/import` |
|
|
| `system/tracking.py` | `/system/track/batch` |
|
|
| `system/notifications.py` | `/system/notifications/test` |
|
|
| `system/host.py` | `/system/ls` |
|
|
| `system/test.py` | `/system/test/reset` |
|
|
|
|
Each module defines its own `APIRouter` with `tags=["System"]` and is registered in `main.py` with `prefix="/system"`.
|
|
|
|
### Index-Only Principle
|
|
|
|
**Never rely on the live filesystem for data, except during a scan.** All read endpoints must operate exclusively on the database index. The filesystem is only accessed during:
|
|
|
|
- **Scan operations** (`/system/scan`) — to discover files, compute hashes, and sync the index.
|
|
- **Configuration endpoints** (`/system/ls`, `/system/browse` when path is outside roots) — to help users pick source roots during setup.
|
|
|
|
Browsing the archive, searching, or checking protection status must use the index only. This guarantees consistent results even when files are temporarily inaccessible, and prevents I/O bottlenecks on network or tape-backed storage.
|
|
|
|
### Shared Helpers (`app/api/common.py`)
|
|
|
|
Cross-cutting helpers and schemas that must not create circular imports:
|
|
|
|
- `get_source_roots(db_session)` → `List[str]`
|
|
- `get_exclusion_spec(db_session)` → `Optional[pathspec.PathSpec]`
|
|
- `get_ignored_status(path, tracking_map, exclusion_spec)` → `bool`
|
|
- `_validate_path_within_roots(path, roots)` → `bool`
|
|
- `_active_job_exists(db_session, job_type)` → `bool`
|
|
- `_get_last_scan_time(db_session)` → `Optional[datetime]`
|
|
- Shared Pydantic schemas: `DashboardStatsSchema`, `JobSchema`, `JobLogSchema`, `FileItemSchema`, `BrowseResponseSchema`, `ScanStatusSchema`, `SettingSchema`, `TestNotificationRequest`, `IgnoreHardwareRequest`, `BatchTrackRequest`
|
|
|
|
**Rule:** `common.py` must NEVER import from any API module (no `app.api.system`, `app.api.archive`, etc.). Only models, database, and standard libraries.
|
|
|
|
### Endpoint Naming Convention
|
|
|
|
All FastAPI route handlers must declare explicit `operation_id` to control the generated TypeScript SDK names.
|
|
|
|
| Pattern | Example Handler | `operation_id` | Generated TS |
|
|
|---------|-----------------|----------------|--------------|
|
|
| GET list | `list_jobs` | `list_jobs` | `listJobs` |
|
|
| GET one | `get_job` | `get_job` | `getJob` |
|
|
| POST create | `create_media` | `create_media` | `createMedia` |
|
|
| POST action | `trigger_scan` | `trigger_scan` | `triggerScan` |
|
|
| PATCH update | `update_media` | `update_media` | `updateMedia` |
|
|
| DELETE | `delete_media` | `delete_media` | `deleteMedia` |
|
|
| Batch actions | `batch_track` | `batch_track` | `batchTrack` |
|
|
|
|
**Never** let FastAPI auto-generate `operationId`. The old auto-generated names looked like `getDashboardStatsSystemDashboardStatsGet` — verbose and brittle.
|
|
|
|
### Router Prefix Rules
|
|
|
|
- Top-level domain routers (`archive`, `backups`, `inventory`, `restores`) define their own prefix in the router constructor (e.g., `APIRouter(prefix="/archive")`).
|
|
- `system` submodules use **no prefix** in the router constructor; `main.py` applies `prefix="/system"` when calling `app.include_router()`.
|
|
|
|
## Frontend Architecture
|
|
|
|
### TypeScript SDK (`frontend/src/lib/api/`)
|
|
|
|
Generated from the backend OpenAPI spec using `@hey-api/openapi-ts`:
|
|
|
|
```bash
|
|
just generate-client
|
|
```
|
|
|
|
This runs the full pipeline: exports the OpenAPI spec from the running FastAPI app and regenerates the TypeScript SDK in `frontend/src/lib/api/`. Use this **after any backend change** that adds, renames, or modifies endpoints or schemas.
|
|
|
|
The generated SDK exports clean camelCase functions (e.g., `getDashboardStats`, `listJobs`, `triggerScan`).
|
|
|
|
**Rule:** After renaming any backend handler or changing an `operation_id`, regenerate the SDK and update all frontend imports. The old verbose names will cause TypeScript errors.
|
|
|
|
### Frontend Imports to Avoid Shadowing
|
|
|
|
Some Svelte components define local functions with the same name as SDK imports (e.g., `cancelJob`, `retryJob` in `jobs/+page.svelte`). When this happens, alias the SDK import:
|
|
|
|
```typescript
|
|
import { cancelJob as cancelJobApi, retryJob as retryJobApi } from '$lib/api';
|
|
```
|
|
|
|
## Testing
|
|
|
|
### Backend Tests
|
|
|
|
```bash
|
|
cd backend && uv run pytest tests/ -v
|
|
```
|
|
|
|
- 77 tests covering API endpoints, providers, services
|
|
- Uses pytest-mock for mocking filesystem/hardware
|
|
- **Important:** Mocks that patch `get_source_roots` or `get_exclusion_spec` must target `app.api.common` (not `app.api.system`), since those helpers moved to `common.py`.
|
|
|
|
### Frontend E2E Tests
|
|
|
|
```bash
|
|
cd frontend && npx playwright test
|
|
```
|
|
|
|
- 34 Playwright tests using Chromium
|
|
- Backend test server auto-starts via `playwright.config.ts` webServer config
|
|
- Tests use `requestContext` for direct API calls + `page` for UI interactions
|
|
|
|
### macOS IPv6 Gotcha
|
|
|
|
On macOS, `localhost` resolves to `::1` (IPv6) by default, but uvicorn may bind to IPv4 only. This causes `ECONNREFUSED ::1:8001` in Playwright tests.
|
|
|
|
**Fix:** Always use `127.0.0.1` instead of `localhost` for backend URLs:
|
|
- `frontend/tests/helpers.ts`: `API_URL = 'http://127.0.0.1:8001'`
|
|
- `frontend/playwright.config.ts`: `webServer.url = 'http://127.0.0.1:8001'`
|
|
|
|
## Common Tasks
|
|
|
|
### Adding a New System Endpoint
|
|
|
|
1. Choose the appropriate `app/api/system/<module>.py` file (or create a new one if it doesn't fit existing categories).
|
|
2. Add the route handler with an explicit `operation_id`.
|
|
3. Import shared helpers from `app.api.common` if needed.
|
|
4. Register the new router in `app/main.py` with `prefix="/system"`.
|
|
5. Regenerate the TypeScript SDK.
|
|
6. Update frontend imports if using the new endpoint.
|
|
7. Add backend tests in `backend/tests/test_api_system.py` (or a new test file if it's a new domain).
|
|
8. Run `just lint` before finishing.
|
|
|
|
### Regenerating the OpenAPI Spec / TypeScript SDK
|
|
|
|
Use the convenience command:
|
|
|
|
```bash
|
|
just generate-client
|
|
```
|
|
|
|
Or run the steps manually:
|
|
|
|
```bash
|
|
cd backend && uv run python -c "import json; from app.main import app; json.dump(app.openapi(), open('openapi.json', 'w'), indent=2)"
|
|
cd ../frontend && npx @hey-api/openapi-ts -i ../backend/openapi.json -o src/lib/api
|
|
```
|
|
|
|
### Verifying No Auto-Generated operationIds
|
|
|
|
```bash
|
|
cd backend && uv run python -c "
|
|
from app.main import app
|
|
import re
|
|
for path, methods in app.openapi()['paths'].items():
|
|
for method, info in methods.items():
|
|
op_id = info.get('operationId', '')
|
|
if re.search(r'_(get|post|put|patch|delete)$', op_id):
|
|
print(f'DIRTY: {method.upper()} {path} = {op_id}')
|
|
print('Check complete')
|
|
"
|
|
```
|
|
|
|
## Lint & Format
|
|
|
|
```bash
|
|
just lint # Runs ruff (Python) + svelte-check (TypeScript/Svelte)
|
|
```
|
|
|
|
Pre-commit hooks are configured but may stash unstaged changes.
|
|
|
|
## Environment
|
|
|
|
- **Backend:** Python 3.13, FastAPI, SQLAlchemy 2.x, SQLite, uv for package management
|
|
- **Frontend:** SvelteKit, TypeScript, Tailwind CSS, shadcn-svelte components
|
|
- **Test server:** `TAPEHOARD_TEST_MODE=true` enables `/system/test/reset` and mock providers
|
|
|
|
## Documentation Files
|
|
|
|
| File | Contents |
|
|
|------|----------|
|
|
| `README.md` | Human-facing project overview |
|
|
| `AGENTS.md` | This file — agent development guide |
|