ligbox-ops-platform/deploy/vm112-wizard/deploy-037-construction.py
Ligbox Spec Hub 038fb8f7ce feat(dns): Spec 037 DNS Viewer — Desk, Console e patches Wizard V4
Entrega read-only de apontamentos DNS (Cloudflare, OpenPanel BIND, público)
no Desk e Console /admin/dominio, com spec, scripts de rollback e patches
VM112 para painel lateral no passo DNS do onboarding (deploy wizard pendente).

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-25 16:36:25 +00:00

134 lines
5.6 KiB
Python

#!/usr/bin/env python3
"""Deploy Spec 037 construção — Fase A + stubs D na VM112."""
from pathlib import Path
BASE = Path("/opt/ligbox-spec-hub/repos/ligbox-ops-platform")
WIZARD = Path("/opt/ligbox-wizard")
REPO = BASE / "projects/wizard/backend/app"
FILES = [
(REPO / "services/project_identity.py", WIZARD / "backend/app/services/project_identity.py"),
(REPO / "services/cf_client_accounts.py", WIZARD / "backend/app/services/cf_client_accounts.py"),
(REPO / "routers/project.py", WIZARD / "backend/app/routers/project.py"),
]
for src, dst in FILES:
dst.parent.mkdir(parents=True, exist_ok=True)
dst.write_text(src.read_text(encoding="utf-8"), encoding="utf-8")
print(f"copied {dst.name}")
# main.py — register project router
main = WIZARD / "backend/app/main.py"
text = main.read_text(encoding="utf-8")
if "project" not in text.split("from app.routers import")[1].split("\n")[0]:
text = text.replace(
"from app.routers import admin_accounts, admin_domains, assist, corporate, domain_admin, onboarding, portal_auth, telemetry",
"from app.routers import admin_accounts, admin_domains, assist, corporate, domain_admin, onboarding, portal_auth, project, telemetry",
)
if "project.router" not in text:
text = text.replace(
"app.include_router(onboarding.router, prefix=\"/api\")",
"app.include_router(onboarding.router, prefix=\"/api\")\napp.include_router(project.router, prefix=\"/api\")",
)
main.write_text(text, encoding="utf-8")
print("main.py patched")
# onboarding.py — phase D endpoints + webhook ligbox_project_email
onb = WIZARD / "backend/app/routers/onboarding.py"
ot = onb.read_text(encoding="utf-8")
if "cf_client_accounts" not in ot:
ot = ot.replace(
"from app.services import dns_byo_vault",
"from app.services import dns_byo_vault, cf_client_accounts, project_identity",
)
PHASE_D = '''
class ProvisionClientAccountRequest(BaseModel):
domain: str = Field(..., min_length=3, max_length=253)
display_name: str | None = None
class HandoffManagerRequest(BaseModel):
domain: str
manager_email: str = Field(..., min_length=5)
def _project_email_for_domain(domain: str) -> str | None:
ident = project_identity.get_project_identity(domain.lower().strip())
return ident.get("ligbox_project_email") if ident else None
@router.post("/dns/cloudflare/provision-client-account")
def provision_client_cf_account(body: ProvisionClientAccountRequest, request: Request):
"""Fase D — conta CF dedicada (fallback zona partilhada até Tenant)."""
get_session_from_request(request)
domain = normalize_domain(body.domain)
try:
validate_primary_domain(domain)
except ValueError as e:
raise HTTPException(status_code=400, detail=invalid_domain_http_detail()) from e
try:
ligbox_email = _project_email_for_domain(domain)
result = cf_client_accounts.provision_client_account(
domain,
ligbox_project_email=ligbox_email,
account_name=body.display_name or domain,
)
ops_webhook.emit_event(
"onboard.cf_account.provisioned",
domain=domain,
session_id=get_session_from_request(request),
data={
"ligbox_project_email": ligbox_email,
"zone_id": result.get("zone", {}).get("id"),
"provision_mode": result.get("record", {}).get("provision_mode"),
},
)
return result
except CloudflareError as e:
raise HTTPException(502, str(e)) from e
@router.post("/dns/cloudflare/handoff-manager")
def handoff_cf_manager(body: HandoffManagerRequest, request: Request):
"""Fase D — convite gestor domínio na conta CF."""
get_session_from_request(request)
domain = normalize_domain(body.domain)
result = cf_client_accounts.handoff_manager(domain, body.manager_email)
ops_webhook.emit_event(
"onboard.cf_handoff.requested",
domain=domain,
session_id=get_session_from_request(request),
data=result,
)
return result
'''
if "/dns/cloudflare/provision-client-account" not in ot:
anchor = "@router.get(\"/session/password-status\")"
ot = ot.replace(anchor, PHASE_D + anchor)
# apply webhook
if "onboard.dns.applied" not in ot:
ot = ot.replace(
' verification = dns_verify.verify_mail_dns(domain, settings.mail_public_ip)\n return {\n "domain": domain,',
' verification = dns_verify.verify_mail_dns(domain, settings.mail_public_ip)\n ops_webhook.emit_event(\n "onboard.dns.applied",\n domain=domain,\n session_id=sid,\n data={\n "dns_mode": dns_mode,\n "ligbox_project_email": _project_email_for_domain(domain),\n "ligbox_account_id": ligbox_acct,\n "zone_id": zone_id,\n "verification_ready": verification.get("ready"),\n },\n )\n return {\n "domain": domain,',
)
# account create webhook extension
if "onboard.account.created" not in ot:
ot = ot.replace(
' activity_log.ok("Processo de onboarding finalizado", source="portal")',
' ops_webhook.emit_event(\n "onboard.account.created",\n domain=domain,\n session_id=sid,\n data={\n "email": email,\n "dns_mode": dns_mode,\n "ligbox_project_email": _project_email_for_domain(domain),\n "verified": verified,\n },\n )\n activity_log.ok("Processo de onboarding finalizado", source="portal")',
)
onb.write_text(ot, encoding="utf-8")
print("onboarding.py patched")
import ast
ast.parse(main.read_text())
ast.parse(onb.read_text())
print("syntax OK")