Crate: tp-webapp
Feature: 003-path-review-webapp
Base URL: http://127.0.0.1:<port> (port defaults to 8765; increments on conflict)
Protocol: HTTP/1.1. All request and response bodies are application/json unless noted.
Authentication: None (localhost-only tool, single session)
This API is considered internal to the tp-lib workspace. It is NOT a public library API and is not subject to semantic versioning. Changes must be co-ordinated between tp-webapp (server) and tp-webapp/static/app.js (client). The contract exists to stabilise the interface for testing purposes.
All endpoints that return a non-2xx status use this body:
{ "ok": false, "error": "<human-readable error message>" }Minimal success acknowledgement used by POST endpoints:
{ "ok": true }Serves the single-page application shell (index.html).
| Property | Value |
|---|---|
| Method | GET |
| Path | / |
| Authentication | None |
| Request body | None |
| Response status | 200 |
| Response content-type | text/html; charset=utf-8 |
| Response body | Contents of static/index.html (embedded via rust-embed) |
All other static assets (/app.js, /style.css, /leaflet/*) are similarly served from the embedded static/ directory at their respective paths.
Returns the complete railway network as a GeoJSON FeatureCollection.
Each feature represents one netelement. The in_path, origin, and confidence properties reflect the current path state at the time of the request.
| Property | Value |
|---|---|
| Method | GET |
| Path | /api/network |
| Authentication | None |
| Request body | None |
| Response status | 200 |
| Response content-type | application/json |
| Response body | GeoJSON FeatureCollection (see schema below) |
FeatureCollection {
type: "FeatureCollection"
features: Feature[]
}
Feature {
type: "Feature"
geometry: {
type: "LineString"
coordinates: [number, number][] // [lon, lat] pairs in WGS 84
}
properties: {
netelement_id: string // unique identifier from network file
in_path: boolean // true if segment is in the current path
origin: "algorithm" // present only when in_path == true
| "manual"
| null // null when in_path == false
confidence: number | null // 0.0–1.0; null when in_path == false
}
}
{
"type": "FeatureCollection",
"features": [
{
"type": "Feature",
"geometry": { "type": "LineString", "coordinates": [[4.35, 50.85], [4.36, 50.86]] },
"properties": { "netelement_id": "NE001", "in_path": true, "origin": "algorithm", "confidence": 0.87 }
},
{
"type": "Feature",
"geometry": { "type": "LineString", "coordinates": [[4.36, 50.86], [4.37, 50.87]] },
"properties": { "netelement_id": "NE002", "in_path": false, "origin": null, "confidence": null }
}
]
}| Status | Condition |
|---|---|
| 500 | State lock poisoned (internal server error) |
Returns the current ordered path as a JSON object.
| Property | Value |
|---|---|
| Method | GET |
| Path | /api/path |
| Authentication | None |
| Request body | None |
| Response status | 200 |
| Response content-type | application/json |
{
segments: PathSegment[]
overall_probability: number // 0.0–1.0
mode: "standalone" | "integrated"
}
PathSegment {
netelement_id: string
probability: number // 0.0–1.0 (always 1.0 for manual segments)
start_intrinsic: number // 0.0–1.0
end_intrinsic: number // 0.0–1.0
gnss_start_index: integer // 0 for manual segments
gnss_end_index: integer // 0 for manual segments
origin: "algorithm" | "manual"
path_index: integer // 0-based position in ordered path
}
{
"segments": [
{
"netelement_id": "NE001", "probability": 0.87,
"start_intrinsic": 0.0, "end_intrinsic": 1.0,
"gnss_start_index": 0, "gnss_end_index": 12,
"origin": "algorithm", "path_index": 0
},
{
"netelement_id": "NE003", "probability": 1.0,
"start_intrinsic": 0.0, "end_intrinsic": 1.0,
"gnss_start_index": 0, "gnss_end_index": 0,
"origin": "manual", "path_index": 1
}
],
"overall_probability": 0.89,
"mode": "standalone"
}Implementation note: The original design sent the full ordered segment list from the client on every edit. During implementation this was replaced by two granular endpoints that trigger server-side snap insertion directly.
PUT /api/pathis no longer used by the browser frontend.
Adds a single netelement to the in-memory path. The server calls edit::add_segment() which performs snap insertion using netrelations topology (FR-009). The browser does not need to manage segment ordering.
| Property | Value |
|---|---|
| Method | POST |
| Path | /api/path/add |
| Authentication | None |
| Request content-type | application/json |
| Request body | See schema below |
| Response status | 200 (success) / 404 (netelement not found) / 500 (internal error) |
| Response content-type | application/json |
{ "netelement_id": "NE001" }{ "ok": true }{ "ok": false, "error": "netelement NE999 not found in loaded network" }After a successful response, the browser refreshes both GET /api/path and GET /api/network to reflect the updated state.
Removes a single netelement from the in-memory path. The server calls edit::remove_segment().
| Property | Value |
|---|---|
| Method | POST |
| Path | /api/path/remove |
| Authentication | None |
| Request content-type | application/json |
| Request body | See schema below |
| Response status | 200 (success) / 500 (internal error) |
| Response content-type | application/json |
{ "netelement_id": "NE001" }{ "ok": true }After a successful response, the browser refreshes both GET /api/path and GET /api/network to reflect the updated state.
Writes the current in-memory path to the output file. Standalone mode only. The server remains running after the write.
| Property | Value |
|---|---|
| Method | POST |
| Path | /api/save |
| Authentication | None |
| Request body | {} or empty |
| Response status | 200 (written) / 409 (wrong mode) / 500 (I/O error) |
| Response content-type | application/json |
{ "ok": true, "path": "/home/user/reviewed_path.csv" }The path field contains the absolute path of the file that was written.
{ "ok": false, "error": "save is not available in integrated mode; use /api/confirm instead" }{ "ok": false, "error": "failed to write output file: permission denied" }Signals the CLI process to continue pipeline execution with the current in-memory path. Integrated mode only. After this call succeeds, the server shuts down gracefully.
| Property | Value |
|---|---|
| Method | POST |
| Path | /api/confirm |
| Authentication | None |
| Request body | {} or empty |
| Response status | 200 (confirmed) / 409 (wrong mode or already confirmed) |
| Response content-type | application/json |
{ "ok": true }The response is sent before the server shuts down — the browser receives the acknowledgement.
{ "ok": false, "error": "confirm is not available in standalone mode; use /api/save instead" }{ "ok": false, "error": "already confirmed" }Signals the CLI process to abort and exit with a non-zero exit code. Integrated mode only. After this call succeeds, the server shuts down gracefully.
| Property | Value |
|---|---|
| Method | POST |
| Path | /api/abort |
| Authentication | None |
| Request body | {} or empty |
| Response status | 200 (aborting) / 409 (wrong mode) |
| Response content-type | application/json |
{ "ok": true }The CLI will print a cancellation message to stderr and exit with exit code 1.
{ "ok": false, "error": "abort is not available in standalone mode" }| Endpoint | Standalone | Integrated |
|---|---|---|
GET / |
✅ | ✅ |
GET /api/network |
✅ | ✅ |
GET /api/path |
✅ | ✅ |
POST /api/path/add |
✅ | ✅ |
POST /api/path/remove |
✅ | ✅ |
POST /api/save |
✅ | ❌ 409 |
POST /api/confirm |
❌ 409 | ✅ |
POST /api/abort |
❌ 409 | ✅ |
startup:
1. Bind to 127.0.0.1:<port> (try 8765..8774)
2. Print URL to terminal
3. Attempt to open browser (ignore failure, URL already printed)
4. Begin accepting requests
standalone shutdown:
- Only on CLI process termination (Ctrl+C / SIGINT)
integrated shutdown sequence:
POST /confirm or POST /abort
→ Response 200 sent to browser
→ CancellationToken triggered
→ Axum server task ends
→ oneshot result delivered to CLI await point
→ CLI continues (confirm) or exits non-zero (abort)