Files
app/Makefile
T
stroblmeandClaude Opus 5 d12c81c8a0 Publish the documentation site: docs.fluksio.com
A zensical site under docs/, served by a new `docs` compose service behind
Traefik, built with --strict in CI. Same pattern the sibling n3xd workspace
uses.

Getting started splits the way the landing page does — one path is
`pip install fluksio` and a training script, the other is a Docker stack and
an afternoon in the browser — because the two audiences will not spend the same
amount of time. Everything after that is shared: the concepts, the web
interface (app and portal), the CLI and the API, and a reference for node types,
payload types and configuration.

The three flow guides move here from the docs submodule rather than being
copied, so there is one version of them.

Styling mirrors DESIGN-GUIDELINES.md: the app's token palette remapped onto
Material's variables in both schemes, Inter, the 16px panel radius, and the one
terracotta accent spent on the facility lane of the audience split.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01M7Xv3cJEW5c8AXxn2hoojV
2026-08-22 05:55:34 +02:00

201 lines
11 KiB
Makefile

# ─── Fluksio app Makefile ───
# Convenience targets for development, testing, linting, and deployment.
# The workspace root delegates to these (see ../Makefile).
.PHONY: dev-utils dev dev-local up down update install dev-backend dev-frontend \
generate-client seed-example seed-demo seed-house seed-aircon seed-hosted-demo test test-backend test-frontend soak bench-startup lint lint-backend \
lint-frontend format-frontend umami build docs docs-serve clean help
COMPOSE_ROOT := $(CURDIR)
# Explicit project name keeps this stack isolated from the sibling website
# stack (otherwise both default to "docker", the directory their compose
# files live in).
COMPOSE_PROJECT := fluksio-app
# Compose interpolation needs the project-local .env before reading compose.yml.
COMPOSE := docker compose -p $(COMPOSE_PROJECT) --env-file $(COMPOSE_ROOT)/.env
COMPOSE_PROD := $(COMPOSE) -f docker/compose.yml
# Production also runs autoheal, which restarts the backend when its deep
# health check fails. It is profile-gated because it mounts the Docker socket.
COMPOSE_PROD_RUN := $(COMPOSE_PROD) --profile autoheal
COMPOSE_DEV := $(COMPOSE_PROD) -f docker/compose.dev.yml
# Integrated local stack: dev stack wired onto the shared `proxy` network.
COMPOSE_LOCAL := $(COMPOSE_DEV) -f docker/compose.local.yml
help: ## Show available targets
@awk 'BEGIN{FS=":.*?## "} /^[a-zA-Z_-]+:.*?##/ {printf " \033[36m%-18s\033[0m %s\n", $$1, $$2}' $(MAKEFILE_LIST)
# ── Development (Docker) ──────────────────────────────────────────
dev-utils: ## Start only the utility containers (proxy, mailcatcher)
$(COMPOSE_DEV) up --build proxy mailcatcher
dev: ## Start the full dev stack (includes a local Traefik proxy)
$(COMPOSE_DEV) up --build
dev-local: ## Start the integrated local stack (called by the root `make dev`)
DOMAIN=$${DOMAIN:-localhost} ENVIRONMENT=$${ENVIRONMENT:-local} \
$(COMPOSE_LOCAL) up --build -d proxy backend frontend mailcatcher
up: ## Start the production stack
$(COMPOSE_PROD_RUN) up --build -d
umami: ## Provision + start the optional Umami site-analytics service (idempotent)
# Guard: the dashboard is internet-facing on analytics.$(DOMAIN); refuse to
# start when its session-signing secret is unset or still the placeholder.
@secret=$$(grep -E '^UMAMI_APP_SECRET=' $(COMPOSE_ROOT)/.env 2>/dev/null | head -1 | cut -d= -f2-); \
if [ -z "$$secret" ] || [ "$$secret" = "changethis" ]; then \
echo "ERROR: UMAMI_APP_SECRET is empty or 'changethis' in .env — run the workspace root's scripts/setup.sh (or set it: openssl rand -hex 32) before 'make umami'." >&2; \
exit 1; \
fi
# --no-recreate: reuse an already-running db instead of recreating it under
# the prod compose set, which would blip every service that depends on it.
$(COMPOSE_PROD) up -d --wait --no-recreate db
# Least-privilege role owning only the umami database, so the third-party
# image never holds the shared superuser credentials.
@pguser=$$(grep -E '^POSTGRES_USER=' $(COMPOSE_ROOT)/.env 2>/dev/null | head -1 | cut -d= -f2-); pguser=$${pguser:-postgres}; \
role=$$(grep -E '^UMAMI_DB_USER=' $(COMPOSE_ROOT)/.env 2>/dev/null | head -1 | cut -d= -f2-); role=$${role:-umami}; \
pw=$$(grep -E '^UMAMI_DB_PASSWORD=' $(COMPOSE_ROOT)/.env 2>/dev/null | head -1 | cut -d= -f2-); \
db=$$(grep -E '^UMAMI_DB=' $(COMPOSE_ROOT)/.env 2>/dev/null | head -1 | cut -d= -f2-); db=$${db:-umami}; \
if [ -z "$$pw" ] || [ "$$pw" = "changethis" ]; then \
echo "ERROR: UMAMI_DB_PASSWORD is empty or 'changethis' in .env — run the workspace root's scripts/setup.sh before 'make umami'." >&2; \
exit 1; \
fi; \
$(COMPOSE_PROD) exec -T db psql -v ON_ERROR_STOP=1 -U "$$pguser" -d postgres \
-v role="$$role" -v pw="$$pw" -v db="$$db" < docker/provision-umami-db.sql
# --no-deps: only (re)create umami, never restart the shared db underneath it.
$(COMPOSE_PROD) --profile analytics up -d --no-deps umami
update: ## Pull, rebuild using the layer cache, and recreate changed containers
git pull
$(COMPOSE_PROD_RUN) build
$(COMPOSE_PROD_RUN) up -d --remove-orphans
docker image prune -f
down: ## Stop all running containers
-$(COMPOSE_LOCAL) down
-$(COMPOSE_PROD_RUN) down
# ── Development (local, no Docker) ───────────────────────────────
# Run `make dev-backend` and `make dev-frontend` in two separate terminals.
install: ## Install all dependencies (backend + frontend)
cd backend && uv sync
cd frontend && bun install
dev-backend: ## Start the FastAPI backend with hot-reload (local)
cd backend && uv run fastapi dev fluksio/main.py
dev-frontend: ## Start the Vite dev server (local)
cd frontend && bun dev
generate-client: ## Regenerate the frontend SDK from the backend's OpenAPI schema
bash scripts/generate-client.sh
seed-example: ## Seed the querying-chart example (needs a running stack + InfluxDB)
cd backend && uv run python ../scripts/seed_example_chart.py
seed-demo: ## Seed the training-run example: a batch flow and its dashboard
cd backend && uv run python ../scripts/seed_demo_training.py
seed-house: ## Seed the house write-path rig (needs the real broker reachable)
cd backend && uv run python ../scripts/seed_house_control.py
seed-aircon: ## Seed the aircon write-path rig (needs the unit reachable)
cd backend && uv run python ../scripts/seed_aircon_control.py
# Operators of the hosted demo only — NOT part of any deployment, and nothing a
# self-hosted instance needs. It wipes and recreates its three flows and its
# dashboard, so re-running it is how the public demo is reset.
# `uv run` does not read .env, so the credentials are lifted out of it here;
# anything already exported wins, which is how a remote instance is targeted:
# API_URL=https://api.example.com make seed-hosted-demo
seed-hosted-demo: ## Seed the hosted demo panel (operators only; API_URL selects the instance)
@user=$$(grep -E '^FIRST_SUPERUSER=' $(COMPOSE_ROOT)/.env 2>/dev/null | head -1 | cut -d= -f2-); \
pass=$$(grep -E '^FIRST_SUPERUSER_PASSWORD=' $(COMPOSE_ROOT)/.env 2>/dev/null | head -1 | cut -d= -f2-); \
cd backend && \
FIRST_SUPERUSER="$${FIRST_SUPERUSER:-$$user}" \
FIRST_SUPERUSER_PASSWORD="$${FIRST_SUPERUSER_PASSWORD:-$$pass}" \
uv run python ../scripts/seed_demo.py
# ── Testing ───────────────────────────────────────────────────────
test: test-backend test-frontend ## Run all tests (backend + frontend)
test-backend: ## Run backend tests (pytest + coverage)
# Its own SQLite file in a temp directory (tests/__init__.py), so this needs
# nothing running and touches no development data.
cd backend && uv run bash scripts/test.sh
# The hostname the stack is served under. A checkout configured for a
# deployment carries the deployment's domain, and *.fluksio.com resolves to the
# live instance from here — which is why the run below maps both names onto the
# local Traefik by address and never lets DNS decide.
export DOMAIN ?= $(shell sed -n 's/^DOMAIN=//p' $(COMPOSE_ROOT)/.env | head -1)
PW_VERSION = $(shell sed -n 's/.*"@playwright\/test": "[^0-9]*\([0-9.]*\)".*/\1/p' frontend/package.json | head -1)
test-frontend: ## Run frontend tests (Playwright e2e) against the local stack
@ip=$$(docker network inspect proxy \
--format '{{range .Containers}}{{if eq .Name "fluksio-app-proxy-1"}}{{.IPv4Address}}{{end}}{{end}}' \
2>/dev/null | cut -d/ -f1); \
[ -n "$$ip" ] || { echo " ✗ proxy network or Traefik container not found — is the stack up?"; exit 1; }; \
docker run --rm --network proxy --ipc=host \
--user $$(id -u):$$(id -g) -e HOME=/tmp \
--add-host app.$(DOMAIN):$$ip --add-host api.$(DOMAIN):$$ip \
-v $(COMPOSE_ROOT):/app -w /app/frontend \
-e PLAYWRIGHT_BASE_URL=http://app.$(DOMAIN) \
-e PLAYWRIGHT_API_URL=http://api.$(DOMAIN) \
-e HOST_RESOLVER_RULES="MAP app.$(DOMAIN) $$ip, MAP api.$(DOMAIN) $$ip" \
-e CI=$${CI:-1} \
-e FIRST_SUPERUSER="$$(sed -n 's/^FIRST_SUPERUSER=//p' $(COMPOSE_ROOT)/.env | head -1)" \
-e FIRST_SUPERUSER_PASSWORD="$$(sed -n 's/^FIRST_SUPERUSER_PASSWORD=//p' $(COMPOSE_ROOT)/.env | head -1)" \
mcr.microsoft.com/playwright:v$(PW_VERSION)-noble \
npx playwright test $(PLAYWRIGHT_ARGS)
# Load and chaos against a *running* stack, never part of `make test`: it
# restarts this stack's containers. `SOAK_ARGS="--dry-run"` only looks.
soak: ## Run the soak/chaos harness (SOAK_ARGS="--minutes 30 --scenario redis")
cd backend && uv run python scripts/soak.py $(SOAK_ARGS)
bench-startup: ## Time submitting a run (BENCH_ARGS="--kedro ../some/kedro/project")
cd backend && uv run python scripts/bench_startup.py $(BENCH_ARGS)
# ── Linting ───────────────────────────────────────────────────────
build: ## Build the fluksio and fluksio-worker wheels into dist/
uv build --all-packages --out-dir dist
lint: lint-backend lint-frontend ## Run all linters
lint-backend: ## Lint backend with ruff + mypy
cd backend && uv run ruff check .
cd backend && uv run ruff format --check .
cd backend && uv run mypy fluksio
cd worker && uv run --no-project --with mypy mypy fluksio_worker
lint-frontend: ## Lint frontend with biome
cd frontend && bun run lint
format-frontend: ## Apply biome's fixes to the frontend (what `lint-frontend` only reports)
cd frontend && bun run format
# ── Documentation ─────────────────────────────────────────────────
# The public site on docs.${DOMAIN}, served by the `docs` service in
# docker/compose.yml. These targets are for local authoring: zensical runs on
# demand via uvx, so it never touches the backend environment. Pinned to the
# version docker/Dockerfile.docs and .gitea/workflows/docs.yml ship, so local
# authoring builds with what docs.fluksio.com actually gets.
ZENSICAL := zensical==0.0.46
docs: ## Build the documentation site into ./site
uvx $(ZENSICAL) build --clean
docs-serve: ## Serve the documentation site locally with live reload
uvx $(ZENSICAL) serve
# ── Cleanup ───────────────────────────────────────────────────────
clean: ## Remove build artifacts and caches
rm -rf frontend/dist frontend/blob-report frontend/test-results
rm -rf backend/.pytest_cache backend/htmlcov site
find backend -type d -name __pycache__ -exec rm -rf {} + 2>/dev/null || true