diff --git a/infra/schema/src/main/java/com/evolveum/midpoint/schema/util/task/ActivityBasedTaskInformation.java b/infra/schema/src/main/java/com/evolveum/midpoint/schema/util/task/ActivityBasedTaskInformation.java
index 0abf34ebebf..55ff8e9a9fb 100644
--- a/infra/schema/src/main/java/com/evolveum/midpoint/schema/util/task/ActivityBasedTaskInformation.java
+++ b/infra/schema/src/main/java/com/evolveum/midpoint/schema/util/task/ActivityBasedTaskInformation.java
@@ -79,13 +79,11 @@ private ActivityBasedTaskInformation(
if (activityStateOverview != null) {
ActivityWorkersInformation workers =
ActivityWorkersInformation.fromActivityStateOverview(activityStateOverview);
- ActivityProgressInformation progress = ActivityProgressInformation.fromRootTask(rootTask, FULL_STATE_PREFERRED)
- .find(activityPath);
return new ActivityBasedTaskInformation(
task,
workers,
computeStatus(activityStateOverview, task.getResultStatus(), workers),
- progress != null ? progress : ActivityProgressInformation.unknown(activityPath),
+ selectSubtaskProgress(task, rootTask, activityPath, activityStateOverview),
getLocalRootActivityState(task));
} else {
return new ActivityBasedTaskInformation(
@@ -97,6 +95,90 @@ private ActivityBasedTaskInformation(
}
}
+ /**
+ * Selects progress information suitable for displaying a subtask row.
+ *
+ * Worker subtasks are special: using the root activity progress would show the
+ * coordinator/aggregate progress for every worker. For workers, use the per-worker
+ * item progress from the activity overview. Non-worker subtasks keep the original
+ * root-task progress lookup.
+ */
+ private static @NotNull ActivityProgressInformation selectSubtaskProgress(
+ @NotNull TaskType task,
+ @NotNull TaskType rootTask,
+ @NotNull ActivityPath activityPath,
+ @NotNull ActivityStateOverviewType activityStateOverview) {
+
+ if (isWorkerTask(task)) {
+ ActivityProgressInformation workerItemsProgress =
+ createWorkerProgressFromOverview(task, activityPath, activityStateOverview);
+ return workerItemsProgress != null ?
+ workerItemsProgress : ActivityProgressInformation.unknown(activityPath);
+ }
+
+ ActivityProgressInformation progress = ActivityProgressInformation.fromRootTask(rootTask, FULL_STATE_PREFERRED)
+ .find(activityPath);
+ return progress != null ? progress : ActivityProgressInformation.unknown(activityPath);
+ }
+
+ private static boolean isWorkerTask(@NotNull TaskType task) {
+ ActivityBucketingStateType bucketing = getLocalRootActivityState(task).getBucketing();
+ return bucketing != null
+ && bucketing.getBucketsProcessingRole() == BucketsProcessingRoleType.WORKER;
+ }
+
+ /**
+ * Creates progress information for a worker row from the worker entry in the activity overview.
+ *
+ * The overview entry contains worker-local item counters. Converting them to
+ * {@link ActivityProgressInformation} lets the GUI display processed object counts
+ * for the worker instead of the coordinator's aggregate progress.
+ */
+ private static @Nullable ActivityProgressInformation createWorkerProgressFromOverview(
+ @NotNull TaskType task,
+ @NotNull ActivityPath activityPath,
+ @NotNull ActivityStateOverviewType activityStateOverview) {
+ ItemsProgressInformation itemsProgress = findWorkerItemsProgress(task, activityStateOverview);
+ if (itemsProgress == null) {
+ return null;
+ }
+
+ ActivityStateType localState = getLocalRootActivityState(task);
+ return new ActivityProgressInformation(
+ localState.getIdentifier(),
+ activityPath,
+ localState.getDisplayOrder(),
+ ActivityProgressInformation.RealizationState.fromFullState(localState.getRealizationState()),
+ null,
+ itemsProgress,
+ null);
+ }
+
+ /**
+ * Finds item progress for the given worker task in the activity overview.
+ *
+ * The worker entry is matched by task OID. Its progress contains worker-local
+ * item/object counters, such as successfully processed, failed, skipped, and
+ * optionally expected total.
+ *
+ * @return item progress for the worker, or {@code null} if no matching progress is available
+ */
+ private static @Nullable ItemsProgressInformation findWorkerItemsProgress(
+ @NotNull TaskType task, @NotNull ActivityStateOverviewType activityStateOverview) {
+ String oid = task.getOid();
+ if (oid == null) {
+ return null;
+ }
+
+ return activityStateOverview.getTask().stream()
+ .filter(taskOverview -> taskOverview.getTaskRef() != null)
+ .filter(taskOverview -> Objects.equals(taskOverview.getTaskRef().getOid(), oid))
+ .map(ItemsProgressInformation::fromTaskOverview)
+ .filter(Objects::nonNull)
+ .findFirst()
+ .orElse(null);
+ }
+
@Override
public String getProgressDescription(boolean longForm) {
return progressInformation.toHumanReadableString(longForm);
@@ -109,6 +191,16 @@ public double getProgress() {
return 1.0;
}
+ // Use direct item progress for leaf activities. If there is no expected total,
+ // return -1 so the GUI shows only the textual progress label.
+ if (progressInformation.getChildren().isEmpty()) {
+ ItemsProgressInformation itemsProgress = progressInformation.getItemsProgress();
+ if (itemsProgress != null) {
+ float percentage = itemsProgress.getPercentage();
+ return Float.isNaN(percentage) ? -1 : percentage;
+ }
+ }
+
// We need to list only leaf activities. Then compare them to the completed ones.
// Current drawback is that activities and their sub activities are created when they
// start, not during the task creation. This means that progress is not accurate for
diff --git a/infra/schema/src/main/java/com/evolveum/midpoint/schema/util/task/ActivityProgressInformation.java b/infra/schema/src/main/java/com/evolveum/midpoint/schema/util/task/ActivityProgressInformation.java
index 2083fc51b88..66ccb0f30e5 100644
--- a/infra/schema/src/main/java/com/evolveum/midpoint/schema/util/task/ActivityProgressInformation.java
+++ b/infra/schema/src/main/java/com/evolveum/midpoint/schema/util/task/ActivityProgressInformation.java
@@ -105,7 +105,7 @@ public class ActivityProgressInformation implements DebugDumpable, Serializable
/** Identifier is estimated from the path. Use only if it needs not be precise. */
static @NotNull ActivityProgressInformation unknown(ActivityPath activityPath) {
return unknown(
- activityPath.isEmpty() ? activityPath.last() : null,
+ activityPath.isEmpty() ? null : activityPath.last(),
activityPath);
}
diff --git a/infra/schema/src/main/java/com/evolveum/midpoint/schema/util/task/ItemsProgressInformation.java b/infra/schema/src/main/java/com/evolveum/midpoint/schema/util/task/ItemsProgressInformation.java
index fd821edc1eb..d77a93b05d7 100644
--- a/infra/schema/src/main/java/com/evolveum/midpoint/schema/util/task/ItemsProgressInformation.java
+++ b/infra/schema/src/main/java/com/evolveum/midpoint/schema/util/task/ItemsProgressInformation.java
@@ -55,6 +55,18 @@ private ItemsProgressInformation(int progress, int errors, Integer expectedProgr
return accumulator.toProgressInformation();
}
+ /**
+ * Creates item progress information from a worker entry in the parent activity state overview.
+ *
+ * The entry contains worker-local item counters. Using these counters lets the GUI display
+ * processed object counts for worker rows instead of the parent/coordinator aggregate progress.
+ */
+ static @Nullable ItemsProgressInformation fromTaskOverview(@NotNull ActivityTaskStateOverviewType overview) {
+ Accumulator accumulator = new Accumulator();
+ accumulator.add(overview.getProgress());
+ return accumulator.toProgressInformation();
+ }
+
static ItemsProgressInformation fromFullState(@NotNull ActivityStateType state,
@NotNull ActivityPath activityPath, @NotNull TaskType task, @NotNull TaskResolver resolver) {
if (BucketingUtil.isCoordinator(state)) {
diff --git a/infra/schema/src/test/java/com/evolveum/midpoint/schema/util/TestTaskInformation.java b/infra/schema/src/test/java/com/evolveum/midpoint/schema/util/TestTaskInformation.java
new file mode 100644
index 00000000000..54e3e954ec9
--- /dev/null
+++ b/infra/schema/src/test/java/com/evolveum/midpoint/schema/util/TestTaskInformation.java
@@ -0,0 +1,114 @@
+/*
+ * Copyright (c) 2026 Evolveum and contributors
+ *
+ * Licensed under the EUPL-1.2 or later.
+ */
+
+package com.evolveum.midpoint.schema.util;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+import org.testng.annotations.Test;
+
+import com.evolveum.midpoint.schema.AbstractSchemaTest;
+import com.evolveum.midpoint.schema.util.task.ActivityBasedTaskInformation;
+import com.evolveum.midpoint.schema.util.task.ActivityProgressInformation;
+import com.evolveum.midpoint.schema.util.task.TaskInformation;
+import com.evolveum.midpoint.xml.ns._public.common.common_3.*;
+
+public class TestTaskInformation extends AbstractSchemaTest {
+
+ private static final String ROOT_OID = "00000000-0000-0000-0000-000000000001";
+ private static final String WORKER_1_OID = "00000000-0000-0000-0000-000000000101";
+ private static final String WORKER_2_OID = "00000000-0000-0000-0000-000000000102";
+
+ /**
+ * Verifies that worker task progress is taken from the matching worker item overview.
+ *
+ * The root activity overview contains aggregate bucket progress, but worker rows should
+ * display worker-local processed-object counts instead of the coordinator progress.
+ */
+ @Test
+ public void testWorkerProgressUsesWorkerItemOverview() {
+ TaskType root = createRootTaskWithWorkerOverview();
+ TaskType worker = createWorkerTask(WORKER_1_OID);
+
+ TaskInformation information = TaskInformation.createForTask(worker, root);
+
+ assertThat(information).isInstanceOf(ActivityBasedTaskInformation.class);
+ assertThat(information.getProgressDescriptionShort())
+ .as("worker progress label")
+ .isEqualTo("5");
+ assertThat(information.getProgress())
+ .as("worker progress bar value")
+ .isEqualTo(-1);
+
+ ActivityProgressInformation progressInformation =
+ ((ActivityBasedTaskInformation) information).getProgressInformation();
+ assertThat(progressInformation.getBucketsProgress())
+ .as("worker bucket progress")
+ .isNull();
+ assertThat(progressInformation.getItemsProgress())
+ .as("worker item progress")
+ .isNotNull();
+ assertThat(progressInformation.getItemsProgress().getProgress())
+ .as("worker processed items")
+ .isEqualTo(5);
+ assertThat(progressInformation.getItemsProgress().getErrors())
+ .as("worker failed items")
+ .isEqualTo(1);
+
+ TaskInformation information2 = TaskInformation.createForTask(createWorkerTask(WORKER_2_OID), root);
+ assertThat(information2.getProgressDescriptionShort())
+ .as("second worker progress label")
+ .isEqualTo("42");
+ }
+
+ private static TaskType createRootTaskWithWorkerOverview() {
+ ActivityStateOverviewType overview = new ActivityStateOverviewType()
+ .bucketProgress(
+ new BucketProgressOverviewType()
+ .completeBuckets(140)
+ .totalBuckets(257))
+ .task(workerOverview(WORKER_1_OID, 2, 1, 2))
+ .task(workerOverview(WORKER_2_OID, 42, 0, 0));
+
+ return new TaskType()
+ .oid(ROOT_OID)
+ .activityState(
+ new TaskActivityStateType()
+ .tree(
+ new ActivityTreeStateType()
+ .activity(overview)));
+ }
+
+ private static ActivityTaskStateOverviewType workerOverview(
+ String oid, int successfullyProcessed, int failed, int skipped) {
+ return new ActivityTaskStateOverviewType()
+ .taskRef(oid, TaskType.COMPLEX_TYPE)
+ .bucketsProcessingRole(BucketsProcessingRoleType.WORKER)
+ .progress(
+ new ItemsProgressOverviewType()
+ .successfullyProcessed(successfullyProcessed)
+ .failed(failed)
+ .skipped(skipped));
+ }
+
+ private static TaskType createWorkerTask(String oid) {
+ ActivityBucketingStateType bucketing = new ActivityBucketingStateType()
+ .bucketsProcessingRole(BucketsProcessingRoleType.WORKER);
+
+ ActivityStateType activity = new ActivityStateType()
+ .realizationState(ActivityRealizationStateType.IN_PROGRESS_LOCAL)
+ .bucketing(bucketing);
+
+ TaskActivityStateType activityState = new TaskActivityStateType()
+ .localRoot(new ActivityPathType())
+ .activity(activity);
+
+ return new TaskType()
+ .oid(oid)
+ .parent(ROOT_OID)
+ .activityState(activityState);
+ }
+}
diff --git a/infra/schema/testng-unit.xml b/infra/schema/testng-unit.xml
index 1d65d3f0f94..502d11a0eca 100644
--- a/infra/schema/testng-unit.xml
+++ b/infra/schema/testng-unit.xml
@@ -123,6 +123,7 @@
+
diff --git a/release-notes.adoc b/release-notes.adoc
index 78482ca3a0f..99f5c3636bc 100644
--- a/release-notes.adoc
+++ b/release-notes.adoc
@@ -104,6 +104,7 @@ Overall, midPoint 4.10 opens up the world of identity management and governance
* Fixed translation of archetype display labels in assignment picker and summary panel. See bug:MID-11177[]
* Fix 10k limit in certification queries using iterative search. See bug:MID-11043[]
* Fixed generic "Fatal error" message in object tables to show available list/search error details. See bug:MID-10911[]
+* Fixed worker progress reporting for distributed bucketed tasks. See bug:MID-11186[]
=== Releases Of Other Components