diff --git a/include/crm/fencing/internal.h b/include/crm/fencing/internal.h
index a49f142416..9eddc866bc 100644
--- a/include/crm/fencing/internal.h
+++ b/include/crm/fencing/internal.h
@@ -1,218 +1,220 @@
 /*
  * Copyright 2011-2022 the Pacemaker project contributors
  *
  * The version control history for this file may have further details.
  *
  * This source code is licensed under the GNU Lesser General Public License
  * version 2.1 or later (LGPLv2.1+) WITHOUT ANY WARRANTY.
  */
 
 #ifndef STONITH_NG_INTERNAL__H
 #  define STONITH_NG_INTERNAL__H
 
 #  include <glib.h>
 #  include <crm/common/ipc.h>
 #  include <crm/common/xml.h>
 #  include <crm/common/output_internal.h>
 #  include <crm/stonith-ng.h>
 
 enum st_device_flags
 {
     st_device_supports_list   = 0x0001,
     st_device_supports_status = 0x0002,
     st_device_supports_reboot = 0x0004,
     st_device_supports_parameter_plug = 0x0008,
     st_device_supports_parameter_port = 0x0010,
 };
 
 #define stonith__set_device_flags(device_flags, device_id, flags_to_set) do { \
         device_flags = pcmk__set_flags_as(__func__, __LINE__, LOG_TRACE,      \
                                           "Fence device", device_id,          \
                                           (device_flags), (flags_to_set),     \
                                           #flags_to_set);                     \
     } while (0)
 
 #define stonith__set_call_options(st_call_opts, call_for, flags_to_set) do { \
         st_call_opts = pcmk__set_flags_as(__func__, __LINE__, LOG_TRACE,     \
                                           "Fencer call", (call_for),         \
                                           (st_call_opts), (flags_to_set),    \
                                           #flags_to_set);                    \
     } while (0)
 
 #define stonith__clear_call_options(st_call_opts, call_for, flags_to_clear) do { \
         st_call_opts = pcmk__clear_flags_as(__func__, __LINE__, LOG_TRACE,     \
                                             "Fencer call", (call_for),         \
                                             (st_call_opts), (flags_to_clear),  \
                                             #flags_to_clear);                  \
     } while (0)
 
 struct stonith_action_s;
 typedef struct stonith_action_s stonith_action_t;
 
 stonith_action_t *stonith_action_create(const char *agent,
                                         const char *_action,
                                         const char *victim,
                                         uint32_t victim_nodeid,
                                         int timeout,
                                         GHashTable * device_args,
                                         GHashTable * port_map,
                                         const char * host_arg);
 void stonith__destroy_action(stonith_action_t *action);
 pcmk__action_result_t *stonith__action_result(stonith_action_t *action);
 int stonith__result2rc(const pcmk__action_result_t *result);
 void stonith__xe_set_result(xmlNode *xml, const pcmk__action_result_t *result);
 void stonith__xe_get_result(xmlNode *xml, pcmk__action_result_t *result);
 xmlNode *stonith__find_xe_with_result(xmlNode *xml);
 
 int
 stonith_action_execute_async(stonith_action_t * action,
                              void *userdata,
                              void (*done) (int pid,
                                            const pcmk__action_result_t *result,
                                            void *user_data),
                              void (*fork_cb) (int pid, void *user_data));
 
 xmlNode *create_level_registration_xml(const char *node, const char *pattern,
                                        const char *attr, const char *value,
                                        int level,
                                        stonith_key_value_t *device_list);
 
 xmlNode *create_device_registration_xml(const char *id,
                                         enum stonith_namespace namespace,
                                         const char *agent,
                                         stonith_key_value_t *params,
                                         const char *rsc_provides);
 
 void stonith__register_messages(pcmk__output_t *out);
 
 GList *stonith__parse_targets(const char *hosts);
 
 const char *stonith__later_succeeded(stonith_history_t *event,
                                      stonith_history_t *top_history);
 stonith_history_t *stonith__sort_history(stonith_history_t *history);
 
 void stonith__device_parameter_flags(uint32_t *device_flags,
                                      const char *device_name,
                                      xmlNode *metadata);
 
 #  define ST_LEVEL_MAX 10
 
 #  define F_STONITH_CLIENTID      "st_clientid"
 #  define F_STONITH_CALLOPTS      "st_callopt"
 #  define F_STONITH_CALLID        "st_callid"
 #  define F_STONITH_CALLDATA      "st_calldata"
 #  define F_STONITH_OPERATION     "st_op"
 #  define F_STONITH_TARGET        "st_target"
 #  define F_STONITH_REMOTE_OP_ID  "st_remote_op"
 #  define F_STONITH_REMOTE_OP_ID_RELAY  "st_remote_op_relay"
 #  define F_STONITH_RC            "st_rc"
 #  define F_STONITH_OUTPUT        "st_output"
 /*! Timeout period per a device execution */
 #  define F_STONITH_TIMEOUT       "st_timeout"
 #  define F_STONITH_TOLERANCE     "st_tolerance"
 #  define F_STONITH_DELAY         "st_delay"
 /*! Action specific timeout period returned in query of fencing devices. */
 #  define F_STONITH_ACTION_TIMEOUT       "st_action_timeout"
 /*! Host in query result is not allowed to run this action */
 #  define F_STONITH_ACTION_DISALLOWED     "st_action_disallowed"
 /*! Maximum of random fencing delay for a device */
 #  define F_STONITH_DELAY_MAX            "st_delay_max"
 /*! Base delay used for a fencing delay */
 #  define F_STONITH_DELAY_BASE           "st_delay_base"
 /*! Has this device been verified using a monitor type
  *  operation (monitor, list, status) */
 #  define F_STONITH_DEVICE_VERIFIED   "st_monitor_verified"
 /*! device is required for this action */
 #  define F_STONITH_DEVICE_REQUIRED   "st_required"
 /*! number of available devices in query result */
 #  define F_STONITH_AVAILABLE_DEVICES "st-available-devices"
 #  define F_STONITH_CALLBACK_TOKEN    "st_async_id"
 #  define F_STONITH_CLIENTNAME        "st_clientname"
 #  define F_STONITH_CLIENTNODE        "st_clientnode"
 #  define F_STONITH_NOTIFY_ACTIVATE   "st_notify_activate"
 #  define F_STONITH_NOTIFY_DEACTIVATE "st_notify_deactivate"
 #  define F_STONITH_DELEGATE      "st_delegate"
 /*! The node initiating the stonith operation.  If an operation
  * is relayed, this is the last node the operation lands on. When
  * in standalone mode, origin is the client's id that originated the
  * operation. */
 #  define F_STONITH_ORIGIN        "st_origin"
 #  define F_STONITH_HISTORY_LIST  "st_history"
 #  define F_STONITH_DATE          "st_date"
 #  define F_STONITH_DATE_NSEC     "st_date_nsec"
 #  define F_STONITH_STATE         "st_state"
 #  define F_STONITH_ACTIVE        "st_active"
 #  define F_STONITH_DIFFERENTIAL  "st_differential"
 
 #  define F_STONITH_DEVICE        "st_device_id"
 #  define F_STONITH_ACTION        "st_device_action"
 #  define F_STONITH_MERGED        "st_op_merged"
 
 #  define T_STONITH_NG        "stonith-ng"
 #  define T_STONITH_REPLY     "st-reply"
 /*! For async operations, an event from the server containing
  * the total amount of time the server is allowing for the operation
  * to take place is returned to the client. */
 #  define T_STONITH_TIMEOUT_VALUE "st-async-timeout-value"
 #  define T_STONITH_NOTIFY    "st_notify"
 
 #  define STONITH_ATTR_ACTION_OP   "action"
 
 #  define STONITH_OP_EXEC        "st_execute"
 #  define STONITH_OP_TIMEOUT_UPDATE        "st_timeout_update"
 #  define STONITH_OP_QUERY       "st_query"
 #  define STONITH_OP_FENCE       "st_fence"
 #  define STONITH_OP_RELAY       "st_relay"
 #  define STONITH_OP_DEVICE_ADD      "st_device_register"
 #  define STONITH_OP_DEVICE_DEL      "st_device_remove"
 #  define STONITH_OP_FENCE_HISTORY   "st_fence_history"
 #  define STONITH_OP_LEVEL_ADD       "st_level_add"
 #  define STONITH_OP_LEVEL_DEL       "st_level_remove"
 
 #  define STONITH_WATCHDOG_AGENT          "fence_watchdog"
 /* Don't change 2 below as it would break rolling upgrade */
 #  define STONITH_WATCHDOG_AGENT_INTERNAL "#watchdog"
 #  define STONITH_WATCHDOG_ID             "watchdog"
 
 /* Exported for crm_mon to reference */
 int stonith__failed_history(pcmk__output_t *out, va_list args);
 int stonith__history(pcmk__output_t *out, va_list args);
 int stonith__full_history(pcmk__output_t *out, va_list args);
 int stonith__pending_actions(pcmk__output_t *out, va_list args);
 
 stonith_history_t *stonith__first_matching_event(stonith_history_t *history,
                                                  bool (*matching_fn)(stonith_history_t *, void *),
                                                  void *user_data);
 bool stonith__event_state_pending(stonith_history_t *history, void *user_data);
 bool stonith__event_state_eq(stonith_history_t *history, void *user_data);
 bool stonith__event_state_neq(stonith_history_t *history, void *user_data);
 
 int stonith__legacy2status(int rc);
 
 int stonith__exit_status(stonith_callback_data_t *data);
 int stonith__execution_status(stonith_callback_data_t *data);
 const char *stonith__exit_reason(stonith_callback_data_t *data);
 
 int stonith__event_exit_status(stonith_event_t *event);
 int stonith__event_execution_status(stonith_event_t *event);
 const char *stonith__event_exit_reason(stonith_event_t *event);
 char *stonith__event_description(stonith_event_t *event);
