Public API

Auto refresh: ON

Public API Documentation

Source: PUBLIC_API.md
Ready
Protected API tests use the server default protected scope; internal CRUD APIs fall back to the default server scope when needed.
Core System APIs
-
-
-
-
-
-
-
-
-
Ricoh Machine Control
-
-
-
-
Scan Folder Queue
Public route: POST /api/devices/<mac_id>/scan-folder. Create/update/delete affects a live agent, so these buttons are manual only and are excluded from Test All APIs.
-
-
-
Camera Recording Control
-
API For Database (CRUD)
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
Scan Email Destinations
-
-
-
VPS Scan Folders
-
-
Agent PC Settings & Utilities
-
-
-
-
-
Click Test to run API check...

Full API Reference

This includes both the core polling APIs and the new database CRUD endpoints.

# Public API for CRM

Base URL (production):
- `https://agentapi.quanlymay.com`

These endpoints are read-only and return JSON.

Note:
- `POST /api/login` and `POST /api/login/google` now include `workspaceIds` in the returned user object.
- User-to-workspace membership is stored in the `UserWorkspace` table.
- Clients no longer need to send any tenant or scope parameter to the API. Protected endpoints resolve request scope from `X-API-Token`; internal CRUD endpoints fall back to the default server scope when needed.
- Table-backed GET/list responses expose audit fields as `created_at`, `updated_at`, and camelCase aliases `createAt`, `updateAt`.

## 1) Machine list
- Method: `GET`
- Path: `/machinelist/`
- Query params (optional):
  - `lan_uid`: filter by LAN id

Example:
```bash
curl -s "https://agentapi.quanlymay.com/machinelist/"
```

Response:
```json
{
  "ok": true,
  "count": 2,
  "machines": [
    {
      "lead": "default",
      "lan_uid": "lanf-33ef2446897e0a57",
      "mac_id": "00:26:73:7D:78:F9",
      "agent_uid": "agent-pc-01",
      "printer_name": "Aficio MP 9002",
      "ip": "192.168.1.224",
      "counter_total": 3653272,
      "system_status": "Status OK",
      "toner_black": {
        "state": "Status OK"
      },
      "created_at": "2026-03-01T16:00:00+00:00",
      "updated_at": "2026-03-02T00:00:00+00:00",
      "last_counter_at": "2026-03-02T00:00:00+00:00",
      "last_status_at": "2026-03-02T00:00:00+00:00"
    }
  ]
}
```

## 2) Network list
- Method: `GET`
- Path: `/networklist/`

Example:
```bash
curl -s "https://agentapi.quanlymay.com/networklist/"
```

Response:
```json
{
  "ok": true,
  "count": 1,
  "networks": [
    {
      "lead": "default",
      "lan_uid": "lanf-33ef2446897e0a57",
      "machine_count": 2,
      "last_seen_at": "2026-03-02T00:00:00+00:00"
    }
  ]
}
```

## 3) Existing infor list (kept as-is)
- Method: `GET`
- Path: `/api/infor/list`

Example:
```bash
curl -s "https://agentapi.quanlymay.com/api/infor/list"
```

Notes:
- The `counter_data` and `status_data` fields are JSON objects.
- `mac_id` is normalized in `AA:BB:CC:DD:EE:FF` format.

## 4) Get device infor by MAC ID
- Method: `GET`
- Path: `/api/public/device/by-mac`
- Query params (required):
  - `mac_id` (or `mac`) – MAC address, any format: `00:26:73:7D:78:F9`, `00-26-73-7D-78-F9`, or `0026737D78F9`

Example:
```bash
curl -s "https://agentapi.quanlymay.com/api/public/device/by-mac?mac_id=0026737D78F9"
```

Notes:
- Success responses always normalize `mac_id` to `AA:BB:CC:DD:EE:FF`.
- Invalid MAC format returns `400`.
- Unknown device returns `404`.

