Initial commit: Busbar Designer

Web tool for designing nickel/copper busbars over cylindrical-cell battery
packs (21700, 18650) in hex holders. Flask + build123d backend exports
STEP/DXF/SVG; vanilla JS frontend with live preview, multi-project SQLite
persistence, snapshot history.

Deploy scripts in deploy/ (proxmox-lxc.sh, install.sh, update.sh).
This commit is contained in:
wenil
2026-05-24 18:59:50 +03:00
commit d8cb0dc06d
28 changed files with 4172 additions and 0 deletions
+131
View File
@@ -0,0 +1,131 @@
# Deploy
Three scripts here, plus the systemd unit.
| File | Where it runs | What it does |
|-------------------------------|--------------------------------|---------------------------------------------------------------------------|
| `proxmox-lxc.sh` | **Proxmox VE host** as root | Creates an unprivileged Debian 12 LXC, then runs `install.sh` inside it. |
| `install.sh` | **inside** an LXC/VM/server | Clones the repo, sets up Python venv, installs deps, starts systemd unit. |
| `update.sh` | **inside** the LXC/VM/server | `git pull` + refresh Python deps + `systemctl restart`. |
| `busbar-designer.service` | systemd | Unit file template; install.sh substitutes paths/user/port. |
All scripts are idempotent (safe to re-run) and use only stdlib + Debian-shipped tools (`pct`, `pveam`, `whiptail`, `git`, `python3`). No external dependencies.
---
## Step 0 — push to your Gitea (one time)
```bash
# on your laptop, in this repo
git remote add gitea https://gitea.local/me/busbar-designer.git
git push gitea main
```
If your Gitea uses a self-signed cert, set `GIT_SSL_NO_VERIFY=1` everywhere (or install the cert into the system trust store).
---
## Path A — Proxmox VE host (recommended)
One-liner from the Proxmox shell:
```bash
bash -c "$(curl -fsSL https://gitea.local/me/busbar-designer/raw/branch/main/deploy/proxmox-lxc.sh)"
```
(Substitute `https://gitea.local/me/busbar-designer/raw/branch/main/deploy/proxmox-lxc.sh` with your repo's raw URL. Gitea's raw URL format is `https://<host>/<user>/<repo>/raw/branch/<branch>/<path>`.)
You'll get whiptail prompts for:
- Container ID (defaults to next available)
- Hostname (`busbar-designer`)
- Disk size (4 GB), cores (2), RAM (1024 MB)
- Storage pool, network bridge, IP (`dhcp` or `1.2.3.4/24,gw=1.2.3.1`)
- Repo URL & branch
- Skip TLS verify? (yes if your Gitea uses self-signed certs)
The script will:
1. Download Debian 12 template if missing.
2. Create the LXC, start it.
3. Inside the LXC: clone the repo, install everything, enable systemd.
4. Print the URL + root password + management commands.
To skip prompts (CI / scripted re-deploys):
```bash
CTID=210 HOSTNAME=busbar DISK_SIZE=4 CORES=2 RAM=1024 \
BRIDGE=vmbr0 IP=dhcp STORAGE=local-lvm \
REPO_URL=https://gitea.local/me/busbar-designer.git BRANCH=main \
bash deploy/proxmox-lxc.sh
```
---
## Path B — inside an existing LXC / VM / bare server
Already have a Debian 12 / Ubuntu 22.04 / 24.04 host? Just run the installer:
```bash
REPO_URL=https://gitea.local/me/busbar-designer.git \
bash -c "$(curl -fsSL https://gitea.local/me/busbar-designer/raw/branch/main/deploy/install.sh)"
```
Defaults to `/opt/busbar-designer`, user `busbar`, port `5000`. Override with `INSTALL_DIR`, `SVC_USER`, `PORT`.
---
## Updating
From the Proxmox host:
```bash
pct exec 210 -- bash /opt/busbar-designer/deploy/update.sh
```
From inside the LXC:
```bash
sudo bash /opt/busbar-designer/deploy/update.sh
```
The updater does `git fetch + reset --hard` on the tracked branch, refreshes Python deps, restarts the service.
### Optional: auto-deploy via Gitea webhook
In your Gitea repo → **Settings → Webhooks → Add Webhook (Gitea)**:
- URL: `http://<lxc-ip>:5050/hook` (you'd need to add a tiny webhook listener; not built-in)
- Trigger: `Push`
- Branch filter: `main`
Out of the box there's no webhook endpoint — the simplest path is a cron `*/5 * * * * root bash /opt/busbar-designer/deploy/update.sh` if you want polling, or just SSH and re-run `update.sh` after each push.
---
## Backup & restore
Everything user-generated lives in `/opt/busbar-designer/data/busbar.db` (SQLite, single file).
```bash
# Backup (from Proxmox host)
pct exec 210 -- cat /opt/busbar-designer/data/busbar.db > busbar-backup-$(date +%F).db
# Restore
cat busbar-backup-2026-05-24.db | pct exec 210 -- bash -c \
'systemctl stop busbar-designer && cat > /opt/busbar-designer/data/busbar.db && chown busbar:busbar /opt/busbar-designer/data/busbar.db && systemctl start busbar-designer'
```
Or via Proxmox's own LXC backup (`vzdump`) which captures the whole rootfs.
---
## Troubleshooting
| Symptom | Fix |
|--------------------------------------------------|---------------------------------------------------------------------------------------|
| `pveversion: command not found` | The Proxmox script is not running on a PVE host. Use `install.sh` directly inside the LXC. |
| Git clone fails with TLS error | Set `GIT_SSL_NO_VERIFY=1` env var, or install your Gitea CA cert into `/usr/local/share/ca-certificates`. |
| Service starts, then dies | `pct exec <CTID> -- journalctl -u busbar-designer -n 100`. Usually the build123d wheel didn't install — Debian 12 ships Python 3.11 which is fine. |
| Browser shows 502 / can't connect | Check the LXC's IP with `pct exec <CTID> -- ip a`. If using DHCP, the IP may have changed. |
| `pveam download` fails | Run `pveam update` on the host first. |
+38
View File
@@ -0,0 +1,38 @@
; systemd unit for running busbar-designer in a Proxmox LXC (or any Linux VM)
; without Docker. Assumes the project lives at /opt/busbar-designer and you've
; created a venv there with `python3 -m venv .venv && .venv/bin/pip install -r
; requirements.txt gunicorn`.
;
; Install:
; sudo cp deploy/busbar-designer.service /etc/systemd/system/
; sudo systemctl daemon-reload
; sudo systemctl enable --now busbar-designer
;
; Logs: journalctl -u busbar-designer -f
[Unit]
Description=Busbar Designer (Flask + build123d)
After=network-online.target
Wants=network-online.target
[Service]
Type=simple
User=busbar
Group=busbar
WorkingDirectory=/opt/busbar-designer
Environment=HOST=0.0.0.0
Environment=PORT=5000
Environment=FLASK_DEBUG=0
Environment=PATH=/opt/busbar-designer/.venv/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
ExecStart=/opt/busbar-designer/.venv/bin/gunicorn --bind=0.0.0.0:5000 --workers=2 --threads=2 --timeout=120 app:app
Restart=on-failure
RestartSec=5
; Hardening
NoNewPrivileges=true
PrivateTmp=true
ProtectSystem=strict
ProtectHome=true
ReadWritePaths=/opt/busbar-designer
[Install]
WantedBy=multi-user.target
+121
View File
@@ -0,0 +1,121 @@
#!/usr/bin/env bash
# install.sh — install Busbar Designer on Debian / Ubuntu.
#
# Runs inside an LXC, VM, or bare-metal host. Idempotent (safe to re-run).
#
# Required env:
# REPO_URL Git URL of the busbar-designer repo
# (e.g. https://gitea.local/me/busbar-designer.git)
#
# Optional env:
# BRANCH git branch to track (default: main)
# INSTALL_DIR install location (default: /opt/busbar-designer)
# SVC_USER systemd service user (default: busbar)
# PORT HTTP port (default: 5000)
# GIT_SSL_NO_VERIFY=1 skip TLS verify for self-signed Gitea certs
set -euo pipefail
REPO_URL="${REPO_URL:-}"
BRANCH="${BRANCH:-main}"
INSTALL_DIR="${INSTALL_DIR:-/opt/busbar-designer}"
SVC_USER="${SVC_USER:-busbar}"
PORT="${PORT:-5000}"
# ---- helpers ---------------------------------------------------------------
GREEN='\033[0;32m'; YELLOW='\033[1;33m'; RED='\033[0;31m'; NC='\033[0m'
log() { echo -e "${GREEN}${NC} $*"; }
warn() { echo -e "${YELLOW}!${NC} $*"; }
die() { echo -e "${RED}$*${NC}" >&2; exit 1; }
[[ $EUID -eq 0 ]] || die "Run as root (or via sudo)."
[[ -n "$REPO_URL" ]] || die "REPO_URL is required (export REPO_URL=https://gitea.local/me/busbar-designer.git)"
if [[ "${GIT_SSL_NO_VERIFY:-0}" == "1" ]]; then
warn "GIT_SSL_NO_VERIFY=1 — skipping TLS verification for git."
export GIT_SSL_NO_VERIFY=true
fi
# ---- system packages -------------------------------------------------------
log "Installing system packages..."
export DEBIAN_FRONTEND=noninteractive
apt-get update -qq
apt-get install -y -qq \
git ca-certificates curl \
python3 python3-venv python3-pip \
libgl1 libglu1-mesa libxrender1 libxext6 libsm6 libgomp1
# ---- service user ----------------------------------------------------------
if ! id "$SVC_USER" >/dev/null 2>&1; then
log "Creating service user '$SVC_USER'..."
useradd --system --create-home --shell /usr/sbin/nologin "$SVC_USER"
else
log "Service user '$SVC_USER' already exists."
fi
# ---- source tree -----------------------------------------------------------
mkdir -p "$(dirname "$INSTALL_DIR")"
if [[ -d "$INSTALL_DIR/.git" ]]; then
log "Updating existing checkout at $INSTALL_DIR..."
chown -R "$SVC_USER:$SVC_USER" "$INSTALL_DIR"
sudo -u "$SVC_USER" git -C "$INSTALL_DIR" remote set-url origin "$REPO_URL"
sudo -u "$SVC_USER" git -C "$INSTALL_DIR" fetch --depth 1 origin "$BRANCH"
sudo -u "$SVC_USER" git -C "$INSTALL_DIR" reset --hard "origin/$BRANCH"
else
log "Cloning $REPO_URL (branch $BRANCH) into $INSTALL_DIR..."
rm -rf "$INSTALL_DIR"
git clone --depth 1 --branch "$BRANCH" "$REPO_URL" "$INSTALL_DIR"
chown -R "$SVC_USER:$SVC_USER" "$INSTALL_DIR"
fi
# ---- virtualenv + python deps ---------------------------------------------
if [[ ! -x "$INSTALL_DIR/.venv/bin/python" ]]; then
log "Creating Python venv..."
sudo -u "$SVC_USER" python3 -m venv "$INSTALL_DIR/.venv"
fi
log "Installing Python dependencies (build123d pulls OpenCASCADE — may take a few minutes)..."
sudo -u "$SVC_USER" "$INSTALL_DIR/.venv/bin/pip" install --quiet --upgrade pip
sudo -u "$SVC_USER" "$INSTALL_DIR/.venv/bin/pip" install --quiet \
-r "$INSTALL_DIR/requirements.txt" gunicorn
# ---- data dir (for SQLite) -------------------------------------------------
sudo -u "$SVC_USER" mkdir -p "$INSTALL_DIR/data"
# ---- systemd unit ----------------------------------------------------------
log "Installing systemd unit..."
UNIT_SRC="$INSTALL_DIR/deploy/busbar-designer.service"
UNIT_DST="/etc/systemd/system/busbar-designer.service"
[[ -f "$UNIT_SRC" ]] || die "deploy/busbar-designer.service missing in the repo."
# Substitute paths / user / port into the unit.
sed -e "s|/opt/busbar-designer|$INSTALL_DIR|g" \
-e "s|User=busbar|User=$SVC_USER|g" \
-e "s|Group=busbar|Group=$SVC_USER|g" \
-e "s|--bind=0.0.0.0:5000|--bind=0.0.0.0:$PORT|g" \
-e "s|Environment=PORT=5000|Environment=PORT=$PORT|g" \
"$UNIT_SRC" > "$UNIT_DST"
systemctl daemon-reload
systemctl enable --now busbar-designer
sleep 2
if ! systemctl is-active --quiet busbar-designer; then
warn "Service is not active. Last 30 log lines:"
journalctl -u busbar-designer -n 30 --no-pager
die "Service failed to start."
fi
IP_ADDR=$(hostname -I 2>/dev/null | awk '{print $1}')
[[ -n "$IP_ADDR" ]] || IP_ADDR="$(hostname)"
echo
log "================================================================"
log " ✓ Busbar Designer installed."
log ""
log " URL: http://$IP_ADDR:$PORT"
log " Logs: journalctl -u busbar-designer -f"
log " Update: sudo bash $INSTALL_DIR/deploy/update.sh"
log " Backup: cp $INSTALL_DIR/data/busbar.db ..."
log "================================================================"
+195
View File
@@ -0,0 +1,195 @@
#!/usr/bin/env bash
# proxmox-lxc.sh — create an unprivileged Debian 12 LXC on a Proxmox VE host
# and install Busbar Designer into it. Inspired by the community-scripts style.
#
# Run on the Proxmox host as root, e.g.:
#
# bash -c "$(curl -fsSL https://gitea.local/me/busbar-designer/raw/branch/main/deploy/proxmox-lxc.sh)"
#
# Interactive whiptail prompts; or pre-set everything via env to skip prompts:
#
# REPO_URL=https://gitea.local/me/busbar-designer.git \
# CTID=210 HOSTNAME=busbar DISK_SIZE=4 CORES=2 RAM=1024 \
# BRIDGE=vmbr0 IP=dhcp STORAGE=local-lvm \
# bash proxmox-lxc.sh
set -euo pipefail
# ---- colors ----------------------------------------------------------------
GREEN='\033[0;32m'; YELLOW='\033[1;33m'; RED='\033[0;31m'
BLUE='\033[0;34m'; BOLD='\033[1m'; NC='\033[0m'
log() { echo -e "${GREEN}${NC} $*"; }
warn() { echo -e "${YELLOW}!${NC} $*"; }
die() { echo -e "${RED}$*${NC}" >&2; exit 1; }
banner() {
cat <<'EOF'
____ _ ____ _
| __ ) _ _ ___| |__ __ _ _ __ | _ \ ___ ___(_) __ _ _ __ ___ _ __
| _ \| | | / __| '_ \ / _` | '__| | | | |/ _ \/ __| |/ _` | '_ \ / _ \ '__|
| |_) | |_| \__ \ |_) | (_| | | | |_| | __/\__ \ | (_| | | | | __/ |
|____/ \__,_|___/_.__/ \__,_|_| |____/ \___||___/_|\__, |_| |_|\___|_|
|___/
Proxmox VE LXC installer
EOF
}
# ---- preflight -------------------------------------------------------------
[[ $EUID -eq 0 ]] || die "Run as root on the Proxmox host."
command -v pveversion >/dev/null 2>&1 || die "pveversion not found — is this a Proxmox VE host?"
command -v pct >/dev/null 2>&1 || die "pct not found — Proxmox VE tools missing?"
banner
# ---- defaults --------------------------------------------------------------
CTID_DEFAULT=$(pvesh get /cluster/nextid 2>/dev/null || echo "200")
CTID="${CTID:-$CTID_DEFAULT}"
HOSTNAME="${HOSTNAME:-busbar-designer}"
DISK_SIZE="${DISK_SIZE:-4}"
CORES="${CORES:-2}"
RAM="${RAM:-1024}"
SWAP="${SWAP:-512}"
BRIDGE="${BRIDGE:-vmbr0}"
IP="${IP:-dhcp}"
STORAGE="${STORAGE:-local-lvm}"
TEMPLATE_STORAGE="${TEMPLATE_STORAGE:-local}"
REPO_URL="${REPO_URL:-}"
BRANCH="${BRANCH:-main}"
GIT_SSL_NO_VERIFY="${GIT_SSL_NO_VERIFY:-0}"
# ---- interactive prompts (whiptail) ----------------------------------------
ask() {
local var="$1" prompt="$2" default="$3" h="${4:-8}" w="${5:-60}"
local val
if [[ -t 0 ]] && command -v whiptail >/dev/null 2>&1; then
val=$(whiptail --title "Busbar Designer" --inputbox "$prompt" "$h" "$w" "$default" \
3>&1 1>&2 2>&3) || die "Cancelled."
else
read -rp "$prompt [$default]: " val
val="${val:-$default}"
fi
printf -v "$var" '%s' "$val"
}
yesno() {
local prompt="$1" default="${2:-no}"
if [[ -t 0 ]] && command -v whiptail >/dev/null 2>&1; then
if [[ "$default" == "yes" ]]; then
whiptail --title "Busbar Designer" --yesno "$prompt" 8 60
else
whiptail --title "Busbar Designer" --yesno "$prompt" --defaultno 8 60
fi
else
read -rp "$prompt [$default]: " val
[[ "${val:-$default}" =~ ^(y|yes)$ ]]
fi
}
if [[ -z "$REPO_URL" ]] || [[ -t 0 ]]; then
ask CTID "Container ID" "$CTID"
ask HOSTNAME "Hostname" "$HOSTNAME"
ask DISK_SIZE "Root disk size (GB)" "$DISK_SIZE"
ask CORES "CPU cores" "$CORES"
ask RAM "RAM (MB)" "$RAM"
ask STORAGE "Storage pool for rootfs (e.g. local-lvm, local-zfs)" "$STORAGE" 8 70
ask BRIDGE "Network bridge" "$BRIDGE"
ask IP "IP config: 'dhcp' or 'a.b.c.d/24,gw=a.b.c.1'" "$IP" 8 70
ask REPO_URL "Git URL of busbar-designer repo (your Gitea / GitHub)" \
"${REPO_URL:-https://gitea.local/me/busbar-designer.git}" 10 70
ask BRANCH "Branch" "$BRANCH"
if yesno "Skip TLS verification for git? (only if your Gitea uses a self-signed cert)"; then
GIT_SSL_NO_VERIFY=1
fi
fi
[[ -n "$REPO_URL" ]] || die "REPO_URL is required."
# ---- template --------------------------------------------------------------
log "Looking for a Debian 12 template..."
TEMPLATE=$(pveam available --section system 2>/dev/null \
| awk '/debian-12-standard/ {print $2}' | sort -r | head -n 1)
[[ -n "$TEMPLATE" ]] || die "Couldn't find debian-12-standard in 'pveam available'. Run 'pveam update' first."
LOCAL_TEMPLATE="/var/lib/vz/template/cache/$TEMPLATE"
if [[ ! -f "$LOCAL_TEMPLATE" ]]; then
log "Downloading template $TEMPLATE..."
pveam download "$TEMPLATE_STORAGE" "$TEMPLATE"
fi
# ---- create LXC ------------------------------------------------------------
if pct status "$CTID" >/dev/null 2>&1; then
die "Container $CTID already exists. Pick a different CTID."
fi
PASSWORD="$(openssl rand -base64 12 | tr -d '/+=' | cut -c1-16)"
if [[ "$IP" == "dhcp" ]]; then
NET="name=eth0,bridge=$BRIDGE,ip=dhcp"
else
NET="name=eth0,bridge=$BRIDGE,ip=$IP"
fi
log "Creating LXC $CTID ($HOSTNAME)..."
pct create "$CTID" "$TEMPLATE_STORAGE:vztmpl/$TEMPLATE" \
--hostname "$HOSTNAME" \
--cores "$CORES" \
--memory "$RAM" \
--swap "$SWAP" \
--rootfs "$STORAGE:$DISK_SIZE" \
--net0 "$NET" \
--password "$PASSWORD" \
--features nesting=1 \
--unprivileged 1 \
--onboot 1 \
--start 1 \
--description "Busbar Designer · $REPO_URL ($BRANCH)" >/dev/null
# ---- wait for network ------------------------------------------------------
log "Waiting for network in CT $CTID..."
for i in {1..30}; do
if pct exec "$CTID" -- bash -c "getent hosts deb.debian.org >/dev/null" 2>/dev/null; then
break
fi
sleep 2
done
# ---- run installer inside the LXC ------------------------------------------
log "Bootstrapping git + curl in the container..."
pct exec "$CTID" -- bash -c "
set -e
export DEBIAN_FRONTEND=noninteractive
apt-get update -qq
apt-get install -y -qq git ca-certificates curl
" || die "Failed to install bootstrap packages in CT."
log "Cloning repo and running deploy/install.sh inside CT $CTID..."
pct exec "$CTID" -- bash -c "
set -e
${GIT_SSL_NO_VERIFY:+export GIT_SSL_NO_VERIFY=true}
rm -rf /opt/busbar-designer
git clone --depth 1 -b '$BRANCH' '$REPO_URL' /opt/busbar-designer
REPO_URL='$REPO_URL' BRANCH='$BRANCH' \
${GIT_SSL_NO_VERIFY:+GIT_SSL_NO_VERIFY=1} \
bash /opt/busbar-designer/deploy/install.sh
" || die "Installer failed. Inspect with: pct enter $CTID"
# ---- report ---------------------------------------------------------------
IP_ADDR=$(pct exec "$CTID" -- bash -c "hostname -I | awk '{print \$1}'" 2>/dev/null || true)
[[ -n "$IP_ADDR" ]] || IP_ADDR="<unknown — check 'pct exec $CTID -- ip a'>"
echo
log "================================================================"
log " ${BOLD}✓ Busbar Designer LXC ready${NC}"
log ""
log " Container ID: $CTID"
log " Hostname: $HOSTNAME"
log " Root password: $PASSWORD"
log " URL: http://$IP_ADDR:5000"
log ""
log " Update: pct exec $CTID -- bash /opt/busbar-designer/deploy/update.sh"
log " Logs: pct exec $CTID -- journalctl -u busbar-designer -f"
log " Enter: pct enter $CTID"
log " Backup: pct exec $CTID -- cat /opt/busbar-designer/data/busbar.db > backup.db"
log "================================================================"
+45
View File
@@ -0,0 +1,45 @@
#!/usr/bin/env bash
# update.sh — pull the latest from the configured branch, refresh deps, restart.
#
# Optional env (override defaults set at install time):
# INSTALL_DIR default /opt/busbar-designer
# SVC_USER default busbar
# BRANCH default main (or whatever the local checkout tracks)
set -euo pipefail
INSTALL_DIR="${INSTALL_DIR:-/opt/busbar-designer}"
SVC_USER="${SVC_USER:-busbar}"
BRANCH="${BRANCH:-}"
GREEN='\033[0;32m'; RED='\033[0;31m'; NC='\033[0m'
log() { echo -e "${GREEN}${NC} $*"; }
die() { echo -e "${RED}$*${NC}" >&2; exit 1; }
[[ $EUID -eq 0 ]] || die "Run as root."
[[ -d "$INSTALL_DIR/.git" ]] || die "$INSTALL_DIR is not a git checkout."
cd "$INSTALL_DIR"
# Use the branch the working copy tracks if not overridden.
if [[ -z "$BRANCH" ]]; then
BRANCH=$(sudo -u "$SVC_USER" git -C "$INSTALL_DIR" rev-parse --abbrev-ref HEAD)
fi
log "Pulling origin/$BRANCH..."
sudo -u "$SVC_USER" git -C "$INSTALL_DIR" fetch --depth 1 origin "$BRANCH"
sudo -u "$SVC_USER" git -C "$INSTALL_DIR" reset --hard "origin/$BRANCH"
log "Refreshing Python deps..."
sudo -u "$SVC_USER" "$INSTALL_DIR/.venv/bin/pip" install --quiet --upgrade \
-r "$INSTALL_DIR/requirements.txt" gunicorn
log "Restarting busbar-designer..."
systemctl restart busbar-designer
sleep 2
if systemctl is-active --quiet busbar-designer; then
log "✓ Updated and restarted."
else
die "Service failed after restart. journalctl -u busbar-designer -n 50"
fi