texas2stepdocs Source on GitHubGitHub

Development

This guide is for working on texas2step itself: running it from source, testing, building the documentation site and the Debian package, and making a release. To install texas2step on a server, see Install.

Prerequisites#

  • Python 3.11 or newer, and uv.
  • Node.js 20 or newer (24 LTS recommended), only to change the web UI.
  • For the .deb: an Ubuntu 26.04 amd64 host with dpkg-deb, binutils and, optionally, lintian.

Run from source#

git clone https://github.com/robert-patrick-texas/texas2step.git
cd texas2step
uv sync          # creates .venv with the app plus the dev and docs tools
make dev         # http://127.0.0.1:8080

The built UI is committed under src/texas2step/static/, so the server runs without Node. To work on the UI, run make web-watch in a second terminal. It rebuilds the bundle on every change, unminified and with source maps.

To try the server under a path prefix the way nginx publishes it, send the header nginx would:

curl -s -H 'X-Forwarded-Prefix: /texas2step' http://127.0.0.1:8080/ | grep '<base'
# <base href="/texas2step/" />

Make targets#

Target What it does
make dev Run the server on 127.0.0.1:8080
make web Install Node dependencies, typecheck and bundle the UI into src/texas2step/static
make web-watch Rebuild the UI on change
make test pytest, including WebRTC round trips on loopback
make lint / make fmt ruff and tsc / ruff format and autofix
make check lint and test
make docs Render every .md file into the static site in html/
make docs-watch Re-render html/ whenever a .md file changes
make deb Build dist/texas2step-<version>-1-amd64.deb and dist/texas2step-setup
make version Print the version
make upgrade Move every Python and npm dependency to its newest release, then rebuild the UI

Project layout#

src/texas2step/        Python package
  app.py               FastAPI app factory, path-prefix support, static UI, SPA routes
  api/wire.py          measurement primitives: ping, down, up, ws, rtc/offer
  api/meta.py          info, whoami, locations, health
  api/measure.py       server-side measurement functions
  api/results.py       result history
  engine.py            Python measurement engine (CLI + /measure)
  rtc.py               WebRTC data-channel echo (aiortc), UDP port range
  page.py              HTML shell: path prefix, version, default theme, branding
  config_template.toml the settings file the package installs, every setting documented
  config_merge.py      `texas2step config merge`: carries settings into a new template
  config_edit.py       `texas2step config set` / `add-location`: edits in place
  stats.py             shared statistical definitions
  store.py             SQLite result store
  static/              built web UI (generated by web/build.mjs; committed)
web/                   TypeScript client (no runtime dependencies)
  src/engine/          browser measurement engine
  src/ui/              gauge, link animation, charts, views
tests/                 pytest: stats, config, API, store, WebRTC, end-to-end
docs/                  the documentation, in Markdown
scripts/build_docs.py  Markdown โ†’ html/ (the GitHub Pages site)
deploy/                files the package installs: systemd unit, defaults, nginx snippets
packaging/             Debian packaging: build-deb.sh, control, maintainer scripts,
                       and texas2step-setup.py, the operators' check-and-install tool
.github/workflows/     GitHub Pages publishing
legacy/                the original single-file prototype, kept for reference

Code conventions#

  • Server-side code is Python. Browser code is TypeScript, bundled by esbuild with no runtime dependencies and no CDN, so the page works on networks without internet access.
  • Statistical definitions live in stats.py and web/src/engine/stats.ts and must stay in step. Methodology documents them.
  • After changing anything under web/, run make web and commit the regenerated src/texas2step/static/ along with the source.
  • The one piece of browser code that isn't ours is Swagger UI, the OpenAPI explorer at /api/docs. web/build.mjs copies it, with its license files, from the pinned swagger-ui-dist npm package into static/assets/swagger/, so the explorer needs no CDN. That package depends on @scarf/scarf, an install-time analytics reporter; its install script is denied in web/package.json (allowScripts), so it never runs.
  • Dependencies are pinned exactly (uv.lock, web/package.json and web/package-lock.json), so builds are repeatable. make upgrade moves them all to their newest releases.

Tests#

make test runs the whole suite in about 15 seconds:

  • Unit tests: statistics, settings, the result store.
  • API tests: a FastAPI test client, including the path-prefix handling.
  • End-to-end tests against a real uvicorn server on loopback: HTTP latency, multi-stream throughput, and WebRTC packet loss, including the pinned UDP port range.

The WebRTC tests need a non-loopback address on the host, because WebRTC never offers 127.0.0.1 as a candidate.