Response:
```json
{
  "ok": true,
  "mac_id": "00:26:73:7D:78:F9",
  "lead": "default",
  "lan_uid": "lanf-33ef2446897e0a57",
  "agent_uid": "agent-pc-01",
  "printer_name": "Aficio MP 9002",
  "ip": "192.168.1.224",
  "counter": {
    "total": "3653272"
  },
  "ok": true,
  "mac_id": "00:26:73:7D:78:F9",
  "lead": "default",
  "lan_uid": "lanf-33ef2446897e0a57",
  "agent_uid": "agent-pc-01",
  "printer_name": "Aficio MP 9002",
  "ip": "192.168.1.224",
  "counter": {
    "total": "3653272"
  },
  "status": {
    "system_status": "Status OK"
  },
  "last_counter_at": "2026-03-02T00:00:00+00:00",
  "last_status_at": "2026-03-02T00:00:00+00:00",
  "updated_at": "2026-03-02T00:00:00+00:00"
}
```

## 4b) Query real-time device infor by MAC ID (Force fresh check)
- Method: `GET`
- Path: `/api/public/device/by-mac-now`
- Query params (required):
  - `mac_id` (or `mac`) – MAC address, any format: `00:26:73:7D:78:F9`, `00-26-73-7D-78-F9`, or `0026737D78F9`

Example:
```bash
curl -s "https://agentapi.quanlymay.com/api/public/device/by-mac-now?mac_id=0026737D78F9"
```

Notes:
- Unlike `/api/public/device/by-mac` which returns cached database values, this endpoint actively triggers an on-demand SNMP/status query command on the managing Agent.
- The request blocks (long-polls) and waits up to 10 seconds for the Agent response.
- Success responses contain fresh, real-time counters and status.
- Automatically updates the database cache for subsequent passive queries.
- Returns `504` on Agent communication timeout, or `400` if the managing Agent is offline.

Response: Same format as `/api/public/device/by-mac` but with real-time data.

## 5) Check online status by MAC ID
- Method: `GET`
- Path: `/api/public/device/online-status`
- Query params:
  - `mac_id` (required) – MAC address, any format: `00:26:73:7D:78:F9`, `00-26-73-7D-78-F9`, or `0026737D78F9`
  - `stale_seconds` (optional) – seconds without polling before considered offline (default: `300`)

Example:
```bash
curl -s "https://agentapi.quanlymay.com/api/public/device/online-status?mac_id=00-26-73-7D-78-F9"
```

Response (online):
```json
{
  "ok": true,
  "mac_id": "00:26:73:7D:78:F9",
  "is_online": true,
  "printer_name": "Aficio MP 9002",
  "ip": "192.168.1.224",
  "lead": "default",
  "lan_uid": "lanf-33ef2446897e0a57",
  "last_seen_at": "2026-03-21T05:00:00+00:00",
  "seconds_since_seen": 42,
  "stale_threshold_seconds": 300,
  "online_source": "polling",
  "is_online_by_polling": true,
  "is_online_by_flag": true
}
```

Response (offline):
```json
{
  "ok": true,
  "mac_id": "00:26:73:7D:78:F9",
  "is_online": false,
  "printer_name": "Aficio MP 9002",
  "ip": "192.168.1.224",
  "lead": "default",
  "lan_uid": "lanf-33ef2446897e0a57",
  "last_seen_at": "2026-03-21T04:00:00+00:00",
  "seconds_since_seen": 3642,
  "stale_threshold_seconds": 300,
  "online_source": "none",
  "is_online_by_polling": false,
  "is_online_by_flag": false
}
```

**Logic xác định online:**
- `is_online_by_polling = true` nếu `updated_at < 300s` (agent đang chạy và poll data)
- `is_online_by_flag = true` nếu `Printer.is_online = true` (set bởi hệ thống `_refresh_stale_offline`)
- `is_online = is_online_by_polling OR is_online_by_flag`
- `online_source`: `"polling"` | `"printer_flag"` | `"none"`

Response (not found):
```json
{ "ok": false, "error": "Device not found" }
```

## 6) Get all machine infor by LAN UID
- Method: `GET`
- Path: `/api/public/network/by-lan`
- Query params (required):
  - `lan_uid`

Example:
```bash
curl -s "https://agentapi.quanlymay.com/api/public/network/by-lan?lan_uid=lanf-33ef2446897e0a57"
```

