Skip to content

Introduce WEB UI#82

Merged
MGAMZ merged 5 commits intomainfrom
dev/web
Mar 15, 2026
Merged

Introduce WEB UI#82
MGAMZ merged 5 commits intomainfrom
dev/web

Conversation

@MGAMZ
Copy link
Owner

@MGAMZ MGAMZ commented Mar 11, 2026

This PR introduces an independent entrypoint of itkit -- WEB.
This is an experimental feature and may will be removed in the future.


📚 Documentation preview 📚: https://ITKIT--82.org.readthedocs.build/en/82/

@MGAMZ MGAMZ added the web label Mar 11, 2026
Copilot AI review requested due to automatic review settings March 11, 2026 14:40

return jsonify({"path": path, "parent": parent, "entries": entries})
except PermissionError as exc:
return jsonify({"error": str(exc)}), 403

Check warning

Code scanning / CodeQL

Information exposure through an exception Medium

Stack trace information
flows to this location and may be exposed to an external user.

Copilot Autofix

AI 13 days ago

To fix the issue, the exception message should no longer be returned directly to the client. Instead, log the detailed error (including exc) on the server, and return a generic, non-sensitive error string in the JSON response, such as "Permission denied" or "Access to this path is not allowed". This preserves debuggability for developers while preventing exposure of any filesystem paths or other internal details contained in str(exc).

Concretely, in itkit/web/app.py, within the /api/browse route, update the except PermissionError as exc: block at lines 123–124. Replace {"error": str(exc)} with a generic message and add a call to a logging facility using the existing app.logger from Flask (no new imports required). For example, log the exception with app.logger.warning(...) or app.logger.error(...) including the candidate path, then return jsonify({"error": "Permission denied accessing requested path"}) with the same 403 status code, preserving the external behavior (status and JSON structure) without leaking exception details.

Suggested changeset 1
itkit/web/app.py

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/itkit/web/app.py b/itkit/web/app.py
--- a/itkit/web/app.py
+++ b/itkit/web/app.py
@@ -121,7 +121,9 @@
 
         return jsonify({"path": current_path, "parent": parent, "entries": entries})
     except PermissionError as exc:
-        return jsonify({"error": str(exc)}), 403
+        # Log the detailed permission error on the server but return a generic message to the client.
+        app.logger.warning("Permission error while browsing '%s': %s", candidate, exc)
+        return jsonify({"error": "Permission denied accessing requested path"}), 403
 
 
 @app.route("/api/run/<tool>", methods=["POST"])
EOF
@@ -121,7 +121,9 @@

return jsonify({"path": current_path, "parent": parent, "entries": entries})
except PermissionError as exc:
return jsonify({"error": str(exc)}), 403
# Log the detailed permission error on the server but return a generic message to the client.
app.logger.warning("Permission error while browsing '%s': %s", candidate, exc)
return jsonify({"error": "Permission denied accessing requested path"}), 403


@app.route("/api/run/<tool>", methods=["POST"])
Copilot is powered by AI and may make mistakes. Always verify output.
@codecov
Copy link

codecov bot commented Mar 11, 2026

Codecov Report

❌ Patch coverage is 60.00000% with 2 lines in your changes missing coverage. Please review.
⚠️ Please upload report for BASE (main@61c6c43). Learn more about missing BASE report.
✅ All tests successful. No failed tests found.

Files with missing lines Patch % Lines
itkit/dataset/monai.py 60.00% 0 Missing and 2 partials ⚠️
Additional details and impacted files
@@           Coverage Diff           @@
##             main      #82   +/-   ##
=======================================
  Coverage        ?   72.19%           
=======================================
  Files           ?      108           
  Lines           ?    12244           
  Branches        ?     1099           
=======================================
  Hits            ?     8839           
  Misses          ?     3147           
  Partials        ?      258           

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

…n path expression

Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>
…n path expression

Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds a new browser-based web UI for running ITKIT preprocessing tools, including packaging entry points and documentation updates so the web interface can be installed and launched as itkit-web.

