|
10 | 10 | import argparse |
11 | 11 | import subprocess |
12 | 12 | import threading |
13 | | -from flask import Flask, jsonify, send_from_directory, abort |
| 13 | +from flask import Flask, jsonify, request, send_from_directory, abort |
14 | 14 | from flask_sock import Sock |
15 | 15 |
|
16 | 16 | # Add server directory to path |
@@ -118,6 +118,121 @@ def health(): |
118 | 118 | return jsonify({'status': 'ok'}) |
119 | 119 |
|
120 | 120 |
|
| 121 | +# --- Project Management --- |
| 122 | + |
| 123 | +_PROJECTS_FILE = os.path.expanduser('~/.humanize/viz-projects.json') |
| 124 | + |
| 125 | + |
| 126 | +def _load_projects(): |
| 127 | + """Load saved project list.""" |
| 128 | + if os.path.isfile(_PROJECTS_FILE): |
| 129 | + try: |
| 130 | + with open(_PROJECTS_FILE, 'r') as f: |
| 131 | + return json.loads(f.read()) |
| 132 | + except (json.JSONDecodeError, OSError): |
| 133 | + pass |
| 134 | + return [] |
| 135 | + |
| 136 | + |
| 137 | +def _save_projects(projects): |
| 138 | + os.makedirs(os.path.dirname(_PROJECTS_FILE), exist_ok=True) |
| 139 | + with open(_PROJECTS_FILE, 'w') as f: |
| 140 | + f.write(json.dumps(projects, indent=2)) |
| 141 | + |
| 142 | + |
| 143 | +def _ensure_current_project(): |
| 144 | + """Make sure the current PROJECT_DIR is in the saved list.""" |
| 145 | + projects = _load_projects() |
| 146 | + if PROJECT_DIR not in projects: |
| 147 | + projects.insert(0, PROJECT_DIR) |
| 148 | + _save_projects(projects) |
| 149 | + |
| 150 | + |
| 151 | +@app.route('/api/projects') |
| 152 | +def api_projects(): |
| 153 | + _ensure_current_project() |
| 154 | + projects = _load_projects() |
| 155 | + result = [] |
| 156 | + for p in projects: |
| 157 | + rlcr_dir = os.path.join(p, '.humanize', 'rlcr') |
| 158 | + session_count = 0 |
| 159 | + if os.path.isdir(rlcr_dir): |
| 160 | + session_count = len([d for d in os.listdir(rlcr_dir) if os.path.isdir(os.path.join(rlcr_dir, d))]) |
| 161 | + result.append({ |
| 162 | + 'path': p, |
| 163 | + 'name': os.path.basename(p), |
| 164 | + 'sessions': session_count, |
| 165 | + 'active': p == PROJECT_DIR, |
| 166 | + }) |
| 167 | + return jsonify(result) |
| 168 | + |
| 169 | + |
| 170 | +@app.route('/api/projects/switch', methods=['POST']) |
| 171 | +def api_switch_project(): |
| 172 | + global PROJECT_DIR, _watcher |
| 173 | + data = json.loads(request.data) if request.data else {} |
| 174 | + new_path = data.get('path', '') |
| 175 | + |
| 176 | + if not new_path or not os.path.isdir(new_path): |
| 177 | + return jsonify({'error': 'Invalid project path'}), 400 |
| 178 | + |
| 179 | + rlcr_dir = os.path.join(new_path, '.humanize', 'rlcr') |
| 180 | + if not os.path.isdir(os.path.join(new_path, '.humanize')): |
| 181 | + return jsonify({'error': 'No .humanize/ directory in this project'}), 400 |
| 182 | + |
| 183 | + # Stop old watcher |
| 184 | + if _watcher: |
| 185 | + _watcher.stop() |
| 186 | + |
| 187 | + # Switch |
| 188 | + PROJECT_DIR = os.path.abspath(new_path) |
| 189 | + _invalidate_cache() |
| 190 | + |
| 191 | + # Restart watcher |
| 192 | + _watcher = SessionWatcher(PROJECT_DIR, broadcast_message) |
| 193 | + _watcher.start() |
| 194 | + |
| 195 | + # Save to project list |
| 196 | + _ensure_current_project() |
| 197 | + |
| 198 | + return jsonify({'status': 'switched', 'path': PROJECT_DIR}) |
| 199 | + |
| 200 | + |
| 201 | +@app.route('/api/projects/add', methods=['POST']) |
| 202 | +def api_add_project(): |
| 203 | + data = json.loads(request.data) if request.data else {} |
| 204 | + new_path = data.get('path', '') |
| 205 | + |
| 206 | + if not new_path: |
| 207 | + return jsonify({'error': 'Path required'}), 400 |
| 208 | + |
| 209 | + new_path = os.path.abspath(os.path.expanduser(new_path)) |
| 210 | + if not os.path.isdir(new_path): |
| 211 | + return jsonify({'error': 'Directory does not exist'}), 400 |
| 212 | + |
| 213 | + if not os.path.isdir(os.path.join(new_path, '.humanize')): |
| 214 | + return jsonify({'error': 'No .humanize/ directory found'}), 400 |
| 215 | + |
| 216 | + projects = _load_projects() |
| 217 | + if new_path not in projects: |
| 218 | + projects.append(new_path) |
| 219 | + _save_projects(projects) |
| 220 | + |
| 221 | + return jsonify({'status': 'added', 'path': new_path}) |
| 222 | + |
| 223 | + |
| 224 | +@app.route('/api/projects/remove', methods=['POST']) |
| 225 | +def api_remove_project(): |
| 226 | + data = json.loads(request.data) if request.data else {} |
| 227 | + path = data.get('path', '') |
| 228 | + |
| 229 | + projects = _load_projects() |
| 230 | + projects = [p for p in projects if p != path] |
| 231 | + _save_projects(projects) |
| 232 | + |
| 233 | + return jsonify({'status': 'removed'}) |
| 234 | + |
| 235 | + |
121 | 236 | # --- REST API --- |
122 | 237 |
|
123 | 238 | @app.route('/api/sessions') |
@@ -196,18 +311,26 @@ def api_generate_report(session_id): |
196 | 311 | with open(report_path, 'r', encoding='utf-8') as f: |
197 | 312 | return jsonify({'status': 'exists', 'content': f.read()}) |
198 | 313 |
|
199 | | - # Collect round summaries and review results |
200 | | - summaries = [] |
| 314 | + # Collect round summaries and review results (sorted numerically by round number) |
201 | 315 | import glob as _glob |
202 | | - for sf in sorted(_glob.glob(os.path.join(session_dir, 'round-*-summary.md'))): |
| 316 | + import re as _re_local |
| 317 | + |
| 318 | + def _sort_round_files(files): |
| 319 | + def _round_num(path): |
| 320 | + m = _re_local.search(r'round-(\d+)-', os.path.basename(path)) |
| 321 | + return int(m.group(1)) if m else 0 |
| 322 | + return sorted(files, key=_round_num) |
| 323 | + |
| 324 | + summaries = [] |
| 325 | + for sf in _sort_round_files(_glob.glob(os.path.join(session_dir, 'round-*-summary.md'))): |
203 | 326 | try: |
204 | 327 | with open(sf, 'r', encoding='utf-8') as f: |
205 | 328 | summaries.append(f'--- {os.path.basename(sf)} ---\n{f.read()}') |
206 | 329 | except (PermissionError, OSError): |
207 | 330 | pass |
208 | 331 |
|
209 | 332 | reviews = [] |
210 | | - for rf in sorted(_glob.glob(os.path.join(session_dir, 'round-*-review-result.md'))): |
| 333 | + for rf in _sort_round_files(_glob.glob(os.path.join(session_dir, 'round-*-review-result.md'))): |
211 | 334 | try: |
212 | 335 | with open(rf, 'r', encoding='utf-8') as f: |
213 | 336 | reviews.append(f'--- {os.path.basename(rf)} ---\n{f.read()}') |
|
0 commit comments