Browse Source

Add installer and portability fixes

main
def 2 weeks ago
parent
commit
c17570cfd7
  1. 17
      README.md
  2. 10
      backend/app.py
  3. 31
      backend/db.py
  4. 143
      install.sh

17
README.md

@ -73,3 +73,20 @@ Notes
- Core billing/export/report workflow is now operational. - Core billing/export/report workflow is now operational.
- Email log table records invoice, revenue report, and accounting package sends. - Email log table records invoice, revenue report, and accounting package sends.
## Installer
From the project root:
```bash
chmod +x install.sh
./install.sh
## Installer
From the project root:
chmod +x install.sh
./install.sh
The installer will install dependencies, configure MariaDB, create the database, and start the service.

10
backend/app.py

@ -4,6 +4,7 @@ from utils import generate_client_code, generate_service_code
from datetime import datetime, timezone from datetime import datetime, timezone
from zoneinfo import ZoneInfo from zoneinfo import ZoneInfo
from decimal import Decimal, InvalidOperation from decimal import Decimal, InvalidOperation
from pathlib import Path
from email.message import EmailMessage from email.message import EmailMessage
from io import BytesIO, StringIO from io import BytesIO, StringIO
@ -22,9 +23,12 @@ app = Flask(
LOCAL_TZ = ZoneInfo("America/Toronto") LOCAL_TZ = ZoneInfo("America/Toronto")
BASE_DIR = Path(__file__).resolve().parent.parent
def load_version(): def load_version():
try: try:
with open("/home/def/otb_billing/VERSION", "r") as f: with open(BASE_DIR / "VERSION", "r") as f:
return f.read().strip() return f.read().strip()
except Exception: except Exception:
return "unknown" return "unknown"
@ -1389,7 +1393,7 @@ def export_invoices_pdf_zip():
logo_url = (settings.get("business_logo_url") or "").strip() logo_url = (settings.get("business_logo_url") or "").strip()
if logo_url.startswith("/static/"): if logo_url.startswith("/static/"):
local_logo_path = "/home/def/otb_billing" + logo_url local_logo_path = str(BASE_DIR) + logo_url
try: try:
pdf.drawImage(ImageReader(local_logo_path), left, y - 35, width=42, height=42, preserveAspectRatio=True, mask='auto') pdf.drawImage(ImageReader(local_logo_path), left, y - 35, width=42, height=42, preserveAspectRatio=True, mask='auto')
except Exception: except Exception:
@ -1879,7 +1883,7 @@ def invoice_pdf(invoice_id):
logo_url = (settings.get("business_logo_url") or "").strip() logo_url = (settings.get("business_logo_url") or "").strip()
if logo_url.startswith("/static/"): if logo_url.startswith("/static/"):
local_logo_path = "/home/def/otb_billing" + logo_url local_logo_path = str(BASE_DIR) + logo_url
try: try:
pdf.drawImage(ImageReader(local_logo_path), left, y - 35, width=42, height=42, preserveAspectRatio=True, mask='auto') pdf.drawImage(ImageReader(local_logo_path), left, y - 35, width=42, height=42, preserveAspectRatio=True, mask='auto')
except Exception: except Exception:

31
backend/db.py

@ -1,11 +1,26 @@
import os
import mysql.connector import mysql.connector
from config import Config
def get_db_connection(): def get_db_connection():
return mysql.connector.connect( host = os.getenv("OTB_BILLING_DB_HOST", "127.0.0.1")
host=Config.DB_HOST, port = int(os.getenv("OTB_BILLING_DB_PORT", "3306"))
port=Config.DB_PORT, database = os.getenv("OTB_BILLING_DB_NAME", "otb_billing")
user=Config.DB_USER, user = os.getenv("OTB_BILLING_DB_USER", "otb_billing")
password=Config.DB_PASSWORD, password = os.getenv("OTB_BILLING_DB_PASSWORD", "")
database=Config.DB_NAME unix_socket = os.getenv("OTB_BILLING_DB_SOCKET", "").strip()
)
kwargs = {
"database": database,
"user": user,
"password": password,
"autocommit": False,
}
if unix_socket:
kwargs["unix_socket"] = unix_socket
else:
kwargs["host"] = host
kwargs["port"] = port
return mysql.connector.connect(**kwargs)

143
install.sh

@ -0,0 +1,143 @@
#!/usr/bin/env bash
set -euo pipefail
APP_NAME="otb_billing"
APP_USER_DEFAULT="${SUDO_USER:-$USER}"
APP_ROOT_DEFAULT="/opt/otb_billing"
ENV_DIR="/etc/otb_billing"
ENV_FILE="${ENV_DIR}/otb_billing.env"
SERVICE_NAME="otb-billing"
SCHEMA_FILE="sql/schema_v0.0.2.sql"
if [[ ! -f "$SCHEMA_FILE" ]]; then
echo "ERROR: $SCHEMA_FILE not found. Run this from the repo root."
exit 1
fi
echo "=================================================="
echo "OTB Billing installer"
echo "=================================================="
read -rp "Install app user [${APP_USER_DEFAULT}]: " APP_USER
APP_USER="${APP_USER:-$APP_USER_DEFAULT}"
read -rp "Install path [${APP_ROOT_DEFAULT}]: " APP_ROOT
APP_ROOT="${APP_ROOT:-$APP_ROOT_DEFAULT}"
read -rp "MariaDB database name [otb_billing]: " DB_NAME
DB_NAME="${DB_NAME:-otb_billing}"
read -rp "MariaDB database user [otb_billing]: " DB_USER
DB_USER="${DB_USER:-otb_billing}"
read -rsp "MariaDB database password: " DB_PASS
echo
if [[ -z "$DB_PASS" ]]; then
echo "ERROR: database password cannot be blank."
exit 1
fi
read -rp "App listen host [0.0.0.0]: " APP_HOST
APP_HOST="${APP_HOST:-0.0.0.0}"
read -rp "App listen port [5050]: " APP_PORT
APP_PORT="${APP_PORT:-5050}"
echo
echo "Installing system packages..."
sudo apt-get update
sudo DEBIAN_FRONTEND=noninteractive apt-get install -y \
python3 python3-venv python3-pip mariadb-server rsync
echo
echo "Creating app directory..."
sudo mkdir -p "$APP_ROOT"
sudo rsync -a --delete \
--exclude '.git' \
--exclude '__pycache__' \
--exclude 'releases' \
--exclude '*.pyc' \
./ "$APP_ROOT"/
sudo chown -R "$APP_USER":"$APP_USER" "$APP_ROOT"
echo
echo "Creating Python virtualenv..."
sudo -u "$APP_USER" python3 -m venv "$APP_ROOT/venv"
sudo -u "$APP_USER" "$APP_ROOT/venv/bin/pip" install --upgrade pip
sudo -u "$APP_USER" "$APP_ROOT/venv/bin/pip" install -r "$APP_ROOT/requirements.txt"
echo
echo "Ensuring MariaDB is running..."
sudo systemctl enable mariadb
sudo systemctl restart mariadb
echo
echo "Creating MariaDB database and user..."
sudo mysql <<SQL
CREATE DATABASE IF NOT EXISTS \`${DB_NAME}\` CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
CREATE USER IF NOT EXISTS '${DB_USER}'@'localhost' IDENTIFIED BY '${DB_PASS}';
ALTER USER '${DB_USER}'@'localhost' IDENTIFIED BY '${DB_PASS}';
GRANT ALL PRIVILEGES ON \`${DB_NAME}\`.* TO '${DB_USER}'@'localhost';
FLUSH PRIVILEGES;
SQL
echo
echo "Importing schema..."
sudo mysql "$DB_NAME" < "$APP_ROOT/$SCHEMA_FILE"
echo
echo "Writing environment file..."
sudo mkdir -p "$ENV_DIR"
sudo tee "$ENV_FILE" >/dev/null <<ENVEOF
OTB_BILLING_DB_HOST=127.0.0.1
OTB_BILLING_DB_PORT=3306
OTB_BILLING_DB_NAME=${DB_NAME}
OTB_BILLING_DB_USER=${DB_USER}
OTB_BILLING_DB_PASSWORD=${DB_PASS}
FLASK_ENV=production
HOST=${APP_HOST}
PORT=${APP_PORT}
ENVEOF
sudo chmod 600 "$ENV_FILE"
echo
echo "Creating systemd service..."
sudo tee "/etc/systemd/system/${SERVICE_NAME}.service" >/dev/null <<SERVICEEOF
[Unit]
Description=OTB Billing Flask App
After=network.target mariadb.service
Requires=mariadb.service
[Service]
Type=simple
User=${APP_USER}
WorkingDirectory=${APP_ROOT}
EnvironmentFile=${ENV_FILE}
ExecStart=${APP_ROOT}/venv/bin/python ${APP_ROOT}/backend/app.py
Restart=always
RestartSec=3
[Install]
WantedBy=multi-user.target
SERVICEEOF
echo
echo "Enabling service..."
sudo systemctl daemon-reload
sudo systemctl enable "${SERVICE_NAME}"
sudo systemctl restart "${SERVICE_NAME}"
echo
echo "Verifying service..."
sudo systemctl --no-pager --full status "${SERVICE_NAME}" || true
echo
echo "Installer complete."
echo "App path: ${APP_ROOT}"
echo "Service: ${SERVICE_NAME}"
echo "URL: http://${APP_HOST}:${APP_PORT}"
echo
echo "Useful commands:"
echo " sudo systemctl status ${SERVICE_NAME}"
echo " sudo journalctl -u ${SERVICE_NAME} -f"
Loading…
Cancel
Save