You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@causeway.apache.org by da...@apache.org on 2026/04/21 21:04:56 UTC

(causeway) branch JDOJPA-281 updated: JDOJPA-281: polishing

This is an automated email from the ASF dual-hosted git repository.

danhaywood pushed a commit to branch JDOJPA-281
in repository https://gitbox.apache.org/repos/asf/causeway.git


The following commit(s) were added to refs/heads/JDOJPA-281 by this push:
     new a8fcb9ca740 JDOJPA-281: polishing
a8fcb9ca740 is described below

commit a8fcb9ca740d7b4a675726a0a1753a03b29c43ef
Author: Dan Haywood <da...@haywood-associates.co.uk>
AuthorDate: Tue Apr 21 22:04:31 2026 +0100

    JDOJPA-281: polishing
---
 .../commandlog/applib/app/CommandLogMenu.java      |  23 ++-
 ...ExportManager#exported.columnOrder.fallback.txt |   6 +
 ...Manager#notYetExported.columnOrder.fallback.txt |   6 +
 .../applib/dom/replay/CommandExportManager.java    | 178 ++++++++++++++++---
 .../CommandExportManager.layout.fallback.xml       |   8 +
 ...anager#pendingOrFailed.columnOrder.fallback.txt |   6 +
 ...er#succeededOrExcluded.columnOrder.fallback.txt |   6 +
 .../applib/dom/replay/CommandReplayManager.java    | 182 +++++++++++++------
 .../ReplayableCommand.columnOrder.fallback.txt     |   6 +
 .../applib/dom/replay/ReplayableCommand.java       | 197 +++++++++++++++------
 .../replay/ReplayableCommand.layout.fallback.xml   |  33 +++-
 11 files changed, 503 insertions(+), 148 deletions(-)

diff --git a/extensions/core/commandlog/applib/src/main/java/org/apache/causeway/extensions/commandlog/applib/app/CommandLogMenu.java b/extensions/core/commandlog/applib/src/main/java/org/apache/causeway/extensions/commandlog/applib/app/CommandLogMenu.java
index a888729eb72..323b7590cc1 100644
--- a/extensions/core/commandlog/applib/src/main/java/org/apache/causeway/extensions/commandlog/applib/app/CommandLogMenu.java
+++ b/extensions/core/commandlog/applib/src/main/java/org/apache/causeway/extensions/commandlog/applib/app/CommandLogMenu.java
@@ -18,6 +18,8 @@
  */
 package org.apache.causeway.extensions.commandlog.applib.app;
 
+import java.sql.Timestamp;
+import java.time.Instant;
 import java.time.LocalDate;
 import java.time.LocalDateTime;
 import java.time.ZoneId;
@@ -33,7 +35,9 @@
 import org.apache.causeway.applib.annotation.DomainService;
 import org.apache.causeway.applib.annotation.DomainServiceLayout;
 import org.apache.causeway.applib.annotation.MemberSupport;
+import org.apache.causeway.applib.annotation.ParameterLayout;
 import org.apache.causeway.applib.annotation.PriorityPrecedence;
+import org.apache.causeway.applib.annotation.PropertyLayout;
 import org.apache.causeway.applib.annotation.Publishing;
 import org.apache.causeway.applib.annotation.RestrictTo;
 import org.apache.causeway.applib.annotation.SemanticsOf;
@@ -44,6 +48,9 @@
 import org.apache.causeway.extensions.commandlog.applib.dom.replay.CommandExportManager;
 import org.apache.causeway.extensions.commandlog.applib.dom.replay.CommandReplayManager;
 import org.apache.causeway.extensions.commandlog.applib.dom.replay.ReplayContext;
+
+import org.jspecify.annotations.NonNull;
+
 import org.springframework.lang.Nullable;
 
 import lombok.RequiredArgsConstructor;