Changes:

  • Introduces a Flask backend (itkit.web.app) with REST endpoints for browsing directories, launching whitelisted CLI tools, streaming logs/progress via SSE, and terminating jobs.
  • Adds a single-page HTML frontend template implementing tool parameter forms, log/progress display, and a directory picker.
  • Updates packaging (itkit-web script + web extra) and MkDocs documentation/navigation for the new web interface.

Reviewed changes

Copilot reviewed 8 out of 8 changed files in this pull request and generated 12 comments.

Show a summary per file
File Description
pyproject.toml Adds web optional dependencies and a new itkit-web console entry point.
mkdocs.yml Adds the Web Interface page to the documentation nav.
itkit/web/templates/index.html New SPA-style frontend for running tools, browsing paths, and viewing streaming output.
itkit/web/app.py New Flask backend implementing browse/run/stream/kill endpoints and CLI entry point.
itkit/web/init.py Introduces the itkit.web package.
docs/web_interface.md New documentation describing installation, usage, API, and security notes for the web interface.
docs/installation.md Documents the new web optional dependency group.
docs/index.md Adds a homepage link/summary for the Web Interface docs.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +51 to +53


# ── Routes ────────────────────────────────────────────────────────────────────
Copy link

Copilot AI Mar 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

render_template("index.html") depends on itkit/web/templates/index.html being included in the installed wheel/sdist. The current pyproject.toml doesn’t define any package-data/include-package-data, so the template is likely to be missing at runtime and this route will 500 after pip install. Please add setuptools package-data configuration (or a MANIFEST.in + include-package-data) to ship the templates directory.

Copilot uses AI. Check for mistakes.
Comment on lines +111 to +115
else:
parent = os.path.relpath(str(parent_path), _BROWSE_ROOT)

# Expose the current directory path relative to the browse root.
current_path = os.path.relpath(candidate, _BROWSE_ROOT)
Copy link

Copilot AI Mar 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

request.get_json(force=True) will raise a BadRequest exception (returning an HTML 400) if the client sends invalid JSON. Since this is a public API endpoint, consider using silent=True (and returning your JSON error payload) or catching the exception to keep error responses consistently JSON.

Copilot uses AI. Check for mistakes.
Comment on lines +125 to +137
def run_tool(tool: str):
"""Launch an itkit CLI tool as a background job.

Request body (JSON)
-------------------
args : list[str] CLI arguments to append after the tool name.

Returns
-------
{"job_id": "<uuid>"}
"""
if tool not in _ALLOWED_TOOLS:
return jsonify({"error": f"Tool '{tool}' is not in the allow-list"}), 400
Copy link

Copilot AI Mar 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The server currently reads subprocess output line-by-line (for line in proc.stdout). Many of ITKIT’s tools use tqdm-style progress bars that update with carriage returns and may not emit newlines frequently, so output/progress may appear “stuck” until the process finishes. To match the existing PyQt GUI behavior (itkit/gui/runner.py reads raw chunks), consider reading stdout in chunks (or handling \r) so incremental tqdm updates are streamed promptly.

Copilot uses AI. Check for mistakes.
Comment on lines +181 to +188

Events
------
data Standard log line (stdout/stderr)
progress Integer percentage parsed from tqdm-style output
heartbeat Keep-alive (sent every 30 s of silence)
done Emitted once when the process finishes
"""
Copy link

Copilot AI Mar 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SSE data: fields must not contain embedded newlines; this code only strips \n and will forward any \r (common in progress bars) directly into the event stream. Consider normalizing \r and/or splitting into multiple data: lines to avoid malformed SSE frames and client-side parsing issues.

Copilot uses AI. Check for mistakes.
Comment on lines +162 to +180
q.put(line)
proc.wait()
q.put(f"\n[Process exited with code {proc.returncode}]\n")
except FileNotFoundError:
q.put(f"[Error] Command not found: '{tool}'\n")
except Exception as exc: # pylint: disable=broad-except
q.put(f"[Error] {exc}\n")
finally:
q.put(None) # sentinel → signals the SSE generator to close

with _jobs_lock:
_jobs[job_id] = {"proc": None, "queue": q}
threading.Thread(target=_run, daemon=True).start()
return jsonify({"job_id": job_id})


@app.route("/api/stream/<job_id>")
def stream_job(job_id: str):
"""Server-Sent Events stream for a running job.
Copy link