Response:
```json
{
  "ok": true,
  "lan_uid": "lanf-33ef2446897e0a57",
  "count": 2,
  "rows": [
    {
      "lead": "default",
      "lan_uid": "lanf-33ef2446897e0a57",
      "mac_id": "00:26:73:7D:78:F9",
      "agent_uid": "agent-pc-01",
      "printer_name": "Aficio MP 9002",
      "ip": "192.168.1.224",
      "counter": { "total": "3653272" },
      "status": { "system_status": "Status OK" },
      "last_counter_at": "2026-03-02T00:00:00+00:00",
      "last_status_at": "2026-03-02T00:00:00+00:00",
      "created_at": "2026-03-01T16:00:00+00:00",
      "updated_at": "2026-03-02T00:00:00+00:00"
    }
  ]
}
```

## 6) Get all data (all LANs, all machines)
- Method: `GET`
- Path: `/all/`
- Query params (optional):
  - `lan_uid`

Example:
```bash
curl -s "https://agentapi.quanlymay.com/all/"
```

Response:
```json
{
  "ok": true,
  "count": 4,
  "rows": [
    {
      "lead": "default",
      "lan_uid": "lanf-33ef2446897e0a57",
      "machine_uid": "00:26:73:7D:78:F9",
      "mac_id": "00:26:73:7D:78:F9",
      "agent_uid": "agent-pc-01",
      "printer_name": "Aficio MP 9002",
      "ip": "192.168.1.224",
      "counter": { "total": "3653272" },
      "status": { "system_status": "Status OK" },
      "last_counter_at": "2026-03-02T00:00:00+00:00",
      "last_status_at": "2026-03-02T00:00:00+00:00",
      "created_at": "2026-03-01T16:00:00+00:00",
      "updated_at": "2026-03-02T00:00:00+00:00"
    }
  ]
}
```

## 7) Agent machine export for CRM

- Method: `GET`

## 8) Get workspaces linked to a user
- Method: `GET`
- Path: `/api/user/workspaces`
- Query params (required):
  - `user_id`

Example:
```bash
curl -s "https://agentapi.quanlymay.com/api/user/workspaces?user_id=7"
```

Response:
```json
{
  "ok": true,
  "user_id": 7,
  "rows": [
    {
      "id": "ws-1",
      "name": "Công ty TNHH Gox Print",
      "logo": "🏭",
      "color": "#2196F3",
      "address": "123 Nguyễn Huệ, Q1, TP.HCM",
      "created_at": "2026-03-10"
    }
  ]
}
```

## 9) Get users linked to a workspace
- Method: `GET`
- Path: `/api/workspace/users`
- Query params (required):
  - `workspace_id`

Example:
```bash
curl -s "https://agentapi.quanlymay.com/api/workspace/users?workspace_id=ws-1"
```

Response:
```json
{
  "ok": true,
  "workspace_id": "ws-1",
  "rows": [
    {
      "id": 7,
      "lead": "default",
      "username": "tech1",
      "full_name": "Lê Minh Cường",
      "email": "tech1@kythuat.vn",
      "type": "tech",
      "role": "tech",
      "workspaceIds": ["ws-1", "ws-2", "ws-3"]
    }
  ]
}
```
- Path: `/api/public/agent-machines`
- Query params (required):
  - `agent_uid`

CRM partners use this endpoint to read the machines that belong to a PC agent (Tier 2). Each machine entry contains the lan/fingerprint metadata that already exists inside `DeviceInfor`, `LanSite`, and the new `NetworkInfo` table, plus counter, status, toner, alert, feature, and lock/unlock history derived from polling.

