Phase 11: remote UX & network attach (0.11.0)
- Live remote-stack logs over a WebSocket proxied through the central app to
the agent (/ws/agent-logs/{agent}/{stack}); agent gains a WS log endpoint.
- Deploy to a remote host from the UI: host selector in the New Stack editor
and template dialog; templates instantiate onto an agent via the proxy.
- Network attach/detach: expandable inspect view per network with
connect/disconnect + container picker; GET /{id}/containers, POST connect/disconnect.
- Remove dead pages/Placeholder.tsx.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
ec7e3e706f
commit
1931500c24
+43
-2
@@ -16,7 +16,21 @@ from dataclasses import asdict
|
||||
|
||||
import tempfile
|
||||
|
||||
from fastapi import Depends, FastAPI, File, Form, Header, HTTPException, Query, Request, UploadFile
|
||||
import json
|
||||
|
||||
from fastapi import (
|
||||
Depends,
|
||||
FastAPI,
|
||||
File,
|
||||
Form,
|
||||
Header,
|
||||
HTTPException,
|
||||
Query,
|
||||
Request,
|
||||
UploadFile,
|
||||
WebSocket,
|
||||
WebSocketDisconnect,
|
||||
)
|
||||
from fastapi.responses import FileResponse, JSONResponse
|
||||
from pydantic import BaseModel
|
||||
|
||||
@@ -26,7 +40,7 @@ from services import backup_service, compose_service
|
||||
|
||||
logger = logging.getLogger("stackpilot.agent")
|
||||
|
||||
AGENT_VERSION = "0.10.0"
|
||||
AGENT_VERSION = "0.11.0"
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------- #
|
||||
@@ -274,6 +288,33 @@ async def restore_stack(
|
||||
os.unlink(tmp.name)
|
||||
|
||||
|
||||
@app.websocket("/agent/ws/logs/{stack_id}")
|
||||
async def ws_logs(websocket: WebSocket, stack_id: str, token: str | None = Query(default=None)):
|
||||
"""Stream `docker compose logs -f` to the central app (token via query param)."""
|
||||
await websocket.accept()
|
||||
expected = settings.AGENT_TOKEN
|
||||
if not expected or token != expected:
|
||||
await websocket.close(code=4401)
|
||||
return
|
||||
if not os.path.isdir(compose_service.stack_dir(stack_id)):
|
||||
await websocket.send_text(json.dumps({"type": "error", "detail": "stack not found"}))
|
||||
await websocket.close()
|
||||
return
|
||||
args = ["logs", "--no-color", "--tail", "200", "--timestamps", "-f"]
|
||||
try:
|
||||
async for line in compose_service.stream_compose(stack_id, args):
|
||||
await websocket.send_text(
|
||||
json.dumps({"type": "log", "stack_id": stack_id, "service": None, "line": line})
|
||||
)
|
||||
except WebSocketDisconnect:
|
||||
pass
|
||||
except Exception as exc: # noqa: BLE001
|
||||
try:
|
||||
await websocket.send_text(json.dumps({"type": "error", "detail": str(exc)}))
|
||||
except Exception: # noqa: BLE001
|
||||
pass
|
||||
|
||||
|
||||
@app.get("/agent/health")
|
||||
def health() -> dict:
|
||||
return {"status": "ok"}
|
||||
|
||||
Reference in New Issue
Block a user