Phase 4: backups w/ volumes, notifications, settings & users, audit page (0.4.0)
- Backup/restore: per-stack tar.gz incl. named-volume snapshots (helper container), upload restore with rename/overwrite/conflict detection. - Notifications: ntfy/Discord/Slack/Gotify/generic webhooks, per-event subscriptions; wired into the update checker and stack lifecycle. - Settings page: update-check interval, webhook CRUD + test, user management (with last-admin safeguards). - Audit log page (searchable, paginated). - Mobile-responsive sidebar/layout. Multi-host agents and remote backup destinations (SFTP/S3) deferred. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
22d9864436
commit
8d19b09abd
@@ -19,8 +19,14 @@ from models.stack import (
|
||||
StackCreate,
|
||||
StackUpdate,
|
||||
)
|
||||
from models.setting import (
|
||||
EVENT_PULL_FAILED,
|
||||
EVENT_STACK_ERROR,
|
||||
EVENT_STACK_START,
|
||||
EVENT_STACK_STOP,
|
||||
)
|
||||
from models.user import User
|
||||
from services import audit_service, compose_service
|
||||
from services import audit_service, compose_service, notify_service
|
||||
from services.convert_service import convert_docker_run
|
||||
|
||||
router = APIRouter(prefix="/api/stacks", tags=["stacks"])
|
||||
@@ -220,6 +226,35 @@ def clone_stack(
|
||||
# --------------------------------------------------------------------------- #
|
||||
|
||||
|
||||
# which lifecycle actions emit a notification on success
|
||||
_START_ACTIONS = {"start", "restart", "update"}
|
||||
_STOP_ACTIONS = {"stop", "down"}
|
||||
|
||||
|
||||
async def _notify_lifecycle(action_name: str, stack_id: str, ok: bool, detail: str, session) -> None:
|
||||
try:
|
||||
if not ok:
|
||||
event = EVENT_PULL_FAILED if action_name in ("pull", "update") else EVENT_STACK_ERROR
|
||||
await notify_service.notify(
|
||||
event,
|
||||
f"Stack '{stack_id}' {action_name} failed",
|
||||
detail or f"compose {action_name} returned a non-zero exit code.",
|
||||
session,
|
||||
)
|
||||
elif action_name in _START_ACTIONS:
|
||||
await notify_service.notify(
|
||||
EVENT_STACK_START, f"Stack '{stack_id}' started",
|
||||
f"compose {action_name} completed successfully.", session,
|
||||
)
|
||||
elif action_name in _STOP_ACTIONS:
|
||||
await notify_service.notify(
|
||||
EVENT_STACK_STOP, f"Stack '{stack_id}' stopped",
|
||||
f"compose {action_name} completed successfully.", session,
|
||||
)
|
||||
except Exception: # noqa: BLE001 - notifications are best-effort
|
||||
pass
|
||||
|
||||
|
||||
async def _lifecycle(action_fn, action_name, stack_id, request, session, user):
|
||||
_get_stack_or_404(session, stack_id)
|
||||
result = await action_fn(stack_id)
|
||||
@@ -227,12 +262,15 @@ async def _lifecycle(action_fn, action_name, stack_id, request, session, user):
|
||||
session, user=user.username, action=f"stack.{action_name}", target=stack_id,
|
||||
detail=f"rc={result.get('returncode')}", ip=_client_ip(request),
|
||||
)
|
||||
if result.get("returncode") not in (0, None):
|
||||
ok = result.get("returncode") in (0, None)
|
||||
stderr = result.get("stderr", "").strip()[-2000:]
|
||||
await _notify_lifecycle(action_name, stack_id, ok, stderr, session)
|
||||
if not ok:
|
||||
raise HTTPException(
|
||||
status_code=500,
|
||||
detail={
|
||||
"error": f"compose {action_name} failed",
|
||||
"detail": result.get("stderr", "").strip()[-2000:],
|
||||
"detail": stderr,
|
||||
},
|
||||
)
|
||||
return result
|
||||
|
||||
Reference in New Issue
Block a user