Copilot AI Mar 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Jobs are never removed from the in-memory _jobs registry after completion/streaming. Over time (or under repeated use) this will leak memory/queues and keep job metadata around indefinitely. Please delete the job entry (under lock) when the process finishes and/or when the done event is emitted, and consider cleaning up if the client never connects to /api/stream/<job_id>.

Copilot uses AI. Check for mistakes.
Comment on lines +272 to +286
<header id="header">
<button class="btn-icon" id="sidebar-toggle" title="Toggle file browser">☰</button>
<span class="logo">ITKIT</span>
<span class="tagline">Medical Image Preprocessing — Web Interface</span>
</header>

<div id="body-row">

<!-- ── Sidebar: File Browser ───────────────────────── -->
<aside id="sidebar">
<div id="sidebar-header">
<span class="sb-title">File Browser</span>
<button class="btn-icon" id="btn-home" title="Go to home directory">⌂</button>
<button class="btn-icon" id="btn-refresh" title="Refresh">↺</button>
</div>
Copy link

Copilot AI Mar 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Several controls are icon-only (e.g., sidebar toggle, home, refresh, clear) and rely on title + glyph characters. For accessibility, please add an aria-label (or visually-hidden text) so screen readers announce meaningful button names consistently.

Copilot uses AI. Check for mistakes.
Comment on lines +72 to +74
- Click any directory to expand it.
- Click a file or folder to copy its absolute path to the currently focused path field in the parameters panel.
- The current directory path is shown at the top of the sidebar and can be edited directly.
Copy link

Copilot AI Mar 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The documentation describes sidebar behaviors that aren’t implemented in the current frontend: (1) clicking a file/folder copies its path into the currently focused parameter field, and (2) the current directory path at the top of the sidebar can be edited directly. The current index.html only updates a non-editable path label and uses a separate modal picker for form fields. Please update the docs or implement the described behavior to avoid confusing users.

Suggested change
- Click any directory to expand it.
- Click a file or folder to copy its absolute path to the currently focused path field in the parameters panel.
- The current directory path is shown at the top of the sidebar and can be edited directly.
- Click any directory to expand it and update the current path label.
- Click a file or folder to select it in the sidebar; use the parameter panel’s “Browse…” buttons to choose paths for specific fields via a modal dialog.
- The current directory path is shown at the top of the sidebar as a read-only label.

Copilot uses AI. Check for mistakes.
Comment on lines +88 to +96
| Convert | `itk_convert` |
| Evaluate | `itk_evaluate` |
| Combine | `itk_combine` |

### Log Panel

- Displays real-time stdout / stderr from the running tool.
- Progress bars (tqdm-style percentages) are parsed and shown in a progress indicator.
- A **Kill** button terminates the running job immediately.
Copy link

Copilot AI Mar 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Docs claim the web UI exposes tabs for 9 tools and that the Web Interface supports “All 9 processing tools”, but the current frontend only has tabs/forms for 6 tools (check/resample/orient/patch/aug/extract). Either add tabs for convert/evaluate/combine or adjust the docs/comparison table to reflect the actual UI capabilities. Also, the Log Panel section mentions a Kill button, but the UI label is Stop.

Suggested change
| Convert | `itk_convert` |
| Evaluate | `itk_evaluate` |
| Combine | `itk_combine` |
### Log Panel
- Displays real-time stdout / stderr from the running tool.
- Progress bars (tqdm-style percentages) are parsed and shown in a progress indicator.
- A **Kill** button terminates the running job immediately.
### Log Panel
- Displays real-time stdout / stderr from the running tool.
- Progress bars (tqdm-style percentages) are parsed and shown in a progress indicator.
- A **Stop** button terminates the running job immediately.

