Development Setup¶
Guide for setting up a local WireBuddy development environment.
Prerequisites¶
Required¶
- Python 3.13+
- Git
- pip and venv
System Dependencies¶
Clone Repository¶
Python Environment¶
Create Virtual Environment¶
Install Dependencies¶
pyproject.toml is the single manifest: it holds the release version, the runtime dependencies, and the dev and docs extras.
Configuration¶
Create .env File¶
Edit .env¶
# Required
WIREBUDDY_SECRET_KEY=dev-secret-key-change-me
# Development settings
LOG_LEVEL=DEBUG
WIREBUDDY_DEV_RELOAD=true
# Database (dev). Both data/ and data_local/ are gitignored — keep the dev
# database inside one of them so encrypted keys never reach a commit.
WIREBUDDY_DATA_DIR=./data_local
# Optional bind overrides are available in Docker as WIREBUDDY_HOST and
# WIREBUDDY_PORT. Local run.py reads host and port from the application DB.
System Configuration¶
# Enable IP forwarding
sudo sysctl -w net.ipv4.ip_forward=1
sudo sysctl -w net.ipv6.conf.all.forwarding=1
# Enable conntrack accounting
sudo sysctl -w net.netfilter.nf_conntrack_acct=1
Database Setup¶
WireBuddy automatically creates database on first run:
This creates data_local/wirebuddy.db with schema and default admin user.
Running WireBuddy¶
Development Server¶
Hot reload is enabled by WIREBUDDY_DEV_RELOAD=true in .env.
Access: http://localhost:8000
Run Tests¶
Run Linter¶
The full configured rule set gates every PR at zero findings; see pyproject.toml for the [tool.ruff.lint] configuration and documented exemptions.
Format Code¶
Ruff is the only formatter and linter used in this project — there is no Black, isort, mypy, or pylint dependency.
IDE Setup¶
VS Code¶
Install recommended extensions:
{
"recommendations": [
"ms-python.python",
"ms-python.vscode-pylance",
"charliermarsh.ruff",
"tamasfe.even-better-toml"
]
}
Settings:
{
"[python]": {
"editor.defaultFormatter": "charliermarsh.ruff",
"editor.formatOnSave": true,
"editor.codeActionsOnSave": {
"source.organizeImports": true
}
}
}
PyCharm¶
- Open project
- Configure Python interpreter (point to
.venv/bin/python) - Enable the Ruff plugin for linting and formatting
Project Structure¶
wirebuddy/
├── app/ # Application code
│ ├── __init__.py
│ ├── main.py # create_app() factory and lifespan
│ ├── api/ # FastAPI routers, split by resource
│ ├── db/ # sqlite_*.py data access, tsdb.py metrics store
│ ├── dns/ # Unbound config, blocklists, query-log ingestion
│ ├── node/ # Node-mode daemon, enrollment, metrics queue
│ ├── tasks/ # Scheduled background jobs
│ ├── middleware/ # CSRF and request middleware
│ ├── speedtest/ # librespeed-cli integration
│ ├── models/ # Pydantic models
│ ├── utils/ # Utilities (crypto, vault, geoip, conntrack, ...)
│ ├── static/ # CSS design system, vanilla JS
│ └── templates/ # Jinja2 templates
├── tests/ # Flat pytest suite (test_*.py)
├── tools/ # UI linter and CI helpers
├── docker/ # Dockerfile, entrypoint, compose files
├── docs/ # MkDocs documentation
├── data/ # Runtime data (gitignored)
├── pyproject.toml # Project metadata, version, dependencies, dev/docs extras
├── run.py # Entry point
├── README.md
├── LICENSE
└── .gitignore
Development Workflow¶
Create Feature Branch¶
Make Changes¶
- Write code
- Add tests
- Run tests:
pytest - Format code:
ruff format . - Lint code:
ruff check .
Commit Changes¶
Use conventional commits — see Commit Messages for the type list and examples.
Push and Create PR¶
Create Pull Request on GitHub.
Debugging¶
VS Code Debugger¶
.vscode/launch.json:
{
"version": "0.2.0",
"configurations": [
{
"name": "Python: FastAPI",
"type": "python",
"request": "launch",
"module": "uvicorn",
"args": [
"app:create_app",
"--factory",
"--reload",
"--host", "0.0.0.0",
"--port", "8000"
],
"jinja": true,
"justMyCode": false
}
]
}
Python Debugger¶
Insert breakpoint:
Logging¶
import logging
logger = logging.getLogger(__name__)
logger.debug("Debug message")
logger.info("Info message")
logger.warning("Warning message")
logger.error("Error message")
Database Management¶
SQLite CLI¶
sqlite3 data_local/wirebuddy.db
# Common commands
.schema # Show schema
.tables # List tables
SELECT * FROM users; # Query
.quit # Exit
Schema Changes¶
There is no Alembic or external migration tool. The baseline schema is created by init_schema() in app/db/sqlite_schema.py, and version-to-version upgrades are applied in app/utils/migration.py against the schema_version table.
Testing¶
The suite is flat — tests/test_*.py, no unit/integration/e2e split. Select by file or by name:
pytest # everything
pytest tests/test_login_lockout.py # one file
pytest -k lockout # by name
pytest -x # stop on first failure
Test Coverage¶
Fixtures¶
The suite is hermetic: no running server, no real WireGuard or Unbound, no network. tests/conftest.py provides the one shared fixture, conn, a fresh in-memory SQLite database with the full application schema:
def test_last_admin_cannot_be_deleted(conn):
user_id = create_user(conn, "admin", "Correct-Horse-1", is_admin=True)
with pytest.raises(LastAdminError):
delete_user(conn, user_id)
Use tmp_path for anything that needs files and monkeypatch for environment and collaborators. See tests/AGENTS.md for the per-file map of what is covered.
Frontend Development¶
CSS¶
Located in app/static/css/:
wb-ui-system.css— design system entry point, withfoundations/,components/,utilities/, andpages/beneath itstyle.cssplus per-page sheets (dashboard.css,peers.css,traffic.css,dns.css,users.css,login.css,status.css)
JavaScript¶
Located in app/static/js/ — vanilla JS, no build step. One script per page (dashboard.js, peers.js, traffic.js, …) plus shared modules: api.js for all backend calls, core/ for primitives, and components/ for reusable widgets.
Templates¶
Jinja2 templates in app/templates/:
{% extends "base.html" %}
{% block title %}Dashboard{% endblock %}
{% block content %}
<h1>Dashboard</h1>
<!-- content -->
{% endblock %}
Hot Reload¶
Templates auto-reload on save (development mode).
Documentation¶
Build Docs Locally¶
# Install docs dependencies (the "docs" extra in pyproject.toml)
pip install -e ".[docs]"
# The CI workflow publishes these two root files as documentation pages.
cp CHANGELOG.md docs/changelog.md
cp LICENSE docs/license.md
# Serve docs
mkdocs serve -f docs/mkdocs.yml
# Open http://127.0.0.1:8000
Add Documentation¶
- Create markdown file in
docs/ - Add to
docs/mkdocs.ymlnavigation - Preview with
mkdocs serve -f docs/mkdocs.yml - Commit changes
Building Docker Image¶
Local Build¶
The Dockerfile lives in docker/ but its build context is the repository root:
To reproduce the CI lint gate without a full build:
Multi-Platform Build¶
Run Local Image¶
docker run -d \
--name wirebuddy-dev \
--network host \
--cap-add NET_ADMIN \
-e WIREBUDDY_SECRET_KEY=dev-key \
-v $(pwd)/data:/app/data \
wirebuddy:dev
Troubleshooting¶
Import Errors¶
# Ensure virtual environment is activated
source .venv/bin/activate
# Reinstall dependencies
pip install -e ".[dev]"
Database Locked¶
# Close all connections
pkill -f wirebuddy
# Or delete database (dev only)
rm data_local/wirebuddy.db
Port Already in Use¶
Next Steps¶
- Architecture - System architecture
- Contributing - Contribution guidelines
- API Documentation - API reference