Initial commit: VoIdeaAI - voice-first AI idea assistant
This commit is contained in:
@@ -0,0 +1,85 @@
|
||||
"""Daily PostgreSQL backup with 7-day retention.
|
||||
|
||||
Cron: 0 3 * * * cd /opt/voidea && python tools/backup_db.py
|
||||
"""
|
||||
|
||||
import datetime
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
BACKUP_DIR = os.environ.get("BACKUP_DIR", "/opt/voidea/backups")
|
||||
RETENTION_DAYS = int(os.environ.get("BACKUP_RETENTION_DAYS", "7"))
|
||||
DB_URL = os.environ.get("DATABASE_URL", "")
|
||||
|
||||
|
||||
def parse_db_url(url: str) -> dict:
|
||||
"""Parse DATABASE_URL into pg_dump arguments."""
|
||||
if not url:
|
||||
print("ERROR: DATABASE_URL not set", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
parts = url.replace("postgresql://", "").replace("postgres://", "").split("@")
|
||||
user_pass = parts[0].split(":")
|
||||
host_db = parts[1].split("/")
|
||||
host_port = host_db[0].split(":")
|
||||
return {
|
||||
"user": user_pass[0],
|
||||
"password": user_pass[1] if len(user_pass) > 1 else "",
|
||||
"host": host_port[0],
|
||||
"port": host_port[1] if len(host_port) > 1 else "5432",
|
||||
"dbname": host_db[1].split("?")[0],
|
||||
}
|
||||
|
||||
|
||||
def main():
|
||||
os.makedirs(BACKUP_DIR, exist_ok=True)
|
||||
config = parse_db_url(DB_URL)
|
||||
|
||||
timestamp = datetime.datetime.now().strftime("%Y%m%d_%H%M%S")
|
||||
filename = f"voidea_backup_{timestamp}.sql.gz"
|
||||
filepath = os.path.join(BACKUP_DIR, filename)
|
||||
|
||||
env = os.environ.copy()
|
||||
env["PGPASSWORD"] = config["password"]
|
||||
|
||||
cmd = [
|
||||
"pg_dump",
|
||||
"-h", config["host"],
|
||||
"-p", config["port"],
|
||||
"-U", config["user"],
|
||||
"-d", config["dbname"],
|
||||
"--clean",
|
||||
"--if-exists",
|
||||
"--no-owner",
|
||||
]
|
||||
|
||||
print(f"Backing up to {filepath} ...")
|
||||
with open(filepath, "wb") as f:
|
||||
proc = subprocess.Popen(cmd, env=env, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
||||
gzip = subprocess.Popen(["gzip"], stdin=proc.stdout, stdout=f, stderr=subprocess.PIPE)
|
||||
_, stderr = gzip.communicate()
|
||||
|
||||
if proc.returncode != 0 or gzip.returncode != 0:
|
||||
print(f"ERROR: Backup failed: {stderr.decode()}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
file_size = os.path.getsize(filepath)
|
||||
print(f"Backup complete: {filepath} ({file_size / 1024 / 1024:.1f} MB)")
|
||||
|
||||
# Cleanup old backups
|
||||
cutoff = datetime.datetime.now() - datetime.timedelta(days=RETENTION_DAYS)
|
||||
removed = 0
|
||||
for fname in os.listdir(BACKUP_DIR):
|
||||
fpath = os.path.join(BACKUP_DIR, fname)
|
||||
if not os.path.isfile(fpath):
|
||||
continue
|
||||
mtime = datetime.datetime.fromtimestamp(os.path.getmtime(fpath))
|
||||
if mtime < cutoff:
|
||||
os.remove(fpath)
|
||||
removed += 1
|
||||
|
||||
print(f"Cleaned up {removed} old backup(s) (retention: {RETENTION_DAYS} days)")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,33 @@
|
||||
"""Generate version.json for frontend from VERSION file.
|
||||
|
||||
Run before frontend build:
|
||||
python tools/generate_version.py > webui/public/version.json
|
||||
"""
|
||||
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def main():
|
||||
root = Path(__file__).resolve().parent.parent
|
||||
|
||||
# Read project version
|
||||
version_file = root / "VERSION"
|
||||
project_version = version_file.read_text(encoding="utf-8").strip()
|
||||
|
||||
# Read agent versions
|
||||
agent_versions_file = root / "AGENT_VERSIONS.json"
|
||||
agent_versions = {}
|
||||
if agent_versions_file.exists():
|
||||
agent_versions = json.loads(agent_versions_file.read_text(encoding="utf-8"))
|
||||
|
||||
output = {
|
||||
"version": project_version,
|
||||
"agentVersions": agent_versions,
|
||||
}
|
||||
|
||||
print(json.dumps(output, ensure_ascii=False, indent=2))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,126 @@
|
||||
"""Simple system metrics collector for VoIdea VPS.
|
||||
|
||||
Runs as a periodic task (Celery or cron) and logs:
|
||||
- CPU load
|
||||
- Memory usage
|
||||
- Disk usage
|
||||
- PostgreSQL connection count
|
||||
"""
|
||||
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import platform
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
from datetime import datetime, timezone
|
||||
|
||||
logger = logging.getLogger("voidea.metrics")
|
||||
|
||||
|
||||
def get_cpu_load() -> float:
|
||||
"""Return CPU load average (1 min) or 0.0."""
|
||||
try:
|
||||
if platform.system() == "Linux":
|
||||
load1, _, _ = os.getloadavg()
|
||||
return round(load1, 2)
|
||||
return 0.0
|
||||
except OSError:
|
||||
return 0.0
|
||||
|
||||
|
||||
def get_memory_usage() -> dict:
|
||||
"""Return memory usage in MB."""
|
||||
try:
|
||||
import psutil
|
||||
mem = psutil.virtual_memory()
|
||||
return {
|
||||
"total_mb": round(mem.total / 1024 / 1024, 1),
|
||||
"available_mb": round(mem.available / 1024 / 1024, 1),
|
||||
"percent_used": mem.percent,
|
||||
}
|
||||
except ImportError:
|
||||
return {"error": "psutil not installed"}
|
||||
|
||||
|
||||
def get_disk_usage(path: str = "/") -> dict:
|
||||
"""Return disk usage for the given path."""
|
||||
try:
|
||||
usage = shutil.disk_usage(path)
|
||||
return {
|
||||
"total_gb": round(usage.total / 1024**3, 1),
|
||||
"used_gb": round(usage.used / 1024**3, 1),
|
||||
"free_gb": round(usage.free / 1024**3, 1),
|
||||
"percent_used": round(usage.used / usage.total * 100, 1),
|
||||
}
|
||||
except OSError:
|
||||
return {"error": f"cannot access {path}"}
|
||||
|
||||
|
||||
def get_pg_connections(db_url: str | None = None) -> int:
|
||||
"""Return active PostgreSQL connection count."""
|
||||
url = db_url or os.environ.get("DATABASE_URL", "")
|
||||
if not url:
|
||||
return -1
|
||||
try:
|
||||
parts = url.replace("postgresql://", "").replace("postgres://", "").split("@")
|
||||
user_pass = parts[0].split(":")
|
||||
host_db = parts[1].split("/")
|
||||
host_port = host_db[0].split(":")
|
||||
config = {
|
||||
"host": host_port[0],
|
||||
"port": host_port[1] if len(host_port) > 1 else "5432",
|
||||
"user": user_pass[0],
|
||||
"dbname": host_db[1].split("?")[0],
|
||||
}
|
||||
env = os.environ.copy()
|
||||
env["PGPASSWORD"] = user_pass[1] if len(user_pass) > 1 else ""
|
||||
result = subprocess.run(
|
||||
[
|
||||
"psql",
|
||||
"-h", config["host"],
|
||||
"-p", config["port"],
|
||||
"-U", config["user"],
|
||||
"-d", config["dbname"],
|
||||
"-tA",
|
||||
"-c", "SELECT count(*) FROM pg_stat_activity;",
|
||||
],
|
||||
env=env,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=5,
|
||||
)
|
||||
return int(result.stdout.strip())
|
||||
except (ValueError, subprocess.TimeoutExpired, OSError):
|
||||
return -1
|
||||
|
||||
|
||||
def collect() -> dict:
|
||||
"""Collect all metrics into a single dict."""
|
||||
return {
|
||||
"timestamp": datetime.now(timezone.utc).isoformat(),
|
||||
"hostname": platform.node(),
|
||||
"cpu_load_1min": get_cpu_load(),
|
||||
"memory": get_memory_usage(),
|
||||
"disk": get_disk_usage("/"),
|
||||
"pg_connections": get_pg_connections(),
|
||||
}
|
||||
|
||||
|
||||
def main():
|
||||
logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s")
|
||||
metrics = collect()
|
||||
log_line = json.dumps(metrics, ensure_ascii=False)
|
||||
logger.info("Metrics: %s", log_line)
|
||||
|
||||
# Optionally write to a file for prometheus/node_exporter
|
||||
metrics_file = os.environ.get("METRICS_FILE", "")
|
||||
if metrics_file:
|
||||
os.makedirs(os.path.dirname(metrics_file), exist_ok=True)
|
||||
with open(metrics_file, "w", encoding="utf-8") as f:
|
||||
f.write(log_line + "\n")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user