feat(git): implement deployment tracking and enhance lifecycle UX

Introduce a deployment recording system to track dashboard versions
across environments and improve the Git management user experience.

- Add `Deployment` model and Alembic migration to persist deployment
  history.
- Implement `GitDeploymentRecorder` and `GitFingerprint` plugins to
  automate deployment logging and content hashing.
- Add `get_deployment_status` API endpoint to retrieve real-time
  environment states.
- Refactor `GitLifecycleHeader` to prioritize Call-to-Action (CTA)
  buttons and improve visual hierarchy.
- Update `GitWorkspacePanel` to emphasize version saving and
  streamline commit workflows.
- Enhance `GitEnvironmentTimeline` with deployment status integration,
  collapsible UI, and improved theme consistency.
- Add auto-navigation logic in `GitManagerModel` to guide users to
  relevant tabs based on recommended actions.
- Clean up obsolete documentation and update i18n strings for
  git visualization features.
This commit is contained in:
2026-07-12 14:57:03 +03:00
parent b39d9991b9
commit 0cb1f80cd6
34 changed files with 2018 additions and 1512 deletions

View File

@@ -118,6 +118,34 @@ async def lifespan(app: FastAPI):
_s.close()
except Exception as _e:
logger.explore("Failed to clean up stuck validation runs", error=str(_e))
# General reconciliation of stuck tasks from previous lifetime (idea from queue recovery patterns).
# Tasks left in RUNNING when the in-memory asyncio tasks were lost (backend restart/crash).
# This generalizes the ValidationRun-specific cleanup above.
try:
from sqlalchemy.orm import Session as _Ses
from src.core.database import TasksSessionLocal as _TasksDb
from src.models.task import TaskRecord as _TR
from datetime import datetime as _dt, timezone as _tz
_s: _Ses = _TasksDb()
_stuck_tasks = _s.query(_TR).filter(_TR.status == "RUNNING").all()
for _t in _stuck_tasks:
_t.status = "FAILED"
_t.finished_at = _dt.now(_tz.utc)
_t.error = "Force-stopped: backend restarted while task was in progress"
# result can carry hint
if not _t.result:
_t.result = {"error": "interrupted_by_restart"}
logger.reason(
"Force-stopped stuck task",
payload={"task_id": _t.id, "type": _t.type},
)
_s.commit()
_s.close()
except Exception as _e:
logger.explore("Failed to clean up stuck general tasks", error=str(_e))
logger.reason("Initializing AsyncJobRunner")
get_async_job_runner() # Initialize singleton with running event loop BEFORE scheduler starts
logger.reason("Starting scheduler")
@@ -126,7 +154,31 @@ async def lifespan(app: FastAPI):
logger.reflect("Application startup complete")
yield
# Shutdown
# Improved graceful shutdown (inspired by Celery worker drain + async best practices).
# 1. Stop accepting new scheduled jobs.
# 2. Attempt to let in-flight tasks finish or cancel them within timeout.
# 3. Best-effort flush.
scheduler.stop()
try:
from .dependencies import get_config_manager, get_task_manager
tm = get_task_manager()
# Access internal tracking (tasks are asyncio.Task objects)
running = list(getattr(tm, '_async_tasks', {}).values())
if running:
cm = get_config_manager()
graceful = getattr(cm, 'settings', None) if cm else None
timeout = 30.0
if graceful and hasattr(graceful, 'graceful_shutdown_timeout'):
timeout = getattr(graceful, 'graceful_shutdown_timeout', 30.0)
logger.reason(f"Draining {len(running)} running task(s) with timeout={timeout}s")
done, pending = await asyncio.wait(running, timeout=timeout)
for p in pending:
p.cancel()
if pending:
await asyncio.gather(*pending, return_exceptions=True)
logger.reason("Task drain complete")
except Exception as _e:
logger.explore("Graceful task drain during shutdown encountered error", error=str(_e))
# #endregion lifespan