Skip to content

Commit 93a3e52

Browse files
kingpanther13claude
andcommitted
feat: add in-UI restart button using Supervisor API
- New POST /api/settings/restart endpoint calls http://supervisor/addons/self/restart with SUPERVISOR_TOKEN - New GET /api/settings/info exposes whether running as add-on - Frontend shows "Restart Add-on" button in the restart notice banner (only visible when running as add-on) - Click opens a confirmation dialog and triggers the restart Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent fe8440a commit 93a3e52

1 file changed

Lines changed: 77 additions & 4 deletions

File tree

src/ha_mcp/settings_ui.py

Lines changed: 77 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,11 @@
1111

1212
import json
1313
import logging
14+
import os
1415
from pathlib import Path
1516
from typing import TYPE_CHECKING, Any
1617

18+
import httpx
1719
from starlette.requests import Request
1820
from starlette.responses import HTMLResponse, JSONResponse
1921

@@ -310,8 +312,14 @@ def apply_tool_visibility(
310312
.pin-notice.show { display: block; }
311313
.restart-notice { background: #3a1a1a; border: 1px solid #7a1a1a; border-radius: 10px;
312314
padding: 12px 16px; margin-bottom: 12px; font-size: 0.9rem; color: #ff9090;
313-
font-weight: 500; display: none; }
314-
.restart-notice.show { display: block; }
315+
font-weight: 500; display: none; align-items: center; justify-content: space-between; gap: 12px; }
316+
.restart-notice.show { display: flex; }
317+
.restart-notice-text { flex: 1; }
318+
.restart-btn { padding: 8px 16px; border-radius: 8px; border: none;
319+
background: var(--accent); color: white; font-weight: 600; cursor: pointer;
320+
font-size: 0.85rem; flex-shrink: 0; }
321+
.restart-btn:hover { background: var(--accent-hover); }
322+
.restart-btn:disabled { opacity: 0.5; cursor: not-allowed; }
315323
</style>
316324
</head>
317325
<body>
@@ -329,8 +337,11 @@ def apply_tool_visibility(
329337
and pinning has no extra effect.
330338
</div>
331339
<div class="restart-notice" id="restartNotice">
332-
⚠ Changes saved. Restart the add-on for them to take effect — disabled
333-
tools will be fully removed from the MCP tool list on next startup.
340+
<span class="restart-notice-text">
341+
⚠ Changes saved. Restart the add-on for them to take effect — disabled
342+
tools will be fully removed from the MCP tool list on next startup.
343+
</span>
344+
<button class="restart-btn" id="restartBtn" style="display:none">Restart Add-on</button>
334345
</div>
335346
<div class="summary" id="summary"></div>
336347
<input type="text" class="search" id="search" placeholder="Search tools...">
@@ -348,6 +359,33 @@ def apply_tool_visibility(
348359
toolStates = data.states;
349360
render();
350361
updateStatus('Loaded');
362+
363+
// Show restart button if running as add-on
364+
try {
365+
const infoResp = await fetch('./api/settings/info');
366+
const info = await infoResp.json();
367+
if (info.is_addon) {
368+
document.getElementById('restartBtn').style.display = '';
369+
}
370+
} catch (_e) {}
371+
}
372+
373+
async function restartAddon() {
374+
const btn = document.getElementById('restartBtn');
375+
if (!confirm('Restart the add-on now? The web UI will become unreachable for ~30 seconds.')) return;
376+
btn.disabled = true;
377+
btn.textContent = 'Restarting...';
378+
try {
379+
const resp = await fetch('./api/settings/restart', {method: 'POST'});
380+
if (resp.ok) {
381+
btn.textContent = 'Restart initiated — reload page in ~30s';
382+
} else {
383+
btn.textContent = 'Restart failed';
384+
btn.disabled = false;
385+
}
386+
} catch (_e) {
387+
btn.textContent = 'Connection lost (expected during restart)';
388+
}
351389
}
352390
353391
const DEFAULT_PINNED = """ + json.dumps(list(DEFAULT_PINNED_TOOLS)) + """;
@@ -565,6 +603,7 @@ def apply_tool_visibility(
565603
});
566604
});
567605
606+
document.getElementById('restartBtn').addEventListener('click', restartAddon);
568607
loadTools();
569608
</script>
570609
</body>
@@ -624,3 +663,37 @@ async def _save_tools(request: Request) -> JSONResponse:
624663
"pinned": pinned_count,
625664
"restart_required": True,
626665
})
666+
667+
@mcp.custom_route("/api/settings/restart", methods=["POST"])
668+
async def _restart_addon(_: Request) -> JSONResponse:
669+
token = os.environ.get("SUPERVISOR_TOKEN")
670+
if not token:
671+
return JSONResponse(
672+
{"success": False, "error": {"code": "NOT_IN_ADDON", "message": "Restart only available in add-on mode"}},
673+
status_code=400,
674+
)
675+
try:
676+
async with httpx.AsyncClient(timeout=10.0) as client:
677+
resp = await client.post(
678+
"http://supervisor/addons/self/restart",
679+
headers={"Authorization": f"Bearer {token}"},
680+
)
681+
if resp.status_code >= 400:
682+
logger.error("Supervisor restart failed: %d %s", resp.status_code, resp.text)
683+
return JSONResponse(
684+
{"success": False, "error": {"code": "SUPERVISOR_ERROR", "message": f"Supervisor returned {resp.status_code}"}},
685+
status_code=502,
686+
)
687+
except httpx.HTTPError as e:
688+
logger.exception("Failed to reach Supervisor for restart")
689+
return JSONResponse(
690+
{"success": False, "error": {"code": "SUPERVISOR_UNREACHABLE", "message": str(e)}},
691+
status_code=502,
692+
)
693+
return JSONResponse({"success": True, "message": "Restart initiated"})
694+
695+
@mcp.custom_route("/api/settings/info", methods=["GET"])
696+
async def _settings_info(_: Request) -> JSONResponse:
697+
return JSONResponse({
698+
"is_addon": bool(os.environ.get("SUPERVISOR_TOKEN")),
699+
})

0 commit comments

Comments
 (0)