jmri_server.topic_help

Human help for each JSON API topic.

Rendered as an accordion under the "Topics available on the JSON API" card on the home tab. Each TopicHelp entry gets one badge on the home page that, when clicked, expands to show the summary, a methods table, and an example round-trip.

Content policy — every method entry needs at minimum a one-sentence description and either an example request or a note saying no data is required. Keep examples short: they're for "remind me what the shape is," not full protocol docs. Full protocol docs live in the generated pdoc reference under /docs/.

Adding a new service:

  • Register a new TopicHelp in TOPIC_HELP.
  • If you can't fill in an example immediately, use example_request=None — the UI hides the block cleanly.
  • The source field is a short pointer for developers reading the reference; usually jmri_server.services.<module>.<Class>.
  1"""Human help for each JSON API topic.
  2
  3Rendered as an accordion under the "Topics available on the JSON API"
  4card on the home tab. Each :class:`TopicHelp` entry gets one badge on
  5the home page that, when clicked, expands to show the summary, a
  6methods table, and an example round-trip.
  7
  8**Content policy** — every method entry needs at minimum a
  9one-sentence description and either an example request or a note
 10saying no data is required. Keep examples short: they're for
 11"remind me what the shape is," not full protocol docs. Full protocol
 12docs live in the generated pdoc reference under ``/docs/``.
 13
 14Adding a new service:
 15
 16* Register a new :class:`TopicHelp` in :data:`TOPIC_HELP`.
 17* If you can't fill in an example immediately, use
 18  ``example_request=None`` — the UI hides the block cleanly.
 19* The ``source`` field is a short pointer for developers reading the
 20  reference; usually ``jmri_server.services.<module>.<Class>``.
 21"""
 22from __future__ import annotations
 23
 24from dataclasses import dataclass, field
 25
 26
 27@dataclass(frozen=True, slots=True)
 28class MethodHelp:
 29    """One method (``list`` / ``get`` / ``put`` / ``post`` / ``delete``)."""
 30
 31    name: str
 32    description: str
 33    example_request: str | None = None
 34    example_response: str | None = None
 35
 36
 37@dataclass(frozen=True, slots=True)
 38class TopicHelp:
 39    """Everything the accordion needs to render one topic's help panel."""
 40
 41    topic: str
 42    summary: str
 43    methods: list[MethodHelp] = field(default_factory=list)
 44    # Short "read the code" pointer — module.ClassName, no leading dots.
 45    source: str = ""
 46    # Any topic that isn't in JMRI Java's original API gets ``True`` so
 47    # the UI can badge it as rewrite-specific. Keeps client-side JMRI
 48    # tooling from expecting it against a legacy server.
 49    rewrite_extension: bool = False
 50
 51
 52# ---- shared examples ---------------------------------------------------
 53
 54# Every bean list follows this shape, so keep one canonical example
 55# to avoid drift across the eight bean services.
 56_BEAN_LIST_EXAMPLE = '{"type": "turnout", "method": "list"}'
 57_BEAN_LIST_RESPONSE = (
 58    '{"type": "list", "data": [\n'
 59    '  {"name": "IT1", "userName": "Main", "state": 2, "inverted": false}\n'
 60    "]}"
 61)
 62_BEAN_GET_EXAMPLE = (
 63    '{"type": "turnout", "method": "get", "data": {"name": "IT1"}}'
 64)
 65_BEAN_POST_EXAMPLE = (
 66    '{"type": "turnout", "method": "post",\n'
 67    ' "data": {"name": "IT1", "state": 2}}'
 68)
 69
 70
 71def _bean_methods(kind: str) -> list[MethodHelp]:
 72    """The standard four-method surface every bean service exposes."""
 73    return [
 74        MethodHelp(
 75            "list", f"Every registered {kind} with its current state.",
 76            example_request=f'{{"type": "{kind}", "method": "list"}}',
 77            example_response=_BEAN_LIST_RESPONSE.replace("turnout", kind),
 78        ),
 79        MethodHelp(
 80            "get", f"One {kind}'s current state by system name.",
 81            example_request=_BEAN_GET_EXAMPLE.replace("turnout", kind),
 82        ),
 83        MethodHelp(
 84            "post", f"Change one {kind}'s state (throw/close, active/inactive).",
 85            example_request=_BEAN_POST_EXAMPLE.replace("turnout", kind),
 86        ),
 87        MethodHelp(
 88            "put", f"Create or update a {kind}. Idempotent — a repeated "
 89            "put with the same fields is a no-op.",
 90        ),
 91    ]
 92
 93
 94# ---- topic help registry ----------------------------------------------
 95
 96
 97TOPIC_HELP: dict[str, TopicHelp] = {
 98    # ---- meta ---------------------------------------------------------
 99    "hello": TopicHelp(
100        topic="hello",
101        summary=(
102            "WebSocket handshake response. Emitted by the server as soon "
103            "as a client connects; carries the server's clock heartbeat "
104            "interval so a client can tell when to expect keep-alives."
105        ),
106        source="jmri_server.services.meta.HelloService",
107    ),
108    "ping": TopicHelp(
109        topic="ping",
110        summary=(
111            "Liveness probe. Send ``{\"type\": \"ping\"}`` and get "
112            "``{\"type\": \"pong\"}`` back — useful to keep an idle "
113            "WebSocket connection alive through a stateful firewall."
114        ),
115        methods=[
116            MethodHelp(
117                "get", "Round-trip ping — returns pong.",
118                example_request='{"type": "ping"}',
119                example_response='{"type": "pong", "data": {}}',
120            ),
121        ],
122        source="jmri_server.services.meta.PingService",
123    ),
124    "version": TopicHelp(
125        topic="version",
126        summary=(
127            "Server version string. Matches the JMRI Java ``version`` "
128            "topic so a legacy JMRI client can gate features by server "
129            "version. Ours reports the ``jmri-server`` package version."
130        ),
131        methods=[
132            MethodHelp(
133                "get", "Report the running server's version.",
134                example_request='{"type": "version"}',
135                example_response='{"type": "version", "data": {"version": "0.1.0b3"}}',
136            ),
137        ],
138        source="jmri_server.services.meta.VersionService",
139    ),
140    "railroad": TopicHelp(
141        topic="railroad",
142        summary=(
143            "Layout metadata — the railroad's user-set name from the "
144            "active profile. Read-only in the current build; JMRI Java "
145            "allows editing here but we defer to profile-edit for now."
146        ),
147        methods=[
148            MethodHelp(
149                "get", "Fetch the railroad name.",
150                example_request='{"type": "railroad"}',
151            ),
152        ],
153        source="jmri_server.services.meta.RailroadService",
154    ),
155    "node": TopicHelp(
156        topic="node",
157        summary=(
158            "This server's identity as a networking peer — hostname, "
159            "profile id, uuid. Consumed by external tools that need to "
160            "distinguish multiple JMRI instances on one LAN."
161        ),
162        methods=[
163            MethodHelp("get", "Report node identity.",
164                       example_request='{"type": "node"}'),
165        ],
166        source="jmri_server.services.meta.NodeService",
167    ),
168    "power": TopicHelp(
169        topic="power",
170        summary=(
171            "Track power state (ON / OFF / UNKNOWN). Wired through the "
172            "active connection's ``PowerManager``. For OpenLCB, "
173            "``set`` emits the well-known power events; for LocoNet / "
174            "NCE / DCC-EX, it sends the driver-specific power packet."
175        ),
176        methods=[
177            MethodHelp(
178                "get", "Current power state.",
179                example_request='{"type": "power"}',
180                example_response=(
181                    '{"type": "power", "data": {"state": 2}}\n'
182                    "// state: 2 = ON, 4 = OFF, 0 = UNKNOWN (JMRI convention)"
183                ),
184            ),
185            MethodHelp(
186                "post", "Set power on or off. State ``2`` = ON, ``4`` = OFF.",
187                example_request='{"type": "power", "method": "post", "data": {"state": 2}}',
188            ),
189        ],
190        source="jmri_server.services.meta.PowerService",
191    ),
192
193    # ---- beans --------------------------------------------------------
194    "turnout": TopicHelp(
195        topic="turnout",
196        summary=(
197            "Turnout points (switch machines). State: 2 = closed, "
198            "4 = thrown. Post the target state to command a change; "
199            "listeners on the WebSocket get pushes on every observed "
200            "transition, including hardware-initiated ones."
201        ),
202        methods=_bean_methods("turnout"),
203        source="jmri_server.services.beans.TurnoutService",
204    ),
205    "sensor": TopicHelp(
206        topic="sensor",
207        summary=(
208            "Track occupancy / block detection sensors. State: 2 = "
209            "active (occupied), 4 = inactive. Typically read-only from "
210            "hardware; the post method is available for testing or "
211            "simulated layouts."
212        ),
213        methods=_bean_methods("sensor"),
214        source="jmri_server.services.beans.SensorService",
215    ),
216    "light": TopicHelp(
217        topic="light",
218        summary=(
219            "Layout lights (buildings, streetlights, tower lamps). "
220            "Modeled as two-state beans on most drivers; some support "
221            "variable intensity via CV writes."
222        ),
223        methods=_bean_methods("light"),
224        source="jmri_server.services.beans.LightService",
225    ),
226    "signalHead": TopicHelp(
227        topic="signalHead",
228        summary=(
229            "Individual signal heads — single-aspect indicators driven "
230            "by a bank of outputs. Composed into signal masts by the "
231            "``signalMast`` service for multi-aspect signalling."
232        ),
233        methods=_bean_methods("signalHead"),
234        source="jmri_server.services.beans.SignalHeadService",
235    ),
236    "signalMast": TopicHelp(
237        topic="signalMast",
238        summary=(
239            "Signal masts — multi-aspect signals composed of one or "
240            "more heads with a named signalling system (e.g. AAR-1946, "
241            "NORAC). State is an aspect string like ``\"Clear\"`` or "
242            "``\"Approach Medium\"``."
243        ),
244        methods=_bean_methods("signalMast"),
245        source="jmri_server.services.beans.SignalMastService",
246    ),
247    "memory": TopicHelp(
248        topic="memory",
249        summary=(
250            "Memory variables — free-form named values used by LogixNG "
251            "logic and panel scripts. Not persistent by default; "
252            "declaring a Memory in PanelPro.xml makes it stick."
253        ),
254        methods=_bean_methods("memory"),
255        source="jmri_server.services.beans.MemoryService",
256    ),
257    "reporter": TopicHelp(
258        topic="reporter",
259        summary=(
260            "Reporters — beans that carry a last-seen value (typically "
261            "a loco address from a Digitrax transponding zone or an "
262            "RFID reader). Different from sensors because they carry "
263            "structured payloads, not just on/off state."
264        ),
265        methods=_bean_methods("reporter"),
266        source="jmri_server.services.beans.ReporterService",
267    ),
268    "block": TopicHelp(
269        topic="block",
270        summary=(
271            "Blocks — segments of track with associated occupancy and "
272            "the loco address currently in them. Feeds routing / "
273            "signalling / dispatcher logic."
274        ),
275        methods=_bean_methods("block"),
276        source="jmri_server.services.beans.BlockService",
277    ),
278    "route": TopicHelp(
279        topic="route",
280        summary=(
281            "Routes — named collections of turnout positions activated "
282            "together (e.g. ``\"Yard Lead Left\"`` throws the ladder "
283            "into a specific yard track). Static; for conditional "
284            "routing use LogixNG."
285        ),
286        methods=_bean_methods("route"),
287        source="jmri_server.services.beans.RouteService",
288    ),
289    "audio": TopicHelp(
290        topic="audio",
291        summary=(
292            "Layout audio sources — sound files triggered by scripts "
293            "or LogixNG. State controls playback (play, stop, loop)."
294        ),
295        methods=_bean_methods("audio"),
296        source="jmri_server.services.audio.AudioService",
297    ),
298    "idTag": TopicHelp(
299        topic="idTag",
300        summary=(
301            "ID tags — the values a Reporter sees. Modeling RFID / "
302            "transponder tokens so LogixNG can match against the "
303            "*identity* rather than the reporter that saw it."
304        ),
305        methods=_bean_methods("idTag"),
306        source="jmri_server.services.id_tag.IdTagService",
307    ),
308    "layoutBlock": TopicHelp(
309        topic="layoutBlock",
310        summary=(
311            "Layout blocks — a routing-graph abstraction over one or "
312            "more ``block`` beans. Adds physical connectivity used by "
313            "the automatic dispatcher and by SSL signalling."
314        ),
315        methods=_bean_methods("layoutBlock"),
316        source="jmri_server.services.layout_block.LayoutBlockService",
317    ),
318    "oblock": TopicHelp(
319        topic="oblock",
320        summary=(
321            "OBlocks (\"occupancy blocks\") — the Warrant system's "
322            "richer block model, with per-block path definitions and "
323            "signalling state. Used by Warrants and the automatic "
324            "dispatcher."
325        ),
326        methods=_bean_methods("oblock"),
327        source="jmri_server.services.oblock.OBlockService",
328    ),
329
330    # ---- programming + throttles --------------------------------------
331    "throttle": TopicHelp(
332        topic="throttle",
333        summary=(
334            "Loco control — acquire an address, set speed, set "
335            "functions, release. One throttle per DCC address on the "
336            "active connection."
337        ),
338        methods=[
339            MethodHelp(
340                "put", "Acquire a throttle for a DCC address.",
341                example_request=(
342                    '{"type": "throttle", "method": "put",\n'
343                    ' "data": {"address": 3, "long_address": false}}'
344                ),
345            ),
346            MethodHelp(
347                "post", "Update speed / direction / function state.",
348                example_request=(
349                    '{"type": "throttle", "method": "post",\n'
350                    ' "data": {"address": 3, "speed": 0.5, '
351                    '"direction": "forward", "functions": {"0": true}}}'
352                ),
353            ),
354            MethodHelp(
355                "delete", "Release the throttle (clean release, keeps "
356                "the loco rolling at its last commanded speed until "
357                "the CS purge timeout).",
358                example_request=(
359                    '{"type": "throttle", "method": "delete",\n'
360                    ' "data": {"address": 3}}'
361                ),
362            ),
363        ],
364        source="jmri_server.services.throttle.ThrottleService",
365    ),
366    "programmer": TopicHelp(
367        topic="programmer",
368        summary=(
369            "CV read / write on the programming track (service mode) "
370            "or on a running loco (ops mode). Ops-mode reads require "
371            "RailCom on the CS + decoder. Requests block for 1–3s per "
372            "CV on service mode."
373        ),
374        methods=[
375            MethodHelp(
376                "get", "Read one CV.",
377                example_request=(
378                    '{"type": "programmer", "method": "get",\n'
379                    ' "data": {"cv": 29, "mode": "service"}}'
380                ),
381            ),
382            MethodHelp(
383                "post", "Write one CV.",
384                example_request=(
385                    '{"type": "programmer", "method": "post",\n'
386                    ' "data": {"cv": 29, "value": 6, "mode": "service"}}'
387                ),
388            ),
389        ],
390        source="jmri_server.services.programmer.ProgrammerService",
391    ),
392    "roster": TopicHelp(
393        topic="roster",
394        summary=(
395            "Roster entries — the profile's loco list, each with a "
396            "decoder family/model/productID and per-loco function "
397            "labels. Backed by ``<profile>/roster.xml``."
398        ),
399        methods=[
400            MethodHelp(
401                "list", "Every roster entry in the active profile.",
402                example_request='{"type": "roster", "method": "list"}',
403            ),
404            MethodHelp(
405                "get", "One entry by roster ID.",
406                example_request=(
407                    '{"type": "roster", "method": "get", "data": {"id": "3601"}}'
408                ),
409            ),
410            MethodHelp(
411                "put", "Create or update a roster entry.",
412            ),
413            MethodHelp(
414                "delete", "Remove a roster entry from the profile.",
415            ),
416        ],
417        source="jmri_server.services.roster.RosterService",
418    ),
419
420    # ---- automation ---------------------------------------------------
421    "consist": TopicHelp(
422        topic="consist",
423        summary=(
424            "Loco consists (multi-unit lash-ups). Add / remove locos, "
425            "set direction-relative-to-consist, kill the consist. "
426            "Backed by the active connection's ``ConsistManager``."
427        ),
428        methods=[
429            MethodHelp(
430                "list", "Every registered consist.",
431                example_request='{"type": "consist", "method": "list"}',
432            ),
433            MethodHelp(
434                "put", "Create a new consist at a given address.",
435                example_request=(
436                    '{"type": "consist", "method": "put",\n'
437                    ' "data": {"address": 20}}'
438                ),
439            ),
440            MethodHelp(
441                "post", "Add or remove a member, set direction.",
442                example_request=(
443                    '{"type": "consist", "method": "post", "data":\n'
444                    ' {"address": 20, "action": "add", "loco": 3601,\n'
445                    '  "direction_normal": true}}'
446                ),
447            ),
448            MethodHelp(
449                "delete", "Kill the consist.",
450                example_request=(
451                    '{"type": "consist", "method": "delete",\n'
452                    ' "data": {"address": 20}}'
453                ),
454            ),
455        ],
456        source="jmri_server.services.consist.ConsistService",
457    ),
458    "logixng": TopicHelp(
459        topic="logixng",
460        summary=(
461            "LogixNG programs — declarative automation trees. Each "
462            "program has a trigger (turnout thrown, sensor active, "
463            "clock time, …) and an action tree (set turnout, run "
464            "route, invoke a script). Stored per-profile as YAML."
465        ),
466        methods=[
467            MethodHelp(
468                "list", "Every loaded LogixNG program.",
469                example_request='{"type": "logixng", "method": "list"}',
470            ),
471            MethodHelp(
472                "get", "One program's structure by system name.",
473            ),
474            MethodHelp(
475                "post", "Enable / disable a program at runtime.",
476            ),
477        ],
478        source="jmri_server.services.logixng.LogixNGService",
479    ),
480    "signalSystem": TopicHelp(
481        topic="signalSystem",
482        summary=(
483            "Loaded signal systems (AAR-1946, NORAC, custom). Read-"
484            "only; systems are defined by XML files under the profile "
485            "and enumerated on load."
486        ),
487        methods=[
488            MethodHelp(
489                "list", "Every loaded signal system with its aspects.",
490                example_request='{"type": "signalSystem", "method": "list"}',
491            ),
492            MethodHelp("get", "One signal system by name."),
493        ],
494        source="jmri_server.services.signal_system.SignalSystemService",
495    ),
496
497    # ---- infra --------------------------------------------------------
498    "time": TopicHelp(
499        topic="time",
500        summary=(
501            "Fast clock — layout time separate from wall-clock time. "
502            "Rate is a multiplier; setting rate to 0 pauses it. "
503            "``time`` is an ISO-8601 timestamp of the current fast-"
504            "clock reading."
505        ),
506        methods=[
507            MethodHelp(
508                "get", "Current fast-clock reading + rate.",
509                example_request='{"type": "time"}',
510            ),
511            MethodHelp(
512                "post", "Set the time or the rate.",
513                example_request=(
514                    '{"type": "time", "method": "post",\n'
515                    ' "data": {"rate": 4.0, "run": true}}'
516                ),
517            ),
518        ],
519        source="jmri_server.services.fast_clock.TimeService",
520    ),
521    "panel": TopicHelp(
522        topic="panel",
523        summary=(
524            "Panel definitions — the operator's on-screen layout "
525            "diagram. Currently exposes the list of loaded panels; the "
526            "full panel-render pipeline is a future item."
527        ),
528        methods=[
529            MethodHelp(
530                "list", "Every loaded panel in the profile.",
531                example_request='{"type": "panel", "method": "list"}',
532            ),
533        ],
534        source="jmri_server.services.panel.PanelService",
535    ),
536    "metadata": TopicHelp(
537        topic="metadata",
538        summary=(
539            "Server metadata — JMRI version, copyright, activation "
540            "profile, Java version (N/A in our Python build), OS. "
541            "Consumed by legacy JMRI clients for feature detection."
542        ),
543        methods=[
544            MethodHelp("list", "Every metadata field."),
545            MethodHelp(
546                "get", "One metadata value by name.",
547                example_request=(
548                    '{"type": "metadata", "method": "get",\n'
549                    ' "data": {"name": "JMRIVERCANON"}}'
550                ),
551            ),
552        ],
553        source="jmri_server.services.util.MetadataService",
554    ),
555    "networkService": TopicHelp(
556        topic="networkService",
557        summary=(
558            "Advertised network services — WiThrottle TCP port, "
559            "WebSocket URL, HTTP root. Legacy JMRI clients enumerate "
560            "these to auto-configure."
561        ),
562        methods=[
563            MethodHelp("list", "Every advertised network service."),
564        ],
565        source="jmri_server.services.util.NetworkServiceService",
566    ),
567    "systemConnection": TopicHelp(
568        topic="systemConnection",
569        summary=(
570            "The active hardware connection (NCE / LocoNet / DCC-EX / "
571            "OpenLCB / internal). Read-only enumeration; connection "
572            "config lives in ``ConnectionConfig.xml`` under the "
573            "profile."
574        ),
575        methods=[
576            MethodHelp("list", "Every configured system connection."),
577        ],
578        source="jmri_server.services.util.SystemConnectionService",
579    ),
580
581    # ---- rewrite-specific ---------------------------------------------
582    "logs": TopicHelp(
583        topic="logs",
584        summary=(
585            "Server log tail — the last N lines from the running "
586            "jmri.log. Used by the home-tab log strip and the /logs "
587            "diagnostics page."
588        ),
589        methods=[
590            MethodHelp(
591                "list", "Last N lines from jmri.log.",
592                example_request='{"type": "logs", "method": "list", "data": {"tail": 50}}',
593            ),
594        ],
595        source="jmri_server.services.logs.LogsService",
596        rewrite_extension=True,
597    ),
598    "configUser": TopicHelp(
599        topic="configUser",
600        summary=(
601            "User management + self-service password/token change. "
602            "Admin-only for list/get/put/delete; the ``change_password`` "
603            "and ``regenerate_token`` post actions are self-service for "
604            "any authenticated user (admin can act on anyone). Never "
605            "returns the password hash — only whether one exists."
606        ),
607        methods=[
608            MethodHelp("list", "Every registered user with role + has-token."),
609            MethodHelp(
610                "put", "Create or update a user.",
611                example_request=(
612                    '{"type": "configUser", "method": "put", "data": {\n'
613                    '  "username": "alice",\n'
614                    '  "password": "secret",\n'
615                    '  "role": "operator"\n'
616                    "}}"
617                ),
618            ),
619            MethodHelp(
620                "delete", "Remove a user. Refuses self-delete + last-admin.",
621            ),
622            MethodHelp(
623                "post",
624                "action=change_password → change your own or (as admin) "
625                "someone else's password. action=regenerate_token → mint "
626                "a new API token; the old one stops working immediately.",
627                example_request=(
628                    '{"type": "configUser", "method": "post", "data": {\n'
629                    '  "action": "regenerate_token",\n'
630                    '  "username": "alice"\n'
631                    "}}"
632                ),
633            ),
634        ],
635        source="jmri_server.services.config_user.ConfigUserService",
636        rewrite_extension=True,
637    ),
638    "configProfile": TopicHelp(
639        topic="configProfile",
640        summary=(
641            "Create / delete / list JMRI profiles under the settings "
642            "root. Delete only unregisters the profile from "
643            "``profiles.xml`` — the on-disk directory is left in "
644            "place so an accidental UI click can't destroy layout "
645            "data."
646        ),
647        methods=[
648            MethodHelp("list", "Every registered profile."),
649            MethodHelp(
650                "put", "Create a new profile.",
651                example_request=(
652                    '{"type": "configProfile", "method": "put",\n'
653                    ' "data": {"name": "production"}}'
654                ),
655            ),
656            MethodHelp(
657                "delete", "Unregister a profile (dir left on disk).",
658                example_request=(
659                    '{"type": "configProfile", "method": "delete",\n'
660                    ' "data": {"id": "production.abcd1234"}}'
661                ),
662            ),
663        ],
664        source="jmri_server.services.config_profile.ConfigProfileService",
665        rewrite_extension=True,
666    ),
667    "beanTransfer": TopicHelp(
668        topic="beanTransfer",
669        summary=(
670            "Copy beans (turnouts / sensors / lights) between "
671            "profiles. Preview shows what would be added; apply "
672            "commits the copy. When the target profile is the active "
673            "one, live managers refresh so the change is visible "
674            "immediately without a profile reload."
675        ),
676        methods=[
677            MethodHelp("list", "Every profile available as source/target."),
678            MethodHelp(
679                "post",
680                "Preview or apply a transfer. ``action=preview`` returns "
681                "the diff; ``action=apply`` commits it.",
682                example_request=(
683                    '{"type": "beanTransfer", "method": "post", "data":\n'
684                    ' {"action": "preview", "source": "dev.abcd",\n'
685                    '  "target": "prod.efgh", "kinds": ["turnout"]}}'
686                ),
687            ),
688        ],
689        source="jmri_server.services.bean_transfer.BeanTransferService",
690        rewrite_extension=True,
691    ),
692    "scripts": TopicHelp(
693        topic="scripts",
694        summary=(
695            "Python scripts loaded from ``<profile>/python/`` and "
696            "``<settings>/python/``. List, save, start, stop, delete. "
697            "Shared scripts (settings-root) are operator-managed and "
698            "can't be deleted through the API."
699        ),
700        methods=[
701            MethodHelp(
702                "list", "Running scripts, or with data={\"library\": true}, "
703                "the whole on-disk library.",
704                example_request='{"type": "scripts", "method": "list"}',
705            ),
706            MethodHelp(
707                "get", "One script's status by name.",
708                example_request=(
709                    '{"type": "scripts", "method": "get",\n'
710                    ' "data": {"name": "morning_setup"}}'
711                ),
712            ),
713            MethodHelp(
714                "put", "Start a script from inline source. "
715                "``save=true`` also writes it to the profile's "
716                "``python/`` dir so it autoloads next boot.",
717                example_request=(
718                    '{"type": "scripts", "method": "put", "data":\n'
719                    ' {"name": "hello", "source": "print(\\"hi\\")",\n'
720                    '  "save": true}}'
721                ),
722            ),
723            MethodHelp(
724                "post",
725                "action=stop → stop a running script; "
726                "action=delete → also remove the on-disk file.",
727            ),
728        ],
729        source="jmri_server.services.scripts.ScriptsService",
730        rewrite_extension=True,
731    ),
732}
733
734
735def get(topic: str) -> TopicHelp | None:
736    """Return the help entry for *topic*, or None if none is registered.
737
738    The web layer falls back to a "no help authored yet" panel when
739    this returns None — the topic still renders as a badge, just
740    without content. That way a new service can land without the
741    help wire-up blocking it.
742    """
743    return TOPIC_HELP.get(topic)
@dataclass(frozen=True, slots=True)
class MethodHelp:
28@dataclass(frozen=True, slots=True)
29class MethodHelp:
30    """One method (``list`` / ``get`` / ``put`` / ``post`` / ``delete``)."""
31
32    name: str
33    description: str
34    example_request: str | None = None
35    example_response: str | None = None