+gchar *stonith__history_description(stonith_history_t *event, bool full_history,
+                                    const char *later_succeeded);
 
 /*!
  * \internal
  * \brief Is a fencing operation in pending state?
  *
  * \param[in] state     State as enum op_state value
  *
  * \return A boolean
  */
 static inline bool
 stonith__op_state_pending(enum op_state state)
 {
     return state != st_failed && state != st_done;
 }
 
 gboolean stonith__watchdog_fencing_enabled_for_node(const char *node);
 gboolean stonith__watchdog_fencing_enabled_for_node_api(stonith_t *st, const char *node);
 
 #endif
diff --git a/lib/fencing/st_output.c b/lib/fencing/st_output.c
index 1832d4b857..0e834d7608 100644
--- a/lib/fencing/st_output.c
+++ b/lib/fencing/st_output.c
@@ -1,487 +1,520 @@
 /*
  * Copyright 2019-2022 the Pacemaker project contributors
  *
  * The version control history for this file may have further details.
  *
  * This source code is licensed under the GNU Lesser General Public License
  * version 2.1 or later (LGPLv2.1+) WITHOUT ANY WARRANTY.
  */
 
 #include <crm_internal.h>
 #include <stdarg.h>
 #include <stdint.h>
 
 #include <crm/stonith-ng.h>
 #include <crm/msg_xml.h>
 #include <crm/common/iso8601.h>
 #include <crm/common/util.h>
 #include <crm/common/xml.h>
 #include <crm/common/output.h>
 #include <crm/common/output_internal.h>
 #include <crm/common/xml_internal.h>
 #include <crm/fencing/internal.h>
 #include <crm/pengine/internal.h>
 
 static char *
 time_t_string(time_t when) {
     crm_time_t *crm_when = crm_time_new(NULL);
     char *buf = NULL;
 
     crm_time_set_timet(crm_when, &when);
     buf = crm_time_as_string(crm_when, crm_time_log_date | crm_time_log_timeofday | crm_time_log_with_timezone);
     crm_time_free(crm_when);
     return buf;
 }
 
