# Plan: OS API Usage Counting & Progress Bar

**Created:** 2026-05-28  
**Status:** Planning

---

## Background

Ordnance Survey's OS Data Hub does not expose a programmatic API for querying usage statistics or monthly quota. Usage is only visible via the web dashboard at `osdatahub.os.uk/dashboard`. There are no response headers returning running totals either.

To display a usage progress bar within GeoLynx, we must track API transactions ourselves, locally.

---

## Goals

- Track OS API transactions per client project (one OS API key per client)
- Convert raw counts to OS billing units (map views / feature batches) and calculate cost in £
- Display a progress bar showing monthly spend (£) against a configurable monthly budget (£)
- Cover both tile requests (client-side, OpenLayers) and future feature API calls (server-side, PHP)
- Track GeoLynx overage charges separately from OS cost to support client billing

---

## Billing Rates

OS charges GeoLynx per billing unit. GeoLynx charges clients at a margin. All cost calculations use these constants (hardcoded in PHP).

### Tile-to-Map-View Conversion

Raw tile counts from OpenLayers must be converted to **map views** before cost is calculated.

| API / Layer type | Tiles per map view |
|---|---|
| OS Maps API — Leisure (1:25k / 1:50k) | 15 |
| OS Maps API — Premium (MasterMap) | 15 |
| OS NGD API – Tiles | 4 |
| OS Vector Tile API | 4 |

### Rates

| `api_type` | Billing unit | OS cost | GeoLynx charge to client |
|---|---|---|---|
| `premium_tiles` | per map view | £0.0331 | £0.040 |
| `leisure_tiles` | per map view | £0.000525 | £0.00063 |
| `features` | per 100-feature batch | £0.19 | £0.23 |
| `addresses` | per 100-address batch | £0.013 | £0.016 |

> OS Names API and OS Linked Identifiers API are free — no cost tracking needed for those.

---

## Transaction Sources

### 1. Map Tile Requests (Client-Side)

- **Library:** OpenLayers (`/www/lib/ol/`)
- **Mechanism:** OpenLayers tile sources emit `tileloadstart`, `tileloadend`, and `tileloaderror` events per source/layer
- **Count on:** `tileloadstart` — this is when the request is made to OS servers, regardless of outcome
- **`api_type` must be known per layer** — the JS layer setup must pass the correct type when flushing so PHP can apply the right tile-to-view ratio and rates
- **Approach:** Hook into the OS tile source on the map, batch count in JS, and periodically POST raw tile count + `api_type` to a PHP endpoint. PHP converts to map views and calculates cost.

```js
const osSource = osLayer.getSource();
let pendingTileCount = 0;

osSource.on('tileloadstart', function () {
    pendingTileCount++;
});

// Flush to server periodically (e.g. every 30 seconds or on map moveend)
function flushTileCount() {
    if (pendingTileCount === 0) return;
    const count = pendingTileCount;
    pendingTileCount = 0;
    $.post('/fn/os_usage_save.php', {
        api_key: currentProjectApiKey,
        api_type: 'premium_tiles', // or 'leisure_tiles' — set per layer config
        raw_tile_count: count
    });
}
```

### 2. Feature / Data API Calls (Server-Side)

- **Mechanism:** All future OS Features, Places, Names, etc. calls will be proxied through PHP backend endpoints
- **`api_type`** set per endpoint: `features` for OS Features/NGD Features, `addresses` for OS Places
- **Approach:** Log each call inline within the PHP wrapper, passing `api_type` and batch size to `os_usage_save.php`

---

## Database

All tables live in the `public` schema.

```sql
-- Per OS Data Hub project config
CREATE TABLE public.os_api_project_config (
    id              SERIAL PRIMARY KEY,
    api_key         TEXT NOT NULL UNIQUE,
    monthly_budget  NUMERIC(10,4) NOT NULL,  -- £ budget per month (GeoLynx client charge basis)
    key_source      TEXT NOT NULL DEFAULT 'geolynx'  -- 'geolynx' | 'client' | 'central'
);
-- key_source = 'geolynx':  key and budget are admin-only, client cannot modify
-- key_source = 'client':   client entered their own key and budget; admin can override
-- key_source = 'central':  set by the future central management system; read-only locally

-- Usage log
CREATE TABLE public.os_api_usage (
    id                SERIAL PRIMARY KEY,
    api_key           TEXT NOT NULL,
    api_type          TEXT NOT NULL,          -- 'premium_tiles' | 'leisure_tiles' | 'features' | 'addresses'
    billing_units     NUMERIC(10,2) NOT NULL, -- map views (tiles) or batches (features/addresses)
    os_cost           NUMERIC(10,6) NOT NULL, -- £ cost to GeoLynx from OS
    client_cost       NUMERIC(10,6) NOT NULL, -- £ charge to client (GeoLynx overage rate)
    recorded_at       TIMESTAMPTZ NOT NULL DEFAULT NOW()
);

CREATE INDEX ON public.os_api_usage (api_key, recorded_at);
```

- `api_type` is required — needed to apply correct tile-to-view ratio and rate
- `billing_units` stores map views (not raw tile count) for tile types; batches for feature/address types
- `os_cost` and `client_cost` calculated at insert time in PHP using hardcoded rate constants
- Monthly cost totals: `SUM(client_cost) WHERE date_trunc('month', recorded_at) = date_trunc('month', NOW())`
- Budget (`monthly_budget`) is in £ — progress bar shows `SUM(client_cost)` vs `monthly_budget`

