Skip to content
This repository was archived by the owner on Jun 19, 2026. It is now read-only.
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 1 addition & 3 deletions agent/db/connection.py
Original file line number Diff line number Diff line change
Expand Up @@ -93,9 +93,7 @@ async def get_pool() -> asyncpg.Pool:
except Exception:
import logging

logging.getLogger(__name__).warning(
"Could not create users table — DB may not be ready yet"
)
logging.getLogger(__name__).warning("Could not create users table — DB may not be ready yet")
return _pool


Expand Down
35 changes: 27 additions & 8 deletions agent/db/zp_store.py
Original file line number Diff line number Diff line change
Expand Up @@ -167,14 +167,33 @@ async def add_card(session_id: str, card_id: str, video_id: str, title: str = ""
return {"card_id": card_id, "video_id": video_id, "title": title, "status": "analyzing"}


_ALLOWED_CARD_COLUMNS = frozenset({
"status", "score", "domain", "reason", "reason_code",
"score_breakdown", "papers_found", "competitors_found",
"saturation", "novelty_boost", "video_summary", "insights",
"mvp_proposal", "build_step", "analysis_step", "repo_url",
"live_url", "thread_id", "title", "video_id",
"build_events", "build_phase", "build_node",
})
_ALLOWED_CARD_COLUMNS = frozenset(
{
"status",
"score",
"domain",
"reason",
"reason_code",
"score_breakdown",
"papers_found",
"competitors_found",
"saturation",
"novelty_boost",
"video_summary",
"insights",
"mvp_proposal",
"build_step",
"analysis_step",
"repo_url",
"live_url",
"thread_id",
"title",
"video_id",
"build_events",
"build_phase",
"build_node",
}
)

_JSONB_COLUMNS = frozenset({"insights", "mvp_proposal", "score_breakdown", "build_events"})

Expand Down
50 changes: 31 additions & 19 deletions agent/nodes/deployer.py
Original file line number Diff line number Diff line change
Expand Up @@ -420,7 +420,9 @@ async def _track_do_phase(phase_name: str) -> None:
if v_verdict == "verified":
logger.info(
"[DEPLOYER] Live URL verified (score=%d, verdict=%s): %s",
v_score, v_verdict, live_url,
v_score,
v_verdict,
live_url,
)
await _emit_step_complete(
"verified",
Expand All @@ -433,7 +435,9 @@ async def _track_do_phase(phase_name: str) -> None:
elif v_verdict == "partial":
logger.warning(
"[DEPLOYER] Live URL partially verified (score=%d, verdict=%s): %s",
v_score, v_verdict, live_url,
v_score,
v_verdict,
live_url,
)
await _emit_step_complete(
"verified",
Expand All @@ -447,7 +451,9 @@ async def _track_do_phase(phase_name: str) -> None:
# verdict == "failed" (score < 50) — attempt repair
logger.warning(
"[DEPLOYER] Live URL verification FAILED (score=%d, verdict=%s): %s — attempting repair",
v_score, v_verdict, live_url,
v_score,
v_verdict,
live_url,
)
await _emit_step_error(
"verified",
Expand All @@ -473,16 +479,10 @@ async def _track_do_phase(phase_name: str) -> None:
diag_lines.append(f"Missing: interactive elements (only {cs.get('interactive_count', 0)} found, need >=3)")
if cs.get("broken_images"):
diag_lines.append(f"Broken images: {cs['broken_images'][:3]}")
failed_eps = [
k for k, v in url_verification.get("endpoint_results", {}).items()
if not v.get("ok")
]
failed_eps = [k for k, v in url_verification.get("endpoint_results", {}).items() if not v.get("ok")]
if failed_eps:
diag_lines.append(f"Failed API endpoints: {', '.join(failed_eps[:5])}")
failed_assets = [
k for k, v in url_verification.get("asset_checks", {}).items()
if not v.get("ok")
]
failed_assets = [k for k, v in url_verification.get("asset_checks", {}).items() if not v.get("ok")]
if failed_assets:
diag_lines.append(f"Failed assets: {', '.join(failed_assets[:5])}")

Expand Down Expand Up @@ -516,7 +516,8 @@ async def _track_do_phase(phase_name: str) -> None:
v_verdict = url_verification.get("verification_verdict", "failed")
logger.info(
"[DEPLOYER] Post-repair verification: score=%d, verdict=%s",
v_score, v_verdict,
v_score,
v_verdict,
)

homepage = live_url or github_repo_url
Expand Down Expand Up @@ -1895,8 +1896,10 @@ def _verify_live_url(live_url: str) -> dict:
content_signals["has_title"] = bool(re.search(r"<title>[^<]{3,}</title>", root_html, re.IGNORECASE))
content_signals["has_components"] = root_html.count("<div") >= 3 or root_html.count("<section") >= 2
content_signals["has_styles"] = (
"tailwind" in html_lower or "globals.css" in html_lower or "<style" in html_lower
or bool(re.search(r'<link[^>]+\.css', root_html, re.IGNORECASE))
"tailwind" in html_lower
or "globals.css" in html_lower
or "<style" in html_lower
or bool(re.search(r"<link[^>]+\.css", root_html, re.IGNORECASE))
)

# Score: has title: +5
Expand All @@ -1916,7 +1919,7 @@ def _verify_live_url(live_url: str) -> dict:
# SPA framework detection
content_signals["has_spa_framework"] = any(
marker in root_html
for marker in ("__NEXT_DATA__", "__NUXT__", "window.__remixContext", "id=\"__next\"", "id=\"app\"")
for marker in ("__NEXT_DATA__", "__NUXT__", "window.__remixContext", 'id="__next"', 'id="app"')
) or bool(re.search(r'<script[^>]+src="[^"]*\.(bundle|chunk|app)\.[^"]*\.js"', root_html, re.IGNORECASE))

# Semantic HTML check: h1, nav/navigation, main/article
Expand Down Expand Up @@ -1957,7 +1960,11 @@ def _verify_live_url(live_url: str) -> dict:
score += 10
logger.info(
"[DEPLOYER][VERIFY] Interactive elements: %d (buttons=%d, forms=%d, links=%d, inputs=%d) → +10",
interactive_total, button_count, form_count, link_count, input_count,
interactive_total,
button_count,
form_count,
link_count,
input_count,
)
else:
logger.warning("[DEPLOYER][VERIFY] Interactive elements insufficient (%d found)", interactive_total)
Expand All @@ -1973,7 +1980,9 @@ def _verify_live_url(live_url: str) -> dict:
# Score: meta tags present: +5
if content_signals["has_meta_tags"]:
score += 5
logger.info("[DEPLOYER][VERIFY] Meta tags present (og:title=%s, description=%s) → +5", has_og_title, has_description)
logger.info(
"[DEPLOYER][VERIFY] Meta tags present (og:title=%s, description=%s) → +5", has_og_title, has_description
)
else:
logger.warning("[DEPLOYER][VERIFY] Meta tags MISSING")

Expand Down Expand Up @@ -2007,7 +2016,9 @@ def _verify_live_url(live_url: str) -> dict:
all_endpoints_ok = False
logger.warning(
"[DEPLOYER][VERIFY] Endpoint probe FAILED: %s %s → status=%s",
ep_method, ep_path, probe["status"],
ep_method,
ep_path,
probe["status"],
)
if not probe.get("valid_response"):
all_responses_valid = False
Expand Down Expand Up @@ -2092,7 +2103,8 @@ def _verify_live_url(live_url: str) -> dict:

logger.info(
"[DEPLOYER][VERIFY] Final score: %d/100 → verdict=%s",
score, result["verification_verdict"],
score,
result["verification_verdict"],
)

return result
Expand Down
14 changes: 9 additions & 5 deletions agent/nodes/design_tokens.py
Original file line number Diff line number Diff line change
Expand Up @@ -106,11 +106,15 @@ def to_css_variables(domain: str = "saas", design_system: dict | None = None) ->
if design_system:
palette = _get_palette(design_system)
else:
palette = {**DOMAIN_PRESETS.get(domain, DOMAIN_PRESETS["tech"]),
"surface": None, "surface_alt": None, "on_primary": None,
"semantic_success": "oklch(55% 0.18 142)",
"semantic_warning": "oklch(75% 0.18 75)",
"semantic_error": "oklch(55% 0.22 25)"}
palette = {
**DOMAIN_PRESETS.get(domain, DOMAIN_PRESETS["tech"]),
"surface": None,
"surface_alt": None,
"on_primary": None,
"semantic_success": "oklch(55% 0.18 142)",
"semantic_warning": "oklch(75% 0.18 75)",
"semantic_error": "oklch(55% 0.22 25)",
}

primary = palette["primary"]
accent = palette["accent"]
Expand Down
1 change: 1 addition & 0 deletions agent/nodes/motion_tokens.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ def generate_motion_tokens(design_system: dict) -> str:
llm_easing = llm_motion.get("easing", "")
if llm_easing and "cubic-bezier" in llm_easing:
import re as _re

m = _re.search(r"cubic-bezier\(([^)]+)\)", llm_easing)
if m:
intensity = {**intensity, "ease": f"[{m.group(1)}]"}
Expand Down
25 changes: 13 additions & 12 deletions agent/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,12 +28,11 @@
import uvicorn
from dotenv import load_dotenv
from fastapi import Depends, FastAPI, Header, HTTPException, Request

from .auth import rate_limit_check, verify_api_key
from fastapi.middleware.cors import CORSMiddleware
from pydantic import BaseModel
from starlette.responses import JSONResponse, StreamingResponse

from .auth import rate_limit_check, verify_api_key
from .cost import estimate_pipeline_cost
from .db.store import ResultStore
from .llm import get_runtime_model_config
Expand Down Expand Up @@ -2078,16 +2077,18 @@ async def dashboard_list_apps():
spec = app.get("spec", {})
name = spec.get("name", "")
active = app.get("active_deployment", {})
result.append({
"id": app.get("id", ""),
"name": name,
"live_url": app.get("live_url", ""),
"region": app.get("region", {}).get("slug", ""),
"phase": active.get("phase", "UNKNOWN"),
"created_at": app.get("created_at", ""),
"updated_at": app.get("updated_at", ""),
"protected": name in _PROTECTED_APP_NAMES,
})
result.append(
{
"id": app.get("id", ""),
"name": name,
"live_url": app.get("live_url", ""),
"region": app.get("region", {}).get("slug", ""),
"phase": active.get("phase", "UNKNOWN"),
"created_at": app.get("created_at", ""),
"updated_at": app.get("updated_at", ""),
"protected": name in _PROTECTED_APP_NAMES,
}
)
return result


Expand Down
20 changes: 17 additions & 3 deletions agent/tools/digitalocean.py
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,9 @@ async def cleanup_expired_apps() -> dict:
continue
logger.info("[TTL] Deleting expired app %s (age=%.1fh, ttl=%dh)", name, age_hours, ttl_hours)
result = await delete_app(app_id)
deleted.append({"name": name, "app_id": app_id, "age_hours": round(age_hours, 1), "result": result.get("status")})
deleted.append(
{"name": name, "app_id": app_id, "age_hours": round(age_hours, 1), "result": result.get("status")}
)

return {"deleted": deleted, "skipped": skipped, "ttl_hours": ttl_hours}

Expand Down Expand Up @@ -321,9 +323,21 @@ def build_app_spec(
envs.append({"key": "POSTGRES_URL", "value": generated_db_url, "scope": "RUN_TIME", "type": "SECRET"})
generated_inference_key = os.getenv("GENERATED_APP_INFERENCE_KEY", "")
if generated_inference_key:
envs.append({"key": "GRADIENT_MODEL_ACCESS_KEY", "value": generated_inference_key, "scope": "RUN_TIME", "type": "SECRET"})
envs.append(
{"key": "DIGITALOCEAN_INFERENCE_KEY", "value": generated_inference_key, "scope": "RUN_TIME", "type": "SECRET"}
{
"key": "GRADIENT_MODEL_ACCESS_KEY",
"value": generated_inference_key,
"scope": "RUN_TIME",
"type": "SECRET",
}
)
envs.append(
{
"key": "DIGITALOCEAN_INFERENCE_KEY",
"value": generated_inference_key,
"scope": "RUN_TIME",
"type": "SECRET",
}
)
envs.append({"key": "DO_INFERENCE_MODEL", "value": "anthropic-claude-4.6-sonnet", "scope": "RUN_TIME"})

Expand Down
Loading
Loading