"""U-043: read-side controllers for `v_deployment_status` -- no operation_id for these exists anywhere in 03-C/the MVCP JSON (this whole view is a PH5 addition with no 02-V spec to begin with, see development-plan.md's own U-043 note), same "view's own filter/display needs directly justify a new read endpoint" precedent as `c_task_list`/`c_artifact_list` before it. `deployment` has no `project_id` column (01-M 1.15) -- there's exactly one graphlemon self-target, not one per project, so these never take a project_id filter the way task/artifact listing does. """ _STATUSES = ( "staged", "candidate_starting", "health_check_running", "health_check_passed", "health_check_failed", "cutover_in_progress", "live", "monitoring", "anomaly_detected", "rollback_in_progress", "rolled_back", "rollback_failed", "blocked_pending_human", "monitoring_window_clean", "finalized", ) _TERMINAL_STATUSES = {"rolled_back", "rollback_failed", "finalized", "blocked_pending_human"} class DeploymentNotFoundError(Exception): """NOT_FOUND -- c_deployment_get on a nonexistent deployment_id.""" def _deployment_row_to_dict(r): return { "deployment_id": r[0], "git_commit_hash": r[1], "previous_commit_hash": r[2], "status": r[3], "candidate_port": r[4], "candidate_instance_dir": r[5], "started_at": r[6], "cutover_at": r[7], "monitoring_closed_at": r[8], "rollback_at": r[9], "rollback_reason": r[10], "previous_instance_dir": r[11], "is_active": r[3] not in _TERMINAL_STATUSES, } _DEPLOYMENT_COLUMNS = ( "deployment_id, git_commit_hash, previous_commit_hash, status, candidate_port, candidate_instance_dir, " "started_at, cutover_at, monitoring_closed_at, rollback_at, rollback_reason, previous_instance_dir" ) def list_deployments(conn, limit=20): """Newest-first (`rowid DESC` -- `deployment` has no created_at column of its own, `started_at` is nullable until c_deploy_supervisor_stage actually sets it, same ordering convention U-016/U-037 already use elsewhere for "most recent" when a table lacks a dedicated timestamp).""" rows = conn.execute( "SELECT {} FROM deployment ORDER BY rowid DESC LIMIT ?".format(_DEPLOYMENT_COLUMNS), (limit,) ).fetchall() return [_deployment_row_to_dict(r) for r in rows] def get_current_deployment(conn): """The single non-terminal deployment, if any (01-M 1.15's own `ux_deployment_active` constraint guarantees at most one) -- what `v_deployment_status` actually wants front and center: "当前是否有 deployment 在进行中". Returns None when nothing is active (the common case -- most of the time nothing is deploying).""" row = conn.execute( "SELECT {} FROM deployment WHERE status NOT IN ({}) ORDER BY rowid DESC LIMIT 1".format( _DEPLOYMENT_COLUMNS, ", ".join("'{}'".format(s) for s in _TERMINAL_STATUSES) ) ).fetchone() return _deployment_row_to_dict(row) if row else None def get_deployment(conn, deployment_id): """Includes recent `deploy_health_signal` rows (newest first) -- "最近 一次健康信号采样结果" per this Unit's own Done line, but the full recent history (not just the latest) is what actually lets a human tell "one blip" from "a real pattern" at a glance, same reasoning U-026's `v_graph_visualizer` tooltip already applies to per-node detail.""" row = conn.execute( "SELECT {} FROM deployment WHERE deployment_id = ?".format(_DEPLOYMENT_COLUMNS), (deployment_id,) ).fetchone() if row is None: raise DeploymentNotFoundError(deployment_id) result = _deployment_row_to_dict(row) result["health_signals"] = _recent_signals(conn, deployment_id) return result def _recent_signals(conn, deployment_id, limit=50): rows = conn.execute( "SELECT signal_id, sampled_at, signal_type, status, detail FROM deploy_health_signal " "WHERE deployment_id = ? ORDER BY rowid DESC LIMIT ?", (deployment_id, limit), ).fetchall() return [ {"signal_id": r[0], "sampled_at": r[1], "signal_type": r[2], "status": r[3], "detail": r[4]} for r in rows ]