Skip to main content

Online Edit

June 2026 | ControlForge v1.0.1069

Online edit lets you change running ST programs without a stop/start cycle. You stage edited files into a holding area, diff them against what the runtime is actually executing, then commit the whole set atomically — the affected tasks reload in place. Every commit is recorded in a history you can inspect and revert. It is a pure HTTP API surface — no ST functions.

The mental model is git for the live runtime: a staging area, an atomic commit, an immutable history, and a one-shot revert that itself goes through staging.


Online edit changes running ST programs without a stop/start: stage edited files, diff them against the live runtime, commit the set atomically so tasks reload in place, and revert through history when needed.

The workflow at a glance

stage → put edited file content into the staging area
status → see staged files and how each compares to live (new/modified/unchanged/deleted)
diff → unified diff (live → staged) for one path
commit → apply the WHOLE staged set to live, atomically; tasks reload
or
discard → drop a staged file without applying it
or
revert → stage the reverse-diff of an earlier commit, then commit that
history → list past commits; fetch one for full per-file detail

A path everywhere below is the bare program name (e.g. Main, PoU_TankSim) — not a filesystem path.

All examples assume a bearer token:

TOKEN=$(curl -s -X POST http://localhost:8302/api/auth/login \
-H 'Content-Type: application/json' \
-d '{"username":"goplc","password":"goplc"}' | python3 -c 'import sys,json;print(json.load(sys.stdin)["token"])')

1. Stage a change

Add or replace a file in the staging area. path is the bare program name; content is the full new ST source for that program. Staging is the only mutating step that touches nothing live — it just parks your version.

curl -s -H "Authorization: Bearer $TOKEN" \
-X POST http://localhost:8302/api/edit/stage \
-H 'Content-Type: application/json' \
-d '{
"path": "Main",
"content": "PROGRAM Main\nVAR\n x : INT;\nEND_VAR\n x := x + 2;\nEND_PROGRAM\n"
}'
{ "path": "Main", "staged_by": "goplc" }

staged_by is the authenticated operator (falls back to client IP). Staging the same path again replaces the prior staged content.


2. Check status

List everything currently staged and how each path relates to the live runtime. This is your "what's pending" view before you commit.

curl -s -H "Authorization: Bearer $TOKEN" \
http://localhost:8302/api/edit/status
{
"count": 1,
"files": [
{
"path": "Main",
"state": "modified",
"staged_at": "2026-06-22T04:48:20Z",
"staged_by": "goplc",
"bytes": 68
}
]
}

The state field is the heart of it:

stateMeaning
newStaged path doesn't exist live yet — commit will add it
modifiedStaged content differs from live — commit will change it
unchangedStaged content matches live byte-for-byte — commit is a no-op for this file
deletedTombstone: staged for removal at next commit

When no edit flow is configured, status returns 503 {"error":"edit flow not enabled"} instead.


3. Diff a staged path

See the exact unified diff (live → staged) for one path. The response is text/plain, not JSON. An empty body means the staged content matches live byte-for-byte.

curl -s -H "Authorization: Bearer $TOKEN" \
"http://localhost:8302/api/edit/diff?path=Main"
--- Main (live)
+++ Main (staged)
@@ -1 +1,7 @@
+PROGRAM Main
+VAR
+ x : INT;
+END_VAR
+ x := x + 2;
+END_PROGRAM

The path query parameter is required. 400 {"error":"path required"} if omitted; 404 {"error":"not staged"} if the path isn't in the staging set.


4. Commit the staged set

Apply the entire staged set to live, atomically. The affected tasks reload in place — no stop/start. An optional message annotates the commit. On a parse or reload failure the staging area is left intact so you can fix and retry.

curl -s -H "Authorization: Bearer $TOKEN" \
-X POST http://localhost:8302/api/edit/commit \
-H 'Content-Type: application/json' \
-d '{ "message": "tighten Main loop increment" }'
{
"id": "0190f3c2-...-uuidv7",
"committed": ["Main"],
"tasks": ["MainTask"],
"message": "tighten Main loop increment",
"by": "goplc",
"kind": "commit"
}
  • id is a UUIDv7 — it doubles as the commit's history ID and its event-spine correlation ID.
  • committed is every path whose content landed live (sorted).
  • tasks is every task that was reloaded as a result (sorted).
  • A commit also auto-saves a project snapshot of the now-running state, so the snapshot timeline captures each online edit alongside Download / Deploy / Import events.