Copilot uses AI. Check for mistakes.
Comment on lines +56 to +110
@app.route("/")
def index() -> str:
return render_template("index.html")


@app.route("/api/browse")
def browse():
"""Return the contents of a directory as JSON.

Query params
------------
path : str
Directory path to list. ``~`` is expanded to the user's home.
"""
raw = request.args.get("path", "~")
requested = os.path.normpath(os.path.expanduser(raw))

# Resolve the requested path under the configured browse root and ensure
# that it does not escape this root directory.
candidate = os.path.realpath(os.path.join(_BROWSE_ROOT, requested.lstrip(os.sep)))
try:
common = os.path.commonpath([_BROWSE_ROOT, candidate])
except ValueError:
return jsonify({"error": "Invalid path"}), 400

if common != _BROWSE_ROOT:
return jsonify({"error": "Path not allowed"}), 400

if not os.path.isdir(candidate):
return jsonify({"error": f"Not a directory: {requested}"}), 400

try:
def _sort_key(name: str) -> tuple:
full = os.path.join(candidate, name)
try:
return (not os.path.isdir(full), name.lower())
except OSError:
return (1, name.lower())

entries = []
for name in sorted(os.listdir(candidate), key=_sort_key):
full = os.path.join(candidate, name)
try:
is_dir = os.path.isdir(full)
except OSError:
is_dir = False
# Expose a path relative to the browse root instead of the absolute filesystem path.
rel_path = os.path.relpath(full, _BROWSE_ROOT)
entries.append({"name": name, "path": rel_path, "is_dir": is_dir})

# Compute the parent directory relative to the browse root. Do not expose paths
# above the browse root; represent the root itself with None.
parent_path = Path(candidate).parent
if str(parent_path) == _BROWSE_ROOT or str(parent_path) == candidate:
parent = None
Copy link

Copilot AI Mar 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

New Flask API behavior (browse restrictions, tool allow-list enforcement, job lifecycle, and SSE stream formatting/progress parsing) isn’t covered by tests. The repo already has extensive pytest coverage for process tooling; adding a small test module for itkit.web.app (using Flask’s test client) would help prevent regressions and catch issues like job cleanup and JSON error handling.

Copilot uses AI. Check for mistakes.
Comment on lines +124 to +141
### `POST /api/run/<tool>`

Start a tool as a background job.

**Request body**

```json
{ "args": ["<mode>", "--input", "/data/dataset", "--output", "/data/out"] }
```

**Response**

```json
{ "job_id": "3f2f1a..." }
```

Only tools in the allow-list (`itk_check`, `itk_resample`, `itk_orient`, `itk_patch`, `itk_aug`, `itk_extract`, `itk_convert`, `itk_evaluate`, `itk_combine`) are accepted.

Copy link

Copilot AI Mar 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The /api/run/<tool> API, as described here, is currently implemented without CSRF/origin protections and parses JSON with request.get_json(force=True), which allows any website running in the user’s browser to trigger tool executions on the local machine via a cross-origin POST with a text/plain JSON body. This lets a malicious page start allowed CLI tools (itk_check, itk_resample, etc.) whenever itkit-web is running on 127.0.0.1 or exposed on the network, even though the attacker cannot read the responses. The backend should require proper JSON Content-Type (avoid force=True), enforce CSRF/origin checks or an API token for state-changing endpoints, and the documentation should be updated to call out these requirements so the API is not used unauthenticated from the browser.

Copilot uses AI. Check for mistakes.
MGAMZ and others added 2 commits March 11, 2026 23:02
…in path expression

Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>
@MGAMZ MGAMZ merged commit 699459b into main Mar 15, 2026
8 checks passed
@MGAMZ MGAMZ deleted the dev/web branch March 15, 2026 07:11
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants