Refactor code structure for improved readability and maintainability
This commit is contained in:
parent
bed4fcde1f
commit
a655e68b71
59
app/app.py
59
app/app.py
|
@ -221,6 +221,45 @@ def register_routes(app):
|
|||
user_language=user_language,
|
||||
)
|
||||
|
||||
@app.route("/script/<int:script_id>/loading")
|
||||
@login_required
|
||||
def script_loading_page(script_id):
|
||||
"""
|
||||
Página de loading intermedia que se muestra mientras el script se inicia
|
||||
"""
|
||||
script = Script.query.get_or_404(script_id)
|
||||
|
||||
# Check if user can access this script
|
||||
if not can_access_script(current_user.user_level, script.required_level):
|
||||
flash("Insufficient permissions to access this script", "error")
|
||||
return redirect(url_for("dashboard"))
|
||||
|
||||
# Get active project for this script's group
|
||||
active_project_id = session.get("active_project_id")
|
||||
project = None
|
||||
|
||||
if active_project_id:
|
||||
project = UserProject.query.filter_by(
|
||||
id=active_project_id,
|
||||
user_id=current_user.id,
|
||||
group_id=script.group_id,
|
||||
).first()
|
||||
|
||||
if not project:
|
||||
flash("No active project found for this script group", "error")
|
||||
return redirect(url_for("script_group_view", group_id=script.group_id))
|
||||
|
||||
# Generate proxy URL
|
||||
proxy_url = f"/project/{project.id}/script/{script.id}/user/{current_user.id}/"
|
||||
|
||||
return render_template(
|
||||
"script_loading.html",
|
||||
script=script,
|
||||
script_group=script.script_group,
|
||||
project=project,
|
||||
proxy_url=proxy_url,
|
||||
)
|
||||
|
||||
# Administration routes
|
||||
@app.route("/admin")
|
||||
@login_required
|
||||
|
@ -568,7 +607,10 @@ def register_routes(app):
|
|||
)
|
||||
|
||||
print(f"[API_EXEC] Script executor result: {result}")
|
||||
return jsonify(result)
|
||||
print(f"[API_EXEC] About to return JSON response...")
|
||||
response = jsonify(result)
|
||||
print(f"[API_EXEC] JSON response created successfully")
|
||||
return response
|
||||
|
||||
@app.route("/api/scripts/<int:script_id>/stop", methods=["POST"])
|
||||
@login_required
|
||||
|
@ -1105,7 +1147,20 @@ def create_app(config_name="default"):
|
|||
app = Flask(__name__)
|
||||
|
||||
# Load configuration
|
||||
app.config.from_object(config[config_name])
|
||||
config_obj = config[config_name]
|
||||
app.config.from_object(config_obj)
|
||||
|
||||
# Apply security configuration if present
|
||||
if hasattr(config_obj, 'SECURITY_CONFIG'):
|
||||
security_config = config_obj.SECURITY_CONFIG
|
||||
|
||||
# Apply session cookie settings
|
||||
if 'session_cookie_secure' in security_config:
|
||||
app.config['SESSION_COOKIE_SECURE'] = security_config['session_cookie_secure']
|
||||
if 'session_cookie_httponly' in security_config:
|
||||
app.config['SESSION_COOKIE_HTTPONLY'] = security_config['session_cookie_httponly']
|
||||
if 'session_cookie_samesite' in security_config:
|
||||
app.config['SESSION_COOKIE_SAMESITE'] = security_config['session_cookie_samesite']
|
||||
|
||||
# Add custom Jinja2 filters
|
||||
@app.template_filter("fromjson")
|
||||
|
|
|
@ -127,6 +127,14 @@ class DevelopmentConfig(Config):
|
|||
'echo': True, # Log SQL queries in development
|
||||
}
|
||||
|
||||
# Development security config - allow HTTP cookies
|
||||
SECURITY_CONFIG = {
|
||||
**Config.SECURITY_CONFIG,
|
||||
"session_cookie_secure": False, # Allow HTTP cookies in development
|
||||
"session_cookie_httponly": True,
|
||||
"session_cookie_samesite": "Lax",
|
||||
}
|
||||
|
||||
|
||||
class ProductionConfig(Config):
|
||||
"""Production configuration."""
|
||||
|
|
|
@ -79,9 +79,12 @@ class DataManager:
|
|||
print(f"Error saving config file {config_file}: {e}")
|
||||
raise
|
||||
|
||||
def list_user_projects(self, user_id: int, group_id: int) -> List[Dict]:
|
||||
"""List all projects for user in specific group."""
|
||||
projects = UserProject.query.filter_by(user_id=user_id, group_id=group_id).all()
|
||||
def list_user_projects(self, user_id: int, group_id: int = None) -> List[Dict]:
|
||||
"""List all projects for user in specific group or all groups if group_id is None."""
|
||||
if group_id is None:
|
||||
projects = UserProject.query.filter_by(user_id=user_id).all()
|
||||
else:
|
||||
projects = UserProject.query.filter_by(user_id=user_id, group_id=group_id).all()
|
||||
|
||||
result = []
|
||||
for project in projects:
|
||||
|
@ -89,7 +92,7 @@ class DataManager:
|
|||
|
||||
# Add file system information
|
||||
project_path = self.get_user_project_path(
|
||||
user_id, group_id, project.project_name
|
||||
user_id, project.group_id, project.project_name
|
||||
)
|
||||
|
||||
if project_path.exists():
|
||||
|
|
|
@ -799,32 +799,11 @@
|
|||
class ScriptGroupManager {
|
||||
async runScript(scriptId, parameters = {}) {
|
||||
try {
|
||||
const response = await fetch(`/api/scripts/${scriptId}/execute`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
parameters: parameters
|
||||
})
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (result.success) {
|
||||
ScriptsManager.showNotification('Script started successfully', 'success');
|
||||
this.updateScriptStatus(scriptId, 'running');
|
||||
|
||||
// If the script has an interface URL, wait for it to be ready before opening
|
||||
if (result.interface_url) {
|
||||
this.waitForScriptAndOpen(result.interface_url, scriptId);
|
||||
}
|
||||
} else {
|
||||
ScriptsManager.showNotification(result.message || 'Failed to start script', 'error');
|
||||
}
|
||||
// Redirect immediately to loading page - it will handle script execution
|
||||
window.location.href = `/script/${scriptId}/loading`;
|
||||
} catch (error) {
|
||||
console.error('Error running script:', error);
|
||||
ScriptsManager.showNotification('Error running script', 'error');
|
||||
console.error('Error redirecting to loading page:', error);
|
||||
ScriptsManager.showNotification('Error starting script', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,285 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Loading Script - ScriptsManager</title>
|
||||
|
||||
<!-- Bootstrap CSS -->
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||
<!-- Bootstrap Icons -->
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.10.3/font/bootstrap-icons.css" rel="stylesheet">
|
||||
|
||||
<style>
|
||||
.loading-container {
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
}
|
||||
|
||||
.loading-card {
|
||||
background: rgba(255, 255, 255, 0.95);
|
||||
border-radius: 20px;
|
||||
padding: 3rem;
|
||||
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.1);
|
||||
text-align: center;
|
||||
max-width: 500px;
|
||||
width: 90%;
|
||||
}
|
||||
|
||||
.loading-spinner {
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
margin: 0 auto 2rem;
|
||||
}
|
||||
|
||||
.progress-bar-container {
|
||||
background-color: #e9ecef;
|
||||
border-radius: 10px;
|
||||
height: 8px;
|
||||
margin: 2rem 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.progress-bar {
|
||||
background: linear-gradient(90deg, #667eea, #764ba2);
|
||||
height: 100%;
|
||||
border-radius: 10px;
|
||||
transition: width 0.3s ease;
|
||||
animation: pulse 2s ease-in-out infinite;
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0%, 100% { opacity: 1; }
|
||||
50% { opacity: 0.7; }
|
||||
}
|
||||
|
||||
.status-text {
|
||||
color: #6c757d;
|
||||
font-size: 1.1rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.script-info {
|
||||
background-color: #f8f9fa;
|
||||
border-radius: 10px;
|
||||
padding: 1rem;
|
||||
margin-top: 2rem;
|
||||
}
|
||||
|
||||
.script-info h5 {
|
||||
color: #495057;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.script-info p {
|
||||
color: #6c757d;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.error-message {
|
||||
background-color: #f8d7da;
|
||||
color: #721c24;
|
||||
border: 1px solid #f5c6cb;
|
||||
border-radius: 10px;
|
||||
padding: 1rem;
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
.open-anyway-btn {
|
||||
margin-top: 1rem;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="loading-container">
|
||||
<div class="loading-card">
|
||||
<!-- Loading Spinner -->
|
||||
<div class="spinner-border text-primary loading-spinner" role="status">
|
||||
<span class="visually-hidden">Loading...</span>
|
||||
</div>
|
||||
|
||||
<!-- Status Text -->
|
||||
<h3 class="mb-3" id="loading-title">Starting Script...</h3>
|
||||
<p class="status-text" id="status-text">Initializing script environment...</p>
|
||||
|
||||
<!-- Progress Bar -->
|
||||
<div class="progress-bar-container">
|
||||
<div class="progress-bar" id="progress-bar" style="width: 10%;"></div>
|
||||
</div>
|
||||
|
||||
<!-- Script Information -->
|
||||
<div class="script-info">
|
||||
<h5>{{ script.display_name or script.filename }}</h5>
|
||||
<p>{{ script.description or "Running script..." }}</p>
|
||||
<small class="text-muted">
|
||||
<i class="bi bi-folder me-1"></i>{{ script_group.name }} |
|
||||
<i class="bi bi-person me-1"></i>{{ current_user.username }} |
|
||||
<i class="bi bi-briefcase me-1"></i>{{ project.project_name }}
|
||||
</small>
|
||||
</div>
|
||||
|
||||
<!-- Error Container (hidden by default) -->
|
||||
<div id="error-container" class="error-message" style="display: none;">
|
||||
<i class="bi bi-exclamation-triangle me-2"></i>
|
||||
<span id="error-text"></span>
|
||||
<button type="button" class="btn btn-outline-danger btn-sm open-anyway-btn" onclick="openScriptAnyway()">
|
||||
<i class="bi bi-box-arrow-up-right me-1"></i>
|
||||
Open Script Anyway
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Bootstrap JS -->
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
|
||||
|
||||
<script>
|
||||
// Configuration from backend
|
||||
const SCRIPT_CONFIG = {
|
||||
scriptId: {{ script.id }},
|
||||
projectId: {{ project.id }},
|
||||
userId: {{ current_user.id }},
|
||||
proxyUrl: "{{ proxy_url }}",
|
||||
fullProxyUrl: "{{ request.url_root.rstrip('/') }}{{ proxy_url }}",
|
||||
maxAttempts: 30,
|
||||
checkInterval: 2000
|
||||
};
|
||||
|
||||
// Loading states and messages
|
||||
const LOADING_STATES = [
|
||||
{ progress: 10, title: "Starting Script...", message: "Initializing script environment..." },
|
||||
{ progress: 25, title: "Loading Dependencies...", message: "Setting up required libraries..." },
|
||||
{ progress: 40, title: "Configuring Environment...", message: "Preparing script workspace..." },
|
||||
{ progress: 55, title: "Starting Web Server...", message: "Launching script interface..." },
|
||||
{ progress: 70, title: "Checking Connectivity...", message: "Verifying script availability..." },
|
||||
{ progress: 85, title: "Finalizing Setup...", message: "Almost ready..." },
|
||||
{ progress: 95, title: "Script Ready!", message: "Redirecting to script interface..." }
|
||||
];
|
||||
|
||||
let currentStateIndex = 0;
|
||||
let attempts = 0;
|
||||
let checkingStarted = false;
|
||||
|
||||
// DOM elements
|
||||
const progressBar = document.getElementById('progress-bar');
|
||||
const loadingTitle = document.getElementById('loading-title');
|
||||
const statusText = document.getElementById('status-text');
|
||||
const errorContainer = document.getElementById('error-container');
|
||||
const errorText = document.getElementById('error-text');
|
||||
|
||||
// Update loading state
|
||||
function updateLoadingState(stateIndex) {
|
||||
if (stateIndex < LOADING_STATES.length) {
|
||||
const state = LOADING_STATES[stateIndex];
|
||||
progressBar.style.width = state.progress + '%';
|
||||
loadingTitle.textContent = state.title;
|
||||
statusText.textContent = state.message;
|
||||
}
|
||||
}
|
||||
|
||||
// Show error message
|
||||
function showError(message) {
|
||||
errorText.textContent = message;
|
||||
errorContainer.style.display = 'block';
|
||||
loadingTitle.textContent = "Error Starting Script";
|
||||
statusText.textContent = "There was an issue starting the script.";
|
||||
progressBar.style.width = '100%';
|
||||
progressBar.style.backgroundColor = '#dc3545';
|
||||
}
|
||||
|
||||
// Open script anyway (fallback)
|
||||
function openScriptAnyway() {
|
||||
window.location.href = SCRIPT_CONFIG.fullProxyUrl;
|
||||
}
|
||||
|
||||
// Check if script is ready
|
||||
async function checkScriptStatus() {
|
||||
attempts++;
|
||||
|
||||
try {
|
||||
// Update UI state periodically
|
||||
if (!checkingStarted) {
|
||||
checkingStarted = true;
|
||||
// Start advancing through loading states
|
||||
const stateInterval = setInterval(() => {
|
||||
if (currentStateIndex < LOADING_STATES.length - 1) {
|
||||
currentStateIndex++;
|
||||
updateLoadingState(currentStateIndex);
|
||||
} else {
|
||||
clearInterval(stateInterval);
|
||||
}
|
||||
}, 3000);
|
||||
}
|
||||
|
||||
// Check if script is responding
|
||||
const response = await fetch(SCRIPT_CONFIG.fullProxyUrl, {
|
||||
method: 'GET',
|
||||
cache: 'no-cache'
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
// Script is ready!
|
||||
updateLoadingState(LOADING_STATES.length - 1);
|
||||
setTimeout(() => {
|
||||
window.location.href = SCRIPT_CONFIG.fullProxyUrl;
|
||||
}, 1000);
|
||||
return;
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(`Attempt ${attempts}: Script not ready yet`, error.message);
|
||||
}
|
||||
|
||||
// Check if we've exceeded max attempts
|
||||
if (attempts >= SCRIPT_CONFIG.maxAttempts) {
|
||||
showError("Script is taking longer than expected to start. You can try opening it manually.");
|
||||
return;
|
||||
}
|
||||
|
||||
// Try again after interval
|
||||
setTimeout(checkScriptStatus, SCRIPT_CONFIG.checkInterval);
|
||||
}
|
||||
|
||||
// Start checking when page loads
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// Start with first loading state
|
||||
updateLoadingState(0);
|
||||
|
||||
// Start script execution via AJAX first
|
||||
startScriptExecution();
|
||||
|
||||
// Begin checking script status after a short delay
|
||||
setTimeout(checkScriptStatus, 3000);
|
||||
});
|
||||
|
||||
// Function to start script execution
|
||||
async function startScriptExecution() {
|
||||
try {
|
||||
const response = await fetch(`/api/scripts/${SCRIPT_CONFIG.scriptId}/execute`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
parameters: {}
|
||||
})
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (result.success) {
|
||||
console.log('Script execution started successfully:', result);
|
||||
} else {
|
||||
console.error('Failed to start script:', result.message);
|
||||
showError(result.message || 'Failed to start script');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error starting script execution:', error);
|
||||
showError('Error starting script: ' + error.message);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
"created_at": "2025-09-14T08:09:16.779232",
|
||||
"last_modified": "2025-09-14T08:09:16.779241",
|
||||
"project_settings": {},
|
||||
"user_preferences": {}
|
||||
}
|
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
"created_at": "2025-09-14T08:01:20.986814",
|
||||
"last_modified": "2025-09-14T08:01:20.986822",
|
||||
"project_settings": {},
|
||||
"user_preferences": {}
|
||||
}
|
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
"created_at": "2025-09-14T07:57:47.252787",
|
||||
"last_modified": "2025-09-14T07:57:47.252795",
|
||||
"project_settings": {},
|
||||
"user_preferences": {}
|
||||
}
|
|
@ -87,7 +87,6 @@ services:
|
|||
- scriptsmanager_network
|
||||
ports:
|
||||
- "5003:5003"
|
||||
- "5200-5400:5200-5400"
|
||||
- "5678:5678" # Debug port
|
||||
environment:
|
||||
# Database Configuration (same as production for parity)
|
||||
|
@ -115,20 +114,21 @@ services:
|
|||
- SIDEL_LOGO_PATH=/app/app/static/images/SIDEL.png
|
||||
- CORPORATE_BRANDING=true
|
||||
volumes:
|
||||
# Hot reload: mount entire codebase
|
||||
# Hot reload: mount entire codebase excluding workspaces
|
||||
- .:/app
|
||||
- ./data:/app/data
|
||||
- ./backup:/app/backup
|
||||
- ./logs:/app/logs
|
||||
# Workspaces for proxy scripts
|
||||
- ./workspaces:/app/workspaces
|
||||
# Workspaces for proxy scripts (mounted separately to avoid hot-reload)
|
||||
- ./workspaces:/app/workspaces:delegated
|
||||
depends_on:
|
||||
postgres:
|
||||
condition: service_healthy
|
||||
restart: unless-stopped
|
||||
command: >
|
||||
bash -c "source activate scriptsmanager &&
|
||||
echo '=== SIDEL ScriptsManager Development Environment ===' &&
|
||||
echo 'Hot reload enabled - code changes will be reflected automatically' &&
|
||||
echo 'Development mode with stable container (hot reload disabled for stability)' &&
|
||||
echo 'Application will be available at: http://localhost:5003' &&
|
||||
echo 'Debug port available at: 5678' &&
|
||||
python scripts/init_db.py &&
|
||||
|
|
|
@ -30,11 +30,22 @@ def init_database():
|
|||
|
||||
with app.app_context():
|
||||
try:
|
||||
# Drop all tables (for fresh start)
|
||||
print("Dropping existing tables...")
|
||||
db.drop_all()
|
||||
# Check if database is already initialized
|
||||
from sqlalchemy import inspect
|
||||
inspector = inspect(db.engine)
|
||||
existing_tables = inspector.get_table_names()
|
||||
|
||||
# Create all tables
|
||||
if existing_tables:
|
||||
print(f"Database already initialized with {len(existing_tables)} tables.")
|
||||
print("Skipping table creation and data initialization.")
|
||||
|
||||
# Still discover script groups in case new ones were added
|
||||
print("Updating script group discovery...")
|
||||
discover_script_groups()
|
||||
print("Database update completed successfully!")
|
||||
return
|
||||
|
||||
# Only create tables if database is empty
|
||||
print("Creating database tables...")
|
||||
db.create_all()
|
||||
|
||||
|
|
|
@ -25,15 +25,29 @@ def run_app():
|
|||
port = int(os.environ.get('PORT', 5003 if os.environ.get('DEBUG') == 'true' else 5002))
|
||||
debug = os.environ.get('DEBUG', 'false').lower() == 'true'
|
||||
|
||||
# For development: disable auto-reloader to prevent container restarts when workspaces change
|
||||
# This avoids the issue where script execution causes Flask to restart
|
||||
use_reloader = False if debug else False
|
||||
|
||||
# Run with SocketIO support
|
||||
socketio.run(app, host="0.0.0.0", port=port, debug=debug)
|
||||
socketio.run(
|
||||
app,
|
||||
host="0.0.0.0",
|
||||
port=port,
|
||||
debug=debug,
|
||||
use_reloader=use_reloader
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
port = int(os.environ.get('PORT', 5003 if os.environ.get('DEBUG') == 'true' else 5002))
|
||||
print("Starting ScriptsManager...")
|
||||
print(f"🚀 Application will be available at: http://localhost:{port}")
|
||||
print("🔄 HOT-RELOAD ENABLED - Modify files and they will update automatically!")
|
||||
if os.environ.get('DEBUG') == 'true':
|
||||
print("<EFBFBD> DEVELOPMENT MODE - Auto-reloader disabled for container stability")
|
||||
print(" Manual restart required for code changes")
|
||||
else:
|
||||
print("🏭 PRODUCTION MODE")
|
||||
print("Press Ctrl+C to stop the server")
|
||||
print("-" * 50)
|
||||
run_app()
|
||||
|
|
|
@ -6,10 +6,10 @@
|
|||
"CONDA_DEFAULT_ENV": "tsnet",
|
||||
"USER_LEVEL": "admin",
|
||||
"PROJECT_ID": "1",
|
||||
"PROJECT_NAME": "test",
|
||||
"PROJECT_NAME": "Hola",
|
||||
"USER_THEME": "light",
|
||||
"USER_LANGUAGE": "en",
|
||||
"SCRIPT_GROUP_NAME": "Hydraulic Analysis Tools"
|
||||
},
|
||||
"created_at": "2025-09-14T07:35:04.965536"
|
||||
"created_at": "2025-09-14T10:08:46.733111"
|
||||
}
|
|
@ -10,7 +10,7 @@ os.chdir(WORKSPACE_PATH)
|
|||
# Variables disponibles para el script del usuario
|
||||
PROJECT_WORKSPACE = WORKSPACE_PATH
|
||||
PARAMETERS = {}
|
||||
ENVIRONMENT = {'CONDA_DEFAULT_ENV': 'tsnet', 'USER_LEVEL': 'admin', 'PROJECT_ID': '1', 'PROJECT_NAME': 'test', 'USER_THEME': 'light', 'USER_LANGUAGE': 'en', 'SCRIPT_GROUP_NAME': 'Hydraulic Analysis Tools'}
|
||||
ENVIRONMENT = {'CONDA_DEFAULT_ENV': 'tsnet', 'USER_LEVEL': 'admin', 'PROJECT_ID': '1', 'PROJECT_NAME': 'Hola', 'USER_THEME': 'light', 'USER_LANGUAGE': 'en', 'SCRIPT_GROUP_NAME': 'Hydraulic Analysis Tools'}
|
||||
|
||||
# Importar Flask y configurar aplicación
|
||||
from flask import Flask, request, jsonify, render_template, send_file, redirect, url_for
|
||||
|
|
|
@ -0,0 +1,7 @@
|
|||
{
|
||||
"port": 5201,
|
||||
"workspace": "/app/workspaces/user_1/project_1/script_8",
|
||||
"parameters": null,
|
||||
"environment": null,
|
||||
"created_at": "2025-09-14T07:38:22.462360"
|
||||
}
|
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue