Auto-commit: v2.11 — Fix live UI log feed (db_log_callback + run_status)

- web/tasks.py: db_log_callback bare `except: break` replaced with
  explicit `except Exception as _e: print(...)` so insertion failures
  are visible in Docker logs. Also fixed datetime.utcnow() → .isoformat()
  for clean string storage in SQLite.
  Same fix applied to db_progress_callback.

- web/routes/run.py (run_status): added db.session.expire_all() to
  force fresh reads; raw sqlite3 bypass query when ORM returns no rows;
  file fallback wrapped in try/except with stdout error reporting;
  secondary check for web_console.log inside the run directory;
  utf-8 encoding on all file opens.

- ai_blueprint.md: bumped to v2.11, documented root causes and fixes.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-21 15:28:27 -05:00
parent 493435e43c
commit 87f24d2bd8
3 changed files with 61 additions and 12 deletions

View File

@@ -74,23 +74,64 @@ def view_run(id):
@run_bp.route('/run/<int:id>/status')
@login_required
def run_status(id):
run = db.session.get(Run, id) or Run.query.get_or_404(id)
import sqlite3 as _sql3
import sys as _sys
from core import config as _cfg
# Expire session so we always read fresh values from disk (not cached state)
db.session.expire_all()
run = db.session.get(Run, id)
if not run:
return {"status": "not_found", "log": "", "cost": 0, "percent": 0, "start_time": None}, 404
log_content = ""
last_log = None
# 1. ORM query for log entries
logs = LogEntry.query.filter_by(run_id=id).order_by(LogEntry.timestamp).all()
if logs:
log_content = "\n".join([f"[{l.timestamp.strftime('%H:%M:%S')}] {l.phase:<15} | {l.message}" for l in logs])
last_log = logs[-1]
# 2. Raw sqlite3 fallback — bypasses any SQLAlchemy session caching
if not log_content:
if run.log_file and os.path.exists(run.log_file):
with open(run.log_file, 'r') as f: log_content = f.read()
elif run.status in ['queued', 'running']:
temp_log = os.path.join(run.project.folder_path, f"system_log_{run.id}.txt")
if os.path.exists(temp_log):
with open(temp_log, 'r') as f: log_content = f.read()
try:
_db_path = os.path.join(_cfg.DATA_DIR, "bookapp.db")
with _sql3.connect(_db_path, timeout=5) as _conn:
_rows = _conn.execute(
"SELECT timestamp, phase, message FROM log_entry WHERE run_id = ? ORDER BY timestamp",
(id,)
).fetchall()
if _rows:
log_content = "\n".join([
f"[{str(r[0])[:8]}] {str(r[1]):<15} | {r[2]}"
for r in _rows
])
except Exception as _e:
print(f"[run_status] sqlite3 fallback error for run {id}: {type(_e).__name__}: {_e}", flush=True, file=_sys.stdout)
# 3. File fallback — reads the log file written by the task worker
if not log_content:
try:
if run.log_file and os.path.exists(run.log_file):
with open(run.log_file, 'r', encoding='utf-8', errors='replace') as f:
log_content = f.read()
elif run.status in ['queued', 'running']:
project_folder = run.project.folder_path
# Temp log written at task start (before run dir exists)
temp_log = os.path.join(project_folder, f"system_log_{run.id}.txt")
if os.path.exists(temp_log):
with open(temp_log, 'r', encoding='utf-8', errors='replace') as f:
log_content = f.read()
else:
# Also check inside the run directory (after engine creates it)
run_dir = os.path.join(project_folder, "runs", f"run_{run.id}")
console_log = os.path.join(run_dir, "web_console.log")
if os.path.exists(console_log):
with open(console_log, 'r', encoding='utf-8', errors='replace') as f:
log_content = f.read()
except Exception as _e:
print(f"[run_status] file fallback error for run {id}: {type(_e).__name__}: {_e}", flush=True, file=_sys.stdout)
response = {
"status": run.status,