This commit is contained in:
Jonas Bengtsson
2026-09-19 18:08:02 +02:00
parent a16a01b1e7
commit 680ad7a624
14 changed files with 483 additions and 14 deletions
+20 -5
View File
@@ -84,13 +84,19 @@ export function BrokerConfigPage() {
});
const deploy = useMutation({
mutationFn: (comment: string | null) =>
api.post<{ status: string; revision: number; broker_healthy: boolean }>(
"/broker/config/deploy",
{ comment },
),
api.post<{
status: string;
revision: number;
broker_healthy: boolean;
restart_required: boolean;
}>("/broker/config/deploy", { comment }),
onSuccess: () => {
refresh();
queryClient.invalidateQueries({ queryKey: ["revisions"] });
// The glowing restart-needed indicator polls on its own cycle;
// invalidate its query so a listener-changing deploy shows it
// IMMEDIATELY instead of up to 15s later.
queryClient.invalidateQueries({ queryKey: ["system-status"] });
},
});
@@ -223,9 +229,18 @@ export function BrokerConfigPage() {
{deploy.isPending ? "Deploying…" : "Validate & deploy configuration"}
</button>
{deploy.data && (
<div className="banner ok">
<div className={`banner ${deploy.data.restart_required ? "warn" : "ok"}`}>
Deployed revision {deploy.data.revision}. Broker health:{" "}
{deploy.data.broker_healthy ? "OK" : "unknown — check logs"}
{deploy.data.restart_required && (
<>
{" "}
<strong>
⚠ Restart required — listener changes are pending. Go to
Administration → System to restart the broker.
</strong>
</>
)}
</div>
)}
{deploy.error && (
+116
View File
@@ -0,0 +1,116 @@
import { useEffect, useState } from "react";
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { api, type SystemStatus } from "../api";
export function SystemPage() {
const queryClient = useQueryClient();
const [error, setError] = useState<string | null>(null);
const [banner, setBanner] = useState<string | null>(null);
const status = useQuery({
queryKey: ["system-status"],
queryFn: () => api.get<SystemStatus>("/system/status"),
// Poll while a restart is in flight so "broker down" resolves by itself.
refetchInterval: (query) =>
query.state.data && !query.state.data.broker_alive ? 3000 : 15000,
});
const restart = useMutation({
mutationFn: () => api.post<{ status: string }>("/system/broker/restart"),
onSuccess: () => {
setBanner("Restart signal sent — the broker stops and Docker brings it back.");
setError(null);
// Force an immediate re-poll; the alive flag flips within seconds.
queryClient.invalidateQueries({ queryKey: ["system-status"] });
},
onError: (e: Error) => setError(e.message),
});
const data = status.data;
const restarting = data !== undefined && !data.broker_alive;
// Clear the transient banner once the broker is back.
useEffect(() => {
if (data?.broker_alive) setBanner(null);
}, [data?.broker_alive]);
return (
<div>
<h2>System</h2>
<p>
Broker lifecycle operations through the control sidecar (spec section
29). The restart stops Mosquitto gracefully — persistent sessions and
queued messages are saved — and Docker's restart policy brings the
container back within seconds.
</p>
{error && <div className="banner error">{error}</div>}
{banner && !error && <div className="banner ok">{banner}</div>}
{status.error && (
<div className="banner error">
Failed to load system status: {(status.error as Error).message}
</div>
)}
{data?.restart_required && (
<div className="banner warn">
<strong>Restart needed:</strong> the deployed configuration contains
listener changes that a config reload cannot apply (spec section 28).
Restart the broker below to activate them.
</div>
)}
<table>
<thead>
<tr>
<th>Service</th>
<th>State</th>
</tr>
</thead>
<tbody>
<tr>
<td>Mosquitto broker</td>
<td>
{restarting ? (
<span style={{ color: "#8a5a00" }}>
restarting… (polling until it is back)
</span>
) : data?.broker_alive ? (
<span style={{ color: "#1e6b3a" }}>running</span>
) : (
<span style={{ color: "#a4342d" }}>unreachable</span>
)}
</td>
</tr>
<tr>
<td>Listener changes pending a restart</td>
<td>{data?.restart_required ? "yes" : "no"}</td>
</tr>
</tbody>
</table>
<p />
<button
className="primary"
disabled={restart.isPending || restarting}
onClick={() => {
if (
confirm(
"Restart the Mosquitto broker?\n\n" +
"All connected devices are disconnected (they can reconnect " +
"once it is back). Persistent sessions and queued messages are " +
"saved. The broker is unavailable for a few seconds.",
)
)
restart.mutate();
}}
>
{restart.isPending
? "Sending restart signal…"
: restarting
? "Broker is restarting…"
: "Restart broker"}
</button>
</div>
);
}