An empty body is valid (commit with no message). 503 if staging or the scheduler isn't configured; 500 (with the staged set untouched) if the commit itself fails.


5. Discard a staged change

Drop a staged file without applying it. Idempotent — discarding a path that isn't staged is a successful no-op.

curl -s -H "Authorization: Bearer $TOKEN" \
-X POST http://localhost:8302/api/edit/discard \
-H 'Content-Type: application/json' \
-d '{ "path": "Main" }'
{ "path": "Main", "removed": true }

removed is true if the path was in the staging set, false if it was already absent. (POST /api/edit/unstage is the same operation under a different name; discard is its alias.)


6. Revert an earlier commit

Undo a past commit by staging its reverse-diff. Revert doesn't bypass the workflow — it puts the inverse change into the staging area, where you then status/diff it like any other pending edit and commit it to actually apply.

curl -s -H "Authorization: Bearer $TOKEN" \
-X POST http://localhost:8302/api/edit/revert \
-H 'Content-Type: application/json' \
-d '{ "commit_id": "0190f3c2-...-uuidv7" }'

This stages the reverse of every file in that commit. Inspect with status/diff, then commit. When you commit a revert, record it as a revert (not a fresh edit) by passing revert_of to the commit call:

curl -s -H "Authorization: Bearer $TOKEN" \
-X POST http://localhost:8302/api/edit/commit \
-H 'Content-Type: application/json' \
-d '{ "message": "back out the loop tweak", "revert_of": "0190f3c2-...-uuidv7" }'

A commit made with revert_of is recorded with kind: "revert" and a parent_commit pointing at the commit it undid — so the history shows the lineage, not just a second forward edit. If other commits touched the same files between the original and the revert, the revert flags those intervening commits as conflict hints for you to resolve.


7. Browse history

List past commits, newest first. Optional filters: path (only commits touching that program), since / until (Unix-millisecond bounds), limit, and cursor for paging.

curl -s -H "Authorization: Bearer $TOKEN" \
"http://localhost:8302/api/edit/history?limit=20"
{
"commits": [
{
"id": "0190f3c2-...-uuidv7",
"ts": "2026-06-22T04:50:11Z",
"by": "goplc",
"message": "tighten Main loop increment",
"kind": "commit",
"reloaded_tasks": ["MainTask"],
"files_count": 1
}
],
"next_cursor": ""
}

A non-empty next_cursor means there are more pages — pass it back as ?cursor=... to continue. An empty commits list ({"commits":[],"next_cursor":""}) just means nothing has been committed yet.

One commit's full detail

Fetch a single commit by its id to get the per-file diffs, not just the summary row:

curl -s -H "Authorization: Bearer $TOKEN" \
http://localhost:8302/api/edit/history/0190f3c2-...-uuidv7

The detail object carries the commit metadata (id, ts, by, message, kind, parent_commit, reloaded_tasks, files_count) plus a files array with each path's before/after hashes and unified diff. Raw file blobs are not exposed on the wire — the diff is the human-readable form. 404 {"error":"commit not found"} for an unknown id.


Notes & limits

  • path is the bare program name, never a filesystem path. Main, PoU_TankSim — the same identifier the program list uses.
  • Commit is all-or-nothing. The whole staged set lands together and the affected tasks reload atomically; a failure leaves staging intact for fix-and-retry. There is no partial commit.
  • discard and unstage are the same operation. In the current build there's no separate server-side "working" state, so both just remove from staging.
  • Revert routes through staging. It stages the inverse change; you still commit (ideally with revert_of) to apply it. This keeps every live change — including undos — visible in one history.
  • Every commit auto-snapshots the running state, so online edits show up on the snapshot timeline alongside Download / Deploy / Import.
  • Auth required. Mutating calls (stage / commit / revert / discard) attribute the operator via the authenticated username, falling back to client IP.
  • 503 "edit flow not enabled" on any endpoint means this runtime wasn't started with the edit-flow staging store configured.