+/*!
+ * \internal
+ * \brief Return a status-friendly description of fence history entry state
+ *
+ * \param[in] history  Fence history entry to describe
+ *
+ * \return One-word description of history entry state
+ * \note This is similar to stonith_op_state_str() except user-oriented (i.e.
+ *       for cluster status) instead of developer-oriented (for debug logs).
+ */
+static const char *
+state_str(stonith_history_t *history)
+{
+    switch (history->state) {
+        case st_failed: return "failed";
+        case st_done:   return "successful";
+        default:        return "pending";
+    }
+}
+
+/*!
+ * \internal
+ * \brief Create a description of a fencing history entry for status displays
+ *
+ * \param[in] history          Fencing history entry to describe
+ * \param[in] full_history     Whether this is for full or condensed history
+ * \param[in] later_succeeded  Node that a later equivalent attempt succeeded
+ *                             from, or NULL if none
+ *
+ * \return Newly created string with fencing history entry description
+ *
+ * \note The caller is responsible for freeing the return value with g_free().
+ * \note This is similar to stonith__event_description(), except this is used
+ *       for history entries (stonith_history_t) in status displays rather than
+ *       event notifications (stonith_event_t) in log messages.
+ */
+gchar *
+stonith__history_description(stonith_history_t *history, bool full_history,
+                             const char *later_succeeded)
+{
+    GString *str = g_string_sized_new(256); // Generous starting size
+    char *retval = NULL;
+    char *completed_time = NULL;
+
+    if ((history->state == st_failed) || (history->state == st_done)) {
+        completed_time = time_t_string(history->completed);
+    }
+
+    g_string_printf(str, "%s of %s %s",
+                    stonith_action_str(history->action), history->target,
+                    state_str(history));
+
+    // For failed actions, add exit reason if available
+    if ((history->state == st_failed) && (history->exit_reason != NULL)) {
+        g_string_append_printf(str, " (%s)", history->exit_reason);
+    }
+
+    g_string_append(str, ": ");
+
+    // For completed actions, add delegate if available
+    if (((history->state == st_failed) || (history->state == st_done))
+        && (history->delegate != NULL)) {
+        g_string_append_printf(str, "delegate=%s, ", history->delegate);
+    }
+
+    // Add information about originator
+    g_string_append_printf(str, "client=%s, origin=%s",
+                           history->client, history->origin);
+
+    // For completed actions, add completion time
+    if (completed_time != NULL) {
+        if (full_history) {
+            g_string_append(str, ", completed");
+        } else if (history->state == st_failed) {
+            g_string_append(str, ", last-failed");
+        } else {
+            g_string_append(str, ", last-successful");
+        }
+        g_string_append_printf(str, "='%s'", completed_time);
+    }
+
+    if ((history->state == st_failed) && (later_succeeded != NULL)) {
+        g_string_append_printf(str, " (a later attempt from %s succeeded)",
+                               later_succeeded);
+    }
+
+    retval = str->str;
+    g_string_free(str, FALSE);
+    free(completed_time);
+    return retval;
+}
+
 PCMK__OUTPUT_ARGS("failed-fencing-list", "stonith_history_t *", "GList *", "uint32_t",
                   "gboolean")
 int
 stonith__failed_history(pcmk__output_t *out, va_list args) {
     stonith_history_t *history = va_arg(args, stonith_history_t *);
     GList *only_node = va_arg(args, GList *);
     uint32_t section_opts = va_arg(args, uint32_t);
     gboolean print_spacer = va_arg(args, gboolean);
 
     int rc = pcmk_rc_no_output;
 
     for (stonith_history_t *hp = history; hp; hp = hp->next) {
         if (hp->state != st_failed) {
             continue;
         }
 
         if (!pcmk__str_in_list(hp->target, only_node, pcmk__str_star_matches|pcmk__str_casei)) {
             continue;
         }
 
         PCMK__OUTPUT_LIST_HEADER(out, print_spacer, rc, "Failed Fencing Actions");
         out->message(out, "stonith-event", hp, pcmk_all_flags_set(section_opts, pcmk_section_fencing_all),
                      stonith__later_succeeded(hp, history));
         out->increment_list(out);
     }
 
     PCMK__OUTPUT_LIST_FOOTER(out, rc);
     return rc;
 }
 
 PCMK__OUTPUT_ARGS("fencing-list", "stonith_history_t *", "GList *", "uint32_t", "gboolean")
 int
 stonith__history(pcmk__output_t *out, va_list args) {
     stonith_history_t *history = va_arg(args, stonith_history_t *);
     GList *only_node = va_arg(args, GList *);
     uint32_t section_opts = va_arg(args, uint32_t);
     gboolean print_spacer = va_arg(args, gboolean);
 
     int rc = pcmk_rc_no_output;
 
     for (stonith_history_t *hp = history; hp; hp = hp->next) {
         if (!pcmk__str_in_list(hp->target, only_node, pcmk__str_star_matches|pcmk__str_casei)) {
             continue;
         }
 
         if (hp->state != st_failed) {
             PCMK__OUTPUT_LIST_HEADER(out, print_spacer, rc, "Fencing History");
             out->message(out, "stonith-event", hp, pcmk_all_flags_set(section_opts, pcmk_section_fencing_all),
                          stonith__later_succeeded(hp, history));
             out->increment_list(out);
         }
     }
 
     PCMK__OUTPUT_LIST_FOOTER(out, rc);
     return rc;
 }
 
 PCMK__OUTPUT_ARGS("full-fencing-list", "crm_exit_t", "stonith_history_t *", "GList *",
                   "uint32_t", "gboolean")
 int
 stonith__full_history(pcmk__output_t *out, va_list args) {
     crm_exit_t history_rc G_GNUC_UNUSED = va_arg(args, crm_exit_t);
     stonith_history_t *history = va_arg(args, stonith_history_t *);
     GList *only_node = va_arg(args, GList *);
     uint32_t section_opts = va_arg(args, uint32_t);
     gboolean print_spacer = va_arg(args, gboolean);
 
     int rc = pcmk_rc_no_output;
 
     for (stonith_history_t *hp = history; hp; hp = hp->next) {
         if (!pcmk__str_in_list(hp->target, only_node, pcmk__str_star_matches|pcmk__str_casei)) {
             continue;
         }
 
         PCMK__OUTPUT_LIST_HEADER(out, print_spacer, rc, "Fencing History");
         out->message(out, "stonith-event", hp, pcmk_all_flags_set(section_opts, pcmk_section_fencing_all),
                      stonith__later_succeeded(hp, history));
         out->increment_list(out);
     }
 
     PCMK__OUTPUT_LIST_FOOTER(out, rc);
     return rc;
 }
 
 PCMK__OUTPUT_ARGS("full-fencing-list", "crm_exit_t", "stonith_history_t *", "GList *",
                   "uint32_t", "gboolean")
 static int
 full_history_xml(pcmk__output_t *out, va_list args) {
     crm_exit_t history_rc = va_arg(args, crm_exit_t);
     stonith_history_t *history = va_arg(args, stonith_history_t *);
     GList *only_node = va_arg(args, GList *);
     uint32_t section_opts = va_arg(args, uint32_t);
     gboolean print_spacer G_GNUC_UNUSED = va_arg(args, gboolean);
 
     int rc = pcmk_rc_no_output;
 
     if (history_rc == 0) {
         for (stonith_history_t *hp = history; hp; hp = hp->next) {
             if (!pcmk__str_in_list(hp->target, only_node, pcmk__str_star_matches|pcmk__str_casei)) {
                 continue;
             }
 
             PCMK__OUTPUT_LIST_HEADER(out, FALSE, rc, "Fencing History");
             out->message(out, "stonith-event", hp, pcmk_all_flags_set(section_opts, pcmk_section_fencing_all),
                          stonith__later_succeeded(hp, history));
             out->increment_list(out);
         }
 
         PCMK__OUTPUT_LIST_FOOTER(out, rc);
     } else {
         char *rc_s = pcmk__itoa(history_rc);
 
         pcmk__output_create_xml_node(out, "fence_history",
                                      "status", rc_s,
                                      NULL);
         free(rc_s);
 
         rc = pcmk_rc_ok;
     }
 
     return rc;
 }
 
 PCMK__OUTPUT_ARGS("last-fenced", "const char *", "time_t")
 static int
 last_fenced_html(pcmk__output_t *out, va_list args) {
     const char *target = va_arg(args, const char *);
     time_t when = va_arg(args, time_t);
 
     if (when) {
         char *buf = crm_strdup_printf("Node %s last fenced at: %s", target, ctime(&when));
         pcmk__output_create_html_node(out, "div", NULL, NULL, buf);
         free(buf);
         return pcmk_rc_ok;
     } else {
         return pcmk_rc_no_output;
     }
 }
 
 PCMK__OUTPUT_ARGS("last-fenced", "const char *", "time_t")
 static int
 last_fenced_text(pcmk__output_t *out, va_list args) {
     const char *target = va_arg(args, const char *);
     time_t when = va_arg(args, time_t);
 
     if (when) {
         pcmk__indented_printf(out, "Node %s last fenced at: %s", target, ctime(&when));
     } else {
         pcmk__indented_printf(out, "Node %s has never been fenced\n", target);
     }
 
     return pcmk_rc_ok;
 }
 
 PCMK__OUTPUT_ARGS("last-fenced", "const char *", "time_t")
 static int
 last_fenced_xml(pcmk__output_t *out, va_list args) {
     const char *target = va_arg(args, const char *);
     time_t when = va_arg(args, time_t);
 
     if (when) {
         char *buf = time_t_string(when);
 
         pcmk__output_create_xml_node(out, "last-fenced",
                                      "target", target,
                                      "when", buf,
                                      NULL);
 
         free(buf);
         return pcmk_rc_ok;
     } else {
         return pcmk_rc_no_output;
     }
 }
 
 PCMK__OUTPUT_ARGS("pending-fencing-list", "stonith_history_t *", "GList *", "uint32_t",
                   "gboolean")
 int
 stonith__pending_actions(pcmk__output_t *out, va_list args) {
     stonith_history_t *history = va_arg(args, stonith_history_t *);
     GList *only_node = va_arg(args, GList *);
     uint32_t section_opts = va_arg(args, uint32_t);
     gboolean print_spacer = va_arg(args, gboolean);
 
     int rc = pcmk_rc_no_output;
 
     for (stonith_history_t *hp = history; hp; hp = hp->next) {
         if (!pcmk__str_in_list(hp->target, only_node, pcmk__str_star_matches|pcmk__str_casei)) {
             continue;
         }
 
         /* Skip the rest of the history after we see a failed/done action */
         if ((hp->state == st_failed) || (hp->state == st_done)) {
             break;
         }
 
         PCMK__OUTPUT_LIST_HEADER(out, print_spacer, rc, "Pending Fencing Actions");
         out->message(out, "stonith-event", hp, pcmk_all_flags_set(section_opts, pcmk_section_fencing_all),
                      stonith__later_succeeded(hp, history));
         out->increment_list(out);
     }
 
     PCMK__OUTPUT_LIST_FOOTER(out, rc);
     return rc;
 }
 
 PCMK__OUTPUT_ARGS("stonith-event", "stonith_history_t *", "gboolean", "const char *")
 static int
 stonith_event_html(pcmk__output_t *out, va_list args) {
     stonith_history_t *event = va_arg(args, stonith_history_t *);
     gboolean full_history = va_arg(args, gboolean);
     const char *succeeded = va_arg(args, const char *);
 
+    gchar *desc = stonith__history_description(event, full_history, succeeded);
+
     switch(event->state) {
-        case st_done: {
-            char *completed_s = time_t_string(event->completed);
-
-            out->list_item(out, "successful-stonith-event",
-                           "%s of %s successful: delegate=%s, client=%s, origin=%s, %s='%s'",
-                           stonith_action_str(event->action), event->target,
-                           event->delegate ? event->delegate : "",
-                           event->client, event->origin,
-                           full_history ? "completed" : "last-successful",
-                           completed_s);
-            free(completed_s);
+        case st_done:
+            out->list_item(out, "successful-stonith-event", "%s", desc);
             break;
-        }
 
-        case st_failed: {
-            char *failed_s = time_t_string(event->completed);
-
-            out->list_item(out, "failed-stonith-event",
-                           "%s of %s failed%s%s%s: "
-                           "delegate=%s, client=%s, origin=%s, %s='%s'%s%s%s",
-                           stonith_action_str(event->action), event->target,
-                           (event->exit_reason == NULL)? "" : " (",
-                           (event->exit_reason == NULL)? "" : event->exit_reason,
-                           (event->exit_reason == NULL)? "" : ")",
-                           event->delegate ? event->delegate : "",
-                           event->client, event->origin,
-                           full_history ? "completed" : "last-failed",
-                           failed_s,
-                           (succeeded == NULL)? "" : " (a later attempt from ",
-                           (succeeded == NULL)? "" : succeeded,
-                           (succeeded == NULL)? "" : " succeeded)");
-            free(failed_s);
+        case st_failed:
+            out->list_item(out, "failed-stonith-event", "%s", desc);
             break;
-        }
 
         default:
-            out->list_item(out, "pending-stonith-event",
-                           "%s of %s pending: client=%s, origin=%s",
-                           stonith_action_str(event->action), event->target,
-                           event->client, event->origin);
+            out->list_item(out, "pending-stonith-event", "%s", desc);
             break;
     }
-
+    g_free(desc);
     return pcmk_rc_ok;
 }
 
 PCMK__OUTPUT_ARGS("stonith-event", "stonith_history_t *", "gboolean", "const char *")
 static int
 stonith_event_text(pcmk__output_t *out, va_list args) {
     stonith_history_t *event = va_arg(args, stonith_history_t *);
     gboolean full_history = va_arg(args, gboolean);
     const char *succeeded = va_arg(args, const char *);
 
-    char *buf = time_t_string(event->completed);
-
-    switch (event->state) {
-        case st_failed:
-            pcmk__indented_printf(out,
-                                  "%s of %s failed%s%s%s: delegate=%s, "
-                                  "client=%s, origin=%s, %s='%s'%s%s%s\n",
-                                  stonith_action_str(event->action), event->target,
-                                  (event->exit_reason == NULL)? "" : " (",
-                                  (event->exit_reason == NULL)? "" : event->exit_reason,
-                                  (event->exit_reason == NULL)? "" : ")",
-                                  event->delegate ? event->delegate : "",
-                                  event->client, event->origin,
-                                  full_history ? "completed" : "last-failed", buf,
-                                  (succeeded == NULL)? "" : " (a later attempt from ",
-                                  (succeeded == NULL)? "" : succeeded,
-                                  (succeeded == NULL)? "" : " succeeded)");
-            break;
-
-        case st_done:
-            pcmk__indented_printf(out, "%s of %s successful: delegate=%s, client=%s, origin=%s, %s='%s'\n",
-                                  stonith_action_str(event->action), event->target,
-                                  event->delegate ? event->delegate : "",
-                                  event->client, event->origin,
-                                  full_history ? "completed" : "last-successful", buf);
-            break;
-
-        default:
-            pcmk__indented_printf(out, "%s of %s pending: client=%s, origin=%s\n",
-                                  stonith_action_str(event->action), event->target,
-                                  event->client, event->origin);
-            break;
-    }
+    gchar *desc = stonith__history_description(event, full_history, succeeded);
 
-    free(buf);
+    pcmk__indented_printf(out, "%s\n", desc);
+    g_free(desc);
     return pcmk_rc_ok;
 }
 
 PCMK__OUTPUT_ARGS("stonith-event", "stonith_history_t *", "gboolean", "const char *")
 static int
 stonith_event_xml(pcmk__output_t *out, va_list args) {
     stonith_history_t *event = va_arg(args, stonith_history_t *);
     gboolean full_history G_GNUC_UNUSED = va_arg(args, gboolean);
     const char *succeeded G_GNUC_UNUSED = va_arg(args, const char *);
 
     char *buf = NULL;
 
     xmlNodePtr node = pcmk__output_create_xml_node(out, "fence_event",
                                                    "action", event->action,
                                                    "target", event->target,
                                                    "client", event->client,
                                                    "origin", event->origin,
                                                    NULL);
 
     switch (event->state) {
         case st_failed:
             pcmk__xe_set_props(node, "status", "failed",
                                XML_LRM_ATTR_EXIT_REASON, event->exit_reason,
                                NULL);
             break;
 
         case st_done:
             crm_xml_add(node, "status", "success");
             break;
 
         default: {
             char *state = pcmk__itoa(event->state);
             pcmk__xe_set_props(node, "status", "pending",
                                "extended-status", state,
                                NULL);
             free(state);
             break;
         }
     }
 
     if (event->delegate != NULL) {
         crm_xml_add(node, "delegate", event->delegate);
     }
 
     if (event->state == st_failed || event->state == st_done) {
         buf = time_t_string(event->completed);
         crm_xml_add(node, "completed", buf);
         free(buf);
     }
 
     return pcmk_rc_ok;
 }
 
 PCMK__OUTPUT_ARGS("validate", "const char *", "const char *", "char *", "char *", "int")
 static int
 validate_agent_html(pcmk__output_t *out, va_list args) {
     const char *agent = va_arg(args, const char *);
     const char *device = va_arg(args, const char *);
     char *output = va_arg(args, char *);
     char *error_output = va_arg(args, char *);
     int rc = va_arg(args, int);
 
     if (device) {
         char *buf = crm_strdup_printf("Validation of %s on %s %s", agent, device,
                                       rc ? "failed" : "succeeded");
         pcmk__output_create_html_node(out, "div", NULL, NULL, buf);
         free(buf);
     } else {
         char *buf = crm_strdup_printf("Validation of %s %s", agent,
                                       rc ? "failed" : "succeeded");
         pcmk__output_create_html_node(out, "div", NULL, NULL, buf);
         free(buf);
     }
 
     out->subprocess_output(out, rc, output, error_output);
     return rc;
 }
 
 PCMK__OUTPUT_ARGS("validate", "const char *", "const char *", "char *", "char *", "int")
 static int
 validate_agent_text(pcmk__output_t *out, va_list args) {
     const char *agent = va_arg(args, const char *);
     const char *device = va_arg(args, const char *);
     char *output = va_arg(args, char *);
     char *error_output = va_arg(args, char *);
     int rc = va_arg(args, int);
 
     if (device) {
         pcmk__indented_printf(out, "Validation of %s on %s %s\n", agent, device,
                               rc ? "failed" : "succeeded");
     } else {
         pcmk__indented_printf(out, "Validation of %s %s\n", agent,
                               rc ? "failed" : "succeeded");
     }
 
     out->subprocess_output(out, rc, output, error_output);
     return rc;
 }
 
 PCMK__OUTPUT_ARGS("validate", "const char *", "const char *", "char *", "char *", "int")
 static int
 validate_agent_xml(pcmk__output_t *out, va_list args) {
     const char *agent = va_arg(args, const char *);
     const char *device = va_arg(args, const char *);
     char *output = va_arg(args, char *);
     char *error_output = va_arg(args, char *);
     int rc = va_arg(args, int);
 
     xmlNodePtr node = pcmk__output_create_xml_node(out, "validate",
                                                    "agent", agent,
                                                    "valid", pcmk__btoa(rc),
                                                    NULL);
 
     if (device != NULL) {
         crm_xml_add(node, "device", device);
     }
 
     pcmk__output_xml_push_parent(out, node);
     out->subprocess_output(out, rc, output, error_output);
     pcmk__output_xml_pop_parent(out);
 
     return rc;
 }
 
 static pcmk__message_entry_t fmt_functions[] = {
     { "failed-fencing-list", "default", stonith__failed_history },
     { "fencing-list", "default", stonith__history },
     { "full-fencing-list", "default", stonith__full_history },
     { "full-fencing-list", "xml", full_history_xml },
     { "last-fenced", "html", last_fenced_html },
     { "last-fenced", "log", last_fenced_text },
     { "last-fenced", "text", last_fenced_text },
     { "last-fenced", "xml", last_fenced_xml },
     { "pending-fencing-list", "default", stonith__pending_actions },
     { "stonith-event", "html", stonith_event_html },
     { "stonith-event", "log", stonith_event_text },
     { "stonith-event", "text", stonith_event_text },
     { "stonith-event", "xml", stonith_event_xml },
     { "validate", "html", validate_agent_html },
     { "validate", "log", validate_agent_text },
     { "validate", "text", validate_agent_text },
     { "validate", "xml", validate_agent_xml },
 
     { NULL, NULL, NULL }
 };
 
 void
 stonith__register_messages(pcmk__output_t *out) {
     pcmk__register_messages(out, fmt_functions);
 }
