");
sb.AppendLine("
");
sb.AppendLine($"
Scope: {HtmlEncode(scopeLabel ?? scopeValue)}
");
- sb.AppendLine($"
New/updated activities: {activities.Count} since {since:yyyy-MM-dd HH:mm} UTC
");
+ sb.AppendLine($"
New activities: {activities.Count} since {since:yyyy-MM-dd HH:mm} UTC
");
sb.AppendLine(activityTypes != null && activityTypes.Count > 0
? $"
Activity types: {HtmlEncode(string.Join(", ", activityTypes))}
"
: "
Activity types: All
");
@@ -1157,7 +1155,7 @@ private static string BuildEmailBody(string userName, string scopeType, string s
sb.AppendLine("
");
sb.AppendLine("
");
sb.AppendLine($"{HtmlEncode(typeLabel)}");
- sb.AppendLine($"{GetActivityTimeLabel(item)}");
+ sb.AppendLine($"{item.CreatedOn:yyyy-MM-dd HH:mm} UTC");
sb.AppendLine("
");
sb.AppendLine($"
{HtmlEncode(item.Subject ?? "(No subject)")}
");
sb.AppendLine($"
Regarding: {HtmlEncode(item.Regarding)}
");
@@ -1176,7 +1174,7 @@ private static string BuildEmailBody(string userName, string scopeType, string s
sb.AppendLine("
");
}
- sb.AppendLine("
You only receive entries created or updated since your previous digest for this subscription.
");
+ sb.AppendLine("
You only receive entries newer than your previous digest for this subscription.
");
sb.AppendLine("
");
sb.AppendLine("
");
sb.AppendLine("You are receiving this because you subscribed to activity notifications in Dynamics Activities.
");
@@ -1398,29 +1396,6 @@ private static string GetTypeBadgeLabel(ActivityItem item)
}
}
- private static DateTime GetActivityTimestamp(ActivityItem item)
- {
- if (item == null) return DateTime.MinValue;
- if (item.ModifiedOn > DateTime.MinValue && item.ModifiedOn > item.CreatedOn) return item.ModifiedOn;
- return item.CreatedOn;
- }
-
- private static string GetActivityTimeLabel(ActivityItem item)
- {
- var timestamp = GetActivityTimestamp(item);
- if (timestamp <= DateTime.MinValue)
- {
- return String.Empty;
- }
-
- if (item?.ModifiedOn > DateTime.MinValue && item.ModifiedOn > item.CreatedOn)
- {
- return $"Updated {timestamp:yyyy-MM-dd HH:mm} UTC";
- }
-
- return $"{timestamp:yyyy-MM-dd HH:mm} UTC";
- }
-
private static string GetDynamicsUrl(string entityType, string activityId)
{
var etn = "activitypointer";
@@ -1463,7 +1438,6 @@ private sealed class ActivityItem
public string RegardingId { get; set; }
public string Regarding { get; set; }
public DateTime CreatedOn { get; set; }
- public DateTime ModifiedOn { get; set; }
}
private sealed class ScopeContext
diff --git a/DynamicsActivitiesPackage/DynamicsActivities_Summarize/DynamicsActivities_Summarize.cs b/DynamicsActivitiesPackage/DynamicsActivities_Summarize/DynamicsActivities_Summarize.cs
index 5f20587..b87759a 100644
--- a/DynamicsActivitiesPackage/DynamicsActivities_Summarize/DynamicsActivities_Summarize.cs
+++ b/DynamicsActivitiesPackage/DynamicsActivities_Summarize/DynamicsActivities_Summarize.cs
@@ -145,11 +145,11 @@ private static string BuildPrompt(SummaryRequest request)
foreach (var activity in request.Activities.Take(50))
{
var type = SafeValue(activity.Type);
- var changedOn = SafeValue(GetActivityTimelineTimestampUtc(activity));
+ var createdOn = SafeValue(activity.CreatedOnUtc);
var regarding = SafeValue(activity.Regarding);
var subject = SafeValue(activity.Subject);
var description = TrimValue(SafeValue(activity.Description), 260);
- lines.Add("- [" + changedOn + "] " + type + " | Regarding: " + regarding + " | Subject: " + subject + " | Note: " + description);
+ lines.Add("- [" + createdOn + "] " + type + " | Regarding: " + regarding + " | Subject: " + subject + " | Note: " + description);
}
return String.Join(Environment.NewLine, lines);
@@ -309,8 +309,6 @@ private static SummaryRequest ParseSummaryRequest(string payload)
request.Activities.Add(new ActivityInput
{
CreatedOnUtc = item["createdOnUtc"]?.Value(),
- ModifiedOnUtc = item["modifiedOnUtc"]?.Value(),
- ChangedOnUtc = item["changedOnUtc"]?.Value(),
Type = item["type"]?.Value(),
Subject = item["subject"]?.Value(),
Regarding = item["regarding"]?.Value(),
@@ -318,10 +316,6 @@ private static SummaryRequest ParseSummaryRequest(string payload)
});
}
- request.Activities = request.Activities
- .OrderByDescending(GetActivityTimelineTimestampSortKey)
- .ToList();
-
return request;
}
@@ -382,7 +376,7 @@ private static string BuildFallbackSummary(SummaryRequest request)
var topItems = activities
.Take(3)
- .Select(a => "[" + SafeValue(GetActivityTimelineTimestampUtc(a)) + "] " + SafeValue(a.Type) + ": " + SafeValue(a.Subject))
+ .Select(a => "[" + SafeValue(a.CreatedOnUtc) + "] " + SafeValue(a.Type) + ": " + SafeValue(a.Subject))
.ToList();
var sb = new StringBuilder();
@@ -436,15 +430,15 @@ private static string BuildSummaryHtml(string summary, SummaryRequest request)
sb.Append("'>");
foreach (var activity in topActivities)
{
- var changedOn = SafeValue(GetActivityTimelineTimestampUtc(activity));
- var hasKnownTimestamp = !String.Equals(changedOn, "-", StringComparison.Ordinal);
+ var createdOn = SafeValue(activity.CreatedOnUtc);
+ var hasKnownTimestamp = !String.Equals(createdOn, "-", StringComparison.Ordinal);
sb.Append("");
if (hasKnownTimestamp)
{
sb.Append("[");
- sb.Append(HtmlEncode(changedOn));
+ sb.Append(HtmlEncode(createdOn));
sb.Append("] ");
}
sb.Append(HtmlEncode(SafeValue(activity.Type)));
@@ -574,42 +568,6 @@ private static string TrimValue(string value, int maxLength)
return value.Substring(0, maxLength) + "...";
}
- private static string GetActivityTimelineTimestampUtc(ActivityInput activity)
- {
- if (activity == null)
- {
- return null;
- }
-
- if (!String.IsNullOrWhiteSpace(activity.ChangedOnUtc))
- {
- return activity.ChangedOnUtc;
- }
-
- if (!String.IsNullOrWhiteSpace(activity.ModifiedOnUtc))
- {
- return activity.ModifiedOnUtc;
- }
-
- return activity.CreatedOnUtc;
- }
-
- private static DateTime GetActivityTimelineTimestampSortKey(ActivityInput activity)
- {
- var raw = GetActivityTimelineTimestampUtc(activity);
- if (String.IsNullOrWhiteSpace(raw))
- {
- return DateTime.MinValue;
- }
-
- if (DateTime.TryParseExact(raw, "o", CultureInfo.InvariantCulture, DateTimeStyles.RoundtripKind, out var parsed))
- {
- return parsed;
- }
-
- return DateTime.MinValue;
- }
-
private sealed class SummaryRequest
{
[JsonProperty("scopeLabel")]
@@ -630,12 +588,6 @@ private sealed class ActivityInput
[JsonProperty("createdOnUtc")]
public string CreatedOnUtc { get; set; }
- [JsonProperty("modifiedOnUtc")]
- public string ModifiedOnUtc { get; set; }
-
- [JsonProperty("changedOnUtc")]
- public string ChangedOnUtc { get; set; }
-
[JsonProperty("type")]
public string Type { get; set; }
diff --git a/README.md b/README.md
index 6462636..0329a1f 100644
--- a/README.md
+++ b/README.md
@@ -103,7 +103,7 @@ Subscriptions are fully **DataMiner-native** in the current implementation.
Behavior to be aware of:
- The notify script has **no internal cadence gate**; each run processes subscriptions matching the requested frequency
-- Activity detection includes both new and updated activities: a record is included if `createdon > since` OR `modifiedon > since`
+- New activity detection uses `createdon > LastSentAt`
- Digest emails can include Assistant-generated HTML timeline summaries, with deterministic fallback when Assistant integration is unavailable
- Sender/from behavior is controlled by DataMiner mail / SMTP configuration
@@ -219,7 +219,23 @@ and the frontend build base path is set to `/public/DynamicsActivitiesDev/` so s
| `.github/workflows/deploy-dma-on-pr-merge.yml` | Caller workflow for production-on-merge, manual `production` from `main` only, and manual `dev` deploys |
| `.github/workflows/deploy-dmapp-reusable.yml` | Reusable build/register/deploy workflow for DMAPP packaging and Catalog deployment |
| `.github/workflows/copilot-bug-triage.yml` | Auto-comments on `bug`-labeled issues to ask `@copilot` for investigation |
-| `.github/workflows/sync-main-to-release-candidate.yml` | Opens or updates the `main` → `release-candidate` promotion PR, enables auto-merge, asks `@copilot` to resolve conflicts, and retries when the PR is updated |
+| `.github/workflows/promote-main-to-release-candidate.yml` | Opens or updates the `main` → `release-candidate` promotion PR after changes reach `main`, using a dedicated branch so `@copilot` can resolve conflicts without modifying `main` |
+
+### Release candidate deployment
+
+`release-candidate` is the integration branch for all changes that create, update, or delete Dataverse data. Every push to it deploys the app to:
+
+```text
+https://solutionsdma-skyline.on.dataminer.services/auth/?url=%2Fpublic%2FDynamicsActivitiesRC%2Findex.html
+```
+
+Open write-capable PRs with `release-candidate` selected as their base branch. When the candidate is approved, open a promotion PR from `release-candidate` into `main`; production remains deployed only when that PR merges.
+
+The release-candidate deployment uses the repository variable `VITE_REDIRECT_URI_RC`, which must match the redirect URI registered on the deployed Entra application:
+
+```text
+https://solutionsdma-skyline.on.dataminer.services/public/DynamicsActivitiesRC/
+```
### Deployment URLs