# Alpha-Media API

Read-only REST access to the **AlfaMedia editorial database** (Editorial
Organiser / "Crossmedia"). Intended for programmatic use by other services and
AI agents.

- **Base URL:** `https://alpha-media.img.ch`
- **Format:** JSON (UTF-8)
- **Auth:** API key required on all `/api/*` data endpoints (see *Authentication*).
- **Interactive spec:** `https://alpha-media.img.ch/swagger` ·
  OpenAPI JSON: `https://alpha-media.img.ch/openapi.json`
- **This document (raw):** `https://alpha-media.img.ch/API.md`

## Architecture

```
Client → Cloudflare → Apache (alpha-media.img.ch) → FastAPI (127.0.0.1:8092)
       → SSH tunnel (127.0.0.1:15432) → PostgreSQL "atlas" @ srvalfadb.srmnet.ch
```

The API host (naumedia06) cannot reach the database directly; it goes through a
persistent SSH tunnel via the Docker host. All queries run in **read-only**
transactions and are strictly parameterised. Database credentials never leave
the server.

---

## Authentication

All data endpoints (`/api/articles`, `/api/articles/{id}`, `/api/images/...`)
require an API key. Provide it in **any** of these ways:

| Method | Example |
|--------|---------|
| Header (recommended) | `X-API-Key: <key>` |
| Bearer token         | `Authorization: Bearer <key>` |
| Query param          | `?api_key=<key>` (handy for `<img>` tags / browsers) |

```bash
curl -H 'X-API-Key: <key>' 'https://alpha-media.img.ch/api/articles'
```

Missing/invalid key → **HTTP 401**. **Public (no key):** `/`, `/API.md`,
`/swagger`, `/openapi.json`, `/api/health`. In `/swagger`, click **Authorize**
and paste the key to try endpoints interactively.

Keys are configured server-side (`AMAPI_API_KEYS`, comma-separated for multiple
keys / rotation). Ask the operator for a key.

---

## Endpoints

### `GET /api/health`
Liveness + database connectivity check.

```json
{ "status": "ok", "database": "atlas" }
```
Returns HTTP 503 with `{"status":"error", ...}` if the database is unreachable.

---

### `GET /api/articles`
List editorial articles in a date range. **Default: the last 7 days.**

#### Query parameters
| Param        | Type   | Default        | Description |
|--------------|--------|----------------|-------------|
| `from`       | date   | today − 7 days | Start date `YYYY-MM-DD`, inclusive. |
| `to`         | date   | today          | End date `YYYY-MM-DD`, inclusive. |
| `date_field` | enum   | `updated`      | Which timestamp the range applies to: `updated` or `created`. |
| `channel`    | string | –              | Filter by publication channel, substring match (e.g. `FN`, `Newsportal`, `St.Galler`). |
| `online_only`| bool   | `true`         | Only articles on an online news channel (`… Newsportal Artikel`); `channels`/`publications` are limited to those. Set `false` for all articles + all channels. |
| `limit`      | int    | `50`           | Page size, 1–500. |
| `offset`     | int    | `0`            | Pagination offset. |

#### Response
```json
{
  "filter": { "from": "2026-07-03", "to": "2026-07-10",
              "date_field": "updated", "channel": "FN" },
  "pagination": { "limit": 50, "offset": 0, "total": 12, "returned": 12 },
  "articles": [
    {
      "id": 521445,
      "title": "Nachttaxi",
      "status_id": 4,
      "topic_id": 442019,
      "created_at": "2026-07-09T16:17:14.450199+02:00",
      "updated_at": "2026-07-10T09:56:07.581248+02:00",
      "text_excerpt": "Stadt Frauenfeld setzt weiterhin auf Nachttaxis ...",
      "char_count": 0,
      "image_count": 1,
      "channels": ["FN Newsportal Artikel"]
    }
  ]
}
```

#### Field notes
- `published_at` — earliest actual publication timestamp, derived from
  `crossmedia_publicationschedule.honored_at` and `crossmedia_exportlog`
  (`null` if never exported/published yet). Note: not a column on the article.
- `title` — the article's `keyword` (editorial slug/headline).
- `text_excerpt` — first ~300 chars of the body, HTML tags stripped.
- `publications` — spelled-out newspaper name(s), e.g. `["Frauenfelder
  Nachrichten"]` (derived from the channel prefix; deduplicated).
- `channels` — raw publication channels the article is scheduled to.
  `Print Layout` = print edition, `Newsportal Artikel` = online.
- `image_count` — number of linked image assets (see *Data model*).
- Timestamps are ISO-8601 with timezone (Europe/Zurich).

#### Examples
```bash
# Default: last 7 days
curl 'https://alpha-media.img.ch/api/articles'

# Explicit range, by creation date
curl 'https://alpha-media.img.ch/api/articles?from=2026-06-01&to=2026-06-30&date_field=created'

# Only Frauenfelder Nachrichten, 100 per page
curl 'https://alpha-media.img.ch/api/articles?channel=FN&limit=100'
```

---

### `GET /api/articles/{id}`
Return a single article with **full text** and **image assets**.

