Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 12 additions & 2 deletions .github/skills/server-api.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,11 +39,11 @@ Served with **uvicorn** via `fastapi_offline.FastAPIOffline`.
| `POST` | `/refresh-dashboard` | Basic (if configured) | Manually triggers `robotdashboard.create_dashboard()` without adding data |
| `GET` | `/get-outputs` | None | Returns list of `{run_start, name, alias, tags}` for all stored runs |
| `POST` | `/add-outputs` | Basic (if configured) | Add output(s) from a path, raw XML string, or folder. See body fields below. |
| `POST` | `/add-output-file` | Basic (if configured) | Multipart upload of `output.xml` (or `.gz`/`.gzip`). Form fields: `tags` (colon-separated), `version`. |
| `POST` | `/add-output-file` | Basic (if configured) | Multipart upload of `output.xml` (or `.gz`/`.gzip`). Form fields: `tags` (colon-separated), `version`, `custom_filters`, `log_url`. |
| `DELETE` | `/remove-outputs` | Basic (if configured) | Remove runs by various selectors. See body fields below. |
| `GET` | `/get-logs` | None | Lists filenames in `robot_logs/` |
| `POST` | `/add-log` | Basic (if configured) | Saves HTML log content to `robot_logs/<log_name>` and links it to the matching run in the DB |
| `POST` | `/add-log-file` | Basic (if configured) | Same as `/add-log` but via multipart file upload (supports `.gz`/`.gzip`) |
| `POST` | `/add-log-file` | Basic (if configured) | Same as `/add-log` but via multipart file upload (supports `.gz`/`.gzip`). Filenames containing `report` are saved as-is with no DB matching — see Report Handling below. |
| `DELETE` | `/remove-log` | Basic (if configured) | Removes one log by `log_name`, or all logs with `all: True` |
| `GET` | `/log` | None | Serves a log HTML file by `?path=` query param; stores parent dir for subsequent resource requests |
| `GET` | `/{full_path:path}` | None | Catch-all: serves static resources (screenshots, etc.) relative to the last served log's directory. Path-traversal protected. |
Expand All @@ -63,6 +63,7 @@ Exactly one input source must be provided (mutually exclusive):
| `output_tags` | `List[str]` | Optional tags to attach to all added runs |
| `output_alias` | `str` | Optional alias override |
| `output_version` | `str` | Optional project version string |
| `output_log_url` | `str` | Optional externally-hosted log URL, mirrors CLI `--logurl`. Requires a `{run_alias}` placeholder when `output_folder_path` is used (potentially multiple runs); rejected without one. `/add-output-file`'s multipart equivalent is the unprefixed `log_url` form field (no placeholder requirement there since it always processes a single file). |

### `DELETE /remove-outputs`
Any combination of the following:
Expand Down Expand Up @@ -112,6 +113,15 @@ The suffix `XYZ` must match between the output file and its corresponding log. T

---

## Report Handling (`/add-log-file`)

Reports are not tracked in the database — Robot Framework's own `log.html` already links to `report.html` (top-right corner), so the dashboard just needs the report saved next to its log with a matching name (`log_XYZ.html` ↔ `report_XYZ.html`).

