feat: Add session management and global Claude terminal
Phase 1 - Accurate study status detection: - Add is_optimization_running() to check for active processes - Add get_accurate_study_status() with proper status logic - Status now: not_started, running, paused, completed - Add "paused" status styling (orange) to Home page Phase 2 - Global Claude terminal: - Create ClaudeTerminalContext for app-level state - Create GlobalClaudeTerminal floating component - Terminal persists across page navigation - Shows green indicator when connected - Remove inline terminal from Dashboard 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -34,6 +34,63 @@ def get_results_dir(study_dir: Path) -> Path:
|
||||
return results_dir
|
||||
|
||||
|
||||
def is_optimization_running(study_id: str) -> bool:
|
||||
"""Check if an optimization process is currently running for a study.
|
||||
|
||||
Looks for Python processes running run_optimization.py with the study_id in the command line.
|
||||
"""
|
||||
study_dir = STUDIES_DIR / study_id
|
||||
|
||||
for proc in psutil.process_iter(['pid', 'name', 'cmdline', 'cwd']):
|
||||
try:
|
||||
cmdline = proc.info.get('cmdline') or []
|
||||
cmdline_str = ' '.join(cmdline) if cmdline else ''
|
||||
|
||||
# Check if this is a Python process running run_optimization.py for this study
|
||||
if 'python' in cmdline_str.lower() and 'run_optimization' in cmdline_str:
|
||||
if study_id in cmdline_str or str(study_dir) in cmdline_str:
|
||||
return True
|
||||
except (psutil.NoSuchProcess, psutil.AccessDenied):
|
||||
continue
|
||||
|
||||
return False
|
||||
|
||||
|
||||
def get_accurate_study_status(study_id: str, trial_count: int, total_trials: int, has_db: bool) -> str:
|
||||
"""Determine accurate study status based on multiple factors.
|
||||
|
||||
Status can be:
|
||||
- not_started: No database or 0 trials
|
||||
- running: Active process found
|
||||
- paused: Has trials but no active process and not completed
|
||||
- completed: Reached trial target
|
||||
- failed: Has error indicators (future enhancement)
|
||||
|
||||
Args:
|
||||
study_id: The study identifier
|
||||
trial_count: Number of completed trials
|
||||
total_trials: Target number of trials from config
|
||||
has_db: Whether the study database exists
|
||||
|
||||
Returns:
|
||||
Status string: "not_started", "running", "paused", or "completed"
|
||||
"""
|
||||
# No database or no trials = not started
|
||||
if not has_db or trial_count == 0:
|
||||
return "not_started"
|
||||
|
||||
# Check if we've reached the target
|
||||
if trial_count >= total_trials:
|
||||
return "completed"
|
||||
|
||||
# Check if process is actively running
|
||||
if is_optimization_running(study_id):
|
||||
return "running"
|
||||
|
||||
# Has trials but not running and not complete = paused
|
||||
return "paused"
|
||||
|
||||
|
||||
@router.get("/studies")
|
||||
async def list_studies():
|
||||
"""List all available optimization studies"""
|
||||
@@ -66,12 +123,13 @@ async def list_studies():
|
||||
study_db = results_dir / "study.db"
|
||||
history_file = results_dir / "optimization_history_incremental.json"
|
||||
|
||||
status = "not_started"
|
||||
trial_count = 0
|
||||
best_value = None
|
||||
has_db = False
|
||||
|
||||
# Protocol 10: Read from Optuna SQLite database
|
||||
if study_db.exists():
|
||||
has_db = True
|
||||
try:
|
||||
# Use timeout to avoid blocking on locked databases
|
||||
conn = sqlite3.connect(str(study_db), timeout=2.0)
|
||||
@@ -97,19 +155,12 @@ async def list_studies():
|
||||
|
||||
conn.close()
|
||||
|
||||
# Determine status
|
||||
total_trials = config.get('optimization_settings', {}).get('n_trials', 50)
|
||||
if trial_count >= total_trials:
|
||||
status = "completed"
|
||||
else:
|
||||
status = "running" # Simplified - would need process check
|
||||
|
||||
except Exception as e:
|
||||
print(f"Warning: Failed to read Optuna database for {study_dir.name}: {e}")
|
||||
status = "error"
|
||||
|
||||
# Legacy: Read from JSON history
|
||||
elif history_file.exists():
|
||||
has_db = True
|
||||
with open(history_file) as f:
|
||||
history = json.load(f)
|
||||
trial_count = len(history)
|
||||
@@ -118,19 +169,15 @@ async def list_studies():
|
||||
best_trial = min(history, key=lambda x: x['objective'])
|
||||
best_value = best_trial['objective']
|
||||
|
||||
# Determine status
|
||||
total_trials = config.get('trials', {}).get('n_trials', 50)
|
||||
if trial_count >= total_trials:
|
||||
status = "completed"
|
||||
else:
|
||||
status = "running" # Simplified - would need process check
|
||||
|
||||
# Get total trials from config (supports both formats)
|
||||
total_trials = (
|
||||
config.get('optimization_settings', {}).get('n_trials') or
|
||||
config.get('trials', {}).get('n_trials', 50)
|
||||
)
|
||||
|
||||
# Get accurate status using process detection
|
||||
status = get_accurate_study_status(study_dir.name, trial_count, total_trials, has_db)
|
||||
|
||||
# Get creation date from directory or config modification time
|
||||
created_at = None
|
||||
try:
|
||||
@@ -240,7 +287,7 @@ async def get_study_status(study_id: str):
|
||||
conn.close()
|
||||
|
||||
total_trials = config.get('optimization_settings', {}).get('n_trials', 50)
|
||||
status = "completed" if trial_count >= total_trials else "running"
|
||||
status = get_accurate_study_status(study_id, trial_count, total_trials, True)
|
||||
|
||||
return {
|
||||
"study_id": study_id,
|
||||
@@ -639,6 +686,82 @@ async def get_pareto_front(study_id: str):
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=f"Failed to get Pareto front: {str(e)}")
|
||||
|
||||
|
||||
@router.get("/studies/{study_id}/nn-pareto-front")
|
||||
async def get_nn_pareto_front(study_id: str):
|
||||
"""Get NN surrogate Pareto front from nn_pareto_front.json"""
|
||||
try:
|
||||
study_dir = STUDIES_DIR / study_id
|
||||
results_dir = get_results_dir(study_dir)
|
||||
nn_pareto_file = results_dir / "nn_pareto_front.json"
|
||||
|
||||
if not nn_pareto_file.exists():
|
||||
return {"has_nn_results": False, "pareto_front": []}
|
||||
|
||||
with open(nn_pareto_file) as f:
|
||||
nn_pareto = json.load(f)
|
||||
|
||||
# Transform to match Trial interface format
|
||||
transformed = []
|
||||
for trial in nn_pareto:
|
||||
transformed.append({
|
||||
"trial_number": trial.get("trial_number"),
|
||||
"values": [trial.get("mass"), trial.get("frequency")],
|
||||
"params": trial.get("params", {}),
|
||||
"user_attrs": {
|
||||
"source": "NN",
|
||||
"feasible": trial.get("feasible", False),
|
||||
"predicted_stress": trial.get("predicted_stress"),
|
||||
"predicted_displacement": trial.get("predicted_displacement"),
|
||||
"mass": trial.get("mass"),
|
||||
"frequency": trial.get("frequency")
|
||||
},
|
||||
"constraint_satisfied": trial.get("feasible", False),
|
||||
"source": "NN"
|
||||
})
|
||||
|
||||
return {
|
||||
"has_nn_results": True,
|
||||
"pareto_front": transformed,
|
||||
"count": len(transformed)
|
||||
}
|
||||
|
||||
except FileNotFoundError:
|
||||
raise HTTPException(status_code=404, detail=f"Study {study_id} not found")
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=f"Failed to get NN Pareto front: {str(e)}")
|
||||
|
||||
|
||||
@router.get("/studies/{study_id}/nn-state")
|
||||
async def get_nn_optimization_state(study_id: str):
|
||||
"""Get NN optimization state/summary from nn_optimization_state.json"""
|
||||
try:
|
||||
study_dir = STUDIES_DIR / study_id
|
||||
results_dir = get_results_dir(study_dir)
|
||||
nn_state_file = results_dir / "nn_optimization_state.json"
|
||||
|
||||
if not nn_state_file.exists():
|
||||
return {"has_nn_state": False}
|
||||
|
||||
with open(nn_state_file) as f:
|
||||
state = json.load(f)
|
||||
|
||||
return {
|
||||
"has_nn_state": True,
|
||||
"total_fea_count": state.get("total_fea_count", 0),
|
||||
"total_nn_count": state.get("total_nn_count", 0),
|
||||
"pareto_front_size": state.get("pareto_front_size", 0),
|
||||
"best_mass": state.get("best_mass"),
|
||||
"best_frequency": state.get("best_frequency"),
|
||||
"timestamp": state.get("timestamp")
|
||||
}
|
||||
|
||||
except FileNotFoundError:
|
||||
raise HTTPException(status_code=404, detail=f"Study {study_id} not found")
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=f"Failed to get NN state: {str(e)}")
|
||||
|
||||
|
||||
@router.post("/studies")
|
||||
async def create_study(
|
||||
config: str = Form(...),
|
||||
|
||||
Reference in New Issue
Block a user