feat: real service runtime — private networks, replicas, lifecycle, rollback

Services now reconcile into actual Docker containers instead of
bookkeeping rows:

- Per-project bridge network with service-name DNS aliases and
  ${{service.KEY}} env expansion (HOST/PORT builtins)
- Replicas, published ports, Traefik domain routing, health checks,
  restart policies persisted on the service spec; spec-hash drift
  recreates containers on redeploy
- Lifecycle API: start/stop/restart/redeploy, live runtime state,
  real image rollback (replaces the fake sleep)
- Manual scale and PUT replicas reconcile live containers; service
  and project delete remove their containers and the project network
- Canvas: real URLs, replica badges, env filter, right-click
  deploy/restart/stop/delete menu, empty state
- Service detail: real lifecycle buttons, live replica/URL strip,
  Networking editor, ${{ }} variable helper
- Fix prebuilt-image pulls racing the tag (drain the pull stream)

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
Tomas DvorakandDevin committed 2026-09-21 19:31:19 +02:00
1 parent c16b32f56a
commit ff356230fe
20 files changed
+2270 -243

No files matched your search

+20 -3
View File
@@ -204,7 +204,16 @@ func (h *ScalingHandler) ManualScale(c *gin.Context) {
event, err := h.autoScaler.ManualScale(c.Request.Context(), serviceID, request.Replicas, strings.TrimSpace(request.Reason))
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
// Service unknown to the autoscaler: the runtime is authoritative —
// reconcile containers directly and persist the replica count.
if runtimeErr := runtimeScaleService(c, serviceID, request.Replicas); runtimeErr != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error(), "runtime_error": runtimeErr.Error()})
return
}
c.JSON(http.StatusOK, gin.H{
"message": "Service scaled successfully",
"state": gin.H{"service_id": serviceID, "desired_replicas": request.Replicas},
})
return
}
@@ -214,11 +223,19 @@ func (h *ScalingHandler) ManualScale(c *gin.Context) {
return
}
c.JSON(http.StatusOK, gin.H{
// Apply the replica change to live containers (best effort — the
// bookkeeping update above already succeeded).
runtimeErr := runtimeScaleService(c, serviceID, request.Replicas)
resp := gin.H{
"message": "Service scaled successfully",
"event": event,
"state": state,
})
}
if runtimeErr != nil {
resp["runtime_error"] = runtimeErr.Error()
}
c.JSON(http.StatusOK, resp)
}
// GetScalingStatus returns the overall status of the auto-scaler