🚧 WIP
This commit is contained in:
@@ -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 && (
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user