PluginProbe
BetterDocs – AI Documentation, Knowledge Base, MCP Server, Docs, Wikis, FAQ & Chatbot / 4.9.5
BetterDocs – AI Documentation, Knowledge Base, MCP Server, Docs, Wikis, FAQ & Chatbot v4.9.5
4.9.5 4.9.4 4.9.3 4.9.2 4.9.1 4.9.0 4.8.2 4.8.1 4.8.0 4.7.0 4.6.2 4.6.1 4.6.0 4.5.6 4.5.5 4.5.4 4.5.3 4.5.2 4.5.1 4.5.0 4.4.1 4.4.0 3.3.4 3.4.0 3.4.1 All 203 releases
betterdocs / react-src / admin / mcp / components / ConnectionHealth.js

ConnectionHealth.js in BetterDocs – AI Documentation, Knowledge Base, MCP Server, Docs, Wikis, FAQ & Chatbot 4.9.5, at react-src/admin/mcp/components/ConnectionHealth.js

305 lines 13.5 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 import React, { useState } from "react";
2 import { __, _n, sprintf } from "@wordpress/i18n";
3 import { useMutation, useQuery } from "@tanstack/react-query";
4
5 import { getHealth, runSelfTest } from "../utils/api";
6 import { IconPlay, IconRefresh } from "./Icons";
7
8 // Exported so the master switch can invalidate this query too: the card mounts
9 // when the switch goes on (ADR-058), and the report it shows then must describe
10 // the server as it is now, not as it was while it refused every connection.
11 export const HEALTH_KEY = "bd-mcp-health";
12
13 // Stages that mean "some clients can't connect", not "nothing works". A client
14 // holding the connection token is fine through all of these; only clients that
15 // connect by URL alone are affected, so they read as a warning rather than a
16 // failure.
17 const WARN_STAGES = ["tls", "redirect", "no_tools", "rewrite", "discovery", "challenge", "ua_filter"];
18
19 const testVariant = (result) => {
20 if (!result) return "info";
21 if (result.ok) return "success";
22 return WARN_STAGES.includes(result.stage) ? "warning" : "error";
23 };
24
25 const Row = ({ label, tone, badge, detail }) => (
26 <div className="bd-mcp-health__row">
27 <div className="bd-mcp-health__text">
28 <span className="bd-mcp-health__label">{label}</span>
29 {detail && <span className="bd-mcp-health__detail">{detail}</span>}
30 </div>
31 <span className={`bda-badge ${tone}`}>{badge}</span>
32 </div>
33 );
34
35 /**
36 * The first URL problem worth naming, most damaging first. A site can have more
37 * than one of these at once, and telling someone about the REST scheme while
38 * they are still on plain HTTP is not help.
39 */
40 const describeUrls = (urls) => {
41 if (!urls.is_https) {
42 return __("This site is served over HTTP. Most AI clients refuse to connect to anything but HTTPS.", "betterdocs");
43 }
44 if (!urls.home_siteurl_match) {
45 return __("Site Address and WordPress Address use different schemes, so requests can bounce between HTTP and HTTPS.", "betterdocs");
46 }
47 if (!urls.rest_matches_home) {
48 return __("The REST API URL uses a different scheme from the site address, which redirects MCP requests.", "betterdocs");
49 }
50 return __("HTTPS is on and every WordPress URL agrees.", "betterdocs");
51 };
52
53 /**
54 * Who provides the Abilities API, and what that means for the tool catalog.
55 */
56 const describeOwner = (owner, version) => {
57 if (owner === "none") {
58 return __("No Abilities API is available, so no tools can be offered to an AI client.", "betterdocs");
59 }
60 if (owner === "foreign") {
61 return __("Another plugin is providing the Abilities API. BetterDocs registers into it, but the version it ships governs.", "betterdocs");
62 }
63 if (owner === "bundled") {
64 return sprintf(
65 /* translators: %s: the bundled abilities-api package version. */
66 __("Provided by the runtime bundled with BetterDocs (%s).", "betterdocs"),
67 version || __("unknown version", "betterdocs"),
68 );
69 }
70 return sprintf(
71 /* translators: %s: the WordPress version providing the Abilities API. */
72 __("Provided by WordPress %s.", "betterdocs"),
73 version || __("(unknown version)", "betterdocs"),
74 );
75 };
76
77 const ownerTone = (owner) => {
78 if (owner === "none") return "red";
79 if (owner === "foreign") return "amber";
80 return "green";
81 };
82
83 const OWNER_LABELS = {
84 core: __("WordPress core", "betterdocs"),
85 bundled: __("BetterDocs (bundled)", "betterdocs"),
86 foreign: __("Another plugin", "betterdocs"),
87 none: __("Nobody", "betterdocs"),
88 };
89
90 export default function ConnectionHealth() {
91 const [result, setResult] = useState(null);
92
93 const { data, isLoading, isError, refetch, isFetching } = useQuery({
94 queryKey: [HEALTH_KEY],
95 queryFn: getHealth,
96 });
97
98 const test = useMutation({
99 mutationFn: runSelfTest,
100 onSuccess: (res) => {
101 setResult(res);
102 refetch();
103 },
104 onError: () =>
105 setResult({
106 ok: false,
107 stage: "unreachable",
108 message: __("The test request itself failed. Reload the page and try again.", "betterdocs"),
109 checks: [],
110 }),
111 });
112
113 if (isLoading) {
114 return (
115 <div className="bda-card">
116 <div className="bda-card-body">
117 <p className="bd-mcp-muted">{__("Checking connection health…", "betterdocs")}</p>
118 </div>
119 </div>
120 );
121 }
122
123 if (isError || !data) {
124 return (
125 <div className="bda-card">
126 <div className="bda-card-body">
127 <div className="bd-mcp-alert is-error">
128 {__("Couldn't load the health report. Reload the page and try again.", "betterdocs")}
129 </div>
130 </div>
131 </div>
132 );
133 }
134
135 const { urls, abilities, runtime, capabilities, database, plugin, mcp } = data;
136
137 const urlsOk = urls.is_https && urls.home_siteurl_match && urls.rest_matches_home;
138 const urlsDetail = describeUrls(urls);
139
140 const owner = runtime?.abilities_api?.source || "none";
141 const toolCount = abilities?.betterdocs || 0;
142 const missing = Array.isArray(capabilities?.missing) ? capabilities.missing : [];
143 const dbOk = !!database?.up_to_date && (!database.pro || database.pro.up_to_date);
144 const proState = plugin?.pro?.state || "pro_not_installed";
145 const proLabels = {
146 ok: __("Pro active", "betterdocs"),
147 pro_unlicensed: __("Pro active, unlicensed", "betterdocs"),
148 pro_active_setting_off: __("Pro active", "betterdocs"),
149 pro_not_active: __("Pro inactive", "betterdocs"),
150 pro_not_installed: __("Free only", "betterdocs"),
151 };
152
153 return (
154 <div className="bda-card bd-mcp-health">
155 <div className="bda-card-head bd-mcp-cardhead">
156 <div>
157 <h3 className="bda-card-title">{__("Connection health", "betterdocs")}</h3>
158 <p className="bd-mcp-muted">
159 {__(
160 "What an AI client finds when it reaches this site. Test connection makes the real round trip from this server and names the first step that fails.",
161 "betterdocs",
162 )}
163 </p>
164 </div>
165 {/* This card only renders while the switch is on (ADR-058), so
166 both buttons are simply available: there is no longer a state
167 in which Test connection could only report "MCP is off". */}
168 <div className="bd-mcp-actions">
169 <button type="button" className="bda-btn ghost sm" onClick={() => refetch()} disabled={isFetching}>
170 <IconRefresh />
171 {__("Re-check", "betterdocs")}
172 </button>
173 <button
174 type="button"
175 className="bda-btn primary sm"
176 onClick={() => test.mutate()}
177 disabled={test.isLoading}
178 >
179 <IconPlay />
180 {test.isLoading ? __("Testing…", "betterdocs") : __("Test connection", "betterdocs")}
181 </button>
182 </div>
183 </div>
184
185 <div className="bda-card-body">
186 {result && <div className={`bd-mcp-alert is-${testVariant(result)}`}>{result.message}</div>}
187
188 {Array.isArray(result?.caveats) &&
189 result.caveats.map((caveat) => (
190 <div key={caveat.slice(0, 60)} className="bd-mcp-alert is-warning">
191 {caveat}
192 </div>
193 ))}
194
195 {Array.isArray(result?.checks) && result.checks.length > 0 && (
196 <ul className="bd-mcp-checks">
197 {result.checks.map((check) => (
198 <li key={check.id} className="bd-mcp-checks__item">
199 <span
200 aria-hidden="true"
201 className={check.ok ? "bd-mcp-checks__ok" : "bd-mcp-checks__warn"}
202 >
203 {check.ok ? "✓" : "!"}
204 </span>
205 <span>
206 <strong>{check.label}</strong>
207 {" — "}
208 {check.detail}
209 </span>
210 </li>
211 ))}
212 </ul>
213 )}
214
215 <div className="bd-mcp-health__list">
216 <Row
217 label={__("Secure connection", "betterdocs")}
218 tone={urlsOk ? "green" : "amber"}
219 badge={urlsOk ? __("OK", "betterdocs") : __("Check", "betterdocs")}
220 detail={urlsDetail}
221 />
222 <Row
223 label={__("Abilities API", "betterdocs")}
224 tone={ownerTone(owner)}
225 badge={OWNER_LABELS[owner] || owner}
226 detail={describeOwner(owner, runtime?.abilities_api?.version)}
227 />
228 <Row
229 label={__("Registered tools", "betterdocs")}
230 tone={toolCount > 0 ? "green" : "amber"}
231 badge={String(toolCount)}
232 detail={
233 toolCount > 0
234 ? sprintf(
235 /* translators: %d: number of registered BetterDocs tools. */
236 _n(
237 "%d BetterDocs tool is registered and offered to connected clients.",
238 "%d BetterDocs tools are registered and offered to connected clients.",
239 toolCount,
240 "betterdocs",
241 ),
242 toolCount,
243 )
244 : __("No BetterDocs tools are registered, so a client would connect and find nothing to call.", "betterdocs")
245 }
246 />
247 <Row
248 label={__("Your capabilities", "betterdocs")}
249 tone={missing.length === 0 ? "green" : "amber"}
250 badge={
251 missing.length === 0
252 ? __("Complete", "betterdocs")
253 : sprintf(
254 /* translators: %d: number of missing capabilities. */
255 __("%d missing", "betterdocs"),
256 missing.length,
257 )
258 }
259 detail={
260 missing.length === 0
261 ? __("You hold every BetterDocs capability, so no tool will refuse you.", "betterdocs")
262 : sprintf(
263 /* translators: %s: comma-separated capability names. */
264 __("Tools needing these will refuse you: %s.", "betterdocs"),
265 missing.join(", "),
266 )
267 }
268 />
269 <Row
270 label={__("Database", "betterdocs")}
271 tone={dbOk ? "green" : "amber"}
272 badge={dbOk ? __("Up to date", "betterdocs") : __("Update due", "betterdocs")}
273 detail={
274 dbOk
275 ? sprintf(
276 /* translators: %s: database schema version. */
277 __("Schema v%s installed.", "betterdocs"),
278 String(database?.installed_version || "?"),
279 )
280 : __("A database update is pending. Reload the admin so it can run.", "betterdocs")
281 }
282 />
283 <Row
284 label={__("BetterDocs", "betterdocs")}
285 tone="slate"
286 badge={String(plugin?.free_version || "")}
287 detail={proLabels[proState] || proState}
288 />
289 {typeof mcp?.lockouts === "number" && mcp.lockouts > 0 && (
290 <Row
291 label={__("Client lockouts", "betterdocs")}
292 tone="amber"
293 badge={String(mcp.lockouts)}
294 detail={__(
295 "One or more client addresses are locked out after repeated failed authentications — usually a client still holding a rotated-away token. While a lockout holds, that address is refused even with a valid token. It clears itself within 15 minutes of the retries stopping.",
296 "betterdocs",
297 )}
298 />
299 )}
300 </div>
301 </div>
302 </div>
303 );
304 }
305