`/add-log-file` detects a `report` substring in the uploaded filename and takes a different path than for `log` files:
- The file is saved to `robot_logs/` as-is; `update_output_path()` (DB matching) is **not** called — a report filename would never match a stored output/log path anyway.
- It checks whether a `log` file exists with the same name (`report` → `log`) in `robot_logs/`. If found: `SUCCESS` with a console note that the report is reachable from its log. If not found: still `SUCCESS`, but the console carries a `WARNING` that the report was saved but has no matching log yet (upload order matters — upload the log first, or the report before it just produces a harmless warning).
- This intentionally returns `success: "1"` either way; before this behavior, a `report*.html` upload always returned `success: "0"` with a misleading DB-matching error even though the file was saved correctly (#308).

## Server State

The `ApiServer` instance holds:
Expand Down
4 changes: 4 additions & 0 deletions docs/dashboard-server.md
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,8 @@ The admin page supports four methods for adding test results:
| **By Folder Path** | Provide a folder path; the server recursively scans for `*output*.xml` files. Supports run tags, version label, and custom filters. |
| **By File Upload** | Upload an `output.xml` file directly. Supports run tags, version label, and custom filters. Gzip-compressed files (`.gz`/`.gzip`) are automatically decompressed. |

> **Tip:** Every method above also accepts an optional log URL (`output_log_url` in `/add-outputs`, `log_url` in `/add-output-file`) mirroring the CLI's [`--logurl`](/basic-command-line-interface-cli.md) flag. Use this when the log is already hosted elsewhere (e.g. a CI artifact or cloud storage URL) instead of uploading it to this server via `/add-log` / `/add-log-file`. Include a `{run_alias}` placeholder in the URL when a single request may add more than one run (e.g. `output_folder_path`), otherwise every run added by that request would be stored with the same URL.

### Removing Outputs

| Method | Description |
Expand All @@ -176,6 +178,8 @@ The admin page supports four methods for adding test results:
| **Remove Log by Name** | Remove a specific log file (e.g., `log-20250219-172535.html`). |
| **Remove All Logs** | Irreversibly deletes all uploaded log files. |

> **Note:** Reports are not tracked in the database — they're reached through the link Robot Framework builds into the top-right corner of `log.html`, so `/add-log` and `/add-log-file` never need to associate a report with a run directly. Uploading a filename containing `report` (e.g. `report_20250219-172535.html`) via `/add-log-file` is still supported: the file is saved to `robot_logs/` as-is and no database matching is attempted. The response reports `SUCCESS` either way, with the console message noting whether a matching `log` file (same name, `report` replaced by `log`) was found in `robot_logs/` — if not, it's just a warning that the report may not yet be reachable from a log, not an error. See [Log Linking](/log-linking.md#accessing-reports) for naming and placement requirements.

### Database & Log Tables

The admin page displays two tables:
Expand Down
2 changes: 2 additions & 0 deletions docs/log-linking.md
Original file line number Diff line number Diff line change
Expand Up @@ -151,3 +151,5 @@ Robot Framework `report.html` files can also be accessed through the log file:
| --------------------------------- | ---------------------------------- |
| `log_nightly.html` | `report_nightly.html` |
| `my_log_123.html` | `my_report_123.html` |

When uploading through the [dashboard server](/dashboard-server.md), upload the report the same way as the log: `POST /add-log-file` with the report file. Reports are not stored in the database, so no output-matching is attempted for a filename containing `report` — the file is just saved to `robot_logs/`. Upload the report after its matching log so the response can confirm the log was found; uploading it first only produces a harmless warning that no matching log exists yet.
5 changes: 4 additions & 1 deletion example/server/interact.http
Original file line number Diff line number Diff line change
Expand Up @@ -38,14 +38,17 @@ content-type: application/json
###

### add an output by folder path — all *output*.xml files are picked up recursively (auth required)
### output_log_url mirrors the CLI '--logurl' flag; the '{run_alias}' placeholder is required here
### because a folder can contain multiple outputs, so every run needs its own URL
POST http://{{host}}:{{port}}/add-outputs HTTP/1.1
Authorization: Basic {{base64credentials}}
content-type: application/json

{
"output_folder_path": "C:\\users\\docs\\prod-outputs",
"output_tags": ["production-run"],
"output_version": "v1.2.3"
"output_version": "v1.2.3",
"output_log_url": "https://ci.example.com/build42/log_{run_alias}.html"
}

###
Expand Down
5 changes: 4 additions & 1 deletion example/server/interact.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,10 +45,13 @@
print()

# add an output by folder path — all *output*.xml files are picked up recursively
# output_log_url mirrors the CLI '--logurl' flag; the '{run_alias}' placeholder is required here
# because a folder can contain multiple outputs, so every run needs its own URL
body = {
"output_folder_path": str(OUTPUT_FOLDER_PATH),
"output_tags": ["production-run"],
"output_version": "v1.2.3",
"output_log_url": "https://ci.example.com/build42/log_{run_alias}.html",
}
response = requests.post(f"{BASE_URL}/add-outputs", json=body, auth=AUTH)
print("add-outputs (by folder):", response.json())
Expand All @@ -60,7 +63,7 @@
from gzip import compress
compressed = compress(f.read())
files = {"file": (f"{output_path.name}.gz", compressed, "application/gzip")}
form = {"tags": "tag1:tag2", "version": "v1.2.3", "custom_filters": "env=prod"}
form = {"tags": "tag1:tag2", "version": "v1.2.3", "custom_filters": "env=prod", "log_url": "https://ci.example.com/build42/log.html"}
response = requests.post(f"{BASE_URL}/add-output-file", files=files, data=form, auth=AUTH)
print("add-output-file:", response.json())
print()
Expand Down
3 changes: 3 additions & 0 deletions example/server/interact.robot
Original file line number Diff line number Diff line change
Expand Up @@ -43,11 +43,14 @@ Add Output By Path With All Options

Add Output By Folder Path
[Documentation] Add all *output*.xml files found recursively in a folder (auth required)
... output_log_url mirrors the CLI '--logurl' flag; the '{run_alias}' placeholder is required
... here because a folder can contain multiple outputs, so every run needs its own URL
VAR @{tags} production-run
VAR &{body}
... output_folder_path=C:\\users\\docs\\prod-outputs
... output_tags=${tags}
... output_version=v1.2.3
... output_log_url=https://ci.example.com/build42/log_{run_alias}.html
${response} POST url=${URL}/add-outputs json=${body} auth=${AUTH}
Log ${response.json()}

Expand Down
63 changes: 57 additions & 6 deletions robotframework_dashboard/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,14 @@
"output_version": "v1.2",
},
},
"output_with_log_url": {
"summary": "Add an output with an externally hosted log URL",
"description": "Provide an output.xml path along with 'output_log_url' to store a link to a log already hosted elsewhere (e.g., a CI artifact or cloud storage URL) instead of a local log file. Use the '{run_alias}' placeholder when processing multiple outputs (e.g. via 'output_folder_path') so each run gets its own URL.",
"value": {
"output_path": "C:\\users\\docs\\output.xml",
"output_log_url": "https://ci.example.com/build42/log.html",
},
},
},
}
}
Expand Down Expand Up @@ -250,6 +258,7 @@ class AddOutput(BaseModel):
output_alias: Optional[str] = None
output_version: Optional[str] = None
output_custom_filters: Optional[str] = None
output_log_url: Optional[str] = None
model_config = add_output_model_config


