Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
d54b296
feat: add job running functionality and update API configurations
khoaguin Oct 9, 2025
795b491
fix: getting wrong ports when running multiple instances of
khoaguin Oct 10, 2025
2353a78
run jobs in non-blocking mode. Add running job component to ui
khoaguin Oct 10, 2025
e34f39b
ui: job view now have the column layout
khoaguin Oct 10, 2025
98ada32
feat: delete and retrieve job details, and implement job logs retrieval
khoaguin Oct 10, 2025
9914358
feat: implement account information retrieval and update related comp…
khoaguin Oct 13, 2025
ba9cb07
fix: device theme for the dashboard
khoaguin Oct 14, 2025
47af77c
feat: taling logs for running jobs
khoaguin Oct 14, 2025
c7f4ecb
feat: integrate colorized logs component into job logs dialog
khoaguin Oct 14, 2025
fc9a8ad
fix: job reject
khoaguin Oct 15, 2025
733dafc
feat: implement job code retrieval and display in JobCodeDialog html
khoaguin Oct 15, 2025
6b9a0be
feat: enhance JobCodeDialog and JobDetailsDialog with improved file s…
khoaguin Oct 15, 2025
8e9154f
frontend: add clipboard copy functionality and improve layout in JobC…
khoaguin Oct 15, 2025
1df6fd5
frontend: syntax highlighting for `View Code`
khoaguin Oct 15, 2025
98b4004
frontend: add clipboard copy functionality to JobLogsDialog and impro…
khoaguin Oct 16, 2025
70a8a6f
feat: add functionality to delete all jobs and update related UI comp…
khoaguin Oct 16, 2025
33cd5aa
feat: add job output retrieval functionality and enhance UI component…
khoaguin Oct 16, 2025
8eaf4a6
frontend: update button styles in JobCodeDialog, JobLogsDialog, and J…
khoaguin Oct 16, 2025
2c45ee3
feat: implement dataset file retrieval and enhance dataset view with …
khoaguin Oct 20, 2025
e1de394
frontend: new build
khoaguin Oct 20, 2025
ea7b2af
feat: Docker support for RDS Dashboard with SyftBox integration
khoaguin Oct 23, 2025
5aabd24
feat: add rerun functionality for jobs with UI integration
khoaguin Nov 3, 2025
c8c0953
update deps
khoaguin Nov 6, 2025
18efaa2
add built static frontend
khoaguin Nov 6, 2025
64d0d72
update Dockerfile and scripts for deployment
khoaguin Nov 6, 2025
1b62645
feat: add APP_VERSION and API_PORT support in Docker setup
khoaguin Nov 6, 2025
1a1824a
dynamically set HOME and APP_USER environment variables
khoaguin Nov 6, 2025
0909d4d
fix: update package installation to include upgrades for dependencies
khoaguin Nov 6, 2025
7824b53
dataset service uses syft-rds v0.5.0
khoaguin Nov 7, 2025
8b4a4d4
update deps
khoaguin Nov 7, 2025
0ab651e
fix: resolve Docker dataset upload permission issues and update to sy…
khoaguin Nov 7, 2025
3210052
updated built frontend
khoaguin Nov 7, 2025
42c6614
update built frontend
khoaguin Nov 10, 2025
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
97 changes: 97 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
# Git
.git
.gitignore
.gitattributes

# Docker
docker/
Dockerfile*
.dockerignore
docker-compose*.yml

# Documentation (not needed in container)
*.md
docs/
.github/

# Environment files (credentials should never be in image)
.env
.env.*
*.env

# Python
__pycache__/
*.py[cod]
*$py.class
*.so
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
pip-wheel-metadata/
share/python-wheels/
*.egg-info/
.installed.cfg
*.egg
MANIFEST
.venv/
venv/
ENV/
env/

# Node.js
node_modules/
npm-debug.log*
yarn-debug.log*
yarn-error.log*
.npm
.eslintcache
.next/
# Note: frontend/out/ is needed for Docker (pre-built static files)

# IDE
.vscode/
.idea/
*.swp
*.swo
*.swn
.DS_Store

# Testing
.pytest_cache/
.coverage
.tox/
htmlcov/
*.cover
.hypothesis/

# SyftBox (user data, not needed in build)
SyftBox/
.syftbox/

# Logs
*.log

# Temporary files
*.tmp
*.bak
*.orig
.cache/

# CI/CD
.travis.yml
.gitlab-ci.yml

# Other
LICENSE
AUTHORS
CONTRIBUTORS
8 changes: 8 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -178,4 +178,12 @@ cython_debug/
# local directory
local/