Documentation site#

scripts/build_docs.py turns every .md file in the repository into a static site in html/:

  • README.md becomes the index page, and every page has a sidebar that links to all the others.
  • Links between .md files become links between pages.
  • A broken link or anchor fails the build.

html/ isn't committed. On this development server it is served for review by nginx at /texas2step/, through the symlink /var/www/html/texas2step โ†’ html/. Run make docs, or keep make docs-watch running while you edit.

On GitHub, .github/workflows/docs.yml builds the same site and publishes it to GitHub Pages on every push to main that touches Markdown. It needs Pages enabled once: in the repository settings, Pages โ†’ Source: GitHub Actions. Until then the workflow builds the site and skips the deploy with a notice.

Build the Debian package#

make deb          # or: packaging/build-deb.sh

packaging/build-deb.sh:

  • Builds the wheel.
  • Creates a relocatable virtualenv on the system /usr/bin/python3, and installs the exact dependency versions from uv.lock into it.
  • Adds the files from deploy/.
  • Writes dist/texas2step-<version>-<revision>-amd64.deb, then runs lintian.
  • Writes dist/texas2step-setup, the operators' setup tool, from packaging/texas2step-setup.py. It stamps in the package's file name and SHA-256, so the tool refuses any other package. Ship the two files together.

The virtualenv holds compiled wheels and links to the system Python, so build on the Ubuntu release you're packaging for. The package then depends on that release's Python minor version (3.14 for 26.04). For a packaging-only fix of the same version, use REVISION=2 make deb.

To test an install without touching the build host, use a throwaway systemd container:

cat > Dockerfile.test <<'EOF'
FROM ubuntu:26.04
RUN apt-get update && DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \
      systemd systemd-sysv dbus nginx curl ca-certificates python3 adduser
STOPSIGNAL SIGRTMIN+3
CMD ["/sbin/init"]
EOF
docker build -t t2s-test -f Dockerfile.test . && rm Dockerfile.test
docker run -d --name t2s-test --privileged --cgroupns=host \
    -v /sys/fs/cgroup:/sys/fs/cgroup:rw -p 127.0.0.1:18080:80 t2s-test
docker exec t2s-test rm -f /usr/sbin/policy-rc.d     # the image blocks service starts
docker cp dist/texas2step-*-amd64.deb t2s-test:/root/
docker exec t2s-test sh -c 'apt-get install -y /root/texas2step-*-amd64.deb'

Then add the include lines from Install to /etc/nginx/sites-available/default in the container, reload nginx, and open http://127.0.0.1:18080/texas2step/.

To test the setup tool instead, copy it in beside the package and use it in place of apt-get install. The container's default site is plain HTTP, so name it:

docker cp dist/texas2step-setup t2s-test:/root/
docker exec -w /root t2s-test ./texas2step-setup --id austin \
    --site /etc/nginx/sites-enabled/default
docker exec -w /root t2s-test ./texas2step-install.sh

For an upgrade, install the previous release's .deb first and edit its settings, then run the new setup tool. Add ufw to the image and enable it, after allowing 22 and 443, to see the firewall step. packaging/texas2step-setup.py keeps its checks in pure functions, which tests/test_setup.py covers with sample nginx -T, ufw, iptables and nftables output.

Release#

  1. Set the version in src/texas2step/__init__.py, the only place it is written. Everything else reads it from there:

    • pyproject.toml, through hatch's dynamic version, so the wheel and pip show too.
    • The server (/api/v1/info, /api/v1/health), and the web UI, which the server tells the version in each page it serves. No UI rebuild is needed for a new version.
    • The CLI (texas2step --version).
    • packaging/build-deb.sh, for the package version and file name.
    • make version, which prints it for scripts.

    The documentation doesn't repeat it.

  2. If the release changes src/texas2step/config_template.toml, keep one setting per line, which is the layout texas2step config merge relies on. If it renames a setting, add the old and new names to RENAMED in config_merge.py, so upgrades carry the value across.

  3. Run make upgrade, so the release ships every dependency's newest version, with its security and bug fixes.

  4. Run make check and make deb. The package installs exactly the versions in uv.lock.

  5. Commit (including the updated lockfiles), tag and publish the package:

v=$(make -s version)
git commit -am "Release $v" && git tag "v$v" && git push origin main "v$v"
gh release create "v$v" "dist/texas2step-$v-1-amd64.deb" dist/texas2step-setup \
    --title "texas2step $v" --generate-notes

License#

texas2step is licensed under the Apache License 2.0. Contributions are accepted under the same license.