added another running instance.

This commit is contained in:
Pablo Revilla
2025-07-22 14:49:43 -07:00
parent b47240fa06
commit 8c99b63399
2 changed files with 247 additions and 0 deletions
+122
View File
@@ -0,0 +1,122 @@
# API Documentation
This document describes the available REST API endpoints for **Chat** and **Nodes**.
---
## **Chat API**
### **`GET /api/chat`**
Fetches chat messages, with support for both initial loading and incremental updates.
#### **Query Parameters**
| Name | Type | Required | Description |
|----------|--------|----------|-------------|
| `limit` | int | No | Number of messages to return (default: 100, max: 200). |
| `since` | string | No | Return only messages with `import_time > since` (ISO 8601 format, e.g., `2025-07-21T12:00:00`). |
#### **Response (200 OK)**
```json
{
"packets": [
{
"id": 123,
"import_time": "2025-07-22T14:12:00.123456",
"channel": "LongFast",
"from_node_id": 456789,
"long_name": "Node A",
"payload": "Hello world!"
}
],
"latest_import_time": "2025-07-22T14:12:00.123456"
}
```
**Fields:**
- `id`: Unique packet ID.
- `import_time`: ISO 8601 timestamp of when the message was imported.
- `channel`: Channel name.
- `from_node_id`: Numeric ID of the node that sent the message.
- `long_name`: Human-readable name of the node (if available).
- `payload`: Actual message text.
#### **Examples**
- **Get last 50 messages:**
```
GET /api/chat?limit=50
```
- **Get messages after a certain timestamp:**
```
GET /api/chat?since=2025-07-22T12:00:00
```
---
## **Nodes API**
### **`GET /api/nodes`**
Returns a list of all nodes, with optional filters based on "last seen" time.
#### **Query Parameters**
| Name | Type | Required | Description |
|-------------------|--------|----------|-------------|
| `hours` | int | No | Only return nodes seen within the last `N` hours. |
| `days` | int | No | Only return nodes seen within the last `N` days. |
| `last_seen_after` | string | No | Custom ISO 8601 timestamp for filtering (`2025-07-21T10:00:00`). |
**Note:** `hours` and `days` take precedence over `last_seen_after`.
#### **Response (200 OK)**
```json
{
"nodes": [
{
"node_id": 123456,
"long_name": "BaseStation",
"short_name": "BS",
"channel": "LongFast",
"last_seen": "2025-07-22T14:10:00.000000",
"hardware": "Heltec V3",
"firmware": "1.3.2",
"role": "CLIENT"
}
]
}
```
**Fields:**
- `node_id`: Unique ID of the node.
- `long_name`: Full descriptive name of the node.
- `short_name`: Short name or alias.
- `channel`: Channel the node is configured on.
- `last_seen`: ISO 8601 timestamp of the last time the node was seen.
- `hardware`: Hardware model (e.g., Heltec V3).
- `firmware`: Firmware version of the node.
- `role`: Node role (e.g., `CLIENT`, `ROUTER`, etc.).
#### **Examples**
- **All nodes:**
```
GET /api/nodes
```
- **Nodes seen in the last 1 hour:**
```
GET /api/nodes?hours=1
```
- **Nodes seen in the last 7 days:**
```
GET /api/nodes?days=7
```
- **Custom timestamp:**
```
GET /api/nodes?last_seen_after=2025-07-21T12:00:00
```
---
## **General Notes**
- All timestamps are returned in **localtime** (ISO 8601 format).
- Both endpoints return JSON responses with `application/json` content type.
- Error responses return `{"error": "message"}` with an appropriate HTTP status code (e.g., `500`).
---
+125
View File
@@ -1431,6 +1431,131 @@ async def get_config(request):
except (json.JSONDecodeError, TypeError):
return web.json_response({"error": "Invalid configuration format"}, status=500)
# API Section
# How this works
# When your frontend calls /api/chat without since, it returns the most recent limit (default 100) messages.
# When your frontend calls /api/chat?since=ISO_TIMESTAMP, it returns only messages with import_time > since.
# The response includes "latest_import_time" for frontend to keep track of the newest message timestamp.
# The backend fetches extra packets (limit*5) to account for filtering messages like "seq N" and since filtering.
@routes.get("/api/chat")
async def api_chat(request):
try:
# Parse query params
limit_str = request.query.get("limit", "100")
since_str = request.query.get("since", None)
try:
limit = min(max(int(limit_str), 1), 200) # Limit between 1 and 200
except ValueError:
limit = 100
if since_str:
try:
since = datetime.datetime.fromisoformat(since_str)
except Exception as e:
print(f"Failed to parse since '{since_str}': {e}")
since = None
else:
since = None
# Fetch packets from store
packets = await store.get_packets(
node_id=0xFFFFFFFF,
portnum=PortNum.TEXT_MESSAGE_APP,
limit=limit*5, # Fetch extra to filter out seq messages and since filter
)
SEQ_REGEX = re.compile(r"seq \d+")
ui_packets = [Packet.from_model(p) for p in packets]
filtered_packets = [p for p in ui_packets if p.payload and not SEQ_REGEX.fullmatch(p.payload)]
# Filter by 'since' timestamp if provided
if since:
filtered_packets = [p for p in filtered_packets if p.import_time > since]
# Sort by import_time descending (latest first)
filtered_packets.sort(key=lambda p: p.import_time, reverse=True)
# Trim to requested limit
filtered_packets = filtered_packets[:limit]
packets_data = [{
"id": p.id,
"import_time": p.import_time.isoformat(),
"channel": getattr(p.from_node, "channel", ""),
"from_node_id": p.from_node_id,
"long_name": getattr(p.from_node, "long_name", ""),
"payload": p.payload,
} for p in filtered_packets]
latest_import_time = filtered_packets[0].import_time.isoformat() if filtered_packets else since_str or None
return web.json_response({
"packets": packets_data,
"latest_import_time": latest_import_time,
})
except Exception as e:
print("Error in /api/chat:", e)
return web.json_response({"error": "Failed to fetch chat data"}, status=500)
# Client to pass ?hours=1 or ?days=7 to filter
@routes.get("/api/nodes")
async def api_nodes(request):
try:
# Query params
hours = request.query.get("hours")
days = request.query.get("days")
last_seen_after = None
# Determine cutoff time
if hours:
try:
last_seen_after = datetime.datetime.now() - datetime.timedelta(hours=int(hours))
except ValueError:
pass
elif days:
try:
last_seen_after = datetime.datetime.now() - datetime.timedelta(days=int(days))
except ValueError:
pass
else:
# Fallback: if a direct ISO timestamp is provided
last_seen_str = request.query.get("last_seen_after")
if last_seen_str:
try:
last_seen_after = datetime.datetime.fromisoformat(last_seen_str)
except Exception as e:
print(f"Failed to parse last_seen_after '{last_seen_str}': {e}")
# Fetch nodes
nodes = await store.get_nodes()
# Apply filter
if last_seen_after:
nodes = [n for n in nodes if n.last_seen and n.last_seen > last_seen_after]
# Prepare response
nodes_data = [{
"node_id": n.id,
"long_name": n.long_name,
"short_name": n.short_name,
"channel": n.channel,
"last_seen": n.last_seen.isoformat() if n.last_seen else None,
"hardware": n.hardware,
"firmware": n.firmware,
"role": n.role,
} for n in nodes]
return web.json_response({"nodes": nodes_data})
except Exception as e:
print("Error in /api/nodes:", e)
return web.json_response({"error": "Failed to fetch nodes"}, status=500)
async def run_server():
app = web.Application()