Expand Down Expand Up @@ -465,6 +474,10 @@ async def add_output_to_database(
1. output_path: str valid path to output.xml (+ optional 'output_tags: List[str]' or optional 'output_version: str' version)
2. output_data: str output.xml content (+ optional 'output_tags: List[str]', optional 'output_alias: str` or optional 'output_version: str' version)
3. output_folder_path: str valid path to folder (subfolders are also searched) that contain *output*.xml (+ optional 'output_tags: List[str]' or optional 'output_version: str' version)
'output_log_url' is optional on all 3 combinations and mirrors the CLI '--logurl' flag: it stores a link to an
externally hosted log instead of the local output path. Use the '{run_alias}' placeholder when the request may
process more than one output (e.g. via 'output_folder_path'), otherwise every run would be stored with the
same URL.
"""
input = "provided input, overwritten on runtime"
console = "no console output"
Expand All @@ -487,13 +500,25 @@ async def add_output_to_database(
raise Exception(
"Please only provide output_path, output_data or output_folder_path, not more than 1 type at the same time!"
)
if (
add_output.output_log_url
and "{run_alias}" not in add_output.output_log_url
and add_output.output_folder_path != None
):
input = "your input"
raise Exception(
"'output_log_url' was provided without a '{run_alias}' placeholder while processing an output_folder_path "
"(potentially multiple outputs). Either add '{run_alias}' to the URL template or provide a single output "
"via 'output_path' or 'output_data'."
)
output_tags = []
if add_output.output_tags:
output_tags = add_output.output_tags
if add_output.output_version:
self.robotdashboard.project_version = add_output.output_version
else:
self.robotdashboard.project_version = None
self.robotdashboard.log_url = add_output.output_log_url or None
self.robotdashboard.custom_filters = add_output.output_custom_filters
if add_output.output_path != None:
input = add_output.output_path
Expand Down Expand Up @@ -547,11 +572,15 @@ async def add_output_file(
tags: str = Form(default=""),
version: str = Form(default=""),
custom_filters: str = Form(default=""),
log_url: str = Form(default=""),
username: str = Depends(authenticate),
) -> ResponseMessage:
"""Add output file to database endpoint function
The tags parameter should be provided as colon-separated values (e.g., 'tag1:tag2:tag3')
The version parameter is an optional string to label the run (e.g., software version)
The log_url parameter is optional and mirrors the CLI '--logurl' flag: it stores a link to an externally
hosted log instead of the local output path. Since this endpoint always processes a single output file,
the '{run_alias}' placeholder is optional here.
"""
console = "no console output"
try:
Expand Down Expand Up @@ -579,6 +608,7 @@ async def add_output_file(
self.robotdashboard.project_version = None

self.robotdashboard.custom_filters = custom_filters
self.robotdashboard.log_url = log_url or None

outputs = [[output_path, output_tags]]
console = self.robotdashboard.process_outputs(
Expand Down Expand Up @@ -740,6 +770,11 @@ async def add_log_file(
) -> ResponseMessage:
"""Add log file to server endpoint function
The log file name should match the output.xml alias (e.g., 'log-alias.html' for 'output-alias.xml')

Report files (filename containing 'report') are handled differently: reports are not tracked in the
database, so no output-matching is attempted. The file is saved as-is and this endpoint only checks
whether a corresponding 'log' file already exists next to it (so it can be reached via the link Robot
Framework builds into log.html) and warns, rather than errors, if it does not.
"""
console = ""
try:
Expand All @@ -759,11 +794,23 @@ async def add_log_file(
with open(log_path, "wb") as buffer:
buffer.write(file_bytes)

console += self.robotdashboard.update_output_path(log_path)
if "ERROR" in console:
raise Exception(
"A problem occurred while adding the log file, check the console message!"
)
log_name = Path(log_path).name
is_report = "report" in log_name
if is_report:
expected_log_name = log_name.replace("report", "log")
if exists(join(self.log_dir, expected_log_name)):
console += f"SUCCESS: matching log file '{expected_log_name}' found, the report is reachable from it.\n"
else:
console += (
f"WARNING: no matching log file '{expected_log_name}' was found in '{self.log_dir}'. "
"The report has been saved but may not be reachable until a matching log is uploaded.\n"
)
else:
console += self.robotdashboard.update_output_path(log_path)
if "ERROR" in console:
raise Exception(
"A problem occurred while adding the log file, check the console message!"
)
console += "======================================================================================\n"
console += f"Added {file.filename} to the folder {self.log_dir}\n"
console += "======================================================================================\n"
Expand All @@ -782,7 +829,11 @@ async def add_log_file(
return response
response = {
"success": "1",
"message": f"SUCCESS: the log file has been placed and the database was updated",
"message": (
"SUCCESS: the report file has been placed"
if is_report
else "SUCCESS: the log file has been placed and the database was updated"
),
"console": console,
}
return response
Expand Down
Loading
Loading