# Agent Guidelines for Django Admin - CART

Django admin interface for managing product↔tag mappings and authentication tokens for ingest-api.

See [../AGENTS.md](../AGENTS.md) for full project context.

## Quick Start

```bash
cd admin

# 1. Copy environment file
cp .env.example .env

# 2. Install dependencies
uv sync

# 3. Run migrations
make migrate

# 4. Create superuser (first time only)
make createsuperuser

# 5. Start development server
make runserver
```

Django admin available at http://localhost:8000/admin/

## Makefile Commands

| Command | Description |
| --- | --- |
| `make help` | Show available commands |
| `make migrate` | Apply database migrations |
| `make makemigrations` | Create new migrations from model changes |
| `make runserver` | Start development server (port 8000) |
| `make createsuperuser` | Create admin superuser account |
| `make shell` | Open Django shell |
| `make check` | Run Django system checks |
| `make test` | Run Django test suite |
| `make gen-test-data` | Generate test data (products, tags, locations) |

## Environment Variables

See [../environment.md](../TODO.md) or `admin/.env` for all variables.

## Code Style Guidelines

### Imports
Order as: standard library → third-party → local packages. Use relative imports within apps.
```python
from django.http import JsonResponse, HttpResponseForbidden
from django.views import View

from simple_history.admin import SimpleHistoryAdmin

from .models import Product, Tag
from .views import bulk_add_tags, import_from_sightings
```

### Models
- Use Django's ORM conventions
- Keep models in sync with sighting schema shared with Go
- Enable `HistoricalRecords` for audit trail (simple_history)
- Add `Meta` with ordering
```python
from django.db import models
from simple_history.models import HistoricalRecords

class Product(models.Model):
    name = models.CharField(max_length=255)
    description = models.TextField(blank=True, default="")
    min_stock_level = models.IntegerField(default=1)
    note = models.CharField(max_length=255, blank=True, default="")
    tags = models.ManyToManyField("Tag", blank=True, related_name="products")
    created_at = models.DateTimeField(auto_now_add=True)
    updated_at = models.DateTimeField(auto_now=True)
    history = HistoricalRecords()

    class Meta:
        ordering = ["name"]

    def __str__(self):
        return self.name
```

### Admin
- Register all models with Django admin
- Use `SimpleHistoryAdmin` for audit trail support
- Use `list_display`, `search_fields`, `filter_horizontal` for usability
- Add custom admin actions via `get_urls()`
```python
from simple_history.admin import SimpleHistoryAdmin

@admin.register(Product)
class ProductAdmin(SimpleHistoryAdmin):
    list_display = ["name", "min_stock_level", "tags_count", "created_at"]
    search_fields = ["name", "description"]
    filter_horizontal = ["tags"]

    def tags_count(self, obj):
        return obj.tags.count()

    tags_count.short_description = "Tags"
```

### Views
- Use class-based views for endpoints with multiple HTTP methods
- Restrict internal endpoints to localhost only (IP whitelist)
- Add proper error handling
```python
from django.http import JsonResponse, HttpResponseForbidden
from django.views import View

class TokenListView(View):
    def dispatch(self, request, *args, **kwargs):
        remote_addr = request.META.get("REMOTE_ADDR", "")
        if remote_addr not in ("127.0.0.1", "::1"):
            return HttpResponseForbidden("Forbidden")
        return super().dispatch(request, *args, **kwargs)

    def get(self, request):
        tokens = (
            AuthToken.objects
            .filter(is_active=True)
            .values("token", "note")
        )
        return JsonResponse({"tokens": list(tokens)})
```

### Migrations
- One migration file per feature/change
- Name migrations descriptively: `0001_initial`, `0002_add_sku_field`
- Test migrations run both forward and backward
- Use `--dry-run` to preview migrations

### Testing
- Use Django's TestCase for database tests
- Use factory_boy or Django factories for model fixtures
- Test admin interfaces with Client
```python
from django.test import TestCase, Client

class ProductTest(TestCase):
    def test_product_creation(self):
        p = Product.objects.create(name="Test", sku="TEST-001")
        self.assertEqual(p.name, "Test")
```

## Database Schema (Products App)

### LocationType Model
| Field | Type | Description |
| --- | --- | --- |
| `id` | AutoField | Primary key |
| `name` | CharField(50) | Unique name (e.g., "warehouse", "rack", "shelf") |
| `description` | TextField | Optional description |
| `created_at` | DateTimeField | Auto-populate on create |
| `updated_at` | DateTimeField | Auto-update on save |
| `history` | HistoricalRecords | Audit trail |

### Location Model
| Field | Type | Description |
| --- | --- | --- |
| `id` | AutoField | Primary key |
| `code` | CharField(64) | Unique location code (e.g., "WH-A1-R01-SH03") |
| `name` | CharField(255) | Human-readable name |
| `description` | TextField | Optional description |
| `parent` | ForeignKey→Location | Optional parent (self-referential hierarchy) |
| `type` | ForeignKey→LocationType | Optional location type |
| `created_at` | DateTimeField | Auto-populate on create |
| `updated_at` | DateTimeField | Auto-update on save |
| `history` | HistoricalRecords | Audit trail |