---

## PHP Endpoints

Following the existing `www/fn/` pattern:

| File | Purpose |
|---|---|
| `www/fn/os_usage_save.php` | Accepts POST of `api_key`, `api_type`, `raw_tile_count` (tiles) or `batch_count` (features/addresses); converts to billing units, calculates `os_cost` + `client_cost`, inserts into `public.os_api_usage` |
| `www/fn/os_usage_load.php` | Returns monthly `SUM(client_cost)`, `SUM(os_cost)`, and `monthly_budget` for a given `api_key` |
| `www/fn/os_config_load.php` | Returns all rows from `os_api_project_config` for the admin panel |
| `www/fn/os_config_save.php` | Insert or update a row in `os_api_project_config` (upsert on `api_key`) |
| `www/fn/os_config_delete.php` | Deletes a row from `os_api_project_config` by `id` |

---

## UI — Progress Bar Widget

- Visible to all users (including end users/clients)
- Displays current month's **£ client cost** (`SUM(client_cost)`) vs. the configured **£ monthly budget**
- Label shows e.g. `£4.23 of £50.00 used this month`
- Colour thresholds hardcoded as constants:
  - `success` — 0–79%
  - `warning` — 80–99%
  - `danger` — 100%+
- Admin view may additionally show `SUM(os_cost)` alongside `SUM(client_cost)` to show GeoLynx margin
- Widget embedded wherever relevant (project view, account view, or dedicated admin usage panel)

---

## Polling / Refresh Strategy

- Tile counts flushed to server: every 30 seconds **or** on map `moveend`/idle event (whichever is less aggressive)
- Progress bar refreshed: on page load + after each tile flush
- No continuous polling needed — tile flush triggers a bar refresh

---

## Admin Panel

A dedicated admin section for managing `os_api_project_config` entries — adding, editing, and deleting OS Data Hub API project keys and their monthly budgets.

### Route

New route following the existing `?do=` pattern:

| Route | Purpose |
|---|---|
| `?do=os_config` | Admin panel for OS API project configuration |

### Files

| File | Purpose |
|---|---|
| `www/html/html_body_os_config.php` | Page shell / HTML template |
| `www/js/os_config.js` | Page JS — table, modals, save/delete interactions |
| `www/css/os_config.css` | Page-specific styles (if needed) |

### UI Behaviour

- Tabulator.js table listing all configured API keys, their monthly budgets (£), and key source
- **Add** — modal form with fields: `api_key`, `monthly_budget` (£), `key_source` (dropdown: GeoLynx / Client)
- **Edit** — admin can edit `monthly_budget` for any entry regardless of `key_source`; API key not editable once set — delete and re-add instead
- **Delete** — confirmation prompt before removing a config entry
- Admin-only route — access controlled via existing `login_check.php` auth

---

## Client Settings — BYOK (Bring Your Own Key)

For clients who supply their own OS Data Hub API key, a settings interface is provided within the normal GeoLynx UI (not the admin panel).

### Access Rules

| Field | GeoLynx key | Client key |
|---|---|---|
| View API key | ✗ | ✓ |
| Edit API key | ✗ | ✓ |
| Edit monthly budget (£) | ✗ | ✓ |
| Admin override (budget) | ✓ | ✓ |

### UI Behaviour

- Client can enter or update their own `api_key` and `monthly_budget`
- On save, `key_source` is set to `'client'`
- If `key_source = 'geolynx'`, the settings fields are rendered as read-only / hidden — client cannot interact with them
- Admin override: admin can edit the budget of a client-supplied key via the admin panel regardless of `key_source`

### Files

| File | Purpose |
|---|---|
| `www/fn/os_config_client_save.php` | Client-facing save — only permitted if `key_source = 'client'` or no key yet configured; sets `key_source = 'client'` |

---

## Future Considerations — Central Management System

> Out of scope for this plan. Captured here to ensure current architecture does not block future implementation.

The intention is for a central GeoLynx management server to exist, from which individual client instances can be managed (access revocation, OS usage limits, etc.). Each client instance will periodically **call back to the central server's API** using an instance-level auth key to check for status and config updates. The central server does not connect to client databases directly.

### Impact on this plan

- `os_api_project_config` becomes a **local cache** of centrally-managed config, refreshed on each call-back
- A third `key_source` value of `'central'` should be reserved for values pushed from the central server — these would be read-only in the local admin panel
- A `call-back` mechanism (cron or on-request) will need to be added to each instance to fetch and apply updates from the central API
- Access revocation (e.g. non-payment) would be returned as part of the central API response and acted on by the instance

### Current plan compatibility

The current schema and architecture is compatible with this future model. No rework anticipated — `os_api_project_config` is a single, clean table that can be written to from either the local admin panel or a central API response.

---

## Decisions Log

| Question | Decision |
|---|---|
| Which schema? | `public` schema |
| Where is budget stored? | Separate `public.os_api_project_config` table — admin-managed, not user-facing |
| Who sees the progress bar? | All users, including end users/clients |
| Per-type breakdown or combined? | Combined £ total for display; `api_type` tracked internally for correct rate calculation |
| Tile counting scope? | All pages where the OS layer is loaded |
