from app.db.database import get_connection from app.db.migrations import run_migrations SUCCESS_STATUSES = ("ok", "success", "succeeded", "done", "deployed", "completed") FAILED_STATUSES = ("failed", "failure", "error", "cancelled", "canceled") RUNNING_STATUSES = ("running", "pending", "queued", "in_progress", "starting") def get_apps(): run_migrations() con = get_connection() rows = con.execute( """ SELECT a.id, a.name, a.language, a.version, a.status, COALESCE(a.memory, '') AS memory, COALESCE(a.cpus, '') AS cpus, a.updated_at, ( SELECT d.status FROM deployments d WHERE d.app_id = a.id ORDER BY d.id DESC LIMIT 1 ) AS last_deploy_status, ( SELECT d.started_at FROM deployments d WHERE d.app_id = a.id ORDER BY d.id DESC LIMIT 1 ) AS last_deploy_started_at FROM apps a ORDER BY a.id """ ).fetchall() con.close() return [dict(row) for row in rows] def get_app(app_id: str): run_migrations() con = get_connection() row = con.execute( """ SELECT id, name, language, version, status, memory, cpus, updated_at, description, owner, template, runtime, repository_url, repository_name, default_branch, domain, health_url, container_port, COALESCE(is_public, 0) AS is_public, COALESCE(is_enabled, 1) AS is_enabled FROM apps WHERE id = ? """, (app_id,), ).fetchone() con.close() return dict(row) if row else None def get_app_templates(create_enabled: bool = False): run_migrations() con = get_connection() create_filter = "AND create_enabled = 1" if create_enabled else "" rows = con.execute( f""" SELECT id, name, runtime, language, description, default_port, default_health_path, create_script, deploy_script, COALESCE(is_enabled, 1) AS is_enabled, COALESCE(create_enabled, 0) AS create_enabled FROM app_templates WHERE COALESCE(is_enabled, 1) = 1 {create_filter} ORDER BY name """ ).fetchall() con.close() return [dict(row) for row in rows] def get_app_template(template_id: str, create_enabled: bool = False): run_migrations() con = get_connection() create_filter = "AND COALESCE(create_enabled, 0) = 1" if create_enabled else "" row = con.execute( f""" SELECT id, name, runtime, language, description, default_port, default_health_path, create_script, deploy_script, COALESCE(is_enabled, 1) AS is_enabled, COALESCE(create_enabled, 0) AS create_enabled FROM app_templates WHERE id = ? AND COALESCE(is_enabled, 1) = 1 {create_filter} """, (template_id,), ).fetchone() con.close() return dict(row) if row else None def update_app_template_metadata(app_id: str, metadata: dict): run_migrations() con = get_connection() con.execute( """ UPDATE apps SET name = ?, template = ?, runtime = ?, language = ?, health_url = ?, container_port = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ? """, ( metadata.get("name") or None, metadata.get("template") or None, metadata.get("runtime") or None, metadata.get("language") or None, metadata.get("health_url") or None, metadata.get("container_port"), app_id, ), ) con.commit() con.close() def upsert_created_app(app_id: str, metadata: dict): run_migrations() con = get_connection() values = ( metadata.get("name") or app_id, metadata.get("language") or None, metadata.get("version") or "1.0.0", metadata.get("status") or "created", metadata.get("memory") or None, metadata.get("cpus") or None, metadata.get("description") or None, metadata.get("owner") or None, metadata.get("template") or None, metadata.get("runtime") or None, metadata.get("repository_url") or None, metadata.get("repository_name") or app_id, metadata.get("default_branch") or "main", metadata.get("health_url") or None, metadata.get("container_port"), 1 if metadata.get("is_public") else 0, 1 if metadata.get("is_enabled") else 0, ) try: cur = con.execute( """ UPDATE apps SET name = ?, language = ?, version = ?, status = ?, memory = ?, cpus = ?, description = ?, owner = ?, template = ?, runtime = ?, repository_url = ?, repository_name = ?, default_branch = ?, health_url = ?, container_port = ?, is_public = ?, is_enabled = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ? """, (*values, app_id), ) if cur.rowcount == 0: con.execute( """ INSERT INTO apps ( id, name, language, version, status, memory, cpus, updated_at, description, owner, template, runtime, repository_url, repository_name, default_branch, health_url, container_port, is_public, is_enabled ) VALUES (?, ?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) """, ( app_id, values[0], values[1], values[2], values[3], values[4], values[5], values[6], values[7], values[8], values[9], values[10], values[11], values[12], values[13], values[14], values[15], values[16], ), ) con.commit() except Exception: con.rollback() raise finally: con.close() def update_app_metadata(app_id: str, metadata: dict): run_migrations() con = get_connection() con.execute( """ UPDATE apps SET name = ?, description = ?, owner = ?, template = ?, runtime = ?, repository_url = ?, repository_name = ?, default_branch = ?, domain = ?, health_url = ?, container_port = ?, is_public = ?, is_enabled = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ? """, ( metadata.get("name") or None, metadata.get("description") or None, metadata.get("owner") or None, metadata.get("template") or None, metadata.get("runtime") or None, metadata.get("repository_url") or None, metadata.get("repository_name") or None, metadata.get("default_branch") or None, metadata.get("domain") or None, metadata.get("health_url") or None, metadata.get("container_port"), 1 if metadata.get("is_public") else 0, 1 if metadata.get("is_enabled") else 0, app_id, ), ) con.commit() con.close() def get_app_variables(app_id: str): run_migrations() con = get_connection() rows = con.execute( """ SELECT id, app_id, "key", value, COALESCE(is_secret, 0) AS is_secret FROM app_variables WHERE app_id = ? ORDER BY "key" """, (app_id,), ).fetchall() con.close() return [dict(row) for row in rows] def create_app_variable(app_id: str, key: str, value: str, is_secret: bool): run_migrations() con = get_connection() con.execute( """ INSERT INTO app_variables (app_id, "key", value, is_secret, created_at, updated_at) VALUES (?, ?, ?, ?, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP) """, (app_id, key, value, 1 if is_secret else 0), ) con.commit() con.close() def update_app_variable(variable_id: int, app_id: str, key: str, value: str | None, is_secret: bool): run_migrations() con = get_connection() if value is None: con.execute( """ UPDATE app_variables SET "key" = ?, is_secret = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ? AND app_id = ? """, (key, 1 if is_secret else 0, variable_id, app_id), ) else: con.execute( """ UPDATE app_variables SET "key" = ?, value = ?, is_secret = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ? AND app_id = ? """, (key, value, 1 if is_secret else 0, variable_id, app_id), ) con.commit() con.close() def delete_app_variable(variable_id: int, app_id: str): run_migrations() con = get_connection() con.execute( """ DELETE FROM app_variables WHERE id = ? AND app_id = ? """, (variable_id, app_id), ) con.commit() con.close() def update_app_resources(app_id: str, memory: str, cpus: str): run_migrations() con = get_connection() con.execute( """ UPDATE apps SET memory = ?, cpus = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ? """, ( memory or None, cpus or None, app_id, ), ) con.commit() con.close() def build_deployment_filters( status: str | None = None, app_id: str | None = None, source: str | None = None, ): filters = [] params = [] if status: normalized_status = status.strip().lower() if normalized_status == "success": filters.append(f"LOWER(COALESCE(status, '')) IN ({','.join('?' for _ in SUCCESS_STATUSES)})") params.extend(SUCCESS_STATUSES) elif normalized_status == "failed": filters.append(f"LOWER(COALESCE(status, '')) IN ({','.join('?' for _ in FAILED_STATUSES)})") params.extend(FAILED_STATUSES) elif normalized_status == "running": filters.append(f"LOWER(COALESCE(status, '')) IN ({','.join('?' for _ in RUNNING_STATUSES)})") params.extend(RUNNING_STATUSES) else: filters.append("LOWER(status) = LOWER(?)") params.append(status) if app_id: filters.append("app_id = ?") params.append(app_id) if source: filters.append("trigger_source = ?") params.append(source) where = f"WHERE {' AND '.join(filters)}" if filters else "" return where, params def get_deployments( limit: int = 20, offset: int = 0, status: str | None = None, app_id: str | None = None, source: str | None = None, ): con = get_connection() where, params = build_deployment_filters(status=status, app_id=app_id, source=source) rows = con.execute( f""" SELECT id, app_id, kind, status, started_at, finished_at, returncode, trigger_source, triggered_by_username, triggered_by_display_name FROM deployments {where} ORDER BY CASE WHEN LOWER(COALESCE(status, '')) IN ({','.join('?' for _ in RUNNING_STATUSES)}) THEN 0 ELSE 1 END, id DESC LIMIT ? OFFSET ? """, (*params, *RUNNING_STATUSES, limit, offset), ).fetchall() con.close() return [dict(row) for row in rows] def count_deployments( status: str | None = None, app_id: str | None = None, source: str | None = None, ): con = get_connection() where, params = build_deployment_filters(status=status, app_id=app_id, source=source) row = con.execute( f""" SELECT COUNT(*) AS count FROM deployments {where} """, params, ).fetchone() con.close() return row["count"] if row else 0 def get_deployment_filter_options(): con = get_connection() apps = con.execute( """ SELECT DISTINCT app_id FROM deployments WHERE app_id IS NOT NULL AND app_id != '' ORDER BY app_id """ ).fetchall() sources = con.execute( """ SELECT DISTINCT trigger_source FROM deployments WHERE trigger_source IS NOT NULL AND trigger_source != '' ORDER BY trigger_source """ ).fetchall() con.close() return { "apps": [row["app_id"] for row in apps], "sources": [row["trigger_source"] for row in sources], } def get_deployment_stats(): con = get_connection() row = con.execute( f""" SELECT COUNT(*) AS total, SUM(CASE WHEN LOWER(COALESCE(status, '')) IN ({','.join('?' for _ in FAILED_STATUSES)}) THEN 1 ELSE 0 END) AS failed, SUM(CASE WHEN LOWER(COALESCE(status, '')) IN ({','.join('?' for _ in RUNNING_STATUSES)}) THEN 1 ELSE 0 END) AS running, SUM(CASE WHEN LOWER(COALESCE(status, '')) IN ({','.join('?' for _ in SUCCESS_STATUSES)}) THEN 1 ELSE 0 END) AS success FROM deployments """, (*FAILED_STATUSES, *RUNNING_STATUSES, *SUCCESS_STATUSES), ).fetchone() con.close() stats = dict(row) if row else {"total": 0, "failed": 0, "running": 0, "success": 0} total = stats.get("total") or 0 success = stats.get("success") or 0 stats["success_rate"] = round((success / total) * 100, 1) if total else 0 return stats def get_app_deployments(app_id: str, limit: int = 10): con = get_connection() rows = con.execute( """ SELECT id, app_id, kind, status, started_at, finished_at, returncode, trigger_source, triggered_by_username, triggered_by_display_name FROM deployments WHERE app_id = ? ORDER BY id DESC LIMIT ? """, (app_id, limit), ).fetchall() con.close() return [dict(row) for row in rows] def get_deployment(deployment_id: int): con = get_connection() row = con.execute( """ SELECT id, app_id, kind, status, started_at, finished_at, returncode, stdout, stderr, trigger_source, triggered_by_username, triggered_by_display_name, commit_author, pusher FROM deployments WHERE id = ? """, (deployment_id,), ).fetchone() con.close() return dict(row) if row else None