Optimization Factory update (#2035)

Co-authored-by: Mohamed Ashraf <mohamedashrraf222@gmail.com>
Co-authored-by: Sarthak Agarwal <sarthak.saga@gmail.com>
Co-authored-by: Kevin Turcios <106575910+KRRT7@users.noreply.github.com>
This commit is contained in:
mashraf-222 2025-12-08 19:52:32 +02:00 committed by GitHub
parent d4ddff44df
commit 8216f32d9e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 878 additions and 84 deletions

View file

@ -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

View file

@ -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/<instance_id>", 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/<path:repo_url>/logs", methods=["GET"])
def get_repo_logs(repo_url: str) -> Any:
"""

View file

@ -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 = `
<div style="display: flex; align-items: center; gap: 8px;">
<span style="font-weight: 500;">${repo.repo_url}</span>
<button class="btn btn-sm btn-secondary" onclick="viewAnalysis('${repo.repo_url}')" title="View Analysis">
📊
<div class="repo-cell-content">
<div class="repo-url-container">
<a href="${repo.repo_url}" target="_blank" rel="noopener noreferrer" class="repo-url-link" title="${repo.repo_url}">
${repo.repo_url}
</a>
<span class="auto-terminate-badge ${autoTermClass}" title="${autoTermBadge}">${autoTermBadge}</span>
</div>
<button class="btn btn-sm btn-secondary btn-view-analysis" onclick="viewAnalysis('${repo.repo_url}')" title="View Analysis">
<span>📊</span> View Analysis
</button>
</div>
`
// 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 = `
<span class="auto-terminate-status ${autoTermEnabled ? "enabled" : "disabled"}">
${autoTermEnabled ? "✅ Enabled" : "❌ Disabled"}
</span>
`
// 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"
? `<button class="btn btn-info btn-sm" onclick="openSshConnect('${repo.repo_url}', '${repo.last_ec2_ip}')" title="SSH Connect">
🖥 SSH
</button>`
: ""
actionsCell.innerHTML = `
<div class="action-buttons">
<button class="btn btn-primary btn-sm" onclick="runRepo('${repo.repo_url}')" title="Run Optimization">
@ -496,19 +612,19 @@ function renderRepos(repos) {
<button class="btn btn-secondary btn-sm" onclick="configureAutoTermination('${repo.repo_url}')" title="Configure Auto-Termination">
Auto-Term
</button>
<button class="btn btn-danger btn-sm" onclick="deleteRepo('${repo.repo_url}')" title="Delete Repository" style="margin-left: 8px;">
🗑 Delete
</button>
<button class="btn btn-info btn-sm" onclick="openCustomRunModal('${repo.repo_url}')" title="Custom Run" style="margin-left: 8px;">
Custom Run
</button>
${sshButton}
<button class="btn btn-danger btn-sm" onclick="deleteRepo('${repo.repo_url}')" title="Delete Repository">
🗑 Delete
</button>
<button class="btn btn-info btn-sm" onclick="openCustomRunModal('${repo.repo_url}')" title="Custom Run">
Custom Run
</button>
</div>
`
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
*/

View file

@ -119,6 +119,13 @@
class="search-input">
<span class="search-icon">🔍</span>
</div>
<div class="filter-container">
<label for="filterRunning" class="filter-label">Filter:</label>
<select id="filterRunning" class="filter-select">
<option value="all">All Repositories</option>
<option value="running">Running Optimizations</option>
</select>
</div>
<div class="table-actions">
<button id="refresh" class="btn btn-secondary">
<span>🔄</span> Refresh
@ -130,21 +137,44 @@
</div>
</div>
</div>
<div class="table-wrapper">
<table id="repos">
<thead>
<tr>
<th>Repository</th>
<th>Module Root</th>
<th>Tests Root</th>
<th>Auto-Terminate</th>
<th>Last EC2 ID</th>
<th>IP Address</th>
<th>Actions</th>
</tr>
</thead>
<tbody></tbody>
</table>
<div class="table-container">
<div class="table-wrapper">
<table id="repos">
<thead>
<tr>
<th class="col-repo">Repository</th>
<th class="col-module">Module Root</th>
<th class="col-tests">Tests Root</th>
<th class="col-ec2">Last EC2 ID</th>
<th class="col-ip">IP Address</th>
<th class="col-actions">Actions</th>
</tr>
</thead>
<tbody></tbody>
</table>
</div>
</div>
<div class="table-footer">
<div class="pagination-info">
<span id="pageInfo">Showing 0-0 of 0</span>
</div>
<div class="pagination-controls">
<div class="page-input-container">
<label for="pageInput">Page:</label>
<input type="number" id="pageInput" min="1" value="1" class="page-input">
<span id="totalPages">of 1</span>
</div>
</div>
<div class="pagination-size">
<label for="pageSizeSelect">Items per page:</label>
<select id="pageSizeSelect" class="page-size-select">
<option value="10">10</option>
<option value="25" selected>25</option>
<option value="50">50</option>
<option value="100">100</option>
<option value="0">All</option>
</select>
</div>
</div>
</div>
@ -243,6 +273,43 @@
</div>
</div>
</div>
<!-- SSH Connect Modal -->
<div id="sshConnectModal" class="modal-overlay" style="display:none;">
<div class="modal-content">
<div class="modal-header">
<h3>🖥️ SSH Connect to Instance</h3>
<button id="closeSshConnectModal" class="btn btn-secondary btn-sm"></button>
</div>
<div class="modal-body">
<div id="sshConnectContent">
<p style="margin: 0 0 16px 0; color: #64748b; font-size: 14px;">
Copy the SSH command below and run it in your terminal to connect to the instance:
</p>
<div style="background: #1e293b; padding: 16px; border-radius: 8px; position: relative;">
<code id="sshCommand"
style="color: #e2e8f0; font-family: 'Monaco', 'Menlo', monospace; font-size: 13px; word-break: break-all;"></code>
<button id="copySshCommand" class="btn btn-primary btn-sm"
style="position: absolute; top: 8px; right: 8px;">
📋 Copy
</button>
</div>
<div
style="margin-top: 16px; padding: 12px; background: #f8fafc; border-radius: 8px; border-left: 4px solid #3b82f6;">
<p style="margin: 0; font-size: 13px; color: #475569;">
<strong>Tip:</strong> The command will automatically rename your terminal tab to the
repository name.
</p>
</div>
</div>
</div>
<div class="modal-footer">
<button id="closeSshConnectModal2" class="btn btn-secondary">
<span></span> Close
</button>
</div>
</div>
</div>
</div>
</div>

View file

@ -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;
}
}