1111
1212import json
1313import logging
14+ import os
1415from pathlib import Path
1516from typing import TYPE_CHECKING , Any
1617
18+ import httpx
1719from starlette .requests import Request
1820from 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
353391const 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);
568607loadTools();
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