ograf-core 0.6.0

Rust implementation of the OGraf v1 graphics-control API (HTTP + WebSocket) - 100% spec-compliant library
Documentation
# ograf-core

An independent Rust implementation of the [OGraf](https://ograf.ebu.io/) graphics-control HTTP + WebSocket API.

> **Note:** This is an unofficial implementation, not affiliated with or endorsed by the EBU. It aims for full spec compliance with the official [OGraf v1 specification]https://ograf.ebu.io/.

## Design

Every access decision — who may connect a renderer, who may see it, who may
target it — is delegated to an [`AccessControl`](src/access.rs) trait that a
consuming binary implements and wires in. `ograf-core` ships one trivial
implementation, [`AllowAllAccessControl`](src/access.rs), that imposes no
restriction at all.

This is a deliberate Dependency Inversion seam: `ograf-core` depends on
nothing outside this crate and knows nothing about tenants, or
credentials. A consumer that wants real access control (zones, per-vendor
API keys, encrypted renderer tokens, ...) implements `AccessControl` in its
own crate and links `ograf-core` as a library — it never needs to fork or
patch this code to do it.

Which renderers exist beyond the connected ones is a second, separate seam:
[`RendererDirectory`](src/directory.rs). Core keeps no storage — on its own
it remembers disconnected renderers for the life of the process. A consumer
with a database implements `known_renderers()` and passes it with
`AppState::with_directory`, so controllers also see renderers that are
offline or haven't connected yet (`status: ERROR`).

## Quick start

```rust
use std::{net::SocketAddr, sync::Arc};

use ograf_core::{access::AllowAllAccessControl, build_router, config::Config, store::renderers::RendererRegistry, AppState};

#[tokio::main]
async fn main() -> anyhow::Result<()> {
    let config = Config::from_env();

    let state = AppState::new(
        Arc::new(config),
        Arc::new(RendererRegistry::new()),
        Arc::new(AllowAllAccessControl),
    );

    let addr: SocketAddr = format!("{}:{}", state.config.host, state.config.port).parse()?;
    let listener = tokio::net::TcpListener::bind(addr).await?;
    axum::serve(listener, build_router(state)).await?;

    Ok(())
}
```

`build_router` returns a plain `axum::Router` — nest it under your own
top-level router alongside whatever admin/auth routes your binary adds.

## Configuration

`Config::from_env()` reads:

| Variable | Default | Meaning |
|---|---|---|
| `OGRAF_HOST` | `0.0.0.0` | Bind address |
| `OGRAF_PORT` | `8080` | Bind port |
| `OGRAF_STORAGE` | `./graphics` | Where graphics live on disk (relative to the process's working directory) |
| `RUST_LOG` | `info` | Log level |
| `OGRAF_ACTION_TIMEOUT_MS` | `5000` | How long an HTTP action call waits for the renderer's confirmation before failing |
| `OGRAF_GRAPHICS_CACHE_TTL_SECS` | `30` | Graphics list cache TTL in seconds (0 = disabled, always fetch fresh) |
| `OGRAF_RENDERER_MAX_PENDING` | `100` | Maximum pending requests per renderer (DoS protection); half of it makes the renderer's status `WARNING` |
| `OGRAF_DELETED_GRAPHIC_RETENTION_SECS` | `86400` | How long a graphic deleted without `force` keeps its files for on-air instances |

## Where graphics come from

`GraphicStore` (`src/store/graphics.rs`) scans `OGRAF_STORAGE` on disk and
caches the result for `OGRAF_GRAPHICS_CACHE_TTL_SECS` (default: 30 seconds).
Set TTL to `0` to disable caching and always fetch fresh from disk.

Any immediate subdirectory containing a `*.ograf.json` manifest is treated
as one graphic; the subdirectory name becomes its `graphicId`. Writing (or
deleting) that directory is entirely the consumer's job — `ograf-core` only
ever reads.

**Note:** With caching enabled, new/deleted graphics may not appear immediately
(up to TTL delay). For development environments where graphics change frequently,
consider `OGRAF_GRAPHICS_CACHE_TTL_SECS=0`.

## API surface

- `GET /ograf/v1/` — server info (name, description, author, version from Core's `Cargo.toml`)
- `GET /ograf/v1/health` — health check (always returns 200 OK, no auth required)
- `GET /ograf/v1/graphics`, `GET /ograf/v1/graphics/:id` — list/inspect graphics
- `DELETE /ograf/v1/graphics/:id?force=` — unlist a graphic (its files stay for on-air instances until the retention ends), or remove it at once with `force=true`
- `GET /ograf/v1/graphics/:id/assets/*path`, `GET /ograf/v1/graphics/:id/thumbnail` — serve graphic assets
- `GET /ograf/v1/renderers`, `GET /ograf/v1/renderers/:id`, `GET /ograf/v1/renderers/:id/target` — list/inspect renderers, each with the spec's `status` (`OK`/`WARNING`/`ERROR`) — see [SPEC_COMPLIANCE.md]SPEC_COMPLIANCE.md#renderer-status
- `PUT /ograf/v1/renderers/:id/target/graphicInstance/{load,clear}`
- `POST /ograf/v1/renderers/:id/target/graphicInstance/{playAction,stopAction,updateAction}`
- `POST /ograf/v1/renderers/:id/target/graphicInstance/customActions/:actionId`
- `POST /ograf/v1/renderers/:id/customActions/:actionId` — renderer-scoped custom action
- `GET /rendererApi/v1/connect` — renderer WebSocket upgrade (`RENDERER_CONNECT_PATH`; outside `/ograf/v1`, since the renderer protocol isn't part of the Server API)

## OGraf v1 Spec Compliance

`ograf-core` implements the complete [OGraf v1 Server API specification](https://ograf.ebu.io/). All endpoints, WebSocket messages, and behaviors match the official spec (see [SPEC_COMPLIANCE.md](SPEC_COMPLIANCE.md) for verification details).

### Non-breaking Extensions

These additions enhance observability and functionality without breaking compatibility with spec-compliant clients or renderers:

#### 1. Instance State Tracking
The `GET /renderers/:id/target` response includes extra fields for each `GraphicInstance`:
- `state` — Current instance state: `loaded`, `playing`, or `stopped`
- `currentStep` — Last reported step from `playAction` (persisted between actions)
- `data` — Last confirmed data from the renderer

**Rationale**: Helps dashboards and UIs display more than "a graphic is loaded here" without requiring clients to track state themselves.

#### 2. Inline Renderer Metrics (v0.3.0+)
The `GET /renderers/:id` response includes an optional `metrics` field with:
- `pendingRequests` — Number of in-flight renderer requests
- `messagesSent` / `messagesReceived` — Cumulative WebSocket message counts
- `uptimeSeconds` — Renderer connection uptime

**Rationale**: Observability without requiring a separate metrics endpoint.

#### 3. Renderer Reconnect State Resync (v0.3.0+)
Renderers can include an optional `instances` array in their `Hello` message to resync Core's view of loaded instances after reconnect. Each instance snapshot includes `instanceId`, `graphicId`, `data`, and `currentStep`.

**Rationale**: Enables seamless renderer reconnect after network blips or Core restarts without losing instance state.

#### 4. AccessControl Extensions (v0.3.0+)
The `AccessControl` trait includes optional methods for fine-grained access control:
- `filter_graphics()` — Graphics-level visibility control for `GET /graphics`
- `can_load_graphic()` — Per-graphic load authorization checked before `load()`

Both have default implementations that allow all access (maintaining backward compatibility). Custom implementations can enforce zone/role-based restrictions.

**Rationale**: Enables multi-tenant deployments with graphics scoped to specific zones or renderers.

#### 5. Lenient `currentStep` Parsing
If a renderer's `playActionResult` contains a non-numeric `currentStep`, it defaults to `0.0` instead of rejecting the entire message.

**Rationale**: Prevents timeout/failure when a template returns unexpected values. The action still succeeds; only this one field degrades gracefully.

#### 6. Health Check Endpoint (v0.3.0+)
`GET /ograf/v1/health` always returns `{"status": "ok"}` with 200 OK. No authentication required.

**Rationale**: Standard endpoint for load balancers and orchestrators to check service health.

**Compatibility**: Clients can safely ignore all extra fields. Renderers that don't send `instances` in Hello behave identically to v0.2.x. See [SPEC_COMPLIANCE.md](SPEC_COMPLIANCE.md) for full details.

## Status

**Early-stage (0.4.0)** — Spec-compliant but not yet battle-tested in production.

This is a library implementation of the OGraf v1 spec. Consumers implement their own access control via the `AccessControl` trait.

**Use in production:** Possible, but be aware this is a new implementation without significant production usage. Test thoroughly in your environment before deploying.

## Learn More About OGraf

- **[OGraf Specification]https://ograf.ebu.io/** — Official EBU specification

Questions or feedback? [Open an issue](https://github.com/HeineFro/ograf-core/issues) on GitHub or write a pm.

## License

Dual-licensed under either of

- [MIT license]LICENSE-MIT
- [Apache License, Version 2.0]LICENSE-APACHE

at your option.