Scripts UI¶
perfshop-scripts-ui is the web workshop for pedagogical test scripts. It combines a Git file editor wired to Forgejo, a test launcher that runs scripts in perfshop-test-runner through docker exec, and a run history UI with all of their artifacts.
Its role differs from perfshop-jmeter-ui:
- JMeter UI drives load shots (
.jmx→ JMeter in another container). - Scripts UI drives functional tests (
.robotand.pyscripts → Robot Framework / pytest inperfshop-test-runner), and additionally manages direct editing of the files versioned in Forgejo.
The architecture is very close to JMeter UI (same Node/Express foundation, same "bind Docker socket to exec" pattern), with two extra specifics: Forgejo integration through its REST API and structured persistence of runs as artifacts.
Source of truth
This page is drawn from the perfshop-scripts-ui block of the compose files, the Dockerfile in scripts-ui/, and the 9 modules in scripts-ui/src/ (server.js, config.js, i18n.js, validation.js, docker.js, security.js, auth.js, forgejo.js, runs.js).
Overall architecture¶
flowchart TB
subgraph browser["Browser"]
UI["Scripts UI<br/>http://localhost:3008"]
end
subgraph sui["perfshop-scripts-ui<br/>(Node 20 + Express, port 3008)"]
direction TB
SRV["server.js<br/>(orchestration)"]
AU["auth.js<br/>(8h session + CSRF + rate limit)"]
FG["forgejo.js<br/>(Forgejo API, tree, CRUD)"]
RN["runs.js<br/>(launch + artifacts)"]
DK["docker.js<br/>(Docker socket)"]
VA["validation.js<br/>(path sanitization)"]
SRV --> AU
SRV --> FG
SRV --> RN
RN --> DK
FG --> VA
end
subgraph fg["perfshop-forgejo<br/>(self-hosted Git, port 3009)"]
REPO["perfshop-tests repository<br/>(Robot Framework tests<br/>+ pytest)"]
end
subgraph tr["perfshop-test-runner<br/>(Python 3.11 + RF 7.0 + pytest)"]
SH["SSH entrypoint"]
RUNNER["exec robot|pytest"]
end
subgraph sel["perfshop-selenium"]
CHROME["standalone-chrome<br/>:4444"]
end
subgraph vols["Docker volumes"]
FTD[/"forgejo-token-data:/token<br/>(RO - Forgejo token)"/]
LOGS[/"./test-runner/logs → /rf-logs<br/>(shared persistent runs)"/]
end
DOCK[/"/var/run/docker.sock"/]
UI -->|HTTPS| SRV
FG -->|REST API v1| fg
DK -->|mounts| DOCK
DOCK -.->|docker exec perfshop-test-runner<br/>robot ... / python -m pytest ...| RUNNER
RUNNER -->|WebDriver| CHROME
RUNNER -->|writes logs + artifacts| LOGS
SRV -->|reads /token/forgejo_token| FTD
SRV -->|reads past runs| LOGS
Three key communication chains:
- Scripts UI → Forgejo — through HTTP REST (
/api/v1API) using the token stored in/token/forgejo_token(a volume shared with the Forgejo seed). - Scripts UI → Test Runner — through
docker execover the Docker socket, to launchrobotorpython -m pytestcommands. - Test Runner → Scripts UI — through the shared bind mount
./test-runner/logs → /rf-logs, which lets the UI read logs and artifacts produced by runs.
Docker Compose configuration¶
| Key | Value |
|---|---|
| Build | ./scripts-ui (local Dockerfile) |
container_name |
perfshop-scripts-ui |
| Exposed port | 3008 (host) → 3008 (container), variable SCRIPTS_UI_PORT |
| Network | perfshop-network |
Bind mounts¶
| Bind mount | Mount point | Mode | Role |
|---|---|---|---|
/var/run/docker.sock |
/var/run/docker.sock |
RW | Docker socket for docker exec perfshop-test-runner |
forgejo-token-data (named volume) |
/token |
RO | Forgejo token generated by perfshop-forgejo-seed, shared through a named volume |
./test-runner/logs |
/rf-logs |
RW | Volume shared with perfshop-test-runner — reading logs + writing persistent runs |
Token sharing through a named volume
The Forgejo token is generated once by perfshop-forgejo-seed and written into /token/forgejo_token inside a Docker named volume (forgejo-token-data). This volume is then mounted read-only into perfshop-scripts-ui. This pattern avoids passing the token through an environment variable (visible in docker inspect) and guarantees that it is freshly generated on every new deployment.
Environment variables¶
| Variable | Default | Role |
|---|---|---|
PORT |
3008 |
HTTP port of the Express server |
PUBLIC_SCRIPTS_URL |
http://localhost:3008 |
Exposed public URL |
PUBLIC_FORGEJO_URL |
http://localhost:3009 |
Forgejo URL exposed in the UI (navigation links) |
FORGEJO_INTERNAL_URL |
http://perfshop-forgejo:3000 |
Internal URL for server-side API calls |
FORGEJO_CI_USER |
perfshop-ci |
Name of the Forgejo CI account used for automatic commits |
FORGEJO_REPO |
perfshop-tests |
Target repository name |
FORGEJO_TOKEN_FILE |
/token/forgejo_token |
Path of the file holding the token |
PERFSHOP_API_INTERNAL |
http://perfshop-app:8080 |
Internal PerfShop backend URL (used by some tests) |
TEST_RUNNER_CONTAINER |
perfshop-test-runner |
Name of the target container for docker exec |
PERFSHOP_FRONTEND_URL / _INTERNAL |
— | Public and internal frontend URLs (passed to test scripts as Robot variables) |
SELENIUM_REMOTE_URL |
http://perfshop-selenium:4444/wd/hub |
Selenium hub URL to use in tests |
RUNS_DIR |
/rf-logs/runs |
Directory for persisting run metadata |
MAX_CONCURRENT_RUNS |
3 |
Maximum number of concurrent runs |
TREE_MAX_DEPTH |
4 |
Maximum depth of the displayed Forgejo tree (safety against malformed repos) |
SESSION_SECRET |
perfshop-dev-secret (dev) |
Cookie signing secret — fail-fast in NODE_ENV=production if left at default |
SESSION_COOKIE_SECURE / _SAME_SITE |
false / lax |
Cookie policy (HTTP / HTTPS) |
PERFSHOP_LANG |
fr |
UI language |
The 9 modules in src/¶
| Module | Lines | Role |
|---|---|---|
server.js |
81 | Express orchestration, session, middlewares, route mounting, window.__CONFIG__ injection |
config.js |
49 | Reads environment variables and exports PUBLIC_CONFIG |
i18n.js |
51 | Loads translation bundles from public/i18n/{fr,en}.json |
validation.js |
33 | Path sanitization (protection against ../ and dangerous characters) |
docker.js |
123 | Low-level Docker API client through the Unix socket |
security.js |
64 | csrfProtection and rateLimit middlewares |
auth.js |
68 | Login/logout/status + requireAuth middleware |
forgejo.js |
312 | Complete wrapper of the Forgejo REST API (tree, file CRUD, commits, sync) |
runs.js |
230 | Run launching, artifact management, concurrency limit |
Key utility — validation.js¶
The entire security of file operations rests on a single function: the strict validation of relative paths supplied by the client. No write operation is performed without passing through this filter, which rejects:
- Absolute paths starting with
/ ..or../sequences- Control characters and shell special characters
- Paths deeper than
TREE_MAX_DEPTH(4 levels)
This function is called by forgejo.js before any Forgejo API call, and by runs.js before any docker exec command.
Forgejo integration — the forgejo.js module¶
The forgejo.js module is a complete wrapper of the Forgejo v1 API that exposes to the browser a set of REST routes modeled on usual Git operations. Internally, it translates each call into a Bearer <token> HTTP request toward FORGEJO_INTERNAL_URL/api/v1.
Exposed routes¶
| Method | Route | Forgejo operation |
|---|---|---|
GET |
/api/tree |
GET /repos/{owner}/{repo}/git/trees/main?recursive=true then hierarchical reconstruction |
GET |
/api/file?path=... |
GET /repos/{owner}/{repo}/contents/{path} (base64 decoding) |
GET |
/api/download?path=... |
Download of the raw content |
PUT |
/api/file |
PUT /repos/{owner}/{repo}/contents/{path} (create or update) |
POST |
/api/upload |
Multipart upload (through multer) then Forgejo PUT |
DELETE |
/api/file?path=... |
DELETE /repos/{owner}/{repo}/contents/{path} |
POST |
/api/mkdir |
Creation of a .gitkeep file in a new folder |
POST |
/api/move |
Rename or move (= delete + create on the Forgejo side) |
GET |
/api/commits |
GET /repos/{owner}/{repo}/commits |
POST |
/api/sync |
git pull on the perfshop-test-runner side through docker exec |
HTTP authentication to Forgejo¶
The token is read once at startup from /token/forgejo_token. If the file is missing or empty, the module logs a warning but keeps going (Forgejo routes will then return 500 on every call — a deliberate silent degradation so that the UI can still start in a degraded state).
sequenceDiagram
autonumber
participant UI as Browser
participant SRV as scripts-ui (Node)
participant FGS as perfshop-forgejo-seed
participant FTD as forgejo-token-data volume
participant FG as perfshop-forgejo
Note over FGS,FTD: At first startup (one-shot)
FGS->>FG: POST /users/admin<br/>(CI account)
FGS->>FG: POST /users/{ci}/tokens<br/>(API token generation)
FG-->>FGS: token
FGS->>FTD: writes /token/forgejo_token
Note over SRV,FTD: At scripts-ui startup
SRV->>FTD: fs.readFileSync('/token/forgejo_token')
FTD-->>SRV: token (in memory, module variable)
Note over UI,FG: Normal operation
UI->>SRV: GET /api/tree
SRV->>FG: GET /api/v1/repos/perfshop-ci/perfshop-tests/git/trees/main<br/>Authorization: Bearer <token>
FG-->>SRV: JSON tree
SRV-->>UI: reformatted tree
Running tests — the runs.js module¶
The runs.js module implements a mini execution engine with:
- A concurrency limit (
MAX_CONCURRENT_RUNS = 3by default) to avoid saturatingperfshop-test-runner - A unique identifier per run (
runIdbased on timestamp + random suffix) - Structured persistence in
/rf-logs/runs/<runId>/with three artifacts:metadata.json,stdout.log,stderr.log - Live output streaming during execution, reachable by polling
/api/run/:runId
Exposed routes¶
| Method | Route | Role |
|---|---|---|
POST |
/api/run |
Launches a script (Robot Framework or pytest) in perfshop-test-runner |
GET |
/api/run/:runId |
Live state of the current run (streamed stdout, status, elapsed time) |
GET |
/api/runs |
List of all past runs (scan of RUNS_DIR) |
GET |
/api/runs/:id |
Full metadata of a past run |
GET |
/api/runs/:id/artifact?name=... |
Download an artifact (log.html, report.html, output.xml…) |
GET |
/api/runner-status |
Public — healthcheck of the perfshop-test-runner container (UP/DOWN) |
Flow of a Robot Framework run launch¶
sequenceDiagram
autonumber
actor S as Student
participant UI as Browser
participant SRV as scripts-ui
participant DK as Docker socket
participant TR as perfshop-test-runner
participant SEL as perfshop-selenium
participant FS as /rf-logs/runs/{runId}
S->>UI: Selects tests/smoke/login.robot<br/>clicks "Launch"
UI->>SRV: POST /api/run {path: "tests/smoke/login.robot", type: "robot"}
SRV->>SRV: Path validation (validation.js)
SRV->>SRV: runId = Date.now() + random
SRV->>FS: mkdir /rf-logs/runs/{runId}
SRV->>FS: write metadata.json (user, start, path, type)
SRV->>DK: docker exec perfshop-test-runner<br/>robot --outputdir /rf-logs/runs/{runId}<br/>--variable SELENIUM_URL:http://perfshop-selenium:4444/wd/hub<br/>/scripts/tests/smoke/login.robot
DK->>TR: exec
TR->>SEL: WebDriver session
SEL-->>TR: Chrome session ID
TR->>TR: Runs Robot keywords
TR->>FS: writes log.html, report.html, output.xml as they go
loop Polling every 2s
UI->>SRV: GET /api/run/{runId}
SRV->>FS: reads stdout.log (tail)
SRV-->>UI: {status: "running", stdout: "...", elapsed: 12.5}
end
TR->>FS: generates final artifacts
TR-->>DK: exit code 0 or 1
SRV->>FS: updates metadata.json (end, duration, exitCode, status)
UI->>SRV: GET /api/run/{runId}
SRV-->>UI: {status: "passed", artifacts: [log.html, report.html, output.xml]}
S->>UI: Clicks on "report.html"
UI->>SRV: GET /api/runs/{runId}/artifact?name=report.html
SRV->>FS: streams the file
SRV-->>UI: HTML rendered inline
Structured persistence of a run¶
Every run creates a folder inside RUNS_DIR (default /rf-logs/runs/):
/rf-logs/runs/
├── 1712582340123-a7f2/
│ ├── metadata.json # user, path, type, start, end, duration, exitCode, status
│ ├── stdout.log # captured standard output
│ ├── stderr.log # captured error output
│ ├── log.html # Robot Framework log (if type=robot)
│ ├── report.html # Robot Framework report
│ └── output.xml # structured XML for aggregation
└── 1712581200456-bc91/
└── ...
This persistence is shared with perfshop-test-runner (same bind mount ./test-runner/logs), which lets the test runner write artifacts directly into the proper tree without intermediate copies.
Security¶
The four layers are the same as for jmeter-ui:
| Layer | Details |
|---|---|
| CSRF | Session-signed token, required on POST, PUT, DELETE |
| Rate limit | rateLimit(60_000, 10) on /api/auth/login |
| Session | express-session 8h, httpOnly cookie, secure / sameSite policy conditional |
| Auth gate | Systematic requireAuth on all sensitive routes, except /api/runner-status (public healthcheck) |
On top of that, strict path sanitization by validation.js before any Forgejo call or docker exec — this layer is specific to Scripts UI and is justified by the richness of file operations exposed.
Known limitations and directions¶
- No SSE/WebSocket for live streaming — polling every 2 seconds was chosen for simplicity and robustness. A future iteration could switch to SSE for streaming
stdout.log. - No multi-user collaborative editing — two instructors editing the same file in parallel risk a Git conflict (resolved by the last commit). Acceptable for the targeted pedagogical use.
- No branch management — the UI only works on
main. Branches and pull requests are managed directly in Forgejo by advanced instructors.
Going further¶
- QA Overview
- Forgejo — configuration of the self-hosted Git service and of the seed
- Test Runner — container where scripts are actually executed
- Selenium — Chrome grid used by web tests
- Squash TM — for formal test case management (complementary usage)