Skip to content

Instantly share code, notes, and snippets.

@pliablepixels
Last active January 31, 2026 11:14
Show Gist options
  • Select an option

  • Save pliablepixels/20fac1262c46ef6711361ec08ffaceac to your computer and use it in GitHub Desktop.

Select an option

Save pliablepixels/20fac1262c46ef6711361ec08ffaceac to your computer and use it in GitHub Desktop.
v3 api migration for zmNg

API V3 Migration Review

1. Architecture Overview

Current API (v1/CakePHP)

  • Base path: /api/ with .json suffix on all endpoints
  • Auth: Token passed as query parameter (?token=xxx)
  • Request format: Form-encoded POST bodies (Monitor[Function]=Modect)
  • Response wrapping: Objects nested inside { monitors: [{ Monitor: {...}, Monitor_Status: {...} }] }
  • Naming: PascalCase field names (MonitorId, StartDateTime, CaptureFPS)
  • IDs: Returned as strings, coerced to numbers client-side via Zod

New API (v3/Rust)

  • Base path: /api/v3/ with no suffix
  • Auth: JWT Bearer token in Authorization header
  • Request format: JSON bodies
  • Response wrapping: Flat objects, no nested wrappers ({ id, name, ... } directly)
  • Naming: snake_case field names (monitor_id, start_date_time, capture_fps)
  • IDs: Native integers

2. Endpoint Mapping (what zmNg uses today → v3 equivalent)

zmNg Function Current Endpoint V3 Endpoint Notes
Login POST /host/login.json (form-encoded) POST /api/v3/auth/login (JSON) V3 uses JSON body, returns TokenResponse with token_type, expire_in (single int, not separate access/refresh expires). V3 login response is a oneOf (Token or Code for 2FA).
Refresh Token POST /host/login.json (with refresh token) POST /api/v3/auth/refresh (JSON {token}) Dedicated refresh endpoint in v3
Logout N/A (zmNg doesn't call logout) GET /api/v3/auth/logout New capability
Get Version GET /host/getVersion.json GET /api/v3/host/getVersion V3 adds db_version field
Get Timezone GET /host/getTimeZone.json MISSING No timezone endpoint in v3 spec
List Monitors GET /monitors.json GET /api/v3/monitors V3 returns flat MonitorResponse[], no Monitor_Status inline
Get Monitor GET /monitors/{id}.json GET /api/v3/monitors/{id} Same
Update Monitor POST /monitors/{id}.json (form: Monitor[key]=val) PATCH /api/v3/monitors/{id} (JSON) Method changes POST→PATCH, format changes form→JSON
Monitor Status Embedded in monitor response (Monitor_Status) GET /api/v3/monitor-status or /api/v3/monitor-status/{monitor_id} Separate endpoint in v3
Alarm On/Off/Status GET /monitors/alarm/id:{id}/command:{cmd}.json PATCH /api/v3/monitors/{id}/alarm (JSON body AlarmControlRequest) GET→PATCH, command in URL→body
Daemon Status (zmc/zma) GET /monitors/daemonStatus/id:{id}/daemon:{name}.json GET /api/v3/daemons or /api/v3/daemons/{id} Different model: v3 lists all daemons centrally, no per-monitor daemon query
PTZ Controls GET /controls/{id}.json GET /api/v3/controls/{id} Same concept
List Events GET /events/index.json (CakePHP query params) GET /api/v3/events (query params: monitor_id, start_time, end_time, page, page_size) Simplified query params; no sort/direction in v3 spec
Get Event GET /events/{id}.json GET /api/v3/events/{id} Same
Update Event PUT /events/{id}.json (form: Event[Archived]=1) PATCH /api/v3/events/{id} (JSON EventUpdateRequest) PUT→PATCH, limited fields in v3 (name, cause, notes, orientation - no Archived)
Delete Event DELETE /events/{id}.json DELETE /api/v3/events/{id} Same
Console Events GET /events/consoleEvents/{interval}.json GET /api/v3/events/counts/{hours} Returns EventCountsResponse with {count, date}[] instead of Record<MonitorId, count>
List States GET /states.json GET /api/v3/states Same
Change State POST /states/change/{name}.json POST /api/v3/states/change/{action} Same pattern
List Tags GET /tags.json GET /api/v3/tags V3 adds event_count field
Tags for Events GET /tags/index/Events.Id:{id1},{id2}.json GET /api/v3/events-tags Different model: v3 uses separate events-tags resource
List Zones GET /zones.json?MonitorId={id} GET /api/v3/monitors/{id}/zones Nested under monitor in v3
List Groups GET /groups.json GET /api/v3/groups + GET /api/v3/groups-monitors V3 separates group-monitor mapping into its own resource
List Servers GET /servers.json GET /api/v3/servers V3 ServerResponse is simpler (no CpuLoad, TotalMem, FreeMem inline)
Daemon Check GET /host/daemonCheck.json GET /api/v3/system/status V3 returns SystemStatusResponse with running boolean + daemon list + stats
Server Load GET /host/getLoad.json GET /api/v3/system/statusstats.cpu_load Folded into system status
Disk Usage GET /host/getDiskPercent.json MISSING No disk usage endpoint in v3 spec
List Configs GET /configs.json GET /api/v3/configs Same
Get Config By Name GET /configs/viewByName/{name}.json GET /api/v3/configs/{name} Same, by name
List Logs GET /logs.json GET /api/v3/logs V3 LogResponse drops TimeKey, ServerId, Pid, File, Line fields
Go2RTC/ZMS paths Config lookups (ZM_GO2RTC_PATH, ZM_PATH_ZMS) GET /api/v3/streams/{id} returns StreamEndpoints V3 has a streaming registry with WebRTC/HLS/MJPEG endpoints per stream

3. Missing Features in V3

These are capabilities zmNg currently uses that have no direct equivalent in the v3 spec:

  1. Timezone endpointGET /host/getTimeZone.json has no v3 equivalent. zmNg uses this during bootstrap to sync server time. Would need to be added to v3 or obtained from system status.

  2. Disk usageGET /host/getDiskPercent.json has no v3 equivalent. zmNg shows this on the dashboard.

  3. Event archive/unarchive — The v3 EventUpdateRequest only supports name, cause, notes, orientation. No archived field. zmNg uses Event[Archived]=1|0 for archive toggling.

  4. Event sorting/filtering — V3 EventQueryParams supports monitor_id, start_time, end_time, page, page_size. Missing: AlarmFrames filter (min alarm frames), sort field, direction (asc/desc). zmNg uses all of these.

  5. Console events per monitor — Current API returns Record<MonitorId, EventCount>. V3 returns {count, date}[] — appears to be aggregate counts by date, not per-monitor breakdown. zmNg depends on per-monitor counts for the console view.

  6. Log filtering by component — Current: GET /logs/index/Component:{name}.json. V3: no component filter parameter visible in the spec.

  7. Log pagination — V3 log endpoint doesn't show pagination params in the spec.

  8. Log detail fields — V3 LogResponse drops TimeKey, ServerId, Pid, File, Line that zmNg currently displays.

  9. Server stats inline — Current ServerResponse includes CpuLoad, TotalMem, FreeMem, State_Id. V3 ServerResponse only has id, name, hostname, port, status. Server stats are a separate resource (/api/v3/server-stats).

  10. ZMS streaming URLs — The legacy CGI-based streaming (nph-zms) has no direct v3 equivalent. V3 replaces this with MSE segments + WebRTC + HLS + MJPEG via /api/v3/streams/{id} and /api/v3/mse/streams/{camera_id}. This is a fundamental streaming architecture change.

  11. PTZ via index.php — Current PTZ sends commands via GET /index.php?view=request&request=control&id=X&control=cmd. No equivalent in v3 spec (v3 has controls CRUD but no PTZ command dispatch endpoint).

  12. Event image/video URLs — Current image/video URLs go through index.php (view=image, view=view_video). V3 has no equivalent — this will likely need a different streaming/media approach.


4. Key Differences Requiring Adaptation

4a. Authentication

Aspect Current V3
Token delivery Query param ?token=xxx Authorization: Bearer xxx header
Login format Form-encoded JSON
Refresh Reuses login endpoint Dedicated /auth/refresh
Token response access_token, access_token_expires, refresh_token, refresh_token_expires, version, apiversion token_type, access_token, refresh_token, expire_in (single expiry)
2FA N/A Supported via LoginResponse.Code variant
Logout N/A GET /auth/logout

4b. Response Structure

  • Current: { monitors: [{ Monitor: {...}, Monitor_Status: {...} }] } — nested with PascalCase
  • V3: [{ id, name, ... }] — flat arrays with snake_case

4c. Data Mutations

  • Current: Form-encoded (Monitor[Function]=Modect)
  • V3: JSON bodies with PATCH method

4d. Monitor Status

  • Current: Bundled with monitor response
  • V3: Separate /api/v3/monitor-status/{monitor_id} endpoint

4e. Streaming

  • Current: ZMS CGI (nph-zms) + Go2RTC (optional)
  • V3: First-class MSE/WebRTC/HLS/MJPEG via /api/v3/streams and /api/v3/mse/streams

5. Migration Plan — API Adaptation Layer

Architecture

┌─────────────────────────────┐
│  App Logic (hooks, stores,  │
│  components)                │  ← No changes needed
├─────────────────────────────┤
│  API Adapter Interface      │  ← New: defines the contract
│  (app/src/api/adapter.ts)   │
├──────────┬──────────────────┤
│ V1 Impl  │    V3 Impl       │  ← Two implementations
│ (legacy) │  (new)           │
└──────────┴──────────────────┘

Step 1: Define the Adapter Interface

Create app/src/api/adapter.ts that defines an ApiAdapter interface matching the function signatures zmNg already uses internally. This interface represents the application's data needs, not any specific API version.

interface ApiAdapter {
  // Auth
  login(username: string, password: string): Promise<LoginResult>;
  refreshToken(refreshToken: string): Promise<LoginResult>;
  getVersion(): Promise<VersionResult>;
  getTimezone(): Promise<string | null>;

  // Monitors
  getMonitors(): Promise<MonitorData[]>;
  getMonitor(id: string): Promise<MonitorData>;
  updateMonitor(id: string, changes: Record<string, unknown>): Promise<void>;
  getMonitorStatus(id: string): Promise<MonitorStatus>;
  triggerAlarm(id: string): Promise<void>;
  cancelAlarm(id: string): Promise<void>;
  getAlarmStatus(id: string): Promise<AlarmStatus>;
  getDaemonStatus(id: string, daemon: string): Promise<DaemonStatus>;

  // Events
  getEvents(params: EventQueryParams): Promise<EventsResult>;
  getEvent(id: string): Promise<Event>;
  updateEvent(id: string, changes: Record<string, unknown>): Promise<void>;
  deleteEvent(id: string): Promise<void>;
  getConsoleEvents(interval: string): Promise<Record<string, number>>;

  // States, Groups, Tags, Zones, Servers, Configs, Logs
  // ... (same pattern)

  // Server health
  daemonCheck(): Promise<boolean>;
  getServerLoad(): Promise<number[]>;
  getDiskUsage(): Promise<{ usage: number; percent: number } | null>;

  // Streaming URL construction
  getStreamEndpoints(monitorId: string): Promise<StreamEndpoints>;
}

The return types here are the internal types that zmNg already uses (or close to them). Each adapter implementation transforms the API-specific response into these types.

Step 2: Extract Current Code into V1 Adapter

Move the current API logic from app/src/api/auth.ts, monitors.ts, events.ts, etc. into a v1/ subfolder that implements the ApiAdapter interface.

app/src/api/
  adapter.ts          ← Interface + factory
  types-internal.ts   ← Internal types (renamed from current types.ts)
  v1/
    auth.ts           ← Current auth.ts logic
    monitors.ts       ← Current monitors.ts logic
    events.ts         ← Current events.ts logic
    ...
    types.ts          ← V1-specific Zod schemas (current types.ts schemas)
    index.ts          ← V1 adapter factory
  v3/
    auth.ts           ← V3 auth implementation
    monitors.ts       ← V3 monitors implementation
    events.ts         ← V3 events implementation
    ...
    types.ts          ← V3-specific Zod schemas
    index.ts          ← V3 adapter factory
  client.ts           ← Shared (needs minor changes for Bearer auth in v3)
  index.ts            ← Exports active adapter based on detected version

Step 3: Update the API Client for V3 Auth

The client.ts needs to support two auth modes:

  • V1: Append ?token=xxx to query params
  • V3: Set Authorization: Bearer xxx header

Add a flag to ApiClient config or detect based on the active adapter.

Step 4: Implement V3 Adapter

For each module, implement the adapter interface:

  • Auth: JSON body, dedicated refresh endpoint, handle oneOf login response (Token vs Code for 2FA)
  • Monitors: Map snake_casePascalCase for internal types; fetch status separately from /monitor-status/{id}
  • Events: Map query params, handle simplified pagination, map snake_case response
  • Console Events: Transform {count, date}[] into Record<MonitorId, count> if the v3 endpoint supports per-monitor breakdown (needs clarification)
  • Groups: Combine /groups + /groups-monitors into the current grouped structure
  • Tags: Map /events-tags to the current batched tag lookup pattern
  • Server/System: Combine /system/status for daemon check, load, and daemon statuses
  • Streaming: Map /streams/{id} endpoints to the URL builder's expectations

Step 5: Version Detection and Adapter Selection

During bootstrap/discovery:

  1. Try GET /api/v3/host/getVersion — if it returns successfully, use V3 adapter
  2. Fall back to GET /api/host/getVersion.json — use V1 adapter
  3. Store the detected version, create the appropriate adapter instance
  4. Pass the adapter to the app via context or store

Step 6: Handle Missing V3 Features

For features missing from v3, the V3 adapter should:

Missing Feature Strategy
Timezone Return Intl.DateTimeFormat().resolvedOptions().timeZone as fallback, or request it be added to v3
Disk usage Return null/unavailable, hide UI element, or request endpoint be added
Event archive Check if v3 adds this field later; for now, disable archive UI when on v3
Event sort/filter Use client-side sorting if v3 doesn't support it server-side
Console events per monitor Fetch all events and aggregate client-side, or request v3 add per-monitor grouping
ZMS streaming Not needed if v3 MSE/WebRTC covers all cases; fall back to v3 streaming endpoints
PTZ dispatch Request endpoint be added to v3 spec, or use controls API differently
Event image/video URLs Use v3 streaming endpoints or request media URL construction be documented
Log detail fields Display only available fields; gracefully degrade UI

Step 7: Update URL Builder

The url-builder.ts constructs ZMS, image, and video URLs from base URL + token query params. For v3:

  • Stream URLs come from the /streams/{id} endpoint (StreamEndpoints with webrtc, hls, mjpeg URLs)
  • Image URLs may need a new v3 endpoint or convention
  • Token is no longer a query param — streaming endpoints likely use cookies or the JWT is embedded differently

Create a StreamUrlResolver interface with V1 and V3 implementations.

Step 8: Incremental Migration

The work can be done incrementally per module:

  1. Auth (enables all other work)
  2. Monitors + Monitor Status (most visible)
  3. Events
  4. Server/System status
  5. Groups, Tags, Zones, States, Configs, Logs
  6. Streaming (largest change)

Each module can be migrated independently since the adapter interface keeps the app logic stable.


6. Risks and Open Questions

  1. Console events format change — The v3 EventCountsResponse returns {count, date}[] which doesn't match the current per-monitor breakdown. Needs clarification: does v3 support per-monitor filtering via query params?

  2. Streaming architecture — The move from ZMS CGI to MSE/WebRTC is not just an API change — it may require changes to the video player components. The adapter layer can abstract URL construction, but the playback mechanism itself may differ.

  3. Event image/video access — No clear v3 equivalent for index.php?view=image or view=view_video. This is a gap that needs to be addressed in the v3 spec or via a different media serving approach.

  4. 2FA support — V3's LoginResponse can return a Code variant for 2FA. zmNg has no 2FA UI currently. The adapter should handle this gracefully (pass through or error).

  5. Permissions model — V3 adds groups-permissions and monitors-permissions which don't exist in v1. The adapter interface should be designed to accommodate these later.

  6. Single expire_in vs separate access/refresh expiry — V3 TokenResponse has one expire_in. Need to determine if this applies to the access token only (likely) and what the refresh token lifetime is.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment