Skip to content
145 changes: 142 additions & 3 deletions docs/API_DEVICE.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,10 @@ Manage a **single device** by its MAC address. Operations include retrieval, upd
* **POST** `/device/<mac>`
Create or update a device record.

> ⚠️ **Full-replace (PUT) semantics.** Every editable field is written on each call. Any field omitted from the payload is reset to its default (empty string or `0`). This matches how the frontend edit form works β€” it always sends the complete device state.
>
> To update a **single field** without affecting others, use [`POST /device/<mac>/update-column`](#7-update-a-single-column) instead.

**Request Body**:

```json
Expand All @@ -62,8 +66,8 @@ Manage a **single device** by its MAC address. Operations include retrieval, upd

**Behavior**:

* If `createNew=true` β†’ creates a new device
* Otherwise β†’ updates existing device fields
* If `createNew=true` β†’ inserts a new device row
* Otherwise β†’ **replaces all editable fields** on the existing device

**Response**:

Expand Down Expand Up @@ -163,7 +167,13 @@ Manage a **single device** by its MAC address. Operations include retrieval, upd
## 7. Update a Single Column

* **POST** `/device/<mac>/update-column`
Update one specific column for a device.
Update exactly one field for a device without touching any other fields.

> βœ… **Partial-update (PATCH) semantics.** Only the specified column is written. All other fields are left unchanged. Use this for automation, integrations, and any workflow that needs to update a single attribute.
>
> To replace all fields at once (e.g. saving from the edit form), use [`POST /device/<mac>`](#2-update-device-fields).

Allowed `columnName` values: `devName`, `devOwner`, `devType`, `devVendor`, `devGroup`, `devLocation`, `devComments`, `devIcon`, `devFavorite`, `devAlertEvents`, `devAlertDown`, `devCanSleep`, `devSkipRepeated`, `devReqNicsOnline`, `devForceStatus`, `devParentMAC`, `devParentPort`, `devParentRelType`, `devSSID`, `devSite`, `devVlan`, `devStaticIP`, `devIsNew`, `devIsArchived`, `devCustomProps`.

**Request Body**:

Expand All @@ -190,6 +200,108 @@ Manage a **single device** by its MAC address. Operations include retrieval, upd

---

## 8. Lock / Unlock a Device Field

* **POST** `/device/<mac>/field/lock`
Lock a field to prevent plugin overwrites, or unlock it to allow overwrites again.

**Request Body**:

```json
{
"fieldName": "devName",
"lock": true
}
```

| Field | Type | Required | Description |
|---|---|---|---|
| `fieldName` | string | βœ… | Field to lock/unlock (e.g. `devName`, `devVendor`) |
| `lock` | boolean | ❌ | `true` to lock, `false` to unlock (default when omitted) |

Comment thread
coderabbitai[bot] marked this conversation as resolved.
**Response** (success):

```json
{
"success": true,
"fieldName": "devName",
"locked": true,
"message": "Field devName locked"
}
```

**Error Responses**:

* Field does not support locking β†’ HTTP 400
* Unauthorized β†’ HTTP 403

---

## 9. Unlock / Clear Device Fields (Bulk)

* **POST** `/devices/fields/unlock`
Unlock fields (clear `LOCKED`/`USER` sources) for one device, a list of devices, or all devices.

**Request Body**:

```json
{
"mac": "AA:BB:CC:DD:EE:FF",
"fields": ["devName", "devVendor"],
"clearAll": false
}
```

| Field | Type | Required | Description |
|---|---|---|---|
| `mac` | string or array | ❌ | Single MAC, list of MACs, or omit for all devices |
| `fields` | array of strings | ❌ | Fields to unlock. Omit to unlock all tracked fields |
| `clearAll` | boolean | ❌ | `true` clears all sources; `false` (default) clears only `LOCKED`/`USER` |

Comment thread
coderabbitai[bot] marked this conversation as resolved.
**Response** (success):

```json
{
"success": true
}
```

**Error Responses**:

* `fields` is not a list β†’ HTTP 400
* Unauthorized β†’ HTTP 403

---

## 10. Set Device Alias

* **POST** `/device/<mac>/set-alias`
Convenience wrapper to update the device display name (`devName`).

**Request Body**:

```json
{
"alias": "My Router"
}
```

**Response** (success):

```json
{
"success": true
}
```

**Error Responses**:

* Missing `alias` β†’ HTTP 400
* Device not found β†’ HTTP 404
* Unauthorized β†’ HTTP 403
Comment thread
coderabbitai[bot] marked this conversation as resolved.

---

## Example `curl` Requests

**Get Device Details**:
Expand Down Expand Up @@ -233,3 +345,30 @@ curl -X POST "http://<server_ip>:<GRAPHQL_PORT>/device/AA:BB:CC:DD:EE:FF/update-
--data '{"columnName":"devName","columnValue":"Updated Device"}'
```

**Lock a Field**:

```bash
curl -X POST "http://<server_ip>:<GRAPHQL_PORT>/device/AA:BB:CC:DD:EE:FF/field/lock" \
-H "Authorization: Bearer <API_TOKEN>" \
-H "Content-Type: application/json" \
--data '{"fieldName":"devName","lock":true}'
```

**Unlock Fields (all devices)**:

```bash
curl -X POST "http://<server_ip>:<GRAPHQL_PORT>/devices/fields/unlock" \
-H "Authorization: Bearer <API_TOKEN>" \
-H "Content-Type: application/json" \
--data '{"fields":["devName","devVendor"]}'
```

**Set Device Alias**:

```bash
curl -X POST "http://<server_ip>:<GRAPHQL_PORT>/device/AA:BB:CC:DD:EE:FF/set-alias" \
-H "Authorization: Bearer <API_TOKEN>" \
-H "Content-Type: application/json" \
--data '{"alias":"My Router"}'
```

5 changes: 5 additions & 0 deletions server/api_server/api_server_start.py
Original file line number Diff line number Diff line change
Expand Up @@ -585,6 +585,11 @@ def api_device_set_alias(mac, payload=None):

device_handler = DeviceInstance()
result = device_handler.updateDeviceColumn(mac, 'devName', alias)

if not result.get("success"):
err = result.get("error") or result.get("message") or f"Failed to update alias for device {mac}"
return jsonify({"success": False, "error": err})

return jsonify(result)


Expand Down
18 changes: 14 additions & 4 deletions server/api_server/openapi/schemas.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,9 +33,19 @@

# Security whitelists & Literals for documentation
ALLOWED_DEVICE_COLUMNS = Literal[
# Main Info
"devName", "devOwner", "devType", "devVendor",
"devGroup", "devLocation", "devComments", "devFavorite",
"devParentMAC", "devCanSleep"
"devGroup", "devLocation", "devComments", "devIcon",
# Alerts & Behavior
"devFavorite", "devAlertEvents", "devAlertDown",
"devCanSleep", "devSkipRepeated", "devReqNicsOnline", "devForceStatus",
# Network topology
"devParentMAC", "devParentPort", "devParentRelType",
"devSSID", "devSite", "devVlan",
# Display / Status
"devStaticIP", "devIsNew", "devIsArchived",
# Custom properties
Comment thread
coderabbitai[bot] marked this conversation as resolved.
"devCustomProps",
]
Comment thread
jokob-sk marked this conversation as resolved.

ALLOWED_NMAP_MODES = Literal[
Expand Down Expand Up @@ -407,7 +417,7 @@ class UpdateDeviceColumnRequest(BaseModel):
class LockDeviceFieldRequest(BaseModel):
"""Request to lock/unlock a device field."""
fieldName: str = Field(..., description="Field name to lock/unlock (e.g., devName, devVendor). Required.")
lock: bool = Field(True, description="True to lock the field, False to unlock")
lock: bool = Field(False, description="True to lock the field, False (default) to unlock")
Comment thread
jokob-sk marked this conversation as resolved.


class UnlockDeviceFieldsRequest(BaseModel):
Expand All @@ -420,7 +430,7 @@ class UnlockDeviceFieldsRequest(BaseModel):
None,
description="List of field names to unlock. If omitted, all tracked fields will be unlocked"
)
clear_all: bool = Field(
clearAll: bool = Field(
Comment thread
jokob-sk marked this conversation as resolved.
False,
description="True to clear all sources, False to clear only LOCKED/USER"
)
Expand Down
Loading