diff --git a/gui-demo/app.py b/gui-demo/app.py index ee24e3e..fe9c13c 100755 --- a/gui-demo/app.py +++ b/gui-demo/app.py @@ -178,6 +178,12 @@ def discover_scenarios() -> list: # Get deployment scripts deployment_scripts = discover_deployment_scripts(item) + # Check for local scripts + local_dir = item / "deployment" / "local" + has_preprocess = (local_dir / "preprocess.sh").exists() + has_save_model = (local_dir / "save-model.sh").exists() + has_train = (local_dir / "train.sh").exists() + scenarios.append({ "name": item.name, "path": str(item), @@ -188,6 +194,11 @@ def discover_scenarios() -> list: "common_vars": vars_info["common_vars"], "scenario_vars": vars_info["scenario_vars"], "deployment_scripts": deployment_scripts, + "local_scripts": { + "has_preprocess": has_preprocess, + "has_save_model": has_save_model, + "has_train": has_train, + }, }) return scenarios @@ -477,6 +488,128 @@ def generate(): return Response(generate(), mimetype='text/event-stream') +@app.route("/api/run-local-script-stream", methods=["POST"]) +def run_local_script_stream(): + """Run a local deployment script with streaming output.""" + data = request.json + script_name = data.get("script") + + scenario = state["current_scenario"] + if not scenario: + return jsonify({"success": False, "error": "No scenario selected"}) + + script_path = SCENARIOS_DIR / scenario / "deployment" / "local" / script_name + + if not script_path.exists(): + return jsonify({"success": False, "error": f"Script not found: {script_path}"}) + + script_key = script_name.replace(".sh", "").replace("-", "_") + state["script_status"][script_key] = "running" + + def generate(): + cwd = script_path.parent + cmd = f"bash {script_path}" + process = run_command(cmd, cwd=str(cwd), stream=True) + state["running_process"] = process + + try: + for line in iter(process.stdout.readline, ''): + if line: + yield f"data: {json.dumps({'line': line})}\n\n" + + process.wait() + if process.returncode == 0: + state["script_status"][script_key] = "completed" + yield f"data: {json.dumps({'status': 'completed'})}\n\n" + else: + state["script_status"][script_key] = "failed" + yield f"data: {json.dumps({'status': 'failed'})}\n\n" + finally: + state["running_process"] = None + + return Response(generate(), mimetype='text/event-stream') + + +@app.route("/api/pull-containers-stream", methods=["POST"]) +def pull_containers_stream(): + """Run both pull-containers.sh scripts (repo-level and scenario-level) with streaming output.""" + scenario = state["current_scenario"] + if not scenario: + return jsonify({"success": False, "error": "No scenario selected"}) + + repo_pull_script = Path(REPO_ROOT) / "ci" / "pull-containers.sh" + scenario_pull_script = SCENARIOS_DIR / scenario / "ci" / "pull-containers.sh" + + if not repo_pull_script.exists(): + return jsonify({"success": False, "error": f"Repo pull-containers.sh not found: {repo_pull_script}"}) + + if not scenario_pull_script.exists(): + return jsonify({"success": False, "error": f"Scenario pull-containers.sh not found: {scenario_pull_script}"}) + + state["script_status"]["pull_containers"] = "running" + + def generate(): + # First run repo-level pull-containers.sh + yield f"data: {json.dumps({'line': '=== Pulling common containers ===\n'})}\n\n" + cwd = repo_pull_script.parent + cmd = f"bash {repo_pull_script}" + process = run_command(cmd, cwd=str(cwd), stream=True) + state["running_process"] = process + + try: + for line in iter(process.stdout.readline, ''): + if line: + yield f"data: {json.dumps({'line': line})}\n\n" + + process.wait() + if process.returncode != 0: + state["script_status"]["pull_containers"] = "failed" + yield f"data: {json.dumps({'status': 'failed', 'line': 'Failed to pull common containers\n'})}\n\n" + return + finally: + state["running_process"] = None + + # Then run scenario-specific pull-containers.sh + yield f"data: {json.dumps({'line': '\n=== Pulling scenario-specific containers ===\n'})}\n\n" + cwd = scenario_pull_script.parent + cmd = f"bash {scenario_pull_script}" + process = run_command(cmd, cwd=str(cwd), stream=True) + state["running_process"] = process + + try: + for line in iter(process.stdout.readline, ''): + if line: + yield f"data: {json.dumps({'line': line})}\n\n" + + process.wait() + if process.returncode == 0: + state["script_status"]["pull_containers"] = "completed" + yield f"data: {json.dumps({'status': 'completed'})}\n\n" + else: + state["script_status"]["pull_containers"] = "failed" + yield f"data: {json.dumps({'status': 'failed'})}\n\n" + finally: + state["running_process"] = None + + return Response(generate(), mimetype='text/event-stream') + + +@app.route("/api/check-local-script", methods=["POST"]) +def check_local_script(): + """Check if a local script exists.""" + data = request.json + script_name = data.get("script") + + scenario = state["current_scenario"] + if not scenario: + return jsonify({"success": False, "error": "No scenario selected"}) + + script_path = SCENARIOS_DIR / scenario / "deployment" / "local" / script_name + exists = script_path.exists() + + return jsonify({"success": True, "exists": exists}) + + @app.route("/api/deploy", methods=["POST"]) def deploy(): """Run the deploy.sh script with contract number and pipeline config.""" diff --git a/gui-demo/static/css/style.css b/gui-demo/static/css/style.css index a0e0f17..ca54ef0 100644 --- a/gui-demo/static/css/style.css +++ b/gui-demo/static/css/style.css @@ -3,7 +3,7 @@ * A warm, amber-tinted dark theme with soft gradients */ -:root { + :root { /* Warm Dark Palette */ --bg-base: #0d0d0f; --bg-card: #141416; @@ -484,6 +484,44 @@ body { 50% { transform: scale(1.1); } } +/* Pre-deployment Steps */ +.pre-deployment-buttons { + display: flex; + flex-direction: column; + gap: var(--sp-3); +} + +.pre-deployment-buttons .btn { + display: flex; + align-items: center; + gap: var(--sp-2); + padding: var(--sp-3) var(--sp-4); + justify-content: flex-start; + text-align: left; +} + +.pre-deployment-buttons .btn svg { + width: 18px; + height: 18px; + flex-shrink: 0; +} + +.pre-deployment-buttons .btn-hint { + font-size: 0.75rem; + color: var(--text-muted); + font-weight: normal; + margin-left: auto; + font-style: italic; +} + +.pre-deployment-buttons .btn-primary .btn-hint { + color: rgba(13, 13, 15, 0.7); +} + +.pre-deployment-buttons .btn.hidden { + display: none; +} + .step-content { flex: 1; min-width: 0; diff --git a/gui-demo/static/js/app.js b/gui-demo/static/js/app.js index 92751fe..ac39f33 100644 --- a/gui-demo/static/js/app.js +++ b/gui-demo/static/js/app.js @@ -39,6 +39,12 @@ class DepaTrainingApp { }); document.getElementById('btnRefreshScenarios').addEventListener('click', () => this.refreshScenarios()); + // Pre-deployment steps + document.getElementById('btnPullContainers').addEventListener('click', () => this.pullContainers()); + document.getElementById('btnPreprocess').addEventListener('click', () => this.runPreprocess()); + document.getElementById('btnSaveModel').addEventListener('click', () => this.runSaveModel()); + document.getElementById('btnTestTraining').addEventListener('click', () => this.runTestTraining()); + // Deploy document.getElementById('btnDeploy').addEventListener('click', () => this.deploy()); document.getElementById('btnDownload').addEventListener('click', () => this.downloadModel()); @@ -153,6 +159,7 @@ class DepaTrainingApp { this.renderScenarioInfo(data.scenario); this.renderPipeline(data.scenario); this.renderScenarioSettings(data.scenario_vars); + this.updatePreDeploymentButtons(data.scenario); this.toast(`Selected: ${this.formatName(scenarioName)}`, 'success'); } } catch (e) { @@ -290,6 +297,118 @@ class DepaTrainingApp { setTimeout(poll, 5000); } + // ============= Pre-deployment Steps ============= + + updatePreDeploymentButtons(scenario) { + const localScripts = scenario.local_scripts || {}; + const saveModelBtn = document.getElementById('btnSaveModel'); + + if (localScripts.has_save_model) { + saveModelBtn.classList.remove('hidden'); + } else { + saveModelBtn.classList.add('hidden'); + } + } + + async pullContainers() { + if (!this.state.currentScenario) { + return this.toast('Select a scenario first', 'error'); + } + + this.openModal('pull-containers.sh'); + + const terminal = document.getElementById('terminalOutput'); + terminal.textContent = `$ ./ci/pull-containers.sh\n$ ./scenarios/${this.state.currentScenario}/ci/pull-containers.sh\n\n`; + + try { + const res = await fetch('/api/pull-containers-stream', { + method: 'POST', + headers: { 'Content-Type': 'application/json' } + }); + + const reader = res.body.getReader(); + const decoder = new TextDecoder(); + + while (true) { + const { done, value } = await reader.read(); + if (done) break; + + for (const line of decoder.decode(value).split('\n')) { + if (line.startsWith('data: ')) { + try { + const data = JSON.parse(line.slice(6)); + if (data.line) { + terminal.textContent += data.line; + terminal.scrollTop = terminal.scrollHeight; + } + if (data.status) { + this.updateModalStatus(data.status); + } + } catch {} + } + } + } + } catch (e) { + this.updateModalStatus('failed'); + } + } + + async runLocalScript(scriptName, displayName) { + if (!this.state.currentScenario) { + return this.toast('Select a scenario first', 'error'); + } + + this.openModal(displayName || scriptName); + + const terminal = document.getElementById('terminalOutput'); + terminal.textContent = `$ ./${scriptName}\n\n`; + + try { + const res = await fetch('/api/run-local-script-stream', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ script: scriptName }) + }); + + const reader = res.body.getReader(); + const decoder = new TextDecoder(); + + while (true) { + const { done, value } = await reader.read(); + if (done) break; + + for (const line of decoder.decode(value).split('\n')) { + if (line.startsWith('data: ')) { + try { + const data = JSON.parse(line.slice(6)); + if (data.line) { + terminal.textContent += data.line; + terminal.scrollTop = terminal.scrollHeight; + } + if (data.status) { + this.updateModalStatus(data.status); + } + } catch {} + } + } + } + } catch (e) { + this.updateModalStatus('failed'); + } + } + + async runPreprocess() { + await this.runLocalScript('preprocess.sh', 'preprocess.sh'); + } + + async runSaveModel() { + await this.runLocalScript('save-model.sh', 'save-model.sh'); + } + + async runTestTraining() { + await this.runLocalScript('train.sh', 'train.sh'); + } + // ============= Scripts ============= async runScript(scriptName, scriptKey) { diff --git a/gui-demo/templates/index.html b/gui-demo/templates/index.html index ea33ff0..d56ac33 100644 --- a/gui-demo/templates/index.html +++ b/gui-demo/templates/index.html @@ -126,6 +126,60 @@

Scenario

+ +
+
+
+ + + + + + + +
+

Pre-deployment Steps

+
+
+
+ + + + +
+
+
+