#### Response
```json
{
  "article": {
    "id": 521445,
    "title": "Nachttaxi",
    "status_id": 4,
    "topic_id": 442019,
    "uuid": "eb138386-99e4-4727-94fb-3ea0d351c114",
    "created_at": "2026-07-09T16:17:14+02:00",
    "updated_at": "2026-07-10T09:56:07+02:00",
    "text_html":  "<!DOCTYPE html>...full article HTML...",
    "text_plain": "Stadt Frauenfeld setzt weiterhin auf Nachttaxis ...",
    "channels": [ { "channel": "FN Newsportal Artikel", "type": "online" } ],
    "images": [
      {
        "id": 1438575,
        "uuid": "fcbadc70-0654-482e-a2a3-48188d7a9c70",
        "keyword": "nachttaxi",
        "caption": "Patrick Haas ... und Stadtingenieur Sascha Bundi ...",
        "author": "Nico Wrzeszcz",
        "copyright": "", "source": "Stadt Frauenfeld",
        "width": 1875, "height": 2500,
        "mime_type": "image/jpeg", "filesize": 1010761,
        "md5_checksum": "5d74bb47245684221a4c6196d5fa7287",
        "role_id": null,
        "thumbnail_url": "/api/images/1438575/thumbnail",
        "lowres_url":    "/api/images/1438575/lowres",
        "highres_url":   "/api/images/1438575/highres"
      }
    ]
  }
}
```
- `text_html` — the full article body as stored (Editorial Organiser HTML).
- `text_plain` — same content, tags stripped and whitespace collapsed.
- `channels` — publication channels (`type`: `print` | `online`).
- `images[]` — linked assets with metadata + ready-to-use image URLs (below).
- Returns `404` if the article does not exist or is trashed.

---

### `GET /api/images/{id}/{rendition}`
Serve an image binary for a `crossmedia_image` id. `rendition` ∈
`thumbnail` (~188 px) · `lowres` (web size) · `highres` (full-resolution
original). Returns `image/jpeg`, cacheable 24 h.

```bash
curl -o photo.jpg 'https://alpha-media.img.ch/api/images/1438575/highres'
```
Bytes are proxied from the internal EO mediaproxy (resolved via
`crossmedia_image.atlas_id → cdb_foto_content`); clients never talk to it
directly. `400` for an unknown rendition, `404` if the image or its file is
missing.

---

## Data model (background)

Articles live in the PostgreSQL database `atlas` (Editorial Organiser /
Crossmedia model):

- `crossmedia_article` — the articles (`keyword` = title, `text` = body,
  `trashed_at IS NULL` = active).
- `crossmedia_article_images` — links articles ↔ image assets.
- `crossmedia_image` — image metadata (author, size, mime, md5). Binary files
  are **not** in the DB; highres files live in the RRFS file store
  (`/rrfs1/rrfs_production/files/atlas/`) referenced via
  `crossmedia_image.atlas_id → cdb_foto_content`.
- Publication assignment: `crossmedia_publicationschedule.channel_id →
  crossmedia_baseexporter` (the channel = newspaper edition / online portal).

Channel abbreviations: WN=Wiler, SN=St.Galler, BN=Bodensee, RB=Rheintaler Bote,
OBNA=Oberthurgauer, WIZE=Winterthurer, TOGGI/TOZ=Toggenburger, KN=Kreuzlinger,
AN=Aarauer, OLT=Neue Oltner, LR=Luzerner Rundschau, ZW=Zuger Woche,
UZE=Unterland, FUR/FU=Furttaler, TBZH=Tagblatt Zürich, RU/RUE=Rümlanger,
FN=Frauenfelder Nachrichten.

---

## Conventions & limits
- Read-only. No write endpoints exist.
- `statement_timeout` = 15 s per query.
- Errors: `400` (bad params), `404` (unknown path), `503` (DB unavailable).
- Pagination is offset-based; use `pagination.total` to page through results.

## Security (current state)
- Read-only; data endpoints require an API key (see *Authentication*).
- Keys are compared in constant time; multiple keys supported for rotation.
- Behind Cloudflare (TLS). For stricter control, Cloudflare Access can be added
  in front as a second layer.

## Changelog
- **0.7.0** — export defaults to online news channels: `online_only` param
  (default true) filters to articles on a `… Newsportal Artikel` channel and
  limits `channels`/`publications` to those.
- **0.5.0** — `publications` (spelled-out newspaper names) on both article
  endpoints; per-channel `publication` in the single article.
- **0.4.0** — `published_at` (derived publication timestamp) on both article
  endpoints; `best_before` on the single article.
- **0.3.0** — API-key authentication on all data endpoints (X-API-Key / Bearer
  / `?api_key=`); docs, Swagger and health stay public.
- **0.2.0** — single article `GET /api/articles/{id}` (full text + images) and
  image renditions `GET /api/images/{id}/{thumbnail|lowres|highres}`.
- **0.1.0** — initial release: `/api/health`, `/api/articles` (date range,
  channel filter, default last 7 days).