One method (list / get / put / post / delete).

MethodHelp( name: str, description: str, example_request: str | None = None, example_response: str | None = None)
name: str
description: str
example_request: str | None
example_response: str | None
@dataclass(frozen=True, slots=True)
class TopicHelp:
38@dataclass(frozen=True, slots=True)
39class TopicHelp:
40    """Everything the accordion needs to render one topic's help panel."""
41
42    topic: str
43    summary: str
44    methods: list[MethodHelp] = field(default_factory=list)
45    # Short "read the code" pointer — module.ClassName, no leading dots.
46    source: str = ""
47    # Any topic that isn't in JMRI Java's original API gets ``True`` so
48    # the UI can badge it as rewrite-specific. Keeps client-side JMRI
49    # tooling from expecting it against a legacy server.
50    rewrite_extension: bool = False

Everything the accordion needs to render one topic's help panel.

TopicHelp( topic: str, summary: str, methods: list[MethodHelp] = <factory>, source: str = '', rewrite_extension: bool = False)
topic: str
summary: str
methods: list[MethodHelp]
source: str
rewrite_extension: bool
TOPIC_HELP: dict[str, TopicHelp] = {'hello': TopicHelp(topic='hello', summary="WebSocket handshake response. Emitted by the server as soon as a client connects; carries the server's clock heartbeat interval so a client can tell when to expect keep-alives.", methods=[], source='jmri_server.services.HelloService', rewrite_extension=False), 'ping': TopicHelp(topic='ping', summary='Liveness probe. Send ``{"type": "ping"}`` and get ``{"type": "pong"}`` back — useful to keep an idle WebSocket connection alive through a stateful firewall.', methods=[MethodHelp(name='get', description='Round-trip ping — returns pong.', example_request='{"type": "ping"}', example_response='{"type": "pong", "data": {}}')], source='jmri_server.services.PingService', rewrite_extension=False), 'version': TopicHelp(topic='version', summary='Server version string. Matches the JMRI Java ``version`` topic so a legacy JMRI client can gate features by server version. Ours reports the ``jmri-server`` package version.', methods=[MethodHelp(name='get', description="Report the running server's version.", example_request='{"type": "version"}', example_response='{"type": "version", "data": {"version": "0.1.0b3"}}')], source='jmri_server.services.VersionService', rewrite_extension=False), 'railroad': TopicHelp(topic='railroad', summary="Layout metadata — the railroad's user-set name from the active profile. Read-only in the current build; JMRI Java allows editing here but we defer to profile-edit for now.", methods=[MethodHelp(name='get', description='Fetch the railroad name.', example_request='{"type": "railroad"}', example_response=None)], source='jmri_server.services.RailroadService', rewrite_extension=False), 'node': TopicHelp(topic='node', summary="This server's identity as a networking peer — hostname, profile id, uuid. Consumed by external tools that need to distinguish multiple JMRI instances on one LAN.", methods=[MethodHelp(name='get', description='Report node identity.', example_request='{"type": "node"}', example_response=None)], source='jmri_server.services.NodeService', rewrite_extension=False), 'power': TopicHelp(topic='power', summary="Track power state (ON / OFF / UNKNOWN). Wired through the active connection's ``PowerManager``. For OpenLCB, ``set`` emits the well-known power events; for LocoNet / NCE / DCC-EX, it sends the driver-specific power packet.", methods=[MethodHelp(name='get', description='Current power state.', example_request='{"type": "power"}', example_response='{"type": "power", "data": {"state": 2}}\n// state: 2 = ON, 4 = OFF, 0 = UNKNOWN (JMRI convention)'), MethodHelp(name='post', description='Set power on or off. State ``2`` = ON, ``4`` = OFF.', example_request='{"type": "power", "method": "post", "data": {"state": 2}}', example_response=None)], source='jmri_server.services.PowerService', rewrite_extension=False), 'turnout': TopicHelp(topic='turnout', summary='Turnout points (switch machines). State: 2 = closed, 4 = thrown. Post the target state to command a change; listeners on the WebSocket get pushes on every observed transition, including hardware-initiated ones.', methods=[MethodHelp(name='list', description='Every registered turnout with its current state.', example_request='{"type": "turnout", "method": "list"}', example_response='{"type": "list", "data": [\n {"name": "IT1", "userName": "Main", "state": 2, "inverted": false}\n]}'), MethodHelp(name='get', description="One turnout's current state by system name.", example_request='{"type": "turnout", "method": "get", "data": {"name": "IT1"}}', example_response=None), MethodHelp(name='post', description="Change one turnout's state (throw/close, active/inactive).", example_request='{"type": "turnout", "method": "post",\n "data": {"name": "IT1", "state": 2}}', example_response=None), MethodHelp(name='put', description='Create or update a turnout. Idempotent — a repeated put with the same fields is a no-op.', example_request=None, example_response=None)], source='jmri_server.services.TurnoutService', rewrite_extension=False), 'sensor': TopicHelp(topic='sensor', summary='Track occupancy / block detection sensors. State: 2 = active (occupied), 4 = inactive. Typically read-only from hardware; the post method is available for testing or simulated layouts.', methods=[MethodHelp(name='list', description='Every registered sensor with its current state.', example_request='{"type": "sensor", "method": "list"}', example_response='{"type": "list", "data": [\n {"name": "IT1", "userName": "Main", "state": 2, "inverted": false}\n]}'), MethodHelp(name='get', description="One sensor's current state by system name.", example_request='{"type": "sensor", "method": "get", "data": {"name": "IT1"}}', example_response=None), MethodHelp(name='post', description="Change one sensor's state (throw/close, active/inactive).", example_request='{"type": "sensor", "method": "post",\n "data": {"name": "IT1", "state": 2}}', example_response=None), MethodHelp(name='put', description='Create or update a sensor. Idempotent — a repeated put with the same fields is a no-op.', example_request=None, example_response=None)], source='jmri_server.services.SensorService', rewrite_extension=False), 'light': TopicHelp(topic='light', summary='Layout lights (buildings, streetlights, tower lamps). Modeled as two-state beans on most drivers; some support variable intensity via CV writes.', methods=[MethodHelp(name='list', description='Every registered light with its current state.', example_request='{"type": "light", "method": "list"}', example_response='{"type": "list", "data": [\n {"name": "IT1", "userName": "Main", "state": 2, "inverted": false}\n]}'), MethodHelp(name='get', description="One light's current state by system name.", example_request='{"type": "light", "method": "get", "data": {"name": "IT1"}}', example_response=None), MethodHelp(name='post', description="Change one light's state (throw/close, active/inactive).", example_request='{"type": "light", "method": "post",\n "data": {"name": "IT1", "state": 2}}', example_response=None), MethodHelp(name='put', description='Create or update a light. Idempotent — a repeated put with the same fields is a no-op.', example_request=None, example_response=None)], source='jmri_server.services.LightService', rewrite_extension=False), 'signalHead': TopicHelp(topic='signalHead', summary='Individual signal heads — single-aspect indicators driven by a bank of outputs. Composed into signal masts by the ``signalMast`` service for multi-aspect signalling.', methods=[MethodHelp(name='list', description='Every registered signalHead with its current state.', example_request='{"type": "signalHead", "method": "list"}', example_response='{"type": "list", "data": [\n {"name": "IT1", "userName": "Main", "state": 2, "inverted": false}\n]}'), MethodHelp(name='get', description="One signalHead's current state by system name.", example_request='{"type": "signalHead", "method": "get", "data": {"name": "IT1"}}', example_response=None), MethodHelp(name='post', description="Change one signalHead's state (throw/close, active/inactive).", example_request='{"type": "signalHead", "method": "post",\n "data": {"name": "IT1", "state": 2}}', example_response=None), MethodHelp(name='put', description='Create or update a signalHead. Idempotent — a repeated put with the same fields is a no-op.', example_request=None, example_response=None)], source='jmri_server.services.SignalHeadService', rewrite_extension=False), 'signalMast': TopicHelp(topic='signalMast', summary='Signal masts — multi-aspect signals composed of one or more heads with a named signalling system (e.g. AAR-1946, NORAC). State is an aspect string like ``"Clear"`` or ``"Approach Medium"``.', methods=[MethodHelp(name='list', description='Every registered signalMast with its current state.', example_request='{"type": "signalMast", "method": "list"}', example_response='{"type": "list", "data": [\n {"name": "IT1", "userName": "Main", "state": 2, "inverted": false}\n]}'), MethodHelp(name='get', description="One signalMast's current state by system name.", example_request='{"type": "signalMast", "method": "get", "data": {"name": "IT1"}}', example_response=None), MethodHelp(name='post', description="Change one signalMast's state (throw/close, active/inactive).", example_request='{"type": "signalMast", "method": "post",\n "data": {"name": "IT1", "state": 2}}', example_response=None), MethodHelp(name='put', description='Create or update a signalMast. Idempotent — a repeated put with the same fields is a no-op.', example_request=None, example_response=None)], source='jmri_server.services.SignalMastService', rewrite_extension=False), 'memory': TopicHelp(topic='memory', summary='Memory variables — free-form named values used by LogixNG logic and panel scripts. Not persistent by default; declaring a Memory in PanelPro.xml makes it stick.', methods=[MethodHelp(name='list', description='Every registered memory with its current state.', example_request='{"type": "memory", "method": "list"}', example_response='{"type": "list", "data": [\n {"name": "IT1", "userName": "Main", "state": 2, "inverted": false}\n]}'), MethodHelp(name='get', description="One memory's current state by system name.", example_request='{"type": "memory", "method": "get", "data": {"name": "IT1"}}', example_response=None), MethodHelp(name='post', description="Change one memory's state (throw/close, active/inactive).", example_request='{"type": "memory", "method": "post",\n "data": {"name": "IT1", "state": 2}}', example_response=None), MethodHelp(name='put', description='Create or update a memory. Idempotent — a repeated put with the same fields is a no-op.', example_request=None, example_response=None)], source='jmri_server.services.MemoryService', rewrite_extension=False), 'reporter': TopicHelp(topic='reporter', summary='Reporters — beans that carry a last-seen value (typically a loco address from a Digitrax transponding zone or an RFID reader). Different from sensors because they carry structured payloads, not just on/off state.', methods=[MethodHelp(name='list', description='Every registered reporter with its current state.', example_request='{"type": "reporter", "method": "list"}', example_response='{"type": "list", "data": [\n {"name": "IT1", "userName": "Main", "state": 2, "inverted": false}\n]}'), MethodHelp(name='get', description="One reporter's current state by system name.", example_request='{"type": "reporter", "method": "get", "data": {"name": "IT1"}}', example_response=None), MethodHelp(name='post', description="Change one reporter's state (throw/close, active/inactive).", example_request='{"type": "reporter", "method": "post",\n "data": {"name": "IT1", "state": 2}}', example_response=None), MethodHelp(name='put', description='Create or update a reporter. Idempotent — a repeated put with the same fields is a no-op.', example_request=None, example_response=None)], source='jmri_server.services.ReporterService', rewrite_extension=False), 'block': TopicHelp(topic='block', summary='Blocks — segments of track with associated occupancy and the loco address currently in them. Feeds routing / signalling / dispatcher logic.', methods=[MethodHelp(name='list', description='Every registered block with its current state.', example_request='{"type": "block", "method": "list"}', example_response='{"type": "list", "data": [\n {"name": "IT1", "userName": "Main", "state": 2, "inverted": false}\n]}'), MethodHelp(name='get', description="One block's current state by system name.", example_request='{"type": "block", "method": "get", "data": {"name": "IT1"}}', example_response=None), MethodHelp(name='post', description="Change one block's state (throw/close, active/inactive).", example_request='{"type": "block", "method": "post",\n "data": {"name": "IT1", "state": 2}}', example_response=None), MethodHelp(name='put', description='Create or update a block. Idempotent — a repeated put with the same fields is a no-op.', example_request=None, example_response=None)], source='jmri_server.services.BlockService', rewrite_extension=False), 'route': TopicHelp(topic='route', summary='Routes — named collections of turnout positions activated together (e.g. ``"Yard Lead Left"`` throws the ladder into a specific yard track). Static; for conditional routing use LogixNG.', methods=[MethodHelp(name='list', description='Every registered route with its current state.', example_request='{"type": "route", "method": "list"}', example_response='{"type": "list", "data": [\n {"name": "IT1", "userName": "Main", "state": 2, "inverted": false}\n]}'), MethodHelp(name='get', description="One route's current state by system name.", example_request='{"type": "route", "method": "get", "data": {"name": "IT1"}}', example_response=None), MethodHelp(name='post', description="Change one route's state (throw/close, active/inactive).", example_request='{"type": "route", "method": "post",\n "data": {"name": "IT1", "state": 2}}', example_response=None), MethodHelp(name='put', description='Create or update a route. Idempotent — a repeated put with the same fields is a no-op.', example_request=None, example_response=None)], source='jmri_server.services.RouteService', rewrite_extension=False), 'audio': TopicHelp(topic='audio', summary='Layout audio sources — sound files triggered by scripts or LogixNG. State controls playback (play, stop, loop).', methods=[MethodHelp(name='list', description='Every registered audio with its current state.', example_request='{"type": "audio", "method": "list"}', example_response='{"type": "list", "data": [\n {"name": "IT1", "userName": "Main", "state": 2, "inverted": false}\n]}'), MethodHelp(name='get', description="One audio's current state by system name.", example_request='{"type": "audio", "method": "get", "data": {"name": "IT1"}}', example_response=None), MethodHelp(name='post', description="Change one audio's state (throw/close, active/inactive).", example_request='{"type": "audio", "method": "post",\n "data": {"name": "IT1", "state": 2}}', example_response=None), MethodHelp(name='put', description='Create or update a audio. Idempotent — a repeated put with the same fields is a no-op.', example_request=None, example_response=None)], source='jmri_server.services.AudioService', rewrite_extension=False), 'idTag': TopicHelp(topic='idTag', summary='ID tags — the values a Reporter sees. Modeling RFID / transponder tokens so LogixNG can match against the *identity* rather than the reporter that saw it.', methods=[MethodHelp(name='list', description='Every registered idTag with its current state.', example_request='{"type": "idTag", "method": "list"}', example_response='{"type": "list", "data": [\n {"name": "IT1", "userName": "Main", "state": 2, "inverted": false}\n]}'), MethodHelp(name='get', description="One idTag's current state by system name.", example_request='{"type": "idTag", "method": "get", "data": {"name": "IT1"}}', example_response=None), MethodHelp(name='post', description="Change one idTag's state (throw/close, active/inactive).", example_request='{"type": "idTag", "method": "post",\n "data": {"name": "IT1", "state": 2}}', example_response=None), MethodHelp(name='put', description='Create or update a idTag. Idempotent — a repeated put with the same fields is a no-op.', example_request=None, example_response=None)], source='jmri_server.services.IdTagService', rewrite_extension=False), 'layoutBlock': TopicHelp(topic='layoutBlock', summary='Layout blocks — a routing-graph abstraction over one or more ``block`` beans. Adds physical connectivity used by the automatic dispatcher and by SSL signalling.', methods=[MethodHelp(name='list', description='Every registered layoutBlock with its current state.', example_request='{"type": "layoutBlock", "method": "list"}', example_response='{"type": "list", "data": [\n {"name": "IT1", "userName": "Main", "state": 2, "inverted": false}\n]}'), MethodHelp(name='get', description="One layoutBlock's current state by system name.", example_request='{"type": "layoutBlock", "method": "get", "data": {"name": "IT1"}}', example_response=None), MethodHelp(name='post', description="Change one layoutBlock's state (throw/close, active/inactive).", example_request='{"type": "layoutBlock", "method": "post",\n "data": {"name": "IT1", "state": 2}}', example_response=None), MethodHelp(name='put', description='Create or update a layoutBlock. Idempotent — a repeated put with the same fields is a no-op.', example_request=None, example_response=None)], source='jmri_server.services.LayoutBlockService', rewrite_extension=False), 'oblock': TopicHelp(topic='oblock', summary='OBlocks ("occupancy blocks") — the Warrant system\'s richer block model, with per-block path definitions and signalling state. Used by Warrants and the automatic dispatcher.', methods=[MethodHelp(name='list', description='Every registered oblock with its current state.', example_request='{"type": "oblock", "method": "list"}', example_response='{"type": "list", "data": [\n {"name": "IT1", "userName": "Main", "state": 2, "inverted": false}\n]}'), MethodHelp(name='get', description="One oblock's current state by system name.", example_request='{"type": "oblock", "method": "get", "data": {"name": "IT1"}}', example_response=None), MethodHelp(name='post', description="Change one oblock's state (throw/close, active/inactive).", example_request='{"type": "oblock", "method": "post",\n "data": {"name": "IT1", "state": 2}}', example_response=None), MethodHelp(name='put', description='Create or update a oblock. Idempotent — a repeated put with the same fields is a no-op.', example_request=None, example_response=None)], source='jmri_server.services.OBlockService', rewrite_extension=False), 'throttle': TopicHelp(topic='throttle', summary='Loco control — acquire an address, set speed, set functions, release. One throttle per DCC address on the active connection.', methods=[MethodHelp(name='put', description='Acquire a throttle for a DCC address.', example_request='{"type": "throttle", "method": "put",\n "data": {"address": 3, "long_address": false}}', example_response=None), MethodHelp(name='post', description='Update speed / direction / function state.', example_request='{"type": "throttle", "method": "post",\n "data": {"address": 3, "speed": 0.5, "direction": "forward", "functions": {"0": true}}}', example_response=None), MethodHelp(name='delete', description='Release the throttle (clean release, keeps the loco rolling at its last commanded speed until the CS purge timeout).', example_request='{"type": "throttle", "method": "delete",\n "data": {"address": 3}}', example_response=None)], source='jmri_server.services.ThrottleService', rewrite_extension=False), 'programmer': TopicHelp(topic='programmer', summary='CV read / write on the programming track (service mode) or on a running loco (ops mode). Ops-mode reads require RailCom on the CS + decoder. Requests block for 1–3s per CV on service mode.', methods=[MethodHelp(name='get', description='Read one CV.', example_request='{"type": "programmer", "method": "get",\n "data": {"cv": 29, "mode": "service"}}', example_response=None), MethodHelp(name='post', description='Write one CV.', example_request='{"type": "programmer", "method": "post",\n "data": {"cv": 29, "value": 6, "mode": "service"}}', example_response=None)], source='jmri_server.services.ProgrammerService', rewrite_extension=False), 'roster': TopicHelp(topic='roster', summary="Roster entries — the profile's loco list, each with a decoder family/model/productID and per-loco function labels. Backed by ``<profile>/roster.xml``.", methods=[MethodHelp(name='list', description='Every roster entry in the active profile.', example_request='{"type": "roster", "method": "list"}', example_response=None), MethodHelp(name='get', description='One entry by roster ID.', example_request='{"type": "roster", "method": "get", "data": {"id": "3601"}}', example_response=None), MethodHelp(name='put', description='Create or update a roster entry.', example_request=None, example_response=None), MethodHelp(name='delete', description='Remove a roster entry from the profile.', example_request=None, example_response=None)], source='jmri_server.services.RosterService', rewrite_extension=False), 'consist': TopicHelp(topic='consist', summary="Loco consists (multi-unit lash-ups). Add / remove locos, set direction-relative-to-consist, kill the consist. Backed by the active connection's ``ConsistManager``.", methods=[MethodHelp(name='list', description='Every registered consist.', example_request='{"type": "consist", "method": "list"}', example_response=None), MethodHelp(name='put', description='Create a new consist at a given address.', example_request='{"type": "consist", "method": "put",\n "data": {"address": 20}}', example_response=None), MethodHelp(name='post', description='Add or remove a member, set direction.', example_request='{"type": "consist", "method": "post", "data":\n {"address": 20, "action": "add", "loco": 3601,\n "direction_normal": true}}', example_response=None), MethodHelp(name='delete', description='Kill the consist.', example_request='{"type": "consist", "method": "delete",\n "data": {"address": 20}}', example_response=None)], source='jmri_server.services.ConsistService', rewrite_extension=False), 'logixng': TopicHelp(topic='logixng', summary='LogixNG programs — declarative automation trees. Each program has a trigger (turnout thrown, sensor active, clock time, …) and an action tree (set turnout, run route, invoke a script). Stored per-profile as YAML.', methods=[MethodHelp(name='list', description='Every loaded LogixNG program.', example_request='{"type": "logixng", "method": "list"}', example_response=None), MethodHelp(name='get', description="One program's structure by system name.", example_request=None, example_response=None), MethodHelp(name='post', description='Enable / disable a program at runtime.', example_request=None, example_response=None)], source='jmri_server.services.LogixNGService', rewrite_extension=False), 'signalSystem': TopicHelp(topic='signalSystem', summary='Loaded signal systems (AAR-1946, NORAC, custom). Read-only; systems are defined by XML files under the profile and enumerated on load.', methods=[MethodHelp(name='list', description='Every loaded signal system with its aspects.', example_request='{"type": "signalSystem", "method": "list"}', example_response=None), MethodHelp(name='get', description='One signal system by name.', example_request=None, example_response=None)], source='jmri_server.services.SignalSystemService', rewrite_extension=False), 'time': TopicHelp(topic='time', summary='Fast clock — layout time separate from wall-clock time. Rate is a multiplier; setting rate to 0 pauses it. ``time`` is an ISO-8601 timestamp of the current fast-clock reading.', methods=[MethodHelp(name='get', description='Current fast-clock reading + rate.', example_request='{"type": "time"}', example_response=None), MethodHelp(name='post', description='Set the time or the rate.', example_request='{"type": "time", "method": "post",\n "data": {"rate": 4.0, "run": true}}', example_response=None)], source='jmri_server.services.TimeService', rewrite_extension=False), 'panel': TopicHelp(topic='panel', summary="Panel definitions — the operator's on-screen layout diagram. Currently exposes the list of loaded panels; the full panel-render pipeline is a future item.", methods=[MethodHelp(name='list', description='Every loaded panel in the profile.', example_request='{"type": "panel", "method": "list"}', example_response=None)], source='jmri_server.services.PanelService', rewrite_extension=False), 'metadata': TopicHelp(topic='metadata', summary='Server metadata — JMRI version, copyright, activation profile, Java version (N/A in our Python build), OS. Consumed by legacy JMRI clients for feature detection.', methods=[MethodHelp(name='list', description='Every metadata field.', example_request=None, example_response=None), MethodHelp(name='get', description='One metadata value by name.', example_request='{"type": "metadata", "method": "get",\n "data": {"name": "JMRIVERCANON"}}', example_response=None)], source='jmri_server.services.MetadataService', rewrite_extension=False), 'networkService': TopicHelp(topic='networkService', summary='Advertised network services — WiThrottle TCP port, WebSocket URL, HTTP root. Legacy JMRI clients enumerate these to auto-configure.', methods=[MethodHelp(name='list', description='Every advertised network service.', example_request=None, example_response=None)], source='jmri_server.services.NetworkServiceService', rewrite_extension=False), 'systemConnection': TopicHelp(topic='systemConnection', summary='The active hardware connection (NCE / LocoNet / DCC-EX / OpenLCB / internal). Read-only enumeration; connection config lives in ``ConnectionConfig.xml`` under the profile.', methods=[MethodHelp(name='list', description='Every configured system connection.', example_request=None, example_response=None)], source='jmri_server.services.SystemConnectionService', rewrite_extension=False), 'logs': TopicHelp(topic='logs', summary='Server log tail — the last N lines from the running jmri.log. Used by the home-tab log strip and the /logs diagnostics page.', methods=[MethodHelp(name='list', description='Last N lines from jmri.log.', example_request='{"type": "logs", "method": "list", "data": {"tail": 50}}', example_response=None)], source='jmri_server.services.LogsService', rewrite_extension=True), 'configUser': TopicHelp(topic='configUser', summary='User management + self-service password/token change. Admin-only for list/get/put/delete; the ``change_password`` and ``regenerate_token`` post actions are self-service for any authenticated user (admin can act on anyone). Never returns the password hash — only whether one exists.', methods=[MethodHelp(name='list', description='Every registered user with role + has-token.', example_request=None, example_response=None), MethodHelp(name='put', description='Create or update a user.', example_request='{"type": "configUser", "method": "put", "data": {\n "username": "alice",\n "password": "secret",\n "role": "operator"\n}}', example_response=None), MethodHelp(name='delete', description='Remove a user. Refuses self-delete + last-admin.', example_request=None, example_response=None), MethodHelp(name='post', description="action=change_password → change your own or (as admin) someone else's password. action=regenerate_token → mint a new API token; the old one stops working immediately.", example_request='{"type": "configUser", "method": "post", "data": {\n "action": "regenerate_token",\n "username": "alice"\n}}', example_response=None)], source='jmri_server.services.ConfigUserService', rewrite_extension=True), 'configProfile': TopicHelp(topic='configProfile', summary="Create / delete / list JMRI profiles under the settings root. Delete only unregisters the profile from ``profiles.xml`` — the on-disk directory is left in place so an accidental UI click can't destroy layout data.", methods=[MethodHelp(name='list', description='Every registered profile.', example_request=None, example_response=None), MethodHelp(name='put', description='Create a new profile.', example_request='{"type": "configProfile", "method": "put",\n "data": {"name": "production"}}', example_response=None), MethodHelp(name='delete', description='Unregister a profile (dir left on disk).', example_request='{"type": "configProfile", "method": "delete",\n "data": {"id": "production.abcd1234"}}', example_response=None)], source='jmri_server.services.ConfigProfileService', rewrite_extension=True), 'beanTransfer': TopicHelp(topic='beanTransfer', summary='Copy beans (turnouts / sensors / lights) between profiles. Preview shows what would be added; apply commits the copy. When the target profile is the active one, live managers refresh so the change is visible immediately without a profile reload.', methods=[MethodHelp(name='list', description='Every profile available as source/target.', example_request=None, example_response=None), MethodHelp(name='post', description='Preview or apply a transfer. ``action=preview`` returns the diff; ``action=apply`` commits it.', example_request='{"type": "beanTransfer", "method": "post", "data":\n {"action": "preview", "source": "dev.abcd",\n "target": "prod.efgh", "kinds": ["turnout"]}}', example_response=None)], source='jmri_server.services.BeanTransferService', rewrite_extension=True), 'scripts': TopicHelp(topic='scripts', summary="Python scripts loaded from ``<profile>/python/`` and ``<settings>/python/``. List, save, start, stop, delete. Shared scripts (settings-root) are operator-managed and can't be deleted through the API.", methods=[MethodHelp(name='list', description='Running scripts, or with data={"library": true}, the whole on-disk library.', example_request='{"type": "scripts", "method": "list"}', example_response=None), MethodHelp(name='get', description="One script's status by name.", example_request='{"type": "scripts", "method": "get",\n "data": {"name": "morning_setup"}}', example_response=None), MethodHelp(name='put', description="Start a script from inline source. ``save=true`` also writes it to the profile's ``python/`` dir so it autoloads next boot.", example_request='{"type": "scripts", "method": "put", "data":\n {"name": "hello", "source": "print(\\"hi\\")",\n "save": true}}', example_response=None), MethodHelp(name='post', description='action=stop → stop a running script; action=delete → also remove the on-disk file.', example_request=None, example_response=None)], source='jmri_server.services.ScriptsService', rewrite_extension=True)}
def get(topic: str) -> TopicHelp | None:
736def get(topic: str) -> TopicHelp | None:
737    """Return the help entry for *topic*, or None if none is registered.
738
739    The web layer falls back to a "no help authored yet" panel when
740    this returns None — the topic still renders as a badge, just
741    without content. That way a new service can land without the
742    help wire-up blocking it.
743    """
744    return TOPIC_HELP.get(topic)

Return the help entry for topic, or None if none is registered.

The web layer falls back to a "no help authored yet" panel when this returns None — the topic still renders as a badge, just without content. That way a new service can land without the help wire-up blocking it.