diff --git a/tools/crm_mon_curses.c b/tools/crm_mon_curses.c
index b55a7087c1..1718aeea8e 100644
--- a/tools/crm_mon_curses.c
+++ b/tools/crm_mon_curses.c
@@ -1,535 +1,498 @@
 /*
  * Copyright 2019-2022 the Pacemaker project contributors
  *
  * The version control history for this file may have further details.
  *
  * This source code is licensed under the GNU Lesser General Public License
  * version 2.1 or later (LGPLv2.1+) WITHOUT ANY WARRANTY.
  */
 
 #include <crm_internal.h>
 #include <stdarg.h>
 #include <stdint.h>
 #include <stdlib.h>
 #include <crm/crm.h>
 #include <crm/common/cmdline_internal.h>
 #include <crm/stonith-ng.h>
 #include <crm/fencing/internal.h>
 #include <crm/pengine/internal.h>
 #include <glib.h>
 #include <pacemaker-internal.h>
 
 #include "crm_mon.h"
 
 #if CURSES_ENABLED
 
 GOptionEntry crm_mon_curses_output_entries[] = {
     { NULL }
 };
 
 typedef struct curses_list_data_s {
     unsigned int len;
     char *singular_noun;
     char *plural_noun;
 } curses_list_data_t;
 
 typedef struct private_data_s {
     GQueue *parent_q;
 } private_data_t;
 
 static void
 curses_free_priv(pcmk__output_t *out) {
     private_data_t *priv = out->priv;
 
     if (priv == NULL) {
         return;
     }
 
     g_queue_free(priv->parent_q);
     free(priv);
     out->priv = NULL;
 }
 
 static bool
 curses_init(pcmk__output_t *out) {
     private_data_t *priv = NULL;
 
     /* If curses_init was previously called on this output struct, just return. */
     if (out->priv != NULL) {
         return true;
     } else {
         out->priv = calloc(1, sizeof(private_data_t));
         if (out->priv == NULL) {
             return false;
         }
 
         priv = out->priv;
     }
 
     priv->parent_q = g_queue_new();
 
     initscr();
     cbreak();
     noecho();
 
     return true;
 }
 
 static void
 curses_finish(pcmk__output_t *out, crm_exit_t exit_status, bool print, void **copy_dest) {
     CRM_ASSERT(out != NULL);
 
     echo();
     nocbreak();
     endwin();
 }
 
 static void
 curses_reset(pcmk__output_t *out) {
     CRM_ASSERT(out != NULL);
 
     curses_free_priv(out);
     curses_init(out);
 }
 
 static void
 curses_subprocess_output(pcmk__output_t *out, int exit_status,
                          const char *proc_stdout, const char *proc_stderr) {
     CRM_ASSERT(out != NULL);
 
     if (proc_stdout != NULL) {
         printw("%s\n", proc_stdout);
     }
 
     if (proc_stderr != NULL) {
         printw("%s\n", proc_stderr);
     }
 
     clrtoeol();
     refresh();
 }
 
 /* curses_version is defined in curses.h, so we can't use that name here.
  * Note that this function prints out via text, not with curses.
  */
 static void
 curses_ver(pcmk__output_t *out, bool extended) {
     CRM_ASSERT(out != NULL);
 
     if (extended) {
         printf("Pacemaker %s (Build: %s): %s\n", PACEMAKER_VERSION, BUILD_VERSION, CRM_FEATURES);
     } else {
         printf("Pacemaker %s\n", PACEMAKER_VERSION);
         printf("Written by Andrew Beekhof and the "
                "Pacemaker project contributors\n");
     }
 }
 
 G_GNUC_PRINTF(2, 3)
 static void
 curses_error(pcmk__output_t *out, const char *format, ...) {
     va_list ap;
 
     CRM_ASSERT(out != NULL);
 
     /* Informational output does not get indented, to separate it from other
      * potentially indented list output.
      */
     va_start(ap, format);
     vw_printw(stdscr, format, ap);
     va_end(ap);
 
     /* Add a newline. */
     addch('\n');
 
     clrtoeol();
     refresh();
     sleep(2);
 }
 
 G_GNUC_PRINTF(2, 3)
 static int
 curses_info(pcmk__output_t *out, const char *format, ...) {
     va_list ap;
 
     CRM_ASSERT(out != NULL);
 
     if (out->is_quiet(out)) {
         return pcmk_rc_no_output;
     }
 
     /* Informational output does not get indented, to separate it from other
      * potentially indented list output.
      */
     va_start(ap, format);
     vw_printw(stdscr, format, ap);
     va_end(ap);
 
     /* Add a newline. */
     addch('\n');
 
     clrtoeol();
     refresh();
     return pcmk_rc_ok;
 }
 
 static void
 curses_output_xml(pcmk__output_t *out, const char *name, const char *buf) {
     CRM_ASSERT(out != NULL);
     curses_indented_printf(out, "%s", buf);
 }
 
 G_GNUC_PRINTF(4, 5)
 static void
 curses_begin_list(pcmk__output_t *out, const char *singular_noun, const char *plural_noun,
                   const char *format, ...) {
     private_data_t *priv = NULL;
     curses_list_data_t *new_list = NULL;
     va_list ap;
 
     CRM_ASSERT(out != NULL && out->priv != NULL);
     priv = out->priv;
 
     /* Empty formats can be used to create a new level of indentation, but without
      * displaying some sort of list header.  In that case we need to not do any of
      * this stuff. vw_printw will act weird if told to print a NULL.
      */
     if (format != NULL) {
         va_start(ap, format);
 
         curses_indented_vprintf(out, format, ap);
         printw(":\n");
 
         va_end(ap);
     }
 
     new_list = calloc(1, sizeof(curses_list_data_t));
     new_list->len = 0;
     pcmk__str_update(&new_list->singular_noun, singular_noun);
     pcmk__str_update(&new_list->plural_noun, plural_noun);
 
     g_queue_push_tail(priv->parent_q, new_list);
 }
 
 G_GNUC_PRINTF(3, 4)
 static void
 curses_list_item(pcmk__output_t *out, const char *id, const char *format, ...) {
     va_list ap;
 
     CRM_ASSERT(out != NULL);
 
     va_start(ap, format);
 
     if (id != NULL) {
         curses_indented_printf(out, "%s: ", id);
         vw_printw(stdscr, format, ap);
     } else {
         curses_indented_vprintf(out, format, ap);
     }
 
     addch('\n');
     va_end(ap);
 
     out->increment_list(out);
 }
 
 static void
 curses_increment_list(pcmk__output_t *out) {
     private_data_t *priv = NULL;
     gpointer tail;
 
     CRM_ASSERT(out != NULL && out->priv != NULL);
     priv = out->priv;
 
     tail = g_queue_peek_tail(priv->parent_q);
     CRM_ASSERT(tail != NULL);
     ((curses_list_data_t *) tail)->len++;
 }
 
 static void
 curses_end_list(pcmk__output_t *out) {
     private_data_t *priv = NULL;
     curses_list_data_t *node = NULL;
 
     CRM_ASSERT(out != NULL && out->priv != NULL);
     priv = out->priv;
 
     node = g_queue_pop_tail(priv->parent_q);
 
     if (node->singular_noun != NULL && node->plural_noun != NULL) {
         if (node->len == 1) {
             curses_indented_printf(out, "%d %s found\n", node->len, node->singular_noun);
         } else {
             curses_indented_printf(out, "%d %s found\n", node->len, node->plural_noun);
         }
     }
 
     free(node);
 }
 
 static bool
 curses_is_quiet(pcmk__output_t *out) {
     CRM_ASSERT(out != NULL);
     return out->quiet;
 }
 
 static void
 curses_spacer(pcmk__output_t *out) {
     CRM_ASSERT(out != NULL);
     addch('\n');
 }
 
 static void
 curses_progress(pcmk__output_t *out, bool end) {
     CRM_ASSERT(out != NULL);
 
     if (end) {
         printw(".\n");
     } else {
         addch('.');
     }
 }
 
 static void
 curses_prompt(const char *prompt, bool do_echo, char **dest)
 {
     int rc = OK;
 
     CRM_ASSERT(prompt != NULL);
     CRM_ASSERT(dest != NULL);
 
     /* This is backwards from the text version of this function on purpose.  We
      * disable echo by default in curses_init, so we need to enable it here if
      * asked for.
      */
     if (do_echo) {
         rc = echo();
     }
 
     if (rc == OK) {
         printw("%s: ", prompt);
 
         if (*dest != NULL) {
             free(*dest);
         }
 
         *dest = calloc(1, 1024);
         /* On older systems, scanw is defined as taking a char * for its first argument,
          * while newer systems rightly want a const char *.  Accomodate both here due
          * to building with -Werror.
          */
         rc = scanw((NCURSES_CONST char *) "%1023s", *dest);
         addch('\n');
     }
 
     if (rc < 1) {
         free(*dest);
         *dest = NULL;
     }
 
     if (do_echo) {
         noecho();
     }
 }
 
 pcmk__output_t *
 crm_mon_mk_curses_output(char **argv) {
     pcmk__output_t *retval = calloc(1, sizeof(pcmk__output_t));
 
     if (retval == NULL) {
         return NULL;
     }
 
     retval->fmt_name = "console";
     retval->request = pcmk__quote_cmdline(argv);
 
     retval->init = curses_init;
     retval->free_priv = curses_free_priv;
     retval->finish = curses_finish;
     retval->reset = curses_reset;
 
     retval->register_message = pcmk__register_message;
     retval->message = pcmk__call_message;
 
     retval->subprocess_output = curses_subprocess_output;
     retval->version = curses_ver;
     retval->err = curses_error;
     retval->info = curses_info;
     retval->output_xml = curses_output_xml;
 
     retval->begin_list = curses_begin_list;
     retval->list_item = curses_list_item;
     retval->increment_list = curses_increment_list;
     retval->end_list = curses_end_list;
 
     retval->is_quiet = curses_is_quiet;
     retval->spacer = curses_spacer;
     retval->progress = curses_progress;
     retval->prompt = curses_prompt;
 
     return retval;
 }
 
 G_GNUC_PRINTF(2, 0)
 void
 curses_formatted_vprintf(pcmk__output_t *out, const char *format, va_list args) {
     vw_printw(stdscr, format, args);
 
     clrtoeol();
     refresh();
 }
 
 G_GNUC_PRINTF(2, 3)
 void
 curses_formatted_printf(pcmk__output_t *out, const char *format, ...) {
     va_list ap;
 
     va_start(ap, format);
     curses_formatted_vprintf(out, format, ap);
     va_end(ap);
 }
 
 G_GNUC_PRINTF(2, 0)
 void
 curses_indented_vprintf(pcmk__output_t *out, const char *format, va_list args) {
     int level = 0;
     private_data_t *priv = out->priv;
 
     CRM_ASSERT(priv != NULL);
 
     level = g_queue_get_length(priv->parent_q);
 
     for (int i = 0; i < level; i++) {
         printw("  ");
     }
 
     if (level > 0) {
         printw("* ");
     }
 
     curses_formatted_vprintf(out, format, args);
 }
 
 G_GNUC_PRINTF(2, 3)
 void
 curses_indented_printf(pcmk__output_t *out, const char *format, ...) {
     va_list ap;
 
     va_start(ap, format);
     curses_indented_vprintf(out, format, ap);
     va_end(ap);
 }
 
 PCMK__OUTPUT_ARGS("maint-mode", "unsigned long long int")
 static int
 cluster_maint_mode_console(pcmk__output_t *out, va_list args) {
     unsigned long long flags = va_arg(args, unsigned long long);
 
     if (pcmk_is_set(flags, pe_flag_maintenance_mode)) {
         curses_formatted_printf(out, "\n              *** Resource management is DISABLED ***\n");
         curses_formatted_printf(out, "  The cluster will not attempt to start, stop or recover services\n");
         return pcmk_rc_ok;
     } else if (pcmk_is_set(flags, pe_flag_stop_everything)) {
         curses_formatted_printf(out, "\n    *** Resource management is DISABLED ***\n");
         curses_formatted_printf(out, "  The cluster will keep all resources stopped\n");
         return pcmk_rc_ok;
     } else {
         return pcmk_rc_no_output;
     }
 }
 
 PCMK__OUTPUT_ARGS("cluster-status", "pe_working_set_t *", "crm_exit_t",
                   "stonith_history_t *", "gboolean", "uint32_t", "uint32_t",
                   "const char *", "GList *", "GList *")
 static int
 cluster_status_console(pcmk__output_t *out, va_list args) {
     int rc = pcmk_rc_no_output;
 
     blank_screen();
     rc = pcmk__cluster_status_text(out, args);
     refresh();
     return rc;
 }
 
 PCMK__OUTPUT_ARGS("stonith-event", "stonith_history_t *", "gboolean", "const char *")
 static int
 stonith_event_console(pcmk__output_t *out, va_list args) {
     stonith_history_t *event = va_arg(args, stonith_history_t *);
     gboolean full_history = va_arg(args, gboolean);
     const char *succeeded = va_arg(args, const char *);
 
-    crm_time_t *crm_when = crm_time_new(NULL);
-    char *buf = NULL;
-
-    crm_time_set_timet(crm_when, &(event->completed));
-    buf = crm_time_as_string(crm_when, crm_time_log_date | crm_time_log_timeofday | crm_time_log_with_timezone);
-
-    switch (event->state) {
-        case st_failed:
-            curses_indented_printf(out,
-                                   "%s of %s failed%s%s%s: "
-                                   "delegate=%s, client=%s, origin=%s, %s='%s'%s%s%s\n",
-                                   stonith_action_str(event->action), event->target,
-                                   (event->exit_reason == NULL)? "" : " (",
-                                   (event->exit_reason == NULL)? "" : event->exit_reason,
-                                   (event->exit_reason == NULL)? "" : ")",
-                                   event->delegate ? event->delegate : "",
-                                   event->client, event->origin,
-                                   full_history ? "completed" : "last-failed", buf,
-                                   (succeeded == NULL)? "" : " (a later attempt from ",
-                                   (succeeded == NULL)? "" : succeeded,
-                                   (succeeded == NULL)? "" : " succeeded)");
-
-            break;
-
-        case st_done:
-            curses_indented_printf(out, "%s of %s successful: delegate=%s, client=%s, origin=%s, %s='%s'\n",
-                                   stonith_action_str(event->action), event->target,
-                                   event->delegate ? event->delegate : "",
-                                   event->client, event->origin,
-                                   full_history ? "completed" : "last-successful", buf);
-            break;
-
-        default:
-            curses_indented_printf(out, "%s of %s pending: client=%s, origin=%s\n",
-                                   stonith_action_str(event->action), event->target,
-                                   event->client, event->origin);
-            break;
-    }
+    gchar *desc = stonith__history_description(event, full_history, succeeded);
 
-    free(buf);
-    crm_time_free(crm_when);
+    curses_indented_printf(out, "%s\n", desc);
+    g_free(desc);
     return pcmk_rc_ok;
 }
 
 static pcmk__message_entry_t fmt_functions[] = {
     { "cluster-status", "console", cluster_status_console },
     { "maint-mode", "console", cluster_maint_mode_console },
     { "stonith-event", "console", stonith_event_console },
 
     { NULL, NULL, NULL }
 };
 
 #endif
 
 void
 crm_mon_register_messages(pcmk__output_t *out) {
 #if CURSES_ENABLED
     pcmk__register_messages(out, fmt_functions);
 #endif
 }
 
 void
 blank_screen(void)
 {
 #if CURSES_ENABLED
     int lpc = 0;
 
     for (lpc = 0; lpc < LINES; lpc++) {
         move(lpc, 0);
         clrtoeol();
     }
     move(0, 0);
     refresh();
 #endif
 }