# WebDev API Contract — CLAUDE.md Parent: [../CLAUDE.md](../CLAUDE.md) ## Overview All external interaction with Ignition tags goes through WebDev endpoints. No external tool should read or write tag files directly at runtime. The test runner, CLI tools, and any future integrations use these endpoints. ## Base URL ``` http://:8088/system/webdev/ ``` Default: `http://localhost:8088/system/webdev/framework` ## Endpoints ### POST /tagWrite Write one or more tag values. **Request:** ```json { "writes": [ { "path": "[default]Devices/Pumps/P-101/Command/Start", "value": true }, { "path": "[default]Devices/Pumps/P-101/Command/SpeedSetpoint", "value": 60.0 } ] } ``` **Response (200):** ```json { "status": "ok", "results": [ { "path": "[default]Devices/Pumps/P-101/Command/Start", "success": true }, { "path": "[default]Devices/Pumps/P-101/Command/SpeedSetpoint", "success": true } ] } ``` **Response (400 — bad request):** ```json { "status": "error", "message": "Invalid tag path: [default]Devices/Pumps/P-999/Command/Start" } ``` **Handler (Jython 2.7):** ```python # WebDev endpoint: tagWrite # Method: POST # Jython 2.7 — no f-strings, no type hints import json def doPost(request, session): logger = system.util.getLogger("webdev.tagWrite") try: payload = json.loads(request["data"]) writes = payload.get("writes", []) if not writes: return {"status": "error", "message": "No writes provided"} paths = [w["path"] for w in writes] values = [w["value"] for w in writes] results = system.tag.writeBlocking(paths, values) response_results = [] for i, qv in enumerate(results): response_results.append({ "path": paths[i], "success": qv.isGood() }) return {"status": "ok", "results": response_results} except Exception as e: logger.error("tagWrite error: %s" % str(e)) return {"status": "error", "message": str(e)} ``` ### GET /tagRead Read one or more tag values. **Request:** ``` GET /tagRead?paths=[default]Devices/Pumps/P-101/Status/Running,[default]Devices/Pumps/P-101/Status/Speed ``` Multiple paths are comma-separated in the query string. **Response (200):** ```json { "status": "ok", "results": [ { "path": "[default]Devices/Pumps/P-101/Status/Running", "value": true, "quality": "Good", "timestamp": "2026-03-14T10:30:00Z" }, { "path": "[default]Devices/Pumps/P-101/Status/Speed", "value": 59.8, "quality": "Good", "timestamp": "2026-03-14T10:30:00Z" } ] } ``` **Handler (Jython 2.7):** ```python # WebDev endpoint: tagRead # Method: GET # Jython 2.7 — no f-strings, no type hints def doGet(request, session): logger = system.util.getLogger("webdev.tagRead") try: paths_param = request["params"].get("paths", "") if not paths_param: return {"status": "error", "message": "No paths provided"} paths = [p.strip() for p in paths_param.split(",")] qvs = system.tag.readBlocking(paths) results = [] for i, qv in enumerate(qvs): results.append({ "path": paths[i], "value": qv.value, "quality": str(qv.quality), "timestamp": str(qv.timestamp) }) return {"status": "ok", "results": results} except Exception as e: logger.error("tagRead error: %s" % str(e)) return {"status": "error", "message": str(e)} ``` ### GET /tagBrowse Browse the tag tree from a root path. **Request:** ``` GET /tagBrowse?root=[default]Devices/Pumps ``` **Response (200):** ```json { "status": "ok", "root": "[default]Devices/Pumps", "children": [ { "name": "P-101", "path": "[default]Devices/Pumps/P-101", "type": "UdtInstance", "has_children": true }, { "name": "P-102", "path": "[default]Devices/Pumps/P-102", "type": "UdtInstance", "has_children": true } ] } ``` **Handler (Jython 2.7):** ```python # WebDev endpoint: tagBrowse # Method: GET # Jython 2.7 — no f-strings, no type hints def doGet(request, session): logger = system.util.getLogger("webdev.tagBrowse") try: root = request["params"].get("root", "") browse_results = system.tag.browse(root) children = [] for result in browse_results.getResults(): children.append({ "name": result["name"], "path": str(result["fullPath"]), "type": str(result["tagType"]), "has_children": result["hasChildren"] }) return {"status": "ok", "root": root, "children": children} except Exception as e: logger.error("tagBrowse error: %s" % str(e)) return {"status": "error", "message": str(e)} ``` ### GET /health Check gateway and connection status. **Request:** ``` GET /health ``` **Response (200):** ```json { "status": "ok", "gateway": { "state": "RUNNING", "uptime_ms": 3600000 }, "connections": { "plc": { "ControlLogix": { "connected": true, "status": "Connected" } }, "database": { "postgres": { "connected": true, "status": "Valid" } } } } ``` ## Rules 1. **All external I/O goes through these endpoints.** No exceptions at runtime. 2. **Never bypass WebDev** to edit tag JSON files while the gateway is running. 3. **Validate inputs** in every handler — check for missing fields, bad paths. 4. **Log errors** with `system.util.getLogger()` in every handler. 5. **Return consistent JSON** — always include `status` field ("ok" or "error"). 6. **Jython 2.7 only** in handler code — no f-strings, no type hints. ## Error Handling Convention All endpoints return this structure on error: ```json { "status": "error", "message": "Human-readable error description" } ``` HTTP status codes: - `200` — success (status: "ok") - `400` — bad request (missing params, invalid paths) - `500` — server error (unhandled exceptions) ## Verification Commands Quick checks from the host or test runner: ```bash # Health check curl -s http://localhost:8088/system/webdev/framework/health | python3 -m json.tool # Read a tag curl -s "http://localhost:8088/system/webdev/framework/tagRead?paths=[default]Devices/Pumps/P-101/Status/Running" | python3 -m json.tool # Write a tag curl -s -X POST http://localhost:8088/system/webdev/framework/tagWrite \ -H "Content-Type: application/json" \ -d '{"writes":[{"path":"[default]Devices/Pumps/P-101/Command/Start","value":true}]}' | python3 -m json.tool # Browse tags curl -s "http://localhost:8088/system/webdev/framework/tagBrowse?root=[default]Devices" | python3 -m json.tool ``` ## File Structure ``` webdev/ ├── CLAUDE.md ← you are here └── endpoints/ ├── tagWrite/ │ ├── code.py ← handler source (Jython 2.7) │ └── resource.json ├── tagRead/ │ ├── code.py │ └── resource.json ├── tagBrowse/ │ ├── code.py │ └── resource.json └── health/ ├── code.py └── resource.json ```