# MacOS files
.DS_Store

# AI artifacts
.claude/

# dev docs and scripts
docs/plans/
scripts/dev/
31 changes: 27 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@

The app will be available at:
- Frontend: http://localhost:3000
- Backend API: http://localhost:8001
- Backend API: http://localhost:8000

Both frontend and backend have hot-reload enabled during development.

Expand All @@ -40,14 +40,14 @@ To run multiple dashboards simultaneously (e.g., for testing with different Syft
```bash
# Terminal 1 - Instance 1
just dev "/path/to/client1.config.json"
# → Backend: :8001, Frontend: :3000
# → Frontend: :3000, Backend: :8000

# Terminal 2 - Instance 2
just dev "/path/to/client2.config.json"
# → Backend: :8002, Frontend: :3001 (auto-incremented)
# → Frontend: :3001, Backend: :8001 (auto-incremented)
```

Each instance automatically gets its own ports. The frontend determines the backend port at runtime using the formula: `backend_port = 8001 + (frontend_port - 3000)`.
Each instance automatically gets its own ports. The backend port is always calculated as: `backend_port = frontend_port + 5000`. The frontend auto-detects the correct API URL based on the `DEBUG` environment variable (same as the backend).

**Debug**: Check API configuration in browser console:
```javascript
Expand All @@ -58,3 +58,26 @@ window.apiConfig.reset() // Reset if needed
## Build

Run `just prod` to export the frontend into a static build and start the FastAPI backend server.

## Note: Frontend Build Output (`frontend/out/`)

**⚠️ The `frontend/out/` directory is intentionally committed to git.**

This is required for SyftBox app installation:

1. **When users install via**: `syftbox app install https://github.com/OpenMined/rds-dashboard`
- SyftBox clones this repository
- Executes `run.sh` which only sets up Python environment (no frontend build step)
- FastAPI serves pre-built static files from `frontend/out/`
- [SyftUI](https://github.com/OpenMined/SyftUI) embeds the app (dashboard) in an iframe

2. **Deployment models**:
- **SyftBox Install**: Requires pre-built `frontend/out/` in git ✅
- **Docker**: Builds frontend during image creation (doesn't need committed build, but harmless)

**Before committing frontend changes**, always rebuild:
```bash
bun run --cwd frontend build
```

This ensures users installing via SyftBox get the latest frontend.
3 changes: 2 additions & 1 deletion backend/api/client_factory.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,8 @@ def create_rds_client() -> RDSClient:
)

rds_client = init_session(
host=syftbox_client.email, syftbox_client=syftbox_client, start_rds_server=True
host=syftbox_client.email,
email=syftbox_client.email,
)
logger.debug(
f"Initialized RDS client for {syftbox_client.email}. Is admin: {rds_client.is_admin}"
Expand Down
3 changes: 2 additions & 1 deletion backend/api/main.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
from typing import Dict
from fastapi import APIRouter
from .routers import datasets, jobs, trusted_datasites
from .routers import account, datasets, jobs, trusted_datasites


v1_router = APIRouter(prefix="/v1")

v1_router.include_router(account.router)
v1_router.include_router(datasets.router)
v1_router.include_router(jobs.router)
v1_router.include_router(trusted_datasites.router)
Expand Down
3 changes: 2 additions & 1 deletion backend/api/routers/__init__.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from . import datasets, jobs, trusted_datasites
from . import account, datasets, jobs, trusted_datasites

__all__ = [
"account",
"datasets",
"jobs",
"trusted_datasites",
Expand Down
35 changes: 35 additions & 0 deletions backend/api/routers/account.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
"""Router for account/client information endpoints."""

from fastapi import APIRouter, Depends
from pydantic import BaseModel
from syft_rds import RDSClient

from ..dependencies import get_rds_client


router = APIRouter(prefix="/account", tags=["account"])


class AccountInfoResponse(BaseModel):
"""Response model for account information."""

email: str
is_admin: bool
host_datasite_url: str


@router.get(
"",
summary="Get account information",
response_model=AccountInfoResponse,
description="Retrieve the current account's email and admin status",
)
async def get_account_info(
rds_client: RDSClient = Depends(get_rds_client),
) -> AccountInfoResponse:
"""Get the current account information."""
return AccountInfoResponse(
email=rds_client.email,
is_admin=rds_client.is_admin,
host_datasite_url=rds_client.host_datasite_url,
)
11 changes: 6 additions & 5 deletions backend/api/routers/datasets.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import traceback
from typing import Optional
from typing import Literal, Optional

from fastapi import APIRouter, Depends, File, Form, HTTPException, UploadFile
from fastapi.responses import JSONResponse, StreamingResponse
Expand Down Expand Up @@ -176,11 +176,12 @@ async def download_dataset_private(
return await service.download_private_file(dataset_uuid)


@router.get("/open-local-directory/{dataset_uid}")
async def open_local_directory(
@router.get("/files/{dataset_uid}")
async def get_dataset_files(
dataset_uid: str,
dataset_type: Literal["private", "mock"] = "private",
rds_client: RDSClient = Depends(get_rds_client),
):
"""Get the file structure and contents of a dataset."""
service = DatasetService(rds_client)
await service.open_local_directory(dataset_uid)
return {"message": f"Opened local directory for dataset {dataset_uid}"}
return await service.get_dataset_files(dataset_uid, dataset_type=dataset_type)
127 changes: 120 additions & 7 deletions backend/api/routers/jobs.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,15 +60,128 @@ async def reject_job(


@router.get(
"/open-code/{job_uid}",
summary="Open job code in browser",
description="Open the code directory for a specific job in the default file browser",
"/code/{job_uid}",
summary="Get job code",
description="Retrieve the code files for a specific job",
status_code=status.HTTP_200_OK,
)
async def get_job_code(
job_uid: str,
rds_client: RDSClient = Depends(get_rds_client),
):
"""Get job code files and their contents."""
service = JobService(rds_client)
return await service.get_job_code(job_uid)


@router.post(
"/run/{job_uid}",
summary="Run an approved job",
description="Execute an approved job on private data in the background",
status_code=status.HTTP_200_OK,
)
async def run_job(
job_uid: str,
rds_client: RDSClient = Depends(get_rds_client),
):
"""Run an approved job on private data."""
service = JobService(rds_client)
await service.run(job_uid)
return JSONResponse(content={"message": f"Job {job_uid} started."}, status_code=200)


@router.post(
"/rerun/{job_uid}",
summary="Rerun a finished or failed job",
description="Re-approve and re-execute a finished or failed job on private data in the background",
status_code=status.HTTP_200_OK,
)
async def rerun_job(
job_uid: str,
rds_client: RDSClient = Depends(get_rds_client),
):
"""Rerun a finished or failed job."""
service = JobService(rds_client)
await service.rerun(job_uid)
return JSONResponse(
content={"message": f"Job {job_uid} restarted."}, status_code=200
)


@router.delete(
"/{job_uid}",
summary="Delete a job",
description="Delete a job by its UID",
status_code=status.HTTP_200_OK,
)
async def delete_job(
job_uid: str,
rds_client: RDSClient = Depends(get_rds_client),
):
"""Delete a job."""
service = JobService(rds_client)
await service.delete(job_uid)
return JSONResponse(content={"message": f"Job {job_uid} deleted."}, status_code=200)


@router.get(
"/logs/{job_uid}",
summary="Get job logs",
description="Retrieve stdout and stderr logs for a specific job",
status_code=status.HTTP_200_OK,
)
async def open_job_code(
async def get_job_logs(
job_uid: str,
rds_client: RDSClient = Depends(get_rds_client),
):
"""Open job code directory in the system file browser."""
"""Get stdout and stderr logs for a job."""
service = JobService(rds_client)
await service.open_job_code(job_uid)
return {"message": f"Opened code directory for job {job_uid}"}
return await service.get_logs(job_uid)


@router.get(
"/output/{job_uid}",
summary="Get job output files",
description="Retrieve all output files and their contents for a specific job",
status_code=status.HTTP_200_OK,
)
async def get_job_output(
job_uid: str,
rds_client: RDSClient = Depends(get_rds_client),
):
"""Get job output files and their contents."""
service = JobService(rds_client)
return await service.get_output_files(job_uid)


@router.get(
"/{job_uid}",
summary="Get job details",
description="Get detailed metadata for a specific job by its UID",
status_code=status.HTTP_200_OK,
)
async def get_job(
job_uid: str,
rds_client: RDSClient = Depends(get_rds_client),
):
"""Get detailed job metadata."""
service = JobService(rds_client)
return await service.get_job(job_uid)


@router.delete(
"",
summary="Delete all jobs",
description="Delete all jobs from the system",
status_code=status.HTTP_200_OK,
)
async def delete_all_jobs(
rds_client: RDSClient = Depends(get_rds_client),
):
"""Delete all jobs."""
service = JobService(rds_client)
deleted_count = await service.delete_all()
return JSONResponse(
content={"message": f"Deleted {deleted_count} job(s).", "count": deleted_count},
status_code=200,
)
Loading