Skip to content

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 (.robot and .py scripts → Robot Framework / pytest in perfshop-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:

  1. Scripts UI → Forgejo — through HTTP REST (/api/v1 API) using the token stored in /token/forgejo_token (a volume shared with the Forgejo seed).
  2. Scripts UI → Test Runner — through docker exec over the Docker socket, to launch robot or python -m pytest commands.
  3. 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 = 3 by default) to avoid saturating perfshop-test-runner
  • A unique identifier per run (runId based 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)