Example response:
```json
{
  "ok": true,
  "lead": "default",
  "agent_uid": "agent-pc-01",
  "agent": {
    "hostname": "PC-01",
    "local_ip": "192.168.1.10",
    "local_mac": "11:22:33:44:55:66"
  },
  "count": 2,
  "machines": [
    {
      "lead": "default",
      "lan_uid": "lanf-33ef2446897e0a57",
      "lan_name": "Factory A",
      "fingerprint_signature": "lead=default|subnet=192.168.1.0/24|gateway_ip=192.168.1.1|gateway_mac=AA:BB:CC:DD:EE:FF",
      "network": {
        "network_id": "net-001",
        "network_name": "Factory A LAN",
        "office_name": "Factory Floor",
        "real_address": "123 Factory Rd, District 1"
      },
      "agent_uid": "agent-pc-01",
      "printer_name": "Aficio MP 9002",
      "mac_id": "00:26:73:7D:78:F9",
      "ip": "192.168.1.224",
      "counter_total": 3653272,
      "counter_summary": {
        "copier_bw": 238346,
        "printer_bw": 316916,
        "fax_bw": 0
      },
      "status": "Status OK",
      "alert": "Energy Saver Mode",
      "toner": {
        "state": "Status OK"
      },
      "auto_alert": {
        "severity": "warning",
        "message": "Black toner is low",
        "status": "pending",
        "triggered_at": "2026-03-09T01:02:03+00:00",
        "resolved_at": ""
      },
      "features": [
        {
          "feature": "address_book",
          "enabled": true,
          "metadata": {},
          "last_seen_at": "2026-03-09T00:00:00+00:00"
        }
      ],
      "lock_history": [
        {
          "action": "lock",
          "reason": "Service window",
          "source": "lead",
          "event_at": "2026-03-09T01:05:00+00:00",
          "metadata": {}
        }
      ],
      "last_counter_at": "2026-03-02T00:00:00+00:00",
      "last_status_at": "2026-03-02T00:00:00+00:00",
      "updated_at": "2026-03-02T00:00:00+00:00"
    }
  ]
}
```

## 8) Ricoh machine lock/unlock

Use these endpoints to queue a lock or unlock command for a Ricoh device already known by the server. The backend pushes the command to the matching agent through polling, then waits for the agent result.

- List devices first: `GET /api/devices`
- Unlock a Ricoh machine: `POST /api/devices/<mac_id>/unlock`
- Lock a Ricoh machine: `POST /api/devices/<mac_id>/lock`
- Legacy alias kept for compatibility: `PATCH /api/devices/<mac_id>/enable` with body `{ "enabled": true|false }`

Notes:
- Use the `mac_id` value returned by `GET /api/devices`.
- The server accepts `mac_id` in these formats: `AA:BB:CC:DD:EE:FF`, `AA-BB-CC-DD-EE-FF`, or `AABBCCDDEEFF`.
- Success responses normalize `mac_id` to `AA:BB:CC:DD:EE:FF`.
- Success returns `200`.
- Agent-side failure returns `409`.
- Timeout waiting for the agent returns `504`.

Test commands:
```bash
curl -s "https://agentapi.quanlymay.com/api/devices"
```

```bash
curl -s -X POST "https://agentapi.quanlymay.com/api/devices/0026737D78F9/unlock"
```

```bash
curl -s -X POST "https://agentapi.quanlymay.com/api/devices/00-26-73-7D-78-F9/lock"
```

Example success response:
```json
{
  "ok": true,
  "id": 123,
  "mac_id": "00:26:73:7D:78:F9",
  "enabled": true,
  "action": "unlock",
  "changed_at": "2026-04-07T10:15:30+00:00",
  "command_id": 456
}
```

## 8A) Device scan-folder assignment

Use this endpoint when the client only knows the Ricoh machine MAC and wants the server to choose the correct Windows agent automatically.

- Discover target machines first: `GET /api/devices`
- Queue scan-folder create/update/delete: `POST /api/devices/<mac_id>/scan-folder`
- Legacy alias kept for compatibility: `POST /api/agents/<agent_id>/ftp-sites`

Notes:
- The server resolves the printer by `mac_id`, reads its `lead + lan_uid`, then picks an agent on the same LAN before queueing the command.
- When multiple same-LAN agents exist, the server prefers an online agent, then the most recently seen one.
- If the body also contains `mac_id`, it must match the path device.
- `create` normally sends `scan_path`; `update` and `delete` normally send `site_name`.
- `scan_path` can be a simple folder name or a full path on the chosen agent.
- If `scan_path` is a simple name, the agent creates the folder under its default scan root.
- `site_name`, `ftp_user`, `ftp_password`, and `port` remain compatibility fields; clients usually do not need them.
- If an explicit `port` is already used by another FTP site, the server returns `409`.
- If no explicit `port` is sent and the default port is busy, the server auto-picks the next free port and returns a `warning`.
- Commands are queued only. The chosen agent applies them on its next polling cycle.

Example create payload:
```json
{
  "action": "create",
  "scan_path": "scan_002673D3250B"
}
```

Queue create:
```bash
curl -s -X POST "https://agentapi.quanlymay.com/api/devices/00:26:73:D3:25:0B/scan-folder" \
  -H "Content-Type: application/json" \
  -d "{\"action\":\"create\",\"scan_path\":\"scan_002673D3250B\"}"
```

Queue update:
```bash
curl -s -X POST "https://agentapi.quanlymay.com/api/devices/002673D3250B/scan-folder" \
  -H "Content-Type: application/json" \
  -d "{\"action\":\"update\",\"site_name\":\"scan_002673D3250B\",\"scan_path\":\"C:/Scans/Ricoh-01\"}"
```

Queue delete:
```bash
curl -s -X POST "https://agentapi.quanlymay.com/api/devices/00-26-73-D3-25-0B/scan-folder" \
  -H "Content-Type: application/json" \
  -d "{\"action\":\"delete\",\"site_name\":\"scan_002673D3250B\"}"
```

Example success response:
```json
{
  "ok": true,
  "queued": true,
  "command_id": 123,
  "status": "pending",
  "action": "create",
  "lead": "default",
  "lan_uid": "lanf-33ef2446897e0a57",
  "agent_id": 35,
  "agent_uid": "tony",
  "agent_local_ip": "192.168.1.10",
  "agent_is_online": true,
  "mac_id": "00:26:73:D3:25:0B",
  "port": 2121,
  "scan_path": "scan_002673D3250B",
  "site_name": "scan_002673D3250B",
  "printer_name": "MP 6503",
  "printer_ip": "192.168.1.226",
  "printer_agent_uid": "tony",
  "warning": ""
}
```

Example validation error when no same-LAN agent exists:
```json
{
  "ok": false,
  "error": "No agent found on lan_uid lanf-33ef2446897e0a57 for printer 00:26:73:D3:25:0B",
  "mac_id": "00:26:73:D3:25:0B",
  "lan_uid": "lanf-33ef2446897e0a57",
  "available_agents": [
    {
      "id": 12,
      "agent_uid": "agent-pc-01",
      "lan_uid": "lanf-deadbeef",
      "local_ip": "192.168.5.10",
      "is_online": false
    }
  ]
}
```

## 8B) Legacy agent queue alias

If the client already knows the exact host agent, it can still call:

- `POST /api/agents/<agent_id>/ftp-sites`

The server keeps the same `action=create|update|delete` payload shape, still validates `lan_uid` for `create`, and now returns the same enriched response fields as the device-based route above.

## 9) List tasks

- Method: `GET`
- Path: `/api/tasks`
- Query params:
  - `agent_uid` (optional)
  - `mac_id` or `mac` (optional)
  - `status` (optional; one of `backlog`, `selected`, `in-progress`, `review`, `done`, `blocked`)
  - `assignee_id` (optional)

Returns the matching tasks. If `X-API-Token` is present, the server scopes the list to the account scope behind that token. Each task links back to `UserAccount` records and reuses the device counters and status tracked inside `DeviceInfor`.

## 10) Create a task

- Method: `POST`
- Path: `/api/tasks`
- Headers:
  - `X-API-Token: <token>` (same API token as `/api/polling`)
- Payload:
  ```json
  {
    "lan_uid": "lanf-33ef2446897e0a57",
    "agent_uid": "agent-pc-01",
    "network_id": "net-001",
    "mac_id": "00:26:73:7D:78:F9",
    "title": "Check paper tray",
    "description": "Tray 1 keeps emptying",
    "status": "backlog",
    "priority": "high",
    "reporter_id": 42,
    "assignee_id": 7,
    "customer_id": 5
  }
  ```

The server derives the task scope from `X-API-Token`, assigns a `task_key`, stores the task in `Task`, and links it to the reporting worker (`reporter_id`), assignee, and customer accounts.

## 11) Update a task

- Method: `PATCH`
- Path: `/api/tasks/<task_id>`
- Headers:
  - `X-API-Token: <token>`
- Payload:
  - All other fields are optional; only include the fields you want to change.
  - Status transitions are limited to Jira-like values (`backlog`, `selected`, `in-progress`, `review`, `done`, `blocked`).
  - Provide `status_updated_at` or `completed_at` timestamps when relevant.
  - The server resolves request scope from `X-API-Token`.

### Delete a task

- Method: `DELETE`
- Path: `/api/tasks/<task_id>`
- Headers:
  - `X-API-Token: <token>`

The server resolves request scope from `X-API-Token`.

Returns:

```json
{
  "ok": true,
  "id": 123
}
```

All task activity materializes in `Task`, while user assignment metadata lives in `UserAccount` together with workspace/location mappings. The `DeviceFeatureFlag`, `DeviceLockHistory`, and `MachineAlert` tables hold the supplementary machine features mentioned earlier (address book, lock/unlock history, automated alerts).

## 12) Workspaces CRUD
- **List:** `GET /api/workspaces` (Params: `name`, `address`, `date_from`, `date_to`)
- **Create:** `POST /api/workspaces` (Body: `id`, `name`, `logo`, `color`, `address`, `userIds`)
- **Update:** `PATCH /api/workspaces/<ws_id>` (Body: `name`, `logo`, `color`, `address`, `userIds`)
- **Delete:** `DELETE /api/workspaces/<ws_id>`
- Response rows include `userIds`, `userCount`, `locationIds`, and `locationCount`.

## 13) Locations CRUD
- **List:** `GET /api/locations` (Params: `name`, `workspace_id`, `date_from`, `date_to`)
- **Create:** `POST /api/locations` (Body: `id`, `name`, `address`, `room`, `phone`, `machine_count`, `workspace_id`)
- **Update:** `PATCH /api/locations/<loc_id>` (Body: `name`, `address`, `room`, `phone`, `machine_count`, `workspace_id`)
- **Delete:** `DELETE /api/locations/<loc_id>`
- Each location belongs to at most one workspace. `workspace_id` is validated on create/update.

## 14) Materials CRUD
- **List:** `GET /api/materials` (Params: `name`, `repair_id`, `date_from`, `date_to`)
- **Create:** `POST /api/materials` (Body: `id`, `repair_request_id`, `name`, `quantity`, `unit_price`, `total_price`)
- **Update:** `PATCH /api/materials/<mat_id>` (Body: `repair_request_id`, `name`, `quantity`, `unit_price`, `total_price`)
- **Delete:** `DELETE /api/materials/<mat_id>`

## 15) Users CRUD
- **List:** `GET /api/users` (Params: `username`, `fullname`, `type`, `role`, `date_from`, `date_to`)
- **Create:** `POST /api/users` (Body: `username`, `password`, `full_name`, `email`, `phone_number`, `type`, `workspaceIds`, `is_active`, `notes`)
- **Update:** `PATCH /api/users/<user_id>` (Body: same as Create)
- **Delete:** `DELETE /api/users/<user_id>`
- User `type` is normalized to one of `tech` or `support`. Legacy `role` input is still accepted as an alias and normalized to the same two values. Create requests inherit the default server scope when no internal scope is supplied.

## 16) Networks/Companies CRUD
- **List:** `GET /api/networks` (Params: `lan_uid`, `name`, `office`, `date_from`, `date_to`)
- **Create:** `POST /api/networks` (Body: `lan_uid`, `network_id`, `network_name`, `office_name`, `real_address`, `notes`)
- **Update:** `PATCH /api/networks/<net_id>` (Body: same as Create)
- **Delete:** `DELETE /api/networks/<net_id>`
- Create requests inherit the default server scope when no internal scope is supplied.

## UserWorkspace membership
- Membership between users and workspaces is stored in `UserWorkspace`.
- A user can belong to many workspaces, and a workspace can contain many users.
- Login endpoints return `workspaceIds` so the frontend can show the workspace picker.
- The read APIs below are the canonical lookup methods for the current membership model.

## 17) Scan Email Destinations (CRUD)
- **List:** `GET /api/lan-emails`
  - Query params (optional):
    - `lead`: filter by lead (e.g., `default`)
    - `lan_uid`: filter by LAN UID
    - `email`: search/filter by email address substring
    - `agent_uid`: filter/determine master status
  - Response:
    ```json
    {
      "ok": true,
      "is_master": false,
      "rows": [
        {
          "id": 1,
          "lead": "default",
          "lan_uid": "lan-1234",
          "email": "user@example.com",
          "email_number": 2130,
          "email_type": "common",
          "pc_name": "PC-01",
          "ftp_user": "goxprint",
          "ftp_password": "goxprint",
          "created_at": "2026-06-11 07:00:00"
        }
      ]
    }
    ```
- **Create:** `POST /api/lan-emails`
  - Headers: `Content-Type: application/json`
  - Body params (required):
    - `lead`: lead name
    - `lan_uid`: LAN UID
    - `email`: email destination address
  - Body params (optional):
    - `email_type`: type of email (e.g., `common`, default: `common`)
    - `pc_name`: target computer name
  - Response:
    ```json
    {
      "ok": true,
      "email": {
        "id": 2,
        "lead": "default",
        "lan_uid": "lan-1234",
        "email": "user2@example.com",
        "email_number": 2131,
        "email_type": "common",
        "pc_name": "PC-02"
      }
    }
    ```
- **Delete:** `DELETE /api/lan-emails/<email_id>`
  - Response:
    ```json
    {
      "ok": true,
      "id": 2
    }
    ```

## 18) VPS Scan Folders (List/Delete files)
- **List Files:** `GET /api/scans/files`
  - Query params (required):
    - `lan_uid`: LAN UID of target network
    - `email`: email folder to list scans from
  - Response:
    ```json
    {
      "ok": true,
      "rows": [
        {
          "name": "scan_20260611_070000.pdf",
          "size": 1048576,
          "mtime": "2026-06-11T00:00:00Z",
          "url": "/static/scans/lan-1234/user_example_com/scan_20260611_070000.pdf",
          "upload_duration": 4.2,
          "upload_completed_at": "2026-06-11T00:00:04Z"
        }
      ]
    }
    ```
- **Delete File:** `DELETE /api/scans/file`
  - Query/Body params (required):
    - `lan_uid`: LAN UID of target network
    - `email`: email folder of the file
    - `filename`: filename of the scan file to delete
  - Response:
    ```json
    {
      "ok": true,
      "message": "Successfully deleted file scan_20260611_070000.pdf"
    }
    ```

## 19) Agent Settings & PC Utilities
- **General Settings:** `POST /api/agents/<agent_uid>/settings`
  - Headers: `Content-Type: application/json`
  - Query/Body params (required):
    - `lead`: lead name
    - `scan_auto_open_file`: boolean (e.g. `true`)
    - `scan_auto_open_dir`: boolean (e.g. `true`)
  - Response:
    ```json
    {
      "ok": true,
      "message": "Settings command queued",
      "command_id": 456
    }
    ```
- **Trigger Utility Action:** `POST /api/agents/<agent_uid>/utility/<action>`
  - Action path parameter can be one of:
    - `devices_and_printers`: open Devices and Printers control panel
    - `open_scan_folder`: open the local scan directory in Explorer
    - `dxdiag`: run DirectX Diagnostics for hardware info
    - `change_ip`: configure IP address settings
  - Body params (optional):
    - `new_ip`: (required for `change_ip`) new IP address
    - `subnet_mask`: (optional for `change_ip`) subnet mask
    - `gateway`: (optional for `change_ip`) gateway address
  - Response:
    ```json
    {
      "ok": true,
      "message": "Utility action 'devices_and_printers' queued",
      "command_id": 457
    }
    ```

## 20) Camera Recording Control
- **Camera Recording Control:** `POST /api/cameras/record-control` (Aliases: `POST /api/public/camera/control`, `POST /api/public/camera/record`, `POST /api/cameras/record`)
  - Headers: `Content-Type: application/json`
  - Body params:
    - `mac_id` / `mac` / `camera_mac`: MAC address of target camera. Optional if `ip` or `camera_id` is supplied.
    - `ip`: (optional fallback) IP address of camera if `mac_id` is not passed.
    - `camera_id` / `id`: (optional fallback) Camera ID if `mac_id` is not passed.
    - `agent_uid` / `agent`: (optional) ID of managing agent. If omitted, automatically selects an online agent in the same LAN.
    - `action`: `'start'`, `'record'`, or `'stop'` (default: `'start'`).
    - `duration`: (optional, default: 30) Recording duration limit in seconds (for `'record'`) or minutes (for `'start'`).
  - Responses:
    - **Success (Start/Record/Stop):**
      ```json
      {
        "ok": true,
        "message": "Đã bắt đầu ghi hình thành công với giới hạn 30 phút"
      }
      ```
    - **Error:**
      ```json
      {
        "ok": false,
        "error": "Không có Agent trực tuyến nào để thực hiện thao tác"
      }
      ```

## 21) Camera Configurations (CRUD)
- **List LAN Cameras:** `GET /api/public/camera/list` (Aliases: `GET /api/cameras/list`, `GET /api/agents/<agent_uid>/cameras`)
  - Query Parameters (optional):
    - `lan_uid`: Unique ID of the LAN site.
    - `agent_uid`: Unique ID of the Agent node.
  - Returns all active and configured cameras within the requested LAN site (`lan_uid`) or Agent (`agent_uid`), deduplicated by MAC address.
  - Response:
    ```json
    {
      "ok": true,
      "cameras": [
        {
          "id": 3232235778,
          "agent_uid": "kythuat02",
          "camera_name": "Camera 192.168.1.67 (Camera 192.168.1.108)",
          "rtsp_url": "rtsp://admin:Donga2011@192.168.1.67:554/cam/realmonitor?channel=1&subtype=0",
          "segment_duration": 60,
          "prefix": "rec",
          "video_codec": "copy",
          "audio_codec": "copy",
          "no_audio": true,
          "is_recording": false,
          "ip": "192.168.1.67, 192.168.1.108",
          "mac_address": "24:14:07:43:31:4D",
          "manufacturer": "Sigmastar",
          "model": "Camera IP",
          "is_online": true
        }
      ]
    }
    ```

- **Save/Edit Camera:** `POST /api/agents/<agent_uid>/cameras`
  - Headers: `Content-Type: application/json`
  - Body params:
    - `id`: (optional, for editing) configuration ID
    - `camera_name`: (optional, default: "Camera")
    - `rtsp_url`: (required) RTSP stream URL
    - `segment_duration`: (optional, default: 60)
    - `prefix`: (optional, default: "rec")
    - `video_codec`: (optional, default: "copy")
    - `audio_codec`: (optional, default: "copy")
    - `no_audio`: (optional, default: true)
  - Response:
    ```json
    {
      "ok": true,
      "camera_id": 1
    }
    ```

- **Delete Camera:** `POST /api/agents/<agent_uid>/cameras/<int:camera_id>/delete`
  - Response:
    ```json
    {
      "ok": true
    }
    ```

## 15) Public IP check
- Method: `GET`
- Path: `/api/public/ip/public`
- Description: Get the public IP address of the caller.
- Response:
  ```json
  {
    "ok": true,
    "public_ip": "115.79.88.99"
  }
  ```

## 16) Workstation (Local) IP check
- Method: `GET`
- Path: `/api/public/ip/workstation`
- Query params (provide at least one):
  - `agent_uid` – Agent unique ID
  - `lan_uid` – LAN unique ID
  - `mac` (or `mac_id`) – MAC address of a printer or device registered under the agent
- Response:
  ```json
  {
    "ok": true,
    "local_ip": "192.168.1.50",
    "hostname": "DESKTOP-TOEFTR1",
    "agent_uid": "agent_01",
    "lan_uid": "lan_01",
    "app_version": "2.0.79",
    "is_online": true,
    "last_seen_at": "2026-07-15T03:00:00Z"
  }
  ```