@@ -163,15 +170,23 @@ public class DomainEvent extends ActionDomainEvent<findAll> { }
     public class exportManager {
         public class DomainEvent extends ActionDomainEvent<exportManager> { }
 
-        @MemberSupport public CommandExportManager act(final java.sql.Timestamp baseline) {
-            return new CommandExportManager(baseline, replayContext);
+        @MemberSupport public CommandExportManager act(
+                @ParameterLayout(describedAs = "Limits the commands shown; only commands since this timestamp are available for export.  Set to a time immediately before the commands to be replayed.")
+                final java.sql.Timestamp since
+        ) {
+            return new CommandExportManager(since, replayContext);
         }
 
-        @MemberSupport public java.sql.Timestamp defaultBaseline() {
-            return clockService.getClock().nowAsJavaSqlTimestamp();
+        @MemberSupport public java.sql.Timestamp defaultSince() {
+            final var now = clockService.getClock().nowAsJavaSqlTimestamp();
+            return truncatedTo(now, ChronoUnit.HOURS);
         }
     }
 
+    private static @NonNull Timestamp truncatedTo(Timestamp now, ChronoUnit hours) {
+        return Timestamp.from(now.toInstant().truncatedTo(hours));
+    }
+
     @Action(
             commandPublishing = Publishing.DISABLED,
             domainEvent = replayManager.DomainEvent.class,
diff --git a/extensions/core/commandlog/applib/src/main/java/org/apache/causeway/extensions/commandlog/applib/dom/replay/CommandExportManager#exported.columnOrder.fallback.txt b/extensions/core/commandlog/applib/src/main/java/org/apache/causeway/extensions/commandlog/applib/dom/replay/CommandExportManager#exported.columnOrder.fallback.txt
new file mode 100644
index 00000000000..bde91a6f91d
--- /dev/null
+++ b/extensions/core/commandlog/applib/src/main/java/org/apache/causeway/extensions/commandlog/applib/dom/replay/CommandExportManager#exported.columnOrder.fallback.txt
@@ -0,0 +1,6 @@
+#interactionId
+timestamp
+targetType
+targetId
+member
+replayState
\ No newline at end of file
diff --git a/extensions/core/commandlog/applib/src/main/java/org/apache/causeway/extensions/commandlog/applib/dom/replay/CommandExportManager#notYetExported.columnOrder.fallback.txt b/extensions/core/commandlog/applib/src/main/java/org/apache/causeway/extensions/commandlog/applib/dom/replay/CommandExportManager#notYetExported.columnOrder.fallback.txt
new file mode 100644
index 00000000000..bde91a6f91d
--- /dev/null
+++ b/extensions/core/commandlog/applib/src/main/java/org/apache/causeway/extensions/commandlog/applib/dom/replay/CommandExportManager#notYetExported.columnOrder.fallback.txt
@@ -0,0 +1,6 @@
+#interactionId
+timestamp
+targetType
+targetId
+member
+replayState
\ No newline at end of file
diff --git a/extensions/core/commandlog/applib/src/main/java/org/apache/causeway/extensions/commandlog/applib/dom/replay/CommandExportManager.java b/extensions/core/commandlog/applib/src/main/java/org/apache/causeway/extensions/commandlog/applib/dom/replay/CommandExportManager.java
index 098f4161ced..7a0a7e9dda2 100644
--- a/extensions/core/commandlog/applib/src/main/java/org/apache/causeway/extensions/commandlog/applib/dom/replay/CommandExportManager.java
+++ b/extensions/core/commandlog/applib/src/main/java/org/apache/causeway/extensions/commandlog/applib/dom/replay/CommandExportManager.java
@@ -18,6 +18,8 @@
  */
 package org.apache.causeway.extensions.commandlog.applib.dom.replay;
 
+import lombok.Getter;
+
 import java.sql.Timestamp;
 import java.time.Instant;
 import java.time.chrono.ChronoZonedDateTime;
@@ -38,6 +40,10 @@
 import org.apache.causeway.applib.annotation.Introspection;
 import org.apache.causeway.applib.annotation.MemberSupport;
 import org.apache.causeway.applib.annotation.ObjectSupport;
+import org.apache.causeway.applib.annotation.Property;
+import org.apache.causeway.applib.annotation.PropertyLayout;
+import org.apache.causeway.applib.annotation.Publishing;
+import org.apache.causeway.applib.annotation.RestrictTo;
 import org.apache.causeway.applib.annotation.SemanticsOf;
 import org.apache.causeway.applib.util.schema.CommandDtoUtils;
 import org.apache.causeway.applib.value.Blob;
@@ -48,6 +54,8 @@
 import org.apache.causeway.extensions.commandlog.applib.dom.CommandLogEntryRepository;
 import org.apache.causeway.extensions.commandlog.applib.dom.ReplayState;
 
+import org.jspecify.annotations.NonNull;
+
 @DomainObject(introspection = Introspection.ANNOTATION_REQUIRED)
 @DomainObjectLayout(cssClassFa = "solid share-from-square")
 @Named(CommandExportManager.LOGICAL_TYPE_NAME)
@@ -55,9 +63,10 @@ public final class CommandExportManager implements ViewModel {
 
 	public static final String LOGICAL_TYPE_NAME = CausewayModuleExtCommandLogApplib.NAMESPACE + ".CommandExportManager";
 
-	private ReplayContext replayContext;
+    public static abstract class ActionDomainEvent<T>
+            extends CausewayModuleExtCommandLogApplib.ActionDomainEvent<T> { }
 
-    private java.sql.Timestamp baseline;
+    private ReplayContext replayContext;
 
     @Inject
     public CommandExportManager(
@@ -67,9 +76,9 @@ public CommandExportManager(
     }
 
     public CommandExportManager(
-            final java.sql.Timestamp baseline,
+            final java.sql.Timestamp since,
             final ReplayContext replayContext) {
-        this.baseline = baseline;
+        this.since = since;
         this.replayContext = replayContext;
     }
 
@@ -78,20 +87,102 @@ public CommandExportManager(
     }
 
 
+    @Property
+    @PropertyLayout(describedAs = "Only commands since this timestamp are available for export")
+    @Getter
+    private java.sql.Timestamp since;
+
+    @Action(
+            semantics = SemanticsOf.SAFE,
+            commandPublishing = Publishing.DISABLED,
+            domainEvent = previousHour.DomainEvent.class,
+            executionPublishing = Publishing.DISABLED
+    )
+    @ActionLayout(
+            associateWith = "since", sequence = "1",
+            position = ActionLayout.Position.PANEL,
+            describedAs = "Move back one hour"
+    )
+    public class previousHour {
+        public class DomainEvent extends ActionDomainEvent<previousHour> { }
+
+        @MemberSupport public CommandExportManager act(final java.sql.Timestamp since) {
+            return new CommandExportManager(addSeconds(since, -3600), replayContext);
+        }
+        @MemberSupport public java.sql.Timestamp defaultSince() {
+            return CommandExportManager.this.since;
+        }
+    }
+
+    @Action(
+            semantics = SemanticsOf.SAFE,
+            commandPublishing = Publishing.DISABLED,
+            domainEvent = nextHour.DomainEvent.class,
+            executionPublishing = Publishing.DISABLED
+    )
+    @ActionLayout(
+            associateWith = "since", sequence = "3",
+            position = ActionLayout.Position.PANEL,
+            describedAs = "Move forward one hour"
+    )
+    public class nextHour {
+        public class DomainEvent extends ActionDomainEvent<nextHour> { }
+        @MemberSupport public CommandExportManager act(final java.sql.Timestamp since) {
+            return new CommandExportManager(addSeconds(since, +3600), replayContext);
+        }
+        @MemberSupport public java.sql.Timestamp defaultSince() {
+            return CommandExportManager.this.since;
+        }
+    }
+
+    @Action(
+            restrictTo = RestrictTo.PROTOTYPING,
+            semantics = SemanticsOf.SAFE,
+            commandPublishing = Publishing.DISABLED,
+            domainEvent = changeSince.DomainEvent.class,
+            executionPublishing = Publishing.DISABLED
+    )
+    @ActionLayout(
+            associateWith = "since", sequence = "2",
+            named = "Change",
+            position = ActionLayout.Position.PANEL
+    )
+    public class changeSince {
+        public class DomainEvent extends ActionDomainEvent<nextHour> { }
+        @MemberSupport public CommandExportManager act(final java.sql.Timestamp since) {
+            return new CommandExportManager(since, replayContext);
+        }
+        @MemberSupport public java.sql.Timestamp defaultSince() {
+            return CommandExportManager.this.since;
+        }
+    }
+
+    private static @NonNull Timestamp addSeconds(Timestamp since, int secondsToAdd) {
+        return Timestamp.from(since.toInstant().plusSeconds(secondsToAdd));
+    }
+
+
     // -- NOT YET EXPORTED
 
     @Collection
     @CollectionLayout(
             describedAs = "Commands that can be exported (since baseline)")
     public List<ReplayableCommand> getNotYetExported() {
-        return commandLogEntryRepository().findSinceAndCanBeExported(baseline).stream()
+        return commandLogEntryRepository().findSinceAndCanBeExported(since).stream()
             .map(entry->new ReplayableCommand(
                     entry.getInteractionId(),
                     replayContext))
             .collect(Collectors.toList());
     }
 
-    @Action(choicesFrom = "notYetExported", semantics = SemanticsOf.NON_IDEMPOTENT)
+    @Action(
+            restrictTo = RestrictTo.PROTOTYPING,
+            choicesFrom = "notYetExported",
+            semantics = SemanticsOf.NON_IDEMPOTENT,
+            commandPublishing = Publishing.DISABLED,
+            domainEvent = exportSelected.DomainEvent.class,
+            executionPublishing = Publishing.DISABLED
+    )
     @ActionLayout(associateWith = "notYetExported",
         sequence = "1.1",
         cssClassFa = "solid share-from-square",
@@ -99,9 +190,13 @@ public List<ReplayableCommand> getNotYetExported() {
         describedAs = "Exports selected Commands as zipped DTOs for import later. "
                 + "(You need to refresh the page to see changed states.)")
     public class exportSelected {
+        public class DomainEvent extends ActionDomainEvent<exportSelected> { }
 
-        @MemberSupport
-        public Blob act(final List<ReplayableCommand> selected) {
+        @MemberSupport public Blob act(
+                final List<ReplayableCommand> selected,
+                final String filenamePrefix,
+                final boolean filenameTimestamp
+                ) {
 
             var selectedCommandLogEntries = selected.stream()
                 .map(ReplayableCommand::commandLogEntry)
@@ -117,12 +212,14 @@ public Blob act(final List<ReplayableCommand> selected) {
                     .collect(Collectors.toList()));
 
             final var replayableCommand = selected.get(0);  // validate ensures there is at least one command
-            final var timestamp = replayableCommand.getTimestampIfAny()
-                    .map(ChronoZonedDateTime::toInstant)
-                    .map(Instant::toString)
-                    .map(x -> "." + x.replaceAll("[^A-Za-z0-9._-]", "_"))   // make safe within filename
-                    .orElse("");
-            final var filename = "commands" + timestamp + ".yaml";
+            final var timestamp = filenameTimestamp
+                    ? replayableCommand.getTimestampIfAny()
+                        .map(ChronoZonedDateTime::toInstant)
+                        .map(Instant::toString)
+                        .map(x -> "." + x.replaceAll("[^A-Za-z0-9._-]", "_"))   // make safe within filename
+                        .orElse("")
+                    : "";
+            final var filename = filenamePrefix + timestamp + ".yaml";
 
             var blob = Clob.of(filename, CommonMimeType.YAML, yaml)
                     .toBlobUtf8()
@@ -134,8 +231,19 @@ public Blob act(final List<ReplayableCommand> selected) {
             return blob;
         }
 
-        @MemberSupport
-        public String validateSelected(final List<ReplayableCommand> selected) {
+        @MemberSupport public String disableAct() {
+            return getNotYetExported().isEmpty() ? "No commands in collection" : null;
+        }
+
+        @MemberSupport public String defaultFilenamePrefix() {
+            return "commands";
+        }
+
+        @MemberSupport public boolean defaultFilenameTimestamp() {
+            return true;
+        }
+
+        @MemberSupport public String validateSelected(final List<ReplayableCommand> selected) {
             return selected != null && selected.isEmpty() ? "Select at least one command to export" : null;
         }
     }
@@ -147,26 +255,43 @@ public String validateSelected(final List<ReplayableCommand> selected) {
     @CollectionLayout(
             describedAs = "Commands that have been exported")
     public List<ReplayableCommand> getExported() {
-        return commandLogEntryRepository().findSinceAndHasBeenExported(baseline).stream()
+        return commandLogEntryRepository().findSinceAndHasBeenExported(since).stream()
             .map(entry->new ReplayableCommand(
                     entry.getInteractionId(),
                     replayContext))
             .collect(Collectors.toList());
     }
 
-    @Action(choicesFrom = "exported")
-    @ActionLayout(associateWith = "exported", sequence = "2.1",
-            describedAs = "Makes selected Commands exportable (again)")
+
+    @Action(
+            restrictTo = RestrictTo.PROTOTYPING,
+            choicesFrom = "exported",
+            commandPublishing = Publishing.DISABLED,
+            semantics =  SemanticsOf.IDEMPOTENT,
+            domainEvent = makeSelectedExportable.DomainEvent.class,
+            executionPublishing = Publishing.DISABLED
+    )
+    @ActionLayout(
+            associateWith = "exported", sequence = "2.1",
+            describedAs = "Makes selected Commands exportable (again)"
+    )
     public class makeSelectedExportable {
+        public class DomainEvent extends ActionDomainEvent<makeSelectedExportable> { }
+
         @MemberSupport
         public CommandExportManager act(final List<ReplayableCommand> selected) {
             selected.forEach(ReplayableCommand::makeExportable); // filtered on its own responsibility
             return CommandExportManager.this;
         }
 
+        @MemberSupport
+        public String disableAct() {
+            return getExported().isEmpty() ? "No commands in collection" : null;
+        }
+
         @MemberSupport
         public String validateSelected(final List<ReplayableCommand> selected) {
-            return selected != null && selected.isEmpty() ? "Select at least one command to export" : null;
+            return selected != null && selected.isEmpty() ? "Select at least one command to make exportable" : null;
         }
     }
 
@@ -175,28 +300,27 @@ public String validateSelected(final List<ReplayableCommand> selected) {
 
     @Override
     public String viewModelMemento() {
-        return toString(this.baseline);
+        return toString(this.since);
     }
 
     private static String toString(Timestamp ts) {
-        return ts.toInstant().toString();   // e.g. 2026-04-20T18:30:15.123Z
+        // Uses ISO-8601 shape, but replace ':' with '~' to avoid URL encoding noise.
+        return ts.toInstant().toString().replace(':', '~');   // e.g. 2026-04-20T18~30~15.123Z
     }
 
     private static Timestamp fromString(String s, Timestamp fallback) {
-        if(s == null) {
+        if (s == null) {
             return fallback;
         }
         try {
-            return Timestamp.from(Instant.parse(s));
+            return Timestamp.from(Instant.parse(s.replace('~', ':')));
         } catch (Exception e) {
             return fallback;
         }
     }
 
-
     // -- HELPER
     private CommandLogEntryRepository commandLogEntryRepository() {
         return replayContext.commandLogEntryRepository();
     }
-
 }
diff --git a/extensions/core/commandlog/applib/src/main/java/org/apache/causeway/extensions/commandlog/applib/dom/replay/CommandExportManager.layout.fallback.xml b/extensions/core/commandlog/applib/src/main/java/org/apache/causeway/extensions/commandlog/applib/dom/replay/CommandExportManager.layout.fallback.xml
index df20c9b73aa..74c6b4db908 100644
--- a/extensions/core/commandlog/applib/src/main/java/org/apache/causeway/extensions/commandlog/applib/dom/replay/CommandExportManager.layout.fallback.xml
+++ b/extensions/core/commandlog/applib/src/main/java/org/apache/causeway/extensions/commandlog/applib/dom/replay/CommandExportManager.layout.fallback.xml
@@ -3,6 +3,14 @@
         <bs:col span="8" unreferencedActions="true">
             <cpt:domainObject/>
         </bs:col>
+        <bs:col span="4">
+            <cpt:fieldSet name=" " id="since">
+                <cpt:action id="previousHour"/>
+                <cpt:action id="changeTo"/>
+                <cpt:action id="nextHour"/>
+                <cpt:property id="since"/>
+            </cpt:fieldSet>
+        </bs:col>
         <bs:col span="4">
             <bs:tabGroup>
                 <bs:tab name="Identity">
diff --git a/extensions/core/commandlog/applib/src/main/java/org/apache/causeway/extensions/commandlog/applib/dom/replay/CommandReplayManager#pendingOrFailed.columnOrder.fallback.txt b/extensions/core/commandlog/applib/src/main/java/org/apache/causeway/extensions/commandlog/applib/dom/replay/CommandReplayManager#pendingOrFailed.columnOrder.fallback.txt
new file mode 100644
index 00000000000..bde91a6f91d
--- /dev/null
+++ b/extensions/core/commandlog/applib/src/main/java/org/apache/causeway/extensions/commandlog/applib/dom/replay/CommandReplayManager#pendingOrFailed.columnOrder.fallback.txt
@@ -0,0 +1,6 @@
+#interactionId
+timestamp
+targetType
+targetId
+member
+replayState
\ No newline at end of file
diff --git a/extensions/core/commandlog/applib/src/main/java/org/apache/causeway/extensions/commandlog/applib/dom/replay/CommandReplayManager#succeededOrExcluded.columnOrder.fallback.txt b/extensions/core/commandlog/applib/src/main/java/org/apache/causeway/extensions/commandlog/applib/dom/replay/CommandReplayManager#succeededOrExcluded.columnOrder.fallback.txt
new file mode 100644
index 00000000000..bde91a6f91d
--- /dev/null
+++ b/extensions/core/commandlog/applib/src/main/java/org/apache/causeway/extensions/commandlog/applib/dom/replay/CommandReplayManager#succeededOrExcluded.columnOrder.fallback.txt
@@ -0,0 +1,6 @@
+#interactionId
+timestamp
+targetType
+targetId
+member
+replayState
\ No newline at end of file
diff --git a/extensions/core/commandlog/applib/src/main/java/org/apache/causeway/extensions/commandlog/applib/dom/replay/CommandReplayManager.java b/extensions/core/commandlog/applib/src/main/java/org/apache/causeway/extensions/commandlog/applib/dom/replay/CommandReplayManager.java
index eba8d52a3ea..0177d4cbb2a 100644
--- a/extensions/core/commandlog/applib/src/main/java/org/apache/causeway/extensions/commandlog/applib/dom/replay/CommandReplayManager.java
+++ b/extensions/core/commandlog/applib/src/main/java/org/apache/causeway/extensions/commandlog/applib/dom/replay/CommandReplayManager.java
@@ -32,9 +32,12 @@
 import org.apache.causeway.applib.annotation.DomainObject;
 import org.apache.causeway.applib.annotation.DomainObjectLayout;
 import org.apache.causeway.applib.annotation.Introspection;
+import org.apache.causeway.applib.annotation.MemberSupport;
 import org.apache.causeway.applib.annotation.ObjectSupport;
 import org.apache.causeway.applib.annotation.Parameter;
 import org.apache.causeway.applib.annotation.Programmatic;
+import org.apache.causeway.applib.annotation.Publishing;
+import org.apache.causeway.applib.annotation.RestrictTo;
 import org.apache.causeway.applib.annotation.SemanticsOf;
 import org.apache.causeway.applib.util.schema.CommandDtoUtils;
 import org.apache.causeway.applib.value.Blob;
@@ -44,8 +47,6 @@
 import org.apache.causeway.schema.cmd.v2.CommandDto;
 
 import lombok.AllArgsConstructor;
-import lombok.Getter;
-import lombok.experimental.Accessors;
 
 @DomainObject(introspection = Introspection.ANNOTATION_REQUIRED)
 @DomainObjectLayout(cssClassFa = "solid circle-play")
@@ -55,7 +56,10 @@ public final class CommandReplayManager implements ViewModel {
 
     public static final String LOGICAL_TYPE_NAME = CausewayModuleExtCommandLogApplib.NAMESPACE + ".CommandReplayManager";
 
-	private final ReplayContext replayContext;
+    public static abstract class ActionDomainEvent<T>
+            extends CausewayModuleExtCommandLogApplib.ActionDomainEvent<T> { }
+
+    private final ReplayContext replayContext;
     @Programmatic
     public ReplayContext replayContext() { return replayContext; }
 
@@ -71,22 +75,33 @@ public CommandReplayManager(
         return "Command Replay Manager";
     }
 
-    @Action
+
+    @Action(
+            restrictTo = RestrictTo.PROTOTYPING,
+            semantics = SemanticsOf.IDEMPOTENT,
+            commandPublishing = Publishing.DISABLED,
+            domainEvent = importCommands.DomainEvent.class,
+            executionPublishing = Publishing.DISABLED
+    )
     @ActionLayout(
             sequence = "0.1",
             associateWith = "pendingOrFailed",
             cssClass = "btn-primary",
-            describedAs = "Imports commands from a zipped yaml, then persists them with replayState=PENDING.")
-    public CommandReplayManager importCommands(
-            @Parameter(fileAccept = ".zip")
-            final Blob zippedCommandsYaml) {
+            describedAs = "Imports commands from a zipped yaml, then persists them with replayState=PENDING."
+    )
+    public class importCommands {
+        public class DomainEvent extends ActionDomainEvent<importCommands> { }
+        public CommandReplayManager act(
+                @Parameter(fileAccept = ".zip")
+                final Blob zippedCommandsYaml) {
 
-        var yamlDs = zippedCommandsYaml.unZip(CommonMimeType.YAML).asDataSource();
+            var yamlDs = zippedCommandsYaml.unZip(CommonMimeType.YAML).asDataSource();
 
-        final List<CommandDto> commandDtos = CommandDtoUtils.fromYaml(yamlDs);
-        commandDtos.forEach(commandLogEntryRepository()::saveForReplay);
+            final List<CommandDto> commandDtos = CommandDtoUtils.fromYaml(yamlDs);
+            commandDtos.forEach(commandLogEntryRepository()::saveForReplay);
 
-        return this;
+            return CommandReplayManager.this;
+        }
     }
 
 
@@ -94,7 +109,8 @@ public CommandReplayManager importCommands(
 
     @Collection
     @CollectionLayout(
-            describedAs = "Imported Commands that can be either replayed (replayState=PENDING) or retried (when replayState=FAILED)")
+            describedAs = "Imported Commands that can be either replayed (replayState=PENDING) or retried (when replayState=FAILED)"
+    )
     public List<ReplayableCommand> getPendingOrFailed() {
         return commandLogEntryRepository().findReplayPendingOrFailed().stream()
             .map(entry->new ReplayableCommand(
@@ -103,51 +119,96 @@ public List<ReplayableCommand> getPendingOrFailed() {
             .collect(Collectors.toList());
     }
 
-    @Action(choicesFrom = "pendingOrFailed")
-    @ActionLayout(associateWith = "pendingOrFailed",
-        sequence = "1.1",
-        cssClassFa = "solid circle-play",
-        cssClass = "btn-primary",
-        describedAs = "Executes the list of commands in sequence, after having sorted them by their timestamp. "
-                + "If any of the given commands fails, "
-                + "the surrounding transaction is rolled back and any successful commands are undone). "
-                + "The command, that caused the failure, gets marked as FAILED.")
-    public CommandReplayManager replayOrRetrySelected(final List<ReplayableCommand> selected) {
-        var replayables = selected.stream()
-            .sorted()
-            .collect(Collectors.toList());
-        for(var replayableCommand : replayables) {
-            var tryReplayOrRetry = replayableCommand.tryReplayOrRetry(); // filtered on its own responsibility
-            if(tryReplayOrRetry.isFailure())
-                return this; // stop further execution
+
+    @Action(
+            restrictTo = RestrictTo.PROTOTYPING,
+            choicesFrom = "pendingOrFailed",
+            semantics = SemanticsOf.NON_IDEMPOTENT,
+            commandPublishing = Publishing.DISABLED,
+            domainEvent = replayOrRetrySelected.DomainEvent.class,
+            executionPublishing = Publishing.DISABLED
+    )
+    @ActionLayout(
+            associateWith = "pendingOrFailed", sequence = "1.1",
+            cssClassFa = "solid circle-play",
+            cssClass = "btn-primary",
+            describedAs = "Executes the list of commands in sequence, after having sorted them by their timestamp. "
+                    + "If any of the given commands fails, "
+                    + "the surrounding transaction is rolled back and any successful commands are undone). "
+                    + "The command, that caused the failure, gets marked as FAILED."
+    )
+    public class replayOrRetrySelected {
+        public class DomainEvent extends ActionDomainEvent<replayOrRetrySelected> { }
+        @MemberSupport public CommandReplayManager act(final List<ReplayableCommand> selected) {
+            var replayables = selected.stream()
+                .sorted()
+                .collect(Collectors.toList());
+            for(var replayableCommand : replayables) {
+                var tryReplayOrRetry = replayableCommand.tryReplayOrRetry(); // filtered on its own responsibility
+                if(tryReplayOrRetry.isFailure()) {
+                    return CommandReplayManager.this; // stop further execution
+                }
+            }
+            return CommandReplayManager.this;
         }
-        return this;
     }
 
-    @Action(choicesFrom = "pendingOrFailed")
-    @ActionLayout(associateWith = "pendingOrFailed", sequence = "1.2",
-            describedAs = "Marks selected Commands to be EXCLUDED from replay")
-    public CommandReplayManager excludeSelectedFromReplay(final List<ReplayableCommand> selected) {
-        selected.stream()
-            .forEach(ReplayableCommand::excludeFromReplay); // filtered on its own responsibility
-        return this;
+
+
+    @Action(
+            restrictTo = RestrictTo.PROTOTYPING,
+            choicesFrom = "pendingOrFailed",
+            semantics = SemanticsOf.NON_IDEMPOTENT,
+            commandPublishing = Publishing.DISABLED,
+            domainEvent = excludeSelectedFromReplay.DomainEvent.class,
+            executionPublishing = Publishing.DISABLED
+    )
+    @ActionLayout(
+            associateWith = "pendingOrFailed", sequence = "1.2",
+            describedAs = "Marks selected Commands to be EXCLUDED from replay"
+    )
+    public class excludeSelectedFromReplay {
+        public class DomainEvent extends ActionDomainEvent<excludeSelectedFromReplay> { }
+        @MemberSupport
+        public CommandReplayManager act(final List<ReplayableCommand> selected) {
+            selected.stream()
+                .forEach(ReplayableCommand::excludeFromReplay); // filtered on its own responsibility
+            return CommandReplayManager.this;
+        }
     }
 
-    @Action(choicesFrom = "pendingOrFailed")
-    @ActionLayout(associateWith = "pendingOrFailed", sequence = "1.3",
-            describedAs = "Deletes selected Commands (cannot be undone)")
-    public CommandReplayManager deleteSelected(final List<ReplayableCommand> selected) {
-        selected.stream()
-            .forEach(ReplayableCommand::delete); // filtered on its own responsibility
-        return this;
+
+
+    @Action(
+            restrictTo = RestrictTo.PROTOTYPING,
+            choicesFrom = "pendingOrFailed",
+            semantics = SemanticsOf.NON_IDEMPOTENT,
+            commandPublishing = Publishing.DISABLED,
+            domainEvent = deleteSelectedPendingOrFailed.DomainEvent.class,
+            executionPublishing = Publishing.DISABLED
+    )
+    @ActionLayout(
+            associateWith = "pendingOrFailed", sequence = "1.3",
+            describedAs = "Deletes selected Commands (cannot be undone)"
+    )
+    public class deleteSelectedPendingOrFailed {
+        public class DomainEvent extends ActionDomainEvent<deleteSelectedPendingOrFailed> { }
+        public CommandReplayManager act(final List<ReplayableCommand> selected) {
+            selected.stream()
+                .forEach(ReplayableCommand::delete); // filtered on its own responsibility
+            return CommandReplayManager.this;
+        }
     }
 
+
+
     // -- OK OR EXCLUDE
 
     @Collection
     @CollectionLayout(
             describedAs = "Imported Commands that were either replayed with success (replayState=OK) "
-                    + "or marked to be excluded from replay (replayState=EXCLUDE)")
+                    + "or marked to be excluded from replay (replayState=EXCLUDE)"
+    )
     public List<ReplayableCommand> getSucceededOrExcluded() {
         return commandLogEntryRepository().findReplaySucceededOrExcluded().stream()
             .map(entry->new ReplayableCommand(
@@ -156,16 +217,30 @@ public List<ReplayableCommand> getSucceededOrExcluded() {
             .collect(Collectors.toList());
     }
 
-    @Action(choicesFrom = "succeededOrExcluded")
-    @ActionLayout(associateWith = "succeededOrExcluded",
+
+    @Action(
+            restrictTo = RestrictTo.PROTOTYPING,
+            choicesFrom = "succeededOrExcluded",
+            semantics = SemanticsOf.IDEMPOTENT,
+            domainEvent = deleteSelectedSucceededOrExcluded.DomainEvent.class,
+            executionPublishing = Publishing.DISABLED
+    )
+    @ActionLayout(
+            associateWith = "succeededOrExcluded",
             named = "Delete Selected",
-            describedAs = "Deletes selected Commands (cannot be undone)")
-    public CommandReplayManager deleteSelected2(final List<ReplayableCommand> selected) {
-        selected.stream()
-            .forEach(ReplayableCommand::delete); // filtered on its own responsibility
-        return this;
+            describedAs = "Deletes selected Commands (cannot be undone)"
+    )
+    public class deleteSelectedSucceededOrExcluded {
+        public class DomainEvent extends ActionDomainEvent<deleteSelectedSucceededOrExcluded> { }
+        public CommandReplayManager act(final List<ReplayableCommand> selected) {
+            selected.stream()
+                .forEach(ReplayableCommand::delete); // filtered on its own responsibility
+            return CommandReplayManager.this;
+        }
     }
 
+
+
     // -- VM STATE
 
     @Override
@@ -179,5 +254,4 @@ public String viewModelMemento() {
     private CommandLogEntryRepository commandLogEntryRepository() {
         return replayContext.commandLogEntryRepository();
     }
-
 }
diff --git a/extensions/core/commandlog/applib/src/main/java/org/apache/causeway/extensions/commandlog/applib/dom/replay/ReplayableCommand.columnOrder.fallback.txt b/extensions/core/commandlog/applib/src/main/java/org/apache/causeway/extensions/commandlog/applib/dom/replay/ReplayableCommand.columnOrder.fallback.txt
new file mode 100644
index 00000000000..bde91a6f91d
--- /dev/null
+++ b/extensions/core/commandlog/applib/src/main/java/org/apache/causeway/extensions/commandlog/applib/dom/replay/ReplayableCommand.columnOrder.fallback.txt
@@ -0,0 +1,6 @@
+#interactionId
+timestamp
+targetType
+targetId
+member
+replayState
\ No newline at end of file
diff --git a/extensions/core/commandlog/applib/src/main/java/org/apache/causeway/extensions/commandlog/applib/dom/replay/ReplayableCommand.java b/extensions/core/commandlog/applib/src/main/java/org/apache/causeway/extensions/commandlog/applib/dom/replay/ReplayableCommand.java
index 6ab05399b62..48c044edd67 100644
--- a/extensions/core/commandlog/applib/src/main/java/org/apache/causeway/extensions/commandlog/applib/dom/replay/ReplayableCommand.java
+++ b/extensions/core/commandlog/applib/src/main/java/org/apache/causeway/extensions/commandlog/applib/dom/replay/ReplayableCommand.java
@@ -18,7 +18,9 @@
  */
 package org.apache.causeway.extensions.commandlog.applib.dom.replay;
 
+import java.time.Instant;
 import java.time.ZonedDateTime;
+import java.time.chrono.ChronoZonedDateTime;
 import java.util.Comparator;
 import java.util.Optional;
 import java.util.UUID;
@@ -39,6 +41,9 @@
 import org.apache.causeway.applib.annotation.Programmatic;
 import org.apache.causeway.applib.annotation.Property;
 import org.apache.causeway.applib.annotation.PropertyLayout;
+import org.apache.causeway.applib.annotation.Publishing;
+import org.apache.causeway.applib.annotation.RestrictTo;
+import org.apache.causeway.applib.annotation.SemanticsOf;
 import org.apache.causeway.applib.annotation.Where;
 import org.apache.causeway.applib.jaxb.JavaTimeXMLGregorianCalendarMarshalling;
 import org.apache.causeway.applib.services.command.CommandExecutorService.InteractionContextPolicy;
@@ -72,7 +77,10 @@
 @AllArgsConstructor
 public final class ReplayableCommand implements ViewModel, Comparable<ReplayableCommand> {
 
-	private final UUID interactionId;
+    public static abstract class ActionDomainEvent<T>
+            extends CausewayModuleExtCommandLogApplib.ActionDomainEvent<T> { }
+
+    private final UUID interactionId;
     @Programmatic
     public UUID interactionId() { return interactionId; }
 
@@ -136,7 +144,8 @@ public ReplayableCommand(
     }
 
     @ObjectSupport public String title() {
-        return "Replayable Command";
+        final var timestamp = getTimestampIfAny().map(ChronoZonedDateTime::toInstant).map(Instant::toString).map(x -> " @ " + x).orElse("");
+        return getTargetType() + ":" + getTargetId() + " #" + getMember() + timestamp;
     }
 
 //requires v4
@@ -256,96 +265,176 @@ public AsciiDoc getDto() {
 
     // -- ACTIONS
 
-    @Action
+    @Action(
+            semantics = SemanticsOf.SAFE,
+            commandPublishing = Publishing.DISABLED,
+            domainEvent = openCommandLogEntry.DomainEvent.class,
+            executionPublishing = Publishing.DISABLED
+    )
     @ActionLayout(
             sequence = "0.2",
-            describedAs = "Opens the associated Command Log Entry")
-    public CommandLogEntry openCommandLogEntry() {
-        return commandLogEntry()
-                .orElse(null);
+            describedAs = "Opens the associated Command Log Entry"
+    )
+    public class openCommandLogEntry {
+        public class DomainEvent extends ActionDomainEvent<openCommandLogEntry> { }
+        @MemberSupport public CommandLogEntry act() {
+            return commandLogEntry()
+                    .orElse(null);
+        }
+        @MemberSupport public String disableAct() {
+            return commandLogEntry().isEmpty() ? "No corresponding CommandLogEntry" : null;
+        }
     }
 
-    @Action
+
+    @Action(
+            restrictTo = RestrictTo.PROTOTYPING,
+            semantics = SemanticsOf.NON_IDEMPOTENT,
+            commandPublishing = Publishing.DISABLED,
+            domainEvent = replayOrRetry.DomainEvent.class,
+            executionPublishing = Publishing.DISABLED
+    )
     @ActionLayout(
             sequence = "0.1",
             cssClassFa = "solid circle-play",
-            cssClass = "btn-primary")
-            //hidden = Where.NOWHERE) // show in tables //TODO NPE bug
-    public ReplayableCommand replayOrRetry() {
-        tryReplayOrRetry();
-        return this;
+            cssClass = "btn-primary"
+            //hidden = Where.NOWHERE // show in tables //TODO NPE bug
+    )
+    public class replayOrRetry {
+        public class DomainEvent extends ActionDomainEvent<replayOrRetry> { }
+        @MemberSupport public ReplayableCommand act() {
+            tryReplayOrRetry();
+            return ReplayableCommand.this;
+        }
+        @MemberSupport public String disableAct() {
+            return disableReplayOrRetry();
+        }
     }
-    @MemberSupport public String disableReplayOrRetry() {
+    private String disableReplayOrRetry() {
         return commandRecord()
-            .map(CommandRecord::canReplayOrRetryOrMarkForExclusion)
-            .orElse(false)
+                .map(CommandRecord::canReplayOrRetryOrMarkForExclusion)
+                .orElse(false)
                 ? null
                 : "Cannot replay, if neither PENDING nor FAILED";
     }
 
-    @Action
+
+    @Action(
+            restrictTo = RestrictTo.PROTOTYPING,
+            semantics = SemanticsOf.IDEMPOTENT,
+            commandPublishing = Publishing.DISABLED,
+            domainEvent = makeExportable.DomainEvent.class,
+            executionPublishing = Publishing.DISABLED
+    )
     @ActionLayout(
             //hidden = Where.NOWHERE, // show in tables //TODO NPE bug
-            sequence = "2.1",
             associateWith = "replayState",
-            describedAs = "Makes Command exportable (again)")
-    public ReplayableCommand makeExportable() {
-        if(disableMakeExportable()!=null)
-            return this; // safe guard when called programmatically
+            sequence = "2.1",
+            describedAs = "Makes Command exportable (again)"
+    )
+    public class makeExportable {
+        public class DomainEvent extends ActionDomainEvent<makeExportable> { }
+
+        @MemberSupport public ReplayableCommand act() {
+            return makeExportable();
+        }
+        @MemberSupport public String disableAct() {
+            return disableMakeExportable();
+        }
+    }
+
+    ReplayableCommand makeExportable() {
+        if(disableMakeExportable()!=null) {
+            return this; // safeguard when called programmatically
+        }
         commandLogEntry()
-            .filter(commandLogEntry->ReplayState.isExported(commandLogEntry.getReplayState()))
-            .ifPresent(commandLogEntry->{
-                commandLogEntry.setReplayState(ReplayState.UNDEFINED);
-                invalidateCachedRecord();
-            });
+                .filter(commandLogEntry->ReplayState.isExported(commandLogEntry.getReplayState()))
+                .ifPresent(commandLogEntry->{
+                    commandLogEntry.setReplayState(ReplayState.UNDEFINED);
+                    invalidateCachedRecord();
+                });
         return this;
     }
-    @MemberSupport public String disableMakeExportable() {
+    String disableMakeExportable() {
         return commandRecord()
-            .map(rec->ReplayState.isExported(rec.replayState()))
-            .orElse(false)
+                .map(rec->ReplayState.isExported(rec.replayState()))
+                .orElse(false)
                 ? null
                 : "Cannot make exportable, if not EXPORTED";
     }
 
-    @Action
+
+    @Action(
+            restrictTo = RestrictTo.PROTOTYPING,
+            commandPublishing = Publishing.DISABLED,
+            domainEvent = excludeFromReplay.DomainEvent.class,
+            executionPublishing = Publishing.DISABLED
+    )
     @ActionLayout(
             //hidden = Where.NOWHERE, // show in tables //TODO NPE bug
             sequence = "2.2",
             associateWith = "replayState",
-            describedAs = "Marks Command to be EXCLUDED from replay.")
-    public ReplayableCommand excludeFromReplay() {
-        if(disableExcludeFromReplay()!=null)
-            return this; // safe guard when called programmatically
+            describedAs = "Marks Command to be EXCLUDED from replay."
+    )
+    public class excludeFromReplay {
+        public class DomainEvent extends ActionDomainEvent<excludeFromReplay> { }
+
+        @MemberSupport public ReplayableCommand act() {
+            return excludeFromReplay();
+        }
+        @MemberSupport private String disableAct() {
+            return disableExcludeFromReplay();
+        }
+    }
+
+    ReplayableCommand excludeFromReplay() {
+        if(disableExcludeFromReplay()!=null) {
+            return ReplayableCommand.this; // safeguard when called programmatically
+        }
         commandLogEntry()
-            .filter(ReplayableCommand::canReplayOrRetryOrMarkForExclusion)
-            .ifPresent(commandLogEntry->{
-                commandLogEntry.setReplayState(ReplayState.EXCLUDED);
-                invalidateCachedRecord();
-            });
-        return this;
+                .filter(ReplayableCommand::canReplayOrRetryOrMarkForExclusion)
+                .ifPresent(commandLogEntry->{
+                    commandLogEntry.setReplayState(ReplayState.EXCLUDED);
+                    invalidateCachedRecord();
+                });
+        return ReplayableCommand.this;
     }
-    @MemberSupport private String disableExcludeFromReplay() {
+    private String disableExcludeFromReplay() {
         return commandRecord()
-            .map(CommandRecord::canReplayOrRetryOrMarkForExclusion)
-            .orElse(false)
+                .map(CommandRecord::canReplayOrRetryOrMarkForExclusion)
+                .orElse(false)
                 ? null
                 : "Cannot mark for exclusion, if neither PENDING nor FAILED";
     }
 
-    @Action
+
+    @Action(
+            restrictTo = RestrictTo.PROTOTYPING,
+            semantics =  SemanticsOf.NON_IDEMPOTENT,
+            commandPublishing = Publishing.DISABLED,
+            domainEvent = delete.DomainEvent.class,
+            executionPublishing = Publishing.DISABLED
+    )
     @ActionLayout(
             sequence = "0.3",
             //hidden = Where.NOWHERE, // show in tables //TODO NPE bug
-            describedAs = "Deletes the associated Command Log Entry (cannot be undone)")
-    public void delete() {
+            describedAs = "Deletes the associated Command Log Entry (cannot be undone)"
+    )
+    public class delete {
+        public class DomainEvent extends ActionDomainEvent<delete> { }
+        @MemberSupport public void act() {
+            delete();
+        }
+    }
+    void delete() {
         commandLogEntry()
-            .ifPresent(commandLogEntry->{
-                replayContext.repositoryService().remove(commandLogEntry);
-                invalidateCachedRecord();
-            });
+                .ifPresent(commandLogEntry->{
+                    replayContext.repositoryService().remove(commandLogEntry);
+                    invalidateCachedRecord();
+                });
     }
 
+
     // -- EXECUTION ORDER GOVERNED BY TIMESTAMP
 
     private static final Comparator<ReplayableCommand> TIMESTAMP_COMPARATOR =
@@ -366,8 +455,9 @@ public String viewModelMemento() {
     // -- UTIL
 
     Try<ReplayableCommand> tryReplayOrRetry() {
-        if(disableReplayOrRetry()!=null)
+        if(disableReplayOrRetry()!=null) {
             return Try.success(null); // guard against disallowed invocation
+        }
         return commandLogEntry()
             .filter(ReplayableCommand::canReplayOrRetryOrMarkForExclusion)
             .map(commandLogEntry->{
@@ -420,5 +510,4 @@ private void onReplayError(final UUID interactionId, final Throwable ex) {
                 replayContext.commandLogEntryRepository().findByInteractionId(interactionId)
                     .ifPresent(entry->entry.saveAnalysis(ex.toString()))));
     }
-
-}
+}
\ No newline at end of file
diff --git a/extensions/core/commandlog/applib/src/main/java/org/apache/causeway/extensions/commandlog/applib/dom/replay/ReplayableCommand.layout.fallback.xml b/extensions/core/commandlog/applib/src/main/java/org/apache/causeway/extensions/commandlog/applib/dom/replay/ReplayableCommand.layout.fallback.xml
index 0b6c5fee4d5..4191657298a 100644
--- a/extensions/core/commandlog/applib/src/main/java/org/apache/causeway/extensions/commandlog/applib/dom/replay/ReplayableCommand.layout.fallback.xml
+++ b/extensions/core/commandlog/applib/src/main/java/org/apache/causeway/extensions/commandlog/applib/dom/replay/ReplayableCommand.layout.fallback.xml
@@ -1,14 +1,22 @@
 <?xml version="1.0" encoding="UTF-8"?><bs:grid xmlns:bs="https://causeway.apache.org/applib/layout/grid/bootstrap3" xmlns:lnk="https://causeway.apache.org/applib/layout/links" xmlns:cpt="https://causeway.apache.org/applib/layout/component" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="https://causeway.apache.org/applib/layout/component https://causeway.apache.org/applib/layout/component/component.xsd https://causeway.apache.org/applib/layout/links https://caus [...]
     <bs:row>
-        <bs:col span="8" unreferencedActions="true">
+        <bs:col span="12" unreferencedActions="true">
             <cpt:domainObject/>
         </bs:col>
-        <bs:col span="4">
+    </bs:row>
+    <bs:row>
+        <bs:col span="6">
             <bs:tabGroup>
                 <bs:tab name="Identity">
                     <bs:row>
                         <bs:col span="12">
-                            <cpt:fieldSet name="Identity" id="identity"/>
+                            <cpt:fieldSet name="Id" id="id">
+                                <cpt:action id="delete" position="PANEL"/>
+                                <cpt:property id="interactionId">
+                                    <cpt:action id="openCommandLogEntry"/>
+                                </cpt:property>
+                                <cpt:property id="timestamp"/>
+                            </cpt:fieldSet>
                         </bs:col>
                     </bs:row>
                 </bs:tab>
@@ -22,16 +30,23 @@
                 <bs:tab name="Metadata">
                     <bs:row>
                         <bs:col span="12">
-                            <cpt:fieldSet name="Metadata" id="metadata"/>
+                            <cpt:fieldSet name="Metadata" id="metadata">
+                                <cpt:property id="logicalTypeName"/>
+                                <cpt:property id="interactionId"/>
+                            </cpt:fieldSet>
                         </bs:col>
                     </bs:row>
                 </bs:tab>
             </bs:tabGroup>
-        </bs:col>
-    </bs:row>
-    <bs:row>
-        <bs:col span="6">
-            <cpt:fieldSet name="Details" id="details"/>
+            <cpt:fieldSet name="Target" id="target">
+                <cpt:property id="targetType"/>
+                <cpt:property id="targetId"/>
+                <cpt:property id="member"/>
+            </cpt:fieldSet>
+            <cpt:fieldSet name="Control" id="control">
+                <cpt:action id="replayOrRetry" position="PANEL"/>
+                <cpt:property id="replayState"/>
+            </cpt:fieldSet>
         </bs:col>
         <bs:col span="6">
             <cpt:fieldSet name="Command Dto" id="dto"/>