### Product Model
| Field | Type | Description |
| --- | --- | --- |
| `id` | AutoField | Primary key |
| `name` | CharField(255) | Product name |
| `part_number` | CharField(255) | Optional alphanumeric part number |
| `description` | TextField | Optional description |
| `location` | ForeignKey→Location | Designated warehouse location (optional) |
| `min_stock_level` | IntegerField | Minimum stock threshold (default: 1) |
| `note` | CharField(255) | Optional note |
| `tags` | ManyToMany→Tag | RFID tags assigned to product |
| `created_at` | DateTimeField | Auto-populate on create |
| `updated_at` | DateTimeField | Auto-update on save |
| `history` | HistoricalRecords | Audit trail (simple_history) |

### Tag Model
| Field | Type | Description |
| --- | --- | --- |
| `id` | AutoField | Primary key |
| `tag_id` | CharField(255) | Unique RFID tag UID |
| `note` | CharField(255) | Optional note about tag |
| `created_at` | DateTimeField | Auto-populate on create |
| `history` | HistoricalRecords | Audit trail |
| `products` | ManyToMany | Many-to-many with Product |

### Sighting Model (read-only from ingest-api writes)
| Field | Type | Description |
| --- | --- | --- |
| `id` | AutoField | Primary key |
| `tag_id` | CharField(64) | References Tag.uid |
| `loc` | CharField(64) | Location identifier from MQTT |
| `seen_at` | DateTimeField(3) | Detection timestamp |
| `signal` | IntegerField | RSSI strength (dBm) |
| `batch_ts` | DateTimeField(3) | Batch generation timestamp |

### AuthToken Model (Tokens App)
| Field | Type | Description |
| --- | --- | --- |
| `id` | AutoField | Primary key |
| `token` | CharField(128) | Unique auth token |
| `note` | CharField(255) | Optional note (e.g. location identifier) |
| `is_active` | BooleanField | Token status |
| `created_at` | DateTimeField | Auto-populate on create |

## DRF API Endpoints

The Products API uses **Django REST Framework** (DRF) with OOTB pagination, search, and ordering.

### GET /cart/api/products/

**Purpose:** Paginated list of products with tags and locations.

**Authentication:** Required (session-based via Django admin login)

**Query Parameters:**
- `page` - Page number (default: 1)
- `page_size` - Items per page (default: 50, max: 100)
- `search` - Search name, part_number, description
- `ordering` - Sort field (`name`, `part_number`, `created_at`, `updated_at`)
- `ordering=-created_at` - Sort descending

**Response:**
```json
{
  "count": 101,
  "next": "http://localhost:8000/cart/api/products/?page=2",
  "previous": null,
  "results": [
    {
      "id": 7,
      "name": "Bolt Kit - 004",
      "part_number": "BLT-KIT-004",
      "tags": [
        {"id": 1, "tag_id": "E2003412B802011890600548", ...}
      ],
      "description": "Bolt assortment kit",
      "min_stock_level": 20,
      "note": "",
      "location": {
        "id": 123,
        "code": "BLD-02-WH01-Z01-R03-SH01-BX02",
        "name": "Bin 2",
        "parent": 122,
        "type": {"id": 5, "name": "bin", ...}
      },
      "created_at": "2026-05-27T11:54:25.744000Z",
      "updated_at": "2026-05-27T11:54:25.744000Z"
    }
  ]
}
```

**Features:**
- PageNumberPagination (50 items/page)
- Search by name/part_number/description
- Ordering by name/part_number/created_at/updated_at
- Session authentication (login required)
- Flat location data (no hierarchy)

### GET /cart/api/products/{id}/

**Purpose:** Get single product by ID.

**Response:** Same as single `results` item above.

### Internal Endpoints

#### GET /internal/tokens/

**Purpose:** ingest-api polls this endpoint every minute to get active tokens.

**Response:**
```json
[
  {
    "token": "change-me-in-production-xxx",
    "note": "rack-A1"
  }
]
```

**Access:** No authentication (ingest-api trusts internal network).

## Django Settings

Located in `cart/settings.py`.

Key configurations:
- `ALLOWED_HOSTS` - Comma-separated from `.env`
- `DATABASES` - SQLite or MySQL from `DATABASE_URL`
- `ROOT_URLCONF` - Points to main URL config
- `INSTALLED_APPS` - Products, Tokens, Sightings (Phase 2)

## Admin Access

1. Navigate to http://localhost:8000/admin/
2. Login with superuser credentials
3. Products → Add/Edit products with RFID tag associations
4. Tokens → Create/auth tokens for ingest-api

## Shared Database with Go

Django reads the same `sightings` table that Go writes to. Schema shared across:
- **Go (ingest-api)** → writes sightings
- **Django (admin)** → reads sightings, manages product↔tag mappings
- **Go (future frontend)** → reads sightings for Phase 2

## Testing Checklist

Before committing:
- [ ] `make check` passes
- [ ] `make test` passes
- [ ] Migrations are backward compatible
- [ ] Admin interface works for all CRUD operations
- [ ] Internal API endpoints return correct responses

## Phase 2 - Sightings App

Future work:
- Add sightings app for frontend
- Implement JWT authentication
- Add sighting table with search/filter
- Implement pagination for large datasets

## Notes

- Django admin is accessible only behind VPN (Tailscale)
- Product↔tag mapping is the bridge between raw RFID and business logic
- Token-based auth allows ingest-api to know submission source location
