From 8216f32d9e89f99ee81211d0725cd45e4745bfc6 Mon Sep 17 00:00:00 2001 From: mashraf-222 Date: Mon, 8 Dec 2025 19:52:32 +0200 Subject: [PATCH] Optimization Factory update (#2035) Co-authored-by: Mohamed Ashraf Co-authored-by: Sarthak Agarwal Co-authored-by: Kevin Turcios <106575910+KRRT7@users.noreply.github.com> --- .../scripts/run_optimization.sh | 49 ++- .../optimization-factory/server/app.py | 83 +++- .../optimization-factory/server/static/app.js | 317 ++++++++++--- .../server/static/index.html | 97 +++- .../server/static/style.css | 416 +++++++++++++++++- 5 files changed, 878 insertions(+), 84 deletions(-) diff --git a/experiments/optimization-factory/scripts/run_optimization.sh b/experiments/optimization-factory/scripts/run_optimization.sh index 62ce4e0c4..9b051e3a4 100644 --- a/experiments/optimization-factory/scripts/run_optimization.sh +++ b/experiments/optimization-factory/scripts/run_optimization.sh @@ -252,7 +252,27 @@ FORK_REPO="codeflash-ai/${FORK_REPO_NAME}" echo "Checking if fork already exists: ${FORK_REPO}" if gh repo view "${FORK_REPO}" >/dev/null 2>&1; then echo "✅ Fork already exists: ${FORK_REPO}" - echo "Using existing fork - no need to create a new one" + echo "Updating fork from upstream before cloning..." + + _stage "sync_fork_start" "\"fork_repo\":\"${FORK_REPO}\",\"upstream\":\"${GITHUB_REPO_URL}\"" + echo "Syncing fork ${FORK_REPO} with upstream ${GITHUB_REPO_URL}..." + + # Sync the fork with its upstream repository + # gh repo sync automatically syncs a fork with its upstream if the repo is a fork + # This updates the fork on GitHub with the latest changes from the upstream repository + # We use --force to perform a hard reset, ensuring the fork matches upstream exactly even if it has diverged + if gh repo sync "${FORK_REPO}" --force 2>&1 | tee -a /tmp/sync_debug.log; then + echo "✅ Successfully synced fork ${FORK_REPO} with upstream" + _stage "sync_fork_complete" "\"fork_repo\":\"${FORK_REPO}\"" + else + SYNC_EXIT_CODE=${PIPESTATUS[0]} + echo "⚠️ Fork sync failed with exit code: $SYNC_EXIT_CODE" + echo "Sync debug log:" + cat /tmp/sync_debug.log 2>/dev/null || echo "No sync debug log available" + echo "Note: If the fork doesn't have upstream configured, sync may fail." + echo "This is non-fatal - continuing with clone anyway - fork may still be usable" + _stage "sync_fork_warning" "\"fork_repo\":\"${FORK_REPO}\",\"exit_code\":$SYNC_EXIT_CODE" + fi else echo "Fork does not exist, creating new fork..." @@ -838,6 +858,29 @@ fi python -c "import pytest" 2>/dev/null || pip install pytest || true +# Ensure CodeFlash environment is properly set up before tests +echo "Setting up CodeFlash environment..." +if [ -n "${CODEFLASH_API_KEY:-}" ]; then + export CODEFLASH_API_KEY="${CODEFLASH_API_KEY}" + echo "✅ CodeFlash API key is set and exported" + + # Verify CodeFlash is accessible + if command -v codeflash >/dev/null 2>&1; then + echo "✅ CodeFlash CLI is available" + else + echo "⚠️ CodeFlash CLI not found, installing..." + pip install codeflash[asyncio] || pip install codeflash || true + fi + + # Test CodeFlash connectivity + if command -v codeflash >/dev/null 2>&1; then + echo "Testing CodeFlash connectivity..." + timeout 10 codeflash --version 2>/dev/null && echo "✅ CodeFlash connectivity verified" || echo "⚠️ CodeFlash connectivity test failed" + fi +else + echo "⚠️ CODEFLASH_API_KEY not set - optimization may fail" +fi + # Optional: preflight test run to detect missing modules (with timeout protection) if [ -d "${TESTS_ROOT_VALUE}" ]; then echo "Preflight test run to detect missing modules..." @@ -1426,7 +1469,7 @@ EOF CLAUDE_CMD="" echo "Checking for Claude Code CLI availability..." | tee -a "$TEST_LOG_FILE" "$CLAUDE_LOG" - # Check for global claude command first + # Check for global claude command first (should be pre-installed) echo "DEBUG: Checking for global claude command..." | tee -a "$TEST_LOG_FILE" if command -v claude >/dev/null 2>&1; then CLAUDE_CMD="claude" @@ -2221,7 +2264,7 @@ echo "DEBUG: Exit code persistence completed" | tee -a "$TEST_LOG_FILE" cat > /tmp/claude_setup_round_${ROUND}.md << EOF # Repository Setup Assistant - Round $ROUND -You are continuing to fix repository setup issues. Previous attempts have been made but tests are still failing. +You are fixing repository setup issues. Previous attempts have been made but tests are still failing. ## Current Situation - This is setup round $ROUND of maximum $SETUP_MAX_ROUNDS diff --git a/experiments/optimization-factory/server/app.py b/experiments/optimization-factory/server/app.py index 0264cdec6..2e63895a1 100644 --- a/experiments/optimization-factory/server/app.py +++ b/experiments/optimization-factory/server/app.py @@ -282,7 +282,7 @@ def _read_csv() -> List[Dict[str, str]]: for row in reader: rows.append({ "repo_url": row.get("repo_url", "").strip(), - "auto_terminate": row.get("auto_terminate", "true").strip(), + "auto_terminate": row.get("auto_terminate", "false").strip(), "analysis_config": row.get("analysis_config", "").strip(), }) logger.debug(f"📊 Read {len(rows)} repositories from CSV") @@ -305,7 +305,7 @@ def _write_csv(rows: List[Dict[str, str]]) -> None: for r in rows: writer.writerow({ "repo_url": r["repo_url"].strip(), - "auto_terminate": r.get("auto_terminate", "true").strip(), + "auto_terminate": r.get("auto_terminate", "false").strip(), "analysis_config": r.get("analysis_config", "").strip(), }) logger.info(f"💾 Saved {len(rows)} repositories to CSV") @@ -469,7 +469,7 @@ echo "EC2 bootstrap complete for {JOB_NAME}" > /home/ubuntu/.bootstrap_done "BlockDeviceMappings": [ { "DeviceName": root_device_name, - "Ebs": {"VolumeSize": 100, "VolumeType": "gp3", "DeleteOnTermination": True}, + "Ebs": {"VolumeSize": 500, "VolumeType": "gp3", "DeleteOnTermination": True}, } ], } @@ -923,7 +923,7 @@ def _start_completion_watcher(instance_id: str, public_ip: str, repo_url: str, l canonical_url = _canon_repo_url(repo_url) for row in rows: if _canon_repo_url(row["repo_url"]) == canonical_url: - auto_terminate_enabled = row.get("auto_terminate", "true").lower() == "true" + auto_terminate_enabled = row.get("auto_terminate", "false").lower() == "true" break except Exception as e: logger.warning(f"⚠️ Could not check auto-termination setting for {repo_url}: {e}") @@ -3539,16 +3539,27 @@ def get_enriched_repos() -> Any: """ GET /api/repos/enriched - Get repositories with analysis data enriched + Query Parameters: + - page: Page number (default: 1) + - page_size: Number of items per page (default: all) + - filter_running: Filter for running optimizations only (default: false) + This endpoint: 1. Loads basic repository data from CSV 2. Enriches each repository with analysis data 3. Returns complete repository information including module_root, tests_root from analysis + 4. Supports pagination and filtering - Returns: JSON with enriched repository data + Returns: JSON with enriched repository data (paginated if requested) """ start_time = time.time() - log_request_start("GET /api/repos/enriched") + # Get pagination and filter parameters + page = request.args.get("page", type=int) or 1 + page_size = request.args.get("page_size", type=int) or None + filter_running = request.args.get("filter_running", type=str, default="false").lower() == "true" + + log_request_start("GET /api/repos/enriched", page=page, page_size=page_size, filter_running=filter_running) try: # Load basic repository data @@ -3584,6 +3595,7 @@ def get_enriched_repos() -> Any: # Get last EC2 ID and IP from jobs last_ec2_id = "None" last_ec2_ip = "None" + is_running = False try: jobs_data = json.loads(JOBS_JSON.read_text(encoding="utf-8")) if JOBS_JSON.exists() else {} canonical_url = _canon_repo_url(repo_url) @@ -3591,6 +3603,14 @@ def get_enriched_repos() -> Any: instance_id = jobs_data[canonical_url] last_ec2_id = instance_id + # Check if instance is running + try: + state = _describe_instance_state(instance_id) + if state and state.lower() == "running": + is_running = True + except Exception: + pass + # Get IP address for the instance try: logger.debug(f"Attempting to get public IP for instance {instance_id}") @@ -3640,17 +3660,34 @@ def get_enriched_repos() -> Any: "analysis_config": row["analysis_config"], "last_ec2_id": last_ec2_id, "last_ec2_ip": last_ec2_ip, - "has_analysis": bool(analysis) + "has_analysis": bool(analysis), + "is_running": is_running } + # Apply filter for running optimizations + if filter_running and not is_running: + continue + enriched_repos.append(enriched_repo) + # Apply pagination if page_size is provided + total_count = len(enriched_repos) + if page_size is not None: + start_idx = (page - 1) * page_size + end_idx = start_idx + page_size + enriched_repos = enriched_repos[start_idx:end_idx] + duration_ms = int((time.time() - start_time) * 1000) - log_request_success("GET /api/repos/enriched", duration_ms, repo_count=len(enriched_repos)) + log_request_success("GET /api/repos/enriched", duration_ms, + page=page, page_size=page_size, filter_running=filter_running, + total_count=total_count, returned_count=len(enriched_repos)) return jsonify({ "ok": True, - "repos": enriched_repos + "repos": enriched_repos, + "total_count": total_count, + "page": page, + "page_size": page_size or total_count }) except Exception as e: @@ -4151,6 +4188,34 @@ def get_custom_run_status(instance_id: str) -> Any: duration_ms = int((time.time() - start_time) * 1000) log_request_error("GET /api/custom-run/status/", str(e), duration_ms, instance_id=instance_id) return jsonify({"ok": False, "error": str(e)}), 500 +@app.get("/api/ssh-config") +def get_ssh_config() -> Any: + """ + GET /api/ssh-config - Get SSH configuration for connecting to EC2 instances + + Returns: JSON with SSH key path and connection instructions + """ + start_time = time.time() + log_request_start("GET /api/ssh-config") + + try: + ssh_key_path = os.path.expanduser(SSH_KEY_PATH) + + duration_ms = int((time.time() - start_time) * 1000) + log_request_success("GET /api/ssh-config", duration_ms) + + return jsonify({ + "ok": True, + "ssh_key_path": ssh_key_path, + "user": "ubuntu" + }) + + except Exception as e: + duration_ms = int((time.time() - start_time) * 1000) + log_request_error("GET /api/ssh-config", str(e), duration_ms) + raise + + @app.route("/api/repos//logs", methods=["GET"]) def get_repo_logs(repo_url: str) -> Any: """ diff --git a/experiments/optimization-factory/server/static/app.js b/experiments/optimization-factory/server/static/app.js index bbacad246..9834b0a1b 100644 --- a/experiments/optimization-factory/server/static/app.js +++ b/experiments/optimization-factory/server/static/app.js @@ -107,6 +107,14 @@ function showDashboard() { let allRepos = [] let filteredRepos = [] +// Global variables for pagination +let currentPage = 1 +let pageSize = 25 +let totalCount = 0 +let filterRunning = false +let isRefreshing = false +let targetPage = 1 // Track the target page the user wants to navigate to + /** * Initialize the main application functionality */ @@ -143,6 +151,20 @@ function setupEventListeners() { document.getElementById("add")?.addEventListener("click", addRepo) document.getElementById("update")?.addEventListener("click", updateRepo) + // Pagination controls + document.getElementById("pageInput")?.addEventListener("change", (e) => { + const page = parseInt(e.target.value) || 1 + navigateToPage(page) + }) + document.getElementById("pageSizeSelect")?.addEventListener("change", (e) => { + pageSize = parseInt(e.target.value) || 0 + navigateToPage(1) + }) + document.getElementById("filterRunning")?.addEventListener("change", (e) => { + filterRunning = e.target.value === "running" + navigateToPage(1) + }) + // Tab switching document.getElementById("singleTab")?.addEventListener("click", () => switchTab("single")) document.getElementById("bulkTab")?.addEventListener("click", () => switchTab("bulk")) @@ -191,6 +213,11 @@ function setupEventListeners() { .getElementById("saveAutoTerminationSettings") ?.addEventListener("click", saveAutoTerminationSettings) + // SSH Connect Modal + document.getElementById("closeSshConnectModal")?.addEventListener("click", closeSshConnectModal) + document.getElementById("closeSshConnectModal2")?.addEventListener("click", closeSshConnectModal) + document.getElementById("copySshCommand")?.addEventListener("click", copySshCommand) + // Modal overlay click handlers document.getElementById("analysisEditorModal")?.addEventListener("click", (e) => { if (e.target.id === "analysisEditorModal") { @@ -215,6 +242,12 @@ function setupEventListeners() { closeInstructionsModal() } }) + + document.getElementById("sshConnectModal")?.addEventListener("click", (e) => { + if (e.target.id === "sshConnectModal") { + closeSshConnectModal() + } + }) } /** @@ -325,33 +358,11 @@ function handleSearch(event) { const searchTerm = event.target.value.toLowerCase().trim() console.log("[Search] Searching for:", searchTerm) - if (searchTerm === "") { - // Show all repositories - filteredRepos = [...allRepos] - } else { - // Filter repositories based on search term - filteredRepos = allRepos.filter((repo) => { - const repoUrl = repo.repo_url.toLowerCase() - const moduleRoot = (repo.module_root || "").toLowerCase() - const testsRoot = (repo.tests_root || "").toLowerCase() - const ec2Id = (repo.last_ec2_id || "").toLowerCase() - const ipAddress = (repo.last_ec2_ip || "").toLowerCase() + // Reset to page 1 when searching + navigateToPage(1) - return ( - repoUrl.includes(searchTerm) || - moduleRoot.includes(searchTerm) || - testsRoot.includes(searchTerm) || - ec2Id.includes(searchTerm) || - ipAddress.includes(searchTerm) - ) - }) - } - - // Sort and render filtered results - const sortedRepos = sortRepositories(filteredRepos) - renderRepos(sortedRepos) - - console.log(`[Search] Found ${filteredRepos.length} repositories matching "${searchTerm}"`) + // Search is performed client-side on already loaded data + console.log(`[Search] Searching within ${allRepos.length} loaded repositories`) } /** @@ -373,16 +384,43 @@ function sortRepositories(repos) { } /** - * Refresh repositories list + * Refresh repositories list with pagination and filtering */ async function refreshRepos() { + // Prevent multiple simultaneous requests + if (isRefreshing) { + console.log(`[Repos] Already refreshing, skipping request for page ${targetPage}`) + return + } + + isRefreshing = true + try { - console.log("[Repos] Refreshing repositories...") - const response = await api("/api/repos/enriched") + console.log(`[Repos] Starting refresh for page ${targetPage}`) + + // Build URL with query parameters + const params = new URLSearchParams() + if (pageSize > 0) { + params.append("page", targetPage.toString()) + params.append("page_size", pageSize.toString()) + } + if (filterRunning) { + params.append("filter_running", "true") + } + + const url = `/api/repos/enriched${params.toString() ? "?" + params.toString() : ""}` + console.log(`[Repos] Fetching URL: ${url}`) + const response = await api(url) if (response.ok) { // Store all repositories globally allRepos = response.repos + totalCount = response.total_count || allRepos.length + currentPage = targetPage // Update to show we're now on this page + + console.log( + `[Repos] Response received. Loaded page ${currentPage}, totalCount: ${totalCount}`, + ) // Apply current search filter if any const searchInput = document.getElementById("repoSearch") @@ -411,15 +449,83 @@ async function refreshRepos() { // Sort and render repositories const sortedRepos = sortRepositories(filteredRepos) renderRepos(sortedRepos) + updatePaginationControls() console.log( - `[Repos] Loaded ${response.repos.length} repositories (${filteredRepos.length} after filtering)`, + `[Repos] Loaded ${totalCount} total repositories, showing page ${currentPage} (${sortedRepos.length} on this page)`, ) } else { console.error("[Repos] Failed to load repositories:", response.error) } } catch (error) { console.error("[Repos] Error refreshing repositories:", error) + } finally { + isRefreshing = false + console.log(`[Repos] Finished refresh. currentPage: ${currentPage}, targetPage: ${targetPage}`) + + // Check if the target page changed while we were loading + if (targetPage !== currentPage) { + console.log(`[Repos] Target page changed to ${targetPage}, making another request`) + refreshRepos() + } + } +} + +/** + * Navigate to a specific page + */ +function navigateToPage(page) { + console.log(`[Pagination] navigateToPage called with: ${page}`) + + const maxPage = Math.ceil(totalCount / (pageSize || totalCount)) || 1 + if (page < 1) page = 1 + if (page > maxPage) page = maxPage + + // Update target page + targetPage = page + currentPage = page + + // Update UI immediately + const pageInput = document.getElementById("pageInput") + if (pageInput) { + pageInput.value = currentPage + } + + // If already refreshing, just update the target and let the current refresh complete + // The refreshRepos function will check if we need to load a different page + if (!isRefreshing) { + console.log(`[Pagination] Starting refresh for page ${page}`) + refreshRepos() + } else { + console.log(`[Pagination] Already refreshing, target page is now ${page}`) + } +} + +/** + * Update pagination controls UI + */ +function updatePaginationControls() { + const maxPage = Math.ceil(totalCount / (pageSize || totalCount)) || 1 + + // Update page input + const pageInput = document.getElementById("pageInput") + if (pageInput) { + pageInput.value = currentPage + pageInput.max = maxPage + } + + // Update total pages display + const totalPagesSpan = document.getElementById("totalPages") + if (totalPagesSpan) { + totalPagesSpan.textContent = `of ${maxPage}` + } + + // Update page info + const startIdx = pageSize > 0 ? (currentPage - 1) * pageSize + 1 : 1 + const endIdx = pageSize > 0 ? Math.min(currentPage * pageSize, totalCount) : totalCount + const pageInfo = document.getElementById("pageInfo") + if (pageInfo) { + pageInfo.textContent = `Showing ${startIdx}-${endIdx} of ${totalCount}` } } @@ -427,58 +533,68 @@ async function refreshRepos() { * Render repositories in the table */ function renderRepos(repos) { - console.log("[UI Debug] Rendering repositories:", repos) const tbody = document.querySelector("#repos tbody") if (!tbody) { - console.error("[UI Debug] Table body not found!") return } tbody.innerHTML = "" repos.forEach((repo, index) => { - console.log(`[UI Debug] Processing repo ${index + 1}:`, repo) - const row = document.createElement("tr") // Repository URL const repoCell = document.createElement("td") + repoCell.className = "col-repo" + const autoTermEnabled = repo.auto_terminate === "true" + const autoTermBadge = autoTermEnabled ? "✅ Auto-Term ON" : "❌ Auto-Term OFF" + const autoTermClass = autoTermEnabled ? "enabled" : "disabled" repoCell.innerHTML = ` -
- ${repo.repo_url} -
` // Module Root const moduleCell = document.createElement("td") + moduleCell.className = "col-module" moduleCell.textContent = repo.module_root || "auto" // Tests Root const testsCell = document.createElement("td") + testsCell.className = "col-tests" testsCell.textContent = repo.tests_root || "auto" - // Auto-Terminate Status - const autoTermCell = document.createElement("td") - const autoTermEnabled = repo.auto_terminate === "true" - autoTermCell.innerHTML = ` - - ${autoTermEnabled ? "✅ Enabled" : "❌ Disabled"} - - ` - // Last EC2 ID const ec2Cell = document.createElement("td") + ec2Cell.className = "col-ec2" ec2Cell.textContent = repo.last_ec2_id || "None" // IP Address const ipCell = document.createElement("td") + ipCell.className = "col-ip" ipCell.textContent = repo.last_ec2_ip || "None" // Actions const actionsCell = document.createElement("td") + actionsCell.className = "col-actions" + + // Only show SSH button if there's a running instance + const sshButton = + repo.last_ec2_id && repo.last_ec2_id !== "None" + ? `` + : "" + actionsCell.innerHTML = `
- - + ${sshButton} + +
` row.appendChild(repoCell) row.appendChild(moduleCell) row.appendChild(testsCell) - row.appendChild(autoTermCell) row.appendChild(ec2Cell) row.appendChild(ipCell) row.appendChild(actionsCell) @@ -1528,6 +1644,99 @@ function closeAutoTerminationModal() { window.currentAutoTermRepo = null } +/** + * Open SSH Connect modal + */ +async function openSshConnect(repoUrl, ipAddress) { + const modal = document.getElementById("sshConnectModal") + const sshCommandEl = document.getElementById("sshCommand") + + if (!modal || !sshCommandEl) { + console.error("[SSH] Modal elements not found!") + return + } + + try { + // Fetch SSH configuration from server + const response = await api("/api/ssh-config") + + if (response.ok) { + // Hardcoded SSH key path for WSL + const sshKeyPath = "/mnt/c/Users/Ashraf/Downloads/Deployed_Optimization_Factory_key_pair.pem" + const user = response.user || "ubuntu" + + // Extract org/repo from repository URL + const match = + repoUrl.match(/github\.com\/([^\/]+)\/([^\/]+)/) || + repoUrl.match(/github\.com\/([^\/]+)\/([^\/]+?)(?:\.git)?$/) + const repoName = match ? `${match[1]}/${match[2]}` : "Optimization" + + // Create SSH command with terminal title rename for Ubuntu/Linux/WSL + // Set title locally before SSH connection to ensure it works in WSL + // Add -o StrictHostKeyChecking=no to skip host verification + const sshCommand = `echo -en '\\033]0;${repoName}\\007' && sudo ssh -i "${sshKeyPath}" -o StrictHostKeyChecking=no ${user}@${ipAddress}` + + sshCommandEl.textContent = sshCommand + modal.style.display = "flex" + + console.log(`[SSH] Opening SSH connect modal for ${repoUrl} at ${ipAddress}`) + } else { + console.error("[SSH] Failed to get SSH config:", response.error) + alert("Failed to get SSH configuration") + } + } catch (error) { + console.error("[SSH] Error getting SSH config:", error) + // Fallback: show generic SSH command with title + const match = repoUrl.match(/github\.com\/([^\/]+)\/([^\/]+)/) + const repoName = match ? `${match[1]}/${match[2]}` : "Optimization" + const sshKeyPath = "/mnt/c/Users/Ashraf/Downloads/Deployed_Optimization_Factory_key_pair.pem" + const sshCommand = `echo -en '\\033]0;${repoName}\\007' && sudo ssh -i "${sshKeyPath}" -o StrictHostKeyChecking=no ubuntu@${ipAddress}` + sshCommandEl.textContent = sshCommand + modal.style.display = "flex" + } +} + +/** + * Close SSH Connect modal + */ +function closeSshConnectModal() { + document.getElementById("sshConnectModal").style.display = "none" +} + +/** + * Copy SSH command to clipboard + */ +async function copySshCommand() { + const sshCommandEl = document.getElementById("sshCommand") + const copyBtn = document.getElementById("copySshCommand") + + if (!sshCommandEl || !copyBtn) return + + try { + await navigator.clipboard.writeText(sshCommandEl.textContent) + + const originalText = copyBtn.innerHTML + copyBtn.innerHTML = "✅ Copied!" + copyBtn.disabled = true + + setTimeout(() => { + copyBtn.innerHTML = originalText + copyBtn.disabled = false + }, 2000) + + console.log("[SSH] Command copied to clipboard") + } catch (error) { + console.error("[SSH] Failed to copy:", error) + // Fallback for older browsers + const textArea = document.createElement("textarea") + textArea.value = sshCommandEl.textContent + document.body.appendChild(textArea) + textArea.select() + document.execCommand("copy") + document.body.removeChild(textArea) + } +} + /** * Open the instructions modal with comprehensive usage guide */ diff --git a/experiments/optimization-factory/server/static/index.html b/experiments/optimization-factory/server/static/index.html index d788e689d..531f5b1ce 100644 --- a/experiments/optimization-factory/server/static/index.html +++ b/experiments/optimization-factory/server/static/index.html @@ -119,6 +119,13 @@ class="search-input"> 🔍 +
+ + +
-
- - - - - - - - - - - - - -
RepositoryModule RootTests RootAuto-TerminateLast EC2 IDIP AddressActions
+
+
+ + + + + + + + + + + + +
RepositoryModule RootTests RootLast EC2 IDIP AddressActions
+
+
+
@@ -243,6 +273,43 @@ + + + diff --git a/experiments/optimization-factory/server/static/style.css b/experiments/optimization-factory/server/static/style.css index 4f31917e2..b62e01993 100644 --- a/experiments/optimization-factory/server/static/style.css +++ b/experiments/optimization-factory/server/static/style.css @@ -13,7 +13,8 @@ body { } .container { - max-width: 1200px; + max-width: 95%; + width: 100%; margin: 20px auto; padding: 0; background: #fff; @@ -22,6 +23,24 @@ body { overflow: hidden; } +@media (min-width: 768px) { + .container { + max-width: 90%; + } +} + +@media (min-width: 1024px) { + .container { + max-width: 85%; + } +} + +@media (min-width: 1440px) { + .container { + max-width: 1600px; + } +} + .header { background: linear-gradient(135deg, #4f46e5 0%, #7c3aed 100%); color: white; @@ -45,6 +64,12 @@ body { padding: 32px; } +@media (min-width: 1440px) { + .content { + padding: 48px; + } +} + .actions { display: flex; gap: 12px; @@ -274,22 +299,92 @@ body { pointer-events: none; } +/* Filter container styles */ +.filter-container { + display: flex; + align-items: center; + gap: 8px; +} + +.filter-label { + font-size: 14px; + color: #374151; + font-weight: 500; +} + +.filter-select { + padding: 8px 12px; + border: 1px solid #d1d5db; + border-radius: 8px; + font-size: 14px; + background: white; + cursor: pointer; + transition: all 0.2s ease; +} + +.filter-select:focus { + outline: none; + border-color: #4f46e5; + box-shadow: 0 0 0 3px rgba(79, 70, 229, 0.1); +} + .table-actions { display: flex; gap: 8px; flex-wrap: wrap; } +/* Table Container - Improved scrolling */ +.table-container { + position: relative; + width: 100%; + overflow: hidden; +} + .table-wrapper { background: white; border-radius: 12px; - overflow: hidden; + overflow: auto; border: 1px solid #e2e8f0; + max-height: 70vh; + position: relative; + width: 100%; +} + +@media (min-width: 1440px) { + .table-wrapper { + max-height: 75vh; + } +} + +/* Horizontal scrollbar styling */ +.table-wrapper::-webkit-scrollbar { + height: 12px; +} + +.table-wrapper::-webkit-scrollbar-track { + background: #f1f5f9; + border-radius: 6px; +} + +.table-wrapper::-webkit-scrollbar-thumb { + background: #cbd5e0; + border-radius: 6px; +} + +.table-wrapper::-webkit-scrollbar-thumb:hover { + background: #94a3b8; +} + +/* Vertical scrollbar styling */ +.table-wrapper::-webkit-scrollbar:vertical { + width: 12px; } table { width: 100%; border-collapse: collapse; + min-width: 100%; } th { @@ -299,7 +394,23 @@ th { padding: 16px; text-align: left; font-size: 14px; - border-bottom: 1px solid #e2e8f0; + border-bottom: 2px solid #cbd5e1; + white-space: nowrap; + position: sticky; + top: 0; + z-index: 10; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05); +} + +/* Header alignment classes */ +th.col-terminate, +th.col-actions { + text-align: center; +} + +th.col-ec2, +th.col-ip { + text-align: left; } td { @@ -309,6 +420,186 @@ td { vertical-align: middle; } +/* Column-specific alignment */ +.col-repo, +.col-module, +.col-tests { + text-align: left; +} + +.col-terminate { + text-align: center; +} + +.col-terminate .auto-terminate-status { + display: inline-flex; + align-items: center; + justify-content: center; + min-width: 100px; + padding: 4px 12px; +} + +.col-ec2, +.col-ip { + text-align: left; + font-family: "Monaco", "Menlo", "Ubuntu Mono", monospace; + font-size: 13px; + font-weight: 500; + color: #6b7280; +} + +.col-ec2 { + min-width: 150px; +} + +.col-ip { + min-width: 130px; +} + +.col-actions { + text-align: center; + min-width: 500px; +} + +.col-actions .action-buttons { + display: flex; + gap: 4px; + flex-wrap: wrap; + align-items: center; + justify-content: center; +} + +/* Repository URL cell */ +.col-repo { + min-width: 350px; + max-width: none; +} + +.repo-cell-content { + display: flex; + flex-direction: column; + gap: 8px; +} + +.repo-url-container { + display: flex; + align-items: center; + gap: 12px; + flex-wrap: wrap; +} + +.repo-url-link { + font-weight: 500; + color: #3b82f6; + text-decoration: none; + flex: 1; + min-width: 0; + display: inline-block; + max-width: 100%; + word-break: break-all; +} + +/* Make sure the link wraps properly on smaller screens */ +.repo-url-container { + word-break: break-word; +} + +.repo-url-link:hover { + color: #2563eb; + text-decoration: underline; +} + +.repo-url { + font-weight: 500; + color: #1e293b; + flex: 1; + min-width: 0; +} + +.auto-terminate-badge { + display: inline-flex; + align-items: center; + gap: 4px; + padding: 3px 10px; + border-radius: 6px; + font-size: 11px; + font-weight: 600; + white-space: nowrap; +} + +.auto-terminate-badge.enabled { + background: #dcfce7; + color: #166534; + border: 1px solid #bbf7d0; +} + +.auto-terminate-badge.disabled { + background: #fee2e2; + color: #991b1b; + border: 1px solid #fecaca; +} + +.btn-view-analysis { + display: inline-flex; + align-items: center; + justify-content: center; + gap: 6px; + padding: 6px 12px; + font-size: 12px; + font-weight: 500; + border-radius: 6px; + cursor: pointer; + transition: all 0.2s ease; + background: #6b7280; + color: white; + border: none; +} + +.btn-view-analysis:hover { + background: #4b5563; + transform: translateY(-1px); + box-shadow: 0 2px 8px rgba(107, 114, 128, 0.3); +} + +.btn-view-analysis:active { + transform: translateY(0); +} + +.btn-view-analysis span { + font-size: 14px; +} + +.col-module, +.col-tests { + min-width: 120px; +} + +/* Enhanced cell padding for different column types */ +.col-terminate td, +td.col-terminate { + padding: 12px 16px; +} + +.col-actions td, +td.col-actions { + padding: 12px 8px; +} + +.col-ec2 td, +td.col-ec2, +.col-ip td, +td.col-ip { + padding: 14px 16px; + word-break: break-all; +} + +/* Allow repo URL to wrap properly */ +.repo-url, +.repo-url-link { + word-break: break-all; + overflow-wrap: anywhere; +} + tbody tr:hover { background: #f8fafc; } @@ -672,6 +963,7 @@ tbody tr:hover { .container { margin: 10px; border-radius: 8px; + max-width: 100%; } .content { @@ -1562,6 +1854,91 @@ tbody tr:hover { } } +/* Table Footer - Pagination */ +.table-footer { + display: flex; + justify-content: space-between; + align-items: center; + padding: 16px; + background: #f8fafc; + border: 1px solid #e2e8f0; + border-top: none; + border-radius: 0 0 12px 12px; + flex-wrap: wrap; + gap: 16px; +} + +.pagination-info { + font-size: 14px; + color: #64748b; + font-weight: 500; +} + +.pagination-controls { + display: flex; + align-items: center; + gap: 8px; +} + +.page-input-container { + display: flex; + align-items: center; + gap: 8px; +} + +.page-input-container label { + font-size: 14px; + color: #374151; + font-weight: 500; +} + +.page-input { + width: 60px; + padding: 6px 8px; + border: 1px solid #d1d5db; + border-radius: 6px; + font-size: 14px; + text-align: center; +} + +.page-input:focus { + outline: none; + border-color: #4f46e5; + box-shadow: 0 0 0 3px rgba(79, 70, 229, 0.1); +} + +.page-input-container span { + font-size: 14px; + color: #64748b; +} + +.pagination-size { + display: flex; + align-items: center; + gap: 8px; +} + +.pagination-size label { + font-size: 14px; + color: #374151; + font-weight: 500; +} + +.page-size-select { + padding: 6px 10px; + border: 1px solid #d1d5db; + border-radius: 6px; + font-size: 14px; + background: white; + cursor: pointer; +} + +.page-size-select:focus { + outline: none; + border-color: #4f46e5; + box-shadow: 0 0 0 3px rgba(79, 70, 229, 0.1); +} + /* Responsive Design */ @media (max-width: 768px) { .modal-content { @@ -1598,4 +1975,37 @@ tbody tr:hover { .table-actions { justify-content: center; } + + /* Table footer responsive */ + .table-footer { + flex-direction: column; + align-items: stretch; + } + + .pagination-info { + text-align: center; + } + + .pagination-controls { + justify-content: center; + } + + .pagination-size { + justify-content: center; + } + + .filter-container { + justify-content: center; + } +} + +/* Better spacing for very large screens */ +@media (min-width: 1920px) { + .container { + max-width: 1800px; + } + + .table-wrapper { + max-height: 80vh; + } }