# artdag-common

Shared components for Art-DAG L1 (celery) and L2 (activity-pub) servers.

## Features

- **Jinja2 Templating**: Unified template environment with shared base templates
- **Reusable Components**: Cards, tables, pagination, DAG visualization, media preview
- **Authentication Middleware**: Cookie and JWT token parsing
- **Content Negotiation**: HTML/JSON/ActivityPub format detection
- **Utility Functions**: Hash truncation, file size formatting, status colors

## Installation

```bash
pip install -e /path/to/artdag-common

# Or add to requirements.txt
-e file:../common
```

## Quick Start

```python
from fastapi import FastAPI, Request
from artdag_common import create_jinja_env, render

app = FastAPI()

# Initialize templates with app-specific directory
templates = create_jinja_env("app/templates")

@app.get("/")
async def home(request: Request):
    return render(templates, "home.html", request, title="Home")
```

## Package Structure

```
artdag_common/
├── __init__.py              # Package exports
├── constants.py             # CDN URLs, colors, configs
├── rendering.py             # Jinja2 environment and helpers
├── middleware/
│   ├── auth.py              # Authentication utilities
│   └── content_negotiation.py  # Accept header parsing
├── models/
│   ├── requests.py          # Shared request models
│   └── responses.py         # Shared response models
├── utils/
│   ├── formatting.py        # Text/date formatting
│   ├── media.py             # Media type detection
│   └── pagination.py        # Pagination helpers
└── templates/
    ├── base.html            # Base layout template
    └── components/
        ├── badge.html       # Status/type badges
        ├── card.html        # Info cards
        ├── dag.html         # Cytoscape DAG visualization
        ├── media_preview.html  # Video/image/audio preview
        ├── pagination.html  # HTMX pagination
        └── table.html       # Styled tables
```

## Jinja2 Templates

### Base Template

The `base.html` template provides:
- Dark theme with Tailwind CSS
- HTMX integration
- Navigation slot
- Content block
- Optional Cytoscape.js block

```html
{% extends "base.html" %}

{% block title %}My Page{% endblock %}

{% block content %}
<h1>Hello World</h1>
{% endblock %}
```

### Reusable Components

#### Card

```html
{% include "components/card.html" %}
```

```html
<!-- Usage in your template -->
<div class="...card styles...">
    {% block card_title %}Title{% endblock %}
    {% block card_content %}Content{% endblock %}
</div>
```

#### Badge

Status and type badges with appropriate colors:

```html
{% from "components/badge.html" import status_badge, type_badge %}

{{ status_badge("completed") }}  <!-- Green -->
{{ status_badge("failed") }}     <!-- Red -->
{{ type_badge("video") }}
```

#### DAG Visualization

Interactive Cytoscape.js graph:

```html
{% include "components/dag.html" %}
```

Requires passing `nodes` and `edges` data to template context.

#### Media Preview

Responsive media preview with format detection:

```html
{% include "components/media_preview.html" %}
```

Supports video, audio, and image formats.

#### Pagination

HTMX-powered infinite scroll pagination:

```html
{% include "components/pagination.html" %}
```

## Template Rendering

### Full Page Render

```python
from artdag_common import render

@app.get("/runs/{run_id}")
async def run_detail(run_id: str, request: Request):
    run = get_run(run_id)
    return render(templates, "runs/detail.html", request, run=run)
```

### Fragment Render (HTMX)

```python
from artdag_common import render_fragment

@app.get("/runs/{run_id}/status")
async def run_status_fragment(run_id: str):
    run = get_run(run_id)
    html = render_fragment(templates, "components/status.html", status=run.status)
    return HTMLResponse(html)
```

## Authentication Middleware

### UserContext

```python
from artdag_common.middleware.auth import UserContext, get_user_from_cookie

@app.get("/profile")
async def profile(request: Request):
    user = get_user_from_cookie(request)
    if not user:
        return RedirectResponse("/login")
    return {"username": user.username, "actor_id": user.actor_id}
```

### Token Parsing

```python
from artdag_common.middleware.auth import get_user_from_header, decode_jwt_claims

@app.get("/api/me")
async def api_me(request: Request):
    user = get_user_from_header(request)
    if not user:
        raise HTTPException(401, "Not authenticated")
    return {"user": user.username}
```

## Content Negotiation

Detect what response format the client wants:

```python
from artdag_common.middleware.content_negotiation import wants_html, wants_json, wants_activity_json

@app.get("/users/{username}")
async def user_profile(username: str, request: Request):
    user = get_user(username)

    if wants_activity_json(request):
        return ActivityPubActor(user)
    elif wants_json(request):
        return user.dict()
    else:
        return render(templates, "users/profile.html", request, user=user)
```

## Constants

### CDN URLs

```python
from artdag_common import TAILWIND_CDN, HTMX_CDN, CYTOSCAPE_CDN

# Available in templates as globals:
# {{ TAILWIND_CDN }}
# {{ HTMX_CDN }}
# {{ CYTOSCAPE_CDN }}
```

### Node Colors

```python
from artdag_common import NODE_COLORS

# {
#     "SOURCE": "#3b82f6",    # Blue
#     "EFFECT": "#22c55e",    # Green
#     "OUTPUT": "#a855f7",    # Purple
#     "ANALYSIS": "#f59e0b",  # Amber
#     "_LIST": "#6366f1",     # Indigo
#     "default": "#6b7280",   # Gray
# }
```

### Status Colors

```python
STATUS_COLORS = {
    "completed": "bg-green-600",
    "cached": "bg-blue-600",
    "running": "bg-yellow-600",
    "pending": "bg-gray-600",
    "failed": "bg-red-600",
}
```

## Custom Jinja2 Filters

The following filters are available in all templates:

| Filter | Usage | Description |
|--------|-------|-------------|
| `truncate_hash` | `{{ hash\|truncate_hash }}` | Shorten hash to 16 chars with ellipsis |
| `format_size` | `{{ bytes\|format_size }}` | Format bytes as KB/MB/GB |
| `status_color` | `{{ status\|status_color }}` | Get Tailwind class for status |

Example:

```html
<span class="{{ run.status|status_color }}">
    {{ run.status }}
</span>

<code>{{ content_hash|truncate_hash }}</code>

<span>{{ file_size|format_size }}</span>
```

## Development

```bash
cd /root/art-dag/common

# Install in development mode
pip install -e .

# Run tests
pytest
```

## Dependencies

- `fastapi>=0.100.0` - Web framework
- `jinja2>=3.1.0` - Templating engine
- `pydantic>=2.0.0` - Data validation
